fix(editor): use a literal dev port so bun dev works on Windows - #551
fix(editor): use a literal dev port so bun dev works on Windows#551evolv3ai wants to merge 3 commits into
bun dev works on Windows#551Conversation
On Windows, `bun run` executes package scripts with Bun's own built-in
shell rather than a POSIX shell. That shell does not implement
default-value parameter expansion, so `${PORT:-3002}` is passed through
to Next.js verbatim and the dev server exits immediately:
error: option '-p, --port <port>' argument '${PORT:-3002}' is invalid.
'${PORT:-3002}' is not a non-negative number.
Setting PORT in the environment does not help — the literal is never
expanded either way.
Replace the expansion with the documented default port (3002).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The scene store hands out `/editor/<id>` as a project's canonical URL (`editorUrlForScene` in packages/mcp/src/storage/sqlite-scene-store.ts) and `/api/scenes` reports it verbatim, but this app only routes `/scene/[id]`. Every MCP-reported `editorUrl` therefore 404s. Rewrite `/editor/:id` to `/scene/:id` rather than redirect: client code parses the project id back out of the browser path (scan upload in packages/editor/src/components/ui/action-menu/view-toggles.tsx), so the `/editor/` prefix has to survive the hop. `/scene/<id>` keeps working for the app's own links, and the MCP-side tests asserting `/editor/<id>` stay valid, so no test or storage changes are needed. Also pass `--hostname 0.0.0.0` explicitly to `next dev`. Next already binds every interface by default, so this pins that behaviour rather than changing it, and surfaces the Network URL in dev output. Reaching the dev server from another device still requires an inbound firewall rule for port 3002. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The editor autosaves on a debounce. When that timer fires before the initial scene load resolves, the client PUTs the empty graph it started from -- or the bare site/building/level scaffold it falls back to. The stored version is still the one the client read, so `expectedVersion` matches, optimistic concurrency waves the write through, and the stored scene is destroyed. Observed against a 49-node scene: six consecutive writes took it to 0, 3, 0, 3, 0, then 4 nodes. `PUT /api/scenes/[id]` now rejects a write that would remove the last authored node from a scene that had one, with 409 and the current ETag, so the existing client conflict path resyncs instead of clobbering. Callers that mean it pass `allowContentClear: true`. The check counts authored nodes rather than raw node count: site, building, and level are the scaffold a fresh session starts from, so a scaffold-only write is a wipe too and a naive emptiness test would have let three of the six through. This addresses the server side only. The client-side race in components/scene-loader.tsx -- autosave arming before the initial graph lands -- is the underlying cause and is still open; the guard is what prevents it reaching the store. Note `expectedVersion: expectedVersion ?? existing.version` on the same handler: a caller that omits `If-Match` gets the current version as its own precondition, which always matches. The guard covers that path too, but the fallback remains a weak point worth revisiting. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Want reviews to match your repository better? Bugbot Learning can learn team-specific rules from PR activity. A team admin can enable Learning in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit d90667b. Configure here.
| version: existing.version, | ||
| }, | ||
| { status: 409, headers: { ETag: `"${existing.version}"` } }, | ||
| ) |
There was a problem hiding this comment.
Clear rejection shown as conflict
Medium Severity
The new content_clear_rejected error returns HTTP 409, a status code the editor client already uses exclusively for version conflicts. This causes the client to display a misleading "another session saved first" message, obscuring the specific guidance for a blocked content clear.
Reviewed by Cursor Bugbot for commit d90667b. Configure here.
| version: existing.version, | ||
| }, | ||
| { status: 409, headers: { ETag: `"${existing.version}"` } }, | ||
| ) |
There was a problem hiding this comment.
Rejected clear still enables wipe
High Severity
The new content-clear guard only blocks a PUT whose incoming graph has zero authored nodes. On content_clear_rejected, scene-loader returns without throwing, so autosave marks the run saved and leaves the emptied in-memory graph in place. Any later edit that adds even one content node makes wouldClearSceneContent false, so the next PUT can still replace the full stored scene with that near-empty graph.
Reviewed by Cursor Bugbot for commit d90667b. Configure here.
|
Thanks — there are three real findings in here, and I verified all three. But they're three separate changes, and one of them is fixed at the wrong layer, so I'd like to split this rather than merge it as-is. Verified true:
But the guard is at the wrong layer, and I found the actual root cause.
That matters for where the fix goes. Fixing the hook protects the hosted app and every consumer of What I'd like instead — three PRs: A. The port fix (please keep this one, it's the one your title describes). Rather than hardcoding, Next's CLI already reads Also please drop B. The MCP URL mismatch — worth its own PR, and I'd rather fix it in C. The autosave guard — in One process note: the title and body still describe a one-line dev-port fix, but the head is 5 files and +153/-1 across three commits with changed persistence semantics. That's the kind of drift that makes a PR hard to review — please update the body when scope grows. Also, the new test file lives in Happy to take A on its own immediately if you push just that. |
…#578) `useAutoSave` refuses to persist a graph that drops from populated to a bare scaffold, on the assumption that it is an accidental full deletion. The baseline it compares against was seeded once when the hook mounted — which happens before the scene has loaded, so it sat at the scaffold count for the whole session and the guard could never fire. The one write it exists to stop is the one it let through: an autosave racing the initial load overwrites the stored scene with the scaffold. The loading branch of the store subscription already refreshed the snapshot, collections, materials and plugin refs; it just never refreshed the count. Rather than add a fourth assignment to a branch whose contract was implicit, the baseline now lives in `createStoredNodeCountTracker`, which distinguishes the two things that were being conflated: a graph read from storage becomes the new baseline, an edited graph does not. That also removes the duplicated guard between `executeSave` and `flushOnExit`, and makes the invariant testable without React — the same approach `floorplan-camera-sync.ts` takes for its closure state. Surfaced by @evolv3ai in #551, which fixed the symptom with a `nodeCount === 0` check in the standalone app's save route. This fixes it in the shared hook instead, so the hosted editor and npm consumers are covered too, and a blocked write can't be laundered through the 409 conflict path that `scene-loader.tsx` treats as success. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Aymericr
left a comment
There was a problem hiding this comment.
The Windows problem is real and worth fixing — thank you for finding it. ${PORT:-3002} is POSIX parameter expansion, apps/editor/package.json:7 is the only place in the repo that uses it, and under cmd.exe/PowerShell it reaches Next as a literal string. So the diagnosis in your title is correct.
I can't merge this as it stands, for two reasons — one about the fix itself, one about scope.
1. The fix trades one bug for another. Next's CLI already binds PORT to --port:
.option('--port <port>', …).argParser(parseValidPositiveInteger).default(3000).env('PORT')
Commander's .env() is only consulted when the flag is absent, so hardcoding --port 3002 makes PORT=4000 bun dev silently ignore PORT — which is how our own community app selects its port. The one-line fix that works on both platforms is to drop the flag and let Next read the env var:
-"dev": "dotenv -e ../../.env.local -- next dev --port ${PORT:-3002}",
+"dev": "dotenv -e ../../.env.local -- next dev",That needs PORT=3002 in .env.local/.env.example to preserve today's default, since Next's own default is 3000. Happy with any variant that keeps PORT working — cross-env would also do it.
Also worth knowing: this doesn't yet make root bun dev work on Windows. The root script is set -a && . ./.env 2>/dev/null; set +a; turbo run dev --env-mode=loose (package.json:6), which is POSIX-only too. Your change fixes bun dev inside apps/editor. Fixing the root script is a genuinely useful follow-up if you want it — the .env loading there could move to dotenv -e ./.env and become portable.
2. The PR does four things and the title names one. The diff is 153 lines across 5 files:
apps/editor/package.json— the port fix (1 line), plus an undisclosed--hostname 0.0.0.0apps/editor/lib/scene-content-guard.ts+ test — a new 42-line scene-persistence guardapps/editor/app/api/scenes/[id]/route.ts— 31 lines wiring that guard into the save pathapps/editor/next.config.ts— an/editor/<id>→/scene/<id>rewrite
--hostname 0.0.0.0 binds the dev server to every interface, which on a laptop on untrusted Wi-Fi exposes it to the local network. That may well be what you want in your setup, but it's a security-relevant default that needs to be its own change with its own rationale, not a rider on a port fix.
The /editor rewrite also collides with #570, which adds an /editor redirect to the same next.config.ts. Two PRs solving the same routing mismatch in one file will conflict, and I'd rather settle which behaviour is right — you rewrite (preserving the /editor/ prefix because client code parses the id out of it), #570 redirects. Your rationale for rewriting is the more careful one, so this shouldn't be lost; it just needs to be decided in one place.
And the persistence guard is the piece I most want reviewed on its own merits: it changes what the save endpoint accepts, which is the path where a bug means data loss. It deserves a description, and reviewers looking at it deliberately — it will get neither while it's inside a PR titled "use a literal dev port".
What I'd suggest, and I'll review each quickly:
- This PR → the one-line port fix only (using the
PORT-preserving form above). I'll merge it as soon as it's trimmed. - A second PR →
--hostname 0.0.0.0, with the use case. Likely answer is to make it opt-in viaHOSTNAMErather than the default. - A third PR → the scene-content guard, with the failure it prevents.
- The
/editorrouting → comment on #570 with your rewrite-vs-redirect argument and we'll land whichever is right there.
If splitting is more work than you want, tell me and I'll cherry-pick the port line myself with credit to you — but I'd genuinely rather have items 2 and 3 as reviewable changes than lose them.
|
Two updates on my review above, one of them a correction to me. Finding B was my mistake — there is no 404. I said // MCP / package metadata returns `/editor/<id>` (hosted route). This open-source
// app serves saved scenes at `/scene/<id>` — redirect so links and bookmarks work.
async redirects() {
return [{ source: '/editor/:id', destination: '/scene/:id', permanent: false }]
}And Finding C is now fixed on /** A graph read from storage — it defines what "populated" means from here. */
trackLoadedGraph(nodeCount: number) {
count = nodeCount
},The loading branch of the subscription now calls it alongside the other refs, so the guard is armed from the first save of the session instead of comparing against a stale zero. That's in That leaves A, the fix your title actually describes. It's still open and I'd still merge it on its own. To restate concretely: "dev": "dotenv -e ../../.env.local -e ../../.env -- next dev"with Push just that and I'll merge it — no need to touch the other two. |
…ride (#587) Two package scripts use POSIX shell syntax that Bun's own shell does not implement, and Bun uses that shell for `bun run` on Windows: - `apps/editor`'s dev script passes `--port ${PORT:-3002}`, which arrives at Next verbatim: `option '-p, --port <port>' argument '${PORT:-3002}' is invalid`. Setting PORT does not help — the literal is never expanded. - the root dev script starts `set -a && . ./.env`, and `set` is not a Bun shell builtin, so it prints `bun: command not found: set` and silently skips loading `.env` entirely. Reproduced both on macOS with `bun run --shell=bun`, which selects the same shell Windows gets: $ echo port=${PORT:-3002} port=${PORT:-3002} # even with PORT=9999 in the environment $ set -a && echo set-worked bun: command not found: set Hardcoding the port would fix Windows but drop the PORT override that SETUP.md and .env.example both document. Instead, load a committed `.env.defaults` last and let `next dev` read PORT from the environment (the CLI already declares `.env('PORT')` on `-p, --port`). Nothing is shell-expanded, so it behaves the same on every platform, and the precedence stays shell PORT > .env.local > .env.defaults — verified at 3002 by default and 4321 with an override, under both shells. `.env.defaults` is needed because `.env` and `.env.local` are gitignored, so a checked-in default has nowhere else to live. Also corrects `.env.example`, which advertised a 3000 default the repo has not used since the port moved to 3002. Reported by @evolv3ai in #551, including the Windows console output and the `set` finding. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
|
All three findings are now resolved, so I'm closing this — but the Windows bug you found is fixed on A (the Windows dev-server break) — fixed in #587, credited to you. I reproduced both of your findings on macOS using Exactly your diagnosis, including the root-script One correction to my earlier review while I'm here: I claimed Bun doesn't expand B (the MCP C (the autosave data loss) — fixed in #578, at the hook layer in Closing since there's nothing left in this branch to merge. Thanks for the report — a bug that makes the repo un-runnable on a whole platform is high-value, and the console output plus the |


Problem
bun devfails immediately on Windows. Theeditordev task dies before Next.js starts:Cause
On Windows,
bun runexecutes package scripts with Bun's own built-in shell rather than handing them to a POSIX shell. That shell does not implement default-value parameter expansion, so${PORT:-3002}inapps/editor/package.jsonreaches Next.js verbatim and is rejected.Setting
PORTin the environment first does not work around it — the literal is never expanded either way. There is no shell-level workaround from the caller's side; the script itself has to avoid the syntax.Fix
Use the documented default port directly:
Verified on Windows 11 / Bun 1.3.6 / Node 24.18.0 —
bun devnow boots the whole workspace and the editor servesHTTP 200on http://localhost:3002.Tradeoff, and an alternative if you'd prefer it
This drops the
PORToverride that SETUP.md documents. I kept the change to one line because that's the smallest thing that unblocks Windows, but I'm happy to switch to a small cross-platform launcher instead, which would preservePORTon every platform:Just say which you'd rather have and I'll update the PR.
Two unrelated things I noticed while debugging
Not touched in this PR, but worth flagging:
The root
devscript is also a no-op on Windows. It begins withset -a && . ./.env 2>/dev/null; set +a; turbo run dev --env-mode=loose. Bun's shell has nosetbuiltin, so it printsbun: command not found: settwice and silently skips loading.env. Turbo still runs, so it's non-fatal, but any variables in a root.envare never loaded on Windows..env.exampleandSETUP.mddisagree on the default port..env.examplesays# Dev server port (default: 3000)/# PORT=3000, while SETUP.md and the dev script both use 3002.Note
Medium Risk
The content-clear guard changes persistence semantics for scene PUTs (intentional clears need an explicit flag); routing and dev script changes are lower risk.
Overview
Scene save protection: PUT
/api/scenes/[id]now blocks writes that would remove every authored node (walls, slabs, zones, etc.) while leaving only the default site/building/level scaffold or an empty graph—matching the race where debounced autosave runs before the initial load. Those saves return 409 withcontent_clear_rejectedunless the body includesallowContentClear: true. Logic lives in newscene-content-guardhelpers with unit tests.Routing: Next.js rewrites
/editor/:id→/scene/:idso MCP/storeeditorUrllinks stop 404ing without changing the browser path (client code still expects/editor/...).Dev:
apps/editordevscript uses a fixed--port 3002(no${PORT:-3002}, which breaks under Bun on Windows) and adds--hostname 0.0.0.0.Reviewed by Cursor Bugbot for commit d90667b. Bugbot is set up for automated code reviews on this repo. Configure here.