diff --git a/scripts/launch-dev.sh b/scripts/launch-dev.sh new file mode 100644 index 0000000..0c0f3f4 --- /dev/null +++ b/scripts/launch-dev.sh @@ -0,0 +1,131 @@ +#!/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 <