Skip to content
Draft
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
25 changes: 25 additions & 0 deletions registry/coder/modules/jetbrains/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,31 @@ module "jetbrains" {
}
```

### Plugin Pre-installation

Automatically install plugins when the workspace starts. Find plugin IDs on the [JetBrains Marketplace](https://plugins.jetbrains.com/) in the "Additional Information" section of each plugin page.

```tf
module "jetbrains" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jetbrains/coder"
version = "1.3.0"
agent_id = coder_agent.example.id
folder = "/home/coder/project"
default = ["IU", "PY"]

# Pre-install plugins
plugins = [
"com.intellij.plugins.terminal", # Terminal plugin
"org.rust.lang", # Rust language support
"com.github.copilot", # GitHub Copilot
]
}
```

> [!NOTE]
> Plugins are installed on workspace startup. If an IDE is not yet installed via Toolbox, plugins will be installed during the first IDE launch.

### Early Access Preview (EAP) Versions

```tf
Expand Down
198 changes: 198 additions & 0 deletions registry/coder/modules/jetbrains/install-plugins.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
#!/usr/bin/env bash
# install-plugins.sh - Install JetBrains plugins in workspace IDEs

set -euo pipefail

PLUGINS="${PLUGINS}"
PLUGIN_INSTALL_ARGS="${PLUGIN_INSTALL_ARGS}"
FOLDER="${FOLDER}"
IDE_METADATA='${IDE_METADATA}'

BOLD='\033[0;1m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
RESET='\033[0m'

echo -e "$${BOLD}🔌 JetBrains Plugin Installer$${RESET}"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"

# Parse plugin list
IFS=',' read -r -a PLUGIN_LIST <<< "$PLUGINS"

if [ $${#PLUGIN_LIST[@]} -eq 0 ] || [ -z "$${PLUGIN_LIST[0]}" ]; then
echo "No plugins to install."
exit 0
fi

echo "Plugins to install: $${#PLUGIN_LIST[@]}"
for plugin in "$${PLUGIN_LIST[@]}"; do
echo " - $plugin"
done
echo ""

# Parse IDE metadata using jq if available, otherwise use basic parsing
if command -v jq &> /dev/null; then
IDE_CODES=$(echo "$IDE_METADATA" | jq -r 'keys[]')
else
# Fallback: extract IDE codes without jq
IDE_CODES=$(echo "$IDE_METADATA" | grep -o '"[A-Z][A-Z]"' | tr -d '"' | sort -u)
fi

if [ -z "$IDE_CODES" ]; then
echo -e "$${YELLOW}⚠️ No IDEs selected. Plugins will be installed when you launch an IDE.$${RESET}"
exit 0
fi

echo "Selected IDEs:"
for ide_code in $IDE_CODES; do
if command -v jq &> /dev/null; then
ide_name=$(echo "$IDE_METADATA" | jq -r ".\"$ide_code\".name")
else
ide_name="$ide_code"
fi
echo " - $ide_name ($ide_code)"
done
echo ""

# Function to find IDE binary path
find_ide_binary() {
local ide_code="$1"
local binary_name=""

# Map IDE codes to binary names
case "$ide_code" in
CL) binary_name="clion" ;;
GO) binary_name="goland" ;;
IU) binary_name="idea" ;;
PS) binary_name="phpstorm" ;;
PY) binary_name="pycharm" ;;
RD) binary_name="rider" ;;
RM) binary_name="rubymine" ;;
RR) binary_name="rustrover" ;;
WS) binary_name="webstorm" ;;
*) return 1 ;;
esac

# Common JetBrains installation paths
local search_paths=(
"$HOME/.local/share/JetBrains/Toolbox/apps/$binary_name"
"$HOME/.cache/JetBrains/Toolbox/apps/$binary_name"
"/opt/$binary_name/bin/$binary_name.sh"
"/usr/local/bin/$binary_name"
"/usr/bin/$binary_name"
)

# Search for the IDE binary
for base_path in "$${search_paths[@]}"; do
if [ -d "$base_path" ]; then
# For Toolbox installations, find the latest version
local latest_version=$(find "$base_path" -maxdepth 1 -type d -name "ch-*" | sort -V | tail -n 1)
if [ -n "$latest_version" ] && [ -f "$latest_version/bin/$binary_name.sh" ]; then
echo "$latest_version/bin/$binary_name.sh"
return 0
fi
elif [ -f "$base_path" ]; then
echo "$base_path"
return 0
fi
done

# Try to find in PATH
if command -v "$binary_name" &> /dev/null; then
command -v "$binary_name"
return 0
fi

return 1
}

