delete a plugin and everything it owns, and stop trusting exit codes
Two halves of the same problem: a plugin installed onto a fresh machine, reported success at every step, mounted, started its sidecar, registered — and then failed its first query with 42P01, because `offscale_servers` had never been created. Nothing said so, and there was no way back to a clean machine to try again. ── The install now asks Postgres ── `pushSchema` only checked the exit code, and `db:push` is a compound command: generate the barrel, cd, run drizzle-kit. It can exit 0 having created nothing. So the install now reads the plugin's schema for the tables it declares and verifies they exist, failing the install with a message naming what is missing instead of leaving a stack trace to appear in a sidecar log minutes later. Tables that are NOT prefixed with the app name are reported as a warning rather than a failure — they still get created, but `purge` will not remove them and a reinstall would find them already there. ── `purge`, next to an uninstall that deliberately keeps everything ── Uninstall is careful on purpose: every table and row survives so a reinstall puts you back where you were. That is what makes it safe to try, and it left no way to get to a CLEAN machine. A second install finds the directory (so the catalogue hides the plugin) and the tables (so push has nothing to do, and a broken schema stays broken). So a different verb, a different word, its own confirmation. It reads the table list from the schema BEFORE deleting the source, uninstalls (stopping the sidecar — nothing may be running against tables about to vanish), drops, deletes the directory, then regenerates the barrel unconditionally, since a barrel naming a directory that no longer exists is what stopped a server booting yesterday. The table list comes from the plugin's own schema, not from `like '<app>_%'` — that query eventually drops somebody else's table, because the prefix is a convention nothing validates and `music` is a prefix of `music-videos`. The prefix is used as a GUARD instead: a schema module could export `users`, and anything outside the plugin's own namespace is refused rather than dropped. ── Verified as a full loop on this machine ── purge offscale → table dropped, directory deleted, barrel regenerated → the marketplace offers it again → install → cloned, ssh checked, "schema: applied in 2072ms (1 table verified)", dock pinned, mounted, sidecar started. I left offscale INSTALLED here rather than restoring the half-state it was in (no install row, table present, sidecar running). tsgo clean, frontend builds, 873 pass / 7 fail unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -10,6 +10,7 @@ import {
|
||||
installPlugin,
|
||||
pluginProcessStatus,
|
||||
setPluginRunning,
|
||||
purgePlugin,
|
||||
uninstallPlugin,
|
||||
type PluginActionResult,
|
||||
} from '../../plugins/install';
|
||||
@@ -170,6 +171,12 @@ pluginsRouter.post('/:appName/install', async (ctx) => {
|
||||
return ctx.json(result, result.ok ? 200 : 400);
|
||||
});
|
||||
|
||||
// Destructive, and the only route here that is. Named `purge` rather than being a flag on uninstall so
|
||||
// nothing can reach it by passing a body field to the verb people already use.
|
||||
pluginsRouter.post('/:appName/purge', async (ctx) => {
|
||||
return ctx.json(await purgePlugin(ctx.req.param('appName')));
|
||||
});
|
||||
|
||||
pluginsRouter.post('/:appName/uninstall', async (ctx) => {
|
||||
const result = await uninstallPlugin(ctx.req.param('appName'));
|
||||
return ctx.json(result, result.ok ? 200 : 400);
|
||||
@@ -205,6 +212,7 @@ const VERBS = {
|
||||
uninstall: (appName, onStep) => uninstallPlugin(appName, onStep),
|
||||
enable: (appName, onStep) => setPluginRunning(appName, true, onStep),
|
||||
disable: (appName, onStep) => setPluginRunning(appName, false, onStep),
|
||||
purge: (appName, onStep) => purgePlugin(appName, onStep),
|
||||
// `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