plugins declare the host binaries they need, and the installer checks

Offscale was self-sufficient. Music is not — it shells out to ffmpeg and
ffprobe — and the way it fails without them is the reason this is a check
rather than a line in a README.

It does not fail. Missing ffprobe means the indexer catches the spawn error
and returns a track carrying its filename and nothing else: no title, artist,
album, duration or embedded lyrics. It then walks the whole library, writes a
complete cache tree and reports success. Five swallowed catches, no log, no
counter, and the only tell is coversSaved: 0 in a report nobody reads.

So `osDependencies` is a manifest field: the binary to probe on PATH, why it
is needed, and a package name per package manager. The shape is taken from
scripts/setup-old/setup.sh rather than invented — probe the binary, case on
$PM — and the names are per-manager rather than canonical-with-overrides
because lib/packages.sh already recorded why that indirection was rejected.
Probing the binary is what makes "built-in on this OS" free: on PATH means the
package map is never consulted.

Four decisions worth naming.

Missing and uninstallable REFUSES the install, first, before a table is
created or a row written — so there is nothing to undo, and the alternative is
a plugin that installs, answers 200 and quietly produces nothing.

The status is on GET /api/plugins and rendered before the button, because the
owner is deciding whether to let the server run a package manager as root and
that needs answering first. Installing by hand and watching it flip to present
is the escape hatch on a machine without passwordless sudo.

Package names get a deliberately narrow regex and reach Bun.spawn as an argv
ARRAY, never a shell. Both halves are load-bearing: the regex means a
metacharacter cannot get there, argv means it would be an argument rather than
syntax if it did. Narrower than package managers actually accept — no `:`, no
`+` version pins — because a plugin needing one wants a conversation.

Success is OBSERVED, not inferred: after installing, the binaries are re-probed.
A package manager exiting 0 having installed something that does not provide
the binary is exactly the failure this exists to catch.

installCommand mirrors lib/packages.sh's pkg_install_now exactly, including
apt's non-interactive environment, so there is one definition of "install a
package" rather than two that drift. sudo always gets -n: under PM2 a password
prompt is not a slow path, it is a hang. brew never escalates.

Verified live. ffmpeg and ffprobe were absent on this machine all evening; the
page showed both missing with the exact root command, the install streamed
`dependencies: installing ffmpeg with apt` then `ffprobe, ffmpeg now on PATH`,
and X-Audio-Duration appeared on a stream response for the first time. The
refusal path was exercised against a temporary probe dependency: HTTP 400,
steps: [], reason named.

THIS CHANGED THE MACHINE: ffmpeg 6.1.1-3ubuntu5 is now installed via apt.

Found on the way: a manifest is read once per process. Discovery does
`await import()` and the module cache holds it, so editing a manifest changes
nothing until pm2 restart officer — including `outdated`. Cost ten minutes and
is now in the runbook.

