plugins: enable, disable, delete — and uninstall is gone

Three states all read as "stopped": disabled, uninstalled, and uninstalled-with-the-directory-still-
there. Nothing on screen distinguished them, and the one that mattered was invisible — an uninstalled
plugin kept its directory, which silently decided what the next install would run. That is how a
jellyfin install on alpha re-applied a checkout from two days earlier while reporting success at
every step.

So uninstall stops being a verb. What it did survives as the first half of deleting: it stops the
sidecar, removes the install row and the PM2 entry, unpublishes the copies, and revokes grants naming
permissions that are about to stop existing.

Delete is offered only on a plugin that is not running — disable is one reversible click, so a live
plugin is always two deliberate steps from gone. It opens a confirmation carrying a checkbox, "also
delete all its data", unticked and never inherited from last time. Unticked, the directory goes and
the rows stay, and they appear in the orphan list further down the same screen. Ticked, the tables go
with it, which is the only way to a machine that has never had the plugin.

The checkbox picks between two ROUTES, not a body flag on one: a flag can be added to a request by
anything that already knows how to call the safe verb, and the difference here is somebody's rows.

Grants are revoked either way, which is load-bearing until the permissions work tomorrow: a grant
naming a key the platform no longer recognises made the whole permissions screen unsaveable, because
the page submits every grant it loaded.

Also corrected two rationales that the change made false — the runbook's "uninstall never destroys
data" verification table, and npm-deps.ts explaining directory survival in terms of uninstall.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-20 21:19:32 +00:00
co-authored by Claude Opus 5
parent d6aef4cb92
commit 287cec39d0
7 changed files with 217 additions and 119 deletions
+22 -14
View File
@@ -78,9 +78,11 @@ Music added the field; see its PLUGIN.md for what it is guarding against.
**Nothing may branch on provenance** except `mountPrefix()`. First-party and third-party differing
anywhere else means two systems, and only one gets tested.
**Uninstall never destroys data.** The generated schema barrel follows plugin **directories**, not the
install table — `db:push` drops what it cannot see, so following installs would delete a plugin's tables
on uninstall. Only deleting a plugin's source can lose its data.
**Only an explicit tick destroys data.** The generated schema barrel follows plugin **directories**, not
the install table — `db:push` drops what it cannot see, so following installs would delete a plugin's
tables the moment it was stopped. Deleting a plugin removes its directory, and the confirmation's
"also delete all its data" checkbox is the one thing that drops its tables. Unticked, the rows outlive
the plugin and show up in the orphan list on the same screen.
---
@@ -125,9 +127,9 @@ Then through `/plugins`, watching PM2 and the browser at each step:
| permissions page | its permission is listed, read/write/none |
| disable | route 404s, sidecar stops, **tables and rows survive** |
| enable | comes back |
| uninstall | route gone, `pm2 list` loses it, **data still there** |
| `bun db:push` while uninstalled | **do not run this — see "What changed on 2026-08-16"** |
| install again | identical to the first install |
| delete, checkbox unticked | route gone, `pm2 list` loses it, directory gone, **data still there** |
| install again | fresh clone, and it finds those rows |
| delete, checkbox ticked | tables dropped too — a machine that never had it |
A normal refresh is enough; the shell is `no-store`. When the log's last line appears, the bundle exists.
@@ -220,15 +222,21 @@ no terminal it applies NOTHING and exits 0, so an install reports success and th
query with 42P01. `--force` would be worse if it worked: the destructive statements are `drop table` for
every plugin whose directory is absent.
So: the row in the verification table that says to run `bun db:push` while uninstalled is struck out. The
property it was checking — uninstall does not destroy data — is now checked by reinstalling and finding
your rows still there.
So: the row in the verification table that said to run `bun db:push` while stopped is gone. The property
it was checking — stopping a plugin does not destroy data — is now checked by deleting it with the
checkbox unticked, installing again, and finding your rows still there.
**Use `purge` to test the install twice.** Uninstall deliberately keeps the directory and the tables, so a
second install is not a fresh one: the marketplace hides a plugin whose directory exists, and the tables
are already there. The Marketplace screen has "Delete <plugin> and all its data" — it drops the tables,
deletes the directory and regenerates the barrel, and afterwards the plugin installs from the catalogue
exactly as a stranger's machine would. **That is the only honest test of an install.**
**Delete WITH the checkbox to test the install twice.** Anything less is not a fresh install: the
marketplace hides a plugin whose directory exists, and tables that are already there make `db:push` a
no-op, so a broken schema stays broken and passes. Ticked, it drops the tables, deletes the directory and
regenerates the barrel — and afterwards the plugin installs from the catalogue exactly as a stranger's
machine would. **That is the only honest test of an install.**
**Uninstall was removed on 2026-08-21.** It sat between disable and delete, keeping the directory and the
data, and the screen could not show the difference: an uninstalled plugin stayed in the list under a
heading that said "Installed" while its directory silently decided what the next install would run. That
is not hypothetical — it is how a jellyfin install on alpha re-applied a checkout from two days earlier,
reporting success at every step. The verbs are now enable, disable and delete.
**The install now verifies its own tables.** After the push it asks Postgres whether each declared table
exists and fails the install naming what is missing. If your extraction reports "schema push reported