core/scripts/workbench

172 lines
7 KiB
Bash
Executable file

#!/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
REQUIRED_PYTHON_VERSION="3.12.13"
REQUIRED_PYTHON_BIN="/opt/homebrew/bin/python3.12"
# --- 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 (${REQUIRED_PYTHON_VERSION})"
uv venv --python "$REQUIRED_PYTHON_BIN" "$VENV" >/dev/null
fi
local py_version
py_version="$("$VENV/bin/python" -c 'import platform; print(platform.python_version())')"
[ "$py_version" = "$REQUIRED_PYTHON_VERSION" ] \
|| die "venv Python is ${py_version}; need exactly ${REQUIRED_PYTHON_VERSION}. Recreate: rm -rf .venv && uv venv --python ${REQUIRED_PYTHON_BIN}"
ok "Python ${py_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