<# .SYNOPSIS Install or uninstall JetBrains plugins in locally installed IDEs (and in the JetBrains Gateway Client) on Windows. .DESCRIPTION Detects every JetBrains IDE under %LOCALAPPDATA%\Programs\* (standalone installs and Toolbox-managed installs) plus Toolbox shims under %LOCALAPPDATA%\JetBrains\Toolbox\scripts\*.cmd, and the most-recent JetBrains Gateway Client under %LOCALAPPDATA%\JetBrains\JetBrainsClient*. You select IDE(s), choose install or uninstall, and then select plugin(s). Standard IDEs get "installPlugins ..." invoked once with all selected plugins. Uninstall reads plugin IDs from the selected IDEs' user plugin directories and removes only the entries selected by the user. The Gateway Client path is special: the bundled client has no installPlugins CLI, so plugins are downloaded directly from plugins.jetbrains.com and unpacked into the client's plugins folder (mirrors the user's plugins_gateway.ps1). .NOTES Process-scope ExecutionPolicy is set to RemoteSigned. No machine-wide change is persisted. If PowerShell still refuses to run the file: powershell -ExecutionPolicy Bypass -File .\install-jetbrains.ps1 #> [CmdletBinding()] param() # Loosen execution policy for THIS process only. try { $current = Get-ExecutionPolicy -Scope Process -ErrorAction Stop if ($current -in @('Restricted', 'AllSigned', 'Undefined')) { Set-ExecutionPolicy -Scope Process -ExecutionPolicy RemoteSigned -Force -ErrorAction Stop } } catch { Write-Verbose ("Could not adjust process execution policy: {0}" -f $_.Exception.Message) } $ErrorActionPreference = 'Stop' [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 try { Add-Type -AssemblyName System.IO.Compression.FileSystem -ErrorAction Stop } catch { Write-Verbose ("Could not preload ZIP support: {0}" -f $_.Exception.Message) } # ------------------------------------------------------------------ # Canonical plugin catalog. Each entry: @{ Id; Label } # ------------------------------------------------------------------ $PluginCatalog = @( @{ Id = 'com.intellij.ml.llm'; Label = 'JetBrains AI Assistant' } @{ Id = 'izhangzhihao.rainbow.brackets'; Label = 'Rainbow Brackets' } @{ Id = 'IdeaVIM'; Label = 'IdeaVim' } @{ Id = 'org.sonarlint.idea'; Label = 'SonarQube for IDE (SonarLint)' } @{ Id = 'Key Promoter X'; Label = 'Key Promoter X' } @{ Id = 'net.ashald.envfile'; Label = 'EnvFile' } @{ Id = 'org.intellij.qodana'; Label = 'Qodana' } ) $KnownLauncherStems = @( 'idea64', 'pycharm64', 'webstorm64', 'goland64', 'rubymine64', 'clion64', 'phpstorm64', 'datagrip64', 'rustrover64', 'rider64', 'studio64', 'fleet' ) function Get-PrettyLabel { param([string]$Stem) switch -Regex ($Stem) { '^idea' { return 'IntelliJ IDEA' } '^pycharm' { return 'PyCharm' } '^webstorm' { return 'WebStorm' } '^goland' { return 'GoLand' } '^rubymine' { return 'RubyMine' } '^clion' { return 'CLion' } '^phpstorm' { return 'PhpStorm' } '^datagrip' { return 'DataGrip' } '^rustrover' { return 'RustRover' } '^rider' { return 'Rider' } '^studio' { return 'Android Studio' } '^fleet' { return 'Fleet' } default { return $Stem } } } # ------------------------------------------------------------------ # IDE candidate scan # Each candidate: PSCustomObject @{ Type; Launcher; Label; Path; Client } # Type: 'Ide' (run launcher installPlugins ...) # | 'WinCmd' (run .cmd shim via cmd.exe) # | 'Gateway' (download-and-unzip flow into $Client\plugins) # ------------------------------------------------------------------ $Candidates = New-Object System.Collections.Generic.List[object] # 1) Standalone & Toolbox-app installs under %LOCALAPPDATA%\Programs\*\bin\*.exe $programs = Join-Path $env:LOCALAPPDATA 'Programs' if (Test-Path $programs) { Get-ChildItem -Path $programs -Directory -ErrorAction SilentlyContinue | ForEach-Object { $binDir = Join-Path $_.FullName 'bin' if (-not (Test-Path $binDir)) { return } Get-ChildItem -Path $binDir -Filter '*.exe' -File -ErrorAction SilentlyContinue | ForEach-Object { $stem = [System.IO.Path]::GetFileNameWithoutExtension($_.Name) if ($KnownLauncherStems -notcontains $stem) { return } $Candidates.Add([pscustomobject]@{ Type = 'Ide' Launcher = $_.FullName Label = ("{0} ({1})" -f (Get-PrettyLabel $stem), $_.FullName) Path = $_.FullName Client = $null }) } } } # 2) Toolbox .cmd shims $tbScripts = Join-Path $env:LOCALAPPDATA 'JetBrains\Toolbox\scripts' if (Test-Path $tbScripts) { Get-ChildItem -Path $tbScripts -Filter '*.cmd' -File -ErrorAction SilentlyContinue | ForEach-Object { $stem = [System.IO.Path]::GetFileNameWithoutExtension($_.Name) # Toolbox shims keep names like "idea.cmd", "rider.cmd" — strip trailing 64 etc. $Candidates.Add([pscustomobject]@{ Type = 'WinCmd' Launcher = $_.FullName Label = ("{0} ({1})" -f (Get-PrettyLabel $stem), $_.FullName) Path = $_.FullName Client = $null }) } } # 3) JetBrains Gateway Client — pick the most-recently-modified one if any. $jbRoot = Join-Path $env:LOCALAPPDATA 'JetBrains' if (Test-Path $jbRoot) { $client = Get-ChildItem -Path $jbRoot -Directory -Filter 'JetBrainsClient*' -ErrorAction SilentlyContinue | Sort-Object LastWriteTime -Descending | Select-Object -First 1 if ($client) { $Candidates.Add([pscustomobject]@{ Type = 'Gateway' Launcher = $client.FullName Label = ("JetBrains Gateway Client ({0})" -f $client.FullName) Path = $client.FullName Client = $client.FullName }) } } if ($Candidates.Count -eq 0) { Write-Host "" Write-Host "No JetBrains IDEs or Gateway clients found." -ForegroundColor Red Write-Host "Install one via the JetBrains Toolbox, then re-run." -ForegroundColor Yellow exit 1 } # ------------------------------------------------------------------ # Multi-select picker — prefer Out-GridView when available. # ------------------------------------------------------------------ function Select-Multi { param( [Parameter(Mandatory)] [string]$Title, [Parameter(Mandatory)] [array]$Items, [Parameter(Mandatory)] [string]$Property ) $hasOgv = $false try { $cmd = Get-Command Out-GridView -ErrorAction Stop if ($cmd) { $hasOgv = $true } } catch { $hasOgv = $false } if ($hasOgv) { $chosen = $Items | Out-GridView -Title $Title -OutputMode Multiple return ,@($chosen) } # Numbered-prompt fallback. Write-Host "" Write-Host $Title -ForegroundColor Cyan for ($i = 0; $i -lt $Items.Count; $i++) { Write-Host (" {0,2}) {1}" -f ($i + 1), $Items[$i].$Property) } while ($true) { $raw = Read-Host "Select (e.g. 1,3,5 or a for all)" if ([string]::IsNullOrWhiteSpace($raw)) { Write-Host " please enter at least one number, or 'a'." -ForegroundColor Yellow continue } if ($raw.Trim().ToLower() -eq 'a') { return ,@($Items) } $idxs = $raw -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne '' } $bad = $false $sel = @() foreach ($t in $idxs) { if ($t -notmatch '^[0-9]+$') { $bad = $true; break } $n = [int]$t if ($n -lt 1 -or $n -gt $Items.Count) { $bad = $true; break } $sel += $Items[$n - 1] } if ($bad -or $sel.Count -eq 0) { Write-Host " invalid input; try again." -ForegroundColor Yellow continue } return ,@($sel) } } function Select-Action { while ($true) { Write-Host "" Write-Host "What do you want to do?" -ForegroundColor Cyan Write-Host " 1) Install plugins" Write-Host " 2) Uninstall plugins" $choice = Read-Host "Action (1 or 2)" switch ($choice.Trim().ToLower()) { { $_ -in @('1', 'i', 'install') } { return 'Install' } { $_ -in @('2', 'u', 'uninstall') } { return 'Uninstall' } default { Write-Host " invalid input; enter 1 for install or 2 for uninstall." -ForegroundColor Yellow } } } } # Find product-info.json for the selected launcher and return the IDE's # dataDirectoryName. This prevents uninstall from touching unrelated profiles. function Get-ProductDataName { param([Parameter(Mandatory)] [psobject]$Ide) if ($Ide.Type -eq 'Gateway') { return $null } $probe = $Ide.Launcher # Toolbox .cmd shims normally contain the real launcher path. if ($Ide.Type -eq 'WinCmd' -and (Test-Path -LiteralPath $probe)) { try { $cmdText = Get-Content -LiteralPath $probe -Raw -ErrorAction Stop $match = [regex]::Match($cmdText, '"([^"\r\n]+\.exe)"', [System.Text.RegularExpressions.RegexOptions]::IgnoreCase) if ($match.Success) { $expanded = [Environment]::ExpandEnvironmentVariables($match.Groups[1].Value) if (Test-Path -LiteralPath $expanded) { $probe = $expanded } } } catch { Write-Verbose ("Could not resolve Toolbox shim {0}: {1}" -f $probe, $_.Exception.Message) } } try { $item = Get-Item -LiteralPath $probe -ErrorAction Stop $dir = if ($item.PSIsContainer) { $item.FullName } else { $item.Directory.FullName } } catch { return $null } while ($dir) { foreach ($relative in @('product-info.json', 'Resources\product-info.json')) { $infoPath = Join-Path $dir $relative if (-not (Test-Path -LiteralPath $infoPath)) { continue } try { $info = Get-Content -LiteralPath $infoPath -Raw -ErrorAction Stop | ConvertFrom-Json if ($info.dataDirectoryName) { return [string]$info.dataDirectoryName } } catch { Write-Verbose ("Could not read {0}: {1}" -f $infoPath, $_.Exception.Message) } } $parent = [System.IO.Directory]::GetParent($dir) if (-not $parent) { break } $dir = $parent.FullName } return $null } function Get-PluginRoot { param([Parameter(Mandatory)] [psobject]$Ide) if ($Ide.Type -eq 'Gateway') { return (Join-Path $Ide.Client 'plugins') } $dataName = Get-ProductDataName -Ide $Ide if (-not $dataName) { return $null } return (Join-Path (Join-Path $env:APPDATA 'JetBrains') (Join-Path $dataName 'plugins')) } function ConvertFrom-PluginXml { param([Parameter(Mandatory)] [string]$Text) try { $settings = New-Object System.Xml.XmlReaderSettings $settings.DtdProcessing = [System.Xml.DtdProcessing]::Prohibit $stringReader = New-Object System.IO.StringReader($Text) $reader = [System.Xml.XmlReader]::Create($stringReader, $settings) $doc = New-Object System.Xml.XmlDocument $doc.XmlResolver = $null $doc.Load($reader) $reader.Dispose() $stringReader.Dispose() $idNode = $doc.SelectSingleNode('/idea-plugin/id') $nameNode = $doc.SelectSingleNode('/idea-plugin/name') $id = if ($idNode) { $idNode.InnerText.Trim() } elseif ($nameNode) { $nameNode.InnerText.Trim() } else { $null } $name = if ($nameNode) { $nameNode.InnerText.Trim() } else { $id } if ($id) { return [pscustomobject]@{ Id = $id; Label = $name } } } catch { Write-Verbose ("Could not parse plugin.xml: {0}" -f $_.Exception.Message) } return $null } function Get-ArchivePluginDescriptor { param([Parameter(Mandatory)] [string]$Path) $zip = $null try { $zip = [System.IO.Compression.ZipFile]::OpenRead($Path) $entry = $zip.Entries | Where-Object { $_.FullName -eq 'META-INF/plugin.xml' } | Select-Object -First 1 if (-not $entry) { return $null } $stream = $entry.Open() $reader = New-Object System.IO.StreamReader($stream) $text = $reader.ReadToEnd() $reader.Dispose() $stream.Dispose() return (ConvertFrom-PluginXml -Text $text) } catch { Write-Verbose ("Could not inspect plugin archive {0}: {1}" -f $Path, $_.Exception.Message) return $null } finally { if ($zip) { $zip.Dispose() } } } function Get-PluginDescriptor { param([Parameter(Mandatory)] [System.IO.FileSystemInfo]$Item) if (-not $Item.PSIsContainer) { return (Get-ArchivePluginDescriptor -Path $Item.FullName) } $looseXml = Join-Path $Item.FullName 'META-INF\plugin.xml' if (Test-Path -LiteralPath $looseXml) { try { return (ConvertFrom-PluginXml -Text (Get-Content -LiteralPath $looseXml -Raw -ErrorAction Stop)) } catch { Write-Verbose ("Could not inspect {0}: {1}" -f $looseXml, $_.Exception.Message) } } $archives = Get-ChildItem -LiteralPath $Item.FullName -Recurse -File -ErrorAction SilentlyContinue | Where-Object { $_.Extension -in @('.jar', '.zip') } foreach ($archive in $archives) { $descriptor = Get-ArchivePluginDescriptor -Path $archive.FullName if ($descriptor) { return $descriptor } } return $null } function Get-InstalledPlugins { param([Parameter(Mandatory)] [psobject]$Ide) $root = Get-PluginRoot -Ide $Ide if (-not $root) { Write-Host ("Could not resolve the plugin directory for: {0}" -f $Ide.Label) -ForegroundColor Yellow return } if (-not (Test-Path -LiteralPath $root)) { return } Get-ChildItem -LiteralPath $root -Force -ErrorAction SilentlyContinue | ForEach-Object { $descriptor = Get-PluginDescriptor -Item $_ if ($descriptor) { [pscustomobject]@{ Id = $descriptor.Id Label = $descriptor.Label Path = $_.FullName PluginRoot = $root TargetLabel = $Ide.Label } } } } Write-Host ("Detected {0} JetBrains target(s)." -f $Candidates.Count) -ForegroundColor Green $selectedIdes = Select-Multi -Title "Pick IDE(s)" -Items $Candidates -Property 'Label' if ($selectedIdes.Count -eq 0) { Write-Host "No IDEs selected; exiting." -ForegroundColor Red exit 1 } $action = Select-Action $installedPluginInstances = @() if ($action -eq 'Install') { $pluginChoices = $PluginCatalog } else { $installedPluginInstances = @($selectedIdes | ForEach-Object { Get-InstalledPlugins -Ide $_ }) $pluginChoices = @($installedPluginInstances | Group-Object Id | ForEach-Object { [pscustomobject]@{ Id = $_.Name; Label = $_.Group[0].Label } } | Sort-Object Label) if ($pluginChoices.Count -eq 0) { Write-Host "No removable user-installed plugins were found for the selected IDE(s)." -ForegroundColor Yellow Write-Host "Bundled plugins cannot be uninstalled; disable them in the IDE instead." -ForegroundColor Yellow exit 0 } } $selectedPlugins = Select-Multi -Title ("Pick plugin(s) to {0}" -f $action.ToLower()) -Items $pluginChoices -Property 'Label' if ($selectedPlugins.Count -eq 0) { Write-Host "No plugins selected; exiting." -ForegroundColor Red exit 1 } # ------------------------------------------------------------------ # Running-IDE preflight # Installing or deleting plugin files requires the selected IDE to be closed. # installPlugins refuses to run while the IDE GUI holds the config-dir lock # ("Only one instance of IDEA can be run at a time."). Best-effort detection # via Get-Process. Gateway Client type doesn't need this (no CLI invoked). # ------------------------------------------------------------------ function Test-IdeRunning { param([Parameter(Mandatory)] [psobject]$Ide) if ($Ide.Type -eq 'Gateway') { if ($action -eq 'Install') { return $false } foreach ($name in @('jetbrains_client64', 'jetbrains_client', 'JetBrainsClient')) { if (Get-Process -Name $name -ErrorAction SilentlyContinue) { return $true } } return $false } # For standalone IDE launchers the process name is the exe basename. # For Toolbox .cmd shims, the spawned process is e.g. idea64 -> use prefix. $stem = [System.IO.Path]::GetFileNameWithoutExtension($Ide.Launcher) $patterns = @($stem) if ($stem -notmatch '64$') { $patterns += ($stem + '64') } foreach ($pat in $patterns) { if (Get-Process -Name $pat -ErrorAction SilentlyContinue) { return $true } } return $false } while ($true) { $running = @($selectedIdes | Where-Object { Test-IdeRunning $_ }) if ($running.Count -eq 0) { break } Write-Host "" Write-Host "The following IDE(s) appear to be running — plugin changes need them closed:" -ForegroundColor Yellow foreach ($r in $running) { Write-Host (" - {0}" -f $r.Label) -ForegroundColor Yellow } Write-Host "" Read-Host "Close them, then press Enter to retry (Ctrl+C to abort)" | Out-Null } # ------------------------------------------------------------------ # Uninstall or install # ------------------------------------------------------------------ if ($action -eq 'Uninstall') { $selectedIds = @($selectedPlugins | ForEach-Object { $_.Id }) $toRemove = @($installedPluginInstances | Where-Object { $_.Id -in $selectedIds } | Where-Object { $parent = [System.IO.Directory]::GetParent($_.Path) $parent -and ($parent.FullName.TrimEnd('\') -eq $_.PluginRoot.TrimEnd('\')) }) if ($toRemove.Count -eq 0) { Write-Host "None of the selected plugins are installed in the selected IDE(s)." -ForegroundColor Yellow exit 0 } Write-Host "" Write-Host "The following plugin installations will be removed:" -ForegroundColor Yellow foreach ($plugin in $toRemove) { Write-Host (" - {0} ({1})" -f $plugin.Label, $plugin.Id) Write-Host (" IDE: {0}" -f $plugin.TargetLabel) Write-Host (" Path: {0}" -f $plugin.Path) } $confirm = Read-Host "Continue? [y/N]" if ($confirm -notmatch '^[Yy]$') { Write-Host "Uninstall cancelled." exit 0 } $removed = 0 $failed = 0 $failedList = @() foreach ($plugin in $toRemove) { Write-Host -NoNewline ("Removing {0} from {1}... " -f $plugin.Label, $plugin.TargetLabel) try { Remove-Item -LiteralPath $plugin.Path -Recurse -Force -ErrorAction Stop Write-Host "ok" -ForegroundColor Green $removed++ } catch { Write-Host "FAILED" -ForegroundColor Red Write-Host (" {0}" -f $_.Exception.Message) -ForegroundColor DarkRed $failed++ $failedList += ("{0} :: {1}" -f $plugin.TargetLabel, $plugin.Id) } } Write-Host ("Done. Removed: {0}, Failed: {1}. Restart the IDE(s) to finish." -f $removed, $failed) if ($failed -gt 0) { foreach ($entry in $failedList) { Write-Host (" - {0}" -f $entry) -ForegroundColor Red } exit 1 } exit 0 } $installed = 0 $failed = 0 $failedList = @() Write-Host "" Write-Host ("Installing {0} plugin(s) into {1} IDE(s)..." -f $selectedPlugins.Count, $selectedIdes.Count) -ForegroundColor Cyan Write-Host "" foreach ($ide in $selectedIdes) { Write-Host (">>> {0}" -f $ide.Label) switch ($ide.Type) { # ---- standard IDE launcher ---- 'Ide' { $args = @('installPlugins') + ($selectedPlugins | ForEach-Object { $_.Id }) try { $output = & $ide.Launcher @args 2>&1 if ($LASTEXITCODE -eq 0) { Write-Host (" all {0} plugin(s) ok" -f $selectedPlugins.Count) -ForegroundColor Green $installed += $selectedPlugins.Count } else { $joined = ($output | Out-String) if ($joined -match 'Only one instance') { Write-Host " IDE is running SKIPPED" -ForegroundColor Yellow Write-Host " Close the IDE and re-run the script to finish." -ForegroundColor DarkYellow } else { Write-Host (" installPlugins exited {0} FAILED" -f $LASTEXITCODE) -ForegroundColor Red $output | ForEach-Object { Write-Host " $_" -ForegroundColor DarkRed } } $failed += $selectedPlugins.Count foreach ($p in $selectedPlugins) { $failedList += ("{0} :: {1}" -f $ide.Label, $p.Id) } } } catch { Write-Host (" launcher invocation threw FAILED") -ForegroundColor Red $failed += $selectedPlugins.Count foreach ($p in $selectedPlugins) { $failedList += ("{0} :: {1}" -f $ide.Label, $p.Id) } Write-Host (" {0}" -f $_.Exception.Message) -ForegroundColor DarkRed } } # ---- Toolbox .cmd shim ---- 'WinCmd' { $idArgs = ($selectedPlugins | ForEach-Object { '"{0}"' -f $_.Id }) -join ' ' try { $output = cmd.exe /c "`"$($ide.Launcher)`" installPlugins $idArgs" 2>&1 if ($LASTEXITCODE -eq 0) { Write-Host (" all {0} plugin(s) ok" -f $selectedPlugins.Count) -ForegroundColor Green $installed += $selectedPlugins.Count } else { $joined = ($output | Out-String) if ($joined -match 'Only one instance') { Write-Host " IDE is running SKIPPED" -ForegroundColor Yellow Write-Host " Close the IDE and re-run the script to finish." -ForegroundColor DarkYellow } else { Write-Host (" installPlugins exited {0} FAILED" -f $LASTEXITCODE) -ForegroundColor Red $output | ForEach-Object { Write-Host " $_" -ForegroundColor DarkRed } } $failed += $selectedPlugins.Count foreach ($p in $selectedPlugins) { $failedList += ("{0} :: {1}" -f $ide.Label, $p.Id) } } } catch { Write-Host (" cmd.exe invocation threw FAILED") -ForegroundColor Red $failed += $selectedPlugins.Count foreach ($p in $selectedPlugins) { $failedList += ("{0} :: {1}" -f $ide.Label, $p.Id) } Write-Host (" {0}" -f $_.Exception.Message) -ForegroundColor DarkRed } } # ---- Gateway Client ---- 'Gateway' { $client = $ide.Client $buildFile = Join-Path $client 'build.txt' if (-not (Test-Path $buildFile)) { Write-Host " build.txt missing under client; cannot determine build id SKIPPED" -ForegroundColor Yellow Write-Host " (open a Remote Project once to populate the client folder)" -ForegroundColor DarkYellow $failed += $selectedPlugins.Count foreach ($p in $selectedPlugins) { $failedList += ("{0} :: {1}" -f $ide.Label, $p.Id) } continue } $buildId = (Get-Content $buildFile -Raw -ErrorAction Stop).Trim() $pluginDir = Join-Path $client 'plugins' if (-not (Test-Path $pluginDir)) { New-Item -ItemType Directory -Path $pluginDir | Out-Null } $ua = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" foreach ($p in $selectedPlugins) { Write-Host -NoNewline (" {0,-34} " -f $p.Id) $encoded = [uri]::EscapeDataString($p.Id) $url = "https://plugins.jetbrains.com/pluginManager/?action=download&id=$encoded&build=$buildId" $tmp = Join-Path $env:TEMP ("jb-" + [guid]::NewGuid().ToString() + ".bin") try { Invoke-WebRequest -Uri $url -OutFile $tmp -UserAgent $ua -ErrorAction Stop -MaximumRedirection 5 $bytes = [System.IO.File]::ReadAllBytes($tmp) | Select-Object -First 2 if ($bytes.Count -ge 2 -and $bytes[0] -eq 0x50 -and $bytes[1] -eq 0x4B) { Expand-Archive -Path $tmp -DestinationPath $pluginDir -Force -ErrorAction Stop Write-Host "ok (zip)" -ForegroundColor Green } else { $jarName = Join-Path $pluginDir ($p.Id -replace '[^A-Za-z0-9._-]', '_') + '.jar' Move-Item -Path $tmp -Destination $jarName -Force Write-Host "ok (jar)" -ForegroundColor Green } $installed++ } catch { Write-Host "FAILED" -ForegroundColor Red Write-Host (" {0}" -f $_.Exception.Message) -ForegroundColor DarkRed $failed++ $failedList += ("{0} :: {1}" -f $ide.Label, $p.Id) } finally { if (Test-Path $tmp) { Remove-Item $tmp -Force -ErrorAction SilentlyContinue } } } Write-Host " Reopen the Gateway Client to load the new plugins." -ForegroundColor Yellow } } Write-Host "" } Write-Host ("Done. Installed: {0}, Failed: {1}" -f $installed, $failed) if ($failed -gt 0) { Write-Host "Failed:" -ForegroundColor Red foreach ($f in $failedList) { Write-Host (" - {0}" -f $f) -ForegroundColor Red } exit 1 }