bunx tsgo clean. 797 tests, 787 pass, 7 fail — the same seven, +25 new.
This commit is contained in:
2026-08-15 02:33:18 +00:00
parent 05eb947bd1
commit e930586878
10 changed files with 713 additions and 16 deletions
+36 -6
View File
@@ -133,14 +133,44 @@ change inside `db/queries.ts`, not a flag on the manifest.
---
## Host dependencies a manifest cannot declare
## Host dependencies — the field music created
`ffmpeg` and `ffprobe`, for tag reading, cover compression and video poster frames. There is no field for
a host binary and inventing one for this would be a field nothing else reads.
`ffmpeg` and `ffprobe`. Offscale needed nothing, so until music there was no reason to build this and no
way to say it; the first draft of this document said "there is no field for a host binary" and left it at
that. That was the wrong answer, because of HOW music fails without them.
**Neither is installed on this machine** (verified 2026-08-15). The sidecar starts and serves fine — the
`X-Audio-Duration` header simply does not appear, and indexing would produce no tags or covers. Nothing
breaks today because there is no library: `~/Music` did not exist either.
It does not fail. `ffprobe` missing means the indexer catches the spawn error and returns a track carrying
its filename and nothing else — no title, artist, album, duration or embedded lyrics — then walks the
whole library, writes a complete cache tree and reports success. Five swallowed catches in
`indexer.ts` and `stream-audio.ts`, no log, no counter. The only tell is `coversSaved: 0` in a report
nobody reads. A refusal wearing the costume of a normal result.
So `osDependencies` is a manifest field now (`servers/plugins/manifest.ts`, `servers/plugins/os-deps.ts`):
```ts
osDependencies: [
{ binary: 'ffprobe', reason: '…', packages: { apt: 'ffmpeg', pacman: 'ffmpeg', dnf: 'ffmpeg', brew: 'ffmpeg' } },
{ binary: 'ffmpeg', reason: '…', packages: { } },
]
```
Both are declared even though one package provides both, because the platform probes BINARIES and these
two fail differently — and the owner should be told which one they are missing. The installer dedupes to
a single `ffmpeg` before anything reaches a command line.
The shape is `scripts/setup-old/setup.sh`'s, not invented: probe the binary, map to a package name per
manager. Probing the binary is what makes "built-in on this OS" free — if it is on PATH the package map
is never consulted. Per-manager names rather than canonical-with-overrides because `packages.sh` already
recorded why that indirection was rejected.
**Verified end to end on 2026-08-15.** Both binaries were absent on this machine all evening. The plugins
page showed `ffprobe missing — ffmpeg` and `ffmpeg missing — ffmpeg` with the exact root command it would
run; installing streamed `dependencies: installing ffmpeg with apt``dependencies: ffprobe, ffmpeg now
on PATH`, and `X-Audio-Duration: 7.026939` appeared on a stream response for the first time. The refusal
path was exercised separately against a temporary probe dependency: HTTP 400, `steps: []`, and the reason
named — nothing had happened, so there was nothing to undo.
`~/Music` still does not exist, so there is no library to index.
`cliamp`, `parec`, `pulseaudio` and `pactl` stayed behind with cliamp. The sidecar logs
`pulseaudio not installed, skipping audio setup` and carries on, which is the right shape.
+24 -3
View File
@@ -39,10 +39,10 @@ import type { PluginManifest } from '@@/plugins/manifest';
// install and gone at uninstall. So the seam switches itself off with the plugin, with no code path
// that knows why.
//
// ── Host dependencies a manifest cannot declare ──
// ── Host dependencies ──
//
// `ffmpeg` and `ffprobe` must be on PATH: tag reading, cover compression and video poster frames. There
// is no field for a host binary and inventing one for this would be a field nothing else reads.
// Music is the plugin that made `osDependencies` exist. Offscale was self-sufficient, so until this one
// there was nothing to declare and no reason to build the field — see ./PLUGIN.md.
export const manifest: PluginManifest = {
publisher: 'officerdev',
version: '1.0.0',
@@ -82,4 +82,25 @@ export const manifest: PluginManifest = {
readOnlyWrites: ['/favorites', '/now-playing', '/playlists', '/queue'],
},
],
// Both come from one package everywhere, which is luck rather than a rule — hence a name per manager
// rather than one canonical name. `packages.sh` records why that indirection was rejected.
//
// They are declared SEPARATELY even so, because the platform probes binaries and these two fail
// differently. Losing `ffprobe` is the quiet one: the indexer catches the spawn error and returns a
// track carrying its filename and nothing else — no title, artist, album, duration or embedded
// lyrics — then reports success. Losing `ffmpeg` costs cover art and video poster frames, which is at
// least visible. Naming both means the owner is told which of the two they are missing.
osDependencies: [
{
binary: 'ffprobe',
reason: 'Reads tags, duration and embedded lyrics. Without it every track indexes as a bare filename.',
packages: { apt: 'ffmpeg', pacman: 'ffmpeg', dnf: 'ffmpeg', brew: 'ffmpeg' },
},
{
binary: 'ffmpeg',
reason: 'Compresses cover art for phones and grabs poster frames from videos.',
packages: { apt: 'ffmpeg', pacman: 'ffmpeg', dnf: 'ffmpeg', brew: 'ffmpeg' },
},
],
};