Compare commits

...
2 Commits
Author SHA1 Message Date
fraujulian 4f2688cba2 Merge remote-tracking branch 'origin/main'
CI / Capabilities and shell quality (push) Successful in 12s
CI / Python tests and compilation (push) Successful in 15s
CI / PowerShell tests on Ubuntu (push) Successful in 40s
CI / CI configuration security checks (push) Successful in 4s
CI / Build and inspect generated packages (push) Successful in 9s
CI / Qwen retrieval integration (push) Successful in 14m23s
CI / Bash integration and installation guards (push) Successful in 43s
CI / Installed client configuration smoke tests (push) Successful in 14s
# Conflicts:
#	scripts/lib/plugins.ps1
2026-09-25 11:11:15 +02:00
fraujulian 7558addb79 fix(ci): tolerate missing client CLIs 2026-09-25 11:05:14 +02:00
+186 -186
View File
@@ -1,188 +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 SilentlyContinue $command = Get-Command $Client.ToLowerInvariant() -ErrorAction SilentlyContinue
if (-not $command) { return @() } if (-not $command) { return @() }
$environment = @{ $environment = @{
HOME = $HomePath HOME = $HomePath
USERPROFILE = $HomePath USERPROFILE = $HomePath
CODEX_HOME = (Join-Path $HomePath '.codex') CODEX_HOME = (Join-Path $HomePath '.codex')
CLAUDE_CONFIG_DIR = (Join-Path $HomePath '.claude') CLAUDE_CONFIG_DIR = (Join-Path $HomePath '.claude')
} }
$previousEnvironment = @{} $previousEnvironment = @{}
foreach ($name in $environment.Keys) { $previousEnvironment[$name] = [Environment]::GetEnvironmentVariable($name, 'Process') } foreach ($name in $environment.Keys) { $previousEnvironment[$name] = [Environment]::GetEnvironmentVariable($name, 'Process') }
try { try {
foreach ($name in $environment.Keys) { [Environment]::SetEnvironmentVariable($name, $environment[$name], 'Process') } foreach ($name in $environment.Keys) { [Environment]::SetEnvironmentVariable($name, $environment[$name], 'Process') }
$global:LASTEXITCODE = 0 $global:LASTEXITCODE = 0
$output = $null | & $command plugin list --json | Out-String $output = $null | & $command plugin list --json | Out-String
if ($LASTEXITCODE -ne 0) { throw "$Client plugin listing failed." } if ($LASTEXITCODE -ne 0) { throw "$Client plugin listing failed." }
$items = $output | ConvertFrom-Json $items = $output | ConvertFrom-Json
if ($Client -eq 'Claude') { return @($items) } if ($Client -eq 'Claude') { return @($items) }
return @($items.installed) return @($items.installed)
} finally { } finally {
foreach ($name in $previousEnvironment.Keys) { foreach ($name in $previousEnvironment.Keys) {
if ($null -eq $previousEnvironment[$name]) { Remove-Item -LiteralPath ("Env:" + $name) -ErrorAction SilentlyContinue } if ($null -eq $previousEnvironment[$name]) { Remove-Item -LiteralPath ("Env:" + $name) -ErrorAction SilentlyContinue }
else { [Environment]::SetEnvironmentVariable($name, $previousEnvironment[$name], 'Process') } else { [Environment]::SetEnvironmentVariable($name, $previousEnvironment[$name], 'Process') }
} }
} }
} }
function Read-PluginToggleSelection { function Read-PluginToggleSelection {
param( param(
[Parameter(Mandatory=$true)][string[]]$Names, [Parameter(Mandatory=$true)][string[]]$Names,
[Parameter(Mandatory=$true)][bool[]]$Checked [Parameter(Mandatory=$true)][bool[]]$Checked
) )
$state = @($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]} } } 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} } $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 .SYNOPSIS
Lets the user toggle each configured plugin on or off. Install mode starts Lets the user toggle each configured plugin on or off. Install mode starts
with everything checked; update mode starts checked to match what the with everything checked; update mode starts checked to match what the
reference client (Claude, or Codex when only Codex is targeted) already reference client (Claude, or Codex when only Codex is targeted) already
has installed, so unchecking a currently installed plugin uninstalls it. has installed, so unchecking a currently installed plugin uninstalls it.
#> #>
param( param(
[Parameter(Mandatory=$true)][string]$RepositoryRoot, [Parameter(Mandatory=$true)][string]$RepositoryRoot,
[ValidateSet('Codex','Claude','Both')][Parameter(Mandatory=$true)][string]$Client, [ValidateSet('Codex','Claude','Both')][Parameter(Mandatory=$true)][string]$Client,
[ValidateSet('Install','Update')][Parameter(Mandatory=$true)][string]$Mode, [ValidateSet('Install','Update')][Parameter(Mandatory=$true)][string]$Mode,
[string]$HomePath = [Environment]::GetFolderPath('UserProfile'), [string]$HomePath = [Environment]::GetFolderPath('UserProfile'),
[switch]$DryRun [switch]$DryRun
) )
$entries = @(Get-ConfiguredPluginEntries -RepositoryRoot $RepositoryRoot) $entries = @(Get-ConfiguredPluginEntries -RepositoryRoot $RepositoryRoot)
$toggleable = @($entries) $toggleable = @($entries)
$fixed = @($entries | Where-Object { $toggleable -notcontains $_ }) $fixed = @($entries | Where-Object { $toggleable -notcontains $_ })
if ($toggleable.Count -eq 0 -or ($DryRun -and $Mode -eq 'Install')) { return @{ Selected = $entries; Deselected = @() } } if ($toggleable.Count -eq 0 -or ($DryRun -and $Mode -eq 'Install')) { return @{ Selected = $entries; Deselected = @() } }
$referenceClients = if ($Client -eq 'Both') { @('Claude','Codex') } else { @($Client) } $referenceClients = if ($Client -eq 'Both') { @('Claude','Codex') } else { @($Client) }
$installed = @{} $installed = @{}
if ($Mode -eq 'Update') { foreach ($referenceClient in $referenceClients) { $installed[$referenceClient] = @(Get-InstalledPlugins -Client $referenceClient -HomePath $HomePath) } } if ($Mode -eq 'Update') { foreach ($referenceClient in $referenceClients) { $installed[$referenceClient] = @(Get-InstalledPlugins -Client $referenceClient -HomePath $HomePath) } }
$checked = @($toggleable | ForEach-Object { $checked = @($toggleable | ForEach-Object {
if ($Mode -eq 'Install') { $true; return } if ($Mode -eq 'Install') { $true; return }
if ($_.codex_method -eq 'cli') { return $null -ne (Get-Command $_.codex_source -ErrorAction SilentlyContinue) } if ($_.codex_method -eq 'cli') { return $null -ne (Get-Command $_.codex_source -ErrorAction SilentlyContinue) }
$entry = $_ $entry = $_
$found = $false $found = $false
foreach ($referenceClient in $referenceClients) { foreach ($referenceClient in $referenceClients) {
$selector = if ($referenceClient -eq 'Claude') { $entry.claude_plugin } else { $entry.codex_plugin } $selector = if ($referenceClient -eq 'Claude') { $entry.claude_plugin } else { $entry.codex_plugin }
if ($referenceClient -eq 'Codex' -and $entry.codex_method -eq 'qmd') { if ($referenceClient -eq 'Codex' -and $entry.codex_method -eq 'qmd') {
$found = $found -or ($null -ne (Get-Command qmd -ErrorAction SilentlyContinue)) $found = $found -or ($null -ne (Get-Command qmd -ErrorAction SilentlyContinue))
} elseif ($referenceClient -eq 'Codex' -and $entry.codex_method -ne 'plugin') { } elseif ($referenceClient -eq 'Codex' -and $entry.codex_method -ne 'plugin') {
$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))) $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)))
} else { } else {
$idField = if ($referenceClient -eq 'Claude') { 'id' } else { 'pluginId' } $idField = if ($referenceClient -eq 'Claude') { 'id' } else { 'pluginId' }
$found = $found -or [bool](@($installed[$referenceClient] | Where-Object { $_.$idField -eq $selector }).Count) $found = $found -or [bool](@($installed[$referenceClient] | Where-Object { $_.$idField -eq $selector }).Count)
} }
} }
$found $found
}) })
$names = @($toggleable | ForEach-Object { $_.name }) $names = @($toggleable | ForEach-Object { $_.name })
if (-not $DryRun) { $checked = Read-PluginToggleSelection -Names $names -Checked $checked } if (-not $DryRun) { $checked = Read-PluginToggleSelection -Names $names -Checked $checked }
$selected = @() $selected = @()
$deselected = @() $deselected = @()
for ($i = 0; $i -lt $toggleable.Count; $i++) { for ($i = 0; $i -lt $toggleable.Count; $i++) {
if ($checked[$i]) { $selected += $toggleable[$i] } else { $deselected += $toggleable[$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 { function Invoke-ManagedExtensions {
param( param(
[Parameter(Mandatory=$true)][string]$RepositoryRoot, [Parameter(Mandatory=$true)][string]$RepositoryRoot,
[Parameter(Mandatory=$true)][string]$Client, [Parameter(Mandatory=$true)][string]$Client,
[string]$HomePath = $HOME, [string]$HomePath = $HOME,
[ValidateSet('sync','install','remove')][string]$Action = 'sync', [ValidateSet('sync','install','remove')][string]$Action = 'sync',
[object[]]$Entries, [object[]]$Entries,
[switch]$DryRun, [switch]$DryRun,
[switch]$Update, [switch]$Update,
[switch]$Summary [switch]$Summary
) )
$arguments = @((Join-Path $PSScriptRoot 'managed-extensions.py'), $Action, '--home', $HomePath, '--manifest', (Join-Path $RepositoryRoot 'adapters/plugins.tsv'), '--client', $Client.ToLowerInvariant()) $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' } if ($DryRun) { $arguments += '--dry-run' }
if ($Update) { $arguments += '--update' } if ($Update) { $arguments += '--update' }
if ($Summary) { $arguments += '--summary' } if ($Summary) { $arguments += '--summary' }
if ($PSBoundParameters.ContainsKey('Entries')) { $arguments += '--selected'; $arguments += @($Entries | ForEach-Object { $_.name }) } if ($PSBoundParameters.ContainsKey('Entries')) { $arguments += '--selected'; $arguments += @($Entries | ForEach-Object { $_.name }) }
& python @arguments & python @arguments
if ($LASTEXITCODE -ne 0) { throw 'Managed extension reconciliation failed.' } if ($LASTEXITCODE -ne 0) { throw 'Managed extension reconciliation failed.' }
} }
function Sync-ConfiguredPlugins { function Sync-ConfiguredPlugins {
param( param(
[Parameter(Mandatory=$true)][string]$RepositoryRoot, [Parameter(Mandatory=$true)][string]$RepositoryRoot,
[ValidateSet('Codex','Claude','Both')][Parameter(Mandatory=$true)][string]$Client, [ValidateSet('Codex','Claude','Both')][Parameter(Mandatory=$true)][string]$Client,
[string]$HomePath = $HOME, [string]$HomePath = $HOME,
[object[]]$Entries, [object[]]$Entries,
[switch]$DryRun, [switch]$DryRun,
[switch]$Update, [switch]$Update,
[switch]$Summary [switch]$Summary
) )
Invoke-ManagedExtensions @PSBoundParameters -Action sync Invoke-ManagedExtensions @PSBoundParameters -Action sync
} }
function Install-ConfiguredPlugins { function Install-ConfiguredPlugins {
param( param(
[Parameter(Mandatory=$true)][string]$RepositoryRoot, [Parameter(Mandatory=$true)][string]$RepositoryRoot,
[ValidateSet('Codex','Claude','Both')][Parameter(Mandatory=$true)][string]$Client, [ValidateSet('Codex','Claude','Both')][Parameter(Mandatory=$true)][string]$Client,
[string]$HomePath = $HOME, [string]$HomePath = $HOME,
[object[]]$Entries, [object[]]$Entries,
[switch]$DryRun, [switch]$DryRun,
[switch]$Update, [switch]$Update,
[switch]$Summary [switch]$Summary
) )
if ($PSBoundParameters.ContainsKey('Entries') -and $Entries.Count -eq 0) { return } if ($PSBoundParameters.ContainsKey('Entries') -and $Entries.Count -eq 0) { return }
Invoke-ManagedExtensions @PSBoundParameters -Action install Invoke-ManagedExtensions @PSBoundParameters -Action install
} }
function Uninstall-DeselectedPlugins { function Uninstall-DeselectedPlugins {
param( param(
[Parameter(Mandatory=$true)][AllowEmptyCollection()][object[]]$Entries, [Parameter(Mandatory=$true)][AllowEmptyCollection()][object[]]$Entries,
[ValidateSet('Codex','Claude','Both')][Parameter(Mandatory=$true)][string]$Client, [ValidateSet('Codex','Claude','Both')][Parameter(Mandatory=$true)][string]$Client,
[string]$RepositoryRoot = (Split-Path (Split-Path $PSScriptRoot -Parent) -Parent), [string]$RepositoryRoot = (Split-Path (Split-Path $PSScriptRoot -Parent) -Parent),
[string]$HomePath = $HOME, [string]$HomePath = $HOME,
[switch]$DryRun, [switch]$DryRun,
[switch]$Summary [switch]$Summary
) )
if ($Entries.Count -eq 0) { return } if ($Entries.Count -eq 0) { return }
Invoke-ManagedExtensions -RepositoryRoot $RepositoryRoot -Client $Client -HomePath $HomePath -Entries $Entries -Action remove -DryRun:$DryRun -Summary:$Summary Invoke-ManagedExtensions -RepositoryRoot $RepositoryRoot -Client $Client -HomePath $HomePath -Entries $Entries -Action remove -DryRun:$DryRun -Summary:$Summary
} }