private/Invoke-WtwCodexProject.ps1
|
function Get-WtwCodexHome { [CmdletBinding()] param() if ($env:CODEX_HOME) { return [System.IO.Path]::GetFullPath($env:CODEX_HOME) } return [System.IO.Path]::GetFullPath((Join-Path $HOME '.codex')) } function Test-WtwCodexPresent { [CmdletBinding()] param( [string] $CodexHome = (Get-WtwCodexHome) ) if ($CodexHome -and (Test-Path $CodexHome)) { return $true } if (Get-Command codex -ErrorAction SilentlyContinue) { return $true } if ($IsMacOS -and (Test-Path '/Applications/Codex.app')) { return $true } return $false } function Test-WtwCodexAppRunning { [CmdletBinding()] param() return [bool](Get-Process -Name 'Codex' -ErrorAction SilentlyContinue) } function Start-WtwCodexApp { [CmdletBinding()] param( [Parameter(Mandatory)][string] $ProjectPath ) $codexExe = (Get-Command codex -ErrorAction SilentlyContinue)?.Source if ($codexExe) { Start-Process -FilePath $codexExe -ArgumentList @('app', $ProjectPath) return $true } if ($IsMacOS -and (Test-Path '/Applications/Codex.app')) { & open -a Codex $ProjectPath return $true } return $false } function Stop-WtwCodexProcess { [CmdletBinding()] param([int] $TimeoutSeconds = 10) $procs = @(Get-Process -Name 'Codex' -ErrorAction SilentlyContinue) if ($procs.Count -eq 0) { return $true } foreach ($process in $procs) { try { $process.CloseMainWindow() | Out-Null } catch { } } $deadline = (Get-Date).AddSeconds($TimeoutSeconds) while ((Get-Date) -lt $deadline) { if (-not (Test-WtwCodexAppRunning)) { return $true } Start-Sleep -Milliseconds 250 } foreach ($process in @(Get-Process -Name 'Codex' -ErrorAction SilentlyContinue)) { try { $process.Kill($true) } catch { try { $process.Kill() } catch { } } } $deadline = (Get-Date).AddSeconds(5) while ((Get-Date) -lt $deadline) { if (-not (Test-WtwCodexAppRunning)) { return $true } Start-Sleep -Milliseconds 250 } return -not (Test-WtwCodexAppRunning) } function Resolve-WtwCodexStateConflict { [CmdletBinding()] param([string] $OperationLabel = 'update Codex project metadata') if (-not (Test-WtwCodexAppRunning)) { return @{ proceed = $true; relaunch = $false } } Write-Host '' Write-Host ' Codex is running — it overwrites project labels on exit.' -ForegroundColor Yellow Write-Host " How should I $OperationLabel"'?' -ForegroundColor Yellow Write-Host ' [c] Close Codex yourself, then write (I will wait, then relaunch)' Write-Host ' [k] Force-kill Codex, write, relaunch' Write-Host ' [i] Ignore — write anyway (Codex may overwrite it)' Write-Host ' [s] Skip — open without changing the sidebar label' $answer = (Read-Host ' Choice [c/k/i/s]').Trim().ToLowerInvariant() if (-not $answer) { $answer = 'c' } switch ($answer) { 'c' { Write-Host ' Waiting for Codex to close (Ctrl+C to abort)...' -ForegroundColor Cyan while (Test-WtwCodexAppRunning) { Start-Sleep -Milliseconds 500 } Write-Host ' Codex closed.' -ForegroundColor Green return @{ proceed = $true; relaunch = $true } } 'k' { Write-Host ' Force-closing Codex...' -ForegroundColor Cyan if (-not (Stop-WtwCodexProcess)) { Write-Host ' Could not stop Codex — skipping label update.' -ForegroundColor Red return @{ proceed = $false; relaunch = $false } } Write-Host ' Codex stopped.' -ForegroundColor Green return @{ proceed = $true; relaunch = $true } } 's' { Write-Host ' Skipped Codex label update.' -ForegroundColor DarkGray return @{ proceed = $false; relaunch = $false } } default { Write-Host ' Writing anyway — quit Codex before restarting if it does not stick.' -ForegroundColor Yellow return @{ proceed = $true; relaunch = $false } } } } function ConvertTo-WtwTomlQuotedKey { [CmdletBinding()] param( [Parameter(Mandatory, Position = 0)] [string] $Value ) $escaped = $Value.Replace('\', '\\').Replace('"', '\"') return '"' + $escaped + '"' } function Get-WtwCodexProjectHeader { [CmdletBinding()] param( [Parameter(Mandatory, Position = 0)] [string] $ProjectPath ) return '[projects.{0}]' -f (ConvertTo-WtwTomlQuotedKey $ProjectPath) } function Set-WtwCodexProjectTrust { [CmdletBinding()] param( [Parameter(Mandatory)][string] $ProjectPath, [string] $ConfigPath = (Join-Path (Get-WtwCodexHome) 'config.toml') ) $configDir = Split-Path $ConfigPath -Parent if (-not (Test-Path $configDir)) { New-Item -Path $configDir -ItemType Directory -Force | Out-Null } $raw = if (Test-Path $ConfigPath) { Get-Content -Path $ConfigPath -Raw } else { '' } $header = Get-WtwCodexProjectHeader $ProjectPath $sectionPattern = "(?ms)^$([regex]::Escape($header))\s*(\r?\n)(.*?)(?=^\[|\z)" $trustLine = 'trust_level = "trusted"' $match = [regex]::Match($raw, $sectionPattern) if ($match.Success) { $section = $match.Groups[0].Value $lineBreak = $match.Groups[1].Value $body = $match.Groups[3].Value if ($body -match '(?m)^trust_level\s*=') { $newBody = [regex]::Replace($body, '(?m)^trust_level\s*=.*$', $trustLine, 1) } else { $newBody = $body.TrimEnd() + $lineBreak + $trustLine + $lineBreak } $newSection = $header + $lineBreak + $newBody $raw = $raw.Substring(0, $match.Index) + $newSection + $raw.Substring($match.Index + $section.Length) } else { $prefix = if ([string]::IsNullOrWhiteSpace($raw)) { '' } else { $raw.TrimEnd() + [Environment]::NewLine + [Environment]::NewLine } $raw = $prefix + $header + [Environment]::NewLine + $trustLine + [Environment]::NewLine } Set-Content -Path $ConfigPath -Value $raw -Encoding utf8 } function Remove-WtwCodexProjectTrust { [CmdletBinding()] param( [Parameter(Mandatory)][string] $ProjectPath, [string] $ConfigPath = (Join-Path (Get-WtwCodexHome) 'config.toml') ) if (-not (Test-Path $ConfigPath)) { return } $raw = Get-Content -Path $ConfigPath -Raw $header = Get-WtwCodexProjectHeader $ProjectPath $sectionPattern = "(?ms)^$([regex]::Escape($header))\s*(\r?\n)(.*?)(?=^\[|\z)" $match = [regex]::Match($raw, $sectionPattern) if (-not $match.Success) { return } $section = $match.Groups[0].Value $lineBreak = $match.Groups[1].Value $body = $match.Groups[3].Value $body = [regex]::Replace($body, '(?m)^trust_level\s*=.*(?:\r?\n)?', '') if ([string]::IsNullOrWhiteSpace($body)) { $raw = $raw.Substring(0, $match.Index) + $raw.Substring($match.Index + $section.Length) } else { $newSection = $header + $lineBreak + $body.TrimEnd() + $lineBreak $raw = $raw.Substring(0, $match.Index) + $newSection + $raw.Substring($match.Index + $section.Length) } Set-Content -Path $ConfigPath -Value $raw.TrimEnd() -Encoding utf8 } function Add-WtwCodexArrayValue { [CmdletBinding()] param( [Parameter(Mandatory)][PSObject] $State, [Parameter(Mandatory)][string] $PropertyName, [Parameter(Mandatory)][string] $Value, [switch] $Prepend ) $items = @() $existing = $State.PSObject.Properties[$PropertyName] if ($existing -and $null -ne $existing.Value) { $items = @($existing.Value) | Where-Object { $_ -and $_ -ne $Value } } $items = if ($Prepend) { @($Value) + $items } else { $items + @($Value) } $State | Add-Member -NotePropertyName $PropertyName -NotePropertyValue @($items) -Force } function Remove-WtwCodexArrayValue { [CmdletBinding()] param( [Parameter(Mandatory)][PSObject] $State, [Parameter(Mandatory)][string] $PropertyName, [Parameter(Mandatory)][string] $Value ) $existing = $State.PSObject.Properties[$PropertyName] if (-not $existing) { return } $items = @($existing.Value) | Where-Object { $_ -and $_ -ne $Value } $State | Add-Member -NotePropertyName $PropertyName -NotePropertyValue @($items) -Force } function Set-WtwCodexProjectLabel { [CmdletBinding()] param( [Parameter(Mandatory)][string] $ProjectPath, [Parameter(Mandatory)][string] $PrettyName, [string] $GlobalStatePath = (Join-Path (Get-WtwCodexHome) '.codex-global-state.json') ) $stateDir = Split-Path $GlobalStatePath -Parent if (-not (Test-Path $stateDir)) { New-Item -Path $stateDir -ItemType Directory -Force | Out-Null } if (Test-Path $GlobalStatePath) { try { $state = Get-Content -Path $GlobalStatePath -Raw | ConvertFrom-Json } catch { Write-Host " Codex: could not parse desktop state — skipping sidebar label update." -ForegroundColor Yellow return $false } } else { $state = [PSCustomObject]@{} } Add-WtwCodexArrayValue -State $state -PropertyName 'electron-saved-workspace-roots' -Value $ProjectPath -Prepend Add-WtwCodexArrayValue -State $state -PropertyName 'project-order' -Value $ProjectPath -Prepend $labelsProp = $state.PSObject.Properties['electron-workspace-root-labels'] $labels = if ($labelsProp -and $labelsProp.Value) { $labelsProp.Value } else { [PSCustomObject]@{} } $labels | Add-Member -NotePropertyName $ProjectPath -NotePropertyValue $PrettyName -Force $state | Add-Member -NotePropertyName 'electron-workspace-root-labels' -NotePropertyValue $labels -Force try { $state | ConvertTo-Json -Depth 80 -Compress | Set-Content -Path $GlobalStatePath -Encoding utf8 return $true } catch { Write-Host " Codex: could not save desktop state — skipping sidebar label update." -ForegroundColor Yellow return $false } } function Get-WtwCodexProjectLabel { [CmdletBinding()] param( [Parameter(Mandatory)][string] $ProjectPath, [string] $GlobalStatePath = (Join-Path (Get-WtwCodexHome) '.codex-global-state.json') ) if (-not (Test-Path $GlobalStatePath)) { return $null } try { $state = Get-Content -Path $GlobalStatePath -Raw | ConvertFrom-Json } catch { return $null } $labelsProp = $state.PSObject.Properties['electron-workspace-root-labels'] if (-not ($labelsProp -and $labelsProp.Value)) { return $null } $labelProp = $labelsProp.Value.PSObject.Properties[$ProjectPath] if ($labelProp) { return [string]$labelProp.Value } $trimmedPath = $ProjectPath.TrimEnd([char]'/', [char]'\') if ($trimmedPath -ne $ProjectPath) { $labelProp = $labelsProp.Value.PSObject.Properties[$trimmedPath] if ($labelProp) { return [string]$labelProp.Value } } return $null } function Test-WtwCodexProjectLabel { [CmdletBinding()] param( [Parameter(Mandatory)][string] $ProjectPath, [Parameter(Mandatory)][string] $PrettyName, [string] $GlobalStatePath = (Join-Path (Get-WtwCodexHome) '.codex-global-state.json') ) $label = Get-WtwCodexProjectLabel -ProjectPath $ProjectPath -GlobalStatePath $GlobalStatePath return $label -eq $PrettyName } function Remove-WtwCodexProjectLabel { [CmdletBinding()] param( [Parameter(Mandatory)][string] $ProjectPath, [string] $GlobalStatePath = (Join-Path (Get-WtwCodexHome) '.codex-global-state.json') ) if (-not (Test-Path $GlobalStatePath)) { return $false } try { $state = Get-Content -Path $GlobalStatePath -Raw | ConvertFrom-Json } catch { Write-Host " Codex: could not parse desktop state — skipping sidebar cleanup." -ForegroundColor Yellow return $false } Remove-WtwCodexArrayValue -State $state -PropertyName 'electron-saved-workspace-roots' -Value $ProjectPath Remove-WtwCodexArrayValue -State $state -PropertyName 'project-order' -Value $ProjectPath Remove-WtwCodexArrayValue -State $state -PropertyName 'active-workspace-roots' -Value $ProjectPath $labelsProp = $state.PSObject.Properties['electron-workspace-root-labels'] if ($labelsProp -and $labelsProp.Value) { $labelsProp.Value.PSObject.Properties.Remove($ProjectPath) } try { $state | ConvertTo-Json -Depth 80 -Compress | Set-Content -Path $GlobalStatePath -Encoding utf8 return $true } catch { Write-Host " Codex: could not save desktop state — skipping sidebar cleanup." -ForegroundColor Yellow return $false } } function Ensure-WtwCodexProjectConfig { [CmdletBinding()] param( [Parameter(Mandatory)][string] $ProjectPath ) $codexDir = Join-Path $ProjectPath '.codex' $configPath = Join-Path $codexDir 'config.toml' if (Test-Path $configPath) { return $false } if (-not (Test-Path $codexDir)) { New-Item -Path $codexDir -ItemType Directory -Force | Out-Null } $content = @( '[features]', 'hooks = true', '' ) -join [Environment]::NewLine Set-Content -Path $configPath -Value $content -Encoding utf8 return $true } function Register-WtwCodexProject { <# .SYNOPSIS Register a worktree as a Codex Desktop workspace when Codex is present. .DESCRIPTION Best-effort integration: creates a minimal project config only when missing, trusts the worktree path in ~/.codex/config.toml, and updates Codex Desktop's known workspace roots/sidebar label if the desktop state file exists. #> [CmdletBinding()] param( [Parameter(Mandatory)][string] $ProjectPath, [Parameter(Mandatory)][string] $PrettyName ) $fullPath = [System.IO.Path]::GetFullPath($ProjectPath) $codexHome = Get-WtwCodexHome if (-not (Test-WtwCodexPresent -CodexHome $codexHome)) { Write-Host ' Codex: not installed/present — skipping project registration.' -ForegroundColor DarkGray return $null } $isAppRunning = Test-WtwCodexAppRunning $createdProjectConfig = Ensure-WtwCodexProjectConfig -ProjectPath $fullPath Set-WtwCodexProjectTrust -ProjectPath $fullPath -ConfigPath (Join-Path $codexHome 'config.toml') $labelUpdated = if ($isAppRunning) { $false } else { Set-WtwCodexProjectLabel -ProjectPath $fullPath -PrettyName $PrettyName -GlobalStatePath (Join-Path $codexHome '.codex-global-state.json') } Write-Host " Codex: trusted project $fullPath" -ForegroundColor Green if ($createdProjectConfig) { Write-Host ' Codex: created .codex/config.toml' -ForegroundColor Green } if ($labelUpdated) { Write-Host " Codex: sidebar label '$PrettyName'" -ForegroundColor Green } elseif ($isAppRunning) { Write-Host " Codex: app is running; run 'wtw codex' to close/relaunch and finalize sidebar label '$PrettyName'." -ForegroundColor DarkGray } else { Write-Host " Codex: run 'wtw codex' from the worktree to open it in Codex Desktop." -ForegroundColor DarkGray } return $fullPath } function Unregister-WtwCodexProject { <# .SYNOPSIS Remove a worktree from Codex Desktop's local project metadata. .DESCRIPTION Best-effort cleanup for the trust entry and sidebar/root state. Safe to call when Codex is absent or the project was never registered. #> [CmdletBinding()] param( [Parameter(Mandatory)][string] $ProjectPath ) $fullPath = [System.IO.Path]::GetFullPath($ProjectPath) $codexHome = Get-WtwCodexHome if (-not (Test-WtwCodexPresent -CodexHome $codexHome)) { return } Remove-WtwCodexProjectTrust -ProjectPath $fullPath -ConfigPath (Join-Path $codexHome 'config.toml') $labelRemoved = Remove-WtwCodexProjectLabel -ProjectPath $fullPath -GlobalStatePath (Join-Path $codexHome '.codex-global-state.json') Write-Host " Codex: removed project metadata for $fullPath" -ForegroundColor Green if ($labelRemoved) { Write-Host ' Codex: removed sidebar label/root entries.' -ForegroundColor Green } } |