fix(install): remove upstream stage-system splice that shadowed pm-era stages

The upstream merge concatenated upstream's owner-era stage system
(InstallStages array + Stage-Uv/Git/Python/SystemPackages wrappers +
Get-InstallStage/Step-OutOfInstallDir/Invoke-Stage) onto the pm-era
installer's stage functions. PowerShell last-def-wins meant the orphaned
wrappers (e.g. 'function Stage-Repository { Install-Repository }' where
Install-Repository exists NOWHERE) shadowed the real pm-era bodies —
every -Stage invocation crashed with 'not recognized'. Deleted the
entire upstream splice (lines 986-1221), restored Stage-Desktop wrapper
(Install-DesktopVoiceDeps + Install-Desktop); file parses clean and
each stage fn has exactly one def.
This commit is contained in:
ethernet
2026-09-04 05:04:26 -04:00
parent 0ee0bd903e
commit dd68009673

View File

@@ -304,6 +304,11 @@ function Stage-Gateway {
& (Join-Path $InstallDir "venv\Scripts\python.exe") (Join-Path $InstallDir "hermes") gateway install
}
function Stage-Desktop {
Install-DesktopVoiceDeps
Install-Desktop
}
function Stage-Complete {
$commit = $Commit
if (-not $commit) { $commit = git -C $InstallDir rev-parse HEAD 2>$null }
@@ -983,242 +988,6 @@ function Write-Completion {
}
}
# ============================================================================
# Stage protocol
# ============================================================================
#
# install.ps1 supports a small, stable "stage protocol" that lets programmatic
# callers (the desktop GUI's onboarding wizard, CI, future install.sh, etc.)
# drive the install one step at a time and surface progress/errors with their
# own UI. CLI users running the canonical `irm | iex` one-liner never
# encounter this -- default invocation behaves exactly as before.
#
# Entry points:
#
# install.ps1 Interactive install (today's behavior).
# install.ps1 -ProtocolVersion Emit the protocol version integer.
# install.ps1 -Manifest Emit the stage manifest as JSON.
# install.ps1 -Stage <name> Run one stage and emit its result.
# install.ps1 -NonInteractive Disable all Read-Host prompts (also
# skips the setup wizard and the gateway
# autostart prompt). Can be combined
# with default invocation to do a full
# non-interactive install.
# install.ps1 -Json Emit machine-readable JSON instead of
# the human-readable success banner at
# the end of a full install.
#
# Manifest schema (the JSON returned by -Manifest):
#
# {
# "protocol_version": 1,
# "stages": [
# {
# "name": "uv",
# "title": "Installing uv package manager",
# "category": "prereqs",
# "needs_user_input": false
# },
# ...
# ]
# }
#
# Stage result (the JSON written by -Stage <name>):
#
# {
# "stage": "uv",
# "ok": true,
# "skipped": false,
# "reason": null,
# "duration_ms": 1234
# }
#
# Exit codes:
#
# 0 -- success (stage ran, or stage was deliberately skipped).
# 1 -- generic failure; the stage threw.
# 2 -- unknown stage name passed to -Stage.
#
# Adding a stage:
#
# 1. Append an entry to $InstallStages below.
# 2. Make sure the worker function it points at is idempotent and respects
# $NonInteractive when it has prompts. Add it before "configure"
# (the wizard) or "gateway" (autostart) if it should run unconditionally;
# after those if it's optional post-install glue.
# 3. Do NOT bump $InstallStageProtocolVersion -- adding stages is additive.
# Drivers iterate the manifest dynamically.
#
# ============================================================================
# Stage definitions -- the single source of truth. Each entry maps a stable
# stage name (the API contract drivers depend on) to the worker function that
# implements it. ``Title`` is what UIs show; ``Category`` lets UIs group
# stages; ``NeedsUserInput`` tells UIs "this stage prompts -- either skip it
# or arrange to provide answers another way."
$InstallStages = @(
@{ Name = "uv"; Title = "Installing uv package manager"; Category = "prereqs"; NeedsUserInput = $false; Worker = "Stage-Uv" }
@{ Name = "git"; Title = "Installing Git"; Category = "prereqs"; NeedsUserInput = $false; Worker = "Stage-Git" }
@{ Name = "node"; Title = "Detecting Node.js"; Category = "prereqs"; NeedsUserInput = $false; Worker = "Stage-Node" }
@{ Name = "system-packages"; Title = "Installing ripgrep and ffmpeg"; Category = "prereqs"; NeedsUserInput = $false; Worker = "Stage-SystemPackages" }
@{ Name = "repository"; Title = "Cloning Hermes repository"; Category = "install"; NeedsUserInput = $false; Worker = "Stage-Repository" }
# Managed Python lives under $InstallDir\.hermes-runtime, so the checkout
# must exist before this stage creates that directory. Otherwise the later
# repository stage treats the runtime-only directory as a broken checkout,
# parks it, and leaves Stage-Venv with no managed interpreter.
@{ Name = "python"; Title = "Verifying Python $PythonVersion"; Category = "prereqs"; NeedsUserInput = $false; Worker = "Stage-Python" }
@{ Name = "venv"; Title = "Creating Python virtual environment"; Category = "install"; NeedsUserInput = $false; Worker = "Stage-Venv" }
@{ Name = "dependencies"; Title = "Installing Python dependencies"; Category = "install"; NeedsUserInput = $false; Worker = "Stage-Dependencies" }
@{ Name = "node-deps"; Title = "Installing Node.js dependencies"; Category = "install"; NeedsUserInput = $false; Worker = "Stage-NodeDeps" }
)
if ($IncludeDesktop) {
# Insert AFTER node-deps so workspace npm is already installed when
# the desktop build runs. Inserted only when explicitly requested
# (Hermes-Setup.exe), never via the irm|iex CLI one-liner.
$InstallStages += @{ Name = "desktop"; Title = "Building desktop app"; Category = "install"; NeedsUserInput = $false; Worker = "Stage-Desktop" }
}
$InstallStages += @(
@{ Name = "path"; Title = "Adding Hermes to PATH"; Category = "finalize"; NeedsUserInput = $false; Worker = "Stage-Path" }
@{ Name = "config-templates"; Title = "Writing configuration templates"; Category = "finalize"; NeedsUserInput = $false; Worker = "Stage-ConfigTemplates" }
@{ Name = "platform-sdks"; Title = "Installing messaging platform SDKs"; Category = "finalize"; NeedsUserInput = $false; Worker = "Stage-PlatformSdks" }
@{ Name = "bootstrap-marker"; Title = "Marking install complete"; Category = "finalize"; NeedsUserInput = $false; Worker = "Stage-BootstrapMarker" }
# Interactive stages. In non-interactive mode these become no-ops; the
# caller (GUI / CI) handles the equivalent UX themselves.
@{ Name = "configure"; Title = "Configuring API keys and models"; Category = "post-install"; NeedsUserInput = $true; Worker = "Stage-Configure" }
@{ Name = "gateway"; Title = "Starting messaging gateway"; Category = "post-install"; NeedsUserInput = $true; Worker = "Stage-Gateway" }
)
# Stage workers -- thin wrappers that delegate to the existing Install-* /
# Test-* / Invoke-* functions while preserving their error semantics. Kept
# as a separate layer so the existing functions remain callable directly
# (helpful for one-off recovery: ``. install.ps1; Install-Venv``).
#
# Stages that depend on uv (anything after Stage-Uv) call Resolve-UvCmd
# first so they work in cross-process driver mode where $script:UvCmd
# set by Stage-Uv in a sibling powershell process is not visible here.
# Resolve-UvCmd is a fast no-op when $script:UvCmd is already populated
# (the default-invocation case where Main runs everything in one
# process), and throws cleanly if uv truly isn't installed yet.
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)) {
if ($script:GitInstallFailureReason) { throw $script:GitInstallFailureReason }
throw "Git not available and auto-install failed -- install from https://git-scm.com/download/win then re-run"
}
}
# 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 }
function Stage-Dependencies { Resolve-UvCmd; Install-Dependencies }
function Stage-NodeDeps { Install-NodeDeps }
function Stage-Desktop { Install-DesktopVoiceDeps; Install-Desktop }
function Stage-Path { Set-PathVariable }
function Stage-ConfigTemplates { Copy-ConfigTemplates }
function Stage-PlatformSdks { Resolve-UvCmd; Install-PlatformSdks }
function Stage-BootstrapMarker { Write-BootstrapMarker }
function Stage-Configure { Invoke-SetupWizard }
function Stage-Gateway { Start-GatewayIfConfigured }
function Get-InstallStage {
param([string]$Name)
foreach ($s in $InstallStages) {
if ($s.Name -eq $Name) { return $s }
}
return $null
}
function Step-OutOfInstallDir {
# Windows refuses to delete a directory any shell is currently cd'd
# inside -- and silently leaves orphan files behind, which then wedge
# "is this a valid git repo" probes on re-install. Harmless when the
# caller ran the installer from somewhere else.
try {
$currentResolved = (Get-Location).ProviderPath
$installResolved = $null
if (Test-Path $InstallDir) {
$installResolved = (Resolve-Path $InstallDir -ErrorAction SilentlyContinue).ProviderPath
}
if ($installResolved -and $currentResolved.ToLower().StartsWith($installResolved.ToLower())) {
Write-Info "Stepping out of $InstallDir so Windows can replace files there if needed..."
Set-Location $env:USERPROFILE
}
} catch {}
}
function Invoke-Stage {
param(
[Parameter(Mandatory=$true)] [hashtable]$StageDef
)
# Refresh PATH from registry so this stage sees binaries installed by
# prior stages, even when each stage runs in its own powershell process.
# No-op in cost-relevant cases (default invocation path syncs once per
# 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
ok = $false
skipped = $false
reason = $null
duration_ms = 0
}
try {
& $StageDef.Worker
$result.ok = $true
if ($script:_StageSkippedReason) {
$result.skipped = $true
$result.reason = $script:_StageSkippedReason
}
} catch {
$result.ok = $false
$result.reason = "$_"
throw
} finally {
$result.duration_ms = [int]([DateTime]::UtcNow - $start).TotalMilliseconds
if ($Json -or $Stage) {
# In stage-driver mode every stage emits a JSON line so the
# caller can stream progress. In default interactive mode we
# stay silent here (the worker already wrote human output).
$result | ConvertTo-Json -Compress | Write-Output
# Tell the entry-point catch that we've already emitted a
# frame for this failure (when $result.ok = $false), so it
# doesn't double-emit a second JSON object and break the
# one-line-per-stage contract the driver protocol promises.
if (-not $result.ok) {
$script:_StageEmittedErrorFrame = $true
}
}
$markerPath = Join-Path $InstallDir ".hermes-bootstrap-complete"
$tmp = "$markerPath.tmp"
$marker | ConvertTo-Json | Set-Content -Path $tmp -Encoding utf8
Move-Item -Force $tmp $markerPath
}
Log "install complete. Run: hermes"
}
function Invoke-StageByName([string]$name) {
switch ($name) {