mirror of
https://github.com/openai/codex.git
synced 2026-05-16 17:23:57 +00:00
755 lines
24 KiB
PowerShell
755 lines
24 KiB
PowerShell
param(
|
|
[string]$Release = "latest"
|
|
)
|
|
|
|
Set-StrictMode -Version Latest
|
|
$ErrorActionPreference = "Stop"
|
|
$ProgressPreference = "SilentlyContinue"
|
|
|
|
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 ($env:CODEX_INSTALL_SCRIPT_NONINTERACTIVE -eq "true") {
|
|
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 Get-ReleaseAssetMetadata {
|
|
param(
|
|
[string]$AssetName,
|
|
[string]$ResolvedVersion
|
|
)
|
|
|
|
$release = Invoke-RestMethod -Uri "https://api.github.com/repos/openai/codex/releases/tags/rust-v$ResolvedVersion"
|
|
$asset = $release.assets | Where-Object { $_.name -eq $AssetName } | Select-Object -First 1
|
|
if ($null -eq $asset) {
|
|
throw "Could not find release asset $AssetName for Codex $ResolvedVersion."
|
|
}
|
|
|
|
$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 = $asset.browser_download_url
|
|
Sha256 = $digestMatch.Groups[1].Value.ToLowerInvariant()
|
|
}
|
|
}
|
|
|
|
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 release metadata. Expected $ExpectedDigest but got $actualDigest."
|
|
}
|
|
}
|
|
|
|
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 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-Version {
|
|
$normalizedVersion = Normalize-Version -RawVersion $Release
|
|
if ($normalizedVersion -ne "latest") {
|
|
return $normalizedVersion
|
|
}
|
|
|
|
$release = Invoke-RestMethod -Uri "https://api.github.com/repos/openai/codex/releases/latest"
|
|
if (-not $release.tag_name) {
|
|
Write-Error "Failed to resolve the latest Codex release version."
|
|
exit 1
|
|
}
|
|
|
|
return (Normalize-Version -RawVersion $release.tag_name)
|
|
}
|
|
|
|
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 "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 {
|
|
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 -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) {
|
|
$existingTarget = [string]$item.Target
|
|
if (-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 $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-ReleaseIsComplete {
|
|
param(
|
|
[string]$ReleaseDir,
|
|
[string]$ExpectedVersion,
|
|
[string]$ExpectedTarget
|
|
)
|
|
|
|
if (-not (Test-Path -LiteralPath $ReleaseDir -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 $ReleaseDir $name) -PathType Leaf)) {
|
|
return $false
|
|
}
|
|
}
|
|
|
|
return (Split-Path -Leaf $ReleaseDir) -eq "$ExpectedVersion-$ExpectedTarget"
|
|
}
|
|
|
|
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
|
|
}
|
|
$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
|
|
$resolvedVersion = Resolve-Version
|
|
$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 = Get-ConflictingInstall -VisibleBinDir $visibleBinDir
|
|
$oldStandaloneBackup = $null
|
|
|
|
$packageAsset = "codex-npm-$npmTag-$resolvedVersion.tgz"
|
|
$tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ("codex-install-" + [System.Guid]::NewGuid().ToString("N"))
|
|
New-Item -ItemType Directory -Force -Path $tempDir | Out-Null
|
|
|
|
try {
|
|
Invoke-WithInstallLock -LockPath $lockPath -Script {
|
|
Remove-StaleInstallArtifacts -ReleasesDir $releasesDir
|
|
|
|
if (-not (Test-ReleaseIsComplete -ReleaseDir $releaseDir -ExpectedVersion $resolvedVersion -ExpectedTarget $target)) {
|
|
if (Test-Path -LiteralPath $releaseDir) {
|
|
Write-WarningStep "Found incomplete existing release at $releaseDir. Reinstalling."
|
|
}
|
|
|
|
$archivePath = Join-Path $tempDir $packageAsset
|
|
$extractDir = Join-Path $tempDir "extract"
|
|
$stagingDir = Join-Path $releasesDir ".staging.$releaseName.$PID"
|
|
$assetMetadata = Get-ReleaseAssetMetadata -AssetName $packageAsset -ResolvedVersion $resolvedVersion
|
|
|
|
Write-Step "Downloading Codex CLI"
|
|
Invoke-WebRequest -Uri $assetMetadata.Url -OutFile $archivePath
|
|
Test-ArchiveDigest -ArchivePath $archivePath -ExpectedDigest $assetMetadata.Sha256
|
|
|
|
New-Item -ItemType Directory -Force -Path $extractDir | Out-Null
|
|
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
|
|
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 (Test-Path -LiteralPath $releaseDir) {
|
|
Remove-Item -LiteralPath $releaseDir -Recurse -Force
|
|
}
|
|
Move-Item -LiteralPath $stagingDir -Destination $releaseDir
|
|
}
|
|
|
|
New-Item -ItemType Directory -Force -Path $standaloneRoot | Out-Null
|
|
Ensure-Junction -LinkPath $currentDir -TargetPath $releaseDir -InstallerOwnedTargetPrefix $releasesDir
|
|
|
|
$visibleParent = Split-Path -Parent $visibleBinDir
|
|
New-Item -ItemType Directory -Force -Path $visibleParent | Out-Null
|
|
$oldStandaloneBackup = Move-OldStandaloneBinIfApproved -VisibleBinDir $visibleBinDir -DefaultVisibleBinDir $defaultVisibleBinDir
|
|
try {
|
|
Ensure-Junction -LinkPath $visibleBinDir -TargetPath $currentDir -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
|
|
}
|
|
|
|
Maybe-HandleConflictingInstall -Conflict $conflictingInstall
|
|
|
|
$userPath = [Environment]::GetEnvironmentVariable("Path", "User")
|
|
if (-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 (-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
|
|
}
|