calendar and contacts are a plugin now — plugins/caldav, its own repository like every other one. a fresh machine runs five processes instead of seven and gets a calendar by installing one plugin, which is the whole point: the platform stops carrying a python calendar server for people who may not want one. what left: the radicale sidecar, four routers, the panel apps, the dav_app_passwords table, the two screens, the two dock tiles, the permission registry entry, the settings section, and radicale from every package list. three platform things had to change first, each committed on its own:6b5c87daa plugin can be more than one screen;e0df28ffa first-party plugin can claim a reserved root path. this commit adds the third, `personal` on a plugin permission — a member granted READ must still mint and revoke their own app password, because that password is how their own phone reaches their own calendar. those rows are theirs and nobody else's. two fixes the work surfaced rather than caused. the dock pinned mountPrefix, so a multi-screen plugin pinned a tile pointing at /caldav — a route no plugin serves. and the OPTIONS carve-out was a literal list of four DAV paths; it asks the claim registry now, so it follows whoever owns the route. /calendar stays in DEFAULT_DOCK_PATHS. it is installed by default and a fresh dock that silently dropped it would read as a downgrade. docs:check earned its keep twice here: it caught spotlight still offering /settings/integrations/dav-app-passwords after the section was deleted, and the permission count going 22 -> 21. neither is greppable and both were already wrong when it ran. verified against alpha, which is still on the pre-extraction code: /.well-known/ caldav 301s, /dav/ answers 401 with the same WWW-Authenticate realm byte for byte, /api/caldav/passwords 401s, and OPTIONS reaches the DAV router rather than being answered as a CORS preflight. installed here end to end — schema applied, sidecar started, two tiles with their own artwork behind one permission. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043
141 lines
6.4 KiB
Bash
141 lines
6.4 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"
|
|
# Push notifications, CORE since 2026-08-19. Built 2026-07-31, then switched off on 2026-08-13 with
|
|
# memos and photos when it was slated to become a plugin — the only one of the three never extracted,
|
|
# so it sat mounted nowhere with its schema commented out.
|
|
#
|
|
# Core rather than a plugin because the producers are the platform's own: the job queue, an agent turn
|
|
# ending, mail arriving. A notifier the owner can uninstall is one those producers cannot rely on.
|
|
# It boots with no credentials at all — every channel reports `isConfigured() === false` and dispatch
|
|
# resolves empty — so an install that never sets an APNs key runs it as a no-op rather than crash-looping.
|
|
"officer-notify|bun|run src/servers/sidecar/notify/index.ts"
|
|
)
|
|
|
|
# Calendar and contacts are NOT here. They were core from 2026-08-19 until 2026-08-27, when caldav became
|
|
# a plugin: it declares its own sidecar, its own `radicale` dependency and its own two screens, and the
|
|
# generated ecosystem file gains an `officer-caldav` entry when it is installed. A fresh machine runs five
|
|
# processes instead of seven, and gets a calendar by installing one plugin.
|
|
|
|
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() {
|
|
as_owner "cd '$(platform_dir)' && pm2 startOrRestart '$(ecosystem_file)' --update-env" 2>&1
|
|
}
|
|
|
|
pm2_save() { as_owner "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.
|
|
#
|
|
# `systemd` is hardcoded because that IS the only path today — pm2's own "platform" argument
|
|
# has to be `launchd` on macOS instead, generating a LaunchAgent plist rather than a systemd
|
|
# unit, and running it is a separate mechanism (`launchctl load`, not "eval a printed sudo
|
|
# command"). That is real, separate work rather than a one-line swap, so this skips with a
|
|
# stated reason on macOS instead of pretending the systemd path could ever produce anything
|
|
# usable there — matches the same call this codebase already made for the Tailscale exit-node
|
|
# NIC tuning and the proxy's systemd docker-ordering fix.
|
|
pm2_enable_startup() {
|
|
if [[ "$(uname -s)" == "Darwin" ]]; then
|
|
warn "pm2 boot-persistence needs a launchd plist on macOS, not a systemd unit — not implemented yet"
|
|
return 1
|
|
fi
|
|
local cmd
|
|
cmd="$(as_owner "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() {
|
|
as_owner "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}`);
|
|
}
|
|
});'
|
|
}
|