feat(cli): prisma-composer dev — bring the whole app up locally, credential-free - #164
Conversation
|
Warning Review limit reached
Next review available in: 35 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (4)
WalkthroughThe CLI now supports a 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
✨ Simplify code
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
commit: |
Run 30003079171 (#164's Test job) died undiagnosable: the teed converge log the wrapping error names is on the dead runner, so its content never reached anyone. All three proving scripts now dump diagnostics to stderr on ANY failure, before exiting — every converge/ session log's tail (bounded ~60 lines, never a full dump), both emulator daemons' log tails, and `prisma dev ls` — so the NEXT failure is readable straight from the GitHub Actions log, no runner access needed. Root-cause reasoning (confirmed vs hypothesis): CONFIRMED, by local reproduction: a fresh (never-used) `prisma dev` state produces a FAST, clean, non-timeout provisioning failure when it can't claim a port — reproduced locally by forcing a genuinely empty `HOME` (a fresh emulator registry AND a fresh prisma-dev config/cache in one move) via a small wrapper process, since the sandbox blocks a bare `HOME=... bun run ...` invocation directly. This reproduction's SPECIFIC failure ("Port 51215 is not available") was itself a machine-local artifact — my own workstation's pre-existing `open-chat` prisma-dev instance squats that exact port range, which a genuinely clean CI runner would not have — so it does not by itself prove CI's exact port collision. What it DOES confirm is the general shape: a `prisma dev` provisioning failure surfaces exactly this way — fast, clean, embedded three provider frames deep inside the alchemy converge — matching CI's `timedOut=false` signature precisely, and CONFIRMS the failure text (the actual provider error, e.g. "Port ... not available" or a download error) was ALREADY being captured correctly into the converge log the whole time; the only reason CI "learned nothing" is that nothing printed that log's content to stdout/stderr before the runner died — exactly what this commit fixes. HYPOTHESIS, not locally confirmed (my dev workstation has 85MB of `~/.cache/prisma` accumulated over many sessions; nothing in this repo or CI currently warms it, and I could not force a truly clean "never-downloaded-anything" state without also colliding with my own machine's other real instances): `prisma dev`'s FIRST-EVER invocation on a given machine downloads its database engine binaries; a CI runner starts genuinely cold every time, unlike a dev workstation. Addressed regardless of whether this is the exact CI trigger — it can only help, never weaken anything: CI now runs `prisma dev --name ci-engine-warmup --detach` (then removes it) as its own isolated step, between building and testing. If a cold download is (part of) the cause, this either fixes it outright or turns it into its own clearly-attributable, early-failing step instead of something buried inside a 5-service `alchemy deploy`. Ruled out (checked directly, not just assumed): the fixture app's own deps/build — `pnpm build` (turbo, every package) already runs before `pnpm test` in the Test job, and the S4 fixture's "built output" is hand-written source checked into git, not generated, so no missing build step there; `@aws-sdk/client-s3` resolution — committed to pnpm-lock.yaml, `pnpm install --frozen-lockfile` verified locally against the committed lockfile; turbo's env passthrough — no `envMode`/`strict` restriction in turbo.json, so `HOME`/`PATH` reach the test process unrestricted; the `prisma` bin's own resolution walk (root `node_modules/.bin/prisma`, not a per-package one) — verified locally via `pnpm exec prisma`, which is what the new CI step uses rather than assuming a path that turned out not to exist per-package. Not confirmed as the actual CI cause — this is what the diagnostics land to determine on the next run: whether the real trigger is the cold engine download, a genuine port collision unique to the runner, or something this reasoning didn't anticipate. Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…t for free-port checks Dependency razor (spec § 2, design commit 904d23a): commodity infrastructure with latent edge cases uses maintained libraries. Our own hand-rolled versions of both already produced real cross-platform bugs this project hit directly — a BSD/GNU pgrep divergence (round 5) and a Linux-only port-probe self-collision (the errno-driven fix two commits back on this branch). Retires the earlier no-new-deps contract for exactly this class of code. daemon.ts: the concurrent-ensure protocol's mkdir+pid-file lock is replaced by proper-lockfile locking a stable per-daemon target file under registryRoot. Its own staleness/compromise semantics are adopted wholesale — no pid liveness checking on top. Our wrapper keeps only what spec § 2 pins: a ~10s bounded wait (proper-lockfile's retries option), the pinned timeout error on ELOCKED exhaustion, re-reading the registry after acquiring, and releasing in a finally on every path. Port allocation (smallestUnused) now calls get-port for the actual bind check; persistence and the >= 4300 range/exclusion policy stay ours. ensureDaemon's signature gains an explicit entry parameter — ensureDaemon(name, entry, opts) — instead of resolving @internal/dev-emulators/<name>-main internally. This mirrors what the S5 branch (bot/claude/local-dev-s5-dev-command) already needs for its own reason (a published dist can't import.meta.resolve a private workspace package), threading it now so that cascade needs no manual reconciliation inside the retry loop. Test helpers: isPortFree/findFreePort's hand-rolled bind probing is deleted; every scratch-port need in the test suite now calls get-port directly. The two lock-behavior tests in daemon.test.ts are rewritten to proper-lockfile's actual on-disk mechanism (an aged .lock directory for staleness; a real second process genuinely holding the same lock via proper-lockfile itself for the live-holder timeout) rather than asserting against the old hand-rolled pid-file format — their assertions (one daemon exists, a stale lock is broken, a live holder times out with the pinned message) are unchanged. New dependencies of @internal/dev-emulators (private, workspace-only): proper-lockfile, get-port, @types/proper-lockfile (dev). tsdown's skipNodeModulesBundle keeps both external in dist/*.mjs, confirmed by inspection of the built output — this package gains no new *transitive* runtime deps for its current (private, in-repo) consumers. Packaging note for S5: when the public package re-emits these daemon entry points for npm consumers (spec § 2's publish note), that public package will need proper-lockfile and get-port as its OWN dependencies too, since they are not inlined — flagging now, no action taken here since no public consumer exists yet on this branch. CI read for the next merge (not fixed here — the cascade replaces S5's daemon.ts with this one): diffed this branch's post-swap daemon.ts against bot/claude/local-dev-s5-dev-command's daemon.ts. The retry loop (exit-detection via awaitHealthy, port-exclusion via usedPorts, error classification via exitedBeforeHealthy) is otherwise identical between the two branches; the only structural difference is the entry-parameter threading this commit adopts. The Linux-only CI failure on #164 (run 30007836065, "a bind failure on a fresh allocation retries the next free port", 529ms fail, "Expected length: 1, Received length: 0" on the listening-line count) is therefore not explained by any logic divergence in the retry path itself between the two branches. Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
954ebec to
57e7374
Compare
Run 30003079171 (#164's Test job) died undiagnosable: the teed converge log the wrapping error names is on the dead runner, so its content never reached anyone. All three proving scripts now dump diagnostics to stderr on ANY failure, before exiting — every converge/ session log's tail (bounded ~60 lines, never a full dump), both emulator daemons' log tails, and `prisma dev ls` — so the NEXT failure is readable straight from the GitHub Actions log, no runner access needed. Root-cause reasoning (confirmed vs hypothesis): CONFIRMED, by local reproduction: a fresh (never-used) `prisma dev` state produces a FAST, clean, non-timeout provisioning failure when it can't claim a port — reproduced locally by forcing a genuinely empty `HOME` (a fresh emulator registry AND a fresh prisma-dev config/cache in one move) via a small wrapper process, since the sandbox blocks a bare `HOME=... bun run ...` invocation directly. This reproduction's SPECIFIC failure ("Port 51215 is not available") was itself a machine-local artifact — my own workstation's pre-existing `open-chat` prisma-dev instance squats that exact port range, which a genuinely clean CI runner would not have — so it does not by itself prove CI's exact port collision. What it DOES confirm is the general shape: a `prisma dev` provisioning failure surfaces exactly this way — fast, clean, embedded three provider frames deep inside the alchemy converge — matching CI's `timedOut=false` signature precisely, and CONFIRMS the failure text (the actual provider error, e.g. "Port ... not available" or a download error) was ALREADY being captured correctly into the converge log the whole time; the only reason CI "learned nothing" is that nothing printed that log's content to stdout/stderr before the runner died — exactly what this commit fixes. HYPOTHESIS, not locally confirmed (my dev workstation has 85MB of `~/.cache/prisma` accumulated over many sessions; nothing in this repo or CI currently warms it, and I could not force a truly clean "never-downloaded-anything" state without also colliding with my own machine's other real instances): `prisma dev`'s FIRST-EVER invocation on a given machine downloads its database engine binaries; a CI runner starts genuinely cold every time, unlike a dev workstation. Addressed regardless of whether this is the exact CI trigger — it can only help, never weaken anything: CI now runs `prisma dev --name ci-engine-warmup --detach` (then removes it) as its own isolated step, between building and testing. If a cold download is (part of) the cause, this either fixes it outright or turns it into its own clearly-attributable, early-failing step instead of something buried inside a 5-service `alchemy deploy`. Ruled out (checked directly, not just assumed): the fixture app's own deps/build — `pnpm build` (turbo, every package) already runs before `pnpm test` in the Test job, and the S4 fixture's "built output" is hand-written source checked into git, not generated, so no missing build step there; `@aws-sdk/client-s3` resolution — committed to pnpm-lock.yaml, `pnpm install --frozen-lockfile` verified locally against the committed lockfile; turbo's env passthrough — no `envMode`/`strict` restriction in turbo.json, so `HOME`/`PATH` reach the test process unrestricted; the `prisma` bin's own resolution walk (root `node_modules/.bin/prisma`, not a per-package one) — verified locally via `pnpm exec prisma`, which is what the new CI step uses rather than assuming a path that turned out not to exist per-package. Not confirmed as the actual CI cause — this is what the diagnostics land to determine on the next run: whether the real trigger is the cold engine download, a genuine port collision unique to the runner, or something this reasoning didn't anticipate. Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1d46fde to
bed2cf7
Compare
Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… command pipeline.ts pulls steps 1-6 of run() (config discovery/load, entry load, Load, coverage validation, name resolution, assemble) into one function consumed by both deploy/destroy and the new dev command, so the two pipelines cannot drift (local-dev spec S6). run() is refactored to call it; deploy/destroy behavior is unchanged (run.test.ts passes unmodified). DevCommand adds `dev <entry>` with --name/--fresh only; --stage and --production are rejected as unknown flags. ParsedArgs.command widens to deploy | destroy | dev. Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
.prisma-composer/dev/alchemy.run.ts, mirroring generate-stack.ts: dev: true, localState() from alchemy/State/LocalState, no report (dev prints its own front door), header comment naming the --stage dev reproduction command. Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
watchTargetsFrom() reads each bundle's watch paths (a bundle without any is reported unwatchable, the pinned startup note); startWatch() fs.watches them (recursive for directories, plain for files), debounced 300ms and coalesced across every service into one callback per burst. Bundle.watch does not exist on this branch yet (spec Section 3, the S2 slice — see .drive/projects/local-dev/spec.md Open Questions for the branch-dependency gap this records). watchTargetsFrom() reads it through a locally-declared structural extension with one justified blindCast, so this is fully wired the moment S2 lands; every bundle takes the pinned "not watched" fallback until then. Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… loop Local-dev spec Section 6's run-dev.ts: reuses pipeline.ts for the shared prefix, then the dev-only steps in order — Windows guard, dev-capability check (every non-build-only extension must declare `dev`), per-extension containers, `--fresh` teardown, preflight, emulators, writes the dev stack file and converges via runAlchemy with --stage dev (a nonzero exit prints the reproduction hint and returns that status without attaching), attach (merged endpoints rendered as the pinned front door, logs pumped prefixed `[<service>] `), and the watch loop — debounced rebuild re-assembles all services, rewrites the stack file, re-converges; a converge failure during watch leaves the running app untouched and keeps watching, a success re-prints the front door. SIGINT/SIGTERM stop every attachment's services and exit 0 — emulators and their data stay up. main.ts's run() dispatches to runDev() as soon as args.command is 'dev', before any deploy/destroy-only step runs. Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ve the Bundle.watch gap note Takes .drive/projects/local-dev/spec.md wholesale from bot/claude/local-dev-feature-design-0b49fd (4ce7160) — picks up the fresh-allocation port-retry protocol for daemon.ts (S3 scope, not touched here). Rewrites the S5 Open Questions entry from an open gap to a resolved note: the Bundle.watch structural-type fallback already shipped in watch.ts covers it, and stays documented in code. Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…sses Two real defects surfaced while building the proving pass against examples/store (a real multi-module app, unlike the flat-address S4 fixture) — both fixed here, verified live against examples/store with Ctrl-C observed end to end. 1. run-dev.ts's SIGINT/SIGTERM handling never ran. alchemy's own library code (pulled in transitively while loading the app's config/providers, e.g. `alchemy/Provider` from the local dev providers) registers its own process-level SIGINT/SIGTERM listeners for its own in-process bookkeeping. Those listeners run before this module's (registered later, in the watch loop), and whichever of them fires first can call process.exit() synchronously, tearing the process down before the watch loop's own async cleanup (stopping the app's services through the attachment) gets a turn — confirmed live: the CLI process died on SIGINT with exit code 130 (killed by the raw signal, default disposition) and every service child was left running. Those listeners are irrelevant here regardless: the actual converge runs in a separately spawned `alchemy` child process (run-alchemy.ts), never in this one. Fixed by stripping whatever is registered and becoming the sole SIGINT/SIGTERM listener right before entering the watch loop. Also adds the "[dev] stopping…"/"[dev] stopped." lines the spec left unpinned but a session needs to observe. 2. LocalComputeServiceProvider/LocalDeploymentProvider (@internal/lowering/src/dev/compute.ts) passed a service's own hierarchical address (e.g. "orders.service", a nested module's service) straight through as the Compute emulator's `<id>` path segment, which the emulator's own API hygiene rule rejects (`/^[a-z0-9][a-z0-9-]*$/` — no dots). Every non-flat-address app (any module nesting a service under an `id`) failed to converge in dev at all. Fixed with a `slugServiceId()` helper applied only at the two emulator call sites; the real address still rides the deployment body's `address` field untouched, so `attach.ts`'s front door and `listServices()` still report it verbatim — attach.ts's log follower is updated alongside to label lines with that real address (not the slugged id) so a service's log prefix matches its front-door line. Both are outside the narrowly sanctioned S5 change to S1-S4 files, but block the coordinator-directed proving pass entirely (examples/store cannot converge or shut down without them) and are minimal, targeted, and verified against the live app — flagged prominently in the S5 report rather than landed silently. Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…real CLI
The plan-S5 proving pass the previous session ran out of budget for.
Unlike every other integration script in this package, this one drives
the real, published `examples/store/node_modules/.bin/prisma-composer`
binary as a genuine child process — argv parsing, the CLI's own SIGINT
handling, and its own process lifetime are themselves what's under test,
against a real four-service, two-Postgres, cron-bearing app.
Proves, against three real `dev` sessions:
- Criterion 1: credential-free bring-up (PRISMA_WORKSPACE_ID/
PRISMA_SERVICE_TOKEN/PRISMA_REGION stripped from the child's env), the
`[dev] ready:` front door parsed from real stdout, an HTTP round-trip.
- Criterion 3: a row written directly against the real local Postgres URL
(read from postgres.json — the storage layer, never the app's own RPC)
survives a Ctrl-C + warm restart, and is gone after `--fresh` (the
fresh instance re-migrates as part of the very same converge, so the
proof is the proving row's absence, not the table's).
- Criterion 6: the second (warm) session's front-door ports match the
first's exactly.
Two real bugs surfaced and fixed while building this (see the prior
commit): run-dev.ts's SIGINT handling never actually ran, and nested
service addresses ("orders.service") failed the Compute emulator's own
segment rule. Without both fixes, examples/store cannot converge or
shut down at all, so this script could not have existed against real
store; it now runs clean, 0 failures, three consecutive dev sessions.
Wired into the package's own `test` script as a second `bun run` step,
matching local-dev.integration.ts's own pattern; `@prisma/example-store`
added as a devDependency so turbo builds it (and its four modules)
before this runs.
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Found on the scratch/s5-watch-proof branch (S2 merged in temporarily to exercise Bundle.watch for real against examples/store — see the S5 report for the full transcript): `fs.watch(filePath)` bound to a single file's inode goes silently dead after that file is deleted and recreated at the same path — exactly what `rm -rf dist && bun build --outfile dist/server.mjs` does on every rebuild (also confirmed with plain `bun build --outfile` alone, no `rm -rf` — bun's own writer replaces the file via an atomic rename, which is the same class of inode change). The watch fired once, on the very first rebuild, then never again. Fixed by watching the file's parent directory (non-recursive) and filtering events to the file's own basename — a directory watch's handle isn't tied to any one child's inode, so it survives however the build tool replaces the file. Directory targets are unaffected (already watched recursively at the target path itself). Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Local-dev spec § 4's rewritten step 3 (pinned, design tip b2cb63e) — the fix for restart amplification: the hosted platform materializes the app-wide env row set into every deployment but DIFFS a deployment only on its own referenced rows. Local dev's materializeEnv was copying the FULL env.json into every service's deployment unscoped, so an early-deployed service's snapshot looks incomplete on the first converge and "completes" (and diffs as changed) on the very next one — restart amplification. scopedEnvRows() fixes it: a service keeps only its own `COMPOSER_<its address>_*` rows plus every row OUTSIDE the `COMPOSER_` namespace entirely (the poison DATABASE_URL/DATABASE_URL_POOLED rows are deliberately unprefixed and app-wide, per the pinned parity note — kept for every service, matching the platform). The dropped sibling rows have no sanctioned reader: run()/load() consume only own-address rows. Note on provenance: the coordinator's cascade instructions named two commits on `claude/local-dev-s5-fix-restart-amplification` (`a745e07` fix, `2dcbb32` test) to cherry-pick. That branch is not present on any remote reachable from this worktree (`git ls-remote bot` lists s1 through s5-dev-command only) — fetching it by name or by commit sha both fail. Reimplemented directly from the pinned spec text instead, verified against it line by line, and added new coverage (dev-compute-scoped-env.test.ts) rather than porting the referenced test commit sight-unseen. Re-ran the S4 integration proof (test/local-dev.integration.ts) against the real converge — still passes, including its DATABASE_URL/DATABASE_URL_POOLED poison-row assertions (both stay app-wide, per the pinned note). Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
) Takes both files verbatim from bot/claude/local-dev-feature-design-0b49fd so the diff against it is empty on this branch — picks up the pinned restart-amplification fix text (spec Section 4) implemented in the prior commit, and the fresh-allocation port-retry protocol (spec Section 2, S3 scope, already on this branch via the S3 rebase). Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Extends the S4 fixture (test/fixtures/local-dev/): - web-service's built server gains a real @aws-sdk/client-s3-backed /blobs/:key PUT/GET, reusing its already-bound bucket() dependency (the same client examples/bucket's blob app uses). - bg-service gains a secret slot (apiKey) and an envParam (greeting), bound in module.ts to LOCALDEV_FIXTURE_API_KEY / LOCALDEV_FIXTURE_GREETING; its built server exposes GET /status reporting whether the secret looks like a minted placeholder and echoing the bound greeting. New local-dev-criteria-4-5.integration.ts drives the real CLI against this fixture (same pattern as local-dev-store.integration.ts): PUTs an object through the app and confirms it lands as a plain file under .prisma-composer/dev/buckets/files/, then drops a file directly on disk and reads it back through the app (criterion 4); one session with the secret unset confirms the pinned placeholder warning appears and the topology still serves, a second with the envParam unset confirms dev exits nonzero with the pinned listing error naming it (criterion 5). Wired into the package's test script as a third step. local-dev.integration.ts (S4's own direct-driving script) needed two fixes to keep passing against the extended fixture: - It sets both env vars before calling dev.preflight() so its own (unrelated) proofs are unaffected by the new required slots, and updates the "secrets.json is empty" assertion to the shell-sourced values it now sets. - Found live: dev.preflight() computes its state directory from process.cwd() rather than accepting one, unlike every other dev hook this script calls (all handed devDir explicitly, fixture-relative). This script runs from the package root, not the fixture dir, so preflight's write previously landed nowhere this script ever looked — invisible only because preflight was a total no-op before this fixture had any secrets/params to iterate over. Fixed by temporarily chdir'ing to the fixture around the preflight call (restored in finally); preflight.ts itself is untouched. Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…are in the proving script Two real, reproducible flakes surfaced running the store proving script repeatedly against a 5-service app (examples/store) rather than the S4 fixtures 2-service one: 1. run-dev.ts: attach()s endpoints() fetch, called immediately after a converge that just PUT dozens of resources through the same loopback connection, occasionally hit a transient refused/reset connection on a real, busier app — confirmed live (converge succeeded, "Done: 102 succeeded", then "Error: fetch failed" tore the whole dev session down). Retried up to 3 times with a 500ms delay before giving up; a genuine failure still surfaces after the retries exhaust. 2. local-dev-store.integration.ts: opening a fresh Bun.SQL client per call against the same URL (this scripts own pattern, not app code) could race the previous connections prepared-statement cleanup, surfacing as `prepared statement "..." already exists`. This is a storage-layer proof, not a prepared-statement-caching one, so `prepare: false` turns caching off. Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The design branch advanced again during this session (strikes the dir() adapter entirely - node()s existing directory form already covers the need, per the operators catch). Re-synced wholesale so the diff against bot/claude/local-dev-feature-design-0b49fd stays empty; does not touch any S5 code (dir() was S2s scope, never referenced from this branch). Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…EnvRows Equivalence check against bot/claude/local-dev-s5-fix-restart-amplification (a745e07, now reachable): its scopedEnv() and this branchs scopedEnvRows() are algorithmically identical — same COMPOSER_<SEGMENTS>_ prefix construction (trailing underscore included), same startsWith(prefix) || !startsWith(COMPOSER_) filter, same untouched port-override/secrets/PATH/HOME handling below it. No behavioral divergence found. Adds the one case the coordinator asked to verify explicitly: web vs web2 is a literal string prefix but the trailing underscore on the match prefix (COMPOSER_WEB_) correctly excludes COMPOSER_WEB2_PORT — both implementations already get this right; this pins it against future regression. Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…e service) Regression proof for the restart-amplification fix in compute.ts: touch ONLY catalog's built artifact, re-converge, assert exactly catalog.service gets a new pid and all four other services' pids are unchanged. Run twice against the same live session. Not driven through the CLI's own file-watch loop: Bundle doesn't declare `watch` on this branch yet (spec S2 slice), so every service is currently unwatchable and a file edit is never detected. Instead: session 1's real `prisma-composer dev` still writes the dev stack file and sets up containers/emulators exactly as a real session would; the artifact is touched, the node build adapter's own assemble() is re-run for catalog specifically (it copies the built runnable into .prisma-composer/artifacts/catalog.service/bundle/ — touching the source alone doesn't move anything alchemy actually reads), then the same stack file is re-converged directly with the real alchemy binary. Session 1's services are never stopped in between — SIGINT is app-scoped and unconditionally restarts every service on the next redeploy (dev-emulators' own pinned "a stopped service always starts on redeploy" behavior), which would defeat this proof entirely. Also fixed a real cleanup gap found while validating this: the script's final --fresh-then-SIGINT teardown left the prisma dev postgres instances that cleanup session's own (mandatory) converge had just recreated, since --fresh only removes what existed BEFORE it runs, never what it creates during its own converge. Call dev.teardown directly afterward to actually remove them. Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Run 30003079171 (#164's Test job) died undiagnosable: the teed converge log the wrapping error names is on the dead runner, so its content never reached anyone. All three proving scripts now dump diagnostics to stderr on ANY failure, before exiting — every converge/ session log's tail (bounded ~60 lines, never a full dump), both emulator daemons' log tails, and `prisma dev ls` — so the NEXT failure is readable straight from the GitHub Actions log, no runner access needed. Root-cause reasoning (confirmed vs hypothesis): CONFIRMED, by local reproduction: a fresh (never-used) `prisma dev` state produces a FAST, clean, non-timeout provisioning failure when it can't claim a port — reproduced locally by forcing a genuinely empty `HOME` (a fresh emulator registry AND a fresh prisma-dev config/cache in one move) via a small wrapper process, since the sandbox blocks a bare `HOME=... bun run ...` invocation directly. This reproduction's SPECIFIC failure ("Port 51215 is not available") was itself a machine-local artifact — my own workstation's pre-existing `open-chat` prisma-dev instance squats that exact port range, which a genuinely clean CI runner would not have — so it does not by itself prove CI's exact port collision. What it DOES confirm is the general shape: a `prisma dev` provisioning failure surfaces exactly this way — fast, clean, embedded three provider frames deep inside the alchemy converge — matching CI's `timedOut=false` signature precisely, and CONFIRMS the failure text (the actual provider error, e.g. "Port ... not available" or a download error) was ALREADY being captured correctly into the converge log the whole time; the only reason CI "learned nothing" is that nothing printed that log's content to stdout/stderr before the runner died — exactly what this commit fixes. HYPOTHESIS, not locally confirmed (my dev workstation has 85MB of `~/.cache/prisma` accumulated over many sessions; nothing in this repo or CI currently warms it, and I could not force a truly clean "never-downloaded-anything" state without also colliding with my own machine's other real instances): `prisma dev`'s FIRST-EVER invocation on a given machine downloads its database engine binaries; a CI runner starts genuinely cold every time, unlike a dev workstation. Addressed regardless of whether this is the exact CI trigger — it can only help, never weaken anything: CI now runs `prisma dev --name ci-engine-warmup --detach` (then removes it) as its own isolated step, between building and testing. If a cold download is (part of) the cause, this either fixes it outright or turns it into its own clearly-attributable, early-failing step instead of something buried inside a 5-service `alchemy deploy`. Ruled out (checked directly, not just assumed): the fixture app's own deps/build — `pnpm build` (turbo, every package) already runs before `pnpm test` in the Test job, and the S4 fixture's "built output" is hand-written source checked into git, not generated, so no missing build step there; `@aws-sdk/client-s3` resolution — committed to pnpm-lock.yaml, `pnpm install --frozen-lockfile` verified locally against the committed lockfile; turbo's env passthrough — no `envMode`/`strict` restriction in turbo.json, so `HOME`/`PATH` reach the test process unrestricted; the `prisma` bin's own resolution walk (root `node_modules/.bin/prisma`, not a per-package one) — verified locally via `pnpm exec prisma`, which is what the new CI step uses rather than assuming a path that turned out not to exist per-package. Not confirmed as the actual CI cause — this is what the diagnostics land to determine on the next run: whether the real trigger is the cold engine download, a genuine port collision unique to the runner, or something this reasoning didn't anticipate. Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Operator-approved exception to the no-new-runtime-deps contract (design tip 74272d8, spec S6 + behavior-contracts): chokidar absorbs the atomic-rename/inode-swap class watch.ts used to hand-fix with a parent-directory workaround (found live proving criterion 2 against examples/store), plus the cross-platform recursive-watch differences. v4 is pure JS, no native code; its glob support is irrelevant here — every target is a literal file or directory path from Bundle.watch, never a pattern. Deletes the parent-directory workaround entirely; the debounce stays on our side (chokidars own awaitWriteFinish is a different semantic — per-file "has this files size stopped changing," not "coalesce a burst across many files into one callback"). Reads Bundle.watch directly, no blindCast — this branch now has the real field via the S2 merge in the next commit; the design doc comment explaining the temporary structural cast is gone with it. Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
…lete+recreate test chokidar's watch() returns synchronously but finishes attaching OS-level watches shortly after. The delete+recreate regression test wrote to the file immediately after startWatch(), racing that setup and missing the first change. Added a 100ms grace wait before the first write; the assertions themselves (calls===1, then calls===2) are unchanged. Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…broke ensure
A nested module Database resource name is dot-separated
("catalog.database"), but the postgres-main daemon path-segment rule
forbids dots — the client threw invalid path segment before the request
left the process (observed live against examples/store). Same seam as
compute.ts slugServiceId: the daemon-facing id is the slug; slug() is
idempotent, so the daemon-side instanceNameFor(app, slug(name)) equals
the recorded instanceNameFor(app, name) and Connection lookup is
unaffected. Proven end to end against the real daemon.
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Follow-up to the base rename (95e4229): run-dev.ts imports resolveLocalTargets/LocalTargetDescriptor/LocalTargetAttachment from @internal/core/local-target; the generated dev stack template imports localTargetProviders/resolveLocalTargets from @prisma/composer/local-target (rendering test updated); the store proving script resolves descriptor.localTarget; the dotted-id drift test uses LocalTargetProvidersInput. spec.md/plan.md synced wholesale from design tip 12956d9. Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
bed2cf7 to
8d0f135
Compare
Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CodeQL (js/clear-text-logging): the top-level catch prints the raw error, and a converge failure can embed a live connection string; apply the spec's masking contract before the text reaches CI's public log. Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…efused Seen on CI: @prisma/dev refused a start because an aux listener port (51301) 'belongs to another Prisma Dev server' — its registry claims ports no bind probe sees, and the previous retry only recognized a conflict on the database port itself, so the refusal surfaced as a 500. All three of @prisma/dev's port-refusal errors carry the offending port; treat a conflict on any of the four requested ports as retryable and exclude conflicted ports from re-allocation. Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rrors CodeQL (js/clear-text-logging): the secrets.json read could ride an assertEqual failure message into the harness's failure dump; compare as a boolean so the value never enters the error text. Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ame registered Seen on CI directly after the port-refusal retry landed: attempt 2 was refused with 'A Prisma Dev server with the name ... is already running' — a start that fails mid-boot can leave its state entry (and sometimes the server itself) behind, and the name is namespaced to this app+database, so the ghost is ours by construction. Recovery: adopt the live server when the refusal carries one (ServerAlreadyRunningError's .server getter, duck-typed), clear a dead state entry via the internal state module's deleteServer and retry. Concurrent PUTs for the same database now also coalesce onto one in-flight start. Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tach time
Seen on CI: the delete-recreate watch test saw zero callbacks — the
change landed before chokidar's OS-level watches attached, so it was
missed entirely, and the test's fixed 100ms grace (plus a 100ms
post-debounce margin) is exactly the kind of guess a loaded runner
breaks. startWatch now returns { ready, stop }: ready resolves on
chokidar's 'ready' (and on stop, so an awaiting caller can't hang), the
dev loop awaits it before handing over — closing the same real startup
race for users — and the tests await readiness and poll to a deadline
instead of sleeping fixed margins.
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 5
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
packages/1-prisma-cloud/0-lowering/dev-emulators/src/postgres-main.ts (1)
449-523: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winStale-name cleanup retry is unreachable for existing (warm-restart) records.
maxAttemptsis1wheneverexistingRecordis defined (a warm restart), and the stale-name recovery branch only firesif (attempt < maxAttempts)— so for an existing record,attempt(1) < maxAttempts(1)is always false. If the daemon restarts and finds on-disk@prisma/devstate for an instance with no live server to adopt (ServerStateAlreadyExistsError, no.serverghost), it falls straight through to the generic throw instead of ever callingdeleteServer+ retrying — exactly the warm-restart self-healing this PR's own comment (481-486) describes. The port-retry budget (MAX_FRESH_PORT_CANDIDATESvs.1) is a different concern (existing records must keep their fixed port) and shouldn't also gate the orthogonal name-cleanup retry.🔧 Proposed fix: separate the name-retry budget from the port-retry budget
- const conflicted = new Set<number>(); - for (let attempt = 1; ; attempt++) { + const conflicted = new Set<number>(); + let nameRetriesLeft = 1; // one stale-name cleanup retry, regardless of fresh vs. existing + for (let attempt = 1; ; attempt++) { const aux = await allocateAuxPorts(new Set([dbPort, ...conflicted])); try { ... } catch (err) { if (isNameAlreadyTaken(err)) { const ghost = await adoptableServerOf(err); if (ghost !== undefined) { ... return { url }; } - if (attempt < maxAttempts) { + if (nameRetriesLeft > 0) { + nameRetriesLeft--; const internalState = await importPrismaDevInternalState(prismaDevModulePath); await internalState.deleteServer(instanceName); continue; } } ...🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/1-prisma-cloud/0-lowering/dev-emulators/src/postgres-main.ts` around lines 449 - 523, Separate stale-name cleanup retries from the port candidate budget in the startup loop around startPrismaDevServer. Keep existing records fixed to their stored database port, but allow one deleteServer(instanceName) cleanup and retry when isNameAlreadyTaken(err) has no adoptable ghost, including warm restarts; track this independently from maxAttempts so the cleanup path is reachable for existing records while preserving the existing fresh-port retry behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/0-framework/3-tooling/cli/src/dev/run-dev.ts`:
- Around line 267-299: Move the SIGINT/SIGTERM listener replacement to the
beginning of runDev(), before any startup pipeline work, while preserving
removal of Alchemy's inherited listeners. Refactor finish() to safely handle
partial startup state, conditionally stopping watch, aborting logs, and stopping
any attachments that have been created, then resolve cleanup without assuming
await watch.ready or full service startup completed.
- Around line 87-101: Update pumpLogs so its catch block suppresses errors only
when signal.aborted is true; otherwise report the log-stream failure with
appropriate error details. Preserve the current behavior of stopping quietly
when abortion interrupts attachment.logs(signal).
- Around line 231-262: Wrap the entire fire-and-forget rebuild body in the watch
callback, including writeDevStackFile, the alchemy deployment, and
printFrontDoor, with a try/catch so exceptions are handled instead of becoming
unhandled promise rejections. Preserve the existing runPipeline failure handling
and ensure caught errors are logged while the watcher continues running.
- Around line 205-218: Update the attachment startup loop around startServices()
to track successfully started attachments and, on failure, stop those
attachments in reverse order before rethrowing the CLI error. Preserve the
existing error conversion via toCliError(error), and ensure cleanup is attempted
only for attachments that started successfully.
In `@test/integration/test/local-dev-criteria-4-5.integration.ts`:
- Around line 319-338: Update the final fresh cleanup flow around
startDevUntilReady and stopDev to invoke the CLI’s app-scoped teardown after
stopDev completes, mirroring the post-stop dev.teardown call used by
local-dev-store.integration.ts. Keep the existing error handling and ensure
teardown runs for the cleanup session so the mandatory converge does not leave
the recreated postgres-main server behind.
---
Outside diff comments:
In `@packages/1-prisma-cloud/0-lowering/dev-emulators/src/postgres-main.ts`:
- Around line 449-523: Separate stale-name cleanup retries from the port
candidate budget in the startup loop around startPrismaDevServer. Keep existing
records fixed to their stored database port, but allow one
deleteServer(instanceName) cleanup and retry when isNameAlreadyTaken(err) has no
adoptable ghost, including warm restarts; track this independently from
maxAttempts so the cleanup path is reachable for existing records while
preserving the existing fresh-port retry behavior.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 3fff45a3-bbc3-4c71-b289-c714f4b4528e
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (27)
.github/workflows/ci.ymlpackages/0-framework/1-core/core/src/control/app-config.tspackages/0-framework/3-tooling/cli/package.jsonpackages/0-framework/3-tooling/cli/src/__tests__/main.test.tspackages/0-framework/3-tooling/cli/src/dev/__tests__/generate-dev-stack.test.tspackages/0-framework/3-tooling/cli/src/dev/__tests__/run-dev.test.tspackages/0-framework/3-tooling/cli/src/dev/__tests__/watch.test.tspackages/0-framework/3-tooling/cli/src/dev/generate-dev-stack.tspackages/0-framework/3-tooling/cli/src/dev/run-dev.tspackages/0-framework/3-tooling/cli/src/dev/watch.tspackages/0-framework/3-tooling/cli/src/main.tspackages/0-framework/3-tooling/cli/src/pipeline.tspackages/1-prisma-cloud/0-lowering/dev-emulators/src/postgres-main.tspackages/1-prisma-cloud/0-lowering/local-target/src/__tests__/compute-scoped-env.test.tspackages/1-prisma-cloud/0-lowering/local-target/src/__tests__/postgres-instance-name-drift.test.tspackages/1-prisma-cloud/0-lowering/local-target/src/compute.tspackages/1-prisma-cloud/0-lowering/local-target/src/exports/index.tspackages/1-prisma-cloud/0-lowering/local-target/src/postgres.tspackages/1-prisma-cloud/1-extensions/target/src/local-target/attach.tstest/integration/package.jsontest/integration/test/fixtures/local-dev/bg-service.tstest/integration/test/fixtures/local-dev/built/bg-server.mjstest/integration/test/fixtures/local-dev/built/web-server.mjstest/integration/test/fixtures/local-dev/module.tstest/integration/test/local-dev-criteria-4-5.integration.tstest/integration/test/local-dev-store.integration.tstest/integration/test/local-dev.integration.ts
The delete-recreate test kept failing on Linux CI with zero events even after readiness was awaited: an inotify watch bound directly to a file dies with the file's inode, and chokidar v4 absorbs atomic renames but NOT unlink+recreate of a directly watched file — which is the shape of every 'rm -rf dist && build --outfile' rebuild. File targets are now watched via a depth-0 chokidar watch on their parent, filtered to the exact path (the approach the pre-chokidar implementation had already proven on Linux); directory targets stay recursive watches of themselves. A nonexistent path counts as a file target, so it starts reporting the moment something creates it. Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Seen on CI: an ensure for a database with a persisted port was refused because its freshly allocated aux HTTP port was claimed by another Prisma Dev server's registry, and the retry cap for existing records was 1 — so a conflict on a port we would happily re-pick surfaced as a 500. The aux listener ports are never persisted; a refusal of one now always retries with fresh candidates. Only the DATABASE port stays frozen once a record exists (endpoints in deploy state reference it) — a refusal there still fails fast for existing records. Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ctions Seen on CI: the daemon died mid-request (connection refused on an ensure, pid gone), taking every app's databases with it. The daemon hosts @prisma/dev's runtime in-process, and a failed or abandoned start attempt can leave background async work behind whose eventual rejection kills the process — bun and node both exit on an unhandled rejection. A machine-shared daemon must not die because one attempt's debris rejected: log it (credential-masked, into the registry log) and keep serving; request paths still report their own errors as 500s. The drift test now prints the daemon's log tail on failure, so the next crash is diagnosable instead of a bare connection error. Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Seen on CI: right after a 102-resource converge the session died on a bare 'fetch failed' from startServices() — the transient-loopback retry existed only for endpoints(). One generic retry now wraps every attach admin call the dev session makes, with a slightly deeper budget (5 x 500ms) for loaded runners. Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rors Seen on CI: a converge died on 'fetch failed' / 'other side closed' from a local provider's emulator call — the keep-alive reuse race (the runtime reuses a pooled connection the daemon's HTTP server just closed idle), plus briefly overloaded daemons under CI load. Every admin call is idempotent by design, so the loopback client now retries each one a few times before failing; a caller-driven abort is never retried. This covers converge-time provider calls too, which the CLI-level attach retry could not reach. Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Four CodeRabbit findings on #164, three code + one test: - the rebuild handler runs fire-and-forget, but only runPipeline was inside its catch — a throw from writeDevStackFile or the alchemy spawn would be an unhandled rejection killing the process; the whole rebuild is now inside one try/catch and prints '[dev] rebuild failed' instead. - pumpLogs stayed quiet on EVERY stream error; now only the session- ending abort is quiet — a genuinely broken log stream says so. - a startServices() failure partway through the attachment list left the earlier attachments running; they are put back to stopped, so a session that never began leaves the machine as the previous Ctrl-C did. - the criteria-4-5 cleanup now runs the direct app-scoped teardown after its --fresh session, mirroring the store test — --fresh tears down BEFORE its own converge, which recreates the postgres server the cleanup meant to remove. Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/0-framework/3-tooling/cli/src/dev/watch.ts`:
- Around line 101-112: Register Chokidar error handlers on both watchers created
in the watcher setup, alongside their existing all listeners, so emitted errors
are handled without becoming uncaught emitter errors or bypassing cleanup.
Update the related watch tests to verify that both directory and parent watcher
error events are handled.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: c4d0cabf-e94e-414e-871f-f0b13bef220b
📒 Files selected for processing (5)
packages/0-framework/3-tooling/cli/src/dev/run-dev.tspackages/0-framework/3-tooling/cli/src/dev/watch.tspackages/1-prisma-cloud/0-lowering/dev-emulators/src/client.tspackages/1-prisma-cloud/0-lowering/dev-emulators/src/postgres-main.tspackages/1-prisma-cloud/0-lowering/local-target/src/__tests__/postgres-instance-name-drift.test.ts
…rently Seen on CI: the postgres daemon died SILENTLY (no JS error, no rejection-guard output, just gone) during a --fresh teardown, then a converge hit ECONNREFUSED. The servers run pglite's native/WASM runtime inside the daemon process, and the only concurrency we control in that window is deleteApp/shutdown closing several servers at once via Promise.all — a native abort takes the process out with no trace a JS guard can leave. All close and delete-state paths now run one server at a time, and a single close failure no longer aborts the rest of the teardown. Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
An 'error' emitted with no listener throws out of the emitter and would take the dev session down; a watch error (EMFILE, a vanished directory) now prints '[dev] watch error: ...' and the session keeps running. Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The decision:
prisma-composer devis a view over the running app, not a process manager.Context in one breath: this is the final layer of the local-dev stack (ADR-0041, in the base branch), on top of #162. Everything underneath already works — emulator daemons own the processes (#160), and the deploy pipeline provisions against them (#162). This PR is the command a person actually types. Stacked PR: the diff here is only this layer.
What the command does
It runs the same pipeline steps as
deploy— shared, extracted code, so the two cannot drift and deploy's behavior is provably unchanged — then diverges after assembly: resolve a local app identity, apply--freshif asked (wipe this app's local instances and data), run the dev value-sourcing checks, make sure the emulator daemons are up, write the generated dev stack file, and converge. Then it attaches: prints the front door (every service's URL, shown above), streams every service's logs with a[service]prefix, and watches for rebuilds. A rebuild triggers re-assemble and re-converge, and the emulator restarts exactly the services whose deployment actually changed. Ctrl-C stops the app's services and exits; the daemons and all data persist, which is why the second run above is warm.Watching is driven by the adapter-declared
watchpaths from #158. Until that merges, every bundle takes the graceful path (a printed "no watchable inputs" note); the loop itself is live and hardened by a bug found while proving it — a single-file watch dies silently when a rebuild deletes and recreates its file, so file targets watch their parent directory instead.Two things worth a reviewer's attention. First, a documented hack: merely importing Alchemy's library code registers exit-on-signal handlers in the CLI process, and whichever fired first killed the CLI before cleanup could run — observed live as exit 130 with every service left running. The dev command strips those handlers and becomes the sole signal listener, at one pinned moment, after which no other listener legitimately exists. Recorded limitation: a second Ctrl-C won't force-quit a hung stop. Second, a real defect fixed here: rebuilding one service used to restart its dependents too, because each service's environment was assembled from an app-wide snapshot that only "completed" on the second converge. Environments are now scoped to each service's own configuration rows. That matches how the platform itself decides restarts, and it's recorded in the design docs as a deliberate, reasoned deviation.
This PR also makes the emulators publish-safe: the daemon programs ship inside the public package's own dist and are resolved against it, so a real npm install no longer references a private workspace package.
Tested
Every behavior below is a required CI gate driving the real CLI binary as a child process against
examples/store:PRISMA_*variables set; HTTP round-trip through the front-door URL.--freshremoves it..prisma-composer/dev/buckets/; a file dropped there reads back through the app.Plus unit suites for the watch debounce, front-door ordering, and the generated stack file's exact source. Manual terminal session transcribed above. Workspace gates green; process-leak audit clean.
Alternatives considered
🤖 Generated with Claude Code