Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down
82 changes: 37 additions & 45 deletions install.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -569,6 +569,36 @@ 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"
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)
$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' }
Comment thread
coderabbitai[bot] marked this conversation as resolved.
$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' }
}


# Test seam: load only the functions above (tests/test_install_kiro_ps1.ps1,
# tests/test_install_env_ps1.ps1).
Expand Down Expand Up @@ -647,42 +677,18 @@ 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." }
$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.'
}
Expand Down Expand Up @@ -932,23 +938,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
Expand Down
82 changes: 28 additions & 54 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -148,55 +148,47 @@ 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 refresh the $cli marketplace — the plugin may stay at its current version"
[ -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" 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"
else
die "claude plugin install failed. Run 'claude plugin install ${PLUGIN_NAME}@${MARKETPLACE_NAME}' 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
}

# ── 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
Expand All @@ -213,27 +205,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) ────────────────────────────────────────────────
Expand Down
111 changes: 111 additions & 0 deletions tests/test_install_plugin_upgrade_ps1.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
#!/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'
$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) }
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 list') {
if ($env:FAKE_LIST_BROKEN) { exit 1 }
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 }
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 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 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
$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
Loading
Loading