#!/usr/bin/env bash # One-command local CASAN Control Plane for demos and offline evaluation. # Platform-only: Core stays a non-blocking sensor/enforcer and local spool. set -uo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SOURCE_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" COMMAND="${1:-start}" [[ $# -gt 0 ]] && shift || true TARGET="${CASAN_APP_ROOT:-$PWD}" OPEN_BROWSER=1 while [[ $# -gt 0 ]]; do case "$1" in --root) TARGET="${2:-}"; shift 2 ;; --no-open) OPEN_BROWSER=0; shift ;; *) echo "casan dashboard: unknown argument: $1" >&2; exit 64 ;; esac done TARGET="$(cd "$TARGET" 2>/dev/null && pwd)" || { echo "casan dashboard: project root does not exist: $TARGET" >&2 exit 66 } [[ -f "$TARGET/.casan/config.json" ]] || { echo "casan dashboard: $TARGET is not an adopted CASAN project" >&2 exit 65 } [[ -f "$SOURCE_ROOT/package.json" ]] || { echo "casan dashboard: Platform source bundle is incomplete" >&2 exit 1 } STATE_DIR="${CASAN_CONTROL_PLANE_STATE_DIR:-$TARGET/.specify/state/control-plane}" mkdir -p "$STATE_DIR" API_PID_FILE="$STATE_DIR/api.pid" UI_PID_FILE="$STATE_DIR/ui.pid" API_LOG="$STATE_DIR/api.log" UI_LOG="$STATE_DIR/ui.log" DASHBOARD_URL="${CASAN_DASHBOARD_URL:-http://127.0.0.1:5174}" alive() { local file="$1" pid [[ -f "$file" ]] || return 1 pid="$(sed -n '1p' "$file" 2>/dev/null || true)" [[ "$pid" =~ ^[0-9]+$ ]] && kill -0 "$pid" 2>/dev/null } enroll() { python3 - "$TARGET/.casan/config.json" "$DASHBOARD_URL" <<'PY' import json import os import sys import tempfile path, url = sys.argv[1:3] with open(path, "r", encoding="utf-8") as handle: config = json.load(handle) control_plane = config.get("control_plane") if not isinstance(control_plane, dict): control_plane = {} control_plane.update({ "dashboard_url": url.rstrip("/"), "delivery": "local_spool", "receipt_links_enabled": True, }) config["control_plane"] = control_plane directory = os.path.dirname(path) fd, temporary = tempfile.mkstemp(prefix=".config.", suffix=".tmp", dir=directory) try: with os.fdopen(fd, "w", encoding="utf-8") as handle: json.dump(config, handle, ensure_ascii=False, indent=2) handle.write("\n") handle.flush() os.fsync(handle.fileno()) os.replace(temporary, path) finally: if os.path.exists(temporary): os.unlink(temporary) PY } open_dashboard() { python3 - "$DASHBOARD_URL" <<'PY' import sys import webbrowser raise SystemExit(0 if webbrowser.open(sys.argv[1], new=2) else 1) PY } case "$COMMAND" in start) if alive "$API_PID_FILE" && alive "$UI_PID_FILE"; then enroll echo "CASAN_CONTROL_PLANE_READY url=$DASHBOARD_URL project=$TARGET" [[ "$OPEN_BROWSER" == "1" ]] && open_dashboard || true exit 0 fi python3 - "$SOURCE_ROOT" "$TARGET" "$API_LOG" "$UI_LOG" "$API_PID_FILE" "$UI_PID_FILE" <<'PY' import os import subprocess import sys source, target, api_log, ui_log, api_pid, ui_pid = sys.argv[1:7] environment = os.environ.copy() environment["CASAN_APP_ROOT"] = target with open(api_log, "ab", buffering=0) as api_output: api = subprocess.Popen( [os.path.join(source, "node_modules", ".bin", "tsx"), "src/main.ts"], cwd=os.path.join(source, "packages", "casan-control-panel", "backend"), env=environment, stdin=subprocess.DEVNULL, stdout=api_output, stderr=subprocess.STDOUT, start_new_session=True, ) with open(ui_log, "ab", buffering=0) as ui_output: ui = subprocess.Popen( [ os.path.join(source, "node_modules", ".bin", "vite"), "--host", "127.0.0.1", "--port", "5174", ], cwd=os.path.join(source, "packages", "casan-control-panel", "frontend"), stdin=subprocess.DEVNULL, stdout=ui_output, stderr=subprocess.STDOUT, start_new_session=True, ) for path, pid in ((api_pid, api.pid), (ui_pid, ui.pid)): with open(path, "w", encoding="utf-8") as handle: handle.write("%s\n" % pid) PY ready=0 for _attempt in $(seq 1 60); do if curl -fsS "http://127.0.0.1:3010/api/v1/project" >/dev/null 2>&1 \ && curl -fsS "$DASHBOARD_URL" >/dev/null 2>&1; then ready=1 break fi sleep 0.25 done if [[ "$ready" != "1" ]]; then echo "casan dashboard: Control Plane did not become ready" >&2 echo "API log: $API_LOG" >&2 echo "UI log: $UI_LOG" >&2 exit 1 fi enroll echo "CASAN_CONTROL_PLANE_READY url=$DASHBOARD_URL project=$TARGET" echo "logs=$STATE_DIR" [[ "$OPEN_BROWSER" == "1" ]] && open_dashboard || true ;; status) api="stopped"; ui="stopped" alive "$API_PID_FILE" && api="running" alive "$UI_PID_FILE" && ui="running" echo "CASAN_CONTROL_PLANE_STATUS api=$api ui=$ui url=$DASHBOARD_URL project=$TARGET" [[ "$api" == "running" && "$ui" == "running" ]] ;; stop) for file in "$API_PID_FILE" "$UI_PID_FILE"; do if alive "$file"; then pid="$(sed -n '1p' "$file")" kill "$pid" fi rm -f "$file" done echo "CASAN_CONTROL_PLANE_STOPPED project=$TARGET" ;; open) enroll echo "$DASHBOARD_URL" open_dashboard ;; *) echo "casan: usage: casan dashboard [--root path] [--no-open]" >&2 exit 64 ;; esac