#!/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);