Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .drive/projects/emulator-commands/deferred.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@

- **A start can join a crash-backoff restart that has already exited (prisma/composer#353).** When the fifth fast exit marks an instance `held`, the readiness probe only notices the exit after its current connection attempt. A start request arriving in that gap joins the dying start and gets its old failure, so nothing resumes. Seen once as an intermittent failure of "a held service resumes on start" in `compute-deployment.test.ts`; not reproduced in 10 runs under CPU load. Likely fix: a start must not join an in-flight start whose process has already exited. Belongs in #353, not this project.
- **What a previous app's databases and buckets become when another app runs from the same directory.** Compute instances are deleted (S1a). Databases and buckets hold data; decide in S1b.
- **`[dev]` notices bypass the CLI engine's output.** The emulators hook writes `[dev] compute emulator ready`, the port-change line and the app-switch line with `console.log` to stdout, so a piped `prisma dev` mixes them into the JSON frames. Fixing it needs a channel for the local target's hooks to report through the engine.
- **`[dev]` notices bypass the CLI engine's output.** The emulators hook writes `[dev] compute emulator ready`, the port-change line and the app-switch line with `console.log` to stdout, so once a pipe gets plain data (prisma-cli's piped-output change) they land among a piped `prisma dev`'s data lines; with `--json` they still mix into the JSON frames. Fixing it needs a channel for the local target's hooks to report through the engine.
- **A deployment that arrives while its instance is being deleted can start a process the emulator no longer tracks.** Same race as #353's app delete.
- **`env.json` and `secrets.json` in `.prisma-composer/dev/` are shared by every app run from a directory, and `--fresh` wipes them.** One app's `--fresh` can drop rows another app in the same directory needs on its next redeploy. Pre-existing. Making them per app, like bucket objects (S1b moved those to `buckets/<app>/<bucket>/`), would fix it.
- **A service that never listens fails `prisma dev` under prisma/composer#353** (the cron scheduler; `examples/store`). Raised with Will; the fix belongs in #353.
- **`prisma dev url` doesn't retry an emulator that is briefly unreachable.** Retry policy is #353's (`withEmulatorRetry`); apply it here once that settles.
- **`prisma dev url` loads the config's extensions, and so Alchemy, to read one JSON file.** It only matters when a script calls it in a loop.
- **A run-then-stop mode for CI: `prisma dev --exec "<cmd>"`, like Firebase `emulators:exec`.** It would bring the app up as `--detach` does, run the command, stop the app and exit with the command's code. Likely next step after S6.
- **Commands that print nothing to a pipe once prisma-cli's piped output becomes plain data.** Each needs data lines in its owning repo:
- Composer `prisma deploy`: the deployed addresses (and URLs where public), one per line.
- ORM (`@prisma/orm-toolchain`): `migration list` and `migration ref list` (one row per item), `migration status` (`key<TAB>value`), `migration log`, `migration show`, `migration plan` and `migration new` (the created migration's id or path), `migration check`, `migration graph`, `migration ref set` and `migration ref delete`, `contract infer`, `contract format`, `contract print` (the printed contract is the data), `db init`, `db schema`, `db sign`, `db update`, `db verify`, `db migrate`, `orm init`. Matched by function name in the built bundle, so approximate.
5 changes: 5 additions & 0 deletions .drive/projects/emulator-commands/design-notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,8 @@ Composer remembers each node's port in the directory's mapping and requests it o
- **Make the local identity "app name + directory hash" and keep app-aware emulators.** Fixes collisions but keeps Composer concepts inside the emulators, which is the opposite of the model.
- **`emulator list` through the core seam.** Requires a project and config for a machine-level command.
- **Print a bare URL even when piped.** An engine output change Will deferred.

## Rulings (Will, 2026-10-15)

- `prisma dev` stays in the foreground, watching, and Ctrl-C stops the app. `prisma dev --detach` waits until every service answers, prints the URLs, exits 0 and leaves the app running for `prisma dev stop`. Prior art (Docker Compose, .NET Aspire, Sail, and Prisma ORM's own `prisma dev`) defaults to foreground with an opt-in detach. Slice S6.
- A piped stdout gets the command's plain data lines, and JSON requires `--json`. No CLI surveyed (gh, aws, kubectl, gcloud, az, docker, supabase) switches to JSON when piped. This supersedes "Piped output stays JSON; docs use `jq`" above. Slice S7.
46 changes: 46 additions & 0 deletions .drive/projects/emulator-commands/slices/s6-dev-detach/spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# S6 — `prisma dev --detach`

Repo: prisma/composer. Branch `feat/dev-detach`, stacked on `feat/emulator-commands` (prisma/composer#357).

## At a glance

```bash
$ prisma dev module.ts --detach # returns once every service answers
dev: ready
gateway: http://localhost:3001
quotes: http://localhost:3000
$ curl "$(prisma dev url gateway)" # same terminal
$ prisma dev stop # the only thing that stops a detached app
```

Will ruled on 2026-10-15 that `prisma dev` stays in the foreground, watching, and Ctrl-C stops the app, as today. `--detach` is opt-in.

This follows prior art: Docker Compose `up -d --wait`, .NET Aspire `run --detach`, and Prisma ORM's own `prisma dev --detach`. Agents and scripts use `--detach` so they don't have to manage a long-running foreground process.

## Chosen design

- `--detach` runs the same pipeline as `prisma dev` up to "ready": converge, wait for every service to accept connections (prisma/composer#353), and print the front door. Then it exits 0.
- A detached run starts no watch loop and installs no stop-on-exit.
- The app keeps running on the emulators until `prisma dev stop`, or until the next `prisma dev` in the directory, which reconverges as today.
- A failure before ready stops what this run started, exactly as the foreground path does, and exits non-zero with the same error.
- Ctrl-C before ready stops the app and exits, as today.
- `--detach` combines with `--fresh` and `--name`.
- The `dev` operation in `@prisma/composer/control` gains an equivalent option. The command renders it, per the rule that the CLI's commands are thin renderers over the operations. The session handle's `stop()`, `closed` and events behave sensibly for a detached session: it is already closed and owns nothing.
- Running `prisma dev` in the foreground while the app is already running detached just takes over. It converges, which is a no-op when nothing changed, watches, and Ctrl-C stops the app. No special case.

## Docs

- Update the ADR that records `dev url`, `dev stop` and `emulator list` in place, since it is unreleased: `--detach` and why foreground stays the default, with the prior-art comparison as the alternative.
- `local-dev.md`: the session lifetimes section.
- The running-locally guide and the core-concepts skill: a "run it in the background" subsection. Agents and CI use `--detach` then `prisma dev stop`.
- The `dev` command's help: a `--detach` flag brief that meets prisma-cli's help standard.

## Slice-specific done conditions

- [ ] Against real emulators in a temporary directory, `prisma dev --detach` exits 0 once every service answers, and the services keep answering after it exits.
- [ ] `prisma dev stop` then stops them.
- [ ] A detached run that fails before ready leaves nothing of the app running.

## Out of scope

A run-then-stop mode (`--exec "<cmd>"`, like Firebase `emulators:exec`). Record it in `deferred.md` as a likely next step for CI.
43 changes: 43 additions & 0 deletions .drive/projects/emulator-commands/slices/s7-piped-output/spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# S7 — A piped stdout gets the command's data, not JSON

Repo: prisma/prisma-cli. Branch `feat/piped-output-is-data` off `main`, which now includes prisma/prisma-cli#349.

## At a glance

```bash
$ prisma project list | head -3 # today: one NDJSON result frame
proj_cm3x… shop
proj_k91… blog
$ curl "$(prisma dev url gateway)" # today: curl gets a JSON frame
$ prisma project list --json | jq '…' # JSON is always explicit
```

Will ruled on 2026-10-15 that when stdout is not a terminal and no format flag is given, the CLI prints the command's plain data lines, and JSON requires `--json` (or `--format json`).

This replaces the engine's rule from the PR that created it ("a terminal gets human output and a pipe gets JSON"). None of the CLIs surveyed switch to JSON when piped (gh, aws, kubectl, gcloud, az, docker, supabase). Single-value commands print the value bare: `docker port`, `encore db conn-uri`.

## Chosen design

**Engine (`packages/cli-engine`).**
- With no `--json`/`--format` flag, the format is `human` whether or not stdout is a TTY.
- In human format, stdout carries only the command's data lines (`presentation.stdout`), as the product docs already require. Human blocks go to stderr, undecorated when stderr isn't a TTY, as today.
- The existing rule that skips the stdout data lines when stdout and stderr are the same terminal stays.
- The pre-parse format sniff (`sniffFormat`) and `resolveAutoFormat` change accordingly. Errors in a pipe render as human errors on stderr with the exit code, unless `--json` is given.
- `--format markdown` is unchanged.
- This is a breaking change to the engine's output contract, so the engine gets a minor version bump with `pnpm bump-cli-engine-version`, and conformance exceptions as the repo's process requires.

**Every mounted command defines its data lines.** About 34 of 58 presentations define no stdout today, so in a pipe they would print nothing. For each mounted command, decide what a pipe user expects and define `stdout`, following `docs/product/output-conventions.md`:
- A list: one line per item, the subject a follow-up command takes (normally its id), plus the name, tab-separated.
- A show: the resource's identifying value, or `key\tvalue` lines when there is no single value.
- A command whose result is one value: the bare value. Examples: a URL, a connection string, a created id.
- A command with no data (logout, delete): nothing on stdout. Its success is the exit code.

Write the rules into the product docs first, as ARCHITECTURE.md requires, then implement. Composer and ORM family commands are owned by those repos. List any whose stdout needs work as follow-ups, with the specific command.

**Docs.** Update every statement that a pipe gets JSON: `output-conventions.md` (TTY and piped behaviour, the `--format markdown` paragraph), `cli-style-guide.md`, `error-conventions.md`, the engine README and AGENTS.md, and the agent skills in `skills/` that tell agents to rely on piped JSON. Agents use `--json`. The e2e harness and tests that rely on piped JSON pass `--json` explicitly.

## Slice-specific done conditions

- [ ] Engine tests: no flag and piped stdout gives the data lines and no JSON; `--json` piped gives one NDJSON result frame as today; an error piped without `--json` gives a human error on stderr and the right exit code.
- [ ] A test that fails when a mounted command defines no stdout data lines and isn't on an explicit, reasoned list of commands with no data.
- [ ] The e2e suite still passes (`pnpm --filter @prisma/cli test:e2e`, if credentials are available; otherwise say so).
27 changes: 23 additions & 4 deletions docs/design/10-domains/local-dev.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ command:
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)).
With `--detach` (the `dev` operation's `detach`), it returns once every
service answers and leaves the app running without watching; see
§ Process lifetimes.
- **`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
Expand Down Expand Up @@ -59,9 +62,10 @@ Cloud target implements it from `emulator-instances.json`; core never reads the
record. `dev url` reports a node only while it is running, so every URL it
prints answers.

Flags: `dev` takes `--name` (override the root's name, as on deploy) and
Flags: `dev` takes `--name` (override the root's name, as on deploy),
`--fresh` (destroy the dev stack and wipe the dev state directory before
starting). Stages do not apply — a working directory has
starting) and `--detach` (return once every service answers and leave the app
running; see § Process lifetimes). Stages do not apply — a working directory has
exactly one dev instance; parallel instances are parallel checkouts.

**Naming.** The `prisma` CLI mounts this as `prisma dev <entry>`
Expand Down Expand Up @@ -174,8 +178,8 @@ service's instance, and the emulator owns the processes:
instances and its database servers, and detaches. Every emulator —
Compute, Postgres, buckets — stays up, and every instance keeps its data
and its port; the next start reuses the same instances (same ports, same
data). `--fresh` is what removes instances and data. (A detached mode where services keep serving with no
session is a designed extension, not v1.)
data). `--fresh` is what removes instances and data. `prisma dev --detach` leaves the app running with no
session; see § Process lifetimes.

**Known gap (proving-pass finding, fixed in #164):** a service actually only restarts
when its `Deployment` resource is re-put — and Alchemy skips calling the
Expand Down Expand Up @@ -356,6 +360,21 @@ dev state is never migrated.
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 `log` operation.
4. **The app's processes** — the service children and database servers the
emulators run for the app. In the foreground, `prisma dev` stops them when
it ends (Ctrl-C, SIGTERM or a failure). `prisma dev --detach` converges,
waits until every service accepts connections, prints the front door and
exits 0, starting no watch loop and no stop on exit, so they keep running
until `prisma dev stop` or the next `prisma dev` in the directory, which
reconverges as usual. A detached session handle is already closed and its
`stop()` stops nothing. A detached run that fails before ready stops the
whole app, including one an earlier `--detach` left running, as the
foreground run does: a failed redeploy has already replaced that service's
old process, so the previous app cannot be kept; fix the build and run
`prisma dev --detach` again. A Ctrl-C before ready stops the app through
`devStop`. A foreground `prisma dev` started while the app runs
detached takes it over: its converge changes nothing, it watches, and its
Ctrl-C stops the app.

The emulator daemon bookkeeping (registry, stable ports, readiness,
version-skew restart) is framework-owned and minimal, and the Postgres
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ every attachment after each converge, before printing the front door — so
a warm start restarts stopped services independent of Alchemy's props
diff. Details in [local-dev.md](../10-domains/local-dev.md). A detached
mode (services keep serving with no session, full platform parity) is a
designed extension, not v1.
designed extension, not v1. *(Amended by ADR-0053: `prisma dev --detach`.)*

**Rebuilds stay the user's** (ADR-0005). Dev watches the *built* output that
assembly consumes, never sources; the user's own watcher (`next dev` is not
Expand Down
Loading
Loading