Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
5b29270
add support pre-install plugins
Harsh9485 Dec 2, 2025
429b629
Merge branch 'main' into Pre-install-JetBrains-plugins
Harsh9485 Dec 2, 2025
b7df74c
enhance the script
Harsh9485 Dec 3, 2025
408eaee
Merge branch 'main' into Pre-install-JetBrains-plugins
Harsh9485 Dec 3, 2025
2b616fc
fix the style issue
Harsh9485 Dec 3, 2025
a95f904
Merge branch 'Pre-install-JetBrains-plugins' of github.com:Harsh9485/…
Harsh9485 Dec 3, 2025
8c98af6
add the example in readme
Harsh9485 Dec 3, 2025
3b6c743
fix the style issue
Harsh9485 Dec 3, 2025
c261779
add ai suggestions
Harsh9485 Dec 3, 2025
c6ae736
fix style issue
Harsh9485 Dec 3, 2025
38fc8be
Merge branch 'main' into Pre-install-JetBrains-plugins
Harsh9485 Dec 17, 2025
0ed1293
fix GFM readme error
Harsh9485 Dec 17, 2025
1ea4d43
Merge branch 'main' into Pre-install-JetBrains-plugins
DevelopmentCats Dec 30, 2025
17edbe5
Merge branch 'main' into Pre-install-JetBrains-plugins
Harsh9485 Dec 31, 2025
6db2e85
Merge branch 'main' into Pre-install-JetBrains-plugins
Harsh9485 Jan 6, 2026
56235ab
Merge branch 'main' into Pre-install-JetBrains-plugins
DevelopmentCats Jan 9, 2026
b019943
Merge branch 'main' into Pre-install-JetBrains-plugins
Harsh9485 Jan 15, 2026
8c6fd4e
test: separate the script to the coder script resource
Harsh9485 Jan 20, 2026
4af40a2
Merge branch 'coder:main' into Pre-install-JetBrains-plugins
Harsh9485 Jan 20, 2026
f9f252d
Merge branch 'Pre-install-JetBrains-plugins' of github.com:Harsh9485/…
Harsh9485 Jan 20, 2026
35bb793
fix: separate the script to the coder_script and fix some issue in sc…
Harsh9485 Jan 20, 2026
498de9e
clean up
Harsh9485 Jan 21, 2026
2ea28e5
clean up
Harsh9485 Jan 21, 2026
0dce07b
clean up
Harsh9485 Jan 21, 2026
7d68fc5
Merge branch 'main' into Pre-install-JetBrains-plugins
Harsh9485 Jan 23, 2026
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
37 changes: 37 additions & 0 deletions registry/coder/modules/jetbrains/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,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 +154 to +165
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
44 changes: 44 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 @@ -214,6 +221,10 @@ 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))

plugin_install_script = file("${path.module}/script/install_plugins.sh")
}

data "coder_parameter" "jetbrains_ides" {
Expand Down Expand Up @@ -241,6 +252,39 @@ 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
#!/bin/bash
set -o errexit
set -o pipefail
echo -n '${base64encode(local.plugin_install_script)}' | base64 -d > /tmp/install_plugins.sh
chmod +x /tmp/install_plugins.sh
nohup /tmp/install_plugins.sh > /tmp/install_plugins.log 2>&1 &
exit 0
EOT
}

resource "coder_app" "jetbrains" {
for_each = local.selected_ides
agent_id = var.agent_id
Expand Down
168 changes: 168 additions & 0 deletions registry/coder/modules/jetbrains/script/install_plugins.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
#!/bin/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"
elif [ -f "$2/bin/$exe" ]; then
echo "$2/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 +99 to +103

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 120
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 60
((attempt++))
fi
done

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

log "All plugins installed. Exiting."