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:
@@ -1556,6 +1556,118 @@ const paths = {
|
||||
},
|
||||
},
|
||||
|
||||
"/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: ["CcConfig"],
|
||||
|
||||
Reference in New Issue
Block a user