<# Build the distributable claude-mode package. Produces, under dist// : claude-mode-.zip the payload (script, key helper, presets, installer) manifest.json version + package name + SHA-256 The Arkylx Index serves that directory, plus dist/bootstrap.ps1 as /tools/claude-mode/install.ps1. See ARKYLX_INDEX_INTEGRATION.md. #> [CmdletBinding()] param([string] $OutRoot) Set-StrictMode -Version 1.0 $ErrorActionPreference = 'Stop' $root = Split-Path -Parent $PSScriptRoot if (-not $OutRoot) { $OutRoot = Join-Path $root 'dist' } $version = (Get-Content (Join-Path $root 'VERSION') -Raw).Trim() if (-not $version) { throw 'VERSION file is empty' } Write-Host "building claude-mode $version" -ForegroundColor Cyan $stage = Join-Path ([System.IO.Path]::GetTempPath()) ("cm-stage-" + [Guid]::NewGuid().ToString('N').Substring(0, 8)) New-Item -ItemType Directory -Path (Join-Path $stage 'bin') -Force | Out-Null New-Item -ItemType Directory -Path (Join-Path $stage 'presets') -Force | Out-Null # Everything the installer needs, and nothing else - no README, no docs, no # .git. The payload is what lands on a user's machine. Copy-Item (Join-Path $root 'claude-mode.ps1') $stage -Force Copy-Item (Join-Path $root 'install.ps1') $stage -Force Copy-Item (Join-Path $root 'profile-snippet.ps1') $stage -Force Copy-Item (Join-Path $root 'VERSION') $stage -Force Get-ChildItem (Join-Path $root 'bin') -File | ForEach-Object { Copy-Item $_.FullName (Join-Path $stage 'bin') -Force } Get-ChildItem (Join-Path $root 'presets') -File | ForEach-Object { Copy-Item $_.FullName (Join-Path $stage 'presets') -Force } # Refuse to ship a package whose main script does not parse - a broken payload # would be installed by every user before anyone noticed. $errs = $null [void][System.Management.Automation.Language.Parser]::ParseFile((Join-Path $stage 'claude-mode.ps1'), [ref]$null, [ref]$errs) if ($errs.Count -gt 0) { $errs | ForEach-Object { Write-Host " L$($_.Extent.StartLineNumber): $($_.Message)" -ForegroundColor Red } throw 'claude-mode.ps1 does not parse - refusing to package' } foreach ($p in (Get-ChildItem (Join-Path $stage 'presets') -File)) { try { [void](Get-Content $p.FullName -Raw | ConvertFrom-Json) } catch { throw "preset $($p.Name) is not valid JSON - refusing to package" } } Write-Host ' ok payload validated' -ForegroundColor Green $outDir = Join-Path $OutRoot $version if (-not (Test-Path -LiteralPath $outDir)) { New-Item -ItemType Directory -Path $outDir -Force | Out-Null } $zipName = "claude-mode-$version.zip" $zipPath = Join-Path $outDir $zipName if (Test-Path -LiteralPath $zipPath) { Remove-Item -LiteralPath $zipPath -Force } Compress-Archive -Path (Join-Path $stage '*') -DestinationPath $zipPath -Force $sha = (Get-FileHash -LiteralPath $zipPath -Algorithm SHA256).Hash.ToLower() $manifest = [ordered]@{ tool = 'claude-mode' version = $version package = $zipName sha256 = $sha size = (Get-Item $zipPath).Length requires = [ordered]@{ powershell = '5.1'; os = 'windows' } entry = 'install.ps1' } $manifest | ConvertTo-Json -Depth 5 | Set-Content -LiteralPath (Join-Path $outDir 'manifest.json') -Encoding UTF8 # --- POSIX payload (Linux + macOS) ----------------------------------------- # Shipped as a tar.gz because zip does not preserve the executable bit, and the # installer must be runnable straight out of the archive. $posixStage = Join-Path ([System.IO.Path]::GetTempPath()) ("cm-posix-" + [Guid]::NewGuid().ToString('N').Substring(0, 8)) New-Item -ItemType Directory -Path (Join-Path $posixStage 'linux') -Force | Out-Null New-Item -ItemType Directory -Path (Join-Path $posixStage 'presets') -Force | Out-Null Get-ChildItem (Join-Path $root 'linux') -File | ForEach-Object { Copy-Item $_.FullName (Join-Path $posixStage 'linux') -Force } Get-ChildItem (Join-Path $root 'presets') -File | ForEach-Object { Copy-Item $_.FullName (Join-Path $posixStage 'presets') -Force } Copy-Item (Join-Path $root 'VERSION') $posixStage -Force # Shell scripts authored on Windows carry CRLF, which makes the kernel reject # the "#!/usr/bin/env bash" line. Normalise before packaging. Get-ChildItem (Join-Path $posixStage 'linux') -File | ForEach-Object { $text = [System.IO.File]::ReadAllText($_.FullName) -replace "`r`n", "`n" [System.IO.File]::WriteAllText($_.FullName, $text, (New-Object System.Text.UTF8Encoding($false))) } $tarName = "claude-mode-$version-posix.tar.gz" $tarPath = Join-Path $outDir $tarName if (Test-Path -LiteralPath $tarPath) { Remove-Item -LiteralPath $tarPath -Force } & tar.exe -czf $tarPath -C $posixStage 'linux' 'presets' 'VERSION' if ($LASTEXITCODE -ne 0) { throw 'tar failed - cannot build the POSIX package' } $shaPosix = (Get-FileHash -LiteralPath $tarPath -Algorithm SHA256).Hash.ToLower() [ordered]@{ tool = 'claude-mode' version = $version package = $tarName sha256 = $shaPosix size = (Get-Item $tarPath).Length requires = [ordered]@{ bash = '4.0'; python3 = '3.6'; os = 'linux,darwin' } entry = 'linux/install.sh' } | ConvertTo-Json -Depth 5 | Set-Content -LiteralPath (Join-Path $outDir 'manifest.posix.json') -Encoding UTF8 Remove-Item -LiteralPath $posixStage -Recurse -Force -ErrorAction SilentlyContinue Write-Host " ok $tarPath" -ForegroundColor Green Write-Host " ok sha256 $shaPosix" -ForegroundColor Green # "latest" is a copy rather than a symlink so a plain static file server can # serve it with no extra configuration. $latest = Join-Path $OutRoot 'latest' if (-not (Test-Path -LiteralPath $latest)) { New-Item -ItemType Directory -Path $latest -Force | Out-Null } Copy-Item $zipPath (Join-Path $latest $zipName) -Force Copy-Item (Join-Path $outDir 'manifest.json') (Join-Path $latest 'manifest.json') -Force Copy-Item $tarPath (Join-Path $latest $tarName) -Force Copy-Item (Join-Path $outDir 'manifest.posix.json') (Join-Path $latest 'manifest.posix.json') -Force Remove-Item -LiteralPath $stage -Recurse -Force -ErrorAction SilentlyContinue Write-Host " ok $zipPath" -ForegroundColor Green Write-Host " ok sha256 $sha" -ForegroundColor Green Write-Host " ok also copied to dist/latest/" -ForegroundColor Green