Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
7bc6d7e
docs(drive): emulator commands project spec, plan and first slice specs
wmadden-electric Oct 13, 2026
77ff3e9
docs(drive): an emulator instance keeps its port until deleted
wmadden-electric Oct 13, 2026
540e748
feat(dev-emulators): key compute instances by ids the emulator generates
wmadden-electric Oct 13, 2026
c34554b
refactor(local-target): find compute instances by their labels until …
wmadden-electric Oct 13, 2026
b20468a
feat(local-target): record each directory's compute instances in emul…
wmadden-electric Oct 13, 2026
2c1aaa0
feat(local-target): switching apps in a directory removes the previou…
wmadden-electric Oct 13, 2026
4e405a7
docs(design): record that Composer maps its nodes to emulator instanc…
wmadden-electric Oct 13, 2026
8099746
docs(guides): each directory runs its own local services
wmadden-electric Oct 13, 2026
a5d3eab
docs(drive): record deferred items for emulator commands
wmadden-electric Oct 13, 2026
7139aa0
fix(dev-emulators): never retry instance creation, and let a stop ski…
wmadden-electric Oct 13, 2026
b927591
fix(local-target): --fresh deletes every recorded instance, whichever…
wmadden-electric Oct 13, 2026
4ce59e6
docs(design): state what the emulator actually receives from Composer
wmadden-electric Oct 13, 2026
dea7fbb
docs(drive): state what the emulator receives from Composer; record d…
wmadden-electric Oct 13, 2026
a99555f
test(local-target): skip the real-emulator instance tests on Windows
wmadden-electric Oct 13, 2026
ced1103
Merge fix/dev-ready-after-listening into feat/emulator-instances
wmadden-electric Oct 14, 2026
f309007
test(local-target): the emulator's listens hint is rewritten to the c…
wmadden-electric Oct 14, 2026
76257ae
Merge fix/dev-ready-after-listening into feat/emulator-instances
wmadden-electric Oct 15, 2026
2bc73cf
test(dev-emulators): readiness tests take ports no other test file uses
wmadden-electric Oct 15, 2026
a16380a
Merge fix/dev-ready-after-listening into feat/emulator-instances
wmadden-electric Oct 15, 2026
21c6308
test(dev-emulators): drop a repeated assertion in the session-resume …
wmadden-electric Oct 15, 2026
40dbfc4
Merge fix/dev-ready-after-listening into feat/emulator-instances
wmadden-electric Oct 15, 2026
6e495ff
Merge main into feat/emulator-instances
wmadden-electric Oct 15, 2026
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: 6 additions & 0 deletions .drive/projects/emulator-commands/deferred.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Deferred

- **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.
- **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.
63 changes: 63 additions & 0 deletions .drive/projects/emulator-commands/design-notes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Design notes β€” emulator commands

Decisions taken while shaping, with the reasoning. Will's rulings are marked; the rest are orchestrator decisions he can overturn.

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

- The commands are `prisma dev url <address>` and `prisma emulator list`. A top-level `prisma url` was rejected as ambiguous (local and deployed running objects).
- `emulator` commands act on the machine's emulators, not on a project.
- The emulators know nothing about Composer. Composer maps (directory, app, address) to emulator instances itself, per directory. Instances carry informative labels only.
- `dev url` resolves a Composer node address only, never an emulator instance id.
- Databases and buckets are in scope alongside services.
- Piped output stays JSON; docs use `jq`. No output-format change to the CLI engine.
- The CLI engine changes so a path can be both a command and a group, keeping `prisma dev <entry>`.
- The emulators may move out of Composer later (own project, or prisma-cli). Not part of this work.

## Facts the design rests on (checked against main, 90a30381)

- The config does not name the entry or the app. ADR-0003 rejected a config that names the app; `deploy-cli.md` lists entry discovery as out of scope. So `dev url` cannot find the app from `prisma.config.ts`; it needs a record `prisma dev` leaves in the directory.
- Today the emulators are app-aware: Compute routes are `/apps/:app/services/:id`, Postgres server names come from `instanceNameFor(app, db)`, buckets are named `<app>--<bucket>`. The local identity is the root name alone (`target/src/local-target/container.ts`), so two directories running one app share instances.
- `LocalTargetAttachment.endpoints()` returns `{ address, url }` only. The emulator's `ServiceInfo` has `status: running | backoff | held | stopped`.
- The engine rejects a command path under another command (`packages/cli-engine/src/execution/command-tree.ts:157`).
- When stdout is not a terminal the engine prints JSON frames: `{"kind":"result","envelope":{"ok":true,"result":…}}` (`shared-flags.ts:191`, `settlement.ts:83`).
- `9-public` may import both `framework` and `prisma-cloud` (`architecture.config.json` cross-domain rules). `framework` imports nothing outside itself.

