Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions bun.lock
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"lockfileVersion": 1,
"configVersion": 0,
"workspaces": {
"": {
"name": "registry",
Expand Down
37 changes: 37 additions & 0 deletions registry/coder/modules/jetbrains/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,43 @@ module "jetbrains" {
}
```

### Plugin Auto‑Installer
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The section title uses a non-breaking hyphen character (U+2011) instead of a standard hyphen. While this may render correctly in most contexts, it could cause issues with some text editors or search functionality. Consider using a standard hyphen:

### Plugin Auto-Installer
Suggested change
### Plugin AutoInstaller
### Plugin Auto-Installer

Copilot uses AI. Check for mistakes.

This module now supports automatic JetBrains plugin installation inside your workspace.

To get a plugin ID, open the plugin’s page on the JetBrains Marketplace. Scroll down to Additional Information and look for Plugin ID. Use that value in the configuration below.

```tf
module "jetbrains" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jetbrains/coder"
version = "1.2.1"
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The version in the example should match the actual module version being released. The PR metadata indicates this is for version v1.1.0, but the example shows version = "1.2.1". Update to the correct version or use a placeholder that clearly indicates users should check for the latest version.

Copilot uses AI. Check for mistakes.
agent_id = coder_agent.main.id
folder = "/home/coder/project"
default = ["IU", "PY"]

jetbrains_plugins = {
"PY" = ["com.koxudaxi.pydantic", "com.intellij.kubernetes"]
"IU" = ["<Plugin-ID>", "<Plugin-ID>"]
"WS" = ["<Plugin-ID>", "<Plugin-ID>"]
"GO" = ["<Plugin-ID>", "<Plugin-ID>"]
"CL" = ["<Plugin-ID>", "<Plugin-ID>"]
"PS" = ["<Plugin-ID>", "<Plugin-ID>"]
"RD" = ["<Plugin-ID>", "<Plugin-ID>"]
"RM" = ["<Plugin-ID>", "<Plugin-ID>"]
"RR" = ["<Plugin-ID>", "<Plugin-ID>"]
}
}
Comment on lines +155 to +166
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation should mention important prerequisites and limitations:

  1. Requires JetBrains Toolbox to be installed
  2. Requires jq to be available
  3. Only works on Debian/Ubuntu-based systems (due to apt-get usage)
  4. Plugins are installed when workspace starts, but may take time depending on IDE availability

Consider adding a "Requirements" or "Prerequisites" subsection before the example.

Copilot uses AI. Check for mistakes.
```

> [!IMPORTANT]\
> This module prerequisites and limitations
>
> 1. Requires JetBrains Toolbox to be installed
> 2. Requires jq to be available
> 3. Only works on Debian/Ubuntu-based systems (due to apt-get usage)
> 4. Plugins are installed when workspace starts, but may take time depending on IDE availability

### Accessing the IDE Metadata

You can now reference the output `ide_metadata` as a map.
Expand Down
36 changes: 36 additions & 0 deletions registry/coder/modules/jetbrains/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,13 @@ variable "ide_config" {
}
}

variable "jetbrains_plugins" {
type = map(list(string))
description = "Map of IDE product codes to plugin ID lists. Example: { IU = [\"com.foo\"], GO = [\"org.bar\"] }."
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The variable description should clarify what valid product codes are and provide a reference. Consider improving the description:

variable "jetbrains_plugins" {
  type        = map(list(string))
  description = "Map of IDE product codes to plugin ID lists. Valid codes: CL, GO, IU, PS, PY, RD, RM, RR, WS. Example: { IU = [\"com.foo.bar\"], PY = [\"org.example.plugin\"] }. Find plugin IDs at https://plugins.jetbrains.com/"
  default     = {}
}
Suggested change
description = "Map of IDE product codes to plugin ID lists. Example: { IU = [\"com.foo\"], GO = [\"org.bar\"] }."
description = "Map of JetBrains IDE product codes to plugin ID lists. Valid codes: CL (CLion), GO (GoLand), IU (IntelliJ IDEA Ultimate), PS (PhpStorm), PY (PyCharm), RD (Rider), RM (RubyMine), RR (ReSharper), WS (WebStorm). Example: { IU = [\"com.foo\"], GO = [\"org.bar\"] }. Find plugin IDs at https://plugins.jetbrains.com/."

Copilot uses AI. Check for mistakes.
default = {}
}


locals {
# Parse HTTP responses once with error handling for air-gapped environments
parsed_responses = {
Expand Down Expand Up @@ -203,6 +210,8 @@ locals {

# Convert the parameter value to a set for for_each
selected_ides = length(var.default) == 0 ? toset(jsondecode(coalesce(data.coder_parameter.jetbrains_ides[0].value, "[]"))) : toset(var.default)

plugin_map_b64 = base64encode(jsonencode(var.jetbrains_plugins))
}

data "coder_parameter" "jetbrains_ides" {
Expand Down Expand Up @@ -230,6 +239,33 @@ data "coder_parameter" "jetbrains_ides" {
data "coder_workspace" "me" {}
data "coder_workspace_owner" "me" {}

resource "coder_script" "store_plugins" {
count = length(var.jetbrains_plugins) > 0 ? 1 : 0
agent_id = var.agent_id
display_name = "Store JetBrains Plugins List"
run_on_start = true
script = <<-EOT
#!/bin/sh
set -eu

mkdir -p "$HOME/.config/jetbrains"
echo -n "${local.plugin_map_b64}" | base64 -d > "$HOME/.config/jetbrains/plugins.json"
chmod 600 "$HOME/.config/jetbrains/plugins.json"
EOT
}

resource "coder_script" "install_jetbrains_plugins" {
count = length(var.jetbrains_plugins) > 0 ? 1 : 0
agent_id = var.agent_id
display_name = "Install JetBrains Plugins"
run_on_start = true
depends_on = [coder_script.store_plugins]

script = <<-EOT
${file("${path.module}/script/install_plugins.sh")}
EOT
}

resource "coder_app" "jetbrains" {
for_each = local.selected_ides
agent_id = var.agent_id
Expand Down
167 changes: 167 additions & 0 deletions registry/coder/modules/jetbrains/script/install_plugins.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
#!/usr/bin/env bash
set -euo pipefail

LOGFILE="$HOME/.config/jetbrains/install_plugins.log"
TOOLBOX_BASE="$HOME/.local/share/JetBrains/Toolbox/apps"
PLUGIN_MAP_FILE="$HOME/.config/jetbrains/plugins.json"

if command -v apt-get > /dev/null 2>&1; then
sudo apt-get update
sudo apt-get install -y libfreetype6
else
echo "Warning: 'apt-get' not found. Please ensure 'libfreetype6' is installed manually for your distribution." >&2
fi

mkdir -p "$(dirname "$LOGFILE")"

exec > >(tee -a "$LOGFILE") 2>&1

log() {
printf '%s %s\n' "$(date --iso-8601=seconds)" "$*" | tee -a "$LOGFILE"
}

# -------- Read plugin JSON --------
get_enabled_codes() {
jq -r 'keys[]' "$PLUGIN_MAP_FILE"
}

get_plugins_for_code() {
jq -r --arg CODE "$1" '.[$CODE][]?' "$PLUGIN_MAP_FILE" 2> /dev/null || true
}

# -------- Product code mapping --------
map_folder_to_code() {
case "$1" in
*pycharm*) echo "PY" ;;
*idea*) echo "IU" ;;
*webstorm*) echo "WS" ;;
Comment on lines +33 to +37

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Make folder-to-code mapping case-insensitive

The folder mapping uses lowercase globs (*pycharm*, *idea*, etc.), but JetBrains Toolbox installs create uppercase product directories (e.g., IDEA-U, PyCharm-P) on Linux. Because Bash pattern matching is case-sensitive, these branches never match, leaving code empty, so the main loop never processes the detected IDEs and the installer spins forever without installing plugins. Normalizing the folder name or enabling case-insensitive matching is needed for Toolbox layouts.

Useful? React with 👍 / 👎.

*goland*) echo "GO" ;;
*clion*) echo "CL" ;;
*phpstorm*) echo "PS" ;;
*rider*) echo "RD" ;;
*rubymine*) echo "RM" ;;
*rustrover*) echo "RR" ;;
*) echo "" ;;
esac
}

# -------- CLI launcher names --------
launcher_for_code() {
case "$1" in
PY) echo "pycharm" ;;
IU) echo "idea" ;;
WS) echo "webstorm" ;;
GO) echo "goland" ;;
CL) echo "clion" ;;
PS) echo "phpstorm" ;;
RD) echo "rider" ;;
RM) echo "rubymine" ;;
RR) echo "rustrover" ;;
*) return 1 ;;
esac
}

find_cli_launcher() {
local exe
exe="$(launcher_for_code "$1")" || return 1

# Look for the newest version directory
local latest_version
latest_version=$(find "$2" -maxdepth 2 -type d -name "ch-*" 2> /dev/null | sort -V | tail -1)

if [ -n "$latest_version" ] && [ -f "$latest_version/bin/$exe" ]; then
echo "$latest_version/bin/$exe"
else
return 1
fi
}

install_plugin() {
log "Installing plugin: $2"
if "$1" installPlugins "$2"; then
log "Successfully installed plugin: $2"
else
log "Failed to install plugin: $2"
return 1
fi
}

# -------- Main --------
log "Plugin installer started"

if [ ! -f "$PLUGIN_MAP_FILE" ]; then
log "No plugins.json found. Exiting."
exit 0
fi

# Load list of IDE codes user actually needs
mapfile -t pending_codes < <(get_enabled_codes)

if [ ${#pending_codes[@]} -eq 0 ]; then
log "No plugin entries found. Exiting."
Comment on lines +97 to +101

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Filter plugins to selected IDEs before waiting

pending_codes is populated from every key in plugins.json, regardless of which IDEs were actually selected or installed. If a user supplies plugin lists for more products than they enable (e.g., the README example lists all codes while default = ["IU", "PY"]), the while loop never clears the extra entries and sleeps forever, so the script never finishes. pending_codes should be intersected with the selected/installed IDE set before entering the loop to avoid hanging.

Useful? React with 👍 / 👎.

exit 0
fi

log "Waiting for IDE installation. Pending codes: ${pending_codes[*]}"

MAX_ATTEMPTS=60 # 10 minutes
attempt=0

# Loop until all plugins installed
while [ ${#pending_codes[@]} -gt 0 ] && [ $attempt -lt $MAX_ATTEMPTS ]; do

if [ ! -d "$TOOLBOX_BASE" ]; then
log "Toolbox directory not found yet, waiting..."
sleep 10
continue
fi

Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The glob pattern "$TOOLBOX_BASE"/* will match the literal string if the directory doesn't exist or is empty. This could lead to unexpected behavior. Consider checking if the directory exists first:

if [ ! -d "$TOOLBOX_BASE" ]; then
  log "Toolbox directory not found yet, waiting..."
  sleep 10
  continue
fi

for product_dir in "$TOOLBOX_BASE"/*; do
  [ -d "$product_dir" ] || continue
  # ... rest of logic
done
Suggested change
if [ ! -d "$TOOLBOX_BASE" ]; then
log "Toolbox directory not found yet, waiting..."
sleep 10
continue
fi

Copilot uses AI. Check for mistakes.
for product_dir in "$TOOLBOX_BASE"/*; do
[ -d "$product_dir" ] || continue

product_name="$(basename "$product_dir")"
code="$(map_folder_to_code "$product_name")"

# Only process codes user requested
if [[ ! " ${pending_codes[*]} " =~ " $code " ]]; then
continue
fi

cli_launcher="$(find_cli_launcher "$code" "$product_dir")" || continue
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The script looks for the CLI launcher in $product_dir/bin/$exe, but this path might not match the actual Toolbox installation structure. JetBrains Toolbox typically installs IDEs in versioned subdirectories like ~/.local/share/JetBrains/Toolbox/apps/PyCharm-P/ch-0/242.23726.102/bin/pycharm. The script needs to find the correct version directory. Consider:

find_cli_launcher() {
  local exe
  exe="$(launcher_for_code "$1")" || return 1
  
  # Look for the newest version directory
  local latest_version
  latest_version=$(find "$2" -maxdepth 2 -type d -name "ch-*" 2>/dev/null | sort -V | tail -1)
  
  if [ -n "$latest_version" ] && [ -f "$latest_version/bin/$exe" ]; then
    echo "$latest_version/bin/$exe"
  else
    return 1
  fi
}

Copilot uses AI. Check for mistakes.

log "Detected IDE $code at $product_dir"

plugins="$(get_plugins_for_code "$code")"
if [ -z "$plugins" ]; then
log "No plugins for $code"
continue
fi

while read -r plugin; do
install_plugin "$cli_launcher" "$plugin"
done <<< "$plugins"

# remove code from pending list after success
tmp=()
for c in "${pending_codes[@]}"; do
[ "$c" != "$code" ] && tmp+=("$c")
done
pending_codes=("${tmp[@]}")

log "Finished $code. Remaining: ${pending_codes[*]:-none}"

done

# If still pending, wait and retry
if [ ${#pending_codes[@]} -gt 0 ]; then
sleep 10
((attempt++))
fi
done

if [ ${#pending_codes[@]} -gt 0 ]; then
log "Timeout: IDEs not found: ${pending_codes[*]}"
exit 1
fi

log "All plugins installed. Exiting."
Loading