From 7756038e43385f54a24869dfda77cb6e6bf3f33a Mon Sep 17 00:00:00 2001 From: Michiel De Smet Date: Thu, 11 Jun 2026 14:37:45 +0800 Subject: [PATCH 1/2] feat: Windows PowerShell installer (install.ps1) Adds a PowerShell installer that mirrors the macOS/Linux `install` bash script: it downloads the Bun-compiled standalone altimate.exe from GitHub releases (no npm/Node dependency) into %USERPROFILE%\.altimate\bin and adds it to the user PATH. - Full baseline parity: AVX2 detection via IsProcessorFeaturePresent(40), windows-x64-baseline fallback, and a STATUS_ILLEGAL_INSTRUCTION retry. - PATH edit via the registry + WM_SETTINGCHANGE broadcast (not setx). - Replaces a locked running .exe by renaming it aside first, so `altimate upgrade` works natively on Windows. - installation/index.ts: `altimate upgrade` now self-updates on native Windows via PowerShell (irm install.ps1 | iex) since there is no bash; the "curl" upgrade branch dispatches on process.platform === "win32". - Docs (README, windows-wsl, troubleshooting) advertise the one-liner: powershell -c "irm https://www.altimate.sh/install.ps1 | iex" - Tests pin Bun-exe-not-npm, install dir, baseline detection/fallback, host consistency, and the win32 upgrade dispatch. Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 10 +- docs/docs/reference/troubleshooting.md | 18 ++ docs/docs/reference/windows-wsl.md | 32 ++- install.ps1 | 250 ++++++++++++++++++ packages/opencode/src/installation/index.ts | 44 ++- .../opencode/test/branding/branding.test.ts | 34 +++ .../test/install/windows-install.test.ts | 74 ++++++ 7 files changed, 457 insertions(+), 5 deletions(-) create mode 100644 install.ps1 create mode 100644 packages/opencode/test/install/windows-install.test.ts diff --git a/README.md b/README.md index 9a4db181b7..4b51dcb128 100644 --- a/README.md +++ b/README.md @@ -30,13 +30,19 @@ into CI pipelines and orchestration DAGs. Precision data tooling for any LLM. npm install -g altimate-code ``` -Or via curl (installs the `altimate` binary to `~/.altimate/bin`): +Or via curl on macOS/Linux (installs the `altimate` binary to `~/.altimate/bin`): ```bash curl -fsSL https://www.altimate.sh/install | bash ``` -The curl install drops a single self-contained binary named `altimate`. The npm install exposes both `altimate` and `altimate-code` on PATH; the curl install only exposes `altimate`. Alpine Linux (musl) and Windows on ARM64 are not currently supported by the standalone binary — use `apk add gcompat` on Alpine, or use WSL on Windows-on-ARM. +On Windows, install the same self-contained binary (to `%USERPROFILE%\.altimate\bin`) from PowerShell — no Node required: + +```powershell +powershell -c "irm https://www.altimate.sh/install.ps1 | iex" +``` + +The standalone install drops a single self-contained binary named `altimate`. The npm install exposes both `altimate` and `altimate-code` on PATH; the standalone install only exposes `altimate`. Alpine Linux (musl) and Windows on ARM64 are not currently supported by the standalone binary — use `apk add gcompat` on Alpine, or use WSL on Windows-on-ARM. For GitHub, [install the Altimate Code App](https://github.com/apps/altimate-code-agent/installations/new) to select repositories for interactive agent tasks. Automatic dbt pull-request diff --git a/docs/docs/reference/troubleshooting.md b/docs/docs/reference/troubleshooting.md index 69ed57864e..7734b456c7 100644 --- a/docs/docs/reference/troubleshooting.md +++ b/docs/docs/reference/troubleshooting.md @@ -52,6 +52,24 @@ Or use a glibc-based base image (`debian`, `ubuntu`, `node:slim`). Run the x64 build under Windows-on-ARM's x64 emulation layer, or use WSL. +### `altimate` not found after the Windows standalone install + +**Symptoms:** `altimate` is unrecognized after running the PowerShell installer. + +The installer adds `%USERPROFILE%\.altimate\bin` to your **user** PATH — open a +new terminal so it takes effect. To (re)install or pin a version: + +```powershell +# Latest +powershell -c "irm https://www.altimate.sh/install.ps1 | iex" + +# Specific version +&([scriptblock]::Create((irm https://www.altimate.sh/install.ps1))) -Version 1.0.180 +``` + +If the binary crashes immediately on an older CPU, the installer normally retries +the baseline (non-AVX2) build automatically; force it with `-ForceBaseline`. + ## Log Files Logs are stored at: diff --git a/docs/docs/reference/windows-wsl.md b/docs/docs/reference/windows-wsl.md index 8dac0f46f4..f2d84cb952 100644 --- a/docs/docs/reference/windows-wsl.md +++ b/docs/docs/reference/windows-wsl.md @@ -4,7 +4,35 @@ altimate runs on Windows both natively (via Node.js on Windows) and through WSL ## Windows Native Install -You can install and run altimate directly in PowerShell or Command Prompt without WSL: +### Standalone install (no Node) + +The fastest path installs the self-contained binary — the same Bun-compiled +`altimate.exe` we ship on macOS/Linux — straight from GitHub releases. It needs +no Node.js or npm: + +```powershell +powershell -c "irm https://www.altimate.sh/install.ps1 | iex" +``` + +This downloads `altimate.exe` to `%USERPROFILE%\.altimate\bin` and adds that +directory to your user PATH (open a new terminal afterwards). The installer +auto-detects AVX2 support and falls back to the baseline build on older CPUs. + +Options (pass via a script block): + +```powershell +# Pin a specific version +&([scriptblock]::Create((irm https://www.altimate.sh/install.ps1))) -Version 1.0.180 + +# Skip the PATH edit +&([scriptblock]::Create((irm https://www.altimate.sh/install.ps1))) -NoPathUpdate +``` + +`altimate upgrade` self-updates a standalone install in place using the same script. + +### npm install + +Alternatively, install via npm with Node.js 18+ installed natively on Windows: ```powershell # PowerShell or CMD — install globally @@ -14,7 +42,7 @@ npm install -g altimate-code altimate ``` -This works with Node.js 18+ installed natively on Windows. All core features work in native mode, including warehouse connections, agent modes, and the TUI. +Both paths support all core features in native mode, including warehouse connections, agent modes, and the TUI. ## WSL Setup (Recommended) diff --git a/install.ps1 b/install.ps1 new file mode 100644 index 0000000000..c992607887 --- /dev/null +++ b/install.ps1 @@ -0,0 +1,250 @@ +#!/usr/bin/env pwsh +# Altimate Code installer for Windows (PowerShell). +# +# Mirrors ./install (the bash installer for macOS/Linux): it downloads the +# Bun-compiled standalone executable (altimate.exe) from GitHub releases and +# drops it in %USERPROFILE%\.altimate\bin — it does NOT depend on npm/Node. +# +# Usage: +# powershell -c "irm https://www.altimate.sh/install.ps1 | iex" +# # pin a version / skip PATH edit / force the baseline (non-AVX2) build: +# &([scriptblock]::Create((irm https://www.altimate.sh/install.ps1))) -Version 1.0.180 +# &([scriptblock]::Create((irm https://www.altimate.sh/install.ps1))) -NoPathUpdate +# &([scriptblock]::Create((irm https://www.altimate.sh/install.ps1))) -ForceBaseline + +param( + # Install a specific version (e.g. 1.0.180). Falls back to $env:VERSION so the + # in-app `altimate upgrade` flow can pin the target version. + [string]$Version = $env:VERSION, + # Don't modify the user PATH (mirrors --no-modify-path). + [switch]$NoPathUpdate = $false, + # Force the baseline (non-AVX2) build even if AVX2 is detected. Also used by + # the illegal-instruction retry below when AVX2 detection is wrong. + [switch]$ForceBaseline = $false +) + +$ErrorActionPreference = "Stop" +# Expand-Archive / Invoke-WebRequest render a slow progress UI over a remote +# stream; silence it so piped installs stay fast and clean. +$ProgressPreference = "SilentlyContinue" + +$App = "altimate" +$InstallDir = Join-Path $env:USERPROFILE ".altimate\bin" +$BinaryName = "$App.exe" +$InstalledBinary = Join-Path $InstallDir $BinaryName + +function Write-Muted($msg) { Write-Host $msg -ForegroundColor DarkGray } +function Write-Err($msg) { Write-Host $msg -ForegroundColor Red } + +# --------------------------------------------------------------------------- +# Architecture / baseline detection +# --------------------------------------------------------------------------- +# Only win32-x64 is built. @altimateai/altimate-core has no NAPI prebuild for +# win32-arm64, so ARM64 has no standalone archive (see packages/opencode/script/build.ts). +$procArch = $env:PROCESSOR_ARCHITECTURE +if ($procArch -ne "AMD64") { + Write-Err "Unsupported OS/Arch: windows/$procArch" + Write-Muted "The standalone Windows build is x64 (AMD64) only. On Windows-on-ARM, use WSL or npm install -g altimate-code." + exit 1 +} +$arch = "x64" + +# AVX2 detection via the same Win32 API the bash installer shells out to. +# IsProcessorFeaturePresent(40) == PF_AVX2_INSTRUCTIONS_AVAILABLE. +function Test-Avx2 { + try { + $sig = '[DllImport("kernel32.dll")] public static extern bool IsProcessorFeaturePresent(int ProcessorFeature);' + $k32 = Add-Type -MemberDefinition $sig -Name "AltimateKernel32" -Namespace "Win32" -PassThru + return [bool]$k32::IsProcessorFeaturePresent(40) + } catch { + # If detection fails, assume no AVX2 and fall back to the baseline build — + # the baseline binary runs everywhere, an AVX2 binary on a non-AVX2 CPU crashes. + return $false + } +} + +# --------------------------------------------------------------------------- +# Resolve version (once) — latest tag or a pinned release +# --------------------------------------------------------------------------- +if ([string]::IsNullOrWhiteSpace($Version)) { + $useLatest = $true + try { + $rel = Invoke-RestMethod -Uri "https://api.github.com/repos/AltimateAI/altimate-code/releases/latest" -Headers @{ "User-Agent" = "altimate-install" } + $specificVersion = ($rel.tag_name -replace '^v', '') + } catch { + Write-Err "Failed to fetch version information" + exit 1 + } + if ([string]::IsNullOrWhiteSpace($specificVersion)) { + Write-Err "Failed to fetch version information" + exit 1 + } +} else { + $useLatest = $false + # Strip a leading 'v' if present. + $Version = $Version -replace '^v', '' + $specificVersion = $Version + + # Verify the release exists before downloading (mirrors the bash 404 pre-check). + try { + Invoke-WebRequest -Uri "https://github.com/AltimateAI/altimate-code/releases/tag/v$Version" -Method Head -UseBasicParsing | Out-Null + } catch { + Write-Err "Error: Release v$Version not found" + Write-Muted "Available releases: https://github.com/AltimateAI/altimate-code/releases" + exit 1 + } +} + +# --------------------------------------------------------------------------- +# Skip if the requested version is already installed +# --------------------------------------------------------------------------- +# Probe both names: the standalone install ships `altimate`, but an npm install +# also exposes the `altimate-code` alias. +$probe = $null +foreach ($name in @("altimate", "altimate-code")) { + $cmd = Get-Command $name -ErrorAction SilentlyContinue + if ($cmd) { $probe = $cmd.Source; break } +} +if ($probe) { + $installedVersion = "" + try { $installedVersion = (& $probe --version 2>$null | Select-Object -First 1).ToString().Trim() } catch {} + if ($installedVersion -eq $specificVersion) { + Write-Muted "Version $specificVersion already installed" + exit 0 + } elseif ($installedVersion) { + Write-Muted "Installed version: $installedVersion." + } +} + +# --------------------------------------------------------------------------- +# Download + extract a single target archive into $InstallDir +# --------------------------------------------------------------------------- +function Install-Target { + param([bool]$Baseline) + + $target = "windows-$arch" + if ($Baseline) { $target = "$target-baseline" } + $filename = "$App-$target.zip" + + if ($useLatest) { + $url = "https://github.com/AltimateAI/altimate-code/releases/latest/download/$filename" + } else { + $url = "https://github.com/AltimateAI/altimate-code/releases/download/v$specificVersion/$filename" + } + + Write-Host "" + Write-Host "Installing $App version: $specificVersion" + + $tmpDir = Join-Path ([System.IO.Path]::GetTempPath()) "altimate_install_$PID" + New-Item -ItemType Directory -Force -Path $tmpDir | Out-Null + $zipPath = Join-Path $tmpDir $filename + + try { + # Prefer curl.exe (ships with Windows 10 1803+) for a fast download with + # --fail so HTTP errors don't write an error page to disk; fall back to + # Invoke-WebRequest where curl.exe is unavailable. + $curl = Get-Command curl.exe -ErrorAction SilentlyContinue + if ($curl) { + & $curl.Source "-#SfLo" $zipPath $url + if ($LASTEXITCODE -ne 0) { throw "curl.exe failed downloading $url (exit $LASTEXITCODE)" } + } else { + Invoke-WebRequest -Uri $url -OutFile $zipPath -UseBasicParsing + } + + Expand-Archive -Path $zipPath -DestinationPath $tmpDir -Force + $extracted = Join-Path $tmpDir $BinaryName + if (-not (Test-Path $extracted)) { + throw "Archive did not contain $BinaryName" + } + New-Item -ItemType Directory -Force -Path $InstallDir | Out-Null + + # Windows locks a running .exe, so `altimate upgrade` (which re-runs this + # installer) can't overwrite the binary that is currently executing. Windows + # *does* allow renaming a running exe — move the old one aside first, then + # drop the new one in. Best-effort cleanup of the stale copy afterward. + if (Test-Path $InstalledBinary) { + $stale = "$InstalledBinary.old" + Remove-Item -Force $stale -ErrorAction SilentlyContinue + try { Move-Item -Force -Path $InstalledBinary -Destination $stale } catch {} + } + Move-Item -Force -Path $extracted -Destination $InstalledBinary + Remove-Item -Force "$InstalledBinary.old" -ErrorAction SilentlyContinue + } finally { + Remove-Item -Recurse -Force -Path $tmpDir -ErrorAction SilentlyContinue + } +} + +$needsBaseline = $ForceBaseline -or (-not (Test-Avx2)) +Install-Target -Baseline:$needsBaseline + +# --------------------------------------------------------------------------- +# Illegal-instruction retry (AVX2 misdetection rescue) +# --------------------------------------------------------------------------- +# If the freshly installed AVX2 binary won't run on this CPU it exits with +# STATUS_ILLEGAL_INSTRUCTION (0xC000001D == 3221225501, surfaced as 1073741795 +# in some shells). Re-download the baseline build once. +if (-not $needsBaseline) { + & $InstalledBinary --version *> $null + $code = $LASTEXITCODE + if ($code -eq 3221225501 -or $code -eq 1073741795 -or $code -eq -1073741795) { + Write-Muted "CPU lacks AVX2 — reinstalling the baseline build" + Install-Target -Baseline:$true + } +} + +# --------------------------------------------------------------------------- +# PATH (user scope, via registry + broadcast) +# --------------------------------------------------------------------------- +# Write the user PATH through the registry (not setx, which truncates at 1024 +# chars) and broadcast WM_SETTINGCHANGE so already-open shells pick it up. +function Publish-EnvChange { + if (-not ("Win32.NativeMethods" -as [type])) { + Add-Type -Namespace Win32 -Name NativeMethods -MemberDefinition @" +[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)] +public static extern IntPtr SendMessageTimeout(IntPtr hWnd, uint Msg, UIntPtr wParam, string lParam, uint fuFlags, uint uTimeout, out UIntPtr lpdwResult); +"@ + } + $HWND_BROADCAST = [IntPtr]0xffff + $WM_SETTINGCHANGE = 0x1a + $result = [UIntPtr]::Zero + [Win32.NativeMethods]::SendMessageTimeout($HWND_BROADCAST, $WM_SETTINGCHANGE, [UIntPtr]::Zero, "Environment", 2, 5000, [ref]$result) | Out-Null +} + +if (-not $NoPathUpdate) { + # Read the raw (unexpanded) user PATH from the registry so we don't clobber + # %VAR%-style entries on write. + $regKey = [Microsoft.Win32.Registry]::CurrentUser.OpenSubKey("Environment", $true) + if (-not $regKey) { + $regKey = [Microsoft.Win32.Registry]::CurrentUser.CreateSubKey("Environment") + } + $userPath = $regKey.GetValue("Path", "", [Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames) + $entries = @($userPath -split ';' | Where-Object { $_ -ne "" }) + if ($entries -notcontains $InstallDir) { + $newPath = (@($InstallDir) + $entries) -join ';' + $regKey.SetValue("Path", $newPath, [Microsoft.Win32.RegistryValueKind]::ExpandString) + Publish-EnvChange + # Update the current session too so the post-install hints work immediately. + $env:Path = "$InstallDir;$env:Path" + Write-Muted "Successfully added $App to PATH in the user environment ($InstallDir)" + } + $regKey.Close() +} else { + Write-Muted "Skipped PATH modification (--no-modify-path). Add manually: $InstallDir" +} + +# GitHub Actions: expose the install dir to subsequent steps (mirrors install:504). +if ($env:GITHUB_ACTIONS -eq "true" -and $env:GITHUB_PATH) { + Add-Content -Path $env:GITHUB_PATH -Value $InstallDir + Write-Muted "Added $InstallDir to `$GITHUB_PATH" +} + +Write-Host "" +Write-Host "" +Write-Muted "Get started:" +Write-Host "" +Write-Host "altimate # Open the TUI" +Write-Host "altimate run `"hello`" # Run a quick task" +Write-Host "altimate --help # See all commands" +Write-Host "" +Write-Muted "Docs: https://altimate-code.dev" +Write-Host "" diff --git a/packages/opencode/src/installation/index.ts b/packages/opencode/src/installation/index.ts index 512ec07177..bc1cff59a8 100644 --- a/packages/opencode/src/installation/index.ts +++ b/packages/opencode/src/installation/index.ts @@ -39,6 +39,10 @@ export namespace Installation { // Amplify Next.js app — tracked separately; revisit when apex DNS is fixed). // Bounded timeout so a stalled CDN/origin can't hang `altimate upgrade` forever. const UPGRADE_INSTALL_URL = "https://www.altimate.sh/install" + // Native Windows has no `bash`, so the curl-installed binary self-updates via + // the PowerShell installer instead (downloads the same Bun exe from GitHub + // releases). Same host as the bash script; both 302 to raw GitHub. + const UPGRADE_INSTALL_PS_URL = "https://www.altimate.sh/install.ps1" const UPGRADE_FETCH_TIMEOUT_MS = 15_000 // altimate_change end @@ -80,6 +84,42 @@ export namespace Installation { } } + // altimate_change start — Windows curl-install upgrade via PowerShell + // The curl/standalone install on native Windows lives in %USERPROFILE%\.altimate\bin + // (detected as method "curl") but there is no `bash` to pipe the install + // script into. Run the PowerShell installer instead; it downloads the same + // Bun exe from GitHub releases and reads $env:VERSION to pin the target. + async function upgradePowershell(target: string) { + // Probe-only fetch to surface a friendly error before we hand the URL to + // PowerShell (which would otherwise fail opaquely inside `irm | iex`). + try { + await fetch(UPGRADE_INSTALL_PS_URL, { + method: "HEAD", + signal: AbortSignal.timeout(UPGRADE_FETCH_TIMEOUT_MS), + }).then((res) => { + if (!res.ok) throw new Error(`HTTP ${res.status} ${res.statusText}`) + }) + } catch (err) { + const cause = err instanceof Error ? err.message : String(err) + throw new Error( + `Could not download install script from ${UPGRADE_INSTALL_PS_URL}: ${cause}. ` + + `Re-run the install manually: powershell -c "irm ${UPGRADE_INSTALL_PS_URL} | iex" — ` + + `or download a release binary directly from https://github.com/AltimateAI/altimate-code/releases/latest`, + ) + } + return Process.run( + ["powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", `irm ${UPGRADE_INSTALL_PS_URL} | iex`], + { + env: { + ...process.env, + VERSION: target, + }, + nothrow: true, + }, + ) + } + // altimate_change end + export type Method = Awaited> export const Event = { @@ -209,7 +249,9 @@ export namespace Installation { let result: Awaited> | undefined switch (method) { case "curl": - result = await upgradeCurl(target) + // altimate_change start — native Windows has no bash; use the PS installer + result = process.platform === "win32" ? await upgradePowershell(target) : await upgradeCurl(target) + // altimate_change end break case "npm": result = await Process.run(["npm", "install", "-g", `@altimateai/altimate-code@${target}`], { nothrow: true }) diff --git a/packages/opencode/test/branding/branding.test.ts b/packages/opencode/test/branding/branding.test.ts index 01543a830c..92a6111086 100644 --- a/packages/opencode/test/branding/branding.test.ts +++ b/packages/opencode/test/branding/branding.test.ts @@ -130,6 +130,40 @@ describe("Installation Script", () => { }) }) +// --------------------------------------------------------------------------- +// 4b. Windows Installation Script (install.ps1) +// --------------------------------------------------------------------------- +describe("Windows Installation Script", () => { + const ps1Content = readText(join(repoRoot, "install.ps1")) + + test("installs the Bun standalone exe, not the npm package", () => { + // The whole point of install.ps1 is parity with the bash installer: pull + // the Bun-compiled altimate.exe from GitHub releases and extract it — not + // shell out to a package manager. (npm is only mentioned as an ARM64 hint.) + expect(ps1Content).toContain("github.com/AltimateAI/altimate-code/releases") + expect(ps1Content).toContain("Expand-Archive") + expect(ps1Content).toContain('"$App.exe"') + expect(ps1Content).not.toContain("npm install -g @altimateai") + }) + + test("install dir is .altimate\\bin", () => { + expect(ps1Content).toContain(".altimate\\bin") + expect(ps1Content).not.toContain(".altimate-code\\bin") + }) + + test("downloads the windows-x64 archive with a baseline fallback", () => { + expect(ps1Content).toContain("windows-$arch") + expect(ps1Content).toContain("-baseline") + // AVX2 detection mirrors the bash installer's IsProcessorFeaturePresent(40). + expect(ps1Content).toContain("IsProcessorFeaturePresent(40)") + }) + + test("no upstream/foreign brand leaks", () => { + expect(ps1Content).not.toContain("opencode.ai") + expect(ps1Content).not.toContain("anomalyco") + }) +}) + // --------------------------------------------------------------------------- // 5. GitHub Action // --------------------------------------------------------------------------- diff --git a/packages/opencode/test/install/windows-install.test.ts b/packages/opencode/test/install/windows-install.test.ts new file mode 100644 index 0000000000..71d3e524f3 --- /dev/null +++ b/packages/opencode/test/install/windows-install.test.ts @@ -0,0 +1,74 @@ +/** + * Windows standalone installer (install.ps1) + native-Windows upgrade wiring. + * + * Pins the parity contract with the bash installer: the PowerShell script must + * pull the Bun-compiled altimate.exe from GitHub releases (NOT npm), share the + * altimate.sh/install.ps1 host with the in-app upgrade path, and the upgrade + * "curl" branch must route native Windows through PowerShell (there is no bash). + */ +import { describe, test, expect } from "bun:test" +import { readFileSync } from "node:fs" +import { join } from "node:path" + +const REPO_ROOT = join(import.meta.dir, "../../../..") +const PS1 = readFileSync(join(REPO_ROOT, "install.ps1"), "utf-8") +const INSTALLATION_SRC = readFileSync(join(REPO_ROOT, "packages/opencode/src/installation/index.ts"), "utf-8") +const README = readFileSync(join(REPO_ROOT, "README.md"), "utf-8") +const WINDOWS_DOC = readFileSync(join(REPO_ROOT, "docs/docs/reference/windows-wsl.md"), "utf-8") + +describe("install.ps1 — Bun exe, not npm", () => { + test("downloads from GitHub releases and extracts the binary, not npm", () => { + expect(PS1).toContain("github.com/AltimateAI/altimate-code/releases") + expect(PS1).toContain("Expand-Archive") + // npm only appears as an ARM64 fallback hint, never as the install mechanism. + expect(PS1).not.toContain("npm install -g @altimateai") + }) + + test("installs the .exe into .altimate\\bin", () => { + expect(PS1).toContain(".altimate\\bin") + expect(PS1).toContain('"$App.exe"') + }) + + test("rejects win32-arm64 (no NAPI prebuild)", () => { + expect(PS1).toContain("Unsupported OS/Arch") + }) +}) + +describe("install.ps1 — baseline (non-AVX2) parity", () => { + test("detects AVX2 via IsProcessorFeaturePresent(40)", () => { + expect(PS1).toContain("IsProcessorFeaturePresent(40)") + }) + + test("falls back to the windows-x64-baseline archive", () => { + expect(PS1).toContain("windows-$arch") + expect(PS1).toContain("$target-baseline") + }) + + test("retries the baseline build on STATUS_ILLEGAL_INSTRUCTION", () => { + expect(PS1).toContain("3221225501") + }) +}) + +describe("install.ps1 — host consistency with the upgrade path", () => { + test("source upgrade URL uses (www.)altimate.sh/install.ps1", () => { + expect(INSTALLATION_SRC).toMatch( + /UPGRADE_INSTALL_PS_URL\s*=\s*"https:\/\/(www\.)?altimate\.sh\/install\.ps1"/, + ) + }) + + test("README and Windows docs advertise the install.ps1 one-liner", () => { + expect(README).toContain("altimate.sh/install.ps1") + expect(WINDOWS_DOC).toContain("altimate.sh/install.ps1") + }) +}) + +describe("upgrade() — native Windows routes through PowerShell", () => { + test("upgradePowershell exists and runs the PS installer", () => { + expect(INSTALLATION_SRC).toContain("async function upgradePowershell") + expect(INSTALLATION_SRC).toMatch(/irm \$\{UPGRADE_INSTALL_PS_URL\} \| iex/) + }) + + test("curl branch dispatches on win32", () => { + expect(INSTALLATION_SRC).toMatch(/process\.platform === "win32"\s*\?\s*await upgradePowershell\(target\)/) + }) +}) From bb393db835d7ebd179adc5795da93b26e5ee48f5 Mon Sep 17 00:00:00 2001 From: Michiel De Smet Date: Mon, 15 Jun 2026 21:15:50 +0800 Subject: [PATCH 2/2] =?UTF-8?q?fix(install.ps1):=20address=20PR=20review?= =?UTF-8?q?=20=E2=80=94=20WOW64=20arch,=20help,=20Pester=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - WOW64 arch fix (MAJOR): resolve via PROCESSOR_ARCHITEW6432 so a 32-bit PowerShell host on 64-bit Windows is correctly detected as AMD64 instead of being rejected as unsupported x86. - Add -Help/-Help usage block (Show-Usage) mirroring the bash installer's usage(). - Consolidate the two Add-Type P/Invoke blocks into one Win32.AltimateNative type (IsProcessorFeaturePresent + SendMessageTimeout). - Standardize user-facing errors through Write-Err with a uniform "error: " prefix. - Document that archive integrity verification is intentionally deferred to match the bash installer (HTTPS from GitHub releases); full checksum publish+verify for both installers tracked as a follow-up PR. - Add Pester behavioral tests (test/windows/install.Tests.ps1) covering syntax, -Help, the WOW64 fix, ARM64/x86 rejection, and unknown-version handling, plus a windows-latest CI lane in ci.yml. Verified: 6/6 pass on PowerShell 7.6.2. - Extend windows-install.test.ts to assert the ARCHITEW6432 logic and -Help block. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/ci.yml | 32 ++++++ install.ps1 | 77 ++++++++++++--- .../test/install/windows-install.test.ts | 11 +++ test/windows/install.Tests.ps1 | 98 +++++++++++++++++++ 4 files changed, 202 insertions(+), 16 deletions(-) create mode 100644 test/windows/install.Tests.ps1 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a9085dd023..a0bee8220a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,6 +23,7 @@ jobs: typescript: ${{ steps.filter.outputs.typescript }} drivers: ${{ steps.filter.outputs.drivers }} dbt-tools: ${{ steps.filter.outputs.dbt-tools }} + installer: ${{ steps.filter.outputs.installer }} steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 @@ -50,6 +51,9 @@ jobs: - 'packages/opencode/test/altimate/connections.test.ts' dbt-tools: - 'packages/dbt-tools/**' + installer: + - 'install.ps1' + - 'test/windows/**' # --------------------------------------------------------------------------- # Main TypeScript tests — excludes driver E2E tests (separate job) and @@ -309,6 +313,34 @@ jobs: run: bun run test working-directory: packages/dbt-tools + # --------------------------------------------------------------------------- + # Windows installer (install.ps1) — Pester behavioral tests on real Windows. + # Runs the script as a subprocess (stopping early via -Help / unknown version + # so nothing is downloaded) to cover arg parsing, the WOW64 arch fix, and + # unknown-version rejection. Only when install.ps1 / its tests change. + # --------------------------------------------------------------------------- + windows-installer: + name: Windows Installer (Pester) + needs: changes + if: needs.changes.outputs.installer == 'true' || github.event_name == 'push' + runs-on: windows-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + + - name: Install Pester + shell: pwsh + run: Install-Module Pester -MinimumVersion 5.0.0 -Force -Scope CurrentUser -SkipPublisherCheck + + - name: Run installer Pester tests + shell: pwsh + run: | + $config = New-PesterConfiguration + $config.Run.Path = "./test/windows/install.Tests.ps1" + $config.Run.Throw = $true + $config.Output.Verbosity = "Detailed" + Invoke-Pester -Configuration $config + # --------------------------------------------------------------------------- # dbt-tools E2E — slow (~3 min), only on push to main. # Tests dbt CLI fallbacks against real dbt versions (1.8, 1.10, 1.11) and diff --git a/install.ps1 b/install.ps1 index c992607887..bcab7223fe 100644 --- a/install.ps1 +++ b/install.ps1 @@ -20,7 +20,9 @@ param( [switch]$NoPathUpdate = $false, # Force the baseline (non-AVX2) build even if AVX2 is detected. Also used by # the illegal-instruction retry below when AVX2 detection is wrong. - [switch]$ForceBaseline = $false + [switch]$ForceBaseline = $false, + # Show usage and exit (mirrors -h/--help in the bash installer). + [switch]$Help = $false ) $ErrorActionPreference = "Stop" @@ -33,17 +35,60 @@ $InstallDir = Join-Path $env:USERPROFILE ".altimate\bin" $BinaryName = "$App.exe" $InstalledBinary = Join-Path $InstallDir $BinaryName +# All user-facing errors go through Write-Err with a uniform "error: " prefix so +# logs are greppable; informational/secondary lines use the muted Write-Muted. function Write-Muted($msg) { Write-Host $msg -ForegroundColor DarkGray } -function Write-Err($msg) { Write-Host $msg -ForegroundColor Red } +function Write-Err($msg) { Write-Host "error: $msg" -ForegroundColor Red } + +function Show-Usage { + Write-Host @" +Altimate Code Installer (Windows) + +Usage: irm https://www.altimate.sh/install.ps1 | iex + or: install.ps1 [options] + +Options: + -Help Display this help message + -Version Install a specific version (e.g. 1.0.180) + -NoPathUpdate Don't modify the user PATH + -ForceBaseline Install the non-AVX2 (baseline) build + +Examples: + powershell -c "irm https://www.altimate.sh/install.ps1 | iex" + &([scriptblock]::Create((irm https://www.altimate.sh/install.ps1))) -Version 1.0.180 +"@ +} + +if ($Help) { + Show-Usage + exit 0 +} + +# A single P/Invoke type carries both native calls we need — the AVX2 CPU probe +# (kernel32) and the PATH-change broadcast (user32) — so we Add-Type once instead +# of compiling a throwaway type per call site. +function Initialize-Native { + if (-not ("Win32.AltimateNative" -as [type])) { + Add-Type -Namespace Win32 -Name AltimateNative -MemberDefinition @" +[DllImport("kernel32.dll")] public static extern bool IsProcessorFeaturePresent(int ProcessorFeature); +[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)] +public static extern IntPtr SendMessageTimeout(IntPtr hWnd, uint Msg, UIntPtr wParam, string lParam, uint fuFlags, uint uTimeout, out UIntPtr lpdwResult); +"@ + } +} # --------------------------------------------------------------------------- # Architecture / baseline detection # --------------------------------------------------------------------------- # Only win32-x64 is built. @altimateai/altimate-core has no NAPI prebuild for # win32-arm64, so ARM64 has no standalone archive (see packages/opencode/script/build.ts). -$procArch = $env:PROCESSOR_ARCHITECTURE -if ($procArch -ne "AMD64") { - Write-Err "Unsupported OS/Arch: windows/$procArch" +# +# Under WOW64 (a 32-bit PowerShell host on 64-bit Windows) PROCESSOR_ARCHITECTURE +# reports "x86"; the true machine arch lives in PROCESSOR_ARCHITEW6432. Prefer the +# latter so a real AMD64 box isn't misdetected as unsupported x86. +$rawArch = if ($env:PROCESSOR_ARCHITEW6432) { $env:PROCESSOR_ARCHITEW6432 } else { $env:PROCESSOR_ARCHITECTURE } +if ($rawArch -ne "AMD64") { + Write-Err "Unsupported OS/Arch: windows/$rawArch" Write-Muted "The standalone Windows build is x64 (AMD64) only. On Windows-on-ARM, use WSL or npm install -g altimate-code." exit 1 } @@ -53,9 +98,8 @@ $arch = "x64" # IsProcessorFeaturePresent(40) == PF_AVX2_INSTRUCTIONS_AVAILABLE. function Test-Avx2 { try { - $sig = '[DllImport("kernel32.dll")] public static extern bool IsProcessorFeaturePresent(int ProcessorFeature);' - $k32 = Add-Type -MemberDefinition $sig -Name "AltimateKernel32" -Namespace "Win32" -PassThru - return [bool]$k32::IsProcessorFeaturePresent(40) + Initialize-Native + return [bool][Win32.AltimateNative]::IsProcessorFeaturePresent(40) } catch { # If detection fails, assume no AVX2 and fall back to the baseline build — # the baseline binary runs everywhere, an AVX2 binary on a non-AVX2 CPU crashes. @@ -89,7 +133,7 @@ if ([string]::IsNullOrWhiteSpace($Version)) { try { Invoke-WebRequest -Uri "https://github.com/AltimateAI/altimate-code/releases/tag/v$Version" -Method Head -UseBasicParsing | Out-Null } catch { - Write-Err "Error: Release v$Version not found" + Write-Err "Release v$Version not found" Write-Muted "Available releases: https://github.com/AltimateAI/altimate-code/releases" exit 1 } @@ -140,6 +184,12 @@ function Install-Target { $zipPath = Join-Path $tmpDir $filename try { + # NOTE: integrity verification (SHA256/signature) of the archive is + # intentionally deferred to match the bash installer's posture — both rely + # on HTTPS from github.com release assets. Releases do not currently publish + # a checksums file; adding one + verifying it in both installers is tracked + # as a follow-up. See PR #930 discussion. + # # Prefer curl.exe (ships with Windows 10 1803+) for a fast download with # --fail so HTTP errors don't write an error page to disk; fall back to # Invoke-WebRequest where curl.exe is unavailable. @@ -198,16 +248,11 @@ if (-not $needsBaseline) { # Write the user PATH through the registry (not setx, which truncates at 1024 # chars) and broadcast WM_SETTINGCHANGE so already-open shells pick it up. function Publish-EnvChange { - if (-not ("Win32.NativeMethods" -as [type])) { - Add-Type -Namespace Win32 -Name NativeMethods -MemberDefinition @" -[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)] -public static extern IntPtr SendMessageTimeout(IntPtr hWnd, uint Msg, UIntPtr wParam, string lParam, uint fuFlags, uint uTimeout, out UIntPtr lpdwResult); -"@ - } + Initialize-Native $HWND_BROADCAST = [IntPtr]0xffff $WM_SETTINGCHANGE = 0x1a $result = [UIntPtr]::Zero - [Win32.NativeMethods]::SendMessageTimeout($HWND_BROADCAST, $WM_SETTINGCHANGE, [UIntPtr]::Zero, "Environment", 2, 5000, [ref]$result) | Out-Null + [Win32.AltimateNative]::SendMessageTimeout($HWND_BROADCAST, $WM_SETTINGCHANGE, [UIntPtr]::Zero, "Environment", 2, 5000, [ref]$result) | Out-Null } if (-not $NoPathUpdate) { diff --git a/packages/opencode/test/install/windows-install.test.ts b/packages/opencode/test/install/windows-install.test.ts index 71d3e524f3..a1aefa51da 100644 --- a/packages/opencode/test/install/windows-install.test.ts +++ b/packages/opencode/test/install/windows-install.test.ts @@ -32,6 +32,17 @@ describe("install.ps1 — Bun exe, not npm", () => { test("rejects win32-arm64 (no NAPI prebuild)", () => { expect(PS1).toContain("Unsupported OS/Arch") }) + + test("resolves arch via PROCESSOR_ARCHITEW6432 (WOW64-safe)", () => { + // A 32-bit PowerShell host on 64-bit Windows reports PROCESSOR_ARCHITECTURE=x86; + // the real arch is in PROCESSOR_ARCHITEW6432. Must prefer the latter. + expect(PS1).toContain("PROCESSOR_ARCHITEW6432") + }) + + test("exposes a -Help/usage block", () => { + expect(PS1).toContain("[switch]$Help") + expect(PS1).toContain("Altimate Code Installer") + }) }) describe("install.ps1 — baseline (non-AVX2) parity", () => { diff --git a/test/windows/install.Tests.ps1 b/test/windows/install.Tests.ps1 new file mode 100644 index 0000000000..1008920078 --- /dev/null +++ b/test/windows/install.Tests.ps1 @@ -0,0 +1,98 @@ +# Pester behavioral tests for install.ps1 (the Windows standalone installer). +# +# These run the real script as a subprocess on Windows PowerShell so they +# exercise actual behavior — not just substring matching. They deliberately +# stop the script early (via -Help or an unknown -Version) so no 268 MB binary +# is ever downloaded, while still covering the risky branches: argument +# parsing, the WOW64 architecture fix, and unknown-version rejection. +# +# Run locally on Windows: Invoke-Pester ./test/windows/install.Tests.ps1 +# CI runs this on windows-latest (see .github/workflows/ci.yml). + +BeforeAll { + $script:InstallScript = Join-Path $PSScriptRoot "..\..\install.ps1" + + # Invoke install.ps1 in a child pwsh with a controlled environment and return + # @{ Code = ; Output = }. PROCESSOR_* env + # vars are passed per-call so we can simulate WOW64 / ARM64 hosts. + function Invoke-Installer { + param( + [string[]]$ScriptArgs = @(), + [hashtable]$Env = @{} + ) + $saved = @{} + foreach ($k in $Env.Keys) { + $saved[$k] = [Environment]::GetEnvironmentVariable($k) + [Environment]::SetEnvironmentVariable($k, $Env[$k]) + } + try { + $output = & pwsh -NoProfile -File $script:InstallScript @ScriptArgs 2>&1 | Out-String + return @{ Code = $LASTEXITCODE; Output = $output } + } finally { + foreach ($k in $Env.Keys) { + [Environment]::SetEnvironmentVariable($k, $saved[$k]) + } + } + } +} + +Describe "install.ps1 syntax" { + It "parses without errors" { + $tokens = $null; $errors = $null + [System.Management.Automation.Language.Parser]::ParseFile($script:InstallScript, [ref]$tokens, [ref]$errors) | Out-Null + $errors | Should -BeNullOrEmpty + } +} + +Describe "install.ps1 -Help" { + It "prints usage and exits 0 without installing" { + $r = Invoke-Installer -ScriptArgs @("-Help") + $r.Code | Should -Be 0 + $r.Output | Should -Match "-NoPathUpdate" + $r.Output | Should -Match "-ForceBaseline" + $r.Output | Should -Match "-Version" + } +} + +Describe "install.ps1 architecture detection" { + It "detects AMD64 under WOW64 (32-bit PowerShell on 64-bit Windows)" { + # PROCESSOR_ARCHITECTURE=x86 but PROCESSOR_ARCHITEW6432=AMD64 → real 64-bit box. + # Using an unknown version makes the script stop at the release 404 check, + # which it can only reach if the WOW64 arch check let it past. + $r = Invoke-Installer -ScriptArgs @("-Version", "0.0.0-nonexistent") -Env @{ + PROCESSOR_ARCHITECTURE = "x86" + PROCESSOR_ARCHITEW6432 = "AMD64" + } + $r.Output | Should -Not -Match "Unsupported OS/Arch" + $r.Output | Should -Match "not found" + } + + It "rejects genuine 32-bit x86 (no ARCHITEW6432)" { + $r = Invoke-Installer -ScriptArgs @("-Version", "0.0.0-nonexistent") -Env @{ + PROCESSOR_ARCHITECTURE = "x86" + PROCESSOR_ARCHITEW6432 = "" + } + $r.Code | Should -Be 1 + $r.Output | Should -Match "Unsupported OS/Arch: windows/x86" + } + + It "rejects ARM64" { + $r = Invoke-Installer -ScriptArgs @("-Version", "0.0.0-nonexistent") -Env @{ + PROCESSOR_ARCHITECTURE = "ARM64" + PROCESSOR_ARCHITEW6432 = "" + } + $r.Code | Should -Be 1 + $r.Output | Should -Match "Unsupported OS/Arch: windows/ARM64" + } +} + +Describe "install.ps1 version handling" { + It "rejects an unknown pinned version with a friendly error" { + $r = Invoke-Installer -ScriptArgs @("-Version", "0.0.0-nonexistent") -Env @{ + PROCESSOR_ARCHITECTURE = "AMD64" + } + $r.Code | Should -Be 1 + $r.Output | Should -Match "Release v0.0.0-nonexistent not found" + $r.Output | Should -Match "Available releases" + } +}