From d37d2419d6e7a9d1ce7f3112513ba24f0b22f971 Mon Sep 17 00:00:00 2001 From: Mika Kuns Date: Tue, 19 May 2026 14:09:21 +0200 Subject: [PATCH] feat(cli): pre-install port check in install-autostart Before registering the Scheduled Task / Run-key / launchd / systemd unit, probe /health on the resolved port. If a non-claude-mailbox service answers, refuse with a helpful hint (`--port ` or mailbox.json) so users don't end up with autostart firing against an occupied port. Pass --skip-port-check to bypass. The doctor already had this logic in Step 2; now standalone install-autostart invocations are protected too. --- node/src/cli.ts | 48 +++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 43 insertions(+), 5 deletions(-) diff --git a/node/src/cli.ts b/node/src/cli.ts index ae6d854..b7d0bbf 100644 --- a/node/src/cli.ts +++ b/node/src/cli.ts @@ -345,11 +345,49 @@ program .option("--port ", "Port to listen on", (v) => parseInt(v, 10)) .option("--bind
", "Bind address") .option("--db-path ", "SQLite database path") - .action(async (opts: { service?: boolean; port?: number; bind?: string; dbPath?: string }) => { - const mgr = await autostartManager(opts.service ? "service" : "default"); - await mgr.install({ port: opts.port, bind: opts.bind, dbPath: opts.dbPath }); - console.log("Autostart installed."); - }); + .option( + "--skip-port-check", + "Skip the pre-install probe for a foreign occupant on the daemon's port", + ) + .action( + async (opts: { + service?: boolean; + port?: number; + bind?: string; + dbPath?: string; + skipPortCheck?: boolean; + }) => { + if (!opts.skipPortCheck) { + const cfg = resolveConfig({ port: opts.port, bind: opts.bind, dbPath: opts.dbPath }); + const probeUrl = `http://${cfg.bind}:${cfg.port}/health`; + try { + const res = await fetch(probeUrl, { headers: { Accept: "application/json" } }); + const text = await res.text(); + let parsed: { status?: string; version?: string } | null = null; + try { + parsed = text.length ? (JSON.parse(text) as { status?: string; version?: string }) : null; + } catch { + parsed = null; + } + if (res.ok && parsed?.status === "ok" && parsed.version) { + console.log( + `Port ${cfg.port} already serves a claude-mailbox daemon (version ${parsed.version}). Autostart will manage that one.`, + ); + } else { + console.error( + `Port ${cfg.port} is held by a non-claude-mailbox service (status ${res.status}). Pick a free port via \`--port \` or write {"port": } to ~/.claude-mailbox/mailbox.json. Use --skip-port-check to bypass.`, + ); + process.exit(4); + } + } catch { + // Connection refused or similar — port is free, proceed. + } + } + const mgr = await autostartManager(opts.service ? "service" : "default"); + await mgr.install({ port: opts.port, bind: opts.bind, dbPath: opts.dbPath }); + console.log("Autostart installed."); + }, + ); program .command("uninstall-autostart")