// 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 })), };