diff --git a/.drive/projects/prisma-cli-v8/deferred.md b/.drive/projects/prisma-cli-v8/deferred.md index fed5bb82..b4a922a5 100644 --- a/.drive/projects/prisma-cli-v8/deferred.md +++ b/.drive/projects/prisma-cli-v8/deferred.md @@ -364,6 +364,14 @@ CLI does not do, and each restarts as engine work if wanted: build` from the engine's `test` script and let turbo's `^build` dependency do that work, or stop the engine's build cleaning a directory another package reads while it runs. + Seen again on the presentations branch (2026-08-12) with a second + message for the same cause — `Cannot find package + '@prisma/cli-engine/testing'` — and a failure count that varied 13, + 34 and 43 files across three runs of one commit, while + `--concurrency=1` and a direct `npx vitest run` in `packages/cli` + both passed all 60 every time. The varying count is the tell: a + change that touches many files shifts the timing and makes it fire + more often, which reads as "this branch broke everything". - **The packed shell manifest carries `devDependencies` on private packages at versions no registry has** — `@repo/cli-telemetry` and `@repo/tsconfig`, both at `8.0.0-rc.1`. Harmless when a consumer diff --git a/packages/cli-engine/src/execution/command-context.ts b/packages/cli-engine/src/execution/command-context.ts index 44c53d63..1062b8d9 100644 --- a/packages/cli-engine/src/execution/command-context.ts +++ b/packages/cli-engine/src/execution/command-context.ts @@ -53,7 +53,26 @@ export function makeUi(colorEnabled: boolean, stderr: OutputStream): Ui { } /** Materializes ONLY the active format's presentation functions, at the - * return site: human → human + stdout + next; json → json + next. */ + * return site: human → human + stdout + next; json → json + next. + * + * `stdout` and `next` may be absent at runtime, so both are called with + * `?.()`. `Presentations` requires all four, so no command compiled + * against this engine can omit one — but `@prisma/orm-toolchain` is + * built against engine `0.0.9`, where three of the four were optional, + * and its published commands took that up: `migration list` declares + * `human` and `json` and neither of the others. Calling them + * unconditionally makes it exit 2 — `stdout` in human mode, `next` in + * both. + * + * `json` is called unconditionally, and stays that way: a missing json + * presentation is the defect this change removes, and every ORM command + * already declares one. + * + * This is version skew in our own code, not a foreign contract. The fix + * is in prisma/prisma: declare the missing presentations in the ORM + * commands and build orm-toolchain against this engine, where the type + * refuses to compile without them. Delete both `?.()` when that + * version is pinned here. */ function materializePresentation( state: RunState, ui: Ui, @@ -63,7 +82,7 @@ function materializePresentation( return { human: [], stdout: [], - json: presentations.json?.(), + json: presentations.json(), next: presentations.next?.() ?? [], }; } diff --git a/packages/cli-engine/src/execution/settlement.ts b/packages/cli-engine/src/execution/settlement.ts index e5bfb619..e654d6d1 100644 --- a/packages/cli-engine/src/execution/settlement.ts +++ b/packages/cli-engine/src/execution/settlement.ts @@ -71,10 +71,7 @@ export function settleCompleted( const envelope: CompletedEnvelope = { ok: true, commandId: state.commandId, - result: - presented.presentation.json === undefined - ? presented.data - : presented.presentation.json, + result: presented.presentation.json, exitCode, diagnostics: presented.diagnostics.map((diagnostic) => withDocsUrl(state, diagnostic), diff --git a/packages/cli-engine/src/presentation.ts b/packages/cli-engine/src/presentation.ts index 5570ed58..8c563d94 100644 --- a/packages/cli-engine/src/presentation.ts +++ b/packages/cli-engine/src/presentation.ts @@ -40,9 +40,8 @@ export interface PresentedResult { readonly diagnostics: readonly Diagnostic[]; /** * Only the active format's presentation is materialized; the other - * format's fields are normalized to empty. `json` stays undefined - * when the handler supplied no json presentation — the envelope's - * `result` then falls back to `data`. + * format's fields are normalized to empty. In human mode `json` is + * undefined because the json presentation was never invoked. */ readonly presentation: { readonly human: readonly Block[]; @@ -54,16 +53,19 @@ export interface PresentedResult { /** * The per-format presentation functions a handler supplies to - * ctx.present. Only the active format's functions are invoked, at the - * return site. `human` composes engine primitives, rendered to stderr; - * `stdout` is the machine-consumable data lines — what a pipe - * receives, the human mode's only stdout writes. + * ctx.present. Every one is required: a command states each output + * surface it publishes rather than inheriting one by omission. Only the + * active format's functions are invoked, at the return site. `human` + * composes engine primitives, rendered to stderr; `stdout` is the + * machine-consumable data lines — what a pipe receives, the human + * mode's only stdout writes; `json` is the `--json` envelope's + * `result`; `next` is the suggested follow-up actions. */ export interface Presentations { readonly human: (ui: Ui) => readonly Block[]; - readonly stdout?: () => readonly string[]; - readonly json?: () => unknown; - readonly next?: () => readonly NextAction[]; + readonly stdout: () => readonly string[]; + readonly json: () => unknown; + readonly next: () => readonly NextAction[]; } /** diff --git a/packages/cli-engine/src/telemetry/commands.ts b/packages/cli-engine/src/telemetry/commands.ts index 8ddf7629..ae433402 100644 --- a/packages/cli-engine/src/telemetry/commands.ts +++ b/packages/cli-engine/src/telemetry/commands.ts @@ -99,6 +99,7 @@ function statusPresentations(status: TelemetryStatus): Presentations { ], stdout: () => [...formatTelemetryStatusLines(status)], json: () => status, + next: () => [], }; } @@ -109,6 +110,7 @@ function consentPresentations(line: string, json: unknown): Presentations { human: () => [{ kind: "summary", status: "ok", text: line }], stdout: () => [line], json: () => json, + next: () => [], }; } diff --git a/packages/cli-engine/tests/blocks.test.ts b/packages/cli-engine/tests/blocks.test.ts index ae249302..edf64d42 100644 --- a/packages/cli-engine/tests/blocks.test.ts +++ b/packages/cli-engine/tests/blocks.test.ts @@ -25,7 +25,17 @@ async function render( const show = defineCommand({ help: { summary: "Render the fixture blocks" }, handler: async (_args, ctx) => - ok(ctx.present({ data: null }, { human: () => blocks })), + ok( + ctx.present( + { data: null }, + { + human: () => blocks, + stdout: () => [], + json: () => null, + next: () => [], + }, + ), + ), }); const result = await createTestCli({ commands: { show } }).run( [ @@ -323,6 +333,9 @@ describe("one character per meaning", () => { human: () => [ { kind: "summary", status: "error", text: "Failed." }, ], + stdout: () => [], + json: () => null, + next: () => [], }, ), ); @@ -371,6 +384,9 @@ describe("one character per meaning", () => { { kind: "summary", status: "warn", text: "Slowly." }, { kind: "summary", status: "info", text: "Noted." }, ], + stdout: () => [], + json: () => null, + next: () => [], }, ), ); diff --git a/packages/cli-engine/tests/clack-isolation.test.ts b/packages/cli-engine/tests/clack-isolation.test.ts index c90d4790..9eb40e7b 100644 --- a/packages/cli-engine/tests/clack-isolation.test.ts +++ b/packages/cli-engine/tests/clack-isolation.test.ts @@ -37,6 +37,9 @@ function promptCommand(run: (prompt: PromptSurface) => Promise) { human: (): readonly Block[] => [ { kind: "summary", status: "ok", text: `answer=${answer}` }, ], + stdout: () => [], + json: () => ({ answer }), + next: () => [], }, ), ); diff --git a/packages/cli-engine/tests/clack-prompts.test.ts b/packages/cli-engine/tests/clack-prompts.test.ts index 70717d4f..dfca2c26 100644 --- a/packages/cli-engine/tests/clack-prompts.test.ts +++ b/packages/cli-engine/tests/clack-prompts.test.ts @@ -64,6 +64,9 @@ function promptCli(run: (prompt: PromptSurface) => Promise) { text: `answer=${JSON.stringify(answer)}`, }, ], + stdout: () => [], + json: () => ({ answer }), + next: () => [], }, ), ); diff --git a/packages/cli-engine/tests/command-capabilities.test-d.ts b/packages/cli-engine/tests/command-capabilities.test-d.ts index 5957dbd9..35c04226 100644 --- a/packages/cli-engine/tests/command-capabilities.test-d.ts +++ b/packages/cli-engine/tests/command-capabilities.test-d.ts @@ -36,7 +36,17 @@ test("installsPackages alone puts ctx.packages on the context and nothing else", expectTypeOf(ctx.packages).toEqualTypeOf(); // @ts-expect-error managesCredentials was not declared void ctx.credentialManager; - return ok(ctx.present({ data: null }, { human: () => [] })); + return ok( + ctx.present( + { data: null }, + { + human: () => [], + stdout: () => [], + json: () => null, + next: () => [], + }, + ), + ); }, }); @@ -51,7 +61,12 @@ test("installsPackages alone puts ctx.packages on the context and nothing else", expectTypeOf(ctx.packages).toEqualTypeOf(); // @ts-expect-error managesCredentials was not declared void ctx.credentialManager; - return ok(ctx.present({ data: null }, { human: () => [] })); + return ok( + ctx.present( + { data: null }, + { human: () => [], stdout: () => [], json: () => null, next: () => [] }, + ), + ); }; expectTypeOf(annotated).toEqualTypeOf(); }); @@ -64,7 +79,17 @@ test("managesCredentials alone puts ctx.credentialManager on the context and not expectTypeOf(ctx.credentialManager).toEqualTypeOf(); // @ts-expect-error installsPackages was not declared void ctx.packages; - return ok(ctx.present({ data: null }, { human: () => [] })); + return ok( + ctx.present( + { data: null }, + { + human: () => [], + stdout: () => [], + json: () => null, + next: () => [], + }, + ), + ); }, }); @@ -79,7 +104,12 @@ test("managesCredentials alone puts ctx.credentialManager on the context and not expectTypeOf(ctx.credentialManager).toEqualTypeOf(); // @ts-expect-error installsPackages was not declared void ctx.packages; - return ok(ctx.present({ data: null }, { human: () => [] })); + return ok( + ctx.present( + { data: null }, + { human: () => [], stdout: () => [], json: () => null, next: () => [] }, + ), + ); }; expectTypeOf(annotated).toEqualTypeOf(); }); @@ -92,7 +122,17 @@ test("both capabilities declared: both surfaces, and the shared context intact", handler: async (_args, ctx) => { expectTypeOf(ctx.credentialManager).toEqualTypeOf(); expectTypeOf(ctx.packages).toEqualTypeOf(); - return ok(ctx.present({ data: null }, { human: () => [] })); + return ok( + ctx.present( + { data: null }, + { + human: () => [], + stdout: () => [], + json: () => null, + next: () => [], + }, + ), + ); }, }); @@ -110,7 +150,12 @@ test("both capabilities declared: both surfaces, and the shared context intact", const annotated: CommandHandler = async (_args, ctx) => { expectTypeOf(ctx.credentialManager).toEqualTypeOf(); expectTypeOf(ctx.packages).toEqualTypeOf(); - return ok(ctx.present({ data: null }, { human: () => [] })); + return ok( + ctx.present( + { data: null }, + { human: () => [], stdout: () => [], json: () => null, next: () => [] }, + ), + ); }; expectTypeOf(annotated).toEqualTypeOf(); }); @@ -123,7 +168,17 @@ test("neither capability declared: neither surface", () => { void ctx.packages; // @ts-expect-error managesCredentials was not declared void ctx.credentialManager; - return ok(ctx.present({ data: null }, { human: () => [] })); + return ok( + ctx.present( + { data: null }, + { + human: () => [], + stdout: () => [], + json: () => null, + next: () => [], + }, + ), + ); }, }); @@ -137,7 +192,12 @@ test("neither capability declared: neither surface", () => { void ctx.packages; // @ts-expect-error managesCredentials was not declared void ctx.credentialManager; - return ok(ctx.present({ data: null }, { human: () => [] })); + return ok( + ctx.present( + { data: null }, + { human: () => [], stdout: () => [], json: () => null, next: () => [] }, + ), + ); }; expectTypeOf(annotated).toEqualTypeOf(); }); @@ -177,7 +237,17 @@ test("a flag that is not a literal infers boolean and loses its surface", () => handler: async (_args, ctx) => { // @ts-expect-error the flag is not the literal true void ctx.packages; - return ok(ctx.present({ data: null }, { human: () => [] })); + return ok( + ctx.present( + { data: null }, + { + human: () => [], + stdout: () => [], + json: () => null, + next: () => [], + }, + ), + ); }, }); @@ -205,7 +275,17 @@ test("an explicit type argument can claim a capability the declaration omits", ( help: { summary: "Claims installsPackages without declaring it" }, handler: async (_args, ctx) => { expectTypeOf(ctx.packages).toEqualTypeOf(); - return ok(ctx.present({ data: null }, { human: () => [] })); + return ok( + ctx.present( + { data: null }, + { + human: () => [], + stdout: () => [], + json: () => null, + next: () => [], + }, + ), + ); }, }); @@ -227,9 +307,17 @@ test("the capability generics leave the exit-code catalogue alone", () => { const annotated: CommandHandler = async (_args, ctx) => { expectTypeOf(ctx.credentialManager).toEqualTypeOf(); expectTypeOf(ctx.packages).toEqualTypeOf(); - // @ts-expect-error 7 is outside the command's catalogue - ctx.present({ data: null, exitCode: 7 }, { human: () => [] }); - return ok(ctx.present({ data: null, exitCode: 4 }, { human: () => [] })); + ctx.present( + // @ts-expect-error 7 is outside the command's catalogue + { data: null, exitCode: 7 }, + { human: () => [], stdout: () => [], json: () => null, next: () => [] }, + ); + return ok( + ctx.present( + { data: null, exitCode: 4 }, + { human: () => [], stdout: () => [], json: () => null, next: () => [] }, + ), + ); }; expectTypeOf(annotated).toEqualTypeOf(); }); @@ -243,7 +331,17 @@ test("maySpawn sits beside the capability flags without widening them", () => { handler: async (_args, ctx) => { expectTypeOf(ctx.credentialManager).toEqualTypeOf(); expectTypeOf(ctx.packages).toEqualTypeOf(); - return ok(ctx.present({ data: null }, { human: () => [] })); + return ok( + ctx.present( + { data: null }, + { + human: () => [], + stdout: () => [], + json: () => null, + next: () => [], + }, + ), + ); }, }); diff --git a/packages/cli-engine/tests/command-families.test.ts b/packages/cli-engine/tests/command-families.test.ts index 3829f2f8..05c20cf3 100644 --- a/packages/cli-engine/tests/command-families.test.ts +++ b/packages/cli-engine/tests/command-families.test.ts @@ -29,7 +29,17 @@ function configCommand(section: ConfigSection) { help: { summary: "Reads a config section" }, needs: { config: section }, handler: async (_args, ctx) => - ok(ctx.present({ data: ctx.config }, { human: () => [] })), + ok( + ctx.present( + { data: ctx.config }, + { + human: () => [], + stdout: () => [], + json: () => ctx.config, + next: () => [], + }, + ), + ), }); } @@ -159,7 +169,12 @@ describe("docs-URL derivation", () => { }, ], }, - { human: () => [] }, + { + human: () => [], + stdout: () => [], + json: () => null, + next: () => [], + }, ), ), }); diff --git a/packages/cli-engine/tests/config.test.ts b/packages/cli-engine/tests/config.test.ts index 43e373e8..02855d7c 100644 --- a/packages/cli-engine/tests/config.test.ts +++ b/packages/cli-engine/tests/config.test.ts @@ -701,6 +701,9 @@ function showCommand( human: () => [ { kind: "summary", status: "ok", text: ctx.config.greeting }, ], + stdout: () => [], + json: () => ctx.config, + next: () => [], }, ), ); @@ -878,7 +881,17 @@ describe("needs.config", { timeout: 60_000 }, () => { const plain = defineCommand({ help: { summary: "No needs at all" }, handler: async (_args, ctx) => - ok(ctx.present({ data: null }, { human: () => [] })), + ok( + ctx.present( + { data: null }, + { + human: () => [], + stdout: () => [], + json: () => null, + next: () => [], + }, + ), + ), }); const cli = createCli({ name: "t", @@ -1037,7 +1050,17 @@ describe("--config on the command line", { timeout: 60_000 }, () => { help: { summary: "Echoes a message" }, args: { flags: { message: flag.string({ brief: "message" }) } }, handler: async (args, ctx) => - ok(ctx.present({ data: args.flags.message }, { human: () => [] })), + ok( + ctx.present( + { data: args.flags.message }, + { + human: () => [], + stdout: () => [], + json: () => args.flags.message, + next: () => [], + }, + ), + ), }); const cli = createTestCli({ commands: { echo } }); const run = await cli.run(["echo", "--message", "--config="]); @@ -1061,7 +1084,17 @@ describe("--config on the command line", { timeout: 60_000 }, () => { }, }, handler: async (args, ctx) => - ok(ctx.present({ data: args.positionals.rest }, { human: () => [] })), + ok( + ctx.present( + { data: args.positionals.rest }, + { + human: () => [], + stdout: () => [], + json: () => args.positionals.rest, + next: () => [], + }, + ), + ), }); const cli = createTestCli({ commands: { passthrough } }); const run = await cli.run(["passthrough", "--", "--config="]); diff --git a/packages/cli-engine/tests/credential-manager.test.ts b/packages/cli-engine/tests/credential-manager.test.ts index 661086ee..9d05c646 100644 --- a/packages/cli-engine/tests/credential-manager.test.ts +++ b/packages/cli-engine/tests/credential-manager.test.ts @@ -71,7 +71,17 @@ const credentialReader = () => { help: { summary: "Reads the active credential" }, handler: async (_args, ctx) => { seen = await ctx.activeCredential(); - return ok(ctx.present({ data: seen }, { human: () => [] })); + return ok( + ctx.present( + { data: seen }, + { + human: () => [], + stdout: () => [], + json: () => seen, + next: () => [], + }, + ), + ); }, }); return { command, seen: () => seen }; @@ -197,7 +207,17 @@ describe("the managesCredentials capability", () => { managesCredentials: true, handler: async (_args, ctx) => { sameInstance = ctx.credentialManager === cli.credentialManager; - return ok(ctx.present({ data: null }, { human: () => [] })); + return ok( + ctx.present( + { data: null }, + { + human: () => [], + stdout: () => [], + json: () => null, + next: () => [], + }, + ), + ); }, }); const cli = createTestCli({ commands: { toy } }); @@ -212,7 +232,17 @@ describe("the managesCredentials capability", () => { help: { summary: "Does not manage credentials" }, handler: async (_args, ctx) => { present = "credentialManager" in ctx; - return ok(ctx.present({ data: null }, { human: () => [] })); + return ok( + ctx.present( + { data: null }, + { + human: () => [], + stdout: () => [], + json: () => null, + next: () => [], + }, + ), + ); }, }); const cli = createTestCli({ commands: { toy } }); @@ -226,7 +256,12 @@ const needsCredentials = defineCommand({ help: { summary: "Needs credentials" }, needs: { credentials: true }, handler: async (_args, ctx) => - ok(ctx.present({ data: null }, { human: () => [] })), + ok( + ctx.present( + { data: null }, + { human: () => [], stdout: () => [], json: () => null, next: () => [] }, + ), + ), }); describe("the manager-backed needs check", () => { @@ -282,7 +317,17 @@ describe("the manager-backed needs check", () => { } catch (cause) { return notOk(cause as CliStructuredError); } - return ok(ctx.present({ data: null }, { human: () => [] })); + return ok( + ctx.present( + { data: null }, + { + human: () => [], + stdout: () => [], + json: () => null, + next: () => [], + }, + ), + ); }, }); @@ -630,7 +675,15 @@ describe("token material never leaves", () => { const stored = await ctx.credentialManager.sessions(); const active = await ctx.activeCredential(); return ok( - ctx.present({ data: { stored, active } }, { human: () => [] }), + ctx.present( + { data: { stored, active } }, + { + human: () => [], + stdout: () => [], + json: () => ({ stored, active }), + next: () => [], + }, + ), ); }, }); diff --git a/packages/cli-engine/tests/engine.type-test.ts b/packages/cli-engine/tests/engine.type-test.ts index b5d84e0a..86d31a60 100644 --- a/packages/cli-engine/tests/engine.type-test.ts +++ b/packages/cli-engine/tests/engine.type-test.ts @@ -181,7 +181,12 @@ const diagnostic: Diagnostic = { nextActions: [], }; -const presentations: Presentations = { human: () => [] }; +const presentations: Presentations = { + human: () => [], + stdout: () => [], + json: () => null, + next: () => [], +}; export const runCheck: CommandHandler = async ( args, @@ -536,7 +541,12 @@ export const managedCommand = defineCommand({ const active: ActiveCredential | null = await ctx.activeCredential(); void manager; void active; - return ok(ctx.present({ data: null }, { human: () => [] })); + return ok( + ctx.present( + { data: null }, + { human: () => [], stdout: () => [], json: () => null, next: () => [] }, + ), + ); }, }); export const managedIsDeclared: true = managedCommand.managesCredentials; @@ -548,7 +558,12 @@ export const unmanagedCommand = defineCommand({ void active; // @ts-expect-error the capability was not declared, so the context carries no credentialManager void ctx.credentialManager; - return ok(ctx.present({ data: null }, { human: () => [] })); + return ok( + ctx.present( + { data: null }, + { human: () => [], stdout: () => [], json: () => null, next: () => [] }, + ), + ); }, }); export const unmanagedIsUndeclared: false = unmanagedCommand.managesCredentials; diff --git a/packages/cli-engine/tests/events.test.ts b/packages/cli-engine/tests/events.test.ts index 5682813d..527d14ff 100644 --- a/packages/cli-engine/tests/events.test.ts +++ b/packages/cli-engine/tests/events.test.ts @@ -72,6 +72,7 @@ const noisy = defineCommand({ { kind: "summary", status: "ok", text: "done" }, ], stdout: () => ["done"], + json: () => ({ done: true }), next: () => [{ kind: "done", label: "Nothing else" }], }, ), @@ -114,6 +115,8 @@ describe("human rendering", () => { { data: null }, { human: () => [], + stdout: () => [], + json: () => null, next: () => [ { kind: "open-url", @@ -150,6 +153,8 @@ describe("human rendering", () => { { data: null }, { human: () => [], + stdout: () => [], + json: () => null, next: () => [ { kind: "run-command", diff --git a/packages/cli-engine/tests/execution.test.ts b/packages/cli-engine/tests/execution.test.ts index 1e5927f5..cbffe4c4 100644 --- a/packages/cli-engine/tests/execution.test.ts +++ b/packages/cli-engine/tests/execution.test.ts @@ -105,6 +105,9 @@ const check = defineCommand({ }, { human: () => [{ kind: "summary", status: "warn", text: "1 finding" }], + stdout: () => [], + json: () => ({ findings: 1 }), + next: () => [], }, ), ), @@ -117,6 +120,34 @@ const throwing = defineCommand({ }, }); +/** Its `data` and its json presentation deliberately differ. Every other + * fixture here passes the same value to both, so none of them can tell + * which one the envelope published — `data` is what the handler worked + * with, `result` is what the command chose to publish, and the engine + * used to publish `data` for any command that declared no json. + * + * What this catches: settlement publishing `data` rather than the json + * presentation. What it does not catch: the old conditional fallback + * returning, because this command declares a json presentation, so the + * fallback's branch is never taken. Nothing compiled against this + * engine can omit `json`, so that branch is unreachable rather than + * untested. */ +const divergent = defineCommand({ + help: { summary: "Publishes something other than its working data" }, + handler: async (_args, ctx) => + ok( + ctx.present( + { data: { internal: "the handler's own object" } }, + { + human: () => [], + stdout: () => [], + json: () => ({ published: "what the command states" }), + next: () => [], + }, + ), + ), +}); + const whoami = defineCommand({ help: { summary: "Show the signed-in user" }, needs: { credentials: true }, @@ -133,6 +164,9 @@ const whoami = defineCommand({ text: `Signed in (${active?.workspaceId})`, }, ], + stdout: () => [], + json: () => ({ workspaceId: active?.workspaceId }), + next: () => [], }, ), ); @@ -146,6 +180,7 @@ function makeCli() { failing, check, throwing, + divergent, "tool greet": greet, "auth whoami": whoami, }, @@ -198,6 +233,15 @@ describe("completed commands", () => { ]); }); + test("the envelope's result is the json presentation, never the handler's data", async () => { + const result = await makeCli().run(["divergent", "--json"]); + + const last = result.json[result.json.length - 1]; + expect( + last.kind === "result" && last.envelope.ok && last.envelope.result, + ).toEqual({ published: "what the command states" }); + }); + test("commandId is the full dotted mount path", async () => { const result = await makeCli().run(["tool", "greet", "world", "--json"]); @@ -517,7 +561,17 @@ describe("needs preconditions", () => { }), }, handler: async (_args, ctx) => - ok(ctx.present({ data: null }, { human: () => [] })), + ok( + ctx.present( + { data: null }, + { + human: () => [], + stdout: () => [], + json: () => null, + next: () => [], + }, + ), + ), }); } @@ -626,7 +680,12 @@ describe("undocumented completion exit codes", () => { ok( ctx.present( { data: null, exitCode: 7 } as unknown as { data: null }, - { human: () => [] }, + { + human: () => [], + stdout: () => [], + json: () => null, + next: () => [], + }, ), ), }); @@ -647,7 +706,17 @@ describe("undocumented completion exit codes", () => { help: { summary: "Documents 4 but returns 5" }, exitCodes: { 4: "findings" }, handler: async (_args, ctx) => - ok(ctx.present({ data: null, exitCode: 5 as 4 }, { human: () => [] })), + ok( + ctx.present( + { data: null, exitCode: 5 as 4 }, + { + human: () => [], + stdout: () => [], + json: () => null, + next: () => [], + }, + ), + ), }); const cli = createTestCli({ commands: { rogue }, now: EPOCH }); const result = await cli.run(["rogue", "--json"]); @@ -683,6 +752,9 @@ describe("sensitive field rows", () => { ], }, ], + stdout: () => [], + json: () => ({ token: "tok_secret" }), + next: () => [], }, ), ), @@ -715,7 +787,17 @@ describe("report() after the handler resolved", () => { help: { summary: "Leaks its report function" }, handler: async (_args, ctx) => { smuggled = ctx.report; - return ok(ctx.present({ data: null }, { human: () => [] })); + return ok( + ctx.present( + { data: null }, + { + human: () => [], + stdout: () => [], + json: () => null, + next: () => [], + }, + ), + ); }, }); const cli = createCli({ @@ -776,7 +858,17 @@ describe("credentials that cannot be read", () => { help: { summary: "Needs credentials" }, needs: { credentials: true }, handler: async (_args, ctx) => - ok(ctx.present({ data: null }, { human: () => [] })), + ok( + ctx.present( + { data: null }, + { + human: () => [], + stdout: () => [], + json: () => null, + next: () => [], + }, + ), + ), }); const cli = createCli({ name: "t", @@ -887,7 +979,17 @@ describe("parse and route failures", () => { }, }, handler: async (_args, ctx) => - ok(ctx.present({ data: null }, { human: () => [] })), + ok( + ctx.present( + { data: null }, + { + human: () => [], + stdout: () => [], + json: () => null, + next: () => [], + }, + ), + ), }); const cli = createTestCli({ commands: { strict }, now: EPOCH }); const result = await cli.run(["strict", "--mode", "z", "--count", "q"], { @@ -910,7 +1012,17 @@ describe("parse and route failures", () => { flags: { count: flag.number({ brief: "how many", placeholder: "n" }) }, }, handler: async (_args, ctx) => - ok(ctx.present({ data: null }, { human: () => [] })), + ok( + ctx.present( + { data: null }, + { + human: () => [], + stdout: () => [], + json: () => null, + next: () => [], + }, + ), + ), }); const cli = createTestCli({ commands: { counting }, now: EPOCH }); const result = await cli.run(["counting", "--count", ""], { @@ -954,7 +1066,12 @@ describe("parse and route failures", () => { ok( ctx.present( { data: { bang: args.flags.withBang } }, - { human: () => [] }, + { + human: () => [], + stdout: () => [], + json: () => ({ bang: args.flags.withBang }), + next: () => [], + }, ), ), }); @@ -970,7 +1087,17 @@ describe("ctx.host", () => { const reporting = defineCommand({ help: { summary: "Report the host" }, handler: async (_args, ctx) => - ok(ctx.present({ data: ctx.host }, { human: () => [] })), + ok( + ctx.present( + { data: ctx.host }, + { + human: () => [], + stdout: () => [], + json: () => ctx.host, + next: () => [], + }, + ), + ), }); test("a command reads the runtime, platform and arch from the context", async () => { @@ -1016,6 +1143,9 @@ describe("flag.optionalBoolean", () => { { data: { link: args.flags.link ?? null } }, { human: () => [], + stdout: () => [], + json: () => ({ link: args.flags.link ?? null }), + next: () => [], }, ), ), @@ -1062,7 +1192,17 @@ describe("help examples", () => { examples: ["greet world --loud", "{bin} greet world | cat"], }, handler: async (_args, ctx) => - ok(ctx.present({ data: null }, { human: () => [] })), + ok( + ctx.present( + { data: null }, + { + human: () => [], + stdout: () => [], + json: () => null, + next: () => [], + }, + ), + ), }); const cli = createTestCli({ commands: { greet: exemplified }, now: EPOCH }); const result = await cli.run(["greet", "--help"], { diff --git a/packages/cli-engine/tests/interaction-affordances.test.ts b/packages/cli-engine/tests/interaction-affordances.test.ts index 8ed52350..41d5d5e5 100644 --- a/packages/cli-engine/tests/interaction-affordances.test.ts +++ b/packages/cli-engine/tests/interaction-affordances.test.ts @@ -41,6 +41,9 @@ function probeCommand(run: (ctx: CommandContext) => Promise) { human: (): readonly Block[] => [ { kind: "summary", status: "ok", text: `answer=${answer}` }, ], + stdout: () => [], + json: () => ({ answer }), + next: () => [], }, ), ); diff --git a/packages/cli-engine/tests/lifetimes.test.ts b/packages/cli-engine/tests/lifetimes.test.ts index 5a4cb8d0..842219ea 100644 --- a/packages/cli-engine/tests/lifetimes.test.ts +++ b/packages/cli-engine/tests/lifetimes.test.ts @@ -194,7 +194,15 @@ describe("signal exit codes", () => { handler: async (_args, ctx) => { await signalDone(ctx.signal); return ok( - ctx.present({ data: { cleanedUp: true } }, { human: () => [] }), + ctx.present( + { data: { cleanedUp: true } }, + { + human: () => [], + stdout: () => [], + json: () => ({ cleanedUp: true }), + next: () => [], + }, + ), ); }, }); @@ -220,7 +228,15 @@ describe("signal exit codes", () => { handler: async (_args, ctx) => { await signalDone(ctx.signal); return ok( - ctx.present({ data: null, exitCode: 4 }, { human: () => [] }), + ctx.present( + { data: null, exitCode: 4 }, + { + human: () => [], + stdout: () => [], + json: () => null, + next: () => [], + }, + ), ); }, }); @@ -320,7 +336,17 @@ describe("the engine owns the double-signal policy", () => { const quick = defineCommand({ help: { summary: "Completes immediately" }, handler: async (_args, ctx) => - ok(ctx.present({ data: null }, { human: () => [] })), + ok( + ctx.present( + { data: null }, + { + human: () => [], + stdout: () => [], + json: () => null, + next: () => [], + }, + ), + ), }); const cli = createCli({ name: "t", @@ -376,6 +402,9 @@ describe("optional dependencies", () => { { data: { resolvable: probe.ok } }, { human: (): readonly Block[] => [], + stdout: () => [], + json: () => ({ resolvable: probe.ok }), + next: () => [], }, ), ); @@ -395,7 +424,17 @@ describe("optional dependencies", () => { help: { summary: "Probes a dependency" }, needs: { dependencies: ["typescript"] }, handler: async (_args, ctx) => - ok(ctx.present({ data: null }, { human: (): readonly Block[] => [] })), + ok( + ctx.present( + { data: null }, + { + human: (): readonly Block[] => [], + stdout: () => [], + json: () => null, + next: () => [], + }, + ), + ), }); const cli = createTestCli({ commands: { command }, now: EPOCH }); const result = await cli.run(["command", "--json"], { cwd: "." }); @@ -458,7 +497,17 @@ describe("optional dependencies", () => { needs: { dependencies: [MISSING] }, handler: async (_args, ctx) => { ran = true; - return ok(ctx.present({ data: null }, { human: () => [] })); + return ok( + ctx.present( + { data: null }, + { + human: () => [], + stdout: () => [], + json: () => null, + next: () => [], + }, + ), + ); }, }); const cli = createTestCli({ @@ -500,7 +549,17 @@ describe("optional dependencies", () => { help: { summary: "Unconditionally needs a missing dependency" }, needs: { dependencies: [MISSING] }, handler: async (_args, ctx) => - ok(ctx.present({ data: null }, { human: () => [] })), + ok( + ctx.present( + { data: null }, + { + human: () => [], + stdout: () => [], + json: () => null, + next: () => [], + }, + ), + ), }); const cli = createTestCli({ commands: { command }, now: EPOCH }); const result = await cli.run(["command", "--json"], { cwd: dir }); @@ -527,7 +586,17 @@ describe("optional dependencies", () => { help: { summary: "Needs installed dependencies" }, needs: { dependencies: ["typescript", "vitest"] }, handler: async (_args, ctx) => - ok(ctx.present({ data: null }, { human: (): readonly Block[] => [] })), + ok( + ctx.present( + { data: null }, + { + human: (): readonly Block[] => [], + stdout: () => [], + json: () => null, + next: () => [], + }, + ), + ), }); const cli = createTestCli({ commands: { command }, now: EPOCH }); const result = await cli.run(["command", "--json"], { diff --git a/packages/cli-engine/tests/management-api.test.ts b/packages/cli-engine/tests/management-api.test.ts index f518ce45..224e89cf 100644 --- a/packages/cli-engine/tests/management-api.test.ts +++ b/packages/cli-engine/tests/management-api.test.ts @@ -147,7 +147,17 @@ const succeed = ( help: { summary: "toy" }, handler: async (_args, ctx) => { await body?.(ctx); - return ok(ctx.present({ data: null }, { human: () => [] })); + return ok( + ctx.present( + { data: null }, + { + human: () => [], + stdout: () => [], + json: () => null, + next: () => [], + }, + ), + ); }, }); diff --git a/packages/cli-engine/tests/package-install-matrix.test.ts b/packages/cli-engine/tests/package-install-matrix.test.ts index 618e5c8c..29e78418 100644 --- a/packages/cli-engine/tests/package-install-matrix.test.ts +++ b/packages/cli-engine/tests/package-install-matrix.test.ts @@ -121,7 +121,9 @@ const init = defineCommand({ { data: { warnings } }, { human: () => warnings.map(warnBlock), + stdout: () => [], json: () => ({ warnings }), + next: () => [], }, ), ); @@ -168,7 +170,12 @@ const vendored = defineCommand({ if (!installed.ok) { return notOk(installed.failure); } - return ok(ctx.present({ data: null }, { human: () => [] })); + return ok( + ctx.present( + { data: null }, + { human: () => [], stdout: () => [], json: () => null, next: () => [] }, + ), + ); }, }); diff --git a/packages/cli-engine/tests/package-operations.test.ts b/packages/cli-engine/tests/package-operations.test.ts index c616ceec..50ca618b 100644 --- a/packages/cli-engine/tests/package-operations.test.ts +++ b/packages/cli-engine/tests/package-operations.test.ts @@ -212,7 +212,17 @@ function installer( if (!outcome.ok) { return notOk(outcome.failure); } - return ok(ctx.present({ data: null }, { human: () => [] })); + return ok( + ctx.present( + { data: null }, + { + human: () => [], + stdout: () => [], + json: () => null, + next: () => [], + }, + ), + ); }, }); } @@ -225,7 +235,17 @@ describe("the installsPackages capability", () => { installsPackages: true, handler: async (_args, ctx) => { present = "packages" in ctx; - return ok(ctx.present({ data: null }, { human: () => [] })); + return ok( + ctx.present( + { data: null }, + { + human: () => [], + stdout: () => [], + json: () => null, + next: () => [], + }, + ), + ); }, }); const cli = createTestCli({ commands: { toy } }); @@ -242,7 +262,17 @@ describe("the installsPackages capability", () => { help: { summary: "Installs nothing" }, handler: async (_args, ctx) => { present = "packages" in ctx; - return ok(ctx.present({ data: null }, { human: () => [] })); + return ok( + ctx.present( + { data: null }, + { + human: () => [], + stdout: () => [], + json: () => null, + next: () => [], + }, + ), + ); }, }); const cli = createTestCli({ commands: { toy } }); @@ -615,7 +645,17 @@ describe("the two ways an operation does not resolve notOk", () => { if (rejected !== undefined) { throw rejected.reason; } - return ok(ctx.present({ data: null }, { human: () => [] })); + return ok( + ctx.present( + { data: null }, + { + human: () => [], + stdout: () => [], + json: () => null, + next: () => [], + }, + ), + ); }, }); const cli = createTestCli({ @@ -677,7 +717,17 @@ describe("the two ways an operation does not resolve notOk", () => { const installing = ctx.packages.install({ packages: ["prisma"] }); controller.abort(); await installing; - return ok(ctx.present({ data: null }, { human: () => [] })); + return ok( + ctx.present( + { data: null }, + { + human: () => [], + stdout: () => [], + json: () => null, + next: () => [], + }, + ), + ); }, }); // No packageManager seed, so detection actually runs. @@ -713,7 +763,17 @@ describe("the two ways an operation does not resolve notOk", () => { (cause: unknown) => String(cause), ); await child; - return ok(ctx.present({ data: failure }, { human: () => [] })); + return ok( + ctx.present( + { data: failure }, + { + human: () => [], + stdout: () => [], + json: () => failure, + next: () => [], + }, + ), + ); }, }); const cli = createTestCli({ @@ -743,7 +803,17 @@ describe("the two ways an operation does not resolve notOk", () => { (cause: unknown) => String(cause), ); await installing; - return ok(ctx.present({ data: failure }, { human: () => [] })); + return ok( + ctx.present( + { data: failure }, + { + human: () => [], + stdout: () => [], + json: () => failure, + next: () => [], + }, + ), + ); }, }); const cli = createTestCli({ @@ -774,7 +844,17 @@ describe("the two ways an operation does not resolve notOk", () => { handler: async (_args, ctx) => { await ctx.packages.install({ packages: ["prisma"] }); sameSignal = captured === ctx.signal; - return ok(ctx.present({ data: null }, { human: () => [] })); + return ok( + ctx.present( + { data: null }, + { + human: () => [], + stdout: () => [], + json: () => null, + next: () => [], + }, + ), + ); }, }); const cli = createTestCli({ diff --git a/packages/cli-engine/tests/palette.test.ts b/packages/cli-engine/tests/palette.test.ts index a7270bb2..6962d02e 100644 --- a/packages/cli-engine/tests/palette.test.ts +++ b/packages/cli-engine/tests/palette.test.ts @@ -128,6 +128,9 @@ const styled = defineCommand({ { kind: "summary", status: "ok", text: [HEADING_SPAN] }, { kind: "list", items: [`width=${ui.width}`] }, ], + stdout: () => [], + json: () => ({}), + next: () => [], }, ), ), diff --git a/packages/cli-engine/tests/prompts.test.ts b/packages/cli-engine/tests/prompts.test.ts index 24d9d656..bb0044c7 100644 --- a/packages/cli-engine/tests/prompts.test.ts +++ b/packages/cli-engine/tests/prompts.test.ts @@ -34,6 +34,9 @@ function promptCommand(run: (prompt: PromptSurface) => Promise) { human: (): readonly Block[] => [ { kind: "summary", status: "ok", text: `answer=${answer}` }, ], + stdout: () => [], + json: () => ({ answer }), + next: () => [], }, ), ); @@ -429,6 +432,9 @@ describe("needs.interaction", () => { human: (): readonly Block[] => [ { kind: "summary", status: "ok", text: "ran" }, ], + stdout: () => [], + json: () => null, + next: () => [], }, ), ); diff --git a/packages/cli-engine/tests/redirects.test.ts b/packages/cli-engine/tests/redirects.test.ts index c9b5f6a6..84ddef20 100644 --- a/packages/cli-engine/tests/redirects.test.ts +++ b/packages/cli-engine/tests/redirects.test.ts @@ -22,7 +22,12 @@ const status = defineCommand({ help: { summary: "Show migration status" }, args: { flags: { detailed: flag.boolean({ brief: "more detail" }) } }, handler: async (_args, ctx) => - ok(ctx.present({ data: null }, { human: () => [] })), + ok( + ctx.present( + { data: null }, + { human: () => [], stdout: () => [], json: () => null, next: () => [] }, + ), + ), }); const MIGRATION_GROUP = { migration: { brief: "Migrations" } }; @@ -590,7 +595,17 @@ describe("retired flags", () => { const list = defineCommand({ help: { summary: "List migrations" }, handler: async (_args, ctx) => - ok(ctx.present({ data: null }, { human: () => [] })), + ok( + ctx.present( + { data: null }, + { + human: () => [], + stdout: () => [], + json: () => null, + next: () => [], + }, + ), + ), }); const cli = createTestCli({ commandFamilies: [ diff --git a/packages/cli-engine/tests/run-hooks.test.ts b/packages/cli-engine/tests/run-hooks.test.ts index 2bcc2a47..c697ac19 100644 --- a/packages/cli-engine/tests/run-hooks.test.ts +++ b/packages/cli-engine/tests/run-hooks.test.ts @@ -28,7 +28,12 @@ const deploy = defineCommand({ ok( ctx.present( { data: null }, - { human: () => [{ kind: "summary", status: "ok", text: "deployed" }] }, + { + human: () => [{ kind: "summary", status: "ok", text: "deployed" }], + stdout: () => [], + json: () => null, + next: () => [], + }, ), ), }); diff --git a/packages/cli-engine/tests/spawn.test.ts b/packages/cli-engine/tests/spawn.test.ts index f1909c20..e25d5b4a 100644 --- a/packages/cli-engine/tests/spawn.test.ts +++ b/packages/cli-engine/tests/spawn.test.ts @@ -182,7 +182,15 @@ describe("the run records its most recent child", () => { maySpawn: true, handler: async (_args, ctx) => ok( - ctx.present({ data: ctx.lastChild() ?? "none" }, { human: () => [] }), + ctx.present( + { data: ctx.lastChild() ?? "none" }, + { + human: () => [], + stdout: () => [], + json: () => ctx.lastChild() ?? "none", + next: () => [], + }, + ), ), }); const cli = createTestCli({ commands: { asking }, now: CLOCK }); @@ -199,7 +207,17 @@ describe("the run records its most recent child", () => { maySpawn: true, handler: async (_args, ctx) => { await ctx.spawn({ command: "alchemy" }); - return ok(ctx.present({ data: ctx.lastChild() }, { human: () => [] })); + return ok( + ctx.present( + { data: ctx.lastChild() }, + { + human: () => [], + stdout: () => [], + json: () => ctx.lastChild(), + next: () => [], + }, + ), + ); }, }); const cli = createTestCli({ @@ -224,7 +242,12 @@ describe("the run records its most recent child", () => { return ok( ctx.present( { data: { afterFirst, afterSecond: ctx.lastChild() } }, - { human: () => [] }, + { + human: () => [], + stdout: () => [], + json: () => ({ afterFirst, afterSecond: ctx.lastChild() }), + next: () => [], + }, ), ); }, @@ -306,7 +329,17 @@ describe("the spawn request", () => { .spawn({ command: "second" }) .then(() => "no error", String); await first; - return ok(ctx.present({ data: second }, { human: () => [] })); + return ok( + ctx.present( + { data: second }, + { + human: () => [], + stdout: () => [], + json: () => second, + next: () => [], + }, + ), + ); }, }); const cli = createTestCli({ commands: { twice }, now: CLOCK }); @@ -322,7 +355,17 @@ describe("the spawn request", () => { help: { summary: "Spawns without declaring it" }, handler: async (_args, ctx) => { await ctx.spawn({ command: "alchemy" }); - return ok(ctx.present({ data: null }, { human: () => [] })); + return ok( + ctx.present( + { data: null }, + { + human: () => [], + stdout: () => [], + json: () => null, + next: () => [], + }, + ), + ); }, }); const cli = createTestCli({ commands: { undeclared }, now: CLOCK }); @@ -338,7 +381,17 @@ describe("the spawn request", () => { help: { summary: "Wants credentials it cannot hand over" }, needs: { credentials: "child" }, handler: async (_args, ctx) => - ok(ctx.present({ data: null }, { human: () => [] })), + ok( + ctx.present( + { data: null }, + { + human: () => [], + stdout: () => [], + json: () => null, + next: () => [], + }, + ), + ), }); expect(() => @@ -393,7 +446,17 @@ describe("output while a child owns the terminal", () => { ctx.report({ kind: "message", severity: "info", text: "during-2" }); await child; ctx.report({ kind: "message", severity: "info", text: "after" }); - return ok(ctx.present({ data: null }, { human: () => [] })); + return ok( + ctx.present( + { data: null }, + { + human: () => [], + stdout: () => [], + json: () => null, + next: () => [], + }, + ), + ); }, }); const cli = createTestCli({ @@ -425,14 +488,32 @@ describe("output while a child owns the terminal", () => { const child = ctx.spawn({ command: "alchemy" }); const failure = (() => { try { - ctx.present({ data: null }, { human: () => [] }); + ctx.present( + { data: null }, + { + human: () => [], + stdout: () => [], + json: () => null, + next: () => [], + }, + ); return "no error"; } catch (cause) { return String(cause); } })(); await child; - return ok(ctx.present({ data: failure }, { human: () => [] })); + return ok( + ctx.present( + { data: failure }, + { + human: () => [], + stdout: () => [], + json: () => failure, + next: () => [], + }, + ), + ); }, }); const cli = createTestCli({ commands: { presenting }, now: CLOCK }); @@ -458,7 +539,17 @@ describe("signals", () => { abortedNow = () => ctx.signal.aborted; await ctx.spawn({ command: "alchemy" }); abortedAfterChild = ctx.signal.aborted; - return ok(ctx.present({ data: null }, { human: () => [] })); + return ok( + ctx.present( + { data: null }, + { + human: () => [], + stdout: () => [], + json: () => null, + next: () => [], + }, + ), + ); }, }); const cli = createTestCli({ @@ -1126,7 +1217,17 @@ describe("the commentary buffer is bounded", () => { }); } await child; - return ok(ctx.present({ data: null }, { human: () => [] })); + return ok( + ctx.present( + { data: null }, + { + human: () => [], + stdout: () => [], + json: () => null, + next: () => [], + }, + ), + ); }, }); const messages: string[] = []; @@ -1161,7 +1262,17 @@ describe("the terminal has one owner at a time", () => { .confirm("Proceed?", { default: true }) .then(() => "no error", String); await child; - return ok(ctx.present({ data: failure }, { human: () => [] })); + return ok( + ctx.present( + { data: failure }, + { + human: () => [], + stdout: () => [], + json: () => failure, + next: () => [], + }, + ), + ); }, }); const cli = createTestCli({ commands: { prompting }, now: CLOCK }); @@ -1185,7 +1296,17 @@ describe("the terminal has one owner at a time", () => { .spawn({ command: "alchemy" }) .then(() => "no error", String); await answer; - return ok(ctx.present({ data: failure }, { human: () => [] })); + return ok( + ctx.present( + { data: failure }, + { + human: () => [], + stdout: () => [], + json: () => failure, + next: () => [], + }, + ), + ); }, }); const cli = createTestCli({ commands: { prompting }, now: CLOCK }); diff --git a/packages/cli-engine/tests/telemetry-payload.test.ts b/packages/cli-engine/tests/telemetry-payload.test.ts index d79539db..8627dfd1 100644 --- a/packages/cli-engine/tests/telemetry-payload.test.ts +++ b/packages/cli-engine/tests/telemetry-payload.test.ts @@ -32,7 +32,12 @@ const deploy = defineCommand({ ok( ctx.present( { data: null }, - { human: () => [{ kind: "summary", status: "ok", text: "deployed" }] }, + { + human: () => [{ kind: "summary", status: "ok", text: "deployed" }], + stdout: () => [], + json: () => null, + next: () => [], + }, ), ), }); diff --git a/packages/cli-engine/tests/telemetry-run.test.ts b/packages/cli-engine/tests/telemetry-run.test.ts index 0303923a..5d28b7d9 100644 --- a/packages/cli-engine/tests/telemetry-run.test.ts +++ b/packages/cli-engine/tests/telemetry-run.test.ts @@ -69,7 +69,12 @@ const deploy = defineCommand({ return ok( ctx.present( { data: null }, - { human: () => [{ kind: "summary", status: "ok", text: "deployed" }] }, + { + human: () => [{ kind: "summary", status: "ok", text: "deployed" }], + stdout: () => [], + json: () => null, + next: () => [], + }, ), ); }, @@ -86,7 +91,12 @@ const telemetryStatus = defineCommand({ ok( ctx.present( { data: null }, - { human: () => [{ kind: "summary", status: "ok", text: "enabled" }] }, + { + human: () => [{ kind: "summary", status: "ok", text: "enabled" }], + stdout: () => [], + json: () => null, + next: () => [], + }, ), ), }); @@ -101,7 +111,12 @@ const signedOut = defineCommand({ return ok( ctx.present( { data: null }, - { human: () => [{ kind: "summary", status: "ok", text: "signed in" }] }, + { + human: () => [{ kind: "summary", status: "ok", text: "signed in" }], + stdout: () => [], + json: () => null, + next: () => [], + }, ), ); }, @@ -116,7 +131,12 @@ const spawning = defineCommand({ return ok( ctx.present( { data: null }, - { human: () => [{ kind: "summary", status: "ok", text: "spawned" }] }, + { + human: () => [{ kind: "summary", status: "ok", text: "spawned" }], + stdout: () => [], + json: () => null, + next: () => [], + }, ), ); }, diff --git a/packages/cli/src/commands/agent/presentation.ts b/packages/cli/src/commands/agent/presentation.ts index ec8a1201..d28fd380 100644 --- a/packages/cli/src/commands/agent/presentation.ts +++ b/packages/cli/src/commands/agent/presentation.ts @@ -61,6 +61,8 @@ export function installPresentations( statusCommand: string | null, ): Presentations { return { + stdout: () => [], + json: () => result, human: () => [ { kind: "summary", @@ -90,6 +92,8 @@ export function statusPresentations( installCommand: string | null, ): Presentations { return { + stdout: () => [], + json: () => result, human: () => [ title(`Checking ${result.statusScope} Prisma skills.`), fields([ diff --git a/packages/cli/src/commands/auth/login.ts b/packages/cli/src/commands/auth/login.ts index 4d73d904..dccbbaee 100644 --- a/packages/cli/src/commands/auth/login.ts +++ b/packages/cli/src/commands/auth/login.ts @@ -69,16 +69,20 @@ function nextActionsFor( ]; } -function presentationsFor(spec: { - readonly session: Session; - readonly environmentCredentialInForce: boolean; - readonly agentSetupTipCommand: string | null; -}): Presentations { +function presentationsFor( + spec: { + readonly session: Session; + readonly environmentCredentialInForce: boolean; + readonly agentSetupTipCommand: string | null; + }, + result: LoginResult, +): Presentations { const rows = [ { label: "status", value: "signed in" }, { label: "workspace", value: sessionLabel(spec.session) }, ]; return { + json: () => result, human: () => [ { kind: "summary", status: "info", text: TITLE }, { kind: "fields", rows }, @@ -152,11 +156,14 @@ export const authLoginCommand = defineCommand({ return ok( ctx.present( { data: result }, - presentationsFor({ - session, - environmentCredentialInForce: environmentSession, - agentSetupTipCommand, - }), + presentationsFor( + { + session, + environmentCredentialInForce: environmentSession, + agentSetupTipCommand, + }, + result, + ), ), ); }, diff --git a/packages/cli/src/commands/auth/logout.ts b/packages/cli/src/commands/auth/logout.ts index 17791ed3..170c0f9a 100644 --- a/packages/cli/src/commands/auth/logout.ts +++ b/packages/cli/src/commands/auth/logout.ts @@ -25,6 +25,7 @@ function presentationsFor( : `Ended ${result.endedCount} workspace ${result.endedCount === 1 ? "session" : "sessions"}.`; const rows = [{ label: "ended", value: String(result.endedCount) }]; return { + json: () => result, human: () => [ { kind: "summary", diff --git a/packages/cli/src/commands/auth/whoami.ts b/packages/cli/src/commands/auth/whoami.ts index 27607556..07879567 100644 --- a/packages/cli/src/commands/auth/whoami.ts +++ b/packages/cli/src/commands/auth/whoami.ts @@ -97,13 +97,17 @@ function mergedIdentity( }; } -function presentationsFor(spec: { - readonly credential: ActiveCredential | null; - readonly identity: CredentialIdentity | null; -}): Presentations { +function presentationsFor( + spec: { + readonly credential: ActiveCredential | null; + readonly identity: CredentialIdentity | null; + }, + result: WhoamiResult, +): Presentations { const rows = credentialFieldRows(spec); const fromEnvironment = spec.credential?.origin.source === "environment"; return { + json: () => result, human: () => [ { kind: "summary", status: "info", text: TITLE }, { kind: "fields", rows }, @@ -157,7 +161,10 @@ export const authWhoamiCommand = defineCommand({ expiresAt: credential?.expiresAt?.toISOString() ?? null, }; return ok( - ctx.present({ data: result }, presentationsFor({ credential, identity })), + ctx.present( + { data: result }, + presentationsFor({ credential, identity }, result), + ), ); }, }); diff --git a/packages/cli/src/commands/auth/workspace-logout.ts b/packages/cli/src/commands/auth/workspace-logout.ts index e9a7d656..8ab41e81 100644 --- a/packages/cli/src/commands/auth/workspace-logout.ts +++ b/packages/cli/src/commands/auth/workspace-logout.ts @@ -15,13 +15,17 @@ export interface WorkspaceLogoutResult { readonly wasSelected: boolean; } -function logoutPresentations(spec: { - readonly label: string; - readonly wasSelected: boolean; - readonly environmentCredentialInForce: boolean; -}): Presentations { +function logoutPresentations( + spec: { + readonly label: string; + readonly wasSelected: boolean; + readonly environmentCredentialInForce: boolean; + }, + result: WorkspaceLogoutResult, +): Presentations { const rows = [{ label: "workspace", value: spec.label }]; return { + json: () => result, human: () => [ { kind: "summary", @@ -95,11 +99,14 @@ export const authWorkspaceLogoutCommand = defineCommand({ return ok( ctx.present( { data: result }, - logoutPresentations({ - label: sessionLabel(session), - wasSelected, - environmentCredentialInForce: environmentCredentialInForce(ctx.env), - }), + logoutPresentations( + { + label: sessionLabel(session), + wasSelected, + environmentCredentialInForce: environmentCredentialInForce(ctx.env), + }, + result, + ), ), ); }, diff --git a/packages/cli/src/commands/auth/workspace-use.ts b/packages/cli/src/commands/auth/workspace-use.ts index b2f610e9..a8a1a03c 100644 --- a/packages/cli/src/commands/auth/workspace-use.ts +++ b/packages/cli/src/commands/auth/workspace-use.ts @@ -34,11 +34,14 @@ function noWorkspaceSessionsError(): CliStructuredError { ); } -function usePresentations(spec: { - readonly session: Session; - readonly previous: Session | undefined; - readonly environmentCredentialInForce: boolean; -}): Presentations { +function usePresentations( + spec: { + readonly session: Session; + readonly previous: Session | undefined; + readonly environmentCredentialInForce: boolean; + }, + result: WorkspaceUseResult, +): Presentations { const rows = [ ...(spec.previous === undefined ? [] @@ -46,6 +49,7 @@ function usePresentations(spec: { { label: "workspace", value: sessionLabel(spec.session) }, ]; return { + json: () => result, human: () => [ { kind: "summary", @@ -124,11 +128,14 @@ export const authWorkspaceUseCommand = defineCommand({ return ok( ctx.present( { data: result }, - usePresentations({ - session, - previous, - environmentCredentialInForce: environmentCredentialInForce(ctx.env), - }), + usePresentations( + { + session, + previous, + environmentCredentialInForce: environmentCredentialInForce(ctx.env), + }, + result, + ), ), ); }, diff --git a/packages/cli/src/commands/branch/list.ts b/packages/cli/src/commands/branch/list.ts index ad2125d3..02ecca64 100644 --- a/packages/cli/src/commands/branch/list.ts +++ b/packages/cli/src/commands/branch/list.ts @@ -24,6 +24,8 @@ function listPresentations(result: BranchListResult): Presentations { branch.envMap, ]); return { + json: () => result, + next: () => [], human: (): Block[] => [ { kind: "summary", status: "info", text: TITLE }, { diff --git a/packages/cli/src/commands/bucket/create.ts b/packages/cli/src/commands/bucket/create.ts index a02c28eb..8ebc3c90 100644 --- a/packages/cli/src/commands/bucket/create.ts +++ b/packages/cli/src/commands/bucket/create.ts @@ -52,6 +52,9 @@ export const bucketCreateCommand = defineCommand({ text: `Created bucket "${bucket.name}" in ${bucketTargetLabel(projectName, bucket.branchId)}.`, }, ], + stdout: () => [], + json: () => result, + next: () => [], }, ), ); diff --git a/packages/cli/src/commands/bucket/delete.ts b/packages/cli/src/commands/bucket/delete.ts index 26ec1fba..0198341e 100644 --- a/packages/cli/src/commands/bucket/delete.ts +++ b/packages/cli/src/commands/bucket/delete.ts @@ -16,6 +16,9 @@ const CONSENT_QUESTION = function deletePresentations(result: BucketDeleteResult): Presentations { return { + stdout: () => [], + json: () => result, + next: () => [], human: (): Block[] => [ { kind: "summary", status: "ok", text: "Deleting object-store bucket." }, { kind: "fields", rows: [{ label: "bucket", value: result.bucket.id }] }, diff --git a/packages/cli/src/commands/bucket/key-create.ts b/packages/cli/src/commands/bucket/key-create.ts index 79dbeda8..af7bd58d 100644 --- a/packages/cli/src/commands/bucket/key-create.ts +++ b/packages/cli/src/commands/bucket/key-create.ts @@ -19,6 +19,8 @@ function resolveKeyRole(role: string | undefined): "read" | "read_write" { function createPresentations(result: BucketKeyCreateResult): Presentations { return { + json: () => result, + next: () => [], human: (): Block[] => [ { kind: "summary", diff --git a/packages/cli/src/commands/bucket/key-delete.ts b/packages/cli/src/commands/bucket/key-delete.ts index 30690a37..058d3497 100644 --- a/packages/cli/src/commands/bucket/key-delete.ts +++ b/packages/cli/src/commands/bucket/key-delete.ts @@ -13,6 +13,9 @@ import { mapBucketOperationError } from "./errors"; function deletePresentations(result: BucketKeyDeleteResult): Presentations { return { + stdout: () => [], + json: () => result, + next: () => [], human: (): Block[] => [ { kind: "summary", status: "ok", text: "Deleting bucket access key." }, { kind: "fields", rows: [{ label: "key", value: result.key.id }] }, diff --git a/packages/cli/src/commands/bucket/key-list.ts b/packages/cli/src/commands/bucket/key-list.ts index 9b0a29a1..6cbb042b 100644 --- a/packages/cli/src/commands/bucket/key-list.ts +++ b/packages/cli/src/commands/bucket/key-list.ts @@ -17,6 +17,7 @@ const TITLE = "Listing access keys for bucket."; function listPresentations(result: BucketKeyListResult): Presentations { const rows = bucketKeyRows(result.keys); return { + next: () => [], human: (): Block[] => [ { kind: "summary", status: "info", text: TITLE }, { kind: "fields", rows: [{ label: "bucket", value: result.bucketId }] }, diff --git a/packages/cli/src/commands/bucket/list.ts b/packages/cli/src/commands/bucket/list.ts index e73c19ff..c92201fa 100644 --- a/packages/cli/src/commands/bucket/list.ts +++ b/packages/cli/src/commands/bucket/list.ts @@ -17,6 +17,7 @@ function listPresentations(result: BucketListResult): Presentations { const rows = bucketRows(result.buckets); const stdoutRows = bucketStdoutRows(result.buckets); return { + next: () => [], human: (): Block[] => [ { kind: "summary", status: "info", text: TITLE }, { diff --git a/packages/cli/src/commands/feedback.ts b/packages/cli/src/commands/feedback.ts index be2200c8..3033fe59 100644 --- a/packages/cli/src/commands/feedback.ts +++ b/packages/cli/src/commands/feedback.ts @@ -92,6 +92,9 @@ function emailInvalidError(value: string): CliStructuredError { function feedbackPresentations(result: FeedbackResult): Presentations { return { + stdout: () => [], + json: () => result, + next: () => [], human: () => [ { kind: "summary", status: "ok", text: "Feedback sent. Thank you!" }, { diff --git a/packages/cli/src/commands/git/connect.ts b/packages/cli/src/commands/git/connect.ts index 510cc1d6..7f8f6241 100644 --- a/packages/cli/src/commands/git/connect.ts +++ b/packages/cli/src/commands/git/connect.ts @@ -126,6 +126,9 @@ function connectPresentations( ): Presentations { const connection = result.repositoryConnection; return { + stdout: () => [], + json: () => result, + next: () => [], human: (): Block[] => [ { kind: "summary", diff --git a/packages/cli/src/commands/git/disconnect.ts b/packages/cli/src/commands/git/disconnect.ts index 0297c33d..e4d1d2d9 100644 --- a/packages/cli/src/commands/git/disconnect.ts +++ b/packages/cli/src/commands/git/disconnect.ts @@ -19,6 +19,9 @@ function disconnectPresentations( result: ProjectRepositoryConnectionResult, ): Presentations { return { + stdout: () => [], + json: () => result, + next: () => [], human: (): Block[] => [ { kind: "summary", diff --git a/packages/cli/src/commands/init/presentation.ts b/packages/cli/src/commands/init/presentation.ts index 6d723284..0710dfcf 100644 --- a/packages/cli/src/commands/init/presentation.ts +++ b/packages/cli/src/commands/init/presentation.ts @@ -73,6 +73,7 @@ export function initPresentations( formatCommand: PrismaCliPackageCommandFormatter, ): Presentations { return { + json: () => result, human: () => [ { diff --git a/packages/cli/src/commands/postgres/backup-list.ts b/packages/cli/src/commands/postgres/backup-list.ts index 1c24240e..a0f66304 100644 --- a/packages/cli/src/commands/postgres/backup-list.ts +++ b/packages/cli/src/commands/postgres/backup-list.ts @@ -27,6 +27,7 @@ function backupListPresentations( const rows = backupRows(result.backups); const stdoutRows = backupStdoutRows(result.backups); return { + next: () => [], human: (): Block[] => [ { kind: "summary", status: "info", text: TITLE }, { diff --git a/packages/cli/src/commands/postgres/connection-create.ts b/packages/cli/src/commands/postgres/connection-create.ts index b093593e..d4fae79e 100644 --- a/packages/cli/src/commands/postgres/connection-create.ts +++ b/packages/cli/src/commands/postgres/connection-create.ts @@ -71,6 +71,8 @@ export const postgresConnectionCreateCommand = defineCommand({ result.connectionString, ), stdout: () => [result.connectionString], + json: () => result, + next: () => [], }, ), ); diff --git a/packages/cli/src/commands/postgres/connection-list.ts b/packages/cli/src/commands/postgres/connection-list.ts index f650106c..de76c4e4 100644 --- a/packages/cli/src/commands/postgres/connection-list.ts +++ b/packages/cli/src/commands/postgres/connection-list.ts @@ -43,6 +43,7 @@ function listPresentations( const rows = connectionRows(result); const stdoutRows = connectionStdoutRows(result); return { + next: () => [], human: (): Block[] => [ { kind: "summary", status: "info", text: TITLE }, { diff --git a/packages/cli/src/commands/postgres/connection-remove.ts b/packages/cli/src/commands/postgres/connection-remove.ts index 3728a068..29cf76d5 100644 --- a/packages/cli/src/commands/postgres/connection-remove.ts +++ b/packages/cli/src/commands/postgres/connection-remove.ts @@ -68,7 +68,9 @@ export const postgresConnectionRemoveCommand = defineCommand({ ], }, ], + stdout: () => [], json: () => ({ connection: result.connection }), + next: () => [], }, ), ); diff --git a/packages/cli/src/commands/postgres/connection-rotate.ts b/packages/cli/src/commands/postgres/connection-rotate.ts index 25144e09..72946c15 100644 --- a/packages/cli/src/commands/postgres/connection-rotate.ts +++ b/packages/cli/src/commands/postgres/connection-rotate.ts @@ -72,6 +72,8 @@ export const postgresConnectionRotateCommand = defineCommand({ result.connectionString, ), stdout: () => [result.connectionString], + json: () => result, + next: () => [], }, ), ); diff --git a/packages/cli/src/commands/postgres/create.ts b/packages/cli/src/commands/postgres/create.ts index 2d0bf270..f36dca93 100644 --- a/packages/cli/src/commands/postgres/create.ts +++ b/packages/cli/src/commands/postgres/create.ts @@ -78,6 +78,8 @@ export const postgresCreateCommand = defineCommand({ result.connectionString, ), stdout: () => [result.connectionString], + json: () => result, + next: () => [], }, ), ); diff --git a/packages/cli/src/commands/postgres/list.ts b/packages/cli/src/commands/postgres/list.ts index b4aaf5c8..ea854eba 100644 --- a/packages/cli/src/commands/postgres/list.ts +++ b/packages/cli/src/commands/postgres/list.ts @@ -40,6 +40,7 @@ function listPresentations(result: DatabaseListResult): Presentations { const rows = databaseRows(result); const stdoutRows = databaseStdoutRows(result); return { + next: () => [], human: (): Block[] => [ { kind: "summary", status: "info", text: TITLE }, { diff --git a/packages/cli/src/commands/postgres/remove.ts b/packages/cli/src/commands/postgres/remove.ts index 5f78b472..3d79e15f 100644 --- a/packages/cli/src/commands/postgres/remove.ts +++ b/packages/cli/src/commands/postgres/remove.ts @@ -20,6 +20,9 @@ const CONSENT_QUESTION = function removePresentations(result: DatabaseRemoveResult): Presentations { return { + stdout: () => [], + json: () => result, + next: () => [], human: (): Block[] => [ { kind: "summary", status: "ok", text: "Removing database." }, { diff --git a/packages/cli/src/commands/postgres/restore.ts b/packages/cli/src/commands/postgres/restore.ts index fcdc2ac8..960f9177 100644 --- a/packages/cli/src/commands/postgres/restore.ts +++ b/packages/cli/src/commands/postgres/restore.ts @@ -37,6 +37,8 @@ function restorePresentations( ]; return { + stdout: () => [], + json: () => result, human: (): Block[] => [ { kind: "summary", diff --git a/packages/cli/src/commands/postgres/show.ts b/packages/cli/src/commands/postgres/show.ts index b0eba814..3954dbfc 100644 --- a/packages/cli/src/commands/postgres/show.ts +++ b/packages/cli/src/commands/postgres/show.ts @@ -53,6 +53,8 @@ function stdoutFieldRows(result: DatabaseShowResult): FieldRow[] { function showPresentations(result: DatabaseShowResult): Presentations { const rows = fieldRows(result); return { + json: () => result, + next: () => [], human: (): Block[] => [ { kind: "summary", status: "info", text: TITLE }, { kind: "fields", rows }, diff --git a/packages/cli/src/commands/postgres/usage.ts b/packages/cli/src/commands/postgres/usage.ts index e34d59ac..d8287f6f 100644 --- a/packages/cli/src/commands/postgres/usage.ts +++ b/packages/cli/src/commands/postgres/usage.ts @@ -67,6 +67,8 @@ function stdoutFieldRows(result: DatabaseUsageResult): FieldRow[] { function usagePresentations(result: DatabaseUsageResult): Presentations { const rows = fieldRows(result); return { + json: () => result, + next: () => [], human: (): Block[] => [ { kind: "summary", status: "info", text: TITLE }, { kind: "fields", rows }, diff --git a/packages/cli/src/commands/project/env-add.ts b/packages/cli/src/commands/project/env-add.ts index d1d889df..75494e18 100644 --- a/packages/cli/src/commands/project/env-add.ts +++ b/packages/cli/src/commands/project/env-add.ts @@ -39,6 +39,9 @@ const TITLE = "Setting a new environment variable."; function singlePresentations(result: EnvAddResult): Presentations { return { + stdout: () => [], + json: () => result, + next: () => [], human: (): Block[] => [ { kind: "summary", status: "info", text: TITLE }, { @@ -127,13 +130,16 @@ export const projectEnvAddCommand = defineCommand({ data: result, diagnostics: previewDefaultDiagnostics(written.warnings), }, - fileWritePresentations({ - title: "Setting new environment variables from file.", - emptyMessage: "No environment variables imported.", - scope: result.scope, - filePath: result.file.path, - variables: result.variables, - }), + fileWritePresentations( + { + title: "Setting new environment variables from file.", + emptyMessage: "No environment variables imported.", + scope: result.scope, + filePath: result.file.path, + variables: result.variables, + }, + result, + ), ), ); } diff --git a/packages/cli/src/commands/project/env-remove.ts b/packages/cli/src/commands/project/env-remove.ts index a679f91d..138d2e46 100644 --- a/packages/cli/src/commands/project/env-remove.ts +++ b/packages/cli/src/commands/project/env-remove.ts @@ -28,6 +28,9 @@ const TITLE = "Removing the environment variable from the scope."; function removePresentations(result: EnvRmResult): Presentations { return { + stdout: () => [], + json: () => result, + next: () => [], human: (): Block[] => [ { kind: "summary", status: "info", text: TITLE }, { diff --git a/packages/cli/src/commands/project/env-shared.ts b/packages/cli/src/commands/project/env-shared.ts index 24bad5b6..bb74f8b3 100644 --- a/packages/cli/src/commands/project/env-shared.ts +++ b/packages/cli/src/commands/project/env-shared.ts @@ -138,15 +138,21 @@ export function variableFieldRows( ]; } -export function fileWritePresentations(input: { - readonly title: string; - readonly emptyMessage: string; - readonly scope: EnvScopeDescriptor; - readonly filePath: string; - readonly variables: readonly EnvVariableMetadata[]; -}): Presentations { +export function fileWritePresentations( + input: { + readonly title: string; + readonly emptyMessage: string; + readonly scope: EnvScopeDescriptor; + readonly filePath: string; + readonly variables: readonly EnvVariableMetadata[]; + }, + result: unknown, +): Presentations { const rows = variableRows(input.variables); return { + stdout: () => [], + json: () => result, + next: () => [], human: (): Block[] => [ { kind: "summary", status: "info", text: input.title }, { diff --git a/packages/cli/src/commands/project/env-update.ts b/packages/cli/src/commands/project/env-update.ts index a2a29042..7a724bd0 100644 --- a/packages/cli/src/commands/project/env-update.ts +++ b/packages/cli/src/commands/project/env-update.ts @@ -38,6 +38,9 @@ const TITLE = "Replacing the environment variable's value."; function singlePresentations(result: EnvUpdateResult): Presentations { return { + stdout: () => [], + json: () => result, + next: () => [], human: (): Block[] => [ { kind: "summary", status: "info", text: TITLE }, { @@ -121,13 +124,16 @@ export const projectEnvUpdateCommand = defineCommand({ return ok( ctx.present( { data: result }, - fileWritePresentations({ - title: "Replacing environment variable values from file.", - emptyMessage: "No environment variables updated.", - scope: result.scope, - filePath: result.file.path, - variables: result.variables, - }), + fileWritePresentations( + { + title: "Replacing environment variable values from file.", + emptyMessage: "No environment variables updated.", + scope: result.scope, + filePath: result.file.path, + variables: result.variables, + }, + result, + ), ), ); } diff --git a/packages/cli/src/commands/project/presentation.ts b/packages/cli/src/commands/project/presentation.ts index 19a91436..6deecf93 100644 --- a/packages/cli/src/commands/project/presentation.ts +++ b/packages/cli/src/commands/project/presentation.ts @@ -43,6 +43,7 @@ export function toNextActions( export function setupPresentations(result: ProjectSetupResult): Presentations { return { + stdout: () => [], human: () => [ ...(result.action === "created" ? [ diff --git a/packages/cli/src/commands/project/remove.ts b/packages/cli/src/commands/project/remove.ts index b579e8dd..90c9ef57 100644 --- a/packages/cli/src/commands/project/remove.ts +++ b/packages/cli/src/commands/project/remove.ts @@ -24,6 +24,9 @@ const CONSENT_QUESTION = function removePresentations(result: ProjectRemoveResult): Presentations { return { + stdout: () => [], + json: () => result, + next: () => [], human: (): Block[] => [ { kind: "summary", status: "ok", text: "Removing project." }, { diff --git a/packages/cli/src/commands/project/rename.ts b/packages/cli/src/commands/project/rename.ts index 6969254c..31936821 100644 --- a/packages/cli/src/commands/project/rename.ts +++ b/packages/cli/src/commands/project/rename.ts @@ -18,6 +18,9 @@ import { mapProjectOperationError } from "./errors"; function renamePresentations(result: ProjectRenameResult): Presentations { return { + stdout: () => [], + json: () => result, + next: () => [], human: () => [ { kind: "summary", status: "ok", text: "Renaming project." }, { diff --git a/packages/cli/src/commands/project/show.ts b/packages/cli/src/commands/project/show.ts index 29feb7e1..9f216afd 100644 --- a/packages/cli/src/commands/project/show.ts +++ b/packages/cli/src/commands/project/show.ts @@ -72,6 +72,7 @@ function showPresentations( ): Presentations { const rows = fieldRows(result, cwd, env); return { + json: () => result, human: () => [ result.project === null ? { diff --git a/packages/cli/src/commands/project/transfer.ts b/packages/cli/src/commands/project/transfer.ts index 33b98e91..68025859 100644 --- a/packages/cli/src/commands/project/transfer.ts +++ b/packages/cli/src/commands/project/transfer.ts @@ -122,6 +122,8 @@ function transferPresentations( toWorkspace: string | undefined, ): Presentations { return { + stdout: () => [], + json: () => result, human: (): Block[] => [ { kind: "summary", status: "ok", text: "Transferring project." }, { diff --git a/packages/cli/src/commands/service/presentation.ts b/packages/cli/src/commands/service/presentation.ts index 510d5234..161109d9 100644 --- a/packages/cli/src/commands/service/presentation.ts +++ b/packages/cli/src/commands/service/presentation.ts @@ -86,6 +86,7 @@ function domainFailureRows(domain: ServiceDomainSummary): FieldRow[] { export function listPresentations(result: ServiceListResult): Presentations { return { + json: () => result, human: () => [ title("Listing services for the selected project."), fields([ @@ -150,6 +151,8 @@ export function createPresentations( result: ServiceCreateResult, ): Presentations { return { + stdout: () => [], + json: () => result, human: () => [ result.existing ? title( @@ -195,6 +198,8 @@ export function showPresentations(result: ServiceShowResult): Presentations { ); } return { + stdout: () => [], + json: () => result, human: () => [ title("Showing the selected service state."), fields([ @@ -219,6 +224,8 @@ export function deploymentListPresentations( result: ServiceDeploymentListResult, ): Presentations { return { + stdout: () => [], + json: () => result, human: () => [ title("Listing deployments for the selected service."), fields([ @@ -262,6 +269,9 @@ export function deploymentShowPresentations( result: ServiceDeploymentShowResult, ): Presentations { return { + stdout: () => [], + json: () => result, + next: () => [], human: () => [ title("Showing deployment details."), fields([ @@ -287,6 +297,7 @@ export function openPresentations( liveDeploymentId: string, ): Presentations { return { + json: () => result, human: () => [ result.opened ? completed("Opened the live URL for the selected service.") @@ -324,6 +335,8 @@ export function promotePresentations( alreadyLive: boolean, ): Presentations { return { + stdout: () => [], + json: () => result, human: () => [ completed( alreadyLive @@ -349,6 +362,8 @@ export function rollbackPresentations( alreadyLive: boolean, ): Presentations { return { + stdout: () => [], + json: () => result, human: () => [ completed( alreadyLive @@ -377,6 +392,8 @@ export function deploymentStartPresentations( result: ServiceDeploymentRunStateResult, ): Presentations { return { + stdout: () => [], + json: () => result, human: () => [ completed( result.alreadyInState @@ -401,6 +418,8 @@ export function deploymentStopPresentations( result: ServiceDeploymentRunStateResult, ): Presentations { return { + stdout: () => [], + json: () => result, human: () => [ completed( result.alreadyInState @@ -422,6 +441,8 @@ export function deploymentDeletePresentations( result: ServiceDeploymentDeleteResult, ): Presentations { return { + stdout: () => [], + json: () => result, human: () => [ completed(`Deleted ${result.deploymentId} from ${result.service.name}.`), fields([ @@ -441,6 +462,8 @@ export function removePresentations( result: ServiceRemoveResult, ): Presentations { return { + stdout: () => [], + json: () => result, human: () => [ completed( `Removed ${result.service.name} and every deployment it owned.`, @@ -461,6 +484,8 @@ export function domainAddPresentations( result: ServiceDomainAddResult, ): Presentations { return { + stdout: () => [], + json: () => result, human: () => [ result.existing ? title("Showing the existing custom domain for the selected service.") @@ -491,6 +516,8 @@ export function domainShowPresentations( result: ServiceDomainShowResult, ): Presentations { return { + stdout: () => [], + json: () => result, human: () => [ title("Showing custom domain status."), fields([ @@ -532,6 +559,9 @@ export function domainRemovePresentations( result: ServiceDomainRemoveResult, ): Presentations { return { + stdout: () => [], + json: () => result, + next: () => [], human: () => [ completed(`Removed ${result.hostname} from ${result.service.name}.`), fields([ @@ -547,6 +577,8 @@ export function domainRetryPresentations( result: ServiceDomainRetryResult, ): Presentations { return { + stdout: () => [], + json: () => result, human: () => [ completed(`Retried verification for ${result.domain.hostname}.`), fields([ @@ -570,6 +602,9 @@ export function domainWaitPresentations( result: ServiceDomainWaitResult, ): Presentations { return { + stdout: () => [], + json: () => result, + next: () => [], human: () => [ completed(`${result.hostname} is live at ${result.liveUrl}`), fields([ diff --git a/packages/cli/tests/auth.test.ts b/packages/cli/tests/auth.test.ts index bbb98dff..069c2c2d 100644 --- a/packages/cli/tests/auth.test.ts +++ b/packages/cli/tests/auth.test.ts @@ -373,7 +373,12 @@ describe("sessions held with none selected", () => { ok( ctx.present( { data: null }, - { human: () => [{ kind: "summary", status: "ok", text: "ran" }] }, + { + human: () => [{ kind: "summary", status: "ok", text: "ran" }], + stdout: () => [], + json: () => null, + next: () => [], + }, ), ), }); @@ -385,7 +390,12 @@ describe("sessions held with none selected", () => { return ok( ctx.present( { data: null }, - { human: () => [{ kind: "summary", status: "ok", text: "ran" }] }, + { + human: () => [{ kind: "summary", status: "ok", text: "ran" }], + stdout: () => [], + json: () => null, + next: () => [], + }, ), ); }, @@ -742,7 +752,17 @@ describe("the environment credential carries no refresh token", () => { help: { summary: "Issues one management API request" }, handler: async (_args, ctx) => { await ctx.api.GET("/v1/me", {}); - return ok(ctx.present({ data: null }, { human: () => [] })); + return ok( + ctx.present( + { data: null }, + { + human: () => [], + stdout: () => [], + json: () => null, + next: () => [], + }, + ), + ); }, }); diff --git a/packages/cli/tests/golden-rendering.test.ts b/packages/cli/tests/golden-rendering.test.ts index 6ed0468e..ad4d31fd 100644 --- a/packages/cli/tests/golden-rendering.test.ts +++ b/packages/cli/tests/golden-rendering.test.ts @@ -238,7 +238,17 @@ describe("the restored card matches the shell it replaced", () => { const show = defineCommand({ help: { summary: "Draw the card" }, handler: async (_args, ctx) => - ok(ctx.present({ data: null }, { human: () => [card] })), + ok( + ctx.present( + { data: null }, + { + human: () => [card], + stdout: () => [], + json: () => null, + next: () => [], + }, + ), + ), }); const result = await createTestCli({ commands: { show } }).run(["show"], { isTty: { stdout: true, stderr: true }, diff --git a/packages/cli/tests/mount-coverage.test.ts b/packages/cli/tests/mount-coverage.test.ts index 9024bf63..abfc7ebf 100644 --- a/packages/cli/tests/mount-coverage.test.ts +++ b/packages/cli/tests/mount-coverage.test.ts @@ -216,7 +216,17 @@ describe("the completeness comparisons report what is wrong", () => { defineCommand({ help: { summary }, handler: async (_args, ctx) => - ok(ctx.present({ data: null, exitCode: 0 }, { human: () => [] })), + ok( + ctx.present( + { data: null, exitCode: 0 }, + { + human: () => [], + stdout: () => [], + json: () => null, + next: () => [], + }, + ), + ), }); const kept = toy("kept"); diff --git a/packages/cli/tests/orm-mount.test.ts b/packages/cli/tests/orm-mount.test.ts index 9faa5272..cfa07f09 100644 --- a/packages/cli/tests/orm-mount.test.ts +++ b/packages/cli/tests/orm-mount.test.ts @@ -87,6 +87,29 @@ describe("the ORM family answers from the assembled tree", () => { }); }); + /** + * The same command in the other format, because the two formats call + * different presentation functions: json mode calls `json` and `next` + * and never touches `stdout`, human mode calls `human`, `stdout` and + * `next` and never touches `json`. A run in one format therefore + * proves nothing about the other. + * + * This exists because it was missed. #171 made the engine call every + * presentation a command declares, was checked against the json test + * above, and was reported safe for `stdout` on that basis — while + * orm-toolchain's `migration list` declares no `stdout` at all, so + * human mode would have exited 2 for every user at a terminal. + */ + it("runs migration list in human mode, which calls different presentations", async () => { + const result = await shell({ orm: ORM_SECTION }).run( + ["migration", "list", "--format", "human"], + { cwd: ORM_PROJECT_DIR }, + ); + + expect(result.exitCode).toBe(0); + expect(result.stderr).toContain("migration"); + }); + it("validates the family's config section before running a command", async () => { const result = await shell({}).run(["migration", "list", "--json"], { cwd: ORM_PROJECT_DIR, diff --git a/packages/cli/tests/whoami.test.ts b/packages/cli/tests/whoami.test.ts index a6700df8..65902e8d 100644 --- a/packages/cli/tests/whoami.test.ts +++ b/packages/cli/tests/whoami.test.ts @@ -48,7 +48,12 @@ const requiresCredentials = defineCommand({ ok( ctx.present( { data: null }, - { human: () => [{ kind: "summary", status: "ok", text: "ran" }] }, + { + human: () => [{ kind: "summary", status: "ok", text: "ran" }], + stdout: () => [], + json: () => null, + next: () => [], + }, ), ), needs: { credentials: true },