# install.ps1 - SGT Fleet one-line installer (Windows PowerShell 5.1) # # usage (customer, one line): # irm https://sgtfleet.com/install.ps1 | iex # usage (private site download from a key file): # powershell -NoProfile -ExecutionPolicy Bypass -File install.ps1 -SgtKeyFile "$env:USERPROFILE\sgt-key.txt" # usage (local package - the hash is MANDATORY): # powershell -NoProfile -ExecutionPolicy Bypass -File install.ps1 -ZipPath "$env:USERPROFILE\Downloads\SGT-Fleet-2026.10.01-commercial-rc34-p039-skill-attached-preview.zip" -ExpectedSha256 <64 hex chars> # # what it does: resolves ONE rc34 package, verifies its SHA-256 BEFORE extraction, stages it under # %LOCALAPPDATA%\Programs\SGT-Fleet, preserves the previous app folder, runs `SGT-Fleet.exe setup --quiet`, # requires activation "active" with exit 0, then requires `SGT-Fleet.exe doctor` ok with exit 0. Only after # that core install passes does it attempt the updater layer (dispatcher, updater library, package record, # daily update task), which verifies its four pinned tool hashes. If the updater attempt fails, the script # warns and leaves the working core install untouched: the app is READY, and the user can rerun the installer # to retry updater setup - the retry can fail again. # It installs no hooks, forces no read-only profile and changes no Codex/Claude permissions; setup only writes # its own instruction block. Nothing is published anywhere. # # bounds: every write this script makes is inside %LOCALAPPDATA%\Programs\SGT-Fleet (the app folder, its # staging folder, its bin folder and its logs folder), the updater state %LOCALAPPDATA%\SGT-Fleet\update\state.json, # the user PATH entry for that install's bin folder and one per-user Scheduled Task (\SGT Fleet\SGT Fleet Update). # # verify: sgt doctor # rollback: sgt update --rollback [CmdletBinding()] param( [string]$ZipPath, [string]$ExpectedSha256, [string]$SgtKeyFile, [string]$OllamaKeyFile, [switch]$KeepStaging ) $ErrorActionPreference = 'Stop' $ProgressPreference = 'SilentlyContinue' [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 $Site = 'https://sgtfleet.com' $Root = Join-Path $env:LOCALAPPDATA 'Programs\SGT-Fleet' $App = Join-Path $Root 'app' $Bin = Join-Path $Root 'bin' $Stamp = Get-Date -Format 'yyyyMMdd-HHmmss' $Staging = Join-Path $Root ('staging-' + $Stamp) $LogDir = Join-Path $Root ('logs-' + $Stamp) $EmbeddedSha256 = 'b30839a3a3848725689fcc000f8820f37d4c3ce8e47afa36c483a93b44e49b63' $EmbeddedManifestSha256 = 'faae0ee18c4819a4b3d4ae7b34732bf7caccfd36a49047ca4af27580d790ed0d' $PackageFolder = 'SGT-Fleet-2026.10.01-commercial-rc34-p039-skill-attached-preview' $ToolSha256 = @{ 'sgt.ps1' = 'a0d949c01da09e4ffac6c434ee62ed65e7dae7d2c7486de3978db714a6ff6106' 'sgt-ota.ps1' = '1c3221812afdab1b81b563e3e8b295c1711ba9f371dbb48c8cf6ce201cad0868' 'sgt-ota-apply.ps1' = 'ab1baf0d7e4ed3d1fdd1704f4e86551e8e8d3f19bc2744d65e1bd90594a26615' 'sgt-ota-crypto.ps1' = '92da2b551a87b152b8c817c58a232153ec94def0254973ae372a4769efd269ba' } $script:OldApp = $null $script:Swapped = $false $script:KeyTemp = $null $script:CmdPath = $null $script:OldCmd = $null $script:CmdChanged = $false $script:OldUserPath = $null $script:PathChanged = $false function Write-Step([int]$Number, [string]$Text) { Write-Host ('[' + $Number + '/8] ' + $Text) } function Write-Note([string]$Text) { Write-Host (' ' + $Text) } # Nothing credential-shaped may reach the console or a log file. Redact first, truncate second. function Get-SafeText([string]$Text, [int]$MaxLength = 400) { if (-not $Text) { return '' } $t = [regex]::Replace($Text, '(?i)\b(sgt_[A-Za-z0-9_\-]{4,}|sk-[A-Za-z0-9_\-]{4,}|Bearer\s+[^\s"'']+)', '') $t = [regex]::Replace($t, '(?i)(authorization|api[-_]?key|token|secret|password)["'']?\s*[:=]\s*["'']?[^\s,;"]+', '$1=') if ($MaxLength -gt 0 -and $t.Length -gt $MaxLength) { $t = $t.Substring(0, $MaxLength) + '...' } return $t } # Accept a clean JSON document, or the first JSON object inside a noisy one. function ConvertFrom-JsonText([string]$Text) { if (-not $Text) { return $null } $t = $Text.Trim() if (-not $t) { return $null } try { return ($t | ConvertFrom-Json) } catch { } $i = $t.IndexOf('{') $j = $t.LastIndexOf('}') if ($i -ge 0 -and $j -gt $i) { try { return ($t.Substring($i, $j - $i + 1) | ConvertFrom-Json) } catch { } } return $null } function Write-NoBom([string]$Path, [string]$Text) { [IO.File]::WriteAllText($Path, $Text, (New-Object Text.UTF8Encoding($false))) } # The repair path: this PC already activated once, so its DPAPI-protected key is on disk. The entropy is the # release folder name (see CLIENT-CONTRACT.md). The key is returned to the caller and never printed. function Get-SavedSgtKey { try { $base = Join-Path $env:LOCALAPPDATA 'SGT-Fleet' if (-not (Test-Path -LiteralPath $base -PathType Container)) { return $null } $candidates = @(Get-ChildItem -LiteralPath $base -Directory -ErrorAction SilentlyContinue | ForEach-Object { $cred = Join-Path $_.FullName 'credential.bin' if (Test-Path -LiteralPath $cred -PathType Leaf) { Get-Item -LiteralPath $cred } } | Sort-Object LastWriteTime -Descending) if ($candidates.Count -eq 0) { return $null } Add-Type -AssemblyName System.Security $bytes = [IO.File]::ReadAllBytes($candidates[0].FullName) $entropy = [Text.Encoding]::UTF8.GetBytes($candidates[0].Directory.Name) $plain = [Security.Cryptography.ProtectedData]::Unprotect($bytes, $entropy, [Security.Cryptography.DataProtectionScope]::CurrentUser) $text = [Text.Encoding]::UTF8.GetString($plain).Trim() if ($text -match '^sgt_[A-Za-z0-9_-]{20,180}$') { return $text } return $null } catch { return $null } } function Test-PersonalizedZip([string]$Path) { Add-Type -AssemblyName System.IO.Compression.FileSystem $archive = [IO.Compression.ZipFile]::OpenRead($Path) try { $prefix = $PackageFolder + '/' $manifestName = $prefix + 'Config/FILE-MANIFEST.json' $entries = @{} foreach ($entry in $archive.Entries) { $name = [string]$entry.FullName if ($name.EndsWith('/')) { continue } if ($name.Contains('\') -or $name.Contains('../') -or $name.StartsWith('/') -or $name.Contains(':')) { throw ('unsafe ZIP entry: ' + $name) } if ($entries.ContainsKey($name)) { throw ('duplicate ZIP entry: ' + $name) } $entries[$name] = $entry } if (-not $entries.ContainsKey('activation.sgtclaim')) { throw 'personal download has no activation claim.' } if ($entries['activation.sgtclaim'].Length -gt 8192) { throw 'activation claim is too large.' } if (-not $entries.ContainsKey($manifestName)) { throw 'package file manifest is missing.' } $sha = [Security.Cryptography.SHA256]::Create() try { $stream = $entries[$manifestName].Open() try { $manifestHash = [BitConverter]::ToString($sha.ComputeHash($stream)).Replace('-', '').ToLowerInvariant() } finally { $stream.Dispose() } if ($manifestHash -ne $EmbeddedManifestSha256) { throw 'package file manifest hash does not match the pinned release.' } $reader = New-Object IO.StreamReader($entries[$manifestName].Open(), [Text.Encoding]::UTF8) try { $manifest = ($reader.ReadToEnd() | ConvertFrom-Json) } finally { $reader.Dispose() } if ($manifest.schema_version -ne 1) { throw 'unsupported package file manifest.' } $expected = @{} $expected[$manifestName] = $manifestHash foreach ($property in $manifest.files.PSObject.Properties) { $name = $prefix + [string]$property.Name if ($expected.ContainsKey($name)) { throw 'duplicate package manifest path.' } $expected[$name] = ([string]$property.Value.sha256).ToLowerInvariant() } if ($entries.Count -ne ($expected.Count + 1)) { throw 'personal ZIP has an unexpected file count.' } foreach ($name in $expected.Keys) { if (-not $entries.ContainsKey($name)) { throw ('package file missing: ' + $name) } $stream = $entries[$name].Open() try { $actualHash = [BitConverter]::ToString($sha.ComputeHash($stream)).Replace('-', '').ToLowerInvariant() } finally { $stream.Dispose() } if ($actualHash -ne $expected[$name]) { throw ('package file hash mismatch: ' + $name) } } return $true } finally { $sha.Dispose() } } finally { $archive.Dispose() } } # The updater layer: dispatcher, updater library, package record and the daily update task. A failure here is # reported as one warning by the caller and never undoes a working install. function Install-SgtUpdater { $tools = @('sgt.ps1', 'sgt-ota.ps1', 'sgt-ota-apply.ps1', 'sgt-ota-crypto.ps1') foreach ($name in $tools) { $dest = Join-Path $Bin $name Invoke-WebRequest -Uri ($Site + '/tools/' + $name) -OutFile $dest -UseBasicParsing $hash = (Get-FileHash -LiteralPath $dest -Algorithm SHA256).Hash.ToLowerInvariant() $want = ([string]$ToolSha256[$name]).Trim().ToLowerInvariant() if ($want -notmatch '^[0-9a-f]{64}$') { throw ('this installer build still carries the unsubstituted updater tool hash for ' + $name + ', so the updater layer cannot be verified. Use the released installer.') } if ($hash -ne $want) { throw ('the updater tool ' + $name + ' does not match the pinned hash (expected ' + $want + ', got ' + $hash + ').') } } Write-Note 'updater tools verified against the pinned hashes' $cmdText = "@echo off`r`n" + '"%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe" -NoProfile -ExecutionPolicy Bypass -File "%~dp0sgt.ps1" %*' + "`r`n" Write-NoBom (Join-Path $Bin 'sgt.cmd') $cmdText Write-Note 'sgt.cmd now runs the dispatcher' $packageId = $null try { $infoText = (Invoke-WebRequest -Uri ($Site + '/api/download/info') -UseBasicParsing).Content $info = ConvertFrom-JsonText ([string]$infoText) if ($info -and $info.release) { $packageId = [string]$info.release } } catch { $packageId = $null } if (-not $packageId) { $packageId = $PackageFolder } $packageJson = [ordered]@{ package_id = $packageId channel = 'stable' sha256 = $actual installed_at = [DateTime]::UtcNow.ToString('yyyy-MM-ddTHH:mm:ssZ') } Write-NoBom (Join-Path $App 'sgt-package.json') (($packageJson | ConvertTo-Json) + "`n") $stateDir = Join-Path $env:LOCALAPPDATA 'SGT-Fleet\update' $statePath = Join-Path $stateDir 'state.json' if (-not (Test-Path -LiteralPath $statePath -PathType Leaf)) { New-Item -ItemType Directory -Force -Path $stateDir | Out-Null $stateJson = [ordered]@{ channel = 'stable' auto = $true current = $packageId latest = $null last_check = $null last_result = $null staged = $null history = @() } Write-NoBom $statePath (($stateJson | ConvertTo-Json) + "`n") } Write-Note ('package record written for ' + $packageId) . (Join-Path $Bin 'sgt-ota.ps1') Register-SgtUpdateTask Write-Note 'daily update task registered' } try { Write-Step 1 'Checking Windows and PowerShell' if (-not $env:LOCALAPPDATA) { throw 'LOCALAPPDATA is not set; this installer needs a normal Windows user profile.' } if (-not [Environment]::Is64BitOperatingSystem) { throw 'SGT Fleet needs 64-bit Windows.' } if ($PSVersionTable.PSVersion.Major -lt 5) { throw 'SGT Fleet needs Windows PowerShell 5.1 or newer.' } Write-Note ('PowerShell ' + $PSVersionTable.PSVersion.ToString() + ' on ' + [Environment]::OSVersion.VersionString) Write-Step 2 'Resolving the rc34 package' $expected = $null if ($ExpectedSha256) { $expected = $ExpectedSha256.Trim().ToLowerInvariant() if ($expected -notmatch '^[0-9a-f]{64}$') { throw '-ExpectedSha256 must be exactly 64 hexadecimal characters.' } } $zip = $null $mode = $null $key = $null $keyPath = $null if ($ZipPath) { $mode = 'local' if (-not $expected) { throw '-ZipPath requires -ExpectedSha256: a local package is never installed unverified.' } if (-not (Test-Path -LiteralPath $ZipPath -PathType Leaf)) { throw ('the local package was not found: ' + $ZipPath) } $zip = (Resolve-Path -LiteralPath $ZipPath).ProviderPath Write-Note ('local package: ' + $zip) Write-Note 'local package supplied; the private site download is skipped' if ($SgtKeyFile) { if (-not (Test-Path -LiteralPath $SgtKeyFile -PathType Leaf)) { throw ('the SGT key file was not found: ' + $SgtKeyFile) } $keyPath = (Resolve-Path -LiteralPath $SgtKeyFile).ProviderPath Write-Note 'the SGT key file path will be handed to setup (its contents are never read or printed here)' } } else { $mode = 'site' if (-not $expected) { $embedded = $EmbeddedSha256.Trim().ToLowerInvariant() if ($embedded -notmatch '^[0-9a-f]{64}$') { throw 'this build still carries the unsubstituted release hash placeholder, so the download cannot be verified. Pass -ExpectedSha256 with the published rc34 hash, or use the released installer.' } $expected = $embedded } } if ($mode -eq 'site') { Write-Step 3 'Getting access' if ($SgtKeyFile) { if (-not (Test-Path -LiteralPath $SgtKeyFile -PathType Leaf)) { throw ('the SGT key file was not found: ' + $SgtKeyFile) } $keyPath = (Resolve-Path -LiteralPath $SgtKeyFile).ProviderPath } elseif (Test-Path -LiteralPath (Join-Path $env:USERPROFILE 'sgt-key.txt') -PathType Leaf) { $keyPath = Join-Path $env:USERPROFILE 'sgt-key.txt' } if ($keyPath) { $key = (Get-Content -LiteralPath $keyPath -Raw).Trim() Write-Note ('using the SGT key file at ' + $keyPath + ' (contents are never printed)') } else { $saved = Get-SavedSgtKey if ($saved) { $key = $saved Write-Note 'using this PC''s saved SGT activation (no key file needed)' } else { $secure = Read-Host 'Paste your SGT key or personal download link (typing is hidden)' -AsSecureString $bstr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($secure) try { $key = [Runtime.InteropServices.Marshal]::PtrToStringBSTR($bstr) } finally { [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr) } Write-Note 'key captured from the masked prompt (never printed, never written to your profile)' } } if (-not $key -or $key.Length -lt 8) { throw 'no usable SGT key was supplied.' } $personalLink = $null if ($key.StartsWith('https://')) { $uri = $null if (-not [Uri]::TryCreate($key, [UriKind]::Absolute, [ref]$uri) -or $uri.Host -ne 'sgtfleet.com' -or $uri.Scheme -ne 'https' -or -not $uri.Fragment -or $uri.Fragment.Length -lt 9) { throw 'the personal download link is not a valid sgtfleet.com link.' } $personalLink = $uri $keyPath = $null } Write-Step 4 'Downloading the rc34 package' New-Item -ItemType Directory -Force -Path $Staging | Out-Null $zip = Join-Path $Staging 'SGT-Fleet-rc34.zip' try { if ($personalLink) { $token = $personalLink.Fragment.Substring(1) Invoke-WebRequest -Uri ($Site + '/api/customer/download-link') -Method POST ` -ContentType 'application/json' -Headers @{ Origin = $Site } ` -Body (@{ token = $token } | ConvertTo-Json -Compress) -OutFile $zip -UseBasicParsing $mode = 'personal-link' $key = $null } else { Invoke-WebRequest -Uri ($Site + '/api/download/key') -Method POST ` -Headers @{ Authorization = ('Bearer ' + $key) } -OutFile $zip -UseBasicParsing } } catch { $status = 0 try { if ($_.Exception.Response) { $status = [int]$_.Exception.Response.StatusCode } } catch { $status = 0 } $body = '' try { if ($_.ErrorDetails -and $_.ErrorDetails.Message) { $body = [string]$_.ErrorDetails.Message } } catch { $body = '' } if (-not $body) { try { $stream = $null if ($_.Exception.Response) { $stream = $_.Exception.Response.GetResponseStream() } if ($stream) { $reader = New-Object IO.StreamReader($stream) try { $body = $reader.ReadToEnd() } finally { $reader.Dispose() } } } catch { $body = '' } } $refusal = ConvertFrom-JsonText $body if ($refusal -and $refusal.error -and $refusal.fix) { throw ('the download was refused: ' + [string]$refusal.error + '. ' + [string]$refusal.fix) } if ($status -eq 401 -or $status -eq 403) { throw 'the SGT key was not accepted for the private download (check the subscription).' } throw ('the download failed: ' + (Get-SafeText ([string]$_.Exception.Message))) } Write-Note ('downloaded ' + [string](Get-Item -LiteralPath $zip).Length + ' bytes') } Write-Step 5 'Verifying the package hash before extraction' if (-not (Test-Path -LiteralPath $zip -PathType Leaf)) { throw ('the package is missing: ' + $zip) } $actual = (Get-FileHash -LiteralPath $zip -Algorithm SHA256).Hash.ToLowerInvariant() if ($mode -eq 'personal-link') { Test-PersonalizedZip $zip | Out-Null Write-Note ('personal ZIP contents verified against pinned manifest ' + $EmbeddedManifestSha256) } elseif ($actual -ne $expected) { throw ('the package hash does not match. expected ' + $expected + ', got ' + $actual + '. Nothing was extracted and nothing was installed.') } Write-Note ('sha256 ' + $actual + ' verified') Write-Step 6 'Staging the package' New-Item -ItemType Directory -Force -Path $Root, $Bin, $Staging | Out-Null $extract = Join-Path $Staging 'extract' if (Test-Path -LiteralPath $extract) { Remove-Item -LiteralPath $extract -Recurse -Force } New-Item -ItemType Directory -Force -Path $extract | Out-Null Expand-Archive -LiteralPath $zip -DestinationPath $extract -Force $found = @(Get-ChildItem -LiteralPath $extract -Recurse -Filter 'SGT-Fleet.exe' -File | Select-Object -First 1) if ($found.Count -eq 0) { throw 'the verified package did not contain SGT-Fleet.exe.' } $pkg = $found[0].DirectoryName $claim = Join-Path $extract 'activation.sgtclaim' if (Test-Path -LiteralPath $claim -PathType Leaf) { if (Test-Path -LiteralPath (Join-Path $pkg 'activation.sgtclaim')) { throw 'duplicate activation claim.' } Move-Item -LiteralPath $claim -Destination (Join-Path $pkg 'activation.sgtclaim') } if (Test-Path -LiteralPath $App) { $old = Join-Path $Root ('app.bak-' + $Stamp) Move-Item -LiteralPath $App -Destination $old -Force $script:OldApp = $old Write-Note ('previous installation preserved at ' + $old) } try { Move-Item -LiteralPath $pkg -Destination $App -Force } catch { if ($script:OldApp -and -not (Test-Path -LiteralPath $App)) { Move-Item -LiteralPath $script:OldApp -Destination $App -Force $script:OldApp = $null } throw ('could not place the new application folder. Close any running SGT, Claude Code or Codex session and run the installer again. ' + (Get-SafeText ([string]$_.Exception.Message))) } $script:Swapped = $true $appExe = Join-Path $App 'SGT-Fleet.exe' if (-not (Test-Path -LiteralPath $appExe -PathType Leaf)) { throw 'the staged application folder has no SGT-Fleet.exe.' } $exeNames = @(Get-ChildItem -LiteralPath $App -Recurse -Filter '*.exe' -File | ForEach-Object { $_.Name } | Sort-Object -Unique) Write-Note ('staged executables: ' + ($exeNames -join ', ')) $userPath = [Environment]::GetEnvironmentVariable('Path', 'User') if (-not $userPath) { $userPath = '' } $script:OldUserPath = $userPath $parts = @($userPath -split ';' | Where-Object { $_ -and $_.Trim() }) $already = $false foreach ($p in $parts) { if ($p.TrimEnd('\') -ieq $Bin.TrimEnd('\')) { $already = $true } } if ($already) { Write-Note ($Bin + ' is already on your user PATH') } else { [Environment]::SetEnvironmentVariable('Path', ((@($parts) + $Bin) -join ';'), 'User') $script:PathChanged = $true Write-Note ('added ' + $Bin + ' to your user PATH') } if ($env:Path -notlike ('*' + $Bin + '*')) { $env:Path = $env:Path.TrimEnd(';') + ';' + $Bin } $script:CmdPath = Join-Path $Bin 'sgt.cmd' $cmdText = "@echo off`r`n" + '"%~dp0..\app\SGT-Fleet.exe" %*' + "`r`nexit /b %ERRORLEVEL%`r`n" if (-not (Test-Path -LiteralPath $script:CmdPath -PathType Leaf) -or [IO.File]::ReadAllText($script:CmdPath) -ne $cmdText) { $newCmd = Join-Path $Bin ('sgt.cmd.new-' + $Stamp) Write-NoBom $newCmd $cmdText if (Test-Path -LiteralPath $script:CmdPath -PathType Leaf) { $script:OldCmd = Join-Path $Root ('sgt.cmd.bak-' + $Stamp) [IO.File]::Replace($newCmd, $script:CmdPath, $script:OldCmd) } else { Move-Item -LiteralPath $newCmd -Destination $script:CmdPath } $script:CmdChanged = $true } Write-Step 7 'Running SGT-Fleet.exe setup --quiet' $setupArgs = @('setup', '--quiet') if ($keyPath) { $setupArgs += @('--sgt-key-file', $keyPath) } elseif ($key) { $script:KeyTemp = Join-Path $Staging 'sgt-key.tmp' Write-NoBom $script:KeyTemp $key $setupArgs += @('--sgt-key-file', $script:KeyTemp) } if ($OllamaKeyFile) { if (-not (Test-Path -LiteralPath $OllamaKeyFile -PathType Leaf)) { throw ('the Ollama key file was not found: ' + $OllamaKeyFile) } $setupArgs += @('--ollama-key-file', (Resolve-Path -LiteralPath $OllamaKeyFile).ProviderPath) } $key = $null # the key is no longer needed in this process $setupOut = Join-Path $Staging 'setup.stdout.json' $setupErr = Join-Path $Staging 'setup.stderr.txt' $argLine = ($setupArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' ' $proc = Start-Process -FilePath $appExe -ArgumentList $argLine -NoNewWindow -Wait -PassThru ` -RedirectStandardOutput $setupOut -RedirectStandardError $setupErr $setupCode = $proc.ExitCode Write-Note ('setup exit code ' + $setupCode) $setupJson = $null if (Test-Path -LiteralPath $setupOut) { $setupJson = ConvertFrom-JsonText (Get-Content -LiteralPath $setupOut -Raw) } if ($setupCode -ne 0) { $detail = '' if ($setupJson -and $setupJson.activation_detail) { $detail = [string]$setupJson.activation_detail } if (-not $detail -and $setupJson -and $setupJson.vendor_cli -and $setupJson.vendor_cli.fix) { $detail = 'vendor prerequisite: ' + [string]$setupJson.vendor_cli.fix } if (-not $detail) { $detail = 'setup exited with code ' + $setupCode + '; see the setup output above.' } throw ('setup did not complete: ' + (Get-SafeText $detail)) } if (-not $setupJson) { throw 'setup did not print the JSON result this installer requires.' } if ([string]$setupJson.activation -ne 'active') { $detail = '' if ($setupJson.activation_detail) { $detail = [string]$setupJson.activation_detail } throw ('setup reported activation "' + [string]$setupJson.activation + '" instead of "active". ' + (Get-SafeText $detail)) } if (@($setupJson.PSObject.Properties.Name) -contains 'claude_permission') { if ([string]$setupJson.claude_permission -ne 'unchanged') { throw ('setup reported a Claude permission change ("' + [string]$setupJson.claude_permission + '"); this candidate must not alter vendor permissions.') } } foreach ($v in @($setupJson.vendors)) { if ($v) { Write-Note ([string]$v.vendor + ': ' + [string]$v.status) } } Write-Note 'activation: active' Write-Step 8 'Running SGT-Fleet.exe doctor' $docOut = Join-Path $Staging 'doctor.stdout.json' $docErr = Join-Path $Staging 'doctor.stderr.txt' $docProc = Start-Process -FilePath $appExe -ArgumentList 'doctor' -NoNewWindow -Wait -PassThru ` -RedirectStandardOutput $docOut -RedirectStandardError $docErr $docCode = $docProc.ExitCode Write-Note ('doctor exit code ' + $docCode) if ($docCode -ne 0) { throw ('doctor exited with code ' + $docCode + '; the installation is not healthy.') } $docJson = $null if (Test-Path -LiteralPath $docOut) { $docJson = ConvertFrom-JsonText (Get-Content -LiteralPath $docOut -Raw) } if (-not $docJson) { throw 'doctor did not print the JSON result this installer requires.' } $okField = $null $okValue = $null foreach ($name in @('ok', 'healthy', 'status')) { if (@($docJson.PSObject.Properties.Name) -contains $name) { $okField = $name; $okValue = $docJson.$name; break } } if (-not $okField) { throw 'doctor JSON carries no ok, healthy or status field, so this installer cannot confirm the installation is healthy.' } if ($okField -eq 'status') { $doctorOk = (@('ok', 'ready', 'active', 'pass', 'healthy') -contains ([string]$okValue).ToLowerInvariant()) } else { $doctorOk = [bool]$okValue } if (-not $doctorOk) { throw ('doctor did not report ok (' + $okField + ' = "' + [string]$okValue + '").') } Write-Note ('doctor ' + $okField + ' = ' + [string]$okValue) $updaterOk = $true try { Install-SgtUpdater } catch { $updaterOk = $false Write-Note ('the updater layer was not installed: ' + (Get-SafeText ([string]$_.Exception.Message)) + ' SGT Fleet itself is installed and working; run the installer again to add updates.') } Write-Host '' Write-Host 'SGT Fleet is READY.' -ForegroundColor Green Write-Host 'Open Claude Code or Codex in your project and ask for what you want built.' Write-Host ('Install folder: ' + $App) if ($updaterOk) { Write-Host 'Updates install automatically. Check: sgt update --check. Go back: sgt update --rollback.' } else { Write-Host 'Automatic updates are not active. Run the installer again to add them.' } } catch { Write-Host '' Write-Host ('FAILED: ' + (Get-SafeText ([string]$_.Exception.Message))) -ForegroundColor Red if ($script:CmdChanged -and $script:CmdPath) { try { if ($script:OldCmd -and (Test-Path -LiteralPath $script:OldCmd -PathType Leaf)) { Copy-Item -LiteralPath $script:OldCmd -Destination $script:CmdPath -Force } elseif (Test-Path -LiteralPath $script:CmdPath -PathType Leaf) { Remove-Item -LiteralPath $script:CmdPath -Force } } catch { Write-Host 'Could not restore the previous sgt.cmd; the saved backup remains under the install root.' -ForegroundColor Yellow } } if ($script:PathChanged) { try { [Environment]::SetEnvironmentVariable('Path', $script:OldUserPath, 'User') } catch { Write-Host 'Could not restore the prior user PATH; remove the SGT bin entry manually.' -ForegroundColor Yellow } } if ($script:Swapped -and $script:OldApp -and (Test-Path -LiteralPath $script:OldApp)) { try { $failed = Join-Path $Root ('app.failed-' + $Stamp) if (Test-Path -LiteralPath $App) { Move-Item -LiteralPath $App -Destination $failed -Force } Move-Item -LiteralPath $script:OldApp -Destination $App -Force $script:OldApp = $null Write-Host ('The previous installation was restored to ' + $App + '.') -ForegroundColor Yellow Write-Host ('The failed candidate is kept at ' + $failed + ' for diagnosis.') -ForegroundColor Yellow } catch { Write-Host ('Could not restore the previous installation automatically; it is still at ' + $script:OldApp + '.') -ForegroundColor Yellow } } elseif ($script:Swapped -and -not $script:OldApp -and (Test-Path -LiteralPath $App)) { try { $failed = Join-Path $Root ('app.failed-' + $Stamp) Move-Item -LiteralPath $App -Destination $failed -Force Write-Host ('No previous installation existed; the failed candidate is kept at ' + $failed + '.') -ForegroundColor Yellow } catch { Write-Host 'Could not move the failed candidate out of app; inspect the install folder.' -ForegroundColor Yellow } } elseif ($script:OldApp -and (Test-Path -LiteralPath $script:OldApp)) { Write-Host ('The previous installation is untouched at ' + $script:OldApp + '.') -ForegroundColor Yellow } $global:LASTEXITCODE = 1 if ($PSCommandPath) { exit 1 } else { return } } finally { if ($script:KeyTemp -and (Test-Path -LiteralPath $script:KeyTemp)) { try { Remove-Item -LiteralPath $script:KeyTemp -Force } catch { } } if ($KeepStaging) { Write-Host ('Staging kept at ' + $Staging) } else { try { $copied = 0 foreach ($f in @('setup.stdout.json', 'setup.stderr.txt', 'doctor.stdout.json', 'doctor.stderr.txt')) { $src = Join-Path $Staging $f if (Test-Path -LiteralPath $src) { if ($copied -eq 0) { New-Item -ItemType Directory -Force -Path $LogDir | Out-Null } Write-NoBom (Join-Path $LogDir $f) (Get-SafeText (Get-Content -LiteralPath $src -Raw) 200000) $copied++ } } if ($copied -gt 0) { Write-Host ('Evidence kept at ' + $LogDir) } } catch { } try { if (Test-Path -LiteralPath $Staging) { Remove-Item -LiteralPath $Staging -Recurse -Force } } catch { } } }