#!/bin/sh # CASAN global installer (Plan-21 hybrid model) — macOS / Linux. # # curl -fsSL https:///admin/casan5/raw/branch/main/install.sh | sh # # or, from a local CASAN source checkout: # sh install.sh # # Installs the CASAN harness ONCE under $CASAN_HOME (default ~/.casan), puts a # `casan` launcher on PATH, and records a content-integrity hash of the gate code. # Projects then run `casan init` to adopt CASAN with only per-project config — # the harness itself is NOT copied into every repo (that is the hybrid model). # # POSIX sh (runs under `curl | sh`). The harness itself needs bash + python3 to # run; this bootstrap only needs sh, cp and python3. set -eu CASAN_HOME="${CASAN_HOME:-$HOME/.casan}" LEVEL="${CASAN_LEVEL:-devkit}" # core | devkit (platform=preview service, enterprise=future) # Parse flags (also accept `--level X`, e.g. `... | sh -s -- --level core`). while [ "$#" -gt 0 ]; do case "$1" in --level) LEVEL="$2"; shift 2 ;; --level=*) LEVEL="${1#*=}"; shift ;; -h|--help) sed -n '2,12p' "$0" 2>/dev/null; exit 0 ;; *) shift ;; esac done log() { printf '[casan-install] %s\n' "$1"; } die() { printf '[casan-install] ERROR: %s\n' "$1" >&2; exit 1; } command -v python3 >/dev/null 2>&1 || die "python3 is required on PATH." # Levels are cumulative (packaging/levels.json is the source of truth). The tool # installer handles the two ADOPT-INTO-PROJECT levels; L3/L4 are separate. case "$LEVEL" in core|devkit) ;; platform) die "Level 3 (platform) is a PREVIEW service, not a per-project tool install. Install the CLI base with '--level devkit', then deploy the platform separately (scripts/package-release.sh platform, docs/packaging)." ;; enterprise) printf '[casan-install] Level 4 (enterprise) is FUTURE / not shipped — refusing (see packaging/levels.json).\n' >&2; exit 3 ;; *) die "unknown --level '$LEVEL' (valid: core | devkit)." ;; esac # ── 1) Locate the source (local checkout, explicit CASAN_SRC, or a dist tarball) SRC="" CLEANUP="" if [ -n "${CASAN_SRC:-}" ] && [ -d "$CASAN_SRC/packages/casan-harness" ]; then SRC="$CASAN_SRC" elif [ -d "./packages/casan-harness" ]; then SRC="$(pwd)" elif [ -n "${CASAN_DIST_URL:-}" ]; then command -v curl >/dev/null 2>&1 || die "curl is required to fetch CASAN_DIST_URL." TMP="$(mktemp -d)" CLEANUP="$TMP" log "downloading CASAN from $CASAN_DIST_URL" curl -fsSL "$CASAN_DIST_URL" -o "$TMP/casan.tar.gz" || die "download failed: $CASAN_DIST_URL" tar -xzf "$TMP/casan.tar.gz" -C "$TMP" || die "extract failed." # Accept either a flat tarball or a single top-level dir. if [ -d "$TMP/packages/casan-harness" ]; then SRC="$TMP" else SRC="$(find "$TMP" -maxdepth 2 -type d -name casan-harness -exec dirname {} \; -quit)" SRC="$(dirname "$SRC" 2>/dev/null || true)" [ -n "$SRC" ] && [ -d "$SRC/packages/casan-harness" ] || die "tarball has no packages/casan-harness." fi else die "no CASAN source found. Run from a CASAN checkout, set CASAN_SRC=, or set CASAN_DIST_URL=." fi VERSION="$(cat "$SRC/VERSION" 2>/dev/null || echo "0.0.0")" log "source : $SRC" log "version : $VERSION" log "level : $LEVEL (core=harness+CLI, devkit=+adoption tooling)" log "install : $CASAN_HOME" # ── 2) Copy harness + devkit + CLI into a versioned dir ────────────────────── DEST="$CASAN_HOME/versions/$VERSION" rm -rf "$DEST" mkdir -p "$DEST/packages" "$DEST/bin" copy_tree() { # [ -e "$SRC/$1" ] || return 0 if command -v rsync >/dev/null 2>&1; then rsync -a --exclude='__pycache__' --exclude='*.pyc' --exclude='.DS_Store' \ --exclude='*.log' "$SRC/$1/" "$DEST/$1/" else mkdir -p "$DEST/$1" cp -R "$SRC/$1/." "$DEST/$1/" fi } # L1 core: harness + CLI + installer + level manifest. copy_tree "packages/casan-harness" cp "$SRC/bin/casan" "$DEST/bin/casan" chmod +x "$DEST/bin/casan" cp "$SRC/VERSION" "$DEST/VERSION" 2>/dev/null || printf '%s\n' "$VERSION" > "$DEST/VERSION" for f in install.sh install.ps1; do [ -f "$SRC/$f" ] && cp "$SRC/$f" "$DEST/$f"; done mkdir -p "$DEST/packaging"; [ -f "$SRC/packaging/levels.json" ] && cp "$SRC/packaging/levels.json" "$DEST/packaging/levels.json" # L2 devkit: adoption tooling (templates, domain-pack, casan init, package-release). if [ "$LEVEL" = "devkit" ]; then copy_tree "packages/casan-devkit" mkdir -p "$DEST/scripts"; [ -f "$SRC/scripts/package-release.sh" ] && cp "$SRC/scripts/package-release.sh" "$DEST/scripts/package-release.sh" [ -d "$SRC/docs/packaging" ] && { mkdir -p "$DEST/docs"; cp -R "$SRC/docs/packaging" "$DEST/docs/packaging"; } fi printf '%s\n' "$LEVEL" > "$DEST/.casan-level" # Prune stray caches the copy may have carried. find "$DEST" -name '__pycache__' -type d -prune -exec rm -rf {} + 2>/dev/null || true find "$DEST" -name '*.pyc' -delete 2>/dev/null || true # ── 3) Record the gate-code integrity hash (pin+verify anchor) ─────────────── HASHER="$DEST/packages/casan-harness/scripts/python/harness_hash.py" HHASH="$(python3 "$HASHER" compute "$DEST/packages/casan-harness")" || die "failed to compute harness hash." printf '%s\n' "$HHASH" > "$DEST/.harness-hash" log "integrity: $HHASH" # ── 4) Point `current` at this version ─────────────────────────────────────── ln -sfn "$DEST" "$CASAN_HOME/current" # ── 5) Write the global launcher (shim) ────────────────────────────────────── mkdir -p "$CASAN_HOME/bin" cat > "$CASAN_HOME/bin/casan" <<'LAUNCH' #!/usr/bin/env bash # CASAN global launcher — resolves the shared harness and the CURRENT project. set -uo pipefail # Self-locate THIS install from the launcher's own path (…//bin/casan) so # invoking a specific launcher always uses ITS install, regardless of any ambient # CASAN_HOME env. Fall back to CASAN_HOME/$HOME only if self-resolution fails. _self="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" if [[ -d "$_self/current" || -d "$_self/versions" ]]; then CASAN_HOME="$_self" else CASAN_HOME="${CASAN_HOME:-$HOME/.casan}" fi CUR="$CASAN_HOME/current" [[ -d "$CUR" ]] || { echo "casan: no install at $CUR (run install.sh)" >&2; exit 1; } export CASAN_HARNESS_ROOT="$CUR/packages/casan-harness" export CASAN_DEVKIT_ROOT="$CUR/packages/casan-devkit" export CASAN_INSTALL_ROOT="$CUR" # Project (app) root = nearest ancestor of CWD carrying a .casan/.specify marker. if [[ -z "${CASAN_APP_ROOT:-}" ]]; then _d="$PWD" while [[ "$_d" != "/" && -n "$_d" ]]; do if [[ -d "$_d/.casan" || -d "$_d/.specify" ]]; then export CASAN_APP_ROOT="$_d"; break; fi _d="$(dirname "$_d")" done fi exec bash "$CUR/bin/casan" "$@" LAUNCH chmod +x "$CASAN_HOME/bin/casan" # ── 6) Put the launcher on PATH (skip with CASAN_NO_PATH_LINK=1, e.g. in tests) LINKED="" if [ "${CASAN_NO_PATH_LINK:-0}" != "1" ]; then for d in "$HOME/.local/bin" "/usr/local/bin"; do if [ -d "$d" ] && printf '%s' ":$PATH:" | grep -q ":$d:"; then if ln -sf "$CASAN_HOME/bin/casan" "$d/casan" 2>/dev/null; then LINKED="$d/casan"; break; fi fi done fi log "installed CASAN $VERSION" if [ -n "$LINKED" ]; then log "launcher : $LINKED (on PATH)" else log "launcher : $CASAN_HOME/bin/casan" log "add to PATH: export PATH=\"$CASAN_HOME/bin:\$PATH\" (add to your shell rc)" fi if [ "$LEVEL" = "devkit" ]; then log "next: cd && casan init (adopt CASAN, config only)" else log "level core installed: 'casan run/gate/verify' available. Re-run with --level devkit to get 'casan init'." fi [ -n "$CLEANUP" ] && rm -rf "$CLEANUP" || true