# Forest CLI installer (Windows) # # irm https://releases.forest.dev/install.ps1 | iex # # Or download and run with options: # .\install.ps1 -Version v0.1.0 # .\install.ps1 -Uninstall # # Env overrides: FOREST_INSTALL_BASE, FOREST_INSTALL_DIR # # Consumes the same R2 artifacts as the GUI installer (cli-installer repo): # manifest at cli/latest/latest.json, sha256-verified binaries. Installs # per-user (no admin) to %LOCALAPPDATA%\forest-cli\bin — the same directory # and HKCU PATH behavior as the GUI installer, so the channels are # interchangeable. [CmdletBinding()] param( [string]$Version = "", [switch]$Uninstall ) $ErrorActionPreference = "Stop" $Base = if ($env:FOREST_INSTALL_BASE) { $env:FOREST_INSTALL_BASE } else { "https://releases.forest.dev" } $InstallDir = if ($env:FOREST_INSTALL_DIR) { $env:FOREST_INSTALL_DIR } else { Join-Path $env:LOCALAPPDATA "forest-cli\bin" } $BinName = "forest.exe" $ManifestPrefix = "cli" $Target = "x86_64-pc-windows-msvc" function Write-Step($msg) { Write-Host $msg -ForegroundColor Cyan } # PowerShell 5.1 can default to old TLS; force modern. [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 # ── uninstall ──────────────────────────────────────────────────────────────── if ($Uninstall) { $binPath = Join-Path $InstallDir $BinName if (Test-Path $binPath) { Remove-Item $binPath -Force Write-Host "Removed $binPath" } else { Write-Host "Nothing to uninstall at $binPath" } $userPath = [Environment]::GetEnvironmentVariable("Path", "User") if ($userPath -and (($userPath -split ";") -contains $InstallDir)) { $newPath = (($userPath -split ";") | Where-Object { $_ -ne $InstallDir }) -join ";" [Environment]::SetEnvironmentVariable("Path", $newPath, "User") Write-Host "Removed $InstallDir from user PATH" } return } # ── arch check ─────────────────────────────────────────────────────────────── $arch = $env:PROCESSOR_ARCHITECTURE if ($arch -eq "ARM64") { # No native ARM64 build yet; the x64 binary runs under Windows 11 emulation. Write-Host "Note: no native ARM64 build yet - installing x64 (runs under emulation)." -ForegroundColor Yellow } elseif ($arch -ne "AMD64") { throw "Unsupported architecture: $arch (only x64 builds are published right now)" } # ── resolve version + file entry from manifest ─────────────────────────────── if ($Version) { $manifestUrl = "$Base/$ManifestPrefix/$Version/latest.json" } else { $manifestUrl = "$Base/$ManifestPrefix/latest/latest.json" } Write-Step "Fetching release manifest..." $manifest = Invoke-RestMethod -Uri $manifestUrl -UseBasicParsing $entry = $manifest.files | Where-Object { $_.target -eq $Target } | Select-Object -First 1 if (-not $entry) { throw "No $Target build found in release $($manifest.tag)" } # Manifest entries carry a full url when CDN_BASE was set at release time # (the GUI installer relies on it); fall back to base + key otherwise. $downloadUrl = if ($entry.url) { $entry.url } else { "$Base/$($entry.key)" } Write-Step "Installing forest $($manifest.tag) ($Target)..." # ── download + verify ──────────────────────────────────────────────────────── $tmp = Join-Path $env:TEMP ("forest-install-" + [Guid]::NewGuid().ToString("N")) New-Item -ItemType Directory -Path $tmp | Out-Null try { $exePath = Join-Path $tmp $BinName Invoke-WebRequest -Uri $downloadUrl -OutFile $exePath -UseBasicParsing $actual = (Get-FileHash $exePath -Algorithm SHA256).Hash.ToLowerInvariant() $expected = $entry.sha256.ToLowerInvariant() if ($actual -ne $expected) { throw "sha256 mismatch (expected $expected, got $actual) - aborting" } # ── install ────────────────────────────────────────────────────────────── New-Item -ItemType Directory -Force -Path $InstallDir | Out-Null Move-Item $exePath (Join-Path $InstallDir $BinName) -Force } finally { Remove-Item $tmp -Recurse -Force -ErrorAction SilentlyContinue } # ── PATH setup (HKCU, same as the GUI installer) ───────────────────────────── $userPath = [Environment]::GetEnvironmentVariable("Path", "User") if (-not $userPath) { $userPath = "" } if (-not (($userPath -split ";") -contains $InstallDir)) { $newPath = if ($userPath) { "$userPath;$InstallDir" } else { $InstallDir } # SetEnvironmentVariable broadcasts WM_SETTINGCHANGE, so new terminals pick it up [Environment]::SetEnvironmentVariable("Path", $newPath, "User") Write-Host "Added $InstallDir to user PATH" } # Make it available in the current session too if (-not (($env:Path -split ";") -contains $InstallDir)) { $env:Path = "$env:Path;$InstallDir" } Write-Host "" Write-Host "forest $($manifest.tag) installed to $InstallDir\$BinName" -ForegroundColor Green Write-Host "Open a new terminal (or use this one) and run: forest --help"