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 "不支持的 Windows 架构:$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 '无法确定 Windows 应用数据目录;请设置 BOT_FORGE_HOME 和 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 中缺少 $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 "文件校验失败:$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 "正在下载 bot-forge($platform)..."
    Download-File $checksumsRelativePath $checksumsPath
    if (-not $checksumsSha256 -or $checksumsSha256 -notmatch '^[A-Fa-f0-9]{64}$') {
        throw '必须通过 BOT_FORGE_CHECKSUMS_SHA256 提供可信的 SHA256SUMS 摘要。'
    }
    $actualChecksumsHash = (Get-FileHash -LiteralPath $checksumsPath -Algorithm SHA256).Hash.ToLowerInvariant()
    if ($actualChecksumsHash -ne $checksumsSha256.ToLowerInvariant()) {
        throw 'SHA256SUMS 发布摘要校验失败。'
    }
    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
    }
    Write-Host '正在验证服务器下发的云端配置...'
    & $binaryPath config validate --config $downloadedConfig
    if ($LASTEXITCODE -ne 0) {
        throw "服务器下发的 bot-forge.toml 配置验证失败,退出码:$LASTEXITCODE"
    }

    if (Test-Path -LiteralPath $configPath) {
        $configBackupPath = "${configPath}.bot-forge.bak"
        Copy-Item -LiteralPath $configPath -Destination $configBackupPath -Force
        Write-Host "已有配置已备份:$configBackupPath"
    }

    $stagedConfig = Join-Path $configDir ".bot-forge.toml.new-$([guid]::NewGuid().ToString('N'))"
    $configReplacementBackup = Join-Path $configDir ".bot-forge.toml.old-$([guid]::NewGuid().ToString('N'))"
    Copy-Item -LiteralPath $downloadedConfig -Destination $stagedConfig
    try {
        if (Test-Path -LiteralPath $configPath) {
            [System.IO.File]::Replace($stagedConfig, $configPath, $configReplacementBackup)
        } else {
            [System.IO.File]::Move($stagedConfig, $configPath)
        }
    } finally {
        Remove-Item -LiteralPath $stagedConfig -Force -ErrorAction SilentlyContinue
        Remove-Item -LiteralPath $configReplacementBackup -Force -ErrorAction SilentlyContinue
    }
    Write-Host "云端配置已写入:$configPath"

    Write-Host '下载和校验完成,开始运行 bot-forge。'
    & $binaryPath install --config $configPath @InstallArgs
    $exitCode = $LASTEXITCODE
} finally {
    if (Test-Path -LiteralPath $tempDir) {
        Remove-Item -LiteralPath $tempDir -Recurse -Force
    }
}

exit $exitCode