cliamp moves into the plugin, and the platform loses its last music file

The owner read the code and asked why `plugins/music/api/router.ts` was three
lines importing `@@/api/music/router` — platform code that knows the string
'music'. He was right, and tracing it found the justification was hollow.

The chain: server.tsx:20 imported the cliamp relay's two exports, which are
used only on commented-out lines; so the relay's functions were never invoked;
so its call to getMusicServerWsUrl never ran; and the file's other export,
getMusicServerUrl, had no consumers at all. A dead import held a music-named
file in the platform, and I documented that as a "seam" last night after
checking the import existed and stopping there.

Everything cliamp now lives in plugins/music/cliamp/:

  sidecar/music/{cliamp-ws,pulse-audio}.ts, asoundrc, the test
  api/cliamp/relay.ts
  apps/FileBrowser/{CliampPanel,AudioStreamPlayer}.tsx

src/servers/sidecar/music/, src/servers/api/cliamp/ and src/servers/api/music/
are gone. server.tsx has no cliamp import, provider name, handler entry or
route. The platform contains no file named for music or cliamp.

Two of the things that moved were live, not inert.

The file browser's `Play` context-menu item, on any audio file or folder, set
?play= and rendered a cliamp terminal pointed at /api/cliamp/ws — a route that
upgraded into a handlers entry that was commented out, so handlers[provider]!
asserted non-null on undefined. Using that menu item crashed the socket
handler. Removed: the action, the layout, the panel wiring and both menu
entries. Verified the routes now 404 rather than crash.

That closed the totality drift as a side effect. server.tsx's route table and
its handlers map agree again for the first time since 2026-08-13, and
registry.test.ts now asserts it rather than pinning the hole.

The proxy is built in the plugin now, and its prefix is DERIVED. It was the
literal '/api/music', which the proxy uses to strip characters off the path —
correct only because mountPrefix returns /music for a first-party publisher.
The same plugin published by anyone else mounts at /api/p/<publisher>/music and
would have forwarded /alice/music/stream to a sidecar expecting /stream. A
latent bug only third parties would ever hit, and a quiet violation of the rule
that mountPrefix is the one function allowed to know about provenance. Offscale
has the identical hardcode and still needs it.

Still open there: appName is passed as a literal, because a plugin's router
cannot see its own directory name — the platform imports the module and reads
`router`, so there is nowhere to inject it. The fix is a factory the installer
calls with the plugin's identity.

Plugin backend coupling is down to 7 imports, all of them "a plugin talks to
its host": data-path, sidecar/connect, sidecar/protocol, officer-url, the
manifest type, officerdb/db and the users.id FK. Nothing music-shaped left.

bunx tsgo clean. 797 tests, 787 pass, same 7 pre-existing failures. Verified
live: manifest 200, favorites 200, stream 206, /api/cliamp/ws 404.
This commit is contained in:
2026-08-15 13:52:53 +00:00
parent 8bfcd40bd2
commit a9bf51407e
25 changed files with 504 additions and 398 deletions
+8 -21
View File
@@ -17,7 +17,6 @@ import { terminalWebsocket } from './servers/api/terminal/websocket';
import { chatWebsocket } from './servers/api/chat/websocket';
import { taskRunnerWebsocket } from './servers/api/tasks/task-executor';
import { pipelineWebsocket } from './servers/api/tasks/pipeline-executor';
import { cliampWebsocket, cliampAudioWebsocket } from './servers/api/cliamp/relay';
// import { desktopWebsocket } from './servers/api/desktop/websocket';
// import { vaultWebsocket, upgradeVaultWs } from './servers/api/vault/websocket';
import officerWeb from './apps/officer-web/index.gen.html';
@@ -38,16 +37,7 @@ type WSData = {
userId: number;
email: string;
username: string;
provider:
| 'terminal'
| 'chat'
| 'task-runner'
| 'pipeline'
| 'cliamp'
| 'cliamp-audio'
| 'desktop'
| 'vault'
| 'sidecar';
provider: 'terminal' | 'chat' | 'task-runner' | 'pipeline' | 'desktop' | 'vault' | 'sidecar';
sessionId?: string;
cwd?: string;
command?: string;
@@ -137,8 +127,6 @@ const handlers: Record<string, any> = {
chat: chatWebsocket,
'task-runner': taskRunnerWebsocket,
pipeline: pipelineWebsocket,
// cliamp: cliampWebsocket,
// 'cliamp-audio': cliampAudioWebsocket,
// desktop: desktopWebsocket,
// vault: vaultWebsocket,
sidecar: sidecarWebsocket,
@@ -264,15 +252,15 @@ console.log(
async function upgradeWs(
req: Request,
server: any,
provider: 'terminal' | 'chat' | 'task-runner' | 'pipeline' | 'cliamp' | 'cliamp-audio' | 'desktop',
provider: 'terminal' | 'chat' | 'task-runner' | 'pipeline' | 'desktop',
) {
const token = new URL(req.url).searchParams.get('token');
if (!token) return new Response('Unauthorized', { status: 401 });
try {
// Same resolver as the two HTTP doors, so a key that works against /api works here too — a music app
// holding one needs cliamp and cliamp-audio, and a socket that only understood JWTs would have made
// "signed in" and "can play audio" two different questions. `jti` is absent on a key, so the
// Same resolver as the two HTTP doors, so a key that works against /api works here too — a mobile app
// holding one gets the same doors as a browser session, and a socket that only understood JWTs would
// have made "signed in" and "can open this" two different questions. `jti` is absent on a key, so the
// blacklist below simply does not apply to one; its revocation is a column, checked in the lookup.
const user = await resolveAuthToken(token);
if (!user) return new Response('Unauthorized', { status: 401 });
@@ -393,8 +381,6 @@ const server = serve({
'/api/tasks/pipeline/ws': (req, server) => upgradeWs(req, server, 'pipeline'),
'/api/terminal/ws': (req, server) => upgradeWs(req, server, 'terminal'),
'/api/chat/ws': (req, server) => upgradeWs(req, server, 'chat'),
'/api/cliamp/ws': (req, server) => upgradeWs(req, server, 'cliamp'),
'/api/cliamp/audio/ws': (req, server) => upgradeWs(req, server, 'cliamp-audio'),
// '/api/desktop/ws': (req, server) => upgradeWs(req, server, 'desktop'),
// CalDAV/CardDAV. These live OUTSIDE /api because DAV clients are given a bare domain and probe
// fixed, spec-defined paths — `/.well-known/caldav` unauthenticated, before they hold any
@@ -514,7 +500,8 @@ void (async () => {
})();
// PulseAudio and the `virtual_out` sink used to be set up here, at every boot of a process that has no
// audio responsibilities. They belong to the music sidecar, which owns both cliamp halves now
// (sidecar/music/pulse-audio.ts).
// audio responsibilities. Every trace of cliamp left on 2026-08-15 — the relay, the two socket routes and
// the provider names — and now lives in `plugins/music/cliamp/`, parked rather than working. Bringing it
// back means a plugin owning a websocket, which is a platform gap, not a music one.
// Pi check/install is handled by bootstrap.ts (imported above)