130 lines
5.2 KiB
Bash
Executable file
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"
|