Files
platform/docs/marketplace-todo.md
pastilhasandClaude Opus 5 19bbbab907 publishing a plugin is pushing a tag
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>
2026-09-03 13:13:43 +01:00

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.ts reads 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 published manifest.json alongside 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. pushPluginSchema generates 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 global db:push appeared 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_metadata is 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.sh every 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. (The main-vs-master half 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 is fetchCatalogue() 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 to public/plugins/<app>/ on mount, which only happens once it is installed; the catalogue's iconUrl works around it by pointing at the repository, and a plugin author has to know to add one.

  • git, pm2 and sudo are still PATH lookups in the install path. They work on both machines we have, and bunx did not — see dc420ba5. Anything new should follow pushPluginSchema'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.