Skip to content

Commit 9ee6669

Browse files
committed
feat(jetbrains): add plugin pre-installation support
1 parent 7b84d91 commit 9ee6669

File tree

4 files changed

+318
-0
lines changed

4 files changed

+318
-0
lines changed

registry/coder/modules/jetbrains/README.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,31 @@ module "jetbrains" {
6161
}
6262
```
6363

64+
### Plugin Pre-installation
65+
66+
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.
67+
68+
```tf
69+
module "jetbrains" {
70+
count = data.coder_workspace.me.start_count
71+
source = "registry.coder.com/coder/jetbrains/coder"
72+
version = "1.3.0"
73+
agent_id = coder_agent.example.id
74+
folder = "/home/coder/project"
75+
default = ["IU", "PY"]
76+
77+
# Pre-install plugins
78+
plugins = [
79+
"com.intellij.plugins.terminal", # Terminal plugin
80+
"org.rust.lang", # Rust language support
81+
"com.github.copilot", # GitHub Copilot
82+
]
83+
}
84+
```
85+
86+
> [!NOTE]
87+
> Plugins are installed on workspace startup. If an IDE is not yet installed via Toolbox, plugins will be installed during the first IDE launch.
88+
6489
### Early Access Preview (EAP) Versions
6590

6691
```tf
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
#!/usr/bin/env bash
2+
# install-plugins.sh - Install JetBrains plugins in workspace IDEs
3+
4+
set -euo pipefail
5+
6+
PLUGINS="${PLUGINS}"
7+
PLUGIN_INSTALL_ARGS="${PLUGIN_INSTALL_ARGS}"
8+
FOLDER="${FOLDER}"
9+
IDE_METADATA='${IDE_METADATA}'
10+
11+
BOLD='\033[0;1m'
12+
GREEN='\033[0;32m'
13+
YELLOW='\033[1;33m'
14+
RED='\033[0;31m'
15+
RESET='\033[0m'
16+
17+
echo -e "$${BOLD}🔌 JetBrains Plugin Installer$${RESET}"
18+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
19+
20+
# Parse plugin list
21+
IFS=',' read -r -a PLUGIN_LIST <<< "$PLUGINS"
22+
23+
if [ $${#PLUGIN_LIST[@]} -eq 0 ] || [ -z "$${PLUGIN_LIST[0]}" ]; then
24+
echo "No plugins to install."
25+
exit 0
26+
fi
27+
28+
echo "Plugins to install: $${#PLUGIN_LIST[@]}"
29+
for plugin in "$${PLUGIN_LIST[@]}"; do
30+
echo " - $plugin"
31+
done
32+
echo ""
33+
34+
# Parse IDE metadata using jq if available, otherwise use basic parsing
35+
if command -v jq &> /dev/null; then
36+
IDE_CODES=$(echo "$IDE_METADATA" | jq -r 'keys[]')
37+
else
38+
# Fallback: extract IDE codes without jq
39+
IDE_CODES=$(echo "$IDE_METADATA" | grep -o '"[A-Z][A-Z]"' | tr -d '"' | sort -u)
40+
fi
41+
42+
if [ -z "$IDE_CODES" ]; then
43+
echo -e "$${YELLOW}⚠️ No IDEs selected. Plugins will be installed when you launch an IDE.$${RESET}"
44+
exit 0
45+
fi
46+
47+
echo "Selected IDEs:"
48+
for ide_code in $IDE_CODES; do
49+
if command -v jq &> /dev/null; then
50+
ide_name=$(echo "$IDE_METADATA" | jq -r ".\"$ide_code\".name")
51+
else
52+
ide_name="$ide_code"
53+
fi
54+
echo " - $ide_name ($ide_code)"
55+
done
56+
echo ""
57+
58+
# Function to find IDE binary path
59+
find_ide_binary() {
60+
local ide_code="$1"
61+
local binary_name=""
62+
63+
# Map IDE codes to binary names
64+
case "$ide_code" in
65+
CL) binary_name="clion" ;;
66+
GO) binary_name="goland" ;;
67+
IU) binary_name="idea" ;;
68+
PS) binary_name="phpstorm" ;;
69+
PY) binary_name="pycharm" ;;
70+
RD) binary_name="rider" ;;
71+
RM) binary_name="rubymine" ;;
72+
RR) binary_name="rustrover" ;;
73+
WS) binary_name="webstorm" ;;
74+
*) return 1 ;;
75+
esac
76+
77+
# Common JetBrains installation paths
78+
local search_paths=(
79+
"$HOME/.local/share/JetBrains/Toolbox/apps/$binary_name"
80+
"$HOME/.cache/JetBrains/Toolbox/apps/$binary_name"
81+
"/opt/$binary_name/bin/$binary_name.sh"
82+
"/usr/local/bin/$binary_name"
83+
"/usr/bin/$binary_name"
84+
)
85+
86+
# Search for the IDE binary
87+
for base_path in "$${search_paths[@]}"; do
88+
if [ -d "$base_path" ]; then
89+
# For Toolbox installations, find the latest version
90+
local latest_version=$(find "$base_path" -maxdepth 1 -type d -name "ch-*" | sort -V | tail -n 1)
91+
if [ -n "$latest_version" ] && [ -f "$latest_version/bin/$binary_name.sh" ]; then
92+
echo "$latest_version/bin/$binary_name.sh"
93+
return 0
94+
fi
95+
elif [ -f "$base_path" ]; then
96+
echo "$base_path"
97+
return 0
98+
fi
99+
done
100+
101+
# Try to find in PATH
102+
if command -v "$binary_name" &> /dev/null; then
103+
command -v "$binary_name"
104+
return 0
105+
fi
106+
107+
return 1
108+
}
109+
110+
# Function to install plugins for a specific IDE
111+
install_plugins_for_ide() {
112+
local ide_code="$1"
113+
local ide_binary="$2"
114+
local ide_name="$3"
115+
116+
echo -e "$${BOLD}Installing plugins for $ide_name ($ide_code)...$${RESET}"
117+
118+
local failed_plugins=()
119+
local success_count=0
120+
121+
for plugin in "$${PLUGIN_LIST[@]}"; do
122+
if [ -z "$plugin" ]; then
123+
continue
124+
fi
125+
126+
echo -n " 🔌 Installing $plugin... "
127+
128+
# Run the plugin installation command
129+
# Note: The IDE must be closed for this to work
130+
if output=$("$ide_binary" installPlugins $PLUGIN_INSTALL_ARGS "$plugin" 2>&1); then
131+
echo -e "$${GREEN}✓$${RESET}"
132+
((success_count++))
133+
else
134+
echo -e "$${RED}✗$${RESET}"
135+
failed_plugins+=("$plugin")
136+
echo " Error: $output"
137+
fi
138+
done
139+
140+
echo ""
141+
142+
if [ $success_count -gt 0 ]; then
143+
echo -e "$${GREEN}✓ Successfully installed $success_count plugin(s) for $ide_name$${RESET}"
144+
fi
145+
146+
if [ $${#failed_plugins[@]} -gt 0 ]; then
147+
echo -e "$${YELLOW}⚠️ Failed to install $${#failed_plugins[@]} plugin(s) for $ide_name:$${RESET}"
148+
for failed_plugin in "$${failed_plugins[@]}"; do
149+
echo " - $failed_plugin"
150+
done
151+
fi
152+
153+
echo ""
154+
}
155+
156+
# Main installation loop
157+
echo -e "$${BOLD}🔍 Searching for IDE installations...$${RESET}"
158+
echo ""
159+
160+
installed_count=0
161+
skipped_count=0
162+
163+
for ide_code in $IDE_CODES; do
164+
if ide_binary=$(find_ide_binary "$ide_code"); then
165+
if command -v jq &> /dev/null; then
166+
ide_name=$(echo "$IDE_METADATA" | jq -r ".\"$ide_code\".name")
167+
else
168+
ide_name="$ide_code"
169+
fi
170+
171+
echo -e "$${GREEN}✓$${RESET} Found $ide_name: $ide_binary"
172+
install_plugins_for_ide "$ide_code" "$ide_binary" "$ide_name"
173+
((installed_count++))
174+
else
175+
if command -v jq &> /dev/null; then
176+
ide_name=$(echo "$IDE_METADATA" | jq -r ".\"$ide_code\".name")
177+
else
178+
ide_name="$ide_code"
179+
fi
180+
echo -e "$${YELLOW}⚠️$${RESET} $ide_name ($ide_code) not found - skipping"
181+
((skipped_count++))
182+
fi
183+
done
184+
185+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
186+
187+
if [ $installed_count -eq 0 ]; then
188+
echo -e "$${YELLOW}⚠️ No IDE installations found.$${RESET}"
189+
echo "Plugins will be installed automatically when you first launch a JetBrains IDE via Toolbox."
190+
else
191+
echo -e "$${GREEN}✓ Plugin installation completed for $installed_count IDE(s).$${RESET}"
192+
if [ $skipped_count -gt 0 ]; then
193+
echo -e "$${YELLOW}⚠️ Skipped $skipped_count IDE(s) (not installed yet).$${RESET}"
194+
fi
195+
fi
196+
197+
echo ""
198+
echo "Note: You may need to restart any running IDEs for plugins to take effect."

registry/coder/modules/jetbrains/jetbrains.tftest.hcl

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,3 +294,67 @@ run "output_multiple_ides" {
294294
error_message = "Expected ide_metadata['PY'].build to be the fallback '${var.expected_ide_config["PY"].build}'"
295295
}
296296
}
297+
298+
run "no_script_when_plugins_empty" {
299+
command = plan
300+
301+
variables {
302+
agent_id = "foo"
303+
folder = "/home/coder"
304+
default = ["GO"]
305+
plugins = []
306+
}
307+
308+
assert {
309+
condition = length(resource.coder_script.jetbrains_plugin_installer) == 0
310+
error_message = "Expected no coder_script when plugins list is empty"
311+
}
312+
}
313+
314+
run "script_created_when_plugins_provided" {
315+
command = plan
316+
317+
variables {
318+
agent_id = "foo"
319+
folder = "/home/coder"
320+
default = ["GO"]
321+
plugins = ["com.intellij.plugins.terminal", "org.rust.lang"]
322+
}
323+
324+
assert {
325+
condition = length(resource.coder_script.jetbrains_plugin_installer) == 1
326+
error_message = "Expected coder_script when plugins list is not empty"
327+
}
328+
}
329+
330+
run "script_runs_on_start" {
331+
command = plan
332+
333+
variables {
334+
agent_id = "foo"
335+
folder = "/home/coder"
336+
default = ["GO"]
337+
plugins = ["com.intellij.plugins.terminal"]
338+
}
339+
340+
assert {
341+
condition = resource.coder_script.jetbrains_plugin_installer[0].run_on_start == true
342+
error_message = "Expected plugin installer script to run on start"
343+
}
344+
}
345+
346+
run "no_script_when_no_ides_selected" {
347+
command = plan
348+
349+
variables {
350+
agent_id = "foo"
351+
folder = "/home/coder"
352+
# default is empty, so no IDEs selected
353+
plugins = ["com.intellij.plugins.terminal"]
354+
}
355+
356+
assert {
357+
condition = length(resource.coder_script.jetbrains_plugin_installer) == 0
358+
error_message = "Expected no coder_script when no IDEs are selected even if plugins are specified"
359+
}
360+
}

registry/coder/modules/jetbrains/main.tf

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,18 @@ variable "ide_config" {
173173
}
174174
}
175175

