codex 安装脚本分析

codex 安装脚本分析

codex 安装脚本是一个非常典型且易于阅读的 PowerShell 脚本安装程序。

一、 为什么在控制面板的“软件卸载”中找不到 Codex?

在 Windows 中,一个软件想要出现在控制面板的“程序和功能”(或设置中的“应用和功能”)列表里,它的安装程序必须在 Windows 的注册表(Registry)中写入特定的卸载信息(通常在 HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\UninstallHKCU 下)。

通过分析这个脚本,可以看出它是一个“绿色/便携式”(Standalone)安装脚本,它完全没有对注册表进行这种写入操作。它所做的事情非常简单直接:

  1. 下载压缩包。
  2. 解压到你的用户文件夹里。
  3. 创建文件夹快捷方式(Junctions)。
  4. 修改你的用户环境变量(PATH)。

因为它没有向系统注册自己,所以控制面板里自然找不到它。如果你想“卸载”它,只需要手动删除相关文件夹,并把环境变量里的路径删掉即可。


二、 如何知道 Codex 的安装位置?

根据脚本中的变量定义,除非你在安装前自己设置了环境变量($env:CODEX_HOME$env:CODEX_INSTALL_DIR),否则它会默认安装在以下两个隐蔽的目录中:

1. 实际文件存放位置(核心程序库):

  • 默认路径:C:\Users\<你的用户名>\.codex\packages\standalone\
  • 脚本对应逻辑$codexHome = Join-Path $env:USERPROFILE ".codex"

2. 可执行文件(快捷方式/目录联接)的对外暴露位置:

  • 默认路径:C:\Users\<你的用户名>\AppData\Local\Programs\OpenAI\Codex\bin
  • 脚本对应逻辑$defaultVisibleBinDir = Join-Path $env:LOCALAPPDATA "Programs\OpenAI\Codex\bin"

最快的查找方法:
由于脚本在最后把安装路径加到了系统的环境变量中,你只需打开任意一个命令提示符(CMD)或 PowerShell,输入以下命令即可直接看到它在哪里:

# 在 PowerShell 中:
Get-Command codex# 在 CMD 中:
where codex

三、 如何了解 Codex 的安装详情(脚本到底做了什么)?

通读脚本逻辑,它的安装流程可以分为以下 5 个主要步骤

1. 解析版本与下载地址 (Resolve-Release)

