Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
230 lines
11 KiB
PowerShell
230 lines
11 KiB
PowerShell
function Invoke-PluginCommand {
|
|
param(
|
|
[Parameter(Mandatory=$true)][string]$Command,
|
|
[Parameter(Mandatory=$true)][string[]]$Arguments,
|
|
[switch]$DryRun
|
|
)
|
|
$display = "$Command $($Arguments -join ' ')"
|
|
if ($DryRun) { Write-Output "DRYRUN $display"; return }
|
|
& $Command @Arguments
|
|
if ($LASTEXITCODE -ne 0) { throw "Plugin command failed: $display" }
|
|
}
|
|
|
|
function Get-ConfiguredPluginEntries {
|
|
param([Parameter(Mandatory=$true)][string]$RepositoryRoot)
|
|
$manifest = Join-Path $RepositoryRoot 'adapters/plugins.tsv'
|
|
if (-not (Test-Path -LiteralPath $manifest)) { throw "Plugin manifest is missing: $manifest" }
|
|
Import-Csv -LiteralPath $manifest -Delimiter ([char]9) | ForEach-Object {
|
|
$_.claude_plugin = $_.claude_plugin.Trim()
|
|
$_.codex_plugin = $_.codex_plugin.Trim()
|
|
$_
|
|
}
|
|
}
|
|
|
|
function Get-InstalledPlugins {
|
|
param([ValidateSet('Claude','Codex')][string]$Client)
|
|
if ($Client -eq 'Claude') {
|
|
if (-not (Get-Command claude -ErrorAction SilentlyContinue)) { throw 'Claude Code CLI is required to manage Claude plugins.' }
|
|
$items = (claude plugin list --json | Out-String) | ConvertFrom-Json
|
|
return @($items)
|
|
}
|
|
if (-not (Get-Command codex -ErrorAction SilentlyContinue)) { throw 'Codex CLI is required to manage Codex plugins.' }
|
|
$items = (codex plugin list --json | Out-String) | ConvertFrom-Json
|
|
return @($items.installed)
|
|
}
|
|
|
|
function Ensure-CodexMarketplace {
|
|
param(
|
|
[Parameter(Mandatory=$true)][string]$Source,
|
|
[Parameter(Mandatory=$true)][string]$MarketplaceName,
|
|
[switch]$DryRun
|
|
)
|
|
if ($DryRun) {
|
|
Write-Output "DRYRUN codex plugin marketplace add $Source"
|
|
return
|
|
}
|
|
$previousErrorActionPreference = $ErrorActionPreference
|
|
try {
|
|
$ErrorActionPreference = 'Continue'
|
|
$output = @(& codex plugin marketplace add $Source 2>&1)
|
|
$exitCode = $LASTEXITCODE
|
|
} catch {
|
|
$output = @($_)
|
|
$exitCode = 1
|
|
} finally {
|
|
$ErrorActionPreference = $previousErrorActionPreference
|
|
}
|
|
$output | ForEach-Object { Write-Output $_ }
|
|
if ($exitCode -eq 0) { return }
|
|
|
|
$text = $output -join [Environment]::NewLine
|
|
if ($text -match 'marketplace .* already added from a different source') {
|
|
Write-Output "WARN Codex marketplace '$MarketplaceName' exists from another source; replacing it with: $Source"
|
|
Invoke-PluginCommand -Command 'codex' -Arguments @('plugin','marketplace','remove',$MarketplaceName)
|
|
Invoke-PluginCommand -Command 'codex' -Arguments @('plugin','marketplace','add',$Source)
|
|
return
|
|
}
|
|
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]$Summary,
|
|
[object[]]$Entries
|
|
)
|
|
$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 = '' }
|
|
$plugin = if ($selectedClient -eq 'Claude') { $entry.claude_plugin } else { $entry.codex_plugin }
|
|
if ([string]::IsNullOrWhiteSpace($plugin)) { throw ('Plugin selector missing for {0}: {1}' -f $selectedClient, $entry.name) }
|
|
|
|
if ($selectedClient -eq 'Codex' -and $entry.codex_method -ne 'plugin') {
|
|
if ($entry.codex_method -eq 'skill') {
|
|
$arguments = @('-y','skills','add',$entry.codex_source,'--global','--agent','codex')
|
|
if ($entry.codex_skill -ne '-') { $arguments = @('-y','skills','add',$entry.codex_source,'--skill',$entry.codex_skill,'--global','--agent','codex') }
|
|
} elseif ($entry.codex_method -eq 'impeccable') {
|
|
$arguments = @('-y','impeccable','install','-y','--providers=codex','--scope=global')
|
|
} else {
|
|
throw "Unknown Codex install method '$($entry.codex_method)' for $($entry.name)."
|
|
}
|
|
Invoke-PluginCommand -Command 'npx' -Arguments $arguments -DryRun:$DryRun
|
|
if (-not $Summary) { Write-Output "PASS Codex skill ensured: $($entry.name)" }
|
|
$tally.Ensured++
|
|
continue
|
|
}
|
|
|
|
$installedItem = if ($selectedClient -eq 'Claude') { $installed | Where-Object { $_.id -eq $plugin } | Select-Object -First 1 } else { $installed | Where-Object { $_.pluginId -eq $plugin } | Select-Object -First 1 }
|
|
if ($Update -and $selectedClient -eq 'Claude' -and $null -ne $installedItem) {
|
|
Invoke-PluginCommand -Command 'claude' -Arguments @('plugin','update',$plugin) -DryRun:$DryRun
|
|
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
|
|
}
|
|
if (-not $Summary) { Write-Output "PASS $selectedClient plugin already installed: $plugin" }
|
|
$tally.AlreadyInstalled++
|
|
continue
|
|
}
|
|
|
|
if ($selectedClient -eq 'Claude') {
|
|
if (-not [string]::IsNullOrWhiteSpace($marketplace)) {
|
|
Invoke-PluginCommand -Command 'claude' -Arguments @('plugin','marketplace','add',$marketplace) -DryRun:$DryRun
|
|
}
|
|
Invoke-PluginCommand -Command 'claude' -Arguments @('plugin','install',$plugin,'--scope','user') -DryRun:$DryRun
|
|
} else {
|
|
if (-not [string]::IsNullOrWhiteSpace($marketplace) -and $marketplace -ne 'openai-curated-remote') {
|
|
$marketplaceName = ($plugin -split '@')[-1]
|
|
Ensure-CodexMarketplace -Source $marketplace -MarketplaceName $marketplaceName -DryRun:$DryRun
|
|
}
|
|
Invoke-PluginCommand -Command 'codex' -Arguments @('plugin','add',$plugin) -DryRun:$DryRun
|
|
}
|
|
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) }
|
|
}
|
|
}
|