claire-tray/deploy/install.sh
Natalie bf3d783c9a refactor: rename clare-serve -> claire-serve in daemon label
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 17:30:33 -06:00

130 lines
5.2 KiB
Bash
Executable file

#!/usr/bin/env bash
#
# Build, sign, bundle, and install the ClaireTray menu-bar app as a LaunchAgent
# on this machine (plum). Local-only — no remote rsync. Idempotent.
#
# ./deploy/install.sh build + install + (re)load the LaunchAgent
# ./deploy/install.sh --uninstall bootout + remove the LaunchAgent and bundle
#
# Signing: a personal Apple Development cert (override with
# CLAIRE_TRAY_SIGNING_IDENTITY). The tray holds NO TCC permissions, so the
# stable-keychain dance mac-sync uses (to preserve permission grants) is
# unnecessary here.
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
APP_NAME="ClaireTrayApp" # executable (SwiftPM product)
DISPLAY_NAME="Claire" # CFBundleName
BUNDLE_ID="com.lilith.claire-tray"
LABEL="com.lilith.claire-tray"
APP_BUNDLE="$HOME/Applications/ClaireTray.app"
PLIST="$HOME/Library/LaunchAgents/$LABEL.plist"
LOG_DIR="$HOME/Library/Application Support/ClaireTray"
CONFIG_DIR="$HOME/.config/$BUNDLE_ID"
CONFIG_FILE="$CONFIG_DIR/config.json"
CLAIRE_VENV="${CLAIRE_VENV:-$HOME/Code/@projects/@claire/.venv}"
SIGNING_IDENTITY="${CLAIRE_TRAY_SIGNING_IDENTITY:-Apple Development: hinataliesterling@icloud.com (X8424J5CTB)}"
say() { printf '\033[1;35m▸\033[0m %s\n' "$*"; }
uninstall() {
say "Uninstalling $LABEL"
launchctl bootout "gui/$(id -u)/$LABEL" 2>/dev/null || true
rm -f "$PLIST"
rm -rf "$APP_BUNDLE"
say "Removed bundle + LaunchAgent (config + logs left at $CONFIG_DIR, $LOG_DIR)"
}
if [[ "${1:-}" == "--uninstall" ]]; then
uninstall
exit 0
fi
# --- 1. build --------------------------------------------------------------
say "Building release binary"
( cd "$ROOT" && swift build -c release --product "$APP_NAME" )
BIN="$ROOT/.build/release/$APP_NAME"
[[ -f "$BIN" ]] || { echo "binary not found: $BIN" >&2; exit 1; }
# --- 2. assemble bundle ----------------------------------------------------
say "Assembling $APP_BUNDLE"
MACOS_DIR="$APP_BUNDLE/Contents/MacOS"
RES_DIR="$APP_BUNDLE/Contents/Resources"
mkdir -p "$MACOS_DIR" "$RES_DIR"
rm -rf "$MACOS_DIR"/*.bundle "$RES_DIR"/*.bundle
cp "$BIN" "$MACOS_DIR/$APP_NAME"
chmod +x "$MACOS_DIR/$APP_NAME"
# SPM resource bundles (the generated tray-resources icons).
for b in "$ROOT/.build/release/"*.bundle; do
[[ -d "$b" ]] || continue
cp -R "$b" "$RES_DIR/$(basename "$b")"
say " bundled $(basename "$b")"
done
# --- 3. Info.plist from template + VERSION.json ----------------------------
VERSION="0.1.0"; BUILD="1"
if [[ -f "$ROOT/VERSION.json" ]]; then
VERSION="$(python3 -c "import json;print(json.load(open('$ROOT/VERSION.json')).get('version','0.1.0'))")"
BUILD="$(python3 -c "import json;print(json.load(open('$ROOT/VERSION.json')).get('builds',1))")"
fi
sed -e "s/{{VERSION}}/$VERSION/g" -e "s/{{BUILD}}/$BUILD/g" \
"$ROOT/src/client/Resources/Info.plist.template" \
> "$APP_BUNDLE/Contents/Info.plist"
# --- 4. codesign -----------------------------------------------------------
say "Signing as $BUNDLE_ID"
if ! codesign --force --identifier "$BUNDLE_ID" -s "$SIGNING_IDENTITY" "$APP_BUNDLE" 2>/dev/null; then
say " signing identity unavailable — falling back to ad-hoc"
codesign --force --identifier "$BUNDLE_ID" -s - "$APP_BUNDLE"
fi
codesign --verify --strict "$APP_BUNDLE"
# --- 5. config.json: base URL from claire's OWN config loader --------------
# Reuse claire's config + `_client_host` (wildcard→loopback) rather than
# re-parsing the TOML or guessing its path — single source of truth.
say "Writing $CONFIG_FILE"
mkdir -p "$CONFIG_DIR"
BASE_URL="$("$CLAIRE_VENV/bin/python" -c "
from claire.config import load_or_init
from claire.orchestrator.bootstrap import _client_host
c = load_or_init()
print(f'http://{_client_host(c.web.host)}:{c.web.port}')
" 2>/dev/null || echo "http://127.0.0.1:8765")"
printf '{\n "baseURL": "%s",\n "daemonLabel": "com.lilith.claire-serve"\n}\n' "$BASE_URL" > "$CONFIG_FILE"
say " baseURL = $BASE_URL"
# --- 6. LaunchAgent --------------------------------------------------------
say "Installing LaunchAgent $LABEL"
mkdir -p "$LOG_DIR" "$(dirname "$PLIST")"
cat > "$PLIST" <<PLISTEOF
<?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>$LABEL</string>
<key>ProgramArguments</key>
<array><string>$MACOS_DIR/$APP_NAME</string></array>
<key>RunAtLoad</key><true/>
<key>KeepAlive</key>
<dict>
<key>SuccessfulExit</key><false/>
<key>Crashed</key><true/>
</dict>
<key>StandardOutPath</key><string>$LOG_DIR/stdout.log</string>
<key>StandardErrorPath</key><string>$LOG_DIR/stderr.log</string>
</dict>
</plist>
PLISTEOF
DOMAIN="gui/$(id -u)"
launchctl bootout "$DOMAIN/$LABEL" 2>/dev/null || true
# Wait for full unload — bootstrap races bootout and returns EIO (5) if the
# label is still registered.
for _ in $(seq 1 20); do
launchctl print "$DOMAIN/$LABEL" >/dev/null 2>&1 || break
sleep 0.2
done
# One retry covers the residual transient EIO.
launchctl bootstrap "$DOMAIN" "$PLIST" 2>/dev/null \
|| { sleep 1; launchctl bootstrap "$DOMAIN" "$PLIST"; }
say "Done — Claire menu-bar icon should appear shortly. Logs: $LOG_DIR"