# Copyright 2026 StoatWorks Software Inc. All rights reserved. $ErrorActionPreference = "Stop" Set-StrictMode -Version Latest $DownloadBase = if ($env:STOATIFY_DOWNLOAD_BASE_URL) { $env:STOATIFY_DOWNLOAD_BASE_URL.TrimEnd("/") } else { "https://www.stoatify.com/downloads/cli" } $InstallDir = if ($env:STOATIFY_INSTALL_DIR) { $env:STOATIFY_INSTALL_DIR } else { Join-Path $env:LOCALAPPDATA "Stoatify\bin" } $Destination = Join-Path $InstallDir "stoatify.exe" $Architecture = if ($env:PROCESSOR_ARCHITEW6432) { $env:PROCESSOR_ARCHITEW6432 } else { $env:PROCESSOR_ARCHITECTURE } if ($Architecture -notin @("AMD64", "x86_64")) { throw "Stoatify CLI does not provide a Windows build for architecture $Architecture." } function Set-StoatifyPath { param([Parameter(Mandatory = $true)][string]$Directory) $NormalizedDirectory = $Directory.TrimEnd("\") $UserPath = [Environment]::GetEnvironmentVariable("Path", "User") $UserEntries = @($UserPath -split ";" | Where-Object { $_ }) if (-not ($UserEntries | Where-Object { $_.TrimEnd("\") -ieq $NormalizedDirectory })) { $UpdatedUserPath = (@($NormalizedDirectory) + $UserEntries) -join ";" [Environment]::SetEnvironmentVariable("Path", $UpdatedUserPath, "User") } $ProcessEntries = @($env:Path -split ";" | Where-Object { $_ }) if (-not ($ProcessEntries | Where-Object { $_.TrimEnd("\") -ieq $NormalizedDirectory })) { $env:Path = "$NormalizedDirectory;$env:Path" } } $TemporaryDir = Join-Path ([System.IO.Path]::GetTempPath()) ("stoatify-install-" + [Guid]::NewGuid().ToString("N")) New-Item -ItemType Directory -Path $TemporaryDir | Out-Null try { Write-Host "Checking the latest Stoatify CLI release..." $ManifestPath = Join-Path $TemporaryDir "checksums.txt" Invoke-WebRequest -UseBasicParsing -Uri "$DownloadBase/latest/checksums.txt" -OutFile $ManifestPath $Manifest = Get-Content -LiteralPath $ManifestPath $ManifestEntries = @($Manifest | Where-Object { $_ -match "^[0-9a-fA-F]{64}\s+stoatify_.+_windows_amd64\.zip$" }) if ($ManifestEntries.Count -ne 1) { throw "The checksum manifest does not contain exactly one Windows x86-64 build." } if ($ManifestEntries[0] -notmatch "^(?[0-9a-fA-F]{64})\s+(?stoatify_(?[0-9]+\.[0-9]+\.[0-9]+(?:[.-][0-9A-Za-z.-]+)?)_windows_amd64\.zip)$") { throw "The Windows release entry is invalid." } $ExpectedHash = $Matches.Hash.ToLowerInvariant() $ArchiveName = $Matches.Archive $Version = $Matches.Version Set-StoatifyPath -Directory $InstallDir $InstalledVersion = $null if (Test-Path -LiteralPath $Destination -PathType Leaf) { $VersionOutput = & $Destination version 2>$null if ($VersionOutput -match "^stoatify version (?\S+)") { $InstalledVersion = $Matches.Version } } if ($InstalledVersion -eq $Version) { Write-Host "Stoatify CLI $Version is already installed at $Destination" } else { $ArchivePath = Join-Path $TemporaryDir $ArchiveName Invoke-WebRequest -UseBasicParsing -Uri "$DownloadBase/v$Version/$ArchiveName" -OutFile $ArchivePath $ActualHash = (Get-FileHash -Algorithm SHA256 -LiteralPath $ArchivePath).Hash.ToLowerInvariant() if ($ActualHash -ne $ExpectedHash) { throw "Checksum verification failed." } $ExtractDir = Join-Path $TemporaryDir "extracted" Expand-Archive -LiteralPath $ArchivePath -DestinationPath $ExtractDir $ExtractedBinary = Join-Path $ExtractDir "stoatify.exe" if (-not (Test-Path -LiteralPath $ExtractedBinary -PathType Leaf)) { throw "The archive does not contain the Stoatify CLI." } $DownloadedVersionOutput = & $ExtractedBinary version 2>$null if ($DownloadedVersionOutput -notmatch "^stoatify version $([Regex]::Escape($Version)) ") { throw "The downloaded binary version does not match the release." } New-Item -ItemType Directory -Force -Path $InstallDir | Out-Null $StagedBinary = Join-Path $InstallDir (".stoatify." + [Guid]::NewGuid().ToString("N") + ".tmp") Copy-Item -LiteralPath $ExtractedBinary -Destination $StagedBinary Move-Item -Force -LiteralPath $StagedBinary -Destination $Destination Write-Host "Installed Stoatify CLI $Version at $Destination" } } finally { Remove-Item -Recurse -Force -LiteralPath $TemporaryDir -ErrorAction SilentlyContinue }