From 2261d16a8a57a5b7600a37b9952a3a397bee1a8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Fri, 21 Aug 2026 17:24:47 +0000 Subject: [PATCH] bun run plugins:update, and the deploy prompt stops carrying its own server list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- package.json | 1 + prompts/deploy-commander.md | 23 +++++-------- scripts/update-plugins.ts | 65 +++++++++++++++++++++++++++++++++++++ 3 files changed, 74 insertions(+), 15 deletions(-) create mode 100644 scripts/update-plugins.ts diff --git a/package.json b/package.json index a7bc2b91..245495ae 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "gen:index": "bun run ./scripts/gen-index.ts", "fix:env": "bun run ./scripts/fix-env.ts", "todo:index": "bun run ./scripts/todo-index.ts", + "plugins:update": "bun run ./scripts/update-plugins.ts", "nudge": "bun run --env-file=.env ./scripts/nudge-agent.ts", "doctor": "bun run --env-file=.env ./scripts/doctor.ts", "secret:set": "bun run --env-file=.env ./scripts/secret-set.ts", diff --git a/prompts/deploy-commander.md b/prompts/deploy-commander.md index 89300c03..9f18b572 100644 --- a/prompts/deploy-commander.md +++ b/prompts/deploy-commander.md @@ -46,16 +46,11 @@ at, and guessing is how a database gets a schema it was not ready for. ## The servers -`docs/servers.md` is the list, and it is authoritative. Six platforms as of 2026-08-21: +**Read `docs/servers.md`.** Do not take a list of hosts from this file — there is deliberately none here, +because a copy rots the day a server moves and the copy is always the one somebody trusts. -| Host | Checkout | -| ---------------------------- | ----------------------------------- | -| `officer-dev` | `~/officer-dev/platform` | -| `alpha` | `~/officerdev/platform` | -| `fofos` | `~/officerdev/platform` | -| `macbookpro` | `~/projects/officer-suite/platform` | -| `edge-pertento-bridge` | `~/officerdev/platform` | -| `production-pertento-bridge` | `~/officerdev/platform` | +That document holds every machine, its tailnet name, its checkout and whether a platform runs on it. Read +it at the start of every deploy, and treat the hosts marked as having no platform as out of scope. Four things that will catch you out, all verified rather than assumed: @@ -79,8 +74,10 @@ assemble at the end from memory is a report that will be wrong about the host th 1. **Refuse a dirty tree.** `git status --porcelain` — if anything is uncommitted, skip this host and say so. Never discard someone's work to ship a release. 2. `git pull --ff-only`. A non-fast-forward means that checkout has diverged: skip and report. -3. **Update the plugins.** There is no CLI for this yet — see "What is missing" below. Until there is, - the plan file says how, or the step is skipped and the report says it was skipped. +3. **Update the plugins.** `bun run plugins:update`. Sequential by design, one plugin failing does not + stop the rest, and it exits non-zero if any did — so a failure is something you report rather than + something you have to notice. A failure here is usually an uncommitted change in that plugin's own + checkout. 4. **Run the plan's commands** for that host, in the order given. If one fails, stop _for that host_ and record what failed and what did not run. Do not continue past a failed `bun install` or `db:push`. 5. **Restart what the plan names**, and nothing else. Never `pm2 restart ecosystem.config.cjs` — it @@ -112,9 +109,5 @@ that are hard to reconstruct afterwards. ## What is missing, as of writing -**There is no CLI for updating plugins.** The only entry point is `POST /api/plugins/update-all`, which is -authenticated, so it needs a key. Until `bun run plugins:update` exists, the plan file must say explicitly -how — or say to skip it, and the report must record that it was skipped. - **Nothing wakes you yet.** A watcher, a cron entry or a repeating prompt has to exist and does not. Until then this runs when a person starts it. diff --git a/scripts/update-plugins.ts b/scripts/update-plugins.ts new file mode 100644 index 00000000..f5e7939b --- /dev/null +++ b/scripts/update-plugins.ts @@ -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);