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

### Pre-install Plugins

Pre-install JetBrains plugins to ensure your team has the required tools ready when they open the IDE:

```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.main.id
folder = "/home/coder/project"
default = ["IU", "GO"]

# Pre-install common plugins
plugins = [
"org.jetbrains.plugins.github", # GitHub integration
"com.intellij.plugins.vscodekeymap", # VS Code keymap
"String Manipulation", # String manipulation tools
]
}
```

> [!NOTE]
> 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.

#### How Plugin Pre-installation Works

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.

2. **Background installation**: A background process monitors for IDE installation and automatically installs plugins when the IDE becomes available.

#### Popular Plugin IDs

| Plugin | ID |
| ---------------- | ----------------------------------- |
| GitHub | `org.jetbrains.plugins.github` |
| GitLab | `org.jetbrains.plugins.gitlab` |
| Docker | `Docker` |
| Kubernetes | `com.intellij.kubernetes` |
| VS Code Keymap | `com.intellij.plugins.vscodekeymap` |
| Rainbow Brackets | `izhangzhihao.rainbow.brackets` |
| SonarLint | `org.sonarlint.idea` |
| Copilot | `com.github.copilot` |

### Accessing the IDE Metadata

You can now reference the output `ide_metadata` as a map.
Expand Down
147 changes: 147 additions & 0 deletions registry/coder/modules/jetbrains/install_plugins.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
#!/usr/bin/env bash

# JetBrains Plugin Pre-installation Script
# This script handles plugin installation for JetBrains IDEs in Coder workspaces

PLUGINS="${PLUGINS}"
PROJECT_DIR="${PROJECT_DIR}"

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

IDE_CACHE_DIR="$HOME/.cache/JetBrains/RemoteDev/dist"
PLUGIN_INSTALL_LOG="/tmp/jetbrains-plugin-install.log"

# Exit early if no plugins specified
if [ -z "$PLUGINS" ]; then
echo "No plugins specified, skipping plugin setup."
exit 0
fi

echo -e "$${BOLD}🔌 Setting up JetBrains plugins...$${RESET}"

# Step 1: Create .idea/externalDependencies.xml
# This ensures users are prompted to install required plugins when opening the project
setup_external_dependencies() {
if [ ! -d "$PROJECT_DIR" ]; then
echo -e "$${YELLOW}⚠️ Project directory $PROJECT_DIR does not exist yet, skipping externalDependencies.xml$${RESET}"
return
fi

mkdir -p "$PROJECT_DIR/.idea"

echo -e "📦 Creating plugin requirements file..."

cat > "$PROJECT_DIR/.idea/externalDependencies.xml" << 'XMLHEADER'
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalDependencies">
XMLHEADER

IFS=',' read -r -a PLUGIN_ARRAY <<< "$PLUGINS"
for plugin in "$${PLUGIN_ARRAY[@]}"; do
plugin=$(echo "$plugin" | xargs) # trim whitespace
if [ -n "$plugin" ]; then
echo " <plugin id=\"$plugin\" />" >> "$PROJECT_DIR/.idea/externalDependencies.xml"
fi
done

cat >> "$PROJECT_DIR/.idea/externalDependencies.xml" << 'XMLFOOTER'
</component>
</project>
XMLFOOTER

echo -e "$${GREEN}✅ Created $PROJECT_DIR/.idea/externalDependencies.xml$${RESET}"
echo " When you open this project, JetBrains IDE will prompt you to install the required plugins."
}

