Answers the 3 adoption questions (Plan-21 follow-up):
1) LEVEL SELECTION (4 packaging levels, packaging/levels.json):
- install.sh --level core|devkit; platform refused (preview service),
enterprise refused (future). Level recorded in .casan-level.
- casan init --level 1..4: L1=gate+Plan-20 hooks only; L2=+CI+domain-pack;
L3=L2 base+preview note; L4=refused. New `casan level show|set`.
- levels.json core now includes adapters/ + schemas/ + install scripts.
2) EXISTING SHELLS (agents/skills): init MERGES Plan-20 hooks into an existing
.claude/settings.json and .codex/{hooks.json,config.toml} idempotently
instead of clobbering — preserves the project's own hooks/agents/skills and
unrelated keys. Re-running never duplicates the CASAN hook.
3) NO RE-INDEX / NO SHELL REWRITE: init only adds config; it does not parse or
index code and does not rewrite the project shell.
Safety fixes after a test accidentally ran init in the real repo:
- launcher shim now SELF-LOCATES its install from its own path (no ambient
CASAN_HOME cross-talk).
- casan init REFUSES to adopt a CASAN source hub into itself (--force to
override), so the Plan-20 hooks can't block the developing agent.
- test always runs init inside throwaway dirs; +source-hub guard test.
hybrid-install-tests.sh: 41/41 PASS.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
134 lines
6.7 KiB
PowerShell
134 lines
6.7 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"
|
|
# Self-locate THIS install from the launcher's own path (<home>\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 <your-project>; casan init"
|
|
if ($cleanup) { Remove-Item -Recurse -Force $cleanup -ErrorAction SilentlyContinue }
|