VS Code + JetBrains Extension Installers
A bundle of cross-platform scripts that install a curated set of editor extensions / plugins, one command per platform.
| File | Editor | Platforms |
|---|---|---|
install.sh |
VS Code | macOS, Linux (Ubuntu) |
install.ps1 |
VS Code | Windows |
install-jetbrains.sh |
JetBrains IDEs + remote-dev-server | macOS, Linux, WSL2 |
install-jetbrains.ps1 |
JetBrains IDEs + Gateway Client | Windows |
Quick start
Replace <GIST> with the raw base of this Gist (everything up to and including /raw/HEAD/).
VS Code
# 1. macOS / Ubuntu (Install to default profile from a LOCAL .vscode/extensions.json)
bash <(curl -sSL https://opengist.rmrf.online/weehong/b2a7c0a2ae6d40e8a14f8d85964a9186/raw/HEAD/install.sh)
# 2. Install remote recommendation JSON to the default profile
bash <(curl -sSL https://opengist.rmrf.online/weehong/b2a7c0a2ae6d40e8a14f8d85964a9186/raw/HEAD/install.sh) -f "https://opengist.rmrf.online/weehong/b2a7c0a2ae6d40e8a14f8d85964a9186/raw/HEAD/typescript_react_recommendation.json"
# 3. Install TypeScript and React remote recommendation JSON to a specific named profile
bash <(curl -sSL https://opengist.rmrf.online/weehong/b2a7c0a2ae6d40e8a14f8d85964a9186/raw/HEAD/install.sh) -p "TypeScript" -f "https://opengist.rmrf.online/weehong/b2a7c0a2ae6d40e8a14f8d85964a9186/raw/HEAD/typescript_react_recommendation.json"
# 4. Install Python remote recommendation JSON to a specific named profile
bash <(curl -sSL https://opengist.rmrf.online/weehong/b2a7c0a2ae6d40e8a14f8d85964a9186/raw/HEAD/install.sh) -p "Python" -f "https://opengist.rmrf.online/weehong/b2a7c0a2ae6d40e8a14f8d85964a9186/raw/HEAD/python_recommendation.json"
# Windows
iwr -useb https://opengist.rmrf.online/weehong/b2a7c0a2ae6d40e8a14f8d85964a9186/raw/HEAD/install.ps1 | iex
# named profile
.\install.ps1 -ProfileName "WorkSetup"
JetBrains
# macOS / Ubuntu / WSL2 — interactive picker for IDEs and plugins
bash <(curl -sSL https://opengist.rmrf.online/weehong/b2a7c0a2ae6d40e8a14f8d85964a9186/raw/HEAD/install-jetbrains.sh)
# Windows — interactive picker
iwr -useb https://opengist.rmrf.online/weehong/b2a7c0a2ae6d40e8a14f8d85964a9186/raw/HEAD/install-jetbrains.ps1 | iex
What's in the catalogs
VS Code extensions
docker.dockergithub.github-vscode-themeGitHub.vscode-github-actionsms-azuretools.vscode-containersms-vscode-remote.vscode-remote-extensionpackpkief.material-icon-theme
(GitHub Copilot Chat is no longer in the list — it ships built-in with VS Code now.)
JetBrains plugins (canonical marketplace IDs)
| xmlId | Plugin |
|---|---|
com.intellij.ml.llm |
JetBrains AI Assistant |
izhangzhihao.rainbow.brackets |
Rainbow Brackets |
IdeaVIM |
IdeaVim |
org.sonarlint.idea |
SonarQube for IDE (SonarLint) |
Key Promoter X |
Key Promoter X |
net.ashald.envfile |
EnvFile |
org.intellij.qodana |
Qodana |
Two IDs are easy to get wrong: IdeaVIM (xmlId is all-caps VIM) and Key Promoter X (xmlId contains spaces — must be quoted when passed to installPlugins).
VS Code installer details
install.sh / install.ps1 are thin wrappers around code --install-extension:
--profile "Name"/-ProfileName "Name"installs into a named profile; VS Code creates the profile if it doesn't exist.--forceis passed so re-runs are idempotent.- Both scripts exit non-zero if any extension fails, and print a final summary.
Prereq: the code CLI must be on PATH.
- macOS: open VS Code → Cmd+Shift+P → "Shell Command: Install 'code' command in PATH".
- Ubuntu: install from https://code.visualstudio.com/ or
sudo snap install --classic code. - Windows: during install, check "Add to PATH" (the default).
JetBrains installer details
Both JetBrains scripts:
- Detect the host OS.
- Scan the system for installed JetBrains IDEs and (where applicable) Gateway / remote-dev targets.
- Show an interactive multi-select picker for IDEs and another for plugins.
- Run each IDE's
installPluginsagainst every selected plugin.
What gets scanned
install-jetbrains.sh
| OS | Source |
|---|---|
| macOS | ~/Library/Application Support/JetBrains/Toolbox/scripts/* (Toolbox shims) |
| macOS | /Applications/<JetBrains IDE>.app/Contents/MacOS/* |
| Linux | ~/.local/share/JetBrains/Toolbox/apps/*/bin/*.sh |
| Linux | /opt/*/bin/*.sh (snap / .deb / tarball) |
| Linux | ~/.cache/JetBrains/RemoteDev/dist/*/bin/remote-dev-server.sh (Gateway, SSH/Orbstack) |
| WSL2 | /mnt/c/Users/<WindowsUser>/AppData/Local/Programs/*/bin/*.exe |
| WSL2 | /mnt/c/Users/<WindowsUser>/AppData/Local/JetBrains/Toolbox/scripts/*.cmd |
| WSL2 | ~/.cache/JetBrains/RemoteDev/dist/*/bin/remote-dev-server.sh |
WSL2 detection works by grepping microsoft in /proc/version. The Windows username is read at runtime via cmd.exe /c 'echo %USERNAME%'.
install-jetbrains.ps1
| Source | What it is |
|---|---|
%LOCALAPPDATA%\Programs\*\bin\*64.exe |
Standalone + Toolbox app installs |
%LOCALAPPDATA%\JetBrains\Toolbox\scripts\*.cmd |
Toolbox shims |
%LOCALAPPDATA%\JetBrains\JetBrainsClient* (newest) |
Gateway Client (special path) |
Picker
- Bash: uses
fzf --multiiffzfis onPATH(TAB to toggle, Enter to confirm); otherwise prints a numbered list and accepts comma-separated indexes (1,3,5) orafor all. - PowerShell: uses
Out-GridView -OutputMode Multiplewhen available; otherwise the same numbered/comma-input fallback.
Gateway Client path (Windows)
When the script detects %LOCALAPPDATA%\JetBrains\JetBrainsClient* and you select it from the picker, the install logic switches: the bundled Gateway client has no installPlugins CLI, so the script reads the client's build.txt and downloads each plugin directly from plugins.jetbrains.com, unpacking it into the client's plugins\ folder.
If build.txt is missing, the client folder hasn't been initialized yet — open a Remote Project from Gateway once, then re-run.
After Gateway installs finish, close and re-open the Gateway Client for it to pick up the new plugins.
Troubleshooting
code not on PATH. See the VS Code installer details section above.
PowerShell refuses to run the script. Each .ps1 already sets RemoteSigned for the current process only. If your environment blocks that too, invoke explicitly:
powershell -ExecutionPolicy Bypass -File .\install.ps1
installPlugins reports a plugin is already installed. Harmless — it's a no-op. Both scripts treat it as success.
"Only one instance of IDEA can be run at a time." The IDE is currently open and holds the config-dir lock. Both JetBrains installers run a preflight check before installing: if a target IDE looks running, they prompt you to close it and press Enter to retry. If a process slips past the preflight, the install loop short-circuits with the same hint instead of repeating the error per plugin.
No JetBrains IDEs detected. Install one via the JetBrains Toolbox (which sets up the standard paths the script scans), or install standalone — both layouts are supported.
WSL2 picker is empty. Verify the Windows username is resolved: in WSL run cmd.exe /c 'echo %USERNAME%'. If that prints nothing, your interop.appendWindowsPath may be disabled; either re-enable it in /etc/wsl.conf or run install-jetbrains.ps1 natively on Windows instead.
Key Promoter X install fails. The xmlId contains spaces; if you're typing it manually elsewhere, quote it. The bundled scripts already do.
Notes for re-running
- Scripts are idempotent — already-installed extensions/plugins are skipped or upgraded in place.
- Exit code is non-zero if any extension/plugin failed.
- The JetBrains scripts ask every time; there's no flag to skip the picker by design.
| 1 | <# |
| 2 | .SYNOPSIS |
| 3 | Install JetBrains plugins into locally installed IDEs (and into the |
| 4 | JetBrains Gateway Client) on Windows. |
| 5 | |
| 6 | .DESCRIPTION |
| 7 | Detects every JetBrains IDE under %LOCALAPPDATA%\Programs\* (standalone |
| 8 | installs and Toolbox-managed installs) plus Toolbox shims under |
| 9 | %LOCALAPPDATA%\JetBrains\Toolbox\scripts\*.cmd, and the most-recent |
| 10 | JetBrains Gateway Client under %LOCALAPPDATA%\JetBrains\JetBrainsClient*. |
| 11 | |
| 12 | You multi-select IDE(s) and plugin(s) interactively. Standard IDEs get |
| 13 | "installPlugins <id>..." invoked once with all selected plugins. The |
| 14 | Gateway Client path is special: the bundled client has no installPlugins |
| 15 | CLI, so plugins are downloaded directly from plugins.jetbrains.com and |
| 16 | unpacked into the client's plugins folder (mirrors the user's |
| 17 | plugins_gateway.ps1). |
| 18 | |
| 19 | .NOTES |
| 20 | Process-scope ExecutionPolicy is set to RemoteSigned. No machine-wide |
| 21 | change is persisted. If PowerShell still refuses to run the file: |
| 22 | powershell -ExecutionPolicy Bypass -File .\install-jetbrains.ps1 |
| 23 | #> |
| 24 | |
| 25 | [CmdletBinding()] |
| 26 | param() |
| 27 | |
| 28 | # Loosen execution policy for THIS process only. |
| 29 | try { |
| 30 | $current = Get-ExecutionPolicy -Scope Process -ErrorAction Stop |
| 31 | if ($current -in @('Restricted', 'AllSigned', 'Undefined')) { |
| 32 | Set-ExecutionPolicy -Scope Process -ExecutionPolicy RemoteSigned -Force -ErrorAction Stop |
| 33 | } |
| 34 | } catch { |
| 35 | Write-Verbose ("Could not adjust process execution policy: {0}" -f $_.Exception.Message) |
| 36 | } |
| 37 | |
| 38 | $ErrorActionPreference = 'Stop' |
| 39 | [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 |
| 40 | |
| 41 | # ------------------------------------------------------------------ |
| 42 | # Canonical plugin catalog. Each entry: @{ Id; Label } |
| 43 | # ------------------------------------------------------------------ |
| 44 | $PluginCatalog = @( |
| 45 | @{ Id = 'com.intellij.ml.llm'; Label = 'JetBrains AI Assistant' } |
| 46 | @{ Id = 'izhangzhihao.rainbow.brackets'; Label = 'Rainbow Brackets' } |
| 47 | @{ Id = 'IdeaVIM'; Label = 'IdeaVim' } |
| 48 | @{ Id = 'org.sonarlint.idea'; Label = 'SonarQube for IDE (SonarLint)' } |
| 49 | @{ Id = 'Key Promoter X'; Label = 'Key Promoter X' } |
| 50 | @{ Id = 'net.ashald.envfile'; Label = 'EnvFile' } |
| 51 | @{ Id = 'org.intellij.qodana'; Label = 'Qodana' } |
| 52 | ) |
| 53 | |
| 54 | $KnownLauncherStems = @( |
| 55 | 'idea64', 'pycharm64', 'webstorm64', 'goland64', 'rubymine64', |
| 56 | 'clion64', 'phpstorm64', 'datagrip64', 'rustrover64', 'rider64', |
| 57 | 'studio64', 'fleet' |
| 58 | ) |
| 59 | |
| 60 | function Get-PrettyLabel { |
| 61 | param([string]$Stem) |
| 62 | switch -Regex ($Stem) { |
| 63 | '^idea' { return 'IntelliJ IDEA' } |
| 64 | '^pycharm' { return 'PyCharm' } |
| 65 | '^webstorm' { return 'WebStorm' } |
| 66 | '^goland' { return 'GoLand' } |
| 67 | '^rubymine' { return 'RubyMine' } |
| 68 | '^clion' { return 'CLion' } |
| 69 | '^phpstorm' { return 'PhpStorm' } |
| 70 | '^datagrip' { return 'DataGrip' } |
| 71 | '^rustrover' { return 'RustRover' } |
| 72 | '^rider' { return 'Rider' } |
| 73 | '^studio' { return 'Android Studio' } |
| 74 | '^fleet' { return 'Fleet' } |
| 75 | default { return $Stem } |
| 76 | } |
| 77 | } |
| 78 | |
| 79 | # ------------------------------------------------------------------ |
| 80 | # IDE candidate scan |
| 81 | # Each candidate: PSCustomObject @{ Type; Launcher; Label; Path; Client } |
| 82 | # Type: 'Ide' (run launcher installPlugins ...) |
| 83 | # | 'WinCmd' (run .cmd shim via cmd.exe) |
| 84 | # | 'Gateway' (download-and-unzip flow into $Client\plugins) |
| 85 | # ------------------------------------------------------------------ |
| 86 | $Candidates = New-Object System.Collections.Generic.List[object] |
| 87 | |
| 88 | # 1) Standalone & Toolbox-app installs under %LOCALAPPDATA%\Programs\*\bin\*.exe |
| 89 | $programs = Join-Path $env:LOCALAPPDATA 'Programs' |
| 90 | if (Test-Path $programs) { |
| 91 | Get-ChildItem -Path $programs -Directory -ErrorAction SilentlyContinue | ForEach-Object { |
| 92 | $binDir = Join-Path $_.FullName 'bin' |
| 93 | if (-not (Test-Path $binDir)) { return } |
| 94 | Get-ChildItem -Path $binDir -Filter '*.exe' -File -ErrorAction SilentlyContinue | ForEach-Object { |
| 95 | $stem = [System.IO.Path]::GetFileNameWithoutExtension($_.Name) |
| 96 | if ($KnownLauncherStems -notcontains $stem) { return } |
| 97 | $Candidates.Add([pscustomobject]@{ |
| 98 | Type = 'Ide' |
| 99 | Launcher = $_.FullName |
| 100 | Label = ("{0} ({1})" -f (Get-PrettyLabel $stem), $_.FullName) |
| 101 | Path = $_.FullName |
| 102 | Client = $null |
| 103 | }) |
| 104 | } |
| 105 | } |
| 106 | } |
| 107 | |
| 108 | # 2) Toolbox .cmd shims |
| 109 | $tbScripts = Join-Path $env:LOCALAPPDATA 'JetBrains\Toolbox\scripts' |
| 110 | if (Test-Path $tbScripts) { |
| 111 | Get-ChildItem -Path $tbScripts -Filter '*.cmd' -File -ErrorAction SilentlyContinue | ForEach-Object { |
| 112 | $stem = [System.IO.Path]::GetFileNameWithoutExtension($_.Name) |
| 113 | # Toolbox shims keep names like "idea.cmd", "rider.cmd" — strip trailing 64 etc. |
| 114 | $Candidates.Add([pscustomobject]@{ |
| 115 | Type = 'WinCmd' |
| 116 | Launcher = $_.FullName |
| 117 | Label = ("{0} ({1})" -f (Get-PrettyLabel $stem), $_.FullName) |
| 118 | Path = $_.FullName |
| 119 | Client = $null |
| 120 | }) |
| 121 | } |
| 122 | } |
| 123 | |
| 124 | # 3) JetBrains Gateway Client — pick the most-recently-modified one if any. |
| 125 | $jbRoot = Join-Path $env:LOCALAPPDATA 'JetBrains' |
| 126 | if (Test-Path $jbRoot) { |
| 127 | $client = Get-ChildItem -Path $jbRoot -Directory -Filter 'JetBrainsClient*' -ErrorAction SilentlyContinue | |
| 128 | Sort-Object LastWriteTime -Descending | Select-Object -First 1 |
| 129 | if ($client) { |
| 130 | $Candidates.Add([pscustomobject]@{ |
| 131 | Type = 'Gateway' |
| 132 | Launcher = $client.FullName |
| 133 | Label = ("JetBrains Gateway Client ({0})" -f $client.FullName) |
| 134 | Path = $client.FullName |
| 135 | Client = $client.FullName |
| 136 | }) |
| 137 | } |
| 138 | } |
| 139 | |
| 140 | if ($Candidates.Count -eq 0) { |
| 141 | Write-Host "" |
| 142 | Write-Host "No JetBrains IDEs or Gateway clients found." -ForegroundColor Red |
| 143 | Write-Host "Install one via the JetBrains Toolbox, then re-run." -ForegroundColor Yellow |
| 144 | exit 1 |
| 145 | } |
| 146 | |
| 147 | # ------------------------------------------------------------------ |
| 148 | # Multi-select picker — prefer Out-GridView when available. |
| 149 | # ------------------------------------------------------------------ |
| 150 | function Select-Multi { |
| 151 | param( |
| 152 | [Parameter(Mandatory)] [string]$Title, |
| 153 | [Parameter(Mandatory)] [array]$Items, |
| 154 | [Parameter(Mandatory)] [string]$Property |
| 155 | ) |
| 156 | |
| 157 | $hasOgv = $false |
| 158 | try { |
| 159 | $cmd = Get-Command Out-GridView -ErrorAction Stop |
| 160 | if ($cmd) { $hasOgv = $true } |
| 161 | } catch { $hasOgv = $false } |
| 162 | |
| 163 | if ($hasOgv) { |
| 164 | $chosen = $Items | Out-GridView -Title $Title -OutputMode Multiple |
| 165 | return ,@($chosen) |
| 166 | } |
| 167 | |
| 168 | # Numbered-prompt fallback. |
| 169 | Write-Host "" |
| 170 | Write-Host $Title -ForegroundColor Cyan |
| 171 | for ($i = 0; $i -lt $Items.Count; $i++) { |
| 172 | Write-Host (" {0,2}) {1}" -f ($i + 1), $Items[$i].$Property) |
| 173 | } |
| 174 | while ($true) { |
| 175 | $raw = Read-Host "Select (e.g. 1,3,5 or a for all)" |
| 176 | if ([string]::IsNullOrWhiteSpace($raw)) { |
| 177 | Write-Host " please enter at least one number, or 'a'." -ForegroundColor Yellow |
| 178 | continue |
| 179 | } |
| 180 | if ($raw.Trim().ToLower() -eq 'a') { |
| 181 | return ,@($Items) |
| 182 | } |
| 183 | $idxs = $raw -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne '' } |
| 184 | $bad = $false |
| 185 | $sel = @() |
| 186 | foreach ($t in $idxs) { |
| 187 | if ($t -notmatch '^[0-9]+$') { $bad = $true; break } |
| 188 | $n = [int]$t |
| 189 | if ($n -lt 1 -or $n -gt $Items.Count) { $bad = $true; break } |
| 190 | $sel += $Items[$n - 1] |
| 191 | } |
| 192 | if ($bad -or $sel.Count -eq 0) { |
| 193 | Write-Host " invalid input; try again." -ForegroundColor Yellow |
| 194 | continue |
| 195 | } |
| 196 | return ,@($sel) |
| 197 | } |
| 198 | } |
| 199 | |
| 200 | Write-Host ("Detected {0} JetBrains target(s)." -f $Candidates.Count) -ForegroundColor Green |
| 201 | |
| 202 | $selectedIdes = Select-Multi -Title "Pick IDE(s)" -Items $Candidates -Property 'Label' |
| 203 | if ($selectedIdes.Count -eq 0) { |
| 204 | Write-Host "No IDEs selected; exiting." -ForegroundColor Red |
| 205 | exit 1 |
| 206 | } |
| 207 | |
| 208 | $selectedPlugins = Select-Multi -Title "Pick plugin(s)" -Items $PluginCatalog -Property 'Label' |
| 209 | if ($selectedPlugins.Count -eq 0) { |
| 210 | Write-Host "No plugins selected; exiting." -ForegroundColor Red |
| 211 | exit 1 |
| 212 | } |
| 213 | |
| 214 | # ------------------------------------------------------------------ |
| 215 | # Running-IDE preflight |
| 216 | # installPlugins refuses to run while the IDE GUI holds the config-dir lock |
| 217 | # ("Only one instance of IDEA can be run at a time."). Best-effort detection |
| 218 | # via Get-Process. Gateway Client type doesn't need this (no CLI invoked). |
| 219 | # ------------------------------------------------------------------ |
| 220 | function Test-IdeRunning { |
| 221 | param([Parameter(Mandatory)] [psobject]$Ide) |
| 222 | if ($Ide.Type -eq 'Gateway') { return $false } |
| 223 | |
| 224 | # For standalone IDE launchers the process name is the exe basename. |
| 225 | # For Toolbox .cmd shims, the spawned process is e.g. idea64 -> use prefix. |
| 226 | $stem = [System.IO.Path]::GetFileNameWithoutExtension($Ide.Launcher) |
| 227 | $patterns = @($stem) |
| 228 | if ($stem -notmatch '64$') { $patterns += ($stem + '64') } |
| 229 | |
| 230 | foreach ($pat in $patterns) { |
| 231 | if (Get-Process -Name $pat -ErrorAction SilentlyContinue) { return $true } |
| 232 | } |
| 233 | return $false |
| 234 | } |
| 235 | |
| 236 | while ($true) { |
| 237 | $running = @($selectedIdes | Where-Object { Test-IdeRunning $_ }) |
| 238 | if ($running.Count -eq 0) { break } |
| 239 | |
| 240 | Write-Host "" |
| 241 | Write-Host "The following IDE(s) appear to be running — installPlugins needs them closed:" -ForegroundColor Yellow |
| 242 | foreach ($r in $running) { Write-Host (" - {0}" -f $r.Label) -ForegroundColor Yellow } |
| 243 | Write-Host "" |
| 244 | Read-Host "Close them, then press Enter to retry (Ctrl+C to abort)" | Out-Null |
| 245 | } |
| 246 | |
| 247 | # ------------------------------------------------------------------ |
| 248 | # Install loop |
| 249 | # ------------------------------------------------------------------ |
| 250 | $installed = 0 |
| 251 | $failed = 0 |
| 252 | $failedList = @() |
| 253 | |
| 254 | Write-Host "" |
| 255 | Write-Host ("Installing {0} plugin(s) into {1} IDE(s)..." -f $selectedPlugins.Count, $selectedIdes.Count) -ForegroundColor Cyan |
| 256 | Write-Host "" |
| 257 | |
| 258 | foreach ($ide in $selectedIdes) { |
| 259 | Write-Host (">>> {0}" -f $ide.Label) |
| 260 | |
| 261 | switch ($ide.Type) { |
| 262 | |
| 263 | # ---- standard IDE launcher ---- |
| 264 | 'Ide' { |
| 265 | $args = @('installPlugins') + ($selectedPlugins | ForEach-Object { $_.Id }) |
| 266 | try { |
| 267 | $output = & $ide.Launcher @args 2>&1 |
| 268 | if ($LASTEXITCODE -eq 0) { |
| 269 | Write-Host (" all {0} plugin(s) ok" -f $selectedPlugins.Count) -ForegroundColor Green |
| 270 | $installed += $selectedPlugins.Count |
| 271 | } else { |
| 272 | $joined = ($output | Out-String) |
| 273 | if ($joined -match 'Only one instance') { |
| 274 | Write-Host " IDE is running SKIPPED" -ForegroundColor Yellow |
| 275 | Write-Host " Close the IDE and re-run the script to finish." -ForegroundColor DarkYellow |
| 276 | } else { |
| 277 | Write-Host (" installPlugins exited {0} FAILED" -f $LASTEXITCODE) -ForegroundColor Red |
| 278 | $output | ForEach-Object { Write-Host " $_" -ForegroundColor DarkRed } |
| 279 | } |
| 280 | $failed += $selectedPlugins.Count |
| 281 | foreach ($p in $selectedPlugins) { $failedList += ("{0} :: {1}" -f $ide.Label, $p.Id) } |
| 282 | } |
| 283 | } catch { |
| 284 | Write-Host (" launcher invocation threw FAILED") -ForegroundColor Red |
| 285 | $failed += $selectedPlugins.Count |
| 286 | foreach ($p in $selectedPlugins) { $failedList += ("{0} :: {1}" -f $ide.Label, $p.Id) } |
| 287 | Write-Host (" {0}" -f $_.Exception.Message) -ForegroundColor DarkRed |
| 288 | } |
| 289 | } |
| 290 | |
| 291 | # ---- Toolbox .cmd shim ---- |
| 292 | 'WinCmd' { |
| 293 | $idArgs = ($selectedPlugins | ForEach-Object { '"{0}"' -f $_.Id }) -join ' ' |
| 294 | try { |
| 295 | $output = cmd.exe /c "`"$($ide.Launcher)`" installPlugins $idArgs" 2>&1 |
| 296 | if ($LASTEXITCODE -eq 0) { |
| 297 | Write-Host (" all {0} plugin(s) ok" -f $selectedPlugins.Count) -ForegroundColor Green |
| 298 | $installed += $selectedPlugins.Count |
| 299 | } else { |
| 300 | $joined = ($output | Out-String) |
| 301 | if ($joined -match 'Only one instance') { |
| 302 | Write-Host " IDE is running SKIPPED" -ForegroundColor Yellow |
| 303 | Write-Host " Close the IDE and re-run the script to finish." -ForegroundColor DarkYellow |
| 304 | } else { |
| 305 | Write-Host (" installPlugins exited {0} FAILED" -f $LASTEXITCODE) -ForegroundColor Red |
| 306 | $output | ForEach-Object { Write-Host " $_" -ForegroundColor DarkRed } |
| 307 | } |
| 308 | $failed += $selectedPlugins.Count |
| 309 | foreach ($p in $selectedPlugins) { $failedList += ("{0} :: {1}" -f $ide.Label, $p.Id) } |
| 310 | } |
| 311 | } catch { |
| 312 | Write-Host (" cmd.exe invocation threw FAILED") -ForegroundColor Red |
| 313 | $failed += $selectedPlugins.Count |
| 314 | foreach ($p in $selectedPlugins) { $failedList += ("{0} :: {1}" -f $ide.Label, $p.Id) } |
| 315 | Write-Host (" {0}" -f $_.Exception.Message) -ForegroundColor DarkRed |
| 316 | } |
| 317 | } |
| 318 | |
| 319 | # ---- Gateway Client ---- |
| 320 | 'Gateway' { |
| 321 | $client = $ide.Client |
| 322 | $buildFile = Join-Path $client 'build.txt' |
| 323 | if (-not (Test-Path $buildFile)) { |
| 324 | Write-Host " build.txt missing under client; cannot determine build id SKIPPED" -ForegroundColor Yellow |
| 325 | Write-Host " (open a Remote Project once to populate the client folder)" -ForegroundColor DarkYellow |
| 326 | $failed += $selectedPlugins.Count |
| 327 | foreach ($p in $selectedPlugins) { $failedList += ("{0} :: {1}" -f $ide.Label, $p.Id) } |
| 328 | continue |
| 329 | } |
| 330 | $buildId = (Get-Content $buildFile -Raw -ErrorAction Stop).Trim() |
| 331 | |
| 332 | $pluginDir = Join-Path $client 'plugins' |
| 333 | if (-not (Test-Path $pluginDir)) { |
| 334 | New-Item -ItemType Directory -Path $pluginDir | Out-Null |
| 335 | } |
| 336 | |
| 337 | $ua = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" |
| 338 | |
| 339 | foreach ($p in $selectedPlugins) { |
| 340 | Write-Host -NoNewline (" {0,-34} " -f $p.Id) |
| 341 | $encoded = [uri]::EscapeDataString($p.Id) |
| 342 | $url = "https://plugins.jetbrains.com/pluginManager/?action=download&id=$encoded&build=$buildId" |
| 343 | $tmp = Join-Path $env:TEMP ("jb-" + [guid]::NewGuid().ToString() + ".bin") |
| 344 | try { |
| 345 | Invoke-WebRequest -Uri $url -OutFile $tmp -UserAgent $ua -ErrorAction Stop -MaximumRedirection 5 |
| 346 | $bytes = [System.IO.File]::ReadAllBytes($tmp) | Select-Object -First 2 |
| 347 | if ($bytes.Count -ge 2 -and $bytes[0] -eq 0x50 -and $bytes[1] -eq 0x4B) { |
| 348 | Expand-Archive -Path $tmp -DestinationPath $pluginDir -Force -ErrorAction Stop |
| 349 | Write-Host "ok (zip)" -ForegroundColor Green |
| 350 | } else { |
| 351 | $jarName = Join-Path $pluginDir ($p.Id -replace '[^A-Za-z0-9._-]', '_') + '.jar' |
| 352 | Move-Item -Path $tmp -Destination $jarName -Force |
| 353 | Write-Host "ok (jar)" -ForegroundColor Green |
| 354 | } |
| 355 | $installed++ |
| 356 | } catch { |
| 357 | Write-Host "FAILED" -ForegroundColor Red |
| 358 | Write-Host (" {0}" -f $_.Exception.Message) -ForegroundColor DarkRed |
| 359 | $failed++ |
| 360 | $failedList += ("{0} :: {1}" -f $ide.Label, $p.Id) |
| 361 | } finally { |
| 362 | if (Test-Path $tmp) { Remove-Item $tmp -Force -ErrorAction SilentlyContinue } |
| 363 | } |
| 364 | } |
| 365 | Write-Host " Reopen the Gateway Client to load the new plugins." -ForegroundColor Yellow |
| 366 | } |
| 367 | } |
| 368 | Write-Host "" |
| 369 | } |
| 370 | |
| 371 | Write-Host ("Done. Installed: {0}, Failed: {1}" -f $installed, $failed) |
| 372 | if ($failed -gt 0) { |
| 373 | Write-Host "Failed:" -ForegroundColor Red |
| 374 | foreach ($f in $failedList) { Write-Host (" - {0}" -f $f) -ForegroundColor Red } |
| 375 | exit 1 |
| 376 | } |
| 377 |
| 1 | #!/usr/bin/env bash |
| 2 | # |
| 3 | # JetBrains IDE plugin installer for macOS, Linux (Ubuntu Desktop) and WSL2. |
| 4 | # |
| 5 | # Detects every locally installed JetBrains IDE (Toolbox + standalone) and, |
| 6 | # on Linux, any JetBrains Gateway remote-dev-server.sh distributions. Lets |
| 7 | # you multi-select IDE(s) and plugin(s) interactively, then installs each |
| 8 | # plugin into each selected IDE. |
| 9 | # |
| 10 | # Picker UX: |
| 11 | # - Uses fzf with --multi when fzf is on PATH (TAB to toggle, Enter to confirm). |
| 12 | # - Otherwise prints a numbered list and accepts space-separated indexes |
| 13 | # (e.g. "1 3 5"), exclusions (e.g. "!1 !4" to select all except 1 and 4), |
| 14 | # or "a" for all. |
| 15 | # |
| 16 | # Usage: |
| 17 | # ./install-jetbrains.sh # interactive |
| 18 | # ./install-jetbrains.sh --help |
| 19 | |
| 20 | set -euo pipefail |
| 21 | IFS=$'\n\t' |
| 22 | |
| 23 | # ------------------------------------------------------------------ |
| 24 | # Canonical marketplace plugin list (xmlId | display label). |
| 25 | # Both columns separated by a literal pipe; the label is for the picker. |
| 26 | # ------------------------------------------------------------------ |
| 27 | readonly PLUGIN_CATALOG=( |
| 28 | "com.intellij.ml.llm|JetBrains AI Assistant" |
| 29 | "izhangzhihao.rainbow.brackets|Rainbow Brackets" |
| 30 | "IdeaVIM|IdeaVim" |
| 31 | "org.sonarlint.idea|SonarQube for IDE (SonarLint)" |
| 32 | "Key Promoter X|Key Promoter X" |
| 33 | "net.ashald.envfile|EnvFile" |
| 34 | "org.intellij.qodana|Qodana" |
| 35 | ) |
| 36 | |
| 37 | # Known JetBrains IDE launcher basenames (Toolbox shims + Linux .sh names). |
| 38 | readonly KNOWN_IDE_NAMES=( |
| 39 | idea idea.sh |
| 40 | pycharm pycharm.sh |
| 41 | webstorm webstorm.sh |
| 42 | goland goland.sh |
| 43 | rubymine rubymine.sh |
| 44 | clion clion.sh |
| 45 | phpstorm phpstorm.sh |
| 46 | datagrip datagrip.sh |
| 47 | rustrover rustrover.sh |
| 48 | rider rider.sh |
| 49 | studio studio.sh |
| 50 | fleet |
| 51 | ) |
| 52 | |
| 53 | usage() { |
| 54 | cat <<EOF |
| 55 | Usage: $(basename "$0") |
| 56 | |
| 57 | Interactive JetBrains IDE plugin installer for macOS, Linux and WSL2. |
| 58 | |
| 59 | The script: |
| 60 | 1. Detects your OS (macOS / Linux / WSL2) |
| 61 | 2. Scans for installed JetBrains IDEs (Toolbox + standalone) and any |
| 62 | local remote-dev-server.sh distributions |
| 63 | 3. Lets you multi-select IDE(s) and plugin(s) |
| 64 | 4. Runs each launcher's "installPlugins" command for every selected plugin |
| 65 | |
| 66 | Use \`brew install fzf\` (or your distro's package manager) for a nicer picker. |
| 67 | EOF |
| 68 | } |
| 69 | |
| 70 | while [[ $# -gt 0 ]]; do |
| 71 | case "$1" in |
| 72 | -h|--help) usage; exit 0 ;; |
| 73 | *) echo "Error: unknown argument '$1'." >&2; usage >&2; exit 2 ;; |
| 74 | esac |
| 75 | done |
| 76 | |
| 77 | # ------------------------------------------------------------------ |
| 78 | # OS detection |
| 79 | # ------------------------------------------------------------------ |
| 80 | OS_KIND="" |
| 81 | case "$(uname -s)" in |
| 82 | Darwin) OS_KIND=macos ;; |
| 83 | Linux) |
| 84 | if grep -qi microsoft /proc/version 2>/dev/null; then |
| 85 | OS_KIND=wsl2 |
| 86 | else |
| 87 | OS_KIND=linux |
| 88 | fi |
| 89 | ;; |
| 90 | *) |
| 91 | echo "Error: unsupported OS '$(uname -s)'. Use install-jetbrains.ps1 on Windows." >&2 |
| 92 | exit 1 |
| 93 | ;; |
| 94 | esac |
| 95 | |
| 96 | # ------------------------------------------------------------------ |
| 97 | # Helpers |
| 98 | # ------------------------------------------------------------------ |
| 99 | |
| 100 | # is_known_ide_name <basename> -> 0 if it matches a known JetBrains launcher. |
| 101 | is_known_ide_name() { |
| 102 | local n="$1" |
| 103 | local k |
| 104 | for k in "${KNOWN_IDE_NAMES[@]}"; do |
| 105 | [[ "$n" == "$k" ]] && return 0 |
| 106 | done |
| 107 | return 1 |
| 108 | } |
| 109 | |
| 110 | # pretty_from_path /path/to/idea.sh -> "IntelliJ IDEA (~/.local/share/.../idea.sh)" |
| 111 | # Builds a short, readable label for the picker. |
| 112 | pretty_label() { |
| 113 | local path="$1" base |
| 114 | base="$(basename "$path")" |
| 115 | base="${base%.sh}" |
| 116 | base="${base%.cmd}" |
| 117 | base="${base%.exe}" |
| 118 | case "$base" in |
| 119 | idea|idea64) printf 'IntelliJ IDEA' ;; |
| 120 | pycharm|pycharm64) printf 'PyCharm' ;; |
| 121 | webstorm|webstorm64) printf 'WebStorm' ;; |
| 122 | goland|goland64) printf 'GoLand' ;; |
| 123 | rubymine|rubymine64) printf 'RubyMine' ;; |
| 124 | clion|clion64) printf 'CLion' ;; |
| 125 | phpstorm|phpstorm64) printf 'PhpStorm' ;; |
| 126 | datagrip|datagrip64) printf 'DataGrip' ;; |
| 127 | rustrover|rustrover64) printf 'RustRover' ;; |
| 128 | rider|rider64) printf 'Rider' ;; |
| 129 | studio|studio64) printf 'Android Studio' ;; |
| 130 | fleet) printf 'Fleet' ;; |
| 131 | *) printf '%s' "$base" ;; |
| 132 | esac |
| 133 | printf ' (%s)' "$path" |
| 134 | } |
| 135 | |
| 136 | # Translate a Linux/WSL home into the Windows-side user dir under /mnt/c. |
| 137 | win_user_dir() { |
| 138 | local winuser |
| 139 | winuser="$(cmd.exe /c 'echo %USERNAME%' 2>/dev/null | tr -d '\r\n' || true)" |
| 140 | [[ -z "$winuser" ]] && winuser="$USER" |
| 141 | printf '/mnt/c/Users/%s' "$winuser" |
| 142 | } |
| 143 | |
| 144 | # ------------------------------------------------------------------ |
| 145 | # IDE candidate scan |
| 146 | # Each candidate is recorded as a single line: |
| 147 | # <type>|<launcher>|<label> |
| 148 | # where <type> is one of: |
| 149 | # ide -> run "<launcher> installPlugins <id>..." |
| 150 | # wincmd -> run "cmd.exe /c <launcher> installPlugins <id>..." |
| 151 | # remotedev -> remote-dev-server.sh installPlugins ... |
| 152 | # ------------------------------------------------------------------ |
| 153 | declare -a CANDIDATES=() |
| 154 | |
| 155 | add_candidate() { |
| 156 | local type="$1" launcher="$2" |
| 157 | CANDIDATES+=("${type}|${launcher}|$(pretty_label "$launcher")") |
| 158 | } |
| 159 | |
| 160 | scan_macos() { |
| 161 | local d="$HOME/Library/Application Support/JetBrains/Toolbox/scripts" |
| 162 | if [[ -d "$d" ]]; then |
| 163 | local f |
| 164 | while IFS= read -r -d '' f; do |
| 165 | local base |
| 166 | base="$(basename "$f")" |
| 167 | is_known_ide_name "$base" || continue |
| 168 | [[ -x "$f" ]] && add_candidate ide "$f" |
| 169 | done < <(find "$d" -maxdepth 1 -type f -print0 2>/dev/null) |
| 170 | fi |
| 171 | |
| 172 | local app bin |
| 173 | for app in /Applications/*.app; do |
| 174 | [[ -d "$app/Contents/MacOS" ]] || continue |
| 175 | [[ "$(basename "$app")" =~ ^(IntelliJ|PyCharm|WebStorm|GoLand|RubyMine|CLion|PhpStorm|DataGrip|RustRover|Rider|Android\ Studio|Fleet) ]] || continue |
| 176 | for bin in "$app"/Contents/MacOS/*; do |
| 177 | [[ -x "$bin" ]] || continue |
| 178 | local b |
| 179 | b="$(basename "$bin")" |
| 180 | is_known_ide_name "$b" || continue |
| 181 | add_candidate ide "$bin" |
| 182 | done |
| 183 | done |
| 184 | } |
| 185 | |
| 186 | scan_linux() { |
| 187 | local apps="$HOME/.local/share/JetBrains/Toolbox/apps" |
| 188 | if [[ -d "$apps" ]]; then |
| 189 | local f |
| 190 | while IFS= read -r -d '' f; do |
| 191 | local b |
| 192 | b="$(basename "$f")" |
| 193 | is_known_ide_name "$b" || continue |
| 194 | [[ -x "$f" ]] && add_candidate ide "$f" |
| 195 | done < <(find "$apps" -maxdepth 3 -type f -name '*.sh' -print0 2>/dev/null) |
| 196 | fi |
| 197 | |
| 198 | # /opt/<ide>/bin/<ide>.sh — common for .deb / snap / tarball installs. |
| 199 | local opt |
| 200 | for opt in /opt/*/bin/*.sh; do |
| 201 | [[ -x "$opt" ]] || continue |
| 202 | local b |
| 203 | b="$(basename "$opt")" |
| 204 | is_known_ide_name "$b" || continue |
| 205 | add_candidate ide "$opt" |
| 206 | done |
| 207 | |
| 208 | # Remote-dev-server distributions (Orbstack/Gateway). |
| 209 | local rd |
| 210 | while IFS= read -r -d '' rd; do |
| 211 | add_candidate remotedev "$rd" |
| 212 | done < <(find "$HOME/.cache/JetBrains/RemoteDev/dist" -maxdepth 3 -type f -name 'remote-dev-server.sh' -print0 2>/dev/null) |
| 213 | } |
| 214 | |
| 215 | scan_wsl2() { |
| 216 | local winhome |
| 217 | winhome="$(win_user_dir)" |
| 218 | [[ -d "$winhome" ]] || return 0 |
| 219 | |
| 220 | # Standalone installs under AppData\Local\Programs\<IDE>\bin\*.exe |
| 221 | local exe b |
| 222 | while IFS= read -r -d '' exe; do |
| 223 | b="$(basename "$exe")" |
| 224 | case "$b" in |
| 225 | idea64.exe|pycharm64.exe|webstorm64.exe|goland64.exe|rubymine64.exe|\ |
| 226 | clion64.exe|phpstorm64.exe|datagrip64.exe|rustrover64.exe|rider64.exe|\ |
| 227 | studio64.exe|fleet.exe) |
| 228 | add_candidate ide "$exe" |
| 229 | ;; |
| 230 | esac |
| 231 | done < <(find "$winhome/AppData/Local/Programs" -maxdepth 4 -type f -name '*.exe' -print0 2>/dev/null) |
| 232 | |
| 233 | # Toolbox shell shims (.cmd) — invoke via cmd.exe. |
| 234 | local scripts="$winhome/AppData/Local/JetBrains/Toolbox/scripts" |
| 235 | if [[ -d "$scripts" ]]; then |
| 236 | local f |
| 237 | while IFS= read -r -d '' f; do |
| 238 | b="$(basename "${f%.cmd}")" |
| 239 | is_known_ide_name "$b" || continue |
| 240 | add_candidate wincmd "$f" |
| 241 | done < <(find "$scripts" -maxdepth 1 -type f -name '*.cmd' -print0 2>/dev/null) |
| 242 | fi |
| 243 | |
| 244 | # Local Linux-side remote-dev-server.sh (still useful inside WSL2). |
| 245 | local rd |
| 246 | while IFS= read -r -d '' rd; do |
| 247 | add_candidate remotedev "$rd" |
| 248 | done < <(find "$HOME/.cache/JetBrains/RemoteDev/dist" -maxdepth 3 -type f -name 'remote-dev-server.sh' -print0 2>/dev/null) |
| 249 | } |
| 250 | |
| 251 | case "$OS_KIND" in |
| 252 | macos) scan_macos ;; |
| 253 | linux) scan_linux ;; |
| 254 | wsl2) scan_wsl2 ;; |
| 255 | esac |
| 256 | |
| 257 | if [[ ${#CANDIDATES[@]} -eq 0 ]]; then |
| 258 | echo "No JetBrains IDEs detected on this system." >&2 |
| 259 | echo "Install one via the JetBrains Toolbox, then re-run." >&2 |
| 260 | exit 1 |
| 261 | fi |
| 262 | |
| 263 | # ------------------------------------------------------------------ |
| 264 | # Multi-select picker (Handles Space separation & Exclusions via !) |
| 265 | # Reads newline-separated options on stdin, prints selected lines on stdout. |
| 266 | # ------------------------------------------------------------------ |
| 267 | pick_multi() { |
| 268 | local prompt="$1" header="$2" |
| 269 | if command -v fzf >/dev/null 2>&1; then |
| 270 | fzf --multi --reverse --prompt="$prompt " --header="$header" |
| 271 | return |
| 272 | fi |
| 273 | |
| 274 | # Fallback: numbered list + space-separated input with exclusion support. |
| 275 | local -a items=() |
| 276 | local line |
| 277 | while IFS= read -r line; do items+=("$line"); done |
| 278 | |
| 279 | local total=${#items[@]} i |
| 280 | while :; do |
| 281 | { |
| 282 | echo "$header" |
| 283 | for (( i=0; i<total; i++ )); do |
| 284 | printf ' %2d) %s\n' "$((i+1))" "${items[i]}" |
| 285 | done |
| 286 | printf '%s' "$prompt (e.g. 1 3, !2 to exclude 2, or a for all): " |
| 287 | } >&2 |
| 288 | |
| 289 | local input |
| 290 | if ! IFS= read -r input </dev/tty; then |
| 291 | echo "no tty available; install fzf or run interactively" >&2 |
| 292 | return 1 |
| 293 | fi |
| 294 | |
| 295 | if [[ "$input" == "a" || "$input" == "A" ]]; then |
| 296 | printf '%s\n' "${items[@]}" |
| 297 | return |
| 298 | fi |
| 299 | [[ -z "$input" ]] && { echo " please enter at least one number, an exclusion, or 'a'." >&2; continue; } |
| 300 | |
| 301 | local -a toks=() |
| 302 | IFS=' ' read -r -a toks <<<"$input" |
| 303 | |
| 304 | # Determine if this is an exclusion request (checks if any token starts with !) |
| 305 | local is_exclusion=0 |
| 306 | local t |
| 307 | for t in "${toks[@]}"; do |
| 308 | if [[ "$t" =~ ^\! ]]; then |
| 309 | is_exclusion=1 |
| 310 | break |
| 311 | fi |
| 312 | done |
| 313 | |
| 314 | local -a picked=() |
| 315 | local bad=0 |
| 316 | |
| 317 | if (( is_exclusion )); then |
| 318 | # Track which indices are explicitly excluded using an associative array map |
| 319 | local -A excluded_map=() |
| 320 | |
| 321 | for t in "${toks[@]}"; do |
| 322 | [[ -z "$t" ]] && continue |
| 323 | if [[ ! "$t" =~ ^\![0-9]+$ ]]; then |
| 324 | echo " Error: When using exclusions, all items must start with '!' (e.g., !1 !3)" >&2 |
| 325 | bad=1 |
| 326 | break |
| 327 | fi |
| 328 | |
| 329 | local idx="${t#\!}" # Strip the '!' character |
| 330 | if (( idx < 1 || idx > total )); then bad=1; break; fi |
| 331 | excluded_map[$((idx-1))]=1 |
| 332 | done |
| 333 | |
| 334 | if (( bad )); then |
| 335 | echo " invalid exclusion input; try again." >&2 |
| 336 | continue |
| 337 | fi |
| 338 | |
| 339 | # Build final selection list by adding everything NOT in our exclusion map |
| 340 | for (( i=0; i<total; i++ )); do |
| 341 | if [[ -z "${excluded_map[$i]+abc}" ]]; then |
| 342 | picked+=("${items[i]}") |
| 343 | fi |
| 344 | done |
| 345 | else |
| 346 | # Standard additive parsing (space-separated) |
| 347 | for t in "${toks[@]}"; do |
| 348 | t="${t// /}" |
| 349 | [[ -z "$t" ]] && continue |
| 350 | if [[ ! "$t" =~ ^[0-9]+$ ]]; then bad=1; break; fi |
| 351 | if (( t < 1 || t > total )); then bad=1; break; fi |
| 352 | picked+=("${items[t-1]}") |
| 353 | done |
| 354 | fi |
| 355 | |
| 356 | if (( bad )); then |
| 357 | echo " invalid input; try again." >&2 |
| 358 | continue |
| 359 | fi |
| 360 | if (( ${#picked[@]} == 0 )); then |
| 361 | echo " no valid selections; try again." >&2 |
| 362 | continue |
| 363 | fi |
| 364 | printf '%s\n' "${picked[@]}" |
| 365 | return |
| 366 | done |
| 367 | } |
| 368 | |
| 369 | # ------------------------------------------------------------------ |
| 370 | # IDE picker |
| 371 | # ------------------------------------------------------------------ |
| 372 | declare -a ide_lines=() |
| 373 | for c in "${CANDIDATES[@]}"; do |
| 374 | # human-readable picker line: just the third field |
| 375 | ide_lines+=("${c#*|*|}") |
| 376 | done |
| 377 | |
| 378 | # Map label -> (type, launcher) so we can recover after picking. |
| 379 | declare -A label_type label_launcher |
| 380 | for c in "${CANDIDATES[@]}"; do |
| 381 | IFS='|' read -r t l lab <<<"$c" |
| 382 | label_type[$lab]="$t" |
| 383 | label_launcher[$lab]="$l" |
| 384 | done |
| 385 | |
| 386 | echo "Detected ${#CANDIDATES[@]} JetBrains target(s)." |
| 387 | mapfile -t SELECTED_IDES < <( |
| 388 | printf '%s\n' "${ide_lines[@]}" | |
| 389 | pick_multi "IDEs>" "Pick IDE(s) — TAB to toggle (fzf) or spaced indexes" |
| 390 | ) |
| 391 | |
| 392 | if (( ${#SELECTED_IDES[@]} == 0 )); then |
| 393 | echo "No IDEs selected; exiting." >&2 |
| 394 | exit 1 |
| 395 | fi |
| 396 | |
| 397 | # ------------------------------------------------------------------ |
| 398 | # Plugin picker |
| 399 | # ------------------------------------------------------------------ |
| 400 | declare -a plugin_lines=() |
| 401 | declare -A label_to_id |
| 402 | for entry in "${PLUGIN_CATALOG[@]}"; do |
| 403 | IFS='|' read -r pid plabel <<<"$entry" |
| 404 | line="$(printf '%-32s %s' "$plabel" "$pid")" |
| 405 | plugin_lines+=("$line") |
| 406 | label_to_id[$line]="$pid" |
| 407 | done |
| 408 | |
| 409 | mapfile -t SELECTED_PLUGIN_LINES < <( |
| 410 | printf '%s\n' "${plugin_lines[@]}" | |
| 411 | pick_multi "Plugins>" "Pick plugin(s) — TAB to toggle (fzf) or spaced indexes" |
| 412 | ) |
| 413 | |
| 414 | if (( ${#SELECTED_PLUGIN_LINES[@]} == 0 )); then |
| 415 | echo "No plugins selected; exiting." >&2 |
| 416 | exit 1 |
| 417 | fi |
| 418 | |
| 419 | declare -a SELECTED_PLUGIN_IDS=() |
| 420 | for line in "${SELECTED_PLUGIN_LINES[@]}"; do |
| 421 | SELECTED_PLUGIN_IDS+=("${label_to_id[$line]}") |
| 422 | done |
| 423 | |
| 424 | # ------------------------------------------------------------------ |
| 425 | # Running-IDE preflight |
| 426 | # JetBrains' `installPlugins` refuses to run while the IDE GUI is open |
| 427 | # ("Only one instance of IDEA can be run at a time."). We do a best-effort |
| 428 | # pgrep against the IDE app directory so the user can close them upfront |
| 429 | # instead of seeing N identical errors during the install loop. |
| 430 | # ------------------------------------------------------------------ |
| 431 | ide_is_running() { |
| 432 | local launcher="$1" type="$2" |
| 433 | |
| 434 | # remote-dev-server.sh spawns short-lived workers; WSL2->Windows lookups |
| 435 | # would need tasklist.exe — skip in both cases. |
| 436 | case "$type" in |
| 437 | remotedev|wincmd) return 1 ;; |
| 438 | esac |
| 439 | |
| 440 | case "$launcher" in |
| 441 | */Toolbox/apps/*) |
| 442 | # /.../Toolbox/apps/<ide-dir>/[ch-N/<ver>/]bin/idea.sh -> match "<ide-dir>" |
| 443 | local rest="${launcher#*/Toolbox/apps/}" |
| 444 | local ide_dir="${rest%%/*}" |
| 445 | [[ -n "$ide_dir" ]] && pgrep -f -- "/Toolbox/apps/${ide_dir}/" >/dev/null 2>&1 |
| 446 | ;; |
| 447 | */Applications/*.app/*) |
| 448 | local app_path="${launcher%/Contents/MacOS/*}" |
| 449 | pgrep -f -- "${app_path}/Contents/" >/dev/null 2>&1 |
| 450 | ;; |
| 451 | *) |
| 452 | local b |
| 453 | b="$(basename "$launcher")" |
| 454 | b="${b%.sh}" |
| 455 | pgrep -fi -- "$b" >/dev/null 2>&1 |
| 456 | ;; |
| 457 | esac |
| 458 | } |
| 459 | |
| 460 | while :; do |
| 461 | declare -a _running=() |
| 462 | for ide_label in "${SELECTED_IDES[@]}"; do |
| 463 | if ide_is_running "${label_launcher[$ide_label]}" "${label_type[$ide_label]}"; then |
| 464 | _running+=("$ide_label") |
| 465 | fi |
| 466 | done |
| 467 | (( ${#_running[@]} == 0 )) && break |
| 468 | |
| 469 | { |
| 470 | echo |
| 471 | echo "The following IDE(s) appear to be running — installPlugins needs them closed:" |
| 472 | for r in "${_running[@]}"; do |
| 473 | echo " - $r" |
| 474 | done |
| 475 | echo |
| 476 | printf 'Close them, then press Enter to retry (or Ctrl+C to abort): ' |
| 477 | } >&2 |
| 478 | if ! IFS= read -r _ </dev/tty; then |
| 479 | echo "no tty; aborting" >&2 |
| 480 | exit 1 |
| 481 | fi |
| 482 | done |
| 483 | |
| 484 | # ------------------------------------------------------------------ |
| 485 | # Install loop |
| 486 | # ------------------------------------------------------------------ |
| 487 | LOG_FILE="$(mktemp -t jb-install.XXXXXX)" |
| 488 | trap 'rm -f "$LOG_FILE"' EXIT |
| 489 | |
| 490 | installed=0 |
| 491 | failed=0 |
| 492 | declare -a FAILED_LIST=() |
| 493 | |
| 494 | echo |
| 495 | echo "Installing ${#SELECTED_PLUGIN_IDS[@]} plugin(s) into ${#SELECTED_IDES[@]} IDE(s)..." |
| 496 | echo |
| 497 | |
| 498 | handle_single_instance_abort() { |
| 499 | local ide_label="$1" current_pid="$2" |
| 500 | shift 2 |
| 501 | echo |
| 502 | echo " ! '${ide_label}' is still running. Skipping remaining plugins for this IDE." >&2 |
| 503 | echo " ! Close the IDE and re-run the script to finish." >&2 |
| 504 | # Mark current and remaining-for-this-IDE plugins as failed. |
| 505 | local mark_from=0 p |
| 506 | for p in "${SELECTED_PLUGIN_IDS[@]}"; do |
| 507 | if (( mark_from )) || [[ "$p" == "$current_pid" ]]; then |
| 508 | mark_from=1 |
| 509 | failed=$((failed + 1)) |
| 510 | FAILED_LIST+=("${ide_label} :: ${p} (IDE running — close it and re-run)") |
| 511 | fi |
| 512 | done |
| 513 | } |
| 514 | |
| 515 | for ide_label in "${SELECTED_IDES[@]}"; do |
| 516 | itype="${label_type[$ide_label]}" |
| 517 | launcher="${label_launcher[$ide_label]}" |
| 518 | |
| 519 | echo ">>> $ide_label" |
| 520 | ide_aborted=0 |
| 521 | for pid in "${SELECTED_PLUGIN_IDS[@]}"; do |
| 522 | (( ide_aborted )) && break |
| 523 | printf ' %-34s ' "$pid" |
| 524 | rc=0 |
| 525 | case "$itype" in |
| 526 | ide|remotedev) |
| 527 | "$launcher" installPlugins "$pid" >"$LOG_FILE" 2>&1 || rc=$? |
| 528 | ;; |
| 529 | wincmd) |
| 530 | cmd.exe /c "$launcher" installPlugins "$pid" >"$LOG_FILE" 2>&1 || rc=$? |
| 531 | ;; |
| 532 | esac |
| 533 | |
| 534 | if (( rc == 0 )); then |
| 535 | echo "ok" |
| 536 | installed=$((installed + 1)) |
| 537 | elif grep -q "Only one instance" "$LOG_FILE" 2>/dev/null; then |
| 538 | echo "SKIPPED" |
| 539 | handle_single_instance_abort "$ide_label" "$pid" |
| 540 | ide_aborted=1 |
| 541 | else |
| 542 | echo "FAILED" |
| 543 | failed=$((failed + 1)) |
| 544 | FAILED_LIST+=("${ide_label} :: ${pid}") |
| 545 | sed 's/^/ /' "$LOG_FILE" >&2 || true |
| 546 | fi |
| 547 | done |
| 548 | echo |
| 549 | done |
| 550 | |
| 551 | echo "Done. Installed: ${installed}, Failed: ${failed}" |
| 552 | |
| 553 | if (( failed > 0 )); then |
| 554 | echo "Failed:" >&2 |
| 555 | for f in "${FAILED_LIST[@]}"; do |
| 556 | echo " - $f" >&2 |
| 557 | done |
| 558 | exit 1 |
| 559 | fi |
| 1 | <# |
| 2 | .SYNOPSIS |
| 3 | Install a curated list of VS Code extensions on Windows. |
| 4 | |
| 5 | .DESCRIPTION |
| 6 | Installs the bundled extension list using the VS Code 'code' CLI. |
| 7 | Sets the current process's ExecutionPolicy to RemoteSigned (no machine-wide |
| 8 | change) so the script can run on a default Windows configuration. |
| 9 | |
| 10 | .PARAMETER ProfileName |
| 11 | Optional VS Code profile name. If supplied, extensions are installed into |
| 12 | that profile. VS Code creates the profile if it doesn't already exist. |
| 13 | Quote names that contain spaces. |
| 14 | |
| 15 | .EXAMPLE |
| 16 | PS> .\install.ps1 |
| 17 | |
| 18 | .EXAMPLE |
| 19 | PS> .\install.ps1 -ProfileName "WorkSetup" |
| 20 | |
| 21 | .NOTES |
| 22 | If PowerShell refuses to run the file, invoke it as: |
| 23 | powershell -ExecutionPolicy Bypass -File .\install.ps1 |
| 24 | #> |
| 25 | |
| 26 | [CmdletBinding()] |
| 27 | param( |
| 28 | [Parameter(Mandatory = $false, Position = 0)] |
| 29 | [string]$ProfileName = "" |
| 30 | ) |
| 31 | |
| 32 | # Loosen execution policy for THIS process only (no persisted change). |
| 33 | try { |
| 34 | $current = Get-ExecutionPolicy -Scope Process -ErrorAction Stop |
| 35 | if ($current -in @('Restricted', 'AllSigned', 'Undefined')) { |
| 36 | Set-ExecutionPolicy -Scope Process -ExecutionPolicy RemoteSigned -Force -ErrorAction Stop |
| 37 | } |
| 38 | } catch { |
| 39 | Write-Verbose ("Could not adjust process execution policy: {0}" -f $_.Exception.Message) |
| 40 | } |
| 41 | |
| 42 | $ErrorActionPreference = 'Stop' |
| 43 | |
| 44 | $Extensions = @( |
| 45 | 'docker.docker', |
| 46 | 'github.github-vscode-theme', |
| 47 | 'GitHub.vscode-github-actions', |
| 48 | 'ms-azuretools.vscode-containers', |
| 49 | 'ms-vscode-remote.vscode-remote-extensionpack', |
| 50 | 'pkief.material-icon-theme' |
| 51 | ) |
| 52 | |
| 53 | $codeCmd = Get-Command code -ErrorAction SilentlyContinue |
| 54 | if (-not $codeCmd) { |
| 55 | Write-Host "" |
| 56 | Write-Host "Error: the 'code' command is not on your PATH." -ForegroundColor Red |
| 57 | Write-Host " Install VS Code from https://code.visualstudio.com/ and make sure" -ForegroundColor Yellow |
| 58 | Write-Host ' "Add to PATH" was checked during setup, then re-open your terminal.' -ForegroundColor Yellow |
| 59 | exit 1 |
| 60 | } |
| 61 | |
| 62 | $extraArgs = @() |
| 63 | if (-not [string]::IsNullOrWhiteSpace($ProfileName)) { |
| 64 | $extraArgs += @('--profile', $ProfileName) |
| 65 | } |
| 66 | |
| 67 | $header = "Installing $($Extensions.Count) extension(s)" |
| 68 | if ($ProfileName) { $header += " into profile '$ProfileName'" } |
| 69 | Write-Host ("{0}..." -f $header) -ForegroundColor Cyan |
| 70 | Write-Host "" |
| 71 | |
| 72 | $installed = 0 |
| 73 | $failed = 0 |
| 74 | $failedList = @() |
| 75 | |
| 76 | foreach ($ext in $Extensions) { |
| 77 | Write-Host -NoNewline (" -> {0,-55} " -f $ext) |
| 78 | try { |
| 79 | $output = & code @extraArgs --install-extension $ext --force 2>&1 |
| 80 | if ($LASTEXITCODE -eq 0) { |
| 81 | Write-Host "ok" -ForegroundColor Green |
| 82 | $installed++ |
| 83 | } else { |
| 84 | Write-Host "FAILED" -ForegroundColor Red |
| 85 | $failed++ |
| 86 | $failedList += $ext |
| 87 | $output | ForEach-Object { Write-Host " $_" -ForegroundColor DarkRed } |
| 88 | } |
| 89 | } catch { |
| 90 | Write-Host "FAILED" -ForegroundColor Red |
| 91 | $failed++ |
| 92 | $failedList += $ext |
| 93 | Write-Host (" {0}" -f $_.Exception.Message) -ForegroundColor DarkRed |
| 94 | } |
| 95 | } |
| 96 | |
| 97 | Write-Host "" |
| 98 | Write-Host ("Done. Installed: {0}, Failed: {1}" -f $installed, $failed) |
| 99 | |
| 100 | if ($failed -gt 0) { |
| 101 | Write-Host "Failed extensions:" -ForegroundColor Red |
| 102 | foreach ($f in $failedList) { |
| 103 | Write-Host (" - {0}" -f $f) -ForegroundColor Red |
| 104 | } |
| 105 | exit 1 |
| 106 | } |
| 107 |
| 1 | #!/usr/bin/env bash |
| 2 | # |
| 3 | # VS Code extension installer for macOS and Linux (Ubuntu). |
| 4 | # |
| 5 | # Usage: |
| 6 | # ./install.sh # looks for .vscode/extensions.json |
| 7 | # ./install.sh -f extensions.json # specify a local JSON file |
| 8 | # ./install.sh -f https://link-to-your/ext.json # specify a remote URL directly |
| 9 | # ./install.sh -p "MyProfile" -f <source> # install into a named profile |
| 10 | |
| 11 | set -euo pipefail |
| 12 | IFS=$'\n\t' |
| 13 | |
| 14 | EXTENSIONS=( |
| 15 | "docker.docker" |
| 16 | "github.github-vscode-theme" |
| 17 | "GitHub.vscode-github-actions" |
| 18 | "ms-azuretools.vscode-containers" |
| 19 | "ms-vscode-remote.vscode-remote-extensionpack" |
| 20 | "pkief.material-icon-theme" |
| 21 | ) |
| 22 | PROFILE="" |
| 23 | SOURCE="" |
| 24 | |
| 25 | usage() { |
| 26 | cat <<EOF |
| 27 | Usage: $(basename "$0") [--profile "ProfileName"] [--file "extensions.json" | "https://..."] |
| 28 | |
| 29 | Installs a curated list of VS Code extensions on macOS or Linux from a local JSON file or URL. |
| 30 | |
| 31 | Options: |
| 32 | -p, --profile <name> Install into the named VS Code profile. |
| 33 | -f, --file <path/url> Path or URL to the JSON file containing recommendations. |
| 34 | Defaults to .vscode/extensions.json if it exists. |
| 35 | -h, --help Show this help and exit. |
| 36 | EOF |
| 37 | } |
| 38 | |
| 39 | while [[ $# -gt 0 ]]; do |
| 40 | case "$1" in |
| 41 | -p|--profile) |
| 42 | if [[ $# -lt 2 || -z "${2:-}" ]]; then |
| 43 | echo "Error: --profile requires a non-empty value." >&2 |
| 44 | exit 2 |
| 45 | fi |
| 46 | PROFILE="$2" |
| 47 | shift 2 |
| 48 | ;; |
| 49 | -f|--file) |
| 50 | if [[ $# -lt 2 || -z "${2:-}" ]]; then |
| 51 | echo "Error: --file requires a non-empty value." >&2 |
| 52 | exit 2 |
| 53 | fi |
| 54 | SOURCE="$2" |
| 55 | shift 2 |
| 56 | ;; |
| 57 | -h|--help) |
| 58 | usage |
| 59 | exit 0 |
| 60 | ;; |
| 61 | *) |
| 62 | echo "Error: unknown argument '$1'." >&2 |
| 63 | usage >&2 |
| 64 | exit 2 |
| 65 | ;; |
| 66 | esac |
| 67 | done |
| 68 | |
| 69 | OS="$(uname -s)" |
| 70 | case "$OS" in |
| 71 | Darwin|Linux) ;; |
| 72 | *) |
| 73 | echo "Error: unsupported OS '$OS'. Use install.ps1 on Windows." >&2 |
| 74 | exit 1 |
| 75 | ;; |
| 76 | esac |
| 77 | |
| 78 | # 1. Check for 'code' command |
| 79 | if ! command -v code >/dev/null 2>&1; then |
| 80 | cat >&2 <<'EOF' |
| 81 | Error: the 'code' command is not on your PATH. |
| 82 | EOF |
| 83 | exit 1 |
| 84 | fi |
| 85 | |
| 86 | # 2. Check for 'jq' command |
| 87 | if ! command -v jq >/dev/null 2>&1; then |
| 88 | echo "Error: 'jq' is not installed. (Ubuntu: sudo apt-get install jq)" >&2 |
| 89 | exit 1 |
| 90 | fi |
| 91 | |
| 92 | # 3. Resolve JSON Source (Local File vs URL) |
| 93 | JSON_FILE="" |
| 94 | TEMP_JSON_FILE="" |
| 95 | USE_HARDCODED=false |
| 96 | |
| 97 | if [[ -z "$SOURCE" ]]; then |
| 98 | if [[ -f ".vscode/extensions.json" ]]; then |
| 99 | JSON_FILE=".vscode/extensions.json" |
| 100 | else |
| 101 | echo "Info: No JSON file specified and .vscode/extensions.json not found. Using hardcoded extensions." >&2 |
| 102 | USE_HARDCODED=true |
| 103 | fi |
| 104 | elif [[ "$SOURCE" =~ ^https?:// ]]; then |
| 105 | if ! command -v curl >/dev/null 2>&1; then |
| 106 | echo "Error: 'curl' is required to download remote JSON files." >&2 |
| 107 | exit 1 |
| 108 | fi |
| 109 | echo "Downloading JSON from URL..." |
| 110 | JSON_FILE="$(mktemp -t vscode-json.XXXXXX)" |
| 111 | TEMP_JSON_FILE="$JSON_FILE" |
| 112 | curl -sSL "$SOURCE" -o "$JSON_FILE" |
| 113 | else |
| 114 | JSON_FILE="$SOURCE" |
| 115 | if [[ ! -f "$JSON_FILE" ]]; then |
| 116 | echo "Error: File '$JSON_FILE' does not exist." >&2 |
| 117 | exit 1 |
| 118 | fi |
| 119 | fi |
| 120 | |
| 121 | # 4. Extract extensions from JSON using jq (if not using hardcoded array) |
| 122 | if [[ "$USE_HARDCODED" == false ]]; then |
| 123 | mapfile -t EXTENSIONS < <(jq -r '.recommendations[]?' "$JSON_FILE") |
| 124 | |
| 125 | if [[ ${#EXTENSIONS[@]} -eq 0 ]]; then |
| 126 | echo "Error: No extensions found under the 'recommendations' key." >&2 |
| 127 | exit 1 |
| 128 | fi |
| 129 | fi |
| 130 | |
| 131 | declare -a CODE_ARGS=() |
| 132 | if [[ -n "$PROFILE" ]]; then |
| 133 | CODE_ARGS+=("--profile" "$PROFILE") |
| 134 | fi |
| 135 | |
| 136 | # Trap to clean up logs and temporary downloaded JSON files |
| 137 | LOG_FILE="$(mktemp -t vscode-install.XXXXXX)" |
| 138 | trap 'rm -f "$LOG_FILE" $TEMP_JSON_FILE' EXIT |
| 139 | |
| 140 | header="Installing ${#EXTENSIONS[@]} extension(s)" |
| 141 | [[ -n "$PROFILE" ]] && header+=" into profile '$PROFILE'" |
| 142 | echo "$header..." |
| 143 | echo |
| 144 | |
| 145 | installed=0 |
| 146 | failed=0 |
| 147 | declare -a FAILED_LIST=() |
| 148 | |
| 149 | for ext in "${EXTENSIONS[@]}"; do |
| 150 | [[ -z "$ext" ]] && continue |
| 151 | |
| 152 | printf ' -> %-55s ' "$ext" |
| 153 | if code "${CODE_ARGS[@]}" --install-extension "$ext" --force >"$LOG_FILE" 2>&1; then |
| 154 | echo "ok" |
| 155 | installed=$((installed + 1)) |
| 156 | else |
| 157 | echo "FAILED" |
| 158 | failed=$((failed + 1)) |
| 159 | FAILED_LIST+=("$ext") |
| 160 | sed 's/^/ /' "$LOG_FILE" >&2 || true |
| 161 | fi |
| 162 | done |
| 163 | |
| 164 | echo |
| 165 | echo "Done. Installed: ${installed}, Failed: ${failed}" |
| 166 | |
| 167 | if (( failed > 0 )); then |
| 168 | echo "Failed extensions:" >&2 |
| 169 | for f in "${FAILED_LIST[@]}"; do |
| 170 | echo " - $f" >&2 |
| 171 | done |
| 172 | exit 1 |
| 173 | fi |
| 1 | { |
| 2 | "recommendations": [ |
| 3 | "ms-python.python", |
| 4 | "ms-python.vscode-pylance", |
| 5 | "charliermarsh.ruff", |
| 6 | "usernamehw.errorlens", |
| 7 | "mikestead.dotenv", |
| 8 | "donjayamanne.python-environment-manager", |
| 9 | "rangav.vscode-thunder-client", |
| 10 | "njpwerner.autodocstring", |
| 11 | "eamodio.gitlens" |
| 12 | ] |
| 13 | } |
| 1 | { |
| 2 | "recommendations": [ |
| 3 | "dsznajder.es7-react-js-snippets", |
| 4 | "planbcoding.vscode-react-refactor", |
| 5 | "bradlc.vscode-tailwindcss", |
| 6 | "ms-vscode.vscode-typescript-next", |
| 7 | "yoavbls.pretty-ts-errors", |
| 8 | "christian-kohler.path-intellisense", |
| 9 | "christian-kohler.npm-intellisense", |
| 10 | "dbaeumer.vscode-eslint", |
| 11 | "esbenp.prettier-vscode", |
| 12 | "usernamehw.errorlens", |
| 13 | "eamodio.gitlens", |
| 14 | "formulahendry.auto-rename-tag", |
| 15 | "GitHub.vscode-github-actions", |
| 16 | "ms-azuretools.vscode-containers", |
| 17 | "GitHub.copilot", |
| 18 | "Codeium.codeium" |
| 19 | ] |
| 20 | } |
| 21 |