diff --git a/plugins/EXTRACT-A-PLUGIN-PROMPT.md b/plugins/EXTRACT-A-PLUGIN-PROMPT.md new file mode 100644 index 00000000..bf26ebe9 --- /dev/null +++ b/plugins/EXTRACT-A-PLUGIN-PROMPT.md @@ -0,0 +1,84 @@ +# Kickoff prompt: extract a feature into a plugin + +Paste everything below the line into a fresh agent, replacing `` with the feature's name +(`photos`, `jellyfin`, `email`, `soulseek`, `wallet`, …). Nothing else needs to be said first. + +--- + +You are extracting **``** out of the Officer platform and into a standalone plugin, the way +`offscale` and `music` were extracted before it. Work in `/home/pastilhas/officer-dev/platform`. + +## Read these first, in this order, before touching anything + +1. **`plugins/EXTRACTING-A-PLUGIN.md`** — the runbook. It is the authority: the rules, the order that + worked, the verification table, and a list of traps that each cost real time once. Read the section + "What changed on 2026-08-16" carefully; it contradicts an earlier instruction in the same file, and the + contradiction is deliberate. +2. **`plugins/example/README.md`** — what a plugin IS, stated as conventions, each one verified against + `src/servers/plugins/discover.ts` rather than remembered. +3. **`src/servers/plugins/`** — the system itself. `discover.ts` (what a directory must contain), + `manifest.ts` (the contract and `mountPrefix`), `mount.ts`, `install.ts`, `schema.ts`, `ecosystem.ts`. + When this prompt and that code disagree, the code is right. +4. **`CLAUDE.md`** — house rules for the whole repository. Style, the permission model, and why + `assertPermissionTotality` refuses to boot the server. +5. If `` holds credentials: **`docs/secret-store.md`**. + +Then read the two plugins that already did this. Both carry a `PLUGIN.md` recording their own extraction: +`gitea.officer.dev/plugins/music` and `gitea.officer.dev/plugins/offscale`. Music is the harder case and +the more useful one, because its boundary was dictated by imports pointing the wrong way. + +## What "done" means + +`` is a public repository that a stranger's Officer can install from the marketplace, and the +platform contains no trace of it beyond dated history. Specifically: + +- its own repo on `gitea.officer.dev/plugins/`, public, cloning anonymously over https +- a `manifest.ts`, and `api/`, `db/`, `sidecar/`, `web/` as it needs them +- a README that works as both documentation and shop window +- a row in the marketplace catalogue, and a matching entry in `BUNDLED` in + `src/servers/plugins/marketplace.ts` +- installs, enables, disables, uninstalls and reinstalls on a live server, verified by you, not reasoned + about +- `bunx tsgo` clean, `bun test` with no new failures against the baseline you recorded before starting + +## How to work + +**Map before you move.** The first task is not a file move — it is finding every line of platform code +that names ``, and every import pointing INTO it from `src/`. A plugin may import platform code +(`@@/…`); the reverse cannot happen. Anything in `src/` that reaches into `` and cannot move +decides where the boundary goes. Music's boundary was chosen by two such imports, found by reading the +graph rather than by reasoning about what music "is". Report what you find before you start moving. + +**Ask before deciding anything that is not mechanical.** The owner would rather answer a question than +review a wrong assumption. Say what you would do and why, and wait, when: a piece cannot cleanly move; a +permission's shape is not obvious; the split would change behaviour for a member; or the feature wants +something the plugin contract does not offer (a global overlay, a websocket, a component export — the +answers are no, not yet, and no, and the runbook says why). + +**Verify on the live server, and purge between attempts.** Uninstall deliberately keeps the directory and +the tables, so a second install is not a fresh one. Use the Marketplace screen's "Delete … and all its +data", then install from the catalogue. That is the only run that resembles a stranger's machine. + +**Do not restart the platform yourself.** Hand the owner the command. `pm2 restart officer` breaks their +chat session, and they will run it when they are ready. + +**Report honestly.** Say what you verified and what you did not. "I did not test the member path" is +useful; a claim you did not check is not. If you assert a behaviour, name the file and line, or the +command whose output you read. + +## When you get stuck + +Blockers go in writing, not into a workaround. Write them down, say plainly that you are blocked and on +what, and move to the next piece. The runbook's "Still open, platform-wide" section lists problems that +are already known — check there before treating one as new. + +## Finishing + +Commit and push both repositories — the plugin's and the platform's — including if it went badly. A +reverted commit with an explanatory message is worth more than a clean history that hides an abandoned +approach. Commit messages: simple lowercase, no prefixes, and say what moved, what it found, and what is +still open. + +Then update `plugins/EXTRACTING-A-PLUGIN.md`: add what this extraction taught, and correct anything it +told you that turned out to be wrong. The runbook is the thing that makes the next one cheaper, and it is +only worth that if each extraction leaves it more accurate than it found it. diff --git a/plugins/EXTRACTING-A-PLUGIN.md b/plugins/EXTRACTING-A-PLUGIN.md index cc133a60..db80489c 100644 --- a/plugins/EXTRACTING-A-PLUGIN.md +++ b/plugins/EXTRACTING-A-PLUGIN.md @@ -1,5 +1,8 @@ # Extracting a feature into a plugin +> **Handing this to a fresh agent?** `plugins/EXTRACT-A-PLUGIN-PROMPT.md` is the kickoff prompt — paste it, +> replace the feature name, and it points back here. This file is the authority; that one is the way in. + The runbook, written the day offscale became the first one. Follow it for music, then for the rest. **This directory holds documentation, not plugins.** Every plugin is its own repository as of @@ -91,7 +94,7 @@ on uninstall. Only deleting a plugin's source can lose its data. ## Verification — run all of it ``` -bun test # 757 pass, 10 pre-existing failures. Any 11th is yours +bun test # record the baseline BEFORE you start. Any new failure is yours pm2 restart officer ``` @@ -107,7 +110,7 @@ Then through `/plugins`, watching PM2 and the browser at each step: | 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 | `No changes detected` — data survives | +| `bun db:push` while uninstalled | **do not run this — see "What changed on 2026-08-16"** | | install again | identical to the first install | A normal refresh is enough; the shell is `no-store`. When the log's last line appears, the bundle exists. @@ -173,6 +176,80 @@ Reopen this only for a feature where the overlay is the whole product, and expec --- +## What changed on 2026-08-16. Read this before following the steps above + +A day of installing plugins onto a machine that had never had one found four things. The runbook above is +still right about the shape of an extraction; these change how you verify it. + +**The install no longer runs `bun db:push`, and you should not either.** A plugin's tables are created by +`pushPluginSchema`: `drizzle-kit generate` against the plugin's schema alone, into an empty directory, +then the statements applied one at a time with "already exists" skipped. It cannot emit a DROP of +anything. + +The global push is a trap on any machine holding tables the current schema does not declare — a plugin +that is not on disk, anything left from a rename. drizzle-kit sees a new table appearing while others are +due to be dropped and asks, interactively, whether it is a rename. **That prompt needs a TTY.** `--force` +does not answer it, `< /dev/null` does not answer it, piping newlines does not either — all measured. With +no terminal it applies NOTHING and exits 0, so an install reports success and the plugin fails its first +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. + +**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 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.** + +**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 +success but did not create …", the push is the problem, not your schema. + +**Tables with no owner are now reported.** The Marketplace screen lists any table no code on the machine +claims, with a row estimate. During an extraction this is how you notice you renamed a table and left the +old one behind — it is exactly the state that produced the trap above. + +--- + +## The marketplace is part of the job now + +An extracted plugin is not finished when it installs from disk. It has to be installable from +`marketplace.officer.dev`, which is three things: + +1. **A public repository that clones anonymously over https.** Check it with + `GIT_TERMINAL_PROMPT=0 git clone --depth 1 /tmp/probe` — an SSH remote only works on the machine + that published it, and the catalogue rewrites SSH to HTTPS precisely so this cannot be got wrong. +2. **A README.** It renders on the plugin's page in the store AND in the platform's install pane, so it is + the shop window as much as the documentation. `plugins/example/README.md` is the model, and the house + voice applies: state what is missing as plainly as what works. +3. **A catalogue row.** `bun --env-file=.env scripts/marketplace-sync.ts` reads every plugin directory's + `manifest.ts` and README and upserts them. Add a matching entry to `BUNDLED` in + `src/servers/plugins/marketplace.ts` as well — that is the offline fallback, and a machine with no + internet shows it. + +`docs/marketplace.md` is the design; `docs/marketplace-todo.md` is what it does not do yet. + +**The screen is `/marketplace` now, not `/plugins`.** The API kept its name. + +--- + +## Two more rules, learned the same day + +**The platform must not know what your plugin is about.** Not just its code — its VOCABULARY. Ten +comments in `src/` still explained what Headscale was, months after the code left, and each one was +correct and in the wrong repository. When you finish, grep the platform for your feature's domain words, +not only its identifiers, and keep only dated history: "X was here until " is a record; explaining +what X does is a leak. + +**Do not add a spawn that trusts `$PATH`.** An install died on `Executable not found in $PATH: "bunx"`, +because PM2 starts the platform with a PATH that has `bun` and not `bunx`. Resolve a path +(`node_modules/.bin/…`, `process.execPath`) as `pushPluginSchema` now does. + +--- + ## Which one next No decision has been made. What the tree says, for whoever picks it: