From fb138d91ca34c3e2e49ce67f3187da6feeedbbdd Mon Sep 17 00:00:00 2001 From: teknium1 <127238744+teknium1@users.noreply.github.com> Date: Sat, 16 May 2026 22:54:56 -0700 Subject: [PATCH] fix(install.ps1): Stage-Node honest reporting + reject empty -Stage Two protocol-correctness gaps from review: 1. Stage-Node used [void](Test-Node) which discarded Test-Node's return value, so the JSON frame always reported ok=true even when Node install fully failed. A GUI driver consuming the manifest couldn't tell 'node ready' from 'node missing'. Wire a soft-skip channel ($script:_StageSkippedReason) that workers can populate to surface 'ran, but the thing it was supposed to set up is not available' as skipped=true with a reason in the JSON, without aborting the install (Node is optional -- browser tools degrade gracefully, matches Write-Completion's existing 'Note: Node.js could not be installed' behavior). Reset before each stage so a prior reason can't leak. 2. The -Stage dispatch used 'if ($Stage)' which is falsy for empty string, so 'install.ps1 -Stage ""' fell through to Main and silently kicked off a full destructive install. Switch to PSBoundParameters.ContainsKey('Stage') so an explicit empty value surfaces as unknown-stage exit 2 with a structured JSON frame, the way every other bad stage name does. --- scripts/install.ps1 | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/scripts/install.ps1 b/scripts/install.ps1 index b23ac54f73..c774e9a860 100644 --- a/scripts/install.ps1 +++ b/scripts/install.ps1 @@ -1926,7 +1926,17 @@ $InstallStages = @( function Stage-Uv { if (-not (Install-Uv)) { throw "uv installation failed" } } function Stage-Python { Resolve-UvCmd; if (-not (Test-Python)) { throw "Python $PythonVersion not available" } } function Stage-Git { if (-not (Install-Git)) { throw "Git not available and auto-install failed -- install from https://git-scm.com/download/win then re-run" } } -function Stage-Node { [void](Test-Node) } +# Node is optional (browser tools degrade gracefully without it). Surface +# failure to the JSON contract as skipped=true / reason rather than ok=true, +# so a GUI driver consuming the manifest can distinguish "node ready" from +# "node missing". Install flow continues either way -- matches the +# existing Write-Completion behavior that prints a "Note: Node.js could +# not be installed" hint instead of aborting. +function Stage-Node { + if (-not (Test-Node)) { + $script:_StageSkippedReason = "Node.js not available; browser tools will be unavailable until node is installed manually from https://nodejs.org/en/download/" + } +} function Stage-SystemPackages { Install-SystemPackages } function Stage-Repository { Install-Repository } function Stage-Venv { Resolve-UvCmd; Install-Venv } @@ -1975,6 +1985,15 @@ function Invoke-Stage { # foreach pass; cross-process drivers get the necessary freshening). Sync-EnvPath + # Per-stage soft-skip channel. A worker can populate + # $script:_StageSkippedReason to surface "ran, but the thing it was + # supposed to set up is not available" as skipped=true in the JSON + # frame, without throwing. Used by Stage-Node so the install flow + # doesn't abort when an optional capability is missing while still + # being honest in the protocol contract. Reset before each stage so + # a prior stage's reason can never leak into a later stage's frame. + $script:_StageSkippedReason = $null + $start = [DateTime]::UtcNow $result = @{ stage = $StageDef.Name @@ -1987,6 +2006,10 @@ function Invoke-Stage { try { & $StageDef.Worker $result.ok = $true + if ($script:_StageSkippedReason) { + $result.skipped = $true + $result.reason = $script:_StageSkippedReason + } } catch { $result.ok = $false $result.reason = "$_" @@ -2060,7 +2083,12 @@ try { exit 0 } - if ($Stage) { + # Use PSBoundParameters rather than $Stage truthiness so that an + # explicit `-Stage ""` from a misbehaving driver doesn't fall through + # to the full-install Main path and silently kick off a destructive + # operation. Empty string is a contract violation; surface it as + # unknown-stage exit 2 with a structured JSON frame. + if ($PSBoundParameters.ContainsKey("Stage")) { $def = Get-InstallStage -Name $Stage if (-not $def) { $err = @{