#!/usr/bin/env pwsh # crucible - bootstrap wrapper (PowerShell). Prefers a crucible already on PATH; # otherwise downloads the latest released binary from Gitea into a per-version # cache and execs it. # # CANONICAL SOURCE: sow-tools/wrappers/crucible.ps1. Do not hand-edit copies in # consumer repos - kept in sync by sow-tools CI (see drift-check). # # Env: CRUCIBLE_HOME, CRUCIBLE_TOKEN, CRUCIBLE_GITEA, CRUCIBLE_REPO. $ErrorActionPreference = 'Stop' # 0. Prefer an existing crucible. $existing = Get-Command crucible -ErrorAction SilentlyContinue if ($existing) { & $existing.Source @args; exit $LASTEXITCODE } $gitea = if ($env:CRUCIBLE_GITEA) { $env:CRUCIBLE_GITEA } else { 'https://git.westgate.pw' } $repo = if ($env:CRUCIBLE_REPO) { $env:CRUCIBLE_REPO } else { 'ShadowsOverWestgate/sow-tools' } # 1. OS/arch -> asset name (.exe on Windows). $os = if ($IsWindows) { 'windows' } elseif ($IsMacOS) { 'darwin' } else { 'linux' } $arch = switch ($env:PROCESSOR_ARCHITECTURE) { 'AMD64' { 'amd64' } 'ARM64' { 'arm64' } default { 'amd64' } } $ext = if ($os -eq 'windows') { '.exe' } else { '' } $asset = "crucible-$os-$arch$ext" # 2. Auth header (anon first, token fallback). $token = $env:CRUCIBLE_TOKEN $tokenFile = Join-Path $HOME '.config/crucible/token' if (-not $token -and (Test-Path $tokenFile)) { $token = (Get-Content $tokenFile -Raw).Trim() } function Invoke-Fetch($url, $outFile) { try { Invoke-WebRequest -Uri $url -OutFile $outFile -UseBasicParsing; return $true } catch {} if ($token) { Invoke-WebRequest -Uri $url -OutFile $outFile -UseBasicParsing -Headers @{ Authorization = "token $token" } return $true } return $false } # 3. Resolve latest tag. $tag = $null try { $hdr = if ($token) { @{ Authorization = "token $token" } } else { @{} } $rel = Invoke-RestMethod -Uri "$gitea/api/v1/repos/$repo/releases/latest" -Headers $hdr $tag = $rel.tag_name } catch {} # 4. Cache dir. $cacheRoot = if ($env:CRUCIBLE_HOME) { $env:CRUCIBLE_HOME } elseif ($env:LOCALAPPDATA) { Join-Path $env:LOCALAPPDATA 'crucible' } else { Join-Path $HOME '.cache/crucible' } # 5. Offline fallback. if (-not $tag) { if (Test-Path $cacheRoot) { $cached = Get-ChildItem $cacheRoot -Directory | Sort-Object Name | Select-Object -Last 1 $cachedBin = if ($cached) { Join-Path $cached.FullName "crucible$ext" } else { $null } if ($cachedBin -and (Test-Path $cachedBin)) { Write-Error "crucible: offline - using cached $($cached.Name)" -ErrorAction Continue & $cachedBin @args; exit $LASTEXITCODE } } Write-Error "crucible: cannot reach $gitea and no cached binary found (set CRUCIBLE_TOKEN if private)." exit 1 } $destDir = Join-Path $cacheRoot $tag $bin = Join-Path $destDir "crucible$ext" # 6. Download + verify on cache miss. if (-not (Test-Path $bin)) { New-Item -ItemType Directory -Force -Path $destDir | Out-Null $base = "$gitea/$repo/releases/download/$tag" $tmp = New-Item -ItemType Directory -Path (Join-Path ([System.IO.Path]::GetTempPath()) ([guid]::NewGuid())) $tmpBin = Join-Path $tmp "crucible$ext" Write-Host "crucible: fetching $asset $tag..." if (-not (Invoke-Fetch "$base/$asset" $tmpBin)) { Write-Error "crucible: download failed for $asset"; exit 1 } $sums = Join-Path $tmp 'SHA256SUMS' if (Invoke-Fetch "$base/SHA256SUMS" $sums) { $want = (Select-String -Path $sums -Pattern ([regex]::Escape($asset) + '$') | Select-Object -First 1).Line -split '\s+' | Select-Object -First 1 $got = (Get-FileHash $tmpBin -Algorithm SHA256).Hash.ToLower() if ($want -and ($want.ToLower() -ne $got)) { Write-Error "crucible: checksum mismatch for $asset"; exit 1 } } Move-Item -Force $tmpBin $bin Remove-Item -Recurse -Force $tmp } & $bin @args exit $LASTEXITCODE