Skip to content

feat(saheli): add jetbrains-plugins module for issue #208 … #225

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
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
141 changes: 141 additions & 0 deletions registry/saheli/modules/jetbrains-plugins/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
---
display_name: JetBrains with Plugin Configuration
description: A complete JetBrains IDE module with automatic plugin pre-configuration for workspaces.
icon: ../../../../.icons/jetbrains.svg
maintainer_github: sahelisaha04
verified: false
tags: [ide, jetbrains, plugins, parameter]
---

# JetBrains IDEs with Plugin Configuration

This module provides complete JetBrains IDE integration with automatic plugin pre-configuration capabilities. It implements full JetBrains Gateway functionality with plugin management features.

```tf
module "jetbrains_with_plugins" {
source = "registry.coder.com/saheli/jetbrains-plugins/coder"
version = "1.0.0"
agent_id = coder_agent.example.id
folder = "/home/coder/project"

# Standard JetBrains module options
default = ["IU", "PY"]

# NEW: Plugin pre-configuration
plugins = [
"org.jetbrains.plugins.github", # GitHub integration
"com.intellij.ml.llm", # AI Assistant
"Pythonid", # Python support for IntelliJ
"org.intellij.plugins.markdown" # Markdown support
]
}
```

## Features

✅ **Complete JetBrains integration** - Full IDE functionality with Gateway support
✅ **Plugin pre-configuration** - Automatically suggests plugins when IDE opens
✅ **Project-level integration** - Creates `.idea/externalDependencies.xml`
✅ **Gateway compatible** - Works with JetBrains Gateway workflow
✅ **Zero setup required** - No manual IDE installation needed
✅ **Standalone implementation** - No external module dependencies

## How It Works

1. **JetBrains apps** are created directly with full Gateway integration
2. **Plugin configuration script** runs on workspace start (when plugins specified)
3. **IDE configuration files** are created for automatic plugin suggestions
4. **When connecting via Gateway** → IDE suggests configured plugins → User accepts → Plugins install

## Plugin Configuration

### Finding Plugin IDs

Plugin IDs can be found on the [JetBrains Marketplace](https://plugins.jetbrains.com/):

1. Navigate to the plugin page
2. Look for the plugin ID in the URL or plugin details
3. Common examples:
- `org.jetbrains.plugins.github` - GitHub integration
- `com.intellij.ml.llm` - AI Assistant
- `Pythonid` - Python support for IntelliJ IDEA
- `org.intellij.plugins.markdown` - Markdown support

### Configuration Process

The module creates:
- **IDE config directories**: `~/.config/JetBrains/[IDE]2025.1/`
- **Plugin suggestions**: `enabled_plugins.txt` and `pluginAdvertiser.xml`
- **Project requirements**: `/workspace/.idea/externalDependencies.xml`

## Examples

### Basic Usage with Plugins

```tf
module "jetbrains_with_plugins" {
source = "registry.coder.com/saheli/jetbrains-plugins/coder"
version = "1.0.0"
agent_id = coder_agent.main.id
folder = "/workspace"
default = ["IU"]

plugins = [
"org.jetbrains.plugins.github"
]
}
```

### Multiple IDEs with Specific Plugins

```tf
module "jetbrains_full_stack" {
source = "registry.coder.com/saheli/jetbrains-plugins/coder"
version = "1.0.0"
agent_id = coder_agent.main.id
folder = "/workspace"
default = ["IU", "PY", "WS"]

plugins = [
"org.jetbrains.plugins.github", # GitHub (all IDEs)
"com.intellij.ml.llm", # AI Assistant (all IDEs)
"Pythonid", # Python (IntelliJ)
"JavaScript", # JavaScript (IntelliJ)
"org.intellij.plugins.markdown" # Markdown (all IDEs)
]
}
```

## Module Parameters

This module accepts all parameters from the base `coder/jetbrains` module, plus:

### New Plugin Parameter

- **`plugins`** (list(string), default: []): List of plugin IDs to pre-configure

### Base Module Parameters

- **`agent_id`** (string, required): Coder agent ID
- **`folder`** (string, required): Project folder path
- **`default`** (set(string), default: []): Pre-selected IDEs or empty for user choice
- **`options`** (set(string)): Available IDE choices
- **`major_version`** (string): IDE version (e.g., "2025.1" or "latest")
- **`channel`** (string): Release channel ("release" or "eap")

## Supported IDEs

All JetBrains IDEs with remote development support:
- CLion (`CL`)
- GoLand (`GO`)
- IntelliJ IDEA Ultimate (`IU`)
- PhpStorm (`PS`)
- PyCharm Professional (`PY`)
- Rider (`RD`)
- RubyMine (`RM`)
- RustRover (`RR`)
- WebStorm (`WS`)

## Contributing

This module addresses [GitHub Issue #208](https://github.com/coder/registry/issues/208) by providing plugin pre-configuration capabilities while following the namespace guidelines for community contributions.
192 changes: 192 additions & 0 deletions registry/saheli/modules/jetbrains-plugins/main.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
import { it, expect, describe } from "bun:test";
import {
runTerraformInit,
testRequiredVariables,
runTerraformApply,
} from "~test";

describe("jetbrains-plugins", async () => {
await runTerraformInit(import.meta.dir);

await testRequiredVariables(import.meta.dir, {
agent_id: "foo",
folder: "/home/foo",
});

// Test without plugins (should only create base JetBrains apps)
describe("without plugins", () => {
it("should create only base JetBrains apps when no plugins specified", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
folder: "/home/coder",
default: '["IU", "PY"]',
// No plugins specified
});

// Should create JetBrains apps directly
const jetbrains_apps = state.resources.filter(
(res) => res.type === "coder_app" && res.name === "jetbrains"
);
expect(jetbrains_apps.length).toBeGreaterThan(0);

// Should NOT create plugin configuration script
const plugin_scripts = state.resources.filter(
(res) => res.type === "coder_script" && res.name === "jetbrains_plugins"
);
expect(plugin_scripts.length).toBe(0);
});
});

// Test with plugins (should create base apps + plugin script)
describe("with plugins", () => {
it("should create JetBrains apps and plugin configuration script", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
folder: "/home/coder",
default: '["IU", "PY"]',
plugins: '["org.jetbrains.plugins.github", "com.intellij.ml.llm"]',
});

// Should create JetBrains apps directly
const jetbrains_apps = state.resources.filter(
(res) => res.type === "coder_app" && res.name === "jetbrains"
);
expect(jetbrains_apps.length).toBeGreaterThan(0);

// Should create plugin configuration script
const plugin_scripts = state.resources.filter(
(res) => res.type === "coder_script" && res.name === "jetbrains_plugins"
);
expect(plugin_scripts.length).toBe(1);

const script = plugin_scripts[0];
expect(script.instances[0].attributes.display_name).toBe("Configure JetBrains Plugins");
expect(script.instances[0].attributes.run_on_start).toBe(true);
expect(script.instances[0].attributes.start_blocks_login).toBe(false);

// Check that plugins are included in the script
const scriptContent = script.instances[0].attributes.script;
expect(scriptContent).toContain("org.jetbrains.plugins.github");
expect(scriptContent).toContain("com.intellij.ml.llm");
});

