From 7f0644dff035373d100c44d71cc8d525b2a98965 Mon Sep 17 00:00:00 2001 From: willbot Date: Sun, 27 Sep 2026 11:48:57 +0200 Subject: [PATCH 1/6] docs: describe credentials and sessions, bring package structure up to date Add docs/architecture/credential-manager.md: how the CLI stores credentials, which one a process acts as, how refresh and the two locks work, how the engine authenticates ctx.api and a spawned child, and the invariants a change must keep. Written from the code in packages/cli/src/auth and packages/cli-engine, not from the project's design notes. Rewrite docs/architecture/package-structure.md for the current workspace: every package and whether it publishes, the prisma wrapper and the three checks that keep its pins equal to @prisma/cli's, the engine's three entry points and what ./protocol exports, where authentication lives, and the rule for splitting agent skills per database instead of adding a carrier package. docs/oss/release-automation.md: a workflow step that must run fails when its secret is missing instead of skipping with exit 0. Signed-off-by: willbot Signed-off-by: Will Madden Co-Authored-By: Claude Opus 5.5 --- docs/README.md | 1 + docs/architecture/credential-manager.md | 206 ++++++++++++++++++++++++ docs/architecture/package-structure.md | 82 +++++++--- docs/oss/release-automation.md | 2 + 4 files changed, 265 insertions(+), 26 deletions(-) create mode 100644 docs/architecture/credential-manager.md diff --git a/docs/README.md b/docs/README.md index 586f0c36..8d39c757 100644 --- a/docs/README.md +++ b/docs/README.md @@ -25,6 +25,7 @@ For local development, continue with: - [Architecture overview](architecture/overview.md) - [Package structure](architecture/package-structure.md) +- [Credentials and sessions](architecture/credential-manager.md) - [Architecture decisions](architecture/adrs/README.md) ## Reference diff --git a/docs/architecture/credential-manager.md b/docs/architecture/credential-manager.md new file mode 100644 index 00000000..ab77b661 --- /dev/null +++ b/docs/architecture/credential-manager.md @@ -0,0 +1,206 @@ +# Credentials and Sessions + +How the CLI stores credentials, decides which one a process authenticates as, refreshes OAuth tokens, and hands a credential to a child process. The contract is `CredentialManager` in `packages/cli-engine/src/credential-manager.ts`. The CLI's implementation is `FileCredentialManager` in `packages/cli/src/auth/credential-manager.ts`, with the file format and locks in `packages/cli/src/auth/state-file.ts` and legacy-store adoption in `packages/cli/src/auth/legacy-state.ts`. The engine consumes the manager in `packages/cli-engine/src/execution/needs.ts` (the credentials check), `execution/api-client.ts` (`ctx.api`), and `execution/spawn.ts` (child credentials). + +## What the server does + +Everything here follows from how the Prisma platform issues tokens. + +- `prisma auth login` produces exactly one kind of credential: an OAuth access and refresh token pair scoped to one workspace. The user picks the workspace on the consent screen. The CLI cannot request or pin a workspace, because the authorize request carries no workspace parameter. The CLI learns which workspace it got by decoding the token's claims, and a refresh cannot change the workspace. +- A token names its workspace one of two ways: an OAuth token carries a `workspace_id` claim, and a service token carries `sub: "workspace:"` and no `workspace_id`. `credentialWorkspaceId` in `packages/cli-engine/src/token-claims.ts` is the one derivation for both. Claims are decoded, never verified: they are used for display and for keying, never for authorizing. +- Refresh tokens are single-use with a ten-second reuse grace: rotation marks the token used, one replay within ten seconds succeeds and issues its own pair, and later replays answer `invalid_grant`. Rotation does not revoke sibling pairs, so any pair that was successfully issued stays valid on its own. Two processes refreshing the same session therefore both end up with a working pair, whichever write lands last. +- `PRISMA_SERVICE_TOKEN` supplies a workspace-scoped bearer token from the environment. It carries no refresh token, so nothing rotates it, and it is never written to disk. +- Tokens carry identity claims (`sub`, `email`) but the stored state enforces no identity. One store may hold sessions minted by different accounts; identity is decoded for display only. + +The endpoints are `PRISMA_MANAGEMENT_API_URL` (default `https://api.prisma.io`) and `PRISMA_AUTH_BASE_URL` (default `https://auth.prisma.io`), read in `packages/cli/src/auth/client.ts` beside the OAuth client id. + +## Three things, kept separate + +A **session** is a stored logged-in state for one workspace: `{ workspaceId, workspaceName, expiresAt }`. Sessions are keyed by workspace id, so there is at most one per workspace, and logging in to the same workspace again replaces its credential. `expiresAt` is the stored access token's expiry, which rotation moves; it is not a deadline on the session. Sessions are what `sessions()` lists, `selectSession` selects, and `endSession` ends. + +The **selection** is one scalar of stored state: the workspace whose session is used when a session is needed. `sessions()` returns it together with the list as `StoredSessions { sessions, selectedWorkspaceId }`, in one read, because reads take no lock and two reads could straddle a write. The manager guarantees `selectedWorkspaceId` either names a listed session or is absent; a dangling selection never leaves the manager. + +The **active credential** is what this process authenticates as: `{ workspaceId, workspaceName, expiresAt, identity, origin }`. It carries no token material. `origin.source` is `"stored"` or `"environment"`; it exists to be printed (`whoami`'s `source` field) and to pick the wording of one error, and comparing against it anywhere else is a defect. `workspaceId` is absent, never the empty string, when an environment token's claims name no workspace. + +Vocabulary: in code the word is *selected* (`selectedWorkspaceId`, `selectSession`). Two places keep *current* and are not to be renamed: the on-disk field `currentWorkspaceId`, and the user-facing surface (`auth workspace use`, and `auth workspace list`'s `context.currentWorkspaceId` and per-item `current`). + +## The interface + +```ts +interface CredentialManager { + activeCredential(): Promise; + sessions(): Promise; + createSession(credential: Credential, workspaceId: string): Promise; + selectSession(workspaceId: string): Promise; + endSession(workspaceId: string): Promise; + endAllSessions(): Promise; + activeCredentialStorage(): Promise; + activeAccessToken(options: ActiveAccessTokenOptions): Promise; +} +``` + +The first six are what the auth commands call. The last two are engine-facing: `activeCredentialStorage()` is the SDK `TokenStorage` the engine forwards into the API client, and `activeAccessToken()` is the one way the engine reads token material, for a child process. + +Rules every implementation follows: + +- The manager never opens a browser, never prompts, and never talks to the user. Login mints a credential elsewhere and hands it to `createSession`. +- `env` is a construction input. Nothing below the manager reads `process.env`. +- The manager resolves no user input. Commands resolve a typed workspace reference against `sessions()` (exact id first, then case-insensitive name) in `packages/cli/src/commands/auth/session-ref.ts` and pass the matched workspace id. A reference that matches nothing is `AUTH.NO_SESSION_FOR_WORKSPACE`; several matches are `AUTH.WORKSPACE_AMBIGUOUS`. +- `createSession` refuses a credential whose workspace claim names a different workspace than the one it is being stored under (`AUTH.CREDENTIAL_WORKSPACE_MISMATCH`). +- `selectSession` refuses a workspace with no session; there is no state in which it would afterwards be selected. +- `endSession` is idempotent: a workspace with no session is already in the requested state, so it writes nothing and succeeds. The mistyped-reference error is raised command-side, before the manager is reached. +- Ending the selected session clears the selection; nothing is auto-promoted. + +Three implementations exist. `FileCredentialManager` (`packages/cli/src/auth/credential-manager.ts`) is the CLI's, over the state file described next. `EnvironmentCredentialManager` (`packages/cli-engine/src/environment-credential-manager.ts`) ships in the engine for hosts whose only credential source is the environment pair `PRISMA_SERVICE_TOKEN` and `PRISMA_WORKSPACE_ID`, such as a child process this CLI spawned; it holds no sessions and every mutation throws `AUTH.SESSIONS_UNSUPPORTED`. `InMemoryCredentialManager` (the engine's `./testing` subpath) implements the same rules over memory, with a seed and a state read-back for tests. + +## What is stored where + +The state file is `PRISMA_AUTH_FILE` when set; `PRISMA_COMPUTE_AUTH_FILE` is a deprecated alias the bin warns about once per process. Otherwise it is `~/Library/Application Support/prisma/auth.json` on macOS, `%APPDATA%\prisma\auth.json` on Windows, and `$XDG_CONFIG_HOME/prisma/auth.json` (default `~/.config`) elsewhere. + +The file is JSON, mode 0600: + +```json +{ + "version": 1, + "sessions": [ + { + "workspaceId": "...", + "name": "...", + "user": { "id": "...", "email": "...", "name": "..." }, + "token": "...", + "refreshToken": "...", + "expiresAt": "..." + } + ], + "currentWorkspaceId": "... or null", + "tokens": [{ "workspaceId": "...", "token": "...", "refreshToken": "..." }] +} +``` + +`name`, `user`, `refreshToken`, and `expiresAt` are optional. `user` is safe account metadata captured at login for labelling sessions; the token stays the only thing that authenticates. `tokens` is a mirror of the sessions in the record shape the 3.x `@prisma/cli` reads, so a 3.x CLI sharing this file keeps seeing the sessions; this CLI's reader branches on `sessions` before it looks at `tokens`, so the mirror is invisible to it. For the same reason every write keeps the `activeWorkspaceId` pointer in the sibling `auth.context.json` in step with `currentWorkspaceId`, and `endAllSessions` deletes that sidecar. + +A file with `tokens` but no `sessions` is a legacy store and is adopted on read (`legacy-state.ts`): every entry whose token decodes to a workspace becomes a session keyed by that workspace, the last of several entries for one workspace wins, a legacy name equal to `Unknown workspace` or to the workspace id adopts as no name, entries without a refresh token adopt, and the selection comes from the context sidecar's pointer when the sidecar exists (a dangling or null pointer means nothing selected) or from the single entry when there is no sidecar and exactly one entry. Adoption is a pure read. The first mutation writes the adopted set in the current shape; it re-reads (and so re-adopts) inside the lock, so a current-shape state another process wrote meanwhile wins. + +A file that is missing reads as empty. A file that exists but cannot be read is `CLI.CREDENTIALS_UNREADABLE`. A file that parses to nothing usable reads as signed out and is never rewritten by a read; the next login replaces it. + +Writes are atomic: a temp file in the same directory, opened exclusively with mode 0600, written, fsynced, and renamed over the state file, after which the file's mode is tightened to 0600. A write that fails before the rename unlinks its temp file, because the temp file holds the whole state, tokens included. `endAllSessions` also deletes any temp files a crashed write left behind. Reads never write and take no lock: the rename guarantees a reader sees a complete state. + +## Which credential a process acts as + +A process decides once, at its first `activeCredential()` call, and the decision holds for the process's lifetime: + +1. If `PRISMA_SERVICE_TOKEN` is set, the process acts as the environment credential. A blank or whitespace value is `AUTH.SERVICE_TOKEN_EMPTY`, raised identically from every read and every mutation while the variable is set. +2. Otherwise, if the file's `currentWorkspaceId` names a stored session, the process acts as that session. +3. Otherwise the process acts as nothing. `activeCredential()` returns `null` when no sessions are stored and throws `CLI.CREDENTIALS_REQUIRED` with the "sessions held, none selected" wording when sessions exist but none is selected. + +Another process moving the selection or replacing records does not redirect a running process; a new process picks up the new selection. The process's own mutations do move it: `createSession` and `selectSession` make the process act as that session, and `endSession` of the session it acts as and `endAllSessions` make it act as nothing. Each of those discards the storage built for the previous decision, so a command that mutates and then reaches for `ctx.api` gets the credential it now acts as. + +What is pinned is the decision, not the material. Every read goes back to the file, so a session another process replaced still resolves, and a session another process ended fails at the next read with `CLI.CREDENTIALS_REQUIRED` in its "session ended" wording. + +While `PRISMA_SERVICE_TOKEN` is set, every mutation still succeeds: selecting or ending a stored session changes stored state while this process keeps authenticating as the environment credential. The commands print a one-line notice that the environment credential remains in force until the variable is unset. + +## How the engine authenticates a command + +A command declares `needs: { credentials: true }`. Before the handler runs, the engine's needs check calls `activeCredential()`; `null` is `CLI.CREDENTIALS_REQUIRED`, and a structured error from the manager passes through verbatim. `ctx.activeCredential()` and the first `ctx.api` call go through the same method, so all three raise the same error for the same state. `auth whoami` declares no credentials need: it calls `ctx.activeCredential()` itself and reports signed out. + +`ctx.api` is the management API client, and the engine constructs and owns it: one client for the active credential, built lazily on the first method call and memoized for the run (`execution/api-client.ts`). It is always the SDK's refreshing client, `createManagementApiSdk({ ...managementApiClientConfig, tokenStorage })`, over the storage `activeCredentialStorage()` returns, whatever the credential's origin: a credential refreshes if it has a refresh token, and nothing hard-codes "environment means never refresh". The bin injects `Runtime.managementApiClientConfig` (`clientId`, `redirectUri`, `apiBaseUrl`, `authBaseUrl`; all four, because the SDK's refreshing fetch requires the full config) beside `Runtime.credentialManager`. The engine forwards the storage into the SDK and does not drive it; its one direct read is `getTokens()` during failure mapping, to ask whether the token set that failed had a refresh token at all. + +`activeCredentialStorage()` returns one of two storages, chosen when the decision resolves: + +- **File-backed**, for a stored session. `getTokens` re-reads the file on every call, with no memory layer in front. That read-through is what lets the SDK recover when another process already rotated: this process sees the newer pair, skips the exchange, and retries. Writes take the state lock. +- **Memory-backed**, for the environment credential. It closes over one variable, is never given the file's path, and touches no file from any method, including `clearTokens`, so an environment credential whose workspace matches a stored session cannot delete that session. The SDK's `Tokens` requires a `workspaceId`; when the claims name none, the storage supplies a fixed placeholder that never leaves the manager. + +A command that declares `managesCredentials: true` also gets `ctx.credentialManager`. Exactly five commands do: `auth login`, `auth logout`, `auth workspace list`, `auth workspace use`, and `auth workspace logout`. + +## Refresh + +The SDK drives refresh on a 401. It calls the storage's `withRefreshLock`, re-reads the tokens inside it, skips the exchange when they no longer match the pair that failed, otherwise posts the refresh token to the token endpoint, writes the rotated pair with `setTokens`, and retries the request. The file-backed storage's `withRefreshLock` holds two locks: an in-process promise chain, so callers in one process run one at a time, and a cross-process file lock (`.refresh-lock`) held across the whole read, exchange, and write, so two processes never spend the same refresh token. The refresh lock is separate from the state lock because it is held across network I/O. + +The file-backed storage's write rules: + +- `setTokens` updates only `token`, `refreshToken`, and `expiresAt` of its workspace's record, in place. It never creates a record, never moves the selection, and never touches `name` or `user`. If the record is gone (ended by another process) it throws `CLI.CREDENTIALS_REQUIRED` rather than resurrect it. If the rotated token's workspace claim disagrees with the record's workspace it throws `AUTH.CREDENTIAL_WORKSPACE_MISMATCH`. The expiry comes from the new token's `exp` claim, else the expiry the caller passed, else the record's existing one, so an SDK-driven rotation (which passes no expiry) does not erase one a proactive refresh stored. +- `clearTokensIfCurrent` removes the record only if its stored `workspaceId`, `accessToken`, and `refreshToken` all still equal the pair that failed. This exact match is what makes a stale replay's `invalid_grant` harmless when a newer pair is already stored. Do not simplify it. +- `clearTokens` removes the record this process acts as and clears the selection if it named it. It never means "end all sessions". + +The engine maps a request failure by state, never by parsing messages (`mapRequestFailure` in `execution/api-client.ts`): + +- A `CliStructuredError` anywhere in the cause chain (the SDK wraps non-SDK errors in `FetchError`) surfaces as itself. This is how a manager error thrown from `setTokens` reaches the user. +- An SDK `AuthError` with `refreshTokenInvalid === true` (the token endpoint answered `invalid_grant`, and the SDK already ran `clearTokensIfCurrent`) is `CLI.CREDENTIALS_REQUIRED` in its "expired" wording for a stored session, and `AUTH.SERVICE_TOKEN_REJECTED` for the environment credential. +- A refresh the SDK refused because the token set carries no refresh token is a credential that could never have been renewed: `AUTH.SERVICE_TOKEN_REJECTED` for the environment credential, "expired" for a stored session without a refresh token (a legacy entry adopted without one). +- Any other `AuthError` on a stored session re-reads `sessions()`: the session gone is "session ended", otherwise `CLI.AUTH_SERVICE_ERROR`, a transient failure of the auth service that cleared nothing. +- A failure from the refresh path that is not an `AuthError` (the SDK throws a plain `Error` when a rotated token will not decode) is `CLI.AUTH_SERVICE_ERROR` too. + +There is no background or pre-emptive refresh for ordinary commands; per-request refresh keeps long runs current. The one proactive refresh is for child processes, below. + +## Locks and mutations + +Every mutation is one `#mutate` call: acquire the state lock, re-read the file, apply one slice, write atomically if the slice produced a new state, release. No mutation writes state it read before acquiring the lock, and no network I/O runs under the state lock. + +| Mutation | May modify | +| --- | --- | +| `createSession` | one record (upsert) and the selection; a second locked write attaches `name` and `user` | +| `selectSession` | the selection | +| `endSession` | one record, and the selection if it named it | +| `endAllSessions` | the whole state | +| `setTokens` | the token fields of the acting record | +| `clearTokens`, `clearTokensIfCurrent` | the acting record, and the selection if it named it | +| `enrichSessions` | `name` and `user` of records that lacked them | + +The state lock is a lock file `.lock`, created exclusively and holding a random id. It is retried every 10 ms, a holder older than 5 s is treated as crashed, and a waiter gives up after 10 s with `CLI.CREDENTIALS_LOCKED`. A stale lock is taken over by renaming it aside and then confirming the moved file's mtime matches what was examined, so two waiters cannot both believe they cleared it and one cannot rename away a lock the other just created. The refresh lock uses the same mechanism at `.refresh-lock` with network-sized budgets: 100 ms retry, 30 s stale, 30 s wait. + +## Login + +`performLogin` (`packages/cli/src/auth/operations.ts`) runs the browser consent flow in `login.ts`: it starts a callback server on an ephemeral localhost port, opens the authorize URL in the browser, and when stdin is a TTY also accepts the callback URL pasted into the terminal. The SDK persists the minted tokens at callback time through a throwaway in-memory `TokenStorage`, never through the manager, and `performLogin` returns them as a `Credential { token, refreshToken, expiresAt }`. Minting and custody stay separate. + +The `auth login` command reads the workspace from the credential's claims (`AUTH.LOGIN_WORKSPACE_UNKNOWN` if it names none) and calls `createSession(credential, workspaceId)`. The manager upserts the record and selects it under the lock, then, outside the lock, calls the injected `fetchSessionMetadata` (`session-metadata.ts`: one `GET /v1/me` with a 3 s timeout) for the workspace name and safe account fields, and attaches them in a second locked write only if the record still holds this exact token. A failed lookup leaves them absent and never fails login. + +Names and users are otherwise never refreshed from the network: `sessionsForDisplay` (`enrichSessions`) fills in a missing name or user when a listing command runs, and never replaces one already stored. A renamed workspace keeps its stored name until the next login to it. + +## Handing a credential to a child + +A command that spawns a program which must authenticate as this process declares `maySpawn` and `needs: { credentials: "child" }`. The child gets an access-token snapshot it cannot refresh, so the engine makes sure the snapshot will last: + +1. In the needs check, before the handler, `activeAccessToken({ minimumValidityMs: 5 minutes })`. A stored OAuth pair with less than five minutes left is refreshed under the refresh lock through `Runtime`'s injected `CredentialRefresher` (`packages/cli/src/auth/refresh.ts`, a POST to `${authBaseUrl}/token` with a 10 s timeout); the rotation is persisted before the new token is judged, and the new access token is returned. `invalid_grant` clears the current pair and is "expired"; a credential with no refresh token inside the window is refused with the "expiring soon" wording. This runs before the handler because pre-spawn work may create platform resources. +2. At `ctx.spawn`, `activeAccessToken({ minimumValidityMs: 0 })` reads the token again, so a rotation by another process in between is observed and only an already-expired token is refused. +3. The child's environment gets `PRISMA_SERVICE_TOKEN` set to that access token and `PRISMA_WORKSPACE_ID` set to the credential's workspace id, or deleted when the credential names none: the two variables are one protocol, written as a unit. They are applied last and cannot be overridden by the handler's `env`. The refresh token is never injected. + +In the child, `EnvironmentCredentialManager` reads that pair: the token's own claims name the workspace when they can, and `PRISMA_WORKSPACE_ID` fills in when they do not. (The CLI's `FileCredentialManager` reads only the token's claims for the environment credential's workspace.) + +The spawn-time read builds no second API client, so the engine's `ctx.api` client stays the only refreshing client the process constructs. + +## The auth commands + +| Command | Manager calls | +| --- | --- | +| `auth login` | `performLogin`, then `createSession(credential, workspaceId)` | +| `auth logout` | `sessions()` for the count, then `endAllSessions()` | +| `auth whoami` | `ctx.activeCredential()`, then a best-effort `GET /v1/me` through `ctx.api` (3 s timeout) whose fields win over the claims when both describe the same user | +| `auth workspace list` | `sessions()` through `sessionsForDisplay`, the selection marked `current` | +| `auth workspace use ` | resolve the ref command-side, then `selectSession(workspaceId)`; it selects among the sessions you have and never opens a browser | +| `auth workspace logout ` | resolve the ref command-side, then `endSession(workspaceId)` | + +## Debugging + +`PRISMA_DEBUG=1` makes the manager write to stderr: the resolved state file path, the acting-as decision, lock acquire, release, and takeover, and rotation and clear writes. The engine adds the client's refresh attempts and, on failure, the `AuthError`'s verdict without the endpoint's free text. Token material never appears in any log line, error, `meta`, or envelope. + +## Invariants a change must not break + +- Sessions are keyed by workspace id: one session per workspace, and `createSession` upserts. +- `selectedWorkspaceId` names a listed session or is absent. +- `Session` and `ActiveCredential` carry no token material; only the engine sees tokens, and only through `activeCredentialStorage()` and `activeAccessToken()`. +- The acting-as decision is made once per process and moved only by the process's own mutations; the material is read fresh on every call. +- Reads never write and take no lock. Every mutation re-reads under the state lock and writes one slice atomically. No network I/O runs under the state lock. +- The file-backed `getTokens` has no cache in front of it. +- `setTokens` never creates a record, never moves the selection, never re-scopes a session to another workspace, and never resurrects an ended one. +- `clearTokensIfCurrent` matches all three fields exactly. +- The memory-backed storage never touches the file. +- The environment credential is never stored and never refreshed; while it is in force, mutations change stored state but not what the process acts as. +- A child receives an access token and a workspace id, never a refresh token, and the parent refreshes a near-expiry pair before the handler runs. +- Refresh failures are classified by state (`refreshTokenInvalid`, whether the set had a refresh token, whether the session still exists), never by message text. +- `ctx.api` is one client per process, built by the engine over the manager's storage; the spawn path builds no second one. +- Login writes nothing through the manager until `createSession`. +- No token material in output, errors, `meta`, envelopes, or debug logs. + +## A legacy module still in use + +`packages/cli/src/auth/token-storage.ts` (`FileTokenStorage`) is the store from before the session model. `project transfer` still uses it through `recipient.ts` to validate the recipient workspace's stored session with its own SDK client pinned to that workspace; it is the one command that authenticates API requests with a stored session other than the active one, outside `ctx.api`. `guard.ts` and the rest of `operations.ts` beyond `performLogin` are reached only by tests. diff --git a/docs/architecture/package-structure.md b/docs/architecture/package-structure.md index 25b5bba8..cd731ae0 100644 --- a/docs/architecture/package-structure.md +++ b/docs/architecture/package-structure.md @@ -1,47 +1,77 @@ # Package Structure -The repository currently contains two publishable packages: +The repository is a pnpm workspace. The root owns shared scripts, docs, release preparation, and the conformance checks. -- `packages/cli`: the public Prisma CLI beta package -- `packages/compute`: runtime utilities for deployed Prisma compute applications +| Directory | Package | Published | What it is | +| --- | --- | --- | --- | +| `packages/cli-engine` | `@prisma/cli-engine` | yes, on its own version line | The execution engine: argv to exit code, needs checks, credentials, rendering, errors | +| `packages/cli` | `@prisma/cli` | yes, `prisma-cli` binary | The CLI shell: mounts every command family and assembles the engine's runtime | +| `packages/prisma` | `prisma` | yes, `prisma` binary | A wrapper that publishes the same shell under the unscoped name | +| `packages/compute` | `@prisma/compute` | yes, on its own version line and workflow | Runtime utilities for applications deployed to Prisma Compute; pending extraction to another repository | +| `packages/cli-telemetry` | `@repo/cli-telemetry` | no | The telemetry sender, bundled into both bins | +| `packages/cli-conformance` | `@repo/cli-conformance` | no | The conformance checks the publish workflow runs on the built output and packed tarballs | +| `packages/tsconfig` | `@repo/tsconfig` | no | The shared TypeScript base config | -The root workspace owns shared scripts, docs, release preparation, and examples. +[Versioning](../oss/versioning.md) explains which packages version in lockstep and which do not. + +## The engine + +`@prisma/cli-engine` is one library package. Product CLI packages (`@prisma/composer-cli`, `@prisma/orm-toolchain`) build their command families against it and declare it as an exact peer, so one install holds one engine ([ADR 0004](adrs/0004-engine-version-pinning.md)). It has three entry points: + +- `@prisma/cli-engine`: command definitions, flag and positional builders, the context and runtime types, the credential manager contract, and `createCli`. +- `@prisma/cli-engine/protocol`: the shapes that cross package and process boundaries, for consumers that need them without the engine's runtime. It is a small runtime module, not types only: it exports `CliStructuredError`, `STRUCTURED_ERROR`, and the result constructors `ok`, `notOk`, and `okVoid`, alongside the `Diagnostic`, `NextAction`, `CliErrorEnvelope`, `Ok`, `NotOk`, and `Result` types. Products import `ok`, `notOk`, and `CliStructuredError` from it. +- `@prisma/cli-engine/testing`: the in-process test harness, the in-memory credential manager, and the JWT minter that seeds it. + +Authentication (token storage, refresh, the login flow) lives in this repository's CLI package (`packages/cli/src/auth`), separate from any Prisma Cloud product code. The engine defines the `CredentialManager` contract and consumes it; the CLI supplies the implementation. See [Credentials and sessions](credential-manager.md). + +## The two bins + +`@prisma/cli` and `prisma` ship the same shell. `packages/prisma/src/bin.ts` imports `@prisma/cli/src/bin`, and its build bundles `@prisma/cli`, `@repo/cli-telemetry`, and `@prisma/credentials-store` into `dist/prisma.js`, so a command cannot behave differently depending on which package a user installed. The wrapper also exports `prisma/config`, which re-exports `definePrismaConfig` from the engine so a user's `prisma.config.ts` never depends on `@prisma/cli-engine` directly, and it ships the `prisma-platform-core-concepts` agent skill in its `skills/` directory, staged from the repository root `skills/` tree at `prepack` by `scripts/stage-skills.mjs`. + +The wrapper's `package.json` carries its own copy of the runtime dependencies, including the product version pins on `@prisma/composer-cli` and `@prisma/orm-toolchain` and the `workspace:` pin on the engine. A user's `npm install prisma` resolves from the wrapper's copy, so the two manifests must declare the same dependencies at the same versions. Three checks keep them equal to `packages/cli`'s: `packages/cli/tests/manifest-pins.test.ts` asserts the wrapper's `dependencies` deep-equal the shell's; the conformance tarball check (`packages/cli-conformance/src/checks/tarball.ts`) fails on any dependency two packed manifests pin differently and on any engine pin that differs from the engine version packed beside it, then installs each packed bin into its own sandbox and starts it; and `scripts/update-product-versions.mjs` and `scripts/bump-cli-engine-version.ts` rewrite both manifests in one step so a pin cannot move in one without the other. + +## Agent skills + +A skill's content only ever comes from the packages named in `packages/cli/src/lib/skills/allowlist.ts`; `prisma skills sync` never scans `node_modules`. Skills travel inside the tarball of the package they describe, so an installed skill matches the installed package version by construction. + +When a skill's content has to differ per database, split it into separately named skills, one per ORM facade package. Never add a carrier package that ships skills on another package's behalf: a transitive carrier package cannot be resolved from the project root under pnpm, and a direct-dependency skills package would break the guarantee that installed skills match the installed package versions. Every ORM facade ships an identical `prisma-8` skill and a version conflict between facades resolves to the highest version, which is only safe while the content is identical and the versions move together. The allowlist grows by one deliberate line per facade either way. ## CLI Source Layout -- `src/bin.ts`: process entrypoint for the `prisma-cli` binary. -- `src/main.ts`: builds the CLI and hands the engine a runtime. -- `src/cli.ts`: mounts every command and command family. -- `src/commands//*`: one file per command — flags, help, handler. -- `src/controllers/*` and `src/presenters/*`: the operation layer the handlers - call, and the serializers they reuse. -- `src/auth/*`: sessions, credentials, and token storage. -- `src/adapters/*`: local state and git. -- `src/lib/*`: feature-specific helpers and client code. -- `src/legacy/*`: the context shape the operation layer still takes. -- `src/types/*`: shared CLI data shapes. +`packages/cli/src/`: + +- `bin.ts`: process entrypoint for the `prisma-cli` binary; the `prisma` bin imports it. +- `main.ts`: builds the CLI, runs the cached update notice, hands the engine a runtime, and after the command runs reports out-of-date agent skills. +- `cli.ts`: mounts every command and command family, including composer's and the ORM toolchain's. +- `runtime.ts`: assembles the engine `Runtime` from `process`: streams, the credential manager, the API client config, the spawn and package-manager adapters, and the telemetry sender. +- `cli-name.ts`, `cli-command.ts`, `command-arguments.ts`, `shell-command.ts`: the CLI's user-facing name and how it renders commands for a user to paste. +- `commands//*`: one file per command: flags, help, handler. +- `controllers/*` and `presenters/*`: the operation layer the handlers call, and the serializers they reuse. +- `auth/*`: sessions, credentials, token storage, and the login flow. +- `adapters/*`: local state and git. +- `lib/*`: feature-specific helpers and client code, including the skills allowlist and sync. +- `output/*`: shared presentation patterns. +- `types/*`: shared CLI data shapes. +- `spawn.ts`, `package-manager-runner.ts`: the `node:child_process` adapters the engine's spawn and package-operation seams are wired to. +- `skills-check.ts`, `update-check.ts`, `state-dir.ts`: the post-command skills staleness notice, the update notice, and local state directory resolution. ## Layering Rules - Product behavior starts in `docs/product`, then code follows. -- Command modules may parse inputs and register help, but should not own - resource resolution or side effects. +- Command modules may parse inputs and register help, but should not own resource resolution or side effects. - The operation layer should not write directly to terminal streams. - Presenters should not perform filesystem, network, or state mutations. -- Adapter and client modules should keep external boundaries behind small, - testable interfaces. -- Output flows through the engine: handlers describe presentation blocks, so - human and JSON behavior stay consistent. +- Adapter and client modules should keep external boundaries behind small, testable interfaces. +- Output flows through the engine: handlers describe presentation blocks, so human, JSON, and markdown behavior stay consistent. ## Tests -Tests live in `packages/cli/tests`. +Unit tests live in `packages/cli/tests`; end-to-end tests against the real management API live in `packages/cli/e2e`. - Use in-process CLI tests for command behavior, output, prompts, and errors. -- Use operation-layer tests when a behavior can be exercised without going - through the engine. +- Use operation-layer tests when a behavior can be exercised without going through the engine. - Use package metadata and tarball-content checks for publishing changes. -- Add subprocess or package smoke tests when changing packaging, entrypoints, or - binary behavior. +- Add subprocess or package smoke tests when changing packaging, entrypoints, or binary behavior. +- Every mounted command needs a happy-path end-to-end test declared with `describeCommand`; `packages/cli/tests/e2e-coverage.test.ts` fails the build for a command without one. The repository `AGENTS.md` states the rule. See [testing patterns](../reference/testing-patterns.md) for more detail. diff --git a/docs/oss/release-automation.md b/docs/oss/release-automation.md index ae4f9aec..fb494655 100644 --- a/docs/oss/release-automation.md +++ b/docs/oss/release-automation.md @@ -62,6 +62,8 @@ A step in the publish workflow, immediately after its publish step and keyed on The payload is informational — this repository always re-reads the registry rather than trusting the event, so a malformed or replayed event cannot pin a version that does not exist. +A workflow step that must run should fail when its secret or token is missing, not skip with exit 0. A step that skips silently when its secret is missing can stay broken indefinitely without anyone noticing: the ORM repository's notify step skipped on every release because it read a secret name that was never configured, and nothing reported it. Check that the secret is set before the step that uses it, and make that check fail the job. + ## When it stops working | Symptom | Cause | Fix | From e4f4d09f7f24ae107129f48a5de59e5436227cc9 Mon Sep 17 00:00:00 2001 From: willbot Date: Sun, 27 Sep 2026 11:48:58 +0200 Subject: [PATCH 2/6] Point the markdown tests at the output conventions doc; stop ignoring project review paths The two engine markdown tests cited a spec file under .drive/projects/prisma-cli-v8 that never existed; the --format markdown section of docs/product/output-conventions.md is where that format is defined. The .gitignore entries for that project's review directories go with the project. Signed-off-by: willbot Signed-off-by: Will Madden Co-Authored-By: Claude Opus 5.5 --- .gitignore | 4 +--- packages/cli-engine/tests/help-markdown.test.ts | 4 ++-- packages/cli-engine/tests/markdown.test.ts | 4 ++-- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/.gitignore b/.gitignore index 81106b38..e216be1f 100644 --- a/.gitignore +++ b/.gitignore @@ -35,9 +35,7 @@ npm-debug.log* # Turborepo cache .turbo/ -# Review artifacts — never committed -.drive/projects/prisma-cli-v8/reviews/ -.drive/projects/prisma-cli-v8/specs/reviews/ +# Working files — never committed wip/ # Reference clones of other repos, read while working. Ignored here rather diff --git a/packages/cli-engine/tests/help-markdown.test.ts b/packages/cli-engine/tests/help-markdown.test.ts index f569a938..4083548a 100644 --- a/packages/cli-engine/tests/help-markdown.test.ts +++ b/packages/cli-engine/tests/help-markdown.test.ts @@ -1,6 +1,6 @@ /** - * Help under `--format markdown`, byte for byte, per the slice spec's - * help shape (.drive/projects/prisma-cli-v8/specs/markdown-format.md): + * Help under `--format markdown`, byte for byte, per the help shape in + * docs/product/output-conventions.md (section "`--format markdown`"): * everything on stdout, stderr empty, colour off. */ import { describe, expect, test } from "vitest"; diff --git a/packages/cli-engine/tests/markdown.test.ts b/packages/cli-engine/tests/markdown.test.ts index bdc808f5..dda77bea 100644 --- a/packages/cli-engine/tests/markdown.test.ts +++ b/packages/cli-engine/tests/markdown.test.ts @@ -1,8 +1,8 @@ /** * `--format markdown`: the same blocks a command describes for human, * rendered as plain Markdown on stdout for a reader that is a model. - * Every byte here is pinned by the slice spec - * (.drive/projects/prisma-cli-v8/specs/markdown-format.md). + * Every byte here is pinned by docs/product/output-conventions.md + * (section "`--format markdown`"). */ import { type Block, From 933a10f9d341d1570bafed901c1bd62da1775f21 Mon Sep 17 00:00:00 2001 From: willbot Date: Sun, 27 Sep 2026 11:50:06 +0200 Subject: [PATCH 3/6] Land the prisma-cli-v8 retro in the repo; list packages/prisma in the lockstep set .drive/HEALTH.md records the two lessons from the project's final retro: close out the deferred ledger at every slice merge, and re-read the acceptance criteria when a slice changes the design. versioning.md omitted the prisma wrapper package from the lockstep list, although it carries the lockstep version. Co-Authored-By: Claude Opus 5.5 Signed-off-by: willbot Signed-off-by: Will Madden --- .drive/HEALTH.md | 22 ++++++++++++++++++++++ docs/oss/versioning.md | 2 +- 2 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 .drive/HEALTH.md diff --git a/.drive/HEALTH.md b/.drive/HEALTH.md new file mode 100644 index 00000000..21a2663d --- /dev/null +++ b/.drive/HEALTH.md @@ -0,0 +1,22 @@ +# Health checks — prisma-cli + +Repository rules for Drive project health checks. They apply at every slice merge, in addition to the standard checks. + +## Close out the deferred ledger as you go + +At each slice merge, go through `.drive/projects//deferred.md`. Every entry the slice touched, and every entry that has survived one slice, leaves the ledger in one of four ways: + +- **Done:** delete it. +- **Moot:** delete it, with the reason in the commit message. +- **Still real:** file a GitHub issue in the repository that owns the fix, and replace the entry with the link. +- **Needs a ruling:** ask the operator now. When the ruling is made, write it into the document it governs (`docs/`, an ADR, `AGENTS.md`), not only into the ledger. + +The ledger holds only entries that are waiting on something named, such as a release or a ruling that has been requested. + +Why: the prisma-cli-v8 project closed on 2026-09-27 with an 81-entry ledger that nothing had revisited in six weeks. 40 entries were already done or moot. One ruling (remove the `npx skills add` copy button from the login page, made 2026-08-24) was never carried out, and another (split per-database agent skills by name) existed only in the ledger, so deleting the project would have deleted it. + +## Re-read the acceptance criteria when the design changes + +When a slice changes the design, re-read the project's acceptance criteria and amend any the change invalidates, with the operator, in that slice. + +Why: at the prisma-cli-v8 close-out, two of eight criteria could no longer be met as written. One asked for a config file with sections for three product families, but the design never gave the Cloud family a config section. Another asked for operator sign-off on per-family parity lists, which lost their purpose when the legacy CLIs were retired. Both had been wrong for weeks and surfaced only at close. diff --git a/docs/oss/versioning.md b/docs/oss/versioning.md index bdc38294..d63bfc75 100644 --- a/docs/oss/versioning.md +++ b/docs/oss/versioning.md @@ -20,7 +20,7 @@ Every lockstep workspace package — publishable, private, and the workspace roo **Exceptions:** `@prisma/compute` versions independently, pending extraction to another repository (operator ruling 2026-08-10), and keeps its own publish workflow ([`publish-compute.yml`](../../.github/workflows/publish-compute.yml)). `@prisma/cli-engine` also versions independently ([ADR 0004](../architecture/adrs/0004-engine-version-pinning.md), operator ruling 2026-08-13): an engine version means "the engine changed", not "the CLI released", which is what keeps the exact peer pins the product CLI packages hold on it cheap — they change only when the engine actually moves. The engine follows honest pre-1.0 semver (a breaking change bumps the minor); bumping it is one command — `pnpm bump-cli-engine-version ` — which edits `packages/cli-engine/package.json`, the `workspace:` pin in every consumer manifest (`packages/cli`, `packages/prisma`), and the lockfile together, landed as a reviewed commit like any other version change (run it in the PR that changes the engine). Both packages are hard-excluded in [`scripts/set-version.ts`](../../scripts/set-version.ts), which still sweeps their `workspace:` pins on lockstep siblings so those never go stale. At publish time the engine ships at its own manifest version **under `latest`**, on the first publish run that carries the bumped version — normally the merge of the PR that bumped it. That is the same rule every other package follows — a deliberately merged version-bump PR is what publishes a version under `latest` — applied at the engine's own bump instead of the root's. An already-published engine version is a no-op. The engine has no dev builds, for two reasons. First, the product CLI packages peer it at an exact release version, and so do their dev builds, so a dev-stamped engine would satisfy none of them and `npm install prisma@dev` would fail. Second, a dev build would expose nothing: CI refuses any change to the engine's published contents that does not bump its version, and that merge publishes the new version, so merged engine work is never unreleased. (Before 2026-08-26 the workflow published the engine inside the dev-build half under `--tag dev`, so a new engine version reached the registry with its `latest` tag stuck on the previous version until an operator moved it by hand — `0.2.x` and `0.3.0` shipped that way.) The engine's own line continues from `0.1.0` (after the published `0.0.x` series); the `8.0.0-rc.N` engine versions that shipped while it was still in lockstep are burned values — they exist on the registry, nothing pins them, and version numbers are never reused. -The lockstep set is: the workspace root, `packages/cli`, `packages/cli-telemetry`, `packages/cli-conformance`, and `packages/tsconfig`. Private packages are never published (`pnpm publish` skips them), but they still version in lockstep so a contributor cloning the repo at any commit sees one consistent answer to "what version is this code?". Workspace-internal dependencies are pinned as `workspace:` (e.g. `workspace:8.0.0-rc.1`); pnpm resolves them locally during development and rewrites them to the exact version at publish time, so every published package carries an exact-version pin on its siblings. +The lockstep set is: the workspace root, `packages/cli`, `packages/prisma`, `packages/cli-telemetry`, `packages/cli-conformance`, and `packages/tsconfig`. Private packages are never published (`pnpm publish` skips them), but they still version in lockstep so a contributor cloning the repo at any commit sees one consistent answer to "what version is this code?". Workspace-internal dependencies are pinned as `workspace:` (e.g. `workspace:8.0.0-rc.1`); pnpm resolves them locally during development and rewrites them to the exact version at publish time, so every published package carries an exact-version pin on its siblings. How the packages published by *other* repositories relate to the engine's version — the product CLI packages the shell mounts, and the product libraries applications install — is governed by [ADR 0004](../architecture/adrs/0004-engine-version-pinning.md): product CLI packages declare `@prisma/cli-engine` as an exact peer dependency the shell satisfies, and product libraries carry no engine relationship at all. From 9d8b0e1edcaed0aaea7b537063f17c4fe01f6093 Mon Sep 17 00:00:00 2001 From: willbot Date: Sun, 27 Sep 2026 17:23:24 +0200 Subject: [PATCH 4/6] chore: delete .drive/projects/prisma-cli-v8 The project's lasting knowledge moved to docs/ in the previous commits, its retro lessons to .drive/HEALTH.md, and every open ledger entry to a GitHub issue that links to the ledger at 4356b361. Co-Authored-By: Claude Opus 5.5 Signed-off-by: willbot Signed-off-by: Will Madden --- .../briefs/1b-leftovers-prisma-prisma.md | 39 - .../assets/briefs/1c-leftovers-composer.md | 92 - .../briefs/composer-cli-split-handover.md | 89 - .../briefs/credential-manager-handover.md | 227 --- .../briefs/deployment-logs-http-endpoint.md | 113 -- .../briefs/init-and-shell-retirement.md | 103 - .../orm-toolchain-engine-peer-handover.md | 54 - .../assets/briefs/s1-handover.md | 42 - .../assets/briefs/s2b-engine-requests.md | 66 - .../assets/briefs/s2b-midslice-handover.md | 131 -- .../assets/briefs/s2c-continuation.md | 183 -- .../assets/briefs/s2c-handover.md | 305 --- .../assets/briefs/s3-closeout-handover.md | 153 -- .../assets/briefs/s5-orm-handover.md | 187 -- .../briefs/windows-ci-credential-manager.md | 25 - .../prisma-cli-v8/assets/command-review.md | 205 -- .../engine/credential-manager-design.md | 898 --------- .../assets/engine/daemon-library-notes.md | 60 - .../assets/engine/engine-interface-draft.ts | 1772 ----------------- .../assets/engine/output-modes-survey.md | 652 ------ .../assets/engine/stricli-vs-clipanion.md | 238 --- .../engine/websocket-transport-design.md | 82 - .../engine/whoami-parity-divergences.md | 240 --- .../prisma-cli-v8/assets/rollout-plan.md | 72 - .../assets/s2/api-overlay-audit.md | 451 ----- .../assets/s2/command-inventory.md | 705 ------- .../assets/s2/parity-divergences-s3.md | 278 --- .../assets/s2/parity-divergences-s7.md | 48 - .../assets/s2/parity-divergences-s8.md | 219 -- .../s2/parity-divergences-service-logs.md | 113 -- .../assets/s2/parity-divergences.md | 1612 --------------- .../assets/s2/shell-deletion-survivors.md | 63 - .../assets/s3/composer-inventory.md | 1565 --------------- .drive/projects/prisma-cli-v8/deferred.md | 528 ----- .drive/projects/prisma-cli-v8/design-notes.md | 65 - .drive/projects/prisma-cli-v8/plan.md | 219 -- .../plans/command-grammar-cleanup.md | 69 - .../plans/config-file-resolution.md | 46 - .../prisma-cli-v8/plans/engine-colour.md | 146 -- .../plans/engine-owns-telemetry.md | 146 -- .../plans/engine-redirect-table.md | 79 - .../prisma-cli-v8/plans/s1-engine-vertical.md | 134 -- .../prisma-cli-v8/plans/s2a-foundations.md | 101 - .../prisma-cli-v8/plans/s2b-resources.md | 37 - .../prisma-cli-v8/plans/s2c-services.md | 57 - .../plans/s2d-init-and-retirement.md | 30 - .../prisma-cli-v8/plans/s3-composer.md | 60 - .../prisma-cli-v8/plans/s6-conformance.md | 130 -- .../prisma-cli-v8/plans/s7-release.md | 112 -- .../prisma-cli-v8/plans/s8-services.md | 102 - .../prisma-cli-v8/plans/service-logs.md | 26 - .../prisma-cli-v8/reviews/code-review-s2c.md | 1439 ------------- .drive/projects/prisma-cli-v8/spec.md | 158 -- .../specs/command-grammar-cleanup.md | 123 -- .../specs/config-file-resolution.md | 84 - .../prisma-cli-v8/specs/engine-colour.md | 370 ---- .../specs/engine-owns-telemetry.md | 424 ---- .../engine-package-manager-capability-plan.md | 123 -- .../engine-package-manager-capability.md | 226 --- .../specs/engine-redirect-table.md | 135 -- .../prisma-cli-v8/specs/s1-engine-vertical.md | 83 - .../prisma-cli-v8/specs/s2-overview.md | 133 -- .../prisma-cli-v8/specs/s2a-foundations.md | 274 --- .../specs/s2b-design/conventions.md | 428 ---- .../specs/s2b-design/d1-project.md | 488 ----- .../specs/s2b-design/d2-postgres.md | 598 ------ .../specs/s2b-design/d3-bucket-branch-git.md | 505 ----- .../s2b-design/facts/facts-d1-project.md | 611 ------ .../s2b-design/facts/facts-d2-postgres.md | 552 ----- .../facts/facts-d3-bucket-branch-git.md | 352 ---- .../s2b-design/facts/facts-v8-patterns.md | 401 ---- .../prisma-cli-v8/specs/s2b-resources.md | 142 -- .../prisma-cli-v8/specs/s2c-services.md | 75 - .../specs/s2d-init-and-retirement.md | 89 - .../prisma-cli-v8/specs/s3-composer.md | 498 ----- .../prisma-cli-v8/specs/s6-conformance.md | 195 -- .../prisma-cli-v8/specs/s7-release.md | 328 --- .../prisma-cli-v8/specs/s8-services.md | 197 -- .../prisma-cli-v8/specs/service-logs.md | 116 -- 79 files changed, 22316 deletions(-) delete mode 100644 .drive/projects/prisma-cli-v8/assets/briefs/1b-leftovers-prisma-prisma.md delete mode 100644 .drive/projects/prisma-cli-v8/assets/briefs/1c-leftovers-composer.md delete mode 100644 .drive/projects/prisma-cli-v8/assets/briefs/composer-cli-split-handover.md delete mode 100644 .drive/projects/prisma-cli-v8/assets/briefs/credential-manager-handover.md delete mode 100644 .drive/projects/prisma-cli-v8/assets/briefs/deployment-logs-http-endpoint.md delete mode 100644 .drive/projects/prisma-cli-v8/assets/briefs/init-and-shell-retirement.md delete mode 100644 .drive/projects/prisma-cli-v8/assets/briefs/orm-toolchain-engine-peer-handover.md delete mode 100644 .drive/projects/prisma-cli-v8/assets/briefs/s1-handover.md delete mode 100644 .drive/projects/prisma-cli-v8/assets/briefs/s2b-engine-requests.md delete mode 100644 .drive/projects/prisma-cli-v8/assets/briefs/s2b-midslice-handover.md delete mode 100644 .drive/projects/prisma-cli-v8/assets/briefs/s2c-continuation.md delete mode 100644 .drive/projects/prisma-cli-v8/assets/briefs/s2c-handover.md delete mode 100644 .drive/projects/prisma-cli-v8/assets/briefs/s3-closeout-handover.md delete mode 100644 .drive/projects/prisma-cli-v8/assets/briefs/s5-orm-handover.md delete mode 100644 .drive/projects/prisma-cli-v8/assets/briefs/windows-ci-credential-manager.md delete mode 100644 .drive/projects/prisma-cli-v8/assets/command-review.md delete mode 100644 .drive/projects/prisma-cli-v8/assets/engine/credential-manager-design.md delete mode 100644 .drive/projects/prisma-cli-v8/assets/engine/daemon-library-notes.md delete mode 100644 .drive/projects/prisma-cli-v8/assets/engine/engine-interface-draft.ts delete mode 100644 .drive/projects/prisma-cli-v8/assets/engine/output-modes-survey.md delete mode 100644 .drive/projects/prisma-cli-v8/assets/engine/stricli-vs-clipanion.md delete mode 100644 .drive/projects/prisma-cli-v8/assets/engine/websocket-transport-design.md delete mode 100644 .drive/projects/prisma-cli-v8/assets/engine/whoami-parity-divergences.md delete mode 100644 .drive/projects/prisma-cli-v8/assets/rollout-plan.md delete mode 100644 .drive/projects/prisma-cli-v8/assets/s2/api-overlay-audit.md delete mode 100644 .drive/projects/prisma-cli-v8/assets/s2/command-inventory.md delete mode 100644 .drive/projects/prisma-cli-v8/assets/s2/parity-divergences-s3.md delete mode 100644 .drive/projects/prisma-cli-v8/assets/s2/parity-divergences-s7.md delete mode 100644 .drive/projects/prisma-cli-v8/assets/s2/parity-divergences-s8.md delete mode 100644 .drive/projects/prisma-cli-v8/assets/s2/parity-divergences-service-logs.md delete mode 100644 .drive/projects/prisma-cli-v8/assets/s2/parity-divergences.md delete mode 100644 .drive/projects/prisma-cli-v8/assets/s2/shell-deletion-survivors.md delete mode 100644 .drive/projects/prisma-cli-v8/assets/s3/composer-inventory.md delete mode 100644 .drive/projects/prisma-cli-v8/deferred.md delete mode 100644 .drive/projects/prisma-cli-v8/design-notes.md delete mode 100644 .drive/projects/prisma-cli-v8/plan.md delete mode 100644 .drive/projects/prisma-cli-v8/plans/command-grammar-cleanup.md delete mode 100644 .drive/projects/prisma-cli-v8/plans/config-file-resolution.md delete mode 100644 .drive/projects/prisma-cli-v8/plans/engine-colour.md delete mode 100644 .drive/projects/prisma-cli-v8/plans/engine-owns-telemetry.md delete mode 100644 .drive/projects/prisma-cli-v8/plans/engine-redirect-table.md delete mode 100644 .drive/projects/prisma-cli-v8/plans/s1-engine-vertical.md delete mode 100644 .drive/projects/prisma-cli-v8/plans/s2a-foundations.md delete mode 100644 .drive/projects/prisma-cli-v8/plans/s2b-resources.md delete mode 100644 .drive/projects/prisma-cli-v8/plans/s2c-services.md delete mode 100644 .drive/projects/prisma-cli-v8/plans/s2d-init-and-retirement.md delete mode 100644 .drive/projects/prisma-cli-v8/plans/s3-composer.md delete mode 100644 .drive/projects/prisma-cli-v8/plans/s6-conformance.md delete mode 100644 .drive/projects/prisma-cli-v8/plans/s7-release.md delete mode 100644 .drive/projects/prisma-cli-v8/plans/s8-services.md delete mode 100644 .drive/projects/prisma-cli-v8/plans/service-logs.md delete mode 100644 .drive/projects/prisma-cli-v8/reviews/code-review-s2c.md delete mode 100644 .drive/projects/prisma-cli-v8/spec.md delete mode 100644 .drive/projects/prisma-cli-v8/specs/command-grammar-cleanup.md delete mode 100644 .drive/projects/prisma-cli-v8/specs/config-file-resolution.md delete mode 100644 .drive/projects/prisma-cli-v8/specs/engine-colour.md delete mode 100644 .drive/projects/prisma-cli-v8/specs/engine-owns-telemetry.md delete mode 100644 .drive/projects/prisma-cli-v8/specs/engine-package-manager-capability-plan.md delete mode 100644 .drive/projects/prisma-cli-v8/specs/engine-package-manager-capability.md delete mode 100644 .drive/projects/prisma-cli-v8/specs/engine-redirect-table.md delete mode 100644 .drive/projects/prisma-cli-v8/specs/s1-engine-vertical.md delete mode 100644 .drive/projects/prisma-cli-v8/specs/s2-overview.md delete mode 100644 .drive/projects/prisma-cli-v8/specs/s2a-foundations.md delete mode 100644 .drive/projects/prisma-cli-v8/specs/s2b-design/conventions.md delete mode 100644 .drive/projects/prisma-cli-v8/specs/s2b-design/d1-project.md delete mode 100644 .drive/projects/prisma-cli-v8/specs/s2b-design/d2-postgres.md delete mode 100644 .drive/projects/prisma-cli-v8/specs/s2b-design/d3-bucket-branch-git.md delete mode 100644 .drive/projects/prisma-cli-v8/specs/s2b-design/facts/facts-d1-project.md delete mode 100644 .drive/projects/prisma-cli-v8/specs/s2b-design/facts/facts-d2-postgres.md delete mode 100644 .drive/projects/prisma-cli-v8/specs/s2b-design/facts/facts-d3-bucket-branch-git.md delete mode 100644 .drive/projects/prisma-cli-v8/specs/s2b-design/facts/facts-v8-patterns.md delete mode 100644 .drive/projects/prisma-cli-v8/specs/s2b-resources.md delete mode 100644 .drive/projects/prisma-cli-v8/specs/s2c-services.md delete mode 100644 .drive/projects/prisma-cli-v8/specs/s2d-init-and-retirement.md delete mode 100644 .drive/projects/prisma-cli-v8/specs/s3-composer.md delete mode 100644 .drive/projects/prisma-cli-v8/specs/s6-conformance.md delete mode 100644 .drive/projects/prisma-cli-v8/specs/s7-release.md delete mode 100644 .drive/projects/prisma-cli-v8/specs/s8-services.md delete mode 100644 .drive/projects/prisma-cli-v8/specs/service-logs.md 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 deleted file mode 100644 index 1deb23e3..00000000 --- a/.drive/projects/prisma-cli-v8/assets/briefs/1b-leftovers-prisma-prisma.md +++ /dev/null @@ -1,39 +0,0 @@ -# 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 deleted file mode 100644 index 3ba7b533..00000000 --- a/.drive/projects/prisma-cli-v8/assets/briefs/1c-leftovers-composer.md +++ /dev/null @@ -1,92 +0,0 @@ -# Brief: composer config-contract compliance and control-API test double - -> **CLOSED at S3 closure (D4, 2026-08-11).** This brief was paused, and -> S3 has now settled all three deliverables — two of them by doing the -> work under a different contract, one by handing the remainder back. -> The dispositions are recorded at the end of this file. The brief is -> kept, not deleted: it is the statement of the problem each disposition -> answers, and the paused-work record in -> `../s3/composer-inventory.md` §6 points here. - -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. - -## Dispositions (S3 closure, D4) - -**Deliverable 1 — config validation returns diagnostics: SUPERSEDED, and -done.** S3's contract rule R-S3-2 asked for the same semantics from a -different starting point — the engine's `ConfigSection.validate` must -return `SectionValidation` and must never throw — so composer's -throwing loader was rewritten to value-plus-diagnostics under that rule -in D2 rather than under this brief. What the brief asked for is what -shipped: a loaded config carries structured diagnostics tagged with the -section they concern, commands fail only on a section they need, an -unevaluable module yields one evaluation diagnostic, and nothing throws -from construction. The rendering is the engine's rather than composer's -own. One thing the brief listed is not S3's to claim: the -before/after pinning of rendered output for currently-failing configs. -The rendering surface changed wholesale with the engine port, so the -guarantee "user-visible behaviour may only change in framing" was -overtaken; the change is enumerated in -`../s2/parity-divergences-s3.md` instead. - -**Deliverable 2 — effect-resolution preflight becomes a diagnostic: -SPLIT.** The part S3 had to own is the part with nowhere else to live: -the prisma bin has no composer `bin.ts` to run an import-time check in, -so the check moved INTO the shared config-load machinery (R-S3-2), -where it surfaces as the already-registered -`DEPS.EFFECT_VERSION_CONFLICT` diagnostic. The user-visible consequence -— commands that need no config, `--help` included, now survive a -mismatched `effect` in the consumer's tree — is recorded in the -divergence file. **The rest stays with the composer team**: the -`DEPS.EXECUTOR_UNLOADABLE` backstop is untouched by S3 and was never -verified against the new path, and the two checks the brief names -(`check:npm-effect-resolution`, the effect CI probe) were updated in D3 -but not run, because they perform real npm installs and need network — -tracked in `../../deferred.md`. Whoever runs them owns any fallout. - -**Deliverable 3 — published control-API test double: DELIVERED**, as -S3's R-S3-5 test surface (2). It is exported from composer's `./testing` -entrypoint, fixture-backed with the same operation signatures and a -working `DevSession` double, with the compile-time conformance check the -brief asked for, plus one requirement the brief did not state and S3 -does: its built chunk must contain no import path to the real -implementation, verified by building the tarball and grepping the chunk -for alchemy and effect. The family export takes an optional operations -argument (`createComposerFamily({ operations })`) so a host can mount -the real grammar against the double, which is how prisma-cli's family -tests stay free of alchemy and containers. diff --git a/.drive/projects/prisma-cli-v8/assets/briefs/composer-cli-split-handover.md b/.drive/projects/prisma-cli-v8/assets/briefs/composer-cli-split-handover.md deleted file mode 100644 index 495cca20..00000000 --- a/.drive/projects/prisma-cli-v8/assets/briefs/composer-cli-split-handover.md +++ /dev/null @@ -1,89 +0,0 @@ -# Handover brief — split @prisma/composer into library + CLI package, engine becomes a peer - -Written 2026-08-13 for an independent agent with NO prior context. The operator is Will Madden ("the operator"). Where this brief summarizes a document, the document wins. Repo paths are absolute; "this project directory" means `.drive/projects/prisma-cli-v8/` in the prisma-cli repo. - -## 1. Read first, in this order - -1. This project directory in **prisma-cli** (`/Users/wmadden/Projects/prisma/prisma-cli`, branch `main` — always `git fetch origin main` first, local checkouts go stale): `spec.md` (project frame), `plan.md` (slices and dependency graph), `design-notes.md` (settled design decisions), `specs/s2-overview.md` (standing rulings that bind every slice). -2. `specs/s6-conformance.md` in the same directory — the conformance-checker contract. Its §5 records the operator rulings of 2026-08-12, and its §1 documents the defect class this whole strategy exists to kill: installing `@prisma/cli` today resolves multiple copies of `@prisma/cli-engine`. -3. `deferred.md` in the same directory, the entry "The engine pin moves to whatever the tandem release publishes" (~line 37) — the standing ruling that pins must match, as a release requirement. -4. `specs/s3-composer.md` — the contract under which composer's current shape was built (one process, the family export, `ctx.spawn`, the S3 acceptance list). You are changing its packaging, not its design. -5. The composer repo itself: `/Users/wmadden/Projects/prisma/composer`. **Its local `main` is routinely stale — `git fetch origin main` and read via `origin/main` before trusting anything on disk.** Do all work in a fresh worktree off `origin/main`. - -## 2. The ruled strategy (operator, 2026-08-13) - -The engine (`@prisma/cli-engine`) must exist exactly once in any installed tree that runs the CLI. The recorded strategy, agreed in discussion with the operator: - -- Product CLI packages (composer's command family; the ORM's `@prisma/orm-toolchain`) declare the engine as an **exact `peerDependency`** (plus a `devDependency` for their own tests). The shell (`@prisma/cli`) carries the one real engine `dependency` and satisfies everyone's peer. Peers resolve against the ancestor, so one engine exists in any tree shape, and an unsatisfiable peer is an **install-time error** instead of a silent second copy. -- Widening exact peers to a **range** is the recorded destination, post-GA, once the engine has a written compatibility contract. Not now: during the rc line the engine breaks consumers deliberately, so a range would be fiction. -- Product **libraries carry no engine relationship at all**. Applications depend on libraries (`@prisma/composer`, `@prisma/orm-postgres`); only the consolidated CLI depends on product CLI packages; product CLI packages are reached only transitively through the CLI. -- The target dependency tree, in the operator's words: `app → @prisma/cli → { orm-cli → engine(peer), composer-cli → engine(peer), engine }` (the shell's published name is `@prisma/cli`), with `app → @prisma/composer` and `app → @prisma/orm-postgres` as ordinary library dependencies alongside. - -The ORM side already conforms structurally: `@prisma/orm-toolchain` is the dev/CLI package (applications reach its vite plugin through `@prisma/orm-postgres/vite-plugin-contract-emit`, a forwarded export — verified 2026-08-13), so it needs only the dependency-field change, in its own repo, **not in this brief's scope**. Composer is the one package that mixes the application-facing runtime library with the CLI family in one manifest. That split is your job. - -## 3. Verified current state of composer (2026-08-13, origin/main and the published 0.6.0-dev.16 tarball) - -- One publishable package `@prisma/composer` (`packages/9-public/composer/`), version 0.6.0 on disk. Exports the **library** surface (`.`, `./config`, `./control`, `./deploy`, `./local-target`, `./report`, `./casts`, `./assertions`, `./arktype`, `./service-rpc`, `./node`, `./node/control`, `./nextjs`, `./nextjs/control`), the **CLI** surface (`./family`, `./testing`), and a bin (`prisma-composer` → `./dist/bin.mjs`). -- `@prisma/cli-engine: "0.0.9"` sits in `dependencies`. No engine peer exists. -- The import graphs are **already disjoint** in the published output: the only dist files whose static graph names the engine are `family.mjs`, `bin.mjs` and their declaration files; zero shared chunks import it; the library entrypoints load with no engine reachable. The split is a packaging operation, not an untangling. -- The family implementation lives in the private workspace package `@internal/cli` (`packages/0-framework/3-tooling/cli/`); the publishable package's `src/exports/family.ts` is a re-export barrel, bundled by tsdown with `noExternal: [/^@internal\//]` and `external: ['esbuild', '@prisma/cli-engine']` (`packages/9-public/composer/tsdown.config.ts`). The comment there explains why the engine is external: composer and the prisma bin must share one engine instance. -- A second publishable package `@prisma/composer-prisma-cloud` exists; it has no engine relationship and is out of scope. -- Composer's own conformance-ish checks: `scripts/check-cli-engine-pin.mjs` (pin exact + identical across composer's two manifests + surviving into the packed manifest + a packed chunk retaining a bare engine import + `dist/bin.mjs` present), `scripts/check-family-static-graph.mjs` (packed output free of `alchemy`/`effect` imports, anchored at `dist/family.mjs`, `dist/bin.mjs`, `dist/testing.mjs`), `scripts/check-floor-imports.mjs`, `scripts/check-npm-effect-resolution.mjs` — all on PR CI (`ci.yml`) only — and `scripts/check-publish-deps.mjs`, the sole check in `publish.yml`. -- The consumer today: prisma-cli's shell imports `createComposerFamily` from `@prisma/composer/family` (`packages/cli/src/cli.ts`) and pins `@prisma/composer` exactly. Its conformance check (`packages/cli/scripts/conformance.ts`) currently expects the composer family package to pin the engine in `dependencies` and carries a recorded exception for the 0.0.9-vs-8.0.0-rc.1 mismatch. - -## 4. The work - -### D1 — the package split - -Create a new publishable package `@prisma/composer-cli` (name is STOP-1) in `packages/9-public/`, following the existing package's conventions (tsdown config extending `@internal/tsdown-config`, same `files`, license, repository fields — copy the manifest discipline from `packages/9-public/composer/package.json`). It takes over from `@prisma/composer`: - -- the `./family` export (the `CommandFamily`, `createComposerFamily`, `composerSection`, the operations seam), -- the `./testing` export (the family's test double belongs with the family), -- the `prisma-composer` bin (STOP-2 covers its fate; default: it moves here unchanged). - -`@prisma/composer` keeps every library export and **loses** `./family`, `./testing`, the bin, and its `@prisma/cli-engine` dependency entirely. Breaking change to the package's export map: record it in composer's changelog/release notes machinery, and note that the only known consumer of the removed subpaths is the prisma-cli shell (§5). - -Both packages bundle from the same `@internal/*` sources; nothing moves in `packages/0-framework/`. The split is manifests, tsdown entries, and export maps. - -### D2 — the engine becomes an exact peer - -In `@prisma/composer-cli`: `peerDependencies: { "@prisma/cli-engine": "0.0.9" }` (or whatever exact version composer builds against at the time), plus the same version in `devDependencies` so composer's own tests and the workspace resolve it. The version stays EXACT — the range destination is post-GA and is not yours to take. - -### D3 — composer's checks follow the packages - -- `check-cli-engine-pin.mjs`: the engine reference it asserts is now `@prisma/composer-cli`'s peer (exact, matching `@internal/cli`'s devDependency, surviving into the packed manifest); the packed-chunk bare-import assertion and the `dist/bin.mjs` presence assertion move to the new package's tarball. -- `check-family-static-graph.mjs`: its three anchored entrypoints now live in `@prisma/composer-cli`'s dist. -- `check-publish-deps.mjs` and `check-npm-effect-resolution.mjs`: three publishable packages now, not two. -- **New assertion, from the ruled strategy: `@prisma/composer`'s packed output must be engine-free** — no `@prisma/cli-engine` import anywhere in its tarball's JavaScript, and no engine entry in any consumer-installed dependency field. This is the library half of the invariant and nothing checks it today. - -### D4 — the publish path runs the checks - -Composer's `publish.yml` runs only `check:publish-deps` today; the pin, static-graph and effect-resolution checks run on PR CI only. Add them to `publish.yml` after `check:publish-deps` (an already-identified hole, in scope here because you are editing these checks anyway). - -## 5. Explicit handshake: what you do NOT do - -- **Do not touch prisma-cli.** After `@prisma/composer-cli` publishes, the shell repins (`@prisma/composer` → `@prisma/composer-cli` in `dependencies` and in `packages/cli/src/cli.ts`), and its conformance check's 3c evolves from pin-equality to peer-satisfaction with the exception list deleted. That is a follow-up in the prisma-cli repo — name it in your PR body as the required next step, with the file pointers above. -- **Do not touch prisma/prisma.** `@prisma/orm-toolchain`'s dependencies→peer change is the same strategy in another repo, separately dispatched. -- **Do not change the engine's versioning.** The engine now versions independently of the shell's lockstep (ruled 2026-08-13, recorded in ADR 0004) — but that ruling is implemented in prisma-cli, not here; composer only consumes whatever exact engine version it builds against. -- **Do not write the strategy ADR.** It is being recorded separately in prisma-cli; your PR implements composer's share of it. - -## 6. STOP — surface before implementing - -- **STOP-1: the package name.** `@prisma/composer-cli` is the operator's sketch; confirm it (npm scope availability, repo conventions) before creating anything. -- **STOP-2: the bin's fate.** The operator: composer "will not continue to publish its own standalone bin (probably)". Default for this slice: the bin moves to `@prisma/composer-cli` unchanged, retirement is a separate decision. If you find the bin materially complicates the split, surface that instead of working around it. -- **STOP-3: versioning of the new package.** Lockstep with `@prisma/composer` (shared `set-version` machinery) is the presumable default; confirm, because it decides how the tandem release names the pair. -- Anything in composer's release/tag automation that assumes exactly two publishable packages. - -## 7. Process rules (operator-enforced, non-negotiable) - -- Work in a fresh worktree off composer `origin/main`. Never trust a stale local `main` — fetch first, in every repo you read. -- Git identity: the `wmadden-electric` bot. Commit `git commit -s --trailer "Signed-off-by: Will Madden "`; end commit bodies with a `Co-Authored-By:` line naming your model. Push ONLY to `git@github-wmadden-electric:prisma/composer.git`. Never force-push. Stage by path, never `git add -A` on directories you have not inspected. -- pnpm only, never npm/npx — except inside `check-npm-effect-resolution.mjs`'s sandbox, which is deliberately npm. -- Tests before implementation; dependency injection, never `vi.mock`/module mocking; composer's existing check scripts show the io-seam style. -- NEVER hard-wrap markdown prose. Plain-English reports; no invented jargon; banned words: "load-bearing", "smoking gun", "belt and suspenders", "gate". -- PR: one PR, base `main`, DRAFT first. Description structure: grounding example first (a real install/run, before/after), then the decision, then the narrative, alternatives last. Reference the strategy discussion date (2026-08-13) and prisma-cli PR #161 / prisma/prisma PR #29998 as the sibling conformance work. -- Verification, each measured as the command's own exit code: composer's full suite, its script tests, every check script run end to end (including your changed ones), and a `publish.yml` dry-run path if the repo offers one. - -## 8. Your first report - -Confirm you read the project docs and this brief; state the STOP-1..3 answers you need; list any fact in §3 that no longer holds on composer `origin/main` (the repo moves fast — re-verify, do not trust this brief's snapshot); then your dispatch plan. diff --git a/.drive/projects/prisma-cli-v8/assets/briefs/credential-manager-handover.md b/.drive/projects/prisma-cli-v8/assets/briefs/credential-manager-handover.md deleted file mode 100644 index 09f80956..00000000 --- a/.drive/projects/prisma-cli-v8/assets/briefs/credential-manager-handover.md +++ /dev/null @@ -1,227 +0,0 @@ -# Credential-manager rework handover — finish remediation, close out PR #130 - -Written 2026-08-10 for an agent with NO context on this session. -The operator is Will Madden. Everything you need is in this file or -the named documents; where this brief summarizes a document, the -document wins. - -## 1. Where you are - -Repo `prisma/prisma-cli`, worktree checked out from branch -`claude/prisma-cli-s1-d6-013cea`, working branch `s2a-foundations` -(= PR #130, open, base `main`). This branch -carries slice S2a of the v8 CLI port PLUS a full rework of the auth -family onto a new component, the **credential manager**. The rework -is functionally complete and reviewed; what remains is exactly: - -1. FINISH the remediation of the verification findings (§4 below) — - an implementer was halted mid-work by a rate limit; its partial, - **unverified** state is committed as `4b006d1`. -2. Re-run verification (§5). -3. Rewrite #130's PR description (§6) and hand the PR to Will. - -No background agents are running. Two INDEPENDENT agents (not -yours) work slices S2b and S2c in other worktrees; they merge this -branch down. Do not touch their branches (`s2b-resources`, -`s2c-services`). - -## 2. The design you are implementing against - -`.drive/projects/prisma-cli-v8/assets/engine/credential-manager-design.md` -— revision 5, NORMATIVE, at HEAD. Read it in full before any code -change. One-paragraph summary: the CLI holds per-workspace -**sessions** (a session = "logged in to workspace X"; at most one -per workspace, keyed by workspace id, one current). A process PINS -its session at first read for its whole lifetime. The engine -(`packages/cli-engine`) owns the management API client; the manager -(`packages/cli/src/auth/credential-manager.ts`, class -`FileCredentialManager`) owns the state file (same path as the -legacy auth file, new shape, atomic 0600 writes, one short advisory -lock, no network under the lock) and implements the platform SDK's -`TokenStorage` so 401→refresh→retry writes land under its rules. -Racing refreshes are deliberately uncoordinated across processes — -the auth server absorbs them (10s refresh-token reuse grace; -sibling pairs stay valid). Identity is NOT tracked (wallet is -identity-blind, operator ruling). Six v8 commands sit on top with -their LEGACY names: `auth login|logout|whoami`, -`auth workspace list|use|logout`. `workspace use` SELECTS among -held sessions only — it never opens a browser (operator ruling). - -Engine affordances added this session (already landed): consent -tokens + global repeatable `--confirm ` flag (type-to-confirm -interactive; exact-match non-interactive; `--yes` still cannot grant -consent), `ctx.openUrl` (degrades to printing the URL), and -`prompt.browserWait` (non-interactive → structured -interaction-required error, exit 2). `needs.interaction` predates -them and is the declarative interactivity requirement. - -## 3. Commit map (this session's work, all pushed) - -- `a8ef3fb` engine surface (rev-4 shape, superseded) -- `9384a95` engine reworked to rev 5 (session model) -- `6bb8452` consent tokens / `--confirm` / openUrl / browserWait -- `9ffbb01` the real manager: persistence, pinning, migration, - `performLogin` custody split, bin wiring -- `ddbb816` six v8 auth commands + parity/contract doc rewrites -- `d7e8df9`, `7716e8b`, and earlier `015ae55`/`e24d1d5` — design-doc - revisions and rulings -- `4b006d1` **PARTIAL, UNVERIFIED** remediation (see §4) - -At `7716e8b` (pre-remediation) every suite was green: cli 814, -cli-engine 234, telemetry 97, typecheck, root lint all exit 0. - -## 4. YOUR FIRST TASK — finish the remediation - -The verification review (full findings below) reported 1 blocker, -6 should-fix, 8 notes. The halted implementer had addressed most of -them; commit `4b006d1` contains its uncommitted tree at halt time — -its last status: "Now the worker stderr capture (finding 14) and -server teardown." NOTHING in `4b006d1` has been test-run. - -Procedure: -1. `git show 4b006d1` and map each hunk to a finding number. -2. Complete what is missing (at minimum finding 14's worker-stderr - leak capture and whatever "server teardown" it was mid-way - through — check `packages/cli/tests/credential-manager-processes.test.ts` - and `tests/helpers/credential-manager-worker.ts` for a scripted - token-endpoint HTTP server that may leak between tests). -3. Run ALL suites (§7 verification commands). Fix what fails. -4. Amend or follow-up commit (`fix(cli): credential-manager - verification findings`, body listing finding numbers; commit - rules §7). - -The findings (severity, file:line refs are pre-remediation at -`7716e8b`; verify against current state): - -- **1 BLOCKER** `packages/cli-engine/src/execution/api-client.ts:161-164`: - a non-`AuthError` from the refresh path must map to the transient - auth-service error (`CLI.AUTH_SERVICE_ERROR`), NOT escape as the - raw cause (`CLI.INTERNAL_ERROR`, exit 1). Spec §6. CLI structured - errors must still pass through unwrapped (existing test - `packages/cli-engine/tests/management-api.test.ts:391`). Needs - tests. `4b006d1` touches this file — verify the fix + tests exist. -- **2** Engine-side `PRISMA_NEXT_DEBUG` valve for the refresh - mapping (spec §6: refresh attempted, endpoint status + error - field). `4b006d1` adds `packages/cli-engine/src/execution/debug.ts` - — verify wiring, on/off tests, and that the leak scan covers it. -- **3** `packages/cli/src/v8/auth/whoami.ts`: env-session identity - from decoding the env token FIRST (no network for it); `/v1/me` - is the stored-session path only (spec §6a as amended). -- **4** Env-override test matrix completeness (spec §5): every - mutation × {unset, set, blank, whitespace}; `createSession` under - blank/whitespace; state-file byte-equality. -- **5** Assert the §8 atomic-write mechanism (temp + fsync + rename; - no `.tmp` sibling remains; sync-before-rename ordering). Do not - weaken `packages/cli/src/auth/state-file.ts:167-190` to test it. -- **6** Assert §8 rotation durability: rotated pair persisted before - the new access token reaches any caller. -- **7** `credential-manager-processes.test.ts`: the two-process - rotation test must drive a REAL refresh through a scripted local - token endpoint (mimic the 10s reuse grace), not direct - `setTokens` calls. `4b006d1` touches these files — verify. -- **9** Blank/whitespace `PRISMA_SERVICE_TOKEN` must not read as "in - force" in `workspace-list.ts` / `login.ts` (`!== undefined` was - the bug); blank → the single `AUTH.SERVICE_TOKEN_EMPTY` outcome. - `4b006d1` adds `packages/cli/src/auth/service-token.ts` — verify - both commands use it, with tests. -- **10** `endAllSessions` env-override no-op (zero stored sessions) - must still unlink the legacy context sidecar (spec §7). -- **11** Reads-never-write probe also spies unlink/rm + sync fs - write APIs. -- **12** `api-client.ts:96-98` blank-token fallback must use the - single-sourced `emptyServiceTokenError` (currently duplicated - logic; unreachable but wrong). -- **14** Leak-scan coverage: rotation/clear debug lines, every - refresh-failure error path, worker-process stderr. -- **SKIP by ruling**: finding 8 (whoami override notice - unconditional — the doc at HEAD §6 was amended to say exactly - that; the reviewer's citation was stale), findings 13 and 15 - (verified fine / unreachable by construction). - -## 5. Then: re-verification - -Dispatch a fresh reviewer subagent (model: Opus, read-only) to -re-verify ONLY the findings above against the code on disk plus a -smoke pass over spec §§3–8 conformance (the previous full -verification found everything else SATISFIED — do not re-litigate -what it passed). Fix anything it raises; loop until clean. - -## 6. Then: PR #130 description + handoff - -Rewrite #130's description (gh CLI; the PR is on -prisma/prisma-cli). Will's ruled structure, in order: a GROUNDING -EXAMPLE first (a real command run, before/after), then the -decision, then the narrative, alternatives last. No internal -process codes, no dispatch/round labels, no reviewer numbering. -Content must cover BOTH the original S2a scope (engine -production-readiness: ctx.api, prompts via clack, telemetry, -versioning/publish machinery, version command) AND the auth rework -(the session model — summarize §2 of this brief; name the -user-visible changes: `logout --workspace` gone, `--confirm ` -for scripted consent, exit-code unifications, whoami shape). The -parity story lives in -`.drive/projects/prisma-cli-v8/assets/s2/parity-divergences.md` -(auth sections just rewritten — link, don't duplicate). Then tell -Will it is ready for his re-review. Do NOT merge; do NOT mark -ready-for-review yourself unless the draft state blocks his review. - -## 7. Process rules (non-negotiable, operator-enforced) - -- Git identity — you are the `wmadden-electric` bot: stage - explicitly by path (NEVER `git add -A`/`-u`; NEVER anything under - `wip/` or `.drive/projects/prisma-cli-v8/specs/reviews/`); commit - `git commit -s --trailer "Signed-off-by: Will Madden "` - with body ending `Co-Authored-By: Claude Fable 5 `; - push ONLY to `git@github-wmadden-electric:prisma/prisma-cli.git` - (remote `bot`). -- Verification per change: `pnpm --filter @prisma/cli test`, - `pnpm --filter @prisma/cli-engine test`, - `pnpm --filter @repo/cli-telemetry test`, `pnpm typecheck`, and - root lint measured as pnpm's OWN exit code with `wip/` moved - aside in one shell: - `mv wip /tmp/wip-stash && pnpm lint; s=$?; mv /tmp/wip-stash wip`. -- `wip/repos/` holds read-only reference clones (pdp-control-plane, - prisma, composer) — never stage, never modify. The platform - SDK source referenced by the design is - `wip/repos/pdp-control-plane/packages/management-api-sdk/src/`. -- Subagents: implementers AND reviewers on Opus (operator ruling, - rate limits). -- Reports to Will: plain English, full sentences, no invented - shorthand, no session-internal labels. Banned words: - "load-bearing", "smoking gun", "belt and suspenders", "gate". - Report only outcomes, decisions he must make, and changes to his - world — fold self-corrected slips silently. Bring questions to - decide, not decisions to ratify. STOP on any design-vs-code - contradiction the design does not anticipate; never improvise. - Do not use the question UI. -- Legacy exports in `packages/cli/src/auth` (listAuthWorkspaces, - switchAuthWorkspace, logoutAuthWorkspace, FileTokenStorage) must - keep working until slice S2d. -- Never commit while another agent has staged changes in this - worktree; when committing docs beside in-flight code, use - path-scoped commits (`git commit --only `). - -## 8. Wider state (context, not tasks) - -- Publishing: `@prisma/cli-engine@0.0.1` is on npm (operator's - manual initial publish); OIDC trusted publishing is configured; - the repo's publish machinery is prisma/prisma's verbatim at - lockstep `8.0.0-rc.1` (root package.json; engine's package.json - must stay at 8.0.0-rc.1). Merged `chore(release)` bump PRs - publish to `latest`; ordinary main pushes publish `-dev.N`. -- S2b (resources) and S2c (services) run with independent agents in - `.claude/worktrees/s2b-resources` and their own worktree; briefs - at `.drive/projects/prisma-cli-v8/assets/briefs/ - {s2b-handover,s2c-handover}.md`. Standing relays already sent to - them: no TTY reads in commands (`needs.interaction` + - browserWait), no hand-rolled consent flags (`--confirm ` - is engine-owned), `git connect` ports against browserWait. -- Operator question ledger + standing S2 rulings: - `.drive/projects/prisma-cli-v8/specs/s2-overview.md`. -- The normative engine interface commentary: - `.drive/projects/prisma-cli-v8/assets/engine/engine-interface-draft.ts` - (amended to rev 5 this session). - -Your first report to Will: confirm you read the design doc and this -brief, state the disposition of `4b006d1` per finding, and give -your plan for §4 step 2. Then execute. diff --git a/.drive/projects/prisma-cli-v8/assets/briefs/deployment-logs-http-endpoint.md b/.drive/projects/prisma-cli-v8/assets/briefs/deployment-logs-http-endpoint.md deleted file mode 100644 index 84553d50..00000000 --- a/.drive/projects/prisma-cli-v8/assets/briefs/deployment-logs-http-endpoint.md +++ /dev/null @@ -1,113 +0,0 @@ -# Brief: serve deployment logs over HTTP (for the platform/control-plane team) - -Written 2026-08-13, for an agent working in `pdp-control-plane` with -no prior context on the CLI project. Operator: Will Madden. - -## The ask, in one paragraph - -Add an HTTP variant of the deployment-logs endpoint: the same -log/terminal records `GET /v1/deployments/{deploymentId}/logs` -streams over its WebSocket today, served instead as newline-delimited -JSON over a plain authenticated `GET`, keeping the `cursor` resume -semantics and the in-band terminal record. This was agreed in -principle on 2026-08-12 (team answer, via Will): **"HTTP instead of -WebSockets would be acceptable, as long as we can add live streaming -at a later date."** This brief is the concrete version of that ask. - -## Why the CLI needs it - -The v8 Prisma CLI (repo `prisma/prisma-cli`, built on -`@prisma/cli-engine`) must ship a `service deployment logs` command — -the last unported command in its entire surface. The old -implementation reached around the engine: it took a raw token and let -`@prisma/compute-sdk`'s `streamLogs` build a `wss:` URL and set the -Authorization header itself. The CLI's credential model now forbids -that — credentials never reach command code; the engine holds them — -so the command was shelved rather than shipped -(see the CLI-side design doc below). - -The CLI's engine speaks HTTP only. Its `build logs` command already -consumes the sibling build-logs endpoint as a streamed HTTP response -(`packages/cli/src/v8/build/logs.ts` in prisma-cli — openapi-fetch, -`parseAs: "stream"`, NDJSON lines). If deployment logs serves the -same shape, the CLI needs zero new transport machinery and the -engine's WebSocket affordance stays unbuilt. That design exists and -is deliberately shelved as the future live-streaming path: - -- **CLI transport design (read §5 and §7):** - - §7's question 2 ("could this be plain HTTP?") is the question your - team answered yes to; §5 records why reconnection-across-segments - must survive in whatever transport ships. - -## What exists on your side (verified against the repo, HEAD `deb158e4e`) - -- **The route** is WebSocket-only. Spec text in - `packages/management-api-sdk/src/api.d.ts` under - `"/v1/deployments/{deploymentId}/logs"`: upgrades to a socket; - messages are `type: "log"` (text + byte metadata) or - `type: "terminal"` (end-of-segment with reconnect cursor); the - stream ends after 10 minutes; reconnect with `cursor`. -- **The interactor** `packages/interactors/src/compute/streamLogs.ts` - is a poll relay, not a push source: it polls Foundry's VM logs - every `DEFAULT_POLL_INTERVAL_MS = 1_000`, chunks tails at - `TAIL_CHUNK_SIZE = 10_240` (a Unikraft limit, per its comment), - runs `SEGMENT_DURATION_MS = 10 * 60 * 1_000` segments, defaults to - `DEFAULT_TAIL_LINES = 100`, holds a lease via - `computeLogStreamLease.repository.ts`, and maps Foundry's 424 (no - VM assigned / deallocated) explicitly. Record shapes: - `LogLine { type: "log"; text; byteStart; byteEnd }` and - `TerminalLine { type: "terminal"; kind: "end" | "error"; code; - message; retryable; cursor; details? }`. - Architecture: `docs/architecture/adrs/ADR-002-compute-log-relay-architecture.md`. -- **The template already in your repo**: - `packages/interactors/src/compute/streamBuildLogs.ts` feeds the - build-logs endpoint the CLI consumes over plain HTTP today. The - deployment-logs HTTP variant is that shape fed by `streamLogs`. - -Because the source is a 1-second poll relay, serving it over a -streamed HTTP response loses nothing real — there is no push -latency to preserve. Genuine live streaming stays a later upgrade, -which is exactly what the 2026-08-12 answer reserved. - -## Contract the CLI will consume (pin this before shipping) - -1. Authenticated `GET` (Authorization header; same credential as the - rest of the management API — no token in the URL, ever). -2. Response: newline-delimited JSON; each line one record with the - EXISTING shapes — `type: "log"` and `type: "terminal"` unchanged. - The CLI maps `terminal.kind`/`retryable` onto its settlement, so - the terminal record must arrive in-band, including on the routine - 10-minute segment end (`kind: "end"` with a `cursor`). -3. `cursor` query parameter resumes a segment chain; a `tail` - parameter for initial history if the WS contract exposes one - (interactor default is 100 lines). -4. The endpoint lands in the management-api OpenAPI spec so the - generated SDK (`@prisma/management-api-sdk`) exposes it — the CLI - consumes it through that SDK's types, not a hand-built URL. - Whether it is a new path or content negotiation on the existing - one is your call; the CLI only needs it addressable through the - generated client. -5. Still marked experimental is fine. Tell the CLI project when the - contract is pinned and when it deploys — that unshelves the - command. - -## CLI-side context, for pointers rather than action - -- **Project plan** (S8 section records the whole history of this): - -- **The slice contract that ruled logs out of the last slice** - (R-S8-5 records the 2026-08-12 answer verbatim): - -- **The open-items file** (entry "Left open by S8": the logs - follow-up and what unblocks it): - -- **The consumer template the CLI will copy**: - - (`ctx.api.GET(..., parseAs: "stream")`, line-parsed records). - -The shelved CLI handler (reviewed and green before shelving) is in -prisma-cli's `s2c-services` branch history; the CLI team restores and -reshapes it against your pinned contract, with fixture-driven tests — -so the CLI side can land before your deploy and light up when the -endpoint ships. diff --git a/.drive/projects/prisma-cli-v8/assets/briefs/init-and-shell-retirement.md b/.drive/projects/prisma-cli-v8/assets/briefs/init-and-shell-retirement.md deleted file mode 100644 index 6dffabb7..00000000 --- a/.drive/projects/prisma-cli-v8/assets/briefs/init-and-shell-retirement.md +++ /dev/null @@ -1,103 +0,0 @@ -# Brief: port `init`, then delete the old CLI - -Written 2026-08-11 for an agent with no prior context. The operator is Will Madden. Everything you need is here or in the documents named; where this brief summarises a document, the document wins. - -## What this slice is - -Repo `prisma/prisma-cli`. This is the last slice of the platform port. Two things happen in it, in this order: - -1. **Port the two commands that are left** — the `init` wizard and the `version` command — onto the new engine. -2. **Delete the old CLI**: the commander-based shell, the fixture/mock machinery, and everything that only existed to serve them. Then make the engine-based binary the one we ship. - -When this lands, `prisma-cli` runs entirely on `@prisma/cli-engine` and the old shell is gone. - -The slice contract is `.drive/projects/prisma-cli-v8/specs/s2d-init-and-retirement.md`. Read it in full before you touch anything — it is normative. One note: an earlier version of that contract contradicted itself mid-sentence on the shipped-binary cutover. That paragraph has been replaced with the ruling described below; if you are reading a stale copy, the question ledger in `.drive/projects/prisma-cli-v8/specs/s2-overview.md` is authoritative. - -## Do not start yet - -Two pull requests must land before you begin, because both add commands to the tree you are about to complete and delete code you are about to remove: - -- **#133** — the resources port, and the `database` → `postgres` rename. Awaiting first review. -- **#132** — the services port (`app` becomes `service`). Currently at changes-requested. - -Starting before these merge means rebasing a very large deletion across two moving branches. Wait. - -## Reading the config file from the shipped binary — already decided - -The CLI reads `prisma.config.ts` from the user's project, and the loader (`packages/cli-engine/src/config-loader.ts`) does a plain dynamic `import()` of that path. That works today only because everything runs under `tsx`. The binary we ship runs on ordinary Node, which cannot execute TypeScript, so as things stand the released CLI cannot read the config file it is built around. - -**Do not design a solution. Copy the two reference repositories, which solved this already and identically** (operator ruling, 2026-08-11): take `c12` as a dependency, import it dynamically at the call site, call `loadConfig({ name, cwd, configFile? })`, and declare `typescript` as a peer dependency. The shapes to copy are `packages/1-framework/3-tooling/config-loader/src/load.ts` in prisma/prisma, and the `cli` and `composer` packages in prisma/composer. - -Copy the dependency declarations, not just the call. `c12` evaluates TypeScript through `jiti`, which it declares as a peer, and that is the part that makes it work on plain Node. - -Two behaviours worth carrying over with it: `c12` discovers the config by `name`, walking up from `cwd`, with an explicit path passed as `configFile`; and prisma/prisma then verifies that the file `c12` actually loaded is the one that was asked for, treating a missing or empty config as a structured not-found error rather than an empty object. - -Nothing in this slice is blocked any more. - -## The work, in the order I would do it - -**1. Port `init`.** It is the hardest command in the product and the reason this slice is last. Today it is `packages/cli/src/controllers/init.ts`, about 1,100 lines. It is a wizard: project name, template and framework selection, a linking question, writing environment files, and an offer to install the agent skill. The contract pins the step list and the current defaults. - -Three things to get right. The prompts run on the engine's prompt surface, so the wizard must work interactively, under `--yes`, non-interactively, and when cancelled — that matrix is the acceptance bar. The files it writes are data, not rendering: the templates stay byte-asserted against what the current CLI produces. And the step that checks whether you are signed in must read auth state without forcing a login, the way `auth whoami` does, offering a sign-in next action instead — if that differs from the current behaviour, record the difference. - -**2. Port `version`.** The `--version` flag already exists on the engine. This is the `version` *command*, presenting version, node and platform, with a JSON serializer. Small, but user-visible, so it gets the same test matrix as anything else. - -**3. Delete the old CLI.** Do this only once every port is green, and as its own commit series so the diff is reviewable. The scale, as of today: - -| Directory | Files | Lines | -| --- | --- | --- | -| `src/shell` | 13 | 2,462 | -| `src/controllers` | 16 | 13,032 | -| `src/presenters` | 12 | 3,317 | -| `src/adapters` | 3 | 1,073 | -| `src/use-cases` | 5 | 713 | - -Not all of that dies. Controllers and presenters survive **only** where the ported commands still call them as an operation layer — the new command asks the old function to do the API work. Enumerate the survivors explicitly in the pull request; a survivor nobody listed is how this kind of deletion goes wrong. - -Also delete: the fixture machinery (`src/adapters/mock-api.ts`, `src/use-cases/**`, the fixture providers, and every `isRealMode` branch — seven files mention it or the `PRISMA_CLI_MOCK_FIXTURE_PATH` variable), all remaining fixture-mode tests, that environment variable itself, and `--trace`. - -**One knot worth knowing about before you pull on it.** `src/auth/errors.ts` still constructs `CliError`, the old shell's error class, and `src/v8/auth/errors.ts` maps those into structured errors. So the auth module depends on the shell it is meant to outlive. When the shell dies, either `CliError` moves somewhere durable or the auth operations throw structured errors directly and both mapping layers go. The second is cleaner. Decide deliberately rather than discovering it halfway through the deletion. - -**4. Cut the binary over.** `packages/cli/package.json`'s `bin` points at the engine entry, the build bundles the new tree, and the `prisma-v8` working name and its root script are deleted. Prove it by running the packed tarball on plain Node — not through `tsx`. - -**5. Add the grammar completeness check.** A build-time test asserting the mounted command tree is exactly the target grammar: every command in the inventory, minus the ruled removals, plus the ruled renames. The removals so far are `service build`, `service deploy` and `service run` — all superseded by Composer — and the mock-only login flags. `.drive/projects/prisma-cli-v8/assets/s2/command-inventory.md` is the inventory. - -**6. Consolidate the divergence record.** Every slice has been appending user-visible differences from the old CLI to `.drive/projects/prisma-cli-v8/assets/s2/parity-divergences.md`. Fold yours in and hand the whole document to the operator for sign-off. This is the last chance to catch a behaviour change nobody meant to ship. - -## What the engine gives you - -The engine surface changed substantially in the slice that just merged, so anything you read in an older document may be stale. As of now: a command reads `ctx.activeCredential()` for what the process is authenticated as; there is no `getCredentials` and no raw token available to a command; the credential manager has seven members; and the test harness seeds are `sessions`, `selectedWorkspaceId`, `credential` and `environmentCredential`. `packages/cli-engine/src/credential-manager.ts` and `context.ts` are the truth. - -For prompts specifically — the surface you will lean on hardest — read `packages/cli-engine/src/context.ts` (`PromptSurface`) and `packages/cli-engine/tests/interaction-affordances.test.ts`. Consent is deliberately not defaultable: `--yes` cannot satisfy it, and a destructive prompt with a token needs `--confirm ` non-interactively. - -## Verification - -Every one of these must exit 0 before you report anything as done: - -``` -pnpm --filter @prisma/cli-engine test -pnpm --filter @prisma/cli test -pnpm --filter @repo/cli-telemetry test -pnpm typecheck -pnpm lint -``` - -Engine tests that use `createTestCli` execute the **built** `dist`, so run the package's own `test` script, which builds first. Invoking vitest directly gives you stale results. This has caused wasted work twice. - -A passing test is not the same as a test that holds the behaviour down. For anything you claim is fixed, break the code deliberately and confirm the test fails. Two real defects in the last slice were found exactly this way, and one flaky-looking test turned out to be a genuine deadlock. - -## Process rules, non-negotiable - -- **Git identity.** You act as the `wmadden-electric` bot. Stage explicitly by path — never `git add -A`, never anything under `wip/` or `.drive/projects/prisma-cli-v8/specs/reviews/`. Commit with `git commit -s --trailer "Signed-off-by: Will Madden "`. Push only to the `bot` remote (`git@github-wmadden-electric:prisma/prisma-cli.git`). -- **Finish the job.** The deliverable is a pull request. Commit, push, and open it — draft if the work is partial, saying what is unresolved. Do not end with work sitting uncommitted. -- **Pull request text.** The operator's structure: a grounding example first (a real command run, before and after), then the decision, then the narrative building up, alternatives last. No internal process codes, no dispatch or round labels, no reviewer numbering. Assume the reader has none of your context — spell out any project shorthand rather than making them look it up. -- **Reports.** Plain English, full sentences, no invented jargon, no session-internal labels. Banned words: "load-bearing", "smoking gun", "belt and suspenders", "gate". Bring questions to decide, not decisions to ratify. Stop on any contradiction between the design and the code that the design does not anticipate — never improvise. -- **Subagents** on Opus, implementers and reviewers alike. - -## Wider state - -The engine publishes as `@prisma/cli-engine`. Nothing publishes automatically any more: a push to `main` publishes only when it changes the committed version, and `8.0.0-rc.1` is committed but deliberately **not** released. Do not bump the version and do not publish. - -Other agents work the two open pull requests independently. Do not touch their branches. - -The remaining unanswered questions are in `.drive/projects/prisma-cli-v8/specs/s2-overview.md`. The one most likely to reach you is whether an unauthenticated command should still launch a browser login automatically the way the old CLI did — the port fails with a sign-in error instead. It is built to that default and unratified. diff --git a/.drive/projects/prisma-cli-v8/assets/briefs/orm-toolchain-engine-peer-handover.md b/.drive/projects/prisma-cli-v8/assets/briefs/orm-toolchain-engine-peer-handover.md deleted file mode 100644 index 13d96a25..00000000 --- a/.drive/projects/prisma-cli-v8/assets/briefs/orm-toolchain-engine-peer-handover.md +++ /dev/null @@ -1,54 +0,0 @@ -# Handover brief — @prisma/orm-toolchain declares the engine as an exact peer - -Written 2026-08-13 for an independent agent with NO prior context. The operator is Will Madden ("the operator"). Where this brief summarizes a document, the document wins. This is the prisma/prisma share of the strategy whose composer share is `composer-cli-split-handover.md` in this directory; the two can land independently. - -## 1. Read first, in this order - -1. **ADR 0004** in prisma-cli: `docs/architecture/adrs/0004-engine-version-pinning.md` (on `main`, or on the `claude/composer-cli-split-brief` branch if not yet merged). It is the normative strategy record: one engine per install, product CLI packages declare `@prisma/cli-engine` as an **exact peerDependency** the shell satisfies, product libraries carry no engine relationship, ranges are post-GA only. -2. This project directory in prisma-cli (`.drive/projects/prisma-cli-v8/`): `specs/s6-conformance.md` §1 and §5 for the defect class and the 2026-08-12 rulings. -3. In prisma/prisma (`/Users/wmadden/Projects/prisma/prisma`): the repo's agent guidance (CLAUDE.md / AGENTS.md), `docs/architecture docs/adrs/ADR 242 - Public npm surface...` (the shell/publish-surface model — note the directory name contains a literal space), and `scripts/check-conformance.mjs` (the conformance checks landed 2026-08-12 as PR #29998). -4. **Always `git fetch origin main` before reading any repo** — local checkouts on this machine are routinely stale, and that has produced wrong conclusions twice. Work in a fresh worktree off prisma/prisma `origin/main`. - -## 2. The work - -`@prisma/orm-toolchain` is already the ORM's dev/CLI package — applications never install it directly (they reach the vite plugin through `@prisma/orm-postgres/vite-plugin-contract-emit`, a forwarded export), so no package split is needed. The change is the dependency field: - -- `@prisma/orm-toolchain`'s published manifest moves `@prisma/cli-engine` from `dependencies` to **`peerDependencies` with the same exact version**, keeping a `devDependency` at that version so the workspace and its tests resolve it. -- **The manifest is generated, not hand-edited.** ADR 242's shell-build derives the published packages' manifests from the internal packages they bundle (`packages/0-config/tsdown/shell-build.ts`, `@internal/publish-surface` at `packages/0-shared/publish-surface/`). There is precedent for hand-declared peers — the `handWrittenPeers` set in shell-build already carries `typescript`, and the packed manifest today shows `typescript` and `vite` as peers. Find the sanctioned route for declaring the engine as a peer of the toolchain shell (likely: the engine joins the hand-written peers for that shell, and `@internal/cli`'s own manifest keeps the engine as a devDependency). Do not fight the generator; if the generator cannot express this, that is a STOP, not an improvisation. -- `@internal/cli` (`packages/1-framework/3-tooling/cli/`) currently carries `"@prisma/cli-engine": "0.0.9"` in `dependencies`. Decide with the generator's rules where it belongs after the change (devDependencies is the expectation, since the published artifact no longer ships the engine as a dependency); `test/integration`'s own pin is private/dev usage and stays. - -## 3. The checks follow the strategy (same repo, same PR) - -`scripts/check-conformance.mjs` currently asserts the engine pin is exact and identical across the manifests that declare it in `dependencies`. Under ADR 0004 it becomes: - -- The toolchain's packed manifest declares the engine in **`peerDependencies`, exact, and NOT in `dependencies`** — a `dependencies` entry reappearing is a finding. -- Every remaining engine reference in the repo (internal cli devDependency, integration tests) agrees with the peer's version. -- The packed-output import-purity check keeps treating a bare `@prisma/cli-engine` import in `cli.mjs` as satisfied — peers count as consumer-installed (they already do in that script's field set; verify, don't assume). -- `scripts/check-publish-deps.mjs` must still pass — read its rules before changing any manifest field; its `@internal/*` exact-pin logic must not start flagging the new arrangement. - -Sandbox note for the tarball leg: with the engine as a peer, `npm install` (v7+) auto-installs it from the registry — but the exact peer version may be UNPUBLISHED at check time (that is the point of the tandem release). The conformance sandbox already supplies unpublished workspace siblings through computed version-qualified `file:` overrides; extend that mechanism to satisfy the engine peer from a packed/local source if needed, and prove the `prisma-next` bin still starts in the sandbox. - -## 4. What you do NOT do - -- No changes in prisma-cli or composer. The shell's side (peer-satisfaction check, exception-list deletion) is a separate follow-up there. -- No version-range peers. Exact only; ranges are a post-GA decision the operator has not made. -- No engine version changes. Whether the engine decouples from the shell's lockstep is marked OPEN in ADR 0004. - -## 5. STOP — surface before implementing - -- **STOP-1**: the generator route for the peer (handWrittenPeers vs something else), if shell-build's model resists it. -- **STOP-2**: if moving `@internal/cli`'s engine dep to devDependencies breaks how shell-build computes the toolchain's dependency set, surface the options rather than picking one. -- Anything that would change `@prisma/orm-toolchain`'s export map or bin — out of scope, surface it. - -## 6. Process rules (operator-enforced, non-negotiable) - -- Git identity: the `wmadden-electric` bot. Commit `git commit -s --trailer "Signed-off-by: Will Madden "`; end commit bodies with a `Co-Authored-By:` line naming your model. Push ONLY to `git@github-wmadden-electric:prisma/prisma.git`. Never force-push. Stage by path. -- pnpm only, never npm/npx — except inside the conformance sandbox, which is deliberately npm. -- Tests first; the repo's io-seam style (`scripts/check-conformance.test.mjs` is the model); no module mocking. New/changed script tests must be in the root `test:scripts` list or they never run. -- NEVER hard-wrap markdown prose. Plain-English reports; banned words: "load-bearing", "smoking gun", "belt and suspenders", "gate". -- One DRAFT PR, base `main`. Description: grounding example first (a real install/resolution before/after), then the decision (cite ADR 0004), then the narrative, alternatives last. Reference prisma/prisma#29998 and prisma-cli#161 as the sibling conformance work. -- Verification, each as the command's own exit code: `pnpm test:scripts`, `node --test scripts/check-conformance.test.mjs`, `pnpm check:conformance` end to end (must exit 0), `pnpm check:publish-deps`, and the touched packages' suites. - -## 7. Your first report - -Confirm you read ADR 0004 and this brief; state STOP answers you need; list any §2/§3 fact that no longer holds on origin/main (re-verify, do not trust this snapshot); then your plan. diff --git a/.drive/projects/prisma-cli-v8/assets/briefs/s1-handover.md b/.drive/projects/prisma-cli-v8/assets/briefs/s1-handover.md deleted file mode 100644 index 745cd6d9..00000000 --- a/.drive/projects/prisma-cli-v8/assets/briefs/s1-handover.md +++ /dev/null @@ -1,42 +0,0 @@ -# S1 handover brief — continue slice s1-engine-vertical - -Written 2026-08-09 for the next orchestrating agent. The prior session halted after dispatch D5 due to rate limits. - -## What this is - -Drive project `prisma-cli-v8`, slice S1: build `@prisma/cli-engine` and prove it with one ported command (`auth whoami`). Read, in order: `../../spec.md` (project), `../../plan.md` (slices), `../../specs/s1-engine-vertical.md` (slice contract), `../../plans/s1-engine-vertical.md` (dispatch plan — six dispatches D1–D6). The v8 interface draft at `../engine/engine-interface-draft.ts` is NORMATIVE: where implementation would contradict it, stop and ask the operator (Will); never improvise. The compile-verified typing claims from the design review rounds live as the permanent type-test suite in `packages/cli-engine/tests/` (review artifacts are never committed). - -## State at handover - -Branch `s1-engine-vertical` (off `cli-engine-requirements`, the living decision-record branch of PR #128). All work committed and pushed. Slice PR is open as a DRAFT targeting `cli-engine-requirements`. - -- D1 done (4cc3e14): package scaffold + `./protocol` subpath (Diagnostic, CliStructuredError, Result, NextAction). -- Orchestrator fix (6241d78): `.drive/**` excluded from biome — the committed design drafts broke root lint. -- D2 done (0df7a10): full v8 type surface in `src/index.ts` + permanent type-test suite from the review record; stale-`@ts-expect-error` fails the package test run. -- D3 done (78383c6): execution engine on exact-pinned `@stricli/core@1.3.0` (fully internal, fake process injected); parse → needs → context → handler → `ctx.present` (active-format-only materialization) → envelope → exit code; formats/log-levels; StreamEvent framing; real `createTestCli` harness. -- D4 done (a6b0a0b): prompts (defaults + `--yes`, consent undefaultable), needs.interaction, full event vocabulary + rendering, requireDependency, session/server settlement, signal exit codes 130/143/3. -- D5 done (6c1e845): config loader. cwd-only discovery of `prisma.config.ts` (test-pinned; the draft is silent on walking up), plain dynamic `import()` evaluation (no new deps; the bin must run under a TS-capable runtime like tsx for user config evaluation), `defineConfig` stamps `$prismaConfig: 1` (exported as `PRISMA_CONFIG_VERSION`), Prisma 7 fail-early diagnostic pinned as `CLI.CONFIG_MISSING_MARKER` (plus `CLI.CONFIG_UNREADABLE`, `CLI.CONFIG_INVALID`); `needs.config` validation wired in `checkNeeds`, `ctx.config` carries the validated section value; validator throw settles as `CLI.INTERNAL_ERROR` exit 1 with the handler proven not to run. `loadConfig(cwd)` and `defineConfig` sit on the main entry pending S3's naming ruling. D6 wiring: `config: await loadConfig(process.cwd())` in the real Runtime. -- D6 NOT started: `prisma-v8` bin + `auth whoami` port + slice e2e + parity-divergence list. Full dispatch spec is in `../../plans/s1-engine-vertical.md` § D6. Grounding: the whoami vertical is `runAuthWhoAmI` (packages/cli/src/controllers/auth.ts:114) over `createAuthUseCases().whoami` (packages/cli/src/use-cases/auth.ts), presenters in packages/cli/src/presenters/auth.ts, token storage in packages/cli/src/adapters/token-storage.ts. - -Verify state with: `pnpm --filter @prisma/cli-engine test` (all green at handover: 93 tests after D4), root `pnpm lint` (exit 0), `pnpm --recursive exec tsc --noEmit` (exit 0). - -## Open items for the operator (do not resolve unilaterally) - -1. D3 interpretation: in human non-quiet mode the engine renders human Blocks only; the materialized `stdout` presentation lines are written only under `--quiet` (they would duplicate the blocks). The draft honors materialization exactly; the rendering-rule reading is unruled. Revisit during D6 whoami parity. -2. D4 interpretation: remediation events render as nextActions at settlement in human mode (not live in the transcript); json streams them live as frames. -3. Diagnostic severity stays `error|warn|info`; the trim to two awaits the ADR 239 amendment (project slice S4). -4. D5: a file-level config diagnostic (unmarked/unreadable `prisma.config.ts`) fails EVERY command, including ones with no config need — the draft's `LoadedConfig` comment says so and won over the 1a foundation design, which says the opposite. Needs an operator ruling before S3. -5. D5: diagnostics returned on a SUCCESSFUL `SectionValidation` (warnings) are currently dropped; the draft doesn't say where they go. - -Put all of these on the slice PR's parity-divergence list for Will's review. - -## Process rules (non-negotiable) - -- Implementer subagents run on Fable; reviewer subagents run on Opus. -- After D6: run the slice review loop (architect + principal-engineer reviews per the drive process), fix findings, then mark the PR ready. Any operator-ruled draft amendments during the slice must be reflected in `../engine/engine-interface-draft.ts` before the PR leaves draft (slice acceptance box). -- Commit as the bot: `git commit -s --trailer "Signed-off-by: Will Madden "`, body ends `Co-Authored-By: Claude Fable 5 `. Stage explicitly, never `git add -A`. Push to `origin` of this clone (it IS the bot remote: `github-wmadden-electric:prisma/prisma-cli.git`). -- Plain English reports (ISO 24495-1). No invented jargon. Don't hand Will decisions to ratify — surface questions and facilitate. - -## Working copy - -The prior session's clone: `wip/repos/prisma-cli` inside the prisma/prisma worktree `.claude/worktrees/dependabot-prs-triage-13c8f8`. Any fresh clone via the bot SSH alias works equally; the branch state on origin is complete. diff --git a/.drive/projects/prisma-cli-v8/assets/briefs/s2b-engine-requests.md b/.drive/projects/prisma-cli-v8/assets/briefs/s2b-engine-requests.md deleted file mode 100644 index c8e17f6b..00000000 --- a/.drive/projects/prisma-cli-v8/assets/briefs/s2b-engine-requests.md +++ /dev/null @@ -1,66 +0,0 @@ -# Engine changes S2b needs — brief for the operator - -Written 2026-08-10 by the s2b-resources orchestrator. Two changes to `packages/cli-engine`, both small and additive. S2b may not touch the engine, so these are for the operator to land on `s2a-foundations`, which is this slice's base and merge target. - -Everything else in S2b is finished and green. These two are the only thing blocking `git connect` step 5 — the wait for a GitHub App installation — and with it the slice's closure review and pull request. - -## Why these are needed at all - -`d3-bucket-branch-git.md` §3.8 was written before the engine had a browser-wait helper. The helper that landed, `ctx.prompt.browserWait`, is narrower than the design assumed, so three pinned facts had nowhere to go. One turned out to be a design error, one is fixed by change 1 below, and one is being dropped deliberately (see "Not requested"). Change 2 is a separate defect the port surfaced. - -## Change 1 — `browserWait` should accept a poll interval - -**Problem.** Legacy `git connect` reads `PRISMA_CLI_GITHUB_INSTALL_POLL_INTERVAL_MS` (default 2000ms) and polls on it. `BrowserWaitRequest` has no interval field, and the engine polls on a private module constant fixed at 1000ms, so the environment variable has nowhere to go. The companion `PRISMA_CLI_GITHUB_INSTALL_TIMEOUT_MS` maps cleanly onto the existing `timeout`; only the interval is affected. - -**Change.** Add an optional interval to the request and honour it. - -- `packages/cli-engine/src/context.ts:116` — `BrowserWaitRequest` gains: - - ```ts - /** How often to call poll. Defaults to the engine's own interval. */ - readonly interval?: number; - ``` - -- `packages/cli-engine/src/execution/prompts.ts:431` — the delay call uses `interval ?? BROWSER_WAIT_POLL_INTERVAL_MS` instead of the bare constant. The constant stays as the default; nothing else moves. - -**Compatibility.** Additive and optional, so every existing caller keeps the 1000ms behaviour. - -**Test.** `packages/cli-engine/tests/interaction-affordances.test.ts:292` already has a `prompt.browserWait` block. One case there: a request with an explicit interval polls on that interval rather than the default. Note the test harness stubs `delay` to a no-op (`src/testing.ts:161`), so assert the value passed to `delay`, not elapsed wall-clock time. - -**What S2b does with it.** `git connect` reads both environment variables from `ctx.env` with the legacy positive-integer parsing and passes them as `interval` and `timeout`. This also makes the design's own pinned test case writable as specified — it asks for the interval set to 1ms to prove the poll loop. - -## Change 2 — `NextAction` needs a kind that means "open this URL" - -**Problem.** This is a correctness defect in the agent-facing envelope, not a parity question. The legacy errors `REPO_INSTALLATION_REQUIRED` and `REPO_NOT_ACCESSIBLE` carry the raw GitHub App install URL in their `nextSteps` (`controllers/project.ts:2208` and `:2229`). Conventions §4 turns every `nextSteps` string into a `run-command` action. The result is an action instructing the consumer to execute `https://github.com/apps/...` as a shell command, which fails if anything takes it literally. None of the four existing kinds — `run-command`, `user-choice`, `edit-file`, `done` — means "open this URL", so the information cannot currently be expressed. - -**Change.** Add the kind and a field to carry the URL. - -- `packages/cli-engine/src/protocol.ts:28` — add `"open-url"` to the kind union. -- `packages/cli-engine/src/protocol.ts` — `NextAction` gains `readonly url?: string`, alongside the existing `command`. -- `packages/cli-engine/src/execution/rendering.ts:150` — `renderNextAction` currently appends `: ${action.command}` when a command is present. Make it fall back to `action.url`, so an `open-url` action renders its URL rather than just its label. - -**Blast radius: none beyond those three edits.** I checked every engine consumer of `nextActions`. Nothing switches on `kind`: `settlement.ts` passes the array straight into the envelope, and `renderNextAction` is the only renderer. The json envelope carries actions through untouched, so the new kind reaches machine consumers for free. - -**Test.** `packages/cli-engine/tests/protocol.test.ts` for the envelope shape, and one rendering case proving an `open-url` action prints its URL. - -**What S2b does with it.** The git mapper stops sending URLs through the `run-command` path and emits `{ kind: "open-url", label, url }`. Separately, the postgres plan-limit error currently smuggles its upgrade URL into a `user-choice` reason; that can move onto the new kind whenever you want the consistency, but it is not part of this request and I would not change it inside this slice. - -## Not requested, deliberately - -**`browserWait` returning whether the browser opened.** I raised this and then withdrew it; recording the reasoning so nobody re-opens it. - -Legacy branched on an `opened` boolean in two places: which of two wait sentences it printed, and which fix text `REPO_INSTALLATION_REQUIRED` carried. `announceUrl` still computes the value and `browserWait` discards it, so exposing it would be about as small as change 1. - -It is not worth it. Both legacy branches existed to solve one problem — making sure the user has the install URL when no browser opened, which is why legacy printed the raw URL on its own line only in the not-opened branch. That problem cannot occur in v8: `rendering.ts:50` writes the endpoint event's URL to stderr unconditionally, and json mode receives it as a frame, so the URL is always present regardless of what the browser did. What remains is tone. On top of that the signal is weak — the runtime's opener is `open(url)` from the npm `open` package, so a true result means the OS accepted the handoff, not that a browser window appeared. - -S2b therefore takes the browser-opened wording and fix text, drops `meta.opened`, and records the divergence. Operator ruling, 2026-08-10. - -**Progress events during the wait.** §3.8 pinned a three-event sequence — the URL, then "waiting", then "connected". No engine change is needed, because the design was wrong: legacy prints one wait line before the poll loop and nothing during it (fact sheet §6, "no status re-print during polling"), and there is no "connected" line either. The engine's single `endpoint` event is exactly the legacy shape. The pinned sequence is struck from the design as an error rather than recorded as a loss. - -## After they land - -1. You push both to `s2a-foundations`. -2. I rebase `s2b-resources-work` onto the new tip and re-run the full check. -3. I pin the four outcomes in `d3-bucket-branch-git.md` §3.8 — interval restored, single announcement event, `opened` dropped, `open-url` for the install URL. -4. The implementer writes step 5 and the four wait-path test cases the design enumerates: installation-required, not-accessible, poll-then-found, and poll timeout. -5. Review round on step 5, then the closure dispatch — divergence consolidation, the architect and principal-engineer review loop, and the pull request onto `s2a-foundations`. diff --git a/.drive/projects/prisma-cli-v8/assets/briefs/s2b-midslice-handover.md b/.drive/projects/prisma-cli-v8/assets/briefs/s2b-midslice-handover.md deleted file mode 100644 index bde4fde6..00000000 --- a/.drive/projects/prisma-cli-v8/assets/briefs/s2b-midslice-handover.md +++ /dev/null @@ -1,131 +0,0 @@ -# S2b mid-slice handover — resume dispatch D2 - -> **SUPERSEDED 2026-08-11. Do not follow the resume sequence below.** -> The slice is finished: all 31 commands are built, mounted, tested and -> recorded, every dispatch review round and the closure architect and -> principal-engineer passes are closed, and the branch is merged up to -> `s2a-foundations` with CI green. The work is PR #133. This file is -> kept as the record of where the slice stood when it changed hands -> mid-D2, and of the rulings in force at that point — several of which -> were later amended. For the current state read, in this order: the -> divergence record `../s2/parity-divergences-s2b.md`, the review -> artifact `../../reviews/code-review.md`, and the amendment blocks in -> `../../specs/s2b-design/conventions.md` and `d3-bucket-branch-git.md`. - -Written 2026-08-10 by the outgoing orchestrating agent, halted by the -operator (rate limit). Successor: you are the orchestrator for Drive -slice s2b-resources under /drive-process. The operator is Will. - -## Where everything is - -- Branch `s2b-resources-work`, pushed to the bot remote - (`git@github-wmadden-electric:prisma/prisma-cli.git`), based on - `origin/s2a-foundations` @ 7716e8b (PR #130's branch — rebase onto - its tip whenever it moves; the eventual PR sets `s2a-foundations` - as its BASE, not main; operator ruling). -- Normative stack, in precedence order: slice contract - `specs/s2b-resources.md` (R-S2b-1..10) → `specs/s2b-design/ - conventions.md` → `specs/s2b-design/d{1,2,3}-*.md` → the verbatim - fact sheets in `specs/s2b-design/facts/`. The design docs contain - every post-ratification amendment; where a per-command section - conflicts with a later ruling, the ruling note in the same file - says so explicitly (e.g. d2 §3's CONSENT SUPERSESSION preamble). -- Review artifact: `reviews/code-review.md` (scoreboard, findings, - round notes). Divergence record: - `assets/s2/parity-divergences-s2b.md` (NEVER edit the shared - `parity-divergences.md` — the auth stream owns it). - -## State - -- **D1 (project group, 11 commands): DONE.** Reviewer verdict - SATISFIED (round 2). Includes the slice template: exported spec - constants in `v8/cli.ts`, `tests/v8-mount-coverage.test.ts`, - `v8/resources-shared/workspace.ts`. -- **D2 (postgres group, 11 commands): ~90% done, halted mid-tail.** - Commit `b9c4b81` holds all 11 commands + `tests/v8-postgres.test.ts` - (implementer's heartbeat says the gate was green there). Commit - `bf6d434` is an honest WIP: legacy `database.test.ts` fixture cases - and `database-plan-limit.test.ts` deleted, D2 divergence section - PARTIAL, gate NOT verified on that state. Remaining D2 work: - finish the D2 divergence section + 11 conformance rows, re-run the - full gate, then the D2 review round, findings fixed. -- **D3 (bucket 6 + branch 1 + git 2): not started.** Design complete - in `d3-bucket-branch-git.md`. No reordering needed; nothing blocked - — consent tokens, `ctx.openUrl`, `prompt.browserWait`, - `needs.interaction` all exist on the current base. -- **D4 (closure):** divergence consolidation check, review loop - (architect + principal-engineer per the handover brief), PR opened - non-draft, ≥1k LOC, description per the operator's ruled structure. - -## Verification gate (every dispatch, pnpm exit codes) - -engine test · cli test · cli-telemetry test · typecheck · lint. The -once-failing `packages/cli-engine` lint should be fixed on the -current base — if root lint still fails there, report to the -operator; that package is a hard no-touch boundary. - -## Process rules in force (operator-ruled; violations were rejected hard) - -1. Zero creative freedom for implementers: unpinned fact → STOP and - surface to the operator; never improvise. Orchestrator pins - design amendments in the docs BEFORE re-delegating. -2. Commands and helpers NEVER read TTY/CI/process state. - Interactivity gating = `needs: { interaction: true }` (git - connect declares it — divergence: all non-interactive runs fail - early). Browser flows = `ctx.openUrl` / `prompt.browserWait`. -3. Consent: engine-owned tokens only. No per-command confirm flags. - `ctx.prompt.consent(, { token: - })`; shared repeatable `--confirm` grants - non-interactively; matrix per conventions §5. -4. Auth: `needs.credentials` + `ctx.api` + `ctx.session()` (via - `resolveActiveWorkspace`) only. No auto-login (ledger Q1). -5. Persistent subagents: ONE implementer + ONE reviewer for the - whole slice, resumed across dispatches, both currently Opus - (operator override for rate-limit headroom; brief originally said - Fable implementer). Spawn fresh only if the prior transcript is - inaccessible — then have them re-read the design stack first. -6. Never touch: `packages/cli/src/v8/auth/**`, `packages/cli/src/ - auth/**` (importing its public index is fine), `packages/ - cli-engine/**`, publish workflow, versioning scripts. Engine gaps - → STOP to operator (the auth stream on s2a-foundations lands - engine changes). -7. Commits: explicit staging; `git commit -s --trailer - "Signed-off-by: Will Madden "`; body ends - `Co-Authored-By: Claude Fable 5 `; push to - the bot remote only. Legacy source edits: `export` keywords at - most, each listed in the PR description. -8. Reports to the operator in plain English, full sentences, no - invented jargon, no compressed arrow-chains; banned words: - "load-bearing", "smoking gun", "belt and suspenders", "gate" - (say "check"/"requirement"). Bring questions to decide, not - decisions to ratify. - -## Open items - -- D2 tail (above), then D3, then D4. -- Operator ratifies via the divergence list at PR review: - `PROJECT.ENV_PREVIEW_DEFAULT_MISSING` (invented warn code), - `PROJECT.LOCAL_STATE_WRITE_FAILED` reuse at warn severity for - pin-cleanup warnings, the `workspaceName`→id fallback, the - interaction-required divergence on git connect, the - `PROJECT_AMBIGUOUS` hardcoded `app deploy` nextStep quirk (ported - verbatim). -- Watch `origin/s2a-foundations`: rebase onto new tips at clean - points (implementer does it mid-dispatch, orchestrator between - dispatches). Conflicts concentrate in `v8/cli.ts` (keep both - streams' entries; auth entries exactly as the incoming side) and - lockfiles. -- S2a stream defect handed over verbally: `performLogin` still does - internal TTY detection (contradicts ruling 2); belongs to the auth - stream, not this slice. - -## Resume sequence for the successor - -1. Read the normative stack + `code-review.md` + the D2 fact sheet. -2. Resume (or respawn per rule 5) the implementer with: finish the - D2 divergence section/conformance rows, run the full gate, - report. -3. Reviewer round on D2 (scope shape: see the D1 round briefs echoed - in `code-review.md` round notes). -4. D3 dispatch per `d3-bucket-branch-git.md`, then D4 closure per - `plans/s2b-resources.md`. diff --git a/.drive/projects/prisma-cli-v8/assets/briefs/s2c-continuation.md b/.drive/projects/prisma-cli-v8/assets/briefs/s2c-continuation.md deleted file mode 100644 index 2df85fd5..00000000 --- a/.drive/projects/prisma-cli-v8/assets/briefs/s2c-continuation.md +++ /dev/null @@ -1,183 +0,0 @@ -# S2c continuation brief — pick up slice s2c-services mid-flight - -Written 2026-08-10 by the outgoing S2c orchestrating agent, halted by a -rate limit. Reader: a fresh orchestrating agent with NO prior context. -The original handover brief is -`assets/briefs/s2c-handover.md` (on `s2a-foundations`, commit 00b4207 — -NOT in this branch's tree; read it via -`git show 00b4207:.drive/projects/prisma-cli-v8/assets/briefs/s2c-handover.md`). -Read it FIRST; this brief only records what changed since, what is -done, and what remains. Where they disagree, this brief wins (it is -newer and carries operator rulings the original predates). - -## 1. Where the work stands - -Branch `s2c-services`, 13 commits, based on `bot/s2a-foundations` @ -`9ffbb01` (NOT `s2b-resources` — see §3). Pushed to the bot remote. -Working state at handoff: dispatches D1, D2 (with an -engine-affordances migration), and D3 are implemented and verified -green; D1 and D2 are reviewer-SATISFIED; **D3 is implemented but -UNREVIEWED** (the reviewer was killed before writing anything — the -review artifact `.drive/projects/prisma-cli-v8/reviews/code-review-s2c.md` -has no D3 section). D4 has not started. - -Verification, all green at handoff (judge by pnpm's own exit codes): -`pnpm --filter @prisma/cli test` (72 files, 977 tests) · -`pnpm --filter @prisma/cli-engine test` (234, package untouched by us) · -`pnpm typecheck` · `pnpm lint`. - -Commands ported (16 of 24): D1 `service -build|show|open|list-deploys|show-deploy`, `service domain -add|show|remove|retry|wait`; D2 `service deploy|promote|rollback|remove`; -D3 `service logs`, `build logs`. All under `packages/cli/src/v8/service/` -(plus `v8/build/logs.ts`), mounted in `packages/cli/src/v8/cli.ts`. -Tests in `packages/cli/tests/v8-service-*.test.ts` + -`v8-build-logs.test.ts`, shared harness `v8-service-testkit.ts` -(`makeServiceCli`: seeds the credential manager by default, `openUrl` -spy, `rawTokenSeed` for the log-stream tests only). - -Remaining scope: D4 = `agent install|update|status`, `feedback`, -divergence-file completion, the fixture-test decision, the pre-PR -review loop, PR open. See §5. - -## 2. Operator rulings received since the original brief (BINDING) - -1. **Consent is engine-owned.** No per-command consent-skip flags, - anywhere. `prompt.consent(question, {token})` — token is the natural - noun of the action; interactive rendering is type-to-confirm; the - global repeatable `--confirm ` grants non-interactively on - exact match (each value consumed once per run). `--yes` remains - "accept defaults" and NEVER grants consent. NOTE the semantics the - engine actually implements (tests pin this): `--confirm` grants only - on the non-interactive branch (including under `--yes`); an - interactive session type-to-confirms even when `--confirm` is - passed. Landed as engine commit `6bb8452`. Our tokens: `service - remove` → service name, deploy's production consent → target - service name, `service domain remove` → hostname. -2. **Q2 is ruled: `app run` is DROPPED**, superseded by Composer's - commands. No v8 port, no exit-code passthrough mechanism, no legacy - carve-out in S2d (the shell deletion takes it). D4 records the drop - as a divergence entry. Do not port `service run`. -3. **Base change:** the branch was rebased onto `s2a-foundations` - (operator instruction — S2b landed nothing we depend on). The PR - opens with base `s2a-foundations` (stacks on PR #130), retargeting - to `main` when that merges. The original brief's `s2b-resources` - geometry is obsolete. -4. **Implementer model is Opus** (operator instruction, rate-limit - headroom), not Fable as the original brief says. Reviewers: Opus. - -## 3. Branch geometry and git mechanics - -- Base: `bot/s2a-foundations` @ `9ffbb01` (credential-manager rework) - on `6bb8452` (consent/openUrl/browserWait affordances). -- Remote: push ONLY to `bot` - (`git@github-wmadden-electric:prisma/prisma-cli.git`). Identity is - the `wmadden-electric` bot (env comes from `~/.zshenv` in agent - shells). Commits: stage files EXPLICITLY by path (never `-A`/`-u`); - `git commit -s --trailer "Signed-off-by: Will Madden - "` with body's last line - `Co-Authored-By: Claude Fable 5 `. -- Merge down from `bot/s2a-foundations` when it moves (the S2a stream - is active). If the test-harness credential-seeding surface changes - again, adopt the new shape during the merge. -- `wip/**` is never staged. The worktree may be a shallow clone — - if history looks parentless, `git fetch bot --unshallow`. - -## 4. Escalated engine gaps (with the operator; do not work around) - -Interims are shipped, recorded as divergences, and safe to leave until -the operator lands engine changes; on the merge-down that brings a -fix, adopt it and delete the interim + its divergence entry. - -1. **`service logs` has no sanctioned raw token under the shipping - (manager-wired) runtime.** Interim: `ctx.getCredentials()` else - settle `SERVICE.LOG_STREAM_CREDENTIALS_UNAVAILABLE`. The command - works only under the manager-less fallback runtime until the engine - exposes a token accessor or (recommended to operator) a - pre-authenticated log-stream client alongside `ctx.api`. NEVER read - the token file or env var from command code. -2. **Streams cannot settle exit 1.** `build logs` on a terminal - `error` record streams everything then settles `BUILD.FAILED` - (exit 2, carries message/code/retryable/cursor + a `--cursor` - resume action); legacy exited 1. Waiting on: stream termination - status, a documented exit 1, or a ruling that it becomes a result - command. -3. **`--db`/`--no-db` tri-state is not expressible** (stricli - auto-negates booleans). Interim: `--db` requests; absent and - `--no-db` both take the signal-driven prompt (default No). Legacy's - "both flags → USAGE_ERROR" and "--db needs --yes non-interactively" - checks are gone. -4. **`prompt.text` has no validator**, so deploy's first-run Project - name typo fails the run instead of re-asking (legacy re-asked via - clack validate). Recorded beside the --db gap. -5. **No handler-facing injectable clock.** `service domain wait` - polls `setTimeout` + `ctx.signal`, interval from - `PRISMA_CLI_DOMAIN_WAIT_POLL_MS`. Accepted for now. - -## 5. What D4 must do (next dispatch) - -1. **Re-run the D3 review first** (fresh reviewer round, before or - alongside D4 implementation): diff `a0b0ea6..55efe06`, dimensions - per the round briefs recorded in `reviews/code-review-s2c.md` - round notes. D3's specifics: channel routing per record, - `skipSelection` on `resolveServiceReadState` (added so `service - logs --deployment ` never prompts — verify no regression on - the other read commands), json framing, the two interims above - implemented exactly as described. -2. Port `agent install|update|status` (local child-process commands, - spawn skills-cli via pnpm dlx/bunx/npx; no auth, no API; `--dry-run` - → `{status:"would-install", command}`) and `feedback` (no auth; - POST to the feedback URL, env-overridable, 3s timeout; legacy had - no JSON serializer — engine envelope is a divergence; the - crash-recovery flow pre-fills this command, keep an equivalent - hook). Inventory §4 entries are normative. -3. Decide (operator default: dies with the shell) the deploy - agent-setup prompt dropped in D2 — currently a recorded divergence. - The operator was told and did not object; record it as final unless - overruled. -4. Divergence file completion: add the `app run` drop entry (ruling - §2.2). File: `assets/s2/parity-divergences-s2c.md` (D1/D2/D3 - sections exist; keep the format). -5. Fixture-test deletion: the standing recommendation (accepted by - the reviewer across D1–D3) is to DELETE NOTHING in S2c — the - legacy `app` shell still ships until S2d and deleting its tests - would leave live code uncovered. The slice contract says otherwise; - this is flagged for the operator at PR time. Restate it in the PR - description rather than silently deviating. -6. Slice review loop before the PR leaves draft: architect + - principal-engineer personas, model Opus, per the drive process. - Fix findings, then push and open the PR: base `s2a-foundations`, - ≥1k LOC (already cleared: ~7k added), description structure per - the original brief §8 (grounding example first — a real command - run before/after — then decision, narrative, alternatives last; no - internal dispatch labels; plain English). - -## 6. Process notes for the incoming orchestrator - -- Drive process, build-slice loop: one persistent implementer + one - persistent reviewer (both Opus), resumed across rounds; findings - live in `reviews/code-review-s2c.md` (scoreboard + findings log + - round notes; orchestrator-owned sections marked). All finding - severities block SATISFIED. Reviewer is read-only on code. -- Operator communication: plain English, full sentences, no invented - shorthand or ledger codes without explanation (he will call it out); - banned words: "load-bearing", "smoking gun", "belt and suspenders", - "gate". Bring questions with recommendations, not decisions. Never - use the question UI. -- Hard boundaries unchanged from the original brief: never modify - `packages/cli-engine/**`, `packages/cli/src/auth/**`, - `packages/cli/src/v8/auth/**`, publish machinery, specs; engine - gaps are STOPs surfaced to the operator with a recommendation. -- Verification before every commit, judged by pnpm's own exit codes; - the engine package must stay untouched and green. -- Implementation conventions established in the code (follow them): - one command per file; shared resolution in `v8/service/target.ts` - (`resolveServiceReadState`, `resolveServiceDomainTarget`, - `openServiceStateStore`, `rememberSelectedService`); errors in - `v8/service/errors.ts` (`SERVICE.*` codes, `renameAppCopy`, - `fromLegacyCliError`, `adviceAction`); handlers call the existing - legacy operation layer, never rewrite it (additive taps only: - `executeAppBuild` `io`, `removeApp` `progress`); tests semantic-only - through `createTestCli`; fake API bodies are `{data: {…}}`-shaped; - no `app` noun in any v8 user-visible surface except the SDK-owned - `app:`/`apps:` compute-config keys (recorded decision). diff --git a/.drive/projects/prisma-cli-v8/assets/briefs/s2c-handover.md b/.drive/projects/prisma-cli-v8/assets/briefs/s2c-handover.md deleted file mode 100644 index 715c9cab..00000000 --- a/.drive/projects/prisma-cli-v8/assets/briefs/s2c-handover.md +++ /dev/null @@ -1,305 +0,0 @@ -# S2c handover brief — execute slice s2c-services - -Written 2026-08-10 for an independent orchestrating agent with NO -prior context on this project. Everything you need to start is in -this file or in the named documents; where this brief summarizes a -document, the document wins. The operator is Will Madden ("the -operator" below). All paths are repo-relative unless absolute. - -## 1. What this project is - -Repo `prisma/prisma-cli` is the v8 rewrite of Prisma's platform CLI. -The legacy CLI (still in this repo, `packages/cli/src/` outside the -`v8/` directory — a commander-based shell) is being ported command by -command onto `@prisma/cli-engine` (`packages/cli-engine/`), a -declarative command engine built in slice S1. The engine owns -argument parsing, help, output envelopes, JSON mode, prompts, -telemetry, error presentation, and exit codes; commands are -definitions plus handlers that receive a `CommandContext` and return -data. When the port completes (slice S2d) the commander shell is -deleted. - -Slice map (S1 merged as PR #129; S2a is PR #130, open): - -| Slice | Scope | State | -| --- | --- | --- | -| S2a | Engine production-readiness: `ctx.api`, auth module, prompts (clack), telemetry, versioning/publish machinery, `version`, auth command family | PR #130 open; an auth rework is landing on it (see §6) | -| S2b | `project *`, `postgres *` (renamed from `database`), `bucket *`, `branch list`, `git *` — 30 commands | Handed to another independent agent; branch `s2b-resources` exists, work not yet pushed | -| S2c | **THIS SLICE**: `service *` (renamed from `app`), `build logs`, `agent *`, `feedback` — 24 commands + 1 parked | Yours | -| S2d | `init` wizard, commander-shell deletion, fixture-machinery deletion, final parity review | Not started | - -## 2. Your normative documents, in reading order - -All under `.drive/projects/prisma-cli-v8/`. Precedence when they -disagree: contract > overview rulings > inventory > this brief. A -contradiction between any of them, or a fact none of them pins, is a -STOP: surface it to the operator; never improvise a resolution. - -1. `specs/s2-overview.md` — the S2 PR split, ten standing rulings - (all bind you; summarized in §5 below), and the operator question - ledger Q1–Q8. Ledger items are ruled defaults unless marked open. -2. `specs/s2c-services.md` — YOUR CONTRACT: mapping rules - R-S2c-1..7, the exact command list, acceptance checklist. It - inherits S2b's mapping rules R-S2b-2/3/4/5/6/9/10 verbatim, so - read `specs/s2b-resources.md` for those (they cover error-code - namespacing, consent/exit-code unification, prompt porting, the - test matrix R-S2b-9, and divergence-entry duty). -3. `plans/s2c-services.md` — your four dispatches D1–D4 (group - core → progress operations → streams → agent/feedback/closure). -4. `assets/s2/command-inventory.md` — the normative record of what - every legacy command does today: flags, API calls, prompts, - errors, exit codes, side effects, test coverage. §4 of it has a - per-command entry for each of your 25 commands (`prisma app *`, - `build logs`, `agent *`, `feedback`). Port from THIS, not from - your own reading of the legacy code; if the inventory and the - code disagree, that too is a STOP (the inventory has a - spec-discrepancies section — check it first). -5. `assets/engine/engine-interface-draft.ts` — the normative engine - interface with commentary. The engine implementation matches it. -6. `assets/s2/parity-divergences.md` — the S2a divergence entries; - your entries follow their format but go in a NEW file (§7). - -## 3. How a command is built on this engine (primer) - -Read the real code in this order; it is the fastest orientation: - -- `packages/cli-engine/src/commands.ts` + `command-family.ts` — how - commands and groups are defined (`CommandFamily` is the ownership - entity; never abbreviate it in identifiers). -- `packages/cli-engine/src/context.ts` — what handlers receive: - `ctx.api` (the authenticated management API client — your ONLY - path to the platform API), `ctx.session()` (read-only auth state), - prompts, logger, injectable clock, `ctx.env`/`ctx.cwd`. -- `packages/cli-engine/src/events.ts` + `presentation.ts` + - `protocol.ts` — the output model. Human output and machine output - are both derived from what the handler returns/emits; you never - write to stdout/stderr yourself. Channel discipline: explanatory - blocks go to stderr, payload to stdout; `--json` frames events. -- `packages/cli-engine/src/execution/` — command kinds. Sync work is - a RESULT command (return a value + serializer). Long-running work - with progress is a SESSION command (emit `step-started/finished`, - `progress`, `status` events). Line-by-line output over time is a - STREAM command (records map to `output` events with a - `data`-vs-`diagnostic` channel per record). -- `packages/cli-engine/src/testing.ts` (exported as - `@prisma/cli-engine/testing`) — `createTestCli`: runs a command - in-process with `ctx.api` faked, prompts scripted, clock - controlled; assertions target the envelope, presented data, events, - and exit codes. This is how ALL your tests work (standing ruling: - semantic-first; never byte-pin output outside the one small golden - suite per output mode). -- `packages/cli/src/v8/` — the ported CLI: `cli.ts` mounts command - groups (the "mount map"); each command lives in one file named for - the command; shared presentation helpers live in named modules. - The `auth/` subtree there is the existing (pre-rework) porting - precedent for file layout. By the time you read this, S2b's - `project/` group may exist on `s2b-resources` — if so, it is the - layout template for your groups. -- Errors: structured, with a stable code (yours are namespaced - `SERVICE.*`, `BUILD.*`, `AGENT.*`, `FEEDBACK.*`), a `why`, and - typed `nextActions` (never free-text "fix" hints). Exit codes: - 0 success, 1 runtime failure, 2 structural/usage/consent-required, - 3 user-canceled, 130 SIGINT. -- Prompts return their input value directly (or throw on cancel); - consent prompts are `prompt.consent`; `--yes` satisfies - consent-grade prompts; non-interactive without `--yes` is the - structured consent-required error (exit 2). These unifications - CHANGE some legacy exit codes — ledger Q5 rules this; every - changed code gets a divergence entry. - -Telemetry: automatic. The engine reports command runs the way the -ORM CLI does, via `@repo/cli-telemetry`. You wire nothing per -command. Tests must NEVER contact the production telemetry endpoint -(the cli package's vitest config already sets -`PRISMA_NEXT_DISABLE_TELEMETRY=1`; telemetry-behavior tests use the -mock endpoint fixture only). - -## 4. What you are porting (the substance) - -Contract scope: 24 commands + 1 parked. The rename is ruled -(R-S2c-1): the deployable unit's noun is **Service** — `app` ports -as `service` in all paths, ids, help, and presenters, with NO alias; -one divergence entry per command. Scope note from the contract: env -vars live under `project env` (S2b); the app group has a `domain` -subgroup and NO env subgroup — follow the inventory. - -Highlights per group (full detail: inventory §4): - -- **`service deploy`** — the flagship and the hardest command in the - CLI. Multi-step session command (upload/build/deploy/promote with - progress callbacks), first-deploy interactive customization, - `--db` branch-database wiring with its own consent, production - protection (second-and-later production deploys require `--prod` - plus `--yes`/interactive confirm; cancel exits per Q5's unified - codes), deploy-all mode for multi-target configs (rejects - per-app inputs), a dozen error codes with build-phase-aware hints. - Budget the most time here; its legacy test files (app.test.ts, - deploy-plan.test.ts, production-deploy-gate.test.ts, and five - more) enumerate the behavior matrix. -- **`service remove`** — destructive; the CLI's only TYPE-THE-NAME - confirmation. Ports to `prompt.consent` + its current flag per - R-S2b-3. -- **`service promote` / `rollback`** — remote operations with - progress (session commands). NOTE the inventory flag: legacy - `rollback` has NO confirmation today despite being - production-affecting. Port as-is (parity) and record it in your - divergence file as a flagged follow-up for the operator — do not - add a prompt unilaterally. -- **`service logs`** (stream) and **`build logs`** (stream) — - R-S2c-2: per-record `source`/`level` routing maps to the engine's - `data` vs `diagnostic` channels; the legacy JSON wrapper-event - opt-out for `build logs` does not port (divergence). `build logs` - has ZERO legacy tests — you write its first ever; full R-S2b-9 - matrix applies. Its terminal-record protocol (a `terminal error` - record sets exit 1 without throwing) must map onto engine stream - termination status. -- **`service domain wait`** — canonical poll→status-events case: - emits a status event per change on the injectable clock, - `--timeout` default 15m, terminal states active/failed/timeout. -- **`service build`** — R-S2c-5: fully local result command (no - `ctx.api`), framework build via child processes, progress events - from the SDK build reporter. -- **`service open`** — R-S2c-6: URL as an `endpoint` event + the - operation layer's existing browser opener; never open without a - TTY (report url + `opened: false`). -- **`agent install|update|status`** — local child-process commands - (spawn `skills-cli` via pnpm dlx/bunx/npx); no auth, no API; - `--dry-run` returns `{status:"would-install", command}`. -- **`feedback`** — no auth; POSTs to the feedback service URL - (env-overridable, 3s timeout). Legacy has no JSON serializer for - it; under the engine it gets the standard envelope (divergence). - The crash-recovery flow pre-fills this command, so the v8 shell - keeps an equivalent hook. -- **PARKED: `service run`** (ledger Q2, OPEN — operator decision). - It passes the child dev-server's exit code through as the CLI's - exit code; engine session commands have no exit-code channel. - DO NOT port it until the operator rules whether the passthrough - mechanism is built in S2c or deferred to Composer's S3. Raise Q2 - with the operator EARLY (your first report), because the answer - shapes your D3. - -Auth for your commands: the app group and `build logs` use -`needs.credentials` + `ctx.api` and never auto-login (the legacy -TTY auto-login does not port — ledger Q1). `agent` and `feedback` -declare no credential needs. The compute-plane operations (deploy, -logs streaming) authenticate the compute SDK client with the -credential — that wiring lives in the auth/operations layer you -consume, not in your command files; if you find no sanctioned path -to an authenticated compute client when you get there, STOP and -surface it (do not read token storage yourself). - -## 5. Standing rulings that bind every line you write - -Full text: `specs/s2-overview.md`. The ones violated most easily: - -1. `CommandFamily` is the contribution entity — never "product", - never "manifest", never shortened. -2. No conditional properties on stored/normalized types: absent = - `T | undefined` with the key required. -3. Tests semantic-first through `createTestCli`; management API - faked at `ctx.api`; auth stubbed at the auth-module seam; no - byte-matching outside the golden suite; delete legacy fixture - tests ONLY for commands you port. -4. No dynamic imports of handlers. No lazy handler loading. -5. Naming: no invented jargon, no mechanism names as domain names, - no dropped meaning-carrying qualifiers, no transient project IDs - in shipped code. Command examples never include the binary name. -6. Comments are a last resort; public-facing doc comments terse. -7. `--json` sets format only; `--quiet`/`--verbose` set log level - only and are not otherwise retained; `--interactive` re-enables - prompts under `--json`. -8. Every user-visible behavior change from legacy gets a divergence - entry (see §7) — renames, exit-code changes, dropped flags, - envelope shape changes, all of it. - -## 6. State of the world and coordination (read carefully) - -Three streams are active in this repo: - -- `s2a-foundations` (PR #130, the base of everything): a - credential-manager rework of the auth family is landing on it - RIGHT NOW (design: `assets/engine/credential-manager-design.md`, - rev 4 final). Consumer-facing surfaces you depend on — `ctx.api`, - `needs.credentials`, `ctx.session()` — are semantically stable; - the test-harness credential-seeding options may change shape once. - If a merge-down changes the seeding surface, adopt the new one - during the merge rather than pinning the old one into new files. -- `s2b-resources`: another independent agent, in progress. You do - not coordinate with it directly; you consume its branch. -- Yours: branch `s2c-services` **off the current tip of - `s2b-resources`** (the operator ruled parallel execution; the - contract's "off main after S2b merges" describes the eventual - merged geometry). Open your PR with base `s2b-resources` — NOT - `main` — and retarget when S2b merges. Merge down from - `s2b-resources` regularly; expected conflict surface is only - `packages/cli/src/v8/cli.ts` (the mount map) and the lockfile. - If S2b has not pushed command work yet when you start, D1 - proceeds anyway: the S2a auth family under `packages/cli/src/v8/` - is a sufficient layout precedent, and you adopt S2b's template on - your first merge-down if it differs. - -Hard boundaries — never modify: -- `packages/cli-engine/**` (an engine gap → STOP, surface to the - operator with the exact need; do not extend the engine yourself), -- `packages/cli/src/auth/**` and `packages/cli/src/v8/auth/**` - (mid-rework by the S2a stream), -- `.github/workflows/publish.yml`, `scripts/determine-version*`, - root/package version fields (publish machinery is settled), -- `assets/s2/parity-divergences.md` (being rewritten by the auth - stream) and anything under `.drive/projects/prisma-cli-v8/specs/` - other than reading it, -- `wip/**` anywhere, if present — never stage it. - -## 7. Your divergence file - -Create `.drive/projects/prisma-cli-v8/assets/s2/parity-divergences-s2c.md`, -same entry format as `parity-divergences.md` (S2d consolidates the -per-slice files). Every R-S2c-1 rename, the `build logs` wrapper -drop, every Q5 exit-code change, the `feedback` envelope addition, -the rollback-has-no-confirm flag, and anything else user-visible. - -## 8. Process (non-negotiable) - -- Orchestrate via the drive process: dispatch implementer subagents - (model: Fable) per plan dispatch D1–D4; run the slice review loop - (architect + principal-engineer reviewers, model: Opus) before - the PR leaves draft; fix findings before reporting done. -- Git identity — you are the `wmadden-electric` bot: - - stage files EXPLICITLY by path; never `git add -A`/`-u`; - - commit: `git commit -s --trailer "Signed-off-by: Will Madden - "`, body's last line - `Co-Authored-By: Claude Fable 5 `; - - push ONLY to the bot remote - `git@github-wmadden-electric:prisma/prisma-cli.git`. -- Verification per dispatch, all green before commit: - `pnpm --filter @prisma/cli test`, - `pnpm --filter @prisma/cli-engine test` (must stay untouched and - green), `pnpm typecheck`, `pnpm lint` — judged by pnpm's OWN exit - code, not a pipeline tail's. -- PR: ≥1k LOC, one PR for the slice. Description structure (ruled): - a grounding example first (a real command run, before/after), then - the decision, then the narrative, alternatives last; no internal - process codes or dispatch labels in the description. -- Reporting to the operator: plain English, full sentences, no - invented shorthand; spell out anything slice-internal. Banned - words: "load-bearing", "smoking gun", "belt and suspenders", - "gate". Bring QUESTIONS to decide, not decisions to ratify. STOP - items (contradictions, unpinned facts, engine gaps) go to the - operator immediately with your recommendation attached. -- Do not use the question UI; write questions in plain messages. - -## 9. What done looks like - -The contract's acceptance list, restated: all 24 commands mounted -and green on the R-S2b-9 test matrix (streams included, `build -logs` tested for the first time); no `app` path surviving anywhere -in v8; deploy/promote/rollback/remove event sequences pinned by -semantic tests; the divergence file complete; Q2 either ruled and -implemented or still parked with the legacy path intact and a note -for S2d; legacy fixture tests for your commands deleted; root -verification green; review loop run and findings fixed; PR open -against `s2b-resources` with the ruled description structure. - -Your first report to the operator should contain: confirmation you -read the four normative docs, your Q2 question, the S2b template -status you found, and your D1 dispatch plan. Then execute. diff --git a/.drive/projects/prisma-cli-v8/assets/briefs/s3-closeout-handover.md b/.drive/projects/prisma-cli-v8/assets/briefs/s3-closeout-handover.md deleted file mode 100644 index 7ce10342..00000000 --- a/.drive/projects/prisma-cli-v8/assets/briefs/s3-closeout-handover.md +++ /dev/null @@ -1,153 +0,0 @@ -# S3 close-out handover — Composer adoption - -Written 2026-08-12 for an agent with NO prior context. The operator -is Will Madden. Slice S3 (Composer adoption) is **substantially -done and merged**; what remains is one open PR, a short list of -follow-ups, and the slice's formal close-out. - -## 1. What S3 was, and its state - -Repo `prisma/prisma-cli` is the v8 Prisma CLI, built on -`@prisma/cli-engine` (`packages/cli-engine`). S3 made **Composer** -(repo `prisma/composer`) the engine's first cross-repo consumer: -composer's four commands — `deploy`, `destroy`, `dev`, `log` — are -now engine commands, composer's own CLI is a thin composition of -them, and the `prisma` binary mounts the same family. - -**Merged (all of it):** - -| PR | What | -| --- | --- | -| prisma-cli #136 | `ctx.spawn` — terminal handoff with credential injection | -| prisma-cli #145 | the engine settles signal-terminated runs (Ctrl-C → 130) | -| prisma-cli #150 | the engine records the child; `ctx.lastChild()`; `exitWithChildStatus()` loses its argument | -| prisma-cli #151 | a handler can fail with more than one finding | -| prisma-cli #155 | the engine detects CI itself; hosts stop answering | -| prisma-cli #152 | **the mount** — composer's family in the v8 bin (`42ee7891`) | -| composer #220 | composer's CLI becomes four commands on the engine | -| composer #224 | engine pin 0.0.9; Node floor 24 → 22.18 | - -**Open, approved, needs merging: composer #226** — a CI check that -imports all 16 published entrypoints on the Node floor. It was -`BEHIND` main; I merged main into it and pushed (`32c85f27`). -Confirm its checks go green, then merge it. Nothing else blocks it. - -## 2. Read these before touching anything - -- `.drive/projects/prisma-cli-v8/specs/s3-composer.md` — the slice - contract, rev 2 final. Normative. Its §10 records every amendment - made during the slice. -- `.drive/projects/prisma-cli-v8/deferred.md` — **the live list of - everything carried out of this slice**, grouped by owner. Read it - in full; most of §3 below is a pointer into it. -- `.drive/projects/prisma-cli-v8/plan.md` — the project plan and - coverage ledger (corrected during S3; see §3). -- `.drive/projects/prisma-cli-v8/assets/s2/parity-divergences-s3.md` - — the nine user-visible changes from composer's own CLI. -- `.drive/projects/prisma-cli-v8/assets/s3/composer-inventory.md` — - what composer's CLI did before the port. Still the reference for - any behavioural question. - -## 3. What is left - -### Immediate -1. **Merge composer #226** (above). -2. **Composer should drop its `isCI` answer.** prisma-cli #155 made - `Runtime.isCI` the optional `isCIOverride` — the engine detects - CI itself now. Composer still passes `isCI` (via `ci-info`) in - `packages/0-framework/3-tooling/cli/src/family/runtime.ts` - because it pins engine `0.0.9`, which predates #155. Harmless - today. Drop the parameter and the `ci-info` dependency when - composer next bumps its engine pin. - -### The engine-copy problem — needs the lockstep release -An install of `@prisma/cli` resolves **two** copies of the engine: -this repo ships its own at `8.0.0-rc.1` while composer pins the -published `0.0.9`. Two exact pins on two release lines cannot -dedupe. It works only because values crossing the boundary are -matched by `Symbol.for` rather than by identity, and **only the -structured-error crossing is tested**. It collapses when both sides -name the same engine version — i.e. when composer pins the lockstep -version `publish.yml` ships. Do not attempt to fix it by pinning a -different `0.0.x`; that changes nothing. - -### Everything else -`deferred.md` is the record. Notable entries, so you know they -exist without reading it cold: -- **Composer's help examples are wrong under the prisma bin.** They - read `{bin} deploy src/service.ts`, so `prisma composer deploy - --help` shows `prisma deploy src/service.ts`, which exits 2. Eight - examples, two per command. Needs a mount-aware placeholder in the - engine plus composer rewriting its strings. -- **R-S3-2's diagnostics list still has no consumer.** Composer - builds a list of every config problem but commands still fail on - the first. Engine #151 supplied the missing surface (a failure can - now carry findings); wiring composer to it is the remaining work. -- **Two flaky tests under load**, both the same shape (a child - writes its ready marker before installing its signal handler): - `packages/cli/tests/v8-spawn-adapter.test.ts` and - `packages/cli-engine/tests/spawn-real-child.test.ts`. -- **`pnpm --filter @prisma/cli test` can pass against a stale engine - build** — it does not build first. Run the engine suite before it, - or use `turbo run test`. -- **The alchemy exit-hook patch** in composer - (`patches/@alchemy.run__node-utils@0.0.5.patch`) is vendored from - the open alchemy-run/node-utils#6. Delete it when that ships - through the chain. - -## 4. Closing the slice - -S3 is not formally closed. Per the drive process, close-out means: -verify the contract's acceptance list against what actually shipped -(the contract's §10 already records the amendments), fold anything -still true into `deferred.md`, and update `plan.md`'s slice table. -**Do not claim acceptance items that were amended away** — read §10 -first; several were, deliberately. - -The next slice by the plan's dependency graph is **S8** (service -primitives), whose design questions S3 answered — the answers are -in `deferred.md` and the inventory's §4a. **S5** (ORM adoption) has -its own brief at `assets/briefs/s5-orm-handover.md` and is -independent. - -## 5. Process rules (operator-enforced) - -- **Git identity is the `wmadden-electric` bot**: stage explicitly - by path (NEVER `git add -A`; NEVER anything under - `.drive/projects/prisma-cli-v8/specs/reviews/` or `wip/`); commit - `git commit -s --trailer "Signed-off-by: Will Madden "` - with body ending `Co-Authored-By: Claude Fable 5 `; - push only to the bot remote (`bot` in prisma-cli, `origin` in the - composer clone — both are the `github-wmadden-electric` SSH alias). -- **Verification**: `pnpm --filter @prisma/cli test`, - `pnpm --filter @prisma/cli-engine test`, - `pnpm --filter @repo/cli-telemetry test`, `pnpm typecheck`, - `pnpm lint` — all must exit 0. **Lint fails on warnings.** Run - suites sequentially; parallel runs race the engine build. -- **Composer clone** lives at - `.claude/worktrees/s3-composer/wip/work/composer`. Its checks: - `pnpm build`, typecheck, `lint`, `lint:casts` (no ratchet delta), - `lint:deps`, `@internal/cli` suite, `check:cli-engine-pin`, - `check:family-static-graph`, `check:npm-effect-resolution`, - `check:publish-deps`, `check:floor-imports`. Known pre-existing - failures NOT yours: `@internal/dev-emulators` / - `@internal/local-target` when a leftover Prisma Dev daemon holds - ports 51316–51325. -- **Every PR**: address every CodeRabbit thread — fix it, or decline - it with verified evidence — then reply and resolve each thread - before merging. Several of its findings this slice were correct - where our own records were wrong. -- **PR descriptions** (ruled): a grounding example first (a real - command run), then the decision, then the narrative, alternatives - last. No internal process codes. -- **Reports to the operator**: plain English, short. Report outcomes, - decisions he must make, and changes to his world — nothing else. - Banned words: "load-bearing", "smoking gun", "belt and suspenders", - "gate". Bring questions to decide, not decisions to ratify. Never - use the question UI. Give absolute paths and branch-qualified - GitHub URLs, never relative links. **Never use spawn_task chips** — - follow-ups go in `deferred.md`. -- **Verify claims against source before asserting them.** This slice - produced several confident statements that were wrong (composer - "fails on Node 22" — it does not; the effect check "never runs" — - it does). Read the code or the published package. diff --git a/.drive/projects/prisma-cli-v8/assets/briefs/s5-orm-handover.md b/.drive/projects/prisma-cli-v8/assets/briefs/s5-orm-handover.md deleted file mode 100644 index ae5d63c8..00000000 --- a/.drive/projects/prisma-cli-v8/assets/briefs/s5-orm-handover.md +++ /dev/null @@ -1,187 +0,0 @@ -# S5 handover brief — port the Prisma ORM CLI onto @prisma/cli-engine - -Written 2026-08-11 for an independent orchestrating agent with NO -prior context. The operator is Will Madden ("the operator"). Where -this brief summarizes a document, the document wins. All paths are -relative to your prisma-cli worktree unless absolute. - -## 1. Project context - -Repo `prisma/prisma-cli` is the v8 rewrite of Prisma's CLI. Its -core is `@prisma/cli-engine` (`packages/cli-engine`): a declarative -command engine owning argv parsing, help, output envelopes, JSON -mode, prompts, consent, telemetry, error presentation, and exit -codes. Commands are definitions + handlers receiving a -`CommandContext`. The engine's consumers land in ruled order — -platform CLI (S2, in flight), Composer (S3, in flight), ORM last -("hardest consumer, twice-hardened"). YOU are the ORM consumer: -slice **S5 — ORM adoption**. - -State of the world (2026-08-11): -- S2a is MERGED to `main` (PR #130, commit 14f9c25): the engine - production surface — `ctx.api` (authenticated management API - client), the credential manager (per-workspace auth sessions), - clack-backed prompts with consent tokens and the global - `--confirm ` flag, `ctx.openUrl` + `prompt.browserWait` - interactivity affordances, `needs.interaction`, telemetry, and - the prisma/prisma-style versioning/publish machinery (lockstep - `8.0.0-rc.1`; merged `chore(release)` bump PRs publish `latest`, - main pushes publish `-dev.N`). -- `@prisma/cli-engine@0.0.2` is published (the current surface); - the operator publishes new versions on demand — ask, don't - engineer around it. -- Three sibling streams run in OTHER worktrees with independent - agents: S2b (`s2b-resources`), S2c (`s2c-services`), S3 - (`s3-composer`). Never touch their branches. S2d (shell - retirement) has not started. - -## 2. What S5 is (plan §S5 — read `.drive/projects/prisma-cli-v8/plan.md` in full) - -Repos: **prisma/prisma + prisma-cli**. Deliverables: -- The `orm` config-section token and the `orm` `CommandFamily`. -- Port `contract *`, `migration *` (retiring the clipanion-based - migration-cli), `db *`, `init`, `telemetry`, and `lsp` (the - server command) onto the engine. -- Prove the DIAGNOSTICS model: `migration check` / `db verify` as - completed-with-findings envelopes with catalogued exit codes, and - the exit-code-4 semantics defined by S4's ADR-239 amendment. -- Reconcile the ORM's three colliding exit-code schemes to the - contract (a finding of the original survey — locate it in - `.drive/projects/prisma-cli-v8/spec.md` / `design-notes.md`). -- Close out the paused 1b brief - (`.drive/projects/prisma-cli-v8/assets/briefs/1b-leftovers-prisma-prisma.md`) - against the section API — read it early; S5 supersedes it, and - anything it promised that S5 does not deliver must be surfaced to - the operator explicitly, not dropped silently (this exact silent - drop happened with a Composer-side brief and was caught in - review). -- Per the rollout plan (`.drive/projects/prisma-cli-v8/assets/rollout-plan.md`): - prisma/prisma retires its own CLI at S5, and the - `@repo/cli-telemetry` implementation ALREADY lives in this repo — - the ORM's reporting must stay identical (shared installation id). - -## 3. HARD ORDERING CONSTRAINT — check S4 first - -Plan §S4: the ADR-239 amendment in prisma/prisma (dotted diagnostic -codes inside completed envelopes, documented exit codes, the -`fix` → typed `nextActions` rename) must land BEFORE S5 relies on -those semantics. Your literal first action: determine S4's status -(search prisma/prisma for the amendment; ask the operator). If S4 -has not landed, raise it in your first report with a proposal — -options: you execute S4 first (it is small and independent), or S5 -proceeds on non-diagnostic commands while S4 lands in parallel. Do -NOT build diagnostics semantics against an unamended ADR. - -## 4. Reading order (before any planning) - -1. `.drive/projects/prisma-cli-v8/plan.md` + `spec.md` — the - project frame, consumer ordering, coverage ledger. -2. `.drive/projects/prisma-cli-v8/specs/s2-overview.md` — the ten - standing rulings (they bind every slice) + the operator question - ledger. -3. `.drive/projects/prisma-cli-v8/assets/engine/engine-interface-draft.ts` - — the normative engine interface with commentary. -4. `.drive/projects/prisma-cli-v8/assets/engine/credential-manager-design.md` - (rev 5) — the auth model. ORM commands are mostly local; where - one needs platform auth it uses `needs.credentials` + `ctx.api` - and NOTHING else. -5. `.drive/projects/prisma-cli-v8/assets/briefs/s2c-handover.md` - §3 — the "how a command is built on this engine" primer with - file pointers; it is accurate and saves you a day. Supplement - with the real code: `packages/cli-engine/src/` and the ported - families under `packages/cli/src/v8/`. -6. `assets/s2/command-inventory.md` and - `assets/s3/composer-inventory.md` — the FORMAT PRECEDENT for - your inventory (per-command entries: Summary/Flags/Positionals/ - Auth/API calls/Behavior/Output/Prompts/Side effects/Tests/ - Engine notes; shared-machinery; discrepancies). -7. The 1b brief (§2 above). - -## 5. Your sequence (the drive process) - -1. **Inventory** — an exhaustive ORM CLI inventory - (`assets/s5/orm-cli-inventory.md`): every command in scope from - the prisma/prisma CLI (contract/migration/db/init/telemetry/lsp - and whatever else exists there today), flags, config, engines - interaction, exit codes (document all THREE colliding schemes - precisely), prompts, side effects, child processes, test census, - spec discrepancies. Clone prisma/prisma READ-ONLY into your - worktree's `wip/repos/` for analysis (never stage `wip/`). -2. **Contract + dispatch plan** (`specs/s5-orm.md`, - `plans/s5-orm.md`) — written to ZERO creative freedom: every - mapping decided, every divergence listed, unpinned facts marked - STOP. Bring the operator the shaping questions BEFORE writing - where a genuine choice exists (e.g. where the `orm` family's - code lives — in prisma/prisma exported like Composer's, or in - this repo — is a contract decision the operator makes; likewise - the `lsp` long-running server command's engine mapping if the - session/stream kinds don't fit — an engine gap is a STOP, never - an improvised engine change). -3. **Architect + principal-engineer review** of the contract - (subagents), findings folded, then execution rounds, slice - review loop, PR. - -## 6. Mechanics and boundaries - -- prisma-cli side: branch off `main` (S2a is merged; you need - nothing from the S2b/S2c/S3 branches). One PR for the slice, - base `main`. -- prisma/prisma side: commits/pushes there also go through the bot - identity. CONFIRM with the operator that `wmadden-electric` has - push access to prisma/prisma before dispatching work against it. -- Engine changes: FORBIDDEN in this slice without an operator - ruling. A gap in the engine (e.g. for `lsp`) is a STOP-and- - surface with your recommendation attached. The S3 stream also - makes engine changes — coordinate through the operator, never by - editing the same files on a guess. -- Commands never read TTY/CI state: declare - `needs: { interaction: true }` for interactivity requirements; - use `prompt.*` (consent with tokens — the global - `--confirm ` flag is engine-owned; NEVER hand-roll a - consent-skip flag), `ctx.openUrl`, `prompt.browserWait`. -- Divergence entries: every user-visible change from the legacy ORM - CLI goes in a NEW file `assets/s2/parity-divergences-s5.md` - (same entry format as `parity-divergences.md`; a later slice - consolidates). -- Tests: semantic-first through `createTestCli` - (`@prisma/cli-engine/testing`); assert envelopes, presented - data, events, exit codes — never output bytes outside a small - golden suite. Telemetry tests NEVER contact the production - endpoint (the cli package's vitest config sets - `PRISMA_NEXT_DISABLE_TELEMETRY=1`; use the mock endpoint - fixture). - -## 7. Process rules (operator-enforced, non-negotiable) - -- Git identity — the `wmadden-electric` bot: stage explicitly by - path (NEVER `git add -A`; NEVER anything under `wip/` or - `.drive/projects/prisma-cli-v8/specs/reviews/`); commit - `git commit -s --trailer "Signed-off-by: Will Madden "`, - body ends `Co-Authored-By: Claude Fable 5 `; - push ONLY to `git@github-wmadden-electric:/.git` - remotes. -- Verification per dispatch: the touched packages' test suites, - `pnpm typecheck`, and root `pnpm lint` measured as pnpm's OWN - exit code with `wip/` moved aside in one shell: - `mv wip /tmp/wip-stash && pnpm lint; s=$?; mv /tmp/wip-stash wip`. -- Subagents: implementers AND reviewers on Opus (operator ruling). -- Reports to the operator: plain English, full sentences, no - invented jargon or session-internal labels. Banned words: - "load-bearing", "smoking gun", "belt and suspenders", "gate". - Report only outcomes, decisions he must make, and changes to his - world. Bring questions to decide, not decisions to ratify. - Never use the question UI. When pointing him at a file, give the - absolute disk path and a branch-qualified GitHub URL — relative - links break. -- PR description structure (ruled): grounding example first (a - real command run, before/after), then the decision, then the - narrative, alternatives last; no internal process codes. - -## 8. Your first report to the operator - -Confirm you read plan/spec/overview/draft/rev-5 design and the 1b -brief; state S4's status and your proposal (§3); confirm bot access -to prisma/prisma; name anything in scope you believe is NOT -portable onto the current engine (candidates: `lsp`'s server -lifetime; migration-cli's clipanion interactivity); then your -inventory dispatch plan. Then execute. diff --git a/.drive/projects/prisma-cli-v8/assets/briefs/windows-ci-credential-manager.md b/.drive/projects/prisma-cli-v8/assets/briefs/windows-ci-credential-manager.md deleted file mode 100644 index 36ca56f7..00000000 --- a/.drive/projects/prisma-cli-v8/assets/briefs/windows-ci-credential-manager.md +++ /dev/null @@ -1,25 +0,0 @@ -# The three Windows CI failures on `s2a-foundations` — resolved - -Written 2026-08-10 by the s2b-resources orchestrator, and updated the same night once the auth stream fixed all three. Kept rather than deleted because one of them was a real concurrency bug and the diagnosis is worth having on the record. - -**Current state: all three pass. The Windows job on PR #133 is green.** Nothing here is outstanding. - -## What was failing - -Three cases in `packages/cli/tests/credential-manager.test.ts`, all pre-existing rather than caused by PR #133 — the same three, and only these three, failed on `s2a-foundations` itself at its then-tip 96e5628 and the two commits before it. PR #133 briefly added a fourth of its own, which was fixed separately by skipping a case whose Unix-only setup Windows cannot reproduce. - -Two were test-only. "writes the normative shape with mode 0600" and "tightens permissions looser than 0600" ended in an assertion of POSIX permission bits, which Windows has no representation for — Node reports `0o666` whatever `chmod` was asked for, hence CI's "expected 438 to be 384". - -**The third was a real defect.** "lets only one of two waiting mutations clear the same crashed holder's lock" asserted that exactly one racer takes over a crashed holder's lock; on Windows both did. The takeover rested entirely on the loser's `fs.rename` failing once the winner had moved the lock away — true on POSIX, not true there — so two processes could believe they held the credential lock at once, which is the interleaving the lock exists to prevent. - -## How they were fixed - -By the auth stream, on `s2a-foundations`: - -- The permission assertions are now guarded by a `POSIX_MODES` constant, so they run where the concept exists and are skipped where it does not. Same approach PR #133 took for its own Unix-only case. -- The real one is `fec6678`, "close the stale-lock takeover race the Windows runner exposed". `rename` cannot be made conditional, so the takeover now confirms afterwards that what it moved aside is the same lock it examined, comparing modification times, and puts it back with `link` — which fails when the path is occupied — if it is not. The exclusivity the lock needs no longer depends on rename semantics that differ by platform. -- The takeover assertion moved from "exactly one" to "at most one". Worth noting that this does not weaken the defect check: the failure being guarded against was **two** takeovers, which the assertion still catches. It now tolerates zero, which is the race simply not materialising. - -## Why this is recorded - -The Windows runner exposed a genuine cross-platform concurrency bug in credential storage that no Unix run would have caught, and it was found only because a resources-slice PR made someone read a red Windows job carefully instead of dismissing it as the usual platform noise. That is the argument for keeping the Windows matrix meaningful and for not reaching for a skip before understanding which failures are artifacts and which are real. diff --git a/.drive/projects/prisma-cli-v8/assets/command-review.md b/.drive/projects/prisma-cli-v8/assets/command-review.md deleted file mode 100644 index f8c0c190..00000000 --- a/.drive/projects/prisma-cli-v8/assets/command-review.md +++ /dev/null @@ -1,205 +0,0 @@ -# Prisma CLI v8 — command review - -Every command the CLI mounts, for a semantic review of what each command means and how the tree is organized. Regenerated 2026-08-21 after the command grammar cleanup (PM review: compute config and `init` removed, destroying `remove` commands renamed `delete`, `postgres restore`/`ref`/`migrate`/`format` moved, composer's verbs mounted at the root). Generated from the mounted command tree in `packages/cli/src/cli.ts`. Flags and options are deliberately omitted. 86 commands in total. - -The tree has five sources: the platform family (this repo), the Composer family (`@prisma/composer-cli`, re-wrapped to `deploy` and `dev`), the ORM family (`@prisma/orm-toolchain`), the engine's telemetry group, and a few local utilities with no owning package. - -## Top-level commands - -| Command | Meaning | -| --- | --- | -| `dev` | Bring up the application whose root node is the entry's default export, entirely on this machine | -| `deploy` | Deploy the application whose root node is the entry's default export | -| `lsp` | Start the Prisma Next language server | -| `feedback` | Send feedback to the Prisma CLI team | - -Notes for the review: there is no top-level `init` (the compute-config wizard was removed; the ORM's project initializer stays at `orm init`) and no `version` command — the engine's `--version` answers. - -## `auth` — Manage local authentication for the CLI - -| Command | Meaning | -| --- | --- | -| `auth login` | Log in to your Prisma platform account | -| `auth logout` | Clear stored authentication credentials | -| `auth whoami` | Show the authenticated user and accessible workspace | - -### `auth workspace` — Manage local workspace sessions - -| Command | Meaning | -| --- | --- | -| `auth workspace list` | List your workspace sessions | -| `auth workspace use` | Make one of your workspace sessions current | -| `auth workspace logout` | End one workspace session | - -## `project` — Manage and inspect your Prisma projects - -| Command | Meaning | -| --- | --- | -| `project list` | List all projects in your workspace | -| `project show` | Show this directory's Project binding | -| `project create` | Create a Project and link this directory | -| `project link` | Link this directory to a Project | -| `project rename` | Rename the resolved Project | -| `project delete` | Delete a Project permanently after exact id confirmation | -| `project transfer` | Transfer a Project to another workspace after exact id confirmation | - -### `project env` — Manage environment variables for the active project - -| Command | Meaning | -| --- | --- | -| `project env add` | Create a new environment variable | -| `project env update` | Replace an existing environment variable's value | -| `project env list` | List environment variable metadata for a scope (no values) | -| `project env delete` | Delete an environment variable from a scope | - -## `postgres` — Manage Prisma Postgres databases for a project - -| Command | Meaning | -| --- | --- | -| `postgres list` | List Prisma Postgres databases for the resolved project | -| `postgres show` | Show database metadata without secret values | -| `postgres create` | Create a Prisma Postgres database and print its one-time connection URL | -| `postgres usage` | Show usage metrics for a database | -| `postgres delete` | Delete a database after exact id confirmation | - -### `postgres backup` — Inspect and restore platform-created database backups - -| Command | Meaning | -| --- | --- | -| `postgres backup list` | List backups for a database | -| `postgres backup restore` | Restore a database from a backup after exact id confirmation | - -### `postgres connection` — Manage one-time-view database connection strings - -| Command | Meaning | -| --- | --- | -| `postgres connection list` | List database connection metadata without secret values | -| `postgres connection create` | Create a database connection and print its one-time connection URL | -| `postgres connection rotate` | Rotate connection credentials and print the new one-time connection URL | -| `postgres connection delete` | Delete a database connection after exact id confirmation | - -## `bucket` — Manage object-store buckets for a project - -| Command | Meaning | -| --- | --- | -| `bucket list` | List object-store buckets for the resolved project | -| `bucket create` | Create an object-store bucket | -| `bucket delete` | Delete a bucket and all its access keys | - -### `bucket key` — Manage access keys for an object-store bucket - -| Command | Meaning | -| --- | --- | -| `bucket key list` | List access keys for a bucket | -| `bucket key create` | Create a bucket access key and print its one-time credentials | -| `bucket key delete` | Revoke and delete a bucket access key | - -## `branch` — View your Platform branches - -| Command | Meaning | -| --- | --- | -| `branch list` | List Platform branches for the resolved project | - -## `git` — Manage Git repository connections for a project - -| Command | Meaning | -| --- | --- | -| `git connect` | Connect the resolved project to a GitHub repository | -| `git disconnect` | Disconnect the GitHub repository from the resolved project | - -## `service` — Manage services and deployments for a project - -| Command | Meaning | -| --- | --- | -| `service list` | List the services in a project | -| `service create` | Create a service in a project | -| `service show` | Show the service and its current deployment | -| `service open` | Open the service's live URL | -| `service logs` | Read logs for a deployment of the service | -| `service delete` | Delete the service from the resolved branch | - -### `service deployment` — Manage deployments for a service - -| Command | Meaning | -| --- | --- | -| `service deployment list` | List deployments for the service | -| `service deployment show` | Show a deployment in detail | -| `service deployment promote` | Promote a deployment to production by rebuilding with production env vars | -| `service deployment rollback` | Roll back production to a previous deployment | -| `service deployment start` | Start a stopped deployment | -| `service deployment stop` | Stop a running deployment | -| `service deployment delete` | Delete a deployment and the artifact it holds | - -### `service domain` — Manage custom domains for a service - -| Command | Meaning | -| --- | --- | -| `service domain add` | Register a custom domain on the service's production branch | -| `service domain show` | Show custom domain status and certificate details | -| `service domain delete` | Delete a custom domain from the service | -| `service domain retry` | Retry custom domain DNS verification and TLS provisioning | -| `service domain wait` | Wait until a custom domain is active or failed | - -## `contract` — Define and emit your application data contract - -| Command | Meaning | -| --- | --- | -| `contract emit` | Emit your contract artifacts | -| `contract infer` | Infer a PSL contract from the live database schema | -| `contract format` | Format your PSL contract source | - -## `db` — Verify, sign and update your database against the contract - -| Command | Meaning | -| --- | --- | -| `db init` | Bootstrap a database to match the current contract and sign it | -| `db schema` | Inspect the live database schema | -| `db sign` | Sign the database with your contract so you can safely run queries | -| `db update` | Update your database schema to match your contract | -| `db verify` | Check whether the database marker and live schema match your contract | -| `db migrate` | Apply planned migrations to advance the database | - -## `migration` — Plan, inspect and scaffold on-disk migrations - -| Command | Meaning | -| --- | --- | -| `migration plan` | Plan a migration from contract changes | -| `migration new` | Scaffold a new migration for manual authoring | -| `migration list` | List on-disk migrations per contract space | -| `migration show` | Display migration package contents | -| `migration status` | Show migration path and pending status | -| `migration log` | Show executed migration history | -| `migration graph` | Show the migration graph topology | -| `migration check` | Verify artifact and graph integrity | - -Applying migrations is `db migrate`. - -### `migration ref` — Manage named refs that point at contracts - -| Command | Meaning | -| --- | --- | -| `migration ref list` | List every named ref | -| `migration ref set` | Point a ref at a contract | -| `migration ref delete` | Delete a ref | - -## `orm` — Initialize a Prisma ORM project - -| Command | Meaning | -| --- | --- | -| `orm init` | Initialize a new Prisma Next project | - -## `agent` — Manage Prisma skills for AI coding agents - -| Command | Meaning | -| --- | --- | -| `agent install` | Install Prisma skills for AI coding agents | -| `agent update` | Refresh Prisma skills for AI coding agents | -| `agent status` | Show installed Prisma skills | - -## `telemetry` — Inspect and change anonymous CLI telemetry - -| Command | Meaning | -| --- | --- | -| `telemetry status` | Show whether anonymous CLI telemetry is enabled and why | -| `telemetry enable` | Enable anonymous CLI telemetry | -| `telemetry disable` | Disable anonymous CLI telemetry | diff --git a/.drive/projects/prisma-cli-v8/assets/engine/credential-manager-design.md b/.drive/projects/prisma-cli-v8/assets/engine/credential-manager-design.md deleted file mode 100644 index 4ce85305..00000000 --- a/.drive/projects/prisma-cli-v8/assets/engine/credential-manager-design.md +++ /dev/null @@ -1,898 +0,0 @@ -# Credential manager — design, revision 5 (normative, final) - -Status: operator-designed session model (2026-08-10). Rev 5 replaced -the rev-3/4 grants model; this final text folds the delta review -(architect + PE) AND the operator's process-pinning concurrency -ruling, which deletes most of the reviewed locking machinery — §10 -records what was adopted and what that ruling made moot. Revisions -1–4 are in git history. NORMATIVE for the implementation on PR #130. - -## 1. The reality this models - -Validated against pdp-control-plane source (2026-08-10): - -- `prisma auth login` can produce exactly ONE kind of thing: a - workspace-scoped OAuth token pair (access + refresh). The user - picks the workspace on the consent screen; the CLI CANNOT request - or pin a workspace — the authorize request carries no workspace - parameter (`AuthorizeSearchSchema`). The CLI learns which - workspace it got by decoding the token's `workspace_id` claim. - Refresh cannot re-scope. -- Refresh tokens are single-use WITH a 10-second reuse grace - (`StaticClientOAuthProvider`: rotation marks the token used; one - replay within 10s succeeds and issues its own pair; later replays - are `invalid_grant`). Rotation does not revoke sibling pairs — - any successfully issued pair remains valid on its own. Racing - refreshes are therefore SERVER-ABSORBED: whichever write lands - last, the file holds a working pair. Client-side coordination - beyond in-process dedup is unnecessary. -- `PRISMA_SERVICE_TOKEN` supplies a workspace-scoped bearer token - from the environment. No refresh, never stored. -- Tokens carry identity claims (`sub`) but the stored state records - and enforces NO identity (operator ruling). A wallet MAY hold - sessions created by different accounts; identity surfaces only as - a read-time claim decode (`whoami`). -- Legacy state: a JSON file of `{workspaceId, accessToken, - refreshToken}` entries plus a context sidecar holding the active - workspace id. §7 migrates it. - -The domain: a set of per-workspace sessions, one current. - -## 2. Entities - -```ts -/** The proof material. Only ever seen by the login flow (which - * mints it) and createSession (which stores it). */ -interface Credential { - readonly token: string; - readonly refreshToken: string | undefined; - readonly expiresAt: Date | undefined; -} - -/** "Logged-in-edness", scoped to a workspace. Identified to users - * by its workspace. The token is INTERNAL: it lives in the stored - * record, never on this public shape. */ -interface Session { - readonly workspaceId: string; - readonly workspaceName: string | undefined; // fetched once at creation (§3) - readonly expiresAt: Date | undefined; - readonly source: "stored" | "environment"; - readonly current: boolean; -} -``` - -- Sessions are KEYED BY WORKSPACE ID: at most one session per - workspace. Logging in to the same workspace again upserts the - record (same key, new credential) — whichever account minted it; - the store cannot hold two credentials for one workspace and does - not try (accepted, matches legacy). A credential backs at most - one session (never store one refresh token under two keys). -- The marker is called CURRENT everywhere (state field - `currentWorkspaceId`, list flag `current`, read - `currentSession()`). -- `source: "environment"` marks the ephemeral session composed from - `PRISMA_SERVICE_TOKEN` (§6). It never appears in `sessions()`. -- `whoami` decodes the current session's claims at read time. - -## 3. The CredentialManager interface (the SPI) - -Manages sessions: six user-facing operations plus one engine-facing -accessor (flagged §10; it exposes a capability the SDK consumes, -never token material to engine code). - -```ts -interface CredentialManager { - /** The session this PROCESS is acting as. Pinned at first read - * (§4): composed from the env token if set, else the file's - * current marker at that moment; later marker changes by other - * processes do not move it. This process's own mutations - * (createSession/useSession/endSession/endAllSessions) DO - * update it. Local-only: never touches the network. */ - currentSession(): Promise; - - /** The available sessions (auth workspace list), read fresh from - * the file. Local-only. Under an env override the file's - * current marker is still shown as `current` and the listing - * command states the env session is what is in force. */ - sessions(): Promise; - - /** Login's write. The caller names the workspace that identifies - * the session; for workspace-bound credentials the manager - * verifies the workspace_id claim matches and refuses on - * mismatch (a future multi-workspace credential makes the - * argument a real choice). Upserts by workspaceId, sets the - * file marker, becomes this process's current. The workspace - * name is fetched best-effort AFTER the write, outside the lock - * (§8), via the injected lookup — failure leaves it undefined, - * never fails login. */ - createSession(credential: Credential, workspaceId: string): Promise; - - /** Switch: sets the file's current marker AND this process's - * pinned session. */ - useSession(session: Session): Promise; - - /** Log out of one workspace: remove that session. If it was - * current (file marker or this process's pin), that current is - * cleared (no auto-promotion). */ - endSession(session: Session): Promise; - - /** Log out entirely: remove all sessions and the marker (also - * reaps legacy files, §7). Returns nothing: the COMMAND reports - * how many it ended, by calling `sessions()` before this. */ - endAllSessions(): Promise; - - /** ENGINE-FACING, not a user operation: the SDK TokenStorage - * view for one workspace's session. The engine forwards it into - * SDK client config and never calls its methods itself. */ - tokenStorage(workspaceId: string): TokenStorage; -} -``` - -Interface rules: -- The manager never talks to the user and never opens a browser. - `performLogin` returns the minted credential; the login command - calls `createSession`. The login flow's own SDK instance uses a - THROWAWAY in-memory TokenStorage (the SDK persists tokens through - its storage at callback time; that write must never reach the - manager — minting and custody stay separate). The manager's - storage is reachable only through `tokenStorage()`. -- Construction dependencies (injected by the bin): `env` (no - library below the manager reads `process.env`; `PRISMA_AUTH_FILE` - names the state file, `PRISMA_COMPUTE_AUTH_FILE` is the warned - deprecated alias) and - `fetchWorkspaceName(credential, workspaceId)` (the manager - constructs no API client). -- The manager resolves NO user input. Commands resolve refs against - `sessions()` (exact id, then case-insensitive name; ambiguity is - the command's error) and pass the matched Session. -- `useSession`/`endSession` treat the argument as a WORKSPACE - reference: only `workspaceId` is read, re-validated against - freshly-read state under the lock. If another process replaced - that workspace's session in between, the operation applies to the - replacement (the intent — switch to or log out of the workspace — - is workspace-keyed). No session for that workspace → structured - error. Passing a `source: "environment"` session is a misuse → - the same error. `useSession` on the already-current session - succeeds and changes nothing. -- Error single-sourcing: blank/whitespace service token → one - structured error raised identically from `currentSession()`, the - needs check, and the engine's request path; unreadable file → - `CLI.CREDENTIALS_UNREADABLE`; parse-corrupt file → signed out - (self-heals on next login), never an exception, never a write. - -Mutations under an env override (`PRISMA_SERVICE_TOKEN` set): -- `useSession`, `endSession` refuse with one structured error - family (why names the env var and whether stored sessions exist; - nextAction is the literal `unset` command). -- `endAllSessions` refuses when stored sessions exist and SUCCEEDS - AS A NO-OP when there are none (CI teardowns running `prisma - auth logout` with only the env token must not fail). Accepted, - stated: while the var is set, existing stored state cannot be - cleared. -- `createSession` is ALLOWED with a mandatory one-line notice that - the env token remains in force until unset. -- Reads work normally. - -## 4. Process pinning and engine integration - -**Process pinning (operator ruling).** A CLI process determines its -session ONCE: env token if set, else the file's current marker at -first read. That session is the process's identity for its entire -lifetime — another process switching the marker or replacing -records does NOT redirect a running process; new processes pick up -the new marker. The process's own auth mutations are the only thing -that move its pin. Consequences, normative: -- ONE stored-session API client per process, built lazily for the - pinned session and memoized for the run (no per-access - re-resolution, no cache invalidation machinery, no - "session-replaced" errors). The SDK's per-client refresh - single-flight therefore IS the per-process refresh dedup. -- Refresh writes are keyed by the pinned session's workspace id - ("by session identity"). Cross-process refresh races on the same - session need no client-side coordination (§1: server grace + - sibling-pair validity make either winner fine). The SDK's - compare-and-clear handles the stale-replay case benignly. -- A process whose pinned session is ended by another process - mid-run fails at its next request with the session-ended wording - (§6) — the honest outcome; nothing tries to re-pin. - -Engine integration: -- `Runtime.credentialManager: CredentialManager` replaces - `Runtime.getCredentials`, staged as before. The bin also injects - the CLIENT CONFIG: `{clientId, redirectUri, apiBaseUrl, - authBaseUrl}` — all four (the SDK's refreshing fetch requires the - full config even though only login paths read redirectUri). The - same config feeds `performLogin`. The engine's placeholder - constants stay deleted; the construction test seam RETURNS via - this injected config (harness points it at a local server). -- `ctx.session(): Promise` on EVERY context — - read-only, local-only (tested: no network I/O). Serves - `currentSession()` (the pin). -- `managesCredentials: true` puts `ctx.credentialManager` on the - context for exactly: `auth login`, `auth logout`, `auth workspace - list`, `auth workspace use`, `auth workspace logout`. `whoami` - uses `ctx.session()` only. -- **The ENGINE constructs and owns the management API client** - (`ctx.api`): the pinned session's client, once per process. - Stored session → the SDK's refreshing path with - `tokenStorage(workspaceId)` in the config. Env session → the - SDK's static-token path (`createManagementApiClient({baseUrl, - token})`), where the ENGINE reads `PRISMA_SERVICE_TOKEN` from the - injected `Runtime.env` for that token — the manager never exposes - token material — so nothing has to hand a credential back out of - the manager; no refresh machinery may exist for it; its error - mapping happens at the call site (the static path has no error - middleware). The auth commands that mutate state don't consume - `ctx.api` as the pinned session afterwards (whoami enrichment - runs in a fresh process). -- **The TokenStorage view**: bound to the workspace id, never to a - credential snapshot — `getTokens` re-reads the file on every call - and returns that workspace's current record. Write rules in §6. - All SDK methods including the required `clearTokens` are - implemented; the engine forwards the view and never calls it — - no exceptions. The engine's own token read for `ctx.spawn`'s - credential injection goes through the named operation - `activeAccessToken()` (§11.5, the S3 amendment as re-ruled after - the PR-136 review, 2026-08-11), so the rule stays absolute. -- Error unwrapping: the SDK's error middleware wraps non-SDK errors - into `FetchError(cause)`; the engine's mapping walks the cause - chain for BOTH `AuthError` and CLI structured errors, so - manager-raised errors surface as themselves. -- Names never refresh (accepted, stated): reads are offline, so a - renamed workspace keeps its stored name until the next login to - it. `list` renders a nameless session by its id. -- Harness: `createTestCli` seeds `{sessions?, currentWorkspaceId?, - credential?, environmentToken?}` over a mutable in-memory manager - with full state read-back, plus the client config (local - endpoint). `environmentToken` composes the env session and is - exported to each run's env as `PRISMA_SERVICE_TOKEN`, which is - where the engine reads it. - -## 5. Fixtures and required tests - -Fixture surface: client config injection (all four fields) pointed -at a local HTTP server scripting 401 → rotated pair → retry, -`invalid_grant`, 5xx/network-throw; a JWT minter (`sub`, -`workspace_id`, `exp`, `email` + an undecodable token); a -legacy-store builder (pointer valid/dangling/null/absent; one/many -entries; duplicate entries for one workspace; placeholder names; -entries without refresh tokens; corrupt context; wrong shape); a -deterministic clock; a way for a second process to hold the lock. - -Required tests: -- process pinning: marker moved by a second process mid-run → the - running process's requests still carry its pinned session's - tokens; a NEW process picks up the new marker; -- pinned session ended by a second process → next request fails - with session-ended wording (not the SDK's synthesized message, - not the transient error); -- two refreshers on one session across two processes: both - complete, the file ends with a valid pair (server-grace test via - the scripted endpoint); -- refresh rotation: only token fields written; name and marker - untouched; `expiresAt` re-derived; rotated pair persisted before - the new access token reaches the caller; -- `endSession` vs in-flight rotation on the same session: the - ended session stays gone (rotation must not resurrect it); -- login flow writes nothing through the manager (throwaway storage - observed; manager file untouched until `createSession`); -- `createSession` holds no lock during the name fetch (a second - process completes a mutation while the name request hangs); -- `createSession` claim/argument mismatch refusal; -- env session never refreshes (token endpoint not hit on 401); -- env-override matrix: every mutation × {unset, set, blank, - whitespace} — error family asserted, state-file bytes unchanged; - plus `endAllSessions` no-op success with zero stored sessions; -- end-current / sessions-held-none-current: one shared assertion - over `ctx.session()`, the needs check, and a bare `ctx.api` - touch; -- reads-never-write probe (filesystem spy: zero writes on every - read path including migration adoption); -- token-material leak scan (seed a known secret; assert absent from - stdout, stderr, debug logs, error meta, envelopes); -- lock: two concurrent mutations in different processes both land - (no lost update); a crashed holder's lock is taken over after - the stale threshold. - -## 6. Runtime flows (normative) - -**Unauthenticated.** `needs.credentials` → -`CLI.CREDENTIALS_REQUIRED` (exit 2, sign-in nextAction) before the -handler loads; bare `ctx.api` touch → same error at request time. -`whoami` → "signed out", exit 0. No auto-login. - -**Sessions held, none current** (migration rows; end-current): same -code, distinct why ("you have workspace sessions but none is -current") with nextActions `auth workspace use` and login. - -**Refresh.** Driven by the SDK on 401 through the bound -TokenStorage view; the exchange itself runs OUTSIDE the file lock -(§8) — only the resulting write takes it: -- `setTokens` (the rotation write): updates IN PLACE only `token`, - `refreshToken`, `expiresAt` (the proactive token-endpoint adapter - supplies the explicit OAuth lifetime; an SDK-driven rotation falls - back to the access token's claim) of its workspace's record. NEVER - creates - a record, NEVER moves the marker, NEVER touches the name. If the - freshly-read state has no record for that workspace (ended by - another process), refuse and throw — no resurrection. If the new - token's `workspace_id` claim disagrees with the bound id, refuse - (refresh cannot re-scope). If the record's credential changed - since the refresh started (a newer login), the write still lands - — either pair is valid (§1); last write wins. -- `clearTokensIfCurrent`: remove the record iff its stored pair - still exactly matches the pair that failed — exact over the - SDK's three compared fields (`workspaceId`, `accessToken`, - `refreshToken`). Clear the marker only if it names that record. - This match is what makes a stale replay's `invalid_grant` benign - when a newer pair is already stored — do not "simplify" it. -- `clearTokens` (required by the SDK's TokenStorage type; reached - by its internal fallbacks): removes only the bound record — same - slice as `clearTokensIfCurrent` without the match. It never - means "end all sessions". The engine never calls `sdk.logout()`. -- `withRefreshLock` is implemented as IN-PROCESS single-flight - only (the SDK requires the hook to lock at all; cross-process - exchange races are server-absorbed, §1). -- Preemptive refresh stays PROHIBITED (per-request resolution - keeps long runs current; a background refresher adds nothing). - -**Refresh failure discrimination.** `AuthError.refreshTokenInvalid` -is `true` only for HTTP 4xx + body error exactly `invalid_grant`: -- `true` → the SDK has run compare-and-clear; if the session - survived (newer pair stored), the retry proceeds — nothing - surfaced. If it cleared, `CLI.CREDENTIALS_REQUIRED`, expiry - wording; the ENGINE's mapping debug-logs endpoint status + - error value (the SDK hands the manager no status — the manager - debug-logs the clear attempt itself) - BEFORE the clear. -- any other `AuthError` → the manager re-reads state FOR THE - WORKSPACE THE CLIENT IS BOUND TO: record gone → - `CLI.CREDENTIALS_REQUIRED`, session-ended wording; otherwise a - transient auth-service error. A state check, never message - parsing. -- any non-`AuthError` from the refresh path (e.g. the SDK's - undecodable-token plain `Error`) → transient auth-service error; - nothing cleared; debug valve records it. -- non-auth failures (network, 5xx) → transient; NOTHING cleared. -The SDK version is exact-pinned; a test asserts clearing happens on -`invalid_grant` and nothing else. - -**Service token (env).** Composes as the process's pinned session -(`source: "environment"`), never stored, absent from `sessions()` -(the file's marked current stays shown; the listing states the -override). The ENGINE builds that session's client by reading -`PRISMA_SERVICE_TOKEN` from the injected `Runtime.env` itself: the -manager composes the session but never hands out the token. -Static-token client, no refresh; 401 → structured error -naming the env var; nothing cleared. `whoami` notes the override -when stored sessions exist. Blank/whitespace → the single -blank-token error. - -**Debug valve.** `PRISMA_NEXT_DEBUG` shape: source won, resolved -state-file path, pin decision, refresh attempted, endpoint status + -error field, lock acquire/release/takeover. Token material NEVER -appears in any log, error, meta, or envelope. - -## 6a. The commands - -Legacy names, unchanged — the session model makes them honest -("log in to a workspace" = create a session for it): - -- `auth login` — browser consent; user picks the workspace; - `createSession(credential, workspaceId-from-claims)`. -- `auth logout` — `sessions()` for the count, then - `endAllSessions()`; the command reports the count it ended. -- `auth whoami` — `ctx.session()`; identity for an ENV session - comes from decoding the env token (read from `ctx.env`); a stored - session's token is unreachable by construction (Session carries - none, whoami has no manager), so its identity comes from `/v1/me` - when online and offline whoami shows the workspace with no user. -- `auth workspace list` — `sessions()`, current marked, nameless - rows rendered by id. Under an env override the listing states - the env session is in force. -- `auth workspace use ` — resolve against `sessions()` - (command-side), `useSession(match)`. -- `auth workspace logout ` — resolve, `endSession(match)`; - prints the workspace it ended. - -**RULED (operator, 2026-08-10): `workspace use` SELECTS among your -sessions; it never creates one.** No session for X → structured -error: "no session for workspace X — run `prisma auth login` and -pick X in the browser" (nextAction: the literal `prisma auth -login`). No browser ever opens from `use`; session creation belongs -to `auth login` alone. (Matches §1: the consent flow cannot target -a workspace. Both reviewers independently concurred.) - -## 7. Migration from the legacy store - -Governing rule: **the migration read writes nothing.** - -| Legacy store state | Rule | -| --- | --- | -| Context file exists, pointer targets an existing entry | All entries adopted as sessions; that one current | -| Context exists, pointer dangles | All adopted; NO current | -| Context exists, `activeWorkspaceId: null` | All adopted; no current | -| No context, exactly one entry | Adopted, current | -| No context, multiple entries | All adopted; NO current (no coin flip) | -| Auth file missing / unparseable / wrong shape | No sessions. Never delete, never rewrite | - -Adoption rules (identity-blind — entries from any account adopt): -- Key and pointer-resolve on the token's `workspace_id` claim (the - legacy `credentialWorkspaceId`), not the hydrated display id. -- Entries whose token does not decode to a `workspace_id` are - ignored (unkeyable). -- Duplicate legacy entries for one workspace: the LAST wins - (matches legacy's latest-wins reads). -- Legacy placeholder names do not adopt: a name equal to - "Unknown workspace" or to the workspace id adopts as undefined. -- Entries without a refresh token adopt (reads until expiry, then - fail cleanly). -- `lastSeenAt` does not carry over; list order is store order. - -Materialization: the adopted view is written into the new -single-file format on the first mutation, writing the FULL adopted -set. The adoption decision is re-made INSIDE the lock beside the -mutation's re-read: if a new-format state exists at that point it -wins outright and no adoption occurs (a naive full-set write could -resurrect tokens another process already rotated). The new format -lives at the SAME PATH as the legacy auth file (ruled 2026-08-10: -one file, one world), so the first v8 mutation rewrites it in the -new shape and a still-installed legacy CLI reads signed-out from -then on — a loud, `prisma auth login`-fixable state, preferred over -two silently diverging auth worlds. Until that first mutation the -file stays untouched and the legacy CLI keeps working. The context -sidecar is reaped by `endAllSessions`, which clears everything. -New writes use mode 0600 and tighten looser permissions on first -write. Env naming (ruled with the implementation): -`PRISMA_AUTH_FILE` names the state file; `PRISMA_COMPUTE_AUTH_FILE` -is the warned deprecated alias (`PRISMA_PLATFORM_AUTH_FILE` never -existed in the repo). - -## 8. File, lock, and atomicity - -- **One file**, shape normative: `{ version, sessions: [{ - workspaceId, name?, token, refreshToken?, expiresAt? }], - currentWorkspaceId | null }`. No context sidecar. Every write - replaces the whole state. -- **Writes are atomic**: temp file in the same directory, fsync, - rename; mode 0600. -- **Reads never write; reads take no lock** (atomic rename - guarantees a complete state). -- **One short advisory lock for read-modify-write.** Every - mutation acquires it, re-reads, applies its slice, writes, - releases. Its ONLY job is lost-update prevention between - processes (two mutations touching different records must both - land). **No network I/O ever runs under it** — the token - exchange happens outside (§6), and `createSession`'s name fetch - happens after release, with a second minimal locked write that - sets `name` iff the record still exists. Holds are - milliseconds, so: no heartbeat, a small fixed stale threshold - (crashed-holder takeover), takeovers debug-logged. The rev-4 - heartbeat/exchange-timeout/steal apparatus existed to survive - network calls under the lock; with none, it is deleted. -- **Slices** (each mutation re-reads under the lock and modifies - only): - - | Mutation | May modify | - | --- | --- | - | `setTokens` (rotation) | token fields of its workspace's record | - | `clearTokensIfCurrent` | removes its record (three-field match); marker only if it names it | - | `clearTokens` | removes its record; marker only if it names it | - | `useSession` | marker only | - | `endSession` | one record; marker if it named it | - | `createSession` | one record (upsert) + marker | - | `createSession` name backfill | `name` of its record | - | `endAllSessions` | whole state | - - No mutation writes state read before lock acquisition. -- **Rotation durability**: the rotated pair is persisted (fsync + - rename) before the new access token reaches any caller. - -## 9. Change surface on PR #130 - -Engine (`packages/cli-engine`) — a rename/reshape pass over the -landed a8ef3fb plus two behavior corrections in `api-client.ts`: -1. client construction returns to the engine (revert to the - pre-a8ef3fb shape, then re-apply the §6 error mapping; config - injection replaces the deleted `createSdk` seam). The run-long - client memoization STAYS (process pinning makes it correct); -2. the failure mapping re-reads state for the BOUND workspace, not - `currentSession()`. -Renames/reshapes: entity types per §2 (Identity/GrantSummary/ -method axis deleted; Session is one-of-many with `current`); SPI -per §3 (`tokenStorage()` added; `apiClient`/`rememberWorkspaceName` -gone); error wording (sessions-held-none-current etc.); ref -resolution moves command-side (codes renamed to session -vocabulary); harness seeding per §4. The `managesCredentials` -capability and `defineCommand` overloads survive unchanged. - -Auth module (`packages/cli/src/auth`): the manager implementation -(§7 migration, §8 file+lock, the TokenStorage views, process -pinning); `performLogin` returns the credential and uses a -throwaway storage; `fetchWorkspaceName` injected. Legacy operations -remain for the legacy shell until S2d. - -v8 tree (`packages/cli/src/v8`): auth family onto the manager with -LEGACY names (`workspace-logout.ts` stays; no forget; `logout ---workspace` still does not return — superseded by `workspace -logout`). Command-side ref resolution. - -Docs: parity-divergences auth sections rewritten — remaining -divergences: error-code map, exit unifications, whoami shape, -env-override mutation refusals (exact error family and exit code, -incl. the `auth logout` no-op rule), the list JSON shape when an -env session is in force, orphan-reaping logout, names no longer -refreshed on read. Amend s2a contract §3/§4/acceptance and S2 -overview auth rows. - -## 10. Disposition record - -Rev 5 (2026-08-10): operator-designed session model. Operator -rulings: per-workspace sessions keyed by workspace id; identity -rule dropped (wallet identity-blind; reviewer-proposed -cross-account disclosure rules NOT adopted — "no different to -today"); legacy command names return; grants vocabulary dead; -`apiClient()`/`rememberWorkspaceName` off the SPI; -`createSession(credential, workspaceId)`; `useSession`/`endSession` -take `Session`; `workspace use` selects only; **process pinning** — -a process's session is fixed at first read, other processes' -switches never redirect it, and racing refreshes are accepted -(server-verified: 10s reuse grace + sibling-pair validity). - -Delta review folded where the pinning ruling left it standing: -throwaway login storage (PE); `clearTokens` bounded to its record -(PE); name fetch outside the lock via injected -`fetchWorkspaceName` (architect + PE); env `endAllSessions` no-op -rule (PE); client config four-field list + env static-token -construction path + cause-chain unwrapping (PE); non-AuthError -refresh throw → transient (PE); migration additions: claim keying, -last-wins duplicates, placeholder names, refresh-token-less -entries, lock-held adoption decision (architect + PE); bound- -workspace failure mapping (architect); `Session` flattened to -`workspaceName` + uniform `current` (architect — VETO-ABLE -deviation 1); `tokenStorage(workspaceId)` as the seventh -engine-facing member (architect — VETO-ABLE deviation 2). - -Made MOOT by process pinning (not adopted): sessionEpoch binding -and session-replaced errors; per-access client cache with -keys/eviction; call-chain-scoped lock re-entrancy (no nested -locking remains — `withRefreshLock` is in-process single-flight, -mutations take the short file lock directly); heartbeat/exchange- -timeout/stale ordering apparatus; cross-account race guards. - -## 11. Revision 6 — the environment credential is not a session - -Operator rulings, 2026-08-10, after review of the rev-5 implementation, -with the architect and principal-engineer passes on this delta folded -in. Rev 6 supersedes the parts of §§1–9 listed in §11.9; everything not -listed stands. NORMATIVE. - -**The mistake rev 5 made.** It modelled the `PRISMA_SERVICE_TOKEN` -credential as a `Session`. It is not one, and forcing it into that -shape produced four defects that are all the same defect: - -- `Session.source` existed to say "this one is not really a session"; -- `current: true` was hardcoded on it, because it has no marker to - compare itself against — so `current` meant "the file's marker names - this" in `sessions()` and "this is what the process acts as" in - `currentSession()`; -- `workspaceId: ""` was written when the token's claims did not name a - workspace, because a non-session was forced to carry a session's key; -- `useSession`/`endSession` needed a guard rejecting an environment - session, because it was shaped like a stored one. - -None of them needs fixing separately. They stop existing. - -### 11.1 The three things, separated - -**A session** is a stored logged-in-ness for one workspace. It is the -only thing called a session: what `sessions()` lists, what -`selectSession` selects, what `endSession` ends. - -```ts -interface Session { - readonly workspaceId: string; - readonly workspaceName: string | undefined; - /** The STORED ACCESS TOKEN's expiry, which rotation changes. Not a - * deadline on the logged-in-ness. */ - readonly expiresAt: Date | undefined; -} -``` - -**The selection** is one scalar of stored state — the workspace whose -session is used where a session is needed. Absent means none selected. -It is read directly, never inferred from a flag on each element. One -read returns both, because reads take no lock (§8) and two reads could -straddle a write: - -```ts -interface StoredSessions { - readonly sessions: readonly Session[]; - readonly selectedWorkspaceId: string | undefined; -} -``` - -Invariant, enforced by the manager: `selectedWorkspaceId` either names -one of the listed sessions or is absent. A dangling selection never -escapes the manager, so no consumer handles that case. - -**The active credential** is what this process authenticates as. The -command-visible shape carries no token material: - -```ts -interface ActiveCredential { - /** Absent when nothing names it — an environment token whose claims - * carry no workspace. Never the empty string. */ - readonly workspaceId: string | undefined; - readonly workspaceName: string | undefined; - readonly expiresAt: Date | undefined; - /** Decoded from the credential's own claims by the manager, so no - * command ever holds a token to decode. */ - readonly identity: CredentialIdentity | undefined; - readonly origin: CredentialOrigin; -} - -interface CredentialOrigin { - /** Exists to be PRINTED — it feeds whoami's `source` field verbatim. - * Outside whoami's renderer and the credential-rejected error - * constructor, comparing against it is a defect. */ - readonly source: "stored" | "environment"; -} -``` - -`CredentialOrigin` says where the credential came from. That is a real -question about the resolution — unlike rev 5's `Session.source`, which -asked it of the session. It carries no prose and no next actions: the -manager never talks to the user (§3). Where wording must differ by -origin, the difference lives in ONE error constructor in -`credential-errors.ts`, which is where wording already lives. - -Absence is `undefined` in every in-memory shape. `null` survives only -in the on-disk JSON. - -**Vocabulary, ruled.** One word per concept. In code the word is -SELECTED: `selectedWorkspaceId`, `selectSession(workspaceId)`, and the -reason `sessions-held-none-selected`. Two things deliberately keep -"current" and are not to be renamed: the on-disk field -`currentWorkspaceId` (no migration for a rename), and the user-facing -surface — the command `auth workspace use` and `auth workspace list`'s -`context.currentWorkspaceId` and per-item `current`, all of which are -contracts. `ctx.session()` becomes `ctx.activeCredential()`; leaving it -named `session` reproduces the mistake one layer out. - -### 11.2 Refresh follows the credential, not its origin - -A credential refreshes if it has a refresh token. Where it came from is -irrelevant. Nothing hard-codes "environment means never refresh". The -engine builds one client, always the refreshing one, over the storage -the manager hands it. `ClientBinding` goes entirely — not just its -`source` field — along with the static-path 401 inspection, because the -mapping asks the manager for the active credential rather than -remembering a binding of its own. - -**Why the uniform path, when no credential source exercises it today.** -A single environment variable supplies one bearer string, so an -environment credential cannot currently carry a refresh token and the -memory-backed rotation is unreachable in practice. The uniform path -exists to delete a construction branch, not to serve a future feature. -Do not "clean it up" as dead code. - -**Which storage, chosen once.** The choice is made when the pin -resolves, and each storage has exactly one source of truth. The -conditional is in which storage is constructed, never inside one that -checks at write time whether it has a home. - -- **File-backed**, for a credential with a home record. Unchanged from - §4: `getTokens` re-reads the file on EVERY call; writes take the - short lock. No memory layer may sit in front. That read-through is - what lets the SDK recover when another process has already rotated — - this process sees the newer pair, skips the exchange, and retries. A - cache would spend a refresh token another process already used and - end in a spurious "sign in again" while the file holds a working - pair. -- **Memory-backed**, for a credential with no home record. Reads and - writes are process memory; nothing survives the process. It closes - over a local variable, is never given the state file's path, and - touches no file on any method — including `clearTokens`, which the - SDK calls when `clearTokensIfCurrent` is absent. An environment - credential whose workspace matches a stored session must not be able - to delete that session. - -The SDK's `Tokens` requires `workspaceId: string`. The memory-backed -storage supplies the claim when the credential has one and a fixed, -obviously-not-a-workspace constant when it does not. That value never -leaves the manager and is never the empty string. - -**A 401 that could never be renewed.** This is the path that actually -runs today. The SDK raises `AuthError("No refresh token available")` -with `refreshTokenInvalid` false, never touching the token endpoint. -It must NOT fall through to the session-ended mapping, which is untrue -and whose remedies do not apply, nor to the transient one, which tells -a CI job to retry a permanent failure forever. The engine discriminates -by state, never by message: after the failure it asks the storage for -the tokens, and a set with no refresh token could never have been -renewed. The result is one credential-rejected error whose wording -follows `origin.source` — for an environment credential that reproduces -today's `AUTH.SERVICE_TOKEN_REJECTED` naming the variable. - -This also repairs a rev-5 defect: §7 adopts legacy entries with no -refresh token and says they "fail cleanly at expiry". They do not — -they reach the same line and surface as a transient error advising a -retry. One fix covers both. - -**Known limit.** Real service tokens name their workspace through a -`sub: "workspace:"` claim rather than `workspace_id`. If they ever -gain refresh tokens, the SDK's own workspace extraction would throw on -the rotated token. - -### 11.3 What the pin holds - -Rev 5's pin is a workspace id re-resolved against the file on every -read. Rev 6 pins THE DECISION — which credential, and from where — at -first read, and keeps reading the material through the storage on every -call. Nothing pins a token value. A session ended by another process -mid-run therefore still fails with the session-ended wording, and a -session replaced by another process still recovers through the SDK's -re-read. - -### 11.4 Custody, restated - -§4's "the manager never exposes token material" means **never to -commands**. The engine may hold credentials — it must, to authenticate. -The manager hands the engine what it needs through -`activeCredentialStorage()`; `ActiveCredential` and `Session` still -carry no token, which is the property the rule protects. The rev-5 -wording, read as absolute, is what forced the engine to reach around -the manager into `Runtime.env`. - -### 11.5 The interface - -```ts -activeCredential(): Promise; -sessions(): Promise; -createSession(credential: Credential, workspaceId: string): Promise; -selectSession(workspaceId: string): Promise; -endSession(workspaceId: string): Promise; -endAllSessions(): Promise; -/** ENGINE-FACING. Zero-argument: process pinning already ruled there - * is one credential per process, and an environment credential may - * have no workspace id to key on. Only valid once activeCredential() - * has returned non-null; the engine resolves that first. */ -activeCredentialStorage(): Promise; -/** ENGINE-FACING (S3). The active credential's ACCESS token, read - * fresh on every call, for handing to a child process. With options, - * refreshes or refuses a token that lacks the required remaining - * lifetime. Never the refresh token. */ -activeAccessToken( - options?: ActiveAccessTokenOptions, -): Promise; -``` - -All three mutations are workspace-id-keyed, symmetric with -`createSession`. `selectSession` returns the selected `Session`, which -`auth workspace use` renders. `tokenStorage(workspaceId)` is deleted -rather than reshaped: nothing needs storage for a workspace other than -the active one, and the parameter implies an axis of variation the -system does not have. - -S3 amendment (2026-08-11, re-ruled after the PR-136 architect review): -the engine forwards the storage `activeCredentialStorage()` returns -into SDK client config and never calls its methods itself — no -exceptions. What the spawn path needs is a manager OPERATION, not a -carve-out: the interface gains `activeAccessToken()`, consumed by the -delegated-credential preflight and by `ctx.spawn`'s credential injection -in the engine's spawn module (`packages/cli-engine/src/execution/spawn.ts`, -`spawnToken`). It is read at spawn time and handed to the child as -`PRISMA_SERVICE_TOKEN` (+ `PRISMA_WORKSPACE_ID` when the credential -names a workspace; when it names none, an inherited -`PRISMA_WORKSPACE_ID` is DELETED from the child environment — the two -variables are one protocol, written as a unit). The injected token is -a snapshot; the child never refreshes; the refresh token is never -injected. The read builds no second API client, so the -one-client-per-process invariant of this design HOLDS: the engine's -pinned refreshing client remains the only client ever constructed in -the process (composer's in-process leg is authenticated by injecting -that same `ctx.api` through its `deps.client` seam, not by composing -another client from env). For an environment-only manager the -operation is a pass-through of the env token — no storage involved. - -S3 amendment (2026-08-14): the child still receives only an access-token -snapshot, but a stored OAuth session is no longer rejected merely because -that snapshot is inside the five-minute window. Before the handler runs, the -engine asks `activeAccessToken(options)` to refresh the pair under the -manager's storage lock, persist the rotation, and return the new access token. -The shipped manager receives a host-side token-endpoint adapter at construction; -the manager remains the sole owner of storage reads and writes, and the -refresh token is never added to child env. The spawn-time call is another -validated fresh read, so rotation by another process between preflight and -spawn is still observed without handing the child an unchecked replacement. - -### 11.6 whoami - -`whoami` asks for the active credential's identity and renders it. It -does not branch on origin and decodes nothing itself. `/v1/me` remains -an online enrichment and WINS where it disagrees with the claims; the -claims are the offline fallback. There is one identity type, -`CredentialIdentity`, for both the claimed and the fetched identity — -the command's own `SessionIdentity` is deleted. - -With no workspace, `whoami` omits the workspace row and its JSON -`workspace` is `null`. It never prints an empty string or `undefined`. - -### 11.7 Mutations while an environment credential is in force - -**RULED: the refusals go.** Rev 5 refused `useSession` and -`endSession`, and refused `endAllSessions` unless the store was empty, -while `PRISMA_SERVICE_TOKEN` was set. That rule existed because the -environment thing was a session occupying the current slot. It is not -one. Selecting or ending a stored session while an environment -credential is in force is coherent: it changes stored state, and this -process keeps authenticating as the environment credential. - -All three now succeed, each printing the one-line notice `createSession` -already prints — that the environment credential remains in force until -the variable is unset. The `endAllSessions` CI carve-out disappears -with the rule it worked around: it simply clears the store. - -### 11.8 Removal is idempotent - -`endSession` on a workspace with no session succeeds: the postcondition -is identical either way. The useful error — a workspace reference the -user never had — is raised earlier and command-side, when the ref fails -to resolve against `sessions()`, so `AUTH.NO_SESSION_FOR_WORKSPACE` -still reaches a user who mistypes. What changes is only the race: a -session removed by another process mid-command now exits 0 rather than -exit 2 with an untrue message. - -`selectSession` is NOT idempotent and still refuses a workspace with no -session: there is no state in which it would afterwards be selected. - -### 11.9 Superseded - -- §1: "No refresh, never stored" — reword as a fact about the token - shape, not a rule: it carries no refresh token, so nothing rotates. -- §2: `Session.source` and `Session.current`; "the marker is called - CURRENT everywhere"; "whoami decodes the current session's claims". -- §3: `currentSession()`; the `sessions()` return type; - `useSession`/`endSession` taking a `Session`; the environment-session - misuse error; ALL the env-override mutation refusals (§11.7). -- §4: `ctx.session()`; `tokenStorage(workspaceId)`; the env - static-token construction path and the engine reading - `PRISMA_SERVICE_TOKEN` itself; "ONE stored-session API client" loses - its qualifier; refresh writes are keyed by the credential's home - record, when it has one; the harness `currentWorkspaceId` and - `environmentToken` seed shapes. -- §5: "env session never refreshes" — the premise becomes "it has no - refresh token"; the env-override matrix follows §11.7; the - `ctx.session()` shared assertion is renamed. -- §6: "no refresh machinery may exist for it"; the service-token 401 - path; the `sessions-held-none-current` reason name. -- §6a: the whoami identity split. -- §9: describes rev-5 work; history, not instruction. - -Unchanged: the migration (§7), the file, lock and atomicity rules (§8), -and process pinning itself as narrowed by §11.3. - -### 11.10 Tests this delta requires - -1. Environment credential with no refresh token: 401, token endpoint - not hit, the credential-rejected error naming `PRISMA_SERVICE_TOKEN`. -2. Environment credential WITH a refresh token: 401, rotation, retry - succeeds, state file bytes unchanged, a second request in the same - process carries the rotated token. -3. Environment credential whose workspace matches a stored session, - refresh answers `invalid_grant`: the stored session survives and the - file is byte-unchanged. -4. Stored session with no refresh token (§7's migration case): the - error says sign in again, not retry. -5. Cross-process rotation recovery: B rotates, A gets a 401, A's - re-read sees B's newer pair and retries without hitting the token - endpoint. -6. `endSession` on a workspace with no session writes nothing and - exits 0; `auth workspace logout X` where another process removed X - mid-command exits 0. -7. `activeCredential()` with a claimless environment token: - `workspaceId` is `undefined`, and neither the human card nor the - JSON renders an empty string or `undefined`. -8. Every mutation succeeds while `PRISMA_SERVICE_TOKEN` is set, each - printing the in-force notice (§11.7). 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 deleted file mode 100644 index 627dfdb1..00000000 --- a/.drive/projects/prisma-cli-v8/assets/engine/daemon-library-notes.md +++ /dev/null @@ -1,60 +0,0 @@ -# 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 deleted file mode 100644 index a0202cf7..00000000 --- a/.drive/projects/prisma-cli-v8/assets/engine/engine-interface-draft.ts +++ /dev/null @@ -1,1772 +0,0 @@ -/** - * 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, command families · - * v8 packaging and residue rulings, amended in review round 2 - * (normalized definition shapes, defineCommandFamily, one severity - * scale, phantom-typed arg specs, ./testing subpath): 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 - * family-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/. - * Amended 2026-08-10 for the credential-manager design rev 5 — the - * SESSION MODEL (credential-manager-design.md, normative; rev 5 - * replaced rev 4's grants model): §4 gains ctx.session and the - * CredentialManager entity surface (ctx.getCredentials removal is - * STAGED — the engine still carries it until the swap's final stage); - * §6 gains the managesCredentials capability; §10 gains - * Runtime.credentialManager and the injected client config; §11 gains - * manager seeding + fixtures. - * Amended 2026-08-10 for the ENGINE INTERACTION AFFORDANCES (operator - * rulings): consent tokens with the shared --confirm flag (§4a), - * ctx.openUrl (§4), and prompt.browserWait (§4a). All three are - * engine-owned so command code never reads TTY or CI state and never - * invents its own consent-skipping flag. - * Amended 2026-08-11 for S3 (the TERMINAL HANDOFF, contract - * s3-composer.md): §4 gains ctx.spawn and §4c its shapes + - * exitWithChildStatus; §6 gains the SpawnDeclarations (maySpawn) and - * the two kind amendments (a maySpawn command owns structured stdout - * while routing child output to diagnostics in json mode; a session settles non-zero through - * exitWithChildStatus and no other way); §10 gains Runtime.spawn. D1 - * rulings: abort-ladder grace 5s; near-expiry refusal threshold 5min. - * Amended 2026-08-14: maySpawn commands now support json. Human mode - * still delegates the terminal; json mode routes child output to - * diagnostic stderr and preserves framed stdout through settlement. - * Re-amended after the PR-136 review round: handing credentials to the - * child is a PRECONDITION, `needs: { credentials: 'child' }` — the - * separate credentialsForSpawn declaration is gone, and the entailment - * (child credentials imply the credentials need) is structural. The - * manager gains the named engine-facing operation activeAccessToken() - * for delegated preflight and the spawn-time read, so the "engine - * never calls storage methods" rule is absolute — no sanctioned - * exception. - * exitWithChildStatus(opts?) takes { nextActions? }, rendered - * to stderr before the exit (R-S3-4's reproduce hint). - * Amended 2026-08-11 (operator ruling) — SIGNAL SETTLEMENT IS THE - * ENGINE'S: a run a delivered signal terminated settles 128+signal - * from the engine's own record of that signal, whatever its handler - * returns (EXIT CODES below). - * Amended 2026-08-11 (operator review of composer#220) — THE ENGINE - * RECORDS THE CHILD, AND OWNS HOW A HANDOFF SETTLES. §4 gains - * ctx.lastChild(): the run's most recent completed child, kept by the - * engine because ctx.spawn already mints every ChildResult. And - * exitWithChildStatus LOSES its child argument — it settles from that - * record. Two consequences, both deliberate. Handlers stop threading - * the child result out of whatever layer spawned it (composer was - * hand-rolling a recorder for exactly this). And the settlement - * ORDERING becomes the engine's: a signal-killed child settles as the - * abort — 128+signal, no envelope, no nextActions — even when the - * caller passed nextActions, so a Ctrl-C'd converge never carries a - * reproduce hint. The "invented child result" fence disappears with - * the argument that made the misuse possible; the maySpawn fence - * stays, joined by a construction error when no child ran at all. - * - * 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. The - * amendment also adopts the `fix` → typed `nextActions` rename - * (operator ruling, 2026-08-09), for the same reason. - * - * Everything a 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 command - * presentation on the error path. The primary error is severity - * 'error' by definition and carries its own typed `nextActions` - * (operator ruling, 2026-08-09: `fix` renamed — fix and nextActions - * solved the same problem); the envelope copies them. - * - * PRECONDITIONS (a command's `needs`) are enforced by the engine BEFORE - * the handler runs: 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 command-family-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 emits one - * StreamEvent per line. Format selects output shape ONLY (operator - * ruling, 2026-08-09): interactivity is detected from the environment - * (TTY stdin outside CI) and overridden by - * `--interactive`/`--no-interactive`. An interactive json run may - * prompt — the prompt UI writes to stderr, so stdout stays a clean - * frame stream; a non-interactive prompt with no default fails - * structurally. - * Commentary is filtered by `--log-level ` - * (default info); `--verbose` is shorthand for `--log-level verbose`; - * `-q/--quiet` for `--log-level error` (operator ruling, 2026-08-09: a - * log-level alias only, not otherwise retained — it never changes what - * a completed result renders). - * CHANNELS, human mode (operator ruling, 2026-08-09): decoration goes - * to stderr, machine-usable payload to stdout. Human Blocks, - * next-action lines, and diagnostics are presentation prose on STDERR; - * the `Presentations.stdout` payload lines (and `output` events with - * channel 'data') are the only writes to STDOUT — human mode is - * pipe-clean. json mode is unchanged: the frame stream owns stdout. - * The engine injects the shared flag family on every non-server - * command: --format/--json, --log-level/-v/--verbose, -q/--quiet, - * -y/--yes, --confirm (repeatable), --interactive/--no-interactive, - * --color/--no-color. - * Commands cannot declare flags with those names. Declared 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. The engine owns the whole - * signal policy (operator ruling, 2026-08-09): the first delivered - * signal fires context.signal and awaits teardown (settling 130/143); - * a second exits immediately through the runtime's exit proxy — the - * engine ends the process only ever via Runtime.exit. - * - * The signal codes are the ENGINE's to settle, from its own record of - * the signal it delivered, and never from anything a handler returns - * (operator ruling, 2026-08-11). A run a delivered signal terminated - * settles 128 + that signal's number for BOTH command kinds — - * including the handler that caught context.signal, cleaned up, and - * returned successfully. The exit code states how the RUN ended, not - * how well the cleanup went, so a session that shuts down cleanly on - * Ctrl-C settles 130 while still reporting a completed envelope. No - * handler can author these codes: documented codes stop at 99, and - * the child-status bypass takes its code from the engine's own record - * of the child rather than from anything the handler hands back — the - * handler names no child and no code at all. Two codes are exempt, - * because neither was this CLI's to - * state in the first place, and both pass through verbatim: a real - * child's status (the child owned the terminal and the signal reached - * it too — that is why exitWithChildStatus reports 128+signal itself) - * and a server command's protocol conclusion. - */ - -// ———————————————————————————————————————————————————————————————————————— -// 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?, nextActions: readonly NextAction[] (always - * present, empty when there are none), where?, meta?, docsUrl? } - * `fix` prose is gone (operator ruling, 2026-08-09): remediation is - * typed nextActions, rendered as → lines in human mode. - * Field-for-field the settled error envelope (ADR 239) minus `ok` — - * identical scales included; the two shapes never diverge (the ADR - * 239 amendment adopts the same rename). - * - * 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 (one name — - * the --log-level flag selects a Severity). 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 Format = 'human' | 'json' - -// ———————————————————————————————————————————————————————————————————————— -// §1 Events — R14: one engine vocabulary, command-family 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 command family's extension: passed through to - * machine consumers untouched, never interpreted by the engine, - * documented and versioned by the owning command family 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 NEVER aggregated into any envelope (`remediation` included — it - * is transcript-only, framed in json mode, unrendered in human mode). - * Follow-ups are handler-owned: completed via `presentations.next`, - * errored via the error's own `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 - } - /** Transcript-only: framed in json mode, never rendered in human - * mode, never aggregated into any envelope. */ - | { 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 - * handler→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; json → json + next. Human rendering writes the blocks, - * next-action lines, and diagnostics to stderr and the `stdout` lines - * to stdout (§header CHANNELS). - * - * 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[] - /** Only the active format's presentation is materialized; the other - * format's fields are normalized to empty. `json` stays undefined - * when the handler supplied no json presentation — the envelope's - * `result` then falls back to `data`. */ - 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), rendered to - * stderr; `stdout` is the machine-consumable data lines — what a pipe - * receives, the human mode's only stdout writes; `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 on stderr (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 FAMILY-level fact (one command family, one section) -// ———————————————————————————————————————————————————————————————————————— - -/** - * A command family's named slice of prisma.config.ts, declared once in - * the family's declaration (§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 family 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 -} - -/** Diagnostics on an OK validation are warnings: the engine writes them - * to stderr as commentary (log-level filtered, human and json alike); - * they never enter the stream or the envelope (operator ruling, - * 2026-08-09). */ -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 section'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 - - /** The session this process is acting as (the manager's - * currentSession() pin), or null when signed out — on EVERY - * context. Read-only and local-only: safe to call anywhere; never - * touches the network. Raises the same single-sourced structured - * errors as the needs check for broken-but-not-signed-out states - * (sessions held none current; blank env token). - * ctx.getCredentials is DELETED (staged: the engine carries it - * until the swap's final stage) — the context ends with fewer auth - * surfaces than before: `api` + `session`, plus - * `credentialManager` for the commands that declare the §6 - * capability. */ - readonly session: () => Promise - - /** The Management API client, constructed and owned by the ENGINE: - * the pinned session's client, built from the injected client - * config on first method CALL, once per run (process pinning makes - * the memoization correct). A stored session gets the SDK's - * refreshing path over manager.activeCredentialStorage(); an env - * session gets the SDK's static-token path with its error mapping - * at the call site. Request failures pass through the engine-side - * mapping (design §6): refreshTokenInvalid === true → the expired - * CLI.CREDENTIALS_REQUIRED; any other SDK AuthError → a state - * re-read for the workspace the client is BOUND to (session gone → - * session-ended CLI.CREDENTIALS_REQUIRED, otherwise the transient - * auth-service error) — state checks, never message parsing; the - * cause chain is walked for both AuthError and CLI structured - * errors. A request made while signed out throws the structured - * CLI.CREDENTIALS_REQUIRED error (the same constructor the - * needs.credentials check uses). */ - readonly api: ManagementApiClient - - /** - * S3: hands the terminal to a child process and resolves when it - * ends. Inherited stdio, same process group (POSIX) / console - * (Windows), no detach — the terminal delivers Ctrl-C to the child - * natively. While a child is live the engine neither aborts nor - * exits: delivered signals are RECORDED and replayed into the - * normal ladder on child exit (one recorded → ctx.signal aborts as - * if just delivered; a signal past that arms a force exit fired - * only after settlement and telemetry), so the engine always - * outlives the child and no path force-exits from inside ctx.spawn. - * SIGTERM — no native path to the child — is forwarded during the - * window, and so is a SECOND recorded press of any signal (as - * SIGTERM): when the engine was signalled directly and no group - * delivered the first press, the escalation keeps the child - * reachable. A programmatic abort terminates the child: SIGTERM, a - * 5s grace (D1 ruling), SIGKILL. ctx.report during a live child is - * buffered (capped, with a dropped-events marker on flush) and - * flushed in order on exit; ctx.present, ctx.prompt, and a second - * concurrent spawn are construction errors — and so is resolving or - * throwing while the child is still live: a handler stays suspended - * on the spawn promise. Only commands declaring `maySpawn` (§6) may - * call it, and a maySpawn command whose Runtime supplies no spawn - * adapter refuses before the needs check ever runs. - * Handlers branch on `signal` before `exitCode`: a signal-killed - * child is an abort, not a failure. - */ - readonly spawn: (options: SpawnOptions) => Promise - - /** S3: how the run's most recent COMPLETED child ended, or undefined - * when none has run. The engine records every child ctx.spawn - * returns — it mints them all anyway — so a handler whose spawn - * happens somewhere else in its own layering can still ask "did my - * child fail?" where it settles, instead of threading the result - * back by hand. Same record exitWithChildStatus settles from. */ - readonly lastChild: () => ChildResult | undefined - - /** The one way to emit while running (§1). */ - readonly report: (event: EngineEvent) => void - - /** Interactive input (§4a). */ - readonly prompt: PromptSurface - - /** Shows the user a URL and, interactively, opens it in their - * browser through the runtime's injected opener. The announcement - * is one `endpoint` event (§1): a stderr line in human mode, a - * frame in json mode, so the URL always reaches both a person and - * a machine consumer. A non-interactive run opens nothing and - * reports `{opened: false}` — the URL in the announcement is how it - * gets done by hand. NEVER an error: a host with no browser is not - * a failed command. To then WAIT for the user to finish in that - * browser, use prompt.browserWait (§4a). */ - readonly openUrl: (request: { - readonly url: string - /** The announcement label — what the URL is for, in the user's - * terms ("Finish signing in"). */ - readonly message: string - }) => Promise<{ readonly opened: boolean }> - - /** Fires on Ctrl-C/SIGTERM (engine-owned; a second signal - * force-exits through the runtime's exit proxy). Session commands - * run until it fires; everything else aborts in-flight work with - * it. */ - readonly signal: AbortSignal - - /** Where the user invoked the CLI. Handlers never read process.cwd(). */ - readonly cwd: string - - /** The invocation's environment, from Runtime.env. Handlers read env - * via ctx.env, never process.env (R4). */ - readonly env: Readonly> - - /** What this process runs on, from Runtime.host. Handlers read the - * runtime version, platform and arch here — never from `process`. */ - readonly host: Host - - /** - * 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. Handlers never craft install prose. - */ - readonly requireDependency: (specifier: string) => Promise> -} - -/** Superseded by the credential-manager surface below; carried only - * through the staged swap (Runtime.getCredentials fallback), then - * deleted. */ -export interface Credentials { - readonly token: string -} - -// —— §4b The credential manager (design rev 5 §2/§3, normative) —— -// A set of per-workspace sessions, one current. Sessions are keyed -// by workspace id: at most one session per workspace. No conditional -// properties: absent = `T | undefined` with the key required. - -/** The proof material. Only ever seen by the login flow (which mints - * it) and createSession (which stores it). */ -export interface Credential { - readonly token: string - readonly refreshToken: string | undefined - readonly expiresAt: Date | undefined -} - -/** "Logged-in-edness", scoped to a workspace. Identified to users by - * its workspace. The token is INTERNAL: it lives in the stored - * record, never on this public shape. `source: "environment"` marks - * the ephemeral session composed from PRISMA_SERVICE_TOKEN; it never - * appears in sessions(). */ -export interface Session { - readonly workspaceId: string - readonly workspaceName: string | undefined - readonly expiresAt: Date | undefined - readonly source: 'stored' | 'environment' - readonly current: boolean -} - -export interface ActiveAccessTokenOptions { - readonly minimumValidityMs: number - readonly now: Date - readonly signal: AbortSignal -} - -/** Manages sessions: six user-facing operations plus one - * engine-facing accessor. Custody, not user interaction: never opens - * a browser, never prompts. Env is a construction input. The manager - * resolves NO user input: commands resolve refs against sessions() - * and pass the matched Session. Full semantics (process pinning, - * env-override mutation rules, error single-sourcing, locking) are - * normative in credential-manager-design.md §3/§4/§6/§8. */ -export interface CredentialManager { - /** The session this PROCESS is acting as: pinned at first read (env - * token if set, else the file's current marker at that moment); - * other processes' marker moves never redirect it, this process's - * own mutations do. Local-only. */ - currentSession(): Promise - /** The available sessions, read fresh from the file. Local-only. - * Under an env override the file's marker is still shown as - * `current`. */ - sessions(): Promise - /** Login's write: verifies the workspace_id claim matches, upserts - * by workspaceId, sets the marker and this process's pin. The name - * is fetched best-effort after the write. */ - createSession(credential: Credential, workspaceId: string): Promise - /** Switch: sets the file's marker AND this process's pin. The - * argument is a workspace reference — only workspaceId is read, - * re-validated against freshly-read state. */ - useSession(session: Session): Promise - /** Log out of one workspace. If it was current (marker or pin), - * that current is cleared — no auto-promotion. */ - endSession(session: Session): Promise - /** Log out entirely: remove all sessions and the marker. */ - endAllSessions(): Promise - /** ENGINE-FACING, not a user operation: the SDK TokenStorage view - * of the ACTIVE credential. Zero-argument — process pinning ruled - * there is one credential per process, and an environment - * credential may have no workspace id to key on; the earlier - * tokenStorage(workspaceId) is deleted rather than reshaped - * (credential-manager-design.md §11.5). The engine forwards it - * into SDK client config and never calls its methods itself — no - * exceptions; the engine's own token read is activeAccessToken(). */ - activeCredentialStorage(): Promise - /** ENGINE-FACING (S3, amended after the PR-136 review): the active - * credential's ACCESS token, read fresh, for handing to a child - * process that authenticates as this process does. Never the - * refresh token — the child gets a snapshot it cannot refresh. - * Refreshes a stored OAuth pair inside the caller's minimum - * validity before returning its access token. Preflight applies the - * near-expiry window; ctx.spawn's fresh read refuses only an - * already-expired token, so a run is never refused mid-handler. The - * refresh token never reaches the child. */ - activeAccessToken(options: ActiveAccessTokenOptions): Promise -} - -/** The SDK's typed client and token-storage contract, re-exported by - * the engine so consumers never import @prisma/management-api-sdk - * directly. */ -import type { - ManagementApiClient as SdkClient, - TokenStorage as SdkTokenStorage, -} from '@prisma/management-api-sdk' - -export type ManagementApiClient = SdkClient -type SdkTokens = NonNullable>> -type StoredTokens = SdkTokens & { readonly expiresAt?: Date } -export type TokenStorage = Omit & { - getTokens(): Promise - setTokens(tokens: SdkTokens, expiresAt?: Date): Promise -} - -/** SDK client construction config, injected by the bin beside the - * manager (§10). All four fields: the SDK's refreshing fetch - * requires the full config even though only login paths read - * redirectUri. */ -export interface ManagementApiClientConfig { - readonly clientId: string - readonly redirectUri: string - readonly apiBaseUrl: string - readonly authBaseUrl: string -} - -// —— §4c The terminal handoff (S3) —— - -/** What a handler passes to ctx.spawn. */ -export interface SpawnOptions { - readonly command: string - readonly args?: readonly string[] - /** Defaults to ctx.cwd. */ - readonly cwd?: string - /** Added to, and overriding, the invocation environment. The - * engine's credential variables are applied last and cannot be - * overridden. */ - readonly env?: Readonly> -} - -/** How a child ended. Branch on `signal` first: a signal-killed child - * is an abort, not a failure. */ -export interface ChildResult { - readonly exitCode: number | null - readonly signal: string | null -} - -/** A fully composed child invocation, as the Runtime adapter receives - * it: env is the child's COMPLETE environment, credentials already - * injected by the engine (the active credential's access token, read - * at spawn time through the manager's activeAccessToken(); - * PRISMA_WORKSPACE_ID alongside when the credential names a - * workspace, and DELETED from the inherited environment when it does - * not — the two variables are one protocol, written as a unit; the - * refresh token NEVER). */ -export interface SpawnRequest { - readonly command: string - readonly args: readonly string[] - readonly cwd: string - readonly env: Readonly> -} - -/** The live child an adapter returns. `ended` rejects only when the - * child could not be launched; the engine phrases that as the - * structured CLI.SPAWN_FAILED. */ -export interface SpawnedChild { - readonly ended: Promise - readonly kill: (signal: 'SIGTERM' | 'SIGKILL') => void -} - -/** The Runtime seam (§10): starts the child with INHERITED stdio, in - * the caller's own process group (POSIX) / console (Windows) — no - * `detached`, no new console. The bin adapts node:child_process; the - * engine never imports it. */ -export type SpawnChild = (request: SpawnRequest) => SpawnedChild - -/** Opaque: built exclusively by exitWithChildStatus. It carries no - * exit code — the code is not the handler's to state, so the engine - * reads it off its own record of the child at settlement. */ -export interface ChildStatusSettlement { - readonly nextActions: readonly NextAction[] -} - -/** The sanctioned "exit with the child's status verbatim" outcome: - * returned inside ok(...), it settles through the no-envelope bypass - * server commands already have, with the status of the child THIS - * RUN spawned — ctx.lastChild(). The handler names no child, so - * there is no way to describe one that never existed. Two - * construction errors fence it: settling it from a command that does - * not declare maySpawn, and settling it when no child ran at all. - * - * The engine owns the ORDERING, which is composer's converge rule - * promoted into the engine. A signal-killed child is the user - * aborting: it settles 128 + the signal number (portable signals), - * with no envelope and NO nextActions — the hint is dropped even - * when the caller passed one, because there is nothing to reproduce. - * Otherwise the child's own code passes through verbatim and - * `nextActions` (a failed converge's reproduce hint, R-S3-4) render - * to stderr in the engine's next-action style before the process - * exits with that code. An unknown signal, or an adapter that cannot - * say how the child ended, settles 1 — unknown is never success. */ -export declare function exitWithChildStatus( - opts?: { readonly nextActions?: readonly NextAction[] }, -): ChildStatusSettlement - -/** - * §4a Prompts (operator ruling, 2026-08-09: prompts return their answer - * value, or throw). Every prompt resolves to its answered value - * directly. Failures THROW engine-internal structured errors the engine - * catches and settles: user cancellation (Ctrl-C/EOF at the prompt) - * exits 3; a prompt that cannot be operated (no default under --yes or - * non-interactive, an invalid answer) exits 2 as a structural error. A - * handler that does not catch simply propagates and the engine settles; - * one that catches cannot swallow the settlement — rethrow or notOk. - * - * Every prompt except `consent` may carry a declared - * `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 throws. In non-interactive - * contexts (no TTY stdin, CI, --no-interactive — format never decides - * interactivity) the same default rule applies; the prompt UI writes to - * stderr, so an interactive json run prompts without touching the - * stdout stream. - * - * Rendering is two-tier (S2a D4). A plain line renderer serves - * scripted answers and any stdin that cannot enter raw mode (piped - * stdin, the test harness); real TTYs — Runtime.isTty.stdin AND - * Runtime.stdin.setRawMode present, no scripted answers — render - * through @clack/prompts, loaded by dynamic import only on that path. - * Both tiers write prompt UI to stderr and share the same structural - * rules: --yes resolution, `--confirm` matching, and structural - * failures are decided before the tier branch. - * - * CONSENT TOKENS AND --confirm (operator ruling, 2026-08-10). A - * consent may declare a `token`: the natural noun of the action being - * consented to — an app name, a hostname — not a yes/no word. The - * token changes both halves of the prompt. - * Interactively it becomes TYPE-TO-CONFIRM: the user must type the - * token exactly. On the clack tier a wrong answer re-prompts (the - * only exits are the exact token and Ctrl-C); on the plain line - * tier — scripted answers, piped stdin — a wrong answer cannot be - * corrected, so it fails structurally (exit 2). - * Non-interactively (and under --yes, which is the same skip - * condition) the consent is satisfied iff one of the run's - * `--confirm ` values matches the token EXACTLY. Each value - * is consumed once per run, so two consents on one token need two - * --confirms. Otherwise the existing structural consent error - * (CLI.CONSENT_REQUIRED, exit 2), whose message now names the - * expected value and the literal `--confirm ` usage. - * A consent WITHOUT a token keeps today's yes/no rendering and stays - * non-interactively unsatisfiable — its error says the command - * should declare a token (the old "pass the command's explicit - * consent flag" wording described the abandoned per-command-flag - * doctrine: commands do not invent consent flags any more). - * --yes is unchanged by all of this: it accepts declared defaults and - * never grants consent, with or without a token. Clack's cancel symbol (including the \x03 byte - * path) maps to the same CLI.PROMPT_CANCELLED exit-3 settlement. - * Clack's spinner/log helpers are forbidden (process-global handlers); - * progress remains engine events. - * - * Accepted quirk: clack reads process.stdout.columns for wrap width — - * the one process-global read on the interactive path. - */ -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 and Enter-through can never satisfy it. - * `token` is the natural noun of the action; supplying one makes the - * interactive prompt type-to-confirm and makes `--confirm ` - * the one non-interactive way to grant it (§4a header). - */ - readonly consent: ( - question: string, - opts?: { readonly token?: string }, - ) => Promise - /** - * On the clack tier, Enter picks the HIGHLIGHTED option — the - * declared default when present, else the first option — so moving - * the highlight and pressing Enter selects the highlighted value, - * not the declared default. - */ - 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 - /** - * Sends the user to a URL and waits for them to finish there. It - * announces the URL (the same one-line `endpoint` announcement - * ctx.openUrl makes), opens the browser through the runtime's - * injected opener, then calls `poll` on the ENGINE's injectable - * clock at the engine's declared interval until it returns true. - * Settles three ways: resolved (poll true); the structured timeout - * error when `timeout` elapses first; the standard prompt-cancel - * exit-3 settlement on Ctrl-C. - * - * Non-interactively it throws the structured interaction-required - * error (exit 2) WITHOUT opening or polling anything, carrying the - * URL so the user can finish by hand. A command that can do nothing - * at all without an interactive terminal should say so declaratively - * with `needs.interaction` (§6) and fail before it starts, rather - * than reaching this error mid-run. - */ - readonly browserWait: (request: { - readonly url: string - readonly message: string - /** Has the user finished? Receives ctx.signal, so a polling - * request aborts with the command. */ - readonly poll: (signal: AbortSignal) => Promise - /** Milliseconds to keep polling before giving up. */ - readonly timeout: number - }) => Promise -} - -// ———————————————————————————————————————————————————————————————————————— -// §5 Flags and positionals — R1: directly executable, typed by inference -// ———————————————————————————————————————————————————————————————————————— - -/** - * Command-declared flags. The shared flag 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 - /** - * A boolean the user can leave unsaid: `--flag`, `--no-flag`, or - * neither, which arrives as undefined. Use it when absence means - * something of its own — "ask me" rather than "no". - */ - optionalBoolean(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 - -export interface FlagSpec { - /** Phantom type carrier for inference; never present at runtime. */ - readonly __flag?: T -} - -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 -} -export interface PositionalSpec { - /** Phantom type carrier for inference; never present at runtime. */ - readonly __positional?: T -} - -/** 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 -} - -/** The normalized argument surface a definition carries: both - * namespaces always present, empty when the command declares none. */ -export interface CommandArgs< - 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 — path-free (R12), runtime-discriminated by -// `kind`. Definitions load their handler's import graph at startup; -// R9's remaining force is that handler bodies defer heavy work to -// execution time. Three modalities at equal rank: -// result / session / server. -// -// Definitions carry NO conditional properties (operator ruling, round -// 2): the define* constructors accept ergonomic specs (optional help -// details, args, needs, exitCodes) and normalize them — every -// definition field is always present, with empty collections and -// explicit undefined. -// ———————————————————————————————————————————————————————————————————————— - -/** 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 - /** Invocations WITHOUT the binary name (operator ruling, 2026-08-09): - * at help render time every `{bin}` is substituted with createCli's - * `name`; an example containing no `{bin}` gets the name prepended. */ - readonly examples?: readonly string[] -} - -/** The normalized help a definition carries. */ -export interface CommandHelp { - readonly summary: string - readonly description: string | undefined - readonly examples: readonly string[] -} - -/** - * The preconditions SPI: everything the engine enforces BEFORE the - * handler runs. 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 command family'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. The - * 'child' form (S3, amended after the PR-136 review — this - * precondition previously lived at the top level as - * credentialsForSpawn) additionally composes the active credential - * into every child environment and refuses the run before the - * handler when it expires within the 5-minute threshold (D1 - * ruling). Requires maySpawn (construction error otherwise), and - * structurally entails the plain credentials need. */ - readonly credentials?: true | 'child' - /** 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 - * non-interactive contexts (no TTY stdin, CI, or --no-interactive; - * format never decides interactivity). 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 -} - -/** The normalized preconditions a definition carries. */ -export interface CommandNeeds { - readonly config: ConfigSection | undefined - readonly credentials: boolean | 'child' - readonly dependencies: readonly string[] - readonly interaction: boolean -} - -export interface CommandDefinition< - TFlags extends Record> = {}, - TPositionals extends Record> = {}, - TConfig = undefined, - TCode extends number = never, - TManagesCredentials extends boolean = false, -> { - readonly kind: 'result-command' - readonly help: CommandHelp - readonly args: CommandArgs - readonly needs: CommandNeeds - - /** - * 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). Empty = the command only exits 0/1/2/3 and the - * outcome carries no exitCode. - */ - readonly exitCodes: Readonly> - - /** - * A CAPABILITY, not a need (design rev 5 §4): when true, - * ctx.credentialManager appears on the context. Declaring it never - * fails a run — documentation and testability, not enforcement. - * Declared by exactly: auth login, auth logout, auth workspace - * list, auth workspace use, auth workspace logout. whoami uses - * ctx.session() only; sessions() lives ONLY on the manager, never - * on the universal context. - */ - readonly managesCredentials: TManagesCredentials - - /** See SpawnDeclarations (S3). */ - readonly maySpawn: boolean - - /** The handler function, referenced directly — never a dynamic import - * (operator ruling, 2026-08-09: "DO NOT DYNAMICALLY IMPORT HANDLERS"). - * R9's keep-heavy-work-out-of-startup concern is the handler BODY's - * business: a handler that needs heavy dependencies imports them at - * execution time, inside itself. A handler defined in another file is - * imported statically and annotated CommandHandler. */ - readonly handler: Handler -} - -export type Handler< - TFlags extends Record>, - TPositionals extends Record>, - TConfig, - TCode extends number = never, - TManagesCredentials extends boolean = false, -> = ( - args: Args, - ctx: CommandContext & - (TManagesCredentials extends true - ? { readonly credentialManager: CredentialManager } - : unknown), -) => Promise | ChildStatusSettlement, CliStructuredError>> - -/** - * S3: the terminal-handoff declaration, accepted by defineCommand and - * defineSessionCommand and normalized onto every definition (server - * commands normalize maySpawn to false: they own stdio already). - * `maySpawn` unlocks ctx.spawn. Human mode hands over the terminal; - * json mode routes the child's stdout and stderr to diagnostics while - * the engine retains framed stdout and emits a terminal result. Handing - * credentials to the child is a PRECONDITION, not a declaration: - * `needs: { credentials: 'child' }` (see NeedsSpec). - * Naming note (PR-136 review): `maySpawn` remains the capability name; - * structured mode no longer delegates stdout, so `delegatesTerminal` - * would now be actively misleading. - */ -export interface SpawnDeclarations { - readonly maySpawn?: boolean -} - -/** For impl files: `const run: CommandHandler = …` */ -export type CommandHandler = D extends CommandDefinition - ? Handler - : never - -/** Two overloads (implementation detail worth documenting: a generic - * TManagesCredentials inference site collapses under contextual - * typing, so the capability is a literal in each overload). */ -export declare function defineCommand< - TFlags extends Record> = {}, - TPositionals extends Record> = {}, - TConfig = undefined, - TCode extends number = never, ->(def: { - readonly help: HelpSpec - readonly args?: ArgsSpec - readonly needs?: NeedsSpec - readonly exitCodes?: Readonly> - readonly managesCredentials: true - readonly handler: Handler -} & SpawnDeclarations): CommandDefinition -export declare function defineCommand< - TFlags extends Record> = {}, - TPositionals extends Record> = {}, - TConfig = undefined, - TCode extends number = never, ->(def: { - readonly help: HelpSpec - readonly args?: ArgsSpec - readonly needs?: NeedsSpec - readonly exitCodes?: Readonly> - readonly handler: Handler -} & SpawnDeclarations): CommandDefinition - -/** - * A session command (dev, log tail): runs until the signal fires, - * speaks entirely through events, returns Result. No - * presentation, no exit-code set. - * - * S3 amendments: a session supports json mode — the event stream is - * its json surface — UNLESS it declares maySpawn, in which case it - * rejects --json as soon as the command is known. A session that - * returns ok(undefined) exits 0 — or 130/143 when a delivered signal - * ended the run, which the engine settles from its own record (EXIT - * CODES); one that returns ok(exitWithChildStatus()) exits with the - * status of the child it spawned. Those are the two ways a session settles - * non-zero without erroring, and neither is a code the handler picks. - */ -export interface SessionCommandDefinition< - TFlags extends Record> = {}, - TPositionals extends Record> = {}, - TConfig = undefined, -> { - readonly kind: 'session-command' - readonly help: CommandHelp - readonly args: CommandArgs - readonly needs: CommandNeeds - /** See SpawnDeclarations (S3). */ - readonly maySpawn: boolean - readonly handler: ( - args: Args, - ctx: CommandContext, - ) => Promise> -} - -export declare function defineSessionCommand< - TFlags extends Record> = {}, - TPositionals extends Record> = {}, - TConfig = undefined, ->(def: { - readonly help: HelpSpec - readonly args?: ArgsSpec - readonly needs?: NeedsSpec - readonly handler: SessionCommandDefinition['handler'] -} & SpawnDeclarations): 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: CommandHelp - readonly args: CommandArgs - readonly needs: CommandNeeds - /** Always false — normalized so every definition carries the field - * and the engine reads it directly (S3, PR-136 review). */ - readonly maySpawn: false - readonly handler: ( - args: Args, - io: { - readonly stdin: InputStream - readonly stdout: OutputStream - readonly stderr: OutputStream - readonly signal: AbortSignal - readonly cwd: string - readonly env: Readonly> - readonly config: TConfig - }, - ) => Promise -} - -export declare function defineServerCommand< - TFlags extends Record> = {}, - TConfig = undefined, ->(def: { - readonly help: HelpSpec - readonly args?: ArgsSpec - readonly needs?: NeedsSpec - readonly handler: ServerCommandDefinition['handler'] -}): ServerCommandDefinition - -/** Erased union for command families and mount maps; `kind` discriminates. */ -export type AnyCommand = - | CommandDefinition - | SessionCommandDefinition - | ServerCommandDefinition - -// ———————————————————————————————————————————————————————————————————————— -// §7 Presentation primitives — the R5 vocabulary -// ———————————————————————————————————————————————————————————————————————— - -// Amended by the engine-colour slice (specs/engine-colour.md, ruled -// 2026-08-11): Status split out of tone, Text everywhere, the drawing -// block, the card's rail, and Ui's palette and width. - -/** What happened: selects the glyph (✔ ✘ ⚠ ℹ) and carries a default Tone - * of the same name. Separate from Tone because a failure can be painted - * a colour that is not `error` — a tree node in its branch lane's hue. */ -export type Status = 'ok' | 'error' | 'warn' | 'info' - -/** What colour to paint, and nothing else. The indexed colours are for - * telling adjacent things apart and exclude red, so no series member - * reads as an error. */ -export type Tone = - | 'ok' - | 'warn' - | 'error' - | 'info' - | 'heading' - | 'identifier' - | 'ref' - | 'placeholder' - | 'link' - | 'emphasis' - | 'muted' - | 'structure' - | 'highlight' - | 'color-1' - | 'color-2' - | 'color-3' - | 'color-4' - | 'color-5' - | 'color-6' - -/** A run of text and the meaning of its colour. A handler never emits - * escape sequences, so the engine measures width from `text` and colour - * cannot break alignment. */ -export interface Span { - readonly text: string - readonly tone?: Tone -} - -/** Display text anywhere a block or Ui takes it. A bare string is untoned. */ -export type Text = string | readonly Span[] - -/** Deliberately small; grows by the same evidence rule as events. */ -export type Block = - | { - readonly kind: 'summary' - readonly status: Status - /** Overrides the colour the status implies. Never the glyph. */ - readonly tone?: Tone - readonly text: Text - } - | { - /** A key/value card: the engine pads the keys so every value starts - * in the same column. `rail` draws the dim `│` down the left. */ - readonly kind: 'fields' - readonly rows: ReadonlyArray<{ label: Text; value: Text; sensitive?: boolean }> - readonly rail?: boolean - } - | { - /** The engine sizes every column to its widest cell. */ - readonly kind: 'table' - readonly columns: readonly Text[] - readonly rows: ReadonlyArray - } - | { readonly kind: 'list'; readonly items: readonly Text[] } - | { readonly kind: 'tree'; readonly roots: readonly TreeNode[] } - | { - /** Lines of spans, rendered verbatim — no layout, no reflow, no - * truncation. For output whose two-dimensional structure the engine - * cannot derive, such as a migration graph's lane gutter. */ - readonly kind: 'drawing' - readonly lines: readonly Text[] - } -// 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: Text - /** Renders the status glyph before the label. */ - readonly status?: Status - /** Colours the glyph and the label; defaults from `status`. */ - readonly tone?: Tone - readonly children?: readonly TreeNode[] -} - -/** Styling helpers usable inside block text; no direct writing. `width` - * is the printing stream's columns, POSITIVE_INFINITY off-terminal — the - * engine never truncates, so a command that wants to fit is told how much - * room it has. */ -export interface Ui { - readonly width: number - readonly tone: (tone: Tone, text: string) => string - 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[] - /** Copied from the error's own nextActions — the uniform consumer - * read path (envelope.nextActions) on both settlement paths. */ - 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 Command families and shell mounting — R12: the shell owns the -// tree; a command family owns its section -// ———————————————————————————————————————————————————————————————————————— - -/** - * The unit of contribution and ownership a package exports for CLI - * purposes: its config section (declared once — a family-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 command family's section is a construction error. - */ -export interface CommandFamily { - readonly configSection: ConfigSection | undefined - readonly commands: Readonly> - /** The family'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 | undefined -} - -export declare function defineCommandFamily(spec: { - readonly configSection?: ConfigSection - readonly commands: Readonly> - readonly docsBaseUrl?: string -}): CommandFamily - -/** 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 command families. 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 commandFamilies: readonly CommandFamily[] - readonly groups: Readonly> - readonly commands: MountedTree -}): Cli - -export interface Cli { - /** Parse, execute, render, return the exit code. Never touches - * process globals — it exits only through the runtime's exit proxy - * (second-signal force exit) and writes only to the provided - * streams. `hooks` is the bin's observation seam (S2a telemetry - * amendment): the engine's internal RunHooks stayed internal, and - * the minimal public surface growth is this optional parameter - * carrying only the settlement observer. */ - run(argv: readonly string[], runtime: Runtime, hooks?: CliRunHooks): Promise -} - -/** - * S2a telemetry amendment. The observation hooks a bin may attach to - * a run — deliberately narrower than the engine's internal hook set - * (whose other members are test seams reachable only through the - * ./testing harness, which also accepts an `onSettled` tap). - */ -export interface CliRunHooks { - /** Fired exactly once per run, after settlement (exit code final, - * terminal output written). Never fired for --help/--version, and - * never for a run that failed before reaching a mounted command - * (nothing executed, so there is no snapshot). Errors thrown by - * the hook are swallowed — a telemetry bug must not break a - * command. */ - readonly onSettled?: (summary: RunSummary) => void -} - -/** What onSettled receives. `durationMs` comes from the engine's - * injectable clock (§11), never from wall time directly. - * `commandId` is derived from the same mount entry as - * `snapshot.commandPath` and always equals - * `snapshot.commandPath.join('.')`; both fields are kept on purpose - * (id for addressing, snapshot as the value-free wire projection). */ -export interface RunSummary { - readonly commandId: string - readonly exitCode: number - readonly durationMs: number - readonly snapshot: EngineCommandSnapshot -} - -/** - * What telemetry records about an invocation, captured when argv is - * parsed. It says which command ran and which flags were given, and - * never what any of them was set to: the command-path segments, the - * flag NAMES with where each value came from, - * and a bare count of positionals. Flag `source` derives from what - * the engine knows at parse time: flags explicitly present on argv - * are 'cli'; the engine reads no flags from the environment today, - * so everything else is 'default' ('env' is reserved for a future - * env-sourced flag mechanism). - */ -export interface EngineCommandSnapshot { - /** Mount-path segments ('telemetry status' → ['telemetry', - * 'status']). Never includes the binary name. */ - readonly commandPath: readonly string[] - /** One entry per flag the command accepts (the engine-injected - * shared family first, then the command's own declarations), in - * the user-facing kebab-case spelling. */ - readonly flags: ReadonlyArray<{ readonly name: string; readonly source: 'cli' | 'env' | 'default' }> - readonly positionalCount: number -} - -/** 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 } - /** Ends the process. The bin passes process.exit; the engine is the - * only caller (second-signal force exit, 130/143). */ - readonly exit: (code: number) => never - /** Subscribes to delivered SIGINT/SIGTERM; returns the unsubscribe. - * The bin is dumb wiring — the engine owns the whole signal policy: - * the first signal aborts ctx.signal and awaits teardown; a second - * calls exit(130|143) immediately. */ - readonly onSignal: (cb: (signal: 'SIGINT' | 'SIGTERM') => void) => () => void - /** Loaded config + file-level diagnostics; the shell builds this via - * the unified loader (R10). Tests hand in fixtures. */ - readonly config: LoadedConfig - /** The credential manager the bin wires (design rev 5 §4). The - * engine prefers it for the needs check, ctx.session, and ctx.api. - * Optional only during the staged swap; getCredentials below is - * the fallback and is deleted — with the optionality — in the - * swap's final mechanical stage. */ - readonly credentialManager?: CredentialManager - /** SDK client construction config the bin injects beside the - * manager; the engine builds ctx.api from it (the same config - * feeds performLogin). Required whenever a credentialManager is - * wired; optional only during the staged swap. */ - readonly managementApiClientConfig?: ManagementApiClientConfig - readonly getCredentials: () => Promise - /** Opens a URL in the user's browser — the login flow's opener, - * wired by the bin so the engine never depends on it. Called only - * for interactive sessions; a throw means "did not open" and never - * fails a command; absent means this host cannot open a browser, - * and the URL is announced instead. */ - readonly openUrl?: (url: string) => Promise | void - /** S3: the terminal-handoff seam (§4c) — starts a child with - * inherited stdio, wired by the bin as a node:child_process - * adapter so the engine never imports it. Absent means this host - * cannot hand the terminal to a child: ctx.spawn then fails with - * the engine's internal error. */ - readonly spawn?: SpawnChild - /** Management API endpoint config; the bin derives baseUrl from env - * (getApiBaseUrl). */ - readonly managementApi: { readonly baseUrl: string } - /** Used by the ENGINE to phrase install commands (handlers never - * do — see needs.dependencies and ctx.requireDependency). */ - readonly packageManager: 'npm' | 'pnpm' | 'yarn' | 'bun' | 'unknown' - /** What this process runs on; commands read it via ctx.host. */ - readonly host: Host -} - -/** The minimal process surface a bin adapts a Runtime from — Node's - * `process` satisfies it structurally; the engine never reads it. */ -export interface HostProcess { - readonly argv: readonly string[] - readonly env: Readonly> - cwd(): string - readonly stdout: { write(text: string): unknown; isTTY?: boolean } - readonly stderr: { write(text: string): unknown; isTTY?: boolean } - readonly stdin: { - isTTY?: boolean - setRawMode?(enabled: boolean): unknown - [Symbol.asyncIterator](): AsyncIterator - } - on(event: 'SIGINT' | 'SIGTERM', listener: () => void): unknown - off(event: 'SIGINT' | 'SIGTERM', listener: () => void): unknown - exit(code: number): never -} - -export interface LoadedConfig { - /** Raw section values by name; validation happens per command via its - * command family's section token. */ - readonly sections: Readonly> - /** File-level problems (unevaluable module, missing version marker) - * carry section: null and fail only commands with a needs.config - * section (operator ruling, 2026-08-09: the CLI still runs; a - * command that needs no config runs normally). */ - readonly diagnostics: ReadonlyArray<{ - readonly section: string | null - readonly diagnostic: Diagnostic - }> -} - -// ———————————————————————————————————————————————————————————————————————— -// §11 The test harness — R7: same machinery, bytes out. Lives on its -// own public subpath, `@prisma/cli-engine/testing` (composer's -// ./testing convention): the main entry ships no test machinery. -// ———————————————————————————————————————————————————————————————————————— - -export declare function createTestCli(spec: { - readonly commandFamilies?: readonly CommandFamily[] - readonly commands: MountedTree - readonly groups?: Readonly> - readonly config?: Readonly> - /** Legacy seed for the staged-swap getCredentials fallback: selects - * a manager-less runtime. Mutually exclusive with the manager - * seeds below; deleted with the swap's final stage. */ - readonly credentials?: Credentials - /** Convenience manager seed: createSession runs its real claims - * derivation on this credential (mint the token with mintTestJwt). */ - readonly credential?: Credential - /** Session-model seeding: stored sessions mirroring the state - * file's records, and the file's current marker. */ - readonly sessions?: ReadonlyArray<{ - readonly workspaceId: string - readonly workspaceName: string | undefined - readonly credential: Credential - }> - readonly currentWorkspaceId?: string - /** Composes the ephemeral env session; also exported to each run's - * env as PRISMA_SERVICE_TOKEN. */ - readonly environmentToken?: string - /** The SDK client construction config; defaults point every - * endpoint at test.invalid hosts (the design's local-endpoint - * fixture surface). */ - readonly managementApiClientConfig?: ManagementApiClientConfig - /** baseUrl defaults to "https://test.invalid"; when `client` is - * supplied, ctx.api IS that object (the uniform mock seam). */ - readonly managementApi?: { - readonly baseUrl?: string - readonly client?: ManagementApiClient - } - readonly packageManager?: 'npm' | 'pnpm' | 'yarn' | 'bun' | 'unknown' - readonly host?: Host - /** Fixed clock for deterministic stream timestamps; a clock that - * advances also drives prompt.browserWait's timeout, whose waiting - * is instant under the harness. */ - readonly now?: () => Date - /** The browser opener behind ctx.openUrl and prompt.browserWait. - * Defaults to one that succeeds without doing anything — a real - * browser is never a test dependency; pass a spy to assert what was - * opened, or a thrower for the could-not-open path. */ - readonly openUrl?: (url: string) => Promise | void - /** S3: the spawn adapter behind ctx.spawn. Defaults to the scripted - * fake below; the real-child tests pass a node:child_process - * adapter, which is how the engine package itself never imports - * one. Every run records its spawns — command, args, cwd, env - * KEYS (values never: a recording must not carry token material), - * and the signals the engine delivered. */ - readonly spawn?: SpawnChild - /** S3: scripts the built-in fake child (defaults to exit 0). The - * script receives the composed SpawnRequest and a nextKill() - * awaiting each engine-delivered signal, so it can model a child - * that ignores SIGTERM and dies only on SIGKILL. */ - readonly spawnScript?: ( - request: SpawnRequest, - child: { readonly nextKill: () => Promise<'SIGTERM' | 'SIGKILL'> }, - ) => ChildResult | Promise -}): TestCli - -/** Mints an unsigned JWT whose payload is exactly `claims` — the - * harness's claim source (`sub`, `workspace_id`, `exp`, `email`) for - * createSession derivation, migration, and expiry tests. The rest of - * the design's fixture surface (the token-endpoint scripting, - * legacy-store builder, deterministic clock, second-process lock - * holder) lands with the real manager implementation, whose behavior - * it exercises. */ -export declare function mintTestJwt(claims: Readonly>): string - -export interface TestCli { - /** The MUTABLE in-memory credential manager backing the runs: the - * full CredentialManager interface — with the design's - * process-pinning semantics (currentSession fixed at first read; - * its own mutations move it) — plus state(), which reads the whole - * stored state back after a run, and overwriteStoredState(), which - * applies a write as ANOTHER process would (never moves the pin). - * Undefined only when the legacy `credentials` seed selected the - * manager-less fallback runtime. */ - readonly credentialManager: - | (CredentialManager & { - state(): { - readonly sessions: ReadonlyArray<{ - readonly workspaceId: string - readonly workspaceName: string | undefined - readonly credential: Credential - }> - readonly currentWorkspaceId: string | null - } - overwriteStoredState(state: { - readonly sessions?: ReadonlyArray<{ - readonly workspaceId: string - readonly workspaceName: string | undefined - readonly credential: Credential - }> - readonly currentWorkspaceId?: string | null - }): void - }) - | undefined - 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): its firing is delivered to the - * engine as a signal (SIGTERM when the reason is 'SIGTERM', - * SIGINT otherwise). */ - readonly abort?: AbortSignal - /** Live event tap, for asserting mid-session behavior. */ - readonly onEvent?: (event: EngineEvent) => void - /** Settlement tap (S2a telemetry amendment): receives the - * RunSummary the engine fires after settlement (once, mounted - * runs only). */ - readonly onSettled?: (summary: RunSummary) => 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; undefined when the run never - * presented. */ - readonly presented: PresentedResult | undefined - }> -} - -/** - * What this process runs on. Deliberately not node-shaped: products are - * runtime-agnostic (R4), so the runtime names itself rather than the - * field naming it. - */ -export interface Host { - readonly runtime: { readonly name: string; readonly version: string } - readonly platform: string - readonly arch: string -} 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 deleted file mode 100644 index 601eaa88..00000000 --- a/.drive/projects/prisma-cli-v8/assets/engine/output-modes-survey.md +++ /dev/null @@ -1,652 +0,0 @@ -# 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 deleted file mode 100644 index 41fb2d85..00000000 --- a/.drive/projects/prisma-cli-v8/assets/engine/stricli-vs-clipanion.md +++ /dev/null @@ -1,238 +0,0 @@ -# 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/assets/engine/websocket-transport-design.md b/.drive/projects/prisma-cli-v8/assets/engine/websocket-transport-design.md deleted file mode 100644 index 35b09ebd..00000000 --- a/.drive/projects/prisma-cli-v8/assets/engine/websocket-transport-design.md +++ /dev/null @@ -1,82 +0,0 @@ -# Engine-owned WebSocket transport — design - -Written 2026-08-10 by the S2c orchestrating agent, at the operator's instruction, after `service logs` was shelved for want of this affordance. Status: **shelved, not scheduled** (2026-08-12): §7's question 2 was answered — the API owners accept serving deployment logs over plain HTTP, provided live streaming can be added later. `service logs` will therefore follow the `build logs` HTTP shape and needs no socket. This design stays on the shelf as the future live-streaming path; build it only when that date arrives. - -## 1. Why this exists - -`service logs` streams a deployment's output from `GET /v1/deployments/{deploymentId}/logs`. The Management API's own specification describes that endpoint as *"Stream deployment logs via WebSocket"* — the request upgrades to a socket. It is the only command in the platform CLI that needs a transport other than HTTP. - -The engine's `ctx.api` is an HTTP client. It can hand a command a streaming HTTP response body, which is exactly how `build logs` works — plain newline-delimited JSON over `GET`, with `parseAs: "stream"`, and no credential ever visible to the command. It cannot open a socket, and a socket upgrade carries its own `Authorization` header. - -So the command reached around the engine: it asked for a raw token through `ctx.getCredentials()` and let `@prisma/compute-sdk`'s `streamLogs` build the URL, flip the scheme to `wss:`, and set the header itself. That is the shape being removed. Credential-manager design rev 6 rules it out in one sentence — **credentials never reach commands; the engine may hold them** — and `getCredentials` is staged for deletion. - -The operator's instruction is the design principle: **the engine provides the authenticated connection. The command does not craft a URL from a string template and does not hold a token.** - -## 2. What already exists to build on - -The engine resolves a credential and constructs an authenticated client today, in `packages/cli-engine/src/execution/api-client.ts`. It reads the active session, branches on where the credential came from, and builds either a static-token client (environment credential) or an SDK client over the manager's token storage (stored credential). The token stays inside that function. - -A socket affordance is the same three steps with a different final constructor. It is not a new concept in the engine; it is the existing concept extended to a second transport. Under rev 6 the engine-facing accessor is `activeCredentialStorage()`, and the socket builder consumes it exactly as the HTTP client builder does. - -## 3. The shape, and the one thing to get right - -"An authenticated URL" has two readings, and only one of them is safe. - -**Rejected: a URL carrying the token.** Putting a bearer token in a query string leaks it into server access logs, shell history, crash reports, and any error message that quotes the URL. The compute SDK deliberately avoids this today — it sets an `Authorization` header on the upgrade request instead. A design that hands commands a token-bearing URL would be a regression against the current implementation and against the custody rule. - -**Chosen: the engine opens the socket and hands back the decoded record stream.** The command receives something it can iterate. It never sees a URL, a header or a token. This also keeps the retry and reconnect logic in one place — see §5. - -Sketch, to be settled during implementation rather than treated as final: - -```ts -// On CommandContext, beside `api`. -readonly stream: (request: StreamRequest) => Promise>; - -interface StreamRequest { - /** A path on the Management API, in the same style ctx.api takes. */ - readonly path: string; - readonly params?: { readonly path?: Record; readonly query?: Record }; - /** Parses one wire message into a record, or rejects it. */ - readonly decode: (message: string) => T; -} - -interface RecordStream extends AsyncIterable { - close(): void; -} -``` - -An async iterable rather than a callback, because the engine's stream commands already consume records in a loop and settle on the way out, and because cancellation through `ctx.signal` composes with iteration more honestly than with a callback. - -`ctx.api` stays exactly as it is. This is an addition beside it, not a replacement, and commands that need HTTP keep using HTTP. - -## 4. Which command kind - -The engine has three: result, session and server. `service logs` was a session command, emitting `output` events per record with a `data`-versus-`diagnostic` channel. That remains right: the socket is a source of records, not a new kind of command. No change to the command kinds is needed or wanted. - -## 5. Reconnection belongs to the engine - -The endpoint ends the stream after ten minutes and expects the client to reconnect with a `cursor` query parameter to continue. The terminal record carries that cursor. - -Reconnection should be the engine's, for the same reason the credential is. Every command that streams would otherwise reimplement it, and each would get the edge cases subtly differently — what counts as a resumable end, how many attempts, what backoff, what happens when the cursor is absent, and how a genuine failure is distinguished from a routine ten-minute rollover. - -Two things follow, and both need a decision at implementation time: - -- **A resumable end must not surface to the command as a stream ending.** If the engine reconnects transparently, the command sees one uninterrupted sequence of records. That is the behaviour to aim for. -- **A terminal record that is genuinely terminal must still reach the command**, because commands map terminal state onto their own settlement. The distinction the wire protocol already draws — `kind: "end" | "error"`, plus `retryable` — is the input to that. - -## 6. Testing - -The harness has to be able to drive this without a network. Today `createTestCli` fakes the Management API by accepting a client; the socket affordance needs the equivalent seam — a way to seed a sequence of wire messages, including a mid-stream resumable end, so reconnection is exercised rather than assumed. - -This is also the moment to fix a harness inconsistency found while investigating: seeding an environment token gives a run a session and sets `PRISMA_SERVICE_TOKEN` in its environment, but the harness's `getCredentials` stub ignores it and resolves nothing, where the real runtime returns that token first. If `getCredentials` is deleted as rev 6 intends, the inconsistency goes with it. If it survives longer than expected, it should be corrected on its own. - -## 7. Open questions for whoever picks this up - -1. **The endpoint is marked experimental** in the Management API specification — "in active development and may change at any time without notice". Confirm it is stable enough to build engine support against before starting, and ideally get the WebSocket contract pinned. -2. **Is a socket the right transport at all?** `build logs` streams over plain HTTP from a sibling endpoint. If deployment logs could be served the same way, the whole affordance becomes unnecessary and `service logs` becomes a copy of `build logs`. This is worth one conversation with the API owners before building anything — it is by far the cheapest outcome. -3. **Does anything other than `service logs` need it?** If not, question 2 gets more attractive, and the affordance should probably wait until a second consumer exists. -4. **Where does the compute SDK's `streamLogs` fit?** The engine could use it internally rather than opening its own socket, provided the token stays engine-side. That keeps the wire protocol in one place, at the cost of a dependency direction the engine may not want. - -## 8. Out of scope - -Rebuilding `service logs` itself. That command's handler — the resolution, the channel routing, the record-to-event mapping — was written, reviewed and green before it was shelved, and it is in this branch's history. Restoring it should be a small piece of work once the affordance exists, not a rewrite. diff --git a/.drive/projects/prisma-cli-v8/assets/engine/whoami-parity-divergences.md b/.drive/projects/prisma-cli-v8/assets/engine/whoami-parity-divergences.md deleted file mode 100644 index d5150a5d..00000000 --- a/.drive/projects/prisma-cli-v8/assets/engine/whoami-parity-divergences.md +++ /dev/null @@ -1,240 +0,0 @@ -# `prisma-v8 auth whoami` — parity divergences from `prisma-cli auth whoami` - -> SCOPE: this document is whoami-scoped (S1). Its engine-global -> sections (json framing, format auto-selection, channel discipline, -> rendering style, `--quiet`, exit codes, flag family) are the -> baseline for every later port. S2 onward, per-command divergences -> accumulate in [`../s2/parity-divergences.md`](../s2/parity-divergences.md). - -Written for D6 of slice s1-engine-vertical (2026-08-09). Every known place -where the v8 port's output or behavior differs from the shipped -`prisma-cli auth whoami`, and why. The v8 side is pinned by -`packages/cli/tests/v8-whoami.test.ts`; the current-CLI side by -`packages/cli/tests/auth.test.ts`. - -## 1. JSON envelope shape and framing - -Current CLI writes ONE pretty-printed (2-space-indented) JSON object to -stdout: - -```json -{ "ok": true, "command": "auth.whoami", "result": { … }, - "warnings": [], "nextSteps": [], "nextActions": [] } -``` - -v8 writes a stream of single-line StreamEvent frames, terminated by -exactly one `result` frame wrapping the envelope: - -```json -{"kind":"result","envelope":{"ok":true,"commandId":"auth.whoami","result":{…},"exitCode":0,"diagnostics":[],"nextActions":[…]},"commandId":"auth.whoami","timestamp":"…"} -``` - -Field-level differences inside the envelope: - -- `command` → `commandId` (same dotted value, `auth.whoami`). -- `warnings: string[]` is gone; findings are typed `diagnostics`. -- `nextSteps: string[]` (bare command strings) is gone; follow-ups are - typed `nextActions` only (see §4). -- `exitCode` is now carried in the envelope; the current CLI never - reports it in JSON. -- Every frame carries `commandId` + ISO `timestamp` stream metadata. - -The `result` payload itself is byte-identical in shape: the raw -`AuthStateResult` (`authenticated`, `provider`, `user`, `workspace`, -`credential`), unchanged. - -## 2. Format auto-selection - -Current CLI is human unless `--json` is passed, regardless of where -stdout goes. v8 auto-selects json when stdout is not a TTY (draft -header: human on a TTY stdout, json otherwise). Piping -`prisma-v8 auth whoami` therefore produces the json stream where the -current CLI would produce the human card on stderr. - -## 3. Human output: channel and rendering - -Current CLI (signed out): - -``` -auth whoami → Showing the current authenticated identity. - -│ status: signed out -``` - -- written to STDERR (stdout stays empty), -- title line `auth whoami → …` with the command path label, -- rail (`│`) card with column-aligned, color-toned values - (`signed in` green, `signed out` dim). - -v8 (signed out), stderr: - -``` -ℹ Showing the current authenticated identity. -status: signed out -→ Sign in: prisma-cli auth login -``` - -v8 (signed out), stdout: - -``` -status: signed out -``` - -- the CHANNELS match the current CLI (operator ruling, 2026-08-09): - human Blocks, next-action lines, and diagnostics are presentation - prose on STDERR, exactly where the current CLI writes its card. The - divergence shrinks to rendering STYLE: - - the title survives as a `summary` block (tone `info`, rendered - `ℹ …`) because the Block vocabulary has no title/descriptor - primitive, - - fields render as unpadded `label: value` lines — no rail, no - column alignment, no per-value color tones, - - the sign-in follow-up renders as a `→` line (see §4). -- one addition beyond the current CLI: the handler's - `Presentations.stdout` payload lines (`label: value` rows) are - written to STDOUT, always — the machine-usable payload of a pipe. - The current CLI's whoami writes nothing to stdout in human mode. - -Signed-in rows are otherwise identical in order and values: `status: -signed in`, `user: `, `provider: -GitHub|Google` (only when the state carries a provider — real-mode state -never does today, so the row appears only against fixture-shaped data), -`workspace: `. - -## 4. Next-step suggestion when signed out - -Current CLI: `nextSteps: ["prisma-cli auth login"]` in JSON only; -human mode shows nothing extra on success. - -v8: a typed NextAction `{kind: "run-command", label: "Sign in", -command: "prisma-cli auth login"}` in the envelope's `nextActions`, and -the human renderer prints it (`→ Sign in: prisma-cli auth login`) — so -human mode now surfaces the suggestion too. The suggested command -remains `prisma-cli auth login` (the v8 bin mounts no login command; -signing in still happens through the shipped CLI in S1). - -## 5. `--quiet` - -Current CLI: whoami has no stdout presenter, so `--quiet` prints -nothing at all. - -v8: `--quiet` is a log-level alias — shorthand for `--log-level error` -(operator ruling, 2026-08-09) — and nothing more. It silences -commentary on stderr; whoami's presentation (the stderr card and the -stdout payload lines) is unchanged by it, and since whoami emits no -commentary, a `--quiet` run's output is identical to a plain run. - -## 6. Empty `PRISMA_SERVICE_TOKEN` (the errored path) - -Current CLI: `AUTH_CONFIG_INVALID` (flat code + `domain: "auth"`), JSON -envelope written to stdout even in error, EXIT 1. - -v8: structured error `AUTH.CONFIG_INVALID` (dotted namespace form, no -domain field), settled as ERRORED per the v8 protocol → EXIT 2. Same -summary ("Authentication configuration is invalid"), same why text; the -fix prose is now a typed `nextActions` entry (operator ruling, -2026-08-09: `fix` renamed to `nextActions`). Human rendering differs: -engine layout `✖ [AUTH.CONFIG_INVALID] … / why: … / → ` -on stderr, versus the current CLI's `✖ summary [CODE]` + -`Why:`/`Fix:` + "More: Re-run with --trace" block. - -(The engine's failure mark became `✘` in the engine-colour slice, which -brought every surface onto the one the style guide and -`docs/product/output-conventions.md` already specified. The engine -layout above is otherwise unchanged; the legacy `✖` is accurate for the -CLI it describes.) - -## 7. Exit-code semantics generally - -Both exit 0 for signed-in AND signed-out whoami (parity kept: whoami -reports auth state; it does not fail when unauthenticated — accordingly -the v8 whoami declares NO `needs.credentials`). Beyond that: v8 reserves -1 for bugs only (an unexpected operations throw is -`CLI.INTERNAL_ERROR`, exit 1), 2 for expected structured errors, 3/130/ -143 for aborts/signals. The current CLI's exit codes are per-error -(`CliError.exitCode`), e.g. exit 1 for the service-token case above. - -## 8. The `needs.credentials` early failure is proven elsewhere - -Because whoami itself must complete when signed out, the engine's -credentials precondition (early failure with -`CLI.CREDENTIALS_REQUIRED`, exit 2, before the handler loads) is -byte-asserted in the slice e2e through a tiny in-test command mounted -only in the harness (`auth locked` in v8-whoami.test.ts), not in the -shipped bin tree. - -## 9. Global flag family - -The v8 bin exposes the engine's shared family (`--format/--json`, -`--log-level/-v/--verbose`, `-q/--quiet`, `-y/--yes`, -`--interactive/--no-interactive`, `--color/--no-color`, `-h/--help`). -The current CLI's `--trace` does not exist in v8 (bugs settle as -`CLI.INTERNAL_ERROR`; no stack-trace flag), and the current CLI has no -`--format`, `--log-level`, `--yes`, or `--interactive`. - -## 10. Mock-fixture mode is not ported - -`prisma-cli auth whoami` supports the fixture mode -(`PRISMA_CLI_MOCK_FIXTURE_PATH` / `--fixture`) via the shell runtime. -The v8 whoami handler calls only the real-mode operations layer -(`readAuthState`); fixture mode is shell infrastructure and out of the -slice's scope. The harness e2e instead stubs the operations layer. - -## 11. Operations residue pending S2 extraction - -RESOLVED for the handler (operator review round 1): `CommandContext` -now carries `env` from `Runtime.env`, and the v8 handler calls -`readAuthState(ctx.env, ctx.signal)` — no `process.env` read remains in -command code. The auth operations themselves still live in -`packages/cli/src/lib/auth/`, and the bin's `getCredentials` (service -token env var, else the stored access token via `FileTokenStorage`) -reads the process environment in the adapter layer, where node-ness -belongs. The S2 auth-library extraction is where the remaining -operations residue resolves. - -## 12. Update notification / agent tips - -The current CLI shell may prepend a cached update notification to any -command's output, and login (not whoami) appends agent-setup tips. -UPDATED in S2a: the v8 bin now wires the same cached update -notification with the legacy sequencing, and `auth login` ports the -agent-setup tip — see the S2 cumulative divergence list. - ---- - -## Open interpretation items carried from the slice (operator review) - -Recorded during D3–D5 and re-listed here per the handover instruction; -none were resolved unilaterally. - -1. RULED (2026-08-09, Option A channel discipline): human Blocks are - presentation prose on stderr; the `Presentations.stdout` payload - lines are the machine-usable payload and are always written to - stdout in human mode — that is what the surface is for. Human mode - is pipe-clean; json mode unchanged (frame stream on stdout). -2. RULED (2026-08-09): the engine never aggregates remediation events - into any envelope. The event kind stays in the vocabulary as - transcript-only — json streams it live as a frame; human mode never - renders it. Follow-ups are handler-owned: completed via - `presentations.next`, errored via the error's own typed - `nextActions` (the renamed `fix`), which the envelope copies. -3. Diagnostic severity stays `error|warn|info`; the trim to two awaits - the ADR 239 amendment (project slice S4). -4. RULED (2026-08-09): a file-level config diagnostic fails only - commands with a `needs.config` section; every other command runs - normally, and a missing file was never a diagnostic at all. -5. RULED (2026-08-09): diagnostics on a SUCCESSFUL `SectionValidation` - are written to stderr as warning commentary (log-level filtered, - both formats), never into the stream or the envelope. -6. DEFERRED to S3: running the config loader under plain Node (the S1 - bin is tsx-run by design, so evaluating a `.ts` config on a bare - `node` host is the S3 loader slice's problem). -7. DEFERRED by operator ruling (review round 1): O13 — using a schema - library such as Arktype for construction-time validation. A heavy - dependency for the engine's minimal validation needs. - -The D6 finding about the lazy-handler pattern's circular type inference -is resolved by an operator ruling (2026-08-09): handlers are never -dynamically imported. `handler` is the handler function itself, so the -inference cycle no longer exists; the whoami handler now lives inline in -`packages/cli/src/v8/auth/whoami.ts`. diff --git a/.drive/projects/prisma-cli-v8/assets/rollout-plan.md b/.drive/projects/prisma-cli-v8/assets/rollout-plan.md deleted file mode 100644 index 7a3edce6..00000000 --- a/.drive/projects/prisma-cli-v8/assets/rollout-plan.md +++ /dev/null @@ -1,72 +0,0 @@ -# Prisma 8 CLI rollout plan - -Operator-agreed 2026-08-09 (discussion on PR #129). Governs how the -unified CLI reaches npm, from first pre-release through owning the bare -`prisma` name. S7 builds its release pipeline against this plan. - -## Names involved - -| npm name | Today | End state | -| --- | --- | --- | -| `@prisma/cli` | Platform CLI 3.x, published from this repo (OIDC) | Carries v8 RC releases under `next` via merged bump PRs (`latest` stays pre-v8 until the deliberate flip; ruling 2026-08-12); deprecated at cutover | -| `prisma-next` | Prisma 8 ORM CLI, published from prisma/prisma `main` | Handed off to this repo at S5; rc channel for ORM early adopters; deprecated at cutover | -| `prisma7` | Does not exist yet | The v7-and-under release train's new home, published from prisma/prisma | -| `prisma` | Prisma 7 CLI, published by prisma/prisma's release train | Owned by this repo via OIDC trusted publishing; the unified CLI | - -## Sequence - -1. **Now → S2 done.** Nothing user-facing ships. Official `@prisma/cli` - releases are manual-dispatch only and are frozen for the migration; - the automatic `dev`-tag publish on main merges continues and is - harmless. -2. **S2 done (platform family ported).** v8 RC-line versions - (`8.0.0-rc.N`) publish as `@prisma/cli`. Tag ruling reversed again - (operator, 2026-08-12, during S7): RC-line bump PRs publish under - **`next`**; `latest` keeps serving the pre-v8 CLI until the operator - moves it deliberately (an explicit `dist-tag: latest` dispatch, or - widening `releaseDistTag` when the line is ready). Merging the bump - PR remains the deliberate publishing act. This repo already owns the - name with OIDC; no cross-repo coordination. -3. **S5 done (ORM family ported).** The `prisma-next` name is handed - off: prisma/prisma stops publishing it and its npm trusted-publisher - config moves to this repo (npm allows one publisher config per - package, so the handoff is a cutover, not a dual-publish phase — the - operator owns both repos and sequences it). From then on - `prisma-next` carries v8 rc builds under a `next` tag. Rationale for - waiting until S5: `prisma-next`'s installed base is ORM early - adopters, and the unified CLI cannot serve them before the ORM - family exists. -4. **`prisma7` ships (owned by the ORM team, timing open).** The - v7-and-under train moves to the `prisma7` name, freeing the bare - `prisma` name. This repo configures OIDC trusted publishing for - `prisma` and publishes `prisma@8.0.0-rc1` under the **`next`/`rc` - dist-tag** (the S7 pipeline's artifact). -5. **Cutover.** Flipping `prisma`'s `latest` to 8.x is its own - deliberate manual act, after an rc soak. In the same step: - deprecation notices on `@prisma/cli`, `prisma-next`, and - `prisma-composer` pointing at `prisma`, per the grammar/consolidation - docs. (Codemods and docs-site updates are the ecosystem-cutover - follow-on, out of this project's scope.) - -## Invariants - -- Every publish path uses OIDC trusted publishing with provenance; no - pasted tokens anywhere (a manual-token fallback for prisma/prisma was - considered and rejected — the `prisma7` rename makes it unnecessary). -- `latest` moves only by an explicit operator act, on any of the names - (tightened per the 2026-08-12 ruling: RC-line bump PRs publish under - `next`, so on this repo's names even a merged bump PR cannot move - `latest` while the line is RC — only an explicit `dist-tag: latest` - dispatch or a stable-version bump can). -- Version pins across the tandem packages follow the committed-versions - ruling (S3). - -## Open items - -- **`prisma7` timing** — owned by the ORM team; blocks step 4 only. -- **`latest`-flip criteria** (step 5) — TBD by the operator; candidate - inputs: rc soak duration, issue rate, parity-divergence sign-off per - family. -- **Exact `prisma-next` handoff mechanics** (step 3) — npm publisher - config transfer + the prisma/prisma workflow change; small, ruled - feasible since the operator owns both repos. diff --git a/.drive/projects/prisma-cli-v8/assets/s2/api-overlay-audit.md b/.drive/projects/prisma-cli-v8/assets/s2/api-overlay-audit.md deleted file mode 100644 index e93a1056..00000000 --- a/.drive/projects/prisma-cli-v8/assets/s2/api-overlay-audit.md +++ /dev/null @@ -1,451 +0,0 @@ -# What the CLI invents on top of the API - -An audit for the defect class PR #144 exposed: the CLI making a decision the API has already made, or supplying information the API did not give. Ranked by whether a user gets a confidently reported wrong answer rather than an error. - -Branch audited: `s2b2-domain-extraction` at `39d6d50`. Trees read in full: `packages/cli/src/lib/**`, `packages/cli/src/controllers/**`, `packages/cli/src/v8/**`, plus `packages/cli/src/adapters/**` and `packages/cli/src/auth/**` where the resource commands read through them. - -## The reference case, and where it now stands - -PR #144 merged on 2026-08-11, and the workspace filter is gone from `main`. It used to sit in `listRealWorkspaceProjects` at `packages/cli/src/controllers/project.ts`: - -```ts - return sortProjects( - (data.data ?? []) - .filter((project) => project.workspace.id === workspace.id) -``` - -I am not counting this as a finding. It is the case that started the audit, and it is fixed. It matters here only for reachability: every project-scoped command in both the legacy shell and the v8 tree reads through this function, so the findings below that used to be masked by it are now reachable. - -One detail from it survives the merge. `listRealWorkspaceProjects` still reads only `data.data` and never follows `pagination.nextCursor`, unlike `listBranches` at `controllers/branch.ts:129-155`, `listDatabases` at `lib/database/provider.ts:244-273` and `listBuckets` at `adapters/bucket/management-provider.ts:48-77`, which all paginate. A workspace with more projects than one page silently loses the rest — a second way for `project list` to under-report while exiting 0. Rechecked against `origin/main` on 2026-08-12: still absent. - -## Findings - -Each finding describes the code as it stood when the audit was written. Where a finding has since been fixed, a **Status** line under the heading says so and the prose below it is left as the record of what was wrong. A finding with no Status line is still live. - -### 1. `app rollback` with no `--to` rolls forward, and reports it as a rollback - -`packages/cli/src/controllers/app.ts:3200-3209`, reached from `runAppRollback` at `:1975-1979`. - -```ts -function resolveRollbackTarget( - deployments: AppDeploymentSummary[], - currentLiveDeploymentId: string | null, -): AppDeploymentSummary { - const previousDeployment = deployments.find( - (deployment) => deployment.id !== currentLiveDeploymentId, - ); -``` - -The CLI decides which deployment is "the previous one" by taking the first entry in a newest-first list that is not the live one. The list is sorted newest-first by the provider at `packages/cli/src/lib/app/app-provider.ts:690-695`. - -**Is there a case where it is correct?** Only when the live deployment is also the newest one. The CLI itself creates the case where it is not. After `app deploy --no-promote` (`packages/cli/src/commands/app/index.ts:253`), the list reads `[unpromoted candidate, live, older…]`, so this returns the deployment the user deliberately chose not to promote. `runAppRollback` then calls `promoteDeployment` on it and returns `status: "running", live: true, previousLiveDeploymentId: `. The user asked to go back and was moved forward onto the build they were holding, described as a successful rollback. The same thing happens after any rollback followed by a new deploy. - -Underneath it is a rename. At `app-provider.ts:686` the provider maps the API's `latestDeploymentId` onto a field it calls `liveDeploymentId`: - -```ts - liveDeploymentId: appResult.value.latestDeploymentId ?? null, -``` - -Latest and live are the same value only until someone uses `--no-promote`. The CLI renamed one API fact into a different one and then built the rollback decision on the renamed version. - -**What breaks on an API change.** If the API's default deployment ordering changes, or the provider's sort is removed, this promotes an arbitrary deployment. "Which deployment is live" is a fact the API states; the CLI is re-deriving it from list position. - -**Reachable.** Yes, `prisma-cli app rollback` in the published binary. Destructive, silent, reported as success. - -### 2. `postgres usage` prints `0` for a metric the API did not send, in a unit the CLI chose - -**Status.** Fixed in #158. `normalizeUsageMetric` carries both `used` and `unit` as `null`, the card reads `unknown`, and stdout and `--json` leave the field empty. - -`packages/cli/src/lib/database/provider.ts:666-686`: - -```ts - metrics: { - operations: { - used: usage.metrics?.operations?.used ?? 0, - unit: usage.metrics?.operations?.unit ?? "ops", - }, - storage: { - used: usage.metrics?.storage?.used ?? 0, - unit: usage.metrics?.storage?.unit ?? "GiB", - }, - }, -``` - -**Is there a case where it is correct?** For `used`, none. Zero is a real, meaningful measurement, and it is the answer a user is most likely to act on — "we have used nothing this period" is what you check before deciding you have headroom. Substituting it for "the API did not report this" makes those two states indistinguishable. For the units, none either: `ops` and `GiB` are guesses about how the platform denominates the values, and if the API ever reports storage in `MiB` or `bytes`, the number is printed against the wrong unit and is wrong by three orders of magnitude. - -**What breaks on an API change.** Renaming `metrics.operations` to anything else, or nesting it, produces `0 ops` and `0 GiB` with no error. The adjacent `period.start ?? ""` and `generatedAt ?? ""` at least degrade to an empty field. - -**Reachable.** Yes. `v8/postgres/usage.ts:33-42` prints `${used} ${unit}` into the human card, the stdout lane and the `--json` record. Unlike most of the placeholders in the v8 presentation layer, this one is not confined to the human lane — `stdoutFieldRows` at `:50-58` emits `String(result.metrics.operations.used)`, so a script consuming stdout gets the invented zero too. - -### 3. `postgres list` and `postgres show` print `default` in the Status column - -**Status.** Fixed in #158. `formatStatus` returns the API's status or `unknown`; `isDefault` no longer reaches the Status cell. - -`packages/cli/src/v8/postgres/presentation.ts:23-34`: - -```ts -export function formatStatus(database: DatabaseSummary): string { - return database.status ?? (database.isDefault ? "default" : "unknown"); -} - -/** ... an absent status is an empty field, and `isDefault` is a - * different fact that does not belong in this one. */ -export function statusValue(database: DatabaseSummary): string { - return database.status ?? ""; -} -``` - -When the API reports no status, the human table answers a different question — is this the project's default database — in the Status cell. The comment on the sibling function three lines below states exactly the rule the first function breaks. - -**Is there a case where it is correct?** None. `default` is not a value in the status vocabulary, and being the default database says nothing about whether it is running. `unknown` admits ignorance; `default` reads as a status and does not. - -**What breaks on an API change.** If `status` is renamed, or omitted from list payloads while kept on show payloads, every row reads `default` or `unknown`, and a stopped or failed database reads `default`. - -**Reachable.** Yes, `postgres list` (`v8/postgres/list.ts:22`) and `postgres show` (`v8/postgres/show.ts:28`). The stdout and `--json` lanes are correct, so only the human table misreports — but that is the lane a person reads before deciding nothing is wrong. - -### 4. `resolveDatabase` substitutes a stale row when the API says the database is gone - -**Status.** The stale row is fixed in #158: a `null` from `showDatabase` now raises `DATABASE_NOT_FOUND`, which the postgres mapper emits as `POSTGRES.NOT_FOUND`. The `ensureProjectId` concern in the last paragraph stands — it still fills in a `projectId` the API did not send. - -`packages/cli/src/controllers/database.ts:947-952`: - -```ts - const selected = matches[0]; - const shown = await provider.showDatabase(selected.id, { - projectId: target.project.id, - signal, - }); - return ensureProjectId(shown ?? selected, target.project.id); -``` - -`showDatabase` returns `null` for exactly one condition: a 404 that is not a plan-limit error (`lib/database/provider.ts:287-292`). That is the API saying the database no longer exists. The `?? selected` treats it as a reason to use the row from the list call made moments earlier, and the command proceeds. - -**Is there a case where it is correct?** For a read-only command you could argue slightly stale metadata beats a failure. For `postgres remove` there is none — the database was deleted between the two calls, and the CLI is about to name it in the destructive-operation confirmation prompt as though it still existed. A `databaseNotFoundError` helper sits twelve lines above and is not used. - -`ensureProjectId` (`database.ts:955-960`) then fills in a `projectId` the API did not send, using whichever project the current directory resolved to. On `create` and `restore` that is a fair echo of what the CLI just asked for. On this path it is an assumption. - -**Reachable.** Yes. `resolveDatabase` is the entry point for `postgres show`, `remove`, `usage`, `restore`, `backup list`, `connection list` and `connection create`. - -### 5. The `live` flag users read is computed by the CLI, and this laptop's cache outranks the API - -`packages/cli/src/controllers/app.ts:1169-1176`: - -```ts - live: providerLiveDeploymentId - ? deployment.deployment.id === providerLiveDeploymentId - : knownLiveDeploymentId - ? deployment.deployment.id === knownLiveDeploymentId - : deployment.deployment.live, -``` - -Three tiers of precedence, invented here. Tier two, `knownLiveDeploymentId`, is read from this machine's local state store at `:1149-1157` — a record of what this machine last deployed. It outranks `deployment.deployment.live`, the field the API just sent. - -**Is there a case where it is correct?** Tier one is fine. Tier two has none. Local state written by past runs on one laptop is never a better authority than the response in hand. If a teammate promoted a different version from the Console, this machine reports `live: true` for the wrong deployment and `live: false` for the real one. - -Two things make it worse. `listDeployments` sets `live: null` on every row (`app-provider.ts:700`), so the middle tier of `resolveCurrentLiveDeploymentId` at `app.ts:3142-3147` — `deployments.find(d => d.live === true)` — never fires in real mode, and the local cache becomes the effective fallback everywhere. And `applyLiveDeploymentHint` at `app.ts:3183-3198` rewrites `live` on every row the API returned to match the CLI's computed answer, so `app show` and `app list-deploys` display a derived flag, not a reported one. - -**Reachable.** Yes: `app show-deploy`, `app show`, `app list-deploys`. - -### 6. Custom domains are refused on any production branch not named `production` or `main` - -`packages/cli/src/controllers/app.ts:3714-3716`, used as a requirement at `:2143-2154`: - -```ts -function toBranchKind(name: string): BranchKind { - return name === "production" || name === "main" ? "production" : "preview"; -} -``` - -```ts - const branch = resolveDomainBranch(options?.branchName); - if (toBranchKind(branch.name) !== "production") { - throw new CliError({ - code: "BRANCH_NOT_DEPLOYABLE", - summary: "Custom domains require the production branch", - why: `Custom domains on preview branch "${branch.name}" are not supported in Public Beta.`, -``` - -The CLI decides whether a branch is production by comparing its name against two string literals. The API states this directly: `resolveAppProjectContext` twelve lines above reads `remoteBranch.role` (`app.ts:3709`), and `RawBranchRecord.role` is declared at `controllers/branch.ts:35` and `controllers/app-env.ts:86`. - -**Is there a case where it is correct?** Only for projects whose production branch happens to be called `production` or `main`. A project on `master`, `prod`, `trunk` or `release` is told its production branch is a preview branch, and a supported operation is refused with a confident explanation of why it cannot work. No API change is needed for this to be wrong — it is wrong today for naming conventions that already exist. - -**Reachable.** Yes. `--branch ` is wired to all five domain subcommands (`commands/app/index.ts:413-417`). The same function is also the fallback branch kind when `--branch` is explicit (`app.ts:3435-3439`), so the `branch.kind` reported in `app show`, `promote`, `rollback` and `remove` output can be wrong the same way. - -### 7. `project env list` silently drops variable classes the CLI has not heard of - -`packages/cli/src/controllers/app-env.ts:906-911`: - -```ts - filter: (row) => - row.branchId === null && - (row.class === "production" || row.class === "preview"), -``` - -The request is already scoped by `projectId`. This narrows the result to two hardcoded class values. - -**Is there a case where it is correct?** None. It encodes a guess about which classes exist, and the API is the thing that knows. - -**What breaks on an API change.** The day a `development`, `staging` or `build` class appears, those variables vanish from the overview listing with no warning and exit 0. A user checks whether a variable is set, is told it is not, and sets it again — which is the #144 failure mode with a different noun. - -**Reachable.** Yes, `project env list` with no `--role` and no `--branch` and no local git branch (`app-env.ts:412`, `:688-696`). - -### 8. `app promote` and `app rollback` assert a status nobody observed - -`packages/cli/src/controllers/app.ts:1908-1912` and `:2020-2024`: - -```ts - deployment: { - ...targetDeployment, - status: "running", - live: true, - }, -``` - -**Is there a case where it is correct?** `live: true` is fair — the CLI just made that true. `status: "running"` has none. A 2xx from promote means the traffic switch was accepted, not that the process is healthy. And when `targetAlreadyLive` is true (`:1868`, `:1980`) no call is made at all, and the CLI still writes `status: "running"` over a status it copied from a list row it never refreshed. - -**Reachable.** Yes. The user sees `status: running` for a deployment that may be crash-looping, and `--json` carries the same claim to any script reading it. - -### 9. `postgres restore` puts an invented status in quotation marks - -`packages/cli/src/v8/postgres/restore.ts:45`: - -```ts - `The restore is running; the database status is "${result.database.status ?? "recovering"}" until it completes.`, -``` - -**Is there a case where it is correct?** None as written. Saying "the restore is running" would be fine — the CLI knows that. Naming a status in quotation marks attributes a specific word to the platform, and if the API sent no status the CLI is quoting itself. This runs immediately after an irreversible operation, and the worst case is that the restore failed, the API omitted the status for that reason, and the user is told it is `recovering`. - -**Reachable.** Yes, `postgres restore` is mounted at `v8/cli.ts:142`. - -### 10. `git connect` and `git disconnect` report automation capabilities the API never sent - -`packages/cli/src/controllers/project.ts:2143-2173`: - -```ts - installation: { - id: record.installationId, - status: "connected", - }, - automation: { - branches: record.status === "active", - pullRequests: false, - comments: false, - }, -``` - -Three inventions in one object, all of which reach the `--json` payload of both commands as though the platform had reported them. The installation status is hardcoded. Branch automation is re-derived from the connection status. Pull-request and comment automation are hardcoded `false`. - -**Is there a case where it is correct?** For `pullRequests` and `comments`, only by coincidence: they are right for exactly as long as the platform does not ship those features, and they will keep saying `false` on the day it does. For `installation.status` there is none — the record existing says nothing about whether the GitHub App installation is currently healthy or suspended, and the API does model this: `findRepositoryInInstallations` at `project.ts:1755` reads a real `installation.suspended` flag. - -Line 2146, `const [owner = "", name = ""] = record.repoFullName.split("/")`, silently yields empty strings for any `repoFullName` in a different shape. - -**Reachable.** Yes, `v8/git/connect.ts:207,257` and `v8/git/disconnect.ts:89`. A script reading `automation.pullRequests` gets a wrong answer, not an error. - -### 11. `branch list` shows an "Env map" column that is the Role column again - -`packages/cli/src/controllers/branch.ts:161-168`, rendered at `v8/branch/list.ts:21-25`: - -```ts -export function toBranchSummary(branch: RawBranchRecord): BranchSummary { - return { - id: branch.id, - name: branch.gitName, - role: branch.role, - envMap: branch.role, - }; -} -``` - -**Is there a case where it is correct?** None. The API states one fact; the CLI presents it twice under two headings, the second implying a relationship between the branch and an environment mapping that the API never described. Every row shows identical values in adjacent columns, which reads to a user as two facts that happen to agree rather than as one fact printed twice — so the wrong inference is the natural one. - -**Reachable.** Yes, `branch list` is mounted. - -### 12. `project env` add, update and remove pick a row by re-filtering what the API already filtered - -`packages/cli/src/controllers/app-env-api.ts:57-60`: - -```ts - const matches = (data.data as RawEnvironmentVariable[]).filter((row) => - rowMatchesExactScope(row, resolved), - ); - return matches[0] ?? null; -``` - -The GET immediately above already sent `projectId`, `class`, `key` and `branchId` as query parameters. The comment at `:36` says the server-side filter is the one that matters. This re-applies `class` and `branchId` client-side and takes the first survivor. - -**Is there a case where it is correct?** None. It is a duplicate of a decision the API made. If the API ignored one of the parameters, the right response is to fail loudly, not to quietly narrow the set. And `matches[0]` picks arbitrarily when more than one row comes back, where `resolveDatabase` (`database.ts:943`) and `resolveProjectForSetup` (`lib/project/setup.ts:53`) both raise an ambiguity error instead. - -**What breaks on an API change.** `RawEnvironmentVariable` is hand-declared at `app-env-api.ts:12-19`, not generated, so a new `class` value or a change in how branch scoping is represented compiles fine and matches nothing at runtime. `env update` and `env remove` then report `ENV_VARIABLE_NOT_FOUND` for a variable that exists, and `env add` proceeds to create a duplicate. - -**Reachable.** Yes, and consequential: in `env remove` the id of the chosen row is what gets passed to `DELETE /v1/environment-variables/{envVarId}`. A wrong match deletes the wrong variable and reports success. The `--file` paths call this once per key (`app-env-file.ts:216-239`). - -### 13. The CLI computes the effective environment itself - -`packages/cli/src/controllers/app-env.ts:986-1009`: - -```ts - const byKey = new Map(); - for (const row of rows) { - if (row.branchId === null && !byKey.has(row.key)) { - byKey.set(row.key, row); - } - } - for (const row of rows) { - if (row.branchId === resolved.apiTarget.branchId) { - byKey.set(row.key, row); - } - } -``` - -This re-implements variable inheritance — project-level rows as the base, branch rows overriding by key — and presents the merged result as the branch's environment. - -**Is there a case where it is correct?** It is correct for exactly as long as the platform's precedence rule is "branch beats project-level within the same class, last write wins". That is a business rule the API owns, and the CLI is asserting it from two fields. - -**What breaks on an API change.** A third class, an explicit precedence field, a per-key "do not inherit" marker, or a project-level variable scoped to a subset of branches — any of those produce a wrong effective list with no error. - -**Reachable.** Yes, `project env list --branch ` and the git-branch-inferred path at `app-env.ts:668-685`. This is the list a user reads to check what a preview deploy will see, so a wrong answer here gets acted on. - -### 14. `toMetadata` relabels a row with the scope the CLI asked for - -`packages/cli/src/controllers/app-env-api.ts:63-72`: - -```ts - const rowScope = - row.branchId === null - ? ({ kind: "role", role: row.class } satisfies EnvScopeDescriptor) - : requestedScope; -``` - -**Is there a case where it is correct?** The `branchId === null` branch is correct — the row genuinely is role-scoped. The other branch has none: it discards what the API said about the row and substitutes what the CLI requested, so a row returned under a different branch than the one asked for is relabelled to match the request. `formatDescriptorLabel` at `:120-128` then renders it with `scope.role ?? "unknown"` and `branch:${scope.branchName ?? scope.branchId ?? "unknown"}`. - -**Reachable.** Yes. The string this produces is the `source` column — the first cell of the `project env list` table (`v8/project/env-shared.ts:108-112`). The user reads a scope the CLI assigned. - -### 15. `normalizeDatabase` invents a precedence across four spellings of the branch name - -`packages/cli/src/lib/database/provider.ts:549-564` and `627-632`: - -```ts - branchId: database.branchId ?? database.branch?.id ?? null, - branchName: - database.branchGitName ?? - database.branchName ?? - database.branch?.gitName ?? - database.branch?.name ?? - null, -``` - -**Is there a case where it is correct?** As a short-lived bridge across two API versions, yes. As permanent code, no — `branch.gitName` and `branch.name` are genuinely different values, a git ref and a display name, and this merges them into one field ranked by guess. Nobody reading this can tell which shape the API actually returns, and the day it populates a different one, the branch column in `postgres list` changes meaning without changing shape. `normalizeRegion` does the same across three candidates, and `normalizeConnection` at `:573` does `connection.name ?? connection.id`, printing an id in a column headed Name. - -**Reachable.** Yes, every `postgres` command. - -### 16. `resolveSessionRef` lost the `wksp_` prefix handling the code beside it has - -`packages/cli/src/v8/auth/session-ref.ts:23-25`: - -```ts - const wanted = ref.trim(); - const byId = sessions.find((session) => session.workspaceId === wanted); -``` - -`session.workspaceId` is the bare `workspace_id` claim. The resolver this replaces, `workspaceMatchesRef` at `packages/cli/src/auth/token-storage.ts:761-773`, strips the prefix from both sides on purpose: - -```ts - stripWorkspacePrefix(workspace.credentialWorkspaceId) === - stripWorkspacePrefix(ref) || - stripWorkspacePrefix(workspace.id) === stripWorkspacePrefix(ref) || -``` - -**Is there a case where it is correct?** Only when the user types the bare claim form. A user pasting the id from the Console or from an API response used to work and no longer does. This is the #144 comparison exactly — bare against prefixed — and the code that knows about the two forms is one directory away. - -**Reachable.** Yes, `auth workspace use` (`v8/auth/workspace-use.ts:110`) and `auth workspace logout` (`v8/auth/workspace-logout.ts:87`). It ranks below the silent findings because the failure is a loud `noSessionForWorkspaceError`, not a wrong answer. - -### 17. The local pin comparisons repeat the #144 shape, and treat "not in this list" as "does not exist" - -`packages/cli/src/lib/project/resolution.ts:636-652`, reached from every project-scoped command: - -```ts - if (localPin.kind === "present") { - if (localPin.pin.workspaceId !== options.workspace.id) { - return Result.err(new LocalProjectWorkspaceMismatchError({...})); - } - const project = projects.find( - (candidate) => candidate.id === localPin.pin.projectId, - ); - if (!project) { - return Result.err(new LocalStateStaleError()); - } -``` - -and the same pair at `packages/cli/src/controllers/project.ts:118-123`. - -**Is there a case where it is correct?** The workspace comparison is correct only because today the same claim value both writes and reads the pin. It has no defence against a pin written by a differently-formatted claim, by the other CLI version, or by a future API form — which is precisely how #144 happened. PR #144 removes the equivalent comparison from `readProjectListLocalBinding` for that reason; this copy is outside its diff. - -The `projects.find(...)` existence check has no correct case at all: it treats absence from a list the CLI assembled — filtered today, unpaginated always — as proof the project does not exist. `resolveExplicitProject` at `:558-573` does the same for user input, matching `project.id === projectRef || project.name === projectRef` exactly and case-sensitively, so the CLI answers "does this project exist" rather than asking. - -**Reachable.** Yes. The user is told to re-link a correctly linked directory, or gets `PROJECT_NOT_FOUND` for a project that exists. - -### 18. A workspace with no fetched name is reported as being named after its own id - -Six places take part in one loop: - -- `packages/cli/src/v8/resources-shared/workspace.ts:35-38` — `name: credential.workspaceName ?? credential.workspaceId` -- `packages/cli/src/v8/auth/session-ref.ts:83-85` — `return session.workspaceName ?? session.workspaceId;` -- `packages/cli/src/v8/auth/credential-card.ts:26-31` — same substitution -- `packages/cli/src/auth/token-storage.ts:244-247` — stores `{ id: tokens.workspaceId, name: tokens.workspaceId }` -- `packages/cli/src/auth/token-storage.ts:779-781` — `return name === credentialWorkspaceId ? UNKNOWN_WORKSPACE_NAME : name;` -- `packages/cli/src/auth/workspaces.ts:229-235` and `legacy-state.ts:70-81` — detect the placeholder by comparing against the literal string `"Unknown workspace"` - -The storage layer writes the id into the name field, another layer detects that and swaps in a magic string, and two more layers detect the magic string to decide whether to re-fetch. - -**Is there a case where it is correct?** The `?? workspaceId` display substitutions are defensible as a convention — the user gets something addressable. The magic-string round trip is not. A workspace genuinely named `Unknown workspace`, or genuinely named after its id, is treated as never-hydrated forever, so `auth workspace list` fires an extra `/v1/workspaces/{id}` request on every invocation and never settles. `resolveOAuthWorkspaceMetadata` at `workspaces.ts:256-271` completes the loop: it fills both fields from the claim, then compares the result back against the claim and returns `null` — meaning "the lookup failed" — when they agree. - -**What breaks on an API change.** If the API returns the prefixed id while credentials keep the bare one, the `workspace.id === workspace.credentialWorkspaceId` check at `workspaces.ts:231` stops firing, hydration goes dead, and every workspace displays as its id. - -**Reachable.** Yes, in the `workspace` field printed by most project commands and every auth command. - -### 19. Pagination stops silently when the API says there is more but sends no cursor - -`packages/cli/src/lib/database/provider.ts:266-272`, `adapters/bucket/management-provider.ts:73-76` and `:147-150`, `controllers/branch.ts:151-155`: - -```ts - if ( - !result.data.pagination.hasMore || - !result.data.pagination.nextCursor - ) { - break; - } -``` - -**Is there a case where it is correct?** The `!hasMore` half, yes. The second half is the CLI deciding what to do when the API contradicts itself, and the decision it makes is to return a partial list as if it were complete. `hasMore: true` with a null cursor is the API stating the response is incomplete; the CLI answers by dropping the statement. `normalizeBackupList` at `provider.ts:702-704` does the softer version of the same thing with `hasMore: body.pagination?.hasMore ?? false`. - -**Reachable.** Yes, on every list command. A truncated list is indistinguishable from a complete one. - -## Same family, lower severity - -These are the same instinct in smaller doses. Each is real; none is worth its own section. - -- **First branch wins for a name query.** `controllers/app-env.ts:633-635`, `:758-766`, `:789-791` take `[0]` from a `?gitName=` list response, ignore any second row, and never paginate. Correct only if git names are unique per project and the query is exact — neither is checked. A silent write to the wrong branch scope if either assumption fails. -- **Errors classified by reading their prose.** `controllers/app.ts:2508-2543` and `:2568-2572` decide whether a domain error is a quota problem or a DNS problem with `text.includes("quota")` and a regex over the human-readable message, and scrape the DNS target out of it with `/\b((?:[a-z0-9-]+\.)+prisma\.build)\b/`. The API sends a structured `code` that is read but not used for these branches. `isMissingProjectError` at `:5012-5014` compares `error.message === "Resource Not Found"` exactly. A prose rewrite on the server reroutes users to the wrong fix. -- **`project list` prints `none` for a region.** `v8/project/list.ts:20-26`. Absence is manufactured upstream: `controllers/project.ts:1463-1465` only copies `defaultRegion` when that literal key is present on the response object, so a rename makes every row read `none`. The stdout lane correctly uses `""`. -- **Bucket key role coercion.** `controllers/bucket.ts:412-414`: `role === "read" ? "read" : "read_write"`. Not reachable with a bad value today — commander constrains it with `.choices([...])` at `commands/bucket/index.ts:210-213`. It is a trap rather than a live defect: the day a third role is added to `choices`, this silently issues read-write credentials for it. A credential-scope decision should be the API's refusal, not a ternary's default. -- **`build logs` prints the word `undefined`.** `controllers/build.ts:97-113` treats every NDJSON record that is not `type: "log"` as terminal, so a new `status` or `heartbeat` record has `record.code === undefined`, fails the `!== "end"` check, and writes `undefined` to stderr for each one. `JSON.parse(line)` at `:137` has no validation and throws out of the read loop on one malformed line. `sawError` at `:71-74` only fires on `kind === "error"`, so a failure signalled some other way exits 0. -- **`agent status` reports zero skills rather than a parse failure.** `controllers/agent.ts:363-384` returns `null` for any record missing a field and `flatMap` drops it, so a rename in the sibling tool makes `skillsInstalled: false` and tells the user to reinstall, with no sign that records were discarded. -- **`local-state.ts` invents a branch name.** `adapters/local-state.ts:41-58` and `:92-94` default the active branch to the literal `"preview"` for a missing or partial state file. A hardcoded assumption about the platform's branch vocabulary living in a file reader. -- **The local pin file rejects any future field.** `lib/project/local-pin.ts:424-431` requires `Object.keys(value).length !== 2`. A `.prisma/local.json` written by any version that adds a third field reads as invalid shape and surfaces as `LOCAL_STATE_STALE`. -- **A dead comparison.** `lib/project/resolution.ts:662-673` compares `platformMapping.workspace.id === options.workspace.id`, but `resolveDurablePlatformMapping` at `:586-588` always returns `null`. Unreachable today, and it is the #144 comparison waiting for someone to implement the function. - -## Looked at and not reporting - -`repositoryFullNamesMatch` (`controllers/project.ts:2260-2262`) lowercases both sides, which is right — GitHub owner and repo names are case-insensitive. `readFirstSourceRepository` (`:2092-2116`) sends `limit: 1` and takes `data.data[0]`, which is the API choosing, not the CLI. `sortProjects`, `sortDatabases` and `sortBranches` order a complete set for display rather than re-deriving an order the API stated. The `?? "unknown"` placeholders in the v8 presentation layer — `v8/postgres/presentation.ts:55-65`, `connection-list.ts:25`, `list.ts:18-19`, `bucket/presentation.ts:17` — are each paired with a stdout row that emits the raw value, which is the right split; they matter only as the area affected when a field is renamed, and `unknown` at least reports ignorance honestly. (`unscoped` for an absent branch name is weaker: it asserts the resource is not branch-scoped, which is a different claim from "the API did not say".) That parenthetical under-rated it. Reading the live API afterwards showed it returning `branchId` with `branchName: null` for every database in a real workspace, so the Branch column read `unscoped` for databases that were all branch-scoped — a wrong answer on every row, not a weak placeholder. Fixed for `postgres list` and `show` in #158, where the label falls back to `branchId` and only a missing id can produce `unscoped`. The same substitution in the other listings named here was not audited against live output and may be wrong in the same way. `v8/project/context.ts:42-54`'s `refuseUnknownReads` proxy is the opposite of this defect class — it refuses to invent rather than filling a gap. The `v8/*/errors.ts` mappers pass unmapped API codes through as `GROUP.` by explicit rule. `v8/auth/whoami.ts:80-98` merges the token claims with `/v1/me`, which the comment above it justifies as the offline fallback; the reasoning is sound, though the `samePerson` check treats a missing id on either side as proof of a match, so if `/v1/me` stops returning `user.id` the CLI will splice one person's email onto another's name — worth a comment, not a finding. - -## What the pattern is - -Three habits produce every finding above. - -The CLI re-derives state the API already states — which deployment is live, which branch is production, which environment variables are in effect, what a database's status is (findings 1, 3, 5, 6, 8, 11, 13). - -The CLI narrows a collection the API scoped, using assumptions about what values exist (7, 12, 19, and the reference case). - -The CLI supplies an answer where the API declined to give one — a zero for a metric, a status for a restore, a capability flag for an integration, a name for a workspace (2, 4, 9, 10, 14, 15, 18). - -The first two produce wrong answers under API change. The third produces wrong answers today, without any change at all. diff --git a/.drive/projects/prisma-cli-v8/assets/s2/command-inventory.md b/.drive/projects/prisma-cli-v8/assets/s2/command-inventory.md deleted file mode 100644 index f2d50557..00000000 --- a/.drive/projects/prisma-cli-v8/assets/s2/command-inventory.md +++ /dev/null @@ -1,705 +0,0 @@ -# S2 Command Inventory — @prisma/cli commander shell (grounding for v8 port specs) - -Source of truth read on branch `claude/prisma-cli-s1-d6-013cea` of the -`prisma/prisma-cli` repository. All paths below are relative to -`packages/cli/` unless prefixed. - -Registration lives in `src/cli.ts` (root program) + `src/commands/*/index.ts` + -`src/commands/env.ts`. Descriptions/examples live centrally in -`src/shell/command-meta.ts` (the `DESCRIPTORS` array). Cross-checked against -`docs/product/command-spec.md`; discrepancies are recorded inline and in the -"Spec discrepancies" list at the end of section 3. - ---- - -## 1. Command index - -Behavior classes: sync = single result envelope; poll = loops on remote status; stream = emits records until the remote side ends; interactive = can prompt. "auth" column: none / local (credential store only) / platform (fails unauthenticated with AUTH_REQUIRED; "platform+login" means an interactive login is triggered on a TTY instead of failing). - -| path | group | behavior | auth | API surface (real mode) | proposed engine kind | -|---|---|---|---|---|---| -| `version` | top | sync | none | none | result | -| `init` | top | sync + interactive, file-writing | none (auth only if link step runs) | none directly; link → GET /v1/projects, POST /v1/projects | session (prompts) or result with needs.consent | -| `feedback` | top | sync | none | external feedback service (not Management API) | result | -| `agent install` | agent | sync, spawns child proc | none | none (runs skills CLI) | result | -| `agent update` | agent | sync, spawns child proc | none | none | result | -| `agent status` | agent | sync, spawns child proc | none | none | result | -| `auth login` | auth | interactive, browser-opening, server-hosting | n/a (creates auth) | OAuth (sdk.getLoginUrl/handleCallback), GET /v1/workspaces/{id} | session | -| `auth logout` | auth | sync | local | GET /v1/me (state readback) | result | -| `auth whoami` | auth | sync | none (reports state) | GET /v1/me, GET /v1/workspaces/{id} | result | -| `auth workspace list` | auth | sync | local | GET /v1/workspaces/{id} (best-effort hydration) | result | -| `auth workspace use` | auth | sync + interactive picker | local | GET /v1/workspaces/{id} (hydration) | result / session | -| `auth workspace logout` | auth | sync | local | GET /v1/workspaces/{id} (hydration) | result | -| `project list` | project | sync | platform+login | GET /v1/projects | result | -| `project show` | project | sync | platform+login | GET /v1/projects | result | -| `project create` | project | sync, file-writing | platform+login | POST /v1/projects (ComputeClient.createProject) | result | -| `project link` | project | sync + interactive picker, file-writing | platform+login | GET /v1/projects (+ POST /v1/projects if "create new" chosen) | session / result | -| `project rename` | project | sync | platform+login | PATCH /v1/projects/{id} | result | -| `project remove` | project | sync, file-deleting | platform+login | DELETE /v1/projects/{id} | result (needs --confirm) | -| `project transfer` | project | sync, file-writing | platform+login | POST /v1/projects/{id}/transfer, GET /v1/workspaces (recipient probe) | result (needs --confirm) | -| `project env add` | project.env | sync | platform+login | GET/POST /v1/environment-variables, branches endpoints | result | -| `project env update` | project.env | sync | platform+login | GET /v1/environment-variables, PATCH /v1/environment-variables/{id} | result | -| `project env list` | project.env | sync | platform+login | GET /v1/environment-variables, GET /v1/projects/{id}/branches | result | -| `project env remove` (alias `rm`) | project.env | sync | platform+login | GET /v1/environment-variables, DELETE /v1/environment-variables/{id} | result | -| `git connect` | git | sync or poll (waits for GitHub App install), browser-opening | platform+login | GET/POST /v1/source-repositories, GET /v1/scm-installations(+/repositories), POST /v1/scm-installations/install-intents | session (poll + browser) | -| `git disconnect` | git | sync | platform+login | GET /v1/source-repositories, DELETE /v1/source-repositories/{id} | result | -| `branch list` | branch | sync | platform+login | GET /v1/projects/{projectId}/branches (paginated) | result | -| `build logs` | build | stream | platform (no login fallback) | GET /v1/builds/{buildId}/logs (NDJSON stream) | stream | -| `database list` | database | sync | platform+login | GET /v1/databases | result | -| `database show` | database | sync | platform+login | GET /v1/databases, GET /v1/databases/{id}, GET /v1/databases/{id}/connections | result | -| `database create` | database | sync | platform+login | POST /v1/databases | result (secret on stdout) | -| `database usage` | database | sync | platform+login | GET /v1/databases/{id}/usage | result | -| `database restore` | database | sync | platform+login | POST (restore endpoint at provider.ts:473) | result (needs --confirm) | -| `database remove` | database | sync | platform+login | DELETE /v1/databases/{id} | result (needs --confirm) | -| `database backup list` | database.backup | sync | platform+login | GET /v1/databases/{id}/backups | result | -| `database connection list` | database.connection | sync | platform+login | GET /v1/databases/{id}/connections | result | -| `database connection create` | database.connection | sync | platform+login | POST /v1/databases/{id}/connections | result (secret on stdout) | -| `database connection rotate` | database.connection | sync | platform+login | POST /v1/connections/{id}/rotate | result (needs --confirm, secret on stdout) | -| `database connection remove` | database.connection | sync | platform+login | DELETE /v1/connections/{id} | result (needs --confirm) | -| `bucket list` | bucket | sync | platform+login | GET /v1/buckets | result | -| `bucket create` | bucket | sync | platform+login | POST /v1/buckets | result | -| `bucket delete` | bucket | sync | platform+login | DELETE /v1/buckets/{bucketId} | result (needs --confirm) | -| `bucket key list` | bucket.key | sync | platform+login | GET /v1/buckets/{bucketId}/keys | result | -| `bucket key create` | bucket.key | sync | platform+login | POST /v1/buckets/{bucketId}/keys | result (secret on stdout) | -| `bucket key delete` | bucket.key | sync | platform+login | DELETE /v1/buckets/{bucketId}/keys/{keyId} | result | -| `app build` | app | sync, local build, file-writing (build artifact) | none | none | result (long-running → progress events) | -| `app run` | app | long-running local process, pass-through output | none | none | server-ish (local dev); rejects --json | -| `app deploy` | app | long-running + progress + interactive, file-writing | platform (no login fallback) | ComputeClient.deployApp, POST /v1/projects, branches, env vars, POST /v1/databases (--db) | session (steps/progress) | -| `app show` | app | sync (+ picker) | platform | GET /v1/apps, listDeployments | result | -| `app open` | app | sync, browser-opening | platform | GET /v1/apps, listDeployments | result + local browser action | -| `app domain add` | app.domain | sync | platform | POST /v1/apps/{appId}/domains | result | -| `app domain show` | app.domain | sync | platform | GET /v1/apps/{appId}/domains, GET /v1/domains/{id} | result | -| `app domain remove` | app.domain | sync + confirm prompt | platform | DELETE /v1/domains/{id} | result (consent) | -| `app domain retry` | app.domain | sync | platform | POST /v1/domains/{id}/retry | result | -| `app domain wait` | app.domain | poll (status until active/failed/timeout) | platform | GET /v1/domains/{id} loop | stream/status events | -| `app logs` | app | stream | platform | ComputeClient.streamDeploymentLogs | stream | -| `app list-deploys` | app | sync (+ picker) | platform | GET /v1/apps, listDeployments | result | -| `app show-deploy` | app | sync | platform | ComputeClient.showDeployment | result | -| `app promote` | app | remote operation w/ progress | platform | ComputeClient.promoteDeployment | session (progress) | -| `app rollback` | app | remote operation w/ progress | platform | ComputeClient.promoteDeployment (promote of older deploy) | session (progress) | -| `app remove` | app | remote destroy (SDK polls), type-to-confirm prompt | platform | ComputeClient.showApp + destroyApp (poll 2s / 120s) | session (consent + progress) | - -Group nodes (print help when invoked bare; no action of their own): root `prisma`, `agent`, `auth`, `auth workspace`, `project`, `project env`, `git`, `branch`, `build`, `database`, `database backup`, `database connection`, `bucket`, `bucket key`, `app`, `app domain`. - -## 2. Group census - -| group | leaf commands | -|---|---| -| top-level (version, init, feedback) | 3 | -| agent | 3 | -| auth (incl. workspace) | 6 (login, logout, whoami, workspace list/use/logout) | -| project (incl. env) | 11 (list, show, create, link, rename, remove, transfer, env add/update/list/remove) | -| git | 2 | -| branch | 1 | -| build | 1 | -| database (incl. backup, connection) | 11 (list, show, create, usage, restore, remove, backup list, connection list/create/rotate/remove) | -| bucket (incl. key) | 6 (list, create, delete, key list/create/delete) | -| app (incl. domain) | 16 (build, run, deploy, show, open, logs, list-deploys, show-deploy, promote, rollback, remove, domain add/show/remove/retry/wait) | -| **Total leaf commands** | **60** | - -Plus 16 group/help nodes and two program-level utilities: `--version` (handled before parse in `src/cli.ts:51`) and `--help` (commander). - -## 3. Shared shell machinery - -### 3.1 Global flags - -Two sets (`src/shell/global-flags.ts`): - -- **Full set** (`addGlobalFlags`, attached to every leaf command): - - `--json` — structured JSON envelope on stdout. Success: `{ok:true, command, result, warnings, nextSteps, nextActions}` (pretty-printed, `src/shell/output.ts:22`). Error: `{ok:false, command, error:{code,domain,severity,summary,why,fix,where,meta,docsUrl}, warnings, nextSteps, nextActions}`. Streaming commands emit one JSON event per line (`writeJsonEvent`) plus a wrapper success event unless opted out (`build logs` opts out, `src/commands/build/index.ts:54`). - - `-q, --quiet` — suppress human stderr rendering; stdout payloads (renderStdout) still print. - - `-v, --verbose` — appends a "Local context" diagnostics block (duration, cwd, state file, git ref/sha/dirty; `src/shell/diagnostics-output.ts`). - - `--trace` — include debug/stack in human error output. - - `-y, --yes` — accept supported confirmation prompts. - - `--interactive` / `--no-interactive` — force/disable prompting. - - `--color` / `--no-color` — force/disable color. -- **Compact set** (`addCompactGlobalFlags`, attached to the root program and every group node): `--json`, `-q/--quiet`, `-v/--verbose`, `--trace`, `--no-interactive`, `-y/--yes`. **`--interactive`, `--color`, `--no-color` are missing from the compact set** even though `docs/product/command-spec.md:55-66` lists them as shared global flags — a flag placed before the subcommand can be rejected by commander. Mitigation: `resolveGlobalFlags` (`global-flags.ts:81`) also scans raw argv, because commander v12 can swallow duplicate parent/child options. -- Program-level `--version` (exits 0, honors `--json`; `src/cli.ts:51,103`). - -**There is no `--fixture` flag.** Fixture mode is enabled only by the `PRISMA_CLI_MOCK_FIXTURE_PATH` env var or the programmatic `runtime.fixturePath` option (`src/shell/runtime.ts:61-68`). Every controller has an `isRealMode()` check on exactly those two inputs. - -Prompting rule `canPrompt` (`src/shell/runtime.ts:105`): false when `--json`, `--no-interactive`, `CI` env without `--interactive`, or stdin/stderr not TTYs. - -### 3.2 Command runner - -`src/shell/command-runner.ts`: `runCommand` (result commands: build context → run handler → render human/stdout/json; maps thrown `CliError`, SDK `AuthError` → `AUTH_REQUIRED`, empty-service-token → `AUTH_CONFIG_INVALID`, aborts → `COMMAND_CANCELED` exit 130) and `runStreamingCommand` (app logs, build logs, domain wait; same error mapping, optional trailing JSON success event). Success human output goes to **stderr**; only `renderStdout` payloads and JSON go to **stdout**. - -### 3.3 Error taxonomy - -`src/shell/errors.ts`: `CliError {code, domain, summary, why, fix, debug, where, meta, docsUrl, exitCode, nextSteps, nextActions, humanLines}`. Domains: `cli | auth | project | branch | app | database | bucket`. Codes are FLAT_UPPER_SNAKE (no dots). Exit codes in use: - -- 0 success — and one deliberate oddity: canceling the production-deploy confirmation throws `CONFIRMATION_REQUIRED` with **exitCode 0** (`src/lib/app/production-deploy-gate.ts:209-219`). -- 1 default error. -- 2 usage errors (`USAGE_ERROR`), commander parse errors (`src/cli.ts:66`), exact-id confirmation failures in project/database/bucket (`CONFIRMATION_REQUIRED` exit 2), `PROJECT_LINK_TARGET_REQUIRED`, `PROJECT_AMBIGUOUS`-family, `APP_AMBIGUOUS`, `WORKSPACE_AMBIGUOUS`, `PROD_DEPLOY_REQUIRES_FLAG`, `BRANCH_NOT_DEPLOYABLE`, `DOMAIN_HOSTNAME_INVALID`. -- 130 `COMMAND_CANCELED` (SIGINT/abort). -- Inconsistency to note for the port: `CONFIRMATION_REQUIRED` is exit **2** for project/database/bucket exact-id confirms but exit **1** for app remove / domain remove / prod-deploy non-interactive confirms (`src/controllers/app.ts:3053-3062, 2368-2381`; `production-deploy-gate.ts:189-207`). - -Full code census (grep `code: "` over src): USAGE_ERROR, AUTH_REQUIRED, AUTH_CONFIG_INVALID, COMMAND_CANCELED, WORKSPACE_SWITCH_UNAVAILABLE, WORKSPACE_NOT_AUTHENTICATED, WORKSPACE_AMBIGUOUS, FEATURE_UNAVAILABLE, UNEXPECTED_ERROR, VERSION_UNAVAILABLE, FEEDBACK_SEND_FAILED, AGENT_SKILLS_INSTALL_FAILED, INIT_CONVERT_INCOMPLETE, INIT_CONVERT_UNSUPPORTED, INIT_CONFIG_EXISTS, INIT_DETECTION_FAILED, COMPUTE_CONFIG_INVALID, COMPUTE_CONFIG_TARGET_REQUIRED, COMPUTE_CONFIG_TARGET_UNKNOWN, PROJECT_NOT_FOUND, PROJECT_AMBIGUOUS, PROJECT_SETUP_REQUIRED, PROJECT_LINK_TARGET_REQUIRED, PROJECT_CREATE_FAILED, PROJECT_RENAME_FAILED, PROJECT_REMOVE_BLOCKED, PROJECT_TRANSFER_REJECTED, TRANSFER_RECIPIENT_REQUIRED, TRANSFER_RECIPIENT_UNAVAILABLE, CONFIRMATION_REQUIRED, LOCAL_STATE_STALE, LOCAL_STATE_WRITE_FAILED, LOCAL_PROJECT_WORKSPACE_MISMATCH, REPO_PROVIDER_UNSUPPORTED, REPO_NOT_CONNECTED, REPO_INSTALLATION_REQUIRED, REPO_NOT_ACCESSIBLE, REPO_ALREADY_CONNECTED, REPO_CONNECTION_FAILED, BRANCH_API_ERROR (or API-provided code), BRANCH_NOT_FOUND, BRANCH_NOT_DEPLOYABLE, BRANCH_DATABASE_SETUP_FAILED, DATABASE_NOT_FOUND, DATABASE_AMBIGUOUS, DATABASE_BACKUP_NOT_FOUND, DATABASE_CONNECTION_NOT_FOUND, DATABASE_CONNECTION_MISSING, DATABASE_CONNECTION_STRING_MISSING, DATABASE_API_ERROR (or API code), DATABASE_BACKUPS_UNSUPPORTED, DATABASE_RESTORE_CONFLICT, PLAN_LIMIT_REACHED, BUCKET_NOT_FOUND, BUCKET_KEY_NOT_FOUND, BUCKET_KEY_SECRET_MISSING, ENV_VARIABLE_ALREADY_EXISTS, ENV_VARIABLE_NOT_FOUND, ENV_BRANCH_SCOPE_IS_PRODUCTION, ENV_BRANCH_NOT_FOUND, ENV_BRANCH_CREATE_REQUIRES_DEFAULT_BRANCH, ENV_FILE_APPLY_FAILED, DEPLOY_FAILED, BUILD_FAILED, RUN_FAILED, REMOVE_FAILED, NO_DEPLOYMENTS, NO_PREVIOUS_DEPLOYMENT, DEPLOYMENT_NOT_FOUND, APP_AMBIGUOUS, BUILD_SETTINGS_MIGRATION_REQUIRED, BUILD_SETTINGS_UNSUPPORTED, FRAMEWORK_NOT_DETECTED, PROD_DEPLOY_REQUIRES_FLAG, DOMAIN_HOSTNAME_INVALID, DOMAIN_QUOTA_EXCEEDED, DOMAIN_ALREADY_REGISTERED, DOMAIN_DNS_NOT_CONFIGURED, DOMAIN_NOT_FOUND, DOMAIN_RETRY_NOT_ELIGIBLE, DOMAIN_VERIFICATION_FAILED, DOMAIN_VERIFICATION_TIMEOUT, BUILD_NOT_FOUND, BUILD_LOGS_FAILED. - -`nextActions` (`src/shell/next-actions.ts`): kinds `run-command | user-choice | edit-file | done`, journeys `project-setup | deploy-app | inspect | recover`. Crashes always produce a structured `UNEXPECTED_ERROR` JSON envelope under `--json` with a pre-filled `prisma-cli feedback "..."` recover action (`src/shell/output.ts:120`). - -### 3.4 Auth machinery - -- Precedence: `PRISMA_SERVICE_TOKEN` env var (empty → `AUTH_CONFIG_INVALID`) then stored OAuth via `FileTokenStorage` (`src/adapters/token-storage.ts`, built on `@prisma/credentials-store`; multi-workspace grants + one active-workspace pointer; file lock for refresh coordination). -- `requireComputeAuth` (`src/lib/auth/guard.ts`) returns a `ManagementApiClient` or null. -- `requireAuthenticatedAuthState` (`src/controllers/auth.ts:206`): used by project/database/bucket/branch/env/git — if unauthenticated **and a TTY is available it launches the full interactive OAuth login** before proceeding; otherwise throws `AUTH_REQUIRED`. The app group and `build logs` instead use `requireComputeAuth` directly and never auto-login. -- Real login (`src/lib/auth/login.ts`): local HTTP callback server on `localhost:`, PKCE via `@prisma/management-api-sdk`, opens browser (`open`), TTY paste-the-callback-URL fallback, HTML success page, `GET /v1/workspaces/{id}` for the workspace name. -- API base URL override: `getApiBaseUrl(env)` in `src/lib/auth/client.ts` (env-driven). - -### 3.5 Fixture-mode machinery (dies with the mock) - -- `src/adapters/mock-api.ts` (696 lines, `MockApi.load(fixturePath)`) — the whole in-memory platform. -- `context.api` getter in `src/shell/runtime.ts:71-80` (throws if touched in real mode). -- `isRealMode()` duplicated in `src/controllers/{auth,project,database,bucket,branch,app,app-env}.ts` and each `else` branch below it. -- `src/use-cases/` (auth.ts, branch.ts, project.ts, contracts.ts, create-cli-gateways.ts) — only reachable from fixture branches. -- Fixture providers: `createFixtureProjectProvider` (`project.ts:908`), `createFixtureDatabaseProvider` (`database.ts:790`), `createFixtureBucketProvider` (`bucket.ts:349`). -- Fixture-only flags: `auth login --provider/--user/--workspace` (hidden via `.hideHelp()`, `src/commands/auth/index.ts:59-61`; real mode ignores them entirely — `runAuthLogin` does not read options when `isRealMode`). -- Fixture-only refusals: `project create` (`FEATURE_UNAVAILABLE` in fixture mode), all `app` commands (`ensurePreviewAppMode`, app.ts:4678: fixture mode → `FEATURE_UNAVAILABLE`). -- Fixture conventions: transfer recipient token = workspace id (`project.ts:757-797`); git connect/disconnect persist a pending connection in the local state store instead of the API. -- Env var: `PRISMA_CLI_MOCK_FIXTURE_PATH`. -- Tests passing `fixturePath` (see §5 census): auth.test.ts, project.test.ts, project-mutations.test.ts, project-controller.test.ts, branch.test.ts, database.test.ts, bucket.test.ts, app.test.ts (for refusal paths), init.test.ts, shell.test.ts, version.test.ts, update-check.test.ts, auth-real-mode.test.ts (asserting the boundary), auth-controller.test.ts. - -### 3.6 Update-check integration - -`src/shell/update-check.ts`, wired in `runCli` before parsing (`src/cli.ts:49`) and in `src/bin.ts` as a detached worker process: - -- Cache file `update-check.json` in a cache dir; stderr-only notification "Update available: prisma-cli X -> Y" at most every 24h. -- Skipped when `NO_UPDATE_NOTIFIER` set, CI/GITHUB_ACTIONS, non-TTY stderr, `--json`, `--quiet`/`-q`, `--version`, or test runtime (unless `PRISMA_CLI_TEST_ENABLE_UPDATE_CHECK=1`). -- Remote discovery runs in a **spawned detached child process** re-invoking the CLI binary with `PRISMA_CLI_RUN_UPDATE_CHECK_WORKER=1` (+ `PRISMA_CLI_UPDATE_CHECK_DIR`, `_INSTALLED_VERSION`, `_REGISTRY_URL`); fetches `https://registry.npmjs.org/@prisma%2fcli` with 3s timeout. -- Install instruction is invocation-aware (pnpm/bun/npm dev-dep, npm global, or docs URL for npx/bunx). - -### 3.7 Agent-setup tip machinery - -Two pieces, both driven by `src/lib/agent/setup-status.ts` state kept in the local state store: - -- `resolveAgentSetupTipCommand` (`src/controllers/auth.ts:707`): after `auth login` (human TTY mode only) appends a one-line tip suggesting the skills install command; suppressed by `--json`, `--quiet`, CI, non-TTY. -- `maybePromptForAgentSetup` (`src/controllers/agent-setup.ts`): one-time confirm prompt "Install the Prisma Compute skill for this project?" run by `init` and `app deploy`; a "no" is remembered via `stateStore.setAgentSetupPromptDismissedAt`; an install failure downgrades to a warning. -- Command strings are rendered through `resolvePrismaCliPackageCommand(FormatterSync)` (`src/lib/agent/cli-command.ts`), which picks `pnpm dlx | bunx | npx -y @prisma/cli@latest ...` per project package manager (overridable via `PRISMA_CLI_PACKAGE_RUNNER`, `PRISMA_CLI_PACKAGE_NAME`, `PRISMA_CLI_PACKAGE_SPEC`, `PRISMA_CLI_BINARY`). - -### 3.8 Local files & env vars (shared) - -- `.prisma/local.json` — project pin `{workspaceId, projectId}` (`src/lib/project/local-pin.ts`); writer also appends `.prisma/` to `.gitignore`. -- `/.prisma/cli/state.json` — local state store (`src/adapters/local-state.ts`; dir override `PRISMA_CLI_STATE_DIR` or runtime.stateDir; project dir located by walking up to the compute config, `src/shell/runtime.ts:92-103`): selected app per project, known live deployment per app, agent-setup status, fixture git connections. -- OAuth tokens — OS credentials store via `@prisma/credentials-store` (token-storage.ts). -- Compute config: `prisma.compute.ts` / `prisma.compute.json` (read by init/app group via `@prisma/compute-sdk/config`). -- Env var census: `PRISMA_SERVICE_TOKEN`, `PRISMA_PROJECT_ID`, `PRISMA_APP_ID`, `PRISMA_CLI_MOCK_FIXTURE_PATH`, `PRISMA_CLI_STATE_DIR`, `PRISMA_CLI_FEEDBACK_URL`, `PRISMA_CLI_DOMAIN_WAIT_POLL_MS`, `PRISMA_CLI_GITHUB_INSTALL_POLL_INTERVAL_MS`, `PRISMA_CLI_GITHUB_INSTALL_TIMEOUT_MS`, `PRISMA_CLI_INIT_INSTALL_COMMAND`, `PRISMA_CLI_PACKAGE_{RUNNER,NAME,SPEC}`, `PRISMA_CLI_BINARY`, `PRISMA_CLI_RUN_UPDATE_CHECK_WORKER`, `PRISMA_CLI_UPDATE_CHECK_{DIR,INSTALLED_VERSION,REGISTRY_URL}`, `PRISMA_CLI_TEST_ENABLE_UPDATE_CHECK`, `NO_UPDATE_NOTIFIER`, `CI`, `INIT_CWD`. - -### 3.9 Spec discrepancies (code vs docs/product/command-spec.md) - -1. Spec documents `build list` (line 2074) and `build show` (line 2105); neither exists in code. Spec marks both "blocked on Management API rollout" — planned, not drift, but the shell registers only `build logs` (`src/commands/build/index.ts`). -2. Spec Global Rules (lines 55-66) list `--interactive`, `--color`, `--no-color` as shared flags; the compact flag set on the root and group nodes omits them (`src/shell/global-flags.ts:48-61`), so their acceptance depends on flag position. -3. `auth login --provider/--user/--workspace` exist in code (hidden, fixture-only) but not in the spec's `auth login` section (line 547). Real mode silently ignores them (`src/controllers/auth.ts:73-75`) rather than erroring. -4. `project env remove` has an undocumented alias `rm` (`src/commands/env.ts:206`); the spec section (line 1849) names only `remove`. -5. Spec's `app remove` heading (line 2232) lists `-y --yes` as if command-specific; in code it is the shared global flag. -6. The env group descriptor description strings end with periods (`project.env.*` in command-meta.ts) while every other descriptor has none — cosmetic inconsistency in help output. - ---- - -## 4. Per-command inventory - -Legend for flag tables: "global" flags (§3.1) are not repeated per command; every leaf command has the full global set. `[F]` = fixture-mode-only. - -### `prisma version` -- **Summary**: "Show CLI build and environment" (command-meta.ts:44). -- **Flags**: globals only. -- **Positionals**: none. -- **Auth**: none. -- **API calls**: none. -- **Behavior**: sync. -- **Output**: human lines with CLI name/version, node version, os platform/arch, invocation kind (`dev|npx|bunx|global|unknown`); no separate `renderJson` serializer (raw result used in JSON). No distinct stdout payload. Errors: `VERSION_UNAVAILABLE` (exit 1) if package.json version missing. -- **Prompts**: none. **Side effects**: none. -- **Tests**: `tests/version.test.ts` (some fixture refs for shell wiring). -- **Engine notes**: pure result kind; also duplicated as the program-level `--version` fast path (`src/cli.ts:123-160`) which bypasses the command runner — the port should unify these. - -### `prisma init` -- **Summary**: "Write a committed compute config for this app". -- **Flags**: - -| name | alias | type | default | required | description | -|---|---|---|---|---|---| -| `--framework ` | — | string (alias-resolved to nextjs/nuxt/astro/hono/nestjs/tanstack-start/bun/custom) | detected | no | Framework override; detected when omitted | -| `--entry ` | — | string | derived (bun/hono: src/index.ts style) | no | Source entrypoint for entrypoint frameworks | -| `--http-port ` | — | string→int | framework default | no | HTTP port the app listens on | -| `--region ` | — | string (COMPUTE_REGIONS) | none | no | Region used when deploy creates the app | -| `--name ` | — | string | inferred (package.json / directory) | no | App name | -| `--link` / `--no-link` | — | boolean | prompt (TTY) / skip | no | Link this directory to a Project / skip | -| `--project ` | — | string | — | no | Project to link to | -| `--install` / `--no-install` | — | boolean | prompt (TTY) / skip | no | Install @prisma/compute-sdk dev dep for types | -| `--format ` | — | enum | ts | no | Config format; explicit `--format ts` over an existing prisma.compute.json performs a conversion | - -- **Positionals**: none. -- **Auth**: none for the write itself; the link step (when taken) goes through `runProjectLink` → `requireAuthenticatedAuthState` (interactive login possible). -- **API calls**: only via link step (GET /v1/projects, optional POST /v1/projects). -- **Behavior**: sync + interactive; file-writing. -- **Output**: settings preview + written path + types/link status; `serializeInit` JSON serializer. Errors: `INIT_CONFIG_EXISTS`, `INIT_CONVERT_INCOMPLETE`, `INIT_CONVERT_UNSUPPORTED`, `INIT_DETECTION_FAILED`, `COMPUTE_CONFIG_INVALID`, USAGE_ERROR variants (exit 2), custom-framework-JSON refusal. -- **Prompts** (TTY, not --yes/--json): "Customize settings?" (framework select + port text), install-types confirm, link confirm (→ project picker via `runProjectLink`), agent-setup skill confirm. -- **Side effects**: writes `prisma.compute.ts` or `prisma.compute.json` (flag `wx`, never clobbers); conversion also deletes the old JSON config; optional `npm/pnpm/bun add -D @prisma/compute-sdk` child process (override `PRISMA_CLI_INIT_INSTALL_COMMAND`); link writes `.prisma/local.json` (+ `.gitignore`); skills installer child process. -- **Tests**: `tests/init.test.ts`, `tests/init-agent-setup.test.ts`. -- **Engine notes**: multi-step with prompts and child processes — session kind with step events; the conversion path is a distinct sub-behavior selected by flag+filesystem state. - -### `prisma feedback ` -- **Summary**: "Send feedback to the Prisma CLI team". Anonymous unless `--email`. -- **Flags**: `--email
` (string, optional, ≤320 chars, regex-validated) + globals. -- **Positionals**: `` required, ≤4000 chars. -- **Auth**: none. -- **API calls**: none (Management). POSTs to feedback service `https://hiieirp2pwqnjvq9axzyg6d0.fra.prisma.build/feedback` (override `PRISMA_CLI_FEEDBACK_URL`), 3s timeout; payload = message, optional email, meta {cliVersion, nodeVersion, platform, arch}. -- **Behavior**: sync. **Output**: confirmation line; no renderJson serializer. Errors: USAGE_ERROR (empty/too long/bad email, exit 2), `FEEDBACK_SEND_FAILED` (exit 1). -- **Prompts**: none. **Side effects**: outbound HTTP only. -- **Tests**: `tests/feedback.test.ts`. -- **Engine notes**: clean result kind; the crash-recovery flow pre-fills this command (`src/shell/output.ts:104`), so the v8 shell must keep an equivalent. - -### `prisma agent install` / `prisma agent update` -- **Summary**: "Install/Refresh Prisma skills for AI coding agents". Same flags, same controller (`runAgentInstall`, operation differs). -- **Flags**: - -| name | type | default | description | -|---|---|---|---| -| `--agent ` | repeatable string[] | claude-family defaults (`DEFAULT_PRISMA_AGENT_TARGETS`) | agent target; repeat for multiple | -| `--all-agents` | boolean | false | pass `*` to the skills CLI | -| `--skill ` | repeatable string[] | `DEFAULT_PRISMA_AGENT_SKILLS` | skill to install; repeat | -| `--global` | boolean | false | install into user dir instead of project | -| `--copy` | boolean | false (forced true on win32) | copy instead of symlink | -| `--dry-run` | boolean | false | show the command without running | - -- **Positionals**: none. **Auth**: none. **API calls**: none. -- **Behavior**: sync; spawns `pnpm dlx|bunx|npx -y skills-cli add --skill … --agent … [--global] [--copy] --yes` via execa (stdin ignored). -- **Output**: install summary; `serializeAgentInstall`. Error: `AGENT_SKILLS_INSTALL_FAILED` (exit 1, nextStep = the raw installer command). -- **Prompts**: none. **Side effects**: child process writes skill files into the project or user dir. -- **Tests**: `tests/agent.test.ts`. -- **Engine notes**: local child-process command; result kind; `--dry-run` returns `{status:"would-install", command}`. - -### `prisma agent status` -- **Summary**: "Show installed Prisma skills". -- **Flags**: `--global` (check user-dir skills instead of project) + globals. -- **Auth**: none. **API**: none. Runs `skills-cli list [-g] --json`, filters names `prisma`/`prisma-*`; falls back to the skills lock file for project scope with a warning when the CLI call fails. -- **Behavior**: sync + child process. **Output**: skills table, statusSource (`skills-cli|skills-lock|unavailable`); `serializeAgentStatus`. -- **Prompts**: none. **Side effects**: child process (read-only). -- **Tests**: `tests/agent.test.ts`. -- **Engine notes**: result kind. - -### `prisma auth login` -- **Summary**: "Log in to your Prisma platform account". -- **Flags**: `--provider `, `--user `, `--workspace ` — all hidden (`hideHelp`) and **[F] fixture-only** (real mode never reads them; fixture mode uses them to skip select prompts). Plus globals. -- **Positionals**: none. -- **Auth**: n/a (creates the session). Never fails for being unauthenticated. -- **API calls** (real): OAuth authorize URL via `sdk.getLoginUrl` (scope `workspace:admin offline_access`), token exchange `sdk.handleCallback`, `GET /v1/workspaces/{id}` for the success page/workspace name; then `readAuthState` → `GET /v1/me` + `GET /v1/workspaces/{id}`. -- **Behavior**: interactive + browser-opening + hosts a localhost HTTP callback server; TTY paste-URL fallback loop. Non-TTY real mode still opens/points at the URL but a failed browser launch is fatal without the paste fallback. -- **Output**: auth state lines (user, workspace) + optional agent-setup tip; **no renderJson serializer** (raw AuthStateResult in JSON). nextSteps: whoami, project list, optional skills install. -- **Prompts**: fixture mode: provider/user/workspace select prompts (usage error if non-interactive without the fixture flags). Real mode: browser + paste fallback. -- **Side effects**: writes OAuth tokens to the OS credentials store; opens browser; binds a localhost TCP port; agent-setup tip reads local state. -- **Tests**: `tests/auth.test.ts`, `tests/auth-login.test.ts` (real login flow), `tests/auth-real-mode.test.ts`, `tests/auth-controller.test.ts`, `tests/auth-ops.test.ts`, `tests/auth-usecases.test.ts` (fixture use-cases). -- **Engine notes**: session kind (browser hand-off, long wait, cancellation); the fixture selection flow and its flags die with the mock. The localhost callback server is machinery the engine must own or replace. - -### `prisma auth logout` -- **Summary**: "Clear stored authentication credentials". -- **Flags**: `--workspace ` — when present the command internally dispatches to `auth.workspace.logout` (same controller/envelope as that command). Plus globals. -- **Auth**: local store. **API calls**: `readAuthState` (GET /v1/me best-effort) after clearing. -- **Behavior**: sync. Clears **all** local OAuth sessions (`FileTokenStorage.clearTokens`); does not touch `PRISMA_SERVICE_TOKEN`. -- **Output**: signed-out state; no renderJson serializer. -- **Prompts**: none. **Side effects**: credentials store mutation. -- **Tests**: auth.test.ts / auth-real-mode.test.ts / auth-ops.test.ts. -- **Engine notes**: result kind. Note the argv-level dispatch: one registered command produces two command ids (`auth.logout` vs `auth.workspace.logout`) depending on the flag. - -### `prisma auth whoami` -- **Summary**: "Show the authenticated user and accessible workspace". -- **Flags**: globals only. **Auth**: none required — reports `authenticated: false` rather than failing (nextSteps suggests login). -- **API calls**: GET /v1/me (principal), fallback JWT-claims + GET /v1/workspaces/{id}; a 401 from either → signed-out state, not an error. -- **Behavior**: sync. **Output**: user/workspace/credential lines; no renderJson serializer. Error path: `AUTH_CONFIG_INVALID` for empty service token. -- **Tests**: auth.test.ts, auth-real-mode.test.ts, v8-whoami.test.ts (v8 parity test, concurrent slice). -- **Engine notes**: result kind; already the S1 v8 pilot command. - -### `prisma auth workspace list` -- **Summary**: "List locally authenticated workspaces". -- **Flags**: globals only. **Positionals**: none. -- **Auth**: local store; works while signed out (empty list, nextStep login). -- **API calls**: best-effort `GET /v1/workspaces/{id}` per stale workspace record to hydrate id/name (failures silent). -- **Behavior**: sync. **Output**: table id/name/active/source(`oauth|service_token`)/switchable/lastSeenAt; `serializeAuthWorkspaceList`. With `PRISMA_SERVICE_TOKEN` set, the token workspace is listed active and all OAuth entries as non-switchable. -- **Prompts**: none. **Side effects**: may rewrite hydrated workspace metadata into the credentials store. -- **Tests**: auth.test.ts, auth-real-mode.test.ts. -- **Engine notes**: result kind. - -### `prisma auth workspace use [id-or-name]` -- **Summary**: "Switch the local CLI workspace". -- **Positionals**: `[id-or-name]` optional; omitted → single workspace auto-selected, multiple → interactive select, non-interactive multiple → USAGE_ERROR. -- **Flags**: globals only. -- **Auth**: local store. Fails `WORKSPACE_SWITCH_UNAVAILABLE` (exit 1) when `PRISMA_SERVICE_TOKEN` set. -- **API calls**: hydration GETs only. **Behavior**: sync + optional select prompt. -- **Output**: previous/selected workspace; `serializeAuthWorkspaceUse`. Errors: `WORKSPACE_NOT_AUTHENTICATED` (1), `WORKSPACE_AMBIGUOUS` (2), USAGE_ERROR "No authenticated workspaces" (2). -- **Side effects**: active-workspace pointer in credentials store. -- **Tests**: auth.test.ts, auth-real-mode.test.ts. -- **Engine notes**: result kind with an optional selection prompt (session if interactive). - -### `prisma auth workspace logout ` -- **Summary**: "Remove one local OAuth workspace session". -- **Positionals**: `` required (controller-level usage error when blank, exit 2). -- **Flags**: globals only. **Auth**: local store; works even with service token set (cleans local state only). -- **API calls**: hydration GETs. **Behavior**: sync. -- **Output**: removed workspace, wasActive, remaining active workspace (never auto-falls-through — user must `workspace use` next); `serializeAuthWorkspaceLogout`. Errors: `WORKSPACE_NOT_AUTHENTICATED`, `WORKSPACE_AMBIGUOUS`. -- **Side effects**: credentials store mutation. -- **Tests**: auth.test.ts, auth-real-mode.test.ts. -- **Engine notes**: result kind. - -### `prisma project list` -- **Summary**: "List all projects in your workspace". -- **Flags**: globals only. **Positionals**: none. -- **Auth**: platform, via `requireAuthenticatedAuthState` (interactive login on TTY, else AUTH_REQUIRED); `WORKSPACE_REQUIRED` usage error if no workspace. -- **API calls**: `GET /v1/projects` (filtered client-side to the active workspace). -- **Behavior**: sync. **Output**: workspace header + project table + localBinding status (`linked|not-linked|invalid` from `.prisma/local.json`); `serializeProjectList`; nextActions steer setup when unlinked. -- **Prompts**: only the auto-login. **Side effects**: none. -- **Tests**: project.test.ts, project-controller.test.ts, project-real-mode.test.ts, project-usecases.test.ts. -- **Engine notes**: result kind; localBinding is a local-filesystem read blended into a remote result. - -### `prisma project show` -- **Summary**: "Show this directory's Project binding". -- **Flags**: `--project ` + globals. -- **Auth**: platform+login. **API**: GET /v1/projects. -- **Behavior**: sync. **Output**: binding status, resolved project or null with `suggestedProjectName` + setup nextActions; `serializeProjectShow`. Errors: resolution family (`PROJECT_NOT_FOUND` 1, `PROJECT_AMBIGUOUS` 2, `LOCAL_STATE_STALE`, `LOCAL_PROJECT_WORKSPACE_MISMATCH`). -- **Tests**: project.test.ts, project-resolution.test.ts, project-real-mode.test.ts. -- **Engine notes**: result kind. - -### `prisma project create ` -- **Summary**: "Create a Project and link this directory". -- **Flags**: `--region ` (Compute region id) + globals. **Positionals**: `` required, validated non-empty (`projectSetupNameRequiredError`). -- **Auth**: platform+login. Fixture mode: refused with `FEATURE_UNAVAILABLE`. -- **API calls**: `ComputeClient.createProject` (POST /v1/projects). -- **Behavior**: sync; file-writing. **Output**: created project + link confirmation; `serializeProjectSetup`. Errors: `PROJECT_CREATE_FAILED` (permission-aware fix text), `LOCAL_STATE_WRITE_FAILED`. -- **Side effects**: writes `.prisma/local.json`, appends `.prisma/` to `.gitignore`. -- **Tests**: project.test.ts, project-real-mode.test.ts, project-mutations.test.ts. -- **Engine notes**: result kind; local pin write is part of the contract. - -### `prisma project link [id-or-name]` -- **Summary**: "Link this directory to a Project". -- **Positionals**: `[id-or-name]` optional. **Flags**: globals only. -- **Auth**: platform+login. **API**: GET /v1/projects (+ POST /v1/projects when the picker's "create new" is chosen; fixture refuses creation). -- **Behavior**: with arg → sync; without arg on TTY (and not `--yes`) → interactive setup picker (`promptForProjectSetupChoice`: select existing / create new via text prompt / cancel); non-interactive without arg → `PROJECT_LINK_TARGET_REQUIRED` (exit 2, carries candidates + suggested name in meta/nextActions). -- **Output**: `serializeProjectSetup`. **Side effects**: `.prisma/local.json` + `.gitignore`. -- **Tests**: project.test.ts, project-mutations.test.ts, project-resolution.test.ts. -- **Engine notes**: session (picker) or result (explicit arg); the error meta is agent-oriented (candidate list) — preserve. - -### `prisma project rename ` -- **Summary**: "Rename the resolved Project". -- **Flags**: `--project ` + globals. **Positionals**: `` required non-empty. -- **Auth**: platform+login. **API**: PATCH /v1/projects/{id}. -- **Behavior**: sync. **Output**: renamed project + previousName; `serializeProjectRename`. Errors: `PROJECT_RENAME_FAILED`, resolution family. -- **Tests**: project-mutations.test.ts. -- **Engine notes**: result kind. - -### `prisma project remove ` -- **Summary**: "Remove a Project permanently after exact id confirmation". -- **Flags**: `--confirm ` (must equal the resolved project id) + globals. **Positionals**: `` id or name, required. -- **Auth**: platform+login. **API**: DELETE /v1/projects/{id}. -- **Behavior**: sync; no interactive prompt — confirmation is flag-only. `CONFIRMATION_REQUIRED` (exit 2, meta.expectedConfirm/receivedConfirm) when missing/mismatched. -- **Output**: removed project + `localPin.cleared`; `serializeProjectRemove`. Errors: `PROJECT_REMOVE_BLOCKED`, `PROJECT_NOT_FOUND`. Warning (not error) if the stale local pin cannot be deleted. -- **Side effects**: deletes `.prisma/local.json` when it pointed at the removed project. -- **Tests**: project-mutations.test.ts. -- **Engine notes**: consent-grade confirmation via exact-id flag; maps to needs.consent in the engine. - -### `prisma project transfer ` -- **Summary**: "Transfer a Project to another workspace after exact id confirmation". -- **Flags**: `--to-workspace ` (locally authenticated recipient) XOR `--recipient-token `; `--confirm `; globals. Mutual exclusion and at-least-one enforced (USAGE_ERROR 2 / `TRANSFER_RECIPIENT_REQUIRED` 2). -- **Auth**: platform+login; `--to-workspace` additionally resolves a second OAuth session locally (`resolveRecipientWorkspaceSession` probes `GET /v1/workspaces` with the recipient tokens). With `PRISMA_SERVICE_TOKEN` set, `--to-workspace` fails `TRANSFER_RECIPIENT_UNAVAILABLE` (exit 1). -- **API**: POST /v1/projects/{id}/transfer (recipient access token in body). -- **Behavior**: sync. **Output**: project, recipient {workspaceId/name/source}, `localPin.action` (`rewritten|cleared|none`); `serializeProjectTransfer`. Errors: `PROJECT_TRANSFER_REJECTED`, `WORKSPACE_NOT_AUTHENTICATED`, `WORKSPACE_AMBIGUOUS`, `CONFIRMATION_REQUIRED` (2). -- **Side effects**: rewrites `.prisma/local.json` to the recipient workspace or deletes it. -- **Tests**: project-mutations.test.ts. -- **Engine notes**: exact-id consent + dual-credential use — the most complex needs.credentials story in the CLI. - -### `prisma project env add` -- **Summary**: "Create a new environment variable." -- **Flags**: - -| name | type | required | description | -|---|---|---|---| -| `--file ` | string | no | read KEY=VALUE assignments from a dotenv file (bulk mode) | -| `--role ` | enum | one of --role/--branch required | project template scope | -| `--branch ` | string | ″ | preview branch override scope | -| `--project ` | string | no | project override | - -- **Positionals**: `[assignment]` — `KEY=VALUE` or bare `KEY` (value pulled from the caller's environment); mutually exclusive with `--file`. -- **Auth**: platform+login. **API**: GET /v1/environment-variables (dup check), POST /v1/environment-variables; `--branch` may create the branch (POST /v1/projects/{projectId}/branches) — `ENV_BRANCH_CREATE_REQUIRES_DEFAULT_BRANCH` guards that. -- **Behavior**: sync. **Output**: metadata of the created var(s) (no values echoed); `serializeEnvAdd`. Errors: `ENV_VARIABLE_ALREADY_EXISTS`, `ENV_BRANCH_SCOPE_IS_PRODUCTION`, `ENV_BRANCH_NOT_FOUND`, `ENV_FILE_APPLY_FAILED` (partial-failure report for file mode), scope USAGE_ERRORs. -- **Prompts**: none. **Side effects**: none local. -- **Tests**: app-env.test.ts, app-env-vars.test.ts, app-env-presenter.test.ts. -- **Engine notes**: result kind; file mode is a batch with per-key partial failure semantics. - -### `prisma project env update` -- Same flags/positional/auth/API family as `add` but replaces an existing value (PATCH /v1/environment-variables/{envVarId}); missing var → `ENV_VARIABLE_NOT_FOUND`; `--branch` never creates a branch here (resolveExistingBranch). Serializer `serializeEnvUpdate`. Tests as above. - -### `prisma project env list` -- **Summary**: "List environment variable metadata for a scope (no values)." -- **Flags**: `--role`, `--branch`, `--project` (+ globals). No scope → overview across scopes (production, preview template, current branch overrides via `readLocalGitBranch`). -- **Auth**: platform+login. **API**: GET /v1/environment-variables (paginated), GET /v1/projects/{projectId}/branches. -- **Output**: metadata table (key, scope, updatedAt; never values); `serializeEnvList`. -- **Tests**: app-env.test.ts. **Engine notes**: result kind. - -### `prisma project env remove KEY` (alias: `rm`) -- **Flags**: `--role`, `--branch`, `--project`. **Positionals**: `` required. -- **Auth**: platform+login. **API**: GET /v1/environment-variables (resolve id), DELETE /v1/environment-variables/{envVarId}. -- **Output**: removed key metadata; `serializeEnvRm`. Errors: `ENV_VARIABLE_NOT_FOUND`, scope errors. -- **Tests**: app-env.test.ts. **Engine notes**: result kind. Alias `rm` is undocumented (spec discrepancy #4). - -### `prisma git connect [git-url]` -- **Summary**: "Connect the resolved project to a GitHub repository". -- **Flags**: `--project ` + globals. **Positionals**: `[git-url]` optional; falls back to the local `origin` remote (`readGitOriginRemote`); non-GitHub URL → `REPO_PROVIDER_UNSUPPORTED` (2); none at all → USAGE_ERROR (2). -- **Auth**: platform+login. -- **API calls**: GET /v1/source-repositories (existing check), GET /v1/scm-installations + GET /v1/scm-installations/{id}/repositories (paginated, per installation), POST /v1/scm-installations/install-intents (install URL), POST /v1/source-repositories. -- **Behavior**: sync when the repo is already reachable; otherwise browser-opening (install URL via `open` when interactive) + **polling**: re-lists installations every 2s (env `PRISMA_CLI_GITHUB_INSTALL_POLL_INTERVAL_MS`) up to 120s (`PRISMA_CLI_GITHUB_INSTALL_TIMEOUT_MS`) waiting for the GitHub App installation/repo access; terminal states: match found / `REPO_NOT_ACCESSIBLE` / `REPO_INSTALLATION_REQUIRED` (both exit 1, meta carries installUrl + opened). -- **Output**: repository connection record; **no renderJson serializer** (raw result). Errors also: `REPO_ALREADY_CONNECTED` (1), `REPO_CONNECTION_FAILED` (1, status-aware fix text; 401/403 → AUTH_REQUIRED). -- **Prompts**: none beyond the browser wait status line. **Side effects**: opens browser; fixture mode writes a pending connection into local state instead. -- **Tests**: project.test.ts, project-real-mode.test.ts (plus git-adapter.test.ts for URL parsing). -- **Engine notes**: session kind — browser hand-off + poll loop with progress ("Waiting for GitHub App installation…"), non-interactive short-circuit. - -### `prisma git disconnect` -- **Flags**: `--project ` + globals. **Positionals**: none. -- **Auth**: platform+login. **API**: GET /v1/source-repositories, DELETE /v1/source-repositories/{id}. -- **Behavior**: sync. **Output**: the removed connection; no renderJson serializer. Error: `REPO_NOT_CONNECTED` (1), `REPO_CONNECTION_FAILED`. -- **Tests**: project.test.ts, project-real-mode.test.ts. **Engine notes**: result kind. - -### `prisma branch list` -- **Summary**: "List Platform branches for the resolved project". -- **Flags**: globals only. **Positionals**: none. (No `--project` flag — resolution is pin/durable only; spec heading agrees.) -- **Auth**: platform+login. **API**: GET /v1/projects/{projectId}/branches, cursor-paginated to exhaustion. -- **Behavior**: sync. **Output**: branch table (name, role production/preview, envMap), production first; `serializeBranchList`. Errors: `BRANCH_API_ERROR` (or API code), resolution family. -- **Tests**: branch.test.ts, branch-controller.test.ts, branch-usecases.test.ts, read-branch.test.ts. -- **Engine notes**: result kind. - -### `prisma build logs ` -- **Summary**: "Stream the logs for a build". -- **Flags**: `--follow` (keep the connection open for a running build), `--cursor ` (resume from a prior terminal cursor) + globals. -- **Positionals**: `` required — a git-push/Console Build id, not a deployment id. -- **Auth**: platform via `requireComputeAuth` only — **no interactive login fallback**; unauthenticated → AUTH_REQUIRED (1). -- **API**: `GET /v1/builds/{buildId}/logs` with `parseAs: "stream"`, NDJSON records `{type:"log"| "terminal"}`. -- **Behavior**: stream. Human mode: log text to stdout (stderr for stderr-source/error-level), terminal non-`end` message to stderr; JSON mode: one event per record, **no wrapper success event** (`emitJsonSuccessEvent: false`). A `terminal error` record sets exit code 1 without throwing. -- **Output**: raw log lines on stdout — the only command whose primary human output is stdout line passthrough. Errors: `BUILD_NOT_FOUND` (404, indistinguishable for foreign builds), `BUILD_LOGS_FAILED`. -- **Prompts/side effects**: none. -- **Tests**: **none** (no test file references runBuildLogs/build.logs). -- **Engine notes**: stream kind with its own terminal-record protocol; the exit-code-via-record pattern must map onto engine stream termination status. - -### `prisma database list` -- **Flags**: `--project `, `--branch ` + globals. -- **Auth**: platform+login (all database commands: `requireAuthenticatedAuthState` + `requireComputeAuth`). -- **API**: GET /v1/databases (provider `createManagementDatabaseProvider`). -- **Behavior**: sync. **Output**: databases sorted branch→name→id; `serializeDatabaseList`. Plan-limit failures map to `PLAN_LIMIT_REACHED` with plan/upgrade info pulled from GET /v1/workspaces/{id}/subscription. -- **Tests**: database.test.ts, database-plan-limit.test.ts. -- **Engine notes**: result kind. - -### `prisma database show ` -- **Flags**: `--project`, `--branch` + globals. **Positionals**: `` id or name (resolved via list; `DATABASE_NOT_FOUND` 1 / `DATABASE_AMBIGUOUS` 1). -- **API**: GET /v1/databases (resolve), GET /v1/databases/{id}, GET /v1/databases/{id}/connections. -- **Output**: metadata + connection metadata, **no secret values**; `serializeDatabaseShow`. **Engine notes**: result kind. - -### `prisma database create ` -- **Flags**: `--region `, `--project`, `--branch` + globals. **Positionals**: `` required non-empty. -- **API**: POST /v1/databases. -- **Behavior**: sync. **Output**: has a **renderStdout** payload — the one-time connection URL is printed to stdout (`renderDatabaseCreateStdout`), separate from the human summary on stderr; `serializeDatabaseCreate` includes connection + connectionString. Errors: `PLAN_LIMIT_REACHED`, `DATABASE_API_ERROR`, USAGE_ERROR. -- **Engine notes**: result kind with a distinct machine-consumable stdout secret — engine needs a "sensitive stdout payload" concept. - -### `prisma database usage ` -- **Flags**: `--from `, `--to ` (date-only expanded to UTC day start/end; invalid calendar dates rejected; from ≤ to enforced), `--project`, `--branch`. -- **API**: GET /v1/databases/{id}/usage. **Output**: period + metrics + generatedAt; `serializeDatabaseUsage`. **Engine notes**: result kind. - -### `prisma database restore ` -- **Flags**: `--backup ` (required — USAGE_ERROR without it), `--source-database ` (backup owner, defaults to target), `--confirm ` (must equal target id), `--project`, `--branch`. -- **API**: restore POST (provider.ts:473). **Behavior**: sync (restore is immediate & irreversible per the confirm copy). Errors: `CONFIRMATION_REQUIRED` (2), `DATABASE_BACKUP_NOT_FOUND`, `DATABASE_RESTORE_CONFLICT`. `serializeDatabaseRestore`. -- **Engine notes**: exact-id consent; destructive. - -### `prisma database remove ` -- **Flags**: `--confirm `, `--project`, `--branch`. **API**: DELETE /v1/databases/{id}. `CONFIRMATION_REQUIRED` exit 2. `serializeDatabaseRemove`. Result kind + consent. - -### `prisma database backup list ` -- **Flags**: `--limit ` (integer 1–100, else USAGE_ERROR), `--project`, `--branch`. **API**: GET /v1/databases/{id}/backups. **Output**: backups + retentionDays + hasMore; `serializeDatabaseBackupList`. Errors: `DATABASE_BACKUPS_UNSUPPORTED`. Result kind. - -### `prisma database connection list ` -- **Flags**: `--project`, `--branch`. **API**: GET /v1/databases/{id}/connections. Metadata only, no secrets; `serializeDatabaseConnectionList`. Result kind. - -### `prisma database connection create ` -- **Flags**: `--name ` (default `cli--`), `--project`, `--branch`. **API**: POST /v1/databases/{id}/connections. **Output**: renderStdout one-time connection URL + `serializeDatabaseConnectionCreate`. Errors: `DATABASE_CONNECTION_STRING_MISSING`. Result kind + sensitive stdout. - -### `prisma database connection rotate ` -- **Flags**: `--confirm ` (exact id; exit 2 otherwise). **Positionals**: `` connection **id** (no project/branch flags; provider-only auth path). **API**: POST /v1/connections/{id}/rotate. **Output**: renderStdout new one-time URL; `serializeDatabaseConnectionRotate`. Errors: `DATABASE_CONNECTION_NOT_FOUND`. Result kind + consent + sensitive stdout. - -### `prisma database connection remove ` -- **Flags**: `--confirm `. **API**: DELETE /v1/connections/{id}. `serializeDatabaseConnectionRemove`. Result kind + consent. - -### `prisma bucket list` -- **Flags**: `--project `, `--branch ` + globals. -- **Auth**: platform+login. **API**: GET /v1/buckets. -- **Output**: bucket table; `serializeBucketList`. Tests: bucket.test.ts. Result kind. - -### `prisma bucket create` -- **Flags**: `--name ` (auto-generated if omitted), `--project`, `--branch`. **API**: POST /v1/buckets. Errors: `BRANCH_NOT_FOUND`. `serializeBucketCreate`. Result kind. - -### `prisma bucket delete ` -- **Summary**: "Delete a bucket and all its access keys" (cascade documented in the confirm copy: permanently removes all objects and access keys). -- **Flags**: `--confirm ` (exact id; `CONFIRMATION_REQUIRED` exit 2). **Positionals**: `` required (id, not name). -- **API**: DELETE /v1/buckets/{bucketId}. Errors: `BUCKET_NOT_FOUND`. `serializeBucketDelete`. -- **Engine notes**: the canonical consent-grade example named in the S2 brief; exact-id flag, no prompt. - -### `prisma bucket key list ` -- **Positionals**: ``. **API**: GET /v1/buckets/{bucketId}/keys. Metadata only. `serializeBucketKeyList`. Result kind. - -### `prisma bucket key create ` -- **Summary**: "Create a bucket access key and print its one-time credentials". -- **Flags**: `--role ` (default read_write — anything not exactly `read` becomes read_write), `--name ` (auto-generated if omitted). -- **API**: POST /v1/buckets/{bucketId}/keys. **Output**: renderStdout one-time credentials (accessKeyId/secretAccessKey/endpoint/bucketName) + `serializeBucketKeyCreate`. Errors: `BUCKET_KEY_SECRET_MISSING`. Result kind + sensitive stdout. - -### `prisma bucket key delete ` -- **Positionals**: both required (USAGE_ERROR 2 when blank). **No --confirm** (revocation is not id-confirmed — inconsistent with bucket delete; note for grammar review). **API**: DELETE /v1/buckets/{bucketId}/keys/{keyId}. Errors: `BUCKET_KEY_NOT_FOUND`. `serializeBucketKeyDelete`. Result kind. - -### `prisma app build [app]` -- **Summary**: "Build the app locally into a deployable artifact". -- **Flags**: `--entry ` (Bun/auto), `--build-type ` (choices `APP_BUILD_TYPES` incl. `auto` default; auto+committed build block resolves via deploy's framework detection) + globals. -- **Positionals**: `[app]` — target key in a multi-app `prisma.compute.ts`. -- **Auth**: none (fully local) — but fixture mode refuses (`ensurePreviewAppMode`? No: app build does NOT call ensurePreviewAppMode; it is local-only and works in any mode). -- **API**: none. **Behavior**: sync local build (`executeAppBuild`). -- **Output**: artifact directory/entrypoint/buildType; `serializeAppBuild`. Errors: `BUILD_FAILED` (1), `FRAMEWORK_NOT_DETECTED`, `BUILD_SETTINGS_UNSUPPORTED`, `COMPUTE_CONFIG_*`, USAGE_ERROR for ambiguous auto detection. -- **Side effects**: writes the build artifact directory; runs framework build tooling as child processes. -- **Tests**: app-build.test.ts, app-bun-compat.test.ts, compute-config.test.ts. -- **Engine notes**: long-running local work → progress events; no credentials. - -### `prisma app run [app]` -- **Summary**: "Run your app locally". -- **Flags**: `--entry `, `--build-type ` (default auto; currently nextjs/bun have dev servers), `--port ` + globals. **Rejects `--json`** with USAGE_ERROR (exit 2) — it streams the framework dev server output directly. -- **Positionals**: `[app]` config target. -- **Auth/API**: none. **Behavior**: long-running local child process until exit/SIGINT; SIGINT → COMMAND_CANCELED (130); non-zero child exit → `RUN_FAILED` **with the child's exit code as the CLI exit code** (app.ts:349-355, runFailedError exitCode param). -- **Output**: pass-through dev-server output; on clean exit a summary (framework, entrypoint, port, command); `serializeAppRun` exists but is unreachable with --json rejected. -- **Tests**: app-local-dev.test.ts. -- **Engine notes**: closest thing to a "server" kind in the current CLI; exit-code passthrough is unique. - -### `prisma app deploy [app]` -- **Summary**: "Creates a new deployment for the app". -- **Flags**: - -| name | type | notes | -|---|---|---| -| `--app ` | string | app selector (create-if-missing semantics) | -| `--project ` | string | explicit project; mutually exclusive with --create-project and PRISMA_PROJECT_ID | -| `--create-project ` | string | create+link a Project first | -| `--branch ` | string | branch override (default: local git branch, else production) | -| `--framework ` | enum | nextjs/nuxt/astro/hono/nestjs/tanstack-start/custom/bun | -| `--entry ` | string | Bun deploys | -| `--http-port ` | string→int validated | port override | -| `--region ` | string | only for newly created apps; mismatch with an existing app's region → USAGE_ERROR | -| `--env ` | repeatable string[] | assignment or dotenv file path | -| `--db` / `--no-db` | boolean | create+wire a branch database / skip; passing both → USAGE_ERROR (checked against raw argv) | -| `--prod` | boolean | confirm intent to replace the live production deployment | -| `--no-promote` | boolean | build without promoting; skips the production confirmation entirely | - -- **Positionals**: `[app]` config target; with a multi-app config and no target, deploys **all** targets sequentially (deploy-all mode) and rejects per-app inputs (`--app/--framework/--entry/--http-port/--region/--env`, `PRISMA_APP_ID`) with USAGE_ERROR. -- **Auth**: platform via `requireComputeAuth` (no interactive login). Env overrides: `PRISMA_PROJECT_ID` (skips/never writes the local pin), `PRISMA_APP_ID`. -- **API calls**: ComputeClient `deployApp` (upload/build/deploy/promote with progress callbacks); POST /v1/projects (--create-project); GET/POST /v1/projects/{id}/branches (branch resolve/create); GET /v1/apps (selection); `--db`: GET/POST /v1/environment-variables + POST /v1/databases (+ DELETE on rollback of a failed setup). -- **Behavior**: long-running with step progress; interactive on first deploy (customize-settings confirm → framework select + port text), ambiguous app name select prompt, `--db` confirm prompt when a Prisma schema signal is found, production-deploy confirmation prompt, agent-setup prompt. Production rules (`enforceProductionDeployGate`): second-and-later production deploys need `--prod` (`PROD_DEPLOY_REQUIRES_FLAG` exit 2), plus `--yes` or an interactive confirm; cancel exits 0. -- **Output**: workspace/project/branch/app/deployment/deploySettings/durationMs; deploy-all wraps per-target results; `serializeAppDeploy` / `serializeAppDeployAll`. Errors: `DEPLOY_FAILED`, `BUILD_FAILED` (build-phase aware, Next standalone-output hint with edit-file nextAction), `APP_AMBIGUOUS` (2), `PROJECT_SETUP_REQUIRED`, `LOCAL_STATE_STALE`, `BRANCH_DATABASE_SETUP_FAILED`, `BUILD_SETTINGS_MIGRATION_REQUIRED`, `COMPUTE_CONFIG_*`, `FRAMEWORK_NOT_DETECTED`; deploy-all failures are re-wrapped with completed/not-attempted context in meta.deployAll. -- **Side effects**: may write `.prisma/local.json` (+ `.gitignore`); writes selected-app + known-live-deployment into state.json; uploads code; may create project/branch/database/env vars; runs local build child processes. -- **Tests**: app.test.ts, app-controller.test.ts, deploy-plan.test.ts, production-deploy-gate.test.ts, app-branch-database.test.ts, app-provider.test.ts, app-state.test.ts, app-env-vars.test.ts. -- **Engine notes**: the flagship session command: step/progress/status events, multiple consent points (--prod, --db, customize), env-var credential injection, and a deploy-all composite. The progress callbacks (`createDeployProgress`) are the natural source of engine progress events. - -### `prisma app show [app]` -- **Flags**: `--app `, `--project ` + globals. **Positionals**: `[app]` config target. -- **Auth**: platform. **API**: GET /v1/apps, ComputeClient.listDeployments. -- **Behavior**: sync; may select-prompt when several apps and no saved selection (non-interactive → USAGE_ERROR "App selection required"). Live deployment resolved via provider liveDeploymentId, falling back to the locally cached known-live id (a72f34a fix: never assumes newest is live). -- **Output**: app, liveDeployment, liveUrl, 5 recent deployments; `serializeAppShow`. Null app (none deployed) is a success with nextStep deploy. -- **Side effects**: caches selected app in state.json. -- **Tests**: app.test.ts, app-controller.test.ts, app-presenter.test.ts, app-state.test.ts. -- **Engine notes**: result kind (+ optional picker). - -### `prisma app open [app]` -- **Flags**: `--app`, `--project`. **Auth**: platform. **API**: GET /v1/apps + listDeployments. -- **Behavior**: sync + browser-opening: opens the live URL with `open` only when `canPrompt`; otherwise reports `opened: false` and prints the URL. -- **Output**: url + opened flag; `serializeAppOpen`. Errors: `NO_DEPLOYMENTS` (1), `FEATURE_UNAVAILABLE` when no live URL. -- **Engine notes**: result + local browser action; the engine needs a "open URL on the client" effect. - -### `prisma app domain add [app]` -- **Flags** (shared domain target set): `--app `, `--project `, `--branch ` + globals. -- **Positionals**: `` (normalized/validated → `DOMAIN_HOSTNAME_INVALID` 2), `[app]` config target. -- **Auth**: platform. Custom domains restricted to the production branch: non-production `--branch` → `BRANCH_NOT_DEPLOYABLE` (2). Env overrides PRISMA_PROJECT_ID/PRISMA_APP_ID honored. -- **API**: POST /v1/apps/{appId}/domains. -- **Output**: domain summary (status, dns records, certificate) + `existing` flag (idempotent re-add); `serializeAppDomainAdd`; nextSteps wait/show. Errors: `DOMAIN_ALREADY_REGISTERED` (registered to another app), `DOMAIN_QUOTA_EXCEEDED`, `DOMAIN_DNS_NOT_CONFIGURED`, `NO_DEPLOYMENTS`, `DEPLOY_FAILED` fallback. -- **Tests**: app.test.ts / app-controller.test.ts / app-provider.test.ts (domain sections). -- **Engine notes**: result kind. - -### `prisma app domain show [app]` -- Same target flags. **API**: list domains → GET /v1/domains/{domainId}. Output `serializeAppDomainShow`; `DOMAIN_NOT_FOUND` (1). Result kind. - -### `prisma app domain remove [app]` -- Same target flags. Confirmation: `--yes` skips; interactive confirm "Detach from App …?" (default No); non-interactive without --yes → `CONFIRMATION_REQUIRED` **exit 1**; declining → USAGE_ERROR "Custom domain removal canceled" (2). **API**: DELETE /v1/domains/{domainId}. `serializeAppDomainRemove`. Consent-grade (yes/no, not exact-id). - -### `prisma app domain retry [app]` -- Same target flags. **API**: POST /v1/domains/{domainId}/retry. Errors: `DOMAIN_RETRY_NOT_ELIGIBLE`. `serializeAppDomainRetry`. Result kind. - -### `prisma app domain wait [app]` -- **Flags**: target set + `--timeout ` (default "15m"; `0` = single check then timeout error). -- **Behavior**: **polling stream** via `runStreamingCommand`: emits a status line/JSON event on every status change (poll interval `PRISMA_CLI_DOMAIN_WAIT_POLL_MS`), GET /v1/domains/{id} each cycle. Terminal states: `active` (success, prints live URL), `failed` → `DOMAIN_VERIFICATION_FAILED` (1), deadline → `DOMAIN_VERIFICATION_TIMEOUT` (1). -- **Output**: status events; no result envelope beyond the streaming success event. -- **Engine notes**: canonical poll→status-events mapping case for the engine. - -### `prisma app logs [app]` -- **Flags**: `--app `, `--project `, `--deployment ` + globals. **Positionals**: `[app]` config target. -- **Auth**: platform (log stream re-authenticates via `createPreviewLogAuthOptions` — service token or stored access token directly). -- **API**: deployment resolution (listApps/listDeployments/showDeployment) then `ComputeClient.streamDeploymentLogs`. -- **Behavior**: stream; without `--deployment` streams the live deployment (NO_DEPLOYMENTS when none). JSON mode: per-record events + wrapper success event. -- **Output**: log text to stdout; header block to stderr. Errors: `DEPLOYMENT_NOT_FOUND` (three variants: unknown id / detached app / foreign project), `NO_DEPLOYMENTS`, `DEPLOY_FAILED`. -- **Tests**: app.test.ts, app-controller.test.ts. -- **Engine notes**: stream kind. - -### `prisma app list-deploys [app]` -- **Flags**: `--app`, `--project`. **Auth**: platform. **API**: GET /v1/apps + listDeployments. -- **Output**: deployments newest-first with live hint; null app = success; `serializeAppListDeploys`. Side effect: caches selected app. Result kind (+ optional picker). - -### `prisma app show-deploy ` -- **Positionals**: `` id required. **Flags**: globals only. -- **Auth**: platform. **API**: ComputeClient.showDeployment. No project resolution — the id is global. -- **Output**: deployment detail with corrected `live` flag (provider live id > cached known-live > record flag); `serializeAppShowDeploy`. Error: `DEPLOYMENT_NOT_FOUND` (1). Result kind. - -### `prisma app promote [app]` -- **Summary**: "Promote a deployment to production by rebuilding with production env vars". -- **Flags**: `--app`, `--project`. **Positionals**: `` required, `[app]` config target. -- **Auth**: platform. **API**: listApps, listDeployments, ComputeClient.promoteDeployment (with progress rendering). -- **Behavior**: remote operation with progress; already-live target short-circuits with a warning instead of an error. -- **Output**: promoted deployment (status running, live true); `serializeAppPromote`. Errors: `DEPLOYMENT_NOT_FOUND`, USAGE_ERROR "App promote requires an existing app", `DEPLOY_FAILED`. -- **Side effects**: caches selected app + known live deployment. -- **Engine notes**: session (progress events); note the local known-live cache is part of correctness for later `show`/`rollback`. - -### `prisma app rollback [app]` -- **Summary**: "Roll back production to a previous deployment". -- **Flags**: `--app`, `--project`, `--to ` (explicit target; default = deployment immediately before the current live one). -- **Auth**: platform. **API**: same promote machinery (rollback = promote of an older deployment). -- **Output**: new live deployment + previousLiveDeploymentId; `serializeAppRollback`. Errors: `NO_PREVIOUS_DEPLOYMENT` (1), `DEPLOYMENT_NOT_FOUND`, `DEPLOY_FAILED`. -- **Engine notes**: session (progress); no confirmation prompt at all today (worth flagging: destructive-ish but unconfirmed). - -### `prisma app remove [app]` -- **Summary**: "Remove the app from the resolved branch". -- **Flags**: `--app `, `--project `, `--branch ` (scopes teardown; empty string rejected with USAGE_ERROR so it cannot silently fall back to production — commit 484c60a) + globals (`--yes` is the documented confirm). -- **Positionals**: `[app]` config target. -- **Auth**: platform. **API**: ComputeClient.showApp + destroyApp (SDK polls status, 2s interval, 120s timeout). -- **Behavior**: destructive with **type-the-app-name** confirmation prompt on TTY; `--yes` skips; non-interactive without --yes → `CONFIRMATION_REQUIRED` **exit 1**. -- **Output**: removed app; `serializeAppRemove`; warnings if local state cleanup fails. Errors: `REMOVE_FAILED` (1), USAGE_ERROR "App remove requires an existing app". -- **Side effects**: clears selected-app and known-live-deployment from state.json. -- **Engine notes**: consent (typed-name — strongest grade in the CLI) + SDK-internal polling → progress events. - ---- - -## 5. Current tests census - -Fixture-mode counts are references to `fixturePath` per file (see command sections for the mapping): - -- Heavy fixture users (fixture-mode CLI-level tests): project.test.ts (47), database.test.ts (40), init.test.ts (35), auth.test.ts (26), bucket.test.ts (25), app.test.ts (23), project-mutations.test.ts (16), shell.test.ts (15), update-check.test.ts (12), auth-real-mode.test.ts (6), branch.test.ts (5), project-controller.test.ts (5), version.test.ts (3), auth-controller.test.ts (2). -- Real-mode / unit tests (no fixture): app-controller, app-build, app-bun-compat, app-branch-database, app-local-dev, app-presenter, app-provider, app-state, app-env*, auth-login, auth-ops, auth-usecases, branch-controller, branch-usecases, command-runner(+auth), compute-config, database-plan-limit, deploy-plan, feedback, git-adapter, init-agent-setup, local-branch, output, production-deploy-gate, project-real-mode, project-resolution, project-usecases, prompt, read-branch, resolve-package-version, token-storage, v8-bin, v8-whoami. -- **Commands with no direct test coverage**: `build logs` (nothing references it), `agent update` (only via shared install path in agent.test.ts), `database usage`/`backup list`/`restore` real-mode paths are covered only through database.test.ts fixtures + provider unit tests. - -## 6. Renames and grammar (v8) - -- **app → service**: per the ruled grammar the deployable unit is **Service**. Affected surface: the entire `app` group (16 leaf commands), the `--app` flag on 9 commands, the `[app]` config-target positional on 14 commands, `PRISMA_APP_ID`, result fields `app{id,name}`, state-store keys (`setSelectedApp`), error copy ("App remove requires an existing app"), and `prisma.compute.ts`'s `app:` block (SDK-owned; rename coordination needed with @prisma/compute-sdk). `app build`/`app run` are local-dev verbs that may belong under the service noun or a dev namespace — flag for the spec author. -- **`project` stays platform-owned** (ruled 2026-08-10). The composer work parks under a separate `composer` root in S3; nothing in the current tree collides with that name. -- **`database` vs `postgres`**: the S2 brief says "database/postgres", but the current shell has **no `postgres` command or alias** — only `database`, described as "Manage Prisma Postgres databases". If the v8 grammar wants `postgres` as the resource noun, that is a pure rename (no alias exists to preserve). -- Grammar conflicts / irregularities in the current tree: - - `app list-deploys` / `app show-deploy` break the ` ` shape with hyphenated compound verbs; a Deployment resource noun (`deployment list/show`) would be regular. - - `build` is a resource group (git builds) whose only verb is `logs`, while `app build` is a verb — same word, two meanings. The spec already plans `build list`/`build show`; the rename should disambiguate Service build (local) from platform Build (resource). - - Deletion verbs are split: `remove` (project, database, app, env, connection) vs `delete` (bucket, bucket key). Confirmation styles are also split three ways: exact-id `--confirm` flag (project/database/bucket), `--yes`/interactive confirm (domain remove, prod deploy), typed-name prompt (app remove). The engine's consent grades should normalize these. - - `auth logout --workspace X` duplicating `auth workspace logout X` is a compat shim worth collapsing. - - `project env` is the env surface (moved off `app`); the S2 brief's "app (incl. env…)" reflects the old layout — env controllers still live in files named `app-env*.ts` and types in `types/app-env.ts` even though the commands are `project env *`. - - Group descriptor `branch` says "View your Platform branches" — read-only group with one verb; fine, but the deploy path creates branches implicitly (POST branches), which the grammar should own explicitly. - ---- - -**2026-08-21 note (PM review, command grammar cleanup):** this inventory is a historical grounding document for the S2 ports. The mounted tree has since changed: top-level `init` is removed, the six destroying `remove` commands are `delete`, `postgres restore` is `postgres backup restore`, `migrate`/`format`/`ref *` are `db migrate`/`contract format`/`migration ref *`, and composer's `deploy` and `dev` are root commands. The regenerated tree lives in [`../command-review.md`](../command-review.md). diff --git a/.drive/projects/prisma-cli-v8/assets/s2/parity-divergences-s3.md b/.drive/projects/prisma-cli-v8/assets/s2/parity-divergences-s3.md deleted file mode 100644 index eef1e647..00000000 --- a/.drive/projects/prisma-cli-v8/assets/s2/parity-divergences-s3.md +++ /dev/null @@ -1,278 +0,0 @@ -# S3 parity divergences — the composer family - -Every known place where the composer family as it now ships differs -from the `prisma-composer` CLI it replaces. Same entry format as -`parity-divergences.md`, and the same standing ruling behind it (S2 -ruling 10: divergences are enumerated, not discovered). - -**The baseline here is not `prisma-cli`.** The other files in this -directory compare a ported command against the shipping platform CLI. -Composer's four commands have no platform predecessor: what changes is -what a `prisma-composer` user sees, so every entry below is written -against composer's own CLI as inventoried in -[`../s3/composer-inventory.md`](../s3/composer-inventory.md). - -Two engine-global records apply wholesale and are not repeated per -command: the S1 whoami-scoped record -([`../engine/whoami-parity-divergences.md`](../engine/whoami-parity-divergences.md)) -for json framing, format auto-selection, channel discipline, rendering -style and the shared flag family; and this directory's `parity-divergences.md` -preamble. Composer's users have seen none of it before, so the engine's -shared flags (`--format`, `--log-level`, `--verbose`, `--quiet`, `--yes`, -`--confirm`, `--interactive`, `--color`, `--config`) are all new on these -four commands, and so is the fact that human output is chosen only when -stdout is a TTY. - -## The invocation - -`prisma-composer ` becomes `prisma composer `: the -family mounts under a `composer` root in the prisma bin (S2 standing -ruling 1; TML-3189 holds the final grammar). Composer's own thin CLI -survives and keeps the unprefixed spellings, so both invocations exist -and run the same handlers in the same way — the prefix is the whole -difference. - -**Help examples name the wrong invocation under the prisma bin.** -Composer writes its examples as `{bin} deploy src/service.ts`, which is -right for `prisma-composer` and wrong for `prisma`, where the command is -`prisma composer deploy`. The engine's `resolveExample` -(`packages/cli-engine/src/execution/stricli-adapter.ts`) substitutes -`{bin}` with the CLI name and nothing else (operator ruling, 2026-08-09: -examples never contain the binary name), and composer cannot know where -a host mounted it. So `prisma composer deploy --help` currently shows -`prisma deploy src/service.ts` in its Examples block — an invocation the -bin answers to with `CLI.UNKNOWN_COMMAND`, since there is no top-level -`deploy`. All eight examples are wrong the same way: two on each of the -four commands, verified in `@prisma/composer@0.6.0-dev.16`'s -`dist/family.mjs`. Nothing else in the help is wrong, and the same -defect is unreachable from composer's own CLI. Fixing it needs a -mount-aware placeholder in the engine (`{command}` substituted with the -command's mounted path) and composer rewriting those eight strings to -use it — a coordinated change across both repos, recorded in -`deferred.md`. - -## `deploy --production` is dropped - -Legacy declared `--production` on the shared abstract command class, so -clipanion listed it in `deploy --help`, and passing it always failed -with `DEPLOY.FLAG_INVALID` (exit 2) — the flag was accepted by the -parser and rejected by the command (inventory D1). The v8 `deploy` -declares only the flags it honours, so `--production` is gone from its -help and `prisma composer deploy --production ` settles -`CLI.INVALID_ARGUMENTS` ("No flag registered for --production"), exit 2. - -The exit code does not move; the error code and message do, and the -help no longer advertises a flag that cannot work. `destroy` keeps -`--production`, where it is genuinely valid. - -## The reproduce hint is a next action, and the failure envelope is gone - -When the alchemy child failed, legacy printed the `DEPLOY.ENGINE_FAILED` -envelope and then two bare `console.error` lines to stderr -(`render-error.ts:27-37`): - -```text -Generated stack file: -Run `` from to reproduce this directly. -``` - -The v8 handler settles through `exitWithChildStatus({ nextActions })` -instead. Three things change for the user: - -- **The hint is a typed `run-command` next action**, rendered in the - engine's next-action style: label "Run the converge directly from - `` to reproduce this", the command as its `command`, and - "Generated stack file: ``" as its `reason`. Same three facts, - now machine-readable. -- **No failure envelope is printed.** The child owned the terminal and - already said what went wrong; the run exits with the child's status - verbatim and prints only the next action. `DEPLOY.ENGINE_FAILED` is - therefore no longer a code a user sees on this path — it survives - only for a failure that never reached a child. -- **The hint is dropped when a signal killed the child.** The user - pressed Ctrl-C: there is nothing to reproduce, so the run settles - 128 + the signal number with no envelope and no actions, whatever the - handler asked for. Legacy collapsed a signalled child's status - (`run-alchemy.ts:61`) and printed the hint anyway. - -## `deploy`, `destroy`, `dev`, and `log` support structured output - -Amended 2026-08-14: `maySpawn` no longer disables JSON. In human mode the -child still inherits the terminal. In JSON mode its stdout and stderr are -routed to diagnostic stderr while the engine retains framed NDJSON stdout and -emits the command family's terminal result. A failed child keeps its verbatim -process exit code and emits `CLI.CHILD_PROCESS_FAILED` with the child status in -`error.meta`. - -This also restores normal format auto-selection: a piped -`prisma composer deploy ` produces structured output, including -Composer's deployment summary, without requiring an explicit flag. - -## Usage, parse errors and bare invocation - -- **A parse failure now says what was wrong.** Any clipanion parse error - — unknown flag, missing ``, a dangling `--name` — was replaced - by the full detailed usage text with the reason discarded (inventory - D6). The engine reports the specific failure: `CLI.INVALID_ARGUMENTS` - naming the flag and the value it could not read, or - `CLI.UNKNOWN_COMMAND` with a suggestion (`prisma composer nope` → - "No command registered for `nope`, did you mean `log`?"). Both exit 2, - as the usage wall did. -- **A bare group invocation exits 0, not 2.** `prisma-composer` with no - arguments printed usage to stderr and exited **2** (inventory D7). - `prisma composer` prints the group's usage and exits **0**. A script - that used the exit code to tell "no arguments" from "ran successfully" - can no longer. -- **Help output shape.** The engine's `USAGE` / `FLAGS` / `ARGUMENTS` - blocks replace clipanion's, every command gains the shared flag family - in its usage line, and the group gains a `COMMANDS` list. `--help` - still exits 0. Which stream it lands on is now the engine's global - format rule rather than a constant: human format writes help to - stdout, json format to stderr, and format is auto-selected from - whether stdout is a TTY — so a piped `--help` writes to stderr, where - legacy always wrote to stdout. - -## `--tail` becomes a typed number flag, and its validation widens - -Legacy took `--tail` as a string and ran `Number.parseInt(value, 10)` -over it, so `--tail 5abc` silently became `5` (inventory D5), and a -hand-written check rejected `NaN` and negatives with a `UsageError` -("`--tail` must be a non-negative integer."), exit 2. - -The v8 `log` declares `flag.number`, which changes the answer in both -directions: - -| input | legacy | v8 | -| --- | --- | --- | -| `--tail 5abc` | silently `5` | `CLI.INVALID_ARGUMENTS`, exit 2, naming the value | -| `--tail abc` | usage error, exit 2 | `CLI.INVALID_ARGUMENTS`, exit 2 | -| `--tail 1.5` | silently `1` | **accepted** as `1.5` | -| `--tail -1` | usage error, exit 2 | **accepted** as `-1` | - -The first two rows are the fix the port was for. The last two are a -real widening: the engine's number flag validates only that the value is -a number, so "non-negative integer" is enforced nowhere, and a negative -or fractional tail reaches the attachment's `logs(signal, { tail })` for -it to interpret. Recorded here rather than silently narrowed, per the -deferred item this closes. One correction to that item while closing it: -it said legacy rejected negatives *and* non-integers, and legacy -rejected only negatives — `parseInt` truncated `1.5` to `1` and the -check saw nothing wrong. If the constraint is wanted back it belongs on -the flag (a validated number flag in the engine), not in the handler. - -## `[dev]` and `[log]` prefixes become engine events - -Legacy wrote its own notices with a literal source prefix: `[dev] -converge failed — …`, `[dev] stopped.`, `[log] stream failed: …`, -`[log] falling behind — dropped the N oldest lines.` — all -`console.error` straight to stderr, unconditionally. - -Those become engine events (`message` at warn/error, `status`, -`endpoint`), which means they are rendered by the engine, filtered by -`--log-level`, hidden by `--quiet`, and framed in json for `log`. The -`[dev]` / `[log]` prefixes are gone: the engine's own rendering -identifies what it is showing. - -What does **not** change: a log line still reads `[] ` -and still goes to stdout, because the service prefix is part of the line -the handler emits rather than a rendering decision. And the empty case -still exits 0 — no running services is a warn event and a clean -shutdown, as legacy's single stderr line and exit 0 were. - -## `CONFIG.PATH_MISMATCH` retires for the explicit-path case only - -Legacy loaded the config through c12 and then compared the file c12 -reported against the file its own discovery walk had found; a mismatch -failed with `CONFIG.PATH_MISMATCH` — "Refusing to deploy against a -different file." (`load-config.ts:156-169`). - -With the engine's `composer` section in `prisma.config.ts`, an explicit -`configPath` wins and the walk is skipped, so there is no second opinion -to disagree with and the check cannot fire. In its place a path that -does not exist is `CONFIG.FILE_MISSING`, naming the section's path and -saying why there is no fallback ("An explicit configPath is used as -given — there is no walk to fall back on"). - -The common case is unchanged: with no `composer` section, composer's -entry-anchored walk runs exactly as before and `CONFIG.PATH_MISMATCH` -survives with it. The code is retired for one branch, not deleted. - -## `dev` settles 130 on Ctrl-C, where legacy exited 0 - -Legacy's watch loop treated Ctrl-C as a clean shutdown and returned 0 -(`run-dev.ts:132`), so `prisma-composer dev` ended a successful session -and an interrupted one identically. The handler still cleans up and -still returns success; the ENGINE now settles the run at 128 + the -signal number from its own record of the signal — 130 for SIGINT, 143 -for SIGTERM — because a run a delivered signal ended is an abort -whatever the handler concluded (operator ruling, 2026-08-11). - -This is the divergence most likely to be noticed: a wrapper script, -Makefile or supervisor that runs `dev` in the foreground and treats a -non-zero exit as failure now reports failure every time a developer -stops the session. `dev` itself documents no exit code of its own, and -a converge failure before the session is live still exits with the -child's status. - -`log` is unaffected in principle but changes the same way in practice: -it also returned 0 on Ctrl-C and now settles 130 through the same rule. - -## Exit-code unifications on the engine-side error paths - -Legacy's top-level mapping was: a number returned by `run()` passes -through; a clipanion `UsageError` prints its raw message to stderr and -exits 2; a `CliStructuredError` renders its envelope and exits 2; -anything else prints `Error: …` plus "this is a bug, please report it" -naming composer's issue tracker, and exits 1. - -What the engine does with each: - -| legacy path | legacy | v8 | -| --- | --- | --- | -| structured composer error | rendered envelope, exit 2 | engine envelope, exit 2 — the code and `meta` survive, and the `fix` prose becomes one `user-choice` next action (untranslated it would be dropped silently: the engine has no `fix` field) | -| clipanion `UsageError` | raw message, exit 2 | `CLI.INVALID_ARGUMENTS` / `CLI.UNKNOWN_COMMAND`, exit 2 | -| unexpected throw | `Error: …` + report-a-bug line naming composer's issues, exit 1 | engine `CLI.INTERNAL_ERROR`, exit 1 — same code, engine envelope, and no composer issue URL | -| alchemy child failed | child's status verbatim, plus an envelope and two hint lines | child's status verbatim, no envelope, hint as a next action (above) | -| alchemy child signalled | collapsed status | 128 + signal, no envelope, no hint (above) | - -Three refusals are new, and all three move work earlier in the run: - -- **Unauthenticated `deploy` / `destroy` fail before anything happens.** - They declare `needs.credentials`, so a signed-out run settles - `CLI.CREDENTIALS_REQUIRED` (exit 2) before the config is read, let - alone before a container is ensured. Legacy never checked: the - credential env vars were read by provider code inside the alchemy - child, so an unauthenticated deploy failed deep in the child, after - the platform work the parent had already done. -- **A session close to expiry is refused up front.** `deploy` and - `destroy` refuse before the in-process leg when the credential expires - within the threshold, rather than creating platform resources and - then failing. New; legacy had no expiry concept. -- **The effect-resolution preflight no longer takes out every command.** - Legacy ran `checkEffectResolution` at import time in `bin.ts`, before - anything else, and exited 2 on a mismatched `effect` — including for - `--help`. It now runs inside config loading and surfaces as the - `DEPS.EFFECT_VERSION_CONFLICT` diagnostic, so commands that need no - config still work on a broken dependency tree, and `prisma --help`, - `prisma composer --help` and every platform command are unaffected by - composer's dependency resolution. Where it moved to, in the published - `0.6.0-dev.16`: `configSource`, the front that the throwing loader and - the diagnostics-returning loader both go through, so every config load - runs it first whichever shape called. (That the diagnostics-returning - loader itself is still uncalled — the `deferred.md` item — does not - reach this check.) A failed executor import diagnoses the same - condition a second time, turning the load failure into - `DEPS.EFFECT_VERSION_CONFLICT` rather than `DEPS.EXECUTOR_UNLOADABLE`. - -## Not a divergence, recorded because it looks like one - -`destroy` still asks nothing before tearing down. The front door cannot -know what the child will destroy, so no engine-side confirmation was -added; the guard remains the required explicit `--stage` / -`--production` target, exactly as legacy. If destroy deserves a -confirmation it is a composer product decision, raised upstream rather -than built at the mount. - -## Command grammar cleanup (2026-08-21 PM review) - -The `composer` group is dissolved: `composer deploy` and `composer dev` mount at the root as `deploy` and `dev`, and `composer destroy` and `composer log` are dropped from the mounted tree entirely. The sections above describe the family as S3 mounted it. diff --git a/.drive/projects/prisma-cli-v8/assets/s2/parity-divergences-s7.md b/.drive/projects/prisma-cli-v8/assets/s2/parity-divergences-s7.md deleted file mode 100644 index ab55f876..00000000 --- a/.drive/projects/prisma-cli-v8/assets/s2/parity-divergences-s7.md +++ /dev/null @@ -1,48 +0,0 @@ -# S7 parity divergences — mounting the ORM family - -**No user-visible divergence from any shipping CLI is introduced by -S7.** This file exists because S2 standing ruling 10 requires -divergences to be enumerated rather than discovered, and because a -slice that changes the command tree has to say so explicitly when the -answer is "none". - -Why the answer is none: S7 mounts commands that no binary this repo -ships could reach before. `prisma migration list`, `prisma db verify`, -`prisma init` and the rest of the ORM family answer for the first time. -Adding an invocation that previously did not exist changes nothing a -user already relied on. - -The divergences between the ORM commands as they run under this shell -and as they run under `prisma-next` — the engine's shared flags, json -framing, channel discipline, the `{bin}` substitution in help examples, -and everything else the port changed — belong to S5, which owns that -record and keeps it in prisma/prisma alongside the port. S7 mounts the -family; it does not change what the family does. - -Nothing already shipped changes behaviour: the platform and composer -commands keep their paths, flags and output, no group brief was -reworded, and no existing invocation was retired or moved. The ORM -family's own redirect table (`migration apply`, `migration ref`, and -four retired `migration status` flags) arrives with the family, so it -describes invocations of `prisma-next` that were already retired there, -not invocations this shell used to answer. - -## One operational fact, not a divergence - -`@prisma/orm-toolchain`'s `./cli` entry statically imports `esbuild` -and `arktype` (and eight `@prisma/orm-framework` subpaths), so every -invocation of this bin now pays that import — including `prisma ---version`, which touches no ORM code. Composer's family avoids this by -keeping its heavy graph behind dynamic executor imports; the ORM family -does not do the same yet. - -This costs startup time, not correctness, and no user-visible output -changes because of it. Fixing it means moving orm-toolchain's handler -imports behind dynamic imports, which is prisma/prisma's change to -make, not this repo's. Mirrored in -[`../../deferred.md`](../../deferred.md) under "Upstream, not ours to -land". - -## Command grammar cleanup (2026-08-21 PM review) - -The ORM mounts moved: `migrate` → `db migrate`, `format` → `contract format`, `ref list|set|delete` → `migration ref list|set|delete`. The shipped `migration ref` redirect is dropped at the mount (the spelling is live again) and `migration apply`'s replacement is respelled to `{bin} db migrate --to `. Command behaviour is unchanged. diff --git a/.drive/projects/prisma-cli-v8/assets/s2/parity-divergences-s8.md b/.drive/projects/prisma-cli-v8/assets/s2/parity-divergences-s8.md deleted file mode 100644 index afa70a40..00000000 --- a/.drive/projects/prisma-cli-v8/assets/s2/parity-divergences-s8.md +++ /dev/null @@ -1,219 +0,0 @@ -# S8 parity divergences — the service family's deployment grammar - -Every known place where the `service` family as it now ships differs -from what S2c left behind. Same entry format as -[`parity-divergences.md`](parity-divergences.md), and the same standing -ruling behind it (S2 ruling 10: divergences are enumerated, not -discovered). - -**The baseline here is v8's own `service` family, not the legacy -platform CLI.** The other files in this directory compare a ported -command against the shipping `prisma-cli`. S8 changes commands that S2c -already ported, so what changes is what a v8 user sees between S2c and -S8. The legacy `app` family is untouched by this slice and keeps its own -spellings. - -Two engine-global records apply wholesale and are not repeated per -command: the S1 whoami-scoped record -([`../engine/whoami-parity-divergences.md`](../engine/whoami-parity-divergences.md)) -and this directory's `parity-divergences.md` preamble. - -## Four commands move under `service deployment` - -The four deployment verbs leave the `service` root for a `deployment` -subgroup. The old spellings are **deleted, with no aliases** (R-S8-1; -ruled because v8 is pre-rc and carries no compatibility debt). - -| was | is | -| --- | --- | -| `service list-deploys` | `service deployment list` | -| `service show-deploy` | `service deployment show` | -| `service promote` | `service deployment promote` | -| `service rollback` | `service deployment rollback` | - -Everything that names them moved together: mount paths, help summaries -and examples, presenter copy, and the `run-command` next actions other -commands emit. The group gained its own brief, "Manage deployments for a -service", next to `service domain`. - -**The command ids in the json envelope moved with the paths**, which is -the part a script notices: - -| was | is | -| --- | --- | -| `service.list-deploys` | `service.deployment.list` | -| `service.show-deploy` | `service.deployment.show` | -| `service.promote` | `service.deployment.promote` | -| `service.rollback` | `service.deployment.rollback` | - -A caller invoking an old spelling gets `CLI.UNKNOWN_COMMAND` (exit 2). -`prisma-cli service promote dep_1` no longer runs anything, and the -suggestion machinery cannot help: `promote` is not a `service` verb any -more, so the near-miss is `service deployment promote`, two tokens away. - -## Five commands are new - -Net-new surface, so nothing about them is a divergence from a previous -spelling — recorded here so the file describes the whole shipped -grammar: - -| command | endpoint | -| --- | --- | -| `service list` | `GET /v1/apps` | -| `service create` | `POST /v1/apps` | -| `service deployment start` | `POST /v1/deployments/{id}/start` | -| `service deployment stop` | `POST /v1/deployments/{id}/stop` | -| `service deployment delete` | `DELETE /v1/deployments/{id}` | - -`service create` is the one that changes what is possible rather than -what is spelled: before it, a service could only come into existence as -a side effect of deploying to it. - -## `live` derives from the platform record alone - -The local CLI cache of "which deployment is live" is **retired in both -directions**. It was read as a fallback when the service record named no -live deployment (`readKnownLiveDeployment`), and it was written by -`promote` and `rollback` after a successful switch. Both are gone: `live` -is now `service.latestDeploymentId` and nothing else. - -The store helpers survive because the legacy `app` family still writes -the same key for the same project, and `service remove` still clears it -for that reason. Nothing in the v8 `service` family reads or writes it. - -Two user-visible consequences: - -- **Where a local cache entry existed, `service deployment list`'s - rows change from `true`/`false` to `null`.** Only machines whose - cache held an entry for the service see this: the old resolver fell - through to the cache when the record named no live deployment (the - rows the provider maps are always `live: null`, so the middle - branch never fired), and a cache hit made one row `true` and the - rest `false`. Without a cache entry the rows were already `null`, - byte-identical to today. The human table renders all of it the same - (an empty cell), so this is a **json-only change**, and `null` is - deliberate: the platform says nothing about which deployment is - live, and `null` says "unknown" where `false` claimed "not live". -- **A stale cache can no longer make a deployment look live.** On a - machine that promoted before the change, the old code could report a - deployment live after the platform had moved on. It cannot now. - -## `service show` suppresses `liveUrl` more narrowly than the contract says - -R-S8-3 asks for the live url whenever `latestDeploymentId` is set. -`service show` is stricter: it presents `liveUrl` only when the named -live deployment **also appears in the deployment listing it just -fetched**, because it derives the live deployment by finding that id -among the listed rows. - -If the platform ever names a `latestDeploymentId` the listing omits, the -service shows `live url: unavailable` rather than the promoted address. -No case was found where the listing omits it — the listing is the same -service's deployments — so this is recorded as a narrower implementation -of the rule, not a known defect. - -## `service deployment show`'s url follows liveness - -Previously every deployment reported the service's promoted -`appEndpointDomain`. That address serves only the live deployment, so -two things were wrong: a non-live deployment showed a url that does not -reach it, and a service that had never been promoted showed a -placeholder domain that resolves to nothing. - -Now the url is the promoted address **only when the shown deployment is -the one the service names as latest**, and the deployment's own -`previewDomain` otherwise. - -`service deployment list` is unchanged and still reports per-row preview -domains: identical promoted urls on every row would be wrong, and no -presenter renders that field. - -## A service with no live deployment presents no live url anywhere - -A service carries an `appEndpointDomain` from the moment it is created, -before anything is deployed to it, and that domain does not resolve -until the first promote. Every presenter added or corrected in this -slice reports a live url only when `latestDeploymentId` is set — -`service show`, `service list`, and `service create` all show -`not deployed` (or `unavailable`) rather than a dead address. - -## `service create --branch` resolves *or creates* the named branch - -The create body needs a branch id, so the command resolves the branch by -git name and **creates it when it does not exist**. A mistyped -`--branch` therefore silently creates a branch rather than failing. - -This is inherited from the provider path the contract grounds the -command in (`resolveOrCreateBranch`, the same helper the deploy flow -uses), and is recorded as a divergence note rather than treated as a -defect: refusing an unknown branch would diverge from how every other -branch-scoped write in this codebase behaves. If it should refuse, that -is a change to the shared helper and affects deploy too. - -## `service create` returns an existing service instead of failing - -When the name is already taken on the branch the API answers 409, and -the command reports the **existing** service with `existing: true` -rather than erroring. This follows `service domain add` in the same -group, which does the same thing for the same reason, and the result -field plus the summary line ("… already exists on main; showing it") -tell the two cases apart. - -**Operator ruling pending on the semantics.** The contract does not rule -on whether `create` should be idempotent. The alternative — a hard -`SERVICE.ALREADY_EXISTS` failure — is a small change in -`createComputeService` and two tests. Recorded so the choice is visible -rather than absorbed. - -## Deleting the live deployment leaves a non-resolving `endpointDomain` - -The API permits deleting the deployment a service currently points at. -Server-side it detaches the endpoint, stops the VM, deletes it, and -clears the service's `latestDeploymentId` in the same transaction — but -it does **not** clear `appEndpointDomain`. The service is left carrying a -domain that no longer resolves. - -No CLI-side guard was added, per the contract: the only 409 documented -on that endpoint is the stop precondition, and promotion is never -consulted. The dead domain is already neutralised at the presenter -layer — with `latestDeploymentId` cleared, every presenter in this slice -reports no live url, which is the same rule the never-promoted case -uses. - -Verified against the control plane's own source and integration tests, -not against a live API; the checkout read was one minor ahead of the SDK -version this CLI pins (1.56.0 against 1.55.0), with the delete path -unchanged in shape across it. - -## `stop` takes production offline with no consent; `delete` demands a token - -Stated because the new grammar puts the verbs side by side, where the -asymmetry is easy to read as an oversight: - -- `service deployment stop dep_1` stops the deployment immediately. If - that deployment is the live one, the service goes offline. **No - confirmation is asked.** -- `service deployment delete dep_1` requires the deployment id typed - back, interactively or via `--confirm`. `--yes` alone cannot grant it. - -This is contract-faithful. R-S8-2 asks for consent on `delete` only, per -the `service remove` precedent (R-S2b-3), and the line the precedent -draws is reversibility: a stopped deployment can be started again, a -deleted one cannot. Recorded, not changed. - -## Not a divergence, recorded because it looks like one - -**`service deployment start` checks no precondition of its own.** The -API requires a deployment's artifact to be uploaded before it will -start. The CLI does not test for that: it makes the call, and if the API -refuses, the API's own message is what the user reads (inside -`SERVICE.DEPLOY_FAILED`). Per R-S8-2 the failure is the API's answer -presented properly, not a precondition the CLI invents — so the absence -of a check here is the requirement being met, not a gap in it. - -The same holds for `delete` against a running deployment: the API states -the stop precondition, the CLI does not pre-empt it. - -## Command grammar cleanup (2026-08-21 PM review) - -`service remove` is renamed `service delete` (result field `removed` becomes `deleted`; `SERVICE.REMOVE_FAILED` becomes `SERVICE.DELETE_FAILED`), and every service command that targets an existing service now requires `--service` or `PRISMA_SERVICE_ID` — the interactive picker and the remembered selection are gone, and `--branch` no longer falls back to the local git branch. diff --git a/.drive/projects/prisma-cli-v8/assets/s2/parity-divergences-service-logs.md b/.drive/projects/prisma-cli-v8/assets/s2/parity-divergences-service-logs.md deleted file mode 100644 index 243bfad1..00000000 --- a/.drive/projects/prisma-cli-v8/assets/s2/parity-divergences-service-logs.md +++ /dev/null @@ -1,113 +0,0 @@ -# service logs parity divergences - -What changes for a user between the `service logs` that S2c shelved and -the one this slice ships. Same entry format as -[`parity-divergences.md`](parity-divergences.md), and the same standing -ruling behind it (S2 standing ruling 10: divergences are enumerated, not -discovered). - -**The baseline is the shelved S2c command**, not a shipping binary. No -released CLI in this repo has had a working `service logs`: `app logs` -died with the commander shell, and S2c's replacement never landed -because the transport it needed did not exist. So nothing below breaks a -user's existing habit — but the command's shape moved a long way from -the one the S2c record describes, and that record is what a reader will -have in hand. - -The command mounts as **`service logs`**, the legacy spelling, not under -the `deployment` subgroup the S8 reshape created (ruled, operator, -2026-08-13; recorded in `deferred.md`). - -## Reading a page replaces holding a socket - -S2c streamed: it opened a WebSocket through the compute SDK's -`streamLogs`, authenticated it with a raw token it fetched itself, and -printed records until the far end closed. There was no flag to ask for -anything else, so **following was the only behaviour**. - -The platform serves one page per plain `GET` and closes it -(pdp-control-plane #4886), so the command is a page read: - -- **Default: the last 100 lines, then exit 0.** The shape `kubectl logs` - has. A script that ran the S2c command and expected it to block until - interrupted now gets output and a clean exit instead. -- **`--follow` asks for the streaming behaviour back**, and gets polling - rather than push: each page closes with a terminal record naming the - cursor the next page starts at, so the command waits two seconds and - asks again from there. The lines are the same; the latency is now - bounded by the poll interval rather than by the server's write. -- The WebSocket upgrade still exists on the same path and the CLI never - uses it. The engine socket transport S2c waited for was not built - (R-S8-5), and the shelved design stays shelved for the live-streaming - date. - -## Three flags that did not exist - -`--tail `, `--from-start` and the page-size default are all new -surface — S2c had no way to say how much log to read. - -- `--tail ` resizes the page; unflagged runs send `tail=100` - explicitly, matching the endpoint's own default rather than relying - on it. -- `--from-start` reads from the beginning instead. -- **Passing both is refused** (`SERVICE.LOGS_RANGE_CONFLICT`, exit 2), - before the target is resolved or anything is read: they name opposite - ends of the same log, so there is no reading that satisfies both. - -`--deployment`, `--service`, `--project` and the config-target -positional carry over from S2c unchanged, resolution and error shapes -included. - -## Two refusals where S2c would have carried on - -Both are cases the platform contract says should not arise. They are -refused rather than absorbed, because absorbing them produces output a -user cannot tell from correct output. - -- **A page that closes with no resume cursor stops `--follow`** - (`SERVICE.LOGS_NO_CURSOR`). Continuing would mean re-requesting with no - range, which the endpoint answers with its default tail — the same - hundred lines reprinted every two seconds, indefinitely, with nothing - saying why. -- **A page whose body ends without its terminal record is an incomplete - read** (`SERVICE.LOGS_INCOMPLETE`), in page mode as well as follow. - The lines that did arrive are still printed; what is refused is - settling as though the whole page had been read, which would show a - user a truncated log with no sign it was truncated. - -Neither settles quietly, and the reasoning for `--follow` is that it has -no successful ending: it runs until interrupted, which settles 130, or -it fails. An exit 0 from a follow would be a new outcome meaning "gave -up", indistinguishable from a follow that never started. - -## An error terminal record ends the run - -`type: "terminal"` with `kind: "error"` is the platform reporting that -the log read itself failed. It settles as `SERVICE.LOGS_FAILED` carrying -the record's own code, message and `retryable` flag, exit 2 — where S2c -printed the message to the diagnostic channel and exited 0. - -In `--follow`, a **retryable** error terminal is retried once after the -poll interval, and the budget resets on any page that succeeds. So a -long follow survives repeated transient failures but never loops on a -persistent one. - -## Not a divergence, recorded because it looks like one - -**Interrupting `--follow` settles 130.** Ctrl-C ends the run at 128 + -SIGINT from the engine's own record of the signal, so a wrapper script -that treats a non-zero exit as failure sees one when a developer stops -following. - -This is worth stating because the S2c handler reads as though it did the -opposite — it treats a cancelled stream as an expected user action and -returns success. That difference is not observable: the engine settles a -signalled run at 128 + the signal "whatever the handler concluded" -(operator ruling, 2026-08-11, recorded against `composer dev` in -[`parity-divergences-s3.md`](parity-divergences-s3.md)). S2c would have -settled 130 too. The exit code comes from the engine rule, not from -anything this slice changed. - -## Command grammar cleanup (2026-08-21 PM review) - -`service logs` now requires a service target (`--service` or `PRISMA_SERVICE_ID`) unless `--deployment` names a deployment id with no service target, which resolves globally within the project. The picker and remembered selection are gone. diff --git a/.drive/projects/prisma-cli-v8/assets/s2/parity-divergences.md b/.drive/projects/prisma-cli-v8/assets/s2/parity-divergences.md deleted file mode 100644 index ae15f6f4..00000000 --- a/.drive/projects/prisma-cli-v8/assets/s2/parity-divergences.md +++ /dev/null @@ -1,1612 +0,0 @@ -# S2 cumulative parity divergences - -Every known place where the v8 ports differ from the shipping `prisma-cli`, in one document for operator sign-off (S2 standing ruling 10: divergences are enumerated, not discovered; maintainability outranks byte parity; R-S2d-6 consolidates). The former per-slice files (`-s2b`, `-s2c`, `-s2d`) are folded in below verbatim, headings demoted one level. S3's and S8's lists belong to those slices and stay separate. - -The S1 whoami-scoped record — [`../engine/whoami-parity-divergences.md`](../engine/whoami-parity-divergences.md) — remains the baseline for everything the engine changes globally (json framing, format auto-selection, stderr/stdout channel discipline, rendering style, `--quiet` as a log-level alias, exit-code semantics, the dropped `--trace`, the shared flag family). Those apply to every ported command and are not repeated per command below. - -## Signed off - -**Ratified by the operator, 2026-08-12**, together with the whole document: every divergence built to a stated default stands, including the items below, which were surfaced as explicit choices rather than defaults. The escalated engine gaps stay ratified-as-shipped; the work to close them is recorded in `../../deferred.md`. - -1. **`init`'s optional steps default to no** — S2d list, entry 10 (marked DECIDE). Interactive users press `y` where they pressed Enter; unattended runs keep today's behaviour exactly. One line per prompt to flip. -2. **A service token whose workspace only the server knows is now refused** — S2c, "ESCALATED — engine gap". -3. **`build logs`: a failed build cannot exit 1** — S2c, "ESCALATED — engine gap". -4. **Help examples lose the package runner, and one command spells itself two ways** — S2c, "ESCALATED — engine gap"; worth one group-wide ruling. -5. **The crash-recovery feedback action does not port** — S2c, "ESCALATED — engine gap". -6. **Open ledger questions** (in `../../specs/s2-overview.md`, built to their stated defaults): Q1 auto-login stays dropped; Q3 the `rm` alias stays dropped; Q5 exit-code unification; Q6 telemetry docs URL; Q7 telemetry config enrichment dropped; Q8 disclosure timing. Signing off this document ratifies those defaults unless ruled otherwise first. - -## Classes that hold for every ported command - -Each slice restates these with its own group's legacy codes; the rules themselves are one set. Errored settlements exit 2 and cancellations exit 3, whatever the legacy per-error code was. Flat `UPPER_SNAKE` codes become dotted codes under the group's namespace. Legacy `fix` prose becomes one `user-choice` next action; each `nextSteps` string becomes a `run-command` action. `warnings: string[]` become coded diagnostics. Human rendering is engine blocks, with the machine-readable rows also written to stdout, where legacy wrote stdout nothing. `--trace` is gone (log levels cover it). Fixture mode is gone. The ruled renames: `database` → `postgres`, `app` → `service`; the ruled removals: `service build`, `service deploy`, `service run` (superseded by Composer), the mock-only login flags, and the `version` command (the engine's `--version` answers). - -## S2a — auth family + update check (this PR) - -The auth family is implemented ON the credential manager, whose normative design is [`../engine/credential-manager-design.md`](../engine/credential-manager-design.md). Read §11 there for the model this section describes: a set of stored per-workspace sessions plus one selection, and — separately — the credential this process authenticates as, which may come from `PRISMA_SERVICE_TOKEN` and is not a session. The COMMAND NAMES are the legacy ones and do not change — there is no rename class in this list. What follows is what a user can still observe as different. - -### Error-code mapping (flat → dotted, session vocabulary) - -Every errored settlement exits 2 in v8, regardless of the legacy -per-error exit code. Legacy `fix` prose maps to one `user-choice` -nextAction; `meta` is preserved. - -| Legacy flat code (exit) | v8 code (exit) | Raised by | -| --- | --- | --- | -| `AUTH_CONFIG_INVALID` (1) — blank `PRISMA_SERVICE_TOKEN` | `AUTH.SERVICE_TOKEN_EMPTY` (2) | every command, single-sourced from `activeCredential()` | -| `WORKSPACE_NOT_AUTHENTICATED` (1) | `AUTH.NO_SESSION_FOR_WORKSPACE` (2) | `workspace use`, `workspace logout` | -| `WORKSPACE_AMBIGUOUS` (2) | `AUTH.WORKSPACE_AMBIGUOUS` (2) | `workspace use`, `workspace logout` | -| `WORKSPACE_SWITCH_UNAVAILABLE` (1) | **no successor** | nothing — the mutations it guarded now succeed (see below) | -| `USAGE_ERROR` (2) — "No authenticated workspaces" | `AUTH.NO_WORKSPACE_SESSIONS` (2) | `workspace use` | -| `USAGE_ERROR` (2) — "Workspace required" (blank ref) | `AUTH.NO_SESSION_FOR_WORKSPACE` (2) | `workspace logout` — a blank/whitespace ref matches no session rather than being its own usage error | -| (none — legacy could not happen) | `AUTH.LOGIN_WORKSPACE_UNKNOWN` (2) | `login`, when the minted credential carries no `workspace_id` claim | -| (none) | `CLI.CREDENTIALS_REQUIRED` (2) | the engine, for signed-out and sessions-held-none-selected | - -No documented 4–99 codes exist in this family. - -### Exit unifications - -Legacy exit 1 for `AUTH_CONFIG_INVALID` and `WORKSPACE_NOT_AUTHENTICATED` becomes exit 2 (could-not-complete) in v8. A failed login (browser launch, callback, token exchange) was an unstructured crash at exit 1 in legacy and still settles at exit 1, now as a structured `CLI.INTERNAL_ERROR`. - -### `auth whoami` — json shape - -The legacy result was `AuthStateResult` (`authenticated`/`provider`/`user`/`workspace`/`credential`). The v8 result describes the active credential: - -```json -{ "authenticated": true, "workspace": { "id": "…", "name": "…" }, - "user": { "id": "…", "email": "…", "name": "…" }, - "source": "stored", "expiresAt": null } -``` - -- **`provider` has NO successor.** Nothing in the model records which identity provider minted a credential, so the field is gone rather than renamed. -- `credential` is gone: the type/id/name of the credential is not a user-facing concept here. -- `source` is new (`"stored"` | `"environment"`) and comes from the credential's origin; `expiresAt` is the credential's expiry. -- `user` keeps `id`, `email` and `name`. There is one identity type for both the claimed and the fetched identity (design §11.6); a token's claims carry an id and an email, and only the online lookup supplies a name, so `name` is null offline. The human card's `user` row shows the email, or is omitted when there is none. -- **A service token reports no user at all.** Its subject names a workspace rather than a person, so `user` is null and the workspace is read from that subject. Reporting `workspace:` as a user id was a defect. -- Identity display: the credential manager decodes the credential's own claims, and `/v1/me` is a best-effort online enrichment that wins field by field where it disagrees. whoami does not branch on the origin — it attempts the enrichment for an environment credential too, and falls back to the claims when the request fails. **This restores legacy behaviour that rev 5 had dropped:** a stored session offline now shows the claim-derived user again, where rev 5 showed the workspace and no user. -- **A credential nothing names renders no workspace at all.** An environment token whose claims carry no workspace reports `"workspace": null` and omits the workspace row from the human card. It is never an empty string and never the literal `undefined` — rev 5 wrote `workspaceId: ""` in that case. -- Signed out still exits 0. - -### Mutations while `PRISMA_SERVICE_TOKEN` is set - -The variable supplies the credential this process authenticates as. It is not a session, so it does not occupy a slot that a stored session could be moved into or out of, and commands that change stored state are free to run. Design §11.7 rules that all of them succeed: - -| Command | Behavior while the variable is set | -| --- | --- | -| `auth workspace use` | **succeeds** — the stored selection moves; this process keeps authenticating as the environment credential | -| `auth workspace logout` | **succeeds** — the named session is removed | -| `auth logout` | **succeeds** — the store is cleared, whether or not it held sessions | -| `auth login` | **succeeds** — a new session is stored and selected | -| every read (`whoami`, `workspace list`) | works normally | - -Each of the four mutations prints the same one-line notice in human output: the environment credential remains in force until the variable is unset. The notice itself is human-only. Two json results carry the fact as a field — `auth workspace list`'s `context.environmentCredentialInForce` and `auth login`'s `environmentCredentialInForce` — and those are the machine-readable signal. - -This is the second change here. Legacy refused workspace switching with `WORKSPACE_SWITCH_UNAVAILABLE` and let `auth logout` clear stored state. Rev 5 of the design refused `workspace use`, `workspace logout` and `auth logout` with `AUTH.ENV_SESSION_IN_FORCE`, carving out an empty store so CI teardowns would not fail. **`AUTH.ENV_SESSION_IN_FORCE` no longer exists**, and neither does the carve-out. The net effect against legacy is that workspace switching now works while the variable is set, where legacy refused it. - -A blank or whitespace-only `PRISMA_SERVICE_TOKEN` is unchanged: it is never an override, and every command — mutations included — fails with `AUTH.SERVICE_TOKEN_EMPTY`. - -### `auth workspace logout` — json shape - -The result is `{ workspace: { id, name }, wasSelected }`. `wasSelected` says whether the session that was removed had been the selected one; when it was, nothing is promoted in its place and the next actions offer `auth workspace list` and `auth workspace use`. - -### Ending a session is idempotent - -`auth workspace logout ` resolves the ref against the sessions you hold, so a workspace you never had is still `AUTH.NO_SESSION_FOR_WORKSPACE`, exit 2. What changed is the race: if another `prisma` process removes that session between the resolution and the write, the command now exits 0 rather than exit 2 with a message that is no longer true. The postcondition — no session for that workspace — holds either way. Selecting is not idempotent and still refuses a workspace with no session. - -### `auth workspace list` - -- Rows are the sessions the manager holds: `name`, `id`, `status`, where status is `current` (legacy: `active`). The legacy `source` column and the `auth source` line are gone — the environment credential never appears as a row. -- While `PRISMA_SERVICE_TOKEN` is set the listing STATES that the environment credential is in force; the stored selection is still shown as current. The json context carries `environmentCredentialInForce: true` alongside `currentWorkspaceId`, which keeps naming the stored selection, not the environment credential's workspace. `currentWorkspaceId` and the per-item `current` keep the word "current" deliberately: they are an output contract, where the code says "selected" (design §11.1). `environmentCredentialInForce` was renamed from `environmentSessionInForce` — the thing it describes is not a session, which is the whole point of §11. -- The json shape is new (`context`/`items`/`count` with - `workspaceId`/`workspaceName`/`current`/`expiresAt`); the legacy - fields `credentialWorkspaceId`, `switchable`, `lastSeenAt` and - `source` have no successor. -- A session whose name was never fetched renders by its workspace id in - both columns. -- Human mode also writes the data rows to stdout; legacy wrote nothing. - -### `auth logout` — orphan reaping and the count - -- `auth logout` ends EVERY workspace session, not just the active one, - and reports how many it ended (`endedCount`). Legacy cleared the - active credential and could leave orphaned per-workspace entries - behind; those are now reaped, together with the legacy files. -- The presentation reports the count; the json result is - `{ endedCount, workspaceIds }`, replacing the raw post-logout - `AuthStateResult`. -- **`auth logout --workspace ` no longer exists.** `auth workspace - logout ` is the one way to end a single session. - -### `auth workspace use` — selects only - -Ruled: `workspace use` SELECTS among the sessions you have and never -creates one. A ref naming a workspace you hold no session for is -`AUTH.NO_SESSION_FOR_WORKSPACE`, exit 2, whose nextAction is the -literal `prisma auth login` ("sign in and pick it in the browser"). No -browser ever opens from `use`. Legacy behaved the same way in effect -(it could not create a session either) but said -`WORKSPACE_NOT_AUTHENTICATED` at exit 1. - -- Ref resolution is command-side: exact workspace id first, then - case-insensitive workspace NAME (legacy matched names exactly). - Several sessions sharing a name is `AUTH.WORKSPACE_AMBIGUOUS`, which - lists the matching workspace ids in `meta.workspaceIds`. -- Absent positional + several sessions + non-interactive: legacy threw - its own `USAGE_ERROR`; v8 lets the engine's structural prompt failure - speak — `CLI.PROMPT_REQUIRED` (exit 2), `CLI.PROMPT_INVALID` (exit 2) - for an invalid scripted answer, `CLI.PROMPT_CANCELLED` (exit 3) on - cancellation. Single-session auto-select is unchanged. - -### Workspace names are never refreshed on read - -A workspace name is fetched once, best-effort, when the session is -created. Reads are entirely offline, so a workspace renamed in the -console keeps its stored name locally until the next login to it (and a -session whose name fetch failed renders by id). Legacy re-fetched names -on every `whoami`/`list` and wrote them back. Accepted and stated. - -### `auth login` - -- The fixture-only flags `--provider`, `--user`, `--workspace` do NOT - port (hidden mock-selection surface; fixture machinery dies in S2d). -- The flow speaks engine events: `step-started`/`step-finished` around - the browser flow, and an `endpoint` event named `verification` - carrying the OAuth authorize URL (via the optional - `onVerificationUrl` hook on `performLogin`). Legacy printed the URL - only inside the interactive instruction prose. -- The interactive paste-fallback prompt and instruction prose inside - `performLogin` still write to the process's own stdin/stderr; - unchanged from legacy. -- The json result is `{ workspace: { id, name }, environmentCredentialInForce }` — the workspace the session was created for, not an auth-state snapshot. -- Agent-setup tip: legacy suppressed it under `--json`, `--quiet`, CI - (unless `--interactive`), and non-TTY stderr. In v8 CI suppression is - kept (`ctx.env.CI`); the tip LINE renders only in the human - presentation; the tip nextAction appears in json envelopes, where - legacy omitted it entirely. `--quiet` no longer suppresses it. -- nextActions: `prisma-cli auth whoami`, `prisma-cli project list`, - plus the tip command when present. - -### Update check (§5) - -- The module moves to `packages/cli/src/update-check.ts` with a - structural `UpdateCheckRuntime` (env/argv/stderr); both shells - consume it. Sequencing copied from the legacy call sites: cached - notify + detached refresh spawn awaited before dispatch - (`src/cli.ts` for the legacy shell, `src/v8/main.ts` for the v8 - bin), worker branch in both bins - (`PRISMA_CLI_RUN_UPDATE_CHECK_WORKER=1`). -- json mode: the legacy shell prints NOTHING when argv contains - `--json`/`--quiet`/`-q` — copied as-is (the contract's - decide-by-current-behavior rule). Note the check is literal argv - matching: the v8 spelling `--format json` is NOT suppressed (and - `-q` still suppresses although v8's quiet is only a log-level - alias). Same for `--version`, CI, non-TTY stderr, and - `NO_UPDATE_NOTIFIER`. - -### Telemetry (§6) - -- **Config enrichment dropped.** The ORM CLI's detached sender loaded - `prisma-next.config.*` via c12 (evaluating arbitrary user TS in the - child) to derive the `databaseTarget` and `extensions` event fields. - That config file does not exist in this product, so the load was - removed: `databaseTarget` ships `null` (unless a parent-side override - is supplied on the wire, kept for compatibility) and `extensions` - ships `[]`, always. The wire shape is unchanged. -- **Emission timing — retired, no longer a divergence.** v8 briefly - emitted at settlement (`onSettled`), so a run that crashed, was - SIGKILLed, or left through `process.exit` emitted nothing where the - reference emitted one before the command started. The engine now - fires at command start from the parse-time snapshot, immediately - after it is built and before the handler — the same point the ORM - CLI's commander `preAction` hook fires from. ADR 217 (prisma/prisma), - which makes "spawned at command start" the isolation decision, stays - true and needs no amendment. -- **First-run disclosure wording.** The ORM CLI says "Prisma Next - collects anonymous CLI usage data"; the engine composes one - disclosure for one product and says "Prisma collects anonymous CLI - usage data". Same channel (stderr), same timing (first enabled run, - before the command runs), same opt-out instructions, and the same - `installationId`-keyed once-only behaviour. The ORM inherits this - wording when its bin ports onto the engine. -- **The preference file and the opt-out variables drop `prisma-next`, and - the file stops being shared.** Ruled by the operator on 2026-08-11: this is - semver zero and the `prisma-next` binary is being retired, which is the - point of the project. The preference now lives under `prisma/` rather than - `prisma-next/`; `PRISMA_NEXT_DISABLE_TELEMETRY`, `PRISMA_NEXT_TELEMETRY_ - ENDPOINT` and `PRISMA_NEXT_DEBUG` become `PRISMA_DISABLE_TELEMETRY`, - `PRISMA_TELEMETRY_ENDPOINT` and `PRISMA_DEBUG`. No read fallback, no - dual-write, no migration — the old location is not consulted and the old - variable names do nothing, pinned by tests so a fallback cannot return. - Consequences, both accepted: every stored preference and installation id - at the old path is abandoned, so an existing opt-out reverts to the - opt-out default and the backend sees its population turn over once; and - the two binaries stop sharing one answer until the ORM's ports onto the - engine. `DO_NOT_TRACK` is a community convention and does not move. -- **The first-run notice no longer offers to be opted out of by hand.** It - named the config file as a third route ("or set `enableTelemetry: false` - in …"); the operator ruled the file is machine-edited and the commands - exist for this. The notice keeps `telemetry disable` and both environment - variables, and `telemetry status` still prints the path. -- **A negated flag ships one name, not two.** Commander gives - `--no-color` the same attribute name as `--color`, so the ORM CLI's - sanitiser sees both option entries sourced from the command line and - emits `["color", "no-color"]` — whichever spelling the user typed. - The engine maps a `--no-` token back to its base key and emits - `["color"]`. Neither preserves polarity, so nothing is lost: the ORM - simply ships a flag the user never typed and double-counts these in - aggregate. Affects `--color` and `--interactive`, the only negatable - flags on either side. This is not new — the engine's snapshot builder - has behaved this way since S1 — but the engine becoming the shared - implementation is when the ORM's counts for those two flags change, - so the backend should expect the discontinuity at its cutover, not at - this PR. - -### Test surface - -- `tests/auth.test.ts` fixture-mode cases covering the six ported commands are deleted; the file keeps its real-mode storage cases and the legacy-shell presentation cases (help text, TTY header) until S2d. The v8 side is pinned semantically in `tests/v8-auth.test.ts` (over the harness's in-memory credential manager, with manager state read-back) and `tests/v8-update-check.test.ts`; the byte pins live in `tests/v8-golden-rendering.test.ts` and `tests/v8-whoami.test.ts`. -- `Runtime.getCredentials` and its `makeGetCredentials` builder are gone. The engine asks the credential manager for the active credential and its token storage instead, so the bin no longer supplies a second, parallel way to read a token. - -## S2b — resources (project, postgres, bucket, branch, git) - -Divergences introduced by the S2b resource port. Grows per dispatch; -D4 consolidates. The auth stream owns `parity-divergences.md` — this -file never duplicates it. - -### D1 — the `project` group - -Delivered: all 11 commands — `project -list|show|create|link|rename|remove|transfer` and -`project env add|update|list|remove`. `remove` and `transfer` landed -in round 2, once the engine's consent tokens arrived -(conventions §5). - -#### Class divergences - -1. **Exit codes.** Every errored settlement exits 2. The legacy - commands exited 1 for `PROJECT_NOT_FOUND`, `PROJECT_AMBIGUOUS`, - `PROJECT_SETUP_REQUIRED`, `LOCAL_STATE_STALE`, - `LOCAL_PROJECT_WORKSPACE_MISMATCH`, `LOCAL_STATE_WRITE_FAILED`, - `PROJECT_CREATE_FAILED`, `PROJECT_RENAME_FAILED`, - `ENV_VARIABLE_ALREADY_EXISTS`, `ENV_VARIABLE_NOT_FOUND`, - `ENV_BRANCH_NOT_FOUND`, `ENV_BRANCH_SCOPE_IS_PRODUCTION`, - `ENV_BRANCH_CREATE_REQUIRES_DEFAULT_BRANCH`, - `ENV_FILE_APPLY_FAILED`, `ENV_API_ERROR` and the API-passthrough - codes; usage errors already exited 2. -2. **Auto-login dropped** (R-S2b-2). Every command declares - `needs.credentials`; an unauthenticated invocation settles with the - engine's `CLI.CREDENTIALS_REQUIRED` (exit 2) instead of launching - an interactive browser login. -3. **Error codes** are dotted (§ "Error code map" below). -4. **`verboseContext` dropped.** `--verbose` is a log level in v8, so - the env results no longer carry the resolved-context block, and the - "Resolved context" / "Env target" verbose blocks do not port. The - json `result` is unchanged, because the legacy serializers already - stripped `verboseContext`. -5. **NextActions.** The legacy `fix` prose becomes exactly one - `user-choice` action and each legacy `nextSteps` string becomes a - `run-command` action whose label is the command. The legacy - `journey` field has no v8 counterpart and is dropped. -6. **Package-runner command strings dropped.** Command strings in - nextActions are `${CLI_NAME} …`, never - `npx -y @prisma/cli@latest …`. -7. **`--trace` fix text** becomes `--log-level verbose`. -8. **List data rows go to stdout** in human mode (`project list`, - `project env list`); legacy human mode wrote nothing to stdout. -9. **Human rendering** is engine blocks, not the legacy rail-and-card - bytes. The title, field labels, table columns and empty-state - sentences port verbatim. - - **Narrowed by the engine-colour slice** (`specs/engine-colour.md`). - The card's aligned key column and its accent-coloured keys are back, - and byte-equal to the legacy `renderFieldRows` — a test in - `packages/cli/tests/v8-golden-rendering.test.ts` asserts the engine's - output against the commander shell's own renderer rather than against - a copied string. Tables align the same way. What is still missing is - the framing around the card, not the card: the dim `│` rail exists as - `fields.rail` but no command sets it yet, and the header line - (`project show → description`), its blank-line spacing and its - `Read more` row have no engine counterpart. Adopting the rail per - command is recorded in `deferred.md`. - -#### D1-specific divergences - -10. **`rm` alias dropped** for `project env remove` (R-S2b-8). The v8 - tree has exact paths only. -11. **`project link` picker, non-interactive.** Without a positional - and without a TTY (or under `--yes`), the engine's - `CLI.PROMPT_REQUIRED` (exit 2) replaces the legacy - `PROJECT_LINK_TARGET_REQUIRED` error. Its `meta.candidates`, - `meta.suggestedProjectName` and its rich nextActions are lost — - **flagged for operator review**. -12. **`PROJECT_AMBIGUOUS`** was exit 1 in the legacy code (the - inventory says 2); it is exit 2 in v8 like every other errored - settlement. -13. **`--role` invalid values.** The engine's enum parse failure - replaces commander's `Allowed choices are production, preview.` - message. -14. **The legacy `AUTH_REQUIRED` code maps mechanically to - `PROJECT.AUTH_REQUIRED`** (summary, why and next steps verbatim, - exit 2 instead of 1). The engine owns every real credentials - failure — an expired stored session settles - `CLI.CREDENTIALS_REQUIRED` and a rejected env token settles - `AUTH.SERVICE_TOKEN_REJECTED`, both before a handler sees them — - so what still reaches `apiCallError` is the permission residue, a - returned 403, which is not a sign-in problem. The legacy next - step's `prisma auth login` copy bug normalizes to - `prisma-cli auth login`. Its fix text also loses the clause ", or - rerun the command in a TTY to sign in interactively." and reads - "Run prisma-cli auth login." — auto-login is gone (R-S2b-2), so - rerunning in a terminal can no longer sign anyone in and the - sentence pointed at a remedy v8 does not have (operator ruling - 2026-08-10; conventions §4). The legacy shell keeps the original - sentence, which is still true there. -15. **`project rename` name-validation copy** still reads "Project - create requires a name" — the legacy copy bug ports verbatim - (recorded, not fixed). -16. **Preview-default warning becomes a diagnostic.** The legacy - `warnings` string on `project env add` (branch scope, key absent - from preview) becomes a `warn` diagnostic under the code - `PROJECT.ENV_PREVIEW_DEFAULT_MISSING` (d1-project.md §4.8; the - operator ratifies it through this list). The local-pin warnings - of `project remove` and `project transfer` become `warn` - diagnostics the same way, under the already-pinned - `PROJECT.LOCAL_STATE_WRITE_FAILED`. -17. **Env file-mode nextSteps.** A `#`-comment line in the legacy - `splitFileNextSteps` output is not an action of its own: it - becomes the `reason` of the run-command action it introduces, so - `# existing keys: "A"` explains the `project env update --file - …existing` step rather than standing beside it. -18. **Legacy shell-context adapter.** The pinned resolution and env - functions (`resolveProjectTarget`, `inspectProjectBinding`, - `resolveEnvWriteInput`, `runEnvAddFile`, `runEnvUpdateFile`, - `cleanupLocalPinForProject`, `rewriteOrClearLocalPinForProject`) - take the legacy shell `CommandContext` but read only - `runtime.cwd`, `runtime.env` and `runtime.signal`. v8 calls them - through the runtime-slice adapter in `v8/project/context.ts` - (d1-project.md §4.9) — accepted for this slice; the signature - cleanup belongs to S2d, when the legacy shell dies. -19. **Consent is engine-owned** (conventions §5). `project remove` - and `project transfer` declare no `--confirm` flag; the engine - injects the shared repeatable one, with the same CLI spelling and - the same exact-project-id value. Interactively the user types the - project id (there was no prompt at all before). The legacy - `CONFIRMATION_REQUIRED` error is gone: a missing or wrong grant is - the engine's `CLI.CONSENT_REQUIRED` (exit 2, was exit 2) or - `CLI.PROMPT_INVALID`, and its `meta.expectedConfirm` / - `meta.receivedConfirm` do not survive. `--yes` never grants - consent. -20. **Transfer's recipient resolution** drops the fixture branch and - calls `resolveRecipientWorkspaceSession` directly; the - service-token guard, the recipient error mapping and every copy - string are unchanged. -46. **A rejected project listing now fails instead of looking like an - empty workspace.** (Numbered after D3 because the sequence is - file-wide.) `listRealWorkspaceProjects` (`controllers/project.ts`) - read only `data` from the SDK response and discarded `error` and - `response`, so any non-2xx became `data === undefined` and then an - empty list. `project list` printed "No projects found." and exited - 0 while the API was refusing the request, and because - `resolveProjectTarget` resolves project names through the same - function, every command that resolves a project by name reported - "Choose a Project before running this command" or a not-found when - the real cause was a rejected request. - - Operator ruling 2026-08-11: fixed rather than recorded. The - function now raises `projectApiError`, which the v8 mapper carries - to `PROJECT.API_ERROR` at exit 2, and the `project list` case - "surfaces a rejected projects request instead of an empty list" - proves it. This is a legacy body change and therefore a divergence - in the other direction: the old shell reported success on a - rejected listing, and both shells now report the failure. Reporting - a refusal as a success was not a behaviour anyone chose, and the - database, branch, bucket, app and env controllers all read through - the same function, so the fix reaches them too. -47. **stdout rows carry raw values where the human table formats - them.** The Option A channel ruling (2026-08-09) makes the human - Blocks presentation prose on stderr and the `Presentations.stdout` - lines the machine-usable payload, so no human formatting and no - placeholder may reach a stdout row: an absent value is an empty - field, and where the two lanes differ the command builds two sets - of rows. The human tables and cards are unchanged, and `--json` - remains the lossless record. - - What changed on stdout, by command. `project list`: an absent - default region is empty, not `none`. `project show`: the local - repo path is raw rather than shortened to `~`, the single - `platform: / ` line becomes a `workspace` and - a `project` line — the labels this command already uses when the - directory is not linked — and an unlinked directory leaves the - project field empty instead of saying `Not linked`. `project env - list` already carried the bare key (entry 42's sibling, fixed the - same day). `postgres list`: absent branch and region are empty, and - the status field carries the raw status — the `isDefault` fallback - is a different fact and does not belong in that column, so an - absent status is empty there too. `postgres show`: the same three. - `postgres usage`: the period becomes `period start` and `period - end` rather than one glued sentence, each metric carries its number - without the unit, and an absent bound or timestamp is empty. - `postgres backup list`: the size is the byte count, not `2.0 KiB`, - and an absent timestamp is empty. `postgres connection list` and - `bucket list`: absent timestamp and branch are empty. - `branch list` and `bucket key list` needed no change — every cell - was already a raw required field. - - Two placeholders survive on stdout and this slice cannot remove - them. `postgres backup list`'s `backupType` and `status` are the - literal string `unknown` when the API omits them, because - `normalizeBackupList` (`lib/database/provider.ts`) substitutes that - word in the **operation layer**, before any presentation runs — the - fix is a legacy body change, out of scope here. Recorded so the - gap is visible rather than assumed closed. - -#### Error code map - -| legacy code | v8 code | -| --- | --- | -| `USAGE_ERROR` (project/app domain) | `PROJECT.USAGE_ERROR` | -| `USAGE_ERROR` "Workspace required" (auth domain) | `AUTH.USAGE_ERROR` | -| `PROJECT_NOT_FOUND` | `PROJECT.NOT_FOUND` | -| `PROJECT_AMBIGUOUS` | `PROJECT.AMBIGUOUS` | -| `PROJECT_SETUP_REQUIRED` | `PROJECT.SETUP_REQUIRED` | -| `LOCAL_STATE_STALE` | `PROJECT.LOCAL_STATE_STALE` | -| `LOCAL_PROJECT_WORKSPACE_MISMATCH` | `PROJECT.LOCAL_WORKSPACE_MISMATCH` | -| `LOCAL_STATE_WRITE_FAILED` | `PROJECT.LOCAL_STATE_WRITE_FAILED` | -| `PROJECT_CREATE_FAILED` | `PROJECT.CREATE_FAILED` | -| `PROJECT_RENAME_FAILED` | `PROJECT.RENAME_FAILED` | -| `ENV_VARIABLE_ALREADY_EXISTS` | `PROJECT.ENV_VARIABLE_ALREADY_EXISTS` | -| `ENV_VARIABLE_NOT_FOUND` | `PROJECT.ENV_VARIABLE_NOT_FOUND` | -| `ENV_BRANCH_NOT_FOUND` | `PROJECT.ENV_BRANCH_NOT_FOUND` | -| `ENV_BRANCH_SCOPE_IS_PRODUCTION` | `PROJECT.ENV_BRANCH_SCOPE_IS_PRODUCTION` | -| `ENV_BRANCH_CREATE_REQUIRES_DEFAULT_BRANCH` | `PROJECT.ENV_BRANCH_CREATE_REQUIRES_DEFAULT_BRANCH` | -| `ENV_FILE_APPLY_FAILED` | `PROJECT.ENV_FILE_APPLY_FAILED` | -| `ENV_API_ERROR` | `PROJECT.ENV_API_ERROR` | -| `PROJECT_API_ERROR` | `PROJECT.API_ERROR` | -| `PROJECT_REMOVE_BLOCKED` | `PROJECT.REMOVE_BLOCKED` | -| `PROJECT_TRANSFER_REJECTED` | `PROJECT.TRANSFER_REJECTED` | -| `TRANSFER_RECIPIENT_REQUIRED` | `PROJECT.TRANSFER_RECIPIENT_REQUIRED` | -| `TRANSFER_RECIPIENT_UNAVAILABLE` | `PROJECT.TRANSFER_RECIPIENT_UNAVAILABLE` | -| `WORKSPACE_AMBIGUOUS` / `WORKSPACE_NOT_AUTHENTICATED` | `AUTH.WORKSPACE_AMBIGUOUS` / `AUTH.WORKSPACE_NOT_AUTHENTICATED` | -| `AUTH_REQUIRED` | `PROJECT.AUTH_REQUIRED` | -| raw API `error.code` X | `PROJECT.X` | -| `FEATURE_UNAVAILABLE` (fixture only) | unreachable in v8 | - -`PROJECT.CONFIRMATION_REQUIRED` and `PROJECT.LINK_TARGET_REQUIRED` -remain in the mapper table but are unreachable: the engine's consent -and prompt errors replace them. - -#### Conformance rows - -| command | inventory entry | rules applied | divergences | -| --- | --- | --- | --- | -| `project list` | `project list` | R-S2b-2, 5, 9, 10; d1 §3.1 | 1, 2, 3, 5, 6, 8, 9 | -| `project show` | `project show` | R-S2b-2, 5, 9, 10; d1 §3.2 | 1, 2, 3, 5, 9, 12, 18 | -| `project create` | `project create` | R-S2b-2, 5, 9, 10; d1 §3.3 | 1, 2, 3, 5, 9 | -| `project link` | `project link` | R-S2b-2, 5, 6, 9, 10; d1 §3.4 | 1, 2, 3, 5, 9, 11, 12 | -| `project rename` | `project rename` | R-S2b-2, 5, 9, 10; d1 §3.5 | 1, 2, 3, 5, 9, 15, 18 | -| `project env add` | `project env add` | R-S2b-2, 5, 9, 10; d1 §3.8 | 1, 2, 3, 4, 5, 9, 13, 14, 16, 17, 18 | -| `project env update` | `project env update` | R-S2b-2, 5, 9, 10; d1 §3.9 | 1, 2, 3, 4, 5, 9, 13, 14, 17, 18 | -| `project env list` | `project env list` | R-S2b-2, 5, 9, 10; d1 §3.10 | 1, 2, 3, 4, 5, 8, 9, 13, 14, 18 | -| `project env remove` | `project env remove` | R-S2b-2, 5, 8, 9, 10; d1 §3.11 | 1, 2, 3, 4, 5, 9, 10, 13, 14, 18 | -| `project remove` | `project remove` | R-S2b-2, 3, 5, 9, 10; d1 §3.6 | 1, 2, 3, 5, 9, 16, 18, 19 | -| `project transfer` | `project transfer` | R-S2b-2, 3, 5, 9, 10; d1 §3.7 | 1, 2, 3, 5, 9, 16, 18, 19, 20 | - -### D2 — the `postgres` group - -Delivered: all 11 commands — -`postgres list|show|create|usage|restore|remove`, -`postgres backup list`, and -`postgres connection list|create|rotate|remove`. - -#### Divergences - -D1's class entries 1 (exit codes), 2 (auto-login dropped), 5 -(nextActions from fix and nextSteps), 6 (package-runner strings -dropped), 7 (`--trace` → `--log-level verbose`), 9 (engine blocks -replace the legacy rail rendering) and 18 (the shell-context adapter, -used here for project resolution) apply identically. On top of them: - -21. **Rename** (R-S2b-1). The group, its subgroups, every command - path and id (`postgres.connection.rotate`), every help string and - example, and every command reference inside `why`, `fix` and - nextAction text move from `database` to `postgres`. No alias - survives. The resource noun "database" in prose is unchanged — - the resource is a Prisma Postgres database. -22. **Error-code map** (see below), including the mechanical - passthrough of raw API codes as `POSTGRES.`. -23. **Consent is engine-owned** for `restore`, `remove`, - `connection rotate` and `connection remove`: no `--confirm` flag - is declared, the engine injects the shared one with the same - spelling and the same exact-id value, and interactively the user - types that id. The legacy `CONFIRMATION_REQUIRED` error is - unreachable, so `POSTGRES.CONFIRMATION_REQUIRED` has no entry in - the mapper and `meta.expectedConfirm` / `meta.receivedConfirm` - are gone. `--yes` never grants consent; a wrong typed answer is - `CLI.PROMPT_INVALID`, exit 2; and cancelling the prompt with - Ctrl-C or EOF settles `CLI.PROMPT_CANCELLED`, exit 3 — an exit - code these commands never produced before, because they had no - prompt to cancel. -24. **Consent prompts are new.** The legacy commands had no - interactive confirmation at all — only the flag. The prompt's - question is each command's legacy confirmation `why` sentence, - verbatim. -25. **Plan-limit rendering.** PR #127's `humanLines` full-page - override does not port. The error keeps its summary, why and meta - verbatim and carries exactly one `user-choice` nextAction whose - reason is the upgrade URL and plan name when the best-effort - subscription lookup returned them, and the Console guidance - otherwise. -26. **List commands write their data rows to stdout** in human mode - (`postgres list`, `postgres backup list`, `postgres connection - list`); legacy human mode wrote nothing to stdout. `show` and - `usage` mirror their field rows the same way. -27. **Pre-result progress lines dropped.** `Creating database...`, - `Creating connection...` and `Rotating connection...` have no v8 - counterpart: these are sync commands with no events. -28. **`verboseContext` dropped** from every result, and with it the - `--verbose` "Resolved context" and metadata blocks. The json - envelope is unchanged, since the legacy serializers already - stripped it. -29. **Fixture-only `DATABASE_CONNECTION_NOT_FOUND`** has no v8 - counterpart: in real mode an unknown connection is an API - passthrough code on rotate and remove. -30. **`database-plan-limit.test.ts` trimmed to its provider cases.** - Seven cases drove the ported `database show` through the legacy - shell and are deleted; the mapped plan-limit error is covered by - the v8 postgres tests, both with and without a subscription - lookup result. The eleven provider unit cases stay, because the - provider is the operation layer v8 calls (d2 §5): the plan-limit - discriminator, the responses that must not be classified as plan - limits, and the 3-second subscription-lookup timeout. The one - behavior that goes with the deleted cases is the legacy shell's - cancel path (exit 130 with `COMMAND_CANCELED` when the caller - aborts during enrichment); in v8 cancellation is engine-owned. - -#### Error code map - -| legacy code | v8 code | -| --- | --- | -| `USAGE_ERROR` (database domain) | `POSTGRES.USAGE_ERROR` | -| `USAGE_ERROR` "Workspace required" (auth domain) | `AUTH.USAGE_ERROR` | -| `DATABASE_NOT_FOUND` | `POSTGRES.NOT_FOUND` | -| `DATABASE_AMBIGUOUS` | `POSTGRES.AMBIGUOUS` | -| `DATABASE_CONNECTION_MISSING` | `POSTGRES.CONNECTION_MISSING` | -| `DATABASE_CONNECTION_STRING_MISSING` | `POSTGRES.CONNECTION_STRING_MISSING` | -| `DATABASE_BACKUPS_UNSUPPORTED` | `POSTGRES.BACKUPS_UNSUPPORTED` | -| `DATABASE_RESTORE_CONFLICT` | `POSTGRES.RESTORE_CONFLICT` | -| `DATABASE_BACKUP_NOT_FOUND` | `POSTGRES.BACKUP_NOT_FOUND` | -| `DATABASE_API_ERROR` | `POSTGRES.API_ERROR` | -| `PLAN_LIMIT_REACHED` | `POSTGRES.PLAN_LIMIT_REACHED` | -| raw API `error.code` X | `POSTGRES.X` | -| `PROJECT_NOT_FOUND` / `PROJECT_AMBIGUOUS` / `PROJECT_SETUP_REQUIRED` / `LOCAL_STATE_STALE` / `LOCAL_PROJECT_WORKSPACE_MISMATCH` | the project group's codes, mapped by the single source in `v8/project/errors.ts` | -| `CONFIRMATION_REQUIRED` | unreachable — the engine's `CLI.CONSENT_REQUIRED` replaces it | -| `AUTH_REQUIRED` / `AUTH_CONFIG_INVALID` | unreachable behind `needs.credentials` | - -#### Conformance rows - -| command | inventory entry | rules applied | divergences | -| --- | --- | --- | --- | -| `postgres list` | `database list` | R-S2b-1, 2, 5, 9, 10; d2 §3.1 | 1, 2, 5, 6, 7, 9, 18, 21, 22, 25, 26, 28 | -| `postgres show` | `database show` | R-S2b-1, 2, 5, 9, 10; d2 §3.2 | 1, 2, 5, 6, 7, 9, 18, 21, 22, 26, 28 | -| `postgres create` | `database create` | R-S2b-1, 2, 4, 5, 9, 10; d2 §3.3 | 1, 2, 5, 6, 7, 9, 18, 21, 22, 25, 27, 28 | -| `postgres usage` | `database usage` | R-S2b-1, 2, 5, 9, 10; d2 §3.4 | 1, 2, 5, 6, 7, 9, 18, 21, 22, 26, 28 | -| `postgres restore` | `database restore` | R-S2b-1, 2, 3, 5, 9, 10; d2 §3.5 | 1, 2, 5, 6, 7, 9, 18, 21, 22, 23, 24, 28 | -| `postgres remove` | `database remove` | R-S2b-1, 2, 3, 5, 9, 10; d2 §3.6 | 1, 2, 5, 6, 7, 9, 18, 21, 22, 23, 24, 28 | -| `postgres backup list` | `database backup list` | R-S2b-1, 2, 5, 9, 10; d2 §3.7 | 1, 2, 5, 6, 7, 9, 18, 21, 22, 26, 28 | -| `postgres connection list` | `database connection list` | R-S2b-1, 2, 5, 9, 10; d2 §3.8 | 1, 2, 5, 6, 7, 9, 18, 21, 22, 26, 28 | -| `postgres connection create` | `database connection create` | R-S2b-1, 2, 4, 5, 9, 10; d2 §3.9 | 1, 2, 5, 6, 7, 9, 18, 21, 22, 27, 28 | -| `postgres connection rotate` | `database connection rotate` | R-S2b-1, 2, 3, 4, 5, 9, 10; d2 §3.10 | 1, 2, 5, 6, 7, 9, 21, 22, 23, 24, 27, 29 | -| `postgres connection remove` | `database connection remove` | R-S2b-1, 2, 3, 5, 9, 10; d2 §3.11 | 1, 2, 5, 6, 7, 9, 21, 22, 23, 24, 29 | - -### D3 — the `bucket`, `branch` and `git` groups - -Delivered: `bucket list|create|delete`, `bucket key list|create|delete`, -`branch list`, `git connect` and `git disconnect` — all nine commands, -`git connect` included. - -`git connect` shipped in two parts. Everything but the wait for the -GitHub App installation landed first; the wait followed once the operator -extended the engine (commit c463aa1) with an optional `interval` on -`BrowserWaitRequest` and an `open-url` kind on `NextAction`, and settled -the three facts d3 §3.8 had pinned against a helper that could not supply -them. The four resolutions are recorded below as divergences 42 to 45 — -42 is the `open-url` change, 43 to 45 the rest — and in §3.8's STEP 5 -RESOLVED block. - -#### Divergences - -D1's class entries 1 (exit codes), 2 (auto-login dropped), 5 (nextActions -from fix and nextSteps), 6 (package-runner strings dropped), 7 (`--trace` -→ `--log-level verbose`), 9 (engine blocks replace the legacy rail -rendering) and 18 (the shell-context adapter, used here for project -resolution) apply identically, as do D2's 26 (list data rows go to -stdout), 27 (pre-result progress lines dropped) and 28 (`verboseContext` -dropped). On top of them: - -31. **Error-code maps** (see below), including the mechanical passthrough - of raw API codes as `BUCKET.`, `BRANCH.` and - `GIT.`. -32. **`bucket delete` gains a consent prompt.** The legacy command had a - `--confirm ` flag and no prompt at all. In v8 the flag is - the engine's shared repeatable `--confirm`, with the same CLI - spelling and the same exact-bucket-id value; interactively the user - types the bucket id. The question is the legacy confirmation `why` - sentence verbatim: "Deleting this bucket permanently removes all - objects and access keys." The legacy `CONFIRMATION_REQUIRED` error is - unreachable, so the bucket mapper has no entry for it and - `meta.expectedConfirm` / `meta.receivedConfirm` are gone. `--yes` - never grants consent; a wrong typed answer is `CLI.PROMPT_INVALID`, - exit 2; cancelling with Ctrl-C or EOF settles `CLI.PROMPT_CANCELLED`, - exit 3 — an exit code this command never produced before. -33. **`bucket key delete` still has no confirmation.** The legacy - inconsistency ports unchanged: deleting a bucket needs consent, - revoking one of its keys does not. Recorded for review, not fixed. -34. **Fixture-only errors die with the fixture machinery.** - `BUCKET_NOT_FOUND`, `BUCKET_KEY_NOT_FOUND` and the bucket domain's - `BRANCH_NOT_FOUND` were raised only by the fixture provider. In real - mode the Management API's own code passes through as - `BUCKET.`, so none of the three has a v8 counterpart. -35. **`bucket key create --role` invalid values.** The engine's enum - parse failure replaces commander's choices error. The controller's - own defaulting is unchanged: any value that is not exactly `read`, - the omitted flag included, is `read_write`. -36. **`bucket key create` credentials are a masked field block.** The - four stdout lines are unchanged and exact — - `S3_ENDPOINT=`, `S3_ACCESS_KEY_ID=`, `S3_SECRET_ACCESS_KEY=`, - `S3_BUCKET=`, in that order — and the json `result` still carries the - secrets. The human card gains the same four values as field rows, - with `S3_ACCESS_KEY_ID` and `S3_SECRET_ACCESS_KEY` masked - (`sensitive: true`); the legacy human output named no values at all. -37. **`branch list` keeps its resolution quirk.** It declares no - `--project` flag and passes no command name to the resolver, so an - unbound directory still reads "…and this command will not choose - one…" and still lacks the retry-with-`--project` next step. The - fixture-mode branch of the legacy controller — which returned - `projectName: "not resolved"` and an empty list instead of erroring — - has no v8 counterpart. -38. **`git connect` no longer refuses every scripted run.** It was - ported declaring `needs: { interaction: true }`, which failed any - non-interactive invocation before the handler ran — including the - ones the legacy command completed happily, where the repository was - already connected or the app already installed. Operator ruling - 2026-08-11 removed the declaration: only the install wait needs a - person, and `prompt.browserWait` refuses a non-interactive session - on its own. Remaining divergence from legacy: where the legacy - command raised `REPO_INSTALLATION_REQUIRED` or `REPO_NOT_ACCESSIBLE` - with the install URL in `meta`, v8 settles the engine's - `CLI.INTERACTION_REQUIRED`, whose summary names the same URL and - whose next action says to finish there and rerun. - -39. **`git connect` and `git disconnect` keep their raw json result.** - Neither had a serializer, so `--json` still emits - `{ workspace, project, resolution, repositoryConnection }` — - resolution object included, unlike the bucket and branch results, - which strip or reshape. Unchanged; recorded for review. -40. **The legacy 401/403 → `AUTH_REQUIRED` mapping does not port.** The - engine settles every real credentials failure itself, so what still - reaches the git mapper is the permission residue of a returned 403. - It maps mechanically to `GIT.AUTH_REQUIRED` — D1's class entry 14, - same reasoning, including that entry's fix-text change: the offer - to rerun in a TTY to sign in interactively is dropped, so the fix - reads "Run prisma-cli auth login." -41. **`PROJECT_AMBIGUOUS`'s hardcoded `app deploy` next step ports - verbatim** for `bucket list`, `bucket create` and the git commands, as - it did in D1. A pre-existing quirk, recorded rather than fixed. -42. **Install-URL next steps become `open-url` actions.** The legacy - `REPO_INSTALLATION_REQUIRED` and `REPO_NOT_ACCESSIBLE` errors put the - raw install URL first in `nextSteps`, beside real commands. - Conventions §4's mechanical mapping would turn it into a - `run-command` whose command is a URL, which tells a consumer to - execute it. `NextAction` now has an `open-url` kind and a `url` - field, so the git mapper sends a `nextSteps` entry that is a URL to - `{ kind: "open-url", label: , url: }` and leaves - command strings on the `run-command` mapping. The URL text is - unchanged. -43. **The install wait moves onto the engine's browser-wait helper.** - The legacy handler opened the browser itself, wrote one wait line to - stderr and ran its own poll loop. `ctx.prompt.browserWait` now owns - all three: it emits one `endpoint` event carrying the wait sentence - and the install URL — which is what the single legacy stderr line - was — opens the browser, and polls on the engine clock. The handler - supplies only the URL, the message, the cadence and the question - being polled, and emits no events of its own. The poll question is - unchanged: re-list the workspace's GitHub App installations and look - for the repository in them. -44. **`opened` is dropped from the wait's terminal errors.** - `browserWait` does not report whether the browser actually opened. - Both legacy branches existed to make sure the user still had the - install URL when no browser opened, and the engine now always writes - the URL, so the distinction has no work left to do. - `GIT.REPO_INSTALLATION_REQUIRED` therefore always carries the - browser-opened fix text, "Finish installing the GitHub App in the - browser, then rerun prisma-cli git connect.", and both terminal - errors carry `meta: { repository, installUrl }` — the `opened` key - is gone. Which of the two errors is raised is unchanged: - `GIT.REPO_NOT_ACCESSIBLE` when at least one installation could be - inspected, `GIT.REPO_INSTALLATION_REQUIRED` otherwise. -45. **The poll cadence is unchanged but now belongs to the engine.** - `PRISMA_CLI_GITHUB_INSTALL_POLL_INTERVAL_MS` (default 2000) and - `PRISMA_CLI_GITHUB_INSTALL_TIMEOUT_MS` (default 120000) are still - read from the environment with the legacy positive-integer parsing — - a non-positive or unparseable value falls back to the default — and - are passed to `browserWait` as `interval` and `timeout`. The engine - raises `CLI.BROWSER_WAIT_TIMEOUT` when the timeout elapses; the - handler catches exactly that code and settles the legacy terminal - error in its place, so the timeout the user sees is unchanged. - Cancelling with Ctrl-C splits by timing: while the wait is sleeping - between polls — where a user spends almost all of a two-minute wait — - it settles `CLI.PROMPT_CANCELLED`, exit 3, where the legacy loop - aborted with the shell's `COMMAND_CANCELED`, exit 130. When a poll - request is already in flight the abort surfaces from the SDK client - instead, and the engine settles `CLI.ABORTED`, exit 130 — the legacy - exit code, unchanged. Both paths are tested. The split is accepted - rather than smoothed over: reshaping the second into the first would - mean catching the engine's own abort settlement inside the handler, - and the engine is a hard boundary for this slice (conventions §14). - -#### Error code maps - -| legacy code | v8 code | -| --- | --- | -| `USAGE_ERROR` (bucket domain) | `BUCKET.USAGE_ERROR` | -| `BUCKET_KEY_SECRET_MISSING` | `BUCKET.KEY_SECRET_MISSING` | -| `BUCKET_API_ERROR` | `BUCKET.API_ERROR` | -| raw API `error.code` X (bucket) | `BUCKET.X` | -| `CONFIRMATION_REQUIRED` (bucket) | unreachable — the engine's `CLI.CONSENT_REQUIRED` replaces it | -| `BUCKET_NOT_FOUND` / `BUCKET_KEY_NOT_FOUND` / `BRANCH_NOT_FOUND` (fixture only) | unreachable in v8 | -| `BRANCH_API_ERROR` | `BRANCH.API_ERROR` | -| raw API `error.code` X (branch) | `BRANCH.X` | -| `USAGE_ERROR` (project domain, raised by git) | `GIT.USAGE_ERROR` | -| `REPO_PROVIDER_UNSUPPORTED` | `GIT.REPO_PROVIDER_UNSUPPORTED` | -| `REPO_ALREADY_CONNECTED` | `GIT.REPO_ALREADY_CONNECTED` | -| `REPO_INSTALLATION_REQUIRED` | `GIT.REPO_INSTALLATION_REQUIRED` | -| `REPO_NOT_ACCESSIBLE` | `GIT.REPO_NOT_ACCESSIBLE` | -| `REPO_NOT_CONNECTED` | `GIT.REPO_NOT_CONNECTED` | -| `REPO_CONNECTION_FAILED` | `GIT.REPO_CONNECTION_FAILED` | -| `AUTH_REQUIRED` (403 residue, git) | `GIT.AUTH_REQUIRED` | -| raw API `error.code` X (git) | `GIT.X` | -| `USAGE_ERROR` "Workspace required" (auth domain) | `AUTH.USAGE_ERROR` | -| `PROJECT_NOT_FOUND` / `PROJECT_AMBIGUOUS` / `PROJECT_SETUP_REQUIRED` / `LOCAL_STATE_STALE` / `LOCAL_PROJECT_WORKSPACE_MISMATCH` | the project group's codes, mapped by the single source in `v8/project/errors.ts` | - -#### Conformance rows - -| command | inventory entry | rules applied | divergences | -| --- | --- | --- | --- | -| `bucket list` | `bucket list` | R-S2b-2, 5, 9, 10; d3 §3.1 | 1, 2, 5, 6, 7, 9, 18, 26, 28, 31, 41 | -| `bucket create` | `bucket create` | R-S2b-2, 5, 9, 10; d3 §3.2 | 1, 2, 5, 6, 7, 9, 18, 27, 28, 31, 34, 41 | -| `bucket delete` | `bucket delete` | R-S2b-2, 3, 5, 9, 10; d3 §3.3 | 1, 2, 5, 6, 7, 9, 31, 32, 34 | -| `bucket key list` | `bucket key list` | R-S2b-2, 5, 9, 10; d3 §3.4 | 1, 2, 5, 6, 7, 9, 26, 31, 34 | -| `bucket key create` | `bucket key create` | R-S2b-2, 4, 5, 9, 10; d3 §3.5 | 1, 2, 5, 6, 7, 9, 27, 31, 34, 35, 36 | -| `bucket key delete` | `bucket key delete` | R-S2b-2, 5, 9, 10; d3 §3.6 | 1, 2, 5, 6, 7, 9, 31, 33, 34 | -| `branch list` | `branch list` | R-S2b-2, 5, 9, 10; d3 §3.7 | 1, 2, 5, 6, 7, 9, 18, 26, 28, 31, 37 | -| `git connect` | `git connect` | R-S2b-2, 5, 7, 9, 10; d3 §3.8 | 1, 2, 5, 6, 7, 9, 18, 31, 38, 39, 40, 41, 42, 43, 44, 45 | -| `git disconnect` | `git disconnect` | R-S2b-2, 5, 9, 10; d3 §3.9 | 1, 2, 5, 6, 7, 9, 18, 31, 39, 40, 41 | - -#### Legacy tests deleted - -- `bucket.test.ts` and `branch.test.ts` in full. Every case in both - drove one of the seven ported bucket and branch commands through the - legacy shell, help cases included, and neither file held a provider or - adapter unit test. There is no bucket-provider unit test anywhere, so - nothing survived them to keep. -- `project.test.ts`: the six git connect and git disconnect fixture - cases. The project and env help cases stay — they belong to D1 and - still pass, and one of them also asserts the legacy shell's git help, - which lives until the shell is deleted in S2d. -- `project-real-mode.test.ts`: six cases in two passes. With the first - D3 commit went connecting through an installed GitHub App, the - already-connected-same-repository short-circuit, and disconnecting - through the source-repositories API. Once `git connect`'s wait landed, - three more followed: the non-interactive install intent when the - workspace has no GitHub App installation, the interactive wait that - connects after approval, and `REPO_NOT_ACCESSIBLE` when the App cannot - see the repository. All six are covered by `v8-git.test.ts`. - -Kept deliberately: `git-adapter.test.ts` (URL-parsing units for -`parseGitHubRepositoryUrl`, an operation-layer function v8 calls); -`branch-controller.test.ts` and `branch-usecases.test.ts` (unit tests for -the branch helpers, which survive as the operation layer — d3 §5's rule, -not the unported-group rule: `branch list` is ported and has a -conformance row); `read-branch.test.ts` and `local-branch.test.ts` -(unported, until S2d); -the two pagination-cursor-stall cases in `project-real-mode.test.ts`, -which cover `listScmInstallations` and `findRepositoryInInstallation`, -operation-layer functions v8 calls and does not otherwise cover; and the -one `project-real-mode.test.ts` case from the GitHub App install path, -"creates an install intent when the stored GitHub App installation is -unavailable". It drives a stored installation answering 422 and being -skipped inside `findRepositoryInInstallations`, an operation-layer -function v8 calls and does not otherwise exercise. Its three siblings -were held while `git connect`'s wait was unported and were deleted once -the wait landed and gave them v8 equivalents. - -## S2c — services (service, build, agent, feedback) - -Every known place where the S2c ports differ from the shipping -`prisma-cli`. Same entry format as `parity-divergences.md`; S2d -consolidates the per-slice files. The S1 whoami-scoped record and the -engine-global divergences (json framing, channel discipline, `--quiet` -as a log-level alias, dropped `--trace`, shared flag family, errored -settlements exit 2) apply to every command here and are not repeated. - -### Dispatch 1 — service group core (show, open, list-deploys, show-deploy, domain add/show/remove/retry/wait) - -#### The rename (R-S2c-1), one entry per command - -`app` ports as `service` — paths, ids, help, presenters, error copy, -flags, positionals. No alias; the legacy `app` spellings do not exist -in the v8 tree. - -| Legacy invocation | v8 invocation | Also renamed on this command | -| --- | --- | --- | -| `prisma-cli app show [app]` | `prisma-cli service show [service]` | `--app ` → `--service `; result field `app` → `service` | -| `prisma-cli app open [app]` | `prisma-cli service open [service]` | `--app` → `--service`; result field `app` → `service` | -| `prisma-cli app list-deploys [app]` | `prisma-cli service list-deploys [service]` | `--app` → `--service`; result field `app` → `service` | -| `prisma-cli app show-deploy ` | `prisma-cli service show-deploy ` | result field `app` → `service` | -| `prisma-cli app domain add [app]` | `prisma-cli service domain add [service]` | `--app` → `--service`; result fields `app`/`appId` → `service`/`serviceId` | -| `prisma-cli app domain show [app]` | `prisma-cli service domain show [service]` | same as domain add | -| `prisma-cli app domain remove [app]` | `prisma-cli service domain remove [service]` | same, plus consent question "Detach … from App …?" → "… from Service …?" | -| `prisma-cli app domain retry [app]` | `prisma-cli service domain retry [service]` | same as domain add | -| `prisma-cli app domain wait [app]` | `prisma-cli service domain wait [service]` | same as domain add | - -- Command ids follow: `app.domain.add` → `service.domain.add`, etc. -- Env override rename: `PRISMA_APP_ID` → `PRISMA_SERVICE_ID` (domain - target selection; `PRISMA_PROJECT_ID` unchanged). The legacy name is - NOT read in v8. -- NOT renamed: `prisma.compute.ts` keys (`app:`/`apps:` are - SDK-owned; rename needs @prisma/compute-sdk coordination — flagged - for the operator), and the shared local state file's internal keys - (`state.json`'s `app.selectedByProject` — the store is still shared - with the legacy shell until S2d). - -#### Error-code mapping (flat → dotted `SERVICE.*`) - -Every errored settlement exits 2 (engine rule; the legacy exit-1 -errors below change as a class). `fix` prose maps to a `user-choice` -nextAction — appended after the legacy typed `nextActions` when an -error carries both (e.g. `PROJECT_SETUP_REQUIRED`), so no advice is -lost; command-shaped `nextSteps` map to `run-command` nextActions -with the renamed `service` spelling. - -Rename inside ported error prose: command lines (`prisma-cli app …` → -`prisma-cli service …`) and the "app target" noun rename; -prose that names the SDK-owned config entries deliberately keeps -`app` — `defineComputeConfig({ app })` and -`ComputeConfigTargetUnknownError`'s "this config defines a single -app." refer to the `prisma.compute.ts` `app:`/`apps:` keys, which do -not rename until the compute-sdk coordination lands (decided, not an -accident of the substitution list). - -| Legacy flat code (exit) | v8 dotted code (exit) | Commands | -| --- | --- | --- | -| `USAGE_ERROR` (2) — named target without a config | `SERVICE.COMPUTE_CONFIG_TARGET_UNKNOWN` (2) | show, open, list-deploys, domain * | -| `COMPUTE_CONFIG_INVALID` (2) | `SERVICE.COMPUTE_CONFIG_INVALID` (2) | show, open, list-deploys, domain * | -| `COMPUTE_CONFIG_TARGET_UNKNOWN` (2) | `SERVICE.COMPUTE_CONFIG_TARGET_UNKNOWN` (2) | all with `[service]` | -| `USAGE_ERROR` (2) — unknown `--app`/saved selection | `SERVICE.SELECTION_INVALID` (2) | show, open, list-deploys, domain * | -| `USAGE_ERROR` (2) — "App selection required in non-interactive mode" | engine `CLI.PROMPT_REQUIRED` (2) | show, open, list-deploys, domain * (see picker entry) | -| `USAGE_ERROR` (2) — domain target has no app | `SERVICE.DOMAIN_TARGET_REQUIRED` (2) | domain * | -| `USAGE_ERROR` (2) — invalid `--timeout` | `SERVICE.TIMEOUT_INVALID` (2) | domain wait | -| `USAGE_ERROR` (2) — "Workspace required" | `SERVICE.WORKSPACE_REQUIRED` (2) | all platform commands | -| `PROJECT_NOT_FOUND` (1) | `SERVICE.PROJECT_NOT_FOUND` (2) | show, open, list-deploys, domain * | -| `PROJECT_AMBIGUOUS` (2) | `SERVICE.PROJECT_AMBIGUOUS` (2) | same | -| `PROJECT_SETUP_REQUIRED` (1) | `SERVICE.PROJECT_SETUP_REQUIRED` (2) | same | -| `LOCAL_STATE_STALE` (1) | `SERVICE.LOCAL_STATE_STALE` (2) | same | -| `LOCAL_PROJECT_WORKSPACE_MISMATCH` (1) | `SERVICE.LOCAL_PROJECT_WORKSPACE_MISMATCH` (2) | same | -| `NO_DEPLOYMENTS` (1) | `SERVICE.NO_DEPLOYMENTS` (2) | open, domain add | -| `FEATURE_UNAVAILABLE` (1) — no live URL | `SERVICE.FEATURE_UNAVAILABLE` (2) | open | -| `DEPLOYMENT_NOT_FOUND` (1) | `SERVICE.DEPLOYMENT_NOT_FOUND` (2) | show-deploy | -| `DEPLOY_FAILED` (1) | `SERVICE.DEPLOY_FAILED` (2) | all remote-listing failures | -| `BRANCH_NOT_DEPLOYABLE` (2) | `SERVICE.BRANCH_NOT_DEPLOYABLE` (2) | domain * | -| `DOMAIN_HOSTNAME_INVALID` (2) | `SERVICE.DOMAIN_HOSTNAME_INVALID` (2) | domain * | -| `DOMAIN_NOT_FOUND` (1) | `SERVICE.DOMAIN_NOT_FOUND` (2) | domain show/remove/retry/wait | -| `DOMAIN_ALREADY_REGISTERED` (1) | `SERVICE.DOMAIN_ALREADY_REGISTERED` (2) | domain add | -| `DOMAIN_QUOTA_EXCEEDED` (1) | `SERVICE.DOMAIN_QUOTA_EXCEEDED` (2) | domain add | -| `DOMAIN_DNS_NOT_CONFIGURED` (1) | `SERVICE.DOMAIN_DNS_NOT_CONFIGURED` (2) | domain add | -| `DOMAIN_RETRY_NOT_ELIGIBLE` (1) | `SERVICE.DOMAIN_RETRY_NOT_ELIGIBLE` (2) | domain retry | -| `DOMAIN_VERIFICATION_FAILED` (1) | `SERVICE.DOMAIN_VERIFICATION_FAILED` (2) | domain wait | -| `DOMAIN_VERIFICATION_TIMEOUT` (1) | `SERVICE.DOMAIN_VERIFICATION_TIMEOUT` (2) | domain wait | - -#### Auth (Q1 class) - -The service group's legacy commands never auto-logged-in -(`requireComputeAuth`); v8 keeps that: `needs.credentials` settles -unauthenticated runs with the engine's `CLI.CREDENTIALS_REQUIRED` -(exit 2) instead of the legacy `AUTH_REQUIRED` (exit 1). - -The workspace those commands then act in comes from the credential the -engine is authenticating with (`ctx.activeCredential()`), which is the -only sanctioned identity surface a handler has; no v8 command reads the -credential file itself. The entry below records what moving to it -fixed. - -#### The workspace comes from the engine, not the credential file - -`requireWorkspace` (`src/v8/service/target.ts`) used to call `readAuthState`, which builds a `FileTokenStorage` and asks it for tokens (`src/auth/operations.ts`). That legacy reader and the engine's credential manager resolve to the same file by default, and that file's shape is about to change. Today `auth login` writes the legacy `{tokens: […]}` shape through `storeLegacyCredential` and `FileTokenStorage` reads it. Once the auth rework merges down from `bot/s2a-foundations`, `auth login` calls `credentialManager.createSession` instead, which writes `{version, sessions, currentWorkspaceId}`; `@prisma/credentials-store` reads `data.tokens || []`, finds nothing, and the legacy reader reports nobody signed in while `credentialManager.currentSession()` still returns a valid session. - -**This entry used to say the merge-down broke 13 of this slice's 20 commands and that the fix belonged to the auth stream. The count was right; the blame was not, and the misplaced part was ours.** That count describes the slice as it stood before `service deploy` and `service build` were dropped and before `service logs` was shelved, when it had 20 commands; it is history, and so is the list. With no tokens `readAuthState` returned `{authenticated: false}` and the command settled `SERVICE.WORKSPACE_REQUIRED`, so a credential file the legacy reader cannot parse made `deploy`, `show`, `open`, `list-deploys`, `logs`, `promote`, `rollback`, `remove` and all five `domain` commands unusable. But no v8 command should have been reading auth state that way at all. The engine hands a handler its identity through the credential manager, whose reader understands both the new `{version, sessions, currentWorkspaceId}` shape and the legacy `{tokens: […]}` one (`src/auth/state-file.ts` adopts the legacy store on read). - -**`requireWorkspace` now reads `ctx.activeCredential()`. Of the 13 that broke, 11 still ship, and the fix repairs all 11.** `show`, `open`, `list-deploys`, `promote`, `rollback`, `remove` and all five `domain` commands resolve their workspace after the merge-down exactly as they do before it. The other two are gone from the slice: `deploy` is no longer a v8 command at all, and `logs` is shelved — both under dispatch 4. `show-deploy` was never affected: it is the one caller that swallows a workspace failure and degrades to a missing live-deployment hint. `build logs`, the three `agent` commands and `feedback` read no auth state at all. - -**A workspace with no name now shows its id.** `ActiveCredential.workspaceName` -is optional where the old `AuthWorkspace.name` was required, so a -session the manager could not name — a workspace-bound service token, -or a login whose best-effort name fetch failed — presents as its -workspace id (`workspace: ws_…`) instead of failing. Legacy asked the -API for the name on every read and settled `WORKSPACE_REQUIRED` when it -could not build a workspace at all; v8 prefers the identifier the user -can still act on. `SERVICE.WORKSPACE_REQUIRED` is still raised when -there is no credential at all, and it is now also raised when there is -a credential that names no workspace — see the escalated entry that -follows this one. - -**A credential that names no workspace is now refused instead of silently getting an empty one.** One commit before the merge-down, `ctx.session()` composed an environment credential's session as `workspaceId: serviceTokenWorkspaceId(token) ?? ""` (`src/auth/credential-manager.ts`), so a `PRISMA_SERVICE_TOKEN` whose claims name no workspace handed `requireWorkspace` `{id: "", name: ""}` and the run carried on: it filtered projects by an empty workspace id, found none, and named a blank workspace in the error it eventually produced. `ActiveCredential.workspaceId` is absent rather than empty in that case, `requireWorkspace` tests it, and the run settles `SERVICE.WORKSPACE_REQUIRED` instead. `tests/v8-service-session.test.ts` pins the refusal. - -**The tests now seed one credential source.** Every service test used to -mock `readAuthState` at the module seam while the engine's credential -check was seeded through the credential manager, so the harness had two -credential seams where production has one file — which is why nothing in -the suite could see any of this. Those mocks are gone: the harness seeds -a session on the credential manager and both the credentials check and -the workspace come from it. `tests/v8-service-session.test.ts` pins the -direction, seeding a session that names a workspace the Management API -fake never reports for the project, so a run taking its identity from -anywhere else resolves a different project or prints a different name. -The refusal above is pinned there too, from a seeded -`PRISMA_SERVICE_TOKEN` whose claims name no workspace. - -#### A service token whose workspace only the server knows is now refused (ESCALATED — engine gap) - -**What legacy did.** With `PRISMA_SERVICE_TOKEN` set, `readAuthState` handed off to `readServiceTokenAuthState` (`src/auth/operations.ts`), which asked the server first: `readCurrentPrincipalAuthState` read `GET /v1/me`, documented in the Management API types as returning the user, workspace and credential the current token represents. When the server named a workspace, legacy used it and the command ran, whatever the token's own claims said. Only when that call produced nothing did legacy decode the token, and only then — finding no workspace in it — did it return signed-out state, which the old `requireWorkspace` settled as `SERVICE.WORKSPACE_REQUIRED`. - -**What v8 does.** `requireWorkspace` reads `ctx.activeCredential()` and nothing else. For an environment credential the workspace id is `serviceTokenWorkspaceId(token)` (`src/auth/claims.ts`): the `workspace_id` claim, or a `sub` of the form `workspace:`. There is no network call, and a credential that names no workspace is refused with `SERVICE.WORKSPACE_REQUIRED` — the same error, from the same builder, that legacy raised on the same input. - -**The one case that differs.** A service token that the platform associates with a workspace, but whose JWT carries neither `workspace_id` nor a `sub` of the form `workspace:`, used to work whenever `/v1/me` answered and named that workspace. It is now refused. Every other input behaves as it did: legacy refused the same token when it could not reach `/v1/me`, when the response carried no principal or no credential, and when the principal named no workspace. The claims derivation is otherwise wider than legacy's, which read only the `sub` form, so this is the single direction in which the new path resolves less. The difference arrived with the rev-6 credential model rather than with any command in this slice, but this slice is where it becomes visible: every service command resolves its workspace this way. - -**The refusal's advice does not fit this case.** `SERVICE.WORKSPACE_REQUIRED` offers one next action, "Sign in" → `auth login`. Under `PRISMA_SERVICE_TOKEN` that cannot clear it: `createSession` writes the stored session but leaves the process pinned to the environment credential, so the next run resolves the same token and fails the same way until the variable is unset. Legacy's advice had the same hole, so this is not a regression — but the case is now reachable where before it produced an empty workspace. The wording is left to the auth stream, because the condition it fires on is an environment-credential state that stream owns. - -**Why this is not fixed here.** Restoring the lookup would mean a service command calling `/v1/me` to complete its own identity. `ctx.activeCredential()` is documented local-only and deliberately never touches the network, and a command reaching around the engine for its own auth state is the exact mistake that produced the defect the entry above records. If identity needs a server round-trip when the claims are insufficient, it belongs in the credential manager, which owns the credential and can do it once for every command — not in one group's `requireWorkspace`. - -**Ruling needed, from the operator and the auth stream:** whether the credential manager should complete an environment credential's workspace from the server when its claims carry none, or whether every token the platform issues is required to carry the claim, which closes this case in the token format instead. - -#### `service domain remove` consent - -Recorded with the group's other consent points in "Consent" under -dispatch 2 below — one table, one mechanism, for all three. - -#### Interactive service picker - -The legacy picker errored with per-command `USAGE_ERROR` copy in -non-interactive contexts and when a saved selection went stale -non-interactively. In v8 the engine prompt settles those runs with the -structural `CLI.PROMPT_REQUIRED` error (R-S2b-6); a stale saved -selection falls through to the picker in both modes. - -#### `service open` browser launch - -The legacy command opened the live URL whenever prompting was allowed -(TTY + not CI + not `--json`). v8 hands the URL to the engine's -`ctx.openUrl`, which announces it as an `endpoint` event and opens the -browser when the session is interactive. Differences from legacy: a -`--json` run in an interactive terminal now DOES open the browser -(legacy suppressed it because json implied non-interactive), a failed -open reports `opened: false` instead of raising, and the URL is also -printed as the stdout payload line (legacy printed nothing on stdout). - -#### `service domain wait` - -- Now a result command with engine `status` events (one per status - change, `from`/`status`/`data.domainId`/`data.elapsedMs`) instead of - the legacy streaming stderr lines / per-poll json events; json mode - frames each status change exactly once (legacy emitted an event per - poll cycle in json mode, including unchanged statuses). -- Success now settles with a result envelope - (`{…target, hostname, status: "active", liveUrl}`); legacy ended - with only the streaming success wrapper event. -- Poll interval still honors `PRISMA_CLI_DOMAIN_WAIT_POLL_MS` - (default 5s); `--timeout` grammar unchanged (default `15m`, `0` = - single check). - -#### Result shape changes (all commands) - -- `verboseContext` (the `--verbose` "Local context" block and json - field) is dropped — `--verbose` is a log-level alias in v8 and is - not otherwise retained. S2 ruling 8 drops `--trace` because log - levels cover it; the same reasoning covers `--verbose`, which that - ruling does not name. -- `service list-deploys` json result is the plain - `{projectId, service, deployments}` record; the legacy - `items`/`count` list-serializer wrapper does not port. -- Domain results drop the `branch.id` field (legacy emitted - `branch: {id, name, kind}` with `id` always `null` for domain - commands; v8 emits `branch: {name, kind}`). -- Human output: whoami-style summary + field rows (and a table for - list-deploys) on stderr; these commands write no stdout payload in - human mode except `service open`, which now prints the URL as its - stdout payload line (pipe-clean; legacy printed nothing on stdout). - Legacy opened every block with a present-progressive title ("Removing - the selected app."). In v8 that summary line is the only success - signal the engine prints, so a command that changed something ends on - a past-tense `ok` line instead ("Removed hello-world and every - deployment it owned."), and only the commands that merely report keep - the informational heading. - -#### Fixture mode - -Legacy app commands refused to run in fixture mode -(`FEATURE_UNAVAILABLE` via `ensurePreviewAppMode`). The v8 tree has no -fixture mode, so the refusal path does not port (fixture machinery -dies in S2d). - -### Dispatch 2 — promote, rollback, remove - -#### The rename (R-S2c-1), one entry per command - -| Legacy invocation | v8 invocation | Also renamed on this command | -| --- | --- | --- | -| `prisma-cli app promote [app]` | `prisma-cli service promote [service]` | `--app` → `--service`; result field `app` → `service`; error copy "App promote requires an existing app" → "Service promote requires an existing service" | -| `prisma-cli app rollback [app]` | `prisma-cli service rollback [service]` | same, plus "…requires an existing service" | -| `prisma-cli app remove [app]` | `prisma-cli service remove [service]` | same, plus the confirmation question "…app removal" → `Remove Service "" and every deployment it owns?` | - -Command ids follow: `app.promote` → `service.promote`, etc. - -#### Consent (Q5 class; operator-ruled 2026-08-10 and 2026-08-11, shipped) - -Consent is engine-owned and this is what ships: each consent point -declares a token — the natural noun of the action — so an interactive -session type-to-confirms it, and the engine's global repeatable -`--confirm ` grants it non-interactively when a supplied value -matches the token exactly (each value consumed once per run). No command -declares a consent flag of its own. `--yes` alone never grants consent; -`--yes` together with a matching `--confirm ` does, because it -takes the same non-interactive branch. - -| Command | Legacy grant | v8 grant | Token | -| --- | --- | --- | --- | -| `service remove` | typed app name on a TTY; `-y/--yes` skipped it; non-interactive without `--yes` → `CONFIRMATION_REQUIRED` (exit 1) | type the service name interactively, or `--confirm ` | the service name | -| `service domain remove` | `-y/--yes` skipped the yes/no confirm | type the hostname interactively, or `--confirm ` | the hostname | -| `service rollback` | none — the command asked nothing and rolled production back | type the target deployment's id interactively, or `--confirm ` | the target deployment's id | - -`service rollback`'s consent is new in v8, not a ported one: this is an operator ruling of 2026-08-11, and it answers the follow-up this file used to carry under "`service promote` / `service rollback`". The shipping CLI changes what production serves and asks nothing at all. **The token is the target deployment's id, not the service name.** The hazard the command carries is promoting the wrong deployment, and typing the service name would not make anyone look at which deployment is about to go live; typing `dep_123` does. The question names both — `Roll back Service "hello-world" to deployment dep_1 and make it live?` — and is asked after the target is resolved, because a user cannot consent to a deployment id the command has not chosen yet. It is asked on both paths: an explicit `--to`, and the resolved default. It is also asked when the target turns out to be the deployment already live, which is the one place a caller sees a behaviour change beyond the consent itself: `service rollback --to ` used to complete with a warning in a non-interactive run and now needs `--confirm ` to reach that same warning. - -`service deploy`'s production replace was a consent point too, until deploy was dropped (see "`app deploy` and `app build` are dropped" under dispatch 4). The mechanism below is unchanged by its removal. - -Transitions, identical on all three: - -- **Granted** interactively by typing the token, non-interactively by - `--confirm `. `--confirm` never SKIPS an interactive prompt: an - interactive session always type-to-confirms, whether or not the flag - was passed. It is a non-interactive affordance only. -- **Wrong token typed interactively**: the engine's structural consent - mismatch, exit 2. Legacy re-asked a bad yes/no answer and treated an - explicit "no" as a cancellation, so what used to be a decline is now a - mismatch — there is no longer a "no" to give. -- **Wrong or missing `--confirm` value non-interactively** (including - under `--yes`): `CLI.CONSENT_REQUIRED`, exit 2, naming the expected - value and carrying it as `meta.consentToken`. Legacy's - `CONFIRMATION_REQUIRED` exited 1 (ledger Q5). - -#### Error-code mapping (dispatch 2 additions) - -| Legacy flat code (exit) | v8 dotted code (exit) | Commands | -| --- | --- | --- | -| `DEPLOY_FAILED` (1) | `SERVICE.DEPLOY_FAILED` (2) | promote, rollback | -| `REMOVE_FAILED` (1) | `SERVICE.REMOVE_FAILED` (2) | remove | -| `NO_PREVIOUS_DEPLOYMENT` (1) | `SERVICE.NO_PREVIOUS_DEPLOYMENT` (2) | rollback | -| `DEPLOYMENT_NOT_FOUND` (1) | `SERVICE.DEPLOYMENT_NOT_FOUND` (2) | promote, rollback | -| `USAGE_ERROR` (2) — "App promote/rollback/remove requires an existing app" | `SERVICE.TARGET_REQUIRED` (2) | promote, rollback, remove | -| `USAGE_ERROR` (2) — empty `--branch` | `SERVICE.BRANCH_INVALID` (2) | remove | -| *(no legacy code — legacy promoted the newest deployment and reported success)* | `SERVICE.LIVE_DEPLOYMENT_UNKNOWN` (2) | rollback (see the ruling below) | - -The deploy-only rows this table used to carry went with the command; see "`app deploy` and `app build` are dropped" under dispatch 4. Two of those codes survive because a read command still raises them, and they keep their dispatch 1 rows: `SERVICE.PROJECT_SETUP_REQUIRED`, which still carries the candidate list and the suggested project name in `meta` exactly as legacy did, and `SERVICE.LOCAL_STATE_STALE`. - -#### `--no-db` cannot be told apart from "not passed" (RETIRED — was an escalated engine gap) - -Retired: this was escalated to the operator as an engine gap and became moot when `service deploy` was dropped, because `--db` was a deploy flag and no shipped command declares it. Kept here so the escalation list reads honestly — seven engine gaps went to the operator during this slice, three are now retired (this one, the `prompt.text` validator below, and the log-stream token under dispatch 3), and four are still open: the service token whose workspace only the server knows under dispatch 1, the `build logs` exit code under dispatch 3, and the `agent` group's help examples and the crash-recovery feedback action under dispatch 4. The dispatch 1 gap is the newest: it arrived with the rev-6 credential merge-down, after the drop ruling under dispatch 4 counted the open ones. All seven are marked where they are written, so the count can be checked against the entries. - -The engine's boolean flag is two-state with an automatic `--no-` -negation and a `false` default, so the legacy tri-state (`--db` request / -`--no-db` opt out / absent = prompt when a database signal is found) was -not expressible. v8 deploy shipped `--db` as the explicit request; both -absent and `--no-db` took the signal-driven prompt path, whose default -answer is No (so a non-interactive `--no-db` still skipped setup). The -legacy "passing both → USAGE_ERROR" check disappeared with the flag pair, -and so did "Database setup requires --yes in non-interactive mode" — -`--db` was itself the explicit request. The ask was smaller than a new -flag type: the engine already computes the missing fact at parse time and -sends it somewhere else. `explicitFlagKeys` -(`packages/cli-engine/src/execution/command-snapshot.ts`) scans argv for -which flag names appear and deliberately marks the base flag when it sees -a `--no-` token; `buildCommandSnapshot` then labels every declared -flag `source: "cli"` or `source: "default"`. Together with the parsed -boolean the handler already receives, that settles all three states: -`default` means absent, `cli` with `true` means `--db`, `cli` with -`false` means `--no-db`. The snapshot goes only to `RunHooks.onSettled`, -after the run, for telemetry; it never reaches `CommandContext`. So what -parity needed was an accessor that hands the handler a fact the engine -already holds — not a declarable tri-state boolean with its own negation -rules. Any future command that wants a three-way boolean will hit this -again. - -#### `prompt.text` has no validator and no re-ask (RETIRED — was an escalated engine gap) - -Retired: this was escalated to the operator as an engine gap and became moot when `service deploy` was dropped, because the first-deploy Project setup prompt was the only place in the slice that needed a validated text answer. No shipped command calls `prompt.text` with a value it must validate. - -Legacy passed a `validate` function to the clack text prompt -(`lib/project/interactive-setup.ts`), so an invalid Project name was -re-asked in place and the deploy continued. The engine's `prompt.text` -takes only `placeholder` and `default` — no validator, no re-ask — so v8 -deploy validated the answer afterwards and settled the whole command with -`SERVICE.PROJECT_NAME_INVALID` (exit 2). A user who typo'd during -first-deploy setup lost the run and reran deploy. The ask was a prompt -validator, or a re-ask affordance, on `prompt.text`; the next command that -takes a constrained text answer will need it. - -#### `service promote` / `service rollback` - -- The already-live short-circuit is unchanged, but the legacy `warnings` - array becomes an engine warn diagnostic - (`SERVICE.DEPLOYMENT_ALREADY_LIVE`), and no promote call or step events - are emitted in that case. -- The SDK's promote progress lines become `status` events for the target - deployment (`starting` → `start-requested` → the SDK's own status values → - `running` → `promoting` → `promoted`) plus an `endpoint` event for the - promoted URL, bracketed by a `promote` / `rollback` step. -- **`service rollback` now asks for consent** — this entry used to record - that it did not, and flagged for the operator that rollback was the - obvious next consent point with the target deployment id as its token. - The operator ruled on it on 2026-08-11 and that is what ships: rollback - is the group's third consent point, on exactly that token. Nothing is - left open — see the consent entry above for the wording, the ordering - and what changes for a non-interactive caller. - -#### `service rollback` refuses to guess which deployment is live (operator ruling, 2026-08-11) - -The second of the two rollback rulings, and like the consent it is a deliberate divergence rather than a port decision. - -**What the shipping CLI does.** Without `--to`, rollback picks "the newest deployment that is not the live one" — `deployments.find((deployment) => deployment.id !== currentLiveDeploymentId)` (`src/controllers/app.ts`, `resolveRollbackTarget`). `resolveCurrentLiveDeploymentId` returns `null` when the service record, the platform's deployment listing and the local cache all fail to name a live deployment, and against `null` that predicate is true for every deployment. So the command takes the newest one — most likely the deployment already live — promotes it, and reports a successful rollback. The user is told production was rolled back when nothing moved, and the local cache is then written with that guess. - -**What v8 does.** `resolveRollbackTarget` (`src/v8/service/release.ts`) refuses instead: with no `--to` and no identifiable live deployment it raises the new `SERVICE.LIVE_DEPLOYMENT_UNKNOWN` (exit 2), whose `why` says nothing names a live deployment and whose two typed next actions are `service rollback --to ` and `service list-deploys`. The refusal lands before the consent prompt, before any promote call and before any local state write. - -The scope is exactly the ambiguous case: - -- **`--to` given** — the user named the target, so there is nothing to resolve. Unchanged, including when the live deployment is unknown: the run promotes the named deployment and reports `previousLiveDeploymentId: null`, which the human presentation already renders as "unknown". -- **No `--to`, live deployment known** — unchanged: the newest deployment that is not the live one. -- **No `--to`, live deployment unknown** — the new refusal. -- **No deployments at all** — still `SERVICE.NO_PREVIOUS_DEPLOYMENT`, which is checked first. An empty listing has no live deployment either, but "there is no earlier deployment" is the more useful of the two answers, and the new error's advice — name a deployment, list them — would point at an empty list. - -#### `service remove` - -- The SDK's internal teardown polling becomes `progress` events - (`stop-deployments`, `delete-deployments`, each with completed/total) and - a `status` event (`removing` → `deleted`), bracketed by a `remove` step. - This required an additive `progress` pass-through on the operation layer's - `removeApp` (`packages/cli/src/lib/app/app-provider.ts`); legacy callers - are unaffected. -- Local state cleanup failures become warn diagnostics - (`SERVICE.LOCAL_STATE_CLEANUP_FAILED`) instead of the legacy `warnings` - array; the removal still succeeds. - -#### Result shape changes (dispatch 2) - -- `verboseContext` is dropped on all three commands (S2 ruling 8, as recorded - for D1). -- Result field `app` → `service` on every result. -- Legacy `warnings` (promote's already-live note, remove's cleanup failures) - become engine diagnostics on the completed envelope. - - -### Dispatch 3 — the log stream (`build logs`) - -#### The rename (R-S2c-1) does not reach this command - -`build logs ` keeps its spelling; its command id is `build.logs` -and its errors move into the `BUILD.*` namespace. - -This dispatch also ported `app logs` as `service logs`. That command is shelved and does not ship — see "`service logs` is shelved" under dispatch 4 — so the entries describing it are gone from this file, and the entries it shared with `build logs` now describe `build logs` alone. - -#### Records become engine events (R-S2c-2) - -`build logs` is a session command. Every record becomes an `output` -event, and the channel decides where human mode writes it: a record is -`diagnostic` when its source is `stderr` or its level is `error`, `data` -otherwise — the legacy routing exactly. A terminal record whose code is -not `end` (e.g. `no_logs`) is a `diagnostic` line, as legacy did. - -Json mode: the engine frames one event per record and terminates with -exactly one result frame. Legacy `build logs` set -`emitJsonSuccessEvent: false` so its json stream had NO wrapper event; -that opt-out does not port — the engine's framing is uniform, so a -completed `build logs` now ends with a result frame. Each record's own -frame is an `output` frame with the engine's envelope shape -(`{kind, source, channel, line, commandId, timestamp}`) instead of the -legacy `{type, command, timestamp, data}` shape. - -The record's own fields ride in the event's free-form `data`, so a json -consumer keeps everything legacy published per record: `cursor`, -`level`, `source` and `step` on a log record, and `kind`, `cursor`, -`code` and `retryable` on a reported terminal record (a `no_logs` end, -any error terminal). - -Two json-surface losses remain, both because the engine owns rendering -and a handler cannot see the format: - -- **The normal terminal record is no longer framed.** Legacy framed - every record in json mode, including the terminal `end` that human - mode printed nothing for; v8 emits no event for it. What a consumer - loses: on a `build logs` run whose build produced no log records at - all, `--json` now reports no cursor anywhere, so there is nothing to - pass to `--cursor` on the next run. On a run that produced log records - the last record's own cursor is the resume point, so nothing is lost - there. Carrying it needs an engine event kind that is framed in json - and silent in human mode; the only such kind is `remediation`, which - carries a `NextAction` and means something else. -- **The header is framed too.** Legacy wrote its header only when - neither `--json` nor `--quiet` was set. `--quiet` still hides it in - v8 — it is `diagnostic` output, whose display severity is `info`, - and `--quiet` is a log-level alias — but `--json` does not, because a - handler cannot read the format and must not branch on it. A json - consumer therefore reads one extra `output` frame before the records - ("Streaming logs for build "). - -`build logs` defaults to json when stdout is not a TTY (engine -auto-format), where legacy defaulted to human text unless `--json` was -passed. - -#### `build logs`: a failed build cannot exit 1 (ESCALATED — engine gap) - -Legacy set `process.exitCode = 1` on a terminal `error` record and let -the stream close normally: the logs printed, and the CLI reported the -build's failure through the exit code. The engine has no equivalent — -a session command returns `Result` and carries no exit-code set, -and documented exit codes are constrained to 4–99 -(`packages/cli-engine/src/execution/command-tree.ts` validateExitCodes), -so exit 1 is reachable only through the engine's own internal-error -path. v8 therefore streams every record and then settles the run as an -errored envelope, `BUILD.FAILED` (exit 2), carrying the terminal -record's message, code, retryable flag and cursor, plus a -`build logs --cursor ` resume action. - -The failure is still reported and still non-zero, but the code changes -1 → 2 and the settlement is an error rather than a clean close. Ruling -needed: either the engine grows a stream termination status (or allows -a documented exit 1), or `build logs` becomes a result command with a -documented code in 4–99. One line in `src/v8/build/logs.ts` changes -either way. - -#### `service logs`: the log stream has no sanctioned token (RETIRED — was an escalated engine gap) - -Retired: this was escalated to the operator as an engine gap and became unreachable when `service logs` was shelved, because no shipped command asks for a raw token. It is the gap the shelve waits on, so the description below stays as the statement of what the engine has to grow before the command can be ported — see "`service logs` is shelved" under dispatch 4. - -Everything from here to the end of the entry describes the base this slice was written against, before the rev-6 credential merge-down; it is kept as the statement of the ask, not as a description of the tree today. - -The log stream did not go through the Management API client: it opened -its own connection and needed the raw access token (legacy built one -from `PRISMA_SERVICE_TOKEN` or the token file in -`createPreviewLogAuthOptions`). On that base the only accessor that -reached a session command at all was `ctx.getCredentials()`, which the -engine already documented as staged for deletion: - -- `ctx.session()` — the accessor that named the workspace, which the - rev-6 model later replaced with `ctx.activeCredential()`, never - alongside it — deliberately omitted the token. The engine's comment - on it read "The token is INTERNAL" at the time; that sentence is gone - from `credential-manager.ts` now, and the rule it stated survives as - "Carries no token material". -- `ctx.credentialManager` (whose `tokenStorage()` was marked - engine-facing) was exposed only to result commands that declared - `managesCredentials`. -- `ctx.getCredentials()` forwarded straight to - `runtime.getCredentials()` and never consulted the credential - manager. The shipping bin wired `makeGetCredentials(proc.env)`, which - returned `PRISMA_SERVICE_TOKEN` when it was set and otherwise - whatever `FileTokenStorage` read out of the credential file — the - same two sources, in the same order, that legacy used. - -v8 asked `ctx.getCredentials()` and, when it resolved nothing, settled with `SERVICE.LOG_STREAM_CREDENTIALS_UNAVAILABLE`; that error builder is deleted with the command. Whether it ever fired was decided by the shape of the credential file rather than by whether the user was signed in, which is the trap this entry existed to record. On that base `auth login` wrote the legacy `{tokens: […]}` shape through `storeLegacyCredential` and `FileTokenStorage` read it, so the error was unreachable. Once the auth rework merged down from `bot/s2a-foundations`, `auth login` called `credentialManager.createSession` instead, `@prisma/credentials-store` read `data.tokens || []` and found nothing, and every signed-in user who had not set `PRISMA_SERVICE_TOKEN` would have hit it. The workspace half of the same problem was real for the commands that do ship, and it is fixed — see "The workspace comes from the engine, not the credential file" under dispatch 1. - -A second, smaller engine ask retires with this one, and it is why the `service logs` tests were red. Those tests seeded `rawTokenSeed`, which selected `createTestCli`'s manager-less runtime — the only way the harness made `ctx.getCredentials()` resolve a token. A manager-less runtime had no session at all, so once the workspace came from `ctx.session()` every one of those runs settled `SERVICE.WORKSPACE_REQUIRED`. The shipping bin wired a credential manager and `getCredentials` together (`src/v8/runtime.ts`), but `createTestCli` rejected that combination (`packages/cli-engine/src/testing.ts`), so no harness could model the runtime the product assembled. The tests are deleted with the command and the seed is gone from the testkit; whatever transport the engine grows for the ported command will need a harness seam of its own. - -**Settled by the merge-down.** The runtime this whole entry describes no longer exists: the rev-6 credential surface has landed here and **deleted `getCredentials` outright**, so there is now no accessor a command could take a token from, and `ctx.session()` is `ctx.activeCredential()`. Shelving the command was therefore the only correct call rather than a cautious one — had it shipped, it would now fail to compile rather than merely fail at runtime. The harness inconsistency retires with the accessor it was about. - -#### Error-code mapping (dispatch 3 additions) - -| Legacy flat code (exit) | v8 dotted code (exit) | Commands | -| --- | --- | --- | -| `BUILD_NOT_FOUND` (1) | `BUILD.NOT_FOUND` (2) | build logs | -| `BUILD_LOGS_FAILED` (1) | `BUILD.LOGS_FAILED` (2) | build logs | -| *(exit code 1, no error)* | `BUILD.FAILED` (2) | build logs (see the gap above) | - -#### `service open`'s announced URL - -`ctx.openUrl` carries one string that is both the human label and the -endpoint event's `name`, so the slug `live-url` became the human phrase -`Live URL`. The json `endpoint.name` changes with it; endpoint events -are a v8-only surface (legacy emitted none), so nothing that shipped -depends on the old spelling. - - -### Dispatch 4 — agent, feedback, closure - -No rename applies here: R-S2c-1 covers the `app` group only, so -`agent install|update|status` and `feedback` keep their legacy -spellings, flags, positionals and result records. Command ids are -`agent.install`, `agent.update`, `agent.status` and `feedback`. -Neither group touches the Management API or declares -`needs.credentials`, so the Q1 auth class does not apply to them. - -#### Error-code mapping (flat → dotted) - -| Legacy flat code (exit) | v8 dotted code (exit) | Commands | -| --- | --- | --- | -| `AGENT_SKILLS_INSTALL_FAILED` (1) | `AGENT.SKILLS_INSTALL_FAILED` (2) | agent install, agent update | -| `USAGE_ERROR` (2) — empty message | `FEEDBACK.MESSAGE_REQUIRED` (2) | feedback | -| `USAGE_ERROR` (2) — message over 4000 characters | `FEEDBACK.MESSAGE_TOO_LONG` (2) | feedback | -| `USAGE_ERROR` (2) — malformed or over-long `--email` | `FEEDBACK.EMAIL_INVALID` (2) | feedback | -| `FEEDBACK_SEND_FAILED` (1) | `FEEDBACK.SEND_FAILED` (2) | feedback | - -The engine validates neither string length nor pattern, so the three -`FEEDBACK.*` argument checks stay hand-rolled in the handler, with the -legacy limits, the legacy order, and the same refusal before any -network call. The one argument failure the engine owns is a missing -``, which settles as its own usage error -(`CLI.INVALID_ARGUMENTS`, exit 2). - -`agent status` has no error path at all, in legacy or in v8: a skills -CLI that cannot be read degrades to a warning (below), never to a -failed run. - -#### `feedback`'s json output: the envelope reshape, and nothing command-specific - -An earlier draft of this entry claimed `feedback` gained a json envelope -it never had, because it registered no `renderJson` serializer. That was -wrong, and the correction matters for anyone reading this file to judge -parity. Legacy's `runCommand` writes a full envelope for every command -and consults the serializer only for the `result` field — -`result: presenter.renderJson ? presenter.renderJson(success.result) : -success.result` (`packages/cli/src/shell/command-runner.ts:110-116`). -With no serializer, `result` simply carried the raw result object, which -for this command is what a serializer would have produced anyway. - -So `feedback` has no command-specific json divergence. Its `--json` -output changes exactly as every other ported command's does, through the -engine-global envelope reshape this file's preamble already covers -(`{ok, command, result, warnings, nextSteps, nextActions}` becomes -`{ok, commandId, result, exitCode, diagnostics, nextActions}`). The -`result` payload itself is unchanged: `{id, email, context: {cliVersion, -nodeVersion, platform, arch}}`. The submitted payload, the 3-second -timeout, the `PRISMA_CLI_FEEDBACK_URL` override (read from `ctx.env`) -and the default endpoint are all unchanged. - -#### `agent install` / `agent update` - -- Legacy's single `nextSteps` line ("Run … to verify the installed - Prisma skills.") becomes the `run-command` nextAction "Verify the - installed Prisma skills", carrying the same package-manager-aware - command string. A `--dry-run` still offers nothing, as legacy did. -- The install failure keeps the installer's own command line, now as a - typed `run-command` nextAction ("Retry the installer directly") - instead of a free-text `nextSteps` entry plus the separate fix "Run - the command below to retry the installer directly." The legacy - `debug` field (the installer's stack) disappears with `--trace` - (engine-global divergence). -- Flags, defaults and the built installer command line are unchanged, - including `--copy` forced on Windows, `--all-agents` sending - `--agent *`, and the package manager detected from the project. -- Human output is the engine's summary line plus field rows instead of - the legacy rail-drawn block. Neither writes a stdout payload. Since the - engine-colour slice the field rows align and their keys carry the accent - colour, byte-equal to the legacy `renderFieldRows`; the rail itself is - available as `fields.rail` but not yet set here. -- **Help examples lose the package runner, and the command now spells itself two ways (ESCALATED — engine gap).** Legacy rendered the `agent` group's examples through the project's own runner (`resolvePrismaCliPackageCommandFormatterSync`), so help read `pnpm dlx @prisma/cli@latest agent install`. The operator ruling of 2026-08-09 on the engine interface says examples are written without the binary name — the engine substitutes `{bin}`, or prepends the CLI name to an example that carries none (`assets/engine/engine-interface-draft.ts`, `HelpSpec.examples`) — so the ported examples are bare (`agent install`). The engine has no way to express the old form: examples are static strings resolved at definition time, and the runner is discovered from the filesystem at run time. The visible consequence is that one command now names itself two ways — help says `agent install`, while the same command's own next action still carries the package-runner form `npx -y @prisma/cli@latest agent status`, because next actions are built at run time and keep legacy's string. Worth settling once, group-wide, alongside the same question for every other ported group; nothing here should diverge on its own. - -#### `agent status` - -- Legacy's `warnings` array becomes an engine warn diagnostic, - `AGENT.SKILLS_LIST_UNAVAILABLE`, with the same sentence (including - the project-scope "Falling back to skills-lock.json"). The run still - completes with exit 0 and still reports `statusSource` as - `skills-lock` or `unavailable`. -- Legacy's `nextSteps` line ("Run … to install or refresh Prisma - skills.") becomes the `run-command` nextAction "Install or refresh - Prisma skills" with the same command string, offered on the same - condition (no skills installed). -- The result record is unchanged field for field. Human output is a - summary line, field rows and a skills table instead of the legacy - rail-drawn block. Since the engine-colour slice the rows and the table - align and carry the accent colour; only the rail is still absent, and - it is now a per-command opt-in rather than a missing capability. - -#### `app run` is dropped (operator ruling, 2026-08-10) - -`prisma-cli app run` has no v8 counterpart and is not coming back: -Composer's commands supersede it. This is a ruled drop, not a -deferral. There is no `service run` port, and S2d needs no -legacy carve-out, because deleting the commander shell deletes the -command with it. (This entry also said there was no engine mechanism -for passing a child process's exit code through, which is what ledger -Q2 asked about. True when written; S3 built one for composer's -converge — `ctx.spawn` and the `exitWithChildStatus` settlement. The -drop stands, and Q2 stays closed by it: the mechanism now exists and -the command still does not.) Anyone running a local dev server through -`prisma-cli app run` moves to Composer. - -#### `app deploy` and `app build` are dropped (operator ruling, 2026-08-10) - -`prisma-cli app deploy` and `prisma-cli app build` have no v8 counterpart. Composer supersedes both. Like `app run`, this is a ruled drop and not a deferral: neither command will be ported as it stands, so there is no `service deploy` and no `service build`, and this ruling took the slice from 20 commands to 18. (The `service logs` shelve below then took it to 17, which is what ships.) - -The reasoning is about the shape of the command, not about how the port went. `app deploy` conflates two different jobs — compiling the service on the developer's machine, and uploading the resulting tarball to the platform — and that shape is wrong. Future commands are to work directly with platform Compute resources instead of shipping a locally built archive. `app build` is the local-compiling half of the same job, so it goes with it. - -Nobody loses a command today. The legacy commander shell still serves `app deploy` and `app build`, and keeps serving them until S2d deletes the shell. What that deletion replaces them with is a Composer question, not a port question, so unlike `app run` this drop does leave something for S2d to answer. - -Two engine gaps escalated during this slice existed only for `app deploy` and are retired with it: the `--db` / `--no-db` three-way flag problem, and the missing validator on `prompt.text`. Both are recorded as retired entries under dispatch 2, so this ruling took the open escalations from six to four (the `service logs` shelve below then took them to three; the rev-6 credential merge-down later added a seventh gap, open, under dispatch 1). The consent table under dispatch 2 loses `service deploy`'s production replace and is down to two consent points (the 2026-08-11 ruling on `service rollback` later made it three again). The dispatch 1 and dispatch 2 divergence entries that described only these two commands are gone, and the entries that covered several commands now name only the ones that ship. - -The tap this slice added to legacy code for `service build` is reverted. `executeAppBuild` and `resolveAppBuildStrategy` (`packages/cli/src/lib/app/build.ts`) had gained an optional `io` parameter so the v8 command could stream the bundler's per-line output as engine events; nothing in the legacy shell ever passed it, so the parameter is removed and the file is back to what it was. - -#### `service logs` is shelved (operator ruling, 2026-08-10) - -`prisma-cli service logs` does not ship in this slice. This is a shelve, not a drop: unlike `app deploy`, the command is coming back in the shape it has, as soon as the engine can carry the connection it needs. Nothing about the command is wrong; the engine cannot yet transport it. The slice ships 17 commands. - -The reason is the transport. The log endpoint (`/v1/deployments/{deploymentId}/logs`) is an HTTP request that upgrades to a **WebSocket**, so the compute SDK opens its own socket and sets an `Authorization` header on the upgrade. The engine's API client is HTTP-only and cannot open or authenticate a socket, which is why the ported command reached for a raw token through `ctx.getCredentials()` — and the ruled credential design says commands never receive credentials. Porting it correctly therefore waits on the engine owning authenticated WebSocket transport. The operator has ruled that engine work into a later slice, and the orchestrator is writing its design now; when it lands, the command returns as it stands, with its handler asking the engine for a stream instead of asking for a token. - -Two facts about the endpoint belong in the record, because whatever the engine grows has to serve them. The endpoint is marked **experimental** in the Management API specification, so its shape is not yet a stable contract. And the stream ends after ten minutes: continuing means reconnecting with the cursor the stream last reported, so a long tail is a sequence of connections, not one. - -What went with the command: `src/v8/service/logs.ts` and `tests/v8-service-logs.test.ts`; its mount in `src/v8/cli.ts`; the `SERVICE.LOG_STREAM_CREDENTIALS_UNAVAILABLE` error builder and the two `SERVICE.DEPLOYMENT_NOT_FOUND` variants only it raised (a deployment with no service, and a deployment outside the resolved project); `getCredentials` on the service commands' `ServiceContext`, which no shipped command now needs; and the read flow's `skipSelectionWhenUnnamed` / `namedService` pair, which existed only so a bare `--deployment ` could skip the service picker. The escalated log-stream token gap is retired with it (dispatch 3), and so is the smaller harness ask that kept its tests red. The legacy `prisma-cli app logs` still ships and still streams, until S2d deletes the commander shell. - -#### Surviving commands no longer suggest a follow-up command - -Ten typed next actions across the shipped commands told the user to run `service deploy`, which the binary has not answered to since `app deploy` was dropped. They are removed. The errors and results keep their explanation and lose the action, so an empty `nextActions` array is now a normal outcome — `service show` on a project with nothing deployed, `service list-deploys` with an empty listing, and a failed deployment listing all offer nothing to run. - -The removals: `SERVICE.NO_DEPLOYMENTS`, `SERVICE.TARGET_REQUIRED` and `SERVICE.NO_PREVIOUS_DEPLOYMENT` lose "Deploy the service"; `SERVICE.DOMAIN_TARGET_REQUIRED`, the `PRISMA_SERVICE_ID` selection error and the domain-add 422 lose "Deploy to production"; `service list-deploys`'s own `SERVICE.DEPLOY_FAILED` loses the single action it carried; and the `service show`, `service list-deploys` and `service remove` presentations lose theirs. All ten are pinned by tests asserting the surviving actions exactly. - -One of the ten needed a replacement rather than a straight removal. The domain-add 422 carried two legacy next steps in order — deploy to production, then rerun `domain add` — plus the `fix` line "Deploy the app to the production branch, then rerun the domain command." Removing the first step left the second one telling the user to rerun the command that had just failed, and the `fix` line had never been carried across as the advice action this file's preamble says every `fix` becomes. The advice is now carried, worded for the commands v8 has: "Promote a deployment on the service's production branch, then add the domain again." - -They can come back pointing at Composer once those commands exist. Nothing about the underlying situation changed — a user with no deployment still has to deploy something — so this is a loss of guidance, not of capability. - -#### The crash-recovery feedback action does not port (ESCALATED — engine gap) - -Legacy pre-filled a bug report on every unexpected error. The shell -caught the crash, built `prisma-cli feedback " crashed: -"` (`src/shell/output.ts:104`), and shipped -it twice: as a human next-step line and, under `--json`, as a typed -`recover` nextAction inside the `UNEXPECTED_ERROR` envelope -(`src/shell/output.ts:120`, wired at `src/cli.ts:72,86`). The -inventory records this under `feedback`, and the S2c contract asks the -v8 shell to keep an equivalent. - -It cannot, on the current engine. The engine settles unexpected -failures itself: `settleBug` -(`packages/cli-engine/src/execution/settlement.ts`) emits -`CLI.INTERNAL_ERROR` with `nextActions: []` written into the envelope -literally, and `settleUnhandled` does the same for framework-level -failures. The only seam a bin may attach is `CliRunHooks.onSettled`, -which receives a `RunSummary` of `{commandId, exitCode, durationMs, -snapshot}` — no error object, no message — and which fires after the -envelope has already been written. Nothing reachable from the shell -ever sees the crash. - -So a v8 crash is the engine's own `CLI.INTERNAL_ERROR` envelope (exit -1) with no recovery action: the user is not offered the pre-filled -report, and an agent gets no `recover` action to run. The command it -would have pointed at (`feedback`) is ported and works; only the -automatic pre-fill is gone. - -Ruling needed, and the affordance is small. The engine would need an -internal-error contribution point — for example -`createCli({onInternalError: (context: {commandId, error}) => readonly -NextAction[]})`, or the same as a `CliRunHooks` member — called from -`settleBug` and `settleUnhandled` before the envelope is emitted, with -the returned actions merged into `nextActions`. The shell would then -supply exactly the legacy action, in both human and json mode. - -No partial version is worth shipping in the meantime. The bin can see -only the run's exit code, so anything it printed afterwards would be a -generic hint with no failing-command text, it would arrive after the -run's terminal output, and it could not reach the json envelope at all -— which is the surface the legacy action existed for. Wrapping every -handler body in a catch that rethrows unknown errors as a structured -one is reachable without an engine change, but it is not the same -thing: it would have to be repeated in every command, it would change -the crash's code and exit code (`CLI.INTERNAL_ERROR` exit 1 becomes a -group error exit 2), and it would still miss every crash outside a -handler — parsing, the needs checks, prompting, presentation — which -is where an unexpected failure is most likely. - -## S2d — init, version, and the host surface - -Divergences introduced by the S2d work on `version` and `init`. Grows as the slice proceeds; the closing dispatch folds this, `parity-divergences-s2b.md`, the S2c list and the auth-owned `parity-divergences.md` into one document for operator sign-off. - -One entry needs an explicit decision rather than a nod, and is marked **DECIDE**. - -### `version` — the command does not port. RULED (operator, 2026-08-11): removed. - -R-S2d-2 asked for the `version` command to be ported. It is not, and the reasoning belongs in the record because the contract said otherwise. - -The engine already answers the question. `--version` is a pre-parse fast path (`settleVersion`, `packages/cli-engine/src/execution/settlement.ts`) that prints the version in human mode and emits `{"commandId":"version","result":{"version":"…"}}` in json. Legacy had the same split, and the command inventory's own note on `version` says the port should unify the two rather than carry both forward. - -Nothing else the command reported survives scrutiny: - -- **`invocation`** (`dev | npx | bunx | global | unknown`) had exactly one consumer in the whole codebase: the presenter that printed it. It was derived from `process.argv[1]` — the process inspecting how it was launched, which is precisely what the engine withholds argv from handlers to prevent. Restoring it would have meant piping an environment fact through the runtime to a command, and no command may reach for the environment that way. -- **node version, platform and arch** are collected for bug reports by the command that actually needs them: `controllers/feedback.ts` builds its own `{cliVersion, nodeVersion, platform, arch}` context. A separate command that prints them for a human to copy is not the mechanism. - -**Consequence for R-S2d-5.** `version` becomes a ruled removal, alongside `service build`, `service deploy`, `service run` and the mock-only login flags. The grammar completeness check must exclude it, or it will report the command as missing against the inventory. - -**Consequence for users.** `prisma-cli version` stops existing; `prisma-cli --version` answers instead, printing the version alone rather than a three-line card. - -### `init` - -#### Class divergences - -1. **Exit codes.** Every errored settlement exits 2. Legacy `INIT_CONFIG_EXISTS`, `INIT_CONVERT_UNSUPPORTED`, `INIT_CONVERT_INCOMPLETE` and `INIT_DETECTION_FAILED` exited 1; `COMPUTE_CONFIG_INVALID` and the usage errors already exited 2. -2. **Error codes are dotted** under `INIT.*` — see the map below. -3. **NextActions.** The legacy `fix` prose becomes one `user-choice` action; each legacy `nextSteps` string becomes a `run-command` action. -4. **Human rendering** is engine blocks rather than the legacy rail-and-card bytes. Every sentence ports verbatim. -5. **The written config path goes to stdout** in human mode. Legacy human mode wrote nothing to stdout. -6. **`warnings: string[]` becomes coded diagnostics.** The five legacy warning sentences keep their text and gain codes. - -#### Init-specific divergences - -7. **`--format ` is renamed `--config-format `.** The engine reserves `--format` globally for the output format (`RESERVED_FLAG_NAMES` in `packages/cli-engine/src/execution/shared-flags.ts`), and the legacy flag means the format of the config file it writes. Values, defaults and behaviour are unchanged, including the conversion path where an explicit `ts` over an existing `prisma.compute.json` rewrites and deletes it. -8. **`--config-format` no longer accepts `typescript`, mixed case, or surrounding whitespace.** Legacy trimmed and lower-cased the value and took `typescript` as a synonym for `ts`. The engine's enum accepts exactly `ts` and `json`; anything else is the parser's `CLI.INVALID_ARGUMENTS` rather than a `USAGE_ERROR` reading "Unknown config format". -9. **Auto-login is dropped from the link step** (R-S2d-1). Legacy reached `requireAuthenticatedAuthState`, which could open a browser sign-in on a terminal. The port reads the credential the way `auth whoami` does. Signed out, the step reports the new status `link.status: "unauthenticated"`, records `INIT.LINK_REQUIRES_SIGN_IN` at warn severity, and offers `prisma-cli auth login` as a next action. `unauthenticated` is a new value in the json result's status union. -10. **The three optional steps now default to no.** **DECIDE.** The engine has one knob where the legacy CLI had two: a preselected answer when it asks, and a separate behaviour when nobody can be asked. `prompt.confirm`'s `default` serves both, because `--yes` and non-interactive take the same branch and a handler cannot read TTY state. Defaulting install-types, link and the agent-skill offer to yes would make `init --yes` in CI run a package-manager install, call the Management API and write agent-skill files into the repo, none of which today's command does; it would also produce a spurious warning on every unattended signed-in run, because the project picker has no default. Defaulting them to no keeps unattended runs behaving exactly as they do today. **The cost:** an interactive user presses `y` rather than Enter to accept each of the three, and the prompt reads `(y/N)` where it read `(Y/n)`. One line per prompt to flip if the other trade is preferred. -11. **`declined` where the legacy said `skipped`.** Because the handler cannot tell a person's "no" from a default answer, an unattended run reports `types.status: "declined"` and `link.status: "declined"` where legacy reported `"skipped"`. Both render the same sentence; only the json result differs. `"skipped"` still means `--no-install` / `--no-link`, no `package.json`, or the JSON config format. -12. **Cancelling the install or link question aborts the run.** Legacy caught the cancel, recorded `declined` and finished at exit 0. A cancelled prompt is now the engine's `CLI.PROMPT_CANCELLED` at exit 3. A config file already written stays on disk. -13. **The agent-skill offer is suppressed in CI via `ctx.env.CI`**, following `v8/auth/agent-setup-tip.ts`; legacy suppressed it through `canPrompt`, which the handler can no longer read. **Residual gap worth review:** a non-CI run with no terminal (piped stdin) answers the offer from its default and records a dismissal in the state directory that nobody gave, which would stop a later `app deploy` offering it. Legacy neither asked nor recorded anything there. -14. **The settings preview is a commentary event, not a styled stderr block.** Same padded columns, same position — after the adjust question, before the write. The source column is no longer dimmed, it is suppressed by the log level rather than by a flag check, and under `--format json` it is a framed `message` event rather than being hidden. -15. **Step events are new stderr commentary** (`▸ write-config`, `✔ install-types`, and so on). The legacy `Installing @prisma/compute-sdk...` line is gone; the `install-types` step event replaces it. -16. **The human next steps now include the types-install command.** The legacy presenter listed only `app deploy` and `project link`, while its json `nextSteps` also carried the install command. There is now one list, and it is the json one. -17. **An invalid port typed at the adjust prompt fails the run.** Legacy passed a `validate` callback so the prompt re-asked. `prompt.text` has no validation hook, so an out-of-range answer settles as `INIT.HTTP_PORT_INVALID` (exit 2) after the framework has already been chosen. - -#### Error code map - -| legacy | v8 | -| --- | --- | -| `INIT_CONFIG_EXISTS` | `INIT.CONFIG_EXISTS` | -| `INIT_CONVERT_UNSUPPORTED` | `INIT.CONVERT_UNSUPPORTED` | -| `INIT_CONVERT_INCOMPLETE` | `INIT.CONVERT_INCOMPLETE` | -| `INIT_DETECTION_FAILED` | `INIT.DETECTION_FAILED` | -| `COMPUTE_CONFIG_INVALID` | `INIT.COMPUTE_CONFIG_INVALID` | -| `USAGE_ERROR` (unknown config format) | `CLI.INVALID_ARGUMENTS` (engine parser) | -| `USAGE_ERROR` (`--install` with json) | `INIT.INSTALL_NOT_APPLICABLE` | -| `USAGE_ERROR` (custom framework with json) | `INIT.CUSTOM_FRAMEWORK_NEEDS_TYPESCRIPT` | -| `USAGE_ERROR` (resolution flags during conversion) | `INIT.CONVERSION_FLAGS_NOT_APPLICABLE` | -| `USAGE_ERROR` (unknown framework) | `INIT.FRAMEWORK_UNKNOWN` | -| `USAGE_ERROR` (empty `--name`) | `INIT.NAME_EMPTY` | -| `USAGE_ERROR` (bad `--http-port`) | `INIT.HTTP_PORT_INVALID` | -| `USAGE_ERROR` (unknown `--region`) | `INIT.REGION_UNKNOWN` | -| `USAGE_ERROR` (`--entry` unsupported) | `INIT.ENTRY_UNSUPPORTED` | -| warning: package.json unreadable | `INIT.TYPES_PACKAGE_JSON_UNREADABLE` (warn) | -| warning: install failed | `INIT.TYPES_INSTALL_FAILED` (warn) | -| warning: link failed | `INIT.LINK_FAILED` (warn) | -| *(no legacy equivalent)* | `INIT.LINK_REQUIRES_SIGN_IN` (warn) | -| warning: skill not installed | `INIT.AGENT_SETUP_FAILED` (warn) | - -Splitting one legacy `USAGE_ERROR` into nine codes changes the json `error.code` for those failures. Every summary, why and fix sentence is preserved verbatim. Distinct codes were chosen because the engine derives a docs link per code, and one shared code across nine unrelated failures makes those links useless — but it is a machine-facing contract change. - -### Two notes on the source documents - -- **R-S2d-1's own summary lists steps the shipping command does not have.** It names "project name" and "env write-out" as wizard steps. Today's `init` has no project-name prompt — the app name comes from `--name`, then `package.json`, then the directory name — and writes no env file. The port follows the inventory, which R-S2d-1 itself names as the contract. A project-name prompt does exist, but inside `project link`'s create-a-Project branch, which `init` reaches only when the user picks "create a new Project". -- **`init`'s link step is now literally `project link`.** The inventory records that legacy `init` called `runProjectLink`, and `runProjectLink` is what the v8 `project link` command ports. Rather than a second picker, `project link`'s handler body is extracted as `linkDirectoryToProject` and both call it, so the two commands cannot drift. - -### Carried out of this slice: commands no longer read `process` - -Not a user-visible divergence, but it changes the engine surface and two files on the services branch, so it belongs in the record. - -Three ported commands reached directly into `process` for host facts, because nothing else offered them: `v8/init/agent-setup.ts` and `v8/agent/skills-cli.ts` both for `process.platform === "win32"`, and `v8/feedback.ts` for `process.version`, `process.platform` and `process.arch`. Handlers already take the working directory and the environment from the context so they never touch process globals; there was no equivalent for the machine. - -`Runtime.host` now carries it, `ctx.host` hands it to commands, and the bin fills it once. The shape is `{ runtime: { name, version }, platform, arch }` — not node-shaped, so bun and deno describe themselves instead of being flattened into a field called `nodeVersion`, which is what R4's runtime-agnostic rule asks for. - -`init` is converted. **The two call sites in the services branch are not**, because converting them means touching that branch. `v8/feedback.ts` should send `ctx.host` rather than building its own payload, and its wire field `nodeVersion` should follow the same renaming. Both are recorded for the deletion pass, which already sweeps the whole tree. - -## The shell deletion itself (S2d, final pass) - -1. **Four legacy commands stop existing**, each previously ruled: `app build`, `app deploy`, `app run` (superseded by Composer) and `version` (`--version` answers). Their error codes, flags and side effects go with them. -2. **`app logs` is shelved, not dropped.** The S2c record said the legacy command would keep shipping "until S2d deletes the commander shell"; that has now happened, so streaming service logs is unavailable in any form until the engine grows the transport `service logs` needs. This is the one real capability loss of the pass. -3. **The feedback payload changes shape.** `{ cliVersion, nodeVersion, platform, arch }` becomes `{ cliVersion, runtime: { name, version }, platform, arch }`, read from `ctx.host` rather than `process`, so a bun or deno binary reports itself truthfully. The human summary line is unchanged; the wire payload and `--format json` result are not. -4. **The bin is the engine shell.** `prisma-cli` resolves to `dist/v8/cli.js`; the `prisma-v8` working name and its root script are gone; `commander` and five other now-unimported dependencies leave the manifest. Proven from a packed tarball on plain Node. -5. **The survivor list** — every legacy file the v8 tree still reaches, and where each went — is [`shell-deletion-survivors.md`](shell-deletion-survivors.md). - -## Ruled during the S7 merge (operator, 2026-08-12) - -**Top-level `init` is the platform's compute-config wizard; the ORM's project initializer mounts at `orm init`.** The unified grammar had both families claiming `init` — the S2d contract for the platform wizard, the ORM family's own command key for the initializer — and the collision only became mountable when S7 landed the ORM family. Users of the old ORM CLI who type `prisma init` expecting a schema scaffold now get the compute wizard and must type `orm init`. The `orm` group exists solely for this command until the ORM family grows more residents or the ruling is revisited (TML-3189 holds the final grammar). - -## Command grammar cleanup (2026-08-21 PM review) - -The grammar cleanup slice supersedes several spellings this record documents. Top-level `init` and the compute config (`prisma.compute.ts`/`.json`) are removed; service commands take parameters only (`--service`/`PRISMA_SERVICE_ID`, `--branch`; the picker, remembered selection, and git-branch inference are gone); `project remove`, `project env remove`, `postgres remove`, `postgres connection remove`, `service remove`, and `service domain remove` are renamed to `delete`; `postgres restore` moves to `postgres backup restore`. No aliases or redirects for the old spellings. This entry records the change; the sections above stay as written for history. diff --git a/.drive/projects/prisma-cli-v8/assets/s2/shell-deletion-survivors.md b/.drive/projects/prisma-cli-v8/assets/s2/shell-deletion-survivors.md deleted file mode 100644 index 5e299528..00000000 --- a/.drive/projects/prisma-cli-v8/assets/s2/shell-deletion-survivors.md +++ /dev/null @@ -1,63 +0,0 @@ -# Survivors of the commander-shell deletion (S2d, R-S2d-4) - -Every file below sits outside `src/v8/` and is reached from `src/v8/`. The list -is derived, not asserted: it is the transitive import closure of `src/v8/**`, -and after the deletion nothing outside that closure remains in `src/`. - -## Where things moved - -| Was | Is now | Why | -| --- | --- | --- | -| `src/shell/runtime.ts` (`CliRuntime`, `CommandContext`) | `src/legacy/runtime.ts` | The operation layer still takes a context. It now declares only the three fields it reads: `cwd`, `env`, `signal`. The commander wiring, `createCommandContext`, `canPrompt`, `stateStore`, `flags` and `ui` are gone. | -| `src/shell/output.ts` (`CliOutput`, `CommandSuccess`) | `src/legacy/output.ts` | `CommandSuccess` is still the return shape of the env-file operations. The shell's JSON and human error writers are gone. | -| `src/shell/errors.ts`, `shell/next-actions.ts`, `shell/command-arguments.ts`, `shell/cli-command.ts` | deleted | They were re-export stubs. Their real modules already lived at `src/errors.ts`, `src/next-actions.ts`, `src/command-arguments.ts`, `src/cli-command.ts`; every import was repointed there. | -| `detectDeployFramework` in `src/controllers/app.ts` | `src/lib/app/deploy-framework.ts` | v8 `init` was the only caller left, and it was pulling a 5,099-line file (and the whole prompt/UI tree behind it) for one self-contained function. | -| `src/shell/ui.ts`, `shell/global-flags.ts`, `shell/command-meta.ts`, `shell/prompt.ts`, `shell/help.ts`, `shell/command-runner.ts`, `shell/diagnostics-output.ts` | deleted | Nothing outside the shell read them once the dead command entry points went. The engine renders now. | - -## Survivors that stayed where they were - -### The operation layer the v8 handlers call - -- `src/controllers/project.ts` — project listing, local-pin rewriting, the GitHub - install/connect operations, transfer recipient errors. -- `src/controllers/database.ts` — database resolution, backup/usage parsing. -- `src/controllers/branch.ts` — branch listing and summaries. -- `src/controllers/app-env.ts`, `app-env-api.ts`, `app-env-file.ts` — environment - variable scope resolution, the API row shape, and the `.env` file writers. -- `src/presenters/project.ts`, `database.ts`, `bucket.ts`, `app-env.ts`, - `verbose-context.ts` — the `serialize*` functions v8 reuses for `--format json`. - Every `render*` function in these files is gone: the engine draws the human - output now. - -### Auth - -`src/auth/client.ts`, `credential-manager.ts`, `errors.ts`, `guard.ts`, -`legacy-state.ts`, `login.ts`, `operations.ts`, `recipient.ts`, -`service-token.ts`, `state-file.ts`, `token-storage.ts`, `workspace-name.ts`. -`src/auth/workspaces.ts` was deleted — only the legacy `auth workspace` command -used it. - -### Adapters and local state - -`src/adapters/git.ts`, `src/adapters/local-state.ts`, `src/state-dir.ts`. - -### Feature libraries (`src/lib/**`) - -`agent/{cli-command,constants,package-manager,setup-status}.ts`, -`app/{app-provider,branch-database-api,build,build-settings,bun-project,compute-config,deploy-framework,domain-guidance,env-config,env-file,env-vars,read-branch}.ts`, -`bucket/provider.ts`, `database/provider.ts`, `fs/home-path.ts`, -`git/local-branch.ts`, `project/{local-pin,provider,resolution,setup}.ts`, -`feedback.ts`, `version.ts`, `workspace-id.ts`. - -Deleted from `src/lib/**` because only `app deploy` / `app run` / `app logs` -reached them: `app/{app-interaction,branch-database,branch-database-deploy,deploy-output,deploy-plan,deploy-progress,local-dev,production-deploy-gate}.ts`, -`diagnostics.ts`, `git/local-status.ts`, `project/interactive-setup.ts`. - -### Shared modules at `src/` - -`cli-command.ts`, `cli-name.ts`, `command-arguments.ts`, `errors.ts`, -`next-actions.ts`, `output/patterns.ts`, `update-check.ts`, -`types/{app,app-env,auth,branch,bucket,database,init,project}.ts`. - -Deleted: `types/{agent,diagnostics,feedback,version}.ts` — the commands that -owned those shapes are ported and carry their own result types in `src/v8/`. diff --git a/.drive/projects/prisma-cli-v8/assets/s3/composer-inventory.md b/.drive/projects/prisma-cli-v8/assets/s3/composer-inventory.md deleted file mode 100644 index eeee24b9..00000000 --- a/.drive/projects/prisma-cli-v8/assets/s3/composer-inventory.md +++ /dev/null @@ -1,1565 +0,0 @@ -# S3 Composer Inventory — the `prisma-composer` CLI as it exists today - -Normative record of what Composer's CLI does now. The S3 contract and all -porting work hang off this document. - -Sources read read-only at -`wip/repos/composer` (branch `main`, commit `a1cd673`). Unless a path is -prefixed otherwise, every `path:line` citation in this document is relative to -that clone's root. Citations into the prisma-cli repo are prefixed -`prisma-cli:`. - -The CLI's code lives in the **private** workspace package `@internal/cli` at -`packages/0-framework/3-tooling/cli` -(`packages/0-framework/3-tooling/cli/package.json:2`). It is published only as -part of `@prisma/composer` -(`packages/9-public/composer/package.json:2`), whose `bin` entry is -`prisma-composer` → `./dist/bin.mjs` -(`packages/9-public/composer/package.json:8-10`). - -Format follows `.drive/projects/prisma-cli-v8/assets/s2/command-inventory.md`. - ---- - -## 1. Command index and per-command inventory - -### 1.1 Index - -The whole grammar is registered in one call: - -``` -Cli.from([DeployCommand, DestroyCommand, DevCommand, LogCommand], …) -``` - -`packages/0-framework/3-tooling/cli/src/main.ts:113`. - -| path | kind today | flags | positionals | auth | spawns | proposed engine kind | -|---|---|---|---|---|---|---| -| `deploy ` | long-running, child-driven | `--name`, `--stage`, (`--production` — accepted by the parser, always an error) | `entry` (required) | platform, indirectly via the extension's providers inside the child | `alchemy deploy`, `git check-ref-format` | result command | -| `destroy ` | long-running, child-driven | `--name`, `--stage`, `--production` | `entry` (required) | same as deploy | `alchemy destroy`, `git check-ref-format` | result command | -| `dev ` | session (runs until SIGINT/SIGTERM) | `--name`, `--fresh` | `entry` (required) | none — credential-free by design | `alchemy deploy` (once per converge, repeatedly on file change) | session command | -| `log [address]` | session (runs until SIGINT/SIGTERM) | `--name`, `--tail` | `entry` (required), `address` (optional) | none | none | session command | - -**There are no other commands.** No hidden, experimental, or debug commands -exist: `Cli.from` receives exactly those four classes -(`packages/0-framework/3-tooling/cli/src/main.ts:113`), there is no dynamic -registration, and no second binary. `packages/9-public/composer/package.json:8` -declares exactly one `bin`. A grep of the workspace for other `bin` entries -returns only that package. - -There are **no group nodes** — the grammar is one level deep. - -There is **no `--json` flag, no `--quiet`, no `--verbose`, no `--color`, no -`-y/--yes`, and no global flag set of any kind.** The four commands' options -are the entire flag surface. - -`--help` / `-h` is handled by clipanion's own fallback and intercepted at -`packages/0-framework/3-tooling/cli/src/main.ts:205-207`: the detailed usage -text is printed to **stdout** and the process exits **0**. A bare -`prisma-composer` with no arguments prints the same text to **stderr** and -exits **2** -(`packages/0-framework/3-tooling/cli/src/main.ts:208`, then -`packages/0-framework/3-tooling/cli/src/cli.ts:21-25`). - -### 1.2 Process-wide behavior shared by all four commands - -**Startup preflight.** `bin.ts` runs `checkEffectResolution(process.cwd())` -*before* importing anything else -(`packages/0-framework/3-tooling/cli/src/bin.ts:14-22`). It walks the app's -installed tree, and if `alchemy` resolves a different `effect` version than -`@prisma/composer` requires, it prints a rendered error envelope to stderr and -calls `process.exit(2)`. This runs for `--help` too. The check is a no-op when -`alchemy` is not installed -(`packages/0-framework/3-tooling/cli/src/check-effect-resolution.ts:114-128`). - -**Top-level error mapping** (`packages/0-framework/3-tooling/cli/src/cli.ts:17-35`): - -| condition | stream | exit code | -|---|---|---| -| `run()` returned a number | — | that number (`cli.ts:19`) | -| clipanion `UsageError` | stderr, raw message | 2 | -| `CliStructuredError` | stderr, rendered envelope | 2 | -| anything else | stderr, `Error: …` plus a "this is a bug, please report it" line naming `https://github.com/prisma/composer/issues` | 1 | - -Exit codes are set via `process.exitCode`, never `process.exit()`, so the -process drains its streams first. - -**Error envelope rendering** (`packages/0-framework/3-tooling/cli/src/render-error.ts:9-18`): - -``` -✖ () - Why: - Fix: - Where: [:] -``` - -No color. `conflicts`, `meta`, and `docsUrl` are deliberately not rendered. -The error model is ADR-0044 (dotted namespace codes, structured at origin, -no fallback codes) -(`docs/design/90-decisions/ADR-0044-errors-are-structural-envelopes-with-dotted-namespace-codes.md`). - -**Prompts: there are none.** No command in this CLI ever reads stdin or asks a -question. The alchemy child is always invoked with `--yes` -(`packages/0-framework/3-tooling/cli/src/run-alchemy.ts:49`), which suppresses -alchemy's own confirmation prompts. There is no confirmation before `destroy` -— the guard is the required explicit `--stage`/`--production` target, not a -prompt. - -**Env vars the CLI itself reads or writes.** Only one is defined by composer: - -| var | direction | site | -|---|---|---| -| `PRISMA_COMPOSER_DEPLOYMENT_RESULT_FILE` | written by the parent onto the child; read inside the child by the report hook | `packages/0-framework/3-tooling/cli/src/deployment-summary.ts:18`; set at `packages/0-framework/3-tooling/cli/src/operations/execute-deploy-destroy.ts:245`; read at `packages/0-framework/3-tooling/cli/src/deployment-summary.ts:46` | -| one var per extension, named by `containerEnvVarName` | written by the parent onto the child; read inside the generated dev stack file | `packages/0-framework/3-tooling/cli/src/run-alchemy.ts:56`; read at `packages/0-framework/3-tooling/cli/src/dev/generate-dev-stack.ts:81` | - -The parent's own `process.env` is otherwise passed through wholesale to the -child (`packages/0-framework/3-tooling/cli/src/run-alchemy.ts:55`). Credential -env vars are therefore consumed by the extension's provider code inside the -child, not by the CLI — see §3. - ---- - -### 1.3 `deploy ` - -**Summary.** "Deploy the application whose root node is ``'s default -export." (`packages/0-framework/3-tooling/cli/src/main.ts:44`) - -**Positionals.** `entry` — required, a path to the entry module, resolved -against the process cwd -(`packages/0-framework/3-tooling/cli/src/main.ts:19`, -`packages/0-framework/3-tooling/cli/src/pipeline.ts:87`). - -**Flags.** - -| flag | type | default | meaning | -|---|---|---|---| -| `--name ` | string | absent → the root node's own name | Overrides the root node's name, i.e. the deployed application name (`main.ts:21-23`, applied at `pipeline.ts:111`) | -| `--stage ` | string | absent → production | Target a named, isolated environment instead of production (`main.ts:25-27`) | -| `--production` | boolean | `false` | **Declared on deploy but never valid.** Inherited from the shared abstract class `DeployCliCommand` (`main.ts:29-32`); `run()` rejects it with `DEPLOY.FLAG_INVALID` (`main.ts:259-267`) | - -**Config consumed.** `prisma-composer.config.ts` — see §2. - -**Auth.** The CLI performs no authentication itself. Credentials are read by -the extension's provider code — partly in-process (the container `ensure` -call, `execute-deploy-destroy.ts:140`) and partly in the alchemy child. See §3. - -**Child processes.** - -1. `git check-ref-format refs/heads/` via `spawnSync` with - `stdio: 'ignore'`, only when `--stage` was given - (`packages/0-framework/3-tooling/cli/src/validate-stage.ts:6-8`). A spawn - error is `DEPLOY.STAGE_UNVALIDATABLE`; a nonzero status is - `DEPLOY.STAGE_INVALID`. -2. The workspace's installed `alchemy` bin — see §1.7 for the exact mechanics. - -**Behavior, in order** (`packages/0-framework/3-tooling/cli/src/operations/execute-deploy-destroy.ts:82-323`): - -1. Validate the stage name (only if `--stage`) — `execute-deploy-destroy.ts:88-97`. -2. Shared pipeline (`pipeline.ts:79-130`): discover and load - `prisma-composer.config.ts` walking up from the entry's directory; import - the entry module; run `Load` on its default export; check the root is a - module; validate registry coverage; resolve the name; assemble each service - through the config's build registries. -3. For each extension with a `container` descriptor: `container.ensure({ - appName, stage })` — this is where the platform's Project/Branch are created - if absent (`execute-deploy-destroy.ts:137-141`). -4. Pin the Alchemy stage: the state-owning extension's container supplies - `alchemyStage`; otherwise the user's `--stage`; if neither exists, - `DEPLOY.SCOPE_MISSING` (`execute-deploy-destroy.ts:162-180`). Alchemy's own - default (`dev_$USER`) must never apply — it is machine-dependent - (recorded as incident TML-3157, `execute-deploy-destroy.ts:158-161`). -5. Preflight: each extension's `preflight({ graph, container, stage })`, before - any stack file is written (`execute-deploy-destroy.ts:186-195`). -6. Write `.prisma-composer/alchemy.run.ts` - (`packages/0-framework/3-tooling/cli/src/generate-stack.ts:82-88`). -7. Spawn alchemy (§1.7). -8. Read and delete the deployment-summary result file - (`execute-deploy-destroy.ts:314`, `execute-deploy-destroy.ts:317-322`). - -**Output.** - -- The deploy tree is printed **from inside the alchemy child**, not from the - CLI process. The generated stack file wires - `report: deploymentReport` into `lower()` - (`packages/0-framework/3-tooling/cli/src/generate-stack.ts:53`, - `generate-stack.ts:71`), and `deploymentReport` prints a blank line then the - rendered topology to **stdout** via `console.log` - (`packages/0-framework/3-tooling/cli/src/render-deployment.ts:123-127`). - The rendering is a box-drawn address tree, one line per deployed node, with - `kind id` per entity, its `url`, and its `details` aligned into one column - (`render-deployment.ts:77-116`). -- All of alchemy's own apply output goes straight to the terminal, because the - child inherits stdio (`run-alchemy.ts:53`). -- The parent's `deploy` success path prints nothing at all: `run()` returns 0 - and discards `result.summary` (`main.ts:272`). -- There is **no JSON output mode.** - -**Prompts.** None. - -**Side effects.** - -| effect | where | -|---|---| -| `/.prisma-composer/alchemy.run.ts` written (overwritten every run) | `generate-stack.ts:82-88` | -| `/.prisma-composer/deployment-result--.json` written by the child, read then deleted by the parent | `execute-deploy-destroy.ts:210-213`, `deployment-summary.ts:45-53`, `execute-deploy-destroy.ts:317-322` | -| `/.alchemy/` — alchemy's own local state directory | referenced at `execute-deploy-destroy.ts:28`; written by alchemy itself | -| Platform Project/Branch created if absent | `execute-deploy-destroy.ts:140` | -| Everything the alchemy apply provisions | inside the child | - -The result file's name is unique per run so two concurrent deploys from one -checkout cannot read or delete each other's file -(`execute-deploy-destroy.ts:205-208`). The `finally` at -`execute-deploy-destroy.ts:317` removes it on every path, success or failure. - -**Error paths and exit codes.** - -| condition | code | exit | -|---|---|---| -| `--production` passed | `DEPLOY.FLAG_INVALID` | 2 | -| bad `--stage` name | `DEPLOY.STAGE_INVALID` / `DEPLOY.STAGE_UNVALIDATABLE` | 2 | -| no config file found walking up | `CONFIG.FILE_MISSING` | 2 | -| config module threw while evaluating | `CONFIG.EVALUATION_FAILED` | 2 | -| config resolved to a different file than discovered | `CONFIG.PATH_MISMATCH` | 2 | -| config shape invalid | `CONFIG.EXPORT_INVALID` / `CONFIG.FIELD_INVALID` / `CONFIG.EXTENSION_DUPLICATE` | 2 | -| entry module unloadable | `COMPOSE.ENTRY_UNLOADABLE` | 2 | -| root not a module / unnamed | `COMPOSE.ROOT_NOT_MODULE` / `COMPOSE.NAME_MISSING` | 2 | -| container ensure threw | `DEPLOY.CONTAINER_FAILED` | 2 | -| no alchemy stage resolvable | `DEPLOY.SCOPE_MISSING` | 2 | -| preflight threw | `DEPLOY.PREFLIGHT_FAILED` | 2 | -| stack file could not be written | `DEPLOY.STACK_WRITE_FAILED` | 2 | -| `alchemy` bin not found | `DEPLOY.ALCHEMY_BIN_MISSING` | 2 | -| spawn itself threw | `DEPLOY.ENGINE_FAILED`, `diagnostics.exitCode === undefined` | 2 | -| **alchemy child exited nonzero** | `DEPLOY.ENGINE_FAILED`, `diagnostics.exitCode = status` | **the child's own status** (§1.7) | -| executor module failed to import | `DEPS.EFFECT_VERSION_CONFLICT` or `DEPS.EXECUTOR_UNLOADABLE` | 2 | -| effect-resolution preflight at startup | `DEPS.EFFECT_VERSION_CONFLICT` | 2 (via `process.exit`) | - -**Test coverage.** `src/__tests__/run.test.ts` (1098 lines) drives the whole -pipeline over injected fakes: `--production` rejection, preflight ordering, -container-supplied `alchemyStage` (both with and without `--stage`), a nonzero -alchemy status propagating with the printed stack-file path. -`src/operations/__tests__/operations.test.ts` (1330 lines) covers the -programmatic surface. `src/__tests__/generate-stack.test.ts` asserts the -generated source text. There is no end-to-end test that actually runs alchemy. - -**Engine notes.** `deploy` is a **result command**. It has one required -positional and two live flags. It has no prompts and no consent — but it is -destructive-adjacent in that it creates platform resources; the current design -has no `--confirm` equivalent, so porting it as-is means declaring no consent. -It should declare `needs.config` on the `composer` section token. Its exit-code -set is the passthrough exception, not a documented `exitCodes` map — see §7. -Long-running with progress output that is written by a *child process on -inherited stdio*, which the engine's `output`/`step` events cannot mediate; -see §7 hazard H1. - ---- - -### 1.4 `destroy ` - -**Summary.** "Tear down the application whose root node is ``'s default -export — same derivation as deploy, Alchemy destroy." -(`packages/0-framework/3-tooling/cli/src/main.ts:53-54`) - -**Positionals.** `entry` — required. - -**Flags.** `--name`, `--stage`, `--production` — all three from the shared -abstract class (`main.ts:18-39`). - -**Target selection is mandatory and exclusive** (`main.ts:276-294`): - -| flags | result | -|---|---| -| `--stage x --production` | `DEPLOY.TARGET_CONFLICT`, exit 2 | -| neither | `DEPLOY.TARGET_MISSING`, exit 2 | -| `--stage x` | target `{ kind: 'stage', stage: x }` | -| `--production` | target `{ kind: 'production' }` | - -This is the only protection against tearing down production: a bare `destroy` -is an error, so an omitted or mistyped stage cannot silently hit production -(`docs/design/10-domains/deploy-cli.md:133`). - -**Behavior.** The same pipeline as deploy, with four differences -(`execute-deploy-destroy.ts:62-77` and `execute-deploy-destroy.ts:82-316`): - -1. **Guardrail first.** If `/.alchemy` is missing or empty, a - `no-local-deploy-state` event fires *before* everything else - (`execute-deploy-destroy.ts:103-105`, predicate at - `execute-deploy-destroy.ts:31-34`). The CLI renders it as a **warning on - stderr**, not a failure (`main.ts:301-308`): "No prior deploy state under - `` — if you deployed from a different directory, run destroy from - there; otherwise this is a no-op." -2. **Containers are located, not created.** `container.locate(...)`; an - `undefined` result is `DEPLOY.TARGET_NOT_FOUND` - (`execute-deploy-destroy.ts:143-150`). -3. **No preflight.** `execute-deploy-destroy.ts:186`. -4. **Two suffix loops, in this order** (`execute-deploy-destroy.ts:283-306`): - every extension's `teardown(...)` first, then every extension's - `container.remove(...)`. The comment records why the order is structural: - a stage's state database must be deleted before its Branch, because a - Branch with an attached database refuses deletion (ADR-0034). - -A destroy still assembles the app, so **the app must be built first**; an -assembly failure is re-coded to `DEPLOY.BUILD_REQUIRED` with a "run the build, -then retry" fix (`execute-deploy-destroy.ts:117-125`). - -**Output.** No summary — `runStackPipeline` returns `ok(undefined)` for destroy -(`execute-deploy-destroy.ts:316`) and `run()` returns 0 silently. All visible -output is alchemy's own, on inherited stdio, plus the possible stderr warning. - -**Prompts.** None. There is no type-to-confirm. - -**Side effects.** Same generated stack file; platform resources removed; -containers removed. The result file is created and cleaned up identically even -though destroy never reads it (`execute-deploy-destroy.ts:210`, `:317`). - -**Error paths.** As deploy, plus `DEPLOY.TARGET_CONFLICT`, -`DEPLOY.TARGET_MISSING`, `DEPLOY.TARGET_NOT_FOUND`, `DEPLOY.BUILD_REQUIRED`, -`DEPLOY.TEARDOWN_FAILED`, `DEPLOY.CONTAINER_REMOVE_FAILED`. All exit 2 except -the alchemy passthrough. - -**Test coverage.** Extensive in `src/__tests__/run.test.ts`: target selection, -the `.alchemy` guardrail (missing / empty / present / not-checked-on-deploy), -container removal after destroy, no removal after a failed destroy, teardown -ordering, a throwing teardown aborting before removal. - -**Engine notes.** A **result command**. It is the one clearly destructive -command in the family and the natural place for a `consent` prompt with the -app name as its token — it has none today, so adding one is a parity -divergence to put to the operator. Its `--stage` / `--production` exclusivity -is exactly the engine's flag-conflict territory. Same passthrough exception as -deploy. - ---- - -### 1.5 `dev ` - -**Summary.** "Bring up the application whose root node is ``'s default -export, entirely on this machine, credential-free." -(`packages/0-framework/3-tooling/cli/src/main.ts:64-65`) - -**Positionals.** `entry` — required. - -**Flags.** - -| flag | type | default | meaning | -|---|---|---|---| -| `--name ` | string | root node's name | Override the dev instance's application name (`main.ts:71-73`) | -| `--fresh` | boolean | `false` | Destroy the dev stack and wipe the dev state directory before starting (`main.ts:75-78`) | - -**There is deliberately no `--stage` and no `--production`**: a working -directory has exactly one dev instance, no stages -(`main.ts:60`, local-dev spec §6). - -**Auth.** None — this is the credential-free command. Everything runs against -local emulators through the extension's `localTarget` descriptor. - -**Child processes.** `alchemy deploy .prisma-composer/dev/…` with stage `dev`, -once at startup and again on every file-change rebuild -(`packages/0-framework/3-tooling/cli/src/operations/execute-dev.ts:141-147`, -`execute-dev.ts:265-271`). Note the stage is the hardcoded literal `'dev'`. - -**Behavior** (`packages/0-framework/3-tooling/cli/src/operations/execute-dev.ts:41-327`): - -1. Refuse on Windows — `DEV.PLATFORM_UNSUPPORTED` (`execute-dev.ts:46-53`). -2. Shared pipeline (`execute-dev.ts:66`). -3. Resolve every non-build-only extension's lazy `localTarget` thunk once; - an extension without one fails with `DEV.TARGET_UNSUPPORTED` raised inside - core (`execute-dev.ts:74`). -4. Local containers ensured (`execute-dev.ts:77-83`). -5. `--fresh` → every local target's `teardown` (`execute-dev.ts:86-95`). -6. Preflight — always (`execute-dev.ts:98-105`). -7. Emulators ensured (`execute-dev.ts:108-115`). -8. Write the dev stack file and converge via alchemy (`execute-dev.ts:125-158`). -9. Attach: `startServices()` on every attachment, then merge `endpoints()` - (`execute-dev.ts:213-237`). -10. Start the file watcher; on each change, re-run the pipeline, rewrite the dev - stack, re-converge. A converge failure keeps the running app and keeps - watching (`execute-dev.ts:248-283`). -11. Wait on `session.closed` (`run-dev.ts:129`). - -Failures between step 9 and handover roll back: the watcher is stopped and -every started service is stopped before the failure surfaces -(`execute-dev.ts:319-326`). - -**Output.** All on stdout via `console.log` except the error events, which go -to stderr (`packages/0-framework/3-tooling/cli/src/dev/run-dev.ts:54-90`). -The shipped order is: front door → logs hint → unwatchable notices -(`run-dev.ts:44-48`, `run-dev.ts:106-108`). - -``` -[dev] ready: -[dev]
(sorted by address depth, then lexicographically) -[dev] logs: prisma-composer log -[dev]
has no watchable inputs -``` - -Error/notice lines, all prefixed `[dev]`: `converge failed — the running app is -untouched; still watching.` (plus the two reproduce-hint lines), -`rebuild failed: …`, `watch error: …`, `a service refused to stop: …`, -`stopping — …`, `stopped.` No JSON mode. - -**Prompts.** None. - -**Signal handling — a notable hazard.** After the session is handed over, -`run-dev.ts` calls `process.removeAllListeners('SIGINT')` and -`process.removeAllListeners('SIGTERM')` and installs itself as the *only* -listener (`run-dev.ts:124-127`). The recorded reason: alchemy's library code, -loaded transitively while importing the app's config and providers, registers -its own signal listeners for in-process bookkeeping that is irrelevant here -(the real converge runs in a spawned child), and whichever runs first can call -`process.exit()` synchronously and kill the process before the watch loop's -async cleanup gets a turn (`run-dev.ts:114-123`). - -**Side effects.** `/.prisma-composer/dev/` — the dev state directory -(`packages/0-framework/1-core/core/src/control/app-config.ts:162`); the -generated dev stack file; emulator daemons started and left running across -sessions; local containers. `--fresh` wipes the dev state directory. - -**Error paths.** `DEV.PLATFORM_UNSUPPORTED`, `DEV.TARGET_UNSUPPORTED`, -`DEV.CONTAINER_FAILED`, `DEV.TEARDOWN_FAILED`, `DEV.PREFLIGHT_FAILED`, -`DEV.EMULATOR_FAILED`, `DEV.STACK_WRITE_FAILED`, `DEV.CONVERGE_FAILED`, -`DEV.ATTACH_FAILED`, `DEV.SERVICE_START_FAILED` — all exit 2, except that a -nonzero alchemy converge status at **startup** takes the passthrough -(`run-dev.ts:96-99`). A converge failure *after* the session is live is an -event, not an exit. A clean shutdown returns 0 (`run-dev.ts:132`) — including -after Ctrl-C, so `dev` exits **0** on SIGINT, not 130. - -**Test coverage.** `src/dev/__tests__/run-dev.test.ts` (25 lines — only the -front-door rendering), `src/dev/__tests__/watch.test.ts` (130), -`src/dev/__tests__/generate-dev-stack.test.ts` (59), plus the dev cases in -`src/operations/__tests__/operations.test.ts`. Repo-level integration tests -exist at `test/integration/test/local-dev.integration.ts`, -`local-dev-store.integration.ts`, `local-dev-criteria-4-5.integration.ts`. - -**Engine notes.** A **session command**: runs until the signal fires, speaks -entirely through events, returns `Result`. Its event vocabulary maps -onto the engine's `EngineEvent` set almost directly — `ready` → -`endpoint` events, `stopping`/`stopped` → `status`, `converge-failed` / -`rebuild-failed` / `watch-error` / `stop-error` → `message` at `warn`/`error`. -Two things do not fit and need operator rulings: (a) the signal-listener -stripping above conflicts with the engine owning `context.signal`, and (b) the -startup-converge exit-code passthrough conflicts with a session command's -"no exit-code set". Its exit-0-on-Ctrl-C also disagrees with the shared -`130` convention. - ---- - -### 1.6 `log [address]` - -**Summary.** "Tail the merged logs of the locally-running application whose -root node is ``'s default export." -(`packages/0-framework/3-tooling/cli/src/main.ts:87-88`) - -**This command is local-only.** It reads nothing from the Prisma management -API. See §4c. - -**Positionals.** `entry` (required); `address` (optional) — restrict output to -one service's dotted address, e.g. `catalog.service` -(`main.ts:95-97`, filtered at -`packages/0-framework/3-tooling/cli/src/operations/execute-log.ts:81`). - -**Flags.** - -| flag | type | default | meaning | -|---|---|---|---| -| `--name ` | string | root node's name | Override the dev instance's application name (`main.ts:99-101`) | -| `--tail ` | string, parsed with `Number.parseInt(…, 10)` | `20` | Trailing history lines before live output (`main.ts:103-105`, `main.ts:143`, parsed at `main.ts:185-188`) | - -`--tail` is the one flag with its own validation: `NaN` or negative → -`UsageError('\`--tail\` must be a non-negative integer.')`, exit 2 -(`main.ts:186-188`). Note it is parsed with `parseInt`, so `--tail 5abc` -silently becomes `5`. - -**Behavior** (`packages/0-framework/3-tooling/cli/src/operations/execute-log.ts:130-208`): - -1. Refuse on Windows — `LOG.PLATFORM_UNSUPPORTED` (`execute-log.ts:135-142`). -2. `resolveAppIdentity` — the pipeline's *front only*: config discovery and - load, entry import, name resolution. Deliberately no `Load`, no coverage - check, and **no assemble**, because `log` neither builds nor provisions and - must not require the user's built output - (`packages/0-framework/3-tooling/cli/src/pipeline.ts:50-70`). -3. Resolve local targets; for each, `container.ensure(...)` then `attach(...)` - (`execute-log.ts:158-169`). -4. Merge every attachment's `logs(signal, { tail })` async iterable into one - stream (`execute-log.ts:39-127`). - -**The merge is bounded.** `LOG_QUEUE_LIMIT = 10_000` -(`execute-log.ts:27`); past that the oldest line is dropped and the consumer is -told via a `lines-dropped` event (`execute-log.ts:82-86`). One pump's throw -becomes a `stream-failed` event and ends that pump only (`execute-log.ts:89-93`). - -**Output.** Lines on **stdout** as `[] ` -(`packages/0-framework/3-tooling/cli/src/log/run-log.ts:66`). Two notices on -**stderr**: `[log] stream failed: …` and `[log] falling behind — dropped the N -oldest lines.` (`run-log.ts:42-48`). No JSON mode. - -**Empty case.** If no services are running, one stderr line — "no running -services for `` — start it first with `prisma-composer dev `" — -and **exit 0** (`run-log.ts:58-63`). - -**Prompts.** None. - -**Signal handling.** SIGINT/SIGTERM → `controller.abort()`, which ends the -merged iterable; listeners are removed in a `finally` -(`run-log.ts:27-31`, `run-log.ts:68-71`). Returns 0. Unlike `dev`, it does not -strip other listeners. - -**Side effects.** None on disk. It calls `container.ensure(...)` -(`execute-log.ts:164`), which for the local target is a purely local identity -resolution. - -**Error paths.** `LOG.PLATFORM_UNSUPPORTED`, `LOG.ATTACH_FAILED`, -`LOG.ADDRESS_UNKNOWN` (names the running services in its message, -`execute-log.ts:192-201`), plus the config/entry codes from -`resolveAppIdentity`. All exit 2 — `run-log.ts:55` rethrows the failure and -`cli.ts` renders it. - -**Test coverage.** `src/log/__tests__/run-log.test.ts` (150 lines) plus the log -cases in `src/operations/__tests__/operations.test.ts`. - -**Engine notes.** A **session command** with a stream-shaped payload. `--tail` -is a number flag the engine would type properly (removing the `parseInt` -laxness). The optional `address` positional and the `LOG.ADDRESS_UNKNOWN` -"did you mean" list map to a normal validation failure. Its clean signal -handling makes it the easiest of the four to port. Because it reads only local -emulator state, it needs **no credentials and no management API** — which is -the fact §4c turns on. - ---- - -### 1.7 The alchemy child-status passthrough, precisely - -This is the exception S3 must preserve. The chain, end to end: - -**Step 1 — resolve the bin.** `resolveAlchemyBin(cwd)` walks up from the cwd -looking for `node_modules/.bin/alchemy` -(`packages/0-framework/3-tooling/cli/src/run-alchemy.ts:16-31`). Deliberately -not `npx`/`bunx`, so it behaves the same under node and bun; the resolved -launcher does its own runtime dispatch. Not found → -`DEPLOY.ALCHEMY_BIN_MISSING`. - -**Step 2 — spawn.** - -```ts -const args = [input.command, input.stackFileRelativePath, '--yes', '--stage', input.stage]; -const result = spawnSync(bin, args, { - cwd: input.cwd, - stdio: 'inherit', - env: { ...(input.env ?? process.env), ...input.containerEnv }, -}); -if (result.error !== undefined) throw result.error; -return result.status ?? 1; -``` - -`packages/0-framework/3-tooling/cli/src/run-alchemy.ts:47-62`. - -Key facts: **synchronous** `spawnSync`; **`stdio: 'inherit'`** so the child -writes directly to the CLI's own stdout/stderr with no interception; the -parent's whole environment is forwarded plus the per-extension container vars; -`--yes` is always passed; the stage is always explicit. A signal-killed child -(`result.status === null`) becomes `1`. - -**Step 3 — a nonzero status becomes a structured failure carrying the status.** - -```ts -if (status !== 0) { - return notOk(new CliStructuredError('DEPLOY.ENGINE_FAILED', - `alchemy ${action} exited with status ${status}.`, - { meta: { exitCode: status, - diagnostics: { exitCode: status, stackFilePath, reproduceCommand, cwd } } })); -} -``` - -`packages/0-framework/3-tooling/cli/src/operations/execute-deploy-destroy.ts:262-275`. -The `reproduceCommand` is built at `execute-deploy-destroy.ts:234` as -``alchemy .prisma-composer/alchemy.run.ts --yes --stage ``. - -**Step 4 — the renderer extracts the status and returns it.** - -```ts -export function renderChildStatusHints(failure: CliStructuredError): number | undefined { - const diagnostics = executionDiagnostics(failure); - if (diagnostics === undefined || diagnostics.exitCode === undefined) return undefined; - console.error(`\nGenerated stack file: ${diagnostics.stackFilePath}`); - console.error(`Run \`${diagnostics.reproduceCommand}\` from ${diagnostics.cwd} to reproduce this directly.`); - return diagnostics.exitCode; -} -``` - -`packages/0-framework/3-tooling/cli/src/render-error.ts:27-37`. The two hint -lines go to **stderr**. `executionDiagnostics` is the structural reader at -`packages/0-framework/3-tooling/cli/src/operations/shared.ts:56-75`; the shape -it reads is `ExecutionDiagnostics` at `shared.ts:44-50` and is explicitly -documented as *not* part of the durable contract ("branch on -`code`/`message`/`cause` for anything durable", `shared.ts:41-42`). - -**Step 5 — the CLI returns it as its own exit code.** - -`renderDeployDestroyFailure` (`main.ts:220-224`) returns the status if there is -one and rethrows otherwise; `run()` returns it (`main.ts:273`, `main.ts:313`); -`cli()` assigns it (`packages/0-framework/3-tooling/cli/src/cli.ts:19`). - -**Which commands pass through:** `deploy` (`main.ts:273`), `destroy` -(`main.ts:313`), and `dev` for a **startup** converge failure only -(`packages/0-framework/3-tooling/cli/src/dev/run-dev.ts:96-99`). `log` never -spawns alchemy and never passes anything through. - -**When it does *not* apply:** if `spawnSync` itself throws (bin missing at -exec time, permissions), `runAlchemy` rethrows -(`run-alchemy.ts:60`), the catch builds `DEPLOY.ENGINE_FAILED` with -`diagnostics.exitCode: undefined` (`execute-deploy-destroy.ts:249-260`), -`renderChildStatusHints` returns `undefined`, and the failure takes the normal -envelope path — exit **2**. - -**The rule it excepts.** ADR-0044's exit-code rule is `0` OK, `1` internal -bug only, `2` expected failure, `3` user abort, `130`/`143` signals. The -documented exception says a passthrough status is the *child's* number, not a -statement in the CLI's own code space, so an expected engine failure may -surface as `1` without contradicting the rule -(`docs/design/90-decisions/ADR-0044-errors-are-structural-envelopes-with-dotted-namespace-codes.md:108-114`). -Renumbering it onto `2` was explicitly considered and rejected -(same file, `:152-154`). - ---- - -## 2. Config machinery - -### 2.1 The file - -One file: **`prisma-composer.config.ts`** -(`packages/0-framework/3-tooling/cli/src/load-config.ts:18`). The literal -filename is the only one looked for — there is no `.js`/`.mjs`/`.json` -variant, no `.config/` directory convention, and no -`prisma.config.ts` involvement today. - -ADR-0017 makes it the ONE file that imports control-plane code; app code never -imports it (`docs/design/90-decisions/ADR-0017-control-plane-loads-through-the-app-config.md`; -see also the example at `examples/store/prisma-composer.config.ts`). - -### 2.2 Discovery - -A plain walk **up** from the entry file's directory to the filesystem root, -testing `fs.existsSync` on `/prisma-composer.config.ts` at each level -(`load-config.ts:27-36`). Not found → `CONFIG.FILE_MISSING`, whose `where` -names the directory the walk started from (`load-config.ts:38-51`). - -Note: discovery is anchored on the **entry**, not the cwd. The generated stack -file, the `.alchemy` state directory, and `.prisma-composer/` are all anchored -on the **cwd**. The two can differ. - -### 2.3 Evaluation strategy - -The file is TypeScript and is executed by **c12** -(`load-config.ts:137-144`), with an explicit `configFile` path and every other -lookup disabled: - -```ts -await c12.loadConfig({ - name: 'prisma-composer', - configFile: configPath, - cwd: path.dirname(configPath), - rcFile: false, globalRc: false, packageJson: false, -}); -``` - -The recorded reason for passing an explicit path rather than letting c12 -discover: the config file's own static imports then resolve from the app root -under whatever package manager is running — no specifier construction, no -anchoring (`load-config.ts:4-8`). c12 handles the TypeScript transpilation -(via jiti, internally); composer does not run `tsc` or esbuild for this file. - -**A same-path check follows the load.** If c12 reports a `configFile` whose -`realpath` differs from the discovered path, the load fails with -`CONFIG.PATH_MISMATCH` — "Refusing to deploy against a different file." -(`load-config.ts:156-169`). - -### 2.4 Validation behavior — "the throwing loader" - -`validateConfigShape` is field-by-field, hand-written, and **deliberately uses -no schema library** — each check raises a structured error naming the offending -field (`load-config.ts:68-127`). Every failure path is a **`throw`**, not a -returned value. This is what §S3 of the plan calls "the current throwing -loader"; the engine's `ConfigSection.validate` must return -`SectionValidation` and must never throw -(`prisma-cli:.drive/projects/prisma-cli-v8/assets/engine/engine-interface-draft.ts:344-360`). - -The checks, in order: - -| check | failure code | message shape | -|---|---|---| -| default export is a non-null object with at least one key | `CONFIG.EXPORT_INVALID` | `"" exported no config.` (`load-config.ts:75-81`) | -| `extensions` is an array | `CONFIG.FIELD_INVALID` | `prisma-composer.config.ts: \`extensions\` must be an array.` (`:84-86`) | -| each `extensions[i]` is an object | `CONFIG.FIELD_INVALID` | `…\`extensions[i]\` must be an extension descriptor object.` (`:89-91`) | -| each `extensions[i].id` is a non-empty string | `CONFIG.FIELD_INVALID` | `…must be a non-empty string (the extension package name).` (`:92-98`) | -| each `extensions[i].nodes` is an object | `CONFIG.FIELD_INVALID` | `…must be an object (the node-ID → control registry).` (`:99-104`) | -| ids are unique | `CONFIG.EXTENSION_DUPLICATE` | `…extension "" is listed more than once in \`extensions\`.` (`:105-111`) | -| `state` is an object with a string `extension` and a function `create` | `CONFIG.FIELD_INVALID` | `…\`state\` must be a state descriptor (e.g. prismaState()).` (`:114-121`) | - -Every `CONFIG.FIELD_INVALID` carries `fix: "See defineConfig() in '@prisma/composer/config'."` and -`meta: { field }` (`load-config.ts:53-62`). `meta` is **not rendered** by -`renderErrorEnvelope`, so the machine-readable field name never reaches the -user's screen — it is only visible through the programmatic surface. - -Nothing deeper is checked: the descriptors inside each `nodes` registry cannot -be structurally validated at runtime, and the code says so -(`load-config.ts:123-126`). - -**Evaluation failure is separately coded.** If the config module itself throws -while being evaluated (a missing env var, a syntax error, a throwing factory), -the c12 call is wrapped into `CONFIG.EVALUATION_FAILED` with the original as -`cause` and `where.path` naming the file (`load-config.ts:145-154`). - -### 2.5 Every config key - -The type is `PrismaAppConfig` -(`packages/0-framework/1-core/core/src/control/app-config.ts:197-200`): - -| key | type | required | use | -|---|---|---|---| -| `extensions` | `ExtensionDescriptor[]` | yes | Every extension the app deploys through | -| `state` | `StateDescriptor` | yes | The ONE deploy state store — explicit, platform-agnostic, never defaulted by an extension | - -`ExtensionDescriptor` (`app-config.ts:31-80`): - -| key | type | required | use | -|---|---|---|---| -| `id` | `string` | yes | The extension's package name, e.g. `"@prisma/composer-prisma-cloud"`; matched against a node's `extension` field | -| `nodes` | `Record` | yes | One registry per extension keyed by node ID; each entry is `kind: 'resource' \| 'service' \| 'build'` (`app-config.ts:187-190`) | -| `provisions` | `ReadonlyMap` | no | Param provisioners keyed by need brand (ADR-0031) | -| `application` | `ApplicationDescriptor` | no | Once-per-lowering hook for the app's shared infrastructure (prisma-cloud's Project) | -| `providers` | `() => Layer.Layer` | no | The extension's Alchemy providers, merged across extensions in config order | -| `preflight` | `(input) => Promise` | no | Deploy-time prerequisite check; runs after containers resolve, before any stack file is written (ADR-0029) | -| `teardown` | `(input) => Promise` | no | Destroy-time cleanup; runs after `alchemy destroy` succeeds, before containers are removed | -| `container` | `ContainerDescriptor` | no | The extension's container lifecycle — `ensure` / `locate` / `remove`, plus the `alchemyStage` an instance supplies (ADR-0038) | -| `localTarget` | `() => Promise` | no | Lazy async thunk to the extension's local-target entry (ADR-0041); keeps local-target code out of every deploy path's static graph | - -`StateDescriptor` (`app-config.ts:86-91`): `extension` (the owning extension's -id) and `create(container) => AlchemyStateLayer`. - -`LocalTargetDescriptor` (`app-config.ts:112-125`) is the `dev`/`log` surface: -`providers`, `container`, optional `preflight`, optional `emulators`, -`attach`, optional `teardown`. `attach` returns a `LocalTargetAttachment` -with `startServices()`, `endpoints()`, `logs(signal, { tail })` and -`stopServices()` (`app-config.ts:147-159`). - -### 2.6 Engine notes on the config port - -The engine's `ConfigSection` is `{ name, validate }` where `validate` takes -the raw section value **or `undefined`** and returns findings, never throwing -(`prisma-cli:…/engine-interface-draft.ts:348-360`). The port has to answer -three questions that today's loader does not: - -1. **File identity.** Today the section is a whole separate file discovered by - walking up from the *entry*. The engine's model is a named section inside - `prisma.config.ts`. Whether `composer` becomes a section of - `prisma.config.ts`, or the engine's section loader is pointed at - `prisma-composer.config.ts`, is an open decision. UNKNOWN from the composer - repo alone — this is an S3 design ruling. -2. **Absence.** Today absence is a hard `CONFIG.FILE_MISSING` failure with a - fix. Under the engine, the validator owns absence and returns a - section-required diagnostic. -3. **Executable values.** The section's validated value holds **functions and - Effect Layers** (`create`, `providers`, `preflight`, `attach`, …). The - engine's draft says validators load with the definition tree at startup and - should be dependency-light (`engine-interface-draft.ts:346-347`). A composer - section validator that must import extension packages to have anything to - validate is in direct tension with that. This is the sharpest config - question S3 has to settle. - ---- - -## 3. Alchemy integration - -One caveat applies to this whole section: `node_modules` is **not installed** -in the clone, so nothing about Alchemy's own internals could be read. Every -claim below comes from composer's own source. - -### 3.1 Process model — a spawned child, never in-process - -Composer never runs Alchemy as a library. It writes a generated stack file and -shells out to the workspace's installed `alchemy` bin. The full mechanics, -including the exit-code passthrough, are in §1.7. - -The one place Alchemy code *is* loaded in-process is incidental and is treated -as a problem: importing the app's config and providers transitively loads -alchemy's provider tree, which is why `bin.ts` runs the effect-version -preflight first (`packages/0-framework/3-tooling/cli/src/bin.ts:6-13`), why the -executor modules are behind lazy imports -(`packages/0-framework/3-tooling/cli/src/operations/deploy.ts:45-47`), and why -`dev` strips alchemy's signal listeners -(`packages/0-framework/3-tooling/cli/src/dev/run-dev.ts:114-127`). - -**Generated stack files.** - -| command | file | contents | -|---|---|---| -| `deploy` / `destroy` | `.prisma-composer/alchemy.run.ts` | `lower(app, config, { name, bundles, report })` — no `state`, no `providers`; those come from the config (`packages/0-framework/3-tooling/cli/src/generate-stack.ts:59-79`) | -| `dev` | `.prisma-composer/dev/alchemy.run.ts` | pins `providers: localTargetProviders(...)` and `state: localState()` from `alchemy/State/LocalState` (`packages/0-framework/3-tooling/cli/src/dev/generate-dev-stack.ts:28-29`, `:55-56`, `:77`) | - -**Container transport.** Each extension's resolved container crosses into the -child as one env var, `PRISMA_COMPOSER_CONTAINER_` — -e.g. `@prisma/composer-prisma-cloud` becomes -`PRISMA_COMPOSER_CONTAINER_PRISMA_COMPOSER_PRISMA_CLOUD` -(`packages/0-framework/1-core/core/src/container-transport.ts:54-61`, built at -`:78-94`). The value is serialized JSON -`{ input: { appName, stage }, projectId, branchId?, defaultBranchId? }` -(`packages/1-prisma-cloud/1-extensions/target/src/container.ts:57-64`), read -back in the child by `deserializeContainers(config.extensions, process.env)` -(`packages/0-framework/1-core/core/src/control/deploy.ts:576`, `:759`). The CLI -is content-blind: it writes these values and never reads them -(`packages/0-framework/3-tooling/cli/src/run-alchemy.ts:40-41`). - -### 3.2 Providers - -Provider code lives in **two** packages, neither of which is the CLI. - -**`@internal/lowering`** (`packages/1-prisma-cloud/0-lowering/lowering`) — the -eight management-API-backed providers, bundled into a -`Provider.ProviderCollection` named `'Prisma'` at -`packages/1-prisma-cloud/0-lowering/lowering/src/providers.ts:19-51`. All paths -below are relative to that package. - -| resource | file | management API calls | -|---|---|---| -| `Prisma.Project` | `src/postgres/Project.ts` | `GET /v1/projects/{id}` (:37, :62); `POST /v1/projects` body `{name, workspaceId}` (:46); `DELETE /v1/projects/{id}` (:54) | -| `Prisma.Database` | `src/postgres/Database.ts` | `GET /v1/databases/{databaseId}` (:47, :94); `POST /v1/databases` body `{projectId, name, region, isDefault?, branchId?}` (:58); `PATCH /v1/databases/{databaseId}` body `{branchId}` (:75); `DELETE /v1/databases/{databaseId}` (:86) | -| `Prisma.Connection` | `src/postgres/Connection.ts` | `POST /v1/databases/{databaseId}/connections` body `{name}` (:42); `DELETE /v1/connections/{id}` (:68) | -| `Prisma.ComputeService` | `src/compute/ComputeService.ts` | `GET /v1/apps/{appId}` (:77, :120); `POST /v1/apps` (:95); `DELETE /v1/apps/{appId}` (:112) | -| `Prisma.Deployment` | `src/compute/Deployment.ts` | `POST /v1/apps/{appId}/deployments` (:88); raw `PUT` to the returned `uploadUrl` via `fetch` (:107); `POST /v1/deployments/{deploymentId}/start` (:123); `GET /v1/deployments/{deploymentId}` (:62, :150); `POST /v1/apps/{appId}/promote` body `{deploymentId}` (:134) | -| `Prisma.EnvironmentVariable` | `src/compute/EnvironmentVariable.ts` | `GET /v1/environment-variables/{envVarId}` (:60, :141); `GET /v1/environment-variables` (:68); `PATCH /v1/environment-variables/{envVarId}` body `{value}` (:110); `POST /v1/environment-variables` (:119); `DELETE /v1/environment-variables/{envVarId}` (:133) | -| `Prisma.Bucket` | `src/buckets/Bucket.ts` | `GET /v1/buckets/{bucketId}` (:37, :68); `POST /v1/buckets` body `{projectId, name, branchId?}` (:46); `DELETE /v1/buckets/{bucketId}` (:60) | -| `Prisma.BucketKey` | `src/buckets/BucketKey.ts` | `POST /v1/buckets/{bucketId}/keys` body `{name, role: 'read_write'}` (:59); `DELETE /v1/buckets/{bucketId}/keys/{keyId}` (:78) | - -Also in that package, with no API calls: `PrismaCloud.ServiceKey` -(`src/compute/ServiceKey.ts`) mints a 256-bit hex key once and keeps it in -Alchemy state. - -**`@internal/prisma-cloud`** (`packages/1-prisma-cloud/1-extensions/target`) — -four more resources merged into the same provider layer at -`src/control/extension.ts:329-339`: `PrismaCloud.S3Credentials` -(mints a SigV4 key pair once), `PrismaCloud.GeneratedParam` (N random bytes, -base64, once), `PrismaCloud.PnMigration` (runs a Prisma-Next migration against -the resolved database URL), `PrismaCloud.PgWarm` (connects with `pg` and runs -`select 1` to ride out cold start). - -**`@internal/local-target`** -(`packages/1-prisma-cloud/0-lowering/local-target/src/providers.ts:24-51`) — -the same eight resource tags backed by local emulator providers. It has no -management-API client and no credentials layer at all (`:1-7`). Used only by -`dev`. - -`@internal/s3-protocol` defines no Alchemy providers. - -**Client.** `@prisma/management-api-sdk` at `^1.57.0` -(`packages/1-prisma-cloud/0-lowering/lowering/package.json:23`), an -openapi-fetch client built once at -`packages/1-prisma-cloud/0-lowering/lowering/src/client.ts:22-34`. The default -origin is `https://api.prisma.io` -(`packages/1-prisma-cloud/0-lowering/lowering/src/client.ts:11`), overridable -via an `apiOrigin` option. - -### 3.3 Credentials — where they enter - -**Three environment variables, and nothing else.** - -| var | purpose | read at | -|---|---|---| -| `PRISMA_SERVICE_TOKEN` | Bearer token for every management API call | `packages/1-prisma-cloud/0-lowering/lowering/src/credentials.ts:19-25` (`Config.redacted('PRISMA_SERVICE_TOKEN')`, held as `Redacted`); direct presence checks at `packages/1-prisma-cloud/1-extensions/target/src/container.ts:139-150`, `:263`, and `packages/1-prisma-cloud/1-extensions/target/src/preflight.ts:185` | -| `PRISMA_WORKSPACE_ID` | Workspace the app's Project is resolved in | `packages/1-prisma-cloud/1-extensions/target/src/container.ts:139`; default for the `prismaCloud()` option at `packages/1-prisma-cloud/1-extensions/target/src/control/extension.ts:285` | -| `PRISMA_REGION` | Optional default compute region, validated against `COMPUTE_REGIONS` | `packages/1-prisma-cloud/1-extensions/target/src/control/extension.ts:291-300` | - -**There is no keychain, no credentials file, no OAuth flow, and no token -refresh anywhere in this repo.** A missing token is a literal error: -"environment variable `PRISMA_SERVICE_TOKEN` is required." -(`packages/1-prisma-cloud/1-extensions/target/src/container.ts:135-136`). - -**Where credentials enter, precisely.** Two places, both outside the CLI -package: - -1. **In the CLI's own process**, when the extension's `container.ensure` / - `container.locate` runs before the stack file is written - (`packages/0-framework/3-tooling/cli/src/operations/execute-deploy-destroy.ts:140`, - `:143`) and when `preflight` runs (`:190`). The extension reads the env var - itself; the CLI never sees a token. -2. **In the alchemy child**, which inherits the parent's whole environment via - the `spawnSync` env spread - (`packages/0-framework/3-tooling/cli/src/run-alchemy.ts:55`), so the - providers read the same env var again. - -This is the reconciliation point for the v8 credential manager. The engine's -model is per-workspace sessions obtained through -`ctx.session()` / `ctx.credentialManager` -(`prisma-cli:.drive/projects/prisma-cli-v8/assets/engine/engine-interface-draft.ts:395`, -`:502-524`). Composer's model is a raw env var read independently by extension -code in two processes. Bridging them means either (a) the ported commands -resolve credentials from the engine's session and inject -`PRISMA_SERVICE_TOKEN` into the child's environment — which keeps the -extensions untouched but writes a secret into a child env, or (b) the -extensions gain a way to receive a token from the caller. **UNKNOWN which**; -this is an S3 design ruling, not a fact recoverable from the clone. - -### 3.4 State - -**Remote — the hosted deploy path.** Alchemy state lives behind the **Prisma -management API**, not S3 -(`packages/1-prisma-cloud/0-lowering/lowering/src/state/layer.ts:109-115`): the -stock `makeHttpStateStore` from `alchemy/State` is pointed at - -``` -{apiOrigin}/v1/projects/{projectId}/branches/{stateBranchId}/alchemy-state -``` - -Selection is the config's `state:` field — -`opts.state ?? config.state.create(containers.get(config.state.extension))` -(`packages/0-framework/1-core/core/src/control/deploy.ts:523-529`), passed to -`Alchemy.Stack` at `:762-766`. The user-facing descriptor is `prismaState()` -(`packages/1-prisma-cloud/1-extensions/target/src/control/extension.ts:161-171`). -This is ADR-0045 ("deploy state lives behind the platform state API"). - -**Concurrency control.** A per-(stack, stage) deploy lease, acquired on layer -init and released in a finalizer -(`packages/1-prisma-cloud/0-lowering/lowering/src/state/layer.ts:84-88`). -Endpoints at -`packages/1-prisma-cloud/0-lowering/lowering/src/state/lease.ts:26`: -`POST …/alchemy-state/lease` body `{stack, stage, holderDescription}` -(`:69-77`, 409 on contention, fails fast with no retry); `PATCH` the same path -as a heartbeat every 20s (`:106-134`, a 404 means the lease was lost — one -warning, then stop); `DELETE` on clean exit (`:141-173`, never throws). Every -state operation carries an `Alchemy-State-Lease-Id` header (`:11`), added to -Effect's redacted-header list (`:18-24`). This is ADR-0010. - -**Bootstrap guard.** Before the store exists, `scopeOccupied` calls -`GET …/alchemy-state/state/stacks/{stack}/stages/{stage}/resources` -(`packages/1-prisma-cloud/0-lowering/lowering/src/state/empty-scope.ts:19-33`). -If it is empty, it lists `GET /v1/apps`, `GET /v1/databases`, -`GET /v1/buckets` scoped to the branch (`:55-66`) and refuses the deploy if any -live resource exists (`:87-111`). - -**Local.** - -| path | what | -|---|---| -| `/.alchemy/` | Referenced by composer only as the destroy guardrail (`packages/0-framework/3-tooling/cli/src/operations/execute-deploy-destroy.ts:28-34`). Dev's local state lives at `/.alchemy/state//dev`, per the `--fresh` teardown at `packages/1-prisma-cloud/1-extensions/target/src/local-target/teardown.ts:39` | -| `/.prisma-composer/` | Generated stack files and the per-run deployment-result JSON | -| `/.prisma-composer/dev/` | The dev state directory (`packages/0-framework/1-core/core/src/control/app-config.ts:162`) | - -**UNKNOWN:** whether the hosted-state path also writes anything under -`/.alchemy`. The remote store is purely HTTP, so on this evidence it -should not — which would make the destroy guardrail a check on a directory -only `dev` populates, and therefore misleading. Resolving this needs Alchemy's -own source (`makeHttpStateStore` and the CLI's state bootstrap), absent from -this clone. **Flagged as hazard H6.** - -**Containers are outside Alchemy entirely.** Project and Branch are -found-or-created by the CLI *before* the stack runs -(`packages/1-prisma-cloud/0-lowering/lowering/src/container.ts:192-206`) and -removed *after* destroy (`:213-236`, driven from -`packages/1-prisma-cloud/1-extensions/target/src/container.ts:289-301`). They -are never Alchemy resources -(`docs/design/05-prisma-cloud/alchemy-lowering.md:80-84`). - ---- - -## 4. The three S8 questions - -### 4a. Does Alchemy hold desired state for which deployment is live? - -**No — but a redeploy still overwrites an out-of-band promotion, by -superseding it rather than reverting it.** - -Evidence: - -- **`Deployment.reconcile` is unconditionally imperative** - (`packages/1-prisma-cloud/0-lowering/lowering/src/compute/Deployment.ts:82-142`): - create → upload artifact → start → poll until `running` → - `POST /v1/apps/{appId}/promote`. The code states why there is no - short-circuit at `:83-86` — a props change (a new `artifactHash`) is what - brought it here, so returning the previous deployment would strand the new - build. Whenever reconcile runs it mints a **brand-new** deployment and - promotes it. -- **`Deployment.read` does not read promotion state** - (`Deployment.ts:147-160`). It returns - `{ deploymentId: v.data.id, deployedUrl: v.data.previewDomain }` — the - *preview* domain of the deployment recorded in state, not the app's - currently-promoted deployment. Nothing anywhere reads "which deployment is - currently live". -- **`Deployment.delete` is a no-op** (`Deployment.ts:143-146`) — promoted - deployments are retained as history. -- **`stables: []`** on Deployment (`Deployment.ts:80`), unlike every other - provider, which pins `['id']`. -- The design doc says it outright: "What we deliberately do not model yet … - **Promotion** as a standalone resource (the Deployment provider - auto-promotes; rollback is unexpressed)." - (`docs/design/05-prisma-cloud/alchemy-lowering.md:77-80`) - -**Consequences for S8's five imperative commands.** There is no resource whose -props say "deployment X is promoted", so nothing reconciles a promotion back. -The behavior splits: - -- **Build unchanged** (same `artifactHash`, `port`, `environment` record refs): - the Deployment resource does not diff, `reconcile` does not run, and - composer's next `deploy` leaves an out-of-band promotion or rollback in - place. An out-of-band *stop* is likewise not restarted — nothing checks - running state outside `reconcile`'s own poll. -- **Anything diffs** (any new build changes `artifactHash`, - `Deployment.ts:15-20`): `reconcile` runs, creates a *new* deployment, and - promotes it. A manual rollback is silently discarded — not reverted to a - recorded desired state, but superseded by a fresh deployment. - -So an imperative `promote`/`rollback`/`start`/`stop` in the CLI would not be -fought by a declarative controller. It would simply be undone by the next -`composer deploy` that changes the build — which is the normal expectation for -"I rolled back, then someone deployed again" and does not by itself argue -against the commands existing. - -**UNKNOWN, and it matters.** Whether Alchemy's planner calls provider `read` -for drift detection on every apply and treats an attribute mismatch as a -change. If it does, `Deployment.read` returning `previewDomain` -(`Deployment.ts:157`) while `reconcile` persisted the post-promote -`appEndpointDomain` (`:140`) looks like permanent attribute drift, and could -force a reconcile — a fresh deploy-and-promote — on *every* run, even with an -unchanged build. That would flip the answer above for the unchanged-build case -and would make the five imperative commands genuinely unstable. -**What would resolve it:** reading Alchemy 2.0.0-beta.67's planner source -(`read`/`diff`/`stables` semantics), which requires an installed -`node_modules` or the upstream repo. The in-repo inspiration notes say -"`read` + `diff` build the plan; `reconcile` + `delete` apply it" -(`docs/design/04-inspirations/Alchemy/glossary.md:132`), but that documents a -general/older Alchemy, not the pinned beta, so it is not sufficient evidence. - -### 4b. What do Composer-created app and deployment records contain? - -**App / service — `POST /v1/apps`** -(`packages/1-prisma-cloud/0-lowering/lowering/src/compute/ComputeService.ts:94-103`): - -```ts -body: { - displayName: news.name, // the node's graph address - projectId: news.projectId, - ...(news.region && { regionId: news.region }), - ...(news.branchId !== undefined && { branchId: news.branchId }), -} -``` - -Props come from the compute descriptor -(`packages/1-prisma-cloud/1-extensions/target/src/descriptors/compute.ts:71-77`): -`name` is the node's graph address; `projectId` from the application hook; -`region` is `o().region ?? DEFAULT_REGION`; `branchId` is set **only for a -named stage**. Attributes kept in state: `{id, name, endpointDomain}` -(`ComputeService.ts:104-108`), `stables: ['id']`. - -`branchId` is in the create body rather than a later PATCH because a create -without it lands on the default Branch and collides with the production app of -the same name (`ComputeService.ts:90-93`). - -**Deployment — `POST /v1/apps/{appId}/deployments`** -(`Deployment.ts:87-92`): - -```ts -body: news.port !== undefined ? { portMapping: { http: news.port } } : {} -``` - -That is the **entire** request payload. Everything else is out of band: - -- The artifact is `PUT` raw to the `uploadUrl` returned in the create response - (`Deployment.ts:95-120`). -- Environment variables are **not** in the body. The platform materializes the - branch's ConfigVariables into the deployment at create time; the - `environment` prop exists only as an Alchemy dependency edge so the variable - writes are ordered before deployment-create (`Deployment.ts:26-35`; design - note at `docs/design/05-prisma-cloud/alchemy-lowering.md:177-183`). -- `artifactHash` is a prop but is never sent — it exists so a new build diffs - as a change (`Deployment.ts:15-20`). - -Attributes persisted: `{deploymentId, deployedUrl?}`, where `deployedUrl` is -`promoted.data.appEndpointDomain` read *after* promote, because the create-time -domain is a placeholder (`Deployment.ts:130-132`, `:140-141`). - -**Environment variables — `POST /v1/environment-variables`** -(`EnvironmentVariable.ts:118-128`): - -```ts -body: { projectId, class: cls, key, value, ...(branchId ? { branchId } : {}) } -``` - -`class` is `'preview'` on a named stage (with `branchId`) and `'production'` on -the default stage -(`packages/1-prisma-cloud/1-extensions/target/src/descriptors/compute.ts:88-90`). -Rows written per deploy: every resolved param (`compute.ts:111-119`), the -serialized input document (`:129-139`), one per generated leaf (`:150-157`), -one per reserved provider param — RPC accepted keys, streams API key, self -origin (`:214-222`) — and the two poison rows `DATABASE_URL` and -`DATABASE_URL_POOLED` set to `"-"` -(`packages/1-prisma-cloud/1-extensions/target/src/control/extension.ts:355-373`). - -**Comparison to the legacy `app deploy` path.** The legacy path's field set is -recorded in `prisma-cli:.drive/projects/prisma-cli-v8/assets/s2/command-inventory.md` -(the `app deploy` entry: `ComputeClient.deployApp`, `POST /v1/projects`, -branches, env vars, optional `POST /v1/databases`). A field-by-field diff of -the two payloads **cannot be completed from the composer clone alone** — it -needs the prisma-cli side's `deployApp` request body read against these. What -*is* established here is the shape of the Alchemy path, which is the half S8 -said nobody had looked at. - -Two gaps worth carrying into S8's design: - -- **`displayName` is the node's graph address**, not a user-chosen app name. - A `service list` presenting `displayName` will show graph addresses for - composer-created services. -- **An env-var value change does not propagate to a new deployment.** - `EnvironmentVariable` exposes only `{id, key}`, so a rotated value does not - diff the consumer `Deployment` and no new version is created — a known - deferred gap (`docs/design/05-prisma-cloud/alchemy-lowering.md:185-189`). - -### 4c. Where does log reading live? - -**`composer log` reads local dev-emulator logs over HTTP from a daemon on the -developer's own machine. It never calls the management API, and it has no -relationship to `/v1/deployments/{id}/logs` whatsoever.** - -The chain: - -1. `packages/0-framework/3-tooling/cli/src/log/run-log.ts:33-56` delegates to - the operation and prints `[service] line`. -2. `packages/0-framework/3-tooling/cli/src/operations/execute-log.ts:151-169` - resolves the app identity, then `resolveLocalTargets(identity.config)`, - `target.container.ensure({appName, stage: undefined})`, and - `target.attach({container, devDir})` where - `devDir = /.prisma-composer/dev`. -3. The attachment's `logs()` is the local emulator client: - `packages/1-prisma-cloud/1-extensions/target/src/local-target/attach.ts:111`, - backed by - `packages/1-prisma-cloud/0-lowering/dev-emulators/src/client.ts:251` — - `GET {baseUrl}/apps//services//logs?follow=1[&tail=N]` against the - local compute-emulator daemon, over HTTP streaming. - -A search for `/logs` across every package in the repo returns only the -dev-emulator client and its tests. There is no websocket log path and no S3 -log stream. - -**What this means for S8's ownership question.** The project spec's rule that a -subgroup is owned by exactly one command family is **not** in tension here. -`composer log` and a platform `service deployment logs` read two different -things from two different places: - -| | `composer log` | proposed `service deployment logs` | -|---|---|---| -| source | local dev-emulator daemon | `GET /v1/deployments/{id}/logs` | -| scope | the app running on this machine under `composer dev` | a deployed remote deployment | -| credentials | none | platform session | -| exists today | yes | no | - -They are not two ways to read the same thing. The naming is the only collision, -and it is a real one: a user who has run `composer dev` and then types -`prisma service deployment logs` should not be surprised, and vice versa. The -S8 design should name them so the local/remote split is visible — this is a -naming decision, not an ownership conflict. - -One consequence for the port: because `log` needs no credentials and no -management API, it is the cheapest of the four commands to move onto the -engine and the best first proof of the engine's session-command kind. - -## 5. Dependency and release surface - -### 5.1 What is published - -Exactly two packages: - -| name | version | path | -|---|---|---| -| `@prisma/composer` | `0.6.0` | `packages/9-public/composer/package.json:2-3` | -| `@prisma/composer-prisma-cloud` | `0.6.0` | `packages/9-public/composer-prisma-cloud/package.json:2-3` | - -Everything else is `private: true` — the 18 `@internal/*` packages (including -the CLI), `website/`, `test/integration/`, every `examples/*`, and the -workspace root (`package.json:2-3`). `publishConfig.access` is `public` -(`packages/9-public/composer/package.json:70-72`); `engines.node` is `>=24` -(`:67-69`). - -**How the CLI reaches the registry.** Not `bundleDependencies` — that key -appears nowhere. tsdown inlines the `@internal` scope: -`packages/9-public/composer/tsdown.config.ts:30` sets -`skipNodeModulesBundle: false` and `:36` sets `noExternal: [/^@internal\//]`, -so the tarball is self-contained while external npm deps stay real imports -(`tsdown.config.ts:4-7`). The `@internal/*` packages are declared as -**devDependencies** in the public manifests so they never reach the registry -(`packages/9-public/composer/package.json:47-56`), a rule enforced by -`scripts/check-publish-deps.mjs:152-163` and grounded in ADR-0028 -(`docs/design/90-decisions/ADR-0028-numbered-domains-and-layers-enforced-by-dependency-cruiser.md:48-53`). - -**Direct consequence for S3.** Only `@internal/*` is inlined. Any dependency -that must survive as a real runtime import has to be **mirrored** into -`packages/9-public/composer/package.json` `dependencies` — which is exactly why -`c12`, `clipanion`, `esbuild`, `alchemy`, `effect`, `arktype` and -`@prisma/management-api-sdk` all appear there -(`packages/9-public/composer/package.json:36-46`) duplicating -`packages/0-framework/3-tooling/cli/package.json:18-25`. **`@prisma/cli-engine` -will have to be declared in both places, at the same exact version.** - -### 5.2 Versioning: lockstep, no changesets - -There is no `.changeset/` directory and no changesets dependency. Every -workspace package — publishable, private, and the root — carries the same -`version` (`docs/oss/versioning.md:24-41`); all manifests currently read -`0.6.0`, with the root `package.json:45` as the source of truth. - -| script | what it does | -|---|---| -| `pnpm bump-minor` → `scripts/bump-minor.ts` | Reads the root version at git HEAD (`:30-47`), computes the next minor (`:49`), calls `set-version.ts` (`:55-59`), regenerates the lockfile (`:62-65`) | -| `scripts/set-version.ts:49-60` | Stamps every package and rewrites `workspace:` deps to `workspace:` | -| `scripts/determine-version.ts:140-168` | Picks version and dist-tag per CI event | - -Release procedure (`docs/oss/versioning.md:94-106`): run `pnpm bump-minor`, -open a PR titled `chore(release): v`, and **merging that PR is the -publish trigger**. - -### 5.3 Pinning - -Mixed, with no general policy document. CONTRIBUTING.md, AGENTS.md, CLAUDE.md -and README.md contain no dependency-version guidance. - -Exact pins exist where type identity or breakage demands them: -`alchemy: "2.0.0-beta.67"` (`packages/9-public/composer/package.json:39`, plus -five more, and patched via root `package.json:38-40` `patchedDependencies`); -`effect: "4.0.0-beta.103"` and the `@effect/*` family (`:37`, `:43`); -`@prisma/orm-*: "8.0.0-rc.1"` -(`packages/9-public/composer-prisma-cloud/package.json:47`, `:58`, `:77`). - -**But `@prisma/management-api-sdk` is a caret — `^1.57.0`** -(`packages/9-public/composer/package.json:45`, -`packages/9-public/composer-prisma-cloud/package.json:46`, -`packages/1-prisma-cloud/0-lowering/lowering/package.json:22`). So the nearest -existing analogue to a new `@prisma/*` external dependency is a range, not an -exact pin. - -Targeted pin checks that do exist: - -| script | what it forces | -|---|---| -| `scripts/lint-orm-pins.mjs:24-25`, `:52-72` | One identical exact version, for `@prisma/orm-*` only | -| `scripts/check-npm-effect-resolution.mjs:58-62` | `effect` is exact in `@prisma/composer` | -| `scripts/check-publish-deps.mjs:65`, `:109-141` | Exact `X.Y.Z` for *workspace-internal* deps | - -**The finding that matters for S3's exact-pin requirement:** "internal" in -`check-publish-deps.mjs` is determined by `pnpm list -r`, not by the `@prisma/` -scope (`scripts/check-publish-deps.mjs:25-28`, `:192-208`; -`docs/oss/versioning.md:83-86`). `@prisma/cli-engine` would therefore be -treated as an ordinary external dependency and **no existing check would force -it to be exact-pinned.** Making the pin mechanical means extending -`lint-orm-pins.mjs` or writing a sibling script. - -### 5.4 CI - -Six workflows under `.github/workflows/`: `ci.yml`, `dco.yml`, -`deploy-docs.yml`, `e2e-deploy.yml`, `preview-publish.yml`, `publish.yml`. - -**`publish.yml` is the only npm publisher.** Triggers: push to `main` with tags -explicitly excluded (`:17-20`, `tags: ["!**"]`), and `workflow_dispatch` with -`dist-tag` and `dry-run` inputs (`:21-32`). **There is no tag trigger.** The -model (`:3-15`): a push to main with the root version unchanged publishes -`-dev.N` on the `dev` tag; a changed root version publishes `` on -`latest` plus a GitHub Release. Auth is npm **OIDC Trusted Publishing** — no -`NODE_AUTH_TOKEN` — with `NPM_CONFIG_PROVENANCE: "true"` (`:47-49`, `:97-105`). -The actual command is -`pnpm publish --access public --tag --no-git-checks` -(`scripts/publish-packages.mjs:87`), idempotent on already-published versions -(`:26-31`, `:114-116`). - -**Tarball verification is manifest-only.** `check-publish-deps.mjs:165-170`, -`:313-337` packs each publishable tarball and inspects -`package/package.json` inside it. It does not check the emitted JavaScript. - -**`publish.yml` runs no lint, typecheck, or test step** (verified across -`:51-142`). Those checks live only in `ci.yml`, on the PR: `lint` (biome plus -`pnpm lint:deps`, `:16-29`), `typecheck` (`:31-42`), `test` with a -`postgres:16` service (`:44-102`), `cast-ratchet` (`:104-124`), -`npm-effect-resolution` — which packs both public packages and installs them -with real npm (`:126-144`) — and `build` plus a clean-worktree check -(`:146-166`). - -**One Node version, no matrix.** Every job uses `./.github/actions/setup` → -`jdx/mise-action` (`.github/actions/setup/action.yml:10-11`) reading -`.tool-versions:1-2` → `node 24.16.0`, `bun 1.3.13`. - -`preview-publish.yml` does pkg.pr.new previews (`:62-77`), not npm publishes. - -### 5.5 Constraints on adding an exact-pinned `@prisma/cli-engine` - -- **No renovate.** Dependency automation is Dependabot - (`.github/dependabot.yml`): npm at `/` covering the pnpm workspace - (`:31-32`), weekly (`:33-37`), grouped runtime/dev (`:52-62`), with an - `ignore` list (`:63-74`). A hand-coordinated dependency needs an ignore - entry. The precedent is `@durable-streams/server-conformance-tests`, ignored - because it is "bumped by hand together with streams-server" (`:70-73`) — - the closest documented analogue to a tandem release anywhere in this repo. -- **dependency-cruiser does not restrict external dependencies.** Its rules - (generated from `architecture.config.json`) constrain module-to-module import - edges only — upward `:91-108`, cross-domain `:110-128`, plane `:130-158`, - `public-is-a-sink` `:161-178`, examples `:179-193` — and it does not follow - `node_modules` (`:204-215`). ADR-0028 explicitly permits external - dependencies at every layer. A `9-public` package may freely add one. -- `scripts/lint-publishable-location.mjs:29-44` — anything outside - `packages/9-public/` must be private; anything inside must not be. -- **No pnpm catalog.** `pnpm-workspace.yaml` has only `packages:`, and - `check-publish-deps.mjs:76-78` actively rejects `catalog:` specifiers in - packed manifests. Each manifest declares its own versions; there is no - central place to pin. -- If `@prisma/cli-engine` ever has a postinstall or build step, it must be - added to root `package.json:33-36` `onlyBuiltDependencies` (currently - `["prisma", "@prisma/engines"]`). -- pnpm `10.27.0` is pinned via `packageManager` (root `package.json:4`); - `.npmrc:5` sets `node-linker=hoisted`; CI always installs - `--frozen-lockfile`. -- `.agents/rules/exports-entrypoints.mdc:48-49` requires new public entrypoints - to be registered in `architecture.config.json` with non-overlapping globs — - relevant if consuming the engine's `./protocol` subpath adds one. - -**UNKNOWN — the tandem-release protocol.** Nothing in the composer repo -references cross-repo release coordination, `cli-engine`, or a "tandem" -release; there is no submodule and no workflow referencing another repo. A -repo-wide grep of the clone for `cli-engine` and `tandem` returns zero hits. -**What would resolve it:** the prisma-cli side's publish workflow plus a -written decision on the coordination protocol. Neither exists yet — designing -it is S3 work. - -**UNKNOWN — externalization semantics under `skipNodeModulesBundle: false`.** -Whether declared `dependencies` are automatically left external is not -documented, and the evidence is mixed: `esbuild` is both a declared dependency -*and* explicitly listed in `external` -(`packages/9-public/composer/tsdown.config.ts:35`), while `chokidar` is an -`@internal/cli` dependency that is *not* mirrored into the public manifest. -**What would resolve it:** running `pnpm --filter @prisma/composer build` and -grepping the emitted `dist/*.mjs` for surviving imports. No `dist/` is checked -in. This must be settled before `@prisma/cli-engine` is added, or the engine -could silently end up inlined into the tarball — which would break the -published-consumption proof S3 exists to deliver. **Flagged as hazard H7.** - ---- - -## 6. The paused 1c brief - -**Path:** `.drive/projects/prisma-cli-v8/assets/briefs/1c-leftovers-composer.md` -(in the prisma-cli repo, not the composer clone). - -**Title:** "Brief: composer config-contract compliance and control-API test -double" (`:1`). Target repo prisma/composer (main); operator Will Madden; -three deliverables (`:3`). - -It has exactly one commit — `6abc20f docs(architecture): requirements for the -unified CLI engine (#128)`, 2026-08-10 — so there is no in-repo edit trail. -Nothing under `wip/repos/composer/docs` mentions it. - -**Context it locks down** (`:7`): composer's error and result rules come from -its ADR-0043 and ADR-0044 — structured errors at origin with dotted codes from -a **closed** registry, one `ok` discriminator, exit 1 for bugs only; adding a -subcode means editing that list in the same change. Plus one constraint marked -as immovable: the effect constellation stays pinned at `4.0.0-beta.103` via the -consumer overrides block, because alchemy is broken on effect ≥ beta.104. - -### The three deliverables - -**1 — config validation returns diagnostics instead of throwing** (`:9-17`). -Today `load-config.ts` and `validate-coverage.ts` throw on the first invalid -field; both still do (see §2.4). The target: loading returns the evaluated -value **plus a diagnostics list** tagged by config section and field via -`meta`; a command fails (exit 2) only when a section it needs is invalid; an -unevaluatable config module yields one `CONFIG.EVALUATION_FAILED` diagnostic -that fails every command early; "no import-time side effects and no throwing -from `defineConfig`-equivalent factories" (`:16`); rendered and `--json` output -pinned before and after, with user-visible behavior allowed to change "only in -framing" (`:17`). - -**2 — the effect-resolution preflight becomes a diagnostic** (`:19-26`). -`check-effect-resolution.ts` throws during import and takes out every command -(see §1.2). The target: run it at config-load / command-dispatch time as a -`DEPS.EFFECT_VERSION_CONFLICT` diagnostic inside deliverable 1's list, so -help and config-inspection commands keep working; the -`DEPS.EXECUTOR_UNLOADABLE` lazy path stays as the backstop; the effect CI probe -and the `npm install effect` dedupe check must still pass. - -**3 — a published test double for the control API** (`:28-30`). Hosts driving -`@prisma/composer/control` (deploy/destroy/dev/log) need a double that never -spawns alchemy or containers: fixture-backed, exported from a published -entrypoint (placement judged against the existing `./control` shim, which -exists at `packages/9-public/composer/package.json:12`), the same operation -signatures and `Result<…, CliStructuredError>` shapes, per-operation fixtures -overridable per test, a working `DevSession` double, and a compile-time -conformance check that the double's surface matches the real operations. - -The brief also carries a verification list (`:32-34`) and commit discipline -(`:36-38`). - -### Why it is paused - -Recorded at `.drive/projects/prisma-cli-v8/design-notes.md:47-56`, under -"Hand-off briefs — handed off, PAUSED by the operator": the 1b and 1c briefs -were already with other agents, but the operator paused that work until the -engine lands, because their config deliverables (diagnostics-not-throw loaders, -marker, validators) will be rewritten against the engine's config API. The -sequencing consequence recorded there is that the engine's protocol and -config-section API is upstream of resuming 1b/1c, and that when resumed the -briefs need revision first. `spec.md:121-123` restates it as a non-goal: -resuming 1b/1c is sequenced after the engine's config API lands, their revision -is the trigger to unpause, and their content is not this project's deliverable. - -**UNKNOWN:** the exact pause date and which agents held the brief. Nothing -in-repo records either; the operator's own session history or the composer PR -list would resolve it. - -### What S3 supersedes, and what it does not - -| deliverable | status under S3 | -|---|---| -| 1 — diagnostics-not-throw config loading | **Superseded.** `plan.md:44-45` puts the throwing-loader rewrite in S3, but re-specified against the engine's `defineConfigSection` / validator-owned-absence / `Diagnostic` model rather than 1c's bespoke diagnostics list. 1c's exit-2 rule and per-section failure semantics are replaced by the engine's protocol, not carried over | -| 2 — effect preflight as a diagnostic | **Not clearly covered.** S3 says nothing about it. Moving it off import time is a prerequisite for the "no import-time side effects" property the engine wants (and for S6's import-purity check), so it is adjacent — but no S3 text claims it. Same for the effect `4.0.0-beta.103` pin constraint, which is live for S3's exact-pin work but is not restated in the plan | -| 3 — published control-API test double | **Not covered at all.** Nothing in S3, and nothing in S6 (whose three checks are import purity, validator no-throw, and tarball verification, `plan.md:74-78`), delivers it. Closing 1c against S3 **drops this deliverable** unless it is re-filed | - -Recommendation for the operator: close 1c against S3 for deliverable 1, fold -deliverable 2 into S3 explicitly (the engine's no-throw-at-startup requirement -makes it S3's problem whether or not the plan says so), and take a decision on -deliverable 3 — drop it or re-file it — rather than letting it lapse silently. - ---- - -## 7. Spec discrepancies and hazards - -### 7.1 Where composer's docs or help text disagree with its code - -**D1 — `deploy --help` advertises a flag that always fails.** `--production` -is declared on the shared abstract class `DeployCliCommand` -(`packages/0-framework/3-tooling/cli/src/main.ts:29-32`), so clipanion lists it -in `deploy`'s help. Its description opens with "destroy:", which is the only -hint. Passing it to `deploy` is always `DEPLOY.FLAG_INVALID` -(`main.ts:259-267`). Same for `--stage` on `destroy`, though that one is -genuinely valid. - -**D2 — a stale reference to an unscoped launcher.** `cli.ts:15` says the -function is "Shared by this package's `bin` and the unscoped `prisma-composer` -launcher." No such launcher exists; ADR-0027 explicitly rejected building one -(`docs/design/90-decisions/ADR-0027-two-packages-compose-and-compose-prisma-cloud.md:19`, -`:93`). Cosmetic, but it will mislead a porter looking for a second entry -point. - -**D3 — the `.alchemy` destroy guardrail may check a directory the hosted path -never writes.** `destroy` warns when `/.alchemy` is missing or empty -(`packages/0-framework/3-tooling/cli/src/operations/execute-deploy-destroy.ts:31-34`, -`:103-105`), telling the user they may be in the wrong directory. But hosted -deploy state lives behind the management API (§3.4), and the only confirmed -writer of `/.alchemy` is `dev`'s local state -(`packages/1-prisma-cloud/1-extensions/target/src/local-target/teardown.ts:39`). -If the hosted path writes nothing there, the warning fires on every legitimate -`destroy` from a machine that has only ever deployed, never run `dev`. See H6. - -**D4 — the trusted-publisher repository name is stale.** -`docs/oss/versioning.md:132` names the repository `compose`, while the -manifests say `https://github.com/prisma/composer.git` -(`packages/9-public/composer/package.json:62-66`). - -**D5 — `--tail` accepts malformed input silently.** `main.ts:185` uses -`Number.parseInt(command.tail, 10)`, so `--tail 5abc` becomes `5` rather than a -usage error. The explicit check only rejects `NaN` and negatives (`:186-188`). - -**D6 — every parse failure produces the same message.** Any clipanion parse -error — unmatched command, missing ``, unknown flag, a trailing -`--name` with no value — is replaced by the **full detailed usage text** -(`main.ts:156-159`). The specific reason is discarded. A user who typos a flag -gets a wall of help with no indication of what was wrong. - -**D7 — bare invocation exits 2.** `prisma-composer` with no arguments prints -the usage text to **stderr** and exits **2** (`main.ts:208`), while -`prisma-composer --help` prints the same text to **stdout** and exits **0** -(`main.ts:205-207`). - -### 7.2 Hazards for the port - -**H1 — the child writes to the terminal, and the engine cannot see it.** -`spawnSync(..., { stdio: 'inherit' })` -(`packages/0-framework/3-tooling/cli/src/run-alchemy.ts:51-58`) means alchemy's -entire apply output, and the deploy topology tree printed by the report hook -*inside the child* (`render-deployment.ts:123-127`), bypass the CLI process -completely. The engine's presentation model — `output` events, `--json` -envelopes, quiet mode — has no purchase on any of it. Three consequences: -composer can never have a working `--json` for `deploy` without changing the -process model; the engine's stdout/stderr discipline cannot be enforced; and -`spawnSync` **blocks the event loop**, so a session command's signal handling -cannot run during a converge. This is the single largest porting decision in -S3 and needs an operator ruling. - -**H2 — the passthrough exception and the engine's exit-code model.** -ADR-0044's exception (§1.7) hands the child's arbitrary status through as the -CLI's own. The engine's `CommandDefinition` types exit codes as a documented -`Record` in the range 4–99 -(`prisma-cli:.drive/projects/prisma-cli-v8/assets/engine/engine-interface-draft.ts:846-855`), -and session commands have no exit-code set at all. An arbitrary passthrough -status — which may be `1`, the engine's "internal bug" code — fits neither. -The plan says to preserve the exception, so the engine needs an explicit escape -hatch for it, and `dev` needs one too (its startup-converge failure passes -through, `run-dev.ts:96-99`). - -**H3 — `dev` strips the process's signal listeners.** `run-dev.ts:124-127` -calls `process.removeAllListeners('SIGINT')` and `removeAllListeners('SIGTERM')` -and installs itself as the only listener, because alchemy's transitively-loaded -library code registers listeners that can `process.exit()` synchronously -(`run-dev.ts:114-123`). Under the engine, `context.signal` is the engine's, and -a command that wipes the host's listeners will break it. The underlying cause — -alchemy's import-time side effects — does not go away by porting. - -**H4 — the config validator must import extension packages.** The engine wants -validators dependency-light because they load with the definition tree at -startup (`engine-interface-draft.ts:346-347`). Composer's validated config -value holds functions and Effect Layers supplied by extension packages (§2.5), -and merely *evaluating* the config file pulls alchemy's provider tree into the -process — which is what the effect preflight at `bin.ts:14` exists to guard. -A `composer` section validator cannot be dependency-light as written. - -**H5 — everything composer depends on for deployment is experimental or -pre-release.** `alchemy@2.0.0-beta.67`, exact-pinned *and locally patched* -(root `package.json:38-40`); `effect@4.0.0-beta.103`, which cannot move because -alchemy breaks on beta.104 or later -(`.drive/projects/prisma-cli-v8/assets/briefs/1c-leftovers-composer.md:7`); -`@prisma/orm-*@8.0.0-rc.1`. On the API side, the plan already records that -every deployment endpoint is marked experimental and subject to change without -notice (`plan.md:99`) — and the providers in §3.2 call `POST /v1/apps`, -`POST /v1/apps/{id}/deployments`, `POST /v1/deployments/{id}/start` and -`POST /v1/apps/{id}/promote` directly. The `alchemy-state` and -`alchemy-state/lease` endpoints (§3.4) are a further unversioned surface that -nothing outside composer uses. - -**H6 — hosted-path local state is unresolved.** Whether the hosted deploy path -writes anything under `/.alchemy` could not be established (§3.4). It -determines whether D3's guardrail is correct or misleading. Resolving it needs -Alchemy's own source. - -**H7 — bundler externalization is unresolved.** Whether a declared dependency -is automatically left external under `skipNodeModulesBundle: false` is not -documented, and the evidence is mixed (§5.5). If `@prisma/cli-engine` were -silently inlined into the published tarball, S3's cross-repo -published-consumption proof would be void while appearing to pass. Settle this -before adding the dependency. S6's tarball-verification check is the natural -place to make it permanent — note that composer's own -`check-publish-deps.mjs` inspects only the packed `package.json`, never the -emitted JavaScript (§5.4). - -**H8 — the alchemy stage is a hidden, container-derived value.** The stage -passed to the child is not the user's `--stage`: it is the state-owning -extension's `container.alchemyStage` when there is one, falling back to -`--stage` (`execute-deploy-destroy.ts:162`). The reproduce hint printed on -failure deliberately includes it, because without it alchemy falls back to its -machine-dependent `dev_$USER` default and reads **different** deploy state -(`render-error.ts:31-33`; incident TML-3157). Any port that reconstructs the -alchemy invocation must preserve this exactly. - -**H9 — discovery anchors differ.** The config file is discovered by walking up -from the **entry** (`load-config.ts:27-36`), while `.prisma-composer/`, -`.alchemy/` and the alchemy child's cwd all anchor on the **process cwd** -(`execute-deploy-destroy.ts:210`, `run-alchemy.ts:52`). The two can diverge, -and the destroy guardrail's "you may be in the wrong directory" warning is a -symptom of that design. The engine's config loading has its own discovery -rules, so this needs reconciling rather than porting. - -**H10 — no prompts today, and one command that arguably needs one.** -`destroy` tears down production with no confirmation; its only protection is -the required explicit target (§1.4). Adding the engine's `consent` prompt with -the app name as its token would be an improvement but is a **parity -divergence** and belongs on S3's divergence list for operator review, not in -the port silently. - diff --git a/.drive/projects/prisma-cli-v8/deferred.md b/.drive/projects/prisma-cli-v8/deferred.md deleted file mode 100644 index 63e75e7a..00000000 --- a/.drive/projects/prisma-cli-v8/deferred.md +++ /dev/null @@ -1,528 +0,0 @@ -# Deferred and follow-up items — prisma-cli-v8 - -Work identified during a slice that is not part of that slice's -contract. Each entry: what, why it was deferred, where it lands. -Nothing here is tracked outside this file. - -## After the latest cutover (2026-08-25, PR #230) - -- **Engine version transitions — CLOSED through 0.6.1 (2026-09-27).** The 0.3.0 transition (the Management API SDK became a peer of the engine, so an SDK bump no longer changes the engine) closed 2026-08-26. The transitions to 0.4.0, 0.5.0 and 0.6.1 followed the same order: engine publishes, both families release peering it, prisma-cli pins those releases and empties the `exceptions` list in `packages/cli/scripts/conformance.ts`. The 0.6.1 exceptions were deleted once `@prisma/composer-cli` 0.23.0 and `@prisma/orm-toolchain` 8.0.0-rc.12 shipped in `prisma` 8.0.0-rc.17. - -- **A stale product `dev` dist-tag can block a release publish.** The - publish run checks the dev channel before the release leg, and the - dev channel resolves each product's `dev` tag with no fallback — so - when prisma/prisma released rc.7 without publishing a dev build - (their workflow's two publish kinds were alternatives), our rc.10 - release run died in the dev conformance check with an - engine-pin-mismatch nothing in this repo caused. Unblocked by a - manual `npm dist-tag add @prisma/orm-toolchain@8.0.0-rc.7 dev`; a - separate agent is porting composer's dual-publish (composer #241) to - prisma/prisma, which removes the trigger. Open decision for THIS - repo: whether a dev-channel failure should stop the release leg at - all, or the release should publish and the run report the dev - failure after — the current ordering makes another repo's stale tag - this repo's release blocker. -- **The `@prisma/cli` deprecation call is open.** Rollout-plan step 5 - listed a deprecation notice pointing installers at `prisma`; the - package is now the scoped twin the workflow actively publishes, so - deprecating it may no longer make sense. Operator decision; needs npm auth either way - (`npm deprecate @prisma/cli@"<8.0.0" "..."`). - -## After S7's first real publish - -- **The publish/Release shell in `publish.yml` should become a tested - script.** The operator called the inline `gh` calls janky - (2026-08-12); the agreed direction, not yet ruled go, is a - `scripts/publish-release.mjs` beside `determine-version.ts` — the - draft-create/attach/publish flow and the already-published tolerance - unit-tested like every other script, the yml steps collapsing to - one-liners. The alternative considered (pinning - `softprops/action-gh-release` into the job that holds - `id-token: write`) widens the trusted set of the repo's most - privileged workflow and was recommended against. - -- **The `v8.0.0-rc.1` GitHub Release has no tarballs attached, and - none can be added.** The first real `next` publish (2026-08-12, run - 31618278670) published both packages to npm successfully, then the - Release step published the Release before uploading assets — and this - repo's releases are immutable, so the upload was refused (HTTP 422) - and the Release froze empty. npm is unaffected; the smoked tarballs - remain retrievable from that run's workflow artifacts (which expire - on the repo's retention schedule) and from npm itself. PR #165 fixes - the step for every future release (draft → attach assets → publish, - the order GitHub's own docs recommend). Repairing rc.1's Release - itself, if ever wanted: merge #165 first, try - `gh release delete v8.0.0-rc.1` (docs are silent on whether a - published immutable release can be deleted; the attempt is the test), - and if deletion works, re-dispatch the publish workflow — the - already-published npm versions are tolerated and the run recreates - the Release complete. Operator ruling (2026-08-12): cosmetic, not - immediate. - -## Still open after S3/D4 — mostly the composer repo, two items need both - -D4 landed the prisma-cli half (the mount, the node floor, the divergence -file, the ledger corrections, the 1c closure). The first two items below -need a composer checkout and nothing else. The last two need a change in -each repo: the engine pin has to match what prisma-cli depends on, and -the help-example fix needs a new placeholder in this repo's engine before -composer can use it. - -- **`loadAppConfigDiagnostics()` is called by nothing.** D2 rewrote - composer's config loading to return diagnostics instead of - throwing (contract R-S3-2), but `pipeline.ts` still calls the - throwing `loadAppConfig`, so the rewrite is currently dead code — - the name does not appear outside comments in the published - `0.6.0-dev.16` bundles. The effect-resolution check is unaffected and - does run: it sits in `configSource`, the front both loader shapes - share, so the throwing path the pipeline uses runs it first. -- **Composer should drop its `isCI` answer.** prisma-cli #155 made - `Runtime.isCI` the optional `isCIOverride` — the engine detects CI - itself now. Composer still passes `isCI` (via `ci-info`) in - `packages/0-framework/3-tooling/cli/src/family/runtime.ts` because - it pins engine `0.0.9`, which predates #155. Harmless today. Drop - the parameter and the `ci-info` dependency when composer next bumps - its engine pin. -- **`check:npm-effect-resolution` fixes are unverified.** D3 updated - three assertions (help proves the family mounted; the adversarial - `deploy` gets a service token because the credential check now - precedes the tree check; `--help` must survive a broken dependency - tree because the family's static graph is alchemy-free). The check - performs real npm installs, so it needs network to run. -- **The engine pin moves to whatever the tandem release publishes**, in - both composer manifests — and it must be the SAME version prisma-cli - depends on. They disagree today: prisma-cli builds against the - workspace engine (`8.0.0-rc.1`) and composer `0.6.0-dev.16` pins - `@prisma/cli-engine@0.0.9` exactly, so an install of `@prisma/cli` - carries two copies of the engine. It works for the one crossing that - is tested: the engine's cross-copy markers are `Symbol.for`, and - `packages/cli-engine/tests/execution.test.ts` ("a structured error - built by another copy of the engine") and `tests/protocol.test.ts` - prove a structured error raised by one copy is recognised by the - other. Nothing tests execution or signal behaviour across two copies, - and the honest reason is that it is not worth writing: matching pins - is a **release requirement for the tandem release**, so the two-copy - install is a preview-only state to end rather than a configuration to - support. S7 update (operator ruling, 2026-08-12): the convergence - choreography is deferred until `8.0.0-rc.1` publishes; the S7 branch - adds a third pin in the same shape (`@prisma/orm-toolchain@ - 8.0.0-rc.1-dev.40`, also carrying engine `0.0.9`). The sequence, when - it runs: engine `8.0.0-rc.N` publishes from this repo → orm-toolchain - and composer bump their engine pins and publish → the rc1 bump PR - here pins those versions. -- **The prisma bin's mount makes composer's help examples wrong.** **Closed by the command grammar cleanup (2026-08-21):** `dev` and `deploy` moved to the root, so `{bin} deploy src/service.ts` renders correctly; `destroy` and `log` were dropped entirely. No engine placeholder needed. Recorded in `assets/s2/parity-divergences-s3.md`. - -## The ORM family does not work through the assembled binary (found 2026-08-13, writing the e2e happy paths) - -Running the shipped `prisma` binary against a scratch directory, rather than the ORM family through the test harness, turns up three things. The first is a defect a user hits on their first command. - -- **`prisma orm init` scaffolds a project the `prisma` binary cannot read.** It writes `prisma-next.config.ts` — the standalone `prisma-next` bin's config file — and then fails its own last step, `Emit the contract`, with exit 5 and `Config is not a defineConfig result`. Nine files are already on disk at that point. Running any ORM command afterwards fails again, differently: the mounted family reads its configuration from an `orm` section of `prisma.config.ts` (`ormConfigSection`, `packages/1-framework/3-tooling/cli/src/orm/config-section.ts` in prisma/prisma), so it reports `CLI.CONFIG_SECTION_INVALID` and `CONFIG.FILE_NOT_FOUND` — "The orm config section is absent, so prisma-next.config.ts was never evaluated." So `prisma orm init && prisma contract emit` cannot work, and the two config surfaces have different shapes: the section nests the whole config under `orm`, while the scaffolded file exports a `defineConfig` result. Which side moves is the ORM's call; that it is broken today is not in question. -- **The e2e coverage convention excludes all 22 ORM commands on reasoning #171 disproved.** `tests/e2e-coverage.test.ts` excuses them with "Real e2e lives in prisma/prisma (R7); the shell proves composition in orm-mount.test.ts (R8)." prisma/prisma's suite passed throughout the presentations change while the assembled binary exited 2, and `orm-mount.test.ts` proves composition for exactly one command, `migration list`, not per family. The operator's ruling (2026-08-13) is that every mounted command needs a happy path in this repo, precisely because the product repos cannot reproduce the assembled CLI. The exclusion should become a backlog entry once the first item above is fixed and the commands can run at all. - -## A live bug carried out of the port (found closing PR #92, 2026-08-12) - -- **The production branch is still resolved by name, not role.** - `packages/cli/src/commands/service/target.ts` derives branch kind from - the literal name (`toBranchKind`, used where domain attachment decides - production-ness), so a project whose production branch is named - `master` cannot attach a custom domain. Closed PR #92 fixed exactly - this in the old controller and died with it; the fix should land in - `target.ts`, taking the role from the API's branch record. - -## Orphaned by the stale-PR sweep (2026-08-12) — capabilities with no engine successor - -Nine pre-port PRs were closed as unmergeable after the shell deletion -(#139). Most were superseded outright; these wanted things the engine -CLI does not do, and each restarts as engine work if wanted: - -- **`branch remove` / branch CRUD** (#110, #73): the `branch` group is - list-only. Returns with exact-id consent if wanted. -- **`env pull` into a dotenv file** (#79): `project env list` never - returns values by design, so an engine `env pull` needs a ruling on - secret handling before it is built. -- **A `github` group for workspace-level GitHub connections** (#113): - the engine ships repo-level `git connect|disconnect` only. -- **Transient-read retry on `build logs` streaming** (#104): moot — the `build` group was removed by the command grammar cleanup (2026-08-21). - -## Ratified-as-shipped at the S2 sign-off (2026-08-12) — the gaps stay real - -- **Streaming service logs is unavailable in any form.** `app logs` died - with the commander shell and `service logs` waits on an engine - streaming transport. The one capability loss of S2d; the S2c record - has the design notes. - **Superseded by the `service-logs` slice (2026-08-13):** `service logs` - ships, reading the platform's HTTP page endpoint, so the capability is - no longer missing. What is still missing is the *streaming* half — - `--follow` polls on a 2s interval rather than holding a socket open. - The open remainder is the WebSocket live tail, in the closed - `service logs` entry further down this file. -- **`build logs` cannot exit 1 on a failed build** — moot: the command was removed with the `build` group by the command grammar cleanup (2026-08-21). The engine gap (a stream settling with a documented non-zero code) remains real for future stream commands. -- **The crash-recovery feedback action does not port** (the legacy - crash envelope pre-filled a `feedback` command; the engine's crash - path has no hook for it). -- **A service token whose workspace only the server knows is refused**; - accepting it needs an engine change to resolve the workspace online - during the needs check. -- **Q6: the telemetry docs URL is the interim prisma.io CLI page**; - the real page is still owed and ships as a one-line change. - -## Owned by whoever lands the next engine change - -- **S3's SPI amendment vs credential-manager rev 6.** #136 recorded - the spawn path's credential read against rev 5's surface and then - adapted to rev 6's `activeCredentialStorage()` during the rebase. - If rev 6's storage surface reshapes again, the single named - consumer (`packages/cli-engine/src/execution/spawn.ts`) moves with - it. Recorded in `assets/engine/credential-manager-design.md`. -- **Nothing bounds a child run to the token it was given.** A - `credentials: "child"` command still hands the child a snapshot of the - access token and never the refresh token - (`packages/cli-engine/src/execution/spawn.ts`). The parent now refreshes a - stored OAuth pair before the handler when its access token is inside - `CREDENTIAL_NEAR_EXPIRY_MS`, so a refreshable session receives a fresh - snapshot instead of an unnecessary sign-in error. That does not bound the - child's total runtime: a converge that outlives even the refreshed snapshot - can still fail after creating resources. The remaining ways out are to hand - the child something that can refresh or to bound the child's run. -- **A validated number flag**, if `--tail`'s old constraint is wanted - back. `flag.number` accepts negatives and fractions, so "non-negative - integer" is enforced nowhere. D4 took the other branch this item - offered and widened the divergence entry instead - (`assets/s2/parity-divergences-s3.md`), which also corrects this - item's claim that legacy rejected non-integers — legacy truncated - them silently, and rejected only negatives and `NaN`. -- **`pnpm --filter @prisma/cli test` can report green against a stale - engine build.** Vitest resolves `@prisma/cli-engine` through the - package's own `exports` map, which points at `./dist`; the `paths` - entries in `tsconfig.json` are read by `tsc`, not vitest, and no - path-resolving plugin is configured. `packages/cli`'s `test` script is - a bare `vitest run` with no build step, so run on its own it exercises - whatever engine `dist` happens to be on disk. The engine's own `test` - script builds first, so a gate that runs the engine suite before the - CLI suite — as every gate in this project does — is honest, and - `turbo run test` is honest too because `turbo.json` declares `test` as - `dependsOn: ["^build"]`. The trap is running one filter in isolation - after editing engine source. Surfaced in the engine-colour slice when - a deliberately introduced defect failed to fail. The mechanism to fix - it already exists in `turbo.json`; the change is to `packages/cli`'s - test script. -- **`spawn-real-child.test.ts` also fails under load, and is a different test from the one below.** In `packages/cli-engine/tests/spawn-real-child.test.ts`, the case "native Ctrl-C reaches the child through the shared process group" failed twice during the engine-colour slice, both times on a machine running the engine and CLI suites concurrently — on the second sighting that run's import phase took 92s against a normal 3–7s. It passed on every isolated and sequential run either side. Nothing in that slice goes near spawn or signals, so this is not its doing. Two independent sightings under load make it worth diagnosing rather than watching: the likely shape is the same as the entry below, a test that waits on a marker the child writes before it is actually ready for the signal. Third sighting, 2026-08-12: it failed on a GitHub runner during #158, a PR that changes no engine file, and passed on a re-run of the same commit and on the same machine in isolation. That moves it from a loaded-laptop annoyance to a test that reddens the shared `Test` check on unrelated work, which teaches people to re-run a red check rather than read it. Worth fixing before the next slice rather than after. -- **`v8-spawn-adapter.test.ts` has a race that fails under load.** - In `packages/cli/tests/v8-spawn-adapter.test.ts`, the "kill - delivers the signal to the live child" case runs an inline child - that writes its `ready` marker BEFORE calling - `process.on('SIGTERM', ...)`. The test waits on that marker and - then kills, so a kill landing in the gap hits the default SIGTERM - disposition: the child dies by signal instead of exiting 42, and - the assertion fails. Reproduced on a loaded machine (2 of 4 runs) - while verifying the child-record change, which does not touch - `packages/cli`. Fix: have the child write `ready` only after the - handler is installed — the same ordering the engine's own - `tests/fixtures/child.mjs` `trap-term` fixture already uses. -- **`credential-manager.test.ts`'s crashed-lock contention test is - flaky on Windows CI.** "lets only one of two waiting mutations - clear the same crashed holder's lock" timed out at 5 s on - `windows-latest` during #181 (2026-08-13, a PR touching nothing - near the credential manager) and passed on the re-run of the same - commit. One sighting so far — same reddens-shared-checks family as - the two entries above. Likely shape: two waiters racing a lock - file under Windows FS latency needs more than the 5 s budget, or - the same write-marker-before-ready ordering. Diagnose on the - second sighting. - -## Owned by whoever converts a family's renderers - -- **The rail is not restored on any card yet.** The engine-colour slice - gives `fields` an opt-in `rail` and restores alignment and the accent - colour to all 36 sites at once, but which cards want the dim `│` rail - is a per-command judgement. The legacy shapes are - `renderCommandHeader` (rail) and `renderFieldRows` (no rail) in - `packages/cli/src/shell/ui.ts`. -- **No presenter binds `ui`.** All 54 platform presenters are - `human: () => [...]`, so spans, `ui.tone`, `ui.width` and `drawing` - have no callers until each family converts. The engine colours only - what it draws itself until then. -- **Glyph mode is not adopted.** The ORM decides between unicode and - ASCII box-drawing from TTY plus a UTF-8 locale - (`prisma/prisma`, `packages/1-framework/3-tooling/cli/src/utils/ - glyph-mode.ts`). The engine emits `✔ ✘ ⚠ ℹ` unconditionally today and - will emit `├─ └─ │` the same way. Adopting the ORM's detection is the - established fix if a non-UTF-8 terminal ever reports mojibake. - -## Upstream, not ours to land - -- **alchemy-run/node-utils#6** (scope exit hooks to owned locks). - **Closed by composer 0.13.0 (2026-08-25):** the release chain - delivered — alchemy 2.0.0-beta.74 carries the node-utils fix and - composer #254 retired the vendored patch, so a `prisma` install now - resolves a node-utils that registers no import-time signal listener. - The canary (`packages/cli/tests/composer-isolation.test.ts`) now - asserts zero listeners, so a regression in that chain says so. The - original entry follows for the record. - Was: open. Vendored as a pnpm patch in composer - (`patches/@alchemy.run__node-utils@0.0.5.patch`, applied to both - `lib/lockfile.js` and `src/lockfile.ts` because the exports map - sends bun to `src/`). **Delete the patch when the release chain - delivers**: node-utils release → alchemy's exact-pin bump → - composer's alchemy bump. Exit condition recorded in - `skills-contrib/upgrade-alchemy-effect/SKILL.md` in the composer - repo. - **The patch does not reach the prisma bin, and D4 measured what that - costs.** A pnpm patch applies in the repo that declares it, so a - `prisma` install resolves the unpatched `@alchemy.run/node-utils`. - D4's canary (`packages/cli/tests/v8-composer-isolation.test.ts`) - imports one composer executor in a fresh prisma bin process and - asserts what the import alone leaves behind: one SIGINT and one - SIGTERM listener — the exact condition the contract's design - consequence 4 says nothing ships with. Both counts are assertions, not - observations, so the day the patch reaches us this test says so. - What those listeners do is the reason it matters: `lib/exit-hook.js` - calls `process.exit(128 + signal)` from inside them, which is a - synchronous exit the engine's abort, child teardown and settlement do - not get to finish behind. Whether they actually preempt the engine on - a real Ctrl-C during a composer command is untested — the listeners - are proven present, the race is not proven either way. - It does not fire on a normal run: the same process running - `--version` loads no alchemy at all and holds no listener, which is - what the test asserts. It fires once a composer command evaluates its - config. Until the release chain delivers, composer's own - sole-listener detector passes on the patch and the prisma bin has no - such protection. -- **Alchemy's `sync` reports permanent drift on Composer resources.** - `Deployment.read` returns `previewDomain` while `reconcile` - persists `appEndpointDomain`; `Sync.ts` deep-equals live against - stored attributes, so every `alchemy sync` would report drift and - "repair" forever. The deploy path is unaffected (it plans on props - only — see the S8 note below), so this is a courtesy report to the - maintainer, not a blocker. -- **The ORM family's entry module loads esbuild and arktype on every - invocation.** `@prisma/orm-toolchain`'s `./cli` subpath statically - imports `esbuild`, `arktype` and eight `@prisma/orm-framework` - subpaths, so mounting the family (S7 D1) makes every run of this bin - pay that import — `prisma --version` included, which touches no ORM - code. Composer solved the same problem by keeping its heavy graph - behind dynamic executor imports; orm-toolchain has not. It costs - startup time only: no output and no exit code changes. The fix is - prisma/prisma's to land (dynamic handler imports in orm-toolchain's - CLI entry), and it closes when a published orm-toolchain's `cli.mjs` - no longer imports those modules at the top level. Recorded also in - `assets/s2/parity-divergences-s7.md`. - -## Answered, feeding a later slice - -- **S8's planner question is settled** (D2's read of alchemy - `2.0.0-beta.67`): the deploy path plans on **props only** and never - compares attributes, so Composer's domain-field mismatch causes no - per-run redeploy. S8's promote/rollback/start/stop design does not - have to defend against Alchemy reverting imperative changes on the - next deploy. Full citations in `assets/s3/composer-inventory.md` - §4a and D2's report. - -## Left open by S8 — the service family - -- **`resolvePinnedProject` can mask an API refusal as "generator body - threw".** `v8/project/context.ts` passes a throwing `listProjects` - into `resolveProjectTarget`, whose `Result.gen` body swallows the - thrown error's message. S8 hit the same shape in the service tree - (fixed in #162 by moving the fetch to the call site); the project - path has the latent equivalent, unverified. Whoever next works the - project family should reproduce and fix it the same way. -- **`auth`'s workspace-ref lookup rejects the `wksp_`-prefixed id the - Console shows.** `src/v8/auth/session-ref.ts:24` compares a - user-typed workspace ref against stored session ids with no prefix - tolerance — the same bare-vs-`wksp_`-prefixed mismatch behind #144 - and S8's workspace-filter defect (`bd8aa78`). Unlike those, it - fails loudly (falls back to name matching, then errors), so it is - an annoyance, not silent data loss. Found by S8's review sweep of - cross-origin id comparisons; the auth family is untouched by S8, so - it lands with whoever next works that family. -- **The Composer-ownership note is deliberately not built (R-S8-4).** - `promote`, `rollback`, `start` and `stop` print no "Composer will - overwrite this on the next deploy" warning. Ruled NOT NOW (operator, - 2026-08-12), and the grounding fact is why the revisit is real rather - than a shrug: **nothing in the app or deployment records identifies a - service as Composer-managed.** The only fingerprint is the `COMPOSER_*` - env-var namespace on the branch, which the CLI never fetches, so a - warning today would either be unconditional (noise on every - hand-managed service) or guesswork. Revisit when the API grows a - `managedBy` marker — the same request deferred alongside it. Users own - their resources, and Composer reconciling a manual change on the next - deploy is accepted behavior until then. -- **`service logs` SHIPPED** (slice `service-logs`, 2026-08-13 — - contract at `specs/service-logs.md`, divergences at - `assets/s2/parity-divergences-service-logs.md`). It mounts as - `service logs`, the legacy spelling (ruled, operator, 2026-08-13), and - reads the platform's HTTP page endpoint (pdp-control-plane #4886): one - page by default, `--follow` polling on the terminal record's cursor. - Closing this entry corrects one thing it predicted: the pinned - `@prisma/management-api-sdk` (1.55.0) did NOT need a bump. The - `query: never` the risk note named is path-item boilerplate that every - path in that file carries; the operation type - (`getV1DeploymentsByDeploymentIdLogs`) already declared `tail`, - `from_start` and `cursor`, so the wiring typechecked against the - existing pin with no cast. - **What stays open: the WebSocket live tail.** The platform serves the - upgrade on the same path and the CLI does not use it, so following is - polling on a 2s interval rather than push. The engine socket design - (`assets/engine/websocket-transport-design.md`) remains **shelved for - that later date, not deleted** — R-S8-5's "provided live streaming can - be added at a later date" is still the standing commitment, and this - slice is what it was traded against. -- **The e2e suite should assert the real service-id prefix.** D2 wrote - `e2e/service.e2e.ts` without credentials to run it, so it asserts only - that `service create` reports a non-empty id. The sibling suites assert - real prefixes (`bkt_`, `db_`) because their authors could see one. - Whoever first runs this suite green should read the id the API actually - returns and tighten the assertion to match, as `bucket.e2e.ts` does. -- **`service show` can have a real e2e now, and should.** It sat on the - `AWAITING_COVERAGE` backlog because the whole `service` family was - assumed to need a deployed service. `service create` falsified that: - `service show` works against a service that has never been promoted — - D1's own unit test asserts that case. Adding it to `e2e/service.e2e.ts` - alongside `create`/`list`/`remove` is a small job and removes the entry - rather than re-explaining it. The `service domain *` entries look like - the same case (they attach a domain to a service, not to a deployment) - and are worth checking at the same time. - -## Composer's public surface — ruled, closed - -- **`ExtensionDescriptor.preflight` moved to method syntax** so an - extension can type its input against its own client (the injected - management client arrives there under S3's in-process credential - leg). Consistent with `ContainerDescriptor` in the same file; - loosens parameter checking for extension authors. RULED KEPT - (operator, 2026-08-11). No further action; the change ships with - S3 and needs a divergence entry only if it breaks a published - extension, which it does not. - -## Found during S6 — not S6's to fix - -- **`pnpm test` fails on a pre-existing concurrency race between two - packages' test tasks.** `@prisma/cli-engine`'s `test` script begins - with `pnpm run build`, and tsdown builds with `clean: true`, so it - empties and rewrites `packages/cli-engine/dist` while it runs. - `@prisma/cli`'s tests resolve `@prisma/cli-engine` through that same - `dist`, and turbo's `test` task depends only on `^build` — never on a - dependency's `test` — so the two run at the same time and the shell's - suite intermittently fails with "Failed to resolve entry for package - @prisma/cli-engine" across roughly two dozen files. Confirmed - pre-existing: the base commit `aa40790` fails three runs out of three, - and `turbo run test --concurrency=1` passes on both that commit and - the S6 branch. It is invisible in CI because `.github/workflows/ - test.yml` runs only `pnpm --filter @prisma/cli test`, and - `pr-quality.yml`'s `pnpm test` has presumably been passing by timing - luck. Two candidate fixes, neither S6's call: drop the `pnpm run - build` from the engine's `test` script and let turbo's `^build` - dependency do that work, or stop the engine's build cleaning a - directory another package reads while it runs. - Seen again on the presentations branch (2026-08-12) with a second - message for the same cause — `Cannot find package - '@prisma/cli-engine/testing'` — and a failure count that varied 13, - 34 and 43 files across three runs of one commit, while - `--concurrency=1` and a direct `npx vitest run` in `packages/cli` - both passed all 60 every time. The varying count is the tell: a - change that touches many files shifts the timing and makes it fire - more often, which reads as "this branch broke everything". -- **The packed shell manifest carries `devDependencies` on private - packages at versions no registry has** — `@repo/cli-telemetry` and - `@repo/tsconfig`, both at `8.0.0-rc.1`. Harmless when a consumer - installs the tarball, because npm ignores a package's own - devDependencies; fatal for anyone installing the unpacked directory. - None of S6's three checks looks at that field, deliberately: check 3 - compares only the fields a consumer installs. composer's and - prisma/prisma's `check-publish-deps.mjs` both catch this class, and - prisma-cli has no equivalent. Worth one small check, in its own - change. - -## Found while fixing engine 0.1.1 (2026-08-17) — needs a decision from Will - -- **A library depends on CLI tooling: `@prisma/composer-prisma-cloud` - imports `@prisma/orm-toolchain/config-loader`.** ADR 0004 says - libraries applications install must not depend on product CLI/dev - packages, and this import breaks that rule today. It is also how an - engine implementation detail reached composer's users: evaluating - `prisma-composer.config.ts` loads `composer-prisma-cloud/control`, - which loads `orm-toolchain/config-loader`, which imports `ci-info` — - putting ORM tooling and its dependencies inside composer's config - evaluation in every user's process. The engine defect that exposed - this is fixed (prisma-cli#187), but the dependency remains. Options: - move `config-loader` into a shared library package the ORM publishes - for exactly this kind of consumer, or cut the cloud extension's use - of it. Spans both product repos; Will decides which. - -## The rc.4 engine mismatch (2026-08-18) — the repair chain - -`prisma@8.0.0-rc.4` on `next` crashes on import: #183 and #184 changed the engine without bumping its version, so the registry's `@prisma/cli-engine@0.1.1` (published ten minutes before #183 merged) lacks exports the CLI imports. The registry is immutable and both products peer the engine exactly, so npm fails with ERESOLVE on any mismatch; the repair is a chain, in order: - -1. **prisma-cli #200**: engine → 0.2.0, plus the CI job that fails any PR changing `packages/cli-engine/` while its version is already on the registry (the check whose absence let rc.4 ship). Merging it publishes engine 0.2.0. -2. **Both product repos**: re-declare the exact engine peer at 0.2.0 and release (`composer-cli` 0.7.1, `orm-toolchain` 8.0.0-rc.3). Their lockfiles cannot resolve 0.2.0 before step 1 publishes. -3. **prisma-cli**: pick up those product versions and cut `8.0.0-rc.5` — the first version on `next` that works again. rc.4 itself cannot be repaired. - -The cost the incident exposes: every engine API change forces this three-repo, three-release sequence, because the exact peer is what guarantees one engine per install (ADR 0004). Making the chain cheaper — automation that opens the product peer-bump PRs when a new engine publishes, riding the same `DEPLOY_GITHUB_TOKEN` as the notification step below — is a design question for Will. - -## Left open by the dev-build fix (2026-08-17) - -The release channel is green as of 2026-08-17: `@prisma/composer-cli@0.7.0` and `@prisma/orm-toolchain@8.0.0-rc.2` are both released and both peer `@prisma/cli-engine@0.1.1`, so the conformance run reports nothing and `packages/cli/scripts/conformance.ts` carries no exceptions. What closed, for the record: composer's `0.6.0` was uninstallable (published out-of-band with `npm publish`, leaving `workspace:0.6.0` in its manifest) and the ORM had no released version carrying the command family. - -Still open: - -- **Both product repos need their publish-notification step** (the work in the closed composer#232 and prisma#30033): a `repository_dispatch` of type `product-published` to `prisma/prisma-cli`, placed immediately after the publish step and keyed on its outcome. Until then a daily scheduled run is what notices a product release, so a new product version reaches the CLI within a day rather than within minutes. `docs/oss/release-automation.md` carries the exact step, and `DEPLOY_GITHUB_TOKEN` is provisioned in all three repositories (2026-08-17). -- **Neither product repo installs its own tarball before publishing.** That is why an uninstallable `@prisma/composer-cli@0.6.0` sat on `latest` unnoticed. prisma-cli's check 3 does exactly this — pack, install into a clean sandbox with `npm --ignore-scripts`, start every declared bin — and is worth porting to both. -- **The engine-pin check compares for equality, not peer satisfaction.** Both families now declare an exact peer equal to the shell's pin, so equality is correct and stricter today. Widening to range satisfaction belongs with the post-GA move to engine ranges (ADR 0004), not before. -- **`credential-manager.ts` uses the banned word.** `packages/cli/src/auth/credential-manager.ts` has a private `#repin` method (about the active-workspace marker, a different concept from dependency versions). The operator banned the word outright; renaming it is a mechanical change to a private method, left out of the publish-channel work to keep that diff to one subject. - -## Left open by the command grammar cleanup (2026-08-21) - -The cleanup PR removed the compute config and `init`, made service commands parameter-only, renamed the six destructive `remove` commands to `delete`, moved `postgres restore`/`ref *`/`migrate`/`format`/`composer dev|deploy`, and dropped `composer destroy|log` and the `build` group. Deliberately left behind: - -- **The wire layer still speaks App/Deployment.** The CLI surface says Service/Version (ADR-012), while the adapter (`packages/cli/src/lib/app/app-provider.ts`), compute-sdk names, `/v1/deployments` paths, and `appId` keep platform vocabulary. They rename in pdp-control-plane's coordinated all-surfaces pass, and the adapter is the one file where both vocabularies are allowed to meet until then. - -- ~~**`project env` still infers scope from the current git branch.**~~ Closed on the PR branch (2026-08-21, operator ruling): `project env list` with no `--role`/`--branch` lists the overview instead of inferring from the checkout; `readLocalGitBranch` and `lib/git/local-branch.ts` are deleted. -- ~~**`knownLiveDeploymentByProject` has no writer.**~~ Closed on the PR branch (2026-08-21): the local-state shape, its store methods, and `service delete`'s cleanup pass were deleted. -- ~~**Upstream family cleanups.**~~ Closed (2026-08-22): composer#253 retired `destroy`/`log` and prisma#30102 rekeyed the ORM family to the mount paths and fixed its redirects; the shell now mounts both families as shipped and the wrapper arithmetic is deleted. Both pins are on released versions: composer-cli 0.12.0, orm-toolchain 8.0.0-rc.5. -- ~~**`PRISMA_PROJECT_ID` is honoured only by the domain commands**~~ Closed on the PR branch (2026-08-21, operator ruling): the env var served the deleted `app deploy` headless flow and survived only in the domain commands by accident; it is removed entirely. Project targeting is `--project` and the link file. -- ~~**orm-toolchain's shipped help examples name retired spellings.**~~ Closed (2026-08-22) by prisma#30102: the family keys are the mount paths and the examples follow; `tests/orm-mount.test.ts` now asserts upstream stays clean. -- ~~**The deployment-id targeting asymmetry is undocumented.**~~ Closed on the PR branch (2026-08-21): every deployment-id command (`promote|start|stop|delete|show`, `logs --deployment`) now resolves the id globally with no service parameter, per the "Subjects are positional" ruling. -- ~~**`GET /v1/deployments/{id}` omits the parent `appId`.**~~ Closed on this branch (2026-08-25): pdp-control-plane#4983 added the owner to the deployment representation as `serviceId` (ADR-012 vocabulary, not `appId`), compute-sdk 0.42.0 exposes it as `DeploymentDetail.serviceId`, and `showDeployment` now resolves the owner with one `GET /v1/apps/{id}` — `findAppForDeployment` and its per-service scan are deleted. - -## From the agent-skills delivery (project closed 2026-08-22) - -- ~~**Config evaluation fails through unrealpath'd pnpm symlinks.**~~ Closed (2026-08-25): the one-line realpath fix shipped — `packages/cli-engine/src/config-loader.ts` imports c12 via `realpathSync(import.meta.resolve("c12"))` on main, released with engine 0.2.2 (#224). The init e2e's rerun workaround came out with the config-file-resolution slice (D4, 2026-08-25): the rerun no longer deletes the scaffold first and now covers the config-present path directly. - - -The agent-skills project (skills sync/list, `prisma init`, the staleness notice; PR #219) closed with these items still open; details were in its own ledger, summarized here as the surviving record. - -- **When facade skill content diverges per database, split the skill by name — never add a carrier package** (operator concurred 2026-08-21). Today every facade ships an identical `prisma-8` skill and conflicts are arbitrated by highest version, safe only while content is identical and versions are lockstep. A transitive carrier package is unresolvable from the project root under pnpm; a direct-dependency skills package breaks the installed-version guarantee. The allowlist grows one deliberate line per facade either way. -- **The browser login success page still shows a static `npx skills add prisma/skills` copy button** (`packages/cli/src/auth/login.ts` ~571) — the last surface promoting the retired third-party installer after the `agent` group's deletion. Decided 2026-08-24: the operator is having the responsible team remove it; not part of PR #219. -- **Composer website hero copy** (prisma/composer `website/src/template.ts`): still says `npx skills add prisma/composer`; the replacement wording and its release timing belong to the site owner, and the new command only exists once the CLI ships. -- **`check-skill-packaging.mjs` hardcodes `@prisma/composer`** (prisma/composer) while `stage-skills.mjs` is generic; generalize when a second skill-bearing composer package appears. -- **Turbo race: `pnpm test` can rebuild `cli-engine` dist while `cli` tests import it** (intermittent `Failed to resolve entry for package "@prisma/cli-engine"`). Fix: `dependsOn` on the engine build in turbo.json. -- **Windows CI: the credential-manager suite needs an owner** — two distinct timing-sensitive tests flaked on 2026-08-21 (`credential-manager.test.ts` "holds no lock while the workspace name is fetched", run 32477175789; `credential-manager-processes.test.ts` "exchanges one refresh token once", run 32497093995), both on pushes touching nothing near credentials. A third hit on 2026-08-24 (run 32737503671, PR #225): the "holds no lock" test again, failing on an EPERM temp-file rename on the Windows runner. Three flakes across two tests; the suite needs an owner. -- **Windows CI: `skills-sync.test.ts` timed out once at the 5s default** (run 32474645762) with a teardown ENOTEMPTY from cleanup racing the timed-out test. If it recurs, raise the suite's per-test timeout on Windows rather than chasing the race. -- **`isLikelyGlobalNpmEntrypoint` (update-check.ts) matches only `prisma-cli` install paths**, so a globally-installed `prisma` gets the docs-link fallback instead of a concrete update command; `selectUpdateInstruction` still names `@prisma/cli`. Newly conspicuous after the CLI_NAME → prisma rename. -- **The feedback client's user-agent changed from `prisma-cli/` to `prisma/`** — wire-visible; whoever reads that dashboard should know. -## Left open by the rc.8 broken release (2026-08-24) - -- **`prisma@8.0.0-rc.8` on npm is broken and immutable.** The `prisma` wrapper package carries its own copies of the product pins, and the grammar-cleanup branch bumped only `packages/cli/package.json` — so the published `prisma` bin resolved `@prisma/orm-toolchain@8.0.0-rc.4`, whose old family keys make the mount table's lookups undefined and every invocation crash ("Cannot read properties of undefined (reading 'needs')"). rc.9 fixes it. Consider `npm deprecate prisma@8.0.0-rc.8` (needs a maintainer's npm auth; CI publishes via OIDC and has no deprecate step). -- ~~**The release checks did not catch a `prisma` bin that crashes on install.**~~ Closed (2026-08-24, on the rc.9 PR): worse than hoisting — check 3b never installed or started the wrapper's bin at all, only the shell's. Three guards now exist: `packages/cli/tests/manifest-pins.test.ts` (every PR: the wrapper's dependencies must deep-equal the shell's), the tarball check's new `sibling-pin-mismatch` finding (pack time: shared dependency names across packed manifests must carry identical specifiers), and per-package sandboxes in check 3b (every bin-bearing package installs and starts from its own tree). Each guard was proven against the planted rc.8 defect. -- **Two manifests hand-carry the same pins.** `update-product-versions.mjs` rewrites both, and three checks now fail on divergence (see the closed entry above), so the class cannot ship again. Deriving one manifest from the other at pack time would remove the duplication itself — still a design call, no longer urgent. - -## Config-file resolution rulings (2026-08-25) - -The design in `specs/config-file-resolution.md` is decided (per-key merge with section-owned semantics, automatic ancestor discovery with a reserved `parent: false | "path"` key, repo-boundary stop, post-merge validation with provenance, declaring-file-relative paths, `--config` anchoring the chain at the named file, no shadowing notices); the dispatch plan is `plans/config-file-resolution.md`. Two rulings recorded here because they close or supersede standing observations: - -- **`readProjectSkillsConfig`'s hand-rolled resolution must consolidate into the engine resolver** (ruled 2026-08-25) — lands as D3 of the slice; until then the staleness notice and the commands can disagree about which config governs when run from a subdirectory. -- **Subdirectory `prisma init` skips the skills sync, postinstall script, and devDependency by default** (ruled 2026-08-25) — those steps belong to the repository root; lands as D4 of the slice, which needs D1's ancestor discovery to detect "subdirectory" at all. - -## Left open by the config-file-resolution slice (2026-08-25) - -- **`--config` does not reach the post-login skills tip — FIXED 2026-08-26.** `CommandContext` now exposes `configPath` (the file `--config` named, undefined otherwise), and `resolveAgentSetupTipCommand` passes it to `readProjectSkillsConfig`, so a login run with `--config` shows the tip against the named file's chain. Regression-tested in `packages/cli/tests/agent-setup-tip.test.ts`. - -## Config-chain review findings: what shipped and what is still open (2026-08-25) - -Post-merge review of the config-chain slice confirmed two issues that could not be fixed inside prisma-cli alone. The operator ruled that the family path resolution is not acceptable to defer, so it was fixed in tandem across all three repositories: - -- **Root-declared family sections resolved their relative paths against cwd — FIXED, in tandem.** The chain delivers a root config's `composer`/`orm` sections to subdirectory runs, and the family packages resolved `configPath`, contract inputs/output, and `migrations.dir` against `ctx.cwd`. The engine gained the seam that makes declaring-file resolution possible: `ConfigSection.validate` now takes a second argument, `validate(raw, provenance)`, and a validator resolves its path-valued keys through `resolveSectionPath(provenance, key, path)`, returning absolute paths so downstream code never resolves against cwd. A one-argument validator stays assignable, so shipped sections keep working. Shipped: the chain itself went out as engine 0.5.0 (#233, 2026-09-21). Composer adopted `resolveSectionPath` in https://github.com/prisma/composer/pull/262 (merged 2026-09-22, released in composer-cli 0.23.0). The orm side went further: engine 0.6.0 (#279) added `configSchema`, where a section marks its path fields and the engine resolves them, and prisma/orm#30372 declared the `orm` section that way (released in orm-toolchain 8.0.0-rc.12), so the hand-written validator in prisma/orm#30128 was closed as superseded. Checked from the published `prisma` 8.0.0-rc.17 on 2026-09-27: from a directory with no config, `contract emit` read and wrote under the root config's paths; a nested config's `contract` won over the root's and its paths resolved against the nested file from two directories below it; the root's `skills.check: false` silenced the staleness notice in both; and `prisma init` below the root scaffolded only the config, skipping postinstall, the devDependency and skills sync with `reason: "governing-config"`. -- **A marker-less Prisma 7 `prisma.config.ts` at the repo root blocks every config-needing command — including `prisma init` — in every subdirectory. RULED 2026-08-26: keep it fatal.** Chain evaluation is deliberately no-skip (ratified: a broken file anywhere fails resolution), and the missing-marker error is chain-fatal, so a repository migrating from Prisma 7 cannot run the v8 migration entry point anywhere until the old root config is updated or removed. The error does name the file and the fix. The operator ruled the case is not worth an escape path: erroring out without doing anything untoward is safe, if inconvenient, and a Prisma 8 install inside a Prisma 7 repository needs far more plumbing than this anyway. No warn-and-ignore softening; the no-skip rule stands as ratified. - -Discovered while doing the above: **removing the deprecated `defineConfig` alias broke the orm repository's configs** that still imported it from `@prisma/cli-engine`. FIXED: prisma/orm#30129 renamed them to `definePrismaConfig` (merged 2026-09-22), and orm-toolchain 8.0.0-rc.12 ships with its upgrade recipe (`define-config-becomes-define-prisma-config`). diff --git a/.drive/projects/prisma-cli-v8/design-notes.md b/.drive/projects/prisma-cli-v8/design-notes.md deleted file mode 100644 index 9cb7e75a..00000000 --- a/.drive/projects/prisma-cli-v8/design-notes.md +++ /dev/null @@ -1,65 +0,0 @@ -# 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, CommandFamily). 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 deleted file mode 100644 index 4621c674..00000000 --- a/.drive/projects/prisma-cli-v8/plan.md +++ /dev/null @@ -1,219 +0,0 @@ -# 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 - -**CLOSED 2026-08-12** — shipped as prisma-cli #130 (S2a), #133 (S2b), #132 (S2c), #139 (S2d: init port, shell deletion, bin cutover, the v8 working name retired). Acceptance verified against `specs/s2d-init-and-retirement.md`; the cumulative divergence record `assets/s2/parity-divergences.md` was ratified by the operator on 2026-08-12; the deletion's survivor list is `assets/s2/shell-deletion-survivors.md`; ratified-as-shipped gaps and the stale-PR sweep's orphaned capabilities are in `deferred.md`. - -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) - -**CLOSED 2026-08-12** — shipped as prisma-cli #136/#145/#150/#151/#155 -plus the mount (#152) and composer #220/#224/#226; acceptance verified -in `specs/s3-composer.md`'s Close-out section; leftovers in -`deferred.md`. Next by the dependency graph: S8 (design first). - -Repos: composer + prisma-cli. Composer exports a `CommandFamily` -(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. -The amendment must also adopt the engine's `fix` → typed `nextActions` -rename (operator ruling, 2026-08-09) so Diagnostic stays -field-for-field identical to the settled envelope. - -### S5 — ORM adoption - -Repos: prisma/prisma + prisma-cli. The `orm` section token + -command family; 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. - -### S8 — Service primitives (design first; after S3, before S7) - -**CLOSED 2026-08-12** — shipped as PR #162; acceptance verified in -the contract's Status line, follow-ups in `deferred.md`. The e2e -suite's first real run caught a family-wide defect (the stale -workspace filter) that four review rounds and 1250 unit tests -missed — the convention earned its keep. - -**Design settled 2026-08-12** (operator discussion); the slice -contract is `specs/s8-services.md`. The four questions below are -answered there: no ownership note for now, records verified -compatible, the log-ownership conflict dissolved on investigation -(`composer log` reads the local dev daemon, not the platform), and -the transport question is ANSWERED — the API owners accept HTTP with -live streaming later, so `service logs` stays shelved only until the -endpoint serves HTTP, and no engine WebSocket transport is built. - -Repo: prisma-cli. Give the platform's service resources an atomic CLI surface, replacing what S2c ported for continuity. - -**Why this slice exists.** The legacy `app` group fused three concerns — building an artifact, wiring a GitHub repo, and deploying — into single commands, most visibly `app deploy`, which builds, creates a project, creates branches, sets environment variables, optionally provisions a database, and deploys. Composer replaces the building and deploying. What the CLI should own is managing the remote resource, and today it cannot: there is no `service list` and no `service create` despite `GET`/`POST /v1/apps`, no deployment start or stop despite `POST /v1/deployments/{id}/start|stop`, and no deployment delete. A service can currently only be born as a side effect of deploying to it. S2c ported the surviving commands under their legacy names so the commander shell could die in S2d; that port is continuity, not endorsement of the shape. - -**The resource model is already right; the CLI hides it.** `/v1/apps` supports list and create, `/v1/apps/{id}` get and delete, `/v1/apps/{id}/deployments` list and create, `/v1/deployments/{id}` get and delete, and `/v1/deployments/{id}/start|stop|logs`. Composer deploys through Alchemy rather than driving that sequence itself, but Alchemy's providers call the same management API, so Composer's services and deployments are ordinary resources under these endpoints. The seam the API already draws is the one to build on: **Composer produces deployments; the CLI manages them.** Promote, rollback, start, stop, delete and logs are resource management, not build concerns — `POST /v1/deployments/{id}/start` says the artifact must be uploaded before it is called, which is the separation stated in the API itself. - -That makes the shape of the slice mostly a rename plus filling holes — a `service deployment` subgroup absorbing `list-deploys`, `show-deploy`, `logs`, `promote` and `rollback`, plus the five operations that have no command at all. The expensive parts (engine, auth, presenters, error model, the `service` rename) are done. - -**Why it still waits for the design work.** Four questions need answers, and none of them is about whether the resources exist. The first three are S3's to give; the fourth is for the engine and the Management API owners, because it asks what can open an authenticated log socket and whether one is needed at all. - -1. ~~Does Alchemy hold desired state?~~ **Answered (operator, 2026-08-10): yes, and changing the platform directly is overwritten on the next `composer deploy`. Accepted.** So the imperative operations stay, and their effect on a Composer-managed service is understood to be transient. What remains for the design is only whether the CLI says so at the point of use — a service the CLI can tell is Composer-managed could carry a line on `promote`, `rollback`, `start` and `stop` noting the next deploy reconciles it. That depends on question 2: whether the records carry anything identifying a service as Composer-managed. -2. What do Composer's app and deployment records actually contain? If the Alchemy path populates a different subset of fields than `app deploy` did, `service show` and `service deployment show` are presenting a shape nobody has looked at. -3. Where does log reading live? `composer log` and a `service deployment logs` would be two ways to read the same thing, and the project spec rules that a subgroup is owned by exactly one command family. -4. **If log reading lands here, what opens the socket?** Added during S2c, which shelved `service logs` rather than ship it. Deployment logs are the one endpoint in the list that upgrades to a **WebSocket**, and the engine's client is HTTP-only, so the port had been taking a raw token and letting the compute SDK build the URL and set the `Authorization` header itself. The rev-6 credential model rules that out — credentials never reach commands, and `getCredentials` is now deleted — so the command cannot come back until the engine can open an authenticated socket. The design for that, written at the operator's instruction, is `assets/engine/websocket-transport-design.md`: the engine opens the socket and hands back a decoded record stream, with reconnection across the ten-minute cutoff owned by the engine rather than reimplemented per command. **Read its §7 before building anything** — if deployment logs can be served over plain HTTP the way `build logs` already is, the transport work disappears and the command becomes a copy of `build logs`. The shelved handler is reviewed, green, and in the `s2c-services` history, so restoring it is small once the transport question is answered. - -One standing caveat: every endpoint above is marked experimental and subject to change without notice. Designing a stable CLI surface over an unstable API is how the next bastardization gets built, so the design has to say what it is willing to depend on. - -**Ordering.** After S3, because Composer's contract is the input. Before S7, because S7 mounts the full grammar tree behind a build-time completeness check and this slice changes that tree. - -### S7 — Release pipeline + rc1 - -**CLOSED 2026-08-12** — shipped as prisma-cli #164 plus the Release-immutability fix #166; acceptance verified in `specs/s7-release.md`'s Close-out; leftovers in `deferred.md`. The DoD artifact exists published: the operator's first real publish put `@prisma/cli@8.0.0-rc.1` (one binary answering platform, composer and ORM) and `@prisma/cli-engine@8.0.0-rc.1` on npm under `next`, `latest` untouched — RC releases publish under `next` by ruling until the deliberate flip. The bare-`prisma` cutover waits on `prisma7`; engine-pin convergence waits on the product repos bumping to the published engine. Next by the graph: S9 after the S5 cutover and S2d land (both dispatched elsewhere). - -### S9 — The error-code catalogue (last) - -Repo: prisma-cli. The engine raises its `CLI.*` codes from sixteen-plus -construction sites and catalogues them nowhere; -`docs/product/error-conventions.md` catalogues the LEGACY flat code -space (`BUILD_FAILED` and friends), which dies with the commander -shell. ADR 0003 requires a new error code to update that document, so -today every new engine code either updates the wrong catalogue or -silently skips the rule. - -This slice writes the real one: every `CLI.*` code the shipped engine -raises, with its meaning, its exit code, and its `meta` shape; the -legacy flat catalogue is replaced, not appended to; ADR 0003's rule is -re-pointed at the new document. Products' own namespaces (`AUTH.*`, -`PROJECT.*`, `POSTGRES.*`, and the ORM/Composer families) are listed by -namespace owner, not enumerated here — each family documents its own. - -**Ordering.** Last, deliberately. Ruled by the operator (2026-08-11) -while triaging the package-manager capability, whose spec asked for -"documented wherever the engine catalogues its own codes" and found no -such place. Cataloguing before the ports land would document a code -space still being written; cataloguing before the old CLI is retired -would document two competing spaces at once. So: after S5 (ports done), -after S2d (commander shell deleted), after S7 if it slips. - -### S10 — Skills catch up (last, after S9) - -Repo: prisma-cli (+ wherever each skill lives). Every agent skill -that teaches or drives these CLIs is rewritten against the shipped -v8 surface: the Composer skills (`skills-contrib/` in the composer -repo), the ORM skills, and the platform-CLI command skills. The v8 -port renames commands, moves them between families, deletes -spellings, and changes output shapes; a skill written against the -legacy CLI silently teaches commands that exit 2. Added at operator -instruction (2026-08-12). Last, because skills document the shipped -surface: after S9, or after S7 if S9 slips — whichever means the -command tree and error catalogue have stopped moving. - -## Dependency graph - -```text -S1 ──► S2 ──► S3 ──► S5 ──► S7 ──► S9 ──► S10 - │ ▲ ▲ ▲ - └──────┘ │ │ -S4 (prisma/prisma) ───┴──► S5 │ -S6 (after S1) ─────────────► wired in during S3/S5 -S3 ──► S8 (design first) ───────────┘ -``` - -## Follow-ups parked on other work - -Recorded so they are not lost between slices. - -- **Restore the "what to run next" hints that pointed at `service - deploy`.** S2c dropped `service deploy` and `service build` (operator - ruling: they conflated local compiling with uploading a tarball, and - Composer supersedes them). Ten typed next actions in the surviving - service commands suggested running `service deploy`, and were removed - rather than left pointing at a command the binary no longer answers - to — `show`, `list-deploys`, `open`, `promote`, `rollback`, `remove` - and the domain commands now explain a failure without offering a - follow-up command. **Once Composer's deploy commands exist, add them - back pointing there** (operator instruction, 2026-08-10). The removal - is recorded in the S2c section of `assets/s2/parity-divergences.md` (the per-slice files were folded into it at S2d). -- **`service logs` returns in S8**, once the engine can open an - authenticated socket. Shelved, not rejected — unlike `service deploy`, - which is not coming back in that shape. - -## 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; `build logs` — `service logs` moved to S8) | -| Auth via context | S2, S3 (deploy, destroy) | -| Refresh under long runs | **Still unproven.** The child receives an access-token snapshot and never the refresh token. As of the 2026-08-14 amendment, the parent proactively rotates a refreshable stored OAuth pair before the handler when the access token is inside `CREDENTIAL_NEAR_EXPIRY_MS`; this avoids rejecting a healthy login and gives the child a fresh snapshot. Nothing bounds the child runtime, so a converge can still outlive that refreshed snapshot and fail after it has created resources. That remaining limitation is recorded in `deferred.md`. | -| Config sections, command families, validator absence | Two levels, and they are proven in different places. The section machinery — a total validator including absence, a validator's warning diagnostic, and the engine's unknown-section check — is proven by the engine's own suite (`packages/cli-engine/tests/config.test.ts`) against toy sections. What S3 adds is ONE real section end to end: composer's, a single optional string field, declared by one family, read from disk by the bin's real loader, accepted by composer's own validator, and arriving at composer's handler as the path it acts on (`v8-bin.test.ts`, "hands the composer section of prisma.config.ts to the composer family"). Only the accepting path is covered there: nothing in the bin shows composer's validator refusing a section, running on an absent one, or warning on an unknown key, because `log` against that one fixture is the only run a shipped composer command makes to config without credentials. The platform family declares no section, so two families contributing to one config file is unproven, and so is any section with required or structured fields. Both wait for 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 | -| Engine error-code catalogue | S9 | - -## 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/command-grammar-cleanup.md b/.drive/projects/prisma-cli-v8/plans/command-grammar-cleanup.md deleted file mode 100644 index 51e4cc94..00000000 --- a/.drive/projects/prisma-cli-v8/plans/command-grammar-cleanup.md +++ /dev/null @@ -1,69 +0,0 @@ -# Command grammar cleanup — dispatch plan - -Slice contract: `specs/command-grammar-cleanup.md`. One PR into `main`, this worktree's branch. Sequential dispatches; each hands the next a state where `pnpm --recursive exec tsc --noEmit` and `pnpm --filter @prisma/cli test` are green (mount-coverage may be legitimately red mid-slice only where a dispatch's note says so). - -## D1 — The compute config and `init` are gone - -**Outcome:** no source, test, or mount references `prisma.compute.ts`/`.json`, `@prisma/compute-sdk/config`, `src/commands/init/`, `src/types/init.ts`, `src/lib/app/{compute-config,build-settings,deploy-framework,build}.ts`, or the `SERVICE.COMPUTE_CONFIG_*` error codes. Service commands lose the config-target positional; `resolveComputeManagementContext` and `resolveComputeTarget` are deleted; agent setup-status stops reading the config. `init` leaves the mount table, `FAMILYLESS`, and `EXPECTED_MOUNT_PATHS`; root help examples drop `init` (respelled fully in D4). - -**Builds on:** clean main. **Hands to D2:** `service/target.ts` free of compute-config imports, service command files free of the positional, suites green with config tests deleted. - -**Focus:** follow the import graph outward from the deleted modules; delete dependents that only served the config path (candidates: `tests/compute-config.test.ts`, `tests/service-compute-config.test.ts`, `tests/init*.test.ts`, `tests/app-build.test.ts`, e2e/`init` entries, `lib/app/bun-project.ts`/`env-config.ts` if orphaned — verify, don't assume). - -## D2 — Service commands take parameters only - -**Outcome:** every service command that targets an existing service accepts `--service ` (match by name) or `PRISMA_SERVICE_ID` (match by id, the domain-flow mechanics generalized); neither present → structured error naming `--service`, exit 2, interactive terminals included. The interactive picker, `readSelectedApp`/`setSelectedApp`/`clearSelectedApp`/`selectedByProject`, `rememberSelectedService`, and `service remove`'s selection cleanup are deleted. Branch targeting is `--branch` only: `resolveRequestedBranch`'s git inference and `lib/git/local-branch.ts` go (keep the existing non-git defaults: "main" read flows, "production" domain flow). Project resolution unchanged; `project link` keeps its picker. - -**Builds on:** D1's target.ts. **Hands to D3:** parameter-only resolution with updated unit tests (picker/selection tests deleted or rewritten as missing-flag error tests), suites green. - -**Focus:** `service create` doesn't resolve an existing target — keep it working. Callers using `skipSelection` (deployment-id flows) keep skipping. Check `git connect`/other importers before deleting `local-branch.ts`. - -## D3 — `remove` renamed to `delete` (six commands) - -**Outcome:** `project delete`, `project env delete`, `postgres delete`, `postgres connection delete`, `service delete`, `service domain delete` exist; no `remove` spelling survives for them in paths, file names, exported symbols, command ids, help, examples, next actions, error copy, consent questions, unit tests, or e2e `describeCommand` markers. `EXPECTED_MOUNT_PATHS` respelled. `git disconnect`, `auth … logout`, bucket commands untouched. - -**Builds on:** D2 (service files settled). **Hands to D4:** renamed tree, suites green. - -**Focus:** mechanical fan-out; grep each old spelling after the rename to prove extinction (`.drive/` history and changelog-like records exempt). - -## D4 — Moves, family wrapping, group removals - -**Outcome:** mount table matches the spec's acceptance tree. `postgres backup restore`, `migration ref list|set|delete`, `db migrate`, `contract format`, root `dev` and `deploy` mounted; `ref` group, `composer` group (+ brief), `build` group (`build logs`, `src/commands/build/`, its tests, e2e entries) gone; `composer destroy`/`log` not mounted. In `cli.ts`, both external families are re-wrapped with `defineCommandFamily` preserving `configSection` and `docsBaseUrl`: composer keeps only `deploy`/`dev`; ORM passes commands through but drops the `migration ref` redirect and respells the `migration apply` replacement to `{bin} db migrate --to `. No aliases or redirects for any old spelling. `EXPECTED_MOUNT_PATHS` equals the acceptance tree; group briefs updated (`postgres backup` brief now covers restore); root help examples live spellings (e.g. `auth login`, `project list`, `deploy`). - -**Builds on:** D3's tree. **Hands to D5:** final grammar, mount-coverage green against the acceptance tree, suites green. - -**Focus:** mount-coverage's family-completeness check runs against the wrapped families — wire `MOUNTED_FAMILIES`/`createCli` to the wrapped objects. Watch `exactOptionalPropertyTypes` when re-passing normalized `CommandRedirect`s as `RedirectSpec`s. - -## D5 — String sweep, docs, process records, full verification - -**Outcome:** no old spelling survives as a command reference anywhere in `packages/`, `README.md`, `docs/` (run-command next actions, help examples, error copy, comments that instruct); `tests/e2e-coverage.test.ts` exclusions/backlog respelled; README/docs command enumerations match the acceptance tree; each s2 divergence record under `.drive/projects/prisma-cli-v8/assets/s2/` gets a short entry for the renames/moves that touch it; `assets/command-review.md` is restored from commit 76a2c8a and regenerated against the new tree. Full verification per AGENTS.md: `pnpm --recursive exec tsc --noEmit`, `pnpm lint`, `pnpm --filter @prisma/cli test`, `pnpm --filter @prisma/compute test`, `pnpm --filter @prisma/cli test:e2e` (report a credential-less skip plainly). - -**Builds on:** D4's final grammar; the sweep inventory appended below. **Hands to:** slice-DoD; PR-open. - -## Sweep inventory (2026-08-21) - -Hazards every dispatch must respect: - -- `packages/cli-engine/src/execution/command-tree.ts:295` throws at `buildCli()` when a redirect's `from` collides with a mounted path. Mounting `migration ref *` while the ORM family still carries the `migration ref` redirect fails construction — the D4 family wrap (which drops that redirect) is mandatory, not cosmetic. -- `tests/e2e-coverage.test.ts` parses `src/cli.ts` as TEXT via the marker `mountedCommands: Readonly> = {`. Keep that literal's shape when editing the mount table. -- `EXPECTED_MOUNT_PATHS` is asserted sorted; keep alphabetical order. -- No help snapshot tests exist; help is asserted by `toContain` in `tests/bin.test.ts:344-430` and `tests/orm-mount.test.ts:145-160`. - -Per-spelling checklist (line numbers pre-change): - -- **init:** cli.ts:32,289-292,310; commands/init/* (init.ts, settings.ts, config-file.ts, agent-setup.ts, link.ts, types.ts), types/init.ts, project/link.ts:142 (prose); mount-coverage:11,46,114; tests/init*.test.ts, compute-config.test.ts; e2e/init.e2e.ts; packages/cli/README.md:82, packages/prisma/README.md:79, docs/product/command-principles.md:31, cli-style-guide.md:61, output-conventions.md:313, error-conventions.md:274-275, docs/architecture/cli-engine-requirements.md:217. -- **project remove:** cli.ts:205; project/remove.ts; project/presentation.ts:18; mount-coverage:144; project.test.ts:115,2496; e2e/project-lifecycle.e2e.ts:211; e2e/deployed-service.ts:147,178 (comments). -- **project env remove:** cli.ts:210; project/env-remove.ts; lib/app/env-config.ts:141 (error fix copy); mount-coverage:140; project.test.ts:120,2316,2473,2490; e2e/project-lifecycle.e2e.ts:186. -- **postgres remove:** cli.ts:216; postgres/remove.ts; controllers/database.ts:143; mount-coverage:133; postgres.test.ts:145,533,1516; e2e/postgres.e2e.ts:336. -- **postgres connection remove:** cli.ts:221; postgres/connection-remove.ts (:36 usage string); mount-coverage:129; postgres.test.ts:150,2424,2472; e2e/postgres.e2e.ts:305. -- **service remove:** cli.ts:243; service/remove.ts; service/errors.ts:354,361 (why + next action); service/release.ts:39; mount-coverage:167; service-remove.test.ts; e2e/service.e2e.ts:7,127. -- **service domain remove:** cli.ts:246; service/domain-remove.ts; service/errors.ts:639 (runCommandAction); mount-coverage:160; service-domain.test.ts:524; e2e-coverage.test.ts:142 (AWAITING_COVERAGE). -- **postgres restore:** cli.ts:215; postgres/restore.ts:90,114; mount-coverage:134; postgres.test.ts:144,1182; e2e-coverage.test.ts:86 (EXCLUSIONS key); README prose (cli:76, prisma:73). -- **ref …:** cli.ts:279-281,:188 (group brief); mount-coverage:148-150; e2e-coverage:73-75; orm-mount.test.ts:145-160 (root-help group assertions incl. `ref`); cli-engine tests/redirects.test.ts:431 (synthetic fixture string — respell for coherence). -- **migrate:** cli.ts:270; mount-coverage:116; e2e-coverage:63; orm-mount.test.ts:141 (pins redirect next-action "prisma-test migrate --to " — respell to db migrate per the wrap); cli-engine redirects.test.ts + telemetry-payload.test.ts:80 (synthetic fixtures); README.md:90, cli README:81, prisma README:78, packages/cli/AGENTS.md:36; command-principles.md:38, cli-engine-requirements.md:158. -- **format:** cli.ts:265; mount-coverage:109; e2e-coverage:62; READMEs + packages/cli/AGENTS.md:36. cli-engine-requirements.md:196 already describes `contract format` (doc ahead of code — now true). -- **composer:** cli.ts:251-254,:180-182; service/presentation.ts:177 (runCommandAction "composer deploy" → "deploy"); mount-coverage:99-102,176; e2e-coverage:92-99,132; bin.test.ts:448-511; v8-conformance.test.ts:41-43,69; composer-isolation.test.ts (prose); orm-mount.test.ts:15,65; scripts/conformance.ts:34,62; README.md:31, cli README:42,80, prisma README:42,77, packages/cli/AGENTS.md:12,36; examples/*/README.md (next-smoke:22, hello-world:5 — update, they ship in-repo). The composer package's own help already says `{bin} deploy` — correct at root; nothing to change upstream. -- **build logs:** cli.ts:250,:179; commands/build/logs.ts (:101 next action, :158-160 examples); mount-coverage:98; build-logs.test.ts; e2e-coverage:128,145; cli README:79, prisma README:76, packages/cli/AGENTS.md:36. -- **README command tables:** packages/cli/README.md:70-82 and packages/prisma/README.md:67-79 are hand-duplicated — edit both. -- **Pre-existing doc bug in touched copy:** error-conventions.md:275 says `init --format json`; flag was `--config-format`. Moot once init is removed — delete the example with the command. -- **.drive/ hits:** historical records (s2 specs/design docs, parity divergence bodies, command-inventory) stay as history; only the short new divergence entries + regenerated command-review.md change. diff --git a/.drive/projects/prisma-cli-v8/plans/config-file-resolution.md b/.drive/projects/prisma-cli-v8/plans/config-file-resolution.md deleted file mode 100644 index 85ce22e9..00000000 --- a/.drive/projects/prisma-cli-v8/plans/config-file-resolution.md +++ /dev/null @@ -1,46 +0,0 @@ -# Config-file resolution — dispatch plan - -Slice contract: `specs/config-file-resolution.md`. One PR into `main`. Sequential dispatches; each hands the next a state where `pnpm typecheck` and `pnpm exec turbo run test --concurrency=1` are green (the sequential run — the parallel `pnpm test` has a known engine-dist race). Engine-surface hazard applies to every dispatch: verify `@prisma/cli-engine`'s committed version is still unpublished before merging the PR; if a train shipped it, bump the engine version first. - -## D1 — The loader discovers a chain - -**Outcome:** `loadConfig` resolves an ordered chain of config files instead of one: anchor directory (cwd, or the `--config` file's directory) upward to the first `.git` directory; `parent: false` ends collection, `parent: "path"` names the next link explicitly (cycle-checked, may cross the boundary), no `.git` above means anchor-only. `parent` joins the reserved top-level keys (loader strips it like the marker; `reservedConfigSectionName` covers it; construction-time rejection includes it). Every file on the chain is evaluated with the existing marker/version/unreadable classification, each failure naming its file. `LoadedConfig` becomes a chain shape — per-file `{path, sections}` in nearest-first order plus file-level diagnostics — and every existing consumer (`needs.ts`, hosts, engine tests, the skills reader temporarily via a nearest-file adapter) compiles and passes against it with single-file behavior unchanged: one file in cwd behaves exactly as today. - -**Builds on:** clean main. **Hands to D2:** the chain type, discovery green under new engine tests (boundary stop, `parent` forms, cycle guard, anchored fixtures per the spec's test-anchoring requirement), all suites green. - -**Focus:** the loader doc comment says "cwd only, no walking up" — it and the `EVALUATE_ONE_FILE_ONLY` rationale need rewriting to the new truth. Symlink-resolve the anchor. Windows realpath on every chain comparison. The unknown-key check in `needs.ts` iterates the chain per file from this dispatch on. - -## D2 — Sections merge per key with provenance - -**Outcome:** `ConfigSection` gains optional `merge(parent, child)`; the engine default merges per key at the section's top level and replaces below. `checkConfiguration` folds each needed section over the chain nearest-first, validates the merged view (validators unchanged), and hands `ctx.config` the resolved value. Every resolved value carries provenance; post-merge validation diagnostics name the contributing file; relative-path resolution against the declaring file is provided as an engine helper the provenance makes possible (sections opt in by resolving paths through it). Merging never mutates the frozen exports. - -**Builds on:** D1's chain. **Hands to D3:** engine tests covering shadowing, fall-through (nested file lacking a section the root has), partial merge (root `skills.check` + package `skills.agents`), provenance in error copy, and the ORM absence-error firing only on a chain with no `orm` section anywhere; all suites green. - -**Focus:** `merge` must be optional and type-backward-compatible — the shipped orm/composer dists implement `ConfigSection` against the current engine and must keep working with the default. `__proto__`/`fromEntries` discipline extends to the merged object. - -## D3 — One resolver in the product - -**Outcome:** `readProjectSkillsConfig` and the out-of-handler reads (skills staleness notice, post-login tip) resolve through the engine's chain resolver; the hand-rolled `existsSync` + direct `loadConfig` path and D1's temporary adapter are deleted. From any subdirectory, the staleness notice and the skills commands agree on the governing config. The skills section keeps its null-collapsing contract for out-of-handler callers. - -**Builds on:** D2's resolver. **Hands to D4:** exactly one resolution code path, skills unit tests green from nested-directory fixtures. - -**Focus:** the "one stat before paying for transpile" property `readProjectSkillsConfig` had should survive — chain discovery is stat-only until a file exists; keep the no-config fast path. - -## D4 — Subdirectory init scaffolds only - -**Outcome:** `prisma init` run in a directory whose discovered chain contains an ancestor config skips the skills sync, the `postinstall` script, and the `prisma` devDependency by default, reporting each as skipped-with-reason; explicit flags still opt in; root init (no ancestor config) is byte-for-byte unchanged. Unit tests cover both shapes; the init e2e gains the subdirectory case. If D1's chain work removed the cause of the e2e rerun workaround (`e2e/init.e2e.ts:189-200`), the workaround comes out; otherwise its comment is updated to name what still forces it. - -**Builds on:** D3 (init detects the ancestor through the same resolver as everything else). **Hands to D5:** init behavior finished, suites and init e2e green. - -## D5 — Docs, records, full verification - -**Outcome:** user-facing docs describe discovery, merging, `parent`, and the two-config layout (the config documentation surface plus `docs/product/*` touchpoints that mention config today); the ledger closes the stale pathe entry (the loader realpath fix shipped in engine 0.2.2) and records this slice's rulings; the spec's status line gains the landed date. Full verification per AGENTS.md, including conformance (`pnpm exec turbo run conformance --filter @prisma/cli --force`) and the sequential test run; the engine-version-unpublished check from the plan header re-verified at PR-open. - -**Builds on:** D4. **Hands to:** slice-DoD; PR-open. - -## Hazard inventory (2026-08-25) - -- Engine surface changes ride the unpublished engine version or force the three-repo re-peer chain — check at start AND at merge; the version can publish out from under a long-running slice. -- This repository contains fixture `prisma.config.ts` files and will gain more; every loader/resolver test must pin its chain (temp dirs outside the repo, or explicit `parent: false` fixtures) or a real ancestor config leaks in — the exact failure the prior round's review caught. -- `tests/e2e-coverage.test.ts` parses `src/cli.ts` as text; D4 does not touch the mount table, but any drive-by edit there must keep the `mountedCommands` literal's shape. -- The parallel `pnpm test` race (engine dist rebuild vs cli tests) predates this slice; verify with the sequential run and do not chase it here. diff --git a/.drive/projects/prisma-cli-v8/plans/engine-colour.md b/.drive/projects/prisma-cli-v8/plans/engine-colour.md deleted file mode 100644 index 8f47422d..00000000 --- a/.drive/projects/prisma-cli-v8/plans/engine-colour.md +++ /dev/null @@ -1,146 +0,0 @@ -# Plan — the engine renders - -Contract: `../specs/engine-colour.md` (ruled 2026-08-11, amended the same day — -read §8 first; it overrides the body wherever the two still read differently). - -Branch `engine-colour`, off `main`, one PR. #140 and #143 also add a field to -`Runtime`; all three additions are independent, so this does not stack. - -Two dispatches. The joint is the styling surface: D1 builds the thing that turns -a tone into bytes and tells a renderer how much room it has, without changing -what any block looks like. D2 rewrites the blocks against it. Splitting the other -way — blocks first, colour after — would mean writing every renderer twice. - -## Dispatches - -### D1 — the palette and the styling surface - -Outcome: the engine can turn a `Tone` into bytes, decides colour from stderr with -the flag beating the environment, and tells a command how wide stderr is. No -block renders differently yet. - -Builds on: nothing. - -Hands to: `Ui` carrying `tone()` and `width`; a text-rendering helper that takes -`Text` and returns bytes; a display-width helper. D2 calls all three. - -Surfaces in play: - -| File | What changes | -| --- | --- | -| `src/presentation.ts` | `Text`, `Span`, `Tone`, `Status`; `Ui` gains `tone` and `width` | -| `src/execution/palette.ts` (new) | Tone → colorette verb, per contract §2.3; `Text` → bytes; display width | -| `src/execution/command-context.ts` | `makeUi` builds the full surface from the resolved colour decision and stderr's width | -| `src/execution/shared-flags.ts` | Colour resolution moves to stderr; flag beats `NO_COLOR` beats stream | -| `src/runtime.ts` | `OutputStream.columns?`, `HostProcess.stderr.columns?` | -| `src/testing.ts` | The harness can set stderr's columns | -| `packages/cli/src/v8/runtime.ts` | The bin passes `columns` through instead of dropping it | -| `package.json` | `colorette` and `string-width` as dependencies | -| `src/exports/index.ts` | Export `Text`, `Span`, `Tone`, `Status` | - -Tests: every tone with colour on and off, exact bytes; `--color` on a -`NO_COLOR=1` environment; `--no-color` on a terminal; colour follows stderr, so -`cmd > file` with a terminal stderr keeps it; `ui.width` is stderr's columns and -`POSITIVE_INFINITY` when stderr is not a terminal. - -### D2 — the blocks draw - -Outcome: tables align, cards are aligned and toned with an opt-in rail, trees -draw connectors and status glyphs, drawings render verbatim, and every block text -field takes `Text`. - -Builds on: D1's `Ui`, text renderer and width helper. - -Hands to: the slice DoD. - -Surfaces in play: - -| File | What changes | -| --- | --- | -| `src/presentation.ts` | `Block` per contract §3: `table`/`fields`/`tree`/`list`/`summary` take `Text`; `fields.rail`; `TreeNode.status`/`.tone`; `drawing` added; `summary.tone` → `summary.status` | -| `src/execution/rendering.ts` | The four renderers per contract §3; `renderBlock` takes the `Ui` | -| `src/exports/index.ts` | Export `Span`-carrying block types and `drawing` | -| `packages/cli/src/v8/**` | The `summary.tone` → `summary.status` rename, ~57 sites, compiler-driven | -| `packages/cli/tests/v8-golden-rendering.test.ts` | Re-pin the card and table bytes | -| `packages/cli/tests/**` | Whatever else pins block bytes | - -Tests: a ragged table aligns; the same table with span cells aligns identically -with colour on and off; a table with a CJK cell aligns; a card aligns values into -one column and tones its labels; `rail: true` reproduces the legacy rail bytes -from `packages/cli/src/shell/ui.ts:81-127`; a tree matches the style guide's -`├─ ✘ table user` example verbatim; a drawing round-trips its spans with no -reflow; a command that overruns `ui.width` prints unmodified. - -## Validation gate - -All green before every commit, judged by pnpm's own exit codes: - -- `pnpm --filter @prisma/cli-engine test` (its `test` script runs build + - typecheck + vitest) -- `pnpm --filter @prisma/cli test` -- `pnpm typecheck` -- `pnpm lint` - -## Halt and surface rather than improvise - -- Any point where the contract and the shipped engine disagree about an existing - mechanism. -- Any need to touch `packages/cli/src/auth/**`, `packages/cli/src/v8/auth/**`, - publish machinery, or a file outside the tables above. -- Any block whose legacy rendering cannot be expressed in the grammar §3 gives — - that is evidence the grammar is missing a kind, which is an operator question, - not an implementer's escape hatch. - -## Open items - -- **The 36 `fields` sites do not get their rail back in this PR.** The engine - gains `rail`, defaulted off; which cards want it is a per-command judgement the - engine cannot make. Alignment and the accent colour return everywhere at once, - which is the loss the S2b divergence list records. Adopting the rail is - follow-on work for the family that owns each command. -- **Nothing binds `ui` yet.** All 54 platform presenters are written - `human: () => [...]`, so after this PR the engine colours what it draws itself - — headings, connectors, glyphs, rails — and nothing else. Commands reaching for - `ui.tone`, spans and `drawing` is the conversion the contract §6 defers to each - family. -- **A bare `vitest run` in `packages/cli-engine` tests `dist`, not source.** - The package's `test` script is `build && typecheck && vitest run`, so the - validation gate is honest — but anyone editing engine source and reaching - straight for `vitest run` is testing the previous build. It surfaced here - when a deliberate defect failed to fail. Worth a line in - `docs/reference/testing-patterns.md`, which is outside this slice. -- **Half of stderr is coloured after this PR.** Blocks are; next actions and - diagnostics still render as plain strings. That is correct for this slice — - the contract's block grammar is what it governs — but it is the first thing a - converting family will notice, and the natural next slice. Surfaced in review - round 2. -- **The style guide's card example has no colon on the key** - (`│ local repo ~/code/apple`), while both legacy renderers and contract §3.2 - write `${label}:`. The engine follows the contract and therefore the shipped - bytes; the guide's example is the outlier. Either the guide gets corrected or - a later block option makes the colon optional — not this slice's call. - Surfaced in review round 2. -- **A table whose last cell is an empty span carrying a tone keeps its - separator spaces**, because the trailing-space strip sees zero-width escape - sequences rather than spaces. Alignment is unaffected and no current caller - can produce the shape. Reviewer recommends no change; recorded so the next - person to touch the strip knows it was considered. -- **Diagnostics on the parse-failure path render before colour is resolved.** - `applySharedFlags` has not run yet, so `state.colorEnabled` is still the - `false` default from `engine.ts:289`. Harmless for this slice, which colours - only the four block renderers — but a later slice that colours diagnostics or - usage errors has to resolve colour pre-parse, the way `sniffFormat` already - resolves format, and for the same reason: the decision must be in force before - the parser can fail. Surfaced in review round 1. -- **Colour and width read two independent signals.** Colour keys off - `runtime.isTty.stderr`, width off `runtime.stderr.columns`. Node keeps the two - consistent, but the test harness lets a caller set a width on a non-terminal - stderr or a terminal stderr with no width. Neither combination is wrong today — - width already falls back to unbounded — but a future change that infers one - from the other should know they are separate. Surfaced in review round 1. -- **Glyph mode is not adopted.** The ORM detects whether the terminal can render - unicode box-drawing (`prisma/prisma` - `packages/1-framework/3-tooling/cli/src/utils/glyph-mode.ts`: TTY plus a UTF-8 - locale, else ASCII). The engine already emits `✔ ✘ ⚠ ℹ` unconditionally, so - connectors do not make it newly wrong, but a tree is more box-drawing than the - engine has ever emitted. Recorded in `deferred.md`. diff --git a/.drive/projects/prisma-cli-v8/plans/engine-owns-telemetry.md b/.drive/projects/prisma-cli-v8/plans/engine-owns-telemetry.md deleted file mode 100644 index 646e458e..00000000 --- a/.drive/projects/prisma-cli-v8/plans/engine-owns-telemetry.md +++ /dev/null @@ -1,146 +0,0 @@ -# Plan — the engine owns telemetry reporting - -Contract: `../specs/engine-owns-telemetry.md` (ruled 2026-08-11, amended the -same day — **read §1.1 first**; it strikes three things the original draft -asked for and overrides the body wherever the two still read differently). - -Branch `spec/engine-owns-telemetry`, PR #143. The spec ships in the same PR -as the implementation. - -Reference material for every dispatch: the shipped ORM implementation is the -specification. It is cloned at `.reference/prisma` (gitignored) — -`packages/1-framework/3-tooling/cli-telemetry/src/`, -`packages/1-framework/3-tooling/cli/src/utils/telemetry.ts`, -`packages/1-framework/3-tooling/cli/src/commands/telemetry/`, and the -user-facing contract in `docs/Telemetry.md`. Where the platform shell's copy -(`packages/cli-telemetry`, `packages/cli/src/v8/telemetry`) and the ORM's -disagree, the ORM wins. - -## Dispatches - -### D1 — the telemetry core inside the engine, called by nothing yet - -Outcome: the engine can decide whether to report, read and write the user's -preference, mint an installation id, resolve the endpoint, and compose a -payload from a command snapshot — all pure or filesystem-only, all tested, -with no execution path calling it. - -Surfaces in play: - -| File | What changes | -| --- | --- | -| `cli-engine/src/telemetry/gating.ts` | `resolveGating` resolving directly to spec §2.3's five-value union, CI first | -| `cli-engine/src/telemetry/user-config.ts` | read / write / `ensureInstallationId` / path resolution, ported as-is | -| `cli-engine/src/telemetry/payload.ts` | `TelemetryPayload` (today's `ParentToSenderPayload`) and the snapshot → payload projection | -| `cli-engine/src/telemetry/endpoint.ts` | the constant and the `PRISMA_NEXT_TELEMETRY_ENDPOINT` override | -| `cli-engine/tests/` | the tables in contract §5 that do not need a running CLI | - -The sanitiser stops redeclaring `EngineCommandSnapshot` structurally and -imports the real type from `../run-summary`. - -Hands to D2 and D3: a tested internal module whose decision, store and -payload are importable from the engine's execution path and its commands. - -### D2 — the engine reports, at command start - -Builds on D1. - -Outcome: a CLI that declares `telemetry: { docsUrl }` discloses on first run, -mints its id, and hands one payload to the runtime seam before the handler -runs — and no telemetry failure is observable in the run. - -**First task, carried over from D1.** D1 ported the preference store as-is, -including its direct `process.env` reads for `$XDG_CONFIG_HOME` and -`%APPDATA%`. Contract §2.5 now pins the fix: thread `env` through -`userConfigPath`, `readUserConfig`, `writeUserConfig` and -`ensureInstallationId`, sourcing it from `runtime.env`. D2 is the first -caller, so it owns the signature change and the D1 test updates that follow; -D3's commands take `ctx.env`. `process.platform` and `process.pid` stay — -see §2.5. Until this lands, a `createTestCli` run reads and writes the real -user's config file, which is why D2's failure-isolation tests cannot be -written before it. - -Surfaces in play: - -| File | What changes | -| --- | --- | -| `src/cli.ts` | the `telemetry` block on `createCli` | -| `src/runtime.ts` | `isCI: boolean`; optional `spawnTelemetry` | -| `src/execution/engine.ts` | the fire point, immediately after `state.snapshot` is assigned (line 390 today) — exemption, gating, disclosure, mint, compose, hand off, swallow | -| `src/testing.ts` | `telemetrySpawner` and `isCI` seeds | -| `src/exports/index.ts` | whatever of the above is public | -| `tests/` | contract §5's timing, exemption, disclosure, no-values and failure-isolation cases | - -The failure-isolation tests are the ones that matter most here: a throwing -spawner, an unwritable config directory and a malformed stored config must -each leave exit code, stdout and stderr byte-identical to the same run with -no telemetry declared. - -Hands to D4: an engine that reports, seeded and assertable offline. - -### D3 — `telemetry status|enable|disable` as engine commands - -Builds on D1 (the store), verified against D2 (the exemption). - -Outcome: a product mounts the three commands in one line and gets the help -text, output and json shape both CLIs already ship. - -The platform shell's current versions (`packages/cli/src/v8/telemetry/ -{status,enable,disable,consent}.ts`) are already engine commands and are the -closest thing to a finished port — move them, with their help text intact, -and drop the `@repo/cli-telemetry` imports in favour of D1's modules. Confirm -each against the ORM's `commands/telemetry/` before moving: the two agree -today, and the ORM is the authority where they do not. - -Hands to D4: three mountable commands exported from the engine. - -### D4 — the shell migrates, the duplication goes - -Builds on D2 and D3. - -Outcome: the platform CLI reports through the engine, its own telemetry -reporting code is gone, and `@repo/cli-telemetry` is reduced to the child -sender. - -Surfaces in play: - -| File | What changes | -| --- | --- | -| `cli/src/v8/runtime.ts` | wires `isCI` (`ci-info`) and `spawnTelemetry` | -| `cli/src/v8/main.ts` | the `resolveTelemetryHooks` block goes | -| `cli/src/v8/cli.ts` | mounts the engine's three commands | -| `cli/src/v8/telemetry/` | `reporting.ts`, `is-ci.ts`, `status.ts`, `enable.ts`, `disable.ts`, `consent.ts` deleted; `sender.ts` build entry stays | -| `cli-telemetry/src/spawn.ts` | shrinks to fork + send + disconnect + unref; no longer re-resolves gating or re-reads the user config | -| `cli-telemetry/src/` | `gating.ts`, `user-config.ts`, `sanitize.ts`, `endpoint.ts` and their tests deleted; `enrich.ts`, `sender.ts`, the payload validator stay | -| `cli/tests/v8-telemetry*.test.ts` | ported onto the engine surface, unchanged in intent | -| `assets/s2/parity-divergences.md` | contract §4's two entries | - -Hands to: the slice DoD. - -## Validation gate - -All four green before each dispatch commits, judged by pnpm's own exit code: - -- `pnpm --filter @prisma/cli-engine test` (its `test` script runs build + - typecheck + vitest) -- `pnpm --filter @prisma/cli test` -- `pnpm typecheck` -- `pnpm lint` - -## Halt and surface rather than improvise - -- Any place the contract and the shipped ORM implementation disagree about - what the behaviour is. The ORM is the authority; a disagreement means the - contract is wrong and the operator decides. -- Any need to change the wire shape, the endpoint, the config path or the id - lifetime — contract §3 forbids all four. -- Any need to touch a file outside the tables above. - -## Open items - -- **`CliRunHooks.onSettled` has no consumer after D4.** Left in place; see - contract §8. Do not remove it as part of this slice. -- **`packages/cli-engine` gains no new dependency.** `ci-info` stays a bin - dependency, reached through `Runtime.isCI`. If a dispatch finds itself - wanting `ci-info` or `node:child_process` in the engine, that is the halt - signal above, not a judgement call. diff --git a/.drive/projects/prisma-cli-v8/plans/engine-redirect-table.md b/.drive/projects/prisma-cli-v8/plans/engine-redirect-table.md deleted file mode 100644 index 7bc704a2..00000000 --- a/.drive/projects/prisma-cli-v8/plans/engine-redirect-table.md +++ /dev/null @@ -1,79 +0,0 @@ -# Plan — engine redirect table - -Contract: `../specs/engine-redirect-table.md` (ruled 2026-08-11, §2 amended -the same day — read the amendment list at the top of §1 first; it overrides -the body wherever the two still read differently). - -Branch `engine-redirect-table`, off `spec/redirect-table` (PR #141), one PR -stacked on #141 and retargeted to `main` when #141 merges. - -## Dispatches - -### D1 — the whole surface, verb and flag redirects together - -One dispatch. The two halves share the matching table, the error -construction and the replacement rendering; splitting them would leave the -second dispatch wiring a second call site into machinery it did not build. - -Outcome: a family can declare retired invocations, and typing one gets a -`CLI.COMMAND_MOVED` envelope naming the replacement instead of a generic -unknown-command or unknown-flag error. - -Surfaces in play: - -| File | What changes | -| --- | --- | -| `src/command-family.ts` | `CommandRedirect`; `redirects` on the family, optional in, always-present out | -| `src/execution/command-tree.ts` | Merge every family's entries into one table; the four construction-time validations | -| `src/execution/engine.ts` | Consult the table when routing failed; derive the attempted path | -| `src/execution/stricli-adapter.ts` | Intercept `FlagNotFoundError` for flag redirects; reuse `resolveExample` for the replacement | -| `src/execution/settlement.ts` | The `CLI.COMMAND_MOVED` settlement | -| `src/exports/index.ts` | Export `CommandRedirect` | -| `tests/` | Per contract §3 | - -Validation gate, all green before commit, judged by pnpm's own exit code: - -- `pnpm --filter @prisma/cli-engine test` (its `test` script runs build + - typecheck + vitest) -- `pnpm --filter @prisma/cli test` — the consumer stays green -- `pnpm typecheck` -- `pnpm lint` - -Halt and surface rather than improvise: any point where the contract and the -shipped engine disagree about an existing mechanism beyond what §4's -follow-the-code note settles; any need to touch a file outside the table -above. - -## Open items - -- **A command that hands the terminal to a child still frames its pre-mount - usage errors as json.** The engine refuses `--json` for such a command - thoroughly — exit 2, no frames — but that refusal lives in - `executeMounted`, which a parse or routing failure never reaches. Verified - empirically on the merged tree: an unknown flag on such a command emits a - json `CLI.INVALID_ARGUMENTS` frame on stdout, and a retired flag emits a - json `CLI.COMMAND_MOVED` frame the same way. This predates the redirect - table and is identical for both settlements, so the redirect work - introduces no new divergence. Guarding only the redirect settlement would - make the two siblings disagree, which is why it was not done here. The - question — whether that refusal should cover failures that never reach the - command — belongs to whoever owns the `--json` refusal next. - -- **A re-mount silently kills a redirect, and nothing fails at construction - to say so.** `from` is an absolute path in the shell's tree, so it is a - fact about the shell, declared in family code. If the shell later moves a - group, the family's redirects stop matching and every check still passes — - construction cannot catch it, because a `from` naming something that does - not exist is the normal case. The operator ruled this trade-off knowingly - on 2026-08-11 when choosing family-declared redirects over shell-mounted - ones; the review surfaced it independently. Carry it to the ORM port: a - regroup means re-reading the redirect table by hand. Say so in the PR. - -- The engine's error codes are catalogued nowhere. Eighteen `CLI.*` codes - ship undocumented; `CLI.COMMAND_MOVED` makes nineteen. Operator ruled - 2026-08-11 that the catalogue waits for project close-out, when the full - list has settled. Not this PR's work. -- `execution/command-tree.ts`, `execution/stricli-adapter.ts`, - `execution/settlement.ts` and `execution/engine.ts` all have in-flight - changes on the Composer and init/shell-retirement branches. Merge down - from `main` before opening the PR. diff --git a/.drive/projects/prisma-cli-v8/plans/s1-engine-vertical.md b/.drive/projects/prisma-cli-v8/plans/s1-engine-vertical.md deleted file mode 100644 index ac302781..00000000 --- a/.drive/projects/prisma-cli-v8/plans/s1-engine-vertical.md +++ /dev/null @@ -1,134 +0,0 @@ -# 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`, `CommandFamily`, `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` (command- -supplied 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 (transcript-only per ruling R-I), endpoint/status/artifact, -opaque command-family `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 (130, 143; 3 is user cancel per ruling R-K). -**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 + nextActions, formerly `fix` before ruling R-I). - -### 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/plans/s2a-foundations.md b/.drive/projects/prisma-cli-v8/plans/s2a-foundations.md deleted file mode 100644 index 6d041e41..00000000 --- a/.drive/projects/prisma-cli-v8/plans/s2a-foundations.md +++ /dev/null @@ -1,101 +0,0 @@ -# S2a dispatch plan — foundations - -Contract: `../specs/s2a-foundations.md` (normative for this PR; the -overview `../specs/s2-overview.md` carries the standing rulings). One -branch `s2a-foundations` off `main`; the PR targets `main`. Every -dispatch verifies: engine + cli suites, `pnpm typecheck`, `pnpm lint` -exit 0 (measured as pnpm's own exit code), before its commit lands. -Commit discipline per the repo's standing rules (bot identity, dual -sign-off, explicit staging). - -The contract leaves no design decisions to dispatches. Where a -dispatch meets a fact the contract did not pin, it STOPS and returns -the question to the orchestrator — never improvises. - -## Dispatches (sequential) - -### D1 — Publish metadata + production dependency - -**Outcome:** contract §1 exactly: cli-engine publish metadata -(version 0.1.0, license file, README, publishConfig, repository, -prepack), `@prisma/cli-engine` in the cli's `dependencies`. -**Builds on:** merged S1. -**Hands to:** the operator's manual `npm publish`; every later -dispatch. -**Completed when:** `npm pack --dry-run` in packages/cli-engine lists -dist + README + LICENSE and nothing else; cli builds green with the -dependency move. THIS DISPATCH LANDS FIRST AND ALONE — the operator -publishes from it while later dispatches proceed. - -### D2 — Auth module extraction - -**Outcome:** contract §3: the three moves, `src/auth/index.ts` as the -single face with the exact export list, workspace operations extracted -from the controller, `makeGetCredentials` relocated, every importer -updated. Zero behavior change — the full existing cli suite passes -unmodified except for import paths in tests that mock the moved -modules. -**Builds on:** D1. -**Hands to:** D3 (getCredentials seam), D6 (the family port's -operations layer). -**Completed when:** legacy shell + v8 bin green; no import of -token-storage/auth-ops/client outside `src/auth/`. - -### D3 — `ctx.api` + harness client override - -**Outcome:** contract §2: SDK dependency exact-pinned in engine and -cli, `Runtime.managementApi`, lazy `ctx.api`, `CLI.CREDENTIALS_REQUIRED` -reuse on unauthenticated use, harness `managementApi.client` override, -draft amendments (§4, §10, §11), the four contract-listed tests. -**Builds on:** D2 (baseUrl source moved to `src/auth/client.ts`). -**Hands to:** D6 and every S2b/S2c port. -**Completed when:** contract §2 tests green; dist `.d.ts` shows -`api: ManagementApiClient` and no direct SDK type names beyond the -re-export alias. - -### D4 — Clack prompt renderer - -**Outcome:** contract §7: the adapter, the branch condition, the -draft notes, the fixture-driven clack-path tests. Reference: spike -branch `spike/clack-prompts` commit 903b25a (reimplement cleanly). -**Builds on:** D1 only (independent of D2/D3 — may run after D1 in -parallel with D2 if the orchestrator chooses; file overlap is nil). -**Hands to:** D6's `workspace use` prompt; S2d's wizard. -**Completed when:** all engine prompt tests green including the new -clack fixture suite; scripted/non-TTY paths proven clack-free. - -### D5 — Telemetry package + engine hook + bin wiring - -**Outcome:** contract §6: `packages/cli-telemetry` ported with the -preserved invariants, `EngineCommandSnapshot`, `RunHooks.onSettled` + -draft amendment, bin gating + detached sender, `telemetry -status|enable|disable` commands, the four contract-listed test areas. -**Builds on:** D3 (hook shape rides the same engine surface); D4 not -required. -**Hands to:** S2b/S2c ports (every command reports automatically). -**Completed when:** contract §6 tests green; a manual smoke run shows -the sender spawn under an enabled config and NO spawn under CI env. - -### D6 — `auth *` family + update check + slice closure - -**Outcome:** contract §4 (six commands, semantic tests, fixture-flag -removal, AUTH.* error mapping) and §5 (update-check move + both-shell -wiring). The S1 whoami handler rewires to `src/auth/index.ts`. -Divergence list updated (new: login flag removals, error-code -mapping, any update-check json-mode finding per §5). The PR -description is drafted per the operator's PR-description structure -(grounding example first, decision, narrative, alternatives last). -**Builds on:** D2, D3, D4 (prompt path), D5 (telemetry observes the -new commands automatically). -**Hands to:** operator review of the S2a PR; S2b. -**Completed when:** every acceptance box in the contract checks -except the operator-publish box (checked when the operator publishes); -review loop (architect + principal-engineer per the drive process) -run and findings fixed; PR opened non-draft. - -## Completeness check - -D1 → §1; D2 → §3; D3 → §2; D4 → §7; D5 → §6; D6 → §4 + §5 + closure. -Every contract section is owned by exactly one dispatch; the -acceptance boxes map 1:1 onto dispatch completion criteria plus the -review loop. diff --git a/.drive/projects/prisma-cli-v8/plans/s2b-resources.md b/.drive/projects/prisma-cli-v8/plans/s2b-resources.md deleted file mode 100644 index 9b53516b..00000000 --- a/.drive/projects/prisma-cli-v8/plans/s2b-resources.md +++ /dev/null @@ -1,37 +0,0 @@ -# S2b dispatch plan — resources - -Contract: `../specs/s2b-resources.md`. Branch `s2b-resources` off -`main` after S2a merges. Sequential dispatches; each verifies the full -root suite before its commit; standing commit/verification rules as in -`s2a-foundations.md`'s plan. Unpinned fact → STOP. - -### D1 — project group -`project list|show|create|link|rename|remove|transfer` + -`project env add|update|list|remove` per contract rules over inventory -entries. Includes the R-S2b-6 picker (`project link`) and two consent -commands. Hands the group-porting pattern (file layout, presentation -helpers, test matrix template) to every later dispatch — this -dispatch's structure IS the template; later dispatches copy it. -Includes a build-time test asserting the command-family maps and the -shell's mount map cover exactly the same command set (no command -mounted without a family entry, none declared but unmounted) — part of -the template every later dispatch inherits. - -### D2 — postgres group (rename included) -All 11 database→postgres commands incl. backup + connection; three -consent commands; two secret-bearing (R-S2b-4). - -### D3 — bucket + branch + git -`bucket *` (6, one consent, one secret), `branch list`, `git -connect|disconnect` (poll + browser event pattern per R-S2b-7/R-S2c-6 -precursor). - -### D4 — slice closure -Divergence list (per-command conformance rows), legacy fixture-test -deletion for ported groups, review loop (architect + principal -engineer), findings fixed, PR opened non-draft with the ruled -description structure. - -Completeness: D1→project+env; D2→postgres; D3→bucket/branch/git; -D4→closure boxes. Every contract acceptance box maps to exactly one -dispatch. diff --git a/.drive/projects/prisma-cli-v8/plans/s2c-services.md b/.drive/projects/prisma-cli-v8/plans/s2c-services.md deleted file mode 100644 index a79aab40..00000000 --- a/.drive/projects/prisma-cli-v8/plans/s2c-services.md +++ /dev/null @@ -1,57 +0,0 @@ -# S2c dispatch plan — services - -Contract: `../specs/s2c-services.md`. Branch `s2c-services` off `main` -after S2b merges. Standing rules as in S2b's plan. - -> **Executed, with three commands removed by operator ruling after the -> dispatches ran.** The dispatch scopes below are kept as the record of -> how the work was actually divided; they are not the shipped scope. -> `service deploy` and `service build` are **dropped** — they conflated -> local compiling with uploading a tarball, Composer supersedes them, -> and they are not coming back in that shape. `service logs` is -> **shelved**, not dropped: it needs an authenticated WebSocket the -> engine cannot yet open, and returns in S8 -> (`../assets/engine/websocket-transport-design.md`). `service run` was -> already ruled dropped. The slice ships **17 commands**; the divergence -> file is the accurate list. - -### D1 — service group core (rename included) -`service build|show|open|list-deploys|show-deploy` + `service domain -add|show|remove|retry|wait` — sync/poll commands first, establishing -the renamed group and the S2b template in this codebase area. - -### D2 — progress operations -`service deploy|promote|rollback|remove` per R-S2c-3 (step/progress/ -status event sequences, SDK polling on the injectable clock, consent -on remove). - -### D3 — streams -`service logs` + `build logs` per R-S2c-2 (session commands, output -events, channel routing; `build logs` gains its first tests). - -### D4 — agent + feedback + closure -`agent install|update|status`, `feedback`; divergence list; legacy -fixture-test deletion for ported groups; review loop; PR. `service -run` stays parked unless ledger Q2 was ruled — if ruled "S2c", it -becomes D3b per the ruling's mechanism. - -Completeness: D1→sync surface; D2→progress; D3→streams; D4→closure. - -### What actually shipped - -D1's group core, minus `service build`; D2's progress operations, minus -`service deploy`; D3's `build logs` only, with `service logs` shelved; -D4 whole. `service run` never ported. - -Two of D4's closure items diverge from the plan deliberately, both -recorded in the review ledger: the legacy fixture tests were **not** -deleted, because the commander shell still serves those commands until -S2d removes it, and the fixture-deletion decision is with the operator; -and the PR opens against `s2a-foundations` rather than `main`, because -that branch is where the engine and auth foundations this slice builds -on still live. - -That second item was written when the branch carried no merge-down from -its base. It has since taken three, the last of them `dc44f75`, which is -how the rev-6 credential surface reached this slice; the merge review -that followed is recorded in the review ledger. diff --git a/.drive/projects/prisma-cli-v8/plans/s2d-init-and-retirement.md b/.drive/projects/prisma-cli-v8/plans/s2d-init-and-retirement.md deleted file mode 100644 index bb0bdd2d..00000000 --- a/.drive/projects/prisma-cli-v8/plans/s2d-init-and-retirement.md +++ /dev/null @@ -1,30 +0,0 @@ -# S2d dispatch plan — init and shell retirement - -Contract: `../specs/s2d-init-and-retirement.md`. Branch -`s2d-init-and-retirement` off `main` after S2c merges. Standing rules -as in S2b's plan. R-S2d-3 (bin cutover) is BLOCKED on ledger Q4 — -sequence it last and confirm the ruling before starting D3. - -### D1 — init + version -R-S2d-1 and R-S2d-2: the wizard on the engine prompt surface (full -prompt matrix incl. `--yes`, non-interactive, cancel; byte-asserted -templates), the `version` command port. - -### D2 — deletions -R-S2d-4's checklist from the inventory: commander shell, fixture -machinery, fixture tests, `--trace`, env surface. Survivor list -enumerated. Legacy suite shrinks to zero fixture tests; everything -remaining is engine-side. - -### D3 — bin cutover (after Q4 ruling) -R-S2d-3 per the ruling; tarball smoke on plain Node (`npm pack` → -install into a temp dir → run `prisma-cli --version`, `auth whoami`, -config-bearing command). - -### D4 — closure -R-S2d-5 grammar completeness test; R-S2d-6 consolidated divergence -document; review loop; PR; S2 closed in the project plan (health -check + retro trigger per the drive process). - -Completeness: D1→wizard/version boxes; D2→deletion boxes; D3→cutover -box; D4→grammar/parity/closure boxes. diff --git a/.drive/projects/prisma-cli-v8/plans/s3-composer.md b/.drive/projects/prisma-cli-v8/plans/s3-composer.md deleted file mode 100644 index e6bc8eef..00000000 --- a/.drive/projects/prisma-cli-v8/plans/s3-composer.md +++ /dev/null @@ -1,60 +0,0 @@ -# S3 dispatch plan — Composer adoption (revision 2) - -Contract: `../specs/s3-composer.md` rev 2. Two repos. prisma-cli on -`s3-composer`; composer branch at D2. Implementers and reviewers on -Opus. Standing process rules as in the S2 plans. - -Ordering and proofs: D1 merges and publishes an engine release -before D2 pins it. D2 proves published cross-repo consumption -(family skeleton + rebuilt CLI on the engine); D3 proves the -commands; D4 pins released versions in both directions. Release -order is always engine → composer → prisma-cli; previews via -composer's pkg.pr.new workflow mid-slice. - -### D1 — `ctx.spawn` + credential injection (prisma-cli: engine) -Contract R-S3-1: the affordance (inherited stdio, same group, -record-and-replay signal latch, SIGTERM forwarding, abort ladder, -outlive-child, buffered commentary, reentrancy rule), -`Runtime.spawn` seam + harness fake, `exitWithChildStatus` + -session-kind settlement amendment, `--json` rejected as soon as -the command is known, credential injection with the SPI amendment -(the manager operation `activeAccessToken()` is the one unified -read for every credential origin — no source-based branch; an -environment-only manager implements it as a pass-through of the -env token; near-expiry refusal threshold ruled here), the -production environment-only CredentialManager exported from the -main entrypoint, draft amendments, the real-child/fake-spawn test split -from the contract's acceptance. - -### D2 — composer foundations (prisma/composer) -Contract R-S3-2/3/5 minus the commands: exact-pinned engine -(dual-manifest, tsdown `external`, Dependabot ignore, tarball -check); the config projection + diagnostics-list loader rewrite + -`fix` → `nextActions` boundary translation; the alchemy-free -static-graph import check; the rebuilt thin CLI (engine + family -skeleton) replacing clipanion `main.ts`; both test surfaces (fake -child script; the control-API double + conformance check from -`./testing`); the S8 planner-drift read (alchemy source from installed -node_modules) reported as a note to the operator. (The -alchemy-patch item is dropped: types-only, no runtime effect.) -D2 and D3 land as a stack; the clipanion shell is deleted in D3. - -### D3 — the four commands + e2e rewrite (prisma/composer) -Contract R-S3-4: deploy/destroy/dev/log as engine handlers -(config-evaluation listener strip; converges via `ctx.spawn`; -dev's live-session failure semantics; H8 stage + nextActions hint; -`--production` dropped). Composer's CLI e2e tests rewritten to -drive the exported commands (rebuilt CLI + `createTestCli`); the -clipanion shell, bespoke runner, and old e2e paths deleted. - -### D4 — mount + release glue + closure (both repos) -Family mounted under `composer` in the prisma bin (node >= 24 -floor); R-S3-6 release glue (ci.yml pin check, exact pins both -directions); divergence file; 1c closure with dispositions; ledger -Q2 + coverage-ledger corrections; slice review loop across both -PRs; PR descriptions per the ruled structure. - -Completeness: D1 → the mechanism; D2 → cross-repo proof + test -infrastructure + the rebuilt CLI; D3 → the product surface proven -in composer's own CI; D4 → composition into `prisma`, release, and -record-keeping. diff --git a/.drive/projects/prisma-cli-v8/plans/s6-conformance.md b/.drive/projects/prisma-cli-v8/plans/s6-conformance.md deleted file mode 100644 index 2f096259..00000000 --- a/.drive/projects/prisma-cli-v8/plans/s6-conformance.md +++ /dev/null @@ -1,130 +0,0 @@ -# S6 dispatch plan — Conformance checker (revision 4) - -Contract: `specs/s6-conformance.md` revision 4. Read it first; this plan decides nothing the contract leaves open. - -**State: all dispatches DONE.** prisma-cli PR #161 and prisma/prisma#29998, both ready for review. `packages/cli-conformance` holds the module graph, check 1 and check 2; the two checked packages call them on themselves. 35 tests, written before the code: 31 in the checker's own suite, 3 in the shell's, 1 in the engine's. Both checks pass against what this repo ships — composer's real validator survives the 21-case hostile corpus, and both published packages' built output imports only what they declare. `pnpm lint`, `pnpm typecheck` and `turbo run test --concurrency=1` are green; plain `pnpm test` fails on a pre-existing race recorded in `deferred.md`, which the base commit fails too. D3, D4 and D5 are unblocked — every question is closed. - -**All questions closed (operator, 2026-08-12; contract §5).** Nothing is blocked. The composer pin mismatch ships as one recorded exception; bins started are the ones the tarball declares; the sandbox is an in-repo gitignored path; no repo consumes the tool — prisma/prisma gets the checks as its own scripts, per the per-repo precedent both sibling repos already follow. - -## Shape - -One workspace package holding the checks as injected functions, and each checked package calling them on itself. That split is what makes the same checks reusable from another repo later: a second repo needs its own call site, not its own checker. - -Note the limit the architect review identified: under STOP-4(a) there is exactly one repo consuming this, so the shape buys testability rather than reuse today. And check 1 cannot express the assertion composer actually needs — "this specifier must *survive* in the built output", because composer bundles its internal scope and an inlined engine leaves no specifier at all. That is composer's existing `check-cli-engine-pin.mjs`, and it stays composer's. - -```text -packages/cli-conformance/ @repo/cli-conformance, private — name per STOP-4 - src/ depends on es-module-lexer and nothing it checks - findings.ts Finding, Report, exitCodeFor, human + json rendering DONE - module-graph.ts bare import roots of built output, via es-module-lexer DONE - subjects.ts CheckableSection + the engine's own section union DONE - checks/import-purity.ts check 1 DONE - checks/validator-no-throw.ts check 2 + the 21-case hostile corpus DONE - checks/tarball.ts checks 3a, 3b, 3c D3 - tests/ 31 tests over injected values and source strings - tests/fixtures/built-output/ the one on-disk fixture: the directory walk - -packages/cli-engine/tests/conformance.test.ts check 1 on its own built output DONE -packages/cli/tests/v8-conformance.test.ts check 1 + check 2 on the shell DONE -``` - -**Each subject is checked by the package that owns it.** Revisions 1 and 2 put the real-subject runs inside the conformance package, which meant it had to depend on `@prisma/cli` — and that inverted arrangement made its `tsc --noEmit` traverse the shell's whole command tree and depend on the engine's built declarations. It failed in practice. The checker now depends on nothing it checks, declares the one shape it needs structurally, and the two consumers call it on themselves from suites that already typecheck those trees. - -Reached through a **turbo task**, not a bare root script: `"conformance": { "dependsOn": ["^build"], "cache": false }` in `turbo.json`, owned by `@repo/cli-conformance`, with root script `"check:conformance": "turbo run conformance"`. The checks read `packages/cli/dist` and `packages/cli-engine/dist`, and neither workflow guarantees them — `pr-quality.yml`'s Test job never runs `pnpm build`, and turbo's `test` task depends on `^build`, which excludes `@prisma/cli`'s own build. The task makes that the graph's problem rather than a step-ordering convention nobody can see, and removes the need for a step condition in `publish.yml`. - -**Where the real pack-and-install runs:** the unit tests inject the pack, install and bin-start seams, so `pnpm test` stays fast. The real packing and installing happen when `check:conformance` runs — in CI, and locally when verifying the slice. - -**Why the checks are ordered, which is not a speed argument.** Both published packages declare `"prepack": "pnpm run build"`, and `packages/cli/tsdown.config.ts` sets `clean: true`, so packing destroys and rebuilds the directory check 1 reads. Verified with a sentinel appended to `packages/cli/dist/cli.js`: gone after `pnpm --filter @prisma/cli pack`. So check 1 completes before check 3 starts, they never run concurrently, and 3a reads only the extracted tarball. - -## D1 — findings, the module graph, and check 1 (import purity) — DONE - -Tests first, in this order: - -1. `module-graph.test.ts` — a static `import … from`, an `export … from`, a dynamic `import()`, a deep subpath (`pkg/sub/thing` reports root `pkg`), a scoped name, a relative and an absolute specifier (both ignored), `node:fs` and bare `fs` (both ignored), and — the case that decides the whole approach — a file containing `import.meta.resolve("@repo/private")`, the same name in a template literal, and the same name as a plain string, none of which is an import. These pass **source strings**, not files: `parse` takes a string, and an on-disk fixture of deliberately odd JavaScript would have to satisfy biome, which lints `**` minus `.drive`. The directory walk gets the only on-disk fixture — `tests/fixtures/built-output/`, two small valid files, one nested — proving a chunk in a subdirectory is found and that a missing directory sweeps nothing. -2. `import-purity.test.ts` — an undeclared import reports one finding naming the specifier and carrying the file in `where.path`; peers and optionals count as declared; a devDependency does not; a private name reports a finding unless in the caller's allowed list; a declared runtime dependency nothing imports reports a finding of its own kind; **only `dependencies` are held to that reverse half**; a dependency the caller marks as reached without a static import reports nothing; **empty output reports a finding rather than passing**; **a missing required specifier reports a finding**. - -Then implement `checkImportPurity({ label, output, manifest, allowedPrivate, allowedUnimported, requiredSpecifiers })`. The swept output is **injected** rather than read from a path inside the check, which is what lets 3a reuse the same function against an extracted tarball, and lets every case above be a plain value with nothing mocked. - -**Acceptance:** both suites pass; run against the real `packages/cli/dist` and `packages/cli-engine/dist` it returns no findings. **Met:** 22 tests in the checker's own suite, plus one test in each consumer package running the real built output with `requiredSpecifiers` set, so a run that swept the wrong directory fails instead of reporting a clean sweep. - -## D2 — check 2 (validator no-throw) — DONE - -Tests first: - -1. `validator-no-throw.test.ts` — a validator that throws unconditionally reports a finding naming the section and the provoking input; one that throws only on the `Proxy` trap case reports one finding, proving the corpus reaches that path; a malformed return reports a finding of its own kind; an empty section list reports a finding; composer's real `composerSection` reports none. -2. The corpus is asserted to contain each documented case, so a later edit cannot quietly shrink it. - -Then implement `checkValidatorNoThrow({ sections })` over the corpus the contract fixes. - -**Subject derivation.** `sectionsFrom({ families, commands })` builds the union the engine uses, per contract §3: family `configSection`s plus every mounted command's `needs.config`, matching `packages/cli-engine/src/execution/engine.ts:740-751`. Both inputs are already exported from `packages/cli/src/v8/cli.ts` — `mountedCommands` at 170, the families at 74 and 136 — and are reached from the shell's OWN test suite by relative import of `../src/v8/cli`, the convention that suite already uses. `sectionsFrom`'s parameter type is structural, so the checker needs no dependency on the engine and a test can pass toy families and commands without building one. Under STOP-8(a) the checker re-derives the union and the drift risk is recorded. - -**Acceptance:** the suite passes; run against the shell's sections it returns no findings. **Met:** 9 tests in the checker's suite, plus `packages/cli/tests/v8-conformance.test.ts`, which pins that the shell mounts exactly `["composer"]` — so the day the ORM slice adds a second section that test fails and the new validator gets checked rather than silently skipped — and that composer's shipped validator survives all 21 hostile inputs. - -## D3 — check 3 (tarball verification) — DONE - -Add `export const commandFamilies: readonly CommandFamily[] = [platformCommandFamily, composerCommandFamily]` to `packages/cli/src/v8/cli.ts`, used by `buildCli()`. This serves **check 2**, whose subjects are section objects the families carry. It does **not** serve 3c: `CommandFamily` carries `configSection`, `commands`, `docsBaseUrl` and `redirects` and no package identity at all (`packages/cli-engine/src/command-family.ts:49-59`), so 3c cannot learn a family's package name from it. 3c's family-package list is therefore hand-written in `bin.ts` alongside the shell's own imports, and a test asserts every name in it appears in the shell's packed manifest `dependencies` — which is what keeps it in step, since a family the shell mounts must be a package the shell depends on. - -Tests first, all with injected seams: - -1. `tarball.test.ts` — 3a: a packed manifest not covering the packed output's imports reports a finding. 3b: two `bin` entries are both started, a non-zero exit from either reports a finding naming that bin, and a failing install reports a finding carrying the installer's output rather than throwing. 3c: differing pins report a finding naming both versions and both packages; equal pins report none; a family not depending on the engine reports none; an installed family version differing from the shell's declared pin reports a finding; more than one engine copy in the install tree reports a finding; a non-exact packed engine pin reports a finding. -2. The exception list: keyed on the observed triple (family package, its engine pin, the shell's engine pin), it suppresses that exact combination and nothing else. A test proves the same family arriving at a *third* version is still reported. -3. One test asserts 3c reports the live shell-versus-composer mismatch when the exception list is empty. That is the test whose failure, once composer republishes and the exception is deleted, means the real defect is being caught. - -Then implement `checkTarball({ packages, familyPackages, exceptions, io })` against this seam, spelled out so no implementation improvises it: - -```ts -export interface TarballIo { - pack(pkgDir: string, destDir: string): Promise<{ tarball: string } | { failed: string }>; - readPackedManifest(tarball: string): Promise; - /** Path → source, .js/.mjs only, so 3a can reuse check 1 unchanged. */ - readPackedFiles(tarball: string): Promise>; - installSandbox(input: { - sandboxDir: string; - rootTarball: string; - overrides: Readonly>; // version-qualified name → "file:" - }): Promise<{ ok: true } | { ok: false; output: string }>; - readInstalledManifest(sandboxDir: string, name: string): Promise; - startBin(input: { - sandboxDir: string; - binName: string; - relPath: string; - argv: readonly string[]; // ["--version"] — not a bare start, which prints help and may exit non-zero - timeoutMs: number; - }): Promise<{ exitCode: number | null; stdout: string; stderr: string; timedOut: boolean }>; -} -``` - -The default `io` packs with `pnpm pack`, computes the override map (each packed dependency matching a workspace package → that package's tarball, recursing into its own workspace dependencies), and installs with `npm install --no-audit --no-fund --ignore-scripts`. `--ignore-scripts` is required, not tidiness: without it the install runs `esbuild`, `workerd` and `msgpackr-extract` postinstalls, two of which `pnpm-workspace.yaml:5-11` deliberately disables, on a runner holding `id-token: write`. Verdicts key on exit codes only, never on stderr content — the install emits real `EBADENGINE` warnings. - -The sandbox lives at a gitignored in-repo path carrying the package name, and is deleted at the **start** of a run rather than the end, so a failure leaves it for inspection instead of leaking a temp directory. Per STOP-5 that path needs `COREPACK_ENABLE_STRICT=0` for the install, because corepack's npm shim walks up to the repo root, sees `"packageManager": "pnpm"`, and refuses. - -**Acceptance:** the suite passes; `check:conformance` locally packs both published packages, installs out of the workspace, starts every declared bin plus `dist/v8/cli.js` at exit 0 (per STOP-6(b)), reports no 3a findings, and reports the composer mismatch as suppressed by a named exception with its reason printed. - -## D4 — wire prisma-cli's publish path, and the records — DONE - -- `.github/workflows/publish.yml`: a `Run conformance checks` step calling `pnpm check:conformance`, after `Run script tests` and before both publish steps so it guards the dry-run and the real publish alike. It still carries `if: ${{ steps.version.outputs.publish == 'true' }}`, matching both its neighbours at lines 103-109 — the turbo task removes the *build-ordering* need for a condition, not the reason to skip the work on a push that publishes nothing. -- `.github/workflows/pr-quality.yml`: the same script in the Test job. No explicit `pnpm build` step is needed once `conformance` is a turbo task with `dependsOn: ["^build"]`; that is the whole reason for making it one. If the install proves too slow for pull requests, the fast checks run there and the full set only at publish, decided by measurement. -- Root `package.json`: `"check:conformance": "turbo run conformance"`; `turbo.json`: the `conformance` task. -- `specs/s6-conformance.md`: acceptance boxes ticked with evidence. -- `plan.md` §S6: what shipped, and what S5 must wire in prisma/prisma. -- `spec.md`'s DoD line: **left as written and unchecked**, with the S5 and composer dependencies recorded against it. Revision 1 planned to reword it to match what shipped; that turns an unmet requirement into a met one by editing the requirement. -- `deferred.md`: the composer engine-pin exception, keyed on the observed triple, with the condition for deleting it. - -**Acceptance:** `pnpm typecheck`, root `pnpm lint` and the touched suites green, each measured as pnpm's own exit code; `check:conformance` green locally. - -## D5 — prisma/prisma's publish path — DONE (prisma/prisma#29998) - -prisma/prisma has landed S5 and has real subjects for all three checks: the `orm` section validator (`packages/1-framework/3-tooling/cli/src/orm/config-section.ts`), the published `@prisma/orm-toolchain` built output, and the engine pin `0.0.9` in three manifests. Following that repo's own conventions (`scripts/*.mjs` + `node --test`, wired into `publish.yml` alongside `check:publish-deps`), add the checks there in a separate PR from the bot. Push access confirmed 2026-08-12. - -## Verification per dispatch - -The touched packages' suites, `pnpm typecheck`, and root `pnpm lint`, each measured as pnpm's own exit code. **No stashing of `wip/` is needed.** Revision 1 carried a `mv wip /tmp/...` dance inherited from the S5 brief; biome already honours `.gitignore` (`biome.jsonc` sets `vcs.useIgnoreFile: true`, and `.gitignore:38` lists `wip/`), so it reports those paths as ignored and the dance does nothing — while writing to a temp directory, which the operator's standing rule forbids. Run the commands directly. - -Four lint rules will bite and are worth expecting rather than discovering: `performance/noAwaitInLoops` (an error here, with 37 existing suppressions in the repo — the tarball check is inherently a sequential loop of awaits, and the repo's convention is a `biome-ignore` with a reason), `performance/useTopLevelRegex` (11 existing suppressions; hoist any regex to module scope), `complexity/useLiteralKeys` (use property access, not `manifest["dependencies"]`), and `complexity/noExcessiveCognitiveComplexity` (split the pack/install/start orchestration rather than fight it). Do not add a barrel `src/index.ts`: `performance/noBarrelFile` is on and the only sanctioned exceptions are tsdown entrypoints. - -## Risks - -- **The sandbox install needs the network**, resolving ~440 packages. A registry outage fails the publish path for a reason that is not a defect, so the install failure is its own finding kind carrying the installer's output — legible rather than looking like a broken tarball. -- **Cold install cost**: 37 s cold, 12–13 s warm. Acceptable in a publish path; measured before deciding whether it belongs on every pull request. -- **The exception list is a place for problems to be parked.** Each entry carries a reason and its removal condition, it is keyed tightly enough that any change on either side reopens the finding, and D3's test proves an empty list catches the real defect. -- **Check 1's flat sweep cannot see a missing subpath export.** `@prisma/composer/family` must exist for the shell to work and no check here would notice it vanishing. Recorded in the contract, not solved by this slice. diff --git a/.drive/projects/prisma-cli-v8/plans/s7-release.md b/.drive/projects/prisma-cli-v8/plans/s7-release.md deleted file mode 100644 index 137b1269..00000000 --- a/.drive/projects/prisma-cli-v8/plans/s7-release.md +++ /dev/null @@ -1,112 +0,0 @@ -# S7 dispatch plan — Release pipeline + rc1 (revision 3) - -Contract: `../specs/s7-release.md` rev 1. One repo (prisma-cli), branch -`claude/s7-release-pipeline-rc1-92c89d`, base `main`. Implementers on -Opus, reviewers on Opus-4.8-mid. Standing process rules as in the S2/S3 -plans: tests before implementation, no `vi.mock`/`vi.doMock`, pnpm only -(tarball smoke's sandbox npm install excepted, as ruled in S6), explicit -staging, bot identity with dual sign-off, push to the bot remote only. - -Rulings applied (2026-08-12): the goal is all available commands in one -binary; rc1 publishes under the current names (`@prisma/cli`, bin -`prisma-cli`); the bare-`prisma` cutover and the exception-list -reconciliation are follow-up work. STOP-2/3/4/8 closed. **All STOPs closed -2026-08-12; D1–D6 shipped on PR #164.** - -Ordering: D1 → D2 are independent of the release machinery and can run -while STOP-5/7/8 settle; D3 → D4 → D5 are strictly ordered (the package -must exist before the automation covers it, the automation before the -pipeline verifies it); D6 closes. One PR for the slice. - -## D1 — Mount the ORM family (packages/cli) - -Tests first: extend `v8-mount-coverage.test.ts` (fails until the mount -lands — the 21 expected paths, `ormCommandFamily` in -`MOUNTED_FAMILIES`); a `v8-bin` semantic test running one ORM command -end to end through `createTestCli` (`migration list` against a fixture -project directory: envelope, presented rows, exit 0); a redirect test -(`migration apply` settles as the typed redirect, exit per engine); a -`--help` test naming the `contract`, `db`, `migration`, `ref` groups. -Then: the `@prisma/orm-toolchain` dependency at the STOP-7(ii) interim -exact version; `cli.ts` imports the family from -`@prisma/orm-toolchain/cli`, spreads its commands, adds the four group -briefs. Watch for: the family keys are full mount paths already — no -renaming layer; config-section and redirects ride the family object. -Divergence file `assets/s2/parity-divergences-s7.md` opened (expected -content: "none"; plus the deferred.md entry for the static-import cost). - -## D2 — The completeness check fails the build (repo root + CI) - -Tests first: a fixture-level test proving the check reports (a) a -family command absent from the tree, (b) a mounted command owned by no -family and not excepted — both via a constructed family/tree pair, not -by mutating the real mount. Then: `check:grammar` as a turbo task -(`dependsOn: ["^build"]`, `cache: false`) running the mount-coverage -suite file; wired into `pr-quality.yml` and `publish.yml` before the -first publish step under its `publish == 'true'` condition. The -exception list gets a doc comment naming STOP-4's ratification and the -rule that additions require an operator ruling. -Reshaped by: STOP-4 (if utilities move into the platform family, the -exception set shrinks to the telemetry trio). - -## D3 — The shipped bin becomes the v8 tree (packages/cli) - -Per the 2026-08-12 ruling: no `prisma` package. Tests first: a -packaging test asserting the DECLARED bin (`package.json` `bin` -entry read, not a hard-coded path) is the v8 entry and that running -it with `--version` on plain Node in a bare env prints the lockstep -version at exit 0. Then: flip `bin.prisma-cli` from `./dist/cli.js` -to `./dist/v8/cli.js`. The legacy entry keeps building and shipping -in the tarball (S2d owns its deletion). Nothing else changes. - -## D4 — Committed versions + conformance wiring (manifests + CI) - -RESOLVED 2026-08-12: STOP-5(b) ruled — the inline smoke path below -shipped; STOP-7 deferred until `8.0.0-rc.1` publishes. The STOP-5(a) -branch is kept only as the record of the road not taken: -`packages/cli` pins `@prisma/composer` and `@prisma/orm-toolchain` -exact (already the style; versions per STOP-7); `pnpm conformance` -added to `publish.yml` before publish steps; the S6-3c interim -exception entries (dated triples, one per family) committed if the -pins have not converged by then. -If STOP-5(b): D4 instead implements the inline smoke per the contract -(S6 3b mechanics, ~40 lines, written to S6's spec so absorption is a -move), and the 3c pin comparison is NOT built here — the pins' exactness -is still asserted by the existing manifest style plus D5's install -smoke resolving a single engine copy. - -## D5 — The pipeline (publish.yml + scripts) - -Blocked by: STOP-1. Written against 1(a): -Tests first where testable: the override-computation helper (workspace -package → packed tarball map, recursive) as a pure function with its -own unit tests in `scripts/`; `determine-version` untouched (nothing -dynamic added). Then, in `publish.yml`: pack stage (engine + cli -tarballs via `pnpm pack` — order matters, packing rebuilds dist per -S6's finding 16, so the grammar check and conformance run before -packing); out-of-workspace install smoke (npm, `--ignore-scripts`, -absolute `file:` overrides, every declared bin from every packed -manifest started with `--version` under a timeout, exit 0 required — -after D3 the declared bin IS the v8 tree, so the smoke exercises the -composer and ORM family boundaries); tarball upload as workflow -artifacts; Release assets attached in the existing Release step. -Dry-run dispatch path covers pack + smoke + upload, skipping registry -writes and Release — this is the verification surface for the whole -slice (never a real publish from this work; a real publish is the -operator's action). - -## D6 — Docs, records, close-out prep - -`docs/oss/versioning.md` (the `prisma` package, the artifact stage, the -guarded publish); `rollout-plan.md` step 4 pointed at the pipeline; -`plan.md` §S7 updated; `deferred.md`: close the two-copy-install entry -when STOP-7 convergence lands, add the ORM import-weight entry; PR -description per the ruled structure (grounding example first, -alternatives last). Slice review loop (architect + principal-engineer -personas), findings folded, operator walkthrough. - -Completeness: D1 → the tree; D2 → the check that guards it; D3 → the -package rc1 ships as; D4 → the pins and their verification; D5 → the -automated path from release commit to verified artifact; D6 → the -records. Together: the operator merges one bump PR and rc1's artifacts -exist, verified, published where the registry allows. diff --git a/.drive/projects/prisma-cli-v8/plans/s8-services.md b/.drive/projects/prisma-cli-v8/plans/s8-services.md deleted file mode 100644 index 91d33ea6..00000000 --- a/.drive/projects/prisma-cli-v8/plans/s8-services.md +++ /dev/null @@ -1,102 +0,0 @@ -# S8 dispatch plan — service primitives - -Contract: `../specs/s8-services.md`. Branch `s8-service-primitives` -off `main`. One PR. Standing rules as in prior plans; the S2b/S2c -command template governs new commands. - -Grounding: commands register flat with dotted names in -`packages/cli/src/v8/cli.ts` (`"service domain add"` proves the -subgroup mechanism — a group brief entry plus dotted command keys); -per-command tests are `packages/cli/tests/v8-service-*.test.ts` over -`v8-service-testkit.ts`; the API surface is -`packages/cli/src/lib/app/app-provider.ts`. - -## D1 — the `deployment` subgroup + presenter corrections - -Outcome: the four renamed commands exist ONLY under -`service deployment` (`list|show|promote|rollback`), and the two -R-S8-3 corrections are in: `deployment show`'s `url` is the promoted -`appEndpointDomain`, and `live` derives from `latestDeploymentId` -alone (the `readKnownLiveDeployment` local-state fallback deleted, -`target.ts:400-448`). Old spellings, ids, help, presenters, test -names all move; nothing answers to `list-deploys`/`show-deploy` or -top-level `promote`/`rollback`. Includes the contract's -placeholder-domain edge case: `service show` presents `live url` -only when a live deployment exists. - -Builds on: main. Hands to D2: the subgroup mounted and green — the -registration pattern and corrected provider mappings D2's new -commands sit beside. - -Completed when: renamed suites green; a grep for the old spellings -in `src/v8` and help output returns nothing; corrected `url`/`live` -pinned by test. - -## D2 — `service list` + `service create` - -Outcome: services can be enumerated and born without deploying. -`service list` on `GET /v1/apps`; `service create` on -`POST /v1/apps` with exactly the four create-body fields (name, -project, optional region, optional branch). Provider additions in -`app-provider.ts`; presenters in the established style; `create` -output respects the placeholder-domain edge case (no dead URL -presented as live). - -Builds on: D1 (corrected provider mappings). Hands to D3: the -provider's deployment-record plumbing untouched and stable. - -Completed when: both commands green through the harness; `create` -proven against the real API in the e2e suite including no-region / -no-branch defaults. - -## D3 — deployment lifecycle: `start` / `stop` / `delete` - -Outcome: `service deployment start|stop|delete` exist per R-S8-2 — -result commands on `POST /v1/deployments/{id}/start|stop` and -`DELETE /v1/deployments/{id}`; consent prompt on `delete` per the -`service remove` precedent; the artifact-not-uploaded failure on -`start` maps to a structured error. Carries the contract's -STOP-and-surface: verify the API's behavior deleting the -currently-promoted deployment before shaping the error path. - -Builds on: D1 (subgroup), D2's untouched deployment plumbing. -Hands to D4: the full S8 grammar in place. - -Completed when: three commands green; consent path tested; the -delete-live-deployment behavior recorded (or surfaced as a STOP). - -## D4 — closure - -Outcome: the records match the shipped surface. Divergence file -`assets/s2/parity-divergences-s8.md` (four renames, deleted -spellings, `live` derivation, `url` change; from D1's review round: -`service deployment list` rows report `live: null` — not `false` — -when the service names no live deployment, a JSON-only change; and -`service show` suppresses `liveUrl` when the named live deployment -is missing from the listing, narrower than "present when -`latestDeploymentId` is set"; the retired local live-state writes); -finding D1-R2-1 (v8-service-remove.test.ts's live-state clearing -assertion is vacuous — seed the key or drop the clause, reviewer's -entry has both options); from D2's review round: `service create ---branch` resolves-or-creates the named branch — a typo silently -creates a branch — inherited from the provider path the contract -grounds the command in, recorded as a divergence note not a defect; finding D2-R2-1 -(e2e-coverage.test.ts's rewritten backlog comment overclaims — -narrow it to the entries that need a deployed service, or write -`service show`'s e2e via `service create` and drop its entry); -from D3: deleting the live deployment leaves the service with a -non-resolving `endpointDomain` (server clears `latestDeploymentId` -but not the domain) — presenters already guard on -`latestDeploymentId`, divergence note only; from D3's review round: -`stop` takes production offline with no consent while `delete` -demands a typed token — contract-faithful, but say so in the -divergence file now that the grammar puts the verbs side by side; `deferred.md` gains the -ownership-note revisit (R-S8-4) and the logs follow-up slice -(R-S8-5); golden-rendering updates; review loop; PR. - -Builds on: D1–D3. Hands to: slice-DoD — completeness check against -the contract's acceptance list. - -Completed when: acceptance list satisfiable line by line; suites -green sequentially (`@prisma/cli-engine` before `@prisma/cli`); -typecheck + lint exit 0. diff --git a/.drive/projects/prisma-cli-v8/plans/service-logs.md b/.drive/projects/prisma-cli-v8/plans/service-logs.md deleted file mode 100644 index 04d0a1e3..00000000 --- a/.drive/projects/prisma-cli-v8/plans/service-logs.md +++ /dev/null @@ -1,26 +0,0 @@ -# service logs dispatch plan - -Contract: `../specs/service-logs.md`. Branch `service-logs` off -`main`. One PR. - -## D1 — the command - -Outcome: `service logs` mounted and green per the contract — the -S2c resolution logic restored from `bot/s2c-services`, the transport -replaced with the page-read GET, page and follow modes, the full -test matrix on fixtures and the injectable clock. STOP if the pinned -SDK's `query: never` blocks typecheck without a cast. - -Builds on: main. Hands to D2: the command green, any SDK blocker -named. - -Completed when: contract acceptance items 1–3; gate green -(engine suite, cli suite, typecheck, lint — sequential, exit 0). - -## D2 — closure - -Outcome: divergence entry, `deferred.md`'s logs entry closes, e2e -backlog entry, review loop, PR. Reconcile with records PR #176's -edit of the same `deferred.md` entry if it has merged. - -Completed when: acceptance items 4–6; gate green. diff --git a/.drive/projects/prisma-cli-v8/reviews/code-review-s2c.md b/.drive/projects/prisma-cli-v8/reviews/code-review-s2c.md deleted file mode 100644 index 60221fe7..00000000 --- a/.drive/projects/prisma-cli-v8/reviews/code-review-s2c.md +++ /dev/null @@ -1,1439 +0,0 @@ -# S2c code review — services slice - -Slice: s2c-services (contract `../specs/s2c-services.md`, plan `../plans/s2c-services.md`). -Branch `s2c-services` off `s2b-resources`. Reviewer-maintained except the -sections marked orchestrator-owned. - -## Subagent IDs (orchestrator-owned) - -- Implementer: (not yet spawned) -- Reviewer: (not yet spawned) - -## AC scoreboard - -| Acceptance criterion | Status | -| --- | --- | -**The acceptance criteria below are the contract's, written for a -twenty-command slice. Three commands were removed late by operator -ruling — `service deploy` and `service build` dropped, `service logs` -shelved — so two criteria now describe work that is deliberately not -here. The rows say so rather than being quietly reworded; S2d -consolidates the contract.** - -| Acceptance criterion | Status | -| --- | --- | -| All in-scope commands mounted, green on R-S2b-9 matrix | met for the slice as ruled — **17 mounted**, matrix complete per command. The contract's headline "24" counted the four `service env` commands its own scope note then moved to S2b, giving 20; the operator then dropped `service deploy` and `service build`, shelved `service logs`, and `service run` was already ruled dropped | -| `service` rename complete; no `app` path in v8 | met | -| Deploy/promote/rollback/remove event sequences pinned | met for what ships — remove pins first/second/last; promote and rollback bracket the SDK transitions. The deploy phase sequence was pinned and is **gone with the command**; this criterion cannot be met in full and does not need to be | -| Divergence file complete | met, verified against the code by the final review: 17 commands, the ten removed next actions, the shelve-versus-drop distinction, and the workspace-from-session entry all checked rather than trusted | -| Q2 ruled+implemented or parked with legacy intact | met — `service run` ruled dropped and recorded, with no exit-code passthrough mechanism and no S2d carve-out needed | -| Legacy fixture tests for ported commands deleted | **deliberately not done — operator decision needed at PR time.** The commander shell still serves these commands until S2d deletes it; removing its tests now leaves live code uncovered for the whole interval. Recommendation: delete nothing here, delete the shell and its tests together. Accepted by every reviewer | -| Root verification green; PR ≥1k LOC; review loop run | green at the tip: **965 cli tests**, 234 engine tests, typecheck, lint. LOC floor cleared many times over. Review loop run — four dispatch reviews, an architect and a principal-engineer pass over the whole slice, a verification round, and a final pass after the removals | - -### Finding status (orchestrator-owned) - -| Round | Findings | Status | -| --- | --- | --- | -| D1 round 1 | F1, F2, F3 | closed in `c7c10de` | -| D2 round 1 | F1–F5 | closed in `040c750`, `55bb185`, `a0b0ea6` | -| D3 round 1 | F1–F5 | closed in `8e3b181` + `f7b3635`; **independently verified closed by the slice review**, not accepted from the fix report. F1 was closed per the orchestrator adjudication under it, not per the finding's own conclusion | -| D4 round 1 | F1, F2 | closed in `f7b3635`; independently verified closed by the slice review | -| Slice review | ENG-F1–F4, ARCH-F1–F4 | closed in `ed7151e` + `0459208`; **independently verified closed** by the round-2 verification pass, which re-counted the affected commands itself and proved the new deploy coverage by mutating `app-provider.ts` and watching the suite fail. ENG-F1 was record-only here: the code fix belongs to the S2a stream (see the merge-down note) | -| Slice review round 2 | R2-F1 | closed in `6458ad9` — a cancellation regression the ARCH-F1 fix introduced. Recorded as deliberately untested; see the finding. **Moot since: it was in `service deploy`, which the operator then dropped** | -| Final review (post-removal) | FINAL-F1, F2, F3 | closed in `6d80563`. F1 restored coverage of `renameAppCopy`, `fromLegacyCliError` and the compute-config path, which the three deleted suites took with them although all three still run on the surviving commands; each mechanism was verified by breaking it and watching the right tests fail. F3 (the stale scoreboard) was closed by the orchestrator | -| Merge review (rev-6 credential surface) | MERGE-F1–F5 | closed in `528f925`. F1 is recorded rather than fixed, by orchestrator ruling — see below. F2's test pins the refusal against the product's own claim derivation, so the harness cannot drift from the product silently. F3 removed the spread that let the rev-6 seed rename be ignored by 104 tests instead of failing once | - -**All findings raised in this slice are closed.** Two items remain open, -and neither is a finding against this branch: - -- **The merge-down credential defect belonged to the S2a stream**, and is - now moot for us — `requireWorkspace` reads the engine instead of the - legacy file reader, so the format change cannot reach these commands. - It still affects anything else calling `readAuthState`. -- **Identity has no server fallback** (MERGE-F1). The shipping CLI asked - `/v1/me` first and used the workspace the server named; the engine's - credential accessor is local-only by design, so a service token the - platform can place but whose claims cannot is refused. Fixing it inside - a command would repeat the mistake that caused the defect above, so it - is recorded as an engine question with a ruling request. **This is the - one open item that needs an answer before the slice is finished - business.** - -## Findings log - -### D1-R1-F1 — R-S2b-9 matrix has per-command axis gaps (should-fix) — CLOSED in c7c10de - -`packages/cli/tests/v8-service-domain.test.ts:204-293` (show, retry), -`:295-395` (remove). - -R-S2b-9 requires every command × (success, errored, json envelope, -unauthenticated, consent, picker where applicable). Two axes are -missing on the domain commands: - -- **Unauthenticated**: covered for `service show`, `open`, - `list-deploys`, `show-deploy`, `domain add`, `domain wait`. Missing - for `domain show`, `domain remove`, `domain retry` — the three - commands whose `needs.credentials` declaration nothing pins. -- **Successful json envelope**: none of the five `domain` commands - assert a completed envelope with its `commandId` and `result`. They - assert errored envelopes only, so `service.domain.*` command ids and - the success `result` payload are unpinned on the json surface. - (`show`, `list-deploys`, `show-deploy`, `build` all have this.) - -Picker coverage exists once, on `service show`, through the shared -`resolveExistingServiceSelection`; that is acceptable — do not -duplicate it per command. - -### D1-R1-F2 — the rename leaks in error prose that flows through `renameAppCopy` (should-fix) — CLOSED in c7c10de - -`packages/cli/src/v8/service/errors.ts:34-39`. - -`renameAppCopy` is a three-entry substitution list, so ported copy that -spells the noun differently survives unrenamed. The visible case is -`ComputeConfigTargetRequiredError` -(`packages/cli/src/lib/app/compute-config.ts:254-268`): the summary -"App target required" IS renamed to "Service target required", but its -`fix` — "Pass the app target, for example `prisma-cli service build -`." — still says "app target". One error message now names the -same thing two ways. This fires on every multi-target -`prisma.compute.ts` for `build`, `show`, `open`, `list-deploys`, and -every `domain` command, so it is a routine path, and R-S2c-1 puts -error copy explicitly inside the rename surface. - -`ComputeConfigTargetUnknownError`'s fix ("Remove the target argument; -this config defines a single app.") has the same shape but arguably -refers to the SDK-owned `app:` config key that deliberately does not -rename. Decide it deliberately and say which way in the divergence -file; do not leave it as an accident of the substitution list. - -### D1-R1-F3 — three real differences from the shipping CLI are not in the divergence file (low) — CLOSED in c7c10de - -`.drive/projects/prisma-cli-v8/assets/s2/parity-divergences-s2c.md`. - -The file is otherwise unusually complete and accurate (the json-mode -per-poll wait events, the `--yes` consent contradiction, the fixture -refusal, the list-serializer wrapper are all correctly recorded). -Missing: - -1. **`service open` loses a next action.** Legacy returned - `["prisma-cli app show", "prisma-cli app show-deploy "]` - (`packages/cli/src/controllers/app.ts:1291-1294`); v8 returns only - "Inspect the service" - (`packages/cli/src/v8/service/presentation.ts:208`). Either restore - the deployment action or record the drop. -2. **The domain result's `branch.id` field is gone.** Legacy's - `toResultBranch` emitted `{id, name, kind}` - (`packages/cli/src/controllers/app.ts:3721-3729`); - `ServiceDomainTarget.branch` is `{name, kind}` - (`packages/cli/src/v8/service/results.ts:77-80`). The value was - always `null` for domain commands, so this is a json result-shape - change, not a behavior change — but the "Result shape changes" - section is where it belongs. -3. **Ported `fix` prose is silently dropped when a legacy error - carries `nextActions`.** `fromLegacyCliError` - (`packages/cli/src/v8/service/errors.ts:47-63`) takes the - nextActions branch OR the fix/nextSteps branch, never both. The one - reachable error with `nextActions` here is `PROJECT_SETUP_REQUIRED` - (`packages/cli/src/lib/project/resolution.ts:411-428`), whose fix - ("Link the directory to an existing Project, or pass --project - for this command.") therefore disappears from the - unlinked-directory error. Either carry the advice through alongside - the actions or record the loss. - -### D2-R1-F1 — the consent section documents the ruled end state as though it ships (should-fix) — CLOSED in a0b0ea6 + 040c750 - -`parity-divergences-s2c.md:210-224` (D2 consent table) with -`:103-108` (the D1 "SUPERSEDED" note). - -The code is right: `service remove` -(`packages/cli/src/v8/service/remove.ts:86-91`) and `service deploy`'s -production consent (`packages/cli/src/v8/service/deploy.ts:278-283`) are -plain `prompt.consent` with no skip flag, per the ruling, and D1's -boolean `--confirm` on `service domain remove` -(`packages/cli/src/v8/service/domain-remove.ts:22-28`) stays until the -merge-down, per the orchestrator note below. **This finding asks for no -code change.** - -The record is what is wrong. The D2 table's "v8 grant (ruled)" column -lists `prompt.consent` **or global `--confirm `** for all three -commands, and the D1 section is headed "SUPERSEDED by the dispatch-2 -consent section below". Read together, they describe a group where all -three consent points accept `--confirm `. What actually ships -today is three different answers: `service remove` and `service deploy ---prod` cannot be granted non-interactively at all, and `service domain -remove` takes D1's boolean `--confirm`. An operator reading this list to -judge parity gets the end state, not the shipped state, with nothing -marking the difference. - -Split the table into "ships today" and "ruled end state (arrives with -the engine consent mechanism)", and soften the D1 heading from -SUPERSEDED to "superseded on merge-down; the boolean still ships". - -### D2-R1-F2 — the Next.js standalone-output hint loses the one line that tells the user what to change (should-fix) — CLOSED in 55bb185 - -`packages/cli/src/v8/service/errors.ts:961-976`. - -Legacy paired the `edit-file` action with a concrete fix string: `Add -output: "standalone" to next.config.*, then rerun deploy.` -(`packages/cli/src/controllers/app.ts:4721-4723`). The port keeps the -action and drops the string. The engine's `NextAction` has no path or -instruction field (`packages/cli-engine/src/protocol.ts:27-33`), so all -that survives is the label "Add Next.js standalone output" and a `reason` -explaining why Compute needs it — the user is told what outcome to reach -and never told that it is `output: "standalone"` in `next.config.*`. - -The command inventory calls this hint out by name for `app deploy` -("Next standalone-output hint with edit-file nextAction"), and the -detection predicate is otherwise a faithful copy. Carry the instruction -through as an `adviceAction`, or fold it into the action's `reason`. - -The test only asserts that some action has `kind: "edit-file"` -(`packages/cli/tests/v8-service-deploy.test.ts:585-589`), which is why -the dropped text went unnoticed; pin the instruction text too. - -### D2-R1-F3 — ported advice points at a command that does not exist in v8 (should-fix) — CLOSED in 55bb185 - -`packages/cli/src/v8/service/branch-database.ts:251`. - -The post-provisioning advice says "Get a connection URL with -`prisma-cli database connection create `", copied verbatim from -legacy (`packages/cli/src/lib/app/branch-database-deploy.ts:216`). R-S2b-1 -renames that group to `postgres` with no alias, so in the v8 tree the -command is `postgres connection create` and the suggested one does not -exist. Every successful `service deploy --db` prints this. - -### D2-R1-F4 — a divergence row states legacy behavior that legacy did not have, and hides a second engine gap (low) — CLOSED in a0b0ea6 - -`parity-divergences-s2c.md:253` and -`packages/cli/src/v8/service/deploy-target.ts:425-432`. - -The row reads "`USAGE_ERROR` (2) — invalid Project name at the setup -prompt → `SERVICE.PROJECT_NAME_INVALID` (2)", which presents this as a -straight code rename. Legacy did not error: it passed `validate` to the -clack text prompt -(`packages/cli/src/lib/project/interactive-setup.ts:87-95`, -`packages/cli/src/shell/prompt.ts:44-59`), so a bad Project name was -re-asked in place and the deploy continued. The engine's `prompt.text` -takes only `placeholder` and `default` -(`packages/cli-engine/src/context.ts:110-113`), so the port validates -after the fact and fails the whole command — a user who typos a Project -name during first-deploy setup now loses the run. - -This is the same class as the `--db` tri-state gap, which the file -records honestly and flags for the operator. Do the same here: state the -real legacy behavior, and name the missing engine affordance (a prompt -validator / re-ask) so the operator sees both gaps together. - -### D2-R1-F5 — dead export (low) — CLOSED in 55bb185 - -`packages/cli/src/v8/service/errors.ts:789`. - -`serviceAmbiguousError` is never referenced in `src/` or `tests/`. The -ambiguous-service path goes to the engine prompt instead -(`packages/cli/src/v8/service/deploy-target.ts:596`), which is correct and -recorded as a divergence — this builder is the abandoned first approach. -Commit fb6bc32 swept dead re-exports out of `deploy-target.ts` and missed -this one. - -### D3-R1-F1 — the log-stream credential gap is recorded backwards: the shipping runtime does resolve a token (should-fix) - -`.drive/projects/prisma-cli-v8/assets/s2/parity-divergences-s2c.md:416-439`, -`packages/cli/src/v8/service/errors.ts:231-233`, -`packages/cli/tests/v8-service-logs.test.ts:427-450`. - -The divergence entry says `ctx.getCredentials()` "is the manager-less -fallback and resolves `undefined` whenever a credential manager is wired -— which is the shipping runtime", and concludes that `service logs` -"reports a clear error under the credential manager". The code does -something else. `ctx.getCredentials()` forwards straight to -`runtime.getCredentials()` and never looks at the manager -(`packages/cli-engine/src/execution/command-context.ts:96-97`). The -shipping runtime wires `getCredentials: makeGetCredentials(proc.env)` -(`packages/cli/src/v8/runtime.ts:95`), which returns -`PRISMA_SERVICE_TOKEN` when it is set and otherwise the access token -`FileTokenStorage` holds (`packages/cli/src/auth/credentials.ts:11-22`) -— the same two sources, in the same order, that legacy's -`createPreviewLogAuthOptions` used -(`packages/cli/src/controllers/app.ts:3284-3306`). `auth login` writes -the token to exactly that place (`packages/cli/src/v8/auth/login.ts:75` -→ `storeLegacyCredential` → `FileTokenStorage.setTokens`, -`packages/cli/src/auth/operations.ts:114-131`), and nothing in the CLI -calls `credentialManager.createSession`, so a signed-in user's token is -where `getCredentials` looks for it. - -So the shipped command streams under the real runtime, and -`SERVICE.LOG_STREAM_CREDENTIALS_UNAVAILABLE` is not reachable in -production today. What makes it fire in the tests is a harness -constraint rather than the product: `createTestCli` rejects the -`credentials` seed combined with any credential-manager seed -(`packages/cli-engine/src/testing.ts:130-134`) and defines -`getCredentials: async () => spec.credentials` (`:214`), so seeding a -manager forces `getCredentials` to resolve `undefined` — a runtime -shape the bin never assembles, because it wires both. The test at -`:427-450` therefore pins the harness, and its comment ("Under the -credential manager — the shipping path — ctx.getCredentials() resolves -nothing") repeats the same wrong fact. The comment on the error builder -(`errors.ts:231-233`) states the opposite wrong fact: "needs.credentials -makes this unreachable in practice". - -The escalation is still worth putting to the operator — `getCredentials` -is documented as staged for deletion -(`packages/cli-engine/src/context.ts:44-48`), the surviving accessors -expose no token, and a session that lives only in the credential -manager's own state file would not be found by `FileTokenStorage`. This -finding asks for none of that to be built. It asks that the three places -describing today's behavior describe today's behavior, so the operator -rules on the real situation: the command ships working, on an accessor -that is scheduled to disappear. - -**Orchestrator adjudication (2026-08-10): the finding is half right, and -the correction it asks for would itself be wrong within one merge-down. -Measured, not reasoned — both credential surfaces resolve to the same -file path by default, and the file's shape decides the answer:** - -- **On this branch today the reviewer is right.** `auth login` still - writes through `storeLegacyCredential` - (`packages/cli/src/v8/auth/login.ts:75`), which produces the legacy - `{tokens: […]}` shape. `ctx.getCredentials()` reads that shape and - returns a token, so `service logs` streams and - `SERVICE.LOG_STREAM_CREDENTIALS_UNAVAILABLE` is indeed unreachable. -- **On `bot/s2a-foundations`, which is our own base and which we merge - down next, it is already false.** There `auth login` calls - `ctx.credentialManager.createSession` - (`bot/s2a-foundations:packages/cli/src/v8/auth/login.ts:132`), which - writes `{version, sessions, currentWorkspaceId}`. - `@prisma/credentials-store` reads `data.tokens || []`, so that file - yields no credential: driving the manager's own API end to end, - `ctx.getCredentials()` flips from a token to `undefined` after a - single `createSession`, while `credentialManager.currentSession()` - still returns a valid session. - -**So the breakage is not hypothetical and not avoided — it is scheduled, -and it arrives with the next merge-down. `service logs` becomes the one -command that fails for a signed-in user whose other commands all work. -`PRISMA_SERVICE_TOKEN` users are unaffected either way, because that -path short-circuits ahead of the file read.** - -**Consequences for the fix round: the error builder and its test STAY — -they are about to become the live path, not dead code. What must change -is the wording in all three places the finding names, which today claims -the command is already broken and would tomorrow claim it already works. -Both are wrong. Each should state the trigger: the shape of the -credential file, flipped by the first `auth login` run after the auth -rework lands.** - -### D3-R1-F2 — `service logs --deployment ` stops honoring the compute-config service (should-fix) - -`packages/cli/src/v8/service/logs.ts:191-199` with -`parity-divergences-s2c.md:443-448`. - -`serviceNamed` counts only `--service` and the positional config target. -Legacy folded the config-selected app name in first — `appName = appName -?? compute.configAppName` (`packages/cli/src/controllers/app.ts:1583`) — -and only then chose between the scoped and the global lookup (`:1646`). -That name is still computed in v8 -(`packages/cli/src/v8/service/target.ts:178-190`), and -`selectComputeDeployTarget` returns the single target whenever the -config declares one, so in any directory holding a `prisma.compute.ts` -the name is present without the user typing anything. - -In a normal service directory, `prisma-cli service logs --deployment -` therefore behaves differently now. Legacy looked the id up inside -the configured service and refused a deployment belonging to a sibling -service (`Deployment "…" not found for app "…"`, -`controllers/app.ts:1669-1673`); v8 resolves the id globally, streams -the sibling's logs, and rewrites the remembered service selection to -that sibling (`logs.ts:90`). A config naming a service that no longer -exists used to fail with "Selected app does not exist in the resolved -project" (`controllers/app.ts:2961-2972`) and is now ignored. - -The divergence entry records the opposite — "Legacy resolved an explicit -deployment id globally when no app was named, and v8 keeps that." -Legacy's "named" included the config; v8's does not. Either feed the -config name into the decision (it cannot reintroduce the picker: an -explicit name never prompts) or record that the meaning of "named" -narrowed. - -### D3-R1-F3 — the json stream drops record fields legacy published, and `--cursor` loses its only source (should-fix) - -`packages/cli/src/v8/build/logs.ts:142-171` and `:202-207`, -`packages/cli/src/v8/service/logs.ts:210-221`, with -`parity-divergences-s2c.md:381-389`. - -1. **Per-record fields.** Legacy `build logs --json` published each - record whole (`data: record`, - `packages/cli/src/controllers/build.ts:87-95`), so every framed line - carried `cursor`, `level`, `source` and `step`. The port keeps the - text, the channel and `step`, and emits nothing at all for a terminal - record whose code is `end` (`build/logs.ts:163`), which legacy framed - too. `--cursor ` is documented as "Resume from a cursor a - previous run reported" (`build/logs.ts:187-190`) — after this change - no successful or partial run reports one, in either format. The only - cursor a user can obtain is the one `BUILD.FAILED` carries, so the - flag now works only after a failed build. The `output` event has a - free-form `data` field, already used here for `step` - (`packages/cli-engine/src/events.ts:52-58`), so carrying the cursor - is a one-line change. -2. **The headers are now json frames.** Legacy suppressed both headers - under `--json` (`controllers/build.ts:47`, - `controllers/app.ts:1609`). v8 reports them as `output` diagnostics, - so a json consumer of `build logs` reads one extra frame before the - records, and a json consumer of `service logs` reads three. - -The divergence file presents the json change as the wrapper-event drop -plus an envelope-shape change, with "the per-record events survive". -Neither the dropped fields nor the added header frames are in it. Carry -the fields through, or record both. - -### D3-R1-F4 — `build logs` belongs to no command family (low) - -`packages/cli/src/v8/cli.ts:45-63` (the platform family) and `:103` -(the mount), as committed in 55efe06. - -Every other Management-API command is listed in a family. `build logs` -is mounted straight into the tree, directly above the comment that -reserves familyless mounting for shell-owned surfaces (`:104`), so a -reader cannot tell whether the omission is a decision or an oversight. -Standing ruling 1 makes `CommandFamily` the ownership entity and gives -each subgroup exactly one owner, and `build logs` is a platform command -— it calls `/v1/builds/{buildId}/logs` through `ctx.api` — not a local -utility like `agent` or `telemetry`. Nothing breaks today because the -platform family declares neither a config section nor a docs base URL, -and the engine derives each diagnostic's docs link from that base -(`packages/cli-engine/src/command-family.ts:11-19`); the day a base URL -is set, this one command silently misses it. - -### D3-R1-F5 — the ndjson test helper does not exercise the buffering it claims to (low) - -`packages/cli/tests/v8-build-logs.test.ts:34-46`. - -The helper's comment says "One chunk per record, plus a split line, so -the reader's buffering is exercised rather than assumed", but it -enqueues exactly one complete, newline-terminated chunk per record. -Nothing splits a record across chunks, so `forEachNdjsonRecord`'s -partial-line buffer is never used, and nothing omits the final newline, -so the branch that parses the last record when the stream is `done` -(`packages/cli/src/v8/build/logs.ts:132-138`) never runs. Those two -paths are the only reason the reader is hand-written, and this dispatch -is the first test coverage `build logs` has ever had. Splitting one -record across two chunks and dropping the last newline covers both. - -### D4-R1-F1 — the `feedback` json divergence describes a change legacy did not make (should-fix) - -`.drive/projects/prisma-cli-v8/assets/s2/parity-divergences-s2c.md`, the section "`feedback` gains the standard json envelope" (added in da04dee). - -The entry says legacy registered no `renderJson` serializer for `feedback`, "so a `--json` run emitted the raw result record", and that the same record "now travels inside" the standard envelope. Legacy already emitted an envelope. `writeCommandSuccess` (`packages/cli/src/shell/command-runner.ts:110-117`) calls `writeJsonSuccess` for every `--json` run and consults `presenter.renderJson` only to decide what goes in the `result` field; with no serializer, the raw result goes there. `writeJsonSuccess` (`packages/cli/src/shell/output.ts:22-29`) prints `{ok: true, nextActions: [], command, result, warnings, nextSteps}`. The shipped behavior is pinned by the legacy test, which parses `prisma-cli feedback --json` and asserts `{ok: true, command: "feedback", result: {id, email, context}}` (`packages/cli/tests/feedback.test.ts:56-87`). - -An operator reading this entry concludes that a json consumer of `feedback` used to read `{id, email, context}` at the top level and must now reach into `.result`. It always had to reach into `.result`. The missing serializer changed nothing on the wire, and the two `agent` commands prove it from the other side: they do have serializers, but both are identity functions (`packages/cli/src/presenters/agent.ts:50-56`), so they shipped the same raw record inside the same envelope. What actually changes for `feedback` is the engine-global envelope reshape — `command` becomes `commandId`, `warnings` becomes `diagnostics`, `nextSteps` becomes typed `nextActions`, plus json framing — which the file's preamble already covers for every command in this slice. - -Say what changed instead: the `result` payload is unchanged (the entry already says this, and it is correct), and `feedback`'s json surface differs only in the engine-global ways. Or delete the section and let the preamble carry it. - -### D4-R1-F2 — the `agent` group's package-manager-aware help examples do not port, and the drop is unrecorded (low) - -`packages/cli/src/v8/agent/install.ts:87-93`, `packages/cli/src/v8/agent/update.ts:6-11`, `packages/cli/src/v8/agent/status.ts:47-50`, against `packages/cli/src/shell/command-meta.ts:22-29` and `:70-113`. - -Legacy renders the `agent` group's help examples through `agentCommandExamples`, which formats each one with the project's own package runner (`resolvePrismaCliPackageCommandFormatterSync`). So `agent install --help` shows `pnpm dlx @prisma/cli@latest agent install` in a pnpm project, `bunx @prisma/cli@latest …` in a bun project, and `npx -y @prisma/cli@latest …` otherwise. A legacy test pins it: `packages/cli/tests/agent.test.ts:414-421` asserts the help output contains `$ pnpm dlx @prisma/cli@latest agent install`. The v8 definitions declare bare examples (`"agent install"`, `"agent status --global"`, and so on) and the engine prepends the binary name (`packages/cli-engine/src/execution/stricli-adapter.ts:166-172`), so the same help now reads `prisma-cli agent install`. - -`agent` and `init` are the only commands legacy spelled this way, and the reason is specific to them: they are what a user runs before the CLI is on PATH. The port keeps the package-runner spelling everywhere else in the group — `agent install`'s next action is `npx -y @prisma/cli@latest agent status` and `agent status`'s is `npx -y @prisma/cli@latest agent install` (`packages/cli/src/v8/agent/install.ts:78-84`, `status.ts:89-97`, both pinned in `packages/cli/tests/v8-agent.test.ts`). One command is therefore spelled two ways in one product: help says `prisma-cli agent status`, the next action says `npx -y @prisma/cli@latest agent status`. - -The engine cannot express the legacy form. `help.examples` is a static `readonly string[]` on the definition (`packages/cli-engine/src/commands.ts:41`), and `resolveExample` either substitutes `{bin}` with the CLI name or prepends it, so no example can carry a package runner and none can vary with the project. The fix is therefore the record, not the code: add the drop to the dispatch-4 divergence section and name the engine constraint, the way the file already does for its other engine gaps. Restoring the behavior would be an engine ask, not a D4 change. - -### SLICE-ENG-F1 — the scheduled credential breakage is 13 commands, not one: `requireWorkspace` reads the same file the same way (should-fix) - -`.drive/projects/prisma-cli-v8/assets/s2/parity-divergences-s2c.md:485-487`, against `packages/cli/src/v8/service/target.ts:93-101` and `packages/cli/src/auth/operations.ts:152-163`. - -The log-stream credential entry, rewritten after the D3-R1-F1 adjudication, is right about the trigger and wrong about the blast radius. It tells the operator: "From the first `auth login` run after that lands, `service logs` is the one command that fails for a signed-in user whose other commands all work." The other commands do not all work. - -Every service command resolves its workspace through `requireWorkspace` (`target.ts:93-101`), which calls `readAuthState(ctx.env, ctx.signal)`. That function builds `new FileTokenStorage(env, signal)` and asks it for tokens (`packages/cli/src/auth/operations.ts:152-153`); when there are none it returns `{authenticated: false, workspace: null}` (`:155-163`), and `requireWorkspace` throws `SERVICE.WORKSPACE_REQUIRED` (`packages/cli/src/v8/service/errors.ts:88-97`). That is the **same** call `ctx.getCredentials()` makes — `makeGetCredentials` is `new FileTokenStorage(env).getTokens()` (`packages/cli/src/auth/credentials.ts:20`). So whatever flips one flips the other. - -I checked the incoming state rather than reasoning about it. On `bot/s2a-foundations`, `auth login` calls `ctx.credentialManager.createSession` (`bot/s2a-foundations:packages/cli/src/v8/auth/login.ts:132`); the manager's state file is `{version, sessions, currentWorkspaceId}` with no `tokens` key (`bot/s2a-foundations:packages/cli/src/auth/state-file.ts:26-30`), `writeCredentialState` replaces the whole file (`:169-193`), and both surfaces resolve to the same path by default (`state-file.ts:57-71` and `client.ts:25-32` both fall through to `defaultAuthFilePath(env)`). `@prisma/credentials-store` reads `data.tokens || []`, so `FileTokenStorage.getTokens()` returns null. `readAuthState` on that branch is untouched — byte-identical to ours. - -The consequence: after that merge-down, 13 of the 20 commands fail for a signed-in user without `PRISMA_SERVICE_TOKEN` — `deploy` (`deploy.ts:463` calls `requireWorkspace` directly), `show`, `open`, `list-deploys`, `logs`, `promote`, `rollback`, `remove` (all through `resolveServiceReadState` → `resolveServiceProjectContext` → `requireWorkspace`, `target.ts:267`), and the five `domain` commands (through `resolveServiceDomainTarget`, `target.ts:651`). `service logs` fails first, at `SERVICE.WORKSPACE_REQUIRED`, before it ever reaches the credential error the entry is about. Only `show-deploy` survives, because it treats a workspace failure as "no remembered project" and continues (`show-deploy.ts:50-54`); `service build`, `build logs`, `agent *` and `feedback` never ask. - -Nothing in the suite can catch this: all 12 service test files that reach `requireWorkspace` replace `readAuthState` with a mock returning `SIGNED_IN` (e.g. `packages/cli/tests/v8-service-logs.test.ts:17-20`), so the workspace and the engine's credential check come from two different seeds in the tests and from one file in production. - -This asks for no code — `packages/cli/src/auth/**` is a hard boundary. It asks the entry to state the real scope, because the operator is being asked to rule on an engine gap whose cost the record puts at one command and which is actually most of the slice. It also belongs beside the merge-down note in the orchestrator section: the merge is currently described as a routine "adopt any new test-harness credential seeding shape", and on this evidence it is a stop-the-line change. - -### SLICE-ENG-F2 — the stream record entry claims every legacy field is carried; `kind` and `details` are not (low) - -`packages/cli/src/v8/service/logs.ts:249-253` and `packages/cli/src/v8/build/logs.ts:177-181`, against `.drive/projects/prisma-cli-v8/assets/s2/parity-divergences-s2c.md:395-399`. - -The dispatch-3 fix carries the per-record fields, and the entry now says "a json consumer keeps everything legacy published per record". Two fields are still dropped from terminal records, and the entry does not list them among its two acknowledged losses. - -Legacy published the whole record — `data: record` (`packages/cli/src/controllers/app.ts:1806-1815`, `packages/cli/src/controllers/build.ts:87-95`). A terminal record is `{type, kind, code, message, retryable, cursor, details?}` for `service logs` (`@prisma/compute-sdk` `log-stream.d.ts:9-17`) and `{type, kind, code, message, retryable, cursor}` for `build logs` (`build/logs.ts:21-28`). Both ports attach `{cursor, code, retryable}` and put `message` in `line`, so `kind` is lost on both streams and `details` is lost on `service logs`. - -`kind` is the field that says whether the stream ended cleanly or in failure, and it is the only one that does so in a vocabulary the consumer already knows — `code` is a server-defined string. On `build logs` that costs little, because a `kind: "error"` terminal also settles the run as `BUILD.FAILED`. On `service logs` it costs the whole signal: a terminal error record is reported as one diagnostic `output` frame and the run still settles 0 (`packages/cli/tests/v8-service-logs.test.ts:204-230` pins exactly that), so a json consumer that used to read `data.kind === "error"` now has nothing to read. Carry the two fields, or add them to the entry's list of losses — the fix that closed D3-R1-F3 is a one-line change either way. - -### SLICE-ENG-F3 — the divergence file cites standing rulings by numbers that name different rulings (low) - -`.drive/projects/prisma-cli-v8/assets/s2/parity-divergences-s2c.md:157`, `:349`, `:610`, against `../specs/s2-overview.md:18-58`. - -Three justifications point the operator at the wrong rule. - -- `:157` and `:349` justify dropping `verboseContext` and the `--verbose` block with "S2 ruling 7". Standing ruling 7 is "Telemetry is essential". The ruling the entries mean is 8, "`--trace` is dropped (log levels cover it)" — and even that names `--trace`, not `--verbose`, so the entry is asserting a slightly wider rule than the overview states. -- `:610` justifies the bare `agent` help examples with "Standing ruling 5 forbids the binary name in an example". Standing ruling 5 is "`ctx.api`: the management API client lives directly on `CommandContext`". The rule that does say this is an operator ruling of 2026-08-09 recorded on the engine interface itself: "Invocations WITHOUT the binary name … at help render time every `{bin}` is substituted" (`.drive/projects/prisma-cli-v8/assets/engine/engine-interface-draft.ts:786-791`). The substance is right; only the citation is wrong. - -The other rule citations in the file check out (`R-S2b-6` at `:111` and `:270` is "Interactive pickers", `R-S2c-1`/`R-S2c-2` are the rename and the streams). This file is the operator's parity artifact, so a reference that lands on the wrong ruling is worth one pass to correct. - -### SLICE-ENG-F4 — nothing asserts the paths the 20 commands are actually mounted at (low) - -`packages/cli/src/v8/cli.ts:85-119` with `packages/cli/tests/v8-bin.test.ts:269-299` and `packages/cli/tests/v8-service-testkit.ts:280-297`. - -`cli.ts`'s `commands` record is the only place the user-facing command paths exist, and no assertion covers it. `v8-bin.test.ts` checks that `buildCli()` does not throw and that a root `--help` prints `USAGE` and `auth`; neither would notice a path typo, because the engine validates collisions and reserved flags, not spelling. The tests that do exercise these commands build their own tree from `SERVICE_COMMANDS` in the testkit — a hand-maintained second copy of the same map — and the agent and feedback suites declare a third and fourth (`packages/cli/tests/v8-agent.test.ts:14-18`, `packages/cli/tests/v8-feedback.test.ts:53-58`). So `"service list-deploys"` in `cli.ts` could read `"service list-deploy"` and all 1012 tests would still pass while the shipped binary answered to a name nothing documents. - -The 20 paths are correct today; I read them against the mount map and the group briefs. The gap is that the slice quadrupled the size of that map and added no way to notice it drifting. One assertion over `buildCli()`'s mounted paths — or having the testkit consume the same map `cli.ts` does, rather than restating it — closes it for every later slice too. - -### SLICE-ARCH-F1 — `service deploy` hand-rolls the local-binding failure instead of calling the operation layer's mapper (should-fix) - -`packages/cli/src/v8/service/deploy.ts:497-511`, against `packages/cli/src/controllers/app.ts:662-664` and `packages/cli/src/lib/project/setup.ts:94-145`. - -The operation layer already owns this failure. `bindProjectToDirectory` returns a `Result` whose error is one of five tagged types, and `projectDirectoryBindingErrorToCliError` (`setup.ts:94-125`) is the function that turns it into user-facing text. Legacy `app deploy` calls that mapper (`controllers/app.ts:663`). The v8 port is the only caller in the tree that does not: it checks `bound.isErr()` and builds its own `CliStructuredError` instead. R-S2b-10 is explicit that handlers call the existing operation layer rather than rewrite it, and every other legacy error surface in this slice does exactly that — `computeConfigErrorToCliError` and `projectResolutionErrorToCliError` are both used, through `fromLegacyCliError`. This is the one mapper of the three that is skipped. - -Four things change for the user as a result. - -1. **Three error variants the operation layer deliberately re-throws are now swallowed into a write failure.** `matchError` in the mapper re-throws `LocalResolutionPinWriteAbortedError`, `LocalResolutionPinGitignoreUpdateAbortedError` and `LocalResolutionPinSerializationError` rather than converting them (`setup.ts:99-124`). The first two are the abort signal firing mid-write — Ctrl-C during the first deploy of an unlinked directory. In v8 all three now settle as `SERVICE.LOCAL_STATE_WRITE_FAILED` at exit 2, advising the user to "Check write permissions for the .prisma directory", which for a cancelled run is simply untrue. -2. **`why` is the stringified error object.** `why: String(bound.error)` yields the error class name followed by its internal message (these are `TaggedError` subclasses of `Error`, `packages/cli/src/lib/project/local-pin.ts:86-120`), so a user reads `LocalResolutionPinWriteFailedError: Could not write .prisma/local.json.` where legacy wrote either "The CLI could not write .prisma/local.json." or "The CLI could not update .gitignore to keep local Project binding state out of git." The two failures are no longer told apart. -3. **`meta` is dropped.** Legacy carried `pinPath` or `gitignorePath` plus `operation` (`create-directory` / `write-temp-file` / `rename-temp-file`); v8 carries neither, so an agent reading the json envelope cannot tell which step failed. -4. **The legacy `fix` is dropped** in favour of shorter advice that names only `.prisma` and not `.gitignore`. - -Separately, `SERVICE.LOCAL_STATE_WRITE_FAILED` is one of only three codes this slice emits that the divergence file's error-code tables do not list. The other two, `SERVICE.BRANCH_DATABASE` and `SERVICE.BUILD_SETTINGS_LEGACY`, are warn diagnostics whose class the file already covers in prose; this one is an errored settlement, and R-S2b-5 asks for every code mapping to be enumerated. - -The smallest correction is one call: `throw fromLegacyCliError(projectDirectoryBindingErrorToCliError(bound.error))`. It produces the same `SERVICE.LOCAL_STATE_WRITE_FAILED` code, restores the summary, the two distinct `why` sentences, the `fix` and the `meta`, and leaves the three re-thrown variants re-thrown. Add the code to the dispatch-2 mapping table alongside it. - -### SLICE-ARCH-F2 — nine service commands report a finished action as though it were still running, with an information marker (should-fix) - -`packages/cli/src/v8/service/presentation.ts:31-33` (the shared `title` helper) and its callers at `:82`, `:203-206`, `:296`, `:332`, `:352`, `:376`, `:395-398`, `:466`, `:481`. - -`title()` hard-codes `tone: "info"`, and the engine renders a summary block's tone as its leading symbol — `ok` is `✔`, `info` is `ℹ` (`packages/cli-engine/src/execution/rendering.ts:65-77`). That summary block is the only success signal a result command has in human mode; the engine prints no completion line of its own. So a successful run currently ends like this: - -- `service remove` → `ℹ Removing the service and every deployment it owns.` -- `service promote` → `ℹ Promoting a deployment to production.` -- `service rollback` → `ℹ Rolling production back to an earlier deployment.` -- `service domain remove` → `ℹ Removing a custom domain from the selected service.` -- `service domain add` → `ℹ Adding a custom domain to the selected service.` -- `service domain retry` → `ℹ Retrying custom domain verification.` -- `service build` → `ℹ Building the local service artifact.` -- `service open` → `ℹ Opening the live URL for the selected service.` -- `service deploy` with no target (deploy-all) → `ℹ Deployed 2 services.` - -A user who runs `service remove` and reads the last line cannot tell whether the service was removed or whether the CLI is announcing what it is about to do. The tense says "in progress" and the marker says "for your information". - -This is not a matter of taste, because the slice contradicts itself and the layout precedent. Three commands in the same slice get it right: `service deploy` renders `✔ Deployed to .` (`presentation.ts:293-300`), `service domain wait` renders `✔ is live at ` (`:503-507`), and `feedback` renders `✔ Feedback sent. Thank you!` (`packages/cli/src/v8/feedback.ts:95`). So `service deploy` and `service deploy` with no target — the same command — disagree with each other. S2a set the pattern the other seven should follow: `auth logout` prints an `info` title describing the action and then a separate `ok` summary confirming the outcome (`packages/cli/src/v8/auth/logout.ts:23-37`), and `auth workspace use` and `auth workspace logout` do the same. Read-only commands correctly stay on `info` — `auth whoami`, `telemetry status`, and in this slice `service show`, `list-deploys`, `show-deploy` and `domain show`. - -Standing ruling 4 says human rendering is not pinned per command. That governs what the tests assert, not whether the output tells the user the command succeeded. - -The smallest correction is to give each of the nine either an `ok`-toned title in the past tense, as `service deploy` already does, or a trailing `ok` summary, as `auth logout` already does. `title()` can keep meaning "an informational heading" and simply stop being the only block these commands emit. - -### SLICE-ARCH-F3 — `service deploy`'s tests replace the mapping layer, and eight of the nine masked methods have no other caller (should-fix) - -`packages/cli/tests/v8-service-deploy.test.ts:16-19` (the `vi.mock("../src/lib/app/app-provider", …)`) and `:63-176` (`installFakeProvider`), against `packages/cli/tests/v8-service-testkit.ts:107-138` and `:229-272`. - -Round 1 of dispatch 2 recorded this seam as a defensible non-finding, on the grounds that "`deployApp` is a compute-SDK upload/build flow with callbacks that no HTTP fake reaches". That reasoning is correct and I am not disputing it. What the note did not weigh is how much else the mock takes with it. Mocking the `createAppProvider` factory replaces the whole provider, so nine methods are answered by a hand-written object: `deployApp`, and also `createProject`, `resolveBranch`, `createBranchDatabase`, `deleteBranchDatabase`, `listEnvironmentVariables`, `createEnvironmentVariable`, `updateEnvironmentVariable` and `deleteEnvironmentVariable` (`packages/cli/src/lib/app/app-provider.ts:281-342` and `:487-562`). Eight of those nine are plain request and response mapping with no callbacks at all. - -`service deploy` is the only production caller of all nine (`packages/cli/src/v8/service/deploy-target.ts:206`, `:341`; `packages/cli/src/v8/service/branch-database.ts:202`, `:299`, `:330`, `:346`, `:379`, `:422`, `:428`). So when this file mocks them, nothing anywhere in the repository covers them. `deployApp`'s live-pointer mapping is the sharpest case: `liveDeploymentId: deployed.promoted ? deployed.deploymentId : deployed.previousDeploymentId` (`app-provider.ts:539-561`) is the same "which deployment is actually live" question that a72f34a had to correct once already, and the deploy test asserts a value the test itself authored. - -The group already has the right pattern, twice over. `releaseRoutes` (`v8-service-testkit.ts:229-272`) models the compute SDK's own start / stop / poll / delete HTTP flow, so `promote`, `rollback` and `remove` run the real provider and the real SDK against route fakes. `service logs` mocks only the one SDK entry point it cannot reach over HTTP (`vi.mock("@prisma/compute-sdk", …)` for `streamLogs`, `v8-service-logs.test.ts:22-25`) and leaves `listApps` / `listDeployments` / `showDeployment` real. - -The smallest correction follows those two: stop mocking `createAppProvider`, serve the project, branch, database and env-var calls through `readFlowRoutes`-style routes, and fake only the compute SDK's deploy entry point. That leaves exactly one method uncovered instead of nine, and it is the one the dispatch-2 note actually argued for. - -### SLICE-ARCH-F4 — the `--db` engine-gap record asks for a larger engine change than the gap needs (low) - -`.drive/projects/prisma-cli-v8/assets/s2/parity-divergences-s2c.md:280-287`, against `packages/cli-engine/src/execution/command-snapshot.ts:57-114` and `packages/cli-engine/src/run-summary.ts:8-27`. - -The entry's statement of the divergence is right: a handler cannot tell `--no-db` from "not passed", so both take the prompt path. Its closing ask is what I am questioning — "the engine needs a declarable tri-state (or non-negatable) boolean before this is parity" — because the engine already derives the missing fact and simply sends it somewhere else. - -`explicitFlagKeys` (`command-snapshot.ts:57-95`) scans argv for which flag names appear, and lines 77-82 deliberately mark the base flag when they see a `--no-` token. `buildCommandSnapshot` then labels every declared flag `source: "cli"` or `source: "default"`, documented as "flags explicitly present on argv are 'cli'" (`run-summary.ts:12-24`). Combined with the parsed boolean the handler already receives, that settles all three states: `default` means absent, `cli` with `true` means `--db`, `cli` with `false` means `--no-db`. The engine computes this at parse time on every run. It hands it only to `RunHooks.onSettled`, after the run is over, for telemetry. - -The gap is therefore not in the flag model. It is that a fact the engine already holds never reaches the handler — the same shape as the interactivity fact the `--db` prompt entry needs four lines further down, and as the token `service logs` needs. Stating it that way changes what the operator would build: an accessor over an existing computation, rather than a new flag type with its own negation rules. Restate the ask; the divergence itself is recorded correctly and nothing in the code needs to change. - -### SLICE-R2-F1 — pressing Ctrl-C during the local Project binding write now reports an internal error (low) — CLOSED by the orchestrator in the tip commit - -**Orchestrator note: fixed exactly as the finding recommends — -`ctx.signal.throwIfAborted()` immediately before the mapper call, which -is the idiom already used 55 lines above in the same function for the -pin read. Raising the signal's own reason gives the engine a value it -recognizes as an abort, so Ctrl-C settles as a cancellation instead of -falling through to the crash path.** - -**Not covered by a test, deliberately.** Pinning it needs the run -aborted between `resolveDeployProjectContext` returning and the binding -write, and there is no event or other observable seam in that window — -the only two statements between them are a field read and the binding -call itself. Every way of forcing an abort earlier risks the test -passing because some earlier await rejected, which would assert nothing -about this line. A fragile test that can pass for the wrong reason is -worse than none, so the gap is recorded here instead. It closes for free -if the deploy flow ever emits a step event before the binding. - -`packages/cli/src/v8/service/deploy.ts:500-503`, against `deploy.ts:443-447` in the same function, `packages/cli/src/lib/project/setup.ts:98-124`, `packages/cli-engine/src/execution/settlement.ts:113-133` and `packages/cli/src/shell/command-runner.ts:40-43`. - -The SLICE-ARCH-F1 fix is right and I am not disputing it: routing through `projectDirectoryBindingErrorToCliError` is what the finding asked for, and leaving the three re-thrown variants re-thrown is what the mapper does. The consequence for two of those three variants was not checked, and it is worse than what the fix replaced. - -`LocalResolutionPinWriteAbortedError` and `LocalResolutionPinGitignoreUpdateAbortedError` are produced only when the abort signal has already fired (`local-pin.ts:348-355` and `:390-397`, both `signal?.throwIfAborted()` wrapped in a `Result.try`) — the user pressed Ctrl-C during the pin write on the first deploy of an unlinked directory. The mapper re-throws them unchanged, so a `TaggedError` leaves the handler. The engine recognises an abort in exactly two shapes: the thrown value is identical to `signal.reason`, or it is an `Error` whose `name` is the string `"AbortError"` (`settlement.ts:113-121`). Neither holds here. `better-result`'s `TaggedError` sets `name` to the tag, so the name is `"LocalResolutionPinWriteAbortedError"` (checked by running it, not by reading it), and the engine aborts with the plain string `"SIGINT"` or `"SIGTERM"` as the reason (`engine.ts:229-234`). `settleThrown` therefore falls through to `settleBug`, and the run ends on a `CLI.INTERNAL_ERROR` envelope at exit 1 with no next actions. - -Legacy did not do that. `toCliError` converts **any** thrown value into a clean cancellation whenever `runtime.signal.aborted` is true (`command-runner.ts:41-42`), so legacy `app deploy` reported a cancelled command. Before this fix v8 reported `SERVICE.LOCAL_STATE_WRITE_FAILED` at exit 2 with advice about write permissions — also wrong, but a structured command error rather than a crash report. - -The correction is one line, and the same function already contains it: the pin **read** path calls `ctx.signal.throwIfAborted()` before throwing its own error (`deploy.ts:446`), which throws `signal.reason` itself and so satisfies the engine's first test, settling `CLI.ABORTED` at 130. Adding the identical call immediately before `throw fromLegacyCliError(…)` closes this and leaves `LocalResolutionPinSerializationError` settling as an internal error, which is the right settlement for a genuine bug. - -Filed low because reaching it needs SIGINT inside the pin-write window of a first deploy. It is still a CLI that answers Ctrl-C with "internal error". - -### FINAL-F1 — the rename, the legacy-error mapper and the compute-config path lost their only tests with the three deleted suites (should-fix) - -`packages/cli/src/v8/service/errors.ts:42-93`, `:633-649`, `packages/cli/src/v8/service/target.ts:117-181`, `:554`, `:602`, against the deleted `packages/cli/tests/v8-service-build.test.ts`, `packages/cli/tests/v8-service-deploy.test.ts` and `packages/cli/tests/v8-service-logs.test.ts`. - -Three pieces of shared code that the eleven surviving service commands still run were only ever tested through the three commands that are now gone. None of the three is broken — I read each path and it behaves exactly as it did before the removals. What is gone is any test that would notice if it stopped. - -**The rename.** `renameAppCopy` (`errors.ts:42-47`) is the whole of R-S2c-1's error-copy surface. The only assertion on it lived in `v8-service-build.test.ts` (at `691566e`, `:171-193`): it ran a multi-target config through `service build` and asserted the serialized error contained neither the string "app target" nor the string "prisma-cli app ". That file is deleted. The function still changes what a user of the shipped commands reads. `ComputeConfigTargetUnknownError`'s summary is `Unknown app target ""` (`packages/cli/src/lib/app/compute-config.ts:274`), and every sentence `formatDomainFailureFix` produces names `prisma-cli app domain retry` or `prisma-cli app domain show` (`packages/cli/src/lib/app/domain-guidance.ts:27-41`), reaching the user through `domainVerificationFailedError` (`errors.ts:388`). I searched all of `packages/cli/tests`: no v8 test asserts any renamed string. The scoreboard row that says the rename is complete now has nothing executable behind it. - -**The legacy-error mapper.** `fromLegacyCliError` (`errors.ts:64-93`) produces six of the dispatch-1 error table's rows — `SERVICE.COMPUTE_CONFIG_INVALID`, `SERVICE.COMPUTE_CONFIG_TARGET_UNKNOWN`, `SERVICE.PROJECT_AMBIGUOUS`, `SERVICE.PROJECT_SETUP_REQUIRED`, `SERVICE.LOCAL_STATE_STALE` and `SERVICE.LOCAL_PROJECT_WORKSPACE_MISMATCH` — all reachable from the eleven commands that resolve a project through `resolveServiceProjectContext` (`target.ts:246-262`). The only v8 test that drove it was `v8-service-deploy.test.ts`'s `SERVICE.PROJECT_SETUP_REQUIRED` case (at `691566e`, `:875-888`), which was also the only proof of the D1-R1-F3 fix that carries a legacy `fix` through alongside typed legacy actions. Grepping the surviving `packages/cli/tests/v8-*` files for those six codes returns nothing; every `SERVICE.*` code the surviving suites assert comes from a native builder in `errors.ts`. - -**Compute-config resolution.** `resolveComputeManagementContext` (`target.ts:169-181`) runs on all eleven commands, through `resolveServiceReadState:554` and `resolveServiceDomainTarget:602`. It decides the project directory and the config-named service, which the D3-R1-F2 fix made rank above the remembered selection (`target.ts:580` and `:631`), and it is where `SERVICE.COMPUTE_CONFIG_TARGET_UNKNOWN` comes from when the positional names a target the config does not define, or names one with no config file present at all (`errors.ts:633-649`). That positional is documented as exactly this on every command that declares it (`show.ts:32-37`). At `691566e` the only v8 tests that wrote a `prisma.compute.*` file for a service command were in the three deleted suites (`v8-service-logs.test.ts:295` and `:328` among them); no surviving suite writes one, so none of this code runs under test. The one v8 test that still writes a compute config is `v8-agent.test.ts:496`, and it exercises the skills-lock walk-up, not this path. - -One test closes all three at once: run `service show ` in a directory holding a two-target `prisma.compute.json` and assert the settled code is `SERVICE.COMPUTE_CONFIG_TARGET_UNKNOWN` with the summary `Unknown service target ""`. That single run loads the config, selects a target, maps a legacy error through `fromLegacyCliError`, and renames the copy. A second short case — `service show web` where the config defines `web`, run with no scripted answers so a picker would fail it — restores the config-named-service coverage the D3-R1-F2 fix had. - -### FINAL-F2 — the divergence file states an escalation count its own entries cannot be checked against (low) - -`.drive/projects/prisma-cli-v8/assets/s2/parity-divergences-s2c.md:257` and `:579`, against `:255`, `:283`, `:391`, `:412`, `:528-543` and `:601`. - -Two sentences carry the arithmetic. Line 257 says "six engine gaps went to the operator during this slice, three are now retired … and three are still open". Line 579 says the deploy and build drop "took the open escalations from six to four (the `service logs` shelve below then took them to three)". - -The retired half checks out: three entries carry "(RETIRED — was an escalated engine gap)" in their headings, at `:255`, `:283` and `:412`. The open half does not. Only two entries carry "(ESCALATED — engine gap)": the `build logs` exit code at `:391` and the crash-recovery action at `:601`. The third open gap is the one where the `agent` group's help examples cannot carry a package runner (`:528-543`), and that entry is written as a group-wide question — "Worth settling once, group-wide, alongside the same question for every other ported group" — with no escalation marker of any kind. An operator counting escalations from the file's own headings finds two open, not three. The entry was unmarked before the rewrite as well, so this is not new; what is new is that the file now asserts a number that depends on counting it. Either mark that heading the way the other five are marked, or name the third open gap in one of the two counting sentences. - -### FINAL-F3 — the ledger's AC scoreboard still describes the twenty-command slice (low) - -`.drive/projects/prisma-cli-v8/reviews/code-review-s2c.md:16`, `:18` and `:22`. - -The divergence file was rewritten for the new shape and this table was not, and this table is the first thing the PR author reads. - -- Row 1 concludes "for 20 mounted in total. That is every in-scope command". The shipped map holds 26 paths, of which 17 belong to this slice (`packages/cli/src/v8/cli.ts:38-67`, asserted as an exact set at `packages/cli/tests/v8-bin.test.ts:277-307`). -- Row 3 supports "Deploy/promote/rollback/remove event sequences pinned" with "deploy pins the full phase sequence as an ordered array". The test that did so went with `v8-service-deploy.test.ts`, and `service deploy` is not a command. -- Row 7 records verification as "1012 cli tests" and the slice as "~16,000 added across 21 commits". At `HEAD` the cli suite is 965 tests across 72 files, which I ran. - -### MERGE-F1 — refusing a credential that names no workspace matches legacy only when legacy could not reach the server, and neither that nor the defect it fixes is recorded (should-fix) - -`packages/cli/src/v8/service/target.ts:91-109`, against `packages/cli/src/auth/operations.ts:133-192` and `:194-229`, with `.drive/projects/prisma-cli-v8/assets/s2/parity-divergences-s2c.md:100-116` and `packages/cli/src/v8/service/errors.ts:100-109`. - -**The change itself is right, and it repairs a real defect.** I compared it against the legacy source rather than reasoning about it. The code `requireWorkspace` replaced was `const state = await readAuthState(ctx.env, ctx.signal); if (!state.workspace) throw workspaceRequiredError();`. With `PRISMA_SERVICE_TOKEN` set, `readAuthState` hands off to `readServiceTokenAuthState`, which decodes the token's claims and, finding no workspace, returns `{authenticated: false, workspace: null}` (`operations.ts:206-220`) — so legacy settled `SERVICE.WORKSPACE_REQUIRED` for exactly this case. The merged code throws the same error from the same builder. It is also strictly better than what this branch had immediately before the merge: `ctx.session()` composed an environment session as `workspaceId: serviceTokenWorkspaceId(token) ?? ""` (`6d80563:packages/cli/src/auth/credential-manager.ts:344`), so `requireWorkspace` returned `{id: "", name: ""}` and the run went on to filter projects by an empty workspace id and print a blank workspace name. That is the case the final reviewer flagged for the orchestrator at the end of these notes; the merge closes it. - -**One difference from legacy survives, and it is not in the record.** Legacy asked the server before it looked at the claims. `readServiceTokenAuthState` calls `readCurrentPrincipalAuthState` first (`operations.ts:199-203`), which reads `GET /v1/me` — documented in the Management API types as "Returns the user, workspace, and credential represented by the current token" and typed `workspace: {id, name} | null`. When the server named a workspace, legacy used it and the command ran, whatever the token's own claims said. The merged path is local-only: `ActiveCredential.workspaceId` comes from `serviceTokenWorkspaceId(token)` and nothing else (`packages/cli/src/auth/credential-manager.ts:575-583`). So a service token that the platform associates with a workspace but whose JWT carries neither a `workspace_id` claim nor a `sub` of the form `workspace:` used to work and is now refused. The claims derivation is otherwise wider than legacy's, not narrower — legacy read only the `sub` form (`operations.ts:59-65`) — so this is the one direction in which the new path resolves less. - -**The refusal also hands the user advice that cannot clear it.** `workspaceRequiredError` offers one next action, "Sign in" → `auth login` (`errors.ts:106`). Under `PRISMA_SERVICE_TOKEN` that does not help: `createSession` writes the stored session but deliberately leaves the process pinned to the environment credential (`packages/cli/src/auth/credential-manager.ts:210-212` with `:411-413`), so the next run resolves the same environment token and fails the same way until the variable is unset. Legacy's advice had the same hole, so this is not a regression — but the merge is what routes this case here, and the case is now reachable where before it silently produced an empty workspace. - -Two things to do, neither large. Record the change in the "The workspace comes from the engine, not the credential file" entry: today that entry ends "`SERVICE.WORKSPACE_REQUIRED` itself is unchanged and still raised when there is no session" (`:115-116`), which is no longer the whole truth — it is now also raised when there is a credential that names no workspace, and legacy's online lookup that sometimes avoided that is gone. And give the error a second next action naming `PRISMA_SERVICE_TOKEN`, so the environment case is told what will actually fix it. - -### MERGE-F2 — the new refusal is untested, and the service harness has no way to seed the credential that reaches it (should-fix) - -`packages/cli/src/v8/service/target.ts:101-104`, `packages/cli/src/v8/service/errors.ts:100-109`, `packages/cli/tests/v8-service-testkit.ts:320-362`. - -`SERVICE.WORKSPACE_REQUIRED` is built in one place and asserted in none. A search of `packages/cli/src` and `packages/cli/tests` for the code returns exactly one hit, the builder itself. Both halves of the new condition are uncovered, for different reasons: - -- `credential === null` cannot be reached by a command that declares `needs.credentials`, because the engine settles those runs first (`packages/cli-engine/src/execution/needs.ts:112-135`). `v8-service-session.test.ts:143-160` pins that, correctly, as `CLI.CREDENTIALS_REQUIRED`. -- `credential.workspaceId === undefined` is reachable in production — set `PRISMA_SERVICE_TOKEN` to a token with no workspace claim and every service command takes it — but no test can produce it. `makeServiceCli` seeds only `sessions` and `selectedWorkspaceId`, and a stored session's `workspaceId` is its key, so it is never absent. The seam exists one level down: `createTestCli` accepts an `environmentCredential` (`packages/cli-engine/src/testing.ts:94-97`), and seeding one with a claimless token would drive the new branch end to end. - -This is the branch the merge added and the doc comment above it argues for. It should have a test, and adding the option to `ServiceCliOptions` is a few lines. - -### MERGE-F3 — the credential seed still reaches the harness inside a spread, so the next rename is silently ignored the same way this one was (should-fix) - -`packages/cli/tests/v8-service-testkit.ts:329-354`. - -The seed rename that broke 104 tests on the first merge was accepted by the type checker rather than rejected, and the reason is still in the file. `makeServiceCli` passes the seed as `...(options.authenticated === false ? {} : { sessions: […], selectedWorkspaceId: workspace.id })`. TypeScript's excess-property check applies to the properties written directly in an object literal, not to properties that arrive through a spread. I confirmed this against the repo's own compiler rather than assuming it: with the same spec type, `take({ commands: {}, sessions: [], currentWorkspaceId: "ws_1" })` fails with TS2353, and the identical set of properties delivered through `...(cond ? {} : {…})` compiles clean. - -So the harness will accept any future seed key the engine does not have, and an authenticated run will silently become an unauthenticated one. It fails eventually — a hundred tests go red at once — but it fails as a mass outage with no indication of the cause, which is what happened. Writing `sessions` and `selectedWorkspaceId` as ordinary properties, with `undefined` for the unauthenticated case, restores the compile-time check. - -I checked the rest of the harness for the same class of problem and found none. The route table throws on an unrouted request rather than falling back (`v8-service-testkit.ts:116-119`), and the unauthenticated axis is genuinely unauthenticated: with no seed the in-memory manager pins to `{kind: "none"}` and returns `null`, and `createTestCli` only puts `PRISMA_SERVICE_TOKEN` into a run's environment when an `environmentCredential` is seeded (`packages/cli-engine/src/testing.ts:178-184`), which `makeServiceCli` never does. - -### MERGE-F4 — the folded S8 slice says three open questions and lists four (low) - -`.drive/projects/prisma-cli-v8/plan.md:93` with `:98`. - -The fold is otherwise exact — I diffed the base's S8 section against the merged one and the only change is the added item. But the paragraph that introduces the list still reads "Three questions need answers that only S3 can give", and the new item is not one only S3 can give: it is a question for the engine and the API owners, which is what its own text says. Correct the count, and say where the fourth question's answer comes from. - -### MERGE-F5 — the retired log-stream entry was renamed in one line and left stale in five (low) - -`.drive/projects/prisma-cli-v8/assets/s2/parity-divergences-s2c.md:420-436`, against `:100` and `packages/cli-engine/src/credential-manager.ts`. - -The merge renamed one bullet in this section from `ctx.session()` to `ctx.activeCredential()` and appended a correcting paragraph at `:440`. The rename made the surrounding text worse rather than better: - -- The section still opens "On the current base the only accessor that reaches a session command at all is `ctx.getCredentials()`" (`:420-422`) in the present tense, and the three bullets under it now describe a runtime that never existed on any branch — `ctx.activeCredential()` and `ctx.getCredentials()` were never present at the same time. -- The renamed bullet attributes a quotation to a file that no longer contains it: "The token is INTERNAL" was in `packages/cli-engine/src/credential-manager.ts:15` before the merge and is gone from the tree now. -- `:436` cross-references the section by its old title, "The workspace comes from the engine's session, not the credential file". The merge renamed that heading to "The workspace comes from the engine, not the credential file" (`:100`), so the only pointer into it is now broken. -- The correcting paragraph at `:440` says "The paragraph above describes a runtime that no longer exists". The paragraph immediately above it is the one about the test harness; the stale claims are two paragraphs further up. - -The smallest fix is to put this section firmly in the past tense — it is already headed RETIRED — and to repair the cross-reference. Related and trivial: `packages/cli/tests/v8-service-session.test.ts:4` had the longer accessor name substituted into a wrapped comment without rewrapping, so that one line now runs well past the block around it. - -## Round notes - -### Round 1 (dispatch D1, 2026-08-10) — reviewer - -**What was checked.** All 26 changed files against the inventory -entries for `app build|show|open|list-deploys|show-deploy` and `app -domain add|show|remove|retry|wait`, read side by side with -`packages/cli/src/controllers/app.ts`. - -**The port is faithful where it matters.** Line-by-line comparison of -the resolution and mapping logic against the legacy controller found -no behavioral drift: branch resolution honors an explicit `--branch` -as-is and resolves an inferred one against the project's branches -(matching `resolveProjectContext`); `resolveCurrentLiveDeploymentId` -and `applyLiveDeploymentHint` reproduce the three-step live-pointer -precedence including the a72f34a "never assume newest is live" fix; -`show-deploy`'s workspace → remembered-project → known-live chain -matches `readCurrentWorkspaceId`; hostname normalization and -validation, the timeout grammar, the poll-interval env var, the -service picker's exact-name matching, and `formatBuildTypeName` are -faithful copies. The `--build-type` "auto" sentinel still reaches -`mergeComputeLocalInputs` correctly, so a compute-config `framework` -is not shadowed by the engine flag default. The domain error mapping -reproduces every status/hint branch of the legacy `domainCommandError`. - -**Engine use is idiomatic.** Thrown `CliStructuredError` with typed -`nextActions`, `ctx.present` + `ok()`, `needs.credentials` on the nine -platform commands and off on `build`, `output` events split -`data`/`diagnostic` by source, `status` events only on transition, -`endpoint` for the live URL. The one operation-layer change is the -additive `io` pass-through on `executeAppBuild` / -`resolveAppBuildStrategy`; legacy callers are untouched. Hard -boundaries (`packages/cli-engine/**`, `src/auth/**`, `v8/auth/**`, -publish files) are untouched. - -**Tests are the right kind.** Semantic throughout: envelope, -`presented.data`, events, exit codes — no byte pinning. Fakes are -shaped like real API responses (paginated `{data, pagination}`, -`{data: …}` singles, status-coded errors), the auth module is stubbed -at its seam, and the domain-wait fake advances a status sequence per -poll rather than pinning timing. The `--yes cannot grant consent` case -is pinned with the divergence cited inline. - -**Not findings — for the orchestrator, not this dispatch.** - -- *Cross-slice error-code consistency.* Project-resolution failures - surface here as `SERVICE.PROJECT_NOT_FOUND` / - `SERVICE.PROJECT_SETUP_REQUIRED` etc., because R-S2b-5 namespaces by - the invoking group. S2b's `project *` commands will emit their own - spelling for the same underlying failure. That is what the rule - says, and D1 recorded it — but two codes for one failure is worth an - explicit ruling before S2d consolidates the divergence files. -- *`target.ts` duplicates ~250 lines of private legacy controller - helpers* (selection, live-pointer, hostname, branch). Justified: the - legacy versions are private and take the shell's `CommandContext`, - and they die with the shell in S2d. Two copies can drift until then; - no action now. -- *`legacyResolutionContext` casts a two-field object through - `unknown` to `LegacyCommandContext`* (`target.ts:106-110`). Correct - today — `resolveProjectTarget` reads only `runtime.cwd`/`signal` — - and the comment says so. It stops compiling honestly if that - changes; flag for S2d cleanup, not now. -- The three escalated items (`--yes` consent, `ctx.interactive`, - injectable clock) were excluded from review per the dispatch brief - and are correctly documented in the divergence file. - -### Round 2 (dispatch D1, commits c7c10de + 0dd65a8) — reviewer - -**All three round-1 findings are closed, verified individually.** - -- **F1.** Unauthenticated cases added for `domain show`, `domain - retry`, `domain remove` — the three that lacked one. Successful json - envelopes now pinned (commandId + `result`) for all five domain - commands: `add`, `show`, `retry`, `remove`, `wait` - (`packages/cli/tests/v8-service-domain.test.ts:76`, `:253`, `:339`, - `:539`; `v8-service-domain-wait.test.ts:182-191`). Every axis - R-S2b-9 names is now covered per command. -- **F2.** `renameAppCopy` (`packages/cli/src/v8/service/errors.ts:36-40`) - replaces the lowercase "app target" noun as well, so - `ComputeConfigTargetRequiredError`'s summary and its fix now agree. - Dropping the old `^Unknown app target` anchored regex is a strict - improvement — the general replacement covers that string wherever it - appears. The `app:`/`apps:` config-key exception is now a recorded - decision, not a leftover, in the divergence file's error-mapping - section. Pinned by a test that fails if the serialized error - contains "app target" or "prisma-cli app " - (`packages/cli/tests/v8-service-build.test.ts:162-193`). -- **F3.** (1) `service open` restores the live-deployment next action, - pinned in `v8-service-open.test.ts:48-58`. (2) The `branch.id` drop - is recorded under "Result shape changes". (3) `fromLegacyCliError` - now appends the fix advice after the typed legacy actions rather - than choosing one branch (`errors.ts:52-58`), so - `PROJECT_SETUP_REQUIRED` keeps both; recorded in the error-mapping - preamble. - -**The `--confirm` grant is the engine's consent system used as -designed.** The engine's own `CLI.CONSENT_REQUIRED` text tells the -user to "pass the command's explicit consent flag if it documents -one" (`packages/cli-engine/src/execution/prompts.ts:100-105`) — a -command-declared flag is exactly the mechanism the engine anticipates, -and there is no engine-side registration to miss. `confirm` is not in -`RESERVED_FLAG_NAMES` -(`packages/cli-engine/src/execution/shared-flags.ts:8-20`), so -declaring it is legal and its value reaches the handler (unlike -`--yes`, whose value the engine hides). The handler checks it only -after resolving the domain, so `--confirm` never skips the -does-this-hostname-exist check -(`packages/cli/src/v8/service/domain-remove.ts:36-56`). - -Coverage of the new flag is real, not nominal: grant × non-interactive, -grant × interactive (run with `answers: []`, so an unexpected prompt -would fail the run — that is what proves no prompt happens), and json -envelopes for both; the pre-existing decline (exit 3), no-grant -non-interactive (exit 2), and `--yes` (exit 2) cases all survive -unchanged (`v8-service-domain.test.ts:422-518`, `:539`, `:588+`). The -divergence entry rewrites the old OPEN QUESTION into the ruled -behavior and states all four transitions, including that `--yes` still -never grants. - -**No regressions.** `openPresentations` gained a required -`liveDeploymentId` parameter; its only caller passes a value that is -provably non-null at that point (the handler throws -`SERVICE.NO_DEPLOYMENTS` above it). Nothing else in the round-2 diff -touches resolution, mapping, or event logic. - -**Note (superseded by the D2 consent ruling):** the concern below about -`--confirm` meaning two different things across the tree is resolved for -this group by the operator's D2 ruling — consent points carry no skip -flag in the interim, and the end state is a single engine-owned -`--confirm `. It still applies to S2b's exact-id `--confirm ` -flags, which are not consent points in the engine's sense. - -**One thing for the operator, not a finding: `--confirm` will mean two -different things across the tree.** Here it is a boolean grant. R-S2b-3 -keeps the *current* confirmation flag for S2b's destructive commands, -and the inventory records those as exact-id `--confirm ` -(`project remove`, `postgres remove`, `bucket delete`). Once S2b -lands, `--confirm` is a boolean on one destructive command and -"retype the resource id" on others. Both are defensible in isolation; -together they are a grammar inconsistency users and agents will hit. -Worth ruling deliberately before S2d consolidates — it is a naming -decision above this dispatch, and nothing in D1 should change for it. -The cross-slice error-code note from round 1 (`SERVICE.PROJECT_*`) -still stands and belongs in the same conversation. - -### Round 1 (dispatch D2 — deploy, promote, rollback, remove) — reviewer - -**What was checked.** All 19 changed files against inventory §4's -`app deploy|promote|rollback|remove` entries, read side by side with -`controllers/app.ts` (`runSingleAppDeploy`, `runAppPromote`, -`runAppRollback`, `runAppRemove`), `lib/app/production-deploy-gate.ts`, -`lib/app/branch-database-deploy.ts`, and `lib/project/interactive-setup.ts`. - -**The deploy port is faithful step for step.** The whole ordering — -exclusive project inputs, compute target, local pin read, branch, -port/region/entry validation, project context, pin binding, framework, -runtime, env vars, service selection, customization, production check, -entrypoint, build settings, legacy-settings inspection, branch database, -`deployApp`, state writes — matches `runSingleAppDeploy` position for -position. The production check reproduces -`enforceProductionDeployGate` exactly, including the empty-deployments -short-circuit and the `--no-promote` bypass; -`requireRemoteBranch` is a byte copy of `toBranchDatabaseDeployBranch`, -raw `Error` and all; the deploy-failure phase logic and its -`isNextStandaloneOutputFailure` predicate match (except the dropped -instruction, D2-R1-F2). `deploy-plan`, `env-vars`, `local-pin`, -`project/setup`, `bindProjectToDirectory` and `compute-config` are reused, -not rewritten. The rollback default target reads the **unsorted** -deployments list, which is what legacy does — a real trap, avoided. - -I checked one apparent omission carefully: legacy's project resolution -has a `resolveDurablePlatformMapping()` step between the local pin and -the prompt that v8 does not. That function is a placeholder that always -returns `null` (`lib/project/resolution.ts:586-588`), so dropping it -changes nothing. - -**The branch-database port matches the legacy decision matrix** — -`--db` false, provided DATABASE_URL/DIRECT_URL, production-after-first, -existing env for target, unsupported schema, signal-driven prompt, create -+ upsert + stale DIRECT_URL delete, and the delete-on-failure rollback -including the cleanup-failed message. The one behavioral shift (the -skip advice now also fires on an interactive "no", because handlers -cannot see interactivity) is deliberate, commented, and recorded. - -**Events genuinely pin ordering.** `service deploy` asserts the full -phase sequence as one ordered array (build → archive → upload → deploy → -promote, each start/ok); `service remove` pins `events[0]`, `events[1]` -and `events.at(-1)` around the SDK teardown counts; promote and rollback -assert the bracketing step plus the transition sequence. That is the bar -the dispatch asked for, met. - -**Test seams are strong where it counts.** promote, rollback and remove -drive the **real** provider through `ctx.api` routes — `releaseRoutes` -models the SDK's own start/stop/poll/delete HTTP flow, so the provider's -request and response mapping is exercised, not stubbed. Consent coverage -is complete on both new consent points (grant, decline → 3, -non-interactive → 2, `--yes` cannot grant). - -**Not findings.** - -- *`removeApp`'s `progress?: unknown` + `as never` - (`lib/app/app-provider.ts:195`, `:359`).* I was ready to file this - until I checked the file: every other SDK progress pass-through there - already uses exactly this shape (`:222`, `:239`, `:245`, `:479`, - `:527`, `:571`, `:584`, all from fcc0b26). The change is additive, - matches house style, and the callback object is type-checked at its - construction site in `release.ts` where it is declared - `DestroyAppProgress`. Typing the whole family properly belongs to the - S2d provider cleanup, not here. -- *`service deploy`'s tests fake `createAppProvider` wholesale* rather - than `ctx.api`, so listApps/resolveBranch/createProject/env-var calls - skip the provider's own mapping. Defensible — `deployApp` is a - compute-SDK upload/build flow with callbacks that no HTTP fake - reaches, and D1 set the precedent with `executeAppBuild`. Worth - revisiting only if provider mapping bugs start slipping through. -- *`ServiceDeployResult`'s optional `branchDatabase` / `localPin` / - `reason` properties* look like standing ruling 3, but that ruling - governs engine definitions, and these mirror legacy `AppDeployResult` - exactly (`types/app.ts:57-90`) — keeping the shipped json result shape - is the right call over internal tidiness. -- The escalated items (`--db` tri-state, rollback unconfirmed, agent-setup - prompt, fixture-test deletion, interactive-only consent) were excluded - per the dispatch and are all recorded in the divergence file. - -### Round 2 (dispatch D2, commits 040c750 + 55bb185 + a0b0ea6) — reviewer - -**Base change.** The branch was rebased onto `bot/s2a-foundations`, so -the engine affordances (consent tokens, `ctx.openUrl`, `needs.interaction`) -and the credential-manager rework are now underneath, and all ten prior -commits have new hashes. Spot-checked rather than re-reviewed: -`git diff 0dd65a8 794c2c0` over `v8/service/**`, the testkit and -`lib/app/build.ts` is **empty**, and `git diff fb6bc32 2159ee4` over -`packages/cli/src` and `tests` shows only the new base's own files -(auth/, v8/runtime.ts, credential-manager tests) — no service file moved. -The replay is content-identical for this dispatch's work. - -**All five findings closed.** - -- **F1** — the divergence file now carries one consent section for the - whole group (`parity-divergences-s2c.md:101-104` points at it, - `:189-217` is the table). Because tokens actually ship, the - ships-today/end-state split I asked for is moot, and the file is - accurate as written: I checked each claim against - `packages/cli-engine/src/execution/prompts.ts:335-351` — the token - branch, `--confirm` consumption once per run - (`consumeConfirmValue`, `:37-46`), `--yes` alone never granting, and - `--yes` plus a matching token granting via the same non-interactive - branch. The stale exit-3 claim is gone: with a token there is no "no" - to give, so a wrong answer is a mismatch (exit 2), and the file says - exactly that. -- **F2** — the instruction is back, in the `edit-file` action's `reason` - and again as its own advice action (`errors.ts:948-956`), and the test - now pins the text in both places rather than just the action kind. -- **F3** — `postgres connection create` (`branch-database.ts:251`). -- **F4** — the row now reads "*(no legacy error — the prompt re-asked)*" - and the prompt-validator gap is recorded beside the `--db` gap - (`parity-divergences-s2c.md:286-296`). -- **F5** — `serviceAmbiguousError` deleted. - -**Consent migration is correct against the engine, including the -semantics my brief had backwards.** `--confirm` grants only on the -non-interactive branch; an interactive session type-to-confirms -regardless. The tests pin exactly that, including the case that -distinguishes the two readings: interactive **with** `--confirm` present -still requires the typed token -(`v8-service-domain.test.ts:499-524`). Also pinned per command: -non-interactive grant, `--yes` + token grants, `--yes` alone does not, -wrong `--confirm` value → `CLI.CONSENT_REQUIRED` with -`meta.consentToken`, mistyped interactive token → exit 2. Deploy adds -the ordering case — `--confirm` without `--prod` still fails -`SERVICE.PROD_DEPLOY_REQUIRES_FLAG` -(`v8-service-deploy.test.ts:466-485`). Tokens are the natural nouns -(service name, hostname), so the typed string is guessable from the -question. - -The three handlers keep an `if (!granted)` guard that a token consent -can never reach (`confirmByTyping` returns true or throws, -`prompts.ts:294-312`); each is commented as a contract guard rather than -a live branch. Defensive but honest — a destructive call must not -proceed on a falsy consent if that contract ever loosens. - -**`ctx.openUrl` adoption is complete and real.** The handler no longer -emits its own endpoint event — the engine emits it inside `announceUrl` -with `name` taken from `message`, so `name: "live-url"` and the json -shape are unchanged. `isInteractive` and the `open` import are gone. -Tests cover the opener spy in all three states: interactive opens -(`opened: true`), non-interactive does not, and a throwing opener still -settles 0 with `opened: false`. I checked the shipping runtime actually -wires an opener (`packages/cli/src/v8/runtime.ts:106-108`), so this is -not test-only behavior. The interactive-`--json`-now-opens divergence is -recorded (`parity-divergences-s2c.md:114-123`); legacy's `canPrompt` -returned false under `--json` (`shell/runtime.ts:93-96`) and the engine -rules format out of interactivity, so the record is right. - -**The credential seeding change does exercise the shipping path.** -Seeding `credential` (not the legacy `credentials`) makes `createTestCli` -build a real `TestCredentialManager` (`testing.ts:125-143`), and -`checkCredentials` takes the manager branch — `currentSession()` — rather -than the `getCredentials` staged-swap fallback -(`packages/cli-engine/src/execution/needs.ts:117-133`). The -unauthenticated axis seeds nothing, so the manager holds no session and -the same `credentialsRequiredError()` fires. The two paths are mutually -exclusive by construction, so this is now the shipping path in every -service test. - -**Observations, not findings.** - -- `ctx.openUrl`'s `message` is documented as a human announcement label - ("Open your dashboard" in the engine's own test); the handler passes - the slug `"live-url"`, which human mode prints verbatim as - `live-url: https://…` (`rendering.ts:49-51`). Keeping the slug - preserves the json `endpoint.name` D1 shipped, so this is a real - trade, not an oversight — worth settling once, group-wide, rather than - per command. -- The standalone-output instruction now appears twice in one error (in - the action's `reason` and as a separate advice action). Harmless - belt-and-braces; a consumer that renders both shows the sentence twice. -- The divergence table conveys "interactive OR `--confirm`" by column - rather than stating that `--confirm` does not skip an interactive - prompt. The tests pin it; one clause in the file would make the record - self-contained. - -### Round 1 (dispatch D3 — the log streams) — reviewer - -**What was checked.** All 12 files of `a0b0ea6..55efe06` against -inventory §4's `prisma app logs` and `prisma build logs` entries, read -beside `controllers/app.ts` (`runAppLogs`, -`resolveExplicitLogDeployment`, `resolveLiveLogDeployment`, -`writeLogRecord`, `createPreviewLogAuthOptions`) and -`controllers/build.ts` (`runBuildLogs`, `writeBuildLogRecord`, -`forEachNdjsonRecord`); plus the engine's `reporting.ts`, -`rendering.ts`, `needs.ts`, `command-context.ts` and `testing.ts` for -the channel, log-level, json and credential semantics, and the shipping -`v8/runtime.ts` for what the bin actually wires. - -**Channel routing is the legacy routing, branch for branch.** `build -logs` sends a record to `diagnostic` when `record.source === "stderr" || -record.level === "error"` and to `data` otherwise — the same predicate -as `writeBuildLogRecord` (`controllers/build.ts:98-101`) — and the -engine writes `data` to stdout and everything else to stderr -(`rendering.ts:33-35`). A terminal record whose code is not `end` goes -to `diagnostic`, matching legacy's `stderr.write(record.message)`. -`service logs` sends every log record to `data`, which is what legacy -did (`writeLogRecord` wrote log text to stdout and dropped terminal -records in human mode entirely); showing the terminal message is the -one difference and it is recorded. Newline handling is equivalent: -legacy wrote the text and appended a newline when one was missing, the -port strips a single trailing newline and the engine appends one, so -the bytes match — including a record that ends in a blank line. -`--quiet` still hides both headers, because diagnostics carry display -severity `info` while `data` lines carry none, so no log level can ever -swallow the logs themselves (`reporting.ts:14-22`). - -**`skipSelection` does not reach the other read commands.** It is -optional and absent by default, so `show`, `open`, `list-deploys` and -`release.ts` (promote / rollback / remove) run the same -`resolveExistingServiceSelection` call as before -(`target.ts:602-610`), and the domain commands go through -`resolveServiceDomainTarget`, which this diff does not touch. The -`--service` / positional / `--deployment` combinations are right except -for the config-named case (D3-R1-F2): `--service` with `--deployment` -scopes the lookup and reproduces `requireDeploymentForApp`; -`--deployment` alone resolves globally, then checks that the deployment -has a service and that the service is in the resolved project, -producing the three `DEPLOYMENT_NOT_FOUND` variants with legacy's three -summaries; and a named service with no selection settles -`SERVICE.NO_DEPLOYMENTS`, which is the branch legacy could only reach -through a usage error. - -**The escalated exit-1 interim ships exactly as described.** A terminal -`error` record is remembered rather than thrown on, every remaining -record still streams, and only after the stream closes does the handler -throw `BUILD.FAILED` carrying `record.message` as `why`, `code` and -`retryable` in `meta`, the cursor in `meta` when there is one, and a -`build logs --cursor ` resume action -(`build/logs.ts:81-104`, `:228-238`). The test asserts the ordering (the -earlier log line is present in the events), the code, the `why`, the -meta and the resume action. Nothing in the port reaches for -`process.exitCode`. - -**The token interim is contained; the record around it is not -(D3-R1-F1).** The command asks `ctx.getCredentials()` and settles a -structured error when it resolves nothing. It never reads -`PRISMA_SERVICE_TOKEN`, never constructs `FileTokenStorage`, never -touches the auth state file. Its only env read is -`getApiBaseUrl(ctx.env)` for the stream's base URL, which is the value -legacy passed too and which no context accessor exposes. `rawTokenSeed` -is confined to one file: 14 uses, all in `v8-service-logs.test.ts`. -`build logs`, deploy, domain, show, open and the rest still seed -`credential` and exercise the credential-manager path, and both -unauthenticated cases deliberately omit the seed, so they still fail -through the manager. What the seed cannot model is the shipping runtime, -which wires a manager and a working `getCredentials` at the same time — -hence the finding, which is about the record, not the code. - -**Tests are the right kind, with one hole.** Semantic throughout: -events compared as an ordered array with their channels, envelopes and -`commandId`, exit codes, the state file for the selection cache, and the -captured request query for `--follow` / `--cursor`. R-S2b-9's axes are -complete for both commands (success, errored, json envelope, -unauthenticated; no consent point and no picker on either — the picker -stays proven once, on `service show`). The hole is the ndjson reader -(D3-R1-F5). The three service-logs error variants are driven through -route fakes that distinguish the branch-scoped listing from the -provider's branch-less global scan, which is a faithful model of -`findAppForDeployment`. - -**Observations, not findings.** - -- The doc comment "promote / rollback / remove need a service that - already exists" now sits above `deploymentDetachedError` rather than - `releaseTargetRequiredError` (`errors.ts:194-195`): the two new - builders were inserted between the comment and the function it - describes. -- `build logs`'s `BUILD.NOT_FOUND` advice points at `auth workspace use - ` where legacy said `auth login`. That is better advice - for "switch to the workspace that owns it" and worth keeping. -- The `service logs` header loses legacy's description line ("Streaming - logs for the selected deployment.") along with the block rendering, - while `build logs` keeps its single header line. The header change is - recorded; that the two commands now differ in shape is not. - -**Not findings — for the orchestrator.** - -- **The review worktree is not clean.** While this review ran, the - worktree gained uncommitted dispatch-4 work: - `packages/cli/src/v8/agent/`, `packages/cli/src/v8/feedback.ts`, - `tests/v8-agent.test.ts`, `tests/v8-feedback.test.ts`, a modified - `packages/cli/src/v8/cli.ts` that mounts them, and a modified - `parity-divergences-s2c.md` (a rewritten D2 agent-setup entry plus a - new dispatch-4 section). Every line reference in the D3 findings is to - the reviewed commit 55efe06; the D2 rewrite inserts four lines above - the dispatch-3 section, so those divergence-file numbers now read four - lines lower on disk. Nothing in D3's committed diff depends on any of - it. -- The engine knows the Management API base URL - (`runtime.managementApi.baseUrl`) and does not expose it on - `CommandContext`, so a self-authenticating stream has to rebuild it - from `ctx.env`. That belongs in the same conversation as the token - accessor, not in a D3 finding. - -### Round 1 (dispatch D4 — agent, feedback) — reviewer - -**What was checked.** All 11 files of `1943501..9f29e58`, and the divergence entries in `9f29e58..da04dee`, against inventory §4's entries for `prisma feedback `, `prisma agent install` / `prisma agent update` and `prisma agent status`. Read beside `controllers/agent.ts`, `controllers/feedback.ts`, `presenters/agent.ts`, `presenters/feedback.ts`, `commands/agent/index.ts`, `commands/feedback/index.ts`, `types/agent.ts`, `types/feedback.ts`, the whole of `lib/agent/**`, and the legacy tests `agent.test.ts` and `feedback.test.ts`. Also read the engine's `args.ts`, `commands.ts`, `command-family.ts`, `command-tree.ts`, `stricli-adapter.ts`, `settlement.ts`, `protocol.ts` and `run-summary.ts` for the parse, help, mounting and crash semantics the entries depend on. - -**The child-process seam is a faithful move.** Compared line by line, `v8/agent/skills-cli.ts` reproduces the legacy controller's private code: the same runner resolution through `resolveSkillsPackageRunner`, the same argument order (` skills@latest add prisma/skills --skill … --agent … [--global] [--copy] --yes`), `--copy` forced on `win32`, `stdin: "ignore"` with no `stdout`/`stderr` option so the installer's output is captured rather than streamed, and the same failure mapping including `exitCode ?? "unknown"`. The abort handling keeps legacy's asymmetry exactly: the install path rethrows only on `isAbortError`, while the list path also rethrows when `ctx.signal.aborted`. `parseSkillsListOutput`, `parseInstalledSkill` and `isPrismaSkillName` are copies down to the JSON-array guard and the string filter on `agents`. - -**The shared helpers are reused, not reimplemented, and no legacy file moved.** `constants.ts`, `package-manager.ts`, `cli-command.ts` and `setup-status.ts` are imported from `lib/agent/`; `git log` shows none of them has changed since e9666a6. The diff adds eleven files and modifies only `v8/cli.ts`. Nothing under `packages/cli-engine/**`, `packages/cli/src/auth/**`, `packages/cli/src/v8/auth/**`, the publish machinery or the version fields is touched. - -**The `skills-cli` / `skills@latest` reasoning holds.** Both the legacy controller and the port import `SKILLS_CLI_PACKAGE` from `packages/cli/src/lib/agent/constants.ts:3`, so the two CLIs invoke the same package by construction. Following the constant is right and the inventory's wording is the thing that is wrong. - -**`agent status` degrades exactly the way legacy degrades.** Same `skillsInstalled` fallback (`skillsList.status === "ok" ? skills.length > 0 : statusScope === "project" && setupStatus.skillsInstalled`), same `statusSource` ladder (`skills-cli`, else `skills-lock` for project scope, else `unavailable`), and the two warning sentences word for word, including "Falling back to skills-lock.json" for project scope and its absence for global scope. Global scope never borrows the project lock. The result record is field-for-field `types/agent.ts`'s `AgentStatusResult`, and the same is true of `AgentInstallResult` and of `feedback`'s `FeedbackResult`. The one structural change is that legacy's `warnings: string[]` becomes an engine `warn` diagnostic, `AGENT.SKILLS_LIST_UNAVAILABLE`; the run still settles 0. - -**`agent status`'s missing errored settlement is the right call, not a gap.** Neither legacy nor the port has an `AGENT.*` error code for `status` — a skills CLI that cannot be read is the fallback, not a failure — so there is no errored settlement to pin. What the dispatch pinned instead is the whole of the real failure behavior: the project fallback with its diagnostic and `statusSource: "skills-lock"`, the global case with `statusSource: "unavailable"` and no fallback, and the install next action offered in both. - -**`feedback`'s hand-rolled validation matches legacy exactly.** Same limits (4000 characters for the message, 320 for the email, the same `/^[^\s@]+@[^\s@]+\.[^\s@]+$/`), same order (message empty, message too long, then email), same trim-before-check, and the same refusal before any network call — pinned by asserting the loopback server received nothing. Each check has its own dotted `FEEDBACK.*` code at exit 2 per R-S2b-5. The missing-positional case is genuinely left to the engine: `positional.string` is required by default (`packages/cli-engine/src/args.ts:126-128`), so `feedback` with no argument settles `CLI.INVALID_ARGUMENTS`, and that is pinned too. The endpoint override is read from `ctx.env`, never `process.env`. `postFeedback` is a line-for-line move: the same `AbortSignal.any([ctx.signal, AbortSignal.timeout(3_000)])`, the same "is this an abort or a timeout" test on every catch (`if (signal.aborted) throw error`), the same `TimeoutError` name check, and the same treatment of a non-JSON 2xx body as a success. - -**Tests are the right kind, and nothing leaves the machine.** `execa` is faked at the module seam with `vi.mock`, and the only other code in these paths that looks like a child process — `resolveSkillsPackageRunner` and `resolvePrismaCliPackageCommand` — is pure filesystem reads, so no test can spawn anything. `feedback` runs against a loopback server on port 0, every test that reaches the network sets `PRISMA_CLI_FEEDBACK_URL`, and the one run that does not set it (`feedback --json` with no message) fails in the parser before any fetch; the unreachable-service case points at `127.0.0.1:9`. No test can contact the production endpoint. The suite is 27 tests in 248 ms, which is consistent with no spawn and no remote call. R-S2b-9's axes are complete per command — success, errored, json envelope — and the unauthenticated axis is pinned twice over, by asserting `needs.credentials === false` on all four definitions and by never seeding a session in any run. - -**Mounting outside a command family is right for these four, and `build logs` is still the odd one out.** A `CommandFamily` carries two things: a config section and a docs base URL (`packages/cli-engine/src/command-family.ts:5-20`). `agent *` and `feedback` need neither. They call no Management API, declare no credentials and read no config; they are contributed by the shell itself, not by a platform package, exactly like `telemetry`. Putting them in the platform family would claim an ownership that does not exist and would give them that family's future docs base URL, which is wrong for local utilities. The mount comment says so plainly. This is the opposite case from D3-R1-F4: `build logs` is a platform command that reaches `/v1/builds/{buildId}/logs` through `ctx.api`, so it does have an owner. I would leave these four where they are and move `build logs` into the platform family. - -**Observations, not findings.** - -- `openStateStore` (`v8/agent/status.ts:21-28`) is the third copy of the same six lines, after `v8/service/target.ts:83-91` and `v8/auth/agent-setup-tip.ts:44-51`. `v8/auth/**` is out of bounds for this dispatch, so consolidating them is S2d work. -- `feedback.ts` hand-rolls `{kind: "user-choice"}` advice actions where the service group has an `adviceAction` helper (`v8/service/errors.ts:15`). For a single command with no group, inlining is fine. -- `agent install --dry-run` uses the `info` summary tone where legacy always used the success tone. Human-only, and arguably more honest about a run that installed nothing. -- `agent status`'s human output moves the skills list below the field rows and renders it as a table; legacy interleaved it right after the "skills:" row. Human rendering is not pinned per command under standing ruling 4. - -**Not findings — for the orchestrator.** - -- **Three groups now ship with no command family** — `agent`, `telemetry`, and `build logs` per D3-R1-F4. The engine's own comment describes familyless commands as "Harness-mounted commands with no family are unowned" (`packages/cli-engine/src/execution/command-tree.ts`), which reads as though the mode exists for the test harness and the shell is not meant to use it. Standing ruling 1 also gives every subgroup exactly one owner. Either the shell needs a family of its own for its local utilities, or the engine's comment should say that familyless is a supported shell mode. It is one decision for the whole tree and it is above this dispatch. -- **The D4 commit also rewrites the dispatch-2 agent-setup entry to call the prompt's drop "final", on the grounds that "the operator was told and did not object".** Standing ruling 10 makes divergences an operator-review item, and no ruling to that effect is recorded anywhere in this file, unlike the Q2 drop which is in the orchestrator notes. Worth turning into either an explicit ruling or an explicit "awaiting ratification" note. The entry's factual claim does check out: the dismissal timestamp is still read and reported, through `readPrismaAgentSetupStatus` reading `state.agent.setupPromptDismissedAt` (`packages/cli/src/adapters/local-state.ts:266-269`) and `agent status` returning it as `promptDismissedAt`. -- **The review worktree is not clean, again.** While this review ran, the dispatch-3 fixer was editing `parity-divergences-s2c.md`, `v8/build/logs.ts`, `v8/cli.ts`, `v8/service/errors.ts`, `v8/service/logs.ts`, `v8/service/target.ts` and the two log-stream tests. Every reference above is to the reviewed commits — 9f29e58 for code and tests, da04dee for the divergence file — not to the working tree. - -### Slice review (principal engineer) — reviewer - -**The seven open findings, verified in the code rather than from the fix reports. All seven are closed.** - -- **D3-R1-F1 — closed.** All three places now name the trigger and neither claims the command works nor that it is broken. The error builder's comment (`packages/cli/src/v8/service/errors.ts:231-238`) says the answer is decided by the shape of the credential file, that today's `{tokens: […]}` shape makes the error unreachable, and that the auth rework flips it. The pinned test's comment (`packages/cli/tests/v8-service-logs.test.ts:592-598`) says the manager-plus-no-raw-token seed models the runtime that arrives with the rework, which is accurate: the harness defines `getCredentials` as the seeded value, so an unseeded raw token reproduces exactly the post-rework combination of a valid session and no readable token. The divergence entry (`parity-divergences-s2c.md:474-489`) says the same thing at length. The error and its test were kept, per the adjudication. **The entry is accurate about the trigger and wrong about the scope — SLICE-ENG-F1.** -- **D3-R1-F2 — closed.** The naming decision moved into `resolveServiceReadState`, which now computes `namedService = options.serviceName ?? compute.configServiceName` (`target.ts:606`) — the shipping precedence, `appName = appName ?? compute.configAppName` (`controllers/app.ts:1583`). This is strictly better than the old code even for the case the finding did not raise: the old `serviceNamed` counted the raw positional, which is a `prisma.compute.ts` target key, not a service name. `resolveExplicitDeployment` branches on `state.namedService !== undefined` (`logs.ts:41`), so a configured directory takes the scoped lookup and refuses a sibling's deployment. **No other caller changed.** `skipSelectionWhenUnnamed` is passed only by `logs.ts:193`; for every other caller the option is absent, so `selected` is computed by the same `resolveExistingServiceSelection` call with the same argument as before, and `show`, `open`, `list-deploys` and `release.ts` (promote / rollback / remove) are byte-identical in behavior. `show-deploy` does not use the function; the domain commands go through `resolveServiceDomainTarget`, untouched. The three new tests earn their keep — the compute-config scoping case fails with `SERVICE.DEPLOYMENT_NOT_FOUND` only because the config is read, and the bare-`--deployment` case runs with no scripted answers, so a picker would fail it. -- **D3-R1-F3 — closed.** Both streams attach their record fields: `build logs` carries `cursor`, `level`, `source` and `step` (`build/logs.ts:158-163`), `service logs` carries `byteStart` and `byteEnd` (`logs.ts:237`), and reported terminal records carry `cursor`, `code` and `retryable` on both. **Keeping the two losses was right.** The unframed normal close is genuinely unreachable: the engine has no event kind that is framed in json and silent in human mode, and inventing one is an engine change; the entry states the cost precisely (a build with no log records at all reports no cursor). The framed headers are forced by the rule that a handler must not read the output format, and the entry counts the extra frames (one for `build logs`, three for `service logs`). The record is honest about those two and overstated about the rest — SLICE-ENG-F2. -- **D3-R1-F4 — closed.** `buildLogs: buildLogsCommand` is in the platform family (`cli.ts:66`). Family membership is by object identity (`packages/cli-engine/src/execution/command-tree.ts:134-141`), so the key name does not matter and the docs base URL, when one is ever set, will reach it. -- **D3-R1-F5 — closed, and more thoroughly than asked.** `ndjsonStream` now cuts every record in half and omits the final newline (`v8-build-logs.test.ts:34-48`), so every test in the file drives both the partial-line buffer and the end-of-stream tail. A dedicated case feeds the body one character per chunk (`:139-165`). The first branch is proven by the records arriving at all; the second by the last record arriving despite no trailing newline. -- **D4-R1-F1 — closed.** The corrected entry (`parity-divergences-s2c.md:567-587`) matches the code: `writeCommandSuccess` calls `writeJsonSuccess` for every `--json` run and consults `renderJson` only for the `result` field (`packages/cli/src/shell/command-runner.ts:103-118`), and `writeJsonSuccess` prints the full envelope (`packages/cli/src/shell/output.ts:22-29`). The entry now says the missing serializer changed nothing on the wire and that `feedback`'s json surface differs only in the engine-global ways, which is right. -- **D4-R1-F2 — closed.** The drop is recorded (`parity-divergences-s2c.md:606-620`), including the engine constraint and the point the finding asked for: help says `agent install` while the same command's own next action says `npx -y @prisma/cli@latest agent status`. Two small inaccuracies in the entry: it cites the wrong ruling number (SLICE-ENG-F3), and it names `resolvePrismaCliPackageCommandSync` where the examples actually go through `resolvePrismaCliPackageCommandFormatterSync` (`packages/cli/src/shell/command-meta.ts:22-29`) — both real functions, but the formatter is the one in play. - -**What the whole-slice reading adds.** Four findings, above. Only the first is about anything a user will hit; the rest are record accuracy and one coverage hole. - -**The four parts are more consistent than four dispatches usually are.** Definition exports are `Command` throughout, result types sit in a `results.ts` and human rendering in a `presentation.ts` for both groups that have more than one command, and the two session commands are the only `defineSessionCommand` users. Where the parts differ, the difference tracks a real difference in the work: `service logs` imports the service group's error builders and next-action helpers, while `build logs` and `feedback` declare their own — right, because those two are the only commands in their namespaces (`BUILD.*`, `FEEDBACK.*`) and importing `service/errors.ts` would give them `SERVICE.`-prefixed codes. The agent group has zero `ctx.report` calls, which looks like a progress gap on a command that shells out to a package manager, but legacy printed nothing there either (`controllers/agent.ts` writes to no output stream), so the port is faithful, not lazy. - -**The one shared helper that grew a caller-specific option grew it correctly.** `resolveServiceReadState` now carries `skipSelectionWhenUnnamed` and returns `namedService` for `service logs` alone. The option is honestly named after what it does, the doc comment says which command needs it and why, and the branch is inert for every other caller. The two other cross-boundary changes are the same shape: `executeAppBuild`'s `io` tap and `removeApp`'s `progress` pass-through are both additive optional parameters, and the legacy call sites pass neither. - -**`service deploy`, read fresh against `runSingleAppDeploy`.** The ordering matches position for position: exclusive project inputs, compute target, merged inputs, service directory, project directory, local pin, branch, port, region, requested-shape entrypoint check, project context, pin binding, framework, runtime, second entrypoint check, env vars, service listing, selection, the deploy line, customization, production protection, third entrypoint check after customization, entrypoint resolution, build settings, legacy-settings inspection, branch database, `deployApp`, then the two state writes. The three entrypoint assertions are not redundant — the first covers `--framework` before anything else runs, the second covers a detected framework, and the third covers a framework the user switched to at the customization prompt. `enforceProductionDeploy` reproduces `resolveCurrentProductionDeployment`'s fallback chain exactly (`liveDeploymentId` match, then any `live === true`, then the first deployment, then null) including the empty-list short-circuit, and the `--no-promote` bypass sits in the same place. `parseDeployPortMapping(String(runtime.port))` looks dropped but is not: its only early return is on a falsy string, and `String(number)` is never falsy, so `{http: runtime.port}` is the same value. Two differences I checked and accepted: the port passes `DEFAULT_HTTP_PORT` explicitly where legacy closed over it, and an unreadable local pin now reports `SERVICE.LOCAL_STATE_STALE` where legacy rethrew an unexpected read exception as a crash — the latter is commented at `deploy.ts:440-445`, keeps the abort case propagating, and is better behavior, though it means a permission error on `.prisma/local.json` is reported as a stale binding. - -**Nothing in the slice parses a flag it never reads.** I traced all 60 declared flags and positionals to a consumption point, following one hop where a handler forwards `args`. Three declared values are unreachable rather than unread, and all three are already accounted for: `--no-db` cannot reach `branch-database.ts:73` because the engine's boolean flag is two-state (the recorded `--db` tri-state gap), and the `--timeout` and `--build-type` defaults make their in-code fallbacks dead. None is a defect. - -**Not findings — for the orchestrator.** - -- **The merge-down decision now has a bigger number attached to it.** SLICE-ENG-F1 is filed as a record correction because that is all this slice can do about it, but the underlying fact is an operator decision, not a wording one: on the base branch as it stands, merging down takes 13 of this slice's 20 commands from working to failing for any signed-in user without `PRISMA_SERVICE_TOKEN`. The orchestrator note about the held merge-down currently reads as a scheduling matter ("merge down once that stream is green"). It is a compatibility matter as well, and the fix is not in this slice's tree. -- **The two credential seams in the test harness cannot both be right.** `makeServiceCli` seeds the engine's credential manager for `needs.credentials` and every service test separately mocks `readAuthState` for the workspace. In production those are one file. That split is what makes the previous point invisible to the suite, and it is not a `rawTokenSeed` problem — `rawTokenSeed` is confined to the log-stream file and is honestly documented. Whatever seeding shape the merge-down adopts should join the two. -- **`prisma-cli` is spelled literally in three places in new v8 code** (`deploy-target.ts:350-352`, `branch-database.ts:251`) where `CLI_NAME` is the stated convention (`packages/cli/src/cli-name.ts:1-7`). No user sees a difference today, because `CLI_NAME` is `"prisma-cli"` and the `deploy-target.ts` strings are rewritten by `renameAppCopy` on the way out. It becomes a real defect the day the binary is renamed, which S2d is scheduled to decide. Sweep it then, with the rest of the naming work. - -### Slice review (architect) — reviewer - -**What was checked.** The whole slice as a shape rather than line by line: the mount map in `v8/cli.ts`; the three resolution seams (`service/target.ts`, `service/deploy-target.ts`, `service/release.ts`) with all 21 of their consumers and the exact symbols each imports; `errors.ts` and `results.ts` against the divergence file's code tables, by extracting every `SERVICE.*` / `BUILD.*` / `AGENT.*` / `FEEDBACK.*` string the slice emits and diffing it against the tables; `presentation.ts` in full; the kind, `needs` and argument surface of all 20 definitions; the seam of all 16 test files; and the engine itself — `commands.ts`, `context.ts`, `args.ts`, `command-snapshot.ts`, `run-summary.ts`, `settlement.ts`, `command-tree.ts`, `rendering.ts` — once per recorded gap. I read the code lens reviewer's four findings before writing; nothing here duplicates them. - -**The layering held, and it is the best thing about the slice.** The v8 tree imports 17 distinct modules from `lib/` and rewrites almost none of them: `deploy-plan`, `env-vars`, `local-pin`, `project/setup`, `project/resolution`, `compute-config`, `read-branch`, `app/build`, `app/branch-database`, `domain-guidance`, `bun-project`, `git/local-branch` and the four `lib/agent/**` modules are all called, not copied. The only operation-layer changes are the two additive pass-throughs the contract allows (`executeAppBuild`'s `io`, `removeApp`'s `progress`). - -I tested the duplication question the only way that settles it: for every block of copied-looking code in `v8/`, is the original **exported** and does it live in `lib/` rather than `controllers/`? The answer was no every time. `lib/app/production-deploy-gate.ts` exports one function, `enforceProductionDeployGate`, which takes the shell context and does its own `--yes` consent, so v8 cannot call it and reimplements the check; the pure live-deployment fallback inside it is private. `lib/app/branch-database-deploy.ts` exports only `maybeSetupBranchDatabase` plus two types — every one of the ten helpers `v8/service/branch-database.ts` reproduces is private to that file, and the primitives underneath it (`lib/app/branch-database`) *are* imported rather than copied. The same holds for `v8/agent/skills-cli.ts` against `controllers/agent.ts`, `v8/feedback.ts` against `controllers/feedback.ts`, and `forEachNdjsonRecord` against `controllers/build.ts`: private in every case. The one exported function that was copied instead of imported, `detectDeployFramework` (`controllers/app.ts:4116`, inlined at `deploy-target.ts:691-709`), lives in `controllers/`, and importing it would pull the legacy controller import graph into `v8/` — the same reason `target.ts` gives for its own copies. - -So the shape is right: every duplicate in this slice is a copy of something that dies with the shell, not a fork of something that survives it. That was the main risk in a 16,000-line port and it did not happen. The cost is bounded and known — two copies of each behavior until S2d, which is what the D1 note already accepted. - -The exceptions are small and countable. Beyond the two the ledger already records (`target.ts` copying private controller helpers, and `listWorkspaceProjects`), the slice added two duplications of its own: `listWorkspaceProjects` is now written **twice inside v8** (`target.ts:228-255` and `deploy.ts:360-387`, identical but for taking a workspace instead of a workspace id), and the six-line state-store opener is on its third copy (`target.ts:82-91`, `agent/status.ts:21-28`, `v8/auth/agent-setup-tip.ts:44-51`). Neither is worth a finding by itself; together they say the group has not formed the habit of looking for the helper before writing one. There is exactly one real bypass of the operation layer, and it is SLICE-ARCH-F1. - -**`target.ts` is a coherent seam, with one accretion I would leave alone.** Its exports fall into three clean layers: primitives (`openServiceStateStore`, `requireWorkspace`, `readServiceEnvOverride`, `serviceProvider`, `listServices`, `resolveComputeTarget`), projections (`toServiceSummary`, `toServiceDomainSummary`, `applyLiveDeploymentHint`, `sortDeploymentsNewestFirst`, `normalizeDomainHostname`), and two composed flows (`resolveServiceReadState` for the read and release commands, `resolveServiceDomainTarget` for the five domain commands). `deploy` uses the primitives and composes its own flow in `deploy-target.ts`, because its resolution genuinely differs — it may create a Project, write a pin and name a service that does not exist yet. Three flows over one set of primitives is the right shape for this group, not accretion. - -The one option that is a special case is `skipSelectionWhenUnnamed`, passed by `service logs` alone, together with the `namedService` field that only `logs` reads. I considered filing it and decided against: the alternative is `logs` calling the four primitives itself, which would duplicate the compute-context / project-context / listing sequence for real. The boolean is the cheaper of the two. It is worth watching, because a second per-command option on that function would tip it. - -The genuine unevenness is on the argument surface, not the resolution surface. `domain-shared.ts` exists precisely to share the `--service` / `--project` / `[service]` block across the domain commands, and then seven other commands declare the same block inline — nine copies of the identical `--project` brief across the group. They all agree today; I checked. A group that has a helper for exactly this and uses it in a third of the places gives the next porting slice no answer to "how do we declare a target?". - -**Error and result modelling is consistent inside the slice; the pain is at its edges.** Every errored settlement in the group is a `CliStructuredError` built in one of two ways — a native `SERVICE.*` builder in `errors.ts`, or `fromLegacyCliError` prefixing `SERVICE.` onto a legacy flat code — and the two never disagree about a code. The apparent duplicates are faithful: `frameworkNotDetectedError` / `deployFrameworkNotDetectedError` and `entrypointUnsupportedError` / `deployEntrypointUnsupportedError` share a code and differ in copy because legacy's copy differed per command. `results.ts` is shaped for its consumers — `service | null` on the read results because a project can have no service, non-null on `open` and the domain target because those commands cannot proceed without one. - -Three things will hurt at consolidation, and only the first is this slice's to fix. - -- `fromLegacyCliError` prefixes `SERVICE.` onto **whatever** code the legacy error carries, including project-domain codes. That is what R-S2b-5 asks for and it is recorded, but it means the mapping is implicit: `SERVICE.PROJECT_AMBIGUOUS`, `SERVICE.PROJECT_CREATE_FAILED`, `SERVICE.LOCAL_PROJECT_WORKSPACE_MISMATCH` and `SERVICE.COMPUTE_CONFIG_TARGET_REQUIRED` appear in the divergence tables and nowhere in the source, so the table is the only inventory of them. Whoever consolidates S2b and S2c has to derive that list from two files rather than read it. -- `toEngineNextAction` (`errors.ts:19-27`) does **not** run typed legacy `nextActions` through `renameAppCopy`, unlike every prose field beside it. It is safe today — I traced every reachable legacy error with typed actions and the only one is `PROJECT_SETUP_REQUIRED`, whose commands are `project` commands plus `prisma-cli --project …` with `commandName` already the v8 spelling. It stops being safe the moment a `lib/app/**` error with typed actions becomes reachable, and `production-deploy-gate.ts:154-172` is exactly such an error (its actions say `prisma-cli app deploy --prod`) that v8 currently sidesteps by rebuilding the error natively. -- `errors.ts` is 1,185 lines and 56 builders in one file, keyed only by naming convention. That is manageable now and will not be after `PROJECT.*`, `POSTGRES.*` and `BUCKET.*` join it in S2d. - -**Command kinds are right, including the ones the contract singles out.** The five long-running operations (`deploy`, `promote`, `rollback`, `remove`, `domain wait`) are result commands emitting `step-started` / `step-finished`, `progress` and `status`, exactly as R-S2c-3 asks; they all have a real result to return, so a session would have been wrong. The two streams are session commands per R-S2c-2, which is right for the same reason in reverse — neither has a result, and the event stream is the json surface. `service build` is a result command with no `needs.credentials` and no `ctx.api` touch, per R-S2c-5. `server-command` is correctly unused. The one place the kind choice bites is `build logs`: a session cannot settle exit 1, which is engine gap 3 and correctly escalated rather than worked around. - -R-S2c-3 also says the polling "drives events through the injectable clock". There is no clock on `CommandContext`, so `domain wait` polls with `setTimeout` and `Date.now()` and the tests set `PRISMA_CLI_DOMAIN_WAIT_POLL_MS: "1"` to make real time cheap. The continuation brief records this as escalation 5, "accepted for now"; it is not in the divergence file, which is correct — it changes nothing a user can see. - -**Test seams: chosen well everywhere except the one command that matters most.** Eleven of the sixteen files fake `ctx.api` and let the real `createAppProvider` mapping run — that is the right level, and `releaseRoutes` goes further by modelling the compute SDK's own start / stop / poll / delete HTTP flow so `promote`, `rollback` and `remove` exercise the SDK too. `service logs` mocks only `streamLogs`, the one SDK entry point no HTTP fake reaches, and leaves resolution real. `agent` fakes `execa` at the module seam, which is the process boundary and the only sane choice. `feedback` runs a loopback server because a session command gets no HTTP seam from the engine, and its own file comment says so. Those are all seams picked for a reason. - -`service deploy` is the exception, and it is the command with the most behavior and the highest cost of being wrong. Mocking the `createAppProvider` factory takes out nine methods, eight of which are ordinary request and response mapping that the group's own testkit already knows how to serve, and `service deploy` is the only production caller of all nine — so nothing anywhere covers them. That is SLICE-ARCH-F3. It is the difference between "we cannot fake the SDK upload" (true, and the dispatch-2 note's actual argument) and "we cannot fake the provider" (not true, and what the mock does). - -**What this hands S2d.** Easier than it could have been, with a short cleanup list. The commander shell's *behavior* is fully duplicated for these 20 commands, so deleting `src/commands/**`, `src/controllers/**` and `src/presenters/**` takes nothing v8 needs — every dependency runs the other way, into `lib/`. What does have to move rather than die is `src/shell/`: v8 imports `shell/errors` (`CliError`, the type the whole operation layer throws), `shell/runtime` (the legacy `CommandContext` type), `shell/next-actions` and `shell/command-arguments`. Those four are not commander machinery; they are operation-layer types living in a directory named after the thing being deleted, and `v8/auth/**` already depends on them, so this predates the slice. Naming that in S2d's contract now would save a surprise later. - -The list of things S2d should sweep, all small: two identical `as unknown as LegacyCommandContext` casts fabricating a fake shell context (`target.ts:113-117`, `deploy.ts:354-358`) — both correct today and both compile-time lies that stop being honest when the shell type goes; the third copy of the state-store opener; the second copy of `listWorkspaceProjects`; the nine inline copies of the domain arg block; and `target.ts`'s ~250 lines of copied private controller helpers, whose originals die with the shell. The ruled decision to keep the legacy fixture tests is right and is the single biggest thing S2d inherits, but it is a deletion, not a migration. - -**The six engine gaps are one missing idea, not six holes.** Read together they are all the same shape: the engine's contract between a command and itself is declared once, statically, and there is no run-time channel in either direction. Everything a command needs at run time that is not "declare up front" or "return a result and emit events" has no way across. - -Four of the six are the outward direction — the command cannot contribute a value to something the engine composes and owns the rendering of. A session command cannot contribute an exit code to the settlement (`build logs`, gap 3; `commands.ts:280-284` returns `Result`, and `command-tree.ts:72-84` confines documented codes to 4–99). The bin cannot contribute next actions to an internal-error envelope (`settlement.ts:175-190` writes `nextActions: []` literally, and `onSettled` fires afterwards with no error object — the crash-recovery gap). A handler cannot contribute a validator to the prompt loop the engine runs (`context.ts` gives `prompt.text` only `placeholder` and `default` — the Project-name gap). A definition cannot contribute an example computed at run time (`commands.ts:30-43`, `help.examples` is a static `readonly string[]` — the `agent` help gap). - -Two are the inward direction — the engine holds a fact and does not hand it to the handler. The token exists and `ctx.getCredentials()` is documented as staged for deletion with no replacement (`context.ts:41-48` — the log-stream gap). And the flag-source fact the `--db` tri-state needs is already computed at parse time, complete with `--no-` handling, and routed to telemetry instead of to the handler (SLICE-ARCH-F4). The same direction explains the two items the file records as consequences rather than gaps: the handler cannot see interactivity (so the `--db` advice fires on a declined answer as well as an unaskable one), and the handler cannot see the output format (so the stream headers are framed under `--json`). - -That is the judgement worth carrying to the operator: this is not six unrelated engine asks that can be prioritised against each other. It is one design property with two faces, and a decision about the property answers most of them at once. An inbound decision — put the invocation facts the engine already computes on `CommandContext` (flag source, interactivity, a token or a pre-authenticated stream client, the API base URL) — closes gaps 1 and 4 and both recorded consequences, and is small because nothing new has to be computed. An outbound decision — one contribution-point pattern, a callback the engine calls at the moment it composes a thing — closes gaps 2, 3, 5 and 6 with one mechanism applied at four call sites. Sequenced that way the six are two changes, not six. - -**Not findings — for the orchestrator.** - -- **The AC scoreboard still reads as it did before the last two commits.** It records the divergence file as "NOT met" on the D3 and D4 surfaces, citing five findings that `8e3b181` and `f7b3635` addressed, and none of the seven D3/D4 findings carries the "CLOSED in" marker the D1 and D2 entries use. The code lens reviewer has verified all seven closed. Whoever opens the PR reads this file; it should not still say the file is incomplete. -- **The cross-slice error-code question is now overdue, not just open.** It has been raised in three separate round notes (D1 round 1, D1 round 2, and implicitly here) and never ruled. S2d consolidates the divergence files, and the consolidation is where a `SERVICE.PROJECT_NOT_FOUND` and a `PROJECT.PROJECT_NOT_FOUND` for one failure become a user-facing inconsistency rather than a table entry. It wants a ruling before S2d starts, not during it. -- **The engine's own gap set deserves one conversation, not six tickets.** See the judgement above. If the operator would find it useful, the two decisions could be written up as a single short engine amendment for S3 rather than six escalations carried forward — the individual entries are all recorded accurately (with the one qualification in SLICE-ARCH-F4) and would survive being restated that way. - -### Slice review round 2 (verification) — reviewer - -**Verdict: NOT SATISFIED.** All eight findings are closed, verified in the code rather than accepted from the fix report. One new low finding, SLICE-R2-F1, is open: the SLICE-ARCH-F1 fix turns a Ctrl-C during the local Project binding write into a `CLI.INTERNAL_ERROR` envelope. - -- **SLICE-ENG-F1 — closed.** The count is 13, and 13 is right. -- **SLICE-ENG-F2 — closed.** The entry now lists exactly what the code emits. -- **SLICE-ENG-F3 — closed.** All three citations corrected; the rest of the file's citations check out. -- **SLICE-ENG-F4 — closed.** One shipped map, an exact-set assertion over it, and every test harness derived from it. -- **SLICE-ARCH-F1 — closed.** The mapper is called; summary, both `why` sentences, `fix` and `meta` are back. See SLICE-R2-F1 for the one consequence the fix did not check. -- **SLICE-ARCH-F2 — closed.** All nine now end on a past-tense `ok` line; the three that were already right did not regress. -- **SLICE-ARCH-F3 — closed.** The provider factory mock is gone and the coverage is real — proven by mutation, not by reading. -- **SLICE-ARCH-F4 — closed.** I verified the engine claim in the engine before accepting the rewrite. - -**Verification state.** At `0459208`: `pnpm --filter @prisma/cli test` green at 1019, `pnpm --filter @prisma/cli-engine test` green at 234, `pnpm typecheck` green, `pnpm lint` exits 0. The one file I mutated (`packages/cli/src/lib/app/app-provider.ts`) was restored to `HEAD` and the tree is clean. - -**SLICE-ARCH-F2, command by command.** `presentation.ts` gains a `completed()` helper (`tone: "ok"`) beside the existing `title()` (`tone: "info"`), and all nine now use it: `service build` ("Built the local service artifact."), `deploy` with no target ("Deployed 2 services."), `promote` ("Promoted dep_1 to production."), `rollback` ("Rolled hello-world back to dep_1."), `remove` ("Removed hello-world and every deployment it owned."), `domain add` ("Added shop.acme.com to hello-world."), `domain remove`, `domain retry`, and `open` ("Opened the live URL for the selected service."). Every one is pinned by a `presentedSummary` assertion on tone and text; the tests that needed it gained `isTty: {stdout: true}`, which is required because the human presentation is only materialized outside json format (`command-context.ts:33-54`, `shared-flags.ts:103-104`) and which cannot turn a run interactive, because interactivity keys off stdin alone (`shared-flags.ts:150-152`). The three that were already right are untouched in substance: `service deploy` and `service domain wait` were refactored into the same helper with the identical block, and `feedback` was not touched. The six remaining `title()` callers are all reads — `show`, `list-deploys`, `show-deploy`, `domain show`, and the two branch exceptions. - -**The two branch exceptions are genuine reads.** `service open` keeps `info` only when `result.opened` is false, which is the case where `ctx.openUrl` did not open anything; the text also moved to the past tense ("Resolved the live URL…"), so it no longer reads as in progress either. `domain add` keeps `info` only when `result.existing` is true, which is the API answering that the hostname was already on this service — nothing was added. Both are pinned. - -**The `alreadyLive` argument is right and covered.** `promote.ts:74` and `rollback.ts:77` already computed `alreadyLive`, and both already skip the SDK call under `if (!alreadyLive)`, so the old "Promoting a deployment to production." line was claiming an action that provably did not happen. The argument is now threaded to the presentation, which says "dep_2 was already live for hello-world." instead. Promote's already-live case was already tested and gained the summary assertion; rollback had no already-live case at all and gained one, with the warn diagnostic and `events: []` asserted beside it. - -**SLICE-ARCH-F1.** `deploy.ts:500-503` is now `throw fromLegacyCliError(projectDirectoryBindingErrorToCliError(bound.error))`, exactly the line the finding prescribed. `setup.ts` is unchanged, so `LocalResolutionPinSerializationError`, `LocalResolutionPinWriteAbortedError` and `LocalResolutionPinGitignoreUpdateAbortedError` still re-throw. `fromLegacyCliError` carries `meta` when it is non-empty (`errors.ts:86`) and turns `fix` into a user-choice action (`:65`), so `pinPath`/`gitignorePath` plus `operation` and the legacy fix advice are all restored, and the two distinct `why` sentences are told apart again. The code is unchanged at `SERVICE.LOCAL_STATE_WRITE_FAILED` and is now in the dispatch-2 mapping table. A new test injects the failure for real — it writes a *file* where `.prisma` belongs — and pins summary, `why`, `meta.pinPath`, `meta.operation` and the rewritten `service deploy --project` action. The one thing the fix did not weigh is SLICE-R2-F1. - -**SLICE-ARCH-F3, checked by breaking the code.** `vi.mock("../src/lib/app/app-provider", …)` is gone. The only remaining fake is a `ComputeClient` subclass overriding `deploy` — the compute-SDK upload the dispatch-2 note actually argued for — so `deployApp`'s own request assembly and response mapping run for real, and so do the other eight: `createProject`, `resolveBranch`, `createBranchDatabase`, `deleteBranchDatabase`, `listEnvironmentVariables`, `createEnvironmentVariable`, `updateEnvironmentVariable` and `deleteEnvironmentVariable`, each through a route in `deployRoutes`. The routes are not decorative: `fakeManagementClient` throws on an unrouted request (`v8-service-testkit.ts:116-119`), so every one of those calls provably reaches the wire. Two new tests exist purely to reach the two env-var methods that had no other caller (`PATCH` for an existing branch `DIRECT_URL`, `DELETE` for a stale one). The coverage is real rather than nominal: I replaced `liveDeploymentId: deployed.promoted ? deployed.deploymentId : deployed.previousDeploymentId` with the plain newest-deployment expression (`app-provider.ts:545-547`) and ran the suite — `v8-service-deploy.test.ts:473` fails, `expected 'dep_new' to be 'dep_old'`, and so does the provider's own `app-provider.test.ts:511`. I restored the file from `HEAD`. - -**SLICE-ENG-F4.** `MOUNTED_COMMANDS` in `cli.ts:41-75` is now the shipped map and `buildCli` passes it straight through (`:128`), so there is one copy. `v8-bin.test.ts:277-308` asserts `Object.keys(MOUNTED_COMMANDS).sort()` against a hand-written list of all 29 paths with `toEqual`, so a missing mount and an unexpected mount both fail — and because that list is written out by hand it is a second, independent statement of the truth, which is the point. A second new test runs `--help` for every mounted path through the real tree. The testkit derives rather than restates: `mountedCommands(groups)` filters the shipped map, `SERVICE_COMMANDS` is `mountedCommands(["service", "build"])`, and the agent and feedback suites call it too, so the three duplicate maps the finding named are gone. - -**SLICE-ENG-F2.** Both streams now attach `kind`, and `service logs` attaches `details` when the record has one. Read against the SDK's own types that is now the whole record: `TerminalRecord` is `{type, kind, code, message, retryable, cursor, details?}` and the port carries every field except `type`, with `message` in `line`; `build logs`'s terminal record has no `details` and the entry says so. Pinned both ways — a terminal error with `details` and one without. - -**SLICE-ENG-F3.** `:157` and `:370` now cite ruling 8 and, at `:157`, say plainly that ruling 8 names `--trace` and not `--verbose` — which is the wider-rule problem the finding raised, not just the number. The `agent` examples entry now cites the operator ruling of 2026-08-09 on `HelpSpec.examples`, and that file says exactly what the entry says it says (`engine-interface-draft.ts:787-790`). I re-checked every remaining rule citation in the file: `R-S2c-1` at `:12`, `:186`, `:382`, `:589`, `R-S2c-2` at `:391`, and `R-S2b-6` at `:111` and `:279` all name the rule the entry means. - -**SLICE-ARCH-F4, verified in the engine.** `explicitFlagKeys` scans argv for flag names and lines 77-82 mark the base flag on a `--no-` token; `buildCommandSnapshot` labels each declared flag `"cli"` or `"default"`. The snapshot is built once (`engine.ts:328`) and read in exactly one other place — the `onSettled` hook (`:303-310`) — so it genuinely never reaches `CommandContext`. Combined with the parsed boolean, the three states are distinguishable whichever way the flag's default falls. The rewritten entry says that and asks for an accessor rather than a new flag type. Accurate. - -**SLICE-ENG-F1 — I counted the 13 myself.** Every service command reaches `readAuthState` through exactly one path, and there are only three: `deploy.ts:466` calls `requireWorkspace` directly; `show`, `open`, `list-deploys`, `logs`, `promote`, `rollback` and `remove` reach it through `resolveServiceReadState` → `resolveServiceProjectContext:267` (promote, rollback and remove via `resolveServiceReleaseState`, `release.ts:35`); the five `domain` commands reach it through `resolveServiceDomainTarget:651`. That is 8 + 5 = 13. `show-deploy` is the fourth caller and swallows the failure — `requireWorkspace(ctx).then(id, () => null)` at `show-deploy.ts:52-55` — so it degrades to a missing live-deployment hint. `service build` declares no `needs` and reads no auth state; `build logs` declares `needs.credentials` but never touches the auth file, so the credential manager still serves it; `agent *` and `feedback` do neither. A repository-wide grep for `readAuthState` under `src/v8/` returns those four service call sites and the three `auth` commands, and nothing else. 13 is right. - -**The two deliberate deviations — both correct.** - -- **`LEGACY_CLI_NAME` for the three `deploy-target.ts` strings.** The reasoning holds and I checked the mechanism rather than the argument. Those strings are `nextSteps` on a legacy `CliError` that `fromLegacyCliError` then filters with `step.startsWith("prisma-cli ")` (`errors.ts:72`) before rewriting each survivor through `renameAppCopy` and `toV8CommandLine`. Substituting `CLI_NAME` would make the producer and the filter disagree the moment the binary is renamed, and the filter fails silently — the three actions would simply stop appearing. Keeping the legacy spelling on the input side and letting the rewriter produce `CLI_NAME` on the output side is the right place to draw the line, and it is drawn consistently: the constant is defined once (`errors.ts:34`), used at all five places that produce or match the legacy prefix, and no literal `prisma-cli ` remains anywhere under `src/v8/`. The companion change is the same judgement from the other side — `branch-database.ts:251` is plain advice that no rewriter touches, so it correctly became `CLI_NAME`. Every legacy `nextSteps` producer in `lib/` writes the literal, so nothing else can fall through the filter. One note, not a finding: `LEGACY_CLI_NAME` is a repo-wide fact about legacy copy that now lives in the service group's `errors.ts`; `cli-name.ts` is its natural home when S2d does the naming sweep. -- **The human-summary divergence bullet belongs.** Standing ruling 4 is a testing rule — it says assertions target the envelope, presented data, events and exit codes, and that a single golden suite pins rendering globally. It does not say user-visible differences from the shipping CLI go unrecorded, and standing ruling 10 says the opposite: divergences are enumerated for operator review, not discovered. This is a real difference on every human-mode run of twelve commands, and the bullet states it accurately — legacy did open each block with a present-progressive title, and the example it quotes is verbatim (`presenters/app.ts:763`, "Removing the selected app."). One bullet in the existing human-output section is the proportionate form; a per-command table would not be. - -**Observations, not findings.** - -- The new per-command summary assertions pin the exact sentence, not just the tone. They are assertions on presented data, which standing ruling 4 allows, and the tone is the thing the finding was about — but a copy edit to any of those nine lines now breaks a test in a different file. Worth knowing before the next copy pass. -- `mountedCommands(["agent"])` would return an empty map on a typo'd group name, and `v8-agent.test.ts`'s `needs.credentials` loop would then pass over nothing. Every other test in that file runs a real command through the same map and would fail loudly, so the risk is contained. - -### Final review (post-removal) — reviewer - -**Verdict: NOT SATISFIED.** One should-fix and two low findings, all three about what the reshaping left behind rather than about the seventeen commands themselves. Nothing I found argues against any of the three removals. - -**What was checked.** `git diff 691566e..HEAD` in full, and `git diff 9ffbb01..HEAD` for the shape of what remains. I did not re-read the seventeen surviving commands line by line; the four questions the brief asks were the whole scope. I ran `pnpm --filter @prisma/cli test` myself at `HEAD`: 965 tests across 72 files, green. - -**Question 1 — nothing dangling in the code, and the sweep was wide.** A search of `packages/`, `docs/` and the repository root for `service deploy`, `service build`, `service logs`, `service.deploy`, `service.build` and `service.logs` returns exactly six hits, all of them comments in surviving tests explaining why an action is absent (`v8-service-show.test.ts:87`, `v8-service-list-deploys.test.ts:60`, `v8-service-open.test.ts:117`, `v8-service-remove.test.ts:46`, `v8-service-rollback.test.ts:188`, `v8-service-domain.test.ts:174`). No help text, no example, no error copy and no next action names a removed command. The legacy shell's own `app deploy` / `app build` / `app logs` copy is untouched, which is right — it still ships those commands. - -Every error builder left in `errors.ts` has exactly one live caller; I checked all twenty-six by name. Every `SERVICE.*` code the source can emit — twenty-five of them, extracted from the source rather than from the record — appears in the divergence file's tables or its prose. The two `SERVICE.DEPLOYMENT_NOT_FOUND` variants that only `service logs` raised are gone and the two that survive are still raised, by `show-deploy.ts:43` and `release.ts:70`. `SERVICE.COMPUTE_CONFIG_TARGET_REQUIRED` was correctly deleted from the table: the only caller of `resolveComputeTarget` now passes `targetOptional: true` (`target.ts:174`), so `ComputeConfigTargetRequiredError` never converts to a settled error any more. `SERVICE.LOCAL_STATE_STALE` was correctly kept: it is still reachable from every read command through `projectResolutionErrorToCliError` (`packages/cli/src/lib/project/resolution.ts:288`, `:722-723`). The `io` tap added to `lib/app/build.ts` for `service build` is reverted byte for byte against `9ffbb01`, and the only operation-layer change the slice still carries is `removeApp`'s `progress` pass-through, which `service remove` needs. - -One loose end, too small to file. `listServices` (`target.ts:304`) lost its last importer when `deploy.ts` and `logs.ts` went and is now used only inside its own file, while the same commits took `export` off `readServiceEnvOverride`, `resolveComputeTarget` and the two environment-variable constants for exactly that reason. The testkit's `SERVICE_COMMANDS` (`v8-service-testkit.ts:299`) and `SERVICE_GROUPS` (`:279`) are the same case — their only outside importer was `v8-service-build.test.ts`. Three `export` keywords, no dead code behind any of them. `renameAppCopy`, `resolveServiceProjectContext` and `resolveExistingServiceSelection` are exported without importers too, but they were already that way before the removals. - -**Question 2 — the surviving code reads as designed, and the `resolveServiceReadState` simplification is honest.** The option and the field both went, and the remaining call is behaviour-identical: the old code computed `namedService = options.serviceName ?? compute.configServiceName` and passed it to `resolveExistingServiceSelection`; the new code passes the same expression inline (`target.ts:575-581`). `skipSelectionWhenUnnamed` was only ever passed by `logs.ts:193` and `namedService` only ever read by `logs.ts:41`, so no surviving command took either branch. The same is true of the `preloaded` option on `resolveComputeTarget` and of `computeTargetDirectory`, both of which had no caller left. `getCredentials` came off `ServiceContext` because nothing asks for it. `presentation.ts` and `results.ts` read as a group that lost two commands rather than as a group with holes in it: the `completed()` and `title()` helpers still have callers of both kinds, `deploymentNextActions` is still shared by promote and rollback, and the result types that went were the deploy-only ones. - -**Question 3 — the record is accurate, with the one exception in FINAL-F2.** I verified the counts against the code rather than reading them. Seventeen commands is right: `MOUNTED_COMMANDS` holds twenty-six paths, nine of which are S2a's `auth` and `telemetry`, and `v8-bin.test.ts:277` asserts that exact set. The ten removed next actions are right — six in `errors.ts` (`noDeploymentsError`, `releaseTargetRequiredError`, `noPreviousDeploymentError`, `domainTargetRequiredError`, `selectedServiceMissingError`, and the domain-add 422 branch of `domainCommandError`), three in `presentation.ts` (`show`, `list-deploys`, `remove`) and one in `list-deploys.ts`. The claim that all ten are pinned is right: each is covered by an exact-set assertion on the surviving actions, added in the same commit. The shelve-versus-drop distinction holds throughout — `service logs` is described as returning in S8 and `deploy` and `build` as not returning, and the S8 slice entry and the transport design exist. The list of what went with `service logs` matches what the diff removed, item for item. The "workspace comes from the engine's session" entry checks out where it matters most: the credential manager's reader does adopt the legacy `{tokens: […]}` shape into sessions (`packages/cli/src/auth/legacy-state.ts:19-40`), which is what makes the eleven surviving commands survive the merge-down, and the arithmetic of "of the 13 that broke, 11 still ship" is correct. Three retired escalations are correctly labelled; the third open one is not, which is FINAL-F2. - -**Question 4 — the matrix still holds; the coverage that went is shared code, which is FINAL-F1.** Every surviving command still has success, errored, json envelope and unauthenticated cases; the picker is still proven once, on `service show`; both consent points still have grant, decline, non-interactive, wrong-token and `--yes` cases. No survivor lost an axis, because each of the three deleted commands owned its own suite. Removing the deploy next actions was pinned rather than merely done, in all ten places. What did weaken is not per command: the rename surface, the legacy-error mapper and the compute-config resolution were only ever driven by the deleted suites, and all three still run on the eleven shipped service commands. - -**Not findings — for the orchestrator.** - -- **The dispatch plan was not updated with the contract.** `plans/s2c-services.md:7,12,17` still hands dispatches the `service build|deploy|logs` scope. The brief rules the contract's stale scope out of bounds because S2d consolidates it; the plan is the same class of document and I am flagging it for the same treatment rather than filing it. -- **A service token with no workspace claim now resolves to an empty workspace id.** `requireWorkspace` takes `session.workspaceId` (`target.ts:88-99`), and the manager composes an environment session as `workspaceId: serviceTokenWorkspaceId(token) ?? ""` (`packages/cli/src/auth/credential-manager.ts:344`). Where the legacy reader asked the Management API for the workspace behind `PRISMA_SERVICE_TOKEN`, the new path derives it from the token's own claims and falls back to an empty string. The divergence entry records the name half of this honestly ("presents as its workspace id") and does not reach the case where there is no derivable id at all. `packages/cli/src/auth/**` is a hard boundary for this slice and is untouched by it, so this is the auth stream's to answer, not a finding here. -- **FINAL-F3 is a section I did not edit deliberately.** The scoreboard is reviewer-maintained, but the brief scoped my write to appending this note, so I filed the correction rather than making it. - -### Merge review (rev-6 credential surface) — reviewer - -**Verdict: NOT SATISFIED.** Three should-fix and two low findings. None of them says the merge got the adaptation wrong. The one change that mattered — how `requireWorkspace` treats a credential that names no workspace — is correct, and it closes a defect this branch was carrying before the merge. What is missing is the record around it, a test for the branch it added, and a harness weakness the merge walked into and left in place. - -**What was checked.** `git log --oneline --merges -2` and `git diff 6d80563...HEAD` in full, plus `git diff fec6678..HEAD` to see the merge from the base's side. I ran all four checks at `HEAD` myself: `pnpm --filter @prisma/cli test` is 987 tests across 73 files, `pnpm --filter @prisma/cli-engine test` is 256 tests across 14 files (the brief's 234 was the pre-merge count; the base brought more), `pnpm typecheck` and `pnpm lint` both clean. Outside `packages/cli/src/v8/auth/**`, the merge touched exactly two source files: `service/target.ts` and `v8/runtime.ts`, the latter only to drop the deleted `getCredentials` wiring. - -**Question 1 — the new refusal, measured against the legacy source.** The answer is in MERGE-F1 and it has three parts. On the path legacy actually took offline, the behaviour is identical: `readServiceTokenAuthState` decoded a service token's claims, found no workspace, returned signed-out state, and the old `requireWorkspace` threw `SERVICE.WORKSPACE_REQUIRED` — which is the same error the merged code throws from the same builder. Against the state of this branch one commit earlier, the merge is a repair: `ctx.session()` gave an environment token with no workspace claim an empty-string workspace id, and the run continued with it. One difference from legacy remains: legacy asked `GET /v1/me` before it read the claims and used the workspace the server named, so a token the platform can place but whose JWT cannot is now refused where it used to work. That difference entered at `691566e`, not at this merge, but the merge is the first commit at which the "no workspace" case has a behaviour of its own, and the divergence file still describes `SERVICE.WORKSPACE_REQUIRED` as firing only when there is no session. - -**Question 2 — nothing was dropped and nothing was applied twice.** The three command removals hold: `src/v8/service/` has no `deploy.ts`, `build.ts` or `logs.ts`, `MOUNTED_COMMANDS` has no mount for them, and a search of `packages/cli/src/v8` for `service deploy`, `service build` and `service logs` returns nothing. The removed next actions are still removed. The restored coverage from `6d80563` is byte-identical at `HEAD` — `git diff 6d80563 HEAD` over `v8-service-compute-config.test.ts`, `v8-service-remove.test.ts` and `v8-service-domain.test.ts` is empty. The `v8-bin.test.ts` conflict was resolved correctly in both directions: the base's deletion of the `makeGetCredentials` tests and the `FileTokenStorage` module mock survived, and the branch's exact-set mount assertion and per-path help run sit on top of them; the assertion's 26 paths match `MOUNTED_COMMANDS` exactly and the test passes. Coming from the other side, the base changed only one file inside the branch's area — that same test — so there was nothing else to revert. `git diff fec6678..HEAD` shows deletions in four files only: `plan.md`, `cli.ts`, `app-provider.ts` and `v8-bin.test.ts`, all of them the branch's own work. - -**Question 3 — stale references.** Outside the two auth trees, `ctx.session()` and `getCredentials` appear nowhere in `packages/cli/src`, `packages/cli/tests` or `packages/cli-engine`. Every remaining `currentWorkspaceId` is the credential file's own field name, which is still spelled that way on disk (`packages/cli/src/auth/state-file.ts:27-35`), so those are correct. The `prisma-cli app *` strings in `packages/cli/src/shell/command-meta.ts` belong to the commander shell, which still ships those commands. What is stale is in the divergence file, and it is MERGE-F5: the retired log-stream section was half-renamed, keeps present-tense claims about a deleted accessor, quotes text that no longer exists in the file it cites, and cross-references a heading the same commit renamed. - -**Question 4 — the harness.** The seed rename is fixed, but the reason it could be ignored is not, and that is MERGE-F3. The seed reaches `createTestCli` inside a spread, and TypeScript does not apply its excess-property check to spread properties — I proved that against the repo's own compiler rather than asserting it. So the same silent failure is available to the next rename. I looked for other places the harness could fail open and found none: the fake Management API throws on an unrouted request instead of falling back, and the unauthenticated axis is real — no seed means the manager pins to "none" and returns null, and `PRISMA_SERVICE_TOKEN` only enters a run's environment when an environment credential is seeded, which the service testkit never does. The gap that is left is coverage rather than fail-open: the branch the merge added cannot be reached from `makeServiceCli` at all (MERGE-F2). - -**Question 5 — the plan resolution is honest.** I diffed the base's S8 section against the merged one: the base's text is preserved to the byte and one numbered item was added. The dependency graph is the base's, unchanged, and the branch's competing `S8 (after S2c; …)` line is gone rather than duplicated. Both documents' other sections survive — the branch's "Follow-ups parked on other work" and the base's "Coverage ledger" are both present. The added question is accurate about why `service logs` was shelved: it matches the shelving commit's own account (WebSocket upgrade, HTTP-only engine client, the port reaching for a raw token, credentials never reaching commands), the design it points at exists, and its §7 does ask the plain-HTTP question the fold says it asks. The one error is the count — MERGE-F4. - -**Not findings — for the orchestrator.** - -- **The test harness's credential manager derives a workspace from fewer tokens than the shipping one.** `InMemoryCredentialManager` reads only the `workspace_id` claim (`packages/cli-engine/src/in-memory-credential-manager.ts:86-90`), while `FileCredentialManager` uses `serviceTokenWorkspaceId`, which accepts `workspace_id` or a `sub` of the form `workspace:` (`packages/cli/src/auth/claims.ts:33-43`). A service token that names its workspace only through `sub` therefore resolves in production and reports no workspace under test — the harness would show a refusal the product would not make. `packages/cli-engine/**` is a hard boundary, so this is a note rather than a finding, but it matters directly to MERGE-F2: a test written against the harness today would pin the wrong answer for that token shape. -- **The same function can return an empty string where its interface says it never will.** `ActiveCredential.workspaceId` is documented "Never the empty string" (`packages/cli-engine/src/credential-manager.ts:62-65`), and the shipping derivation enforces it (`claims.ts:26-28` requires a non-empty string). The engine's test manager does not — it accepts any string, `""` included. Nothing is broken today, because `requireWorkspace` tests the value for truthiness rather than for `undefined`, but the harness can construct a credential the interface forbids. Also a hard-boundary note. -- **`SERVICE.WORKSPACE_REQUIRED`'s advice needs a second next action either way.** MERGE-F1 asks for it, and `errors.ts` is inside this slice, so it can be fixed here — but the condition it fires on is an environment-credential state owned by the auth stream, so the wording should probably be agreed with them rather than settled unilaterally. - -## Orchestrator notes (orchestrator-owned) - -- 2026-08-10: slice started. Branch cut from bot/s2b-resources @ 01c8183 - (S2b has pushed no command work yet — S2a auth family is the layout - precedent until the first merge-down). -- 2026-08-10: D1 SATISFIED after two rounds. Operator consent ruling - (post-D1): consent becomes engine-owned — token per consent point, - type-to-confirm rendering, global `--confirm ` grant. D2 ports - consent points as plain prompt.consent (no skip flags); D1's boolean - `--confirm` on domain remove migrates off on the merge-down that - brings the engine mechanism. -- 2026-08-10: implementer swapped Fable → Opus at D2 start - (operator-requested, rate-limit headroom). Fresh subagent; same - conventions via D1's code + this file. -- 2026-08-10: operator ruled Q2: `app run` is DROPPED, superseded by - Composer's commands. No v8 port, no exit-code passthrough mechanism, - no legacy carve-out needed in S2d (the shell deletion takes it). - D4 records the drop as a divergence entry. The slice's "parked" - scoreboard row resolves to "ruled: dropped". -- 2026-08-10, continuation orchestrator picks the slice up after a rate - limit. Baseline reproduced green (977 tests) only after `pnpm build` — - the engine and telemetry packages must be built before vitest can - resolve them. D3's review was run (round 1, NOT SATISFIED, five - findings), D4 was implemented and committed at 1004 tests, and D4's - review was run (round 1, NOT SATISFIED, two findings, both about the - record rather than the code). -- 2026-08-10: **merge-down from `bot/s2a-foundations` deliberately - held.** That branch moved five commits ahead of our base, and its tip - (`4b006d1`) says in its own message that no test suite has been run - against it and its implementer was halted mid-work. Merging it would - make this branch's verification meaningless. Our base `9ffbb01` is - still an ancestor of that tip, so the PR against `s2a-foundations` - shows only our commits. -- 2026-08-10, **UPGRADED after SLICE-ENG-F1: the merge-down is a - compatibility decision, not a scheduling one, and the defect is - S2a's rather than ours.** The auth rework on that branch changes the - credential *writer* — `auth login` now calls - `credentialManager.createSession`, which writes `{version, sessions, - currentWorkspaceId}` — but leaves the *reader* - `readAuthState` untouched (`packages/cli/src/auth/operations.ts` is - byte-identical between `9ffbb01` and `bot/s2a-foundations`), and that - reader still goes through `FileTokenStorage`, which parses - `data.tokens || []`. - So on the merged result, a signed-in user without - `PRISMA_SERVICE_TOKEN` reads back as unauthenticated. In this slice - that is **13 of the 20 commands**, not just `service logs`: `deploy`, - `show`, `open`, `list-deploys`, `logs`, `promote`, `rollback`, - `remove` and all five `domain` commands, every one of them through - `requireWorkspace` → `readAuthState` - (`packages/cli/src/v8/service/target.ts:93-101`, reached by - `resolveServiceProjectContext:267`). - (An earlier draft of this note said 14 and counted `show-deploy`. That - was wrong, and the fix-round implementer caught it by reading the code - rather than deferring to the note: `show-deploy` is the one caller - that swallows the failure — - `requireWorkspace(ctx).then(id, () => null)`, - `packages/cli/src/v8/service/show-deploy.ts:51-55` — so it degrades to - a missing live-deployment hint instead of failing. It uses neither - shared resolver, so that is its only auth path.) - `service build`, `build logs`, the three `agent` commands and - `feedback` are unaffected because they never read auth state. - `readAuthState` is shared, so the blast radius is not confined to this - slice — any other ported group calling it breaks the same way. - **No test in this slice can see it**: every service test mocks - `readAuthState` at the module seam, so the harness has two credential - seams where production has one file. Whichever seeding shape the - merge-down adopts must join them. - This is for the S2a stream to fix before its branch goes green — the - correction is in `readAuthState`, not in this tree. Do not merge down - until it is fixed. -- 2026-08-10: **D3-R1-F1 adjudicated against the reviewer's conclusion** - — see the block under the finding. The dispute was settled by running - the credential surfaces rather than reading them. Both the reviewer - and the outgoing orchestrator's brief were each right about a - different code state: `service logs` streams on this branch and breaks - on the very next merge-down, because `auth login` changes the - credential file's shape there. The interim error and its test stay; - only the wording changes. -- 2026-08-10: **the in-scope command count is 20, not 24.** The - contract's headline counts the four `service env *` commands that its - own scope note then assigns to `project env` in S2b, and `service run` - is ruled dropped. Nothing is missing; the headline and the scope note - disagree with each other. Worth one correction in the contract at S2d - consolidation. -- 2026-08-10: **the D2 agent-setup prompt entry now reads "final", and - its provenance is this ledger, not a recorded ruling.** The outgoing - orchestrator's continuation brief records that the operator was told - and did not object, and instructs the next dispatch to record it as - final unless overruled; D4 did so on that instruction. Flagged to the - operator for an explicit yes or no at PR time. (D4's reviewer raised - the same point; the entry's factual claim checks out either way.) -- 2026-08-10: **three groups now ship without a command family** - (`agent`, `telemetry`, and `build logs` until D3-R1-F4 is cleared). - A family carries only an optional config section and docs base URL, - neither of which any family in this CLI sets, so being familyless has - no behavioral effect today — but the engine's own comment - (`packages/cli-engine/src/execution/command-tree.ts:134-135`) calls - familyless commands "unowned", which reads as though the mode is not - meant for shell use. One decision for the whole tree, above this - slice: either the shell declares a family for local utilities, or the - engine states that familyless is supported. -- 2026-08-10, **operator rulings that reshaped the slice from 20 - commands to 17.** `service deploy` and `service build` are **dropped** - — deploy conflated local compiling with uploading a tarball, Composer - supersedes both, and commands are to work directly with platform - Compute resources. Not a deferral. `service logs` is **shelved**, - which is different: it returns once the engine can open an - authenticated socket (design in - `../assets/engine/websocket-transport-design.md`, folded into the - base's own S8 slice). `service run` was already ruled dropped. The ten - next actions that pointed at `service deploy` are removed rather than - left dangling, with the follow-up to repoint them at Composer parked - in the project plan. The fixture-test deviation is **approved**: the - commander shell and its tests are deleted together in S2d. -- 2026-08-10: **the branch is merged onto `bot/s2a-foundations`** at the - operator's instruction, and all CI checks pass. The base had landed - the rev-6 credential surface, which deleted `getCredentials` and - renamed `ctx.session()` to `ctx.activeCredential()`. Two things worth - carrying forward. The rename of the harness seed - `currentWorkspaceId` → `selectedWorkspaceId` was **silently ignored** - rather than rejected, so the first merge produced 104 failing tests - with no clue why; the testkit no longer passes seeds through a spread, - so the same rename now fails typecheck at the line that made it. And - the base's own S8, "Service primitives", collided with the WebSocket - slice added here — theirs is the real one and already asks where log - reading belongs, so the socket design folds into it as a fourth - question rather than competing with it. -- 2026-08-10: **the engine gap count is seven raised, three retired, - four open**, and the divergence file carries exactly that many marked - headings so the count can be checked rather than trusted. The three - retired ones died with the commands that needed them. diff --git a/.drive/projects/prisma-cli-v8/spec.md b/.drive/projects/prisma-cli-v8/spec.md deleted file mode 100644 index c3800d91..00000000 --- a/.drive/projects/prisma-cli-v8/spec.md +++ /dev/null @@ -1,158 +0,0 @@ -# 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 their command families - (`CommandFamily`: 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 (`composer deploy|dev|log|destroy`, stubs where ruled — a - subgroup is owned by exactly ONE command family; mixing management-API - and Composer commands in one subgroup is ruled out (operator, - 2026-08-10). Interim parking (operator, 2026-08-10): Composer's - commands live under a `composer` root, the platform's cloud-project - CRUD keeps `project`; the final grammar — including whether the - old "no composer-named surface" goal returns — stays open as - TML-3189; moving the tree later is cosmetic), 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 - their command families; 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 is never a package manager (R13, amended 2026-08-11); optional - capability = optional peer dependency + engine-phrased error. A command - may run the USER's manager, in the user's project, at their request, - through the engine's package operations — visible command, structured - failure, bin-owned execution. -- 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/command-grammar-cleanup.md b/.drive/projects/prisma-cli-v8/specs/command-grammar-cleanup.md deleted file mode 100644 index 98572525..00000000 --- a/.drive/projects/prisma-cli-v8/specs/command-grammar-cleanup.md +++ /dev/null @@ -1,123 +0,0 @@ -# Command grammar cleanup (slice contract) - -Status: rev 1 (2026-08-21). One PR into `main`. Repo: prisma-cli only. Source: the PM command-review brief (2026-08-20/21), reproduced in the operator's message; this spec adds the grounded design decisions, not new scope. No new commands; this pass removes, renames and moves existing ones. - -## At a glance - -The shell owns the mount table (`packages/cli/src/cli.ts`), so most of the work is mount-table edits plus every string that names a command: help summaries, examples, `run-command` next actions, error copy, tests, docs. Four behavioural changes ride along: the compute config (`prisma.compute.ts`/`.json`) and `init` are removed outright; service commands stop using ambient context (remembered selection, interactive picker, git-branch inference); six `remove` commands become `delete`; and the composer/build groups dissolve. - -## Chosen design - -### 1. Compute config and `init` removal - -- Delete `src/commands/init/` (all files), `src/types/init.ts`, the `init` mount, its help presence, `tests/init.test.ts`, `tests/init-agent-setup.test.ts`, and the e2e coverage entry. -- Delete `src/lib/app/compute-config.ts`, `build-settings.ts`, `deploy-framework.ts`, `build.ts`, and whatever only the config path reached (follow the import graph; `tests/compute-config.test.ts`, `tests/service-compute-config.test.ts`, `tests/app-build.test.ts` and friends go with their subjects). Drop the `@prisma/compute-sdk/config` import if nothing else needs it. -- `src/commands/service/target.ts`: delete `resolveComputeTarget`, `resolveComputeManagementContext`, the config-target positional (`[service]`) on every service command, and the `SERVICE.COMPUTE_CONFIG_INVALID` / `SERVICE.COMPUTE_CONFIG_TARGET_UNKNOWN` error codes plus their helpers (`configTargetRequiresConfigError`, `computeConfigErrorToCliError` call sites) in `service/errors.ts`. -- `src/lib/agent/setup-status.ts`: remove the compute-config read; agent status no longer depends on a config file. - -### 2. Service commands take parameters only - -The only ambient context a platform command may use is the directory's project link (`.prisma/local.json`). - -- **Service targeting:** `--service ` (matched by name) or `PRISMA_SERVICE_ID` (matched by id — the existing domain-flow mechanics generalized to every service command that targets a service). Neither present → a structured error naming `--service`, exit 2, in interactive terminals too. Delete `resolveExistingServiceSelection`'s saved-selection branch and its `ctx.prompt.select` picker; delete `rememberSelectedService`, `LocalStateStore.readSelectedApp` / `setSelectedApp` / `clearSelectedApp` and the `selectedByProject` state shape (`service delete` loses its local-state cleanup with it). -- **Branch:** `--branch ` only; when absent, the existing non-git fallback stands (`"main"` for read flows, `"production"` for the domain flow). Delete `resolveRequestedBranch`'s git inference and `src/lib/git/local-branch.ts` + `tests/local-branch.test.ts` if nothing else imports it. -- **Project:** unchanged (`--project`, `PRISMA_PROJECT_ID`, link file). `project link` keeps its interactive picker. - -### 3. `delete` destroys, `remove` detaches — renames, no aliases - -`project remove`→`project delete`, `project env remove`→`project env delete`, `postgres remove`→`postgres delete`, `postgres connection remove`→`postgres connection delete`, `service remove`→`service delete`, `service domain remove`→`service domain delete`. Rename files, exported symbols, command ids, help, examples, next actions, error copy, consent questions, and tests (unit + e2e `describeCommand` markers). `git disconnect`, `auth … logout`, `bucket delete`, `bucket key delete` unchanged. - -### 4. Moves - -| was | is | -| --- | --- | -| `postgres restore` | `postgres backup restore` | -| `ref list` / `ref set` / `ref delete` | `migration ref list` / `set` / `delete` | -| `migrate` | `db migrate` | -| `format` | `contract format` | -| `composer dev` | `dev` | -| `composer deploy` | `deploy` | - -No aliases, no redirects for old spellings; an old spelling settles the engine's unknown-command error. - -**Family wrapping (the shell-side mechanism).** Both external families are re-wrapped in `cli.ts` with `defineCommandFamily`, preserving `configSection` and `docsBaseUrl`: - -- **Composer:** keep only `deploy` and `dev`; `destroy` and `log` are dropped commands, and dropping them from the wrapped family is what keeps mount-coverage's "mounts every family command" check honest. -- **ORM:** commands pass through unchanged (the mount table respells their paths). Redirects are rewritten: the `migration ref` → `ref` entry is dropped (mounting it as-is would redirect a now-live spelling), and the `migration apply` entry's replacement `{bin} migrate --to ` is respelled `{bin} db migrate --to `. The `migration status` flag redirects name live commands and pass through. - -### 5. Removals - -`composer destroy`, `composer log`, the `composer` group and its brief, and the `build` group (`build logs`, `src/commands/build/`, `tests/build-logs.test.ts`, the `build` group brief). - -### 6. Housekeeping in the same PR - -- `tests/mount-coverage.test.ts`: `EXPECTED_MOUNT_PATHS` becomes the acceptance tree below; `initCommand` leaves `FAMILYLESS`; the header's `init` ruling note is updated (the 2026-08-12 ruling is superseded by the 2026-08-21 PM review). -- `tests/e2e-coverage.test.ts` exclusions/backlog entries respelled; e2e `describeCommand` markers respelled; no command loses its coverage status silently. -- Root help examples (`cli.ts` `help.examples`): drop `init`; use live spellings (e.g. `auth login`, `project list`, `deploy`). -- Every `run-command` next action, help example, and error string naming an old spelling, across `packages/` (the sweep inventory in the slice plan is the checklist). -- README / docs pages that enumerate commands. -- Divergence records under `.drive/projects/prisma-cli-v8/assets/s2/` get a short entry each for the renames and moves; `assets/command-review.md` (currently only on branch `claude/command-review-divergences-17ee99`, commit 76a2c8a) is brought into this branch and regenerated against the new tree. - -## Coherence rationale - -One reviewer can hold this in one sitting because the change is one rule applied uniformly: the grammar moves, and every string follows. The three genuinely behavioural pieces (compute-config removal, parameter-only service targeting, family wrapping) are each small and local; the rest is mechanical renaming verified by the grammar completeness test and the full suite. It rolls back as one unit — a partial landing would leave the help, the tree, and the docs disagreeing, which is exactly what the single-PR rule prevents. - -## Scope - -**In:** everything above, in this repo. - -**Deliberately out:** new commands (`project unlink`, `service domain list`, `bucket show`, `contract validate`, `auth token *`); the `app` → `service` control-plane API rename (CLI keeps calling `/v1/apps`); any ORM command behaviour change (only mount paths move); changes to `@prisma/composer-cli` or `@prisma/orm-toolchain` packages themselves (the shell wraps, upstream cleanups are follow-ups for those repos). - -## Pre-investigated edge cases - -- The ORM family ships a live redirect table (`cli.mjs`: `migration apply`, `migration ref`, three `migration status` flag entries). Mounting it unwrapped would point users at spellings this PR retires (`migrate`) or capture a spelling it revives (`migration ref`). Hence the wrap in §4. -- mount-coverage's family-completeness check fails on any family command the tree does not mount — dropping `composer destroy`/`log` therefore requires the wrapped composer family, not just mount-table deletion. -- `PRISMA_SERVICE_ID` matches by id, `--service` by name; the existing domain-flow behaviour is the model. Do not conflate them. -- `service create` names its service with a flag/argument already and does not resolve an existing target; the picker removal must not break it. -- Composer's help examples read `{bin} deploy …`; the root move makes them correct by itself. Do not "fix" them in the composer package. - -## Slice-specific done conditions - -- The mounted tree equals the acceptance tree below (mount-coverage green asserts it). -- `grep -rn` across `packages/`, `README.md`, `docs/` finds no remaining old spelling used as a command reference (excepting historical records: `.drive/` process artifacts, changelogs). -- Full pre-commit verification per AGENTS.md, including `pnpm --filter @prisma/cli test:e2e` (credentialed run if the env provides `PRISMA_E2E_SERVICE_TOKEN`; otherwise the suite's skip is reported, not hidden). - -## Acceptance tree - -``` -auth login | logout | whoami | workspace list|use|logout -project list | show | create | link | rename | delete | transfer | env add|update|list|delete -postgres list | show | create | usage | delete | backup list|restore | connection list|create|rotate|delete -bucket list | create | delete | key list|create|delete -branch list -git connect | disconnect -service list | create | show | open | logs | delete - deployment list|show|promote|rollback|start|stop|delete - domain add|show|delete|retry|wait -dev | deploy -contract emit | infer | format -db init | schema | sign | update | verify | migrate -migration plan | new | list | show | status | log | graph | check | ref list|set|delete -orm init -lsp -agent install | update | status -telemetry status | enable | disable -feedback -``` - -## References - -- Brief: operator message 2026-08-21 (this spec's source of scope). -- `packages/cli/src/cli.ts` (mount table), `packages/cli/tests/mount-coverage.test.ts` (grammar check), `packages/cli/src/commands/service/target.ts` (ambient context), `packages/cli/node_modules/@prisma/orm-toolchain/dist/cli.mjs` (shipped redirect table), `@prisma/composer-cli/dist/family.mjs` (composer family shape). - -## Amendment (2026-08-21, operator ruling on PR #218) - -§2's `--service ` targeting is superseded: any command that operates on a subject resource takes that resource's identifier as its first positional argument (recorded as "Subjects are positional" in `docs/product/command-principles.md`). Concretely: `service show|open|logs|delete ` and `service deployment list|rollback ` take the service name as an optional positional (PRISMA_SERVICE_ID stays as the env fallback; neither present is still the SERVICE.TARGET_REQUIRED refusal). `service deployment promote|start|stop|delete ` and `service logs --deployment ` are targeted by the globally-unique deployment id alone, resolved the way `service deployment show` always was — they take no `--service`, `--project`, or `--branch`, and their results carry no `projectId`. `project show [id-or-name]` follows the same rule. Domain commands keep `--service` as a scope flag: their positional is the hostname, and the management API has no global hostname lookup. - -## Amendment (2026-08-21, ADR-012 vocabulary) - -pdp-control-plane ADR-012 retires "Deployment" as a noun (a deploy produces a Version) and renames App to Service across every surface. The CLI adopts it now, pre-rc, on this branch: `service deployment *` mounts become `service version *`, `service logs --deployment` becomes `--version-id` (`version` is an engine-reserved flag name), JSON result fields respell (`deployment`→`version`, `deploymentId`→`versionId`, `liveDeployment`→`liveVersion`, `recentDeployments`→`recentVersions`, `previousLiveDeploymentId`→`previousLiveVersionId`, `liveDeploymentId`→`liveVersionId` on list entries), error codes respell (`SERVICE.DEPLOYMENT_*`→`SERVICE.VERSION_*`, `NO_DEPLOYMENTS`→`NO_VERSIONS`, `NO_PREVIOUS_DEPLOYMENT`→`NO_PREVIOUS_VERSION`, `LIVE_DEPLOYMENT_UNKNOWN`→`LIVE_VERSION_UNKNOWN`), progress steps respell (`stop-deployments`→`stop-versions`, `delete-deployments`→`delete-versions`), and all help/error prose says "service version" (qualified, per the ADR; example ids use the real `cpv_` prefix). The wire layer deliberately keeps platform vocabulary until the platform's own coordinated rename: `/v1/deployments` paths, compute-sdk names, `appId`, and the adapter in `packages/cli/src/lib/app/app-provider.ts`, which is the seam where the two vocabularies meet. - -## Amendment (2026-08-22, operator ruling: no env-var targeting, ids primary) - -§2's `PRISMA_SERVICE_ID` mechanism is removed, superseding the earlier subjects-positional amendment's "env fallback" clause. The env var was a hidden second targeting mode — it matched by id while the argument matched by name — and the workflow it served (the headless deploy pipeline) no longer exists. Instead, the service argument itself accepts an id or a name with one visible rule: the stable platform id is primary, the name is the secondary fallback, and an id match always wins. The same precedence now applies to the project and database matchers, and the general rule is recorded under "Subjects are positional" in `docs/product/command-principles.md`. `SERVICE.SELECTION_INVALID` is the one not-found error for either form; the env-specific error and advice copy are gone. diff --git a/.drive/projects/prisma-cli-v8/specs/config-file-resolution.md b/.drive/projects/prisma-cli-v8/specs/config-file-resolution.md deleted file mode 100644 index 2934fe7e..00000000 --- a/.drive/projects/prisma-cli-v8/specs/config-file-resolution.md +++ /dev/null @@ -1,84 +0,0 @@ -# Config-file resolution: ancestor discovery and per-key merging - -Status: **landed** (operator rulings 2026-08-25; implementation landed 2026-08-25). This file was the design discussion; it is now the slice contract. The discussion history survives condensed under "Design decisions"; the appendix's edge-case catalogue is carried forward as implementation requirements. - -## At a glance - -`prisma.config.ts` resolution grows from "one file in cwd" to "a chain of files discovered upward", merged per key with the most-local value winning — the ESLint/webpack-merge model users already know. The primary layout served: one config at the repository root (deploy target, platform settings), one in the `db` package (ORM settings) — already the mainstream monorepo pattern and part of Composer best practices. Discovery is automatic; an explicit `parent` key overrides it. The hand-rolled skills-config reader consolidates into the same resolver, and `prisma init` in a subdirectory scaffolds config only. - -## Decided behavior - -### Discovery - -- From the anchor directory, search upward collecting every `prisma.config.ts` on the path. The anchor is cwd, or with `--config ` the named file's own directory (the named file is the nearest layer; its ancestors and `parent` declarations apply as usual — never cwd's lineage). -- The search stops at the repository boundary: the first directory containing `.git`. No `.git` found anywhere above → the anchor directory only. Filesystem root and home directory are never reached implicitly. Rationale: every file on the chain is *executed* TypeScript; nothing outside the repository runs without explicit consent. -- A file may declare `parent: false` — "I am the root", collection ends here (supersedes the earlier `root: true` design; one field, not two) — or `parent: "path"`, naming its parent explicitly. An explicit `parent` path may cross the repository boundary (git submodules); crossing is the consent. Absent `parent` means automatic discovery continues. A `parent` chain must be cycle-checked. -- `parent` is an engine-reserved top-level key like the `$prismaConfig` marker: read by the loader, never a section, rejected as a section name at tree construction (`reservedConfigSectionName` + `rejectReservedSectionName`). -- Automatic discovery cannot be replaced by `parent`: the common case is a subdirectory with **no config file at all**, which has nowhere to declare a parent. `parent` only overrides what discovery would have chosen. - -### Merging - -- Sections resolve **per key, most-local value winning**, over the discovered chain. A nested file shadows only the keys it actually writes; a root-level `skills: { check: false }` reaches every subdirectory that does not override it. -- Merge semantics are owned by the section type: `ConfigSection` gains an optional `merge(parent, child)` and the owner decides array/atomic behavior. The engine default, for sections that do not customize: per-key merge at the section's top level, replace below. -- No shadowing notices. Overriding parent config is the mechanism working as intended; introspection belongs in verbose output, not per-run warnings. -- `definePrismaConfig` freezes its result; merging constructs fresh objects and never mutates a file's export. - -### Validation and provenance - -- Per-file checks: evaluation, marker, version, and the unknown-top-level-key check run for **every** file on the chain — a typo'd key in a nested file errors even when the command's sections resolved elsewhere. A broken file anywhere on the chain fails the command with an error naming that file; no skipping. -- Required-key/section validation runs **after** the merge, on the resolved view — a child file may hold a valid partial that the parent completes. Validators keep their current contract (own absence, never throw); the ORM section's "required" absence-error now fires only when no file on the chain supplies the section, which is the intended semantics. -- Every resolved value carries provenance (which file contributed it), so post-merge validation errors and diagnostics name the file to fix, and: -- **Relative paths resolve against the file that declared them**, never against cwd or the nearest config. A root config's `orm: { migrations: "./migrations" }` means the root's `migrations` directory from anywhere in the repo. - -### Consolidation - -- `readProjectSkillsConfig` and every other out-of-handler config read (the skills staleness notice, the post-login tip) goes through the engine resolver. The hand-rolled `existsSync(cwd/prisma.config.ts)` + direct `loadConfig` path is deleted; there is exactly one resolution behavior in the product. - -### `prisma init` in a subdirectory - -- `init` acts on cwd with no special-casing of the scaffold itself (init at root makes the root config; init in `packages/db` makes the package config; discovery wires them together). -- When init runs in a directory that has an ancestor config on the discovered chain (a subdirectory init), the skills sync, the `postinstall` script, and the `prisma` devDependency additions are **skipped by default** — those belong to the repository root. Explicit flags may still opt in. Root init behavior is unchanged. - -## Design decisions (condensed history) - -- **"Highest file wins"** (with `root: true`): rejected — ignores a `db` package's ORM config entirely; briefly implemented in the engine (commits `ba48d46`, `f72503c`, removed by `9b2f9d0`), recoverable from history as reference. -- **"Nearest file wins"**: rejected — root-scoped commands (`deploy`, `project link`) break from inside packages. -- **Per-scope declarations** (sections declare root- vs nearest-scoped): rejected as redundant — where a section is written already encodes it. -- **Atomic per-section resolution** (nearest definition of a section wins whole): superseded 2026-08-25 by per-key merging — with merge semantics delegated to typed section owners, merging does what users of ESLint/tsconfig already expect, and partial overrides (`skills.check` at the root, `skills.agents` in a package) work. -- **`export default merge(parentConfig, {...})`** as the layering mechanism: rejected — moves resolution into user code, defeating loader-controlled ordering, caching, error attribution, per-file validation, and the boundary rule. Fine as userland sugar; not the contract. -- **`--config` reads only the named file**: rejected in favor of the named file anchoring the normal chain. - -## Implementation requirements (carried from the prior round's review) - -- Resolve the search's starting directory through symlinks so errors name real paths. -- Keep loader tests anchored so a stray `prisma.config.ts` in a real ancestor of the checkout cannot leak into them — this repository itself will contain fixture configs; the test harness must pin the chain. -- Enforce reserved-key handling on the engine side of the pluggable-loader boundary, not only inside the default loader. -- Name the offending value (and now its file) in validation errors. -- Windows: realpath both sides of any path comparison (the loaded-file identity check already does; the chain comparisons must too). - -## Scope - -**In:** engine loader (chain discovery, `parent`, boundary stop, per-file checks), `LoadedConfig` shape change and everything downstream of it (`needs.ts`, hosts, tests), `ConfigSection.merge` and the default merge, provenance, declaring-file-relative path contract, skills-reader consolidation, init subdirectory behavior, user docs, ledger updates. - -**Deliberately out:** ORM and composer `merge()` customizations (upstream packages; the engine default covers them), any change to their validators, the topology of which sections exist, performance work beyond bounded-depth evaluation (cache within a run only if free), shadowing introspection UX. - -## Hazards - -- The engine's exact-peer discipline: this slice changes `@prisma/cli-engine`'s public surface (`LoadedConfig`, `ConfigSection`), so it must ride an **unpublished** engine version (0.2.3 at the time of writing) or trigger the three-repo family re-peer chain. Verify the version is unpublished at merge time; if a release train has shipped it, bump first. -- `ConfigSection` is implemented by the shipped orm-toolchain and composer dists against the current engine; adding `merge` must be optional and backward-compatible at the type level, or it forces the family chain regardless. -- c12/`extends` stays off; `parent` is ours, not c12's merge directive. `omit$Keys` stays off or the marker dies. -- The frozen exports: any in-place mutation during merge throws in strict mode. - -## Slice-specific done conditions - -- The two-config monorepo layout works end to end: from `packages/db`, ORM commands read the package's `orm` section; `deploy`-scoped sections fall through to the root; a root `skills.check: false` reaches the package. -- `readProjectSkillsConfig`'s hand-rolled resolution is gone; the staleness notice and commands agree on which config governs from any directory. -- Subdirectory `prisma init` writes only the scaffold; root init unchanged; both covered by tests (unit + the existing init e2e extended). -- Ledger: the stale pathe entry is closed (the loader fix shipped in engine 0.2.2; the init e2e rerun workaround at `e2e/init.e2e.ts:189-200` comes out with this slice if the chain work removes its cause, else its entry is updated honestly). - -## References - -- Engine: `packages/cli-engine/src/config-loader.ts`, `config-section.ts`, `runtime.ts` (`LoadedConfig`), `execution/needs.ts` (`checkConfiguration`), `execution/command-tree.ts` (reserved names), `execution/shared-flags.ts` (`--config`). -- Shell: `packages/cli/src/commands/skills/config.ts` (`readSkillsConfig`, `readProjectSkillsConfig`), `packages/cli/src/commands/init.ts` (`renderConfigScaffold`, the postinstall/devDependency steps), `packages/prisma/src/config.ts` (`prisma/config`). -- Section owners at current pins: skills (shell), `orm` (orm-toolchain rc.5+: absence is an error), `composer` (composer-cli 0.12.0+: absence is `{}`). -- Prior implementation for reference: commits `ba48d46`, `f72503c` (removed by `9b2f9d0`); review findings in `.drive/projects/agent-skills-npm-packages/reviews/code-review.md`, round "Init slice — Round 1". diff --git a/.drive/projects/prisma-cli-v8/specs/engine-colour.md b/.drive/projects/prisma-cli-v8/specs/engine-colour.md deleted file mode 100644 index 710956a0..00000000 --- a/.drive/projects/prisma-cli-v8/specs/engine-colour.md +++ /dev/null @@ -1,370 +0,0 @@ -# Engine spec — the palette, rendering, and terminal width - -Status: ruled 2026-08-11 (operator); amended 2026-08-11 after surveying the -shipping implementations in prisma/prisma and prisma/composer (§8 records the -amendments and why). Deliverable: one PR to `packages/cli-engine`. Consumers: -the ORM family and the platform family, both of which lose rendering today. - -Supersedes two earlier revisions of this document. The first routed rich -renderings through `Presentations.stdout` (wrong: that is the machine channel). -The second proposed a separate `graphic` presenter and left tables, trees and -width alone (wrong: the engine owns rendering, so the fix belongs in the block -grammar). - -## 1. What is broken - -The engine ships a common renderer that does almost no rendering. - -- **No colour at all.** `makeUi` (`execution/command-context.ts:24-37`) emits - exactly two escape sequences — SGR bold and dim. There is no colour anywhere - in the engine, and `colorette` is not a dependency. Every ported platform - presenter is written `human: () => [...]`; not one binds `ui`. The entire v8 - CLI is unstyled. -- **Tables do not align.** `renderBlock` (`execution/rendering.ts:86-91`) is - `write(columns.join(" "))` then `write(row.join(" "))`. No sizing, no - padding. The golden suite pins the result: `name id status` over - `Acme Inc ws_1 current`. -- **Key/value cards lost their alignment and their colour.** The legacy shell - drew a card: keys padded to a common width, keys in the accent colour, an - optional dim `│` rail down the left - (`packages/cli/src/shell/ui.ts:81-159`). The v8 `fields` block prints - `label: value` with none of it, across 36 sites. The S2b divergence list - records the loss as accepted; it is not accepted. -- **Trees have no connectors.** `writeTree` (`:103-124`) emits two-space - indents. The style guide specifies `├─ ✘ table user` with dim connectors and - coloured glyphs; the block cannot express it, and has zero users. -- **There is no way to draw.** A migration DAG with gutter lanes is not a tree - and not a table. No block kind fits. -- **Width is unreachable.** `OutputStream` is `{ write(text: string): void }` - (`runtime.ts:7-9`) — no `columns`, structurally, even from the bin. Nothing in - the engine reads terminal size. Consequently every ORM picture already breaks - on a narrow terminal today: they assume unlimited width and let the terminal - hard-wrap, which destroys the gutter alignment that carries their meaning. - -Both consumers have already lost rendering to this. The platform port flattened -`auth workspace list`, `project list` and `agent status` from aligned -rail-and-card renderings to ragged blocks, recorded as accepted divergences. The -ORM port ships its renderers colourless, and two of them bypass `NO_COLOR` -outright (`createColors({ useColor: true })`) to get colour at all. - -## 2. Text, status, and the palette - -### 2.1 One text type, everywhere - -Anywhere a block takes display text, it takes `Text`: - -```ts -type Text = string | readonly Span[]; - -interface Span { - readonly text: string; - readonly tone?: Tone; -} -``` - -A bare string is untoned text. Spans carry meaning, never escape sequences: -**a handler never emits ANSI**, so the engine can measure, re-theme, and strip by -construction. Width is computed from `span.text`, so colour cannot break -alignment — the pad-versus-colour trap that exists in shipped code today -(`packages/cli/src/lib/app/deploy-output.ts:41`) becomes unrepresentable. - -### 2.2 Status is not tone - -`Status` says what happened and selects a glyph. `Tone` says what colour to -paint and selects nothing else. They are separate fields because they are -separate questions: a tree node can be a failure (`✘`) painted in its branch -lane's hue, and a heading can be cyan without being a status. - -```ts -type Status = 'ok' | 'error' | 'warn' | 'info'; // ✔ ✘ ⚠ ℹ -``` - -Each `Status` carries a default `Tone` of the same name, so a block that states -a status is coloured without also stating a tone. An explicit `tone` overrides -that default; it never changes the glyph. - -Today `summary.tone` does both jobs — it is typed `'ok' | 'error' | 'warn' | -'info'` and `TONE_SYMBOL` (`execution/rendering.ts:65-72`) reads it to pick the -symbol. That field becomes `summary.status`, and the 57 call sites in -`packages/cli` are renamed with it. - -### 2.3 The palette - -One `Tone` union, drawn on by `Span`, the blocks, and `Ui` alike. Semantic names -and indexed colours in the same union; a command asks for meaning where it has -one, and for a distinguishable colour where it does not. - -```ts -type Tone = - // status - | 'ok' | 'warn' | 'error' | 'info' - // text roles - | 'heading' // a section label, a field key, a table column header - | 'identifier' // a name the user typed or will type: a table, a ref, a hash, a command - | 'ref' // a marker or ref name and its punctuation - | 'placeholder' // an argument slot: - | 'link' // a URL - | 'emphasis' // the primary value on a line - | 'muted' // secondary detail: counts, timestamps, empty states, off-path text - | 'structure' // the drawing itself: gutters, rails, connectors, separators - | 'highlight' // the selected route through a drawing - // indexed, non-semantic — for telling adjacent things apart - | 'color-1' | 'color-2' | 'color-3' | 'color-4' | 'color-5' | 'color-6'; -``` - -Every rendering is taken from a shipping implementation rather than chosen -fresh. The platform column is `packages/cli/src/shell/ui.ts:63-78`; the ORM -column is the `prisma/prisma` CLI's formatters. - -| Tone | Renders as | Taken from | -| --- | --- | --- | -| `ok` | `greenBright` | platform `success` | -| `warn` | `yellow` | platform `warning`, ORM `migration-graph-labels` | -| `error` | `redBright` | platform `error` | -| `info` | `blue` | platform `info` | -| `heading` | `cyan` | platform `accent` — field keys and column headers | -| `identifier` | `cyan` | ORM `styled.ts` — command names, schema names, hashes | -| `ref` | `green` | ORM `migration-list-styler.ts` `styleRefName` | -| `placeholder` | `dim` | style guide: "show placeholder values such as `` in dim text" | -| `link` | `blue` | platform `link`, ORM `formatReadMoreLine` | -| `emphasis` | `bold` | platform `strong`, and the engine's existing `Ui.emphasize` | -| `muted` | `dim` | both | -| `structure` | `dim` | both — the rail and the tree connectors are dim in each | -| `highlight` | `greenBright` | ORM's on-path route colour | -| `color-1` … `color-6` | `white`, `cyan`, `yellow`, `blueBright`, `magenta`, `green` | ORM `migration-graph-occlusion-render.ts` `LANE_COLORIZERS`, in order | - -`heading` and `identifier` both render `cyan` today, because both shipping CLIs -use cyan for both. They stay separately named so that a later re-theme can -separate them without touching a single command. - -The indexed colours are for series — the migration graph assigns one per branch -lane, `lane % 6`, and tints the lane's gutter cells, node glyph and migration -name alike so they read as one colour. A command says `color-3`; it does not -know or care what that is. - -Colour comes from `colorette`, which is already the colour dependency of this -repo's shell and of the ORM CLI. Basic 16-colour SGR only: no 256-colour, no -truecolor, and therefore no colour-depth detection to own. `LANE_COLORIZERS` -deliberately excludes red so that no lane can be mistaken for `error`, and the -indexed set inherits that. - -### 2.4 `Ui` - -`Ui` gains a verb per tone, for styling inside text, and the available width -(§4): - -```ts -interface Ui { - readonly width: number; // §4 - readonly emphasize: (text: string) => string; // kept: existing callers - readonly dim: (text: string) => string; // kept - readonly code: (text: string) => string; // kept - readonly tone: (tone: Tone, text: string) => string; -} -``` - -Colour disabled ⇒ every verb is an identity function, so a renderer has one code -path and no `if (colour)` branch. - -### 2.5 Colour resolution - -Engine-owned, corrected in one respect: it must key off **the stream the engine -is printing to**. Today it keys off `isTty.stdout` -(`execution/shared-flags.ts:156-158`) while blocks render to stderr, so -`cmd > file` disables colour for output a human is watching and `cmd 2> file` -leaves it on for output nobody sees. - -The precedence is explicit-flag, then environment, then the stream (operator -ruling, 2026-08-11): - -1. `--color` / `--no-color` decides, whatever else is set. An explicit flag on - the invocation beats anything in the environment. -2. Otherwise `NO_COLOR` being set disables colour. -3. Otherwise colour is on iff stderr is a terminal. - -With the palette engine-owned there is no per-renderer colour switch left, so -the ORM's two `createColors({ useColor: true })` bypasses have nothing to bypass -once those renderers convert. - -## 3. The block grammar - -`Block` grows and three existing kinds are fixed. The rule is unchanged: **the -engine renders; a command describes.** A command reaches for `drawing` only when -its layout genuinely cannot be described structurally. - -### 3.1 `table` — the engine aligns it - -```ts -{ kind: 'table'; columns: readonly Text[]; rows: ReadonlyArray } -``` - -The engine sizes each column to its widest cell (measured on text, not escapes), -pads, and joins with two spaces. Column headers render as `heading` unless the -cell carries its own tone. This alone restores `auth workspace list`, -`project list` and every other flattened platform table. - -### 3.2 `fields` — the engine draws the card - -```ts -{ - kind: 'fields'; - rows: ReadonlyArray<{ - readonly label: Text; - readonly value: Text; - readonly sensitive?: boolean; - }>; - rail?: boolean; // default false -} -``` - -The legacy card, restored: `${label}:` padded so every value starts in the same -column, the label rendered as `heading`, the value as given. With -`rail: true` the engine prefixes each row with a `structure`-toned `│` and two -spaces, matching `renderCommandHeader`. - -The rail is a property of the block rather than a global setting because the -legacy shell had both shapes — `renderFieldRows` without the rail, -`renderCommandHeader` with it — and a command knows which one it is drawing. It -defaults to `false`, so restoring the rail on a given command is that command's -own edit; alignment and colour come back everywhere at once. - -### 3.3 `tree` — the engine draws the connectors - -```ts -{ kind: 'tree'; roots: readonly TreeNode[] } - -interface TreeNode { - readonly label: Text; - readonly status?: Status; // renders ✔ ✘ ⚠ ℹ before the label - readonly tone?: Tone; // colours the label; defaults from status - readonly children?: readonly TreeNode[]; -} -``` - -The engine draws `├─`, `└─`, `│` in `structure` tone and the status glyph from -`status`, matching the style guide's `├─ ✘ table user`. This restores the ORM's -schema tree and the operation trees, and gives `agent status` a way to express -the nesting it lost. - -### 3.4 `drawing` — the escape hatch - -```ts -{ kind: 'drawing'; lines: readonly Text[] } -``` - -Lines of spans, rendered verbatim: the engine applies the palette and writes each -line, and does nothing else — no layout, no reflow, no truncation. This is for -output whose 2D structure the engine cannot derive: the migration DAG's lane -gutter, where lane assignment comes from a BFS over the graph and the same hue -must reach the gutter cell, the node glyph and the label text. - -`summary` and `list` keep their shapes, with `Text` in place of `string` and — -for `summary` — `status` in place of the overloaded `tone`. - -## 4. Terminal width - -The engine reads the terminal width **of the stream it is printing to** and hands -it to the command as `ui.width`, because only the command knows what to -sacrifice: the graph would shorten labels before dropping a lane; the list would -shorten migration names before hashes. Blocks print to stderr, so `ui.width` is -stderr's width. - -- When that stream is not a terminal, `ui.width` is `Number.POSITIVE_INFINITY` — - unbounded. It behaves correctly in the arithmetic a renderer already does - (`Math.min(x, ui.width)`, `ui.width - gutter`), so no renderer needs a special - case. -- **If a command overruns anyway, the engine prints it.** No truncation, no - wrapping, no ellipsis. The engine stays dumb; a command that wants to fit was - told how much room it had. -- Reaching `columns` requires widening the runtime seam: `OutputStream` - (`runtime.ts:7-9`) and `HostProcess` (`:97-111`) gain an optional - `columns?: number`, and the bin adapter (`packages/cli/src/v8/runtime.ts`) - passes it through instead of dropping it. Keep the structural typing — no - `NodeJS.*` in the public surface. -- Width is read per render, not cached, so a resized terminal is respected on the - next command. - -Display width is measured with `string-width`, which is already the measurement -dependency of this repo's shell and of the ORM CLI. Measuring by code unit would -misalign every column to the right of a CJK name, and shipping alignment that -works only for Latin text is worse than the ragged output it replaces, because -it looks deliberate. - -## 5. Testing - -- Every tone, colour on and off, exact bytes; the palette pinned in the - golden-rendering suite so a colour change is a visible diff. -- A table with ragged content aligns; a table whose cells carry spans aligns - identically with colour on and off; a table containing a wide (CJK) cell - aligns. -- A card aligns its values into one column and tones its labels; `rail: true` - reproduces the legacy rail bytes. -- A tree renders connectors and glyphs matching the style guide's example - verbatim. -- A drawing round-trips its spans with no reflow. -- `ui.width` is stderr's columns; `POSITIVE_INFINITY` when stderr is not a - terminal; a command that overruns is printed unmodified. -- `NO_COLOR` suppresses every tone; `--color` overrides it; `--no-color` - suppresses colour on a terminal. -- Colour resolution follows stderr: `cmd > file` on a terminal keeps colour. - -## 6. Coordination - -- Lands in `packages/cli-engine`, ships in a published `@prisma/cli-engine`; - both families convert their renderers afterwards. -- Touches `presentation.ts`, `execution/rendering.ts`, - `execution/command-context.ts`, `execution/shared-flags.ts`, `runtime.ts`, and - the exports barrel, plus the `packages/cli` call sites the `status` rename - reaches and the goldens the new rendering re-pins. -- Branches from `main`. The runtime change overlaps the package-manager - capability (#140) and telemetry (#143) specs, but all three only add a field to - `Runtime`, so the conflicts are additive and no stacking is warranted - (operator: work it out, 2026-08-11). -- `Text` replacing `string` in block fields is source-compatible: every existing - call site passes strings. `summary.tone` → `summary.status` is not, and is - renamed in the same PR. - -## 7. Acceptance - -- [ ] One `Tone` union — semantic names plus indexed colours — drawn on by - `Span`, the blocks, and `Ui`; `Status` is a separate field that selects the - glyph and nothing else. -- [ ] `Text = string | Span[]` accepted everywhere a block takes display text; - handlers never emit escape sequences. -- [ ] `table` aligns; `fields` restores the aligned, toned card with an opt-in - rail; `tree` draws connectors and status glyphs; `drawing` renders spans - verbatim. -- [ ] `ui.width` is stderr's width, unbounded off-terminal; overruns print - unmodified. -- [ ] Colour resolution follows stderr; `--color`/`--no-color` beats `NO_COLOR`, - which beats the stream. -- [ ] The indexed colours are the ORM's lane rotation, which excludes red. -- [ ] Goldens pin the palette, table alignment, card alignment, and tree - connectors. - -## 8. Amendments after the implementation survey - -Four changes to the ruled spec, each made because the shipping code answered a -question the spec had left to taste. The operator's instruction was to find the -established solution rather than invent one. - -1. **Six indexed colours, not eight.** The ORM's `LANE_COLORIZERS` is a - six-entry rotation over basic ANSI — `white, cyan, yellow, blueBright, - magenta, green` — chosen to exclude red so a lane cannot read as an error. - Basic ANSI has no two further hues that stay distinguishable from those six; - the remaining candidates are bright variants of colours already in the set. - Reaching eight would mean moving to a 256-colour palette that neither - shipping CLI uses. Six mutually distinguishable colours beat eight where two - are indistinguishable. -2. **`Status` split out of `Tone`.** The ruled spec had `Block.tone` and - `TreeNode.tone` selecting a status glyph, inheriting the conflation from - today's `summary.tone`. A colour and a status symbol are different facts: - fusing them means a tree node cannot be a failure painted in its lane's hue. -3. **`fields` restores the card.** The ruled spec left `fields` shape-only. - It is the most-used block in the CLI (36 sites) and the one that lost the most - — the legacy card's alignment and accent colour. Restoring it is the point of - the slice, not an extension of it. -4. **`--color` beats `NO_COLOR`.** The ruled spec said `NO_COLOR` was absolute. - An explicit flag on the invocation beats anything in the environment - (operator, 2026-08-11); "absolute" governs renderers reaching around the - engine, which the engine-owned palette makes impossible anyway. diff --git a/.drive/projects/prisma-cli-v8/specs/engine-owns-telemetry.md b/.drive/projects/prisma-cli-v8/specs/engine-owns-telemetry.md deleted file mode 100644 index 4c4acd3d..00000000 --- a/.drive/projects/prisma-cli-v8/specs/engine-owns-telemetry.md +++ /dev/null @@ -1,424 +0,0 @@ -# Engine spec — the engine owns telemetry reporting - -Status: ruled 2026-08-11 (operator: telemetry reporting is not a consumer's -responsibility; it belongs in the engine). Amended the same day by three -further rulings — **read §1.1 before the body**, it overrides the original -draft wherever the two still read differently. - -Deliverable: one PR to `packages/cli-engine`, plus the deletion of the -shell's copy. The second consumer is the ORM's `prisma-next` bin, which must -not have to re-implement any of this. - -## 1. What this is and why - -The engine already knows everything telemetry needs at parse time: the -command path, the flag names with their value source, and a count of -positionals (`EngineCommandSnapshot`, `src/run-summary.ts`), built in -`executeMounted` (`src/execution/engine.ts:390`). It stops there. Sending is -left to each consumer, and today the platform shell does it in 136 lines of -bin code (`packages/cli/src/v8/telemetry/reporting.ts`): resolve gating, -print the first-run disclosure, resolve the sender's path, attach a hook, -spawn the detached sender, swallow every failure. - -That leaves the ORM's `prisma-next` bin to write the same logic against a -second copy of the telemetry package, with the same installation id, the same -endpoint, the same wire shape, and the same first-run disclosure. The two -copies have **already drifted**: `@repo/cli-telemetry`'s gating resolves CI -inside `resolveGating` and reports `env-opt-out`, while the ORM's resolves CI -in the caller and reports `env-override`, with a second projection in its -`telemetry status` command translating one vocabulary into the other. Nothing -user-visible has broken yet; that is luck, not design. - -The project's own framing has the engine owning "argv parsing, help, output -envelopes, JSON mode, prompts, consent, **telemetry**, error presentation, -and exit codes". Reporting is the half that never landed. - -There is no purity argument against it: the engine already constructs an -authenticated API client, opens URLs, and reads the filesystem -(`src/config-loader.ts` imports `node:fs`). Telemetry is one more engine-owned -surface with a bin-supplied seam for the actual process work. - -### 1.1 Operator rulings — these override the original draft - -1. **Whatever the ORM does today moves into the engine, as it is.** The - engine reproduces the shipped ORM behaviour; it does not improve it in - passing. Where the platform shell and the ORM disagree, the ORM wins. -2. **Report a command's option names without special-casing; never report - param values.** No per-command allowlist, no curation. Option names as the - user spelled them, and no value of any option or positional ever leaves - the process. -3. **Follow the ORM's example on timing** — the event fires at *command - start*, from the parse-time snapshot, not at settlement. - -Three things the original draft asked for are struck by those rulings: - -- **No outcome data on the wire.** The draft added exit code, duration, - platform/arch/node version and a CI flag. The shipped event has none of - them, and prisma/prisma's published `docs/Telemetry.md` tells users in - writing: "**No outcome data.** Phase 1 does not collect success/failure, - exit code, or elapsed time." Firing at command start makes this structural - — at that point no exit code or duration exists to send. -- **No `onSettled`.** `RunSummary` is by construction a settlement artifact. - The engine fires from `EngineCommandSnapshot` instead, at the point the - snapshot is built. -- **No `ctx.telemetryStatus`, no `ctx.telemetry.setEnabled`, no - `managesTelemetry` capability.** The engine ships the three commands - itself (§2.4), so no product needs private access to render or mutate the - preference. Unbuilt surface, dropped rather than built unused. - -## 2. The surface - -### 2.1 Declaration at `createCli` - -```ts -createCli({ - name, - version, - commandFamilies, - groups, - commands, - telemetry: { docsUrl: 'https://…' }, // named in the first-run disclosure -}) -``` - -One field. The endpoint, the opt-out environment variable names, the -user-config path and the disclosure wording are Prisma constants and live in -the engine — the engine is Prisma-specific by design, and a generic -extension mechanism here would be surface with one caller. The CLI's own -name and version come from `createCli`'s existing fields. - -Omitting `telemetry` means the CLI reports nothing: the engine reads no -config, prints no disclosure, mints no id, and calls no seam. There is no -default endpoint and no way to report without declaring the block. - -A host that declares the block but wires no `spawnTelemetry` (§2.5) is the -same case and takes the same path. Both halves are required before anything -happens, and the check comes first, before the config read. A CLI that -cannot deliver an event must not tell the user it collects data, and must -not mint an installation id it has no use for — the disclosure is a promise -about what the binary does, not about what it declares. - -### 2.2 What the engine does, and when - -Immediately after `state.snapshot` is assigned in `executeMounted` -(`src/execution/engine.ts:390`) — before the needs check, before the handler, -before any command output — and exactly once per run: - -1. **Skip the exemption.** A run whose command path starts with `telemetry` - sends nothing and mints nothing. `telemetry disable` must not report a - usage event on its way to disabling, and `telemetry status` must not mint - an id while merely reporting state. This is the only command-specific - exemption, and it is the one the ORM already has. -2. **Resolve gating**, in the ORM's order: CI hard-disables first, then the - environment opt-outs (`PRISMA_DISABLE_TELEMETRY` truthy, or - `DO_NOT_TRACK=1`), then a stored `enableTelemetry: false`, then a stored - `true`, then the opt-out default (absent means on). The resolution carries - its reason from the five-value union in §2.3. -3. **Print the first-run disclosure** when the decision is enabled and no - installation id is stored yet — to stderr, never stdout, so it cannot - corrupt piped output. Composed by the engine from the CLI name and the - declared `docsUrl`. It names the `telemetry disable` command and the two - environment variables, and does **not** name the preference file: that - file is machine-edited, which is what the commands are for (operator - ruling, 2026-08-11). `telemetry status` reports its path for anyone who - wants it — describing where the preference lives is not the same as - telling someone to edit it there. -4. **Mint and store the installation id** — a v4 UUID, written without - touching `enableTelemetry`, so a default-on first run records no consent - the user never gave. Never rotated, never derived from anything - machine-identifying. -5. **Compose the payload and hand it to the seam** (§2.5). The engine - composes; the bin spawns. -6. **Swallow every failure.** An unwritable config directory, a throwing - seam, a malformed stored config — none of it changes an exit code, writes - to a command's output, or delays the run. - -A run that never mounts a command (`--help`, `--version`, an unknown -command, a usage error) builds no snapshot and therefore reports nothing. - -**No values, ever.** The payload carries the command path joined with -spaces and the *names* of the options whose source is `cli` — the options -the user actually typed, with no per-command filtering. Option values, -positional values and raw argv never reach the payload. `positionalCount` -exists on the snapshot and is deliberately never read. - -The name reported is the option's **declared key** in the engine's -kebab-case spelling, not the token the user typed. A negated spelling -reports its base key — `--no-color` reports `color` — so polarity never -reaches the wire, and an alias reports the long name it resolves to. -Affects `--color` and `--interactive`, the only negatable options. -`assets/s2/parity-divergences.md` records the counting consequence -against the ORM CLI, which emits both spellings. - -### 2.3 The status reasons - -The five the two CLIs already agree on at the surface, evaluated in that -order: - -```ts -type TelemetryStatusReason = - | 'ci' // a CI environment was detected - | 'env-opt-out' // DO_NOT_TRACK / PRISMA_DISABLE_TELEMETRY - | 'stored-opt-out' // "enableTelemetry": false - | 'stored-opt-in' // "enableTelemetry": true - | 'default-on'; // no explicit choice stored -``` - -The original draft's `env-override` and `not-configured` spellings are -dropped. `env-override` is the ORM's *internal* gating vocabulary, which its -own `telemetry status` already translates to `env-opt-out` before showing a -user; the engine collapses that translation by resolving to the user-facing -union directly. `not-configured` describes a CLI that declared no telemetry -block, which has no status command mounted to report it. - -### 2.4 The commands - -The engine ships `telemetry status`, `telemetry enable` and `telemetry -disable`, mountable by a product in one line, with the help text, output and -exit behaviour the two CLIs already share: - -- **`status`** — read-only. Reports enabled/disabled with the reason, the - config file path, and whether an installation id is stored. Never prints - the id itself, never mints, never writes, never sends. -- **`enable`** — stores `enableTelemetry: true` and mints an installation id - if none exists. -- **`disable`** — stores `enableTelemetry: false`. Mints nothing, sends - nothing. - -They are ordinary engine commands: `ctx.present` with human, stdout and json -presentations, so `--json` works the way it does everywhere else. - -### 2.5 The runtime seam - -```ts -interface Runtime { - // …existing members… - /** True when this process runs in CI. The bin wires `ci-info`; the - * engine never detects CI itself. */ - readonly isCI: boolean; - /** Fire-and-forget delivery of one composed telemetry payload. The bin - * owns the process work and the detachment. Absent means this host - * reports nothing — not an error. */ - readonly spawnTelemetry?: (payload: TelemetryPayload) => void; -} -``` - -The engine composes and hands over; the bin forks. An absent seam is not an -error, and per §2.1 it suppresses the whole sequence rather than only the -delivery. The engine imports no `node:child_process` and performs no network -I/O for telemetry. `isCI` is a -Runtime field rather than an engine-side `ci-info` import because the engine -never reads process globals — the same rule that keeps TTY detection on the -Runtime. - -That rule binds the preference store too, and it is the one place a -port-as-is would break it. `$XDG_CONFIG_HOME`, `%APPDATA%`, `$HOME` and -`%USERPROFILE%` are all invocation inputs: they must resolve from -`runtime.env`, threaded through the path resolver, the reader, the writer and -the id mint, not read from `process.env` — and `os.homedir()` is a -`process.env` read wearing a different hat, so it has no place here either. -When none of the four is present the store has no path, and telemetry reports -nothing rather than guessing at one. In production every host sets `$HOME`, so -this only ever fires in a test that seeded no environment — where doing -nothing is the correct answer. The engine reads `process.env` nowhere today — five separate -doc comments across `context.ts`, `credential-manager.ts`, -`environment-credential-manager.ts` and `execution/debug.ts` say so — and a -telemetry module that did would also mean any `createTestCli` run touched the -real user's config file. - -`process.platform` and `process.pid` stay. Neither varies with an invocation, -neither is modelled on the Runtime, and adding a `platform` field for one -caller is surface without a second consumer. Nothing in either CLI's test -suite exercises the Windows branch of the path resolver today, so this -changes no coverage. - -`createTestCli` gains a `telemetrySpawner` seed and an `isCI` seed so a -product's telemetry behaviour is assertable offline; no test contacts a real -endpoint. - -### 2.6 What moves, and what stays - -| Module | Where it lands | -| --- | --- | -| `cli-telemetry/src/gating.ts` | engine — resolving to the §2.3 union | -| `cli-telemetry/src/user-config.ts` | engine — read, write, mint, path resolution | -| `cli-telemetry/src/sanitize.ts` | engine — importing the real `EngineCommandSnapshot` instead of redeclaring it | -| `cli-telemetry/src/endpoint.ts` | engine — constant plus the `PRISMA_TELEMETRY_ENDPOINT` test override | -| `ParentToSenderPayload` (the type) | engine — it is what the engine composes | -| `isParentToSenderPayload` (the validator) | stays in `cli-telemetry` — it guards the child's trust boundary | -| `cli-telemetry/src/enrich.ts`, `sender.ts` | stay — the child still probes the system and POSTs | -| `cli-telemetry/src/spawn.ts` | stays, shrunk: the engine has already decided, so it forks and sends, and no longer re-resolves gating or re-reads the user config | -| `cli/src/v8/telemetry/reporting.ts`, `is-ci.ts`, `status.ts`, `enable.ts`, `disable.ts`, `consent.ts` | deleted | -| `cli/src/v8/telemetry/sender.ts` | stays — the build entry that carries the forkable sender into the published cli | - -`packages/cli/src/v8/runtime.ts` gains `isCI` (wiring `ci-info`) and -`spawnTelemetry`; `main.ts` drops its `resolveTelemetryHooks` block; `cli.ts` -mounts the engine's three commands in place of its own. - -## 3. What must not change, and the one thing that does - -**The wire shape and the endpoint.** The 13-field `TelemetryEvent` is -untouched, and the parent still sends -`{ installationId, version, command, flags, projectRoot, endpoint }` over IPC, -so events from the platform CLI before and after this change are -indistinguishable to the backend. - -**The `prisma-next` naming goes, with no legacy support** (operator ruling, -2026-08-11). This overrides the earlier draft, which required the config path -and environment variables to stay put: - -| Was | Is | -| --- | --- | -| `$XDG_CONFIG_HOME/prisma-next/config.json`, `~/.config/prisma-next/config.json`, `%APPDATA%\prisma-next\config.json` | the same three, under `prisma` | -| `PRISMA_NEXT_DISABLE_TELEMETRY` | `PRISMA_DISABLE_TELEMETRY` | -| `PRISMA_NEXT_TELEMETRY_ENDPOINT` | `PRISMA_TELEMETRY_ENDPOINT` | -| `PRISMA_NEXT_DEBUG` | `PRISMA_DEBUG` | - -`DO_NOT_TRACK` is a community convention and does not move. - -No read fallback, no dual-write, no migration: the old location is not -consulted and the old variable names do nothing. Tests pin that absence, so a -fallback cannot creep back in later. - -Two consequences, both accepted on the ruling. Every stored preference and -installation id at the old path is abandoned — an existing opt-out reverts to -the opt-out default and an existing id is orphaned, so the backend sees the -population turn over once. And the file stops being shared with the ORM's -`prisma-next` binary, so until that binary ports onto the engine each holds -its own answer. Both are acceptable because this is semver zero and retiring -that binary is the project's whole purpose. - -`PRISMA_DEBUG` is now one switch for every diagnostic the CLI has — the -engine's execution valve, the auth layer's state-file logging, the telemetry -spawner and the child sender. Renaming only the telemetry half would have left -a user setting one variable and getting half an answer. - -`ParentToSenderPayload.databaseTarget` stays on the type for wire -compatibility and the engine never populates it — the ORM's parent never did -either; the child derives it from the user's config. - -## 4. Divergences this creates - -Both are recorded in `assets/s2/parity-divergences.md` as part of the slice. - -- **The platform CLI moves from settlement to command start.** Its S2a - divergence note — that a run which crashes or exits early before settlement - emits nothing — is retired rather than extended, because the event now - fires before the command runs at all. ADR 217 (prisma/prisma), which makes - "spawned at command start" the load-bearing isolation decision, stays true - and needs no amendment. -- **The ORM's first-run disclosure wording changes** from "Prisma Next - collects anonymous CLI usage data" to "Prisma collects anonymous CLI usage - data", because the engine composes one disclosure for one product. Same - channel, same timing, same opt-out instructions. - -## 5. Testing - -- **Gating precedence**, every combination of CI, both environment opt-outs - (including the falsy spellings `''`, `'0'`, `'false'`, which must not - disable), and stored `true` / `false` / absent — asserted on both the - decision and its reason. -- **Disclosure** fires exactly once, only on an enabled run with no stored - id, only on stderr; never when disabled, never in CI, never for - `telemetry *`. -- **The id** is minted once, never rotated across an on → off → on cycle, and - a default-on mint leaves `enableTelemetry` absent. -- **Timing and exemption**: the seam is called before the handler runs, not - after; once per run; never for `--help`, `--version`, an unknown command, - or any `telemetry *` path. -- **No values**: a run with option values, positionals and an option that - defaults asserts the payload carries the command path and the typed option - names only. -- **Every failure is invisible**: a throwing spawner, an unwritable config - directory and a malformed stored config each leave exit code, stdout and - stderr byte-identical to the same run without telemetry. -- The platform CLI's existing telemetry tests - (`packages/cli/tests/v8-telemetry*.test.ts`) port onto the new surface and - keep passing unchanged in intent. - -## 6. Coordination - -- Lands in `packages/cli-engine` and ships in a published - `@prisma/cli-engine`; consumers pick it up by version. -- Touches `cli.ts`, `runtime.ts`, `testing.ts`, `execution/engine.ts` — the - same files as the package-manager capability (PR #140) and the redirect - table (PR #142). Sequence with the operator. -- **The ORM port depends on this for its cutover.** Until it publishes, the - ORM's new bin reports nothing and the commander CLI keeps reporting as it - always has — no user-visible gap, because the commander CLI owns the binary - until then. - -## 7. Acceptance - -- [ ] `createCli({ telemetry: { docsUrl } })` is the only wiring a consumer - writes; no consumer re-implements gating, disclosure, id minting, or - firing. -- [ ] The event fires at command start, from the parse-time snapshot, before - the handler runs. -- [ ] Gating precedence, disclosure timing and id lifetime match the shipped - ORM behaviour exactly. -- [ ] The engine mounts `telemetry status|enable|disable`; a `telemetry *` - run reports nothing and mints nothing. -- [ ] The payload carries no value from argv; positionals never appear. -- [ ] Every telemetry failure is invisible to the run. -- [ ] `createTestCli` seeds the spawner and `isCI`; no test reaches a real - endpoint. -- [ ] The shell's `reporting.ts`, `is-ci.ts` and three telemetry commands are - deleted, and its telemetry tests pass against the engine surface. -- [ ] Wire shape, endpoint, config path and installation ids are unchanged - for existing users. - -## 8. Follow-ups - -Two questions were raised during review and ruled by the operator rather than -left open: - -- **A non-boolean `enableTelemetry` leaves telemetry on.** `readUserConfig` - casts parsed JSON without validating field types, so a hand-edited - `"enableTelemetry": "false"` — the string — matches neither the `false` nor - the `true` branch and lands on the opt-out default. **Ruled: leave it** - (operator, 2026-08-11). It is the ORM's behaviour exactly, it takes a - malformed hand-edit to reach, and the three other opt-out routes — the - `telemetry disable` command and both environment variables — cannot be - mistyped into the wrong answer. -- **The first-run notice named `prisma-v8`.** The engine composes it from - `createCli`'s `name`, which the shell hardcoded, while every other - user-facing string in the same shell used the `CLI_NAME` constant. - **Ruled: pass the constant** (operator, 2026-08-11). Fixed; the shell no - longer contradicts itself between its help output and its next actions. - -- **`CliRunHooks.onSettled` has no consumer left.** It was introduced for - telemetry. Kept as-is here — removing published engine surface is its own - decision — and recorded so the next person to touch `cli.ts` can weigh it. -- **prisma/prisma's `docs/Telemetry.md` documents the old names.** The page - describes the `prisma-next` binary as shipped, which still reads - `prisma-next/config.json` and `PRISMA_NEXT_DISABLE_TELEMETRY`, so it is - correct today and would be wrong if updated now. It changes when that binary - ports onto the engine. The hand-editing half of the ruling did not have to - wait and is already in flight as prisma/prisma#29976. -- **The engine has two different answers to "am I in CI".** Interactivity - is decided by `runtime.isTty.stdin && runtime.env.CI === undefined` - (`src/execution/shared-flags.ts`), overridable with `--no-interactive`; - telemetry gates on `Runtime.isCI`, which the bin fills from `ci-info`. They - disagree on `CI=false`, and on a vendor that sets its own marker but not - `CI` — and `--no-interactive` moves one and not the other. Nothing is wrong - today, because each is used only where it was meant to be, and `ctx.isCI`'s - documentation now says so explicitly. Reconciling them is its own change: - it decides whether `--no-interactive` should also suppress telemetry, which - is a product question, not a cleanup. -- **`Runtime.isCI` is a required field on a published interface.** Every host - that constructs a `Runtime` must add it. Correct — a host that forgot an - optional one would silently report from CI — but it needs a release note at - `8.0.0-rc.1`. -- **A CLI that declares telemetry without mounting the commands prints an - opt-out instruction naming a command it does not have.** §2.1 lets the two - halves mount independently on purpose, and the notice always names the - environment variables and the config file as well, so a working opt-out - survives — but the friendliest one it offers would not run. No consumer is - in that state today: the platform shell mounts both. -- **An unwritable config directory makes `telemetry enable|disable` fail as - `CLI.INTERNAL_ERROR`.** Identical to the platform shell's current behaviour - and better than the ORM's, so §1.1 rule 1 says leave it here. But a consent - surface deserves a phrased error naming the file it could not write, not the - engine's generic one — "my opt-out did not take" is the worst failure this - surface has. diff --git a/.drive/projects/prisma-cli-v8/specs/engine-package-manager-capability-plan.md b/.drive/projects/prisma-cli-v8/specs/engine-package-manager-capability-plan.md deleted file mode 100644 index 7e746c4f..00000000 --- a/.drive/projects/prisma-cli-v8/specs/engine-package-manager-capability-plan.md +++ /dev/null @@ -1,123 +0,0 @@ -# Dispatch plan — the package-manager capability - -Slice contract: `engine-package-manager-capability.md` (amended 2026-08-11). -Branch: `spec/package-manager-capability` (PR #140). Five dispatches, sequential. - -Verification for every dispatch (repo `AGENTS.md`): `pnpm --recursive exec tsc ---noEmit`, `pnpm lint`, and the changed package's tests. The engine's own -`pnpm --filter @prisma/cli-engine test` runs build + typecheck + vitest, so the -type tests are part of it. - -## D1 — Detection and the spelling table - -**Outcome.** One engine-internal module resolves a concrete `PackageManagerId` -from a directory and spells every command line the engine will ever run, for -all five managers and both forms. `Runtime.packageManager` stops being the -source of truth and survives only as an optional host override, which -`resolvePackageManager` takes as its `host` argument; -`missingDependencyError` reads the new table; `'unknown'` is out of the -codebase. - -**Focus.** `package-manager-detector` exact-pinned at `1.8.0` as an engine -dependency (zero transitive dependencies — verify before adding). Precedence -per spec §3.1: explicit override, then optional `Runtime.packageManager`, then -`detect({ cwd })`, then the library's `getUserAgent()`, then `'npm'`. Spelling -matches the ORM's table exactly, including npm's `add` alias and deno's `npm:` -prefixes. - -Demoting `Runtime.packageManager` to an override touches its two former call -sites (`execution/needs.ts`, `execution/command-context.ts`), which read the -detected manager instead; the harness seed in `src/testing.ts`, which becomes -that override unchanged; the `Runtime` literal in `tests/engine.type-test.ts`; -and the bin's `assembleRuntime` in `packages/cli/src/v8/runtime.ts`, which -stops populating the field — its `detectPackageManager` helper dies with it. - -**Builds on.** Nothing. -**Hands to.** D2: a detection function and a spelling table, unit-tested per -manager and per precedence step, with no remaining reference to `'unknown'`. - -## D2 — The Runtime seam and the bin's implementation - -**Outcome.** `Runtime.runPackageManager` exists with the `onOutput` callback, -the bin implements it over execa, and a real `prisma` process can run a -package manager. The engine still imports no `child_process`. - -**Focus.** Seam shape per spec §2.3. The bin adapter streams stdout and stderr -to `onOutput` as they arrive AND accumulates stderr bounded to the last 64 KiB. -Cancellation wires `signal` through to the child. `createTestCli` gains the -`packageManagerRunner` seed; absent seed must still resolve the -runner-unavailable path rather than throwing. - -**Builds on.** D1's `PackageManagerId`. -**Hands to.** D3: an execution seam that can be driven from tests by a scripted -fake and from the bin by a real child process. - -## D3 — `ctx.packages`, the capability flag, and the structured failure - -**Outcome.** `installsPackages: true` puts a typed `ctx.packages` on the -context; both operations run end to end through the seam; failures are -`CLI.PACKAGE_MANAGER_FAILED` with the documented meta; redaction, step events, -and `output` events are in place. - -**Focus.** Mirror `managesCredentials` exactly — the fifth generic, the -intersection in `Handler`, the two `defineCommand` overloads, the -`Object.defineProperty` attachment in `execution/command-context.ts`, the flag -read in `execution/engine.ts`. One exported error constructor for the code, per -the engine's one-constructor-per-code discipline. Redaction is its own -unit-tested helper. New public types go through `src/exports/index.ts`. - -Three behaviors that are easy to miss: cancellation THROWS the abort reason -(§3.6) rather than resolving `notOk`; a second concurrent call is a caller bug -raising `CLI.INTERNAL_ERROR` (§3.7); no message interpolates a hardcoded -manager name (§3.5). - -**Builds on.** D2's seam. -**Hands to.** D4: a working capability with unit coverage. - -## D4 — The proof: a sample command's install matrix, offline - -**Outcome.** A command declaring `installsPackages` exercises success, failure, -the pnpm→npm retry shape, cancellation, and the absent runner — all through -`createTestCli` with no network and no real package manager — plus type tests -asserting `ctx.packages` exists if and only if the capability is declared. - -**Focus.** The retry case must prove the ORM's real fallback is expressible: -script the fake to fail with `ERR_PNPM_WORKSPACE_PKG_NOT_FOUND` in stderr, -assert the handler can match it off `meta.stderrTail` after redaction, and -assert the second call goes out with `manager: 'npm'`. Assert human and json -output per the repo's testing bar (`docs/onboarding/testing.md`). - -**Builds on.** D3. -**Hands to.** D5: green evidence the surface works as specified. - -## D5 — The R13 amendment - -**Outcome.** R13 in `docs/architecture/cli-engine-requirements.md` names the -exception in its own words, per spec §7, and the prohibition on hidden package -state stands unchanged. - -**Focus.** Documentation only, no code. Keep R13's existing voice — the -requirement states a rule and then a **Why:** paragraph grounded in the history -that motivated it. - -**Builds on.** D3 (the amendment describes what actually shipped). -**Hands to.** Slice done. - -## Follow-up dispatches (added after the five planned ones) - -**D6 — success returns nothing.** `install` and `run` resolve `okVoid()`; -the sample command presents warnings instead of the command lines it ran. - -**D7 — merge `main`.** `ctx.spawn`, the c12 config loader and the -`app` → `service` rename all landed while this branch was built. - -**D8 — the review's findings.** Two confirmed secret leaks in redaction, -an unpaired step event on cancellation, an announce-and-spawn on an -already-aborted run, substring matching on secret names, a quadratic -stderr tail, two untrue claims in prose, the failure constructor made -private, and the terminal-ownership interlock against `ctx.spawn`. - -## Settled during review - -The `deno` widening of `PackageManagerId` (spec §2.2, amendment 3) was -approved by the operator on 2026-08-11. diff --git a/.drive/projects/prisma-cli-v8/specs/engine-package-manager-capability.md b/.drive/projects/prisma-cli-v8/specs/engine-package-manager-capability.md deleted file mode 100644 index 428f15cc..00000000 --- a/.drive/projects/prisma-cli-v8/specs/engine-package-manager-capability.md +++ /dev/null @@ -1,226 +0,0 @@ -# Engine spec — the package-manager capability - -Status: ruled 2026-08-11 (the operator approved building the affordance; this document is the implementation spec), **amended 2026-08-11** after a second round of operator rulings — see the amendment log at the end. Deliverable: one PR (PR #140's own branch), touching `packages/cli-engine` and the bin's Runtime assembly in `packages/cli`. The consumer is the ORM family's `init` command (ported in prisma/prisma), so the surface here is a contract between repos — treat every name in §2 as frozen unless the operator re-rules. - -## 1. What this is and why - -A command like `prisma init` scaffolds a project and installs the dependencies it just wrote into `package.json`. Today (in the ORM CLI being ported) the command spawns `pnpm add` / `npm install` / `npx skills add …` itself: it detects the manager, spells the argv, parses stderr, and phrases the failure prose. - -Engine requirement R13 said, before this change, that the CLI never touches a package manager. Its intent was that the CLI must not become a second, worse package manager — installing command submodules into hidden `node_modules`, guessing at lockfiles, hiding what it runs. Running the user's own package manager, in the user's own project, at the user's explicit request, with the command visible and the failure structured, is not that. The operator has ruled the engine gains a first-class affordance for it, and that R13's text is amended to name the exception (§7). - -The shape follows the engine's existing capability pattern (`managesCredentials`): a **capability, not a need** — declaring it never fails a run; it only adds a surface to the command's context. - -## 2. The surface - -### 2.1 Declaration - -```ts -defineCommand({ - installsPackages: true, // capability flag, same pattern as managesCredentials - // … -}) -``` - -Declaring `installsPackages: true` intersects `packages: PackageOperations` onto the handler's `CommandContext`. Commands without the declaration have no `ctx.packages` (a compile error, exactly as `ctx.credentialManager` behaves). - -**`defineCommand` has no overloads** (operator ruling, 2026-08-11). The v8 draft carried two, with a note that a single signature had been tried and inference collapsed. A second capability flag would have made that four, and every further flag doubles it again. The claim was retested against the real API: one generic signature with both flags optional, each constrained `extends boolean`, infers correctly in all four combinations — the handler is context-sensitive, so TypeScript fixes both booleans from the object literal before checking the handler body, and a primitive-constrained parameter does not widen its literal. The overloads are deleted. Two consequences: an explicit `managesCredentials: false` now compiles where the overloads rejected it as an excess property, and `defineCommand`'s body carries one cast that only an explicit type-argument call could defeat. - -The ruling came with a condition: **the type tests must use vitest's type matchers** (`expectTypeOf`) rather than the `export const x: true = …` style the existing tests use, so the inference claim is asserted by a suite that runs rather than by a file that merely compiles. Coverage: all four flag combinations, the `CommandHandler` annotation path, and explicit `false` behaving as omission. - -**The setup is copied from Composer and the ORM, not invented** (operator ruling, 2026-08-11) — this repo has no vitest type-checking today. The shape that fits is the ORM's, because vitest here also runs the runtime suite: `typecheck: { enabled: true, include: ['tests/**/*.test-d.ts'] }`, which makes a plain `vitest run` execute the type tests with no change to the `test` script. `enabled: true` is the part that matters. The ORM's own `contract` package writes the same block WITHOUT it and its type tests consequently never run — the trap to avoid, and the reason this slice verifies the harness by breaking an assertion on purpose and watching the suite go red. Both repos use `*.test-d.ts` and both mix `expectTypeOf` for positive assertions with `@ts-expect-error` for "this must not compile"; that mixture is the house idiom, not a compromise. - -### 2.2 `ctx.packages` - -```ts -interface PackageOperations { - install(request: { - readonly packages: readonly string[]; // specifiers as the user would type them, e.g. "prisma-next@latest" - readonly dev?: boolean; // dev dependency; default false - readonly cwd?: string; // default ctx.cwd - readonly manager?: PackageManagerId; // explicit override; default = detection (§3.1) - }): Promise>; - - run(request: { - readonly package: string; // the package whose bin to execute, e.g. "skills" - readonly args: readonly string[]; - readonly cwd?: string; // default ctx.cwd - readonly manager?: PackageManagerId; // explicit override; default = detection (§3.1) - }): Promise>; -} - -type PackageManagerId = 'npm' | 'pnpm' | 'yarn' | 'bun' | 'deno'; -``` - -`deno` is in the set because the ORM's `init` supports it today: `formatAddArgs` spells `deno add npm:` (`.../commands/init/detect-package-manager.ts`) and the one-off runner table spells `deno run -A npm:` (`.../commands/init/skill-install.ts`, `formatPackageManagerCommand`). A four-member type would silently drop deno support at the port, which the project spec's parity bar (FR6) forbids as an undiscovered divergence. This widens the frozen §2 type by one member; the operator was asked twice, reviewed this section with the widening called out in it, and approved (2026-08-11). - -Note which ORM function the runner table comes from: `skill-install.ts`'s `formatPackageManagerCommand` (`pnpm dlx` / `yarn dlx` / `bunx` / `npx` / `deno run -A npm:`), NOT `detect-package-manager.ts`'s `formatRunCommand`, which spells how to run a binary the project has already installed and is a different thing. - -- `install` adds dependencies to the project at `cwd` (the manager's own add/install verb, with the manager's dev flag when `dev` is true). -- `run` is the one-off runner form — `npx` / `pnpm dlx` / `yarn dlx` / `bunx` — for executing a package's bin without adding a dependency. (The ORM `init`'s agent-skill install is this form.) -- Success resolves `okVoid()`. The caller gets NO command line back (operator ruling, 2026-08-11): the whole point of the capability is to hold the command away from the package manager, so returning a rendered command line on success hands back exactly what was abstracted away — and under R5 a product cannot print it anyway. The engine announces the command in the step label before it runs, streams the manager's output while it runs, and carries the same line, redacted, on the failure's next action (§3.4). Nothing is left for the caller to do with the string. -- Failure resolves `notOk(CliStructuredError)` — never a throw for an install that failed; a throw remains what it always is (a bug). The single exception is cancellation, which throws (§3.6). -- The `manager` override exists so a caller can retry with a different manager (§5). It is an override of the *choice*, not a bypass of the machinery. -- Neither operation can be called while another is still running (§3.7). - -### 2.3 The Runtime seam (bin-owned execution) - -```ts -interface Runtime { - // …existing members… - runPackageManager?: (spec: { - readonly file: string; // executable, e.g. "pnpm" - readonly args: readonly string[]; - readonly cwd: string; - readonly signal: AbortSignal; - /** Called with each chunk as the child writes it, so the engine can - * emit `output` events while the operation runs. */ - readonly onOutput: ( - channel: 'data' | 'diagnostic', - chunk: string, - ) => void; - }) => Promise<{ exitCode: number; stderr: string }>; -} -``` - -The engine composes `file` + `args` and calls this; the bin spawns. The engine never imports `child_process`. When `runPackageManager` is absent and a command calls `ctx.packages.*`, the engine resolves `notOk` with the structured failure below (`meta.reason: 'runner-unavailable'`) — it does not throw, so a harness without the seam still exercises the failure path deterministically. - -The child's output is streamed to `onOutput` as it arrives and surfaced as `output` events (§3.8). Stderr is ALSO accumulated, bounded (last 64 KiB), and carried on the failure for the caller's predicate (§5) after redaction (§3.4) — streaming it and buffering it are not alternatives. - -Two cases the return type has to answer even though no child produced them, settled during implementation: - -- **`exitCode` when the child never ran or was killed by a signal.** Both leave the spawner with no exit code of its own, and the field is a plain `number`. It reports `1`. (127 was the alternative, but "command not found" is a lie for the signal case, and the two are not distinguishable without a second field nobody needs — the consumer only branches zero versus non-zero.) -- **`stderr` when the child never started.** A process that failed to spawn wrote nothing, so the failure would carry no account of why the manager did not run. The adapter substitutes the spawn error's own short message, and ONLY when the child wrote nothing itself — so it can never displace real manager output, and it cannot accidentally satisfy §5's pnpm predicate, which matches `ERR_PNPM_*` and catalog/workspace text. -- **Where the 64 KiB bound cuts.** The bound is counted in bytes, and a cut at an arbitrary byte can take a URL's scheme with it — what survives is `user:secret@host`, which §3.4's redaction recognises as nothing at all and passes through verbatim. So the retained window is advanced past its first line break, and a window holding no line break anywhere is one truncated line and is dropped. The tail is therefore at most 64 KiB and always begins at a line boundary. - -**The bin implements this seam in the same change.** It is optional on the interface so the harness can exercise the absent-runner path, not so the shipped binary can omit it: with no bin implementation every `ctx.packages` call in the ORM and Composer ports fails `runner-unavailable`, which is the whole capability dead on arrival. `packages/cli` already depends on execa (`^9.6.1`); the implementation is a thin adapter, and it is the only place on the v8 engine that spawns a package manager. It is not yet the only place in the repo: the legacy commander shell spawns its own, with its own detection, in `packages/cli/src/controllers/init.ts` and `packages/cli/src/lib/agent/package-manager.ts`. That becomes true when S2d ports `init` and retires the shell. - -## 3. What the engine owns - -1. **Manager choice — the engine detects.** `request.manager` if present, else `Runtime.packageManager` if the host supplied one, else detection from the project at `cwd`. Detection always yields a concrete manager, so `ctx.packages` can never fail for want of one (operator ruling, 2026-08-11: the engine either finds a way to perform the action or fails with a structured error — "we couldn't tell what you use" is neither). - - **The mechanism is the ORM's, copied** (operator ruling, 2026-08-11): the `package-manager-detector` package, exact-pinned at `1.8.0` — the version the ORM resolves today, zero runtime dependencies, which is the same bar `@stricli/core` was held to. `detect({ cwd })` with the library's default strategies, then the user agent, then `'npm'`. Using the same library rather than a re-implementation is the point: the ORM's `init` must behave identically before and after the port, and a hand-rolled lookalike would diverge the first time the library changed a precedence rule. - - **One exception: the engine reads the user agent from `Runtime.env`, not from the library.** The library's `getUserAgent()` reads `process.env.npm_config_user_agent` directly, which R4 forbids — the engine takes the environment through `Runtime`, never off the process. Reading a file under `cwd` is not an R4 violation (R4 bans process globals, and the engine already resolves modules from `cwd` in `dependencyResolvable` and reads files in the config loader), but reading `process.env` plainly is. So `detect({ cwd })` is used as-is and only the user-agent step is done by the engine: take `Runtime.env['npm_config_user_agent']`, split on `/`, accept the leading token if it names a known manager. That parse is four lines and is exactly what the library does. The practical payoff is that `createTestCli`'s `env` seed decides that step instead of the ambient process, so the test is deterministic. - - Three consequences of the library's behavior, inherited deliberately and documented here so they are not rediscovered as bugs: it walks parent directories with no project boundary, so a stray lockfile anywhere above the project is picked up; within one directory a `package.json` `packageManager` field beats a lockfile; and the walk stops one level BELOW the filesystem root, so a lockfile at `/` is never seen and a `cwd` of `/` (the test harness's default) inspects nothing at all and falls through to the user agent. - - **`Runtime.packageManager` is deleted.** It exists today only to spell the install command inside `missingDependencyError` (`src/execution/needs.ts`, reached from `needs.dependencies` and `ctx.requireDependency`), it is populated from `npm_config_user_agent` alone (`packages/cli/src/v8/runtime.ts`), and that variable is unset whenever the CLI is not invoked through a package-manager script — which is the common case. Its `'unknown'` member is what forces that error into a next action with no runnable command. Detection replaces it at both call sites, and `'unknown'` leaves the codebase. What remains on `Runtime` is an OPTIONAL `packageManager?: PackageManagerId` override for hosts that know better; `createTestCli`'s existing seed becomes that override unchanged. Reading the project from disk is not an R4 violation — R4 bans process globals, and the engine already resolves modules from `cwd` in `dependencyResolvable` and reads files in the config loader. -2. **Argv spelling** per manager for both forms. The table is the ORM's current spelling, so the port is behavior-identical (install: ` add [-D]` for npm/pnpm/yarn/bun — note the ORM uses npm's `add` alias, not `install` — and `deno add [--dev] npm:`; run: `npx` / `pnpm dlx` / `yarn dlx` / `bunx` / `deno run -A npm:`). One module owns the table; nothing else in the engine or in any command spells a manager command, and `missingDependencyError` reads the same table instead of its own private copy. -3. **Events.** One `step-started` / `step-finished` pair per operation, the step label carrying the human-readable command (`pnpm add -D prisma-next`). In json mode these frame like every other step event. -4. **Redaction.** Captured stderr is redacted before it is stored on the failure or surfaced anywhere: URL userinfo (`https://user:token@host` → `https://…@host`) and values of environment-variable-looking assignments containing `TOKEN`/`KEY`/`SECRET`/`PASSWORD`. The redaction helper is engine-internal and unit-tested on its own. - - **The composed command line is redacted the same way, everywhere it leaves the engine** — the step label, `meta.command`, and the failure's `run-command` remedy — with no exception for the remedy (amendment 13). The only spelling that keeps its credential is the argv handed to `Runtime.runPackageManager`, because that is the one that has to run. -5. **The structured failure.** Code `CLI.PACKAGE_MANAGER_FAILED` — one code for both forms, named for what it covers rather than for the install form alone, because machine consumers branch on `code` and a failed `run` reporting an install code is a lie (operator ruling, 2026-08-11, superseding the `CLI.INSTALL_FAILED` name this document first carried). Shape: - - `summary`: "Installing packages with failed" / "Running with failed". The manager name is interpolated from the resolved manager; no manager name is ever hardcoded in a message, here or anywhere else in the engine. - - `meta`: `{ form: 'install' | 'run', manager, command, exitCode, stderrTail, reason?: 'runner-unavailable' }` (`stderrTail` redacted, bounded). - - `nextActions`: one `run-command` action whose `command` is the redacted command line — character for character the string `meta.command` carries (§3.4) — labeled "Run the install yourself" (install form) / "Run the command yourself" (run form). The labels still hold: the user runs their own install, re-supplying their own credential where one was redacted out. - - Exit code: the command's own documented code decides what the *command* exits with; `CLI.PACKAGE_MANAGER_FAILED` itself is an ordinary expected failure (2) when returned as the command's primary error. -6. **Cancellation.** `ctx.signal` aborts the child through the seam's `signal`. An aborted operation THROWS the signal's abort reason rather than resolving `notOk` (operator ruling, 2026-08-11): a returned structured error settles 2 like any other expected failure, so resolving `notOk` on abort would make Ctrl-C during an install exit differently from Ctrl-C anywhere else. Throwing reaches `settleThrown` → `settleAborted` and settles 130/143. This is the one exception to §2.2's "never a throw": an abort is not an install that failed. - - Re-verified after main's `8d3f7c5` ("Ctrl-C exits 130 because the engine says so"), which made the engine's record of the delivered signal decide the exit code for a handler that caught the abort and returned **successfully**. That does not reach `settleErrored`, which still settles 2 for every returned structured error — so the ruling's premise is intact and the throw is still what earns the 130. Proven by counterfactual rather than by reading: replacing the throw with a returned failure turns both 130-asserting tests red with `expected 2 to be 130`. What main's change DID falsify is the narrower claim that a thrown abort is the only route to 130/143 — `settleCompleted` and `settleSessionCompleted` now also reach those codes from the signal record. Throwing additionally avoids the shape that rule produces for a handler returning `ok()` after a signal: a COMPLETED envelope carrying exit code 130. Cancelling an install yields `CLI.ABORTED`, which is what a cancelled thing should look like. -7. **Serialization.** Concurrent `ctx.packages.*` calls are NOT permitted (operator ruling, 2026-08-11) — two package managers writing one project's lockfile corrupt it. A second call made while one is in flight is caller error, not a race the engine papers over: the engine rejects it as a bug (`CLI.INTERNAL_ERROR`, exit 1), the same treatment any other contract violation gets. -8. **Progress output.** The package manager's own stdout/stderr reaches the user as `output` events (`source: `, `channel: 'data' | 'diagnostic'`) while the operation runs, so a slow install is not silence under a static step label. This is why the seam takes an `onOutput` callback (§2.3) rather than only returning a final buffer: the seam's shape is a published cross-repo contract, and adding streaming to it after the ORM and Composer compile against it would cost a coordinated release across three repos. Rendering policy is the engine's under R5 — commands hand the operation over and say nothing about how it is displayed. - -### 3a. Details settled during implementation - -Answers to questions the sections above left open. Recorded so they are contract, not folklore. - -- **Redaction covers the streamed output too**, both channels, not only the captured stderr tail. §3.4's wording named stderr, but the `--json` event stream is precisely where a token must not land, and a manager prints to stdout at least as readily. Test-pinned. -- **The engine assembles lines from chunks.** The seam delivers whatever the pipe gives it; the `output` event's field is a line. The engine holds a partial line across chunks, flushes any remainder when the child exits, and strips a trailing `\r`. -- **The engine does not re-bound the stderr tail.** The 64 KiB bound is the seam's contract and the adapter enforces it in bytes; re-bounding in the engine would apply a different unit to the same value for no gain. -- **Runner-unavailable still fills the whole `meta` shape** — `exitCode: 1` (the same sentinel §2.3 sets for a child that never ran) and `stderrTail: ''` — so no consumer has to branch on whether a field is present. -- **An operation that never ran emits no step events.** §3.3 says one pair per operation; a call that failed because there is no runner never became one, and announcing `step-started` for a command nothing spawned would tell the user we tried. -- **Summaries are sentences** and end with a period, matching every other engine summary; the fragments quoted in §3.5 are the wording, not the punctuation. - -## 4. What the engine explicitly does NOT do - -- No version resolution, no lockfile awareness, no workspace/catalog logic. -- No parsing of any manager's error output. The engine reports; interpretation belongs to the caller. -- No retries. Retry policy is caller logic (§5). -- No network probes, no registry configuration, no proxy handling — the child inherits the user's environment. -- No global installs; there is deliberately no `global` option. - -## 5. The caller-side retry precedent (context, not engine work) - -The ORM's `init` falls back from pnpm to npm when pnpm fails with a recognized workspace/catalog-resolution error. That policy stays in `init`'s handler: a documented predicate over `meta.stderrTail`, then a second `ctx.packages.install({ …, manager: 'npm' })`. The engine's `manager` override exists for exactly this call shape. The engine must not learn pnpm's error strings. - -The predicate already exists — `isRecognisedPnpmResolutionError` in the ORM's `init.ts` matches `ERR_PNPM_WORKSPACE_PKG_NOT_FOUND`, `ERR_PNPM_NO_MATCHING_VERSION`, and three regexes over catalog/workspace specifier text. It moves to the ported handler unchanged. Confirming the engine's side of the contract is enough here: `meta.stderrTail` must carry enough of stderr for those matches to still fire, which the 64 KiB bound covers, and redaction must not eat the `ERR_PNPM_*` tokens (it targets URL userinfo and secret-looking assignments, so it does not). - -## 5a. Who actually consumes this - -Verified against both product repos (2026-08-11): - -- **The ORM** is the consumer. Its `init` installs two dependency sets through `execFile` and runs the agent-skills installer through the one-off runner form, with the pnpm→npm fallback above. -- **Composer does not install anything, by explicit design.** It never invokes a package manager in any shipped code path; it resolves already-installed bins and modules itself (walking up `node_modules/.bin`, `createRequire`) and, when something is missing, raises a structured error naming the fix. That rule is stated in three separate file headers. Composer therefore needs no part of this capability, and its port should not acquire one. -- **This repo's own `init`** is the second real consumer: today it installs `@prisma/compute` via execa (`packages/cli/src/controllers/init.ts`), and S2d ports it. S2d's contract does not currently mention that install; it should be amended to route it through `ctx.packages` once this lands. - -## 6. Testing - -- `createTestCli` gains a `packageManagerRunner` seed (same shape as `Runtime.runPackageManager`). Absent → the runner-unavailable failure path. Present → a spy/scripted fake; tests can assert the composed `file`/`args`/`cwd` per manager and script exit codes and stderr, so a consumer command's full install matrix (success, failure, fallback retry, cancellation) runs with no network and no real manager. -- Engine unit coverage: the spelling table (every manager × both forms × dev flag), redaction, event framing, the absent-runner failure, abort propagation, the concurrent-call rejection. -- Detection coverage on real temp directories, one case per precedence step, mirroring the ORM's own suite (`test/commands/init/detect-package-manager.test.ts`) so a behavior change in the library is caught here rather than in the port: lockfile in `cwd`; lockfile in an ancestor; `packageManager` field beating a lockfile in the same directory; user agent used when no manifest or lockfile exists anywhere; the `npm` default when nothing at all matches. -- Type tests: `ctx.packages` present iff `installsPackages: true`, mirroring the `managesCredentials` type tests. - -## 7. The R13 amendment - -R13's text (PR #128's requirements record) gains the exception in its own words, in the same change: the CLI never installs or manages packages *on its own initiative or into its own hidden state*; a command may run the user's package manager in the user's project through the engine's package operations, which make the command visible, the failure structured, and the execution bin-owned. The prohibition on the CLI acquiring command submodules or maintaining private package state stands. - -## 8. Coordination and sequencing - -- Lands on PR #140's own branch (`spec/package-manager-capability`) — the implementation joins the spec on the open PR rather than stacking behind it (operator, 2026-08-11). Touches `packages/cli-engine` and the bin's Runtime assembly in `packages/cli`, then ships in a published `@prisma/cli-engine` version; the ORM port consumes it only from the published package. -- The S3/Composer stream is editing the same package; sequence the PR with the operator to avoid overlapping edits in `commands.ts` / `context.ts` / `runtime.ts` / `testing.ts`. -- The shipped engine source is normative where this document and the code disagree on existing mechanisms (overload shape, event kinds, settlement) — follow the code's established patterns. - -## 9. Acceptance - -- [ ] `installsPackages: true` adds typed `ctx.packages`; absent declaration means no such property (type test). -- [ ] Both operations compose correct argv for npm, pnpm, yarn, bun (unit-tested table), execute only through `Runtime.runPackageManager`, and never import `child_process` in the engine (a test asserting the engine's import graph — the project's conformance checker does not exist yet and this slice does not build it). -- [ ] Detection resolves a concrete manager from the project (§3.1), with unit coverage per signal and per precedence step; `Runtime.packageManager` is no longer the source of truth and survives only as an optional host override, with its two former call sites reading the detected value. -- [ ] The bin implements `runPackageManager`, covered against real child processes (streaming order, the byte bound, non-zero exit, a missing executable, abort), and `assembleRuntime` is asserted to wire it. Driving a real package manager is deliberately NOT a test: it would need the network and a writable project, and the seam exists precisely so that is not required. -- [ ] Step events frame both operations in human and json modes; the manager's own output surfaces as `output` events. -- [ ] Failures are `CLI.PACKAGE_MANAGER_FAILED` with the documented meta shape, redacted bounded stderr, and a `run-command` next action carrying the redacted command; absent runner yields the same code with `reason: 'runner-unavailable'`. No message hardcodes a manager name. -- [ ] Abort via `ctx.signal` cancels the child and settles 130/143. -- [ ] A second concurrent `ctx.packages.*` call fails as a bug (`CLI.INTERNAL_ERROR`, exit 1). -- [ ] `createTestCli` seeds `packageManagerRunner`; a sample command's install matrix is testable offline. -- [ ] R13's text amended in the same PR. -- [ ] Cataloguing the code is explicitly NOT in this slice: the engine catalogues its codes nowhere, and building that catalogue is project slice S9, ruled to land after the ports and the commander shell's retirement (operator, 2026-08-11). - -## 10. Amendment log — operator rulings, 2026-08-11 - -Each entry names what the first draft said, what it says now, and why. - -1. **Detection replaces `Runtime.packageManager`** (§3.1). Was: the bin detects and hands in a manager, engine has no detection. Now: the engine detects with the ORM's own library, `Runtime.packageManager` is deleted and reappears only as an optional host override. Why: the field was fed by `npm_config_user_agent` alone, which is unset for a directly-invoked CLI, so the spec's motivating example resolved `'unknown'` on the common path. -2. **An undetectable manager is not a failure mode** (§3.1). Detection ends at `'npm'`, so the operation is always attempted. Why: offering an operation and then refusing it for want of detection is the worst of both. -3. **`deno` joins `PackageManagerId`** (§2.2). Why: the ORM supports it today; omitting it is a silent parity regression. **Needs operator confirmation — this widens a frozen §2 name.** -4. **`CLI.INSTALL_FAILED` → `CLI.PACKAGE_MANAGER_FAILED`** (§3.5), and no message hardcodes a manager name. -5. **Cancellation throws instead of resolving `notOk`** (§3.6). Why: only a thrown abort reaches `settleAborted`; a returned error settles 2, so Ctrl-C during an install would have exited differently from Ctrl-C anywhere else. -6. **Concurrent calls are rejected as a caller bug** (§3.7). Why: two managers on one lockfile corrupt it, and silently serializing hides the caller's mistake. -7. **The manager's output is streamed** (§3.8, §2.3). The seam gains `onOutput`. Why: the seam is a published cross-repo contract, so adding streaming after the ports compile against it costs a coordinated release. -8. **The bin implements the seam in this change** (§2.3). Why: optional on the interface is for the test harness, not for the shipped binary; without it every `ctx.packages` call fails at runtime. -9. **Consumers verified** (§5a). Composer never invokes a package manager by design and needs none of this; this repo's own `init` is a second consumer via S2d. -10. **Documenting the code moves out of this slice** (§9) into project slice S9. -11. **Success returns nothing** (§2.2). Was: `ok({ command })`. Why: the capability exists to keep the command away from the package manager, and returning a rendered command line on success hands back precisely what was abstracted away. R5 forbids a product printing it regardless. -12. **`defineCommand`'s overloads are deleted** in favour of one generic signature (§2.1), with vitest type matchers required to prove the inference holds. -13. **The failure's remedy is redacted like everything else** (§3.4, §3.5), operator ruling, 2026-08-11. Was: the `run-command` next action carried the command line with its secrets intact, while the step label and `meta.command` carried a redacted copy, on the grounds that a command offered for the user to run has to actually run. Now: one unconditional rule — every command line the engine emits is redacted, and only the argv handed to the seam keeps the credential. Why: the capability's whole job is that secrets do not escape into output, and an exception carved into the one component whose purpose is not leaking secrets is where leaks live. Anyone who supplied a credential-bearing package specifier already holds that credential and can reconstruct the line; printing it back at them buys a little convenience and risks an unrecoverable leak into a CI log. One unconditional rule beats a rule plus an exception. - -## 10a. Two seams, deliberately - -`Runtime.spawn` (main's terminal handoff, `09c1df5`) and `Runtime.runPackageManager` land in the same interface and both start a child process. They are not duplicates and should not be unified, on four counts, each doing work: - -- **Terminal ownership.** `spawn` inherits stdio so the child owns the terminal and Ctrl-C reaches it natively — which is exactly why a spawning command rejects `--json`. The package runner pipes, so the engine can frame the operation as events and package operations work under `--json`. -- **Credentials.** `SpawnRequest` carries a fully composed environment including injected credentials; it is the mechanism behind `needs: { credentials: 'child' }`. The package-manager seam takes no environment at all, and must not — a package manager has no business receiving a Prisma service token. -- **Settlement.** `spawn` rejects on launch failure and settles the child's status verbatim (128+n for signals). The package runner resolves everything and produces `CLI.PACKAGE_MANAGER_FAILED` at exit 2. -- **Lifetime.** `spawn` returns a live handle the engine tracks across settlement; the package runner is one awaited call with an `AbortSignal`. - -Unifying them would need a mode discriminator leaving half the fields inert in each mode, on a surface published across three repos. - -**What they DO share is the terminal, and that interlock is now explicit** (§3.7): `ctx.packages` refuses while a spawned child holds the terminal, and `ctx.spawn` refuses while a package operation is in flight, both raising the construction error the three pre-existing surfaces (`ctx.present`, `ctx.prompt`, `ctx.spawn`) already raise. Four surfaces now track terminal ownership through their own flags; consolidating that into one claim they all consult is a real improvement and belongs with whoever owns the spawn design, not here. - -## 11. Open — needs a ruling before the ORM port - -**How a command offers a package-manager command it did NOT run.** The ORM's `init --no-install` prints the commands it would have run as manual steps. Ported, that becomes a `run-command` next action — and building one needs a spelled command line while nothing executes. Amendment 11 removed the only way a handler could obtain one, correctly, but this case is real and R5 forbids the product spelling it itself. Two candidate shapes, neither built: - -- **A spelling-only call** on `ctx.packages` that returns the line and runs nothing. Small; matches how the failure's next action already works; the string exists only where the engine is offering a command to the user. Whether that line is redacted needs its own answer under amendment 13 — nothing ran, so there is no seam invocation to carry the credential instead. -- **A next-action shape the engine fills in** — the handler names the operation, the engine renders the whole action. Tighter, but it adds to the shared next-action vocabulary and only earns that if other engine surfaces want the same thing. - -This does not block the current PR: nothing in this repo needs it until `init` ports. diff --git a/.drive/projects/prisma-cli-v8/specs/engine-redirect-table.md b/.drive/projects/prisma-cli-v8/specs/engine-redirect-table.md deleted file mode 100644 index d8053a62..00000000 --- a/.drive/projects/prisma-cli-v8/specs/engine-redirect-table.md +++ /dev/null @@ -1,135 +0,0 @@ -# Engine spec — redirect tables in command-family metadata - -Status: ruled 2026-08-11; §2 amended 2026-08-11 after a design review against the shipped engine (the amendments are listed at the end of this section). Deliverable: one PR to `packages/cli-engine`. The first consumer is the ORM family (ported in prisma/prisma), which carries two retired verbs and four retired flags; the surface in §2 is a cross-repo contract — names are frozen unless the operator re-rules. - -Amendments, all operator-ruled 2026-08-11: - -1. `from` is the path the user types — an absolute path in the mounted tree, the same convention as `MountedTree` keys. The original text called it relative to the family's mount position; a family has no mount position (the shell mounts each command individually, and a family's commands are routinely scattered across several roots), and a relative path would not match what the user typed anyway. Families declare their redirects; `createCli` mounts them, exactly as it mounts commands. -2. A `from` that resolves to anything already in the tree — a command **or a group** — is a construction error. Live commands do not take precedence over redirects, because an overlap is never a legitimate steady state. -3. `replacement` is rendered by the help-example convention that already ships, so a missing `{bin}` is no longer a construction error. -4. Flag redirects ship in the same PR; the option to split them into a follow-up is withdrawn (stricli exports `FlagNotFoundError` carrying the offending flag, so the interception is ordinary work). -5. Cataloguing `CLI.COMMAND_MOVED` alongside the engine's other codes is deferred to project close-out, when the full error list has settled. No code catalogue exists yet, and this PR does not create one. - -## 1. What this is and why - -When a CLI renames a command, users and scripts keep typing the old name for years. The ORM CLI keeps a table of retired invocations (`migration apply` → `migrate --to`, `migration ref` → `ref set|list|delete`, plus four removed `migration status` flags) and answers them with a targeted "use X instead" message rather than a generic unknown-command error. - -The engine has no way to express this today. Registering the dead names as real commands puts them in help and in the grammar tree forever, and a family cannot write a runnable replacement invocation with the binary name in it: the same family mounts under `prisma-next` now and `prisma` at cutover, and the operator's 2026-08-09 ruling keeps binary names out of family-authored invocations entirely. - -So the engine gains a **redirect table** on `CommandFamily`: declarative metadata, consulted only when an invocation fails to resolve. Redirect entries are not commands: they never appear in help, never occupy the grammar tree, and cannot be executed. - -## 2. The surface - -### 2.1 Declaration - -```ts -defineCommandFamily({ - configSection, - commands, - docsBaseUrl, - redirects: [ - { - from: 'migration apply', - replacement: 'migrate --to ', - reason: 'migration apply was replaced by migrate --to.', - }, - { - from: 'migration ref', - replacement: 'ref set|list|delete', - reason: 'Refs are managed by the ref command group.', - }, - { - from: 'migration status', - flag: 'graph', - replacement: 'migration graph', - reason: 'The --graph flag became its own command.', - }, - ], -}) -``` - -Two types, following the `HelpSpec` → `CommandHelp` pattern the engine already uses: the ergonomic one you write, and the total one the family carries. - -```ts -/** What you declare. */ -interface RedirectSpec { - /** The retired invocation as the user types it: a space-separated - * absolute path in the mounted tree, the same convention as - * MountedTree keys. */ - readonly from: string; - /** When present, this is a retired FLAG on a live command: `from` names - * the live command's path and `flag` the retired flag's camelCase name - * (rendered --kebab-case, as flag declarations are). */ - readonly flag?: string; - /** The replacement invocation, written the way help examples are - * written: no binary name, `{bin}` available when the name has to sit - * mid-string. Placeholder arguments use angle brackets (``). */ - readonly replacement: string; - /** One sentence of context, surfaced as the error's `why`. */ - readonly reason?: string; -} - -/** What the normalized family carries. */ -interface CommandRedirect { - readonly from: string; - readonly flag: string | undefined; - readonly replacement: string; - readonly reason: string | undefined; -} -``` - -`redirects` is optional; normalized definitions carry it as an always-present (possibly empty) readonly array of `CommandRedirect`, per the no-conditional-properties ruling. Annotate a declaration with `RedirectSpec`, not `CommandRedirect` — the latter's keys are all required. - -Families declare their redirects; `createCli` collects them from every mounted family into one table, the same way it collects commands. Nothing about a redirect is relative to its family — `from` is what the user types. - -### 2.2 Behavior - -**Verb redirects** (`flag` absent). When argv fails to resolve to any mounted command and the attempted path exactly matches a redirect's `from`, the run settles as an ERRORED envelope: - -- Code: **`CLI.COMMAND_MOVED`** (new, engine-owned). Exit 2. -- Summary: `` `` has been replaced``; `why` from `reason` when present. -- `nextActions`: one `run-command` action, `label` "Use the replacement", `command` = `replacement` rendered by the help-example convention — `{bin}` substituted with `createCli`'s `name` when present, the name prepended when it is not. An angle-bracket placeholder in the command follows the documented placeholder convention (user substitutes the value). - -Longest-match wins if a redirect path prefixes another; matching is exact on path segments, never fuzzy. When no redirect matches, unknown-command behavior is exactly what it is today. - -**Flag redirects** (`flag` present). When a *live* command's parse fails on an unknown flag and the (command path, flag) pair matches an entry, the parse failure is replaced by the same `CLI.COMMAND_MOVED` envelope (the flag named in the summary: `` `--graph` on `migration status` has been replaced``). When no entry matches, today's unknown-flag error is untouched. - -### 2.3 Construction-time validation (fail at `createCli`, like every other tree defect) - -- A verb redirect (`flag` absent) whose `from` resolves to anything already in the tree — a mounted command **or a group** — is a construction error. The group half matters: stricli resolves a bare group path to the help integration, so it never reaches the unknown-command branch, and a redirect sitting there could never fire. Live commands do not silently win; the tree is wrong and says so at construction. -- A flag redirect (`flag` present) whose `from` does NOT name a mounted command is a construction error. -- A flag redirect whose `flag` is not camelCase is a construction error, the same rule flag declarations obey. Matching camel-cases what the user typed, so a `flag` stored as `old-flag` can never be hit: the entry looks plausible and silently does nothing. Added 2026-08-11 after review. -- A `from` that is empty once normalized is a construction error: nothing can produce it. - -`from` is normalized when a `RedirectSpec` becomes a `CommandRedirect` — trimmed, with every run of whitespace collapsed to a single space, so `' migration \t apply '` is stored as `'migration apply'`. A path is a sequence of segments and whitespace is only the separator between them, so a doubled space or a tab has exactly one sensible reading and the engine takes it rather than refusing the declaration. This matters because lookup rejoins the user's argv tokens with single spaces: an un-normalized key holding a doubled space is one no invocation could ever produce, and it would sit in the table silently matching nothing. Added 2026-08-11 (operator) after the first version shipped without it. -- A flag redirect whose `flag` IS declared by the named command is a construction error. -- Two redirects with the same `from` (and, for flag entries, the same `flag`) are a construction error. - -The first and last of these bind across families: `createCli` validates the merged table, so two families cannot each claim the same retired path. - -### 2.4 Non-behavior - -- Redirect entries never appear in `--help` at any level. -- They are invisible to grammar-completeness checks (S7's tree check ignores them). -- They are never executable; there is no handler. -- Telemetry: a redirect settlement reports like any other unmounted/errored run under the existing rules — no new telemetry surface. - -## 3. Testing - -- Unit: matching (exact on path segments, longest-match, no fuzzy, a redirect under a live group), replacement rendering both with and without `{bin}`, each construction-time validation, flag-redirect interception, help output proven free of redirect entries. -- Harness: `createTestCli` needs no new seams — tests assert the `CLI.COMMAND_MOVED` envelope, exit code, and next action through the existing `run()` result. -- Type tests: `redirects` optional on the spec input, always-present on the normalized family. - -## 4. Coordination - -- Lands in `packages/cli-engine`, ships in a published `@prisma/cli-engine` version; the ORM port consumes it only from the published package and its round is sequenced behind that publish (with a fallback: if unpublished when the port reaches it, the ORM ships without redirects and adds them in a follow-up — the port does not block on this). -- The implementation branches off this document's branch (`spec/redirect-table`, PR #141) so the ruled contract travels with the code, and its PR stacks on #141 — retarget to `main` when #141 merges. The S3/Composer stream and the init/shell-retirement stream both have in-flight changes in `execution/command-tree.ts`, `execution/stricli-adapter.ts`, `execution/settlement.ts` and `execution/engine.ts`, so merge down from `main` before opening and expect conflicts in those four files. The package-manager capability (a sibling engine spec) lands independently; its surface is `defineCommand` and `Runtime`, which this PR does not touch. -- Where this document and the shipped engine source disagree on existing mechanisms, follow the code's established patterns. - -## 5. Acceptance - -- [ ] `defineCommandFamily` accepts `redirects`; normalized families always carry the array; `createCli` merges every family's entries into one table. -- [ ] A retired verb settles as `CLI.COMMAND_MOVED`, exit 2, with a `run-command` next action carrying the rendered replacement; unmatched unknowns behave exactly as before. -- [ ] A retired flag on a live command settles the same way. -- [ ] All six construction-time validations fail at `createCli` with clear messages. -- [ ] No redirect appears in any help output (test-proven). diff --git a/.drive/projects/prisma-cli-v8/specs/s1-engine-vertical.md b/.drive/projects/prisma-cli-v8/specs/s1-engine-vertical.md deleted file mode 100644 index 4d7a4e01..00000000 --- a/.drive/projects/prisma-cli-v8/specs/s1-engine-vertical.md +++ /dev/null @@ -1,83 +0,0 @@ -# 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); CommandFamily 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/.drive/projects/prisma-cli-v8/specs/s2-overview.md b/.drive/projects/prisma-cli-v8/specs/s2-overview.md deleted file mode 100644 index 821314c6..00000000 --- a/.drive/projects/prisma-cli-v8/specs/s2-overview.md +++ /dev/null @@ -1,133 +0,0 @@ -# S2 — Platform family port (slice overview) - -S2 ports the platform CLI onto `@prisma/cli-engine` and retires the -commander shell. It ships as FOUR stacked PRs, split by area, each its -own contract + dispatch plan, each ≥1k LOC (operator floor, applies to -the port PRs; infrastructure PRs may be smaller where inherently so): - -| PR | Contract | Content | -| --- | --- | --- | -| S2a | `s2a-foundations.md` | Engine publishable + production dep; `ctx.api`; auth module extraction; the credential manager + the `auth *` family on it; update check; telemetry package move + wiring; clack prompt renderer | -| S2b | `s2b-resources.md` | `project *`, `postgres *` (database), `bucket *` (incl. keys), `branch list` | -| S2c | `s2c-services.md` | `service *` (renamed from `app`, incl. env + domain subgroups), `build *`, `git *`, `agent *`, `feedback` | -| S2d | `s2d-init-and-retirement.md` | `init` wizard; commander-shell deletion; fixture-mode machinery deletion; final parity review | - -Branch mechanics: each PR branches off `main` (S1 merged as PR #129) -and lands into `main`. S2b depends on S2a; S2c on S2b; S2d on S2c. - -## Standing rulings that govern every S2 PR - -All operator-ruled; none are open to implementer judgment. - -1. **Vocabulary**: the contribution/ownership entity is `CommandFamily` - (never "product"/"manifest"; `commandFamily`/`commandFamilies` never - shortened in identifiers). A subgroup is owned by exactly one - command family. `project` belongs to the platform family; Composer - parks under a `composer` root in S3 (TML-3189 holds the final - grammar). -2. **Renames**: the deployable-unit noun is Service — the `app` group - ports as `service` (S2c). No other renames are ruled. -3. **Types**: no conditional properties on stored types — `define*` - inputs may be optional, normalized definitions are total (`T | - undefined` or a natural empty). -4. **Testing**: semantic-first. Commands are tested through - `createTestCli` (`@prisma/cli-engine/testing`) with the management - API faked at `ctx.api` and sessions seeded into the harness's - in-memory credential manager (state read back after the run). - Assertions target the envelope, presented data, events, and exit - codes — NOT output bytes. A single small golden suite per output - surface pins human rendering and channel discipline globally. - Fixture-mode tests are deleted batch-by-batch as their commands - port; no fixture machinery survives S2d. -5. **`ctx.api`**: the management API client lives directly on - `CommandContext` (operator: no extension mechanisms — this is - Prisma's engine). Spec in S2a. -6. **Auth**: an internal module (`packages/cli/src/auth/`), not a - workspace package, holding the credential manager behind the - engine's `CredentialManager` SPI. Sessions are per-workspace, one - current; the `auth *` commands keep their legacy names. Spec in - S2a, design in `../assets/engine/credential-manager-design.md`. -7. **Telemetry is essential**: this CLI reports exactly the way the - ORM CLI does today; the `@internal/cli-telemetry` implementation - moves to this repo (prisma/prisma retires it with its CLI at S5). - Spec in S2a. -8. **`--trace` is dropped** (log levels cover it). The update - notification is ported in S2a (not deferred). -9. **Prompts**: interactive rendering is backed by `@clack/prompts` - 1.5.0 (exact-pinned, fully internal, prompts only — never its - spinners; progress stays engine events). Spike-verified - (2026-08-10); landing spec in S2a. -10. **Parity**: divergences from the shipping CLI are enumerated per - PR in a divergence list for operator review, not discovered. - Maintainability outranks byte parity. - -## Grounding inventory - -`assets/s2/command-inventory.md` catalogues every current command -(flags, positionals, auth requirement, API calls, behavior class, -output, prompts, side effects, tests, engine mapping). S2b–S2d -contracts enumerate their commands FROM that inventory; the inventory -is the single source for "what exists today". - -## Open questions for the operator (S2 ledger) - -**All remaining questions RATIFIED at their stated defaults by the -operator's S2 sign-off, 2026-08-12** (Q6's final URL is still owed as -follow-up work; the interim URL ships). The entries stay below as the -record of what each default was. - -Contracts build to the stated default where one is given; the ruling -can overrule before the affected dispatch runs. - -- **Q1 — auto-login.** ~30 legacy commands auto-launch interactive - OAuth on a TTY when unauthenticated; the engine's `needs.credentials` - fails early instead. Default built to: sign-in structured error + - `auth login` nextAction (consistent, agent-friendly). Ratify or - reinstate auto-login as an engine feature. -- **Q2 — `service run`. RULED (operator, 2026-08-11): dropped.** It does - not port. The command started a local dev server and passed its exit - code through, which is Composer's `dev`. Nothing of the commander - shell survives S2d on its account, and the removal joins the divergence - list alongside `service build` and `service deploy`. - **Corrected at S3 closure (D4):** this row used to add that the engine - does not grow a child-exit-code passthrough, and S3 built one — - `ctx.spawn` plus the `exitWithChildStatus` settlement, for composer's - converge. So the mechanism exists and the command does not, which is - the opposite of the reading S3 was once planned around ("S3 closes Q2 - by building the mechanism"). Q2 stays closed by main's #135, which - dropped the command outright; anything that ever revives it rides the - mechanism that is now there. -- **Q3 — `project env remove`'s `rm` alias** (the only alias in the - tree) does not port. Ratify the drop or rule alias support. -- **Q4 — reading the config file from the shipped binary. RULED (operator, 2026-08-11): copy prisma/prisma and prisma/composer, which both use `c12`.** Our loader does a plain dynamic `import()` of `prisma.config.ts`, which only works under a TypeScript-capable runtime; the shipped binary runs on ordinary Node. Both reference repositories solved this already and identically, so there is nothing to design. `c12` is a dependency, imported dynamically at the call site, invoked as `loadConfig({ name, cwd, configFile? })`. See `packages/1-framework/3-tooling/config-loader/src/load.ts` in prisma/prisma, and the `cli` and `composer` packages in prisma/composer. One ordinary `dependencies` entry is the whole contract: `c12` evaluates TypeScript through `jiti`, which `c12@3.3.4` depends on directly, so installing `c12` installs the part that makes it work on plain Node. `c12`'s only peer dependency is `magicast`, and that one is optional. -- **Q5 — exit-code unification.** R-S2b-3 changes user-visible codes: - legacy consent failures split 1/2 and prod-deploy cancel exits 0; - engine rules make these 2 (structural) and 3 (cancel). Built to - engine rules; ratify. - -Added during the S2a review loop (2026-08-10, built to the stated -defaults): - -- **Q6 — telemetry docs URL.** The first-run disclosure and telemetry - help need this CLI's real telemetry docs page; interim: the existing - prisma.io CLI docs URL. Supply the final URL. -- **Q7 — telemetry config enrichment dropped.** The ORM sender - evaluates `prisma-next.config.*` (c12, arbitrary TS in a detached - child) for two wire fields; dead in this product, so the port drops - the load — `databaseTarget` null, `extensions` empty. Ratify, or - rule a `prisma.config.ts`-based replacement (interacts with the - config-loading ruling above). -- **Q8 — disclosure timing.** Events fire at settlement (`onSettled`, - per contract); the first-run privacy disclosure prints pre-run so - users learn before output, but crashed/killed runs emit nothing - (the ORM's preAction timing emitted before the command). Ratify. - -## Definition of done (whole slice) - -- Every platform command runs on the engine; the commander shell and - fixture machinery are deleted; `prisma-v8` naming is retired in - favor of the real bin wiring (final naming ruled in S2d). -- Per-PR divergence lists reviewed by the operator. -- Engine published and consumed as a production dependency. -- Telemetry reporting live with the ORM-identical client and shared - installation id. diff --git a/.drive/projects/prisma-cli-v8/specs/s2a-foundations.md b/.drive/projects/prisma-cli-v8/specs/s2a-foundations.md deleted file mode 100644 index de15e846..00000000 --- a/.drive/projects/prisma-cli-v8/specs/s2a-foundations.md +++ /dev/null @@ -1,274 +0,0 @@ -# S2a — Foundations (slice contract) - -One PR into `main`, branch `s2a-foundations`. First of S2's four PRs -(`s2-overview.md`). Everything the later port PRs depend on lands here. -The v8 draft (`../assets/engine/engine-interface-draft.ts`) is -normative; the amendments this contract specifies are operator-ruled — -apply them to the draft in the same PR. Nothing in this contract is -open to implementer judgment; where a detail is not stated, the -reference implementation cited for it is the specification. - -## 1. Engine publishable + production dependency - -- `packages/cli-engine/package.json` gains publish metadata: - `version: "8.0.0-rc.1"` (erratum: originally `"0.1.0"`; operator - ruling 2026-08-10 adopted prisma/prisma's versioning machinery and - number — the lockstep jumps to the 8.0.0-rc line), `description` - (one line: the execution engine of - the unified Prisma CLI), `license: "Apache-2.0"`, `files: ["dist", - "README.md", "LICENSE"]`, `repository` (type git, url - https://github.com/prisma/prisma-cli.git, directory - packages/cli-engine), `homepage`, `bugs`, `publishConfig: { access: - "public" }`, `engines.node: ">=22.12.0"` (match `@prisma/cli`). - Add `LICENSE` (copy root/cli Apache-2.0) and a minimal `README.md` - (what the package is, the three subpaths, link to the repo). The - operator publishes manually — no publish workflow in this PR. -- `@prisma/cli-engine` moves from `devDependencies` to `dependencies` - of `packages/cli` (still `workspace:*`). -- `prepack` on cli-engine: `pnpm run build` (match sibling packages). - -## 2. `ctx.api` — the management API client on the context - -Operator-ruled: the client sits directly on `CommandContext`; no -extension mechanism. - -- Engine gains dependency `@prisma/management-api-sdk` at the EXACT - version `packages/cli` currently resolves (pin the resolved version, - not a range; update the cli's own range to the same exact pin in - this PR — committed-versions discipline). -- `Runtime` gains `readonly managementApi: { readonly baseUrl: - string }`. The bin computes `baseUrl` exactly as - `getApiBaseUrl(env)` does today (module moves in §3); the harness - defaults it to `"https://test.invalid"` and `createTestCli`'s spec - gains `managementApi?: { baseUrl?: string; client?: - ManagementApiClient }` — when `client` is supplied, `ctx.api` IS - that object (the uniform mock seam). -- `CommandContext` gains `readonly api: ManagementApiClient`. - `ManagementApiClient` is a type alias the engine re-exports for the - SDK's client type; consumers never import the SDK directly. -- Construction: lazy, once per run, on first property access (Proxy or - getter — match how `execution/command-context.ts` builds the rest of - the context; the client construction itself copies the current - shell's construction in `packages/cli/src/controllers/auth.ts` - (`createManagementApiSdk` call sites) with the token source backed - by `ctx.getCredentials` so refresh during long runs is picked up - per-request. That call site is the reference implementation; do not - redesign it. -- Unauthenticated use: a command WITHOUT `needs.credentials` that - touches `ctx.api` while `getCredentials()` resolves undefined gets a - thrown `CliStructuredError` `CLI.CREDENTIALS_REQUIRED` — the same - code, summary, and nextActions the needs-check failure uses (single - source: the constructor already in `execution/needs.ts` — export and - reuse it; no second phrasing). -- Draft amendment: §4 CommandContext (+`api`), §10 Runtime - (+`managementApi`), §11 harness spec. -- Tests: context exposes the injected fake; lazy construction (no - SDK construction when `api` untouched — assert via a throwing - factory fake); unauthenticated throw path; refresh pickup (two - `getCredentials` values across two `ctx.api` calls). - -## 3. Auth module extraction - -An internal module — NOT a workspace package (operator-ruled). - -Moves (git mv; update every importer; zero behavior change): - -| From | To | -| --- | --- | -| `packages/cli/src/adapters/token-storage.ts` | `packages/cli/src/auth/token-storage.ts` | -| `packages/cli/src/lib/auth/auth-ops.ts` | `packages/cli/src/auth/operations.ts` | -| `packages/cli/src/lib/auth/client.ts` | `packages/cli/src/auth/client.ts` | - -New `packages/cli/src/auth/index.ts` — the module's ONLY public face; -everything else in `src/auth/` is internal to it. Exports exactly: -`readAuthState`, `performLogin`, `performLogout`, `FileTokenStorage`, -`EmptyServiceTokenError`, `isEmptyServiceTokenError`, -`SERVICE_TOKEN_ENV_VAR`, `getApiBaseUrl`, `CLIENT_ID`, the workspace -list/use/logout operations currently in `controllers/auth.ts`'s -real-mode helpers (`listAuthWorkspaces`, `switchAuthWorkspace`, -`logoutAuthWorkspace` — extracted from the controller into -`src/auth/workspaces.ts`, controller delegates; erratum: the review -loop dropped the interim `*Real*` spelling — there is no fixture-mode -counterpart to distinguish from), and -`makeGetCredentials` (moved from `src/v8/runtime.ts`; the v8 runtime -imports it from here), plus `WorkspaceSelectionError` and -`StoredAuthWorkspace` (production consumers exist). The legacy shell -and controllers import ONLY via `src/auth/index.ts` — production code -rule; white-box TESTS of the module's own internals (and `vi.mock` -targets, which must name the module the code under test imports) are -the permitted exception. The `Credentials` shape stays the engine's -`{ token: string }` — S2a does not redesign it. - -**Erratum (2026-08-10, post-review).** The credential-manager rework -landed in this PR after the contract was reviewed, so the module's -public face grew accordingly. `src/auth/index.ts` additionally exports -`FileCredentialManager` + `FetchWorkspaceName` and `fetchWorkspaceName` -(the manager and its injected name lookup), `resolveStateFilePath` / -`STATE_FILE_ENV_VAR` / `DEPRECATED_STATE_FILE_ENV_VAR`, -`claimedWorkspaceId` and `decodeClaims` (the claim decoders the login -command keys sessions by), `authenticatedManagementApiClient`, -`DEFAULT_REDIRECT_URI` / `getAuthBaseUrl`, the recipient-session -helpers, and `storeLegacyCredential` (which now serves the LEGACY shell -only). `performLogin` returns the minted `Credential` instead of -writing it anywhere; custody belongs to the manager. The engine's -`Credentials` shape survives only on the staged-swap fallback path — -`ctx.session()` and `ctx.api` are the surviving auth surfaces. - -## 4. `auth *` family port - -Mounted in the v8 bin under the existing `auth` group. All commands -are result commands in the platform command family. Fixture-mode-only -surface does not port (fixture machinery dies in S2d): `auth login` -loses `--provider`, `--user`, `--workspace` (mock-selection flags). - -| Command | Args | needs | Capability | Behavior | -| --- | --- | --- | --- | --- | -| `auth login` | none | none | `managesCredentials` | `performLogin` returns the minted credential; the command calls `createSession(credential, workspaceIdFromClaims)`. Events: `step-started/finished` for the flow, `endpoint` for the verification URL. Card + the agent-setup tip line when `resolveAgentSetupTipCommand` fires; nextActions `auth whoami`, `project list`, the tip command. Under an env override it SUCCEEDS and prints the mandatory one-line notice that `PRISMA_SERVICE_TOKEN` stays in force until unset | -| `auth logout` | none | none | `managesCredentials` | `sessions()` for the count, then `endAllSessions()`; reports how many it ended. `--workspace` does not exist. Under an env override: refuses when stored sessions exist, succeeds as a no-op when there are none | -| `auth whoami` | none | none | none | `ctx.session()` only (no manager on the context) + `ctx.api` enrichment when online; signed out exits 0 | -| `auth workspace list` | none | none | `managesCredentials` | `sessions()`, the current one marked, a nameless session rendered by its id; states when the env session is in force; json serializer included | -| `auth workspace use [workspace]` | optional positional | none | `managesCredentials` | Command-side ref resolution against `sessions()` (exact id, then case-insensitive name; several matches → `AUTH.WORKSPACE_AMBIGUOUS` listing them), then `useSession(match)`. **Selects only** — a ref it holds no session for is `AUTH.NO_SESSION_FOR_WORKSPACE` telling the user to run `prisma auth login` and pick that workspace in the browser; no browser ever opens from `use`. Absent positional + interactive → `prompt.select` over the sessions; absent + non-interactive → the engine's structural prompt failure | -| `auth workspace logout ` | required positional | none | `managesCredentials` | Same resolution, then `endSession(match)`; prints the workspace it ended | - -The manager resolves no user input: every ref is resolved -command-side, in one shared module in the v8 auth family -(`src/v8/auth/session-ref.ts`), and the matched `Session` is what -reaches the manager. - -Error mapping: the current shell's flat codes port to dotted codes in -the SESSION vocabulary, enumerated in the divergence list. No -documented 4–99 codes in this family. - -Tests: semantic, per ruling — the harness's in-memory credential -manager seeded with `{sessions, currentWorkspaceId, credential, -environmentToken}`, with manager state read back after each run; -`ctx.api` faked where a command enriches; every command × (success, -errored, json, env-override where meaningful); prompt path for -`workspace use` via scripted answers. Delete -`packages/cli/tests/auth.test.ts` fixture-mode cases that cover ported -commands; keep the file's untouched-shell cases until S2d. - -**Erratum (2026-08-10, post-review).** The auth family was reworked -onto the credential manager after this section was reviewed: the table -above is the final state. The reviewed version had the commands calling -the legacy `readAuthState` / `listAuthWorkspaces` / `switchAuthWorkspace` -/ `logoutAuthWorkspace` operations and gave `auth logout` a -`--workspace ` flag. Those operations now serve the legacy shell -alone, and `logout --workspace` is gone — `auth workspace logout ` -is the one way to end a single session. - -## 5. Update check port - -- `packages/cli/src/shell/update-check.ts` moves to - `packages/cli/src/update-check.ts`; its `CliRuntime` parameter - narrows to the exact fields it uses (type them structurally so both - shells satisfy it). The legacy shell keeps consuming it; the v8 bin - (`src/v8/main.ts`) wires it identically to the legacy shell's two - touchpoints: read-and-notify before the run's output settles is NOT - the current behavior — copy the CURRENT sequencing exactly (cached - notify + detached refresh spawn; consult the legacy call sites as - the reference implementation). Notification line goes to stderr. -- Tests: notify-when-cached-newer, refresh-spawn arguments, silence - inside the notification interval, silence in json format (decide by - the current behavior — if the legacy shell prints it in json mode - today, KEEP that and record it in the divergence list; do not - invent a new rule). - -## 6. Telemetry - -Operator-ruled: essential, identical to the ORM CLI's mechanism; the -implementation moves to this repo. - -- New workspace package `packages/cli-telemetry`, name - `@repo/cli-telemetry`, `private: true` (bundled into the cli — it - must appear in the cli's tsdown bundle, not as a published dep). - Source ported from prisma/prisma `packages/1-framework/3-tooling/ - cli-telemetry` (reference clone: `wip/repos/prisma`). Preserve - UNCHANGED: the user-config path and format (shared installation id - with the ORM CLI), gating resolution (consent state, CI detection, - env opt-outs — CI is part of the gating resolution itself, one total - resolver returning enabled/disabled with a reason), the - detached-subprocess sender, endpoint and wire protocol, the - sanitizer's value-free discipline. -- Replace the Commander snapshot type with the engine shape: - `EngineCommandSnapshot { commandPath: readonly string[]; flags: - ReadonlyArray<{ name: string; source: "cli" | "env" | "default" }>; - positionalCount: number }` — no values, ever. -- Engine amendment: `RunHooks` gains `onSettled?: (summary: - RunSummary) => void` where `RunSummary { commandId: string; - exitCode: number; durationMs: number; snapshot: - EngineCommandSnapshot }`, fired exactly once per run after - settlement, never for `--help`/`--version`, errors in the hook are - swallowed (a telemetry bug must not break a command). Draft §10 - amendment. `durationMs` from the injectable clock. -- Bin wiring (`src/v8/main.ts`): resolve gating; when enabled, pass an - `onSettled` hook that spawns the detached sender. Spawn semantics - (fork + IPC + disconnect + unref, every failure swallowed) are - copied from the ORM CLI's util wiring (reference: - `wip/repos/prisma/.../cli/src/utils/telemetry.ts`); TIMING is - onSettled by design — the event fires at settlement, not from a - preAction-style pre-run hook — with the first-run disclosure printed - pre-run (before the command's output). The consequence (crashed / - killed / process.exit runs emit nothing) is recorded in the - divergence list. -- Commands `telemetry status|enable|disable` port from the ORM CLI's - consent surface as engine result commands, mounted shell-owned (no - family), group `telemetry`. Copy the ORM's semantics and copy; - presented as cards; json serializers included. -- Tests: sanitizer (engine snapshot → wire shape), gating matrix - (consent × CI × env), hook firing (once, correct summary, swallowed - throw), consent commands. - -## 7. Clack prompt renderer - -Land the spike design (spike branch `spike/clack-prompts`, commit -903b25a — reference implementation; reimplement cleanly, do not -cherry-pick): - -- `@clack/prompts` exact-pinned `1.5.0`, engine dependency, loaded by - dynamic import only on the interactive path. -- New `packages/cli-engine/src/execution/clack-renderer.ts`: stream - adapters (`Readable.from` over `Runtime.stdin` with `setRawMode` - forwarded; `Writable` over stderr `OutputStream`), prompt mapping - for confirm/consent/select/text with `{ input, output }` injection. -- Branch condition in `execution/prompts.ts`: clack renders IFF no - scripted answers AND `runtime.isTty.stdin` AND - `runtime.stdin.setRawMode` is present; otherwise the existing plain - line renderer. Structural failures and `--yes` resolution stay - BEFORE the branch. Cancellation maps to the existing - `CLI.PROMPT_CANCELLED` path. Clack spinners/log helpers are - forbidden (process-global handlers): progress remains engine - events. -- Draft amendments: two-tier rendering note (§4a); select's - Enter-picks-highlighted note; the accepted - `process.stdout.columns` read quirk. -- Tests: fake raw-mode stdin fixture driving confirm/select/text - through the clack path (assert resolved values + stderr-only - writes); cancellation byte (`\x03`) → exit 3; harness/scripted path - proven clack-free (dynamic import spy). - -## Out of scope - -`project`/`postgres`/`bucket`/`branch` (S2b), `service`/`build`/`git`/ -`agent`/`feedback` (S2c), `init` + shell deletion + fixture removal -(S2d), engine version bumps beyond 0.1.0, Credentials shape redesign, -`composer` root (S3). - -## Acceptance - -- [ ] Operator has published `@prisma/cli-engine@0.1.0` (metadata PR - landed first; publish is the operator's single action). -- [x] `ctx.api` on the context with the harness `client` override; - draft amended; refresh-pickup test green. -- [x] Auth module extracted; legacy shell green against it; v8 runtime - consumes `makeGetCredentials` from it. -- [x] All six `auth *` commands on the engine, over the credential - manager, with semantic tests; fixture-only flags gone; - `logout --workspace` gone; divergence list updated. -- [x] Update check ported to both shells; sequencing matches legacy. -- [x] Telemetry: package ported, hook amendment landed, bin wired, - consent commands mounted, sanitizer value-free by test. -- [x] Clack renderer landed per spike; all prompt tests green - including the clack-path fixture suite. -- [x] Root verification: engine + cli suites, typecheck, lint exit 0. -- [ ] PR ≥1k LOC (expected: well above), divergence list reviewed. diff --git a/.drive/projects/prisma-cli-v8/specs/s2b-design/conventions.md b/.drive/projects/prisma-cli-v8/specs/s2b-design/conventions.md deleted file mode 100644 index 7f5f721e..00000000 --- a/.drive/projects/prisma-cli-v8/specs/s2b-design/conventions.md +++ /dev/null @@ -1,428 +0,0 @@ -# S2b design — conventions binding every dispatch - -Status: BINDING (grounded in the `facts/` extraction sheets; -nothing here is implementer judgment). Operator rulings 2026-08-10: -DECISION 1 ratified (readAuthState helper until ctx.session()); -DECISION 2 ratified (drafted consent questions; deny → exit 2); -DECISION 3 ruled, corrected 2026-08-10: interactivity gating is the -EXISTING `needs: { interaction: true }` (S2a, execution/needs.ts) — -declare it, no engine change needed; the browser-wait helper + -openUrl effect arrive as mechanics via merge-down; consent-grant -flags are engine-owned (see §5). No TTY/CI reads in commands or -helpers, ever. Child documents: `d1-project.md`, `d2-postgres.md`, -`d3-bucket-branch-git.md` — one per dispatch, each pinning its -commands exhaustively. Precedence: slice contract -`../s2b-resources.md` (R-S2b-1..10) > this file > child docs; a child -doc may only add detail, never contradict a rule. - -## 1. File layout (R-S2b-10, pinned) - -One command per file, named for the command path minus the group -directory, kebab-case: - -```text -packages/cli/src/v8/project/list.ts → projectListCommand -packages/cli/src/v8/project/show.ts → projectShowCommand -packages/cli/src/v8/project/create.ts → projectCreateCommand -packages/cli/src/v8/project/link.ts → projectLinkCommand -packages/cli/src/v8/project/rename.ts → projectRenameCommand -packages/cli/src/v8/project/remove.ts → projectRemoveCommand -packages/cli/src/v8/project/transfer.ts → projectTransferCommand -packages/cli/src/v8/project/env-add.ts → projectEnvAddCommand -packages/cli/src/v8/project/env-update.ts → projectEnvUpdateCommand -packages/cli/src/v8/project/env-list.ts → projectEnvListCommand -packages/cli/src/v8/project/env-remove.ts → projectEnvRemoveCommand -packages/cli/src/v8/postgres/list.ts → postgresListCommand -packages/cli/src/v8/postgres/show.ts → postgresShowCommand -packages/cli/src/v8/postgres/create.ts → postgresCreateCommand -packages/cli/src/v8/postgres/usage.ts → postgresUsageCommand -packages/cli/src/v8/postgres/restore.ts → postgresRestoreCommand -packages/cli/src/v8/postgres/remove.ts → postgresRemoveCommand -packages/cli/src/v8/postgres/backup-list.ts → postgresBackupListCommand -packages/cli/src/v8/postgres/connection-list.ts → postgresConnectionListCommand -packages/cli/src/v8/postgres/connection-create.ts → postgresConnectionCreateCommand -packages/cli/src/v8/postgres/connection-rotate.ts → postgresConnectionRotateCommand -packages/cli/src/v8/postgres/connection-remove.ts → postgresConnectionRemoveCommand -packages/cli/src/v8/bucket/list.ts → bucketListCommand -packages/cli/src/v8/bucket/create.ts → bucketCreateCommand -packages/cli/src/v8/bucket/delete.ts → bucketDeleteCommand -packages/cli/src/v8/bucket/key-list.ts → bucketKeyListCommand -packages/cli/src/v8/bucket/key-create.ts → bucketKeyCreateCommand -packages/cli/src/v8/bucket/key-delete.ts → bucketKeyDeleteCommand -packages/cli/src/v8/branch/list.ts → branchListCommand -packages/cli/src/v8/git/connect.ts → gitConnectCommand -packages/cli/src/v8/git/disconnect.ts → gitDisconnectCommand -``` - -Definitions + handler colocated in the file (S1 whoami pattern). -Shared per-group presentation helpers in -`packages/cli/src/v8//presentation.ts`. Shared per-group error -mapping in `packages/cli/src/v8//errors.ts` (v8 auth -precedent). Cross-group helpers (project-ref resolution presentation, -consent helpers) — only if two groups need the identical function — -live in `packages/cli/src/v8/resources-shared/`; a child doc names -each such file explicitly or it does not exist. - -## 2. Mounting (pinned) - -`packages/cli/src/v8/cli.ts` gains, per dispatch: - -- Family-map entries (camelCase keys): `projectList`, `projectShow`, - `projectCreate`, `projectLink`, `projectRename`, `projectRemove`, - `projectTransfer`, `projectEnvAdd`, `projectEnvUpdate`, - `projectEnvList`, `projectEnvRemove`, `postgresList`, …, - `bucketKeyDelete`, `branchList`, `gitConnect`, `gitDisconnect` — - all in the existing (single) platform command family alongside the - auth entries. -- Mount-map entries with exact paths: `"project list"`, …, - `"project env add"`, …, `"postgres backup list"`, - `"postgres connection rotate"`, …, `"bucket key create"`, - `"branch list"`, `"git connect"`, `"git disconnect"`. -- Group declarations with briefs: `project`, `project env`, - `postgres`, `postgres backup`, `postgres connection`, `bucket`, - `bucket key`, `branch`, `git`. Brief strings: the legacy group -descriptions from command-meta.ts, enumerated verbatim in each child -doc's mounting section. - -No `database` path or identifier survives anywhere in v8 code, help, -ids, or tests (R-S2b-1). - -## 3. Auth (R-S2b-2, pinned) - -Every command in this slice declares `needs: { credentials: true }`. -No auto-login. No handler calls the legacy auth guard -(`requireAuthenticatedAuthState` / interactive login) — the engine's -early failure (`CLI.CREDENTIALS_REQUIRED`, exit 2) is the only -unauthenticated behavior. Handlers touch the management API -exclusively through `ctx.api`. - -### 3a. Workspace source (OPERATOR DECISION 1 — ratified 2026-08-10) - -Resource commands need the active workspace (project listing filter, -provider `workspaceId`, plan-limit lookup). `ctx` exposes only -`getCredentials` (`{token}`) and `api` today; RESOLVED at the 2026-08-10 merge-down (session model, engine commit -9384a95): `ctx.session(): Promise` exists — Session -`{ workspaceId, workspaceName?, expiresAt?, source, current }`. The -helper `resolveActiveWorkspace(ctx)` in -`packages/cli/src/v8/resources-shared/workspace.ts` returns the legacy -`{ id, name }` workspace shape from `workspaceId`/`workspaceName` — -`name` falls back to the workspace id when nothing names it (name is a -required string reaching human output; pinned 2026-08-10); having no -workspace behind `needs.credentials` is defensive-only → the ported -`AUTH.USAGE_ERROR` "Workspace required" (copy unchanged). No handler -reads the auth module. - -**Amended 2026-08-10 (rev-6 credential model, auth-stream commit -96e5628).** `ctx.session()` no longer exists. Its replacement is -`ctx.activeCredential(): Promise`, and -`workspaceId` on it is `string | undefined` rather than required — a -credential whose claims name no workspace now reports none instead of -manufacturing an empty id. So the helper reads `ctx.activeCredential()` -and raises the same "Workspace required" error in two cases rather than -one: a null credential, and a credential carrying no `workspaceId`. The -second is exactly what that error's `why` already describes — "the -authenticated session does not have one" — so the copy is unchanged and -this is not a divergence. The `workspaceName ?? workspaceId` fallback -is unchanged. - -## 4. Error mapping (R-S2b-5, pinned) - -- Dotted namespaces by group: `PROJECT.*`, `POSTGRES.*`, `BUCKET.*`, - `BRANCH.*`, `GIT.*`. The subcode is the legacy flat code minus any - redundant group prefix (`PROJECT_NOT_FOUND` → `PROJECT.NOT_FOUND`, - `DATABASE_CONNECTION_NOT_FOUND` → `POSTGRES.CONNECTION_NOT_FOUND`, - `ENV_VARIABLE_NOT_FOUND` → `PROJECT.ENV_VARIABLE_NOT_FOUND`, - `REPO_NOT_CONNECTED` → `GIT.REPO_NOT_CONNECTED`). The child docs - enumerate EVERY legacy code reachable by their commands with its - exact v8 code — an implementer never invents a code. -- Every errored settlement exits 2 (legacy 1→2 recorded once as a - class divergence; the S2a precedent). -- `summary`/`why` text ports verbatim. Legacy `fix` prose maps to one - `user-choice` nextAction; legacy `nextSteps` strings fold into - nextActions (S1/S2a precedent). `meta` is preserved verbatim. -- Mapping is implemented in `v8//errors.ts` following the - exact helper shape of `v8/auth/errors.ts`: a - `_CODE_MAP: Record` table + - `mapOperationError(error: unknown): CliStructuredError | - null` returning null for non-CliError/unmapped codes; callers - `notOk(mapped)` or rethrow (engine settles `CLI.INTERNAL_ERROR`, - exit 1). `new CliStructuredError(code, summary, { why, meta, - nextActions })`; `meta` only when non-empty. -- Fix-text substitutions (OPERATOR RULING 2026-08-10). Two clauses in - legacy `fix` prose describe mechanisms v8 does not have, so every - group mapper rewrites them: `--trace` becomes `--log-level verbose`, - and the legacy `authRequiredError` offer ", or rerun the command in a - TTY to sign in interactively." is deleted, leaving "Run - `${CLI_NAME} auth login`." R-S2b-2 removed auto-login, so no v8 run - can sign in by being rerun in a terminal, and the sentence sent people - to a remedy that no longer exists. The legacy shell keeps the original - string — it still auto-logs-in, and `shell/errors.ts` is shared — so - the rewrite lives in the v8 mappers, never in legacy source. - Divergence entry; both groups that map `AUTH_REQUIRED` test it. -- nextActions on mapped errors: legacy `fix` → exactly one - `{ kind: "user-choice", label: fix }`; each legacy `nextSteps` - command string additionally maps to - `{ kind: "run-command", label: , command: - }` (§0 substitutions applied). This preserves - more than the S2a auth mapper (which dropped nextSteps as - duplicative there) — divergence-list note, one class entry. -- API-passthrough codes (raw API `error.code` in the legacy CliError - `code` field) map mechanically to `.`. - -## 5. Consent (R-S2b-3, pinned — rewritten at the 2026-08-10 merge-down, engine commit 6bb8452) - -Applies to: `project remove`, `project transfer`, -`postgres restore`, `postgres remove`, `postgres connection rotate`, -`postgres connection remove`, `bucket delete`. (`bucket key delete` -has no confirmation today and gains none — divergence review note -only.) The former holds are LIFTED — all seven commands ship. - -- The consent mechanism is ENGINE-OWNED end to end. Commands declare - NO confirm flag; the engine injects the shared repeatable - `--confirm ` flag. -- Handler call: `await ctx.prompt.consent(question, { token })` where - `token` is the EXACT resolved resource id (project.id, database.id, - connection id, bucket id — the legacy exact-id semantics) and - `question` is the command's pinned legacy confirmation `why` - sentence, verbatim (child docs). The previously ratified yes/no - question drafts are superseded by the engine's type-to-confirm - rendering; the ratified sentences survive as the question text. -- Semantics (engine-owned, not re-tested per command beyond the - matrix): interactive → type-to-confirm (clack re-prompts wrong - answers; plain line tier fails structurally on a wrong scripted - answer, exit 2); non-interactive and `--yes` → satisfied iff one - `--confirm ` equals the token exactly (values consumed once - per run); otherwise the engine's `CLI.CONSENT_REQUIRED` (exit 2, - message names the expected value and the `--confirm ` - usage); Ctrl-C/EOF → `CLI.PROMPT_CANCELLED`, exit 3. -- DELETED from the design (unreachable in v8): the per-group - `*.CONFIRMATION_REQUIRED` mapper entries, the - `*.CONSENT_DECLINED` deny path, and the legacy - `meta.expectedConfirm/receivedConfirm` surface — the engine error - replaces them all. Divergence entries: legacy per-command - `--confirm` flag → shared engine flag (same CLI spelling); - CONFIRMATION_REQUIRED → CLI.CONSENT_REQUIRED (meta gone); yes/no - never existed for these commands (type-to-confirm is the new - interactive surface). -- Consent test matrix per command: non-interactive `--confirm ` → - success; non-interactive without → `CLI.CONSENT_REQUIRED` exit 2; - interactive scripted answer = the token → success; wrong scripted - answer → structural failure exit 2; `--yes` without `--confirm` → - `CLI.CONSENT_REQUIRED` exit 2. - -## 6. Secrets (R-S2b-4, pinned) - -Commands: `postgres create`, `postgres connection create`, -`postgres connection rotate`, `bucket key create`. - -- The secret is the `stdout` presentation payload — exact line - format per child doc (ports the legacy renderStdout bytes). -- Human Blocks show the secret masked via `sensitive: true` field - rows. -- The json envelope `result` carries the secret exactly as the legacy - serializer did. - -## 7. Operation layer (R-S2b-10, pinned) - -Handlers call the EXISTING controller/provider operation functions — -no reimplementation of API flows, resolution logic, or validation. -Where an operation takes an SDK/client argument, the handler passes a -client built from `ctx.api` (the exact per-operation call sites are -pinned in each child doc's operation-calls section). Local file side effects (`.prisma/local.json`, -`.gitignore` append) reuse the existing lib functions. The child docs -name the exact function per command step; calling anything else is -out of contract. - -## 8. Presentation (pinned) - -- `human`: blocks via the engine vocabulary. Every command's block - sequence is pinned in its child-doc section (summary tone + text, - field rows with exact labels, table columns with exact headers and - row cell derivations, sort order). -- `stdout`: pinned per command; empty for commands with no legacy - renderStdout payload EXCEPT list/show data rows where the child doc - says otherwise (S2a workspace-list precedent: table data rows go to - stdout). The child doc states the exact lines for every command — - no implementer choice. -- `json`: ports the legacy serializer key-for-key (child doc lists - the keys). Commands with no legacy serializer present the raw - result (S1 precedent) — child doc says which. -- `next`: exact NextAction list per command per state (child doc). -- **The stdout lane carries data, not decoration (amended 2026-08-11).** - "Table data rows go to stdout" means the values, not the cells the - human table happens to render. Where a human column glues two facts - together for readability, stdout takes the raw one. The case that - forced this: `project env list`'s first column is - `` `${key} (${source})` ``, and reusing it for stdout made a piped - line read `STRIPE_KEY (project)` — a consumer would have to split on - `" ("` to recover the key, which defeats the entire reason the lane - exists. stdout carries the bare key; anything needing the source uses - `--json`, which carries the whole record. Check every list command's - stdout rows against this, not just the one that was caught. - **This was never an open question, and treating it as one was an - error.** The Option A channel ruling (2026-08-09, recorded in - `../../assets/engine/whoami-parity-divergences.md`) already settles - it: "human Blocks are presentation prose on stderr; the - `Presentations.stdout` payload lines are the machine-usable payload - and are always written to stdout in human mode — that is what the - surface is for. Human mode is pipe-clean." So the rule is not merely - "do not glue two facts together" — it is that stdout carries values a - program can consume, and every human affordance stays on the human - side. - - Concretely, none of these belong in a stdout row: a size rendered as - `2.0 KiB`, which will not parse back to 2048; the placeholders - `unknown`, `unscoped`, `none` and `default`, which a consumer cannot - tell from a real value of the same text; and a column whose meaning - changes by row, as `postgres list`'s status does when it falls back to - `isDefault`. An absent value is an empty field. The human table keeps - its formatting and its placeholders; where the two differ, the - command builds two sets of rows, as `project env list` already does. -- **Cancellation is never remapped (amended 2026-08-11).** A handler - that wraps a rejected operation in a mapped error must first rethrow - when `ctx.signal.aborted`, so a cancelled run settles as cancelled - rather than as a failure of the thing it was doing. Both wrapping - sites — `project create` and `project link`, each around - `createProject` — do this. -- Title lines follow the S1 card convention: a `summary` block whose - text is the legacy descriptor-derived sentence pinned per command. - -## 9. Events (pinned) - -- Sync commands: no events. -- `git connect` (R-S2b-7): waits through the engine's browser-wait - prompt-family primitive (operator ruling 2026-08-10; landing on - s2a-foundations) — the primitive owns announce/open/poll events; - the handler emits none. Mapping pinned in d3-bucket-branch-git.md - §3.8. Commands and helpers NEVER read TTY/CI state. - -## 10. Tests (R-S2b-9, pinned) - -- One test file per group: - `packages/cli/tests/v8-project.test.ts`, `v8-postgres.test.ts`, - `v8-bucket.test.ts`, `v8-branch.test.ts`, `v8-git.test.ts`. -- Structure copies `v8-auth.test.ts`: `createTestCli({ commands: - , groups, sessions: [], - selectedWorkspaceId: , managementApi: { client: }, - now: () => new Date(0) })`; the fake client is a plain object - implementing exactly the SDK methods the operation layer calls - (typed `as ManagementApiClient`), returning recorded SDK-shaped - responses. Unauthenticated cases: omit the session seeds. - Envelope assertions via the `resultFrame(result.json)` helper - (copy from v8-auth.test.ts). - **Corrected 2026-08-11.** This bullet used to seed - `credentials: { token }` and to require mocking the auth module with - `vi.mock("../src/auth")` for the workspace read. Neither survives the - credential-manager merge-down: the workspace comes from - `ctx.activeCredential()`, which the harness serves from its seeded - sessions, so no auth-module mock is needed and a test following the - old instruction would leave resource commands with no workspace at - all. The shipped group suites are the reference. -- Matrix per command: success; errored (at least one mapped legacy - error); json envelope (commandId, result keys, exitCode, - nextActions); unauthenticated (needs failure: engine sign-in error, - exit 2); consent grant/deny/non-interactive + cancel where §5 - applies; picker path (scripted answers) where R-S2b-6 applies. - Child docs enumerate the exact case list per command — the - implementer adds no cases and drops none without a plan amendment. -- Assertions target `exitCode`, `presented` (data + presentation - arrays), `events`, and the json `result` frame — never raw bytes. -- Golden rendering: extend `v8-golden-rendering.test.ts` by exactly - the entries the child docs name (one representative per new output - surface class), nothing else. -- Build-time coverage test (D1 template, inherited by D2/D3): no such - test exists today (v8-bin.test.ts only asserts `buildCli()` does - not throw), and `buildCli` hides its spec. D1 therefore refactors - `v8/cli.ts` to export the spec pieces as module constants — - `platformCommandFamily` (the `defineCommandFamily` result), - `mountedCommands` (the mount map record), `cliGroups` — consumed by - `buildCli()` unchanged. New test `v8-mount-coverage.test.ts` - asserts by object identity: every value in - `platformCommandFamily.commands` appears in `mountedCommands`, and - every `mountedCommands` value is either in the family or in the - enumerated shell-owned allowlist (`telemetry status|enable|disable`). -- Legacy fixture-test deletion: only the fixture cases covering - commands ported in the SAME dispatch are deleted, per child doc's - explicit file/case list; files keep unported-command cases. - -### 10a. Closure amendments (orchestrator, 2026-08-10) - -Four additions the closure review pass required. Each is an amendment -because §10 otherwise forbids adding cases the child docs do not name. - -1. **An errored case for `project list` and `project env list`.** - R-S2b-9 requires one per command and d1 §3.1 and §3.10 omitted it; - the contract outranks the child doc, so the omission is the error. - These are the only two commands whose error mapper no test reaches, - and a mapper returning null where it should map is a defect this - slice has already shipped once and had to fix. `project list` drives - a 403 and asserts `PROJECT.AUTH_REQUIRED` at exit 2; `project env - list` drives a 500 and asserts `PROJECT.ENV_API_ERROR` at exit 2. -2. **A golden-rendering entry for a masked secret card.** §10 asks for - one representative per new output surface class, and the masked - secret is one — no child doc named it, so none was added. Without it - nothing in this package proves `sensitive: true` reaches the screen - as `********`; the engine could regress and the suite would stay - green. `bucket key create` is the representative: assert the exact - stderr card including the mask and the exact four stdout lines. - Note what the mask is and is not — the card masks while stdout - prints the same secret in the clear a line later, because that is - how the caller receives it. It is a scroll-back and screen-share - courtesy, not containment. -3. **The mount-coverage test asserts a literal command list.** Its - three existing assertions compare the two maps only to each other, - so deleting a command from both leaves it green and it would pass on - a five-command CLI. It gains a fourth assertion comparing the sorted - mount paths to a literal sorted array — the only one that can catch - a deletion or a misspelling, and the test S2c and S2d will lean on. -4. **The legacy-context adapter reports its own limits.** `v8/project/ - context.ts` casts a three-field object to the legacy `CommandContext`. - The cast is accepted for this slice and the structural fix stays with - S2d, but a future legacy edit reading a fourth field currently - compiles clean and throws an unhelpful runtime error — worst case - inside `project transfer`, after the project has already moved. The - adapter therefore refuses unknown reads with a message naming the - key, proven by a test driving all five call sites. - -## 11. Divergences (R-S2b-1/2/3/5/8 + standing ruling 10, pinned) - -New file `.drive/projects/prisma-cli-v8/assets/s2/parity-divergences-s2b.md` -(never edit the shared `parity-divergences.md` — the auth stream owns -it). Grows per dispatch. Format: the S2a file's section style PLUS a -per-command conformance table: - -```markdown -| command | inventory entry | rules applied | divergences | -``` - -Every rename, dropped alias, error-code change, exit-code change, and -presentation-surface change gets a row/entry. D4 consolidates. - -## 12. Verification gate (every dispatch, pinned) - -```bash -pnpm --filter @prisma/cli-engine test -pnpm --filter @prisma/cli test -pnpm --filter @repo/cli-telemetry test -pnpm typecheck -pnpm lint -``` - -All measured by pnpm's own exit code. Green before every commit. - -## 13. Commits (pinned) - -Stage explicitly (never `git add -A`). Commit as the bot: -`git commit -s --trailer "Signed-off-by: Will Madden "`, -body ending `Co-Authored-By: Claude Fable 5 `. -One commit per dispatch minimum; separate commits for legacy-test -deletion. Push to `git@github-wmadden-electric:prisma/prisma-cli.git`. -PR base: `s2a-foundations` (operator ruling 2026-08-10, supersedes -the brief's stacked-on-main note). - -## 14. Hard boundaries (from the handover brief, restated) - -Never touch: `packages/cli/src/v8/auth/**`, `packages/cli/src/auth/**`, -`packages/cli-engine/**` (an engine change needed = STOP), -`.github/workflows/publish.yml`, versioning scripts. Unpinned fact → -STOP and surface; never improvise. diff --git a/.drive/projects/prisma-cli-v8/specs/s2b-design/d1-project.md b/.drive/projects/prisma-cli-v8/specs/s2b-design/d1-project.md deleted file mode 100644 index b6b9dd01..00000000 --- a/.drive/projects/prisma-cli-v8/specs/s2b-design/d1-project.md +++ /dev/null @@ -1,488 +0,0 @@ -# D1 design — the project group (11 commands) + slice template - -> **CONSENT SUPERSESSION (orchestrator amendment, 2026-08-11).** This -> document was drafted before consent became engine-owned. Wherever a -> section below still shows a per-command `confirm` flag or a -> `*.CONFIRMATION_REQUIRED` mapper entry, **conventions §5 wins** — a -> child doc may add detail, never contradict a rule. Commands declare -> NO confirm flag; the engine injects the shared repeatable -> `--confirm `, and the handler calls -> `ctx.prompt.consent( sentence>, { token: })`. The legacy -> `CONFIRMATION_REQUIRED` error is unreachable, so its mapper row is -> dead and `meta.expectedConfirm` / `meta.receivedConfirm` do not -> exist. The affected lines are struck below at their own sites; the -> sections remain the source for every copy string. Added on the -> CodeRabbit review of PR #133, which read the surviving declarations -> as still binding — the per-section notes were not enough. - -Binding design for dispatch D1. Parent: `conventions.md`. Grounding -fact sheet: `facts/facts-d1-project.md` (verbatim legacy extraction -with file:line references — the implementer treats it as part of this -doc). Corrections it makes to the inventory, all binding: -`PROJECT_AMBIGUOUS` is legacy exit 1 (not 2); `PRISMA_PROJECT_ID` is -never read by these commands; "WORKSPACE_REQUIRED" is a `USAGE_ERROR` -with summary "Workspace required". - -## 0. Template work (this dispatch only) - -1. **`v8/cli.ts` refactor**: extract module-level exported constants - `platformCommandFamily` (the existing `defineCommandFamily` call, - now including the auth entries + this dispatch's project entries), - `mountedCommands`, `cliGroups`; `buildCli()` consumes them - unchanged. No behavior change. -2. **`tests/v8-mount-coverage.test.ts`**: per conventions §10 — - identity-based family↔mount coverage with the telemetry allowlist. -3. **`v8/resources-shared/workspace.ts`**: - `resolveActiveWorkspace(ctx)` per conventions §3a (OPERATOR - DECISION 1). Returns the legacy `AuthWorkspace` shape; throws the - mapped `AUTH.USAGE_ERROR` "Workspace required" (copy per fact - sheet §14.3) when absent. -4. **Legacy helper exports**: where a named legacy helper below is - not currently exported from its file, add `export` to it — no - body changes, no moves. Every such edit is listed in the PR - description. - -## 1. Group mounting - -| group | brief (verbatim legacy description) | -| --- | --- | -| `project` | Manage and inspect your Prisma projects | -| `project env` | Manage environment variables for the active project | - -Mount paths `project list|show|create|link|rename|remove|transfer`, -`project env add|update|list|remove`. Family keys `projectList`, -`projectShow`, `projectCreate`, `projectLink`, `projectRename`, -`projectRemove`, `projectTransfer`, `projectEnvAdd`, -`projectEnvUpdate`, `projectEnvList`, `projectEnvRemove`. -`project env remove` mounts WITHOUT the legacy `rm` alias -(R-S2b-8; divergence entry). - -## 2. Shared machinery - -### 2.1 Error mapper `v8/project/errors.ts` - -Shape per conventions §4. Complete map: - -| legacy code (exit) | v8 code | -| --- | --- | -| `USAGE_ERROR` domain project/app (2) | `PROJECT.USAGE_ERROR` | -| `USAGE_ERROR` domain auth "Workspace required" (2) | `AUTH.USAGE_ERROR` | -| `PROJECT_NOT_FOUND` (1) | `PROJECT.NOT_FOUND` | -| `PROJECT_AMBIGUOUS` (1) | `PROJECT.AMBIGUOUS` | -| `PROJECT_SETUP_REQUIRED` (1) | `PROJECT.SETUP_REQUIRED` | -| `LOCAL_STATE_STALE` (1) | `PROJECT.LOCAL_STATE_STALE` | -| `LOCAL_PROJECT_WORKSPACE_MISMATCH` (1) | `PROJECT.LOCAL_WORKSPACE_MISMATCH` | -| `LOCAL_STATE_WRITE_FAILED` (1) | `PROJECT.LOCAL_STATE_WRITE_FAILED` | -| `PROJECT_CREATE_FAILED` (1) | `PROJECT.CREATE_FAILED` | -| `PROJECT_RENAME_FAILED` (1) | `PROJECT.RENAME_FAILED` | -| `PROJECT_REMOVE_BLOCKED` (1) | `PROJECT.REMOVE_BLOCKED` | -| `PROJECT_TRANSFER_REJECTED` (1) | `PROJECT.TRANSFER_REJECTED` | -| `TRANSFER_RECIPIENT_REQUIRED` (2) | `PROJECT.TRANSFER_RECIPIENT_REQUIRED` | -| `TRANSFER_RECIPIENT_UNAVAILABLE` (1) | `PROJECT.TRANSFER_RECIPIENT_UNAVAILABLE` | -| ~~`CONFIRMATION_REQUIRED` domain project (2)~~ | ~~`PROJECT.CONFIRMATION_REQUIRED`~~ — struck: unreachable, the engine's `CLI.CONSENT_REQUIRED` replaces it | -| `PROJECT_LINK_TARGET_REQUIRED` (2) | `PROJECT.LINK_TARGET_REQUIRED` (only reachable via `--yes`-suppressed picker; see 3.4) | -| `WORKSPACE_NOT_AUTHENTICATED` (1) / `WORKSPACE_AMBIGUOUS` (2) | `AUTH.WORKSPACE_NOT_AUTHENTICATED` / `AUTH.WORKSPACE_AMBIGUOUS` (S2a codes; copy from the recipient machinery ports verbatim) | -| `ENV_VARIABLE_ALREADY_EXISTS` (1) | `PROJECT.ENV_VARIABLE_ALREADY_EXISTS` | -| `ENV_VARIABLE_NOT_FOUND` (1) | `PROJECT.ENV_VARIABLE_NOT_FOUND` | -| `ENV_BRANCH_NOT_FOUND` (1) | `PROJECT.ENV_BRANCH_NOT_FOUND` | -| `ENV_BRANCH_SCOPE_IS_PRODUCTION` (1) | `PROJECT.ENV_BRANCH_SCOPE_IS_PRODUCTION` | -| `ENV_BRANCH_CREATE_REQUIRES_DEFAULT_BRANCH` (1) | `PROJECT.ENV_BRANCH_CREATE_REQUIRES_DEFAULT_BRANCH` | -| `ENV_FILE_APPLY_FAILED` (1) | `PROJECT.ENV_FILE_APPLY_FAILED` | -| `ENV_API_ERROR` or API-passthrough code X (1) | `PROJECT.ENV_API_ERROR` / `PROJECT.X` | -| `PROJECT_API_ERROR` or passthrough (1) | `PROJECT.API_ERROR` / `PROJECT.X` | -| legacy 401/403 → `AUTH_REQUIRED` (`apiCallError`) | `PROJECT.AUTH_REQUIRED`, legacy summary/why/nextSteps verbatim, exit 2 — the plain mechanical prefix rule, no special case. Re-amended 2026-08-10 (operator): the ENGINE already owns every real credentials failure — a stored session's 401 is intercepted by the SDK's refresh middleware and settles as `CLI.CREDENTIALS_REQUIRED` (expired / session-ended) or `CLI.AUTH_SERVICE_ERROR`; an env session's returned 401 settles as `AUTH.SERVICE_TOKEN_REJECTED` (engine `api-client.ts`). What still reaches `apiCallError` is the residue the engine deliberately does not claim — chiefly a returned **403** (authenticated but not permitted), which is NOT a sign-in problem. Handlers therefore never hand-build engine credential errors (duplicating engine copy and mislabelling a permission failure), and never rethrow (that would settle `CLI.INTERNAL_ERROR`, exit 1, a bug class). | -| `FEATURE_UNAVAILABLE` (fixture-only) | unreachable in v8 — no entry | - -All copy/meta/fix/nextSteps per conventions §4 and the fact sheet's -verbatim strings, with §0-of-d2-style substitutions: package-runner -`formatCommand` strings become `${CLI_NAME} …`; `prisma auth login` -copy bug normalizes to `${CLI_NAME} auth login` (divergence entry); -`--trace` fix text substitutes per conventions. The legacy -`buildProjectSetupNextActions` list ports verbatim minus its -`journey` fields (v8 NextAction has none; divergence entry). - -### 2.2 Consent - -`project remove` and `project transfer` per conventions §5. -Confirmation copy verbatim (fact sheet §6/§7): - -- remove: summary `Confirm project removal`, why `Removing a project - is permanent, deletes its databases, and stops its apps, so it - requires the exact project id.`, rerun action `${CLI_NAME} project - remove ${id} --confirm ${id}`. -- transfer: summary `Confirm project transfer`, why `Transferring - moves the project to another workspace and this workspace loses - access, so it requires the exact project id.`, rerun action - `${CLI_NAME} project transfer ${id} <--to-workspace | - --recipient-token > --confirm ${id}`. -- Drafted consent questions (OPERATOR DECISION 2): remove — `Remove - project ${id}? This permanently deletes the project, its - databases, and stops its apps.`; transfer — `Transfer project - ${id} to the recipient workspace? This workspace loses access.` - -### 2.3 Operation calls - -Handlers call exactly the functions in fact sheet §14.7, passing -`ctx.api` wherever the table marks a client argument -(`listRealWorkspaceProjects(ctx.api, workspace, ctx.signal)`, -`createAppProvider(ctx.api)`, -`createManagementProjectProvider(ctx.api)`, inline -`ctx.api.POST/PATCH/DELETE` for env writes, -`findVariableByNaturalKey(ctx.api, …)`). `resolveTransferRecipient` -machinery: call `resolveRecipientWorkspaceSession(workspaceRef, -ctx.env, ctx.signal)` directly (it builds its own SDK from stored -tokens by design — recipient credentials are a second identity, not -`ctx.api`'s). Local pin: `writeLocalResolutionPin`, -`ensureLocalResolutionPinGitignore`, read/unlink per fact sheet -§14.6; failures map through the legacy `LOCAL_STATE_WRITE_FAILED` -constructors. Env helpers: `resolveEnvScope`, -`parseKeyValuePositional`, `readEnvFileAssignments`, -`resolveEnvWriteSource`, `resolveScopeToApi`, -`resolveListScopeToApi`, `findVariableByNaturalKey`, `toMetadata`, -`collectEnvironmentVariables` — imported (adding `export` where -needed per §0.4), never reimplemented. - -### 2.4 Project resolution modes (pinned per command, fact sheet §14.1/§15.8) - -- Pin-based resolution (`resolveProjectTarget`; explicit `--project` - → local pin → setup-required error): `rename`, all four `env` - commands. -- Positional-only against the workspace list - (`resolveProjectForSetup`): `remove`, `transfer`, `link` (when arg - given). -- Binding inspection (unbound = success): `show`. -- None: `list`, `create`. -- No command reads `PRISMA_PROJECT_ID`. - -## 3. Per-command design - -Common: `needs: { credentials: true }`; no events; no exitCodes. -data = legacy result minus `verboseContext` (class divergence per -d2 §3 intro). Presentation title lines are pinned per command below -as the `summary` block text. - -### 3.1 `project list` — `v8/project/list.ts` - -- help.summary `List all projects in your workspace`; examples - `project list`, `project list --json`. No args. -- Handler: workspace → `listRealWorkspaceProjects(ctx.api, - workspace, ctx.signal)` → `sortProjects` (name→id) → - `readProjectListLocalBinding(ctx.cwd, workspace, projects, - ctx.signal)`. -- data `{ workspace, projects, localBinding }`. -- human: summary info `Listing projects for the authenticated - workspace.`; fields `workspace: name`; empty → list `["No projects - found."]`; else table `name | id | region` (region cell `none` - when absent). -- stdout: table data rows tab-joined `name\tid\tregion`. -- json: legacy `serializeProjectList`: `{ context: { workspace }, - items: [{name,id,status:null}], count, localBinding }`. -- next: empty when linked; else the two setup actions from - `buildProjectSetupNextActions` with the exact per-state `reason` - strings (fact sheet §1). -- Tests: linked success; not-linked (nextActions asserted); invalid - binding; empty list; json; unauth. - -### 3.2 `project show` — `v8/project/show.ts` - -- help.summary `Show this directory's Project binding`; examples - `project show`, `project show --project proj_123 --json`; flag - `project: flag.string({ brief: "Project id or name", placeholder: - "id-or-name" })`. -- Handler: workspace → `inspectProjectBinding` (unbound = success - with suggestion fields) via `ctx.api`-backed `listProjects`. -- data: the `ProjectShowResult` union verbatim (fact sheet §2). -- human bound: summary info `This directory is linked to the - following platform project.`; fields `local repo: - `, `platform: `, url - row when present, `region` when present. Unbound: summary warn - `This directory is not linked to a Prisma Project.`; fields - `workspace`, `project: Not linked`. -- stdout: `label: value` mirror of the fields block. -- json: result unchanged. next: unbound → the setup actions with the - §2 reason string; bound → none. -- Tests: bound; unbound (success + nextActions); `--project` miss → - `PROJECT.NOT_FOUND` exit 2; ambiguous → `PROJECT.AMBIGUOUS` - (legacy exit 1 → 2, meta.matches verbatim); stale pin; workspace - mismatch; json; unauth. - -### 3.3 `project create ` — `v8/project/create.ts` - -- help.summary `Create a Project and link this directory`; examples - `project create my-app`, `project create my-app --json`; - positional `name` (brief `Project name`); flag `region: - flag.string({ brief: "Prisma Compute region id", placeholder: - "region" })`. -- Handler: workspace → name trim/validate - (`isValidProjectSetupName`; invalid → mapped - `projectSetupNameRequiredError("project create")` → - `PROJECT.USAGE_ERROR`) → `createAppProvider(ctx.api) - .createProject({ name, region, signal })` (failure → - `projectCreateFailedError` with the exact option strings from fact - sheet §3) → local pin write + gitignore (`"created"` action). -- data: `ProjectSetupResult` (fact sheet §3). -- human: three ok summary lines as list-of-blocks: summary ok - `Created Project "${project.name}"`; summary ok `Linked - "${directory}" to Project "${project.name}"`; summary info `Saved - .prisma/local.json`. -- stdout: none. json: result unchanged. -- next: run-command `${CLI_NAME} app deploy` (legacy nextSteps). -- Tests: success (pin written — temp cwd, gitignore appended); - whitespace name; create rejected 403 (permission why/fix - verbatim); pin write failure → `PROJECT.LOCAL_STATE_WRITE_FAILED`; - json; unauth. - -### 3.4 `project link [id-or-name]` — `v8/project/link.ts` (picker, R-S2b-6) - -- help.summary `Link this directory to a Project`; examples - `project link`, `project link proj_123`, - `project link "Acme Dashboard" --json`; positional - `project: positional.optionalString({ brief: "Project id or - name", placeholder: "id-or-name" })`. -- Handler: workspace → `listRealWorkspaceProjects` → - - arg given: `resolveProjectForSetup(ref, projects, workspace)` - (ambiguous/not-found map per §2.1) → bind (`"linked"`). - - no arg: `ctx.prompt.select("Which Project should this directory - use?", options)` where options are pinned: first `{ value: - "__create__", label: "+ Create a new Project" }`; then projects - sorted name→id, `value: project.id`, `label: project.name` or - `` `${name} (${id})` `` on duplicate names; last `{ value: - "__cancel__", label: "Cancel" }`. Non-interactive/`--yes` → the - engine's structural prompt failure (`CLI.PROMPT_REQUIRED`, exit - 2) — the legacy `PROJECT_LINK_TARGET_REQUIRED` rich error does - NOT port on this path (R-S2b-6 + S2a workspace-use precedent; - divergence entry + operator-review flag, candidates meta is - lost). - - `__cancel__` → mapped legacy cancel usage error → - `PROJECT.USAGE_ERROR` (`Project setup canceled` + link cancel - why/fix/nextSteps verbatim, fact sheet §4). - - `__create__` → `ctx.prompt.text("Project name", { placeholder: - inferTargetName(ctx.cwd).name, default: })` (empty input - falls back to the suggestion — engine default semantics) → - `createAppProvider(ctx.api).createProject` (failure copy per - fact sheet §4) → bind (`"created"`). -- data/human/json/next: as 3.3 (`"linked"` renders only the - Linked + Saved lines). -- Tests: explicit arg success; explicit ambiguous/not-found; picker - select existing (scripted answers); picker create-new (text - answer + default fallback); picker cancel; non-interactive → - `CLI.PROMPT_REQUIRED` exit 2; json; unauth. - -### 3.5 `project rename ` — `v8/project/rename.ts` - -- help.summary `Rename the resolved Project`; examples - `project rename "Acme Dashboard v2"`, - `project rename billing-api --project proj_123`; positional - `name` (brief `New project name`); flag `project` (as 3.2). -- Handler: workspace → validate name (legacy helper — its "Project - create requires a name" copy bug ports verbatim; divergence-list - note, not a fix) → pin-based resolution → - `createManagementProjectProvider(ctx.api).renameProject({ - projectId, name, signal })`. -- data `{ workspace, project: renamed, previousName }`. -- human: summary ok `Renaming project.`; fields `workspace`, - `project: ${previousName}`, `id`; list `["The project is now named - "${project.name}". Directory bindings pin the project id, so they - stay valid."]`. -- stdout: none. json: result unchanged. next: none. -- Tests: success; 422 → `PROJECT.RENAME_FAILED` (API message/hint - passthrough); unbound dir → `PROJECT.SETUP_REQUIRED`; json; - unauth. - -### 3.6 `project remove ` — `v8/project/remove.ts` (consent) - -> HOLD LIFTED (merge-down 2026-08-10): build per conventions §5 — -> `ctx.prompt.consent(, { token: project.id })`, -> no confirm flag declaration, engine `--confirm` grants -> non-interactively. Ignore this section's references to a declared -> `confirm` flag and to `PROJECT.CONFIRMATION_REQUIRED` (both -> superseded by conventions §5). - -- help.summary `Remove a Project permanently after exact id - confirmation`; example - `project remove proj_123 --confirm proj_123`; positional - `project` (brief `Project id or name`); ~~flag `confirm: - flag.string({ brief: "Exact project id required to remove", - placeholder: "project-id" })`~~ — struck, no flag is declared. -- Handler: workspace → positional-only resolve → consent (§2.2) → - `createManagementProjectProvider(ctx.api).removeProject` → - `cleanupLocalPinForProject` semantics (pin delete when matching; - delete failure → warning diagnostic severity `warn` with the - legacy warning text, NOT an error). -- data `{ workspace, project, localPin: { cleared } }`. -- human: summary ok `Removing project.`; fields `workspace`, - `project`, `id`; list `["The project, its databases, and its apps - were removed."` + when cleared `"This directory's local project - binding was cleared."]`. -- stdout: none. json: result unchanged. next: none. -- Tests: success + pin cleared; success pin-delete-failure (warning - diagnostic asserted); consent matrix; 400 → - `PROJECT.REMOVE_BLOCKED`; not-found/ambiguous; json; unauth. - -### 3.7 `project transfer ` — `v8/project/transfer.ts` (consent) - -> HOLD LIFTED with 3.6 — same conventions §5 mechanism, token = -> project.id. Completing 3.6 + 3.7 (and their legacy fixture-test -> deletion) is round-2+ work for this dispatch. - -- help.summary `Transfer a Project to another workspace after exact - id confirmation`; examples per fact sheet §7; positional - `project`; flags `toWorkspace: flag.string({ brief: "Locally - authenticated workspace to receive the project", placeholder: - "id-or-name" })`, `recipientToken: flag.string({ brief: "Access - token for the receiving workspace", placeholder: "token" })`, - ~~`confirm` (brief `Exact project id required to transfer`)~~ — - struck, no flag is declared. -- Handler order (fact sheet §7): both recipient flags → - `PROJECT.USAGE_ERROR` (mutual-exclusion copy verbatim); neither → - `PROJECT.TRANSFER_RECIPIENT_REQUIRED`; resolve positional-only; - consent (§2.2); `resolveTransferRecipient` (service-token guard → - `PROJECT.TRANSFER_RECIPIENT_UNAVAILABLE`; recipient errors → - AUTH.* per §2.1); `transferProject({ projectId, - recipientAccessToken, signal })`; pin rewrite/clear/none semantics - + failure warnings verbatim. -- data `{ workspace, project, recipient, localPin: { action } }`. -- human: summary ok `Transferring project.`; fields `workspace`, - `project`, `id`, `recipient` (name ?? id ?? `workspace of the - provided recipient token`); list with the two detail sentences per - state (fact sheet §7). -- stdout: none. json: result unchanged. -- next: `--to-workspace` runs → run-command `${CLI_NAME} auth - workspace use `; else none. -- Tests: to-workspace success (+ nextAction + pin rewritten); - recipient-token success (+ pin cleared); both-flags; neither; - service-token guard; recipient ambiguous/not-authenticated; - consent matrix; 400 → `PROJECT.TRANSFER_REJECTED`; json; unauth. - -### 3.8–3.11 `project env add|update|list|remove` — `v8/project/env-*.ts` - -Shared: flags `role: flag.enum({ brief: "Project template scope -(production or preview)", values: ["production", "preview"] })` -(engine enum parse failure replaces commander's choices error; -divergence entry), `branch: flag.string({ brief: "Preview branch -override scope", placeholder: "git-name" })` (list uses brief -`Preview branch resolved scope`), `project` flag as 3.2. All scope / -input parsing via the legacy helpers (§2.3) — every usage-error copy -verbatim (fact sheet §14.2), mapped `PROJECT.USAGE_ERROR`. Pin-based -project resolution. API writes inline on `ctx.api`. - -**add** (`env-add.ts`): positional `assignment: -positional.optionalString({ brief: "Variable assignment as -KEY=VALUE or KEY from the current environment", placeholder: -"assignment" })`; flag `file: flag.string({ brief: "Read KEY=VALUE -assignments from a dotenv file", placeholder: "path" })`. -help.summary `Create a new environment variable.`; examples: the six -descriptor examples verbatim (fact sheet §8). Flow: write-source → -scope (requireExplicit) → parse input → resolve scope -(createBranchIfMissing true; default-branch guard) → duplicate check -(→ `PROJECT.ENV_VARIABLE_ALREADY_EXISTS`) → POST; branch-scope -preview-missing warning ports as a `warn` diagnostic with the legacy -text. File mode: per-key precheck, sequential writes, mid-loop -failure → `PROJECT.ENV_FILE_APPLY_FAILED` with meta -`{file, failedKey, writtenKeys}` and the split-file nextSteps -verbatim as run-command actions — a `#`-comment line is NOT its own -action: it becomes the `reason` of the immediately following -run-command action (amended 2026-08-10 after D1 round 1). data: single `{ projectId, scope, variable }`; file -`{ projectId, scope, variables, file: {path, count} }`. human: -single — summary info `Setting a new environment variable.`, fields -`project`, `scope`, `key`, `id`, `last updated`; file — summary info -`Setting new environment variables from file.`, fields `target: - from `, table `variable | id | status` rows -`` `${key} (${source})` ``/id/`default`-when-managed, empty → list -`["No environment variables imported."]`. stdout: none (metadata -only, no values — legacy). json: `stripVerboseContext` shape. next: -none. - -**update** (`env-update.ts`): identical surface; help.summary -`Replace an existing environment variable's value.`; examples -verbatim. Differences: `createBranchIfMissing: false` (→ -`PROJECT.ENV_BRANCH_NOT_FOUND`); missing var → -`PROJECT.ENV_VARIABLE_NOT_FOUND` (update copy); PATCH; no preview -warning; file-mode missing-keys error + retry steps verbatim. -Titles: `Replacing the environment variable's value.` / file -`Replacing environment variable values from file.`, empty `["No -environment variables updated."]`. - -**list** (`env-list.ts`): no positional, no `--file`. help.summary -`List environment variable metadata for a scope (no values).`; -examples verbatim. Flow: optional scope → `resolveListScopeToApi` -(explicit / local-git-branch / overview modes with the exact target -computation, fact sheet §10) → variables via the legacy list -functions (effective-row overlay + ordering preserved). data -`{ projectId, scope, target, variables }`. human: summary info -`Listing environment variables for the selected scope.`; fields -`target: ` (label rules verbatim incl. `" (not -created yet)"`); table `variable | id | status` as add's file mode; -empty → list `["No environment variables defined in this scope."]`. -stdout: table data rows tab-joined. json: legacy `serializeEnvList` -shape verbatim (context/items/count/variables + scope + target). -next: empty-list → run-command `${CLI_NAME} project env add -KEY=value `; else none. - -**remove** (`env-remove.ts`): positional `key: -positional.string({ brief: "Variable key to remove", placeholder: -"key" })`; flags role/branch/project; NO file flag; NO rm alias. -help.summary `Remove an environment variable from a scope.`; -examples verbatim. Flow: scope (requireExplicit) → resolve -(createBranchIfMissing false) → find (missing → -`PROJECT.ENV_VARIABLE_NOT_FOUND`, remove copy) → DELETE. data -`{ projectId, scope, key }`. human: summary info `Removing the -environment variable from the scope.`; fields `project`, `scope`, -`key`. stdout: none. json: strip shape. next: none. - -Env tests (each command): success role scope; success branch scope -(incl. branch-create path for add, default-branch guard error); -scope usage errors (both flags / neither / — for add/update — both -input sources / bad assignment / env-key fallback); duplicate / -missing variable errors; file mode success + partial-failure -(`meta.writtenKeys`); list overview + local-git + not-created-yet -target labels + empty nextAction; remove success + missing; json -envelope each; unauth each. - -## 4. Divergence entries this dispatch adds - -The d2 §4 classes 2/3(with this doc's map)/4/7/8/9/10/11 apply -identically, plus: -1. `rm` alias dropped (R-S2b-8). -2. Picker non-interactive path: `CLI.PROMPT_REQUIRED` replaces - `PROJECT_LINK_TARGET_REQUIRED` (candidates meta lost) — flagged - for operator review. -3. `PROJECT_AMBIGUOUS` legacy exit 1 → 2. -4. Env `--role` invalid values: engine enum error replaces - commander choices error. -5. Legacy `AUTH_REQUIRED` residue from `apiCallError` (403/permission - class) maps mechanically to `PROJECT.AUTH_REQUIRED`, exit 2 (§2.1 - final pin); real credential failures are engine-settled. The - `prisma auth login` copy bug does not port. -6. NextAction `journey` fields dropped. -7. Rename's "Project create requires a name" copy bug ports - verbatim (recorded, not fixed). -8. Warn-diagnostic code for the env preview-default warning is - `PROJECT.ENV_PREVIEW_DEFAULT_MISSING` (pinned 2026-08-10 after D1 - round 1; operator ratifies via the divergence list). -9a. The remove/transfer local-pin cleanup warnings carry the - already-pinned `PROJECT.LOCAL_STATE_WRITE_FAILED` code at `warn` - severity (no new code invented; pinned 2026-08-10 after D1 round - 2; operator ratifies via the divergence list). -9. Legacy resolution/env functions taking the shell CommandContext - are called through the `v8/project/context.ts` runtime-slice - adapter (cwd/env/signal only, read-surface verified) — accepted - for this slice; the structural-signature cleanup belongs to S2d - when the legacy shell dies. - -## 5. Legacy test deletion (this dispatch) - -Delete fixture-mode cases covering the 11 ported commands from: -`project.test.ts`, `project-mutations.test.ts`, -`project-controller.test.ts` (whole files if nothing else remains), -and the env-command cases in `app-env.test.ts`-family files that are -fixture-driven. Keep: `project-real-mode.test.ts`, -`project-resolution.test.ts`, `project-usecases.test.ts` (use-case -files die in S2d with the fixture machinery), env unit tests of the -helpers (still production code). - -## 6. Conformance rows - -One row per command in `assets/s2/parity-divergences-s2b.md`, per -conventions §11. diff --git a/.drive/projects/prisma-cli-v8/specs/s2b-design/d2-postgres.md b/.drive/projects/prisma-cli-v8/specs/s2b-design/d2-postgres.md deleted file mode 100644 index 7ff2ab5b..00000000 --- a/.drive/projects/prisma-cli-v8/specs/s2b-design/d2-postgres.md +++ /dev/null @@ -1,598 +0,0 @@ -# D2 design — the postgres group (11 commands) - -> **CONSENT SUPERSESSION (orchestrator amendment, 2026-08-11).** This -> document was drafted before consent became engine-owned. Wherever a -> section below still shows a per-command `confirm` flag or a -> `*.CONFIRMATION_REQUIRED` mapper entry, **conventions §5 wins** — a -> child doc may add detail, never contradict a rule. Commands declare -> NO confirm flag; the engine injects the shared repeatable -> `--confirm `, and the handler calls -> `ctx.prompt.consent( sentence>, { token: })`. The legacy -> `CONFIRMATION_REQUIRED` error is unreachable, so its mapper row is -> dead and `meta.expectedConfirm` / `meta.receivedConfirm` do not -> exist. The affected lines are struck below at their own sites; the -> sections remain the source for every copy string. Added on the -> CodeRabbit review of PR #133, which read the surviving declarations -> as still binding — the per-section notes were not enough. - -Binding design for dispatch D2. Parent: `conventions.md` (layout, -mounting, error-mapping, consent, secrets, tests). Grounding: -`scratchpad fact sheet facts-d2-postgres.md` extracted from the legacy -code; legacy references cite `packages/cli/src/...`. Every "ports -verbatim" below means byte-identical strings except the pinned -substitutions of §0. - -## 0. Rename substitutions (R-S2b-1, applied everywhere) - -- Command paths, ids, files, groups: `database` → `postgres` - (`postgres.connection.rotate` etc.). No alias. -- Inside ANY user-facing string (help, examples, why/fix text, - nextAction commands): a legacy command reference - `prisma-cli database …` becomes `${CLI_NAME} postgres …`. The - resource noun "database" in prose is NOT renamed (the resource is a - Prisma Postgres database). -- The legacy package-runner command formatter - (`resolvePrismaCliPackageCommandFormatterSync`) is NOT used in v8 - strings: every command string in nextActions is - `${CLI_NAME} postgres …` (S1/S2a precedent). Divergence entry - (one class entry). -- Legacy fix text `"Re-run with --trace for the underlying API - response details."` becomes `"Re-run with --log-level verbose for - the underlying API response details."` (`--trace` is dropped by - standing ruling 8). Divergence entry. - -## 1. Group mounting - -Groups and briefs (legacy descriptions ported, rename applied): - -| group | brief | -| --- | --- | -| `postgres` | Manage Prisma Postgres databases for a project | -| `postgres backup` | Inspect platform-created database backups | -| `postgres connection` | Manage one-time-view database connection strings | - -Mount paths: `postgres list|show|create|usage|restore|remove`, -`postgres backup list`, -`postgres connection list|create|rotate|remove`. -Family keys: `postgresList`, `postgresShow`, `postgresCreate`, -`postgresUsage`, `postgresRestore`, `postgresRemove`, -`postgresBackupList`, `postgresConnectionList`, -`postgresConnectionCreate`, `postgresConnectionRotate`, -`postgresConnectionRemove`. - -## 2. Shared machinery (implemented once, in the named files) - -### 2.1 `v8/postgres/context.ts` — group context helper - -`resolvePostgresContext(ctx, { projectRef, branchName }, commandName)`: - -1. Workspace: per conventions §3a (the pinned workspace source). - Missing workspace → the ported `AUTH.USAGE_ERROR` "Workspace - required" (copy verbatim from `workspaceRequiredError`, - errors.ts:141: summary `Workspace required`, why `This command - needs an active workspace, but the authenticated session does not - have one.`, nextAction user-choice from fix `Run ${CLI_NAME} auth - login and choose a workspace.` + run-command `${CLI_NAME} auth - login`). -2. Project resolution: call the EXISTING - `resolveProjectTarget({ context-free inputs })` - (`lib/project/resolution.ts:155`) with - `listProjects: () => listRealWorkspaceProjects(client, workspace, - signal)` where `client` is `ctx.api`. Conversion of resolution - errors stays in the existing - `projectResolutionErrorToCliError`; the v8 error mapper (§2.5) - maps the resulting CliErrors. -3. Provider: `createManagementDatabaseProvider(ctx.api, - { workspaceId: workspace.id })` — the `formatCommand` option is - NOT passed (v8 strings are `${CLI_NAME}`-phrased; the provider's - fallback formatter output is replaced by the v8 error mapper - rewriting nextSteps into nextActions, see §2.5). - -`resolvePostgresProviderOnly(ctx)` (for `connection rotate|remove`): -no workspace requirement, no project resolution — -`createManagementDatabaseProvider(ctx.api, { workspaceId: -workspace?.id })` with workspace best-effort per conventions §3a. -Matches legacy `requireDatabaseProviderOnly` (database.ts:765). - -Both helpers never call `requireAuthenticatedAuthState` / -`authenticatedManagementApiClient` — `needs.credentials` + `ctx.api` -replace them (R-S2b-2). - -### 2.2 Database resolution - -Reuse the legacy `resolveDatabase(provider, target, databaseRef, -branchName, signal)` flow (database.ts:912) — call it if importable -without dragging fixture machinery; otherwise the handler-side copy -in `v8/postgres/resolve.ts` reproduces it EXACTLY: - -- blank ref → usage error `Database id or name required` / `This - command needs a database id or name.` / nextAction from `Pass a - database id or name.` + run-command `${CLI_NAME} postgres list` - → `POSTGRES.USAGE_ERROR`, exit 2. -- 0 matches → `POSTGRES.NOT_FOUND` (legacy `DATABASE_NOT_FOUND`), - copy verbatim incl. the scope suffix - `` in project "…"[ on branch "…"] ``, exit 2 (legacy 1→2 class). -- >1 → `POSTGRES.AMBIGUOUS`, copy + `meta.matches` verbatim, exit 2. -- 1 match → `provider.showDatabase(id, { projectId, signal })`, - `ensureProjectId` fallback exactly as database.ts:955-960. - -### 2.3 Exact-id confirmation + consent - -Per conventions §5 (rewritten at merge-down — engine consent tokens; -holds lifted): no per-command `confirm` flags, no -`requirePostgresConfirmation` helper, no `POSTGRES.CONFIRMATION_ -REQUIRED`. Each consent command calls `ctx.prompt.consent(, { token: })` before the mutation, -at the same point the legacy check sat. The copy below remains the -question-text source. The v8 consent helper -`v8/postgres/consent.ts::requirePostgresConfirmation` reproduces -`requireExactConfirmation` (database.ts:976) semantics: pass iff -`confirm === id` (strict). On flag mismatch/absence in -non-interactive contexts → `POSTGRES.CONFIRMATION_REQUIRED`, exit 2, -copy per command (see per-command sections; `fix` → -run-command nextAction `${CLI_NAME} `), -meta `{ expectedConfirm, receivedConfirm }` verbatim -(`receivedConfirm: null` when absent). Interactive without the flag: -`ctx.prompt.consent(question)` with the question pinned per command -(§ per-command); grant → proceed; deny/cancel → conventions §5. - -### 2.4 Plan-limit error (ports PR #127) - -The v8 mapper detects legacy `PLAN_LIMIT_REACHED` CliErrors → -`POSTGRES.PLAN_LIMIT_REACHED`, exit 2, summary/why verbatim -(`Workspace plan limit reached` / `Database operations are blocked -because this workspace has used the operations included in its plan. -This is a workspace plan limit, not a Prisma outage.`), meta verbatim -(`workspaceId, blockedFeature, planName, usageBlocked, upgradeUrl`), -nextActions exactly one `user-choice`: -- with upgradeUrl: label `Upgrade the workspace plan`, reason - `` Upgrade at ${upgradeUrl}${planName ? ` (current plan: ${planName})` : ""}. `` -- without: label `Upgrade the workspace plan`, reason `Open Prisma - Console and upgrade the affected workspace plan.` - -The legacy `humanLines` full-rendering override does not port (no v8 -equivalent; engine renders the error layout). Divergence entry: -plan-limit recovery lines move from bespoke human rendering to -why + nextAction + meta. The 3s best-effort subscription lookup -(`readWorkspaceSubscription`, provider.ts:871) still runs inside the -provider — unchanged, no port work. - -### 2.5 Error mapper `v8/postgres/errors.ts` - -Follows `v8/auth/errors.ts` helper shape (conventions §4). Catches -CliError from the operation layer and maps by code. Complete map — -implementers add no entries: - -| legacy code (exit) | v8 code (exit 2 unless noted) | -| --- | --- | -| `USAGE_ERROR` domain database (2) | `POSTGRES.USAGE_ERROR` | -| `USAGE_ERROR` domain auth — workspace required (2) | `AUTH.USAGE_ERROR` | -| `DATABASE_NOT_FOUND` (1) | `POSTGRES.NOT_FOUND` | -| `DATABASE_AMBIGUOUS` (1) | `POSTGRES.AMBIGUOUS` | -| ~~`CONFIRMATION_REQUIRED` (2)~~ | ~~`POSTGRES.CONFIRMATION_REQUIRED`~~ — struck: unreachable, the engine's `CLI.CONSENT_REQUIRED` replaces it | -| `PLAN_LIMIT_REACHED` (1) | `POSTGRES.PLAN_LIMIT_REACHED` | -| `DATABASE_CONNECTION_MISSING` (1) | `POSTGRES.CONNECTION_MISSING` | -| `DATABASE_CONNECTION_STRING_MISSING` (1) | `POSTGRES.CONNECTION_STRING_MISSING` | -| `DATABASE_BACKUPS_UNSUPPORTED` (1) | `POSTGRES.BACKUPS_UNSUPPORTED` | -| `DATABASE_RESTORE_CONFLICT` (1) | `POSTGRES.RESTORE_CONFLICT` | -| `DATABASE_BACKUP_NOT_FOUND` (1) | `POSTGRES.BACKUP_NOT_FOUND` | -| `DATABASE_API_ERROR` (1) | `POSTGRES.API_ERROR` | -| any other API-passthrough code `X` (1) | `POSTGRES.X` (mechanical: prefix the raw API code; e.g. API code `planLimitReached` never reaches here — caught above) | -| `PROJECT_NOT_FOUND` / `PROJECT_AMBIGUOUS` / `PROJECT_SETUP_REQUIRED` / `LOCAL_STATE_STALE` / `LOCAL_PROJECT_WORKSPACE_MISMATCH` | `PROJECT.NOT_FOUND` / `PROJECT.AMBIGUOUS` / `PROJECT.SETUP_REQUIRED` / `PROJECT.LOCAL_STATE_STALE` / `PROJECT.LOCAL_WORKSPACE_MISMATCH` — shared with D1's mapper (single source in `v8/project/errors.ts`; D2 imports) | -| `AUTH_REQUIRED` / `AUTH_CONFIG_INVALID` | unreachable in v8 (needs.credentials / S2a auth errors); if seen, bug — rethrow | - -For every mapped error: summary/why verbatim (+ §0 substitutions); -legacy `fix` → one `user-choice` nextAction (label = fix text); -legacy `nextSteps` strings → `run-command` nextActions (command = -the string, §0-substituted); `meta` verbatim. - -### 2.6 Presentation helpers `v8/postgres/presentation.ts` - -- `postgresTargetLabel(projectName, branchName)` → - `branchName ? `${projectName} / ${branchName}` : projectName` - (legacy `formatDatabaseTarget`). -- `formatStatus(db)` → `db.status ?? (db.isDefault ? "default" : - "unknown")` (legacy presenters/database.ts). -- `formatBackupSize(size)` → legacy rules verbatim: null → - `unknown`; else B/KiB/MiB/GiB, 1024 boundaries, one decimal. -- Secret-bearing card rows use `sensitive: true` for the - connection-string row (conventions §6). - -## 3. Per-command design - -CONSENT SUPERSESSION (merge-down 2026-08-10): in sections 3.5, 3.6, -3.10, 3.11 below, IGNORE any `confirm: flag.string(...)` declaration -and any drafted yes/no "consent question ... pending ratification" -text — both predate the engine consent-token mechanism. The binding -form is conventions §5: no flag declaration; at the legacy check's -position call `ctx.prompt.consent(, { token: })`; the engine's -shared `--confirm` grants non-interactively; the consent test matrix -is conventions §5's. The sections' summaries/why sentences/rerun -nextActions/meta notes remain the copy source, except -`meta.expectedConfirm/receivedConfirm` (engine-owned error now; no -such meta). - -Common to all 11: `needs: { credentials: true }`; command family = -platform; no events; diagnostics always empty; no documented 4–99 -exit codes (`exitCodes` omitted). Result `data` = the legacy result -object minus `verboseContext` (the verbose-context block does not -port — `--verbose` is a log level in v8, not a data toggle; -divergence entry, one class). Json presentation = the legacy -serializer shape minus `verboseContext` (which `stripVerboseContext` -already removed — so key-identical to legacy `--json` output; state -per command below). - -### 3.1 `postgres list` — `v8/postgres/list.ts` - -- help.summary: `List Prisma Postgres databases for the resolved - project`; examples: `postgres list`, - `postgres list --branch feature/foo`, `postgres list --json`. -- args.flags: `project: flag.string({ brief: "Project id or name", - placeholder: "id-or-name" })`, `branch: flag.string({ brief: - "Branch git name", placeholder: "git-name" })`. -- Handler: `resolvePostgresContext` → - `provider.listDatabases({ projectId, branchName, signal: - ctx.signal })` → sort `branchName ?? "" → name → id` ascending - via localeCompare (legacy `sortDatabases`). -- data: `{ projectId, projectName, branchName: branchName ?? null, - databases }`. -- human: summary block info `Listing databases for the resolved - project.`; fields block `project:` + (`branch:` when set); empty → - list block `["No databases found."]`; else table - `Name | Branch | Region | Status | Id` — Branch `unscoped` when - null, Region `unknown` when null, Status via `formatStatus`. -- stdout: the table's data rows as tab-joined - `name\tbranch\tregion\tstatus\tid` lines (S2a workspace-list - precedent: list data rows are the machine payload); empty list → - no stdout lines. Divergence entry (legacy wrote nothing to stdout). -- json: legacy `serializeDatabaseList` shape verbatim: - `{ context: { project, branch? }, items: [{ name, id, status }], - count, projectId, branchName, databases }` (items.status = - `isDefault ? "default" : null`). -- next: none. -- Tests: success (2 dbs, sort proven); success empty; `--branch` - filter passthrough; json envelope (commandId `postgres.list`, keys - as above); unauthenticated (engine sign-in error, exit 2); errored - (API failure → `POSTGRES.API_ERROR` or passthrough-coded, exit 2); - plan-limit (`POSTGRES.PLAN_LIMIT_REACHED`, meta + nextAction - pinned). - -### 3.2 `postgres show ` — `v8/postgres/show.ts` - -- help.summary: `Show database metadata without secret values`; - examples: `postgres show db_123`, - `postgres show acme-preview --branch preview --json`. -- args.positionals: `database: positional.string({ brief: "Database - id or name", placeholder: "database" })`; flags: project, branch - (as 3.1). -- Handler: context → `resolveDatabase` → `provider.listConnections - (database.id, { signal })`. -- data: `{ projectId, projectName, database, connections }`. -- human: summary info `Showing database metadata.`; fields in order: - `project`, `database` (name), `id`, `branch` (`unscoped` when - null), `region` (`unknown` when null), `status` (formatStatus), - `connections` (count as string). -- stdout: `label: value` lines mirroring the fields block (S1 whoami - precedent). -- json: `{ projectId, projectName, database, connections }` (legacy - `stripVerboseContext` shape). -- next: none. -- Tests: success by id; success by name; not-found → - `POSTGRES.NOT_FOUND` exit 2 (why includes project/branch scope); - ambiguous → `POSTGRES.AMBIGUOUS` + meta.matches; json; unauth. - -### 3.3 `postgres create ` — `v8/postgres/create.ts` (secret) - -- help.summary: `Create a Prisma Postgres database and print its - one-time connection URL`; examples: `postgres create my-db`, - `postgres create my-db --branch feature/foo --region eu-central-1`. -- args.positionals: `name: positional.string({ brief: "Database - name", placeholder: "name" })`; flags: `region: flag.string({ - brief: "Prisma Postgres region id", placeholder: "region" })`, - project, branch. -- Handler: trim name; whitespace-only → `POSTGRES.USAGE_ERROR` - (`Database name required` / `Database create needs a non-empty - name.` / nextActions: user-choice `Pass a database name.` + - run-command `${CLI_NAME} postgres create `), exit 2. Context - → `provider.createDatabase({ projectId, name, branchName, region, - signal })` → `ensureProjectId`. -- data: `{ projectId, projectName, database, connection, - connectionString }`. -- human: summary ok `` Created database "${name}" in - ${postgresTargetLabel(...)}. `` + list block `["The connection URL - below is shown once, so save it now."]` + fields block with row - `connection URL` value = connectionString, `sensitive: true`. (The - legacy `Creating database...` progress line does not port — sync - command, no events; divergence entry, class: pre-result progress - lines dropped for sync commands.) -- stdout: exactly `[connectionString]` (legacy - `renderDatabaseCreateStdout`). -- json: `{ projectId, projectName, database, connection, - connectionString }`. -- next: none. -- Tests: success (stdout = bare URL; human masks; envelope carries - connectionString); whitespace name → usage error; connection - missing (`POSTGRES.CONNECTION_MISSING`, copy verbatim); - connection-string missing (`POSTGRES.CONNECTION_STRING_MISSING`); - plan-limit; json; unauth. - -### 3.4 `postgres usage ` — `v8/postgres/usage.ts` - -- help.summary: `Show usage metrics for a database`; examples: - `postgres usage db_123`, - `postgres usage acme-production --from 2026-06-01 --to 2026-06-30`. -- args.positionals: `database` (as 3.2); flags: `from: flag.string({ - brief: "Start of the usage period", placeholder: "iso-date" })`, - `to: flag.string({ brief: "End of the usage period", placeholder: - "iso-date" })`, project, branch. -- Handler: parse dates FIRST (before context), reproducing - `parseUsageDate` rules verbatim (fact sheet "usage date - validation": date-only regex → UTC day-boundary expansion - T00:00:00.000Z / T23:59:59.999Z; datetime prefix + calendar - round-trip check; invalid → `POSTGRES.USAGE_ERROR` `Invalid usage - period` with the exact legacy why per flag; from>to → the exact - range-error copy). Then context → `resolveDatabase` → - `provider.getUsage(database.id, { from, to, signal })`. -- data: `{ projectId, projectName, database, period, metrics, - generatedAt }`. -- human: summary info `Showing database usage metrics.`; fields: - `project`, `database`, `id`, `period` = `` `${start||"unknown"} to - ${end||"unknown"}` ``, `operations` = `${used} ${unit}`, `storage` - = `${used} ${unit}`, `generated` (`||"unknown"`). -- stdout: `label: value` mirror. json: strip shape. next: none. -- Tests: success; date-only expansion asserted via fake client - receiving expanded query; invalid date → usage error exit 2 - (both flag variants); from>to; json; unauth. - -### 3.5 `postgres restore ` — `v8/postgres/restore.ts` (consent) - -- help.summary: `Restore a database from a backup after exact id - confirmation`; example: - `postgres restore db_123 --backup bkp_456 --confirm db_123`. -- args.positionals: `database: positional.string({ brief: "Target - database id or name", placeholder: "database" })`; flags: - `backup: flag.string({ brief: "Backup to restore from", - placeholder: "backup-id" })`, `sourceDatabase: flag.string({ - brief: "Database the backup belongs to (defaults to the target)", - placeholder: "database" })`, ~~`confirm: flag.string({ brief: "Exact - target database id required to restore", placeholder: - "database-id" })`~~ — struck, no flag is declared — project, branch. -- Handler order (legacy database.ts:471): (1) blank `--backup` → - `POSTGRES.USAGE_ERROR` `Backup id required` / `Database restore - needs the backup to restore from.` / nextActions from fix+nextStep - referencing `${CLI_NAME} postgres backup list `; (2) - context; resolve target; resolve source when `--source-database` - set (same branch scope) else source = target; (3) confirmation via - §2.3 with copy: summary `Confirm database restore`, why `Restoring - immediately and irreversibly overwrites all data in the target - database, so it requires the exact target database id.`, - rerun nextAction `` ${CLI_NAME} postgres restore ${database.id} - --backup ${backupId}[ --source-database ${source.id}] --confirm - ${database.id} ``; consent question (interactive, pending operator - ratification — conventions §5): `Restore database ${database.id} - from backup ${backupId}? This immediately and irreversibly - overwrites all data in the target database.`; (4) - `provider.restoreDatabase({ targetDatabaseId, sourceDatabaseId, - backupId, projectId, signal })`. -- data: `{ projectId, projectName, database: restored, source: { - databaseId, backupId } }`. -- human: summary ok `Restoring database from backup.`; fields: - `project`, `database`, `id`, `backup`, + `source` only when source - ≠ target; list block: - `` The restore is running; the database status is - "${status ?? "recovering"}" until it completes. `` and - `Connections and credentials are preserved.` -- stdout: none. json: strip shape. -- next: run-command `${CLI_NAME} postgres show ${database.id}` (the - only success nextAction in the group — legacy nextSteps). -- Tests: success (incl. nextAction); missing --backup; confirm - matrix (absent non-interactive → engine consent failure exit 2; - absent interactive → consent grant proceeds / deny per conventions - §5 / cancel exit 3; mismatched --confirm → - `POSTGRES.CONFIRMATION_REQUIRED` + meta); 409 → - `POSTGRES.RESTORE_CONFLICT`; 404 → `POSTGRES.BACKUP_NOT_FOUND`; - source-database variant; json; unauth. - -### 3.6 `postgres remove ` — `v8/postgres/remove.ts` (consent) - -- help.summary: `Remove a database after exact id confirmation`; - example: `postgres remove db_123 --confirm db_123`. -- args: positional `database` (brief `Database id or name`); flags - ~~`confirm: flag.string({ brief: "Exact database id required to - remove", placeholder: "database-id" })`~~ — struck, no flag is - declared — project, branch. -- Handler: context → resolve → confirmation (§2.3, default copy: - summary `Confirm database removal`, why `Removing this database is - destructive and requires the exact id.`, rerun nextAction - `${CLI_NAME} postgres remove ${id} --confirm ${id}`; consent - question pending ratification: `Remove database ${id}? This - permanently deletes the database and its data.`) → - `provider.removeDatabase(database.id, { signal })`. -- data: `{ projectId, projectName, database }` (pre-removal - summary). -- human: summary ok `Removing database.`; fields `project`, - `database`, `id`; list block `["Database and its connection - metadata were removed."]`. -- stdout: none. json: strip shape. next: none. -- Tests: success; consent matrix (as 3.5); not-found/ambiguous; - json; unauth. - -### 3.7 `postgres backup list ` — `v8/postgres/backup-list.ts` - -- help.summary: `List backups for a database`; examples: - `postgres backup list db_123`, - `postgres backup list acme-production --limit 50`. -- args: positional `database`; flags `limit: flag.string({ brief: - "Maximum number of backups to return", placeholder: "n" })`, - project, branch. (`limit` stays a string flag parsed by the - handler — the legacy integer/range rule is the contract, not the - engine's number parsing: trim → Number → integer 1..100 else - `POSTGRES.USAGE_ERROR` `Invalid backup limit` / `--limit must be - an integer between 1 and 100.` / nextActions from fix + example - `${CLI_NAME} postgres backup list --limit 50`.) -- Handler: parse limit FIRST → context → resolve → - `provider.listBackups(database.id, { limit, signal })`. -- data: `{ projectId, projectName, database, backups, retentionDays, - hasMore }`. -- human: summary info `Listing platform-created database backups.`; - fields `database:` + `retention: ${retentionDays} days` when - non-null; empty → list `["No backups found."]`; else table - `Id | Type | Status | Size | Created` (Size via formatBackupSize, - Created `unknown` when ""), API order preserved; when hasMore: - list `["More backups exist; raise --limit to see them."]`. -- stdout: table data rows tab-joined. json: legacy - `serializeDatabaseBackupList` shape: `{ context: { project, - database }, items: [{ name: id, id, status: null }], count, - projectId, database, backups, retentionDays, hasMore }`. -- next: none. -- Tests: success; empty; hasMore line; limit validation (0, 101, - non-integer → exit 2); 422 → `POSTGRES.BACKUPS_UNSUPPORTED` copy - verbatim; json; unauth. - -### 3.8 `postgres connection list ` — `v8/postgres/connection-list.ts` - -- help.summary: `List database connection metadata without secret - values`; examples: `postgres connection list db_123`, - `postgres connection list acme-preview --branch preview --json`. -- args: positional `database`; flags project, branch. -- Handler: context → resolve → `provider.listConnections`. -- data: `{ projectId, projectName, database, connections }`. -- human: summary info `Listing database connection metadata.`; - fields `database:`; empty → list `["No database connections - found."]`; else table `Name | Id | Created` (Created `unknown` - when null), API order. -- stdout: table data rows tab-joined. json: legacy serializer: - `{ context: { project, database }, items: [{ name, id, status: - null }], count, projectId, database, connections }`. -- next: none. Tests: success; empty; not-found; json; unauth. - -### 3.9 `postgres connection create ` — `v8/postgres/connection-create.ts` (secret) - -- help.summary: `Create a database connection and print its one-time - connection URL`; examples: `postgres connection create db_123`, - `postgres connection create db_123 --name readonly`. -- args: positional `database`; flags `name: flag.string({ brief: - "Connection name", placeholder: "name" })`, project, branch. -- Handler: context → resolve → `provider.createConnection({ - databaseId, name: flags.name?.trim() || defaultConnectionName(), - signal })` — `defaultConnectionName` reproduced verbatim - (`cli-<17-digit compact ISO>-<4 hex>`; whitespace `--name` falls - back). -- data: `{ projectId, projectName, database, connection, - connectionString }`. -- human: summary ok `` Added a connection to "${database.name}" in - ${postgresTargetLabel(...)}. `` + list `["The connection URL below - is shown once, so save it now."]` + sensitive connection-URL field - row. -- stdout: `[connectionString]`. json: strip shape (carries - connectionString). next: none. -- Tests: success (stdout URL, masked human, default name pattern - `^cli-\d{17}-[0-9a-f]{4}$` when --name omitted); named create; - string-missing → `POSTGRES.CONNECTION_STRING_MISSING`; json; - unauth. - -### 3.10 `postgres connection rotate ` — `v8/postgres/connection-rotate.ts` (consent + secret) - -- help.summary: `Rotate connection credentials and print the new - one-time connection URL`; example: - `postgres connection rotate conn_123 --confirm conn_123`. -- args: positional `connection: positional.string({ brief: - "Connection id", placeholder: "connection-id" })`; flags - ~~`confirm: flag.string({ brief: "Exact connection id required to - rotate", placeholder: "connection-id" })`~~ — struck, no flag is - declared. NO project/branch. -- Handler order (legacy database.ts:551): (1) blank id → - `POSTGRES.USAGE_ERROR` `Connection id required` / `Database - connection rotation needs a connection id.` / nextActions incl. - example `${CLI_NAME} postgres connection rotate - --confirm `; (2) confirmation BEFORE any API call — - copy: summary `Confirm database connection rotation`, why - `Rotating revokes the previous credentials and breaks clients - still using them, so it requires the exact connection id.`, rerun - nextAction `${CLI_NAME} postgres connection rotate ${id} --confirm - ${id}`; consent question pending ratification: `Rotate connection - ${id}? The previous credentials stop working immediately.`; (3) - `resolvePostgresProviderOnly` → `provider.rotateConnection(id, - { signal })`. -- data: `{ connection, database: {id,name}|null, connectionString }` - (no project fields — legacy shape). -- human: summary ok `` Rotated credentials for ${database ? - `"${database.name}"` : `connection ${connection.id}`}. The - previous credentials no longer work. `` + one-time list line + - sensitive URL row. -- stdout: `[connectionString]`. json: result unchanged (legacy - identity serializer). next: none. -- Tests: success; blank id; consent matrix; rotate-response - string-missing (`POSTGRES.CONNECTION_STRING_MISSING`, the rotate - variant copy: `Rotated connection strings are one-time-view - secrets…`); real-mode 404 → API passthrough code, exit 2; json; - unauth. - -### 3.11 `postgres connection remove ` — `v8/postgres/connection-remove.ts` (consent) - -- help.summary: `Remove a database connection after exact id - confirmation`; example: - `postgres connection remove conn_123 --confirm conn_123`. -- args: positional `connection` (as 3.10); ~~flag `confirm` (brief - `Exact connection id required to remove`)~~ — struck, no flag is - declared. NO project/branch. -- Handler: blank id → `POSTGRES.USAGE_ERROR` `Connection id - required` / `Database connection removal needs a connection id.` - (example nextAction `${CLI_NAME} postgres connection remove - --confirm `); confirmation (§2.3, - default copy: summary `Confirm database connection removal`, why - `Removing this database connection is destructive and requires the - exact id.`; consent question pending ratification: `Remove - connection ${id}? Clients using it lose access.`); provider-only → - `provider.removeConnection(id, { signal })`. -- data: `{ connection: { id } }`. -- human: summary ok `Removing database connection.`; fields - `connection` = id; list `["The connection metadata was removed. - Existing one-time secrets were not shown."]`. -- stdout: none. json: `{ connection }`. next: none. -- Tests: success; blank id; consent matrix; json; unauth. - -## 4. Divergence entries this dispatch adds - -1. Rename class: every `database` path/id/help/example → - `postgres` (R-S2b-1); no alias. -2. Exit-code class: all errored paths exit 2 (legacy 1) — commands - enumerated per conformance row; `CONFIRMATION_REQUIRED` stays 2; - consent cancel = 3 (new). -3. Error-code map of §2.5 (flat → dotted), row per code. -4. Auto-login drop (R-S2b-2 / Q1) — all 11 commands. -5. Consent prompts added for restore/remove/rotate/connection-remove - (legacy: flag-only, no prompt) with pinned question texts — - operator ratification per conventions §5. -6. Plan-limit humanLines rendering → structured why/nextAction/meta - (§2.4). -7. Package-runner formatter dropped from command strings (§0). -8. `--trace` fix-text substitution (§0). -9. verboseContext / `--verbose` context block dropped (log-level - ruling); resolution provenance no longer rendered. -10. List commands write data rows to stdout in human mode (S2a - precedent; legacy stdout was empty). -11. Sync-command progress lines (`Creating database...` etc.) - dropped. -12. Fixture-only `DATABASE_CONNECTION_NOT_FOUND` dies with fixture - machinery (real mode passes API codes through) — no v8 - counterpart. - -## 5. Legacy test deletion (this dispatch) - -Delete from `packages/cli/tests/database.test.ts` every fixture-mode -case exercising the 11 ported commands (the file's entire -command-level surface); keep any case that exercises unported shell -behavior. `database-plan-limit.test.ts`: port assertions are -superseded by 3.1's plan-limit test — delete the file if all its -cases target ported commands, else keep the remainder. Provider unit -tests (`app-provider`-style, database provider internals) are NOT -deleted — the provider survives as the operation layer. - -## 6. Conformance rows - -The implementer appends one row per command to -`assets/s2/parity-divergences-s2b.md` (conventions §11 format), -citing this doc's section as "rules applied". diff --git a/.drive/projects/prisma-cli-v8/specs/s2b-design/d3-bucket-branch-git.md b/.drive/projects/prisma-cli-v8/specs/s2b-design/d3-bucket-branch-git.md deleted file mode 100644 index ea029bb1..00000000 --- a/.drive/projects/prisma-cli-v8/specs/s2b-design/d3-bucket-branch-git.md +++ /dev/null @@ -1,505 +0,0 @@ -# D3 design — bucket (6) + branch list + git (2) - -> **CONSENT SUPERSESSION (orchestrator amendment, 2026-08-10).** This -> document was drafted before consent became engine-owned. Wherever a -> section below still shows a per-command `confirm` flag or a -> `*.CONFIRMATION_REQUIRED` mapper entry, conventions §5 wins — the -> child doc may add detail, never contradict a rule. Concretely: §3.3 -> declares NO `confirm` flag (the engine injects the shared repeatable -> `--confirm `), and §2.1 drops `CONFIRMATION_REQUIRED` from the -> bucket map because the engine's `CLI.CONSENT_REQUIRED` replaces it. -> The affected lines are struck below at their own sites. D1 shipped -> with a dead `PROJECT.CONFIRMATION_REQUIRED` row still in its mapper, -> already recorded as unreachable in the divergence list; that residue -> is not repeated here and D1 is not reopened for it. - -> **FOUR DETAILS PINNED (orchestrator, 2026-08-10).** The implementer -> stopped on these rather than improvising, which was right. All four -> are mechanical consistency choices with no legacy copy to violate, so -> they are settled here and ratified through the divergence list at PR -> review rather than blocking the dispatch. -> -> 1. **Positional placeholders** are the kebab-case form of the argument -> name: `bucketId` → `bucket-id`, `keyId` → `key-id`. This follows -> `gitUrl` → `git-url`, the one placeholder §3.8 already pins. -> 2. **`bucket key create`'s field-row labels** are the environment -> variable names — `S3_ENDPOINT`, `S3_ACCESS_KEY_ID`, -> `S3_SECRET_ACCESS_KEY`, `S3_BUCKET`. Legacy human output had no -> field rows at all (fact sheet §5: only the two literal lines and -> the summary), so the rows are a v8 addition and no legacy label -> exists to port. The env-var names are the right labels because the -> list line immediately above them says to set these environment -> variables. -> 3. **The git group's `--project` placeholder** is `id-or-name`, as the -> shipped project and postgres groups already use. Its brief, -> "Project id or name", was already pinned. -> 4. **D3 divergence entries are numbered from 31**, continuing D2's -> sequence in the one shared file. - -Binding design for dispatch D3. Parent: `conventions.md`; template -from D1. Grounding fact sheet: `facts/facts-d3-bucket-branch-git.md` -(verbatim legacy extraction — part of this doc). Binding corrections -to the inventory: bucket key create's stdout lines are -`S3_ENDPOINT=` / `S3_ACCESS_KEY_ID=` / `S3_SECRET_ACCESS_KEY=` / -`S3_BUCKET=` in that order; `branch list` resolution passes no -commandName (its setup-required copy says "this command"). - -## 1. Group mounting - -| group | brief | -| --- | --- | -| `bucket` | Manage object-store buckets for a project | -| `bucket key` | Manage access keys for an object-store bucket | -| `branch` | View your Platform branches | -| `git` | Manage Git repository connections for a project | - -Mount paths `bucket list|create|delete`, -`bucket key list|create|delete`, `branch list`, -`git connect|disconnect`. Family keys `bucketList`, `bucketCreate`, -`bucketDelete`, `bucketKeyList`, `bucketKeyCreate`, -`bucketKeyDelete`, `branchList`, `gitConnect`, `gitDisconnect`. - -## 2. Shared machinery - -### 2.1 Error mappers - -`v8/bucket/errors.ts` (BUCKET.*), `v8/branch/errors.ts` (BRANCH.*), -`v8/git/errors.ts` (GIT.*) per conventions §4. Complete maps: - -Bucket: `USAGE_ERROR` domain bucket (2) → `BUCKET.USAGE_ERROR`; -~~`CONFIRMATION_REQUIRED` (2) → `BUCKET.CONFIRMATION_REQUIRED`~~ -(struck by the consent supersession above — the engine's -`CLI.CONSENT_REQUIRED` replaces it, so the bucket map has no -`CONFIRMATION_REQUIRED` row); -`BUCKET_KEY_SECRET_MISSING` (1) → `BUCKET.KEY_SECRET_MISSING`; -`BUCKET_API_ERROR` / passthrough X (1) → `BUCKET.API_ERROR` / -`BUCKET.X`. Fixture-only `BUCKET_NOT_FOUND`, `BUCKET_KEY_NOT_FOUND`, -`BRANCH_NOT_FOUND` (bucket domain) die with fixture machinery — no -v8 entries (real mode passes API codes through; divergence entry). -Project-resolution errors (bucket list/create) → `PROJECT.*` via -D1's shared mapper. Workspace-required → `AUTH.USAGE_ERROR`. - -Branch: `BRANCH_API_ERROR` / passthrough X (1) → `BRANCH.API_ERROR` -/ `BRANCH.X`; resolution family → `PROJECT.*`; workspace → -`AUTH.USAGE_ERROR`. - -Git: `USAGE_ERROR` domain project raised by git commands (2) → -`GIT.USAGE_ERROR`; `REPO_PROVIDER_UNSUPPORTED` (2) → -`GIT.REPO_PROVIDER_UNSUPPORTED`; `REPO_ALREADY_CONNECTED` (1) → -`GIT.REPO_ALREADY_CONNECTED`; `REPO_INSTALLATION_REQUIRED` (1) → -`GIT.REPO_INSTALLATION_REQUIRED`; `REPO_NOT_ACCESSIBLE` (1) → -`GIT.REPO_NOT_ACCESSIBLE`; `REPO_NOT_CONNECTED` (1) → -`GIT.REPO_NOT_CONNECTED`; `REPO_CONNECTION_FAILED` (1) → -`GIT.REPO_CONNECTION_FAILED` (status-aware fix text verbatim, incl. -the 404/409/422 variants; meta `{status, apiCode?}`); legacy -401/403 → `AUTH_REQUIRED`: the engine settles every real credentials -failure itself, so a genuine sign-in problem never reaches this mapper -— it becomes `CLI.CREDENTIALS_REQUIRED` before the handler runs. What -does still arrive is the permission residue of a returned 403, and it -maps mechanically to `GIT.AUTH_REQUIRED` like any unmapped code -(corrected 2026-08-11: this line previously read "does not port", -which contradicted divergence 40 and the shipped -`v8-git.test.ts` case; divergence entry, same class as D1). Resolution family → `PROJECT.*` (with `commandName: -"git connect"` / `"git disconnect"` preserved). - -All copy verbatim + conventions §0-substitutions (command strings → -`${CLI_NAME} …`, `--trace` fix text, package-runner formatter -dropped). `PROJECT_AMBIGUOUS`'s hardcoded `app deploy` nextStep -ports verbatim (pre-existing quirk; divergence note, not a fix — -consistent with D1). - -### 2.2 Operation calls - -- Bucket: `createManagementBucketProvider(ctx.api)` → - `listBuckets` / `createBucket` / `deleteBucket` / `listKeys` / - `createKey` / `deleteKey` (signatures per fact sheet S5). - `requireBucketContext` equivalent: workspace (conventions §3a) + - `resolveProjectTarget` with `commandName` `"bucket list"` / - `"bucket create"` — only for list/create. delete + all key - commands: provider only, NO workspace/project resolution (legacy - `requireBucketProviderOnly`). -- Branch: `listBranches(ctx.api, projectId, ctx.signal)` - (controllers/branch.ts — export per D1 §0.4 rule); resolution via - `resolveProjectTarget` with NO explicitProject and NO commandName - (quirk ports verbatim). -- Git: the flow functions in controllers/project.ts re-homed per - §3.8 below: `readGitOriginRemote`, `parseGitHubRepositoryUrl`, - `readFirstSourceRepository`, `listScmInstallations`, - `findRepositoryInInstallations`, `createGitHubInstallIntent`, - `toRepositoryConnection`, inline `ctx.api.POST/DELETE` on - source-repositories — imported/exported, not reimplemented. - -## 3. Per-command design - -Common: `needs: { credentials: true }`; data = legacy result minus -`verboseContext`; no exitCodes. - -### 3.1 `bucket list` — `v8/bucket/list.ts` - -- help.summary `List object-store buckets for the resolved project`; - examples `bucket list`, `bucket list --branch preview`, - `bucket list --json`; flags `project`/`branch` (briefs `Project id - or name` / `Branch git name`). -- Handler: workspace → resolve project (`"bucket list"`) → - `provider.listBuckets({ projectId, branchName, signal })`. -- data `{ projectId, projectName, branchName: branchName ?? null, - buckets }`. -- human: summary info `Listing object-store buckets for the resolved - project.`; fields `project:` (+ `branch:` when set); empty → list - `["No buckets found."]`; else table - `Name | Id | Status | Branch | Created` (Branch cell - `branchId ?? "unscoped"`). -- stdout: table data rows tab-joined. json: legacy - `serializeBucketList` shape `{ context, items, count, projectId, - branchName, buckets }`. next: none. -- Tests: success; empty; branch filter passthrough; resolution error - (`PROJECT.SETUP_REQUIRED`); json; unauth. - -### 3.2 `bucket create` — `v8/bucket/create.ts` - -- help.summary `Create an object-store bucket`; examples - `bucket create`, `bucket create --name my-store`, - `bucket create --branch preview --json`; flags `name: - flag.string({ brief: "Bucket display name (auto-generated if - omitted)", placeholder: "name" })`, `project`, `branch`. -- Handler: workspace → resolve (`"bucket create"`) → - `provider.createBucket({ projectId, name: flags.name?.trim() || - undefined, branchGitName: branchName, signal })`. -- data `{ projectId, projectName, bucket }`. -- human: summary ok `` Created bucket "${bucket.name}" in - ${projectName}[ / ${branchId}] `` (legacy `formatBucketTarget`; - the `Creating bucket...` progress line drops — d2 class). -- stdout: none. json: strip shape. next: none. -- Tests: success (named + auto-name/undefined passthrough); API - error passthrough → `BUCKET.` exit 2; json; unauth. - -### 3.3 `bucket delete ` — `v8/bucket/delete.ts` (consent) - -- help.summary `Delete a bucket and all its access keys`; example - `bucket delete bkt_123 --confirm bkt_123`; positional `bucketId` - (brief `Bucket id`). NO `confirm` flag is declared — the engine - injects the shared repeatable `--confirm `, so the command - declares only the positional. (The struck draft text read: flag - `confirm: flag.string({ brief: "Exact bucket id to confirm - deletion", placeholder: "bucket-id" })`. Follow `postgres remove` - in `v8/postgres/remove.ts` for the shipped shape.) -- Handler: blank id → `BUCKET.USAGE_ERROR` (`Bucket id required` / - `Bucket deletion needs a bucket id.` / nextActions from fix + - `${CLI_NAME} bucket list`); consent per conventions §5 (hold - lifted): `ctx.prompt.consent("Deleting this bucket permanently - removes all objects and access keys.", { token: id })`; then - provider-only → `provider.deleteBucket(id, { signal })`. -- data `{ bucket: { id } }`. -- human: summary ok `Deleting object-store bucket.`; fields - `bucket: id`; list `["Bucket and all its access keys were - removed."]`. -- stdout: none. json: `{ bucket: { id } }`. next: none. -- Tests: success; blank id; consent matrix (grant/deny/ - non-interactive/cancel/mismatch); API error; json; unauth. - -### 3.4 `bucket key list ` — `v8/bucket/key-list.ts` - -- help.summary `List access keys for a bucket`; examples per fact - sheet; positional `bucketId`. -- Handler: blank → `BUCKET.USAGE_ERROR` (`Bucket key listing needs a - bucket id.`); provider-only → `provider.listKeys`. -- data `{ bucketId, keys }`. human: summary info `Listing access - keys for bucket.`; fields `bucket: id`; empty → `["No keys - found."]`; table `Name | Id | Role | Hint | Created`. -- stdout: table rows tab-joined. json: legacy shape `{ context: - { bucket }, items, count, bucketId, keys }`. next: none. -- Tests: success; empty; blank id; json; unauth. - -### 3.5 `bucket key create ` — `v8/bucket/key-create.ts` (secret) - -- help.summary `Create a bucket access key and print its one-time - credentials`; examples per fact sheet; positional `bucketId`; - flags `role: flag.enum({ brief: "Access role (default: - read_write)", values: ["read", "read_write"] })`, `name: - flag.string({ brief: "Key display name (auto-generated if - omitted)", placeholder: "name" })`. -- Handler: blank id → usage error (`Bucket key creation needs a - bucket id.`); role via legacy `resolveKeyRole` semantics - (`role === "read" ? "read" : "read_write"`); provider-only → - `provider.createKey({ bucketId, name: trim-or-undefined, role, - signal })`. -- data `{ bucketId, key, secretAccessKey, accessKeyId, endpoint, - bucketName }`. -- human: summary ok `` Created key "${key.name}" for bucket - "${bucketName}". `` + list `["The credentials below are shown once - — copy them now.", "Set these environment variables to use this - bucket:"]` + fields block with the four rows, each - `sensitive: true` where secret (`S3_SECRET_ACCESS_KEY`, - `S3_ACCESS_KEY_ID`) and plain for endpoint/bucket. -- stdout EXACTLY (order pinned): - `S3_ENDPOINT=${endpoint}`, `S3_ACCESS_KEY_ID=${accessKeyId}`, - `S3_SECRET_ACCESS_KEY=${secretAccessKey}`, - `S3_BUCKET=${bucketName}`. -- json: result unchanged (secret included). next: none. -- Tests: success (stdout bytes, masked rows, envelope secret); - role default (`--role` omitted → read_write on the wire); blank - id; credentials-missing → `BUCKET.KEY_SECRET_MISSING` copy - verbatim; json; unauth. - -### 3.6 `bucket key delete ` — `v8/bucket/key-delete.ts` - -- help.summary `Revoke and delete a bucket access key`; example per - fact sheet; positionals `bucketId`, `keyId` (briefs `Bucket id` / - `Key id`). NO confirm flag, no consent (legacy behavior; - divergence review note only). -- Handler: either blank → `BUCKET.USAGE_ERROR` (`Bucket id and key - id required` copy verbatim, nextAction `${CLI_NAME} bucket key - list `); provider-only → `provider.deleteKey`. -- data `{ key: { id } }`. human: summary ok `Deleting bucket access - key.`; fields `key: id`; list `["The access key was revoked and - removed."]`. stdout none; json `{ key }`; next none. -- Tests: success; blank ids (each); API error; json; unauth. - -### 3.7 `branch list` — `v8/branch/list.ts` - -- help.summary `List Platform branches for the resolved project`; - examples `branch list`, `branch list --json`. NO args (no - `--project`; pin/durable resolution only — legacy). -- Handler: workspace → `resolveProjectTarget` (no explicitProject, - no commandName — quirk verbatim) → `listBranches(ctx.api, - projectId, ctx.signal)` (cursor pagination to exhaustion, - production-first then name sort, `envMap = role` copy). -- data `{ projectId, projectName, branches }`. -- human: summary info `Listing branches for the resolved project.`; - fields `project:`; empty → `["No branches found."]`; table - `Name | Role | Env map`. -- stdout: table rows tab-joined. json: `{ projectId, projectName, - branches }`. next: none. -- Tests: success (sort proven: production first, then alphabetical); - pagination (two pages via fake client); empty; API-code - passthrough → `BRANCH.`; unbound → `PROJECT.SETUP_REQUIRED` - with the "this command" why-variant; json; unauth. - -### 3.8 `git connect [git-url]` — `v8/git/connect.ts` (poll, R-S2b-7) - -- help.summary `Connect the resolved project to a GitHub - repository`; examples per fact sheet; positional `gitUrl: - positional.optionalString({ brief: "GitHub repository URL", - placeholder: "git-url" })`; flag `project`. -- Handler flow (legacy runGitConnect, re-homed): - 1. workspace → resolve project (`"git connect"`). - 2. URL: positional ?? `readGitOriginRemote(ctx.cwd, ctx.signal)`; - none → `GIT.USAGE_ERROR` (copy verbatim); - `parseGitHubRepositoryUrl` null → - `GIT.REPO_PROVIDER_UNSUPPORTED`. - 3. `readFirstSourceRepository(ctx.api, projectId, signal)`: - same-repo (case-insensitive) → idempotent success with the - existing connection; different repo → - `GIT.REPO_ALREADY_CONNECTED`. - 4. Install resolution: `listScmInstallations` + - `findRepositoryInInstallations`; on miss, - `createGitHubInstallIntent` → installUrl. - 5. **INTERACTION NEED REMOVED (operator ruling, 2026-08-11).** - `git connect` declares `needs: { credentials: true }` and nothing - more. The earlier ruling below — declare `needs.interaction` — - was made when the engine had no interaction error of its own and - the only alternative was a command reading TTY state, which is - banned. `prompt.browserWait` now refuses a non-interactive session - itself, naming the install URL, so the declaration bought nothing - and cost every scripted run that never reaches the wait: the - repository already connected, or the app already installed. Those - work again. A non-interactive run that does need the wait settles - the engine's `CLI.INTERACTION_REQUIRED`, which names the URL and - what to do. Everything else in step 5 stands. - - 5. **STEP 5 RESOLVED (orchestrator, 2026-08-10, after the operator - landed engine commit c463aa1).** The draft below was written - before `browserWait` existed and pinned three facts it could not - supply. All four points are now settled; where the draft text - conflicts, these win. - - **Interval: restored.** `BrowserWaitRequest` now takes an - optional `interval`, so the handler passes - `PRISMA_CLI_GITHUB_INSTALL_POLL_INTERVAL_MS` (default 2000) - as `interval` and `PRISMA_CLI_GITHUB_INSTALL_TIMEOUT_MS` - (default 120000) as `timeout`, both read from `ctx.env` with - the legacy positive-integer parsing (`readPositiveIntegerEnv` - semantics: a non-positive or unparseable value falls back to - the default). The design's test case asserting a 1ms interval - is writable as pinned — `createTestCli` takes a `delay` spy, - so assert the value the poll loop asked for, not elapsed time. - - **Events: one, not three.** The draft's `endpoint` → `status - waiting` → `status connected` sequence was a design error, not - an engine gap: legacy prints one wait line before the poll loop - and nothing during it (fact sheet §6, "no status re-print - during polling"), and has no "connected" line. The helper's - single `endpoint` event is exactly the legacy shape. Assert one - event. The three-event sequence is struck. - - **`opened`: dropped.** `browserWait` does not report whether - the browser opened, and it is not worth recovering: both legacy - branches existed to make sure the user had the install URL when - no browser opened, and the engine now always writes the URL - (`rendering.ts:50` in human mode, a frame in json). Use the - browser-opened wait sentence, "Waiting for GitHub App - installation or repository access approval...", as `message`, - and the browser-opened fix text on - `GIT.REPO_INSTALLATION_REQUIRED`, "Finish installing the GitHub - App in the browser, then rerun prisma-cli git connect." Drop - `opened` from both terminal errors' meta, leaving - `{ repository, installUrl }`. Divergence entry. - - **The install URL is an `open-url` action, not a - `run-command`.** `NextAction` now has an `open-url` kind and a - `url` field. In the git mapper, a `nextSteps` entry that is a - URL becomes `{ kind: "open-url", label: , url: }`; command strings keep the `run-command` mapping. This - affects `GIT.REPO_INSTALLATION_REQUIRED` and - `GIT.REPO_NOT_ACCESSIBLE`, whose first next step is the raw - installUrl. Divergence entry; it supersedes entry 42, which - recorded the defect. **A `nextSteps` entry counts as a URL when - it starts with `https://` or `http://`** (pinned 2026-08-10): - the test is total, and no legacy command string in this slice - begins with a scheme, so nothing else can match it. - - Draft text follows, superseded at the four points above. - - Wait: OPERATOR RULING (2026-08-10, corrected) — `git connect` - declares `needs: { interaction: true }` (the EXISTING S2a - mechanism, execution/needs.ts): non-interactive runs fail - early, before the handler and any side effects, with the - engine's interaction-required error (exit 2). Commands and - helpers never read TTY/CI state. The interactive wait flow - ports against `ctx.prompt.browserWait` (landed, engine commit - 6bb8452: `{ url, message, poll(signal), timeout }` — announce + - open through the runtime opener + poll on the engine clock; - timeout → structured timeout error; Ctrl-C → exit 3; - non-interactive → interaction-required with the URL, unreachable - here behind needs.interaction) — do not hand-roll polling. - Binding mapping onto the helper: url = - installUrl; poll predicate = "the GitHub App installation - exists" (re-list installations, `findRepositoryInInstallations` - match); interval/timeout from - `PRISMA_CLI_GITHUB_INSTALL_POLL_INTERVAL_MS` (default 2000) / - `PRISMA_CLI_GITHUB_INSTALL_TIMEOUT_MS` (default 120000) read - from ctx.env; announcement copy = the legacy wait line - verbatim. Outcomes: predicate satisfied → proceed to step 6; - timeout → the legacy terminal errors - (`GIT.REPO_NOT_ACCESSIBLE` when inspectableInstallationCount > - 0, else `GIT.REPO_INSTALLATION_REQUIRED`; meta `{repository, - installUrl, opened}`). Non-interactive runs never reach the - handler at all (needs.interaction) — divergence entry: legacy - non-interactive `git connect` succeeded when the repo was - already reachable and errored with installUrl meta otherwise; - v8 fails every non-interactive run early with the engine's - interaction-required error (exit 2). Announce/open/poll events - and rendering are the HELPER'S — the handler emits no - endpoint/status events of its own. Exact call surface: bind to - the landed helper's API at merge-down; any mismatch with this - mapping is a STOP, not an adaptation. - 6. `ctx.api.POST("/v1/source-repositories", { body: { projectId, - provider: "github", providerRepositoryId, installationId }, - signal })`; error → `GIT.REPO_CONNECTION_FAILED` family. -- data: `{ workspace, project, resolution, repositoryConnection }` - (legacy raw shape — it had no serializer; ports as-is). -- human: summary ok `Connecting Git to the resolved project.`; - fields `project`, `workspace`, `repository` (fullName), `status`; - list `[]`. -- stdout: none. json: result unchanged. next: none. -- Tests: explicit-url success; origin-remote fallback (fake - `readGitOriginRemote` via cwd-scoped temp git config is NOT used — - mock the exported function via vi.mock on its module); - no-url usage error; non-GitHub URL; already-connected idempotent - success; different-repo conflict; installation-required (meta - asserted); not-accessible; poll-then-found - (the single `endpoint` event asserted — the struck three-event - sequence is superseded by STEP 5 RESOLVED above, which this bullet - now follows; fake client scripted across two list calls; interval - env set to 1ms); poll timeout; connection-failed 409 fix text; - json; unauth. -- Test-list amendment (orchestrator, 2026-08-10): because `git - connect` declares `needs: { interaction: true }`, every case that - reaches the handler must run interactively. The draft's - "installation-required (non-interactive, meta asserted)" case could - never reach the handler, so it is corrected above to an interactive - case. Add exactly one case in its place: a non-interactive run - settles the engine's interaction-required error at exit 2 before - any API call, proven by the fake client recording zero calls. That - case is the test of divergence §4.3. - -### 3.9 `git disconnect` — `v8/git/disconnect.ts` - -- help.summary `Disconnect the GitHub repository from the resolved - project`; examples per fact sheet; flag `project`. -- Handler: workspace → resolve (`"git disconnect"`) → - `readFirstSourceRepository`; none → `GIT.REPO_NOT_CONNECTED` - (copy verbatim); else `ctx.api.DELETE - ("/v1/source-repositories/{id}")`; error → - `GIT.REPO_CONNECTION_FAILED` family. No confirmation (legacy). -- data: `{ workspace, project, resolution, repositoryConnection: - }`. -- human: summary ok `Disconnecting Git from the resolved project.`; - fields `project`, `workspace`, `repository`; list `["GitHub branch - automation is no longer active for this project."]`. -- stdout none; json raw; next none. -- Tests: success; not-connected; API error; json; unauth. -- Test-list amendment (orchestrator, 2026-08-10, from review finding - D3-R1-01): the single "API error" case becomes two, because - `repoConnectionFixForStatus` has two arms and one case can only reach - one of them. Keep the 422 case asserting its status-specific fix text, - and add a case with a status that has no variant (500), which reaches - the default arm — the only place in the git group where the `--trace` - to `--log-level verbose` substitution of conventions §0 fires. Without - it that substitution is untested here. - -### 3.8a Dependencies — RESOLVED (merge-down 2026-08-10) - -browserWait, ctx.openUrl, and consent tokens are all landed (engine -commit 6bb8452). Nothing in D3 waits; no reordering. The timeout -error browserWait raises on poll expiry replaces the handler-side -timeout branch: catch it and settle the legacy terminal errors -(`GIT.REPO_NOT_ACCESSIBLE` / `GIT.REPO_INSTALLATION_REQUIRED`) per -§3.8 step 5. - -## 4. Divergence entries this dispatch adds - -D2 classes 2/3/4/7/8/9/10/11 apply, plus: -1. Bucket delete consent prompt added (flag-only legacy) — pinned - question, OPERATOR DECISION 2. -2. Fixture-only BUCKET_NOT_FOUND / BUCKET_KEY_NOT_FOUND / - BRANCH_NOT_FOUND die; real-mode API codes pass through as - `BUCKET.`. -3. git connect: declares needs.interaction — ALL non-interactive - runs fail early with the engine's interaction-required error - (exit 2), including the legacy non-interactive success case - (repo already reachable) and the legacy immediate REPO_* errors - with installUrl meta; the interactive wait flow moves onto the - engine's browser-wait helper (its announce/open/poll surface - replaces the legacy stderr wait line). -4. git connect/disconnect keep their serializer-less raw json result - (resolution object included) — unchanged, recorded for review - since other groups strip it. -5. 401/403 → `CLI.CREDENTIALS_REQUIRED` class (as D1). -6. `bucket key create --role`: engine enum error replaces commander - choices error. - -## 5. Legacy test deletion (this dispatch) - -Delete fixture-mode cases covering these 9 commands from -`bucket.test.ts` and `branch.test.ts` (whole files if nothing else -remains). git connect/disconnect fixture cases live in -`project.test.ts` / `project-real-mode.test.ts` — delete only the -git-command cases; `git-adapter.test.ts` (URL parsing units) stays. - -Amendment (orchestrator, 2026-08-10, once step 5 landed): D3 kept four -`project-real-mode.test.ts` cases because `git connect`'s wait was -unported. Three now have v8 equivalents and are deleted — "creates an -install intent when the workspace has no GitHub App installation", -"waits for GitHub App installation in interactive mode and connects -after approval", and "returns REPO_NOT_ACCESSIBLE when the GitHub App -cannot see the repository". The fourth stays: "creates an install -intent when the stored GitHub App installation is unavailable" drives -a stored installation answering 422 and being skipped inside -`findRepositoryInInstallations`, a helper v8 calls and does not -otherwise exercise. Same rule as the D2 provider unit tests — a -command-level case for a ported command goes, a unit test for a -surviving helper stays. The two pagination cursor-stall guards stay -for the same reason. -`branch-controller.test.ts` / `branch-usecases.test.ts` / -`read-branch.test.ts` / `local-branch.test.ts` stay until S2d. - -## 6. Conformance rows - -One row per command in `assets/s2/parity-divergences-s2b.md`. diff --git a/.drive/projects/prisma-cli-v8/specs/s2b-design/facts/facts-d1-project.md b/.drive/projects/prisma-cli-v8/specs/s2b-design/facts/facts-d1-project.md deleted file mode 100644 index 1d7b2f50..00000000 --- a/.drive/projects/prisma-cli-v8/specs/s2b-design/facts/facts-d1-project.md +++ /dev/null @@ -1,611 +0,0 @@ -# D1 verbatim facts — `project` + `project env` commands (legacy commander CLI) - -Extracted from code on branch `claude/prisma-cli-v8-onboarding-30e694`. -All paths below are relative to `packages/cli/src/` unless absolute, and to -the repository root when they start with `.drive/` or `packages/`. Line -numbers are from that branch. -Cross-checked against `.drive/projects/prisma-cli-v8/assets/s2/command-inventory.md`; discrepancies flagged inline and at the end. - ---- - -## 0. Group registrations - -### `project` group — `commands/project/index.ts:43-61` -- `createProjectCommand(runtime)`: `new Command("project")` → `configureRuntimeCommand` → `attachCommandDescriptor(cmd, "project")`. -- Gets **compact** global flags: `addCompactGlobalFlags(project)` (line 49). -- Subcommands added in order: list, show, create, link, rename, remove, transfer, then `createEnvCommand(runtime)` (line 58). -- Registered on the root program in `cli.ts:112` (`program.addCommand(createProjectCommand(runtime))`). Root program name is `"prisma"` (`cli.ts:105`). -- Group descriptor (`shell/command-meta.ts:178-187`): - ``` - id: "project" - path: ["prisma", "project"] - description: "Manage and inspect your Prisma projects" - examples: ["prisma-cli project list", "prisma-cli project link proj_123", "prisma-cli project create my-app"] - ``` -- Bare `prisma project` prints help via `resolveBareHelpCommand` (`cli.ts:174-200`: a single-token argv naming a command that has subcommands prints `helpInformation()` to stderr, exit 0). - -### `project env` group — `commands/env.ts:30-43` -- `createEnvCommand(runtime)`: `new Command("env")`, descriptor id `"project.env"`. -- `env.description("Manage environment variables for the active project")` (line 36). -- **No global flags added to the env group node itself** (neither compact nor full) — unlike the `project` group node. Subcommands added in order: add, update, list, remove. -- Group descriptor (`shell/command-meta.ts:649-660`): - ``` - id: "project.env" - path: ["prisma", "project", "env"] - description: "Manage environment variables for the active project" - examples: [ - "prisma-cli project env list", - "prisma-cli project env add STRIPE_KEY=sk_test_xxx --role production", - "prisma-cli project env add --file .env --role preview", - "prisma-cli project env add DATABASE_URL=postgresql://branch --branch feature/foo", - "prisma-cli project env remove STRIPE_KEY --role preview", - ] - ``` - -All 11 leaf commands get the **full** global flag set via `addGlobalFlags(command)` and dispatch through `runCommand(runtime, "", options, handler, {renderHuman, renderJson})` (`shell/command-runner.ts:68-100`). None of these commands defines `renderStdout` — human rendering goes to stderr; `--json` writes the envelope `{ok:true, nextActions:[], ...success}` pretty-printed (2-space) to stdout (`shell/output.ts:22-29`). - ---- - -## 1. `project list` - -**Registration** (`commands/project/index.ts:242-265`): `new Command("list")`, descriptor `"project.list"`. No positionals, no command-specific flags. Full global flags. - -**Descriptor** (`command-meta.ts:306-311`): -``` -description: "List all projects in your workspace" -examples: ["prisma-cli project list", "prisma-cli project list --json"] -``` - -**Controller** `runProjectList(context)` — `controllers/project.ts:143-210`: -1. `requireAuthenticatedAuthState(context)` (`controllers/auth.ts:205`) — real mode: `readAuthState(env, signal)`; if not authenticated and `canPrompt(context)` is false → throw `authRequiredError()`; else `performLogin(...)` (interactive browser login) then re-read. -2. `if (!workspace) throw workspaceRequiredError()`. -3. Real mode (`isRealMode` = no `context.runtime.fixturePath` and no `env.PRISMA_CLI_MOCK_FIXTURE_PATH`, project.ts:99-104): - - `client = await authenticatedManagementApiClient(context.runtime.env, context.runtime.signal)` (`auth/guard.ts:19-60`); `if (!client) throw authRequiredError()`. - - `projects = sortProjects(await listRealWorkspaceProjects(client, workspace, signal))`. - - `localBinding = await readProjectListLocalBinding(cwd, workspace, projects, signal)` (project.ts:106-125): reads `.prisma/local.json`; `"linked"` iff pin.workspaceId === workspace.id AND pin.projectId is in the project list; `"invalid"` otherwise if pin present or unparseable; `"not-linked"` if missing. - - `nextActions = buildProjectListNextActions(localBinding)`. -4. Fixture mode: `createProjectUseCases(createCliUseCaseGateways(context)).list(authState)` then same localBinding/nextActions. - -**Success envelope**: `command: "project.list"`, `result: { workspace, projects: ProjectSummary[], localBinding }`, `warnings: []`, `nextSteps: []`, `nextActions` as below. - -**nextActions on success** (project.ts:212-224): empty when `localBinding.status === "linked"`; otherwise `buildProjectSetupNextActions({ createCommand: "prisma-cli project create ", reason })` with reason: -- invalid: `"This directory has an invalid local Project binding. Ask the user which Prisma Project to link before running Project-scoped commands."` -- not-linked: `"This directory is not linked to a Prisma Project. Project list shows available Projects, but none is selected for this directory."` - -**Errors**: `AUTH_REQUIRED`, `WORKSPACE_REQUIRED` (see §14.3). No command-specific errors. - -**Human output** `renderProjectList` (`presenters/project.ts:27-90`): -- Title line: `{strong(descriptor label)} → {dim("Listing projects for the authenticated workspace.")}` then blank line. -- `│ workspace: {workspace.name}` (rail `│` dimmed, key accented). -- Empty: `│ No projects found.` (dim). -- Table: header row `│ name id region` (accent, columns padded to max width via `stringWidth`); one row per project: `│ {name} {id} {defaultRegion || dim("none")}`. -- When localBinding is `not-linked`/`invalid`, appends `renderNextSteps([...])` → `"" / "Next steps:" / "- Link an existing Project you choose: prisma-cli project link " / "- Create a new Project: prisma-cli project create "` (`shell/ui.ts:161-171` renders `"Next step:"` singular for one item). - -**JSON serializer** `serializeProjectList` (`presenters/project.ts:92-107`): -```js -{ - context: { workspace: result.workspace.name }, - items: result.projects.map(p => ({ name: p.name, id: p.id, status: null })), // via serializeList: label→name - count: items.length, - localBinding: result.localBinding ?? null // { status: "linked"|"not-linked"|"invalid" } or null -} -``` -(`serializeList` is `output/patterns.ts:93-106`.) - -**Side effects**: none (read-only pin read). - ---- - -## 2. `project show` - -**Registration** (`commands/project/index.ts:267-293`): `new Command("show")`, descriptor `"project.show"`. Flag: `.option("--project ", "Project id or name")`. No positional. - -**Descriptor** (`command-meta.ts:312-320`): -``` -description: "Show this directory's Project binding" -examples: ["prisma-cli project show", "prisma-cli project show --project proj_123 --json"] -``` - -**Controller** `runProjectShow(context, explicitProject)` — project.ts:226-259: -1. `requireAuthenticatedAuthState`; `workspaceRequiredError()` if no workspace. -2. Real mode → `resolveProjectShowInRealMode` (project.ts:1346-1371): `authenticatedManagementApiClient` (null → `authRequiredError()`), then `inspectProjectBinding({context, workspace, explicitProject, listProjects: () => listRealWorkspaceProjects(client, workspace, signal), commandName: "project show"})`; errors mapped via `projectResolutionErrorToCliError`. -3. `inspectProjectBinding` (`lib/project/resolution.ts:185-221`) differs from `resolveProjectTarget` in that `allowEnvProjectId: false` and an unbound directory is a **success** with `project: null` plus `buildProjectSetupSuggestion` fields, not an error. -4. Success: `command: "project.show"`, `result: ProjectShowResult`, `warnings: []`, `nextSteps: []`; `nextActions` when `result.project === null`: `buildProjectSetupNextActions({ commandName: "project show", suggestedProjectName: result.suggestedProjectName, reason: "This directory is not linked to a Prisma Project. Package and directory names can suggest setup defaults, but they do not select a Project." })`. - -**Result shape** (`types/project.ts:49-68`): bound → `{ workspace, project: ProjectSummary, resolution: { projectSource, targetName?, targetNameSource? } }`; unbound → `{ workspace, project: null, localBinding: { status: "not-linked" }, resolution: { projectSource: "unbound" }, suggestedProjectName, suggestedProjectNameSource: "package-name"|"directory-name", candidates: ProjectSummary[], recoveryCommands: string[] }`. - -**Errors**: resolution family (§14.1): `PROJECT_NOT_FOUND` (bad `--project`), `PROJECT_AMBIGUOUS`, `LOCAL_STATE_STALE`, `LOCAL_PROJECT_WORKSPACE_MISMATCH`. **Note: unbound is not an error for show.** *Inventory discrepancy*: inventory §`project show` writes "`PROJECT_AMBIGUOUS` 2" — the code sets `exitCode: 1` (`resolution.ts:268-285`). - -**Human output** `renderProjectShow` (`presenters/project.ts:109-154`): -- Unbound: `renderShow` card, title `"This directory is not linked to a Prisma Project."`, fields `workspace: {name}` and `project: Not linked` (tone warning); then always-appended verbose block (`renderVerboseBlock` only prints with `-v`) titled `"Resolved context"` with rows `workspace`, `workspace id` (dim), `project source: unbound`, `suggested name: {name} ({source})`; then next steps: - ``` - Link an existing Project you choose: prisma-cli project link - Create a new Project: prisma-cli project create {formatCommandArgument(suggestedProjectName)} - ``` -- Bound `renderBoundProjectShow` (presenters/project.ts:345-381): title line `{strong(label)} → {dim("This directory is linked to the following platform project.")}`, blank, `│ local repo {shortenHomePath(cwd)}`, `│ platform {strong(workspace.name + " / " + project.name)}` (keys padded to width of "local repo"); if `project.url`: `│` then `│ → {link(url)}`; if `defaultRegion`: `│ region {dim(region)}`; then `renderResolvedProjectContextBlock` (verbose-only "Resolved context": workspace, workspace id, project, project id, project source — formatted `explicit→"--project"`, `env→"environment"`, `local-pin→".prisma/local.json"`, `platform-mapping→"platform mapping"`, plus target name row `"{targetName} ({targetNameSource})"`; `presenters/verbose-context.ts:34-101`). - -**JSON serializer** `serializeProjectShow(result)` → returns `result` unchanged (presenters/project.ts:156-158). Keys exactly as in the result shape above. - -**Side effects**: none. - ---- - -## 3. `project create ` - -**Registration** (`commands/project/index.ts:183-210`): `new Command("create")`, descriptor `"project.create"`. Positional `.argument("", "Project name")` (required). Flag `new Option("--region ", "Prisma Compute region id")`. - -**Descriptor** (`command-meta.ts:321-329`): -``` -description: "Create a Project and link this directory" -examples: ["prisma-cli project create my-app", "prisma-cli project create my-app --json"] -``` - -**Controller** `runProjectCreate(context, projectName, options?: {region?})` — project.ts:261-337: -1. Auth + workspace guard as above. -2. `if (!isValidProjectSetupName(projectName))` (trimmed non-empty, `lib/project/setup.ts:27-29`) → `projectSetupNameRequiredError("project create")` = `usageError("Project create requires a name", "The project name must be a non-empty value.", "Pass a Project name explicitly.", ["prisma-cli project create my-app"], "project")` (setup.ts:160-168; exit 2, code USAGE_ERROR). -3. Fixture mode → `featureUnavailableError("Project create is not available in fixture mode", "Creating Projects requires live platform integration.", "Rerun without fixture mode enabled to create a Project.", ["prisma-cli auth login"], "project")` (code FEATURE_UNAVAILABLE, exit 1). -4. `client = authenticatedManagementApiClient(...)`; null → `authRequiredError()`. -5. `provider = createAppProvider(client)` (`lib/app/app-provider.ts:271`); `created = await provider.createProject({ name: projectName.trim(), region: options?.region, signal })`. Internally `new ComputeClient(client)` then `sdk.createProject({name, region, signal})`; a Result-err becomes `throw new Error(result.error.message)`. -6. `.catch` → `projectCreateFailedError(error, name, workspace, { nextSteps: ["prisma-cli project list", "prisma-cli project link "], permissionFix: "Grant the token permission to create Projects in this workspace, or link an existing Project.", fallbackFix: "Retry the command, or choose an existing Project with prisma-cli project link ." })`. -7. `bindProjectToDirectory(context, workspace, {id, name, defaultRegion?}, "created")` (§14.6). Err → `projectDirectoryBindingErrorToCliError` (`LOCAL_STATE_WRITE_FAILED`). -8. Success: `command: "project.create"`, `result: ProjectSetupResult`, `warnings: []`, `nextSteps: ["prisma-cli app deploy"]`. - -**`PROJECT_CREATE_FAILED`** (`lib/project/setup.ts:170-205`), exit 1, domain project: -- HTTP 401/403 (from `statusCode`/`status` prop or `"(HTTP \d{3})"` in message): summary `Could not create Project "{projectName}"`, why `The platform rejected the Project create in workspace "{workspace.name}" (HTTP {status}).`, fix = permissionFix, debug = stack/message. -- else: same summary, why = `error.message`, fix = fallbackFix. - -**Human output** `renderProjectSetup` (presenters/project.ts:160-186), action `"created"`: -``` -✔ Created Project "{project.name}" -✔ Linked "{directory}" to Project "{project.name}" -Saved .prisma/local.json -``` -(`✔` via `renderSummaryLine(ui, "success", ...)`; directory is `./{basename(cwd)}` or shortened home path when binding an ancestor dir, setup.ts:207-219.) - -**JSON serializer** `serializeProjectSetup(result)` → result unchanged: `{ workspace: {id,name,...}, project: {id,name,url?,defaultRegion?}, directory: string, localPin: { path: ".prisma/local.json", written: true }, action: "created" }`. - -**Side effects**: writes `.prisma/local.json`, appends `.prisma/` to `.gitignore` (§14.6). - ---- - -## 4. `project link [id-or-name]` - -**Registration** (`commands/project/index.ts:212-240`): `new Command("link")`, descriptor `"project.link"`. Positional `.argument("[id-or-name]", "Project id or name")` (optional). No command-specific flags. - -**Descriptor** (`command-meta.ts:330-339`): -``` -description: "Link this directory to a Project" -examples: ["prisma-cli project link", "prisma-cli project link proj_123", 'prisma-cli project link "Acme Dashboard" --json'] -``` - -**Controller** `runProjectLink(context, projectRef)` — project.ts:339-399: -1. Auth + workspace guard. -2. Real mode: client (null → `authRequiredError()`), `provider = createAppProvider(client)`, `projects = await listRealWorkspaceProjects(client, workspace, signal)`. Fixture: `provider = null`, `projects = listFixtureWorkspaceProjects(context, workspace)`. -3. Branch: - - `projectRef?.trim()` truthy → `resolveProjectForSetup(projectRef.trim(), projects, workspace)` (`lib/project/setup.ts:42-57`: matches `project.id === ref || project.name === ref`; >1 → throws `projectAmbiguousError`, 0 → `projectNotFoundError`) → `requireProjectDirectoryBinding(context, workspace, toProjectSummary(project), "linked")`. - - else if `canPrompt(context) && !context.flags.yes` → `resolveInteractiveProjectLinkSetup` (project.ts:401-443) using `promptForProjectSetupChoice` (§ prompt below), then bind with the chosen action. - - else → `throw await projectLinkTargetRequiredError(context, projects)`. -4. Success: `command: "project.link"`, `result: ProjectSetupResult`, `warnings: []`, `nextSteps: ["prisma-cli app deploy"]`. - -**Prompt** `promptForProjectSetupChoice` (`lib/project/interactive-setup.ts:29-105`): -- `selectPrompt` on `runtime.stdin`/`runtime.stderr`, message: `"Which Project should this directory use?"`. -- Choice labels in order: `"+ Create a new Project"` first; then sorted projects (name asc, id asc), label = `project.name`, or `"{name} ({id})"` when the name is duplicated in the list; last: `"Cancel"`. -- Cancel → `usageError("Project setup canceled", cancel.why, cancel.fix, cancel.nextSteps, "project")`; for link the cancel object is: why `"Project link needs a Project before it can continue."`, fix `"Choose an existing Project or create a new one, then rerun project link."`, nextSteps `["prisma-cli project link ", "prisma-cli project create "]` (project.ts:427-434). -- "Create" → `textPrompt` message `"Project name"`, placeholder = `inferTargetName(cwd).name`, validate `validateProjectSetupNameText` (error text `"Enter a Project name."`, setup.ts:31-40). Empty input falls back to the suggested name. Then `createProject(projectName)`: - - fixture: `featureUnavailableError(...)` (same strings as project create). - - real: `provider.createProject({name, signal})`, `.catch` → `projectCreateFailedError(error, projectName, workspace, { nextSteps: ["prisma-cli project list", "prisma-cli project link ", "prisma-cli project create {formatCommandArgument(projectName)}"], permissionFix/fallbackFix same as create })` (project.ts:464-491). -- Returns `{project, action: "linked"|"created", targetName, targetNameSource}`. - -**Non-interactive fallback error** `PROJECT_LINK_TARGET_REQUIRED` (project.ts:493-528), domain project, exit **2**: -``` -summary: "Choose a Project to link this directory" -why: "This directory is not linked to a Prisma Project. Existing Projects are candidates until the user chooses one, and package or directory names are suggestions only." -fix: "Run prisma-cli project link in a TTY to choose from the setup list, pass a Project id or name, or create a new Project." -meta: { suggestedProjectName, suggestedProjectNameSource, candidates: ProjectSummary[], recoveryCommands: ["prisma-cli project link ", "prisma-cli project create {formatCommandArgument(suggestedName)}"] } -nextSteps: ["prisma-cli project list", ...recoveryCommands] -nextActions: buildProjectSetupNextActions({ suggestedProjectName, createCommand, reason: "Project link needs the user to choose an existing Project or create a new one. Existing Projects, package names, and directory names are candidates only, not selections." }) -``` - -**Human/JSON output**: same presenters as create (`renderProjectSetup` / `serializeProjectSetup`); action `"linked"` prints only the Linked + Saved lines (no "Created" line). - -**Side effects**: `.prisma/local.json` + `.gitignore` (§14.6). - ---- - -## 5. `project rename ` - -**Registration** (`commands/project/index.ts:63-91`): `new Command("rename")`, descriptor `"project.rename"`. Positional `.argument("", "New project name")`. Flag `new Option("--project ", "Project id or name")`. - -**Descriptor** (`command-meta.ts:340-348`): -``` -description: "Rename the resolved Project" -examples: ['prisma-cli project rename "Acme Dashboard v2"', "prisma-cli project rename billing-api --project proj_123"] -``` - -**Controller** `runProjectRename(context, newName, {project?})` — project.ts:544-584: -1. Auth + workspace guard. -2. `name = newName.trim()`; invalid → `projectSetupNameRequiredError("project rename")` — **note the copy still says "Project create requires a name"** with example `prisma-cli project rename my-app`. -3. `requireProjectCommandContext(context, workspace, options.project, "project rename")` (project.ts:862-891): real mode builds client (`requireProjectClient`, null → `authRequiredError()`), then `resolveProjectTarget({context, workspace, explicitProject, listProjects, commandName})` (§14.1 — this one **does** error `PROJECT_SETUP_REQUIRED` when unbound); provider = `createManagementProjectProvider(client)` (real) or fixture provider. -4. `renamed = await provider.renameProject({ projectId: target.project.id, name, signal })`. -5. Success: `command: "project.rename"`, `result: { workspace, project: renamed, previousName }`, `warnings: []`, `nextSteps: []`. - -**Provider** `createManagementProjectProvider(client).renameProject` (`lib/project/provider.ts:42-70`): `client.PATCH("/v1/projects/{id}", { params: {path:{id}}, body: {name}, signal })`. Status 400/422 → `projectRenameFailedError(name, result.error)`; other error → `projectApiError("Failed to rename project", response, error)`. - -**Errors**: -- `PROJECT_RENAME_FAILED` (provider.ts:115-130), exit 1: summary `"Project rename failed"`, why `error?.error?.message ?? 'The platform rejected the name "{name}".'`, fix `error?.error?.hint ?? "Pass a different project name and retry the rename."`, nextSteps `[]`. -- `PROJECT_API_ERROR` fallback (provider.ts:166-185): code = api error code or `"PROJECT_API_ERROR"`, why `The Management API returned status {status || "unknown"}.`, fix `"Re-run with --trace for the underlying API response details."`, exit 1. -- Resolution family (§14.1). - -**Human output** `renderProjectRename` (presenters/project.ts:192-214) via `renderMutate`: title `"Renaming project."`, context rows `workspace`, `project: {previousName}`, `id` (dim); operation `"Renaming project"` count 1; details: `The project is now named "{project.name}". Directory bindings pin the project id, so they stay valid.` - -**JSON serializer** `serializeProjectRename(result)` → unchanged: `{ workspace, project: {id,name,url?}, previousName }`. - -**Side effects**: none. - ---- - -## 6. `project remove ` - -**Registration** (`commands/project/index.ts:93-126`): `new Command("remove")`, descriptor `"project.remove"`. Positional `.argument("", "Project id or name")`. Flag `new Option("--confirm ", "Exact project id required to remove")`. **No `rm` alias.** No interactive confirm; `--yes` does not bypass. - -**Descriptor** (`command-meta.ts:349-354`): -``` -description: "Remove a Project permanently after exact id confirmation" -examples: ["prisma-cli project remove proj_123 --confirm proj_123"] -``` - -**Controller** `runProjectRemove(context, projectRef, {confirm?})` — project.ts:586-642: -1. `formatCommand = resolvePrismaCliPackageCommandFormatterSync(cwd)` (formats as `npx -y @prisma/cli@latest …` or the detected package runner; `lib/agent/cli-command.ts` + `shell/cli-command.ts`). -2. Auth + workspace guard. -3. `requireProjectMutationContext(context, workspace)` (project.ts:840-860): real → `{ provider: createManagementProjectProvider(client), projects: listRealWorkspaceProjects(...) }`. **No local-pin resolution — positional only**, resolved via `resolveProjectForSetup(projectRef.trim(), projects, workspace)` (throws PROJECT_AMBIGUOUS/PROJECT_NOT_FOUND). -4. `requireProjectExactConfirmation({ id: project.id, confirm, summary: "Confirm project removal", why: "Removing a project is permanent, deletes its databases, and stops its apps, so it requires the exact project id.", nextStep: formatCommand(["project","remove",project.id,"--confirm",project.id]) })`. -5. `provider.removeProject({ projectId, signal })` → `client.DELETE("/v1/projects/{id}", { params:{path:{id}}, signal })`; status 400 → `projectRemoveBlockedError(projectId, result.error)`; other error → `projectApiError("Failed to remove project", ...)`. -6. `cleanupLocalPinForProject(context, project.id, hooks)` (project.ts:1033-1061): if pin present and `pin.projectId === projectId`, `unlink(cwd + "/.prisma/local.json")`; delete failure pushes warning `"The local pin .prisma/local.json points at the removed project but could not be deleted."` and cleared=false. -7. Success: `command: "project.remove"`, `result: { workspace, project, localPin: { cleared } }`, `warnings`, `nextSteps: []`. - -**CONFIRMATION_REQUIRED** (project.ts:958-982), domain project, exit **2**: -``` -code: "CONFIRMATION_REQUIRED" -summary: "Confirm project removal" -why: "Removing a project is permanent, deletes its databases, and stops its apps, so it requires the exact project id." -fix: "Rerun with --confirm {project.id}." -nextSteps: ["{formatCommand(["project","remove",id,"--confirm",id])}"] -meta: { expectedConfirm: project.id, receivedConfirm: confirm ?? null } -``` -Check passes only when `options.confirm === options.id` (strict equality). - -**PROJECT_REMOVE_BLOCKED** (provider.ts:132-147), exit 1: summary `"Project cannot be removed yet"`, why `error?.error?.message ?? 'Project "{projectId}" still has active deployments.'`, fix `"Remove the project's apps first, then retry the removal."`, nextSteps `["npx -y @prisma/cli@latest app remove --app "]` (via `formatPrismaCliCommand`). - -Fixture provider also has a bespoke `PROJECT_NOT_FOUND` (project.ts:933-943): why `No project matched "{projectId}".`, fix `Pass a project id or name from {formatCommand(["project","list"])}.`, exit 1. - -**Human output** `renderProjectRemove` (presenters/project.ts:220-245) via `renderMutate`: title `"Removing project."`, context `workspace`/`project`/`id`(dim), operation `"Removing project"` ×1, details: `"The project, its databases, and its apps were removed."` plus, when `localPin.cleared`, `"This directory's local project binding was cleared."` - -**JSON serializer** `serializeProjectRemove(result)` → unchanged: `{ workspace, project, localPin: { cleared: boolean } }`. - -**Side effects**: may delete `.prisma/local.json`. - ---- - -## 7. `project transfer ` - -**Registration** (`commands/project/index.ts:128-181`): `new Command("transfer")`, descriptor `"project.transfer"`. Positional `.argument("", "Project id or name")`. Flags: -- `new Option("--to-workspace ", "Locally authenticated workspace to receive the project")` -- `new Option("--recipient-token ", "Access token for the receiving workspace")` -- `new Option("--confirm ", "Exact project id required to transfer")` - -**Descriptor** (`command-meta.ts:355-364`): -``` -description: "Transfer a Project to another workspace after exact id confirmation" -examples: ['prisma-cli project transfer proj_123 --to-workspace "Prisma Labs" --confirm proj_123', - "prisma-cli project transfer proj_123 --recipient-token --confirm proj_123"] -``` - -**Controller** `runProjectTransfer(context, projectRef, {toWorkspace?, recipientToken?, confirm?})` — project.ts:644-735: -1. Auth + workspace guard. -2. Both flags set → `usageError("Choose one transfer recipient source", "--to-workspace and --recipient-token are mutually exclusive.", "Pass either --to-workspace or --recipient-token .", [formatCommand(["project","transfer","","--to-workspace","","--confirm",""])], "project")` (exit 2). -3. Neither (after trim) → `transferRecipientRequiredError(formatCommand)` (below). -4. `requireProjectMutationContext` + `resolveProjectForSetup` (positional only, like remove). -5. `requireProjectExactConfirmation({ id, confirm, summary: "Confirm project transfer", why: "Transferring moves the project to another workspace and this workspace loses access, so it requires the exact project id.", nextStep: "{formatCommand(["project","transfer",id])} {--to-workspace {arg} | --recipient-token } --confirm {id}" })` — exact nextStep expression project.ts:694-698. -6. `recipient = await resolveTransferRecipient(context, options)` (§14.4). -7. `provider.transferProject({ projectId, recipientAccessToken: recipient.accessToken, signal })` → `client.POST("/v1/projects/{id}/transfer", { params:{path:{id}}, body:{recipientAccessToken}, signal })`; status 400 → `projectTransferRejectedError`; other error → `projectApiError("Failed to transfer project", ...)`. -8. `rewriteOrClearLocalPinForProject(context, project.id, recipient.workspaceId, hooks)` (project.ts:1063-1107): pin present + matching projectId → if recipient.workspaceId known, `writeLocalResolutionPin(cwd, {workspaceId: recipientWorkspaceId, projectId})` → `"rewritten"` (failure warning `"The local pin .prisma/local.json points at the transferred project but could not be rewritten."`, action `"none"`); if recipient workspace unknown (recipient-token in real mode), `unlink` → `"cleared"` (failure warning `"...could not be cleared."`); else `"none"`. -9. Success: `command: "project.transfer"`, `result: { workspace, project, recipient: { workspaceId, workspaceName, source }, localPin: { action } }`, `warnings`, `nextSteps: options.toWorkspace ? ["{formatCommand(["auth","workspace","use"])} {formatCommandArgument(options.toWorkspace)}"] : []`. - -**Errors** (see also §14.4): -- `TRANSFER_RECIPIENT_REQUIRED` (project.ts:984-1007), exit 2: summary `"Transfer recipient required"`, why `"Project transfer needs the receiving workspace."`, fix `"Pass --to-workspace for a locally authenticated workspace, or --recipient-token for a cross-account transfer."`, nextSteps `[formatCommand(["auth","workspace","list"]), formatCommand(["project","transfer","","--to-workspace","","--confirm",""])]`. -- `TRANSFER_RECIPIENT_UNAVAILABLE` (project.ts:1009-1031), exit 1 — raised when `env.PRISMA_SERVICE_TOKEN !== undefined` and `--to-workspace` used: summary `"Local workspace sessions are unavailable"`, why `"--to-workspace resolves locally stored OAuth sessions, but PRISMA_SERVICE_TOKEN is set and service-token mode does not read them."`, fix `"Pass --recipient-token with an access token for the receiving workspace, or unset the service token."`, nextSteps `[formatCommand(["project","transfer","","--recipient-token","","--confirm",""])]`. -- `PROJECT_TRANSFER_REJECTED` (provider.ts:149-164), exit 1: summary `"Project transfer was rejected"`, why `error?.error?.message ?? 'The platform rejected the transfer of project "{projectId}", for example because the recipient token is invalid or expired.'`, fix `"Check the recipient workspace session or token and retry the transfer."`, nextSteps `[]`. -- `WORKSPACE_NOT_AUTHENTICATED` / `WORKSPACE_AMBIGUOUS` (§14.4), `CONFIRMATION_REQUIRED` (exit 2, meta as in remove but summary/why per step 5). - -**Human output** `renderProjectTransfer` (presenters/project.ts:251-288) via `renderMutate`: title `"Transferring project."`; context rows `workspace`, `project`, `id`(dim), `recipient: {workspaceName ?? workspaceId ?? "workspace of the provided recipient token"}`; operation `"Transferring project"` ×1; details: `"The project now belongs to the recipient workspace; this workspace no longer has access."` plus `"This directory's local project binding now points at the recipient workspace."` (rewritten) or `"This directory's local project binding was cleared."` (cleared). - -**JSON serializer** `serializeProjectTransfer(result)` → unchanged: `{ workspace, project, recipient: { workspaceId: string|null, workspaceName: string|null, source: "workspace-session"|"recipient-token" }, localPin: { action: "rewritten"|"cleared"|"none" } }`. - -**Side effects**: rewrites or deletes `.prisma/local.json`. - ---- - -## 8. `project env add [assignment]` - -**Registration** (`commands/env.ts:45-100`): `new Command("add")`, descriptor `"project.env.add"`. Positional `.argument("[assignment]", "Variable assignment as KEY=VALUE or KEY from the current environment")`. Flags: -- `new Option("--file ", "Read KEY=VALUE assignments from a dotenv file")` -- `new Option("--role ", "Project template scope (production or preview)").choices(["production","preview"])` (commander enforces choices → its own invalid-argument error before the controller runs) -- `new Option("--branch ", "Preview branch override scope")` -- `new Option("--project ", "Project id or name")` - -**Descriptor** (`command-meta.ts:661-673`): description `"Create a new environment variable."`, examples: -``` -prisma-cli project env add STRIPE_KEY=sk_test_xxx --role production -prisma-cli project env add STRIPE_KEY=sk_test_xxx --role preview -prisma-cli project env add --file .env --role preview -prisma-cli project env add DATABASE_URL=postgresql://branch --branch feature/foo -prisma-cli project env add --file .env.local --branch feature/foo -API_URL=https://api.example prisma-cli project env add API_URL --project proj_123 --role preview -``` - -**Controller** `runEnvAdd(context, rawAssignment, {roleName, branchName, projectRef, filePath})` — `controllers/app-env.ts:89-204`. Order of operations: -1. `resolveEnvWriteSource(rawAssignment, filePath, "add")` (app-env.ts:303-348) — usage errors §14.2.d. -2. `resolveEnvScope(flags, { requireExplicit: true, command: "add" })` (§14.2.a). Null (unreachable given requireExplicit throws, but defensively) → `usageError("prisma-cli project env add requires --role or --branch", "Writing without an explicit scope is rejected.", "Pass --role production, --role preview, or --branch .", ["prisma-cli project env add KEY=value --role production"], "app")`. -3. `resolveEnvWriteInput` — file: `readEnvFileAssignments(cwd, filePath, "add")` (§14.2.e); single: `parseKeyValuePositional(rawAssignment, "add", context.runtime.env)` (§14.2.c). -4. `requireClientAndProject(context, projectRef, "project env add")` (§14.2.f — auth, client, full project resolution incl. local pin). -5. `resolveScopeToApi(client, projectId, scope, { createBranchIfMissing: true, signal })` (§14.2.b). -6. File mode → `runEnvAddFile` (§ below). Single mode: - - `findVariableByNaturalKey(client, projectId, key, resolved, signal)` (`controllers/app-env-api.ts:29-61`): `GET /v1/environment-variables` with query `{projectId, class, key, branchId?}` then exact-scope filter (`row.class === class && row.branchId === branchId`). - - Exists → `ENV_VARIABLE_ALREADY_EXISTS` (app-env.ts:140-152), domain app, exit 1: summary `Variable "{key}" already exists in {formatScopeLabel(scope)}` (scope label = role name or `branch:{name}`), why `"A variable with this key already exists in the targeted scope."`, fix `` "Use `prisma-cli project env update` to change an existing variable's value." ``, nextSteps `["prisma-cli project env update {key}= {--role r|--branch b}"]`. - - Branch scope + key absent from preview role → warning `Variable "{key}" does not exist in preview. It will only exist on branch:{name}.` (app-env.ts:154-169). - - `client.POST("/v1/environment-variables", { body: { projectId, class, branchId?, key, value }, signal })`; error → `apiCallError("Failed to add {key}", response, error)` (§14.2.g). -7. Success: `command: "project.env.add"`, `result: { projectId, verboseContext, scope: resolved.descriptor, variable: toMetadata(row, descriptor) }`, `warnings`, `nextSteps: []`. - -**File mode** `runEnvAddFile` (`controllers/app-env-file.ts:28-123`): -- Per-key lookup of all assignment keys; any existing → `ENV_VARIABLE_ALREADY_EXISTS`, summary `{n} environment variable(s) already exist in {scopeLabel}`, why `Existing keys: "{K1}", "{K2}".`, fix `"Split the input file by key state: update existing keys and add new keys separately."`, meta `{ keys: existingKeys }`, nextSteps (`splitFileNextSteps`, file.ts:343-369): - ``` - # existing keys: "K1", "K2" - prisma-cli project env update --file {filePath}.existing {scopeFlag} - # new keys only - prisma-cli project env add --file {filePath}.new {scopeFlag} - ``` -- Branch scope: warning for keys missing from preview — single `Variable "{K}" does not exist in preview. It will only exist on branch:{b}.` / plural `Variables "{K1}", "{K2}" do not exist in preview. They will only exist on branch:{b}.` -- Sequential POST per assignment; mid-loop failure → `ENV_FILE_APPLY_FAILED` (file.ts:288-324), exit 1: summary `Failed to add "{failedKey}" from "{filePath}"`, why `No variables were written before {failedKey} failed. Cause: {cause}` or `Written keys before failure: "{K}". Cause: {cause}`, fix `"Inspect the target scope, then retry the remaining keys once the API issue is resolved."`, nextSteps `["prisma-cli project env list {scopeFlag}", retryStep]` where retryStep is `prisma-cli project env add --file {filePath} {scopeFlag}` (nothing written) or `prisma-cli project env add --file {scopeFlag}` (partial), meta `{ file, failedKey, writtenKeys }`. -- Success result: `{ projectId, verboseContext, scope, variables: EnvVariableMetadata[], file: { path, count } }`. - -**`toMetadata`** (`app-env-api.ts:63-80`): `{ id, key, scope, source, isManagedBySystem, updatedAt }`; `scope` = `{kind:"role",role:row.class}` when `row.branchId === null` else the requested descriptor; `source` = `role` / `"overview"` / `branch:{branchName}` label. - -**Human output** `renderEnvAdd` (`presenters/app-env.ts:153-201`): -- Single: `renderShow` title `"Setting a new environment variable."`, fields `project: {projectId}`, `scope: {scopeLabel}`, `key`, `id` (dim), `last updated: {updatedAt}` (dim). -- File: `renderList` title `"Setting new environment variables from file."`, parent row `target: {scopeLabel} from {file.path}`, items `⚬ variable: {key} ({source})` + id, status `"default"` when `isManagedBySystem`; empty message `"No environment variables imported."`. -- Both append verbose-only blocks: "Resolved context" (workspace/project/resolution) and "Env target" (`project id` dim, `scope`, optional target/file rows, `keys: {sorted keys or "none"}`) (app-env.ts presenter lines 48-151). - -**JSON serializer** `serializeEnvAdd(result)` = `stripVerboseContext(result)` → drops `verboseContext`, everything else unchanged: single `{ projectId, scope, variable }`; file `{ projectId, scope, variables, file: {path, count} }`. `scope` is the `EnvScopeDescriptor` union; `variable(s)` entries are the `toMetadata` shape. - -**Side effects**: none local. - ---- - -## 9. `project env update [assignment]` - -**Registration** (`commands/env.ts:102-157`): identical positional/flags/descriptions to `add` (same strings), descriptor `"project.env.update"`. - -**Descriptor** (`command-meta.ts:674-684`): description `"Replace an existing environment variable's value."`, examples: -``` -prisma-cli project env update STRIPE_KEY=sk_new_xxx --role production -prisma-cli project env update STRIPE_KEY=sk_new_xxx --role preview -prisma-cli project env update --file .env --role production -prisma-cli project env update DATABASE_URL=postgresql://branch --branch feature/foo -``` - -**Controller** `runEnvUpdate` — app-env.ts:206-301. Same pipeline as add with differences: -- `resolveScopeToApi(..., { createBranchIfMissing: false })` → nonexistent branch → `ENV_BRANCH_NOT_FOUND` (§14.2.b). -- Missing var → `ENV_VARIABLE_NOT_FOUND` (app-env.ts:257-269), exit 1: summary `Variable "{key}" not found in {scopeLabel}`, why `"No variable with this key exists in the targeted scope."`, fix `` "Use `prisma-cli project env add` to create a new variable." ``, nextSteps `["prisma-cli project env add {key}= {scopeFlag}"]`. -- Mutation: `client.PATCH("/v1/environment-variables/{envVarId}", { params: {path:{envVarId: existing.id}}, body: {value}, signal })`; error → `apiCallError("Failed to update value for {key}", ...)`. -- No preview-default warning; `warnings: []`. -- File mode `runEnvUpdateFile` (file.ts:125-214): missing keys → `ENV_VARIABLE_NOT_FOUND` summary `{n} environment variable(s) not found in {scopeLabel}`, why `Missing keys: "{K}".`, fix `"Split the input file by key state: add missing keys and update existing keys separately."`, meta `{keys: missingKeys}`, nextSteps (`splitFileNextSteps` add-missing variant): - ``` - # missing keys: "K1" - prisma-cli project env add --file {filePath}.new {scopeFlag} - # existing keys only - prisma-cli project env update --file {filePath}.existing {scopeFlag} - ``` - Mid-loop failure → `ENV_FILE_APPLY_FAILED` with retry step `prisma-cli project env update --file {filePath} {scopeFlag}` (always the same file for update). -- Success: `command: "project.env.update"`, result shapes identical to add (single `variable` / file `variables`+`file`). - -**Human output** `renderEnvUpdate`: single title `"Replacing the environment variable's value."`; file title `"Replacing environment variable values from file."`, empty message `"No environment variables updated."`; same fields as add. **JSON**: `serializeEnvUpdate` = `stripVerboseContext`. - ---- - -## 10. `project env list` - -**Registration** (`commands/env.ts:159-197`): `new Command("list")`, descriptor `"project.env.list"`. No positional. Flags: -- `new Option("--role ", "Project template scope").choices(["production","preview"])` (note: no "(production or preview)" suffix here) -- `new Option("--branch ", "Preview branch resolved scope")` -- `new Option("--project ", "Project id or name")` - -**Descriptor** (`command-meta.ts:685-695`): description `"List environment variable metadata for a scope (no values)."`, examples: -``` -prisma-cli project env list -prisma-cli project env list --role production -prisma-cli project env list --role preview -prisma-cli project env list --branch feature/foo -``` - -**Controller** `runEnvList(context, {roleName, branchName, projectRef})` — app-env.ts:377-431: -1. `resolveEnvScope(flags, { requireExplicit: false, command: "list" })` — returns null when neither flag given (still throws on both flags / invalid role). -2. `requireClientAndProject(context, projectRef, "project env list")`. -3. `resolveListScopeToApi(client, projectId, explicit ?? undefined, {cwd, signal})` (app-env.ts:611-697): - - Explicit scope → `resolveScopeToApi(..., createBranchIfMissing: false)` → kind "scoped"; target `{source:"explicit", envMap: role}` for role or `{source:"explicit", branchName, branchId, branchRole:"preview", branchExists:true, envMap:"preview"}` for branch. - - No scope: `readLocalGitBranch(cwd, signal)` (`lib/git/local-branch.ts:10-30`; walks up to nearest `.git`, reads `HEAD`, returns branch name or null for detached/no repo): - - branch found remotely via `GET /v1/projects/{projectId}/branches?gitName=` → if `role === "production"`: descriptor `{kind:"role",role:"production"}`, target `{source:"local-git", branchName, branchId, branchRole, branchExists:true, envMap:"production"}`; else branch descriptor + envMap `"preview"`. - - branch not on platform → descriptor `{kind:"role",role:"preview"}`, target `{source:"local-git", branchName, branchExists:false, envMap:"preview"}`, addScope `{kind:"branch",branchName}`. - - no git branch → kind "overview": descriptor `{kind:"overview"}`, target `{source:"overview", envMap:"overview"}`, addScope `{kind:"role",role:"preview"}`. -4. Variables: scoped → `listVariables` (paginated `GET /v1/environment-variables?projectId&class[&cursor]`, filter `rowMatchesScope` — for branch scope keeps branch rows **and** class-level rows, then `materializeEffectiveRows` overlays branch rows over role defaults per key, sorted by key); overview → `listOverviewVariables` (no class filter; keeps `branchId===null` rows of both classes; sort production-first then key asc). -5. Success: `command: "project.env.list"`, `result: { projectId, verboseContext, scope: descriptor, target, variables: toMetadata[] }`, `warnings: []`, `nextSteps`: empty-list case → `["prisma-cli project env add KEY=value {formatScopeFlag(addScope)}"]`, else `[]`. - -**Human output** `renderEnvList` (presenter:261-286): `renderList`, title `"Listing environment variables for the selected scope."`, parent row `target: {listTargetLabel}` where label = `"overview"`, or `branch:{name} -> {envMap}` with suffix `" (not created yet)"` when `branchExists === false`, or plain scope label; items `⚬ variable: {key} ({source})` + id + status `"default"` if managed; empty message `"No environment variables defined in this scope."`; then verbose blocks. - -**JSON serializer** `serializeEnvList` (presenter:288-308): -```js -{ - projectId, - scope, // EnvScopeDescriptor - target, // EnvListTarget - context: { target: listTargetLabel }, - items: variables.map(v => ({ name: `${v.key} (${v.source})`, id: v.id, status: v.isManagedBySystem ? "default" : null })), - count, - variables // full EnvVariableMetadata[] -} -``` - ---- - -## 11. `project env remove ` (alias `rm`) - -**Registration** (`commands/env.ts:199-240`): `new Command("remove")`, `.alias("rm")`, descriptor `"project.env.remove"`. Positional `.argument("", "Variable key to remove")`. Flags identical to add (`--role` with "(production or preview)" suffix, `--branch ` "Preview branch override scope", `--project`). No `--confirm`/`--yes` requirement. - -**Descriptor** (`command-meta.ts:696-705`): description `"Remove an environment variable from a scope."`, examples: -``` -prisma-cli project env remove STRIPE_KEY --role production -prisma-cli project env remove STRIPE_KEY --role preview -prisma-cli project env remove DATABASE_URL --branch feature/foo -``` - -**Controller** `runEnvRemove(context, key, flags)` — app-env.ts:433-512: -1. `!key` → `usageError("prisma-cli project env remove requires KEY", "No KEY positional argument was supplied.", "Pass the variable name to remove, e.g. STRIPE_KEY.", ["prisma-cli project env remove STRIPE_KEY --role production"], "app")` (defensive; commander enforces the required arg first). -2. `resolveEnvScope({requireExplicit: true, command: "remove"})`; null fallback → `usageError("prisma-cli project env remove requires --role or --branch", "Writing without an explicit scope is rejected.", "Pass --role production, --role preview, or --branch .", ["prisma-cli project env remove {key} --role production"], "app")`. -3. `requireClientAndProject(context, projectRef, "project env remove")`; `resolveScopeToApi(..., createBranchIfMissing: false)`. -4. `findVariableByNaturalKey`; missing → `ENV_VARIABLE_NOT_FOUND` (app-env.ts:478-488): summary `Variable "{key}" not found in {scopeLabel}`, why `"No variable with this key exists in the targeted scope, so there is nothing to remove."`, fix `"Run prisma-cli project env list with the same scope to see the available variables."`, nextSteps `["prisma-cli project env list {scopeFlag}"]`, exit 1. -5. `client.DELETE("/v1/environment-variables/{envVarId}", { params:{path:{envVarId: existing.id}}, signal })`; error → `apiCallError("Failed to remove {key}", ...)`. -6. Success: `command: "project.env.remove"`, `result: { projectId, verboseContext, scope: descriptor, key }`, `warnings: []`, `nextSteps: []`. - -Note: `validateKey` (256-char / POSIX-shape checks) is **not** run on the remove positional — only add/update parse paths validate. - -**Human output** `renderEnvRm`: `renderShow` title `"Removing the environment variable from the scope."`, fields `project`, `scope`, `key`; plus verbose blocks. **JSON** `serializeEnvRm` = `stripVerboseContext` → `{ projectId, scope, key }`. - ---- - -## 14. Shared machinery - -### 14.1 Project resolution (`lib/project/resolution.ts`) - -Used by: rename (via `requireProjectCommandContext`), all four env commands (via `requireClientAndProject`), show (via `inspectProjectBinding`). **Not** used by remove/transfer/link/create (positional-or-picker only) or list. - -`resolveProjectTarget(options)` (resolution.ts:155-183) precedence, given `{context, workspace, explicitProject?, envProjectId?, commandName?, projectDir?, listProjects}`: -1. `explicitProject` (`--project`): `resolveExplicitProject` matches `project.id === ref || project.name === ref` (exact, case-sensitive). 1 match → source `"explicit"`; >1 → `ProjectAmbiguousError`; 0 → `ProjectNotFoundError`. -2. `envProjectId` (matched by id only) — **the 13 commands in scope never pass `envProjectId`**; only `app deploy`/`app run` read `PRISMA_PROJECT_ID` (`controllers/app.ts:168`). So `PRISMA_PROJECT_ID` does NOT affect project/env commands. (Task-brief assumption to correct.) -3. Local pin `.prisma/local.json` (read from `projectDir ?? cwd`): pin.workspaceId ≠ active workspace → `LocalProjectWorkspaceMismatchError`; pin projectId not in workspace list → `LocalStateStaleError`; invalid JSON/shape while reading → `LocalStateStaleError` (via `localPinReadErrorToProjectError`); match → source `"local-pin"`. -4. `resolveDurablePlatformMapping()` — hardcoded `null` (resolution.ts:586-588), source `"platform-mapping"` is dead code today. -5. Nothing → `ProjectSetupRequiredError` carrying `buildProjectSetupSuggestion` (suggested name from `package.json` `name` if matching `/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/`, else `basename(cwd)`; candidates = projects whose id/name/slug equals the suggested name). - -CliError mappings (`projectResolutionErrorToCliError`, resolution.ts:352-375): - -- **PROJECT_NOT_FOUND** (232-245), domain project, exit 1: summary `"Project not found"`, why `The project "{projectRef}" does not exist in workspace "{workspace.name}" or is not accessible.`, fix `"Pass a project id or name from prisma-cli project list."`, nextSteps `["prisma-cli project list"]`. -- **PROJECT_AMBIGUOUS** (256-285), exit **1** (inventory says 2 for show — code says 1): summary `"Project resolution is ambiguous"`, why `Multiple projects matched "{projectRef}".` (or `"Multiple projects matched the current directory context."` when ref null), fix `"Pass --project to choose the project explicitly."`, meta `{ matches: [{id,name}] }`, nextSteps `["prisma-cli project list"]` plus, when a first match exists, `"prisma-cli app deploy --project {firstMatch.id}"`. -- **LOCAL_STATE_STALE** (291-307), exit 1: summary `"Local project binding is stale"`, why `The target recorded in .prisma/local.json is no longer available in the selected workspace.`, fix `Delete .prisma/local.json, then choose a Project explicitly.`, meta `{ pinPath: ".prisma/local.json" }`, nextSteps `["prisma-cli project list", "prisma-cli project link "]`. -- **LOCAL_PROJECT_WORKSPACE_MISMATCH** (319-344), exit 1: summary `"Project link uses another workspace"`, why `.prisma/local.json links this directory to project {pinnedProjectId} in workspace {pinnedWorkspaceId}, but your current CLI session is workspace "{activeWorkspace.name}" ({activeWorkspace.id}).`, fix `"Switch to the linked workspace, or relink this directory to a project in the current workspace."`, meta `{ pinPath, pinnedWorkspaceId, pinnedProjectId, activeWorkspaceId, activeWorkspaceName }`, nextSteps `["prisma-cli auth workspace use {pinnedWorkspaceId}", "prisma-cli project list", "prisma-cli project link "]`. -- **PROJECT_SETUP_REQUIRED** (411-429), exit 1: summary `"Choose a Project before running this command"`, why = `This directory is not linked to a Prisma Project, and {("prisma-cli " + commandName) | "this command"} will not choose one from package or directory names.`, fix `"Link the directory to an existing Project, or pass --project for this command."`, meta `{ suggestedProjectName, suggestedProjectNameSource, candidates, recoveryCommands }`, nextSteps `["prisma-cli project list", "prisma-cli project link ", "prisma-cli {commandName} --project "]`, nextActions `buildProjectSetupNextActions({commandName, suggestedProjectName})`. - -**`buildProjectSetupNextActions`** (resolution.ts:431-494) — NextAction objects, `journey: "project-setup"` (retry action `"recover"`): -1. `{ kind: "user-choice", label: "Ask the user whether to link an existing Project or create a new one", commands: ["prisma-cli project list", "prisma-cli project link "(, "prisma-cli {commandName} --project ")], reason: options.reason ?? "This directory is not linked to a Prisma Project. Package and directory names are suggestions only, not a safe Project selection." }` -2. `{ kind: "run-command", label: "Link the chosen Project", command: "prisma-cli project link ", reason: "Linking writes the durable local Project binding for this directory." }` -3. When createCommand or suggested name present: `{ kind: "run-command", label: "Create and link a new Project", command: createCommand ?? "prisma-cli project create {formatCommandArgument(suggestedProjectName)}", reason: "Use this when the user wants a new Prisma Project instead of an existing one." }` -4. When commandName present: `{ kind: "run-command", journey: "recover", label: "Retry with an explicit Project", command: "prisma-cli {commandName} --project " }` - -`listRealWorkspaceProjects(client, workspace, signal?)` (project.ts:1438-1466): `client.GET("/v1/projects", { signal })` — **single call, NOT paginated, no error branch (`data?.data ?? []` swallows errors as empty)**; filters `project.workspace.id === workspace.id` client-side; maps `{id, name, url?, defaultRegion?, slug, workspace:{id,name}}`; `sortProjects` = name `localeCompare` then id. - -### 14.2 Env scope machinery - -**(a) `resolveEnvScope(flags, {requireExplicit, command})`** (`lib/app/env-config.ts:27-81`) — all USAGE_ERROR (exit 2), domain `"app"`: -- both flags: summary `prisma-cli project env {command} accepts either --role or --branch`, why `"--role targets a project-level config map; --branch targets a preview branch override."`, fix `"Pass exactly one scope flag."`, nextSteps `["prisma-cli project env {command} {positionalHint}--role preview", "...{positionalHint}--branch feature/foo"]` (positionalHint = `"KEY=value "` for add/update, `"KEY "` for remove, `""` for list). -- invalid role (defensive; commander `.choices` normally rejects first with `error: option '--role ' argument 'x' is invalid. Allowed choices are production, preview.`): summary `Unknown role "{roleName}"`, why `"--role accepts production or preview."`, fix `"Pass --role production or --role preview."`. -- neither + requireExplicit: summary `prisma-cli project env {command} requires --role or --branch`, why `"Writing without an explicit scope is rejected so the command never silently targets production."`, fix `"Pass --role production, --role preview, or --branch ."`, nextSteps 3 hints (`--role production`, `--role preview`, `--branch feature/foo`). - -**(b) `resolveScopeToApi(client, projectId, scope, {createBranchIfMissing, signal})`** (app-env.ts:560-609): -- role → `{descriptor: {kind:"role",role}, apiTarget: {class: role, branchId: null}}`. -- branch → `listBranchesByName` = `GET /v1/projects/{projectId}/branches?gitName={branchName}` (first row). Missing: - - `createBranchIfMissing: false` → **ENV_BRANCH_NOT_FOUND** (app-env.ts:767-780), exit 1: summary `Branch "{branchName}" not found`, why `"Branch update, list, and remove commands only target existing preview branches."`, fix `` "Create the branch by deploying it, or use `project env add --branch` to create its first override." ``, nextSteps `["prisma-cli project env add KEY=value --branch {branchName}"]`. - - `createBranchIfMissing: true` (`resolveOrCreateBranch`, app-env.ts:783-834): first checks `projectHasDefaultBranch` (paginated `GET /v1/projects/{projectId}/branches` walking `pagination.nextCursor` while `hasMore`); no default branch → **ENV_BRANCH_CREATE_REQUIRES_DEFAULT_BRANCH** (796-806), exit 1: summary `Cannot create branch "{branchName}" from project env`, why `"Creating the first branch would make it the project default, but branch overrides are preview-only."`, fix `"Create or deploy the default branch first, then add the branch override."`, nextSteps `["prisma-cli app deploy --branch main"]`. Then `POST /v1/projects/{projectId}/branches` body `{gitName: branchName, isDefault: false}`; 409 → re-list race recovery; else `apiCallError('Failed to create branch "{branchName}"', ...)`. -- resolved branch with `role === "production"` → **ENV_BRANCH_SCOPE_IS_PRODUCTION** (588-597), exit 1: summary `Branch "{branchName}" is the production branch`, why `"Production variables are project-level only; branch overrides apply to preview branches."`, fix `"Use --role production for the production branch."`, nextSteps `["prisma-cli project env list --role production"]`. -- branch success → `{descriptor: {kind:"branch", branchName: branch.gitName, branchId: branch.id}, apiTarget: {class:"preview", branchId: branch.id}}`. - -**(c) `parseKeyValuePositional(raw, command, env)`** (env-config.ts:83-148) — all usage errors exit 2 domain app: -- no `=` and matches `KEY_SHAPE = /^[A-Z_][A-Z0-9_]*$/`: reads `env[raw]`; unset/empty → `Value for "{raw}" was not provided` / why `No KEY=VALUE assignment was supplied, and {raw} is not set in the current environment.` / fix `"Pass KEY=VALUE or export the variable before running the command."` / nextSteps `["prisma-cli project env {command} {raw}=value --role production", "{raw}=value prisma-cli project env {command} {raw} --role production"]`. -- no `=`, not key-shaped: `KEY=VALUE argument is missing the = separator`, why `"{raw}" does not contain an = character.`, fix `"Pass the variable as KEY=VALUE, e.g. STRIPE_KEY=sk_test_xxx."`. -- empty value after `=`: `KEY=VALUE argument has an empty value`, why `"{raw}" has an empty value after the = separator.`, fix `"Pass a non-empty value, or use prisma-cli project env remove to remove a variable."`. -- `validateKey` (env-config.ts:152-189): empty key → `Variable key cannot be empty`; >256 chars → `Variable key "{key}" exceeds the 256-character limit` (why `"Env-var keys are capped at 256 characters by the platform."`, fix `"Use a shorter key."`); shape violation → `Variable key "{key}" must match the POSIX env-var shape`, why `"Keys must start with an uppercase letter or underscore and contain only uppercase letters, digits, and underscores."`, fix `"Rename the key to match [A-Z_][A-Z0-9_]*."`. - -**(d) `resolveEnvWriteSource`** usage errors (app-env.ts:303-348), exit 2 domain app: -- both positional+file: `prisma-cli project env {command} accepts either KEY=VALUE or --file`, why `"The command received both a positional assignment and a dotenv file path."`, fix `"Pass one input source."`, nextSteps `["prisma-cli project env {command} KEY=value --role preview", "prisma-cli project env {command} --file .env --role preview"]`. -- `--file` with empty string: `prisma-cli project env {command} --file requires a path`, why `"The --file flag was passed without a file path."`, fix `"Pass a readable dotenv file path."`. -- neither: `prisma-cli project env {command} requires KEY=VALUE or --file`, why `"No environment variable input was supplied."`, fix `"Pass a single KEY=VALUE assignment or a dotenv file path."`, same two nextSteps. - -**(e) dotenv parsing `readEnvFileAssignments(cwd, filePath, command)`** (`lib/app/env-file.ts:23-101`): resolves relative to cwd; read failure → usage error `Failed to read env file "{filePath}"` (why = fs error message, fix `"Pass a readable dotenv file path."`, nextSteps `["prisma-cli project env {command} --file .env --role preview"]`). Parsing: custom key extraction regex `/^\s*(?:export\s+)?([^#=\s]+)\s*=/` with multiline-quote tracking, values from `dotenv.parse`. Errors (all usage, exit 2, app): `No environment variables found in "{filePath}"` (why `"The file does not contain any KEY=VALUE assignments."`); per-key validation wrapped as `Invalid environment variable "{key}" in "{filePath}"` (why `Line {n}: {validateKey message}`); `Duplicate environment variable "{key}" in "{filePath}"` (why `Lines {a} and {b} both define {key}.`, fix `"Keep one assignment for each key before importing the file."`); `Environment variable "{key}" in "{filePath}" has an empty value` (why `Line {n} defines {key} with an empty value.`, fix `"Pass a non-empty value, or omit the key from the file."`). - -**(f) `requireClientAndProject(context, explicitProject, commandName)`** (app-env.ts:514-558): `requireAuthenticatedAuthState` → `authenticatedManagementApiClient` (null → `authRequiredError(["prisma-cli auth login"])`) → workspace guard → `resolveProjectTarget` (listProjects = `listRealWorkspaceProjects`). Returns `{client, projectId, verboseContext: {workspace, project, resolution}}`. **Env commands are real-mode only in this path** (no fixture branch; fixture behavior for env lives in the mock API elsewhere). - -**(g) `apiCallError(summary, response, error)`** (`controllers/app-env-api.ts:92-118`): 401/403 → `authRequiredError(["prisma auth login"])` — note **`"prisma auth login"`, not `"prisma-cli auth login"`** (inconsistency worth preserving-or-fixing consciously). Otherwise `CliError { code: apiCode ?? "ENV_API_ERROR", domain: "app", summary, why: apiMessage ?? 'The Management API returned status {status || "unknown"}.', fix: apiHint ?? "Re-run with --trace for the underlying API response details.", exitCode: 1, nextSteps: [] }`. - -### 14.3 Auth / workspace requirement - -- `requireAuthenticatedAuthState(context)` (`controllers/auth.ts:205-237`): real mode reads stored auth; unauthenticated + non-TTY → `authRequiredError()`; unauthenticated + TTY → runs interactive `performLogin` then re-reads ("platform+login" behavior). -- `authRequiredError(nextSteps = ["prisma-cli auth login"])` (`shell/errors.ts:101-115`): code `AUTH_REQUIRED`, domain auth, exit 1, summary `"Authentication required"`, why `"This command needs an authenticated session."`, fix `"Run prisma-cli auth login, or rerun the command in a TTY to sign in interactively."`. -- `workspaceRequiredError()` (`shell/errors.ts:141-149`) = `usageError(...)` so the code is **`USAGE_ERROR`** (not a `WORKSPACE_REQUIRED` code), exit 2, domain auth: summary `"Workspace required"`, why `"This command needs an active workspace, but the authenticated session does not have one."`, fix `"Run prisma-cli auth login and choose a workspace."`, nextSteps `["prisma-cli auth login"]`. (*Inventory calls this "`WORKSPACE_REQUIRED` usage error" — the wire code is `USAGE_ERROR`.*) -- `authenticatedManagementApiClient(env, signal)` (`auth/guard.ts:19-60`): `PRISMA_SERVICE_TOKEN` set → `createManagementApiClient({baseUrl: getApiBaseUrl(env), token})` (empty token throws `Error("PRISMA_SERVICE_TOKEN is set but empty. Provide a valid token or unset the variable.")` → mapped to `AUTH_CONFIG_INVALID` by the runner); else `FileTokenStorage` + `createManagementApiSdk({clientId: CLIENT_ID = "cmm3lndn701oo0uefvxzo0ivw", redirectUri: "http://localhost:0/auth/callback", tokenStorage, apiBaseUrl})` → `sdk.client`; no stored tokens → `null`. Base URL: `env.PRISMA_MANAGEMENT_API_URL?.trim() || "https://api.prisma.io"`. -- Command runner also maps SDK `AuthError` → `authRequiredError(["prisma-cli auth login"], {debug})` and abort → `COMMAND_CANCELED` exit 130 (`shell/command-runner.ts:40-66`). - -### 14.4 Transfer recipient machinery - -`resolveTransferRecipient(context, options)` (project.ts:744-833), real mode: -- `--recipient-token` (trimmed): `{accessToken: token, workspaceId: null, workspaceName: null, source: "recipient-token"}` — no validation call. -- `--to-workspace`: `PRISMA_SERVICE_TOKEN` present → `TRANSFER_RECIPIENT_UNAVAILABLE` (§7). Else `resolveRecipientWorkspaceSession(workspaceRef, env, signal)` (`auth/recipient.ts:29-65`): `FileTokenStorage.resolveWorkspace(ref)` (match rule `workspaceMatchesRef`, token-storage.ts:761-772: credentialWorkspaceId, id, prefix-stripped forms, or case-insensitive name; 0 matches → `WorkspaceSelectionError("not-found")`, >1 → `("ambiguous", ref, matches)`); then a pinned-storage SDK probe `GET /v1/workspaces` (triggers token refresh); probe error or missing tokens → `RecipientSessionInvalidError`. Returns `{workspace, accessToken}` → `{workspaceId: workspace.id, workspaceName: workspace.name, source: "workspace-session"}`. -- Error mapping (project.ts:814-832): `WorkspaceSelectionError` ambiguous → `workspaceAmbiguousError(ref, matches)` (`auth/errors.ts:38-55`): code `WORKSPACE_AMBIGUOUS`, exit 2, summary `"Workspace name is ambiguous"`, why `Multiple authenticated workspaces matched "{ref}".`, fix `"Run prisma-cli auth workspace list and switch by workspace id."`, meta `{workspaceRef, matches: [{id,name,credentialWorkspaceId}]}`, nextSteps `["prisma-cli auth workspace list"]`. Other reasons and `RecipientSessionInvalidError` → `workspaceNotAuthenticatedError(ref)` (auth/errors.ts:23-36): code `WORKSPACE_NOT_AUTHENTICATED`, exit 1, summary `"Workspace is not authenticated"`, why `No stored OAuth session matched "{workspaceRef}".`, fix `"Run prisma-cli auth login and authorize that workspace, then switch to it."`, meta `{workspaceRef}`, nextSteps `["prisma-cli auth workspace list", "prisma-cli auth login"]`. - -### 14.5 Pagination - -- `GET /v1/projects` (`listRealWorkspaceProjects`): **not paginated** — single request. -- `GET /v1/environment-variables` list paths (`collectEnvironmentVariables`, app-env.ts:919-965) and `projectHasDefaultBranch` (app-env.ts:836-876): cursor loop — repeat while `pagination.hasMore && pagination.nextCursor`, passing `cursor` in query. -- `findVariableByNaturalKey`: single request; relies on server-side `key`+`branchId` filters (comment at app-env-api.ts:36 explains the 100-row page limit rationale). -- Branch lookup by name (`listBranchesByName`): single request with `gitName` query, takes `[0]`. - -### 14.6 Local pin write / gitignore (`lib/project/local-pin.ts`) - -- Pin path constant: `LOCAL_RESOLUTION_PIN_RELATIVE_PATH = ".prisma/local.json"`. -- Content written (`writeLocalResolutionPin`, 250-291): `JSON.stringify({workspaceId, projectId}, null, 2) + "\n"` — exactly two keys; written atomically via `mkdir .prisma` → temp file `local.{pid}.{Date.now()}.tmp` → `rename`. -- Read validation (`isLocalResolutionPin`, 419-440): object with **exactly** keys `workspaceId` and `projectId`, both non-empty strings; extra keys → invalid shape. -- `ensureLocalResolutionPinGitignore` (293-346): no `.gitignore` → create with content `".prisma/\n"`; existing file → skip if any trimmed line equals `.prisma/` or `.prisma/local.json`; else append `".prisma/\n"` (with a `"\n"` separator first when the file doesn't end in a newline). -- Failure surface: `LOCAL_STATE_WRITE_FAILED` (`lib/project/setup.ts:94-145`), exit 1, domain project: summary `"Could not save local Project binding"`, why `"The CLI could not write .prisma/local.json."` (pin) or `"The CLI could not update .gitignore to keep local Project binding state out of git."` (gitignore), fix `"Check that this directory is writable and that .prisma/local.json and .gitignore are not blocked by directories or permissions, then retry."`, meta `{pinPath, operation}` or `{gitignorePath, operation}`, nextSteps `["prisma-cli project link ", "prisma-cli app deploy --project "]`. - -### 14.7 Provider/operation signatures the port must call - -Per the port rule (client built from ctx.api), these are the exact call surfaces: - -| operation | exported name / call | file | client arg? | params | -|---|---|---|---|---| -| list projects | `listRealWorkspaceProjects(client, workspace, signal?)` → `client.GET("/v1/projects", {signal})` | controllers/project.ts:1438 | yes, first arg (`ManagementApiClient`) | `(client, workspace: AuthWorkspace, signal?: AbortSignal)` | -| create project | `createAppProvider(client).createProject({name, region?, signal?}): Promise` (wraps `ComputeClient(client).createProject`) | lib/app/app-provider.ts:271,281 | yes (factory takes client) | as shown | -| rename | `createManagementProjectProvider(client).renameProject({projectId, name, signal?}): Promise` → `PATCH /v1/projects/{id}` body `{name}` | lib/project/provider.ts:38-70 | yes (factory) | as shown | -| remove | `.removeProject({projectId, signal?}): Promise` → `DELETE /v1/projects/{id}` | provider.ts:72-89 | yes | as shown | -| transfer | `.transferProject({projectId, recipientAccessToken, signal?}): Promise` → `POST /v1/projects/{id}/transfer` body `{recipientAccessToken}` | provider.ts:91-111 | yes | as shown | -| env find | `findVariableByNaturalKey(client, projectId, key, resolved: ResolvedEnvApiScope, signal)` | controllers/app-env-api.ts:29 | yes, first arg | as shown | -| env create | inline `client.POST("/v1/environment-variables", {body: {projectId, class, branchId?, key, value}, signal})` | app-env.ts:171 / app-env-file.ts:75 | yes | — | -| env update | inline `client.PATCH("/v1/environment-variables/{envVarId}", {params:{path:{envVarId}}, body:{value}, signal})` | app-env.ts:271 / file.ts:169 | yes | — | -| env delete | inline `client.DELETE("/v1/environment-variables/{envVarId}", {params:{path:{envVarId}}, signal})` | app-env.ts:490 | yes | — | -| env list | `collectEnvironmentVariables(client, projectId, signal, {className?, filter})` (internal) | app-env.ts:919 | yes | — | -| branch by name | `listBranchesByName(client, projectId, branchName, signal)` → `GET /v1/projects/{projectId}/branches?gitName=` (internal) | app-env.ts:731 | yes | — | -| branch create | inline `client.POST("/v1/projects/{projectId}/branches", {params:{path:{projectId}}, body:{gitName, isDefault:false}, signal})` | app-env.ts:808 | yes | — | -| recipient session | `resolveRecipientWorkspaceSession(workspaceRef, env, signal?)` | auth/recipient.ts:29 | **no** — builds its own SDK from FileTokenStorage + env | `(workspaceRef: string, env: NodeJS.ProcessEnv, signal?: AbortSignal)` | -| client factory | `authenticatedManagementApiClient(env, signal?): Promise` | auth/guard.ts:19 | n/a (this IS the factory) | as shown | - -Note the AppProvider also exposes `listEnvironmentVariables/createEnvironmentVariable/updateEnvironmentVariable/deleteEnvironmentVariable` (app-provider.ts:164-188) but the env commands do **not** use them — they call the client inline. - ---- - -## 15. Discrepancies / gaps vs the inventory & task brief - -1. **`PROJECT_AMBIGUOUS` exit code**: inventory §`project show` says exit 2; code sets `exitCode: 1` (resolution.ts:283). Same code path everywhere. -2. **`PRISMA_PROJECT_ID`**: none of the 13 commands read it. `resolveProjectTarget` accepts `envProjectId` but only `controllers/app.ts` passes it (app deploy/run). The shared resolution section of any design doc should not claim env-var resolution for these commands. -3. **`WORKSPACE_REQUIRED`**: not a distinct code — it is a `USAGE_ERROR` with summary `"Workspace required"` (exit 2). Inventory phrasing is loose here. -4. **`apiCallError` 401/403 nextStep** says `"prisma auth login"` (no `-cli`), unlike everywhere else (`app-env-api.ts:103`). Pre-existing copy inconsistency. -5. **`projectSetupNameRequiredError("project rename")`** produces summary "Project create requires a name" for rename (only the example command varies). Pre-existing copy bug. -6. `GET /v1/projects` is unpaginated and its error result is silently treated as an empty list (`data?.data ?? []`, project.ts:1443) — no error surface on list failure. -7. Two identical `toProjectSummary` implementations exist (resolution.ts:755 private, setup.ts:147 exported). -8. `project remove`/`transfer` resolve the positional against the workspace list only (no local pin, no `--project` flag); rename/env commands use full pin resolution; show uses pin resolution but treats unbound as success. Inventory matches this but does not spell out the remove/transfer non-pin path. -9. Env group node (`commands/env.ts`) gets no compact global flags, unlike other group nodes. -10. `project remove` has no `rm` alias (only `project env remove` has `rm`). diff --git a/.drive/projects/prisma-cli-v8/specs/s2b-design/facts/facts-d2-postgres.md b/.drive/projects/prisma-cli-v8/specs/s2b-design/facts/facts-d2-postgres.md deleted file mode 100644 index e7287927..00000000 --- a/.drive/projects/prisma-cli-v8/specs/s2b-design/facts/facts-d2-postgres.md +++ /dev/null @@ -1,552 +0,0 @@ -# Facts: `database` group (11 commands) — legacy commander CLI, verbatim extraction - -All paths are relative to the repository root (written below as ``, -the checkout of branch `claude/prisma-cli-v8-onboarding-30e694`). Key -files: - -- Registration: `/packages/cli/src/commands/database/index.ts` -- Controllers: `/packages/cli/src/controllers/database.ts` -- Provider: `/packages/cli/src/lib/database/provider.ts` -- Presenters: `/packages/cli/src/presenters/database.ts` -- Descriptors: `/packages/cli/src/shell/command-meta.ts` -- Errors: `/packages/cli/src/shell/errors.ts` -- Types: `/packages/cli/src/types/database.ts` -- Grounding inventory: `/.drive/projects/prisma-cli-v8/assets/s2/command-inventory.md` - ---- - -## Shared: command tree registration - -`createDatabaseCommand(runtime)` (commands/database/index.ts:64-82) builds group `database` (descriptor id `"database"`), attaches `addCompactGlobalFlags`, then adds subcommands in this order: `list`, `show`, `create`, `usage`, `restore`, `remove`, `backup` (group, compact flags, one child `list`), `connection` (group, compact flags, children in order `list`, `create`, `rotate`, `remove`). - -Every leaf gets `addGlobalFlags(command)`. The project/branch flag pair is one helper (index.ts:84-88): - -```ts -function addProjectAndBranchOptions(command: Command): Command { - return command - .addOption(new Option("--project ", "Project id or name")) - .addOption(new Option("--branch ", "Branch git name")); -} -``` - -Group descriptors (command-meta.ts): - -- `database` (219-227): description `"Manage Prisma Postgres databases for a project"`, examples `"prisma-cli database list"`, `"prisma-cli database create my-db"`, `"prisma-cli database connection create db_123"`. -- `database.backup` (443-447): `"Inspect platform-created database backups"`, example `"prisma-cli database backup list db_123"`. -- `database.connection` (458-466): `"Manage one-time-view database connection strings"`, examples `"prisma-cli database connection list db_123"`, `"prisma-cli database connection create db_123"`, `"prisma-cli database connection remove conn_123 --confirm conn_123"`. - -## Shared: auth + provider construction + project resolution - -`requireDatabaseContext(context, flags, commandName)` (controllers/database.ts:705-763) is used by every command that takes `--project/--branch`. Flow: - -1. `requireAuthenticatedAuthState(context)` (from `controllers/auth.ts`; per inventory §3.4 it launches interactive OAuth login on a TTY if unauthenticated, else throws `AUTH_REQUIRED`). -2. If `authState.workspace` is missing → `workspaceRequiredError()` (errors.ts:141-149): a `usageError` (`USAGE_ERROR`, exit 2, domain `"auth"`) with summary `"Workspace required"`, why `"This command needs an active workspace, but the authenticated session does not have one."`, fix `"Run prisma-cli auth login and choose a workspace."`, nextSteps `["prisma-cli auth login"]`. -3. Real mode (`isRealMode` = neither `context.runtime.fixturePath` nor env `PRISMA_CLI_MOCK_FIXTURE_PATH`, database.ts:91-96): `authenticatedManagementApiClient(context.runtime.env, context.runtime.signal)`; null → `authRequiredError()` (errors.ts:101-115: code `AUTH_REQUIRED`, domain `auth`, exit 1, summary `"Authentication required"`, why `"This command needs an authenticated session."`, fix `"Run prisma-cli auth login, or rerun the command in a TTY to sign in interactively."`, nextSteps `["prisma-cli auth login"]`). -4. `resolveProjectTarget({context, workspace, explicitProject: flags.projectRef, listProjects: () => listRealWorkspaceProjects(client, workspace, signal), commandName})` (lib/project/resolution.ts:155-183). Resolution order inside: implicit local pin (`.prisma/local.json`) then explicit/pin match against the listed projects. **`PRISMA_PROJECT_ID` does NOT affect these commands**: `resolveProjectTarget` accepts an `envProjectId` option, but `requireDatabaseContext` does not pass one (see the call quoted above) and only `controllers/app.ts` reads the variable, for `app deploy`/`app run`. Corrected 2026-08-11 against the source; `facts-d1-project.md` §14.1 step 2 and §15.2 say the same. Unbound → `ProjectSetupRequiredError` whose message is `` `This directory is not linked to a Prisma Project, and ${commandLabel} will not choose one from package or directory names.` `` (resolution.ts:106-126). Errors are converted via `projectResolutionErrorToCliError` (resolution.ts:352) to `PROJECT_NOT_FOUND` / `PROJECT_AMBIGUOUS` / `PROJECT_SETUP_REQUIRED` / `LOCAL_STATE_STALE` / `LOCAL_PROJECT_WORKSPACE_MISMATCH` CliErrors. -5. Provider: `createManagementDatabaseProvider(client, { formatCommand: resolvePrismaCliPackageCommandFormatterSync(context.runtime.cwd), workspaceId: workspace.id })` (database.ts:738-743). - -Fixture mode instead builds `createFixtureDatabaseProvider(context)` (database.ts:789-910) over `context.api` and resolves projects via `listFixtureWorkspaceProjects`. - -`requireDatabaseProviderOnly(context)` (database.ts:765-787) — used by `connection remove` and `connection rotate` only (no project resolution at all): requires auth state, builds the client, and calls `createManagementDatabaseProvider(client, { formatCommand, workspaceId: authState.workspace?.id })`. Note `workspaceId` may be undefined here. - -`resolvePrismaCliPackageCommandFormatterSync(cwd)` (lib/agent/cli-command.ts:28-32) returns a formatter that prefixes args with the package-manager runner (`pnpm dlx | bunx | npx -y @prisma/cli@latest ...`, overridable via `PRISMA_CLI_PACKAGE_RUNNER`/`_NAME`/`_SPEC`/`PRISMA_CLI_BINARY`). The provider's default formatter fallback is `formatPrismaCliCommand(args)` (shell/cli-command.ts:13-18), plain join with a `prisma-cli`-style prefix. - -## Shared: database resolution (id-or-name) - -`resolveDatabase(provider, target, databaseRef, branchName, signal)` (database.ts:912-953): - -- Empty/whitespace ref → `usageError("Database id or name required", "This command needs a database id or name.", "Pass a database id or name.", ["prisma-cli database list"], "database")` (exit 2). -- Calls `provider.listDatabases({projectId: target.project.id, branchName, signal})` and filters `database.id === ref || database.name === ref`. -- 0 matches → `databaseNotFoundError(ref, target.project.name, branchName)` (database.ts:1018-1035): - -``` -code: DATABASE_NOT_FOUND, domain: database, exitCode: 1 -summary: "Database not found" -why: `No database matched "${databaseRef}"${scope}.` // scope = ` in project "${projectName}"` + optional ` on branch "${branchName}"` -fix: "Pass a database id or name from prisma-cli database list." -nextSteps: ["prisma-cli database list"] -``` - -- >1 matches → `databaseAmbiguousError(ref, matches, branchName)` (database.ts:1037-1060): - -``` -code: DATABASE_AMBIGUOUS, domain: database, exitCode: 1 -summary: "Database resolution is ambiguous" -why: branchName ? `Multiple databases matched "${databaseRef}" on branch "${branchName}".` - : `Multiple databases matched "${databaseRef}".` -fix: "Pass the database id, or pass --branch to narrow the match." -nextSteps: ["prisma-cli database list"] -meta: { matches: [{id, name, branchName}, ...] } -``` - -- Exactly 1: `provider.showDatabase(selected.id, {projectId, signal})` then `ensureProjectId(shown ?? selected, target.project.id)` (fills `projectId` if absent, database.ts:955-960). - -## Shared: exact-id confirmation - -`requireExactConfirmation(options)` (database.ts:976-1007). Passes only when `options.confirm === options.id` (strict string equality; undefined ≠ id). Otherwise: - -```ts -throw new CliError({ - code: "CONFIRMATION_REQUIRED", - domain: "database", - summary: options.summary ?? `Confirm ${options.resourceName} removal`, - why: options.why ?? `Removing this ${options.resourceName} is destructive and requires the exact id.`, - fix: `Rerun with --confirm ${options.id}.`, - exitCode: 2, - nextSteps: [options.nextStep ?? `prisma-cli ${options.commandName} ${options.id} --confirm ${options.id}`], - meta: { expectedConfirm: options.id, receivedConfirm: options.confirm ?? null }, -}); -``` - -Note the default nextStep uses a hard-coded `prisma-cli` prefix (not the package-runner formatter); restore and rotate pass explicit `nextStep` strings built with the formatter. - -## Shared: usage date validation (verbatim rules) - -`parseUsageDate(value, flagName, dayBoundary, formatCommand)` (database.ts:613-672): - -- `undefined` → `undefined` (flag omitted = no query param). -- Trimmed value matching `/^\d{4}-\d{2}-\d{2}$/` AND a valid calendar date is expanded: `--from` (dayBoundary "start") → `` `${trimmed}T00:00:00.000Z` ``; `--to` (dayBoundary "end") → `` `${trimmed}T23:59:59.999Z` ``. -- Trimmed value matching `/^\d{4}-\d{2}-\d{2}T/` with `Date.parse` not NaN AND valid calendar date on the first 10 chars → passed through unchanged. -- Calendar validity (database.ts:666-672): `Date.parse(datePart + "T00:00:00.000Z")` must round-trip — `new Date(ts).toISOString().startsWith(datePart)` — so rollovers like `2026-02-30` are rejected. -- Otherwise → `usageError("Invalid usage period", `${flagName} must be an ISO date such as 2026-06-01 or an ISO datetime such as 2026-06-01T12:00:00Z.`, `Pass an ISO date or datetime to ${flagName}.`, [formatCommand(["database","usage","","--from","2026-06-01","--to","2026-06-30"])], "database")` — exit 2. -- Range check (database.ts:374-392): after parsing both, `if (from && to && Date.parse(from) > Date.parse(to))` → `usageError("Invalid usage period", "--from must not be later than --to.", "Pass a --from date that is on or before the --to date.", [same example], "database")`. - -Design intent comment (database.ts:626-632): the Management API validates `startDate`/`endDate` as full ISO datetimes; date-only input is expanded to UTC day boundaries so `--from X --to Y` stays a calendar-day-inclusive range. - -## Shared: backup --limit validation - -`parseBackupLimit(value, formatCommand)` (database.ts:674-703): omitted → `undefined`. Else `Number(value.trim())`; `!Number.isInteger(limit) || limit < 1 || limit > 100` → `usageError("Invalid backup limit", "--limit must be an integer between 1 and 100.", "Pass a --limit between 1 and 100.", [formatCommand(["database","backup","list","","--limit","50"])], "database")` — exit 2. - -## Shared: API error mapping and plan-limit machinery (PR #127) - -All Management-API failures in the real provider funnel through `databaseApiError(options)` (provider.ts:794-865), invoked as `toDatabaseApiError(summary, result.response, result.error, signal)` with a per-operation summary string. - -**Plan-limit branch.** `isPlanLimitApiError(error)` (provider.ts:867-869) is true when the API body has `error.error.code === "planLimitReached"`. Then: - -1. If `workspaceId` is known, fetch `GET /v1/workspaces/{id}/subscription` via `readWorkspaceSubscription` (provider.ts:871-902): best-effort, `SUBSCRIPTION_LOOKUP_TIMEOUT_MS = 3_000` ms timeout via its own AbortController combined with the outer signal (`AbortSignal.any`), every failure path returns `null` (but re-throws user aborts via `signal?.throwIfAborted()`). -2. Build the error (provider.ts:823-848): - -```ts -new CliError({ - code: "PLAN_LIMIT_REACHED", - domain: "database", - summary: "Workspace plan limit reached", - why: "Database operations are blocked because this workspace has used the operations included in its plan. This is a workspace plan limit, not a Prisma outage.", - fix: upgradeUrl - ? `Upgrade the workspace plan at ${upgradeUrl}.` - : "Open Prisma Console and upgrade the affected workspace plan.", - meta: { - workspaceId: options.workspaceId ?? null, - blockedFeature: null, - planName, // subscription?.planName || null - usageBlocked, // subscription?.usageBlocked ?? null - upgradeUrl, // subscription?.upgradeUrl || null - }, - exitCode: 1, - nextSteps: [], - humanLines: [ - "Workspace plan limit reached [PLAN_LIMIT_REACHED]", - "", - "Database operations are blocked because this workspace has used the operations included in its plan. This is a workspace plan limit, not a Prisma outage.", - "", - workspaceLine, // `Workspace: ${workspaceId}` or "Workspace: unavailable" - ...recoveryLines, // optional `Current plan: ${planName}`, then - // `Upgrade: ${upgradeUrl}` or - // "Upgrade: Open Prisma Console and upgrade the affected workspace plan." - ], -}); -``` - -`humanLines` fully replaces the standard human error rendering. `nextActions` is NOT set (empty). Tests: `database-plan-limit.test.ts`. - -**Plan-limit precedence over status-specific mappings.** The special-status branches in `showDatabase` (404→null), `listBackups` (422→`DATABASE_BACKUPS_UNSUPPORTED`), and `restoreDatabase` (409→`DATABASE_RESTORE_CONFLICT`, 404→`DATABASE_BACKUP_NOT_FOUND`) each check `!isPlanLimitApiError(result.error)` first, so a planLimitReached body always wins regardless of HTTP status. - -**API-code passthrough (generic branch, provider.ts:851-864).** When not plan-limit: - -```ts -const status = options.response?.status ?? 0; -return new CliError({ - code: options.error?.error?.code ?? "DATABASE_API_ERROR", - domain: "database", - summary: options.summary, // e.g. "Failed to list databases" - why: options.error?.error?.message ?? `The Management API returned status ${status || "unknown"}.`, - fix: options.error?.error?.hint ?? "Re-run with --trace for the underlying API response details.", - exitCode: 1, - nextSteps: [], -}); -``` - -So the CliError `code` is the raw API `error.code` string when present ("DATABASE_API_ERROR or API-provided code"), and `why`/`fix` prefer the API's `message`/`hint`. - -Per-operation summary strings (provider.ts): `"Failed to list databases"` (257), `"Failed to show database"` (295), `"Failed to create database"` (322), `"Failed to remove database"` (344), `"Failed to list database connections"` (364), `"Failed to create database connection"` (391), `"Failed to remove database connection"` (413), `"Failed to fetch database usage"` (434), `"Failed to list database backups"` (462), `"Failed to restore database"` (509), `"Failed to rotate database connection"` (531). - -## Shared: output channel semantics - -`runCommand` / `writeCommandSuccess` (shell/command-runner.ts:103-144): with `--json`, only the JSON envelope goes to stdout (renderJson output replaces `result`). Human mode: `renderStdout` lines (secrets) go to **stdout**, `renderHuman` + warnings + verbose diagnostics go to **stderr**; with `--quiet` only the renderStdout payload prints. When both exist a blank line is appended to the human block before the stdout payload is written. JSON envelope shape: success `{ok:true, command, result, warnings, nextSteps, nextActions}`, error `{ok:false, command, error:{code,domain,severity,summary,why,fix,where,meta,docsUrl}, warnings, nextSteps, nextActions}` (inventory §3.1). - -`CliError` fields (errors.ts:13-66): `{code, domain, summary, why, fix, debug, where, meta, docsUrl, exitCode (default 1), nextSteps, nextActions, humanLines}`. `usageError(...)` = code `USAGE_ERROR`, exit 2. - -## Shared: default connection name - -`defaultConnectionName()` (database.ts:1009-1016): - -```ts -function defaultConnectionName(): string { - const timestamp = new Date() - .toISOString() - .replace(/[-:.TZ]/g, "") - .slice(0, 17); // YYYYMMDDHHmmssSSS (17 digits) - const suffix = randomBytes(2).toString("hex"); // 4 hex chars - return `cli-${timestamp}-${suffix}`; -} -``` - -Applied in `runDatabaseConnectionCreate` as `name: flags.name?.trim() || defaultConnectionName()` (database.ts:303) — so an all-whitespace `--name` also falls back to the default. - -## Shared: verbose context block - -Every project-scoped result carries `verboseContext` (the `ResolvedProjectTarget`: `{workspace, project, resolution}`). Human renderers append `renderResolvedProjectContextBlock` (presenters/verbose-context.ts:18-32), which only renders under `--verbose` (via `renderVerboseBlock`), titled `"Resolved context"` with rows: workspace, workspace id, project, project id, project source (`--project` | environment | `.prisma/local.json` | platform mapping | created | prompt | unbound), optional target name, optional branch rows. JSON serializers strip it with `stripVerboseContext` (verbose-context.ts:69-74) — the plain result minus `verboseContext`. - -## Shared: provider normalization types - -`DatabaseSummary` (types/database.ts): `{id, name, projectId, branchId: string|null, branchName: string|null, region: string|null, status: string|null, isDefault: boolean|null, createdAt: string|null}`. `normalizeDatabase` (provider.ts:545-565) maps branchName from `branchGitName ?? branchName ?? branch?.gitName ?? branch?.name ?? null` and region from string `region` | `region.id` | `regionId` | null. - -`DatabaseConnectionSummary`: `{id, name, databaseId, createdAt: string|null}`; `normalizeConnection` (provider.ts:567-577) defaults `name` to the connection id. - -`extractConnectionString` (provider.ts:654-664) preference order: `endpoints.pooled.connectionString` → top-level `connectionString` → `endpoints.direct` → `endpoints.accelerate` → null. - -`normalizeUsage` (provider.ts:666-686) defaults: period start/end `""`, operations `{used: 0, unit: "ops"}`, storage `{used: 0, unit: "GiB"}`, generatedAt `""`. - -`normalizeBackupList` (provider.ts:688-702): backups with `backupType ?? "unknown"`, `status ?? "unknown"`, `size ?? null`, `createdAt ?? ""`; `retentionDays` from `meta.backupRetentionDays ?? null`; `hasMore` from `pagination.hasMore ?? false`. - ---- - -## 1. `database list` - -**Registration** (index.ts:90-117): `new Command("list")`, descriptor `database.list`. No positionals. Flags: `--project ` "Project id or name", `--branch ` "Branch git name", + full globals. No renderStdout. - -**Descriptor** (command-meta.ts:390-399): description `"List Prisma Postgres databases for the resolved project"`; examples `"prisma-cli database list"`, `"prisma-cli database list --branch feature/foo"`, `"prisma-cli database list --json"`. - -**Controller** `runDatabaseList(context, {projectRef, branchName})` (database.ts:98-127): `requireDatabaseContext(context, flags, "database list")` → `provider.listDatabases({projectId, branchName, signal})` → `sortDatabases`. Real provider (provider.ts:239-278): paginated `client.GET("/v1/databases", {query: {projectId, branchGitName: branchName, cursor}})`, sequential loop over `pagination.nextCursor` while `hasMore`. - -**Sort** `sortDatabases` (database.ts:962-974): `localeCompare` on `branchName ?? ""`, then `name`, then `id` — branch → name → id ascending. - -**Result** `{command: "database.list", result: {projectId, projectName, branchName: flags.branchName ?? null, verboseContext, databases}, warnings: [], nextSteps: []}`. - -**Human output** `renderDatabaseList` (presenters/database.ts:25-76): title line `` `${strong(label)} ${dim("→")} ${dim("Listing databases for the resolved project.")}` `` then a `│` rail with `project:` and (if set) `branch:` rows; empty case `"No databases found."`; else a column table with headers `Name Branch Region Status Id` — Branch shows `"unscoped"` when null, Region `"unknown"` when null, Status via `formatStatus` = `status ?? (isDefault ? "default" : "unknown")`. - -**JSON** `serializeDatabaseList` (78-96): spread of `serializeList({context: {project, branch?}, items: [{noun: "database", label: name, id, status: isDefault ? "default" : null}]})` — which yields `{context, items: [{name, id, status}], count}` (output/patterns.ts:93-106) — plus `projectId`, `branchName`, `databases` (full summaries). - -**Errors**: auth/workspace/project-resolution shared errors; `PLAN_LIMIT_REACHED` or API passthrough with summary `"Failed to list databases"`. - -## 2. `database show ` - -**Registration** (index.ts:119-148): positional `` "Database id or name" (required); `--project`, `--branch`; globals. - -**Descriptor** (400-408): `"Show database metadata without secret values"`; examples `"prisma-cli database show db_123"`, `"prisma-cli database show acme-preview --branch preview --json"`. - -**Controller** `runDatabaseShow` (database.ts:129-162): shared context (`"database show"`) → `resolveDatabase` → `provider.listConnections(database.id, {signal})`. Real: `GET /v1/databases/{databaseId}` (show; 404 non-plan-limit → `null`, which makes resolveDatabase fall back to the list entry) and `GET /v1/databases/{databaseId}/connections`. - -**Result** `{command: "database.show", result: {projectId, projectName, verboseContext, database, connections}}`. - -**Human** `renderDatabaseShow` (98-131): `renderShow` card titled `"Showing database metadata."` with fields in order: `project`, `database` (name), `id` (dim), `branch` (`"unscoped"` dim when null), `region` (`"unknown"` dim when null), `status` (formatStatus), `connections` (count as string). No secret values. - -**JSON** `serializeDatabaseShow` = `stripVerboseContext(result)` → `{projectId, projectName, database, connections}`. - -**Errors**: shared + `DATABASE_NOT_FOUND` / `DATABASE_AMBIGUOUS`; API summaries `"Failed to show database"`, `"Failed to list database connections"`. - -## 3. `database create ` - -**Registration** (index.ts:150-184): positional `` "Database name" (required); `--region ` "Prisma Postgres region id"; `--project`, `--branch`; globals. HAS `renderStdout`. - -**Descriptor** (409-418): `"Create a Prisma Postgres database and print its one-time connection URL"`; examples `"prisma-cli database create my-db"`, `"prisma-cli database create my-db --branch feature/foo --region eu-central-1"`. - -**Controller** `runDatabaseCreate` (database.ts:164-206): trims name; empty → `usageError("Database name required", "Database create needs a non-empty name.", "Pass a database name.", ["prisma-cli database create "], "database")` (exit 2). Then shared context (`"database create"`) → `provider.createDatabase({projectId, name, branchName, region, signal})`. NOTE: no `resolveDatabase` here; commander requires the positional so only whitespace names hit the usage error. - -**Real provider** (provider.ts:309-333): `POST /v1/databases` body `{projectId, name, source: {type: "empty"}, branchGitName?, region?}`. Response → `normalizeCreatedDatabase` (provider.ts:579-600): takes `connections[0]`; missing → - -``` -code: DATABASE_CONNECTION_MISSING, domain: database, exit 1 -summary: "Created database did not return a connection string" -why: "The Management API created the database but did not include the one-time connection payload." -fix: "Create a connection explicitly with prisma-cli database connection create ." -nextSteps: [`prisma-cli database connection create ${database.id}`] -``` - -then `normalizeCreatedConnection` (see command 9) which can throw `DATABASE_CONNECTION_STRING_MISSING`. - -**Result** `{command: "database.create", result: {projectId, projectName, verboseContext, database (ensureProjectId), connection, connectionString}}`. - -**Output**: `renderDatabaseCreateStdout` (presenters:137-143) returns exactly `[result.connectionString]` — the bare URL, one line, nothing else, on stdout. Human (stderr, presenters:145-167): - -``` -Creating database... - Created database "" in . - The connection URL below is shown once, so save it now. -``` - -(`formatDatabaseTarget` = `branchName ? `${projectName} / ${branchName}` : projectName`.) Under `--verbose`, appends metadata rows: optional `workspace`, `project`, `branch` (or "unscoped"), `database` `name (id)`, `region`, `status`, `connection` `name (id)` (presenters:604-628). - -**JSON** `serializeDatabaseCreate` = `stripVerboseContext` → includes `connectionString` in clear. - -**Errors**: shared; `PLAN_LIMIT_REACHED`; passthrough summary `"Failed to create database"`; `DATABASE_CONNECTION_MISSING`; `DATABASE_CONNECTION_STRING_MISSING`. nextSteps on success: `[]`. - -## 4. `database usage ` - -**Registration** (index.ts:186-225): positional `` "Database id or name"; `--from ` "Start of the usage period"; `--to ` "End of the usage period"; `--project`, `--branch`; globals. - -**Descriptor** (419-427): `"Show usage metrics for a database"`; examples `"prisma-cli database usage db_123"`, `"prisma-cli database usage acme-production --from 2026-06-01 --to 2026-06-30"`. - -**Controller** `runDatabaseUsage` (database.ts:364-426): parses dates FIRST (before auth/context) via `parseUsageDate` + range check (see shared section), then shared context (`"database usage"`) → `resolveDatabase` → `provider.getUsage(database.id, {from, to, signal})`. Real: `GET /v1/databases/{databaseId}/usage` with query `startDate`/`endDate` only when set (provider.ts:421-442). - -**Result** `{command: "database.usage", result: {projectId, projectName, verboseContext, database, period, metrics, generatedAt}}`. - -**Human** `renderDatabaseUsage` (337-375): `renderShow` card titled `"Showing database usage metrics."`, fields: `project`, `database`, `id` (dim), `period` = `` `${start||"unknown"} to ${end||"unknown"}` ``, `operations` = `` `${used} ${unit}` ``, `storage` = `` `${used} ${unit}` ``, `generated` (dim, `||"unknown"`). - -**JSON** `serializeDatabaseUsage` = `stripVerboseContext`. - -**Errors**: `USAGE_ERROR` ("Invalid usage period", 2 variants) exit 2; shared; `DATABASE_NOT_FOUND`/`DATABASE_AMBIGUOUS`; passthrough summary `"Failed to fetch database usage"`. - -## 5. `database restore ` - -**Registration** (index.ts:227-280): positional `` "Target database id or name"; `--backup ` "Backup to restore from"; `--source-database ` "Database the backup belongs to (defaults to the target)"; `--confirm ` "Exact target database id required to restore"; `--project`, `--branch`; globals. No renderStdout. - -**Descriptor** (428-435): `"Restore a database from a backup after exact id confirmation"`; example `"prisma-cli database restore db_123 --backup bkp_456 --confirm db_123"`. - -**Controller** `runDatabaseRestore` (database.ts:471-549), in order: - -1. Missing/blank `--backup` → `usageError("Backup id required", "Database restore needs the backup to restore from.", `Pass --backup from ${formatCommand(["database","backup","list",""])}.`, [that list command], "database")` exit 2. -2. Shared context (`"database restore"`); `resolveDatabase` for target; if `--source-database` set, `resolveDatabase` again for the source (same branch scope), else source = target. -3. Confirmation via `requireExactConfirmation` with overrides — verbatim consent copy: - - summary: `"Confirm database restore"` - - why: `"Restoring immediately and irreversibly overwrites all data in the target database, so it requires the exact target database id."` - - fix: `` `Rerun with --confirm ${database.id}.` `` - - nextStep: `` `${formatCommand(["database","restore",database.id,"--backup",backupId])}${sourceDatabaseArg} --confirm ${database.id}` `` where `sourceDatabaseArg` is `""` when source===target else `` ` --source-database ${sourceDatabase.id}` `` - - exit 2, meta `{expectedConfirm, receivedConfirm}`. (`--confirm` must equal the resolved **target database id**, never the name.) -4. `provider.restoreDatabase({targetDatabaseId, sourceDatabaseId, backupId, projectId, signal})`. - -**Real provider** (provider.ts:472-520): `POST /v1/databases/{targetDatabaseId}/restore` body `{source: {type: "backup", databaseId: sourceDatabaseId, backupId}}`. Status mappings (all skipped when body is planLimitReached): - -- 409 → `restoreConflictError` (provider.ts:776-792): - -``` -code: DATABASE_RESTORE_CONFLICT, domain: database, exit 1 -summary: "Database cannot be restored right now" -why: apiMessage ?? `Database "${targetDatabaseId}" is provisioning or already recovering.` -fix: "Wait for the database to become ready, then retry the restore." -nextSteps: [formatCommand(["database","show",targetDatabaseId])] -``` - -- 404 → `restoreBackupNotFoundError` (provider.ts:752-774) — code comment: target and source were resolved before the call so a 404 identifies the backup: - -``` -code: DATABASE_BACKUP_NOT_FOUND, domain: database, exit 1 -summary: "Database backup not found" -why: apiMessage ?? `No backup matched "${backupId}" for database "${sourceDatabaseId}".` -fix: `Pass a backup id from ${formatCommand(["database","backup","list",sourceDatabaseId])}.` -nextSteps: [that list command] -``` - -(The fixture provider's equivalent `backupNotFoundError` at database.ts:1062-1082 has identical copy without the API-message override.) - -- other errors → passthrough summary `"Failed to restore database"`. - -**Result** `{command: "database.restore", result: {projectId, projectName, verboseContext, database: restored, source: {databaseId: sourceDatabase.id, backupId}}, warnings: [], nextSteps: [formatCommand(["database","show",database.id])]}` — the ONLY database command with a success nextStep. - -**Human** `renderDatabaseRestore` (465-496): `renderMutate` card titled `"Restoring database from backup."`; context rows `project`, `database`, `id` (dim), `backup`, plus `source` row only when source ≠ target; operation `"Restoring database"` count 1; details: - -``` -The restore is running; the database status is "" until it completes. -Connections and credentials are preserved. -``` - -**JSON** `serializeDatabaseRestore` = `stripVerboseContext`. - -## 6. `database remove ` - -**Registration** (index.ts:335-376): positional `` "Database id or name"; `--confirm ` "Exact database id required to remove"; `--project`, `--branch`; globals. - -**Descriptor** (436-441): `"Remove a database after exact id confirmation"`; example `"prisma-cli database remove db_123 --confirm db_123"`. - -**Controller** `runDatabaseRemove` (database.ts:208-247): shared context (`"database remove"`) → `resolveDatabase` → `requireExactConfirmation({resourceName: "database", commandName: "database remove", id: database.id, confirm})` — default copy: summary `"Confirm database removal"`, why `"Removing this database is destructive and requires the exact id."`, fix `` `Rerun with --confirm ${id}.` ``, nextSteps `` [`prisma-cli database remove ${id} --confirm ${id}`] ``, exit 2 → `provider.removeDatabase(database.id, {signal})` (real: `DELETE /v1/databases/{databaseId}`, provider.ts:335-350). - -**Result** `{command: "database.remove", result: {projectId, projectName, verboseContext, database}}` (the pre-removal summary). - -**Human** `renderDatabaseRemove` (173-197): `renderMutate` titled `"Removing database."`; context `project`, `database`, `id` (dim); operation `"Removing database"` count 1; detail `"Database and its connection metadata were removed."`. - -**JSON** `serializeDatabaseRemove` = `stripVerboseContext`. - -**Errors**: shared; `CONFIRMATION_REQUIRED` exit 2; `DATABASE_NOT_FOUND`/`DATABASE_AMBIGUOUS`; passthrough `"Failed to remove database"`. - -## 7. `database backup list ` - -**Registration** (index.ts:295-333, under group `backup` 282-293): positional `` "Database id or name"; `--limit ` "Maximum number of backups to return"; `--project`, `--branch`; globals. - -**Descriptor** (448-456): `"List backups for a database"`; examples `"prisma-cli database backup list db_123"`, `"prisma-cli database backup list acme-production --limit 50"`. - -**Controller** `runDatabaseBackupList` (database.ts:428-469): `parseBackupLimit` FIRST (before auth), then shared context (`"database backup list"`) → `resolveDatabase` → `provider.listBackups(database.id, {limit, signal})`. Real (provider.ts:444-470): `GET /v1/databases/{databaseId}/backups` with `query.limit` only when defined; 422 non-plan-limit → `backupsUnsupportedError(databaseId, result.error)` (provider.ts:735-750): - -``` -code: DATABASE_BACKUPS_UNSUPPORTED, domain: database, exit 1 -summary: "Backups are not available for this database" -why: apiMessage ?? `The platform does not manage backups for database "${databaseId}", for example because it is a remote/BYO database.` -fix: "Use your own backup tooling for externally managed databases." -nextSteps: [] -``` - -else passthrough `"Failed to list database backups"`. - -**Result** `{command: "database.backup.list", result: {projectId, projectName, verboseContext, database, backups, retentionDays, hasMore}}`. - -**Human** `renderDatabaseBackupList` (381-441): title `"Listing platform-created database backups."`; rail rows `database:` and (when retentionDays !== null) `` `retention: ${retentionDays} days` ``; empty `"No backups found."`; table columns `Id Type Status Size Created` (sizes via `formatBackupSize`: `"unknown"` for null, then `B`/`KiB`/`MiB`/`GiB` with one decimal, 1024 boundaries); when `hasMore`: `"More backups exist; raise --limit to see them."` No client-side sorting — API order preserved. - -**JSON** `serializeDatabaseBackupList` (443-463): `serializeList({context: {project, database}, items: [{noun: "backup", label: id, id, status: null}]})` spread + `projectId`, `database`, `backups`, `retentionDays`, `hasMore`. - -## 8. `database connection list ` - -**Registration** (index.ts:432-464): positional `` "Database id or name"; `--project`, `--branch`; globals. - -**Descriptor** (467-475): `"List database connection metadata without secret values"`; examples `"prisma-cli database connection list db_123"`, `"prisma-cli database connection list acme-preview --branch preview --json"`. - -**Controller** `runDatabaseConnectionList` (database.ts:249-282): shared context (`"database connection list"`) → `resolveDatabase` → `provider.listConnections(database.id, {signal})` (real: `GET /v1/databases/{databaseId}/connections`). - -**Result** `{command: "database.connection.list", result: {projectId, projectName, verboseContext, database, connections}}` — structurally identical to `database.show`'s result. - -**Human** `renderDatabaseConnectionList` (203-247): title `"Listing database connection metadata."`; rail row `database:`; empty `"No database connections found."`; table `Name Id Created` (Created `"unknown"` when null). API order preserved. - -**JSON** `serializeDatabaseConnectionList` (249-269): `serializeList({context: {project, database}, items: [{noun: "connection", label: name, id, status: null}]})` spread + `projectId`, `database`, `connections`. - -## 9. `database connection create ` - -**Registration** (index.ts:466-504): positional `` "Database id or name"; `--name ` "Connection name"; `--project`, `--branch`; globals. HAS renderStdout. - -**Descriptor** (476-485): `"Create a database connection and print its one-time connection URL"`; examples `"prisma-cli database connection create db_123"`, `"prisma-cli database connection create db_123 --name readonly"`. - -**Controller** `runDatabaseConnectionCreate` (database.ts:284-320): shared context (`"database connection create"`) → `resolveDatabase` → `provider.createConnection({databaseId: database.id, name: flags.name?.trim() || defaultConnectionName(), signal})`. Real (provider.ts:376-402): `POST /v1/databases/{databaseId}/connections` body `{name}` → `normalizeCreatedConnection` (provider.ts:602-625): missing connection string → - -``` -code: DATABASE_CONNECTION_STRING_MISSING, domain: database, exit 1 -summary: "Created connection did not return a connection string" -why: "Database connection strings are one-time-view secrets, but the Management API did not include one in this create response." -fix: "Create another database connection and store the returned URL immediately." -nextSteps: [`prisma-cli database connection create ${fallbackDatabaseId}`] -``` - -**Result** `{command: "database.connection.create", result: {projectId, projectName, verboseContext, database, connection, connectionString}}`. - -**Output**: `renderDatabaseConnectionCreateStdout` (271-277) = `[result.connectionString]` — bare URL on stdout. Human (279-301): - -``` -Creating connection... - Added a connection to "" in . - The connection URL below is shown once, so save it now. -``` - -Verbose rows (630-652): optional workspace, project, branch, database `name (id)`, connection `name (id)`. - -**JSON** `serializeDatabaseConnectionCreate` = `stripVerboseContext` (includes `connectionString`). - -**Errors**: shared; `DATABASE_NOT_FOUND`/`AMBIGUOUS`; `DATABASE_CONNECTION_STRING_MISSING`; passthrough `"Failed to create database connection"`; `PLAN_LIMIT_REACHED`. - -## 10. `database connection rotate ` - -**Registration** (index.ts:394-430): positional `` "Connection id"; `--confirm ` "Exact connection id required to rotate"; globals only — **no `--project`/`--branch`** (provider-only auth path; the positional must be the connection **id**, no name resolution). HAS renderStdout. - -**Descriptor** (486-494): `"Rotate connection credentials and print the new one-time connection URL"`; example `"prisma-cli database connection rotate conn_123 --confirm conn_123"`. - -**Controller** `runDatabaseConnectionRotate` (database.ts:551-611): - -1. Blank id → `usageError("Connection id required", "Database connection rotation needs a connection id.", "Pass the connection id to rotate.", [formatCommand(["database","connection","rotate","","--confirm",""])], "database")` exit 2. -2. Confirmation BEFORE any API call, with overrides — verbatim consent copy: - - summary: `"Confirm database connection rotation"` - - why: `"Rotating revokes the previous credentials and breaks clients still using them, so it requires the exact connection id."` - - fix: `` `Rerun with --confirm ${connectionId}.` `` - - nextStep: `formatCommand(["database","connection","rotate",connectionId,"--confirm",connectionId])` - - exit 2, meta `{expectedConfirm, receivedConfirm}`. -3. `requireDatabaseProviderOnly(context)` (no workspace requirement, no project resolution) → `provider.rotateConnection(connectionId, {signal})`. - -**Real provider** (provider.ts:522-541): `POST /v1/connections/{id}/rotate` → `normalizeRotatedConnection` (provider.ts:704-733): missing connection string → - -``` -code: DATABASE_CONNECTION_STRING_MISSING, domain: database, exit 1 -summary: "Rotated connection did not return a connection string" -why: "Rotated connection strings are one-time-view secrets, but the Management API did not include one in this rotate response." -fix: "Re-run the rotation, or create a replacement connection and store the returned URL immediately." -nextSteps: [] -``` - -`database` in the record is `{id, name}` only when the response embeds both, else `null`. - -**Result** `{command: "database.connection.rotate", result: {connection, database: {id,name}|null, connectionString}}` — no projectId/projectName/verboseContext. - -**Output**: `renderDatabaseConnectionRotateStdout` (502-508) = `[result.connectionString]`. Human (510-535): - -``` -Rotating connection... - Rotated credentials for <"db name" | connection conn_id>. The previous credentials no longer work. - The connection URL below is shown once, so save it now. -``` - -Verbose rows (543-571): optional database `name (id)`, connection `name (id)`. - -**JSON** `serializeDatabaseConnectionRotate` (537-541) = `result` unchanged (no strip needed). - -**Errors**: `USAGE_ERROR` exit 2; `CONFIRMATION_REQUIRED` exit 2; `DATABASE_CONNECTION_NOT_FOUND` (fixture path only, database.ts:1084-1094: summary `"Database connection not found"`, why `` `No database connection matched "${connectionId}".` ``, fix `"Pass a connection id from prisma-cli database connection list ."`, exit 1, nextSteps `["prisma-cli database connection list "]`); real mode maps an API 404 through the generic passthrough (`"Failed to rotate database connection"` + API code/message); `DATABASE_CONNECTION_STRING_MISSING`. - -## 11. `database connection remove ` - -**Registration** (index.ts:506-540): positional `` "Connection id"; `--confirm ` "Exact connection id required to remove"; globals only — no `--project`/`--branch`. No renderStdout. - -**Descriptor** (495-502): `"Remove a database connection after exact id confirmation"`; example `"prisma-cli database connection remove conn_123 --confirm conn_123"`. - -**Controller** `runDatabaseConnectionRemove` (database.ts:322-362): - -1. Blank id → `usageError("Connection id required", "Database connection removal needs a connection id.", "Pass the connection id to remove.", ["prisma-cli database connection remove --confirm "], "database")` exit 2. (Hard-coded `prisma-cli` example here, unlike rotate which uses the formatter — inconsistency to note.) -2. `requireExactConfirmation({resourceName: "database connection", commandName: "database connection remove", id: connectionId, confirm})` — default copy: summary `"Confirm database connection removal"`, why `"Removing this database connection is destructive and requires the exact id."`, nextSteps `` [`prisma-cli database connection remove ${id} --confirm ${id}`] ``, exit 2. -3. `requireDatabaseProviderOnly(context)` → `provider.removeConnection(connectionId, {signal})` (real: `DELETE /v1/connections/{id}`). - -**Result** `{command: "database.connection.remove", result: {connection: {id: connectionId}}}`. - -**Human** `renderDatabaseConnectionRemove` (309-329): `renderMutate` titled `"Removing database connection."`; context row `connection` = id (dim); operation `"Removing database connection"` count 1; detail `"The connection metadata was removed. Existing one-time secrets were not shown."` No verbose-context block. - -**JSON** `serializeDatabaseConnectionRemove` (331-335) = `{connection: result.connection}`. - -**Errors**: `USAGE_ERROR`, `CONFIRMATION_REQUIRED` (exit 2), fixture `DATABASE_CONNECTION_NOT_FOUND`, real passthrough `"Failed to remove database connection"`. - ---- - -## Provider interface signatures (provider.ts:81-130, verbatim) - -```ts -export interface DatabaseProvider { - listDatabases(options: { projectId: string; branchName?: string; signal?: AbortSignal }): Promise; - showDatabase(databaseId: string, options?: { projectId?: string; signal?: AbortSignal }): Promise; - createDatabase(options: DatabaseCreateInput): Promise; - removeDatabase(databaseId: string, options?: { signal?: AbortSignal }): Promise; - listConnections(databaseId: string, options?: { signal?: AbortSignal }): Promise; - createConnection(options: DatabaseConnectionCreateInput): Promise; - removeConnection(connectionId: string, options?: { signal?: AbortSignal }): Promise; - getUsage(databaseId: string, options?: { from?: string; to?: string; signal?: AbortSignal }): Promise; - listBackups(databaseId: string, options?: { limit?: number; signal?: AbortSignal }): Promise; - restoreDatabase(options: DatabaseRestoreInput): Promise; - rotateConnection(connectionId: string, options?: { signal?: AbortSignal }): Promise; -} -``` - -`DatabaseCreateInput = {projectId, name, branchName?, region?, signal?}`; `DatabaseConnectionCreateInput = {databaseId, name, signal?}`; `DatabaseRestoreInput = {targetDatabaseId, sourceDatabaseId, backupId, projectId, signal?}`. The SDK client (`ManagementApiClient` from `@prisma/management-api-sdk`) is captured in the provider closure by `createManagementDatabaseProvider(client, options?)` — no per-call client argument; controllers never touch the client directly except to construct the provider. - -## Inventory cross-check (§6, code vs doc) - -- Inventory §6: the current shell has **no `postgres` command or alias** — only `database`, described `"Manage Prisma Postgres databases"`. The rename to `postgres` is a pure rename with no alias to preserve. -- Inventory §3.3: `CONFIRMATION_REQUIRED` is exit **2** for database exact-id confirms (matches code) but exit 1 elsewhere (app remove/domain remove) — the port should normalize consent grades. -- Inventory §1 marks all 11 commands: sync, auth `platform+login`, engine kind `result`; create / connection create / connection rotate flagged "secret on stdout"; restore / remove / connection rotate / connection remove flagged "needs --confirm". - -## Discrepancies found (code vs inventory/doc) - -1. Inventory §4 `database list` says auth is "`requireAuthenticatedAuthState` + `requireComputeAuth`" — code actually uses `requireAuthenticatedAuthState` + `authenticatedManagementApiClient` (controllers/database.ts:710, 717); no symbol named `requireComputeAuth` on this path. -2. Inventory §4 `connection rotate` lists `DATABASE_CONNECTION_NOT_FOUND` as its error; in real mode a 404 from `POST /v1/connections/{id}/rotate` maps through the generic passthrough (API code or `DATABASE_API_ERROR`), so `DATABASE_CONNECTION_NOT_FOUND` is only guaranteed in fixture mode. Same for `connection remove`. -3. `requireDatabaseProviderOnly` (rotate/remove connection) does NOT require a workspace (`workspaceRequiredError` is only in `requireDatabaseContext`), and its `workspaceId` may be undefined — plan-limit errors on those two commands then render `"Workspace: unavailable"` with no subscription lookup. -4. Confirmation-error nextSteps are inconsistent: default (`database remove`, `connection remove`) hard-codes a `prisma-cli` prefix; `restore`/`rotate` pass formatter-built commands (`pnpm dlx …` etc.). The `connection remove` blank-id usage example is also hard-coded while `rotate`'s is formatter-built. -5. `databaseNotFoundError` in the fixture provider is thrown without project/branch context (bare `No database matched "…".`), while the resolveDatabase path includes `in project "…" on branch "…"`. -6. `database.show` and `database.connection.list` return structurally identical results but different serializers: show → `stripVerboseContext` (raw shape), connection list → `serializeList` envelope + extras. -7. PLAN_LIMIT_REACHED sets `nextActions: []` despite the inventory's `nextActions` recovery-journey machinery; recovery guidance lives only in `humanLines` and `meta` (PR #127 behavior as shipped). diff --git a/.drive/projects/prisma-cli-v8/specs/s2b-design/facts/facts-d3-bucket-branch-git.md b/.drive/projects/prisma-cli-v8/specs/s2b-design/facts/facts-d3-bucket-branch-git.md deleted file mode 100644 index 119f8a0b..00000000 --- a/.drive/projects/prisma-cli-v8/specs/s2b-design/facts/facts-d3-bucket-branch-git.md +++ /dev/null @@ -1,352 +0,0 @@ -# Verbatim facts: bucket / bucket key / branch list / git connect|disconnect - -All paths relative to `packages/cli/src` unless they start with `.drive/` -or `packages/`, which are relative to the repository root. Extracted from code on branch `claude/prisma-cli-v8-onboarding-30e694` (worktree labeled s2b-resources-work). Grounded against `.drive/projects/prisma-cli-v8/assets/s2/command-inventory.md` (§1 rows 44–46, 59–64; per-command sections at lines 441–463 and 524–547). - ---- - -## Shared sections - -### S1. Global flags - -`shell/global-flags.ts:23-61`. - -- `addGlobalFlags(command)` (leaf commands): `--json` "Emit structured JSON output.", `-q, --quiet` "Reduce human-oriented output.", `-v, --verbose` "Increase human-oriented output detail.", `--trace` "Show deeper diagnostics for failures.", `-y, --yes` "Accept supported confirmation prompts.", `--interactive` "Force interactive behavior when prompts are supported.", `--no-interactive` "Disable interactive behavior and prompts.", `--color` "Force color output in supported terminals.", `--no-color` "Disable color output." -- `addCompactGlobalFlags(command)` (group nodes `bucket`, `bucket key`, `branch`, `git`): `--json`, `-q, --quiet`, `-v, --verbose`, `--trace`, `--no-interactive`, `-y, --yes` only. -- `resolveGlobalFlags` (`global-flags.ts:81-100`) also scans raw argv because Commander v12 can swallow a flag defined at both parent and child level. - -### S2. Command runner contract - -`shell/command-runner.ts:68-165`. - -- `runCommand(runtime, commandName, options, handler, presenter)` where presenter has `renderStdout?`, `renderHuman`, `renderJson?`. -- JSON mode (`--json`): `writeJsonSuccess(output, { ...success, result: presenter.renderJson ? presenter.renderJson(success.result) : success.result })` — **when `renderJson` is absent, the raw controller result object is emitted as `result`** (command-runner.ts:105-116). This is the git connect/disconnect case. -- Quiet mode: only `renderStdout` lines are written. Human mode: `renderHuman` lines + warnings + optional verbose diagnostics on stderr, then `renderStdout` lines on stdout (with a blank separator line if both exist). -- Errors: `CliError` → JSON envelope or human error, `process.exitCode = cliError.exitCode`. SDK `AuthError` → `authRequiredError(["prisma-cli auth login"], { debug })`. Abort → `commandCanceledError()` (code `COMMAND_CANCELED`, exit 130). - -### S3. Auth requirement - -- `requireAuthenticatedAuthState(context)` (`controllers/auth.ts:206`): in real mode, reads auth state; if unauthenticated and `canPrompt(context)` is true it launches the full interactive OAuth login; otherwise throws `authRequiredError()`. -- `authRequiredError` (`shell/errors.ts:101-115`): - ``` - code: "AUTH_REQUIRED", domain: "auth", summary: "Authentication required", - why: "This command needs an authenticated session.", - fix: "Run prisma-cli auth login, or rerun the command in a TTY to sign in interactively.", - exitCode: 1, nextSteps default ["prisma-cli auth login"] - ``` -- `workspaceRequiredError` (`shell/errors.ts:141-149`) = `usageError(...)`: - ``` - code: "USAGE_ERROR", domain: "auth", exitCode: 2, - summary: "Workspace required", - why: "This command needs an active workspace, but the authenticated session does not have one.", - fix: "Run prisma-cli auth login and choose a workspace.", - nextSteps: ["prisma-cli auth login"] - ``` -- `canPrompt(context)` (`shell/runtime.ts:93-108`): false if `flags.json`; false if `flags.interactive === false`; false if `env.CI` set and `flags.interactive !== true`; else `Boolean(stdin.isTTY && stderr.isTTY)`. - -### S4. Real mode vs fixture mode - -`isRealMode(context)` is duplicated per controller (`controllers/bucket.ts:57-62`, `controllers/branch.ts:25-30`, project.ts): real mode = `!context.runtime.fixturePath && !context.runtime.env.PRISMA_CLI_MOCK_FIXTURE_PATH`. - -### S5. Bucket provider construction - -- Real mode: `authenticatedManagementApiClient(context.runtime.env, context.runtime.signal)` (from `../auth`); `null` client → `throw authRequiredError()`. Then `createManagementBucketProvider(client)` (`lib/bucket/provider.ts:83-268`), which takes a `ManagementApiClient` from `@prisma/management-api-sdk`. -- Fixture mode: `createFixtureBucketProvider(context)` (`controllers/bucket.ts:349-410`) backed by `context.api` (mock API) — throws `BUCKET_NOT_FOUND` / `BUCKET_KEY_NOT_FOUND` / `BRANCH_NOT_FOUND` when mock lookups fail. -- `BucketProvider` interface (`lib/bucket/provider.ts:29-50`): - ```ts - listBuckets(options: { projectId: string; branchName?: string; signal?: AbortSignal }): Promise; - createBucket(options: BucketCreateInput): Promise; // { projectId, name?, branchGitName?, signal? } - deleteBucket(bucketId: string, options?: { signal? }): Promise; - listKeys(bucketId: string, options?: { signal? }): Promise; - createKey(options: BucketKeyCreateInput): Promise; // { bucketId, name?, role: "read" | "read_write", signal? } - deleteKey(bucketId: string, keyId: string, options?: { signal? }): Promise; - ``` - `BucketKeyCreateRecord = { key: BucketKeySummary; secretAccessKey: string; accessKeyId: string; endpoint: string; bucketName: string }`. -- Management provider endpoints: `GET /v1/buckets` (query `projectId`, `branchGitName`, `cursor`; cursor-paginated to exhaustion, loop breaks when `!pagination.hasMore || !pagination.nextCursor`), `POST /v1/buckets` (body `{ projectId, name?, branchGitName? }`), `DELETE /v1/buckets/{bucketId}`, `GET /v1/buckets/{bucketId}/keys` (cursor-paginated), `POST /v1/buckets/{bucketId}/keys` (body `{ role, name? }`), `DELETE /v1/buckets/{bucketId}/keys/{keyId}`. -- `normalizeBucket` (`provider.ts:270-278`) → `{ id, name, status, branchId, createdAt }`; `normalizeKey` (`provider.ts:280-288`) → `{ id, name, role, valueHint, createdAt }`. -- `bucketApiError(summary, response, error)` (`provider.ts:290-309`): API error passthrough — - ``` - code: error?.error?.code ?? "BUCKET_API_ERROR", domain: "bucket", summary: , - why: error?.error?.message ?? `The Management API returned status ${status || "unknown"}.`, - fix: error?.error?.hint ?? "Re-run with --trace for the underlying API response details.", - exitCode: 1, nextSteps: [] - ``` - Caller summaries: `"Failed to list buckets"`, `"Failed to create bucket"`, `"Failed to delete bucket"`, `"Failed to list bucket keys"`, `"Failed to create bucket key"`, `"Failed to delete bucket key"`. - -### S6. --project/--branch resolution for bucket list/create - -`requireBucketContext(context, flags, commandName)` (`controllers/bucket.ts:287-340`), commandName is `"bucket list"` / `"bucket create"`: -1. `requireAuthenticatedAuthState(context)`; missing `authState.workspace` → `workspaceRequiredError()`. -2. Real mode: `authenticatedManagementApiClient(...)`; null → `authRequiredError()`. -3. `resolveProjectTarget({ context, workspace, explicitProject: flags.projectRef, listProjects, commandName })` (`lib/project/resolution.ts:155-183`). `listProjects` = `listRealWorkspaceProjects(client, workspace, signal)` (`controllers/project.ts:1438-1466`, does `GET /v1/projects` and filters by `workspace.id`) or `listFixtureWorkspaceProjects(context, workspace)` (`project.ts:1468-1481`). -4. Errors converted via `projectResolutionErrorToCliError` (`resolution.ts:352-375`). - -Resolution order inside `resolveProjectTarget` (`resolution.ts:590-676`): explicit `--project` (match by exact `id` or `name`; 1 match ok, >1 → `PROJECT_AMBIGUOUS`, 0 → `PROJECT_NOT_FOUND`) → `envProjectId` (NOT passed by any of these commands, so inert) → local pin `.prisma/local.json` (workspace mismatch → `LOCAL_PROJECT_WORKSPACE_MISMATCH`; pinned project missing → `LOCAL_STATE_STALE`) → durable platform mapping (`resolveDurablePlatformMapping()` currently returns `null`, `resolution.ts:586-588`) → `PROJECT_SETUP_REQUIRED`. - -Resolution-family CliErrors (`resolution.ts`), all `domain: "project"`, exit 1: -- `PROJECT_NOT_FOUND`: summary `"Project not found"`, why `` `The project "${projectRef}" does not exist in workspace "${workspace.name}" or is not accessible.` ``, fix `"Pass a project id or name from prisma-cli project list."`, nextSteps `["prisma-cli project list"]`. -- `PROJECT_AMBIGUOUS`: summary `"Project resolution is ambiguous"`, why `` `Multiple projects matched "${projectRef}".` `` (or `"Multiple projects matched the current directory context."`), fix `"Pass --project to choose the project explicitly."`, meta `{ matches: [{id,name},...] }`, nextSteps `["prisma-cli project list", "prisma-cli app deploy --project "]` (note: the second next step hardcodes `app deploy`, resolution.ts:265). -- `PROJECT_SETUP_REQUIRED`: summary `"Choose a Project before running this command"`, why = `` `This directory is not linked to a Prisma Project, and ${commandLabel} will not choose one from package or directory names.` `` where commandLabel is `` `prisma-cli ${commandName}` `` or `"this command"` when commandName omitted; fix `"Link the directory to an existing Project, or pass --project for this command."`; meta = spread of `ProjectSetupSuggestion` (`suggestedProjectName`, `suggestedProjectNameSource` (`"package-name"` from package.json name if it matches `/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/`, else `"directory-name"`), `candidates`, `recoveryCommands`); nextSteps `["prisma-cli project list", "prisma-cli project link ", "prisma-cli --project "]` (last only when commandName present); plus `nextActions` from `buildProjectSetupNextActions` (`resolution.ts:431-494`): user-choice "Ask the user whether to link an existing Project or create a new one", run-command "Link the chosen Project", optional run-command "Create and link a new Project" (`prisma-cli project create `), and run-command "Retry with an explicit Project" when commandName present. -- `LOCAL_STATE_STALE`: summary `"Local project binding is stale"`, why `` `The target recorded in ${LOCAL_RESOLUTION_PIN_RELATIVE_PATH} is no longer available in the selected workspace.` ``, fix `` `Delete ${LOCAL_RESOLUTION_PIN_RELATIVE_PATH}, then choose a Project explicitly.` ``, meta `{ pinPath }`, nextSteps `["prisma-cli project list", "prisma-cli project link "]`. -- `LOCAL_PROJECT_WORKSPACE_MISMATCH`: summary `"Project link uses another workspace"`, why `` `${pin path} links this directory to project ${pinnedProjectId} in workspace ${pinnedWorkspaceId}, but your current CLI session is workspace "${activeWorkspace.name}" (${activeWorkspace.id}).` ``, fix `"Switch to the linked workspace, or relink this directory to a project in the current workspace."`, meta `{ pinPath, pinnedWorkspaceId, pinnedProjectId, activeWorkspaceId, activeWorkspaceName }`, nextSteps `["prisma-cli auth workspace use ", "prisma-cli project list", "prisma-cli project link "]`. - -### S7. requireBucketProviderOnly (bucket delete, all bucket key commands) - -`controllers/bucket.ts:342-347`: `await requireAuthenticatedAuthState(context); return resolveBucketProvider(context);` — **no workspace check, no project resolution**. These commands operate purely by bucket id. (`resolveBucketProvider`, bucket.ts:271-285, is the client/provider construction of S5.) - -### S8. Result types - -`types/bucket.ts`: `BucketSummary { id, name, status, branchId: string | null, createdAt }`; `BucketKeySummary { id, name, role: "read"|"read_write", valueHint, createdAt }`; `BucketListResult { projectId, projectName, branchName: string | null, verboseContext?, buckets }`; `BucketCreateResult { projectId, projectName, verboseContext?, bucket }`; `BucketDeleteResult { bucket: { id } }`; `BucketKeyListResult { bucketId, keys }`; `BucketKeyCreateResult { bucketId, key, secretAccessKey, accessKeyId, endpoint, bucketName }`; `BucketKeyDeleteResult { key: { id } }`. - -`types/branch.ts`: `BranchRole = "preview" | "production"`; `BranchSummary { id, name, role: BranchRole, envMap: BranchRole }`; `BranchListResult { projectId, projectName, verboseContext?: { workspace, project, resolution }, branches }`. - -`types/project.ts:108-137`: `GitRepositoryConnection { id: string|null, provider: "github", repoId: number|null, repository: { owner, name, fullName, url }, defaultBranch: string|null, isPrivate: boolean|null, status: "pending"|"active"|"archived", installation: { id: string|null, status: "pending"|"connected" }, automation: { branches: boolean, pullRequests: boolean, comments: boolean }, connectedAt: string|null, updatedAt: string|null }`; `ProjectRepositoryConnectionResult extends BoundProjectShowResult { repositoryConnection }` where `BoundProjectShowResult = { workspace: AuthWorkspace, project: ProjectSummary, resolution: ProjectResolution }`. - -### S9. nextSteps on success - -Every one of the 9 commands returns `warnings: []` and `nextSteps: []` on success (controllers/bucket.ts:88-90, 116-118, 160-162, 191-193, 233-235, 265-267; controllers/branch.ts:45-47, 55-57; controllers/project.ts:1156-1158, 1197-1199, 1246-1248, 1313-1315, 1340-1342). No `nextActions` on any success path. - ---- - -## bucket (group) - -- Registration: `createBucketCommand` (`commands/bucket/index.ts:42-56`), descriptor id `"bucket"`, compact global flags, subcommands `list`, `create`, `delete`, `key`. -- Descriptor (`shell/command-meta.ts:228-237`): path `["prisma","bucket"]`, description `"Manage object-store buckets for a project"`, examples `["prisma-cli bucket list", "prisma-cli bucket create", "prisma-cli bucket key create bkt_123"]`. -- `bucket key` group descriptor (`command-meta.ts:264-273`): description `"Manage access keys for an object-store bucket"`, examples `["prisma-cli bucket key list bkt_123", "prisma-cli bucket key create bkt_123", "prisma-cli bucket key delete bkt_123 bkey_456"]`. - -## bucket list - -1. **Registration** (`commands/bucket/index.ts:64-91`): `prisma bucket list`. No positionals. Flags via `addProjectAndBranchOptions` (index.ts:58-62): `--project ` "Project id or name", `--branch ` "Branch git name"; plus full globals. No defaults, no choices. -2. **Descriptor** (`command-meta.ts:238-247`): description `"List object-store buckets for the resolved project"`, examples `["prisma-cli bucket list", "prisma-cli bucket list --branch preview", "prisma-cli bucket list --json"]`. -3. **Controller** `runBucketList(context, { projectRef, branchName })` (`controllers/bucket.ts:64-91`): `requireBucketContext(context, flags, "bucket list")` (S6) → `provider.listBuckets({ projectId: target.project.id, branchName: flags.branchName, signal })`. Note `--branch` is passed straight through as API query `branchGitName`; no client-side branch validation for list. -4. **Errors**: AUTH_REQUIRED / USAGE_ERROR(workspace) / resolution family (S6) / `BUCKET_API_ERROR`-or-API-code with summary `"Failed to list buckets"` (S5). -5. **Output**: `renderBucketList` (`presenters/bucket.ts:19-70`): header `({ brief, placeholder?, alias?, default? }) : FlagSpec -flag.requiredString({ brief, placeholder?, alias? }) : FlagSpec -flag.number({ brief, placeholder?, alias?, default? }) : FlagSpec -flag.boolean({ brief, alias? }) : FlagSpec -flag.enum({ brief, values, alias?, default? }) : FlagSpec -flag.repeated({ brief, placeholder?, alias? }) : FlagSpec -positional.string({ brief, placeholder }) : PositionalSpec -positional.optionalString({ brief, placeholder }) : PositionalSpec -positional.variadic({ brief, placeholder }) : PositionalSpec // at most one, declared last -``` -Alias is single-char (type-enforced `Char`). Declared camelCase flag keys become `--kebab-case`. Reserved shared family (engine-injected): `--format/--json, --log-level/--verbose, --quiet, --yes, --interactive, --color`. - -### ok / notOk / CliStructuredError (packages/cli-engine/src/protocol.ts) - -`ok(value): Ok`, `notOk(failure): NotOk`, `okVoid()`. Ok/NotOk are frozen class instances with `assertOk()`/`assertNotOk()`. - -`CliStructuredError` is a **class** (protocol.ts:53), constructed with `new`: -```ts -new CliStructuredError( - code: `${string}.${string}`, // dotted NAMESPACE.SUBCODE - summary: string, - options?: { - severity?: "error" | "warn" | "info"; // default "error" - why?: string; - nextActions?: readonly NextAction[]; // default [] - where?: { path?: string; line?: number }; - meta?: Record; - docsUrl?: string; - cause?: unknown; - }) -``` -Members: `.toEnvelope(): CliErrorEnvelope` and static `CliStructuredError.is(error)` (duck-typed guard by `name === "CliStructuredError"`). - -`NextAction` (protocol.ts:26): -```ts -export interface NextAction { - readonly kind: "run-command" | "user-choice" | "edit-file" | "done"; - readonly label: string; - readonly command?: string; - readonly commands?: readonly string[]; - readonly reason?: string; -} -``` - -### CommandContext (packages/cli-engine/src/context.ts:14-95) - -Fields: `config: TConfig`; `present(outcome, presentations): PresentedResult` (the only PresentedResult constructor; `Outcome` = `{ data, diagnostics? }` plus `exitCode: TCode | 0` iff exitCodes documented); `getCredentials(): Promise` (`Credentials = { readonly token: string }`); **`api: ManagementApiClient` IS present** (lazy getter, see §6); `report(event: EngineEvent): void`; `prompt: PromptSurface`; `signal: AbortSignal`; `cwd: string`; `env: Readonly>`; `requireDependency(specifier): Promise>`. - -### needs.credentials (packages/cli-engine/src/execution/needs.ts:104-153) - -`checkNeeds` order: interaction → dependencies → credentials → config section. `checkCredentials` calls `runtime.getCredentials()`: -- throw → `CLI.CREDENTIALS_UNREADABLE`, summary "The stored credentials could not be read.", why = first line of the cause message, one user-choice action "Sign in again to replace the stored credentials, then run the command again." -- `undefined` → `credentialsRequiredError()` (needs.ts:138-152), the single sign-in error shared with ctx.api: -```ts -new CliStructuredError("CLI.CREDENTIALS_REQUIRED", "You must be signed in to run this command.", - { nextActions: [{ kind: "user-choice", label: "Sign in, then run the command again." }] }) -``` -All needs failures settle as errored envelopes, **exit 2** (verified byte-exact in v8-whoami.test.ts:243-253). Other needs errors: `CLI.INTERACTION_REQUIRED` (needs.ts:70), `CLI.MISSING_DEPENDENCY` (needs.ts:253, with `run-command` install action when the package manager is known and `meta: { specifier, installCommand? }`), `CLI.CONFIG_INVALID` (needs.ts:194). - -### Prompt surface + structural failure codes (packages/cli-engine/src/execution/prompts.ts) - -`PromptSurface`: `confirm(question, { default? })`, `consent(question)` (no default parameter — structurally undefaultable), `select(question, options: {value,label}[], { default? })`, `text(question, { placeholder?, default? })`. Prompt UI writes to **stderr**. - -Semantics: under `--yes` or non-interactive (no TTY stdin, CI, `--no-interactive`) a prompt with a default resolves to it silently; without a default it throws. Failure errors (all engine-settled): -- `CLI.PROMPT_REQUIRED` (prompts.ts:73) — no default, --yes or non-interactive. **Exit 2.** -- `CLI.CONSENT_REQUIRED` (prompts.ts:98) — consent under --yes/non-interactive; --yes can never grant. **Exit 2.** -- `CLI.PROMPT_INVALID` (prompts.ts:120) — unparseable answer, `"${raw}" is not a valid answer to "${question}".` **Exit 2.** -- `CLI.PROMPT_CANCELLED` (prompts.ts:66) — EOF/cancel at the prompt. **Exit 3.** -Scripted answers (test harness `answers`) bypass clack; prompting past the script throws a harness Error (test failure). - -### Events (packages/cli-engine/src/events.ts) - -`EngineEvent` kinds: `step-started` (`step`, `id?`, `parentId?`), `step-finished` (`step`, `outcome: "ok"|"failed"|"skipped"|"warning"`), `progress` (`completed`, `total?`), `message` (`severity: "warn"|"info"|"verbose"`, `text`), `output` (`source`, `channel: "data"|"diagnostic"`, `line`), `remediation` (`action: NextAction`), `endpoint` (`name`, `url`), `status` (`subject`, `status`, `from?`), `artifact` (`path`, `description?`). Every kind takes optional `data?: unknown` passthrough. Emitted via `ctx.report(...)`; rendered in human mode, framed as `StreamEvent` lines (`EngineEvent & { commandId, timestamp }`) in json mode; never aggregated into the envelope. - -### Blocks + sensitive fields (packages/cli-engine/src/presentation.ts:78-105) - -```ts -type Block = - | { kind: "summary"; tone: "ok" | "error" | "warn" | "info"; text: string } - | { kind: "fields"; rows: ReadonlyArray<{ label: string; value: string; sensitive?: boolean }> } - | { kind: "table"; columns: readonly string[]; rows: ReadonlyArray } - | { kind: "list"; items: readonly string[] } - | { kind: "tree"; roots: readonly TreeNode[] }; -``` -`sensitive?: boolean` exists **only on fields rows**. `Presentations = { human: (ui: Ui) => Block[]; stdout?: () => string[]; json?: () => unknown; next?: () => NextAction[] }` — only the active format's functions run, at the return site (`command-context.ts:36-56`): json mode materializes `json` + `next`; human mode materializes `human` + `stdout` + `next`. When `json()` is absent the envelope `result` falls back to `data`. Human blocks + next + diagnostics render to stderr; `stdout()` lines are the only stdout writes (pipe-clean). - -Guard in `ctx.present` (command-context.ts:74-83): a severity-"error" diagnostic with exitCode 0 throws (bug). - ---- - -## 3. createTestCli — exact spec on this branch (packages/cli-engine/src/testing.ts:69-90) - -```ts -export function createTestCli(spec: { - readonly commandFamilies?: readonly CommandFamily[]; - readonly commands: MountedTree; // required - readonly groups?: Readonly>; - readonly config?: Readonly>; // runtime.config.sections - readonly credentials?: Credentials; // runtime.getCredentials resolves this (undefined = signed out) - readonly managementApi?: { readonly baseUrl?: string; readonly client?: ManagementApiClient }; - // baseUrl defaults to "https://test.invalid"; when client is supplied, ctx.api IS that object - readonly packageManager?: "npm" | "pnpm" | "yarn" | "bun" | "unknown"; - readonly now?: () => Date; // fixed clock for stream timestamps -}): TestCli -``` - -`TestCli.run(argv, opts?)` (testing.ts:11-58) opts: -```ts -{ stdin?: string; answers?: ReadonlyArray; abort?: AbortSignal; - onEvent?: (event: EngineEvent) => void; onSettled?: (summary: RunSummary) => void; - cwd?: string; isTty?: { stdin?: boolean; stdout?: boolean; stderr?: boolean }; - env?: Readonly> } -``` -Result: -```ts -{ exitCode: number; stdout: string; stderr: string; - json: readonly StreamEvent[]; // parsed json-mode frames incl. terminal result - events: readonly EngineEvent[]; // every reported event - presented: PresentedResult | undefined } // the handler's returned PresentedResult -``` -Engine name in the harness is `"prisma-test"`, version `"0.0.0"`. Defaults: cwd `"/"`, env `{}`, all isTty false. `runtime.exit` throws. The `answers` script feeds prompts in order; prompting past it fails the test. - ---- - -## 4. The v8 test pattern - -### packages/cli/tests/v8-auth.test.ts (family suite) - -- **Mock the operations module wholesale, keep the rest real**: - ```ts - vi.mock("../src/auth", async (importOriginal) => ({ - ...(await importOriginal()), - performLogin: vi.fn(), performLogout: vi.fn(), readAuthState: vi.fn(), - listAuthWorkspaces: vi.fn(), switchAuthWorkspace: vi.fn(), logoutAuthWorkspace: vi.fn(), - })); - ``` - Real error classes (`EmptyServiceTokenError`) and legacy error constructors (`workspaceAmbiguousError` from `../src/shell/errors`) are imported to script rejections. -- **Shared fixture constants** at module top (`SIGNED_OUT`, `SIGNED_IN`, `TWO_OAUTH_WORKSPACES`, `MIXED_SOURCES`, `EMPTY_LIST`). -- **`makeCli()`** builds `createTestCli({ commands: {: command,...}, groups: {...}, now: () => new Date(0) })` — commands mounted flat by path, mirroring `cli.ts`; no commandFamilies in tests. -- **Result-frame extraction helper** (v8-auth.test.ts:135-141): - ```ts - function resultFrame(frames: ReadonlyArray<{ kind: string }>) { - const frame = frames.at(-1); - if (frame === undefined || frame.kind !== "result") throw new Error("expected a terminal result frame"); - return frame as Extract; - } - ``` -- **Envelope assertions**: run with `--json`, `resultFrame(result.json)`, then `expect(frame.envelope).toMatchObject({ ok, commandId: "auth.login", error: { code, summary, why?, meta? } })` or `result` for completed. Human-mode tests assert `result.stderr` `toContain` for cards/next-action lines (`"→ Sign in: prisma-cli auth login\n"`) and assert semantically via `result.presented?.presentation.stdout / .next / .human.find(b => b.kind === "table")`. -- **Events**: `expect(result.events).toEqual([...])` for the step/endpoint sequence. -- **Prompts scripted** with `answers: ["ws_2"]` + `isTty: { stdin: true, stdout: true }`; invalid answers assert `[CLI.PROMPT_INVALID]` exit 2; non-interactive (`isTty.stdin: false`) asserts `[CLI.PROMPT_REQUIRED]` exit 2. -- **Filesystem-dependent behavior** (agent tip) uses `mkdtemp` temp cwd + `env: { PRISMA_CLI_STATE_DIR }`. -- `beforeEach` resets every mock. -- Definition-shape assertion: `expect(authLoginCommand.needs.credentials).toBe(false)`. - -### packages/cli/tests/v8-whoami.test.ts (S1 byte-pin baseline) - -Same mock pattern (only `readAuthState`). Additionally: -- Byte-exact stderr/stdout pins for whoami (`expect(result.stderr).toBe("ℹ ...\n...")`), including the full json line for the envelope with fixed clock `T0 = "1970-01-01T00:00:00.000Z"`. -- **Unauthenticated path pattern** (v8-whoami.test.ts:31-44, 230-266): a local `requiresCredentials = defineCommand({ needs: { credentials: true }, ... })` mounted as `"auth locked"`, run with/without `createTestCli({ credentials: { token: "tok_1" } })`; asserts the byte-exact `CLI.CREDENTIALS_REQUIRED` rendering (exit 2) and success when credentials present. -- ctx.env passthrough test: asserts the mock was called with the exact `env` object passed to `run`. - -### packages/cli/tests/v8-golden-rendering.test.ts - -Header comment states the rule: byte-exact pins live ONLY here, **one representative per rendering surface** — currently: human card (`auth logout`), table (`auth workspace list`), error (`AUTH.WORKSPACE_AMBIGUOUS` via `auth workspace logout`). Every other v8 test asserts semantically. New commands are added here **only** if they introduce a new rendering surface; otherwise nothing is added — when the engine's rendering style changes, this is the one file re-pinned. Structure is the same makeCli/mocks pattern with `now: () => new Date(0)`. - ---- - -## 5. Mount map + family map (packages/cli/src/v8/cli.ts) - -`buildCli()` calls `createCli({ name: "prisma-v8", version: getCliVersion(), commandFamilies: [defineCommandFamily({ commands: { login: authLoginCommand, logout: ..., whoami: ..., workspaceList: ..., workspaceUse: ..., workspaceLogout: ... } })], groups: { auth: {...}, "auth workspace": {...}, telemetry: {...} }, commands: { "auth login": authLoginCommand, ..., "telemetry status": telemetryStatusCommand, ... } })`. - -Notes: -- One `defineCommandFamily` for auth (family keys are camelCase names, mount keys are space-separated paths referencing the **same object identities**). Telemetry commands are mounted with **no family** ("Shell-owned consent surface"). -- `defineCommandFamily` spec (command-family.ts:21-31): `{ configSection?, commands: Record, docsBaseUrl? }`. Family membership is **object identity**: `commandFamilyOf` (execution/command-tree.ts:136-143) finds the family whose `commands` record `.includes(def)`. Unowned (harness/shell) commands are allowed. -- Construction-time validation (command-tree.ts): path collisions, unknown groups (every path prefix must be a declared group), reserved-flag/grammar violations, exit-code range, **section ownership** (a command whose `needs.config` is not its family's section fails construction). There is **NO check that every family command is mounted, or every mounted command belongs to a family**. -- **No family-map-vs-mount-map coverage test exists yet.** `v8-bin.test.ts:235-237` only asserts `expect(() => buildCli()).not.toThrow()` plus --help/--version through the real tree. A coverage test could be written from exported values: iterate `commandFamilies[i].commands` values and assert each appears in `Object.values(spec.commands)` (and vice versa) — but `buildCli` currently returns the constructed `Cli`, not the spec, so the test would either need the spec exported separately or reconstruct the same records. - ---- - -## 6. Credentials / managementApi wiring and ctx.api - -**Bin side** (packages/cli/src/v8/runtime.ts:41-76 `assembleRuntime(proc: HostProcess)`): builds `Runtime` with `getCredentials: makeGetCredentials(proc.env)` and `managementApi: { baseUrl: getApiBaseUrl(proc.env) }`, both from `../auth`. `makeGetCredentials` behavior (pinned in v8-bin.test.ts:140-163): non-empty `PRISMA_SERVICE_TOKEN` wins (trimmed); blank-but-set token **throws** ("PRISMA_SERVICE_TOKEN is set but empty") rather than falling back; otherwise stored tokens. `getApiBaseUrl` default `https://api.prisma.io`, overridden by `PRISMA_MANAGEMENT_API_URL`. Also `detectPackageManager(env)` from `npm_config_user_agent`, `makeOnSignal(proc)`, `config: await loadConfig(proc.cwd())`. - -**main.ts** (packages/cli/src/v8/main.ts): `main(proc, buildCliForRun = buildCli)` — construction error → one stderr line, return 1; `maybeWriteCachedUpdateNotification` before dispatch; `assembleRuntime`; `resolveTelemetryHooks(proc)` (returns `CliRunHooks | undefined`, `{ onSettled }` spawning the detached sender; any throw → hooks undefined); `cli.run(proc.argv.slice(2), runtime, hooks)`. - -**Engine side** — ctx.api (execution/command-context.ts:90-96): lazy getter, once per run: -```ts -get api(): ManagementApiClient { - api ??= invocation.hooks.managementApi?.client ?? buildManagementApiClient(invocation); - return api; -} -``` -`buildManagementApiClient` (execution/api-client.ts:28-60): returns a Proxy; the SDK module is dynamically imported only on first actual method call. `constructClient` calls `createSdk({ clientId: "", redirectUri: "", tokenStorage: { getTokens: async () => { const credentials = await invocation.runtime.getCredentials(); if (credentials === undefined) throw credentialsRequiredError(); return { workspaceId: "", accessToken: credentials.token }; }, setTokens: async () => {}, clearTokens: async () => {} }, apiBaseUrl: invocation.runtime.managementApi.baseUrl })` — token read **per request**, so mid-run refresh is picked up. `restoreStructuredThrow` unwraps the SDK's FetchError cause chain: a `CliStructuredError` in the chain is rethrown as itself; an SDK `AuthError` (matched by `name === "AuthError"`, never instanceof) maps to `credentialsRequiredError()`. - -`CLI.CREDENTIALS_REQUIRED` verbatim (execution/needs.ts:141-151): code `"CLI.CREDENTIALS_REQUIRED"`, summary `"You must be signed in to run this command."`, nextActions `[{ kind: "user-choice", label: "Sign in, then run the command again." }]`. Byte rendering (pinned): `✘ [CLI.CREDENTIALS_REQUIRED] You must be signed in to run this command.\n→ Sign in, then run the command again.\n`, exit 2. (The mark was `✖` when this was surveyed; the engine-colour slice moved every failure surface onto `✘`, the one the style guide and `docs/product/output-conventions.md` already specified.) - ---- - -## 7. CLI_NAME / cli-name.ts - -packages/cli/src/cli-name.ts: -```ts -export const CLI_NAME = "prisma-cli"; -export const CLI_DOCS_URL = "https://www.prisma.io/docs/orm/tools/prisma-cli"; -``` -Every nextAction `command` string is template-built from it: `` `${CLI_NAME} auth login` ``, `` `${CLI_NAME} auth whoami` ``, `` `${CLI_NAME} project list` ``, `` `${CLI_NAME} auth workspace list` ``, `` `${CLI_NAME} auth workspace use ` `` (placeholder args in angle brackets). Prose inside `why`/`label` also interpolates it (e.g. `` `Run ${CLI_NAME} auth login and authorize a workspace.` ``, `` `Pass a workspace from ${CLI_NAME} auth workspace list.` ``). Note: the engine's `{bin}` substitution applies only to help examples; nextActions use CLI_NAME directly. - ---- - -## 8. Legacy CliError (packages/cli/src/shell/errors.ts) - -```ts -export type ErrorDomain = "cli" | "auth" | "project" | "branch" | "app" | "database" | "bucket"; - -export interface CliErrorOptions { - code: string; domain: ErrorDomain; summary: string; - why: string | null; fix: string | null; - debug?: string | null; where?: string | null; - meta?: Record; docsUrl?: string | null; - exitCode?: number; // default 1 - nextSteps?: string[]; nextActions?: NextAction[]; // NextAction from src/shell/next-actions - humanLines?: string[]; -} -export class CliError extends Error { /* all of the above as readonly fields; severity: "error"; name "CliError" */ } -``` -Constructors in the file: `usageError(summary, why, fix, nextSteps?, domain?)` (code `USAGE_ERROR`, exit 2), `authRequiredError`, `authConfigInvalidError`, `commandCanceledError` (exit 130), `workspaceRequiredError`, `featureUnavailableError`; auth-specific ones re-exported from `src/auth/errors.ts` (`workspaceAmbiguousError`, `workspaceNotAuthenticatedError`, `workspaceSwitchUnavailableError`). - -The only existing v8 mapping helper is `mapAuthOperationError` in `packages/cli/src/v8/auth/errors.ts` (§1) — per-family map table, not generic. Fields dropped in mapping: `domain` (folded into the dotted namespace), `debug`, `where` (legacy is a string; structured `where` is `{path?,line?}`), `exitCode`, `nextSteps`, `nextActions`, `humanLines`. Legacy `fix` → one `user-choice` nextAction. - ---- - -## 9. Legacy fixture-test files for project/database/bucket/branch/git (packages/cli/tests/) - -Resource-command test files and whether they reference `fixturePath` (helpers.ts:51,75,100,130 defines it in the test-shell builders): - -| File | fixturePath? | -|---|---| -| project.test.ts | yes | -| project-mutations.test.ts | yes | -| project-controller.test.ts | yes | -| project-real-mode.test.ts | no | -| project-resolution.test.ts | no | -| project-usecases.test.ts | no | -| database.test.ts | yes | -| database-plan-limit.test.ts | no | -| bucket.test.ts | yes | -| branch.test.ts | yes | -| branch-controller.test.ts | no | -| branch-usecases.test.ts | no | -| local-branch.test.ts | no | -| read-branch.test.ts | no | -| app-branch-database.test.ts | no | -| git-adapter.test.ts | no (adapter unit tests, no fixtures) | - -Other fixturePath users (not resource commands, for context): auth.test.ts, auth-controller.test.ts, app.test.ts, init.test.ts, shell.test.ts, update-check.test.ts, version.test.ts, helpers.ts, use-case-helpers.ts. `auth-real-mode.test.ts` matches "fixture" prose but not `fixturePath`. - -There is no dedicated `git.test.ts`; git-related coverage lives in `git-adapter.test.ts` (and branch tests). diff --git a/.drive/projects/prisma-cli-v8/specs/s2b-resources.md b/.drive/projects/prisma-cli-v8/specs/s2b-resources.md deleted file mode 100644 index b28f6798..00000000 --- a/.drive/projects/prisma-cli-v8/specs/s2b-resources.md +++ /dev/null @@ -1,142 +0,0 @@ -# S2b — Resources (slice contract) - -One PR into `main`, branch `s2b-resources`, after S2a merges. Ports the -resource-administration groups onto the engine: `project *` (incl. -`env`), `postgres *` (renamed from `database`, incl. `backup` and -`connection`), `bucket *` (incl. `key`), `branch list`, `git *`. - -Normative sources, in precedence order: (1) this contract's mapping -rules and per-command decisions; (2) the command inventory -`../assets/s2/command-inventory.md` for every current-behavior fact -(flags, positionals, API calls, output shapes, prompts, side effects) -— port behavior is the inventory's record EXCEPT where a mapping rule -below changes it, and every such change is a divergence-list entry; -(3) the v8 draft. Unpinned fact → STOP, never improvise. - -## Mapping rules (apply to every command in this PR) - -R-S2b-1 **Rename**: the `database` group ports as `postgres` -(target grammar). All paths, help, ids (`postgres.connection.rotate`), -and docs update; no alias to the old name. Divergence entry. - -R-S2b-2 **Auth**: every command the inventory marks `platform` or -`platform+login` declares `needs.credentials`. The legacy -auto-launched interactive OAuth login (`platform+login`) DOES NOT -port: unauthenticated invocations settle with the engine's sign-in -error and a `run-command` nextAction for `auth login`. (Operator -ratification pending — morning-questions ledger Q1; build to this -rule unless overruled.) - -R-S2b-3 **Consent**: destructive operations (`remove`/`delete`/ -`restore`/`rotate`/`transfer` — exactly the inventory's -"needs --confirm"/consent rows) keep their CURRENT confirmation flag -(name and value semantics per inventory, e.g. exact-id `--confirm -`); interactively, absent the flag, they use `prompt.consent` -with the inventory's current question text. Non-interactive without -the flag → the engine's `CLI.CONSENT_REQUIRED` (exit 2). Cancel → -exit 3. The legacy split (exit 1 vs 2 vs 0-on-cancel) unifies to -engine codes; each changed code is a divergence entry. - -R-S2b-4 **Secrets**: commands the inventory marks "secret on stdout" -(`postgres create`, `postgres connection create|rotate`, `bucket key -create`) present the secret as the `stdout` payload lines (pipe-clean -by Option A) and mask it in human Blocks via `sensitive: true`. The -json envelope carries it in `result` exactly as today. - -R-S2b-5 **Errors**: legacy flat codes map to dotted codes under the -group's namespace (`PROJECT.*`, `POSTGRES.*`, `BUCKET.*`, `GIT.*`, -`BRANCH.*`), preserving summary/why text; every mapping is enumerated -in the divergence list. Errored paths exit 2 (legacy 1 → 2 -divergences enumerated once as a class). - -R-S2b-6 **Interactive pickers** (`project link`, others per -inventory): `prompt.select` (clack path) with the inventory's option -labels; non-interactive without the disambiguating arg → structural -prompt failure. - -R-S2b-7 **Polling** (`git connect` install wait): result command -emitting `status` events per poll transition, engine-clock injectable; -timeout behavior per inventory. - -R-S2b-8 **Aliases**: `project env remove`'s `rm` alias does not port -(the tree has exact paths; it is the only alias in the shell). -Divergence entry. (Ledger Q3.) - -R-S2b-9 **Tests**: semantic per the S2 ruling — `ctx.api` faked with -recorded SDK-shaped responses; every command × (success, errored, -json envelope, unauthenticated, consent grant/deny/non-interactive -where applicable, picker path where applicable). No fixture mode. Each -command's test asserts envelope + presented data + events + exit code, -not bytes. - -R-S2b-10 **Files**: v8 command modules live at -`packages/cli/src/v8//.ts`, one command per file, -definitions + handler colocated (S1 whoami pattern); shared per-group -presentation helpers in `packages/cli/src/v8//presentation.ts`. -Handlers call the existing controllers'/providers' operation layer -(inventory names the exact functions) — S2b does NOT rewrite the -operations, only re-homes invocation behind `ctx.api`-built clients -where the operation takes an SDK/client argument (inventory's "API -surface" column names it). - -## Commands in scope (31) - -`project list|show|create|link|rename|remove|transfer`, -`project env add|update|list|remove`, `git connect|disconnect`, -`branch list`, `postgres list|show|create|usage|restore|remove`, -`postgres backup list`, -`postgres connection list|create|rotate|remove`, -`bucket list|create|delete`, `bucket key list|create|delete`. - -Every command: one inventory entry = its behavior contract; the -mapping rules above are the only deltas. The implementer builds a -per-command conformance row (command → inventory entry → applied -rules → divergences) in the PR's divergence list. - -## Out of scope - -`service`/`app`, `build`, `agent`, `feedback` (S2c); `init`, shell -deletion (S2d); auto-login reinstatement (ledger Q1); command aliases -(ledger Q3). - -## Acceptance - -- [x] All 31 commands mounted in the v8 bin under groups - `project`, `git`, `branch`, `postgres`, `bucket` (+ declared - subgroup help), passing R-S2b-9's test matrix. The mount-coverage - test now asserts the literal path list, so a missing command - fails it. -- [x] `postgres` rename complete; no `database` path survives in v8. - The three v8 strings that named the old group and left the error - mapper's regex to rewrite them were found by the closure pass and - now name `postgres` directly. -- [x] Consent matrix proven for every destructive command — all seven, - each matrix non-vacuous, with the success case driving a real - API call. -- [x] Secrets pipe-clean and masked per R-S2b-4. The golden entry added - at closure is what joins our `sensitive` flag to the engine's - `********`; before it, neither end proved the other. -- [x] Divergence list updated, 46 entries and a conformance row for - every command. -- [x] Legacy fixture tests for ported commands deleted, unit tests for - helpers and providers the new commands still call kept; legacy - shell still green for unported groups. -- [x] Root verification green; PR #133 at roughly +17,700 lines; both - the per-dispatch review rounds and the closure architect and - principal-engineer passes run, with every finding dispositioned. - -Nothing is left recorded-but-unfixed. Three things were recorded here -first and then fixed, all because recording them was the wrong call. - -`git connect` declared `needs.interaction`, so non-interactive runs -failed before any API call even when no waiting was needed. Removed on -the operator's ruling of 2026-08-11; only the install wait needs a -person, and the engine refuses that itself. `project list` reported an empty -workspace at exit 0 when the API rejected the request — a refusal -reported as a success is not a behaviour anyone chose (divergence 46). -And the stdout lane carried human formatting — sizes as `2.0 KiB`, -placeholders like `unknown` and `unscoped` — which the Option A channel -ruling of 2026-08-09 had already settled: stdout is the machine-usable -payload and human mode is pipe-clean. That ruling was in the normative -stack the whole time; presenting it as an open question was an error, -not a judgement call. diff --git a/.drive/projects/prisma-cli-v8/specs/s2c-services.md b/.drive/projects/prisma-cli-v8/specs/s2c-services.md deleted file mode 100644 index 852c6feb..00000000 --- a/.drive/projects/prisma-cli-v8/specs/s2c-services.md +++ /dev/null @@ -1,75 +0,0 @@ -# S2c — Services (slice contract) - -One PR into `main`, branch `s2c-services`, after S2b merges. Ports the -deployment-and-delivery groups: `service *` (renamed from `app`, incl. -`domain`), `build logs`, `agent *`, `feedback`. - -Normative sources and precedence as in `s2b-resources.md`; S2b's -mapping rules R-S2b-2/3/4/5/6/9/10 apply here unchanged (with -namespace `SERVICE.*`, `BUILD.*`, `AGENT.*`, `FEEDBACK.*`). Additional -rules: - -R-S2c-1 **Rename**: `app` ports as `service` (ruled: the deployable -unit's noun is Service). All paths, ids, help, presenters. No alias. -Divergence entry per command. - -R-S2c-2 **Streams** (`service logs`, `build logs`): session commands. -Records map to `output` events — the inventory's per-record -`source`/`level` routing maps channel `data` (stdout) vs `diagnostic` -(stderr); json mode frames them (the legacy JSON wrapper-event opt-out -for `build logs` does not port — the engine stream IS the json -surface; divergence entry). `build logs` gains its first tests ever -(inventory finding): the full R-S2b-9 matrix. - -R-S2c-3 **Progress operations** (`service deploy`, `promote`, -`rollback`, `remove`, `domain wait`): result commands emitting -`step-started/finished`, `progress`, and `status` events per the -inventory's step structure; SDK polling drives events through the -injectable clock. `service remove`'s type-the-name confirmation ports -to `prompt.consent` + its current flag per R-S2b-3. - -R-S2c-4 **`service run`** — RULED (operator, 2026-08-11): DROPPED. It -does not port and no engine child-exit-code passthrough is built for -it. Starting a local dev server and passing its exit code through is -Composer's `dev`. Divergence entry alongside `service build` and -`service deploy`. - -R-S2c-5 **`service build`**: result command; local build; progress -events from the SDK build reporter; no `ctx.api`. - -R-S2c-6 **Browser opening** (`service open`, inherited by S2b's -`git connect`): the URL is presented as an `endpoint` event + opened -via the operation layer's existing opener; `--no-open`-style flags -per inventory. - -R-S2c-7 **Update notification + shell parity**: no new work — S2a -landed both shells on the shared module; S2c only confirms the v8 bin -covers the newly ported groups (no per-command wiring exists). - -## Commands in scope (24 + 1 parked) - -`service build|deploy|show|open|logs|list-deploys|show-deploy|promote| -rollback|remove`, `service domain add|show|remove|retry|wait`, -`service env *` — NOTE: the inventory places env under `project env` -(S2b) only; `app` has a `domain` subgroup and no `env` subgroup — -scope follows the inventory. `build logs`, `agent -install|update|status`, `feedback`. Dropped: `service run`, -`service build`, `service deploy` (ruled; superseded by Composer). - -## Out of scope - -`init`, shell deletion (S2d); Composer (S3). - -## Acceptance - -- [ ] All in-scope commands mounted and green on the R-S2b-9 matrix - (streams included); `build logs` covered for the first time. -- [ ] `service` rename complete; no `app` path survives in v8. -- [ ] Deploy/promote/rollback/remove event sequences pinned by - semantic tests (step/progress/status ordering). -- [ ] Divergence list updated (rename class, stream-wrapper drop, - consent/exit unifications, error-code map). -- [x] Q2 ruled: `service run` dropped, so S2d deletes the commander - shell with no exceptions. -- [ ] Legacy fixture tests for ported commands deleted; root - verification green; PR ≥1k LOC; review loop run. diff --git a/.drive/projects/prisma-cli-v8/specs/s2d-init-and-retirement.md b/.drive/projects/prisma-cli-v8/specs/s2d-init-and-retirement.md deleted file mode 100644 index 9bfc8b21..00000000 --- a/.drive/projects/prisma-cli-v8/specs/s2d-init-and-retirement.md +++ /dev/null @@ -1,89 +0,0 @@ -# S2d — Init and shell retirement (slice contract) - -One PR into `main`, branch `s2d-init-and-retirement`, after S2c -merges. Ports `init` and `version`, deletes the commander shell and -fixture machinery, and makes the v8 bin the shipped binary. Closes -slice S2. - -Sources and precedence as in `s2b-resources.md`. S2b rules apply where -relevant. Additional rules: - -R-S2d-1 **`init`**: result command; the wizard runs on the engine -prompt surface (clack path from S2a): the inventory's step list is the -contract (project name, template/framework selection, linking -question, env write-out, agent-setup offer). Consent semantics: file -writes into a non-empty directory follow the current confirmation -behavior via `prompt.confirm`; `--yes` accepts defaults per engine -rules; every prompt has the inventory's current default. File-writing -side effects byte-match current templates (template files are data, -not rendering — they stay byte-asserted). The auth-linking step uses -`needs`-free auth probing like `auth whoami` (state read, no forced -login) + a sign-in nextAction when unauthenticated (R-S2b-2 spirit; -enumerate divergence from any legacy auto-login). - -R-S2d-1a **`init`'s dependency install**: today's `init` installs the -compute SDK by spawning the user's package manager through execa -(`packages/cli/src/controllers/init.ts`) — a step this contract did not -previously mention. It ports onto the engine's package-manager -capability: the handler declares `installsPackages: true` and calls -`ctx.packages.install(...)`; it does not spawn, spell a manager command -line, or phrase the failure itself. The current behavior of treating a -failed install as a warning and continuing (rather than failing the -command) is preserved — the capability returns a `Result`, so the -handler keeps that choice. Prerequisite: the capability must have -landed (`engine-package-manager-capability.md`); until it does, this -sub-rule is the reason `init` cannot be ported. - -R-S2d-2 **`version`**: the engine's `--version` surface already -exists; the `version` COMMAND ports as a result command presenting -the inventory's current fields (version, node, platform) with a json -serializer. Trivial but user-visible; test matrix applies. - -R-S2d-3 **The bin cutover**: `packages/cli/package.json` `bin` (`prisma-cli`) points at the v8 entry; the tsdown build bundles the v8 tree; the `prisma-v8` working name and root script are deleted. The config-loader plain-Node constraint is resolved: RULED (operator, 2026-08-11) to copy prisma/prisma and prisma/composer, which both load the config with `c12` — a dependency, dynamically imported, called as `loadConfig({ name, cwd, configFile? })`. `c12@3.3.4` depends on `jiti` directly, so an ordinary `dependencies` entry is all that has to be declared; its only peer dependency is `magicast`, which is optional. Nothing to design; port their shape. This no longer blocks any part of the slice. - -R-S2d-4 **Deletions** (after all ports green; single dedicated -commit series): the commander shell (`src/cli.ts` program wiring, -`src/shell/*` minus the modules S2a relocated), fixture machinery -(`src/adapters/mock-api.ts`, `src/use-cases/**`, fixture providers, -`isRealMode` branches — the inventory's "what dies" list is the -deletion checklist), all remaining fixture-mode tests, the -`PRISMA_CLI_MOCK_FIXTURE_PATH` env surface, and `--trace`. Legacy -presenters/controllers survive ONLY where S2b/S2c handlers still call -them as operation layers (enumerate survivors in the PR). Known -survivors as of S2a: `src/state-dir.ts` (relocated out of the shell; -`shell/runtime.ts` merely re-exports it) and the `CliError` base class -in `shell/errors.ts` — `src/auth/errors.ts` still constructs CliError -instances (the auth module's one remaining legacy dependency) and -`src/v8/auth/errors.ts` maps them to structured errors; when the -legacy shell dies, either CliError moves to a durable home or the auth -operations throw structured errors directly and both mapping layers -go. - -R-S2d-5 **Grammar completeness check**: a build-time test asserts the -mounted tree equals the S2 target grammar exactly (every inventory -command minus ruled removals plus ruled renames; `service run` is -a ruled removal, so nothing of the legacy shell survives on its -account). This is the platform slice of the S7 grammar check. - -R-S2d-6 **Final parity review**: the cumulative S2 divergence list -(S2a+S2b+S2c+S2d) is consolidated into one document for operator -sign-off: `../assets/s2/parity-divergences.md`. - -## Out of scope - -Composer (S3), ORM (S5), publish pipeline (S7), auto-login -reinstatement (Q1 unless ruled meanwhile). - -## Acceptance - -- [x] `init` wizard green on the full prompt matrix (interactive, - `--yes`, non-interactive, cancel) with byte-asserted templates. -- [x] Bin cutover complete, config loaded through `c12` as the - reference repositories do; `prisma-cli` runs the - engine shell from a packed tarball on plain Node. -- [x] Commander shell + fixture machinery deleted per R-S2d-4's - checklist; survivor list enumerated. -- [x] Grammar completeness test green (landed via S7's `check:grammar`; `version` is a ruled removal, and the ORM's initializer moved to `orm init` so top-level `init` is the platform wizard — operator, 2026-08-12). -- [x] Consolidated divergence document reviewed and RATIFIED by the operator (2026-08-12). -- [x] Root verification green; PR ≥1k LOC; review loop run; S2 slice - closed in the project plan. diff --git a/.drive/projects/prisma-cli-v8/specs/s3-composer.md b/.drive/projects/prisma-cli-v8/specs/s3-composer.md deleted file mode 100644 index c93e2b75..00000000 --- a/.drive/projects/prisma-cli-v8/specs/s3-composer.md +++ /dev/null @@ -1,498 +0,0 @@ -# S3 — Composer adoption (slice contract, revision 2 final) - -Status: CLOSED 2026-08-12 (acceptance verified; dispositions in the -Close-out section). Was rev 2 final (2026-08-11) — the -first-principles design (operator-settled) plus both delta reviews -folded (architect + PE, accept-with-changes; all findings adopted, -dispositions in §10). -Precedence: this contract > `specs/s2-overview.md` standing rulings -> `assets/s3/composer-inventory.md` > source. Unpinned facts are -STOP-and-surface. - -Repos: **prisma/composer + prisma-cli.** Branches: `s3-composer` -(prisma-cli, stacked on merged-S2a `main`); composer branch at D2. -D2 and D3 land as a STACK and release together (composer's CLI is -never commandless on `main`; the clipanion shell is deleted in D3, -not D2). - -## The design (operator-settled, 2026-08-11) - -**One process.** Composer's commands are ENGINE commands: the -composer repo exports a `CommandFamily` whose handlers are -composer's own code, running in whichever process mounts the family -— composer's own thin CLI or the `prisma` bin. Composer has no -binary and none is created. The engine is the front door (grammar, -help, arg validation, auth context) plus one affordance for the -moment composer hands the terminal to alchemy. - -### `ctx.spawn` (the affordance) - -```ts -const child = await ctx.spawn({ command, args, cwd, env }); -// child: { exitCode: number | null, signal: string | null } -``` - -- **Terminal**: inherited stdio, SAME process group (POSIX) / - shared console (Windows) — the terminal delivers Ctrl-C to the - child natively. No forwarding, no detach, no new console. -- **Signals while live**: the engine neither aborts nor exits, but - RECORDS delivered signals; on child exit it replays them into its - normal ladder (one recorded → `ctx.signal` aborts as if just - delivered; two+ → abort fires and the next signal force-exits). - A replayed signal is a delivered signal like any other, so the - run settles from the engine's record the same way (below). - The latch never advances past what the user pressed; no - force-exit path skips handler cleanup that has not had a turn. - The engine always outlives the child. SIGTERM (no native path to - the child — supervisors signal the engine pid) is forwarded to - the child during a live window, and a SECOND recorded press of - any signal is forwarded as SIGTERM (PR-136 review, §10): when - the engine was signalled directly and no process group delivered - the first press to the child, the escalation is the interrupt - path that keeps the child reachable. -- **Programmatic abort** (`ctx.signal` aborted by non-signal - means, or non-TTY contexts): the engine terminates the child — - SIGTERM, a stated grace period (ruled in D1), then SIGKILL; the - resulting `signal` appears on the child result. -- **Windows**: shared-console delivery covers Ctrl-C for - `deploy`/`destroy` (spawned without `detached`, no new console); - the engine keeps its own handling (no group semantics to defer - to) and terminates the child on abort. `dev`/`log` refuse - Windows, as today. Group-semantics tests are POSIX-only; - Windows behavior is asserted at the fake-spawn level. -- **Output**: no engine write may interleave with a live child. - Commentary events (`ctx.report`) during a live spawn are - BUFFERED and flushed in order on child exit (session handlers - have asynchronous producers — watchers, log pumps — that cannot - be quiesced around a converge); `ctx.present`/settlement during - a live spawn is a construction error. -- **Reentrancy**: one live child per run; a second `ctx.spawn` - while one is live is a construction error. `dev` coalesces - rebuilds, as today's loop does. -- **Afterwards**: the handler resumes with the child's status, and - so does the ENGINE — it records every child `ctx.spawn` returns. - `ctx.lastChild()` reports the run's most recent completed child, - or undefined when none ran, which is what lets a handler ask how - its child ended at the point it settles when the spawn itself - happened somewhere else in its own layering. - To exit with the child's code verbatim the handler settles via - the sanctioned `exitWithChildStatus(opts?)` outcome. It names no - child — the engine settles from its record — and `opts` carries - `{ nextActions? }`, rendered to stderr in the engine's - next-action style before the exit (R-S3-4's reproduce hint; the - envelope stays absent). It uses the settlement bypass server - commands already have. - The ORDER that settlement is read in is the ENGINE's, not the - handler's: **a signal-killed child is an ABORT, not a failure**, - and settles 128+signal with no envelope and NO next actions — - the hint is dropped even when the handler passed one, because the - user stopped the run and there is nothing to reproduce. Otherwise - the child's own code passes through verbatim, and an unknown - termination settles 1, never 0. A handler reading a `ChildResult` - for its own purposes still branches on `signal` before `exitCode`. - The bypass is FENCED, each fence a construction error: settling - it from a command that does not declare `maySpawn`, and settling - it when no child ran at all (amended 2026-08-11, operator review - of composer#220 — the earlier "a child result the engine did not - produce" fence is gone, because with no child argument there is - nothing left to invent). - The session kind's settlement is amended to permit non-zero - through this path (it used to hard-code 0, and now also settles - 130/143 on its own — below), - and the session-kind "always supports json" guarantee is amended: - a command that may spawn keeps stdout framed in JSON mode by routing the - child's output to diagnostic stderr; the session then emits its normal - terminal result frame. -- **Exit codes are the engine's** (operator ruling, 2026-08-11): a - run a delivered signal terminated settles 128+signal from the - ENGINE's own record of that signal, for both command kinds and - including the handler that caught the signal, cleaned up and - returned successfully. A handler cannot author 130/143 — - documented codes stop at 99, and the child-status bypass takes - its code from the engine's record of the child rather than from - anything the handler hands back. The verbatim codes stay - verbatim: a real child's status passes through untouched (the - child owned the terminal and the signal reached it too), as does - a server command's protocol conclusion. -- **Seam**: spawning enters through `Runtime.spawn` (the bin - passes a `node:child_process` adapter; the engine never imports - `node:child_process`); `createTestCli` seeds a scripted fake - recording command/args/cwd/env KEYS — never env values. -- **Telemetry**: unchanged — `RunSummary.exitCode` carries the - settled code; `onSettled` fires; `durationMs` includes the child. - -### Auth — two legs, one protocol - -`PRISMA_SERVICE_TOKEN` + `PRISMA_WORKSPACE_ID` are the Prisma -ecosystem's inter-process credential protocol; the engine speaks -it. A command declares credentials; handlers never touch token -material; the engine's own process env is NEVER mutated. - -- **The CHILD leg**: the engine resolves the current session into - the affordance's child env through ONE unified read — the - manager operation `activeAccessToken()`, called at spawn time - whatever the credential's origin. (Amended in the PR-136 review - round, §10: rev 2 prescribed a `source` conditional — env - pass-through vs `tokenStorage(workspaceId)` — but branching on - `origin.source` outside whoami is a defect by the - credential-manager design; an environment-only manager satisfies - the operation by passing the env token through, no accessor - call, which preserves what the conditional was for.) The - operation is the ONE credential-manager SPI amendment this slice - makes, recorded in `credential-manager-design.md` §11.5 with its - single named consumer. The injected token is a snapshot; the - child never refreshes; the refresh token is NEVER injected. A - credential naming no workspace DELETES an inherited - `PRISMA_WORKSPACE_ID` from the child env — the two variables are - one protocol, written as a unit. -- **The IN-PROCESS leg**: container ensure/locate and preflight - run in the engine process before any spawn, and composer's code - reads env directly there today (`container.ts:138-149`, - `preflight.ts:185`). They are authenticated instead through - composer's existing injection seam: `ctx.api` (the engine's - pinned, refreshing client) into `deps.client` - (`container.ts:144-147` skips the env check when injected), and - the workspace id from `ctx.session()` into - `prismaCloud({workspaceId})`. Any extension path that still - reads env directly is a composer-side change in this slice, not - a family-boundary workaround. With this, the one-client-per- - process invariant of the credential-manager design HOLDS (no - second, non-refreshing client is ever constructed on a stored - session) — recorded in the same SPI amendment. -- **Near-expiry**: if the session expires within a threshold - (ruled in D1), the command refuses BEFORE the in-process leg — - not merely before spawn (a late refusal would create platform - resources and then abort) — with the credentials-required error - and a re-auth next action. The child runs on a static token that - may expire mid-run past that threshold; accepted and documented. - The plan's coverage-ledger row "refresh under long runs" is - corrected in D4: S3 proves the static-handoff case. - -**No engine-side consent.** `destroy` ports without confirmation -(legacy parity): the front door cannot know what the child will -destroy. If destroy deserves a confirmation it belongs in composer -— raised upstream as a product candidate, not built here. - -### The prisma bin imports the family directly (ruled) - -Committed consequences: -1. **Node floor, scoped to the bin**: the published `prisma` - package declares `node >= 24` (composer's floor; max wins). - `@prisma/cli-engine` and `@prisma/cli` floors do NOT move — - composer itself consumes the engine and must not re-inherit its - own floor through it. -2. **Install footprint** (operator-acknowledged consequence): - every `prisma` install pulls composer's constellation — - `alchemy@2.0.0-beta.67`, `effect@4.0.0-beta.103`, - `@prisma/orm-*@8.0.0-rc.1`, esbuild, c12, arktype — whether or - not composer commands are used. (Composer's alchemy pnpm patch - is NOT a concern: it is types-only — a 13-line - `Aliases?: … | undefined` widening in `lib/Resource.d.ts` with - zero runtime effect. It stays applied in composer's repo for - its own typecheck; upstreaming is a courtesy ask, not a slice - dependency.) -3. **Alchemy must not load on `prisma version`.** The family's - command DEFINITIONS and handler entry functions are statically - imported (standing ruling). The EXECUTOR modules stay behind - composer's existing dynamic-import boundary - (`operations/deploy.ts:47`, `destroy.ts:45`, `dev.ts:68`, - `log.ts:74`) — that boundary is the mechanism keeping effect - out of the static graph (`execute-dev.ts:13` and - `execute-log.ts:11` reach `effect/Layer` via - `resolveLocalTargets`); flattening it is a contract violation, - not an optimization. Alchemy/effect enter only at - config-evaluation time (and dev/log's local-target thunk - resolution) inside a running composer command. Enforced by a CI - import check anchored at the family entrypoint and run against - BUILT OUTPUT (type-only imports must be proven erased), catching - any handler that statically imports an executor. -4. **Signal listeners**: alchemy's import-time listener - registration (the @alchemy.run/node-utils exit-hook) is being - fixed upstream — alchemy-run/node-utils#6 scopes the hooks to - owned locks, so a bare import registers nothing. OPERATOR - RULING (2026-08-11): NO workaround is built. The slice proceeds - on the basis that the fix lands through the delivery chain - (node-utils release → alchemy's exact pin bump → composer's - alchemy bump) before D3 ships. The family's test suite keeps - ONE assertion as the detector: after a composer command's - config evaluation (and dev/log local-target resolution) the - engine is the sole SIGINT/SIGTERM listener. If the chain has - not delivered by D3, that failing test is the STOP that - resurfaces this decision — nothing ships with alchemy's exit - handlers live in the engine process. Residual (accepted): - in-process alchemy code that ACQUIRES a lockfile lock registers - handlers for the lock's duration; D2/D3 verify no in-process - path takes locks. - -## Mapping rules - -R-S3-1 **Engine additions** (D1, `packages/cli-engine`): -`ctx.spawn` and `ctx.lastChild` per above; `Runtime.spawn` + -harness fake; -`exitWithChildStatus`; parse-time `--json` rejection + the two -kind amendments; credential injection (SPI amendment recorded); -the signal record-and-replay + SIGTERM forwarding + abort ladder; -**and a production, environment-only `CredentialManager` exported -from the MAIN entrypoint** — composes the env session from -`PRISMA_SERVICE_TOKEN`/`PRISMA_WORKSPACE_ID`, refuses mutations -with a structured error; composer's rebuilt CLI wires it (today -the only implementation lives in the testing entrypoint and is -not usable in production). Draft amendments; tests per the -acceptance split. Generic — no composer knowledge. - -R-S3-2 **Config.** The engine's `composer` section is -`{ configPath?: string }` (fields grow only by contract -amendment). Discovery: with an explicit `configPath` the section -wins and the walk is skipped (and `CONFIG.PATH_MISMATCH` is -redundant and retires FOR THAT CASE); with no section or no -`prisma.config.ts` — the common case — composer's entry-anchored -walk runs unchanged and the PATH_MISMATCH check survives with it. -The throwing loader is rewritten to diagnostics-list semantics -(value + structured diagnostics; commands fail on sections they -need), rendered through ENGINE presentation. The effect-resolution -preflight moves INTO the shared config-load machinery (not import -time), mapping to the existing `DEPS.EFFECT_VERSION_CONFLICT` -structured error — the part of 1c deliverable 2 that S3 must own -because the prisma bin has no composer `bin.ts`; the rest stays -with the composer team. Composer-internal errors crossing into the -engine are translated at the family boundary (composer's -`CliStructuredError` shares the engine class's duck-typed name; -untranslated it would silently drop its `fix` text — mapped to -`nextActions`, pinned by test). - -R-S3-3 **Family export + composer's own CLI** (D2/D3): composer -publishes the `CommandFamily` from a dedicated entrypoint with an -alchemy-free static graph. `@prisma/cli-engine` EXACT-pinned, -declared identically in BOTH `packages/9-public/composer` and the -internal CLI package, named in tsdown's `external` array -(bundling is the default there), Dependabot ignore per the -`@durable-streams/server-conformance-tests` precedent, verified -external in the packed tarball. **Composer's repo CLI is rebuilt -as a thin composition of its own exported family** — `createCli` + -`Cli.run` (public API suffices; the substantive work is the -`Runtime` composer constructs: streams, env, cwd, exit proxy, -signal subscription, and the R-S3-1 env-only credential manager) — -replacing the clipanion `main.ts`; shell and bespoke runner die in -D3. **Composer's CLI e2e tests are rewritten to drive the exported -commands** (rebuilt CLI for process-level coverage; `createTestCli` -for semantic coverage), so the family is proven standalone in -composer's CI before the prisma bin mounts it. - -R-S3-4 **The four commands** (D3), engine handlers in composer's -family: -- `deploy ` / `destroy `: result commands. Handler: - section/args → near-expiry check → config evaluation → pipeline/preflight/artifact with engine - presentation, authenticated via the in-process leg → - `ctx.spawn(alchemy converge)` → failure: `exitWithChildStatus` - with the reproduce hint as `nextActions` (stage stays - container-derived, inventory H8) → success: read the - deployment-result file, present the summary. The handler does not - order the signal case itself: the ENGINE settles a signal-killed - child as the abort (128+signal, no failure envelope, no reproduce - hint) whatever the handler asked for, which is what replaces the - status collapse at `run-alchemy.ts:61`. `deploy --production` (accepted-but-always- - errors today) is dropped. The `.alchemy` destroy warning ports - verbatim, correctness tracked as H6. -- `dev `: session command (kind amendments apply). Watch - loop, emulators, live attachments are handler state; converges - via `ctx.spawn` (coalesced rebuilds); local-target thunk - resolution followed by `reclaimSignals`. A converge failure - BEFORE the session is live settles with the child's status; - AFTER, it is a warn event and the session continues. A - signal-killed converge is SHUTDOWN (cleanup, settle 130), never - `converge-failed`. Ctrl-C settles 130 (legacy exits 0 — - divergence): the handler cleans up and returns `ok(undefined)`, - and the ENGINE settles 130 from its own signal record — `dev` - states no exit code of its own. Windows: refuses, as today. -- `log [address]`: session command reading the LOCAL - dev-emulator daemon (§4c; not the platform logs surface — S8 - note stands). Windows: refuses, as today. -- Auth: `deploy`/`destroy` declare credentials (both legs); - `dev`/`log` credential-free. - -R-S3-5 **Test surfaces** (D2/D3): (1) the fake child (scripted -program / `Runtime.spawn` fake) for engine and family tests. -(2) The published control-API double (claimed 1c deliverable 3) -from composer's `./testing` entrypoint: fixture-backed, same -signatures, working `DevSession` double, compile-time conformance -check in composer's typecheck; its built chunk must contain no -import path to the real implementation (types only; verified by -building the tarball and grepping the chunk for alchemy/effect). -(3) **The family-injection seam**: the family export takes an -optional operations argument (`createComposerFamily({operations?})`) -defaulting to the real control operations; prisma-cli's family -tests mount the family with the double. If D2 judges that seam -wrong, prisma-cli's family tests scope to grammar/mounting/arg -validation/credential refusal and the "green through the double" -claim moves entirely into composer's CI — D2 decides and records -which. Either way: prisma-cli's tests never spawn alchemy or -containers, and its typecheck must not require the alchemy/effect -constellation beyond what `@prisma/composer` itself demands. - -R-S3-6 **Tandem release** (D4): order engine → composer → -prisma-cli; no step consumes an unpublished sibling (previews via -composer's pkg.pr.new mid-slice). The `@prisma/`-scope -pin-enforcement extension lands in composer's `ci.yml` -(publish.yml runs no checks). prisma-cli pins `@prisma/composer` -exactly; S7 consumes committed versions. - -## Out of scope - -`service run` (rides `ctx.spawn` later; ledger Q2 closes -"mechanism built in S3", updated in D4); S8 (consumes this slice; -its remaining unknown — planner drift detection — is a D2 read of -alchemy's source from installed node_modules, reported to the -operator); 1c deliverable 2 beyond the config-load effect check -(composer team; recorded in the closure); alchemy upstream asks -(courtesy, not dependencies). - -## Acceptance - -- [x] Engine (real child, trivial script): exit passthrough incl. - 1/2/3; ENOENT structured error; native Ctrl-C reaching the - child (POSIX; fake-level on Windows); record-and-replay - after child exit (one signal → abort; two → escalation); - SIGTERM forwarded during window; abort ladder - TERM→grace→KILL; engine-outlives-child; unframed child - stdout; buffered events flushed in order. -- [x] Engine (fake spawn): `--json` parse rejection; - near-expiry refusal; env composition both session sources; - env KEYS never values; reentrancy construction error; - telemetry settlement; the env-only credential manager's - composition + mutation refusals. -- [x] SPI amendment recorded (single named call site + the - one-client invariant outcome). -- [x] Composer family static graph alchemy-free + effect-free on - BUILT output (CI check anchored at the family entrypoint); - executors remain behind the dynamic-import boundary; the - engine-sole-listener DETECTOR assertion after config - evaluation and local-target resolution (see design - consequence 4 — no workaround behind it, by ruling). -- [x] Composer's rebuilt CLI (engine + own family + env-only - manager) replaces clipanion; e2e tests drive the exported - commands; old shell/runner deleted in D3; D2/D3 stacked. -- [x] Four commands green in composer CI (double + fake child; no - alchemy, no containers) and mounted under `composer` in the - prisma bin. -- [x] Tarball checks: engine external + exact pin; double's chunk - import-clean; dual-manifest pin equality; Dependabot ignore. -- [x] prisma bin: `node >= 24` (bin only); install-footprint - consequence recorded. AMENDED — shipped as `>=22.18.0`; see - Close-out. -- [x] Divergences (`assets/s2/parity-divergences-s3.md`): dev - Ctrl-C 130-vs-0; `--production` dropped; reproduce-hint - shape; `--json` rejection; PATH_MISMATCH conditional - retirement; help/usage output shape + bare-invocation exit - (inventory D6/D7); `--tail` becomes a typed number flag - (D5); `[dev]`/`[log]` console prefixes become engine events; - exit unifications on engine-side error paths. -- [x] 1c closed with explicit dispositions (D1 → R-S3-2; D2 split: - config-load effect check owned here, rest composer team; - D3 → R-S3-5). -- [x] Ledger Q2 + coverage-ledger rows corrected. AMENDED — Q2's - disposition changed; see Close-out. -- [x] Both PRs through the slice review loop; suites green in - both repos. - -## §10 Disposition record - -Rev 1 (2026-08-11): reviewed architect (reject) + PE -(accept-with-changes); superseded by the operator's first- -principles session — one process, `ctx.spawn`, native signal -delivery, no engine-side consent, direct family import, composer -CLI rebuilt on its own family with e2e against the exported -commands. - -Rev 2 deltas (architect + PE, accept-with-changes; all adopted): -two-leg auth via composer's `deps.client` seam + workspace-id -threading; diff-based `reclaimSignals` at both alchemy entry -points; record-and-replay signal latch + SIGTERM forwarding + -abort ladder; buffered commentary during live spawns; reentrancy -rule; family-injection seam with the D2 fallback; D2/D3 stacking; -Windows shared-console mechanism; effect-resolution check into -config load; divergence additions; node floor scoped to the bin; -alchemy patch downgraded to a types-only note (PE read the -patch); executor lazy boundary named as the static-graph -mechanism + built-output check; production env-only credential -manager added to D1 (composer's rebuilt CLI needs one; only a -testing implementation exists); PATH_MISMATCH conditional -retirement; session-kind settlement amendment. Post-fold operator -ruling: the reclaimSignals workaround is OMITTED on the basis of -alchemy-run/node-utils#6 (verified: deletes the module-scope -registration, scopes hooks to owned locks); the sole-listener test -remains as the detector. Operator items: -install footprint acknowledged as a committed consequence -(accepted — operator proceeded to implementation, 2026-08-11); everything else mechanical. - -PR-136 review round (architect + PE on D1, 2026-08-11; orchestrator -rulings applied): the child-status settlement bypass is fenced to -`maySpawn` commands (runtime check; the architect blocker); -`exitWithChildStatus(opts?)` gains `{ nextActions? }` rendered -before the exit — the R-S3-4 surface as written now exists; the spawn -path's storage read is replaced by the named manager operation -`activeAccessToken()` and the Auth section's `source` conditional is -amended away (branching on `origin.source` outside whoami is a defect -by the credential-manager design — the code's unified read stands and -the contract now describes it); handing credentials to the child is -declared `needs: { credentials: "child" }` (the top-level -`credentialsForSpawn` coinage is gone; the entailment of the -credentials need is structural); a second recorded signal press is -forwarded to the child as SIGTERM (the direct-signal escalation path); -the environment-only `CredentialManager` the rebase dropped is -restored as `EnvironmentCredentialManager` on the main entrypoint. - -composer#220 review round (operator on the D3 family, 2026-08-11): -two things composer's `converge.ts` was hand-rolling belong to the -engine, and move there. Composer kept a mutable closure recording -whatever `ctx.spawn` returned, so its handler could read the child -where it settles — the engine mints every `ChildResult` anyway, so it -now keeps the run's most recent one and exposes it as -`ctx.lastChild()`. And composer's `settleConverge` hand-rolled the -order the outcome is read in — signal-killed child first, then a -failure that reached a failing child, then an ordinary structured -error — so the signal-first half becomes the engine's: -`exitWithChildStatus` loses its child argument, settles from the -record, and settles a signal-killed child as the abort whatever -`nextActions` the caller passed. The "invented child result" fence -retires with the argument that made the misuse reachable; a run that -settles this way with no child on record is the construction error -that takes its place. - -## Close-out (2026-08-12) - -Acceptance verified against source and merged PRs: prisma-cli #136, -#145, #150, #151, #155, #152 (the mount, `42ee7891`); composer #220, -#224, #226. Evidence, per item: the real-child and fake-spawn suites -are `packages/cli-engine/tests/spawn-real-child.test.ts` and -`spawn.test.ts` (plus `environment-credential-manager.test.ts`); the -SPI amendment is `credential-manager-design.md` §11.5 -(`activeAccessToken(options)`, consumed by delegated preflight and -`execution/spawn.ts`); the -static-graph check is composer's `check:family-static-graph` and the -sole-listener detector is composer's -`cli/src/family/__tests__/signal-listeners.test.ts`; the tarball -checks are composer's `check:cli-engine-pin` / `check:publish-deps` -plus the Dependabot ignore in composer's `.github/dependabot.yml`; -the 1c closure is `assets/briefs/1c-leftovers-composer.md`. - -Two items shipped amended, deliberately, and are NOT claimed as -written: - -- **The bin's Node floor is `>=22.18.0`, not `>=24`.** Composer #224 - dropped composer's own floor to 22.18 (Node 22 suffices), and the - contract's rule — the bin takes composer's floor, max wins — held; - only the number moved. `@prisma/cli-engine` stays at 22.12 so - composer does not re-inherit a floor through the engine. -- **Ledger Q2's disposition changed.** The Out-of-scope line "ledger - Q2 closes 'mechanism built in S3'" was superseded: `service run` - was RULED dropped (operator, 2026-08-11; `s2-overview.md` Q2), so - D4 recorded that S3 built the mechanism for a command that no - longer exists rather than claiming Q2 closed by it. - -Two acceptance-suite tests are flaky under load (a child writes its -ready marker before installing its signal handler); both are recorded -in `deferred.md`. Everything carried out of the slice — including the -two-engine-copy install state that only the tandem release ends, and -composer dropping its `isCI` answer at its next engine-pin bump — is -in `deferred.md`. Hand-over context for a fresh agent: -`assets/briefs/s3-closeout-handover.md`. diff --git a/.drive/projects/prisma-cli-v8/specs/s6-conformance.md b/.drive/projects/prisma-cli-v8/specs/s6-conformance.md deleted file mode 100644 index d39d096e..00000000 --- a/.drive/projects/prisma-cli-v8/specs/s6-conformance.md +++ /dev/null @@ -1,195 +0,0 @@ -# S6 — Conformance checker (slice contract, revision 4 — all questions closed) - -Status: COMPLETE pending merge. prisma-cli half on PR #161, prisma/prisma half on prisma/prisma#29998; both ready for review (operator go-ahead 2026-08-12). All nine questions were closed by the operator on 2026-08-12 (see §5). Checks 1 and 2 built and green; check 3 and CI wiring in progress. Rev 4 also corrects rev 1–3's central factual error: they were grounded on a stale prisma/prisma checkout and claimed S5 had not started there. It has landed on origin/main — the engine at 0.0.9 in three manifests including published @prisma/orm-toolchain, the orm config section, the ported commands. Every claim below about prisma/prisma lacking subjects is struck. -Precedence: this contract > `specs/s2-overview.md` standing rulings > source. Unpinned facts are STOP-and-surface. - -Repo: prisma-cli. Branch: `claude/s6-conformance-checker-ebd3dd`, base `main`. - -Mandate (project plan §S6, spec.md FR9, design-notes "Conformance"): the small three-check tool — import purity, validator no-throw on hostile input, published-tarball verification — wired into both products' publish CI. Project DoD line: "Conformance checker runs in CI for both products' publish paths." - -## 1. The grounding example - -`@prisma/cli@8.0.0-rc.1` declares `@prisma/cli-engine` at the workspace version, which `pnpm pack` rewrites to `8.0.0-rc.1` in the published manifest. It also pins `@prisma/composer@0.6.0-dev.16`, and that package declares `@prisma/cli-engine: "0.0.9"`. Installing the shell resolves two different copies of the engine. Verified in this worktree: - -```text -$ npm ls @prisma/cli-engine --all # in a sandbox install of the packed shell -`-- @prisma/cli@8.0.0-rc.1 - +-- @prisma/cli-engine@8.0.0-rc.1 - `-- @prisma/composer@0.6.0-dev.16 - `-- @prisma/cli-engine@0.0.9 -``` - -This is already a recorded project decision, and the decision is what makes it S6's business. `deferred.md:37-51` states that the engine pin "must be the SAME version prisma-cli depends on", records the current disagreement, and rules that **matching pins is a release requirement for the tandem release** — the two-copy install is "a preview-only state to end rather than a configuration to support". `packages/cli/src/v8/cli.ts:9-15` says the same. - -Note what is *not* the argument, because the difference decides how the check is written. The engine deliberately survives two copies for structured errors: its cross-copy markers are `Symbol.for`, and two tests prove an error raised by one copy is recognised by the other (`packages/cli-engine/tests/execution.test.ts`, "a structured error built by another copy of the engine", and `tests/protocol.test.ts`). The check is not defending type identity, which is defended already. What is untested across two copies is execution and signal behaviour, and `deferred.md` rules that not worth testing precisely *because* matching pins is a release requirement. - -That leaves the requirement with no enforcement anywhere. Composer's `check-cli-engine-pin.mjs` compares composer's two manifests against each other and never against the shell's; no repo compares across the boundary. A check in the publish path is the enforcement the existing ruling implies, and it is the one thing in this slice nobody has built. - -## 2. What already exists (the non-duplication boundary) - -Two of the three checks are substantially built already, in the two other repos, and prisma-cli — which owns both the engine and the shell — has none of them. - -**composer** (all from S3, all in `ci.yml` on pull requests, none in `publish.yml`): - -| Script | What it asserts | -| --- | --- | -| `check-cli-engine-pin.mjs` | The engine pin is exact, identical across composer's two manifests, and survives into the packed manifest; packed `dist/**/*.mjs` retains a bare `@prisma/cli-engine` import, proving the engine was not inlined; `dist/bin.mjs` exists. | -| `check-family-static-graph.mjs` | Packed output's static import graph names no `alchemy`, `effect` or `@effect/*`, anchored at three entrypoints with anti-vacuity markers. | -| `check-floor-imports.mjs` | Each published entrypoint imports in a fresh `node` process, one process per entry. Added after `dev.16`. | -| `check-npm-effect-resolution.mjs` | **The whole of check 3b's mechanism already exists here**: packs both public packages with `pnpm pack`, writes a manifest into a `mkdtemp` outside the workspace, installs the tarballs with real npm against the real registry, starts the built `prisma-composer` bin inside that sandbox, and fails if more than one `effect` resolves. | -| `check-publish-deps.mjs` | `workspace:`/`catalog:` leaks, internal-sibling exact pins, dependencies on private workspace packages. The only validation step in `publish.yml`. | - -**prisma/prisma**: `check-publish-deps.mjs` (leaks, `@internal/*` exact pins within one manifest, `.d.ts`-declared-dependency resolvability), `lint-publishability.mjs`, `lint-consumer-internal-imports.mjs`, `lint-single-import-root.mjs`, `validate-package-manifests.mjs`, `validate-typescript-peer.mjs`. Three check steps in its publish path. - -**prisma-cli**: nothing. `publish.yml` runs `pnpm build`, `pnpm test:scripts`, then publishes. The closest existing thing is `packages/cli-engine/tests/no-child-process-in-dist.test.ts`, a single built-output substring assertion. - -What exists nowhere, in any repo: - -1. **Cross-repo pin agreement** — no check compares the shell's engine pin against the pin of any family it mounts. This is §1's defect. -2. **Any conformance check at all in prisma-cli.** -3. **Built-JavaScript imports measured against the declared dependency set.** prisma/prisma reads `.d.ts` only; composer checks a forbidden list, not the declared set. -4. **Validator no-throw as a check any section must pass.** composer tests its own validator inline; the engine tests that *the engine* survives a throwing validator (`config.test.ts:818`), which is the opposite direction. - -Because composer already owns the mechanism for 3b and most of 1, the honest reading of this slice is that its new capability is item 1, its new *coverage* is item 2, and items 3 and 4 are refinements. STOP-2 turns on that. - -## 3. The three checks - -Each check is a function over explicit inputs returning findings. No check reads ambient state; the caller supplies the subjects. That is what makes them testable without mocking. - -**Every check reports a finding when its subject set is empty.** An empty `dist/`, zero config sections, or a tarball containing no JavaScript is a broken invocation, not a pass. Every comparable script in the sibling repos carries this protection — composer's `MUST_COVER`, its "at least one chunk keeps the specifier" requirement, and this repo's own `expect(files.length).toBeGreaterThan(0)` in `no-child-process-in-dist.test.ts:24`. Check 1 additionally asserts that a known specifier is present in the output it swept, so a check that silently swept the wrong directory fails rather than passes. - -### Check 1 — import purity - -**Asserts.** For a built package directory and its manifest, every bare module specifier appearing in the built output belongs to a package declared in `dependencies`, `peerDependencies` or `optionalDependencies`, or to the allowed-private list the caller passes. It also reports a declared runtime dependency that the output never imports. Node builtins, relative and absolute specifiers are out of scope. - -**Measured over** static `import`/`export … from` specifiers and dynamic `import()` in every `.js`/`.mjs` file of the built output, parsed with `es-module-lexer` — not by substring search. This distinction is not cosmetic. `packages/cli/dist/v8/cli.js:13965` contains the string `@repo/cli-telemetry/sender` inside an `import.meta.resolve()` call wrapped in `try`/`catch`, with a documented fallback for exactly the published case (`packages/cli/src/v8/runtime.ts:45-58`). It is deliberate and correct. A substring check fails it on day one; a lexer does not see it, because it is not an import. - -**A flat sweep, not a reachability walk.** Every JavaScript file in the output is measured, whether or not `exports` or `bin` names it — `packages/cli/dist` holds 109 files and the manifest names two. The flat sweep is the stricter choice and is deliberate. Its consequence: check 1 cannot detect a subpath the manifest fails to expose. `@prisma/composer/family` is the live example of a subpath that must exist for the shell to work and that no check here would miss if it vanished. - -**Does not assert** anything about `.d.ts` files (prisma/prisma's `check-publish-deps` owns that), about which subpath a specifier names, or about the directory a package lives in. - -**Measured today.** Run against both published packages with the real lexer: `@prisma/cli` imports 16 bare roots, `@prisma/cli-engine` 8; every one declared, no declared runtime dependency unimported. Both directions clean, so this lands as a regression guard rather than a cleanup job. - -### Check 2 — validator no-throw - -**Asserts.** Given a list of `ConfigSection` values, each `validate` returns a well-formed `SectionValidation` for every input in a fixed hostile corpus, and throws for none. A malformed return — neither `ok: true` with a `value` nor `ok: false` — is its own finding. - -**The subject set is the union the engine itself uses.** Not "the families the shell mounts". The engine derives its recognised sections from command families *and* from standalone mounted commands' `needs.config` (`packages/cli-engine/src/execution/engine.ts:740-751`, whose comment says "whether it reaches the tree through a command family or on its own — the shell mounts its own commands with no family"). A check over families alone states a narrower rule than the engine enforces. Both inputs are already exported from `packages/cli/src/v8/cli.ts`: `mountedCommands` at line 170, and the families at 74 and 136. The set of standalone-command sections is empty today, which is exactly why the narrower rule would have looked correct. See STOP-8 on who should own this derivation. - -**The semantic** is R10, "Each product contributes a named section and a never-throwing validator". The engine treats a throwing validator as an internal bug: exit code 1, `CLI.INTERNAL_ERROR`, summary "'' config section validator threw" (`packages/cli-engine/tests/config.test.ts:818-840`). A validator that throws turns a user's config mistake into a CLI crash. - -**The corpus** is fixed and lives with the check: `undefined`, `null`, primitives of each type, empty and populated arrays, functions, `Symbol`, `NaN`, a frozen object, a null-prototype object, a deeply nested object, a self-referencing object, an object whose keys are the section's own field names with wrong-typed values, a `Proxy` whose `get` and `ownKeys` traps throw, and an object with a throwing getter. The throwing `Proxy` matters most: composer's validator spreads `raw` inside a `try`/`catch` specifically because of it (`section.ts:68-88`). - -**Does not assert** that a validator returns `ok: false` for bad input, or anything about diagnostic content. Whether garbage is refused or defaulted is the product's decision; not crashing is not. - -**Subjects available today:** one shipped validator, composer's `composer` section. - -### Check 3 — published-tarball verification - -Three assertions sharing a packing step. - -**3a — the tarball's declared dependencies match the built output.** Check 1's comparison run against the *packed* tarball rather than the working `dist/`, so `files`, `.npmignore` and the `workspace:` rewrite are in the measured path. - -**3b — the tarball installs outside the workspace and its bins start on plain Node.** Pack with `pnpm pack`, not `npm pack`: only pnpm rewrites `workspace:8.0.0-rc.1` to `8.0.0-rc.1`, and `npm pack` would ship a specifier no registry consumer can resolve, failing the check for a reason that is an artifact of the tool. Install into a sandbox with `npm install --no-audit --no-fund --ignore-scripts`, then run each bin with plain `node --version`-style invocation under a timeout, requiring a zero exit. - -`--ignore-scripts` is not optional. Without it the install executes third-party postinstall scripts — measured: `esbuild`, `workerd`, and `msgpackr-extract`'s `node-gyp-build-optional-packages`. This repo deliberately disables two of those three in `pnpm-workspace.yaml:5-11`, and the publish runner holds `id-token: write` and `contents: write`. Running vendor postinstalls and compiling native code there inverts the repo's own policy at its most privileged moment. Verified: with `--ignore-scripts`, both `dist/cli.js` and `dist/v8/cli.js` still start at exit 0. - -Unpublished workspace siblings must be supplied locally. `@prisma/cli-engine@8.0.0-rc.1` is not on the registry — `latest` is `0.0.9` — so an unaided install of the shell's tarball fails on a missing version, the normal state of a lockstep release before its publish step. The sandbox maps each such sibling to its own packed tarball through npm `overrides`, with absolute `file:` paths (a relative path in a nested override resolves against `node_modules`, not the sandbox root) and **version-qualified keys** (`"@prisma/cli-engine@8.0.0-rc.1"`), for the reason 3c gives. **The override list is computed, not written**: for each packed dependency whose name matches a workspace package, map it to that package's tarball, recursing into its own workspace dependencies. Exactly one override is needed today, and hand-writing it would break silently the first time another private sibling becomes a runtime dependency. - -**Packing rebuilds the directory check 1 reads, so the checks are strictly ordered.** Both published packages declare `"prepack": "pnpm run build"`, and `packages/cli/tsdown.config.ts` sets `clean: true`. Verified: a sentinel appended to `packages/cli/dist/cli.js` is gone after `pnpm --filter @prisma/cli pack`. Neither `pnpm pack --ignore-scripts` (not a valid flag) nor `npm_config_ignore_scripts=true pnpm pack` suppresses it. So check 1 runs to completion before check 3 begins, they never run concurrently, and 3a reads only from the extracted tarball — never from a file list gathered before packing. - -**3c — the shell's engine pin agrees with every family it mounts.** Compared between manifests: the packed manifest's `@prisma/cli-engine` version against the same field in each family package's manifest. Three things it pins down, each of which an implementer would otherwise decide alone: - -- **`dependencies` only.** The packed manifest's `devDependencies` name `@repo/cli-telemetry` and `@repo/tsconfig` at `8.0.0-rc.1` — private packages at versions no registry has. Reading any field but `dependencies` produces nonsense. -- **String equality, not semver satisfaction.** Both sides pin exactly today (`8.0.0-rc.1` and `0.0.9`). A family pinning a range is out of scope and reported as its own finding rather than silently resolved, because the release requirement is a matching pin, not a compatible one. -- **The family package list is an input.** `CommandFamily` carries `configSection`, `commands`, `docsBaseUrl` and `redirects` and no package identity (`packages/cli-engine/src/command-family.ts:49-59`), so the check cannot learn a family's package name from the family. It takes `familyPackages: readonly string[]`, and a test asserts every name in it appears in the shell's packed `dependencies` — which is what keeps it honest, since a family the shell mounts must be a package the shell depends on. - -3c additionally asserts that the installed family version equals the version the shell's packed manifest declares, so a lockfile disagreeing with the declared pin is itself a finding. - -**Revision 2 correction: 3c *can* also be measured in the install tree, and the earlier claim that it could not was wrong.** Revision 1 reported that npm's `overrides` erases the divergence, having tested a blanket override and one scoped to `@prisma/cli`. The architect review pointed out the third form. A version-qualified key replaces only the matching request, and it preserves the divergence — verified: - -```text -overrides: { "@prisma/cli-engine@8.0.0-rc.1": "file:/prisma-cli-engine-8.0.0-rc.1.tgz" } - -$ npm ls @prisma/cli-engine --all -`-- @prisma/cli@8.0.0-rc.1 - +-- @prisma/cli-engine@8.0.0-rc.1 - `-- @prisma/composer@0.6.0-dev.16 - `-- @prisma/cli-engine@0.0.9 # preserved -``` - -So "exactly one engine copy resolves in the installed tree" is available, and it is the stronger statement: it catches a mismatch introduced by a family's own transitive dependency, which a pairwise manifest comparison cannot see. It costs nothing extra because 3b already performs the install. The manifest comparison is still worth keeping — it works offline, it runs before any install, and it names both versions and both packages in the finding, which a copy count cannot. STOP-5 asks whether to take both. - -**Two further assertions that are non-vacuous today**, and stay meaningful while §5's exception is in place: the packed shell manifest's engine pin is an exact version with no `workspace:` prefix or range operator left in it, and every mounted family package declares the engine at all. - -**Does not assert** that the tarball's dependencies exist on the registry, anything about tarball size or `files` contents beyond what 3a and 3b touch, or that a bin does anything useful beyond starting. - -**What 3b proves today is narrow, and STOP-6 is where that is decided.** `@prisma/cli`'s only declared bin is `prisma-cli` → `dist/cli.js`, the legacy commander shell, and `exports` contains only `./package.json`. Of the packed output only `dist/v8/cli.js` imports `@prisma/composer/family`; `dist/cli.js` imports it nowhere. So starting every entry in the packed `bin` map starts the shell S2d is retiring, never loads composer's family, never resolves the `/family` subpath, and never exercises the engine boundary this slice exists to protect. - -**Feasibility proven.** In this worktree: `pnpm pack` of both packages, an out-of-workspace sandbox, `npm install` resolving 438 packages (440 with both engine copies) in 37 s cold and 12-13 s warm, and `node node_modules/@prisma/cli/dist/cli.js --version` printing `prisma-cli 8.0.0-rc.1` at exit 0. - -## 4. Shape and mechanics - -- The checker is a package in prisma-cli. Name and publication status depend on STOP-4. -- Checks are pure functions over injected inputs; the repo-facing entry supplies the subject list. No `vi.mock`/`vi.doMock` — satisfied by construction, not discipline. -- Tests before implementation. Each check gets a failing-input test proving it reports the defect it exists for, and a passing-input test, before the check is written. -- Fixtures live inside the repo. 3b's sandbox is the exception, and **revision 2's stated reason for it was wrong**: the principal-engineer review ran the install inside the worktree and resolution defeated nothing — 438 packages, the override applied, the bin starting at exit 0. The real blocker is corepack. `corepack enable` runs in `.github/workflows/test.yml:36-41`, and the corepack `npm` shim walks up past the sandbox's own manifest to the repo root, finds `"packageManager": "pnpm"`, and refuses. So an in-repo sandbox breaks wherever corepack is enabled — CI, and any developer who has run `corepack enable`. `COREPACK_ENABLE_STRICT=0` also lifts it. STOP-5 asks which way to go. -- The checker's entry is reached through a turbo task, not a bare script: `"conformance": { "dependsOn": ["^build"], "cache": false }`. The checks read two built directories, and neither existing workflow guarantees them — `pr-quality.yml`'s Test job never runs `pnpm build`, and turbo's `test` task depends on `^build`, which excludes `@prisma/cli`'s own build. A turbo task makes the dependency the graph's problem instead of a step-ordering convention nobody can see. -- **The checker depends on nothing it checks, and the consumers run it on themselves.** This is a correction to revisions 1 and 2, which had `@repo/cli-conformance` depend on `@prisma/cli` so it could reach the shell's families. Built that way, its own `tsc --noEmit` followed the import into the shell's whole command tree, duplicating the shell's typecheck and making it depend on the engine's built declarations being present and settled — which failed in practice. So: the checker declares the one shape it needs structurally (a section with a `name` and a `validate`, which the engine's `ConfigSection` satisfies) and depends only on `es-module-lexer`. `@prisma/cli-engine`'s own suite checks its built output; `@prisma/cli`'s suite checks its built output and its mounted sections, reaching the families by relative import of `../src/v8/cli` — the convention that package's tests already use. Each subject is checked by the package that owns it, in a tree it already typechecks. -- The checker exports subpaths rather than a root barrel, because `performance/noBarrelFile` is on and the only sanctioned exceptions are tsdown entrypoints. -- `@prisma/cli` cannot be imported by name from anywhere: its `exports` map carries `./package.json` and nothing else, and its built v8 entry runs the CLI at top level rather than exporting anything. That is why the shell's own tests, not an external runner, are where its subjects are reached. -- `es-module-lexer` is the parser, matching prisma/prisma's precedent (`packages/0-config/tsdown/shell-build.ts` uses it for the same job). Already in this repo's graph via tsdown; the checker declares it directly. -- pnpm only, except `npm install` and the bins started inside 3b's sandbox. - -## 5. Questions — CLOSED (operator, 2026-08-12) - -The nine questions revisions 1–3 carried are all closed. The record, in the operator's words where they were short enough to quote: - -1. **"Both products" = prisma-cli and prisma/prisma, both in scope NOW.** Revisions 1–3 claimed prisma/prisma had no engine; that was a stale checkout. S5 has landed on its origin/main: engine `0.0.9` pinned in `packages/1-framework/3-tooling/cli`, `packages/9-public/@prisma/orm-toolchain` and `test/integration`; the `orm` config section at `packages/1-framework/3-tooling/cli/src/orm/config-section.ts`. Every check has real subjects there. The project plan had already answered this ("wired into both products' publish CI as S3/S5 land"). -2. **No replace-or-duplicate dilemma exists.** The mandate is to add the three checks to both publish paths; other repos' existing checks were never in question. -3. **The composer pin mismatch: "Doesn't matter. Just ignore for now."** Implemented as one recorded exception (keyed on the observed triple) so the finding stays visible and any new mismatch fails. -4. **No second repo consumes the tool.** Per-repo check scripts are the standing precedent (`check-publish-deps.mjs` exists separately in composer and prisma/prisma). prisma/prisma gets the checks in its own repo; nothing is published. -5. **Sandbox and pin-check form: as recommended** (in-repo gitignored sandbox, both pin-check forms) — subsumed by the above; no ruling was needed. -6. **Bins: start what the tarball declares.** The v8-entry question was overthought and is withdrawn; the check starts the packed manifest's `bin` entries. -7. **The missing citation: "I don't care."** Closed; 3c is built to `deferred.md:37-51`. -8. **Validator-check ownership / engine export: withdrawn.** The checker derives the section union itself; no engine change. -9. **Registry outage: dead question.** If the registry is down the publish cannot proceed anyway; an install failure is a blocking finding. - -## 6. Acceptance - -Written against the §5 rulings. - -- [x] Check 1 runs against `@prisma/cli` and `@prisma/cli-engine` built output and passes; tests prove it reports an undeclared bare import, reports a declared-but-unimported runtime dependency, does not report `import.meta.resolve("@repo/cli-telemetry/sender")`, and reports a finding on an empty subject set. -- [x] Check 2 runs over the union the engine uses — families plus standalone mounted commands' `needs.config` — for every section the shell mounts, over the full hostile corpus; tests prove it reports a validator that throws on a `Proxy` trap, reports a malformed return, and reports a finding on an empty section list. -- [x] Check 3a/3b/3c run against both published packages: declared dependencies match packed output; the tarball installs into the sandbox and every declared bin starts on plain Node at exit 0; pin agreement holds modulo §5's exception; exactly one engine copy resolves in the installed tree; the packed engine pin is an exact version; every mounted family package declares the engine. -- [x] A test proves 3c reports the live shell-versus-composer mismatch when the exception list is empty, and that the exception does not suppress the same family arriving at a third version. -- [x] prisma-cli's `publish.yml` runs the checker before both publish steps, under the same `publish == 'true'` condition its neighbours carry. -- [x] Both products' publish paths run the checks: prisma-cli's `publish.yml` (PR #161) and prisma/prisma's `publish.yml` (prisma/prisma#29998, `scripts/check-conformance.mjs` after `check:publish-deps`). The project DoD line becomes true when both merge. -- [x] `pnpm typecheck`, root `pnpm lint`, and the touched packages' suites green, each measured as pnpm's own exit code, with `wip/` stashed inside the worktree rather than in a temp directory. - -## 7. Disposition record - -Rev 1 (2026-08-12): shaping work, seven questions, contract + plan opened as draft PR #161. - -Rev 2 (2026-08-12): architect review returned ACCEPT-WITH-CHANGES with 15 findings; all adopted. - -- Findings 1, 2, 4, 5, 7, 8 changed what the checks assert: check 2's subject set widened to the engine's own union; the `commandFamilies` export justified for check 2 and *not* for 3c, which needs package identity `CommandFamily` does not carry; the exception keyed on the observed triple; STOP-6's recommendation reversed to (b) on the evidence that the declared bin never loads composer; anti-vacuity requirements added to every check; "reachable" struck from check 1 with the subpath limitation stated. -- Finding 3 corrected a factual error: the version-qualified override form preserves the pin divergence, so 3c *can* be measured in the install tree. Verified before adopting. STOP-5 rewritten from "confirm my proof" to "take the stronger form as well?". -- Finding 9 corrected a process error: revision 1's plan closed the DoD line by rewording it. Now left unchecked with dependencies recorded, and surfaced in STOP-2. -- Finding 6 moved the scope split: only D1 and D2 are ruling-independent. The plan states that instead of claiming D1-D4. -- Finding 10 fixed in code: `packages/cli/src/v8/cli.ts:9-15` said composer `dev.15` pins engine `0.0.7`; the real versions are `dev.16` and `0.0.9`. -- Findings 11, 12, 13, 14, 15 fold during implementation: an explicit build before the check in `pr-quality.yml`; the `publish == 'true'` condition on the new publish step; 3c's manifest source named and the installed-versus-declared assertion added; the `wip/` stash moved inside the worktree, per the operator's own standing rule; the two extra non-vacuous 3c assertions claimed. -- New question raised by the review and added: STOP-8, on who owns check 2 and whether the engine should export the section derivation. - -Rev 3 (2026-08-12): principal-engineer review returned ACCEPT-WITH-CHANGES with 20 findings; all adopted. Checks 1 and 2 built to it. - -- **Finding 16 changed the checks' ordering, and it is the one that would have broken the slice quietly.** Both published packages declare `"prepack": "pnpm run build"` and tsdown cleans, so `pnpm pack` destroys and recreates the very directory check 1 reads. Verified with a sentinel: appended to `packages/cli/dist/cli.js`, gone after packing. Neither `pnpm pack --ignore-scripts` nor `npm_config_ignore_scripts=true` suppresses it. The checks are now strictly ordered, 1 before 3, never concurrent, and 3a reads only the extracted tarball. -- **Finding 4 is a security correction.** The sandbox install was to run without `--ignore-scripts`, which executes `esbuild`, `workerd` and `msgpackr-extract` postinstalls — two of which this repo deliberately disables in `pnpm-workspace.yaml` — on a runner holding `id-token: write`. Now `--ignore-scripts`, verified not to break either bin. -- **Finding 3 corrected the second of my two wrong reasons.** The sandbox does not need to leave the repo because of pnpm resolution; the review ran it in-repo successfully. The blocker is corepack's npm shim refusing inside a tree whose root declares pnpm. STOP-5 gained that choice, and now recommends the in-repo path with `COREPACK_ENABLE_STRICT=0` so a failed run leaves evidence. -- **Finding 1 replaced the root script with a turbo task.** `tsx` removes the build-ordering requirement for the checker itself, not for the two `dist/` directories it reads — and neither workflow builds them (`pr-quality.yml`'s Test job never runs `pnpm build`; turbo's `test` task depends on `^build`, which excludes `@prisma/cli`'s own). A `conformance` task with `dependsOn: ["^build"]` makes that the graph's problem and removes the need for a step condition. -- Findings 2, 5, 11, 12 pinned down what an implementer would otherwise decide alone: the relative-source-import route to the shell's families and why it is the only one; the override list computed rather than written; 3c's field set, comparison rule and `familyPackages` input; and `allowedUnimported`, without which a dependency reached only through `import.meta.resolve` fails check 1's reverse half — the shipped telemetry pattern exactly. -- Findings 9, 10, 8a supplied the shapes the plan was missing: `Finding`, `Report`, `exitCodeFor` (zero only when every finding is suppressed, and suppressed findings still print), and the `TarballIo` seam that keeps check 3 testable without mocking `node:child_process`. -- Findings 6, 17 became STOP-9 and a specified bin invocation: a registry outage's verdict is the operator's call, and a bin start needs an argument and a timeout rather than "run it and require zero". -- Findings 7, 13, 14, 15, 18, 19 fold during implementation: sandbox deleted at the start of a run rather than the end; `await init` before `parse`, and computed dynamic imports silently invisible; the lockfile regenerated (done); a manifest type rather than `any`; exit codes never stderr content; and the four lint rules that will bite. -- **Finding 20 deleted a step I had copied from the S5 brief.** Biome already honours `.gitignore`, so moving `wip/` aside before linting does nothing — and it moved it to `/tmp`, which the operator's standing rule forbids. Verified: biome reports `wip/` paths as ignored. Gone from the plan. -- Recorded for `deferred.md`, not fixed here: the packed shell manifest's `devDependencies` name `@repo/cli-telemetry` and `@repo/tsconfig` at versions no registry has. Harmless for a tarball install, fatal for anyone installing the unpacked directory, and none of the three checks looks at that field. composer's `check-publish-deps.mjs` catches this class. diff --git a/.drive/projects/prisma-cli-v8/specs/s7-release.md b/.drive/projects/prisma-cli-v8/specs/s7-release.md deleted file mode 100644 index ddc2591f..00000000 --- a/.drive/projects/prisma-cli-v8/specs/s7-release.md +++ /dev/null @@ -1,328 +0,0 @@ -# S7 — Release pipeline + rc1 (slice contract, revision 3 — all STOPs closed) - -Status: revision 2 applies the operator's ruling (2026-08-12): **rc1 -publishes under the existing names** — `@prisma/cli` with its existing -`prisma-cli` bin — and the cutover to the bare `prisma` npm name is a -follow-up piece of work, recorded in `deferred.md`, not this slice. -Former STOP-2 (what rc1's one action is, given the name), STOP-3 (the -`prisma` package's shape) and STOP-8 (`prisma` publish credentials) are -closed by that ruling and their sections record the disposition. -Second ruling (operator, 2026-08-12): **the slice's goal is combining -all available commands into one binary**; the bare-`prisma` cutover is a -following step, and so is reconciling the missing/discrepancy commands -behind the grammar exception list — STOP-4 closes with the current -exception set standing as-is. D1 and D2 are unblocked and in -implementation. STOP-1 and STOP-5…7 remain open for the release-side -deliverables; D1/D2 proceed on the recorded working defaults. -Precedence: this contract > `specs/s2-overview.md` standing rulings > source. -Unpinned facts are STOP-and-surface. - -Repo: prisma-cli. Branch: `claude/s7-release-pipeline-rc1-92c89d`, base `main`. - -Mandate (project plan §S7, spec.md FR10, project DoD): the unified binary -package assembled — full grammar tree mounted behind a build-time -completeness check, committed-versions release automation (R11), and a -pipeline that emits a publishable rc1 artifact which the operator -publishes with one action. Under the 2026-08-12 ruling the rc1 artifact -is `@prisma/cli@8.0.0-rc.N` (bin `prisma-cli`, the v8 tree); the bare -`prisma` name follows later. - -## 1. The grounding example - -Today, a user who wants the unified CLI cannot get it: - -```text -$ npm install -g prisma@8.0.0-rc.1 -npm error notarget No matching version found for prisma@8.0.0-rc.1 -# `prisma` on npm is the v7 train, published by prisma/prisma. - -$ npm install -g @prisma/cli@latest && prisma-cli migrate -# latest is 3.0.0-beta.30 — the pre-v8 platform CLI. No ORM commands. -# Nothing 8.x has ever published from this repo: the workspace says -# 8.0.0-rc.1, npm's newest @prisma/cli-engine is 0.0.9. -``` - -And even at HEAD, the assembled tree is not assembled: `@prisma/cli`'s one -declared bin is `prisma-cli` → `dist/cli.js`, the legacy commander shell. -The v8 tree builds to `dist/v8/cli.js`, undeclared in `bin`, and mounts -platform + composer only. The ORM family — 21 commands, published today in -`@prisma/orm-toolchain@8.0.0-rc.1-dev.40` under the `./cli` subpath — is -mounted nowhere. `prisma migrate`, `prisma db verify`, `prisma init` do not -exist in any binary this repo ships. - -After this slice: a release commit produces, in CI, a verified -`prisma-cli-8.0.0-rc.N.tgz` whose `prisma-cli` bin is the v8 tree, which -answers every platform, composer, and ORM command, whose product pins -agree on one engine, and which installs and starts on plain Node outside -the workspace. The operator performs one deliberate action to release it. -The bare `prisma` name is follow-up work once `prisma7` frees it. - -## 2. What exists today (the facts the deliverables build on) - -- **The mount and its check.** `packages/cli/src/v8/cli.ts` exports - `platformCommandFamily`, `composerCommandFamily`, `cliGroups`, - `mountedCommands`, and `buildCli()`. `packages/cli/tests/ - v8-mount-coverage.test.ts` already asserts both completeness directions — - every family command mounted, every mounted command family-owned — with a - deliberate `FAMILYLESS` exception set (the engine's three `telemetry` - commands, `agent install|update|status`, `feedback`) and an explicit - expected-paths list. It runs in the test suite only; the publish path - runs `pnpm build` + `pnpm test:scripts` and would ship a tree this test - has never seen. -- **The ORM family is importable now.** `@prisma/orm-toolchain@ - 8.0.0-rc.1-dev.40` (dist-tag `dev`, published 2026-08-12) exports - `ormCommandFamily` and `ormConfigSection` from `./cli`. The family - carries `configSection`, `docsBaseUrl`, and the verb/flag `redirects` - (R-S5-23), and keys its 21 commands by full mount path (`"contract - emit"`, `"db update"`, `"migration status"`, `init`, `format`, `migrate`, - `lsp`, `ref set|list|delete`, …). Its `dependencies` pin - `@prisma/cli-engine: "0.0.9"`. Note the tag: nothing on the rc line of - orm-toolchain is published as `latest` yet. -- **Import weight.** orm-toolchain's `dist/cli.mjs` statically imports - `esbuild`, `arktype`, and eight `@prisma/orm-framework` subpaths. - Mounting the family pays that import on every shell invocation, - including `prisma --version`. Composer's family was built to keep its - heavy graph behind dynamic executor imports; the ORM family was not. - Recorded here as a known cost and a candidate upstream fix, not a - blocker (STOP-9 lists it for the operator's awareness). -- **The versioning model is settled and tagless.** `docs/oss/versioning.md` - (ported from prisma/prisma by ruling 2026-08-10): the root - `package.json` `version` is the single source of truth; `pnpm - bump-version` writes lockstep; a push to `main` that changes the root - version publishes `latest`; **merging the bump PR is the deliberate act - that moves `latest`**; `workflow_dispatch` re-publishes or dry-runs. The - GitHub Release (and its `v8.0.0-rc.N` tag) is created BY the publish - run, after npm. `publish.yml` even excludes tag pushes (`tags: - ["!**"]`). There is no tag-triggered path anywhere, by design. -- **The engine pins disagree, knowably.** The shell ships the workspace - engine at `8.0.0-rc.1` (unpublished); `@prisma/composer@0.6.0-dev.16` - and `@prisma/orm-toolchain@…dev.40` both pin `0.0.9`. `deferred.md` - rules matching pins "a release requirement for the tandem release" and - the two-copy install "a preview-only state to end". rc1 is precisely the - release that requirement was written for. -- **S6 is specified, partially built, unmerged.** PR #161 carries - `specs/s6-conformance.md` (rev 3): checks 1–2 built on that branch; - check 3 (tarball verification: 3a declared-deps-vs-built-output, 3b - out-of-workspace npm install with computed `file:` overrides and - `--ignore-scripts`, bins start on plain Node, 3c cross-repo engine-pin - agreement + single-engine-copy resolution) is fully designed but awaits - the operator's STOP-1…STOP-9 rulings there. Its 3b IS the "S5-era - smoke" this slice's mandate names; its 3c IS the pin verification R11 - needs. S6's own STOP-4 and STOP-6 explicitly defer to S7 for publish- - list changes and for the `prisma` bin appearing in the packed bin map. -- **The `prisma` npm name is not ours yet.** Rollout plan step 4: the name - frees only when the ORM team ships `prisma7`; then this repo configures - OIDC trusted publishing for `prisma` and publishes `8.0.0-rc1` under a - pre-release dist-tag. That timing is an open item the operator owns. - `prisma-next` handoff (step 3) is likewise an operator-owned cross-repo - cutover. -- **S8 is changing the tree this slice checks.** PR #162 - (`s8-service-primitives`) adds `service create|list|delete`, the - `service deployment` subgroup, and the lifecycle verbs — all inside the - platform family, so the completeness check keeps passing across that - merge in either order; only the expected-paths list and `cliGroups` - entries collide textually. Coordination is STOP-6. - -## 3. Deliverables - -**D1 — Mount the ORM family.** `packages/cli` gains a `dependencies` entry -on `@prisma/orm-toolchain` at an exact version (R-S5-28 direction; interim -version per STOP-7). `cli.ts` imports `ormCommandFamily` from -`@prisma/orm-toolchain/cli`, adds it to `commandFamilies`, spreads its 21 -commands into `mountedCommands` at the family's own paths, and adds the -`contract`, `db`, `migration`, `ref` group briefs to `cliGroups`. The -family's config section and redirects ride in via the family object; no -per-command wiring. Semantic tests through `createTestCli`: one ORM -command end to end in the shell (`prisma migration list` against a -fixture project is the candidate), redirect resolution (`prisma migration -apply` → typed redirect), section validation reachable, and `--help` -naming all four new groups. No `vi.mock`. - -**D2 — The completeness check fails the build.** Extend -`v8-mount-coverage.test.ts` with the ORM family in `MOUNTED_FAMILIES` and -the 21 new expected paths. Promote the check out of "just a test the -publish never runs": a `check:grammar` invocation (the same test file run -via vitest, not a parallel implementation) wired as a turbo task with -`dependsOn: ["^build"]`, run by `pr-quality.yml` AND by `publish.yml` -before any publish step, under the same `publish == 'true'` condition its -neighbours carry. The exception set is ratified, not grown: STOP-4 puts -the current `FAMILYLESS` list in front of the operator; adding to it after -this slice requires a ruling recorded in the file. - -**D3 — The shipped bin becomes the v8 tree.** Ruled 2026-08-12: no -`prisma` package this slice. Instead, `@prisma/cli`'s declared bin -(`prisma-cli`) moves from `dist/cli.js` (the legacy commander shell) to -`dist/v8/cli.js` (the engine tree) — an 8.x version whose bin is the -retiring commander would misdescribe itself. The legacy entry keeps -building and shipping inside the tarball (its deletion is S2d, out of -scope); only the bin map changes. The packaging test proves the declared -bin prints the lockstep version on plain Node at exit 0. The bare-name -cutover (a `prisma` package or bin rename, OIDC trusted publishing for -the name, dist-tag choice) is recorded in `deferred.md` as follow-up -work blocked on `prisma7`. - -**D4 — Committed-versions release automation (R11).** All product pins in -committed manifests, bumped only in PRs: `@prisma/orm-toolchain` and -`@prisma/composer` exact-pinned in `packages/cli/package.json`; -`@prisma/cli-engine` stays `workspace:` (pnpm rewrites to exact -at pack). No publish-time resolution anywhere — `determine-version.ts` -already refuses to invent versions; this deliverable adds nothing dynamic. -Pin agreement (shell engine version == every mounted family's engine pin) -is verified by S6's 3c, wired per STOP-5 — S7 does not write a second pin -checker. - -**D5 — The pipeline: release commit → verified artifact → one action.** -Per STOP-1's ruling on trigger shape. Written against the recommendation -(STOP-1a): `publish.yml` gains an artifact-emission stage — after `pnpm -build` and the grammar check, it packs the engine and cli tarballs with -`pnpm pack`, runs the tarball smoke (S6 check 3b mechanics: -out-of-workspace install with computed absolute `file:` overrides, -`--ignore-scripts`, every declared bin starts on plain Node, exit 0, -under a timeout — which after D3 means the smoke exercises the v8 tree -and the composer/ORM family boundary), uploads the tarballs as workflow -artifacts, and attaches them to the GitHub Release it already creates. -The operator's one action for rc1 stays what versioning.md already -rules: merge the `chore(release)` bump PR. Everything after that push — -build, grammar check, smoke, npm publishes, Release + tag, artifact -upload — is the pipeline. Dry-run dispatch exercises all of it minus -registry writes. - -**D6 — Docs and records.** `docs/oss/versioning.md` gains the `prisma` -package and the artifact stage; `rollout-plan.md` step 4 updated to point -at the built pipeline; `plan.md` §S7 marked with what shipped; -`deferred.md` updated where this slice closes or supersedes entries (the -two-copy engine install entry closes when STOP-7's convergence lands). -Divergence file: mounting previously-unreachable commands is not a -divergence from a shipping CLI, but `assets/s2/parity-divergences-s7.md` -records anything user-visible this slice changes in already-shipped -surfaces (expected: none; the file says so explicitly if so). - -## 4. Open questions (STOP) - -**STOP-1 — CLOSED (operator, 2026-08-12).** Option (a): the repo's existing publishing mechanisms stand. The mandate's "tagged commit" wording is set aside; the release commit is the merged bump PR, the pipeline tags it after publishing, and no tag-triggered path exists. D5 is built inside `publish.yml`. - -**STOP-2 — CLOSED (operator, 2026-08-12).** rc1 publishes under the -current names; the one action is merging the bump PR, exactly as -versioning.md rules. The bare-`prisma` cutover is follow-up work blocked -on `prisma7`, recorded in `deferred.md`. The project DoD's -"prisma@8.0.0-rc1" reads as "the unified CLI's rc1", which under this -ruling is `@prisma/cli@8.0.0-rc.N`. - -**STOP-3 — CLOSED (operator, 2026-08-12).** No `prisma` package this -slice. D3 is now the bin flip: `prisma-cli` → the v8 entry. The legacy -commander stays in the tarball until S2d. The follow-up cutover work -owns the package/bin naming question when the name frees. - -**STOP-4 — CLOSED (operator, 2026-08-12).** The current exception set -stands: the engine's three `telemetry` commands, `agent -install|update|status`, `feedback`. Reconciling missing/discrepancy -commands behind the exception list is a following step, recorded in -`deferred.md`, not this slice. Additions to the set still require an -operator ruling recorded in the test file. - -**STOP-5 — CLOSED (operator, 2026-08-12).** Option (b): S6 proceeds in parallel and S7 carries the install smoke itself (`scripts/tarball-smoke.mjs` + `tarball-smoke-utils.mjs`), written to S6's check-3b design — same override computation, same `--ignore-scripts` stance, sandbox outside the workspace — so S6 absorbs it as a move, not a rewrite. - -**STOP-6 — CLOSED by events (2026-08-12).** #162 merged first; this branch carries main's S8 merge, and the mount-path collision resolved as the predicted textual union. The original question, for the record: - -PR #162 adds platform-family commands, so -the completeness check is order-independent with S7 — but -`EXPECTED_MOUNT_PATHS`, `cliGroups`, and `v8/cli.ts` imports collide -textually in both orders. Preference? (a) S7 lands first, #162 rebases -(its adds are mechanical); (b) #162 first, S7 rebases (same cost, -S7 is the one branch currently unmerged everywhere). Either is fine; -the mandate tells me not to decide interactions with S8's tree alone. - -**Working defaults while STOP-5…7 stay open** (recorded so D1/D2 can -proceed; override any of them): the interim `@prisma/orm-toolchain` pin -is `8.0.0-rc.1-dev.40`, exact and committed (STOP-7 ii); S8's #162 and -this branch rebase in whichever order you merge them, the collision is -textual only (STOP-6); no S6 mechanism is duplicated in D1/D2 — the -smoke question only arises at D5 (STOP-5). - -**STOP-7 — DEFERRED (operator, 2026-08-12).** The convergence choreography below is set aside until `8.0.0-rc.1` actually publishes; the committed interim pins (orm-toolchain `8.0.0-rc.1-dev.40`, composer `0.6.0-dev.16`, both carrying engine `0.0.9` beside the workspace engine) stand, and the two-copy install remains the accepted preview state. The original question, kept for that day: - -For rc1's -install to resolve ONE engine, before the rc1 bump PR merges: -prisma-cli publishes engine `8.0.0-rc.N` (the existing lockstep publish -does this — note it publishes `@prisma/cli` in the same run, which is -fine: rc respins are cheap and `latest` moving is your merge); then -orm-toolchain and composer bump their `@prisma/cli-engine` pins to that -exact version and publish; then prisma-cli's bump PR pins those exact -product versions and rc1 ships with agreeing pins, S6-3c green with an -empty exception list. Two of those three moves are in repos this slice -does not own. Confirm: (i) you own/sequence the orm-toolchain and -composer pin-bump publishes (same model as the S3 tandem glue), (ii) -until then, S7 development pins `@prisma/orm-toolchain@8.0.0-rc.1-dev.40` -(a `dev`-tag version — acceptable for a committed interim pin, or do you -want an rc-line orm-toolchain published first?), and (iii) the S6-3c -exception list carries the interim triple, dated, as S6's STOP-3a -designed. - -**STOP-8 — CLOSED (operator, 2026-08-12), by dissolution.** No `prisma` -name is published this slice, so no new publish credentials, trusted- -publisher config, or guarded steps exist. `@prisma/cli` and -`@prisma/cli-engine` keep their existing OIDC configuration untouched. -The credentials question moves wholesale into the deferred cutover work. - -**STOP-9 — acknowledged, not asked.** (i) `prisma-next` handoff (rollout -step 3) stays out of S7 unless you say otherwise. (ii) The ORM family's -static `esbuild`/`arktype` import cost on every shell start is real, -measured at the import graph, and belongs to prisma/prisma to fix -(dynamic handler imports like composer's); recorded in deferred.md by -this slice. (iii) rc1 ships with the legacy commander tree still in -`@prisma/cli`'s tarball; its deletion is S2d, which remains open on the -plan. - -## 5. Acceptance - -Written against the final rulings (2026-08-12): STOP-1 keep the existing publish model, STOP-2/3/8 as applied, STOP-4 as listed, STOP-5(b) — the smoke lives in this slice, so "conformance" below means the inline tarball smoke, not S6's checker — and STOP-7 deferred, so pin agreement is NOT an acceptance item; the interim pins stand until `8.0.0-rc.1` publishes. A later ruling (same day) sends RC-line releases to the `next` dist-tag; the one-action item reads accordingly. - -- [x] `prisma migration list`, `prisma db verify --help`, `prisma init - --help`, `prisma migrate --help` answer from the assembled tree; - one ORM command proven end to end through `createTestCli`; the - family's redirects and config section reachable through the shell. -- [x] The completeness check covers platform + composer + ORM families - both directions, fails the build on a seeded omission in either - direction (test proves it), runs in `pr-quality.yml` and in - `publish.yml` before any publish step, and its exception list is - exactly the ratified one. -- [x] `@prisma/cli`'s declared `prisma-cli` bin is the v8 tree; the - packed tarball's bin prints the lockstep version on plain Node at - exit 0. -- [x] All product pins exact and committed; no publish-time version - resolution. (Pin AGREEMENT is deferred with STOP-7; S6-3c arrives - with S6.) -- [x] A release run (dry-run dispatch proves it end to end without - registry writes) produces: build → grammar check → conformance → - pack (engine + cli tarballs) → out-of-workspace install smoke - (every bin starts on plain Node, exit 0) → publish steps → - GitHub Release with tarballs attached. -- [x] The operator's release action is exactly one: merging the - `chore(release): 8.0.0-rc.N` PR. Nothing between that merge and - the published artifacts requires a human. -- [x] `pnpm typecheck`, root `pnpm lint`, touched suites green, measured - as pnpm's own exit codes. - -## Close-out (2026-08-12) - -Acceptance verified against source, merged PRs, and the registry: prisma-cli #164 (the slice, squash-merged `c5fe09d`) and #166 (the Release-immutability fix). Evidence, per item: the ORM mount and its end-to-end proof are `packages/cli/tests/v8-orm-mount.test.ts` (real `migration list` run, redirect settlement, section validation, group help) with every mount path written out in `packages/cli/src/v8/cli.ts` (operator review: the bin is the source of truth for mount points, R12); the completeness check's both-directions failure proof is the constructed-family suite at the bottom of `packages/cli/tests/v8-mount-coverage.test.ts`, and the check runs as the `Grammar Completeness` job in `pr-quality.yml` and as `pnpm check:grammar` in `publish.yml` before any publish step; the declared-bin proof is `packages/cli/e2e/declared-bin.e2e.ts` (manifest-read bin, plain Node, bare env, envelope-asserted); the install smoke is `scripts/tarball-smoke.mjs` with its override computation unit-tested in `scripts/tarball-smoke-utils.test.mjs`; the release-tag rule is `releaseDistTag` in `scripts/determine-version-utils.ts`. Two dry-run dispatches (runs 31601868449, 31602392124) proved the pipeline without registry writes. - -The slice was also proven by fire the same day: the operator performed the first real publish. `@prisma/cli-engine@8.0.0-rc.1` and `@prisma/cli@8.0.0-rc.1` are live on npm under `next` with `latest` untouched — the project's DoD artifact exists, published. Two incidents from that run, both now handled: - -- **npm's trusted publisher for `@prisma/cli` still named the deleted `publish-cli.yml`**, so the OIDC token exchange 404'd; the engine (already configured for `publish.yml`) published, the CLI did not. The operator updated the registered workflow filename and re-dispatched; the rerun-tolerance in `publish.yml` (built for exactly this) carried the run past the already-published engine. -- **The GitHub Release published before its assets uploaded, and this repo's releases are immutable** — `v8.0.0-rc.1` froze assetless (HTTP 422 on upload). #166 reorders the step (draft with tarballs attached, then publish by the draft's id) so every future Release carries its assets. rc.1's Release stays assetless permanently; ruled cosmetic, repair path in `deferred.md`. - -One item shipped amended, deliberately: "GitHub Release with tarballs attached" holds for every release from #166 onward, not for `v8.0.0-rc.1` itself. - -A same-day ruling extended the slice beyond the contract: RC-line bump PRs publish under `next` and `latest` waits for a deliberate flip (an explicit `dist-tag: latest` dispatch is the cutover act). The rule, its Release condition, and the accident-proof dispatch default are in `scripts/determine-version.ts` / `releaseDistTag`, documented in `docs/oss/versioning.md`. - -Everything carried out of the slice is in `deferred.md`: the pin-convergence choreography (deferred until the products bump to engine `8.0.0-rc.1`), the assetless rc.1 Release, the ORM family's static import weight (prisma/prisma's to fix), and the offered-but-undecided extraction of the publish/Release shell into a tested script. The S5 cutover in prisma/prisma — the retirement this slice's mount made possible — has its own brief: `assets/briefs/s5-cutover-handover.md`. - -## 6. Out of scope - -The bare-`prisma` cutover (package, bin name, OIDC config, dist-tag — -follow-up work, ruled 2026-08-12, blocked on `prisma7`); S2d (commander -retirement in this repo); the `prisma-next` npm handoff; flipping any -`latest` at cutover (rollout step 5); S8's service tree (#162); fixing -the ORM family's static import weight (prisma/prisma); S6's checks 1–3 -themselves (consumed, not built, under STOP-5a). diff --git a/.drive/projects/prisma-cli-v8/specs/s8-services.md b/.drive/projects/prisma-cli-v8/specs/s8-services.md deleted file mode 100644 index 2931261b..00000000 --- a/.drive/projects/prisma-cli-v8/specs/s8-services.md +++ /dev/null @@ -1,197 +0,0 @@ -# S8 — Service primitives (slice contract) - -Status: CLOSED 2026-08-12 — shipped as PR #162 (squash `9730012`). -Acceptance verified line by line at closure (D4) and the one -by-convention line — `service create` against the real API — proven -on the PR: the e2e suite's first real run failed on a genuine defect -(the service tree's stale workspace filter, pre-#144 copy; fixed in -`bd8aa78`) and passed after it. One ruling still owed: `service -create`'s 409-idempotent semantics (divergence file marks it -operator-ruling-pending). Follow-ups in `deferred.md`. Was rev 1 -(2026-08-12) — design settled in operator discussion, 2026-08-12; -rulings in §Dispositions. One PR into `main`, branch -`s8-service-primitives`. Repo: prisma-cli only. - -Gives the platform's service resources the atomic CLI surface the -plan describes: the resource model the Management API already draws -(**Composer produces deployments; the CLI manages them**), replacing -the shape S2c ported for continuity. Mostly a rename plus filling -holes — the expensive parts (engine, auth, presenters, error model, -the `service` noun) are done. - -Normative sources and precedence as in `s2c-services.md`; S2b/S2c -mapping rules apply unchanged (namespace `SERVICE.*`). Unpinned facts -are STOP-and-surface. - -## The command tree - -```text -service list NEW GET /v1/apps -service create NEW POST /v1/apps -service show | open | remove as today -service domain add|show|remove|retry|wait as today -service deployment list was: service list-deploys -service deployment show was: service show-deploy -service deployment promote was: service promote -service deployment rollback was: service rollback -service deployment start NEW POST /v1/deployments/{id}/start -service deployment stop NEW POST /v1/deployments/{id}/stop -service deployment delete NEW DELETE /v1/deployments/{id} -``` - -## Mapping rules - -R-S8-1 **The `deployment` subgroup.** `list-deploys`, `show-deploy`, -`promote`, `rollback` move under `service deployment` as -`list|show|promote|rollback`. The old spellings are DELETED — no -aliases (ruled: v8 is pre-rc, there is no compatibility debt; each -rename is a divergence entry). All paths, ids, help, presenters. - -R-S8-2 **The five new commands**, all result commands on existing -endpoints, presenters in the established `service` style: - -- `service list` — `GET /v1/apps`, table shape per `project list` - precedent. -- `service create` — `POST /v1/apps`; args from the create body the - API takes and Composer/legacy both send (`displayName`, project, - optional region, optional branch — the four fields, nothing else; - grounded in `ComputeService.ts:94-105` and - `app-provider.ts:1037-1045`). Ends the state where a service can - only be born as a side effect of deploying to it. -- `service deployment start|stop` — `POST /v1/deployments/{id}/start` - / `stop`. The API states the artifact must be uploaded before - `start`; the failure maps to a structured error, not a precondition - the CLI invents. -- `service deployment delete` — `DELETE /v1/deployments/{id}`. - Destructive: consent prompt per the `service remove` precedent - (R-S2b-3). - -R-S8-3 **Two presenter corrections**, in files this slice rewrites -anyway (both wrong today for Composer-deployed services): - -- `service deployment show`'s `url`: for the LIVE deployment, the - promoted `appEndpointDomain` — Composer reports the promoted - address at deploy time, so the CLI must not show a different URL - than the deploy did (`app-provider.ts:735`; `listDeployments`'s - per-row preview domains at `:701` stay — identical promoted URLs - on every row would be wrong, and no presenter renders that field). - For a NON-live deployment, its own `previewDomain` — the promoted - domain does not serve it (amended after D1, 2026-08-12: the - original flat "always `appEndpointDomain`" violated its own - rationale for non-live deployments). -- Local CLI live-state is RETIRED in both directions: the read - fallback (`readKnownLiveDeployment`, `target.ts:400-448`) and the - writes (`setKnownLiveDeployment` in promote/rollback) go. `live` - derives from the service's `latestDeploymentId` alone. (Amended - after D1, 2026-08-12: the premise "only legacy `app deploy` wrote - that state" was falsified — v8 promote/rollback wrote it too; - nothing in v8 reads it, so the writes are dead and go with the - reads. Divergence entry.) - -R-S8-4 **No Composer-ownership note** (ruled, 2026-08-12): `promote`, -`rollback`, `start`, `stop` print no "Composer will overwrite this" -warning. Users have full ownership of their resources; Composer -reconciling manual changes on the next deploy is accepted behavior. -Grounding fact: nothing in the app/deployment records identifies a -service as Composer-managed anyway — the only fingerprint is the -`COMPOSER_*` env-var namespace on the branch, which the CLI never -fetches. Revisit later; not now. - -R-S8-5 **`service logs` stays shelved** (ruled, 2026-08-12). The -transport question is ANSWERED (API owners via operator, 2026-08-12): -**HTTP instead of WebSocket is acceptable, provided live streaming -can be added at a later date.** So no engine socket transport is -built — `service logs` returns as a copy of `build logs` (plain -HTTP, `parseAs: "stream"`) in a follow-up slice once the endpoint -serves HTTP. The engine WebSocket design -(`assets/engine/websocket-transport-design.md`) is shelved as the -later live-streaming path, not deleted. Nothing in this slice builds -the command; the shelved handler in the `s2c-services` history -remains the starting point. - -R-S8-6 **What this slice depends on.** Every endpoint above is -marked experimental in the Management API specification. The slice -depends on: `GET/POST /v1/apps`, `GET/DELETE /v1/apps/{id}`, -`GET /v1/apps/{id}/deployments`, `GET/DELETE /v1/deployments/{id}`, -`POST /v1/deployments/{id}/start|stop`, `POST /v1/apps/{id}/promote` -— the CRUD-and-lifecycle set, and deliberately NOT the logs -endpoint (the one known to carry a transport question). A breaking -change to any of these is absorbed at the provider layer -(`app-provider.ts`), not in command shapes. - -## Out of scope - -`service logs` and the engine WebSocket transport (R-S8-5); any -Composer-managed marker or API ask for one (R-S8-4); `service -deploy`/`service build`/`service run` (dropped in S2c, superseded by -Composer — not coming back); S7's grammar-tree completeness check -(this slice changes the tree; S7 checks it). - -## Pre-investigated edge cases - -- An app created but never promoted carries a placeholder - `appEndpointDomain` that does not resolve (`Deployment.ts:129-132`) - — `service show`'s `live url` and `service create`'s output must - not present a dead URL as live. Present the domain only when a - live deployment exists (`latestDeploymentId` set). -- `DELETE /v1/deployments/{id}` against the currently-promoted - deployment: ANSWERED during D3 (2026-08-12), from the control - plane's source (`pdp-control-plane`, - `packages/interactors/src/compute/deployment.ts:494-522` and - `tearDownDeployment.ts`; integration tests pin the order - detach-endpoint → stop → delete). The API permits it and handles - liveness by teardown, not refusal; `latestDeploymentId` is cleared - in the same transaction. No CLI-side guard. Consequence for the - divergence file: the server does NOT clear the service's - `endpointDomain`, so after deleting the live deployment the - service keeps a non-resolving domain — already neutralized in the - CLI because every S8 presenter reports a live url only when - `latestDeploymentId` is set. Caveat: read from a checkout one - minor ahead (SDK 1.56.0 vs the pinned 1.55.0); shape unchanged - across the drift. - -## Acceptance - -- [ ] The tree above is the whole `service` grammar: old spellings - gone, subgroup mounted, five new commands green through the - harness (byte-asserted presenters, envelope + exit codes, the - R-S2b-9 matrix where a command streams nothing). -- [ ] `service create` proven against the real API end to end - (e2e suite), including the no-region and no-branch defaults. -- [ ] Consent prompt on `service deployment delete` per the - `service remove` precedent. -- [ ] R-S8-3's two presenter corrections, pinned by test. -- [ ] Divergence entries (`assets/s2/parity-divergences-s8.md`): the - four renames, the deleted spellings, the `live` derivation - change, the `url` change. -- [ ] `deferred.md` updated: the ownership-note revisit (R-S8-4) and - the logs follow-up slice (R-S8-5) recorded. -- [ ] Suites green: `pnpm --filter @prisma/cli test`, - `pnpm --filter @prisma/cli-engine test`, `pnpm typecheck`, - `pnpm lint`. - -## Dispositions - -Design discussion (operator + architect lens, 2026-08-12), settling -the plan's four questions: - -1. Ownership note: NOT NOW (R-S8-4). Alternatives rejected: warn - unconditionally (noise on non-Composer services); ask the API for - a `managedBy` field (deferred with the revisit — no marker exists - today, verified against the create bodies and both record - schemas). -2. Records content: verified — Composer and legacy `app deploy` send - identical create bodies for apps and deployments; presenters - render nothing Composer fails to populate, except the two - corrections R-S8-3 folds in. -3. Log reading: the plan's ownership conflict DISSOLVED on - investigation — `composer log` attaches to the local dev daemon's - streams (`execute-log.ts`), a `service deployment logs` would - read the platform endpoint. Different data, no shared subgroup. - Shelved per R-S8-5 regardless, pending the transport answer. -4. Transport: ANSWERED (API owners via operator, 2026-08-12). HTTP - is acceptable in place of the WebSocket, as long as live - streaming can be added later. Consequence: the engine socket - affordance is not built; `service logs` follows the `build logs` - HTTP shape once the endpoint serves it; the socket design shelves - as the future live-streaming path. diff --git a/.drive/projects/prisma-cli-v8/specs/service-logs.md b/.drive/projects/prisma-cli-v8/specs/service-logs.md deleted file mode 100644 index 826d37e4..00000000 --- a/.drive/projects/prisma-cli-v8/specs/service-logs.md +++ /dev/null @@ -1,116 +0,0 @@ -# service logs (slice contract) - -Status: rev 1 (2026-08-13). One PR into `main`, branch -`service-logs`. Repo: prisma-cli only. Unshelves the S2c `service -logs` command against the platform's new HTTP page-read contract -(pdp-control-plane PR #4886, the base for this slice). - -Operator rulings carried in: the command mounts as **`service logs`** -(legacy spelling — ruled 2026-08-13, recorded in `deferred.md`); no -engine WebSocket transport (R-S8-5; the WS live tail stays the -platform's, unused by the CLI until the live-streaming date). - -## The endpoint contract (PR #4886, pinned) - -`GET /v1/deployments/{deploymentId}/logs`, authenticated, plain GET: -one request returns ONE PAGE as `application/x-ndjson` — records -`{type:"log", text, byteStart, byteEnd}` — and ends with -`{type:"terminal", kind:"end"|"error", code, message, retryable, -cursor}` before closing. Query: `tail=N` (last N lines, default -100), `from_start=true` (page from the beginning), `cursor` -(continue a chain; the terminal record's cursor is the next start). -No held-open connection; the WebSocket upgrade on the same path is -out of scope for the CLI. - -## The command - -`service logs [] [--service name] [--project id-or-name] -[--deployment id] [--tail n] [--from-start] [--follow]` - -- Session command in the platform family's service group. Target - resolution ports VERBATIM from the shelved S2c handler - (`bot/s2c-services`, `packages/cli/src/v8/service/logs.ts`): - explicit `--deployment` resolved globally then checked against the - project; otherwise the service's live deployment; the S2c error - shapes (`deploymentNotFoundError`, `deploymentOutsideProjectError`, - `noDeploymentsError`, …) return with it. The dead parts do NOT - port: `streamLogs`/compute-sdk, `getApiBaseUrl`, - `logStreamCredentialsError` (no credential ever reaches the - command — the transport is `ctx.api`). -- **Default: one page, then exit 0** — `tail` 100 like the endpoint, - the kubectl-logs shape. `--tail n` passes through; `--from-start` - maps to `from_start=true` (constructing it with `--tail` is a - parse-time conflict). A routine terminal record (`kind:"end"`) - ends the page; its cursor is not surfaced (the CLI owns resume). -- **`--follow`**: after each `kind:"end"` terminal record, wait the - poll interval and re-request with that record's cursor; run until - the user interrupts (the engine settles 130 from its signal - record, as `dev` does). Poll interval 2 s on the injectable clock. -- Records map per `build logs` (R-S2c-2): `type:"log"` → `output` - events, channel `data`; json mode frames them (session kind). - `type:"terminal"` with `kind:"error"` → structured error carrying - the record's code/message, exit non-zero; `retryable: true` on an - error terminal in `--follow` mode retries ONCE after the interval, - then fails (do not loop on a persistent error). -- Transport: `ctx.api.GET("/v1/deployments/{deploymentId}/logs", - { parseAs: "stream", params: { query: ... } })` — the `build logs` - shape, line-split NDJSON, tolerant of a final partial line. - -## The SDK risk — RETIRED at D1 (the premise was wrong) - -Rev 1 claimed the pinned SDK types this path's `query` as `never`. -That read the path-item boilerplate (identical on every path); the -OPERATION type (`getV1DeploymentsByDeploymentIdLogs`, -`dist/index.d.ts:6188` in `@prisma/management-api-sdk@1.55.0`) -already publishes `tail?: number`, `from_start?: "true" | "false"` -(a string union — the command sends `"true"`), and -`cursor?: string`. Verified empirically at D1 under `pnpm -typecheck`. The stream body keeps `build logs`' established cast -(the spec documents no 200 body); no new cast kind. - -## D1 amendments (implementer decisions, orchestrator-ratified) - -- The `--tail`/`--from-start` conflict refuses at HANDLER TOP per - the `project transfer` precedent (the engine has no declarative - flag-conflict mechanism), as `SERVICE.LOGS_RANGE_CONFLICT`, exit - 2, before any request. -- The poll interval is `PRISMA_CLI_SERVICE_LOGS_POLL_MS` per the - `service domain wait` precedent — the engine's delay seam is not - reachable from `CommandContext`; exposing it is an engine change - this slice does not make. -- `build logs`' private NDJSON reader moved to `lib/ndjson.ts`, - shared by both commands — an extraction, not a behavior change; - duplicating chunk-boundary handling is the drift class the S8 - workspace-filter defect came from. -- Follow-mode retry: a retryable error terminal is retried once per - FAILURE, with the budget reset by any successful page — a long - follow survives repeated transients but never loops on a - persistent error. -- e2e: `EXCLUSIONS` (needs a Composer-deployed service), matching - the S8 lifecycle commands — supersedes acceptance item 6's - "backlog" wording. - -## Out of scope - -The WebSocket live tail (later date, platform's move); any engine -transport work; `composer log` (different data source — the local -dev daemon); changes to `build logs`. - -## Acceptance - -- [ ] `service logs` mounted (legacy spelling), group help updated; - grammar per above with the `--tail`/`--from-start` conflict at - parse time. -- [ ] Page mode: fixture-backed tests for tail default, `--tail`, - `--from-start`, explicit `--deployment`, the S2c resolution - errors, unframed data output, json framing, and the error - terminal record → structured error. -- [ ] Follow mode: fixture drives page → end(cursor) → page → - interrupt on the injectable clock; cursor passed correctly; - retryable-error single retry pinned; interrupt settles 130. -- [ ] Divergence entry (`assets/s2/parity-divergences-s8.md` gains a - follow-up section or a new sibling file): default is page-read - (legacy followed); `--follow` is polling, not push. -- [ ] `deferred.md`'s logs entry closes; e2e joins the - deployed-service backlog beside `service open`. -- [ ] Suites green sequentially; typecheck + lint exit 0. From 24db9022e88b9ce18dca32be0fb5f4119a4a913f Mon Sep 17 00:00:00 2001 From: willbot Date: Sun, 27 Sep 2026 17:24:18 +0200 Subject: [PATCH 5/6] HEALTH.md: the ledger held 79 entries, not 81 Co-Authored-By: Claude Opus 5.5 Signed-off-by: willbot Signed-off-by: Will Madden --- .drive/HEALTH.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.drive/HEALTH.md b/.drive/HEALTH.md index 21a2663d..72f2c13e 100644 --- a/.drive/HEALTH.md +++ b/.drive/HEALTH.md @@ -13,7 +13,7 @@ At each slice merge, go through `.drive/projects//deferred.md`. Every e The ledger holds only entries that are waiting on something named, such as a release or a ruling that has been requested. -Why: the prisma-cli-v8 project closed on 2026-09-27 with an 81-entry ledger that nothing had revisited in six weeks. 40 entries were already done or moot. One ruling (remove the `npx skills add` copy button from the login page, made 2026-08-24) was never carried out, and another (split per-database agent skills by name) existed only in the ledger, so deleting the project would have deleted it. +Why: the prisma-cli-v8 project closed on 2026-09-27 with a 79-entry ledger that nothing had revisited in six weeks. 40 entries were already done or moot. One ruling (remove the `npx skills add` copy button from the login page, made 2026-08-24) was never carried out, and another (split per-database agent skills by name) existed only in the ledger, so deleting the project would have deleted it. ## Re-read the acceptance criteria when the design changes From 37b89d734b5409d908cdb61095b61523ada878d8 Mon Sep 17 00:00:00 2001 From: willbot Date: Sun, 27 Sep 2026 17:35:55 +0200 Subject: [PATCH 6/6] Address review; leave the engine test comments for the next engine change credential-manager.md: a session mutation moves the process's active credential only when no environment credential is in force, and the mutations-still-succeed rule applies to a non-blank PRISMA_SERVICE_TOKEN (a blank one is AUTH.SERVICE_TOKEN_EMPTY from every mutation). The two engine test comments go back to main's text: the engine-version check counts any file under packages/cli-engine/, so fixing a comment would require an engine release. Co-Authored-By: Claude Opus 5.5 Signed-off-by: willbot Signed-off-by: Will Madden --- docs/architecture/credential-manager.md | 4 ++-- packages/cli-engine/tests/help-markdown.test.ts | 4 ++-- packages/cli-engine/tests/markdown.test.ts | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/architecture/credential-manager.md b/docs/architecture/credential-manager.md index ab77b661..f0bdb5c0 100644 --- a/docs/architecture/credential-manager.md +++ b/docs/architecture/credential-manager.md @@ -93,11 +93,11 @@ A process decides once, at its first `activeCredential()` call, and the decision 2. Otherwise, if the file's `currentWorkspaceId` names a stored session, the process acts as that session. 3. Otherwise the process acts as nothing. `activeCredential()` returns `null` when no sessions are stored and throws `CLI.CREDENTIALS_REQUIRED` with the "sessions held, none selected" wording when sessions exist but none is selected. -Another process moving the selection or replacing records does not redirect a running process; a new process picks up the new selection. The process's own mutations do move it: `createSession` and `selectSession` make the process act as that session, and `endSession` of the session it acts as and `endAllSessions` make it act as nothing. Each of those discards the storage built for the previous decision, so a command that mutates and then reaches for `ctx.api` gets the credential it now acts as. +Another process moving the selection or replacing records does not redirect a running process; a new process picks up the new selection. When no environment credential is in force, the process's own mutations do move it: `createSession` and `selectSession` make the process act as that session, and `endSession` of the session it acts as and `endAllSessions` make it act as nothing. Each of those discards the storage built for the previous decision, so a command that mutates and then reaches for `ctx.api` gets the credential it now acts as. What is pinned is the decision, not the material. Every read goes back to the file, so a session another process replaced still resolves, and a session another process ended fails at the next read with `CLI.CREDENTIALS_REQUIRED` in its "session ended" wording. -While `PRISMA_SERVICE_TOKEN` is set, every mutation still succeeds: selecting or ending a stored session changes stored state while this process keeps authenticating as the environment credential. The commands print a one-line notice that the environment credential remains in force until the variable is unset. +While `PRISMA_SERVICE_TOKEN` holds a non-blank value, every mutation still succeeds: selecting or ending a stored session changes stored state while this process keeps authenticating as the environment credential. The commands print a one-line notice that the environment credential remains in force until the variable is unset. ## How the engine authenticates a command diff --git a/packages/cli-engine/tests/help-markdown.test.ts b/packages/cli-engine/tests/help-markdown.test.ts index 4083548a..f569a938 100644 --- a/packages/cli-engine/tests/help-markdown.test.ts +++ b/packages/cli-engine/tests/help-markdown.test.ts @@ -1,6 +1,6 @@ /** - * Help under `--format markdown`, byte for byte, per the help shape in - * docs/product/output-conventions.md (section "`--format markdown`"): + * Help under `--format markdown`, byte for byte, per the slice spec's + * help shape (.drive/projects/prisma-cli-v8/specs/markdown-format.md): * everything on stdout, stderr empty, colour off. */ import { describe, expect, test } from "vitest"; diff --git a/packages/cli-engine/tests/markdown.test.ts b/packages/cli-engine/tests/markdown.test.ts index dda77bea..bdc808f5 100644 --- a/packages/cli-engine/tests/markdown.test.ts +++ b/packages/cli-engine/tests/markdown.test.ts @@ -1,8 +1,8 @@ /** * `--format markdown`: the same blocks a command describes for human, * rendered as plain Markdown on stdout for a reader that is a model. - * Every byte here is pinned by docs/product/output-conventions.md - * (section "`--format markdown`"). + * Every byte here is pinned by the slice spec + * (.drive/projects/prisma-cli-v8/specs/markdown-format.md). */ import { type Block,