Download scripts/install/install.ps1 from SaylorTwift/codex: direct link, hf CLI and curl.
- Browser
- Download file 43.8 kB
-
https://huggingface.co/SaylorTwift/codex/resolve/main/scripts/install/install.ps1
- Command line
-
hf download hf://SaylorTwift/codex/scripts/install/install.ps1
-
curl -L -o install.ps1 https://huggingface.co/SaylorTwift/codex/resolve/main/scripts/install/install.ps1
43.8 kB
| [CmdletBinding()] | |
| param( | |
| [string]$Release = $env:CODEX_RELEASE | |
| ) | |
| Set-StrictMode -Version Latest | |
| $ErrorActionPreference = "Stop" | |
| $ProgressPreference = "SilentlyContinue" | |
| if ([string]::IsNullOrWhiteSpace($Release)) { | |
| $Release = "latest" | |
| } | |
| $NonInteractive = $env:CODEX_NON_INTERACTIVE -match "^(?i:1|true|yes)$" | |
| $DefaultPreferReleasesOpenAICom = $true | |
| $PreferReleasesOpenAICom = if ([string]::IsNullOrWhiteSpace($env:CODEX_INSTALLER_USE_RELEASES_OPENAI_COM)) { | |
| $DefaultPreferReleasesOpenAICom | |
| } else { | |
| $env:CODEX_INSTALLER_USE_RELEASES_OPENAI_COM -match "^(?i:1|true|yes)$" | |
| } | |
| $ReleasesBaseUri = "https://releases.openai.com/codex" | |
| $ReleasesMetadataTimeoutSec = 30 | |
| $ReleasesAssetTimeoutSec = 300 | |
| function Write-Step { | |
| param( | |
| [string]$Message | |
| ) | |
| Write-Host "==> $Message" | |
| } | |
| function Write-WarningStep { | |
| param( | |
| [string]$Message | |
| ) | |
| Write-Warning $Message | |
| } | |
| function Prompt-YesNo { | |
| param( | |
| [string]$Prompt | |
| ) | |
| if ($NonInteractive) { | |
| return $false | |
| } | |
| if ([Console]::IsInputRedirected -or [Console]::IsOutputRedirected) { | |
| return $false | |
| } | |
| $choice = Read-Host "$Prompt [y/N]" | |
| return $choice -match "^(?i:y(?:es)?)$" | |
| } | |
| function Normalize-Version { | |
| param( | |
| [string]$RawVersion | |
| ) | |
| if ([string]::IsNullOrWhiteSpace($RawVersion) -or $RawVersion -eq "latest") { | |
| return "latest" | |
| } | |
| if ($RawVersion.StartsWith("rust-v")) { | |
| return $RawVersion.Substring(6) | |
| } | |
| if ($RawVersion.StartsWith("v")) { | |
| return $RawVersion.Substring(1) | |
| } | |
| return $RawVersion | |
| } | |
| function Assert-ValidReleaseVersion { | |
| param( | |
| [string]$Version | |
| ) | |
| if ($Version -cne "latest" -and $Version -cnotmatch "^[0-9]+\.[0-9]+\.[0-9]+(?:-alpha(?:\.[0-9]+){0,2}|-beta(?:\.[0-9]+)?)?$") { | |
| throw "Invalid Codex release version: $Version. Expected latest or x.y.z[-alpha[.N[.M]]|-beta[.N]]." | |
| } | |
| } | |
| function Find-ReleaseAssetMetadata { | |
| param( | |
| [string]$AssetName, | |
| [object]$ReleaseMetadata, | |
| [string]$Url = $null, | |
| [string]$FallbackUrl = $null | |
| ) | |
| $asset = $ReleaseMetadata.assets | Where-Object { $_.name -eq $AssetName } | Select-Object -First 1 | |
| if ($null -eq $asset) { | |
| return $null | |
| } | |
| $digestMatch = [regex]::Match([string]$asset.digest, "^sha256:([0-9a-fA-F]{64})$") | |
| if (-not $digestMatch.Success) { | |
| throw "Could not find SHA-256 digest for release asset $AssetName." | |
| } | |
| return [PSCustomObject]@{ | |
| Url = if ([string]::IsNullOrWhiteSpace($Url)) { $asset.browser_download_url } else { $Url } | |
| FallbackUrl = $FallbackUrl | |
| Sha256 = $digestMatch.Groups[1].Value.ToLowerInvariant() | |
| } | |
| } | |
| function Invoke-WebRequestWithFallback { | |
| param( | |
| [object]$Metadata, | |
| [string]$OutFile, | |
| [string]$ExpectedDigest, | |
| [string]$AssetName, | |
| [string]$ReleaseVersion, | |
| [string]$RequiredManifestAsset | |
| ) | |
| try { | |
| if ($Metadata.Url.StartsWith("$ReleasesBaseUri/", [System.StringComparison]::OrdinalIgnoreCase)) { | |
| Invoke-WebRequest -UseBasicParsing -Uri $Metadata.Url -OutFile $OutFile -TimeoutSec $ReleasesAssetTimeoutSec | |
| } else { | |
| Invoke-WebRequest -UseBasicParsing -Uri $Metadata.Url -OutFile $OutFile | |
| } | |
| Test-ArchiveDigest -ArchivePath $OutFile -ExpectedDigest $ExpectedDigest | |
| if (-not [string]::IsNullOrWhiteSpace($RequiredManifestAsset)) { | |
| $null = Get-PackageArchiveDigest -ManifestPath $OutFile -AssetName $RequiredManifestAsset | |
| } | |
| } catch { | |
| if ([string]::IsNullOrWhiteSpace($Metadata.FallbackUrl)) { | |
| throw | |
| } | |
| Write-WarningStep "Could not download or verify $($Metadata.Url); retrying from GitHub Releases." | |
| Invoke-WebRequest -UseBasicParsing -Uri $Metadata.FallbackUrl -OutFile $OutFile | |
| try { | |
| Test-ArchiveDigest -ArchivePath $OutFile -ExpectedDigest $ExpectedDigest | |
| if (-not [string]::IsNullOrWhiteSpace($RequiredManifestAsset)) { | |
| $null = Get-PackageArchiveDigest -ManifestPath $OutFile -AssetName $RequiredManifestAsset | |
| } | |
| } catch { | |
| $githubRelease = Resolve-ReleaseFromGitHub -NormalizedVersion $ReleaseVersion | |
| $githubAssetMetadata = Find-ReleaseAssetMetadata -AssetName $AssetName -ReleaseMetadata $githubRelease.Metadata | |
| if ($null -eq $githubAssetMetadata) { | |
| throw "Could not find GitHub release metadata for asset $AssetName." | |
| } | |
| Test-ArchiveDigest -ArchivePath $OutFile -ExpectedDigest $githubAssetMetadata.Sha256 | |
| if (-not [string]::IsNullOrWhiteSpace($RequiredManifestAsset)) { | |
| $null = Get-PackageArchiveDigest -ManifestPath $OutFile -AssetName $RequiredManifestAsset | |
| } | |
| } | |
| } | |
| } | |
| function Resolve-ReleaseAssetSelection { | |
| param( | |
| [object]$ResolvedRelease, | |
| [string]$Target, | |
| [string]$NpmTag | |
| ) | |
| $version = $ResolvedRelease.Version | |
| $releaseMetadata = $ResolvedRelease.Metadata | |
| $packageAsset = "codex-package-$Target.tar.gz" | |
| $checksumAsset = "codex-package_SHA256SUMS" | |
| $packageUrl = $null | |
| $packageFallbackUrl = $null | |
| $checksumUrl = $null | |
| $checksumFallbackUrl = $null | |
| if ($ResolvedRelease.Source -eq "ReleasesOpenAICom") { | |
| $packageUrl = "$ReleasesBaseUri/releases/$version/$packageAsset" | |
| $packageFallbackUrl = "https://github.com/openai/codex/releases/download/rust-v$version/$packageAsset" | |
| $checksumUrl = "$ReleasesBaseUri/releases/$version/$checksumAsset" | |
| $checksumFallbackUrl = "https://github.com/openai/codex/releases/download/rust-v$version/$checksumAsset" | |
| } | |
| $packageMetadata = Find-ReleaseAssetMetadata -AssetName $packageAsset -ReleaseMetadata $releaseMetadata -Url $packageUrl -FallbackUrl $packageFallbackUrl | |
| $checksumMetadata = Find-ReleaseAssetMetadata -AssetName $checksumAsset -ReleaseMetadata $releaseMetadata -Url $checksumUrl -FallbackUrl $checksumFallbackUrl | |
| if ($null -ne $packageMetadata -and $null -ne $checksumMetadata) { | |
| return [PSCustomObject]@{ | |
| PackageAsset = $packageAsset | |
| PackageMetadata = $packageMetadata | |
| ChecksumMetadata = $checksumMetadata | |
| InstallLayout = "Package" | |
| } | |
| } | |
| $packageAsset = "codex-npm-$NpmTag-$version.tgz" | |
| $packageUrl = $null | |
| $packageFallbackUrl = $null | |
| if ($ResolvedRelease.Source -eq "ReleasesOpenAICom") { | |
| $packageUrl = "$ReleasesBaseUri/releases/$version/$packageAsset" | |
| $packageFallbackUrl = "https://github.com/openai/codex/releases/download/rust-v$version/$packageAsset" | |
| } | |
| $packageMetadata = Find-ReleaseAssetMetadata -AssetName $packageAsset -ReleaseMetadata $releaseMetadata -Url $packageUrl -FallbackUrl $packageFallbackUrl | |
| if ($null -eq $packageMetadata) { | |
| throw "Could not find Codex package or platform npm release assets for Codex $version." | |
| } | |
| return [PSCustomObject]@{ | |
| PackageAsset = $packageAsset | |
| PackageMetadata = $packageMetadata | |
| ChecksumMetadata = $null | |
| InstallLayout = "LegacyPlatformNpm" | |
| } | |
| } | |
| function Test-ArchiveDigest { | |
| param( | |
| [string]$ArchivePath, | |
| [string]$ExpectedDigest | |
| ) | |
| $actualDigest = (Get-FileHash -LiteralPath $ArchivePath -Algorithm SHA256).Hash.ToLowerInvariant() | |
| if ($actualDigest -ne $ExpectedDigest) { | |
| throw "Downloaded Codex archive checksum did not match expected digest. Expected $ExpectedDigest but got $actualDigest." | |
| } | |
| } | |
| function Get-PackageArchiveDigest { | |
| param( | |
| [string]$ManifestPath, | |
| [string]$AssetName | |
| ) | |
| $escapedAssetName = [regex]::Escape($AssetName) | |
| foreach ($line in Get-Content -LiteralPath $ManifestPath) { | |
| $match = [regex]::Match($line, "^\s*([0-9a-fA-F]{64})\s+$escapedAssetName\s*$") | |
| if ($match.Success) { | |
| return $match.Groups[1].Value.ToLowerInvariant() | |
| } | |
| } | |
| throw "Could not find SHA-256 digest for $AssetName in codex-package_SHA256SUMS." | |
| } | |
| function Path-Contains { | |
| param( | |
| [string]$PathValue, | |
| [string]$Entry | |
| ) | |
| if ([string]::IsNullOrWhiteSpace($PathValue)) { | |
| return $false | |
| } | |
| $needle = $Entry.TrimEnd("\") | |
| foreach ($segment in $PathValue.Split(";", [System.StringSplitOptions]::RemoveEmptyEntries)) { | |
| if ($segment.TrimEnd("\") -ieq $needle) { | |
| return $true | |
| } | |
| } | |
| return $false | |
| } | |
| function Prepend-PathEntry { | |
| param( | |
| [string]$PathValue, | |
| [string]$Entry | |
| ) | |
| $needle = $Entry.TrimEnd("\") | |
| $segments = @($Entry) | |
| if (-not [string]::IsNullOrWhiteSpace($PathValue)) { | |
| $segments += $PathValue.Split(";", [System.StringSplitOptions]::RemoveEmptyEntries) | | |
| Where-Object { $_.TrimEnd("\") -ine $needle } | |
| } | |
| return ($segments -join ";") | |
| } | |
| function Invoke-WithInstallLock { | |
| param( | |
| [string]$LockPath, | |
| [scriptblock]$Script | |
| ) | |
| New-Item -ItemType Directory -Force -Path (Split-Path -Parent $LockPath) | Out-Null | |
| $lock = $null | |
| while ($null -eq $lock) { | |
| try { | |
| $lock = [System.IO.File]::Open( | |
| $LockPath, | |
| [System.IO.FileMode]::OpenOrCreate, | |
| [System.IO.FileAccess]::ReadWrite, | |
| [System.IO.FileShare]::None | |
| ) | |
| } catch [System.IO.IOException] { | |
| Start-Sleep -Milliseconds 250 | |
| } | |
| } | |
| try { | |
| & $Script | |
| } finally { | |
| $lock.Dispose() | |
| } | |
| } | |
| function Remove-StaleInstallArtifacts { | |
| param( | |
| [string]$ReleasesDir | |
| ) | |
| if (Test-Path -LiteralPath $ReleasesDir -PathType Container) { | |
| Get-ChildItem -LiteralPath $ReleasesDir -Force -Directory -Filter ".staging.*" -ErrorAction SilentlyContinue | | |
| Remove-Item -Recurse -Force -ErrorAction SilentlyContinue | |
| } | |
| } | |
| function Resolve-VersionFromReleaseMetadata { | |
| param( | |
| [object]$ReleaseMetadata | |
| ) | |
| if (-not $ReleaseMetadata.tag_name) { | |
| throw "Failed to resolve the latest Codex release version." | |
| } | |
| $resolvedVersion = Normalize-Version -RawVersion $ReleaseMetadata.tag_name | |
| Assert-ValidReleaseVersion -Version $resolvedVersion | |
| return $resolvedVersion | |
| } | |
| function Resolve-ReleaseFromGitHub { | |
| param( | |
| [string]$NormalizedVersion | |
| ) | |
| if ($NormalizedVersion -eq "latest") { | |
| $requestedRelease = "latest" | |
| $metadataUri = "https://api.github.com/repos/openai/codex/releases/latest" | |
| } else { | |
| $resolvedVersion = $NormalizedVersion | |
| $requestedRelease = $resolvedVersion | |
| $metadataUri = "https://api.github.com/repos/openai/codex/releases/tags/rust-v$resolvedVersion" | |
| } | |
| try { | |
| $releaseMetadata = Invoke-RestMethod -Uri $metadataUri | |
| } catch { | |
| throw "Could not fetch GitHub release metadata for Codex $requestedRelease. GitHub API may be unavailable or rate limited. $($_.Exception.Message)" | |
| } | |
| if ($NormalizedVersion -eq "latest") { | |
| $resolvedVersion = Resolve-VersionFromReleaseMetadata -ReleaseMetadata $releaseMetadata | |
| } | |
| return [PSCustomObject]@{ | |
| Version = $resolvedVersion | |
| Metadata = $releaseMetadata | |
| Source = "GitHub" | |
| } | |
| } | |
| function Resolve-ReleaseFromReleases { | |
| param( | |
| [string]$NormalizedVersion | |
| ) | |
| $metadataUri = if ($NormalizedVersion -eq "latest") { | |
| "$ReleasesBaseUri/channels/latest" | |
| } else { | |
| "$ReleasesBaseUri/releases/$NormalizedVersion/release.json" | |
| } | |
| try { | |
| $metadataResponse = Invoke-WebRequest -UseBasicParsing -Uri $metadataUri -TimeoutSec $ReleasesMetadataTimeoutSec | |
| $releaseMetadata = [string]$metadataResponse.Content | ConvertFrom-Json -ErrorAction Stop | |
| $resolvedVersion = Resolve-VersionFromReleaseMetadata -ReleaseMetadata $releaseMetadata | |
| if ($NormalizedVersion -ne "latest" -and $resolvedVersion -cne $NormalizedVersion) { | |
| throw "Release metadata version did not match requested Codex version $NormalizedVersion." | |
| } | |
| $resolvedRelease = [PSCustomObject]@{ | |
| Version = $resolvedVersion | |
| Metadata = $releaseMetadata | |
| Source = "ReleasesOpenAICom" | |
| } | |
| $null = Resolve-ReleaseAssetSelection -ResolvedRelease $resolvedRelease -Target $target -NpmTag $npmTag | |
| } catch { | |
| return $null | |
| } | |
| return $resolvedRelease | |
| } | |
| function Resolve-Release { | |
| $normalizedVersion = Normalize-Version -RawVersion $Release | |
| Assert-ValidReleaseVersion -Version $normalizedVersion | |
| if ($PreferReleasesOpenAICom) { | |
| $release = Resolve-ReleaseFromReleases -NormalizedVersion $normalizedVersion | |
| if ($null -ne $release) { | |
| return $release | |
| } | |
| Write-WarningStep "releases.openai.com is unavailable; falling back to GitHub Releases." | |
| } | |
| return Resolve-ReleaseFromGitHub -NormalizedVersion $normalizedVersion | |
| } | |
| function Get-VersionFromBinary { | |
| param( | |
| [string]$CodexPath | |
| ) | |
| if (-not (Test-Path -LiteralPath $CodexPath -PathType Leaf)) { | |
| return $null | |
| } | |
| try { | |
| $versionOutput = & $CodexPath --version 2>$null | |
| } catch { | |
| return $null | |
| } | |
| if ($versionOutput -match '([0-9][0-9A-Za-z.+-]*)$') { | |
| return $matches[1] | |
| } | |
| return $null | |
| } | |
| function Get-CurrentInstalledVersion { | |
| param( | |
| [string]$StandaloneCurrentDir | |
| ) | |
| $standaloneVersion = Get-VersionFromBinary -CodexPath (Join-Path $StandaloneCurrentDir "bin\codex.exe") | |
| if (-not [string]::IsNullOrWhiteSpace($standaloneVersion)) { | |
| return $standaloneVersion | |
| } | |
| $standaloneVersion = Get-VersionFromBinary -CodexPath (Join-Path $StandaloneCurrentDir "codex.exe") | |
| if (-not [string]::IsNullOrWhiteSpace($standaloneVersion)) { | |
| return $standaloneVersion | |
| } | |
| return $null | |
| } | |
| function Test-OldStandaloneBinLayout { | |
| param( | |
| [string]$VisibleBinDir, | |
| [string]$DefaultVisibleBinDir | |
| ) | |
| if (-not $VisibleBinDir.Equals($DefaultVisibleBinDir, [System.StringComparison]::OrdinalIgnoreCase)) { | |
| return $false | |
| } | |
| if (-not (Test-Path -LiteralPath $VisibleBinDir -PathType Container)) { | |
| return $false | |
| } | |
| $item = Get-Item -LiteralPath $VisibleBinDir -Force | |
| if ($item.Attributes -band [IO.FileAttributes]::ReparsePoint) { | |
| return $false | |
| } | |
| $requiredFiles = @("codex.exe", "rg.exe") | |
| foreach ($fileName in $requiredFiles) { | |
| if (-not (Test-Path -LiteralPath (Join-Path $VisibleBinDir $fileName) -PathType Leaf)) { | |
| return $false | |
| } | |
| } | |
| $knownFiles = @( | |
| "codex.exe", | |
| "rg.exe", | |
| "codex-command-runner.exe", | |
| "codex-windows-sandbox.exe", | |
| "codex-windows-sandbox-setup.exe" | |
| ) | |
| foreach ($child in Get-ChildItem -LiteralPath $VisibleBinDir -Force) { | |
| if ($child.PSIsContainer) { | |
| return $false | |
| } | |
| if ($knownFiles -notcontains $child.Name) { | |
| return $false | |
| } | |
| } | |
| return $true | |
| } | |
| function Move-OldStandaloneBinIfApproved { | |
| param( | |
| [string]$VisibleBinDir, | |
| [string]$DefaultVisibleBinDir | |
| ) | |
| if (-not (Test-OldStandaloneBinLayout -VisibleBinDir $VisibleBinDir -DefaultVisibleBinDir $DefaultVisibleBinDir)) { | |
| return $null | |
| } | |
| Write-Step "We found an older Codex install at $VisibleBinDir" | |
| Write-WarningStep "To continue, Codex needs to update the install at this path." | |
| if (-not (Prompt-YesNo "Replace it with the current Codex setup now?")) { | |
| throw "Cannot replace older standalone install without confirmation: $VisibleBinDir" | |
| } | |
| $backupDir = "$VisibleBinDir.backup.$([DateTimeOffset]::UtcNow.ToUnixTimeSeconds()).$PID" | |
| Write-Step "Moving older standalone install to $backupDir" | |
| Move-Item -LiteralPath $VisibleBinDir -Destination $backupDir | |
| return $backupDir | |
| } | |
| function Add-JunctionSupportType { | |
| # Older installer types remain loaded when users rerun irm | iex in one session. | |
| if (([System.Management.Automation.PSTypeName]'CodexInstaller.JunctionV2').Type) { | |
| return | |
| } | |
| Add-Type -TypeDefinition @" | |
| using System; | |
| using System.ComponentModel; | |
| using System.IO; | |
| using System.Runtime.InteropServices; | |
| using System.Text; | |
| using Microsoft.Win32.SafeHandles; | |
| namespace CodexInstaller | |
| { | |
| public static class JunctionV2 | |
| { | |
| private const uint GENERIC_WRITE = 0x40000000; | |
| private const uint FILE_SHARE_READ = 0x00000001; | |
| private const uint FILE_SHARE_WRITE = 0x00000002; | |
| private const uint FILE_SHARE_DELETE = 0x00000004; | |
| private const uint OPEN_EXISTING = 3; | |
| private const uint FILE_FLAG_BACKUP_SEMANTICS = 0x02000000; | |
| private const uint FILE_FLAG_OPEN_REPARSE_POINT = 0x00200000; | |
| private const uint FSCTL_SET_REPARSE_POINT = 0x000900A4; | |
| private const uint IO_REPARSE_TAG_MOUNT_POINT = 0xA0000003; | |
| private const int HeaderLength = 20; | |
| [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] | |
| private static extern SafeFileHandle CreateFileW( | |
| string lpFileName, | |
| uint dwDesiredAccess, | |
| uint dwShareMode, | |
| IntPtr lpSecurityAttributes, | |
| uint dwCreationDisposition, | |
| uint dwFlagsAndAttributes, | |
| IntPtr hTemplateFile); | |
| [DllImport("kernel32.dll", SetLastError = true)] | |
| private static extern bool DeviceIoControl( | |
| SafeFileHandle hDevice, | |
| uint dwIoControlCode, | |
| byte[] lpInBuffer, | |
| int nInBufferSize, | |
| IntPtr lpOutBuffer, | |
| int nOutBufferSize, | |
| out int lpBytesReturned, | |
| IntPtr lpOverlapped); | |
| [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] | |
| private static extern uint GetFinalPathNameByHandleW( | |
| SafeFileHandle file, StringBuilder path, uint length, uint flags); | |
| public static string ResolveDirectory(string path) | |
| { | |
| using (SafeFileHandle handle = CreateFileW( | |
| path, 0, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, | |
| IntPtr.Zero, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, IntPtr.Zero)) | |
| { | |
| if (handle.IsInvalid) { throw new Win32Exception(Marshal.GetLastWin32Error()); } | |
| StringBuilder resolved = new StringBuilder(32768); | |
| uint length = GetFinalPathNameByHandleW(handle, resolved, (uint)resolved.Capacity, 0); | |
| if (length == 0) { throw new Win32Exception(Marshal.GetLastWin32Error()); } | |
| if (length >= resolved.Capacity) { throw new IOException("Resolved path is too long."); } | |
| return resolved.ToString().TrimEnd('\\'); | |
| } | |
| } | |
| public static void SetTarget(string linkPath, string targetPath) | |
| { | |
| string substituteName = "\\??\\" + Path.GetFullPath(targetPath); | |
| byte[] substituteNameBytes = Encoding.Unicode.GetBytes(substituteName); | |
| if (substituteNameBytes.Length > ushort.MaxValue - HeaderLength) { | |
| throw new ArgumentException("Junction target path is too long.", "targetPath"); | |
| } | |
| byte[] reparseBuffer = new byte[substituteNameBytes.Length + HeaderLength]; | |
| WriteUInt32(reparseBuffer, 0, IO_REPARSE_TAG_MOUNT_POINT); | |
| WriteUInt16(reparseBuffer, 4, checked((ushort)(substituteNameBytes.Length + 12))); | |
| WriteUInt16(reparseBuffer, 8, 0); | |
| WriteUInt16(reparseBuffer, 10, checked((ushort)substituteNameBytes.Length)); | |
| WriteUInt16(reparseBuffer, 12, checked((ushort)(substituteNameBytes.Length + 2))); | |
| WriteUInt16(reparseBuffer, 14, 0); | |
| Buffer.BlockCopy(substituteNameBytes, 0, reparseBuffer, 16, substituteNameBytes.Length); | |
| using (SafeFileHandle handle = CreateFileW( | |
| linkPath, | |
| GENERIC_WRITE, | |
| FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, | |
| IntPtr.Zero, | |
| OPEN_EXISTING, | |
| FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT, | |
| IntPtr.Zero)) | |
| { | |
| if (handle.IsInvalid) { | |
| throw new Win32Exception(Marshal.GetLastWin32Error()); | |
| } | |
| int bytesReturned; | |
| if (!DeviceIoControl( | |
| handle, | |
| FSCTL_SET_REPARSE_POINT, | |
| reparseBuffer, | |
| reparseBuffer.Length, | |
| IntPtr.Zero, | |
| 0, | |
| out bytesReturned, | |
| IntPtr.Zero)) | |
| { | |
| throw new Win32Exception(Marshal.GetLastWin32Error()); | |
| } | |
| } | |
| } | |
| private static void WriteUInt16(byte[] buffer, int offset, ushort value) | |
| { | |
| buffer[offset] = (byte)value; | |
| buffer[offset + 1] = (byte)(value >> 8); | |
| } | |
| private static void WriteUInt32(byte[] buffer, int offset, uint value) | |
| { | |
| buffer[offset] = (byte)value; | |
| buffer[offset + 1] = (byte)(value >> 8); | |
| buffer[offset + 2] = (byte)(value >> 16); | |
| buffer[offset + 3] = (byte)(value >> 24); | |
| } | |
| } | |
| } | |
| "@ | |
| } | |
| function Set-JunctionTarget { | |
| param( | |
| [string]$LinkPath, | |
| [string]$TargetPath | |
| ) | |
| Add-JunctionSupportType | |
| [CodexInstaller.JunctionV2]::SetTarget($LinkPath, $TargetPath) | |
| } | |
| function Test-IsJunction { | |
| param( | |
| [string]$Path | |
| ) | |
| if (-not (Test-Path -LiteralPath $Path)) { | |
| return $false | |
| } | |
| $item = Get-Item -LiteralPath $Path -Force | |
| return ($item.Attributes -band [IO.FileAttributes]::ReparsePoint) -and $item.LinkType -eq "Junction" | |
| } | |
| function Ensure-Junction { | |
| param( | |
| [string]$LinkPath, | |
| [string]$TargetPath, | |
| [string]$InstallerOwnedTargetPrefix | |
| ) | |
| if (-not (Test-Path -LiteralPath $LinkPath)) { | |
| New-Item -ItemType Junction -Path $LinkPath -Target $TargetPath | Out-Null | |
| return | |
| } | |
| $item = Get-Item -LiteralPath $LinkPath -Force | |
| if (Test-IsJunction -Path $LinkPath) { | |
| Add-JunctionSupportType | |
| $existingTarget = [CodexInstaller.JunctionV2]::ResolveDirectory($LinkPath) | |
| if (-not [string]::IsNullOrWhiteSpace($InstallerOwnedTargetPrefix)) { | |
| $ownedTargetPrefix = [CodexInstaller.JunctionV2]::ResolveDirectory($InstallerOwnedTargetPrefix) + "\" | |
| if (-not $existingTarget.StartsWith($ownedTargetPrefix, [System.StringComparison]::OrdinalIgnoreCase)) { | |
| throw "Refusing to retarget junction at $LinkPath because it is not managed by this installer." | |
| } | |
| } | |
| if ($existingTarget.Equals([CodexInstaller.JunctionV2]::ResolveDirectory($TargetPath), [System.StringComparison]::OrdinalIgnoreCase)) { | |
| return | |
| } | |
| # Keep the path itself in place and only retarget the junction. That | |
| # avoids a gap where current or the visible bin path disappears during | |
| # an update. | |
| Set-JunctionTarget -LinkPath $LinkPath -TargetPath $TargetPath | |
| return | |
| } | |
| if ($item.Attributes -band [IO.FileAttributes]::ReparsePoint) { | |
| throw "Refusing to replace non-junction reparse point at $LinkPath." | |
| } | |
| if ($item.PSIsContainer) { | |
| if ((Get-ChildItem -LiteralPath $LinkPath -Force | Select-Object -First 1) -ne $null) { | |
| throw "Refusing to replace non-empty directory at $LinkPath with a junction." | |
| } | |
| Remove-Item -LiteralPath $LinkPath -Force | |
| New-Item -ItemType Junction -Path $LinkPath -Target $TargetPath | Out-Null | |
| return | |
| } | |
| throw "Refusing to replace file at $LinkPath with a junction." | |
| } | |
| function Test-PackageContentsAreComplete { | |
| param( | |
| [string]$PackageDir | |
| ) | |
| if (-not (Test-Path -LiteralPath $PackageDir -PathType Container)) { | |
| return $false | |
| } | |
| $expectedFiles = @( | |
| "codex-package.json", | |
| "bin\codex.exe", | |
| "bin\codex-code-mode-host.exe", | |
| "codex-path\rg.exe", | |
| "codex-resources\codex-command-runner.exe", | |
| "codex-resources\codex-windows-sandbox-setup.exe" | |
| ) | |
| foreach ($name in $expectedFiles) { | |
| if (-not (Test-Path -LiteralPath (Join-Path $PackageDir $name) -PathType Leaf)) { | |
| return $false | |
| } | |
| } | |
| return $true | |
| } | |
| function Test-LegacyPlatformNpmContentsAreComplete { | |
| param( | |
| [string]$PackageDir | |
| ) | |
| if (-not (Test-Path -LiteralPath $PackageDir -PathType Container)) { | |
| return $false | |
| } | |
| $expectedFiles = @( | |
| "codex.exe", | |
| "codex-resources\codex-command-runner.exe", | |
| "codex-resources\codex-windows-sandbox-setup.exe", | |
| "codex-resources\rg.exe" | |
| ) | |
| foreach ($name in $expectedFiles) { | |
| if (-not (Test-Path -LiteralPath (Join-Path $PackageDir $name) -PathType Leaf)) { | |
| return $false | |
| } | |
| } | |
| return $true | |
| } | |
| function Test-ReleaseIsComplete { | |
| param( | |
| [string]$ReleaseDir, | |
| [string]$ExpectedVersion, | |
| [string]$ExpectedTarget, | |
| [string]$Layout | |
| ) | |
| switch ($Layout) { | |
| "Package" { | |
| if (-not (Test-PackageContentsAreComplete -PackageDir $ReleaseDir)) { | |
| return $false | |
| } | |
| $codexPath = Join-Path $ReleaseDir "bin\codex.exe" | |
| } | |
| "LegacyPlatformNpm" { | |
| if (-not (Test-LegacyPlatformNpmContentsAreComplete -PackageDir $ReleaseDir)) { | |
| return $false | |
| } | |
| $codexPath = Join-Path $ReleaseDir "codex.exe" | |
| } | |
| default { | |
| throw "Unknown Codex installer layout: $Layout" | |
| } | |
| } | |
| return (Split-Path -Leaf $ReleaseDir) -eq "$ExpectedVersion-$ExpectedTarget" -and | |
| (Get-VersionFromBinary -CodexPath $codexPath) -ceq $ExpectedVersion | |
| } | |
| function Get-ExistingCodexCommand { | |
| $existing = Get-Command codex -ErrorAction SilentlyContinue | |
| if ($null -eq $existing) { | |
| return $null | |
| } | |
| return $existing.Source | |
| } | |
| function Get-ExistingCodexManager { | |
| param( | |
| [string]$ExistingPath, | |
| [string]$VisibleBinDir | |
| ) | |
| if ([string]::IsNullOrWhiteSpace($ExistingPath)) { | |
| return $null | |
| } | |
| if ($ExistingPath.StartsWith($VisibleBinDir, [System.StringComparison]::OrdinalIgnoreCase)) { | |
| return $null | |
| } | |
| if ($ExistingPath -match "\\.bun\\") { | |
| return "bun" | |
| } | |
| if ($ExistingPath -match "node_modules" -or $ExistingPath -match "\\npm\\") { | |
| return "npm" | |
| } | |
| return $null | |
| } | |
| function Get-ConflictingInstall { | |
| param( | |
| [string]$VisibleBinDir | |
| ) | |
| $existingPath = Get-ExistingCodexCommand | |
| $manager = Get-ExistingCodexManager -ExistingPath $existingPath -VisibleBinDir $VisibleBinDir | |
| if ($null -eq $manager) { | |
| return $null | |
| } | |
| Write-Step "Detected existing $manager-managed Codex at $existingPath" | |
| Write-WarningStep "Multiple managed Codex installs can be ambiguous because PATH order decides which one runs." | |
| return [PSCustomObject]@{ | |
| Manager = $manager | |
| Path = $existingPath | |
| } | |
| } | |
| function Maybe-HandleConflictingInstall { | |
| param( | |
| [object]$Conflict | |
| ) | |
| if ($null -eq $Conflict) { | |
| return | |
| } | |
| $manager = $Conflict.Manager | |
| $uninstallArgs = if ($manager -eq "bun") { | |
| @("remove", "-g", "@openai/codex") | |
| } else { | |
| @("uninstall", "-g", "@openai/codex") | |
| } | |
| $uninstallCommand = if ($manager -eq "bun") { "bun" } else { "npm" } | |
| if (Prompt-YesNo "Uninstall the existing $manager-managed Codex now?") { | |
| Write-Step "Running: $uninstallCommand $($uninstallArgs -join ' ')" | |
| try { | |
| & $uninstallCommand @uninstallArgs | |
| } catch { | |
| Write-WarningStep "Failed to uninstall the existing $manager-managed Codex. Continuing with the standalone install." | |
| } | |
| } else { | |
| Write-WarningStep "Leaving the existing $manager-managed Codex installed. PATH order will determine which codex runs." | |
| } | |
| } | |
| function Test-VisibleCodexCommand { | |
| param( | |
| [string]$VisibleBinDir | |
| ) | |
| $codexCommand = Join-Path $VisibleBinDir "codex.exe" | |
| & $codexCommand --version *> $null | |
| if ($LASTEXITCODE -ne 0) { | |
| throw "Installed Codex command failed verification: $codexCommand --version" | |
| } | |
| } | |
| if ($env:OS -ne "Windows_NT") { | |
| Write-Error "install.ps1 supports Windows only. Use install.sh on macOS or Linux." | |
| exit 1 | |
| } | |
| if (-not [Environment]::Is64BitOperatingSystem) { | |
| Write-Error "Codex requires a 64-bit version of Windows." | |
| exit 1 | |
| } | |
| $architecture = [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture | |
| $target = $null | |
| $platformLabel = $null | |
| $npmTag = $null | |
| switch ($architecture) { | |
| "Arm64" { | |
| $target = "aarch64-pc-windows-msvc" | |
| $platformLabel = "Windows (ARM64)" | |
| $npmTag = "win32-arm64" | |
| } | |
| "X64" { | |
| $target = "x86_64-pc-windows-msvc" | |
| $platformLabel = "Windows (x64)" | |
| $npmTag = "win32-x64" | |
| } | |
| default { | |
| Write-Error "Unsupported architecture: $architecture" | |
| exit 1 | |
| } | |
| } | |
| $codexHome = if ([string]::IsNullOrWhiteSpace($env:CODEX_HOME)) { | |
| Join-Path $env:USERPROFILE ".codex" | |
| } else { | |
| $env:CODEX_HOME | |
| } | |
| $daemonOnly = $env:CODEX_INSTALL_DAEMON_ONLY -eq "1" | |
| $standaloneRoot = Join-Path $codexHome $(if ($daemonOnly) { "packages\app-server-daemon" } else { "packages\standalone" }) | |
| $releasesDir = Join-Path $standaloneRoot "releases" | |
| $currentDir = Join-Path $standaloneRoot "current" | |
| $deferSelection = $env:CODEX_INSTALL_DEFER_SELECTION -eq "1" | |
| if ($deferSelection) { | |
| if (-not $daemonOnly) { throw "Deferred selection requires a daemon-only installation." } | |
| $currentDir = Join-Path $standaloneRoot ".migration-current" | |
| } | |
| $autoUpdateVersion = Join-Path $standaloneRoot "auto-update-version" | |
| $lockPath = Join-Path $standaloneRoot "install.lock" | |
| $defaultVisibleBinDir = Join-Path $env:LOCALAPPDATA "Programs\OpenAI\Codex\bin" | |
| if ([string]::IsNullOrWhiteSpace($env:CODEX_INSTALL_DIR)) { | |
| $visibleBinDir = $defaultVisibleBinDir | |
| } else { | |
| $visibleBinDir = $env:CODEX_INSTALL_DIR | |
| } | |
| $currentVersion = Get-CurrentInstalledVersion -StandaloneCurrentDir $currentDir | |
| $resolvedRelease = Resolve-Release | |
| $resolvedVersion = $resolvedRelease.Version | |
| $releaseMetadata = $resolvedRelease.Metadata | |
| $releaseName = "$resolvedVersion-$target" | |
| $releaseDir = Join-Path $releasesDir $releaseName | |
| if (-not [string]::IsNullOrWhiteSpace($currentVersion) -and $currentVersion -ne $resolvedVersion) { | |
| Write-Step "Updating Codex CLI from $currentVersion to $resolvedVersion" | |
| } elseif (-not [string]::IsNullOrWhiteSpace($currentVersion)) { | |
| Write-Step "Updating Codex CLI" | |
| } else { | |
| Write-Step "Installing Codex CLI" | |
| } | |
| Write-Step "Detected platform: $platformLabel" | |
| Write-Step "Resolved version: $resolvedVersion" | |
| $conflictingInstall = if ($daemonOnly) { $null } else { Get-ConflictingInstall -VisibleBinDir $visibleBinDir } | |
| $oldStandaloneBackup = $null | |
| $checksumAsset = "codex-package_SHA256SUMS" | |
| $assetSelection = Resolve-ReleaseAssetSelection -ResolvedRelease $resolvedRelease -Target $target -NpmTag $npmTag | |
| $packageAsset = $assetSelection.PackageAsset | |
| $packageMetadata = $assetSelection.PackageMetadata | |
| $checksumMetadata = $assetSelection.ChecksumMetadata | |
| $installLayout = $assetSelection.InstallLayout | |
| $tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ("codex-install-" + [System.Guid]::NewGuid().ToString("N")) | |
| New-Item -ItemType Directory -Force -Path $tempDir | Out-Null | |
| $guardRejected = $false | |
| try { | |
| Invoke-WithInstallLock -LockPath $lockPath -Script { | |
| $updaterRecord = Join-Path $codexHome "app-server-daemon\app-server-updater.pid" | |
| if ($daemonOnly) { $updaterRecord = Join-Path $codexHome "app-server-daemon\daemon-updater.pid" } | |
| $oldUpdaterParent = $false | |
| if ($Release -eq "latest" -and $env:CODEX_INSTALL_IF_LATEST -ne "1" -and $env:CODEX_INSTALL_IF_CURRENT -ne "1" -and (Test-Path -LiteralPath $updaterRecord)) { | |
| $updaterPid = $null | |
| $updaterStartTime = $null | |
| try { | |
| $record = Get-Content -LiteralPath $updaterRecord -Raw | ConvertFrom-Json | |
| $updaterPid = [long]$record.pid | |
| $updaterStartTime = [string]$record.processStartTime | |
| } catch { | |
| # Empty or stale PID reservations must not block a manual install. | |
| } | |
| if ($null -ne $updaterPid -and $updaterPid -gt 0 -and -not [string]::IsNullOrEmpty($updaterStartTime)) { | |
| $updaterProcess = Get-Process -Id $updaterPid -ErrorAction SilentlyContinue | |
| if ($null -ne $updaterProcess -and $updaterStartTime -eq [string]$updaterProcess.StartTime.ToFileTimeUtc()) { | |
| try { | |
| $parentPid = (Get-CimInstance Win32_Process -Filter "ProcessId = $PID" -ErrorAction Stop).ParentProcessId | |
| if ([long]$parentPid -le 0) { throw "Missing updater parent process." } | |
| } catch { | |
| try { | |
| $parentPid = (Get-WmiObject Win32_Process -Filter "ProcessId = $PID" -ErrorAction Stop).ParentProcessId | |
| if ([long]$parentPid -le 0) { throw "Missing updater parent process." } | |
| } catch { | |
| throw "Cannot verify whether the standalone installer was launched by an older updater." | |
| } | |
| } | |
| $oldUpdaterParent = $updaterPid -eq $parentPid | |
| } | |
| } | |
| } | |
| if ($env:CODEX_INSTALL_IF_LATEST -eq "1" -or $env:CODEX_INSTALL_IF_CURRENT -eq "1" -or $oldUpdaterParent) { | |
| $previousRelease = if ($oldUpdaterParent -and (Test-Path -LiteralPath $autoUpdateVersion)) { | |
| [System.IO.File]::ReadAllText($autoUpdateVersion) | |
| } else { | |
| $env:CODEX_UPDATE_FROM_RELEASE | |
| } | |
| Add-JunctionSupportType | |
| $currentTarget = if (Test-Path -LiteralPath $currentDir) { (Get-Item -LiteralPath $currentDir).Target } else { $null } | |
| if ($Release -ne "latest" -or [string]::IsNullOrEmpty($previousRelease) -or [string]::IsNullOrEmpty($currentTarget) -or | |
| -not (Test-Path -LiteralPath (Join-Path $releasesDir $previousRelease)) -or | |
| [CodexInstaller.JunctionV2]::ResolveDirectory($currentDir) -ne [CodexInstaller.JunctionV2]::ResolveDirectory((Join-Path $releasesDir $previousRelease))) { | |
| if ($env:CODEX_INSTALL_IF_CURRENT -eq "1") { throw "Daemon selection changed; retry the update." } | |
| $script:guardRejected = $true | |
| return | |
| } | |
| # Explicit daemon updates may leave a local or pinned release. | |
| if ($env:CODEX_INSTALL_IF_CURRENT -ne "1" -and | |
| (-not (Test-Path -LiteralPath $autoUpdateVersion) -or | |
| [System.IO.File]::ReadAllText($autoUpdateVersion) -cne $previousRelease)) { | |
| $script:guardRejected = $true | |
| return | |
| } | |
| } | |
| Remove-StaleInstallArtifacts -ReleasesDir $releasesDir | |
| if (-not (Test-ReleaseIsComplete -ReleaseDir $releaseDir -ExpectedVersion $resolvedVersion -ExpectedTarget $target -Layout $installLayout)) { | |
| if (Test-Path -LiteralPath $releaseDir) { | |
| if ($daemonOnly) { throw "Refusing to overwrite existing daemon release $releaseDir." } | |
| Write-WarningStep "Found incomplete existing release at $releaseDir. Reinstalling." | |
| } | |
| $archivePath = Join-Path $tempDir $packageAsset | |
| $checksumPath = Join-Path $tempDir $checksumAsset | |
| $stagingDir = Join-Path $releasesDir ".staging.$releaseName.$PID" | |
| Write-Step "Downloading Codex CLI" | |
| if ($installLayout -eq "Package") { | |
| Invoke-WebRequestWithFallback -Metadata $checksumMetadata -OutFile $checksumPath -ExpectedDigest $checksumMetadata.Sha256 -AssetName $checksumAsset -ReleaseVersion $resolvedVersion -RequiredManifestAsset $packageAsset | |
| $expectedPackageDigest = Get-PackageArchiveDigest -ManifestPath $checksumPath -AssetName $packageAsset | |
| } else { | |
| $expectedPackageDigest = $packageMetadata.Sha256 | |
| } | |
| Invoke-WebRequestWithFallback -Metadata $packageMetadata -OutFile $archivePath -ExpectedDigest $expectedPackageDigest -AssetName $packageAsset -ReleaseVersion $resolvedVersion | |
| New-Item -ItemType Directory -Force -Path $releasesDir | Out-Null | |
| if (Test-Path -LiteralPath $stagingDir) { | |
| Remove-Item -LiteralPath $stagingDir -Recurse -Force | |
| } | |
| New-Item -ItemType Directory -Force -Path $stagingDir | Out-Null | |
| if ($installLayout -eq "Package") { | |
| tar -xzf $archivePath -C $stagingDir | |
| if (-not (Test-PackageContentsAreComplete -PackageDir $stagingDir)) { | |
| throw "Downloaded Codex package archive did not contain the expected package layout." | |
| } | |
| } else { | |
| $extractDir = Join-Path $tempDir "extract" | |
| New-Item -ItemType Directory -Force -Path $extractDir | Out-Null | |
| tar -xzf $archivePath -C $extractDir | |
| $vendorRoot = Join-Path $extractDir "package/vendor/$target" | |
| $resourcesDir = Join-Path $stagingDir "codex-resources" | |
| New-Item -ItemType Directory -Force -Path $resourcesDir | Out-Null | |
| $copyMap = @{ | |
| "codex/codex.exe" = "codex.exe" | |
| "codex/codex-command-runner.exe" = "codex-resources\codex-command-runner.exe" | |
| "codex/codex-windows-sandbox-setup.exe" = "codex-resources\codex-windows-sandbox-setup.exe" | |
| "path/rg.exe" = "codex-resources\rg.exe" | |
| } | |
| foreach ($relativeSource in $copyMap.Keys) { | |
| Copy-Item -LiteralPath (Join-Path $vendorRoot $relativeSource) -Destination (Join-Path $stagingDir $copyMap[$relativeSource]) | |
| } | |
| if (-not (Test-LegacyPlatformNpmContentsAreComplete -PackageDir $stagingDir)) { | |
| throw "Downloaded Codex npm archive did not contain the expected legacy platform package layout." | |
| } | |
| } | |
| if (Test-Path -LiteralPath $releaseDir) { | |
| Remove-Item -LiteralPath $releaseDir -Recurse -Force | |
| } | |
| Move-Item -LiteralPath $stagingDir -Destination $releaseDir | |
| } | |
| if (-not (Test-ReleaseIsComplete -ReleaseDir $releaseDir -ExpectedVersion $resolvedVersion -ExpectedTarget $target -Layout $installLayout)) { | |
| throw "Installed Codex command did not report expected version $resolvedVersion." | |
| } | |
| New-Item -ItemType Directory -Force -Path $standaloneRoot | Out-Null | |
| if ($deferSelection -and (Get-Item -LiteralPath (Join-Path $standaloneRoot "current") -Force -ErrorAction SilentlyContinue)) { | |
| throw "A dedicated daemon is already selected; retry the update." | |
| } | |
| if ($daemonOnly -and -not $deferSelection) { | |
| $installedCodex = Join-Path $releaseDir $(if ($installLayout -eq "Package") { "bin\codex.exe" } else { "codex.exe" }) | |
| & $installedCodex app-server daemon pid-update-loop --check-package-ownership | Out-Null | |
| if ($LASTEXITCODE -ne 0) { | |
| throw "The production release does not support daemon-owned packages; the current selection was left unchanged." | |
| } | |
| } | |
| Ensure-Junction -LinkPath $currentDir -TargetPath $releaseDir -InstallerOwnedTargetPrefix $releasesDir | |
| if ($Release -eq "latest") { | |
| $tempMarker = "$autoUpdateVersion.tmp.$PID" | |
| [System.IO.File]::WriteAllText($tempMarker, $releaseName) | |
| Move-Item -LiteralPath $tempMarker -Destination $autoUpdateVersion -Force | |
| } else { | |
| if (Test-Path -LiteralPath $autoUpdateVersion) { | |
| Remove-Item -LiteralPath $autoUpdateVersion -Force -ErrorAction Stop | |
| } | |
| } | |
| if ($daemonOnly) { return } | |
| $visibleParent = Split-Path -Parent $visibleBinDir | |
| $currentBinDir = if ($installLayout -eq "Package") { | |
| Join-Path $currentDir "bin" | |
| } else { | |
| $currentDir | |
| } | |
| New-Item -ItemType Directory -Force -Path $visibleParent | Out-Null | |
| $oldStandaloneBackup = Move-OldStandaloneBinIfApproved -VisibleBinDir $visibleBinDir -DefaultVisibleBinDir $defaultVisibleBinDir | |
| try { | |
| Ensure-Junction -LinkPath $visibleBinDir -TargetPath $currentBinDir -InstallerOwnedTargetPrefix $standaloneRoot | |
| Test-VisibleCodexCommand -VisibleBinDir $visibleBinDir | |
| } catch { | |
| if ($null -ne $oldStandaloneBackup -and (Test-Path -LiteralPath $oldStandaloneBackup)) { | |
| if (Test-Path -LiteralPath $visibleBinDir) { | |
| Remove-Item -LiteralPath $visibleBinDir -Recurse -Force | |
| } | |
| Move-Item -LiteralPath $oldStandaloneBackup -Destination $visibleBinDir | |
| } | |
| throw | |
| } | |
| if ($null -ne $oldStandaloneBackup) { | |
| Remove-Item -LiteralPath $oldStandaloneBackup -Recurse -Force | |
| } | |
| } | |
| } finally { | |
| Remove-Item -Recurse -Force $tempDir -ErrorAction SilentlyContinue | |
| } | |
| if ($guardRejected -or $daemonOnly) { return } | |
| Maybe-HandleConflictingInstall -Conflict $conflictingInstall | |
| $userPath = [Environment]::GetEnvironmentVariable("Path", "User") | |
| $prioritizeVisibleBin = $null -ne $conflictingInstall | |
| if ($prioritizeVisibleBin) { | |
| $newUserPath = Prepend-PathEntry -PathValue $userPath -Entry $visibleBinDir | |
| if ($newUserPath -cne $userPath) { | |
| [Environment]::SetEnvironmentVariable("Path", $newUserPath, "User") | |
| Write-Step "PATH updated for future PowerShell sessions." | |
| } else { | |
| Write-Step "$visibleBinDir is already first on PATH." | |
| } | |
| } elseif (-not (Path-Contains -PathValue $userPath -Entry $visibleBinDir)) { | |
| if ([string]::IsNullOrWhiteSpace($userPath)) { | |
| $newUserPath = $visibleBinDir | |
| } else { | |
| $newUserPath = "$visibleBinDir;$userPath" | |
| } | |
| [Environment]::SetEnvironmentVariable("Path", $newUserPath, "User") | |
| Write-Step "PATH updated for future PowerShell sessions." | |
| } elseif (Path-Contains -PathValue $env:Path -Entry $visibleBinDir) { | |
| Write-Step "$visibleBinDir is already on PATH." | |
| } else { | |
| Write-Step "PATH is already configured for future PowerShell sessions." | |
| } | |
| if ($prioritizeVisibleBin) { | |
| $env:Path = Prepend-PathEntry -PathValue $env:Path -Entry $visibleBinDir | |
| } elseif (-not (Path-Contains -PathValue $env:Path -Entry $visibleBinDir)) { | |
| if ([string]::IsNullOrWhiteSpace($env:Path)) { | |
| $env:Path = $visibleBinDir | |
| } else { | |
| $env:Path = "$visibleBinDir;$env:Path" | |
| } | |
| } | |
| Write-Step "Current PowerShell session: codex" | |
| Write-Step "Future PowerShell windows: open a new PowerShell window and run: codex" | |
| Write-Host "Codex CLI $resolvedVersion installed successfully." | |
| $codexCommand = Join-Path $visibleBinDir "codex.exe" | |
| if (Prompt-YesNo "Start Codex now?") { | |
| Write-Step "Launching Codex" | |
| & $codexCommand | |
| } | |