diff --git a/scripts/lib/plugins.ps1 b/scripts/lib/plugins.ps1 index b34f2b7..da7f31d 100644 --- a/scripts/lib/plugins.ps1 +++ b/scripts/lib/plugins.ps1 @@ -67,18 +67,109 @@ function Ensure-CodexMarketplace { throw "Plugin command failed: codex plugin marketplace add $Source" } +function Read-PluginToggleSelection { + param( + [Parameter(Mandatory=$true)][string[]]$Names, + [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 ($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 Select-ConfiguredPlugins { + <# + .SYNOPSIS + Lets the user toggle each configured plugin on or off. Install mode starts + with everything checked; update mode starts checked to match what the + 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, + [ValidateSet('Codex','Claude','Both')][Parameter(Mandatory=$true)][string]$Client, + [ValidateSet('Install','Update')][Parameter(Mandatory=$true)][string]$Mode, + [switch]$DryRun + ) + $entries = @(Get-ConfiguredPluginEntries -RepositoryRoot $RepositoryRoot) + $toggleable = @($entries | Where-Object { $Client -in @('Claude','Both') -or $_.codex_method -eq 'plugin' }) + $fixed = @($entries | Where-Object { $toggleable -notcontains $_ }) + if ($DryRun -or $toggleable.Count -eq 0) { return @{ Selected = $entries; Deselected = @() } } + + $referenceClient = if ($Client -eq 'Codex') { 'Codex' } else { 'Claude' } + $installed = @(Get-InstalledPlugins -Client $referenceClient) + $idField = if ($referenceClient -eq 'Claude') { 'id' } else { 'pluginId' } + $checked = @($toggleable | ForEach-Object { + if ($Mode -eq 'Install') { $true; return } + $selector = if ($referenceClient -eq 'Claude') { $_.claude_plugin } else { $_.codex_plugin } + [bool](@($installed | Where-Object { $_.$idField -eq $selector }).Count) + }) + $names = @($toggleable | ForEach-Object { $_.name }) + $checked = Read-PluginToggleSelection -Names $names -Checked $checked + + $selected = @() + $deselected = @() + for ($i = 0; $i -lt $toggleable.Count; $i++) { + if ($checked[$i]) { $selected += $toggleable[$i] } else { $deselected += $toggleable[$i] } + } + return @{ Selected = ($selected + $fixed); Deselected = $deselected } +} + +function Uninstall-DeselectedPlugins { + param( + [Parameter(Mandatory=$true)][object[]]$Entries, + [ValidateSet('Codex','Claude','Both')][Parameter(Mandatory=$true)][string]$Client, + [switch]$DryRun, + [switch]$Summary + ) + $clients = if ($Client -eq 'Both') { @('Claude','Codex') } else { @($Client) } + foreach ($selectedClient in $clients) { + $installed = if ($DryRun) { @() } else { @(Get-InstalledPlugins -Client $selectedClient) } + $idField = if ($selectedClient -eq 'Claude') { 'id' } else { 'pluginId' } + $removedCount = 0 + foreach ($entry in $Entries) { + $plugin = if ($selectedClient -eq 'Claude') { $entry.claude_plugin } else { $entry.codex_plugin } + if ([string]::IsNullOrWhiteSpace($plugin)) { continue } + $isInstalled = [bool](@($installed | Where-Object { $_.$idField -eq $plugin }).Count) + if (-not $isInstalled) { continue } + if ($selectedClient -eq 'Claude') { + Invoke-PluginCommand -Command 'claude' -Arguments @('plugin','uninstall',$plugin,'--scope','user') -DryRun:$DryRun + } else { + Invoke-PluginCommand -Command 'codex' -Arguments @('plugin','remove',$plugin) -DryRun:$DryRun + } + if (-not $Summary) { Write-Output "PASS $selectedClient plugin removed: $plugin" } + $removedCount++ + } + if ($Summary -and $removedCount -gt 0) { Write-Output "PLUGINS ${selectedClient}: $removedCount removed" } + } +} + function Install-ConfiguredPlugins { param( [Parameter(Mandatory=$true)][string]$RepositoryRoot, [ValidateSet('Codex','Claude','Both')][Parameter(Mandatory=$true)][string]$Client, [switch]$DryRun, - [switch]$Update + [switch]$Update, + [switch]$Summary, + [object[]]$Entries ) - $entries = @(Get-ConfiguredPluginEntries -RepositoryRoot $RepositoryRoot) + $entries = if ($Entries) { $Entries } else { @(Get-ConfiguredPluginEntries -RepositoryRoot $RepositoryRoot) } $clients = if ($Client -eq 'Both') { @('Claude','Codex') } else { @($Client) } foreach ($selectedClient in $clients) { $installed = if ($DryRun) { @() } else { @(Get-InstalledPlugins -Client $selectedClient) } + $tally = @{ Ensured = 0; AlreadyInstalled = 0; Updated = 0 } foreach ($entry in $entries) { $marketplace = if ($selectedClient -eq 'Claude') { $entry.claude_marketplace.Trim() } else { $entry.codex_marketplace.Trim() } if ($marketplace -eq '-') { $marketplace = '' } @@ -95,7 +186,8 @@ function Install-ConfiguredPlugins { throw "Unknown Codex install method '$($entry.codex_method)' for $($entry.name)." } Invoke-PluginCommand -Command 'npx' -Arguments $arguments -DryRun:$DryRun - Write-Output "PASS Codex skill ensured: $($entry.name)" + if (-not $Summary) { Write-Output "PASS Codex skill ensured: $($entry.name)" } + $tally.Ensured++ continue } @@ -105,13 +197,15 @@ function Install-ConfiguredPlugins { if (-not $installedItem.enabled) { Invoke-PluginCommand -Command 'claude' -Arguments @('plugin','enable',$plugin) -DryRun:$DryRun } + $tally.Updated++ continue } if (-not $Update -and $null -ne $installedItem) { if ($selectedClient -eq 'Claude' -and -not $installedItem.enabled) { Invoke-PluginCommand -Command 'claude' -Arguments @('plugin','enable',$plugin) -DryRun:$DryRun } - Write-Output "PASS $selectedClient plugin already installed: $plugin" + if (-not $Summary) { Write-Output "PASS $selectedClient plugin already installed: $plugin" } + $tally.AlreadyInstalled++ continue } @@ -127,7 +221,9 @@ function Install-ConfiguredPlugins { } Invoke-PluginCommand -Command 'codex' -Arguments @('plugin','add',$plugin) -DryRun:$DryRun } - Write-Output "PASS $selectedClient plugin ensured: $plugin" + if (-not $Summary) { Write-Output "PASS $selectedClient plugin ensured: $plugin" } + $tally.Ensured++ } + 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 b398d0d..19dbf63 100755 --- a/scripts/lib/plugins.sh +++ b/scripts/lib/plugins.sh @@ -40,11 +40,140 @@ ensure_codex_marketplace() { return 1 } +get_plugin_names() { + local root=$1 name rest + while IFS=$'\t' read -r name rest; do + [ "$name" = name ] && continue + [ -n "$name" ] || continue + printf '%s\n' "$name" + done < "$root/adapters/plugins.tsv" +} + +# plugin_field -- field is one of the plugins.tsv column names +plugin_field() { + local root=$1 target=$2 field=$3 + local name claude_marketplace claude_plugin codex_marketplace codex_plugin codex_method codex_source codex_skill + while IFS=$'\t' read -r name claude_marketplace claude_plugin codex_marketplace codex_plugin codex_method codex_source codex_skill; do + [ "$name" = "$target" ] || continue + printf '%s\n' "${!field}" + return 0 + done < "$root/adapters/plugins.tsv" + return 1 +} + +plugin_toggleable() { + local root=$1 name=$2 client=$3 + case "$client" in claude|both) return 0;; esac + [ "$(plugin_field "$root" "$name" codex_method)" = plugin ] +} + +# read_plugin_toggle_selection +read_plugin_toggle_selection() { + local -n names_ref=$1 + local -n checked_ref=$2 + local answer i mark + while :; do + printf 'Select plugins (enter a number to toggle, "done" to confirm):\n' + for ((i = 0; i < ${#names_ref[@]}; i++)); do + mark=' ' + [ "${checked_ref[$i]}" = true ] && mark=x + printf ' %d) [%s] %s\n' "$((i + 1))" "$mark" "${names_ref[$i]}" + done + read -r -p 'Toggle number or "done" ' answer + [ "$answer" = done ] && return 0 + if [[ "$answer" =~ ^[0-9]+$ ]] && [ "$answer" -ge 1 ] && [ "$answer" -le "${#names_ref[@]}" ]; then + i=$((answer - 1)) + if [ "${checked_ref[$i]}" = true ]; then checked_ref[$i]=false; else checked_ref[$i]=true; fi + fi + done +} + +# select_configured_plugins +select_configured_plugins() { + local root=$1 client=$2 mode=$3 dry_run=$4 + local -n selected_ref=$5 + local -n deselected_ref=$6 + selected_ref=() + deselected_ref=() + + local name all_names=() toggle_names=() fixed_names=() + while IFS= read -r name; do + all_names+=("$name") + if plugin_toggleable "$root" "$name" "$client"; then toggle_names+=("$name"); else fixed_names+=("$name"); fi + done < <(get_plugin_names "$root") + + if [ "$dry_run" = true ] || [ ${#toggle_names[@]} -eq 0 ]; then + selected_ref=("${all_names[@]}") + return 0 + fi + + local reference_client=claude + [ "$client" = codex ] && reference_client=codex + + local checked=() selector + for name in "${toggle_names[@]}"; do + if [ "$mode" = Install ]; then + checked+=(true) + continue + fi + if [ "$reference_client" = claude ]; then selector=$(plugin_field "$root" "$name" claude_plugin); else selector=$(plugin_field "$root" "$name" codex_plugin); fi + if plugin_installed "$reference_client" "$selector"; then checked+=(true); else checked+=(false); fi + done + + read_plugin_toggle_selection toggle_names checked + + local i + for ((i = 0; i < ${#toggle_names[@]}; i++)); do + if [ "${checked[$i]}" = true ]; then selected_ref+=("${toggle_names[$i]}"); else deselected_ref+=("${toggle_names[$i]}"); fi + done + selected_ref+=("${fixed_names[@]}") +} + +# uninstall_deselected_plugins [summary] +uninstall_deselected_plugins() { + local root=$1 client_selection=$2 dry_run=$3 + local -n names_ref=$4 + local summary=${5:-false} + local clients=() + case "$client_selection" in + codex) clients=(codex);; + claude) clients=(claude);; + both) clients=(claude codex);; + esac + local client name plugin removed + for client in "${clients[@]}"; do + removed=0 + for name in "${names_ref[@]}"; do + if [ "$client" = claude ]; then plugin=$(plugin_field "$root" "$name" claude_plugin); else plugin=$(plugin_field "$root" "$name" codex_plugin); fi + [ -n "$plugin" ] || continue + if [ "$dry_run" = false ] && plugin_installed "$client" "$plugin"; then + if [ "$client" = claude ]; then + run_plugin_command "$dry_run" claude plugin uninstall "$plugin" --scope user + else + run_plugin_command "$dry_run" codex plugin remove "$plugin" + fi + [ "$summary" = true ] || printf 'PASS %s plugin removed: %s\n' "$client" "$plugin" + removed=$((removed + 1)) + fi + done + if [ "$summary" = true ] && [ "$removed" -gt 0 ]; then printf 'PLUGINS %s: %s removed\n' "$client" "$removed"; fi + done +} + +# install_configured_plugins [selected-names-array-name] [summary] install_configured_plugins() { - local root=$1 client_selection=$2 dry_run=$3 update=$4 + local root=$1 client_selection=$2 dry_run=$3 update=$4 selected_name=${5:-} summary=${6:-false} local manifest="$root/adapters/plugins.tsv" [ -f "$manifest" ] || { printf 'Plugin manifest is missing: %s\n' "$manifest" >&2; return 1; } + local -A selected_set=() + if [ -n "$selected_name" ]; then + local -n selected_names_ref=$selected_name + local sn + for sn in "${selected_names_ref[@]}"; do selected_set[$sn]=1; done + fi + local ensured=0 already_installed=0 updated_count=0 + local clients=() case "$client_selection" in codex) clients=(codex);; @@ -54,12 +183,14 @@ install_configured_plugins() { esac local client name claude_marketplace claude_plugin codex_marketplace codex_plugin codex_method codex_source codex_skill marketplace plugin - while IFS=$'\t' read -r name claude_marketplace claude_plugin codex_marketplace codex_plugin codex_method codex_source codex_skill; do - [ "$name" = name ] && continue - [ -n "$name" ] || continue - [ "$claude_marketplace" = - ] && claude_marketplace= - [ "$codex_marketplace" = - ] && codex_marketplace= - for client in "${clients[@]}"; do + for client in "${clients[@]}"; do + ensured=0 already_installed=0 updated_count=0 + while IFS=$'\t' read -r name claude_marketplace claude_plugin codex_marketplace codex_plugin codex_method codex_source codex_skill; do + [ "$name" = name ] && continue + [ -n "$name" ] || continue + [ -n "$selected_name" ] && [ -z "${selected_set[$name]+x}" ] && continue + [ "$claude_marketplace" = - ] && claude_marketplace= + [ "$codex_marketplace" = - ] && codex_marketplace= if [ "$client" = claude ]; then marketplace=$claude_marketplace; plugin=$claude_plugin; else marketplace=$codex_marketplace; plugin=$codex_plugin; fi [ -n "$plugin" ] || { printf 'Plugin selector missing for %s: %s\n' "$client" "$name" >&2; return 1; } if [ "$client" = codex ] && [ "$codex_method" != plugin ]; then @@ -76,17 +207,21 @@ install_configured_plugins() { ;; *) printf 'Unknown Codex install method for %s: %s\n' "$client" "$name" >&2; return 1;; esac - printf 'PASS Codex skill ensured: %s\n' "$name" + [ "$summary" = true ] || printf 'PASS Codex skill ensured: %s\n' "$name" + ensured=$((ensured + 1)) continue fi if [ "$dry_run" = false ] && plugin_installed "$client" "$plugin"; then if [ "$update" = true ] && [ "$client" = claude ]; then run_plugin_command "$dry_run" claude plugin update "$plugin" run_plugin_command "$dry_run" claude plugin enable "$plugin" + updated_count=$((updated_count + 1)) elif [ "$update" = true ] && [ "$client" = codex ]; then run_plugin_command "$dry_run" codex plugin add "$plugin" + updated_count=$((updated_count + 1)) else - printf 'PASS %s plugin already installed: %s\n' "$client" "$plugin" + [ "$summary" = true ] || printf 'PASS %s plugin already installed: %s\n' "$client" "$plugin" + already_installed=$((already_installed + 1)) fi continue fi @@ -100,7 +235,11 @@ install_configured_plugins() { fi run_plugin_command "$dry_run" codex plugin add "$plugin" fi - printf 'PASS %s plugin ensured: %s\n' "$client" "$plugin" - done - done < "$manifest" + [ "$summary" = true ] || printf 'PASS %s plugin ensured: %s\n' "$client" "$plugin" + ensured=$((ensured + 1)) + done < "$manifest" + if [ "$summary" = true ]; then + printf 'PLUGINS %s: %s ensured, %s already installed, %s updated\n' "$client" "$ensured" "$already_installed" "$updated_count" + fi + done }