Last active 6 days ago

Bundled installer scripts. VS Code: install.{sh,ps1}. JetBrains IDEs (incl. Gateway / remote-dev-server): install-jetbrains.{sh,ps1}. Each script detects your OS (Windows / macOS / Ubuntu / WSL2) and is interactive.

Revision 26a87c8886c0089be7938f435c9d32ccda18be96

README.md Raw

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

# macOS / Ubuntu (default profile)
curl -sSL <GIST>/install.sh | bash

# named profile
bash <(curl -sSL <GIST>/install.sh) --profile "WorkSetup"
# Windows
iwr -useb <GIST>/install.ps1 | iex

# named profile
.\install.ps1 -ProfileName "WorkSetup"

JetBrains

# macOS / Ubuntu / WSL2 — interactive picker for IDEs and plugins
bash <(curl -sSL <GIST>/install-jetbrains.sh)
# Windows — interactive picker
iwr -useb <GIST>/install-jetbrains.ps1 | iex

What's in the catalogs

VS Code extensions

  • docker.docker
  • github.github-vscode-theme
  • ms-vscode-remote.vscode-remote-extensionpack
  • pkief.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.
  • --force is 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:

  1. Detect the host OS.
  2. Scan the system for installed JetBrains IDEs and (where applicable) Gateway / remote-dev targets.
  3. Show an interactive multi-select picker for IDEs and another for plugins.
  4. Run each IDE's installPlugins against 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 --multi if fzf is on PATH (TAB to toggle, Enter to confirm); otherwise prints a numbered list and accepts comma-separated indexes (1,3,5) or a for all.
  • PowerShell: uses Out-GridView -OutputMode Multiple when 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.

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.
install-jetbrains.ps1 Raw
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()]
26param()
27
28# Loosen execution policy for THIS process only.
29try {
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
60function 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'
90if (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'
110if (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'
126if (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
140if ($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# ------------------------------------------------------------------
150function 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
200Write-Host ("Detected {0} JetBrains target(s)." -f $Candidates.Count) -ForegroundColor Green
201
202$selectedIdes = Select-Multi -Title "Pick IDE(s)" -Items $Candidates -Property 'Label'
203if ($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'
209if ($selectedPlugins.Count -eq 0) {
210 Write-Host "No plugins selected; exiting." -ForegroundColor Red
211 exit 1
212}
213
214# ------------------------------------------------------------------
215# Install loop
216# ------------------------------------------------------------------
217$installed = 0
218$failed = 0
219$failedList = @()
220
221Write-Host ""
222Write-Host ("Installing {0} plugin(s) into {1} IDE(s)..." -f $selectedPlugins.Count, $selectedIdes.Count) -ForegroundColor Cyan
223Write-Host ""
224
225foreach ($ide in $selectedIdes) {
226 Write-Host (">>> {0}" -f $ide.Label)
227
228 switch ($ide.Type) {
229
230 # ---- standard IDE launcher ----
231 'Ide' {
232 $args = @('installPlugins') + ($selectedPlugins | ForEach-Object { $_.Id })
233 try {
234 $output = & $ide.Launcher @args 2>&1
235 if ($LASTEXITCODE -eq 0) {
236 Write-Host (" all {0} plugin(s) ok" -f $selectedPlugins.Count) -ForegroundColor Green
237 $installed += $selectedPlugins.Count
238 } else {
239 Write-Host (" installPlugins exited {0} FAILED" -f $LASTEXITCODE) -ForegroundColor Red
240 $failed += $selectedPlugins.Count
241 foreach ($p in $selectedPlugins) { $failedList += ("{0} :: {1}" -f $ide.Label, $p.Id) }
242 $output | ForEach-Object { Write-Host " $_" -ForegroundColor DarkRed }
243 }
244 } catch {
245 Write-Host (" launcher invocation threw FAILED") -ForegroundColor Red
246 $failed += $selectedPlugins.Count
247 foreach ($p in $selectedPlugins) { $failedList += ("{0} :: {1}" -f $ide.Label, $p.Id) }
248 Write-Host (" {0}" -f $_.Exception.Message) -ForegroundColor DarkRed
249 }
250 }
251
252 # ---- Toolbox .cmd shim ----
253 'WinCmd' {
254 $idArgs = ($selectedPlugins | ForEach-Object { '"{0}"' -f $_.Id }) -join ' '
255 try {
256 $output = cmd.exe /c "`"$($ide.Launcher)`" installPlugins $idArgs" 2>&1
257 if ($LASTEXITCODE -eq 0) {
258 Write-Host (" all {0} plugin(s) ok" -f $selectedPlugins.Count) -ForegroundColor Green
259 $installed += $selectedPlugins.Count
260 } else {
261 Write-Host (" installPlugins exited {0} FAILED" -f $LASTEXITCODE) -ForegroundColor Red
262 $failed += $selectedPlugins.Count
263 foreach ($p in $selectedPlugins) { $failedList += ("{0} :: {1}" -f $ide.Label, $p.Id) }
264 $output | ForEach-Object { Write-Host " $_" -ForegroundColor DarkRed }
265 }
266 } catch {
267 Write-Host (" cmd.exe invocation threw FAILED") -ForegroundColor Red
268 $failed += $selectedPlugins.Count
269 foreach ($p in $selectedPlugins) { $failedList += ("{0} :: {1}" -f $ide.Label, $p.Id) }
270 Write-Host (" {0}" -f $_.Exception.Message) -ForegroundColor DarkRed
271 }
272 }
273
274 # ---- Gateway Client ----
275 'Gateway' {
276 $client = $ide.Client
277 $buildFile = Join-Path $client 'build.txt'
278 if (-not (Test-Path $buildFile)) {
279 Write-Host " build.txt missing under client; cannot determine build id SKIPPED" -ForegroundColor Yellow
280 Write-Host " (open a Remote Project once to populate the client folder)" -ForegroundColor DarkYellow
281 $failed += $selectedPlugins.Count
282 foreach ($p in $selectedPlugins) { $failedList += ("{0} :: {1}" -f $ide.Label, $p.Id) }
283 continue
284 }
285 $buildId = (Get-Content $buildFile -Raw -ErrorAction Stop).Trim()
286
287 $pluginDir = Join-Path $client 'plugins'
288 if (-not (Test-Path $pluginDir)) {
289 New-Item -ItemType Directory -Path $pluginDir | Out-Null
290 }
291
292 $ua = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
293
294 foreach ($p in $selectedPlugins) {
295 Write-Host -NoNewline (" {0,-34} " -f $p.Id)
296 $encoded = [uri]::EscapeDataString($p.Id)
297 $url = "https://plugins.jetbrains.com/pluginManager/?action=download&id=$encoded&build=$buildId"
298 $tmp = Join-Path $env:TEMP ("jb-" + [guid]::NewGuid().ToString() + ".bin")
299 try {
300 Invoke-WebRequest -Uri $url -OutFile $tmp -UserAgent $ua -ErrorAction Stop -MaximumRedirection 5
301 $bytes = [System.IO.File]::ReadAllBytes($tmp) | Select-Object -First 2
302 if ($bytes.Count -ge 2 -and $bytes[0] -eq 0x50 -and $bytes[1] -eq 0x4B) {
303 Expand-Archive -Path $tmp -DestinationPath $pluginDir -Force -ErrorAction Stop
304 Write-Host "ok (zip)" -ForegroundColor Green
305 } else {
306 $jarName = Join-Path $pluginDir ($p.Id -replace '[^A-Za-z0-9._-]', '_') + '.jar'
307 Move-Item -Path $tmp -Destination $jarName -Force
308 Write-Host "ok (jar)" -ForegroundColor Green
309 }
310 $installed++
311 } catch {
312 Write-Host "FAILED" -ForegroundColor Red
313 Write-Host (" {0}" -f $_.Exception.Message) -ForegroundColor DarkRed
314 $failed++
315 $failedList += ("{0} :: {1}" -f $ide.Label, $p.Id)
316 } finally {
317 if (Test-Path $tmp) { Remove-Item $tmp -Force -ErrorAction SilentlyContinue }
318 }
319 }
320 Write-Host " Reopen the Gateway Client to load the new plugins." -ForegroundColor Yellow
321 }
322 }
323 Write-Host ""
324}
325
326Write-Host ("Done. Installed: {0}, Failed: {1}" -f $installed, $failed)
327if ($failed -gt 0) {
328 Write-Host "Failed:" -ForegroundColor Red
329 foreach ($f in $failedList) { Write-Host (" - {0}" -f $f) -ForegroundColor Red }
330 exit 1
331}
332
install-jetbrains.sh Raw
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 comma-separated indexes
13# (e.g. "1,3,5") or "a" for all.
14#
15# Usage:
16# ./install-jetbrains.sh # interactive
17# ./install-jetbrains.sh --help
18
19set -euo pipefail
20IFS=$'\n\t'
21
22# ------------------------------------------------------------------
23# Canonical marketplace plugin list (xmlId | display label).
24# Both columns separated by a literal pipe; the label is for the picker.
25# ------------------------------------------------------------------
26readonly PLUGIN_CATALOG=(
27 "com.intellij.ml.llm|JetBrains AI Assistant"
28 "izhangzhihao.rainbow.brackets|Rainbow Brackets"
29 "IdeaVIM|IdeaVim"
30 "org.sonarlint.idea|SonarQube for IDE (SonarLint)"
31 "Key Promoter X|Key Promoter X"
32 "net.ashald.envfile|EnvFile"
33 "org.intellij.qodana|Qodana"
34)
35
36# Known JetBrains IDE launcher basenames (Toolbox shims + Linux .sh names).
37readonly KNOWN_IDE_NAMES=(
38 idea idea.sh
39 pycharm pycharm.sh
40 webstorm webstorm.sh
41 goland goland.sh
42 rubymine rubymine.sh
43 clion clion.sh
44 phpstorm phpstorm.sh
45 datagrip datagrip.sh
46 rustrover rustrover.sh
47 rider rider.sh
48 studio studio.sh
49 fleet
50)
51
52usage() {
53 cat <<EOF
54Usage: $(basename "$0")
55
56Interactive JetBrains IDE plugin installer for macOS, Linux and WSL2.
57
58The script:
59 1. Detects your OS (macOS / Linux / WSL2)
60 2. Scans for installed JetBrains IDEs (Toolbox + standalone) and any
61 local remote-dev-server.sh distributions
62 3. Lets you multi-select IDE(s) and plugin(s)
63 4. Runs each launcher's "installPlugins" command for every selected plugin
64
65Use \`brew install fzf\` (or your distro's package manager) for a nicer picker.
66EOF
67}
68
69while [[ $# -gt 0 ]]; do
70 case "$1" in
71 -h|--help) usage; exit 0 ;;
72 *) echo "Error: unknown argument '$1'." >&2; usage >&2; exit 2 ;;
73 esac
74done
75
76# ------------------------------------------------------------------
77# OS detection
78# ------------------------------------------------------------------
79OS_KIND=""
80case "$(uname -s)" in
81 Darwin) OS_KIND=macos ;;
82 Linux)
83 if grep -qi microsoft /proc/version 2>/dev/null; then
84 OS_KIND=wsl2
85 else
86 OS_KIND=linux
87 fi
88 ;;
89 *)
90 echo "Error: unsupported OS '$(uname -s)'. Use install-jetbrains.ps1 on Windows." >&2
91 exit 1
92 ;;
93esac
94
95# ------------------------------------------------------------------
96# Helpers
97# ------------------------------------------------------------------
98
99# is_known_ide_name <basename> -> 0 if it matches a known JetBrains launcher.
100is_known_ide_name() {
101 local n="$1"
102 local k
103 for k in "${KNOWN_IDE_NAMES[@]}"; do
104 [[ "$n" == "$k" ]] && return 0
105 done
106 return 1
107}
108
109# pretty_from_path /path/to/idea.sh -> "IntelliJ IDEA (~/.local/share/.../idea.sh)"
110# Builds a short, readable label for the picker.
111pretty_label() {
112 local path="$1" base
113 base="$(basename "$path")"
114 base="${base%.sh}"
115 base="${base%.cmd}"
116 base="${base%.exe}"
117 case "$base" in
118 idea|idea64) printf 'IntelliJ IDEA' ;;
119 pycharm|pycharm64) printf 'PyCharm' ;;
120 webstorm|webstorm64) printf 'WebStorm' ;;
121 goland|goland64) printf 'GoLand' ;;
122 rubymine|rubymine64) printf 'RubyMine' ;;
123 clion|clion64) printf 'CLion' ;;
124 phpstorm|phpstorm64) printf 'PhpStorm' ;;
125 datagrip|datagrip64) printf 'DataGrip' ;;
126 rustrover|rustrover64) printf 'RustRover' ;;
127 rider|rider64) printf 'Rider' ;;
128 studio|studio64) printf 'Android Studio' ;;
129 fleet) printf 'Fleet' ;;
130 *) printf '%s' "$base" ;;
131 esac
132 printf ' (%s)' "$path"
133}
134
135# Translate a Linux/WSL home into the Windows-side user dir under /mnt/c.
136win_user_dir() {
137 local winuser
138 winuser="$(cmd.exe /c 'echo %USERNAME%' 2>/dev/null | tr -d '\r\n' || true)"
139 [[ -z "$winuser" ]] && winuser="$USER"
140 printf '/mnt/c/Users/%s' "$winuser"
141}
142
143# ------------------------------------------------------------------
144# IDE candidate scan
145# Each candidate is recorded as a single line:
146# <type>|<launcher>|<label>
147# where <type> is one of:
148# ide -> run "<launcher> installPlugins <id>..."
149# wincmd -> run "cmd.exe /c <launcher> installPlugins <id>..."
150# remotedev -> remote-dev-server.sh installPlugins ...
151# ------------------------------------------------------------------
152declare -a CANDIDATES=()
153
154add_candidate() {
155 local type="$1" launcher="$2"
156 CANDIDATES+=("${type}|${launcher}|$(pretty_label "$launcher")")
157}
158
159scan_macos() {
160 local d="$HOME/Library/Application Support/JetBrains/Toolbox/scripts"
161 if [[ -d "$d" ]]; then
162 local f
163 while IFS= read -r -d '' f; do
164 local base
165 base="$(basename "$f")"
166 is_known_ide_name "$base" || continue
167 [[ -x "$f" ]] && add_candidate ide "$f"
168 done < <(find "$d" -maxdepth 1 -type f -print0 2>/dev/null)
169 fi
170
171 local app bin
172 for app in /Applications/*.app; do
173 [[ -d "$app/Contents/MacOS" ]] || continue
174 [[ "$(basename "$app")" =~ ^(IntelliJ|PyCharm|WebStorm|GoLand|RubyMine|CLion|PhpStorm|DataGrip|RustRover|Rider|Android\ Studio|Fleet) ]] || continue
175 for bin in "$app"/Contents/MacOS/*; do
176 [[ -x "$bin" ]] || continue
177 local b
178 b="$(basename "$bin")"
179 is_known_ide_name "$b" || continue
180 add_candidate ide "$bin"
181 done
182 done
183}
184
185scan_linux() {
186 local apps="$HOME/.local/share/JetBrains/Toolbox/apps"
187 if [[ -d "$apps" ]]; then
188 local f
189 while IFS= read -r -d '' f; do
190 local b
191 b="$(basename "$f")"
192 is_known_ide_name "$b" || continue
193 [[ -x "$f" ]] && add_candidate ide "$f"
194 done < <(find "$apps" -maxdepth 3 -type f -name '*.sh' -print0 2>/dev/null)
195 fi
196
197 # /opt/<ide>/bin/<ide>.sh — common for .deb / snap / tarball installs.
198 local opt
199 for opt in /opt/*/bin/*.sh; do
200 [[ -x "$opt" ]] || continue
201 local b
202 b="$(basename "$opt")"
203 is_known_ide_name "$b" || continue
204 add_candidate ide "$opt"
205 done
206
207 # Remote-dev-server distributions (Orbstack/Gateway).
208 local rd
209 while IFS= read -r -d '' rd; do
210 add_candidate remotedev "$rd"
211 done < <(find "$HOME/.cache/JetBrains/RemoteDev/dist" -maxdepth 3 -type f -name 'remote-dev-server.sh' -print0 2>/dev/null)
212}
213
214scan_wsl2() {
215 local winhome
216 winhome="$(win_user_dir)"
217 [[ -d "$winhome" ]] || return 0
218
219 # Standalone installs under AppData\Local\Programs\<IDE>\bin\*.exe
220 local exe b
221 while IFS= read -r -d '' exe; do
222 b="$(basename "$exe")"
223 case "$b" in
224 idea64.exe|pycharm64.exe|webstorm64.exe|goland64.exe|rubymine64.exe|\
225 clion64.exe|phpstorm64.exe|datagrip64.exe|rustrover64.exe|rider64.exe|\
226 studio64.exe|fleet.exe)
227 add_candidate ide "$exe"
228 ;;
229 esac
230 done < <(find "$winhome/AppData/Local/Programs" -maxdepth 4 -type f -name '*.exe' -print0 2>/dev/null)
231
232 # Toolbox shell shims (.cmd) — invoke via cmd.exe.
233 local scripts="$winhome/AppData/Local/JetBrains/Toolbox/scripts"
234 if [[ -d "$scripts" ]]; then
235 local f
236 while IFS= read -r -d '' f; do
237 b="$(basename "${f%.cmd}")"
238 is_known_ide_name "$b" || continue
239 add_candidate wincmd "$f"
240 done < <(find "$scripts" -maxdepth 1 -type f -name '*.cmd' -print0 2>/dev/null)
241 fi
242
243 # Local Linux-side remote-dev-server.sh (still useful inside WSL2).
244 local rd
245 while IFS= read -r -d '' rd; do
246 add_candidate remotedev "$rd"
247 done < <(find "$HOME/.cache/JetBrains/RemoteDev/dist" -maxdepth 3 -type f -name 'remote-dev-server.sh' -print0 2>/dev/null)
248}
249
250case "$OS_KIND" in
251 macos) scan_macos ;;
252 linux) scan_linux ;;
253 wsl2) scan_wsl2 ;;
254esac
255
256if [[ ${#CANDIDATES[@]} -eq 0 ]]; then
257 echo "No JetBrains IDEs detected on this system." >&2
258 echo "Install one via the JetBrains Toolbox, then re-run." >&2
259 exit 1
260fi
261
262# ------------------------------------------------------------------
263# Multi-select picker
264# Reads newline-separated options on stdin, prints selected lines on stdout.
265# ------------------------------------------------------------------
266pick_multi() {
267 local prompt="$1" header="$2"
268 if command -v fzf >/dev/null 2>&1; then
269 fzf --multi --reverse --prompt="$prompt " --header="$header"
270 return
271 fi
272
273 # Fallback: numbered list + comma-input.
274 local -a items=()
275 local line
276 while IFS= read -r line; do items+=("$line"); done
277
278 local total=${#items[@]} i
279 while :; do
280 {
281 echo "$header"
282 for (( i=0; i<total; i++ )); do
283 printf ' %2d) %s\n' "$((i+1))" "${items[i]}"
284 done
285 printf '%s' "$prompt (e.g. 1,3,5 or a for all): "
286 } >&2
287
288 local input
289 if ! IFS= read -r input </dev/tty; then
290 echo "no tty available; install fzf or run interactively" >&2
291 return 1
292 fi
293
294 if [[ "$input" == "a" || "$input" == "A" ]]; then
295 printf '%s\n' "${items[@]}"
296 return
297 fi
298 [[ -z "$input" ]] && { echo " please enter at least one number, or 'a'." >&2; continue; }
299
300 local -a picked=() bad=0
301 local tok
302 IFS=',' read -r -a toks <<<"$input"
303 for tok in "${toks[@]}"; do
304 tok="${tok// /}"
305 [[ -z "$tok" ]] && continue
306 if [[ ! "$tok" =~ ^[0-9]+$ ]]; then bad=1; break; fi
307 if (( tok < 1 || tok > total )); then bad=1; break; fi
308 picked+=("${items[tok-1]}")
309 done
310 if (( bad )); then
311 echo " invalid input; try again." >&2
312 continue
313 fi
314 if (( ${#picked[@]} == 0 )); then
315 echo " no valid selections; try again." >&2
316 continue
317 fi
318 printf '%s\n' "${picked[@]}"
319 return
320 done
321}
322
323# ------------------------------------------------------------------
324# IDE picker
325# ------------------------------------------------------------------
326declare -a ide_lines=()
327for c in "${CANDIDATES[@]}"; do
328 # human-readable picker line: just the third field
329 ide_lines+=("${c#*|*|}")
330done
331
332# Map label -> (type, launcher) so we can recover after picking.
333declare -A label_type label_launcher
334for c in "${CANDIDATES[@]}"; do
335 IFS='|' read -r t l lab <<<"$c"
336 label_type[$lab]="$t"
337 label_launcher[$lab]="$l"
338done
339
340echo "Detected ${#CANDIDATES[@]} JetBrains target(s)."
341mapfile -t SELECTED_IDES < <(
342 printf '%s\n' "${ide_lines[@]}" |
343 pick_multi "IDEs>" "Pick IDE(s) — TAB to toggle (fzf) or comma indexes"
344)
345
346if (( ${#SELECTED_IDES[@]} == 0 )); then
347 echo "No IDEs selected; exiting." >&2
348 exit 1
349fi
350
351# ------------------------------------------------------------------
352# Plugin picker
353# ------------------------------------------------------------------
354declare -a plugin_lines=()
355declare -A label_to_id
356for entry in "${PLUGIN_CATALOG[@]}"; do
357 IFS='|' read -r pid plabel <<<"$entry"
358 line="$(printf '%-32s %s' "$plabel" "$pid")"
359 plugin_lines+=("$line")
360 label_to_id[$line]="$pid"
361done
362
363mapfile -t SELECTED_PLUGIN_LINES < <(
364 printf '%s\n' "${plugin_lines[@]}" |
365 pick_multi "Plugins>" "Pick plugin(s) — TAB to toggle (fzf) or comma indexes"
366)
367
368if (( ${#SELECTED_PLUGIN_LINES[@]} == 0 )); then
369 echo "No plugins selected; exiting." >&2
370 exit 1
371fi
372
373declare -a SELECTED_PLUGIN_IDS=()
374for line in "${SELECTED_PLUGIN_LINES[@]}"; do
375 SELECTED_PLUGIN_IDS+=("${label_to_id[$line]}")
376done
377
378# ------------------------------------------------------------------
379# Install loop
380# ------------------------------------------------------------------
381LOG_FILE="$(mktemp -t jb-install.XXXXXX)"
382trap 'rm -f "$LOG_FILE"' EXIT
383
384installed=0
385failed=0
386declare -a FAILED_LIST=()
387
388echo
389echo "Installing ${#SELECTED_PLUGIN_IDS[@]} plugin(s) into ${#SELECTED_IDES[@]} IDE(s)..."
390echo
391
392for ide_label in "${SELECTED_IDES[@]}"; do
393 itype="${label_type[$ide_label]}"
394 launcher="${label_launcher[$ide_label]}"
395
396 echo ">>> $ide_label"
397 for pid in "${SELECTED_PLUGIN_IDS[@]}"; do
398 printf ' %-34s ' "$pid"
399 case "$itype" in
400 ide|remotedev)
401 if "$launcher" installPlugins "$pid" >"$LOG_FILE" 2>&1; then
402 echo "ok"
403 installed=$((installed + 1))
404 else
405 echo "FAILED"
406 failed=$((failed + 1))
407 FAILED_LIST+=("${ide_label} :: ${pid}")
408 sed 's/^/ /' "$LOG_FILE" >&2 || true
409 fi
410 ;;
411 wincmd)
412 if cmd.exe /c "$launcher" installPlugins "$pid" >"$LOG_FILE" 2>&1; then
413 echo "ok"
414 installed=$((installed + 1))
415 else
416 echo "FAILED"
417 failed=$((failed + 1))
418 FAILED_LIST+=("${ide_label} :: ${pid}")
419 sed 's/^/ /' "$LOG_FILE" >&2 || true
420 fi
421 ;;
422 esac
423 done
424 echo
425done
426
427echo "Done. Installed: ${installed}, Failed: ${failed}"
428
429if (( failed > 0 )); then
430 echo "Failed:" >&2
431 for f in "${FAILED_LIST[@]}"; do
432 echo " - $f" >&2
433 done
434 exit 1
435fi
436
install.ps1 Raw
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()]
27param(
28 [Parameter(Mandatory = $false, Position = 0)]
29 [string]$ProfileName = ""
30)
31
32# Loosen execution policy for THIS process only (no persisted change).
33try {
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 'ms-vscode-remote.vscode-remote-extensionpack',
48 'pkief.material-icon-theme'
49)
50
51$codeCmd = Get-Command code -ErrorAction SilentlyContinue
52if (-not $codeCmd) {
53 Write-Host ""
54 Write-Host "Error: the 'code' command is not on your PATH." -ForegroundColor Red
55 Write-Host " Install VS Code from https://code.visualstudio.com/ and make sure" -ForegroundColor Yellow
56 Write-Host ' "Add to PATH" was checked during setup, then re-open your terminal.' -ForegroundColor Yellow
57 exit 1
58}
59
60$extraArgs = @()
61if (-not [string]::IsNullOrWhiteSpace($ProfileName)) {
62 $extraArgs += @('--profile', $ProfileName)
63}
64
65$header = "Installing $($Extensions.Count) extension(s)"
66if ($ProfileName) { $header += " into profile '$ProfileName'" }
67Write-Host ("{0}..." -f $header) -ForegroundColor Cyan
68Write-Host ""
69
70$installed = 0
71$failed = 0
72$failedList = @()
73
74foreach ($ext in $Extensions) {
75 Write-Host -NoNewline (" -> {0,-55} " -f $ext)
76 try {
77 $output = & code @extraArgs --install-extension $ext --force 2>&1
78 if ($LASTEXITCODE -eq 0) {
79 Write-Host "ok" -ForegroundColor Green
80 $installed++
81 } else {
82 Write-Host "FAILED" -ForegroundColor Red
83 $failed++
84 $failedList += $ext
85 $output | ForEach-Object { Write-Host " $_" -ForegroundColor DarkRed }
86 }
87 } catch {
88 Write-Host "FAILED" -ForegroundColor Red
89 $failed++
90 $failedList += $ext
91 Write-Host (" {0}" -f $_.Exception.Message) -ForegroundColor DarkRed
92 }
93}
94
95Write-Host ""
96Write-Host ("Done. Installed: {0}, Failed: {1}" -f $installed, $failed)
97
98if ($failed -gt 0) {
99 Write-Host "Failed extensions:" -ForegroundColor Red
100 foreach ($f in $failedList) {
101 Write-Host (" - {0}" -f $f) -ForegroundColor Red
102 }
103 exit 1
104}
105
install.sh Raw
1#!/usr/bin/env bash
2#
3# VS Code extension installer for macOS and Linux (Ubuntu).
4#
5# Usage:
6# ./install.sh # install into the default profile
7# ./install.sh --profile "MyProfile" # install into a named profile
8# ./install.sh --help
9#
10# If --profile is provided and the profile does not exist, VS Code creates
11# it automatically. Quote names that contain spaces.
12
13set -euo pipefail
14IFS=$'\n\t'
15
16readonly EXTENSIONS=(
17 "docker.docker"
18 "github.github-vscode-theme"
19 "ms-vscode-remote.vscode-remote-extensionpack"
20 "pkief.material-icon-theme"
21)
22
23PROFILE=""
24
25usage() {
26 cat <<EOF
27Usage: $(basename "$0") [--profile "ProfileName"]
28
29Installs a curated list of VS Code extensions on macOS or Linux.
30
31Options:
32 -p, --profile <name> Install into the named VS Code profile.
33 VS Code creates the profile if it doesn't exist.
34 -h, --help Show this help and exit.
35EOF
36}
37
38while [[ $# -gt 0 ]]; do
39 case "$1" in
40 -p|--profile)
41 if [[ $# -lt 2 || -z "${2:-}" ]]; then
42 echo "Error: --profile requires a non-empty value." >&2
43 exit 2
44 fi
45 PROFILE="$2"
46 shift 2
47 ;;
48 -h|--help)
49 usage
50 exit 0
51 ;;
52 *)
53 echo "Error: unknown argument '$1'." >&2
54 usage >&2
55 exit 2
56 ;;
57 esac
58done
59
60OS="$(uname -s)"
61case "$OS" in
62 Darwin|Linux) ;;
63 *)
64 echo "Error: unsupported OS '$OS'. Use install.ps1 on Windows." >&2
65 exit 1
66 ;;
67esac
68
69if ! command -v code >/dev/null 2>&1; then
70 cat >&2 <<'EOF'
71Error: the 'code' command is not on your PATH.
72
73 macOS: open VS Code, press Cmd+Shift+P, run
74 "Shell Command: Install 'code' command in PATH".
75 Linux: install VS Code from https://code.visualstudio.com/
76 (or: sudo snap install --classic code), then reopen your shell.
77EOF
78 exit 1
79fi
80
81declare -a CODE_ARGS=()
82if [[ -n "$PROFILE" ]]; then
83 CODE_ARGS+=("--profile" "$PROFILE")
84fi
85
86LOG_FILE="$(mktemp -t vscode-install.XXXXXX)"
87trap 'rm -f "$LOG_FILE"' EXIT
88
89header="Installing ${#EXTENSIONS[@]} extension(s)"
90[[ -n "$PROFILE" ]] && header+=" into profile '$PROFILE'"
91echo "$header..."
92echo
93
94installed=0
95failed=0
96declare -a FAILED_LIST=()
97
98for ext in "${EXTENSIONS[@]}"; do
99 printf ' -> %-55s ' "$ext"
100 if code "${CODE_ARGS[@]}" --install-extension "$ext" --force >"$LOG_FILE" 2>&1; then
101 echo "ok"
102 installed=$((installed + 1))
103 else
104 echo "FAILED"
105 failed=$((failed + 1))
106 FAILED_LIST+=("$ext")
107 sed 's/^/ /' "$LOG_FILE" >&2 || true
108 fi
109done
110
111echo
112echo "Done. Installed: ${installed}, Failed: ${failed}"
113
114if (( failed > 0 )); then
115 echo "Failed extensions:" >&2
116 for f in "${FAILED_LIST[@]}"; do
117 echo " - $f" >&2
118 done
119 exit 1
120fi
121