From d2706bef9de4b5c9a8ea409b3402e71be174b841 Mon Sep 17 00:00:00 2001 From: willbot Date: Wed, 26 Aug 2026 15:30:40 +0200 Subject: [PATCH 1/3] One-time secrets print bare: the sensitive mask is removed Interactively, prisma postgres create showed its one-time connection URL nowhere: the human card masked it as ******** (sensitive: true), and since #172 the bare stdout mirror is skipped when stdout and stderr share a screen. The two rulings composed into a secret with no output path. Same hole in postgres connection create/rotate and bucket key create. Operator ruling (2026-08-26): the human card is where its owner reads a secret the command exists to hand over, so it prints bare. The sensitive flag is removed from the block type, the renderer, and both producers; the stdout mirror rules and --json are unchanged, so pipes still receive the clean data lines. Golden pins and the output-convention docs are updated to match. Co-Authored-By: Claude Fable 5 Signed-off-by: willbot Signed-off-by: Will Madden --- docs/product/cli-style-guide.md | 5 ++--- docs/product/output-conventions.md | 13 +++++------ .../cli-engine/src/execution/rendering.ts | 4 +--- packages/cli-engine/src/presentation.ts | 1 - packages/cli-engine/tests/blocks.test.ts | 6 ++--- packages/cli-engine/tests/execution.test.ts | 8 +++---- .../cli/src/commands/bucket/key-create.ts | 12 ++-------- .../cli/src/commands/postgres/presentation.ts | 9 +++----- packages/cli/tests/bucket.test.ts | 8 ++----- packages/cli/tests/golden-rendering.test.ts | 22 +++++++++---------- packages/cli/tests/postgres.test.ts | 2 -- 11 files changed, 33 insertions(+), 57 deletions(-) diff --git a/docs/product/cli-style-guide.md b/docs/product/cli-style-guide.md index 51149b70..d747b20d 100644 --- a/docs/product/cli-style-guide.md +++ b/docs/product/cli-style-guide.md @@ -94,7 +94,7 @@ Rules: - align keys in a compact column - use accent color for keys and default text for values - prefer display labels in default human output and keep opaque ids in JSON unless a later verbose mode explicitly asks for them -- mask sensitive values rather than omitting their presence entirely when the value matters to the flow +- print values bare; a secret the command exists to hand over is never masked, because the human card is where its owner reads it (operator ruling, 2026-08-26) - include only rows that are actually known for the current command - use human labels such as `Not linked` instead of internal resolution terms such as `unbound` - hide internal resolution terms such as `local pin` from default human output when the visible binding is clearer @@ -212,8 +212,7 @@ Non-TTY behavior should be automation-friendly: - Do not rely on color alone. - Keep text compact and translatable. -- Never print secrets. -- Scrub sensitive values in logs, errors, and previews. +- Never leak secrets into logs, errors, telemetry, or previews. A secret the command exists to hand over prints bare, once. ## Design Rule diff --git a/docs/product/output-conventions.md b/docs/product/output-conventions.md index 8f4efd1e..a3614ea2 100644 --- a/docs/product/output-conventions.md +++ b/docs/product/output-conventions.md @@ -186,16 +186,13 @@ No current MVP command uses `verify` or `inspect`, but new commands must still c ### One-Time Secret Output -Commands that create one-time-view secrets may write the raw secret value to -stdout in human mode. This is still machine-readable output, not decorative -human output. +Commands that create one-time-view secrets print the secret bare in the human card and write the raw value to stdout. The card is the only place an interactive user ever sees the secret — when stdout and stderr render to one screen the stdout mirror is skipped, so masking the card would hide the secret from everyone including its owner (operator ruling, 2026-08-26). The stdout line is machine-readable output for pipes and redirection. Rules: -- write exactly one raw secret value per successful create command -- write human creation summaries to stderr before writing the raw secret to stdout -- do not repeat the secret on stderr -- do not wrap the secret in labels such as `DATABASE_URL=` +- show the bare secret in the human card on stderr +- write exactly one raw secret value per successful create command to stdout +- do not wrap the stdout secret in labels such as `DATABASE_URL=` - use `--verbose` for human metadata such as resource ids; keep generated names and opaque ids out of default human output unless they are the user-selected target - `--quiet` suppresses successful human stderr output and still writes the raw secret to stdout - list and show commands must never print or return secret values @@ -270,7 +267,7 @@ Rules: - use a flat aligned key-value card with no bullets - keys use the accent color and values use the default foreground unless status coloring applies -- sensitive values are masked rather than omitted +- values print bare; a secret the command exists to hand over is never masked (operator ruling, 2026-08-26) - human output prefers display labels, URLs, and statuses over opaque ids #### `mutate` diff --git a/packages/cli-engine/src/execution/rendering.ts b/packages/cli-engine/src/execution/rendering.ts index c93aeda8..75537614 100644 --- a/packages/cli-engine/src/execution/rendering.ts +++ b/packages/cli-engine/src/execution/rendering.ts @@ -83,7 +83,6 @@ const STATUS_SYMBOL: Readonly> = { info: "ℹ", }; -const MASK = "********"; const COLUMN_GAP = " "; const RAIL = "│"; const BRANCH = "├─"; @@ -175,7 +174,6 @@ function writeFields( rows: ReadonlyArray<{ readonly label: Text; readonly value: Text; - readonly sensitive?: boolean; }>, rail: boolean, paint: Paint, @@ -183,7 +181,7 @@ function writeFields( ): void { const cells = rows.map((row) => ({ label: toned(extend(row.label, ":"), "heading"), - value: row.sensitive === true ? MASK : orPlaceholder(row.value), + value: orPlaceholder(row.value), })); const width = Math.max(0, ...cells.map((cell) => textWidth(cell.label))); const prefix = rail ? `${paint("structure", RAIL)}${COLUMN_GAP}` : ""; diff --git a/packages/cli-engine/src/presentation.ts b/packages/cli-engine/src/presentation.ts index 8c563d94..efa0f230 100644 --- a/packages/cli-engine/src/presentation.ts +++ b/packages/cli-engine/src/presentation.ts @@ -140,7 +140,6 @@ export type Block = readonly rows: ReadonlyArray<{ readonly label: Text; readonly value: Text; - readonly sensitive?: boolean; }>; /** Draws the dim `│` rail down the left of the card. A command * knows whether it is drawing a header card or a plain one, so diff --git a/packages/cli-engine/tests/blocks.test.ts b/packages/cli-engine/tests/blocks.test.ts index ac40e0d4..d5522c70 100644 --- a/packages/cli-engine/tests/blocks.test.ts +++ b/packages/cli-engine/tests/blocks.test.ts @@ -150,18 +150,18 @@ describe("fields", () => { ); }); - test("a sensitive value is masked and the mask is what gets aligned", async () => { + test("a credential value renders bare — the card is where a human reads it", async () => { expect( await render([ { kind: "fields", rows: [ - { label: "key", value: "AKIA", sensitive: true }, + { label: "key", value: "AKIA" }, { label: "endpoint", value: "https://s3.prisma.io" }, ], }, ]), - ).toBe("key: ********\nendpoint: https://s3.prisma.io\n"); + ).toBe("key: AKIA\nendpoint: https://s3.prisma.io\n"); }); }); diff --git a/packages/cli-engine/tests/execution.test.ts b/packages/cli-engine/tests/execution.test.ts index e6467183..2deb7677 100644 --- a/packages/cli-engine/tests/execution.test.ts +++ b/packages/cli-engine/tests/execution.test.ts @@ -735,7 +735,7 @@ describe("undocumented completion exit codes", () => { }); }); -describe("sensitive field rows", () => { +describe("credential field rows", () => { const reveal = defineCommand({ help: { summary: "Show a credential" }, handler: async (_args, ctx) => @@ -748,7 +748,7 @@ describe("sensitive field rows", () => { kind: "fields", rows: [ { label: "name", value: "deploy key" }, - { label: "token", value: "tok_secret", sensitive: true }, + { label: "token", value: "tok_secret" }, ], }, ], @@ -760,13 +760,13 @@ describe("sensitive field rows", () => { ), }); - test("human rendering masks a sensitive field value", async () => { + test("human rendering prints a credential value bare", async () => { const cli = createTestCli({ commands: { reveal }, now: EPOCH }); const result = await cli.run(["reveal", "--format", "human"]); expect(result.exitCode).toBe(0); expect(result.stdout).toBe(""); - expect(result.stderr).toBe("name: deploy key\ntoken: ********\n"); + expect(result.stderr).toBe("name: deploy key\ntoken: tok_secret\n"); }); test("the json result payload is the command's own and stays unmasked", async () => { diff --git a/packages/cli/src/commands/bucket/key-create.ts b/packages/cli/src/commands/bucket/key-create.ts index 5459f5cc..cac2795e 100644 --- a/packages/cli/src/commands/bucket/key-create.ts +++ b/packages/cli/src/commands/bucket/key-create.ts @@ -40,16 +40,8 @@ function createPresentations(result: BucketKeyCreateResult): Presentations { kind: "fields", rows: [ { label: "S3_ENDPOINT", value: result.endpoint }, - { - label: "S3_ACCESS_KEY_ID", - value: result.accessKeyId, - sensitive: true, - }, - { - label: "S3_SECRET_ACCESS_KEY", - value: result.secretAccessKey, - sensitive: true, - }, + { label: "S3_ACCESS_KEY_ID", value: result.accessKeyId }, + { label: "S3_SECRET_ACCESS_KEY", value: result.secretAccessKey }, { label: "S3_BUCKET", value: result.bucketName }, ], }, diff --git a/packages/cli/src/commands/postgres/presentation.ts b/packages/cli/src/commands/postgres/presentation.ts index 5aef8755..643b47f6 100644 --- a/packages/cli/src/commands/postgres/presentation.ts +++ b/packages/cli/src/commands/postgres/presentation.ts @@ -9,7 +9,6 @@ import type { export interface FieldRow { readonly label: string; readonly value: string; - readonly sensitive?: boolean; } /** Legacy `formatDatabaseTarget`. */ @@ -105,8 +104,8 @@ export function backupStdoutRows( ]); } -/** The one-time-secret card: the URL is masked in the human blocks - * and printed bare on stdout. */ +/** The one-time-secret card. The URL prints bare: this card is the only + * place an interactive user ever sees it. */ export function secretBlocks( headline: string, connectionString: string, @@ -119,9 +118,7 @@ export function secretBlocks( }, { kind: "fields", - rows: [ - { label: "connection URL", value: connectionString, sensitive: true }, - ], + rows: [{ label: "connection URL", value: connectionString }], }, ]; } diff --git a/packages/cli/tests/bucket.test.ts b/packages/cli/tests/bucket.test.ts index e75f4411..3d957550 100644 --- a/packages/cli/tests/bucket.test.ts +++ b/packages/cli/tests/bucket.test.ts @@ -741,12 +741,8 @@ describe("prisma bucket key create", () => { kind: "fields", rows: [ { label: "S3_ENDPOINT", value: "https://s3.prisma.io" }, - { - label: "S3_ACCESS_KEY_ID", - value: "AKIAEXAMPLE", - sensitive: true, - }, - { label: "S3_SECRET_ACCESS_KEY", value: "s3cr3t", sensitive: true }, + { label: "S3_ACCESS_KEY_ID", value: "AKIAEXAMPLE" }, + { label: "S3_SECRET_ACCESS_KEY", value: "s3cr3t" }, { label: "S3_BUCKET", value: "assets" }, ], }, diff --git a/packages/cli/tests/golden-rendering.test.ts b/packages/cli/tests/golden-rendering.test.ts index 201feb53..b31c0adf 100644 --- a/packages/cli/tests/golden-rendering.test.ts +++ b/packages/cli/tests/golden-rendering.test.ts @@ -1,7 +1,7 @@ /** * The sanctioned golden-rendering suite (S2 ruling: byte-exact pins * live here, one representative per rendering surface — card, table, - * error, masked secret). Every other test asserts semantically + * error, one-time secret). Every other test asserts semantically * (envelope / presented / events / exit code); when the engine's * rendering style changes deliberately, THIS file is the one place the * new bytes get re-pinned. The S1 whoami byte pins in @@ -120,12 +120,12 @@ describe("golden rendering", () => { }); /** - * What the mask is and is not: the card writes `********` to stderr - * while stdout prints the same secret in the clear a line later, - * because printing it is how the caller receives it. It is a - * scroll-back and screen-share courtesy, not containment. + * The card carries the bare secret: when stdout and stderr share a + * screen the stdout mirror is skipped, so the card is the only place + * an interactive user ever sees a one-time credential (operator + * ruling, 2026-08-26). */ - it("masked secret (representative: bucket key create)", async () => { + it("one-time secret card (representative: bucket key create)", async () => { const result = await makeCli( [record("ws_1", "Acme Inc")], "ws_1", @@ -140,8 +140,8 @@ describe("golden rendering", () => { "- Set these environment variables to use this bucket:\n" + "\n" + "S3_ENDPOINT: https://s3.prisma.io\n" + - "S3_ACCESS_KEY_ID: ********\n" + - "S3_SECRET_ACCESS_KEY: ********\n" + + "S3_ACCESS_KEY_ID: AKIAEXAMPLE\n" + + "S3_SECRET_ACCESS_KEY: s3cr3t\n" + "S3_BUCKET: assets\n", ); expect(result.stdout).toBe( @@ -174,7 +174,7 @@ describe("golden rendering", () => { * The cases above run with a non-terminal stderr and stay plain; these * two are the same surfaces with a terminal stderr. */ - it("coloured card and mask (representative: bucket key create)", async () => { + it("coloured card (representative: bucket key create)", async () => { const result = await makeCli( [record("ws_1", "Acme Inc")], "ws_1", @@ -190,8 +190,8 @@ describe("golden rendering", () => { "- Set these environment variables to use this bucket:\n" + "\n" + "\u001b[36mS3_ENDPOINT: \u001b[39m https://s3.prisma.io\n" + - "\u001b[36mS3_ACCESS_KEY_ID: \u001b[39m ********\n" + - "\u001b[36mS3_SECRET_ACCESS_KEY:\u001b[39m ********\n" + + "\u001b[36mS3_ACCESS_KEY_ID: \u001b[39m AKIAEXAMPLE\n" + + "\u001b[36mS3_SECRET_ACCESS_KEY:\u001b[39m s3cr3t\n" + "\u001b[36mS3_BUCKET: \u001b[39m assets\n", ); }); diff --git a/packages/cli/tests/postgres.test.ts b/packages/cli/tests/postgres.test.ts index d4aa9b98..11d0cee2 100644 --- a/packages/cli/tests/postgres.test.ts +++ b/packages/cli/tests/postgres.test.ts @@ -736,7 +736,6 @@ describe("prisma postgres create", () => { { label: "connection URL", value: "postgres://user:pass@host/db", - sensitive: true, }, ], }, @@ -2105,7 +2104,6 @@ describe("prisma postgres connection create", () => { { label: "connection URL", value: "postgres://pooled/db", - sensitive: true, }, ], }); From cd5f921457c39a0bbe7f9a833b7487f41a38ba27 Mon Sep 17 00:00:00 2001 From: willbot Date: Wed, 26 Aug 2026 15:33:57 +0200 Subject: [PATCH 2/3] The changed engine ships as 0.4.0 Dropping the block type's sensitive field is the pre-1.0 breaking bump. Co-Authored-By: Claude Fable 5 Signed-off-by: willbot Signed-off-by: Will Madden --- packages/cli-engine/package.json | 2 +- packages/cli/package.json | 2 +- packages/prisma/package.json | 2 +- pnpm-lock.yaml | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/cli-engine/package.json b/packages/cli-engine/package.json index dc6b436e..da5df478 100644 --- a/packages/cli-engine/package.json +++ b/packages/cli-engine/package.json @@ -1,6 +1,6 @@ { "name": "@prisma/cli-engine", - "version": "0.3.0", + "version": "0.4.0", "description": "The execution engine of the unified Prisma CLI.", "type": "module", "exports": { diff --git a/packages/cli/package.json b/packages/cli/package.json index 33534cd4..71b9b7b5 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -49,7 +49,7 @@ }, "dependencies": { "@manypkg/tools": "^2.1.2", - "@prisma/cli-engine": "workspace:0.3.0", + "@prisma/cli-engine": "workspace:0.4.0", "@prisma/composer-cli": "0.16.0", "@prisma/compute-sdk": "0.42.0", "@prisma/management-api-sdk": "1.69.0", diff --git a/packages/prisma/package.json b/packages/prisma/package.json index ce3b357c..d8396f86 100644 --- a/packages/prisma/package.json +++ b/packages/prisma/package.json @@ -49,7 +49,7 @@ }, "dependencies": { "@manypkg/tools": "^2.1.2", - "@prisma/cli-engine": "workspace:0.3.0", + "@prisma/cli-engine": "workspace:0.4.0", "@prisma/composer-cli": "0.16.0", "@prisma/compute-sdk": "0.42.0", "@prisma/management-api-sdk": "1.69.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a92a0bfb..51cf74de 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -27,7 +27,7 @@ importers: specifier: ^2.1.2 version: 2.1.2 '@prisma/cli-engine': - specifier: workspace:0.3.0 + specifier: workspace:0.4.0 version: link:../cli-engine '@prisma/composer-cli': specifier: 0.16.0 @@ -201,7 +201,7 @@ importers: specifier: ^2.1.2 version: 2.1.2 '@prisma/cli-engine': - specifier: workspace:0.3.0 + specifier: workspace:0.4.0 version: link:../cli-engine '@prisma/composer-cli': specifier: 0.16.0 From fc2520c5e4d2feccd5bb96289bfc3aad203c550c Mon Sep 17 00:00:00 2001 From: willbot Date: Wed, 26 Aug 2026 15:44:39 +0200 Subject: [PATCH 3/3] The engine stays at 0.3.0: only the producers stop masking Published composer-cli and orm-toolchain peer on the exact engine version, so any engine bump fails tarball conformance until the products re-release against it. The engine keeps its sensitive field, unused; the CLI's own commands stop passing it, which is what fixes the bug. Deleting the dead flag rides the next engine release. Co-Authored-By: Claude Fable 5 Signed-off-by: willbot Signed-off-by: Will Madden --- packages/cli-engine/package.json | 2 +- packages/cli-engine/src/execution/rendering.ts | 4 +++- packages/cli-engine/src/presentation.ts | 1 + packages/cli-engine/tests/blocks.test.ts | 6 +++--- packages/cli-engine/tests/execution.test.ts | 8 ++++---- packages/cli/package.json | 2 +- packages/prisma/package.json | 2 +- pnpm-lock.yaml | 4 ++-- 8 files changed, 16 insertions(+), 13 deletions(-) diff --git a/packages/cli-engine/package.json b/packages/cli-engine/package.json index da5df478..dc6b436e 100644 --- a/packages/cli-engine/package.json +++ b/packages/cli-engine/package.json @@ -1,6 +1,6 @@ { "name": "@prisma/cli-engine", - "version": "0.4.0", + "version": "0.3.0", "description": "The execution engine of the unified Prisma CLI.", "type": "module", "exports": { diff --git a/packages/cli-engine/src/execution/rendering.ts b/packages/cli-engine/src/execution/rendering.ts index 75537614..c93aeda8 100644 --- a/packages/cli-engine/src/execution/rendering.ts +++ b/packages/cli-engine/src/execution/rendering.ts @@ -83,6 +83,7 @@ const STATUS_SYMBOL: Readonly> = { info: "ℹ", }; +const MASK = "********"; const COLUMN_GAP = " "; const RAIL = "│"; const BRANCH = "├─"; @@ -174,6 +175,7 @@ function writeFields( rows: ReadonlyArray<{ readonly label: Text; readonly value: Text; + readonly sensitive?: boolean; }>, rail: boolean, paint: Paint, @@ -181,7 +183,7 @@ function writeFields( ): void { const cells = rows.map((row) => ({ label: toned(extend(row.label, ":"), "heading"), - value: orPlaceholder(row.value), + value: row.sensitive === true ? MASK : orPlaceholder(row.value), })); const width = Math.max(0, ...cells.map((cell) => textWidth(cell.label))); const prefix = rail ? `${paint("structure", RAIL)}${COLUMN_GAP}` : ""; diff --git a/packages/cli-engine/src/presentation.ts b/packages/cli-engine/src/presentation.ts index efa0f230..8c563d94 100644 --- a/packages/cli-engine/src/presentation.ts +++ b/packages/cli-engine/src/presentation.ts @@ -140,6 +140,7 @@ export type Block = readonly rows: ReadonlyArray<{ readonly label: Text; readonly value: Text; + readonly sensitive?: boolean; }>; /** Draws the dim `│` rail down the left of the card. A command * knows whether it is drawing a header card or a plain one, so diff --git a/packages/cli-engine/tests/blocks.test.ts b/packages/cli-engine/tests/blocks.test.ts index d5522c70..ac40e0d4 100644 --- a/packages/cli-engine/tests/blocks.test.ts +++ b/packages/cli-engine/tests/blocks.test.ts @@ -150,18 +150,18 @@ describe("fields", () => { ); }); - test("a credential value renders bare — the card is where a human reads it", async () => { + test("a sensitive value is masked and the mask is what gets aligned", async () => { expect( await render([ { kind: "fields", rows: [ - { label: "key", value: "AKIA" }, + { label: "key", value: "AKIA", sensitive: true }, { label: "endpoint", value: "https://s3.prisma.io" }, ], }, ]), - ).toBe("key: AKIA\nendpoint: https://s3.prisma.io\n"); + ).toBe("key: ********\nendpoint: https://s3.prisma.io\n"); }); }); diff --git a/packages/cli-engine/tests/execution.test.ts b/packages/cli-engine/tests/execution.test.ts index 2deb7677..e6467183 100644 --- a/packages/cli-engine/tests/execution.test.ts +++ b/packages/cli-engine/tests/execution.test.ts @@ -735,7 +735,7 @@ describe("undocumented completion exit codes", () => { }); }); -describe("credential field rows", () => { +describe("sensitive field rows", () => { const reveal = defineCommand({ help: { summary: "Show a credential" }, handler: async (_args, ctx) => @@ -748,7 +748,7 @@ describe("credential field rows", () => { kind: "fields", rows: [ { label: "name", value: "deploy key" }, - { label: "token", value: "tok_secret" }, + { label: "token", value: "tok_secret", sensitive: true }, ], }, ], @@ -760,13 +760,13 @@ describe("credential field rows", () => { ), }); - test("human rendering prints a credential value bare", async () => { + test("human rendering masks a sensitive field value", async () => { const cli = createTestCli({ commands: { reveal }, now: EPOCH }); const result = await cli.run(["reveal", "--format", "human"]); expect(result.exitCode).toBe(0); expect(result.stdout).toBe(""); - expect(result.stderr).toBe("name: deploy key\ntoken: tok_secret\n"); + expect(result.stderr).toBe("name: deploy key\ntoken: ********\n"); }); test("the json result payload is the command's own and stays unmasked", async () => { diff --git a/packages/cli/package.json b/packages/cli/package.json index 71b9b7b5..33534cd4 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -49,7 +49,7 @@ }, "dependencies": { "@manypkg/tools": "^2.1.2", - "@prisma/cli-engine": "workspace:0.4.0", + "@prisma/cli-engine": "workspace:0.3.0", "@prisma/composer-cli": "0.16.0", "@prisma/compute-sdk": "0.42.0", "@prisma/management-api-sdk": "1.69.0", diff --git a/packages/prisma/package.json b/packages/prisma/package.json index d8396f86..ce3b357c 100644 --- a/packages/prisma/package.json +++ b/packages/prisma/package.json @@ -49,7 +49,7 @@ }, "dependencies": { "@manypkg/tools": "^2.1.2", - "@prisma/cli-engine": "workspace:0.4.0", + "@prisma/cli-engine": "workspace:0.3.0", "@prisma/composer-cli": "0.16.0", "@prisma/compute-sdk": "0.42.0", "@prisma/management-api-sdk": "1.69.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 51cf74de..a92a0bfb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -27,7 +27,7 @@ importers: specifier: ^2.1.2 version: 2.1.2 '@prisma/cli-engine': - specifier: workspace:0.4.0 + specifier: workspace:0.3.0 version: link:../cli-engine '@prisma/composer-cli': specifier: 0.16.0 @@ -201,7 +201,7 @@ importers: specifier: ^2.1.2 version: 2.1.2 '@prisma/cli-engine': - specifier: workspace:0.4.0 + specifier: workspace:0.3.0 version: link:../cli-engine '@prisma/composer-cli': specifier: 0.16.0