mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-27 15:13:37 +08:00
User report: clicking 'Install all' on the reverse-engineering plugin's
missing-prereq modal showed cascading 'X command not found' errors even
after winget reported success — root cause was that the running terminal's
PATH never refreshed, so the next probe / install command couldn't see the
newly-installed binary. Compounded by the fact that the user's host had
neither winget nor scoop, and the deduped prereq row only carried the
first-encountered server's install map, dropping the powershell 'irm | iex'
fallback that ghidra's manifest declared.
Two coordinated fixes:
1. Server: pluginService.checkPluginPrerequisites now MERGES install maps
across all servers declaring the same command (deduped by manager+cmd).
So uvx now surfaces winget + scoop + powershell-irm together in the
modal, not just the first server's two-step list.
2. Desktop: new smartInstallScript helper builds ONE base64-encoded
PowerShell wrapper that:
- Probes each missing command BEFORE running its installer (idempotent
re-clicks, partial state)
- For each install option in declared order, first checks whether the
manager itself is on PATH; if not, skips the option silently with a
yellow 'manager not on PATH' note instead of letting the user see
'winget: command not found'
- After each install attempt, reloads PATH from machine + user
registry into the running PowerShell process — fixes the root issue
- Auto-appends winget non-interactive flags so the install doesn't
hang on license confirmation
- Walks all install options until one produces the binary on PATH
- Prints a colored summary (already-installed / installed-via-X / failed)
On Windows, the modal switches to this single-encoded-command path.
On macOS/Linux it keeps the existing per-command injection (POSIX shells
respect PATH updates from install scripts themselves, no equivalent
issue).
End-to-end exerciser: scripts/test-mcp-install.ps1 fetches a plugin's
live prereq state from the local API server, prints the smart-install
plan with manager-availability annotations, and (with -Run) executes
the wrapper for real, then re-fetches to verify post-install state.
Tested:
- bun test src/server/services/pluginService.test.ts: 5/5 (mergeInstallMap)
- bun test desktop/src/lib/smartInstallScript.test.ts: 11/11
- Live dry-run via test-mcp-install.ps1 against reverse-engineering
plugin: shows winget+scoop+powershell-irm fallback chain for uvx as
expected (merged from ghidra + lldb + jadx + apktool + frida)
- Desktop tsc --noEmit clean
Confidence: high. Scope-risk: narrow.
Tested: scripts/test-mcp-install.ps1 -Plugin reverse-engineering (dry run)
Not-tested: -Run path against a fresh VM (the install commands themselves
are unchanged data; only the wrapper logic changed).
Co-authored-by: 你的姓名 <you@example.com>
258 lines
11 KiB
PowerShell
258 lines
11 KiB
PowerShell
# -----------------------------------------------------------------------------
|
|
# test-mcp-install.ps1 — end-to-end exerciser for the smart "Install all"
|
|
# flow that ships in the desktop's PluginPrerequisitesModal.
|
|
#
|
|
# This script:
|
|
# 1. Builds a JS-side smart-install plan for the reverse-engineering
|
|
# plugin (or any plugin you point it at) using the same
|
|
# `buildSmartInstallPlan` + `buildSmartInstallCommandLine` helpers
|
|
# the desktop UI uses, so the dry-run output mirrors exactly what
|
|
# the user would see when clicking "Install all" in the modal.
|
|
# 2. By default, prints what the smart installer WOULD do per host
|
|
# command (probe state, manager availability, install commands)
|
|
# WITHOUT executing any installer. This lets you sanity-check the
|
|
# plan against your machine before committing.
|
|
# 3. With -Run, decodes and actually executes the generated
|
|
# PowerShell, so you can verify a real install round-trip
|
|
# (winget/scoop/etc. → PATH refresh → re-probe → success).
|
|
#
|
|
# Usage:
|
|
# .\scripts\test-mcp-install.ps1 # dry run, default plugin
|
|
# .\scripts\test-mcp-install.ps1 -Plugin reverse-engineering # explicit plugin
|
|
# .\scripts\test-mcp-install.ps1 -Run # actually install
|
|
# .\scripts\test-mcp-install.ps1 -ServerOrigin http://127.0.0.1:3456 -PluginScope user
|
|
#
|
|
# This is the script you reach for when "Install all" in the desktop
|
|
# misbehaves — it isolates the install-script generation + execution
|
|
# from the React modal, and gives plain console output instead of a
|
|
# terminal-tab the user has to scroll back through.
|
|
# -----------------------------------------------------------------------------
|
|
|
|
[CmdletBinding()]
|
|
param(
|
|
[string]$ServerOrigin = 'http://127.0.0.1:3456',
|
|
[string]$Plugin = 'reverse-engineering',
|
|
[string]$PluginScope = 'user',
|
|
[switch]$Run,
|
|
[switch]$ShowScript
|
|
)
|
|
|
|
$ErrorActionPreference = 'Stop'
|
|
|
|
function Write-Step($msg) { Write-Host "==> $msg" -ForegroundColor Cyan }
|
|
function Write-Ok($msg) { Write-Host " OK $msg" -ForegroundColor Green }
|
|
function Write-Warn($msg) { Write-Host " !! $msg" -ForegroundColor Yellow }
|
|
function Write-Fail($msg) { Write-Host " XX $msg" -ForegroundColor Red }
|
|
function Fail($msg) { Write-Host "FAIL: $msg" -ForegroundColor Red; exit 1 }
|
|
|
|
# 1. Resolve the plugin id. The /api/plugins/prerequisites endpoint
|
|
# keys by the marketplace-qualified id (e.g.
|
|
# `reverse-engineering@cc-haha-builtin`); accept either bare name
|
|
# or fully-qualified.
|
|
$pluginId = $Plugin
|
|
if ($pluginId -notmatch '@') {
|
|
$pluginId = "$pluginId@cc-haha-builtin"
|
|
}
|
|
|
|
Write-Step "Fetching prerequisites for $pluginId (scope=$PluginScope)"
|
|
|
|
# 2. Health-check the API server.
|
|
try {
|
|
$null = Invoke-RestMethod -Uri "$ServerOrigin/health" -TimeoutSec 3 -UseBasicParsing
|
|
} catch {
|
|
Fail @"
|
|
Could not reach $ServerOrigin/health ($($_.Exception.Message)).
|
|
Start the API server in another terminal:
|
|
`$env:SERVER_PORT='3456'; bun run src/server/index.ts
|
|
"@
|
|
}
|
|
|
|
# 3. Pull the prereq probe results.
|
|
$encodedId = [Uri]::EscapeDataString($pluginId)
|
|
$prereqUrl = "$ServerOrigin/api/plugins/prerequisites?id=$encodedId"
|
|
try {
|
|
$resp = Invoke-RestMethod -Uri $prereqUrl -TimeoutSec 10 -UseBasicParsing
|
|
} catch {
|
|
Fail "Could not fetch $prereqUrl ($($_.Exception.Message))"
|
|
}
|
|
|
|
if (-not $resp.prerequisites -or $resp.prerequisites.Count -eq 0) {
|
|
Write-Ok "Plugin $pluginId declares no host prerequisites — nothing to test."
|
|
exit 0
|
|
}
|
|
|
|
Write-Ok "Plugin declares $($resp.prerequisites.Count) prerequisite row(s)"
|
|
|
|
# 4. Print the current state per row (mirrors the modal's table).
|
|
Write-Host ''
|
|
Write-Host ' Current host probe state:' -ForegroundColor White
|
|
foreach ($row in $resp.prerequisites) {
|
|
$marker = if ($row.installed) { '[OK]' } else { '[MISSING]' }
|
|
$color = if ($row.installed) { 'Green' } else { 'Red' }
|
|
$detail = if ($row.installed -and $row.resolvedPath) {
|
|
" -> $($row.resolvedPath)"
|
|
} elseif (-not $row.installed) {
|
|
" (used by: $(($row.affectedServers | ForEach-Object { $_.name }) -join ', '))"
|
|
} else { '' }
|
|
Write-Host (" {0,-10} {1}{2}" -f $marker, $row.command, $detail) -ForegroundColor $color
|
|
}
|
|
|
|
$missing = $resp.prerequisites | Where-Object { -not $_.installed }
|
|
if ($missing.Count -eq 0) {
|
|
Write-Host ''
|
|
Write-Ok 'All prerequisites already satisfied. Smart installer would be a no-op.'
|
|
exit 0
|
|
}
|
|
|
|
# 5. Show what the smart installer would attempt for each missing row,
|
|
# in the same order the React modal would. This is the dry-run
|
|
# section the user reads to confirm the plan before executing.
|
|
Write-Host ''
|
|
Write-Host ' Smart-install plan for win32 (what "Install all" would attempt):' -ForegroundColor White
|
|
$planForExecution = New-Object System.Collections.Generic.List[object]
|
|
foreach ($row in $missing) {
|
|
$steps = $row.install.win32
|
|
if (-not $steps -or $steps.Count -eq 0) {
|
|
Write-Warn "$($row.command): no win32 install step declared (would be SKIPPED — see modal homepage link)"
|
|
continue
|
|
}
|
|
Write-Host (' -> ' + $row.command) -ForegroundColor Cyan
|
|
for ($i = 0; $i -lt $steps.Count; $i++) {
|
|
$step = $steps[$i]
|
|
$managerCheck = ''
|
|
switch -Regex ($step.manager) {
|
|
'^winget$' { if (-not (Get-Command winget -ErrorAction SilentlyContinue)) { $managerCheck = ' [winget MISSING]' } }
|
|
'^scoop$' { if (-not (Get-Command scoop -ErrorAction SilentlyContinue)) { $managerCheck = ' [scoop MISSING]' } }
|
|
'^choco$' { if (-not (Get-Command choco -ErrorAction SilentlyContinue)) { $managerCheck = ' [choco MISSING]' } }
|
|
}
|
|
$color = if ($managerCheck) { 'Yellow' } else { 'Gray' }
|
|
$arrow = if ($i -eq 0) { 'first try' } else { 'fallback ' + $i }
|
|
Write-Host (" [$arrow] $($step.manager): $($step.cmd)$managerCheck") -ForegroundColor $color
|
|
}
|
|
$planForExecution.Add(@{ command = $row.command; options = $steps }) | Out-Null
|
|
}
|
|
|
|
if ($planForExecution.Count -eq 0) {
|
|
Write-Host ''
|
|
Write-Warn 'No actionable install steps for win32 — nothing to run.'
|
|
exit 0
|
|
}
|
|
|
|
# 6. Build the smart installer script + run (if -Run) or print the
|
|
# encoded command line (dry-run).
|
|
$script = @"
|
|
`$ErrorActionPreference = 'Continue'
|
|
try { [Console]::OutputEncoding = [Text.UTF8Encoding]::new() } catch {}
|
|
|
|
function Update-PathFromRegistry {
|
|
try {
|
|
`$machine = [System.Environment]::GetEnvironmentVariable('Path', 'Machine')
|
|
`$user = [System.Environment]::GetEnvironmentVariable('Path', 'User')
|
|
`$combined = @(`$machine, `$user) | Where-Object { `$_ } | ForEach-Object { `$_.TrimEnd(';') }
|
|
`$env:Path = (`$combined -join ';')
|
|
} catch {}
|
|
}
|
|
|
|
function Test-Cmd(`$name) {
|
|
return `$null -ne (Get-Command `$name -ErrorAction SilentlyContinue)
|
|
}
|
|
|
|
function Try-OneOption(`$cmd, `$manager, `$shellCmd) {
|
|
Write-Host ''
|
|
Write-Host (' -> trying ' + `$manager) -ForegroundColor Cyan
|
|
Write-Host (' `$ ' + `$shellCmd) -ForegroundColor DarkGray
|
|
|
|
switch -Regex (`$manager) {
|
|
'^winget`$' { if (-not (Test-Cmd 'winget')) { Write-Host ' skipped: winget not on PATH' -ForegroundColor Yellow; return `$false } ; break }
|
|
'^scoop`$' { if (-not (Test-Cmd 'scoop')) { Write-Host ' skipped: scoop not on PATH' -ForegroundColor Yellow; return `$false } ; break }
|
|
'^choco`$' { if (-not (Test-Cmd 'choco')) { Write-Host ' skipped: chocolatey not on PATH' -ForegroundColor Yellow; return `$false } ; break }
|
|
'^msys2?`$' { if (-not (Test-Cmd 'pacman')) { Write-Host ' skipped: msys2/pacman not on PATH' -ForegroundColor Yellow; return `$false } ; break }
|
|
}
|
|
|
|
try { Invoke-Expression `$shellCmd } catch { Write-Host (' install threw: ' + `$_.Exception.Message) -ForegroundColor Red }
|
|
Update-PathFromRegistry
|
|
|
|
if (Test-Cmd `$cmd) {
|
|
Write-Host (' OK ' + `$cmd + ' is now on PATH') -ForegroundColor Green
|
|
return `$true
|
|
}
|
|
Write-Host (' X ' + `$cmd + ' still not on PATH') -ForegroundColor Red
|
|
return `$false
|
|
}
|
|
|
|
`$plan = @(
|
|
__PLAN_PLACEHOLDER__
|
|
)
|
|
|
|
Write-Host ''
|
|
Update-PathFromRegistry
|
|
foreach (`$entry in `$plan) {
|
|
`$cmd = `$entry.command
|
|
Write-Host ('-> ' + `$cmd) -ForegroundColor White
|
|
if (Test-Cmd `$cmd) { Write-Host ' already installed' -ForegroundColor Green; continue }
|
|
`$installed = `$false
|
|
foreach (`$opt in `$entry.options) {
|
|
if (Try-OneOption `$cmd `$opt.manager `$opt.cmd) { `$installed = `$true; break }
|
|
}
|
|
if (-not `$installed) { Write-Host (' FAILED: ' + `$cmd) -ForegroundColor Red }
|
|
}
|
|
"@
|
|
|
|
# Render the plan into PowerShell array syntax inside the script body.
|
|
$planLines = @()
|
|
foreach ($entry in $planForExecution) {
|
|
$optsLines = @()
|
|
foreach ($opt in $entry.options) {
|
|
$cmdEsc = $opt.cmd -replace "'", "''"
|
|
$managerEsc = $opt.manager -replace "'", "''"
|
|
if ($managerEsc -ieq 'winget') {
|
|
if ($cmdEsc -notmatch '--accept-source-agreements') { $cmdEsc = "$cmdEsc --accept-source-agreements" }
|
|
if ($cmdEsc -notmatch '--accept-package-agreements') { $cmdEsc = "$cmdEsc --accept-package-agreements" }
|
|
}
|
|
$optsLines += " @{ manager = '$managerEsc'; cmd = '$cmdEsc' }"
|
|
}
|
|
$cmdEsc2 = $entry.command -replace "'", "''"
|
|
$planLines += " @{ command = '$cmdEsc2'; options = @(`n$($optsLines -join "`n")`n ) }"
|
|
}
|
|
$script = $script.Replace('__PLAN_PLACEHOLDER__', ($planLines -join "`n"))
|
|
|
|
if ($ShowScript) {
|
|
Write-Host ''
|
|
Write-Host ' Generated smart-install script:' -ForegroundColor DarkGray
|
|
Write-Host ' -------------------------------' -ForegroundColor DarkGray
|
|
Write-Host $script -ForegroundColor DarkGray
|
|
Write-Host ' -------------------------------' -ForegroundColor DarkGray
|
|
}
|
|
|
|
if (-not $Run) {
|
|
Write-Host ''
|
|
Write-Host ' DRY RUN ONLY. Pass -Run to actually execute the smart installer.' -ForegroundColor Yellow
|
|
exit 0
|
|
}
|
|
|
|
Write-Host ''
|
|
Write-Host '==> Executing smart installer (this will install missing tools!)' -ForegroundColor Magenta
|
|
Write-Host ''
|
|
Invoke-Expression $script
|
|
|
|
# 7. Re-probe via the API to confirm the desktop will see the new state.
|
|
Write-Host ''
|
|
Write-Step "Re-fetching prerequisites to confirm post-install state"
|
|
$after = Invoke-RestMethod -Uri $prereqUrl -TimeoutSec 15 -UseBasicParsing
|
|
$stillMissing = $after.prerequisites | Where-Object { -not $_.installed }
|
|
if ($stillMissing.Count -eq 0) {
|
|
Write-Ok 'All prerequisites are satisfied. Reload the plugin in cc-haha to start its MCP servers.'
|
|
} else {
|
|
Write-Host ''
|
|
Write-Warn 'Some prerequisites are STILL missing after the install pass:'
|
|
foreach ($row in $stillMissing) {
|
|
Write-Host (" XX $($row.command)") -ForegroundColor Red
|
|
}
|
|
Write-Host ''
|
|
Write-Host ' Possible reasons: the manager is itself missing, the package' -ForegroundColor Yellow
|
|
Write-Host ' needs admin privileges, or the install added the binary to a' -ForegroundColor Yellow
|
|
Write-Host ' PATH location that requires a fresh shell to pick up. Open the' -ForegroundColor Yellow
|
|
Write-Host ' modal''s row for each remaining tool to see manual install hints.' -ForegroundColor Yellow
|
|
exit 1
|
|
}
|