scripts/ was holding two unrelated kinds of thing: install-this-machine, and run-this-occasionally. The eight installers now live in scripts/setup/; what stays at the top level is the build steps (gen-index, prebuild, build/) and the two maintenance scripts (reindex-music, rebuild-soulseek-tree). The move is not just a rename. Three of these derive the repo root from their own location: setup.sh:51 PROJECT_DIR="$(dirname "$SCRIPT_DIR")" setup_mac_light.sh:51 same cleanup-desktop.sh:134 ENV_FILE="$(dirname "$0")/../.env" Left alone, all three would now resolve to scripts/ — and nothing downstream complains. PROJECT_DIR is where .env is written, where `bun install`, `gen:index` and `db:push` run, and what pm2 is pointed at, so a fresh install would have quietly provisioned scripts/ and reported success. cleanup-desktop.sh fails the other way: it would find no .env, print "No .env — skipping", and leave the real VNC_PASSWORD in the real file. All three are now `../..` with a comment saying why the level matters. provision-user-dirs.ts imports data-path.ts relatively; that one tsgo caught. Also disambiguated `setup.sh` where it had become two files. app-store/templates/<name>/setup.sh is a per-sidecar installer with its own contract, and preflight.ts + docs/sidecar-app-store.md discussed both in the same paragraph. The host one is now spelled with its full path at those sites. Verified: bash -n on all six shell scripts, tsgo clean, os-user tests pass, both derivations resolve to the repo root, starship.toml still resolves from os-user-shell.ts, and provision-user-dirs.ts runs under DRY_RUN. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
540 lines
23 KiB
Bash
Executable File
540 lines
23 KiB
Bash
Executable File
#!/bin/bash
|
|
# Officer — macOS laptop setup.
|
|
#
|
|
# The barebones counterpart to scripts/setup/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/setup_mac_light.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/setup_mac_light.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)"
|
|
# ../.. — this lives in scripts/setup/. See the note in setup.sh: PROJECT_DIR is where .env is written
|
|
# and where bun install, gen:index, db:push and pm2 are pointed, and none of them fails loudly on the
|
|
# wrong directory.
|
|
PROJECT_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
|
|
|
PG_FORMULA="postgresql@18"
|
|
PG_DATABASE="officer_dev"
|
|
NODE_FORMULA="node@22"
|
|
ENV_FILE="$PROJECT_DIR/.env"
|
|
ECOSYSTEM="$PROJECT_DIR/ecosystem.mac.light.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/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"
|
|
# Plain http://localhost needs nothing relaxed to work: browsers treat it as a secure context, so
|
|
# passkeys, microphone capture and the clipboard are all available without TLS, and origin checking
|
|
# is already off by default (ALLOW_ANY_ORIGIN). Reaching this from another device is the case that
|
|
# needs an HTTPS proxy — http://192.168.x.x is not a secure context and those APIs fail there.
|
|
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="production"
|
|
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.light.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:"
|
|
# Read the names with node rather than grepping for `name:`. The profile derives its apps from
|
|
# ecosystem.config.cjs and has no literal name keys to match, so a grep silently lists nothing —
|
|
# which looks identical to "no services configured". Loading it also exercises the profile's own
|
|
# consistency checks, which is exactly the moment you want to hear about a drifted include list.
|
|
ECOSYSTEM_APPS=$(node -e "require('$ECOSYSTEM').apps.forEach(a=>console.log(a.name))" 2>/dev/null) \
|
|
|| fail "$(basename "$ECOSYSTEM") could not be loaded — run: node -e \"require('./$(basename "$ECOSYSTEM")')\" to see why"
|
|
for app in $ECOSYSTEM_APPS; 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.light.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.light.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/setup_mac_light.sh"
|
|
echo ""
|