mirror of
https://github.com/saymrwulf/QuantumLearning.git
synced 2026-05-14 20:58:00 +00:00
Harden consumer app lifecycle and docs
This commit is contained in:
parent
9fe3ed6b22
commit
ef778073de
10 changed files with 636 additions and 93 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -12,6 +12,7 @@ __pycache__/
|
|||
.DS_Store
|
||||
.coverage
|
||||
.coverage.*
|
||||
.logs/
|
||||
.playwright-browsers/
|
||||
.tmp_test_artifacts/
|
||||
htmlcov/
|
||||
|
|
|
|||
10
FIRST_RUN.md
10
FIRST_RUN.md
|
|
@ -7,20 +7,20 @@ Use this the first time you open the platform, or anytime you want a clean local
|
|||
1. If this is a fresh Mac or a fresh clone, bootstrap the repo first:
|
||||
|
||||
```bash
|
||||
bash ./scripts/bootstrap_mac.sh
|
||||
bash ./scripts/app.sh bootstrap
|
||||
```
|
||||
|
||||
2. Check the local state:
|
||||
|
||||
```bash
|
||||
bash ./scripts/project_status.sh
|
||||
bash ./scripts/run_validation.sh --quick
|
||||
bash ./scripts/app.sh status
|
||||
bash ./scripts/app.sh validate --quick
|
||||
```
|
||||
|
||||
3. Start the repo-local server:
|
||||
|
||||
```bash
|
||||
./scripts/start_jupyter.sh
|
||||
bash ./scripts/app.sh start --open
|
||||
```
|
||||
|
||||
4. Open the localhost URL printed by the script.
|
||||
|
|
@ -67,7 +67,7 @@ Use this the first time you open the platform, or anytime you want a clean local
|
|||
If you want a quick terminal check before opening the browser:
|
||||
|
||||
```bash
|
||||
bash ./scripts/run_validation.sh --quick
|
||||
bash ./scripts/app.sh validate --quick
|
||||
```
|
||||
|
||||
For the full operational guide, see [OPERATIONS.md](/Users/oho/GitClone/CodexProjects/QuantumLearning/OPERATIONS.md).
|
||||
|
|
|
|||
|
|
@ -34,10 +34,10 @@ Then bootstrap the repo:
|
|||
|
||||
```bash
|
||||
cd /path/to/QuantumLearning
|
||||
bash ./scripts/bootstrap_mac.sh
|
||||
bash ./scripts/app.sh bootstrap
|
||||
```
|
||||
|
||||
That script will:
|
||||
That command will:
|
||||
|
||||
- create `.venv/` if needed
|
||||
- install the Python dependencies from `pyproject.toml`
|
||||
|
|
@ -46,24 +46,45 @@ That script will:
|
|||
If you want the bootstrap to finish with a full validation run:
|
||||
|
||||
```bash
|
||||
bash ./scripts/bootstrap_mac.sh --run-validation
|
||||
bash ./scripts/app.sh bootstrap --run-validation
|
||||
```
|
||||
|
||||
If you want to delay the browser download:
|
||||
|
||||
```bash
|
||||
bash ./scripts/bootstrap_mac.sh --skip-browser
|
||||
bash ./scripts/app.sh bootstrap --skip-browser
|
||||
```
|
||||
|
||||
## Start And Use The Course
|
||||
|
||||
Start JupyterLab:
|
||||
Start the app:
|
||||
|
||||
```bash
|
||||
./scripts/start_jupyter.sh
|
||||
bash ./scripts/app.sh start --open
|
||||
```
|
||||
|
||||
Open the exact localhost URL printed by the script. Do not guess the port; Jupyter will automatically move to the next free port if the requested one is already occupied.
|
||||
That starts the repo-local Jupyter server as a detached background service, prints the exact URL, cleans up stale runtime files when needed, and opens it in the default browser.
|
||||
|
||||
If you run the launcher from an automation shell that reaps background child processes when the shell exits, use foreground mode instead:
|
||||
|
||||
```bash
|
||||
bash ./scripts/app.sh start --foreground
|
||||
```
|
||||
|
||||
Other normal consumer commands:
|
||||
|
||||
```bash
|
||||
bash ./scripts/app.sh status
|
||||
bash ./scripts/app.sh restart
|
||||
bash ./scripts/app.sh stop
|
||||
bash ./scripts/app.sh logs -f
|
||||
```
|
||||
|
||||
If you want foreground mode for debugging:
|
||||
|
||||
```bash
|
||||
bash ./scripts/app.sh start --foreground
|
||||
```
|
||||
|
||||
Use the course in this order:
|
||||
|
||||
|
|
@ -75,7 +96,7 @@ Use the course in this order:
|
|||
Use the repo-native status script:
|
||||
|
||||
```bash
|
||||
bash ./scripts/project_status.sh
|
||||
bash ./scripts/app.sh status
|
||||
```
|
||||
|
||||
It reports:
|
||||
|
|
@ -86,7 +107,7 @@ It reports:
|
|||
- whether the project-local Playwright browser runtime is installed
|
||||
- whether a repo-local Jupyter server is running and, if so, its full URL
|
||||
|
||||
If you lose the Jupyter URL, `bash ./scripts/project_status.sh` is the fastest recovery path.
|
||||
If you lose the Jupyter URL, `bash ./scripts/app.sh status` is the fastest recovery path.
|
||||
|
||||
In restricted automation sandboxes, the script may report that the live process probe is unavailable. In a normal terminal on your Mac, it will report the running server directly when the runtime file and process are both visible.
|
||||
|
||||
|
|
@ -95,7 +116,7 @@ In restricted automation sandboxes, the script may report that the live process
|
|||
Quick local validation:
|
||||
|
||||
```bash
|
||||
bash ./scripts/run_validation.sh --quick
|
||||
bash ./scripts/app.sh validate --quick
|
||||
```
|
||||
|
||||
This runs:
|
||||
|
|
@ -106,7 +127,7 @@ This runs:
|
|||
Standard validation:
|
||||
|
||||
```bash
|
||||
bash ./scripts/run_validation.sh --standard
|
||||
bash ./scripts/app.sh validate --standard
|
||||
```
|
||||
|
||||
This runs the full pytest suite without coverage reporting.
|
||||
|
|
@ -114,7 +135,7 @@ This runs the full pytest suite without coverage reporting.
|
|||
Release-style validation:
|
||||
|
||||
```bash
|
||||
bash ./scripts/run_validation.sh --full
|
||||
bash ./scripts/app.sh validate --full
|
||||
```
|
||||
|
||||
This runs:
|
||||
|
|
@ -151,15 +172,15 @@ It now validates learner-style flows such as:
|
|||
|
||||
If something feels wrong, use this order:
|
||||
|
||||
1. `bash ./scripts/project_status.sh`
|
||||
2. `bash ./scripts/run_validation.sh --quick`
|
||||
3. if needed, `bash ./scripts/run_validation.sh --full`
|
||||
4. restart Jupyter with `./scripts/start_jupyter.sh`
|
||||
1. `bash ./scripts/app.sh status`
|
||||
2. `bash ./scripts/app.sh validate --quick`
|
||||
3. if needed, `bash ./scripts/app.sh validate --full`
|
||||
4. restart Jupyter with `bash ./scripts/app.sh restart`
|
||||
|
||||
If `.venv/` is missing or broken, re-run:
|
||||
|
||||
```bash
|
||||
bash ./scripts/bootstrap_mac.sh
|
||||
bash ./scripts/app.sh bootstrap
|
||||
```
|
||||
|
||||
## Another-Mac Transfer Pattern
|
||||
|
|
@ -168,8 +189,19 @@ The clean transfer model is:
|
|||
|
||||
1. clone or copy the repo to the new Mac
|
||||
2. make sure Python `3.12` exists
|
||||
3. run `bash ./scripts/bootstrap_mac.sh`
|
||||
4. run `bash ./scripts/run_validation.sh --quick`
|
||||
5. run `./scripts/start_jupyter.sh`
|
||||
3. run `bash ./scripts/app.sh bootstrap`
|
||||
4. run `bash ./scripts/app.sh validate --quick`
|
||||
5. run `bash ./scripts/app.sh start --open`
|
||||
|
||||
That is the intended self-service installation path. You do not need agent support for normal setup, validation, status checks, or Jupyter startup.
|
||||
|
||||
## Underlying Plumbing
|
||||
|
||||
The lower-level scripts still exist for direct use or debugging:
|
||||
|
||||
- `bash ./scripts/bootstrap_mac.sh`
|
||||
- `bash ./scripts/project_status.sh`
|
||||
- `bash ./scripts/run_validation.sh --quick`
|
||||
- `./scripts/start_jupyter.sh --foreground`
|
||||
|
||||
Those are implementation details now. The normal consumer interface is `bash ./scripts/app.sh ...`.
|
||||
|
|
|
|||
51
README.md
51
README.md
|
|
@ -45,46 +45,67 @@ QuantumLearning/
|
|||
|
||||
## Quick Start
|
||||
|
||||
Use the repo-local bootstrap script on macOS:
|
||||
Use the consumer-facing control script on macOS:
|
||||
|
||||
```bash
|
||||
cd /Users/oho/GitClone/CodexProjects/QuantumLearning
|
||||
bash ./scripts/bootstrap_mac.sh
|
||||
bash ./scripts/app.sh bootstrap
|
||||
```
|
||||
|
||||
That script creates `.venv/`, installs the Python dependencies, and installs the project-local Playwright Chromium runtime into `.playwright-browsers/`.
|
||||
That creates `.venv/`, installs the Python dependencies, and installs the project-local Playwright Chromium runtime into `.playwright-browsers/`.
|
||||
|
||||
For normal consumer use, these are the only commands you should need:
|
||||
|
||||
```bash
|
||||
bash ./scripts/app.sh start --open
|
||||
bash ./scripts/app.sh status
|
||||
bash ./scripts/app.sh restart
|
||||
bash ./scripts/app.sh stop
|
||||
```
|
||||
|
||||
If you want a fast operational check immediately after bootstrap:
|
||||
|
||||
```bash
|
||||
bash ./scripts/project_status.sh
|
||||
bash ./scripts/run_validation.sh --quick
|
||||
bash ./scripts/app.sh status
|
||||
bash ./scripts/app.sh validate --quick
|
||||
```
|
||||
|
||||
Launch Jupyter from the project root with the repo-local wrapper so the notebooks, kernel, and local config stay aligned.
|
||||
The app command starts Jupyter as a detached background service, prints the exact URL, cleans up stale runtime files when needed, and writes logs to `.logs/jupyterlab.log`.
|
||||
|
||||
```bash
|
||||
./scripts/start_jupyter.sh
|
||||
bash ./scripts/app.sh start
|
||||
```
|
||||
|
||||
The launcher writes only inside the project directory, opens the repo-local `QuantumLearning (.venv)` kernel, and defaults to the onboarding notebook.
|
||||
If the server is already running, the same command returns the current URL instead of starting a duplicate server.
|
||||
|
||||
If you want a different local port:
|
||||
|
||||
```bash
|
||||
./scripts/start_jupyter.sh 8890
|
||||
bash ./scripts/app.sh start --port 8890
|
||||
```
|
||||
|
||||
If you want to restart the whole Jupyter layer cleanly:
|
||||
|
||||
```bash
|
||||
bash ./scripts/app.sh restart
|
||||
```
|
||||
|
||||
If you want to inspect the live log:
|
||||
|
||||
```bash
|
||||
bash ./scripts/app.sh logs -f
|
||||
```
|
||||
|
||||
Run the tests:
|
||||
|
||||
```bash
|
||||
bash ./scripts/run_validation.sh --standard
|
||||
bash ./scripts/app.sh validate --standard
|
||||
```
|
||||
|
||||
Run the measured local coverage report:
|
||||
|
||||
```bash
|
||||
bash ./scripts/run_validation.sh --full
|
||||
bash ./scripts/app.sh validate --full
|
||||
```
|
||||
|
||||
On a normal Mac terminal, the full validation path runs the browser and notebook execution suites as well. In restricted automation sandboxes, those suites may skip because localhost kernel or browser ports are blocked.
|
||||
|
|
@ -142,6 +163,14 @@ The older single-notebook sequence is still present as legacy reference material
|
|||
See [FIRST_RUN.md](/Users/oho/GitClone/CodexProjects/QuantumLearning/FIRST_RUN.md) for the short startup checklist.
|
||||
See [OPERATIONS.md](/Users/oho/GitClone/CodexProjects/QuantumLearning/OPERATIONS.md) for setup on another Mac, monitoring, validation, and recovery.
|
||||
|
||||
The lower-level scripts still exist:
|
||||
- `scripts/bootstrap_mac.sh`
|
||||
- `scripts/start_jupyter.sh`
|
||||
- `scripts/project_status.sh`
|
||||
- `scripts/run_validation.sh`
|
||||
|
||||
Those are now plumbing. `scripts/app.sh` is the consumer entrypoint.
|
||||
|
||||
## Local-First Design Rules
|
||||
|
||||
- Main workflow uses local simulators only.
|
||||
|
|
|
|||
479
scripts/app.sh
Executable file
479
scripts/app.sh
Executable file
|
|
@ -0,0 +1,479 @@
|
|||
#!/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.
|
||||
|
||||
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 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/COURSE_BLUEPRINT.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
|
||||
}
|
||||
|
||||
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
|
||||
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."
|
||||
return 0
|
||||
fi
|
||||
|
||||
pid="$(server_field "$json_path" "pid")"
|
||||
if ! kill -0 "$pid" >/dev/null 2>&1; then
|
||||
local removed_paths
|
||||
removed_paths="$(cleanup_stale_runtime_files 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
|
||||
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
|
||||
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
|
||||
|
||||
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" "$@"
|
||||
}
|
||||
|
||||
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
|
||||
;;
|
||||
restart)
|
||||
restart_command "$@"
|
||||
;;
|
||||
status)
|
||||
exec bash "$ROOT_DIR/scripts/project_status.sh"
|
||||
;;
|
||||
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
|
||||
|
|
@ -90,7 +90,7 @@ cat <<EOF
|
|||
Bootstrap complete.
|
||||
|
||||
Next steps:
|
||||
bash ./scripts/project_status.sh
|
||||
bash ./scripts/run_validation.sh --quick
|
||||
./scripts/start_jupyter.sh
|
||||
bash ./scripts/app.sh status
|
||||
bash ./scripts/app.sh validate --quick
|
||||
bash ./scripts/app.sh start --open
|
||||
EOF
|
||||
|
|
|
|||
|
|
@ -44,6 +44,12 @@ else
|
|||
echo "Browser runtime: missing"
|
||||
fi
|
||||
|
||||
if [[ -f "$ROOT_DIR/.logs/jupyterlab.log" ]]; then
|
||||
echo "Jupyter log: $ROOT_DIR/.logs/jupyterlab.log"
|
||||
else
|
||||
echo "Jupyter log: none yet"
|
||||
fi
|
||||
|
||||
if compgen -G "$ROOT_DIR/.jupyter_runtime/jpserver-*.json" >/dev/null; then
|
||||
LATEST_SERVER_JSON="$(ls -t "$ROOT_DIR"/.jupyter_runtime/jpserver-*.json | head -n 1)"
|
||||
"$ROOT_DIR/.venv/bin/python" - <<'PY' "$LATEST_SERVER_JSON"
|
||||
|
|
@ -76,6 +82,11 @@ else
|
|||
echo "Jupyter: no repo-local runtime file found"
|
||||
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 " Logs: bash ./scripts/app.sh logs -f"
|
||||
echo "Validation:"
|
||||
echo " Quick: bash ./scripts/run_validation.sh --quick"
|
||||
echo " Full: bash ./scripts/run_validation.sh --full"
|
||||
echo " Quick: bash ./scripts/app.sh validate --quick"
|
||||
echo " Full: bash ./scripts/app.sh validate --full"
|
||||
|
|
|
|||
|
|
@ -2,55 +2,9 @@
|
|||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
PORT="${1:-8888}"
|
||||
|
||||
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
|
||||
cat <<'EOF'
|
||||
Usage: ./scripts/start_jupyter.sh [port]
|
||||
|
||||
Starts repo-local JupyterLab from .venv with project-local config, runtime,
|
||||
kernel registration, and notebook defaults.
|
||||
EOF
|
||||
exit 0
|
||||
if [[ $# -gt 0 && "$1" =~ ^[0-9]+$ ]]; then
|
||||
exec bash "$ROOT_DIR/scripts/app.sh" start --port "$1"
|
||||
fi
|
||||
|
||||
if ! [[ "$PORT" =~ ^[0-9]+$ ]]; then
|
||||
echo "Port must be an integer, got: $PORT" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -x "$ROOT_DIR/.venv/bin/python" ]]; then
|
||||
echo "Missing repo-local .venv at $ROOT_DIR/.venv" >&2
|
||||
echo "Create it first with: python3.12 -m venv .venv" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p \
|
||||
"$ROOT_DIR/.cache/matplotlib" \
|
||||
"$ROOT_DIR/.ipython" \
|
||||
"$ROOT_DIR/.jupyter_config" \
|
||||
"$ROOT_DIR/.jupyter_data" \
|
||||
"$ROOT_DIR/.jupyter_runtime"
|
||||
|
||||
# Keep JupyterLab away from the broken Homebrew node installation on this machine.
|
||||
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"
|
||||
|
||||
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
|
||||
|
||||
exec "$ROOT_DIR/.venv/bin/jupyter" lab \
|
||||
--no-browser \
|
||||
--ip=127.0.0.1 \
|
||||
--port="$PORT"
|
||||
exec bash "$ROOT_DIR/scripts/app.sh" start "$@"
|
||||
|
|
|
|||
|
|
@ -18,6 +18,19 @@ def test_bootstrap_script_help_works():
|
|||
assert "Usage:" in result.stdout
|
||||
|
||||
|
||||
def test_app_script_help_works():
|
||||
result = subprocess.run(
|
||||
["bash", "scripts/app.sh", "--help"],
|
||||
cwd=project_root(),
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
|
||||
assert result.returncode == 0
|
||||
assert "Consumer-facing control surface" in result.stdout
|
||||
|
||||
|
||||
def test_validation_script_help_works():
|
||||
result = subprocess.run(
|
||||
["bash", "scripts/run_validation.sh", "--help"],
|
||||
|
|
@ -45,12 +58,32 @@ def test_project_status_script_runs():
|
|||
assert "Validation:" in result.stdout
|
||||
|
||||
|
||||
def test_app_status_runs():
|
||||
result = subprocess.run(
|
||||
["bash", "scripts/app.sh", "status"],
|
||||
cwd=project_root(),
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
|
||||
assert result.returncode == 0
|
||||
assert "Consumer:" in result.stdout
|
||||
|
||||
|
||||
def test_app_script_uses_detached_launch_and_stale_runtime_cleanup():
|
||||
text = (project_root() / "scripts" / "app.sh").read_text()
|
||||
|
||||
assert "cleanup_stale_runtime_files" in text
|
||||
assert "nohup " in text
|
||||
|
||||
|
||||
def test_operations_docs_use_tested_command_forms():
|
||||
root = project_root()
|
||||
expected_commands = [
|
||||
"bash ./scripts/bootstrap_mac.sh",
|
||||
"bash ./scripts/project_status.sh",
|
||||
"bash ./scripts/run_validation.sh --quick",
|
||||
"bash ./scripts/app.sh bootstrap",
|
||||
"bash ./scripts/app.sh status",
|
||||
"bash ./scripts/app.sh validate --quick",
|
||||
]
|
||||
|
||||
for relative_path in ("README.md", "FIRST_RUN.md", "OPERATIONS.md"):
|
||||
|
|
|
|||
|
|
@ -83,6 +83,10 @@ def test_operations_guide_exists():
|
|||
assert (project_root() / "OPERATIONS.md").exists()
|
||||
|
||||
|
||||
def test_consumer_app_script_exists():
|
||||
assert (project_root() / "scripts" / "app.sh").exists()
|
||||
|
||||
|
||||
def test_repo_local_jupyter_config_exists():
|
||||
assert (project_root() / ".jupyter_config" / "jupyter_lab_config.py").exists()
|
||||
assert (project_root() / ".jupyter_config" / "jupyter_server_config.py").exists()
|
||||
|
|
|
|||
Loading…
Reference in a new issue