Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,5 @@
*.log

*.dmg

.env
236 changes: 233 additions & 3 deletions scripts/run.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,118 @@ param(
[string]$DmgPath,
[string]$WorkDir = (Join-Path $PSScriptRoot "..\work"),
[string]$CodexCliPath,
[string]$CodexHome,
[switch]$Reuse,
[switch]$NoLaunch
)

Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"

function Parse-DotEnvValue([string]$Value) {
if ($null -eq $Value) { return $null }
$v = $Value.Trim()
if ($v.StartsWith('"') -and $v.EndsWith('"') -and $v.Length -ge 2) {
return $v.Substring(1, $v.Length - 2) -replace '\"', '"'
}
if ($v.StartsWith("'") -and $v.EndsWith("'") -and $v.Length -ge 2) {
return $v.Substring(1, $v.Length - 2) -replace "''", "'"
}
return $v
}

function Read-DotEnv([string]$Path) {
$map = @{}
if (-not (Test-Path $Path)) { return $map }
foreach ($line in (Get-Content -LiteralPath $Path -ErrorAction SilentlyContinue)) {
if ($null -eq $line) { continue }
$t = $line.Trim()
if ($t.Length -eq 0) { continue }
if ($t.StartsWith("#")) { continue }
$idx = $t.IndexOf("=")
if ($idx -lt 1) { continue }
$k = $t.Substring(0, $idx).Trim()
if ($k.Length -gt 0 -and $k[0] -eq [char]0xFEFF) { $k = $k.TrimStart([char]0xFEFF) }
$v = $t.Substring($idx + 1)
if (-not $k) { continue }
$parsed = (Parse-DotEnvValue $v)
if ($null -eq $parsed) { continue }
if (($parsed -is [string]) -and ($parsed.Trim().Length -eq 0)) { continue }
$map[$k] = $parsed
}
return $map
}

function Ensure-DotEnvTemplate([string]$Path) {
if (-not $Path) { return }
$dir = Split-Path $Path -Parent
if ($dir) { New-Item -ItemType Directory -Force -Path $dir | Out-Null }

$enc = [System.Text.UTF8Encoding]::new($false)
if (-not (Test-Path $Path)) {
$content = @(
"# Codex Windows local config"
"# Set CODEX_HOME to your Codex profile directory (leave empty to use default)."
"CODEX_HOME="
""
) -join "`n"
[System.IO.File]::WriteAllText($Path, $content, $enc)
return
}

try {
$text = Get-Content -LiteralPath $Path -Raw -ErrorAction SilentlyContinue
} catch {
return
}
if ($null -eq $text) { $text = "" }
if ($text -match "(?m)^\\s*CODEX_HOME\\s*=") { return }

if ($text.Length -gt 0 -and -not $text.EndsWith("`n")) { $text += "`n" }
if ($text.Length -gt 0 -and -not $text.EndsWith("`n`n")) { $text += "`n" }
$text += "CODEX_HOME=`n"
[System.IO.File]::WriteAllText($Path, $text, $enc)
}

function Set-DotEnvVar([string]$Path, [string]$Key, [string]$Value) {
$dir = Split-Path $Path -Parent
if ($dir) { New-Item -ItemType Directory -Force -Path $dir | Out-Null }

$lines = @()
if (Test-Path $Path) {
$lines = @(Get-Content -LiteralPath $Path -ErrorAction SilentlyContinue)
}

$escaped = ($Value -replace '"', '\"')
$newLine = "$Key=""$escaped"""

$updated = $false
for ($i = 0; $i -lt $lines.Count; $i++) {
$current = $lines[$i]
if ($null -eq $current) { $current = "" }
$t = $current.Trim()
if ($t.StartsWith("#")) { continue }
if ($t -match ("^\s*" + [regex]::Escape($Key) + "\s*=")) {
$lines[$i] = $newLine
$updated = $true
break
}
}

if (-not $updated) {
$last = $null
if ($lines.Count -gt 0) { $last = $lines[$lines.Count - 1] }
if ($null -eq $last) { $last = "" }
if ($lines.Count -gt 0 -and $last.Trim().Length -ne 0) {
$lines += ""
}
$lines += $newLine
}

# Avoid UTF-8 BOM (it can break simple dotenv key parsing).
[System.IO.File]::WriteAllLines($Path, $lines, [System.Text.UTF8Encoding]::new($false))
}