# Function to install plugins for a specific IDE
install_plugins_for_ide() {
local ide_code="$1"
local ide_binary="$2"
local ide_name="$3"

echo -e "$${BOLD}Installing plugins for $ide_name ($ide_code)...$${RESET}"

local failed_plugins=()
local success_count=0

for plugin in "$${PLUGIN_LIST[@]}"; do
if [ -z "$plugin" ]; then
continue
fi

echo -n " 🔌 Installing $plugin... "

# Run the plugin installation command
# Note: The IDE must be closed for this to work
if output=$("$ide_binary" installPlugins $PLUGIN_INSTALL_ARGS "$plugin" 2>&1); then
echo -e "$${GREEN}✓$${RESET}"
((success_count++))
else
echo -e "$${RED}✗$${RESET}"
failed_plugins+=("$plugin")
echo " Error: $output"
fi
done

echo ""

if [ $success_count -gt 0 ]; then
echo -e "$${GREEN}✓ Successfully installed $success_count plugin(s) for $ide_name$${RESET}"
fi

if [ $${#failed_plugins[@]} -gt 0 ]; then
echo -e "$${YELLOW}⚠️ Failed to install $${#failed_plugins[@]} plugin(s) for $ide_name:$${RESET}"
for failed_plugin in "$${failed_plugins[@]}"; do
echo " - $failed_plugin"
done
fi

echo ""
}

# Main installation loop
echo -e "$${BOLD}🔍 Searching for IDE installations...$${RESET}"
echo ""

installed_count=0
skipped_count=0

for ide_code in $IDE_CODES; do
if ide_binary=$(find_ide_binary "$ide_code"); then
if command -v jq &> /dev/null; then
ide_name=$(echo "$IDE_METADATA" | jq -r ".\"$ide_code\".name")
else
ide_name="$ide_code"
fi

echo -e "$${GREEN}✓$${RESET} Found $ide_name: $ide_binary"
install_plugins_for_ide "$ide_code" "$ide_binary" "$ide_name"
((installed_count++))
else
if command -v jq &> /dev/null; then
ide_name=$(echo "$IDE_METADATA" | jq -r ".\"$ide_code\".name")
else
ide_name="$ide_code"
fi
echo -e "$${YELLOW}⚠️$${RESET} $ide_name ($ide_code) not found - skipping"
((skipped_count++))
fi
done

echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"

if [ $installed_count -eq 0 ]; then
echo -e "$${YELLOW}⚠️ No IDE installations found.$${RESET}"
echo "Plugins will be installed automatically when you first launch a JetBrains IDE via Toolbox."
else
echo -e "$${GREEN}✓ Plugin installation completed for $installed_count IDE(s).$${RESET}"
if [ $skipped_count -gt 0 ]; then
echo -e "$${YELLOW}⚠️ Skipped $skipped_count IDE(s) (not installed yet).$${RESET}"
fi
fi

echo ""
echo "Note: You may need to restart any running IDEs for plugins to take effect."
64 changes: 64 additions & 0 deletions registry/coder/modules/jetbrains/jetbrains.tftest.hcl
Original file line number Diff line number Diff line change
Expand Up @@ -294,3 +294,67 @@ run "output_multiple_ides" {
error_message = "Expected ide_metadata['PY'].build to be the fallback '${var.expected_ide_config["PY"].build}'"
}
}

run "no_script_when_plugins_empty" {
command = plan

variables {
agent_id = "foo"
folder = "/home/coder"
default = ["GO"]
plugins = []
}

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

run "script_created_when_plugins_provided" {
command = plan

variables {
agent_id = "foo"
folder = "/home/coder"
default = ["GO"]
plugins = ["com.intellij.plugins.terminal", "org.rust.lang"]
}

assert {
condition = length(resource.coder_script.jetbrains_plugin_installer) == 1
error_message = "Expected coder_script when plugins list is not empty"
}
}

run "script_runs_on_start" {
command = plan

variables {
agent_id = "foo"
folder = "/home/coder"
default = ["GO"]
plugins = ["com.intellij.plugins.terminal"]
}

assert {
condition = resource.coder_script.jetbrains_plugin_installer[0].run_on_start == true
error_message = "Expected plugin installer script to run on start"
}
}

run "no_script_when_no_ides_selected" {
command = plan

variables {
agent_id = "foo"
folder = "/home/coder"
# default is empty, so no IDEs selected
plugins = ["com.intellij.plugins.terminal"]
}

assert {
condition = length(resource.coder_script.jetbrains_plugin_installer) == 0
error_message = "Expected no coder_script when no IDEs are selected even if plugins are specified"
}
}
31 changes: 31 additions & 0 deletions registry/coder/modules/jetbrains/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,18 @@ variable "ide_config" {
}
}

variable "plugins" {
type = list(string)
description = "List of JetBrains plugin IDs to pre-install. Find plugin IDs on the JetBrains Marketplace (e.g., \"com.intellij.plugins.terminal\", \"org.rust.lang\")."
default = []
}

variable "plugin_install_args" {
type = string
description = "Additional arguments to pass to the plugin installation command."
default = ""
}

locals {
# Parse HTTP responses once with error handling for air-gapped environments
parsed_responses = {
Expand Down Expand Up @@ -259,6 +271,25 @@ resource "coder_app" "jetbrains" {
])
}

resource "coder_script" "jetbrains_plugin_installer" {
count = length(var.plugins) > 0 && length(local.selected_ides) > 0 ? 1 : 0
agent_id = var.agent_id
display_name = "JetBrains Plugins"
icon = "/icon/jetbrains-toolbox.svg"
script = templatefile("${path.module}/install-plugins.sh", {
PLUGINS : join(",", var.plugins),
PLUGIN_INSTALL_ARGS : var.plugin_install_args,
FOLDER : var.folder,
IDE_METADATA : jsonencode({
for key in local.selected_ides : key => {
code = key
name = local.options_metadata[key].name
}
})
})
run_on_start = true
}

output "ide_metadata" {
description = "A map of the metadata for each selected JetBrains IDE."
value = {
Expand Down