update a plugin, or all of them, from the marketplace
There was no update path. fetchPlugin REFUSES an existing directory — cloning over one is how uncommitted work disappears — so install was a one-way door, and the answer to "my plugin changed" was uninstall and reinstall, which drops its tables on the way past. An update is a git pull and then the ORDINARY install path. installPlugin already skips the clone when the directory is present and re-runs everything else: os dependencies, schema push, assets, the ecosystem entry, the sidecar, the remount and the frontend rebuild. So there is no second implementation of "apply a plugin" here to drift from the first. Two refusals, both about not destroying work. A dirty working tree is refused rather than stashed or force-pulled — every plugin directory on a developer's machine is a live checkout, and this repository's own are edited in place. Non-fast-forward is refused for the same reason: a diverged local commit is somebody's work and merging it automatically is not the platform's decision. The per-plugin button replaces one that was quietly broken: it appeared only when `outdated` and called `install`, which cannot fetch, so it re-applied stale code. Nothing bumps a version today either, so with everything pinned at 1.0.0 that flag never fires and the button never appeared. Now always offered, and it pulls. Update-all is a plain POST rather than the streaming door: twelve interleaved step streams is not a readable log, and the useful answer is the summary. It runs sequentially — each update pushes schema, republishes assets, rewrites the ecosystem file and rebuilds the frontend, and two of those at once is a race over one file each. One failure does not stop the rest, and failures are named with their reason, because "2 failed" sends someone to the terminal. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -15,6 +15,7 @@ import {
|
||||
uninstallPlugin,
|
||||
type PluginActionResult,
|
||||
} from '../../plugins/install';
|
||||
import { updatePlugin } from '../../plugins/update';
|
||||
|
||||
// /api/plugins — what is on this machine, what is installed, and the four verbs that change it.
|
||||
//
|
||||
@@ -176,6 +177,31 @@ pluginsRouter.get('/', async (ctx) => {
|
||||
* "installed" and "installed but the sidecar would not start" are different outcomes and the second is
|
||||
* the one worth reading. See `plugins/install.ts` for why the order inside each is what it is.
|
||||
*/
|
||||
/**
|
||||
* Update every installed plugin, in sequence.
|
||||
*
|
||||
* SEQUENTIAL, not parallel: each update runs a schema push, republishes assets, rewrites the ecosystem
|
||||
* file and rebuilds the frontend. Two of those doing it at once is a race over one file each.
|
||||
*
|
||||
* One plugin failing does NOT stop the rest — a dirty checkout is the expected failure here (every
|
||||
* plugin directory on a developer's machine is a live one), and it should not prevent the other eleven
|
||||
* from moving. The per-plugin result says which, and why.
|
||||
*/
|
||||
pluginsRouter.post('/update-all', async (ctx) => {
|
||||
const userId = ctx.get('user').id;
|
||||
const { states } = await snapshotPlugins();
|
||||
const installed = states.filter((st) => st.install).map((st) => st.plugin.appName);
|
||||
const results: PluginActionResult[] = [];
|
||||
for (const appName of installed) {
|
||||
results.push(await updatePlugin(appName, undefined, userId));
|
||||
}
|
||||
return ctx.json({
|
||||
results,
|
||||
updated: results.filter((r) => r.ok).length,
|
||||
failed: results.filter((r) => !r.ok).map((r) => ({ appName: r.appName, error: r.error })),
|
||||
});
|
||||
});
|
||||
|
||||
pluginsRouter.post('/:appName/install', async (ctx) => {
|
||||
const result = await installPlugin(ctx.req.param('appName'), undefined, ctx.get('user').id);
|
||||
return ctx.json(result, result.ok ? 200 : 400);
|
||||
@@ -228,6 +254,9 @@ const VERBS = {
|
||||
enable: (appName, onStep) => setPluginRunning(appName, true, onStep),
|
||||
disable: (appName, onStep) => setPluginRunning(appName, false, onStep),
|
||||
purge: (appName, onStep, _userId, onLog) => purgePlugin(appName, onStep, onLog),
|
||||
// A pull and then the ordinary install path — see plugins/update.ts. It is a verb rather than its own
|
||||
// route so it inherits the streaming door and the UI that already drives install.
|
||||
update: (appName, onStep, userId, onLog) => updatePlugin(appName, onStep, userId, onLog),
|
||||
// `satisfies` rather than an annotation: an annotation would widen the keys to `string`, which turns
|
||||
// `isVerb` into a guard that proves nothing and lets an unknown verb index the map.
|
||||
} satisfies Record<string, VerbFn>;
|
||||
|
||||
Reference in New Issue
Block a user