Skip to content

fix(editor): use a literal dev port so bun dev works on Windows - #551

Open
evolv3ai wants to merge 3 commits into
pascalorg:mainfrom
evolv3ai:fix/windows-dev-port-expansion
Open

fix(editor): use a literal dev port so bun dev works on Windows#551
evolv3ai wants to merge 3 commits into
pascalorg:mainfrom
evolv3ai:fix/windows-dev-port-expansion

Conversation

@evolv3ai

@evolv3ai evolv3ai commented Jul 27, 2026

Copy link
Copy Markdown

Problem

bun dev fails immediately on Windows. The editor dev task dies before Next.js starts:

editor:dev: $ dotenv -e ../../.env.local -- next dev --port ${PORT:-3002}
editor:dev: error: option '-p, --port <port>' argument '${PORT:-3002}' is invalid. '${PORT:-3002}' is not a non-negative number.
editor:dev: error: script "dev" exited with code 1

Cause

On Windows, bun run executes 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} in apps/editor/package.json reaches Next.js verbatim and is rejected.

Setting PORT in 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:

-"dev": "dotenv -e ../../.env.local -- next dev --port ${PORT:-3002}"
+"dev": "dotenv -e ../../.env.local -- next dev --port 3002"

Verified on Windows 11 / Bun 1.3.6 / Node 24.18.0 — bun dev now boots the whole workspace and the editor serves HTTP 200 on http://localhost:3002.

Tradeoff, and an alternative if you'd prefer it

This drops the PORT override 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 preserve PORT on every platform:

// apps/editor/scripts/dev.mjs
import { spawn } from "node:child_process";
const port = process.env.PORT ?? "3002";
spawn("next", ["dev", "--port", port], { stdio: "inherit", shell: true });

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:

  1. The root dev script is also a no-op on Windows. It begins with set -a && . ./.env 2>/dev/null; set +a; turbo run dev --env-mode=loose. Bun's shell has no set builtin, so it prints bun: command not found: set twice and silently skips loading .env. Turbo still runs, so it's non-fatal, but any variables in a root .env are never loaded on Windows.

  2. .env.example and SETUP.md disagree on the default port. .env.example says # 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 with content_clear_rejected unless the body includes allowContentClear: true. Logic lives in new scene-content-guard helpers with unit tests.

Routing: Next.js rewrites /editor/:id/scene/:id so MCP/store editorUrl links stop 404ing without changing the browser path (client code still expects /editor/...).

Dev: apps/editor dev script 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.

evolv3ai and others added 3 commits July 27, 2026 12:30
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>

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.

Fix All in Cursor

❌ 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}"` } },
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit d90667b. Configure here.

version: existing.version,
},
{ status: 409, headers: { ETag: `"${existing.version}"` } },
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit d90667b. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant