From ff4d06867f1c20c84592681f2af429d1ed64a2c7 Mon Sep 17 00:00:00 2001 From: willbot Date: Tue, 13 Oct 2026 16:11:12 +0200 Subject: [PATCH 1/8] test(engine): a command path can also be a group with child commands Stand-in dev, dev url and dev stop commands. These fail until the engine accepts the shape. Co-Authored-By: Claude Opus 5.5 Signed-off-by: willbot Signed-off-by: Will Madden --- .../cli-engine/tests/command-groups.test.ts | 304 ++++++++++++++++++ 1 file changed, 304 insertions(+) create mode 100644 packages/cli-engine/tests/command-groups.test.ts diff --git a/packages/cli-engine/tests/command-groups.test.ts b/packages/cli-engine/tests/command-groups.test.ts new file mode 100644 index 00000000..7b2356fa --- /dev/null +++ b/packages/cli-engine/tests/command-groups.test.ts @@ -0,0 +1,304 @@ +/** + * A path that is both a command and a group: `dev` runs with an entry, + * and `dev url` and `dev stop` are its children. A child's name wins + * over a positional spelled the same. + */ +import { + defineCommand, + defineCommandFamily, + flag, + type MountedTree, + positional, + type RedirectSpec, +} from "@prisma/cli-engine"; +import { ok } from "@prisma/cli-engine/protocol"; +import { createTestCli, type TestCli } from "@prisma/cli-engine/testing"; +import { describe, expect, test } from "vitest"; + +function presenting(ran: string, data: Record) { + return { ran, ...data }; +} + +const dev = defineCommand({ + help: { + summary: "Run the app locally", + description: "Starts the entry module and watches it for changes.", + examples: ["dev module.ts"], + }, + args: { + flags: { port: flag.number({ brief: "Port to listen on" }) }, + positionals: { + entry: positional.optionalString({ + brief: "The module to run", + placeholder: "entry", + }), + }, + }, + statements: { delete: { arity: 1, brief: "Delete a resource: --delete X" } }, + handler: async (args, ctx) => { + const data = presenting("dev", { + entry: args.positionals.entry, + port: args.flags.port, + deleted: ctx.statements.take("delete").map((answer) => answer.text), + }); + return ok( + ctx.present( + { data }, + { + human: () => [], + stdout: () => [], + json: () => data, + next: () => [], + }, + ), + ); + }, +}); + +const devUrl = defineCommand({ + help: { summary: "Print the local URL of a running service" }, + args: { + positionals: { + service: positional.optionalString({ + brief: "The service to print", + placeholder: "service", + }), + }, + }, + handler: async (args, ctx) => { + const data = presenting("dev url", { service: args.positionals.service }); + return ok( + ctx.present( + { data }, + { + human: () => [], + stdout: () => [], + json: () => data, + next: () => [], + }, + ), + ); + }, +}); + +const devStop = defineCommand({ + help: { summary: "Stop the running app" }, + handler: async (_args, ctx) => { + const data = presenting("dev stop", {}); + return ok( + ctx.present( + { data }, + { + human: () => [], + stdout: () => [], + json: () => data, + next: () => [], + }, + ), + ); + }, +}); + +const DEV_GROUP = { + dev: { brief: "Run the app locally. Start it, print URLs, stop it" }, +}; + +function devCli( + commands: MountedTree = { + dev, + "dev url": devUrl, + "dev stop": devStop, + }, + redirects: readonly RedirectSpec[] = [], +): TestCli { + return createTestCli({ + commandFamilies: [defineCommandFamily({ commands, redirects })], + commands, + groups: DEV_GROUP, + }); +} + +function lastEnvelope(result: Awaited>) { + const last = result.json[result.json.length - 1]; + if (last.kind !== "result") { + throw new Error("expected a result frame"); + } + return last.envelope; +} + +describe("routing", () => { + test("the path's own command runs with a positional", async () => { + const result = await devCli().run(["dev", "module.ts", "--json"]); + + expect(result.exitCode).toBe(0); + expect(result.presented?.data).toEqual({ + ran: "dev", + entry: "module.ts", + port: undefined, + deleted: [], + }); + expect(lastEnvelope(result).commandId).toBe("dev"); + }); + + test("the path's own command runs with no arguments instead of printing help", async () => { + const result = await devCli().run(["dev"]); + + expect(result.exitCode).toBe(0); + expect(result.presented?.data).toMatchObject({ ran: "dev" }); + }); + + test("the path's own command runs with only flags", async () => { + const result = await devCli().run(["dev", "--port", "3000", "--json"]); + + expect(result.exitCode).toBe(0); + expect(result.presented?.data).toMatchObject({ ran: "dev", port: 3000 }); + }); + + test("a child runs", async () => { + const result = await devCli().run(["dev", "url", "gateway", "--json"]); + + expect(result.exitCode).toBe(0); + expect(result.presented?.data).toEqual({ + ran: "dev url", + service: "gateway", + }); + expect(lastEnvelope(result).commandId).toBe("dev.url"); + }); + + test("a child's name wins over a positional spelled the same", async () => { + const result = await devCli().run(["dev", "url", "--json"]); + + expect(result.exitCode).toBe(0); + expect(result.presented?.data).toEqual({ + ran: "dev url", + service: undefined, + }); + }); + + test("routing does not depend on the order the paths were mounted in", async () => { + const result = await devCli({ + "dev url": devUrl, + "dev stop": devStop, + dev, + }).run(["dev", "module.ts", "--json"]); + + expect(result.presented?.data).toMatchObject({ + ran: "dev", + entry: "module.ts", + }); + }); + + test("the path's own command takes its statement flags", async () => { + const result = await devCli().run([ + "dev", + "module.ts", + "--delete", + "Legacy", + "--json", + ]); + + expect(result.exitCode).toBe(0); + expect(result.presented?.data).toMatchObject({ deleted: ["Legacy"] }); + }); + + test("an unknown flag on the path's own command is a usage error for that command", async () => { + const result = await devCli().run(["dev", "--frobnicate", "--json"]); + + expect(result.exitCode).toBe(2); + const envelope = lastEnvelope(result); + expect(envelope.commandId).toBe("dev"); + expect(!envelope.ok && envelope.error.code).toBe("CLI.INVALID_ARGUMENTS"); + }); + + test("a retired flag on the path's own command names its replacement", async () => { + const result = await devCli(undefined, [ + { from: "dev", flag: "listen", replacement: "dev --port 3000" }, + ]).run(["dev", "--listen", "3000", "--json"]); + + expect(result.exitCode).toBe(2); + const envelope = lastEnvelope(result); + expect(!envelope.ok && envelope.error.code).toBe("CLI.COMMAND_MOVED"); + }); +}); + +describe("help", () => { + test("--help on the path shows its command's usage and lists the children", async () => { + const result = await devCli().run(["dev", "--help", "--no-color"]); + + expect(result.exitCode).toBe(0); + expect(result.stdout).toBe( + "prisma-test dev → Run the app locally\n\n│ Usage\n│ $ prisma-test dev [options] [entry]\n│\n│ Starts the entry module and watches it for changes.\n│\n│ Commands\n│ url [service] Print the local URL of a running service\n│ stop Stop the running app\n│\n│ Arguments\n│ entry The module to run (optional)\n│\n│ Options\n│ --port Port to listen on\n│ --delete ... Delete a resource: --delete X\n│\n│ Global options also apply: --format, --json, --log-level, --verbose,\n│ --quiet, --yes, --confirm, --interactive, --color, --config. Run\n│ 'prisma-test --help' for details.\n│\n│ Examples\n│ $ prisma-test dev module.ts\n\n", + ); + }); + + test("--help after a positional shows the path's own help", async () => { + const withPositional = await devCli().run([ + "dev", + "module.ts", + "--help", + "--no-color", + ]); + const bare = await devCli().run(["dev", "--help", "--no-color"]); + + expect(withPositional.stdout).toBe(bare.stdout); + }); + + test("--help on a child shows the child's help", async () => { + const result = await devCli().run(["dev", "url", "--help", "--no-color"]); + + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain( + "prisma-test dev url → Print the local URL of a running service", + ); + expect(result.stdout).toContain("$ prisma-test dev url [options] [service]"); + }); + + test("Markdown help lists the children under the usage", async () => { + const result = await devCli().run(["dev", "--help", "--format", "markdown"]); + + expect(result.stdout).toContain( + "## Usage\n\n```bash\nprisma-test dev [options] [entry]\n```", + ); + expect(result.stdout).toContain( + "## Commands\n\n| Command | Description |\n| --- | --- |\n| `url [service]` | Print the local URL of a running service |\n| `stop` | Stop the running app |", + ); + }); + + test("the parent card lists the path once, with its group brief", async () => { + const result = await devCli().run(["--help", "--no-color"]); + + expect(result.stdout).toContain( + "│ dev Run the app locally. Start it, print URLs, stop it\n", + ); + expect(result.stdout.match(/│ {2}dev /g)).toHaveLength(1); + }); +}); + +describe("construction", () => { + test("the path still needs a group declaration", () => { + expect(() => + createTestCli({ commands: { dev, "dev url": devUrl } }), + ).toThrow("unknown group 'dev'"); + }); + + test("the path's own command is still validated", () => { + const offender = defineCommand({ + help: { summary: "Offender" }, + args: { flags: { json: flag.boolean({ brief: "mine" }) } }, + handler: null as never, + }); + + expect(() => + createTestCli({ + commands: { dev: offender, "dev url": devUrl }, + groups: DEV_GROUP, + }), + ).toThrow("command 'dev' declares reserved flag 'json'"); + }); + + test("a verb redirect onto the path collides with its command", () => { + expect(() => + devCli(undefined, [{ from: "dev", replacement: "run" }]), + ).toThrow("redirect 'dev' collides with a mounted command"); + }); +}); From 0dcb619e99936e488c0db978de31ca7b1cfbf18f Mon Sep 17 00:00:00 2001 From: willbot Date: Tue, 13 Oct 2026 16:14:13 +0200 Subject: [PATCH 2/8] feat(engine): a command path can also be a group with child commands createCli accepted 'dev url' only if nothing was mounted at 'dev'. Now 'dev' can be a command too: 'dev module.ts' runs it, 'dev url' runs the child, and a child's name wins over a positional spelled the same. 'dev --help' shows the command's help and lists the children. The group's own command sits on the group's node in the command tree, and Stricli routes to it as the route map's default command. Co-Authored-By: Claude Opus 5.5 Signed-off-by: willbot Signed-off-by: Will Madden --- .../cli-engine/src/execution/command-tree.ts | 66 +++++++++++++------ packages/cli-engine/src/execution/help.ts | 34 ++++++++-- .../src/execution/statement-flags.ts | 9 +-- .../src/execution/stricli-adapter.ts | 21 +++++- .../cli-engine/tests/command-groups.test.ts | 14 ++-- packages/cli-engine/tests/engine.test.ts | 9 --- 6 files changed, 105 insertions(+), 48 deletions(-) diff --git a/packages/cli-engine/src/execution/command-tree.ts b/packages/cli-engine/src/execution/command-tree.ts index 7f276484..da91f6c2 100644 --- a/packages/cli-engine/src/execution/command-tree.ts +++ b/packages/cli-engine/src/execution/command-tree.ts @@ -132,45 +132,62 @@ export interface CommandTreeEntry { } export interface CommandTreeNode { + /** The command mounted at this group's own path, such as `dev` beside + * `dev url`. It runs when the next argument names no child. */ + command: CommandTreeEntry | undefined; readonly commands: Map; readonly children: Map; } function emptyNode(): CommandTreeNode { - return { commands: new Map(), children: new Map() }; + return { command: undefined, commands: new Map(), children: new Map() }; } +function childNode(node: CommandTreeNode, segment: string): CommandTreeNode { + let child = node.children.get(segment); + if (child === undefined) { + child = emptyNode(); + node.children.set(segment, child); + } + return child; +} + +/** Every path some other mounted path extends: `dev` when `dev url` is + * mounted. */ +function groupPathsOf(paths: readonly string[]): ReadonlySet { + const groups = new Set(); + for (const path of paths) { + const segments = path.split(" "); + for (let depth = 1; depth < segments.length; depth += 1) { + groups.add(segments.slice(0, depth).join(" ")); + } + } + return groups; +} + +/** A path that is also a group keeps its command on the group's node, + * so the node's children and its own command meet in one place. */ function insertCommand( root: CommandTreeNode, path: string, def: AnyCommand, docsBaseUrl: string | undefined, + isGroup: boolean, ): void { const segments = path.split(" "); if (segments.some((segment) => segment.length === 0)) { throw constructionError(`invalid command path '${path}'`); } - let node = root; - for (const segment of segments.slice(0, -1)) { - if (node.commands.has(segment)) { - throw constructionError( - `command path '${path}' collides with a command mounted at a prefix of it`, - ); - } - let child = node.children.get(segment); - if (child === undefined) { - child = emptyNode(); - node.children.set(segment, child); - } - node = child; - } + const entry = { def, id: segments.join("."), docsBaseUrl }; + const parent = segments + .slice(0, -1) + .reduce((node, segment) => childNode(node, segment), root); const leaf = segments[segments.length - 1]; - if (node.commands.has(leaf) || node.children.has(leaf)) { - throw constructionError( - `command path '${path}' collides with another mounted command`, - ); + if (isGroup) { + childNode(parent, leaf).command = entry; + } else { + parent.commands.set(leaf, entry); } - node.commands.set(leaf, { def, id: segments.join("."), docsBaseUrl }); } /** A command belongs to the family whose commands record contains it @@ -253,6 +270,7 @@ export function buildCommandTree(spec: EngineSpec): CommandTreeNode { } validateDocsBaseUrls(spec); validateConfigSectionNames(spec); + const groupPaths = groupPathsOf(paths); const root = emptyNode(); for (const path of paths) { const def = spec.commands[path]; @@ -271,7 +289,13 @@ export function buildCommandTree(spec: EngineSpec): CommandTreeNode { ); } } - insertCommand(root, path, def, commandFamilyOf(spec, def)?.docsBaseUrl); + insertCommand( + root, + path, + def, + commandFamilyOf(spec, def)?.docsBaseUrl, + groupPaths.has(path), + ); } return root; } diff --git a/packages/cli-engine/src/execution/help.ts b/packages/cli-engine/src/execution/help.ts index d1aac3e2..e5ba4272 100644 --- a/packages/cli-engine/src/execution/help.ts +++ b/packages/cli-engine/src/execution/help.ts @@ -86,10 +86,16 @@ function helpPath(argv: readonly string[]): readonly string[] { type HelpTarget = | { readonly kind: "node"; readonly node: CommandTreeNode } - | { readonly kind: "leaf"; readonly entry: CommandTreeEntry }; + | { + readonly kind: "leaf"; + readonly entry: CommandTreeEntry; + /** Set when the command's path is also a group. */ + readonly children: CommandTreeNode | undefined; + }; /** Walks as far as the segments stay recognized; help for `project - * frobnicate` is project's help, not a dead end. */ + * frobnicate` is project's help, not a dead end. A group with its own + * command answers with that command's help. */ function resolveTarget( root: CommandTreeNode, segments: readonly string[], @@ -99,7 +105,10 @@ function resolveTarget( for (const segment of segments) { const entry = node.commands.get(segment); if (entry !== undefined) { - return { target: { kind: "leaf", entry }, path: [...path, segment] }; + return { + target: { kind: "leaf", entry, children: undefined }, + path: [...path, segment], + }; } const child = node.children.get(segment); if (child === undefined) { @@ -108,14 +117,21 @@ function resolveTarget( node = child; path.push(segment); } + if (node.command !== undefined) { + return { + target: { kind: "leaf", entry: node.command, children: node }, + path, + }; + } return { target: { kind: "node", node }, path }; } /** A BARE group invocation (`prisma-cli project`, or no argv at all) * is a help request; anything carrying flags or extra tokens is not — * `cli --unknown` and `cli project --frobnicate` must reach routing - * and usage validation, not exit 0 with a help card. A bare leaf is a - * command run and is left alone. The format-selection flags do not + * and usage validation, not exit 0 with a help card. A bare leaf, or + * a bare group with its own command, is a command run and is left + * alone. The format-selection flags do not * count: `cli project --format markdown` asks for the group's help in * Markdown. */ export function bareGroupInvocation( @@ -197,7 +213,7 @@ export function helpCard( ): HelpCard { const { target, path } = resolveTarget(root, helpPath(argv)); return target.kind === "leaf" - ? leafCard(spec, target.entry, path) + ? leafCard(spec, target.entry, target.children, path) : nodeCard(spec, target.node, path); } @@ -234,6 +250,7 @@ function nodeCard( function leafCard( spec: EngineSpec, entry: CommandTreeEntry, + children: CommandTreeNode | undefined, path: readonly string[], ): HelpCard { const def = entry.def; @@ -255,7 +272,7 @@ function leafCard( tagline: def.help.summary, usage, description: def.help.description, - commands: [], + commands: children === undefined ? [] : nodeRows(spec, children, path), workflow: [], arguments: Object.values>(def.args.positionals) .map((spec) => positionalRuntime(spec)) @@ -311,6 +328,9 @@ function renderHelpTerminal(card: HelpCard, paint: Paint): string { lines.push(rail(paint)); proseLines(card.description, paint, lines); } + if (card.kind === "leaf") { + rowSection("Commands", card.commands, paint, lines); + } workflowLines(card.workflow, paint, lines); rowSection("Arguments", card.arguments, paint, lines); rowSection("Options", card.options, paint, lines); diff --git a/packages/cli-engine/src/execution/statement-flags.ts b/packages/cli-engine/src/execution/statement-flags.ts index a3762fb7..693c4f81 100644 --- a/packages/cli-engine/src/execution/statement-flags.ts +++ b/packages/cli-engine/src/execution/statement-flags.ts @@ -46,8 +46,9 @@ export interface StatementFlagValue { } /** The command argv routes to, found the way the parser routes: the - * leading words, group by group, until one names a command. Like the - * parser, a kebab-case word matches a camelCase name. */ + * leading words, group by group, until one names a command. A group + * with its own command routes to it at the first word that names no + * child. Like the parser, a kebab-case word matches a camelCase name. */ export function routedCommand( tree: CommandTreeNode, argv: readonly string[], @@ -62,11 +63,11 @@ export function routedCommand( const child = node.children.get(token) ?? node.children.get(camelCase(token)); if (child === undefined) { - return undefined; + return node.command; } node = child; } - return undefined; + return node.command; } function statementFlagIn( diff --git a/packages/cli-engine/src/execution/stricli-adapter.ts b/packages/cli-engine/src/execution/stricli-adapter.ts index 8fb440d1..667e9598 100644 --- a/packages/cli-engine/src/execution/stricli-adapter.ts +++ b/packages/cli-engine/src/execution/stricli-adapter.ts @@ -239,6 +239,11 @@ function toStricliCommand( }); } +/** The route a group's own command sits under, hidden from listings. + * A child's name is one path segment and never contains a space, so no + * child can take this name. */ +const OWN_COMMAND_ROUTE = ""; + export function buildRoutes( spec: EngineSpec, node: CommandTreeNode, @@ -246,6 +251,13 @@ export function buildRoutes( runEntry: RunEntry, ): StricliRouteMap { const routes: Record = {}; + if (node.command !== undefined) { + routes[OWN_COMMAND_ROUTE] = toStricliCommand( + node.command, + spec.name, + runEntry, + ); + } for (const [name, entry] of node.commands) { routes[name] = toStricliCommand(entry, spec.name, runEntry); } @@ -260,7 +272,14 @@ export function buildRoutes( brief: spec.groups[groupPath].brief, fullDescription: spec.groups[groupPath].description, }; - return buildRouteMap({ routes, docs }); + if (node.command === undefined) { + return buildRouteMap({ routes, docs }); + } + return buildRouteMap({ + routes, + defaultCommand: OWN_COMMAND_ROUTE, + docs: { ...docs, hideRoute: { [OWN_COMMAND_ROUTE]: true } }, + }); } export function capturingText(state: RunState): ApplicationText { diff --git a/packages/cli-engine/tests/command-groups.test.ts b/packages/cli-engine/tests/command-groups.test.ts index 7b2356fa..7d8a3666 100644 --- a/packages/cli-engine/tests/command-groups.test.ts +++ b/packages/cli-engine/tests/command-groups.test.ts @@ -118,6 +118,8 @@ function devCli( }); } +const TERMINAL = { isTty: { stdout: true } }; + function lastEnvelope(result: Awaited>) { const last = result.json[result.json.length - 1]; if (last.kind !== "result") { @@ -223,11 +225,11 @@ describe("routing", () => { describe("help", () => { test("--help on the path shows its command's usage and lists the children", async () => { - const result = await devCli().run(["dev", "--help", "--no-color"]); + const result = await devCli().run(["dev", "--help", "--no-color"], TERMINAL); expect(result.exitCode).toBe(0); expect(result.stdout).toBe( - "prisma-test dev → Run the app locally\n\n│ Usage\n│ $ prisma-test dev [options] [entry]\n│\n│ Starts the entry module and watches it for changes.\n│\n│ Commands\n│ url [service] Print the local URL of a running service\n│ stop Stop the running app\n│\n│ Arguments\n│ entry The module to run (optional)\n│\n│ Options\n│ --port Port to listen on\n│ --delete ... Delete a resource: --delete X\n│\n│ Global options also apply: --format, --json, --log-level, --verbose,\n│ --quiet, --yes, --confirm, --interactive, --color, --config. Run\n│ 'prisma-test --help' for details.\n│\n│ Examples\n│ $ prisma-test dev module.ts\n\n", + "prisma-test dev → Run the app locally\n\n│ Usage\n│ $ prisma-test dev [options] [entry]\n│\n│ Starts the entry module and watches it for changes.\n│\n│ Commands\n│ url [service] Print the local URL of a running service\n│ stop Stop the running app\n│\n│ Arguments\n│ entry The module to run (optional)\n│\n│ Options\n│ --port Port to listen on\n│ --delete ... Delete a resource: --delete X\n│\n│ Global options also apply: --format, --json, --log-level, --verbose,\n│ --quiet, --yes, --confirm, --interactive, --color, --config. Run\n│ 'prisma-test --help' for details.\n│\n│ Examples\n│ $ prisma-test dev module.ts\n\n", ); }); @@ -237,14 +239,14 @@ describe("help", () => { "module.ts", "--help", "--no-color", - ]); - const bare = await devCli().run(["dev", "--help", "--no-color"]); + ], TERMINAL); + const bare = await devCli().run(["dev", "--help", "--no-color"], TERMINAL); expect(withPositional.stdout).toBe(bare.stdout); }); test("--help on a child shows the child's help", async () => { - const result = await devCli().run(["dev", "url", "--help", "--no-color"]); + const result = await devCli().run(["dev", "url", "--help", "--no-color"], TERMINAL); expect(result.exitCode).toBe(0); expect(result.stdout).toContain( @@ -265,7 +267,7 @@ describe("help", () => { }); test("the parent card lists the path once, with its group brief", async () => { - const result = await devCli().run(["--help", "--no-color"]); + const result = await devCli().run(["--help", "--no-color"], TERMINAL); expect(result.stdout).toContain( "│ dev Run the app locally. Start it, print URLs, stop it\n", diff --git a/packages/cli-engine/tests/engine.test.ts b/packages/cli-engine/tests/engine.test.ts index 79ea2429..936a553c 100644 --- a/packages/cli-engine/tests/engine.test.ts +++ b/packages/cli-engine/tests/engine.test.ts @@ -207,15 +207,6 @@ describe("construction validation", () => { ).toThrow("unknown group 'auth'"); }); - test("a command mount colliding with a group prefix fails construction", () => { - expect(() => - createTestCli({ - commands: { auth: command, "auth whoami": command }, - groups: { auth: { brief: "Authentication" } }, - }), - ).toThrow("collides"); - }); - test("reserved shared-family flag names fail construction", () => { const offender = defineCommand({ help: { summary: "Offender" }, From 9fd7ec76e0d06903fefdc08f1edf07c0631042e8 Mon Sep 17 00:00:00 2001 From: willbot Date: Tue, 13 Oct 2026 16:14:57 +0200 Subject: [PATCH 3/8] docs: a command path can also be a group The engine README shows the mount, and the style guide says what the combined help card shows and how a child's name wins over a positional. Co-Authored-By: Claude Opus 5.5 Signed-off-by: willbot Signed-off-by: Will Madden --- docs/product/cli-style-guide.md | 6 ++++++ packages/cli-engine/README.md | 14 ++++++++++++++ packages/cli-engine/src/cli.ts | 6 ++++-- 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/docs/product/cli-style-guide.md b/docs/product/cli-style-guide.md index ab4972e0..c30f603b 100644 --- a/docs/product/cli-style-guide.md +++ b/docs/product/cli-style-guide.md @@ -164,6 +164,12 @@ A flag brief states what the flag does; when the flag exists for a distinct situ A group card may declare a workflow: the ordered commands of that group's common path, each with a short purpose column. The engine renders it as a `Workflow` section, before any examples, with each step `$`-prefixed and copy-pastable. Declare a workflow only where a real multi-command path exists; a group of independent commands has no workflow. +### Groups With Their Own Command + +A path can be a command and a group at once: `prisma dev ` runs the app, and `prisma dev url` and `prisma dev stop` are commands beneath it. `prisma dev --help` shows the `dev` command's card, with the commands beneath it in a `Commands` section after its description. The group's brief is still its row on the parent card. The group's description and workflow do not appear on that card, so the command's description defines any term the rows beneath it rely on. + +When the word after the path names a command beneath it, that command runs, so `prisma dev url` never runs `dev` with an entry named `url`. Give the command a positional whose values cannot be spelled like those commands' names, such as a file path. + ## Flags Shared flag rules: diff --git a/packages/cli-engine/README.md b/packages/cli-engine/README.md index e3ce61cb..ed713785 100644 --- a/packages/cli-engine/README.md +++ b/packages/cli-engine/README.md @@ -10,6 +10,20 @@ Every command describes its output once, as blocks, and the engine renders it in - `@prisma/cli-engine/protocol` — the wire types for machine-readable (JSON) output. - `@prisma/cli-engine/testing` — the test harness for running commands in-process. +## Mounting commands + +`createCli` takes commands by path. Every path with more than one word needs its group declared in `groups`, and the group's `brief` is its row on the parent's help card. A path can be a command and a group at once: + +```ts +createCli({ + groups: { dev: { brief: "Run the app locally. Start it, print URLs, stop it" } }, + commands: { dev, "dev url": devUrl, "dev stop": devStop }, + // ... +}); +``` + +`dev module.ts` runs `dev` with the positional `module.ts`, and `dev url gateway` runs `dev url`. When the word after `dev` names a command beneath it, that command runs, so `dev url` never reaches `dev` as a positional. `dev --help` shows the `dev` command's help and lists `url` and `stop`. + ## Statement prompts A command asks what the user means to happen to something with `ctx.prompt.statement`. The answer is a verb and free text, and the command validates it. diff --git a/packages/cli-engine/src/cli.ts b/packages/cli-engine/src/cli.ts index 5b44239b..5555e119 100644 --- a/packages/cli-engine/src/cli.ts +++ b/packages/cli-engine/src/cli.ts @@ -32,8 +32,10 @@ export interface Cli { } /** - * Shell-side construction. Group help is declared with the mount. - * Collisions, unknown groups, reserved-flag violations, grammar + * Shell-side construction. Group help is declared with the mount. A + * path may be both a command and a group (`dev` beside `dev url`); a + * child's name wins over the command's positional. Redirects onto + * mounted paths, unknown groups, reserved-flag violations, grammar * violations, and foreign-section references fail construction (build * time, not run time). */ From 7207d22fc97ef9fa4907fe676abaf6cfb94312f4 Mon Sep 17 00:00:00 2001 From: willbot Date: Tue, 13 Oct 2026 16:15:35 +0200 Subject: [PATCH 4/8] chore(engine): @prisma/cli-engine 0.7.0 -> 0.7.1 A patch release: a path that is also a group is new behaviour, and nothing that built before changes. The conformance check's two temporary exceptions move from 0.6.3/0.7.0 to 0.7.0/0.7.1. @prisma/composer-cli and @prisma/orm-toolchain now peer 0.7.0, so the old exceptions no longer matched anything, and the new ones last until both release against 0.7.1. Co-Authored-By: Claude Opus 5.5 Signed-off-by: willbot Signed-off-by: Will Madden --- packages/cli-engine/package.json | 2 +- packages/cli/package.json | 2 +- packages/cli/scripts/conformance.ts | 16 ++++++++-------- packages/prisma/package.json | 2 +- pnpm-lock.yaml | 4 ++-- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/packages/cli-engine/package.json b/packages/cli-engine/package.json index bc3124dd..62a80a80 100644 --- a/packages/cli-engine/package.json +++ b/packages/cli-engine/package.json @@ -1,6 +1,6 @@ { "name": "@prisma/cli-engine", - "version": "0.7.0", + "version": "0.7.1", "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 6a88750b..03e04138 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.7.0", + "@prisma/cli-engine": "workspace:0.7.1", "@prisma/composer-cli": "0.29.1", "@prisma/compute-sdk": "0.43.0", "@prisma/management-api-sdk": "1.80.0", diff --git a/packages/cli/scripts/conformance.ts b/packages/cli/scripts/conformance.ts index a4f64251..c1fc5a2e 100644 --- a/packages/cli/scripts/conformance.ts +++ b/packages/cli/scripts/conformance.ts @@ -112,19 +112,19 @@ async function tarball(): Promise { exceptions: [ { familyPackage: "@prisma/composer-cli", - familyPin: "0.6.3", - shellPin: "0.7.0", - reason: "engine 0.7.0 must publish before composer-cli can peer it", + familyPin: "0.7.0", + shellPin: "0.7.1", + reason: "engine 0.7.1 must publish before composer-cli can peer it", removeWhen: - "composer-cli releases peering 0.7.0 and the follow-up bump PR pins that release", + "composer-cli releases peering 0.7.1 and the follow-up bump PR pins that release", }, { familyPackage: "@prisma/orm-toolchain", - familyPin: "0.6.3", - shellPin: "0.7.0", - reason: "engine 0.7.0 must publish before orm-toolchain can peer it", + familyPin: "0.7.0", + shellPin: "0.7.1", + reason: "engine 0.7.1 must publish before orm-toolchain can peer it", removeWhen: - "orm-toolchain releases peering 0.7.0 and the follow-up bump PR pins that release", + "orm-toolchain releases peering 0.7.1 and the follow-up bump PR pins that release", }, ], channel: CHANNEL, diff --git a/packages/prisma/package.json b/packages/prisma/package.json index e9298dba..e0398fd8 100644 --- a/packages/prisma/package.json +++ b/packages/prisma/package.json @@ -50,7 +50,7 @@ }, "dependencies": { "@manypkg/tools": "^2.1.2", - "@prisma/cli-engine": "workspace:0.7.0", + "@prisma/cli-engine": "workspace:0.7.1", "@prisma/composer-cli": "0.29.1", "@prisma/compute-sdk": "0.43.0", "@prisma/management-api-sdk": "1.80.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 13061259..448a02e1 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.7.0 + specifier: workspace:0.7.1 version: link:../cli-engine '@prisma/composer-cli': specifier: 0.29.1 @@ -210,7 +210,7 @@ importers: specifier: ^2.1.2 version: 2.1.2 '@prisma/cli-engine': - specifier: workspace:0.7.0 + specifier: workspace:0.7.1 version: link:../cli-engine '@prisma/composer-cli': specifier: 0.29.1 From 2f609db7f07e586a42932925e33c325e4b557da7 Mon Sep 17 00:00:00 2001 From: willbot Date: Tue, 13 Oct 2026 16:16:33 +0200 Subject: [PATCH 5/8] style(engine): format the command group tests Co-Authored-By: Claude Opus 5.5 Signed-off-by: willbot Signed-off-by: Will Madden --- .../cli-engine/tests/command-groups.test.ts | 31 +++++++++++++------ 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/packages/cli-engine/tests/command-groups.test.ts b/packages/cli-engine/tests/command-groups.test.ts index 7d8a3666..80ea8701 100644 --- a/packages/cli-engine/tests/command-groups.test.ts +++ b/packages/cli-engine/tests/command-groups.test.ts @@ -225,7 +225,10 @@ describe("routing", () => { describe("help", () => { test("--help on the path shows its command's usage and lists the children", async () => { - const result = await devCli().run(["dev", "--help", "--no-color"], TERMINAL); + const result = await devCli().run( + ["dev", "--help", "--no-color"], + TERMINAL, + ); expect(result.exitCode).toBe(0); expect(result.stdout).toBe( @@ -234,29 +237,37 @@ describe("help", () => { }); test("--help after a positional shows the path's own help", async () => { - const withPositional = await devCli().run([ - "dev", - "module.ts", - "--help", - "--no-color", - ], TERMINAL); + const withPositional = await devCli().run( + ["dev", "module.ts", "--help", "--no-color"], + TERMINAL, + ); const bare = await devCli().run(["dev", "--help", "--no-color"], TERMINAL); expect(withPositional.stdout).toBe(bare.stdout); }); test("--help on a child shows the child's help", async () => { - const result = await devCli().run(["dev", "url", "--help", "--no-color"], TERMINAL); + const result = await devCli().run( + ["dev", "url", "--help", "--no-color"], + TERMINAL, + ); expect(result.exitCode).toBe(0); expect(result.stdout).toContain( "prisma-test dev url → Print the local URL of a running service", ); - expect(result.stdout).toContain("$ prisma-test dev url [options] [service]"); + expect(result.stdout).toContain( + "$ prisma-test dev url [options] [service]", + ); }); test("Markdown help lists the children under the usage", async () => { - const result = await devCli().run(["dev", "--help", "--format", "markdown"]); + const result = await devCli().run([ + "dev", + "--help", + "--format", + "markdown", + ]); expect(result.stdout).toContain( "## Usage\n\n```bash\nprisma-test dev [options] [entry]\n```", From d3ca86c663d64721fba3ef5d84ef46620d10a5f4 Mon Sep 17 00:00:00 2001 From: willbot Date: Tue, 13 Oct 2026 16:25:40 +0200 Subject: [PATCH 6/8] fix(engine): a retired child of a group's own command names its replacement With a redirect from 'dev up', 'dev up' ran 'dev' with the entry 'up': redirects were consulted only when routing failed, and under a group's own command it never fails. The engine now matches the redirect before parsing, so a retired child's name wins over the positional the way a live child's name does. Also: - the hidden route the own command sits under is dropped from the reported path, so a literally typed '' never shows up in a commandId - CommandTreeNode.command is now ownCommand, and the help target for a command is kind 'command' with 'subcommands' - a test pins that '--' passes a word spelled like a child as the positional Co-Authored-By: Claude Opus 5.5 Signed-off-by: willbot Signed-off-by: Will Madden --- .../cli-engine/src/execution/command-tree.ts | 6 ++-- packages/cli-engine/src/execution/engine.ts | 30 ++++++++++++++++ packages/cli-engine/src/execution/help.ts | 19 +++++----- .../cli-engine/src/execution/settlement.ts | 9 +++-- .../src/execution/statement-flags.ts | 4 +-- .../src/execution/stricli-adapter.ts | 11 +++--- .../cli-engine/tests/command-groups.test.ts | 35 +++++++++++++++++++ 7 files changed, 93 insertions(+), 21 deletions(-) diff --git a/packages/cli-engine/src/execution/command-tree.ts b/packages/cli-engine/src/execution/command-tree.ts index da91f6c2..b41c4684 100644 --- a/packages/cli-engine/src/execution/command-tree.ts +++ b/packages/cli-engine/src/execution/command-tree.ts @@ -134,13 +134,13 @@ export interface CommandTreeEntry { export interface CommandTreeNode { /** The command mounted at this group's own path, such as `dev` beside * `dev url`. It runs when the next argument names no child. */ - command: CommandTreeEntry | undefined; + ownCommand: CommandTreeEntry | undefined; readonly commands: Map; readonly children: Map; } function emptyNode(): CommandTreeNode { - return { command: undefined, commands: new Map(), children: new Map() }; + return { ownCommand: undefined, commands: new Map(), children: new Map() }; } function childNode(node: CommandTreeNode, segment: string): CommandTreeNode { @@ -184,7 +184,7 @@ function insertCommand( .reduce((node, segment) => childNode(node, segment), root); const leaf = segments[segments.length - 1]; if (isGroup) { - childNode(parent, leaf).command = entry; + childNode(parent, leaf).ownCommand = entry; } else { parent.commands.set(leaf, entry); } diff --git a/packages/cli-engine/src/execution/engine.ts b/packages/cli-engine/src/execution/engine.ts index 8de363c7..aaa03dad 100644 --- a/packages/cli-engine/src/execution/engine.ts +++ b/packages/cli-engine/src/execution/engine.ts @@ -457,6 +457,18 @@ export class EngineImpl implements Engine { ); return 0; } + const retired = this.retiredPathBeneathCommand(argv); + if (retired !== undefined) { + unsubscribe(); + const exitCode = settleCommandMoved( + this.spec, + invocation, + retired, + retired.from.replaceAll(" ", "."), + ); + this.fireOnSettled(invocation, exitCode, startedAtMs); + return exitCode; + } const stricliArgv = this.takeStatementFlags(invocation, argv, startedAtMs); if (stricliArgv === undefined) { unsubscribe(); @@ -537,6 +549,24 @@ export class EngineImpl implements Engine { return extraction.argv; } + /** A retired path beneath a mounted command, such as `dev up` beside + * `dev`. The parser would hand `up` to `dev` as a positional, so the + * redirect is matched first: a retired child's name wins over the + * positional, as a live child's name does. */ + private retiredPathBeneathCommand( + argv: readonly string[], + ): CommandRedirect | undefined { + const routed = routedCommand(this.tree, argv); + if (routed === undefined) { + return undefined; + } + const redirect = matchVerbRedirect(this.redirects, attemptedPath(argv)); + return redirect !== undefined && + redirect.from.split(" ").length > routed.id.split(".").length + ? redirect + : undefined; + } + /** stricli routed or parsed nothing runnable. When the redirect table * claims the invocation the user typed, the run names its * replacement; otherwise it settles as the usage error it is. */ diff --git a/packages/cli-engine/src/execution/help.ts b/packages/cli-engine/src/execution/help.ts index e5ba4272..225677e1 100644 --- a/packages/cli-engine/src/execution/help.ts +++ b/packages/cli-engine/src/execution/help.ts @@ -87,10 +87,10 @@ function helpPath(argv: readonly string[]): readonly string[] { type HelpTarget = | { readonly kind: "node"; readonly node: CommandTreeNode } | { - readonly kind: "leaf"; + readonly kind: "command"; readonly entry: CommandTreeEntry; /** Set when the command's path is also a group. */ - readonly children: CommandTreeNode | undefined; + readonly subcommands: CommandTreeNode | undefined; }; /** Walks as far as the segments stay recognized; help for `project @@ -106,7 +106,7 @@ function resolveTarget( const entry = node.commands.get(segment); if (entry !== undefined) { return { - target: { kind: "leaf", entry, children: undefined }, + target: { kind: "command", entry, subcommands: undefined }, path: [...path, segment], }; } @@ -117,9 +117,9 @@ function resolveTarget( node = child; path.push(segment); } - if (node.command !== undefined) { + if (node.ownCommand !== undefined) { return { - target: { kind: "leaf", entry: node.command, children: node }, + target: { kind: "command", entry: node.ownCommand, subcommands: node }, path, }; } @@ -212,8 +212,8 @@ export function helpCard( argv: readonly string[], ): HelpCard { const { target, path } = resolveTarget(root, helpPath(argv)); - return target.kind === "leaf" - ? leafCard(spec, target.entry, target.children, path) + return target.kind === "command" + ? leafCard(spec, target.entry, target.subcommands, path) : nodeCard(spec, target.node, path); } @@ -250,7 +250,7 @@ function nodeCard( function leafCard( spec: EngineSpec, entry: CommandTreeEntry, - children: CommandTreeNode | undefined, + subcommands: CommandTreeNode | undefined, path: readonly string[], ): HelpCard { const def = entry.def; @@ -272,7 +272,8 @@ function leafCard( tagline: def.help.summary, usage, description: def.help.description, - commands: children === undefined ? [] : nodeRows(spec, children, path), + commands: + subcommands === undefined ? [] : nodeRows(spec, subcommands, path), workflow: [], arguments: Object.values>(def.args.positionals) .map((spec) => positionalRuntime(spec)) diff --git a/packages/cli-engine/src/execution/settlement.ts b/packages/cli-engine/src/execution/settlement.ts index f4db2e5e..f8410a68 100644 --- a/packages/cli-engine/src/execution/settlement.ts +++ b/packages/cli-engine/src/execution/settlement.ts @@ -33,7 +33,11 @@ import { writeSections, } from "./rendering"; import { emitFrame } from "./reporting"; -import { resolveExample, usageErrorCode } from "./stricli-adapter"; +import { + OWN_COMMAND_ROUTE, + resolveExample, + usageErrorCode, +} from "./stricli-adapter"; function undocumentedExitCode( def: AnyCommand, @@ -491,7 +495,8 @@ export function commandSegments( spec: EngineSpec, prefix: readonly string[], ): readonly string[] { - return prefix[0] === spec.name ? prefix.slice(1) : prefix; + const path = prefix[0] === spec.name ? prefix.slice(1) : prefix; + return path.filter((segment) => segment !== OWN_COMMAND_ROUTE); } /** Maps stricli's own settlement (parse/route failures, framework bugs) diff --git a/packages/cli-engine/src/execution/statement-flags.ts b/packages/cli-engine/src/execution/statement-flags.ts index 693c4f81..8d90740f 100644 --- a/packages/cli-engine/src/execution/statement-flags.ts +++ b/packages/cli-engine/src/execution/statement-flags.ts @@ -63,11 +63,11 @@ export function routedCommand( const child = node.children.get(token) ?? node.children.get(camelCase(token)); if (child === undefined) { - return node.command; + return node.ownCommand; } node = child; } - return node.command; + return node.ownCommand; } function statementFlagIn( diff --git a/packages/cli-engine/src/execution/stricli-adapter.ts b/packages/cli-engine/src/execution/stricli-adapter.ts index 667e9598..3b5104e0 100644 --- a/packages/cli-engine/src/execution/stricli-adapter.ts +++ b/packages/cli-engine/src/execution/stricli-adapter.ts @@ -241,8 +241,9 @@ function toStricliCommand( /** The route a group's own command sits under, hidden from listings. * A child's name is one path segment and never contains a space, so no - * child can take this name. */ -const OWN_COMMAND_ROUTE = ""; + * child can take this name. A user can still type it, so it is dropped + * from the path the parser reports. */ +export const OWN_COMMAND_ROUTE = ""; export function buildRoutes( spec: EngineSpec, @@ -251,9 +252,9 @@ export function buildRoutes( runEntry: RunEntry, ): StricliRouteMap { const routes: Record = {}; - if (node.command !== undefined) { + if (node.ownCommand !== undefined) { routes[OWN_COMMAND_ROUTE] = toStricliCommand( - node.command, + node.ownCommand, spec.name, runEntry, ); @@ -272,7 +273,7 @@ export function buildRoutes( brief: spec.groups[groupPath].brief, fullDescription: spec.groups[groupPath].description, }; - if (node.command === undefined) { + if (node.ownCommand === undefined) { return buildRouteMap({ routes, docs }); } return buildRouteMap({ diff --git a/packages/cli-engine/tests/command-groups.test.ts b/packages/cli-engine/tests/command-groups.test.ts index 80ea8701..197e1ac4 100644 --- a/packages/cli-engine/tests/command-groups.test.ts +++ b/packages/cli-engine/tests/command-groups.test.ts @@ -221,6 +221,41 @@ describe("routing", () => { const envelope = lastEnvelope(result); expect(!envelope.ok && envelope.error.code).toBe("CLI.COMMAND_MOVED"); }); + + test("a retired child's name wins over the positional, as a live child's does", async () => { + const cli = devCli(undefined, [{ from: "dev up", replacement: "dev" }]); + + const retired = await cli.run(["dev", "up", "--json"]); + expect(retired.exitCode).toBe(2); + const envelope = lastEnvelope(retired); + expect(envelope.commandId).toBe("dev.up"); + expect(!envelope.ok && envelope.error.code).toBe("CLI.COMMAND_MOVED"); + + const live = await cli.run(["dev", "module.ts", "--json"]); + expect(live.presented?.data).toMatchObject({ + ran: "dev", + entry: "module.ts", + }); + }); + + test("-- passes a word spelled like a child to the path's own command", async () => { + const result = await devCli().run(["dev", "--json", "--", "url"]); + + expect(result.exitCode).toBe(0); + expect(result.presented?.data).toMatchObject({ ran: "dev", entry: "url" }); + }); + + test("the route the own command sits under never appears in the command id", async () => { + const result = await devCli().run([ + "dev", + "", + "--frobnicate", + "--json", + ]); + + expect(result.exitCode).toBe(2); + expect(lastEnvelope(result).commandId).toBe("dev"); + }); }); describe("help", () => { From 8196d39b30b39a582b06ede561d20ced1f948350 Mon Sep 17 00:00:00 2001 From: willbot Date: Tue, 13 Oct 2026 16:25:40 +0200 Subject: [PATCH 7/8] docs: '--' passes a word spelled like a child as the positional Co-Authored-By: Claude Opus 5.5 Signed-off-by: willbot Signed-off-by: Will Madden --- docs/product/cli-style-guide.md | 2 +- packages/cli-engine/README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/product/cli-style-guide.md b/docs/product/cli-style-guide.md index c30f603b..95a426e1 100644 --- a/docs/product/cli-style-guide.md +++ b/docs/product/cli-style-guide.md @@ -168,7 +168,7 @@ A group card may declare a workflow: the ordered commands of that group's common A path can be a command and a group at once: `prisma dev ` runs the app, and `prisma dev url` and `prisma dev stop` are commands beneath it. `prisma dev --help` shows the `dev` command's card, with the commands beneath it in a `Commands` section after its description. The group's brief is still its row on the parent card. The group's description and workflow do not appear on that card, so the command's description defines any term the rows beneath it rely on. -When the word after the path names a command beneath it, that command runs, so `prisma dev url` never runs `dev` with an entry named `url`. Give the command a positional whose values cannot be spelled like those commands' names, such as a file path. +When the word after the path names a command beneath it, that command runs, so `prisma dev url` never runs `dev` with an entry named `url`; `prisma dev -- url` does. Give the command a positional whose values cannot be spelled like those commands' names, such as a file path. ## Flags diff --git a/packages/cli-engine/README.md b/packages/cli-engine/README.md index ed713785..7b782a71 100644 --- a/packages/cli-engine/README.md +++ b/packages/cli-engine/README.md @@ -22,7 +22,7 @@ createCli({ }); ``` -`dev module.ts` runs `dev` with the positional `module.ts`, and `dev url gateway` runs `dev url`. When the word after `dev` names a command beneath it, that command runs, so `dev url` never reaches `dev` as a positional. `dev --help` shows the `dev` command's help and lists `url` and `stop`. +`dev module.ts` runs `dev` with the positional `module.ts`, and `dev url gateway` runs `dev url`. When the word after `dev` names a command beneath it, that command runs, so `dev url` never reaches `dev` as a positional. To pass such a word as the positional, put `--` before it: `dev -- url` runs `dev` with `url`. A retired path declared as a redirect, such as `dev up`, wins over the positional the same way. `dev --help` shows the `dev` command's help and lists `url` and `stop`. ## Statement prompts From 5125618ea8babe822d718be2afd3efe7099580f4 Mon Sep 17 00:00:00 2001 From: willbot Date: Tue, 13 Oct 2026 16:25:51 +0200 Subject: [PATCH 8/8] chore(engine): @prisma/cli-engine 0.7.1 -> 0.8.0 A tree that 0.7.0 refused now builds. That is a new capability, which takes a minor bump, as the statement prompts did for 0.7.0. The conformance exceptions move to 0.7.0/0.8.0 to match. Co-Authored-By: Claude Opus 5.5 Signed-off-by: willbot Signed-off-by: Will Madden --- packages/cli-engine/package.json | 2 +- packages/cli/package.json | 2 +- packages/cli/scripts/conformance.ts | 12 ++++++------ packages/prisma/package.json | 2 +- pnpm-lock.yaml | 4 ++-- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/cli-engine/package.json b/packages/cli-engine/package.json index 62a80a80..2d73958a 100644 --- a/packages/cli-engine/package.json +++ b/packages/cli-engine/package.json @@ -1,6 +1,6 @@ { "name": "@prisma/cli-engine", - "version": "0.7.1", + "version": "0.8.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 03e04138..a9764df2 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.7.1", + "@prisma/cli-engine": "workspace:0.8.0", "@prisma/composer-cli": "0.29.1", "@prisma/compute-sdk": "0.43.0", "@prisma/management-api-sdk": "1.80.0", diff --git a/packages/cli/scripts/conformance.ts b/packages/cli/scripts/conformance.ts index c1fc5a2e..0d56163a 100644 --- a/packages/cli/scripts/conformance.ts +++ b/packages/cli/scripts/conformance.ts @@ -113,18 +113,18 @@ async function tarball(): Promise { { familyPackage: "@prisma/composer-cli", familyPin: "0.7.0", - shellPin: "0.7.1", - reason: "engine 0.7.1 must publish before composer-cli can peer it", + shellPin: "0.8.0", + reason: "engine 0.8.0 must publish before composer-cli can peer it", removeWhen: - "composer-cli releases peering 0.7.1 and the follow-up bump PR pins that release", + "composer-cli releases peering 0.8.0 and the follow-up bump PR pins that release", }, { familyPackage: "@prisma/orm-toolchain", familyPin: "0.7.0", - shellPin: "0.7.1", - reason: "engine 0.7.1 must publish before orm-toolchain can peer it", + shellPin: "0.8.0", + reason: "engine 0.8.0 must publish before orm-toolchain can peer it", removeWhen: - "orm-toolchain releases peering 0.7.1 and the follow-up bump PR pins that release", + "orm-toolchain releases peering 0.8.0 and the follow-up bump PR pins that release", }, ], channel: CHANNEL, diff --git a/packages/prisma/package.json b/packages/prisma/package.json index e0398fd8..9df3e9d1 100644 --- a/packages/prisma/package.json +++ b/packages/prisma/package.json @@ -50,7 +50,7 @@ }, "dependencies": { "@manypkg/tools": "^2.1.2", - "@prisma/cli-engine": "workspace:0.7.1", + "@prisma/cli-engine": "workspace:0.8.0", "@prisma/composer-cli": "0.29.1", "@prisma/compute-sdk": "0.43.0", "@prisma/management-api-sdk": "1.80.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 448a02e1..2dc4d767 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.7.1 + specifier: workspace:0.8.0 version: link:../cli-engine '@prisma/composer-cli': specifier: 0.29.1 @@ -210,7 +210,7 @@ importers: specifier: ^2.1.2 version: 2.1.2 '@prisma/cli-engine': - specifier: workspace:0.7.1 + specifier: workspace:0.8.0 version: link:../cli-engine '@prisma/composer-cli': specifier: 0.29.1