Adopt CASAN like a normal tool (codegraph-style): install the harness ONCE
per machine, then `casan init` per project writes CONFIG ONLY — the harness
is no longer copied into every repo.
- install.sh / install.ps1: global bootstrap (curl|sh / irm|iex or local
source). Installs harness to $CASAN_HOME/versions/<ver>, writes a `casan`
launcher that resolves the shared harness + the current project's .specify,
and records a gate-code integrity hash. CASAN_NO_PATH_LINK for tests.
- harness_hash.py: deterministic content hash over gate code (scripts/bash,
scripts/python, security, level5) — the pin+verify anchor.
- casan-init.py: `casan init` writes .casan/{config,version.lock,agentic.env},
.specify/ marker, and the Plan-20 client hooks — no harness copy. `verify`
recomputes the harness hash LIVE and compares to the project pin (drift/
tamper -> rc 3), preserving the Plan-16 trusted-gates guarantee off-repo.
- bin/casan: new `init` and `verify-harness` commands.
- hybrid-install-tests.sh: 21/21 (install, config-only init, no-copy, pin,
verify ok, tamper drift, bridge runs against project state via global harness).
- docs: CASAN_INSTALL_HYBRID.md + Plan-21.
The path model (casan-paths.sh) already separated harness/state/domain roots,
so this is installer + init, not a core rewrite. Remote dist tarball, real
Windows run, and signed .harness-hash are the documented next steps.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
129 lines
6.3 KiB
PowerShell
129 lines
6.3 KiB
PowerShell
#requires -Version 5.1
|
|
<#
|
|
CASAN global installer (Plan-21 hybrid model) — Windows PowerShell.
|
|
|
|
irm https://<your-gitea>/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 <dir>, 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 harness + devkit + CLI 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
|
|
|
|
function Copy-Tree($rel) {
|
|
$src = Join-Path $Source $rel
|
|
if (-not (Test-Path $src)) { return }
|
|
$dst = Join-Path $dest $rel
|
|
New-Item -ItemType Directory -Force -Path $dst | Out-Null
|
|
Copy-Item -Recurse -Force (Join-Path $src '*') $dst
|
|
}
|
|
Copy-Tree 'packages\casan-harness'
|
|
Copy-Tree 'packages\casan-devkit'
|
|
Copy-Item -Force (Join-Path $Source 'bin\casan') (Join-Path $dest 'bin\casan')
|
|
Set-Content -Path (Join-Path $dest 'VERSION') -Value $version -Encoding ASCII
|
|
|
|
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"
|
|
$CasanHome = if ($env:CASAN_HOME) { $env:CASAN_HOME } else { 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 <your-project>; casan init"
|
|
if ($cleanup) { Remove-Item -Recurse -Force $cleanup -ErrorAction SilentlyContinue }
|