Última atividade 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.

Revisão 68290dc7bff5ea4d5e4a58ddf516a38686fa8c95

README.md Bruto

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 Bruto
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 Bruto
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 Bruto
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 | ForEach-Object { Get-InstalledPlugins -Ide $_ })
384 $pluginChoices = @($installedPluginInstances |
385 Group-Object Id |
386 ForEach-Object { [pscustomobject]@{ Id = $_.Name; Label = $_.Group[0].Label } } |
387 Sort-Object Label)
388
389 if ($pluginChoices.Count -eq 0) {
390 Write-Host "No removable user-installed plugins were found for the selected IDE(s)." -ForegroundColor Yellow
391 Write-Host "Bundled plugins cannot be uninstalled; disable them in the IDE instead." -ForegroundColor Yellow
392 exit 0
393 }
394}
395
396$selectedPlugins = Select-Multi -Title ("Pick plugin(s) to {0}" -f $action.ToLower()) -Items $pluginChoices -Property 'Label'
397if ($selectedPlugins.Count -eq 0) {
398 Write-Host "No plugins selected; exiting." -ForegroundColor Red
399 exit 1
400}
401
402# ------------------------------------------------------------------
403# Running-IDE preflight
404# Installing or deleting plugin files requires the selected IDE to be closed.
405# installPlugins refuses to run while the IDE GUI holds the config-dir lock
406# ("Only one instance of IDEA can be run at a time."). Best-effort detection
407# via Get-Process. Gateway Client type doesn't need this (no CLI invoked).
408# ------------------------------------------------------------------
409function Test-IdeRunning {
410 param([Parameter(Mandatory)] [psobject]$Ide)
411 if ($Ide.Type -eq 'Gateway') {
412 if ($action -eq 'Install') { return $false }
413 foreach ($name in @('jetbrains_client64', 'jetbrains_client', 'JetBrainsClient')) {
414 if (Get-Process -Name $name -ErrorAction SilentlyContinue) { return $true }
415 }
416 return $false
417 }
418
419 # For standalone IDE launchers the process name is the exe basename.
420 # For Toolbox .cmd shims, the spawned process is e.g. idea64 -> use prefix.
421 $stem = [System.IO.Path]::GetFileNameWithoutExtension($Ide.Launcher)
422 $patterns = @($stem)
423 if ($stem -notmatch '64$') { $patterns += ($stem + '64') }
424
425 foreach ($pat in $patterns) {
426 if (Get-Process -Name $pat -ErrorAction SilentlyContinue) { return $true }
427 }
428 return $false
429}
430
431while ($true) {
432 $running = @($selectedIdes | Where-Object { Test-IdeRunning $_ })
433 if ($running.Count -eq 0) { break }
434
435 Write-Host ""
436 Write-Host "The following IDE(s) appear to be running — plugin changes need them closed:" -ForegroundColor Yellow
437 foreach ($r in $running) { Write-Host (" - {0}" -f $r.Label) -ForegroundColor Yellow }
438 Write-Host ""
439 Read-Host "Close them, then press Enter to retry (Ctrl+C to abort)" | Out-Null
440}
441
442# ------------------------------------------------------------------
443# Uninstall or install
444# ------------------------------------------------------------------
445if ($action -eq 'Uninstall') {
446 $selectedIds = @($selectedPlugins | ForEach-Object { $_.Id })
447 $toRemove = @($installedPluginInstances | Where-Object { $_.Id -in $selectedIds } | Where-Object {
448 $parent = [System.IO.Directory]::GetParent($_.Path)
449 $parent -and ($parent.FullName.TrimEnd('\') -eq $_.PluginRoot.TrimEnd('\'))
450 })
451
452 if ($toRemove.Count -eq 0) {
453 Write-Host "None of the selected plugins are installed in the selected IDE(s)." -ForegroundColor Yellow
454 exit 0
455 }
456
457 Write-Host ""
458 Write-Host "The following plugin installations will be removed:" -ForegroundColor Yellow
459 foreach ($plugin in $toRemove) {
460 Write-Host (" - {0} ({1})" -f $plugin.Label, $plugin.Id)
461 Write-Host (" IDE: {0}" -f $plugin.TargetLabel)
462 Write-Host (" Path: {0}" -f $plugin.Path)
463 }
464 $confirm = Read-Host "Continue? [y/N]"
465 if ($confirm -notmatch '^[Yy]$') {
466 Write-Host "Uninstall cancelled."
467 exit 0
468 }
469
470 $removed = 0
471 $failed = 0
472 $failedList = @()
473 foreach ($plugin in $toRemove) {
474 Write-Host -NoNewline ("Removing {0} from {1}... " -f $plugin.Label, $plugin.TargetLabel)
475 try {
476 Remove-Item -LiteralPath $plugin.Path -Recurse -Force -ErrorAction Stop
477 Write-Host "ok" -ForegroundColor Green
478 $removed++
479 } catch {
480 Write-Host "FAILED" -ForegroundColor Red
481 Write-Host (" {0}" -f $_.Exception.Message) -ForegroundColor DarkRed
482 $failed++
483 $failedList += ("{0} :: {1}" -f $plugin.TargetLabel, $plugin.Id)
484 }
485 }
486
487 Write-Host ("Done. Removed: {0}, Failed: {1}. Restart the IDE(s) to finish." -f $removed, $failed)
488 if ($failed -gt 0) {
489 foreach ($entry in $failedList) { Write-Host (" - {0}" -f $entry) -ForegroundColor Red }
490 exit 1
491 }
492 exit 0
493}
494
495$installed = 0
496$failed = 0
497$failedList = @()
498
499Write-Host ""
500Write-Host ("Installing {0} plugin(s) into {1} IDE(s)..." -f $selectedPlugins.Count, $selectedIdes.Count) -ForegroundColor Cyan
501Write-Host ""
502
503foreach ($ide in $selectedIdes) {
504 Write-Host (">>> {0}" -f $ide.Label)
505
506 switch ($ide.Type) {
507
508 # ---- standard IDE launcher ----
509 'Ide' {
510 $args = @('installPlugins') + ($selectedPlugins | ForEach-Object { $_.Id })
511 try {
512 $output = & $ide.Launcher @args 2>&1
513 if ($LASTEXITCODE -eq 0) {
514 Write-Host (" all {0} plugin(s) ok" -f $selectedPlugins.Count) -ForegroundColor Green
515 $installed += $selectedPlugins.Count
516 } else {
517 $joined = ($output | Out-String)
518 if ($joined -match 'Only one instance') {
519 Write-Host " IDE is running SKIPPED" -ForegroundColor Yellow
520 Write-Host " Close the IDE and re-run the script to finish." -ForegroundColor DarkYellow
521 } else {
522 Write-Host (" installPlugins exited {0} FAILED" -f $LASTEXITCODE) -ForegroundColor Red
523 $output | ForEach-Object { Write-Host " $_" -ForegroundColor DarkRed }
524 }
525 $failed += $selectedPlugins.Count
526 foreach ($p in $selectedPlugins) { $failedList += ("{0} :: {1}" -f $ide.Label, $p.Id) }
527 }
528 } catch {
529 Write-Host (" launcher invocation threw FAILED") -ForegroundColor Red
530 $failed += $selectedPlugins.Count
531 foreach ($p in $selectedPlugins) { $failedList += ("{0} :: {1}" -f $ide.Label, $p.Id) }
532 Write-Host (" {0}" -f $_.Exception.Message) -ForegroundColor DarkRed
533 }
534 }
535
536 # ---- Toolbox .cmd shim ----
537 'WinCmd' {
538 $idArgs = ($selectedPlugins | ForEach-Object { '"{0}"' -f $_.Id }) -join ' '
539 try {
540 $output = cmd.exe /c "`"$($ide.Launcher)`" installPlugins $idArgs" 2>&1
541 if ($LASTEXITCODE -eq 0) {
542 Write-Host (" all {0} plugin(s) ok" -f $selectedPlugins.Count) -ForegroundColor Green
543 $installed += $selectedPlugins.Count
544 } else {
545 $joined = ($output | Out-String)
546 if ($joined -match 'Only one instance') {
547 Write-Host " IDE is running SKIPPED" -ForegroundColor Yellow
548 Write-Host " Close the IDE and re-run the script to finish." -ForegroundColor DarkYellow
549 } else {
550 Write-Host (" installPlugins exited {0} FAILED" -f $LASTEXITCODE) -ForegroundColor Red
551 $output | ForEach-Object { Write-Host " $_" -ForegroundColor DarkRed }
552 }
553 $failed += $selectedPlugins.Count
554 foreach ($p in $selectedPlugins) { $failedList += ("{0} :: {1}" -f $ide.Label, $p.Id) }
555 }
556 } catch {
557 Write-Host (" cmd.exe invocation threw FAILED") -ForegroundColor Red
558 $failed += $selectedPlugins.Count
559 foreach ($p in $selectedPlugins) { $failedList += ("{0} :: {1}" -f $ide.Label, $p.Id) }
560 Write-Host (" {0}" -f $_.Exception.Message) -ForegroundColor DarkRed
561 }
562 }
563
564 # ---- Gateway Client ----
565 'Gateway' {
566 $client = $ide.Client
567 $buildFile = Join-Path $client 'build.txt'
568 if (-not (Test-Path $buildFile)) {
569 Write-Host " build.txt missing under client; cannot determine build id SKIPPED" -ForegroundColor Yellow
570 Write-Host " (open a Remote Project once to populate the client folder)" -ForegroundColor DarkYellow
571 $failed += $selectedPlugins.Count
572 foreach ($p in $selectedPlugins) { $failedList += ("{0} :: {1}" -f $ide.Label, $p.Id) }
573 continue
574 }
575 $buildId = (Get-Content $buildFile -Raw -ErrorAction Stop).Trim()
576
577 $pluginDir = Join-Path $client 'plugins'
578 if (-not (Test-Path $pluginDir)) {
579 New-Item -ItemType Directory -Path $pluginDir | Out-Null
580 }
581
582 $ua = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
583
584 foreach ($p in $selectedPlugins) {
585 Write-Host -NoNewline (" {0,-34} " -f $p.Id)
586 $encoded = [uri]::EscapeDataString($p.Id)
587 $url = "https://plugins.jetbrains.com/pluginManager/?action=download&id=$encoded&build=$buildId"
588 $tmp = Join-Path $env:TEMP ("jb-" + [guid]::NewGuid().ToString() + ".bin")
589 try {
590 Invoke-WebRequest -Uri $url -OutFile $tmp -UserAgent $ua -ErrorAction Stop -MaximumRedirection 5
591 $bytes = [System.IO.File]::ReadAllBytes($tmp) | Select-Object -First 2
592 if ($bytes.Count -ge 2 -and $bytes[0] -eq 0x50 -and $bytes[1] -eq 0x4B) {
593 Expand-Archive -Path $tmp -DestinationPath $pluginDir -Force -ErrorAction Stop
594 Write-Host "ok (zip)" -ForegroundColor Green
595 } else {
596 $jarName = Join-Path $pluginDir ($p.Id -replace '[^A-Za-z0-9._-]', '_') + '.jar'
597 Move-Item -Path $tmp -Destination $jarName -Force
598 Write-Host "ok (jar)" -ForegroundColor Green
599 }
600 $installed++
601 } catch {
602 Write-Host "FAILED" -ForegroundColor Red
603 Write-Host (" {0}" -f $_.Exception.Message) -ForegroundColor DarkRed
604 $failed++
605 $failedList += ("{0} :: {1}" -f $ide.Label, $p.Id)
606 } finally {
607 if (Test-Path $tmp) { Remove-Item $tmp -Force -ErrorAction SilentlyContinue }
608 }
609 }
610 Write-Host " Reopen the Gateway Client to load the new plugins." -ForegroundColor Yellow
611 }
612 }
613 Write-Host ""
614}
615
616Write-Host ("Done. Installed: {0}, Failed: {1}" -f $installed, $failed)
617if ($failed -gt 0) {
618 Write-Host "Failed:" -ForegroundColor Red
619 foreach ($f in $failedList) { Write-Host (" - {0}" -f $f) -ForegroundColor Red }
620 exit 1
621}
622
install-jetbrains.sh Bruto
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=()
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 IFS='|' read -r pid plabel <<<"$metadata"
547 INSTALLED_PLUGIN_RECORDS+=("${pid}|${plabel}|${item}|${plugin_root}|${ide_label}")
548 if [[ -z "${seen_plugin_ids[$pid]+x}" ]]; then
549 line="$(printf '%-32s %s' "$plabel" "$pid")"
550 plugin_lines+=("$line")
551 label_to_id[$line]="$pid"
552 seen_plugin_ids[$pid]=1
553 fi
554 done < <(find "$plugin_root" -mindepth 1 -maxdepth 1 -print0 2>/dev/null)
555 done
556
557 if (( ${#plugin_lines[@]} == 0 )); then
558 echo "No removable user-installed plugins were found for the selected IDE(s)." >&2
559 echo "Bundled plugins cannot be uninstalled; disable them in the IDE instead." >&2
560 exit 0
561 fi
562fi
563
564mapfile -t SELECTED_PLUGIN_LINES < <(
565 printf '%s\n' "${plugin_lines[@]}" |
566 pick_multi "Plugins>" "Pick plugin(s) to ${ACTION} — TAB to toggle (fzf) or spaced indexes"
567)
568
569if (( ${#SELECTED_PLUGIN_LINES[@]} == 0 )); then
570 echo "No plugins selected; exiting." >&2
571 exit 1
572fi
573
574declare -a SELECTED_PLUGIN_IDS=()
575for line in "${SELECTED_PLUGIN_LINES[@]}"; do
576 SELECTED_PLUGIN_IDS+=("${label_to_id[$line]}")
577done
578
579# ------------------------------------------------------------------
580# Running-IDE preflight
581# JetBrains' `installPlugins` refuses to run while the IDE GUI is open
582# ("Only one instance of IDEA can be run at a time."). We do a best-effort
583# pgrep against the IDE app directory so the user can close them upfront
584# instead of seeing N identical errors during the install loop.
585# ------------------------------------------------------------------
586ide_is_running() {
587 local launcher="$1" type="$2"
588
589 # remote-dev-server.sh spawns short-lived workers; WSL2->Windows lookups
590 # would need tasklist.exe — skip in both cases.
591 case "$type" in
592 remotedev|wincmd) return 1 ;;
593 esac
594
595 case "$launcher" in
596 */Toolbox/apps/*)
597 # /.../Toolbox/apps/<ide-dir>/[ch-N/<ver>/]bin/idea.sh -> match "<ide-dir>"
598 local rest="${launcher#*/Toolbox/apps/}"
599 local ide_dir="${rest%%/*}"
600 [[ -n "$ide_dir" ]] && pgrep -f -- "/Toolbox/apps/${ide_dir}/" >/dev/null 2>&1
601 ;;
602 */Applications/*.app/*)
603 local app_path="${launcher%/Contents/MacOS/*}"
604 pgrep -f -- "${app_path}/Contents/" >/dev/null 2>&1
605 ;;
606 *)
607 local b
608 b="$(basename "$launcher")"
609 b="${b%.sh}"
610 pgrep -fi -- "$b" >/dev/null 2>&1
611 ;;
612 esac
613}
614
615while :; do
616 declare -a _running=()
617 for ide_label in "${SELECTED_IDES[@]}"; do
618 if ide_is_running "${label_launcher[$ide_label]}" "${label_type[$ide_label]}"; then
619 _running+=("$ide_label")
620 fi
621 done
622 (( ${#_running[@]} == 0 )) && break
623
624 {
625 echo
626 echo "The following IDE(s) appear to be running — plugin changes need them closed:"
627 for r in "${_running[@]}"; do
628 echo " - $r"
629 done
630 echo
631 printf 'Close them, then press Enter to retry (or Ctrl+C to abort): '
632 } >&2
633 if ! IFS= read -r _ </dev/tty; then
634 echo "no tty; aborting" >&2
635 exit 1
636 fi
637done
638
639# ------------------------------------------------------------------
640# Install/uninstall loop
641# ------------------------------------------------------------------
642if [[ "$ACTION" == "uninstall" ]]; then
643 declare -A selected_id_map=()
644 for pid in "${SELECTED_PLUGIN_IDS[@]}"; do selected_id_map[$pid]=1; done
645
646 declare -a TO_REMOVE=()
647 for record in "${INSTALLED_PLUGIN_RECORDS[@]}"; do
648 IFS='|' read -r pid plabel plugin_path plugin_root ide_label <<<"$record"
649 [[ -n "${selected_id_map[$pid]+x}" ]] || continue
650 # Safety boundary: only top-level children found in the resolved plugin root.
651 [[ "$(dirname "$plugin_path")" == "$plugin_root" ]] || continue
652 TO_REMOVE+=("$record")
653 done
654
655 if (( ${#TO_REMOVE[@]} == 0 )); then
656 echo "None of the selected plugins are installed in the selected IDE(s)." >&2
657 exit 0
658 fi
659
660 echo
661 echo "The following plugin installations will be removed:"
662 for record in "${TO_REMOVE[@]}"; do
663 IFS='|' read -r pid plabel plugin_path plugin_root ide_label <<<"$record"
664 echo " - $plabel ($pid)"
665 echo " IDE: $ide_label"
666 echo " Path: $plugin_path"
667 done
668 printf 'Continue? [y/N]: ' >&2
669 if ! IFS= read -r confirm </dev/tty || [[ ! "$confirm" =~ ^[Yy]$ ]]; then
670 echo "Uninstall cancelled."
671 exit 0
672 fi
673
674 removed=0
675 failed=0
676 declare -a FAILED_LIST=()
677 for record in "${TO_REMOVE[@]}"; do
678 IFS='|' read -r pid plabel plugin_path plugin_root ide_label <<<"$record"
679 printf 'Removing %-32s from %s... ' "$plabel" "$ide_label"
680 if rm -rf -- "$plugin_path"; then
681 echo "ok"
682 removed=$((removed + 1))
683 else
684 echo "FAILED"
685 failed=$((failed + 1))
686 FAILED_LIST+=("${ide_label} :: ${pid}")
687 fi
688 done
689
690 echo "Done. Removed: ${removed}, Failed: ${failed}. Restart the IDE(s) to finish."
691 if (( failed > 0 )); then
692 printf ' - %s\n' "${FAILED_LIST[@]}" >&2
693 exit 1
694 fi
695 exit 0
696fi
697
698LOG_FILE="$(mktemp -t jb-install.XXXXXX)"
699trap 'rm -f "$LOG_FILE"' EXIT
700
701installed=0
702failed=0
703declare -a FAILED_LIST=()
704
705echo
706echo "Installing ${#SELECTED_PLUGIN_IDS[@]} plugin(s) into ${#SELECTED_IDES[@]} IDE(s)..."
707echo
708
709handle_single_instance_abort() {
710 local ide_label="$1" current_pid="$2"
711 shift 2
712 echo
713 echo " ! '${ide_label}' is still running. Skipping remaining plugins for this IDE." >&2
714 echo " ! Close the IDE and re-run the script to finish." >&2
715 # Mark current and remaining-for-this-IDE plugins as failed.
716 local mark_from=0 p
717 for p in "${SELECTED_PLUGIN_IDS[@]}"; do
718 if (( mark_from )) || [[ "$p" == "$current_pid" ]]; then
719 mark_from=1
720 failed=$((failed + 1))
721 FAILED_LIST+=("${ide_label} :: ${p} (IDE running — close it and re-run)")
722 fi
723 done
724}
725
726for ide_label in "${SELECTED_IDES[@]}"; do
727 itype="${label_type[$ide_label]}"
728 launcher="${label_launcher[$ide_label]}"
729
730 echo ">>> $ide_label"
731 ide_aborted=0
732 for pid in "${SELECTED_PLUGIN_IDS[@]}"; do
733 (( ide_aborted )) && break
734 printf ' %-34s ' "$pid"
735 rc=0
736 case "$itype" in
737 ide|remotedev)
738 "$launcher" installPlugins "$pid" >"$LOG_FILE" 2>&1 || rc=$?
739 ;;
740 wincmd)
741 cmd.exe /c "$launcher" installPlugins "$pid" >"$LOG_FILE" 2>&1 || rc=$?
742 ;;
743 esac
744
745 if (( rc == 0 )); then
746 echo "ok"
747 installed=$((installed + 1))
748 elif grep -q "Only one instance" "$LOG_FILE" 2>/dev/null; then
749 echo "SKIPPED"
750 handle_single_instance_abort "$ide_label" "$pid"
751 ide_aborted=1
752 else
753 echo "FAILED"
754 failed=$((failed + 1))
755 FAILED_LIST+=("${ide_label} :: ${pid}")
756 sed 's/^/ /' "$LOG_FILE" >&2 || true
757 fi
758 done
759 echo
760done
761
762echo "Done. Installed: ${installed}, Failed: ${failed}"
763
764if (( failed > 0 )); then
765 echo "Failed:" >&2
766 for f in "${FAILED_LIST[@]}"; do
767 echo " - $f" >&2
768 done
769 exit 1
770fi
771
install.ps1 Bruto
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 Bruto
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 Bruto
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 Bruto
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