Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/product/output-conventions.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ command:
- in human mode, the child inherits stdin, stdout, and stderr
- in JSON mode, stdin remains inherited while the child's stdout and stderr
are routed to the CLI's diagnostic stderr stream
- when the command transforms the child's output (`transformOutput`), stdin remains inherited and the engine reads the child's stdout and stderr, passes each through the command's transform, and writes the result to the same place the two cases above would have: the child's stdout to stdout and its stderr to stderr in human and markdown format, and both to stderr in JSON mode. The engine also replaces the credential it injected into the child's environment with `[redacted]`; without `transformOutput` it does not.
- JSON stdout remains an NDJSON event stream with exactly one terminal result
frame
- a non-zero child status is preserved as the process exit code and is
Expand Down
34 changes: 34 additions & 0 deletions packages/cli-engine/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,4 +75,38 @@ A statement is the command's own declared input, unlike a `--confirm` value, whi

Each flag answers one question. A run that succeeds with a flag nothing consumed fails with `CLI.CONSENT_UNUSED`, so a mistyped subject or a second answer to one question is never ignored. The same holds for a `--confirm` token no consent asked for, on a command that declares statements (other commands still ignore a leftover `--confirm`; see [#339](https://github.com/prisma/prisma-cli/issues/339)). A refusal also names any given statement flag that matches none of its questions, and any `--confirm` token, so the user hears of it before answering a prompt. That check runs when the handler returns, after the command has acted. A command that asks everything in one batch passes `statements(questions, { last: true })` to get the same failure right after the questions are answered, before it does anything with them.

## Transforming a child's output

A command that declares `maySpawn` runs a child process with `ctx.spawn`. By default the child writes straight to the terminal, or, with `--json`, to stderr. To change the text before it is written, for example to redact secrets, pass `transformOutput`:

```ts
if (!ctx.spawnOptionNames?.includes("transformOutput")) {
throw new Error("This command needs @prisma/cli-engine 0.10.0 or later.");
}
const result = await ctx.spawn({
command: "alchemy",
args: ["deploy"],
transformOutput: (stream) => redactor(stream),
});
```

The engine calls `transformOutput` once for `"stdout"` and once for `"stderr"` before the child starts. It pipes each stream, decoded as UTF-8, through the `TransformStream` you return, and writes what comes out where the child's output would have gone without it:

- human and markdown format: the child's stdout to stdout, its stderr to stderr;
- json format: both to stderr, so stdout carries only the engine's NDJSON output.

Each stream keeps its own order; there is no order between the two. The engine reads the child only as fast as the terminal takes the text, so a slow reader slows the child down instead of filling memory. When a stream ends, the transform's `flush` runs and its output is written before `ctx.spawn` settles, including when the child was aborted or failed. A transform can therefore hold text back between chunks, as a redactor that matches across chunk boundaries must. Hold back only what could still become a secret, the longest end of the text that is a start of one, and not a fixed number of characters: a child that prints a prompt with no newline and waits for input must show the prompt at once. The engine never splits a UTF-16 surrogate pair across two writes, so a transform that cuts text by length does not corrupt an emoji.

When the command declares `needs: { credentials: "child" }`, the engine puts the access token in the child's `PRISMA_SERVICE_TOKEN`. The command never sees that value, so when the command passes `transformOutput`, the engine replaces the token with `[redacted]` itself, before your transform runs. Without `transformOutput` the engine does not read the child's output and does not redact the token, in any format, json included.

The engine still owns the child. Stdin stays inherited, Ctrl-C reaches the child as before, and a SIGTERM or an abort of `ctx.signal` ends it with SIGTERM, then SIGKILL after a grace period. If a transform throws or errors, or the destination fails, the engine stops reading that stream, ends the child the same way, and `ctx.spawn` rejects with that error once the child has exited. With `PRISMA_DEBUG=1`, a child output stream whose read fails, or that a grandchild keeps open with no output after the child exits, leaves a line saying its later output was dropped. A transform or destination failure leaves no such line, because `ctx.spawn` rejects with its error. A slow reader does not cause this: the grace period counts only time the engine spends waiting for output that does not come.

### Checking for support

Hosts older than 0.10.0 ignore spawn options they do not know, so on those hosts a child would run with its output untransformed. `ctx.spawnOptionNames` lists the option names `ctx.spawn` accepts; it is absent on those hosts, and its type is optional so the compiler makes you check it. Check it before spawning, as above. From 0.10.0 on, `ctx.spawn` refuses any option name it does not know, so a future option cannot be ignored silently.

### Spawn adapters

A spawn adapter supports `transformOutput` by starting the child with stdout and stderr piped when the request's `output` is `"engine"`, and returning both as `stdout` and `stderr` on the child. If the adapter returns no streams, the engine kills the child with SIGKILL at once and `ctx.spawn` rejects. An adapter written before `"engine"` existed has usually started that child with inherited output by then, so some of it can reach the terminal before the kill. This check catches a wrongly wired adapter; it does not make redaction safe with one.

Part of [prisma/prisma-cli](https://github.com/prisma/prisma-cli).
2 changes: 1 addition & 1 deletion packages/cli-engine/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@prisma/cli-engine",
"version": "0.9.0",
"version": "0.10.0",
"description": "The execution engine of the unified Prisma CLI.",
"type": "module",
"exports": {
Expand Down
16 changes: 13 additions & 3 deletions packages/cli-engine/src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,11 @@ export interface CommandContext<
readonly api: ManagementApiClient;

/**
* Hands the terminal to a child process and resolves when it ends.
* The child inherits stdio and runs in this process's own group
* (POSIX) or console (Windows), so Ctrl-C reaches it natively.
* Runs a child process and resolves when it ends. The child inherits
* stdin and runs in this process's own group (POSIX) or console
* (Windows), so Ctrl-C reaches it natively. Its stdout and stderr go
* where the output format sends them (see output-conventions.md),
* passing through `transformOutput` when the handler gives one.
*
* While a child is live the engine neither aborts nor exits on a
* delivered signal: it records signals and replays them into its
Expand All @@ -85,6 +87,14 @@ export interface CommandContext<
*/
readonly spawn: (options: SpawnOptions) => Promise<ChildResult>;

/**
* The option names ctx.spawn accepts on this host. ctx.spawn refuses
* any other name. Absent on hosts older than 0.10.0, which ignore
* options they do not know, so a command that depends on an option
* checks for it here before spawning.
*/
readonly spawnOptionNames?: readonly (keyof SpawnOptions)[];

/**
* How the run's most recent completed child ended, or undefined when
* none has run. The engine records every child ctx.spawn returns, so
Expand Down
2 changes: 2 additions & 0 deletions packages/cli-engine/src/execution/command-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
} from "../presentation";
import { type Diagnostic, notOk, okVoid } from "../protocol";
import type { LoadedConfigFile, OutputStream } from "../runtime";
import { SPAWN_OPTION_NAMES } from "../spawn";
import { buildManagementApiClient } from "./api-client";
import { constructionError } from "./command-tree";
import type { Invocation, RunState } from "./engine";
Expand Down Expand Up @@ -178,6 +179,7 @@ export function makeContext(
return api;
},
spawn: makeSpawn(invocation, def),
spawnOptionNames: SPAWN_OPTION_NAMES,
lastChild: () => state.lastChild,
report: (event) => reportEvent(invocation, event),
prompt: makePromptSurface(invocation),
Expand Down
Loading
Loading