feat(design): compiled binary self-execs as daemon; unified version lookup

Two small but production-critical fixes once the binary actually runs:

1. Compiled binary couldn't spawn the daemon. daemon-client previously
   pointed at design/src/daemon.ts via import.meta.dir — fine in dev,
   fatal in production (the source path doesn't exist on a user's
   machine). Fix: design CLI now self-execs in --daemon-mode when
   invoked with that flag, so the spawn is `process.execPath
   --daemon-mode --marker gstack-design-daemon` for the compiled binary
   and `bun run cli.ts --daemon-mode ...` in dev. Same one binary, two
   modes, no separate daemon entrypoint to ship.

2. Client and daemon disagreed on VERSION in the compiled binary.
   Both used a source-tree-relative path that resolves to "unknown"
   at runtime, which silently shorted the version-mismatch refusal
   path (client expected "unknown" + daemon reported "unknown" → match
   → no refusal even when DESIGN_DAEMON_VERSION was set on one side).
   New readVersionString() consults DESIGN_DAEMON_VERSION env first,
   then design/dist/.version (sidecar baked at build time by build.sh),
   then VERSION at the source-tree root. Both client and daemon now go
   through this one helper.

Manual smoke (compiled binary, all checks green):
  - DAEMON_STARTED + BOARD_PUBLISHED with trailing slash
  - GET /boards/<id> (no slash) → 301 Location /boards/<id>/
  - Second `$D serve` invocation → DAEMON_ATTACHED, new board on same port
  - feedback.json gets boardId + publishedAt fields
  - DESIGN_DAEMON_VERSION=v2-different on second invocation with
    active board → WARNING + "Refusing to auto-kill" + exit 1,
    original daemon still alive
  - `$D daemon stop --force` removes state file

All 67 design tests still green after the refactor (16 serve + 30
daemon + 17 discovery + 4 daemon round-trip).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-05-25 15:29:59 -07:00
parent e7c49bcd05
commit 77b5ad2fa1
4 changed files with 88 additions and 31 deletions
+31
View File
@@ -55,6 +55,37 @@ export function resolveStartupLogPath(): string {
return path.join(os.homedir(), ".gstack", "design-daemon-startup.log");
}
/**
* Read the gstack version both client and daemon should agree on. Looks
* (in order): DESIGN_DAEMON_VERSION env, design/dist/.version baked at
* build time, VERSION at the source-tree root (dev), then "unknown".
*
* Compiled binaries lose the source-tree relative path at runtime, so we
* try the dist/.version sidecar (which build.sh writes) before falling
* back. This keeps client.expectedVersion and daemon.VERSION coherent.
*/
export function readVersionString(): string {
const env = process.env.DESIGN_DAEMON_VERSION;
if (env) return env;
const candidates = [
// Compiled binary: design/dist/design lives alongside design/dist/.version
path.join(path.dirname(process.execPath), ".version"),
// Dev: design/src/* → repo root is two levels up
path.join(import.meta.dir, "..", "..", "VERSION"),
// Defensive: design/dist sibling of source tree
path.join(import.meta.dir, "..", "dist", ".version"),
];
for (const p of candidates) {
try {
const v = fs.readFileSync(p, "utf-8").trim();
if (v) return v;
} catch {
// try next
}
}
return "unknown";
}
export function readStateFile(stateFile: string = resolveStateFilePath()): DaemonState | null {
try {
return JSON.parse(fs.readFileSync(stateFile, "utf-8")) as DaemonState;