Zuletzt aktiv 2 weeks 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.

Änderung b89d770fdfb1b90fd0682d0971a3db3659d25add

README.md Originalformat

VS Code + JetBrains Extension Managers

A bundle of cross-platform scripts for editor extensions and plugins, one command per platform. The JetBrains scripts install and uninstall plugins; the VS Code scripts install extensions.

File Editor Platforms
install.sh VS Code macOS, Linux (Ubuntu)
install.ps1 VS Code Windows
install-jetbrains.sh JetBrains IDEs + remote-dev-server macOS, Linux, WSL2
install-jetbrains.ps1 JetBrains IDEs + Gateway Client Windows

Quick start

Replace <GIST> with the raw base of this Gist (everything up to and including /raw/HEAD/).

VS Code

# 1. macOS / Ubuntu (Install to default profile from a LOCAL .vscode/extensions.json)
bash <(curl -sSL https://opengist.resetrix.work/weehong/b2a7c0a2ae6d40e8a14f8d85964a9186/raw/HEAD/install.sh)

# 2. Install remote recommendation JSON to the default profile
bash <(curl -sSL https://opengist.resetrix.work/weehong/b2a7c0a2ae6d40e8a14f8d85964a9186/raw/HEAD/install.sh) -f "https://opengist.resetrix.work/weehong/b2a7c0a2ae6d40e8a14f8d85964a9186/raw/HEAD/typescript_react_recommendation.json"

# 3. Install TypeScript and React remote recommendation JSON to a specific named profile
bash <(curl -sSL https://opengist.resetrix.work/weehong/b2a7c0a2ae6d40e8a14f8d85964a9186/raw/HEAD/install.sh) -p "TypeScript" -f "https://opengist.resetrix.work/weehong/b2a7c0a2ae6d40e8a14f8d85964a9186/raw/HEAD/typescript_react_recommendation.json"

# 4. Install Python remote recommendation JSON to a specific named profile
bash <(curl -sSL https://opengist.resetrix.work/weehong/b2a7c0a2ae6d40e8a14f8d85964a9186/raw/HEAD/install.sh) -p "Python" -f "https://opengist.resetrix.work/weehong/b2a7c0a2ae6d40e8a14f8d85964a9186/raw/HEAD/python_recommendation.json"
# Windows
iwr -useb https://opengist.resetrix.work/weehong/b2a7c0a2ae6d40e8a14f8d85964a9186/raw/HEAD/install.ps1 | iex

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

JetBrains

# macOS / Ubuntu / WSL2 — IDE, action, then plugin picker
bash <(curl -sSL https://opengist.resetrix.work/weehong/b2a7c0a2ae6d40e8a14f8d85964a9186/raw/HEAD/install-jetbrains.sh)
# Windows — IDE, action, then plugin picker
iwr -useb https://opengist.resetrix.work/weehong/b2a7c0a2ae6d40e8a14f8d85964a9186/raw/HEAD/install-jetbrains.ps1 | iex

What's in the catalogs

VS Code extensions

  • docker.docker
  • github.github-vscode-theme
  • GitHub.vscode-github-actions
  • ms-azuretools.vscode-containers
  • 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 the IDE picker first.
  4. Ask whether to install or uninstall.
  5. For install, show the curated catalog; for uninstall, show the user-installed plugins actually found in the selected IDEs.
  6. Install the selected marketplace IDs or, after showing exact paths and asking for confirmation, remove the selected plugin files.

JetBrains AI Assistant is identified by its marketplace ID, com.intellij.ml.llm, so uninstall works even though its on-disk directory is normally named ml-llm.

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 space-separated indexes (1 3 5), exclusions (!2 !4), or a for all.
  • PowerShell: uses Out-GridView -OutputMode Multiple when available; otherwise the same numbered/comma-input fallback.

Uninstall behavior

JetBrains launchers provide installPlugins but no matching uninstall command. The scripts therefore resolve the selected IDE's dataDirectoryName from product-info.json, inspect each user-installed plugin's META-INF/plugin.xml, and present those plugin names and IDs for selection. Before removal, the script prints every resolved IDE and path and requires confirmation.

Only user-installed plugins are offered. Bundled plugins cannot be removed this way and must be disabled in the IDE. Restart the selected IDEs after uninstalling.

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 scripts run a preflight check before changing plugins: 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 for IDE, action, and plugin choices every time; there's no flag to skip the picker by design.
ideavim-cheatsheet.html Originalformat
1<!doctype html>
2<html lang="en">
3<head>
4 <meta charset="utf-8">
5 <meta name="viewport" content="width=device-width, initial-scale=1">
6 <title>IdeaVim Cheatsheet</title>
7 <style>
8 @page {
9 size: A4;
10 margin: 14mm;
11 }
12
13 * {
14 box-sizing: border-box;
15 }
16
17 body {
18 margin: 0;
19 color: #15171a;
20 background: #ffffff;
21 font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
22 font-size: 10.5px;
23 line-height: 1.35;
24 }
25
26 header {
27 margin-bottom: 14px;
28 padding-bottom: 10px;
29 border-bottom: 2px solid #15171a;
30 }
31
32 h1 {
33 margin: 0 0 3px;
34 font-size: 25px;
35 line-height: 1.1;
36 letter-spacing: 0;
37 }
38
39 .subtitle {
40 margin: 0;
41 color: #4d5662;
42 font-size: 11px;
43 }
44
45 .grid {
46 display: grid;
47 grid-template-columns: 1fr 1fr;
48 gap: 11px 14px;
49 align-items: start;
50 }
51
52 section {
53 break-inside: avoid;
54 page-break-inside: avoid;
55 }
56
57 h2 {
58 margin: 0 0 5px;
59 padding: 5px 7px;
60 color: #ffffff;
61 background: #243447;
62 border-radius: 4px;
63 font-size: 11px;
64 line-height: 1.2;
65 letter-spacing: 0;
66 }
67
68 table {
69 width: 100%;
70 border-collapse: collapse;
71 table-layout: fixed;
72 border: 1px solid #d5dbe3;
73 border-radius: 4px;
74 overflow: hidden;
75 }
76
77 th,
78 td {
79 padding: 5px 6px;
80 vertical-align: top;
81 border-bottom: 1px solid #e5e9ef;
82 }
83
84 tr:last-child td {
85 border-bottom: 0;
86 }
87
88 th {
89 color: #4d5662;
90 background: #f4f6f8;
91 font-weight: 650;
92 text-align: left;
93 font-size: 9px;
94 text-transform: uppercase;
95 }
96
97 th:first-child,
98 td:first-child {
99 width: 33%;
100 }
101
102 code {
103 display: inline-block;
104 padding: 1px 4px;
105 color: #0f1720;
106 background: #eef2f7;
107 border: 1px solid #dce3ec;
108 border-radius: 3px;
109 font-family: "JetBrains Mono", "SFMono-Regular", Consolas, monospace;
110 font-size: 9.5px;
111 line-height: 1.25;
112 white-space: nowrap;
113 }
114
115 .note {
116 margin: 10px 0 0;
117 padding: 7px 8px;
118 color: #313942;
119 background: #fff7e1;
120 border: 1px solid #ead48a;
121 border-radius: 4px;
122 font-size: 10px;
123 }
124
125 .wide {
126 grid-column: 1 / -1;
127 }
128 </style>
129</head>
130<body>
131 <header>
132 <h1>IdeaVim Cheatsheet</h1>
133 <p class="subtitle">Leader key: <code>Space</code>. Example: <code>&lt;leader&gt;ff</code> means <code>Space f f</code>.</p>
134 </header>
135
136 <main class="grid">
137 <section>
138 <h2>Navigation</h2>
139 <table>
140 <thead><tr><th>Key</th><th>Action</th></tr></thead>
141 <tbody>
142 <tr><td><code>gd</code></td><td>Go to declaration</td></tr>
143 <tr><td><code>gi</code></td><td>Go to implementation</td></tr>
144 <tr><td><code>gr</code></td><td>Find usages</td></tr>
145 <tr><td><code>gb</code></td><td>Go back</td></tr>
146 <tr><td><code>gB</code></td><td>Go forward</td></tr>
147 </tbody>
148 </table>
149 </section>
150
151 <section>
152 <h2>Files And Search</h2>
153 <table>
154 <thead><tr><th>Key</th><th>Action</th></tr></thead>
155 <tbody>
156 <tr><td><code>Space ff</code></td><td>Go to file</td></tr>
157 <tr><td><code>Space fs</code></td><td>Find in project</td></tr>
158 <tr><td><code>Space fa</code></td><td>Go to IDE action</td></tr>
159 <tr><td><code>Space fr</code></td><td>Recent files</td></tr>
160 <tr><td><code>Space e</code></td><td>Select current file in project view</td></tr>
161 </tbody>
162 </table>
163 </section>
164
165 <section>
166 <h2>Code Actions</h2>
167 <table>
168 <thead><tr><th>Key</th><th>Action</th></tr></thead>
169 <tbody>
170 <tr><td><code>Space ca</code></td><td>Show intention actions / quick fix</td></tr>
171 <tr><td><code>Space cf</code></td><td>Reformat code</td></tr>
172 <tr><td><code>Space co</code></td><td>Optimize imports</td></tr>
173 <tr><td><code>Space R</code></td><td>Rename symbol</td></tr>
174 </tbody>
175 </table>
176 </section>
177
178 <section>
179 <h2>Run And Debug</h2>
180 <table>
181 <thead><tr><th>Key</th><th>Action</th></tr></thead>
182 <tbody>
183 <tr><td><code>Space rr</code></td><td>Run</td></tr>
184 <tr><td><code>Space dd</code></td><td>Debug</td></tr>
185 <tr><td><code>Space db</code></td><td>Toggle breakpoint</td></tr>
186 </tbody>
187 </table>
188 </section>
189
190 <section>
191 <h2>Tool Windows</h2>
192 <table>
193 <thead><tr><th>Key</th><th>Action</th></tr></thead>
194 <tbody>
195 <tr><td><code>Space tt</code></td><td>Open terminal</td></tr>
196 <tr><td><code>Space hh</code></td><td>Hide all tool windows</td></tr>
197 </tbody>
198 </table>
199 </section>
200
201 <section>
202 <h2>Completion</h2>
203 <table>
204 <thead><tr><th>Key</th><th>Action</th></tr></thead>
205 <tbody>
206 <tr><td><code>Tab</code></td><td>Next completion item</td></tr>
207 <tr><td><code>Shift+Tab</code></td><td>Previous completion item</td></tr>
208 </tbody>
209 </table>
210 </section>
211
212 <section class="wide">
213 <h2>Surround</h2>
214 <table>
215 <thead><tr><th>Key</th><th>Example / Meaning</th></tr></thead>
216 <tbody>
217 <tr><td><code>cs"'</code></td><td>Change <code>"hello"</code> to <code>'hello'</code></td></tr>
218 <tr><td><code>cs({</code></td><td>Change <code>(x)</code> to <code>{ x }</code></td></tr>
219 <tr><td><code>ds"</code></td><td>Delete surrounding quotes from <code>"hello"</code></td></tr>
220 <tr><td><code>ysiw"</code></td><td>Surround current word with quotes</td></tr>
221 <tr><td><code>yss)</code></td><td>Surround the whole line with parentheses</td></tr>
222 </tbody>
223 </table>
224 </section>
225
226 <section>
227 <h2>Text Objects</h2>
228 <table>
229 <thead><tr><th>Key</th><th>Action</th></tr></thead>
230 <tbody>
231 <tr><td><code>ciq</code></td><td>Change inside any quote</td></tr>
232 <tr><td><code>caq</code></td><td>Change around any quote</td></tr>
233 <tr><td><code>cib</code></td><td>Change inside any bracket</td></tr>
234 <tr><td><code>cab</code></td><td>Change around any bracket</td></tr>
235 <tr><td><code>diq</code></td><td>Delete inside any quote</td></tr>
236 <tr><td><code>dib</code></td><td>Delete inside any bracket</td></tr>
237 <tr><td><code>viq</code></td><td>Select inside any quote</td></tr>
238 <tr><td><code>vib</code></td><td>Select inside any bracket</td></tr>
239 </tbody>
240 </table>
241 </section>
242
243 <section>
244 <h2>Multi-Cursor</h2>
245 <table>
246 <thead><tr><th>Key</th><th>Action</th></tr></thead>
247 <tbody>
248 <tr><td><code>Ctrl+n</code></td><td>Select next occurrence of word</td></tr>
249 <tr><td><code>g Ctrl+n</code></td><td>Select next occurrence, not whole word</td></tr>
250 <tr><td><code>Alt+n</code></td><td>Select all occurrences in file</td></tr>
251 <tr><td><code>Ctrl+x</code></td><td>Skip current occurrence</td></tr>
252 <tr><td><code>Ctrl+p</code></td><td>Remove current occurrence</td></tr>
253 </tbody>
254 </table>
255 </section>
256
257 <section class="wide">
258 <h2>Reload Config</h2>
259 <table>
260 <thead><tr><th>Key / Command</th><th>Action</th></tr></thead>
261 <tbody>
262 <tr><td><code>Ctrl+Shift+O</code></td><td>Reload <code>~/.ideavimrc</code> from JetBrains</td></tr>
263 <tr><td><code>:source ~/.ideavimrc</code></td><td>Reload <code>~/.ideavimrc</code> from Vim command mode</td></tr>
264 </tbody>
265 </table>
266 <p class="note">EasyMotion requires the JetBrains IdeaVim-EasyMotion and AceJump IDE plugins.</p>
267 </section>
268 </main>
269</body>
270</html>
271
ideavimrc Originalformat
1" .ideavimrc is a configuration file for IdeaVim plugin. It uses
2" the same commands as the original .vimrc configuration.
3" You can find a list of commands here: https://jb.gg/h38q75
4" Find more examples here: https://jb.gg/share-ideavimrc
5
6" Source your .vimrc
7source ~/.vimrc
8
9"" -- Suggested options --
10" Show a few lines of context around the cursor. Note that this makes the
11" text scroll if you mouse-click near the start or end of the window.
12set scrolloff=5
13
14" Do incremental searching.
15set incsearch
16
17" Don't use Ex mode, use Q for formatting.
18map Q gq
19
20" --- Enable IdeaVim plugins https://jb.gg/ideavim-plugins
21
22" Highlight copied text
23Plug 'machakann/vim-highlightedyank'
24" Commentary plugin
25Plug 'tpope/vim-commentary'
26" Surround text objects and changes
27Plug 'tpope/vim-surround'
28" Better text objects around quotes, brackets, arguments, and separators
29Plug 'wellle/targets.vim'
30" Fast cursor jumps. Requires the IdeaVim-EasyMotion and AceJump IDE plugins.
31Plug 'easymotion/vim-easymotion'
32" Multi-cursor editing
33Plug 'terryma/vim-multiple-cursors'
34
35" --- Better IdeaVim / IDE integration ---
36set ideajoin
37set clipboard+=unnamedplus
38set ideamarks
39
40" --- Optional built-in IdeaVim extensions ---
41set mini-ai
42set youcompleteme
43
44" --- IDE navigation ---
45nmap gd <Action>(GotoDeclaration)
46nmap gi <Action>(GotoImplementation)
47nmap gr <Action>(FindUsages)
48nmap gb <Action>(Back)
49nmap gB <Action>(Forward)
50
51" --- Search / files / project ---
52nmap <leader>ff <Action>(GotoFile)
53nmap <leader>fs <Action>(FindInPath)
54nmap <leader>fa <Action>(GotoAction)
55nmap <leader>fr <Action>(RecentFiles)
56nmap <leader>e <Action>(SelectInProjectView)
57
58" --- Code actions ---
59nmap <leader>ca <Action>(ShowIntentionActions)
60nmap <leader>cf <Action>(ReformatCode)
61nmap <leader>co <Action>(OptimizeImports)
62nmap <leader>R <Action>(RenameElement)
63
64" --- Run / debug ---
65nmap <leader>rr <Action>(Run)
66nmap <leader>dd <Action>(Debug)
67nmap <leader>db <Action>(ToggleLineBreakpoint)
68
69" --- Tool windows ---
70nmap <leader>tt <Action>(ActivateTerminalToolWindow)
71nmap <leader>hh <Action>(HideAllWindows)
72
73
74"" -- Map IDE actions to IdeaVim -- https://jb.gg/abva4t
75"" Map \r to the Reformat Code action
76"map \r <Action>(ReformatCode)
77
78"" Map <leader>d to start debug
79"map <leader>d <Action>(Debug)
80
81"" Map \b to toggle the breakpoint on the current line
82"map \b <Action>(ToggleLineBreakpoint)
83
install-jetbrains.ps1 Originalformat
1<#
2.SYNOPSIS
3 Install or uninstall JetBrains plugins in locally installed IDEs (and in
4 the 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 select IDE(s), choose install or uninstall, and then select plugin(s).
13 Standard IDEs get "installPlugins <id>..." invoked once with all selected
14 plugins. Uninstall reads plugin IDs from the selected IDEs' user plugin
15 directories and removes only the entries selected by the user. The
16 Gateway Client path is special: the bundled client has no installPlugins
17 CLI, so plugins are downloaded directly from plugins.jetbrains.com and
18 unpacked into the client's plugins folder (mirrors the user's
19 plugins_gateway.ps1).
20
21.NOTES
22 Process-scope ExecutionPolicy is set to RemoteSigned. No machine-wide
23 change is persisted. If PowerShell still refuses to run the file:
24 powershell -ExecutionPolicy Bypass -File .\install-jetbrains.ps1
25#>
26
27[CmdletBinding()]
28param()
29
30# Loosen execution policy for THIS process only.
31try {
32 $current = Get-ExecutionPolicy -Scope Process -ErrorAction Stop
33 if ($current -in @('Restricted', 'AllSigned', 'Undefined')) {
34 Set-ExecutionPolicy -Scope Process -ExecutionPolicy RemoteSigned -Force -ErrorAction Stop
35 }
36} catch {
37 Write-Verbose ("Could not adjust process execution policy: {0}" -f $_.Exception.Message)
38}
39
40$ErrorActionPreference = 'Stop'
41[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
42try {
43 Add-Type -AssemblyName System.IO.Compression.FileSystem -ErrorAction Stop
44} catch {
45 Write-Verbose ("Could not preload ZIP support: {0}" -f $_.Exception.Message)
46}
47
48# ------------------------------------------------------------------
49# Canonical plugin catalog. Each entry: @{ Id; Label }
50# ------------------------------------------------------------------
51$PluginCatalog = @(
52 @{ Id = 'com.intellij.ml.llm'; Label = 'JetBrains AI Assistant' }
53 @{ Id = 'izhangzhihao.rainbow.brackets'; Label = 'Rainbow Brackets' }
54 @{ Id = 'IdeaVIM'; Label = 'IdeaVim' }
55 @{ Id = 'org.sonarlint.idea'; Label = 'SonarQube for IDE (SonarLint)' }
56 @{ Id = 'Key Promoter X'; Label = 'Key Promoter X' }
57 @{ Id = 'net.ashald.envfile'; Label = 'EnvFile' }
58 @{ Id = 'org.intellij.qodana'; Label = 'Qodana' }
59)
60
61$KnownLauncherStems = @(
62 'idea64', 'pycharm64', 'webstorm64', 'goland64', 'rubymine64',
63 'clion64', 'phpstorm64', 'datagrip64', 'rustrover64', 'rider64',
64 'studio64', 'fleet'
65)
66
67function Get-PrettyLabel {
68 param([string]$Stem)
69 switch -Regex ($Stem) {
70 '^idea' { return 'IntelliJ IDEA' }
71 '^pycharm' { return 'PyCharm' }
72 '^webstorm' { return 'WebStorm' }
73 '^goland' { return 'GoLand' }
74 '^rubymine' { return 'RubyMine' }
75 '^clion' { return 'CLion' }
76 '^phpstorm' { return 'PhpStorm' }
77 '^datagrip' { return 'DataGrip' }
78 '^rustrover' { return 'RustRover' }
79 '^rider' { return 'Rider' }
80 '^studio' { return 'Android Studio' }
81 '^fleet' { return 'Fleet' }
82 default { return $Stem }
83 }
84}
85
86# ------------------------------------------------------------------
87# IDE candidate scan
88# Each candidate: PSCustomObject @{ Type; Launcher; Label; Path; Client }
89# Type: 'Ide' (run launcher installPlugins ...)
90# | 'WinCmd' (run .cmd shim via cmd.exe)
91# | 'Gateway' (download-and-unzip flow into $Client\plugins)
92# ------------------------------------------------------------------
93$Candidates = New-Object System.Collections.Generic.List[object]
94
95# 1) Standalone & Toolbox-app installs under %LOCALAPPDATA%\Programs\*\bin\*.exe
96$programs = Join-Path $env:LOCALAPPDATA 'Programs'
97if (Test-Path $programs) {
98 Get-ChildItem -Path $programs -Directory -ErrorAction SilentlyContinue | ForEach-Object {
99 $binDir = Join-Path $_.FullName 'bin'
100 if (-not (Test-Path $binDir)) { return }
101 Get-ChildItem -Path $binDir -Filter '*.exe' -File -ErrorAction SilentlyContinue | ForEach-Object {
102 $stem = [System.IO.Path]::GetFileNameWithoutExtension($_.Name)
103 if ($KnownLauncherStems -notcontains $stem) { return }
104 $Candidates.Add([pscustomobject]@{
105 Type = 'Ide'
106 Launcher = $_.FullName
107 Label = ("{0} ({1})" -f (Get-PrettyLabel $stem), $_.FullName)
108 Path = $_.FullName
109 Client = $null
110 })
111 }
112 }
113}
114
115# 2) Toolbox .cmd shims
116$tbScripts = Join-Path $env:LOCALAPPDATA 'JetBrains\Toolbox\scripts'
117if (Test-Path $tbScripts) {
118 Get-ChildItem -Path $tbScripts -Filter '*.cmd' -File -ErrorAction SilentlyContinue | ForEach-Object {
119 $stem = [System.IO.Path]::GetFileNameWithoutExtension($_.Name)
120 # Toolbox shims keep names like "idea.cmd", "rider.cmd" — strip trailing 64 etc.
121 $Candidates.Add([pscustomobject]@{
122 Type = 'WinCmd'
123 Launcher = $_.FullName
124 Label = ("{0} ({1})" -f (Get-PrettyLabel $stem), $_.FullName)
125 Path = $_.FullName
126 Client = $null
127 })
128 }
129}
130
131# 3) JetBrains Gateway Client — pick the most-recently-modified one if any.
132$jbRoot = Join-Path $env:LOCALAPPDATA 'JetBrains'
133if (Test-Path $jbRoot) {
134 $client = Get-ChildItem -Path $jbRoot -Directory -Filter 'JetBrainsClient*' -ErrorAction SilentlyContinue |
135 Sort-Object LastWriteTime -Descending | Select-Object -First 1
136 if ($client) {
137 $Candidates.Add([pscustomobject]@{
138 Type = 'Gateway'
139 Launcher = $client.FullName
140 Label = ("JetBrains Gateway Client ({0})" -f $client.FullName)
141 Path = $client.FullName
142 Client = $client.FullName
143 })
144 }
145}
146
147if ($Candidates.Count -eq 0) {
148 Write-Host ""
149 Write-Host "No JetBrains IDEs or Gateway clients found." -ForegroundColor Red
150 Write-Host "Install one via the JetBrains Toolbox, then re-run." -ForegroundColor Yellow
151 exit 1
152}
153
154# ------------------------------------------------------------------
155# Multi-select picker — prefer Out-GridView when available.
156# ------------------------------------------------------------------
157function Select-Multi {
158 param(
159 [Parameter(Mandatory)] [string]$Title,
160 [Parameter(Mandatory)] [array]$Items,
161 [Parameter(Mandatory)] [string]$Property
162 )
163
164 $hasOgv = $false
165 try {
166 $cmd = Get-Command Out-GridView -ErrorAction Stop
167 if ($cmd) { $hasOgv = $true }
168 } catch { $hasOgv = $false }
169
170 if ($hasOgv) {
171 $chosen = $Items | Out-GridView -Title $Title -OutputMode Multiple
172 return ,@($chosen)
173 }
174
175 # Numbered-prompt fallback.
176 Write-Host ""
177 Write-Host $Title -ForegroundColor Cyan
178 for ($i = 0; $i -lt $Items.Count; $i++) {
179 Write-Host (" {0,2}) {1}" -f ($i + 1), $Items[$i].$Property)
180 }
181 while ($true) {
182 $raw = Read-Host "Select (e.g. 1,3,5 or a for all)"
183 if ([string]::IsNullOrWhiteSpace($raw)) {
184 Write-Host " please enter at least one number, or 'a'." -ForegroundColor Yellow
185 continue
186 }
187 if ($raw.Trim().ToLower() -eq 'a') {
188 return ,@($Items)
189 }
190 $idxs = $raw -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne '' }
191 $bad = $false
192 $sel = @()
193 foreach ($t in $idxs) {
194 if ($t -notmatch '^[0-9]+$') { $bad = $true; break }
195 $n = [int]$t
196 if ($n -lt 1 -or $n -gt $Items.Count) { $bad = $true; break }
197 $sel += $Items[$n - 1]
198 }
199 if ($bad -or $sel.Count -eq 0) {
200 Write-Host " invalid input; try again." -ForegroundColor Yellow
201 continue
202 }
203 return ,@($sel)
204 }
205}
206
207function Select-Action {
208 while ($true) {
209 Write-Host ""
210 Write-Host "What do you want to do?" -ForegroundColor Cyan
211 Write-Host " 1) Install plugins"
212 Write-Host " 2) Uninstall plugins"
213 $choice = Read-Host "Action (1 or 2)"
214 switch ($choice.Trim().ToLower()) {
215 { $_ -in @('1', 'i', 'install') } { return 'Install' }
216 { $_ -in @('2', 'u', 'uninstall') } { return 'Uninstall' }
217 default { Write-Host " invalid input; enter 1 for install or 2 for uninstall." -ForegroundColor Yellow }
218 }
219 }
220}
221
222# Find product-info.json for the selected launcher and return the IDE's
223# dataDirectoryName. This prevents uninstall from touching unrelated profiles.
224function Get-ProductDataName {
225 param([Parameter(Mandatory)] [psobject]$Ide)
226
227 if ($Ide.Type -eq 'Gateway') { return $null }
228 $probe = $Ide.Launcher
229
230 # Toolbox .cmd shims normally contain the real launcher path.
231 if ($Ide.Type -eq 'WinCmd' -and (Test-Path -LiteralPath $probe)) {
232 try {
233 $cmdText = Get-Content -LiteralPath $probe -Raw -ErrorAction Stop
234 $match = [regex]::Match($cmdText, '"([^"\r\n]+\.exe)"', [System.Text.RegularExpressions.RegexOptions]::IgnoreCase)
235 if ($match.Success) {
236 $expanded = [Environment]::ExpandEnvironmentVariables($match.Groups[1].Value)
237 if (Test-Path -LiteralPath $expanded) { $probe = $expanded }
238 }
239 } catch {
240 Write-Verbose ("Could not resolve Toolbox shim {0}: {1}" -f $probe, $_.Exception.Message)
241 }
242 }
243
244 try {
245 $item = Get-Item -LiteralPath $probe -ErrorAction Stop
246 $dir = if ($item.PSIsContainer) { $item.FullName } else { $item.Directory.FullName }
247 } catch { return $null }
248
249 while ($dir) {
250 foreach ($relative in @('product-info.json', 'Resources\product-info.json')) {
251 $infoPath = Join-Path $dir $relative
252 if (-not (Test-Path -LiteralPath $infoPath)) { continue }
253 try {
254 $info = Get-Content -LiteralPath $infoPath -Raw -ErrorAction Stop | ConvertFrom-Json
255 if ($info.dataDirectoryName) { return [string]$info.dataDirectoryName }
256 } catch {
257 Write-Verbose ("Could not read {0}: {1}" -f $infoPath, $_.Exception.Message)
258 }
259 }
260 $parent = [System.IO.Directory]::GetParent($dir)
261 if (-not $parent) { break }
262 $dir = $parent.FullName
263 }
264 return $null
265}
266
267function Get-PluginRoot {
268 param([Parameter(Mandatory)] [psobject]$Ide)
269 if ($Ide.Type -eq 'Gateway') { return (Join-Path $Ide.Client 'plugins') }
270
271 $dataName = Get-ProductDataName -Ide $Ide
272 if (-not $dataName) { return $null }
273 return (Join-Path (Join-Path $env:APPDATA 'JetBrains') (Join-Path $dataName 'plugins'))
274}
275
276function ConvertFrom-PluginXml {
277 param([Parameter(Mandatory)] [string]$Text)
278 try {
279 $settings = New-Object System.Xml.XmlReaderSettings
280 $settings.DtdProcessing = [System.Xml.DtdProcessing]::Prohibit
281 $stringReader = New-Object System.IO.StringReader($Text)
282 $reader = [System.Xml.XmlReader]::Create($stringReader, $settings)
283 $doc = New-Object System.Xml.XmlDocument
284 $doc.XmlResolver = $null
285 $doc.Load($reader)
286 $reader.Dispose()
287 $stringReader.Dispose()
288
289 $idNode = $doc.SelectSingleNode('/idea-plugin/id')
290 $nameNode = $doc.SelectSingleNode('/idea-plugin/name')
291 $id = if ($idNode) { $idNode.InnerText.Trim() } elseif ($nameNode) { $nameNode.InnerText.Trim() } else { $null }
292 $name = if ($nameNode) { $nameNode.InnerText.Trim() } else { $id }
293 if ($id) { return [pscustomobject]@{ Id = $id; Label = $name } }
294 } catch {
295 Write-Verbose ("Could not parse plugin.xml: {0}" -f $_.Exception.Message)
296 }
297 return $null
298}
299
300function Get-ArchivePluginDescriptor {
301 param([Parameter(Mandatory)] [string]$Path)
302 $zip = $null
303 try {
304 $zip = [System.IO.Compression.ZipFile]::OpenRead($Path)
305 $entry = $zip.Entries | Where-Object { $_.FullName -eq 'META-INF/plugin.xml' } | Select-Object -First 1
306 if (-not $entry) { return $null }
307 $stream = $entry.Open()
308 $reader = New-Object System.IO.StreamReader($stream)
309 $text = $reader.ReadToEnd()
310 $reader.Dispose()
311 $stream.Dispose()
312 return (ConvertFrom-PluginXml -Text $text)
313 } catch {
314 Write-Verbose ("Could not inspect plugin archive {0}: {1}" -f $Path, $_.Exception.Message)
315 return $null
316 } finally {
317 if ($zip) { $zip.Dispose() }
318 }
319}
320
321function Get-PluginDescriptor {
322 param([Parameter(Mandatory)] [System.IO.FileSystemInfo]$Item)
323
324 if (-not $Item.PSIsContainer) {
325 return (Get-ArchivePluginDescriptor -Path $Item.FullName)
326 }
327
328 $looseXml = Join-Path $Item.FullName 'META-INF\plugin.xml'
329 if (Test-Path -LiteralPath $looseXml) {
330 try {
331 return (ConvertFrom-PluginXml -Text (Get-Content -LiteralPath $looseXml -Raw -ErrorAction Stop))
332 } catch {
333 Write-Verbose ("Could not inspect {0}: {1}" -f $looseXml, $_.Exception.Message)
334 }
335 }
336
337 $archives = Get-ChildItem -LiteralPath $Item.FullName -Recurse -File -ErrorAction SilentlyContinue |
338 Where-Object { $_.Extension -in @('.jar', '.zip') }
339 foreach ($archive in $archives) {
340 $descriptor = Get-ArchivePluginDescriptor -Path $archive.FullName
341 if ($descriptor) { return $descriptor }
342 }
343 return $null
344}
345
346function Get-InstalledPlugins {
347 param([Parameter(Mandatory)] [psobject]$Ide)
348 $root = Get-PluginRoot -Ide $Ide
349 if (-not $root) {
350 Write-Host ("Could not resolve the plugin directory for: {0}" -f $Ide.Label) -ForegroundColor Yellow
351 return
352 }
353 if (-not (Test-Path -LiteralPath $root)) { return }
354
355 Get-ChildItem -LiteralPath $root -Force -ErrorAction SilentlyContinue | ForEach-Object {
356 $descriptor = Get-PluginDescriptor -Item $_
357 if ($descriptor) {
358 [pscustomobject]@{
359 Id = $descriptor.Id
360 Label = $descriptor.Label
361 Path = $_.FullName
362 PluginRoot = $root
363 TargetLabel = $Ide.Label
364 }
365 }
366 }
367}
368
369Write-Host ("Detected {0} JetBrains target(s)." -f $Candidates.Count) -ForegroundColor Green
370
371$selectedIdes = Select-Multi -Title "Pick IDE(s)" -Items $Candidates -Property 'Label'
372if ($selectedIdes.Count -eq 0) {
373 Write-Host "No IDEs selected; exiting." -ForegroundColor Red
374 exit 1
375}
376
377$action = Select-Action
378$installedPluginInstances = @()
379
380if ($action -eq 'Install') {
381 $pluginChoices = $PluginCatalog
382} else {
383 $installedPluginInstances = @($selectedIdes |
384 ForEach-Object { Get-InstalledPlugins -Ide $_ } |
385 Group-Object Path |
386 ForEach-Object { $_.Group[0] })
387 $pluginChoices = @($installedPluginInstances |
388 Group-Object Id |
389 ForEach-Object { [pscustomobject]@{ Id = $_.Name; Label = $_.Group[0].Label } } |
390 Sort-Object Label)
391
392 if ($pluginChoices.Count -eq 0) {
393 Write-Host "No removable user-installed plugins were found for the selected IDE(s)." -ForegroundColor Yellow
394 Write-Host "Bundled plugins cannot be uninstalled; disable them in the IDE instead." -ForegroundColor Yellow
395 exit 0
396 }
397}
398
399$selectedPlugins = Select-Multi -Title ("Pick plugin(s) to {0}" -f $action.ToLower()) -Items $pluginChoices -Property 'Label'
400if ($selectedPlugins.Count -eq 0) {
401 Write-Host "No plugins selected; exiting." -ForegroundColor Red
402 exit 1
403}
404
405# ------------------------------------------------------------------
406# Running-IDE preflight
407# Installing or deleting plugin files requires the selected IDE to be closed.
408# installPlugins refuses to run while the IDE GUI holds the config-dir lock
409# ("Only one instance of IDEA can be run at a time."). Best-effort detection
410# via Get-Process. Gateway Client type doesn't need this (no CLI invoked).
411# ------------------------------------------------------------------
412function Test-IdeRunning {
413 param([Parameter(Mandatory)] [psobject]$Ide)
414 if ($Ide.Type -eq 'Gateway') {
415 if ($action -eq 'Install') { return $false }
416 foreach ($name in @('jetbrains_client64', 'jetbrains_client', 'JetBrainsClient')) {
417 if (Get-Process -Name $name -ErrorAction SilentlyContinue) { return $true }
418 }
419 return $false
420 }
421
422 # For standalone IDE launchers the process name is the exe basename.
423 # For Toolbox .cmd shims, the spawned process is e.g. idea64 -> use prefix.
424 $stem = [System.IO.Path]::GetFileNameWithoutExtension($Ide.Launcher)
425 $patterns = @($stem)
426 if ($stem -notmatch '64$') { $patterns += ($stem + '64') }
427
428 foreach ($pat in $patterns) {
429 if (Get-Process -Name $pat -ErrorAction SilentlyContinue) { return $true }
430 }
431 return $false
432}
433
434while ($true) {
435 $running = @($selectedIdes | Where-Object { Test-IdeRunning $_ })
436 if ($running.Count -eq 0) { break }
437
438 Write-Host ""
439 Write-Host "The following IDE(s) appear to be running — plugin changes need them closed:" -ForegroundColor Yellow
440 foreach ($r in $running) { Write-Host (" - {0}" -f $r.Label) -ForegroundColor Yellow }
441 Write-Host ""
442 Read-Host "Close them, then press Enter to retry (Ctrl+C to abort)" | Out-Null
443}
444
445# ------------------------------------------------------------------
446# Uninstall or install
447# ------------------------------------------------------------------
448if ($action -eq 'Uninstall') {
449 $selectedIds = @($selectedPlugins | ForEach-Object { $_.Id })
450 $toRemove = @($installedPluginInstances | Where-Object { $_.Id -in $selectedIds } | Where-Object {
451 $parent = [System.IO.Directory]::GetParent($_.Path)
452 $parent -and ($parent.FullName.TrimEnd('\') -eq $_.PluginRoot.TrimEnd('\'))
453 })
454
455 if ($toRemove.Count -eq 0) {
456 Write-Host "None of the selected plugins are installed in the selected IDE(s)." -ForegroundColor Yellow
457 exit 0
458 }
459
460 Write-Host ""
461 Write-Host "The following plugin installations will be removed:" -ForegroundColor Yellow
462 foreach ($plugin in $toRemove) {
463 Write-Host (" - {0} ({1})" -f $plugin.Label, $plugin.Id)
464 Write-Host (" IDE: {0}" -f $plugin.TargetLabel)
465 Write-Host (" Path: {0}" -f $plugin.Path)
466 }
467 $confirm = Read-Host "Continue? [y/N]"
468 if ($confirm -notmatch '^[Yy]$') {
469 Write-Host "Uninstall cancelled."
470 exit 0
471 }
472
473 $removed = 0
474 $failed = 0
475 $failedList = @()
476 foreach ($plugin in $toRemove) {
477 Write-Host -NoNewline ("Removing {0} from {1}... " -f $plugin.Label, $plugin.TargetLabel)
478 try {
479 Remove-Item -LiteralPath $plugin.Path -Recurse -Force -ErrorAction Stop
480 Write-Host "ok" -ForegroundColor Green
481 $removed++
482 } catch {
483 Write-Host "FAILED" -ForegroundColor Red
484 Write-Host (" {0}" -f $_.Exception.Message) -ForegroundColor DarkRed
485 $failed++
486 $failedList += ("{0} :: {1}" -f $plugin.TargetLabel, $plugin.Id)
487 }
488 }
489
490 Write-Host ("Done. Removed: {0}, Failed: {1}. Restart the IDE(s) to finish." -f $removed, $failed)
491 if ($failed -gt 0) {
492 foreach ($entry in $failedList) { Write-Host (" - {0}" -f $entry) -ForegroundColor Red }
493 exit 1
494 }
495 exit 0
496}
497
498$installed = 0
499$failed = 0
500$failedList = @()
501
502Write-Host ""
503Write-Host ("Installing {0} plugin(s) into {1} IDE(s)..." -f $selectedPlugins.Count, $selectedIdes.Count) -ForegroundColor Cyan
504Write-Host ""
505
506foreach ($ide in $selectedIdes) {
507 Write-Host (">>> {0}" -f $ide.Label)
508
509 switch ($ide.Type) {
510
511 # ---- standard IDE launcher ----
512 'Ide' {
513 $args = @('installPlugins') + ($selectedPlugins | ForEach-Object { $_.Id })
514 try {
515 $output = & $ide.Launcher @args 2>&1
516 if ($LASTEXITCODE -eq 0) {
517 Write-Host (" all {0} plugin(s) ok" -f $selectedPlugins.Count) -ForegroundColor Green
518 $installed += $selectedPlugins.Count
519 } else {
520 $joined = ($output | Out-String)
521 if ($joined -match 'Only one instance') {
522 Write-Host " IDE is running SKIPPED" -ForegroundColor Yellow
523 Write-Host " Close the IDE and re-run the script to finish." -ForegroundColor DarkYellow
524 } else {
525 Write-Host (" installPlugins exited {0} FAILED" -f $LASTEXITCODE) -ForegroundColor Red
526 $output | ForEach-Object { Write-Host " $_" -ForegroundColor DarkRed }
527 }
528 $failed += $selectedPlugins.Count
529 foreach ($p in $selectedPlugins) { $failedList += ("{0} :: {1}" -f $ide.Label, $p.Id) }
530 }
531 } catch {
532 Write-Host (" launcher invocation threw FAILED") -ForegroundColor Red
533 $failed += $selectedPlugins.Count
534 foreach ($p in $selectedPlugins) { $failedList += ("{0} :: {1}" -f $ide.Label, $p.Id) }
535 Write-Host (" {0}" -f $_.Exception.Message) -ForegroundColor DarkRed
536 }
537 }
538
539 # ---- Toolbox .cmd shim ----
540 'WinCmd' {
541 $idArgs = ($selectedPlugins | ForEach-Object { '"{0}"' -f $_.Id }) -join ' '
542 try {
543 $output = cmd.exe /c "`"$($ide.Launcher)`" installPlugins $idArgs" 2>&1
544 if ($LASTEXITCODE -eq 0) {
545 Write-Host (" all {0} plugin(s) ok" -f $selectedPlugins.Count) -ForegroundColor Green
546 $installed += $selectedPlugins.Count
547 } else {
548 $joined = ($output | Out-String)
549 if ($joined -match 'Only one instance') {
550 Write-Host " IDE is running SKIPPED" -ForegroundColor Yellow
551 Write-Host " Close the IDE and re-run the script to finish." -ForegroundColor DarkYellow
552 } else {
553 Write-Host (" installPlugins exited {0} FAILED" -f $LASTEXITCODE) -ForegroundColor Red
554 $output | ForEach-Object { Write-Host " $_" -ForegroundColor DarkRed }
555 }
556 $failed += $selectedPlugins.Count
557 foreach ($p in $selectedPlugins) { $failedList += ("{0} :: {1}" -f $ide.Label, $p.Id) }
558 }
559 } catch {
560 Write-Host (" cmd.exe invocation threw FAILED") -ForegroundColor Red
561 $failed += $selectedPlugins.Count
562 foreach ($p in $selectedPlugins) { $failedList += ("{0} :: {1}" -f $ide.Label, $p.Id) }
563 Write-Host (" {0}" -f $_.Exception.Message) -ForegroundColor DarkRed
564 }
565 }
566
567 # ---- Gateway Client ----
568 'Gateway' {
569 $client = $ide.Client
570 $buildFile = Join-Path $client 'build.txt'
571 if (-not (Test-Path $buildFile)) {
572 Write-Host " build.txt missing under client; cannot determine build id SKIPPED" -ForegroundColor Yellow
573 Write-Host " (open a Remote Project once to populate the client folder)" -ForegroundColor DarkYellow
574 $failed += $selectedPlugins.Count
575 foreach ($p in $selectedPlugins) { $failedList += ("{0} :: {1}" -f $ide.Label, $p.Id) }
576 continue
577 }
578 $buildId = (Get-Content $buildFile -Raw -ErrorAction Stop).Trim()
579
580 $pluginDir = Join-Path $client 'plugins'
581 if (-not (Test-Path $pluginDir)) {
582 New-Item -ItemType Directory -Path $pluginDir | Out-Null
583 }
584
585 $ua = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
586
587 foreach ($p in $selectedPlugins) {
588 Write-Host -NoNewline (" {0,-34} " -f $p.Id)
589 $encoded = [uri]::EscapeDataString($p.Id)
590 $url = "https://plugins.jetbrains.com/pluginManager/?action=download&id=$encoded&build=$buildId"
591 $tmp = Join-Path $env:TEMP ("jb-" + [guid]::NewGuid().ToString() + ".bin")
592 try {
593 Invoke-WebRequest -Uri $url -OutFile $tmp -UserAgent $ua -ErrorAction Stop -MaximumRedirection 5
594 $bytes = [System.IO.File]::ReadAllBytes($tmp) | Select-Object -First 2
595 if ($bytes.Count -ge 2 -and $bytes[0] -eq 0x50 -and $bytes[1] -eq 0x4B) {
596 Expand-Archive -Path $tmp -DestinationPath $pluginDir -Force -ErrorAction Stop
597 Write-Host "ok (zip)" -ForegroundColor Green
598 } else {
599 $jarName = Join-Path $pluginDir ($p.Id -replace '[^A-Za-z0-9._-]', '_') + '.jar'
600 Move-Item -Path $tmp -Destination $jarName -Force
601 Write-Host "ok (jar)" -ForegroundColor Green
602 }
603 $installed++
604 } catch {
605 Write-Host "FAILED" -ForegroundColor Red
606 Write-Host (" {0}" -f $_.Exception.Message) -ForegroundColor DarkRed
607 $failed++
608 $failedList += ("{0} :: {1}" -f $ide.Label, $p.Id)
609 } finally {
610 if (Test-Path $tmp) { Remove-Item $tmp -Force -ErrorAction SilentlyContinue }
611 }
612 }
613 Write-Host " Reopen the Gateway Client to load the new plugins." -ForegroundColor Yellow
614 }
615 }
616 Write-Host ""
617}
618
619Write-Host ("Done. Installed: {0}, Failed: {1}" -f $installed, $failed)
620if ($failed -gt 0) {
621 Write-Host "Failed:" -ForegroundColor Red
622 foreach ($f in $failedList) { Write-Host (" - {0}" -f $f) -ForegroundColor Red }
623 exit 1
624}
625
install-jetbrains.sh Originalformat
1#!/usr/bin/env bash
2#
3# JetBrains IDE plugin manager 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), choose install or uninstall, and then select the
8# plugins to manage.
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 "Key Promoter X|Key Promoter X"
33 "net.ashald.envfile|EnvFile"
34 "org.intellij.qodana|Qodana"
35)
36
37# Known JetBrains IDE launcher basenames (Toolbox shims + Linux .sh names).
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 manager 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 choose whether to install or uninstall plugins
64 4. Installs catalog plugins, or lists the selected IDEs' installed plugins
65 and lets you choose which ones to remove
66
67Use \`brew install fzf\` (or your distro's package manager) for a nicer picker.
68EOF
69}
70
71while [[ $# -gt 0 ]]; do
72 case "$1" in
73 -h|--help) usage; exit 0 ;;
74 *) echo "Error: unknown argument '$1'." >&2; usage >&2; exit 2 ;;
75 esac
76done
77
78# ------------------------------------------------------------------
79# OS detection
80# ------------------------------------------------------------------
81OS_KIND=""
82case "$(uname -s)" in
83 Darwin) OS_KIND=macos ;;
84 Linux)
85 if grep -qi microsoft /proc/version 2>/dev/null; then
86 OS_KIND=wsl2
87 else
88 OS_KIND=linux
89 fi
90 ;;
91 *)
92 echo "Error: unsupported OS '$(uname -s)'. Use install-jetbrains.ps1 on Windows." >&2
93 exit 1
94 ;;
95esac
96
97# ------------------------------------------------------------------
98# Helpers
99# ------------------------------------------------------------------
100
101# is_known_ide_name <basename> -> 0 if it matches a known JetBrains launcher.
102is_known_ide_name() {
103 local n="$1"
104 local k
105 for k in "${KNOWN_IDE_NAMES[@]}"; do
106 [[ "$n" == "$k" ]] && return 0
107 done
108 return 1
109}
110
111# pretty_from_path /path/to/idea.sh -> "IntelliJ IDEA (~/.local/share/.../idea.sh)"
112# Builds a short, readable label for the picker.
113pretty_label() {
114 local path="$1" base
115 base="$(basename "$path")"
116 base="${base%.sh}"
117 base="${base%.cmd}"
118 base="${base%.exe}"
119 case "$base" in
120 idea|idea64) printf 'IntelliJ IDEA' ;;
121 pycharm|pycharm64) printf 'PyCharm' ;;
122 webstorm|webstorm64) printf 'WebStorm' ;;
123 goland|goland64) printf 'GoLand' ;;
124 rubymine|rubymine64) printf 'RubyMine' ;;
125 clion|clion64) printf 'CLion' ;;
126 phpstorm|phpstorm64) printf 'PhpStorm' ;;
127 datagrip|datagrip64) printf 'DataGrip' ;;
128 rustrover|rustrover64) printf 'RustRover' ;;
129 rider|rider64) printf 'Rider' ;;
130 studio|studio64) printf 'Android Studio' ;;
131 fleet) printf 'Fleet' ;;
132 *) printf '%s' "$base" ;;
133 esac
134 printf ' (%s)' "$path"
135}
136
137# Translate a Linux/WSL home into the Windows-side user dir under /mnt/c.
138win_user_dir() {
139 local winuser
140 winuser="$(cmd.exe /c 'echo %USERNAME%' 2>/dev/null | tr -d '\r\n' || true)"
141 [[ -z "$winuser" ]] && winuser="$USER"
142 printf '/mnt/c/Users/%s' "$winuser"
143}
144
145# ------------------------------------------------------------------
146# IDE candidate scan
147# Each candidate is recorded as a single line:
148# <type>|<launcher>|<label>
149# where <type> is one of:
150# ide -> run "<launcher> installPlugins <id>..."
151# wincmd -> run "cmd.exe /c <launcher> installPlugins <id>..."
152# remotedev -> remote-dev-server.sh installPlugins ...
153# ------------------------------------------------------------------
154declare -a CANDIDATES=()
155
156add_candidate() {
157 local type="$1" launcher="$2"
158 CANDIDATES+=("${type}|${launcher}|$(pretty_label "$launcher")")
159}
160
161scan_macos() {
162 local d="$HOME/Library/Application Support/JetBrains/Toolbox/scripts"
163 if [[ -d "$d" ]]; then
164 local f
165 while IFS= read -r -d '' f; do
166 local base
167 base="$(basename "$f")"
168 is_known_ide_name "$base" || continue
169 [[ -x "$f" ]] && add_candidate ide "$f"
170 done < <(find "$d" -maxdepth 1 -type f -print0 2>/dev/null)
171 fi
172
173 local app bin
174 for app in /Applications/*.app; do
175 [[ -d "$app/Contents/MacOS" ]] || continue
176 [[ "$(basename "$app")" =~ ^(IntelliJ|PyCharm|WebStorm|GoLand|RubyMine|CLion|PhpStorm|DataGrip|RustRover|Rider|Android\ Studio|Fleet) ]] || continue
177 for bin in "$app"/Contents/MacOS/*; do
178 [[ -x "$bin" ]] || continue
179 local b
180 b="$(basename "$bin")"
181 is_known_ide_name "$b" || continue
182 add_candidate ide "$bin"
183 done
184 done
185}
186
187scan_linux() {
188 local apps="$HOME/.local/share/JetBrains/Toolbox/apps"
189 if [[ -d "$apps" ]]; then
190 local f
191 while IFS= read -r -d '' f; do
192 local b
193 b="$(basename "$f")"
194 is_known_ide_name "$b" || continue
195 [[ -x "$f" ]] && add_candidate ide "$f"
196 done < <(find "$apps" -maxdepth 3 -type f -name '*.sh' -print0 2>/dev/null)
197 fi
198
199 # /opt/<ide>/bin/<ide>.sh — common for .deb / snap / tarball installs.
200 local opt
201 for opt in /opt/*/bin/*.sh; do
202 [[ -x "$opt" ]] || continue
203 local b
204 b="$(basename "$opt")"
205 is_known_ide_name "$b" || continue
206 add_candidate ide "$opt"
207 done
208
209 # Remote-dev-server distributions (Orbstack/Gateway).
210 local rd
211 while IFS= read -r -d '' rd; do
212 add_candidate remotedev "$rd"
213 done < <(find "$HOME/.cache/JetBrains/RemoteDev/dist" -maxdepth 3 -type f -name 'remote-dev-server.sh' -print0 2>/dev/null)
214}
215
216scan_wsl2() {
217 local winhome
218 winhome="$(win_user_dir)"
219 [[ -d "$winhome" ]] || return 0
220
221 # Standalone installs under AppData\Local\Programs\<IDE>\bin\*.exe
222 local exe b
223 while IFS= read -r -d '' exe; do
224 b="$(basename "$exe")"
225 case "$b" in
226 idea64.exe|pycharm64.exe|webstorm64.exe|goland64.exe|rubymine64.exe|\
227 clion64.exe|phpstorm64.exe|datagrip64.exe|rustrover64.exe|rider64.exe|\
228 studio64.exe|fleet.exe)
229 add_candidate ide "$exe"
230 ;;
231 esac
232 done < <(find "$winhome/AppData/Local/Programs" -maxdepth 4 -type f -name '*.exe' -print0 2>/dev/null)
233
234 # Toolbox shell shims (.cmd) — invoke via cmd.exe.
235 local scripts="$winhome/AppData/Local/JetBrains/Toolbox/scripts"
236 if [[ -d "$scripts" ]]; then
237 local f
238 while IFS= read -r -d '' f; do
239 b="$(basename "${f%.cmd}")"
240 is_known_ide_name "$b" || continue
241 add_candidate wincmd "$f"
242 done < <(find "$scripts" -maxdepth 1 -type f -name '*.cmd' -print0 2>/dev/null)
243 fi
244
245 # Local Linux-side remote-dev-server.sh (still useful inside WSL2).
246 local rd
247 while IFS= read -r -d '' rd; do
248 add_candidate remotedev "$rd"
249 done < <(find "$HOME/.cache/JetBrains/RemoteDev/dist" -maxdepth 3 -type f -name 'remote-dev-server.sh' -print0 2>/dev/null)
250}
251
252case "$OS_KIND" in
253 macos) scan_macos ;;
254 linux) scan_linux ;;
255 wsl2) scan_wsl2 ;;
256esac
257
258if [[ ${#CANDIDATES[@]} -eq 0 ]]; then
259 echo "No JetBrains IDEs detected on this system." >&2
260 echo "Install one via the JetBrains Toolbox, then re-run." >&2
261 exit 1
262fi
263
264# ------------------------------------------------------------------
265# Multi-select picker (Handles Space separation & Exclusions via !)
266# Reads newline-separated options on stdin, prints selected lines on stdout.
267# ------------------------------------------------------------------
268pick_multi() {
269 local prompt="$1" header="$2"
270 if command -v fzf >/dev/null 2>&1; then
271 fzf --multi --reverse --prompt="$prompt " --header="$header"
272 return
273 fi
274
275 # Fallback: numbered list + space-separated input with exclusion support.
276 local -a items=()
277 local line
278 while IFS= read -r line; do items+=("$line"); done
279
280 local total=${#items[@]} i
281 while :; do
282 {
283 echo "$header"
284 for (( i=0; i<total; i++ )); do
285 printf ' %2d) %s\n' "$((i+1))" "${items[i]}"
286 done
287 printf '%s' "$prompt (e.g. 1 3, !2 to exclude 2, or a for all): "
288 } >&2
289
290 local input
291 if ! IFS= read -r input </dev/tty; then
292 echo "no tty available; install fzf or run interactively" >&2
293 return 1
294 fi
295
296 if [[ "$input" == "a" || "$input" == "A" ]]; then
297 printf '%s\n' "${items[@]}"
298 return
299 fi
300 [[ -z "$input" ]] && { echo " please enter at least one number, an exclusion, or 'a'." >&2; continue; }
301
302 local -a toks=()
303 IFS=' ' read -r -a toks <<<"$input"
304
305 # Determine if this is an exclusion request (checks if any token starts with !)
306 local is_exclusion=0
307 local t
308 for t in "${toks[@]}"; do
309 if [[ "$t" =~ ^\! ]]; then
310 is_exclusion=1
311 break
312 fi
313 done
314
315 local -a picked=()
316 local bad=0
317
318 if (( is_exclusion )); then
319 # Track which indices are explicitly excluded using an associative array map
320 local -A excluded_map=()
321
322 for t in "${toks[@]}"; do
323 [[ -z "$t" ]] && continue
324 if [[ ! "$t" =~ ^\![0-9]+$ ]]; then
325 echo " Error: When using exclusions, all items must start with '!' (e.g., !1 !3)" >&2
326 bad=1
327 break
328 fi
329
330 local idx="${t#\!}" # Strip the '!' character
331 if (( idx < 1 || idx > total )); then bad=1; break; fi
332 excluded_map[$((idx-1))]=1
333 done
334
335 if (( bad )); then
336 echo " invalid exclusion input; try again." >&2
337 continue
338 fi
339
340 # Build final selection list by adding everything NOT in our exclusion map
341 for (( i=0; i<total; i++ )); do
342 if [[ -z "${excluded_map[$i]+abc}" ]]; then
343 picked+=("${items[i]}")
344 fi
345 done
346 else
347 # Standard additive parsing (space-separated)
348 for t in "${toks[@]}"; do
349 t="${t// /}"
350 [[ -z "$t" ]] && continue
351 if [[ ! "$t" =~ ^[0-9]+$ ]]; then bad=1; break; fi
352 if (( t < 1 || t > total )); then bad=1; break; fi
353 picked+=("${items[t-1]}")
354 done
355 fi
356
357 if (( bad )); then
358 echo " invalid input; try again." >&2
359 continue
360 fi
361 if (( ${#picked[@]} == 0 )); then
362 echo " no valid selections; try again." >&2
363 continue
364 fi
365 printf '%s\n' "${picked[@]}"
366 return
367 done
368}
369
370pick_action() {
371 local input
372 while :; do
373 {
374 echo
375 echo "What do you want to do?"
376 echo " 1) Install plugins"
377 echo " 2) Uninstall plugins"
378 printf 'Action (1 or 2): '
379 } >&2
380 if ! IFS= read -r input </dev/tty; then
381 echo "no tty available; run interactively" >&2
382 return 1
383 fi
384 case "$input" in
385 1|i|I|install|Install) printf 'install\n'; return ;;
386 2|u|U|uninstall|Uninstall) printf 'uninstall\n'; return ;;
387 *) echo " invalid input; enter 1 for install or 2 for uninstall." >&2 ;;
388 esac
389 done
390}
391
392# Locate product-info.json for a launcher and print dataDirectoryName. This
393# ties uninstalling to the IDE selected above instead of scanning every
394# JetBrains profile on the machine.
395product_data_name() {
396 local launcher="$1" probe="$1" dir info parent raw
397
398 # Resolve a symlinked Toolbox launcher when possible.
399 if [[ -L "$probe" ]]; then
400 raw="$(readlink "$probe" 2>/dev/null || true)"
401 if [[ -n "$raw" ]]; then
402 [[ "$raw" == /* ]] || raw="$(dirname "$probe")/$raw"
403 probe="$raw"
404 fi
405 fi
406
407 # Toolbox .cmd shims point at the real Windows launcher. Resolve that path
408 # when running under WSL so product-info.json can be found beside the IDE.
409 if [[ "$launcher" == *.cmd && -f "$launcher" ]]; then
410 raw="$(tr -d '\r' <"$launcher" | sed -nE 's/.*"([A-Za-z]:\\[^"]+\.exe)".*/\1/p' | head -n 1)"
411 if [[ -n "$raw" ]] && command -v wslpath >/dev/null 2>&1; then
412 probe="$(wslpath -u "$raw" 2>/dev/null || printf '%s' "$launcher")"
413 fi
414 elif [[ "$OS_KIND" == "macos" && -f "$launcher" ]]; then
415 # Toolbox shell launchers usually contain the real *.app executable path.
416 raw="$(sed -nE 's/.*"([^\"]+\.app\/Contents\/MacOS\/[^\"]+)".*/\1/p' "$launcher" | head -n 1)"
417 raw="${raw/#\~\//$HOME/}"
418 raw="${raw/#\$HOME\//$HOME/}"
419 [[ -n "$raw" && -e "$raw" ]] && probe="$raw"
420 fi
421
422 dir="$probe"
423 [[ -f "$dir" ]] && dir="$(dirname "$dir")"
424 while [[ -n "$dir" && "$dir" != "/" ]]; do
425 for info in "$dir/product-info.json" "$dir/Resources/product-info.json"; do
426 [[ -f "$info" ]] || continue
427 sed -nE 's/.*"dataDirectoryName"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/p' "$info" | head -n 1
428 return
429 done
430 parent="$(dirname "$dir")"
431 [[ "$parent" == "$dir" ]] && break
432 dir="$parent"
433 done
434}
435
436plugin_root_for_target() {
437 local launcher="$1" type="$2" data_name
438 data_name="$(product_data_name "$launcher")"
439 [[ -n "$data_name" ]] || return 1
440
441 if [[ "$type" == "remotedev" ]]; then
442 printf '%s/.local/shared/JetBrains/%s\n' "$HOME" "$data_name"
443 elif [[ "$OS_KIND" == "macos" ]]; then
444 printf '%s/Library/Application Support/JetBrains/%s/plugins\n' "$HOME" "$data_name"
445 elif [[ "$OS_KIND" == "wsl2" && ( "$type" == "wincmd" || "$launcher" == /mnt/* ) ]]; then
446 printf '%s/AppData/Roaming/JetBrains/%s/plugins\n' "$(win_user_dir)" "$data_name"
447 else
448 printf '%s/.local/share/JetBrains/%s\n' "$HOME" "$data_name"
449 fi
450}
451
452xml_tag_value() {
453 local xml="$1" tag="$2" rest
454 [[ "$xml" == *"<$tag>"* ]] || return 1
455 rest="${xml#*<$tag>}"
456 rest="${rest%%</$tag>*}"
457 # plugin.xml values are normally plain text; trim surrounding whitespace.
458 printf '%s' "$rest" | sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//'
459}
460
461# Print "id|name" for one top-level item in an IDE's plugins directory.
462plugin_metadata() {
463 local item="$1" xml="" archive id name
464 if [[ -f "$item/META-INF/plugin.xml" ]]; then
465 xml="$(tr '\n' ' ' <"$item/META-INF/plugin.xml")"
466 elif [[ -f "$item" ]]; then
467 xml="$(unzip -p "$item" META-INF/plugin.xml 2>/dev/null | tr '\n' ' ' || true)"
468 elif [[ -d "$item" ]]; then
469 while IFS= read -r -d '' archive; do
470 xml="$(unzip -p "$archive" META-INF/plugin.xml 2>/dev/null | tr '\n' ' ' || true)"
471 [[ -n "$xml" ]] && break
472 done < <(find "$item" -maxdepth 3 -type f \( -name '*.jar' -o -name '*.zip' \) -print0 2>/dev/null)
473 fi
474 [[ -n "$xml" ]] || return 1
475 id="$(xml_tag_value "$xml" id || true)"
476 name="$(xml_tag_value "$xml" name || true)"
477 [[ -n "$id" ]] || id="$name"
478 [[ -n "$name" ]] || name="$id"
479 [[ -n "$id" ]] || return 1
480 printf '%s|%s\n' "$id" "$name"
481}
482
483# ------------------------------------------------------------------
484# IDE picker
485# ------------------------------------------------------------------
486declare -a ide_lines=()
487for c in "${CANDIDATES[@]}"; do
488 # human-readable picker line: just the third field
489 ide_lines+=("${c#*|*|}")
490done
491
492# Map label -> (type, launcher) so we can recover after picking.
493declare -A label_type label_launcher
494for c in "${CANDIDATES[@]}"; do
495 IFS='|' read -r t l lab <<<"$c"
496 label_type[$lab]="$t"
497 label_launcher[$lab]="$l"
498done
499
500echo "Detected ${#CANDIDATES[@]} JetBrains target(s)."
501mapfile -t SELECTED_IDES < <(
502 printf '%s\n' "${ide_lines[@]}" |
503 pick_multi "IDEs>" "Pick IDE(s) — TAB to toggle (fzf) or spaced indexes"
504)
505
506if (( ${#SELECTED_IDES[@]} == 0 )); then
507 echo "No IDEs selected; exiting." >&2
508 exit 1
509fi
510
511# ------------------------------------------------------------------
512# Action, then plugin picker
513# ------------------------------------------------------------------
514ACTION="$(pick_action)"
515
516declare -a plugin_lines=()
517declare -A label_to_id
518declare -a INSTALLED_PLUGIN_RECORDS=()
519
520if [[ "$ACTION" == "install" ]]; then
521 for entry in "${PLUGIN_CATALOG[@]}"; do
522 IFS='|' read -r pid plabel <<<"$entry"
523 line="$(printf '%-32s %s' "$plabel" "$pid")"
524 plugin_lines+=("$line")
525 label_to_id[$line]="$pid"
526 done
527else
528 if ! command -v unzip >/dev/null 2>&1; then
529 echo "Uninstall requires 'unzip' so installed plugin IDs can be read safely." >&2
530 exit 1
531 fi
532
533 declare -A seen_plugin_ids=() seen_plugin_paths=()
534 for ide_label in "${SELECTED_IDES[@]}"; do
535 itype="${label_type[$ide_label]}"
536 launcher="${label_launcher[$ide_label]}"
537 if ! plugin_root="$(plugin_root_for_target "$launcher" "$itype")"; then
538 echo "Could not resolve the plugin directory for: $ide_label" >&2
539 continue
540 fi
541 [[ -d "$plugin_root" ]] || continue
542
543 while IFS= read -r -d '' item; do
544 metadata="$(plugin_metadata "$item" || true)"
545 [[ -n "$metadata" ]] || continue
546 [[ -z "${seen_plugin_paths[$item]+x}" ]] || continue
547 seen_plugin_paths["$item"]=1
548 IFS='|' read -r pid plabel <<<"$metadata"
549 INSTALLED_PLUGIN_RECORDS+=("${pid}|${plabel}|${item}|${plugin_root}|${ide_label}")
550 if [[ -z "${seen_plugin_ids[$pid]+x}" ]]; then
551 line="$(printf '%-32s %s' "$plabel" "$pid")"
552 plugin_lines+=("$line")
553 label_to_id[$line]="$pid"
554 seen_plugin_ids[$pid]=1
555 fi
556 done < <(find "$plugin_root" -mindepth 1 -maxdepth 1 -print0 2>/dev/null)
557 done
558
559 if (( ${#plugin_lines[@]} == 0 )); then
560 echo "No removable user-installed plugins were found for the selected IDE(s)." >&2
561 echo "Bundled plugins cannot be uninstalled; disable them in the IDE instead." >&2
562 exit 0
563 fi
564fi
565
566mapfile -t SELECTED_PLUGIN_LINES < <(
567 printf '%s\n' "${plugin_lines[@]}" |
568 pick_multi "Plugins>" "Pick plugin(s) to ${ACTION} — TAB to toggle (fzf) or spaced indexes"
569)
570
571if (( ${#SELECTED_PLUGIN_LINES[@]} == 0 )); then
572 echo "No plugins selected; exiting." >&2
573 exit 1
574fi
575
576declare -a SELECTED_PLUGIN_IDS=()
577for line in "${SELECTED_PLUGIN_LINES[@]}"; do
578 SELECTED_PLUGIN_IDS+=("${label_to_id[$line]}")
579done
580
581# ------------------------------------------------------------------
582# Running-IDE preflight
583# JetBrains' `installPlugins` refuses to run while the IDE GUI is open
584# ("Only one instance of IDEA can be run at a time."). We do a best-effort
585# pgrep against the IDE app directory so the user can close them upfront
586# instead of seeing N identical errors during the install loop.
587# ------------------------------------------------------------------
588ide_is_running() {
589 local launcher="$1" type="$2"
590
591 # remote-dev-server.sh spawns short-lived workers; WSL2->Windows lookups
592 # would need tasklist.exe — skip in both cases.
593 case "$type" in
594 remotedev|wincmd) return 1 ;;
595 esac
596
597 case "$launcher" in
598 */Toolbox/apps/*)
599 # /.../Toolbox/apps/<ide-dir>/[ch-N/<ver>/]bin/idea.sh -> match "<ide-dir>"
600 local rest="${launcher#*/Toolbox/apps/}"
601 local ide_dir="${rest%%/*}"
602 [[ -n "$ide_dir" ]] && pgrep -f -- "/Toolbox/apps/${ide_dir}/" >/dev/null 2>&1
603 ;;
604 */Applications/*.app/*)
605 local app_path="${launcher%/Contents/MacOS/*}"
606 pgrep -f -- "${app_path}/Contents/" >/dev/null 2>&1
607 ;;
608 *)
609 local b
610 b="$(basename "$launcher")"
611 b="${b%.sh}"
612 pgrep -fi -- "$b" >/dev/null 2>&1
613 ;;
614 esac
615}
616
617while :; do
618 declare -a _running=()
619 for ide_label in "${SELECTED_IDES[@]}"; do
620 if ide_is_running "${label_launcher[$ide_label]}" "${label_type[$ide_label]}"; then
621 _running+=("$ide_label")
622 fi
623 done
624 (( ${#_running[@]} == 0 )) && break
625
626 {
627 echo
628 echo "The following IDE(s) appear to be running — plugin changes need them closed:"
629 for r in "${_running[@]}"; do
630 echo " - $r"
631 done
632 echo
633 printf 'Close them, then press Enter to retry (or Ctrl+C to abort): '
634 } >&2
635 if ! IFS= read -r _ </dev/tty; then
636 echo "no tty; aborting" >&2
637 exit 1
638 fi
639done
640
641# ------------------------------------------------------------------
642# Install/uninstall loop
643# ------------------------------------------------------------------
644if [[ "$ACTION" == "uninstall" ]]; then
645 declare -A selected_id_map=()
646 for pid in "${SELECTED_PLUGIN_IDS[@]}"; do selected_id_map[$pid]=1; done
647
648 declare -a TO_REMOVE=()
649 for record in "${INSTALLED_PLUGIN_RECORDS[@]}"; do
650 IFS='|' read -r pid plabel plugin_path plugin_root ide_label <<<"$record"
651 [[ -n "${selected_id_map[$pid]+x}" ]] || continue
652 # Safety boundary: only top-level children found in the resolved plugin root.
653 [[ "$(dirname "$plugin_path")" == "$plugin_root" ]] || continue
654 TO_REMOVE+=("$record")
655 done
656
657 if (( ${#TO_REMOVE[@]} == 0 )); then
658 echo "None of the selected plugins are installed in the selected IDE(s)." >&2
659 exit 0
660 fi
661
662 echo
663 echo "The following plugin installations will be removed:"
664 for record in "${TO_REMOVE[@]}"; do
665 IFS='|' read -r pid plabel plugin_path plugin_root ide_label <<<"$record"
666 echo " - $plabel ($pid)"
667 echo " IDE: $ide_label"
668 echo " Path: $plugin_path"
669 done
670 printf 'Continue? [y/N]: ' >&2
671 if ! IFS= read -r confirm </dev/tty || [[ ! "$confirm" =~ ^[Yy]$ ]]; then
672 echo "Uninstall cancelled."
673 exit 0
674 fi
675
676 removed=0
677 failed=0
678 declare -a FAILED_LIST=()
679 for record in "${TO_REMOVE[@]}"; do
680 IFS='|' read -r pid plabel plugin_path plugin_root ide_label <<<"$record"
681 printf 'Removing %-32s from %s... ' "$plabel" "$ide_label"
682 if rm -rf -- "$plugin_path"; then
683 echo "ok"
684 removed=$((removed + 1))
685 else
686 echo "FAILED"
687 failed=$((failed + 1))
688 FAILED_LIST+=("${ide_label} :: ${pid}")
689 fi
690 done
691
692 echo "Done. Removed: ${removed}, Failed: ${failed}. Restart the IDE(s) to finish."
693 if (( failed > 0 )); then
694 printf ' - %s\n' "${FAILED_LIST[@]}" >&2
695 exit 1
696 fi
697 exit 0
698fi
699
700LOG_FILE="$(mktemp -t jb-install.XXXXXX)"
701trap 'rm -f "$LOG_FILE"' EXIT
702
703installed=0
704failed=0
705declare -a FAILED_LIST=()
706
707echo
708echo "Installing ${#SELECTED_PLUGIN_IDS[@]} plugin(s) into ${#SELECTED_IDES[@]} IDE(s)..."
709echo
710
711handle_single_instance_abort() {
712 local ide_label="$1" current_pid="$2"
713 shift 2
714 echo
715 echo " ! '${ide_label}' is still running. Skipping remaining plugins for this IDE." >&2
716 echo " ! Close the IDE and re-run the script to finish." >&2
717 # Mark current and remaining-for-this-IDE plugins as failed.
718 local mark_from=0 p
719 for p in "${SELECTED_PLUGIN_IDS[@]}"; do
720 if (( mark_from )) || [[ "$p" == "$current_pid" ]]; then
721 mark_from=1
722 failed=$((failed + 1))
723 FAILED_LIST+=("${ide_label} :: ${p} (IDE running — close it and re-run)")
724 fi
725 done
726}
727
728for ide_label in "${SELECTED_IDES[@]}"; do
729 itype="${label_type[$ide_label]}"
730 launcher="${label_launcher[$ide_label]}"
731
732 echo ">>> $ide_label"
733 ide_aborted=0
734 for pid in "${SELECTED_PLUGIN_IDS[@]}"; do
735 (( ide_aborted )) && break
736 printf ' %-34s ' "$pid"
737 rc=0
738 case "$itype" in
739 ide|remotedev)
740 "$launcher" installPlugins "$pid" >"$LOG_FILE" 2>&1 || rc=$?
741 ;;
742 wincmd)
743 cmd.exe /c "$launcher" installPlugins "$pid" >"$LOG_FILE" 2>&1 || rc=$?
744 ;;
745 esac
746
747 if (( rc == 0 )); then
748 echo "ok"
749 installed=$((installed + 1))
750 elif grep -q "Only one instance" "$LOG_FILE" 2>/dev/null; then
751 echo "SKIPPED"
752 handle_single_instance_abort "$ide_label" "$pid"
753 ide_aborted=1
754 else
755 echo "FAILED"
756 failed=$((failed + 1))
757 FAILED_LIST+=("${ide_label} :: ${pid}")
758 sed 's/^/ /' "$LOG_FILE" >&2 || true
759 fi
760 done
761 echo
762done
763
764echo "Done. Installed: ${installed}, Failed: ${failed}"
765
766if (( failed > 0 )); then
767 echo "Failed:" >&2
768 for f in "${FAILED_LIST[@]}"; do
769 echo " - $f" >&2
770 done
771 exit 1
772fi
773
install.ps1 Originalformat
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 'GitHub.vscode-github-actions',
48 'ms-azuretools.vscode-containers',
49 'ms-vscode-remote.vscode-remote-extensionpack',
50 'pkief.material-icon-theme'
51)
52
53$codeCmd = Get-Command code -ErrorAction SilentlyContinue
54if (-not $codeCmd) {
55 Write-Host ""
56 Write-Host "Error: the 'code' command is not on your PATH." -ForegroundColor Red
57 Write-Host " Install VS Code from https://code.visualstudio.com/ and make sure" -ForegroundColor Yellow
58 Write-Host ' "Add to PATH" was checked during setup, then re-open your terminal.' -ForegroundColor Yellow
59 exit 1
60}
61
62$extraArgs = @()
63if (-not [string]::IsNullOrWhiteSpace($ProfileName)) {
64 $extraArgs += @('--profile', $ProfileName)
65}
66
67$header = "Installing $($Extensions.Count) extension(s)"
68if ($ProfileName) { $header += " into profile '$ProfileName'" }
69Write-Host ("{0}..." -f $header) -ForegroundColor Cyan
70Write-Host ""
71
72$installed = 0
73$failed = 0
74$failedList = @()
75
76foreach ($ext in $Extensions) {
77 Write-Host -NoNewline (" -> {0,-55} " -f $ext)
78 try {
79 $output = & code @extraArgs --install-extension $ext --force 2>&1
80 if ($LASTEXITCODE -eq 0) {
81 Write-Host "ok" -ForegroundColor Green
82 $installed++
83 } else {
84 Write-Host "FAILED" -ForegroundColor Red
85 $failed++
86 $failedList += $ext
87 $output | ForEach-Object { Write-Host " $_" -ForegroundColor DarkRed }
88 }
89 } catch {
90 Write-Host "FAILED" -ForegroundColor Red
91 $failed++
92 $failedList += $ext
93 Write-Host (" {0}" -f $_.Exception.Message) -ForegroundColor DarkRed
94 }
95}
96
97Write-Host ""
98Write-Host ("Done. Installed: {0}, Failed: {1}" -f $installed, $failed)
99
100if ($failed -gt 0) {
101 Write-Host "Failed extensions:" -ForegroundColor Red
102 foreach ($f in $failedList) {
103 Write-Host (" - {0}" -f $f) -ForegroundColor Red
104 }
105 exit 1
106}
107
install.sh Originalformat
1#!/usr/bin/env bash
2#
3# VS Code extension installer for macOS and Linux (Ubuntu).
4#
5# Usage:
6# ./install.sh # looks for .vscode/extensions.json
7# ./install.sh -f extensions.json # specify a local JSON file
8# ./install.sh -f https://link-to-your/ext.json # specify a remote URL directly
9# ./install.sh -p "MyProfile" -f <source> # install into a named profile
10
11set -euo pipefail
12IFS=$'\n\t'
13
14EXTENSIONS=(
15 "docker.docker"
16 "github.github-vscode-theme"
17 "GitHub.vscode-github-actions"
18 "ms-azuretools.vscode-containers"
19 "ms-vscode-remote.vscode-remote-extensionpack"
20 "pkief.material-icon-theme"
21)
22PROFILE=""
23SOURCE=""
24
25usage() {
26 cat <<EOF
27Usage: $(basename "$0") [--profile "ProfileName"] [--file "extensions.json" | "https://..."]
28
29Installs a curated list of VS Code extensions on macOS or Linux from a local JSON file or URL.
30
31Options:
32 -p, --profile <name> Install into the named VS Code profile.
33 -f, --file <path/url> Path or URL to the JSON file containing recommendations.
34 Defaults to .vscode/extensions.json if it exists.
35 -h, --help Show this help and exit.
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 -f|--file)
50 if [[ $# -lt 2 || -z "${2:-}" ]]; then
51 echo "Error: --file requires a non-empty value." >&2
52 exit 2
53 fi
54 SOURCE="$2"
55 shift 2
56 ;;
57 -h|--help)
58 usage
59 exit 0
60 ;;
61 *)
62 echo "Error: unknown argument '$1'." >&2
63 usage >&2
64 exit 2
65 ;;
66 esac
67done
68
69OS="$(uname -s)"
70case "$OS" in
71 Darwin|Linux) ;;
72 *)
73 echo "Error: unsupported OS '$OS'. Use install.ps1 on Windows." >&2
74 exit 1
75 ;;
76esac
77
78# 1. Check for 'code' command
79if ! command -v code >/dev/null 2>&1; then
80 cat >&2 <<'EOF'
81Error: the 'code' command is not on your PATH.
82EOF
83 exit 1
84fi
85
86# 2. Check for 'jq' command
87if ! command -v jq >/dev/null 2>&1; then
88 echo "Error: 'jq' is not installed. (Ubuntu: sudo apt-get install jq)" >&2
89 exit 1
90fi
91
92# 3. Resolve JSON Source (Local File vs URL)
93JSON_FILE=""
94TEMP_JSON_FILE=""
95USE_HARDCODED=false
96
97if [[ -z "$SOURCE" ]]; then
98 if [[ -f ".vscode/extensions.json" ]]; then
99 JSON_FILE=".vscode/extensions.json"
100 else
101 echo "Info: No JSON file specified and .vscode/extensions.json not found. Using hardcoded extensions." >&2
102 USE_HARDCODED=true
103 fi
104elif [[ "$SOURCE" =~ ^https?:// ]]; then
105 if ! command -v curl >/dev/null 2>&1; then
106 echo "Error: 'curl' is required to download remote JSON files." >&2
107 exit 1
108 fi
109 echo "Downloading JSON from URL..."
110 JSON_FILE="$(mktemp -t vscode-json.XXXXXX)"
111 TEMP_JSON_FILE="$JSON_FILE"
112 curl -sSL "$SOURCE" -o "$JSON_FILE"
113else
114 JSON_FILE="$SOURCE"
115 if [[ ! -f "$JSON_FILE" ]]; then
116 echo "Error: File '$JSON_FILE' does not exist." >&2
117 exit 1
118 fi
119fi
120
121# 4. Extract extensions from JSON using jq (if not using hardcoded array)
122if [[ "$USE_HARDCODED" == false ]]; then
123 EXTENSIONS=()
124 while IFS= read -r ext; do
125 EXTENSIONS+=("$ext")
126 done < <(jq -r '.recommendations[]?' "$JSON_FILE")
127
128 if [[ ${#EXTENSIONS[@]} -eq 0 ]]; then
129 echo "Error: No extensions found under the 'recommendations' key." >&2
130 exit 1
131 fi
132fi
133
134# Trap to clean up logs and temporary downloaded JSON files
135LOG_FILE="$(mktemp -t vscode-install.XXXXXX)"
136trap 'rm -f "$LOG_FILE" "${TEMP_JSON_FILE:-}"' EXIT
137
138install_extension() {
139 local ext="$1"
140
141 if [[ -n "$PROFILE" ]]; then
142 code --profile "$PROFILE" --install-extension "$ext" --force
143 else
144 code --install-extension "$ext" --force
145 fi
146}
147
148header="Installing ${#EXTENSIONS[@]} extension(s)"
149[[ -n "$PROFILE" ]] && header+=" into profile '$PROFILE'"
150echo "$header..."
151echo
152
153installed=0
154failed=0
155declare -a FAILED_LIST=()
156
157for ext in "${EXTENSIONS[@]}"; do
158 [[ -z "$ext" ]] && continue
159
160 printf ' -> %-55s ' "$ext"
161 if install_extension "$ext" >"$LOG_FILE" 2>&1; then
162 echo "ok"
163 installed=$((installed + 1))
164 else
165 echo "FAILED"
166 failed=$((failed + 1))
167 FAILED_LIST+=("$ext")
168 sed 's/^/ /' "$LOG_FILE" >&2 || true
169 fi
170done
171
172echo
173echo "Done. Installed: ${installed}, Failed: ${failed}"
174
175if (( failed > 0 )); then
176 echo "Failed extensions:" >&2
177 for f in "${FAILED_LIST[@]}"; do
178 echo " - $f" >&2
179 done
180 exit 1
181fi
182
python_recommendation.json Originalformat
1{
2 "recommendations": [
3 "ms-python.python",
4 "ms-python.vscode-pylance",
5 "charliermarsh.ruff",
6 "usernamehw.errorlens",
7 "mikestead.dotenv",
8 "donjayamanne.python-environment-manager",
9 "rangav.vscode-thunder-client",
10 "njpwerner.autodocstring",
11 "eamodio.gitlens"
12 ]
13}
typescript_react_recommendation.json Originalformat
1{
2 "recommendations": [
3 "dsznajder.es7-react-js-snippets",
4 "planbcoding.vscode-react-refactor",
5 "bradlc.vscode-tailwindcss",
6 "ms-vscode.vscode-typescript-next",
7 "yoavbls.pretty-ts-errors",
8 "christian-kohler.path-intellisense",
9 "christian-kohler.npm-intellisense",
10 "dbaeumer.vscode-eslint",
11 "esbenp.prettier-vscode",
12 "usernamehw.errorlens",
13 "eamodio.gitlens",
14 "formulahendry.auto-rename-tag",
15 "GitHub.vscode-github-actions",
16 "ms-azuretools.vscode-containers",
17 "GitHub.copilot"
18 ]
19}
20