fix(ci): tolerate missing client CLIs
CI / Capabilities and shell quality (push) Canceled after 0s
CI / Python tests and compilation (push) Canceled after 0s
CI / PowerShell tests on Ubuntu (push) Canceled after 0s
CI / Build and inspect generated packages (push) Canceled after 0s
CI / Bash integration and installation guards (push) Canceled after 0s
CI / Installed client configuration smoke tests (push) Canceled after 0s
CI / Qwen retrieval integration (push) Canceled after 0s
CI / CI configuration security checks (push) Canceled after 0s

This commit is contained in:
fraujulian committed 2026-09-25 08:44:08 +00:00
1 parent 632afdf6b4
commit fedbc32e71
1 file changed
+188 -187
+188 -187
View File
@@ -1,187 +1,188 @@
function Invoke-PluginCommand { function Invoke-PluginCommand {
param( param(
[Parameter(Mandatory=$true)][string]$Command, [Parameter(Mandatory=$true)][string]$Command,
[Parameter(Mandatory=$true)][string[]]$Arguments, [Parameter(Mandatory=$true)][string[]]$Arguments,
[switch]$DryRun, [switch]$DryRun,
[switch]$Summary [switch]$Summary
) )
$display = "$Command $($Arguments -join ' ')" $display = "$Command $($Arguments -join ' ')"
if ($DryRun) { if (-not $Summary) { Write-Output "DRYRUN $display" }; return } if ($DryRun) { if (-not $Summary) { Write-Output "DRYRUN $display" }; return }
$resolvedCommand = Get-Command $Command -ErrorAction Stop $resolvedCommand = Get-Command $Command -ErrorAction Stop
if (-not $Summary) { if (-not $Summary) {
if ($script:PluginNonInteractive) { $null | & $resolvedCommand @Arguments } else { & $resolvedCommand @Arguments } if ($script:PluginNonInteractive) { $null | & $resolvedCommand @Arguments } else { & $resolvedCommand @Arguments }
if ($LASTEXITCODE -ne 0) { throw "Plugin command failed: $display" } if ($LASTEXITCODE -ne 0) { throw "Plugin command failed: $display" }
return return
} }
$previousPreference = $ErrorActionPreference $previousPreference = $ErrorActionPreference
try { try {
$ErrorActionPreference = 'Continue' $ErrorActionPreference = 'Continue'
$output = if ($script:PluginNonInteractive) { @($null | & $resolvedCommand @Arguments 2>&1) } else { @(& $resolvedCommand @Arguments 2>&1) } $output = if ($script:PluginNonInteractive) { @($null | & $resolvedCommand @Arguments 2>&1) } else { @(& $resolvedCommand @Arguments 2>&1) }
$exitCode = $LASTEXITCODE $exitCode = $LASTEXITCODE
} finally { $ErrorActionPreference = $previousPreference } } finally { $ErrorActionPreference = $previousPreference }
if ($exitCode -ne 0 -or -not $Summary -or $env:AI_CONFIG_VERBOSE -eq '1') { $output | Write-Output } else { $output | Where-Object { "$_" -match '(?i)warn|error|fail|deprecat' } } if ($exitCode -ne 0 -or -not $Summary -or $env:AI_CONFIG_VERBOSE -eq '1') { $output | Write-Output } else { $output | Where-Object { "$_" -match '(?i)warn|error|fail|deprecat' } }
if ($exitCode -ne 0) { throw "Plugin command failed: $display" } if ($exitCode -ne 0) { throw "Plugin command failed: $display" }
} }
function Get-ConfiguredPluginEntries { function Get-ConfiguredPluginEntries {
param([Parameter(Mandatory=$true)][string]$RepositoryRoot) param([Parameter(Mandatory=$true)][string]$RepositoryRoot)
$manifest = Join-Path $RepositoryRoot 'adapters/plugins.tsv' $manifest = Join-Path $RepositoryRoot 'adapters/plugins.tsv'
if (-not (Test-Path -LiteralPath $manifest)) { throw "Plugin manifest is missing: $manifest" } if (-not (Test-Path -LiteralPath $manifest)) { throw "Plugin manifest is missing: $manifest" }
Import-Csv -LiteralPath $manifest -Delimiter ([char]9) | ForEach-Object { Import-Csv -LiteralPath $manifest -Delimiter ([char]9) | ForEach-Object {
$_.claude_plugin = $_.claude_plugin.Trim() $_.claude_plugin = $_.claude_plugin.Trim()
$_.codex_plugin = $_.codex_plugin.Trim() $_.codex_plugin = $_.codex_plugin.Trim()
$_ $_
} }
} }
function Get-InstalledPlugins { function Get-InstalledPlugins {
param( param(
[ValidateSet('Claude','Codex')][string]$Client, [ValidateSet('Claude','Codex')][string]$Client,
[string]$HomePath = [Environment]::GetFolderPath('UserProfile') [string]$HomePath = [Environment]::GetFolderPath('UserProfile')
) )
$command = Get-Command $Client.ToLowerInvariant() -ErrorAction Stop $command = Get-Command $Client.ToLowerInvariant() -ErrorAction SilentlyContinue
$environment = @{ if (-not $command) { return @() }
HOME = $HomePath $environment = @{
USERPROFILE = $HomePath HOME = $HomePath
CODEX_HOME = (Join-Path $HomePath '.codex') USERPROFILE = $HomePath
CLAUDE_CONFIG_DIR = (Join-Path $HomePath '.claude') CODEX_HOME = (Join-Path $HomePath '.codex')
} CLAUDE_CONFIG_DIR = (Join-Path $HomePath '.claude')
$previousEnvironment = @{} }
foreach ($name in $environment.Keys) { $previousEnvironment[$name] = [Environment]::GetEnvironmentVariable($name, 'Process') } $previousEnvironment = @{}
try { foreach ($name in $environment.Keys) { $previousEnvironment[$name] = [Environment]::GetEnvironmentVariable($name, 'Process') }
foreach ($name in $environment.Keys) { [Environment]::SetEnvironmentVariable($name, $environment[$name], 'Process') } try {
$global:LASTEXITCODE = 0 foreach ($name in $environment.Keys) { [Environment]::SetEnvironmentVariable($name, $environment[$name], 'Process') }
$output = $null | & $command plugin list --json | Out-String $global:LASTEXITCODE = 0
if ($LASTEXITCODE -ne 0) { throw "$Client plugin listing failed." } $output = $null | & $command plugin list --json | Out-String
$items = $output | ConvertFrom-Json if ($LASTEXITCODE -ne 0) { throw "$Client plugin listing failed." }
if ($Client -eq 'Claude') { return @($items) } $items = $output | ConvertFrom-Json
return @($items.installed) if ($Client -eq 'Claude') { return @($items) }
} finally { return @($items.installed)
foreach ($name in $previousEnvironment.Keys) { } finally {
if ($null -eq $previousEnvironment[$name]) { Remove-Item -LiteralPath ("Env:" + $name) -ErrorAction SilentlyContinue } foreach ($name in $previousEnvironment.Keys) {
else { [Environment]::SetEnvironmentVariable($name, $previousEnvironment[$name], 'Process') } if ($null -eq $previousEnvironment[$name]) { Remove-Item -LiteralPath ("Env:" + $name) -ErrorAction SilentlyContinue }
} else { [Environment]::SetEnvironmentVariable($name, $previousEnvironment[$name], 'Process') }
} }
} }
}
function Read-PluginToggleSelection {
param( function Read-PluginToggleSelection {
[Parameter(Mandatory=$true)][string[]]$Names, param(
[Parameter(Mandatory=$true)][bool[]]$Checked [Parameter(Mandatory=$true)][string[]]$Names,
) [Parameter(Mandatory=$true)][bool[]]$Checked
$state = @($Checked) )
if ([Console]::IsInputRedirected -or $env:CI -eq 'true' -or $env:AI_CONFIG_NO_INTERACTIVE -eq '1') { while($true){ Write-Host 'Select plugins (enter a number to toggle, "done" to confirm):'; for($i=0;$i -lt $Names.Count;$i++){Write-Host (" {0}) [{1}] {2}" -f ($i+1),$(if($state[$i]){'x'}else{' '}),$Names[$i])}; $answer=Read-Host 'Toggle number or "done"'; if([string]::IsNullOrWhiteSpace($answer)-or $answer -eq 'done'){return $state}; $index=0;if([int]::TryParse($answer,[ref]$index)-and $index -ge 1 -and $index -le $Names.Count){$state[$index-1]= -not $state[$index-1]} } } $state = @($Checked)
$index=0; while($true){ Clear-Host; Write-Host 'Select plugins'; for($i=0;$i -lt $Names.Count;$i++){ $focus=if($i -eq $index){'>'}else{' '}; $mark=if($state[$i]){'x'}else{' '}; Write-Host "$focus [$mark] $($Names[$i])" }; Write-Host 'Up/Down Navigate Space Toggle Enter Confirm'; $key=[Console]::ReadKey($true); if($key.Key -eq 'UpArrow'){$index=($index+$Names.Count-1)%$Names.Count}elseif($key.Key -eq 'DownArrow'){$index=($index+1)%$Names.Count}elseif($key.Key -eq 'Spacebar'){$state[$index]= -not $state[$index]}elseif($key.Key -eq 'Enter'){Clear-Host;return $state} } if ([Console]::IsInputRedirected -or $env:CI -eq 'true' -or $env:AI_CONFIG_NO_INTERACTIVE -eq '1') { while($true){ Write-Host 'Select plugins (enter a number to toggle, "done" to confirm):'; for($i=0;$i -lt $Names.Count;$i++){Write-Host (" {0}) [{1}] {2}" -f ($i+1),$(if($state[$i]){'x'}else{' '}),$Names[$i])}; $answer=Read-Host 'Toggle number or "done"'; if([string]::IsNullOrWhiteSpace($answer)-or $answer -eq 'done'){return $state}; $index=0;if([int]::TryParse($answer,[ref]$index)-and $index -ge 1 -and $index -le $Names.Count){$state[$index-1]= -not $state[$index-1]} } }
} $index=0; while($true){ Clear-Host; Write-Host 'Select plugins'; for($i=0;$i -lt $Names.Count;$i++){ $focus=if($i -eq $index){'>'}else{' '}; $mark=if($state[$i]){'x'}else{' '}; Write-Host "$focus [$mark] $($Names[$i])" }; Write-Host 'Up/Down Navigate Space Toggle Enter Confirm'; $key=[Console]::ReadKey($true); if($key.Key -eq 'UpArrow'){$index=($index+$Names.Count-1)%$Names.Count}elseif($key.Key -eq 'DownArrow'){$index=($index+1)%$Names.Count}elseif($key.Key -eq 'Spacebar'){$state[$index]= -not $state[$index]}elseif($key.Key -eq 'Enter'){Clear-Host;return $state} }
}
function Select-ConfiguredPlugins {
<# function Select-ConfiguredPlugins {
.SYNOPSIS <#
Lets the user toggle each configured plugin on or off. Install mode starts .SYNOPSIS
with everything checked; update mode starts checked to match what the Lets the user toggle each configured plugin on or off. Install mode starts
reference client (Claude, or Codex when only Codex is targeted) already with everything checked; update mode starts checked to match what the
has installed, so unchecking a currently installed plugin uninstalls it. reference client (Claude, or Codex when only Codex is targeted) already
#> has installed, so unchecking a currently installed plugin uninstalls it.
param( #>
[Parameter(Mandatory=$true)][string]$RepositoryRoot, param(
[ValidateSet('Codex','Claude','Both')][Parameter(Mandatory=$true)][string]$Client, [Parameter(Mandatory=$true)][string]$RepositoryRoot,
[ValidateSet('Install','Update')][Parameter(Mandatory=$true)][string]$Mode, [ValidateSet('Codex','Claude','Both')][Parameter(Mandatory=$true)][string]$Client,
[string]$HomePath = [Environment]::GetFolderPath('UserProfile'), [ValidateSet('Install','Update')][Parameter(Mandatory=$true)][string]$Mode,
[switch]$DryRun [string]$HomePath = [Environment]::GetFolderPath('UserProfile'),
) [switch]$DryRun
$entries = @(Get-ConfiguredPluginEntries -RepositoryRoot $RepositoryRoot) )
$toggleable = @($entries) $entries = @(Get-ConfiguredPluginEntries -RepositoryRoot $RepositoryRoot)
$fixed = @($entries | Where-Object { $toggleable -notcontains $_ }) $toggleable = @($entries)
if ($toggleable.Count -eq 0 -or ($DryRun -and $Mode -eq 'Install')) { return @{ Selected = $entries; Deselected = @() } } $fixed = @($entries | Where-Object { $toggleable -notcontains $_ })
if ($toggleable.Count -eq 0 -or ($DryRun -and $Mode -eq 'Install')) { return @{ Selected = $entries; Deselected = @() } }
$referenceClients = if ($Client -eq 'Both') { @('Claude','Codex') } else { @($Client) }
$installed = @{} $referenceClients = if ($Client -eq 'Both') { @('Claude','Codex') } else { @($Client) }
if ($Mode -eq 'Update') { foreach ($referenceClient in $referenceClients) { $installed[$referenceClient] = @(Get-InstalledPlugins -Client $referenceClient -HomePath $HomePath) } } $installed = @{}
$checked = @($toggleable | ForEach-Object { if ($Mode -eq 'Update') { foreach ($referenceClient in $referenceClients) { $installed[$referenceClient] = @(Get-InstalledPlugins -Client $referenceClient -HomePath $HomePath) } }
if ($Mode -eq 'Install') { $true; return } $checked = @($toggleable | ForEach-Object {
if ($_.codex_method -eq 'cli') { return $null -ne (Get-Command $_.codex_source -ErrorAction SilentlyContinue) } if ($Mode -eq 'Install') { $true; return }
$entry = $_ if ($_.codex_method -eq 'cli') { return $null -ne (Get-Command $_.codex_source -ErrorAction SilentlyContinue) }
$found = $false $entry = $_
foreach ($referenceClient in $referenceClients) { $found = $false
$selector = if ($referenceClient -eq 'Claude') { $entry.claude_plugin } else { $entry.codex_plugin } foreach ($referenceClient in $referenceClients) {
if ($referenceClient -eq 'Codex' -and $entry.codex_method -eq 'qmd') { $selector = if ($referenceClient -eq 'Claude') { $entry.claude_plugin } else { $entry.codex_plugin }
$found = $found -or ($null -ne (Get-Command qmd -ErrorAction SilentlyContinue)) if ($referenceClient -eq 'Codex' -and $entry.codex_method -eq 'qmd') {
} elseif ($referenceClient -eq 'Codex' -and $entry.codex_method -ne 'plugin') { $found = $found -or ($null -ne (Get-Command qmd -ErrorAction SilentlyContinue))
$found = $found -or (Test-Path -LiteralPath (Join-Path $HomePath ('.agents/skills/' + $entry.codex_skill))) -or (Test-Path -LiteralPath (Join-Path $HomePath ('.codex/skills/' + $entry.codex_skill))) } elseif ($referenceClient -eq 'Codex' -and $entry.codex_method -ne 'plugin') {
} else { $found = $found -or (Test-Path -LiteralPath (Join-Path $HomePath ('.agents/skills/' + $entry.codex_skill))) -or (Test-Path -LiteralPath (Join-Path $HomePath ('.codex/skills/' + $entry.codex_skill)))
$idField = if ($referenceClient -eq 'Claude') { 'id' } else { 'pluginId' } } else {
$found = $found -or [bool](@($installed[$referenceClient] | Where-Object { $_.$idField -eq $selector }).Count) $idField = if ($referenceClient -eq 'Claude') { 'id' } else { 'pluginId' }
} $found = $found -or [bool](@($installed[$referenceClient] | Where-Object { $_.$idField -eq $selector }).Count)
} }
$found }
}) $found
$names = @($toggleable | ForEach-Object { $_.name }) })
if (-not $DryRun) { $checked = Read-PluginToggleSelection -Names $names -Checked $checked } $names = @($toggleable | ForEach-Object { $_.name })
if (-not $DryRun) { $checked = Read-PluginToggleSelection -Names $names -Checked $checked }
$selected = @()
$deselected = @() $selected = @()
for ($i = 0; $i -lt $toggleable.Count; $i++) { $deselected = @()
if ($checked[$i]) { $selected += $toggleable[$i] } else { $deselected += $toggleable[$i] } for ($i = 0; $i -lt $toggleable.Count; $i++) {
} if ($checked[$i]) { $selected += $toggleable[$i] } else { $deselected += $toggleable[$i] }
return @{ Selected = ($selected + $fixed); Deselected = $deselected } }
} return @{ Selected = ($selected + $fixed); Deselected = $deselected }
}
function Invoke-ManagedExtensions {
param( function Invoke-ManagedExtensions {
[Parameter(Mandatory=$true)][string]$RepositoryRoot, param(
[Parameter(Mandatory=$true)][string]$Client, [Parameter(Mandatory=$true)][string]$RepositoryRoot,
[string]$HomePath = $HOME, [Parameter(Mandatory=$true)][string]$Client,
[ValidateSet('sync','install','remove')][string]$Action = 'sync', [string]$HomePath = $HOME,
[object[]]$Entries, [ValidateSet('sync','install','remove')][string]$Action = 'sync',
[switch]$DryRun, [object[]]$Entries,
[switch]$Update, [switch]$DryRun,
[switch]$Summary [switch]$Update,
) [switch]$Summary
$arguments = @((Join-Path $PSScriptRoot 'managed-extensions.py'), $Action, '--home', $HomePath, '--manifest', (Join-Path $RepositoryRoot 'adapters/plugins.tsv'), '--client', $Client.ToLowerInvariant()) )
if ($DryRun) { $arguments += '--dry-run' } $arguments = @((Join-Path $PSScriptRoot 'managed-extensions.py'), $Action, '--home', $HomePath, '--manifest', (Join-Path $RepositoryRoot 'adapters/plugins.tsv'), '--client', $Client.ToLowerInvariant())
if ($Update) { $arguments += '--update' } if ($DryRun) { $arguments += '--dry-run' }
if ($Summary) { $arguments += '--summary' } if ($Update) { $arguments += '--update' }
if ($PSBoundParameters.ContainsKey('Entries')) { $arguments += '--selected'; $arguments += @($Entries | ForEach-Object { $_.name }) } if ($Summary) { $arguments += '--summary' }
& python @arguments if ($PSBoundParameters.ContainsKey('Entries')) { $arguments += '--selected'; $arguments += @($Entries | ForEach-Object { $_.name }) }
if ($LASTEXITCODE -ne 0) { throw 'Managed extension reconciliation failed.' } & python @arguments
} if ($LASTEXITCODE -ne 0) { throw 'Managed extension reconciliation failed.' }
}
function Sync-ConfiguredPlugins {
param( function Sync-ConfiguredPlugins {
[Parameter(Mandatory=$true)][string]$RepositoryRoot, param(
[ValidateSet('Codex','Claude','Both')][Parameter(Mandatory=$true)][string]$Client, [Parameter(Mandatory=$true)][string]$RepositoryRoot,
[string]$HomePath = $HOME, [ValidateSet('Codex','Claude','Both')][Parameter(Mandatory=$true)][string]$Client,
[object[]]$Entries, [string]$HomePath = $HOME,
[switch]$DryRun, [object[]]$Entries,
[switch]$Update, [switch]$DryRun,
[switch]$Summary [switch]$Update,
) [switch]$Summary
Invoke-ManagedExtensions @PSBoundParameters -Action sync )
} Invoke-ManagedExtensions @PSBoundParameters -Action sync
}
function Install-ConfiguredPlugins {
param( function Install-ConfiguredPlugins {
[Parameter(Mandatory=$true)][string]$RepositoryRoot, param(
[ValidateSet('Codex','Claude','Both')][Parameter(Mandatory=$true)][string]$Client, [Parameter(Mandatory=$true)][string]$RepositoryRoot,
[string]$HomePath = $HOME, [ValidateSet('Codex','Claude','Both')][Parameter(Mandatory=$true)][string]$Client,
[object[]]$Entries, [string]$HomePath = $HOME,
[switch]$DryRun, [object[]]$Entries,
[switch]$Update, [switch]$DryRun,
[switch]$Summary [switch]$Update,
) [switch]$Summary
if ($PSBoundParameters.ContainsKey('Entries') -and $Entries.Count -eq 0) { return } )
Invoke-ManagedExtensions @PSBoundParameters -Action install if ($PSBoundParameters.ContainsKey('Entries') -and $Entries.Count -eq 0) { return }
} Invoke-ManagedExtensions @PSBoundParameters -Action install
}
function Uninstall-DeselectedPlugins {
param( function Uninstall-DeselectedPlugins {
[Parameter(Mandatory=$true)][AllowEmptyCollection()][object[]]$Entries, param(
[ValidateSet('Codex','Claude','Both')][Parameter(Mandatory=$true)][string]$Client, [Parameter(Mandatory=$true)][AllowEmptyCollection()][object[]]$Entries,
[string]$RepositoryRoot = (Split-Path (Split-Path $PSScriptRoot -Parent) -Parent), [ValidateSet('Codex','Claude','Both')][Parameter(Mandatory=$true)][string]$Client,
[string]$HomePath = $HOME, [string]$RepositoryRoot = (Split-Path (Split-Path $PSScriptRoot -Parent) -Parent),
[switch]$DryRun, [string]$HomePath = $HOME,
[switch]$Summary [switch]$DryRun,
) [switch]$Summary
if ($Entries.Count -eq 0) { return } )
Invoke-ManagedExtensions -RepositoryRoot $RepositoryRoot -Client $Client -HomePath $HomePath -Entries $Entries -Action remove -DryRun:$DryRun -Summary:$Summary if ($Entries.Count -eq 0) { return }
} Invoke-ManagedExtensions -RepositoryRoot $RepositoryRoot -Client $Client -HomePath $HomePath -Entries $Entries -Action remove -DryRun:$DryRun -Summary:$Summary
}