feature (jetbrains-plugins): add module for installing jetbrains plugin (#772)

Co-authored-by: DevCats <christofer@coder.com>
Co-authored-by: DevCats <chris@dualriver.com>
This commit is contained in:
Harsh Singh Panwar
2026-04-22 09:17:53 +05:30
committed by GitHub
parent b72577707c
commit b108185c14
6 changed files with 417 additions and 0 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

+11
View File
@@ -0,0 +1,11 @@
---
display_name: Harsh Singh Panwar
bio: Open source contributor
github: Harsh9485
avatar: ./.images/avatar.png
status: community
---
# Harsh Singh Panwar
Community modules for Coder workspaces.
@@ -0,0 +1,80 @@
---
display_name: JetBrains Plugin Installer
description: Companion module for coder/jetbrains that automatically installs JetBrains Marketplace plugins.
icon: ../../../../.icons/jetbrains.svg
tags: [ide, jetbrains, plugins]
---
# JetBrains Plugin Installer
A companion module for
[coder/jetbrains](https://registry.coder.com/modules/jetbrains) that
automatically installs JetBrains Marketplace plugins into your workspace.
Use this alongside the core `coder/jetbrains` module — it handles plugin
installation while `coder/jetbrains` handles IDE setup and Toolbox
integration.
```tf
module "jetbrains_plugins" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/harsh9485/jetbrains-plugins/coder"
version = "0.1.0"
agent_id = coder_agent.main.id
jetbrains_plugins = {
"PY" = ["com.koxudaxi.pydantic", "com.intellij.kubernetes"]
}
}
```
## Prerequisites
- The [coder/jetbrains](https://registry.coder.com/modules/jetbrains)
module (or equivalent JetBrains Toolbox setup) must already be
configured in your template.
- `jq` must be available on `PATH`.
- Linux environment only.
## Finding Plugin IDs
Open the plugin page on the
[JetBrains Marketplace](https://plugins.jetbrains.com/). Scroll to
**Additional Information** and copy the **Plugin ID**.
## Usage
```tf
module "jetbrains" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jetbrains/coder"
version = "1.4.0"
agent_id = coder_agent.main.id
folder = "/home/coder/project"
default = ["PY", "GO"]
}
module "jetbrains_plugins" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/harsh9485/jetbrains-plugins/coder"
version = "0.1.0"
agent_id = coder_agent.main.id
jetbrains_plugins = {
"PY" = ["com.koxudaxi.pydantic", "com.intellij.kubernetes"]
"GO" = ["org.jetbrains.plugins.go-template"]
}
}
```
The keys in `jetbrains_plugins` are IDE product codes (`PY`, `GO`, `IU`,
etc.) matching the codes used by the `coder/jetbrains` module. Each value
is a list of Marketplace plugin IDs to install for that IDE.
> [!IMPORTANT]
> After installing the IDE, restart the workspace. On the next start the
> module detects installed IDEs and automatically installs the configured
> plugins.
Some plugins may be disabled by default due to JetBrains security
defaults — you might need to enable them manually in the IDE.
@@ -0,0 +1,44 @@
run "no_script_when_plugins_empty" {
command = plan
variables {
agent_id = "foo"
jetbrains_plugins = {}
}
assert {
condition = length(resource.coder_script.install_jetbrains_plugins) == 0
error_message = "Expected no plugin install script when plugins map is empty"
}
}
run "script_created_when_plugins_provided" {
command = plan
variables {
agent_id = "foo"
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_invalid_product_code" {
command = plan
variables {
agent_id = "foo"
jetbrains_plugins = {
"INVALID" = ["com.example.plugin"]
}
}
expect_failures = [
var.jetbrains_plugins,
]
}
@@ -0,0 +1,59 @@
terraform {
required_version = ">= 1.9"
required_providers {
coder = {
source = "coder/coder"
version = ">= 2.5"
}
}
}
variable "agent_id" {
type = string
description = "The resource ID of a Coder agent."
}
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 = {}
validation {
condition = alltrue([
for code in keys(var.jetbrains_plugins) : contains(
["CL", "GO", "IU", "PS", "PY", "RD", "RM", "RR", "WS"], code
)
])
error_message = "Keys must be valid JetBrains product codes: CL, GO, IU, PS, PY, RD, RM, RR, WS."
}
}
locals {
plugin_map_b64 = base64encode(jsonencode(var.jetbrains_plugins))
plugin_install_script = file("${path.module}/scripts/install_plugins.sh")
}
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
EOT
}
@@ -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."