Files
platform/ecosystem-files/ecosystem.profile.cjs
T
pastilhasandClaude Opus 5 3f071c0b24 the install root is derived, not configured
Seven variables out of .env. DATA_PATH, OFFICER_ITEMS_DIR and HOME_DIR are gone
from the code entirely; PUBLIC_URL, PUBLIC_BUILD_ENV, JWT_SECRET and
VAULT_STORE_KEY are no longer written by the setup script.

data-path.ts now derives OFFICER_ROOT as dirname(process.cwd()), with data/,
capabilities/ and dockers/ as fixed names under it. The direction used to run the
other way — DATA_PATH from env, then OFFICER_ROOT = dirname(DATA_PATH) in
app-store/paths.ts — which meant three environment variables that had to agree
with each other and with the tree on disk.

Eight files re-read process.env.DATA_PATH independently, each with its own
`?? cwd()/data` fallback. They import the one value now, which is what made
removing it safe: otherwise each would have derived its own and drifted.

Three things this turned up.

The cwd pin in ecosystem.profile.cjs was broken. It set `cwd: __dirname` under a
comment asserting "__dirname is the repo root — this file sits beside
ecosystem.config.cjs", which stopped being true when these files moved into
ecosystem-files/. It walks up to the platform's package.json now, which holds
wherever the file lives. That was a live bug before this change and a load-bearing
one after it, since cwd now decides where the install is.

assertInstallLayout joins the other two boot assertions. A wrong cwd does not
error — it computes a plausible root somewhere else and writes managed homes and
agent runs into it, so the install looks empty and the data looks lost with
nothing naming the cause. It throws before serve(), first of the three, because a
wrong answer there makes the other two check the wrong files.

getOwnerHomeDir captures homedir() once at module load rather than per call.
Measured on bun 1.3.10: both os.homedir() and os.userInfo().homedir return $HOME
when set rather than reading passwd, and user-instance.ts assigns process.env.HOME
on its way to spawning an agent. A lazy read would have returned the owner's home
on the first call and a member's afterwards. data-path.ts imports only node
builtins, so it is evaluated before any of that runs.

JWT_SECRET and VAULT_STORE_KEY leaving .env means an install made by this script
does not boot — jwt.ts throws at module load without one. That is the agreed
sequencing: they move to the SQLite store (docs/secret-store.md), and writing them
here meanwhile would create a second origin for a secret the store then has to be
reconciled with. Said plainly in .env.example and in lib/env.sh rather than left
to be discovered.

Not typechecked: node_modules is empty here and installs are frozen. Every edited
file parses under `bun build --no-bundle`; the profile loads and pins the right
cwd; assertInstallLayout was exercised from both the repo and /tmp; the setup
section was run and writes five variables. Prettier was NOT run — 3.9.6 via bunx
is not the pinned resolution and reformatted unrelated unions and line wraps in
six files, so those were reverted and the edits re-applied by hand.

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

86 lines
4.2 KiB
JavaScript

// 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<string,string>} spec.excluded app name → why it is not in this profile
*/
// The directory holding the platform's package.json, found by walking up from this file. Independent of
// where in the tree this config is kept, and of where pm2 was invoked from.
function repoRoot() {
const { existsSync, readFileSync } = require('node:fs');
const { dirname, join } = require('node:path');
let dir = __dirname;
for (;;) {
const manifest = join(dir, 'package.json');
if (existsSync(manifest)) {
try {
if (JSON.parse(readFileSync(manifest, 'utf8')).name === 'officer') return dir;
} catch {
// Unparseable is not ours; keep walking.
}
}
const up = dirname(dir);
if (up === dir) throw new Error("ecosystem.profile.cjs: could not find the platform's package.json above " + __dirname);
dir = up;
}
}
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.
//
// It also decides where the install is. src/servers/data-path.ts derives OFFICER_ROOT as the PARENT of
// the working directory, and data/, capabilities/ and dockers/ hang off that — so a wrong cwd does not
// fail, it relocates the whole install. `assertInstallLayout` is the boot check that catches it.
//
// This was `__dirname`, with a comment asserting "__dirname is the repo root — this file sits beside
// ecosystem.config.cjs". That stopped being true the moment these files were moved into
// ecosystem-files/, and nothing said so. Found by walking up to the package.json instead, which is
// true wherever this file ends up living.
return { apps: include.map((name) => ({ ...byName.get(name), cwd: repoRoot() })) };
}
module.exports = { defineProfile };