bot-forge 1.0.2

Rust CLI for installing agent skills and developer tools from configurable forms.
Documentation
[CmdletBinding()]
param(
    [Parameter(ValueFromRemainingArguments = $true)]
    [string[]]$InstallArgs
)

$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'
if ($PSVersionTable.PSEdition -eq 'Desktop') {
    [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
}

$staticBaseUrl = if ($env:BOT_FORGE_BASE_URL) {
    $env:BOT_FORGE_BASE_URL.TrimEnd('/')
} else {
    $null
}
$releaseTag = if ($env:BOT_FORGE_RELEASE_TAG) {
    $env:BOT_FORGE_RELEASE_TAG
} else {
    'v1.0.2'
}
$releaseAttachmentsBaseUrl = "https://api.gitcode.com/api/v5/repos/xuanwu/bot-forge/releases/$releaseTag/attach_files"

$architecture = try {
    [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString()
} catch {
    if ($env:PROCESSOR_ARCHITEW6432) {
        $env:PROCESSOR_ARCHITEW6432
    } else {
        $env:PROCESSOR_ARCHITECTURE
    }
}
$platform = switch ($architecture) {
    { $_ -in 'X64', 'AMD64' } { 'windows-x86_64'; break }
    { $_ -in 'Arm64', 'ARM64' } { 'windows-aarch64'; break }
    default { throw "Unsupported Windows architecture: $architecture" }
}

$localAppData = if ($env:LOCALAPPDATA) {
    $env:LOCALAPPDATA
} else {
    [Environment]::GetFolderPath([Environment+SpecialFolder]::LocalApplicationData)
}
$roamingAppData = if ($env:APPDATA) {
    $env:APPDATA
} else {
    [Environment]::GetFolderPath([Environment+SpecialFolder]::ApplicationData)
}
if (-not $localAppData -or -not $roamingAppData) {
    throw 'Unable to determine Windows application data directories; set BOT_FORGE_HOME and BOT_FORGE_CONFIG_DIR.'
}

$dataDir = if ($env:BOT_FORGE_HOME) {
    $env:BOT_FORGE_HOME
} else {
    Join-Path $localAppData 'bot-forge'
}
$binDir = if ($env:BOT_FORGE_BIN_DIR) {
    $env:BOT_FORGE_BIN_DIR
} else {
    Join-Path $dataDir 'bin'
}
$configDir = if ($env:BOT_FORGE_CONFIG_DIR) {
    $env:BOT_FORGE_CONFIG_DIR
} else {
    Join-Path $roamingAppData 'bot-forge'
}

$binaryRelativePath = "$platform-bot-forge.exe"
$configRelativePath = 'bot-forge.toml'
$checksumsRelativePath = 'SHA256SUMS'
$checksumsSha256 = $env:BOT_FORGE_CHECKSUMS_SHA256
$binaryPath = Join-Path $binDir 'bot-forge.exe'
$configPath = Join-Path $configDir 'bot-forge.toml'
$tempDir = Join-Path ([System.IO.Path]::GetTempPath()) "bot-forge-$PID-$([guid]::NewGuid().ToString('N'))"
$exitCode = 1

function Download-File([string]$RelativePath, [string]$Destination) {
    $uri = if ($staticBaseUrl) {
        "$staticBaseUrl/$RelativePath"
    } else {
        "$releaseAttachmentsBaseUrl/$RelativePath/download"
    }
    Invoke-WebRequest -UseBasicParsing -Uri $uri -OutFile $Destination
}

function Get-ExpectedChecksum([string]$RelativePath, [string]$ChecksumsPath) {
    foreach ($line in Get-Content -LiteralPath $ChecksumsPath) {
        $match = [regex]::Match($line, '^\s*([A-Fa-f0-9]{64})\s+\*?(.+?)\s*$')
        if ($match.Success -and $match.Groups[2].Value -eq $RelativePath) {
            return $match.Groups[1].Value.ToLowerInvariant()
        }
    }
    throw "SHA256SUMS does not contain a checksum for $RelativePath."
}

function Test-DownloadedFile([string]$RelativePath, [string]$Path, [string]$ChecksumsPath) {
    $expected = Get-ExpectedChecksum $RelativePath $ChecksumsPath
    $actual = (Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash.ToLowerInvariant()
    if ($actual -ne $expected) {
        throw "Checksum verification failed: $RelativePath"
    }
}

try {
    New-Item -ItemType Directory -Force -Path $dataDir, $binDir, $configDir, $tempDir | Out-Null
    $checksumsPath = Join-Path $tempDir 'SHA256SUMS'
    $downloadedBinary = Join-Path $tempDir 'bot-forge.exe'
    $downloadedConfig = Join-Path $tempDir 'bot-forge.toml'

    Write-Host "Downloading bot-forge ($platform)..."
    Download-File $checksumsRelativePath $checksumsPath
    if (-not $checksumsSha256 -or $checksumsSha256 -notmatch '^[A-Fa-f0-9]{64}$') {
        throw 'BOT_FORGE_CHECKSUMS_SHA256 must provide the trusted SHA256SUMS digest.'
    }
    $actualChecksumsHash = (Get-FileHash -LiteralPath $checksumsPath -Algorithm SHA256).Hash.ToLowerInvariant()
    if ($actualChecksumsHash -ne $checksumsSha256.ToLowerInvariant()) {
        throw 'SHA256SUMS release digest verification failed.'
    }
    Download-File $binaryRelativePath $downloadedBinary
    Download-File $configRelativePath $downloadedConfig

    Test-DownloadedFile $binaryRelativePath $downloadedBinary $checksumsPath
    Test-DownloadedFile $configRelativePath $downloadedConfig $checksumsPath

    $stagedBinary = Join-Path $binDir ".bot-forge.new-$([guid]::NewGuid().ToString('N')).exe"
    $binaryReplacementBackup = Join-Path $binDir ".bot-forge.old-$([guid]::NewGuid().ToString('N')).exe"
    Copy-Item -LiteralPath $downloadedBinary -Destination $stagedBinary
    try {
        if (Test-Path -LiteralPath $binaryPath) {
            [System.IO.File]::Replace($stagedBinary, $binaryPath, $binaryReplacementBackup)
        } else {
            [System.IO.File]::Move($stagedBinary, $binaryPath)
        }
    } finally {
        Remove-Item -LiteralPath $stagedBinary -Force -ErrorAction SilentlyContinue
        Remove-Item -LiteralPath $binaryReplacementBackup -Force -ErrorAction SilentlyContinue
    }
    if (-not (Test-Path -LiteralPath $configPath)) {
        $stagedConfig = Join-Path $configDir ".bot-forge.toml.new-$([guid]::NewGuid().ToString('N'))"
        Copy-Item -LiteralPath $downloadedConfig -Destination $stagedConfig
        try {
            [System.IO.File]::Move($stagedConfig, $configPath)
        } catch [System.IO.IOException] {
            Remove-Item -LiteralPath $stagedConfig -Force -ErrorAction SilentlyContinue
            if (Test-Path -LiteralPath $configPath) {
                Write-Host "Keeping configuration created concurrently: $configPath"
            } else {
                throw
            }
        }
    } else {
        Write-Host "Keeping existing configuration: $configPath"
    }

    Write-Host 'Download and verification complete; starting bot-forge.'
    & $binaryPath install --config $configPath @InstallArgs
    $exitCode = $LASTEXITCODE
} finally {
    if (Test-Path -LiteralPath $tempDir) {
        Remove-Item -LiteralPath $tempDir -Recurse -Force
    }
}

exit $exitCode