脚本首先会去 [releases.openai.com/codex](https://releases.openai.com/codex) 或 GitHub Releases API 检查当前最新的版本号(或者你指定的特定版本),并匹配你的电脑架构(比如 Windows x64 还是 ARM64),找到对应的 .tar.gz.tgz 压缩包的下载链接。

2. 下载与校验 (Invoke-WebRequestWithFallback / Test-ArchiveDigest)

脚本会在你的系统的临时文件夹(Temp)里建一个专属目录,把压缩包和 SHA256 校验文件下载下来。下载完成后,它会自动计算压缩包的哈希值,确保文件在下载过程中没有损坏或被篡改。

3. 解压与存放 (Remove-StaleInstallArtifacts / tar -xzf)

它会将旧的、失败的临时安装残留清理掉,然后调用系统自带的 tar 命令把压缩包解压到 C:\Users\<你的用户名>\.codex\packages\standalone\releases\<版本号-架构> 目录下。

4. 建立目录联接/软链接 (Ensure-Junction)

为了方便以后升级(不用每次都去改环境变量),脚本玩了一个聪明的“指针”魔法(Directory Junctions):

  • 它创建了一个叫 current 的文件夹快捷方式,指向刚刚下载好的具体版本文件夹。
  • 它又在 %LOCALAPPDATA%\Programs\OpenAI\Codex\bin 创建了一个快捷方式,指向 current 里的 bin 目录。
    这样做的好处是,以后再升级,只需要把 current 重新指向新版本的文件夹就行了,外面的路径永远不用变。

5. 解决冲突与配置环境变量 (Maybe-HandleConflictingInstall / Environment::SetEnvironmentVariable)

  • 冲突检查:它会检查你是不是以前用 npmbun 全局安装过 @openai/codex。如果有,它会弹窗问你是否要卸载旧的,以免系统不知道该运行哪一个。
  • 写环境变量:最后,它将 %LOCALAPPDATA%\Programs\OpenAI\Codex\bin 写入你当前用户的 PATH 环境变量。这一步是为了让你在以后打开任何终端时,只需要敲 codex 就能直接运行软件,而不用输入长长的一串绝对路径。

codex 完整安装脚本

[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 = 300function 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 1if ($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 = $FallbackUrlSha256 = $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 $ExpectedDigestif (-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 $OutFiletry {Test-ArchiveDigest -ArchivePath $OutFile -ExpectedDigest $ExpectedDigestif (-not [string]::IsNullOrWhiteSpace($RequiredManifestAsset)) {$null = Get-PackageArchiveDigest -ManifestPath $OutFile -AssetName $RequiredManifestAsset}} catch {$githubRelease = Resolve-ReleaseFromGitHub -NormalizedVersion $ReleaseVersion$githubAssetMetadata = Find-ReleaseAssetMetadata -AssetName $AssetName -ReleaseMetadata $githubRelease.Metadataif ($null -eq $githubAssetMetadata) {throw "Could not find GitHub release metadata for asset $AssetName."}Test-ArchiveDigest -ArchivePath $OutFile -ExpectedDigest $githubAssetMetadata.Sha256if (-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 = $nullif ($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 $checksumFallbackUrlif ($null -ne $packageMetadata -and $null -ne $checksumMetadata) {return [PSCustomObject]@{PackageAsset = $packageAssetPackageMetadata = $packageMetadataChecksumMetadata = $checksumMetadataInstallLayout = "Package"}}$packageAsset = "codex-npm-$NpmTag-$version.tgz"$packageUrl = $null$packageFallbackUrl = $nullif ($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 $packageFallbackUrlif ($null -eq $packageMetadata) {throw "Could not find Codex package or platform npm release assets for Codex $version."}return [PSCustomObject]@{PackageAsset = $packageAssetPackageMetadata = $packageMetadataChecksumMetadata = $nullInstallLayout = "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 = $nullwhile ($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_nameAssert-ValidReleaseVersion -Version $resolvedVersionreturn $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 = $resolvedVersionMetadata = $releaseMetadataSource = "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 $releaseMetadataif ($NormalizedVersion -ne "latest" -and $resolvedVersion -cne $NormalizedVersion) {throw "Release metadata version did not match requested Codex version $NormalizedVersion."}$resolvedRelease = [PSCustomObject]@{Version = $resolvedVersionMetadata = $releaseMetadataSource = "ReleasesOpenAICom"}$null = Resolve-ReleaseAssetSelection -ResolvedRelease $resolvedRelease -Target $target -NpmTag $npmTag} catch {return $null}return $resolvedRelease
}function Resolve-Release {$normalizedVersion = Normalize-Version -RawVersion $ReleaseAssert-ValidReleaseVersion -Version $normalizedVersionif ($PreferReleasesOpenAICom) {$release = Resolve-ReleaseFromReleases -NormalizedVersion $normalizedVersionif ($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 -Forceif ($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 $backupDirreturn $backupDir
}function Add-JunctionSupportType {if (([System.Management.Automation.PSTypeName]'CodexInstaller.Junction').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 Junction{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);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.Junction]::SetTarget($LinkPath, $TargetPath)
}function Test-IsJunction {param([string]$Path)if (-not (Test-Path -LiteralPath $Path)) {return $false}$item = Get-Item -LiteralPath $Path -Forcereturn ($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-Nullreturn}$item = Get-Item -LiteralPath $LinkPath -Forceif (Test-IsJunction -Path $LinkPath) {$existingTarget = [string]$item.Targetif (-not [string]::IsNullOrWhiteSpace($InstallerOwnedTargetPrefix)) {$ownedTargetPrefix = $InstallerOwnedTargetPrefix.TrimEnd("\\")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($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 $TargetPathreturn}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 -ForceNew-Item -ItemType Junction -Path $LinkPath -Target $TargetPath | Out-Nullreturn}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 SilentlyContinueif ($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 $VisibleBinDirif ($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 = $managerPath = $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 *> $nullif ($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
}
$standaloneRoot = Join-Path $codexHome "packages\standalone"
$releasesDir = Join-Path $standaloneRoot "releases"
$currentDir = Join-Path $standaloneRoot "current"
$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 $releaseNameif (-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 = 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-Nulltry {Invoke-WithInstallLock -LockPath $lockPath -Script {Remove-StaleInstallArtifacts -ReleasesDir $releasesDirif (-not (Test-ReleaseIsComplete -ReleaseDir $releaseDir -ExpectedVersion $resolvedVersion -ExpectedTarget $target -Layout $installLayout)) {if (Test-Path -LiteralPath $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 $resolvedVersionNew-Item -ItemType Directory -Force -Path $releasesDir | Out-Nullif (Test-Path -LiteralPath $stagingDir) {Remove-Item -LiteralPath $stagingDir -Recurse -Force}New-Item -ItemType Directory -Force -Path $stagingDir | Out-Nullif ($installLayout -eq "Package") {tar -xzf $archivePath -C $stagingDirif (-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-Nulltar -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-NullEnsure-Junction -LinkPath $currentDir -TargetPath $releaseDir -InstallerOwnedTargetPrefix $releasesDir$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 $defaultVisibleBinDirtry {Ensure-Junction -LinkPath $visibleBinDir -TargetPath $currentBinDir -InstallerOwnedTargetPrefix $standaloneRootTest-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
}Maybe-HandleConflictingInstall -Conflict $conflictingInstall$userPath = [Environment]::GetEnvironmentVariable("Path", "User")
$prioritizeVisibleBin = $null -ne $conflictingInstall
if ($prioritizeVisibleBin) {$newUserPath = Prepend-PathEntry -PathValue $userPath -Entry $visibleBinDirif ($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
}