# Step 2: Background plugin installer
# Monitors for IDE installation and installs plugins automatically
install_plugins_background() {
echo -e "🔄 Starting background plugin installer..."

# Run in background subshell
(
exec > "$PLUGIN_INSTALL_LOG" 2>&1

MAX_WAIT=7200 # Wait up to 2 hours for IDE to be installed
INTERVAL=30 # Check every 30 seconds
WAITED=0

echo "[$(date)] Background plugin installer started"
echo "[$(date)] Watching for IDE installation in $IDE_CACHE_DIR"
echo "[$(date)] Plugins to install: $PLUGINS"

while [ $WAITED -lt $MAX_WAIT ]; do
# Look for any installed IDE's remote-dev-server.sh
if [ -d "$IDE_CACHE_DIR" ]; then
IDE_SCRIPT=$(find "$IDE_CACHE_DIR" -name "remote-dev-server.sh" -type f 2> /dev/null | head -1)

if [ -n "$IDE_SCRIPT" ] && [ -x "$IDE_SCRIPT" ]; then
echo "[$(date)] Found IDE at: $IDE_SCRIPT"

# Get the IDE's bin directory
IDE_BIN_DIR=$(dirname "$IDE_SCRIPT")

# Wait a moment for IDE to fully initialize
sleep 5

# Install each plugin
IFS=',' read -r -a PLUGIN_ARRAY <<< "$PLUGINS"
INSTALL_SUCCESS=true

for plugin in "$${PLUGIN_ARRAY[@]}"; do
plugin=$(echo "$plugin" | xargs) # trim whitespace
if [ -n "$plugin" ]; then
echo "[$(date)] Installing plugin: $plugin"

# Try using remote-dev-server.sh first
if "$IDE_SCRIPT" installPlugins "$PROJECT_DIR" "$plugin" 2>&1; then
echo "[$(date)] Successfully installed: $plugin"
else
echo "[$(date)] Failed to install via remote-dev-server.sh, trying alternative methods..."

# Try finding IDE-specific launcher (idea.sh, pycharm.sh, etc.)
for launcher in "$IDE_BIN_DIR"/*.sh; do
if [[ "$launcher" != *"remote-dev"* ]] && [ -x "$launcher" ]; then
echo "[$(date)] Trying launcher: $launcher"
if "$launcher" installPlugins "$plugin" 2>&1; then
echo "[$(date)] Successfully installed via $launcher: $plugin"
break
fi
fi
done
fi
fi
done

echo "[$(date)] Plugin installation process completed!"
exit 0
fi
fi

sleep $INTERVAL
WAITED=$((WAITED + INTERVAL))
done

echo "[$(date)] Timed out waiting for IDE installation after $MAX_WAIT seconds"
) &

local BG_PID=$!
echo -e "📋 Background installer running (PID: $BG_PID)"
echo -e " Log file: $PLUGIN_INSTALL_LOG"

# Save PID for potential cleanup
echo "$BG_PID" > /tmp/jetbrains-plugin-installer.pid
}

# Main execution
setup_external_dependencies
install_plugins_background

echo -e "$${GREEN}🎉 JetBrains plugin setup complete!$${RESET}"
echo ""
echo "Plugins will be installed automatically when JetBrains IDE is detected."
echo "You can also install them manually via the IDE's plugin manager."
132 changes: 132 additions & 0 deletions registry/coder/modules/jetbrains/jetbrains.tftest.hcl
Original file line number Diff line number Diff line change
Expand Up @@ -351,3 +351,135 @@ run "validate_output_schema" {
error_message = "The ide_metadata output schema has changed. Please update the 'main.tf' and this test."
}
}

# Plugin installation tests
run "no_plugin_script_when_plugins_empty" {
command = plan

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

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

run "plugin_script_created_when_plugins_provided" {
command = plan

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

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

run "plugin_script_has_correct_display_name" {
command = plan

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

assert {
condition = resource.coder_script.jetbrains_plugins[0].display_name == "JetBrains Plugins"
error_message = "Expected plugin script display_name to be 'JetBrains Plugins'"
}
}

run "plugin_script_runs_on_start" {
command = plan

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

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

run "plugins_output_contains_correct_values" {
command = plan

variables {
agent_id = "foo"
folder = "/home/coder"
default = ["GO"]
plugins = ["org.jetbrains.plugins.github", "Docker"]
}

assert {
condition = length(output.plugins) == 2
error_message = "Expected plugins output to contain 2 items"
}

assert {
condition = contains(output.plugins, "org.jetbrains.plugins.github")
error_message = "Expected plugins output to contain 'org.jetbrains.plugins.github'"
}

assert {
condition = contains(output.plugins, "Docker")
error_message = "Expected plugins output to contain 'Docker'"
}
}

run "plugins_output_empty_when_not_provided" {
command = plan

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

assert {
condition = length(output.plugins) == 0
error_message = "Expected plugins output to be empty when not provided"
}
}

run "multiple_plugins_supported" {
command = plan

variables {
agent_id = "foo"
folder = "/home/coder"
default = ["GO"]
plugins = [
"org.jetbrains.plugins.github",
"com.intellij.plugins.vscodekeymap",
"Docker",
"izhangzhihao.rainbow.brackets"
]
}

assert {
condition = length(output.plugins) == 4
error_message = "Expected plugins output to contain 4 items"
}

assert {
condition = length(resource.coder_script.jetbrains_plugins) == 1
error_message = "Expected exactly one coder_script for plugins"
}
}
Loading