fix(plugin): restart a server left on a retired plugin root

A plugin update applied mid-session (/plugin followed by /reload-plugins)
fires no SessionStart, so the bootstrap's version handover never runs and the
dashboard server keeps executing a cache directory Claude Code has already
discarded. /ccam-open then rebuilt the bundle from the new root against that
old server, and the new bundle called routes the old server never had:
/api/lanes/pipeline-stats fell through to /:id and rendered "lane not found",
/api/workflows/skill-usage fell through to the SPA catch-all and broke
JSON.parse on the returned HTML. Both Workflows panels showed an error while
the code they needed was sitting on disk.

plugin-open now re-runs bootstrap when the live server's recorded pluginRoot
differs from the current one, or when no server is live at all. Bootstrap
already owns the stop/start and the dependency install the new root may need,
so this only restores the trigger that a mid-session update skips.
This commit is contained in:
2026-08-26 10:57:03 +07:00
parent 79e56c3221
commit c017f6dad9
3 changed files with 20 additions and 4 deletions
+1 -1
View File
@@ -149,7 +149,7 @@ output goes to `client-build.log` next to it; the server's own output goes to
|---|---|
| `/ccam-doctor` | Node version, bootstrap state, runtime deps, server liveness, duplicate hooks, CLI launcher + PATH, MCP build freshness, UI bundle |
| `/ccam-update` | Pull the latest `ccam` release via `claude plugin marketplace update` + `claude plugin update ccam@<marketplace> -y`, then reinstall dependencies and restart the server (`plugin-bootstrap.js --force`). The plugin pull needs a manual `/reload-plugins` afterward to take effect in the current session — not scriptable from a command. |
| `/ccam-open` | Build the UI bundle if missing, then print the dashboard URL |
| `/ccam-open` | Rebuild the UI bundle and print the dashboard URL. Re-runs the bootstrap first when the server is down or still executing a plugin root that a mid-session `/plugin` update retired, so a fresh bundle never talks to an older server's routes |
### Where things live
+6 -2
View File
@@ -12,8 +12,12 @@ node "${CLAUDE_PLUGIN_ROOT}/scripts/plugin-open.js" --force
```
Install + build takes a few minutes if the client toolchain isn't already
installed; otherwise the rebuild itself takes ~10-15s. No server restart is
needed — the server already serves from that directory.
installed; otherwise the rebuild itself takes ~10-15s. The server serves from
that directory, so it usually needs no restart — but if it is down, or still
running from a plugin root a mid-session `/plugin` update retired, the command
re-runs bootstrap first and prints `Server restarted on the current plugin
version.` (without it, the fresh bundle would call routes that old server
never had).
Then help the user open it:
+13 -1
View File
@@ -6,7 +6,10 @@
* on session start, so client-only routes (e.g. `/run`) work the moment
* `claude` is started, not just the API and MCP tools. `/ccam-open` re-runs
* this standalone — a no-op when the bundle already matches — as a fallback
* for `--force` rebuilds or a first build that failed during bootstrap.
* for `--force` rebuilds or a first build that failed during bootstrap, and it
* re-runs bootstrap first when the live server belongs to a retired plugin root
* (a mid-session `/plugin` update) so the new bundle is not left calling routes
* the running server never had.
*
* The client source is copied out of the (replaced-on-update) plugin cache into
* the runtime dir before building, so neither the cache nor the checkout is
@@ -74,12 +77,21 @@ function dashboardUrl() {
}
if (require.main === module) {
// A plugin update applied mid-session (/plugin + /reload-plugins) fires no
// SessionStart, so the server keeps executing a cache dir Claude Code has
// already retired: a bundle rebuilt from the new root then calls routes that
// old server never had. Hand the version handover back to bootstrap, which
// owns stop/start and the dependency install the new root may need.
const stale = (boot.readState() || {}).pluginRoot !== PLUGIN_ROOT || !boot.serverIsLive();
if (stale) boot.bootstrap();
const result = buildClient({ force: process.argv.includes("--force") });
if (result === "failed") {
console.error("Client build failed — the API and MCP tools still work.");
process.exitCode = 1;
} else {
if (result === "built") console.log("Dashboard UI built.");
if (stale) console.log("Server restarted on the current plugin version.");
console.log(dashboardUrl());
}
}