From 2fe209635123fb4617c21d95a95aea3405a023c3 Mon Sep 17 00:00:00 2001 From: amos Date: Tue, 20 Oct 2026 15:38:55 +0300 Subject: [PATCH 1/4] fix(install): upgrade marketplace plugins that are already installed (FIRE-2097) Re-running the installer on a machine with the plugin installed reported success but left Claude and Codex on the old version, so auto-update never moved them. Two causes: - `claude plugin install` exits 0 as a no-op when the plugin is already installed, so the `plugin update` fallback never ran. - `claude/codex plugin marketplace add` exits 0 as a no-op when the marketplace already exists, so the marketplace refresh (which only ran when `add` failed) was skipped and Codex reinstalled from a stale snapshot. Both installers now always refresh the marketplace after adding it, and try `plugin update` before `plugin install` for Claude and Copilot. The shared steps live in add_and_refresh_marketplace / update_or_install_plugin (Add-AndRefreshMarketplace / Update-OrInstallPlugin in install.ps1). Regression tests run the installer against fake CLIs that model the no-op install and the stale marketplace snapshot; they fail on the previous installer. Co-Authored-By: Claude Opus 5.5 (1M context) --- .github/workflows/validate.yml | 4 + install.ps1 | 64 ++++++--------- install.sh | 78 ++++++------------ tests/test_install_plugin_upgrade_ps1.ps1 | 93 ++++++++++++++++++++++ tests/test_install_plugin_upgrade_sh.sh | 96 +++++++++++++++++++++++ 5 files changed, 239 insertions(+), 96 deletions(-) create mode 100644 tests/test_install_plugin_upgrade_ps1.ps1 create mode 100755 tests/test_install_plugin_upgrade_sh.sh diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 34c6ae7..69dcfa4 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -126,6 +126,7 @@ jobs: TEST_SH=dash bash tests/test_managed_hooks.sh TEST_SH=bash bash tests/test_managed_hooks.sh bash tests/test_install_env_sh.sh + bash tests/test_install_plugin_upgrade_sh.sh - name: Kiro installer (temp HOME, fake kiro-cli) # install.sh --kiro is the only installer that WRITES the vendor's hook # wiring itself (a hook file, Crew wrappers, a merge into every agent @@ -371,6 +372,7 @@ jobs: pwsh -NoProfile -File tests/test_env_first_found.ps1 pwsh -NoProfile -File tests/test_install_kiro_ps1.ps1 pwsh -NoProfile -File tests/test_install_env_ps1.ps1 + pwsh -NoProfile -File tests/test_install_plugin_upgrade_ps1.ps1 pwsh -NoProfile -File tests/test_status_kiro_ps1.ps1 windows: @@ -438,6 +440,8 @@ jobs: if ($LASTEXITCODE -ne 0) { exit 1 } powershell -NoProfile -File tests/test_install_env_ps1.ps1 if ($LASTEXITCODE -ne 0) { exit 1 } + powershell -NoProfile -File tests/test_install_plugin_upgrade_ps1.ps1 + if ($LASTEXITCODE -ne 0) { exit 1 } powershell -NoProfile -File tests/test_status_kiro_ps1.ps1 if ($LASTEXITCODE -ne 0) { exit 1 } powershell -NoProfile -File tests/test_muse_ps1.ps1 diff --git a/install.ps1 b/install.ps1 index c1c2595..e754de8 100644 --- a/install.ps1 +++ b/install.ps1 @@ -569,6 +569,23 @@ function Configure-Credentials { Write-UserEnvFile } +function Add-AndRefreshMarketplace { + param([string]$Cli, [string]$RefreshCommand, [string]$Marketplace) + Log "Adding marketplace $PluginRepo" + try { & $Cli plugin marketplace add $PluginRepo 2>&1 | Out-Null } catch {} + $refreshed = $false + try { & $Cli plugin marketplace $RefreshCommand $Marketplace 2>&1 | Out-Null; $refreshed = $LASTEXITCODE -eq 0 } catch {} + if ($refreshed) { Ok 'Marketplace up to date' } + else { Warn2 'Could not add or update marketplace (continuing - it may already be present).' } +} + +function Update-OrInstallPlugin { + param([string]$Cli, [string]$PluginSlug) + try { & $Cli plugin update $PluginSlug 2>&1 | Out-Null; if ($LASTEXITCODE -eq 0) { Ok 'Plugin updated'; return } } catch {} + try { & $Cli plugin install $PluginSlug 2>&1 | Out-Null; if ($LASTEXITCODE -eq 0) { Ok 'Plugin installed'; return } } catch {} + Die "$Cli plugin install failed. Run '$Cli plugin install $PluginSlug' to see the error." +} + # Test seam: load only the functions above (tests/test_install_kiro_ps1.ps1, # tests/test_install_env_ps1.ps1). @@ -647,39 +664,16 @@ Configure-Credentials if ($hasClaude) { Write-Host "" Write-Host "Rogue Security - Claude Code" -ForegroundColor Cyan - Log "Adding marketplace $PluginRepo" - $mktOk = $false - try { & claude plugin marketplace add $PluginRepo 2>&1 | Out-Null; if ($LASTEXITCODE -eq 0) { $mktOk = $true } } catch {} - if ($mktOk) { Ok 'Marketplace added' } - else { - try { & claude plugin marketplace update $MarketplaceName 2>&1 | Out-Null; if ($LASTEXITCODE -eq 0) { $mktOk = $true } } catch {} - if ($mktOk) { Ok 'Marketplace updated' } - else { Warn2 'Could not add or update marketplace (continuing - it may already be present).' } - } + Add-AndRefreshMarketplace -Cli 'claude' -RefreshCommand 'update' -Marketplace $MarketplaceName Log "Installing plugin $PluginName@$MarketplaceName" - $installed = $false - try { & claude plugin install "$PluginName@$MarketplaceName" 2>&1 | Out-Null; if ($LASTEXITCODE -eq 0) { $installed = $true } } catch {} - if (-not $installed) { - try { & claude plugin update $PluginName 2>&1 | Out-Null; if ($LASTEXITCODE -eq 0) { $installed = $true } } catch {} - } - if (-not $installed) { Die "claude plugin install failed. Run 'claude plugin install $PluginName@$MarketplaceName' to see the error." } - Ok 'Plugin installed' + Update-OrInstallPlugin -Cli 'claude' -PluginSlug "$PluginName@$MarketplaceName" } if ($hasCodex) { Write-Host "" Write-Host "Rogue Security - OpenAI Codex" -ForegroundColor Cyan - Log "Adding marketplace $PluginRepo" - $mktOk = $false - try { & codex plugin marketplace add $PluginRepo 2>&1 | Out-Null; if ($LASTEXITCODE -eq 0) { $mktOk = $true } } catch {} - if ($mktOk) { Ok 'Marketplace added' } - else { - try { & codex plugin marketplace upgrade $MarketplaceName 2>&1 | Out-Null; if ($LASTEXITCODE -eq 0) { $mktOk = $true } } catch {} - if ($mktOk) { Ok 'Marketplace updated' } - else { Warn2 'Could not add or update Codex marketplace (continuing - it may already be present).' } - } + Add-AndRefreshMarketplace -Cli 'codex' -RefreshCommand 'upgrade' -Marketplace $MarketplaceName Log "Installing plugin $PluginName@$MarketplaceName" - # Codex uses `plugin add` (not `install`); idempotent re-add is fine. $installed = $false try { & codex plugin add "$PluginName@$MarketplaceName" 2>&1 | Out-Null; if ($LASTEXITCODE -eq 0) { $installed = $true } } catch {} if (-not $installed) { Die "codex plugin add failed. Run 'codex plugin add $PluginName@$MarketplaceName' to see the error." } @@ -932,23 +926,9 @@ if ($hasMuse) { if ($hasCopilot) { Write-Host "" Write-Host "Rogue Security - GitHub Copilot CLI" -ForegroundColor Cyan - Log "Adding marketplace $PluginRepo" - $mktOk = $false - try { & copilot plugin marketplace add $PluginRepo 2>&1 | Out-Null; if ($LASTEXITCODE -eq 0) { $mktOk = $true } } catch {} - if ($mktOk) { Ok 'Marketplace added' } - else { - try { & copilot plugin marketplace update $CopilotMarketplaceName 2>&1 | Out-Null; if ($LASTEXITCODE -eq 0) { $mktOk = $true } } catch {} - if ($mktOk) { Ok 'Marketplace updated' } - else { Warn2 'Could not add or update Copilot marketplace (continuing - it may already be present).' } - } + Add-AndRefreshMarketplace -Cli 'copilot' -RefreshCommand 'update' -Marketplace $CopilotMarketplaceName Log "Installing plugin $PluginName@$CopilotMarketplaceName" - $installed = $false - try { & copilot plugin install "$PluginName@$CopilotMarketplaceName" 2>&1 | Out-Null; if ($LASTEXITCODE -eq 0) { $installed = $true } } catch {} - if (-not $installed) { - try { & copilot plugin update $PluginName 2>&1 | Out-Null; if ($LASTEXITCODE -eq 0) { $installed = $true } } catch {} - } - if (-not $installed) { Die "copilot plugin install failed. Run 'copilot plugin install $PluginName@$CopilotMarketplaceName' to see the error." } - Ok 'Plugin installed' + Update-OrInstallPlugin -Cli 'copilot' -PluginSlug "$PluginName@$CopilotMarketplaceName" Warn2 'Copilot skips untrusted hooks - open /hooks in Copilot CLI and trust the Rogue entries once.' # Mirrors install.sh: Rogue reaches JetBrains only through Copilot's # CLI/Agent provider. The IDE's built-in "Local" agent runs its own hook diff --git a/install.sh b/install.sh index 08aa67d..2cd2ae5 100755 --- a/install.sh +++ b/install.sh @@ -148,55 +148,43 @@ have_cmd() { command -v "$1" >/dev/null 2>&1; } # Kiro has no plugin CLI either: the bridge is copied under ~/.rogue/plugins/kiro and # the installer writes the hook files that point Kiro at it (see kiro_install_plugin). -# ── Marketplace + plugin install (Claude) ───────────────────────────────────── -claude_install_plugin() { +add_and_refresh_marketplace() { + local cli="$1" refresh_command="$2" marketplace="$3" add_err note "Adding marketplace ${C_DIM}$ROGUE_PLUGIN_REPO${C_RESET}" - # Capture stderr so a real failure (e.g. missing git, clone error) is surfaced - # instead of swallowed — otherwise it resurfaces later as a misleading - # "plugin not found" from the install step. - local add_err - if add_err="$(claude plugin marketplace add "$ROGUE_PLUGIN_REPO" 2>&1)"; then - ok "Marketplace added" + add_err="$("$cli" plugin marketplace add "$ROGUE_PLUGIN_REPO" 2>&1)" && add_err="" + if "$cli" plugin marketplace "$refresh_command" "$marketplace" >/dev/null 2>&1; then + ok "Marketplace up to date" else - # Already present (or transient) — refresh from source instead. - if claude plugin marketplace update "$MARKETPLACE_NAME" >/dev/null 2>&1; then - ok "Marketplace updated" - else - warn "Could not add or update marketplace (continuing — it may already be present)" - [ -n "$add_err" ] && note "${C_DIM}${add_err}${C_RESET}" - fi + warn "Could not add or update marketplace (continuing — it may already be present)" + [ -n "$add_err" ] && note "${C_DIM}${add_err}${C_RESET}" fi +} - note "Installing plugin ${C_DIM}${PLUGIN_NAME}@${MARKETPLACE_NAME}${C_RESET}" - if claude plugin install "${PLUGIN_NAME}@${MARKETPLACE_NAME}" >/dev/null 2>&1; then - ok "Plugin installed" - elif claude plugin update "$PLUGIN_NAME" >/dev/null 2>&1; then +update_or_install_plugin() { + local cli="$1" plugin_slug="$2" + if "$cli" plugin update "$plugin_slug" >/dev/null 2>&1; then ok "Plugin updated" + elif "$cli" plugin install "$plugin_slug" >/dev/null 2>&1; then + ok "Plugin installed" else - die "claude plugin install failed. Run 'claude plugin install ${PLUGIN_NAME}@${MARKETPLACE_NAME}' to see the error." + die "$cli plugin install failed. Run '$cli plugin install $plugin_slug' to see the error." fi } +# ── Marketplace + plugin install (Claude) ───────────────────────────────────── +claude_install_plugin() { + add_and_refresh_marketplace claude update "$MARKETPLACE_NAME" + note "Installing plugin ${C_DIM}${PLUGIN_NAME}@${MARKETPLACE_NAME}${C_RESET}" + update_or_install_plugin claude "${PLUGIN_NAME}@${MARKETPLACE_NAME}" +} + # ── Marketplace + plugin install (Codex) ────────────────────────────────────── # Same monorepo: Codex reads .agents/plugins/marketplace.json, Claude reads # .claude-plugin/marketplace.json — both name the marketplace `rogue-marketplace` # and the plugin `rogue`, so the slug and `${PLUGIN_NAME}@${MARKETPLACE_NAME}` match. codex_install_plugin() { - note "Adding marketplace ${C_DIM}$ROGUE_PLUGIN_REPO${C_RESET}" - local add_err - if add_err="$(codex plugin marketplace add "$ROGUE_PLUGIN_REPO" 2>&1)"; then - ok "Marketplace added" - else - if codex plugin marketplace upgrade "$MARKETPLACE_NAME" >/dev/null 2>&1; then - ok "Marketplace updated" - else - warn "Could not add or update Codex marketplace (continuing — it may already be present)" - [ -n "$add_err" ] && note "${C_DIM}${add_err}${C_RESET}" - fi - fi - + add_and_refresh_marketplace codex upgrade "$MARKETPLACE_NAME" note "Installing plugin ${C_DIM}${PLUGIN_NAME}@${MARKETPLACE_NAME}${C_RESET}" - # Codex uses `plugin add` (not `install`); idempotent re-add is fine. if codex plugin add "${PLUGIN_NAME}@${MARKETPLACE_NAME}" >/dev/null 2>&1; then ok "Plugin installed" else @@ -213,27 +201,9 @@ codex_install_plugin() { # rogue@rogue-copilot to disambiguate. COPILOT_MARKETPLACE_NAME="rogue-copilot" copilot_install_plugin() { - note "Adding marketplace ${C_DIM}$ROGUE_PLUGIN_REPO${C_RESET}" - local add_err - if add_err="$(copilot plugin marketplace add "$ROGUE_PLUGIN_REPO" 2>&1)"; then - ok "Marketplace added" - else - if copilot plugin marketplace update "$COPILOT_MARKETPLACE_NAME" >/dev/null 2>&1; then - ok "Marketplace updated" - else - warn "Could not add or update Copilot marketplace (continuing — it may already be present)" - [ -n "$add_err" ] && note "${C_DIM}${add_err}${C_RESET}" - fi - fi - + add_and_refresh_marketplace copilot update "$COPILOT_MARKETPLACE_NAME" note "Installing plugin ${C_DIM}${PLUGIN_NAME}@${COPILOT_MARKETPLACE_NAME}${C_RESET}" - if copilot plugin install "${PLUGIN_NAME}@${COPILOT_MARKETPLACE_NAME}" >/dev/null 2>&1; then - ok "Plugin installed" - elif copilot plugin update "$PLUGIN_NAME" >/dev/null 2>&1; then - ok "Plugin updated" - else - die "copilot plugin install failed. Run 'copilot plugin install ${PLUGIN_NAME}@${COPILOT_MARKETPLACE_NAME}' to see the error." - fi + update_or_install_plugin copilot "${PLUGIN_NAME}@${COPILOT_MARKETPLACE_NAME}" } # ── File-copy install (Cursor) ──────────────────────────────────────────────── diff --git a/tests/test_install_plugin_upgrade_ps1.ps1 b/tests/test_install_plugin_upgrade_ps1.ps1 new file mode 100644 index 0000000..2a7e896 --- /dev/null +++ b/tests/test_install_plugin_upgrade_ps1.ps1 @@ -0,0 +1,93 @@ +#!/usr/bin/env pwsh +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$installer = [System.IO.Path]::Combine($here, '..', 'install.ps1') + +$work = Join-Path ([System.IO.Path]::GetTempPath()) ("rogue-plugin-upgrade-" + [System.IO.Path]::GetRandomFileName()) +$bin = Join-Path $work 'bin' +New-Item -ItemType Directory -Path $bin -Force | Out-Null +$prevPath = $env:PATH +$prevProfile = $env:USERPROFILE +$env:USERPROFILE = $work +$env:FAKE_CLI_STATE = $work + +$fakeCli = Join-Path $bin 'fake-plugin-cli-fixture.ps1' +@' +param([Parameter(ValueFromRemainingArguments = $true)][string[]]$Argv) +$snapshot = Join-Path $env:FAKE_CLI_STATE 'marketplace-snapshot' +$installed = Join-Path $env:FAKE_CLI_STATE 'installed-version' +Add-Content -LiteralPath (Join-Path $env:FAKE_CLI_STATE 'cli.log') -Value ($Argv -join ' ') +$command = ($Argv | Select-Object -First 3) -join ' ' +if ($command -eq 'plugin marketplace add') { + if (-not (Test-Path -LiteralPath $snapshot)) { [System.IO.File]::WriteAllText($snapshot, $env:PUBLISHED_VERSION) } + exit 0 +} +if ($command -eq 'plugin marketplace update') { + [System.IO.File]::WriteAllText($snapshot, $env:PUBLISHED_VERSION) + exit 0 +} +$sub = "$($Argv[0]) $($Argv[1])" +if ($sub -eq 'plugin install') { + if (Test-Path -LiteralPath $installed) { exit 0 } + Copy-Item -LiteralPath $snapshot -Destination $installed +} +if ($sub -eq 'plugin update') { + if (-not (Test-Path -LiteralPath $installed)) { exit 1 } + Copy-Item -LiteralPath $snapshot -Destination $installed -Force +} +exit 0 +'@ | Set-Content -LiteralPath $fakeCli -Encoding UTF8 +if ($env:OS -eq 'Windows_NT') { + [System.IO.File]::WriteAllText((Join-Path $bin 'fake-plugin-cli.cmd'), + "@echo off`r`npowershell.exe -NoProfile -File `"$fakeCli`" %*`r`nexit /b %errorlevel%`r`n") +} else { + $shim = Join-Path $bin 'fake-plugin-cli' + [System.IO.File]::WriteAllText($shim, "#!/bin/sh`nexec pwsh -NoProfile -File `"$fakeCli`" `"`$@`"`n") + & chmod +x $shim +} +$env:PATH = $bin + [System.IO.Path]::PathSeparator + $env:PATH + +$env:ROGUE_PLUGIN_REPO = 'rogue-test/rogue-plugins' +$env:ROGUE_INSTALL_LIB_ONLY = '1' +. $installer +$env:ROGUE_INSTALL_LIB_ONLY = $null + +$fails = 0 +$count = 0 +function Assert-Eq { + param($Got, $Expected, [string]$Label) + $script:count++ + if ([string]$Got -ceq [string]$Expected) { Write-Host " ok: $Label" } + else { Write-Host "FAIL [$Label]: got <$Got>, expected <$Expected>"; $script:fails++ } +} +$installedFile = Join-Path $work 'installed-version' +$logFile = Join-Path $work 'cli.log' +function Get-InstalledVersion { [System.IO.File]::ReadAllText($installedFile) } +function Get-CliCalls { @(Get-Content -LiteralPath $logFile) -join ' | ' } + +function Install-FromMarketplace { + Remove-Item -LiteralPath $logFile -ErrorAction SilentlyContinue + Add-AndRefreshMarketplace -Cli 'fake-plugin-cli' -RefreshCommand 'update' -Marketplace 'rogue-marketplace' + Update-OrInstallPlugin -Cli 'fake-plugin-cli' -PluginSlug 'rogue@rogue-marketplace' +} + +$env:PUBLISHED_VERSION = '1.0.0' +Install-FromMarketplace +Assert-Eq (Get-InstalledVersion) '1.0.0' 'fresh: the published version is installed' +Assert-Eq (Get-CliCalls) 'plugin marketplace add rogue-test/rogue-plugins | plugin marketplace update rogue-marketplace | plugin update rogue@rogue-marketplace | plugin install rogue@rogue-marketplace' 'fresh: marketplace added and refreshed, update tried, then install' + +$env:PUBLISHED_VERSION = '1.0.1' +Install-FromMarketplace +Assert-Eq (Get-InstalledVersion) '1.0.1' 'upgrade: the installed version moves to the newly published version' +Assert-Eq (Get-CliCalls) 'plugin marketplace add rogue-test/rogue-plugins | plugin marketplace update rogue-marketplace | plugin update rogue@rogue-marketplace' 'upgrade: marketplace refreshed, update alone runs' + +$env:PATH = $prevPath +$env:USERPROFILE = $prevProfile +$env:FAKE_CLI_STATE = $null +$env:PUBLISHED_VERSION = $null +$env:ROGUE_PLUGIN_REPO = $null +Remove-Item -Recurse -Force $work -ErrorAction SilentlyContinue + +Write-Host "" +if ($fails -eq 0) { Write-Host "test_install_plugin_upgrade_ps1: all $count passed"; exit 0 } +Write-Host "test_install_plugin_upgrade_ps1: $fails of $count failed" +exit 1 diff --git a/tests/test_install_plugin_upgrade_sh.sh b/tests/test_install_plugin_upgrade_sh.sh new file mode 100755 index 0000000..539c613 --- /dev/null +++ b/tests/test_install_plugin_upgrade_sh.sh @@ -0,0 +1,96 @@ +#!/usr/bin/env bash +set -euo pipefail + +REPO="$(cd "$(dirname "$0")/.." && pwd)" +WORK="$(mktemp -d)" +trap 'rm -rf "$WORK"' EXIT +fails=0 + +ok() { echo " ok: $1"; } +bad() { echo "FAIL [$1]: $2"; fails=$((fails + 1)); } +check() { + if [ "$2" = "$3" ]; then ok "$1"; else bad "$1" "expected [$2], got [$3]"; fi +} + +BIN="$WORK/bin"; mkdir -p "$BIN" +cat > "$BIN/curl" <<'STUB' +#!/usr/bin/env bash +printf '{}\n200' +STUB +cat > "$BIN/fake-plugin-cli" <<'STUB' +#!/usr/bin/env bash +cli="$(basename "$0")" +snapshot="$HOME/$cli-marketplace-snapshot" +installed="$HOME/$cli-installed-version" +echo "$*" >> "$HOME/$cli.log" +[ -z "${FAKE_CLI_BROKEN:-}" ] || exit 1 +case "$*" in + "plugin marketplace add "*) + if [ -f "$snapshot" ]; then + [ "$cli" = copilot ] && exit 1 + exit 0 + fi + printf '%s' "$PUBLISHED_VERSION" > "$snapshot" ;; + "plugin marketplace update "*|"plugin marketplace upgrade "*) + [ -f "$snapshot" ] || exit 1 + printf '%s' "$PUBLISHED_VERSION" > "$snapshot" ;; + "plugin install "*) + [ -f "$installed" ] && exit 0 + cp "$snapshot" "$installed" ;; + "plugin update "*) + [ -f "$installed" ] || exit 1 + cp "$snapshot" "$installed" ;; + "plugin add "*) + cp "$snapshot" "$installed" ;; +esac +exit 0 +STUB +chmod +x "$BIN/curl" "$BIN/fake-plugin-cli" +for cli in claude codex copilot; do ln -s fake-plugin-cli "$BIN/$cli"; done + +OUT="$WORK/out"; ERR="$WORK/err" +run_install() { + local home="$1"; shift + mkdir -p "$home" + set +e + ( cd "$home" && env "$@" PATH="$BIN:$PATH" HOME="$home" NO_COLOR=1 ROGUE_NON_INTERACTIVE=1 \ + ROGUE_API_KEY=test-key bash "$REPO/install.sh" --claude --codex --copilot ) > "$OUT" 2> "$ERR" + LAST_RC=$? + set -e +} +installed_version() { cat "$1/$2-installed-version" 2>/dev/null || echo none; } + +H="$WORK/fresh" +run_install "$H" PUBLISHED_VERSION=1.0.0 +check "fresh: install exits 0" "0" "$LAST_RC" +[ "$LAST_RC" = 0 ] || cat "$ERR" +for cli in claude codex copilot; do + check "fresh: $cli gets the published version" "1.0.0" "$(installed_version "$H" "$cli")" +done +check "fresh: claude installs the qualified plugin" "1" "$(grep -cx 'plugin install rogue@rogue-marketplace' "$H/claude.log")" +check "fresh: copilot installs the qualified plugin" "1" "$(grep -cx 'plugin install rogue@rogue-copilot' "$H/copilot.log")" + +rm -f "$H"/*.log +run_install "$H" PUBLISHED_VERSION=1.0.1 +check "upgrade: install exits 0" "0" "$LAST_RC" +[ "$LAST_RC" = 0 ] || cat "$ERR" +for cli in claude codex copilot; do + check "upgrade: $cli moves to the newly published version" "1.0.1" "$(installed_version "$H" "$cli")" +done +check "upgrade: claude refreshes its marketplace" "1" "$(grep -cx 'plugin marketplace update rogue-marketplace' "$H/claude.log")" +check "upgrade: codex refreshes its marketplace" "1" "$(grep -cx 'plugin marketplace upgrade rogue-marketplace' "$H/codex.log")" +check "upgrade: copilot refreshes its marketplace" "1" "$(grep -cx 'plugin marketplace update rogue-copilot' "$H/copilot.log")" +check "upgrade: claude and copilot report the update" "2" "$(grep -c 'Plugin updated' "$ERR" || :)" + +run_install "$H" PUBLISHED_VERSION=1.0.2 +for cli in claude codex copilot; do + check "second upgrade: $cli moves again" "1.0.2" "$(installed_version "$H" "$cli")" +done + +H="$WORK/broken" +run_install "$H" PUBLISHED_VERSION=1.0.0 FAKE_CLI_BROKEN=1 +[ "$LAST_RC" != 0 ] && ok "broken: install fails" || bad "broken: install fails" "exited 0" +check "broken: the error names the command to retry" "1" \ + "$(grep -c "Run 'claude plugin install rogue@rogue-marketplace' to see the error" "$ERR" || :)" + +[ "$fails" -eq 0 ] && echo "all plugin upgrade installer tests passed" || { echo "$fails failure(s)"; exit 1; } From 6a6a62dbd949c88df5ffb5b11a3a334768cc8447 Mon Sep 17 00:00:00 2001 From: amos Date: Tue, 20 Oct 2026 15:53:30 +0300 Subject: [PATCH 2/4] fix(install): fail loudly when updating an installed plugin fails (FIRE-2097) Falling back to `plugin install` after a failed `plugin update` reported success for an installed plugin, because install is a successful no-op there. Check `plugin list` instead: update an installed plugin and fail if that fails, install a missing one. Co-Authored-By: Claude Opus 5.5 (1M context) --- install.ps1 | 10 +++++++--- install.sh | 10 ++++++---- tests/test_install_plugin_upgrade_ps1.ps1 | 10 ++++++++-- tests/test_install_plugin_upgrade_sh.sh | 14 +++++++++++++- 4 files changed, 34 insertions(+), 10 deletions(-) diff --git a/install.ps1 b/install.ps1 index e754de8..11554ed 100644 --- a/install.ps1 +++ b/install.ps1 @@ -581,9 +581,13 @@ function Add-AndRefreshMarketplace { function Update-OrInstallPlugin { param([string]$Cli, [string]$PluginSlug) - try { & $Cli plugin update $PluginSlug 2>&1 | Out-Null; if ($LASTEXITCODE -eq 0) { Ok 'Plugin updated'; return } } catch {} - try { & $Cli plugin install $PluginSlug 2>&1 | Out-Null; if ($LASTEXITCODE -eq 0) { Ok 'Plugin installed'; return } } catch {} - Die "$Cli plugin install failed. Run '$Cli plugin install $PluginSlug' to see the error." + $installedPlugins = '' + try { $installedPlugins = (& $Cli plugin list 2>$null | Out-String) } catch {} + $command = if ($installedPlugins.Contains($PluginSlug)) { 'update' } else { 'install' } + $succeeded = $false + try { & $Cli plugin $command $PluginSlug 2>&1 | Out-Null; $succeeded = $LASTEXITCODE -eq 0 } catch {} + if (-not $succeeded) { Die "$Cli plugin $command failed. Run '$Cli plugin $command $PluginSlug' to see the error." } + if ($command -eq 'update') { Ok 'Plugin updated' } else { Ok 'Plugin installed' } } diff --git a/install.sh b/install.sh index 2cd2ae5..cf2b4e9 100755 --- a/install.sh +++ b/install.sh @@ -162,12 +162,14 @@ add_and_refresh_marketplace() { update_or_install_plugin() { local cli="$1" plugin_slug="$2" - if "$cli" plugin update "$plugin_slug" >/dev/null 2>&1; then + if "$cli" plugin list 2>/dev/null | grep -qF "$plugin_slug"; then + "$cli" plugin update "$plugin_slug" >/dev/null 2>&1 \ + || die "$cli plugin update failed. Run '$cli plugin update $plugin_slug' to see the error." ok "Plugin updated" - elif "$cli" plugin install "$plugin_slug" >/dev/null 2>&1; then - ok "Plugin installed" else - die "$cli plugin install failed. Run '$cli plugin install $plugin_slug' to see the error." + "$cli" plugin install "$plugin_slug" >/dev/null 2>&1 \ + || die "$cli plugin install failed. Run '$cli plugin install $plugin_slug' to see the error." + ok "Plugin installed" fi } diff --git a/tests/test_install_plugin_upgrade_ps1.ps1 b/tests/test_install_plugin_upgrade_ps1.ps1 index 2a7e896..99d7e3e 100644 --- a/tests/test_install_plugin_upgrade_ps1.ps1 +++ b/tests/test_install_plugin_upgrade_ps1.ps1 @@ -15,6 +15,7 @@ $fakeCli = Join-Path $bin 'fake-plugin-cli-fixture.ps1' param([Parameter(ValueFromRemainingArguments = $true)][string[]]$Argv) $snapshot = Join-Path $env:FAKE_CLI_STATE 'marketplace-snapshot' $installed = Join-Path $env:FAKE_CLI_STATE 'installed-version' +$installedSlug = Join-Path $env:FAKE_CLI_STATE 'installed-slug' Add-Content -LiteralPath (Join-Path $env:FAKE_CLI_STATE 'cli.log') -Value ($Argv -join ' ') $command = ($Argv | Select-Object -First 3) -join ' ' if ($command -eq 'plugin marketplace add') { @@ -26,9 +27,14 @@ if ($command -eq 'plugin marketplace update') { exit 0 } $sub = "$($Argv[0]) $($Argv[1])" +if ($sub -eq 'plugin list') { + if (Test-Path -LiteralPath $installedSlug) { Get-Content -LiteralPath $installedSlug } + exit 0 +} if ($sub -eq 'plugin install') { if (Test-Path -LiteralPath $installed) { exit 0 } Copy-Item -LiteralPath $snapshot -Destination $installed + [System.IO.File]::WriteAllText($installedSlug, $Argv[2]) } if ($sub -eq 'plugin update') { if (-not (Test-Path -LiteralPath $installed)) { exit 1 } @@ -73,12 +79,12 @@ function Install-FromMarketplace { $env:PUBLISHED_VERSION = '1.0.0' Install-FromMarketplace Assert-Eq (Get-InstalledVersion) '1.0.0' 'fresh: the published version is installed' -Assert-Eq (Get-CliCalls) 'plugin marketplace add rogue-test/rogue-plugins | plugin marketplace update rogue-marketplace | plugin update rogue@rogue-marketplace | plugin install rogue@rogue-marketplace' 'fresh: marketplace added and refreshed, update tried, then install' +Assert-Eq (Get-CliCalls) 'plugin marketplace add rogue-test/rogue-plugins | plugin marketplace update rogue-marketplace | plugin list | plugin install rogue@rogue-marketplace' 'fresh: marketplace added and refreshed, plugin installed' $env:PUBLISHED_VERSION = '1.0.1' Install-FromMarketplace Assert-Eq (Get-InstalledVersion) '1.0.1' 'upgrade: the installed version moves to the newly published version' -Assert-Eq (Get-CliCalls) 'plugin marketplace add rogue-test/rogue-plugins | plugin marketplace update rogue-marketplace | plugin update rogue@rogue-marketplace' 'upgrade: marketplace refreshed, update alone runs' +Assert-Eq (Get-CliCalls) 'plugin marketplace add rogue-test/rogue-plugins | plugin marketplace update rogue-marketplace | plugin list | plugin update rogue@rogue-marketplace' 'upgrade: marketplace refreshed, installed plugin updated' $env:PATH = $prevPath $env:USERPROFILE = $prevProfile diff --git a/tests/test_install_plugin_upgrade_sh.sh b/tests/test_install_plugin_upgrade_sh.sh index 539c613..73a4472 100755 --- a/tests/test_install_plugin_upgrade_sh.sh +++ b/tests/test_install_plugin_upgrade_sh.sh @@ -22,6 +22,7 @@ cat > "$BIN/fake-plugin-cli" <<'STUB' cli="$(basename "$0")" snapshot="$HOME/$cli-marketplace-snapshot" installed="$HOME/$cli-installed-version" +installed_slug="$HOME/$cli-installed-slug" echo "$*" >> "$HOME/$cli.log" [ -z "${FAKE_CLI_BROKEN:-}" ] || exit 1 case "$*" in @@ -34,11 +35,15 @@ case "$*" in "plugin marketplace update "*|"plugin marketplace upgrade "*) [ -f "$snapshot" ] || exit 1 printf '%s' "$PUBLISHED_VERSION" > "$snapshot" ;; + "plugin list") + cat "$installed_slug" 2>/dev/null ;; "plugin install "*) [ -f "$installed" ] && exit 0 - cp "$snapshot" "$installed" ;; + cp "$snapshot" "$installed" + printf '%s\n' "$3" > "$installed_slug" ;; "plugin update "*) [ -f "$installed" ] || exit 1 + [ -z "${FAKE_UPDATE_BROKEN:-}" ] || exit 1 cp "$snapshot" "$installed" ;; "plugin add "*) cp "$snapshot" "$installed" ;; @@ -87,6 +92,13 @@ for cli in claude codex copilot; do check "second upgrade: $cli moves again" "1.0.2" "$(installed_version "$H" "$cli")" done +run_install "$H" PUBLISHED_VERSION=1.0.3 FAKE_UPDATE_BROKEN=1 +[ "$LAST_RC" != 0 ] && ok "failed update: install fails" || bad "failed update: install fails" "exited 0" +check "failed update: claude stays on its version" "1.0.2" "$(installed_version "$H" claude)" +check "failed update: the error names the update command" "1" \ + "$(grep -c "Run 'claude plugin update rogue@rogue-marketplace' to see the error" "$ERR" || :)" +check "failed update: no success is reported" "0" "$(grep -c 'Plugin installed' "$ERR" || :)" + H="$WORK/broken" run_install "$H" PUBLISHED_VERSION=1.0.0 FAKE_CLI_BROKEN=1 [ "$LAST_RC" != 0 ] && ok "broken: install fails" || bad "broken: install fails" "exited 0" From 5185078254bf26e65304db9f7a4196d8f7c95f0b Mon Sep 17 00:00:00 2001 From: amos Date: Tue, 20 Oct 2026 16:02:40 +0300 Subject: [PATCH 3/4] fix(install): stop when plugin list fails, warn that a stale marketplace may not upgrade (FIRE-2097) A failed `plugin list` was read as "not installed", which led to `plugin install`, a successful no-op for an installed plugin. The installers now stop with an error instead. In install.ps1 the listing redirects stderr with 2>&1, since 2>$null under $ErrorActionPreference = 'Stop' can throw on Windows PowerShell 5.1. A failed marketplace refresh stays a warning, so a transient network error does not abort the installer for every agent, but the warning now says the plugin may stay at its current version. Co-Authored-By: Claude Opus 5.5 (1M context) --- install.ps1 | 7 ++++--- install.sh | 8 +++++--- tests/test_install_plugin_upgrade_ps1.ps1 | 11 +++++++++++ tests/test_install_plugin_upgrade_sh.sh | 14 +++++++++++++- 4 files changed, 33 insertions(+), 7 deletions(-) diff --git a/install.ps1 b/install.ps1 index 11554ed..61fdd02 100644 --- a/install.ps1 +++ b/install.ps1 @@ -576,13 +576,14 @@ function Add-AndRefreshMarketplace { $refreshed = $false try { & $Cli plugin marketplace $RefreshCommand $Marketplace 2>&1 | Out-Null; $refreshed = $LASTEXITCODE -eq 0 } catch {} if ($refreshed) { Ok 'Marketplace up to date' } - else { Warn2 'Could not add or update marketplace (continuing - it may already be present).' } + else { Warn2 "Could not refresh the $Cli marketplace - the plugin may stay at its current version." } } function Update-OrInstallPlugin { param([string]$Cli, [string]$PluginSlug) - $installedPlugins = '' - try { $installedPlugins = (& $Cli plugin list 2>$null | Out-String) } catch {} + $listed = $false + try { $installedPlugins = (& $Cli plugin list 2>&1 | Out-String); $listed = $LASTEXITCODE -eq 0 } catch {} + if (-not $listed) { Die "$Cli plugin list failed. Run '$Cli plugin list' to see the error." } $command = if ($installedPlugins.Contains($PluginSlug)) { 'update' } else { 'install' } $succeeded = $false try { & $Cli plugin $command $PluginSlug 2>&1 | Out-Null; $succeeded = $LASTEXITCODE -eq 0 } catch {} diff --git a/install.sh b/install.sh index cf2b4e9..569f710 100755 --- a/install.sh +++ b/install.sh @@ -155,14 +155,16 @@ add_and_refresh_marketplace() { if "$cli" plugin marketplace "$refresh_command" "$marketplace" >/dev/null 2>&1; then ok "Marketplace up to date" else - warn "Could not add or update marketplace (continuing — it may already be present)" + warn "Could not refresh the $cli marketplace — the plugin may stay at its current version" [ -n "$add_err" ] && note "${C_DIM}${add_err}${C_RESET}" fi } update_or_install_plugin() { - local cli="$1" plugin_slug="$2" - if "$cli" plugin list 2>/dev/null | grep -qF "$plugin_slug"; then + local cli="$1" plugin_slug="$2" installed_plugins + installed_plugins="$("$cli" plugin list 2>/dev/null)" \ + || die "$cli plugin list failed. Run '$cli plugin list' to see the error." + if printf '%s\n' "$installed_plugins" | grep -qF "$plugin_slug"; then "$cli" plugin update "$plugin_slug" >/dev/null 2>&1 \ || die "$cli plugin update failed. Run '$cli plugin update $plugin_slug' to see the error." ok "Plugin updated" diff --git a/tests/test_install_plugin_upgrade_ps1.ps1 b/tests/test_install_plugin_upgrade_ps1.ps1 index 99d7e3e..eb8f7d4 100644 --- a/tests/test_install_plugin_upgrade_ps1.ps1 +++ b/tests/test_install_plugin_upgrade_ps1.ps1 @@ -28,6 +28,7 @@ if ($command -eq 'plugin marketplace update') { } $sub = "$($Argv[0]) $($Argv[1])" if ($sub -eq 'plugin list') { + if ($env:FAKE_LIST_BROKEN) { exit 1 } if (Test-Path -LiteralPath $installedSlug) { Get-Content -LiteralPath $installedSlug } exit 0 } @@ -86,6 +87,16 @@ Install-FromMarketplace Assert-Eq (Get-InstalledVersion) '1.0.1' 'upgrade: the installed version moves to the newly published version' Assert-Eq (Get-CliCalls) 'plugin marketplace add rogue-test/rogue-plugins | plugin marketplace update rogue-marketplace | plugin list | plugin update rogue@rogue-marketplace' 'upgrade: marketplace refreshed, installed plugin updated' +$env:FAKE_LIST_BROKEN = '1' +$powershell = (Get-Process -Id $PID).Path +$failedListing = & $powershell -NoProfile -Command "`$env:ROGUE_INSTALL_LIB_ONLY = '1'; . '$installer'; Update-OrInstallPlugin -Cli 'fake-plugin-cli' -PluginSlug 'rogue@rogue-marketplace'" 2>&1 | Out-String +$failedListingExit = $LASTEXITCODE +$env:FAKE_LIST_BROKEN = $null +Assert-Eq $failedListingExit 1 'failed listing: the installer stops' +Assert-Eq (Get-InstalledVersion) '1.0.1' 'failed listing: the installed version is unchanged' +Assert-Eq ($failedListing.Contains("Run 'fake-plugin-cli plugin list' to see the error")) $true 'failed listing: the error names the list command' +Assert-Eq ($failedListing.Contains('Plugin installed')) $false 'failed listing: no success is reported' + $env:PATH = $prevPath $env:USERPROFILE = $prevProfile $env:FAKE_CLI_STATE = $null diff --git a/tests/test_install_plugin_upgrade_sh.sh b/tests/test_install_plugin_upgrade_sh.sh index 73a4472..368c53c 100755 --- a/tests/test_install_plugin_upgrade_sh.sh +++ b/tests/test_install_plugin_upgrade_sh.sh @@ -34,8 +34,10 @@ case "$*" in printf '%s' "$PUBLISHED_VERSION" > "$snapshot" ;; "plugin marketplace update "*|"plugin marketplace upgrade "*) [ -f "$snapshot" ] || exit 1 + [ -z "${FAKE_REFRESH_BROKEN:-}" ] || exit 1 printf '%s' "$PUBLISHED_VERSION" > "$snapshot" ;; "plugin list") + [ -z "${FAKE_LIST_BROKEN:-}" ] || exit 1 cat "$installed_slug" 2>/dev/null ;; "plugin install "*) [ -f "$installed" ] && exit 0 @@ -99,10 +101,20 @@ check "failed update: the error names the update command" "1" \ "$(grep -c "Run 'claude plugin update rogue@rogue-marketplace' to see the error" "$ERR" || :)" check "failed update: no success is reported" "0" "$(grep -c 'Plugin installed' "$ERR" || :)" +run_install "$H" PUBLISHED_VERSION=1.0.3 FAKE_LIST_BROKEN=1 +[ "$LAST_RC" != 0 ] && ok "failed listing: install fails" || bad "failed listing: install fails" "exited 0" +check "failed listing: claude stays on its version" "1.0.2" "$(installed_version "$H" claude)" +check "failed listing: the error names the list command" "1" "$(grep -c "Run 'claude plugin list' to see the error" "$ERR" || :)" +check "failed listing: no success is reported" "0" "$(grep -c 'Plugin installed' "$ERR" || :)" + +run_install "$H" PUBLISHED_VERSION=1.0.4 FAKE_REFRESH_BROKEN=1 +check "failed refresh: claude stays on the last fetched version" "1.0.3" "$(installed_version "$H" claude)" +check "failed refresh: the warning says the plugin may not move" "3" "$(grep -c 'marketplace — the plugin may stay at its current version' "$ERR" || :)" + H="$WORK/broken" run_install "$H" PUBLISHED_VERSION=1.0.0 FAKE_CLI_BROKEN=1 [ "$LAST_RC" != 0 ] && ok "broken: install fails" || bad "broken: install fails" "exited 0" check "broken: the error names the command to retry" "1" \ - "$(grep -c "Run 'claude plugin install rogue@rogue-marketplace' to see the error" "$ERR" || :)" + "$(grep -c "Run 'claude plugin list' to see the error" "$ERR" || :)" [ "$fails" -eq 0 ] && echo "all plugin upgrade installer tests passed" || { echo "$fails failure(s)"; exit 1; } From 11e72e74ee7376113882b11a8368af2ddd9d2ea2 Mon Sep 17 00:00:00 2001 From: amos Date: Tue, 20 Oct 2026 16:13:06 +0300 Subject: [PATCH 4/4] fix(install.ps1): run plugin CLIs under Continue so stderr cannot fail them (FIRE-2097) Under $ErrorActionPreference = 'Stop', Windows PowerShell 5.1 can turn a native command's redirected stderr into a terminating error, so a CLI that succeeded while printing a warning made the installer die. Invoke-PluginCli runs the command with 'Continue' and returns its output and exit code, as Invoke-KiroCli already does for kiro-cli. The marketplace, plugin and `codex plugin add` calls use it, and the test's fake CLI now writes to stderr on every call so the Windows PowerShell 5.1 job exercises the case. Co-Authored-By: Claude Opus 5.5 (1M context) --- install.ps1 | 35 ++++++++++++++--------- tests/test_install_plugin_upgrade_ps1.ps1 | 1 + 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/install.ps1 b/install.ps1 index 61fdd02..7c9a7f4 100644 --- a/install.ps1 +++ b/install.ps1 @@ -569,25 +569,33 @@ function Configure-Credentials { Write-UserEnvFile } +function Invoke-PluginCli { + param([string]$Cli, [string[]]$CliArgs) + $ErrorActionPreference = 'Continue' + try { + $output = (& $Cli @CliArgs 2>&1 | Out-String) + return @{ Output = $output; ExitCode = $LASTEXITCODE } + } catch { + return @{ Output = $_.Exception.Message; ExitCode = 1 } + } +} + function Add-AndRefreshMarketplace { param([string]$Cli, [string]$RefreshCommand, [string]$Marketplace) Log "Adding marketplace $PluginRepo" - try { & $Cli plugin marketplace add $PluginRepo 2>&1 | Out-Null } catch {} - $refreshed = $false - try { & $Cli plugin marketplace $RefreshCommand $Marketplace 2>&1 | Out-Null; $refreshed = $LASTEXITCODE -eq 0 } catch {} - if ($refreshed) { Ok 'Marketplace up to date' } + Invoke-PluginCli $Cli @('plugin', 'marketplace', 'add', $PluginRepo) | Out-Null + $refresh = Invoke-PluginCli $Cli @('plugin', 'marketplace', $RefreshCommand, $Marketplace) + if ($refresh.ExitCode -eq 0) { Ok 'Marketplace up to date' } else { Warn2 "Could not refresh the $Cli marketplace - the plugin may stay at its current version." } } function Update-OrInstallPlugin { param([string]$Cli, [string]$PluginSlug) - $listed = $false - try { $installedPlugins = (& $Cli plugin list 2>&1 | Out-String); $listed = $LASTEXITCODE -eq 0 } catch {} - if (-not $listed) { Die "$Cli plugin list failed. Run '$Cli plugin list' to see the error." } - $command = if ($installedPlugins.Contains($PluginSlug)) { 'update' } else { 'install' } - $succeeded = $false - try { & $Cli plugin $command $PluginSlug 2>&1 | Out-Null; $succeeded = $LASTEXITCODE -eq 0 } catch {} - if (-not $succeeded) { Die "$Cli plugin $command failed. Run '$Cli plugin $command $PluginSlug' to see the error." } + $listing = Invoke-PluginCli $Cli @('plugin', 'list') + if ($listing.ExitCode -ne 0) { Die "$Cli plugin list failed. Run '$Cli plugin list' to see the error." } + $command = if ($listing.Output.Contains($PluginSlug)) { 'update' } else { 'install' } + $result = Invoke-PluginCli $Cli @('plugin', $command, $PluginSlug) + if ($result.ExitCode -ne 0) { Die "$Cli plugin $command failed. Run '$Cli plugin $command $PluginSlug' to see the error." } if ($command -eq 'update') { Ok 'Plugin updated' } else { Ok 'Plugin installed' } } @@ -679,9 +687,8 @@ if ($hasCodex) { Write-Host "Rogue Security - OpenAI Codex" -ForegroundColor Cyan Add-AndRefreshMarketplace -Cli 'codex' -RefreshCommand 'upgrade' -Marketplace $MarketplaceName Log "Installing plugin $PluginName@$MarketplaceName" - $installed = $false - try { & codex plugin add "$PluginName@$MarketplaceName" 2>&1 | Out-Null; if ($LASTEXITCODE -eq 0) { $installed = $true } } catch {} - if (-not $installed) { Die "codex plugin add failed. Run 'codex plugin add $PluginName@$MarketplaceName' to see the error." } + $codexAdd = Invoke-PluginCli 'codex' @('plugin', 'add', "$PluginName@$MarketplaceName") + if ($codexAdd.ExitCode -ne 0) { Die "codex plugin add failed. Run 'codex plugin add $PluginName@$MarketplaceName' to see the error." } Ok 'Plugin installed' Warn2 'Codex skips untrusted hooks - open /hooks in Codex and trust the Rogue entries once.' } diff --git a/tests/test_install_plugin_upgrade_ps1.ps1 b/tests/test_install_plugin_upgrade_ps1.ps1 index eb8f7d4..2c971af 100644 --- a/tests/test_install_plugin_upgrade_ps1.ps1 +++ b/tests/test_install_plugin_upgrade_ps1.ps1 @@ -17,6 +17,7 @@ $snapshot = Join-Path $env:FAKE_CLI_STATE 'marketplace-snapshot' $installed = Join-Path $env:FAKE_CLI_STATE 'installed-version' $installedSlug = Join-Path $env:FAKE_CLI_STATE 'installed-slug' Add-Content -LiteralPath (Join-Path $env:FAKE_CLI_STATE 'cli.log') -Value ($Argv -join ' ') +[Console]::Error.WriteLine('notice: the fake plugin CLI writes to stderr on every call') $command = ($Argv | Select-Object -First 3) -join ' ' if ($command -eq 'plugin marketplace add') { if (-not (Test-Path -LiteralPath $snapshot)) { [System.IO.File]::WriteAllText($snapshot, $env:PUBLISHED_VERSION) }