Skip to content
Open
Show file tree
Hide file tree
Changes from 8 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
29 changes: 29 additions & 0 deletions registry/coder/modules/jetbrains/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,35 @@ 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.
```

### Accessing the IDE Metadata

You can now reference the output `ide_metadata` as a map.
Expand Down
34 changes: 34 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,31 @@ data "coder_parameter" "jetbrains_ides" {
data "coder_workspace" "me" {}
data "coder_workspace_owner" "me" {}

resource "coder_script" "store_plugins" {
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
}
Comment on lines 242 to 255
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 store_plugins script always runs, even when jetbrains_plugins is empty (default {}). This creates an empty JSON file unnecessarily. Consider adding a condition to only create these resources when plugins are actually configured:

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
}

Apply the same pattern to coder_script.install_jetbrains_plugins.

Copilot uses AI. Check for mistakes.

resource "coder_script" "install_jetbrains_plugins" {
agent_id = var.agent_id
display_name = "Install JetBrains Plugins"
run_on_start = true

script = <<-EOT

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.

There's an unnecessary blank line at the beginning of the heredoc. This will be included in the script output. Consider removing it:

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

Copilot uses AI. Check for mistakes.
${file("${path.module}/script/install_plugins.sh")}
EOT
}
Comment on lines 257 to 267
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 install_jetbrains_plugins script depends on the plugins.json file created by store_plugins, but there's no explicit dependency between these resources. While both have run_on_start = true, Terraform doesn't guarantee execution order without an explicit dependency. Add a depends_on to ensure proper ordering:

resource "coder_script" "install_jetbrains_plugins" {
  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
}

Copilot uses AI. Check for mistakes.

resource "coder_app" "jetbrains" {
for_each = local.selected_ides
agent_id = var.agent_id
Expand Down
139 changes: 139 additions & 0 deletions registry/coder/modules/jetbrains/script/install_plugins.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
#!/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"

sudo apt-get update
sudo apt-get install -y libfreetype6
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 uses jq to parse JSON but doesn't install it or check if it's available before use. This will cause the script to fail silently when jq commands are executed (lines 21, 25).

Consider either:

  1. Installing jq alongside libfreetype6: sudo apt-get install -y libfreetype6 jq
  2. Adding a command check similar to other modules: if ! command -v jq > /dev/null; then echo "jq is not installed"; exit 1; fi
Suggested change
sudo apt-get install -y libfreetype6
sudo apt-get install -y libfreetype6 jq

Copilot uses AI. Check for mistakes.
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.

Running apt-get update and apt-get install with sudo assumes the script runs in a Debian/Ubuntu environment with sudo privileges. This may not work on other distributions (e.g., Alpine, Fedora) or systems without sudo access. Consider adding a check for the package manager or documenting this requirement in the module's README.

Suggested change
sudo apt-get update
sudo apt-get install -y libfreetype6
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

Copilot uses AI. Check for mistakes.

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

if [ -f "$2/bin/$exe" ]; then
echo "$2/bin/$exe"

Choose a reason for hiding this comment

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

P1 Badge Search JetBrains CLI inside channel/build directories

find_cli_launcher currently looks only for $product_dir/bin/<launcher> under ~/.local/share/JetBrains/Toolbox/apps, but Toolbox installs place binaries under channel/build folders such as <product>/ch-0/<build>/bin/idea.sh. For any normal install this check always fails, so cli_launcher stays empty, pending_codes is never reduced, and the installer loops forever without installing plugins. The search needs to descend into the channel/build subdirectories to locate the CLI.

Useful? React with 👍 / 👎.

else
return 1
fi
}

install_plugin() {
log "Installing plugin: $2"
"$1" installPlugins "$2"
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 install_plugin function doesn't check if the plugin installation succeeds. If the IDE CLI command fails (e.g., invalid plugin ID, network issues, or CLI not supporting installPlugins), the script continues without error. Consider capturing the exit status and logging failures:

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
}
Suggested change
"$1" installPlugins "$2"
if "$1" installPlugins "$2"; then
log "Successfully installed plugin: $2"
else
log "Failed to install plugin: $2"
return 1
fi

Copilot uses AI. Check for mistakes.
}

# -------- 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[*]}"

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

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
fi
done
Comment on lines 110 to 160
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 infinite while loop continues polling every 10 seconds even if no IDEs will ever be installed. If an IDE in jetbrains_plugins is never installed via Toolbox, this script will run indefinitely. Consider adding:

  1. A maximum retry count or timeout
  2. A way to skip codes that will never be available (e.g., if user only installs a subset of requested IDEs)

Example:

MAX_ATTEMPTS=60  # 10 minutes
attempt=0
while [ ${#pending_codes[@]} -gt 0 ] && [ $attempt -lt $MAX_ATTEMPTS ]; do
  # ... existing loop logic ...
  if [ ${#pending_codes[@]} -gt 0 ]; then
    sleep 10
    ((attempt++))
  fi
done

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

Copilot uses AI. Check for mistakes.

log "All plugins installed. Exiting."