diff --git a/bun.lock b/bun.lock index 7fcb771f2..1b013ef29 100644 --- a/bun.lock +++ b/bun.lock @@ -1,5 +1,6 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "name": "registry", diff --git a/registry/coder/modules/jetbrains/README.md b/registry/coder/modules/jetbrains/README.md index 718613599..eb0316264 100644 --- a/registry/coder/modules/jetbrains/README.md +++ b/registry/coder/modules/jetbrains/README.md @@ -137,6 +137,43 @@ module "jetbrains" { } ``` +### Plugin Auto‑Installer + +This module now supports automatic JetBrains plugin installation inside your workspace. + +To get a plugin ID, open the plugin’s page on the JetBrains Marketplace. Scroll down to Additional Information and look for Plugin ID. Use that value in the configuration below. + +```tf +module "jetbrains" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/coder/jetbrains/coder" + version = "1.2.1" + agent_id = coder_agent.main.id + folder = "/home/coder/project" + default = ["IU", "PY"] + + jetbrains_plugins = { + "PY" = ["com.koxudaxi.pydantic", "com.intellij.kubernetes"] + "IU" = ["", ""] + "WS" = ["", ""] + "GO" = ["", ""] + "CL" = ["", ""] + "PS" = ["", ""] + "RD" = ["", ""] + "RM" = ["", ""] + "RR" = ["", ""] + } +} +``` + +> [!IMPORTANT]\ +> This module prerequisites and limitations +> +> 1. Requires JetBrains Toolbox to be installed +> 2. Requires jq to be available +> 3. Only works on Debian/Ubuntu-based systems (due to apt-get usage) +> 4. Plugins are installed when workspace starts, but may take time depending on IDE availability + ### Accessing the IDE Metadata You can now reference the output `ide_metadata` as a map. diff --git a/registry/coder/modules/jetbrains/main.tf b/registry/coder/modules/jetbrains/main.tf index 51f7c8168..52cc6bdd1 100644 --- a/registry/coder/modules/jetbrains/main.tf +++ b/registry/coder/modules/jetbrains/main.tf @@ -173,6 +173,13 @@ variable "ide_config" { } } +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 = {} +} + + locals { # Parse HTTP responses once with error handling for air-gapped environments parsed_responses = { @@ -203,6 +210,8 @@ locals { # Convert the parameter value to a set for for_each selected_ides = length(var.default) == 0 ? toset(jsondecode(coalesce(data.coder_parameter.jetbrains_ides[0].value, "[]"))) : toset(var.default) + + plugin_map_b64 = base64encode(jsonencode(var.jetbrains_plugins)) } data "coder_parameter" "jetbrains_ides" { @@ -230,6 +239,33 @@ data "coder_parameter" "jetbrains_ides" { data "coder_workspace" "me" {} data "coder_workspace_owner" "me" {} +resource "coder_script" "store_plugins" { + count = length(var.jetbrains_plugins) > 0 ? 1 : 0 + agent_id = var.agent_id + display_name = "Store JetBrains Plugins List" + run_on_start = true + script = <<-EOT + #!/bin/sh + set -eu + + mkdir -p "$HOME/.config/jetbrains" + echo -n "${local.plugin_map_b64}" | base64 -d > "$HOME/.config/jetbrains/plugins.json" + chmod 600 "$HOME/.config/jetbrains/plugins.json" + EOT +} + +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 + depends_on = [coder_script.store_plugins] + + script = <<-EOT + ${file("${path.module}/script/install_plugins.sh")} + EOT +} + resource "coder_app" "jetbrains" { for_each = local.selected_ides agent_id = var.agent_id diff --git a/registry/coder/modules/jetbrains/script/install_plugins.sh b/registry/coder/modules/jetbrains/script/install_plugins.sh new file mode 100644 index 000000000..c3bb3f2be --- /dev/null +++ b/registry/coder/modules/jetbrains/script/install_plugins.sh @@ -0,0 +1,167 @@ +#!/usr/bin/env 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" + +if command -v apt-get > /dev/null 2>&1; then + sudo apt-get update + sudo apt-get install -y libfreetype6 +else + echo "Warning: 'apt-get' not found. Please ensure 'libfreetype6' is installed manually for your distribution." >&2 +fi + +mkdir -p "$(dirname "$LOGFILE")" + +exec > >(tee -a "$LOGFILE") 2>&1 + +log() { + printf '%s %s\n' "$(date --iso-8601=seconds)" "$*" | tee -a "$LOGFILE" +} + +# -------- 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 +} + +# -------- 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" + else + return 1 + fi +} + +install_plugin() { + log "Installing plugin: $2" + if "$1" installPlugins "$2"; then + log "Successfully installed plugin: $2" + 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 + +# 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[*]}" + +MAX_ATTEMPTS=60 # 10 minutes +attempt=0 + +# Loop until all plugins installed +while [ ${#pending_codes[@]} -gt 0 ] && [ $attempt -lt $MAX_ATTEMPTS ]; do + + if [ ! -d "$TOOLBOX_BASE" ]; then + log "Toolbox directory not found yet, waiting..." + sleep 10 + continue + fi + + 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 + + cli_launcher="$(find_cli_launcher "$code" "$product_dir")" || continue + + log "Detected IDE $code at $product_dir" + + plugins="$(get_plugins_for_code "$code")" + if [ -z "$plugins" ]; then + log "No plugins for $code" + continue + fi + + while read -r plugin; do + install_plugin "$cli_launcher" "$plugin" + done <<< "$plugins" + + # 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 still pending, wait and retry + if [ ${#pending_codes[@]} -gt 0 ]; then + sleep 10 + ((attempt++)) + fi +done + +if [ ${#pending_codes[@]} -gt 0 ]; then + log "Timeout: IDEs not found: ${pending_codes[*]}" + exit 1 +fi + +log "All plugins installed. Exiting."