it("should work with parameter mode and plugins", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
folder: "/home/coder",
// default is empty (parameter mode)
plugins: '["org.jetbrains.plugins.github"]',
});

// Should create parameter for IDE selection directly
const parameters = state.resources.filter(
(res) => res.type === "coder_parameter" && res.name === "jetbrains_ides"
);
expect(parameters.length).toBe(1);

// Should create plugin configuration script
const plugin_scripts = state.resources.filter(
(res) => res.type === "coder_script" && res.name === "jetbrains_plugins"
);
expect(plugin_scripts.length).toBe(1);
});

it("should pass through all base module parameters correctly", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "test-agent",
folder: "/workspace",
default: '["GO"]',
major_version: "2025.1",
channel: "eap",
plugins: '["org.jetbrains.plugins.github"]',
coder_app_order: 5,
});

// Should create GoLand app with correct parameters
const jetbrains_apps = state.resources.filter(
(res) => res.type === "coder_app" && res.name === "jetbrains"
);
expect(jetbrains_apps.length).toBe(1);

const app = jetbrains_apps[0];
expect(app.instances[0].attributes.agent_id).toBe("test-agent");
expect(app.instances[0].attributes.display_name).toBe("GoLand");
expect(app.instances[0].attributes.order).toBe(5);
expect(app.instances[0].attributes.url).toContain("folder=/workspace");
});

it("should work with single plugin", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
folder: "/home/coder",
default: '["IU"]',
plugins: '["Pythonid"]',
});

// Should create plugin script with single plugin
const plugin_scripts = state.resources.filter(
(res) => res.type === "coder_script" && res.name === "jetbrains_plugins"
);
expect(plugin_scripts.length).toBe(1);

const scriptContent = plugin_scripts[0].instances[0].attributes.script;
expect(scriptContent).toContain("Pythonid");
expect(scriptContent).toContain("PLUGINS=(Pythonid)");
});

it("should work with empty plugins list", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
folder: "/home/coder",
default: '["IU"]',
plugins: '[]',
});

// Should NOT create plugin script when plugins list is empty
const plugin_scripts = state.resources.filter(
(res) => res.type === "coder_script" && res.name === "jetbrains_plugins"
);
expect(plugin_scripts.length).toBe(0);

// Should still create base JetBrains apps
const jetbrains_apps = state.resources.filter(
(res) => res.type === "coder_app" && res.name === "jetbrains"
);
expect(jetbrains_apps.length).toBe(1);
});
});

// Test base module integration
describe("base module integration", () => {
it("should preserve all base module functionality", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
folder: "/home/coder",
default: '["IU", "WS"]',
major_version: "latest",
channel: "release",
});

// Should create multiple IDE apps
const jetbrains_apps = state.resources.filter(
(res) => res.type === "coder_app" && res.name === "jetbrains"
);
expect(jetbrains_apps.length).toBe(2);

// Check app properties
const app_names = jetbrains_apps.map(
(app) => app.instances[0].attributes.display_name
);
expect(app_names).toContain("IntelliJ IDEA");
expect(app_names).toContain("WebStorm");

// Check URLs contain proper JetBrains Gateway links
jetbrains_apps.forEach((app) => {
const url = app.instances[0].attributes.url;
expect(url).toContain("jetbrains://gateway/coder");
expect(url).toContain("ide_product_code=");
expect(url).toContain("ide_build_number=");
});
});
});
});
Loading