#!/usr/bin/env bash set -euo pipefail ROOT="${MOUSEHOLD_ROOT:-/mnt/DATA/git/mousehold}" PYTHON="${PYTHON:-$ROOT/.venv/bin/python}" NPM="${NPM:-npm}" BACKEND_HOST="${MOUSEHOLD_BACKEND_HOST:-127.0.0.1}" BACKEND_PORT="${MOUSEHOLD_BACKEND_PORT:-8000}" FRONTEND_HOST="${MOUSEHOLD_FRONTEND_HOST:-127.0.0.1}" FRONTEND_PORT="${MOUSEHOLD_FRONTEND_PORT:-5173}" OPEN_BROWSER="${OPEN_BROWSER:-1}" LOG_DIR="$ROOT/data/dev-launcher" BACKEND_LOG="$LOG_DIR/backend.log" FRONTEND_LOG="$LOG_DIR/frontend.log" BACKEND_URL="http://$BACKEND_HOST:$BACKEND_PORT" FRONTEND_URL="http://$FRONTEND_HOST:$FRONTEND_PORT" backend_pid="" frontend_pid="" fail() { printf 'launch-dev: %s\n' "$*" >&2 exit 1 } port_is_free() { "$PYTHON" - "$1" "$2" <<'PY' import socket import sys host = sys.argv[1] port = int(sys.argv[2]) with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) try: sock.bind((host, port)) except OSError: raise SystemExit(1) PY } wait_for_url() { "$PYTHON" - "$1" <<'PY' import sys import time import urllib.request url = sys.argv[1] deadline = time.monotonic() + 60 last_error = None while time.monotonic() < deadline: try: with urllib.request.urlopen(url, timeout=2) as response: if 200 <= response.status < 500: raise SystemExit(0) except Exception as exc: # noqa: BLE001 - printed only on timeout. last_error = exc time.sleep(1) print(f"Timed out waiting for {url}: {last_error}", file=sys.stderr) raise SystemExit(1) PY } cleanup() { if [ -n "${frontend_pid:-}" ] && kill -0 "$frontend_pid" 2>/dev/null; then kill "$frontend_pid" 2>/dev/null || true fi if [ -n "${backend_pid:-}" ] && kill -0 "$backend_pid" 2>/dev/null; then kill "$backend_pid" 2>/dev/null || true fi } trap cleanup EXIT INT TERM [ -x "$PYTHON" ] || fail "Python virtualenv not found at $PYTHON. Run: cd $ROOT && python -m venv .venv && . .venv/bin/activate && pip install -r requirements-dev.txt && pip install -e ." command -v "$NPM" >/dev/null 2>&1 || fail "npm not found. Install Node.js/npm, then rerun this launcher." [ -d "$ROOT/node_modules" ] || fail "node_modules missing. Run: cd $ROOT && npm install" mkdir -p "$LOG_DIR" : > "$BACKEND_LOG" : > "$FRONTEND_LOG" port_is_free "$BACKEND_HOST" "$BACKEND_PORT" || fail "$BACKEND_URL is already in use" port_is_free "$FRONTEND_HOST" "$FRONTEND_PORT" || fail "$FRONTEND_URL is already in use" printf 'Starting Mousehold API at %s\n' "$BACKEND_URL" ( cd "$ROOT" "$PYTHON" -m uvicorn mousehold_api.main:app --host "$BACKEND_HOST" --port "$BACKEND_PORT" --reload ) >"$BACKEND_LOG" 2>&1 & backend_pid="$!" printf 'Waiting for %s/health\n' "$BACKEND_URL" wait_for_url "$BACKEND_URL/health" || { tail -n 80 "$BACKEND_LOG" >&2 || true fail "backend did not become healthy" } printf 'Starting Mousehold Web UI at %s\n' "$FRONTEND_URL" ( cd "$ROOT/apps/web" "$NPM" run dev -- --host "$FRONTEND_HOST" --port "$FRONTEND_PORT" ) >"$FRONTEND_LOG" 2>&1 & frontend_pid="$!" printf 'Waiting for %s\n' "$FRONTEND_URL" wait_for_url "$FRONTEND_URL" || { tail -n 80 "$FRONTEND_LOG" >&2 || true fail "frontend did not become reachable" } if [ "$OPEN_BROWSER" = "1" ] && command -v xdg-open >/dev/null 2>&1; then xdg-open "$FRONTEND_URL" >/dev/null 2>&1 || true fi cat <