1124 lines
39 KiB
PowerShell
1124 lines
39 KiB
PowerShell
#requires -Version 5
|
|
|
|
[CmdletBinding()]
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string[]]$Capability,
|
|
|
|
[switch]$SkipRefresh,
|
|
|
|
[switch]$StartServices,
|
|
|
|
[ValidateSet('Claude', 'Codex', 'Both')]
|
|
[string]$McpHostTarget = 'Both'
|
|
)
|
|
|
|
# 临时目录统一入口($env:TEMP 在 Linux/macOS 上可能未设置)
|
|
$tmpBase = if ($env:TEMP) { $env:TEMP } else { [System.IO.Path]::GetTempPath() }
|
|
|
|
Set-StrictMode -Version Latest
|
|
$ErrorActionPreference = 'Stop'
|
|
[Console]::InputEncoding = [System.Text.UTF8Encoding]::new($false)
|
|
[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false)
|
|
$OutputEncoding = [System.Text.UTF8Encoding]::new($false)
|
|
|
|
. (Join-Path $PSScriptRoot 'lib\ToolDiscovery.ps1')
|
|
. (Join-Path $PSScriptRoot 'lib\BootstrapSupplyChain.ps1')
|
|
|
|
function Get-BootstrapDependency {
|
|
param([Parameter(Mandatory = $true)][string]$Name)
|
|
|
|
$manifest = Get-Content -LiteralPath (Get-ReverseBootstrapManifestPath) -Raw -Encoding UTF8 | ConvertFrom-Json
|
|
$dependency = $manifest.bootstrapDependencies.PSObject.Properties[$Name].Value
|
|
if ($null -eq $dependency -or [string]::IsNullOrWhiteSpace([string]$dependency.package) -or [string]::IsNullOrWhiteSpace([string]$dependency.version)) {
|
|
throw "bootstrapDependencies.$Name must define package and version."
|
|
}
|
|
return $dependency
|
|
}
|
|
|
|
$Capability = @(
|
|
foreach ($item in @($Capability)) {
|
|
if ([string]::IsNullOrWhiteSpace($item)) {
|
|
continue
|
|
}
|
|
foreach ($name in ($item -split ',')) {
|
|
$trimmed = $name.Trim()
|
|
if (-not [string]::IsNullOrWhiteSpace($trimmed)) {
|
|
$trimmed
|
|
}
|
|
}
|
|
}
|
|
) | Select-Object -Unique
|
|
|
|
function Get-FirstCommandPath {
|
|
param(
|
|
[string[]]$Names,
|
|
[switch]$PreferApplication
|
|
)
|
|
|
|
foreach ($name in $Names) {
|
|
$commands = @(Get-Command $name -All -ErrorAction SilentlyContinue)
|
|
if ($PreferApplication) {
|
|
$preferred = $commands | Where-Object { $_.CommandType -eq 'Application' } | Select-Object -First 1
|
|
if ($preferred) {
|
|
return $preferred.Source
|
|
}
|
|
}
|
|
$cmd = $commands | Select-Object -First 1
|
|
if ($cmd) {
|
|
return $cmd.Source
|
|
}
|
|
}
|
|
return ''
|
|
}
|
|
|
|
function Get-PreferredPowerShellPath {
|
|
$pwsh = Get-FirstCommandPath -Names @('pwsh.exe', 'pwsh') -PreferApplication
|
|
if (-not [string]::IsNullOrWhiteSpace($pwsh)) {
|
|
return $pwsh
|
|
}
|
|
return Get-FirstCommandPath -Names @('powershell.exe', 'powershell') -PreferApplication
|
|
}
|
|
|
|
function Get-NodeCommandPath {
|
|
param([Parameter(Mandatory = $true)][string]$Name)
|
|
|
|
return Get-FirstCommandPath -Names @("$Name.cmd", "$Name.exe", $Name) -PreferApplication
|
|
}
|
|
|
|
function Test-SuccessExitCode {
|
|
param([int]$ExitCode)
|
|
|
|
return $ExitCode -in @(0, 3010, 1641)
|
|
}
|
|
|
|
function Get-McpHostTargets {
|
|
switch ($McpHostTarget) {
|
|
'Claude' { return @('Claude') }
|
|
'Codex' { return @('Codex') }
|
|
default { return @('Claude', 'Codex') }
|
|
}
|
|
}
|
|
|
|
function Test-ReverseIsWindows {
|
|
return $env:OS -eq 'Windows_NT'
|
|
}
|
|
|
|
function Test-ReverseIsElevated {
|
|
if (-not (Test-ReverseIsWindows)) {
|
|
return $true
|
|
}
|
|
|
|
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
|
|
$principal = New-Object Security.Principal.WindowsPrincipal($identity)
|
|
return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
|
|
}
|
|
|
|
function Ensure-WingetPackage {
|
|
param(
|
|
[Parameter(Mandatory = $true)][string]$Id,
|
|
[Parameter(Mandatory = $true)][string]$Label,
|
|
[string]$Override
|
|
)
|
|
|
|
$winget = Get-FirstCommandPath -Names @('winget')
|
|
if ([string]::IsNullOrWhiteSpace($winget)) {
|
|
throw "Cannot auto-install $Label because winget is not available."
|
|
}
|
|
|
|
$arguments = @(
|
|
'install',
|
|
'--id', $Id,
|
|
'--source', 'winget',
|
|
'--accept-package-agreements',
|
|
'--accept-source-agreements',
|
|
'--disable-interactivity'
|
|
)
|
|
if (-not [string]::IsNullOrWhiteSpace($Override)) {
|
|
$arguments += @('--override', $Override)
|
|
}
|
|
|
|
& $winget @arguments
|
|
if (-not (Test-SuccessExitCode -ExitCode $LASTEXITCODE)) {
|
|
throw "winget install failed for $Label ($Id)."
|
|
}
|
|
}
|
|
|
|
function Ensure-NodeRuntime {
|
|
if (-not (Get-FirstCommandPath -Names @('node'))) {
|
|
Ensure-WingetPackage -Id 'OpenJS.NodeJS.22' -Label 'Node.js 22'
|
|
}
|
|
if (-not (Get-NodeCommandPath -Name 'npx')) {
|
|
Ensure-WingetPackage -Id 'OpenJS.NodeJS.22' -Label 'Node.js 22'
|
|
}
|
|
}
|
|
|
|
function Ensure-PythonRuntime {
|
|
if (-not (Get-FirstCommandPath -Names @('python', 'python3'))) {
|
|
Ensure-WingetPackage -Id 'Python.Python.3.13' -Label 'Python 3.13'
|
|
}
|
|
}
|
|
|
|
function Ensure-JavaRuntime {
|
|
if (-not (Get-FirstCommandPath -Names @('java'))) {
|
|
Ensure-WingetPackage -Id 'Microsoft.OpenJDK.21' -Label 'OpenJDK 21'
|
|
}
|
|
}
|
|
|
|
function Get-AnythingAnalyzerUserDataPaths {
|
|
$candidates = @(
|
|
(Join-Path $env:APPDATA 'anything-analyzer'),
|
|
(Join-Path $env:APPDATA 'Anything Analyzer')
|
|
)
|
|
|
|
$existing = @($candidates | Where-Object { Test-Path -LiteralPath $_ -PathType Container })
|
|
if ($existing.Count -gt 0) {
|
|
return @($existing | Sort-Object -Unique)
|
|
}
|
|
|
|
return @($candidates | Sort-Object -Unique)
|
|
}
|
|
|
|
function Ensure-AnythingAnalyzerMcpConfig {
|
|
param([int]$Port = 23816)
|
|
|
|
$token = ''
|
|
foreach ($userDataPath in Get-AnythingAnalyzerUserDataPaths) {
|
|
$configPath = Join-Path $userDataPath 'mcp-server-config.json'
|
|
if (-not (Test-Path -LiteralPath $configPath)) {
|
|
continue
|
|
}
|
|
try {
|
|
$existing = Get-Content -LiteralPath $configPath -Raw -Encoding utf8 | ConvertFrom-Json
|
|
if (-not [string]::IsNullOrWhiteSpace([string]$existing.authToken)) {
|
|
$token = [string]$existing.authToken
|
|
break
|
|
}
|
|
}
|
|
catch {
|
|
$token = ''
|
|
}
|
|
}
|
|
if ([string]::IsNullOrWhiteSpace($token)) {
|
|
$tokenBytes = New-Object byte[] 32
|
|
$rng = [Security.Cryptography.RandomNumberGenerator]::Create()
|
|
try {
|
|
$rng.GetBytes($tokenBytes)
|
|
}
|
|
finally {
|
|
$rng.Dispose()
|
|
}
|
|
$token = [Convert]::ToBase64String($tokenBytes)
|
|
}
|
|
|
|
$payload = [ordered]@{
|
|
enabled = $true
|
|
port = $Port
|
|
authEnabled = $true
|
|
authToken = $token
|
|
}
|
|
|
|
foreach ($userDataPath in Get-AnythingAnalyzerUserDataPaths) {
|
|
if (-not (Test-Path -LiteralPath $userDataPath)) {
|
|
New-Item -ItemType Directory -Path $userDataPath -Force | Out-Null
|
|
}
|
|
$configPath = Join-Path $userDataPath 'mcp-server-config.json'
|
|
$payload | ConvertTo-Json -Depth 4 | Set-Content -LiteralPath $configPath -Encoding utf8
|
|
}
|
|
|
|
return $token
|
|
}
|
|
|
|
function Test-VsBuildToolsInstalled {
|
|
$roots = @(
|
|
(Join-Path ${env:ProgramFiles(x86)} 'Microsoft Visual Studio\2022\BuildTools'),
|
|
(Join-Path $env:ProgramFiles 'Microsoft Visual Studio\2022\BuildTools')
|
|
) | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }
|
|
|
|
foreach ($root in $roots) {
|
|
$vcToolsPath = Join-Path $root 'VC\Tools\MSVC'
|
|
if (Test-Path -LiteralPath $vcToolsPath -PathType Container) {
|
|
return $true
|
|
}
|
|
}
|
|
|
|
return $false
|
|
}
|
|
|
|
function Ensure-VsBuildTools {
|
|
if (Test-VsBuildToolsInstalled) {
|
|
return
|
|
}
|
|
|
|
$override = '--wait --quiet --norestart --add Microsoft.VisualStudio.Workload.VCTools --includeRecommended'
|
|
try {
|
|
Ensure-WingetPackage -Id 'Microsoft.VisualStudio.2022.BuildTools' -Label 'Visual Studio Build Tools 2022' -Override $override
|
|
}
|
|
catch {
|
|
if (-not (Test-ReverseIsElevated)) {
|
|
throw "$($_.Exception.Message) Current PowerShell process is not elevated; Visual Studio Build Tools may require an administrator/UAC install."
|
|
}
|
|
throw
|
|
}
|
|
|
|
if (-not (Test-VsBuildToolsInstalled)) {
|
|
throw 'Visual Studio Build Tools installed command completed, but the C++ workload was not detected.'
|
|
}
|
|
}
|
|
|
|
function Test-AnythingAnalyzerElectronHealthy {
|
|
param(
|
|
[Parameter(Mandatory = $true)][string]$RepoDir,
|
|
[Parameter(Mandatory = $true)][string]$PnpmPath
|
|
)
|
|
|
|
Push-Location $RepoDir
|
|
try {
|
|
& $PnpmPath exec electron --version *> $null
|
|
return $LASTEXITCODE -eq 0
|
|
}
|
|
catch {
|
|
return $false
|
|
}
|
|
finally {
|
|
Pop-Location
|
|
}
|
|
}
|
|
|
|
function Set-AnythingAnalyzerPnpmBuildApprovals {
|
|
param(
|
|
[Parameter(Mandatory = $true)][string]$RepoDir,
|
|
[Parameter(Mandatory = $true)][string[]]$Packages
|
|
)
|
|
|
|
$workspacePath = Join-Path $RepoDir 'pnpm-workspace.yaml'
|
|
$lines = @()
|
|
if (Test-Path -LiteralPath $workspacePath) {
|
|
$lines = @(Get-Content -LiteralPath $workspacePath)
|
|
}
|
|
|
|
$existingPackages = New-Object System.Collections.Generic.List[string]
|
|
$preserved = New-Object System.Collections.Generic.List[string]
|
|
$skipOnlyBuilt = $false
|
|
|
|
foreach ($line in $lines) {
|
|
if ($line -match '^\s*onlyBuiltDependencies\s*:') {
|
|
$skipOnlyBuilt = $true
|
|
continue
|
|
}
|
|
|
|
if ($skipOnlyBuilt) {
|
|
if ($line -match '^\s*-\s*(.+?)\s*$') {
|
|
$existingPackages.Add($matches[1].Trim("'`""))
|
|
continue
|
|
}
|
|
if ($line -match '^\s+' -or [string]::IsNullOrWhiteSpace($line)) {
|
|
continue
|
|
}
|
|
$skipOnlyBuilt = $false
|
|
}
|
|
|
|
$preserved.Add($line)
|
|
}
|
|
|
|
while ($preserved.Count -gt 0 -and [string]::IsNullOrWhiteSpace($preserved[$preserved.Count - 1])) {
|
|
$preserved.RemoveAt($preserved.Count - 1)
|
|
}
|
|
|
|
$approvedPackages = @($existingPackages + @($Packages)) | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Sort-Object -Unique
|
|
if ($preserved.Count -gt 0) {
|
|
$preserved.Add('')
|
|
}
|
|
$preserved.Add('onlyBuiltDependencies:')
|
|
foreach ($package in $approvedPackages) {
|
|
$preserved.Add(" - $package")
|
|
}
|
|
|
|
Set-Content -LiteralPath $workspacePath -Value @($preserved) -Encoding utf8
|
|
}
|
|
|
|
function Approve-AnythingAnalyzerBuildScripts {
|
|
param(
|
|
[Parameter(Mandatory = $true)][string]$RepoDir
|
|
)
|
|
|
|
$buildPackages = @('electron', 'esbuild', 'better-sqlite3')
|
|
|
|
Set-AnythingAnalyzerPnpmBuildApprovals -RepoDir $RepoDir -Packages $buildPackages
|
|
}
|
|
|
|
function Get-GitHubLatestReleaseAsset {
|
|
param(
|
|
[Parameter(Mandatory = $true)][string]$Repo,
|
|
[Parameter(Mandatory = $true)][string]$AssetRegex,
|
|
[string]$ReleaseTag = ''
|
|
)
|
|
|
|
if (-not [string]::IsNullOrWhiteSpace($ReleaseTag)) {
|
|
$uri = "https://api.github.com/repos/$Repo/releases/tags/$ReleaseTag"
|
|
}
|
|
else {
|
|
$uri = "https://api.github.com/repos/$Repo/releases/latest"
|
|
}
|
|
$release = Invoke-RestMethod -Uri $uri -Headers @{ 'User-Agent' = 'reverse-skill-bootstrap' }
|
|
$asset = @($release.assets) | Where-Object { $_.name -match $AssetRegex } | Select-Object -First 1
|
|
if ($null -eq $asset) {
|
|
throw "No release asset matched $AssetRegex for $Repo (tag=$ReleaseTag)"
|
|
}
|
|
return $asset
|
|
}
|
|
|
|
function Get-FileSha256Hex {
|
|
param([Parameter(Mandatory = $true)][string]$Path)
|
|
return (Get-FileHash -Algorithm SHA256 -LiteralPath $Path).Hash.ToLowerInvariant()
|
|
}
|
|
|
|
function Assert-DownloadedFileIntegrity {
|
|
param(
|
|
[Parameter(Mandatory = $true)][string]$Path,
|
|
$Definition = $null,
|
|
$Asset = $null
|
|
)
|
|
|
|
if (-not (Test-Path -LiteralPath $Path)) {
|
|
throw "Integrity check failed: file missing $Path"
|
|
}
|
|
|
|
$actual = Get-FileSha256Hex -Path $Path
|
|
$expected = $null
|
|
$source = $null
|
|
|
|
if ($null -ne $Definition -and $Definition.PSObject.Properties['assetSha256'] -and -not [string]::IsNullOrWhiteSpace([string]$Definition.assetSha256)) {
|
|
$expected = ([string]$Definition.assetSha256 -replace '^(?i)sha256:', '').Trim().ToLowerInvariant()
|
|
$source = 'manifest.assetSha256'
|
|
}
|
|
elseif ($null -ne $Asset -and $Asset.PSObject.Properties['digest'] -and -not [string]::IsNullOrWhiteSpace([string]$Asset.digest)) {
|
|
$expected = ([string]$Asset.digest -replace '^(?i)sha256:', '').Trim().ToLowerInvariant()
|
|
$source = 'github.api.digest'
|
|
}
|
|
|
|
if (-not [string]::IsNullOrWhiteSpace($expected)) {
|
|
if ($actual -ne $expected) {
|
|
Remove-Item -LiteralPath $Path -Force -ErrorAction SilentlyContinue
|
|
throw "SHA256 mismatch for $(Split-Path -Leaf $Path) (via $source): expected $expected got $actual — file deleted"
|
|
}
|
|
Write-Host ("[integrity] SHA256 OK ({0}): {1}" -f $source, $actual) -ForegroundColor Green
|
|
return $actual
|
|
}
|
|
|
|
Write-Warning ("[integrity] No pinned digest for {0}; recorded sha256={1} (supply-chain residual — prefer assetSha256 in manifest)" -f (Split-Path -Leaf $Path), $actual)
|
|
return $actual
|
|
}
|
|
|
|
function Expand-ArchiveIntoDirectory {
|
|
param(
|
|
[Parameter(Mandatory = $true)][string]$ZipPath,
|
|
[Parameter(Mandatory = $true)][string]$Destination
|
|
)
|
|
|
|
$tempExtract = Join-Path $tmpBase ("reverse-bootstrap-" + [System.Guid]::NewGuid().ToString('N'))
|
|
New-Item -ItemType Directory -Path $tempExtract -Force | Out-Null
|
|
Expand-Archive -LiteralPath $ZipPath -DestinationPath $tempExtract -Force
|
|
|
|
if (Test-Path -LiteralPath $Destination) {
|
|
Remove-Item -LiteralPath $Destination -Recurse -Force
|
|
}
|
|
New-Item -ItemType Directory -Path $Destination -Force | Out-Null
|
|
|
|
$children = Get-ChildItem -LiteralPath $tempExtract
|
|
if ($children.Count -eq 1 -and $children[0].PSIsContainer) {
|
|
$sourceDir = $children[0].FullName
|
|
}
|
|
else {
|
|
$sourceDir = $tempExtract
|
|
}
|
|
|
|
Get-ChildItem -LiteralPath $sourceDir -Force | ForEach-Object {
|
|
Move-Item -LiteralPath $_.FullName -Destination $Destination -Force
|
|
}
|
|
|
|
Remove-Item -LiteralPath $tempExtract -Recurse -Force
|
|
}
|
|
|
|
function Ensure-DownloadDirectory {
|
|
param([Parameter(Mandatory = $true)][string]$Path)
|
|
|
|
if (-not (Test-Path -LiteralPath $Path)) {
|
|
New-Item -ItemType Directory -Path $Path -Force | Out-Null
|
|
}
|
|
}
|
|
|
|
function Ensure-GitHubZipInstall {
|
|
param(
|
|
[Parameter(Mandatory = $true)]$Definition,
|
|
[Parameter(Mandatory = $true)][string]$TargetPath,
|
|
[Parameter(Mandatory = $true)][string]$VerifyName
|
|
)
|
|
|
|
$existing = Resolve-ReverseToolSpec -Name $VerifyName
|
|
if ($existing.Available) {
|
|
return $existing
|
|
}
|
|
|
|
$releaseTag = if ($Definition.PSObject.Properties['releaseTag']) { [string]$Definition.releaseTag } else { '' }
|
|
$asset = Get-GitHubLatestReleaseAsset -Repo $Definition.repo -AssetRegex $Definition.assetRegex -ReleaseTag $releaseTag
|
|
$downloadUrl = if ($asset.PSObject.Properties['browser_download_url']) { $asset.browser_download_url } else { $asset.url }
|
|
$downloadPath = Join-Path $tmpBase $asset.name
|
|
Invoke-WebRequest -Uri $downloadUrl -OutFile $downloadPath -Headers @{ 'Accept' = 'application/octet-stream' }
|
|
Assert-DownloadedFileIntegrity -Path $downloadPath -Definition $Definition -Asset $asset | Out-Null
|
|
Ensure-DownloadDirectory -Path (Split-Path -Path $TargetPath -Parent)
|
|
Expand-ArchiveIntoDirectory -ZipPath $downloadPath -Destination $TargetPath
|
|
Remove-Item -LiteralPath $downloadPath -Force
|
|
|
|
# Refresh PATH so newly installed tools are discoverable
|
|
$binCandidates = @(
|
|
(Join-Path $TargetPath 'bin'),
|
|
$TargetPath
|
|
)
|
|
foreach ($binDir in $binCandidates) {
|
|
if ((Test-Path -LiteralPath $binDir) -and ($env:PATH -notlike "*$binDir*")) {
|
|
$env:PATH = "$binDir;$env:PATH"
|
|
}
|
|
}
|
|
|
|
return (Resolve-ReverseToolSpec -Name $VerifyName)
|
|
}
|
|
|
|
function Ensure-ApktoolInstall {
|
|
param([Parameter(Mandatory = $true)]$Definition)
|
|
|
|
$existing = Resolve-ReverseToolSpec -Name 'apktool'
|
|
if ($existing.Available) {
|
|
return $existing
|
|
}
|
|
|
|
Ensure-JavaRuntime
|
|
$releaseTag = if ($Definition.PSObject.Properties['releaseTag']) { [string]$Definition.releaseTag } else { '' }
|
|
$asset = Get-GitHubLatestReleaseAsset -Repo $Definition.repo -AssetRegex $Definition.assetRegex -ReleaseTag $releaseTag
|
|
$installDir = $Definition.installDir
|
|
Ensure-DownloadDirectory -Path $installDir
|
|
|
|
$jarName = [System.IO.Path]::GetFileName($asset.name)
|
|
$jarPath = Join-Path $installDir 'apktool.jar'
|
|
$downloadUrl = if ($asset.PSObject.Properties['browser_download_url']) { $asset.browser_download_url } else { $asset.url }
|
|
Invoke-WebRequest -Uri $downloadUrl -OutFile $jarPath -Headers @{ 'Accept' = 'application/octet-stream' }
|
|
Assert-DownloadedFileIntegrity -Path $jarPath -Definition $Definition -Asset $asset | Out-Null
|
|
|
|
$wrapperPath = Join-Path $installDir $Definition.wrapperName
|
|
@(
|
|
'@echo off',
|
|
'setlocal',
|
|
'java -jar "%~dp0apktool.jar" %*'
|
|
) | Set-Content -LiteralPath $wrapperPath -Encoding ascii
|
|
|
|
# Add to PATH so discovery can find it immediately
|
|
if ($env:PATH -notlike "*$installDir*") {
|
|
$env:PATH = "$installDir;$env:PATH"
|
|
}
|
|
|
|
return (Resolve-ReverseToolSpec -Name 'apktool')
|
|
}
|
|
|
|
function Ensure-PipPackageInstall {
|
|
param([Parameter(Mandatory = $true)]$Definition)
|
|
|
|
Ensure-PythonRuntime
|
|
$python = Get-FirstCommandPath -Names @('python', 'python3')
|
|
# Use pipSource (git URL) if available, otherwise use pipPackage name
|
|
$installTarget = if ($Definition.PSObject.Properties['pipSource'] -and -not [string]::IsNullOrWhiteSpace($Definition.pipSource)) {
|
|
$Definition.pipSource
|
|
} else {
|
|
$Definition.pipPackage
|
|
}
|
|
& $python -m pip install --upgrade $installTarget
|
|
if ($LASTEXITCODE -ne 0) {
|
|
throw "pip install failed for $installTarget"
|
|
}
|
|
}
|
|
|
|
function Get-ClaudeMcpConfig {
|
|
$path = Get-ClaudeMcpConfigPath
|
|
if (-not (Test-Path -LiteralPath $path)) {
|
|
return @{ path = $path; json = @{ mcpServers = @{} } }
|
|
}
|
|
|
|
$json = Read-ReverseJsonAsHashtable -Path $path
|
|
if ($null -eq $json) {
|
|
$json = @{ mcpServers = @{} }
|
|
}
|
|
if (-not $json.Contains('mcpServers')) {
|
|
$json['mcpServers'] = @{}
|
|
}
|
|
return @{ path = $path; json = $json }
|
|
}
|
|
|
|
function Save-ClaudeMcpConfig {
|
|
param([Parameter(Mandatory = $true)]$Config)
|
|
|
|
$parent = Split-Path -Path $Config.path -Parent
|
|
if (-not (Test-Path -LiteralPath $parent)) {
|
|
New-Item -ItemType Directory -Path $parent -Force | Out-Null
|
|
}
|
|
|
|
$Config.json | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath $Config.path -Encoding utf8
|
|
}
|
|
|
|
function ConvertTo-TomlLiteral {
|
|
param([Parameter(Mandatory = $true)]$Value)
|
|
|
|
if ($Value -is [bool]) {
|
|
return $Value.ToString().ToLowerInvariant()
|
|
}
|
|
if ($Value -is [int] -or $Value -is [long] -or $Value -is [double] -or $Value -is [decimal]) {
|
|
return [string]$Value
|
|
}
|
|
if ($Value -is [System.Collections.IEnumerable] -and -not ($Value -is [string])) {
|
|
$items = foreach ($item in $Value) {
|
|
ConvertTo-TomlLiteral -Value $item
|
|
}
|
|
return "[{0}]" -f ($items -join ', ')
|
|
}
|
|
|
|
$escaped = ([string]$Value).Replace('\', '\\').Replace('"', '\"')
|
|
return '"' + $escaped + '"'
|
|
}
|
|
|
|
function Remove-CodexMcpServerBlocks {
|
|
param(
|
|
[AllowEmptyCollection()]
|
|
[string[]]$Lines,
|
|
[Parameter(Mandatory = $true)][string]$ServerName
|
|
)
|
|
|
|
$targetPattern = '^\[mcp_servers\.{0}(?:\.env)?\]\s*$' -f [regex]::Escape($ServerName)
|
|
$result = New-Object System.Collections.Generic.List[string]
|
|
$skipBlock = $false
|
|
|
|
if ($null -eq $Lines) {
|
|
return @()
|
|
}
|
|
|
|
foreach ($line in $Lines) {
|
|
if ($line -match '^\[') {
|
|
if ($line -match $targetPattern) {
|
|
$skipBlock = $true
|
|
continue
|
|
}
|
|
|
|
if ($skipBlock) {
|
|
$skipBlock = $false
|
|
}
|
|
}
|
|
|
|
if (-not $skipBlock) {
|
|
$result.Add($line)
|
|
}
|
|
}
|
|
|
|
return @($result)
|
|
}
|
|
|
|
function Set-CodexMcpServer {
|
|
param(
|
|
[Parameter(Mandatory = $true)][string]$ServerName,
|
|
[Parameter(Mandatory = $true)][hashtable]$ServerDefinition
|
|
)
|
|
|
|
$path = Get-CodexConfigPath
|
|
$parent = Split-Path -Path $path -Parent
|
|
if (-not (Test-Path -LiteralPath $parent)) {
|
|
New-Item -ItemType Directory -Path $parent -Force | Out-Null
|
|
}
|
|
|
|
$lines = @()
|
|
if (Test-Path -LiteralPath $path) {
|
|
$rawLines = @(Get-Content -LiteralPath $path)
|
|
if ($rawLines.Count -eq 1 -and [string]::IsNullOrEmpty($rawLines[0])) {
|
|
$lines = @()
|
|
}
|
|
else {
|
|
$lines = $rawLines
|
|
}
|
|
}
|
|
$lines = @(Remove-CodexMcpServerBlocks -Lines $lines -ServerName $ServerName)
|
|
|
|
while ($lines.Count -gt 0 -and [string]::IsNullOrWhiteSpace($lines[-1])) {
|
|
$lines = if ($lines.Count -gt 1) { $lines[0..($lines.Count - 2)] } else { @() }
|
|
}
|
|
if ($lines.Count -gt 0) {
|
|
$lines += ''
|
|
}
|
|
|
|
$lines += "[mcp_servers.$ServerName]"
|
|
foreach ($key in @('type', 'url', 'command', 'args', 'bearer_token_env_var')) {
|
|
if ($ServerDefinition.Contains($key)) {
|
|
$lines += "$key = $(ConvertTo-TomlLiteral -Value $ServerDefinition[$key])"
|
|
}
|
|
}
|
|
foreach ($key in ($ServerDefinition.Keys | Where-Object { $_ -notin @('type', 'url', 'command', 'args', 'bearer_token_env_var', 'env') } | Sort-Object)) {
|
|
$lines += "$key = $(ConvertTo-TomlLiteral -Value $ServerDefinition[$key])"
|
|
}
|
|
|
|
if ($ServerDefinition.Contains('env') -and $ServerDefinition['env'] -is [System.Collections.IDictionary] -and $ServerDefinition['env'].Count -gt 0) {
|
|
$lines += ''
|
|
$lines += "[mcp_servers.$ServerName.env]"
|
|
foreach ($envKey in ($ServerDefinition['env'].Keys | Sort-Object)) {
|
|
$lines += "$envKey = $(ConvertTo-TomlLiteral -Value $ServerDefinition['env'][$envKey])"
|
|
}
|
|
}
|
|
|
|
Set-Content -LiteralPath $path -Value $lines -Encoding utf8
|
|
}
|
|
|
|
function Ensure-McpServer {
|
|
param(
|
|
[Parameter(Mandatory = $true)][string]$ServerName,
|
|
[Parameter(Mandatory = $true)][hashtable]$ServerDefinition
|
|
)
|
|
|
|
foreach ($target in Get-McpHostTargets) {
|
|
switch ($target) {
|
|
'Claude' {
|
|
$config = Get-ClaudeMcpConfig
|
|
$claudeDefinition = @{}
|
|
foreach ($key in $ServerDefinition.Keys) {
|
|
if ($key -ne 'bearer_token_env_var') {
|
|
$claudeDefinition[$key] = $ServerDefinition[$key]
|
|
}
|
|
}
|
|
$config.json.mcpServers[$ServerName] = $claudeDefinition
|
|
Save-ClaudeMcpConfig -Config $config
|
|
}
|
|
'Codex' {
|
|
$codexDefinition = @{}
|
|
foreach ($key in $ServerDefinition.Keys) {
|
|
if ($key -ne 'headers') {
|
|
$codexDefinition[$key] = $ServerDefinition[$key]
|
|
}
|
|
}
|
|
Set-CodexMcpServer -ServerName $ServerName -ServerDefinition $codexDefinition
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function Get-McpCommandServerDefinition {
|
|
param(
|
|
[Parameter(Mandatory = $true)][string]$Command,
|
|
[Parameter(Mandatory = $true)][string[]]$Arguments,
|
|
[hashtable]$Env = @{}
|
|
)
|
|
|
|
if ((Test-ReverseIsWindows) -and $Command -in @('npx', 'pnpm', 'npm')) {
|
|
return @{
|
|
type = 'stdio'
|
|
command = 'cmd'
|
|
args = @('/c', $Command) + @($Arguments)
|
|
env = $Env
|
|
}
|
|
}
|
|
|
|
return @{
|
|
type = 'stdio'
|
|
command = $Command
|
|
args = @($Arguments)
|
|
env = $Env
|
|
}
|
|
}
|
|
|
|
function Wait-ForPort {
|
|
param(
|
|
[Parameter(Mandatory = $true)][int]$Port,
|
|
[int]$TimeoutSeconds = 90
|
|
)
|
|
|
|
$deadline = (Get-Date).AddSeconds($TimeoutSeconds)
|
|
while ((Get-Date) -lt $deadline) {
|
|
if (Test-ReverseTcpPort -Port $Port) {
|
|
return $true
|
|
}
|
|
Start-Sleep -Seconds 2
|
|
}
|
|
return $false
|
|
}
|
|
|
|
function Start-AnythingAnalyzerService {
|
|
param(
|
|
[Parameter(Mandatory = $true)]$Definition,
|
|
[string]$AuthToken = ''
|
|
)
|
|
|
|
if ([string]::IsNullOrWhiteSpace($AuthToken)) {
|
|
$AuthToken = Ensure-AnythingAnalyzerMcpConfig -Port ([int]$Definition.servicePort)
|
|
}
|
|
|
|
$repoDir = [string]$Definition.installDir
|
|
$checkoutDefinition = [pscustomobject]@{
|
|
repo = [string]$Definition.repoUrl
|
|
pinnedCommit = [string]$Definition.pinnedCommit
|
|
}
|
|
Ensure-GitCloneInstall -Definition $checkoutDefinition -TargetPath $repoDir | Out-Null
|
|
|
|
if (Test-ReverseTcpPort -Port ([int]$Definition.servicePort)) {
|
|
return
|
|
}
|
|
|
|
Ensure-Pnpm
|
|
$vsBuildToolsError = ''
|
|
if (Test-ReverseIsWindows) {
|
|
try {
|
|
Ensure-VsBuildTools
|
|
}
|
|
catch {
|
|
$vsBuildToolsError = $_.Exception.Message
|
|
Write-Warning "Visual Studio Build Tools auto-install failed; continuing with pnpm prebuilt/native rebuild path. $vsBuildToolsError"
|
|
}
|
|
}
|
|
|
|
$pnpm = Get-NodeCommandPath -Name 'pnpm'
|
|
if ([string]::IsNullOrWhiteSpace($pnpm)) {
|
|
throw 'pnpm is not available after installation.'
|
|
}
|
|
$git = Get-FirstCommandPath -Names @('git')
|
|
Invoke-AnythingAnalyzerPinnedInstall -RepoDir $repoDir -PnpmPath $pnpm -GitPath $git `
|
|
-PinnedCommit ([string]$Definition.pinnedCommit) -VsBuildToolsError $vsBuildToolsError
|
|
|
|
$stdoutLog = Join-Path $repoDir 'anything-analyzer-dev.log'
|
|
$stderrLog = Join-Path $repoDir 'anything-analyzer-dev.err.log'
|
|
Remove-Item -LiteralPath $stdoutLog, $stderrLog -Force -ErrorAction SilentlyContinue
|
|
|
|
Start-Process -FilePath $pnpm -ArgumentList @('dev') -WorkingDirectory $repoDir -WindowStyle Hidden -RedirectStandardOutput $stdoutLog -RedirectStandardError $stderrLog | Out-Null
|
|
if (-not (Wait-ForPort -Port ([int]$Definition.servicePort) -TimeoutSeconds 120)) {
|
|
throw "anything-analyzer did not open port 23816 in time. Logs: $stdoutLog ; $stderrLog"
|
|
}
|
|
}
|
|
|
|
function Start-IdaProService {
|
|
param([Parameter(Mandatory = $true)]$Definition)
|
|
|
|
if (Test-ReverseTcpPort -Port ([int]$Definition.servicePort)) {
|
|
return
|
|
}
|
|
|
|
$startScript = $Definition.startScript
|
|
$shell = Get-PreferredPowerShellPath
|
|
& $shell -NoProfile -ExecutionPolicy Bypass -File $startScript
|
|
if ($LASTEXITCODE -ne 0 -and -not (Test-ReverseTcpPort -Port ([int]$Definition.servicePort))) {
|
|
throw 'Failed to start idapro service.'
|
|
}
|
|
|
|
if (-not (Wait-ForPort -Port ([int]$Definition.servicePort) -TimeoutSeconds 45)) {
|
|
throw 'idapro service did not open port 13337 in time.'
|
|
}
|
|
}
|
|
|
|
function Ensure-AndroidPlatformTools {
|
|
$adb = Resolve-ReverseToolSpec -Name 'adb'
|
|
if ($adb.Available) {
|
|
return $adb
|
|
}
|
|
|
|
Ensure-WingetPackage -Id 'Google.PlatformTools' -Label 'Android SDK Platform-Tools'
|
|
return (Resolve-ReverseToolSpec -Name 'adb')
|
|
}
|
|
|
|
function Ensure-Capability {
|
|
param([Parameter(Mandatory = $true)][string]$Name)
|
|
|
|
$definition = Get-ReverseBootstrapDefinition -Name $Name
|
|
if ($null -eq $definition) {
|
|
throw "No bootstrap definition for capability: $Name"
|
|
}
|
|
|
|
# If capability is marked as not auto-installable, output guidance and skip
|
|
if ($definition.PSObject.Properties['canAutoInstall'] -and $definition.canAutoInstall -eq $false) {
|
|
$hint = if ($definition.PSObject.Properties['manualInstallHint']) { $definition.manualInstallHint } else { "Please install $Name manually. Docs: $($definition.docsUrl)" }
|
|
Write-Warning "MANUAL_INSTALL_REQUIRED: $Name — $hint"
|
|
# Still try to register MCP URL if applicable
|
|
if ($definition.PSObject.Properties['mcpNames'] -and $definition.PSObject.Properties['mcpUrl']) {
|
|
Ensure-McpServer -ServerName $definition.mcpNames[0] -ServerDefinition @{ url = $definition.mcpUrl }
|
|
}
|
|
return $false
|
|
}
|
|
|
|
$existingState = Get-ReverseCapabilityState -Name $Name
|
|
if ($existingState -and -not $definition.PSObject.Properties['mcpNames'] -and $definition.bootstrapKind -ne 'git-clone') {
|
|
$toolSpec = $null
|
|
try {
|
|
$toolSpec = Resolve-ReverseToolSpec -Name $Name
|
|
}
|
|
catch {
|
|
$toolSpec = $null
|
|
}
|
|
|
|
if ($toolSpec -and $toolSpec.Available) {
|
|
return $toolSpec
|
|
}
|
|
}
|
|
|
|
switch ($definition.bootstrapKind) {
|
|
'github-release-zip' {
|
|
# Generic handler for all github-release-zip capabilities
|
|
$verifyName = if ($definition.PSObject.Properties['verifyCommand'] -and -not [string]::IsNullOrWhiteSpace($definition.verifyCommand)) {
|
|
$definition.verifyCommand
|
|
} else { $Name }
|
|
return Ensure-GitHubZipInstall -Definition $definition -TargetPath $definition.installDir -VerifyName $verifyName
|
|
}
|
|
'git-clone' {
|
|
return Ensure-GitCloneInstall -Definition $definition -TargetPath $definition.installDir
|
|
}
|
|
'github-release-jar-wrapper' {
|
|
return Ensure-ApktoolInstall -Definition $definition
|
|
}
|
|
'pip-package' {
|
|
Ensure-PipPackageInstall -Definition $definition
|
|
return $true
|
|
}
|
|
'winget-package' {
|
|
$wingetId = $definition.wingetId
|
|
Ensure-WingetPackage -Id $wingetId -Label $Name
|
|
$toolSpec = Resolve-ReverseToolSpec -Name $Name
|
|
if (-not $toolSpec.Available) {
|
|
throw "$Name was installed, but bootstrap could not resolve the executable in this process."
|
|
}
|
|
return $toolSpec
|
|
}
|
|
'npm-mcp' {
|
|
Ensure-NodeRuntime
|
|
$envMap = @{}
|
|
if ($definition.PSObject.Properties['mcpEnv'] -and $null -ne $definition.mcpEnv) {
|
|
foreach ($property in $definition.mcpEnv.PSObject.Properties) {
|
|
$envMap[$property.Name] = $property.Value
|
|
}
|
|
}
|
|
$serverDefinition = Get-McpCommandServerDefinition -Command $definition.mcpCommand -Arguments @($definition.mcpArgs) -Env $envMap
|
|
Ensure-McpServer -ServerName $definition.mcpNames[0] -ServerDefinition $serverDefinition
|
|
return $true
|
|
}
|
|
'npm-global' {
|
|
Ensure-NodeRuntime
|
|
$npm = Get-NodeCommandPath -Name 'npm'
|
|
if ([string]::IsNullOrWhiteSpace($npm)) {
|
|
throw 'npm is not available after Node.js installation.'
|
|
}
|
|
& $npm install -g $definition.npmPackage
|
|
if ($LASTEXITCODE -ne 0) {
|
|
throw "npm install -g $($definition.npmPackage) failed."
|
|
}
|
|
# Run post-install command if specified (e.g. playwright install)
|
|
if ($definition.PSObject.Properties['postInstall'] -and -not [string]::IsNullOrWhiteSpace($definition.postInstall)) {
|
|
$postParts = $definition.postInstall -split ' ', 2
|
|
$postCmd = Get-FirstCommandPath -Names @($postParts[0])
|
|
if (-not [string]::IsNullOrWhiteSpace($postCmd)) {
|
|
if ($postParts.Count -gt 1) {
|
|
& $postCmd $postParts[1].Split(' ')
|
|
}
|
|
else {
|
|
& $postCmd
|
|
}
|
|
}
|
|
else {
|
|
# Try via npx
|
|
$npx = Get-NodeCommandPath -Name 'npx'
|
|
if ($npx) {
|
|
& $npx $definition.postInstall.Split(' ')
|
|
}
|
|
}
|
|
}
|
|
# Run setup script if specified
|
|
if ($definition.PSObject.Properties['setupScript'] -and -not [string]::IsNullOrWhiteSpace($definition.setupScript)) {
|
|
$setupPath = $definition.setupScript
|
|
if (Test-Path -LiteralPath $setupPath) {
|
|
$shell = Get-PreferredPowerShellPath
|
|
& $shell -NoProfile -ExecutionPolicy Bypass -File $setupPath -SkipBrowserInstall
|
|
}
|
|
}
|
|
return $true
|
|
}
|
|
'local-http-mcp' {
|
|
if ($Name -eq 'anything-analyzer') {
|
|
$authToken = Ensure-AnythingAnalyzerMcpConfig -Port ([int]$definition.servicePort)
|
|
$env:ANYTHING_ANALYZER_MCP_TOKEN = $authToken
|
|
try {
|
|
[Environment]::SetEnvironmentVariable('ANYTHING_ANALYZER_MCP_TOKEN', $authToken, 'User')
|
|
}
|
|
catch {
|
|
Write-Warning "Could not persist ANYTHING_ANALYZER_MCP_TOKEN for future MCP clients: $($_.Exception.Message)"
|
|
}
|
|
$serverDefinition = @{
|
|
url = $definition.mcpUrl
|
|
headers = @{ Authorization = "Bearer $authToken" }
|
|
bearer_token_env_var = 'ANYTHING_ANALYZER_MCP_TOKEN'
|
|
}
|
|
Ensure-McpServer -ServerName 'anything-analyzer' -ServerDefinition $serverDefinition
|
|
if ($StartServices) {
|
|
Start-AnythingAnalyzerService -Definition $definition -AuthToken $authToken
|
|
}
|
|
return $true
|
|
}
|
|
if ($Name -eq 'idapro') {
|
|
Ensure-Capability -Name 'idalib-mcp'
|
|
Ensure-McpServer -ServerName 'idapro' -ServerDefinition @{ url = $definition.mcpUrl }
|
|
if ($StartServices) {
|
|
Start-IdaProService -Definition $definition
|
|
}
|
|
return $true
|
|
}
|
|
}
|
|
'go-install' {
|
|
if (-not $definition.PSObject.Properties['goPackage'] -or [string]::IsNullOrWhiteSpace($definition.goPackage)) {
|
|
throw "go-install capability $Name is missing goPackage in bootstrap-manifest.json."
|
|
}
|
|
$goCmd = Get-FirstCommandPath -Names @('go')
|
|
if ([string]::IsNullOrWhiteSpace($goCmd)) {
|
|
try {
|
|
Ensure-WingetPackage -Id 'GoLang.Go' -Label 'Go'
|
|
}
|
|
catch {
|
|
Write-Warning "Go runtime unavailable: $($_.Exception.Message)"
|
|
}
|
|
$goCmd = Get-FirstCommandPath -Names @('go')
|
|
if ([string]::IsNullOrWhiteSpace($goCmd)) {
|
|
$goCandidates = @(
|
|
(Join-Path $env:LOCALAPPDATA 'Programs\Go\bin\go.exe'),
|
|
'C:\Program Files\Go\bin\go.exe',
|
|
(Join-Path $env:USERPROFILE 'go\bin\go.exe')
|
|
) | Where-Object { Test-Path -LiteralPath $_ } | Select-Object -First 1
|
|
if ($goCandidates) { $goCmd = $goCandidates }
|
|
}
|
|
}
|
|
if (-not [string]::IsNullOrWhiteSpace($goCmd)) {
|
|
& $goCmd install $definition.goPackage
|
|
if ($LASTEXITCODE -eq 0) {
|
|
if ($definition.PSObject.Properties['mcpNames'] -and -not [string]::IsNullOrWhiteSpace($definition.mcpCommand)) {
|
|
$serverDefinition = Get-McpCommandServerDefinition -Command $definition.mcpCommand -Arguments @($definition.mcpArgs)
|
|
Ensure-McpServer -ServerName $definition.mcpNames[0] -ServerDefinition $serverDefinition
|
|
}
|
|
return $true
|
|
}
|
|
Write-Warning "go install failed for $Name (exit code $LASTEXITCODE)."
|
|
}
|
|
# Docker fallback (parity with bootstrap-reverse.sh ensure_pentestswarm)
|
|
if ($definition.PSObject.Properties['fallbackKind'] -and $definition.fallbackKind -eq 'docker-image' -and $definition.PSObject.Properties['dockerImage']) {
|
|
$docker = Get-FirstCommandPath -Names @('docker')
|
|
if (-not [string]::IsNullOrWhiteSpace($docker)) {
|
|
$serverDefinition = @{
|
|
type = 'stdio'
|
|
command = 'docker'
|
|
args = @('run', '--rm', '-i', $definition.dockerImage) + @($definition.mcpArgs)
|
|
}
|
|
Ensure-McpServer -ServerName $definition.mcpNames[0] -ServerDefinition $serverDefinition
|
|
Write-Warning "$Name Go install failed; registered Docker fallback $($definition.dockerImage)"
|
|
return $true
|
|
}
|
|
}
|
|
throw "Capability $Name requires Go 1.24+ or Docker to install. Docs: $($definition.docsUrl)"
|
|
}
|
|
default {
|
|
throw "Unsupported bootstrap kind: $($definition.bootstrapKind)"
|
|
}
|
|
}
|
|
|
|
throw "Capability bootstrap fell through without action: $Name"
|
|
}
|
|
|
|
function Expand-CapabilityDependencies {
|
|
param([Parameter(Mandatory = $true)][string[]]$Names)
|
|
|
|
$ordered = New-Object System.Collections.Generic.List[string]
|
|
$seen = @{}
|
|
|
|
function Add-Capability {
|
|
param([string]$CapabilityName)
|
|
|
|
if ($seen.ContainsKey($CapabilityName)) {
|
|
return
|
|
}
|
|
$seen[$CapabilityName] = $true
|
|
|
|
$definition = Get-ReverseBootstrapDefinition -Name $CapabilityName
|
|
if ($null -ne $definition -and $definition.PSObject.Properties['dependsOn']) {
|
|
foreach ($dependency in @($definition.dependsOn)) {
|
|
Add-Capability -CapabilityName $dependency
|
|
}
|
|
}
|
|
|
|
$ordered.Add($CapabilityName)
|
|
}
|
|
|
|
foreach ($name in $Names) {
|
|
Add-Capability -CapabilityName $name
|
|
}
|
|
|
|
return $ordered
|
|
}
|
|
|
|
function Test-BootstrapResultsSucceeded {
|
|
param([Parameter(Mandatory = $true)][object[]]$Results)
|
|
|
|
return (@($Results | Where-Object { $_.status -eq 'failed' }).Count -eq 0)
|
|
}
|
|
|
|
$expandedCapabilities = Expand-CapabilityDependencies -Names $Capability
|
|
$results = @()
|
|
|
|
foreach ($name in $expandedCapabilities) {
|
|
$definition = Get-ReverseBootstrapDefinition -Name $name
|
|
if ($null -eq $definition) {
|
|
$results += [pscustomobject]@{ name = $name; status = 'missing-definition' }
|
|
continue
|
|
}
|
|
|
|
try {
|
|
$manualRequired = $false
|
|
switch ($name) {
|
|
'adb' {
|
|
Ensure-AndroidPlatformTools | Out-Null
|
|
}
|
|
default {
|
|
$ensureResult = Ensure-Capability -Name $name
|
|
if ($ensureResult -eq $false) {
|
|
$def = Get-ReverseBootstrapDefinition -Name $name
|
|
$hint = if ($def -and $def.PSObject.Properties['manualInstallHint']) { $def.manualInstallHint } else { "Install manually. Docs: $($def.docsUrl)" }
|
|
$results += [pscustomobject]@{
|
|
name = $name
|
|
status = 'manual-required'
|
|
hint = $hint
|
|
docs_url = [string]$def.docsUrl
|
|
}
|
|
$manualRequired = $true
|
|
}
|
|
}
|
|
}
|
|
|
|
if (-not $manualRequired) {
|
|
$state = Get-ReverseCapabilityState -Name $name
|
|
$status = if ($state -and -not $state.Ready) { 'configured-not-ready' } else { 'ready' }
|
|
$results += [pscustomobject]@{
|
|
name = $name
|
|
status = $status
|
|
ready = if ($state) { $state.Ready } else { $null }
|
|
registered = if ($state) { $state.Registered } else { $null }
|
|
service_online = if ($state) { $state.ServiceOnline } else { $null }
|
|
}
|
|
}
|
|
}
|
|
catch {
|
|
$results += [pscustomobject]@{
|
|
name = $name
|
|
status = 'failed'
|
|
error = $_.Exception.Message
|
|
}
|
|
}
|
|
}
|
|
|
|
if (-not $SkipRefresh) {
|
|
$shell = Get-PreferredPowerShellPath
|
|
& $shell -NoProfile -ExecutionPolicy Bypass -File (Join-Path $PSScriptRoot 'refresh-tool-index.ps1') | Out-Null
|
|
}
|
|
|
|
$results | ConvertTo-Json -Depth 5
|
|
if (-not (Test-BootstrapResultsSucceeded -Results $results)) {
|
|
exit 1
|
|
}
|