diff --git a/.drive/projects/prisma-cli-v8/assets/briefs/1b-leftovers-prisma-prisma.md b/.drive/projects/prisma-cli-v8/assets/briefs/1b-leftovers-prisma-prisma.md new file mode 100644 index 00000000..1deb23e3 --- /dev/null +++ b/.drive/projects/prisma-cli-v8/assets/briefs/1b-leftovers-prisma-prisma.md @@ -0,0 +1,39 @@ +# Brief: prisma/prisma config-contract repairs and ControlClient test double + +Repo: prisma/prisma (main). Three independent deliverables; land as separate PRs or one PR with separate commits. Operator: Will Madden. Process: verify every claim below against current code before changing it; if you hit a judgment call this brief doesn't settle, stop and report it as a numbered question with options and a recommendation — do not decide it yourself. + +## Context + +These are the remaining config/contract items from the CLI-consolidation work. The governing rules are in-repo: ADR 239 (structural error envelopes, dotted codes), ADR 245 (errors structured at origin, no catch-all codes; one `ok` discriminator on results), and `docs/CLI Style Guide.md` (exit codes). The error-code registry is `docs/reference/error-reference.md`, enforced by `pnpm run check:error-reference` — any new code or new producing site is added there in the same change. + +## Deliverable 1: config loading returns diagnostics instead of throwing + +Today `loadConfig` (`packages/1-framework/3-tooling/config-loader/src/load.ts`, validation in `packages/1-framework/1-core/config/`) throws `CONFIG.VALIDATION_FAILED` / `CONFIG.EVALUATION_FAILED`-shaped errors on the first problem. The target semantics: + +- Evaluating a config module never takes down the command wholesale. Loading returns the evaluated config **plus a diagnostics list** (each diagnostic a `CliErrorEnvelope`-shaped structured error with a `meta.section` identifying the config section it concerns). +- A command that needs section X fails (exit 2, rendering that diagnostic) only if section X has a diagnostic; commands not touching X proceed. +- A config file that cannot be evaluated at all (module threw, unparseable) yields a single evaluation diagnostic attached to no section; every command then fails early with it. +- Existing error codes are reused; genuinely new conditions get new registered codes. No behavior change to what users *see* for currently-failing configs beyond message framing — pin representative `--json` envelopes before and after. + +Design the exact return type first (the repo convention is the shared `Result` from `@internal/utils/result`, but a config-with-diagnostics is not a failure — a `{ config, diagnostics }` value inside `Ok` is the expected shape) and validate it against every `loadConfig` call site before writing code. + +## Deliverable 2: versioned `defineConfig` marker + +`defineConfig` (`packages/1-framework/1-core/config/src/config-types.ts`) stamps the object it returns with a config-format version marker (non-enumerable; survives spreads is NOT required — document that configs must return the `defineConfig` result directly). The loader then enforces: + +- Marker present and current → proceed. +- Evaluation succeeded but no marker (a plain object export, or a config produced by a different `defineConfig` — i.e. a classic Prisma 7 file once the unified filename lands) → **fail early** with a new registered code (suggested: `CONFIG.UNVERSIONED_CONFIG`) whose fix text names `defineConfig` and links the migration path. This ruling is settled: fail early; no best-effort reading of unmarked configs. + +The marker's purpose is downstream: the future unified host loader will claim `prisma.config.ts`, a filename Prisma 7 already uses, and must distinguish the two by marker rather than misparse. Build the marker and enforcement here; do not build any Prisma-7-filename discovery in this repo. + +## Deliverable 3: published fixture-backed `ControlClient` test double + +Hosts and product tests need to drive the CLI's control-api surface without a real database. Export a fixture-backed double of the control client (`packages/1-framework/3-tooling/cli/src/control-api/client.ts`) from a **published** entrypoint (decide placement against how `@prisma/orm-toolchain` composes its published surface; the double must not drag the real driver/database imports into consumers). It covers every seam operation the control API exposes, returns the shared `Result` shapes with realistic fixture payloads, and its per-operation fixtures are overridable per test. Add a conformance-style test asserting the double's surface stays in sync with the real client (compile-time: same operation names and signatures). + +## Verification (all must pass before pushing) + +`pnpm turbo build --filter=@internal/cli...`; full test + typecheck + lint in config-loader, config, cli packages; `test/integration` typecheck; `pnpm run check:error-reference` with zero failures; `pnpm lint:deps`. + +## Commit discipline + +Explicit staging only. `git commit -s --trailer "Signed-off-by: Will Madden "`, body ends with `Co-Authored-By: `. Push via the `bot` remote, never origin. Verify the PR is open before any push to an existing PR branch. diff --git a/.drive/projects/prisma-cli-v8/assets/briefs/1c-leftovers-composer.md b/.drive/projects/prisma-cli-v8/assets/briefs/1c-leftovers-composer.md new file mode 100644 index 00000000..8fe87a84 --- /dev/null +++ b/.drive/projects/prisma-cli-v8/assets/briefs/1c-leftovers-composer.md @@ -0,0 +1,38 @@ +# Brief: composer config-contract compliance and control-API test double + +Repo: prisma/composer (main). Three deliverables. Operator: Will Madden. Process: verify every claim against current code first; stop and report numbered questions with options and a recommendation on anything this brief doesn't settle. + +## Context + +Composer's error/result rules are recorded in its ADR-0043 and ADR-0044 (`docs/design/90-decisions/`): structured errors at origin with dotted codes from the closed registry, one `ok` discriminator, exit 1 = bug only. The registry in ADR-0044 is closed — a new subcode is an edit to that list in the same change. Constraint that must not move: the effect constellation stays pinned at `4.0.0-beta.103` via the consumer overrides block (alchemy is broken on effect >= beta.104; see `skills-contrib/upgrade-alchemy-effect/SKILL.md`). + +## Deliverable 1: config validation returns diagnostics instead of throwing + +Today `load-config.ts` / `validate-coverage.ts` throw on the first invalid field. Target semantics (the config contract all products will share): + +- Loading a config returns the evaluated value **plus a diagnostics list** (structured errors, each tagged with the config section/field it concerns via `meta`), instead of throwing per field. +- Commands fail (exit 2, rendering the diagnostic) only when a section they need is invalid. +- A config module that cannot be evaluated at all yields one evaluation diagnostic (`CONFIG.EVALUATION_FAILED` already exists) and every command fails early with it. +- No import-time side effects and no throwing from `defineConfig`-equivalent factories: constructing a config value never throws; problems surface as diagnostics at load time. +- Pin representative rendered/`--json` output before and after — user-visible behavior for currently-failing configs may only change in framing. + +## Deliverable 2: effect-resolution preflight becomes a diagnostic + +`check-effect-resolution.ts` currently detects a mismatched `effect` in the consumer's tree by throwing during import, which takes out every command — including ones that never touch the deploy executor. Target: + +- The preflight runs at config-load/command-dispatch time, not import time, and surfaces as a structured diagnostic (`DEPS.EFFECT_VERSION_CONFLICT`, already registered) carried in the diagnostics list from Deliverable 1. +- Commands that need the executor fail early rendering it; commands that don't (e.g. help, config inspection) still work. +- The lazy executor-load failure path (`DEPS.EXECUTOR_UNLOADABLE`) is unchanged — it remains the backstop when the preflight didn't fire. +- The effect-CI probe and the `npm install effect dedupe` check must still pass; do not weaken either. + +## Deliverable 3: published test double for the control API + +Hosts driving `@prisma/composer/control` (deploy/destroy/dev/log) need a double that never spawns alchemy or containers. Export a fixture-backed double from a published entrypoint (placement judged against the existing `./control` shim in `packages/9-public/composer`): same operation signatures, same `Result<…, CliStructuredError>` shapes, per-operation fixtures overridable per test, including a `DevSession` double whose lifecycle methods behave. Add a compile-time conformance check that the double's surface matches the real operations. + +## Verification (all must pass before pushing) + +`pnpm build`, `pnpm typecheck`, `pnpm lint`, `pnpm lint:casts` (delta 0), `pnpm lint:deps` (all sub-checks), `@internal/cli` and integration test suites, `pnpm run check:npm-effect-resolution`. Known pre-existing failures that are not yours to fix: the `@internal/local-target` timeout pair. + +## Commit discipline + +Explicit staging only. `git commit -s --trailer "Signed-off-by: Will Madden "`, body ends with `Co-Authored-By: `. Composer's `origin` in the operator's clones is the bot SSH alias; verify the target PR is open before pushing to an existing PR branch. diff --git a/.drive/projects/prisma-cli-v8/assets/engine/daemon-library-notes.md b/.drive/projects/prisma-cli-v8/assets/engine/daemon-library-notes.md new file mode 100644 index 00000000..627dfdb1 --- /dev/null +++ b/.drive/projects/prisma-cli-v8/assets/engine/daemon-library-notes.md @@ -0,0 +1,60 @@ +# Daemon library — design conclusions, parked + +Status: **excluded from the CLI-engine scope** (Will, 2026-08-09) — it is a +runtime dependency of product control clients, orthogonal to the engine. +These notes preserve what the design conversation concluded so the work is +picked up, not re-derived. Evidence citations: `output-modes-survey.md` +(emulators/daemons section). + +## The finding that shaped the engine + +"Daemon mode" needs **zero engine surface**. Commands touching daemons are +ordinary commands: `ls` is a result command presenting a table over +`scan()`; `stop` presents a result over `stop()`; `composer dev` calls +`ensure()` during startup then runs as a normal session command. The +daemon-ness lives in what handlers do (operations layer), like spawning +alchemy already does. + +## What the library is + +The lifecycle-and-discovery primitives that Composer's +`dev-emulators/src/daemon.ts` and `@prisma/dev`'s state layer each +hand-built (convergent evolution — the evidence they're one concept): + +- **ensure(name, entry, opts)** — idempotent start: read registry entry, + probe health (identity/version-matched), adopt a healthy same-version + daemon, terminate-and-replace a stale-version one, spawn detached+unref + when absent, record `{pid, port, version, logPath}`, await health — all + serialized under a lockfile so concurrent CLI invocations can't race. +- **stop(name)** — SIGTERM, grace, SIGKILL, remove entry. +- **scan() / status(name)** — registry entries probed to + running/starting/dead. +- **logs(name)** — per-daemon stdio log file. +- Daemon-side: an entry-script harness (bind localhost port, serve + /health + the product's admin API, SIGTERM cleanup). + +Each daemon's **admin API stays product-owned**; the library owns only +lifecycle and discovery. Registry entries need a product-data extension +slot (same shape of reasoning as the engine's R14). + +## Open questions when picked up + +1. **Unified machine-wide registry vs per-product registries + an + aggregating command.** Unified (one `ls` shows Composer emulators and + dev servers; one stop semantics; the second liveness implementation + stops existing) costs a real `@prisma/dev` internal migration + (`server.json` format, its `proper-lockfile` usage) including + old-format servers; per-product costs nothing now but keeps two + liveness protocols forever and adds one per future daemon. +2. **The package's home.** +3. The management-command surface the grammar parked (`emulator` + root: ls/stop/status) — the gap that today lets Composer leave + daemons on the machine with no user-facing way to list or stop them + (`stopDaemon` is "not called by any v1 command"). + +## Effect on @prisma/dev (under unification) + +Public API (`startPrismaDevServer`, scan/status surface) unchanged; +internals swap to the shared library; its domain fields (ports, exports) +ride the registry's extension slot; migration must handle servers created +under the old on-disk format. diff --git a/.drive/projects/prisma-cli-v8/assets/engine/engine-interface-draft.ts b/.drive/projects/prisma-cli-v8/assets/engine/engine-interface-draft.ts new file mode 100644 index 00000000..51f346db --- /dev/null +++ b/.drive/projects/prisma-cli-v8/assets/engine/engine-interface-draft.ts @@ -0,0 +1,884 @@ +/** + * DRAFT v8 — the unified CLI engine's public interface. + * v1 initial · v2 round-1 fixes · v3 return-site presentation · + * v4 completed/errored, --format, log levels, prompt defaults · + * v5 round-3 closure · v6 Diagnostic, warnings fold, stream flatten · + * v7 outcome-first present, help/args/needs, product manifests · + * v8 packaging and residue rulings: ONE library package + * (@prisma/cli-engine, with @stricli/core as an ordinary exact-pinned + * dependency — bundling was considered and rejected: unusual for a + * library, blinds security audit; R3's hiding is about types, which no + * dependency violates) with a ./protocol subpath for types-only + * consumers; + * NextAction.journey dropped (no consumer); docs URLs derived from a + * manifest-supplied base; committed versions for releases; auth library + * lives in the CLI repo, distinct from Prisma Cloud. Prior versions + * preserved as -v1…-v7.ts; reviews in ./reviews/. + * + * THE MODEL, in one analogy (operator, 2026-08-09): commands settle like + * promises. A command can COMPLETE — and its completion can be + * successful or unsuccessful, both presented through the same machinery, + * distinguished by exit code and diagnostics — or it can ERROR, which + * aborts out of the normal process and gets its own special handling. + * + * Implementation prerequisite: ADR 239 (prisma/prisma) is amended so + * completed-but-unsuccessful command results (verify/check/runner + * findings) are carried as diagnostics with their dotted codes inside a + * completed envelope with a documented exit code — not as structured + * failures with exit 2, as it classifies them today. The amendment also + * checks whether any shipped error uses severity 'info'; if none does, + * the severity scale of CliStructuredError and Diagnostic trims to + * error|warn — together, so the two shapes stay identical. + * + * Everything a product package imports for CLI purposes lives here (R3). + * Requirement references (R1–R14) point at docs/architecture/ + * cli-engine-requirements.md in prisma-cli. Nothing from stricli appears — + * it is an internal of the engine package. + * + * EXECUTION PROTOCOL. A handler receives (args, context), emits zero or + * more events through context.report, and finishes one of two ways: + * + * COMPLETED — it returns ok(ctx.present(outcome, presentations)): the + * command executed to its end. The outcome is what it concluded — + * data, diagnostics (recorded findings — data, never thrown), and, + * when the command documents exit codes, an explicit code at every + * return site. Presentation always runs for completed results. + * + * ERRORED — it returns notOk(structuredError): the command did not + * complete. The engine renders the error envelope; there is no product + * presentation on the error path. The primary error is severity + * 'error' by definition. `remediation` events emitted before the + * error are aggregated into the errored envelope's nextActions. + * + * PRECONDITIONS (a command's `needs`) are enforced by the engine BEFORE + * the handler loads: an invalid needed config section, missing + * credentials, an absent optional dependency, or a non-interactive + * context each fail the command early with the engine's own structured + * error — a handler only ever runs in a world where it can operate. + * + * Session commands run until context.signal fires, then clean up and + * return. Server commands hand the stdio conversation to a foreign + * client. Liveness display is the engine's. Nothing product-authored + * executes after the handler resolves. + * + * FORMATS AND LEVELS. `--format `, auto-selected when + * unspecified (human on a TTY stdout, json otherwise); `--json` is + * shorthand for `--format json`. In json mode the engine suppresses + * prompts (they fail structurally) and emits one StreamEvent per line. + * Commentary is filtered by `--log-level ` + * (default info); `--verbose` is shorthand for `--log-level verbose`. + * The engine injects the shared flag family on every non-server + * command: --format/--json, --log-level/-v/--verbose, -q/--quiet, + * -y/--yes, --interactive/--no-interactive, --color/--no-color. + * Products cannot declare flags with those names. Product flag keys are + * camelCase and transliterate to --kebab-case. + * + * EXIT CODES (R6): 0 completed; 1 bug only; 2 errored (expected, + * structured); 3 user abort; 4–99 documented per command in + * `exitCodes`; 130/143 delivered signals. First signal fires + * context.signal and awaits teardown; a second exits immediately. + */ + +// ———————————————————————————————————————————————————————————————————————— +// Protocol types — the ./protocol subpath of this same package: the +// shapes that cross package and process boundaries (CliStructuredError, +// Result, NextAction, Diagnostic). Types-only consumers (the repos' +// duplicated foundations, external tools) import the subpath and drag +// nothing else. Shown for reading convenience. +// ———————————————————————————————————————————————————————————————————————— + +import type { CliStructuredError, Diagnostic, NextAction, Result } from '@prisma/cli-engine/protocol' + +/* + * For reference — the foundation shapes this file leans on: + * + * Diagnostic — a recorded finding: pure data, never thrown, no stack. + * { code: 'NAMESPACE.SUBCODE', severity: 'error' | 'warn' | 'info', + * summary, why?, fix?, where?, meta?, docsUrl? } + * Field-for-field the settled error envelope (ADR 239) minus `ok` — + * identical scales included; the two shapes never diverge. + * + * NextAction — the typed agent-facing follow-up (platform-shipped form, + * minus its `journey` grouping label — dropped: no consumer branches on + * it; R14's evidence rule readmits it if one appears): + * { kind: 'run-command' | 'user-choice' | 'edit-file' | 'done', + * label, command?, commands?, reason? } + */ + +/** The commentary severity scale; also the log-level axis. Distinct from + * Diagnostic severity: 'verbose' grades commentary, which never enters + * the envelope. Step outcomes are completion states, not severities. */ +export type Severity = 'error' | 'warn' | 'info' | 'verbose' +export type LogLevel = Severity + +export type Format = 'human' | 'json' + +// ———————————————————————————————————————————————————————————————————————— +// §1 Events — R14: one engine vocabulary, product extensions in `data` +// ———————————————————————————————————————————————————————————————————————— + +/** + * The engine event envelope. `kind`-specific fields are the common + * vocabulary the engine renders consistently (human mode) and streams + * (json mode, §9). `data` is the product extension: passed through to + * machine consumers untouched, never interpreted by the engine, + * documented and versioned by the product as its own public API. A + * structure recurring inside `data` across commands is the promotion + * signal (R14). + * + * Rendering, human mode: `output` events with channel 'data' are the + * command's data and go to OUR stdout; everything else is commentary on + * stderr, filtered by the active log level. Events are transcript: they + * are NOT aggregated into the envelope (the one exception is + * `remediation` → nextActions). Findings that belong in the envelope + * are diagnostics on the presented outcome, not events. + * + * report() is synchronous fire-and-forget; the engine buffers and writes + * asynchronously. Calling it after the handler has resolved is a bug + * (InternalError). Events during teardown (after the signal, before + * resolution) are normal. + */ +export type EngineEvent = + | { + readonly kind: 'step-started' + readonly step: string + readonly id?: string + readonly parentId?: string + readonly data?: unknown + } + | { + readonly kind: 'step-finished' + readonly step: string + readonly id?: string + readonly outcome: 'ok' | 'failed' | 'skipped' | 'warning' + readonly data?: unknown + } + | { + readonly kind: 'progress' + readonly step?: string + readonly completed: number + readonly total?: number + readonly data?: unknown + } + /** Commentary at a severity; display-filtered by log level. Transcript + * only. 'error' is not valid here: fatal problems are the Result's + * error; envelope-worthy findings are diagnostics. */ + | { + readonly kind: 'message' + readonly severity: Exclude + readonly text: string + readonly data?: unknown + } + | { + readonly kind: 'output' + readonly source: string + readonly channel: 'data' | 'diagnostic' + readonly line: string + readonly data?: unknown + } + | { readonly kind: 'remediation'; readonly action: NextAction; readonly data?: unknown } + | { + readonly kind: 'endpoint' + readonly name: string + readonly url: string + readonly data?: unknown + } + | { + readonly kind: 'status' + readonly subject: string + readonly status: string + readonly from?: string + readonly data?: unknown + } + | { + readonly kind: 'artifact' + readonly path: string + readonly description?: string + readonly data?: unknown + } + +// ———————————————————————————————————————————————————————————————————————— +// §2 Outcomes and presented results — presentation materializes at the +// return site +// ———————————————————————————————————————————————————————————————————————— + +/** + * What a command concluded, stated at the return site. `exitCode` is + * REQUIRED at every return site iff the command documents exit codes + * (`0` for the clean path, a documented code otherwise — the type makes + * forgetting impossible exactly where a decision exists) and forbidden + * otherwise. `diagnostics` may be omitted at the call site; the + * presented result always carries an array. + */ +export type Outcome = [TCode] extends [never] + ? { + readonly data: T + readonly diagnostics?: readonly Diagnostic[] + } + : { + readonly data: T + readonly exitCode: TCode | 0 + readonly diagnostics?: readonly Diagnostic[] + } + +declare const PRESENTED: unique symbol + +/** + * What a completed command's handler returns inside `ok(...)`: the + * outcome plus the presentation the ACTIVE FORMAT already materialized. + * Built exclusively by ctx.present (the brand makes hand-construction a + * type error) — the context knows the format, calls only the + * presentation functions it needs, and the value crossing the + * product→engine boundary is data all the way down. + * + * `data` is what the envelope's `result` serializes (json presentation + * overrides when supplied). Materialization by format: human → human + + * stdout + next; human+--quiet → stdout; json → json + next. + * + * Guardrail (runtime, at the return site): a severity-'error' diagnostic + * requires a non-zero exitCode — a genuine could-not-complete belongs in + * notOk. The test: notOk when the command couldn't do its job; + * diagnostics when finding these WAS the job. + */ +export interface PresentedResult { + readonly [PRESENTED]: true + readonly data: T + /** 0 unless the outcome selected a documented code. */ + readonly exitCode: number + /** Never undefined; empty when the outcome recorded no findings. */ + readonly diagnostics: readonly Diagnostic[] + readonly presentation: { + readonly human?: readonly Block[] + readonly stdout?: readonly string[] + readonly json?: unknown + readonly next?: readonly NextAction[] + } +} +export { PRESENTED } + +/** + * The per-format presentation functions a handler supplies to + * ctx.present. Only the active format's functions are invoked, at the + * return site. `human` composes engine primitives (R5); `stdout` is the + * machine-consumable data lines — what --quiet leaves, what a pipe + * receives; `json` overrides the envelope's `result` (default: the + * data); `next` supplies the typed nextActions — agents branch on them; + * the human renderer formats them as prose (no stored string form). + */ +export interface Presentations { + readonly human: (ui: Ui) => readonly Block[] + readonly stdout?: () => readonly string[] + readonly json?: () => unknown + readonly next?: () => readonly NextAction[] +} + +// ———————————————————————————————————————————————————————————————————————— +// §3 Config sections — a PRODUCT-level fact (one product, one section) +// ———————————————————————————————————————————————————————————————————————— + +/** + * A product's named slice of prisma.config.ts, declared once in the + * product's manifest (§10). The token couples the section name, its + * validated type, and its total validator. Commands that need the + * section reference the token in `needs.config`, which is how the + * engine knows which commands an invalid section fails — and how + * ctx.config gets its type. + * + * The validator OWNS absence: its input is the raw section value, or + * undefined when the config file has no such section. A product that + * wants defaults applies them here; one that requires the section emits + * a section-required diagnostic here. It returns findings; it never + * throws (R10). Keep validators dependency-light: they load with the + * definition tree at startup (R9). + */ +export interface ConfigSection { + readonly name: string + readonly validate: (raw: unknown | undefined) => SectionValidation +} + +export type SectionValidation = + | { readonly ok: true; readonly value: T; readonly diagnostics: readonly Diagnostic[] } + | { readonly ok: false; readonly diagnostics: readonly Diagnostic[] } + +export declare function defineConfigSection(spec: { + readonly name: string + readonly validate: (raw: unknown | undefined) => SectionValidation +}): ConfigSection + +// ———————————————————————————————————————————————————————————————————————— +// §4 The handler context — R4: the whole world arrives as one argument +// ———————————————————————————————————————————————————————————————————————— + +export interface CommandContext { + /** The validated value of the command's needed config section — + * exactly TConfig; absence semantics belong to the product's + * validator (§3). Commands with no config need get undefined. */ + readonly config: TConfig + + /** Builds the PresentedResult for the active format: "present this + * outcome, via these presentations." The only constructor of + * PresentedResult. */ + readonly present: ( + outcome: Outcome, + presentations: Presentations, + ) => PresentedResult + + /** Management-API credentials, resolved at call time so long-lived + * sessions survive token refresh. Undefined when unauthenticated. + * Commands with needs.credentials never see undefined — the engine + * fails them early with the sign-in error. */ + readonly getCredentials: () => Promise + + /** The one way to emit while running (§1). */ + readonly report: (event: EngineEvent) => void + + /** Interactive input (§4a). */ + readonly prompt: PromptSurface + + /** Fires on Ctrl-C/SIGTERM (engine-owned; second signal force-exits). + * Session commands run until it fires; everything else aborts + * in-flight work with it. */ + readonly signal: AbortSignal + + /** Where the user invoked the CLI. Products never read process.cwd(). */ + readonly cwd: string + + /** + * R13, the conditional form (evidence: composer needs @prisma/dev only + * when the config declares postgres resources — unconditional needs + * belong in `needs.dependencies`). Resolves when the optional peer + * dependency is importable from the user's project; otherwise returns + * the ENGINE'S structured missing-dependency error — install command + * phrased by the engine with the user's package manager — for the + * handler to pass to notOk. Products never craft install prose. + */ + readonly requireDependency: (specifier: string) => Promise> +} + +export interface Credentials { + /** Opaque to the engine; shape owned by the Cloud product's auth + * library (placeholder pending its design). Workspace selection is + * session state, not a credential — it lives with that library, not + * here. */ + readonly token: string +} + +/** + * §4a Prompts. Every prompt except `consent` may carry a + * product-specified `default`. Interactively, Enter accepts the default. + * Under --yes, a prompt WITH a default resolves to it without + * displaying; a prompt WITHOUT a default cannot be operated and the + * invocation halts with a structured error (exit 2). In + * json/non-interactive/CI/non-TTY contexts the same default rule + * applies. User cancellation (Ctrl-C at the prompt) is a distinct + * structured error the engine maps to exit 3. + */ +export interface PromptSurface { + readonly confirm: ( + question: string, + opts?: { readonly default?: boolean }, + ) => Promise> + /** + * A question requiring EXPLICIT consent — never inferable, not + * necessarily destructive. Structurally undefaultable: no default + * parameter exists, so --yes, Enter-through, and non-interactive + * contexts can never satisfy it; the command's fix names the explicit + * flag that grants consent non-interactively. + */ + readonly consent: (question: string) => Promise> + readonly select: ( + question: string, + options: ReadonlyArray<{ value: T; label: string }>, + opts?: { readonly default?: T }, + ) => Promise> + readonly text: ( + question: string, + opts?: { readonly placeholder?: string; readonly default?: string }, + ) => Promise> +} + +// ———————————————————————————————————————————————————————————————————————— +// §5 Flags and positionals — R1: directly executable, typed by inference +// ———————————————————————————————————————————————————————————————————————— + +/** + * Product-declared flags. The shared family (§header) is engine-injected + * and reserved; handlers never see those values. Parse-time validation + * failures become structured errors carrying the allowed values. + * + * Deliberate asymmetry, matching CLI convention: flags are optional by + * default (requiredString is the exception); positionals are required by + * default (optionalString is the exception). + */ +export declare const flag: { + string(spec: { + brief: string + placeholder?: string + alias?: A & Char + default?: string + }): FlagSpec + requiredString(spec: { + brief: string + placeholder?: string + alias?: A & Char + }): FlagSpec + number(spec: { + brief: string + placeholder?: string + alias?: A & Char + default?: number + }): FlagSpec + boolean(spec: { brief: string; alias?: A & Char }): FlagSpec + enum(spec: { + brief: string + values: T + alias?: A & Char + default?: T[number] + }): FlagSpec + repeated(spec: { + brief: string + placeholder?: string + alias?: A & Char + }): FlagSpec +} + +/** Single-character alias, enforced at the type level: `Char<'q'>` is + * 'q'; `Char<'ab'>` is never. */ +export type Char = S extends `${string}${infer Rest}` + ? Rest extends '' + ? S + : never + : never + +declare const FLAG: unique symbol +export interface FlagSpec { + /** Phantom carrier for inference; exported so declaration emit works. */ + readonly [FLAG]: T +} +export { FLAG } + +export declare const positional: { + string(spec: { brief: string; placeholder: string }): PositionalSpec + optionalString(spec: { brief: string; placeholder: string }): PositionalSpec + /** Zero or more trailing values; at most one, declared last (order = + * declaration order; keys must not be integer-like). */ + variadic(spec: { brief: string; placeholder: string }): PositionalSpec +} +declare const POSITIONAL: unique symbol +export interface PositionalSpec { + readonly [POSITIONAL]: T +} +export { POSITIONAL } + +/** The parse SPI: a command's argument surface, one property. */ +export interface ArgsSpec< + TFlags extends Record>, + TPositionals extends Record>, +> { + readonly flags?: TFlags + readonly positionals?: TPositionals +} + +/** What a handler receives: separate namespaces, symmetric access — + * `args.flags.to`, `args.positionals.name`. */ +export interface Args< + TFlags extends Record>, + TPositionals extends Record>, +> { + readonly flags: { readonly [K in keyof TFlags]: TFlags[K] extends FlagSpec ? T : never } + readonly positionals: { + readonly [K in keyof TPositionals]: TPositionals[K] extends PositionalSpec ? T : never + } +} + +// ———————————————————————————————————————————————————————————————————————— +// §6 Command definitions — light at startup (R9), path-free (R12), +// runtime-discriminated by `kind`. Three modalities at equal rank: +// result / session / server. +// ———————————————————————————————————————————————————————————————————————— + +/** The help SPI: how the command shows itself in help output. Words + * only — the engine formats. */ +export interface HelpSpec { + /** One line, imperative, shown in listings. */ + readonly summary: string + readonly description?: string + /** Copy-pastable invocations, shown verbatim. */ + readonly examples?: readonly string[] +} + +/** + * The preconditions SPI: everything the engine gates BEFORE the handler + * loads. Each unmet need fails the command early with the engine's own + * structured error — consistent phrasing by construction, and a handler + * never runs in a world where it can't operate. + */ +export interface NeedsSpec { + /** The product's config section token: validate it, fail me on its + * error diagnostics, hand me the value as ctx.config. */ + readonly config?: ConfigSection + /** Fail early with the sign-in error when unauthenticated. */ + readonly credentials?: true + /** Optional peer dependencies this command cannot run without; the + * engine probes resolvability and phrases the install error. + * (Conditional needs use ctx.requireDependency instead.) */ + readonly dependencies?: readonly string[] + /** + * Fail early (before execution, before side effects) in + * json/non-interactive/CI/non-TTY contexts. This is a MECHANICAL + * precondition — "an interactive terminal is required" — and + * deliberately NOT an agent barrier: the client's nature is + * unverifiable, and a flag claiming to exclude agents would be a + * false guarantee. Anything requiring a verified human belongs + * server-side, where identity actually exists. + */ + readonly interaction?: true +} + +export interface CommandDefinition< + TFlags extends Record> = {}, + TPositionals extends Record> = {}, + TConfig = undefined, + TCode extends number = never, +> { + readonly kind: 'result-command' + readonly help: HelpSpec + readonly args?: ArgsSpec + readonly needs?: NeedsSpec + + /** + * The command's documented exit codes (4–99): code → meaning. + * Rendered in help without executing anything; the keys type the + * outcome's exitCode, making it REQUIRED at every return site (`0` or + * a documented code). Absent = the command only exits 0/1/2/3 and the + * outcome carries no exitCode. + */ + readonly exitCodes?: Readonly> + + /** The heavy part, loaded only at execution (R9). The module's default + * export is the handler — annotate it with CommandHandler + * (type-only import of the light definition; no runtime cycle). */ + readonly handler: () => Promise<{ default: Handler }> +} + +export type Handler< + TFlags extends Record>, + TPositionals extends Record>, + TConfig, + TCode extends number = never, +> = ( + args: Args, + ctx: CommandContext, +) => Promise, CliStructuredError>> + +/** For impl files: `const run: CommandHandler = …` */ +export type CommandHandler = D extends CommandDefinition + ? Handler + : never + +export declare function defineCommand< + TFlags extends Record> = {}, + TPositionals extends Record> = {}, + TConfig = undefined, + TCode extends number = never, +>( + def: Omit, 'kind'>, +): CommandDefinition + +/** + * A session command (dev, log tail): runs until the signal fires, + * speaks entirely through events, returns Result. No + * presentation, no exit-code set. A session always supports json mode: + * the event stream is its json surface. + */ +export interface SessionCommandDefinition< + TFlags extends Record> = {}, + TPositionals extends Record> = {}, + TConfig = undefined, +> { + readonly kind: 'session-command' + readonly help: HelpSpec + readonly args?: ArgsSpec + readonly needs?: NeedsSpec + readonly handler: () => Promise<{ + default: ( + args: Args, + ctx: CommandContext, + ) => Promise> + }> +} + +export declare function defineSessionCommand< + TFlags extends Record> = {}, + TPositionals extends Record> = {}, + TConfig = undefined, +>( + def: Omit, 'kind'>, +): SessionCommandDefinition + +/** + * A server command (lsp): a foreign client on the other end of stdio + * owns the conversation, so the engine hands over the streams. Events, + * presentation, formats, and prompts do not apply — by definition, not + * by opt-out; the handler returns the exit code directly. The shared + * flag family is NOT injected. + */ +export interface ServerCommandDefinition< + TFlags extends Record> = {}, + TConfig = undefined, +> { + readonly kind: 'server-command' + readonly help: HelpSpec + readonly args?: ArgsSpec + readonly needs?: NeedsSpec + readonly handler: () => Promise<{ + default: ( + args: Args, + io: { + readonly stdin: InputStream + readonly stdout: OutputStream + readonly stderr: OutputStream + readonly signal: AbortSignal + readonly cwd: string + readonly config: TConfig + }, + ) => Promise + }> +} + +export declare function defineServerCommand< + TFlags extends Record> = {}, + TConfig = undefined, +>( + def: Omit, 'kind'>, +): ServerCommandDefinition + +/** Erased union for manifests and mount maps; `kind` discriminates. */ +export type AnyCommand = + | CommandDefinition + | SessionCommandDefinition + | ServerCommandDefinition + +// ———————————————————————————————————————————————————————————————————————— +// §7 Presentation primitives — the R5 vocabulary +// ———————————————————————————————————————————————————————————————————————— + +/** Deliberately small; grows by the same evidence rule as events. */ +export type Block = + | { + readonly kind: 'summary' + readonly tone: 'ok' | 'error' | 'warn' | 'info' + readonly text: string + } + | { + readonly kind: 'fields' + readonly rows: ReadonlyArray<{ label: string; value: string; sensitive?: boolean }> + } + | { + readonly kind: 'table' + readonly columns: readonly string[] + readonly rows: ReadonlyArray + } + | { readonly kind: 'list'; readonly items: readonly string[] } + | { readonly kind: 'tree'; readonly roots: readonly TreeNode[] } +// NOTE: recorded findings are NOT a Block — they are the outcome's +// diagnostics; the engine renders them with the top-level error layout +// and carries them into the envelope, so the two surfaces cannot +// diverge. + +export interface TreeNode { + readonly label: string + readonly children?: readonly TreeNode[] +} + +/** Styling helpers usable inside block text; no direct writing. */ +export interface Ui { + readonly emphasize: (text: string) => string + readonly dim: (text: string) => string + readonly code: (text: string) => string +} + +// ———————————————————————————————————————————————————————————————————————— +// §8 Streams — minimal structural types; no NodeJS.* in the public +// surface +// ———————————————————————————————————————————————————————————————————————— + +export interface OutputStream { + write(text: string): void +} +/** Byte-oriented, so server commands can implement byte-counted + * protocols (lsp's Content-Length framing). setRawMode is present + * where the platform supports keypress input. */ +export interface InputStream extends AsyncIterable { + readonly setRawMode?: (enabled: boolean) => void +} + +// ———————————————————————————————————————————————————————————————————————— +// §9 Envelopes and the json stream +// ———————————————————————————————————————————————————————————————————————— + +export interface CompletedEnvelope { + /** ok = COMPLETED (the command executed to its end). A completed + * result may still carry findings and a non-zero exit code — bad news + * is a result, not an error. */ + readonly ok: true + /** The command's stable dotted identity — its full mount path + * ('project.env.add'). The schema-dispatch key for machine consumers; + * says nothing about arguments. */ + readonly commandId: string + readonly result: T + readonly exitCode: number + /** The recorded findings, verbatim from the presented outcome. */ + readonly diagnostics: readonly Diagnostic[] + readonly nextActions: readonly NextAction[] +} + +export interface ErroredEnvelope { + /** ok = false: the command did NOT complete. */ + readonly ok: false + readonly commandId: string + /** The PRIMARY error — what aborted the command. Severity 'error' by + * definition. A thrown CliStructuredError serializes to exactly this + * shape. */ + readonly error: Diagnostic + /** Accompanying findings when the abort had several (three config + * typos are three diagnostics, not one flattened error). */ + readonly diagnostics: readonly Diagnostic[] + /** Aggregated from remediation events. */ + readonly nextActions: readonly NextAction[] +} + +/** json mode emits one StreamEvent per line: the handler's events, + * flattened with the stream metadata, then exactly one terminal + * 'result' member carrying the envelope. */ +export type StreamEvent = + | (EngineEvent & StreamMeta) + | ({ readonly kind: 'result'; readonly envelope: CompletedEnvelope | ErroredEnvelope } & StreamMeta) + +export interface StreamMeta { + readonly commandId: string + /** ISO 8601 UTC. Injectable clock in tests (§11). */ + readonly timestamp: string +} + +// ———————————————————————————————————————————————————————————————————————— +// §10 Product manifests and shell mounting — R12: the shell owns the +// tree; a product owns its section +// ———————————————————————————————————————————————————————————————————————— + +/** + * What a product package exports: its config section (declared once — + * a product-level fact) and its commands by NAME. The unified config + * loader consumes the sections; the shell mounts the commands. A + * command whose needs.config token is not its product's section is a + * construction error. + */ +export interface ProductManifest { + readonly configSection?: ConfigSection + readonly commands: Readonly> + /** The product's documentation base URL. The engine derives each + * diagnostic's docs link from base + code; a diagnostic's own + * `docsUrl` field is the per-raise override (unused until a use case + * appears). */ + readonly docsBaseUrl?: string +} + +/** What the shell builds: commands by PATH (space-separated, + * 'db migrate'). */ +export type MountedTree = Readonly> + +/** + * Shell-side construction. Group help is declared with the mount, since + * groups belong to the tree, not to products. Collisions, unknown + * groups, reserved-flag violations, grammar violations, and + * foreign-section references fail construction (build time, not run + * time). + */ +export declare function createCli(spec: { + readonly name: string + readonly version: string + readonly products: readonly ProductManifest[] + readonly groups: Readonly> + readonly commands: MountedTree +}): Cli + +export interface Cli { + /** Parse, execute, render, return the exit code. Never calls + * process.exit; never touches streams other than the provided ones. */ + run(argv: readonly string[], runtime: Runtime): Promise +} + +/** Everything environmental, injected once by the bin (or by a test). */ +export interface Runtime { + readonly stdout: OutputStream + readonly stderr: OutputStream + readonly stdin: InputStream + readonly cwd: string + readonly env: Readonly> + readonly isTty: { readonly stdin: boolean; readonly stdout: boolean; readonly stderr: boolean } + readonly signal: AbortSignal + /** Loaded config + file-level diagnostics; the shell builds this via + * the unified loader (R10). Tests hand in fixtures. */ + readonly config: LoadedConfig + readonly getCredentials: () => Promise + /** Used by the ENGINE to phrase install commands (products never + * do — see needs.dependencies and ctx.requireDependency). */ + readonly packageManager: 'npm' | 'pnpm' | 'yarn' | 'bun' | 'unknown' +} + +export interface LoadedConfig { + /** Raw section values by name; validation happens per command via its + * product's section token. */ + readonly sections: Readonly> + /** File-level problems (unevaluable module, missing version marker) — + * section: null fails every command. */ + readonly diagnostics: ReadonlyArray<{ + readonly section: string | null + readonly diagnostic: Diagnostic + }> +} + +// ———————————————————————————————————————————————————————————————————————— +// §11 The product-repo test harness — R7: same machinery, bytes out +// ———————————————————————————————————————————————————————————————————————— + +export declare function createTestCli(spec: { + readonly products?: readonly ProductManifest[] + readonly commands: MountedTree + readonly groups?: Readonly> + readonly config?: Readonly> + readonly credentials?: Credentials + readonly packageManager?: 'npm' | 'pnpm' | 'yarn' | 'bun' | 'unknown' + /** Fixed clock for deterministic stream timestamps. */ + readonly now?: () => Date +}): TestCli + +export interface TestCli { + run( + argv: readonly string[], + opts?: { + readonly stdin?: string + /** Scripted prompt answers, consumed in order; a run that prompts + * past the script fails the test. */ + readonly answers?: ReadonlyArray + /** Abort the run (session tests): fires the context signal. */ + readonly abort?: AbortSignal + /** Live event tap, for asserting mid-session behavior. */ + readonly onEvent?: (event: EngineEvent) => void + readonly cwd?: string + readonly isTty?: { stdin?: boolean; stdout?: boolean; stderr?: boolean } + readonly env?: Readonly> + }, + ): Promise<{ + readonly exitCode: number + readonly stdout: string + readonly stderr: string + /** Parsed stream (events + the terminal result) when json mode. */ + readonly json: readonly StreamEvent[] + /** Every EngineEvent the handler emitted, for semantic assertions. */ + readonly events: readonly EngineEvent[] + /** The PresentedResult the handler returned, for semantic + * assertions without byte-scraping. */ + readonly presented?: PresentedResult + }> +} diff --git a/.drive/projects/prisma-cli-v8/assets/engine/output-modes-survey.md b/.drive/projects/prisma-cli-v8/assets/engine/output-modes-survey.md new file mode 100644 index 00000000..601eaa88 --- /dev/null +++ b/.drive/projects/prisma-cli-v8/assets/engine/output-modes-survey.md @@ -0,0 +1,652 @@ +# Execution modes and event dialects across the three CLI families + +Snapshot: 2026-08-09. Evidence survey feeding the unified CLI engine's event +vocabulary (cli-engine-requirements.md R5, R6, R14) and its command-execution +modes. Style follows `docs/architecture docs/research/commander-friction-points.md`: +no claim without a citation. Path prefixes: + +- **ORM** = `packages/1-framework/3-tooling/cli/src` in prisma/prisma (this repo) +- **Composer** = `wip/repos/composer/packages/0-framework/3-tooling/cli/src` +- **Platform** = `wip/repos/prisma-cli/packages/cli/src` (plus `packages/compute`) +- **Emulators** = `wip/repos/composer/packages/1-prisma-cloud/0-lowering/dev-emulators/src` + +Sub-survey evidence was gathered per family and the central claims were +re-verified against the code (the span-event union, the `migrate` progress +gap, JSON auto-selection, the detached daemon spawn, the domain-wait loop). + +## A. Command inventory with execution mode + +Mode key (refined from the four-mode starting taxonomy; the final proposal is +in §F): + +- **S** — synchronous request/response: one operation call, one rendered result. +- **S+prog** — synchronous with in-flight progress reporting (spans, step + lines, or events) that ends when the operation ends. +- **P** — poll-until-terminal: a wait loop against a remote state machine, + with timeout semantics. +- **W** — long-lived session/watch: runs until signal/abort, re-emits over + its lifetime. +- **D** — daemon-coupled: the command manages or implicitly ensures a + machine-scoped process that outlives the CLI invocation. +- **X** — interactive wizard: prompts drive the flow. +- **V** — stdio protocol server: the process becomes a protocol endpoint. +- **B** — browser hand-off and/or local callback web server (a capability + layered on another mode, not a mode of its own — see §F). + +### A1. Prisma ORM (`prisma-next`) — commander main CLI + clipanion migration-file CLI + +Framework: commander for the main binary (cli.ts:71-73; commands registered +cli.ts:323-331), clipanion for the separate migration-file CLI, chosen for +in-process testability (migration-cli.ts:39-42). Global flags via +`addGlobalOptions`: `--format `, `--json`, `-q/--quiet`, +`-v/--verbose`, `--trace`, `--color/--no-color`, +`--interactive/--no-interactive`, `-y/--yes` (utils/command-helpers.ts:368-388). +Output discipline: stdout = data, stderr = decoration +(utils/terminal-ui.ts:9-23, 284-301). Uniform result funnel `handleResult` +with exit 2 for structured failure, 3 for user abort +(utils/result-handler.ts:16-44). Every command forks a detached telemetry +child at `preAction` (cli.ts:82-88; utils/telemetry.ts:160-177). + +| Command | Mode | Output pattern | `--json` | Citation | +|---|---|---|---|---| +| `init` | **X** + child processes | clack intro/outro, per-file logs, spinners for install/skills/emit, manual-steps note box | yes — arktype-validated `{ok, target, authoring, schemaPath, filesWritten[], filesDeleted[], packagesInstalled, contractEmitted, nextSteps[], warnings[]}` | commands/init/index.ts:45-113; init/output.ts:19-52; init/init.ts:113-143, 585-594 | +| `migrate` | S (DB session; **no progress adapter** — see B3 gap) | styled header; one `ui.step('Loading contract spaces…')`; final rendered block | yes — `{ok, migrationsApplied, migrationsTotal, markerHash, applied[], summary, perSpace[], pathDecision?, timings{total}, advancedRef}` | commands/migrate.ts:709-731, 777-779, 844-855, 928-938 | +| `migrate --show` | S (read-only preview) | full ASCII graph visualization with cross-space column alignment; skipped entirely under JSON | yes | commands/migrate.ts:159, 435-470, 910-925 | +| `format` | S | header; `ui.success`/`ui.info` line | yes — `{formatted, path?}` (compact) | commands/format.ts:20-74 | +| `lsp` | **V** — long-lived stdio LSP server | none (protocol on stdio); lazy-imports `@internal/language-server` | n/a (`--stdio` accepted, documented as the only transport) | commands/lsp.ts:8-31; language-server/src/start-server.ts:1-7; server.ts:628-629 | +| `contract emit` | **S+prog** (spans `resolveSource`, `emit`) | header; spinner per span; `ui.warn` | yes — `{ok, storageHash, executionHash?, profileHash?, outDir, files{json,dts}, timings{total}}` | commands/contract-emit.ts:105-130, 180-192; utils/formatters/emit.ts:55-66 | +| `contract infer` | **S+prog** + writes file | header; spans; `✔ Contract written to ` | yes — `{ok, summary, target, psl{path}, meta, timings}` | commands/contract-infer.ts:27-38, 70-93, 114-127 | +| `db verify` (full / `--marker-only` / `--schema-only`) | **S+prog** (spans `connect`, `verify`, `introspect`) | header; spinner spans; result block; drift rendered even under `--quiet`, exit **1** on drift | yes — mode-discriminated verify shape | commands/db-verify.ts:214-215, 366, 389-450, 458-499, 544-583; utils/formatters/verify.ts:38-74, 140-158 | +| `db init` | **S+prog** (spans `connect`, `introspect`, `plan`, `apply` + nested per-operation spans) | header incl. `mode: dry run`; spinner per top-level span; plan tree or apply summary | yes — `MigrationCommandResult` (see B3) | commands/db-init.ts:147-152, 194-231; utils/migration-command-scaffold.ts:79-89, 161; utils/formatters/migrations.ts:51-95 | +| `db update` | **S+prog** + interactive re-run loop: on destructive-op rejection, prompts and **re-executes the whole command** with `yes:true` | as `db init` | yes — same shape | commands/db-update.ts:170-176, 320-343, 345-357 | +| `db schema` | **S+prog** (read-only) | header; introspection tree | yes — `IntrospectSchemaResult` | commands/db-schema.ts:18-29, 48-74 | +| `db sign` | **S+prog** (spans `schemaVerify`, `sign`) | header; from→to hashes; exit **1** on verify failure | yes | commands/db-sign.ts:205-229, 293-325 | +| `migration plan` | S, offline, writes migration packages + snapshots (with cross-space seed side effects) | header; `ui.step` per seeded space; final tree/summary | yes | commands/migration-plan.ts:235-242, 382-400, 726-767 | +| `migration new` | S, offline, scaffolds files | header; success block naming dir/from/to | yes — `{ok, dir, from, to, summary}` | commands/migration-new.ts:236-302 | +| `migration show` | S, offline | header; operations + SQL preview | yes | commands/migration-show.ts:102-115, 213-258; commands/json/schemas.ts:156-177 | +| `migration status` | S; DB-connected by default, offline with `--from` | header; tree sections | yes — includes per-migration `status: 'applied'\|'pending'\|null` and `diagnostics[]` with `hints[]` | commands/migration-status.ts:383-384, 639-712; json/schemas.ts:70-121 | +| `migration log` | S, DB required | header; table render | yes | commands/migration-log.ts:67-161; json/schemas.ts:123-141 | +| `migration list` | S, offline | header + optional legend; tree | yes | commands/migration-list.ts:209-253, 304-328 | +| `migration graph` | S, offline; three output modes — `--dot` (Graphviz to stdout) **takes precedence over `--json`** | header; tree; DOT | yes | commands/migration-graph.ts:95-96, 240-270 | +| `migration check` | S, offline; own exit-code scheme (0/2/**4** = integrity failed, via `exitOverride`) | `✔ summary` or per-failure `✗ [CODE] where: why` + `fix:` | yes — `{ok, failures[{space,code,where,why,fix}], summary}` | commands/migration-check.ts:377-378, 604-698; migration-check/exit-codes.ts:1-3 | +| `ref set` / `delete` / `list` | S, offline | one line each | yes (compact) | commands/ref.ts:178-269 | +| `telemetry status` / `enable` / `disable` | S | 1–3 lines | yes (compact) | commands/telemetry/index.ts:18-83 | +| migration-file CLI (`node migration.ts`) | S; separate clipanion CLI | `--dry-run` prints framed `--- migration.json ---` / `--- ops.json ---` blobs; else writes files + one line | **no `--json`** (flags: `--help`, `--dry-run`, `--config`) | migration-cli.ts:104, 113-149, 508-529; exit codes 0/1/2 :181-186 | + +### A2. Composer (`prisma-composer`) — clipanion, 4 commands + +| Command | Mode | Output pattern | `--json` | Citation | +|---|---|---|---|---| +| `deploy ` | S + child passthrough | alchemy child inherits stdio; topology tree rendered by the report hook inside the child; failure → envelope or child-status hints | no | main.ts:258-274; run-alchemy.ts:46-53 (`stdio: 'inherit'`); render-deployment.ts:77-127 | +| `destroy ` | S with one pre-event + child passthrough | `DestroyEvent` 'no-local-deploy-state' → console.warn | no | main.ts:296-313; operations/destroy.ts:18-20 | +| `dev ` | **W** (event session) + **D** (implicitly ensures machine-scoped emulator daemons) | `DevEvent` union rendered line-by-line; session object `stop()`/`closed`; CLI owns SIGINT/SIGTERM | no | operations/dev.ts:14-52; dev/run-dev.ts:43-133; emulators below | +| `log [address]` | **W** (stream) | `AsyncIterable` printed `[service] line`; side-channel `LogEvent` union; AbortSignal ends it | no | operations/log.ts:15-58; log/run-log.ts:26-74 | + +Composer has **no** `--json` anywhere (main.ts:18-110 declares only +`--name/--stage/--production/--fresh/--tail`); the machine surface is the +programmatic operations API (`@prisma/composer/control`, +exports/control.ts:16-32) instead. + +### A3. Platform (`prisma-cli`) — commander v14, ~60 commands + +Framework: Commander v14 (`cli/package.json:47`; cli.ts:3), wrapped so all +Commander output goes to stderr with `exitOverride()` +(shell/runtime.ts:34-55). Twelve top-level `addCommand` calls +(cli.ts:107-118); a descriptor table of 72 command ids +(shell/command-meta.ts:34-700). Global flags: `--json`, `-q/--quiet`, +`-v/--verbose`, `--trace`, `-y/--yes`, `--interactive`/`--no-interactive`, +`--color`/`--no-color` (shell/global-flags.ts:23-45). + +The overwhelming default is **S** through one choke point +(shell/command-runner.ts:70-147) with a uniform `--json` envelope. +"presenter" below means that default. + +| Command | Mode | Output pattern | `--json` | Citation | +|---|---|---|---|---| +| `version` / `--version` | S (local) | presenter | yes | commands/version/index.ts:11-32; cli.ts:123-160 | +| `feedback ` | S | presenter | yes | commands/feedback/index.ts:11-43 | +| `init` | **X** | prompts + presenter | yes (prompts suppressed) | commands/init/index.ts:11-93; controllers/init.ts prompt sites :331, :762, :914, :925, :950, :1020 | +| `agent install/update/status` | S (local, shells out) | presenter | yes | commands/agent/index.ts:39-123 | +| `auth login` | S + **B** (local OAuth callback server + browser + paste race) | login progress direct to stderr; presenter at end | yes | commands/auth/index.ts:52-79; lib/auth/login.ts:39-155 (ephemeral-port server :45-53; `Promise.race` callback-vs-paste :136-146; HTML success page :373-462) | +| `auth logout` / `whoami` | S | presenter | yes | commands/auth/index.ts:81-150 | +| `auth workspace list/use/logout` | S (`use` prompts when arg omitted) | presenter | yes | commands/auth/index.ts:167-250; prompt controllers/auth.ts:585 | +| `project list/show/create/rename` | S | presenter | yes | commands/project/index.ts:64-92, 184-211, 243-291 | +| `project link` | S + prompt | presenter | yes | commands/project/index.ts:213-241; lib/project/interactive-setup.ts:44, 87 | +| `project remove/transfer` | S, typed `--confirm ` | presenter | yes | commands/project/index.ts:94-182 | +| `project env add/update/list/remove` | S | presenter | yes | commands/env.ts:45-240 | +| `git connect` | **P** + **B** (GitHub App install wait) | presenter | yes | commands/git/index.ts:31-59; poll controllers/project.ts:1780-1829; `open()` :2075 | +| `git disconnect` | S | presenter | yes | commands/git/index.ts:61-88 | +| `branch list` (the only branch cmd) | S | presenter | yes | commands/branch/index.ts:27-50 | +| `build logs ` | **W** with `--follow`, else bounded stream | NDJSON records split stdout/stderr by `source`/`level` | yes — per-record events, no envelope | commands/build/index.ts:24-58; controllers/build.ts:34-150 | +| `database list/show/create/usage/restore/remove` | S (`create` and `restore` are **single POSTs** — §A4) | presenter | yes | commands/database/index.ts:92-375; lib/database/provider.ts:309-333, 472-489 | +| `database backup list`, `connection list/create/rotate/remove` | S | presenter | yes | commands/database/index.ts:297-539 | +| `bucket list/create/delete`, `bucket key list/create/delete` | S | presenter; `bucket key create` is the one 3-way presenter (secret → stdout) | yes | commands/bucket/index.ts:64-269 | +| `app build` | S (local build) | presenter | yes | commands/app/index.ts:108-148 | +| `app run` | **W** — hosts framework dev server for the session | passthrough | **no — hard error** | commands/app/index.ts:150-197; rejection controllers/app.ts:273-281 | +| `app deploy` | **S+prog** with SDK-internal **P** | discrete step lines to stderr, off when `--json`/`--quiet` | yes; two result shapes (single vs all) | commands/app/index.ts:199-320; progress controllers/app.ts:801-812; SDK poll lib/app/app-provider.ts:507-527 | +| `app show` / `list-deploys` / `show-deploy` | S | presenter | yes | commands/app/index.ts:326-359, 675-735 | +| `app open` | S + **B** | presenter | yes | commands/app/index.ts:361-394; controllers/app.ts:1268-1272 | +| `app domain add/show/remove/retry` | S | presenter | yes | commands/app/index.ts:420-590 | +| `app domain wait ` | **P** — the one user-facing wait verb | status-transition lines with elapsed `mm:ss`; `--timeout` default 15m | yes — one NDJSON `{type:"status",...}` event per transition | commands/app/index.ts:592-633; loop controllers/app.ts:1506-1563, 2651-2687 | +| `app logs` | **W** (stream) | header + per-record write | yes | commands/app/index.ts:635-666; controllers/app.ts:1566-1633 | +| `app promote/rollback/remove` | **S+prog** with SDK **P** (120s budget) | progress lines | yes | commands/app/index.ts:737-862; lib/app/app-provider.ts:344-360, 471-479 | + +The sibling `compute` package has **zero commands** — it is a runtime +library (`KeepAwakeGuard`, `waitUntil`; `compute/src/index.ts:1-6`, no `bin` +in `compute/package.json:8-13`). + +### A4. The management-API async answer + +**The management API is overwhelmingly synchronous CRUD; asynchronous +poll-until-terminal behavior exists in exactly three places, all deliberate.** + +Async, loop in the CLI: + +1. `app domain wait` — `while (true)` at controllers/app.ts:1506, terminal on + `active`/`failed`, deadline throws `DOMAIN_VERIFICATION_TIMEOUT` + (:1516-1552), abort-aware sleep clamped to remaining budget (:1554-1557), + `--timeout 0` = check once (:1542). Real provisioning state machine: + `pending_dns | verifying | provisioning_tls | verified_routing_blocked | + active | failed` (types/app.ts:175). +2. `git connect` — `waitForInstalledRepository` + (controllers/project.ts:1780-1829) polls SCM installations until the human + finishes the GitHub App install; interval/timeout env-overridable + (:1789-1796); polls only when `canPrompt(context)` (:1708-1717) — + non-interactive callers error immediately instead. + +Async, loop delegated to `@prisma/compute-sdk` but configured by the CLI +(`timeoutSeconds: 120, pollIntervalMs: 2000`): `deploy` +(lib/app/app-provider.ts:507-527), `promote` (:471-479), `destroyApp` +(:353-358), `updateEnv`-then-promote (:563-584). Deploy progress callbacks +expose the state machine (`onStatusChange`, lib/app/deploy-progress.ts:116-118; +steps build → archive → upload → start → running → promoted, :57-89). + +Explicitly **not** async (negative findings): + +- `database create` is a single POST, no wait-for-ready + (lib/database/provider.ts:309-333); same for `database restore` (:472-489) + and branch creation (lib/app/app-provider.ts:869-895). +- No `"provisioning"`/`"ready"` state on database/branch paths; the one "wait + for the database to become ready" string is advice text inside an error + message (lib/database/provider.ts:788), not a loop. +- Most `while (true)` occurrences are cursor pagination + (lib/app/app-provider.ts:909-930; lib/database/provider.ts:244-270; + lib/bucket/provider.ts:92, 170). No `setInterval` in the package. + +### A5. Emulators and daemons (mode D evidence) + +**`@prisma/dev` (PPG-local postgres) is npm-published only** — no source in +any local clone (`wip/repos/` holds composer, create-prisma, ignite, +pdp-control-plane, prisma-cli, project-compute; no dev repo). Surveyed from +the published tarballs (0.13.0 / 0.24.14 / 0.25.1; pinned 0.25.1 in +`pnpm-workspace.yaml`). Its surface: + +- **No `bin`** in any surveyed version — a library, not a CLI. (The line + `pnpm dlx @prisma/dev start` in `examples/react-router-demo/.env.example:2` + cannot work; stale doc.) +- Programmatic API: `startPrismaDevServer(options?): Promise` + (`dist/index.d.ts:79`) returning `{database, shadowDatabase, ppg.url, + http.url, name, close()}` (:46-55). Default ports 51213–51216 (:57-60). +- **Machine-scoped management primitives** (`dist/state-CNKFAMiX.d.ts`): + `ServerState.scan()` (:206 — the "ps"), `getServerStatus` (:239), + `isServerRunning` (:240), `killServer` (:241 — SIGTERM, poll, SIGKILL), + `deleteServer` (:238), status enum `"running" | "starting_up" | + "not_running" | "no_such_server" | "unknown" | "error"` (:236), + `persistenceMode: "stateless" | "stateful"` (:178). +- On-disk state under `env-paths("prisma-dev")`: per-server dir with + `server.json` dump (pid, ports, exports), `.pglite/`, and a + `proper-lockfile` `.lock` whose held/free state plus an HTTP + `GET /health` name-match probe *is* the liveness check (decompiled + `dist/chunk-HFONW2ZS.js`). TCP loopback only, no unix socket. +- `dist/daemon.js` is a script the **consumer** `fork()`s: name in + `process.argv[2]`, reports `{type:"started"|"error"}` over Node IPC + (`dist/daemon.d.ts:14-22`), SIGTERM/SIGINT handlers close and exit. + `@prisma/dev` never detaches itself — machine- vs session-scope is the + caller's choice. +- The 0.25.1 README documents both scopes: the Vite plugin is in-process + ("There is no background daemon", README:184), while `prisma dev` servers + are machine-scoped with an ORM-CLI management surface — "invisible to + `prisma dev ls`, `stop`, and `rm`" (README:126) and "leaves it running + when Vite exits" (README:131). +- Consumers today: prisma-next test utils wrap start/close session-scoped + (`test/utils/src/exports/index.ts:30-58`); the legacy bundled prisma 6 CLI + (`packages/cli/build/index.js:4090`) uses `@prisma/dev` + + `internal/state` for `prisma init`/`prisma dev` (foreground; it never + imports `internal/daemon`); the platform CLI references it **zero** times. + +**Composer's dev emulators are the most rigorous machine-scoped daemon design +in the survey** (`@internal/dev-emulators`): + +- Every daemon is "a detached, `unref()`'d child process that outlives + whatever called `ensureDaemon`" (daemon.ts:2-6); spawn at + daemon.ts:273-291 (`detached: true`, stdio to a log file, `child.unref()`). +- Machine registry at `~/.prisma-composer/emulators/`: `.json` entry + {pid, port, version, logPath} (daemon.ts:26-31, 105-120), state dir, log + file, `proper-lockfile` lock (:327-363). +- Readiness = HTTP health poll (200 ms up to 10 s) that **requires the + health payload's version to match the caller's** so a foreign process on + the port is never adopted (daemon.ts:159-218). +- `ensureDaemon` is an idempotent adopt-or-start-or-replace lifecycle: + classify `healthy | stale-version | dead-or-unhealthy | absent` under the + lock; stale version ⇒ kill and replace; persisted port never moves + (daemon.ts:305-317, 381-478). +- Three daemons — `compute` (supervises `bun bootstrap.js` children with + crash backoff, compute-main.ts:22-28, 422, 772-832), `buckets` + (fs-backed S3, buckets-main.ts:336-415), `postgres` (hosts + `@prisma/dev` `startPrismaDevServer()` in-process, one stateful named + server per Database resource, postgres-main.ts:650-662). Admin surface is + loopback JSON APIs (client.ts:178-199, 285-293, 376-384). +- `prisma-composer dev` **implicitly ensures** them (local-target/ + emulators.ts:42-54) and its `stop()` deliberately leaves them running — + "emulators and data stay up" (operations/dev.ts:47-49; run-dev.ts:83). + `--fresh` deletes per-app records only (local-target/teardown.ts:22-37). +- **There is no user-facing stop/status/ls command**: `stopDaemon` is + documented as "Not called by any v1 command — an operator escape hatch, + exported for tests" (daemon.ts:480-489). The strongest daemon + implementation has the weakest management surface. + +Other daemon-adjacent findings: project-compute has nothing daemon-shaped +(grep for daemon/emulator/detached/unref across cli+sdk: zero hits; its only +local server is the ephemeral OAuth callback, +project-compute/cli/src/lib/auth/login.ts:34-35). The platform CLI's one +detached process is the seconds-long update-check worker that re-execs the +CLI with `detached: true` + `unref()` and an env-var worker branch in +bin.ts (shell/update-check.ts:225-237; bin.ts:7-9). The ORM CLI's telemetry +child is the same fire-and-forget shape (utils/telemetry.ts:160-177). + +## B. Event and progress dialects + +Every distinct mechanism by which command code reports progress or +intermediate state. + +### B1. Composer: per-operation typed event unions + `onEvent` callback + +Each operation input carries `onEvent?: (event: XEvent) => void`, one +discriminated union per operation, on `kind`: + +- `DevEvent` — `ready {endpoints}`, `unwatchable {address}`, + `rebuild-failed {message}`, `watch-error {message}`, + `converge-failed {stackFilePath, reproduceCommand, cwd}`, `stopping`, + `stop-error {message}`, `stopped` (operations/dev.ts:14-31). +- `DestroyEvent` — `no-local-deploy-state {cwd}` (operations/destroy.ts:18-20). +- `LogEvent` — `stream-failed {message}`, `lines-dropped {count}` + (operations/log.ts:20-25). +- `deploy` — no events; resolves to `DeploySuccess {summary?}` + (operations/deploy.ts:25-35). + +Properties: rendering lives entirely in the CLI adapter (a `switch` over +`event.kind`, run-dev.ts:54-90; run-log.ts:40-48); a throwing host `onEvent` +must not kill the session (execute-dev.ts:190-196); lifetime is a session +object (`DevSession.stop()` / `closed`, operations/dev.ts:44-52) or an +`AsyncIterable` ended by the caller's `AbortSignal` (operations/log.ts:36-58). +Failures ride the shared `Result`/`CliStructuredError` shape +(operations/shared.ts:81-126); cli.ts:17-35 maps structured → exit 2, +escape → exit 1 + report hint. + +### B2. Composer: child-process passthrough + cross-process result file + +`deploy`/`destroy` spawn an `alchemy` child with `stdio: 'inherit'` +(run-alchemy.ts:46-53) — the child's own output *is* the progress display. +The structured result crosses back via a JSON file named in +`PRISMA_COMPOSER_DEPLOYMENT_RESULT_FILE` (deployment-summary.ts:18), written +best-effort by a report hook inside the child (:45-53) and re-validated +field-by-field by the parent (:63-101). On failure the error carries +`meta.diagnostics` (`ExecutionDiagnostics {exitCode, stackFilePath, +reproduceCommand, cwd}`, operations/shared.ts:44-75); the CLI prints two +reproduce-hint lines and passes the child's exit status through +(render-error.ts:27-37). + +### B3. ORM: progress spans (`ControlProgressEvent`) + +The exact shape (ORM control-api/types.ts:91-111, verified): + +```ts +export type ControlProgressEvent = + | { readonly action: ControlActionName; readonly kind: 'spanStart'; + readonly spanId: string; readonly parentSpanId?: string; readonly label: string } + | { readonly action: ControlActionName; readonly kind: 'spanEnd'; + readonly spanId: string; readonly outcome: 'ok' | 'skipped' | 'error' }; +``` + +`ControlActionName` = `'dbInit' | 'dbUpdate' | 'dbVerify' | 'migrate' | +'verify' | 'schemaVerify' | 'sign' | 'introspect' | 'emit'` (types.ts:67-76). +Design notes in-source (types.ts:78-90): only two event kinds; all +operation-specific progress is modeled as **nested spans** via `parentSpanId` +(per-migration-operation spans are `operation:` children, +control-api/operations/migration-helpers.ts:22-48); zero overhead when the +callback is absent. + +Renderer: `createProgressAdapter` (utils/progress-adapter.ts:32-74) — no-op +under `--quiet`, `--json`, or non-interactive (:36-38); top-level span → +clack spinner (delay-gated 100 ms, terminal-ui.ts:172-224), nested span → +`ui.step` line; `spanEnd` closes with elapsed-ms suffix, `(skipped)`, or +`(failed)` (:60-72). + +Wired by: `contract emit` (contract-emit.ts:121), `db schema` + +`contract infer` (inspect-live-schema.ts:136), `db sign` (db-sign.ts:205), +`db verify` both paths (db-verify.ts:366, 472), `db init` + `db update` +(migration-command-scaffold.ts:161). Span ids in use: `connect`, `verify`, +`schemaVerify`, `sign`, `introspect`, `resolveSource`, `emit`, `plan`, +`apply` (control-api/client.ts:215-222, 245-273, 298-327, 352-377, 548-567, +618-753; operations/db-run.ts:59-62; run-migration.ts:137-173). + +**Gap:** `migrate` — the longest-running, most destructive command — never +creates a progress adapter. `client.migrate()` threads `onProgress` +(client.ts:499-532) and `runMigration` emits `apply` + nested spans, but +commands/migrate.ts calls `client.migrate({...})` with no `onProgress` +(migrate.ts:808-814, verified); its only in-flight feedback is one `ui.step` +(migrate.ts:777-779). + +### B4. Platform: presenter objects + one success choke point + +End-state presentation, not streaming: controllers return a result; +`writeCommandSuccess` (shell/command-runner.ts:105-147) picks the channel. +Presenter interface (command-runner.ts:25-37): `renderStdout?` +(machine-usable payload → stdout), `renderHuman` (prose → stderr), +`renderJson?` (envelope override). Stream discipline is strict — human to +stderr, data to stdout (shell/output.ts:185-191; command-runner.ts:164-171) +— which is what makes `--quiet` pipe-clean (command-runner.ts:124-127). +Warnings render in human mode too so degraded steps are never silent +(command-runner.ts:130-135). Reusable card patterns (list/show/mutate) pair +a human renderer with a serializer side by side (output/patterns.ts:44-107), +with secret masking built into the UI layer (ui.ts:10; patterns.ts:14, 41). + +### B5. Platform: discrete step lines for long operations — no spinners + +Repo-wide grep for spinner/ora: zero hits outside `@clack/prompts`. Long +operations print append-only step lines: `createDeployProgress` +(lib/app/deploy-progress.ts:36-91; "Building locally...", "Uploading...", +"Deploying..." + status rows via `onStatusChange` :116-118), +`createPromoteProgress` (:97-137), disabled wholesale by +`enabled = !json && !quiet` (controllers/app.ts:801-811). `app domain wait` +prints only on status transitions with elapsed `mm:ss` +(controllers/app.ts:2670-2687). Everything is line-oriented, identical piped +or interactive apart from color and headers. (Contrast: the ORM uses clack +spinners for top-level spans, B3 — the two families made opposite calls.) + +### B6. Platform: NDJSON event streams + +Where the platform CLI streams, it emits single-line JSON events via +`writeJsonEvent` (shell/output.ts:31-36), distinct from the pretty-printed +success envelope: build-log records (controllers/build.ts:88-93), domain-wait +status transitions (`{type:"status", command, timestamp, data}`, +controllers/app.ts:2651-2662), wrapper success/error events +(command-runner.ts:213-235). `build logs` sets `emitJsonSuccessEvent: false` +because the stream carries its own `terminal` record +(commands/build/index.ts:52-55; command-runner.ts:208-222). + +### B7. Direct console writes at the adapter layer + +In all three families the final human rendering is direct +`console.log`/stderr writes concentrated at one adapter layer per command: +Composer's run-dev.ts:54-90 and run-log.ts:40-48; the platform's login +progress (lib/auth/login.ts) before the presenter runs; the ORM's `ui.*` +methods over stderr (terminal-ui.ts:284-301). The structure lives one layer +down (events, results, presenters); the writes are the rendering. + +### B8. Daemon readiness: HTTP health polling, not events + +The emulator daemons report readiness by health endpoint, not by event or +IPC: `awaitHealthy` polls `GET /health` and requires a version match +(daemon.ts:159-218). `@prisma/dev`'s forkable daemon script is the one IPC +user (`process.send({type:"started"|"error"})`, `dist/daemon.d.ts:14-22`). +Transient loopback failures after heavy converges are absorbed by a retry +helper (5 × 500 ms, Composer operations/emulator-retry.ts:9-24). + +## C. Recurring structures — R14 promotion candidates + +Ranked by breadth of occurrence (families out of 3, then by site count). + +1. **Structured error with code / summary / why / fix / where / docsUrl — + 3/3 families, uniform.** ORM: `ok:false, code, severity, summary, why?, + fix?, where?{path,line}, meta?, docsUrl?` + (packages/1-framework/1-core/errors/src/control.ts:9-19; rendered + utils/formatters/errors.ts:32-122). Composer: `CliErrorEnvelope` with + summary/code/why/fix/where rendered `✖ summary (CODE)` + indented lines + (render-error.ts:9-18). Platform: `{code, domain, severity, summary, why, + fix, where, meta, docsUrl}` (shell/output.ts:38-50). Already settled by + ADR 239/245 + Composer ADR-0043/0044 per R6; the survey confirms it is + the single most uniform structure in the corpus. + +2. **Remediation / next-step, in five competing encodings — 3/3 families.** + (a) the error `fix` field everywhere (above); (b) platform envelope-level + `nextSteps` + `nextActions` on **every** success (shell/output.ts:22-29), + including a pre-filled `feedback` recover action on crashes + (shell/output.ts:104-114); (c) ORM structured `hints[]` only on + `migration status` diagnostics (migration-status.ts:316-321, 525-534; + json/schemas.ts:78-103) and a first-class `nextSteps[]` only in `init` + JSON (init/output.ts:38, 117-150); (d) ORM free-text "Next:" prose lines + (formatters/migrations.ts:326-327, 458-464; migration-plan.ts:827, + 908-910; migration-new.ts:295-297); (e) Composer's **reproduce command**: + `reproduceCommand` + `stackFilePath` + `cwd` in both the + `converge-failed` event (operations/dev.ts:22-27) and failure + `meta.diagnostics` (operations/shared.ts:44-75; render-error.ts:27-37). + This is the clearest case of one engine concept currently spelled five + ways. + +3. **Per-item outcome lists — 3/3 families.** ORM: per-space blocks + `{spaceId, kind, operations[], marker?}` (control-api/types.ts:330-352; + renderer formatters/migrations.ts:119-154), `applied[]` + (migrations.ts:276-283), per-migration `status:'applied'|'pending'|null` + (json/schemas.ts:70-76), `failures[{space,code,where,why,fix}]` + (json/schemas.ts:179-187), truncated-to-3 conflict lists with a + "re-run with -v" footer (formatters/errors.ts:54-98). Platform: the + shared list/show card patterns with paired serializers + (output/patterns.ts:57-107). Composer: `DeploymentSummary.nodes[]` each + `{address, entities[{kind, id, url?, details?}]}` + (deployment-summary.ts:22-30) rendered as a topology tree + (render-deployment.ts:77-116). + +4. **Warnings list — 3/3.** ORM `warnings[]` in results + (formatters/migrations.ts:97-107; init/output.ts:66-68; + formatters/verify.ts:104-118); platform envelope `warnings` rendered even + in human mode (shell/output.ts:22-29; command-runner.ts:130-135); + Composer's warning-severity events (`watch-error`, `stop-error`, + `stream-failed`, `lines-dropped`; operations/dev.ts:19-30, + operations/log.ts:20-25). + +5. **Endpoints / URLs — 3/3, three senses.** Service endpoints: Composer + `ServiceEndpoint {address, url}` (operations/shared.ts:19-22) in `ready` + events and `log` attachments; entity `url` in deploy summaries + (render-deployment.ts:104-106); platform `liveUrl` (`app open`, + controllers/app.ts:1268-1272) and emulator base URLs + (`http://127.0.0.1:`, dev-emulators client.ts:58). Docs URLs: ORM + headers carry `https://pris.ly/...` "Read more" links (styled.ts:55-66; + e.g. db-init.ts:129) and envelope `docsUrl` under `-v` (errors.ts:99-101); + platform envelope `docsUrl` (output.ts:38-50). Masked connection URLs: + ORM `maskConnectionUrl`/`sanitizeErrorMessage` + (command-helpers.ts:308-359); platform `URL_CREDENTIALS_PATTERN` + + `maskValue` (ui.ts:10; patterns.ts:14, 41). + +6. **Counts + one-line summary — 3/3.** ORM "Planned/Applied N operation(s) + across M contract space(s)" (migrations.ts:174-182, 433-446), "N + migration(s) applied" (migration-log.ts:142), `operationCount` fields + (json/schemas.ts:8, 130); platform `summary` strings throughout the + presenters and `renderSummaryLine` glyphs ✔/✘/⚠/ℹ (ui.ts:129-143); + Composer `lines-dropped {count}` (operations/log.ts:24-25). + +7. **File paths / artifacts written — 3/3.** ORM `files{json,dts}` + `outDir` + (emit.ts:12-19), `filesWritten[]`/`filesDeleted[]` (init/output.ts:23-31), + `dir`/`baselineDir` (migration-new.ts:238; migration-plan.ts:884-897), + relativized to cwd nearly everywhere (emit.ts:33-34; + command-helpers.ts:138-140). Composer `stackFilePath` + (operations/shared.ts:48) and the generated-stack reproduce hint. + Platform: log paths in the daemon registry (dev-emulators + daemon.ts:26-31) — thinner here. + +8. **Child-process output — 3/3, three strategies.** Passthrough: Composer + `stdio: 'inherit'` (run-alchemy.ts:46-53). Captured + redacted + carried + in the error: ORM init reads child stderr, strips credentials, surfaces + an excerpt or `meta.stderrLines` (init/init.ts:809, 834-845, 925-945; + skill-install.ts:207-241). As typed events: platform build-log NDJSON + records with `source`/`level` routing to stdout/stderr + (controllers/build.ts:34-150). An engine vocabulary needs a + child-output-line concept that all three can target. + +9. **Durations — 2/3 consistently.** ORM `timings: {total}` ms rendered only + under `-v` (emit.ts:17-19; migrations.ts:92-94, 261-263, 329-332, + 477-479; verify.ts:71-73) plus span elapsed-ms suffixes + (progress-adapter.ts:62-69); platform `--verbose` timing diagnostics + appended best-effort (command-runner.ts:136-139, 173-191) and domain-wait + elapsed `mm:ss` (controllers/app.ts:2682-2687). Composer surfaces no + durations. + +10. **Status/state-machine enums — 2/3 (+ the daemon layer).** Platform + domain statuses (types/app.ts:175) and deploy display statuses + (presenters/app.ts:790-802); ORM per-migration + `'applied'|'pending'|null`; `@prisma/dev` `ServerStatusV1.status` + six-value enum (state d.ts:236). Any engine "wait" concept needs a + from→to status-transition event (the platform already emits exactly + that, controllers/app.ts:2651-2662). + +11. **Typed confirmation for destructive operations — 2/3.** Platform + `--confirm ` (commands/project/index.ts:101-106, 136-153; + database/bucket variants); ORM `db update`'s prompt-then-re-execute with + `yes:true` (db-update.ts:320-343); Composer's flag-encoded target + (`destroy` requires `--stage` or `--production`, main.ts:276-294). + +## D. `--json` reality + +- **ORM: near-universal, per-command shapes, plus surprises.** Every main-CLI + command has `--json` via the shared flag set (command-helpers.ts:368-388); + shapes are per-command result objects (the tables in §A1), several + arktype/schema-validated (commands/json/schemas.ts; init/output.ts:19-52). + **JSON auto-selects when stdout is not a TTY even without `--json`** + (utils/global-flags.ts:67-69, verified; `--format pretty` is the escape + hatch, terminal-ui.ts:334). No streaming JSON exists — spans never reach + `--json` consumers (the progress adapter no-ops under JSON, + progress-adapter.ts:36-38). Indentation is inconsistent: most commands + pretty-print, `ref`/`format`/`telemetry` emit compact single lines + (ref.ts:203, 230, 252; format.ts:67; telemetry/index.ts:33, 56, 77). + `migration graph --dot --json` silently emits DOT + (migration-graph.ts:249-258). The migration-file CLI has no `--json`. +- **Composer: none.** No JSON flag exists (main.ts:18-110). The machine + surface is the typed programmatic API (`@prisma/composer/control`) — + events as callbacks, results as values — rather than serialized output. +- **Platform: near-total, one envelope, streaming where needed.** Success: + `{ok: true, command, result, warnings, nextSteps, nextActions}` + pretty-printed (shell/output.ts:9-29); error: `{ok: false, command, error, + warnings, nextSteps, nextActions}` (:38-50, 164-183); crashes still emit + the envelope (`UNEXPECTED_ERROR` + recover action, :120-162; cli.ts:69-71). + Streaming commands switch to single-line NDJSON events (§B6). Deviations: + `app run` rejects `--json` outright (controllers/app.ts:273-281); + `app deploy` has two result shapes (commands/app/index.ts:311-314). + +Cross-family delta worth naming: the ORM buries remediation in per-command +shapes and prose while the platform reserved envelope-level `nextSteps` / +`nextActions` on every command; and only the platform has a +crash-still-emits-JSON guarantee. + +## E. Framework support for execution modes (stricli, clipanion) + +Both are parse-and-dispatch frameworks; **neither models execution modes at +all** — no concept of long-running commands, streaming, progress, daemons, +or watch modes. + +- **stricli** (bloomberg.github.io/stricli): documented features are routing, + typed argument parsing, isolated context, lazy command loading, + autocomplete. Its "Out of Scope" page explicitly declares out of scope: + cross-argument validation, local system access (use context injection), + **logging** ("no first-party logging solution"), **enhanced formatting** + (recommends chalk etc.), and **prompting/stdin** ("interactive user input + beyond command-line arguments is not supported"; recommends + enquirer/clack). Execution model: the command function runs to completion; + `run()` writes help/errors to the injected `context.process.stdout/stderr` + and sets `context.process.exitCode`. Nothing constrains what the function + does while running — a long-lived session is just a promise that hasn't + resolved. (Full internals evaluation: + wip/designs/engine/stricli-vs-clipanion.md.) +- **clipanion** (mael.dev/clipanion): documents command paths, option types, + execution contexts (`stdin`/`stdout`/`stderr`/`env`/`colorDepth`), + validation, error handling, help. Its one stream-relevant claim is + compositional: streams live in the context "so commands can easily + intercept the output of other commands". No modeling of long-running + commands, progress, or daemons. Composer's usage confirms the division of + labor: clipanion parses (main.ts:112-160); session lifetime, signals, and + events are hand-built above it (run-dev.ts:110-132). Same in the ORM + migration-file CLI (migration-cli.ts). + +Consequence: execution modes are the engine's to define. Nothing in either +framework will be contradicted by an engine-level mode taxonomy, and nothing +can be reused for it — the frameworks end where the handler begins. + +## F. The mode taxonomy the evidence supports + +The four starting modes hold, with refinements: "event-streaming +asynchronous" splits into in-flight progress (ends on its own) versus +poll-until-terminal (a remote state machine with timeout semantics), and two +modes must be added (wizard, stdio server). Browser hand-off and detached +worker spawns are capabilities that ride on other modes, not modes. + +1. **Synchronous request/response** — the dominant mode everywhere: + ~55 platform commands, ~14 ORM commands, 2 Composer commands (§A tables). + The engine's baseline: result value in, one rendering out, uniform + envelope. + +2. **Synchronous with in-flight progress** — same lifetime as (1), plus + intermediate reporting that both human and `--json` surfaces may consume. + Three dialects to unify: ORM nested spans with outcome + elapsed (B3, 7 + wiring sites), platform discrete step lines + `onStatusChange` callbacks + (B5), Composer's single pre-event on `destroy` (B1). Today none of the + ORM's span data reaches `--json`; the platform's step lines are + human-only. R14's vocabulary should make progress events representable in + both channels, which no family does today. + +3. **Poll-until-terminal (wait)** — distinct from (2) because it carries + timeout/deadline semantics, an explicit remote status enum, and + transition events: `app domain wait` (with `--timeout`, `--timeout 0` = + probe once, NDJSON transition events), `git connect`'s + interactivity-conditional poll, and the SDK-delegated deploy/promote/ + destroy polls (§A4). Small today (one dedicated verb) but structurally + different enough — and precedented enough — to be its own engine concept: + the platform team invented a `wait` verb rather than bolting waiting onto + `add`. + +4. **Long-lived session / watch** — runs until signal or abort, re-emits + over its lifetime: Composer `dev` (session object with `stop()`/`closed` + + typed events) and `log` (AsyncIterable + AbortSignal), platform + `app run` (dev-server passthrough), `app logs`, `build logs --follow` + (§A2, §A3). Two lifetime idioms exist — session object versus + abort-signal-terminated iterable — and the engine must pick or support + both; Composer deliberately keeps signal ownership in the host + (operations/dev.ts:41-43; run-dev.ts:124-131), which matches R4/R5. + +5. **Daemon-coupled (machine-scoped)** — the controlled process outlives the + CLI invocation: Composer's three emulator daemons (implicitly ensured by + `dev`, never stoppable from the CLI) and `@prisma/dev` stateful servers + (library primitives for scan/status/kill; the legacy `prisma dev` + `ls`/`stop`/`rm` surface per its README) (§A5). Consistent mechanics + across both implementations — registry file + lockfile liveness + HTTP + health with identity check + SIGTERM-then-SIGKILL — which is effectively + a specification for the engine's daemon concept. The evidence also shows + the gap the unified CLI must not reproduce: composer daemons have **no** + user-facing ps/stop/status. + +6. **Interactive wizard** — prompts drive the flow: ORM `init` (clack, + spinners, child installs), platform `init` (1065-line controller, six + prompt sites), plus scattered single-prompt commands (`project link`, + `auth workspace use`, `db update`'s confirm-and-re-run) (§A1, §A3). Both + families gate every prompt on an interactivity check that `--json`, + CI, and non-TTY force off (platform `canPrompt`, shell/runtime.ts:105-119; + ORM progress/prompt gating, progress-adapter.ts:36-38, + global-flags interactivity flags) — an engine-level capability check, not + per-command logic. + +7. **Stdio protocol server** — ORM `lsp`: lazy-imports the language server, + hands the process to `connection.listen()`, no exit path of its own + (§A1). One instance, but structurally unlike everything else: the CLI's + own rendering machinery must get out of the way entirely. + +Cross-cutting capabilities (not modes): **browser hand-off / local callback +server** (auth login's ephemeral-port server + callback-vs-paste race, +git connect, app open — always layered on S or P); **child-process +passthrough** (Composer deploy/destroy); **detached fire-and-forget self +processes** (platform update-check worker, ORM telemetry child) — invisible +to users, but the engine should know the pattern exists because both +families independently built it. + +Straddlers, noted rather than force-fit: `composer dev` is (4)+(5) — a +session that implicitly manages daemons; `app deploy` is (2) with (3) inside +the SDK; `db update` is (2) wrapped in a (6)-style confirm loop that +re-executes the command; `migration graph` is (1) with three output +renderers (tree/DOT/JSON). diff --git a/.drive/projects/prisma-cli-v8/assets/engine/stricli-vs-clipanion.md b/.drive/projects/prisma-cli-v8/assets/engine/stricli-vs-clipanion.md new file mode 100644 index 00000000..41fb2d85 --- /dev/null +++ b/.drive/projects/prisma-cli-v8/assets/engine/stricli-vs-clipanion.md @@ -0,0 +1,238 @@ +# Stricli vs clipanion as the CLI engine's internal framework + +Snapshot: 2026-08-09. Research-only evaluation of `@stricli/core` 1.3.0 +(Bloomberg) against the 10-criterion rubric in +`docs/architecture docs/research/commander-friction-points.md` and the engine +requirements R1–R13 in +`wip/repos/prisma-cli/docs/architecture/cli-engine-requirements.md`. +Compared point-by-point with clipanion 3.x (the currently chosen internals). + +Evidence base: the published package (`npm pack @stricli/core@1.3.0`, type +declarations `dist/index.d.ts` and the built `dist/index.js` read in full), +the docs site (bloomberg.github.io/stricli), the GitHub repo via API, npm +registry data, and the composer repo's living clipanion usage +(`packages/0-framework/3-tooling/cli/src/cli.ts`). Line references below are +into the unpacked `dist/index.d.ts` of 1.3.0. + +## Verdict + +**Strong contender — the comparative prototype spike is justified.** + +Stricli scores **10/10** on the rubric (clipanion: 7/10). It passes every +criterion clipanion passes, and it passes criterion 10 (maintenance), which is +clipanion's only hard failure — and clipanion's position there has worsened +since the friction doc was written (last push 2024-09-06, now ~23 months ago; +4.0 still in RC after 3 years; stable 3.2.1 dates from June 2023). Stricli is +also a better structural fit for R9 and R12 than clipanion: its command tree +is literally a static route map built by the mounting side, with lazy handler +loading as a first-class, documented feature. The weaknesses section below +lists real warts (negative internal exit codes, no parse-only API, limited +help-layout customization, single primary maintainer), but none is +disqualifying for our wrap-everything design, and several disappear entirely +because we render output ourselves (R5). + +## Rubric score table + +Criteria abbreviated; full text in commander-friction-points.md ("Concrete +evaluation criteria for the replacement"). + +| # | Criterion | Stricli | Clipanion | Winner | +|---|-----------|---------|-----------|--------| +| 1 | Never calls `process.exit` from parse/dispatch; no message string-matching needed | **Pass.** Zero occurrences of `process.exit(` in `dist/index.js`/`index.cjs` (verified by grep). `run()` sets `context.process.exitCode ??= exitCode` on the *injected* process object (d.ts:1757; index.js `run` impl). Failure classes are typed constants in the exported `ExitCode` object (d.ts:1547–1580). | **Pass** (friction doc: `cli.run` returns exit code as a promise; caller sets `process.exitCode`). | Tie | +| 2 | Injected `{stdout, stderr, env}` per invocation, no globals | **Pass.** `run(app, inputs, context)` requires a context whose `process` is a `StricliProcess` — `{stdout, stderr, env?, exitCode?}` with a minimal structural `Writable` (`write`, optional `getColorDepth`) (d.ts:4–46). Verified in the built JS: every `process` reference is a function parameter fed from the context; no global reads. Docs ("Isolated Context"): "this indirection allows for injecting alternate implementations without hot-swapping globals." | **Pass** (friction doc; composer's `run(argv)` passes streams via clipanion context). | Tie | +| 3 | Help, errors, command output all go to the injected streams | **Pass.** All framework output (help text, parse errors, did-you-mean, integration errors) is written via `context.process.stdout/stderr` (verified in `dist/index.js`; e.g. the integration-error path writes `context.process.stderr.write(...)`). | **Pass**, with the caveat of open issue [arcanis/clipanion#176] (errors to stdout instead of stderr in some paths), which the migration-CLI swap works around with parse-only `cli.process`. | Stricli (no known stream-routing bug) | +| 4 | `parse(argv, ctx)` signals success/failure structurally | **Pass with note.** `run()` returns `Promise` and communicates the exit code by assigning `context.process.exitCode` — we read it off the injected object we own. Codes are the typed `ExitCode` constants: `0` success, `1` command threw, negative codes for framework-level failures (`-4` InvalidArgument, `-5` UnknownCommand, `-2` CommandLoadError, `-3` ContextLoadError, `-1` InternalError, `-10` IntegrationError) (d.ts:1547–1580). `determineExitCode?: (exc: unknown) => number` in `ApplicationConfiguration` (d.ts:593) maps a thrown value to a code. No string matching anywhere. | **Pass** (`cli.run` resolves to the exit code directly — slightly cleaner shape). | Tie (clipanion's return shape is nicer; stricli's codes are more finely discriminated) | +| 5 | Help generated from the same declarations the parser uses | **Pass.** One declaration per parameter: `flags: { verbose: { kind: "boolean", brief: "..." } }` etc. is both the parse spec and the help source. `Command` and `RouteMap` expose `brief`, `fullDescription`, `formatUsageLine`, `formatHelp` from those same objects (d.ts:1118–1155), and `generateHelpTextForAllCommands` walks the tree (d.ts:1356). Docs site's "No Magic" principle is exactly this: no parallel DSL. | **Pass** (friction doc: `usage = {…}` lives on the command class next to the typed option declarations). | Tie | +| 6 | Parse-time enum/constraint validation with structured errors | **Pass.** `kind: "enum"` flags with `values: readonly T[]` are validated during scanning; failure throws `EnumValidationError` carrying typed fields `externalFlagName`, `input`, `values`, `corrections` (d.ts:1443–1457). All nine scanner errors are exported subclasses of `ArgumentScannerError` with typed payloads (`FlagNotFoundError.corrections`, `UnsatisfiedPositionalError.limit`, …) plus a `formatMessageForArgumentScannerError` dispatcher (d.ts:1367–1386). This routes directly into our error envelope with no message parsing. | **Pass** (typanion validators produce structured errors at parse time). | Tie (stricli's error taxonomy is richer and flatter to consume) | +| 7 | Hook for unknown command/flag with "did you mean" | **Pass.** Built in: route scanning computes corrections via Damerau-Levenshtein with git's empirically-derived weights, configurable (`ScannerConfiguration.distanceOptions`, d.ts:428–446), and hands `{input, corrections}` to the replaceable `noCommandRegisteredForInput` formatter (d.ts:260–264). `FlagNotFoundError` also carries `corrections`. We inject our own wording; no internal error interception. | **Pass** (friction doc). | Stricli (suggestions computed for us, formatter injectable) | +| 8 | Command carries user-defined metadata without shims | **Pass with note.** `docs: { brief, fullDescription?, customUsage? }` on every command (d.ts:1612–1625) and `{ brief, fullDescription?, hideRoute? }` on route maps. No arbitrary free-form metadata bag (e.g. docs URL) — but under R1/R3 the engine builds stricli objects *from our own definition type*, which is where arbitrary metadata lives; the framework never needs to carry it. No WeakMap shims required either way. | **Pass** (class fields hold anything). | Tie in practice | +| 9 | Runtime-agnostic parser (no `node:*` imports) | **Pass, strongest possible form.** Zero runtime dependencies (`package.json`: no `dependencies` key; tagline "no dependencies"). Zero `node:` imports in the ESM build (the single grep hit in the CJS build is a tsup comment). All environment access goes through the injected context. Ships ESM + CJS + `.d.ts`; single file ~86 KB unminified. | **Pass with caveat**: clipanion needs a `platform/` shim layer and has open issue [arcanis/clipanion#178] (invalid `lib/platform/node.mjs` require). | **Stricli** | +| 10 | Healthy maintenance trajectory | **Pass.** 1.0.0 published 2024-10-01; 16 releases since, latest 1.3.0 on 2026-07-16; repo pushed 2026-08-07 (two days before this snapshot). 19 open issues, actively triaged (recent bugs like white-on-white help #140 and green-bleed help #170 fixed and released). ~699k weekly npm downloads. Bloomberg OSS project, Apache-2.0. Adopters beyond Bloomberg: **Sentry's `sentry-cli`** (getsentry/cli), MystenLabs ts-sdks, Matt Pocock's `evalite`, LaunchDarkly's MCP server, Inkeep agents, Hookdeck Outpost, xs-dev, and every Speakeasy-generated MCP CLI (which is where much of the download volume comes from). Bus factor caveat below. | **Fail** (friction doc, and worse now: last push to arcanis/clipanion 2024-09-06 — ~23 months; 4.0.0-rc.4 was the last publish, 4.0 in RC since 2023-07; stable 3.2.1 from 2023-06; 42 open issues accumulating). | **Stricli, decisively** | + +**Totals: stricli 10/10, clipanion 7/10** (clipanion per the friction doc: +passes 1–9, fails 10). The friction doc's threshold: ≥8/10 is "a clear win +over Commander". Both clear it; only stricli clears criterion 10, which the +friction doc itself flags as the criterion that becomes more important "for a +larger surface" — exactly our case. + +## Fit against R1–R13 + +- **R1 (one directly executable language).** Good fit. `buildCommand` / + `buildRouteMap` / `buildApplication` are plain functions taking plain object + literals — our engine vocabulary can be a thin typed layer whose output *is* + the runnable stricli tree, no interpreter. Note the engine still owns its own + definition type per R3, so there is one translation (ours → stricli's), same + as with clipanion. +- **R2 (handlers end in typed operation calls).** Neutral/good. A stricli + command function is `(this: CONTEXT, flags: FLAGS, ...args: ARGS) => void | + Error | Promise` (d.ts:1103) — flags and positionals arrive + fully typed; the `FLAGS` type parameter is checked against the parameter + declarations in *both* directions (`FlagParametersForType` maps every key + of the handler's flags type to a required declaration, d.ts:910). Declaring + a flag the type doesn't have, or omitting one it does, is a compile error. + This is stronger inference than clipanion's per-field `Option.String(...)` + class properties. +- **R3 (engine package is the whole contract).** Good fit. Everything is + interface-typed and generically parameterized on `CONTEXT`; the objects + `buildCommand` returns are opaque values we never re-export. Nothing forces + stricli types into our public surface. +- **R4 (context, never the environment).** Direct match. Stricli's whole + design is a context object bound as `this` in command functions, extended + with whatever we add (docs: "Isolated Context"). `StricliDynamicCommandContext` + supports a `forCommand(info) => CONTEXT | Promise` builder + (d.ts:78–88) — per-invocation, possibly async construction of the + handler-facing context (config section, credentials, output surface) after + routing but before execution. This maps one-to-one onto our handler-context + design. Parsers themselves also get the context (`InputParser`, + d.ts:987), which clipanion's typanion validators do not. +- **R5 (engine renders everything).** Good fit with one design decision to + make in the spike. Two viable postures: + 1. *Own rendering wholesale*: skip the `help`/`version` integrations + (since 1.2 they are opt-in integrations, d.ts:1728/1755) and render help + ourselves by walking the public tree — `RouteMap.getAllEntries()`, + `Command.parameters` (flags with `brief`/`default`/`values`/`hidden`, + positionals with placeholders), `brief`/`fullDescription` (d.ts:1118–1141). + All parse metadata is on public readonly properties — no + private-field spelunking like Commander's `defaultValue` (friction §8). + 2. *Own the text, let stricli lay it out*: replace the entire + `ApplicationText` object (`localization: { text }`) — every error string + and help header is a formatter we supply, receiving the *typed* error + objects (d.ts:192–319). + Parse/route errors are formatted by our functions and written to our + streams either way. The one thing stricli insists on doing is the physical + `stderr.write` of the formatted error inside `run()` — acceptable because + both the string and the stream are ours; if we ever need the error as a + value instead, see the "no parse-only API" weakness below. +- **R6 (structured errors, exit-code mapping).** Good fit. Scanner errors are + typed classes → our envelope; `ExitCode` discriminates usage errors (−4/−5 → + our 2) from bugs (−1/−2/−3 → our 1); `determineExitCode` maps handler + throws. We read `context.process.exitCode` off our own injected object and + translate before touching the real process (see weaknesses: never hand + stricli the real `process`). +- **R7 (product-repo e2e tests, instance-based).** Direct match — this is the + hard requirement, and stricli meets it structurally. `Application` is a + value; `run(app, argv, context)` is a pure-ish call over injected streams; + no module-level state anywhere in the bundle (verified). A product mounts + its commands in a throwaway route map and runs argv-in/bytes-out in its own + repo with a `{ process: fakeStreams }` context. The docs advertise exactly + this testing story. +- **R8 (shell integration proof).** Neutral — same machinery, no obstacle. +- **R9 (static tree, lazy guts).** Direct match, better than clipanion. + Route maps are built eagerly at startup from cheap declarations; each + command takes either `func` (inline) or `loader: () => Promise` (d.ts:1107–1113, 1637–1648) — the dynamic import of the + heavy handler module happens only when that command is actually executed, + *after* routing and before argument parsing. Help renders from the static + declarations without ever invoking the loader. A loader failure is its own + exit class (`CommandLoadError`, −2) with its own formatter + (`exceptionWhileLoadingCommandFunction`), so "Composer's dependency tree + crashed at import" becomes a classified, renderable failure rather than a + startup crash. Clipanion has no built-in lazy-module story (its + registration is runtime `cli.register` per class; stricli's own + "Alternatives" page criticizes it for runtime command loading). +- **R10 (config).** Out of framework scope; nothing in stricli touches config + files. No conflict. +- **R11 (pinning).** No conflict. Zero transitive dependencies makes exact + pinning trivial and audit surface minimal. +- **R12 (shell defines the tree).** Direct match. A `Command` contains no + path; paths exist only as keys in `buildRouteMap({ routes: { migrate: + cmd } })` (d.ts:1673–1703), composed by whoever mounts. The same command + value can be mounted at any path, at several paths, or under a test-only + root in a product repo. Route-level `aliases` and `hideRoute` are also + mounting-side concerns. This is structurally the R12 split. +- **R13 (no package manager).** Pass. Nothing self-installs; zero runtime + deps; nothing interferes with optional-peer-dependency detection (a handler + doing its own `import()` probe is untouched by the framework). The optional + version-check feature (`getLatestVersion`) is opt-in and purely advisory; + we simply don't enable it. + +## Honest weaknesses of stricli + +1. **Negative internal exit codes.** Framework failures set + `context.process.exitCode` to −1…−10. POSIX exit statuses are 0–255; if + the *real* `process` were passed as context, `-5` becomes exit 251. For us + this is a mapping obligation, not a bug — the engine must always inject + its own process facade and translate — but it is a trap for anyone who + follows the docs' quickstart (`run(app, argv, { process })`) and expects + sane shell-visible codes for usage errors. +2. **No parse-only public API.** `run()` is the only execution entry point + (plus `proposeCompletions`). There is no exported "parse this argv against + this command and give me the flags/route result without executing" + function — `RouteScanResult` is a type you only meet inside integration + hooks. Commander-style metadata recovery isn't needed (declarations are + public), but if the spike finds we want parse-errors-as-values rather than + parse-errors-as-formatted-strings, the workaround is a custom + `ApplicationText` whose formatters capture the typed error object onto our + context before returning a string — functional, slightly inelegant. +3. **Help layout is stricli's unless we render ourselves.** Section order, + indentation, and column layout of `formatHelp` are not configurable beyond + headers/keywords and a few booleans; issue #87 ("Customize usage/help + layout", open since 2025-05) confirms. Under R5 we intend to render help + ourselves anyway, which sidesteps this — but it means the spike must + verify that walking `Command.parameters` gives us everything our formatter + needs (it appears to: briefs, placeholders, defaults, enum values, + optional/variadic/hidden are all in the public d.ts). +4. **No user-defined global flags across commands.** A flag like `--json` + must either be declared on every command (our engine can inject it into + every definition it builds — mechanical) or expressed as an + application-level integration flag, which takes over the run like + `--help` does. Open issues #146 ("Support application-level global + flags") and #127 ("Root flags?") confirm this is a real gap upstream. +5. **Single-character flag aliases only.** `Aliases` is typed as one ASCII + letter → flag name (d.ts:998–1001). No long-form alias (`--data-proxy` + aliasing `--accelerate`); route-level aliases are unrestricted, flag-level + are not. If we need long flag aliases for deprecation migrations, we + declare a second hidden flag and merge in the handler — our engine can + abstract that. +6. **CamelCase-first flag naming.** Flag names are TypeScript object keys, so + multi-word flags are natively `camelCase`; kebab input/output is a scanner + and display mode (`allow-kebab-for-camel` / `convert-camel-to-kebab`, + d.ts:399, 453). Fine, but it is a convention the engine must set once + (Prisma's user-facing flags are kebab-case) and the "original vs + converted" duality shows up in APIs like `getOtherAliasesForInput` + returning per-case-style records. +7. **Bus factor.** Contributor stats: molisani (Michael Molisani, Bloomberg) + 125 commits; next human contributor 30; everyone else ≤4. This is a + one-primary-maintainer project with corporate backing — better than + clipanion's one-maintainer-who-moved-on, and Bloomberg has kept it staffed + for 22 months of releases, but it is not a multi-maintainer community. + Mitigation is the same wrap-per-R3 posture that lets us swap internals. +8. **API still settling.** 1.2 deprecated the whole `DocumentationConfiguration` + / `versionInfo` surface in favor of integrations (deprecation notices + throughout the d.ts). Handled gracefully (deprecate, don't break), and it + moved in a direction we like (help/version became optional), but expect + some churn inside 1.x. +9. **Mandatory `brief` on everything** (issue #92). For us a non-issue — the + engine requires descriptions anyway — noted for completeness. +10. **Docs gloss over `forCommand`.** The dynamic per-command context builder + (`StricliDynamicCommandContext.forCommand`) is in the types but + undocumented on the site (issue #126). We would rely on it for R4; the + spike should exercise it explicitly. + +## Clipanion-specific notes not in the friction doc + +- Maintenance has degraded further since the 2026-04-30 snapshot: the repo's + last push remains 2024-09-06 (now ~23 months), last npm publish is + 4.0.0-rc.4 (2024-09-06), and the stable 3.x line's last release is 3.2.1 + (2023-06-05). Open issues: 42. The friction doc's "re-evaluate criterion 10 + before adopting clipanion at larger scope" instruction, applied today, + reads as a failure for this scope. +- Composer's living usage (`wip/repos/composer/packages/0-framework/3-tooling/ + cli/src/cli.ts`) confirms the pleasant parts: `run(argv)` returns the exit + code, `UsageError` is a typed catchable, envelope mapping is a small + try/catch. Nothing in that file argues against clipanion ergonomically — + the case against it is purely criterion 10 plus the weaker R9 story. + +## Bottom line for the spike decision + +Stricli is not merely "clipanion with a pulse". It is a closer structural +match to this requirements document than clipanion on the three requirements +that shaped the engine design (R7 instance/context model, R9 static tree with +lazy loaders, R12 mounting-side route maps), it has the best +runtime-agnosticism profile of any candidate examined (zero deps, zero +`node:*`, all environment access injected), and its maintenance trajectory is +the strongest signal: 16 releases in 22 months, active triage, corporate +backing, and credible external adopters (Sentry CLI, Speakeasy, MystenLabs). +Run the comparative spike; the specific things the spike must prove are the +R5 own-rendering path (weakness 3), the `forCommand` context handoff +(weakness 10), and the exit-code translation layer (weakness 1). diff --git a/.drive/projects/prisma-cli-v8/design-notes.md b/.drive/projects/prisma-cli-v8/design-notes.md new file mode 100644 index 00000000..7158421b --- /dev/null +++ b/.drive/projects/prisma-cli-v8/design-notes.md @@ -0,0 +1,65 @@ +# cli-host — design notes + +Orchestrator-authored index of the settled design inputs this project +builds on. The artifacts themselves live where they were produced; this +file is the map. + +## Settled (do not re-litigate without the operator) + +- **Engine interface, v8** — `assets/engine/engine-interface-draft.ts`. + Settled through facilitated line-by-line design with Will plus five + adversarial review rounds (architect + principal engineer, both + closed clean). Every novel typing claim compile-verified; the claims + live on as the permanent type-test suite in `@prisma/cli-engine` + (review artifacts and superseded draft versions are not committed). +- **Requirements R1–R14** — prisma-cli PR #128 + (`docs/architecture/cli-engine-requirements.md`), unmerged. +- **Packaging** — ONE library package `@prisma/cli-engine` with a + `./protocol` subpath for types-only consumers; `@stricli/core` as an + ordinary exact-pinned dependency (bundling rejected); committed + versions, bumped in PRs. +- **Model** — commands settle like promises: COMPLETED (presented + outcome: data + diagnostics + documented exitCodes) vs ERRORED + (structured error, engine-rendered). `Diagnostic` ≡ the error envelope + shape; findings are data, never thrown. +- **Framework decision record** — + `assets/engine/stricli-vs-clipanion.md` (stricli 10/10 vs + clipanion 7/10 on the repo's own rubric). +- **Evidence base** — `assets/engine/output-modes-survey.md` + (~85 commands, three families, mode taxonomy, recurring structures). +- **Auth** — an auth library (token storage, refresh, login guts) lives + in the prisma-cli repo, DISTINCT from Prisma Cloud; Cloud extraction + later leaves auth behind. `{ token }` is the engine-visible shape; + credentials resolve per-call so refresh works under long sessions. +- **Conformance** — a small 3-check tool only: import purity, validator + no-throw on garbage, published-tarball verification. +- **ADR 239 amendment (prisma/prisma) is an implementation + prerequisite** — completed-but-unsuccessful results carry dotted codes + as diagnostics inside completed envelopes; includes the + severity-'info' evidence check (trim both scales together if unused). + +## Parked (excluded from this project by ruling) + +- **Daemon library** — `assets/engine/daemon-library-notes.md`: + runtime dependency of product control clients, orthogonal to the + engine; zero engine surface needed. + +## Hand-off briefs — handed off, PAUSED by the operator + +- `assets/briefs/1b-leftovers-prisma-prisma.md` and + `assets/briefs/1c-leftovers-composer.md` are already with other agents, + but the operator paused that work until the engine lands: their config + deliverables (diagnostics-not-throw loaders, marker, validators) will + be rewritten against the engine's config API (v8 §3: + defineConfigSection tokens, validator-owned absence, Diagnostic + findings, ProductManifest). Sequencing consequence: the engine's + protocol + config-section API is upstream of resuming 1b/1c; when + resumed, the briefs need revision first. + +## Prior art / superseded + +- The consolidate-clis project (closed 2026-08-07): grammar doc, spec, + plan recoverable from prisma/prisma PR #29917's head ref + (`refs/pull/29917/head`). Its Phase 2–3 content (host build, ports, + ecosystem cutover) informs this project's plan but was never + re-ratified — treat as input, not contract. diff --git a/.drive/projects/prisma-cli-v8/plan.md b/.drive/projects/prisma-cli-v8/plan.md new file mode 100644 index 00000000..819979db --- /dev/null +++ b/.drive/projects/prisma-cli-v8/plan.md @@ -0,0 +1,117 @@ +# prisma-cli-v8 — project plan + +Consumer ordering (operator, 2026-08-09): **platform → Composer → ORM**. +The engine meets its first consumer in its own repo (flex is a same-PR +edit; the variable is isolated), its second across a repo boundary with +real config and sessions, and its hardest consumer last, twice-hardened. +Slices are one-PR units; each port slice ships its parity-divergence +list for operator review (spec FR6). + +Branch mechanics: slices land as PRs into the prisma-cli repo's +`cli-engine-requirements` branch lineage (PR #128 is the living decision +record) or their own repos; #128 merges when the operator says so. + +## Slices + +### S1 — Engine package + one vertical command + +Repo: prisma-cli. Implement `@prisma/cli-engine` per the v8 interface +(execution protocol, events, return-site presentation, config-section +tokens, prompts incl. consent + defaults, three command kinds, +envelopes/stream, mounting, `./protocol` subpath, the test harness; +`@stricli/core` exact-pinned). Prove it end to end with ONE ported +platform command (`project list` or `auth whoami`) mounted in a minimal +shell bin: parse → context → handler → presentation → envelope → exit +code, byte-asserted through the harness. First-contact flex on the v8 +draft returns to the operator as design questions; the draft in +`assets/engine/` is updated to match what ships. + +### S2 — Platform family port + auth extraction + +Repo: prisma-cli. Port the ~60 management-API commands onto the engine +in grouped batches (auth + project; database + bucket; app + build + +git + agent + env; init wizard last — it stresses prompts hardest). +Extract the auth library (token storage, refresh, login-flow guts) as +its own package, distinct from Prisma Cloud code, consumed by the shell +for `getCredentials`. Retire the commander shell (`exitOverride` maze, +custom help formatter, WeakMap shims — the friction-points doc is the +kill list). DoD: every platform command on the engine, old shell +deleted, per-family shell integration proofs, parity list reviewed. + +### S3 — Composer adoption (first cross-repo consumer) + +Repos: composer + prisma-cli. Composer exports a `ProductManifest` +(the `composer` config section token — its validator rewritten from the +current throwing loader per the section API — plus its command set); +ports `deploy`/`destroy` (result commands), `dev`/`log` (session +commands), preserving the alchemy child-status passthrough exception. +Consumes the PUBLISHED engine (`./protocol` from outside, exact pins); +stands up the tandem-release workflow glue on committed versions. The +paused 1c brief is superseded by this slice — close it out against +what ships here. Proves: config machinery under real product use, +sessions, cross-repo consumption. + +### S4 — ADR 239 amendment (parallel; before S5) + +Repo: prisma/prisma. Completed-but-unsuccessful results carry dotted +codes as diagnostics inside completed envelopes with documented exit +codes; the severity-'info' evidence check (trim both scales together if +unused). Small, independent; the only ordering constraint is landing +before S5 relies on the semantics. + +### S5 — ORM adoption + +Repos: prisma/prisma + prisma-cli. The `orm` section token + +manifest; port `contract *`, `migration *` (retiring the clipanion +migration-cli), `db *`, `init`, `telemetry`, `lsp` (the server +command). Proves the diagnostics model (`migration check`, `db verify` +as completed-with-findings + catalogued exit codes) and the +exit-code-4 semantics under S4's amendment. The paused 1b brief is +superseded here — close it out against the section API. The ORM's +three colliding exit-code schemes reconcile to the contract (survey +finding). + +### S6 — Conformance checker (parallel after S1) + +Repo: prisma-cli. The small three-check tool — import purity, +validator no-throw on hostile input, published-tarball verification — +wired into both products' publish CI as S3/S5 land. + +### S7 — Release pipeline + rc1 + +Repo: prisma-cli. The `prisma` binary package assembled: full grammar +tree mounted with the build-time grammar check, committed-versions +release automation, pinned product versions, and the pipeline emitting +a publishable `prisma@8.0.0-rc1` artifact from a tagged commit. Ends +when the operator can publish with one action (project DoD). + +## Dependency graph + +```text +S1 ──► S2 ──► S3 ──► S5 ──► S7 + │ ▲ ▲ + └──────┘ (published engine exists after S2's engine hardening) +S4 (prisma/prisma) ────────► S5 +S6 (after S1) ─────────────► wired in during S3/S5 +``` + +## Coverage ledger (what proves what) + +| Engine surface | Proven by | +| --- | --- | +| Sync commands, presenters, envelopes, exit codes | S1, S2 | +| Prompts (defaults, consent, wizard) | S2 (init) | +| Poll + status events; output streams | S2 (domain wait; app/build logs) | +| Auth via context, refresh under long runs | S2, S3 (deploy) | +| Config sections, manifests, validator absence | S3, S5 | +| Session commands, signal lifetime | S3 (dev, log) | +| Cross-repo/published consumption, pins, tandem releases | S3 | +| Child-status passthrough exception | S3 | +| Diagnostics model, catalogued exit codes | S5 | +| Server command (stdio) | S5 (lsp) | +| Grammar tree completeness | S7 | + +## Out of plan (per spec non-goals) + +Daemon library and `emulator` root; ecosystem cutover/codemods/ +deprecations; `prisma.compute.ts`; GA. diff --git a/.drive/projects/prisma-cli-v8/plans/s1-engine-vertical.md b/.drive/projects/prisma-cli-v8/plans/s1-engine-vertical.md new file mode 100644 index 00000000..ffd70d09 --- /dev/null +++ b/.drive/projects/prisma-cli-v8/plans/s1-engine-vertical.md @@ -0,0 +1,133 @@ +# S1 dispatch plan — engine package + auth whoami vertical + +Slice contract: `../specs/s1-engine-vertical.md`. The v8 draft +(`../assets/engine/engine-interface-draft.ts`) is normative; any +first-contact contradiction STOPS the dispatch and returns to the +operator as a design question. + +Branch mechanics: all dispatches commit to one branch +`s1-engine-vertical` off `cli-engine-requirements`; the slice PR +targets `cli-engine-requirements`. + +Codebase grounding (2026-08-09): pnpm workspace, packages/cli + +packages/compute; @prisma/cli builds with tsdown, tests with vitest +(`packages/cli/tests/*.test.ts`); the whoami vertical is +`runAuthWhoAmI` (packages/cli/src/controllers/auth.ts:114) over +`createAuthUseCases().whoami` (packages/cli/src/use-cases/auth.ts) with +presentations in packages/cli/src/presenters/auth.ts; token storage is +packages/cli/src/adapters/token-storage.ts (+ @prisma/credentials-store); +the commander shell lives in packages/cli/src/shell/. + +## Dispatches (sequential) + +### D1 — Package scaffold + protocol subpath + +**Outcome:** `packages/cli-engine` (`@prisma/cli-engine`) exists in the +workspace, builds with tsdown, runs vitest, and exports the `./protocol` +subpath carrying the protocol types (`Diagnostic`, `CliStructuredError` +with `toEnvelope()`, `Result`, `NextAction`) ported from the +prisma/prisma donor sources with the settled adjustments (Diagnostic is +pure data ≡ envelope shape minus `ok`; NextAction has no `journey`; +severity scales identical). +**Builds on:** nothing (first dispatch). +**Hands to:** a building package whose `./protocol` import is proven +type-only (a test that imports it and a check that importing it executes +no engine code). +**Completed when:** package builds; protocol unit tests green +(`toEnvelope()` shape pinned); type-only import proven; workspace lint +passes. + +### D2 — Definition surface + type-test suite + +**Outcome:** the full v8 *type* surface compiles: defineCommand / +defineSessionCommand / defineServerCommand, flag + positional builders +with the `Char` alias typing, `Args`, `Outcome`, `Presentations` / +`PresentedResult`, `defineConfigSection` + `SectionValidation`, +`NeedsSpec` / `HelpSpec`, `ProductManifest`, `createCli` mounting types, +`Runtime` / `LoadedConfig` / `Credentials` shapes. Pure types and +constructors only — no execution. +**Builds on:** D1's package + protocol types. +**Hands to:** the definition types D3 executes against and D5 loads +config for. +**Completed when:** every compile-verified claim from the design +review rounds is a permanent +type-test with stale-@ts-expect-error discipline: Char alias +accept/reject, exitCode required-iff-catalogued in both directions, +needs.config → ctx.config inference, PresentedResult brand. Suite green. + +### D3 — Execution engine + test harness (result commands) + +**Outcome:** a result command runs end to end inside the package's own +tests: `createCli` mounts the tree on `@stricli/core@1.3.0` +(exact-pinned, fully internal), parse → needs checks → context assembly +→ handler → `ctx.present` materializing only the active format → +envelope → exit code. Both settlements work: COMPLETED (presented +result, diagnostics, documented exit codes) and ERRORED +(CliStructuredError → error envelope, engine-rendered). `--format +human|json` (`--json` alias, auto-json when stdout is not a TTY), +`--log-level` (`--verbose` alias), StreamEvent framing, `createTestCli` +harness (answers, abort, onEvent, cwd, now; exitCode/stdout/stderr/ +json/events/presented). The engine never calls `process.exit` and +writes only to provided streams — proven by harness construction. +**Builds on:** D2's definition surface. +**Hands to:** an executable engine D4 completes and D6 mounts a real +command on. +**Completed when:** harness e2e for a toy in-test command byte-asserts +human, json-stream + envelope, and errored paths with correct exit +codes (0/1/2 + catalogued). + +### D4 — Prompts, events, session/server lifetimes + +**Outcome:** the remaining execution surface: `ctx.prompt` (product +defaults accepted by `--yes`/Enter; no default halts under `--yes`; +`prompt.consent` structurally undefaultable), the event vocabulary + +rendering rules (step, progress, message severities, output channels, +remediation → NextAction, endpoint/status/artifact, opaque product +`data`), `ctx.report`, `ctx.requireDependency` (engine-phrased install +error), session-command lifetime (runs until signal, no presentation) +and server-command stdio handoff, signal exit codes (3, 130, 143). +**Builds on:** D3's execution engine + harness. +**Hands to:** the complete engine surface the acceptance sweep checks. +**Completed when:** each behavior above has a harness test; prompt +default/consent semantics test-pinned; session command terminates +cleanly on abort in tests. + +### D5 — Config loader (marker fail-early) + +**Outcome:** `Runtime.config` is populated by a minimal loader: +discover `prisma.config.ts` from cwd, evaluate it, check the +`defineConfig` version marker, produce `LoadedConfig` (raw sections + +file-level diagnostics). An evaluated file WITHOUT the marker (a +Prisma 7 config) yields the settled typed fail-early diagnostic. +**Builds on:** D2's `LoadedConfig` / section-token types (not D4 — +non-linear; the loader needs no prompts/events surface). +**Hands to:** config loading D6's bin wires into its Runtime. +**Completed when:** loader tests cover found/absent/marked/unmarked +files; the Prisma 7 fail-early diagnostic is test-pinned (code + +summary + fix). + +### D6 — `prisma-v8` bin + auth whoami port + slice e2e + +**Outcome:** a minimal unpublished bin (working name `prisma-v8`) in +packages/cli: `createCli` with one group, `auth whoami` mounted; +Runtime assembled from the real process (streams, env, TTY, signals), +`getCredentials` backed by the existing token-storage adapter in place; +the whoami definition + lazy handler calling the existing +use-case/controller logic as its operations layer; presentations +matching the current `prisma-cli auth whoami` output (parity), stdout +payload, json envelope. +**Builds on:** D3 (execution), D4 (full surface), D5 (config in +Runtime). +**Hands to:** the slice DoD: the proven vertical + the parity-divergence +list for operator review. +**Completed when:** harness e2e green for whoami human bytes, `--json` +stream + envelope, `--quiet`, errored, and unauthenticated +(needs.credentials) paths; parity divergences documented (expected: +envelope shape, exit codes); every slice acceptance box checkable. + +## Completeness check + +D1+D2 → package/protocol/type-test acceptance boxes; D3+D4 → engine +behavior + never-exits box; D5 → Prisma 7 fail-early box; D6 → parity + +e2e boxes and the draft-amendment box (any operator rulings during the +slice update `assets/engine/` before the PR opens). diff --git a/.drive/projects/prisma-cli-v8/spec.md b/.drive/projects/prisma-cli-v8/spec.md new file mode 100644 index 00000000..deee8d2c --- /dev/null +++ b/.drive/projects/prisma-cli-v8/spec.md @@ -0,0 +1,148 @@ +# Summary + +Ship the unified `prisma` CLI: one binary, one `prisma.config.ts`, and the +ORM, Composer, and Cloud command families mounted on the agreed grammar +tree — built on the settled engine design (interface v8, requirements +R1–R14). **Definition of done: the operator can publish the `prisma` npm +binary as `8.0.0-rc1` implementing the full design.** Publishing is the +operator's act; this project delivers the publishable state. + +# Description + +Prisma users today face three CLIs (`prisma-next`, `prisma-composer`, +`@prisma/cli`), three config files, and three unrelated help/output/error +dialects. The consolidation direction, grammar tree, and engine design +are settled (see `design-notes.md` for the authoritative map: engine +interface v8 with five review rounds closed; requirements R1–R14 on +prisma-cli PR #128; packaging, auth, config, and error-model rulings). +What remains — this project — is to build it: the engine library, the +unified config machinery, the shell, the auth library, and the port of +all three command families, ending in a release pipeline that can produce +`prisma@8.0.0-rc1`. + +Repos involved: **prisma-cli** (the shell, the engine package, the auth +library — home repo), **prisma/prisma** (ORM product integration; the +ADR 239 amendment), **prisma/composer** (Composer product integration). + +# Requirements + +## Functional Requirements + +1. **The engine library exists and is consumable**: `@prisma/cli-engine` + implements interface v8 — the execution protocol + (completed/errored), events, return-site presentation, config-section + tokens, prompts (defaults, `consent`), the three command kinds, the + envelopes and json stream, mounting, and the test harness — with a + `./protocol` subpath for types-only consumers and `@stricli/core` as + an exact-pinned internal dependency. Every deviation from the v8 + draft discovered during implementation returns to the operator as a + design question, not a silent fix. +2. **ADR 239 is amended first** (prisma/prisma): completed-but- + unsuccessful results carry dotted codes as diagnostics inside + completed envelopes with documented exit codes; includes the + severity-`info` evidence check (trim `CliStructuredError` and + `Diagnostic` scales together if unused). +3. **One config file**: the shell discovers and evaluates + `prisma.config.ts` exactly once; the `defineConfig` version marker + distinguishes v8 configs, and an evaluated file without the marker — + in particular a classic Prisma 7 config — fails early with a typed + error naming the migration path (fail-early is ruled; no best-effort + reading). Products contribute sections via manifests + (`ProductManifest`: section token + commands + docs base); validation + is per-section diagnostics; a command fails only when a section it + needs is invalid. +4. **The shell**: the `prisma` binary in the prisma-cli repo — mounts + the grammar tree (shell-owned paths, R12), injects the shared flag + family, implements formats (`--format`, `--json` alias, auto-json on + non-TTY stdout), log levels, prompts, signals, exit codes, and the + crash envelope, all through the engine. +5. **Auth**: the auth library (token storage, refresh, login flow guts — + extracted from `@prisma/cli`'s existing implementation) lives in the + prisma-cli repo, distinct from Prisma Cloud code; the shell consumes + it to supply `getCredentials`; credentials authenticated through any + command reach every product's operations through context. +6. **All three command families port onto the tree** per the grammar + doc: ORM (`contract *`, `migration *`, `db *`, `init`, `lsp`, …), + Composer (`project deploy|dev|log|destroy`, stubs where ruled), and + Cloud/platform (`auth *`, `project *`, `postgres *`, `service *`, + `bucket *`, `git`, `agent`, with the ruled renames). Parity bar: + behavior equivalent to the shipping CLIs except where a settled + ruling changed it (envelopes, exit codes, renames) — divergences are + enumerated, not discovered. +7. **Products integrate as designed**: prisma/prisma and composer export + manifests and command sets; the shell pins exact versions; tandem + releases run on committed versions with workflow glue. +8. **Product-repo e2e is real** (R7): each product runs argv-in/ + bytes-out tests against the engine's harness in its own repo; the + shell's own suite proves composition per family (R8). +9. **The conformance checker exists**: the small three-check tool — + import purity, validator no-throw on hostile input, published-tarball + verification — wired into CI where products publish. +10. **A release pipeline produces the publishable artifact**: versioned + per the committed-versions ruling, capable of emitting + `prisma@8.0.0-rc1` on demand. + +## Non-Functional Requirements + +- Requirements **R1–R14** (prisma-cli `docs/architecture/ + cli-engine-requirements.md`) govern throughout; this spec does not + restate them. +- The settled error/result conventions (prisma/prisma ADR 239 as + amended, ADR 245; composer ADR-0043/0044) hold everywhere. +- The exit-code contract: 0 completed / 1 bug only / 2 errored / + 3 abort / 4–99 documented / 130,143 signals. +- The CLI never touches a package manager (R13); optional capability = + optional peer dependency + engine-phrased error. +- Runtime-agnostic products (R4): context-not-environment discipline + throughout the ports. +- The engine design artifacts live in this project directory + (`assets/engine/`); the durable subset (the final interface record, + ADRs) migrates into `docs/` at the appropriate slices and at + close-out. + +## Non-goals + +- **Daemon library and emulator management commands** — parked by + ruling (`assets/engine/daemon-library-notes.md`); the `emulator` + root stays off this project's tree. +- **Prisma 7 config compatibility** beyond the fail-early typed error. +- **Ecosystem cutover**: codemods (`prisma-next.config.ts` → + `prisma.config.ts`), create-prisma templates, deprecation of the + three existing binaries, docs-site updates, the npm takeover + sequencing itself — follow-on work after rc1 exists. +- **`prisma.compute.ts` migration** — separate Terminal effort. +- **Resuming the paused 1b/1c briefs** — sequenced after the engine's + config API lands; their revision is the trigger to unpause, tracked + in delivery, but their content is not this project's deliverable. +- **GA (non-rc) release.** + +# Acceptance Criteria + +- [ ] `@prisma/cli-engine` published (or publishable) from the + prisma-cli repo; its `./protocol` subpath consumed type-only by at + least one product; the v8 draft's compile-verified typing claims + hold in the shipped package's tests. +- [ ] ADR 239 amendment merged in prisma/prisma before any port relies + on completed-with-diagnostics semantics. +- [ ] A v8 `prisma.config.ts` with sections for all three products + drives a real workflow end to end; a Prisma 7 config file produces + the typed fail-early error, test-pinned. +- [ ] Every command family mounted; the shipped tree checked against the + grammar doc by a build-time test; per-family parity divergence + lists reviewed by the operator. +- [ ] `prisma auth login` through to a Composer deploy consuming the + same credentials via context, e2e. +- [ ] Product-repo e2e suites exist and pass in prisma/prisma and + composer using the engine harness; shell integration proofs pass + per family. +- [ ] Conformance checker runs in CI for both products' publish paths. +- [ ] The release pipeline emits a `prisma@8.0.0-rc1` artifact from a + tagged commit; the operator can publish it with one action. + +# Cross-cutting + +- Drive process governs delivery: slices are one-PR units; deviations + from settled design return to the operator; retros land learnings in + durable memory. +- Commit/PR discipline per the operator's standing rules (bot identity, + dual sign-off, explicit staging, verify-PR-open-before-push). diff --git a/.drive/projects/prisma-cli-v8/specs/s1-engine-vertical.md b/.drive/projects/prisma-cli-v8/specs/s1-engine-vertical.md new file mode 100644 index 00000000..83dc6706 --- /dev/null +++ b/.drive/projects/prisma-cli-v8/specs/s1-engine-vertical.md @@ -0,0 +1,83 @@ +# S1 — Engine package + one vertical command (slice contract) + +One PR into the `cli-engine-requirements` lineage (prisma-cli repo). + +## Goal + +`@prisma/cli-engine` exists, implements the v8 interface, and is proven +end to end by ONE ported platform command — `auth whoami` — running +through a minimal bin: parse → preconditions → context → handler → +presentation → envelope → exit code, byte-asserted through the +package's own test harness. + +## In scope + +1. **The engine package** (new workspace package in this repo): + - The protocol types (`CliStructuredError`, `Result`, `Diagnostic`, + `NextAction`) implemented from the prisma/prisma donor sources + (`packages/1-framework/0-foundation/utils/`, `1-core/errors/ + control.ts` lines ~9–111) with the settled adjustments (Diagnostic + as pure data ≡ envelope shape; NextAction without `journey`), + exposed at the `./protocol` subpath. + - The full v8 surface from + `.drive/projects/prisma-cli-v8/assets/engine/ + engine-interface-draft.ts`: defineConfigSection, defineCommand/ + defineSessionCommand/defineServerCommand, flag/positional builders + (Char alias typing), Args, CommandContext (present with Outcome, + report, prompt incl. consent + defaults under --yes, signal, cwd, + requireDependency, getCredentials), events + rendering rules, + Blocks + Ui, envelopes + StreamEvent framing, createCli + Runtime + + LoadedConfig, createTestCli. `@stricli/core@1.3.0` exact-pinned, + fully internal. + - The compile-verified typing claims from the design review rounds + become permanent type-tests in the package + (@ts-expect-error suites): Char alias accept/reject, Outcome + exitCode required-iff-catalogued both directions, needs.config → + ctx.config inference, PresentedResult brand. +2. **A minimal config loader** behind `Runtime.config`: discover + `prisma.config.ts` from cwd, evaluate it, check the `defineConfig` + version marker, produce `LoadedConfig` (raw sections + + file-level diagnostics). An evaluated file WITHOUT the marker (a + classic Prisma 7 config) yields the typed fail-early diagnostic — + test-pinned. (Full loader polish, section registration UX, and the + `defineConfig` helper's final home evolve in S3; the marker + semantics are settled and land now.) +3. **A minimal bin** (`prisma-v8` working name, not published): + createCli with one group, `auth whoami` mounted, Runtime assembled + from the real process (streams, env, TTY, signals) with + `getCredentials` backed by the EXISTING token-storage adapter + in place (extraction is S2). +4. **The `auth whoami` port**: definition + lazy handler calling the + existing controller logic as its operations layer; presentations per + the platform's current output (parity), stdout payload, json + envelope. +5. **Tests**: engine unit tests; harness e2e for whoami (human bytes, + `--json` stream + envelope, `--quiet`, exit codes incl. errored and + unauthenticated preconditions); marker fail-early; "engine never + calls process.exit and writes only to provided streams" proven by + harness construction. + +## Out of scope + +Every other command; commander-shell removal (S2); auth-library +extraction (S2); ProductManifest consumption from another repo (S3); +publishing. + +## Design authority + +The v8 draft is normative. Where implementation contradicts it, STOP +and return the question — the draft gets amended by the operator's +ruling, never silently. Requirements R1–R14 +(`docs/architecture/cli-engine-requirements.md`) govern. + +## Acceptance + +- [ ] Package builds; `./protocol` subpath importable type-only. +- [ ] Type-test suite green, including every ported compile-verified + claim (with stale-@ts-expect-error control discipline). +- [ ] `prisma-v8 auth whoami` parity with `prisma-cli auth whoami` + (documented divergences only: envelope shape, exit codes). +- [ ] Harness e2e green for human/json/quiet/errored/unauthenticated. +- [ ] Prisma 7 config file → typed fail-early error, test-pinned. +- [ ] v8 draft in `assets/engine/` updated to match any operator-ruled + amendments made during the slice. diff --git a/biome.jsonc b/biome.jsonc index 9d7b8c00..dea2743e 100644 --- a/biome.jsonc +++ b/biome.jsonc @@ -6,7 +6,8 @@ "useIgnoreFile": true }, "files": { - "ignoreUnknown": false + "ignoreUnknown": false, + "includes": ["**", "!.drive"] }, "formatter": { "enabled": true, diff --git a/docs/architecture/cli-engine-requirements.md b/docs/architecture/cli-engine-requirements.md new file mode 100644 index 00000000..0a9bfc58 --- /dev/null +++ b/docs/architecture/cli-engine-requirements.md @@ -0,0 +1,285 @@ +# Requirements for the unified CLI engine + +Status: **Agreed** (Will Madden, 2026-08-09), including the engine-internals +decision at the end. This document records the design constraints for the +consolidated `prisma` CLI — the interface between the CLI shell and the +product packages it hosts (Prisma ORM, Prisma Composer, Prisma Cloud) — and +why each constraint matters. It precedes and will govern the engine's design; +tool and framework choices are evaluated against this list, not the other way +around. + +## The shape being constrained + +One `prisma` binary. It owns everything user-facing — argument parsing, help, +prompts, rendering, `--json`, exit codes, loading `prisma.config.ts` — and it +gets its functionality from product packages it depends on. Each product +already exposes a typed operations API returning structured results; the +engine is the thin layer that turns argv into operation calls. These +requirements constrain that layer and the package interface around it. + +## Requirements + +### R1 — One language, directly executable + +A product authors its CLI contributions in the engine's vocabulary, and that +artifact is what runs. There is no product-side data structure that a separate +interpreter translates into commands. + +**Why:** every stage of interpretation is complexity we own forever — a schema +that grows toward being a CLI framework, an interpreter to maintain, and a +translation step contributors must understand before they can add a flag. +Complexity raises the odds something goes wrong, deters outside contributions, +and makes testing indirect. One vocabulary that is itself runnable keeps the +system learnable and the test path short. + +### R2 — Commands end in typed operation calls + +A command handler calls its product's operations client, and TypeScript +enforces the arguments. The command layer contains phrasing and wiring, never +business logic. + +**Why:** the operations layer is where correctness is enforced and where +exhaustive testing lives. If command handlers grow logic, that logic escapes +the product's own test suite and the type checker's view of the operation +contract. Keeping handlers thin means the compiler proves the CLI calls each +product correctly. + +### R3 — The engine package is the whole contract + +Products import our engine package and nothing else for CLI purposes. No +third-party type appears in a product's exports, whether or not the engine +internally wraps a third-party framework. + +**Why:** whatever sits in the public interface between our packages can only +be replaced by coordinating simultaneous releases across every repo. Keeping +third-party primitives out of that interface bounds our exposure: swapping the +engine's internals is one package's problem, not an ecosystem event. + +### R4 — Products receive a context, never the environment + +Product code never reads engine-owned state directly: the process +environment, the process streams, TTY state, and the CLI's configuration +reach a handler only through one typed context object, which carries its +validated config section, credentials, the invocation's `cwd` and `env`, +and the output surface. The output surface is a typed result and event +sink — it exposes no writable streams and no way to exit the process; +rendering and exit codes stay in the engine (R5), and the process ends +only through the runtime's exit proxy, which the engine alone calls. + +This does not prohibit product-domain disk access. Reading the user's +project — including Composer importing the user's own modules at +execution time under R9 — is a product's job, done inside the handler +and anchored at the context's `cwd` (with `requireDependency` on the +context resolving optional dependencies from the user's project per +R13). What is banned is reaching around the context to process globals. + +**Why:** three reasons. Cross-cutting state — above all authentication +credentials, which Composer needs even when the user authenticated through a +Cloud command — has to flow somewhere, and handing it in as context is +strictly simpler than sharing the read-it-off-disk logic between packages. +Products that never touch the environment are agnostic about runtime (node, +bun, deno) by construction. And a handler whose whole world arrives as one +argument is trivially fakeable in tests. + +### R5 — Products have no presentational API + +Products supply words and structure: command descriptions, flag names, +examples, structured errors. They cannot print, color, format, or exit — +the interface offers no way to express it. Help layout, ANSI and color +policy, error rendering, `--json` envelopes, streaming format, and exit +codes are implemented exactly once, in the engine. + +**Why:** our CLIs were authored in different years by different teams with +different goals, and it shows: help text, phrasing, error codes, visual +display, color handling — none of it is consistent, and convention (style +guides, review comments) demonstrably did not hold the line. Consistency has +to be structural: a product that cannot render cannot diverge. This is the +constraint the rest of the design serves. + +Two clarifications. First, this requirement constrains *who owns* rendering, +not how it is built: the engine may adopt its internal framework's renderer +wherever that output satisfies the CLI Style Guide — custom rendering is the +escape hatch for what the framework cannot express, not the default. (The +hand-rolled help formatter in the current ORM CLI exists because its +framework rendered badly, not because owning the pixels is a goal.) Second, +there are no global flags: a flag like `--json` is declared per command — +many commands legitimately don't accept it — and the engine provides a +shared flag-set so the commands that do support it declare it uniformly. + +### R6 — Errors and results follow the settled conventions + +Every user-surfaced failure is a structured error built at its origin, with a +dotted namespace code, carried in the shared `Result` shape with the single +`ok` discriminator. The engine maps failures to the error envelope and exit +codes uniformly (0 ok; 1 bug only; 2 expected failure; 3 user abort). + +**Why:** these rules are already accepted and shipping (prisma/prisma ADR 239 +and ADR 245; Composer ADR-0043/0044). The engine is where they become visible +to users, so it must implement them once rather than trusting each product's +rendering. Machine consumers — agents, CI — branch on `ok`, `code`, and exit +codes; that only works if exactly one code space and one envelope exist. + +### R7 — Product-repo end-to-end tests are first-class + +A product can instantiate the engine with only its own commands and run real +argv-in, bytes-out tests in its own repository. Production is the same +machinery, so those tests are valid evidence about the shipped CLI. + +**Why:** piloting the operations client alone misses whatever happens in real +CLI execution — parsing, context handoff, rendering, exit codes. If products +cannot test that in-repo, every CLI regression is discovered late, in the +shell's repo, by someone who didn't make the change. The engine being +instance-based (no global state) is what makes this possible; it is a hard +requirement on any framework we adopt or build. + +### R8 — The shell's test burden is integration proof + +The shell end-to-end tests the happy path of each product's critical +operations: the mounted tree, config handoff, and auth context demonstrably +work. Exhaustive error-case testing stays in product repos. + +**Why:** the shell must prove composition works — that is the one thing only +it can test. Duplicating the products' error matrices there would rot and +would blur who owns which guarantee. Cheap for the shell, exhaustive in the +product: each test lives where its failure would be introduced. + +### R9 — Static tree, lazy guts + +Command definitions are cheap and load at startup; heavy dependencies load +inside handlers at execution time. No dynamic or discovery-driven tree +construction: each engine instance builds its tree once at startup from +statically defined structure — command definitions are direct function +references, and nothing about the tree is discovered at run time. + +**Why:** a statically known tree is simpler to reason about, renders complete +help without executing product code, and fails at build time when it is wrong. +The expensive parts — driver stacks, Composer's dependency tree (which imports +the user's own modules and has crashed at import in the past) — stay out of +the startup path, so `prisma migrate` can never be taken down by a product it +isn't using. The split follows the existing design for isolating heavy +dependency subtrees behind execution-time imports. + +### R10 — One config file, validated by its products, never a crash + +The engine discovers and evaluates one `prisma.config.ts`. Each product +contributes a named section and a never-throwing validator; validation +produces per-section diagnostics, and a command fails only if a section it +needs is invalid. "Never a crash" covers loading too: a file that fails to +import or evaluate — a syntax error, a throwing top-level statement — is +caught by the engine and surfaced as a typed file-level diagnostic naming +the config path, never a stack trace. + +The version-marker contract is exact. The engine package owns both sides +of it: its `defineConfig` stamps the exported config value with a +`$prismaConfig` field carrying the config contract version, and its +loader checks that field before interpreting anything else. Each failure +mode has its own typed, file-level diagnostic carrying the config path: +an evaluated file without the marker (in particular a classic Prisma 7 +config, which uses the same filename) fails early with +`CLI.CONFIG_MISSING_MARKER`; a marker declaring a version this CLI does +not support — older or newer, future markers included — fails with +`CLI.CONFIG_INVALID`; a file that cannot be evaluated at all fails with +`CLI.CONFIG_UNREADABLE`, per the previous paragraph. No best-effort +reading of unmarked files. + +**Why:** the unified CLI claims a filename Prisma 7 already owns; a silently +misparsed v7 file is the worst launch bug available, so detection is a +structural marker, not a heuristic. Per-section diagnostics exist because one +product's config problem must not brick the other products' commands. And +validators that throw turn a user's typo into a stack trace instead of a +diagnostic with a fix. + +### R11 — Pinned versions, tandem releases + +The shell pins exact product versions. Shipping a product change to users +means releasing the shell with a bumped pin; release automation or workflow +glue makes that cheap, and no version ranges are used. + +**Why:** with ranges, two users on the same shell version can run different +product code — support and reproducibility poison. Exact pins mean a shell +version fully determines behavior. The cost is tandem releasing, which is +tedious but simple, and simplicity wins. + +### R12 — The shell defines the command tree + +Products export commands; the shell decides where each one mounts. A command's +path in the tree does not appear in the product's code or its interface. + +**Why:** the tree is a whole-CLI concern, and the evidence is its own history: +defining the consolidated tree took roughly six months of iteration by the +responsible product manager, coordinating renames and regroupings across +product lines (`app` → `service`, `database` → `postgres`, standalone +`format` folded into `contract format`) that no product would have made +locally. From a product's point of view the path is purely cosmetic — the +real invocation is the command and its arguments, and a product's in-repo +e2e tests exercise exactly that by mounting the command at any path. From +the shell's point of view the tree is structural: central definition makes +path collisions impossible and keeps the shipped tree checkable against the +agreed grammar in one place. + +### R13 — The CLI never touches a package manager + +The shell does not install, download, or vendor packages at runtime — no +self-installing command modules, no hidden `node_modules`, ever. Components +that only some commands need are declared as optional peer dependencies; a +command that requires one checks for it at execution time and, when it is +absent, returns a structured error naming the dependency and how to install +it with the user's own package manager. + +**Why:** a previous incarnation of the Prisma CLI installed command +submodules on demand into a hidden `node_modules` in the working directory. +That approach is compatible with exactly one package manager and produces +edge cases everywhere else — lockfiles that lie, deduplication that never +happens, state the user cannot see or clean. The user already has a package +manager; the CLI's job is to declare what it needs and say clearly what is +missing, not to become a second, worse package manager. The structured +"optional dependency missing" error follows R6 like every other expected +failure. + + +### R14 — One event vocabulary, engine-defined, with product extensions + +Commands report progress as structured events in a vocabulary the engine +defines: generic, CLI-shaped concepts (steps, warnings, remediation, +child-process output, endpoints) with consistent fields the engine knows how +to present in both human and `--json` modes. Products must fill and +primarily use those common fields. Alongside them, an event may carry +product-populated extension data: context-dependent structures the product +defines, versions, and documents as part of its own public API — the engine +passes them through to `--json` consumers untouched and does not enforce +them. A structure recurring across commands or products is the signal that +the engine vocabulary is missing a concept and should adopt it. + +**Why:** two event dialects already evolved independently (Composer's +per-operation event unions; the ORM's progress spans), which is the machine- +surface version of the presentational drift R5 exists to kill — one +vocabulary means agents and CI learn one language for the whole CLI. But a +strictly closed vocabulary would either lose product-specific facts or grow +by fiat; the extension field keeps machine consumers fully informed, puts +the compatibility burden where the knowledge is (the product publishes its +extension interfaces), and gives the vocabulary an evidence-driven growth +path instead of speculation. + +## The engine's internals + +Decided (Will Madden, 2026-08-09): the engine wraps **@stricli/core**, +fully hidden per R3 — no stricli type appears in the engine's public +interface, so the internals remain replaceable. + +The decision followed the evaluation rubric recorded in prisma/prisma at +[`docs/architecture docs/research/commander-friction-points.md`](https://github.com/prisma/prisma/blob/main/docs/architecture%20docs/research/commander-friction-points.md) +(the directory really is named `architecture docs`, space included). Commander +was ruled out there. Clipanion, the incumbent candidate with in-house +precedent, passes the rubric's nine technical criteria but fails the tenth: +at decision time its last publish, 4.0.0-rc.4 (2024-09-06), was 23 months +old with its 4.x line in release-candidate state for three years, and its +latest stable release, 3.2.1, dated from June 2023. Stricli — evaluated at +`@stricli/core` 1.3.0 (published 2026-07-16), the version the engine now +pins exactly — passes all ten: zero runtime dependencies, no `node:` +imports, no `process.exit` (verified against the published 1.3.0 +artifact), per-invocation injected context, static +route maps with lazy command loading, parse-time validation with typed +errors, active institutional maintenance — and its known limitations +(per-command flags only, fixed help layout without formatter replacement) +are neutralized or made irrelevant by this document's own rules (R5's +engine-owned rendering; the no-global-flags rule).