
Amplicode Install
- 118 installs
- 105 repo stars
- Updated July 27, 2026
- amplicode/spring-skills
Find installed IntelliJ IDEA and GigaIDE paths on Windows so Amplicode or Spring tooling can be installed into the right IDE without manual hunting.
About
amplicode-install is an agent skill backed by a PowerShell detector that locates locally installed IntelliJ IDEA (Ultimate or Community) and GigaIDE on Windows. Solo builders shipping Spring or JVM apps with Amplicode often need a machine-readable list of valid IDE homes before plugins or marketplace steps run. The skill runs detect-ides.ps1 (or equivalent) and prints a JSON array of candidates on standard output, or an empty array when nothing is found. Bounded Invoke-WithTimeout and Find-ProductInfoFiles helpers keep scans from blocking indefinitely, which matters when agents automate repetitive environment setup. It fits Claude Code, Cursor, and Codex workflows where filesystem inspection and shell execution are allowed. Pair it with Amplicode marketplace or configuration skills once candidates are known. It does not install plugins by itself—it only discovers where installation should target.
- PowerShell script scans Windows for IntelliJ IDEA Ultimate and Community editions
- Detects GigaIDE installations alongside JetBrains products
- Emits a UTF-8 JSON array of IDE candidates on stdout for downstream install steps
- Uses bounded timeouts and depth limits so directory walks do not hang the agent
- Skips reparse points and continues safely when individual probes time out
Amplicode Install by the numbers
- 118 all-time installs (skills.sh)
- Ranked #32 of 89 Java & JVM skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/amplicode/spring-skills --skill amplicode-installAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 118 |
|---|---|
| repo stars | ★ 105 |
| Last updated | July 27, 2026 |
| Repository | amplicode/spring-skills ↗ |
What it does
Find installed IntelliJ IDEA and GigaIDE paths on Windows so Amplicode or Spring tooling can be installed into the right IDE without manual hunting.
Files
amplicode-install
Installs the Amplicode IntelliJ plugin into all locally installed IntelliJ IDEA (Ultimate/Community) and GigaIDE installations the user picks.
What this skill does
1. Runs a platform-specific detection script that finds installed IntelliJ-based IDEs on the machine. 2. Filters down to IDEA Ultimate (`IU`), IDEA Community (`IC`), and GigaIDE. Skips everything else. 3. Filters out IDEs where Amplicode is already installed. 4. Asks the user which IDE(s) to install into when more than one candidate remains. 5. Invokes the standard JetBrains CLI: <ide-binary> installPlugins com.haulmont.amplicode https://amplicode.ru/marketplace. The IDE itself picks the right version for its build, downloads the ZIP, and unpacks it into the right plugins directory. 6. Tells the user to launch the IDE, open any project, and click the "Настроить Spring Agent" button on the Amplicode welcome screen. That button (and only that button) triggers MCP auto-config + spring-skills install.
What this skill does NOT do
- It does not write any MCP client config. The Amplicode plugin needs a running MCP server inside the IDE to know the port — only the plugin itself can fill in the configs of supported agents. That happens when the user clicks "Настроить Spring Agent" on the welcome screen (not automatically on IDE start).
- It does not install spring-skills or any agent-side skill packages. That is also done by the welcome-screen button.
- It does not download or extract the plugin ZIP by hand. JetBrains'
installPluginsCLI handles fetching, version matching against the IDE build, and unpacking.
Step-by-step
1. Run the detection script
The scripts live next to this SKILL.md under scripts/. Run the right one for the user's OS and parse its stdout as a JSON array; the script already filters down to compatible IDEs (IDEA Ultimate / Community / GigaIDE), so the agent only needs these fields per element: amplicodeInstalled, name, running, pid, hostsCurrentProcess, dataDirectoryName, exePath, appBundle.
- macOS / Linux:
bash scripts/detect-ides.sh - Windows:
pwsh scripts/detect-ides.ps1(fallbackpowershellifpwshis not available)
If the JSON array is empty — tell the user no compatible IDE was found and stop.
2. Filter
- Drop entries with
amplicodeInstalled: true— never reinstall, never ask. - If nothing left — tell the user Amplicode is already installed in every compatible IDE and stop.
3. Pick targets
- Exactly one candidate left: install into it without asking. Tell the user which IDE you are using.
- Two or more: ask the user a multi-select question (using whichever interactive-prompt tool the client provides). Each option is one IDE (label =
name). Do not add "all of them" as a separate option — multi-select already covers it. Recommend selecting all, but let the user decide.
3a. Handle running IDEs (auto-restart flow)
For each picked IDE, check the running flag from the detection JSON. Trust it — do not roll your own `pgrep` check; pgrep truncates long JVM command lines and gives false negatives. If running: true, the JetBrains CLI will fail with "Only one instance of IDEA can be run at a time."
Self-host check (do this first). For each picked IDE with running: true, look at hostsCurrentProcess. If true, you are running inside that IDE's terminal — sending SIGTERM to the IDE will kill you before installPlugins ever runs, leaving the user with a closed IDE and no plugin. Never offer auto-restart for such IDEs. Skip the question below for that IDE and go straight to the manual instructions described after "If the user picks the second option" — substitute the IDE's own exePath. Tell the user up front (in their language) why: "I'm running inside this IDE — if I close it, I die with it and the install step never runs. So you'll need to do this part yourself."
When at least one picked IDE has running: true and `hostsCurrentProcess: false`, offer the user an auto-restart flow for those IDEs. Phrase the user-facing question in the user's language (mirror whatever language the user has been chatting in); the wording below is the English baseline:
"<IDE name> is currently running. I can gracefully shut it down, install the plugin, and relaunch it. If the IDE has unsaved changes it will show a save dialog and refuse to exit — in that case I will stop and you handle it manually. OK?"
Ask a single-select question with two options:
- "Yes — close it, install, relaunch" (Recommended)
- "I will do it myself"
If the user picks the second option, do not stop silently and do not just ask them to "re-run this skill" — they may be running you from inside that very IDE, in which case re-running the skill after closing the IDE is impossible. Instead, give them the exact command and the post-install steps they need to perform themselves. Tell them (in their language):
Close the IDE yourself, then run this command in a terminal:
>
```
"<exePath>" installPlugins com.haulmont.amplicode https://amplicode.ru/marketplace
```
>
Substitute <exePath> with the IDE launcher path from the detection JSON (quote it — there are usually spaces). Wait for it to finish; exit code 0 means the plugin is installed.>
Then open the IDE again. On any open project:
1. If the Amplicode welcome screen appears — click Настроить Spring Agent.
2. Otherwise open it manually via Find Action (Cmd+Shift+Aon macOS,Ctrl+Shift+Aon Windows/Linux) → type Spring Agent Toolkit → Enter, then click Настроить Spring Agent.
3. After that, restart the MCP client you are using (so it picks up the new MCP server config).
Then stop the skill.
If the user agrees to auto-restart, for each running picked IDE do graceful quit → wait → install → relaunch:
Quit (SIGTERM, never SIGKILL):
- macOS / Linux:
kill -TERM <pid> - Windows:
taskkill /PID <pid>(no/F)
Wait for shutdown by polling: re-run the detect script and look for the same dataDirectoryName. The IDE is fully down when running: false (the .pid file is removed). Poll every 5 seconds for up to 30 seconds — do NOT poll every second, that's just noise.
If still running after 30 seconds — stop. Tell the user (in their language): "The IDE didn't shut down within 30 seconds — there's probably an unsaved-changes dialog open. Finish saving/exiting it manually, then re-run this skill." Do not retry, do not force-kill.
Install as described in step 4.
Relaunch:
- macOS:
open -a "<appBundle>"(this is whatappBundleis for; falls back toopen -a "<exePath>"if missing). - Linux:
nohup "<exePath>" >/dev/null 2>&1 &(detach from the agent process group). - Windows:
Start-Process -FilePath "<exePath>"from PowerShell, orstart "" "<exePath>"from cmd.
4. Install
For each picked IDE, run:
"<exePath>" installPlugins com.haulmont.amplicode https://amplicode.ru/marketplaceNotes:
- Run it as a normal
Bashcall. Always quoteexePath— it commonly contains spaces. - Do NOT run this while that IDE is open by the user — JetBrains' CLI may behave unpredictably with a running instance. If you suspect the IDE is running, ask the user to close it first.
- If multiple targets — run them sequentially, not in parallel. Each invocation needs to write to that IDE's plugins directory.
- Treat a non-zero exit code as failure for that IDE. Show the user the stderr and continue with the remaining IDEs.
5. Wrap up
Tell the user the following (translate it into the user's language — mirror whatever language they have been chatting in):
Amplicode plugin installed into: <list>.
>
Next:
1. Open any project in the IDE — the Amplicode welcome screen should appear automatically.
If it does not, open it manually: Find Action (Cmd+Shift+Aon macOS,Ctrl+Shift+Aon Windows/Linux) → type Spring Agent Toolkit → Enter.
2. Click the "Настроить Spring Agent" button on that screen. The plugin will then write the Amplicode MCP server into the configs of the supported agents and install spring-skills. Without clicking this button, MCP is not configured. The welcome screen only shows up on an open project, not on the IDE's start window.
3. Restart the MCP client you are currently using. Most clients read MCP config only at startup, so the newly added amplicode server is not visible in the current session. After the restart, check the client's MCP server list.Keep Настроить Spring Agent and Spring Agent Toolkit verbatim in the translated message — they are literal labels rendered in the IDE's UI, not strings to localize.
6. Detect failure of step 5 (optional, if the client exposes its MCP server list to you)
After the user restarts their MCP client and continues the conversation, if you can introspect the list of configured MCP servers and amplicode is not present, the user most likely skipped the welcome-screen button. Tell them (in their language):
Looks like theamplicodeMCP server isn't connected on your side. That means the welcome-screen button wasn't pressed yet. Open any project in the IDE, then run Find Action (Cmd+Shift+A/Ctrl+Shift+A) → Spring Agent Toolkit → click Настроить Spring Agent on the screen that opens. Then restart this client again.
# Detects locally installed IntelliJ IDEA (Ultimate/Community) and GigaIDE installations on Windows.
# Prints a JSON array of candidates on stdout. Empty array if nothing found.
#
# Usage: pwsh detect-ides.ps1 (or powershell -ExecutionPolicy Bypass -File detect-ides.ps1)
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
$ErrorActionPreference = 'Continue'
# ---------- bounded helpers ----------
# Runs a self-contained scriptblock with a wall-clock timeout.
# The scriptblock runs in a separate runspace and does not share caller variables.
function Invoke-WithTimeout {
param(
[Parameter(Mandatory)][scriptblock]$ScriptBlock,
[int]$TimeoutMs = 5000
)
$ps = [System.Management.Automation.PowerShell]::Create()
$null = $ps.AddScript($ScriptBlock.ToString())
$async = $ps.BeginInvoke()
if ($async.AsyncWaitHandle.WaitOne($TimeoutMs)) {
try {
return ,($ps.EndInvoke($async))
} finally {
$ps.Dispose()
}
}
# Clean up in the background so detection can continue after timeout.
[System.Threading.ThreadPool]::QueueUserWorkItem({
param($p)
try { $p.Stop() } catch {}
try { $p.Dispose() } catch {}
}, $ps) | Out-Null
throw [System.TimeoutException]::new("Operation timed out after $TimeoutMs ms")
}
# Finds product-info.json files with bounded depth, time, and directory count.
# Reparse points are skipped.
function Find-ProductInfoFiles {
param(
[string]$Root,
[int]$MaxDepth = 4,
[int]$TimeoutMs = 8000,
[int]$MaxDirs = 20000
)
$results = New-Object System.Collections.Generic.List[string]
if ([string]::IsNullOrEmpty($Root) -or -not (Test-Path -LiteralPath $Root)) {
return $results
}
$sw = [System.Diagnostics.Stopwatch]::StartNew()
$dirCount = 0
$queue = New-Object System.Collections.Generic.Queue[object]
$queue.Enqueue([pscustomobject]@{ Path = $Root; Depth = 0 })
while ($queue.Count -gt 0) {
if ($sw.ElapsedMilliseconds -gt $TimeoutMs) { break }
if ($dirCount -ge $MaxDirs) { break }
$node = $queue.Dequeue()
$dirCount++
$pi = Join-Path $node.Path 'product-info.json'
if (Test-Path -LiteralPath $pi -PathType Leaf) {
$results.Add($pi)
}
if ($node.Depth -ge $MaxDepth) { continue }
$children = $null
try {
$children = Get-ChildItem -LiteralPath $node.Path -Directory -Force -ErrorAction SilentlyContinue
} catch {
$children = $null
}
if (-not $children) { continue }
foreach ($child in $children) {
if ($child.Attributes -band [System.IO.FileAttributes]::ReparsePoint) { continue }
$queue.Enqueue([pscustomobject]@{ Path = $child.FullName; Depth = $node.Depth + 1 })
}
}
return $results
}
# ---------- search roots ----------
$searchRoots = @()
if ($env:LOCALAPPDATA) {
$searchRoots += "$env:LOCALAPPDATA\Programs"
$searchRoots += "$env:LOCALAPPDATA\JetBrains\Toolbox\apps"
}
$searchRoots += "C:\Program Files\JetBrains"
$searchRoots += "C:\Program Files (x86)\JetBrains"
# ---------- find product-info.json files ----------
$piPaths = New-Object System.Collections.Generic.HashSet[string]
foreach ($root in $searchRoots) {
if (-not (Test-Path -LiteralPath $root)) { continue }
foreach ($f in (Find-ProductInfoFiles -Root $root -MaxDepth 6 -TimeoutMs 10000)) {
$null = $piPaths.Add($f)
}
}
# Fallback: inspect top-level directories on fixed drives.
# Recursion is limited to IDE-looking directories.
$skipTop = @('Windows', 'Windows.old', '$Recycle.Bin', 'System Volume Information',
'PerfLogs', 'Recovery', 'Boot', 'EFI', 'MSOCache', 'OneDriveTemp')
$ideNamePattern = '(?i)^(idea|intellij|giga|jetbrains|amplicode|toolbox)'
# Inspect standard Program Files locations for IDEs installed under vendor folders.
# Other drives are handled by the top-level fallback below.
$programFilesRoots = @('C:\Program Files', 'C:\Program Files (x86)')
foreach ($pfRoot in $programFilesRoots) {
if (-not (Test-Path -LiteralPath $pfRoot)) { continue }
$children = $null
try {
$children = Get-ChildItem -LiteralPath $pfRoot -Directory -Force -ErrorAction SilentlyContinue
} catch { $children = $null }
if (-not $children) { continue }
foreach ($td in $children) {
if ($td.Attributes -band [System.IO.FileAttributes]::ReparsePoint) { continue }
$directPi = Join-Path $td.FullName 'product-info.json'
if (Test-Path -LiteralPath $directPi -PathType Leaf) {
$null = $piPaths.Add($directPi)
}
if ($td.Name -match $ideNamePattern) {
foreach ($f in (Find-ProductInfoFiles -Root $td.FullName -MaxDepth 4 -TimeoutMs 8000)) {
$null = $piPaths.Add($f)
}
}
}
}
# Enumerate fixed drives with a bounded system query and a filesystem fallback.
try {
$fixedDrives = Invoke-WithTimeout -TimeoutMs 5000 -ScriptBlock {
Get-CimInstance -ClassName Win32_LogicalDisk -Filter 'DriveType=3' -OperationTimeoutSec 4 -ErrorAction Stop |
ForEach-Object { "$($_.DeviceID)\" }
}
} catch {
$fixedDrives = $null
}
if (-not $fixedDrives) {
$fixedDrives = Get-PSDrive -PSProvider FileSystem -ErrorAction SilentlyContinue |
Where-Object { $_.Root -match '^[A-Z]:\\$' } |
ForEach-Object { $_.Root }
}
# Global budget for the whole fallback scan.
$fallbackBudget = [System.Diagnostics.Stopwatch]::StartNew()
foreach ($drive in $fixedDrives) {
if ($fallbackBudget.ElapsedMilliseconds -gt 30000) { break }
if (-not (Test-Path -LiteralPath $drive)) { continue }
$topDirs = $null
try {
$topDirs = Get-ChildItem -LiteralPath $drive -Directory -Force -ErrorAction SilentlyContinue
} catch {
$topDirs = $null
}
if (-not $topDirs) { continue }
foreach ($td in $topDirs) {
if ($skipTop -contains $td.Name) { continue }
if ($td.Attributes -band [System.IO.FileAttributes]::ReparsePoint) { continue }
# Fast path: product-info.json directly inside this top-level dir.
$directPi = Join-Path $td.FullName 'product-info.json'
if (Test-Path -LiteralPath $directPi -PathType Leaf) {
$null = $piPaths.Add($directPi)
}
# Bounded recursion only for IDE-looking directories.
if ($td.Name -match $ideNamePattern) {
foreach ($f in (Find-ProductInfoFiles -Root $td.FullName -MaxDepth 4 -TimeoutMs 8000)) {
$null = $piPaths.Add($f)
}
}
}
}
# ---------- helpers ----------
function Find-Launcher {
param([string]$piPath)
$piDir = Split-Path -Parent $piPath
# Windows IDE layout: product-info.json in install root, exe in bin\idea64.exe
$candidates = @(
(Join-Path $piDir 'bin\idea64.exe'),
(Join-Path $piDir 'bin\idea.bat'),
(Join-Path $piDir 'bin\idea.exe')
)
foreach ($c in $candidates) {
if (Test-Path -LiteralPath $c) { return $c }
}
return $null
}
function Test-Target {
param([string]$productCode, [string]$productName)
if ($productCode -in @('IU', 'IC')) { return $true }
if ($productName -match '(?i)giga\s*ide') { return $true }
return $false
}
function Test-AmplicodeInstalled {
param([string]$pluginsDir)
if (-not (Test-Path -LiteralPath $pluginsDir)) { return $false }
$hits = Get-ChildItem -LiteralPath $pluginsDir -Directory -ErrorAction SilentlyContinue |
Where-Object { $_.Name -match '(?i)^amplicode' }
return [bool]$hits
}
# Vendor namespace is used in per-user IntelliJ Platform directories.
# Older builds may omit it and use the JetBrains namespace.
function Get-PluginsDir {
param([string]$dataDirName, [string]$vendor = 'JetBrains')
if (-not $env:APPDATA) { return $null }
return Join-Path $env:APPDATA "$vendor\$dataDirName\plugins"
}
function Get-SystemDir {
param([string]$dataDirName, [string]$vendor = 'JetBrains')
if (-not $env:LOCALAPPDATA) { return $null }
return Join-Path $env:LOCALAPPDATA "$vendor\$dataDirName"
}
# Returns true when this script is running under the target IDE process.
# Uses one bounded process snapshot, then walks parent PIDs in memory.
function Test-DescendantOf {
param([int]$targetPid)
if ($targetPid -le 0) { return $false }
$parentOf = @{}
try {
$all = Invoke-WithTimeout -TimeoutMs 5000 -ScriptBlock {
Get-CimInstance -ClassName Win32_Process -OperationTimeoutSec 4 -ErrorAction Stop |
Select-Object ProcessId, ParentProcessId
}
} catch {
# If the process snapshot is unavailable, keep detection moving.
return $false
}
if (-not $all) { return $false }
foreach ($p in $all) {
$parentOf[[int]$p.ProcessId] = [int]$p.ParentProcessId
}
$cur = $PID
$depth = 0
while ($cur -and $cur -ne 0 -and $depth -lt 50) {
if ($cur -eq $targetPid) { return $true }
if (-not $parentOf.ContainsKey($cur)) { return $false }
$cur = $parentOf[$cur]
$depth++
}
return $false
}
# Returns the IDE PID from its .pid file when the process is still running.
function Get-IdeRunningPid {
param([string]$dataDirName, [string]$vendor = 'JetBrains')
$systemDir = Get-SystemDir $dataDirName $vendor
if (-not $systemDir) { return $null }
$pidFile = Join-Path $systemDir '.pid'
if (-not (Test-Path -LiteralPath $pidFile)) { return $null }
$raw = (Get-Content -LiteralPath $pidFile -ErrorAction SilentlyContinue | Select-Object -First 1)
if (-not $raw) { return $null }
$pidNum = 0
if (-not [int]::TryParse(($raw -replace '\D', ''), [ref]$pidNum)) { return $null }
if ($pidNum -le 0) { return $null }
try {
$null = Get-Process -Id $pidNum -ErrorAction Stop
return $pidNum
} catch {
return $null
}
}
# ---------- build results ----------
$results = @()
foreach ($pi in $piPaths) {
try {
$info = Get-Content -LiteralPath $pi -Raw -Encoding UTF8 | ConvertFrom-Json
} catch {
continue
}
$productCode = $info.productCode
$productName = $info.name
$version = $info.version
$dataDirName = $info.dataDirectoryName
$productVendor = $info.productVendor
if (-not $productVendor) { $productVendor = 'JetBrains' }
if (-not $productCode -or -not $dataDirName) { continue }
if (-not (Test-Target $productCode $productName)) { continue }
$exePath = Find-Launcher $pi
if (-not $exePath) { continue }
$pluginsDir = Get-PluginsDir $dataDirName $productVendor
$amplicodeInstalled = if ($pluginsDir) { Test-AmplicodeInstalled $pluginsDir } else { $false }
$runningPid = Get-IdeRunningPid $dataDirName $productVendor
$running = [bool]$runningPid
# Check the process tree only when the IDE process is running.
$hostsCurrentProcess = if ($runningPid) { Test-DescendantOf $runningPid } else { $false }
$edition = switch ($productCode) {
'IU' { 'Ultimate' }
'IC' { 'Community' }
default { '' }
}
$display = if ($edition) { "$productName $edition $version" } else { "$productName $version" }
$results += [pscustomobject]@{
name = $display.Trim()
dataDirectoryName = $dataDirName
exePath = $exePath
amplicodeInstalled = $amplicodeInstalled
running = $running
pid = $runningPid
hostsCurrentProcess = $hostsCurrentProcess
appBundle = $null
}
}
# Emit JSON array (always an array, even with a single element).
if ($results.Count -eq 0) {
Write-Output '[]'
} else {
,$results | ConvertTo-Json -Depth 4 -Compress
}
#!/usr/bin/env bash
#
# Detects locally installed IntelliJ IDEA (Ultimate/Community) and GigaIDE installations.
# Prints a JSON array of candidates on stdout. Empty array if nothing found.
#
# No external deps beyond bash, find, grep, sed. We assemble JSON by hand to avoid
# requiring jq on the user's machine.
set -u
OS="$(uname -s)"
# ---------- helpers ----------
json_escape() {
# Escape backslashes, quotes, and control chars for JSON string literal.
local s="$1"
s="${s//\\/\\\\}"
s="${s//\"/\\\"}"
s="${s//$'\n'/\\n}"
s="${s//$'\r'/\\r}"
s="${s//$'\t'/\\t}"
printf '%s' "$s"
}
# Read a string field from product-info.json without requiring jq.
read_pi_field() {
local file="$1" field="$2"
grep -E "\"$field\"" "$file" 2>/dev/null \
| head -n1 \
| sed -E 's/.*"'"$field"'"[[:space:]]*:[[:space:]]*"([^"]*)".*/\1/'
}
# Given a product-info.json path, figure out the matching launcher binary for this OS.
# Sets globals: EXE_PATH, IDE_ROOT, APP_BUNDLE (macOS only; empty otherwise)
locate_launcher() {
local pi="$1"
EXE_PATH=""
IDE_ROOT=""
APP_BUNDLE=""
local pi_dir
pi_dir="$(dirname "$pi")"
# macOS .app bundle: product-info.json lives in Contents/Resources, binary in ../MacOS/idea
if [ -x "$pi_dir/../MacOS/idea" ]; then
EXE_PATH="$(cd "$pi_dir/../MacOS" && pwd)/idea"
IDE_ROOT="$(cd "$pi_dir/../.." && pwd)"
APP_BUNDLE="$IDE_ROOT"
return 0
fi
# Linux/Windows layout: product-info.json at the install root, launcher in bin/
if [ -x "$pi_dir/bin/idea.sh" ]; then
EXE_PATH="$pi_dir/bin/idea.sh"
IDE_ROOT="$pi_dir"
return 0
fi
if [ -x "$pi_dir/bin/idea" ]; then
EXE_PATH="$pi_dir/bin/idea"
IDE_ROOT="$pi_dir"
return 0
fi
return 1
}
# Returns 0 if the candidate matches our target list (IDEA U/C or GigaIDE).
is_target() {
local product_code="$1" name="$2"
case "$product_code" in
IU|IC) return 0 ;;
esac
# GigaIDE may use its own productCode; fall back to a name match.
if printf '%s' "$name" | grep -qiE 'giga[[:space:]]*ide'; then
return 0
fi
return 1
}
# Check whether Amplicode is already present in this IDE's plugins directory.
amplicode_installed_in() {
local plugins_dir="$1"
[ -d "$plugins_dir" ] || return 1
# Plugin folder is typically "Amplicode" (sometimes versioned like "Amplicode-2025.x.x").
local d
while IFS= read -r -d '' d; do
case "$(basename "$d")" in
[Aa][Mm][Pp][Ll][Ii][Cc][Oo][Dd][Ee]*) return 0 ;;
esac
done < <(find "$plugins_dir" -maxdepth 1 -mindepth 1 -type d -print0 2>/dev/null)
return 1
}
# Compute the runtime system directory.
# Vendor namespace is used in per-user IntelliJ Platform directories.
system_dir_for() {
local data_dir_name="$1" vendor="${2:-JetBrains}"
case "$OS" in
Darwin) printf '%s' "$HOME/Library/Caches/$vendor/$data_dir_name" ;;
Linux) printf '%s' "$HOME/.cache/$vendor/$data_dir_name" ;;
*)
if [ -n "${LOCALAPPDATA:-}" ]; then
printf '%s' "$LOCALAPPDATA/$vendor/$data_dir_name"
else
printf '%s' "$HOME/AppData/Local/$vendor/$data_dir_name"
fi
;;
esac
}
# Returns 0 when this script is running under the target IDE process.
is_descendant_of_pid() {
local target_pid="$1"
[ -n "$target_pid" ] || return 1
local pid=$$
local depth=0
while [ -n "$pid" ] && [ "$pid" != "0" ] && [ "$pid" != "1" ]; do
if [ "$pid" = "$target_pid" ]; then
return 0
fi
pid="$(ps -o ppid= -p "$pid" 2>/dev/null | tr -d ' \t\n')"
depth=$((depth + 1))
[ "$depth" -gt 50 ] && return 1
done
return 1
}
# Returns the IDE PID from its .pid file when the process is still running.
ide_running_pid() {
local data_dir_name="$1" vendor="${2:-JetBrains}"
local system_dir
system_dir="$(system_dir_for "$data_dir_name" "$vendor")"
local pid_file="$system_dir/.pid"
[ -f "$pid_file" ] || return 1
local pid
pid="$(head -n1 "$pid_file" 2>/dev/null | tr -dc '0-9')"
[ -n "$pid" ] || return 1
if kill -0 "$pid" 2>/dev/null; then
printf '%s' "$pid"
return 0
fi
return 1
}
# Compute the plugins directory from dataDirectoryName and vendor namespace.
plugins_dir_for() {
local data_dir_name="$1" vendor="${2:-JetBrains}"
case "$OS" in
Darwin)
printf '%s' "$HOME/Library/Application Support/$vendor/$data_dir_name/plugins"
;;
Linux)
printf '%s' "$HOME/.local/share/$vendor/$data_dir_name"
;;
*)
# MSYS/Cygwin fallback (Windows users should use the PowerShell script instead)
if [ -n "${APPDATA:-}" ]; then
printf '%s' "$APPDATA/$vendor/$data_dir_name/plugins"
else
printf '%s' "$HOME/AppData/Roaming/$vendor/$data_dir_name/plugins"
fi
;;
esac
}
# ---------- search ----------
# Storage for unique product-info.json paths.
pi_paths=()
add_pi() {
local p="$1"
[ -f "$p" ] || return 0
local resolved
if command -v realpath >/dev/null 2>&1; then
resolved="$(realpath "$p" 2>/dev/null || printf '%s' "$p")"
else
resolved="$p"
fi
local existing
for existing in "${pi_paths[@]:-}"; do
[ "$existing" = "$resolved" ] && return 0
done
pi_paths+=("$resolved")
}
# Recursively find product-info.json files under a search root, limited depth.
# Search depth is limited per install location.
scan_root() {
local root="$1" max_depth="$2"
[ -d "$root" ] || return 0
local pi
while IFS= read -r -d '' pi; do
add_pi "$pi"
done < <(find "$root" -maxdepth "$max_depth" -name "product-info.json" -print0 2>/dev/null)
}
case "$OS" in
Darwin)
# Apps: /Applications/IntelliJ IDEA.app/Contents/Resources/product-info.json — depth 4
scan_root "/Applications" 4
scan_root "$HOME/Applications" 5
# Toolbox v2 on macOS keeps apps under ~/Library/Application Support/JetBrains/Toolbox/apps
scan_root "$HOME/Library/Application Support/JetBrains/Toolbox/apps" 8
;;
Linux)
# Installs: /opt/<ide>/product-info.json — depth 2
scan_root "/opt" 3
# Snap: /snap/<ide>/current/product-info.json — depth 3
scan_root "/snap" 4
# User-local installs and Toolbox
scan_root "$HOME" 4
scan_root "$HOME/.local/share/JetBrains/Toolbox/apps" 6
;;
*)
# MSYS/Cygwin/Git-Bash on Windows — limited support; recommend PowerShell script
if [ -n "${LOCALAPPDATA:-}" ]; then
scan_root "$LOCALAPPDATA/Programs" 3
scan_root "$LOCALAPPDATA/JetBrains/Toolbox/apps" 6
fi
;;
esac
# ---------- build JSON ----------
results=()
for pi in "${pi_paths[@]:-}"; do
[ -f "$pi" ] || continue
locate_launcher "$pi" || continue
[ -n "$EXE_PATH" ] || continue
product_code="$(read_pi_field "$pi" productCode)"
product_name="$(read_pi_field "$pi" name)"
version="$(read_pi_field "$pi" version)"
data_dir_name="$(read_pi_field "$pi" dataDirectoryName)"
product_vendor="$(read_pi_field "$pi" productVendor)"
[ -n "$product_vendor" ] || product_vendor="JetBrains"
[ -n "$product_code" ] || continue
[ -n "$data_dir_name" ] || continue
is_target "$product_code" "$product_name" || continue
plugins_dir="$(plugins_dir_for "$data_dir_name" "$product_vendor")"
if amplicode_installed_in "$plugins_dir"; then
amplicode_installed=true
else
amplicode_installed=false
fi
running_pid="$(ide_running_pid "$data_dir_name" "$product_vendor" || true)"
if [ -n "$running_pid" ]; then
running=true
pid_json="$running_pid"
if is_descendant_of_pid "$running_pid"; then
hosts_current_process=true
else
hosts_current_process=false
fi
else
running=false
pid_json="null"
hosts_current_process=false
fi
if [ -n "$APP_BUNDLE" ]; then
app_bundle_json='"'"$(json_escape "$APP_BUNDLE")"'"'
else
app_bundle_json="null"
fi
edition=""
case "$product_code" in
IU) edition="Ultimate" ;;
IC) edition="Community" ;;
esac
display="$product_name"
[ -n "$edition" ] && display="$product_name $edition"
[ -n "$version" ] && display="$display $version"
entry=""
entry+='{'
entry+='"name":"'"$(json_escape "$display")"'",'
entry+='"dataDirectoryName":"'"$(json_escape "$data_dir_name")"'",'
entry+='"exePath":"'"$(json_escape "$EXE_PATH")"'",'
entry+='"amplicodeInstalled":'"$amplicode_installed"','
entry+='"running":'"$running"','
entry+='"pid":'"$pid_json"','
entry+='"hostsCurrentProcess":'"$hosts_current_process"','
entry+='"appBundle":'"$app_bundle_json"
entry+='}'
results+=("$entry")
done
# Emit JSON array
if [ "${#results[@]}" -eq 0 ]; then
printf '[]\n'
else
printf '['
for i in "${!results[@]}"; do
[ "$i" -gt 0 ] && printf ','
printf '%s' "${results[$i]}"
done
printf ']\n'
fi