← back to Exo Keepalive
bind-exo-node.sh
83 lines
#!/usr/bin/env bash
# bind-exo-node.sh — make THIS Mac Studio a permanent exo cluster node.
# Portable: uses $HOME + the local user, so it works on any studio
# (macstudio3 / stevestudio2 / steveabramsdesignsgmail.com / etc).
# Run it ON the node you want to bind. Idempotent — safe to re-run.
#
# Steve's rule: the exo cluster is ALWAYS on. This installs a launchd
# KeepAlive agent so exo (master+worker, API :52415) survives crash/logout/reboot
# and auto-joins the LAN cluster via exo's zero-config peer discovery.
set -uo pipefail
USER_HOME="$HOME"
UV="$(command -v uv || echo "$USER_HOME/.local/bin/uv")"
EXO_DIR="$USER_HOME/exo"
KA_DIR="$USER_HOME/Projects/exo-keepalive"
PLIST="$USER_HOME/Library/LaunchAgents/com.steve.exo-keepalive.plist"
UID_N="$(id -u)"
echo "== binding exo node: user=$(whoami) home=$USER_HOME =="
# 0. sanity
[ -x "$UV" ] || { echo "!! uv not found ($UV). Install uv first: curl -LsSf https://astral.sh/uv/install.sh | sh"; exit 1; }
[ -d "$EXO_DIR" ] || { echo "!! ~/exo not present. Clone it first: git clone <exo-repo> ~/exo"; exit 1; }
# 1. dashboard build (exo hard-requires ~/exo/dashboard/build/index.html)
if [ ! -f "$EXO_DIR/dashboard/build/index.html" ]; then
echo "== building dashboard (one-time; needs npm install/build) =="
( cd "$EXO_DIR/dashboard" && npm install && npm run build ) || {
echo "!! dashboard build failed. If it's the npm allow-scripts gate, run:"
echo " cd ~/exo/dashboard && npm approve-scripts --all && npm run build"; exit 1; }
else
echo "== dashboard already built =="
fi
# 2. keepalive runner (portable)
mkdir -p "$KA_DIR"
cat > "$KA_DIR/run-exo.sh" <<EOF
#!/usr/bin/env bash
set -uo pipefail
export HOME="$USER_HOME"
export PATH="$USER_HOME/.local/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin"
cd "$EXO_DIR" || exit 1
echo "[\$(date +%FT%T%z)] === exo start (pid \$\$) ===" >> "$KA_DIR/exo.log"
exec "$UV" run exo >> "$KA_DIR/exo.log" 2>&1
EOF
chmod +x "$KA_DIR/run-exo.sh"
# 3. launchd plist (portable paths)
mkdir -p "$USER_HOME/Library/LaunchAgents"
cat > "$PLIST" <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"><dict>
<key>Label</key><string>com.steve.exo-keepalive</string>
<key>ProgramArguments</key><array>
<string>/bin/bash</string><string>$KA_DIR/run-exo.sh</string>
</array>
<key>EnvironmentVariables</key><dict>
<key>HOME</key><string>$USER_HOME</string>
<key>PATH</key><string>$USER_HOME/.local/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string>
</dict>
<key>RunAtLoad</key><true/>
<key>KeepAlive</key><true/>
<key>ThrottleInterval</key><integer>15</integer>
<key>StandardOutPath</key><string>$KA_DIR/launchd.out.log</string>
<key>StandardErrorPath</key><string>$KA_DIR/launchd.err.log</string>
</dict></plist>
EOF
# 4. (re)bootstrap
launchctl bootout "gui/$UID_N/com.steve.exo-keepalive" 2>/dev/null || true
launchctl bootstrap "gui/$UID_N" "$PLIST" 2>&1 || launchctl load -w "$PLIST" 2>&1
launchctl kickstart -k "gui/$UID_N/com.steve.exo-keepalive" 2>/dev/null || true
# 5. verify
echo "== waiting for :52415 =="
for i in $(seq 1 25); do
code=$(curl -s -o /dev/null -w "%{http_code}" -m3 http://localhost:52415/v1/models 2>/dev/null)
[ "$code" = "200" ] && { echo "✅ exo UP on $(hostname) :52415 — node bound, permanent across reboot"; exit 0; }
sleep 6
done
echo "⚠️ exo not up yet — check $KA_DIR/exo.log"; exit 1