Skip to content

Commit dde2ded

Browse files
committed
feat(jetbrains): add plugin pre-installation support
Add ability to pre-install JetBrains plugins in Coder workspaces via the `plugins` variable. Uses a two-pronged approach: 1. Creates .idea/externalDependencies.xml so IDE prompts for plugins 2. Background installer monitors for IDE and auto-installs plugins New features: - `plugins` variable accepting list of plugin IDs - `plugins` output exposing configured plugins - `coder_script` resource for plugin installation - Comprehensive Terraform and E2E tests Closes #208
1 parent 51676b6 commit dde2ded

5 files changed

Lines changed: 528 additions & 0 deletions

File tree

registry/coder/modules/jetbrains/README.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,50 @@ module "jetbrains" {
136136
}
137137
```
138138

139+
### Pre-install Plugins
140+
141+
Pre-install JetBrains plugins to ensure your team has the required tools ready when they open the IDE:
142+
143+
```tf
144+
module "jetbrains" {
145+
count = data.coder_workspace.me.start_count
146+
source = "registry.coder.com/coder/jetbrains/coder"
147+
version = "1.3.0"
148+
agent_id = coder_agent.main.id
149+
folder = "/home/coder/project"
150+
default = ["IU", "GO"]
151+
152+
# Pre-install common plugins
153+
plugins = [
154+
"org.jetbrains.plugins.github", # GitHub integration
155+
"com.intellij.plugins.vscodekeymap", # VS Code keymap
156+
"String Manipulation", # String manipulation tools
157+
]
158+
}
159+
```
160+
161+
> [!NOTE]
162+
> Plugin IDs can be found on the [JetBrains Marketplace](https://plugins.jetbrains.com). Go to any plugin's page and look under "Additional Information" for the Plugin ID.
163+
164+
#### How Plugin Pre-installation Works
165+
166+
1. **Project-level configuration**: Creates `.idea/externalDependencies.xml` in your project folder. When you open the project in your IDE, you'll be prompted to install any missing plugins.
167+
168+
2. **Background installation**: A background process monitors for IDE installation and automatically installs plugins when the IDE becomes available.
169+
170+
#### Popular Plugin IDs
171+
172+
| Plugin | ID |
173+
| ---------------- | ----------------------------------- |
174+
| GitHub | `org.jetbrains.plugins.github` |
175+
| GitLab | `org.jetbrains.plugins.gitlab` |
176+
| Docker | `Docker` |
177+
| Kubernetes | `com.intellij.kubernetes` |
178+
| VS Code Keymap | `com.intellij.plugins.vscodekeymap` |
179+
| Rainbow Brackets | `izhangzhihao.rainbow.brackets` |
180+
| SonarLint | `org.sonarlint.idea` |
181+
| Copilot | `com.github.copilot` |
182+
139183
### Accessing the IDE Metadata
140184

141185
You can now reference the output `ide_metadata` as a map.
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
#!/usr/bin/env bash
2+
3+
# JetBrains Plugin Pre-installation Script
4+
# This script handles plugin installation for JetBrains IDEs in Coder workspaces
5+
6+
PLUGINS="${PLUGINS}"
7+
PROJECT_DIR="${PROJECT_DIR}"
8+
9+
BOLD='\033[0;1m'
10+
GREEN='\033[0;32m'
11+
YELLOW='\033[0;33m'
12+
RESET='\033[0m'
13+
14+
IDE_CACHE_DIR="$HOME/.cache/JetBrains/RemoteDev/dist"
15+
PLUGIN_INSTALL_LOG="/tmp/jetbrains-plugin-install.log"
16+
17+
# Exit early if no plugins specified
18+
if [ -z "$PLUGINS" ]; then
19+
echo "No plugins specified, skipping plugin setup."
20+
exit 0
21+
fi
22+
23+
echo -e "$${BOLD}🔌 Setting up JetBrains plugins...$${RESET}"
24+
25+
# Step 1: Create .idea/externalDependencies.xml
26+
# This ensures users are prompted to install required plugins when opening the project
27+
setup_external_dependencies() {
28+
if [ ! -d "$PROJECT_DIR" ]; then
29+
echo -e "$${YELLOW}⚠️ Project directory $PROJECT_DIR does not exist yet, skipping externalDependencies.xml$${RESET}"
30+
return
31+
fi
32+
33+
mkdir -p "$PROJECT_DIR/.idea"
34+
35+
echo -e "📦 Creating plugin requirements file..."
36+
37+
cat > "$PROJECT_DIR/.idea/externalDependencies.xml" << 'XMLHEADER'
38+
<?xml version="1.0" encoding="UTF-8"?>
39+
<project version="4">
40+
<component name="ExternalDependencies">
41+
XMLHEADER
42+
43+
IFS=',' read -r -a PLUGIN_ARRAY <<< "$PLUGINS"
44+
for plugin in "$${PLUGIN_ARRAY[@]}"; do
45+
plugin=$(echo "$plugin" | xargs) # trim whitespace
46+
if [ -n "$plugin" ]; then
47+
echo " <plugin id=\"$plugin\" />" >> "$PROJECT_DIR/.idea/externalDependencies.xml"
48+
fi
49+
done
50+
51+
cat >> "$PROJECT_DIR/.idea/externalDependencies.xml" << 'XMLFOOTER'
52+
</component>
53+
</project>
54+
XMLFOOTER
55+
56+
echo -e "$${GREEN}✅ Created $PROJECT_DIR/.idea/externalDependencies.xml$${RESET}"
57+
echo " When you open this project, JetBrains IDE will prompt you to install the required plugins."
58+
}
59+
60+
# Step 2: Background plugin installer
61+
# Monitors for IDE installation and installs plugins automatically
62+
install_plugins_background() {
63+
echo -e "🔄 Starting background plugin installer..."
64+
65+
# Run in background subshell
66+
(
67+
exec > "$PLUGIN_INSTALL_LOG" 2>&1
68+
69+
MAX_WAIT=7200 # Wait up to 2 hours for IDE to be installed
70+
INTERVAL=30 # Check every 30 seconds
71+
WAITED=0
72+
73+
echo "[$(date)] Background plugin installer started"
74+
echo "[$(date)] Watching for IDE installation in $IDE_CACHE_DIR"
75+
echo "[$(date)] Plugins to install: $PLUGINS"
76+
77+
while [ $WAITED -lt $MAX_WAIT ]; do
78+
# Look for any installed IDE's remote-dev-server.sh
79+
if [ -d "$IDE_CACHE_DIR" ]; then
80+
IDE_SCRIPT=$(find "$IDE_CACHE_DIR" -name "remote-dev-server.sh" -type f 2> /dev/null | head -1)
81+
82+
if [ -n "$IDE_SCRIPT" ] && [ -x "$IDE_SCRIPT" ]; then
83+
echo "[$(date)] Found IDE at: $IDE_SCRIPT"
84+
85+
# Get the IDE's bin directory
86+
IDE_BIN_DIR=$(dirname "$IDE_SCRIPT")
87+
88+
# Wait a moment for IDE to fully initialize
89+
sleep 5
90+
91+
# Install each plugin
92+
IFS=',' read -r -a PLUGIN_ARRAY <<< "$PLUGINS"
93+
INSTALL_SUCCESS=true
94+
95+
for plugin in "$${PLUGIN_ARRAY[@]}"; do
96+
plugin=$(echo "$plugin" | xargs) # trim whitespace
97+
if [ -n "$plugin" ]; then
98+
echo "[$(date)] Installing plugin: $plugin"
99+
100+
# Try using remote-dev-server.sh first
101+
if "$IDE_SCRIPT" installPlugins "$PROJECT_DIR" "$plugin" 2>&1; then
102+
echo "[$(date)] Successfully installed: $plugin"
103+
else
104+
echo "[$(date)] Failed to install via remote-dev-server.sh, trying alternative methods..."
105+
106+
# Try finding IDE-specific launcher (idea.sh, pycharm.sh, etc.)
107+
for launcher in "$IDE_BIN_DIR"/*.sh; do
108+
if [[ "$launcher" != *"remote-dev"* ]] && [ -x "$launcher" ]; then
109+
echo "[$(date)] Trying launcher: $launcher"
110+
if "$launcher" installPlugins "$plugin" 2>&1; then
111+
echo "[$(date)] Successfully installed via $launcher: $plugin"
112+
break
113+
fi
114+
fi
115+
done
116+
fi
117+
fi
118+
done
119+
120+
echo "[$(date)] Plugin installation process completed!"
121+
exit 0
122+
fi
123+
fi
124+
125+
sleep $INTERVAL
126+
WAITED=$((WAITED + INTERVAL))
127+
done
128+
129+
echo "[$(date)] Timed out waiting for IDE installation after $MAX_WAIT seconds"
130+
) &
131+
132+
local BG_PID=$!
133+
echo -e "📋 Background installer running (PID: $BG_PID)"
134+
echo -e " Log file: $PLUGIN_INSTALL_LOG"
135+
136+
# Save PID for potential cleanup
137+
echo "$BG_PID" > /tmp/jetbrains-plugin-installer.pid
138+
}
139+
140+
# Main execution
141+
setup_external_dependencies
142+
install_plugins_background
143+
144+
echo -e "$${GREEN}🎉 JetBrains plugin setup complete!$${RESET}"
145+
echo ""
146+
echo "Plugins will be installed automatically when JetBrains IDE is detected."
147+
echo "You can also install them manually via the IDE's plugin manager."

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

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,3 +351,135 @@ run "validate_output_schema" {
351351
error_message = "The ide_metadata output schema has changed. Please update the 'main.tf' and this test."
352352
}
353353
}
354+
355+
# Plugin installation tests
356+
run "no_plugin_script_when_plugins_empty" {
357+
command = plan
358+
359+
variables {
360+
agent_id = "foo"
361+
folder = "/home/coder"
362+
default = ["GO"]
363+
plugins = []
364+
}
365+
366+
assert {
367+
condition = length(resource.coder_script.jetbrains_plugins) == 0
368+
error_message = "Expected no coder_script when plugins list is empty"
369+
}
370+
}
371+
372+
run "plugin_script_created_when_plugins_provided" {
373+
command = plan
374+
375+
variables {
376+
agent_id = "foo"
377+
folder = "/home/coder"
378+
default = ["GO"]
379+
plugins = ["org.jetbrains.plugins.github"]
380+
}
381+
382+
assert {
383+
condition = length(resource.coder_script.jetbrains_plugins) == 1
384+
error_message = "Expected coder_script to be created when plugins are provided"
385+
}
386+
}
387+
388+
run "plugin_script_has_correct_display_name" {
389+
command = plan
390+
391+
variables {
392+
agent_id = "foo"
393+
folder = "/home/coder"
394+
default = ["GO"]
395+
plugins = ["org.jetbrains.plugins.github"]
396+
}
397+
398+
assert {
399+
condition = resource.coder_script.jetbrains_plugins[0].display_name == "JetBrains Plugins"
400+
error_message = "Expected plugin script display_name to be 'JetBrains Plugins'"
401+
}
402+
}
403+
404+
run "plugin_script_runs_on_start" {
405+
command = plan
406+
407+
variables {
408+
agent_id = "foo"
409+
folder = "/home/coder"
410+
default = ["GO"]
411+
plugins = ["org.jetbrains.plugins.github"]
412+
}
413+
414+
assert {
415+
condition = resource.coder_script.jetbrains_plugins[0].run_on_start == true
416+
error_message = "Expected plugin script to run on start"
417+
}
418+
}
419+
420+
run "plugins_output_contains_correct_values" {
421+
command = plan
422+
423+
variables {
424+
agent_id = "foo"
425+
folder = "/home/coder"
426+
default = ["GO"]
427+
plugins = ["org.jetbrains.plugins.github", "Docker"]
428+
}
429+
430+
assert {
431+
condition = length(output.plugins) == 2
432+
error_message = "Expected plugins output to contain 2 items"
433+
}
434+
435+
assert {
436+
condition = contains(output.plugins, "org.jetbrains.plugins.github")
437+
error_message = "Expected plugins output to contain 'org.jetbrains.plugins.github'"
438+
}
439+
440+
assert {
441+
condition = contains(output.plugins, "Docker")
442+
error_message = "Expected plugins output to contain 'Docker'"
443+
}
444+
}
445+
446+
run "plugins_output_empty_when_not_provided" {
447+
command = plan
448+
449+
variables {
450+
agent_id = "foo"
451+
folder = "/home/coder"
452+
default = ["GO"]
453+
}
454+
455+
assert {
456+
condition = length(output.plugins) == 0
457+
error_message = "Expected plugins output to be empty when not provided"
458+
}
459+
}
460+
461+
run "multiple_plugins_supported" {
462+
command = plan
463+
464+
variables {
465+
agent_id = "foo"
466+
folder = "/home/coder"
467+
default = ["GO"]
468+
plugins = [
469+
"org.jetbrains.plugins.github",
470+
"com.intellij.plugins.vscodekeymap",
471+
"Docker",
472+
"izhangzhihao.rainbow.brackets"
473+
]
474+
}
475+
476+
assert {
477+
condition = length(output.plugins) == 4
478+
error_message = "Expected plugins output to contain 4 items"
479+
}
480+
481+
assert {
482+
condition = length(resource.coder_script.jetbrains_plugins) == 1
483+
error_message = "Expected exactly one coder_script for plugins"
484+
}
485+
}

0 commit comments

Comments
 (0)