From a39c580b3c6b144493686d80236285f9345ce54f Mon Sep 17 00:00:00 2001 From: vickydotbat Date: Tue, 16 Jun 2026 08:46:23 +0200 Subject: [PATCH] feat(wrappers): canonical PowerShell bootstrap wrapper --- wrappers/crucible.ps1 | 89 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 wrappers/crucible.ps1 diff --git a/wrappers/crucible.ps1 b/wrappers/crucible.ps1 new file mode 100644 index 0000000..0762977 --- /dev/null +++ b/wrappers/crucible.ps1 @@ -0,0 +1,89 @@ +#!/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