Files
platform/ecosystem.light.config.cjs
T
pastilhasandClaude Opus 5 1f4dbbb810 scripts: add a light install profile
OFFICER_PROFILE=light installs the same thing the macOS build does, on Linux:
the file browser, the terminal, and Claude/opencode chat. It skips the archive
extras, the sudoers entry and auto-suspend disabling, Go, Rust, PulseAudio,
cliamp, Neovim, the shell tooling and yt-dlp, brings up Postgres alone of the
five Docker services, and starts ecosystem.light.config.cjs. Unset or `full`
behaves exactly as before.

The profile changes which processes start, not which code ships — every API
route stays mounted, so features whose sidecars are absent report themselves
unavailable rather than disappearing.

ecosystem.light.config.cjs DERIVES its apps from ecosystem.config.cjs rather
than copying them, because the hand-copied Mac list was broken within days of
being written by a sidecar split in two and a pty entry point that moved, and
both failures were silent. Here a script/args change on the host propagates for
free, and two consistency checks turn the silent cases loud:

- a name the profile needs that the host no longer defines throws at load
- an app added to the host that is in neither the include list nor the annotated
  exclusion list throws, so a new sidecar cannot default to "not in the profile"
  without someone deciding

Both were tested against a mutated copy of the host ecosystem: renaming
officer-agent and adding an unclassified sidecar each throw, and the unmodified
file loads five apps.

The verification block now reads app names with node instead of grepping for
`name:` — the derived file has no literal keys to match, so a grep would have
silently verified nothing — and skips the checks for tools the profile did not
install, so a clean light run does not report Go and cliamp as missing.

Full-profile behaviour is unchanged by construction: every guard wraps the
original code in an else branch. The preamble was tested across unset, full,
light and an invalid value.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 12:41:47 +00:00

76 lines
4.1 KiB
JavaScript

// Linux light profile — the same process set as the Mac laptop build, on a Linux host.
//
// For a machine that should run the platform without the self-hosted estate around it: the file
// browser, the terminal, and Claude/opencode chat. Everything else in ecosystem.config.cjs either
// fronts a container, supervises a daemon, needs an owner-configured external service, or holds
// material a small install has no business holding.
//
// DERIVED, NOT COPIED — and that is the whole point. ecosystem.mac.config.cjs was a hand-copied
// process list, and within days of being written it was running a sidecar that had been split in two
// and pointing at a pty entry point that had moved. Both failures were silent. Here the entry points
// come from ecosystem.config.cjs, so a `script`/`args` change on the host reaches this profile for
// free, and a REMOVED or RENAMED app throws at load instead of quietly starting nothing.
//
// To change what the light profile runs, edit LIGHT_APPS. To change how an app is launched, edit
// ecosystem.config.cjs and both profiles follow.
//
// Start with: pm2 startOrRestart ecosystem.light.config.cjs
const full = require('./ecosystem.config.cjs');
const LIGHT_APPS = [
'officer', // the app itself: SPA, /api, websockets
'officer-anthropic-proxy', // holds the Anthropic credential, forwards upstream
'officer-agent', // spawns `claude` — chat is dead without it
'officer-opencode', // the alternative agent
'officer-pty', // the terminal
];
// Everything in ecosystem.config.cjs that is deliberately NOT here, with the reason. Kept as data so
// the two lists can be checked against each other below: an app that is in neither is a mistake, and
// saying so at load beats discovering it when a feature silently does nothing.
const EXCLUDED = {
'officer-vnc': 'mirrors an Xorg display with x11vnc; a light install has no desktop to mirror',
'officer-email': 'needs the mbsync/IMAP stack the light profile does not install',
'officer-music': 'the ffprobe indexer works, but a full library index is not a light-install concern',
'officer-vault': 'reverse-proxies a self-hosted Vaultwarden container',
'officer-slskd': 'supervises the slskd daemon',
'officer-headscale': 'fronts a headscale server',
'officer-transmission': 'fronts a transmission daemon',
'officer-invoiceshelf': 'fronts an InvoiceShelf container',
'officer-memos': 'needs an owner-configured Memos instance URL and token',
'officer-photos': 'needs an owner-configured Immich instance URL and API key',
'officer-caldav': 'supervises Radicale, which the light profile does not install',
'officer-notify': 'its producers are the queue and the email/agent sidecars; nothing to notify about',
'officer-wallet': 'holds seed and node credentials',
};
const byName = new Map(full.apps.map((app) => [app.name, app]));
// A name in LIGHT_APPS that the host no longer defines is the exact failure that broke the Mac file.
// Fail loudly at load rather than start a short list and look healthy.
const missing = LIGHT_APPS.filter((name) => !byName.has(name));
if (missing.length) {
throw new Error(
`ecosystem.light.config.cjs: ${missing.join(', ')} not found in ecosystem.config.cjs — ` +
`the app was renamed or removed. Update LIGHT_APPS.`,
);
}
// And an app added to the host that nobody has classified: it belongs in LIGHT_APPS or in EXCLUDED.
// Without this, a new sidecar silently defaults to "not in the light profile" and nobody decides.
const unclassified = full.apps.map((app) => app.name).filter((name) => !LIGHT_APPS.includes(name) && !(name in EXCLUDED));
if (unclassified.length) {
throw new Error(
`ecosystem.light.config.cjs: ${unclassified.join(', ')} is in ecosystem.config.cjs but neither ` +
`included nor excluded here. Add it to LIGHT_APPS or to EXCLUDED with a reason.`,
);
}
// `cwd` is pinned because Bun auto-loads .env from the working directory (and the pty sidecar does
// `import 'dotenv/config'`). Without it, starting pm2 from anywhere but the repo root silently falls
// back to PORT=5000 with no POSTGRES_URL.
module.exports = {
apps: LIGHT_APPS.map((name) => ({ ...byName.get(name), cwd: __dirname })),
};