Report the gateway's real state at the end of setup - #11
Conversation
The autostart step's background path called startDaemon(), which logs "Gateway already running (pid N). Use `stop` or `restart`." and returns 1 when a PID is live. On a rerun the wizard still exited 0 while the gateway kept serving the .env it had just replaced -- a rotated signing key or a new identity silently did not take effect. The boot-autostart path already handles this: it always issues a service restart rather than `enable --now` for exactly this reason. Give the background path the same property by restarting when a PID is live, and say so rather than leaving the operator to read it out of a log line. runningDaemonPid() exposes the paths-plus-liveness probe so the wizard can ask without reassembling it, and both lifecycle calls are injectable so tests never touch a real process. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Setup always signed off with "Setup complete / check it with doctor", whether or not anything was actually running, and never named the identity the agent had just been wired to. configureAutostart now reports whether a gateway ended up running, by reading the exit codes startDaemon/restartDaemon already return. When one is live, close on a banner naming the identity and the health command. When nothing is listening, keep the sign-off and add the start command that was missing from it. The scripted-answers helper that walks a test to the autostart step moves to module scope so the new cases can reach it too. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Pushed Setup always signed off the same way whether or not anything was running, and never named the identity the agent had just been wired to. When nothing is listening the old sign-off stays — plus 4 more tests driving the wizard end to end: banner content, box geometry, the to-do fallback, and a daemon that exits 1 (which must not print the banner). The scripted-answers helper that walks a test to the autostart step moved to module scope so the new cases can reach it. 657 tests, |
startDaemon returns 0 the moment it has spawned the child -- it never checks that the gateway survived. A bad bind kills it a second or two later, well after that return, so setup printed "started" and then the status banner over a process that was already gone. Poll for liveness across a short window before claiming it, and point at the log when the pid disappears. Both the window and the delay are injectable so tests neither wait nor spin. The sibling Python bridges needed a larger fix here (they forked the caller, which this one never did -- it already spawns a fresh interpreter the way the service units do). This is the one piece that ports. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Pushed What ported: writePidFile(pidFile, child.pid);
console.log(`Gateway started (pid ${child.pid}). Logs: ${logFile}`);
return 0;A bad bind kills it a second or two later, well after that return — so setup printed "started" and then the status banner over a process that was already gone. This bridge was actually more exposed than the Python two, which at least slept 1.5s and re-read the pid. Now polls for liveness across a 5s window before claiming it, and points at the log when the pid disappears. Window and delay are both injectable so tests neither wait nor spin. What did not port, and why:
That last row is worth noting: this bridge had the right daemon shape all along. The Python ones were forking a wizard that had already driven the SDK, which handed the child a half-initialised copy of httpx's pools — dying before logging was even configured. Separate, fleet-wide, not fixed here: all three bridges default to port 8767 ( 2 new tests: a gateway that dies right after starting must not print the banner, and the confirm must poll rather than probe once. 659 tests, |
Summary
Setup could finish with a green sign-off over a gateway that was dead or still running the previous
.env. Three commits, all found by running the sibling bridges on a fresh macOS install.1. A rerun left the gateway on the old config
configureAutostart()'s background path calledstartDaemon(), which refuses on a live PID:The wizard ignored that return code, so setup still exited 0 while the gateway kept serving the
.envit had just replaced — a rotated signing key or switched identity silently did not take effect. The only hint was aconsole.errorline the operator had to notice and act on.The boot-autostart path already got this right, and its comment names the failure: "
enable --nowis a no-op on an already-running service, which would keep a stale gateway on old credentials — so enable, then always restart." The background path now matches, and says what it did:No second prompt — answering yes to "start it" when one is running can only mean "make it running with the config I just wrote".
runningDaemonPid()exposes the paths-plus-liveness probe so the wizard does not reassembledaemonPaths+readPidFile+pidAliveitself. Both lifecycle calls go through injectableCtxdeps, matchingstartDaemonFn/installAutostartFn.2. The sign-off read the same whether or not anything was running
Setup complete. / Check it anytime with: inkbox-opencode doctorprinted either way, and never named the identity. So the one case where you needed a start command never mentioned one.configureAutostart()now reports liveness — read off exit codesstartDaemon/restartDaemonwere already returning and discarding. Live:Not live: the old sign-off, plus the
inkbox-opencode startline that was missing.3. "Started" was claimed before the gateway had survived
startDaemonreturns 0 the moment it has spawned the child, and never checks it is still there:A bad bind kills the gateway a second or two later — well after that return — so setup printed "started" and then the banner over a process that was already gone. This bridge was in fact more exposed than the Python siblings, which at least slept 1.5s and re-read the pid.
Now polls for liveness across a 5s window before claiming it, and points at the log when the pid disappears:
Window and delay are both injectable so tests neither wait nor spin.
What this bridge already got right
Worth recording, since the sibling PRs carry fixes for all of these:
.envpath — the wizard writes topath.join(gatewayHome(env), ".env"), never cwd. The Python bridges fell back to cwd while their daemons read the state dir, so running setup from a home directory put an API key in~/.env.doctor— already loads env files viaenvFileCandidates/readEnvFile. The Python ones calledread_config()directly and reported every credential missing while the gateway ran fine on them.spawn(process.execPath, [entry, "run"]). The Python bridges forked a wizard that had already driven the SDK, handing the child a half-initialised copy of httpx's connection pools; it could die before logging was configured. Their fix was to adopt exactly the shape this bridge already had.Separate, not fixed here
All three bridges default to port 8767 (
src/config.ts:305and the equivalents). Running any two on one machine collides on the bind. Picking distinct defaults is a coordinated change across three repos rather than something to slip into this PR.Notes
659 tests pass,
tsc --noEmitclean.Siblings: inkbox-ai/claude-code-plugin#47, inkbox-ai/codex-plugin#29.