scripts: add a light install profile
OFFICER_PROFILE=light installs the same thing the macOS build does, on Linux: the file browser, the terminal, and Claude/opencode chat. It skips the archive extras, the sudoers entry and auto-suspend disabling, Go, Rust, PulseAudio, cliamp, Neovim, the shell tooling and yt-dlp, brings up Postgres alone of the five Docker services, and starts ecosystem.light.config.cjs. Unset or `full` behaves exactly as before. The profile changes which processes start, not which code ships — every API route stays mounted, so features whose sidecars are absent report themselves unavailable rather than disappearing. ecosystem.light.config.cjs DERIVES its apps from ecosystem.config.cjs rather than copying them, because the hand-copied Mac list was broken within days of being written by a sidecar split in two and a pty entry point that moved, and both failures were silent. Here a script/args change on the host propagates for free, and two consistency checks turn the silent cases loud: - a name the profile needs that the host no longer defines throws at load - an app added to the host that is in neither the include list nor the annotated exclusion list throws, so a new sidecar cannot default to "not in the profile" without someone deciding Both were tested against a mutated copy of the host ecosystem: renaming officer-agent and adding an unclassified sidecar each throw, and the unmodified file loads five apps. The verification block now reads app names with node instead of grepping for `name:` — the derived file has no literal keys to match, so a grep would have silently verified nothing — and skips the checks for tools the profile did not install, so a clean light run does not report Go and cliamp as missing. Full-profile behaviour is unchanged by construction: every guard wraps the original code in an else branch. The preamble was tested across unset, full, light and an invalid value. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
// Linux light profile — the same process set as the Mac laptop build, on a Linux host.
|
||||
//
|
||||
// For a machine that should run the platform without the self-hosted estate around it: the file
|
||||
// browser, the terminal, and Claude/opencode chat. Everything else in ecosystem.config.cjs either
|
||||
// fronts a container, supervises a daemon, needs an owner-configured external service, or holds
|
||||
// material a small install has no business holding.
|
||||
//
|
||||
// DERIVED, NOT COPIED — and that is the whole point. ecosystem.mac.config.cjs was a hand-copied
|
||||
// process list, and within days of being written it was running a sidecar that had been split in two
|
||||
// and pointing at a pty entry point that had moved. Both failures were silent. Here the entry points
|
||||
// come from ecosystem.config.cjs, so a `script`/`args` change on the host reaches this profile for
|
||||
// free, and a REMOVED or RENAMED app throws at load instead of quietly starting nothing.
|
||||
//
|
||||
// To change what the light profile runs, edit LIGHT_APPS. To change how an app is launched, edit
|
||||
// ecosystem.config.cjs and both profiles follow.
|
||||
//
|
||||
// Start with: pm2 startOrRestart ecosystem.light.config.cjs
|
||||
|
||||
const full = require('./ecosystem.config.cjs');
|
||||
|
||||
const LIGHT_APPS = [
|
||||
'officer', // the app itself: SPA, /api, websockets
|
||||
'officer-anthropic-proxy', // holds the Anthropic credential, forwards upstream
|
||||
'officer-agent', // spawns `claude` — chat is dead without it
|
||||
'officer-opencode', // the alternative agent
|
||||
'officer-pty', // the terminal
|
||||
];
|
||||
|
||||
// Everything in ecosystem.config.cjs that is deliberately NOT here, with the reason. Kept as data so
|
||||
// the two lists can be checked against each other below: an app that is in neither is a mistake, and
|
||||
// saying so at load beats discovering it when a feature silently does nothing.
|
||||
const EXCLUDED = {
|
||||
'officer-vnc': 'mirrors an Xorg display with x11vnc; a light install has no desktop to mirror',
|
||||
'officer-email': 'needs the mbsync/IMAP stack the light profile does not install',
|
||||
'officer-music': 'the ffprobe indexer works, but a full library index is not a light-install concern',
|
||||
'officer-vault': 'reverse-proxies a self-hosted Vaultwarden container',
|
||||
'officer-slskd': 'supervises the slskd daemon',
|
||||
'officer-headscale': 'fronts a headscale server',
|
||||
'officer-transmission': 'fronts a transmission daemon',
|
||||
'officer-invoiceshelf': 'fronts an InvoiceShelf container',
|
||||
'officer-memos': 'needs an owner-configured Memos instance URL and token',
|
||||
'officer-photos': 'needs an owner-configured Immich instance URL and API key',
|
||||
'officer-caldav': 'supervises Radicale, which the light profile does not install',
|
||||
'officer-notify': 'its producers are the queue and the email/agent sidecars; nothing to notify about',
|
||||
'officer-wallet': 'holds seed and node credentials',
|
||||
};
|
||||
|
||||
const byName = new Map(full.apps.map((app) => [app.name, app]));
|
||||
|
||||
// A name in LIGHT_APPS that the host no longer defines is the exact failure that broke the Mac file.
|
||||
// Fail loudly at load rather than start a short list and look healthy.
|
||||
const missing = LIGHT_APPS.filter((name) => !byName.has(name));
|
||||
if (missing.length) {
|
||||
throw new Error(
|
||||
`ecosystem.light.config.cjs: ${missing.join(', ')} not found in ecosystem.config.cjs — ` +
|
||||
`the app was renamed or removed. Update LIGHT_APPS.`,
|
||||
);
|
||||
}
|
||||
|
||||
// And an app added to the host that nobody has classified: it belongs in LIGHT_APPS or in EXCLUDED.
|
||||
// Without this, a new sidecar silently defaults to "not in the light profile" and nobody decides.
|
||||
const unclassified = full.apps.map((app) => app.name).filter((name) => !LIGHT_APPS.includes(name) && !(name in EXCLUDED));
|
||||
if (unclassified.length) {
|
||||
throw new Error(
|
||||
`ecosystem.light.config.cjs: ${unclassified.join(', ')} is in ecosystem.config.cjs but neither ` +
|
||||
`included nor excluded here. Add it to LIGHT_APPS or to EXCLUDED 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.
|
||||
module.exports = {
|
||||
apps: LIGHT_APPS.map((name) => ({ ...byName.get(name), cwd: __dirname })),
|
||||
};
|
||||
+93
-14
@@ -1,7 +1,25 @@
|
||||
#!/bin/bash
|
||||
# Officer — full host dependency setup
|
||||
# Officer — host dependency setup
|
||||
# Run once on a fresh Ubuntu/Debian host before launching the server.
|
||||
# Usage: bash scripts/setup.sh
|
||||
#
|
||||
# Usage:
|
||||
# bash scripts/setup.sh # full server install
|
||||
# OFFICER_PROFILE=light bash scripts/setup.sh # light install
|
||||
#
|
||||
# PROFILES
|
||||
# full Everything: the self-hosted estate, the remote desktop, the music/audio stack, the shell
|
||||
# and editor tooling. What a dedicated Officer server wants.
|
||||
# light The same process set as the macOS build — the file browser, the terminal, and
|
||||
# Claude/opencode chat — on a Linux host. Installs only what those need: node, bun, ffmpeg,
|
||||
# Postgres, pm2 and the two agent CLIs, then starts ecosystem.light.config.cjs.
|
||||
#
|
||||
# Skipped by `light`: archive extras, the sudoers entry and auto-suspend disabling, Go, Rust,
|
||||
# PulseAudio, cliamp, Neovim, the shell tooling (starship/oh-my-zsh/eza/lazygit), yt-dlp, and
|
||||
# the remote desktop. Of the Docker services only Postgres is brought up.
|
||||
#
|
||||
# The app itself is identical — every API route stays mounted, so the features whose sidecars
|
||||
# are not running report themselves unavailable rather than disappearing. A profile changes
|
||||
# which processes start, not which code ships.
|
||||
|
||||
set -e
|
||||
|
||||
@@ -14,9 +32,21 @@ ok() { echo -e " ${GREEN}✓${NC} $1"; }
|
||||
warn() { echo -e " ${YELLOW}!${NC} $1"; }
|
||||
fail() { echo -e " ${RED}✗${NC} $1"; }
|
||||
skip() { echo -e " - $1 (already installed)"; }
|
||||
omit() { echo -e " - $1 (skipped: light profile)"; }
|
||||
|
||||
has() { command -v "$1" &>/dev/null; }
|
||||
|
||||
OFFICER_PROFILE="${OFFICER_PROFILE:-full}"
|
||||
case "$OFFICER_PROFILE" in
|
||||
full|light) ;;
|
||||
*) echo "Unknown OFFICER_PROFILE '$OFFICER_PROFILE' — expected 'full' or 'light'." >&2; exit 2 ;;
|
||||
esac
|
||||
is_light() { [ "$OFFICER_PROFILE" = "light" ]; }
|
||||
|
||||
# Which pm2 process list this install starts and verifies. ecosystem.light.config.cjs derives its apps
|
||||
# from ecosystem.config.cjs, so the two cannot disagree about how a process is launched.
|
||||
if is_light; then ECOSYSTEM_FILE="ecosystem.light.config.cjs"; else ECOSYSTEM_FILE="ecosystem.config.cjs"; fi
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
|
||||
|
||||
@@ -228,6 +258,10 @@ fi
|
||||
echo ""
|
||||
echo "── Archive utilities (optional) ──"
|
||||
|
||||
if is_light; then
|
||||
omit "7z, unrar"
|
||||
else
|
||||
|
||||
ARCHIVE_PKGS=()
|
||||
|
||||
# p7zip
|
||||
@@ -252,6 +286,7 @@ if [ ${#ARCHIVE_PKGS[@]} -gt 0 ]; then
|
||||
install_pkg "${ARCHIVE_PKGS[@]}" || warn "Some archive packages may need non-free repos"
|
||||
ok "Installed: ${ARCHIVE_PKGS[*]}"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ─── 3. ffmpeg ─────────────────────────────────────────────────────────────────
|
||||
echo ""
|
||||
@@ -268,6 +303,13 @@ fi
|
||||
echo ""
|
||||
echo "── Sudoers (Linux user isolation) ──"
|
||||
|
||||
# Both of these are server decisions. A passwordless sudoers entry is a security posture a small
|
||||
# install should opt into deliberately, and a laptop-shaped host wants to keep suspending — the macOS
|
||||
# build does neither, so `light` does neither.
|
||||
if is_light; then
|
||||
omit "sudoers entry, auto-suspend disabling"
|
||||
else
|
||||
|
||||
SERVICE_USER="$(whoami)"
|
||||
SUDOERS_FILE="/etc/sudoers.d/officer-service"
|
||||
|
||||
@@ -344,6 +386,7 @@ if [ "$LOGIND_CHANGED" = "1" ]; then
|
||||
else
|
||||
skip "logind auto-suspend settings"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ─── 5. Node.js 22 (system-wide) ─────────────────────────────────────────────
|
||||
echo ""
|
||||
@@ -433,6 +476,15 @@ else
|
||||
skip "bun symlink at /usr/local/bin/bun"
|
||||
fi
|
||||
|
||||
# Sections 7-13 are one block because `light` skips all of them. Go and PulseAudio exist to build and
|
||||
# feed cliamp; Rust has no consumer left in the tree; Neovim, the shell tooling and yt-dlp are host
|
||||
# comforts and capability dependencies rather than anything the app needs to serve a file browser, a
|
||||
# terminal and a chat.
|
||||
if is_light; then
|
||||
echo ""
|
||||
omit "Go, Rust, PulseAudio, cliamp, Neovim, shell tooling (starship/oh-my-zsh/eza/lazygit), yt-dlp"
|
||||
else
|
||||
|
||||
# ─── 7. Go ─────────────────────────────────────────────────────────────────────
|
||||
echo ""
|
||||
echo "── Go ──"
|
||||
@@ -735,6 +787,8 @@ else
|
||||
esac
|
||||
fi
|
||||
|
||||
fi # end of the light-profile skip for sections 7-13
|
||||
|
||||
# ─── 14. npm global packages (user-local) ───────────────────────────────────
|
||||
echo ""
|
||||
echo "── npm global packages (user-local) ──"
|
||||
@@ -813,9 +867,15 @@ elif [ -f "$PROJECT_DIR/.env" ]; then
|
||||
fi
|
||||
|
||||
if [ "$GENERATE_ENV" = true ]; then
|
||||
# Run setup-dockers.sh and capture its stdout output
|
||||
# Run setup-dockers.sh and capture its stdout output. Postgres is the only one of the five the light
|
||||
# profile needs — it is the platform's only database. NPM, Mailhog, Redis and SearXNG all serve parts
|
||||
# of the estate a light install is not running.
|
||||
echo " Setting up Docker Compose services..."
|
||||
if is_light; then
|
||||
DOCKER_OUTPUT=$(SETUP_DOCKER_SERVICES=2 bash "$SCRIPT_DIR/setup-dockers.sh")
|
||||
else
|
||||
DOCKER_OUTPUT=$(bash "$SCRIPT_DIR/setup-dockers.sh")
|
||||
fi
|
||||
|
||||
# Parse output from setup-dockers.sh
|
||||
COMPOSE_DIR=$(echo "$DOCKER_OUTPUT" | grep '^COMPOSE_DIR=' | cut -d= -f2-)
|
||||
@@ -893,6 +953,12 @@ echo "── Remote Desktop (Ubuntu Desktop + VNC) ──"
|
||||
# reboot. setup-desktop.sh is idempotent throughout — every step either no-ops or is individually
|
||||
# guarded — so letting it run each time converges a partially configured host instead of trusting a
|
||||
# proxy for state it never actually checked.
|
||||
# The light profile does not run officer-vnc, so there is nothing to mirror. This is the single most
|
||||
# expensive section — it pulls the whole ubuntu-desktop meta-package — and the one most clearly outside
|
||||
# "file browser, terminal, chat".
|
||||
if is_light; then
|
||||
omit "remote desktop (ubuntu-desktop, GDM, x11vnc, Brave)"
|
||||
else
|
||||
case $PM in
|
||||
apt)
|
||||
bash "$SCRIPT_DIR/setup-desktop.sh"
|
||||
@@ -901,6 +967,7 @@ case $PM in
|
||||
warn "Remote desktop setup is Ubuntu/Debian only — skipping"
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# ─── 18. project initialization ──────────────────────────────────────────────
|
||||
echo ""
|
||||
@@ -932,14 +999,14 @@ echo "── Services (pm2) ──"
|
||||
|
||||
# Installing pm2 is not the same as running anything with it. Without this the setup finishes with
|
||||
# every dependency in place and nothing actually listening — and the sidecars matter beyond the web
|
||||
# app: /desktop returns 503 until officer-vnc is connected, and chat needs officer-claude.
|
||||
if ! has pm2 || [ ! -f "$PROJECT_DIR/ecosystem.config.cjs" ]; then
|
||||
warn "Skipping service start (pm2 or ecosystem.config.cjs missing)"
|
||||
# app: /desktop returns 503 until officer-vnc is connected, and chat needs officer-agent.
|
||||
if ! has pm2 || [ ! -f "$PROJECT_DIR/$ECOSYSTEM_FILE" ]; then
|
||||
warn "Skipping service start (pm2 or $ECOSYSTEM_FILE missing)"
|
||||
else
|
||||
# startOrRestart also picks up apps added to the ecosystem since the last run. These are all
|
||||
# fork-mode apps, so reload would buy nothing over restart.
|
||||
echo " Starting Officer and its sidecars..."
|
||||
if (cd "$PROJECT_DIR" && pm2 startOrRestart ecosystem.config.cjs); then
|
||||
echo " Starting Officer and its sidecars ($ECOSYSTEM_FILE)..."
|
||||
if (cd "$PROJECT_DIR" && pm2 startOrRestart "$ECOSYSTEM_FILE"); then
|
||||
ok "services started"
|
||||
else
|
||||
fail "pm2 could not start the services — check 'pm2 logs'"
|
||||
@@ -988,14 +1055,18 @@ check gcc
|
||||
|
||||
echo ""
|
||||
echo "Dev tools:"
|
||||
# Only the ones the light profile actually installs are checked under it — reporting Go and cliamp as
|
||||
# NOT FOUND on an install that deliberately skipped them makes a clean run look broken.
|
||||
if ! is_light; then
|
||||
check go
|
||||
check rustc
|
||||
check cargo
|
||||
check nvim
|
||||
check zsh
|
||||
check starship
|
||||
check lazygit
|
||||
check eza
|
||||
fi
|
||||
check zsh
|
||||
check rg
|
||||
check fd
|
||||
check jq
|
||||
@@ -1005,12 +1076,14 @@ check tree
|
||||
check btop
|
||||
check sqlite3
|
||||
|
||||
if ! is_light; then
|
||||
echo ""
|
||||
echo "Audio (cliamp):"
|
||||
check pulseaudio
|
||||
check parec
|
||||
check pactl
|
||||
check cliamp
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "AI agents:"
|
||||
@@ -1027,18 +1100,24 @@ check 7z
|
||||
check unrar
|
||||
check pgrep
|
||||
check fuser
|
||||
check yt-dlp
|
||||
if ! is_light; then check yt-dlp; fi
|
||||
|
||||
echo ""
|
||||
echo "═══════════════════════════════════════════"
|
||||
echo " Setup complete!"
|
||||
echo "═══════════════════════════════════════════"
|
||||
# Services actually running is a better signal than the binaries being present. The list comes from
|
||||
# ecosystem.config.cjs so it cannot drift as sidecars are added.
|
||||
if has pm2 && [ -f "$PROJECT_DIR/ecosystem.config.cjs" ]; then
|
||||
# Services actually running is a better signal than the binaries being present. The list comes from the
|
||||
# ecosystem this install started, so it cannot drift as sidecars are added.
|
||||
#
|
||||
# Read with node rather than grepped: ecosystem.light.config.cjs derives its apps from the full file
|
||||
# and has no literal `name:` keys to match, so a grep would silently verify nothing. Loading it also
|
||||
# exercises its own consistency checks, which is worth doing here.
|
||||
if has pm2 && [ -f "$PROJECT_DIR/$ECOSYSTEM_FILE" ]; then
|
||||
echo ""
|
||||
echo "Services:"
|
||||
for app in $(grep -oE "name: *'[^']+'" "$PROJECT_DIR/ecosystem.config.cjs" | sed "s/.*'\(.*\)'/\1/"); do
|
||||
ECOSYSTEM_APPS=$(node -e "require('$PROJECT_DIR/$ECOSYSTEM_FILE').apps.forEach(a=>console.log(a.name))" 2>/dev/null) \
|
||||
|| fail "$ECOSYSTEM_FILE could not be loaded — run: node -e \"require('./$ECOSYSTEM_FILE')\" to see why"
|
||||
for app in $ECOSYSTEM_APPS; do
|
||||
if pm2 pid "$app" >/dev/null 2>&1 && [ -n "$(pm2 pid "$app" 2>/dev/null | tr -d '[:space:]')" ]; then
|
||||
ok "$app"
|
||||
else
|
||||
|
||||
Reference in New Issue
Block a user