#!/usr/bin/env bash # manage.sh — Turnstone diagnostic intelligence layer # Usage: ./manage.sh [args] set -euo pipefail RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; BLUE='\033[0;34m'; NC='\033[0m' info() { echo -e "${BLUE}[turnstone]${NC} $*"; } success() { echo -e "${GREEN}[turnstone]${NC} $*"; } warn() { echo -e "${YELLOW}[turnstone]${NC} $*"; } error() { echo -e "${RED}[turnstone]${NC} $*" >&2; exit 1; } SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" cd "$SCRIPT_DIR" API_PORT=8534 UI_PORT=8535 VITE_PORT=5174 # local HMR port in dev mode (proxies /api → 8534) LOG_DIR="log" API_PID_FILE=".turnstone-api.pid" UI_PID_FILE=".turnstone-ui.pid" DB="${TURNSTONE_DB:-${SCRIPT_DIR}/data/turnstone.db}" CONDA_BASE="${CONDA_BASE:-/devl/miniconda3}" PYTHON="${CONDA_BASE}/envs/cf/bin/python" # ── Helpers ─────────────────────────────────────────────────────────────────── _is_alive() { local pid_file="$1" [[ -f "$pid_file" ]] && kill -0 "$(<"$pid_file")" 2>/dev/null } _kill_pid_file() { local pid_file="$1" label="$2" if [[ -f "$pid_file" ]]; then local pid pid=$(<"$pid_file") if kill -0 "$pid" 2>/dev/null; then kill "$pid" && rm -f "$pid_file" success "$label stopped (PID $pid)." else warn "Stale PID file for $label (PID $pid not running). Cleaning up." rm -f "$pid_file" fi else warn "$label not running." fi } _wait_for_port() { local port="$1" label="$2" pid_file="$3" for _i in $(seq 1 20); do sleep 0.5 (echo "" >/dev/tcp/127.0.0.1/"$port") 2>/dev/null && return 0 if ! _is_alive "$pid_file"; then rm -f "$pid_file" error "$label died during startup. Check ${LOG_DIR}/api.log" fi done error "$label did not bind to port $port within 10 s." } # ── Usage ───────────────────────────────────────────────────────────────────── usage() { echo "" echo -e " ${BLUE}Turnstone — Diagnostic Log Intelligence${NC}" echo "" echo " Usage: ./manage.sh [args]" echo "" echo " Production-like (built SPA + uvicorn):" echo -e " ${GREEN}start${NC} Build Vue SPA, start API (:${API_PORT}) + static UI (:${UI_PORT})" echo -e " ${GREEN}stop${NC} Stop API and UI servers" echo -e " ${GREEN}restart${NC} Stop then start" echo -e " ${GREEN}status${NC} Show running processes" echo -e " ${GREEN}logs [api|ui]${NC} Tail log files (default: api)" echo -e " ${GREEN}open${NC} Open UI in browser" echo "" echo " Development (hot-reload):" echo -e " ${GREEN}dev${NC} uvicorn --reload (:${API_PORT}) + Vite HMR (:${VITE_PORT})" echo "" echo " Data:" echo -e " ${GREEN}ingest CORPUS_DIR [DB]${NC} Ingest a corpus directory into the database" echo -e " ${GREEN}build-fts${NC} Rebuild the FTS search index" echo "" echo " Tests:" echo -e " ${GREEN}test [args]${NC} Run pytest suite" echo "" echo " DB: ${DB}" echo " Conda env: cf" echo "" echo " Examples:" echo " ./manage.sh start" echo " ./manage.sh dev" echo " ./manage.sh ingest corpus/raw/" echo " ./manage.sh ingest corpus/raw/ data/custom.db" echo "" } # ── Commands ────────────────────────────────────────────────────────────────── CMD="${1:-help}" shift || true case "$CMD" in start) if _is_alive "$API_PID_FILE"; then warn "API already running (PID $(<"$API_PID_FILE")) — use 'restart' to rebuild." exit 0 fi mkdir -p "$LOG_DIR" data info "Building Vue SPA…" (cd web && npm run build) 2>&1 | tee "${LOG_DIR}/build.log" | grep -E "built in|error" || true success "SPA built → web/dist/" info "Starting FastAPI on port ${API_PORT}…" TURNSTONE_DB="$DB" nohup "$PYTHON" -m uvicorn app.rest:app \ --host 0.0.0.0 --port "$API_PORT" \ >> "${LOG_DIR}/api.log" 2>&1 & echo $! > "$API_PID_FILE" _wait_for_port "$API_PORT" "FastAPI" "$API_PID_FILE" success "API → http://localhost:${API_PORT} (PID $(<"$API_PID_FILE"))" info "Starting static UI server on port ${UI_PORT}…" nohup "$PYTHON" -m http.server "$UI_PORT" --directory web/dist \ >> "${LOG_DIR}/ui.log" 2>&1 & echo $! > "$UI_PID_FILE" _wait_for_port "$UI_PORT" "UI server" "$UI_PID_FILE" success "UI → http://localhost:${UI_PORT} (PID $(<"$UI_PID_FILE"))" ;; stop) _kill_pid_file "$API_PID_FILE" "FastAPI" _kill_pid_file "$UI_PID_FILE" "UI server" ;; restart) bash "$0" stop exec bash "$0" start ;; status) echo "" if _is_alive "$API_PID_FILE"; then success "FastAPI RUNNING PID $(<"$API_PID_FILE") → http://localhost:${API_PORT}" else echo -e " FastAPI ${RED}STOPPED${NC}" fi if _is_alive "$UI_PID_FILE"; then success "UI server RUNNING PID $(<"$UI_PID_FILE") → http://localhost:${UI_PORT}" else echo -e " UI server ${RED}STOPPED${NC}" fi echo "" ;; logs) target="${1:-api}" case "$target" in api) tail -f "${LOG_DIR}/api.log" ;; ui) tail -f "${LOG_DIR}/ui.log" ;; *) error "Unknown log target: $target. Use 'api' or 'ui'." ;; esac ;; open) URL="http://localhost:${UI_PORT}" info "Opening ${URL}" if command -v xdg-open &>/dev/null; then xdg-open "$URL" elif command -v open &>/dev/null; then open "$URL" else echo "$URL" fi ;; dev) DEV_API_PID=".turnstone-dev-api.pid" mkdir -p "$LOG_DIR" data if _is_alive "$DEV_API_PID"; then warn "Dev API already running (PID $(<"$DEV_API_PID"))" else info "Starting uvicorn --reload on port ${API_PORT}…" TURNSTONE_DB="$DB" nohup "$PYTHON" -m uvicorn app.rest:app \ --host 0.0.0.0 --port "$API_PORT" --reload \ >> "${LOG_DIR}/api.log" 2>&1 & echo $! > "$DEV_API_PID" _wait_for_port "$API_PORT" "FastAPI (dev)" "$DEV_API_PID" success "API (hot-reload) → http://localhost:${API_PORT}" fi _cleanup_dev() { local pid pid=$(<"$DEV_API_PID" 2>/dev/null) || true [[ -n "${pid:-}" ]] && kill "$pid" 2>/dev/null && rm -f "$DEV_API_PID" info "Dev servers stopped." } trap _cleanup_dev EXIT INT TERM info "Starting Vite HMR on port ${VITE_PORT}…" success "Frontend (HMR) → http://localhost:${VITE_PORT}" (cd web && npm run dev -- --port "$VITE_PORT") ;; ingest) if [[ $# -lt 1 ]]; then error "Usage: ./manage.sh ingest CORPUS_DIR [DB_PATH]" fi info "Ingesting $1 → ${2:-$DB}…" "$PYTHON" scripts/ingest_corpus.py "$1" "${2:-$DB}" ;; build-fts) info "Rebuilding FTS index for ${DB}…" TURNSTONE_DB="$DB" "$PYTHON" scripts/build_fts_index.py "$DB" success "FTS index rebuilt." ;; test) info "Running test suite…" PYTEST="${CONDA_BASE}/envs/cf/bin/pytest" [[ -x "$PYTEST" ]] || error "pytest not found in cf env at ${PYTEST}" TURNSTONE_DB=":memory:" "$PYTEST" tests/ -v "$@" ;; help|--help|-h) usage ;; *) error "Unknown command: ${CMD}. Run './manage.sh help' for usage." ;; esac