mirror of
https://github.com/saymrwulf/QuantumLearning.git
synced 2026-05-14 20:58:00 +00:00
704 lines
17 KiB
Bash
Executable file
704 lines
17 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
LOG_DIR="$ROOT_DIR/.logs"
|
|
LOG_FILE="$LOG_DIR/jupyterlab.log"
|
|
|
|
usage() {
|
|
cat <<'EOF'
|
|
Usage: bash ./scripts/app.sh <command> [options]
|
|
|
|
Consumer-facing control surface for QuantumLearning.
|
|
|
|
Commands:
|
|
bootstrap [--skip-browser] [--run-validation]
|
|
Create or refresh the repo-local environment.
|
|
|
|
start [--port PORT] [--open] [--foreground] [--restart]
|
|
Start the repo-local Jupyter server.
|
|
Default mode is background service mode.
|
|
|
|
stop
|
|
Stop the repo-local Jupyter server for this project.
|
|
|
|
reset-state
|
|
Remove repo-local Jupyter runtime and saved UI state files.
|
|
|
|
restart [--port PORT] [--open] [--foreground]
|
|
Restart the repo-local Jupyter server.
|
|
|
|
status
|
|
Print the current project and Jupyter status.
|
|
|
|
url
|
|
Print the current Jupyter URL for this project.
|
|
|
|
open
|
|
Open the current Jupyter URL in the default browser.
|
|
|
|
logs [-f|--follow]
|
|
Show the Jupyter log file. Use -f to follow live output.
|
|
|
|
validate [--quick|--standard|--full]
|
|
Run the validation suite.
|
|
|
|
Examples:
|
|
bash ./scripts/app.sh bootstrap
|
|
bash ./scripts/app.sh start --open
|
|
bash ./scripts/app.sh restart
|
|
bash ./scripts/app.sh status
|
|
bash ./scripts/app.sh stop
|
|
bash ./scripts/app.sh reset-state
|
|
bash ./scripts/app.sh validate --quick
|
|
EOF
|
|
}
|
|
|
|
die() {
|
|
echo "$*" >&2
|
|
exit 1
|
|
}
|
|
|
|
ensure_dirs() {
|
|
mkdir -p \
|
|
"$ROOT_DIR/.cache/matplotlib" \
|
|
"$ROOT_DIR/.ipython" \
|
|
"$ROOT_DIR/.jupyter_config" \
|
|
"$ROOT_DIR/.jupyter_data" \
|
|
"$ROOT_DIR/.jupyter_runtime" \
|
|
"$LOG_DIR"
|
|
}
|
|
|
|
ensure_venv() {
|
|
if [[ ! -x "$ROOT_DIR/.venv/bin/python" ]]; then
|
|
die "Missing repo-local .venv. Run: bash ./scripts/app.sh bootstrap"
|
|
fi
|
|
}
|
|
|
|
set_jupyter_env() {
|
|
export PATH="$ROOT_DIR/.venv/bin:/usr/bin:/bin:/usr/sbin:/sbin"
|
|
export JUPYTER_CONFIG_DIR="$ROOT_DIR/.jupyter_config"
|
|
export JUPYTER_DATA_DIR="$ROOT_DIR/.jupyter_data"
|
|
export JUPYTER_RUNTIME_DIR="$ROOT_DIR/.jupyter_runtime"
|
|
export IPYTHONDIR="$ROOT_DIR/.ipython"
|
|
export MPLCONFIGDIR="$ROOT_DIR/.cache/matplotlib"
|
|
}
|
|
|
|
ensure_kernel() {
|
|
local kernel_dir="$ROOT_DIR/.venv/share/jupyter/kernels/quantum-learning"
|
|
if [[ ! -f "$kernel_dir/kernel.json" ]]; then
|
|
"$ROOT_DIR/.venv/bin/python" -m ipykernel install \
|
|
--sys-prefix \
|
|
--name quantum-learning \
|
|
--display-name "QuantumLearning (.venv)" \
|
|
--env IPYTHONDIR "$IPYTHONDIR" \
|
|
--env MPLCONFIGDIR "$MPLCONFIGDIR"
|
|
fi
|
|
}
|
|
|
|
server_json_for_root() {
|
|
"$ROOT_DIR/.venv/bin/python" - <<'PY' "$ROOT_DIR"
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
root = Path(sys.argv[1]).resolve()
|
|
runtime_dir = root / ".jupyter_runtime"
|
|
matches = []
|
|
for path in runtime_dir.glob("jpserver-*.json"):
|
|
try:
|
|
payload = json.loads(path.read_text())
|
|
except Exception:
|
|
continue
|
|
if Path(payload.get("root_dir", "")).resolve() == root:
|
|
matches.append((path.stat().st_mtime, path))
|
|
|
|
if matches:
|
|
print(max(matches)[1])
|
|
PY
|
|
}
|
|
|
|
server_json_for_pid() {
|
|
local pid="$1"
|
|
"$ROOT_DIR/.venv/bin/python" - <<'PY' "$ROOT_DIR" "$pid"
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
root = Path(sys.argv[1]).resolve()
|
|
target_pid = int(sys.argv[2])
|
|
runtime_dir = root / ".jupyter_runtime"
|
|
matches = []
|
|
for path in runtime_dir.glob("jpserver-*.json"):
|
|
try:
|
|
payload = json.loads(path.read_text())
|
|
except Exception:
|
|
continue
|
|
if Path(payload.get("root_dir", "")).resolve() == root and int(payload.get("pid", -1)) == target_pid:
|
|
matches.append((path.stat().st_mtime, path))
|
|
|
|
if matches:
|
|
print(max(matches)[1])
|
|
PY
|
|
}
|
|
|
|
server_field() {
|
|
local json_path="$1"
|
|
local field_name="$2"
|
|
"$ROOT_DIR/.venv/bin/python" - <<'PY' "$json_path" "$field_name"
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
path = Path(sys.argv[1])
|
|
field_name = sys.argv[2]
|
|
payload = json.loads(path.read_text())
|
|
value = payload[field_name]
|
|
print(value)
|
|
PY
|
|
}
|
|
|
|
server_url_from_json() {
|
|
local json_path="$1"
|
|
"$ROOT_DIR/.venv/bin/python" - <<'PY' "$json_path"
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
path = Path(sys.argv[1])
|
|
payload = json.loads(path.read_text())
|
|
print(f"{payload['url']}lab/tree/notebooks/START_HERE.ipynb?token={payload['token']}")
|
|
PY
|
|
}
|
|
|
|
cleanup_stale_runtime_files() {
|
|
"$ROOT_DIR/.venv/bin/python" - <<'PY' "$ROOT_DIR" "${1:-0}"
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
root = Path(sys.argv[1]).resolve()
|
|
verbose = sys.argv[2] == "1"
|
|
runtime_dir = root / ".jupyter_runtime"
|
|
removed = []
|
|
|
|
for path in sorted(runtime_dir.glob("jpserver-*.json")):
|
|
try:
|
|
payload = json.loads(path.read_text())
|
|
except Exception:
|
|
continue
|
|
|
|
if Path(payload.get("root_dir", "")).resolve() != root:
|
|
continue
|
|
|
|
pid = int(payload.get("pid", -1))
|
|
alive = False
|
|
if pid > 0:
|
|
try:
|
|
os.kill(pid, 0)
|
|
except ProcessLookupError:
|
|
alive = False
|
|
except PermissionError:
|
|
alive = True
|
|
else:
|
|
alive = True
|
|
|
|
if alive:
|
|
continue
|
|
|
|
for candidate in (path, path.with_name(f"{path.stem}-open.html")):
|
|
if candidate.exists():
|
|
candidate.unlink()
|
|
removed.append(candidate)
|
|
|
|
if verbose:
|
|
for path in removed:
|
|
print(path)
|
|
PY
|
|
}
|
|
|
|
purge_runtime_artifacts() {
|
|
"$ROOT_DIR/.venv/bin/python" - <<'PY' "$ROOT_DIR" "${1:-0}"
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
root = Path(sys.argv[1]).resolve()
|
|
verbose = sys.argv[2] == "1"
|
|
runtime_dir = root / ".jupyter_runtime"
|
|
patterns = (
|
|
"jpserver-*.json",
|
|
"jpserver-*-open.html",
|
|
"nbserver-*.json",
|
|
"nbserver-*-open.html",
|
|
"kernel-*.json",
|
|
)
|
|
removed = []
|
|
|
|
for pattern in patterns:
|
|
for path in sorted(runtime_dir.glob(pattern)):
|
|
if path.exists():
|
|
path.unlink()
|
|
removed.append(path)
|
|
|
|
if verbose:
|
|
for path in removed:
|
|
print(path)
|
|
PY
|
|
}
|
|
|
|
purge_saved_ui_state() {
|
|
"$ROOT_DIR/.venv/bin/python" - <<'PY' "$ROOT_DIR" "${1:-0}"
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
root = Path(sys.argv[1]).resolve()
|
|
verbose = sys.argv[2] == "1"
|
|
data_dir = root / ".jupyter_data"
|
|
paths = [
|
|
data_dir / "nbsignatures.db",
|
|
data_dir / "notebook_secret",
|
|
]
|
|
workspace_dir = data_dir / "lab" / "workspaces"
|
|
removed = []
|
|
|
|
for path in paths:
|
|
if path.exists():
|
|
path.unlink()
|
|
removed.append(path)
|
|
|
|
for path in sorted(workspace_dir.glob("*.jupyterlab-workspace")):
|
|
if path.exists():
|
|
path.unlink()
|
|
removed.append(path)
|
|
|
|
if verbose:
|
|
for path in removed:
|
|
print(path)
|
|
PY
|
|
}
|
|
|
|
print_ui_state_summary() {
|
|
local workspace_count="0"
|
|
if compgen -G "$ROOT_DIR/.jupyter_data/lab/workspaces/*.jupyterlab-workspace" >/dev/null; then
|
|
workspace_count="$(find "$ROOT_DIR/.jupyter_data/lab/workspaces" -maxdepth 1 -name '*.jupyterlab-workspace' | wc -l | tr -d ' ')"
|
|
fi
|
|
|
|
local trust_db="absent"
|
|
local notebook_secret="absent"
|
|
[[ -f "$ROOT_DIR/.jupyter_data/nbsignatures.db" ]] && trust_db="present"
|
|
[[ -f "$ROOT_DIR/.jupyter_data/notebook_secret" ]] && notebook_secret="present"
|
|
|
|
if [[ "$workspace_count" == "0" && "$trust_db" == "absent" && "$notebook_secret" == "absent" ]]; then
|
|
echo "Jupyter UI state: clean"
|
|
return 0
|
|
fi
|
|
|
|
echo "Jupyter UI state: workspaces=$workspace_count, trust-db=$trust_db, notebook-secret=$notebook_secret"
|
|
echo " Clean slate: bash ./scripts/app.sh reset-state"
|
|
}
|
|
|
|
have_running_server() {
|
|
local json_path
|
|
cleanup_stale_runtime_files >/dev/null
|
|
json_path="$(server_json_for_root || true)"
|
|
if [[ -z "$json_path" ]]; then
|
|
return 1
|
|
fi
|
|
|
|
local pid
|
|
pid="$(server_field "$json_path" "pid")"
|
|
kill -0 "$pid" >/dev/null 2>&1
|
|
}
|
|
|
|
print_running_server() {
|
|
local json_path url pid
|
|
cleanup_stale_runtime_files >/dev/null
|
|
json_path="$(server_json_for_root || true)"
|
|
if [[ -z "$json_path" ]]; then
|
|
die "No repo-local Jupyter runtime file found. Run: bash ./scripts/app.sh start"
|
|
fi
|
|
url="$(server_url_from_json "$json_path")"
|
|
pid="$(server_field "$json_path" "pid")"
|
|
echo "Jupyter URL: $url"
|
|
echo "PID: $pid"
|
|
echo "Runtime file: $json_path"
|
|
echo "Log file: $LOG_FILE"
|
|
}
|
|
|
|
stop_server() {
|
|
ensure_venv
|
|
ensure_dirs
|
|
|
|
local json_path pid removed_paths
|
|
cleanup_stale_runtime_files >/dev/null
|
|
json_path="$(server_json_for_root || true)"
|
|
if [[ -z "$json_path" ]]; then
|
|
echo "No repo-local Jupyter runtime file found."
|
|
removed_paths="$(purge_runtime_artifacts 1 || true)"
|
|
if [[ -n "$removed_paths" ]]; then
|
|
echo "Removed stale runtime files:"
|
|
echo "$removed_paths"
|
|
fi
|
|
return 0
|
|
fi
|
|
|
|
pid="$(server_field "$json_path" "pid")"
|
|
if ! kill -0 "$pid" >/dev/null 2>&1; then
|
|
removed_paths="$(cleanup_stale_runtime_files 1 || true)"
|
|
local extra_removed
|
|
extra_removed="$(purge_runtime_artifacts 1 || true)"
|
|
echo "Jupyter runtime file exists, but the process is not running."
|
|
echo "Runtime file: $json_path"
|
|
if [[ -n "$removed_paths" ]]; then
|
|
echo "Removed stale runtime files:"
|
|
echo "$removed_paths"
|
|
fi
|
|
if [[ -n "$extra_removed" ]]; then
|
|
echo "Removed leftover runtime artifacts:"
|
|
echo "$extra_removed"
|
|
fi
|
|
return 0
|
|
fi
|
|
|
|
kill -TERM "$pid"
|
|
for _ in $(seq 1 30); do
|
|
if ! kill -0 "$pid" >/dev/null 2>&1; then
|
|
cleanup_stale_runtime_files >/dev/null
|
|
purge_runtime_artifacts >/dev/null
|
|
echo "Stopped Jupyter (pid $pid)."
|
|
return 0
|
|
fi
|
|
sleep 1
|
|
done
|
|
|
|
die "Jupyter did not stop cleanly after 30 seconds. Inspect: $LOG_FILE"
|
|
}
|
|
|
|
start_server() {
|
|
local port="$1"
|
|
local foreground="$2"
|
|
local open_browser="$3"
|
|
local restart="$4"
|
|
|
|
ensure_venv
|
|
ensure_dirs
|
|
set_jupyter_env
|
|
ensure_kernel
|
|
cleanup_stale_runtime_files >/dev/null
|
|
|
|
if have_running_server; then
|
|
if [[ "$restart" == "1" ]]; then
|
|
stop_server
|
|
else
|
|
echo "Jupyter is already running for this project."
|
|
print_running_server
|
|
if [[ "$open_browser" == "1" ]]; then
|
|
local existing_url
|
|
existing_url="$(server_url_from_json "$(server_json_for_root)")"
|
|
open "$existing_url" >/dev/null 2>&1 || true
|
|
fi
|
|
return 0
|
|
fi
|
|
fi
|
|
|
|
purge_runtime_artifacts >/dev/null
|
|
|
|
if [[ "$foreground" == "1" ]]; then
|
|
exec "$ROOT_DIR/.venv/bin/jupyter" lab \
|
|
--no-browser \
|
|
--ip=127.0.0.1 \
|
|
--port="$port"
|
|
fi
|
|
|
|
: > "$LOG_FILE"
|
|
nohup "$ROOT_DIR/.venv/bin/jupyter" lab \
|
|
--no-browser \
|
|
--ip=127.0.0.1 \
|
|
--port="$port" \
|
|
< /dev/null \
|
|
>>"$LOG_FILE" 2>&1 &
|
|
local launch_pid=$!
|
|
|
|
local json_path=""
|
|
for _ in $(seq 1 60); do
|
|
if ! kill -0 "$launch_pid" >/dev/null 2>&1; then
|
|
echo "Jupyter exited during startup. Recent log output:" >&2
|
|
tail -n 40 "$LOG_FILE" >&2 || true
|
|
exit 1
|
|
fi
|
|
|
|
json_path="$(server_json_for_pid "$launch_pid" || true)"
|
|
if [[ -n "$json_path" ]]; then
|
|
local url
|
|
url="$(server_url_from_json "$json_path")"
|
|
echo "Jupyter started."
|
|
echo "URL: $url"
|
|
echo "PID: $launch_pid"
|
|
echo "Log: $LOG_FILE"
|
|
if [[ "$open_browser" == "1" ]]; then
|
|
open "$url" >/dev/null 2>&1 || true
|
|
fi
|
|
return 0
|
|
fi
|
|
sleep 1
|
|
done
|
|
|
|
die "Timed out waiting for Jupyter startup. Inspect: $LOG_FILE"
|
|
}
|
|
|
|
show_logs() {
|
|
ensure_dirs
|
|
if [[ ! -f "$LOG_FILE" ]]; then
|
|
echo "No Jupyter log file exists yet."
|
|
return 0
|
|
fi
|
|
|
|
if [[ "${1:-}" == "--follow" || "${1:-}" == "-f" ]]; then
|
|
tail -f "$LOG_FILE"
|
|
else
|
|
tail -n 80 "$LOG_FILE"
|
|
fi
|
|
}
|
|
|
|
validate_command() {
|
|
bash "$ROOT_DIR/scripts/run_validation.sh" "$@"
|
|
}
|
|
|
|
reset_state_command() {
|
|
ensure_venv
|
|
ensure_dirs
|
|
|
|
if have_running_server; then
|
|
stop_server >/dev/null
|
|
else
|
|
cleanup_stale_runtime_files >/dev/null
|
|
purge_runtime_artifacts >/dev/null
|
|
fi
|
|
|
|
local removed_runtime removed_ui
|
|
removed_runtime="$(purge_runtime_artifacts 1 || true)"
|
|
removed_ui="$(purge_saved_ui_state 1 || true)"
|
|
|
|
if [[ -z "$removed_runtime" && -z "$removed_ui" ]]; then
|
|
echo "Jupyter state was already clean."
|
|
return 0
|
|
fi
|
|
|
|
if [[ -n "$removed_runtime" ]]; then
|
|
echo "Removed runtime artifacts:"
|
|
echo "$removed_runtime"
|
|
fi
|
|
|
|
if [[ -n "$removed_ui" ]]; then
|
|
echo "Removed saved UI state:"
|
|
echo "$removed_ui"
|
|
fi
|
|
}
|
|
|
|
status_command() {
|
|
cd "$ROOT_DIR"
|
|
ensure_dirs
|
|
|
|
local branch commit git_state
|
|
branch="$(git branch --show-current)"
|
|
commit="$(git rev-parse --short HEAD)"
|
|
if [[ -n "$(git status --short)" ]]; then
|
|
git_state="dirty"
|
|
else
|
|
git_state="clean"
|
|
fi
|
|
|
|
echo "Root: $ROOT_DIR"
|
|
echo "Git: $branch @ $commit ($git_state)"
|
|
|
|
if [[ -x "$ROOT_DIR/.venv/bin/python" ]]; then
|
|
local python_version removed_runtime json_path pid url
|
|
python_version="$("$ROOT_DIR/.venv/bin/python" -c 'import sys; print(sys.version.split()[0])')"
|
|
echo "Venv: present (Python $python_version)"
|
|
cleanup_stale_runtime_files >/dev/null
|
|
if ! have_running_server; then
|
|
removed_runtime="$(purge_runtime_artifacts 1 || true)"
|
|
else
|
|
removed_runtime=""
|
|
fi
|
|
json_path="$(server_json_for_root || true)"
|
|
|
|
if compgen -G "$ROOT_DIR/.playwright-browsers/chromium-*" >/dev/null; then
|
|
echo "Browser runtime: installed"
|
|
else
|
|
echo "Browser runtime: missing"
|
|
fi
|
|
|
|
if [[ -f "$LOG_FILE" ]]; then
|
|
echo "Jupyter log: $LOG_FILE"
|
|
else
|
|
echo "Jupyter log: none yet"
|
|
fi
|
|
|
|
if [[ -n "$json_path" ]]; then
|
|
pid="$(server_field "$json_path" "pid")"
|
|
url="$(server_url_from_json "$json_path")"
|
|
if kill -0 "$pid" >/dev/null 2>&1; then
|
|
echo "Jupyter: running (pid $pid)"
|
|
echo " URL: $url"
|
|
echo " Runtime file: $json_path"
|
|
else
|
|
echo "Jupyter: runtime file exists but process is not running"
|
|
echo " Runtime file: $json_path"
|
|
fi
|
|
else
|
|
echo "Jupyter: no repo-local runtime file found"
|
|
if [[ -n "${removed_runtime:-}" ]]; then
|
|
echo " Purged stale runtime artifacts:"
|
|
echo "$removed_runtime"
|
|
fi
|
|
fi
|
|
|
|
print_ui_state_summary
|
|
else
|
|
echo "Venv: missing"
|
|
if compgen -G "$ROOT_DIR/.playwright-browsers/chromium-*" >/dev/null; then
|
|
echo "Browser runtime: installed"
|
|
else
|
|
echo "Browser runtime: missing"
|
|
fi
|
|
if [[ -f "$LOG_FILE" ]]; then
|
|
echo "Jupyter log: $LOG_FILE"
|
|
else
|
|
echo "Jupyter log: none yet"
|
|
fi
|
|
echo "Jupyter: repo-local environment is missing"
|
|
print_ui_state_summary
|
|
fi
|
|
|
|
echo "Consumer:"
|
|
echo " Start: bash ./scripts/app.sh start --open"
|
|
echo " Stop: bash ./scripts/app.sh stop"
|
|
echo " Restart: bash ./scripts/app.sh restart"
|
|
echo " Reset: bash ./scripts/app.sh reset-state"
|
|
echo " Logs: bash ./scripts/app.sh logs -f"
|
|
echo "Validation:"
|
|
echo " Quick: bash ./scripts/app.sh validate --quick"
|
|
echo " Full: bash ./scripts/app.sh validate --full"
|
|
}
|
|
|
|
start_command() {
|
|
local port="8888"
|
|
local foreground="0"
|
|
local open_browser="0"
|
|
local restart="0"
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--port)
|
|
[[ $# -ge 2 ]] || die "--port requires a value"
|
|
port="$2"
|
|
shift 2
|
|
;;
|
|
--open)
|
|
open_browser="1"
|
|
shift
|
|
;;
|
|
--foreground)
|
|
foreground="1"
|
|
shift
|
|
;;
|
|
--restart)
|
|
restart="1"
|
|
shift
|
|
;;
|
|
-h|--help)
|
|
cat <<'EOF'
|
|
Usage: bash ./scripts/app.sh start [--port PORT] [--open] [--foreground] [--restart]
|
|
EOF
|
|
return 0
|
|
;;
|
|
*)
|
|
die "Unknown start option: $1"
|
|
;;
|
|
esac
|
|
done
|
|
|
|
[[ "$port" =~ ^[0-9]+$ ]] || die "Port must be an integer, got: $port"
|
|
start_server "$port" "$foreground" "$open_browser" "$restart"
|
|
}
|
|
|
|
restart_command() {
|
|
start_command --restart "$@"
|
|
}
|
|
|
|
if [[ $# -eq 0 ]]; then
|
|
usage
|
|
exit 1
|
|
fi
|
|
|
|
COMMAND="$1"
|
|
shift
|
|
|
|
case "$COMMAND" in
|
|
bootstrap)
|
|
exec bash "$ROOT_DIR/scripts/bootstrap_mac.sh" "$@"
|
|
;;
|
|
start)
|
|
start_command "$@"
|
|
;;
|
|
stop)
|
|
stop_server
|
|
;;
|
|
reset-state)
|
|
reset_state_command "$@"
|
|
;;
|
|
restart)
|
|
restart_command "$@"
|
|
;;
|
|
status)
|
|
status_command
|
|
;;
|
|
_status_internal)
|
|
status_command
|
|
;;
|
|
url)
|
|
ensure_venv
|
|
cleanup_stale_runtime_files >/dev/null
|
|
json_path="$(server_json_for_root || true)"
|
|
[[ -n "$json_path" ]] || die "No repo-local Jupyter runtime file found. Run: bash ./scripts/app.sh start"
|
|
server_url_from_json "$json_path"
|
|
;;
|
|
open)
|
|
ensure_venv
|
|
cleanup_stale_runtime_files >/dev/null
|
|
json_path="$(server_json_for_root || true)"
|
|
[[ -n "$json_path" ]] || die "No repo-local Jupyter runtime file found. Run: bash ./scripts/app.sh start"
|
|
url="$(server_url_from_json "$json_path")"
|
|
open "$url"
|
|
echo "Opened: $url"
|
|
;;
|
|
logs)
|
|
show_logs "${1:-}"
|
|
;;
|
|
validate)
|
|
validate_command "$@"
|
|
;;
|
|
-h|--help|help)
|
|
usage
|
|
;;
|
|
*)
|
|
usage >&2
|
|
die "Unknown command: $COMMAND"
|
|
;;
|
|
esac
|