diff --git a/docs/design/03-domain-model/glossary.md b/docs/design/03-domain-model/glossary.md index 2e1e1d8e..13d7a9f1 100644 --- a/docs/design/03-domain-model/glossary.md +++ b/docs/design/03-domain-model/glossary.md @@ -366,7 +366,7 @@ is in `layering.md`; this is the term-by-term catalogue. - **Stack** — the root of an Alchemy program; a set of Resources deployed as a unit. `Alchemy.Stack(name, { providers, state }, Effect.gen(…))`. `lower()` - emits one Stack for the whole app; `prisma-composer deploy ` drives it over the + emits one Stack for the whole app; `prisma deploy ` drives it over the app module directly (no hand-written stack file, no config file — ADR-0003). `→` **Topology / implicit root Module**. - **Resource\** — a managed entity with a string type diff --git a/docs/design/10-domains/README.md b/docs/design/10-domains/README.md index ff3342dd..faaba071 100644 --- a/docs/design/10-domains/README.md +++ b/docs/design/10-domains/README.md @@ -6,11 +6,11 @@ architecture/design phase that comes *after* the high-level model is settled. - [`core-model.md`](core-model.md) — the complete class/data-structure design of `@prisma/composer` and the target-pack contract, with `@prisma/composer-prisma-cloud` as the worked instance. -- [`deploy-cli.md`](deploy-cli.md) — Prisma Composer's own deploy - entrypoint (`prisma-composer deploy` / `prisma-composer destroy`): the pipeline, +- [`deploy-cli.md`](deploy-cli.md) — Prisma Composer's deploy entrypoint + (`prisma deploy`, and the `destroy` operation): the pipeline, stages and container (Project/Branch) resolution, the pack CLI seam and per-kind assembly contracts, and the error surface. Rests on ADR-0003 … - ADR-0006, ADR-0023, ADR-0024. + ADR-0006, ADR-0023, ADR-0024, ADR-0049 and ADR-0050. - [`module-composition.md`](module-composition.md) — module boundaries (deps/expose), forwarding, nesting, and the packaged reusable module. Rests on ADR-0016. @@ -18,9 +18,10 @@ architecture/design phase that comes *after* the high-level model is settled. (a caller-owned schema), carried through deploy, serialized to platform storage by the target (over key/value string pairs), and read back at boot. Rests on ADR-0018 and ADR-0019. -- [`local-dev.md`](local-dev.md) — the local dev loop (`prisma-composer dev`): +- [`local-dev.md`](local-dev.md) — the local dev loop (`prisma dev`): the pipeline deltas vs deploy, the process table and supervisor, per-resource - stand-ins, value sourcing, and the dev error surface. Rests on ADR-0041. + stand-ins, value sourcing, and the dev error surface. Rests on ADR-0041 and + ADR-0050. The settled high-level model is recorded in: diff --git a/docs/design/10-domains/core-model.md b/docs/design/10-domains/core-model.md index 29dfa732..8bcfd7a1 100644 --- a/docs/design/10-domains/core-model.md +++ b/docs/design/10-domains/core-model.md @@ -54,9 +54,9 @@ glossary's Lowering — live in `/deploy`): **`/control` is a shipped entry, not a reservation** (ADR-0017): every extension exposes one, and it is where its heavy deploy-time code lives — `@prisma/composer-prisma-cloud/control` exports `prismaCloud()`, -`@prisma/composer/node/control` exports `nodeBuild()`. Only -`prisma-composer.config.ts` imports those entries, and only the CLI loads that -file, so nothing an app's own import graph reaches can pull control-plane code +`@prisma/composer/node/control` exports `nodeBuild()`. Only the `composer` +section of `prisma.config.ts` imports those entries (ADR-0049), and app code +never imports that file, so nothing an app's own import graph reaches can pull control-plane code into the runtime artifact. Core's own control-plane surface (`Load`, `configOf`) is small and pure and still sits on `.`; the config *types* (`defineConfig`, `ExtensionDescriptor`) live on `@prisma/composer/config`. @@ -64,7 +64,7 @@ is small and pure and still sits on `.`; the config *types* (`defineConfig`, | Entry | Exports | Imports (weight) | | --- | --- | --- | | `@prisma/composer` | node factories (`service`, `resource`, `dependency`, `module`), `Load`, `configOf`, `hydrate`, `BuildAdapter` type, model types (incl. `Config`) | nothing | -| `@prisma/composer/config` | `defineConfig`, `PrismaAppConfig`, `ExtensionDescriptor`, `NodeDescriptor`, `PreflightInput`, `TeardownInput` — the types `prisma-composer.config.ts` is checked against (ADR-0017) | nothing (types + one identity function) | +| `@prisma/composer/config` | `defineConfig`, `PrismaAppConfig`, `ExtensionDescriptor`, `NodeDescriptor`, `PreflightInput`, `TeardownInput` — the types the `composer` section of `prisma.config.ts` is checked against (ADR-0017, ADR-0049) | nothing (types + one identity function) | | `@prisma/composer/deploy` | `lower()`, `lowering()`, the SPI types (`ServiceLowering`, `Lowering`, `ApplicationDescriptor`, `ProvisionerDescriptor`, `LowerContext`, `Outputs`, `LoweredResult`, `DeployedEntity`), `Bundle`/`AssembleInput` (the assembler's contract, defined once here) | `alchemy`, `effect` | | `@prisma/composer-prisma-cloud` | `compute()` (declares a service; carries `run`/`load`), `rawPostgres()` (`{ name }` identity or `{ client }` dependency, by argument shape) + `rawPostgresContract`, `http()` | `@prisma/composer` only | | `@prisma/composer/arktype` | `secretString()` — the arktype spelling of a `SecretString` input leaf (ADR-0042). Opt-in: no other entry imports it, so a Zod app never loads arktype | `arktype` | @@ -105,14 +105,14 @@ Who imports what, end to end: bundles this file (their bundler, or `next build`) — the framework never touches it; - the **deploy entry is the app module itself**: everything about the *application* is still derived from the root node (ADR-0003). - `prisma-composer deploy ` imports it and calls + `prisma deploy ` imports it and calls `@prisma/composer/deploy`'s `lower()` internally; -- the **`prisma-composer.config.ts`** at the app root carries the two things the - graph cannot yield — the **extension list** and the **state store** (ADR-0017). - The CLI finds it by walking up from the deploy entry and loads it with c12. It - is the only importer of the `/control` entries where heavy code lives, and app +- the **`composer` section of `prisma.config.ts`** carries the two things the + graph cannot yield — the **extension list** and the **state store** (ADR-0017, + ADR-0049). The Prisma CLI engine finds and loads the file, searching from the + command's working directory up to the repository root. The section is the only importer of the `/control` entries where heavy code lives, and app code never imports it, which is what keeps that code out of the artifact. The - app author still writes no *stack* file — `prisma-composer deploy` generates one + app author still writes no *stack* file — `prisma deploy` generates one at `.prisma-composer/alchemy.run.ts` per run and drives it; see [`deploy-cli.md`](deploy-cli.md). @@ -455,10 +455,10 @@ finds, deps before dependents. import type { Layer } from "effect" import type { Effect } from "effect" -// The config file's default export (ADR-0017). `prisma-composer.config.ts` sits at -// the app root and STATICALLY imports each extension's /control entry, so the -// framework never builds a module specifier, never resolves a path, and never -// imports by a computed name. Only the CLI loads it — app code never does, which +// The `composer` section of prisma.config.ts (ADR-0017, ADR-0049). The file +// STATICALLY imports each extension's /control entry, so the framework never +// builds a module specifier, never resolves a path, and never imports by a +// computed name. The Prisma CLI engine loads it — app code never does, which // is what keeps control-plane code out of the runtime artifact. interface PrismaAppConfig { readonly extensions: ExtensionDescriptor[] @@ -638,7 +638,7 @@ interface DeploymentResult { interface LowerOptions { readonly name: string // stack name (+ Load's root id override) - // `prisma-composer deploy` runs each service's build-adapter assembler and writes + // `prisma deploy` runs each service's build-adapter assembler and writes // the resulting bundle dirs here, into the generated stack file it hands to // `lower()` — one bundle per provision id (the deploy root is always a // module). A hand-composed / mixed-stack caller (the escape hatch — see @@ -965,7 +965,7 @@ const stash = (shape: readonly ConfigDeclaration[], config: Config): void => { / ``` Control entry (`@prisma/composer-prisma-cloud/control`) — the extension -descriptor `prisma-composer.config.ts` lists, and the only place `@internal/lowering` +descriptor the `composer` section of `prisma.config.ts` lists, and the only place `@internal/lowering` is imported. Each node kind's hooks live in their own descriptor file; the descriptor stitches them into one `nodes` registry: @@ -1210,11 +1210,11 @@ Bun.serve({ port, hostname: "0.0.0.0", // The app builds itself first (its own bundler produces dist/server.js), then: // -// prisma-composer deploy src/module.ts +// prisma deploy src/module.ts // // The application is derived from the module root (ADR-0003); the extension list -// and state store come from prisma-composer.config.ts at the app root (ADR-0017), -// whose /control entries the CLI loads. It runs each service's assembly and drives +// and state store come from the `composer` section of prisma.config.ts (ADR-0017, +// ADR-0049), whose /control entries the CLI loads. It runs each service's assembly and drives // Alchemy — no bundle map, no hand-written stack file. ``` @@ -1278,7 +1278,7 @@ export default module("storefront-auth", (h) => { }) // No deploy config file (ADR-0003): build both apps, then -// prisma-composer deploy app.ts +// prisma deploy app.ts ``` At deploy, core sequences: the db resource (lowered once) → auth provision → @@ -1326,7 +1326,7 @@ producer from a resource — one mechanism. - **Build-adapter ecosystem** — `node` and `nextjs` are the first two; the descriptor/assembler split is the seam for community adapters (Nuxt, TanStack Start, a cron access-pattern, a static site). Each is a package the app lists in - its `prisma-composer.config.ts`; nothing in core, the prisma-cloud extension, + the `composer` section of its `prisma.config.ts`; nothing in core, the prisma-cloud extension, `@internal/assemble`, or the CLI changes to add one — the assembler is a `{ kind: "build" }` entry in the adapter's own `/control` registry, found by the build descriptor's `(extension, type)` pair (ADR-0017), exactly like every other diff --git a/docs/design/10-domains/deploy-cli.md b/docs/design/10-domains/deploy-cli.md index 5d989bf0..a4eac0f2 100644 --- a/docs/design/10-domains/deploy-cli.md +++ b/docs/design/10-domains/deploy-cli.md @@ -1,7 +1,7 @@ -# The deploy CLI (`prisma-composer`) +# The deploy commands (`prisma deploy`, `prisma dev`) -Prisma Composer's own deploy entrypoint: what the `prisma-composer` -command does, the contracts it introduces, and what stays out of its scope. +Prisma Composer's deploy entrypoint: what the `prisma deploy` and `prisma dev` +commands do, the contracts they introduce, and what stays out of its scope. The decisions it rests on are recorded in [ADR-0003](../90-decisions/ADR-0003-deploy-derives-everything-from-the-root-node.md) @@ -17,33 +17,41 @@ root's name names the application), [ADR-0008](../90-decisions/ADR-0008-wrapper-inlines-everything-except-runtime-builtins.md) (wrapper inlining), and [ADR-0017](../90-decisions/ADR-0017-control-plane-loads-through-the-app-config.md) -(control plane loads through `prisma-composer.config.ts`). +(control plane loads through the app's config, the `composer` section of +`prisma.config.ts` per +[ADR-0049](../90-decisions/ADR-0049-composers-configuration-is-the-composer-section-of-prisma-config.md)), +and +[ADR-0050](../90-decisions/ADR-0050-composer-runs-as-prisma-deploy-and-prisma-dev.md) +(the command surface: two commands in the `prisma` CLI, no Composer binary). ## Scope -Three commands: +Two commands, mounted by the `prisma` CLI from `@prisma/composer-cli`'s +command family, plus one operation with no command: -- **`prisma-composer deploy `** — deploy the application whose root node is +- **`prisma deploy `** — deploy the application whose root node is `entry`'s default export, to a stage (default: production). -- **`prisma-composer destroy `** — tear a stage down (same derivation, - Alchemy destroy); the target stage is always explicit (see § Stages and - containers). -- **`prisma-composer dev `** — bring up the application whose root node +- **`destroy`**, an operation on `@prisma/composer/control` with no command — + tear a stage down (same derivation, Alchemy destroy); the target is always + explicit (see § Stages and containers). Users call it from a script. +- **`prisma dev `** — bring up the application whose root node is `entry`'s default export, entirely on this machine, credential-free — local counterparts of every provisioned resource, real service processes, no platform account. See [local-dev.md](local-dev.md) (ADR-0041) for the design; flags: `--name` (same override semantics as deploy), `--fresh` (tear down and reprovision every local instance before starting). -Deploy/destroy flags: `--name` (override the root's name — per-run ephemeral +`deploy` flags: `--name` (override the root's name — per-run ephemeral deploys in shared workspaces), `--stage ` (target a named, isolated -environment instead of production), `--production` (destroy-only — -explicitly target the production environment). `--stage`/`--production` do -not exist on `dev` (clipanion rejects them as unknown flags). `prisma-composer -build` and topology emission are out of scope (see § Out of scope). - -**Runtime.** The bin is runtime-agnostic — no bun-only APIs anywhere in the -CLI or assembly code — so it runs under both bun and node (≥ 22.18, where +environment instead of production), `--report ` (write the deploy's +outcome as JSON to that file), `--build-id ` (join a deploy record that CI +already created). The `destroy` operation takes the same +`name` and a required `target`, `{ kind: 'stage', stage }` or +`{ kind: 'production' }`. `--stage` does not exist on `dev`. A build command +and topology emission are out of scope (see § Out of scope). + +**Runtime.** The command family is runtime-agnostic — no bun-only APIs +anywhere in the CLI or assembly code — so it runs under both bun and node (≥ 22.18, where type stripping imports the user's `.ts` entry natively). Under node, the CLI also registers a synchronous resolve hook (`node:module` `registerHooks`) so that relative imports inside the entry graph may use `./x.js` or extensionless @@ -68,7 +76,7 @@ user can rerun it by hand (ADR-0007). ## The pipeline -`prisma-composer deploy` is one pass from a module path to a driven Alchemy stack: +`prisma deploy` is one pass from a module path to a driven Alchemy stack: 1. **Import the entry module.** Its default export must be a node (service or Module). No marked root exists in the model — whatever you point the CLI at @@ -79,10 +87,9 @@ user can rerun it by hand (ADR-0007). composing Module. The deploy root must be a Module — a bare service is not independently deployable; the CLI errors naming the fix (wrap it: `module('name', ({ provision }) => { provision(...); })`). -3. **Load the config + validate coverage.** `prisma-composer.config.ts` (or its - `.mts`/`.mjs`/`.js` spelling; `.ts` wins when several sit in one directory) — - found by walking up from the deploy entry, loaded with c12, never imported - by app code — supplies the extension registries and the deploy's one state store +3. **Load the config + validate coverage.** The `composer` section of + `prisma.config.ts` — loaded by the Prisma CLI engine (ADR-0049), never + imported by app code — supplies the extension registries and the deploy's one state store (ADR-0017). Every node's and build descriptor's `(extension, type)` must have a registry entry; a gap errors naming the extension to add to the config. Extension factories validate their own environment during config @@ -129,7 +136,7 @@ file, and consumed in one motion. ## Stages and containers An app deploys to a named **stage** — a deploy-time environment, never -authored in the topology (ADR-0024). `prisma-composer deploy` with no `--stage` +authored in the topology (ADR-0024). `prisma deploy` with no `--stage` targets **production**; `--stage ` targets a **named stage**. - **Containers.** Any extension whose platform has platform-side containers — @@ -149,14 +156,14 @@ targets **production**; `--stage ` targets a **named stage**. extension; core hands that extension's own resolved container to `state.create()`, so the state layer is built from it rather than from the environment. -- **Destroy is explicit.** `prisma-composer destroy` requires `--stage ` or `--production`; a bare `destroy` is an error, so an omitted or mistyped stage can never silently tear down production. `destroy` resolves find-only (no container is ever created); after `alchemy destroy` succeeds and after every extension's `teardown` has run, the CLI removes each resolved container. That two-loop order — every teardown, then every removal — is what guarantees every extension's teardown runs against a still-live container. +- **Destroy is explicit.** The `destroy` operation requires a `target`, `{ kind: 'stage', stage }` or `{ kind: 'production' }`; a call without one is an error, so an omitted or mistyped stage can never silently tear down production. `destroy` resolves find-only (no container is ever created); after `alchemy destroy` succeeds and after every extension's `teardown` has run, the CLI removes each resolved container. That two-loop order — every teardown, then every removal — is what guarantees every extension's teardown runs against a still-live container. **Prisma Cloud's own containers** are its app's **Project** and, for a named stage, that stage's **Branch** — found by name, created if absent on deploy, never created on destroy; each stage's deploy state lives behind the platform state API, scoped to its Branch (production's to the Project's implicit default Branch). See [ADR-0023](../90-decisions/ADR-0023-a-prisma-app-is-one-project-a-stage-is-a-branch.md) (App = one Project, Stage = Branch), [ADR-0024](../90-decisions/ADR-0024-a-stage-is-a-deploy-time-environment-resolved-to-project-and-branch.md) (stage resolution mechanics), and [ADR-0045](../90-decisions/ADR-0045-deploy-state-lives-behind-the-platform-state-api.md) (deploy state behind the platform state API, per Branch). ## Build ownership Per ADR-0005, the CLI initiates no user builds. The contract is that built -output exists first — `turbo run build && prisma-composer deploy`, or whatever the +output exists first — `turbo run build && prisma deploy`, or whatever the user's tooling does. Assembly *consumes* that output and applies the framework's envelope: @@ -174,7 +181,7 @@ current model. ## Contracts this introduces One seam, uniform for every node kind — the **extension seam** (ADR-0017): -the app's `prisma-composer.config.ts` statically imports each extension's control +the `composer` section of the app's `prisma.config.ts` statically imports each extension's control descriptor, and deploy tooling looks up control-plane behavior by the data every node already carries: @@ -218,19 +225,18 @@ them: | Default export isn't a node | what the entry module must export | | Deploy root isn't a Module | to wrap the service in a Module | | Unwired dependency slot | which input, and to deploy the composing Module | -| Missing `prisma-composer.config.ts` | the expected filename, where the walk-up looked, and what it must export | -| Node `(extension, type)` not covered | the extension to add to `prisma-composer.config.ts` | +| Missing `composer` section in `prisma.config.ts` | what the section must contain (ADR-0049) | +| Old `configPath` field in the section (`CONFIG.FIELD_RETIRED`) | to move `extensions` and `state` into the section and delete the old file (ADR-0049) | +| Old `prisma-composer.config.*` beside `prisma.config.ts` (`CONFIG.FILE_RETIRED`) | the same fix (ADR-0049) | +| Node `(extension, type)` not covered | the extension to add to the `composer` section | | Missing extension env | the exact variable(s) the extension factory needed | | Built output missing | the expected path, and "run your build" | ## Out of scope (designed around) -- **`prisma-composer build`** — and with it any build-command convention or override. +- **A build command** — and with it any build-command convention or override. - **Topology emission** — the serialized-topology artifact for agents/tooling; when it lands it must strip the machine-specific `build.module` (ADR-0004). -- **Config-file escape hatch** — a `prisma-composer.config.ts` may exist one day as - the *optional* override for multi-target or heavily parameterized setups; - never the standard path. - **Freshness checks** — detecting stale (not just missing) built output. - **Entry discovery** — the entry path is required; bare invocation errors with usage. A discovery convention (e.g. a `package.json` field) would be @@ -258,7 +264,7 @@ them: ## Known limitations -- **`destroy` requires built artifacts.** `prisma-composer destroy` evaluates the +- **`destroy` requires built artifacts.** The `destroy` operation evaluates the same stack program as deploy, and the pack's `package()` reads the assembled bundle — so the app must build before it can be torn down. The destroy-path error says exactly that. Whether Alchemy's destroy can run diff --git a/docs/design/10-domains/local-dev.md b/docs/design/10-domains/local-dev.md index e0174036..c5c5cc60 100644 --- a/docs/design/10-domains/local-dev.md +++ b/docs/design/10-domains/local-dev.md @@ -1,4 +1,4 @@ -# Local dev (`prisma-composer dev`) +# Local dev (`prisma dev`) The local dev loop: one command brings up the whole topology from the root module, credential-free, with deploy parity everywhere above the Alchemy @@ -11,29 +11,33 @@ this doc is the mechanics. ## Scope -Two commands: +One command and one operation: -- **`prisma-composer dev `** — bring up the application whose root node +- **`prisma dev `** — bring up the application whose root node is `entry`'s default export, entirely on the local machine, and keep it up: watch built output, restart changed services, until interrupted. It does **not** stream service logs — once it is supervising several processes, - streaming them all inline drowns the front door and the rebuild notices; it - prints a one-line pointer to `log` instead. -- **`prisma-composer log [address]`** — tail the merged logs of that - already-running app: every service, or one named by its dotted `address` - (`catalog.service`). Follows live; `--tail ` sets how much recent history - to show first (default 20, `0` for live-only). It only reads the running + streaming them all inline drowns the front door and the rebuild notices. To + read them, call the `log` operation from a script (see + [docs/guides/running-locally.md](../../guides/running-locally.md)). +- **`log`**, an operation on `@prisma/composer/control` with no command + (ADR-0050) — tail the merged logs of that already-running app: every + service, or one named by its dotted `address` (`catalog.service`). Follows + live until the caller aborts; a `tail` count sets how many recent lines to + return first (default 0, which returns live output only). It only reads the running app's logs — it neither builds, provisions, starts, nor stops anything. -Flags: `dev` takes `--fresh` (destroy the dev stack and wipe the dev state -directory before starting). Stages do not apply — a working directory has +Flags: `dev` takes `--name` (override the root's name, as on deploy) and +`--fresh` (destroy the dev stack and wipe the dev state directory before +starting). Stages do not apply — a working directory has exactly one dev instance; parallel instances are parallel checkouts. -**Naming.** goals.md calls the local emulator "`prisma dev`". That name is -owned today by the ORM CLI's local-Postgres command — which this harness itself -shells out to. The command is therefore `prisma-composer dev`; convergence on a -shorter name is a CLI-distribution question (Composer joining a unified -`prisma` CLI), not a design question here. +**Naming.** The `prisma` CLI mounts this as `prisma dev ` +([ADR-0050](../90-decisions/ADR-0050-composer-runs-as-prisma-deploy-and-prisma-dev.md)). +Local Postgres comes from `@prisma/dev`, the local Prisma Postgres server +library that the Prisma 7 ORM CLI's own `prisma dev` command runs. Composer +does not run that command: its Postgres emulator daemon calls `@prisma/dev`'s +`startPrismaDevServer()` in-process, one server per `Database` resource. ## The pipeline, relative to deploy @@ -55,8 +59,9 @@ Dev re-runs [deploy's pipeline](deploy-cli.md#the-pipeline) with these deltas: stable local identity from the app name with no platform calls. Then the `localTarget.emulators` hook inspects the loaded graph and ensures one emulator daemon per node kind the topology uses (Compute always; buckets when - bucket resources exist; Postgres needs no pre-start — its instances are - created at provision through the ORM CLI). + bucket resources exist; Postgres when `postgres` or `raw-postgres` + resources exist). The Postgres daemon starts no server here: the database + provider asks it for one `@prisma/dev` server per `Database` at provision. 5. **Lower + converge** — a dev-generated stack file (ADR-0007's pattern, at `.prisma-composer/dev/alchemy.run.ts`), driven with the extension's `localTarget.providers()` layer and `devState()` (Alchemy's built-in file store, exported by `@prisma/composer/local-target`), always @@ -66,7 +71,7 @@ Dev re-runs [deploy's pipeline](deploy-cli.md#the-pipeline) with these deltas: 6. **Attach** — new, dev-only: through `localTarget.attach`, render the front door (every service's endpoint) and watch for rebuilds, loop. Ctrl-C stops the app's service instances through the attachment and exits; emulators and - data persist. Logs are not streamed here — `prisma-composer log` reads the + data persist. Logs are not streamed here — the `log` operation reads the same attachment's merged-log view on demand, so `dev`'s own output stays the front door plus lifecycle and rebuild notices. @@ -94,7 +99,7 @@ emulator, which owns the processes: crash-backoff respawn keeps the run's crash trail, but past sessions and past deployments are gone. A follow starts at the current end by default (`?tail=` includes the last `n` lines as backlog first); - `prisma-composer log`'s `--tail` maps straight onto it. A live follower is + the `log` operation's `tail` count maps straight onto it. A live follower is reset to the new start when the log is cleared under it (the daemon tracks a per-log clear generation, so a follower never reads stale bytes). - **Instance deleted** (service removed from the topology, `--fresh`) → stop @@ -112,7 +117,7 @@ emulator, which owns the processes: unchanged from its last recorded apply. A Ctrl-C stop is invisible to that diff: nothing about the resource's *props* changed, only the process's live status, which Alchemy's state file does not track. So a second - `prisma-composer dev` after a plain Ctrl-C can converge with everything + `prisma dev` after a plain Ctrl-C can converge with everything reported "noop" and leave every previously-stopped service `stopped` — the CLI still prints `[dev] ready:` with each service's URL, but nothing is listening on them. Confirmed against the open-chat proving port (the @@ -146,8 +151,8 @@ the scoping changes restart behavior only. nothing started here survives its exit. 2. **Machine-scoped emulators** — one per node kind per registry root, multi-tenant, holding every long-lived process: the Compute emulator - (service children), the `prisma dev` Postgres instances (managed by the - ORM CLI, per-Database), and the bucket emulator. They survive dev + (service children), the Postgres emulator (one `@prisma/dev` server per + `Database`, hosted in-process), and the bucket emulator. They survive dev sessions; `--fresh` removes an app's instances. The registry root is `~/.prisma-composer/emulators` unless `PRISMA_COMPOSER_EMULATORS_DIR` names another absolute directory, so by default one machine shares one @@ -159,12 +164,12 @@ the scoping changes restart behavior only. checkout stops that. 3. **The dev session owns no processes at all** — it is a view (`attach`): endpoints, the stop control, and the watch loop. That same view's merged - logs back the separate `prisma-composer log` command. + logs back the separate `log` operation. The emulator daemon bookkeeping (registry, stable ports, readiness, -version-skew restart) is framework-owned and minimal; Postgres reuses the -ORM CLI's mature manager (`prisma dev ls|stop|rm`) rather than duplicating -it. +version-skew restart) is framework-owned and minimal, and the Postgres +emulator uses the same bookkeeping, hosting `@prisma/dev` servers rather than +reimplementing a Postgres server. ## Resource substitution @@ -175,7 +180,7 @@ semantics): | Resource | Dev behavior | | --- | --- | | `Project` | a local identity record; no platform | -| `Database` | a database on the local Postgres server (ORM `prisma dev`) | +| `Database` | a database on a local `@prisma/dev` Postgres server, hosted by the Postgres emulator | | `Connection` | the local connection URL | | `App` | registers the service with the Compute emulator, which allocates its stable port; `appEndpointDomain = http://localhost:` — which makes origin (ADR-0039) work unchanged | | `Deployment` | unpacks the artifact once per hash, materializes the env, and puts the deployment at the Compute emulator, which (re)starts the child | @@ -194,7 +199,7 @@ SQLite test server remains a testing utility, not part of the dev loop. ### Postgres -The emulator is the ORM CLI's local Postgres (`prisma dev`), **one named, detached instance per `Database` resource** — instance names are derived from the app and database ids, so instances are isolated, discoverable (`prisma dev ls`), and survive across dev sessions for warm starts. Migrations are not special-cased: `OrmMigration` runs exactly as it does in a deploy, against the local URL — replay-only (ADR-0022 as revised), so it applies committed migrations and never synthesizes schema. At reconcile time it reloads the emitted `contract.json` identified by `prisma.config.ts`, attests its `storageHash` against the compact contract identity persisted in deploy state, and only then opens the database. Dev-loop schema iteration therefore happens through the ORM's own `prisma db update`, run directly against the emulator database: `db update` moves the database and its marker to the current contract, and the pipeline's migration step no-ops because the marker matches the target. A dev run against a database that was neither updated nor covered by a planned migration hits the same structured refusal a deploy would, naming both exits (`prisma db update` to iterate, `contract emit` + `migration plan` to author the path). `PgWarm` is near-instant locally and is kept (not stubbed) so the provider set stays uniform. +The emulator is a daemon hosting `@prisma/dev` servers, **one named, persistent server per `Database` resource** — server names are derived from the app and database ids, so instances are isolated and survive across dev sessions for warm starts. Migrations are not special-cased: `OrmMigration` runs exactly as it does in a deploy, against the local URL — replay-only (ADR-0022 as revised), so it applies committed migrations and never synthesizes schema. At reconcile time it reloads the emitted `contract.json` identified by `prisma.config.ts`, attests its `storageHash` against the compact contract identity persisted in deploy state, and only then opens the database. Dev-loop schema iteration therefore happens through the ORM's own `prisma db update`, run directly against the emulator database: `db update` moves the database and its marker to the current contract, and the pipeline's migration step no-ops because the marker matches the target. A dev run against a database that was neither updated nor covered by a planned migration hits the same structured refusal a deploy would, naming both exits (`prisma db update` to iterate, `contract emit` + `migration plan` to author the path). `PgWarm` is near-instant locally and is kept (not stubbed) so the provider set stays uniform. ### Buckets: a disk-backed S3 emulator @@ -266,9 +271,9 @@ Deploy's rule holds: every failure names its fix. | extension has no `localTarget` descriptor | which extension, and that it does not support local dev | | built output missing | same as deploy: the expected path, "run your build" | | `bun` not on PATH | that dev runs services under bun (the Compute runtime) and how to install it | -| no installed `prisma` bin (the local-Postgres emulator) | what was searched for and to add `prisma` to devDependencies | +| `@prisma/dev` cannot be loaded (the local-Postgres emulator) | the module path that was tried | | the bucket emulator fails to start or report healthy | the instance name, its log file path, and the port it tried | -| ORM `prisma dev` fails to start | the exact command that was attempted and its output, sanitized — connection-URL credentials are masked before embedding | +| a `@prisma/dev` server fails to start | the underlying error, sanitized — connection-URL credentials are masked before embedding | | port conflict on a persisted allocation | which service, which port, and how to free or re-allocate (`--fresh`) | | secret slot unbound | warning (not an error) naming the env var and the placeholder behavior | | env-sourced param unbound | hard error listing the missing names, deploy-preflight style | @@ -294,7 +299,7 @@ Deploy's rule holds: every failure names its fix. (none outstanding from the design phase — see Known limitations below for gaps found during implementation.) -(Settled since the first draft: Postgres runs one named `prisma dev` instance +(Settled since the first draft: Postgres runs one named `@prisma/dev` server per `Database` resource; the front door prints every service URL ordered by address depth then name, shallowest first; port allocation and the remaining mechanics are described in this document and [ADR-0041](../90-decisions/ADR-0041-local-dev-runs-the-deploy-pipeline-against-local-providers.md). @@ -305,7 +310,7 @@ Restart latency is measured — see Known limitations.) The design's founding claim was proven against open-chat — a real, pre-existing Composer app in its own repo (chat service, Postgres, streams and storage modules), written before local dev existed. With no cloud -credentials of any kind in the shell, `prisma-composer dev module.ts` +credentials of any kind in the shell, `prisma dev module.ts` brought it up: sign-in worked, chat history loaded, and the live-tail stream delivered events. Chat generation failed at exactly one place — the outbound OpenRouter call — because the local run minted a placeholder for diff --git a/docs/design/10-domains/module-composition.md b/docs/design/10-domains/module-composition.md index 4a2ddd06..02004b89 100644 --- a/docs/design/10-domains/module-composition.md +++ b/docs/design/10-domains/module-composition.md @@ -147,13 +147,13 @@ What deploy requires is also provenance-independent, and already follows from the path and build rules: - **Built runnables must exist** at each service descriptor's - `dirname(module)`-relative `entry` when `prisma-composer deploy` runs (ADR-0005). + `dirname(module)`-relative `entry` when `prisma deploy` runs (ADR-0005). Who built them is the consumer's arrangement with the module: an app importing module source builds it like the rest of its code; a prebuilt package ships its runnables and satisfies the requirement at publish time (ADR-0004 makes the paths resolve correctly either way, including from inside `node_modules`). - **A service's build descriptor names its extension as data** (`extension` + - `type`), and the consuming app's `prisma-composer.config.ts` lists the extensions + `type`), and the `composer` section of the consuming app's `prisma.config.ts` lists the extensions its graph uses — including those a published module uses internally. The module documents them and can re-export a config fragment (`extensions: [...authModuleExtensions, prismaCloud()]`), keeping the cost to diff --git a/docs/design/90-decisions/ADR-0003-deploy-derives-everything-from-the-root-node.md b/docs/design/90-decisions/ADR-0003-deploy-derives-everything-from-the-root-node.md index 3bed5f5a..8ba51906 100644 --- a/docs/design/90-decisions/ADR-0003-deploy-derives-everything-from-the-root-node.md +++ b/docs/design/90-decisions/ADR-0003-deploy-derives-everything-from-the-root-node.md @@ -1,5 +1,7 @@ # ADR-0003: `prisma-composer deploy` derives the application from the root node +Amended by [ADR-0050](ADR-0050-composer-runs-as-prisma-deploy-and-prisma-dev.md): the command is `prisma deploy `; the `prisma-composer` binary is retired. Amended by [ADR-0049](ADR-0049-composers-configuration-is-the-composer-section-of-prisma-config.md): the extensions and state store live in the `composer` section of `prisma.config.ts`, not in `prisma-composer.config.ts`. The derivation from the root node below is unchanged. + ## Decision The deploy entrypoint is `prisma-composer deploy `, where `entry` is a module diff --git a/docs/design/90-decisions/ADR-0006-every-node-is-named.md b/docs/design/90-decisions/ADR-0006-every-node-is-named.md index 87c8e8a0..8512f88c 100644 --- a/docs/design/90-decisions/ADR-0006-every-node-is-named.md +++ b/docs/design/90-decisions/ADR-0006-every-node-is-named.md @@ -1,11 +1,13 @@ # ADR-0006: Every node is named; the root's name names the application +Amended by [ADR-0050](ADR-0050-composer-runs-as-prisma-deploy-and-prisma-dev.md): the override is `prisma deploy --name`; the `prisma-composer` binary is retired. The naming rules below are unchanged. + ## Decision Every node — module, service, or resource — carries an explicit, human-readable name, given at authoring. When a node is deployed as the root, its name becomes the application's name (on Prisma Cloud: the Project name). -`prisma-composer deploy --name` overrides it for a single run. Nothing derives a +`prisma-composer deploy --name` overrides it for a single run. *(Amended by [ADR-0050](ADR-0050-composer-runs-as-prisma-deploy-and-prisma-dev.md): the command is now `prisma deploy --name`.)* Nothing derives a name from a `package.json` or a directory. ## Reasoning diff --git a/docs/design/90-decisions/ADR-0007-deploy-drives-alchemy-through-a-generated-stack-file.md b/docs/design/90-decisions/ADR-0007-deploy-drives-alchemy-through-a-generated-stack-file.md index 8204cef1..662b87a3 100644 --- a/docs/design/90-decisions/ADR-0007-deploy-drives-alchemy-through-a-generated-stack-file.md +++ b/docs/design/90-decisions/ADR-0007-deploy-drives-alchemy-through-a-generated-stack-file.md @@ -1,5 +1,7 @@ # ADR-0007: Deploy drives Alchemy through a generated stack file +Amended by [ADR-0050](ADR-0050-composer-runs-as-prisma-deploy-and-prisma-dev.md): the commands that generate and drive the stack file are `prisma deploy` and `prisma dev`; teardown is the `destroy` operation on `@prisma/composer/control`, which drives `alchemy destroy` against the same generated file. The `prisma-composer` binary is retired. The generated-file mechanism below is unchanged. + ## Decision `prisma-composer deploy` materializes its work as a small, human-readable stack diff --git a/docs/design/90-decisions/ADR-0024-a-stage-is-a-deploy-time-environment-resolved-to-project-and-branch.md b/docs/design/90-decisions/ADR-0024-a-stage-is-a-deploy-time-environment-resolved-to-project-and-branch.md index fbd1242c..3e4a4f5e 100644 --- a/docs/design/90-decisions/ADR-0024-a-stage-is-a-deploy-time-environment-resolved-to-project-and-branch.md +++ b/docs/design/90-decisions/ADR-0024-a-stage-is-a-deploy-time-environment-resolved-to-project-and-branch.md @@ -1,5 +1,7 @@ # ADR-0024: A stage is a deploy-time environment; the CLI resolves its Project and Branch before Alchemy +Amended by [ADR-0050](ADR-0050-composer-runs-as-prisma-deploy-and-prisma-dev.md): deploys run as `prisma deploy [--stage ]`. Teardown is the `destroy` operation on `@prisma/composer/control`, whose required `target` is `{ kind: 'stage', stage }` or `{ kind: 'production' }`; there is no destroy command. The `prisma-composer` binary is retired. Stage resolution below is unchanged. + ## Decision An app is deployed to a named **stage** — an environment. The topology is @@ -96,7 +98,11 @@ derived from Branch presence, never from a role lookup. `--stage ` or `--production`; a bare destroy is an error, so an omitted or mistyped stage can never silently tear down production. Destroying a named stage removes its resources and then its Branch; the production Branch is - never deleted. + never deleted. *(Amended by + [ADR-0050](ADR-0050-composer-runs-as-prisma-deploy-and-prisma-dev.md): there + is no destroy command; the `destroy` operation's `target` is required, either + `{ kind: 'stage', stage }` or `{ kind: 'production' }`, so the same rule holds + in code.)* - **Stage is a first-class deploy-plane input**, threaded from the CLI through lowering to the target. - **Deploy state stays per (Project, Branch)** through Alchemy's existing diff --git a/docs/design/90-decisions/ADR-0041-local-dev-runs-the-deploy-pipeline-against-local-providers.md b/docs/design/90-decisions/ADR-0041-local-dev-runs-the-deploy-pipeline-against-local-providers.md index 551ef489..67f42c06 100644 --- a/docs/design/90-decisions/ADR-0041-local-dev-runs-the-deploy-pipeline-against-local-providers.md +++ b/docs/design/90-decisions/ADR-0041-local-dev-runs-the-deploy-pipeline-against-local-providers.md @@ -1,5 +1,7 @@ # ADR-0041: Local dev runs the deploy pipeline against local providers +Amended by [ADR-0050](ADR-0050-composer-runs-as-prisma-deploy-and-prisma-dev.md): the command is `prisma dev `, and reading the running app's logs is the `log` operation on `@prisma/composer/control`, not a command. The `prisma-composer` binary is retired. The local pipeline below is unchanged. + ## Decision `prisma-composer dev ` boots the whole application locally, with no cloud diff --git a/docs/design/90-decisions/ADR-0043-the-control-subpath-is-the-programmatic-deploy-surface.md b/docs/design/90-decisions/ADR-0043-the-control-subpath-is-the-programmatic-deploy-surface.md index c309faf4..1fffbfd4 100644 --- a/docs/design/90-decisions/ADR-0043-the-control-subpath-is-the-programmatic-deploy-surface.md +++ b/docs/design/90-decisions/ADR-0043-the-control-subpath-is-the-programmatic-deploy-surface.md @@ -1,8 +1,10 @@ # ADR-0043: `@prisma/composer/control` is the programmatic deploy surface +Amended by [ADR-0049](ADR-0049-composers-configuration-is-the-composer-section-of-prisma-config.md): each operation takes a required `config: { value, file }`, and the effect-conflict diagnosis is retired. Amended by [ADR-0050](ADR-0050-composer-runs-as-prisma-deploy-and-prisma-dev.md): the `prisma-composer` binary is retired; the `prisma` CLI renders two of the four operations, as `prisma deploy` and `prisma dev`, and `destroy` and `log` are called from scripts. The surface below is otherwise unchanged. + ## Decision -Composer's deploy pipeline is drivable in-process through one published subpath, **`@prisma/composer/control`**. It exposes four typed operations — `deploy`, `destroy`, `dev`, `log` — that take structured inputs and return structured results. They never parse argv and never call `process.exit`; the operations print nothing — the spawned alchemy child streams its own output to the terminal. The `prisma-composer` CLI is a thin renderer over these same operations, so the command-line surface and the programmatic surface cannot drift apart. +Composer's deploy pipeline is drivable in-process through one published subpath, **`@prisma/composer/control`**. It exposes four typed operations — `deploy`, `destroy`, `dev`, `log` — that take structured inputs and return structured results. They never parse argv and never call `process.exit`; the operations print nothing — the spawned alchemy child streams its own output to the terminal. The `prisma-composer` CLI is a thin renderer over these same operations, so the command-line surface and the programmatic surface cannot drift apart. *(Amended by [ADR-0050](ADR-0050-composer-runs-as-prisma-deploy-and-prisma-dev.md): the `prisma-composer` binary is retired. The `prisma` CLI renders two of the operations, as `prisma deploy` and `prisma dev`; `destroy` and `log` have no command and are called from scripts.)* A host — another CLI embedding Composer, a CI tool, a test — uses it like this: diff --git a/docs/design/90-decisions/ADR-0049-composers-configuration-is-the-composer-section-of-prisma-config.md b/docs/design/90-decisions/ADR-0049-composers-configuration-is-the-composer-section-of-prisma-config.md index 8f1d6735..3d80dfd2 100644 --- a/docs/design/90-decisions/ADR-0049-composers-configuration-is-the-composer-section-of-prisma-config.md +++ b/docs/design/90-decisions/ADR-0049-composers-configuration-is-the-composer-section-of-prisma-config.md @@ -1,5 +1,7 @@ # ADR-0049: Composer's configuration is the `composer` section of `prisma.config.ts` +Amended after acceptance: the Alternatives section adds four rejected ways to keep a Composer-owned effect check, and the Consequences state the exit code of `CLI.CONFIG_UNREADABLE`. The decision is unchanged. + ## Decision Composer reads its configuration from one place: the `composer` section of the app's `prisma.config.ts`, the file every Prisma CLI command family already shares. There is no separate Composer config file. @@ -45,7 +47,7 @@ Moving evaluation into the engine also retires Composer's effect pre-flight. The - `dev` reads the section once, at start, and watches the declaring file. After an edit it reports that the file changed and pauses rebuilds until it is restarted, so the in-process steps and the child never run against different versions of the file. - `deploy`, `destroy`, `dev` and `log` on `@prisma/composer/control` take a required `config: ComposerConfigSource` and never look for a config file. This is a breaking change to that surface. `log` reads only `value`; it takes the same input so a host builds it once. - `@prisma/composer-cli/family` no longer exports `ComposerSection`; the section's value type is `ComposerConfigSource`, exported from `@prisma/composer/control`. -- A broken `effect` tree surfaces as the engine's `CLI.CONFIG_UNREADABLE`, not as a Composer code. `DEPS.EFFECT_VERSION_CONFLICT` no longer exists. +- A broken `effect` tree fails fast: it surfaces as the engine's `CLI.CONFIG_UNREADABLE` with exit code 2, not as a Composer code. `DEPS.EFFECT_VERSION_CONFLICT` no longer exists. - On the CLI, every section-level failure (`CONFIG.SECTION_MISSING`, `CONFIG.FIELD_RETIRED`, `CONFIG.FILE_RETIRED` and field errors) appears as a diagnostic under the engine's `CLI.CONFIG_SECTION_INVALID` headline. In code, the same `CONFIG.` code is the failure itself. - `c12` is no longer a Composer dependency. - The dependency-cruiser configuration excludes `prisma.config.ts` by name, so the `/control` imports in the section are not cruised, and [ADR-0028](ADR-0028-numbered-domains-and-layers-enforced-by-dependency-cruiser.md)'s rule that examples, the website and tests import only the published packages no longer covers the file that imports `/control` entries. @@ -57,6 +59,10 @@ Moving evaluation into the engine also retires Composer's effect pre-flight. The - **The engine's default per-key merge**: rejected. A section merged from several files has no single file for the generated stack to import. - **Migrate the old file automatically**: rejected. Rewriting a user's `prisma.config.ts` is out of place for a deploy command; a refusal naming the exact change is deterministic and short. - **Keep the effect pre-flight**: rejected for the reasons above. It would be a second, Composer-owned check for a failure the engine already reports, fixable only in the user's package manager. +- **An engine hook that lets a command family veto config evaluation with its own diagnostic**, so the effect check could run before the engine evaluates `prisma.config.ts`: rejected. It would be an engine extension point with one consumer. +- **Composer's commands load the config themselves, after running the effect check**: rejected. It has the same cost in practice, and only Composer's commands would benefit; every other command that reads `prisma.config.ts` would still report the raw import error. +- **Run the effect check at import time inside `@prisma/composer/config`**: rejected. It depends on the import order in the user's file, and import sorters such as Biome's organize-imports put `@prisma/composer-prisma-cloud/control`, which loads Alchemy, before `@prisma/composer/config`. +- **Make the extensions' `/control` entries import Alchemy lazily**, so evaluating the config never loads it: rejected for now. Those entries import Alchemy and `effect` at module load in many files, so this is a refactor of the lowering layer. - **Programmatic operations that find the config themselves**: rejected. It would need a private loader in `@prisma/composer`, which must stay free of the engine, and so reintroduce the second loader. - **The handler recovers the declaring file by resolving the config chain again**: rejected. It repeats work the engine already did for the validator, and a command that forgot the call would silently skip the old-file check. The validator returns the file instead. diff --git a/docs/design/90-decisions/ADR-0050-composer-runs-as-prisma-deploy-and-prisma-dev.md b/docs/design/90-decisions/ADR-0050-composer-runs-as-prisma-deploy-and-prisma-dev.md new file mode 100644 index 00000000..e3a26dff --- /dev/null +++ b/docs/design/90-decisions/ADR-0050-composer-runs-as-prisma-deploy-and-prisma-dev.md @@ -0,0 +1,72 @@ +# ADR-0050: Composer runs as `prisma deploy` and `prisma dev`; it has no binary of its own + +## Decision + +Composer has no command-line binary of its own. The `prisma` CLI is its only command line, and it mounts exactly two Composer commands: + +```sh +prisma deploy module.ts --stage staging # deploy the app whose root module is module.ts +prisma dev module.ts # run the same app locally +``` + +Tearing an environment down and reading a local app's logs are not commands. They are the `destroy` and `log` operations on `@prisma/composer/control`, called from a short script: + +```ts +// destroy-staging.ts +import { fileURLToPath } from 'node:url'; +import { destroy } from '@prisma/composer/control'; +import prismaConfig from './prisma.config.ts'; + +const result = await destroy({ + entry: 'module.ts', + target: { kind: 'stage', stage: 'staging' }, // or { kind: 'production' } + config: { + value: prismaConfig.composer, + file: fileURLToPath(new URL('./prisma.config.ts', import.meta.url)), + }, +}); +if (!result.ok) { + console.error(result.failure.message); + process.exitCode = 1; +} +``` + +The script needs `PRISMA_SERVICE_TOKEN` and `PRISMA_WORKSPACE_ID` in the environment, and the app must be built first, as for a deploy. `entry` resolves against the directory the script runs in; `file` resolves against the script's own location. + +The standalone `prisma-composer` binary, with its `deploy`, `destroy`, `dev` and `log` subcommands, is retired. + +## Reasoning + +`@prisma/composer-cli` used to declare a `bin` and also export a command family: a set of command definitions that the `prisma` host (the `prisma` package, built on the Prisma CLI engine) mounts into its own command tree. Two command-line entry points ran the same operations. Users had to learn which one to run, and the docs had to describe both. Composer's configuration already moved into the shared `prisma.config.ts` ([ADR-0049](ADR-0049-composers-configuration-is-the-composer-section-of-prisma-config.md)), which the engine loads for every command family. A second binary that also loads that file adds nothing. So the package declares no `bin` and ships only the command family. + +The family contributes two commands. Both run the pipeline the earlier ADRs describe; only the name a user types changes. `prisma deploy ` takes `--stage`, `--name`, `--report` (write the deploy's outcome as JSON to a file) and `--build-id` (join a deploy record that CI already created). `prisma dev ` takes `--name` and `--fresh`. Both are thin renderers over the matching `@prisma/composer/control` operations ([ADR-0043](ADR-0043-the-control-subpath-is-the-programmatic-deploy-surface.md)). + +Most `prisma` commands follow a noun-then-verb grammar, `prisma `, such as `prisma auth login`. A few are mounted at the root as bare verbs, such as `prisma init`. `deploy` and `dev` are mounted at the root as bare verbs on purpose, and are not renamed to `prisma project deploy` and `prisma project dev`. They are the two commands a Composer user runs most, the short form is what users and agents will type, and a noun in front adds nothing a reader needs. + +`destroy` and `log` get no command. For `destroy`, two things do not fit the CLI. The CLI's destructive verb is `delete`, so a `destroy` command would add a second one. And `destroy` would need a production flag that no other Composer command has: `prisma deploy` targets production when `--stage` is left out, and no Composer command takes `--production`. If teardown gets a command, it should name production the way the CLI already does elsewhere (`prisma project env` takes `--role production`). Where teardown and logs belong in the `prisma` command tree is not decided here; until it is, both stay operations. + +A script that calls `destroy` cannot use the `prisma auth login` session; it reads `PRISMA_SERVICE_TOKEN` and `PRISMA_WORKSPACE_ID` from the environment, as the `deploy` operation does. The guides show one script for each operation. + +## Consequences + +- `@prisma/composer-cli` declares no `bin`. Its only consumer is the `prisma` host, which pins the Composer version it mounts. +- Earlier ADRs that show `prisma-composer deploy`, `destroy` or `dev` describe the same pipeline under the old command name. Their amendment notes point here. +- Teardown and logs need a script, and teardown needs service-token credentials. Before, each was one command. This is the main cost of the decision, and it falls on agents as much as on users: the Agent-first principle asks for tight feedback loops ([guiding principles](../01-principles/guiding-principles.md)), and an agent that wants to read `prisma dev`'s service logs or tear down a stage must now write and run a script, such as the ones in `docs/guides/running-locally.md` and `docs/guides/deploying.md`. It is accepted until teardown and logs get a place in the `prisma` command tree. +- Inside this repository, examples and CI run the published `prisma` host. A root pnpm override points `@prisma/composer-cli` at the workspace package, so they exercise the code in the checkout. Teardown in CI is `scripts/composer-destroy.ts`, which calls the `destroy` operation. +- The examples' `destroy` package scripts pass `--production` or `--stage ` to `scripts/composer-destroy.ts`. Those flags name the operation's two targets, `{ kind: 'production' }` and `{ kind: 'stage', stage }`, for this repository's own scripts. They are not a public command grammar, and no `prisma` command takes them. This is accepted because the script is internal and the flags are the shortest way to name a target in a package script. +- A CI lint, `scripts/lint-retired-binary-name.mjs`, fails on the old binary's name in the guides, skills, examples, website, workflows and the packages' shipped source. Files that name it on purpose are listed with an exact count of mentions. The design docs under `docs/design/` are not scanned, because ADRs keep their history. + +## Alternatives considered + +- **Keep the `prisma-composer` binary alongside the family**: rejected. Two command-line entry points over one pipeline means two sets of docs, two install stories and two places for help text to drift. +- **Name the commands `prisma project deploy` and `prisma project dev`**, to follow the noun-then-verb grammar: rejected. The two most common commands would get longer for no gain in clarity. +- **Mount `destroy` in the `prisma` host**: rejected. It would add a second destructive verb next to `delete`, and a production flag that no other Composer command takes. +- **Mount `log` in the `prisma` host now**: rejected. Where logs belong in the command tree is not decided, and the CLI already has `prisma service logs` for deployed services. A Composer logs command shipped now would probably need renaming once that is decided. + +## Related + +- [ADR-0043](ADR-0043-the-control-subpath-is-the-programmatic-deploy-surface.md): the operations both commands render, and that the destroy and log scripts call. +- [ADR-0049](ADR-0049-composers-configuration-is-the-composer-section-of-prisma-config.md): the shared `prisma.config.ts` that made the separate binary unnecessary. +- [ADR-0003](ADR-0003-deploy-derives-everything-from-the-root-node.md), [ADR-0006](ADR-0006-every-node-is-named.md), [ADR-0007](ADR-0007-deploy-drives-alchemy-through-a-generated-stack-file.md), [ADR-0024](ADR-0024-a-stage-is-a-deploy-time-environment-resolved-to-project-and-branch.md), [ADR-0041](ADR-0041-local-dev-runs-the-deploy-pipeline-against-local-providers.md): the decisions whose command examples this renames. +- [`../10-domains/deploy-cli.md`](../10-domains/deploy-cli.md) and [`../10-domains/local-dev.md`](../10-domains/local-dev.md): the two commands' mechanics. +- [Guiding principles](../01-principles/guiding-principles.md): Agent-first, whose cost is weighed above. diff --git a/docs/design/90-decisions/README.md b/docs/design/90-decisions/README.md index 9795c50d..60998449 100644 --- a/docs/design/90-decisions/README.md +++ b/docs/design/90-decisions/README.md @@ -24,11 +24,11 @@ the superseded entry. A retired ADR stays in place; the Index says so. _Earlier drafts (ADR-0001, ADR-0002) were retired as the high-level design settled._ -- [ADR-0003](ADR-0003-deploy-derives-everything-from-the-root-node.md) — `prisma-composer deploy` derives everything from the root node; there is no deploy config file. +- [ADR-0003](ADR-0003-deploy-derives-everything-from-the-root-node.md) — `prisma-composer deploy` derives everything from the root node; there is no deploy config file. *(Amended by ADR-0049: the extensions and state store live in the `composer` section of `prisma.config.ts`. Amended by ADR-0050: the command is `prisma deploy `.)* - [ADR-0004](ADR-0004-paths-resolve-relative-to-the-authoring-file.md) — Paths resolve relative to the file that writes them; the build adapter carries the authoring module. - [ADR-0005](ADR-0005-users-build-the-framework-assembles.md) — Users build the app's code; the framework assembles the artifact by documented, deterministic steps (validate, wrap, each app-type's documented deploy step — e.g. Next's static/public copy). No guessing (arithmetic/depth-inference/discovery), no laundering (symlink = hard error); read the build tool's own manifest (Next's `relativeAppDir`), don't walk or compute. *(Superseded in part by ADR-0047: a symlink whose resolved target stays inside the bundle is preserved as a link; the no-guessing and no-dereferencing rules stand.)* -- [ADR-0006](ADR-0006-every-node-is-named.md) — Every node is named; the root's name names the application. -- [ADR-0007](ADR-0007-deploy-drives-alchemy-through-a-generated-stack-file.md) — Deploy drives Alchemy through a generated, inspectable stack file. +- [ADR-0006](ADR-0006-every-node-is-named.md) — Every node is named; the root's name names the application. *(Amended by ADR-0050: the override is `prisma deploy --name`.)* +- [ADR-0007](ADR-0007-deploy-drives-alchemy-through-a-generated-stack-file.md) — Deploy drives Alchemy through a generated, inspectable stack file. *(Amended by ADR-0050: `prisma deploy` and `prisma dev` generate the file; teardown is the `destroy` operation.)* - [ADR-0008](ADR-0008-wrapper-inlines-everything-except-runtime-builtins.md) — The boot wrapper inlines everything except runtime built-ins. - [ADR-0009](ADR-0009-deploy-state-is-hosted-in-the-workspace.md) — Deploy state is hosted in the workspace, not in local files. *(Superseded by ADR-0034: still hosted, now per-stage in the app's own Project.)* - [ADR-0010](ADR-0010-deploys-hold-a-session-advisory-lock.md) — Deploys hold a session advisory lock per stack and stage. *(Superseded by ADR-0045: the lease is server-side now; the fail-fast contention behavior survives.)* @@ -45,7 +45,7 @@ _Earlier drafts (ADR-0001, ADR-0002) were retired as the high-level design settl - [ADR-0021](ADR-0021-params-are-read-through-config-not-load.md) — A service reads dependencies through `load()` and config params through a sibling `config()`; the two never share a namespace. *(Superseded by ADR-0042: `config()` and the `params` declaration are replaced by one `input` schema read through `service.input()`; `load()` stays separate.)* - [ADR-0022](ADR-0022-data-deps-carry-a-prisma-orm-contract.md) — Data deps carry a Prisma ORM contract: typed client binding, one contract per database via its config, deploys migrate along authored edges to the contract hash. Revised 2026-08-25: deploys are replay-only — the first-deploy `dbInit` synthesis path is removed; a fresh database replays the committed baseline from empty, and a missing authored path is a refusal naming the fix (`prisma db update` locally, `contract emit` + `migration plan` to ship). *(Proposed)* *(Amended 2026-10-02: a service that uses a database deploys only after that database's migration has completed.)* - [ADR-0023](ADR-0023-a-prisma-app-is-one-project-a-stage-is-a-branch.md) — A Prisma App is one Prisma Cloud Project; Modules are Apps/Databases inside it; a Stage is a Branch, and deploy state is per `(Project, Branch)`. -- [ADR-0024](ADR-0024-a-stage-is-a-deploy-time-environment-resolved-to-project-and-branch.md) — A stage is a deploy-time environment; the CLI resolves the app's Project (by root-module name) and the stage's Branch outside Alchemy before the stack runs. +- [ADR-0024](ADR-0024-a-stage-is-a-deploy-time-environment-resolved-to-project-and-branch.md) — A stage is a deploy-time environment; the CLI resolves the app's Project (by root-module name) and the stage's Branch outside Alchemy before the stack runs. *(Amended by ADR-0050: deploys run as `prisma deploy`; teardown is the `destroy` operation, whose target is required.)* - [ADR-0025](ADR-0025-name-the-unit-of-composition-module.md) — The unit of composition is a **Module**, authored with `module()`; supersedes ADR-0014's unit noun ("System"). Registers: package (npm's word) / extension (config slot) / Module (composition). - [ADR-0026](ADR-0026-name-the-framework-prisma-compose.md) — The framework is **Prisma Composer**; "Prisma App" names only the artifact you build. Supersedes ADR-0014's framework, package, and CLI names: `@prisma/composer*` (incl. `compose-alchemy`), `prisma-composer`, `prisma-composer.config.ts`. - [ADR-0027](ADR-0027-two-packages-compose-and-compose-prisma-cloud.md) — Ship two **public** packages: `@prisma/composer` (core + CLI + agnostic subpaths) and `@prisma/composer-prisma-cloud` (target + first-party modules as entrypoints, cron first). Boundary = the user's one choice: where does it run. @@ -62,12 +62,13 @@ _Earlier drafts (ADR-0001, ADR-0002) were retired as the high-level design settl - [ADR-0038](ADR-0038-containers-are-an-extension-descriptor.md) — Container lifecycle (ensure/locate/remove) is an optional `container` descriptor on `ExtensionDescriptor`, the same pattern as `preflight`/`teardown`; the resolved instance is opaque to core and crosses the CLI parent→alchemy child boundary as one framework-named environment variable per extension, via the extension's own `serialize()`/`deserialize()`. `StateDescriptor` names its owning extension so core can hand it that extension's resolved container. Deletes the `crossDomainExceptions` entry that let the CLI import `@internal/lowering` directly — `0-framework` imports nothing again. - [ADR-0039](ADR-0039-a-compute-services-own-origin-is-a-target-resolved-property.md) — A compute service's own platform-assigned origin is a target-resolved property, read as the service's `origin()` — never a declared param, never operator config, never in `config()`. It rides ADR-0031's reserved provider-param channel as the first *service-derived* entry (`valueForService(provisioned, address)`, written for every compute service, exposing or not), sourced from the provisioned App's `appEndpointDomain` (ADR-0048) — made trustworthy pre-promote by the upstream PRO-200 fix. `envParam(…)` remains correct for operator-known origins (custom domains); narrows ADR-0032's `appOrigin` example accordingly. - [ADR-0040](ADR-0040-the-orm-binding-carries-the-url-and-a-lazy-client.md) — `postgres(contract)`'s dependency binding is `{ url, client }`: the raw connection string plus the typed client, constructed lazily and memoized on first `client` access — `hydrate` builds nothing. The contract remains the compatibility interface (hash check and deploy-time migration unchanged, ADR-0022); the binding becomes a strict superset of plain `rawPostgres()`'s `{ url }`, so an app that owns its database client still gets framework-run migrations. Contract validation cost and failure move from `load()` (where one bad input poisoned every input, unattributed) to the first `client` access. Cross-kind satisfaction (`'postgres'` satisfying `'postgres'`) rejected in its favor. -- [ADR-0041](ADR-0041-local-dev-runs-the-deploy-pipeline-against-local-providers.md) — `prisma-composer dev` runs the **same deploy pipeline** (Load → assemble → lower → Alchemy converge) against local implementations of the same Alchemy resource types, declared on an optional `localTarget` field of `ExtensionDescriptor` (a lazy thunk resolving a `LocalTargetDescriptor`; subpaths `@prisma/composer/local-target` and `@prisma/composer-prisma-cloud/local-target` — "dev" names only the user-facing command/prefix/state dir) (providers, container, preflight, emulators, attach, teardown — **no** `nodes`/`provisions`, so the lowering cannot diverge; no `state` either — dev uses Alchemy's own `localState()` through `LowerOptions.state`). The target runs **emulators per node kind**: Compute and buckets are machine-global, multi-tenant daemons (the Compute emulator owns the service child processes — deployment PUTs, crash supervision, logs; buckets serve the S3 wire over plain files on disk), while Postgres runs one detached ORM `prisma dev` instance per `Database` resource under the ORM CLI's own manager. Providers provision instances by communicating with the emulators during converge, and the dev command is a view through `attach`; `ServiceKey`/`S3Credentials`/`PgWarm`/`OrmMigration` are shared verbatim. Credential-free by requirement. Rejects a local Management API (reimplements another team's server-side semantics, drifts silently) and per-kind dev descriptors (an open-set parallel seam). +- [ADR-0041](ADR-0041-local-dev-runs-the-deploy-pipeline-against-local-providers.md) — `prisma-composer dev` runs the **same deploy pipeline** (Load → assemble → lower → Alchemy converge) against local implementations of the same Alchemy resource types, declared on an optional `localTarget` field of `ExtensionDescriptor` (a lazy thunk resolving a `LocalTargetDescriptor`; subpaths `@prisma/composer/local-target` and `@prisma/composer-prisma-cloud/local-target` — "dev" names only the user-facing command/prefix/state dir) (providers, container, preflight, emulators, attach, teardown — **no** `nodes`/`provisions`, so the lowering cannot diverge; no `state` either — dev uses Alchemy's own `localState()` through `LowerOptions.state`). The target runs **emulators per node kind**: Compute and buckets are machine-global, multi-tenant daemons (the Compute emulator owns the service child processes — deployment PUTs, crash supervision, logs; buckets serve the S3 wire over plain files on disk), while Postgres runs one detached ORM `prisma dev` instance per `Database` resource under the ORM CLI's own manager. Providers provision instances by communicating with the emulators during converge, and the dev command is a view through `attach`; `ServiceKey`/`S3Credentials`/`PgWarm`/`OrmMigration` are shared verbatim. Credential-free by requirement. Rejects a local Management API (reimplements another team's server-side semantics, drifts silently) and per-kind dev descriptors (an open-set parallel seam). *(Amended by ADR-0050: the command is `prisma dev `; logs are the `log` operation, not a command.)* - [ADR-0042](ADR-0042-service-input-is-one-standard-schema.md) — A compute service declares its entire incoming configuration — config and secrets together — as one Standard Schema (`input`), read back through one typed accessor; `params`/`secrets` and `config()`/`secrets()` are replaced. The framework never introspects the schema (validate-only, per the spec): the operator's binding is the traversable structure (sourcing: literals, `envParam`, `envSecret`), the schema is the black-box judge of legality (invoked at deploy over the resolved binding with secrets as opaque `SecretString` boxes, and again at boot), and secretness is a leaf *type* enforced by validation in both directions. The wire format is one self-describing JSON document row per service with `$secret` pointers to platform variables; an env-bound key whose variable is unset resolves to key-omitted and the schema arbitrates absence — subsuming optional secrets and conditional config (`stripeId` only when `stripeEnabled`) without a framework DSL. -- [ADR-0043](ADR-0043-the-control-subpath-is-the-programmatic-deploy-surface.md) — `@prisma/composer/control` is the programmatic deploy surface: typed `deploy`/`destroy`/`dev`/`log` operations (structured inputs/results, no argv/console/exit) implemented in `@internal/cli`'s `src/operations/` and re-exported per ADR-0035; the CLI is a thin renderer over them. The entry's static graph stays import-light — each operation lazily imports its executor, so importing the subpath executes nothing, and a tree that cannot load the deploy stack surfaces as a structured `pipeline` failure — and `PRISMA_COMPOSER_DEPLOYMENT_RESULT_FILE` carries the deploy result across the process boundary: the alchemy child's report hook writes a serializable `DeploymentSummary` to the named file, the operation reads it back best-effort (absent/malformed = undefined summary, never a failure). Distinct from an extension's ADR-0017 `/control` entry. *(Amended by ADR-0049: each operation takes a required `config: { value, file }`, and the effect-conflict diagnosis is retired.)* +- [ADR-0043](ADR-0043-the-control-subpath-is-the-programmatic-deploy-surface.md) — `@prisma/composer/control` is the programmatic deploy surface: typed `deploy`/`destroy`/`dev`/`log` operations (structured inputs/results, no argv/console/exit) implemented in `@internal/cli`'s `src/operations/` and re-exported per ADR-0035; the CLI is a thin renderer over them. The entry's static graph stays import-light — each operation lazily imports its executor, so importing the subpath executes nothing, and a tree that cannot load the deploy stack surfaces as a structured `pipeline` failure — and `PRISMA_COMPOSER_DEPLOYMENT_RESULT_FILE` carries the deploy result across the process boundary: the alchemy child's report hook writes a serializable `DeploymentSummary` to the named file, the operation reads it back best-effort (absent/malformed = undefined summary, never a failure). Distinct from an extension's ADR-0017 `/control` entry. *(Amended by ADR-0049: each operation takes a required `config: { value, file }`, and the effect-conflict diagnosis is retired.)* *(Amended by ADR-0050: the `prisma` CLI renders two of the four operations, as `prisma deploy` and `prisma dev`; `destroy` and `log` are called from scripts.)* - [ADR-0044](ADR-0044-errors-are-structural-envelopes-with-dotted-namespace-codes.md) — Errors are structural envelopes with dotted `NAMESPACE.SUBCODE` codes (the shared prisma/prisma foundation, duplicated pending extraction): structured at origin with why/fix splits, no catch-all codes, bugs carry no code (exit 1 + report hint), recognition is structural (`CliStructuredError.is()`), operation results ride the shared `Result` `ok` discriminator, expected failures exit 2 — with the alchemy child-status passthrough as the documented exception. *(Amended by ADR-0049: `DEPS.EFFECT_VERSION_CONFLICT` is retired.)* - [ADR-0045](ADR-0045-deploy-state-lives-behind-the-platform-state-api.md) — Deploy state lives behind the platform state API (the Management API implements Alchemy's stock `HttpStateApi` wire contract per Branch; composer's state layer is Alchemy's stock HTTP client), and deploys hold a server-side per-`(stack, stage)` lease (TTL 60s, heartbeated, released on exit; contention fails fast naming the holder; state operations without a live lease fail 409). Supersedes ADR-0010 (lock → lease) and the storage half of ADR-0034 (Branch scoping and lifetime stand; the visible per-stage database is gone); closes ADR-0012 as obsolete. No migration: legacy stages are refused until destroyed or deleted. - [ADR-0046](ADR-0046-the-orm-facade-is-a-peer-dependency.md) — `@prisma/composer-prisma-cloud` takes the Prisma ORM postgres facade (`@prisma/orm-postgres`) as a **peer** dependency at one exact version, not a regular dependency: Composer registers an extension pack against the application's copy of the target, and two copies of a shell in one tree means two codec/operation registries and two class identities — a value from one is rejected by the other, silently. As a peer, that combination fails at install instead. Every `@prisma/orm-*` spec in the workspace is one exact version and all name the same one (`scripts/lint-orm-pins.mjs`). `@prisma/orm-toolchain`, which Composer drives rather than extends, stays a regular dependency. Replaces ADR-0022's consequence bullet on how the ORM is installed. - [ADR-0047](ADR-0047-compute-assembly-preserves-safe-runtime-topology.md) — Compute assembly traces runtime files from the author-declared Node entry without rebundling app code, preserves only symlinks whose resolved targets remain inside the staged bundle, and installs narrowly runtime-gated bootstrap compatibility when a framework needs Node semantics. Each local mechanism is removed once the upstream Alchemy Compute provider owns the equivalent guarantee. Supersedes ADR-0005's blanket ban on symlinks and its assumption that directory output is already self-contained. - [ADR-0048](ADR-0048-prisma-cloud-resources-come-from-the-upstream-alchemy-provider.md) — The six Management-API resource families (project, database, connection, app, deployment, environment variable) are the upstream `alchemy/Prisma` provider's classes, registered in Composer's `PrismaComposer` collection; Composer defines resources only where the upstream provider has no support yet (buckets) or no Management API exists behind them. Compute binds the low-level App/Deployment/EnvironmentVariable trio (the `COMPOSER_*_ORIGIN` self-edge and ADR-0005 rule out composite `Compute`); the env→deployment ordering edge rides the deployment's `app` prop, and a deployment is replaced exactly when its artifact or environment fingerprint changes — unchanged services are reused. Legacy rows in the platform state API (retired type-ids and shapes) migrate on read — the older retired SQL state stores stay destroy-only under ADR-0045 — branch-stage databases take generated physical names, and the platform's seeded `DATABASE_URL` is left system-managed. -- [ADR-0049](ADR-0049-composers-configuration-is-the-composer-section-of-prisma-config.md) — Composer's configuration is the `composer` section of `prisma.config.ts`, loaded by the Prisma CLI engine. The validator checks only the fields that identify extension and state descriptors, passes descriptors through as the file's own objects, rejects unknown keys, and takes the nearest declaring file's section whole so the generated stack imports exactly one file. The old `prisma-composer.config.ts` and the `configPath` pointer are refused with `CONFIG.SECTION_MISSING`, `CONFIG.FIELD_RETIRED` and `CONFIG.FILE_RETIRED`, never migrated. The validator returns the section with its declaring file, and the programmatic operations take the same `ComposerConfigSource` (`{ value, file }`) and run the same checks. The Alchemy child evaluates the whole `prisma.config.ts`. The effect pre-flight and `DEPS.EFFECT_VERSION_CONFLICT` are retired: a broken tree fails evaluation of `prisma.config.ts` with the engine's `CLI.CONFIG_UNREADABLE`. +- [ADR-0049](ADR-0049-composers-configuration-is-the-composer-section-of-prisma-config.md) — Composer's configuration is the `composer` section of `prisma.config.ts`, loaded by the Prisma CLI engine. The validator checks only the fields that identify extension and state descriptors, passes descriptors through as the file's own objects, rejects unknown keys, and takes the nearest declaring file's section whole so the generated stack imports exactly one file. The old `prisma-composer.config.ts` and the `configPath` pointer are refused with `CONFIG.SECTION_MISSING`, `CONFIG.FIELD_RETIRED` and `CONFIG.FILE_RETIRED`, never migrated. The validator returns the section with its declaring file, and the programmatic operations take the same `ComposerConfigSource` (`{ value, file }`) and run the same checks. The Alchemy child evaluates the whole `prisma.config.ts`. The effect pre-flight and `DEPS.EFFECT_VERSION_CONFLICT` are retired: a broken tree fails evaluation of `prisma.config.ts` with the engine's `CLI.CONFIG_UNREADABLE`. *(Amended: the alternatives section lists the rejected ways to keep a Composer-owned effect check.)* +- [ADR-0050](ADR-0050-composer-runs-as-prisma-deploy-and-prisma-dev.md) — Composer has no binary of its own: the `prisma` CLI mounts exactly two Composer commands, `prisma deploy ` and `prisma dev `, mounted at the root as bare verbs on purpose rather than under a noun. `destroy` and `log` are operations on `@prisma/composer/control`, called from a script; a teardown script reads `PRISMA_SERVICE_TOKEN` and `PRISMA_WORKSPACE_ID`. Where teardown and logs belong in the `prisma` command tree is not decided; until it is, agents and users need a script for both. A `destroy` command was rejected: the CLI's destructive verb is `delete`, and no Composer command takes `--production`. Retires the `prisma-composer` binary; a CI lint keeps its name out of user-facing files.