feat(cc-config): update every plugin and marketplace at once, then reload the open sessions

The plugins tab could already update one plugin or one marketplace at a time. It gains an install-wide batch and, separately, the reload step that actually makes an update take effect.

Update all runs in a DETACHED worker rather than the request handler, for a structural reason: the batch ends with the plugin the dashboard ITSELF runs from, and that step's plugin-bootstrap.js --force SIGTERMs this very server. An in-process loop would die partway through with the remaining plugins unrun and nothing left to report. Order is marketplaces (one call, no name = all) -> every other installed plugin -> AgentHub last -> one reload. Each transition is journalled to cc-update-all.json in the runtime dir, outside the plugin cache the update replaces, so whichever server is alive serves progress from it; the POST's response is only an acknowledgement.

readUpdateAll DERIVES an abandoned status for a running journal older than an hour, because a detached worker killed by a reboot leaves nobody to write the terminal entry and the button would stay wedged forever. isRunning gates a concurrent batch: two claude plugin update chains over one config dir race.

Reload is its own capability (server/lib/cc-reload.js, POST /api/cc-config/reload). /reload-plugins and /reload-skills are scoped BY CLAUDE CODE to the session that runs them, so nothing can reload a session from outside - typing into a session already running is the only real mechanism, and that is what this does, over tmux send-keys into the dashboard's live lane panes. It spawns NO claude process on purpose: a one-shot claude -p "/reload-plugins" would reload only its own short-lived session, exit, and leave a junk session in the dashboard's history - a headless run gets a full sessions row and there is no filter anywhere to suppress it. Reusing live panes means no session is created at all. Panes at a shell prompt are skipped (the text would run as a shell command) and only claude lanes are addressed, since Codex has no such command.

Update-all reloads once at the end; updating a single plugin passes reload:true and reloads immediately, and never after a failed action.

Fixes a real pre-existing bug on the way: pluginUpdate omitted -y while runClaude closes stdin, and the CLI requires it whenever stdin is not a TTY - so the per-plugin Update button failed for any plugin whose marketplace declares an install command. Also completes CcConfigChangedPayload's action union, which claimed write|delete while nine other values were already being emitted.