## Where the code lives

- **Emulators** (`dev-emulators`): instance-keyed routes and state; every create call accepts a `labels: Record<string, string>` that is stored and returned verbatim. Each emulator gains a route that lists all its instances.
- **The mapping** is owned by the Prisma Cloud local target (`local-target` / `target/src/local-target`), because that code creates the instances and learns their ids. It lives in `<cwd>/.prisma-composer/dev/` (the dev state directory, already per working directory), and records the app name plus, per node address, the emulator kind and instance id.
- **`prisma dev url`** goes through the core seam, like the `log` operation: config, then `resolveLocalTargets`, then the target's attachment. The attachment gains a method that resolves one address to `{ address, kind, url, state }`. Core stays target-agnostic; the framework domain imports nothing new. It runs from a Composer project, which `dev url` needs anyway.
- **`prisma emulator list`** needs no project and no config, so it cannot go through the seam. Its handler lives in the Prisma Cloud domain next to the emulator client, and `@prisma/composer-cli` (in `9-public`, which may import `prisma-cloud`) adds it to the command family. No dependency-cruiser exception is needed.
- The emulator client stays the only module that speaks the emulators' HTTP API, so a later move out of Composer replaces one dependency.

## Ports (Will, 2026-10-13: agents in many worktrees start and stop environments)

An instance keeps its port from creation until it is deleted, running or stopped. A deployment carries its port inside the service's environment (`COMPOSER_<ADDRESS>_PORT`) and in its dependents' URLs, and Alchemy's no-op reconcile on a warm start never redeploys, so a port that could change between sessions would leave services built for the old port. Releasing ports on stop was considered and dropped for that reason (implementer finding, S1a dispatch 1). The cost: instances left behind by deleted worktrees keep their ports until the later remove command deletes them.

Composer remembers each node's port in the directory's mapping and requests it only when it has to create the instance again, for example after the emulator lost its state. A requested port is a preference: if another instance or process holds it, the emulator gives the lowest free port from 3000 and `prisma dev` prints one line saying the port changed. Failing instead would break an agent's `prisma dev` whenever another worktree happened to take the port. On a first start, ports still go in dependency order (prisma/composer#350).

## Labels Composer writes

`composer.directory` (absolute path), `composer.app` (app name), `composer.address` (node address). `emulator list` shows them; nothing reads them back as identity.

## Output shapes

- `dev url`, human: the URL alone on stdout's data line, so `--format human` also works in `$(…)`. JSON `result`: `{ address, kind, url, state }`, where `kind` is `service | database | bucket` and `state` is the service status, or `running` for a database or bucket whose emulator is up.
- `emulator list`, JSON `result`: `{ emulators: [{ kind, running, url, instances: [{ id, kind, url, state, labels }] }] }`. Human: one block per emulator, instances listed under it with the address and directory from their labels when present. It doesn't fit the list convention's `{ name, id, status }` items, because two levels are nested. That is a deliberate departure, noted in the slice spec.

## Error codes (namespace `DEV` for `dev url`, new `EMULATOR` for `emulator list`)

