Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
0afb18e
feat: add pre-install plugins support
Harsh9485 Feb 28, 2026
f0d55e1
fix: typos
Harsh9485 Feb 28, 2026
d5852fd
fix: update version
Harsh9485 Feb 28, 2026
2562d2a
Merge branch 'main' into Pre-install-jetBrains-Plugins-Support-(Insta…
DevelopmentCats Mar 2, 2026
0fe965a
Merge branch 'main' into Pre-install-jetBrains-Plugins-Support-(Insta…
Harsh9485 Mar 9, 2026
28b75d5
Merge branch 'main' into Pre-install-jetBrains-Plugins-Support-(Insta…
Harsh9485 Mar 10, 2026
79a852f
feat|fix: add check_plugins_installed and mark_plugins_installed, and…
Harsh9485 Mar 10, 2026
d7667b1
Merge branch 'Pre-install-jetBrains-Plugins-Support-(InstallPlugins)'…
Harsh9485 Mar 10, 2026
85077e2
fix: the plugins variable and style
Harsh9485 Mar 10, 2026
0a94145
Merge branch 'main' into Pre-install-jetBrains-Plugins-Support-(Insta…
Harsh9485 Mar 10, 2026
d4ebd2d
fix: script throws an error if any IDE plugins remain uninstalled
Harsh9485 Mar 10, 2026
15c7a43
Merge branch 'Pre-install-jetBrains-Plugins-Support-(InstallPlugins)'…
Harsh9485 Mar 10, 2026
998c677
fix: log duplication and add warning that some plugins are disabled b…
Harsh9485 Mar 11, 2026
6af22cc
fix: change the location of CONFIG_DIR
Harsh9485 Mar 11, 2026
a2aa95a
docs: improve wording in README
Harsh9485 Mar 11, 2026
58bd453
Merge branch 'main' into Pre-install-jetBrains-Plugins-Support-(Insta…
Harsh9485 Mar 11, 2026
7f445eb
Merge branch 'main' into Pre-install-jetBrains-Plugins-Support-(Insta…
Harsh9485 Mar 13, 2026
25cf7ee
fix: make script compatible with all distributions
Harsh9485 Mar 13, 2026
d7e2314
Merge branch 'Pre-install-jetBrains-Plugins-Support-(InstallPlugins)'…
Harsh9485 Mar 13, 2026
9a9fca9
Merge branch 'main' into Pre-install-jetBrains-Plugins-Support-(Insta…
Harsh9485 Mar 13, 2026
1d82a88
Merge branch 'main' into Pre-install-jetBrains-Plugins-Support-(Insta…
Harsh9485 Mar 16, 2026
34a82f9
Merge branch 'main' into Pre-install-jetBrains-Plugins-Support-(Insta…
Harsh9485 Mar 20, 2026
0aead61
fix: remove incompatible and unnecessary code
Harsh9485 Mar 21, 2026
871708c
Merge branch 'Pre-install-jetBrains-Plugins-Support-(InstallPlugins)'…
Harsh9485 Mar 21, 2026
d7533bb
Merge branch 'main' into Pre-install-jetBrains-Plugins-Support-(Insta…
Harsh9485 Mar 21, 2026
670bdeb
Merge branch 'main' into Pre-install-jetBrains-Plugins-Support-(Insta…
Harsh9485 Mar 30, 2026
19fa569
Merge branch 'main' into Pre-install-jetBrains-Plugins-Support-(Insta…
Harsh9485 Apr 2, 2026
edf6b7e
fix: resolve conflict in main.tf
Harsh9485 Apr 2, 2026
ad80aab
Merge branch 'main' into Pre-install-jetBrains-Plugins-Support-(Insta…
DevelopmentCats Apr 3, 2026
fdba29c
chore: update JetBrains directory path
Harsh9485 Apr 4, 2026
200c6d7
Merge branch 'Pre-install-jetBrains-Plugins-Support-(InstallPlugins)'…
Harsh9485 Apr 4, 2026
3f52c8e
Merge branch 'main' into Pre-install-jetBrains-Plugins-Support-(Insta…
Harsh9485 Apr 4, 2026
b974900
Merge branch 'main' into Pre-install-jetBrains-Plugins-Support-(Insta…
Harsh9485 Apr 6, 2026
377c57f
Merge branch 'main' into Pre-install-jetBrains-Plugins-Support-(Insta…
Harsh9485 Apr 9, 2026
f010501
Merge branch 'main' into Pre-install-jetBrains-Plugins-Support-(Insta…
Harsh9485 Apr 9, 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
42 changes: 42 additions & 0 deletions registry/coder/modules/jetbrains/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,48 @@ module "jetbrains" {
}
```

### Plugin Auto‑Installer

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"
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>"]
}
}
```

> [!IMPORTANT]
> After installing the IDE, restart the workspace.
> When the workspace starts again, the scripts will detect the installed IDE and automatically install the configured plugins.
>
> This module prerequisites and limitations
>
> 1. Requires JetBrains Toolbox to be installed
> 2. Requires jq to be available
> 3. only works in a Linux environment.

> [!WARNING]
> Some plugins are disabled by default due to JetBrains security defaults, so you might need to enable them manually.

### 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/jetbrains.tftest.hcl
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,40 @@ run "validate_output_schema" {
}
}

run "no_plugin_script_when_plugins_empty" {
command = plan

variables {
agent_id = "foo"
folder = "/home/coder"
default = ["PY"]
jetbrains_plugins = {}
}

assert {
condition = length(resource.coder_script.install_jetbrains_plugins) == 0
error_message = "Expected no plugin install script when plugins list is empty"
}
}

run "plugin_script_created_when_plugins_provided" {
command = plan

variables {
agent_id = "foo"
folder = "/home/coder"
default = ["PY"]
jetbrains_plugins = {
"PY" = ["com.koxudaxi.pydantic", "com.intellij.kubernetes"]
}
}

assert {
condition = length(resource.coder_script.install_jetbrains_plugins) == 1
error_message = "Expected script to be created when plugins are provided"
}
}

run "rejects_major_version_with_ide_config" {
command = plan

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 @@ -189,6 +189,12 @@ 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\"] }."
default = {}
}

locals {
# Static IDE metadata for name and icon lookups when ide_config is null.
ide_metadata = {
Expand Down Expand Up @@ -247,6 +253,10 @@ locals {
json_data = var.ide_config != null ? null : local.selected_releases[code]
}
}

plugin_map_b64 = base64encode(jsonencode(var.jetbrains_plugins))

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

data "coder_parameter" "jetbrains_ides" {
Expand Down Expand Up @@ -274,6 +284,30 @@ data "coder_parameter" "jetbrains_ides" {
data "coder_workspace" "me" {}
data "coder_workspace_owner" "me" {}

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

script = <<-EOT
#!/bin/bash
set -o errexit
set -o pipefail

CONFIG_DIR="$HOME/.config/Jetbrains"

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

echo -n '${base64encode(local.plugin_install_script)}' | base64 -d > /tmp/install_plugins.sh
chmod +x /tmp/install_plugins.sh

/tmp/install_plugins.sh > /tmp/install_plugins.log 2>&1
EOT
}

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

# Verify jq is available
if ! command -v jq > /dev/null 2>&1; then
echo "Error: 'jq' is required but not installed. Please install it manually." >&2
exit 1
fi

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

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

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

# -------- 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
}

# Returns only plugins that are NOT already installed
check_plugins_installed() {
local code="$1"
shift
local plugins=("$@")

local installed_file="$PLUGIN_ALREADY_INSTALLED_MAP/${code}_installed.json"

# If no installed file exists, all plugins need to be installed
if [ ! -f "$installed_file" ]; then
printf '%s\n' "${plugins[@]}"
return 0
fi

installed_plugins=$(jq -r '.[]?' "$installed_file" 2> /dev/null)

for plugin in "${plugins[@]}"; do
if ! echo "$installed_plugins" | grep -Fxq "$plugin"; then
echo "$plugin"
fi
done
return 0
}

# -------- Product code mapping --------
map_folder_to_code() {
case "$1" in
*pycharm*) echo "PY" ;;
*idea*) echo "IU" ;;
*webstorm*) echo "WS" ;;
*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
}

# Marks a plugin as installed by adding it to the installed plugins JSON file
mark_plugins_installed() {
local code="$1"
local plugin="$2"

local installed_file="$PLUGIN_ALREADY_INSTALLED_MAP/${code}_installed.json"

mkdir -p "$PLUGIN_ALREADY_INSTALLED_MAP"

# Create file with empty array if it doesn't exist
if [ ! -f "$installed_file" ]; then
echo '[]' > "$installed_file" || {
log "Error: Failed to create $installed_file"
return 1
}
fi

jq --arg PLUGIN "$plugin" '. += [$PLUGIN]' "$installed_file" > "${installed_file}.tmp" 2> /dev/null \
&& mv "${installed_file}.tmp" "$installed_file" || {
log "Error: Failed to update $installed_file with plugin $plugin"
rm -f "${installed_file}.tmp"
return 1
}
log "Marked plugin as installed: $plugin"
return 0
}

install_plugin() {
log "Installing plugin: $2"
if "$1" installPlugins "$2"; then
log "Successfully installed plugin: $2"
return 0
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

if [ ! -d "$TOOLBOX_BASE" ]; then
log "Toolbox directory not 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."
exit 0
fi

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

# Loop until all plugins installed
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

# Store plugins as array for consistency
mapfile -t plugins_list < <(get_plugins_for_code "$code")
if [ ${#plugins_list[@]} -eq 0 ]; then
log "No plugins for $code"
continue
fi

# Get only plugins that are not already installed
mapfile -t new_plugins < <(check_plugins_installed "$code" "${plugins_list[@]}")
if [ ${#new_plugins[@]} -eq 0 ]; then
log "All plugins for $code are already installed"
# Remove code from pending list since all plugins are installed
tmp=()
for c in "${pending_codes[@]}"; do
[ "$c" != "$code" ] && tmp+=("$c")
done
pending_codes=("${tmp[@]}")
continue
fi

cli_launcher_path="$(find_cli_launcher "$code" "$product_dir")" || continue
log "Detected IDE $code at $product_dir"
log "Plugins to install for $code: ${#new_plugins[@]} plugin(s)"

# Install only the plugins that are not yet installed
for plugin in "${new_plugins[@]}"; do
if install_plugin "$cli_launcher_path" "$plugin"; then
# Mark plugin as installed after successful installation
mark_plugins_installed "$code" "$plugin"
fi
done

# 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 [ ${#pending_codes[@]} -gt 0 ]; then
log "These IDEs not found: ${pending_codes[*]}"
fi

log "Exiting."
Loading