#!/usr/bin/env bash # # CORE Workbench launcher — one command, env-checked, fail-loud. # # scripts/workbench # doctor + setup (idempotent) + run both servers # scripts/workbench up # same as no-arg # scripts/workbench doctor # preflight checks only, change nothing # scripts/workbench setup # preflight + install deps, do not start servers # # Brings up the Python API (stdlib HTTP, :8765) and the Vite UI (:5173), # waits until each is actually healthy, then prints the URL. Ctrl+C stops both. # Override ports with API_PORT / UI_PORT. # # The backend runs on pure Python (numpy backend) — no Rust build required. set -euo pipefail API_PORT="${API_PORT:-8765}" UI_PORT="${UI_PORT:-5173}" API_HOST="127.0.0.1" UI_HOST="127.0.0.1" MIN_NODE_MAJOR=20 MIN_PY_MINOR=11 # requires-python >=3.11 # --- resolve repo root from this script's location (works from anywhere) ----- SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" VENV="$ROOT/.venv" UI_DIR="$ROOT/workbench-ui" bold() { printf '\033[1m%s\033[0m\n' "$*"; } ok() { printf ' \033[32m✓\033[0m %s\n' "$*"; } warn() { printf ' \033[33m!\033[0m %s\n' "$*"; } die() { printf ' \033[31m✗ %s\033[0m\n' "$*" >&2; exit 1; } # --------------------------------------------------------------------------- # Preflight — every failure prints the exact fix. # --------------------------------------------------------------------------- doctor() { bold "Preflight" command -v uv >/dev/null 2>&1 \ || die "uv not found. Install it: curl -LsSf https://astral.sh/uv/install.sh | sh" ok "uv $(uv --version 2>/dev/null | awk '{print $2}')" command -v node >/dev/null 2>&1 \ || die "node not found. Install Node >= ${MIN_NODE_MAJOR} (https://nodejs.org or nvm)." local node_major node_major="$(node -v | sed 's/^v//' | cut -d. -f1)" [ "$node_major" -ge "$MIN_NODE_MAJOR" ] \ || die "node $(node -v) is too old; need >= ${MIN_NODE_MAJOR}. Try: nvm install ${MIN_NODE_MAJOR}" ok "node $(node -v)" if ! command -v pnpm >/dev/null 2>&1; then if command -v corepack >/dev/null 2>&1; then warn "pnpm not on PATH — enabling via corepack (uses the pinned version)" corepack enable pnpm >/dev/null 2>&1 || true fi command -v pnpm >/dev/null 2>&1 \ || die "pnpm not found. Enable it: corepack enable pnpm (or: npm i -g pnpm)" fi ok "pnpm $(pnpm -v)" command -v curl >/dev/null 2>&1 || die "curl not found (needed for health checks)." ok "curl present" } # --------------------------------------------------------------------------- # Setup — idempotent. Only does work when something is actually missing. # --------------------------------------------------------------------------- setup() { bold "Setup" if [ ! -x "$VENV/bin/python" ]; then warn "creating Python venv (.venv) with uv" uv venv "$VENV" >/dev/null fi # Confirm the venv Python meets the minimum. local py_minor py_minor="$("$VENV/bin/python" -c 'import sys; print(sys.version_info[1])')" [ "$py_minor" -ge "$MIN_PY_MINOR" ] \ || die "venv Python is 3.${py_minor}; need >= 3.${MIN_PY_MINOR}. Recreate: rm -rf .venv && uv venv --python 3.12" ok "Python $("$VENV/bin/python" -c 'import platform; print(platform.python_version())')" if [ ! -x "$VENV/bin/core" ] || ! "$VENV/bin/python" -c 'import workbench.server' >/dev/null 2>&1; then warn "installing the CORE package into .venv (editable) — first run only, may take a minute" (cd "$ROOT" && uv pip install --python "$VENV/bin/python" -e . >/dev/null) fi "$VENV/bin/python" -c 'import workbench.server, core.cognition.pipeline' \ || die "workbench backend failed to import. Re-run: rm -rf .venv && scripts/workbench setup" ok "CORE backend importable (pure Python, no Rust build needed)" if [ ! -d "$UI_DIR/node_modules" ]; then warn "installing UI dependencies (pnpm, frozen lockfile) — first run only" (cd "$UI_DIR" && pnpm install --frozen-lockfile >/dev/null) fi ok "UI dependencies installed" } # --------------------------------------------------------------------------- # Run — health-gated startup of both servers; Ctrl+C stops both. # --------------------------------------------------------------------------- port_responds() { curl -fsS "http://$1:$2/health" >/dev/null 2>&1; } port_open() { curl -fsS -o /dev/null "http://$1:$2" 2>/dev/null; } BACK_PID="" FRONT_PID="" cleanup() { trap - INT TERM EXIT for pid in "$FRONT_PID" "$BACK_PID"; do [ -n "$pid" ] && kill -TERM "$pid" 2>/dev/null || true done sleep 1 for pid in "$FRONT_PID" "$BACK_PID"; do [ -n "$pid" ] && kill -KILL "$pid" 2>/dev/null || true done printf '\n'; bold "Workbench stopped." } run() { bold "Start" trap cleanup INT TERM EXIT if port_responds "$API_HOST" "$API_PORT"; then ok "API already healthy on :$API_PORT (reusing)" else "$VENV/bin/core" workbench api --host "$API_HOST" --port "$API_PORT" >/tmp/core-workbench-api.log 2>&1 & BACK_PID=$! printf ' … waiting for API on :%s' "$API_PORT" for _ in $(seq 1 60); do if port_responds "$API_HOST" "$API_PORT"; then printf '\r'; ok "API healthy on :$API_PORT "; break; fi kill -0 "$BACK_PID" 2>/dev/null || die "API process exited. Logs: /tmp/core-workbench-api.log" printf '.'; sleep 1 done port_responds "$API_HOST" "$API_PORT" || die "API did not become healthy in 60s. Logs: /tmp/core-workbench-api.log" fi if port_open "$UI_HOST" "$UI_PORT"; then ok "UI already serving on :$UI_PORT (reusing)" else # Invoke vite directly (not via `pnpm dev`) so FRONT_PID is the server # itself — a pnpm wrapper would not forward the stop signal to vite. (cd "$UI_DIR" && exec node_modules/.bin/vite --host "$UI_HOST" --port "$UI_PORT" --strictPort) >/tmp/core-workbench-ui.log 2>&1 & FRONT_PID=$! printf ' … waiting for UI on :%s' "$UI_PORT" for _ in $(seq 1 60); do if port_open "$UI_HOST" "$UI_PORT"; then printf '\r'; ok "UI serving on :$UI_PORT "; break; fi kill -0 "$FRONT_PID" 2>/dev/null || die "UI process exited. Logs: /tmp/core-workbench-ui.log" printf '.'; sleep 1 done port_open "$UI_HOST" "$UI_PORT" || die "UI did not start in 60s. Logs: /tmp/core-workbench-ui.log" fi printf '\n' bold "CORE Workbench is up:" printf ' \033[1;36mhttp://%s:%s\033[0m (API: http://%s:%s)\n\n' "$UI_HOST" "$UI_PORT" "$API_HOST" "$API_PORT" printf ' Press Ctrl+C to stop.\n' # Stay in the foreground; if either child dies, surface it and exit. while true; do [ -n "$BACK_PID" ] && ! kill -0 "$BACK_PID" 2>/dev/null && die "API exited unexpectedly. Logs: /tmp/core-workbench-api.log" [ -n "$FRONT_PID" ] && ! kill -0 "$FRONT_PID" 2>/dev/null && die "UI exited unexpectedly. Logs: /tmp/core-workbench-ui.log" sleep 2 done } case "${1:-up}" in doctor) doctor ;; setup) doctor; setup ;; up|"") doctor; setup; run ;; *) die "unknown command '$1' (use: doctor | setup | up)" ;; esac