(flags?: GlobalFlags)
| 402 | } |
| 403 | |
| 404 | async function ensureServer(flags?: GlobalFlags): Promise<ServerState> { |
| 405 | const state = readState(); |
| 406 | const desiredHash = flags?.configHash; |
| 407 | const extraEnv: Record<string, string> = {}; |
| 408 | if (flags?.proxyUrl) extraEnv.BROWSE_PROXY_URL = flags.proxyUrl; |
| 409 | if (flags?.headed) extraEnv.BROWSE_HEADED = '1'; |
| 410 | if (desiredHash) extraEnv.BROWSE_CONFIG_HASH = desiredHash; |
| 411 | |
| 412 | // Health-check-first: HTTP is definitive proof the server is alive and responsive. |
| 413 | // This replaces the PID-gated approach which breaks on Windows (Bun's process.kill |
| 414 | // always throws ESRCH for Windows PIDs in compiled binaries). |
| 415 | if (state && await isServerHealthy(state.port)) { |
| 416 | // D2 daemon-mismatch check: existing daemon's configHash must match the |
| 417 | // CLI's resolved hash. If --proxy or --headed are passed and the existing |
| 418 | // daemon was started with different config, refuse with a `disconnect` |
| 419 | // hint. No silent restart — that would drop tab state, cookies, and |
| 420 | // logged-in sessions without warning. |
| 421 | if (desiredHash && state.configHash && state.configHash !== desiredHash) { |
| 422 | console.error(`[browse] existing daemon has different config (proxy/headed mismatch).`); |
| 423 | console.error(`[browse] run 'browse disconnect' first to apply --proxy/--headed.`); |
| 424 | process.exit(1); |
| 425 | } |
| 426 | // Same path: existing daemon is plain (no flags) but caller passes |
| 427 | // --proxy/--headed. Refuse for the same reason — apply explicitly via |
| 428 | // disconnect+reconnect. |
| 429 | if (desiredHash && !state.configHash && (flags?.proxyUrl || flags?.headed)) { |
| 430 | console.error(`[browse] existing daemon was started without --proxy/--headed.`); |
| 431 | console.error(`[browse] run 'browse disconnect' first to apply new flags.`); |
| 432 | process.exit(1); |
| 433 | } |
| 434 | |
| 435 | // Check for binary version mismatch (auto-restart on update) |
| 436 | const currentVersion = readVersionHash(); |
| 437 | if (currentVersion && state.binaryVersion && currentVersion !== state.binaryVersion) { |
| 438 | console.error('[browse] Binary updated, restarting server...'); |
| 439 | await killServer(state.pid); |
| 440 | return startServer(extraEnv); |
| 441 | } |
| 442 | return state; |
| 443 | } |
| 444 | |
| 445 | // BROWSE_NO_AUTOSTART: sidebar agent sets this so the child claude never |
| 446 | // spawns an invisible headless browser. If the headed server is down, |
| 447 | // fail fast with a clear error instead of silently starting a new one. |
| 448 | if (process.env.BROWSE_NO_AUTOSTART === '1') { |
| 449 | console.error('[browse] Server not available and BROWSE_NO_AUTOSTART is set.'); |
| 450 | console.error('[browse] The headed browser may have been closed. Run /open-gstack-browser to restart.'); |
| 451 | process.exit(1); |
| 452 | } |
| 453 | |
| 454 | // Guard: never silently replace a headed server with a headless one. |
| 455 | // Headed mode means a user-visible Chrome window is (or was) controlled. |
| 456 | // Silently replacing it would be confusing — tell the user to reconnect. |
| 457 | if (state && state.mode === 'headed' && isProcessAlive(state.pid)) { |
| 458 | console.error(`[browse] Headed server running (PID ${state.pid}) but not responding.`); |
| 459 | console.error(`[browse] Run '/open-gstack-browser' to restart.`); |
| 460 | process.exit(1); |
| 461 | } |
no test coverage detected