Skip to content

feat(engine): a command can also have subcommands, so prisma dev <entry> and prisma dev url can coexist - #349

Open
wmadden-electric wants to merge 8 commits into
mainfrom
feat/engine-command-group
Open

wmadden-electric wants to merge 8 commits into
mainfrom
feat/engine-command-group

Conversation

@wmadden-electric

Copy link
Copy Markdown
Contributor

At a glance

A CLI can now mount a command at a path that also has subcommands:

createCli({
  groups: { dev: { brief: "Run a Composer app locally" } },
  commands: {
    dev: devCommand,          // prisma dev module.ts
    "dev url": devUrlCommand, // prisma dev url gateway
    "dev stop": devStopCommand,
  },
});
prisma dev module.ts     runs `dev` with entry "module.ts"
prisma dev url gateway   runs `dev url`
prisma dev -- url        runs `dev` with entry "url"
prisma dev --help        shows `dev`'s own help, then lists `url` and `stop`

Before this PR, createCli refused this shape: "command path 'dev url' collides with a command mounted at a prefix of it".

What this PR changes

  1. A path may carry its own command and child commands at the same time.
  2. The word after such a path picks a child when it names one; otherwise the path's own command runs with the remaining arguments.
  3. A redirect for a retired child path takes priority over the positional, so prisma dev up reports that the command moved instead of running dev with entry up.
  4. @prisma/cli-engine goes from 0.7.0 to 0.8.0.

No command in the prisma binary uses the new shape yet.

Why

Composer's prisma dev <entry> runs an app on local stand-ins for Prisma Cloud. Composer is adding prisma dev url <address>, which prints the local URL of one of that app's services, and prisma dev stop, which stops the app's local processes. A bare dev that also has subcommands is the usual shape for this (pnpm dev, next dev), and it keeps every existing prisma dev module.ts in docs and scripts working. Those commands mount in a later PR, once a Composer release has them.

How it works

The tree. command-tree.ts builds a tree of groups and commands from the mounted paths. A group node can now hold its own command (CommandTreeNode.ownCommand), alongside its children. The group still needs an entry in groups, because it has children. Every other construction check applies to the own command and to each child. The old prefix-collision refusal is removed. Two commands at one exact path still can't exist, because mounts are a record keyed by path, so there is no check (and no test) for that case.

Routing. The engine routes through Stricli, whose route maps take a defaultCommand: the command that runs when the next word names no route. The own command is registered under a hidden route name, OWN_COMMAND_ROUTE, and used as that default. A child's name is one word and can't contain the hidden name's characters, so they never collide. The hidden name never appears in help, listings, suggestions or commandId.

The rule for words. A child's name wins over a positional spelled the same. That is harmless for dev, whose positional is a file path with an extension. -- passes such a word as the positional (prisma dev -- url).

Redirects. Redirects are normally checked only when the typed words reach no command. Under a path with its own command, every word reaches one, because an unknown word becomes the positional. So the engine now checks the typed words against redirects before parsing. A redirect that matches a longer path than the command those words reach ends the run with CLI.COMMAND_MOVED. This also applies to a redirect under a plain command, such as one from project link old, which used to be passed through as a positional. No shipped redirect is affected: the ORM's redirects sit under migration, which has no command of its own.

Help. prisma dev --help shows dev's own help card with a Commands section after the description. A bare prisma dev runs the command; it does not print help. The style guide now says that the group's own description doesn't appear on the combined card, so the command's description must define the terms its children rely on.

Version. Engine changes ship with pnpm bump-cli-engine-version in the same PR. This adds a capability, so it is a minor bump, as with ADR 0006. @prisma/composer-cli 0.29.1 and @prisma/orm-toolchain 8.0.0-rc.17 declare an exact peer on engine 0.7.0, so the conformance exceptions in packages/cli/scripts/conformance.ts move to 0.7.0 → 0.8.0 until both release against it.

Tests

packages/cli-engine/tests/command-groups.test.ts mounts stand-in dev, dev url and dev stop commands and checks:

  • dev module.ts runs dev, and dev url gateway runs dev url;
  • --help on dev and on dev url;
  • a child name wins over a same-spelled positional, and -- overrides it;
  • a redirect from dev up exits 2 with CLI.COMMAND_MOVED, while dev module.ts still runs dev;
  • a typed hidden route name reports commandId dev.

pnpm --filter @prisma/cli-engine test (1163 passed), pnpm --filter @prisma/cli test (1027 passed, 1 skipped), pnpm typecheck, and Biome all pass.

