Skip to content
Merged
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
8 changes: 8 additions & 0 deletions .drive/projects/prisma-cli-v8/deferred.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
23 changes: 21 additions & 2 deletions packages/cli-engine/src/execution/command-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -63,7 +82,7 @@ function materializePresentation(
return {
human: [],
stdout: [],
json: presentations.json?.(),
json: presentations.json(),
next: presentations.next?.() ?? [],
};
}
Expand Down
5 changes: 1 addition & 4 deletions packages/cli-engine/src/execution/settlement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Comment thread
coderabbitai[bot] marked this conversation as resolved.
exitCode,
diagnostics: presented.diagnostics.map((diagnostic) =>
withDocsUrl(state, diagnostic),
Expand Down
22 changes: 12 additions & 10 deletions packages/cli-engine/src/presentation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,8 @@ export interface PresentedResult<T> {
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[];
Expand All @@ -54,16 +53,19 @@ export interface PresentedResult<T> {

/**
* 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[];
}

/**
Expand Down
2 changes: 2 additions & 0 deletions packages/cli-engine/src/telemetry/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ function statusPresentations(status: TelemetryStatus): Presentations {
],
stdout: () => [...formatTelemetryStatusLines(status)],
json: () => status,
next: () => [],
};
}

Expand All @@ -109,6 +110,7 @@ function consentPresentations(line: string, json: unknown): Presentations {
human: () => [{ kind: "summary", status: "ok", text: line }],
stdout: () => [line],
json: () => json,
next: () => [],
};
}

Expand Down
18 changes: 17 additions & 1 deletion packages/cli-engine/tests/blocks.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
[
Expand Down Expand Up @@ -323,6 +333,9 @@ describe("one character per meaning", () => {
human: () => [
{ kind: "summary", status: "error", text: "Failed." },
],
stdout: () => [],
json: () => null,
next: () => [],
},
),
);
Expand Down Expand Up @@ -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: () => [],
},
),
);
Expand Down
3 changes: 3 additions & 0 deletions packages/cli-engine/tests/clack-isolation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ function promptCommand(run: (prompt: PromptSurface) => Promise<unknown>) {
human: (): readonly Block[] => [
{ kind: "summary", status: "ok", text: `answer=${answer}` },
],
stdout: () => [],
json: () => ({ answer }),
next: () => [],
},
),
);
Expand Down
3 changes: 3 additions & 0 deletions packages/cli-engine/tests/clack-prompts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ function promptCli(run: (prompt: PromptSurface) => Promise<unknown>) {
text: `answer=${JSON.stringify(answer)}`,
},
],
stdout: () => [],
json: () => ({ answer }),
next: () => [],
},
),
);
Expand Down
126 changes: 112 additions & 14 deletions packages/cli-engine/tests/command-capabilities.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,17 @@ test("installsPackages alone puts ctx.packages on the context and nothing else",
expectTypeOf(ctx.packages).toEqualTypeOf<PackageOperations>();
// @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: () => [],
},
),
);
},
});

Expand All @@ -51,7 +61,12 @@ test("installsPackages alone puts ctx.packages on the context and nothing else",
expectTypeOf(ctx.packages).toEqualTypeOf<PackageOperations>();
// @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<typeof def.handler>();
});
Expand All @@ -64,7 +79,17 @@ test("managesCredentials alone puts ctx.credentialManager on the context and not
expectTypeOf(ctx.credentialManager).toEqualTypeOf<CredentialManager>();
// @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: () => [],
},
),
);
},
});

Expand All @@ -79,7 +104,12 @@ test("managesCredentials alone puts ctx.credentialManager on the context and not
expectTypeOf(ctx.credentialManager).toEqualTypeOf<CredentialManager>();
// @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<typeof def.handler>();
});
Expand All @@ -92,7 +122,17 @@ test("both capabilities declared: both surfaces, and the shared context intact",
handler: async (_args, ctx) => {
expectTypeOf(ctx.credentialManager).toEqualTypeOf<CredentialManager>();
expectTypeOf(ctx.packages).toEqualTypeOf<PackageOperations>();
return ok(ctx.present({ data: null }, { human: () => [] }));
return ok(
ctx.present(
{ data: null },
{
human: () => [],
stdout: () => [],
json: () => null,
next: () => [],
},
),
);
},
});

Expand All @@ -110,7 +150,12 @@ test("both capabilities declared: both surfaces, and the shared context intact",
const annotated: CommandHandler<typeof def> = async (_args, ctx) => {
expectTypeOf(ctx.credentialManager).toEqualTypeOf<CredentialManager>();
expectTypeOf(ctx.packages).toEqualTypeOf<PackageOperations>();
return ok(ctx.present({ data: null }, { human: () => [] }));
return ok(
ctx.present(
{ data: null },
{ human: () => [], stdout: () => [], json: () => null, next: () => [] },
),
);
};
expectTypeOf(annotated).toEqualTypeOf<typeof def.handler>();
});
Expand All @@ -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: () => [],
},
),
);
},
});

Expand All @@ -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<typeof def.handler>();
});
Expand Down Expand Up @@ -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: () => [],
},
),
);
},
});

Expand Down Expand Up @@ -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<PackageOperations>();
return ok(ctx.present({ data: null }, { human: () => [] }));
return ok(
ctx.present(
{ data: null },
{
human: () => [],
stdout: () => [],
json: () => null,
next: () => [],
},
),
);
},
});

Expand All @@ -227,9 +307,17 @@ test("the capability generics leave the exit-code catalogue alone", () => {
const annotated: CommandHandler<typeof def> = async (_args, ctx) => {
expectTypeOf(ctx.credentialManager).toEqualTypeOf<CredentialManager>();
expectTypeOf(ctx.packages).toEqualTypeOf<PackageOperations>();
// @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<typeof def.handler>();
});
Expand All @@ -243,7 +331,17 @@ test("maySpawn sits beside the capability flags without widening them", () => {
handler: async (_args, ctx) => {
expectTypeOf(ctx.credentialManager).toEqualTypeOf<CredentialManager>();
expectTypeOf(ctx.packages).toEqualTypeOf<PackageOperations>();
return ok(ctx.present({ data: null }, { human: () => [] }));
return ok(
ctx.present(
{ data: null },
{
human: () => [],
stdout: () => [],
json: () => null,
next: () => [],
},
),
);
},
});

Expand Down
Loading
Loading