function Ensure-Command([string]$Name) {
if (-not (Get-Command $Name -ErrorAction SilentlyContinue)) {
throw "$Name not found."
Expand Down Expand Up @@ -99,6 +204,71 @@ function Resolve-CodexCliPath([string]$Explicit) {
return $null
}

function Patch-AppMainJs([string]$AppDir) {
$mainJs = Join-Path $AppDir ".vite\\build\\main.js"
if (-not (Test-Path $mainJs)) { return }

$text = Get-Content -LiteralPath $mainJs -Raw
$insertions = @()

# Implement "open-config-toml" in Electron by opening (and creating if needed) $CODEX_HOME/config.toml.
if ($text -notmatch "Failed to open config\\.toml") {
$insertions += ';(()=>{try{const e=Sue?.prototype?.handleMessage;if(typeof e!="function")return;Sue.prototype.handleMessage=async function(t,n){if(n?.type==="open-config-toml"){try{const r=[zn({preferWsl:!0}),zn({preferWsl:!1})].filter(i=>typeof i=="string"&&i.length>0),a=r.map(i=>ae.join(i,"config.toml"));let o=a.find(i=>be.existsSync(i))??a[a.length-1];o&&(be.mkdirSync(ae.dirname(o),{recursive:!0}),be.existsSync(o)||be.writeFileSync(o,"# Codex configuration\n","utf8"),await F.shell.openPath(o))}catch(r){try{Ft().error("Failed to open config.toml",r)}catch{}}return}return e.call(this,t,n)}}catch{}})();'
}

# If a persisted host config points at a Windows npm shim (e.g. %APPDATA%\\npm\\codex),
# patch child_process.spawn to transparently resolve a real codex.exe instead.
if ($text -notmatch "__codexWindowsPatched") {
$insertions += ';(()=>{try{if(process.platform!=="win32")return;const e=require("child_process"),t=require("fs"),n=require("path");if(e.spawn&&e.spawn.__codexWindowsPatched)return;const r=e.spawn;function i(){try{const a=process.env.CODEX_CLI_PATH||process.env.CUSTOM_CLI_PATH;if(a&&/\\.exe$/i.test(a)&&t.existsSync(a))return a}catch{}try{const a=process.env.APPDATA;if(a){for(const o of["x86_64-pc-windows-msvc","aarch64-pc-windows-msvc"]){const s=n.join(a,"npm","node_modules","@openai","codex","vendor",o,"codex","codex.exe");if(t.existsSync(s))return s}}}catch{}return null}e.spawn=function(a,o,s){try{if(typeof a=="string"){const c=n.basename(a.toLowerCase());if(c==="codex"||c==="codex.cmd"||c==="codex.ps1"){const u=i();u&&(a=u)}}}catch{}return r.call(this,a,o,s)};e.spawn.__codexWindowsPatched=!0}catch{}})();'
}

if ($insertions.Count -eq 0) { return }

$insertionText = ($insertions -join "`n") + "`n"
$pattern = "(?m)^//# sourceMappingURL=main\\.js\\.map\\s*$"
if ($text -match $pattern) {
$text = [regex]::Replace($text, $pattern, $insertionText + "//# sourceMappingURL=main.js.map", 1)
} else {
$text = $text + "`n" + $insertionText
}

[System.IO.File]::WriteAllText($mainJs, $text, [System.Text.UTF8Encoding]::new($false))

# Patch renderer bundle logging so errors don't show up as "[object Object]".
$assetsDir = Join-Path $AppDir "webview\\assets"
if (Test-Path $assetsDir) {
$indexFiles = Get-ChildItem -LiteralPath $assetsDir -Filter "index-*.js" -File -ErrorAction SilentlyContinue
foreach ($f in $indexFiles) {
try {
$rt = Get-Content -LiteralPath $f.FullName -Raw
$orig = $rt

# Prefer a more verbose formatter than sanitizeLogValue() for key error logs.
# sanitizeLogValue intentionally collapses objects to "object(keys=N)", which is still too opaque for debugging.
$verboseLt = '${(()=>{try{return typeof lt==="string"?lt:lt&&typeof lt==="object"?JSON.stringify(lt,(k,v)=>v instanceof Error?{name:v.name,message:v.message,stack:v.stack}:v):String(lt)}catch(e){try{return String(lt)}catch(e2){return "unserializable"}}})()}'
$verboseKt = '${(()=>{try{return typeof Kt==="string"?Kt:Kt&&typeof Kt==="object"?JSON.stringify(Kt,(k,v)=>v instanceof Error?{name:v.name,message:v.message,stack:v.stack}:v):String(Kt)}catch(e){try{return String(Kt)}catch(e2){return "unserializable"}}})()}'

# Update older variants and the intermediate sanitizeLogValue variant.
$rt = $rt.Replace('Received app server error: ${Ye} ${String(lt)}', ('Received app server error: ${Ye} ' + $verboseLt))
$rt = $rt.Replace('Received app server error: ${Ye} ${sanitizeLogValue(lt)}', ('Received app server error: ${Ye} ' + $verboseLt))

$rt = $rt.Replace('[worktree-cleanup] failed to refresh cleanup inputs: ${String(Kt)}', ('[worktree-cleanup] failed to refresh cleanup inputs: ' + $verboseKt))
$rt = $rt.Replace('[worktree-cleanup] failed to refresh cleanup inputs: ${sanitizeLogValue(Kt)}', ('[worktree-cleanup] failed to refresh cleanup inputs: ' + $verboseKt))

# A few other common noisy logs.
$rt = $rt.Replace('[automation-run-cleanup] failed to refresh conversations: ${String(St)}', '[automation-run-cleanup] failed to refresh conversations: ${sanitizeLogValue(St)}')
$rt = $rt.Replace('[automation-run-cleanup] failed to load more conversations: ${String(St)}', '[automation-run-cleanup] failed to load more conversations: ${sanitizeLogValue(St)}')
$rt = $rt.Replace('[automation-run-cleanup] failed to archive conversation ${Et}: ${String(Ct)}', '[automation-run-cleanup] failed to archive conversation ${Et}: ${sanitizeLogValue(Ct)}')
$rt = $rt.Replace('[automation-run-cleanup] failed to mark run archived ${Et}: ${String(Ct)}', '[automation-run-cleanup] failed to mark run archived ${Et}: ${sanitizeLogValue(Ct)}')

if ($rt -ne $orig) {
[System.IO.File]::WriteAllText($f.FullName, $rt, [System.Text.UTF8Encoding]::new($false))
}
} catch {}
}
}
}

function Write-Header([string]$Text) {
Write-Host "`n=== $Text ===" -ForegroundColor Cyan
}
Expand Down Expand Up @@ -132,6 +302,62 @@ function Ensure-GitOnPath() {
}
}

$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
$dotEnvPath = Join-Path $repoRoot ".env"
Ensure-DotEnvTemplate $dotEnvPath
$dotEnv = Read-DotEnv $dotEnvPath

$dotEnvCodexHome = if ($dotEnv.ContainsKey("CODEX_HOME")) { $dotEnv["CODEX_HOME"] } else { $null }
$envCodexHome = if ($env:CODEX_HOME) { $env:CODEX_HOME } elseif ($env:CODEX_Home) { $env:CODEX_Home } else { $null }

function Normalize-PathString([string]$Path) {
if (-not $Path) { return $null }
$p = [Environment]::ExpandEnvironmentVariables($Path).Trim().Trim('"').Trim("'")
$p = $p -replace "/", "\"
try {
$full = [System.IO.Path]::GetFullPath($p)
$root = [System.IO.Path]::GetPathRoot($full)
if ($root -and ($full.Length -le $root.Length)) { return $root }
return $full.TrimEnd("\")
} catch {
if ($p -match "^[a-zA-Z]:\\$") { return $p }
if ($p -match "^\\\\[^\\]+\\[^\\]+\\$") { return $p }
return $p.TrimEnd("\")
}
}

$defaultCodexHome = Join-Path ([Environment]::GetFolderPath("UserProfile")) ".codex"
$envIsDefaultHome = (Normalize-PathString $envCodexHome) -eq (Normalize-PathString $defaultCodexHome)
$envDiffersFromDotEnv = $dotEnvCodexHome -and ((Normalize-PathString $envCodexHome) -ne (Normalize-PathString $dotEnvCodexHome))

# Treat CODEX_HOME from the environment as an explicit override only when:
# - there is no `.env` yet, OR
# - it differs from `.env` AND it's not the default "~/.codex" value (avoids a system default overriding a chosen profile).
$envCodexHomeExplicit = $false
if ($envCodexHome) {
if (-not $dotEnvCodexHome) { $envCodexHomeExplicit = $true }
elseif ($envDiffersFromDotEnv -and -not $envIsDefaultHome) { $envCodexHomeExplicit = $true }
}

# Precedence:
# - `-CodexHome` (always explicit + persist)
# - env CODEX_HOME (if explicit per rules above + persist)
# - `.env` (persisted default)
# - env CODEX_HOME (system/user env fallback)
$desiredCodexHome = if ($CodexHome) { $CodexHome } elseif ($envCodexHomeExplicit) { $envCodexHome } elseif ($dotEnvCodexHome) { $dotEnvCodexHome } elseif ($envCodexHome) { $envCodexHome } else { $null }
if ($desiredCodexHome) {
Write-Header "Configuring CODEX_HOME"
$resolvedCodexHome = [Environment]::ExpandEnvironmentVariables($desiredCodexHome)
New-Item -ItemType Directory -Force -Path $resolvedCodexHome | Out-Null
$env:CODEX_HOME = (Resolve-Path $resolvedCodexHome).Path

# Only rewrite `.env` when the user explicitly requests a change.
if ($CodexHome -or $envCodexHomeExplicit) {
Set-DotEnvVar $dotEnvPath "CODEX_HOME" $env:CODEX_HOME
}
Write-Host "CODEX_HOME=$($env:CODEX_HOME)" -ForegroundColor Cyan
}

Ensure-Command node
Ensure-Command npm
Ensure-Command npx
Expand Down Expand Up @@ -200,11 +426,15 @@ if (-not $Reuse) {

Write-Header "Syncing app.asar.unpacked"
$unpacked = Join-Path $electronDir "Codex Installer\Codex.app\Contents\Resources\app.asar.unpacked"
if (Test-Path $unpacked) {
& robocopy $unpacked $appDir /E /NFL /NDL /NJH /NJS /NC /NS | Out-Null
}
if (Test-Path $unpacked) {
& robocopy $unpacked $appDir /E /NFL /NDL /NJH /NJS /NC /NS | Out-Null
}

}

Write-Header "Patching Electron bundle"
Patch-AppMainJs $appDir

Write-Header "Patching preload"
Patch-Preload $appDir

Expand Down