#!/usr/bin/env bash # Voice-Agent Mac-Worker — vollständiger Installer. # # Macht alles, was nötig ist, damit der Worker dauerhaft läuft: # - Python-venv anlegen + Dependencies installieren (idempotent) # - .env aus Vorlage anlegen (falls fehlt) # - LaunchAgent registrieren / aktualisieren / starten # - Health-Check # # Kann beliebig oft erneut laufen (z. B. nach git pull). # # Befehle: # ./install-launchagent.sh # installieren / aktualisieren # ./install-launchagent.sh uninstall # entfernen # ./install-launchagent.sh status # Status + letzte Logs # ./install-launchagent.sh restart # Worker neu starten (kein Reinstall) set -euo pipefail LABEL="com.voiceagent.worker" PLIST_DST="$HOME/Library/LaunchAgents/${LABEL}.plist" LOG_DIR="$HOME/Library/Logs" DOMAIN="gui/$(id -u)" SERVICE_TARGET="${DOMAIN}/${LABEL}" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" WORKDIR="$(cd "$SCRIPT_DIR/.." && pwd)" TEMPLATE="$SCRIPT_DIR/com.voiceagent.worker.plist.template" cmd="${1:-install}" # Bevorzugter Python-Interpreter (venv-Anlage). Reihenfolge: 3.11, 3.12, 3 pick_python() { for p in python3.11 python3.12 python3; do if command -v "$p" >/dev/null 2>&1; then echo "$p" return 0 fi done echo ""; return 1 } ensure_venv() { local py py="$(pick_python)" if [ -z "$py" ]; then echo "FEHLER: kein python3 gefunden. Bitte 'brew install python@3.11' ausführen." >&2 exit 1 fi if [ ! -x "$WORKDIR/.venv/bin/python" ]; then echo "[venv] Erzeuge $WORKDIR/.venv mit $py" "$py" -m venv "$WORKDIR/.venv" fi echo "[venv] Pip aktualisieren + Dependencies installieren" "$WORKDIR/.venv/bin/pip" install --upgrade pip >/dev/null "$WORKDIR/.venv/bin/pip" install -r "$WORKDIR/requirements.txt" } ensure_env() { if [ ! -f "$WORKDIR/.env" ]; then echo "[env] Lege $WORKDIR/.env aus .env.example an (bitte ggf. anpassen)." cp "$WORKDIR/.env.example" "$WORKDIR/.env" fi } write_plist() { mkdir -p "$LOG_DIR" "$HOME/Library/LaunchAgents" sed \ -e "s|__WORKDIR__|$WORKDIR|g" \ -e "s|__HOME__|$HOME|g" \ "$TEMPLATE" > "$PLIST_DST" } # Modernes launchctl: bootout/bootstrap statt unload/load # (auf neueren macOS-Versionen wirft "launchctl unload -w" Input/output error 5). service_unload() { launchctl bootout "$SERVICE_TARGET" 2>/dev/null || true } service_load() { launchctl bootstrap "$DOMAIN" "$PLIST_DST" } service_kickstart() { # -k = Prozess hart killen + neu starten (greift auch wenn schon läuft). launchctl kickstart -k "$SERVICE_TARGET" } uninstall() { if [ -f "$PLIST_DST" ]; then echo "[*] Stoppe und entlade LaunchAgent…" service_unload rm -f "$PLIST_DST" echo "[OK] $PLIST_DST entfernt." else echo "[i] Kein LaunchAgent installiert ($PLIST_DST nicht vorhanden)." fi } show_status() { echo "── launchctl print $SERVICE_TARGET ─────────────────────" launchctl print "$SERVICE_TARGET" 2>/dev/null | head -20 || echo "(nicht geladen)" echo echo "── Health-Check ─────────────────────────────────────────" if curl -fsS http://127.0.0.1:8080/health 2>/dev/null; then echo else echo "(nicht erreichbar)" fi echo echo "── stderr.log (letzte 30 Zeilen) ────────────────────────" tail -n 30 "$LOG_DIR/voice-agent-worker.err.log" 2>/dev/null || echo "(keine Logs)" } restart_only() { if [ ! -f "$PLIST_DST" ]; then echo "[!] Kein LaunchAgent installiert — bitte zuerst './install-launchagent.sh' ausführen." >&2 exit 1 fi echo "[*] kickstart -k $SERVICE_TARGET" service_kickstart sleep 2 if curl -fsS http://127.0.0.1:8080/health >/dev/null 2>&1; then echo "[OK] Worker läuft wieder." else echo "[!] Health-Check schlägt nach Restart fehl — siehe $LOG_DIR/voice-agent-worker.err.log" >&2 tail -n 20 "$LOG_DIR/voice-agent-worker.err.log" 2>/dev/null || true exit 1 fi } install_agent() { echo "[1/6] venv + Dependencies" ensure_venv echo "[2/6] .env" ensure_env echo "[3/6] plist generieren ($PLIST_DST)" write_plist echo "[4/6] LaunchAgent (bootout falls vorhanden, dann bootstrap)" service_unload service_load echo "[5/6] Health-Check (max. 8 s warten)" local ok=0 for _ in 1 2 3 4 5 6 7 8; do sleep 1 if curl -fsS http://127.0.0.1:8080/health >/dev/null 2>&1; then ok=1 break fi done if [ "$ok" -ne 1 ]; then echo echo "[!] Health-Check fehlgeschlagen. Letzte Log-Zeilen:" >&2 tail -n 30 "$LOG_DIR/voice-agent-worker.err.log" 2>/dev/null || true echo echo " Plist: $PLIST_DST" echo " Logs: $LOG_DIR/voice-agent-worker.{out,err}.log" exit 1 fi echo "[6/6] Health-Antwort:" curl -s http://127.0.0.1:8080/health | python3 -m json.tool 2>/dev/null || true cat <&2 exit 1 ;; esac