mirror of
https://github.com/saymrwulf/autoresearch-quantum.git
synced 2026-05-14 20:37:51 +00:00
Users now see explicit messages about whether Jupyter runs in background or foreground, what happens when the terminal closes, and how to stop the server.
644 lines
22 KiB
Bash
Executable file
644 lines
22 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 ""
|
||
echo " $url"
|
||
echo ""
|
||
echo " To stop: bash scripts/app.sh stop"
|
||
echo " To restart: bash scripts/app.sh restart"
|
||
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 mode)..."
|
||
local url="http://localhost:$port/lab/tree/00_START_HERE.ipynb"
|
||
echo ""
|
||
echo " ${BOLD}$url${NC}"
|
||
echo ""
|
||
echo " Jupyter is running in the foreground — this terminal is occupied."
|
||
echo " Press Ctrl-C to stop. Closing this terminal also stops Jupyter."
|
||
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) on port $port"
|
||
echo ""
|
||
echo " ${BOLD}$url${NC}"
|
||
echo ""
|
||
echo " Jupyter is running in the background — this terminal is free."
|
||
echo " It will keep running even if you close this terminal."
|
||
echo ""
|
||
echo " To stop: bash scripts/app.sh stop"
|
||
echo " To check: bash scripts/app.sh status"
|
||
echo " Logs: bash scripts/app.sh logs -f"
|
||
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
|