feat: add keyboard plugin selection
CI / linux (push) Failing after 11s

This commit is contained in:
Julian lechner committed 2026-09-16 17:37:53 +02:00
1 parent 9b5e862d8a
commit c6c8bf7105
5 files changed
+62 -75

No files matched your search

+23 -47
View File
@@ -1,59 +1,35 @@
function Read-InstallPlatform { function Read-InstallPlatform {
param([string]$Platform) param([string]$Platform)
if ($Platform) { return $Platform } if ($Platform) { return $Platform }
Write-Host 'Select target platform:' return Read-InteractiveSingle -Title 'Select shell' -Names @('PowerShell','Bash') -Values @('Windows','Linux')
Write-Host '1) Windows'
Write-Host '2) Linux'
do { $selection = Read-Host 'Selection [1-2]' } while ($selection -notin @('1','2'))
return @{'1'='Windows'; '2'='Linux'}[$selection]
} }
function Read-InstallClient { function Read-InstallClient {
param([string]$Client) param([string]$Client)
if ($Client) { return $Client } if ($Client) { return $Client }
Write-Host 'Select installation target:' return Read-InteractiveSingle -Title 'Select client' -Names @('Codex','Claude','Both') -Values @('Codex','Claude','Both')
Write-Host '1) Codex' }
Write-Host '2) Claude' function Clear-InteractiveScreen { if (-not [Console]::IsInputRedirected -and $env:CI -ne 'true' -and $env:AI_CONFIG_NO_INTERACTIVE -ne '1') { Clear-Host } }
Write-Host '3) Both' function Read-InteractiveSingle {
do { $selection = Read-Host 'Selection [1-3]' } while ($selection -notin @('1','2','3')) param([string]$Title,[string[]]$Names,[string[]]$Values)
return @{'1'='Codex'; '2'='Claude'; '3'='Both'}[$selection] if ([Console]::IsInputRedirected -or $env:CI -eq 'true' -or $env:AI_CONFIG_NO_INTERACTIVE -eq '1') {
Write-Host $Title; for($i=0;$i -lt $Names.Count;$i++){ Write-Host ('{0}) {1}' -f ($i+1),$Names[$i]) }
do { $answer=Read-Host 'Selection' } while($answer -notmatch ('^[1-{0}]$' -f $Names.Count)); return $Values[[int]$answer-1]
}
$index=0
while($true) {
Clear-InteractiveScreen; Write-Host $Title
for($i=0;$i -lt $Names.Count;$i++){ $prefix=if($i -eq $index){'>'}else{' '}; if($i -eq $index -and $env:NO_COLOR -ne '1'){Write-Host ('{0} {1}' -f $prefix,$Names[$i]) -ForegroundColor Cyan}else{Write-Host ('{0} {1}' -f $prefix,$Names[$i])} }
Write-Host 'Up/Down Navigate Enter Confirm'; $key=[Console]::ReadKey($true)
if($key.Key -eq 'Up'){ $index=($index+$Names.Count-1)%$Names.Count } elseif($key.Key -eq 'Down'){ $index=($index+1)%$Names.Count } elseif($key.Key -eq 'Enter'){ Clear-InteractiveScreen; return $Values[$index] }
}
} }
function Get-InstallDestinations { function Get-InstallDestinations {
param( param([Parameter(Mandatory=$true)][string]$HomePath,[ValidateSet('Codex','Claude','Both')][Parameter(Mandatory=$true)][string]$Client)
[Parameter(Mandatory=$true)][string]$HomePath, $destinations=@(); if($Client -in @('Codex','Both')){$destinations+=Join-Path $HomePath '.codex'}; if($Client -in @('Claude','Both')){$destinations+=Join-Path $HomePath '.claude'}; return $destinations
[ValidateSet('Codex','Claude','Both')][Parameter(Mandatory=$true)][string]$Client
)
$destinations = @()
if ($Client -in @('Codex','Both')) { $destinations += Join-Path $HomePath '.codex' }
if ($Client -in @('Claude','Both')) { $destinations += Join-Path $HomePath '.claude' }
return $destinations
} }
function Get-InstallTargets { function Get-InstallTargets {
param( param([Parameter(Mandatory=$true)][string]$Generated,[Parameter(Mandatory=$true)][string]$HomePath,[Parameter(Mandatory=$true)][string]$Platform,[ValidateSet('Codex','Claude','Both')][Parameter(Mandatory=$true)][string]$Client)
[Parameter(Mandatory=$true)][string]$Generated, $targets=@(); if($Client -in @('Codex','Both')){$targets+=@{Source=(Join-Path $Generated "codex-$Platform");Destination=(Join-Path $HomePath '.codex')};$targets+=@{Source=(Join-Path $Generated "codex-$Platform/skills");Destination=(Join-Path $HomePath '.agents/skills')}}; if($Client -in @('Claude','Both')){$targets+=@{Source=(Join-Path $Generated "claude-$Platform");Destination=(Join-Path $HomePath '.claude')};}; return $targets
[Parameter(Mandatory=$true)][string]$HomePath,
[Parameter(Mandatory=$true)][string]$Platform,
[ValidateSet('Codex','Claude','Both')][Parameter(Mandatory=$true)][string]$Client
)
$targets = @()
if ($Client -in @('Codex','Both')) {
$targets += @{ Source=(Join-Path $Generated "codex-$Platform"); Destination=(Join-Path $HomePath '.codex') }
$targets += @{ Source=(Join-Path $Generated "codex-$Platform/skills"); Destination=(Join-Path $HomePath '.agents/skills') }
}
if ($Client -in @('Claude','Both')) {
$targets += @{ Source=(Join-Path $Generated "claude-$Platform"); Destination=(Join-Path $HomePath '.claude') }
}
return $targets
}
function Test-AnyManifestPresent {
param([Parameter(Mandatory=$true)][string[]]$Destinations)
return [bool](@($Destinations | Where-Object { Test-Path (Get-ManagedManifestPath $_) }).Count)
}
function Test-AllManifestsPresent {
param([Parameter(Mandatory=$true)][string[]]$Destinations)
return [bool](@($Destinations | Where-Object { -not (Test-Path (Get-ManagedManifestPath $_)) }).Count -eq 0)
} }
function Test-AnyManifestPresent { param([Parameter(Mandatory=$true)][string[]]$Destinations); return [bool](@($Destinations|Where-Object{Test-Path(Get-ManagedManifestPath $_)}).Count) }
function Test-AllManifestsPresent { param([Parameter(Mandatory=$true)][string[]]$Destinations); return [bool](@($Destinations|Where-Object{-not(Test-Path(Get-ManagedManifestPath $_))}).Count -eq 0) }
+10 -12
View File
@@ -9,12 +9,7 @@ read_install_platform() {
*) printf 'Unknown --platform value: %s (expected windows or linux)\n' "$platform" >&2; return 1 ;; *) printf 'Unknown --platform value: %s (expected windows or linux)\n' "$platform" >&2; return 1 ;;
esac esac
fi fi
printf 'Select target platform:\n1) Windows\n2) Linux\n' >&2 read_single_selection 'Select shell' 'PowerShell' 'windows' 'Bash' 'linux'
local selection
while :; do
read -r -p 'Selection [1-2] ' selection
case "$selection" in 1) printf 'windows\n'; return 0 ;; 2) printf 'linux\n'; return 0 ;; esac
done
} }
read_install_client() { read_install_client() {
@@ -27,12 +22,15 @@ read_install_client() {
*) printf 'Unknown --client value: %s (expected codex, claude, or both)\n' "$client" >&2; return 1 ;; *) printf 'Unknown --client value: %s (expected codex, claude, or both)\n' "$client" >&2; return 1 ;;
esac esac
fi fi
printf 'Select installation target:\n1) Codex\n2) Claude\n3) Both\n' >&2 read_single_selection 'Select client' 'Codex' 'codex' 'Claude' 'claude' 'Both' 'both'
local selection }
while :; do
read -r -p 'Selection [1-3] ' selection clear_interactive() { [ -t 0 ] && [ "${CI:-}" != true ] && [ "${AI_CONFIG_NO_INTERACTIVE:-}" != 1 ] && printf '\033[2J\033[H'; }
case "$selection" in 1) printf 'codex\n'; return 0 ;; 2) printf 'claude\n'; return 0 ;; 3) printf 'both\n'; return 0 ;; esac read_single_selection() {
done local title=$1; shift; local labels=() values=() label value index=0 key
while [ $# -gt 0 ]; do labels+=("$1"); values+=("$2"); shift 2; done
if [ ! -t 0 ] || [ "${CI:-}" = true ] || [ "${AI_CONFIG_NO_INTERACTIVE:-}" = 1 ]; then printf '%s\n' "$title" >&2; for ((index=0;index<${#labels[@]};index++)); do printf '%d) %s\n' "$((index+1))" "${labels[$index]}" >&2; done; while read -r -p 'Selection ' value; do [[ "$value" =~ ^[1-9][0-9]*$ ]] && [ "$value" -le "${#labels[@]}" ] && { printf '%s\n' "${values[$((value-1))]}"; return; }; done; return 1; fi
local selected=0; while :; do clear_interactive; printf '%s\n' "$title"; for ((index=0;index<${#labels[@]};index++)); do [ "$index" -eq "$selected" ] && printf '> %s\n' "${labels[$index]}" || printf ' %s\n' "${labels[$index]}"; done; printf '↑/↓ Navigate Enter Confirm\n'; IFS= read -rsn1 key || return 1; case "$key" in $'\x1b') read -rsn2 key; case "$key" in '[A') selected=$(( (selected+${#labels[@]}-1)%${#labels[@]} ));; '[B') selected=$(( (selected+1)%${#labels[@]} ));; esac;; '') clear_interactive; printf '%s\n' "${values[$selected]}"; return;; esac; done
} }
# get_install_destinations <home> <client> -- prints one destination per line # get_install_destinations <home> <client> -- prints one destination per line
+13 -14
View File
@@ -19,7 +19,7 @@ function Invoke-PluginCommand {
$output = @(& $resolvedCommand @Arguments 2>&1) $output = @(& $resolvedCommand @Arguments 2>&1)
$exitCode = $LASTEXITCODE $exitCode = $LASTEXITCODE
} finally { $ErrorActionPreference = $previousPreference } } finally { $ErrorActionPreference = $previousPreference }
$output | Where-Object { -not $Summary -or $exitCode -ne 0 -or $_ -is [System.Management.Automation.ErrorRecord] -or "$_" -match '(?i)warn|error|fail|deprecat' } if (-not $Summary -or $env:AI_CONFIG_VERBOSE -eq '1') { $output | Write-Output } elseif ($exitCode -eq 0) { $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" }
} }
@@ -68,7 +68,7 @@ function Ensure-CodexMarketplace {
} finally { } finally {
$ErrorActionPreference = $previousErrorActionPreference $ErrorActionPreference = $previousErrorActionPreference
} }
$output | Where-Object { -not $Summary -or $exitCode -ne 0 -or $_ -is [System.Management.Automation.ErrorRecord] -or "$_" -match '(?i)warn|error|fail|deprecat' } if (-not $Summary -or $env:AI_CONFIG_VERBOSE -eq '1') { $output | Write-Output } elseif ($exitCode -eq 0) { $output | Where-Object { "$_" -match '(?i)warn|error|fail|deprecat' } }
if ($exitCode -eq 0) { return } if ($exitCode -eq 0) { return }
$text = $output -join [Environment]::NewLine $text = $output -join [Environment]::NewLine
@@ -87,19 +87,14 @@ function Read-PluginToggleSelection {
[Parameter(Mandatory=$true)][bool[]]$Checked [Parameter(Mandatory=$true)][bool[]]$Checked
) )
$state = @($Checked) $state = @($Checked)
while ($true) { 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]} } }
Write-Host 'Select plugins (enter a number to toggle, "done" to confirm):' $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 '↑/↓ Navigate Space Toggle Enter Confirm'; $key=[Console]::ReadKey($true); if($key.Key -eq 'Up'){$index=($index+$Names.Count-1)%$Names.Count}elseif($key.Key -eq 'Down'){$index=($index+1)%$Names.Count}elseif($key.Key -eq 'Spacebar'){$state[$index]= -not $state[$index]}elseif($key.Key -eq 'Enter'){Clear-Host;return $state} }
for ($i = 0; $i -lt $Names.Count; $i++) {
$mark = if ($state[$i]) { 'x' } else { ' ' }
Write-Host (" {0}) [{1}] {2}" -f ($i + 1), $mark, $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]
}
} }
function Update-CodexMarketplace {
param([Parameter(Mandatory=$true)][string]$MarketplaceName,[Parameter(Mandatory=$true)][string]$Source,[switch]$DryRun,[switch]$Summary)
try { Invoke-PluginCommand -Command 'codex' -Arguments @('plugin','marketplace','upgrade',$MarketplaceName) -DryRun:$DryRun -Summary:$Summary }
catch { Ensure-CodexMarketplace -Source $Source -MarketplaceName $MarketplaceName -DryRun:$DryRun -Summary:$Summary }
} }
function Select-ConfiguredPlugins { function Select-ConfiguredPlugins {
@@ -231,6 +226,9 @@ function Install-ConfiguredPlugins {
} }
Invoke-PluginCommand -Command 'claude' -Arguments @('plugin','install',$plugin,'--scope','user') -DryRun:$DryRun -Summary:$Summary Invoke-PluginCommand -Command 'claude' -Arguments @('plugin','install',$plugin,'--scope','user') -DryRun:$DryRun -Summary:$Summary
} else { } else {
if ($Update -and -not [string]::IsNullOrWhiteSpace($marketplace) -and $marketplace -ne 'openai-curated-remote') {
Update-CodexMarketplace -MarketplaceName (($plugin -split '@')[-1]) -Source $marketplace -DryRun:$DryRun -Summary:$Summary
}
if (-not [string]::IsNullOrWhiteSpace($marketplace) -and $marketplace -ne 'openai-curated-remote') { if (-not [string]::IsNullOrWhiteSpace($marketplace) -and $marketplace -ne 'openai-curated-remote') {
$marketplaceName = ($plugin -split '@')[-1] $marketplaceName = ($plugin -split '@')[-1]
Ensure-CodexMarketplace -Source $marketplace -MarketplaceName $marketplaceName -DryRun:$DryRun -Summary:$Summary Ensure-CodexMarketplace -Source $marketplace -MarketplaceName $marketplaceName -DryRun:$DryRun -Summary:$Summary
@@ -243,3 +241,4 @@ function Install-ConfiguredPlugins {
if ($Summary) { Write-Output ("PLUGINS {0}: {1} ensured, {2} already installed, {3} updated" -f $selectedClient, $tally.Ensured, $tally.AlreadyInstalled, $tally.Updated) } if ($Summary) { Write-Output ("PLUGINS {0}: {1} ensured, {2} already installed, {3} updated" -f $selectedClient, $tally.Ensured, $tally.AlreadyInstalled, $tally.Updated) }
} }
} }
+14 -2
View File
@@ -22,7 +22,8 @@ run_plugin_command() {
printf '%s\n' "$output" | grep -Ei 'warn|error|fail|deprecat' || true printf '%s\n' "$output" | grep -Ei 'warn|error|fail|deprecat' || true
else else
status=$? status=$?
printf '%s\n' "$output" >&2 [ "${AI_CONFIG_VERBOSE:-}" = 1 ] && printf '%s\n' "$output" >&2
printf 'Plugin command failed: %s\n' "$*" >&2
return "$status" return "$status"
fi fi
else else
@@ -30,6 +31,13 @@ run_plugin_command() {
fi fi
} }
update_codex_marketplace() {
local dry_run=$1 marketplace_name=$2 source=$3
if ! run_plugin_command "$dry_run" codex plugin marketplace upgrade "$marketplace_name"; then
ensure_codex_marketplace "$dry_run" "$source" "$marketplace_name"
fi
}
ensure_codex_marketplace() { ensure_codex_marketplace() {
local dry_run=$1 source=$2 marketplace_name=$3 output local dry_run=$1 source=$2 marketplace_name=$3 output
if [ "$dry_run" = true ]; then if [ "$dry_run" = true ]; then
@@ -81,7 +89,10 @@ plugin_toggleable() {
read_plugin_toggle_selection() { read_plugin_toggle_selection() {
local -n names_ref=$1 local -n names_ref=$1
local -n checked_ref=$2 local -n checked_ref=$2
local answer i mark local answer i mark index=0 key
if [ -t 0 ] && [ "${CI:-}" != true ] && [ "${AI_CONFIG_NO_INTERACTIVE:-}" != 1 ]; then
while :; do clear_interactive; printf 'Select plugins\n'; for ((i=0;i<${#names_ref[@]};i++)); do mark=' '; [ "${checked_ref[$i]}" = true ] && mark=x; [ "$i" -eq "$index" ] && printf '> [%s] %s\n' "$mark" "${names_ref[$i]}" || printf ' [%s] %s\n' "$mark" "${names_ref[$i]}"; done; printf '↑/↓ Navigate Space Toggle Enter Confirm\n'; IFS= read -rsn1 key || return 1; case "$key" in $'\x1b') read -rsn2 key; case "$key" in '[A') index=$(( (index+${#names_ref[@]}-1)%${#names_ref[@]} ));; '[B') index=$(( (index+1)%${#names_ref[@]} ));; esac;; ' ') [ "${checked_ref[$index]}" = true ] && checked_ref[$index]=false || checked_ref[$index]=true;; '') clear_interactive; return 0;; esac; done
fi
while :; do while :; do
printf 'Select plugins (enter a number to toggle, "done" to confirm):\n' printf 'Select plugins (enter a number to toggle, "done" to confirm):\n'
for ((i = 0; i < ${#names_ref[@]}; i++)); do for ((i = 0; i < ${#names_ref[@]}; i++)); do
@@ -227,6 +238,7 @@ install_configured_plugins() {
run_plugin_command "$dry_run" claude plugin enable "$plugin" run_plugin_command "$dry_run" claude plugin enable "$plugin"
updated_count=$((updated_count + 1)) updated_count=$((updated_count + 1))
elif [ "$update" = true ] && [ "$client" = codex ]; then elif [ "$update" = true ] && [ "$client" = codex ]; then
[ -z "$marketplace" ] || [ "$marketplace" = openai-curated-remote ] || update_codex_marketplace "$dry_run" "${plugin##*@}" "$marketplace"
run_plugin_command "$dry_run" codex plugin add "$plugin" run_plugin_command "$dry_run" codex plugin add "$plugin"
updated_count=$((updated_count + 1)) updated_count=$((updated_count + 1))
else else
+2
View File
@@ -30,8 +30,10 @@ $detailed = @(Invoke-PluginCommand -Command Test-PluginOutput -Arguments @('succ
if ($detailed.Count -ne 2 -or $detailed[0] -ne 'download progress') { throw 'Detailed output must retain progress.' } if ($detailed.Count -ne 2 -or $detailed[0] -ne 'download progress') { throw 'Detailed output must retain progress.' }
$diagnostics = [System.Collections.Generic.List[string]]::new() $diagnostics = [System.Collections.Generic.List[string]]::new()
$failed = $false $failed = $false
$env:AI_CONFIG_VERBOSE = '1'
try { Invoke-PluginCommand -Command Test-PluginOutput -Arguments @('fail') -Summary | ForEach-Object { $diagnostics.Add("$_") } } try { Invoke-PluginCommand -Command Test-PluginOutput -Arguments @('fail') -Summary | ForEach-Object { $diagnostics.Add("$_") } }
catch { $failed = $true } catch { $failed = $true }
$env:AI_CONFIG_VERBOSE = $null
if (-not $failed -or $diagnostics -notcontains 'download progress' -or $diagnostics -notcontains 'WARN test warning') { throw 'Plugin failure must retain full diagnostics and fail.' } if (-not $failed -or $diagnostics -notcontains 'download progress' -or $diagnostics -notcontains 'WARN test warning') { throw 'Plugin failure must retain full diagnostics and fail.' }
$global:LASTEXITCODE = 0 $global:LASTEXITCODE = 0
function Get-InstalledPlugins { [pscustomobject]@{ pluginId = 'test@market' } } function Get-InstalledPlugins { [pscustomobject]@{ pluginId = 'test@market' } }