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