- `DEV.NOT_STARTED_HERE`: no `prisma dev` has run in this directory (no mapping). Fix: run `prisma dev <entry>` here.
- `DEV.ADDRESS_UNKNOWN`: no node with that address in this directory's app; lists the known addresses.
- `DEV.EMULATOR_UNREACHABLE` (from #353): the node's emulator is not running or not answering.
- `DEV.SERVICE_NOT_RUNNING`: the service exists but is stopped, backing off or held; says which, and points to `prisma dev`.
- `EMULATOR.*`: only if `list` needs one beyond `DEV.EMULATORS_DIR_INVALID` and `DEV.EMULATOR_UNREACHABLE`. Final names are fixed in the slice specs and recorded in ADR-0044.

## Alternatives considered

- **`prisma emulator url <address>`.** Same behaviour, but `url` asks a Composer question about this directory's app, which belongs under `dev`.
- **Rename the run command to `prisma dev start`.** Avoids the engine change, but breaks every `prisma dev module.ts` and reverses ADR-0050's root verb.
- **Name the app on the command line (`url my-app gateway`) or with `--app`.** Addresses are meant relative to the local app, and names collide across directories.
- **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.
64 changes: 64 additions & 0 deletions .drive/projects/emulator-commands/plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Emulator commands β€” project plan

**Spec:** `.drive/projects/emulator-commands/spec.md` Β· **Design notes:** `design-notes.md`

## Slices

### S1 β€” Emulators know nothing about Composer (prisma/composer)

**Outcome.** The Compute, Postgres and bucket emulators key instances by their own ids, accept and return free-form labels, and each lists all its instances. The Prisma Cloud local target creates instances, labels them, and records the per-directory mapping in `.prisma-composer/dev/`. Every Composer operation on the emulators goes through the mapping: deploy, session start and stop, logs, `--fresh` teardown. `prisma dev` behaves the same from outside, except that pre-upgrade local state is discarded. Two directories running one app get separate instances. Port allocation keeps its current rule on a first start: services reserve ports in dependency order (prisma/composer#350), lowest free from 3000. An instance keeps its port from creation until it is deleted, stopped or not, so a directory's ports never change. The mapping remembers each node's port and requests it when an instance has to be re-created. When `prisma dev` ends it stops all of the directory's instances, Postgres servers included (the Postgres emulator gains a stop that keeps data).

**Builds on:** prisma/composer#353 (stack on its branch until it merges).
**Hands to S3:** the mapping file and its reader; the emulator client's list-everything calls; ADR amending ADR-0041.

Likely too large for one review: split at pickup into Compute (with the mapping and remembered ports), then Postgres and buckets (with the Postgres stop).

### S2 β€” A command path can also be a group (prisma/prisma-cli)

**Outcome.** The CLI engine accepts a command mounted at a path that other commands mount under. `prisma dev module.ts` runs the command, `prisma dev url gateway` runs the subcommand, a subcommand name wins over a positional argument with the same spelling, and `prisma dev --help` shows the command's usage and its subcommands. Proven with engine tests using a stand-in command; no Composer change needed.

**Builds on:** nothing. Runs in parallel with S1.
**Hands to S4:** an engine release that allows the `dev` mount.

### S3 β€” `prisma dev url`, `prisma dev stop` and `prisma emulator list` (prisma/composer)

**Outcome.** The command family exports `dev url`, `dev stop` and `emulator list` with tests first (bun:test; emulator tests set `PRISMA_COMPOSER_EMULATORS_DIR` to a temporary directory). The core local-target attachment gains the resolve-one-address method. Error codes are recorded in ADR-0044. A new ADR amends ADR-0050 (the family is no longer exactly `deploy` and `dev`). `local-dev.md`, the running-locally guide and the core-concepts skill are updated, including that `start` and `stop` are planned.

**Builds on:** S1.
**Hands to S4:** a Composer release containing both commands.

### S4 β€” Mount the commands (prisma/prisma-cli)

**Outcome.** The `prisma` binary pins the Composer release from S3 and mounts `dev url`, `dev stop` and `emulator list`, with an `emulator` group whose help defines "emulator", `mount-coverage.test.ts` updated, and `EXCLUSIONS` entries in `e2e-coverage.test.ts`.

**Builds on:** S2, S3 (released).
**Hands to S5:** a `prisma` release with both commands.

### S5 β€” Docs stop hard-coding ports (prisma/web)

**Outcome.** The Composer getting-started and local-development pages use `prisma dev url` instead of `localhost:3000`/`3001`.

**Builds on:** S4 (released).

## Sequence

```
#353 ─► S1 ─► S3 ─┐
β”œβ”€β–Ί S4 ─► S5
S2 β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
```

S1 and S2 run in parallel. S4 and S5 wait on releases, which are currently blocked on an upstream Alchemy issue.

Five slices is one over the usual ceiling. S4 and S5 are small handoffs into other repos, and each one waits on a release, so they can't fold into a neighbour.

## Tracking

No Linear project exists for this work. Slices are tracked in this plan.

## Close-out

- [ ] Verify every Project DoD item in `spec.md`.
- [ ] Long-lived content lives in ADRs, `docs/design/10-domains/local-dev.md`, the guide and the skill.
- [ ] Strip repo-wide references to `.drive/projects/emulator-commands/`.
- [ ] Delete `.drive/projects/emulator-commands/`.
Loading
Loading