#!/usr/bin/env bash set -euo pipefail ROOT="$(cd "$(dirname "$0")" && pwd)" GODOT_DIR="$ROOT/godot-desktop" TRAY_DIR="$ROOT/services/tray" BRIDGE_DIR="$ROOT/services/bridge" GODOT="flatpak run --user org.godotengine.Godot" PIDFILE="$ROOT/.godot.pid" TRAY_PIDFILE="$ROOT/.tray.pid" BRIDGE_PIDFILE="$ROOT/.bridge.pid" # Load .env then .env.development (development overrides base) if [ -f "$ROOT/.env" ]; then set -o allexport # shellcheck disable=SC1091 source "$ROOT/.env" set +o allexport fi if [ -f "$ROOT/.env.development" ]; then set -o allexport # shellcheck disable=SC1091 source "$ROOT/.env.development" set +o allexport fi REQUIRED_SERVICES=( "model-boss-coordinator:model-boss" "chatterbox-tts:speech-synthesis" ) check_services() { local all_ok=0 for entry in "${REQUIRED_SERVICES[@]}"; do local unit="${entry%%:*}" local label="${entry##*:}" if systemctl --user is-active --quiet "$unit" 2>/dev/null; then echo " ✓ $label ($unit)" else echo " ✗ $label ($unit) — not running" all_ok=1 fi done return $all_ok } ensure_services() { echo "Checking required services..." if [ "${RESTART_DEPENDENCY_SERVICES:-}" = "TRUE" ]; then echo "Restarting dependency services (RESTART_DEPENDENCY_SERVICES=TRUE)..." for entry in "${REQUIRED_SERVICES[@]}"; do local unit="${entry%%:*}" local label="${entry##*:}" if systemctl --user restart "$unit" 2>/dev/null; then echo " ✓ Restarted $label ($unit)" else echo " ⚠ Failed to restart $label ($unit) — continuing anyway" fi done sleep 2 echo "" echo "Service status after restart:" check_services || true return 0 fi if check_services; then return 0 fi echo "" echo "Starting missing services..." for entry in "${REQUIRED_SERVICES[@]}"; do local unit="${entry%%:*}" local label="${entry##*:}" if ! systemctl --user is-active --quiet "$unit" 2>/dev/null; then if systemctl --user start "$unit" 2>/dev/null; then echo " ✓ Started $label ($unit)" else echo " ⚠ Failed to start $label ($unit) — continuing anyway" fi fi done # Brief settle time for services to initialize sleep 2 echo "" echo "Service status after startup:" check_services || true } cmd_start() { if [ -f "$PIDFILE" ] && kill -0 "$(cat "$PIDFILE")" 2>/dev/null; then echo "Already running (pid $(cat "$PIDFILE"))" return 1 fi ensure_services echo "" # 1. Bridge (pub/sub relay for vision events) if [ -f "$BRIDGE_DIR/chobit_bridge.py" ]; then python3 "$BRIDGE_DIR/chobit_bridge.py" & echo $! > "$BRIDGE_PIDFILE" echo "Started bridge (pid $!)" fi # 2. Godot + tray setsid $GODOT --path "$GODOT_DIR" & echo $! > "$PIDFILE" echo "Started Godot (pid $!)" if [ -f "$TRAY_DIR/chobit_tray.py" ]; then python3 "$TRAY_DIR/chobit_tray.py" & echo $! > "$TRAY_PIDFILE" echo "Started tray (pid $!)" fi } cmd_dev() { if [ -f "$PIDFILE" ] && kill -0 "$(cat "$PIDFILE")" 2>/dev/null; then echo "Already running (pid $(cat "$PIDFILE"))" return 1 fi ensure_services echo "" # 1. Bridge if [ -f "$BRIDGE_DIR/chobit_bridge.py" ]; then python3 "$BRIDGE_DIR/chobit_bridge.py" & echo $! > "$BRIDGE_PIDFILE" echo "Started bridge (pid $!)" fi # 2. Godot + multi-tray (dev mode) setsid $GODOT --path "$GODOT_DIR" & echo $! > "$PIDFILE" echo "Started Godot (pid $!)" if [ -f "$TRAY_DIR/dev_trays.py" ]; then python3 "$TRAY_DIR/dev_trays.py" & echo $! > "$TRAY_PIDFILE" echo "Started dev trays (pid $!)" fi } cmd_stop() { # Stop tray if [ -f "$TRAY_PIDFILE" ]; then local tray_pid tray_pid=$(cat "$TRAY_PIDFILE" 2>/dev/null) if [ -n "$tray_pid" ] && kill -0 "$tray_pid" 2>/dev/null; then kill "$tray_pid" 2>/dev/null && echo "Stopped tray (pid $tray_pid)" || true # Wait for GTK cleanup (status notifier deregistration) — up to 3s local waited=0 while kill -0 "$tray_pid" 2>/dev/null && [ "$waited" -lt 30 ]; do sleep 0.1 waited=$((waited + 1)) done kill -0 "$tray_pid" 2>/dev/null && { kill -9 "$tray_pid" 2>/dev/null || true; } fi rm -f "$TRAY_PIDFILE" fi pgrep -f "(chobit_tray|dev_trays)\\.py" | while read -r cpid; do kill "$cpid" 2>/dev/null done || true pgrep -f "chobit_vision\\.py" | while read -r cpid; do kill "$cpid" 2>/dev/null done || true # Stop Godot local stopped=0 if [ -f "$PIDFILE" ]; then local pid pid=$(cat "$PIDFILE" 2>/dev/null) if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then kill -- -"$pid" 2>/dev/null || kill "$pid" 2>/dev/null || true pkill -P "$pid" 2>/dev/null || true wait "$pid" 2>/dev/null || true echo "Stopped Godot (pid $pid)" stopped=1 fi rm -f "$PIDFILE" fi # Sweep stale Godot processes local sweep_count=0 for cpid in $(pgrep -f "godot-bin.*(--path godot|@chobit/godot)" 2>/dev/null); do kill "$cpid" 2>/dev/null && sweep_count=$((sweep_count + 1)) done for cpid in $(pgrep -f "bwrap.*-- godot --path godot" 2>/dev/null); do kill "$cpid" 2>/dev/null done if [ "$stopped" -eq 0 ] && [ "$sweep_count" -eq 0 ]; then echo "Godot not running" elif [ "$sweep_count" -gt 0 ]; then echo "Swept $sweep_count stale Godot process(es)" fi # Stop bridge last — Godot may flush state on exit if [ -f "$BRIDGE_PIDFILE" ]; then local bridge_pid bridge_pid=$(cat "$BRIDGE_PIDFILE" 2>/dev/null) if [ -n "$bridge_pid" ] && kill -0 "$bridge_pid" 2>/dev/null; then kill "$bridge_pid" 2>/dev/null && echo "Stopped bridge (pid $bridge_pid)" || true fi rm -f "$BRIDGE_PIDFILE" fi pgrep -f "chobit_bridge\\.py" | while read -r cpid; do kill "$cpid" 2>/dev/null done || true } cmd_launch() { if [ "${ENVIRONMENT:-}" = "development" ]; then cmd_dev else cmd_start fi } cmd_restart() { cmd_stop sleep 2 cmd_launch } cmd_verify() { local failed=0 echo "=== Shared Source: Lint ===" if (cd "$GODOT_DIR" && gdlint src/); then echo "PASS" else echo "FAIL" failed=1 fi echo "" echo "=== Desktop Platform: Lint ===" if (cd "$GODOT_DIR" && gdlint platform/); then echo "PASS" else echo "FAIL" failed=1 fi echo "" echo "=== Shared Source: Format Check ===" if (cd "$GODOT_DIR" && gdformat --check src/ 2>&1); then echo "PASS" else echo "FAIL (run: cd godot-desktop && gdformat src/)" failed=1 fi echo "" echo "=== Desktop Platform: Format Check ===" if (cd "$GODOT_DIR" && gdformat --check platform/ 2>&1); then echo "PASS" else echo "FAIL (run: cd godot-desktop && gdformat platform/)" failed=1 fi echo "" echo "=== Godot Import ===" local import_errors import_errors=$($GODOT --headless --path "$GODOT_DIR" --import 2>&1 | grep -iE "error|fail" || true) if [ -z "$import_errors" ]; then echo "PASS" else echo "$import_errors" echo "FAIL" failed=1 fi echo "" if [ "$failed" -eq 0 ]; then echo "All checks passed." else echo "Verification failed." return 1 fi } cmd_editor() { $GODOT --editor --path "$GODOT_DIR" } cmd_mobile_editor() { $GODOT --editor --path "$ROOT/godot-mobile" } cmd_screenshot() { $GODOT --path "$GODOT_DIR" --script tools/screenshot.gd 2>&1 | tail -1 } cmd_test() { echo "Running unit tests..." $GODOT --headless --path "$GODOT_DIR" --script tests/test_runner.gd 2>&1 } SYSTEMD_UNIT="$ROOT/infrastructure/services/chobit.service" SYSTEMD_DEST="$HOME/.config/systemd/user/chobit.service" cmd_install_service() { if [ ! -f "$SYSTEMD_UNIT" ]; then echo "Error: $SYSTEMD_UNIT not found" return 1 fi mkdir -p "$(dirname "$SYSTEMD_DEST")" cp "$SYSTEMD_UNIT" "$SYSTEMD_DEST" systemctl --user daemon-reload echo "Installed chobit.service → $SYSTEMD_DEST" echo "" echo "Enable with: systemctl --user enable chobit" echo "Start with: systemctl --user start chobit" echo "" echo "This will auto-start model-boss-coordinator and chatterbox-tts via Wants= dependency." } cmd_status() { echo "=== Chobit Services ===" echo "" echo "External dependencies:" check_services || true echo "" echo "Local processes:" if [ -f "$PIDFILE" ] && kill -0 "$(cat "$PIDFILE")" 2>/dev/null; then echo " ✓ Godot (pid $(cat "$PIDFILE"))" else echo " ✗ Godot — not running" fi if [ -f "$TRAY_PIDFILE" ] && kill -0 "$(cat "$TRAY_PIDFILE")" 2>/dev/null; then echo " ✓ Tray (pid $(cat "$TRAY_PIDFILE"))" else echo " ✗ Tray — not running" fi if [ -f "$BRIDGE_PIDFILE" ] && kill -0 "$(cat "$BRIDGE_PIDFILE")" 2>/dev/null; then echo " ✓ Bridge (pid $(cat "$BRIDGE_PIDFILE"))" else echo " ✗ Bridge — not running" fi } case "${1:-}" in ""|start) cmd_launch ;; dev) cmd_dev ;; stop) cmd_stop ;; restart) cmd_restart ;; status) cmd_status ;; install-service) cmd_install_service ;; verify) cmd_verify ;; test) cmd_test ;; editor) cmd_editor ;; mobile-editor) cmd_mobile_editor ;; screenshot) cmd_screenshot ;; *) echo "Usage: ./run [command]" echo "" echo "Commands:" echo " (none), start Launch bridge + companion + tray (desktop)" echo " dev Launch with dev multi-tray [Bridge:dev][Chat:dev][Char:dev][Speech:dev][Face:dev][Main]" echo " stop Stop everything" echo " restart Stop then start" echo " status Check service status (local + dependencies)" echo " install-service Install systemd user unit (auto-starts dependencies)" echo " verify Run lint, format check, and Godot import" echo " test Run unit tests (headless)" echo " editor Open Godot desktop editor" echo " mobile-editor Open Godot mobile editor" echo " screenshot Capture a screenshot" exit 1 ;; esac