// Shared machinery for the pm2 install profiles (ecosystem.light.config.cjs, // ecosystem.mac.light.config.cjs). // // A profile is a SUBSET of ecosystem.config.cjs, declared as names plus reasons. It never restates how // a process is launched — `script` and `args` are read from the host file at load — because a // hand-copied process list is exactly what failed here: the macOS list was written on 2026-07-28 and // within days was starting a sidecar that had been split in two and pointing at a pty entry point that // had moved. Neither failure said anything; the processes simply did not come up. // // So the rule is: ecosystem.config.cjs is the only place a launch command is written down, and a // profile only decides which of them to run. // // Two consistency checks, both of which turn a silent breakage into a loud one at load: // 1. a name the profile INCLUDES that the host no longer defines — the app was renamed or removed // 2. an app the host defines that the profile neither includes nor excludes — a new sidecar, which // must be classified deliberately rather than defaulting to absent because nobody noticed // // The second is the one that matters over time. Without it, every sidecar added to the host silently // stays out of every profile, and the profiles quietly stop meaning what their comments claim. /** * @param {object} spec * @param {string} spec.file this profile's filename, for error messages * @param {string[]} spec.include app names to run, in start order * @param {Record} spec.excluded app name → why it is not in this profile */ function defineProfile({ file, include, excluded }) { const full = require('./ecosystem.config.cjs'); const byName = new Map(full.apps.map((app) => [app.name, app])); const missing = include.filter((name) => !byName.has(name)); if (missing.length) { throw new Error( `${file}: ${missing.join(', ')} not found in ecosystem.config.cjs — the app was renamed or ` + `removed. Update this profile's include list.`, ); } const unclassified = full.apps .map((app) => app.name) .filter((name) => !include.includes(name) && !(name in excluded)); if (unclassified.length) { throw new Error( `${file}: ${unclassified.join(', ')} is in ecosystem.config.cjs but neither included nor ` + `excluded here. Add it to the include list, or to the excluded map 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. __dirname is the repo root — this file sits beside // ecosystem.config.cjs. return { apps: include.map((name) => ({ ...byName.get(name), cwd: __dirname })) }; } module.exports = { defineProfile };