#!/usr/bin/env bash # Launch the prospector operator app locally and open it as a Chrome app window # (the containerless PWA experience). Starts the NestJS API + the vite preview # front door (serves the built web/ assets and injects the bearer token), waits # for health, then opens Chrome --app at the panel URL. # # scripts/app.sh launch in the foreground (Ctrl-C stops it) # scripts/app.sh --build rebuild backend + web first # scripts/app.sh --detach start in the background and return (tray/menubar) # ./run app (preferred entrypoint) # # If the app is already running, just opens the window. Foreground mode stops # both processes on Ctrl-C; detached mode leaves them running (stop with # './run stop'). Only the PIDs we started are killed — never a blanket node kill. set -euo pipefail . "$(dirname "${BASH_SOURCE[0]}")/lib.sh" load_env API_PORT="${PROSPECTOR_API_PORT:-3210}" WEB_PORT="${PROSPECTOR_WEB_PORT:-4173}" HEALTH_URL="http://127.0.0.1:${API_PORT}/health" RUN_DIR="$REPO_ROOT/.run" PIDFILE="$RUN_DIR/app.pids" BUILD=0 DETACH=0 WEB_HOST="${PROSPECTOR_WEB_HOST:-}" # empty = localhost only (default) for a in "$@"; do case "$a" in --build) BUILD=1 ;; --detach) DETACH=1 ;; --host) WEB_HOST="0.0.0.0" ;; --host=*) WEB_HOST="${a#--host=}" ;; *) die "unknown flag: $a (use --build / --detach / --host[=addr])" ;; esac done # Binding the panel beyond localhost exposes an app with no login of its own — # require a passcode (the vite preview gate) before doing so. HOST_ARGS=() if [[ -n "$WEB_HOST" ]]; then HOST_ARGS=(--host "$WEB_HOST") if [[ -z "$(grep -sE '^PROSPECTOR_PANEL_PASSCODE=.+' "$REPO_ROOT/web/.env.local")" ]]; then warn "exposing the panel on $WEB_HOST with NO passcode — anyone who can reach :$WEB_PORT gets full operator control." warn "set PROSPECTOR_PANEL_PASSCODE in web/.env.local first (then it prompts for a passcode)." fi fi # Host used to reach the panel from this machine (health-check + opening the # window). A specific --host binds only that address; unset/0.0.0.0 still answer # on localhost. if [[ -z "$WEB_HOST" || "$WEB_HOST" == "0.0.0.0" || "$WEB_HOST" == "::" ]]; then REACH_HOST="localhost" else REACH_HOST="$WEB_HOST" fi PANEL_URL="http://${REACH_HOST}:${WEB_PORT}/#/markets" api_up() { curl -fsS "$HEALTH_URL" >/dev/null 2>&1; } # Any HTTP response = up (the passcode gate answers 401 to non-HTML requests, so # don't use -f here — a 401 still means the preview server is listening). web_up() { curl -sS -o /dev/null "http://${REACH_HOST}:${WEB_PORT}" 2>/dev/null; } open_window() { info "opening $PANEL_URL" if [[ -d "/Applications/Google Chrome.app" ]]; then open -na "Google Chrome" --args --app="$PANEL_URL" || open "$PANEL_URL" else warn "Google Chrome not found — opening in your default browser" open "$PANEL_URL" 2>/dev/null || true fi } if [[ "$BUILD" == "1" ]]; then info "building backend"; ( cd "$REPO_ROOT" && npm run build ) info "building web"; ( cd "$REPO_ROOT/web" && npm run build ) fi [[ -f "$REPO_ROOT/dist/main.js" ]] || die "backend not built — run 'scripts/app.sh --build' or './run install'" [[ -d "$REPO_ROOT/web/dist" ]] || die "web not built — run 'scripts/app.sh --build' or './run install'" mkdir -p "$RUN_DIR" BACK_PID="" WEB_PID="" # Reuse whatever is already up (idempotent); only start what's down. This avoids # colliding with an existing instance on the same ports. if api_up; then ok "API already running on :$API_PORT — reusing" else info "starting API on :$API_PORT${DETACH:+ (detached)}" if [[ "$DETACH" == "1" ]]; then ( cd "$REPO_ROOT" && exec node dist/main.js ) >"$RUN_DIR/api.log" 2>&1 & else ( cd "$REPO_ROOT" && exec node dist/main.js ) & fi BACK_PID=$! info "waiting for API health" for _ in $(seq 1 40); do kill -0 "$BACK_PID" 2>/dev/null || die "API exited during startup — port :$API_PORT in use or DB unreachable? (see $RUN_DIR/api.log)" api_up && { ok "API healthy"; break; } sleep 0.5 done api_up || die "API did not become healthy at $HEALTH_URL" fi if web_up; then ok "web front door already running on :$WEB_PORT — reusing" else info "starting web front door on :$WEB_PORT${DETACH:+ (detached)}" if [[ "$DETACH" == "1" ]]; then ( cd "$REPO_ROOT/web" && exec npx vite preview ${HOST_ARGS[@]+"${HOST_ARGS[@]}"} --port "$WEB_PORT" --strictPort ) >"$RUN_DIR/web.log" 2>&1 & else ( cd "$REPO_ROOT/web" && exec npx vite preview ${HOST_ARGS[@]+"${HOST_ARGS[@]}"} --port "$WEB_PORT" --strictPort ) & fi WEB_PID=$! for _ in $(seq 1 40); do kill -0 "$WEB_PID" 2>/dev/null || die "web preview exited during startup — port :$WEB_PORT in use? (see $RUN_DIR/web.log)" web_up && break sleep 0.5 done web_up || die "web preview did not come up on :$WEB_PORT" fi # Record only the PIDs we started, so 'stop' touches nothing it didn't launch. : > "$PIDFILE" [[ -n "$BACK_PID" ]] && echo "$BACK_PID" >> "$PIDFILE" [[ -n "$WEB_PID" ]] && echo "$WEB_PID" >> "$PIDFILE" open_window if [[ "$DETACH" == "1" ]]; then ok "prospector running. API :$API_PORT · panel :$WEB_PORT · stop with './run stop'" exit 0 fi # Foreground: clean up only what we started; if we reused both, just exit. if [[ -z "$BACK_PID" && -z "$WEB_PID" ]]; then ok "prospector already running — opened window. API :$API_PORT · panel :$WEB_PORT" exit 0 fi cleanup() { [[ -n "$WEB_PID" ]] && kill "$WEB_PID" 2>/dev/null || true [[ -n "$BACK_PID" ]] && kill "$BACK_PID" 2>/dev/null || true rm -f "$PIDFILE" } trap cleanup EXIT INT TERM ok "prospector running. API :$API_PORT · panel :$WEB_PORT" echo " Tip: in the Chrome window, ⋮ → Save and Share → Install Prospector for a dock icon." echo " Press Ctrl-C to stop." wait "${BACK_PID:-$WEB_PID}"