mirror of
https://github.com/saymrwulf/autoresearch-quantum.git
synced 2026-05-14 20:37:51 +00:00
app.sh: full Jupyter env isolation (CONFIG_DIR, DATA_DIR, RUNTIME_DIR, IPYTHONDIR), auto port allocation, foreground mode, restart command, graceful stop (SIGTERM → wait → SIGKILL), orphan detection, stale runtime JSON cleanup, reset-state command. test_browser_ux.py: add TestNavigationClickThrough (click Plan A link, verify target opens; click forward-link in Plan D), TestWidgetInteraction (run all cells, verify output areas, click quiz Submit and verify feedback), TestProgressPersistence (kernel API session, progress file schema validation). .gitignore: add Jupyter isolation directories. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
630 lines
21 KiB
Bash
Executable file
630 lines
21 KiB
Bash
Executable file
#!/usr/bin/env bash
|
||
# ──────────────────────────────────────────────────────────────────────
|
||
# app.sh — Consumer lifecycle manager for autoresearch-quantum
|
||
#
|
||
# Usage:
|
||
# bash scripts/app.sh bootstrap Create venv, install deps, verify
|
||
# bash scripts/app.sh start Launch JupyterLab (opens browser)
|
||
# bash scripts/app.sh start --no-open Launch without opening browser
|
||
# bash scripts/app.sh start --foreground Run in foreground (Ctrl-C to stop)
|
||
# bash scripts/app.sh start --port 9999 Use a specific port
|
||
# bash scripts/app.sh stop Stop running JupyterLab
|
||
# bash scripts/app.sh restart Stop + start
|
||
# bash scripts/app.sh status Show service status
|
||
# bash scripts/app.sh validate Run full validation suite
|
||
# bash scripts/app.sh validate --quick Lint + unit tests only
|
||
# bash scripts/app.sh logs Tail JupyterLab logs
|
||
# bash scripts/app.sh logs -f Follow live output
|
||
# bash scripts/app.sh reset Reset learner progress files
|
||
# bash scripts/app.sh reset-state Reset Jupyter runtime + UI state
|
||
# ──────────────────────────────────────────────────────────────────────
|
||
set -euo pipefail
|
||
|
||
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||
VENV_DIR="$PROJECT_ROOT/.venv"
|
||
PYTHON="$VENV_DIR/bin/python"
|
||
JUPYTER="$VENV_DIR/bin/jupyter"
|
||
|
||
# ── Isolated Jupyter directories (prevents cross-project interference) ──
|
||
LOG_DIR="$PROJECT_ROOT/.logs"
|
||
PID_FILE="$LOG_DIR/jupyter.pid"
|
||
LOG_FILE="$LOG_DIR/jupyterlab.log"
|
||
JUPYTER_CONFIG_DIR="$PROJECT_ROOT/.jupyter_config"
|
||
JUPYTER_DATA_DIR="$PROJECT_ROOT/.jupyter_data"
|
||
JUPYTER_RUNTIME_DIR="$PROJECT_ROOT/.jupyter_runtime"
|
||
IPYTHONDIR="$PROJECT_ROOT/.ipython"
|
||
MPLCONFIGDIR="$PROJECT_ROOT/.cache/matplotlib"
|
||
|
||
# ── Colours ───────────────────────────────────────────────────────────
|
||
RED='\033[0;31m'
|
||
GREEN='\033[0;32m'
|
||
YELLOW='\033[1;33m'
|
||
BLUE='\033[0;34m'
|
||
BOLD='\033[1m'
|
||
NC='\033[0m'
|
||
|
||
info() { echo -e "${BLUE}[info]${NC} $*"; }
|
||
ok() { echo -e "${GREEN}[ ok]${NC} $*"; }
|
||
warn() { echo -e "${YELLOW}[warn]${NC} $*"; }
|
||
fail() { echo -e "${RED}[FAIL]${NC} $*"; }
|
||
|
||
# ── Helpers ───────────────────────────────────────────────────────────
|
||
|
||
ensure_dirs() {
|
||
mkdir -p "$LOG_DIR" "$JUPYTER_CONFIG_DIR" "$JUPYTER_DATA_DIR" \
|
||
"$JUPYTER_RUNTIME_DIR" "$IPYTHONDIR" "$MPLCONFIGDIR"
|
||
}
|
||
|
||
set_jupyter_env() {
|
||
export JUPYTER_CONFIG_DIR
|
||
export JUPYTER_DATA_DIR
|
||
export JUPYTER_RUNTIME_DIR
|
||
export IPYTHONDIR
|
||
export MPLCONFIGDIR
|
||
}
|
||
|
||
# Check if a PID is alive
|
||
_pid_alive() {
|
||
local pid="$1"
|
||
kill -0 "$pid" 2>/dev/null
|
||
}
|
||
|
||
# Get the PID from our PID file, if any and alive
|
||
_get_running_pid() {
|
||
if [[ -f "$PID_FILE" ]]; then
|
||
local pid
|
||
pid=$(cat "$PID_FILE")
|
||
if [[ -n "$pid" ]] && _pid_alive "$pid"; then
|
||
echo "$pid"
|
||
return 0
|
||
fi
|
||
fi
|
||
return 1
|
||
}
|
||
|
||
# Find server URL from runtime JSON files (the Jupyter-native way)
|
||
_server_url() {
|
||
"$PYTHON" -c '
|
||
import json, sys
|
||
from pathlib import Path
|
||
|
||
runtime = Path(sys.argv[1])
|
||
root = Path(sys.argv[2]).resolve()
|
||
best = None
|
||
for path in sorted(runtime.glob("jpserver-*.json"), key=lambda p: p.stat().st_mtime, reverse=True):
|
||
try:
|
||
data = json.loads(path.read_text())
|
||
except Exception:
|
||
continue
|
||
pid = data.get("pid", -1)
|
||
try:
|
||
import os
|
||
os.kill(pid, 0)
|
||
except (ProcessLookupError, PermissionError, TypeError):
|
||
continue
|
||
url = data.get("url", "").rstrip("/")
|
||
token = data.get("token", "")
|
||
entry = "/lab/tree/00_START_HERE.ipynb"
|
||
if token:
|
||
print(f"{url}{entry}?token={token}")
|
||
else:
|
||
print(f"{url}{entry}")
|
||
sys.exit(0)
|
||
|
||
sys.exit(1)
|
||
' "$JUPYTER_RUNTIME_DIR" "$PROJECT_ROOT" 2>/dev/null
|
||
}
|
||
|
||
# Clean up stale runtime JSON files (process no longer running)
|
||
_cleanup_stale_runtime() {
|
||
"$PYTHON" -c '
|
||
import json, os
|
||
from pathlib import Path
|
||
|
||
runtime = Path("'"$JUPYTER_RUNTIME_DIR"'")
|
||
for path in runtime.glob("jpserver-*.json"):
|
||
try:
|
||
data = json.loads(path.read_text())
|
||
pid = int(data.get("pid", -1))
|
||
os.kill(pid, 0)
|
||
except (ProcessLookupError, ValueError, KeyError, json.JSONDecodeError):
|
||
path.unlink(missing_ok=True)
|
||
html = path.with_name(f"{path.stem}-open.html")
|
||
html.unlink(missing_ok=True)
|
||
except PermissionError:
|
||
pass # process exists but owned by another user
|
||
' 2>/dev/null || true
|
||
}
|
||
|
||
# ── Bootstrap ─────────────────────────────────────────────────────────
|
||
cmd_bootstrap() {
|
||
info "Bootstrapping autoresearch-quantum..."
|
||
|
||
# Python version check
|
||
local py_cmd
|
||
for candidate in python3.12 python3.11 python3; do
|
||
if command -v "$candidate" &>/dev/null; then
|
||
py_cmd="$candidate"
|
||
break
|
||
fi
|
||
done
|
||
if [[ -z "${py_cmd:-}" ]]; then
|
||
fail "Python 3.11+ not found. Install Python first."
|
||
exit 1
|
||
fi
|
||
|
||
local py_version
|
||
py_version=$("$py_cmd" -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')")
|
||
local py_major py_minor
|
||
py_major=$(echo "$py_version" | cut -d. -f1)
|
||
py_minor=$(echo "$py_version" | cut -d. -f2)
|
||
if (( py_major < 3 || py_minor < 11 )); then
|
||
fail "Python >= 3.11 required (found $py_version)"
|
||
exit 1
|
||
fi
|
||
ok "Python $py_version ($py_cmd)"
|
||
|
||
# Create venv
|
||
if [[ ! -d "$VENV_DIR" ]]; then
|
||
info "Creating virtual environment..."
|
||
"$py_cmd" -m venv "$VENV_DIR"
|
||
ok "Virtual environment created"
|
||
else
|
||
ok "Virtual environment exists"
|
||
fi
|
||
|
||
# Install package
|
||
info "Installing autoresearch-quantum + dependencies..."
|
||
"$PYTHON" -m pip install --upgrade pip --quiet
|
||
"$PYTHON" -m pip install -e "$PROJECT_ROOT[dev,notebooks]" --quiet
|
||
ok "Package installed"
|
||
|
||
# Create isolated Jupyter directories
|
||
ensure_dirs
|
||
|
||
# Install Jupyter kernel (into the venv, not user-global)
|
||
set_jupyter_env
|
||
"$PYTHON" -m ipykernel install \
|
||
--sys-prefix \
|
||
--name autoresearch-quantum \
|
||
--display-name "Autoresearch Quantum" \
|
||
--env IPYTHONDIR "$IPYTHONDIR" \
|
||
--env MPLCONFIGDIR "$MPLCONFIGDIR" \
|
||
2>/dev/null || true
|
||
ok "Jupyter kernel registered (venv-local)"
|
||
|
||
# Verify imports
|
||
if "$PYTHON" -c "from autoresearch_quantum.models import ExperimentSpec; print('Import OK')" &>/dev/null; then
|
||
ok "Import verification passed"
|
||
else
|
||
fail "Import verification failed — check installation"
|
||
exit 1
|
||
fi
|
||
|
||
echo ""
|
||
ok "${BOLD}Bootstrap complete!${NC}"
|
||
echo ""
|
||
echo " Next steps:"
|
||
echo " bash scripts/app.sh start # Launch JupyterLab"
|
||
echo " bash scripts/app.sh validate # Run validation suite"
|
||
}
|
||
|
||
# ── Start ─────────────────────────────────────────────────────────────
|
||
cmd_start() {
|
||
local open_browser=true
|
||
local foreground=false
|
||
local requested_port=""
|
||
|
||
while [[ $# -gt 0 ]]; do
|
||
case "$1" in
|
||
--no-open) open_browser=false; shift ;;
|
||
--foreground) foreground=true; shift ;;
|
||
--port)
|
||
[[ $# -ge 2 ]] || { fail "--port requires a value"; exit 1; }
|
||
requested_port="$2"; shift 2 ;;
|
||
*) fail "Unknown start option: $1"; exit 1 ;;
|
||
esac
|
||
done
|
||
|
||
if [[ ! -f "$PYTHON" ]]; then
|
||
fail "Not bootstrapped. Run: bash scripts/app.sh bootstrap"
|
||
exit 1
|
||
fi
|
||
|
||
ensure_dirs
|
||
set_jupyter_env
|
||
_cleanup_stale_runtime
|
||
|
||
# Check if already running
|
||
local existing_pid
|
||
if existing_pid=$(_get_running_pid); then
|
||
local url
|
||
url=$(_server_url || echo "http://localhost:8888/lab/tree/00_START_HERE.ipynb")
|
||
warn "JupyterLab already running (PID $existing_pid)"
|
||
echo " $url"
|
||
if $open_browser; then
|
||
if command -v open &>/dev/null; then
|
||
open "$url"
|
||
elif command -v xdg-open &>/dev/null; then
|
||
xdg-open "$url"
|
||
fi
|
||
fi
|
||
return 0
|
||
fi
|
||
|
||
# Find free port
|
||
local port="${requested_port:-8888}"
|
||
if [[ -z "$requested_port" ]]; then
|
||
while lsof -i :"$port" &>/dev/null; do
|
||
port=$((port + 1))
|
||
if (( port > 8899 )); then
|
||
fail "No free port in range 8888–8899"
|
||
fail "Check for orphan Jupyter processes: lsof -i :8888"
|
||
exit 1
|
||
fi
|
||
done
|
||
fi
|
||
|
||
# Foreground mode — Ctrl-C stops the server cleanly
|
||
if $foreground; then
|
||
info "Starting JupyterLab on port $port (foreground, Ctrl-C to stop)..."
|
||
local url="http://localhost:$port/lab/tree/00_START_HERE.ipynb"
|
||
echo ""
|
||
echo " ${BOLD}$url${NC}"
|
||
echo ""
|
||
if $open_browser; then
|
||
# Open browser after a short delay (in background)
|
||
(sleep 3 && {
|
||
if command -v open &>/dev/null; then
|
||
open "$url"
|
||
elif command -v xdg-open &>/dev/null; then
|
||
xdg-open "$url"
|
||
fi
|
||
}) &
|
||
fi
|
||
# exec replaces this shell — Ctrl-C goes straight to Jupyter
|
||
exec "$JUPYTER" lab \
|
||
--port="$port" \
|
||
--no-browser \
|
||
--ip=127.0.0.1 \
|
||
--notebook-dir="$PROJECT_ROOT/notebooks"
|
||
fi
|
||
|
||
# Background mode — PID tracked, survives shell close
|
||
info "Starting JupyterLab on port $port..."
|
||
|
||
cd "$PROJECT_ROOT"
|
||
: > "$LOG_FILE"
|
||
nohup "$JUPYTER" lab \
|
||
--port="$port" \
|
||
--no-browser \
|
||
--ip=127.0.0.1 \
|
||
--notebook-dir="$PROJECT_ROOT/notebooks" \
|
||
--ServerApp.token='' \
|
||
--ServerApp.password='' \
|
||
> "$LOG_FILE" 2>&1 &
|
||
|
||
local pid=$!
|
||
echo "$pid" > "$PID_FILE"
|
||
|
||
# Wait for server to start (check both port and runtime JSON)
|
||
local tries=0
|
||
while ! curl -s "http://localhost:$port/api" &>/dev/null; do
|
||
sleep 0.5
|
||
tries=$((tries + 1))
|
||
if (( tries > 40 )); then
|
||
fail "JupyterLab failed to start. Check: cat $LOG_FILE"
|
||
exit 1
|
||
fi
|
||
# Check if the process died
|
||
if ! _pid_alive "$pid"; then
|
||
fail "JupyterLab exited during startup. Recent log output:"
|
||
tail -n 20 "$LOG_FILE" >&2 || true
|
||
rm -f "$PID_FILE"
|
||
exit 1
|
||
fi
|
||
done
|
||
|
||
local url="http://localhost:$port/lab/tree/00_START_HERE.ipynb"
|
||
ok "JupyterLab running (PID $pid)"
|
||
echo ""
|
||
echo " ${BOLD}$url${NC}"
|
||
echo ""
|
||
|
||
if $open_browser; then
|
||
if command -v open &>/dev/null; then
|
||
open "$url"
|
||
elif command -v xdg-open &>/dev/null; then
|
||
xdg-open "$url"
|
||
fi
|
||
fi
|
||
}
|
||
|
||
# ── Stop ──────────────────────────────────────────────────────────────
|
||
cmd_stop() {
|
||
_cleanup_stale_runtime
|
||
|
||
if [[ -f "$PID_FILE" ]]; then
|
||
local pid
|
||
pid=$(cat "$PID_FILE")
|
||
if _pid_alive "$pid"; then
|
||
kill -TERM "$pid"
|
||
# Wait for graceful shutdown (up to 10 seconds)
|
||
local i=0
|
||
while _pid_alive "$pid" && (( i < 20 )); do
|
||
sleep 0.5
|
||
i=$((i + 1))
|
||
done
|
||
if _pid_alive "$pid"; then
|
||
warn "Process $pid did not stop gracefully, sending SIGKILL"
|
||
kill -KILL "$pid" 2>/dev/null || true
|
||
fi
|
||
ok "JupyterLab stopped (PID $pid)"
|
||
else
|
||
warn "PID $pid not running (stale pid file)"
|
||
fi
|
||
rm -f "$PID_FILE"
|
||
else
|
||
warn "No PID file — JupyterLab not managed by app.sh"
|
||
# Try to find and report any orphan Jupyter processes for this project
|
||
local orphans
|
||
orphans=$(ps aux | grep "[j]upyter.*--notebook-dir.*$PROJECT_ROOT" | awk '{print $2}' || true)
|
||
if [[ -n "$orphans" ]]; then
|
||
warn "Found possible orphan Jupyter process(es): $orphans"
|
||
echo " To stop them: kill $orphans"
|
||
fi
|
||
fi
|
||
|
||
_cleanup_stale_runtime
|
||
}
|
||
|
||
# ── Restart ───────────────────────────────────────────────────────────
|
||
cmd_restart() {
|
||
cmd_stop
|
||
sleep 1
|
||
cmd_start "$@"
|
||
}
|
||
|
||
# ── Status ────────────────────────────────────────────────────────────
|
||
cmd_status() {
|
||
echo ""
|
||
echo " ${BOLD}autoresearch-quantum${NC}"
|
||
echo " Root: $PROJECT_ROOT"
|
||
echo ""
|
||
|
||
# Git
|
||
if command -v git &>/dev/null && [[ -d "$PROJECT_ROOT/.git" ]]; then
|
||
local branch commit
|
||
branch=$(cd "$PROJECT_ROOT" && git branch --show-current 2>/dev/null || echo "?")
|
||
commit=$(cd "$PROJECT_ROOT" && git rev-parse --short HEAD 2>/dev/null || echo "?")
|
||
ok "Git: $branch @ $commit"
|
||
fi
|
||
|
||
# Venv
|
||
if [[ -f "$PYTHON" ]]; then
|
||
local py_ver
|
||
py_ver=$("$PYTHON" --version 2>&1)
|
||
ok "Virtual environment: $py_ver"
|
||
else
|
||
fail "Virtual environment: not found"
|
||
fi
|
||
|
||
# JupyterLab
|
||
_cleanup_stale_runtime
|
||
local running_pid
|
||
if running_pid=$(_get_running_pid); then
|
||
local url
|
||
url=$(_server_url || echo "(could not determine URL)")
|
||
ok "JupyterLab: running (PID $running_pid)"
|
||
echo " URL: $url"
|
||
echo " Log: $LOG_FILE"
|
||
else
|
||
warn "JupyterLab: not running"
|
||
# Check for orphans
|
||
local orphans
|
||
orphans=$(ps aux | grep "[j]upyter.*--notebook-dir.*$PROJECT_ROOT" | awk '{print $2}' || true)
|
||
if [[ -n "$orphans" ]]; then
|
||
warn " Orphan process(es) detected: $orphans"
|
||
echo " Stop them: kill $orphans"
|
||
fi
|
||
fi
|
||
|
||
# Port scan
|
||
local ports_in_use=""
|
||
for p in $(seq 8888 8899); do
|
||
if lsof -i :"$p" &>/dev/null; then
|
||
local owner
|
||
owner=$(lsof -ti :"$p" 2>/dev/null | head -1)
|
||
ports_in_use="$ports_in_use $p(pid:$owner)"
|
||
fi
|
||
done
|
||
if [[ -n "$ports_in_use" ]]; then
|
||
info "Ports in use:$ports_in_use"
|
||
fi
|
||
|
||
# Notebooks
|
||
local nb_count
|
||
nb_count=$(find "$PROJECT_ROOT/notebooks" -name "*.ipynb" | wc -l | tr -d ' ')
|
||
ok "Notebooks: $nb_count found"
|
||
|
||
# Learner progress
|
||
local progress_count
|
||
progress_count=$(find "$PROJECT_ROOT" -name "*_progress.json" 2>/dev/null | wc -l | tr -d ' ')
|
||
if (( progress_count > 0 )); then
|
||
ok "Learner progress files: $progress_count"
|
||
else
|
||
info "Learner progress files: none (fresh start)"
|
||
fi
|
||
|
||
echo ""
|
||
}
|
||
|
||
# ── Validate ──────────────────────────────────────────────────────────
|
||
cmd_validate() {
|
||
local mode="${1:---standard}"
|
||
|
||
if [[ ! -f "$PYTHON" ]]; then
|
||
fail "Not bootstrapped. Run: bash scripts/app.sh bootstrap"
|
||
exit 1
|
||
fi
|
||
|
||
echo ""
|
||
info "${BOLD}Running validation ($mode)...${NC}"
|
||
echo ""
|
||
|
||
local failed=0
|
||
|
||
# Ruff
|
||
info "Ruff lint..."
|
||
if "$VENV_DIR/bin/ruff" check src/ tests/ scripts/ --quiet; then
|
||
ok "Ruff: clean"
|
||
else
|
||
fail "Ruff: errors found"
|
||
failed=1
|
||
fi
|
||
|
||
# Mypy
|
||
info "Mypy type check..."
|
||
if "$PYTHON" -m mypy src/autoresearch_quantum/ --no-error-summary 2>/dev/null; then
|
||
ok "Mypy: clean"
|
||
else
|
||
fail "Mypy: type errors found"
|
||
failed=1
|
||
fi
|
||
|
||
if [[ "$mode" == "--quick" ]]; then
|
||
# Quick: unit tests only (no notebook execution)
|
||
info "Unit tests (quick)..."
|
||
if "$PYTHON" -m pytest tests/ -k "not test_notebook_executes and not test_browser" -q --tb=short --no-header 2>&1; then
|
||
ok "Unit tests: passed"
|
||
else
|
||
fail "Unit tests: failures"
|
||
failed=1
|
||
fi
|
||
else
|
||
# Standard: all tests except browser UX
|
||
info "Full test suite..."
|
||
if "$PYTHON" -m pytest tests/ -k "not test_browser" -v --tb=short --no-header 2>&1; then
|
||
ok "Tests: passed"
|
||
else
|
||
fail "Tests: failures"
|
||
failed=1
|
||
fi
|
||
fi
|
||
|
||
echo ""
|
||
if (( failed == 0 )); then
|
||
ok "${BOLD}All validation checks passed.${NC}"
|
||
else
|
||
fail "${BOLD}Some checks failed — see above.${NC}"
|
||
exit 1
|
||
fi
|
||
}
|
||
|
||
# ── Logs ──────────────────────────────────────────────────────────────
|
||
cmd_logs() {
|
||
if [[ -f "$LOG_FILE" ]]; then
|
||
if [[ "${1:-}" == "-f" || "${1:-}" == "--follow" ]]; then
|
||
tail -f "$LOG_FILE"
|
||
else
|
||
tail -n 80 "$LOG_FILE"
|
||
fi
|
||
else
|
||
warn "No log file found. Start JupyterLab first."
|
||
fi
|
||
}
|
||
|
||
# ── Reset (learner progress) ─────────────────────────────────────────
|
||
cmd_reset() {
|
||
info "Resetting learner progress..."
|
||
local count=0
|
||
while IFS= read -r -d '' f; do
|
||
rm "$f"
|
||
count=$((count + 1))
|
||
done < <(find "$PROJECT_ROOT" -name "*_progress.json" -print0 2>/dev/null)
|
||
ok "Removed $count progress file(s)"
|
||
info "Notebook outputs are preserved (use nbstripout to clear them)"
|
||
}
|
||
|
||
# ── Reset State (Jupyter runtime + UI state) ─────────────────────────
|
||
cmd_reset_state() {
|
||
# Stop server first if running
|
||
local running_pid
|
||
if running_pid=$(_get_running_pid 2>/dev/null); then
|
||
info "Stopping running server first..."
|
||
cmd_stop
|
||
fi
|
||
|
||
info "Cleaning Jupyter runtime state..."
|
||
local count=0
|
||
|
||
# Runtime JSON files
|
||
for f in "$JUPYTER_RUNTIME_DIR"/jpserver-*.json "$JUPYTER_RUNTIME_DIR"/jpserver-*-open.html \
|
||
"$JUPYTER_RUNTIME_DIR"/kernel-*.json; do
|
||
if [[ -f "$f" ]]; then
|
||
rm "$f"
|
||
count=$((count + 1))
|
||
fi
|
||
done
|
||
|
||
# Saved UI state (workspaces, trust DB)
|
||
for f in "$JUPYTER_DATA_DIR/nbsignatures.db" "$JUPYTER_DATA_DIR/notebook_secret"; do
|
||
if [[ -f "$f" ]]; then
|
||
rm "$f"
|
||
count=$((count + 1))
|
||
fi
|
||
done
|
||
if [[ -d "$JUPYTER_DATA_DIR/lab/workspaces" ]]; then
|
||
local ws_count
|
||
ws_count=$(find "$JUPYTER_DATA_DIR/lab/workspaces" -name "*.jupyterlab-workspace" 2>/dev/null | wc -l | tr -d ' ')
|
||
if (( ws_count > 0 )); then
|
||
find "$JUPYTER_DATA_DIR/lab/workspaces" -name "*.jupyterlab-workspace" -delete
|
||
count=$((count + ws_count))
|
||
fi
|
||
fi
|
||
|
||
# PID file
|
||
rm -f "$PID_FILE"
|
||
|
||
ok "Removed $count runtime/state file(s)"
|
||
info "JupyterLab will start fresh on next launch"
|
||
}
|
||
|
||
# ── Main dispatch ─────────────────────────────────────────────────────
|
||
case "${1:-help}" in
|
||
bootstrap) cmd_bootstrap ;;
|
||
start) shift; cmd_start "$@" ;;
|
||
stop) cmd_stop ;;
|
||
restart) shift; cmd_restart "$@" ;;
|
||
status) cmd_status ;;
|
||
validate) cmd_validate "${2:-}" ;;
|
||
logs) cmd_logs "${2:-}" ;;
|
||
reset) cmd_reset ;;
|
||
reset-state) cmd_reset_state ;;
|
||
help|--help|-h)
|
||
echo ""
|
||
echo " ${BOLD}autoresearch-quantum${NC} — lifecycle manager"
|
||
echo ""
|
||
echo " Usage: bash scripts/app.sh <command>"
|
||
echo ""
|
||
echo " Commands:"
|
||
echo " bootstrap Create venv, install deps, verify imports"
|
||
echo " start [options] Launch JupyterLab"
|
||
echo " --no-open Don't open browser"
|
||
echo " --foreground Run in foreground (Ctrl-C to stop)"
|
||
echo " --port PORT Use a specific port"
|
||
echo " stop Stop JupyterLab"
|
||
echo " restart [options] Stop + start (same options as start)"
|
||
echo " status Show service and project status"
|
||
echo " validate [--quick] Run lint, type check, and tests"
|
||
echo " logs [-f] Show or follow JupyterLab output"
|
||
echo " reset Delete learner progress files"
|
||
echo " reset-state Reset Jupyter runtime + UI state"
|
||
echo ""
|
||
;;
|
||
*)
|
||
fail "Unknown command: $1"
|
||
echo " Run 'bash scripts/app.sh help' for usage."
|
||
exit 1
|
||
;;
|
||
esac
|