#!/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}`); } });' }