180 lines
5.3 KiB
Bash
Executable File
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
|