soulseek, extracted into plugins/soulseek — the platform half

Seventh extraction, and the first of something that was already dead. Soulseek
had been switched off since 2026-08-13: hono.ts carried the router import and
its PROTECTED_MOUNTS entry commented out, so createSidecarProxy never ran and
registerProxiedPrefix was never called; officer-slskd was not in the ecosystem
at all. Because the permission entry had been reduced to api: [], the totality
check was satisfied and the server booted clean — the UI rendered and every
request 404'd. Worth naming: the boot check proves every MOUNTED prefix has a
permission, and is silent about a feature that mounts nothing.

Also closes a live data hazard: schema.ts had the soulseek export commented
out, so the three tables were absent from the drizzle barrel and bun db:push
would have dropped soulseek_favorites, soulseek_browse_snapshots and
soulseek_browse_dirs on any server holding them. rebuild-soulseek-tree.ts
documented the workaround rather than the problem; deleted with the rest.

The connection stays in core service_connections under service 'slskd' while
the mount is now /api/soulseek. Those differ on purpose.

Test fixtures: slskd was canonical in three app-store tests, the third
extraction running to break them. installer.test.ts now builds a literal
fixture instead of reaching into the catalogue — it pins planSteps, which is
installer logic, and no catalogue entry is both provisioned and members:'none'
any more. catalogue.test.ts and members.test.ts re-anchored to vnc/notify/gitea.

Unrelated but mine: check:theme caught 5 palette classes I introduced in
CommandHQ earlier tonight against a baseline of 0. Converted to bg-success /
bg-warning. check:theme is not part of bun test, which is why the green suite
never showed it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-19 02:55:35 +00:00
co-authored by Claude Opus 5
parent c4b262dc51
commit b196c9cb55
52 changed files with 41 additions and 5807 deletions
+5 -4
View File
@@ -107,10 +107,11 @@ describe('member provisioning is declared for every entry', () => {
});
it('never promises accounts for a single-tenant daemon', () => {
// slskd has no user concept; claiming otherwise would make the installer try to create accounts
// against an API that does not exist. (Transmission was the other example until 2026-08-18, when it
// became `plugins/transmission/`.)
for (const id of ['slskd']) expect(byId(id)!.members).toBe('none');
// A daemon with no user concept; claiming otherwise would make the installer try to create accounts
// against an API that does not exist. Transmission was an example until 2026-08-18 and slskd until
// 2026-08-19, when they became `plugins/transmission/` and `plugins/soulseek/` — hence the list
// rather than a single id: it must stay non-empty, or this assertion passes by iterating nothing.
for (const id of ['vnc', 'notify', 'gitea']) expect(byId(id)!.members).toBe('none');
});
// A test asserting the vault is invite-only lived here until 2026-08-18, when it left to become
-21
View File
@@ -226,27 +226,6 @@ export const CATALOGUE: CatalogueEntry[] = [
// Deleting the entry is REQUIRED, not tidying — see the note where photos used to sit, and the trap in
// `EXTRACTING-A-PLUGIN.md`. A leftover entry makes the permission permanently `unavailable`, because
// `permissionAvailability()` derives from `sidecar_installs` and a plugin never gets a row there.
{
id: 'slskd',
ui: {
name: 'Soulseek',
image: '/plugins/slskd/icon.png',
color: '#ffffff',
rootRoute: '/soulseek',
routes: ['/soulseek'],
},
process: 'officer-slskd',
label: 'Soulseek',
summary: 'slskd — search and download from the Soulseek network',
members: 'none',
modes: ['existing', 'provisioned'],
permission: 'soulseek',
composeTemplate: 'slskd',
existingFields: [
{ key: 'url', label: 'slskd URL', type: 'url', required: true },
{ key: 'secret', label: 'API key', type: 'secret', required: true },
],
},
{
id: 'caldav',
ui: {
+8 -2
View File
@@ -7,7 +7,13 @@ import { byId } from './catalogue';
// when something goes wrong: resume, blocking, and not doing work twice.
const jellyfin = byId('jellyfin')!; // existing|provisioned, members: 'accounts'
const slskd = byId('slskd')!; // existing|provisioned, members: 'none'
// A LITERAL, not `byId(...)`. This was `byId('slskd')` until slskd became `plugins/soulseek/`, and
// before that the same assertion had to be re-pointed when photos and transmission left. No catalogue
// entry today is both `provisioned` and `members: 'none'`, so there is nothing left to borrow — and
// borrowing was the mistake anyway: this pins `planSteps`, which is installer logic, and it should not
// break because an unrelated service was extracted. Shaped from jellyfin so the rest of the entry stays
// realistic.
const noMemberConcept = { ...byId('jellyfin')!, id: 'test-single-tenant', members: 'none' as const };
const configOnly = byId('notify')!; // config only
/** Records what was actually called, so a test can assert on absence as well as presence. */
@@ -46,7 +52,7 @@ describe('planSteps', () => {
it('omits the members step for a service with no user concept', () => {
// Otherwise a Transmission install reports a members step that did nothing, which reads as a
// silent failure to anyone debugging why a member has no access.
expect(planSteps(slskd, 'provisioned')).not.toContain('members');
expect(planSteps(noMemberConcept, 'provisioned')).not.toContain('members');
expect(planSteps(jellyfin, 'provisioned')).toContain('members');
});
+2 -2
View File
@@ -12,13 +12,13 @@ describe('what adding a member would do', () => {
it('predicts the outcome per service without contacting anything', () => {
expect(plannedOutcome(byId('jellyfin')!)).toBe('provisioned');
expect(plannedOutcome(inviteOnly)).toBe('invited');
expect(plannedOutcome(byId('slskd')!)).toBe('not-applicable');
expect(plannedOutcome(byId('vnc')!)).toBe('not-applicable');
});
});
describe('which installed services have work to do', () => {
it('skips single-tenant daemons entirely', () => {
const ids = servicesNeedingMemberProvision(['jellyfin', 'slskd', 'memos']).map((e) => e.id);
const ids = servicesNeedingMemberProvision(['jellyfin', 'vnc', 'memos']).map((e) => e.id);
expect(ids.sort()).toEqual(['jellyfin', 'memos']);
});