feat(install): auto-start the dashboard on boot via systemd, no Claude Code needed
Adds ccam service install/uninstall/status for checkout installs and wires the same systemd --user unit into plugin-bootstrap.js, so plugin-installed colleagues get boot persistence too instead of needing a Claude Code session to (re)start the server after every reboot.
This commit is contained in:
+91
-1
@@ -9,7 +9,7 @@
|
||||
* directly from any shell as `ccam <command>`, or interactively at the
|
||||
* `ccam ›` prompt via `ccam repl`.
|
||||
*
|
||||
* Server status · start · repl (interactive shell)
|
||||
* Server status · start · service (install/uninstall/status) · repl
|
||||
* Monitoring health · stats · kanban · tail
|
||||
* Data browsing sessions · session <id> · agents · events
|
||||
* Insights analytics · workflows · runs · cost
|
||||
@@ -1433,6 +1433,88 @@ async function cmdStart(flags) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Installs/removes a systemd `--user` service so the dashboard survives
|
||||
* reboots without anything (Claude Code included) needing to start it first.
|
||||
* Linux/systemd only; other platforms are told to keep using `ccam start`.
|
||||
*/
|
||||
async function cmdService(sub, flags) {
|
||||
const svc = require(path.join(REPO_ROOT, "scripts", "systemd-service.js"));
|
||||
if (!svc.systemctlAvailable()) {
|
||||
console.error(c.red("✖ systemd --user is not available on this machine."));
|
||||
console.error(
|
||||
c.dim(" Auto-start on boot needs Linux + systemd. Use `ccam start` manually instead.")
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
const name = "ccam";
|
||||
if (sub === "install") {
|
||||
const clientDist = path.join(REPO_ROOT, "client", "dist", "index.html");
|
||||
if (!fs.existsSync(clientDist)) {
|
||||
console.error(c.red("✖ client/dist is missing — build it first: npm run build"));
|
||||
process.exit(1);
|
||||
}
|
||||
const env = { NODE_ENV: "production" };
|
||||
if (flags.port) env.DASHBOARD_PORT = String(flags.port);
|
||||
const result = svc.installUserService({
|
||||
name,
|
||||
description: "CCAM dashboard server",
|
||||
workingDir: REPO_ROOT,
|
||||
execStart: `${process.execPath} ${path.join(REPO_ROOT, "server", "index.js")}`,
|
||||
env,
|
||||
});
|
||||
if (result === "foreign") {
|
||||
console.error(
|
||||
c.red(`✖ ~/.config/systemd/user/${name}.service already exists and wasn't written by ccam.`)
|
||||
);
|
||||
console.error(c.dim(" Remove or rename it, then re-run `ccam service install`."));
|
||||
process.exit(1);
|
||||
}
|
||||
if (result === "failed") {
|
||||
console.error(c.red("✖ Failed to write/enable the systemd unit."));
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(
|
||||
`${c.green("✔")} systemd --user service '${name}' ${result === "unchanged" ? "already up to date" : "installed"} and enabled.`
|
||||
);
|
||||
const lingered = svc.enableLinger();
|
||||
if (lingered) {
|
||||
console.log(
|
||||
c.dim(" Linger enabled — the dashboard will start even on a cold boot with no login.")
|
||||
);
|
||||
} else {
|
||||
console.log(
|
||||
c.dim(" Could not enable linger automatically. For it to survive a reboot with nobody")
|
||||
);
|
||||
console.log(
|
||||
c.dim(
|
||||
` logged in yet, run: sudo loginctl enable-linger ${require("os").userInfo().username}`
|
||||
)
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (sub === "uninstall") {
|
||||
const result = svc.uninstallUserService(name);
|
||||
if (result === "foreign") {
|
||||
console.error(
|
||||
c.red(`✖ ~/.config/systemd/user/${name}.service wasn't written by ccam — not touching it.`)
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(
|
||||
`${c.green("✔")} systemd service ${result === "absent" ? "was not installed" : "removed"}.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (sub === "status") {
|
||||
spawn("systemctl", ["--user", "status", `${name}.service`, "--no-pager"], { stdio: "inherit" });
|
||||
return;
|
||||
}
|
||||
console.error(c.red("✖ Usage: ccam service install|uninstall|status [--port N]"));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
/** Up/down indicator without exiting non-zero noise — the at-a-glance check. */
|
||||
async function cmdStatus() {
|
||||
if (await serverIsUp()) {
|
||||
@@ -2589,6 +2671,11 @@ const COMMAND_GROUPS = [
|
||||
[
|
||||
["status", "", "Up/down indicator for the dashboard server"],
|
||||
["start", "[--port N]", "Start the server in the background and wait for healthy"],
|
||||
[
|
||||
"service",
|
||||
"install|uninstall|status [--port N]",
|
||||
"Auto-start on boot via systemd --user (Linux)",
|
||||
],
|
||||
["repl", "", "Open the interactive shell (also: shell, i)"],
|
||||
],
|
||||
],
|
||||
@@ -3490,6 +3577,7 @@ const COMMANDS = (() => {
|
||||
/** Subcommand tokens per command, used by the REPL's tab completer. */
|
||||
const SUBCOMMANDS = {
|
||||
alerts: ["ack", "ack-all"],
|
||||
service: ["install", "uninstall", "status"],
|
||||
pricing: ["set", "delete", "reset"],
|
||||
webhooks: ["test"],
|
||||
import: ["rescan", "path"],
|
||||
@@ -3509,6 +3597,8 @@ async function runCommand(argv) {
|
||||
return cmdStatus();
|
||||
case "start":
|
||||
return cmdStart(flags);
|
||||
case "service":
|
||||
return cmdService(positional[0], flags);
|
||||
case "stats":
|
||||
return cmdStats();
|
||||
case "kanban":
|
||||
|
||||
Reference in New Issue
Block a user