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:
2026-08-28 16:15:45 +07:00
parent 8bd74a57e6
commit 1cb7ac0a4b
5 changed files with 303 additions and 7 deletions
+37 -3
View File
@@ -133,7 +133,14 @@ waits on an install:
5. **Legacy hook cleanup** — see above.
6. **CLI** — write the `~/.local/bin/ccam` launcher, never clobbering a `ccam` the bootstrap did not write.
7. **UI build** — `client/` is copied into the runtime dir and built there (`npm install && npm run build`), landing in `runtime/client-dist` — the same directory the server serves from. Skipped when a bundle for the current version already exists; a failure here does not fail the bootstrap (API and MCP still work, and `/ccam-open` can retry) but does mean `/run` 404s until it's fixed.
8. **Server** — spawn `server/index.js` detached, with `NODE_PATH` at the runtime `node_modules` and `DASHBOARD_CLIENT_DIST` at the runtime `client-dist`.
8. **Server** — on Linux with `systemd --user` available, install/refresh a
`ccam.service` unit (see [Auto-start on boot](#auto-start-on-boot)) instead
of a bare spawn, so the server is supervised and comes back after a reboot
with no Claude Code session involved. Everywhere else (macOS, Windows, or
no systemd), spawn `server/index.js` detached, with `NODE_PATH` at the
runtime `node_modules` and `DASHBOARD_CLIENT_DIST` at the runtime
`client-dist` — scoped to this session, same as before this fell back to
systemd.
9. **Record state** — `runtime/state.json`.
The first run takes a few minutes — most of it is the UI build, which is what
@@ -143,6 +150,26 @@ goes to `~/.claude/agent-dashboard/runtime/bootstrap.log`; `npm run build`'s own
output goes to `client-build.log` next to it; the server's own output goes to
`server.log`.
### Auto-start on boot
On Linux, every plugin bootstrap (not just the first) installs/refreshes a
systemd `--user` service (`~/.config/systemd/user/ccam.service`, managed via
`scripts/systemd-service.js` — shared with the checkout path's
`ccam service install`, see the README). Its `ExecStart`/`WorkingDirectory`
point at the current plugin cache path, so a plugin update that moves the
cache directory rewrites and restarts the unit on the next session start, the
same self-healing pattern the CLI launcher already uses. The bootstrap also
tries `loginctl enable-linger` so the service starts on a cold boot before
anyone logs in; when that needs elevated rights, it's skipped silently (check
`~/.claude/agent-dashboard/runtime/bootstrap.log`, or run
`sudo loginctl enable-linger <user>` once by hand). A pre-existing
`ccam.service` unit not written by this bootstrap is left untouched, and the
server falls back to the old per-session detached spawn instead.
On macOS, Windows, or a Linux box without systemd, nothing changes: the
server is a detached spawn started fresh on the next `SessionStart` after a
reboot, same as always.
### Commands
| Command | Does |
@@ -171,6 +198,9 @@ the plugin from the enabled list — verified against a real install/uninstall,
not assumed. It does **not**:
- stop the running server (it keeps serving from the now-uninstalled cache dir)
- disable or remove the `ccam.service` systemd `--user` unit (Linux) — it
will keep restarting the old server on every reboot, since `systemctl`
knows nothing about `claude plugin uninstall`
- remove the cached source under `~/.claude/plugins/cache/.../ccam/<version>/`
- remove the hook entries `claude plugin install` wrote into
`~/.claude/settings.json` (still pointing at that cache dir — they keep
@@ -181,8 +211,12 @@ not assumed. It does **not**:
For a clean machine, do all of this yourself:
```bash
# 1. Stop the server (skip if ~/.claude/.agent-dashboard.json is already gone)
kill "$(node -e 'console.log(JSON.parse(require("fs").readFileSync(require("os").homedir()+"/.claude/.agent-dashboard.json","utf8")).pid)')"
# 1. Stop the server. On Linux, check for the systemd unit first — a bare
# `kill` just gets restarted by it (Restart=on-failure).
systemctl --user disable --now ccam.service 2>/dev/null
rm -f ~/.config/systemd/user/ccam.service && systemctl --user daemon-reload 2>/dev/null
# Fallback for a detached (non-systemd) server, or if the unit above was absent:
kill "$(node -e 'console.log(JSON.parse(require("fs").readFileSync(require("os").homedir()+"/.claude/.agent-dashboard.json","utf8")).pid)')" 2>/dev/null
# 2. Runtime state and the CLI launcher
rm -rf ~/.claude/agent-dashboard/runtime # deps, UI bundle, logs, state