no ecosystem files in git; officer-setup generates one

Deleted all four — ecosystem.config.cjs, .light., .mac.light. and the
.profile. they derived from. The repository now contains no ecosystem file at
all, and .gitignore keeps it that way.

officer-setup writes one at the end, describing exactly the six processes a core
install runs: officer, officer-anthropic-proxy, officer-agent, officer-opencode,
officer-pty and officer-headscale. No profiles, no derivation, no plugins.

The four existed because a profile has to subtract from something, so the full
list had to name every plugin's process whether or not anybody installed it —
and a test then had to assert the two files still agreed. Generating one file
removes the subtraction, the second list and the test that policed them.

It is .cjs, not the .js PM2's docs use, and that is not a preference:
package.json declares "type": "module", so a .js file here is ESM and
`module.exports` throws. PM2 require()s the config.

Sections 10 (Services) and 11 (Verify) are built on top of it — write,
startOrRestart, save, optional boot hook, then check every process is online with
a sane restart count AND that the API actually answers on PORT. A process can be
`online` and serving nothing, so the port is asked directly rather than inferred.
That completes all eleven sections.

catalogue.test.ts required both deleted files at import, so it could not even
load. Its central assertion — "the store offers exactly what light leaves out" —
has no meaning without a full list to subtract from, which is the point of the
change. Replaced by two weaker but real checks: the store must not offer a core
process, and every process it names must have a sidecar directory to run. The
second catches the same typo the old one did without needing a manifest of
everything; verified it holds for all 15 catalogue entries.

app-store/pm2.ts starts a sidecar with `--only` against this file, which now
holds core alone — so it can stop a plugin but cannot start one that was never
written in. Marked `[open]` there rather than left to be discovered: appending a
plugin's entry is the plugin system's job.

Generated one in a scratch directory and required it with node: six apps, correct
cwd on each, valid CommonJS.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-13 02:35:12 +00:00
co-authored by Claude Opus 5
parent b075f1f882
commit 62cbf510cb
10 changed files with 260 additions and 387 deletions
+110
View File
@@ -0,0 +1,110 @@
#!/bin/bash
# =============================================================================
# officer-setup — the pm2 ecosystem file, and starting the processes
# =============================================================================
#
# Definitions only.
#
# ── The ecosystem file is GENERATED, and is not in git ──
#
# There used to be four of them — ecosystem.config.cjs, .light., .mac.light. and
# a .profile. that the others derived from. A profile deriving from a full list
# means the full list has to exist, which means every plugin's process is
# described in the repository whether or not anybody installed it, and a test had
# to assert that the two files still agreed with each other.
#
# One generated file removes all of that. It describes exactly the processes this
# install runs, it is written once at setup, and nothing in git can drift from
# it. A plugin adds its own entry when it is installed.
#
# ── Why .cjs and not .js ──
#
# PM2's own convention is ecosystem.config.js, and it would be wrong here:
# package.json declares "type": "module", so a .js file in this directory is ESM
# and `module.exports` throws "module is not defined in ES module scope". PM2
# require()s the config, so the extension has to say CommonJS out loud.
[[ -n "${OFFICER_SETUP_SERVICES_LOADED:-}" ]] && return 0
OFFICER_SETUP_SERVICES_LOADED=1
ecosystem_file() { echo "$(platform_dir)/ecosystem.config.cjs"; }
# The processes a core install runs. Everything else is a plugin.
#
# `officer-pty` is node rather than bun, and that is not an oversight: it loads
# node-pty, a native module built against Node's ABI. Everything else is bun.
CORE_PROCESSES=(
"officer|bun|start"
"officer-anthropic-proxy|bun|run src/servers/sidecar/claude/index.ts"
"officer-agent|bun|run src/servers/sidecar/claude/user-instance.ts"
"officer-opencode|bun|run src/servers/sidecar/opencode/index.ts"
"officer-pty|node|src/servers/sidecar/pty/index.mjs"
"officer-headscale|bun|run src/servers/sidecar/headscale/index.ts"
)
write_ecosystem() {
local dest entry name script args
dest="$(ecosystem_file)"
{
cat <<'HEADER'
// Generated by officer-setup. Not in git, and not meant to be — it describes THIS
// install, and the next machine generates its own.
//
// `cwd` is pinned on every app for two reasons. Bun auto-loads .env from the
// working directory (and the pty sidecar does `import 'dotenv/config'`), so
// without it a process started from anywhere else comes up with no POSTGRES_URL.
// And src/servers/data-path.ts derives the install root as the PARENT of the
// working directory, so a wrong cwd does not fail — it relocates data/,
// capabilities/ and dockers/ somewhere else entirely. `assertInstallLayout`
// refuses to boot when that happens.
//
// To add a plugin later, add its entry here. Nothing derives this file from
// anything, so there is no second list to keep it agreeing with.
module.exports = {
apps: [
HEADER
for entry in "${CORE_PROCESSES[@]}"; do
IFS='|' read -r name script args <<<"$entry"
printf " { name: '%s', script: '%s', args: '%s', cwd: '%s', watch: false },\n" \
"$name" "$script" "$args" "$(platform_dir)"
done
cat <<'FOOTER'
],
};
FOOTER
} >"$dest"
chown "${USERNAME}:$(user_group)" "$dest"
return 0
}
pm2_start() {
sudo -u "$USERNAME" bash -c "cd '$(platform_dir)' && pm2 startOrRestart '$(ecosystem_file)' --update-env" 2>&1
}
pm2_save() { sudo -u "$USERNAME" pm2 save 2>&1; }
# Survive a reboot. `pm2 startup` PRINTS a command for root to run rather than
# doing it — so this runs what it prints, which is the whole point of already
# being root here.
pm2_enable_startup() {
local cmd
cmd="$(sudo -u "$USERNAME" bash -c "cd '$(platform_dir)' && pm2 startup systemd -u '$USERNAME' --hp '$USER_HOME'" 2>/dev/null | grep -E '^sudo ' | tail -1)"
[[ -z "$cmd" ]] && return 1
eval "${cmd#sudo }"
}
# One line per process: name, status, restarts.
pm2_status_lines() {
sudo -u "$USERNAME" pm2 jlist 2>/dev/null |
node -e '
let s = ""; process.stdin.on("data", (d) => (s += d)).on("end", () => {
let apps = []; try { apps = JSON.parse(s); } catch { }
for (const a of apps) {
const st = a.pm2_env?.status ?? "?";
console.log(`${a.name}|${st}|${a.pm2_env?.restart_time ?? 0}`);
}
});'
}