176+
variable "plugins" {
177+
type = list(string)
178+
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\")."
179+
default = []
180+
}
181+
182+
variable "plugin_install_args" {
183+
type = string
184+
description = "Additional arguments to pass to the plugin installation command."
185+
default = ""
186+
}
187+
176188
locals {
177189
# Parse HTTP responses once with error handling for air-gapped environments
178190
parsed_responses = {
@@ -259,6 +271,25 @@ resource "coder_app" "jetbrains" {
259271
])
260272
}
261273

274+
resource "coder_script" "jetbrains_plugin_installer" {
275+
count = length(var.plugins) > 0 && length(local.selected_ides) > 0 ? 1 : 0
276+
agent_id = var.agent_id
277+
display_name = "JetBrains Plugins"
278+
icon = "/icon/jetbrains-toolbox.svg"
279+
script = templatefile("${path.module}/install-plugins.sh", {
280+
PLUGINS : join(",", var.plugins),
281+
PLUGIN_INSTALL_ARGS : var.plugin_install_args,
282+
FOLDER : var.folder,
283+
IDE_METADATA : jsonencode({
284+
for key in local.selected_ides : key => {
285+
code = key
286+
name = local.options_metadata[key].name
287+
}
288+
})
289+
})
290+
run_on_start = true
291+
}
292+
262293
output "ide_metadata" {
263294
description = "A map of the metadata for each selected JetBrains IDE."
264295
value = {

0 commit comments

Comments
 (0)