Three mechanisms where there should be one. The catalogue was filled by hand, and each host separately polled twenty-one repositories with git to work out whether a release existed — a second source of truth for a fact one place already knew. Shipping photos 2026.09.03.02 correctly and watching alpha not notice for ten minutes is what started this. The timer. scripts/marketplace-cron.sh, every minute, on the machine that publishes. Safe now only because the previous commit made the sync read tags instead of working trees; over the old one this would have published somebody's afternoon on a schedule. Nothing is written unless the release commit differs from the one already published, so a tick with nothing to do is one query, one fetch per repo and no writes — about 2.7s. One timestamped summary line per tick regardless, because a fresh timestamp is the only thing that separates "nothing to publish" from "the timer died"; a named line only for a plugin that moved. The badge. `updateable` now compares the tag at HEAD with the tag the catalogue says is newest — difference, not ordering, since parsing 2026.08.27.01 works right up until somebody tags v2 and then fails silently. refreshRemote() and its two-minute window are gone with the polling they existed to paper over. The git rule stays as the fallback for a plugin the store cannot speak for: an unregistered checkout, or an unreachable store. That case now reads whatever refs the last pull left, which is the accepted cost of not making every machine poll every repository forever. fetchCatalogue()'s cache went from five minutes to one. It only drew tiles before; now the wait for a release to appear would have been the sync's minute plus its five. Also: the Update button names its destination whenever a release is known, not only on `new-release`. An untagged checkout reports `untagged` and used to lose the tag with it, so the button said "Update" and the owner had to guess where it landed — the marketplace knows, even for a checkout that has never fetched it. Verified: forced a stale row, the timer republished it inside 60s and logged one line. Badge decisions checked against the live catalogue for seven plugins, including the two shapes that matter — at a tag the store agrees with, and untagged with a release known only to the store. 9 release tests pass, 100% of release.ts covered, four of them new and all against real repositories. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
4.2 KiB
Marketplace — deferred work
What marketplace.officer.dev and the plugin install path do not do yet, as of 2026-08-16. Design and
rationale live in docs/marketplace.md; this is the list.
Ranked by what actually blocks something.
Blocks other people using it
-
Third-party submissions cannot use the sync.
scripts/marketplace-sync.tsreads a manifest by IMPORTING it, which is code execution. Fine for our own repositories, not a mechanism a stranger's plugin can go through. Needs either a sandbox or a publishedmanifest.jsonalongside the TypeScript one. Until this exists the catalogue is ours only, and everything below is cosmetic. -
No write API and no submission flow. Registering a plugin means running the sync by hand against the database. Deliberate while nobody can submit; the first outside contributor makes it the gap.
Blocks a plugin evolving
-
Plugin installs CREATE, they do not MIGRATE.
pushPluginSchemagenerates the SQL that builds a schema from nothing and applies it, skipping objects that already exist. A new version that adds a column will not get it — the CREATE is skipped as "already exists" and the column is silently absent. Plugin upgrades need real migrations. The old globaldb:pushappeared to handle this, at the price of being able to drop half the database, so this is a known trade rather than a regression. -
No versions table.
plugin_metadatais one row per plugin — a snapshot, not a history. A store that can offer "install 1.2 instead of 1.3", or show a changelog, needs a different shape.
Staleness
-
Nothing refreshes the catalogue automatically. Done 2026-09-03:
scripts/marketplace-cron.shevery minute. A Gitea webhook would make it a second instead, at the price of a public write route on the one process here with no login — not worth it for a minute. -
The sync reads local clones, not the remotes. Done 2026-09-03. Bare mirrors, read at the newest tag, driven by the registry rather than by a directory listing. The catalogue no longer describes any machine's working copies.
-
rawUrl()assumes Gitea. A GitHub-hosted plugin gets a 404 for its icon. The forge belongs in the registry rather than inferred from a URL shape. (Themain-vs-masterhalf of this is gone — URLs pin to the release tag now.) -
Four plugins have never been tagged, so the store still shows rows the old sync took from a working tree: av-club, kanban, openseo, fofos-admin. They are frozen until somebody tags them, and the sync names them on every run somebody asks for.
Scale
-
fetchCatalogue()fetches everything, every time. ~737 bytes per entry, so 100 plugins ≈ 72KB and 1,000 ≈ 720KB. Correct at three; the fix at that size is search rather than pagination, and search is a server-side query that changes the shape anyway — so this is deliberately not built early. The seam isfetchCatalogue()itself, which already returns a promise with three fallback paths behind it. -
The platform fetches the whole catalogue to answer "what can I install", then discards everything already on disk. Fine at three, wasteful at 300. Moving that filter server-side means the platform telling the store what it has, which would be the first data sent TO the marketplace — worth doing deliberately rather than by accident.
-
No search, no categories, no install counts. Three plugins do not need them.
Smaller
-
A plugin's own icon is invisible before install.
assets/is published topublic/plugins/<app>/on mount, which only happens once it is installed; the catalogue'siconUrlworks around it by pointing at the repository, and a plugin author has to know to add one. -
git,pm2andsudoare still PATH lookups in the install path. They work on both machines we have, andbunxdid not — seedc420ba5. Anything new should followpushPluginSchema's pattern of resolving a path rather than trusting$PATH. -
Uninstall leaves the ecosystem file's PM2 entry removed but the process's logs behind. Harmless, unexamined.