最後活躍 1 week 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.

修訂 a7cce344bdae9e63043310a91656e0cb0e611d35

README.md 原始檔案

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.copilot-chat
  • github.github-vscode-theme
  • ms-vscode-remote.vscode-remote-extensionpack
  • pkief.material-icon-theme

JetBrains plugins (canonical marketplace IDs)

xmlId Plugin
com.intellij.ml.llm JetBrains AI Assistant
izhangzhihao.rainbow.brackets Rainbow Brackets
IdeaVIM IdeaVim
ru.adelf.idea.dotenv .env files
org.sonarlint.idea SonarQube for IDE (SonarLint)
org.intellij.qodana Qodana
Key Promoter X Key Promoter X
com.crunch42.openapi OpenAPI (Swagger) Editor
net.ashald.envfile EnvFile
software.xdev.saveactions Save Actions X

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