Give commands an authenticated API client, per-workspace auth sessions, telemetry, and real prompts — the foundations for porting the platform CLI onto the engine - #130
Conversation
…atch plan S2 ships as four area PRs (>=1k LOC floor). S2a: engine publish metadata + production dependency, ctx.api on the command context, auth module extraction, the auth family port, update check, cli-telemetry package move with the RunHooks.onSettled amendment, and the clack prompt renderer from the spike. Contracts pin every design decision; dispatches stop rather than improvise. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
…ction dependency Give @prisma/cli-engine everything npm publish needs: version 0.1.0, the one-line description, Apache-2.0 license file, files list (dist, README, LICENSE), repository/homepage/bugs pointing at prisma/prisma-cli, publishConfig access public, engines.node >=22.12.0, and a prepack build script, all shaped like packages/cli's package.json. Drop private: true so the package can actually be published, and add a terse README covering the package and its three entry points. Move @prisma/cli-engine from the cli's devDependencies to dependencies (still workspace:*) and settle the lockfile. npm pack --dry-run ships exactly dist + README.md + LICENSE + package.json. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
Move token-storage, auth-ops (now operations), and the auth client into src/auth/, extract the real-mode workspace helpers out of the auth controller into src/auth/workspaces.ts, and relocate makeGetCredentials from the v8 runtime into src/auth/credentials.ts. src/auth/index.ts is the module's only public face; controllers, the legacy shell, and the v8 code import through it. No behavior change: both suites pass with only import and mock path updates. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
…2 question ledger The 60-command inventory grounds the S2b (resources), S2c (services), and S2d (init + shell retirement) contracts. The overview gains the operator question ledger (Q1 auto-login, Q2 service-run passthrough, Q3 rm alias, Q4 config evaluation for the shipped bin, Q5 exit-code unification) with the defaults the contracts build to. S2a contract errata from D2/D6 grounding: auth logout --workspace, the auth index export list, the white-box test exception. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
Contract s2a-foundations §2 (dispatch D3).
- @prisma/management-api-sdk pinned exact (1.55.0) in the engine and
the cli
- Runtime gains managementApi.baseUrl; the v8 bin derives it via
getApiBaseUrl(env)
- CommandContext.api: lazy, once per run, constructed on first access
by copying the shell's createManagementApiSdk call site with the
token source backed by ctx.getCredentials, so refresh is picked up
per request; the client is proxied to rethrow structured errors the
SDK's onError middleware would otherwise wrap in FetchError
- Unauthenticated use throws CLI.CREDENTIALS_REQUIRED via the single
constructor now exported from execution/needs.ts
- Harness spec gains managementApi { baseUrl?, client? }; an injected
client IS ctx.api; baseUrl defaults to https://test.invalid
- Draft amendments: §4 CommandContext, §10 Runtime, §11 harness spec
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
Prompt rendering is now two-tier. Real TTYs — isTty.stdin AND
stdin.setRawMode present, no scripted answers — render through
@clack/prompts (exact-pinned 1.5.0, loaded by dynamic import only on
that path); scripted answers, piped stdin, and the test harness stay
on the plain line renderer. --yes resolution and structural failures
are decided before the tier branch, so both tiers share identical
semantics.
clack-renderer.ts adapts Runtime streams for clack: Readable.from over
Runtime.stdin presenting isTTY with setRawMode forwarded, a Writable
over the stderr OutputStream, and { input, output } injected per
prompt, so all prompt UI stays on stderr. Clack's cancel symbol
(including the \x03 byte path) maps to the existing
CLI.PROMPT_CANCELLED exit-3 settlement. consent maps to clack confirm
starting on No: Enter-through returns false, only explicit Yes grants.
Clack's spinner/log helpers are never used (process-global handlers);
progress remains engine events.
Draft notes: §4a two-tier rendering, select's Enter-picks-highlighted
behavior, and the accepted quirk that clack reads
process.stdout.columns for wrap width.
Tests: a fake raw-mode stdin fixture drives confirm/consent/select/
text through the clack tier, asserting resolved values, stderr-only
UI bytes, setRawMode forwarding, and \x03 -> exit 3; a module-load
spy (with a canary validating the spy) proves the scripted and
non-TTY paths never load @clack/prompts. Dist .d.ts stays clack-free.
Reimplements spike/clack-prompts (903b25a) on the current module
layout.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
…elemetry Ports packages/1-framework/3-tooling/cli-telemetry from prisma/prisma into this repo as a private workspace package, per the S2a contract (§6). Preserved unchanged: the shared prisma-next user-config path and format (one installation id with the ORM CLI), gating resolution with the exact env var names and precedence (PRISMA_NEXT_DISABLE_TELEMETRY truthy, DO_NOT_TRACK=1, stored consent, opt-out default-on), the detached fork/IPC/unref sender mechanism with its silence and exit-0 contract, the production endpoint and wire protocol, and the sanitizer's value-free discipline. Adapted for this repo: the Commander snapshot is replaced by the engine shape EngineCommandSnapshot (command path, flag names with value source, positional count — no values, ever) and the sanitizer projects it to the same wire fields; the arktype payload schema is spelled out as a hand-rolled guard with identical semantics (no arktype dependency here); the ORM's @internal/config validator is replaced by a structural extraction of target.targetId and extensions[].id with the same empty-on-invalid outcome; pathe and @internal/utils are replaced by node:path and an inline spread. The DB-backed integration harness is replaced by a local mock HTTP backend driven through the same endpoint override the reference suite used; the production endpoint is never contacted from tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
…nt commands
Engine amendment (draft §10): RunHooks gains onSettled(summary) with
RunSummary { commandId, exitCode, durationMs, snapshot } and the
value-free EngineCommandSnapshot { commandPath, flags (name + source),
positionalCount }. The snapshot is captured at parse time when a
command mounts: flags explicitly present on argv are source 'cli'
(long, =-form, kebab or camel spelling, --no- negation, and aliases);
the engine reads no flags from the environment today, so everything
else is 'default'. The hook fires exactly once per run after
settlement, never for --help/--version or pre-mount usage errors,
with durationMs from the injectable clock; a throwing hook is
swallowed. RunHooks stays internal — the public growth is the minimal
hooks parameter on Cli.run (CliRunHooks, onSettled only), and the
test harness gains a matching onSettled tap.
Bin wiring (v8 main): the CI/env/consent decision resolves before the
run; when enabled an onSettled hook fires runTelemetry (detached
fork + IPC send + disconnect + unref, reference spawn semantics),
performing the first-run stderr disclosure + shared-id mint when no
installation id is stored; the telemetry command family is exempt;
when disabled no hook is attached. CI detection ports the ORM CLI's
ci-info wrapper.
Commands telemetry status|enable|disable port the ORM consent surface
as engine result commands, mounted shell-owned under the new
telemetry group, with the reference copy, cards per the S1 whoami
pattern, and json serializers. status is a pure read.
The cli bundles @repo/cli-telemetry (workspace devDependency, never a
published dep): a second tsdown config emits dist/v8/cli.js with the
telemetry source inlined plus the forkable dist/v8/sender.js; the
sender's third-party deps (c12, @vercel/detect-agent) move onto the
cli's own dependencies. Smoke-verified against a local mock backend:
a force-enabled run POSTs exactly one wire-shape event with the
stored installation id; CI=1 spawns nothing.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
Mounts all six auth commands (login, logout [--workspace], whoami, workspace list/use/logout) as engine result commands in the platform command family, backed by the extracted auth module. - login runs the real OAuth flow via performLogin, emitting step-started/finished events around it and an 'endpoint' event for the verification URL (new optional onVerificationUrl hook on performLogin; legacy callers unaffected), then reads the auth state and appends the ported agent-setup tip (CI-suppressed; tip line is human-mode only). Fixture-only --provider/--user/--workspace flags do not port. - logout --workspace calls the shared workspace-logout operation directly instead of the legacy argv re-dispatch. - workspace use resolves by id/case-insensitive name, auto-selects a single workspace, prompts via ctx.prompt.select otherwise, and lets the engine's structural prompt failure cover non-interactive runs. - Legacy flat error codes map mechanically to dotted AUTH.* codes (fix prose -> one user-choice nextAction, meta preserved), matching the S1 whoami precedent. - The workspace operations' context parameter narrows to a structural type both shells satisfy; whoami now shares the state-card and config-invalid helpers. - Semantic tests stub the auth module at the src/auth/index.ts seam; fixture-mode auth.test.ts cases covering ported commands are deleted (real-mode and shell-presentation cases stay until S2d). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
packages/cli/src/shell/update-check.ts moves to packages/cli/src/update-check.ts; its CliRuntime parameter narrows to the structural UpdateCheckRuntime (env/argv/stderr) both shells satisfy. The legacy shell keeps its call sites; the v8 bin copies their sequencing exactly: main() awaits the cached notification (and detached refresh spawn) before dispatch, and the v8 bin entry gains the PRISMA_CLI_RUN_UPDATE_CHECK_WORKER branch. Suppression rules are copied as-is, including silence when argv contains --json/--quiet/-q, recorded in the S2 divergence list. Tests cover the v8 wiring: cached-newer notify, interval silence, json-mode silence, non-TTY silence, and the detached refresh spawn's arguments and worker env contract. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
…oxes Creates the S2 cumulative parity divergence list (seeded with a pointer to the S1 whoami-scoped record) covering the auth family's AUTH.* error-code mapping, login's fixture-flag removal and event surface, the logout --workspace commandId change, prompt-path behavior for workspace use, and the update-check json-mode finding. Marks the whoami record as whoami-scoped and updates its stale update-notification note. Checks every S2a acceptance box except the operator publish and the operator's divergence-list review. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
… load
Review-round fixes for slice s2a-foundations (B1, C4, A3, A7, C7):
- Attach a swallowing 'error' listener on the forked sender child so an
async fork failure can neither crash the parent CLI nor flip its exit
code (B1), with a regression test driving a fake child's error event.
- Drop the detached child's prisma-next.config.* c12 load entirely: the
config does not exist in this product and the load evaluated arbitrary
user TS in a detached process. databaseTarget now ships null (payload
override kept for wire compatibility) and extensions ships []; the c12
and magicast dependencies are removed (C4). Recorded as an S2a parity
divergence.
- resolveGating now takes { env, config, inCI } and returns a total
reason union (ci | env-opt-out | stored-opt-out | stored-opt-in |
default-on); CI is part of the resolution itself (A3).
- Public-surface tightening: the duplicated EngineCommandSnapshot type
is no longer re-exported (the structural declaration stays internal)
and SanitisedCommand is normalized to SanitizedCommand (A7).
- Pin @clack/prompts to exact 1.5.0 in packages/cli (C7).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
…odule
Review-round fixes for slice s2a-foundations (C1, A10, A11, A8):
- restoreStructuredThrows maps the SDK's AuthError (the 401 /
refresh-unavailable path) to the shared CLI.CREDENTIALS_REQUIRED
structured error, matched structurally by name so a duplicate module
instance cannot defeat it (C1).
- The cause-chain walk tracks visited errors with a depth cap so a
cyclic cause chain terminates (C1), with a settlement test.
- The @prisma/management-api-sdk module now loads via dynamic import on
the first actual request: ctx.api is a Proxy whose async method
wrappers await the lazy construction, mirroring the clack renderer's
lazy-import pattern; the structured-throw restoration merged into the
same wrapper (A10).
- The engine's OAuth client id / redirect URI are inert empty-string
placeholders — no refresh token is ever supplied, so the SDK's OAuth
flow is unreachable (A11).
- RunSummary.commandId documents its derivation from
snapshot.commandPath (always equals commandPath.join('.')); draft §10
carries the same note (A8).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
…e homes
Review-round fixes for slice s2a-foundations (A1, A2, A5, A6, C8, C6):
- src/lib/auth/{login,guard,recipient}.ts move into src/auth/; the
index gains requireComputeAuth and the recipient exports, and every
controller imports via src/auth/index.ts only (A1). src/auth is now a
leaf cluster: its one remaining legacy import is the CliError base
class (named as an S2d survivor).
- The auth-specific error constructors move from shell/errors into
src/auth/errors.ts (shell re-exports for legacy imports), and
resolveStateDir moves to src/state-dir.ts with shell/runtime
re-exporting (A2).
- WorkspaceOperationContext flattens to { env, signal }; the v8 adapter
shape is gone and legacy controller call sites build the flat context
(A5).
- listRealAuthWorkspaces/useRealAuthWorkspace/logoutRealAuthWorkspace
rename to listAuthWorkspaces/useAuthWorkspace/logoutAuthWorkspace —
there is no fixture-mode counterpart to distinguish from (A6).
- The onVerificationUrl observer hook is invoked inside try/catch so an
observer bug cannot break the login flow (C8).
- New src/cli-name.ts owns the user-facing binary name and docs URL;
getCliName and the update-check fallback URL consume it (C6).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
…g semantics Review-round fixes for slice s2a-foundations (A4, A9, C2, C3, C5, C6, C9, C10, A13): - v8/auth/workspace-commands.ts splits into workspace-list.ts, workspace-use.ts, workspace-logout.ts with a workspace-shared.ts helper module; the shared runWorkspaceLogout operation gets its own module. v8/telemetry/commands.ts splits into status.ts (merged with the status resolution), enable.ts, disable.ts, and a shared consent presentation (A4). - v8/telemetry/wiring.ts renames to reporting.ts; options type renamed to TelemetryReportingOptions (A9). - auth workspace list now maps an empty PRISMA_SERVICE_TOKEN to AUTH.CONFIG_INVALID (exit 2), matching whoami/login/logout, with a divergence row (C2). - The first-run telemetry disclosure prints at gating time — pre-run, before the command's output — while the event still fires at settlement; the installation-id mint stays at first settlement so 'telemetry status' keeps reporting 'not stored' (C5). Status projects the gating resolver's total reason union directly (A3). - The disclosure and the telemetry group help now use the real docs page (the update-check fallback URL) and the CLI_NAME constant in every user-facing command string (C6). - New packages/cli/vitest.config.ts sets PRISMA_NEXT_DISABLE_TELEMETRY=1 suite-wide, mirroring the cli-telemetry package's own guard, so no test reaches the developer's real user config or the endpoint (C3); the bin/update-check test processes opt out explicitly. - Tests: AUTH.CONFIG_INVALID on login and logout, the json login tip envelope (agentSetupTip + tip nextAction), and the tip-suppression branch when Prisma skills are already installed (C9); update-check notify-before-dispatch ordering via a marker-writing stub CLI and the '--format json is NOT suppressed' literal-argv quirk (C10). - v8-auth.test.ts byte assertions convert to semantic assertions (envelope / presented / events / exit code); the sanctioned golden suite v8-golden-rendering.test.ts pins one representative card, table, and error rendering byte-exactly; the S1 whoami byte pins remain (A13). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
Operator-process-sanctioned S2 doc amendments from the review round: - parity-divergences.md: workspace list joins the AUTH_CONFIG_INVALID mapping row; new sections for the empty-service-token structuring and the two telemetry divergences (config enrichment dropped; onSettled emission timing with pre-run disclosure — crashed/killed/process.exit runs emit nothing). - s2a-foundations.md §3: the workspace operation exports are the *AuthWorkspaces names, with a one-line erratum; §6: CI is part of the gating resolution, and the wording separates spawn semantics (copied) from timing (onSettled by design, pre-run disclosure). - s2d-init-and-retirement.md R-S2d-4: known survivors named — src/state-dir.ts and the CliError base class residue behind src/auth/errors.ts and the v8 error mapping. - s2b-resources.md D1: build-time test requirement that the family maps and the mount map cover the same command set. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
Telemetry docs URL, config-enrichment drop, disclosure timing — each built to a stated default awaiting ratification. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
|
Important Review skippedToo many files! This PR contains 186 files, which is 86 over the limit of 100. To get a review, reduce the PR to 100 files or fewer by splitting it into smaller PRs or changing its base branch. Upgrade to a paid plan to raise the limit. This review couldn't start because sufficient usage credits or metered capacity aren't available. Add credits or update usage-based reviews in the billing tab, then retry. ⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Plus Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (186)
You can disable this status message by setting the Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
… line Operator ruling 2026-08-10: this repo takes over prisma@8.0.0-rcX, so it adopts prisma/prisma's versioning scripts AND its version number. Ports determine-version / set-version / bump-version (+ pure-helper tests, run via the new root test:scripts) from wip/repos/prisma, adapted to node:path and this repo's package set. Stamps 8.0.0-rc.1 in lockstep across the root, cli, cli-engine, cli-telemetry, and tsconfig manifests with workspace:8.0.0-rc.1 internal pins (the reference's convention). @prisma/compute is hard-excluded from the lockstep (second ruling: it versions independently pending extraction); its manifest moves to its honest npm state 0.1.0-beta.0 and resolve-package-version.mjs survives trimmed to the dev/next-beta commands publish-compute.yml still uses, with its test moved into the script test suite (the cli-package copy is deleted). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
…ion workflow Ports publish.yml from the reference verbatim per the operator ruling (2026-08-10): a push to main with the root version unchanged publishes <base>-dev.N under the dev dist-tag; a push that changes the root version (a merged bump PR) publishes the committed base under latest for BOTH @prisma/cli-engine and @prisma/cli (engine first — the cli depends on it) plus a GitHub Release marked pre-release on the rc line. The operator's words: merging a version bump PR counts as a deliberate explicit action to alter latest. workflow_dispatch keeps the chosen dist-tag + dry-run escape hatch. publish-cli.yml is deleted whole — both its jobs are superseded. The PR preview drops version stamping and copies the reference's pkg.pr.new model (the committed rc base ships as-is at per-commit URLs); the engine is published alongside the cli so its workspace pin resolves to the preview build. pr-quality's test job now runs the ported script tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
Adapts prisma/prisma's docs/oss/versioning.md to this repo (package names, the verbatim latest model, the @prisma/compute exclusion pending extraction — operator rulings 2026-08-10). README/CONTRIBUTING publish sections and ADR 0001's status now point at the committed-version model; the S2a contract carries a one-line erratum (engine version 0.1.0 -> 8.0.0-rc.1); the rollout plan's step 2 and its latest invariant are reworded per the ruling — latest moves only through a deliberately merged version-bump PR (or manual dispatch), superseding the earlier next-tag interim. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
The v8 telemetry suites pointed only XDG_CONFIG_HOME at their temp dir, but on win32 userConfigPath() resolves from APPDATA first, so every read and write on the Windows runner hit the real %APPDATA%\prisma-next\config.json. That file is never cleaned between tests, so earlier cases leaked stored ids and consent choices into later ones — the six Windows-only failures. Point APPDATA at the same temp dir alongside XDG_CONFIG_HOME and restore both afterwards; expected paths already derive from userConfigPath(). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
Operator ruling 2026-08-10: a version change alone must not move latest — merging PR #130 jumps the lockstep to 8.0.0-rc.1 but only a deliberately merged 'chore(release): ...' bump PR publishes a release. Any other version-changing merge publishes a dev build, loudly. pkg.pr.new previews are unaffected and already cover both packages. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
The release procedure's step 1 is now the ported skill, not a bare script invocation: fresh worktree off origin/main, pnpm bump-version, lockfile refresh, diff sanity check, and the release PR. Adapted for this repo: the PR title must carry the chore(release) marker because squash merges make the PR title the commit subject the publish workflow checks; compute exclusion and generated release notes noted. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
… prisma/prisma Operator ruling: the publish machinery must match prisma/prisma exactly so it never needs remembering. The transient problem the gate solved (the lockstep-adoption merge changing the root version without being a release) is handled by sequencing instead: a root-version pre-PR lands on main under the old workflows first, so this branch's merge shows no version change to the new workflow. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
The use- prefix read as a React hook. The operation switches the active workspace session — its own presenter already says so. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
…iClient The name was wrong twice: 'Compute' is branding residue that now collides with the unrelated @prisma/compute package, and 'require' implied a throw where the function returns null. It resolves an authenticated management API client (service token, else stored OAuth with refresh) or null. Legacy-shell-only; dies with it in S2d. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
The request-failure mapping checks for a CLI structured error before it checks whether the failure came from the refresh path, and that order is what lets a manager-raised error survive. Nothing held the order down: swapping the two blocks left every suite green, while in production a session ended by another process mid-rotation would have surfaced as the transient auth-service error — "try again" — instead of telling the user their session had ended. There is now a test that fails when the order is swapped. The test harness made that case unreachable, because its TokenStorage threw a plain error where the real manager throws a structured one; it now throws the same error, with a test pinning the agreement. The engine built the environment-session bearer from the raw variable while the manager composed the session from the trimmed value, so PRISMA_SERVICE_TOKEN=" tok " would report a valid session and then send a padded bearer on the wire. The legacy path trimmed. The engine now trims too. The debug valve echoed the token endpoint's error_description verbatim, which is free text the auth service chooses, immediately below a branch that deliberately logs only an error's type for exactly that reason. It now logs the endpoint's verdict without the description: the OAuth error field for a 4xx, and the SDK's own message — which carries the HTTP status — otherwise. That is what the design asks for. The reads-never-write probe also now spies the synchronous rename, rm and open calls, and asserts a positive control on `open` as well as `rename`, so its breadth is real rather than nominal. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
Clearing a crashed holder's lock was not atomic. Two waiting processes could both see the same stale lock, and the second one's unlink removed the FIRST one's freshly created lock — both then ran their read-modify-write at once and one update was silently lost, which is the one thing the lock exists to prevent, in the one situation the takeover path exists for. Clearing is now a rename, which only one process can win; the loser waits. A test holds both waiters until each has seen the lock as stale and asserts exactly one of them reports a takeover. It fails against the unlink version on every run. A takeover that could not remove the lock still reported success, and that success made the acquisition loop skip both the timeout check and the sleep. With a stale lock in a directory the process cannot write to, the loop ran flat out forever: no timeout, one core pinned, the command never returning. The timeout is now checked on every pass and the sleep is skipped only after a takeover that really happened. The test for this finishes in under a second against the fix and hangs until the test timeout against the defect. A write that failed after opening its temp file left that file on disk holding the whole state, tokens included, under a name nothing looks for again — so someone running `auth logout` to revoke local access could be left with a working refresh token. Every path out of the write now removes the temp file, and `endAllSessions` reaps any that an earlier crash left behind. Also removes a duplicated file comment. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
…it is `TestCredentialManager` was named for who uses it rather than what it is. It is not a stub: it implements the same session rules as the file-backed manager — pinning, upsert by workspace, the environment-override refusals, and all four TokenStorage write slices — and adds a seed and a state read-back. Tests are simply where it is most useful. `TestCredentialManager` becomes `InMemoryCredentialManager`, its seed and state types follow, and the module becomes `in-memory-credential-manager.ts`. `TestSessionRecord` becomes `SessionRecord` for the same reason: it is the manager's record, not a test's. `createTestCli` and `mintTestJwt` keep their names, because a test CLI and a test JWT minter are exactly what those are. This renames exports on the ./testing subpath. The engine is at 8.0.0-rc.1 and unpublished at that version, so nothing outside this repo consumes the old names. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
…t action Both are what S2b needs to finish `git connect`, and both are additive. `BrowserWaitRequest` gains an optional `interval`. Legacy `git connect` polls on PRISMA_CLI_GITHUB_INSTALL_POLL_INTERVAL_MS, and until now that value had nowhere to go: the engine polled on a private constant. The constant stays as the default, so every existing caller is unchanged. The test harness gains an optional `delay` for the same reason `now` is already there — a poll loop's cadence is only assertable if the test can see it. `NextAction` gains an `open-url` kind and a `url` field. Two legacy repo errors carry a GitHub app install URL in their follow-up steps, and every follow-up string currently becomes a `run-command`, so the envelope was telling consumers to execute a URL as a shell command. A URL is not a command. `renderNextAction` falls back to the url, so an open-url action prints its address the way a run-command prints its command; nothing in the engine switches on the kind, so machine consumers get the new one for free. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
…hang Reported from S2b as a flake under full-suite load. It is not flakiness, it is a hang the winner usually outruns. The barrier releases both waiters once both have seen the stale lock, then resets to empty. The waiter that loses the takeover race sleeps and comes back for another look, and if the winner is still holding the lock at that moment it stats again — entering a fresh barrier cycle that needs two arrivals and will only ever get one. It blocks until vitest times the test out at five seconds. It normally passes because the winner finishes its whole mutation inside the loser's ten-millisecond sleep, so the loser's next attempt to create the lock succeeds and it never stats a second time. Load is what removes that margin, which is why adding test files makes it appear. The barrier now latches open, which is what its comment always claimed. Five runs in isolation and three full-suite runs pass, and it still fails on every run against the unlink version of the takeover, so it holds the behaviour down exactly as before. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
…nt credential is not a session Rev 5 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: a `source` field to say "this is not really a session", a hardcoded `current: true` that gave the word two meanings, a `workspaceId: ""` because a non-session was made to carry a session's key, and a guard rejecting the non-session from APIs that only take sessions. Rev 6 separates the three things — a stored session, the selection, and the credential this process authenticates as — and all four stop existing. Refresh follows from the credential having a refresh token, not from where it came from, so the engine builds one client over storage the manager hands it: file-backed with no cache in front, or memory-backed touching no file at all. The custody rule is restated as what it always meant — credentials never reach commands; the engine may hold them. Removal is idempotent. The mutation refusals under an environment token go, since the rule they enforced was the last piece of the session confusion. Written with the architect and principal-engineer passes folded in. The principal engineer caught that a 401 on a credential that never had a refresh token would lose today's service-token message and tell CI to retry a permanent failure; the design now discriminates by state rather than by message, which also repairs a rev-5 defect for migrated entries with no refresh token. The architect caught that the delta introduced "selected" without retiring "current", leaving three words for one idea; §11.1 rules the vocabulary and names what deliberately keeps the old word. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
…atch The legacy app group fused building, repo wiring, and deploying into single commands. Composer takes the building and deploying; what the CLI should own is managing the remote resource, and today it cannot — there is no way to list or create a service, none to start or stop a deployment, and a service can only be born as a side effect of deploying to it. S2c ported the survivors under their legacy names so the commander shell could die in S2d. That is continuity, not endorsement. The slice is recorded as blocked on design work, and the design work is blocked on a fact we do not have. Composer deploys through Alchemy, not the management API, so a Composer-deployed service does not appear under /v1/apps at all and the resource model those endpoints describe is the product being replaced. Until we know what a service is after Composer, there is nothing to design primitives over. An earlier sizing of this as "small, mostly a rename" assumed Composer would create deployments through the platform API; it does not, so the sizing is withdrawn and the slice is unsized. Ordering is after S3, because Composer's contract is the input, and before S7, because S7 mounts the full grammar tree behind a completeness check that this slice changes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
… API resources The previous entry claimed a Composer-deployed service would not appear under /v1/apps because Composer deploys through Alchemy. That does not follow: Alchemy's providers call the management API to do the work, so Composer's services and deployments are ordinary resources under the same endpoints. Only the orchestration differs. So the seam is the one the API already draws — Composer produces deployments, the CLI manages them — and the slice is mostly a rename into a `service deployment` subgroup plus the five operations that have no command at all. The sizing withdrawn in the previous commit is restored. What the slice genuinely waits on is narrower and sharper: whether Alchemy holds desired state, in which case an imperative promote or rollback from the CLI either fights Composer or is reverted on its next run; what Composer's app and deployment records actually contain; and whether log reading belongs to `composer log` or to the service subgroup, since a subgroup is owned by exactly one command family. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
…eleted The environment credential stops being modelled as a session. Session loses `source` and `current`; the selection is read directly instead of scanned off a flag on each element; what the process authenticates as becomes ActiveCredential, carrying no token material. ctx.session() becomes ctx.activeCredential(). There is now one API client. ClientBinding, the static-token path and the resolved-401 inspection are gone: the engine builds the SDK's refreshing client over whatever storage the manager hands it, which is file-backed for a stored session and memory-backed for a credential with no home. A 401 on a credential that could never be renewed no longer falls through to the session-ended or transient mapping — the engine asks the storage for its tokens, and a set with no refresh token yields the credential-rejected error, whose wording follows the origin. getCredentials goes with it: the context accessor, Runtime's member, the Credentials type, the needs-check fallback and the harness's legacy seed were the bridge that let commands reach a raw token while the manager landed, and rev 6 finishes that swap. packages/cli does not compile against this yet; its cascade is the next commit. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
`label` is required and `command` is optional, so a mapper building an action out of a legacy error's follow-up step — a bare command string with no prose beside it — has nothing to put in the label but the command. Both fields then hold the same string and the renderer printed it twice: → prisma-cli project list: prisma-cli project list Fixed in the renderer rather than the callers, because label is required: every mapper in that position would otherwise have to invent prose, and the renderer is the only place that sees both fields and can tell they are the same. `url` takes the same path and is covered too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
Composer holds desired state through Alchemy, and changing the platform directly is overwritten on its next deploy. The operator accepts that, so the imperative deployment operations stay and their effect on a Composer-managed service is understood to be transient. What survives for the design is narrower: whether the CLI should say so at the point of use. That depends on whether an app or deployment record carries anything identifying it as Composer-managed, which is already question 2. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
The CLI half of the cascade. The file-backed manager implements the rev-6 interface: the pin holds the decision, not a token, and the material is read through the file on every call. Two storages, chosen once — file-backed reads through with no cache in front, so the SDK can still recover when another process rotated first; memory-backed closes over a local variable, never receives the file path, and touches no file on any method, so an environment credential cannot delete the stored session whose workspace its token happens to name. `serviceTokenWorkspaceId(token) ?? ""` is gone. It was the only place in the codebase that manufactured an empty workspace id, and it reached the user: whoami printed a workspace whose id was the empty string. A credential whose claims name no workspace now reports none, whoami omits the row, and its JSON workspace is null. Ending a session is idempotent — the postcondition is the same either way — so losing the race to another process exits 0 instead of claiming you have no such session. Selecting still refuses a workspace with no session, because there is no state in which it would afterwards be selected. The mutation refusals under PRISMA_SERVICE_TOKEN go entirely; all three succeed and say the environment credential stays in force until the variable is unset. whoami asks for an identity and renders it, with no branch on where the credential came from and no token in the command's hands. /v1/me wins field by field over the claims and the claims are the offline fallback. Also restores the user's name to whoami's output. CredentialIdentity was defined with only a user id and an email, which silently dropped a field the previous shape carried; only an online lookup supplies it, so it is absent offline rather than removed. The JSON reports it as `user.id` rather than `user.userId`, which reads badly beside `workspace.id`. `environmentSessionMutationError` and its AUTH.ENV_SESSION_IN_FORCE code are deleted from the engine, having lost their last consumer. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
Renaming the corpse aside instead of unlinking it narrowed the window but did not close it. Two waiters both see the lock as stale; the first renames it away and creates its own; the second then renames away THAT lock, and both believe they hold it. Windows produced the ordering naturally and macOS did not, which is how it reached CI green. Rename cannot be made conditional, so the takeover now confirms afterwards that what it moved aside is the corpse it examined, by comparing mtime. If it is not, the lock belongs to whoever created it and goes back — via `link`, which fails when the path is occupied, so restoring can never overwrite a third process's lock. The test forces the interleaving rather than leaving it to the scheduler: one hook holds both waiters until each has seen the lock as stale, a second holds the loser's removal until the winner's lock actually exists, signalled off the create rather than polled. Six runs pass with the fix and six fail without it, where before it passed either way on macOS. The assertion is "at most one takeover", because a winner that releases before the loser looks leaves no corpse and zero is also correct; two is the defect. Also guards the two 0600 assertions behind a POSIX check. Windows has no Unix permission bits and reports 0o666 whatever the file was created with, so those assertions could only ever fail there. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
A real service token's subject is `workspace:<id>`, not a person.
`claimedIdentity` read `sub` and called it a user id unconditionally,
twenty lines below a function that existed precisely because the subject
can name a workspace. So `PRISMA_SERVICE_TOKEN=<real token> prisma auth
whoami --json` emitted `"user": {"id": "workspace:ws_abc"}` whenever
/v1/me returned no user, which is the likely answer for a machine
credential. The human card hid it, because it only prints a user row
when there is an email.
The reason no test caught it is the second half of this change. The four
claim helpers were duplicated across the CLI's `claims.ts` and the
engine's in-memory manager, and the copies had drifted: production knew
about the `workspace:` subject and the harness did not, so a service
token that works in production could not be represented in a test at
all, and the harness reported no workspace where production reported
one. There is now one implementation in the engine that both managers
use, and the CLI keeps its existing names by delegating to it.
The engine's public surface grows by the four helpers the CLI consumes.
The raw decoder stays internal, and `decodeClaims` is dropped from the
CLI's barrel — it was re-exported and never used.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
Rev 6 stopped whoami branching on where the credential came from, so it now attempts the /v1/me enrichment for an environment credential too, where rev 5's path was local and instant. Nothing bounded that request: ctx.signal only fires on Ctrl-C, so a host that accepts the connection and never answers would hold the command for as long as the runtime's own timeouts allow — minutes behind a black-holing proxy, which is exactly the setting a service token gets used in. The enrichment now carries its own three-second deadline alongside ctx.signal. A cancellation still propagates, because the catch rethrows on the original signal; a timeout falls through to the claims, which is what whoami answers from anyway. The test points it at a server that accepts and never responds, and it completes in about three seconds with the claims intact. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
…to share Two full implementations of one contract, and every command test in this slice and the next two runs against the in-memory one — so where they disagree, the tests assert behaviour the product does not have. Three disagreements left after the claim helpers were unified. The workspace-mismatch refusal threw a plain harness error where the file-backed manager throws a structured one. The constructor moves to the engine and both raise it, so a test of that refusal now sees what production raises. Same for the rotation-time re-scope check. withRefreshLock was `fn => fn()`. Section 6 requires the hook to serialise within a process, and the file-backed manager runs a real queue, so two concurrent refreshes would pass a harness test and fail in production. The in-memory manager now runs the same queue. The blank-PRISMA_SERVICE_TOKEN refusal genuinely cannot live here — this manager is handed a credential and never reads the variable — so the header now says so instead of claiming parity it does not have. That rule is covered end to end through the real environment in the CLI's tests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
Revision 6 is titled "the environment credential is not a session", and
the command layer went on calling it one — in the notice every mutation
prints, in the helper that reports it, in two json field names, and in
the module that builds whoami's card. The retired model was what the
user read.
ENVIRONMENT_SESSION_NOTICE becomes ENVIRONMENT_CREDENTIAL_NOTICE and
says "supplies the credential in force". environmentSessionInForce
becomes environmentCredentialInForce, including the json fields on
`auth workspace list`'s context and `auth login`'s result. session-card
becomes credential-card, since it builds rows for an ActiveCredential.
Also the leftover "current" outside the deliberate exceptions:
`wasCurrent` in workspace logout's result becomes `wasSelected`,
whoami's title says "active authenticated identity", and `auth logout`
says it is clearing your stored workspace sessions rather than "the
current CLI session" — it clears every one of them, so the old wording
was inaccurate as well as retired. The on-disk currentWorkspaceId, the
`auth workspace use` command name, and `auth workspace list`'s json
`currentWorkspaceId` and per-item `current` keep the word on purpose:
they are contracts, and the divergence document now says so for the
renamed field too. The legacy shell's own wording is untouched; it goes
in S2d.
serviceTokenRejectedError leaves the public exports. Wording that
differs by origin belongs in credentialRejectedError alone, and an
export was a second door into the environment-specific text.
The divergence document described a whoami shape the code does not
produce — it claimed `user.userId` and that `user.name` had no
successor, where the code emits `user: {id, email, name}` — and said no
json result carried the environment flag when two do. Both corrected,
and `auth workspace logout`'s result is now described at all.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
Four things, none of which changes a green suite on its own. The stored-state mapping could tell an environment credential that its session had expired or ended. Both are statements about a stored session, and an environment credential has none, so neither can be the answer for it however its refresh fails. It gets the credential-rejected error for a rejected refresh token and the transient error for a transient failure — never an explanation whose remedy does nothing while the variable is set. Unreachable today, because one environment variable carries one bearer string and so there is no refresh token to fail, but §11.2 keeps the uniform path deliberately and the discrimination now travels with it. whoami's identity merge could report a person who does not exist. The claims and the /v1/me lookup are read at different moments, so another process replacing the session in between leaves them describing two different users; filling a gap in one from the other then produces one user's id beside another's email. Field-by-field merging is right where both describe the same person and is now limited to that; otherwise the lookup is taken whole. The active credential's storage was memoized, but every mutation moves the pin afterwards, so a command that mutated and then reached for ctx.api would have been handed storage for the credential it used to be acting as. Moving the pin now discards it. Nothing hits this today — auth login never touches ctx.api — but the next command that mutates would have. Also the two comments naming ctx.session and currentSession(), both deleted in rev 6 and both flagged twice; and the blank-token check whose body was a discarded getter call with nothing at the call site to say that reading is what raises. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
Review comment on packages/cli/src/auth/index.ts: separate exports from src, and if it is not a package entrypoint, delete it. It is not — packages/cli ships a binary and its package.json exposes only ./package.json — so the barrel goes and its twenty-two importers now name the module they want. biome's noBarrelFile was already flagging it; the rule is exempted for the two src/exports directories, which is the pattern this file was not following. The claims module went the same way. It had just become a re-export of the engine's token-claims, so it was a second barrel by the time it was flagged; its three importers now use the engine directly, and `serviceTokenWorkspaceId` loses its alias in favour of the one name the engine gives it. Test mocks that stubbed the barrel now name the module holding the function they replace, which also makes it visible which module each test is actually faking. Repo-wide barrel count is zero. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
The telemetry group said "Inspect and change anonymous CLI telemetry".
You do not change telemetry; you change whether the CLI sends it. It now
says so.
Three files built an SDK client with `http://localhost:0/auth/callback`
written out by hand. The SDK's config demands a redirect URI even for a
client that only ever calls the API with tokens it already has, so the
value exists to satisfy a type and no browser is ever sent to it. It is
now one named constant next to the real one, with a comment saying why
port zero is the honest choice.
In the interface draft: the two inline `import('…')` type positions
become a top-level `import type`, matching what the shipped
management-api module already does. And the telemetry snapshot's
description now says what it means — which command ran and which flags
were given, never what any of them was set to — instead of naming
itself "value-free" and leaving the reader to work it out.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
S2a landed on main as a squash (#130), so main carries its content without its commits. This branch already had those commits from the earlier merge, which is why the overlap resolved cleanly on content. Eight conflicts, all resolved toward the state this branch already holds, checked one at a time rather than by taking a side wholesale: - v8/cli.ts: kept the exported spec constants and all 40 mounted commands; main's inline version is the pre-slice shape. - The three .drive documents: kept this branch's, which are newer and carry the removal of the committed workstation paths. - v8-golden-rendering.test.ts: this branch has main's three entries plus the masked-secret one; the only text main had that this branch lacked was the file header, which was deliberately rewritten to list four surfaces. - v8-update-check.test.ts: this branch's version spreads the original module before overriding, a superset of main's. - project-controller.test.ts: deleted here, import-path updated there. Deletion stands; the change was mechanical. - project-real-mode.test.ts: main still holds three git cases this branch deleted once their v8 equivalents existed. Kept the deletions and main's mock-path updates in the five surviving cases. Verified on the merge: build, cli-engine test, cli test at 62 files and 1006 tests, cli-telemetry test, typecheck and lint all green.
PR #130 was squash-merged, so main carries the whole s2a-foundations lineage as one commit rather than the commits this branch already merged. The content is identical — a diff between main and the s2a-foundations tip is empty — so the four conflicts are all the same shape: main's copy of a shared file predates this slice's additions, and ours is a superset. Verified rather than assumed. The mount map was checked programmatically: every command main mounts is still mounted here, and this branch adds exactly its seventeen. The plan kept every slice heading main has, including the S8 the other stream wrote. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
You can now be signed in to several workspaces at once, and say which one you are working in without signing in again.
Today's CLI stores the same per-workspace tokens but only really models one "active" workspace: it re-fetches workspace names on every read, ends a single workspace through
auth logout --workspace, and can leave entries behind that nothing cleans up. The runs above are the new shell against a real credential file — the second command changed which session is current without opening a browser, and the third failed at exit 2 with an error that tells you the only thing that can fix it.The decision
We are replacing this repo's commander-based shell with
@prisma/cli-engine(merged in #129). Every platform command will be re-mounted on the engine and the old shell will be deleted. Before that port can start, the engine has to be production-ready and the auth family has to sit on something the rest of the port can build on. This pull request builds those foundations and proves them by porting the first command group,auth. The port itself follows in sibling pull requests for resources, services, and init plus shell removal.The foundations
An API client on the command context. Nearly every platform command calls the management API. Rather than each command constructing its own SDK client, the engine builds
ctx.apionce per run, lazily, for the session that process is acting as. A token refreshed mid-run is picked up on the next request, and an expired session surfaces as the standard sign-in error instead of a crash. The test harness accepts a fake client, which is the single mock seam every ported command will use.Real interactive prompts. On a terminal, prompts render through
@clack/prompts— the same library today's CLI uses, so the upcominginitwizard keeps its current feel. In tests, pipes, and CI a plain line-based renderer runs instead, so no test depends on terminal rendering. Clack is internal to the engine and invisible in its public API. Its spinners are deliberately unused, because they install process-global signal handlers.Telemetry, identical to the ORM CLI's. The detached sender process, the consent configuration with a shared installation id, and the value-free command snapshots move into this repo and report through one engine hook that fires once per command with its id, exit code, and duration.
telemetry status|enable|disableare ported. Flag values, arguments, and paths never reach the wire, which hostile-input tests hold down.One version number and prisma/prisma's release machinery. This repo adopts prisma/prisma's versioning model unchanged: a single lockstep version, now
8.0.0-rc.1, committed in every manifest and advanced only by a maintainer runningpnpm bump-versionand merging the resulting pull request.prisma --versionreports it. Merging a bump publishes that version tolatest; ordinary merges publish-dev.Nbuilds. The old publish-time arithmetic against npm dist-tags is deleted.@prisma/compute, an app-runtime library slated for extraction to another repo, keeps its own line. Merge order: merge #131 first. It pre-sets the root version onmain, so merging this pull request is a version no-op to the publish workflow and ships only a development build. The first reallatestrelease then happens through a deliberate release bump.The auth rework: sessions, and the thing that is not one
The auth family no longer keeps a pile of credentials with a pointer at one of them. It separates three things that were previously one.
A session is a stored logged-in-ness for one workspace — at most one per workspace. It is the only thing called a session: what
auth workspace listlists, whatuseselects, whatlogoutends. The selection is a separate scalar of stored state: which session is used where a session is needed. And the active credential is what a given process authenticates as, which is either the selected session's or the onePRISMA_SERVICE_TOKENsupplies.That third thing is why this went through two revisions. The first modelled the environment token as a session, and it produced four defects that were all the same defect: a
sourcefield whose job was to say "this one is not really a session", a hardcodedcurrent: truethat gave the word two meanings, an empty-string workspace id because a non-session was forced to carry a session's key, and a guard rejecting the non-session from APIs that only take sessions. Separating the three made all four stop existing rather than each need a fix.A new component, the credential manager, owns the state. It is the only thing that touches the credential file, and it is also what the platform SDK writes through, so a token refreshed on a 401 lands under the same rules as a fresh login. The engine owns the API client; the manager never hands a token to a command, and never talks to the user or opens a browser.
Four decisions are worth knowing:
prisma auth loginfixes it and nothing else has to.The commands keep their existing names, which the session model makes honest:
auth login,auth logout,auth whoami, andauth workspace list|use|logout.auth workspace useselects among the sessions you already hold and never creates one, because the consent screen cannot be told which workspace to grant — you pick it in the browser.The engine grew three things the auth commands needed, all of which the rest of the port will use. A repeatable global
--confirm <value>flag carries consent for destructive work: interactively you type the value to confirm, non-interactively you pass it exactly, and--yesdeliberately cannot grant it.ctx.openUrlopens a browser and degrades to printing the URL.prompt.browserWaitwaits for a browser round trip on a terminal and returns a structured "this needs interaction" error at exit 2 everywhere else.What changes for users
The published binary still runs the old shell; the engine-based shell is an unpublished development binary until the port finishes. For the commands ported here, every behavioural difference is written down in the divergence record rather than left to be discovered. The ones most likely to affect someone:
auth logout --workspace <ref>is gone.auth workspace logout <ref>is the one way to end a single session, andauth logoutnow ends every session and reports how many, reaping orphaned entries the old code could leave behind.PRISMA_SERVICE_TOKENis set, where the old CLI refused it. The variable supplies the credential this process uses; it does not occupy a slot, so changing stored state is coherent and every mutation succeeds, each saying the environment credential stays in force until you unset it.prismaprocess removes the session between your command reading it and writing, you now get exit 0 rather than an exit 2 telling you something that is no longer true.AUTH.NO_SESSION_FOR_WORKSPACE), and several failures that exited 1 now exit 2, which means "could not complete" rather than "crashed".auth whoamireturns the active credential rather than an auth-state snapshot: workspace, user, source, and expiry. Identity comes from the credential's own claims, enriched from/v1/mewhen that answers within a short deadline. A service token whose subject names a workspace reports no user, where a naive reading would have putworkspace:<id>in the user field.--confirm <value>. The mock-only login flags--provider,--user, and--workspacedo not port.Verification
840 CLI tests, 258 engine tests, 97 telemetry tests, typecheck, and lint all pass on Linux, macOS and Windows. Beyond the usual coverage, the credential manager is tested across real processes on a real filesystem: two processes mutating at once both land, a crashed process's lock is taken over, a running process keeps its pinned session when another switches the marker, and two processes really refresh the same session through a scripted token endpoint. A filesystem spy asserts that no read path writes, including migration adoption, and a leak scan seeds known secrets and asserts they never appear in output, debug logs, error metadata, envelopes, or a worker process's stderr.
Alternatives considered
whoami.auth workspace usecreate a session by opening the browser: rejected, and it is not actually possible — the authorize request carries no workspace parameter, so the CLI cannot ask for a particular workspace. Creating a session belongs toauth loginalone.The remaining open design questions are collected in s2-overview.md.