merge the macos branch: setup script, mac pm2 ecosystem, claude binary resolution
Three files, all additive — nothing on master is modified by this beyond the CLAUDE_BIN change below, and no file is deleted. The branch predates master by about 180 commits, but it touches nothing master has touched since, so the merge is clean. The part that matters beyond macOS is claude-manager.ts. CLAUDE_BIN was pinned to /usr/local/bin/claude, which dated from the bwrap-sandboxed architecture: the jail ro-bound /usr and saw nothing else, so the installer's real target (~/.local/bin/claude) had to be symlinked somewhere the sandbox could reach. That sandbox is gone, and the hardcoded path left the sidecar unrunnable on any host without it. It now resolves an explicit CLAUDE_BIN pin, then PATH, then the locations Anthropic's installer actually writes to — mirroring how OPENCODE_BIN is already resolved in the opencode sidecar. ecosystem.mac.config.cjs is deliberately a trimmed set of processes rather than a mac port of the full ecosystem. It is also stale in two specific ways, left as-is here and worth fixing separately: it names officer-claude, which master renamed to officer-anthropic-proxy, and its officer-pty runs src/servers/api/terminal/pty-sidecar.mjs, which moved to src/servers/sidecar/pty/index.mjs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
// macOS process list — the laptop subset of ecosystem.config.cjs.
|
||||
//
|
||||
// Only what a Mac install can actually run and what the laptop workflow needs (file browser +
|
||||
// Claude/opencode chat + terminal). Deliberately omitted:
|
||||
// officer-vnc — mirrors an Xorg :0 with x11vnc; there is no Xorg on macOS.
|
||||
// officer-email — needs the mbsync/IMAP stack that setup_mac.sh does not install.
|
||||
// officer-music — the indexer is ffprobe-driven and works, but it is not part of the laptop
|
||||
// workflow and a full ~/Music index is an expensive thing to start by default.
|
||||
//
|
||||
// Start with: pm2 startOrRestart ecosystem.mac.config.cjs
|
||||
// The Linux host keeps using ecosystem.config.cjs; neither file references the other.
|
||||
//
|
||||
// `cwd` is pinned on every app because Bun auto-loads .env from the working directory (and
|
||||
// pty-sidecar.mjs does `import 'dotenv/config'`). Without it, starting pm2 from anywhere other than
|
||||
// the repo root silently falls back to PORT=5000 with no POSTGRES_URL.
|
||||
|
||||
const cwd = __dirname;
|
||||
|
||||
module.exports = {
|
||||
apps: [
|
||||
{
|
||||
name: 'officer',
|
||||
script: 'bun',
|
||||
args: 'start',
|
||||
cwd,
|
||||
watch: false,
|
||||
},
|
||||
{
|
||||
name: 'officer-claude',
|
||||
script: 'bun',
|
||||
args: 'run src/servers/sidecar/claude/index.ts',
|
||||
cwd,
|
||||
watch: false,
|
||||
},
|
||||
{
|
||||
name: 'officer-opencode',
|
||||
script: 'bun',
|
||||
args: 'run src/servers/sidecar/opencode/index.ts',
|
||||
cwd,
|
||||
watch: false,
|
||||
},
|
||||
{
|
||||
name: 'officer-pty',
|
||||
script: 'node',
|
||||
args: 'src/servers/api/terminal/pty-sidecar.mjs',
|
||||
cwd,
|
||||
watch: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
Executable
+510
@@ -0,0 +1,510 @@
|
||||
#!/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
|
||||
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 installed on macOS: VNC desktop, email sync, music indexer, cliamp audio"
|
||||
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 ""
|
||||
@@ -1,3 +1,5 @@
|
||||
import { existsSync } from 'node:fs';
|
||||
import { homedir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { query, type Query } from '@anthropic-ai/claude-agent-sdk';
|
||||
import type { ChatEvent } from '../../api/chat/types';
|
||||
@@ -7,9 +9,27 @@ import { createParseState, processMessage } from './stream-parser';
|
||||
|
||||
const SEND_TIMEOUT_MS = 30 * 60 * 1000;
|
||||
|
||||
// Use /usr/local/bin/claude so it's visible inside bwrap sandbox (which ro-binds /usr).
|
||||
// The actual binary lives at ~/.local/bin/claude, symlinked from /usr/local/bin/claude.
|
||||
const CLAUDE_BIN = '/usr/local/bin/claude';
|
||||
// Where the Claude Code CLI lives. This was hardcoded to /usr/local/bin/claude, which dated from the
|
||||
// bwrap-sandboxed architecture: the jail ro-bound /usr and saw nothing else, so the installer's real
|
||||
// target (~/.local/bin/claude) had to be symlinked into a path the sandbox could reach. That sandbox
|
||||
// is gone, and the hardcoded path made the sidecar unrunnable anywhere it does not exist — a stock
|
||||
// macOS host has no /usr/local/bin at all.
|
||||
//
|
||||
// Resolution order mirrors OPENCODE_BIN in the opencode sidecar: an explicit pin, then PATH, then the
|
||||
// locations Anthropic's installer actually writes to.
|
||||
function resolveClaudeBin(): string {
|
||||
const pinned = process.env.CLAUDE_BIN;
|
||||
if (pinned) return pinned;
|
||||
|
||||
const onPath = Bun.which('claude');
|
||||
if (onPath) return onPath;
|
||||
|
||||
const candidates = [join(homedir(), '.local', 'bin', 'claude'), '/usr/local/bin/claude', '/opt/homebrew/bin/claude'];
|
||||
return candidates.find((candidate) => existsSync(candidate)) ?? 'claude';
|
||||
}
|
||||
|
||||
const CLAUDE_BIN = resolveClaudeBin();
|
||||
console.log(`[claude] CLI resolved to ${CLAUDE_BIN}`);
|
||||
|
||||
// Capture original HOME before user-instance overrides it
|
||||
const HOST_HOME = process.env.HOME!;
|
||||
|
||||
Reference in New Issue
Block a user