Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
5142f11
feat(engine): command families register statement verbs as shared flags
wmadden-electric Oct 7, 2026
61b1303
feat(engine): ctx.prompt.statement asks a consent answered with a verb
wmadden-electric Oct 7, 2026
1cccdcc
docs: statement prompts, their verb flags, and CLI.CONSENT_UNUSED
wmadden-electric Oct 7, 2026
6d80e3b
chore(engine): bump @prisma/cli-engine to 0.7.0
wmadden-electric Oct 7, 2026
5a94bd3
refactor(engine): a command declares its statements instead of a fami…
wmadden-electric Oct 7, 2026
3651bd3
feat(engine): a statement flag takes its arity in values, and leftove…
wmadden-electric Oct 7, 2026
1eaddbc
docs: statements are declared per command, with arity, and { last: tr…
wmadden-electric Oct 7, 2026
e820a12
fix(engine): statement flags route like the parser and explain values…
wmadden-electric Oct 7, 2026
25d03a9
refactor(engine): take statement flags out of argv in their own method
wmadden-electric Oct 7, 2026
c4d3238
docs(adr): 0006, a consent can be a statement the command declares an…
wmadden-electric Oct 7, 2026
9c35666
fix(engine): a session command declaring statements fails construction
wmadden-electric Oct 7, 2026
2455811
feat(engine): ctx.statements.take reads a verb's values as the comman…
wmadden-electric Oct 7, 2026
949a03a
fix(engine): a statement value goes to the subject it equals, else th…
wmadden-electric Oct 7, 2026
19f76cb
docs(engine): prefix subjects belong in one batch; take before last, …
wmadden-electric Oct 7, 2026
29edb29
docs(engine): a question may list a verb the command took
wmadden-electric Oct 7, 2026
84ce08e
feat(engine): ctx.statements.values() lists unconsumed statement valu…
wmadden-electric Oct 8, 2026
990fe24
fix(engine): printed statement flags are shell-safe, and the prompt s…
wmadden-electric Oct 8, 2026
c560ced
fix(engine): quote a printed statement value that starts with '='
wmadden-electric Oct 8, 2026
206c1af
fix(engine): unused --confirm tokens fail the run, refusals name stra…
wmadden-electric Oct 8, 2026
adb07a8
fix(engine): unused --confirm fails only statement commands, and sign…
wmadden-electric Oct 8, 2026
1cc84f8
test(engine): --confirm with statements, a last batch, and an interac…
wmadden-electric Oct 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions docs/architecture/adrs/0006-consent-as-a-statement.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# ADR 0006 - A consent can be a statement the command declares and the user answers with

## Status

Accepted (operator, 2026-10-07).

## Context

The engine's consent prompt asks the user to type a fixed token, or to pass it as `--confirm <token>`. That fits an operation with one thing at stake and one way to agree: delete this project, restore over this database. It does not fit a command that finds several risky operations in one run and needs to know, for each one, what the user *means*. The ORM's migration planner is the first such command: a dropped table may be a rename or a deletion, and only the user can say which. A yes/no answer, or a copied token, cannot carry that meaning, and one `--confirm` cannot answer several questions in a way a reader of the script can check.

The constraints the existing consent set still hold: nothing may be inferred, `--yes` must never grant it, a script or an agent must be able to answer up front, and a human in a terminal must be asked.

## Decision

A command declares the statements it may ask for, with the number of values each takes:

```ts
defineCommand({
statements: { rename: { arity: 1, brief: "..." }, delete: { arity: 1, brief: "..." } },
...
})
```

The engine knows no verbs. For that command only, it parses `--<verb>` followed by `arity` values, repeatable, before the rest of argv reaches the argument parser, keeps the values in argv order, and keeps them out of the handler's flags. No other command accepts the flag, and a verb may not share a name with any flag of the command or of the engine.

The command asks with `ctx.prompt.statement(question, { subject, verbs, validate })`, or several questions at once with `ctx.prompt.statements([...], { last })`. Each question names a subject in the command's own vocabulary and the verbs that may answer it, and `validate` decides whether an answer is acceptable. The engine interprets one thing in a value: whether it names the subject, which it does when it equals the subject or starts with the subject followed by `:`. The separator is `:` because that is the subject grammar of the first consumer, the ORM, whose renames read `Old:New`; a product whose values use another separator picks subjects that no value can name by accident. Beyond that, the engine never interprets the answer's text. A question is answered in this order:

1. From the command line, by a value of one of its verbs that names the subject. A value `validate` rejects fails the run with `CLI.PROMPT_INVALID`, since a wrong flag cannot be corrected by asking again.
2. Outside an interactive terminal, or under `--yes`, by nobody: the run fails with one `CLI.CONSENT_REQUIRED` that lists every unanswered question with the flag that would answer it.
3. Interactively, by the user typing `<verb> <value>` (or `<verb>` alone for the subject itself), validated the same way. A rejected answer is asked again on a terminal; scripted or piped input cannot be corrected, so it fails with `CLI.PROMPT_INVALID`.

A statement is the command's own declared input, unlike a `--confirm` value, which a handler never sees: a command whose work depends on some statements, as the ORM plans with its renames before it knows what still loses data, takes a verb's values in argv order with `ctx.statements.take(verb)`, which consumes them.

A value nothing asked about or took is an error, `CLI.CONSENT_UNUSED`, at the end of a run that otherwise succeeded, or at once when the command marks its final batch with `last: true`. A statement is a consent: it has no default and `--yes` never answers it.

`consent(question, { token })` and `--confirm` stay for commands with one thing at stake.

### Rules added after acceptance

These came from the ORM's first use and the reviews of this change, all in engine 0.7.0:

- **`ctx.statements.take(verb)`** returns a verb's values in argv order and consumes them, for statements that are input to the command's work. A question may still list a taken verb; no value of it is left to answer the question, so the verb serves the refusal's flag form and the typed answer. Take before any `last: true` batch.
- **`ctx.statements.values()`** lists every unconsumed value in argv order without consuming any, so a command can show what it was given without spending it.
- **Longest subject first.** Within one `statements` batch, a value equal to a subject answers that subject first, and any other value goes to the question with the longest subject it names. So `A:B` answers the question about `A:B`, not the one about `A`. Questions whose subjects are prefixes of one another belong in one batch, because separate calls cannot be resolved this way.
- **`:` in a subject is allowed**, with the matching rule unchanged.
- **Unused `--confirm` tokens fail the run** with `CLI.CONSENT_UNUSED` on a command that declares statements, where such a token is certainly a mistake: it is the same mistake as a misspelled statement. On other commands a leftover `--confirm` stays silent for now, because `bucket key delete` and `project env delete` take `--confirm` and never consume it; [prisma/prisma-cli#339](https://github.com/prisma/prisma-cli/issues/339) tracks applying the rule to every command. A statement refusal also names any `--confirm` token and any statement flag that names none of its subjects, so the user learns of the mistake before answering a prompt.
- **A signal while a prompt waits cancels the prompt** with `CLI.PROMPT_CANCELLED`: exit 3 for SIGINT, as Ctrl-C at the prompt is the user declining, and 143 for SIGTERM, which ends the run as a delivered signal. Later prompts in the run are cancelled at once, and a further signal force-exits.
- **`last: true`** marks a batch as the run's final ask: values still unconsumed fail with `CLI.CONSENT_UNUSED` as soon as it is answered, before the command acts on the answers.

## Consequences

- A product adds a consent vocabulary by declaring it on the command that uses it, and its help text with it. The engine stays free of product words.
- The interactive form and the scripted form share one validation (`validate`) and one refusal text, which names the flag. They differ in how a value is matched to a question: a flag value must name the subject, while a typed answer is given to the question being asked.
- Statement flags are removed from argv before the argument parser sees it, because that parser gives a flag one value per occurrence and a statement may take more. The engine routes the leading words to the command itself to know which verbs apply; that routing is tested against the parser's own.
- A typed answer or a flag value is validated by the command, so an unknown subject is the command's error, with the command's message.
- This is a minor engine release (0.7.0): a new prompt surface member, new error codes, and a new optional field on `defineCommand`. Commands built against 0.6 load unchanged.
1 change: 1 addition & 0 deletions docs/architecture/adrs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ long-term architecture boundaries.
| [0003](0003-structured-output-and-errors.md) | Accepted | Treat structured output and stable error codes as public contracts. |
| [0004](0004-engine-version-pinning.md) | Accepted | One engine per install: product CLI packages declare the engine as an exact peer, product libraries carry no engine relationship. |
| [0005](0005-config-sections-declare-their-shape.md) | Accepted | A command family declares its config section once as a schema with `path` fields; the engine derives validation, diagnostics, and path resolution from it. |
| [0006](0006-consent-as-a-statement.md) | Accepted | A command declares the statements it may ask for; the engine parses their flags for that command only and asks for them as consents a human answers by typing the statement. |

## ADR Template

Expand Down
9 changes: 9 additions & 0 deletions docs/product/cli-style-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,15 @@ When a command needs confirmation and cannot prompt:
- explain what needs confirmation
- suggest `-y` when it is a valid bypass

### Consent

Consent is a question `--yes` never answers and Enter never answers. It comes in two forms.

- **A yes/no consent** (`ctx.prompt.consent`). With a token, the user types the token, or passes `--confirm <token>`. Without a token, only an interactive terminal can grant it.
- **A statement** (`ctx.prompt.statement`). The user states what should happen to a subject with a verb: `delete`, or `rename Legacy:Archive`. The command declares its verbs, and each verb is a flag on that command only, so `--delete Legacy` gives the same answer on the command line. A flag value answers the question only when it names the subject: it is the subject, or starts with `<subject>:`.

Without an answer, a non-interactive run fails with `CLI.CONSENT_REQUIRED` and lists the flags that would answer every open question. A statement flag that no question used, or, on a command that declares statements, a `--confirm` token that no consent asked for, fails the run with `CLI.CONSENT_UNUSED`, because a mistyped subject or token must not pass silently.

## Loading Indicators

Loading indicators are for slow or remote work, not for every step.
Expand Down
2 changes: 2 additions & 0 deletions docs/product/error-conventions.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@ Human-readable errors should follow this shape:
5. where when relevant
6. hint for `--log-level verbose` when helpful

A `why` that runs over several lines is indented on every line, each continuation line aligned under the text after `why: `.

Example:

```text
Expand Down
12 changes: 9 additions & 3 deletions docs/reference/error-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,12 @@ The config file's `$prismaConfig` marker declares a version other than the one t

A consent prompt was reached under `--yes` or in a non-interactive session. Consent has no default answer and `--yes` does not grant it, so there is nothing for the run to assume. When the consent declares a token, the message and next action say to pass `--confirm <token>`, and the token travels in meta; without a token, the only path is running the command interactively. Meta: `consentToken` (only when the consent declares a token).

A statement prompt (`ctx.prompt.statement` or `ctx.prompt.statements`) raises the same code when no statement flag on the command line answers it. One error lists every question still unanswered: the summary names the subjects, `why` carries the questions, and the next actions give one flag to pass per verb, such as `--delete Legacy` or `--rename 'Legacy:<new name>'`. `why` also lists every given statement flag that names none of the questions' subjects, and any `--confirm` token, which answers no statement, so a misspelled flag is reported before anyone answers a prompt. Meta: `unanswered` (a list of `{ subject, verbs }`), plus `subject` and `verbs` when exactly one question is unanswered; `unmatched` (a list of `{ verb, values }`) and `confirm` (the tokens) when any.

### CLI.CONSENT_UNUSED

A consent flag was given but answered nothing: a statement flag such as `--delete Legacy`, or a `--confirm` token. The summary says why for each flag: no statement prompt asked about that subject and no `ctx.statements.take` took it (usually a mistyped subject), the question about it was already answered by another flag (a flag given twice, or two verbs for one subject), or no consent in the run asked for that `--confirm` token. The `--confirm` case applies only to a command that declares statements, where such a token is certainly a mistake; elsewhere a leftover `--confirm` is still ignored until [prisma/prisma-cli#339](https://github.com/prisma/prisma-cli/issues/339) lands. `why` lists the subjects the run asked about. Raised when the handler returns from a run that would otherwise have succeeded, so a run that failed for another reason reports that reason only; or earlier, as soon as `ctx.prompt.statements(questions, { last: true })` has answered its questions, before the command acts. Exits 2. Meta: `unused` (a list of `{ verb, values }`), `confirm` (the unused `--confirm` tokens, when any), `asked` (the subjects asked about, when any).

### CLI.CREDENTIALS_LOCKED

The advisory lock on the stored-credentials file was held by another prisma process for longer than the wait timeout, so this run's credential mutation gave up; the fix is to wait for the other command and retry. Raised by the auth state file's lock helper in `packages/cli`. Meta: none.
Expand All @@ -178,7 +184,7 @@ A bug, not a user error: a non-structured throw from a handler, an engine invari

### CLI.INVALID_ARGUMENTS

The invocation's arguments did not parse or contradict each other. Raise sites: stricli's argument-parse failure mapped at the adapter boundary (its usage text becomes summary and `why`), `--config=` given an empty value, `prisma init --skills` given `none` combined with agent names or an unknown agent name, and `prisma skills sync` given both `--disable` and `--enable`. Exits 2 as a usage error. Meta: none.
The invocation's arguments did not parse or contradict each other. Raise sites: stricli's argument-parse failure mapped at the adapter boundary (its usage text becomes summary and `why`), `--config=` given an empty value, a statement flag (such as `--delete`) given the wrong number of values for its declared arity or an empty value, `prisma init --skills` given `none` combined with agent names or an unknown agent name, and `prisma skills sync` given both `--disable` and `--enable`. Exits 2 as a usage error. Meta: none.

### CLI.MISSING_DEPENDENCY

Expand All @@ -190,11 +196,11 @@ A `ctx.packages` operation (an install, or running a package through the manager

### CLI.PROMPT_CANCELLED

The user cancelled a prompt: EOF on stdin at a line-rendered prompt, a clack cancel (Ctrl-C at the prompt UI), an abort during a browserWait poll, Ctrl-C at the `prisma auth login` paste prompt, or — via the service commands' `userCancelledError` — consent declined interactively. Settles with exit 3, the cancellation code, instead of 2. Meta: none.
The user cancelled a prompt: EOF on stdin at a line-rendered prompt, a clack cancel (Ctrl-C at the prompt UI), a SIGINT or SIGTERM delivered while a prompt waits for input, an abort during a browserWait poll, Ctrl-C at the `prisma auth login` paste prompt, or — via the service commands' `userCancelledError` — consent declined interactively. Settles with exit 3, the cancellation code, instead of 2; a SIGTERM that cancels a waiting prompt settles 143 instead, because it ends the run as a delivered signal, while a SIGINT is the user declining, like Ctrl-C. After a signal has cancelled a prompt, every later prompt in the run is cancelled at once and a further signal force-exits. Meta: none.

### CLI.PROMPT_INVALID

An answer could not be interpreted: not a yes/no for a confirm, not one of a select's options with no default to fall back to, or a consent token typed wrong where re-prompting is impossible (scripted answers or piped stdin — the interactive clack renderer re-prompts instead). Meta: `consentToken` (token-mismatch raise only).
An answer could not be interpreted: not a yes/no for a confirm, not one of a select's options with no default to fall back to, a consent token typed wrong where re-prompting is impossible (scripted answers or piped stdin — the interactive clack renderer re-prompts instead), or a statement answer the command rejected. A statement answer is rejected when a verb flag's value fails the command's check (re-prompting cannot correct a flag), or when a typed answer does not start with one of the verbs or fails the check where re-prompting is impossible; the summary carries the command's reason. Meta: `consentToken` (token-mismatch raise only).

### CLI.PROMPT_REQUIRED

Expand Down
Loading
Loading