Files
pastilhasandClaude Opus 5 bbc60b34ac headscale leaves the baseline, and offscale gets a design doc
first step of extracting headscale into a plugin. CORE_PROCESSES is five now,
and the catalogue.test CORE[] mirror follows it — not optional, since that list
asserts "the catalogue must not offer a core process" and would have blocked
adding offscale to the catalogue later.

the local generated ecosystem file lost its entry too, and officer-headscale was
stopped and deleted from pm2 by hand. the platform still mounts /api/headscale
and still declares the headscale and vpn capabilities, so the feature is
present-but-unavailable rather than gone.

docs/offscale-plugin.md is a live document for the rest of it. what it records
that nothing else does: core is now `officer` alone and everything else is a
plugin; routes are /api/<app-name> for ours and /api/p/<creator>/<app-name> for
third parties, derived by one function so the two can never become two systems;
tables stay in public with an app-name prefix; mounting becomes genuinely
dynamic, which retires the "every route stays mounted" premise and relocates
assertCapabilityTotality from a boot check to a per-mount transaction.

it also records a rejected experiment with evidence — a postgres schema per
plugin works completely, including cross-schema FK, idempotent push and
DROP SCHEMA CASCADE as uninstall — and the reason not to: drizzle-kit 0.31.8
needs schemaFilter naming every schema, contradicting its own docs, and without
it push reports "No changes detected" and creates nothing. a plugin install that
reports success and makes no tables is the exact failure shape we have hit three
times this week.

and /api/vpn is dead: no caller in the mobile monorepo, none in the web app, no
grants in the database. offscale is permanently standalone, so it never comes
back. the invite flow is unaffected — the phone claims from the Companion, not
from officer.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 17:37:55 +00:00

115 lines
4.5 KiB
Bash

#!/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.
#
# `officer-claude-code` was `officer-agent` until 2026-08-13. The old name said
# nothing about what it runs, and it sits beside officer-anthropic-proxy — which
# is a different process doing a different job — so "the agent" was ambiguous
# exactly where it mattered. It spawns `claude`; the name says so now.
CORE_PROCESSES=(
"officer|bun|start"
"officer-anthropic-proxy|bun|run src/servers/sidecar/claude/index.ts"
"officer-claude-code|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"
)
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}`);
}
});'
}