mac: name the laptop build a profile, and derive it like the others

Renamed for parity now that Linux has a light profile too:
  scripts/setup_mac.sh       -> scripts/setup_mac_light.sh
  ecosystem.mac.config.cjs   -> ecosystem.mac.light.config.cjs

The macOS process list was still a hand-copied subset, which is the shape that
broke it: written 2026-07-28, within days it was running the Anthropic proxy
under the name officer-claude with nothing spawning `claude`, and pointing at a
pty entry point that had moved. Both silent. It now declares names and reasons
and reads script/args from ecosystem.config.cjs, so a launch change on the host
reaches it for free.

The include/exclude checks moved into ecosystem.profile.cjs rather than being
copied into the second profile — duplicating the guard rails would have repeated
the mistake they exist to catch. Both profiles were re-tested against a mutated
host ecosystem: renaming an included app and adding an unclassified sidecar each
throw in both, and an unmodified host loads five apps in both.

Kept as two files rather than collapsed into one, even though they currently
produce identical output. The exclusions do not mean the same thing: on macOS
officer-vnc CANNOT run, there being no Xorg; on a Linux light install it could
run fine and you have chosen not to. Merging them would lose that, and they
diverge the moment one profile gains something the other cannot have.

setup_mac_light.sh's verification loop now reads app names with node instead of
grepping for `name:` — the derived profile has no literal keys, so the grep
would have silently listed no services at all, which reads the same as a healthy
install with nothing configured.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-04 12:47:27 +00:00
co-authored by Claude Opus 5
parent 1f4dbbb810
commit 0a1766768d
5 changed files with 170 additions and 160 deletions
+39 -66
View File
@@ -1,75 +1,48 @@
// Linux light profile — the same process set as the Mac laptop build, on a Linux host.
// Linux light profile — the platform without the self-hosted estate around it.
//
// 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.
// For a machine that should run the file browser, the terminal and Claude/opencode chat, and nothing
// else. Paired with `OFFICER_PROFILE=light bash scripts/setup.sh`, which installs only what these
// processes need: node, bun, ffmpeg, Postgres, pm2 and the two agent CLIs.
//
// 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.
// This is a subset of ecosystem.config.cjs, not a copy of it — see ecosystem.profile.cjs for why, and
// for the two checks that make a drifted profile fail loudly instead of silently starting less than it
// claims. To change what runs, edit INCLUDE. To change HOW something runs, edit ecosystem.config.cjs
// and every profile follows.
//
// 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.
// The app itself is unchanged: every API route stays mounted, so features whose sidecars are absent
// report themselves unavailable rather than disappearing. A profile decides which processes start, not
// which code ships.
//
// Start with: pm2 startOrRestart ecosystem.light.config.cjs
const full = require('./ecosystem.config.cjs');
const { defineProfile } = require('./ecosystem.profile.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
];
module.exports = defineProfile({
file: 'ecosystem.light.config.cjs',
// 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',
};
include: [
'officer', // the app: 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
],
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 })),
};
// Excluded by CHOICE rather than by platform limits — every one of these would run on a Linux host.
// A light install simply is not running the thing behind it.
excluded: {
'officer-vnc': 'no desktop to mirror on a light install',
'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',
},
});