Verified: server 1683/1683, client 486/486 (27 new tests), build, prettier, file-header audit, openapi.yaml regenerated (117 paths). Update all was NOT executed against this machine - it would update 7 real plugins and restart the running dashboard.
This commit is contained in:
2026-09-10 16:05:07 +07:00
parent cc85e35439
commit aca35dd4cd
17 changed files with 1745 additions and 20 deletions
+101 -4
View File
@@ -6788,17 +6788,18 @@ paths:
post:
tags:
- Updates
summary: Pull, rebuild, and restart the dashboard when fast-forwardable
summary: Update this install to the newest release and restart the dashboard
description: 'Two modes, chosen by how this copy was installed and never by the request body (which is ignored). checkout: fast-forward onto the release tag, npm run setup, npm run build in production, then self-restart. plugin: the install runs from ~/.claude/plugins/cache/, so a detached helper runs `claude plugin marketplace update`, `claude plugin update -y`, then plugin-bootstrap.js --force from the NEW cache directory, which stops this server and starts the new one. In both modes the response is written before anything stops the process.'
operationId: applyUpdate
responses:
'200':
description: Checkout fast-forwarded and rebuilt; the process is about to self-restart
description: 'Update is being applied; the process is about to be replaced. mode: "plugin" means the detached plugin chain is running, otherwise the checkout was fast-forwarded and rebuilt in-place'
content:
application/json:
schema:
type: object
additionalProperties: true
description: 'Same shape as GET /api/updates/status, plus applied: true.'
description: 'Same shape as GET /api/updates/status, plus applied: true. In plugin mode also mode: "plugin" and runtime_dir (where the chain writes its verdict, read back afterwards as last_plugin_update).'
'409':
description: Declined - nothing to apply, or the checkout isn't safely fast-forwardable
content:
@@ -6806,7 +6807,7 @@ paths:
schema:
type: object
additionalProperties: true
description: 'applied: false plus reason: "up_to_date" | "not_fast_forwardable".'
description: 'applied: false plus reason: "up_to_date" | "not_fast_forwardable" | "plugin_managed". plugin_managed appears here only when a plugin-cache install has nothing newer published; when a newer release exists the plugin chain runs and the answer is 200 instead. Run /agenthub-update to force a refresh.'
'500':
description: Update apply failed
content:
@@ -7975,6 +7976,102 @@ paths:
pluginJson:
name: superpowers
version: 1.2.0
/api/cc-config/plugins/update-all:
get:
tags:
- CcConfig
summary: Progress of the current or last update-all batch
description: Read-only. Returns the detached batch worker's on-disk journal from ~/.claude/agenthub/runtime/cc-update-all.json, or `{batch:null}` when no batch has ever run. `status` is `running`|`done`|`partial`|`failed`|`abandoned`; `abandoned` is DERIVED, not stored — a `running` journal older than one hour is reported that way because the worker is detached and a host reboot leaves nobody to write the terminal entry. `partial` means the batch finished with at least one failed step. Malformed or half-written journals return null rather than erroring, since the UI polls this endpoint.
operationId: ccConfigGetUpdateAll
responses:
'200':
description: The batch journal, or null.
content:
application/json:
schema:
type: object
additionalProperties: true
example:
batch:
status: partial
started_at: '2026-09-10T08:30:00.000Z'
finished_at: '2026-09-10T08:34:12.000Z'
steps:
- key: marketplaces
label: Update all marketplaces
status: ok
- key: plugin:ponytail@ponytail
label: ponytail@ponytail
status: ok
- key: plugin:vercel@claude-plugins-official
label: vercel@claude-plugins-official
status: failed
error: marketplace unreachable
- key: reload
label: Reload open Claude Code sessions
status: ok
lanes_total: 2
post:
tags:
- CcConfig
summary: Update every marketplace and plugin, then reload the open sessions
description: 'Mutating. Spawns a DETACHED worker and returns immediately — the response is an acknowledgement, not a result; poll GET on this path to follow it. Order: `claude plugin marketplace update` (no name = all), then `claude plugin update <id> -y` for every installed plugin, then `/reload-plugins` + `/reload-skills` typed into the live lane panes. The plugin this dashboard itself runs from is updated LAST and followed by `plugin-bootstrap.js --force`, which stops this server and starts the replacement — which is why the work cannot happen in the request handler and why the journal lives outside the plugin cache. Plugin ids come from `claude plugin list --json`, never from the body, and the "self" plugin is derived from this server''s own path, so no caller can nominate a different plugin for the restart. Answers 409 EBUSY when a batch is already in flight; the body is ignored apart from an optional `reload:false`.'
operationId: ccConfigUpdateAll
responses:
'200':
description: Batch worker spawned.
content:
application/json:
schema:
type: object
additionalProperties: true
example:
ok: true
spawned: true
runtime_dir: /Users/son/.claude/agenthub/runtime
planned: 7
self_plugin:
marketplace: sg
plugin: agenthub
'409':
description: A batch is already running.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
/api/cc-config/reload:
post:
tags:
- CcConfig
summary: Tell the open Claude Code sessions to re-read plugins and skills
description: 'Types `/reload-plugins` and `/reload-skills` into the dashboard''s live lane panes over `tmux send-keys`. Both commands are scoped BY CLAUDE CODE to the session that runs them, so there is no way to reload a session from outside — typing into a session that is already running is what a human would do, and it is what this does. It spawns no `claude` process: a one-shot `claude -p "/reload-plugins"` would reload only its own short-lived session and leave a junk session behind. Changes nothing on disk, so it does not queue on the CLI mutation chain and emits no `cc_config_changed`. Panes sitting at a shell prompt are skipped (the text would run as a shell command); only `claude` lanes are addressed, since Codex has no such command. Optional body `{kinds:["plugins"|"skills"]}` narrows which commands are sent; unknown kinds are ignored, as the command strings are fixed server-side.'
operationId: ccConfigReload
responses:
'200':
description: Commands delivered. `lanes_total` counts only panes running a live claude; zero is a normal outcome, not a failure.
content:
application/json:
schema:
type: object
additionalProperties: true
example:
ok: true
commands:
- /reload-plugins
- /reload-skills
lanes_total: 1
lanes:
- id: agenthub-lane-3
laneId: 3
sent:
- /reload-plugins
- /reload-skills
'400':
description: '`kinds` was present but not an array.'
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
/api/cc-config/mcp:
get:
tags: