ecosystem.mac.config.cjs was written on 2026-07-28 and broken within days by two changes it never caught up with, leaving two of its four processes dead: - it ran `officer-claude` against src/servers/sidecar/claude/index.ts. That file is the Anthropic credential proxy; the process that actually spawns `claude` is user-instance.ts, which was never started. Chat had a credential holder and nothing driving it. Split into officer-anthropic-proxy + officer-agent to match the host, and retired the `officer-claude` name that caused it. - officer-pty pointed at src/servers/api/terminal/pty-sidecar.mjs, which no longer exists; the sidecar moved to src/servers/sidecar/pty/index.mjs. The terminal could not start at all. No startup ordering is needed between the proxy and the agent: the agent reads the proxy secret from disk and, when it is not there yet, warns and re-reads before the next spawn. Five sidecars have been added since the file was written (memos, caldav, photos, notify, wallet). All are excluded, and every exclusion is now listed with its reason so the next reader can tell "not applicable on macOS" from "forgotten" — which is the failure this commit is fixing. setup_mac.sh needed no path corrections; its verification loop derives the process names from the ecosystem file, so it picks the new list up on its own. Two gaps closed there: - it called `pm2 save` without `pm2 startup`, writing a process list that nothing reads — a reboot left the machine with nothing running. Added the launchd equivalent behind SETUP_BOOT, optional because a laptop is not a server, and non-fatal because pm2's launchd integration can want a prompt. - the closing "not installed on macOS" note listed four omissions when there are now fourteen. Both files parse and every ecosystem entry point resolves to a file that exists. The launchd path is the one thing not verified here — it cannot be exercised from the Linux host. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
530 lines
22 KiB
Bash
Executable File
530 lines
22 KiB
Bash
Executable File
#!/bin/bash
|
|
# Officer — macOS laptop setup.
|
|
#
|
|
# The barebones counterpart to scripts/setup.sh (which targets an Ubuntu/Debian server and is left
|
|
# alone). This installs only what a laptop workflow needs: the file browser, Claude/opencode chat,
|
|
# and a terminal. No Go/Rust/cliamp/PulseAudio, no neovim, no shell dotfile stack, no VNC desktop,
|
|
# no sudoers grant, no power-management changes.
|
|
#
|
|
# EVERY STEP IS OPTIONAL. Each one prompts before doing anything, and can be preset non-interactively:
|
|
#
|
|
# SETUP_POSTGRES=0 SETUP_OPENCODE=0 bash scripts/setup_mac.sh
|
|
#
|
|
# SETUP_PACKAGES brew node@22 / bun / ffmpeg SETUP_CLAUDE claude code CLI
|
|
# SETUP_POSTGRES brew postgresql@18 + createdb SETUP_OPENCODE opencode CLI
|
|
# SETUP_LINK_NODE brew link --force node@22 SETUP_DEPS bun install
|
|
# SETUP_PM2 pm2 via npm SETUP_ENV write .env
|
|
# SETUP_INIT gen:index + db:push SETUP_SERVICES pm2 startOrRestart
|
|
#
|
|
# Accepted values: 1/y/yes/true to run, anything else to skip. Unset = ask (or take the default when
|
|
# stdin is not a terminal).
|
|
#
|
|
# This script never calls sudo itself — everything lands under the Homebrew prefix or $HOME. Note
|
|
# that Homebrew's own installer does ask for an administrator password on a fresh Mac.
|
|
#
|
|
# Usage: bash scripts/setup_mac.sh
|
|
|
|
set -euo pipefail
|
|
|
|
GREEN='\033[0;32m'
|
|
YELLOW='\033[1;33m'
|
|
RED='\033[0;31m'
|
|
BOLD='\033[1m'
|
|
NC='\033[0m'
|
|
|
|
ok() { echo -e " ${GREEN}✓${NC} $1"; }
|
|
warn() { echo -e " ${YELLOW}!${NC} $1"; }
|
|
fail() { echo -e " ${RED}✗${NC} $1"; }
|
|
skip() { echo -e " - $1"; }
|
|
step() { echo ""; echo -e "${BOLD}── $1 ──${NC}"; }
|
|
|
|
has() { command -v "$1" &>/dev/null; }
|
|
|
|
# `set -e` is on, so anything allowed to fail must be guarded explicitly — either inside an `if`, or
|
|
# with a trailing `|| true`. Note the classic trap this file avoids everywhere: a bare top-level
|
|
# `cmd_a && cmd_b` list returns non-zero when cmd_a fails, which aborts the script. Those are all
|
|
# written as `if cmd_a; then cmd_b; fi` instead.
|
|
FAILURES=()
|
|
note_failure() { FAILURES+=("$1"); fail "$1"; }
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
|
|
|
|
PG_FORMULA="postgresql@18"
|
|
PG_DATABASE="officer_dev"
|
|
NODE_FORMULA="node@22"
|
|
ENV_FILE="$PROJECT_DIR/.env"
|
|
ECOSYSTEM="$PROJECT_DIR/ecosystem.mac.config.cjs"
|
|
|
|
# Ask, unless the matching SETUP_* variable already decided. $1 = variable name, $2 = prompt,
|
|
# $3 = default (y|n) used for a bare Enter and for non-interactive runs.
|
|
confirm() {
|
|
local var="$1" prompt="$2" default="$3" preset reply hint
|
|
preset="${!var:-}"
|
|
if [ -n "$preset" ]; then
|
|
case "$preset" in
|
|
1|y|Y|yes|YES|true) return 0 ;;
|
|
*) return 1 ;;
|
|
esac
|
|
fi
|
|
if [ ! -t 0 ]; then
|
|
if [ "$default" = "y" ]; then return 0; else return 1; fi
|
|
fi
|
|
hint="[y/N]"
|
|
if [ "$default" = "y" ]; then hint="[Y/n]"; fi
|
|
echo -en " ${BOLD}${prompt}${NC} ${hint}: "
|
|
reply=""
|
|
read -r reply || true
|
|
reply="${reply:-$default}"
|
|
case "$reply" in
|
|
y|Y|yes|YES) return 0 ;;
|
|
*) return 1 ;;
|
|
esac
|
|
}
|
|
|
|
# Prompt for a value with a default. Never fails the script.
|
|
ask() {
|
|
local __var="$1" prompt="$2" default="$3" reply=""
|
|
if [ ! -t 0 ]; then
|
|
printf -v "$__var" '%s' "$default"
|
|
return 0
|
|
fi
|
|
echo -en " ${BOLD}${prompt}${NC} [${default}]: "
|
|
read -r reply || true
|
|
printf -v "$__var" '%s' "${reply:-$default}"
|
|
}
|
|
|
|
echo ""
|
|
echo "═══════════════════════════════════════════"
|
|
echo " Officer — macOS setup"
|
|
echo "═══════════════════════════════════════════"
|
|
|
|
# ─── 0. preflight ─────────────────────────────────────────────────────────────
|
|
step "Preflight"
|
|
|
|
if [ "$(uname -s)" != "Darwin" ]; then
|
|
fail "This script is macOS-only. On Linux use scripts/setup.sh."
|
|
exit 1
|
|
fi
|
|
|
|
# Running under sudo would install bun/claude/opencode into /var/root and write HOME_DIR=/var/root
|
|
# into .env. Nothing here needs root except the symlinks, which prompt individually.
|
|
if [ "$(id -u)" = "0" ] || [ -n "${SUDO_USER:-}" ]; then
|
|
fail "Do not run this under sudo — run it as your normal user."
|
|
exit 1
|
|
fi
|
|
ok "macOS $(sw_vers -productVersion), running as $(whoami)"
|
|
|
|
# Xcode Command Line Tools. Not needed to compile anything: the only two native modules, node-pty and
|
|
# argon2, both ship darwin-arm64/darwin-x64 prebuilds and fall back to node-gyp only when a prebuild
|
|
# is missing for the running arch. (node-pty has no Linux prebuild — that is why setup.sh needs
|
|
# build-essential and this one does not.)
|
|
#
|
|
# They still matter on a fresh Mac, because `git` comes from them and the file browser shells out to
|
|
# it. Installing Homebrew below pulls the CLT in, so this is a note, not a gate.
|
|
if xcode-select -p &>/dev/null; then
|
|
ok "Xcode command line tools ($(xcode-select -p))"
|
|
else
|
|
warn "Xcode command line tools not found — Homebrew's installer will pull them in"
|
|
echo " Standalone: xcode-select --install (also what a fresh Mac needs for git)"
|
|
fi
|
|
|
|
if has brew; then
|
|
ok "homebrew ($(brew --prefix))"
|
|
elif confirm SETUP_BREW "Homebrew is not installed. Install it?" y; then
|
|
if /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"; then
|
|
for candidate in /opt/homebrew/bin/brew /usr/local/bin/brew; do
|
|
if [ -x "$candidate" ]; then
|
|
eval "$("$candidate" shellenv)" || true
|
|
break
|
|
fi
|
|
done
|
|
fi
|
|
if has brew; then ok "homebrew installed"; else note_failure "homebrew install failed"; fi
|
|
else
|
|
skip "homebrew (declined — brew-based steps will be skipped)"
|
|
fi
|
|
|
|
# macOS ships zip, unzip, curl, python3 and openssl (LibreSSL) in the base system — none are
|
|
# installed here.
|
|
for builtin_tool in zip unzip curl openssl nc; do
|
|
if ! has "$builtin_tool"; then warn "$builtin_tool not found (expected to ship with macOS)"; fi
|
|
done
|
|
|
|
# git needs its own check: /usr/bin/git is a stub on a Mac without the Command Line Tools, so
|
|
# `command -v git` succeeds while running it only pops the "install developer tools" dialog. The file
|
|
# browser shells out to git, so verify it actually executes. Invoking it here is deliberate — on a
|
|
# fresh Mac it triggers that install prompt early, rather than at first use inside the app.
|
|
if git --version &>/dev/null; then
|
|
ok "git ($(git --version 2>/dev/null || true))"
|
|
else
|
|
warn "git is not usable yet — accept the Command Line Tools prompt, then re-run this script"
|
|
fi
|
|
|
|
# ─── 1. homebrew packages ─────────────────────────────────────────────────────
|
|
step "Packages"
|
|
|
|
NODE_PREFIX=""
|
|
brew_install() {
|
|
local formula="$1" probe="$2"
|
|
if [ -n "$probe" ] && has "$probe"; then
|
|
skip "$formula (already present: $(command -v "$probe"))"
|
|
return 0
|
|
fi
|
|
if brew list --formula "$formula" &>/dev/null; then
|
|
skip "$formula (already installed)"
|
|
return 0
|
|
fi
|
|
echo " Installing $formula..."
|
|
if brew install "$formula"; then ok "$formula installed"; else note_failure "$formula install failed"; fi
|
|
}
|
|
|
|
if ! has brew; then
|
|
skip "packages (homebrew unavailable)"
|
|
elif confirm SETUP_PACKAGES "Install node@22, bun and ffmpeg via Homebrew?" y; then
|
|
# node@22 exactly — package.json's preinstall rejects anything else (`v < 22 || v > 22`), so plain
|
|
# `brew install node` (currently v24+) would break `bun install`.
|
|
brew_install "$NODE_FORMULA" ""
|
|
brew_install bun bun
|
|
# ffmpeg/ffprobe: the only external binaries the file browser shells out to besides git and zip.
|
|
brew_install ffmpeg ffmpeg
|
|
else
|
|
skip "packages (declined)"
|
|
fi
|
|
|
|
if has brew; then
|
|
NODE_PREFIX="$(brew --prefix "$NODE_FORMULA" 2>/dev/null || true)"
|
|
fi
|
|
|
|
# ─── 2. node on PATH ──────────────────────────────────────────────────────────
|
|
step "Node on PATH"
|
|
|
|
# node@22 is keg-only, so Homebrew does not put it on PATH. `brew link --force` symlinks it into the
|
|
# Homebrew prefix (user-owned, already on PATH) — no sudo, and nothing lands in /usr/local.
|
|
if ! has brew; then
|
|
skip "node link (homebrew unavailable)"
|
|
elif [ -z "$NODE_PREFIX" ] || [ ! -x "$NODE_PREFIX/bin/node" ]; then
|
|
skip "node link ($NODE_FORMULA not installed)"
|
|
elif [ "$(command -v node 2>/dev/null || true)" = "$NODE_PREFIX/bin/node" ]; then
|
|
skip "node already resolves to $NODE_FORMULA"
|
|
elif confirm SETUP_LINK_NODE "Link $NODE_FORMULA onto PATH (brew link --force)?" y; then
|
|
if brew link --force --overwrite "$NODE_FORMULA" >/dev/null 2>&1; then
|
|
ok "$NODE_FORMULA linked into $(brew --prefix)/bin"
|
|
else
|
|
note_failure "brew link $NODE_FORMULA failed — add $NODE_PREFIX/bin to PATH manually"
|
|
fi
|
|
else
|
|
skip "node link (declined)"
|
|
fi
|
|
|
|
# Verify node really is 22 — `bun install` fails its preinstall check otherwise.
|
|
if has node; then
|
|
NODE_MAJOR="$(node -v 2>/dev/null | cut -d. -f1 | sed 's/^v//' || true)"
|
|
if [ "$NODE_MAJOR" = "22" ]; then
|
|
ok "node $(node -v)"
|
|
else
|
|
note_failure "node $(node -v) is on PATH but package.json requires exactly v22"
|
|
fi
|
|
else
|
|
warn "node not on PATH"
|
|
fi
|
|
|
|
# ─── 3. postgres ──────────────────────────────────────────────────────────────
|
|
step "PostgreSQL"
|
|
|
|
# Detect a reachable server first — this machine may already run Postgres in Docker, in which case
|
|
# there is nothing to install and nothing to start.
|
|
PG_PREFIX=""
|
|
PG_RUNNING=0
|
|
if nc -z -G 2 127.0.0.1 5432 &>/dev/null; then
|
|
PG_RUNNING=1
|
|
ok "postgres already reachable on 127.0.0.1:5432 — nothing to install"
|
|
else
|
|
skip "no postgres on 127.0.0.1:5432"
|
|
if ! has brew; then
|
|
skip "postgres install (homebrew unavailable)"
|
|
# Default yes: nothing is listening, so there is no Docker/remote server to reuse and Homebrew is
|
|
# the only way this host gets a database.
|
|
elif confirm SETUP_POSTGRES "Install and start $PG_FORMULA via Homebrew?" y; then
|
|
brew_install "$PG_FORMULA" ""
|
|
PG_PREFIX="$(brew --prefix "$PG_FORMULA" 2>/dev/null || true)"
|
|
if [ -n "$PG_PREFIX" ] && [ -x "$PG_PREFIX/bin/pg_isready" ]; then
|
|
echo " Starting $PG_FORMULA..."
|
|
brew services start "$PG_FORMULA" >/dev/null 2>&1 || true
|
|
for _ in $(seq 1 20); do
|
|
if "$PG_PREFIX/bin/pg_isready" -h 127.0.0.1 -q 2>/dev/null; then break; fi
|
|
sleep 1
|
|
done
|
|
if "$PG_PREFIX/bin/pg_isready" -h 127.0.0.1 -q 2>/dev/null; then
|
|
PG_RUNNING=1
|
|
ok "postgres running"
|
|
# Homebrew's postgres trusts local connections for the current user, so no password needed.
|
|
if "$PG_PREFIX/bin/psql" -h 127.0.0.1 -lqt 2>/dev/null | cut -d'|' -f1 | grep -qw "$PG_DATABASE"; then
|
|
skip "database '$PG_DATABASE' exists"
|
|
elif "$PG_PREFIX/bin/createdb" -h 127.0.0.1 "$PG_DATABASE" 2>/dev/null; then
|
|
ok "database '$PG_DATABASE' created"
|
|
else
|
|
note_failure "could not create database '$PG_DATABASE'"
|
|
fi
|
|
else
|
|
note_failure "postgres did not become ready — try: brew services start $PG_FORMULA"
|
|
fi
|
|
fi
|
|
else
|
|
skip "postgres install (declined — supply a POSTGRES_URL below)"
|
|
fi
|
|
fi
|
|
|
|
# ─── 4. pm2 ───────────────────────────────────────────────────────────────────
|
|
step "pm2"
|
|
|
|
# User-local npm prefix so global installs never need sudo.
|
|
export PATH="$HOME/.local/bin:$PATH"
|
|
if has pm2; then
|
|
skip "pm2 ($(command -v pm2))"
|
|
elif ! has npm; then
|
|
skip "pm2 (npm unavailable)"
|
|
elif confirm SETUP_PM2 "Install pm2 (process manager for the server + sidecars)?" y; then
|
|
npm config set prefix "$HOME/.local" >/dev/null 2>&1 || true
|
|
echo " Installing pm2..."
|
|
if npm install -g pm2 >/dev/null 2>&1; then ok "pm2 installed"; else note_failure "pm2 install failed"; fi
|
|
else
|
|
skip "pm2 (declined)"
|
|
fi
|
|
# ~/.local/bin is not on the macOS default PATH (see /etc/paths). It is exported for this run, but a
|
|
# later `pm2 logs` from a fresh shell needs it permanently.
|
|
case ":${PATH}:" in
|
|
*":$HOME/.local/bin:"*) ;;
|
|
*) warn "add ~/.local/bin to your PATH so pm2 and claude stay available in new shells" ;;
|
|
esac
|
|
|
|
# ─── 5. agents (claude, opencode) ─────────────────────────────────────────────
|
|
step "Agents"
|
|
|
|
if has claude; then
|
|
skip "claude ($(command -v claude))"
|
|
elif confirm SETUP_CLAUDE "Install Claude Code?" y; then
|
|
echo " Installing claude..."
|
|
if curl -fsSL https://claude.ai/install.sh | bash; then
|
|
if has claude; then ok "claude installed"; else note_failure "claude installed but not on PATH"; fi
|
|
else
|
|
note_failure "claude install failed"
|
|
fi
|
|
else
|
|
skip "claude (declined)"
|
|
fi
|
|
# No symlink needed: the claude sidecar resolves its CLI from CLAUDE_BIN, then PATH, then the
|
|
# installer's own locations (see resolveClaudeBin in sidecar/claude/claude-manager.ts).
|
|
|
|
if [ -x "$HOME/.opencode/bin/opencode" ]; then
|
|
skip "opencode ($HOME/.opencode/bin/opencode)"
|
|
elif confirm SETUP_OPENCODE "Install opencode?" y; then
|
|
echo " Installing opencode..."
|
|
if curl -fsSL https://opencode.ai/install | bash; then
|
|
if [ -x "$HOME/.opencode/bin/opencode" ]; then
|
|
ok "opencode installed"
|
|
else
|
|
note_failure "opencode installed but not at $HOME/.opencode/bin/opencode"
|
|
fi
|
|
else
|
|
note_failure "opencode install failed"
|
|
fi
|
|
else
|
|
skip "opencode (declined — officer-opencode will not start)"
|
|
fi
|
|
|
|
# ─── 6. project dependencies ──────────────────────────────────────────────────
|
|
step "Project dependencies"
|
|
|
|
if ! has bun; then
|
|
skip "bun install (bun unavailable)"
|
|
elif confirm SETUP_DEPS "Run bun install?" y; then
|
|
echo " Running bun install..."
|
|
if (cd "$PROJECT_DIR" && bun install); then
|
|
ok "dependencies installed"
|
|
else
|
|
note_failure "bun install failed"
|
|
fi
|
|
else
|
|
skip "bun install (declined)"
|
|
fi
|
|
|
|
# ─── 7. environment (.env) ────────────────────────────────────────────────────
|
|
step "Environment (.env)"
|
|
|
|
WRITE_ENV=0
|
|
if [ -f "$ENV_FILE" ]; then
|
|
if confirm SETUP_ENV "$ENV_FILE exists. Regenerate it?" n; then WRITE_ENV=1; else skip ".env (kept existing)"; fi
|
|
elif confirm SETUP_ENV "Generate .env?" y; then
|
|
WRITE_ENV=1
|
|
else
|
|
skip ".env (declined)"
|
|
fi
|
|
|
|
ENV_PUBLIC_URL=""
|
|
if [ "$WRITE_ENV" = "1" ]; then
|
|
echo ""
|
|
ask ENV_PORT "PORT" "9010"
|
|
# PUBLIC_BUILD_ENV=development makes IS_DEV_BUILD true, which short-circuits origin validation
|
|
# (isOriginAllowed returns true immediately) and relaxes rate limits and password rules. That is
|
|
# what makes plain http://localhost work with no HTTPS reverse proxy in front.
|
|
ask ENV_PUBLIC_URL "PUBLIC_URL" "http://localhost:$ENV_PORT"
|
|
ask ENV_DATA_PATH "DATA_PATH" "$HOME/.local/data"
|
|
ask ENV_ITEMS_DIR "OFFICER_ITEMS_DIR" "$(dirname "$PROJECT_DIR")/officer-items"
|
|
# A Docker postgres usually wants a password; brew's trusts the local user. Default to the plain
|
|
# local form and let it be edited — this is the one value the script cannot infer reliably.
|
|
ask ENV_POSTGRES_URL "POSTGRES_URL" "postgresql://$(whoami)@127.0.0.1:5432/$PG_DATABASE"
|
|
|
|
# jwt.ts throws at import time unless this is >= 32 chars, so verify rather than trust the pipeline.
|
|
JWT_SECRET="$(openssl rand -base64 48 | tr -d '/+=' | head -c 48 || true)"
|
|
if [ ${#JWT_SECRET} -lt 32 ]; then
|
|
note_failure "could not generate a JWT_SECRET (got ${#JWT_SECRET} chars) — .env not written"
|
|
else
|
|
mkdir -p "$ENV_DATA_PATH" "$ENV_ITEMS_DIR" || warn "could not create DATA_PATH/OFFICER_ITEMS_DIR"
|
|
if cat > "$ENV_FILE" <<ENVFILE
|
|
PORT="$ENV_PORT"
|
|
JWT_SECRET="$JWT_SECRET"
|
|
PUBLIC_URL="$ENV_PUBLIC_URL"
|
|
PUBLIC_BUILD_ENV="development"
|
|
DATA_PATH="$ENV_DATA_PATH"
|
|
OFFICER_ITEMS_DIR="$ENV_ITEMS_DIR"
|
|
HOME_DIR="$HOME"
|
|
POSTGRES_URL="$ENV_POSTGRES_URL"
|
|
ENVFILE
|
|
then
|
|
# Holds the JWT signing secret and the database URL.
|
|
chmod 600 "$ENV_FILE" || warn "could not chmod 600 $ENV_FILE"
|
|
ok ".env written to $ENV_FILE (mode 600)"
|
|
else
|
|
note_failure "could not write $ENV_FILE"
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# ─── 8. project initialization ────────────────────────────────────────────────
|
|
step "Project initialization"
|
|
|
|
if ! has bun || [ ! -f "$ENV_FILE" ]; then
|
|
skip "initialization (bun or .env missing)"
|
|
elif confirm SETUP_INIT "Generate index.gen.html and apply the database schema?" y; then
|
|
# index.gen.html is gitignored and built from PUBLIC_URL, so it never exists on a fresh clone.
|
|
echo " Generating index.gen.html..."
|
|
if (cd "$PROJECT_DIR" && bun run gen:index); then
|
|
ok "index.gen.html generated"
|
|
else
|
|
note_failure "gen:index failed — the app will not serve until this succeeds"
|
|
fi
|
|
|
|
# Officer applies its schema with push; there are no migrations.
|
|
echo " Applying database schema..."
|
|
if (cd "$PROJECT_DIR" && bun db:push); then
|
|
ok "schema applied"
|
|
else
|
|
note_failure "db:push failed — check POSTGRES_URL and that postgres is reachable"
|
|
fi
|
|
else
|
|
skip "initialization (declined)"
|
|
fi
|
|
|
|
# ─── 9. services ──────────────────────────────────────────────────────────────
|
|
step "Services (pm2)"
|
|
|
|
if ! has pm2 || [ ! -f "$ECOSYSTEM" ]; then
|
|
skip "services (pm2 or ecosystem.mac.config.cjs missing)"
|
|
elif [ ! -f "$ENV_FILE" ]; then
|
|
# Starting without .env gives a server on port 5000 with no database and a JWT_SECRET throw.
|
|
skip "services (.env missing — they would crash-loop)"
|
|
elif confirm SETUP_SERVICES "Start Officer and its sidecars with pm2?" y; then
|
|
echo " Starting services..."
|
|
if (cd "$PROJECT_DIR" && pm2 startOrRestart "$ECOSYSTEM"); then
|
|
ok "services started"
|
|
else
|
|
note_failure "pm2 could not start the services — check 'pm2 logs'"
|
|
fi
|
|
if pm2 save >/dev/null 2>&1; then ok "process list saved"; else warn "pm2 save failed"; fi
|
|
|
|
# `pm2 save` on its own writes a process list that nothing ever reads. The Linux setup pairs it with
|
|
# `pm2 startup`, which installs the boot unit that resurrects that list; without the pair, a reboot
|
|
# silently leaves the machine with nothing running. On macOS the equivalent is a launchd agent rather
|
|
# than a systemd unit. Kept optional because a laptop is not a server — you may not want the whole
|
|
# stack coming back at every login — and non-fatal, because pm2's launchd integration can want an
|
|
# elevated prompt that a scripted run should not force.
|
|
if confirm SETUP_BOOT "Start Officer automatically at login (pm2 + launchd)?" y; then
|
|
if pm2 startup launchd -u "$(whoami)" --hp "$HOME" >/dev/null 2>&1; then
|
|
pm2 save >/dev/null 2>&1
|
|
ok "services will start at login"
|
|
else
|
|
warn "Could not install the launchd agent — run: pm2 startup (and follow its instructions)"
|
|
fi
|
|
else
|
|
skip "login startup (declined — 'pm2 startup' enables it later)"
|
|
fi
|
|
else
|
|
skip "services (declined)"
|
|
fi
|
|
|
|
# ─── verification ─────────────────────────────────────────────────────────────
|
|
echo ""
|
|
echo "═══════════════════════════════════════════"
|
|
echo " Verification"
|
|
echo "═══════════════════════════════════════════"
|
|
echo ""
|
|
|
|
check() { if has "$1"; then ok "$1"; else fail "$1 — NOT FOUND"; fi; }
|
|
|
|
echo "Required:"
|
|
check node
|
|
check npm
|
|
check bun
|
|
check git
|
|
check zip
|
|
check ffmpeg
|
|
check ffprobe
|
|
|
|
echo ""
|
|
echo "Database:"
|
|
if nc -z -G 2 127.0.0.1 5432 &>/dev/null; then ok "postgres reachable on 127.0.0.1:5432"; else fail "postgres — NOT REACHABLE on 127.0.0.1:5432"; fi
|
|
|
|
echo ""
|
|
echo "Agents:"
|
|
check claude
|
|
if [ -x "$HOME/.opencode/bin/opencode" ]; then ok "opencode"; else fail "opencode — NOT FOUND"; fi
|
|
|
|
echo ""
|
|
echo "Process manager:"
|
|
check pm2
|
|
|
|
if has pm2 && [ -f "$ECOSYSTEM" ]; then
|
|
echo ""
|
|
echo "Services:"
|
|
for app in $(grep -oE "name: *'[^']+'" "$ECOSYSTEM" | sed "s/.*'\(.*\)'/\1/" || true); do
|
|
if [ -n "$(pm2 pid "$app" 2>/dev/null | tr -d '[:space:]' || true)" ]; then
|
|
ok "$app"
|
|
else
|
|
fail "$app — not running (pm2 logs $app)"
|
|
fi
|
|
done
|
|
fi
|
|
|
|
echo ""
|
|
echo "═══════════════════════════════════════════"
|
|
if [ ${#FAILURES[@]} -eq 0 ]; then
|
|
echo -e " ${GREEN}Setup complete${NC}"
|
|
else
|
|
echo -e " ${YELLOW}Setup finished with ${#FAILURES[@]} problem(s)${NC}"
|
|
for f in "${FAILURES[@]}"; do echo " • $f"; done
|
|
fi
|
|
echo "═══════════════════════════════════════════"
|
|
|
|
echo ""
|
|
echo "Notes:"
|
|
echo " • Open ${ENV_PUBLIC_URL:-http://localhost:9010} — the first-run screen creates the owner account"
|
|
echo " • Logs: pm2 logs Restart: pm2 restart ecosystem.mac.config.cjs"
|
|
echo " • Postgres must be running before the services start, or db:push and boot will fail"
|
|
echo " • Not run on macOS: VNC desktop, email sync, music indexer, cliamp audio, and the sidecars"
|
|
echo " that front a container or an external service — vault, slskd, headscale, transmission,"
|
|
echo " invoiceshelf, memos, photos, caldav, notify, wallet. See ecosystem.mac.config.cjs."
|
|
echo " • Pin a specific Claude CLI with CLAUDE_BIN=/path/to/claude in .env if you need to"
|
|
echo " • Re-run any single step with e.g. SETUP_OPENCODE=1 bash scripts/setup_mac.sh"
|
|
echo ""
|