diff --git a/scripts/lib/install-targets.ps1 b/scripts/lib/install-targets.ps1 index 07f3288..7e2171c 100644 --- a/scripts/lib/install-targets.ps1 +++ b/scripts/lib/install-targets.ps1 @@ -1,59 +1,35 @@ function Read-InstallPlatform { param([string]$Platform) if ($Platform) { return $Platform } - Write-Host 'Select target platform:' - 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] + return Read-InteractiveSingle -Title 'Select shell' -Names @('PowerShell','Bash') -Values @('Windows','Linux') } - function Read-InstallClient { param([string]$Client) if ($Client) { return $Client } - Write-Host 'Select installation target:' - Write-Host '1) Codex' - Write-Host '2) Claude' - Write-Host '3) Both' - do { $selection = Read-Host 'Selection [1-3]' } while ($selection -notin @('1','2','3')) - return @{'1'='Codex'; '2'='Claude'; '3'='Both'}[$selection] + return Read-InteractiveSingle -Title 'Select client' -Names @('Codex','Claude','Both') -Values @('Codex','Claude','Both') +} +function Clear-InteractiveScreen { if (-not [Console]::IsInputRedirected -and $env:CI -ne 'true' -and $env:AI_CONFIG_NO_INTERACTIVE -ne '1') { Clear-Host } } +function Read-InteractiveSingle { + param([string]$Title,[string[]]$Names,[string[]]$Values) + 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 { - param( - [Parameter(Mandatory=$true)][string]$HomePath, - [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 + param([Parameter(Mandatory=$true)][string]$HomePath,[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 { - 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 - ) - $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) + 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) + $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) } diff --git a/scripts/lib/install-targets.sh b/scripts/lib/install-targets.sh index 6f307c0..2101dee 100644 --- a/scripts/lib/install-targets.sh +++ b/scripts/lib/install-targets.sh @@ -9,12 +9,7 @@ read_install_platform() { *) printf 'Unknown --platform value: %s (expected windows or linux)\n' "$platform" >&2; return 1 ;; esac fi - printf 'Select target platform:\n1) Windows\n2) Linux\n' >&2 - 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_single_selection 'Select shell' 'PowerShell' 'windows' 'Bash' 'linux' } 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 ;; esac fi - printf 'Select installation target:\n1) Codex\n2) Claude\n3) Both\n' >&2 - local selection - while :; do - read -r -p 'Selection [1-3] ' selection - case "$selection" in 1) printf 'codex\n'; return 0 ;; 2) printf 'claude\n'; return 0 ;; 3) printf 'both\n'; return 0 ;; esac - done + read_single_selection 'Select client' 'Codex' 'codex' 'Claude' 'claude' 'Both' 'both' +} + +clear_interactive() { [ -t 0 ] && [ "${CI:-}" != true ] && [ "${AI_CONFIG_NO_INTERACTIVE:-}" != 1 ] && printf '\033[2J\033[H'; } +read_single_selection() { + 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 -- prints one destination per line diff --git a/scripts/lib/plugins.ps1 b/scripts/lib/plugins.ps1 index c761809..6d7b463 100644 --- a/scripts/lib/plugins.ps1 +++ b/scripts/lib/plugins.ps1 @@ -19,7 +19,7 @@ function Invoke-PluginCommand { $output = @(& $resolvedCommand @Arguments 2>&1) $exitCode = $LASTEXITCODE } 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" } } @@ -68,7 +68,7 @@ function Ensure-CodexMarketplace { } finally { $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 } $text = $output -join [Environment]::NewLine @@ -87,19 +87,14 @@ function Read-PluginToggleSelection { [Parameter(Mandatory=$true)][bool[]]$Checked ) $state = @($Checked) - while ($true) { - Write-Host 'Select plugins (enter a number to toggle, "done" to confirm):' - 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] - } - } + 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 '↑/↓ 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} } +} + +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 { @@ -231,6 +226,9 @@ function Install-ConfiguredPlugins { } Invoke-PluginCommand -Command 'claude' -Arguments @('plugin','install',$plugin,'--scope','user') -DryRun:$DryRun -Summary:$Summary } 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') { $marketplaceName = ($plugin -split '@')[-1] 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) } } } + diff --git a/scripts/lib/plugins.sh b/scripts/lib/plugins.sh index 9b9d694..30a1828 100755 --- a/scripts/lib/plugins.sh +++ b/scripts/lib/plugins.sh @@ -22,7 +22,8 @@ run_plugin_command() { printf '%s\n' "$output" | grep -Ei 'warn|error|fail|deprecat' || true else status=$? - printf '%s\n' "$output" >&2 + [ "${AI_CONFIG_VERBOSE:-}" = 1 ] && printf '%s\n' "$output" >&2 + printf 'Plugin command failed: %s\n' "$*" >&2 return "$status" fi else @@ -30,6 +31,13 @@ run_plugin_command() { 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() { local dry_run=$1 source=$2 marketplace_name=$3 output if [ "$dry_run" = true ]; then @@ -81,7 +89,10 @@ plugin_toggleable() { read_plugin_toggle_selection() { local -n names_ref=$1 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 printf 'Select plugins (enter a number to toggle, "done" to confirm):\n' for ((i = 0; i < ${#names_ref[@]}; i++)); do @@ -227,6 +238,7 @@ install_configured_plugins() { run_plugin_command "$dry_run" claude plugin enable "$plugin" updated_count=$((updated_count + 1)) 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" updated_count=$((updated_count + 1)) else diff --git a/scripts/test-plugin-selection.ps1 b/scripts/test-plugin-selection.ps1 index af7f33d..eefae39 100644 --- a/scripts/test-plugin-selection.ps1 +++ b/scripts/test-plugin-selection.ps1 @@ -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.' } $diagnostics = [System.Collections.Generic.List[string]]::new() $failed = $false +$env:AI_CONFIG_VERBOSE = '1' try { Invoke-PluginCommand -Command Test-PluginOutput -Arguments @('fail') -Summary | ForEach-Object { $diagnostics.Add("$_") } } 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.' } $global:LASTEXITCODE = 0 function Get-InstalledPlugins { [pscustomobject]@{ pluginId = 'test@market' } }