bun run plugins:update, and the deploy prompt stops carrying its own server list

Updating plugins was only possible through POST /api/plugins/update-all, which is authenticated — so a
deploy had to mint an API key to update the machine it was already logged into. A credential created,
used and hopefully deleted, on every host, for a local operation.

Same work through the same functions with the HTTP layer removed, not a second implementation beside
it. Sequential like the route (each update pushes a schema, republishes assets, rewrites the ecosystem
file and rebuilds the frontend — two at once is a race over one file each), one plugin's failure does
not stop the rest, and it exits non-zero if any failed so a deploy can stop rather than report a
success it did not have. Verified: 3/3 in 18s.

Its header says the one thing that is not obvious — it runs OUTSIDE the server process, so files,
schema, assets and the SPA bundle are current when it finishes but the route table is not, until
something restarts officer. A deploy restarts anyway, which is exactly why that would otherwise be
found out rather than known.

The prompt had a table of six hosts under a line saying docs/servers.md is authoritative. Both cannot
be true, and the copy is the one people trust while it rots. Removed, with the reason written in its
place so it does not come back.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-21 17:24:47 +00:00
co-authored by Claude Opus 5
parent 07f854bf11
commit 2261d16a8a
3 changed files with 74 additions and 15 deletions
+65
View File
@@ -0,0 +1,65 @@
#!/usr/bin/env bun
import { snapshotPlugins } from '../src/servers/plugins/mount';
import { updatePlugin, syncCapabilities } from '../src/servers/plugins/update';
// Update every installed plugin, from a terminal.
//
// bun run plugins:update
//
// ── Why this exists ──
//
// The only way to do this was `POST /api/plugins/update-all`, which is authenticated — so a deploy had to
// mint an API key to update the plugins on the machine it was already logged into. That is a credential
// created, used and hopefully deleted, on every host, for a local operation.
//
// This is the same work through the same functions, with the HTTP layer removed rather than a second
// implementation beside it. `prompts/deploy-commander.md` calls it.
//
// ── It runs OUTSIDE the server process ──
//
// Which matters, and is the one thing to know before reading the output. `updatePlugin` pulls, re-applies
// and rebuilds the frontend — all of which touch disk and are picked up by the running server. But
// remounting happens in the process that calls it, and this is not that process, so the routes the
// server is serving do not change until it restarts.
//
// So: the files, the schema, the assets and the SPA bundle are all current when this finishes. The route
// table is not. A deploy restarts `officer` afterwards, which is what makes the difference invisible —
// and the reason this says so rather than leaving somebody to find out.
const started = Date.now();
const { states } = await snapshotPlugins();
const installed = states.filter((st) => st.install).map((st) => st.plugin.appName);
if (installed.length === 0) {
console.log('no plugins installed — nothing to update');
process.exit(0);
}
console.log(`updating ${installed.length} plugin(s): ${installed.join(', ')}\n`);
let failed = 0;
// SEQUENTIAL, like the route: each update pushes a schema, republishes assets, rewrites the ecosystem
// file and rebuilds the frontend. Two at once is a race over one file each.
for (const appName of installed) {
process.stdout.write(`→ ${appName}\n`);
// One plugin failing must not stop the rest. A dirty checkout is the EXPECTED failure — every plugin
// directory on a developer machine is a live one — and it should not hold back the other eleven.
const result = await updatePlugin(appName, async (step) => console.log(` ${step}`));
if (!result.ok) {
failed++;
console.error(` FAILED: ${result.error}`);
}
}
// The capabilities repository rides along, the same as it does through the button: it is one question —
// "get me what has been published since I installed" — and a second gesture for it is one nobody remembers.
const capabilities = await syncCapabilities();
console.log(`\ncapabilities: ${capabilities.action}${capabilities.error ? ` — ${capabilities.error}` : ''}`);
const ok = installed.length - failed;
console.log(`${ok}/${installed.length} updated in ${Math.round((Date.now() - started) / 1000)}s`);
if (failed) console.log("a failure above is usually an uncommitted change in that plugin's checkout");
// Non-zero on failure, so a deploy script can stop rather than reporting a success it did not have.
process.exit(failed > 0 ? 1 : 0);