mirror of
https://github.com/coder/registry.git
synced 2026-06-02 20:48:14 +00:00
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:
committed by
GitHub
parent
b72577707c
commit
b108185c14
Binary file not shown.
|
After Width: | Height: | Size: 19 KiB |
@@ -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."
|
||||
Reference in New Issue
Block a user