Skip to content

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

Merged
wmadden-electric merged 9 commits into
mainfrom
feat/engine-command-group
Oct 15, 2026
Merged

wmadden-electric merged 9 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: 81c5fb66-c5cb-4c71-a2fd-8df5d0b3a8e7
📥 Commits

Reviewing files that changed from the base of the PR and between 5125618 and 0d0b5ec.

⛔ Files ignored due to path filters: 1
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing: 2
  • packages/cli/package.json
  • packages/prisma/package.json

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 also serve as a group with child commands. When input matches a child command or a retired command redirect, that route takes precedence; 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: ➖ Normal

Merge Risk: ⚪ Minimal · up to 0d0b5

The package version updates match the engine release, and no unresolved merge risk is identified.

### Architecture Summary

Architecture risk: 🔵 Low · up to 0d0b5

The change affects 4 systems.

Changed systems: packages/cli-engine, packages/cli, docs, packages/prisma

Architecture concerns
No architecture-level concerns identified.

Review details

Systems and components

  • observed — packages/cli-engine (library) was modified; 11 changed files map to changed impact.
  • observed — packages/cli (library) was modified; 2 changed files map to changed impact.
  • observed — docs (service) was modified; 1 changed file maps to changed impact.
  • observed — packages/prisma (library) was modified; 1 changed file maps to changed impact.

Before / after behavior

  • observed — Modified behavior in docs/product/cli-style-guide.md: Added rules for commands that are also groups: their help cards show child commands after the description but omit the group description and workflow; child command names take precedence over matching positional values, while -- passes such a value to the parent command.
  • observed — Modified behavior in packages/cli-engine/README.md: Adds documentation for declaring command paths and required groups, then describes how nested commands, redirects, --, and --help affect command selection and help output.
  • observed — Modified behavior in packages/cli-engine/package.json: The package version was updated from 0.7.0 to 0.8.0.
  • observed — Modified behavior in packages/cli-engine/src/cli.ts: The construction documentation now describes command/group path overlap and child-name precedence, and adds redirects onto mounted paths as a construction failure; the previous blanket statement that collisions fail is removed.
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. (2 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 main change: a command path can also have subcommands, with the example behavior stated directly.
Description check Passed The description directly explains the command-and-group mounting behavior, routing precedence, redirects, help changes, version bump, rationale, 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. (2 skipped: 2 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: 0d0b5ec

…roup

# Conflicts:
#	packages/cli/package.json
#	packages/prisma/package.json
@wmadden-electric
wmadden-electric merged commit a3ea080 into main Oct 15, 2026
16 checks passed
@wmadden-electric
wmadden-electric deleted the feat/engine-command-group branch October 15, 2026 14:22
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.

2 participants