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 fc264ce12f271d8cbae790d7f9774ef14f22f767

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 https://opengist.rmrf.online/weehong/b2a7c0a2ae6d40e8a14f8d85964a9186/raw/HEAD/install.sh | bash

# named profile
bash <(curl -sSL https://opengist.rmrf.online/weehong/b2a7c0a2ae6d40e8a14f8d85964a9186/raw/HEAD/install.sh) --profile "WorkSetup"
# 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.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.

"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.
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# 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# ------------------------------------------------------------------
220function 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
236while ($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
254Write-Host ""
255Write-Host ("Installing {0} plugin(s) into {1} IDE(s)..." -f $selectedPlugins.Count, $selectedIdes.Count) -ForegroundColor Cyan
256Write-Host ""
257
258foreach ($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
371Write-Host ("Done. Installed: {0}, Failed: {1}" -f $installed, $failed)
372if ($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
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# Running-IDE preflight
380# JetBrains' `installPlugins` refuses to run while the IDE GUI is open
381# ("Only one instance of IDEA can be run at a time."). We do a best-effort
382# pgrep against the IDE app directory so the user can close them upfront
383# instead of seeing N identical errors during the install loop.
384# ------------------------------------------------------------------
385ide_is_running() {
386 local launcher="$1" type="$2"
387
388 # remote-dev-server.sh spawns short-lived workers; WSL2->Windows lookups
389 # would need tasklist.exe — skip in both cases.
390 case "$type" in
391 remotedev|wincmd) return 1 ;;
392 esac
393
394 case "$launcher" in
395 */Toolbox/apps/*)
396 # /.../Toolbox/apps/<ide-dir>/[ch-N/<ver>/]bin/idea.sh -> match "<ide-dir>"
397 local rest="${launcher#*/Toolbox/apps/}"
398 local ide_dir="${rest%%/*}"
399 [[ -n "$ide_dir" ]] && pgrep -f -- "/Toolbox/apps/${ide_dir}/" >/dev/null 2>&1
400 ;;
401 */Applications/*.app/*)
402 local app_path="${launcher%/Contents/MacOS/*}"
403 pgrep -f -- "${app_path}/Contents/" >/dev/null 2>&1
404 ;;
405 *)
406 local b
407 b="$(basename "$launcher")"
408 b="${b%.sh}"
409 pgrep -fi -- "$b" >/dev/null 2>&1
410 ;;
411 esac
412}
413
414while :; do
415 declare -a _running=()
416 for ide_label in "${SELECTED_IDES[@]}"; do
417 if ide_is_running "${label_launcher[$ide_label]}" "${label_type[$ide_label]}"; then
418 _running+=("$ide_label")
419 fi
420 done
421 (( ${#_running[@]} == 0 )) && break
422
423 {
424 echo
425 echo "The following IDE(s) appear to be running — installPlugins needs them closed:"
426 for r in "${_running[@]}"; do
427 echo " - $r"
428 done
429 echo
430 printf 'Close them, then press Enter to retry (or Ctrl+C to abort): '
431 } >&2
432 if ! IFS= read -r _ </dev/tty; then
433 echo "no tty; aborting" >&2
434 exit 1
435 fi
436done
437
438# ------------------------------------------------------------------
439# Install loop
440# ------------------------------------------------------------------
441LOG_FILE="$(mktemp -t jb-install.XXXXXX)"
442trap 'rm -f "$LOG_FILE"' EXIT
443
444installed=0
445failed=0
446declare -a FAILED_LIST=()
447
448echo
449echo "Installing ${#SELECTED_PLUGIN_IDS[@]} plugin(s) into ${#SELECTED_IDES[@]} IDE(s)..."
450echo
451
452handle_single_instance_abort() {
453 local ide_label="$1" current_pid="$2"
454 shift 2
455 echo
456 echo " ! '${ide_label}' is still running. Skipping remaining plugins for this IDE." >&2
457 echo " ! Close the IDE and re-run the script to finish." >&2
458 # Mark current and remaining-for-this-IDE plugins as failed.
459 local mark_from=0 p
460 for p in "${SELECTED_PLUGIN_IDS[@]}"; do
461 if (( mark_from )) || [[ "$p" == "$current_pid" ]]; then
462 mark_from=1
463 failed=$((failed + 1))
464 FAILED_LIST+=("${ide_label} :: ${p} (IDE running — close it and re-run)")
465 fi
466 done
467}
468
469for ide_label in "${SELECTED_IDES[@]}"; do
470 itype="${label_type[$ide_label]}"
471 launcher="${label_launcher[$ide_label]}"
472
473 echo ">>> $ide_label"
474 ide_aborted=0
475 for pid in "${SELECTED_PLUGIN_IDS[@]}"; do
476 (( ide_aborted )) && break
477 printf ' %-34s ' "$pid"
478 rc=0
479 case "$itype" in
480 ide|remotedev)
481 "$launcher" installPlugins "$pid" >"$LOG_FILE" 2>&1 || rc=$?
482 ;;
483 wincmd)
484 cmd.exe /c "$launcher" installPlugins "$pid" >"$LOG_FILE" 2>&1 || rc=$?
485 ;;
486 esac
487
488 if (( rc == 0 )); then
489 echo "ok"
490 installed=$((installed + 1))
491 elif grep -q "Only one instance" "$LOG_FILE" 2>/dev/null; then
492 echo "SKIPPED"
493 handle_single_instance_abort "$ide_label" "$pid"
494 ide_aborted=1
495 else
496 echo "FAILED"
497 failed=$((failed + 1))
498 FAILED_LIST+=("${ide_label} :: ${pid}")
499 sed 's/^/ /' "$LOG_FILE" >&2 || true
500 fi
501 done
502 echo
503done
504
505echo "Done. Installed: ${installed}, Failed: ${failed}"
506
507if (( failed > 0 )); then
508 echo "Failed:" >&2
509 for f in "${FAILED_LIST[@]}"; do
510 echo " - $f" >&2
511 done
512 exit 1
513fi
514
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