Install, uninstall and purge narrated what the platform was doing and hid what it
was running. Every spawn captured stdout and stderr, held them, and surfaced the
last 600 characters only if the exit code was non-zero.
That is enough to explain a crash and useless for the two things people want while
watching an install: knowing a slow step is still moving, and seeing the warning a
command prints on its way to exiting 0. The second is not hypothetical — `db:push`
reported success on a machine where it created no tables. Its output was captured,
discarded because the exit code was fine, and the failure surfaced minutes later as
42P01 in a sidecar log. Whatever drizzle said about that push, nobody ever read it.
`runLogged` streams both pipes as they arrive, so they interleave the way they do
in a terminal. Following only stdout would have been silent for exactly the
commands worth watching: git writes everything to stderr, drizzle-kit writes
progress there and results to stdout.
Two pieces of noise control, because a log nobody can read is not an improvement:
`git clone` gets `--progress` instead of `--quiet` (git says nothing when it thinks
it has no terminal, and that is the step where a slow link looks like a hang) with
per-percent rewrites collapsed to the last fragment per chunk; and drizzle's
spinner, which is eight lines saying the same thing with a different braille glyph,
collapses to the first — otherwise "Changes applied" scrolls past underneath it.
Steps and output share ONE ordered list rather than two. Their order relative to
each other is the whole information: "which step was this output from" is the only
question the log exists to answer. Output renders dimmer and indented under the
step that produced it.
Verified by running a real purge and reinstall: the clone's object counts, the
barrel generation naming both plugins, drizzle's push, then "schema: applied in
2029ms (1 table verified)".
tsgo clean, frontend builds, 873 pass / 7 fail unchanged.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The /plugins route showed only what was already on disk, so a plugin nobody had
copied in by hand did not exist. It now shows a third list — what a remote
catalogue offers — and installing one fetches its source.
- plugins/marketplace.ts — a stand-in for the remote store, a JSON array in
process. One entry: music, pointing at gitea.officer.dev/plugins/music. It is
deliberately a module and not a fetch, so the shape of a catalogue entry gets
settled before anything depends on a server answering.
- plugins/fetch.ts — git clone --depth 1 into plugins/<appName>/, INSIDE the
platform repo because bun resolves the workspace links by walking up and a
plugin cloned beside it cannot import anything the platform provides
(measured, not assumed). https only, no credentials, GIT_TERMINAL_PROMPT=0 so
a repo that turned private fails instead of hanging. A failed clone removes
the directory: a half-written one reads as a broken plugin rather than an
absent one, which is the worse of the two.
- install.ts fetches first when the directory is missing, then RE-DISCOVERS
rather than assuming what it cloned. Everything after is the existing path,
including the frontend rebuild — so a cloned plugin's panels reach the SPA
without a restart.
- GET /api/plugins returns `available`, catalogue minus what is on disk. Each
entry gets the same live PATH probe a local plugin gets, so music's ffmpeg and
ffprobe are answered for this machine BEFORE the clone rather than after.
- The detail panel is a separate component for a catalogue entry, not a mode of
the installed one: there is no sidecar status, no enabled state and no route
to open, and what there IS — the source URL — the installed panel has no
reason to show. The streaming log is shared, because the marketplace install
is the case that needs it most: it clones before it does anything, and a slow
clone behind a blank panel looks like a hang.
The catalogue's icon is an absolute URL to the store's copy, which is the
[open] question the previous commit left: assets only publish at install, so a
plugin you have never installed had no artwork to draw.
Not done: no update verb, no signature, no private repos, and removePluginSource
has no caller — deleting source drops tables on the next push, and that wants a
verb that says so.
tsgo clean. 767 pass / 6 fail, all 6 identical on HEAD without this change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>