<# .SYNOPSIS Installs the native GapCode CLI for 64-bit Windows (x64 and ARM64). .EXAMPLE irm https://gapgpt.app/install.ps1 | iex .EXAMPLE $env:GAPCODE_VERSION = '0.147.14'; irm https://gapgpt.app/install.ps1 | iex #> [CmdletBinding()] param( [string]$Version = $env:GAPCODE_VERSION, [string]$InstallHome = $env:GAPCODE_HOME ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' $ProgressPreference = 'SilentlyContinue' if ($PSVersionTable.PSVersion -lt [version]'5.1') { throw 'GapCode installation requires Windows PowerShell 5.1 or newer.' } $LatestVersionUrl = 'https://gapgpt.app/api/v1/cli/latest-version' $ReleasesBaseUrl = 'https://gapgpt.app/releases/app/gapcode/releases' $LogEventUrl = 'https://gapgpt.app/api/v1/logs/event' $Target = '' $Arch = '' $InstallId = [guid]::NewGuid().ToString() $LandingSessionId = $env:GAPCODE_LANDING_SESSION_ID $InstallStage = 'bootstrap' $StartedAt = [DateTimeOffset]::UtcNow $TemporaryRoot = $null function Write-InstallProgress { param( [Parameter(Mandatory = $true)] [string]$Status, [int]$PercentComplete = 0, [switch]$Completed ) $previousProgressPreference = $ProgressPreference try { $ProgressPreference = 'Continue' if ($Completed) { Write-Progress -Activity 'Installing GapCode' -Status $Status -Completed } else { Write-Progress ` -Activity 'Installing GapCode' ` -Status $Status ` -PercentComplete $PercentComplete } } catch { # Progress rendering must never interrupt installation. } finally { $ProgressPreference = $previousProgressPreference } } function Send-InstallEvent { param( [Parameter(Mandatory = $true)] [string]$Name, [string]$Status, [string]$Reason = '' ) $duration = [Math]::Max( 0, [int]([DateTimeOffset]::UtcNow - $script:StartedAt).TotalSeconds ) $payload = @{ name = $Name data = @{ source = 'install.ps1' install_id = $script:InstallId landing_session_id = [string]$script:LandingSessionId status = $Status os = 'Windows' arch = [string]$script:Arch target = [string]$script:Target version = [string]$script:Version install_dir = [string]$script:InstallHome duration_seconds = $duration stage = $script:InstallStage reason = $Reason } links = @{} } | ConvertTo-Json -Depth 4 -Compress try { Invoke-RestMethod ` -Uri $script:LogEventUrl ` -Method Post ` -ContentType 'application/json' ` -Body $payload ` -TimeoutSec 4 | Out-Null } catch { # Telemetry must never interrupt installation. } } function Get-RequiredJsonString { param( [Parameter(Mandatory = $true)] [psobject]$Object, [Parameter(Mandatory = $true)] [string]$Name ) $property = $Object.PSObject.Properties[$Name] if ($null -eq $property -or [string]::IsNullOrWhiteSpace([string]$property.Value)) { throw "Package metadata has no $Name" } return [string]$property.Value } function Get-PackageLayout { param( [Parameter(Mandatory = $true)] [string]$Root, [Parameter(Mandatory = $true)] [string]$ExpectedVersion, [Parameter(Mandatory = $true)] [string]$ExpectedTarget ) $manifestFiles = @(Get-ChildItem ` -LiteralPath $Root ` -Filter 'codex-package.json' ` -File ` -Recurse) if ($manifestFiles.Count -ne 1) { throw 'GapCode package must contain exactly one codex-package.json' } $manifest = Get-Content ` -LiteralPath $manifestFiles[0].FullName ` -Raw | ConvertFrom-Json $packageVersion = Get-RequiredJsonString -Object $manifest -Name 'version' $packageTarget = Get-RequiredJsonString -Object $manifest -Name 'target' $entrypointRelative = Get-RequiredJsonString -Object $manifest -Name 'entrypoint' $resourcesRelative = Get-RequiredJsonString -Object $manifest -Name 'resourcesDir' $pathRelative = Get-RequiredJsonString -Object $manifest -Name 'pathDir' if ($packageVersion -ne $ExpectedVersion -or $packageTarget -ne $ExpectedTarget) { throw "Package is $packageVersion/$packageTarget, expected $ExpectedVersion/$ExpectedTarget" } $packageRoot = $manifestFiles[0].DirectoryName $requiredFiles = @( $entrypointRelative, 'bin/codex-code-mode-host.exe', "$resourcesRelative/codex-command-runner.exe", "$resourcesRelative/codex-windows-sandbox-setup.exe", "$pathRelative/rg.exe" ) foreach ($relativePath in $requiredFiles) { $windowsRelativePath = $relativePath.Replace('/', [IO.Path]::DirectorySeparatorChar) if (-not (Test-Path -LiteralPath (Join-Path $packageRoot $windowsRelativePath) -PathType Leaf)) { throw "GapCode package is incomplete: missing $relativePath" } } return [PSCustomObject]@{ Root = $packageRoot EntrypointRelative = $entrypointRelative } } function Get-ReusableRelease { param( [Parameter(Mandatory = $true)] [string]$ReleasesDirectory, [Parameter(Mandatory = $true)] [string]$ExpectedVersion, [Parameter(Mandatory = $true)] [string]$ExpectedTarget ) if (-not (Test-Path -LiteralPath $ReleasesDirectory -PathType Container)) { return $null } $releasePrefix = "$ExpectedVersion-$ExpectedTarget" $candidateDirectories = @(Get-ChildItem ` -LiteralPath $ReleasesDirectory ` -Directory ` -ErrorAction SilentlyContinue | Where-Object { $_.Name.Equals($releasePrefix, [StringComparison]::OrdinalIgnoreCase) -or $_.Name.StartsWith("$releasePrefix-", [StringComparison]::OrdinalIgnoreCase) } | Sort-Object LastWriteTimeUtc -Descending) foreach ($candidateDirectory in $candidateDirectories) { try { $layout = Get-PackageLayout ` -Root $candidateDirectory.FullName ` -ExpectedVersion $ExpectedVersion ` -ExpectedTarget $ExpectedTarget if (-not [string]::Equals( $layout.Root, $candidateDirectory.FullName, [StringComparison]::OrdinalIgnoreCase )) { continue } $entrypoint = Join-Path ` $candidateDirectory.FullName ` $layout.EntrypointRelative.Replace('/', [IO.Path]::DirectorySeparatorChar) $versionOutput = (& $entrypoint --version 2>&1 | Out-String).Trim() if ($LASTEXITCODE -ne 0 -or $versionOutput -notmatch [regex]::Escape($ExpectedVersion)) { continue } return [PSCustomObject]@{ Name = $candidateDirectory.Name Directory = $candidateDirectory.FullName EntrypointRelative = $layout.EntrypointRelative } } catch { # Ignore incomplete releases and install a verified package instead. } } return $null } function Invoke-DownloadFile { param( [Parameter(Mandatory = $true)] [string]$Uri, [Parameter(Mandatory = $true)] [string]$OutFile ) for ($attempt = 1; $attempt -le 3; $attempt += 1) { try { Invoke-WebRequest -Uri $Uri -OutFile $OutFile -UseBasicParsing return } catch { if ($attempt -eq 3) { throw } Write-Warning "Download failed; retrying (attempt $($attempt + 1) of 3)..." Start-Sleep -Seconds 1 } } } function Add-ToUserPath { param( [Parameter(Mandatory = $true)] [string]$Directory ) $normalizedDirectory = $Directory.TrimEnd([IO.Path]::DirectorySeparatorChar) $userPath = [Environment]::GetEnvironmentVariable('Path', 'User') $alreadyPresent = @($userPath -split ';' | Where-Object { [string]::Equals( $_.Trim().TrimEnd([IO.Path]::DirectorySeparatorChar), $normalizedDirectory, [StringComparison]::OrdinalIgnoreCase ) }).Count -gt 0 if (-not $alreadyPresent) { $updatedPath = if ([string]::IsNullOrWhiteSpace($userPath)) { $Directory } else { "$userPath;$Directory" } [Environment]::SetEnvironmentVariable('Path', $updatedPath, 'User') } $presentInProcess = @($env:Path -split ';' | Where-Object { [string]::Equals( $_.Trim().TrimEnd([IO.Path]::DirectorySeparatorChar), $normalizedDirectory, [StringComparison]::OrdinalIgnoreCase ) }).Count -gt 0 if (-not $presentInProcess) { $env:Path = "$Directory;$env:Path" } } try { Write-InstallProgress -Status 'Detecting Windows architecture' -PercentComplete 5 Send-InstallEvent -Name 'gapcode_install_started' -Status 'started' $InstallStage = 'detect_target' if ($env:OS -ne 'Windows_NT') { throw 'This installer is for native Windows. Use install.sh on macOS or Linux.' } $nativeArchitecture = if ($env:PROCESSOR_ARCHITEW6432) { $env:PROCESSOR_ARCHITEW6432 } else { $env:PROCESSOR_ARCHITECTURE } if ($nativeArchitecture -in @('AMD64', 'x86_64')) { $Target = 'x86_64-pc-windows-msvc' $Arch = 'x64' } elseif ($nativeArchitecture -in @('ARM64', 'aarch64')) { $Target = 'aarch64-pc-windows-msvc' $Arch = 'arm64' } else { throw "Native GapCode supports x64 and ARM64 Windows only; detected $nativeArchitecture" } Write-InstallProgress -Status 'Preparing the install directory' -PercentComplete 10 $userProfile = [Environment]::GetFolderPath('UserProfile') if ([string]::IsNullOrWhiteSpace($InstallHome)) { $InstallHome = Join-Path $userProfile '.gapcode' } else { $InstallHome = [Environment]::ExpandEnvironmentVariables($InstallHome) if ($InstallHome -eq '~') { $InstallHome = $userProfile } elseif ($InstallHome.StartsWith('~\') -or $InstallHome.StartsWith('~/')) { $InstallHome = Join-Path $userProfile $InstallHome.Substring(2) } } $InstallHome = [IO.Path]::GetFullPath($InstallHome) $InstallStage = 'resolve_version' Write-InstallProgress -Status 'Resolving the GapCode version' -PercentComplete 15 if ([string]::IsNullOrWhiteSpace($Version)) { $latest = Invoke-RestMethod -Uri $LatestVersionUrl -Method Get $versionProperty = $latest.PSObject.Properties['version'] if ($null -eq $versionProperty) { throw 'Latest-version response has no version field' } $Version = [string]$versionProperty.Value } if ($Version -notmatch '^[0-9]+\.[0-9]+\.[0-9]+(?:[+-][0-9A-Za-z.-]+)?$') { throw "Invalid GapCode version: $Version" } $archiveName = "gapcode-$Target.tar.gz" $versionRoot = "$ReleasesBaseUrl/v$Version" $archiveUrl = "$versionRoot/$archiveName" $TemporaryRoot = Join-Path ` ([IO.Path]::GetTempPath()) ` ("gapcode-install-" + [guid]::NewGuid().ToString('N')) $extractDirectory = Join-Path $TemporaryRoot 'package' $archivePath = Join-Path $TemporaryRoot $archiveName New-Item -ItemType Directory -Path $extractDirectory -Force | Out-Null $InstallStage = 'download_checksums' Write-InstallProgress -Status 'Downloading release checksums' -PercentComplete 20 $checksumResponse = Invoke-WebRequest ` -Uri "$versionRoot/SHA256SUMS" ` -UseBasicParsing $checksumText = if ($checksumResponse.Content -is [byte[]]) { [Text.Encoding]::UTF8.GetString($checksumResponse.Content) } else { [string]$checksumResponse.Content } $escapedArchiveName = [regex]::Escape($archiveName) $checksumMatches = [regex]::Matches( $checksumText, "(?im)^([a-f0-9]{64})\s+\*?$escapedArchiveName\s*$" ) if ($checksumMatches.Count -ne 1) { throw "SHA256SUMS does not contain exactly one $archiveName entry" } $expectedChecksum = $checksumMatches[0].Groups[1].Value.ToLowerInvariant() $InstallStage = 'download_archive' Write-InstallProgress -Status 'Downloading the GapCode package' -PercentComplete 30 Write-Host "Downloading GapCode v$Version for native Windows $Arch..." -ForegroundColor Cyan Invoke-DownloadFile -Uri $archiveUrl -OutFile $archivePath $InstallStage = 'verify_archive' Write-InstallProgress -Status 'Verifying the package checksum' -PercentComplete 65 $actualChecksum = (Get-FileHash -Path $archivePath -Algorithm SHA256).Hash.ToLowerInvariant() if ($actualChecksum -ne $expectedChecksum) { throw "Archive checksum mismatch: expected $expectedChecksum, got $actualChecksum" } $tar = Get-Command tar.exe -CommandType Application -ErrorAction SilentlyContinue if ($null -eq $tar) { throw 'Windows tar.exe is required. Install current Windows updates and try again.' } $InstallStage = 'extract_archive' Write-InstallProgress -Status 'Extracting the GapCode package' -PercentComplete 75 & $tar.Source -xzf $archivePath -C $extractDirectory if ($LASTEXITCODE -ne 0) { throw "tar.exe could not extract the GapCode archive (exit code $LASTEXITCODE)" } Write-InstallProgress -Status 'Validating the package contents' -PercentComplete 82 $packageLayout = Get-PackageLayout ` -Root $extractDirectory ` -ExpectedVersion $Version ` -ExpectedTarget $Target $packageRoot = $packageLayout.Root $entrypointRelative = $packageLayout.EntrypointRelative $InstallStage = 'install_package' Write-InstallProgress -Status 'Selecting the installed release' -PercentComplete 88 $releasesDirectory = Join-Path $InstallHome 'packages\standalone\releases' New-Item -ItemType Directory -Path $releasesDirectory -Force | Out-Null $reusableRelease = Get-ReusableRelease ` -ReleasesDirectory $releasesDirectory ` -ExpectedVersion $Version ` -ExpectedTarget $Target if ($null -ne $reusableRelease) { $releaseName = $reusableRelease.Name $releaseDirectory = $reusableRelease.Directory $entrypointRelative = $reusableRelease.EntrypointRelative Write-Host "Reusing existing GapCode release $releaseName." -ForegroundColor Cyan } else { $releaseName = "$Version-$Target" $releaseDirectory = Join-Path $releasesDirectory $releaseName if (Test-Path -LiteralPath $releaseDirectory) { $releaseName = "$releaseName-$($InstallId.Replace('-', ''))" $releaseDirectory = Join-Path $releasesDirectory $releaseName } Move-Item -LiteralPath $packageRoot -Destination $releaseDirectory } $installedEntrypoint = Join-Path ` $releaseDirectory ` $entrypointRelative.Replace('/', [IO.Path]::DirectorySeparatorChar) $InstallStage = 'verify_installation' Write-InstallProgress -Status 'Verifying the installed executable' -PercentComplete 93 $versionOutput = (& $installedEntrypoint --version 2>&1 | Out-String).Trim() if ($LASTEXITCODE -ne 0 -or $versionOutput -notmatch [regex]::Escape($Version)) { throw "Installed GapCode failed its version check: $versionOutput" } $InstallStage = 'activate_installation' Write-InstallProgress -Status 'Updating the GapCode launcher' -PercentComplete 96 $binDirectory = Join-Path $InstallHome 'bin' $launcherPath = Join-Path $binDirectory 'gapcode.cmd' New-Item -ItemType Directory -Path $binDirectory -Force | Out-Null $launcherEntrypoint = "..\packages\standalone\releases\$releaseName\$($entrypointRelative.Replace('/', '\'))" [IO.File]::WriteAllLines( $launcherPath, @( '@echo off', 'for %%I in ("%~dp0..") do set "GAPCODE_HOME=%%~fI"', "`"%~dp0$launcherEntrypoint`" %*" ), [Text.Encoding]::ASCII ) $InstallStage = 'configure_path' Write-InstallProgress -Status 'Updating your user PATH' -PercentComplete 98 Add-ToUserPath -Directory $binDirectory $env:GAPCODE_HOME = $InstallHome Write-InstallProgress -Status 'Installation complete' -PercentComplete 100 Send-InstallEvent -Name 'gapcode_install_succeeded' -Status 'success' Write-InstallProgress -Status 'Installation complete' -Completed Write-Host "Installed GapCode v$Version for native Windows $Arch." -ForegroundColor Green Write-Host "Open a new PowerShell window and run: gapcode" } catch { $reason = $_.Exception.Message Send-InstallEvent -Name 'gapcode_install_failed' -Status 'failed' -Reason $reason Write-InstallProgress -Status 'Installation failed' -Completed throw "GapCode installation failed during $InstallStage`: $reason" } finally { if ($null -ne $TemporaryRoot -and (Test-Path -LiteralPath $TemporaryRoot)) { Remove-Item -LiteralPath $TemporaryRoot -Recurse -Force -ErrorAction SilentlyContinue } }