Port migration show, log and graph; produce typed next actions in the command layer - #29973
Conversation
…ion marker
Config loading no longer fails wholesale on the first structural problem.
- loadConfig now returns Result<{ config, diagnostics }, CliStructuredError>.
Structural problems in an evaluated config become CONFIG.VALIDATION_FAILED
diagnostics tagged with the config section they concern (meta.section,
meta.field). Commands fail (exit 2, rendering the diagnostic) only when a
diagnostic concerns a section they read, via requireConfigSections /
loadConfigForSections; commands not touching that section proceed.
- A config module that cannot be evaluated at all fails the load with the
new CONFIG.EVALUATION_FAILED (previously surfaced as CLI.UNEXPECTED).
- defineConfig stops validating and throwing: a throw there happens at
module-evaluation time and would turn every structural problem into an
all-commands-fail evaluation error. Validation moved to the loader via
collectConfigIssues, which reports every problem instead of the first.
- defineConfig now normalizes and stamps a non-enumerable config-format
version marker; the loader rejects configs that were not created by the
current defineConfig with the new CONFIG.VERSION_MARKER_MISSING (fail
early, no best-effort reading of unmarked configs). The marker is read
from c12's raw layer because the c12 merge drops non-enumerable
properties.
- All production call sites (CLI commands and operations, language server,
Vite plugin, cli-telemetry) consume the new API. Ten commands that
previously leaked config errors as unhandled throws (stack trace,
exit 1) — including db init and db update — now render the structured
envelope and exit 2.
- Error registry: CONFIG.EVALUATION_FAILED and CONFIG.VERSION_MARKER_MISSING
added; CONFIG.VALIDATION_FAILED updated with the new meta shape and
producing sites.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
…sting Adds createFixtureControlClient, a ControlClient double for host and product tests: every operation resolves realistic postgres-flavored fixture payloads without touching a database or driver, each fixture is overridable per test, and every call is recorded for assertions. The implementing class `implements ControlClient` and a type test asserts the double stays in sync with the real client, so an interface change fails compilation here. Published via the @internal/cli ./control-api/testing subpath, which the shell build maps to @prisma/orm-toolchain/cli/control-api/testing automatically. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
…rker-and-control-client-double
The framework domain is family-blind, and `lint:framework-vocabulary` counted seven new SQL/target-vocabulary lines: six in the fixture ControlClient defaults and one in the `CONFIG.VERSION_MARKER_MISSING` fix text. The fixture defaults now report neutral `FIXTURE_TARGET_ID` / `FIXTURE_FAMILY_ID` ids (both exported so tests can assert them), an operation label that names the model rather than the DDL, and an empty introspection payload. The marker error points at the target package config entrypoint without naming a target; the error reference keeps the concrete example. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
The example-migration regen writes a temporary config that spreads the example's real config. The version marker is non-enumerable, so the spread dropped it and every emit failed with CONFIG.VERSION_MARKER_MISSING. Passing the spread result through defineConfig re-stamps it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
…tion src/load.ts sat below the package coverage thresholds: loadConfigForSections had no test at all, and none of the toConfigLoadFailure arms were exercised beyond the plain-Error case. Adds tests for loadConfigForSections (clean, blocking diagnostic, and load failure), for a config module that throws a CliStructuredError, a plain structured error, a non-Error value, or fails to resolve an import, and for a contract that declares no inputs. finalizeConfig grew a finalizeContractConfig half that takes a contract section directly, so the loader no longer re-checks a contract it has already narrowed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
`lint:legacy-name` rejects `prisma-next` outside the allowed uses, and the unresolvable-import test used it in a made-up package name. Any unresolvable specifier works. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
The `orm` command family owns one config section, and the whole Prisma Next configuration nests under it: `contract`, `db`, `migrations` and the rest become subsections of `orm`. The engine models one section per family, so per-subsection blocking does not survive the nesting — any structural problem anywhere in the section blocks every `orm` command. The validator is synchronous and total: it takes the raw section value, returns engine diagnostics, and never throws. Structural checking reuses `collectConfigIssues`; the emitted-artifact-as-contract-input check moves here too, since it needs neither the filesystem nor the config directory. Everything the loader does asynchronously — evaluating the file and finalizing paths against its directory — stays in the bin adapter. `@prisma/cli-engine` is pinned at an exact version so the shell and the family share one module instance. It is a first-party Prisma package, so it joins the release-age cooldown exemption list for the same reason `@prisma/dev` is on it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
The engine and prisma/prisma each define a class called `CliStructuredError`, and the engine's duck-typed guard accepts both. The two are not the same shape: the engine settles on `error.nextActions`, which prisma/prisma's class never sets, and prisma/prisma emits `fix` prose, which the protocol has no field for. Left unconverted, an error settles as an envelope with `nextActions: undefined` and a stray `fix`. `normalizeError` is that conversion, and it is the only one: handlers pass what they return through `notOk` and what a top-of-handler catch sees through the same helper. Everything below keeps raising exactly what it raises today. `nextActions` is always present, derived from the `fix` prose while the transition lasts — one action per line of it, since a multi-line fix is several pieces of advice. `toEngineDiagnostic` is the same projection without the throw, for callers that need a diagnostic rather than an error. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
The engine takes its config from the bin, and its own convenience loader reads prisma.config.ts with the $prismaConfig marker — not the file this bin reads. So the bin owns the load: the ORM's c12 loader evaluates the module and finalizes contract paths against the config file's directory, and the adapter nests the whole result as the single `orm` section. Only failures that prevent evaluation entirely — no file, a module that does not evaluate, a missing version marker — become diagnostics here, tagged `section: null` so they fail exactly the commands that read config. A config that evaluates but is structurally wrong passes through untouched; that verdict is the section validator's. Config discovery now takes the directory as a parameter instead of reading process state, so the adapter honours the cwd the engine was given. That is what lets a test harness run several projects in one process, and it defaults to process.cwd() for every existing caller. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
The `orm` command family, the second prisma-next binary, and the three telemetry commands, all landing beside the commander program rather than replacing it — the old bin still owns `bin.prisma-next` until the retirement round. The family carries the config section, an empty command map for now, and the docs base the engine appends each diagnostic code to. `telemetry status|enable|disable` are deliberately NOT family members: they are commands of this binary only, the unified shell has its own, and they retire with the binary at cutover. Telemetry now reports from `onSettled`, so the event carries the exit code. Two consequences, both intended: a run killed before settlement emits nothing, and so does a run that never reaches a mounted command. The old `telemetry`-group exemption is gone with the pre-run fire — under `onSettled`, `telemetry disable` has already disabled by the time the hook would send. The wire shape is unchanged: the engine's value-free snapshot is projected into the shape the existing sanitiser reads, and the detached sender still derives the two config-derived fields itself. The pinned engine has no shell-level `--config`, so the bin reads the flag off argv and strips it before the engine parses. That interim is deleted when a version carrying the engine's own `--config` publishes. `--db` gets one shared spec so its brief and placeholder cannot drift across the commands that will declare it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
The first ported command, and the template the rest copy: one file per command under src/orm/<group>/, definition and handler together, the handler calling the existing control-API operation with config and cwd from the context rather than loading either itself. The tree renderer keeps its line-producing code and its lines ship as the `stdout` presentation — the engine writes them to stdout in human mode and drops them in json mode, which keeps the json document the only machine surface. They are deliberately not `list` blocks: block rendering prefixes every item with "- ", which would glue a bullet onto box drawing. Colour is off until the engine exposes its resolved colour mode. The header details become a fields block on stderr. The old commander `migration list` is untouched and still owns the shipped binary; this one runs on the second bin. Two behaviour changes fall out of the engine surface, both divergences. `--legend` is now human-only decoration rather than an error when combined with `--json` or `--quiet`: a handler cannot see the resolved format, and in json mode the human presentation is never materialized, so the legend simply does not appear. And migrations resolve against the invocation directory rather than the config file's directory — the same place for every invocation that does not pass the interim `--config`. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
Journeys have always run in-process against commander factories, calling `process.chdir` and passing `--config` on every step — which is why their vitest config needs `pool: forks`. `runOnEngine` is the replacement: it builds a fresh `TestCli` per step and passes the step directory as `cwd`, so nothing about a run is process-global. The harness takes config as an already-evaluated record and `run()` has no config option, so the journey's real `prisma-next.config.ts` is evaluated here, through the same adapter the binary uses. Evaluating it per step is what lets a step that writes or rewrites the config be picked up by the next one, which is what `init` journeys will need. Only `migration list` moves onto it this round — it is the only ported command. Every other wrapper still runs the commander factory, and both paths coexist until the shell is retired. The new journey is the proof the harness works against a real project on disk: two planned migrations, the tree on stdout in human mode, a clean frame stream in json mode, and an unknown space settling as an errored envelope with typed next actions and no `fix`. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
Six operation modules loaded config themselves and fell back to the process cwd for path resolution. Under a harness that supplies a directory per run, that is silently wrong — the operation would read a different project than the one the command was invoked against. They now take the already-loaded config and the invocation directory as explicit parameters, and no file under src/control-api/ calls the config loader or reads process.cwd. `resolveMigrationPaths` takes the directory too. `contract emit` loses its double load in the process: the command already loaded config to compute header display paths, and that result is now what the operation receives. Two ordering changes fall out of hoisting the load, both only observable when the config is broken. `contract emit`'s header load now asks for all five sections rather than just `contract`, so a config broken elsewhere errors before the header prints. And `ref set` loads config before checking the ref name, so an invalid name plus an unloadable config reports the config error. `migration check` was not on the list but is under the same directory and had two process-cwd reads, so it takes a cwd too. Its `enumerateCheckSpaces` is re-exported, making that a published signature change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
…rting `telemetry status|enable|disable` already exist on the prisma-cli side and will be served by the unified binary, so the prisma-next bin does not carry its own copies — they would only die again with the commander shell at cutover. The commander CLI still ships its versions until then; this removes the engine ports written earlier in the round. Reporting is unaffected: the bin still wires the engine's onSettled hook to this repo's telemetry sender. `@prisma/orm-toolchain` declares the same exact engine pin as `@internal/cli`, which the shell build check requires — the two must resolve one module instance rather than two copies with distinct brands and classes. The init journey installs into a scratch project outside the workspace, so it needs its own release-age exemption for that pin. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
The typed remediation is produced in the command layer and nowhere else: a run-command action names an executable CLI invocation, which is knowledge only the CLI has. `src/utils/cli-errors.ts` is CLI-package code, so its fourteen factories attach the actions directly; `@internal/errors` and every other library keeps raising `code`/`why`/`fix` prose, and no foundation package learns the NextAction type. The factories now build an `ActionableCliError`, a CLI-package subclass that carries both fields. `fix` stays because the commander shell still renders it and every pre-cutover change is additive; the handler boundary is what drops it. `normalizeError` therefore prefers a raised error's typed actions over deriving them from prose, and only short-circuits for an error the engine itself built — identity, not a duck test, since the engine is an exact-pinned unbundled dependency with one module instance. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
The `--json` document `migration check` publishes is the CLI's own output, so its failure rows carry the typed remediation rather than `fix` prose: `checkFailureSchema` swaps the `fix: string` field for `nextActions`, and the two producers — the integrity-violation catalogue and the explicit per-space graph checks — build actions instead of sentences. This is a breaking change to the published `--json` shape, and the one user-visible change in this PR that reaches the commander shell: its human output now prints a `next:` line per action where it printed one `fix:` line. Recorded for the divergence file. The two action constructors move to their own module so the check producers and the error factories share them without the check path importing the error module. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
The second ported command, and the first with a positional. The detail block — metadata, the operation tree, the DDL preview — is a rich renderer, so its lines keep their line-producing code and ship as the `stdout` presentation exactly as `migration list`'s table does; the header details become a fields block on stderr, minus the `config` row, since config discovery is the shell's and no ported command declares `--config`. A path-looking target used to resolve against the process working directory. It now resolves against `ctx.cwd`, which is what makes the command correct under a harness that supplies a cwd per run; `resolveAppTargetPath` and `resolveTargetPathAcrossSpaces` take it explicitly and `migration check` threads its own through unchanged. The path-resolution helpers move to a sibling module the migration commands share, and the two errors `migration show` raises on its own become named factories carrying typed next actions, used by both the commander command and the ported handler. The old commander `migration show` is untouched and still owns the shipped binary. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
The ledger table keeps its line-producing code and ships as the `stdout` presentation; the masked connection URL becomes the one header field on stderr, the `config` row going away with per-command `--config`. Carries the dotted-code fix the contract calls for: an unsupported target raised `CLI.UNEXPECTED` here where every sibling raises `MIGRATION.TARGET_UNSUPPORTED`. The ported command raises the migration code, which is already documented in the error reference. `--db` comes from the shared flag constant, so its brief and placeholder cannot drift from the other database-touching commands. The old commander `migration log` is untouched and still owns the shipped binary. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
The engine reserves --format, so DOT cannot become a format value. Instead the precedence quirk goes away: with --dot the DOT text is the command's stdout payload in human mode, and in json mode the result carries it as a `dot` field alongside the graph document, so a caller asking for json never gets DOT where json was promised. --dot with --legend stays an error and --dot still ignores --space. Tree-section building, the human rendering and the DOT rendering move to a formatter module the commander command and the ported handler share, so the two cannot drift while they coexist. Header paths render relative to the invocation directory, as the commander shell rendered them. `migration list` moves to the same helper — it was printing the absolute migrations directory, which was a needless divergence from the shell it replaces. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
`migration show`, `migration log` and `migration graph` join `migration list` on `runOnEngine`, so their journeys drive the ported commands rather than the commander factories. Assertions move from captured stdout to the envelope, the presented document and the exit code. The DOT journey changes shape because its premise did: it used to pin that an explicit `--dot` beat the auto-JSON default when stdout was piped. Under the engine that precedence problem cannot arise — piping selects json and the DOT rides the result as a `dot` field alongside the graph document, so a caller who asked for json is never handed DOT. The journey now pins both halves plus the `--legend` refusal. `migration log` gets the end-to-end coverage it has never had: two migrations applied, both edges reported from the live ledger, chained so each edge starts where the last ended. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
The seeded operation used an operationClass the op schema rejects, so the loader dropped the package and the assertions ran against an empty project — including one that expected the empty-project line right after seeding. Seed a valid operation, assert the rendered table, and cover the empty project as its own case. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
The four `as Record<string, unknown>` casts in `config-validation.ts` broke the repo rule against bare `as` in production code, and they also hid a reporting wart: a section holding a non-object (`family: 'sql'`) walked the descriptor fields anyway and produced one issue per missing field instead of one clear "must be an object" issue. `isObject` already narrows to `Record<string, unknown>` without a cast, so `validateFamily`, `validateTarget`, and the `adapter`/`driver` walks now use it and report a single object-type issue per malformed section. Also rewrites the rulecard's "Default Path Resolution" example, which told authors to use `pathe` but then showed only a bare string fallback. It now shows what the loader actually does: resolve the config-relative default against the config file's directory. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
Two defects in `loadConfig`, both reported against the wrong file. First, load failures were classified by searching the error message for 'not found', 'Cannot find' or 'ENOENT'. A config that imports a missing package throws `Cannot find module 'x'`, so the user was told their config file did not exist when it did. Verified against c12: a missing config file never throws — c12 resolves nothing and returns an empty config, which `loadConfig` already maps to CONFIG.FILE_NOT_FOUND on the non-throwing path. So everything reaching this catch came from evaluating a file that exists, and all of it is now CONFIG.EVALUATION_FAILED. Second, the version-marker check accepted the marker from any c12 layer, including `extends` bases and rc files. A config that does not itself go through `defineConfig` passed as long as some base did. c12 puts the requested config file first in `layers` (we pass no `overrides`), so the check now reads only that layer. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
A command that reads a section it did not declare bypasses section-scoped diagnostics: the malformed value flows into execution and fails later as something else. `migration status` turned a malformed `contract` section into a CONTRACT.UNREADABLE warning; `migrate` failed while resolving the contract path. Swept all 21 `loadConfigForSections` call sites against the sections each one reads, directly or through a helper (`resolveContractPath`, `readContractEnvelope`, `loadContractRawSafely`, `buildReadAggregate`, `loadAggregateIntegrityViolations` read `contract`; `resolveMigrationPaths` reads `migrations`). Eleven were short: - `contract` added to migrate, migration status, migration list, migration graph, migration check, migration ref set, migrate --show - `migrations` added to migration log, db sign, db verify, and the shared migration-command scaffold (db init, db update) `driver` is deliberately not added to the read-only commands that build a control stack. `createControlStack` stores the descriptor but only database operations use it, and every command that connects already declares `driver`; requiring it everywhere would defeat section scoping. The regression test lives in the integration suite rather than the CLI package: the CLI suite runs with `isolate: false`, so a new file importing command modules collides with the per-file `@internal/config-loader` mocks its neighbours already register. The integration test drives the real commands against a real malformed config and covers the command handlers that are module-private. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
…ycle The double served every operation from fixtures whether or not `connect()` had been awaited, so a caller that forgot to connect passed its tests and then failed against the real client. It now tracks initialization the way `ControlClientImpl` does: `init()` is idempotent, `connect()` calls it, and the eleven operations the real client routes through `ensureConnected` (verify, schemaVerify, sign, dbInit, dbUpdate, dbVerify, readMarker, readAllMarkers, readLedger, migrate, introspect) reject with DRIVER.NOT_CONNECTED while disconnected. The five that only need `init()` in the real client (toSchemaView, inferPslContract, getPslBlockDescriptors, toOperationPreview, emit) keep working without a connection. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
…s from `resolveConfigInputs` required family, target, adapter, driver and extensions before it knew whether the project was PSL or TypeScript. A TypeScript contract project derives its inputs from `contract.source` alone and never builds a control stack, so a diagnostic on an unrelated control section stopped formatting and analysis for it. It now loads once and narrows twice: `contract` and `formatter` for every project, and the control sections only on the PSL branch that actually assembles the stack. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
The real client connects from options.connection before it checks for a driver, so an operation that supplies one needs no prior connect(). The double rejected first, failing calls production accepts. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
Reworking the load-failure classification and the marker check changed load.ts branch count and dropped it under the 95% threshold. The three uncovered arms are defensive fallbacks for conditions their callees exclude: a non-Error throw, and two c12 fields that are always set on the paths that reach them. Each is annotated with its reason. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
…to s5-orm-cli-port Brings in the seven review-fix commits that landed on the base branch after this port forked from it, and reconciles them with the port's refactor. The two conflicts are the same shape: the review fix added `contract` to a `loadConfigForSections` call inside a control-API operation, while the port had removed that call entirely — operations now take `config` and `cwd` as parameters. Kept the port's parameterized form and moved the added section to the caller that now loads config: - `migrate --show` (`commands/migrate.ts`) gains `contract` - `ref set` (`commands/ref.ts`) gains `contract` Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
Every conflict is the same shape: main carries the squash-merged #29936 work, this branch carries the same work plus the port's refactor on top, so ours is strictly newer on every conflicted file. Verified that main and the #29936 branch tip are byte-identical across the CLI, config, config-loader and integration paths before resolving in favour of ours. Three things the merge could not settle on its own: - `config-loader/src/load.ts` keeps the three `/* v8 ignore */` annotations. Main does not have them, and without them `src/load.ts` drops to 94% branch coverage. Also keeps the port's `cwd` option on `loadConfig`. - `executeRefDeleteCommand` had main's `loadConfigForSections(options.config, ...)` left behind by a clean auto-merge, where `options.config` is now a loaded config object rather than a path. Dropped it; the caller loads. - Three test files kept an `ok` import that only main's config-loader mock used. The lockfile takes main's wholesale, then `pnpm install` re-adds the `@prisma/cli-engine` pin. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
Picks up main and the seven #29936 review-fix commits through the rebased first PR. No conflicts: this branch descends from the pre-merge tip of s5-orm-cli-port, so every incoming change lands on files it did not touch or touched compatibly. Audited the section lists afterwards — every `loadConfigForSections` call site on this branch matches the reviewed list from #29936, and the three newly ported read commands (`migration show`, `migration log`, `migration graph`) take the whole ORM config as one engine section, so per-section scoping does not apply to them. The lockfile is regenerated and matches s5-orm-cli-port exactly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
None of these came from the merge — every one was already failing on this branch before it. - The tarball smoke tests install packed shells into a scratch project. The repo's release-age cooldown reaches that project through the outer workspace, but its exemption list does not, so the `@prisma/cli-engine` pin this port adds to `@prisma/orm-toolchain` failed the install for the first 24 hours after the engine published. The scratch workspace now turns the cooldown off; it only ever resolves dependencies the repo already vets. - The `migration list` journey ran without per-test timeouts. It passes under `test:journeys` (which sets a long timeout) but `test:integration` picks the same files up under the 200 ms default, where every step that emits a contract or plans a migration runs out of time. Every sibling journey already passes `timeouts.typeScriptCompilation`; this one now does too. - The framework-vocabulary ratchet counted the telemetry doc comment's "collection" as a Mongo collection. Reworded. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
Two framework-vocabulary sites the ratchet was right to flag, and one it was not. `errorPathUnreachable` lives in the family-blind CLI layer but told the user about a "NOT NULL column". Every other family says that differently, so the prose now says "a required field". The SQL wording stays where it belongs, in packages/3-targets. `MigrationLogTable` is the rendered `migration log` output, not a database table — the same case as `SymbolTable`, which the allow list already carries. Allowing the compound drops the recorded count to 794 and locks that in. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
…text The nextActions conversion copied two `errorPathUnreachable` sentences verbatim, so the same wording now lives twice in one call and only the `fix` copy picked up the family-blind rewording. Both read from a constant, which also puts the branch back on the framework-vocabulary count. The `migration graph` section builder's doc comment said "column widths" for what are tree layout widths. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
|
|
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (18)
🚧 Files skipped from review as they are similar to previous changes (7)
📝 WalkthroughWalkthroughThe CLI adds migration graph, log, and show commands. It centralizes migration paths and graph formatting. Migration errors and check failures now expose typed remediation actions. Client cleanup now suppresses close failures. ChangesMigration remediation and error contracts
Migration commands and shared infrastructure
Cleanup and validation
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant CLI as ORM CLI
participant Command as migration graph/log/show
participant Loader as migration loader or control client
participant Formatter as migration formatter
CLI->>Command: invoke migration command
Command->>Loader: load graph, ledger, or migration preview
Loader-->>Command: return migration data
Command->>Formatter: render selected presentation
Formatter-->>CLI: return human, stdout, or JSON result
Possibly related PRs
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
prisma-next
@prisma/orm-extension-arktype-json
@prisma/orm-extension-middleware-cache
@prisma/orm-extension-paradedb
@prisma/orm-extension-pgvector
@prisma/orm-extension-postgis
@prisma/orm-extension-supabase
@prisma/orm-family-mongo
@prisma/orm-family-sql
@prisma/orm-framework
@prisma/orm-mongo
@prisma/orm-postgres
@prisma/orm-sqlite
@prisma/orm-target-mongo
@prisma/orm-target-postgres
@prisma/orm-target-sqlite
@prisma/orm-toolchain
commit: |
size-limit report 📦
|
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (3)
packages/1-framework/3-tooling/cli/src/utils/formatters/migration-graph-sections.ts (1)
98-107: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winConsider escaping
dirNamein the DOT label.
edge.dirNameis interpolated into a quoted DOT attribute without escaping. A directory name that contains"or\produces malformed DOT. Directory names come from disk, so a renamed directory can break the output. The truncation hazard is documented, but escaping is not addressed.♻️ Proposed hardening
+function escapeDotText(text: string): string { + return text.replace(/\\/g, '\\\\').replace(/"/g, '\\"'); +} + export function renderMigrationGraphDot(graph: MigrationGraph): string { const lines = ['digraph migrations {']; for (const edge of graph.migrationByHash.values()) { lines.push( - ` "${edge.from.slice(0, 12)}" -> "${edge.to.slice(0, 12)}" [label="${edge.dirName}"];`, + ` "${edge.from.slice(0, 12)}" -> "${edge.to.slice(0, 12)}" [label="${escapeDotText(edge.dirName)}"];`, ); } lines.push('}'); return lines.join('\n'); }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/1-framework/3-tooling/cli/src/utils/formatters/migration-graph-sections.ts` around lines 98 - 107, Update renderMigrationGraphDot to escape edge.dirName before interpolating it into the quoted DOT label, handling at least backslashes and double quotes so directory names cannot produce malformed DOT. Keep the existing graph iteration, hash truncation, and output structure unchanged.packages/1-framework/3-tooling/cli/src/utils/formatters/migrations.ts (1)
355-364: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueRename the
spaceparameter in the new public function.The parameter holds a migration, not a contract space. The name comes from the private
formatSpaceShowBlock. SincerenderMigrationShowLinesis a new public API, usemigrationto match the type name.♻️ Proposed rename
export function renderMigrationShowLines( - space: MigrationShowPresent, + migration: MigrationShowPresent, options: { readonly colorize: boolean }, ): readonly string[] { - return formatSpaceShowBlock(space, options.colorize); + return formatSpaceShowBlock(migration, options.colorize); }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/1-framework/3-tooling/cli/src/utils/formatters/migrations.ts` around lines 355 - 364, Rename the parameter of the public renderMigrationShowLines function from space to migration, and update its use in the formatSpaceShowBlock call while preserving the existing behavior.packages/1-framework/3-tooling/cli/src/orm/cli.ts (1)
71-74: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winThe engine command registry is declared twice. Both files list the same four
migration *keys mapped to the same command objects. Each new command must be added in two places, and a missed entry makes the command reachable through one entry point only.
packages/1-framework/3-tooling/cli/src/orm/cli.ts#L71-L74: consume a single shared command map instead of redeclaring the four entries inBIN_COMMANDS.packages/1-framework/3-tooling/cli/src/orm/family.ts#L17-L20: export the shared command map from one module and pass it todefineCommandFamily.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/1-framework/3-tooling/cli/src/orm/cli.ts` around lines 71 - 74, Eliminate the duplicated migration command registry by exporting one shared command map from defineCommandFamily in packages/1-framework/3-tooling/cli/src/orm/family.ts#L17-L20, then have BIN_COMMANDS consume that map in packages/1-framework/3-tooling/cli/src/orm/cli.ts#L71-L74 instead of redeclaring the four migration entries; keep all command mappings synchronized through the shared export.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/1-framework/3-tooling/cli/src/orm/migration/log.ts`:
- Around line 107-122: Guard the cleanup in the finally block around
client.close() so any rejection is handled without replacing the structured
notOk result from the catch block. Update the try/catch/finally flow using the
existing client symbol, preserving the original migration or connection error as
the returned outcome when cleanup fails.
---
Nitpick comments:
In `@packages/1-framework/3-tooling/cli/src/orm/cli.ts`:
- Around line 71-74: Eliminate the duplicated migration command registry by
exporting one shared command map from defineCommandFamily in
packages/1-framework/3-tooling/cli/src/orm/family.ts#L17-L20, then have
BIN_COMMANDS consume that map in
packages/1-framework/3-tooling/cli/src/orm/cli.ts#L71-L74 instead of redeclaring
the four migration entries; keep all command mappings synchronized through the
shared export.
In
`@packages/1-framework/3-tooling/cli/src/utils/formatters/migration-graph-sections.ts`:
- Around line 98-107: Update renderMigrationGraphDot to escape edge.dirName
before interpolating it into the quoted DOT label, handling at least backslashes
and double quotes so directory names cannot produce malformed DOT. Keep the
existing graph iteration, hash truncation, and output structure unchanged.
In `@packages/1-framework/3-tooling/cli/src/utils/formatters/migrations.ts`:
- Around line 355-364: Rename the parameter of the public
renderMigrationShowLines function from space to migration, and update its use in
the formatSpaceShowBlock call while preserving the existing behavior.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yml
Review profile: CHILL
Plan: Pro Plus
Run ID: 81019d34-d75f-42ee-a5af-fce37acac8f8
📒 Files selected for processing (31)
packages/1-framework/3-tooling/cli/src/commands/json/schemas.tspackages/1-framework/3-tooling/cli/src/commands/migration-check.tspackages/1-framework/3-tooling/cli/src/commands/migration-graph.tspackages/1-framework/3-tooling/cli/src/commands/migration-show.tspackages/1-framework/3-tooling/cli/src/control-api/operations/migration-check.tspackages/1-framework/3-tooling/cli/src/orm/cli.tspackages/1-framework/3-tooling/cli/src/orm/family.tspackages/1-framework/3-tooling/cli/src/orm/migration/graph.tspackages/1-framework/3-tooling/cli/src/orm/migration/list.tspackages/1-framework/3-tooling/cli/src/orm/migration/log.tspackages/1-framework/3-tooling/cli/src/orm/migration/paths.tspackages/1-framework/3-tooling/cli/src/orm/migration/show.tspackages/1-framework/3-tooling/cli/src/orm/normalize-error.tspackages/1-framework/3-tooling/cli/src/utils/cli-errors.tspackages/1-framework/3-tooling/cli/src/utils/formatters/migration-graph-sections.tspackages/1-framework/3-tooling/cli/src/utils/formatters/migrations.tspackages/1-framework/3-tooling/cli/src/utils/integrity-violation-to-check-failure.tspackages/1-framework/3-tooling/cli/src/utils/migration-path-target.tspackages/1-framework/3-tooling/cli/src/utils/next-actions.tspackages/1-framework/3-tooling/cli/test/cli-errors.test.tspackages/1-framework/3-tooling/cli/test/commands/migration-graph-coloured-output.test.tspackages/1-framework/3-tooling/cli/test/commands/migration-show.test.tspackages/1-framework/3-tooling/cli/test/orm/migration-graph.test.tspackages/1-framework/3-tooling/cli/test/orm/migration-list.test.tspackages/1-framework/3-tooling/cli/test/orm/migration-log.test.tspackages/1-framework/3-tooling/cli/test/orm/migration-show.test.tspackages/1-framework/3-tooling/cli/test/orm/normalize-error.test.tstest/integration/test/cli-journeys/migration-graph-dot.e2e.test.tstest/integration/test/cli-journeys/migration-log.e2e.test.tstest/integration/test/cli-journeys/migration-show-reachability.e2e.test.tstest/integration/test/utils/journey-test-helpers.ts
Ten review threads, plus one the same review pass surfaced in normalizeError. The structural ones first: - `defineOrmCommand` wraps `defineCommand` with a top-of-handler catch that runs `normalizeError`. Without it a thrown error settles as a malformed envelope: both repos export a class named `CliStructuredError`, so the engine accepts prisma/prisma's, calls its `toEnvelope()`, and emits a non-protocol `fix` with no `nextActions`. The remaining 21 commands inherit the boundary by being defined through it. - `normalizeError` now recognizes both of the repo's error shapes. `isRaisedError` required a `toEnvelope` method, which values from `structuredError()` do not have, so every one of them flattened to `CLI.UNEXPECTED` and lost its code, `why`, `fix`, `where` and `meta`. It also promised a dotted code while checking only for a string; it now uses `isStructuredError`, which holds the dotted-code pattern. A code that does not conform maps to `CLI.UNEXPECTED` and survives in `meta` rather than producing a docs link to a page that does not exist. - `runOrmCli` gained the startup failure boundary. Everything before the engine owns a run has no invocation to attach a diagnostic to, so a throw reached the user as a raw stack trace. - `migrations.dir` is finalized against the config file's directory, as `contract` already was, and `resolveMigrationPaths` resolves a relative `--config` from its `cwd` argument instead of `process.cwd()`. Together these stop `cd /tmp && prisma-next migration list --config /app/prisma-next.config.ts` reading `/tmp/migrations`. And the local ones: - `migrate --show --from <ref>` no longer requires a `driver` section. The offline plan never reads one, but the section list demanded it, so a broken driver config failed a command that does not use it. - `stripConfigFlag` rejects an empty `--config=` and a `--config` whose next token is a flag; both were consumed silently, the second swallowing the following flag as the path. - The artifact-collision check compares normalized paths, so `./out/contract.json` against a derived `out/contract.json` is caught. - Scratch projects install under the repo's release-age cooldown with its narrow first-party exemption list, rather than disabling the cooldown for every dependency. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
Carries the review fixes down the stack. Two conflicts, both additive on both sides: the bin's import block, and the normalizeError test file. `migration graph|log|show` are defined through `defineOrmCommand` now, like `migration list` — the wrapper is the whole point of the boundary, so a command that skips it keeps the defect the wrapper exists to remove. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
A rejection thrown out of a `finally` supersedes the value the
`try`/`catch` already returned. Every `finally { await client.close() }`
in the CLI therefore had a hole: if the close rejected, the mapped
error the catch had just built was discarded and the close's own error
escaped instead.
It bites hardest in the case users actually hit. A `connect()` that
fails leaves nothing to close, so the close is the most likely to
reject exactly when there is a real connection error to report — and
the user is shown an unmapped failure in place of it.
`closeQuietly` is the single hang-up, and all eleven sites use it: the
command has already decided its result, and failing to hang up cannot
change it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io> # Conflicts: # packages/1-framework/3-tooling/cli/src/orm/cli.ts # packages/1-framework/3-tooling/cli/src/orm/family.ts # packages/1-framework/3-tooling/cli/src/orm/migration/list.ts # packages/1-framework/3-tooling/cli/src/orm/normalize-error.ts # packages/1-framework/3-tooling/cli/src/utils/cli-errors.ts # packages/1-framework/3-tooling/cli/test/orm/migration-list.test.ts # packages/1-framework/3-tooling/cli/test/orm/normalize-error.test.ts # test/integration/test/utils/journey-test-helpers.ts
Repo-wide vitest runs with isolate: false, so whichever ORM test file loads src/orm/cli first fixes the control client the module tree is bound to. This file imported it at module scope, so a sibling loading first left it driving the real client — green locally, red in CI on the other ordering. Import after the reset, inside beforeAll. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
Stacked on #29970 — review that one first; this PR's diff is only the three commands and the error conversion.
What this looks like in practice
migration graph, through the engine-built binary againstexamples/prisma-8-demo. The drawing is byte-identical to the commander CLI's:An error now carries a typed action rather than a sentence:
The decision
Three more commands ported —
migration show,migration log,migration graph— following the templatemigration listset in #29970. Still additive: the commander CLI keeps working and keeps owning theprisma-nextbinary.The other half is where typed remediation gets produced. Ruled: no error type outside the CLI package learns about
nextActions. Arun-commandaction names an executable CLI invocation, which is knowledge only the CLI has — a foundation error raised by the language server or the Vite plugin has no business spelling one. So@internal/errorsand every framework package keep raisingcode/why/fixprose unchanged, and the conversion happens at the command layer:ActionableCliError(a CLI-package subclass) carries typed actions from the fourteen factories that know the invocation, andnormalizeErrorderives an action fromfixprose for everything else, so an unconverted raise site still settles as a conforming envelope.Reviewer notes
migration check --jsonchanges shape:checkFailureSchemaswapsfixfornextActions. It describes the CLI's own published output, so it converts with the rest. Recorded as a divergence.fixprose alongside the typed actions, deliberately — the commander shell still renders it, and every pre-cutover PR has to leave that CLI working.migration graph --dotno longer beats auto-JSON. Piping selects json and the DOT ridesresult.dot, somigration graph --dot | dot -Tsvgnow needs--format human. That is the ruled behavior, not an accident.migration listprinting an absolute path.Verification
pnpm build,pnpm --filter @internal/cli test(129 files, 1553 tests),pnpm typecheck,pnpm lint,pnpm lint:deps,check:error-reference,test:journeys(50 files, 166 tests),test:packages(83/83),fixtures:check(no drift). Each ported command was also run through the built binary against a real example project and diffed against the commander CLI's output.🤖 Generated with Claude Code
Summary by CodeRabbit
migration graph,migration log, andmigration showCLI commands.