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 b8a1417b74cf8b183c2f9e5de4a791af64b56892

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 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
20set -euo pipefail
21IFS=$'\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# ------------------------------------------------------------------
27readonly 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 "9792|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).
38readonly 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
53usage() {
54 cat <<EOF
55Usage: $(basename "$0")
56
57Interactive JetBrains IDE plugin installer for macOS, Linux and WSL2.
58
59The 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
66Use \`brew install fzf\` (or your distro's package manager) for a nicer picker.
67EOF
68}
69
70while [[ $# -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
75done
76
77# ------------------------------------------------------------------
78# OS detection
79# ------------------------------------------------------------------
80OS_KIND=""
81case "$(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 ;;
94esac
95
96# ------------------------------------------------------------------
97# Helpers
98# ------------------------------------------------------------------
99
100# is_known_ide_name <basename> -> 0 if it matches a known JetBrains launcher.
101is_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.
112pretty_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.
137win_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# ------------------------------------------------------------------
153declare -a CANDIDATES=()
154
155add_candidate() {
156 local type="$1" launcher="$2"
157 CANDIDATES+=("${type}|${launcher}|$(pretty_label "$launcher")")
158}
159
160scan_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
186scan_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
215scan_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
251case "$OS_KIND" in
252 macos) scan_macos ;;
253 linux) scan_linux ;;
254 wsl2) scan_wsl2 ;;
255esac
256
257if [[ ${#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
261fi
262
263# ------------------------------------------------------------------
264# Multi-select picker (Handles Space separation & Exclusions via !)
265# Reads newline-separated options on stdin, prints selected lines on stdout.
266# ------------------------------------------------------------------
267pick_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# ------------------------------------------------------------------
372declare -a ide_lines=()
373for c in "${CANDIDATES[@]}"; do
374 # human-readable picker line: just the third field
375 ide_lines+=("${c#*|*|}")
376done
377
378# Map label -> (type, launcher) so we can recover after picking.
379declare -A label_type label_launcher
380for c in "${CANDIDATES[@]}"; do
381 IFS='|' read -r t l lab <<<"$c"
382 label_type[$lab]="$t"
383 label_launcher[$lab]="$l"
384done
385
386echo "Detected ${#CANDIDATES[@]} JetBrains target(s)."
387mapfile -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
392if (( ${#SELECTED_IDES[@]} == 0 )); then
393 echo "No IDEs selected; exiting." >&2
394 exit 1
395fi
396
397# ------------------------------------------------------------------
398# Plugin picker
399# ------------------------------------------------------------------
400declare -a plugin_lines=()
401declare -A label_to_id
402for 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"
407done
408
409mapfile -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
414if (( ${#SELECTED_PLUGIN_LINES[@]} == 0 )); then
415 echo "No plugins selected; exiting." >&2
416 exit 1
417fi
418
419declare -a SELECTED_PLUGIN_IDS=()
420for line in "${SELECTED_PLUGIN_LINES[@]}"; do
421 SELECTED_PLUGIN_IDS+=("${label_to_id[$line]}")
422done
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# ------------------------------------------------------------------
431ide_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
460while :; 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
482done
483
484# ------------------------------------------------------------------
485# Install loop
486# ------------------------------------------------------------------
487LOG_FILE="$(mktemp -t jb-install.XXXXXX)"
488trap 'rm -f "$LOG_FILE"' EXIT
489
490installed=0
491failed=0
492declare -a FAILED_LIST=()
493
494echo
495echo "Installing ${#SELECTED_PLUGIN_IDS[@]} plugin(s) into ${#SELECTED_IDES[@]} IDE(s)..."
496echo
497
498handle_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
515for 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
549done
550
551echo "Done. Installed: ${installed}, Failed: ${failed}"
552
553if (( failed > 0 )); then
554 echo "Failed:" >&2
555 for f in "${FAILED_LIST[@]}"; do
556 echo " - $f" >&2
557 done
558 exit 1
559fi
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