<# .SYNOPSIS Install JetBrains plugins into locally installed IDEs (and into 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 multi-select IDE(s) and plugin(s) interactively. Standard IDEs get "installPlugins ..." invoked once with all selected plugins. 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 # ------------------------------------------------------------------ # 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) } } 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 } $selectedPlugins = Select-Multi -Title "Pick plugin(s)" -Items $PluginCatalog -Property 'Label' if ($selectedPlugins.Count -eq 0) { Write-Host "No plugins selected; exiting." -ForegroundColor Red exit 1 } # ------------------------------------------------------------------ # Running-IDE preflight # 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') { 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 — installPlugins needs 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 } # ------------------------------------------------------------------ # Install loop # ------------------------------------------------------------------ $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 }