diff --git a/registry/harsh9485/.images/avatar.png b/registry/harsh9485/.images/avatar.png new file mode 100644 index 00000000..938202b9 Binary files /dev/null and b/registry/harsh9485/.images/avatar.png differ diff --git a/registry/harsh9485/README.md b/registry/harsh9485/README.md new file mode 100644 index 00000000..25cc2184 --- /dev/null +++ b/registry/harsh9485/README.md @@ -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. diff --git a/registry/harsh9485/modules/jetbrains-plugins/README.md b/registry/harsh9485/modules/jetbrains-plugins/README.md new file mode 100644 index 00000000..2da5d62a --- /dev/null +++ b/registry/harsh9485/modules/jetbrains-plugins/README.md @@ -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. diff --git a/registry/harsh9485/modules/jetbrains-plugins/jetbrains-plugins.tftest.hcl b/registry/harsh9485/modules/jetbrains-plugins/jetbrains-plugins.tftest.hcl new file mode 100644 index 00000000..9f432932 --- /dev/null +++ b/registry/harsh9485/modules/jetbrains-plugins/jetbrains-plugins.tftest.hcl @@ -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, + ] +} diff --git a/registry/harsh9485/modules/jetbrains-plugins/main.tf b/registry/harsh9485/modules/jetbrains-plugins/main.tf new file mode 100644 index 00000000..149979a2 --- /dev/null +++ b/registry/harsh9485/modules/jetbrains-plugins/main.tf @@ -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 +} diff --git a/registry/harsh9485/modules/jetbrains-plugins/scripts/install_plugins.sh b/registry/harsh9485/modules/jetbrains-plugins/scripts/install_plugins.sh new file mode 100644 index 00000000..95feb21d --- /dev/null +++ b/registry/harsh9485/modules/jetbrains-plugins/scripts/install_plugins.sh @@ -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."