Files
CASAN/packages/casan-control-panel/scripts/control-plane-local.sh
T

180 lines
5.3 KiB
Bash
Executable File

#!/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 <start|status|stop|open> [--root path] [--no-open]" >&2
exit 64
;;
esac