#requires -Version 5.1 <# CASAN global installer (Plan-21 hybrid model) — Windows PowerShell. irm https:///admin/casan5/raw/branch/main/install.ps1 | iex # or, from a local CASAN source checkout: pwsh .\install.ps1 Installs the harness ONCE under $env:CASAN_HOME (default %LOCALAPPDATA%\casan), writes a `casan` launcher, and records the gate-code integrity hash. Projects then run `casan init` to adopt CASAN with only per-project config. Requires: python3 on PATH, and bash (Git for Windows / Git Bash) to RUN the harness — see docs/casan/CASAN_AGENTIC_CLIENTS_WINDOWS.md. #> [CmdletBinding()] param( [string]$Source, [switch]$NoPathLink ) $ErrorActionPreference = 'Stop' function Log($m) { Write-Host "[casan-install] $m" } function Die($m) { Write-Error "[casan-install] ERROR: $m"; exit 1 } $py = (Get-Command python3 -ErrorAction SilentlyContinue) ?? (Get-Command python -ErrorAction SilentlyContinue) if (-not $py) { Die 'python3 is required on PATH.' } $CasanHome = if ($env:CASAN_HOME) { $env:CASAN_HOME } else { Join-Path $env:LOCALAPPDATA 'casan' } # ── 1) Locate source (param, CASAN_SRC, local checkout, or CASAN_DIST_URL) ──── $cleanup = $null if (-not $Source) { $Source = $env:CASAN_SRC } if ((-not $Source) -and (Test-Path './packages/casan-harness')) { $Source = (Get-Location).Path } if ((-not $Source) -and $env:CASAN_DIST_URL) { $tmp = Join-Path ([System.IO.Path]::GetTempPath()) ("casan-" + [guid]::NewGuid().ToString('N')) New-Item -ItemType Directory -Force -Path $tmp | Out-Null $cleanup = $tmp Log "downloading CASAN from $($env:CASAN_DIST_URL)" $tgz = Join-Path $tmp 'casan.tar.gz' Invoke-WebRequest -UseBasicParsing -Uri $env:CASAN_DIST_URL -OutFile $tgz tar -xzf $tgz -C $tmp if (Test-Path (Join-Path $tmp 'packages/casan-harness')) { $Source = $tmp } else { $found = Get-ChildItem -Path $tmp -Recurse -Directory -Filter 'casan-harness' -Depth 2 | Select-Object -First 1 if ($found) { $Source = (Split-Path -Parent (Split-Path -Parent $found.FullName)) } } } if ((-not $Source) -or (-not (Test-Path (Join-Path $Source 'packages/casan-harness')))) { Die 'no CASAN source found. Run from a checkout, pass -Source , or set CASAN_DIST_URL.' } $version = if (Test-Path (Join-Path $Source 'VERSION')) { (Get-Content (Join-Path $Source 'VERSION') -Raw).Trim() } else { '0.0.0' } Log "source : $Source" Log "version : $version" Log "install : $CasanHome" # ── 2) Copy the production runtime allowlist into a versioned dir ───────────── $dest = Join-Path $CasanHome "versions\$version" if (Test-Path $dest) { Remove-Item -Recurse -Force $dest } New-Item -ItemType Directory -Force -Path (Join-Path $dest 'packages'), (Join-Path $dest 'bin') | Out-Null $runtimeCopier = Join-Path $Source 'scripts\copy-runtime.py' if (-not (Test-Path $runtimeCopier)) { Die 'missing production runtime copier: scripts/copy-runtime.py' } & $py.Source $runtimeCopier --source-root $Source --destination-root $dest --component harness --component devkit --clean if ($LASTEXITCODE -ne 0) { Die 'failed to assemble the production runtime.' } Copy-Item -Force (Join-Path $Source 'bin\casan') (Join-Path $dest 'bin\casan') Set-Content -Path (Join-Path $dest 'VERSION') -Value $version -Encoding ASCII Set-Content -Path (Join-Path $dest '.casan-level') -Value 'devkit' -Encoding ASCII New-Item -ItemType Directory -Force -Path (Join-Path $dest 'packaging') | Out-Null Copy-Item -Force (Join-Path $Source 'packaging\runtime-layout.json') (Join-Path $dest 'packaging\runtime-layout.json') Get-ChildItem -Path $dest -Recurse -Directory -Filter '__pycache__' -ErrorAction SilentlyContinue | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue Get-ChildItem -Path $dest -Recurse -File -Filter '*.pyc' -ErrorAction SilentlyContinue | Remove-Item -Force -ErrorAction SilentlyContinue # ── 3) Record gate-code integrity hash ─────────────────────────────────────── $hasher = Join-Path $dest 'packages\casan-harness\scripts\python\harness_hash.py' $hhash = (& $py.Source $hasher compute (Join-Path $dest 'packages\casan-harness')).Trim() if (-not $hhash) { Die 'failed to compute harness hash.' } Set-Content -Path (Join-Path $dest '.harness-hash') -Value $hhash -Encoding ASCII Log "integrity: $hhash" # ── 4) Point current -> this version ───────────────────────────────────────── $cur = Join-Path $CasanHome 'current' if (Test-Path $cur) { (Get-Item $cur).Delete() } New-Item -ItemType Junction -Path $cur -Target $dest | Out-Null # ── 5) Launcher: casan.cmd -> casan.ps1 (sets env, invokes the bash CLI) ───── $binDir = Join-Path $CasanHome 'bin' New-Item -ItemType Directory -Force -Path $binDir | Out-Null $launcherPs1 = @' $ErrorActionPreference = "Stop" # Self-locate THIS install from the launcher's own path (\bin\casan.ps1) so a # specific launcher always uses ITS install regardless of any ambient CASAN_HOME. $selfHome = Split-Path -Parent (Split-Path -Parent $MyInvocation.MyCommand.Path) if ((Test-Path (Join-Path $selfHome "current")) -or (Test-Path (Join-Path $selfHome "versions"))) { $CasanHome = $selfHome } elseif ($env:CASAN_HOME) { $CasanHome = $env:CASAN_HOME } else { $CasanHome = Join-Path $env:LOCALAPPDATA "casan" } $cur = Join-Path $CasanHome "current" if (-not (Test-Path $cur)) { Write-Error "casan: no install at $cur (run install.ps1)"; exit 1 } $env:CASAN_HARNESS_ROOT = Join-Path $cur "packages\casan-harness" $env:CASAN_DEVKIT_ROOT = Join-Path $cur "packages\casan-devkit" $env:CASAN_INSTALL_ROOT = $cur if (-not $env:CASAN_APP_ROOT) { $d = (Get-Location).Path while ($d -and (Split-Path $d -Parent)) { if ((Test-Path (Join-Path $d ".casan")) -or (Test-Path (Join-Path $d ".specify"))) { $env:CASAN_APP_ROOT = $d; break } $d = Split-Path $d -Parent } } $bash = (Get-Command bash -ErrorAction SilentlyContinue) if (-not $bash) { Write-Error "casan: bash not found. Install Git for Windows (Git Bash)."; exit 1 } & $bash.Source (Join-Path $cur "bin/casan") @args exit $LASTEXITCODE '@ Set-Content -Path (Join-Path $binDir 'casan.ps1') -Value $launcherPs1 -Encoding UTF8 Set-Content -Path (Join-Path $binDir 'casan.cmd') -Value "@echo off`r`npowershell -NoProfile -ExecutionPolicy Bypass -File `"%~dp0casan.ps1`" %*" -Encoding ASCII # ── 6) Put launcher on PATH (user scope) ───────────────────────────────────── if (-not $NoPathLink -and $env:CASAN_NO_PATH_LINK -ne '1') { $userPath = [Environment]::GetEnvironmentVariable('Path', 'User') if ($userPath -notlike "*$binDir*") { [Environment]::SetEnvironmentVariable('Path', "$binDir;$userPath", 'User') Log "added $binDir to user PATH (restart shell to pick it up)" } } Log "installed CASAN $version" Log "launcher : $binDir\casan.cmd" Log "next: cd ; casan init" if ($cleanup) { Remove-Item -Recurse -Force $cleanup -ErrorAction SilentlyContinue }