Alternatives considered

  • Rename the run command to prisma dev start. No engine change, but it breaks every prisma dev module.ts in docs, guides and scripts, and reverses Composer's decision to mount dev as a bare verb at the root.
  • Put the new commands in their own group, such as prisma emulator url. It works today, but url answers a question about the app prisma dev runs from this directory, so it belongs under dev.
  • Route in the engine's adapter instead of Stricli's defaultCommand. More code for the same behaviour.
  • Refuse redirects under a path with its own command. Simpler, but it would block ever retiring a dev subcommand.

Agent: attila-51

🤖 Generated with Claude Code

wmadden-electric and others added 8 commits October 13, 2026 16:11
Stand-in dev, dev url and dev stop commands. These fail until the
engine accepts the shape.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
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 <noreply@anthropic.com>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
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 <noreply@anthropic.com>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
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 <noreply@anthropic.com>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
…cement

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 '<own command>' 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 <noreply@anthropic.com>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
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 <noreply@anthropic.com>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
@coderabbitai

coderabbitai Bot commented Oct 13, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: 3b121c78-83c4-41da-8738-50bd21443072
📥 Commits

Reviewing files that changed from the base of the PR and between 56d4b64 and 5125618.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (15)
  • docs/product/cli-style-guide.md
  • packages/cli-engine/README.md
  • packages/cli-engine/package.json
  • packages/cli-engine/src/cli.ts
  • packages/cli-engine/src/execution/command-tree.ts
  • packages/cli-engine/src/execution/engine.ts
  • packages/cli-engine/src/execution/help.ts
  • packages/cli-engine/src/execution/settlement.ts
  • packages/cli-engine/src/execution/statement-flags.ts
  • packages/cli-engine/src/execution/stricli-adapter.ts
  • packages/cli-engine/tests/command-groups.test.ts
  • packages/cli-engine/tests/engine.test.ts
  • packages/cli/package.json
  • packages/cli/scripts/conformance.ts
  • packages/prisma/package.json
💤 Files with no reviewable changes (1)
  • packages/cli-engine/tests/engine.test.ts

Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.

Summary by CodeRabbit

  • New Features
    • A command can now also serve as a group with child commands. When an input matches a child command, that child runs; use -- to pass the same word as a positional value to the parent command.
    • Help for commands that also have child commands now lists those commands.
  • Documentation
    • Updated the CLI guides to explain command groups, help displays, and how to distinguish child commands from positional values.

Walkthrough

The CLI engine now allows a mounted path to act as both a command and a group. Child command names take precedence over matching positional values, while -- passes the value to the parent command. The engine handles retired child-path redirects and includes child commands in help output. Documentation and tests describe and verify these changes. The CLI engine package version and dependent workspace references changed to 0.8.0.

Priority: ⬇️ Low

Merge Risk: ⚪ Minimal · up to 51256

This change lets a command share a path with a group of subcommands. No concrete defect was found, and no command in the shipped binary uses this shape yet. It looks safe to merge after the usual checks.

Security architecture risk: 🔵 Low · up to 51256

The inspected routing changes preserve command-specific execution controls, and the shipped CLI does not yet use the new combined command-and-group shape. No introduced security issue was established. Remaining uncertainty concerns downstream consumers and custom runtime behavior.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — The inspected authority surface is command selection within a caller-supplied mount specification. An argv-controlling caller can select an own command or child, but the selected definition still determines its execution requirements and capabilities. Wider exposure depends on downstream command definitions and runtime implementations not comprehensively reviewed here.

Trust Boundaries and Controls

  • observed — The new hidden default route does not introduce a separate handler path: own and child commands use the common mounted pipeline. Command-specific needs are checked before ordinary handler invocation; the separate server-command branch also performs its preflight before invocation. Help and retired redirects terminate without executing a handler.

Resilience and Maintainability Implications

  • observed — Each execute call allocates its own invocation state and cancellation controller. The added retired-path branch unsubscribes signal handling before settlement and returns without an intervening await or handler invocation, avoiding new command-owned resources or shared active-run state on that path. Exceptions or reentrancy in host-provided callbacks remain outside the inspected guarantees.
Pre-merge checks · Passed 4 · Failed 1

Failed Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 28.57% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 21 functions across 9 files. (5 skipped: … Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks (4 passed)
Check name Status Explanation
Title check Passed The title clearly summarizes the primary change: commands can now also have subcommands at the same path.
Description check Passed The description directly explains the command-and-group mount behavior, routing precedence, redirects, help changes, version bump, and tests.
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Full details: Docstring Coverage

Explanation

Docstring coverage is 28.57% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 21 functions across 9 files. (5 skipped: 5 unsupported.)

  • Fix all pre-merge checks with AI
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
✨ Simplify code
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

@pkg-pr-new

pkg-pr-new Bot commented Oct 13, 2026

Copy link
Copy Markdown

Open in StackBlitz

npx https://pkg.pr.new/@prisma/cli@349
npx https://pkg.pr.new/@prisma/cli-engine@349

commit: 5125618

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant