feat(cli): pre-install port check in install-autostart
All checks were successful
CI (Node) / build-test (push) Successful in 9s
Release / release (push) Successful in 8s
Release (Node) / release (push) Successful in 12s

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 <n>` 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.
This commit is contained in:
Mika Kuns
2026-05-19 14:09:21 +02:00
parent ee0b72f43b
commit d37d2419d6

View File

@@ -345,11 +345,49 @@ program
.option("--port <port>", "Port to listen on", (v) => parseInt(v, 10))
.option("--bind <address>", "Bind address")
.option("--db-path <path>", "SQLite database path")
.action(async (opts: { service?: boolean; port?: number; bind?: string; dbPath?: string }) => {
.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 <n>\` or write {"port": <n>} 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")