diff --git a/completions-cron/src/__generated__/completions-index.ts b/completions-cron/src/__generated__/completions-index.ts index a3714a4a..bcf804c5 100644 --- a/completions-cron/src/__generated__/completions-index.ts +++ b/completions-cron/src/__generated__/completions-index.ts @@ -10,15 +10,23 @@ import mod5 from '../../../src/resources/python/uv/completions/uv.pythonVersions import mod6 from '../../../src/resources/python/pyenv/completions/pyenv.pythonVersions.js'; import mod7 from '../../../src/resources/python/pip/completions/pip.install.js'; import mod8 from '../../../src/resources/ollama/completions/ollama.models.js'; -import mod9 from '../../../src/resources/javascript/pnpm/completions/pnpm.globalEnvNodeVersion.js'; -import mod10 from '../../../src/resources/javascript/nvm/completions/nvm.nodeVersions.js'; -import mod11 from '../../../src/resources/javascript/npm/completions/npm.install.js'; -import mod12 from '../../../src/resources/homebrew/completions/homebrew.formulae.js'; -import mod13 from '../../../src/resources/homebrew/completions/homebrew.casks.js'; -import mod14 from '../../../src/resources/cursor/completions/cursor.extensions.js'; -import mod15 from '../../../src/resources/asdf/completions/asdf.plugins.js'; -import mod16 from '../../../src/resources/asdf/completions/asdf-plugin.plugin.js'; -import mod17 from '../../../src/resources/apt/completions/apt.install.js'; +import mod9 from '../../../src/resources/jetbrains/rustrover/completions/rustrover.plugins.js'; +import mod10 from '../../../src/resources/jetbrains/rubymine/completions/rubymine.plugins.js'; +import mod11 from '../../../src/resources/jetbrains/rider/completions/rider.plugins.js'; +import mod12 from '../../../src/resources/jetbrains/pycharm/completions/pycharm.plugins.js'; +import mod13 from '../../../src/resources/jetbrains/phpstorm/completions/phpstorm.plugins.js'; +import mod14 from '../../../src/resources/jetbrains/intellij-idea/completions/intellij-idea.plugins.js'; +import mod15 from '../../../src/resources/jetbrains/goland/completions/goland.plugins.js'; +import mod16 from '../../../src/resources/jetbrains/clion/completions/clion.plugins.js'; +import mod17 from '../../../src/resources/javascript/pnpm/completions/pnpm.globalEnvNodeVersion.js'; +import mod18 from '../../../src/resources/javascript/nvm/completions/nvm.nodeVersions.js'; +import mod19 from '../../../src/resources/javascript/npm/completions/npm.install.js'; +import mod20 from '../../../src/resources/homebrew/completions/homebrew.formulae.js'; +import mod21 from '../../../src/resources/homebrew/completions/homebrew.casks.js'; +import mod22 from '../../../src/resources/cursor/completions/cursor.extensions.js'; +import mod23 from '../../../src/resources/asdf/completions/asdf.plugins.js'; +import mod24 from '../../../src/resources/asdf/completions/asdf-plugin.plugin.js'; +import mod25 from '../../../src/resources/apt/completions/apt.install.js'; export interface CompletionModule { resourceType: string @@ -36,13 +44,21 @@ export const completionModules: CompletionModule[] = [ { resourceType: 'pyenv', parameterPath: '/pythonVersions', fetch: mod6 }, { resourceType: 'pip', parameterPath: '/install', fetch: mod7 }, { resourceType: 'ollama', parameterPath: '/models', fetch: mod8 }, - { resourceType: 'pnpm', parameterPath: '/globalEnvNodeVersion', fetch: mod9 }, - { resourceType: 'nvm', parameterPath: '/nodeVersions', fetch: mod10 }, - { resourceType: 'npm', parameterPath: '/install', fetch: mod11 }, - { resourceType: 'homebrew', parameterPath: '/formulae', fetch: mod12 }, - { resourceType: 'homebrew', parameterPath: '/casks', fetch: mod13 }, - { resourceType: 'cursor', parameterPath: '/extensions', fetch: mod14 }, - { resourceType: 'asdf', parameterPath: '/plugins', fetch: mod15 }, - { resourceType: 'asdf-plugin', parameterPath: '/plugin', fetch: mod16 }, - { resourceType: 'apt', parameterPath: '/install', fetch: mod17 }, + { resourceType: 'rustrover', parameterPath: '/plugins', fetch: mod9 }, + { resourceType: 'rubymine', parameterPath: '/plugins', fetch: mod10 }, + { resourceType: 'rider', parameterPath: '/plugins', fetch: mod11 }, + { resourceType: 'pycharm', parameterPath: '/plugins', fetch: mod12 }, + { resourceType: 'phpstorm', parameterPath: '/plugins', fetch: mod13 }, + { resourceType: 'intellij-idea', parameterPath: '/plugins', fetch: mod14 }, + { resourceType: 'goland', parameterPath: '/plugins', fetch: mod15 }, + { resourceType: 'clion', parameterPath: '/plugins', fetch: mod16 }, + { resourceType: 'pnpm', parameterPath: '/globalEnvNodeVersion', fetch: mod17 }, + { resourceType: 'nvm', parameterPath: '/nodeVersions', fetch: mod18 }, + { resourceType: 'npm', parameterPath: '/install', fetch: mod19 }, + { resourceType: 'homebrew', parameterPath: '/formulae', fetch: mod20 }, + { resourceType: 'homebrew', parameterPath: '/casks', fetch: mod21 }, + { resourceType: 'cursor', parameterPath: '/extensions', fetch: mod22 }, + { resourceType: 'asdf', parameterPath: '/plugins', fetch: mod23 }, + { resourceType: 'asdf-plugin', parameterPath: '/plugin', fetch: mod24 }, + { resourceType: 'apt', parameterPath: '/install', fetch: mod25 }, ] diff --git a/docs/resources/(resources)/claude-code-project.mdx b/docs/resources/(resources)/ai-agents/claude-code-project.mdx similarity index 100% rename from docs/resources/(resources)/claude-code-project.mdx rename to docs/resources/(resources)/ai-agents/claude-code-project.mdx diff --git a/docs/resources/(resources)/claude-code.mdx b/docs/resources/(resources)/ai-agents/claude-code.mdx similarity index 100% rename from docs/resources/(resources)/claude-code.mdx rename to docs/resources/(resources)/ai-agents/claude-code.mdx diff --git a/docs/resources/(resources)/ai-agents/meta.json b/docs/resources/(resources)/ai-agents/meta.json new file mode 100644 index 00000000..8ed55c6b --- /dev/null +++ b/docs/resources/(resources)/ai-agents/meta.json @@ -0,0 +1,4 @@ +{ + "title": "ai & agents", + "pages": ["claude-code", "claude-code-project", "ollama", "openclaw"] +} diff --git a/docs/resources/(resources)/ollama.mdx b/docs/resources/(resources)/ai-agents/ollama.mdx similarity index 100% rename from docs/resources/(resources)/ollama.mdx rename to docs/resources/(resources)/ai-agents/ollama.mdx diff --git a/docs/resources/(resources)/openclaw.mdx b/docs/resources/(resources)/ai-agents/openclaw.mdx similarity index 100% rename from docs/resources/(resources)/openclaw.mdx rename to docs/resources/(resources)/ai-agents/openclaw.mdx diff --git a/docs/resources/(resources)/android-studio.mdx b/docs/resources/(resources)/editors-ides/android-studio.mdx similarity index 100% rename from docs/resources/(resources)/android-studio.mdx rename to docs/resources/(resources)/editors-ides/android-studio.mdx diff --git a/docs/resources/(resources)/editors-ides/clion.mdx b/docs/resources/(resources)/editors-ides/clion.mdx new file mode 100644 index 00000000..4600d9ce --- /dev/null +++ b/docs/resources/(resources)/editors-ides/clion.mdx @@ -0,0 +1,60 @@ +--- +title: clion +description: A reference page for the clion resource +--- + +The clion resource installs [JetBrains CLion](https://www.jetbrains.com/clion/), a C/C++ IDE. On macOS it is installed via Homebrew Cask (`brew install --cask clion`); on Linux via Snap (`snap install clion --classic`). + +## Parameters + +- **settingsZip** *(string, optional)* — Absolute path to a CLion settings ZIP file (exported via *File | Manage IDE Settings | Export Settings*) to import on first install. The archive is extracted directly into the CLion config directory, so all exported settings (keymaps, code styles, inspections, etc.) are applied before CLion is first launched. + +- **importSettings** *(boolean, optional, default: `true`)* — Controls whether the `settingsZip` is imported during `create`. Set to `false` to skip the import even when `settingsZip` is specified. This is a setting parameter and is not tracked as state, so it only has effect when the resource is first applied. + +- **plugins** *(string[], optional)* — JetBrains Marketplace plugin IDs to install (e.g. `"com.github.copilot"`, `"Docker"`). Plugin IDs can be found on the plugin's page in the Marketplace under *Additional Information*. Plugins are managed statefully: Codify adds missing plugins and removes plugins no longer in the list. + +- **jvmMaxHeapSize** *(string, optional)* — Maximum JVM heap allocated to CLion, e.g. `"2048m"` for 2 GB or `"4096m"` for 4 GB. Written to `clion.vmoptions` in the IDE config directory as `-Xmx`. + +- **jvmMinHeapSize** *(string, optional)* — Initial JVM heap allocated to CLion, e.g. `"512m"`. Written to `clion.vmoptions` as `-Xms`. Typically set to half the max heap size. + +## Example usage + +### Install CLion with plugins + +```json title="codify.jsonc" +[ + { + "type": "clion", + "plugins": [ + "com.github.copilot", + "Docker" + ] + } +] +``` + +### Install CLion, import previous settings, and increase heap + +```json title="codify.jsonc" +[ + { + "type": "clion", + "settingsZip": "/path/to/clion-settings.zip", + "importSettings": true, + "jvmMaxHeapSize": "4096m", + "jvmMinHeapSize": "1024m", + "plugins": [ + "com.github.copilot", + "Docker" + ] + } +] +``` + +## Notes + +- On macOS a CLI launcher symlink is created at `/usr/local/bin/clion` during install so that `clion` is available in terminal sessions. It is removed on destroy. +- Plugin IDs must be exact JetBrains Marketplace IDs. You can find them on the plugin's Marketplace page under *Additional Information → Plugin ID*. +- The `settingsZip` import only runs during `create` (first apply), not on subsequent applies. If you need to re-import, destroy and re-apply the resource. +- JVM options are written to `clion.vmoptions` in `~/Library/Application Support/JetBrains/CLion/` on macOS and `~/.config/JetBrains/CLion/` on Linux. If CLion has never been launched, Codify creates this directory and file automatically. +- On Linux, Snap must be available. Codify will attempt to install `snapd` via the system package manager if it is not found. diff --git a/docs/resources/(resources)/cursor.mdx b/docs/resources/(resources)/editors-ides/cursor.mdx similarity index 100% rename from docs/resources/(resources)/cursor.mdx rename to docs/resources/(resources)/editors-ides/cursor.mdx diff --git a/docs/resources/(resources)/editors-ides/goland.mdx b/docs/resources/(resources)/editors-ides/goland.mdx new file mode 100644 index 00000000..3a50c5ea --- /dev/null +++ b/docs/resources/(resources)/editors-ides/goland.mdx @@ -0,0 +1,60 @@ +--- +title: goland +description: A reference page for the goland resource +--- + +The goland resource installs [JetBrains GoLand](https://www.jetbrains.com/go/), a Go IDE. On macOS it is installed via Homebrew Cask (`brew install --cask goland`); on Linux via Snap (`snap install goland --classic`). + +## Parameters + +- **settingsZip** *(string, optional)* — Absolute path to a GoLand settings ZIP file (exported via *File | Manage IDE Settings | Export Settings*) to import on first install. The archive is extracted directly into the GoLand config directory, so all exported settings (keymaps, code styles, inspections, etc.) are applied before GoLand is first launched. + +- **importSettings** *(boolean, optional, default: `true`)* — Controls whether the `settingsZip` is imported during `create`. Set to `false` to skip the import even when `settingsZip` is specified. This is a setting parameter and is not tracked as state, so it only has effect when the resource is first applied. + +- **plugins** *(string[], optional)* — JetBrains Marketplace plugin IDs to install (e.g. `"com.github.copilot"`, `"Docker"`). Plugin IDs can be found on the plugin's page in the Marketplace under *Additional Information*. Plugins are managed statefully: Codify adds missing plugins and removes plugins no longer in the list. + +- **jvmMaxHeapSize** *(string, optional)* — Maximum JVM heap allocated to GoLand, e.g. `"2048m"` for 2 GB or `"4096m"` for 4 GB. Written to `goland.vmoptions` in the IDE config directory as `-Xmx`. + +- **jvmMinHeapSize** *(string, optional)* — Initial JVM heap allocated to GoLand, e.g. `"512m"`. Written to `goland.vmoptions` as `-Xms`. Typically set to half the max heap size. + +## Example usage + +### Install GoLand with plugins + +```json title="codify.jsonc" +[ + { + "type": "goland", + "plugins": [ + "com.github.copilot", + "Docker" + ] + } +] +``` + +### Install GoLand, import previous settings, and increase heap + +```json title="codify.jsonc" +[ + { + "type": "goland", + "settingsZip": "/path/to/goland-settings.zip", + "importSettings": true, + "jvmMaxHeapSize": "4096m", + "jvmMinHeapSize": "1024m", + "plugins": [ + "com.github.copilot", + "Docker" + ] + } +] +``` + +## Notes + +- On macOS a CLI launcher symlink is created at `/usr/local/bin/goland` during install so that `goland` is available in terminal sessions. It is removed on destroy. +- Plugin IDs must be exact JetBrains Marketplace IDs. You can find them on the plugin's Marketplace page under *Additional Information → Plugin ID*. +- The `settingsZip` import only runs during `create` (first apply), not on subsequent applies. If you need to re-import, destroy and re-apply the resource. +- JVM options are written to `goland.vmoptions` in `~/Library/Application Support/JetBrains/GoLand/` on macOS and `~/.config/JetBrains/GoLand/` on Linux. If GoLand has never been launched, Codify creates this directory and file automatically. +- On Linux, Snap must be available. Codify will attempt to install `snapd` via the system package manager if it is not found. diff --git a/docs/resources/(resources)/editors-ides/intellij-idea.mdx b/docs/resources/(resources)/editors-ides/intellij-idea.mdx new file mode 100644 index 00000000..bfb164f1 --- /dev/null +++ b/docs/resources/(resources)/editors-ides/intellij-idea.mdx @@ -0,0 +1,61 @@ +--- +title: intellij-idea +description: A reference page for the intellij-idea resource +--- + +The intellij-idea resource installs [JetBrains IntelliJ IDEA](https://www.jetbrains.com/idea/), a general-purpose JVM/Java IDE. As of 2025.3, IntelliJ IDEA ships as a unified distribution that includes both the free Community tier and Ultimate features, which unlock within the same install via a subscription. On macOS it is installed via Homebrew Cask (`brew install --cask intellij-idea`); on Linux via Snap (`snap install intellij-idea-community --classic`). + +## Parameters + +- **settingsZip** *(string, optional)* — Absolute path to an IntelliJ IDEA settings ZIP file (exported via *File | Manage IDE Settings | Export Settings*) to import on first install. The archive is extracted directly into the IntelliJ IDEA config directory, so all exported settings (keymaps, code styles, inspections, etc.) are applied before IntelliJ IDEA is first launched. + +- **importSettings** *(boolean, optional, default: `true`)* — Controls whether the `settingsZip` is imported during `create`. Set to `false` to skip the import even when `settingsZip` is specified. This is a setting parameter and is not tracked as state, so it only has effect when the resource is first applied. + +- **plugins** *(string[], optional)* — JetBrains Marketplace plugin IDs to install (e.g. `"com.github.copilot"`, `"Docker"`). Plugin IDs can be found on the plugin's page in the Marketplace under *Additional Information*. Plugins are managed statefully: Codify adds missing plugins and removes plugins no longer in the list. + +- **jvmMaxHeapSize** *(string, optional)* — Maximum JVM heap allocated to IntelliJ IDEA, e.g. `"2048m"` for 2 GB or `"4096m"` for 4 GB. Written to `idea.vmoptions` in the IDE config directory as `-Xmx`. + +- **jvmMinHeapSize** *(string, optional)* — Initial JVM heap allocated to IntelliJ IDEA, e.g. `"512m"`. Written to `idea.vmoptions` as `-Xms`. Typically set to half the max heap size. + +## Example usage + +### Install IntelliJ IDEA with plugins + +```json title="codify.jsonc" +[ + { + "type": "intellij-idea", + "plugins": [ + "com.github.copilot", + "Docker" + ] + } +] +``` + +### Install IntelliJ IDEA, import previous settings, and increase heap + +```json title="codify.jsonc" +[ + { + "type": "intellij-idea", + "settingsZip": "/path/to/intellij-idea-settings.zip", + "importSettings": true, + "jvmMaxHeapSize": "4096m", + "jvmMinHeapSize": "1024m", + "plugins": [ + "com.github.copilot", + "Docker" + ] + } +] +``` + +## Notes + +- On macOS a CLI launcher symlink is created at `/usr/local/bin/idea` during install so that `idea` is available in terminal sessions. It is removed on destroy. +- Plugin IDs must be exact JetBrains Marketplace IDs. You can find them on the plugin's Marketplace page under *Additional Information → Plugin ID*. +- The `settingsZip` import only runs during `create` (first apply), not on subsequent applies. If you need to re-import, destroy and re-apply the resource. +- JVM options are written to `idea.vmoptions` in `~/Library/Application Support/JetBrains/IntelliJIdea/` on macOS and `~/.config/JetBrains/IntelliJIdea/` on Linux. If IntelliJ IDEA has never been launched, Codify creates this directory and file automatically. +- On Linux, Snap must be available. Codify will attempt to install `snapd` via the system package manager if it is not found. +- IntelliJ IDEA is the unified distribution covering both the free Community tier and the Ultimate edition; Ultimate features are unlocked within the same install via an active subscription. diff --git a/docs/resources/(resources)/editors-ides/meta.json b/docs/resources/(resources)/editors-ides/meta.json new file mode 100644 index 00000000..5ce119b6 --- /dev/null +++ b/docs/resources/(resources)/editors-ides/meta.json @@ -0,0 +1,4 @@ +{ + "title": "editors & ides", + "pages": ["vscode", "cursor", "intellij-idea", "clion", "goland", "phpstorm", "pycharm", "rider", "rubymine", "rustrover", "webstorm", "android-studio"] +} diff --git a/docs/resources/(resources)/editors-ides/phpstorm.mdx b/docs/resources/(resources)/editors-ides/phpstorm.mdx new file mode 100644 index 00000000..e5503f76 --- /dev/null +++ b/docs/resources/(resources)/editors-ides/phpstorm.mdx @@ -0,0 +1,60 @@ +--- +title: phpstorm +description: A reference page for the phpstorm resource +--- + +The phpstorm resource installs [JetBrains PhpStorm](https://www.jetbrains.com/phpstorm/), a PHP IDE. On macOS it is installed via Homebrew Cask (`brew install --cask phpstorm`); on Linux via Snap (`snap install phpstorm --classic`). + +## Parameters + +- **settingsZip** *(string, optional)* — Absolute path to a PhpStorm settings ZIP file (exported via *File | Manage IDE Settings | Export Settings*) to import on first install. The archive is extracted directly into the PhpStorm config directory, so all exported settings (keymaps, code styles, inspections, etc.) are applied before PhpStorm is first launched. + +- **importSettings** *(boolean, optional, default: `true`)* — Controls whether the `settingsZip` is imported during `create`. Set to `false` to skip the import even when `settingsZip` is specified. This is a setting parameter and is not tracked as state, so it only has effect when the resource is first applied. + +- **plugins** *(string[], optional)* — JetBrains Marketplace plugin IDs to install (e.g. `"com.github.copilot"`, `"Docker"`). Plugin IDs can be found on the plugin's page in the Marketplace under *Additional Information*. Plugins are managed statefully: Codify adds missing plugins and removes plugins no longer in the list. + +- **jvmMaxHeapSize** *(string, optional)* — Maximum JVM heap allocated to PhpStorm, e.g. `"2048m"` for 2 GB or `"4096m"` for 4 GB. Written to `phpstorm.vmoptions` in the IDE config directory as `-Xmx`. + +- **jvmMinHeapSize** *(string, optional)* — Initial JVM heap allocated to PhpStorm, e.g. `"512m"`. Written to `phpstorm.vmoptions` as `-Xms`. Typically set to half the max heap size. + +## Example usage + +### Install PhpStorm with plugins + +```json title="codify.jsonc" +[ + { + "type": "phpstorm", + "plugins": [ + "com.github.copilot", + "Docker" + ] + } +] +``` + +### Install PhpStorm, import previous settings, and increase heap + +```json title="codify.jsonc" +[ + { + "type": "phpstorm", + "settingsZip": "/path/to/phpstorm-settings.zip", + "importSettings": true, + "jvmMaxHeapSize": "4096m", + "jvmMinHeapSize": "1024m", + "plugins": [ + "com.github.copilot", + "Docker" + ] + } +] +``` + +## Notes + +- On macOS a CLI launcher symlink is created at `/usr/local/bin/phpstorm` during install so that `phpstorm` is available in terminal sessions. It is removed on destroy. +- Plugin IDs must be exact JetBrains Marketplace IDs. You can find them on the plugin's Marketplace page under *Additional Information → Plugin ID*. +- The `settingsZip` import only runs during `create` (first apply), not on subsequent applies. If you need to re-import, destroy and re-apply the resource. +- JVM options are written to `phpstorm.vmoptions` in `~/Library/Application Support/JetBrains/PhpStorm/` on macOS and `~/.config/JetBrains/PhpStorm/` on Linux. If PhpStorm has never been launched, Codify creates this directory and file automatically. +- On Linux, Snap must be available. Codify will attempt to install `snapd` via the system package manager if it is not found. diff --git a/docs/resources/(resources)/editors-ides/pycharm.mdx b/docs/resources/(resources)/editors-ides/pycharm.mdx new file mode 100644 index 00000000..43b71604 --- /dev/null +++ b/docs/resources/(resources)/editors-ides/pycharm.mdx @@ -0,0 +1,60 @@ +--- +title: pycharm +description: A reference page for the pycharm resource +--- + +The pycharm resource installs [JetBrains PyCharm](https://www.jetbrains.com/pycharm/), a Python IDE. On macOS it is installed via Homebrew Cask (`brew install --cask pycharm`); on Linux via Snap (`snap install pycharm-community --classic`). + +## Parameters + +- **settingsZip** *(string, optional)* — Absolute path to a PyCharm settings ZIP file (exported via *File | Manage IDE Settings | Export Settings*) to import on first install. The archive is extracted directly into the PyCharm config directory, so all exported settings (keymaps, code styles, inspections, etc.) are applied before PyCharm is first launched. + +- **importSettings** *(boolean, optional, default: `true`)* — Controls whether the `settingsZip` is imported during `create`. Set to `false` to skip the import even when `settingsZip` is specified. This is a setting parameter and is not tracked as state, so it only has effect when the resource is first applied. + +- **plugins** *(string[], optional)* — JetBrains Marketplace plugin IDs to install (e.g. `"intellij.jupyter"`, `"Docker"`). Plugin IDs can be found on the plugin's page in the Marketplace under *Additional Information*. Plugins are managed statefully: Codify adds missing plugins and removes plugins no longer in the list. + +- **jvmMaxHeapSize** *(string, optional)* — Maximum JVM heap allocated to PyCharm, e.g. `"2048m"` for 2 GB or `"4096m"` for 4 GB. Written to `pycharm.vmoptions` in the IDE config directory as `-Xmx`. + +- **jvmMinHeapSize** *(string, optional)* — Initial JVM heap allocated to PyCharm, e.g. `"512m"`. Written to `pycharm.vmoptions` as `-Xms`. Typically set to half the max heap size. + +## Example usage + +### Install PyCharm with plugins + +```json title="codify.jsonc" +[ + { + "type": "pycharm", + "plugins": [ + "intellij.jupyter", + "Docker" + ] + } +] +``` + +### Install PyCharm, import previous settings, and increase heap + +```json title="codify.jsonc" +[ + { + "type": "pycharm", + "settingsZip": "/path/to/pycharm-settings.zip", + "importSettings": true, + "jvmMaxHeapSize": "4096m", + "jvmMinHeapSize": "1024m", + "plugins": [ + "intellij.jupyter", + "Docker" + ] + } +] +``` + +## Notes + +- On macOS a CLI launcher symlink is created at `/usr/local/bin/pycharm` during install so that `pycharm` is available in terminal sessions. It is removed on destroy. +- Plugin IDs must be exact JetBrains Marketplace IDs. You can find them on the plugin's Marketplace page under *Additional Information → Plugin ID*. +- The `settingsZip` import only runs during `create` (first apply), not on subsequent applies. If you need to re-import, destroy and re-apply the resource. +- JVM options are written to `pycharm.vmoptions` in `~/Library/Application Support/JetBrains/PyCharm/` on macOS and `~/.config/JetBrains/PyCharm/` on Linux. If PyCharm has never been launched, Codify creates this directory and file automatically. +- On Linux, Snap must be available. Codify will attempt to install `snapd` via the system package manager if it is not found. diff --git a/docs/resources/(resources)/editors-ides/rider.mdx b/docs/resources/(resources)/editors-ides/rider.mdx new file mode 100644 index 00000000..60e131aa --- /dev/null +++ b/docs/resources/(resources)/editors-ides/rider.mdx @@ -0,0 +1,60 @@ +--- +title: rider +description: A reference page for the rider resource +--- + +The rider resource installs [JetBrains Rider](https://www.jetbrains.com/rider/), JetBrains' .NET/C# IDE, free for non-commercial use. On macOS it is installed via Homebrew Cask (`brew install --cask rider`); on Linux via Snap (`snap install rider --classic`). + +## Parameters + +- **settingsZip** *(string, optional)* — Absolute path to a Rider settings ZIP file (exported via *File | Manage IDE Settings | Export Settings*) to import on first install. The archive is extracted directly into the Rider config directory, so all exported settings (keymaps, code styles, inspections, etc.) are applied before Rider is first launched. + +- **importSettings** *(boolean, optional, default: `true`)* — Controls whether the `settingsZip` is imported during `create`. Set to `false` to skip the import even when `settingsZip` is specified. This is a setting parameter and is not tracked as state, so it only has effect when the resource is first applied. + +- **plugins** *(string[], optional)* — JetBrains Marketplace plugin IDs to install (e.g. `"com.github.copilot"`, `"Docker"`). Plugin IDs can be found on the plugin's page in the Marketplace under *Additional Information*. Plugins are managed statefully: Codify adds missing plugins and removes plugins no longer in the list. + +- **jvmMaxHeapSize** *(string, optional)* — Maximum JVM heap allocated to Rider, e.g. `"2048m"` for 2 GB or `"4096m"` for 4 GB. Written to `rider.vmoptions` in the IDE config directory as `-Xmx`. + +- **jvmMinHeapSize** *(string, optional)* — Initial JVM heap allocated to Rider, e.g. `"512m"`. Written to `rider.vmoptions` as `-Xms`. Typically set to half the max heap size. + +## Example usage + +### Install Rider with plugins + +```json title="codify.jsonc" +[ + { + "type": "rider", + "plugins": [ + "com.github.copilot", + "Docker" + ] + } +] +``` + +### Install Rider, import previous settings, and increase heap + +```json title="codify.jsonc" +[ + { + "type": "rider", + "settingsZip": "/path/to/rider-settings.zip", + "importSettings": true, + "jvmMaxHeapSize": "4096m", + "jvmMinHeapSize": "1024m", + "plugins": [ + "com.github.copilot", + "Docker" + ] + } +] +``` + +## Notes + +- On macOS a CLI launcher symlink is created at `/usr/local/bin/rider` during install so that `rider` is available in terminal sessions. It is removed on destroy. +- Plugin IDs must be exact JetBrains Marketplace IDs. You can find them on the plugin's Marketplace page under *Additional Information → Plugin ID*. +- The `settingsZip` import only runs during `create` (first apply), not on subsequent applies. If you need to re-import, destroy and re-apply the resource. +- JVM options are written to `rider.vmoptions` in `~/Library/Application Support/JetBrains/Rider/` on macOS and `~/.config/JetBrains/Rider/` on Linux. If Rider has never been launched, Codify creates this directory and file automatically. +- On Linux, Snap must be available. Codify will attempt to install `snapd` via the system package manager if it is not found. diff --git a/docs/resources/(resources)/editors-ides/rubymine.mdx b/docs/resources/(resources)/editors-ides/rubymine.mdx new file mode 100644 index 00000000..340a9c8a --- /dev/null +++ b/docs/resources/(resources)/editors-ides/rubymine.mdx @@ -0,0 +1,60 @@ +--- +title: rubymine +description: A reference page for the rubymine resource +--- + +The rubymine resource installs [JetBrains RubyMine](https://www.jetbrains.com/ruby/), a Ruby and Rails IDE. On macOS it is installed via Homebrew Cask (`brew install --cask rubymine`); on Linux via Snap (`snap install rubymine --classic`). + +## Parameters + +- **settingsZip** *(string, optional)* — Absolute path to a RubyMine settings ZIP file (exported via *File | Manage IDE Settings | Export Settings*) to import on first install. The archive is extracted directly into the RubyMine config directory, so all exported settings (keymaps, code styles, inspections, etc.) are applied before RubyMine is first launched. + +- **importSettings** *(boolean, optional, default: `true`)* — Controls whether the `settingsZip` is imported during `create`. Set to `false` to skip the import even when `settingsZip` is specified. This is a setting parameter and is not tracked as state, so it only has effect when the resource is first applied. + +- **plugins** *(string[], optional)* — JetBrains Marketplace plugin IDs to install (e.g. `"com.github.copilot"`, `"Docker"`). Plugin IDs can be found on the plugin's page in the Marketplace under *Additional Information*. Plugins are managed statefully: Codify adds missing plugins and removes plugins no longer in the list. + +- **jvmMaxHeapSize** *(string, optional)* — Maximum JVM heap allocated to RubyMine, e.g. `"2048m"` for 2 GB or `"4096m"` for 4 GB. Written to `rubymine.vmoptions` in the IDE config directory as `-Xmx`. + +- **jvmMinHeapSize** *(string, optional)* — Initial JVM heap allocated to RubyMine, e.g. `"512m"`. Written to `rubymine.vmoptions` as `-Xms`. Typically set to half the max heap size. + +## Example usage + +### Install RubyMine with plugins + +```json title="codify.jsonc" +[ + { + "type": "rubymine", + "plugins": [ + "com.github.copilot", + "Docker" + ] + } +] +``` + +### Install RubyMine, import previous settings, and increase heap + +```json title="codify.jsonc" +[ + { + "type": "rubymine", + "settingsZip": "/path/to/rubymine-settings.zip", + "importSettings": true, + "jvmMaxHeapSize": "4096m", + "jvmMinHeapSize": "1024m", + "plugins": [ + "com.github.copilot", + "Docker" + ] + } +] +``` + +## Notes + +- On macOS a CLI launcher symlink is created at `/usr/local/bin/rubymine` during install so that `rubymine` is available in terminal sessions. It is removed on destroy. +- Plugin IDs must be exact JetBrains Marketplace IDs. You can find them on the plugin's Marketplace page under *Additional Information → Plugin ID*. +- The `settingsZip` import only runs during `create` (first apply), not on subsequent applies. If you need to re-import, destroy and re-apply the resource. +- JVM options are written to `rubymine.vmoptions` in `~/Library/Application Support/JetBrains/RubyMine/` on macOS and `~/.config/JetBrains/RubyMine/` on Linux. If RubyMine has never been launched, Codify creates this directory and file automatically. +- On Linux, Snap must be available. Codify will attempt to install `snapd` via the system package manager if it is not found. diff --git a/docs/resources/(resources)/editors-ides/rustrover.mdx b/docs/resources/(resources)/editors-ides/rustrover.mdx new file mode 100644 index 00000000..b55c1619 --- /dev/null +++ b/docs/resources/(resources)/editors-ides/rustrover.mdx @@ -0,0 +1,60 @@ +--- +title: rustrover +description: A reference page for the rustrover resource +--- + +The rustrover resource installs [JetBrains RustRover](https://www.jetbrains.com/rust/), JetBrains' Rust IDE (free for non-commercial use). On macOS it is installed via Homebrew Cask (`brew install --cask rustrover`); on Linux via Snap (`snap install rustrover --classic`). + +## Parameters + +- **settingsZip** *(string, optional)* — Absolute path to a RustRover settings ZIP file (exported via *File | Manage IDE Settings | Export Settings*) to import on first install. The archive is extracted directly into the RustRover config directory, so all exported settings (keymaps, code styles, inspections, etc.) are applied before RustRover is first launched. + +- **importSettings** *(boolean, optional, default: `true`)* — Controls whether the `settingsZip` is imported during `create`. Set to `false` to skip the import even when `settingsZip` is specified. This is a setting parameter and is not tracked as state, so it only has effect when the resource is first applied. + +- **plugins** *(string[], optional)* — JetBrains Marketplace plugin IDs to install (e.g. `"com.github.copilot"`, `"Docker"`). Plugin IDs can be found on the plugin's page in the Marketplace under *Additional Information*. Plugins are managed statefully: Codify adds missing plugins and removes plugins no longer in the list. + +- **jvmMaxHeapSize** *(string, optional)* — Maximum JVM heap allocated to RustRover, e.g. `"2048m"` for 2 GB or `"4096m"` for 4 GB. Written to `rustrover.vmoptions` in the IDE config directory as `-Xmx`. + +- **jvmMinHeapSize** *(string, optional)* — Initial JVM heap allocated to RustRover, e.g. `"512m"`. Written to `rustrover.vmoptions` as `-Xms`. Typically set to half the max heap size. + +## Example usage + +### Install RustRover with plugins + +```json title="codify.jsonc" +[ + { + "type": "rustrover", + "plugins": [ + "com.github.copilot", + "Docker" + ] + } +] +``` + +### Install RustRover, import previous settings, and increase heap + +```json title="codify.jsonc" +[ + { + "type": "rustrover", + "settingsZip": "/path/to/rustrover-settings.zip", + "importSettings": true, + "jvmMaxHeapSize": "4096m", + "jvmMinHeapSize": "1024m", + "plugins": [ + "com.github.copilot", + "Docker" + ] + } +] +``` + +## Notes + +- On macOS a CLI launcher symlink is created at `/usr/local/bin/rustrover` during install so that `rustrover` is available in terminal sessions. It is removed on destroy. +- Plugin IDs must be exact JetBrains Marketplace IDs. You can find them on the plugin's Marketplace page under *Additional Information → Plugin ID*. +- The `settingsZip` import only runs during `create` (first apply), not on subsequent applies. If you need to re-import, destroy and re-apply the resource. +- JVM options are written to `rustrover.vmoptions` in `~/Library/Application Support/JetBrains/RustRover/` on macOS and `~/.config/JetBrains/RustRover/` on Linux. If RustRover has never been launched, Codify creates this directory and file automatically. +- On Linux, Snap must be available. Codify will attempt to install `snapd` via the system package manager if it is not found. diff --git a/docs/resources/(resources)/vscode.mdx b/docs/resources/(resources)/editors-ides/vscode.mdx similarity index 100% rename from docs/resources/(resources)/vscode.mdx rename to docs/resources/(resources)/editors-ides/vscode.mdx diff --git a/docs/resources/(resources)/webstorm.mdx b/docs/resources/(resources)/editors-ides/webstorm.mdx similarity index 100% rename from docs/resources/(resources)/webstorm.mdx rename to docs/resources/(resources)/editors-ides/webstorm.mdx diff --git a/docs/resources/(resources)/meta.json b/docs/resources/(resources)/meta.json index c9810c52..a6e0de73 100644 --- a/docs/resources/(resources)/meta.json +++ b/docs/resources/(resources)/meta.json @@ -1,6 +1,8 @@ { "pages": [ + "ai-agents", "package-managers", + "editors-ides", "asdf", "git", "javascript", diff --git a/docs/resources/index.mdx b/docs/resources/index.mdx index a812bdb4..a5db2c41 100644 --- a/docs/resources/index.mdx +++ b/docs/resources/index.mdx @@ -97,6 +97,15 @@ Configure popular development environments: - **[android-studio](/docs/resources/android-studio)** - Android Studio IDE - **[xcode-tools](/docs/resources/xcode-tools)** - Xcode Command Line Tools - **[pgcli](/docs/resources/pgcli)** - Postgres CLI with auto-completion +- **[webstorm](/docs/resources/webstorm)** - JetBrains WebStorm JavaScript IDE +- **[intellij-idea](/docs/resources/intellij-idea)** - JetBrains IntelliJ IDEA Java/Kotlin IDE +- **[pycharm](/docs/resources/pycharm)** - JetBrains PyCharm Python IDE +- **[clion](/docs/resources/clion)** - JetBrains CLion C/C++ IDE +- **[rustrover](/docs/resources/rustrover)** - JetBrains RustRover Rust IDE +- **[phpstorm](/docs/resources/phpstorm)** - JetBrains PhpStorm PHP IDE +- **[goland](/docs/resources/goland)** - JetBrains GoLand Go IDE +- **[rider](/docs/resources/rider)** - JetBrains Rider .NET/C# IDE +- **[rubymine](/docs/resources/rubymine)** - JetBrains RubyMine Ruby/Rails IDE ### File Synchronisation diff --git a/package.json b/package.json index 508574f0..4685ce0a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "default", - "version": "1.8.3", + "version": "1.9.1", "description": "Default plugin for Codify - provides 50+ declarative resources for managing development tools and system configuration across macOS and Linux", "main": "dist/index.js", "scripts": { diff --git a/src/index.ts b/src/index.ts index e3ee3418..46fa56dd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -61,6 +61,14 @@ import { VscodeResource } from './resources/vscode/vscode.js'; import { WebStormResource } from './resources/webstorm/webstorm.js'; import { XcodeToolsResource } from './resources/xcode-tools/xcode-tools.js'; import { YumResource } from './resources/yum/yum.js'; +import { PyCharmResource } from './resources/jetbrains/pycharm/pycharm.js'; +import { ClionResource } from './resources/jetbrains/clion/clion.js'; +import { IntellijIdeaResource } from './resources/jetbrains/intellij-idea/intellij-idea.js'; +import { RustRoverResource } from './resources/jetbrains/rustrover/rustrover.js'; +import { PhpStormResource } from './resources/jetbrains/phpstorm/phpstorm.js'; +import { GoLandResource } from './resources/jetbrains/goland/goland.js'; +import { RiderResource } from './resources/jetbrains/rider/rider.js'; +import { RubyMineResource } from './resources/jetbrains/rubymine/rubymine.js'; export const MIN_SUPPORTED_CLI_VERSION: string | undefined = '1.1.0'; @@ -89,6 +97,14 @@ runPlugin(Plugin.create( new CursorResource(), new VscodeResource(), new WebStormResource(), + new PyCharmResource(), + new ClionResource(), + new IntellijIdeaResource(), + new RustRoverResource(), + new PhpStormResource(), + new GoLandResource(), + new RiderResource(), + new RubyMineResource(), new GitRepositoryResource(), new GitRepositoriesResource(), new AndroidStudioResource(), diff --git a/src/resources/jetbrains/clion/clion.ts b/src/resources/jetbrains/clion/clion.ts new file mode 100644 index 00000000..63bb274f --- /dev/null +++ b/src/resources/jetbrains/clion/clion.ts @@ -0,0 +1,203 @@ +import { + CreatePlan, + DestroyPlan, + ExampleConfig, + ModifyPlan, + ParameterChange, + Resource, + ResourceSettings, + SpawnStatus, + Utils, + getPty, + z, +} from '@codifycli/plugin-core'; +import { OS } from '@codifycli/schemas'; +import fs from 'node:fs/promises'; + +import { + JetBrainsCommon, + JetBrainsPluginsParameter, + JetBrainsProductInfo, +} from '../common/jetbrains-common.js'; + +const PRODUCT: JetBrainsProductInfo = { + macAppName: 'CLion', + macBinaryName: 'clion', + configDirPrefix: 'CLion', + caskName: 'clion', + snapName: 'clion', + linuxCommand: 'clion', +}; + +const schema = z + .object({ + settingsZip: z + .string() + .optional() + .describe('Absolute path to a CLion settings ZIP file to import on first install.'), + importSettings: z + .boolean() + .optional() + .describe( + 'Whether to import the settings from settingsZip during create. ' + + 'Defaults to true. Set to false to skip the import even when settingsZip is provided.' + ), + plugins: z + .array(z.string()) + .optional() + .describe( + 'JetBrains Marketplace plugin IDs to install ' + + '(e.g. "com.github.copilot", "Docker"). ' + + 'Plugin IDs can be found on the plugin page under Additional Information.' + ), + jvmMaxHeapSize: z + .string() + .optional() + .describe('Maximum JVM heap size for CLion, e.g. "2048m" for 2 GB. Defaults to the IDE default (~2 GB).'), + jvmMinHeapSize: z + .string() + .optional() + .describe('Initial JVM heap size for CLion, e.g. "512m". Defaults to the IDE default.'), + }) + .meta({ $comment: 'https://codifycli.com/docs/resources/clion' }) + .describe('Install and configure JetBrains CLion IDE with plugins and JVM settings.'); + +export type ClionConfig = z.infer; + +const defaultConfig: Partial = { + plugins: [], +}; + +const exampleBasic: ExampleConfig = { + title: 'CLion with GitHub Copilot and Docker plugins', + description: + 'Install CLion and add the GitHub Copilot and Docker integration plugins for a modern C/C++ workflow.', + configs: [ + { + type: 'clion', + plugins: ['com.github.copilot', 'Docker'], + }, + ], +}; + +const exampleAdvanced: ExampleConfig = { + title: 'CLion with tuned JVM and imported settings', + description: + 'Install CLion, import previous settings from a ZIP, and increase the heap to 4 GB for large projects.', + configs: [ + { + type: 'clion', + settingsZip: '/path/to/clion-settings.zip', + importSettings: true, + jvmMaxHeapSize: '4096m', + jvmMinHeapSize: '1024m', + plugins: ['com.github.copilot', 'Docker'], + }, + ], +}; + +export class ClionResource extends Resource { + getSettings(): ResourceSettings { + return { + id: 'clion', + operatingSystems: [OS.Darwin, OS.Linux], + schema, + defaultConfig, + exampleConfigs: { + example1: exampleBasic, + example2: exampleAdvanced, + }, + parameterSettings: { + settingsZip: { type: 'string', setting: true }, + importSettings: { type: 'boolean', setting: true }, + plugins: { type: 'stateful', definition: new JetBrainsPluginsParameter(PRODUCT), order: 1 }, + jvmMaxHeapSize: { type: 'string', canModify: true }, + jvmMinHeapSize: { type: 'string', canModify: true }, + }, + }; + } + + override async refresh(parameters: Partial): Promise | null> { + const installed = await JetBrainsCommon.isInstalled(PRODUCT); + if (!installed) return null; + + const result: Partial = {}; + + const configDir = await JetBrainsCommon.findConfigDir(PRODUCT); + if (configDir) { + const vmOptions = await JetBrainsCommon.readVmOptions(PRODUCT, configDir); + if (parameters.jvmMaxHeapSize != null) result.jvmMaxHeapSize = vmOptions.maxHeap; + if (parameters.jvmMinHeapSize != null) result.jvmMinHeapSize = vmOptions.minHeap; + } + + return result; + } + + override async create(plan: CreatePlan): Promise { + if (Utils.isMacOS()) { + await JetBrainsCommon.installMacOS(PRODUCT); + } else { + await JetBrainsCommon.installLinux(PRODUCT); + } + + const { settingsZip, importSettings = true, jvmMaxHeapSize, jvmMinHeapSize } = plan.desiredConfig; + + if (settingsZip && importSettings) { + await this.importSettingsZip(settingsZip); + } + + if (jvmMaxHeapSize != null || jvmMinHeapSize != null) { + const configDir = await JetBrainsCommon.getOrCreateConfigDir(PRODUCT); + if (configDir) { + await JetBrainsCommon.writeVmOptions(PRODUCT, configDir, jvmMaxHeapSize, jvmMinHeapSize); + } + } + } + + override async modify(pc: ParameterChange, plan: ModifyPlan): Promise { + if (pc.name !== 'jvmMaxHeapSize' && pc.name !== 'jvmMinHeapSize') return; + + const configDir = await JetBrainsCommon.getOrCreateConfigDir(PRODUCT); + if (!configDir) return; + + const { jvmMaxHeapSize, jvmMinHeapSize } = plan.desiredConfig; + + if (jvmMaxHeapSize == null && jvmMinHeapSize == null) { + await JetBrainsCommon.removeVmOptions(PRODUCT, configDir); + } else { + await JetBrainsCommon.writeVmOptions(PRODUCT, configDir, jvmMaxHeapSize, jvmMinHeapSize); + } + } + + override async destroy(plan: DestroyPlan): Promise { + const { jvmMaxHeapSize, jvmMinHeapSize } = plan.currentConfig; + + if (jvmMaxHeapSize != null || jvmMinHeapSize != null) { + const configDir = await JetBrainsCommon.findConfigDir(PRODUCT); + if (configDir) await JetBrainsCommon.removeVmOptions(PRODUCT, configDir); + } + + if (Utils.isMacOS()) { + await JetBrainsCommon.uninstallMacOS(PRODUCT); + } else { + await JetBrainsCommon.uninstallLinux(PRODUCT); + } + } + + private async importSettingsZip(settingsZip: string): Promise { + const $ = getPty(); + + const unzipCheck = await $.spawnSafe('which unzip'); + if (unzipCheck.status !== SpawnStatus.SUCCESS) { + await Utils.installViaPkgMgr('unzip'); + } + + const configDir = await JetBrainsCommon.getOrCreateConfigDir(PRODUCT); + if (!configDir) { + throw new Error('Cannot determine CLion config directory for settings import.'); + } + + await fs.mkdir(configDir, { recursive: true }); + await $.spawn(`unzip -o "${settingsZip}" -d "${configDir}"`, { interactive: true }); + } +} diff --git a/src/resources/jetbrains/clion/completions/clion.plugins.ts b/src/resources/jetbrains/clion/completions/clion.plugins.ts new file mode 100644 index 00000000..9c91f6a3 --- /dev/null +++ b/src/resources/jetbrains/clion/completions/clion.plugins.ts @@ -0,0 +1,15 @@ +export default async function loadClionPlugins(): Promise { + const response = await fetch( + 'https://plugins.jetbrains.com/api/plugins?build=CL&orderBy=downloads&offset=0&limit=500', + { headers: { Accept: 'application/json' } } + ); + + if (!response.ok) { + return []; + } + + const data = await response.json() as Array<{ xmlId?: string }>; + return data + .map((p) => p.xmlId) + .filter((id): id is string => typeof id === 'string' && id.length > 0); +} diff --git a/src/resources/jetbrains/common/jetbrains-common.ts b/src/resources/jetbrains/common/jetbrains-common.ts new file mode 100644 index 00000000..fd6d8118 --- /dev/null +++ b/src/resources/jetbrains/common/jetbrains-common.ts @@ -0,0 +1,371 @@ +import { ArrayStatefulParameter, getPty, Plan, SpawnStatus, Utils } from '@codifycli/plugin-core'; +import { StringIndexedObject } from '@codifycli/schemas'; +import fs from 'node:fs/promises'; +import os from 'node:os'; +import path from 'node:path'; + +/** + * Static metadata describing a single JetBrains IDE product. Each product + * resource (pycharm, clion, rider, ...) provides one of these to the shared + * helper functions below so install/config-discovery/plugin-management logic + * only needs to be written once. + */ +export interface JetBrainsProductInfo { + /** Name of the .app bundle on macOS, without extension, e.g. 'PyCharm', 'IntelliJ IDEA'. */ + macAppName: string; + /** Binary name inside `.app/Contents/MacOS/`, e.g. 'pycharm', 'idea'. Also used as the CLI symlink name on macOS and as the `.vmoptions` file prefix. */ + macBinaryName: string; + /** Prefix of the per-version config/plugins directory under `JetBrains/`, e.g. 'PyCharm', 'IntelliJIdea'. */ + configDirPrefix: string; + /** Homebrew cask name, e.g. 'pycharm', 'intellij-idea'. */ + caskName: string; + /** Snap package name (installed with `--classic`), e.g. 'pycharm-community', 'clion'. */ + snapName: string; + /** CLI command name available on Linux after the snap is installed, e.g. 'pycharm-community', 'clion'. */ + linuxCommand: string; +} + +export class JetBrainsCommon { + static getMacAppPath(product: JetBrainsProductInfo): string { + return `/Applications/${product.macAppName}.app`; + } + + static getMacBinary(product: JetBrainsProductInfo): string { + return `${JetBrainsCommon.getMacAppPath(product)}/Contents/MacOS/${product.macBinaryName}`; + } + + static getBinary(product: JetBrainsProductInfo): string { + return Utils.isMacOS() ? JetBrainsCommon.getMacBinary(product) : product.linuxCommand; + } + + private static getJetBrainsParentDir(): string { + return Utils.isMacOS() + ? path.join(os.homedir(), 'Library', 'Application Support', 'JetBrains') + : path.join(os.homedir(), '.config', 'JetBrains'); + } + + static async findConfigDir(product: JetBrainsProductInfo): Promise { + const parentDir = JetBrainsCommon.getJetBrainsParentDir(); + + try { + const entries = await fs.readdir(parentDir); + const dirs = entries.filter((e) => e.startsWith(product.configDirPrefix)).sort(); + return dirs.length > 0 ? path.join(parentDir, dirs[dirs.length - 1]) : null; + } catch { + return null; + } + } + + static async getOrCreateConfigDir(product: JetBrainsProductInfo): Promise { + const existing = await JetBrainsCommon.findConfigDir(product); + if (existing) return existing; + + const version = await JetBrainsCommon.getMajorMinorVersion(product); + if (!version) return null; + + const parentDir = JetBrainsCommon.getJetBrainsParentDir(); + const configDir = path.join(parentDir, `${product.configDirPrefix}${version}`); + await fs.mkdir(configDir, { recursive: true }); + return configDir; + } + + private static async getMajorMinorVersion(product: JetBrainsProductInfo): Promise { + const $ = getPty(); + + if (Utils.isMacOS()) { + const result = await $.spawnSafe( + `/usr/libexec/PlistBuddy -c "Print CFBundleShortVersionString" "${JetBrainsCommon.getMacAppPath(product)}/Contents/Info.plist"` + ); + if (result.status !== SpawnStatus.SUCCESS) return null; + const parts = result.data.trim().split('.'); + return parts.length >= 2 ? `${parts[0]}.${parts[1]}` : null; + } + + if (Utils.isLinux()) { + const result = await $.spawnSafe(`snap list ${product.snapName}`); + if (result.status !== SpawnStatus.SUCCESS) return null; + const lines = result.data.split('\n'); + const line = lines.find((l) => l.startsWith(product.snapName)); + const match = line?.match(/(\d+\.\d+)/); + return match ? match[1] : null; + } + + return null; + } + + static getPluginsDir(product: JetBrainsProductInfo, configDir: string): string { + // macOS: plugins are in a `plugins/` subdir of the config dir + // Linux: plugins are in ~/.local/share/JetBrains// directly + if (Utils.isMacOS()) { + return path.join(configDir, 'plugins'); + } + // For Linux, derive from config dir path by swapping .config -> .local/share + const version = path.basename(configDir); + return path.join(os.homedir(), '.local', 'share', 'JetBrains', version); + } + + static getBundledPluginsDir(product: JetBrainsProductInfo): string | null { + if (Utils.isMacOS()) return path.join(JetBrainsCommon.getMacAppPath(product), 'Contents', 'plugins'); + if (Utils.isLinux()) return `/snap/${product.snapName}/current/plugins`; + return null; + } + + static async readPluginIdFromDir(pluginDir: string): Promise { + // Try plain META-INF/plugin.xml first (user-installed plugins unzipped as directories) + const xmlPath = path.join(pluginDir, 'META-INF', 'plugin.xml'); + try { + const content = await fs.readFile(xmlPath, 'utf8'); + const match = content.match(/([^<]+)<\/id>/); + if (match) return match[1].trim(); + } catch { /* fall through to JAR search */ } + + // Bundled plugins ship as directories containing JAR files in lib/. + // Requires unzip; skip silently if not available. + const $ = getPty(); + const unzipCheck = await $.spawnSafe('which unzip'); + if (unzipCheck.status !== SpawnStatus.SUCCESS) return null; + + const tryReadIdFromJars = async (subdir: string): Promise => { + const libDir = subdir === '.' ? pluginDir : path.join(pluginDir, subdir); + const entries = await fs.readdir(libDir); + const pluginName = path.basename(pluginDir).toLowerCase(); + // Try the JAR named after the plugin dir first - it's almost always the main one + const jars = entries + .filter((e) => e.endsWith('.jar')) + .sort((a, b) => { + const aMatch = a.toLowerCase().startsWith(pluginName) ? -1 : 0; + const bMatch = b.toLowerCase().startsWith(pluginName) ? -1 : 0; + return aMatch - bMatch; + }); + for (const entry of jars) { + const result = await $.spawnSafe(`unzip -p "${path.join(libDir, entry)}" META-INF/plugin.xml`); + if (result.status !== SpawnStatus.SUCCESS || !result.data) continue; + const match = result.data.match(/([^<]+)<\/id>/); + if (match) return match[1].trim(); + } + throw new Error('no id'); + }; + + const results = await Promise.allSettled(['lib', '.'].map(tryReadIdFromJars)); + for (const r of results) { + if (r.status === 'fulfilled') return r.value; + } + + return null; + } + + // ── vmoptions file helpers ────────────────────────────────────────────────── + + static async readVmOptions(product: JetBrainsProductInfo, configDir: string): Promise<{ maxHeap?: string; minHeap?: string }> { + try { + const content = await fs.readFile(path.join(configDir, `${product.macBinaryName}.vmoptions`), 'utf8'); + const lines = content.split('\n'); + const maxHeap = lines.find((l) => l.startsWith('-Xmx'))?.slice('-Xmx'.length).trim(); + const minHeap = lines.find((l) => l.startsWith('-Xms'))?.slice('-Xms'.length).trim(); + return { maxHeap, minHeap }; + } catch { + return {}; + } + } + + static async writeVmOptions(product: JetBrainsProductInfo, configDir: string, maxHeap?: string, minHeap?: string): Promise { + const optionsPath = path.join(configDir, `${product.macBinaryName}.vmoptions`); + let lines: string[] = []; + + try { + lines = (await fs.readFile(optionsPath, 'utf8')).split('\n'); + } catch { /* file doesn't exist yet */ } + + lines = lines.filter((l) => !l.startsWith('-Xmx') && !l.startsWith('-Xms')); + if (maxHeap) lines.push(`-Xmx${maxHeap}`); + if (minHeap) lines.push(`-Xms${minHeap}`); + + await fs.mkdir(configDir, { recursive: true }); + await fs.writeFile(optionsPath, lines.join('\n').trim() + '\n'); + } + + static async removeVmOptions(product: JetBrainsProductInfo, configDir: string): Promise { + const optionsPath = path.join(configDir, `${product.macBinaryName}.vmoptions`); + try { + const lines = (await fs.readFile(optionsPath, 'utf8')) + .split('\n') + .filter((l) => !l.startsWith('-Xmx') && !l.startsWith('-Xms')); + const content = lines.join('\n').trim(); + if (content) { + await fs.writeFile(optionsPath, content + '\n'); + } else { + await fs.rm(optionsPath, { force: true }); + } + } catch { /* nothing to remove */ } + } + + // ── install / uninstall ──────────────────────────────────────────────────── + + static async isInstalled(product: JetBrainsProductInfo): Promise { + if (Utils.isMacOS()) { + try { + await fs.access(JetBrainsCommon.getMacBinary(product)); + return true; + } catch { + return false; + } + } + + const $ = getPty(); + const result = await $.spawnSafe(`which ${product.linuxCommand}`); + return result.status === SpawnStatus.SUCCESS; + } + + static async installMacOS(product: JetBrainsProductInfo): Promise { + const $ = getPty(); + await $.spawn(`brew install --cask ${product.caskName}`, { + interactive: true, + env: { HOMEBREW_NO_AUTO_UPDATE: '1' }, + }); + // Create a CLI launcher symlink so `` works from the terminal + await $.spawnSafe( + `ln -sf "${JetBrainsCommon.getMacBinary(product)}" /usr/local/bin/${product.macBinaryName}`, + { requiresRoot: true } + ); + } + + static async uninstallMacOS(product: JetBrainsProductInfo): Promise { + const $ = getPty(); + await $.spawnSafe(`brew uninstall --cask ${product.caskName}`, { + env: { HOMEBREW_NO_AUTO_UPDATE: '1' }, + }); + await $.spawnSafe(`rm -f /usr/local/bin/${product.macBinaryName}`, { requiresRoot: true }); + } + + static async installLinux(product: JetBrainsProductInfo): Promise { + const $ = getPty(); + const snapCheck = await $.spawnSafe('which snap'); + if (snapCheck.status !== SpawnStatus.SUCCESS) { + await Utils.installViaPkgMgr('snapd'); + } + await $.spawn(`snap install ${product.snapName} --classic`, { + interactive: true, + requiresRoot: true, + }); + // unzip is needed to read plugin IDs from bundled JAR files + await Utils.installViaPkgMgr('unzip'); + } + + static async uninstallLinux(product: JetBrainsProductInfo): Promise { + const $ = getPty(); + await $.spawnSafe(`snap remove ${product.snapName}`, { requiresRoot: true }); + } +} + +// ── plugins ────────────────────────────────────────────────────────────────── + +/** + * Stateful parameter that manages a JetBrains IDE's installed plugin list. + * `C` is the resource's config type, which must include an optional `plugins?: string[]` field. + */ +export class JetBrainsPluginsParameter extends ArrayStatefulParameter { + constructor(private readonly product: JetBrainsProductInfo) { + super(); + } + + override getSettings() { + return { + type: 'array' as const, + isElementEqual: (desired: string, current: string) => + desired.toLowerCase() === current.toLowerCase(), + }; + } + + override async refresh(desired: string[] | null): Promise { + const readIdsFromDir = async (dir: string): Promise => { + const entries = await fs.readdir(dir, { withFileTypes: true }); + const results = await Promise.all( + entries + .filter((e) => e.isDirectory()) + .map((e) => JetBrainsCommon.readPluginIdFromDir(path.join(dir, e.name))) + ); + return results.filter((id): id is string => id != null); + }; + + const [configDir, bundledDir] = await Promise.all([ + JetBrainsCommon.findConfigDir(this.product), + Promise.resolve(JetBrainsCommon.getBundledPluginsDir(this.product)), + ]); + + if (!configDir && !bundledDir) return null; + + const userIds = configDir + ? await readIdsFromDir(JetBrainsCommon.getPluginsDir(this.product, configDir)).catch(() => [] as string[]) + : []; + + // Only check the bundled dir for desired plugins not found in the user dir, + // to avoid flooding refresh with all default-installed bundled plugins. + if (bundledDir && desired) { + const missing = desired.filter((d) => !userIds.some((u) => u.toLowerCase() === d.toLowerCase())); + if (missing.length > 0) { + const bundledEntries = await fs.readdir(bundledDir, { withFileTypes: true }).catch(() => []); + const bundledIds = await Promise.all( + bundledEntries + .filter((e) => e.isDirectory()) + .map((e) => JetBrainsCommon.readPluginIdFromDir(path.join(bundledDir, e.name))) + ); + for (const id of bundledIds) { + if (id && missing.some((m) => m.toLowerCase() === id.toLowerCase())) { + userIds.push(id); + } + } + } + } + + return userIds; + } + + async addItem(item: string, _plan: Plan): Promise { + // If the plugin is already present in the bundled plugins dir, skip installation. + // On Linux the snap binary fails headlessly with XDG_RUNTIME_DIR errors, so + // we must not call it for plugins that are already bundled. + const bundledDir = JetBrainsCommon.getBundledPluginsDir(this.product); + if (bundledDir) { + try { + const entries = await fs.readdir(bundledDir, { withFileTypes: true }); + for (const entry of entries) { + if (!entry.isDirectory()) continue; + const id = await JetBrainsCommon.readPluginIdFromDir(path.join(bundledDir, entry.name)); + if (id?.toLowerCase() === item.toLowerCase()) return; + } + } catch { /* bundled dir inaccessible, fall through to install */ } + } + + const $ = getPty(); + const binary = JetBrainsCommon.getBinary(this.product); + try { + await $.spawn(`"${binary}" installPlugins ${item}`, { interactive: true }); + } catch (e: unknown) { + const msg = (e instanceof Error ? e.message : String(e)).toLowerCase(); + if (msg.includes('already installed')) return; + if (msg.includes('one instance') || msg.includes('already running') || msg.includes('already open')) { + throw new Error(`${this.product.macAppName} is currently open. JetBrains IDEs only allow one instance open at a time. Please close it and re-run.`); + } + throw e; + } + } + + async removeItem(item: string, _plan: Plan): Promise { + const configDir = await JetBrainsCommon.findConfigDir(this.product); + if (!configDir) return; + + const pluginsDir = JetBrainsCommon.getPluginsDir(this.product, configDir); + try { + const entries = await fs.readdir(pluginsDir, { withFileTypes: true }); + + for (const entry of entries) { + if (!entry.isDirectory()) continue; + const id = await JetBrainsCommon.readPluginIdFromDir(path.join(pluginsDir, entry.name)); + if (id?.toLowerCase() === item.toLowerCase()) { + await fs.rm(path.join(pluginsDir, entry.name), { recursive: true, force: true }); + return; + } + } + } catch { /* plugin dir doesn't exist, nothing to remove */ } + } +} diff --git a/src/resources/jetbrains/goland/completions/goland.plugins.ts b/src/resources/jetbrains/goland/completions/goland.plugins.ts new file mode 100644 index 00000000..8664ee20 --- /dev/null +++ b/src/resources/jetbrains/goland/completions/goland.plugins.ts @@ -0,0 +1,15 @@ +export default async function loadGoLandPlugins(): Promise { + const response = await fetch( + 'https://plugins.jetbrains.com/api/plugins?build=GO&orderBy=downloads&offset=0&limit=500', + { headers: { Accept: 'application/json' } } + ); + + if (!response.ok) { + return []; + } + + const data = await response.json() as Array<{ xmlId?: string }>; + return data + .map((p) => p.xmlId) + .filter((id): id is string => typeof id === 'string' && id.length > 0); +} diff --git a/src/resources/jetbrains/goland/goland.ts b/src/resources/jetbrains/goland/goland.ts new file mode 100644 index 00000000..f4bc7659 --- /dev/null +++ b/src/resources/jetbrains/goland/goland.ts @@ -0,0 +1,203 @@ +import { + CreatePlan, + DestroyPlan, + ExampleConfig, + ModifyPlan, + ParameterChange, + Resource, + ResourceSettings, + SpawnStatus, + Utils, + getPty, + z, +} from '@codifycli/plugin-core'; +import { OS } from '@codifycli/schemas'; +import fs from 'node:fs/promises'; + +import { + JetBrainsCommon, + JetBrainsPluginsParameter, + JetBrainsProductInfo, +} from '../common/jetbrains-common.js'; + +const PRODUCT: JetBrainsProductInfo = { + macAppName: 'GoLand', + macBinaryName: 'goland', + configDirPrefix: 'GoLand', + caskName: 'goland', + snapName: 'goland', + linuxCommand: 'goland', +}; + +const schema = z + .object({ + settingsZip: z + .string() + .optional() + .describe('Absolute path to a GoLand settings ZIP file to import on first install.'), + importSettings: z + .boolean() + .optional() + .describe( + 'Whether to import the settings from settingsZip during create. ' + + 'Defaults to true. Set to false to skip the import even when settingsZip is provided.' + ), + plugins: z + .array(z.string()) + .optional() + .describe( + 'JetBrains Marketplace plugin IDs to install ' + + '(e.g. "com.github.copilot", "Docker"). ' + + 'Plugin IDs can be found on the plugin page under Additional Information.' + ), + jvmMaxHeapSize: z + .string() + .optional() + .describe('Maximum JVM heap size for GoLand, e.g. "2048m" for 2 GB. Defaults to the IDE default (~2 GB).'), + jvmMinHeapSize: z + .string() + .optional() + .describe('Initial JVM heap size for GoLand, e.g. "512m". Defaults to the IDE default.'), + }) + .meta({ $comment: 'https://codifycli.com/docs/resources/goland' }) + .describe('Install and configure JetBrains GoLand IDE with plugins and JVM settings.'); + +export type GoLandConfig = z.infer; + +const defaultConfig: Partial = { + plugins: [], +}; + +const exampleBasic: ExampleConfig = { + title: 'GoLand with GitHub Copilot and Docker plugins', + description: + 'Install GoLand and add the GitHub Copilot and Docker integration plugins for a modern Go development workflow.', + configs: [ + { + type: 'goland', + plugins: ['com.github.copilot', 'Docker'], + }, + ], +}; + +const exampleAdvanced: ExampleConfig = { + title: 'GoLand with tuned JVM and imported settings', + description: + 'Install GoLand, import previous settings from a ZIP, and increase the heap to 4 GB for large projects.', + configs: [ + { + type: 'goland', + settingsZip: '/path/to/goland-settings.zip', + importSettings: true, + jvmMaxHeapSize: '4096m', + jvmMinHeapSize: '1024m', + plugins: ['com.github.copilot', 'Docker'], + }, + ], +}; + +export class GoLandResource extends Resource { + getSettings(): ResourceSettings { + return { + id: 'goland', + operatingSystems: [OS.Darwin, OS.Linux], + schema, + defaultConfig, + exampleConfigs: { + example1: exampleBasic, + example2: exampleAdvanced, + }, + parameterSettings: { + settingsZip: { type: 'string', setting: true }, + importSettings: { type: 'boolean', setting: true }, + plugins: { type: 'stateful', definition: new JetBrainsPluginsParameter(PRODUCT), order: 1 }, + jvmMaxHeapSize: { type: 'string', canModify: true }, + jvmMinHeapSize: { type: 'string', canModify: true }, + }, + }; + } + + override async refresh(parameters: Partial): Promise | null> { + const installed = await JetBrainsCommon.isInstalled(PRODUCT); + if (!installed) return null; + + const result: Partial = {}; + + const configDir = await JetBrainsCommon.findConfigDir(PRODUCT); + if (configDir) { + const vmOptions = await JetBrainsCommon.readVmOptions(PRODUCT, configDir); + if (parameters.jvmMaxHeapSize != null) result.jvmMaxHeapSize = vmOptions.maxHeap; + if (parameters.jvmMinHeapSize != null) result.jvmMinHeapSize = vmOptions.minHeap; + } + + return result; + } + + override async create(plan: CreatePlan): Promise { + if (Utils.isMacOS()) { + await JetBrainsCommon.installMacOS(PRODUCT); + } else { + await JetBrainsCommon.installLinux(PRODUCT); + } + + const { settingsZip, importSettings = true, jvmMaxHeapSize, jvmMinHeapSize } = plan.desiredConfig; + + if (settingsZip && importSettings) { + await this.importSettingsZip(settingsZip); + } + + if (jvmMaxHeapSize != null || jvmMinHeapSize != null) { + const configDir = await JetBrainsCommon.getOrCreateConfigDir(PRODUCT); + if (configDir) { + await JetBrainsCommon.writeVmOptions(PRODUCT, configDir, jvmMaxHeapSize, jvmMinHeapSize); + } + } + } + + override async modify(pc: ParameterChange, plan: ModifyPlan): Promise { + if (pc.name !== 'jvmMaxHeapSize' && pc.name !== 'jvmMinHeapSize') return; + + const configDir = await JetBrainsCommon.getOrCreateConfigDir(PRODUCT); + if (!configDir) return; + + const { jvmMaxHeapSize, jvmMinHeapSize } = plan.desiredConfig; + + if (jvmMaxHeapSize == null && jvmMinHeapSize == null) { + await JetBrainsCommon.removeVmOptions(PRODUCT, configDir); + } else { + await JetBrainsCommon.writeVmOptions(PRODUCT, configDir, jvmMaxHeapSize, jvmMinHeapSize); + } + } + + override async destroy(plan: DestroyPlan): Promise { + const { jvmMaxHeapSize, jvmMinHeapSize } = plan.currentConfig; + + if (jvmMaxHeapSize != null || jvmMinHeapSize != null) { + const configDir = await JetBrainsCommon.findConfigDir(PRODUCT); + if (configDir) await JetBrainsCommon.removeVmOptions(PRODUCT, configDir); + } + + if (Utils.isMacOS()) { + await JetBrainsCommon.uninstallMacOS(PRODUCT); + } else { + await JetBrainsCommon.uninstallLinux(PRODUCT); + } + } + + private async importSettingsZip(settingsZip: string): Promise { + const $ = getPty(); + + const unzipCheck = await $.spawnSafe('which unzip'); + if (unzipCheck.status !== SpawnStatus.SUCCESS) { + await Utils.installViaPkgMgr('unzip'); + } + + const configDir = await JetBrainsCommon.getOrCreateConfigDir(PRODUCT); + if (!configDir) { + throw new Error('Cannot determine GoLand config directory for settings import.'); + } + + await fs.mkdir(configDir, { recursive: true }); + await $.spawn(`unzip -o "${settingsZip}" -d "${configDir}"`, { interactive: true }); + } +} diff --git a/src/resources/jetbrains/intellij-idea/completions/intellij-idea.plugins.ts b/src/resources/jetbrains/intellij-idea/completions/intellij-idea.plugins.ts new file mode 100644 index 00000000..b3afde74 --- /dev/null +++ b/src/resources/jetbrains/intellij-idea/completions/intellij-idea.plugins.ts @@ -0,0 +1,15 @@ +export default async function loadIntellijIdeaPlugins(): Promise { + const response = await fetch( + 'https://plugins.jetbrains.com/api/plugins?build=IC&orderBy=downloads&offset=0&limit=500', + { headers: { Accept: 'application/json' } } + ); + + if (!response.ok) { + return []; + } + + const data = await response.json() as Array<{ xmlId?: string }>; + return data + .map((p) => p.xmlId) + .filter((id): id is string => typeof id === 'string' && id.length > 0); +} diff --git a/src/resources/jetbrains/intellij-idea/intellij-idea.ts b/src/resources/jetbrains/intellij-idea/intellij-idea.ts new file mode 100644 index 00000000..bab038eb --- /dev/null +++ b/src/resources/jetbrains/intellij-idea/intellij-idea.ts @@ -0,0 +1,203 @@ +import { + CreatePlan, + DestroyPlan, + ExampleConfig, + ModifyPlan, + ParameterChange, + Resource, + ResourceSettings, + SpawnStatus, + Utils, + getPty, + z, +} from '@codifycli/plugin-core'; +import { OS } from '@codifycli/schemas'; +import fs from 'node:fs/promises'; + +import { + JetBrainsCommon, + JetBrainsPluginsParameter, + JetBrainsProductInfo, +} from '../common/jetbrains-common.js'; + +const PRODUCT: JetBrainsProductInfo = { + macAppName: 'IntelliJ IDEA', + macBinaryName: 'idea', + configDirPrefix: 'IntelliJIdea', + caskName: 'intellij-idea', + snapName: 'intellij-idea-community', + linuxCommand: 'intellij-idea-community', +}; + +const schema = z + .object({ + settingsZip: z + .string() + .optional() + .describe('Absolute path to an IntelliJ IDEA settings ZIP file to import on first install.'), + importSettings: z + .boolean() + .optional() + .describe( + 'Whether to import the settings from settingsZip during create. ' + + 'Defaults to true. Set to false to skip the import even when settingsZip is provided.' + ), + plugins: z + .array(z.string()) + .optional() + .describe( + 'JetBrains Marketplace plugin IDs to install ' + + '(e.g. "com.github.copilot", "Docker"). ' + + 'Plugin IDs can be found on the plugin page under Additional Information.' + ), + jvmMaxHeapSize: z + .string() + .optional() + .describe('Maximum JVM heap size for IntelliJ IDEA, e.g. "2048m" for 2 GB. Defaults to the IDE default (~2 GB).'), + jvmMinHeapSize: z + .string() + .optional() + .describe('Initial JVM heap size for IntelliJ IDEA, e.g. "512m". Defaults to the IDE default.'), + }) + .meta({ $comment: 'https://codifycli.com/docs/resources/intellij-idea' }) + .describe('Install and configure JetBrains IntelliJ IDEA IDE with plugins and JVM settings.'); + +export type IntellijIdeaConfig = z.infer; + +const defaultConfig: Partial = { + plugins: [], +}; + +const exampleBasic: ExampleConfig = { + title: 'IntelliJ IDEA with GitHub Copilot and Docker plugins', + description: + 'Install IntelliJ IDEA and add the GitHub Copilot and Docker integration plugins for a modern JVM development workflow.', + configs: [ + { + type: 'intellij-idea', + plugins: ['com.github.copilot', 'Docker'], + }, + ], +}; + +const exampleAdvanced: ExampleConfig = { + title: 'IntelliJ IDEA with tuned JVM and imported settings', + description: + 'Install IntelliJ IDEA, import previous settings from a ZIP, and increase the heap to 4 GB for large projects.', + configs: [ + { + type: 'intellij-idea', + settingsZip: '/path/to/intellij-idea-settings.zip', + importSettings: true, + jvmMaxHeapSize: '4096m', + jvmMinHeapSize: '1024m', + plugins: ['com.github.copilot', 'Docker'], + }, + ], +}; + +export class IntellijIdeaResource extends Resource { + getSettings(): ResourceSettings { + return { + id: 'intellij-idea', + operatingSystems: [OS.Darwin, OS.Linux], + schema, + defaultConfig, + exampleConfigs: { + example1: exampleBasic, + example2: exampleAdvanced, + }, + parameterSettings: { + settingsZip: { type: 'string', setting: true }, + importSettings: { type: 'boolean', setting: true }, + plugins: { type: 'stateful', definition: new JetBrainsPluginsParameter(PRODUCT), order: 1 }, + jvmMaxHeapSize: { type: 'string', canModify: true }, + jvmMinHeapSize: { type: 'string', canModify: true }, + }, + }; + } + + override async refresh(parameters: Partial): Promise | null> { + const installed = await JetBrainsCommon.isInstalled(PRODUCT); + if (!installed) return null; + + const result: Partial = {}; + + const configDir = await JetBrainsCommon.findConfigDir(PRODUCT); + if (configDir) { + const vmOptions = await JetBrainsCommon.readVmOptions(PRODUCT, configDir); + if (parameters.jvmMaxHeapSize != null) result.jvmMaxHeapSize = vmOptions.maxHeap; + if (parameters.jvmMinHeapSize != null) result.jvmMinHeapSize = vmOptions.minHeap; + } + + return result; + } + + override async create(plan: CreatePlan): Promise { + if (Utils.isMacOS()) { + await JetBrainsCommon.installMacOS(PRODUCT); + } else { + await JetBrainsCommon.installLinux(PRODUCT); + } + + const { settingsZip, importSettings = true, jvmMaxHeapSize, jvmMinHeapSize } = plan.desiredConfig; + + if (settingsZip && importSettings) { + await this.importSettingsZip(settingsZip); + } + + if (jvmMaxHeapSize != null || jvmMinHeapSize != null) { + const configDir = await JetBrainsCommon.getOrCreateConfigDir(PRODUCT); + if (configDir) { + await JetBrainsCommon.writeVmOptions(PRODUCT, configDir, jvmMaxHeapSize, jvmMinHeapSize); + } + } + } + + override async modify(pc: ParameterChange, plan: ModifyPlan): Promise { + if (pc.name !== 'jvmMaxHeapSize' && pc.name !== 'jvmMinHeapSize') return; + + const configDir = await JetBrainsCommon.getOrCreateConfigDir(PRODUCT); + if (!configDir) return; + + const { jvmMaxHeapSize, jvmMinHeapSize } = plan.desiredConfig; + + if (jvmMaxHeapSize == null && jvmMinHeapSize == null) { + await JetBrainsCommon.removeVmOptions(PRODUCT, configDir); + } else { + await JetBrainsCommon.writeVmOptions(PRODUCT, configDir, jvmMaxHeapSize, jvmMinHeapSize); + } + } + + override async destroy(plan: DestroyPlan): Promise { + const { jvmMaxHeapSize, jvmMinHeapSize } = plan.currentConfig; + + if (jvmMaxHeapSize != null || jvmMinHeapSize != null) { + const configDir = await JetBrainsCommon.findConfigDir(PRODUCT); + if (configDir) await JetBrainsCommon.removeVmOptions(PRODUCT, configDir); + } + + if (Utils.isMacOS()) { + await JetBrainsCommon.uninstallMacOS(PRODUCT); + } else { + await JetBrainsCommon.uninstallLinux(PRODUCT); + } + } + + private async importSettingsZip(settingsZip: string): Promise { + const $ = getPty(); + + const unzipCheck = await $.spawnSafe('which unzip'); + if (unzipCheck.status !== SpawnStatus.SUCCESS) { + await Utils.installViaPkgMgr('unzip'); + } + + const configDir = await JetBrainsCommon.getOrCreateConfigDir(PRODUCT); + if (!configDir) { + throw new Error('Cannot determine IntelliJ IDEA config directory for settings import.'); + } + + await fs.mkdir(configDir, { recursive: true }); + await $.spawn(`unzip -o "${settingsZip}" -d "${configDir}"`, { interactive: true }); + } +} diff --git a/src/resources/jetbrains/phpstorm/completions/phpstorm.plugins.ts b/src/resources/jetbrains/phpstorm/completions/phpstorm.plugins.ts new file mode 100644 index 00000000..0e7ba076 --- /dev/null +++ b/src/resources/jetbrains/phpstorm/completions/phpstorm.plugins.ts @@ -0,0 +1,15 @@ +export default async function loadPhpStormPlugins(): Promise { + const response = await fetch( + 'https://plugins.jetbrains.com/api/plugins?build=PS&orderBy=downloads&offset=0&limit=500', + { headers: { Accept: 'application/json' } } + ); + + if (!response.ok) { + return []; + } + + const data = await response.json() as Array<{ xmlId?: string }>; + return data + .map((p) => p.xmlId) + .filter((id): id is string => typeof id === 'string' && id.length > 0); +} diff --git a/src/resources/jetbrains/phpstorm/phpstorm.ts b/src/resources/jetbrains/phpstorm/phpstorm.ts new file mode 100644 index 00000000..f2349bbb --- /dev/null +++ b/src/resources/jetbrains/phpstorm/phpstorm.ts @@ -0,0 +1,203 @@ +import { + CreatePlan, + DestroyPlan, + ExampleConfig, + ModifyPlan, + ParameterChange, + Resource, + ResourceSettings, + SpawnStatus, + Utils, + getPty, + z, +} from '@codifycli/plugin-core'; +import { OS } from '@codifycli/schemas'; +import fs from 'node:fs/promises'; + +import { + JetBrainsCommon, + JetBrainsPluginsParameter, + JetBrainsProductInfo, +} from '../common/jetbrains-common.js'; + +const PRODUCT: JetBrainsProductInfo = { + macAppName: 'PhpStorm', + macBinaryName: 'phpstorm', + configDirPrefix: 'PhpStorm', + caskName: 'phpstorm', + snapName: 'phpstorm', + linuxCommand: 'phpstorm', +}; + +const schema = z + .object({ + settingsZip: z + .string() + .optional() + .describe('Absolute path to a PhpStorm settings ZIP file to import on first install.'), + importSettings: z + .boolean() + .optional() + .describe( + 'Whether to import the settings from settingsZip during create. ' + + 'Defaults to true. Set to false to skip the import even when settingsZip is provided.' + ), + plugins: z + .array(z.string()) + .optional() + .describe( + 'JetBrains Marketplace plugin IDs to install ' + + '(e.g. "com.github.copilot", "Docker"). ' + + 'Plugin IDs can be found on the plugin page under Additional Information.' + ), + jvmMaxHeapSize: z + .string() + .optional() + .describe('Maximum JVM heap size for PhpStorm, e.g. "2048m" for 2 GB. Defaults to the IDE default (~2 GB).'), + jvmMinHeapSize: z + .string() + .optional() + .describe('Initial JVM heap size for PhpStorm, e.g. "512m". Defaults to the IDE default.'), + }) + .meta({ $comment: 'https://codifycli.com/docs/resources/phpstorm' }) + .describe('Install and configure JetBrains PhpStorm IDE with plugins and JVM settings.'); + +export type PhpStormConfig = z.infer; + +const defaultConfig: Partial = { + plugins: [], +}; + +const exampleBasic: ExampleConfig = { + title: 'PhpStorm with GitHub Copilot and Docker plugins', + description: + 'Install PhpStorm and add the GitHub Copilot and Docker integration plugins for a modern PHP development workflow.', + configs: [ + { + type: 'phpstorm', + plugins: ['com.github.copilot', 'Docker'], + }, + ], +}; + +const exampleAdvanced: ExampleConfig = { + title: 'PhpStorm with tuned JVM and imported settings', + description: + 'Install PhpStorm, import previous settings from a ZIP, and increase the heap to 4 GB for large projects.', + configs: [ + { + type: 'phpstorm', + settingsZip: '/path/to/phpstorm-settings.zip', + importSettings: true, + jvmMaxHeapSize: '4096m', + jvmMinHeapSize: '1024m', + plugins: ['com.github.copilot', 'Docker'], + }, + ], +}; + +export class PhpStormResource extends Resource { + getSettings(): ResourceSettings { + return { + id: 'phpstorm', + operatingSystems: [OS.Darwin, OS.Linux], + schema, + defaultConfig, + exampleConfigs: { + example1: exampleBasic, + example2: exampleAdvanced, + }, + parameterSettings: { + settingsZip: { type: 'string', setting: true }, + importSettings: { type: 'boolean', setting: true }, + plugins: { type: 'stateful', definition: new JetBrainsPluginsParameter(PRODUCT), order: 1 }, + jvmMaxHeapSize: { type: 'string', canModify: true }, + jvmMinHeapSize: { type: 'string', canModify: true }, + }, + }; + } + + override async refresh(parameters: Partial): Promise | null> { + const installed = await JetBrainsCommon.isInstalled(PRODUCT); + if (!installed) return null; + + const result: Partial = {}; + + const configDir = await JetBrainsCommon.findConfigDir(PRODUCT); + if (configDir) { + const vmOptions = await JetBrainsCommon.readVmOptions(PRODUCT, configDir); + if (parameters.jvmMaxHeapSize != null) result.jvmMaxHeapSize = vmOptions.maxHeap; + if (parameters.jvmMinHeapSize != null) result.jvmMinHeapSize = vmOptions.minHeap; + } + + return result; + } + + override async create(plan: CreatePlan): Promise { + if (Utils.isMacOS()) { + await JetBrainsCommon.installMacOS(PRODUCT); + } else { + await JetBrainsCommon.installLinux(PRODUCT); + } + + const { settingsZip, importSettings = true, jvmMaxHeapSize, jvmMinHeapSize } = plan.desiredConfig; + + if (settingsZip && importSettings) { + await this.importSettingsZip(settingsZip); + } + + if (jvmMaxHeapSize != null || jvmMinHeapSize != null) { + const configDir = await JetBrainsCommon.getOrCreateConfigDir(PRODUCT); + if (configDir) { + await JetBrainsCommon.writeVmOptions(PRODUCT, configDir, jvmMaxHeapSize, jvmMinHeapSize); + } + } + } + + override async modify(pc: ParameterChange, plan: ModifyPlan): Promise { + if (pc.name !== 'jvmMaxHeapSize' && pc.name !== 'jvmMinHeapSize') return; + + const configDir = await JetBrainsCommon.getOrCreateConfigDir(PRODUCT); + if (!configDir) return; + + const { jvmMaxHeapSize, jvmMinHeapSize } = plan.desiredConfig; + + if (jvmMaxHeapSize == null && jvmMinHeapSize == null) { + await JetBrainsCommon.removeVmOptions(PRODUCT, configDir); + } else { + await JetBrainsCommon.writeVmOptions(PRODUCT, configDir, jvmMaxHeapSize, jvmMinHeapSize); + } + } + + override async destroy(plan: DestroyPlan): Promise { + const { jvmMaxHeapSize, jvmMinHeapSize } = plan.currentConfig; + + if (jvmMaxHeapSize != null || jvmMinHeapSize != null) { + const configDir = await JetBrainsCommon.findConfigDir(PRODUCT); + if (configDir) await JetBrainsCommon.removeVmOptions(PRODUCT, configDir); + } + + if (Utils.isMacOS()) { + await JetBrainsCommon.uninstallMacOS(PRODUCT); + } else { + await JetBrainsCommon.uninstallLinux(PRODUCT); + } + } + + private async importSettingsZip(settingsZip: string): Promise { + const $ = getPty(); + + const unzipCheck = await $.spawnSafe('which unzip'); + if (unzipCheck.status !== SpawnStatus.SUCCESS) { + await Utils.installViaPkgMgr('unzip'); + } + + const configDir = await JetBrainsCommon.getOrCreateConfigDir(PRODUCT); + if (!configDir) { + throw new Error('Cannot determine PhpStorm config directory for settings import.'); + } + + await fs.mkdir(configDir, { recursive: true }); + await $.spawn(`unzip -o "${settingsZip}" -d "${configDir}"`, { interactive: true }); + } +} diff --git a/src/resources/jetbrains/pycharm/completions/pycharm.plugins.ts b/src/resources/jetbrains/pycharm/completions/pycharm.plugins.ts new file mode 100644 index 00000000..7ab33b11 --- /dev/null +++ b/src/resources/jetbrains/pycharm/completions/pycharm.plugins.ts @@ -0,0 +1,15 @@ +export default async function loadPyCharmPlugins(): Promise { + const response = await fetch( + 'https://plugins.jetbrains.com/api/plugins?build=PC&orderBy=downloads&offset=0&limit=500', + { headers: { Accept: 'application/json' } } + ); + + if (!response.ok) { + return []; + } + + const data = await response.json() as Array<{ xmlId?: string }>; + return data + .map((p) => p.xmlId) + .filter((id): id is string => typeof id === 'string' && id.length > 0); +} diff --git a/src/resources/jetbrains/pycharm/pycharm.ts b/src/resources/jetbrains/pycharm/pycharm.ts new file mode 100644 index 00000000..d48adcdf --- /dev/null +++ b/src/resources/jetbrains/pycharm/pycharm.ts @@ -0,0 +1,203 @@ +import { + CreatePlan, + DestroyPlan, + ExampleConfig, + ModifyPlan, + ParameterChange, + Resource, + ResourceSettings, + SpawnStatus, + Utils, + getPty, + z, +} from '@codifycli/plugin-core'; +import { OS } from '@codifycli/schemas'; +import fs from 'node:fs/promises'; + +import { + JetBrainsCommon, + JetBrainsPluginsParameter, + JetBrainsProductInfo, +} from '../common/jetbrains-common.js'; + +const PRODUCT: JetBrainsProductInfo = { + macAppName: 'PyCharm', + macBinaryName: 'pycharm', + configDirPrefix: 'PyCharm', + caskName: 'pycharm', + snapName: 'pycharm-community', + linuxCommand: 'pycharm-community', +}; + +const schema = z + .object({ + settingsZip: z + .string() + .optional() + .describe('Absolute path to a PyCharm settings ZIP file to import on first install.'), + importSettings: z + .boolean() + .optional() + .describe( + 'Whether to import the settings from settingsZip during create. ' + + 'Defaults to true. Set to false to skip the import even when settingsZip is provided.' + ), + plugins: z + .array(z.string()) + .optional() + .describe( + 'JetBrains Marketplace plugin IDs to install ' + + '(e.g. "com.intellij.ml.llm", "Pythonid"). ' + + 'Plugin IDs can be found on the plugin page under Additional Information.' + ), + jvmMaxHeapSize: z + .string() + .optional() + .describe('Maximum JVM heap size for PyCharm, e.g. "2048m" for 2 GB. Defaults to the IDE default (~2 GB).'), + jvmMinHeapSize: z + .string() + .optional() + .describe('Initial JVM heap size for PyCharm, e.g. "512m". Defaults to the IDE default.'), + }) + .meta({ $comment: 'https://codifycli.com/docs/resources/pycharm' }) + .describe('Install and configure JetBrains PyCharm IDE with plugins and JVM settings.'); + +export type PyCharmConfig = z.infer; + +const defaultConfig: Partial = { + plugins: [], +}; + +const exampleBasic: ExampleConfig = { + title: 'PyCharm with data science and Docker plugins', + description: + 'Install PyCharm and add the Jupyter notebook support and Docker integration plugins for a data science workflow.', + configs: [ + { + type: 'pycharm', + plugins: ['intellij.jupyter', 'Docker'], + }, + ], +}; + +const exampleAdvanced: ExampleConfig = { + title: 'PyCharm with tuned JVM and imported settings', + description: + 'Install PyCharm, import previous settings from a ZIP, and increase the heap to 4 GB for large projects.', + configs: [ + { + type: 'pycharm', + settingsZip: '/path/to/pycharm-settings.zip', + importSettings: true, + jvmMaxHeapSize: '4096m', + jvmMinHeapSize: '1024m', + plugins: ['intellij.jupyter', 'Docker'], + }, + ], +}; + +export class PyCharmResource extends Resource { + getSettings(): ResourceSettings { + return { + id: 'pycharm', + operatingSystems: [OS.Darwin, OS.Linux], + schema, + defaultConfig, + exampleConfigs: { + example1: exampleBasic, + example2: exampleAdvanced, + }, + parameterSettings: { + settingsZip: { type: 'string', setting: true }, + importSettings: { type: 'boolean', setting: true }, + plugins: { type: 'stateful', definition: new JetBrainsPluginsParameter(PRODUCT), order: 1 }, + jvmMaxHeapSize: { type: 'string', canModify: true }, + jvmMinHeapSize: { type: 'string', canModify: true }, + }, + }; + } + + override async refresh(parameters: Partial): Promise | null> { + const installed = await JetBrainsCommon.isInstalled(PRODUCT); + if (!installed) return null; + + const result: Partial = {}; + + const configDir = await JetBrainsCommon.findConfigDir(PRODUCT); + if (configDir) { + const vmOptions = await JetBrainsCommon.readVmOptions(PRODUCT, configDir); + if (parameters.jvmMaxHeapSize != null) result.jvmMaxHeapSize = vmOptions.maxHeap; + if (parameters.jvmMinHeapSize != null) result.jvmMinHeapSize = vmOptions.minHeap; + } + + return result; + } + + override async create(plan: CreatePlan): Promise { + if (Utils.isMacOS()) { + await JetBrainsCommon.installMacOS(PRODUCT); + } else { + await JetBrainsCommon.installLinux(PRODUCT); + } + + const { settingsZip, importSettings = true, jvmMaxHeapSize, jvmMinHeapSize } = plan.desiredConfig; + + if (settingsZip && importSettings) { + await this.importSettingsZip(settingsZip); + } + + if (jvmMaxHeapSize != null || jvmMinHeapSize != null) { + const configDir = await JetBrainsCommon.getOrCreateConfigDir(PRODUCT); + if (configDir) { + await JetBrainsCommon.writeVmOptions(PRODUCT, configDir, jvmMaxHeapSize, jvmMinHeapSize); + } + } + } + + override async modify(pc: ParameterChange, plan: ModifyPlan): Promise { + if (pc.name !== 'jvmMaxHeapSize' && pc.name !== 'jvmMinHeapSize') return; + + const configDir = await JetBrainsCommon.getOrCreateConfigDir(PRODUCT); + if (!configDir) return; + + const { jvmMaxHeapSize, jvmMinHeapSize } = plan.desiredConfig; + + if (jvmMaxHeapSize == null && jvmMinHeapSize == null) { + await JetBrainsCommon.removeVmOptions(PRODUCT, configDir); + } else { + await JetBrainsCommon.writeVmOptions(PRODUCT, configDir, jvmMaxHeapSize, jvmMinHeapSize); + } + } + + override async destroy(plan: DestroyPlan): Promise { + const { jvmMaxHeapSize, jvmMinHeapSize } = plan.currentConfig; + + if (jvmMaxHeapSize != null || jvmMinHeapSize != null) { + const configDir = await JetBrainsCommon.findConfigDir(PRODUCT); + if (configDir) await JetBrainsCommon.removeVmOptions(PRODUCT, configDir); + } + + if (Utils.isMacOS()) { + await JetBrainsCommon.uninstallMacOS(PRODUCT); + } else { + await JetBrainsCommon.uninstallLinux(PRODUCT); + } + } + + private async importSettingsZip(settingsZip: string): Promise { + const $ = getPty(); + + const unzipCheck = await $.spawnSafe('which unzip'); + if (unzipCheck.status !== SpawnStatus.SUCCESS) { + await Utils.installViaPkgMgr('unzip'); + } + + const configDir = await JetBrainsCommon.getOrCreateConfigDir(PRODUCT); + if (!configDir) { + throw new Error('Cannot determine PyCharm config directory for settings import.'); + } + + await fs.mkdir(configDir, { recursive: true }); + await $.spawn(`unzip -o "${settingsZip}" -d "${configDir}"`, { interactive: true }); + } +} diff --git a/src/resources/jetbrains/rider/completions/rider.plugins.ts b/src/resources/jetbrains/rider/completions/rider.plugins.ts new file mode 100644 index 00000000..254aca66 --- /dev/null +++ b/src/resources/jetbrains/rider/completions/rider.plugins.ts @@ -0,0 +1,15 @@ +export default async function loadRiderPlugins(): Promise { + const response = await fetch( + 'https://plugins.jetbrains.com/api/plugins?build=RD&orderBy=downloads&offset=0&limit=500', + { headers: { Accept: 'application/json' } } + ); + + if (!response.ok) { + return []; + } + + const data = await response.json() as Array<{ xmlId?: string }>; + return data + .map((p) => p.xmlId) + .filter((id): id is string => typeof id === 'string' && id.length > 0); +} diff --git a/src/resources/jetbrains/rider/rider.ts b/src/resources/jetbrains/rider/rider.ts new file mode 100644 index 00000000..307c14a5 --- /dev/null +++ b/src/resources/jetbrains/rider/rider.ts @@ -0,0 +1,203 @@ +import { + CreatePlan, + DestroyPlan, + ExampleConfig, + ModifyPlan, + ParameterChange, + Resource, + ResourceSettings, + SpawnStatus, + Utils, + getPty, + z, +} from '@codifycli/plugin-core'; +import { OS } from '@codifycli/schemas'; +import fs from 'node:fs/promises'; + +import { + JetBrainsCommon, + JetBrainsPluginsParameter, + JetBrainsProductInfo, +} from '../common/jetbrains-common.js'; + +const PRODUCT: JetBrainsProductInfo = { + macAppName: 'Rider', + macBinaryName: 'rider', + configDirPrefix: 'Rider', + caskName: 'rider', + snapName: 'rider', + linuxCommand: 'rider', +}; + +const schema = z + .object({ + settingsZip: z + .string() + .optional() + .describe('Absolute path to a Rider settings ZIP file to import on first install.'), + importSettings: z + .boolean() + .optional() + .describe( + 'Whether to import the settings from settingsZip during create. ' + + 'Defaults to true. Set to false to skip the import even when settingsZip is provided.' + ), + plugins: z + .array(z.string()) + .optional() + .describe( + 'JetBrains Marketplace plugin IDs to install ' + + '(e.g. "com.github.copilot", "Docker"). ' + + 'Plugin IDs can be found on the plugin page under Additional Information.' + ), + jvmMaxHeapSize: z + .string() + .optional() + .describe('Maximum JVM heap size for Rider, e.g. "2048m" for 2 GB. Defaults to the IDE default (~2 GB).'), + jvmMinHeapSize: z + .string() + .optional() + .describe('Initial JVM heap size for Rider, e.g. "512m". Defaults to the IDE default.'), + }) + .meta({ $comment: 'https://codifycli.com/docs/resources/rider' }) + .describe('Install and configure JetBrains Rider IDE with plugins and JVM settings.'); + +export type RiderConfig = z.infer; + +const defaultConfig: Partial = { + plugins: [], +}; + +const exampleBasic: ExampleConfig = { + title: 'Rider with GitHub Copilot and Docker plugins', + description: + 'Install Rider and add the GitHub Copilot and Docker integration plugins for a modern .NET development workflow.', + configs: [ + { + type: 'rider', + plugins: ['com.github.copilot', 'Docker'], + }, + ], +}; + +const exampleAdvanced: ExampleConfig = { + title: 'Rider with tuned JVM and imported settings', + description: + 'Install Rider, import previous settings from a ZIP, and increase the heap to 4 GB for large projects.', + configs: [ + { + type: 'rider', + settingsZip: '/path/to/rider-settings.zip', + importSettings: true, + jvmMaxHeapSize: '4096m', + jvmMinHeapSize: '1024m', + plugins: ['com.github.copilot', 'Docker'], + }, + ], +}; + +export class RiderResource extends Resource { + getSettings(): ResourceSettings { + return { + id: 'rider', + operatingSystems: [OS.Darwin, OS.Linux], + schema, + defaultConfig, + exampleConfigs: { + example1: exampleBasic, + example2: exampleAdvanced, + }, + parameterSettings: { + settingsZip: { type: 'string', setting: true }, + importSettings: { type: 'boolean', setting: true }, + plugins: { type: 'stateful', definition: new JetBrainsPluginsParameter(PRODUCT), order: 1 }, + jvmMaxHeapSize: { type: 'string', canModify: true }, + jvmMinHeapSize: { type: 'string', canModify: true }, + }, + }; + } + + override async refresh(parameters: Partial): Promise | null> { + const installed = await JetBrainsCommon.isInstalled(PRODUCT); + if (!installed) return null; + + const result: Partial = {}; + + const configDir = await JetBrainsCommon.findConfigDir(PRODUCT); + if (configDir) { + const vmOptions = await JetBrainsCommon.readVmOptions(PRODUCT, configDir); + if (parameters.jvmMaxHeapSize != null) result.jvmMaxHeapSize = vmOptions.maxHeap; + if (parameters.jvmMinHeapSize != null) result.jvmMinHeapSize = vmOptions.minHeap; + } + + return result; + } + + override async create(plan: CreatePlan): Promise { + if (Utils.isMacOS()) { + await JetBrainsCommon.installMacOS(PRODUCT); + } else { + await JetBrainsCommon.installLinux(PRODUCT); + } + + const { settingsZip, importSettings = true, jvmMaxHeapSize, jvmMinHeapSize } = plan.desiredConfig; + + if (settingsZip && importSettings) { + await this.importSettingsZip(settingsZip); + } + + if (jvmMaxHeapSize != null || jvmMinHeapSize != null) { + const configDir = await JetBrainsCommon.getOrCreateConfigDir(PRODUCT); + if (configDir) { + await JetBrainsCommon.writeVmOptions(PRODUCT, configDir, jvmMaxHeapSize, jvmMinHeapSize); + } + } + } + + override async modify(pc: ParameterChange, plan: ModifyPlan): Promise { + if (pc.name !== 'jvmMaxHeapSize' && pc.name !== 'jvmMinHeapSize') return; + + const configDir = await JetBrainsCommon.getOrCreateConfigDir(PRODUCT); + if (!configDir) return; + + const { jvmMaxHeapSize, jvmMinHeapSize } = plan.desiredConfig; + + if (jvmMaxHeapSize == null && jvmMinHeapSize == null) { + await JetBrainsCommon.removeVmOptions(PRODUCT, configDir); + } else { + await JetBrainsCommon.writeVmOptions(PRODUCT, configDir, jvmMaxHeapSize, jvmMinHeapSize); + } + } + + override async destroy(plan: DestroyPlan): Promise { + const { jvmMaxHeapSize, jvmMinHeapSize } = plan.currentConfig; + + if (jvmMaxHeapSize != null || jvmMinHeapSize != null) { + const configDir = await JetBrainsCommon.findConfigDir(PRODUCT); + if (configDir) await JetBrainsCommon.removeVmOptions(PRODUCT, configDir); + } + + if (Utils.isMacOS()) { + await JetBrainsCommon.uninstallMacOS(PRODUCT); + } else { + await JetBrainsCommon.uninstallLinux(PRODUCT); + } + } + + private async importSettingsZip(settingsZip: string): Promise { + const $ = getPty(); + + const unzipCheck = await $.spawnSafe('which unzip'); + if (unzipCheck.status !== SpawnStatus.SUCCESS) { + await Utils.installViaPkgMgr('unzip'); + } + + const configDir = await JetBrainsCommon.getOrCreateConfigDir(PRODUCT); + if (!configDir) { + throw new Error('Cannot determine Rider config directory for settings import.'); + } + + await fs.mkdir(configDir, { recursive: true }); + await $.spawn(`unzip -o "${settingsZip}" -d "${configDir}"`, { interactive: true }); + } +} diff --git a/src/resources/jetbrains/rubymine/completions/rubymine.plugins.ts b/src/resources/jetbrains/rubymine/completions/rubymine.plugins.ts new file mode 100644 index 00000000..1d5d18dc --- /dev/null +++ b/src/resources/jetbrains/rubymine/completions/rubymine.plugins.ts @@ -0,0 +1,15 @@ +export default async function loadRubyMinePlugins(): Promise { + const response = await fetch( + 'https://plugins.jetbrains.com/api/plugins?build=RM&orderBy=downloads&offset=0&limit=500', + { headers: { Accept: 'application/json' } } + ); + + if (!response.ok) { + return []; + } + + const data = await response.json() as Array<{ xmlId?: string }>; + return data + .map((p) => p.xmlId) + .filter((id): id is string => typeof id === 'string' && id.length > 0); +} diff --git a/src/resources/jetbrains/rubymine/rubymine.ts b/src/resources/jetbrains/rubymine/rubymine.ts new file mode 100644 index 00000000..b04bf8e7 --- /dev/null +++ b/src/resources/jetbrains/rubymine/rubymine.ts @@ -0,0 +1,203 @@ +import { + CreatePlan, + DestroyPlan, + ExampleConfig, + ModifyPlan, + ParameterChange, + Resource, + ResourceSettings, + SpawnStatus, + Utils, + getPty, + z, +} from '@codifycli/plugin-core'; +import { OS } from '@codifycli/schemas'; +import fs from 'node:fs/promises'; + +import { + JetBrainsCommon, + JetBrainsPluginsParameter, + JetBrainsProductInfo, +} from '../common/jetbrains-common.js'; + +const PRODUCT: JetBrainsProductInfo = { + macAppName: 'RubyMine', + macBinaryName: 'rubymine', + configDirPrefix: 'RubyMine', + caskName: 'rubymine', + snapName: 'rubymine', + linuxCommand: 'rubymine', +}; + +const schema = z + .object({ + settingsZip: z + .string() + .optional() + .describe('Absolute path to a RubyMine settings ZIP file to import on first install.'), + importSettings: z + .boolean() + .optional() + .describe( + 'Whether to import the settings from settingsZip during create. ' + + 'Defaults to true. Set to false to skip the import even when settingsZip is provided.' + ), + plugins: z + .array(z.string()) + .optional() + .describe( + 'JetBrains Marketplace plugin IDs to install ' + + '(e.g. "com.github.copilot", "Docker"). ' + + 'Plugin IDs can be found on the plugin page under Additional Information.' + ), + jvmMaxHeapSize: z + .string() + .optional() + .describe('Maximum JVM heap size for RubyMine, e.g. "2048m" for 2 GB. Defaults to the IDE default (~2 GB).'), + jvmMinHeapSize: z + .string() + .optional() + .describe('Initial JVM heap size for RubyMine, e.g. "512m". Defaults to the IDE default.'), + }) + .meta({ $comment: 'https://codifycli.com/docs/resources/rubymine' }) + .describe('Install and configure JetBrains RubyMine IDE with plugins and JVM settings.'); + +export type RubyMineConfig = z.infer; + +const defaultConfig: Partial = { + plugins: [], +}; + +const exampleBasic: ExampleConfig = { + title: 'RubyMine with GitHub Copilot and Docker plugins', + description: + 'Install RubyMine and add the GitHub Copilot and Docker integration plugins for a modern Ruby on Rails development workflow.', + configs: [ + { + type: 'rubymine', + plugins: ['com.github.copilot', 'Docker'], + }, + ], +}; + +const exampleAdvanced: ExampleConfig = { + title: 'RubyMine with tuned JVM and imported settings', + description: + 'Install RubyMine, import previous settings from a ZIP, and increase the heap to 4 GB for large projects.', + configs: [ + { + type: 'rubymine', + settingsZip: '/path/to/rubymine-settings.zip', + importSettings: true, + jvmMaxHeapSize: '4096m', + jvmMinHeapSize: '1024m', + plugins: ['com.github.copilot', 'Docker'], + }, + ], +}; + +export class RubyMineResource extends Resource { + getSettings(): ResourceSettings { + return { + id: 'rubymine', + operatingSystems: [OS.Darwin, OS.Linux], + schema, + defaultConfig, + exampleConfigs: { + example1: exampleBasic, + example2: exampleAdvanced, + }, + parameterSettings: { + settingsZip: { type: 'string', setting: true }, + importSettings: { type: 'boolean', setting: true }, + plugins: { type: 'stateful', definition: new JetBrainsPluginsParameter(PRODUCT), order: 1 }, + jvmMaxHeapSize: { type: 'string', canModify: true }, + jvmMinHeapSize: { type: 'string', canModify: true }, + }, + }; + } + + override async refresh(parameters: Partial): Promise | null> { + const installed = await JetBrainsCommon.isInstalled(PRODUCT); + if (!installed) return null; + + const result: Partial = {}; + + const configDir = await JetBrainsCommon.findConfigDir(PRODUCT); + if (configDir) { + const vmOptions = await JetBrainsCommon.readVmOptions(PRODUCT, configDir); + if (parameters.jvmMaxHeapSize != null) result.jvmMaxHeapSize = vmOptions.maxHeap; + if (parameters.jvmMinHeapSize != null) result.jvmMinHeapSize = vmOptions.minHeap; + } + + return result; + } + + override async create(plan: CreatePlan): Promise { + if (Utils.isMacOS()) { + await JetBrainsCommon.installMacOS(PRODUCT); + } else { + await JetBrainsCommon.installLinux(PRODUCT); + } + + const { settingsZip, importSettings = true, jvmMaxHeapSize, jvmMinHeapSize } = plan.desiredConfig; + + if (settingsZip && importSettings) { + await this.importSettingsZip(settingsZip); + } + + if (jvmMaxHeapSize != null || jvmMinHeapSize != null) { + const configDir = await JetBrainsCommon.getOrCreateConfigDir(PRODUCT); + if (configDir) { + await JetBrainsCommon.writeVmOptions(PRODUCT, configDir, jvmMaxHeapSize, jvmMinHeapSize); + } + } + } + + override async modify(pc: ParameterChange, plan: ModifyPlan): Promise { + if (pc.name !== 'jvmMaxHeapSize' && pc.name !== 'jvmMinHeapSize') return; + + const configDir = await JetBrainsCommon.getOrCreateConfigDir(PRODUCT); + if (!configDir) return; + + const { jvmMaxHeapSize, jvmMinHeapSize } = plan.desiredConfig; + + if (jvmMaxHeapSize == null && jvmMinHeapSize == null) { + await JetBrainsCommon.removeVmOptions(PRODUCT, configDir); + } else { + await JetBrainsCommon.writeVmOptions(PRODUCT, configDir, jvmMaxHeapSize, jvmMinHeapSize); + } + } + + override async destroy(plan: DestroyPlan): Promise { + const { jvmMaxHeapSize, jvmMinHeapSize } = plan.currentConfig; + + if (jvmMaxHeapSize != null || jvmMinHeapSize != null) { + const configDir = await JetBrainsCommon.findConfigDir(PRODUCT); + if (configDir) await JetBrainsCommon.removeVmOptions(PRODUCT, configDir); + } + + if (Utils.isMacOS()) { + await JetBrainsCommon.uninstallMacOS(PRODUCT); + } else { + await JetBrainsCommon.uninstallLinux(PRODUCT); + } + } + + private async importSettingsZip(settingsZip: string): Promise { + const $ = getPty(); + + const unzipCheck = await $.spawnSafe('which unzip'); + if (unzipCheck.status !== SpawnStatus.SUCCESS) { + await Utils.installViaPkgMgr('unzip'); + } + + const configDir = await JetBrainsCommon.getOrCreateConfigDir(PRODUCT); + if (!configDir) { + throw new Error('Cannot determine RubyMine config directory for settings import.'); + } + + await fs.mkdir(configDir, { recursive: true }); + await $.spawn(`unzip -o "${settingsZip}" -d "${configDir}"`, { interactive: true }); + } +} diff --git a/src/resources/jetbrains/rustrover/completions/rustrover.plugins.ts b/src/resources/jetbrains/rustrover/completions/rustrover.plugins.ts new file mode 100644 index 00000000..01346c0e --- /dev/null +++ b/src/resources/jetbrains/rustrover/completions/rustrover.plugins.ts @@ -0,0 +1,15 @@ +export default async function loadRustRoverPlugins(): Promise { + const response = await fetch( + 'https://plugins.jetbrains.com/api/plugins?build=RR&orderBy=downloads&offset=0&limit=500', + { headers: { Accept: 'application/json' } } + ); + + if (!response.ok) { + return []; + } + + const data = await response.json() as Array<{ xmlId?: string }>; + return data + .map((p) => p.xmlId) + .filter((id): id is string => typeof id === 'string' && id.length > 0); +} diff --git a/src/resources/jetbrains/rustrover/rustrover.ts b/src/resources/jetbrains/rustrover/rustrover.ts new file mode 100644 index 00000000..e240b9eb --- /dev/null +++ b/src/resources/jetbrains/rustrover/rustrover.ts @@ -0,0 +1,203 @@ +import { + CreatePlan, + DestroyPlan, + ExampleConfig, + ModifyPlan, + ParameterChange, + Resource, + ResourceSettings, + SpawnStatus, + Utils, + getPty, + z, +} from '@codifycli/plugin-core'; +import { OS } from '@codifycli/schemas'; +import fs from 'node:fs/promises'; + +import { + JetBrainsCommon, + JetBrainsPluginsParameter, + JetBrainsProductInfo, +} from '../common/jetbrains-common.js'; + +const PRODUCT: JetBrainsProductInfo = { + macAppName: 'RustRover', + macBinaryName: 'rustrover', + configDirPrefix: 'RustRover', + caskName: 'rustrover', + snapName: 'rustrover', + linuxCommand: 'rustrover', +}; + +const schema = z + .object({ + settingsZip: z + .string() + .optional() + .describe('Absolute path to a RustRover settings ZIP file to import on first install.'), + importSettings: z + .boolean() + .optional() + .describe( + 'Whether to import the settings from settingsZip during create. ' + + 'Defaults to true. Set to false to skip the import even when settingsZip is provided.' + ), + plugins: z + .array(z.string()) + .optional() + .describe( + 'JetBrains Marketplace plugin IDs to install ' + + '(e.g. "com.github.copilot", "Docker"). ' + + 'Plugin IDs can be found on the plugin page under Additional Information.' + ), + jvmMaxHeapSize: z + .string() + .optional() + .describe('Maximum JVM heap size for RustRover, e.g. "2048m" for 2 GB. Defaults to the IDE default (~2 GB).'), + jvmMinHeapSize: z + .string() + .optional() + .describe('Initial JVM heap size for RustRover, e.g. "512m". Defaults to the IDE default.'), + }) + .meta({ $comment: 'https://codifycli.com/docs/resources/rustrover' }) + .describe('Install and configure JetBrains RustRover IDE with plugins and JVM settings.'); + +export type RustRoverConfig = z.infer; + +const defaultConfig: Partial = { + plugins: [], +}; + +const exampleBasic: ExampleConfig = { + title: 'RustRover with Copilot and Docker plugins', + description: + 'Install RustRover and add the GitHub Copilot and Docker integration plugins for a modern Rust development workflow.', + configs: [ + { + type: 'rustrover', + plugins: ['com.github.copilot', 'Docker'], + }, + ], +}; + +const exampleAdvanced: ExampleConfig = { + title: 'RustRover with tuned JVM and imported settings', + description: + 'Install RustRover, import previous settings from a ZIP, and increase the heap to 4 GB for large projects.', + configs: [ + { + type: 'rustrover', + settingsZip: '/path/to/rustrover-settings.zip', + importSettings: true, + jvmMaxHeapSize: '4096m', + jvmMinHeapSize: '1024m', + plugins: ['com.github.copilot', 'Docker'], + }, + ], +}; + +export class RustRoverResource extends Resource { + getSettings(): ResourceSettings { + return { + id: 'rustrover', + operatingSystems: [OS.Darwin, OS.Linux], + schema, + defaultConfig, + exampleConfigs: { + example1: exampleBasic, + example2: exampleAdvanced, + }, + parameterSettings: { + settingsZip: { type: 'string', setting: true }, + importSettings: { type: 'boolean', setting: true }, + plugins: { type: 'stateful', definition: new JetBrainsPluginsParameter(PRODUCT), order: 1 }, + jvmMaxHeapSize: { type: 'string', canModify: true }, + jvmMinHeapSize: { type: 'string', canModify: true }, + }, + }; + } + + override async refresh(parameters: Partial): Promise | null> { + const installed = await JetBrainsCommon.isInstalled(PRODUCT); + if (!installed) return null; + + const result: Partial = {}; + + const configDir = await JetBrainsCommon.findConfigDir(PRODUCT); + if (configDir) { + const vmOptions = await JetBrainsCommon.readVmOptions(PRODUCT, configDir); + if (parameters.jvmMaxHeapSize != null) result.jvmMaxHeapSize = vmOptions.maxHeap; + if (parameters.jvmMinHeapSize != null) result.jvmMinHeapSize = vmOptions.minHeap; + } + + return result; + } + + override async create(plan: CreatePlan): Promise { + if (Utils.isMacOS()) { + await JetBrainsCommon.installMacOS(PRODUCT); + } else { + await JetBrainsCommon.installLinux(PRODUCT); + } + + const { settingsZip, importSettings = true, jvmMaxHeapSize, jvmMinHeapSize } = plan.desiredConfig; + + if (settingsZip && importSettings) { + await this.importSettingsZip(settingsZip); + } + + if (jvmMaxHeapSize != null || jvmMinHeapSize != null) { + const configDir = await JetBrainsCommon.getOrCreateConfigDir(PRODUCT); + if (configDir) { + await JetBrainsCommon.writeVmOptions(PRODUCT, configDir, jvmMaxHeapSize, jvmMinHeapSize); + } + } + } + + override async modify(pc: ParameterChange, plan: ModifyPlan): Promise { + if (pc.name !== 'jvmMaxHeapSize' && pc.name !== 'jvmMinHeapSize') return; + + const configDir = await JetBrainsCommon.getOrCreateConfigDir(PRODUCT); + if (!configDir) return; + + const { jvmMaxHeapSize, jvmMinHeapSize } = plan.desiredConfig; + + if (jvmMaxHeapSize == null && jvmMinHeapSize == null) { + await JetBrainsCommon.removeVmOptions(PRODUCT, configDir); + } else { + await JetBrainsCommon.writeVmOptions(PRODUCT, configDir, jvmMaxHeapSize, jvmMinHeapSize); + } + } + + override async destroy(plan: DestroyPlan): Promise { + const { jvmMaxHeapSize, jvmMinHeapSize } = plan.currentConfig; + + if (jvmMaxHeapSize != null || jvmMinHeapSize != null) { + const configDir = await JetBrainsCommon.findConfigDir(PRODUCT); + if (configDir) await JetBrainsCommon.removeVmOptions(PRODUCT, configDir); + } + + if (Utils.isMacOS()) { + await JetBrainsCommon.uninstallMacOS(PRODUCT); + } else { + await JetBrainsCommon.uninstallLinux(PRODUCT); + } + } + + private async importSettingsZip(settingsZip: string): Promise { + const $ = getPty(); + + const unzipCheck = await $.spawnSafe('which unzip'); + if (unzipCheck.status !== SpawnStatus.SUCCESS) { + await Utils.installViaPkgMgr('unzip'); + } + + const configDir = await JetBrainsCommon.getOrCreateConfigDir(PRODUCT); + if (!configDir) { + throw new Error('Cannot determine RustRover config directory for settings import.'); + } + + await fs.mkdir(configDir, { recursive: true }); + await $.spawn(`unzip -o "${settingsZip}" -d "${configDir}"`, { interactive: true }); + } +} diff --git a/src/resources/openclaw/openclaw.ts b/src/resources/openclaw/openclaw.ts index 6aa08fbe..405a3aa7 100644 --- a/src/resources/openclaw/openclaw.ts +++ b/src/resources/openclaw/openclaw.ts @@ -249,7 +249,7 @@ const agentsSchema = z })).optional() .describe('Named agent definitions, each with its own identity, model, and capabilities.'), }) - .describe('Agent defaults and named agent list.'); + .describe('Agent defaults and named agent list. Named agents go in agents.list (array with id field) — there is no agents.workers key.'); const browserSchema = z .looseObject({ diff --git a/test/jetbrains/clion/clion.test.ts b/test/jetbrains/clion/clion.test.ts new file mode 100644 index 00000000..423bc91b --- /dev/null +++ b/test/jetbrains/clion/clion.test.ts @@ -0,0 +1,126 @@ +import { Utils } from '@codifycli/plugin-core'; +import { PluginTester, testSpawn } from '@codifycli/plugin-test'; +import { expect, describe, it, beforeAll, afterAll } from 'vitest'; +import fs from 'node:fs/promises'; +import os from 'node:os'; +import path from 'node:path'; + +describe('CLion integration tests', async () => { + const pluginPath = path.resolve('./src/index.ts'); + + let xdgLine: string | null = null; + + beforeAll(async () => { + if (!Utils.isLinux()) return; + + // Wait for unattended-upgrades to release the dpkg lock before running any apt installs. + await testSpawn('systemctl stop unattended-upgrades || true', { requiresRoot: true }); + await testSpawn('flock /var/lib/dpkg/lock-frontend true', { requiresRoot: true }); + + const uid = process.getuid!(); + const xdgDir = `/tmp/xdg-runtime-${uid}`; + await fs.mkdir(xdgDir, { recursive: true }); + await fs.chmod(xdgDir, 0o700); + process.env.XDG_RUNTIME_DIR = xdgDir; + + const bashrc = path.join(os.homedir(), '.bashrc'); + const line = `export XDG_RUNTIME_DIR=${xdgDir}`; + const contents = await fs.readFile(bashrc, 'utf8').catch(() => ''); + if (!contents.includes(line)) { + await fs.appendFile(bashrc, `\n${line}\n`); + xdgLine = line; + } + }); + + afterAll(async () => { + if (!xdgLine) return; + + const bashrc = path.join(os.homedir(), '.bashrc'); + const contents = await fs.readFile(bashrc, 'utf8').catch(() => ''); + await fs.writeFile(bashrc, contents.replace(`\n${xdgLine}\n`, '')); + }); + + it('Can install CLion', { timeout: 600_000 }, async () => { + await PluginTester.fullTest(pluginPath, [{ type: 'clion' }], { + validateApply: async () => { + if (Utils.isMacOS()) { + const stat = await fs.lstat('/Applications/CLion.app'); + expect(stat.isDirectory()).to.be.true; + } else { + const { data } = await testSpawn('which clion'); + expect(data?.trim()).to.include('clion'); + } + }, + validateDestroy: async () => { + if (Utils.isMacOS()) { + const exists = await fs.access('/Applications/CLion.app').then(() => true).catch(() => false); + expect(exists).to.be.false; + } else { + const { data } = await testSpawn('which clion'); + expect(data?.trim() ?? '').not.to.include('clion'); + } + }, + }); + }); + + it('Can manage JVM heap size', { timeout: 600_000 }, async () => { + const configParent = Utils.isMacOS() + ? path.join(os.homedir(), 'Library', 'Application Support', 'JetBrains') + : path.join(os.homedir(), '.config', 'JetBrains'); + + const findVmOptions = async (): Promise => { + try { + const entries = await fs.readdir(configParent); + const dir = entries.filter((e) => e.startsWith('CLion')).sort().pop(); + if (!dir) return null; + return path.join(configParent, dir, 'clion.vmoptions'); + } catch { + return null; + } + }; + + await PluginTester.fullTest(pluginPath, [{ + type: 'clion', + jvmMaxHeapSize: '2048m', + jvmMinHeapSize: '512m', + }], { + validateApply: async () => { + const vmOptionsPath = await findVmOptions(); + expect(vmOptionsPath).to.not.be.null; + const { data } = await testSpawn(`cat "${vmOptionsPath}"`); + expect(data).to.include('-Xmx2048m'); + expect(data).to.include('-Xms512m'); + }, + testModify: { + modifiedConfigs: [{ + type: 'clion', + jvmMaxHeapSize: '4096m', + jvmMinHeapSize: '1024m', + }], + validateModify: async () => { + const vmOptionsPath = await findVmOptions(); + expect(vmOptionsPath).to.not.be.null; + const { data } = await testSpawn(`cat "${vmOptionsPath}"`); + expect(data).to.include('-Xmx4096m'); + expect(data).to.include('-Xms1024m'); + }, + }, + validateDestroy: async () => { + const vmOptionsPath = await findVmOptions(); + if (!vmOptionsPath) return; + try { + const content = await fs.readFile(vmOptionsPath, 'utf8'); + expect(content).not.to.include('-Xmx'); + expect(content).not.to.include('-Xms'); + } catch { /* file removed, that's fine */ } + }, + }); + }); + + it('Can install plugins', { timeout: 600_000 }, async () => { + await PluginTester.fullTest(pluginPath, [{ + type: 'clion', + plugins: ['Docker'], + }]) + }) +}); diff --git a/test/jetbrains/goland/goland.test.ts b/test/jetbrains/goland/goland.test.ts new file mode 100644 index 00000000..d67e50ab --- /dev/null +++ b/test/jetbrains/goland/goland.test.ts @@ -0,0 +1,126 @@ +import { Utils } from '@codifycli/plugin-core'; +import { PluginTester, testSpawn } from '@codifycli/plugin-test'; +import { expect, describe, it, beforeAll, afterAll } from 'vitest'; +import fs from 'node:fs/promises'; +import os from 'node:os'; +import path from 'node:path'; + +describe('GoLand integration tests', async () => { + const pluginPath = path.resolve('./src/index.ts'); + + let xdgLine: string | null = null; + + beforeAll(async () => { + if (!Utils.isLinux()) return; + + // Wait for unattended-upgrades to release the dpkg lock before running any apt installs. + await testSpawn('systemctl stop unattended-upgrades || true', { requiresRoot: true }); + await testSpawn('flock /var/lib/dpkg/lock-frontend true', { requiresRoot: true }); + + const uid = process.getuid!(); + const xdgDir = `/tmp/xdg-runtime-${uid}`; + await fs.mkdir(xdgDir, { recursive: true }); + await fs.chmod(xdgDir, 0o700); + process.env.XDG_RUNTIME_DIR = xdgDir; + + const bashrc = path.join(os.homedir(), '.bashrc'); + const line = `export XDG_RUNTIME_DIR=${xdgDir}`; + const contents = await fs.readFile(bashrc, 'utf8').catch(() => ''); + if (!contents.includes(line)) { + await fs.appendFile(bashrc, `\n${line}\n`); + xdgLine = line; + } + }); + + afterAll(async () => { + if (!xdgLine) return; + + const bashrc = path.join(os.homedir(), '.bashrc'); + const contents = await fs.readFile(bashrc, 'utf8').catch(() => ''); + await fs.writeFile(bashrc, contents.replace(`\n${xdgLine}\n`, '')); + }); + + it('Can install GoLand', { timeout: 600_000 }, async () => { + await PluginTester.fullTest(pluginPath, [{ type: 'goland' }], { + validateApply: async () => { + if (Utils.isMacOS()) { + const stat = await fs.lstat('/Applications/GoLand.app'); + expect(stat.isDirectory()).to.be.true; + } else { + const { data } = await testSpawn('which goland'); + expect(data?.trim()).to.include('goland'); + } + }, + validateDestroy: async () => { + if (Utils.isMacOS()) { + const exists = await fs.access('/Applications/GoLand.app').then(() => true).catch(() => false); + expect(exists).to.be.false; + } else { + const { data } = await testSpawn('which goland'); + expect(data?.trim() ?? '').not.to.include('goland'); + } + }, + }); + }); + + it('Can manage JVM heap size', { timeout: 600_000 }, async () => { + const configParent = Utils.isMacOS() + ? path.join(os.homedir(), 'Library', 'Application Support', 'JetBrains') + : path.join(os.homedir(), '.config', 'JetBrains'); + + const findVmOptions = async (): Promise => { + try { + const entries = await fs.readdir(configParent); + const dir = entries.filter((e) => e.startsWith('GoLand')).sort().pop(); + if (!dir) return null; + return path.join(configParent, dir, 'goland.vmoptions'); + } catch { + return null; + } + }; + + await PluginTester.fullTest(pluginPath, [{ + type: 'goland', + jvmMaxHeapSize: '2048m', + jvmMinHeapSize: '512m', + }], { + validateApply: async () => { + const vmOptionsPath = await findVmOptions(); + expect(vmOptionsPath).to.not.be.null; + const { data } = await testSpawn(`cat "${vmOptionsPath}"`); + expect(data).to.include('-Xmx2048m'); + expect(data).to.include('-Xms512m'); + }, + testModify: { + modifiedConfigs: [{ + type: 'goland', + jvmMaxHeapSize: '4096m', + jvmMinHeapSize: '1024m', + }], + validateModify: async () => { + const vmOptionsPath = await findVmOptions(); + expect(vmOptionsPath).to.not.be.null; + const { data } = await testSpawn(`cat "${vmOptionsPath}"`); + expect(data).to.include('-Xmx4096m'); + expect(data).to.include('-Xms1024m'); + }, + }, + validateDestroy: async () => { + const vmOptionsPath = await findVmOptions(); + if (!vmOptionsPath) return; + try { + const content = await fs.readFile(vmOptionsPath, 'utf8'); + expect(content).not.to.include('-Xmx'); + expect(content).not.to.include('-Xms'); + } catch { /* file removed, that's fine */ } + }, + }); + }); + + it('Can install plugins', { timeout: 600_000 }, async () => { + await PluginTester.fullTest(pluginPath, [{ + type: 'goland', + plugins: ['Docker'], + }]) + }) +}); diff --git a/test/jetbrains/intellij-idea/intellij-idea.test.ts b/test/jetbrains/intellij-idea/intellij-idea.test.ts new file mode 100644 index 00000000..662d6aab --- /dev/null +++ b/test/jetbrains/intellij-idea/intellij-idea.test.ts @@ -0,0 +1,126 @@ +import { Utils } from '@codifycli/plugin-core'; +import { PluginTester, testSpawn } from '@codifycli/plugin-test'; +import { expect, describe, it, beforeAll, afterAll } from 'vitest'; +import fs from 'node:fs/promises'; +import os from 'node:os'; +import path from 'node:path'; + +describe('IntelliJ IDEA integration tests', async () => { + const pluginPath = path.resolve('./src/index.ts'); + + let xdgLine: string | null = null; + + beforeAll(async () => { + if (!Utils.isLinux()) return; + + // Wait for unattended-upgrades to release the dpkg lock before running any apt installs. + await testSpawn('systemctl stop unattended-upgrades || true', { requiresRoot: true }); + await testSpawn('flock /var/lib/dpkg/lock-frontend true', { requiresRoot: true }); + + const uid = process.getuid!(); + const xdgDir = `/tmp/xdg-runtime-${uid}`; + await fs.mkdir(xdgDir, { recursive: true }); + await fs.chmod(xdgDir, 0o700); + process.env.XDG_RUNTIME_DIR = xdgDir; + + const bashrc = path.join(os.homedir(), '.bashrc'); + const line = `export XDG_RUNTIME_DIR=${xdgDir}`; + const contents = await fs.readFile(bashrc, 'utf8').catch(() => ''); + if (!contents.includes(line)) { + await fs.appendFile(bashrc, `\n${line}\n`); + xdgLine = line; + } + }); + + afterAll(async () => { + if (!xdgLine) return; + + const bashrc = path.join(os.homedir(), '.bashrc'); + const contents = await fs.readFile(bashrc, 'utf8').catch(() => ''); + await fs.writeFile(bashrc, contents.replace(`\n${xdgLine}\n`, '')); + }); + + it('Can install IntelliJ IDEA', { timeout: 600_000 }, async () => { + await PluginTester.fullTest(pluginPath, [{ type: 'intellij-idea' }], { + validateApply: async () => { + if (Utils.isMacOS()) { + const stat = await fs.lstat('/Applications/IntelliJ IDEA.app'); + expect(stat.isDirectory()).to.be.true; + } else { + const { data } = await testSpawn('which intellij-idea-community'); + expect(data?.trim()).to.include('intellij-idea-community'); + } + }, + validateDestroy: async () => { + if (Utils.isMacOS()) { + const exists = await fs.access('/Applications/IntelliJ IDEA.app').then(() => true).catch(() => false); + expect(exists).to.be.false; + } else { + const { data } = await testSpawn('which intellij-idea-community'); + expect(data?.trim() ?? '').not.to.include('intellij-idea-community'); + } + }, + }); + }); + + it('Can manage JVM heap size', { timeout: 600_000 }, async () => { + const configParent = Utils.isMacOS() + ? path.join(os.homedir(), 'Library', 'Application Support', 'JetBrains') + : path.join(os.homedir(), '.config', 'JetBrains'); + + const findVmOptions = async (): Promise => { + try { + const entries = await fs.readdir(configParent); + const dir = entries.filter((e) => e.startsWith('IntelliJIdea')).sort().pop(); + if (!dir) return null; + return path.join(configParent, dir, 'idea.vmoptions'); + } catch { + return null; + } + }; + + await PluginTester.fullTest(pluginPath, [{ + type: 'intellij-idea', + jvmMaxHeapSize: '2048m', + jvmMinHeapSize: '512m', + }], { + validateApply: async () => { + const vmOptionsPath = await findVmOptions(); + expect(vmOptionsPath).to.not.be.null; + const { data } = await testSpawn(`cat "${vmOptionsPath}"`); + expect(data).to.include('-Xmx2048m'); + expect(data).to.include('-Xms512m'); + }, + testModify: { + modifiedConfigs: [{ + type: 'intellij-idea', + jvmMaxHeapSize: '4096m', + jvmMinHeapSize: '1024m', + }], + validateModify: async () => { + const vmOptionsPath = await findVmOptions(); + expect(vmOptionsPath).to.not.be.null; + const { data } = await testSpawn(`cat "${vmOptionsPath}"`); + expect(data).to.include('-Xmx4096m'); + expect(data).to.include('-Xms1024m'); + }, + }, + validateDestroy: async () => { + const vmOptionsPath = await findVmOptions(); + if (!vmOptionsPath) return; + try { + const content = await fs.readFile(vmOptionsPath, 'utf8'); + expect(content).not.to.include('-Xmx'); + expect(content).not.to.include('-Xms'); + } catch { /* file removed, that's fine */ } + }, + }); + }); + + it('Can install plugins', { timeout: 600_000 }, async () => { + await PluginTester.fullTest(pluginPath, [{ + type: 'intellij-idea', + plugins: ['Docker'], + }]) + }) +}); diff --git a/test/jetbrains/phpstorm/phpstorm.test.ts b/test/jetbrains/phpstorm/phpstorm.test.ts new file mode 100644 index 00000000..c7b30e2a --- /dev/null +++ b/test/jetbrains/phpstorm/phpstorm.test.ts @@ -0,0 +1,126 @@ +import { Utils } from '@codifycli/plugin-core'; +import { PluginTester, testSpawn } from '@codifycli/plugin-test'; +import { expect, describe, it, beforeAll, afterAll } from 'vitest'; +import fs from 'node:fs/promises'; +import os from 'node:os'; +import path from 'node:path'; + +describe('PhpStorm integration tests', async () => { + const pluginPath = path.resolve('./src/index.ts'); + + let xdgLine: string | null = null; + + beforeAll(async () => { + if (!Utils.isLinux()) return; + + // Wait for unattended-upgrades to release the dpkg lock before running any apt installs. + await testSpawn('systemctl stop unattended-upgrades || true', { requiresRoot: true }); + await testSpawn('flock /var/lib/dpkg/lock-frontend true', { requiresRoot: true }); + + const uid = process.getuid!(); + const xdgDir = `/tmp/xdg-runtime-${uid}`; + await fs.mkdir(xdgDir, { recursive: true }); + await fs.chmod(xdgDir, 0o700); + process.env.XDG_RUNTIME_DIR = xdgDir; + + const bashrc = path.join(os.homedir(), '.bashrc'); + const line = `export XDG_RUNTIME_DIR=${xdgDir}`; + const contents = await fs.readFile(bashrc, 'utf8').catch(() => ''); + if (!contents.includes(line)) { + await fs.appendFile(bashrc, `\n${line}\n`); + xdgLine = line; + } + }); + + afterAll(async () => { + if (!xdgLine) return; + + const bashrc = path.join(os.homedir(), '.bashrc'); + const contents = await fs.readFile(bashrc, 'utf8').catch(() => ''); + await fs.writeFile(bashrc, contents.replace(`\n${xdgLine}\n`, '')); + }); + + it('Can install PhpStorm', { timeout: 600_000 }, async () => { + await PluginTester.fullTest(pluginPath, [{ type: 'phpstorm' }], { + validateApply: async () => { + if (Utils.isMacOS()) { + const stat = await fs.lstat('/Applications/PhpStorm.app'); + expect(stat.isDirectory()).to.be.true; + } else { + const { data } = await testSpawn('which phpstorm'); + expect(data?.trim()).to.include('phpstorm'); + } + }, + validateDestroy: async () => { + if (Utils.isMacOS()) { + const exists = await fs.access('/Applications/PhpStorm.app').then(() => true).catch(() => false); + expect(exists).to.be.false; + } else { + const { data } = await testSpawn('which phpstorm'); + expect(data?.trim() ?? '').not.to.include('phpstorm'); + } + }, + }); + }); + + it('Can manage JVM heap size', { timeout: 600_000 }, async () => { + const configParent = Utils.isMacOS() + ? path.join(os.homedir(), 'Library', 'Application Support', 'JetBrains') + : path.join(os.homedir(), '.config', 'JetBrains'); + + const findVmOptions = async (): Promise => { + try { + const entries = await fs.readdir(configParent); + const dir = entries.filter((e) => e.startsWith('PhpStorm')).sort().pop(); + if (!dir) return null; + return path.join(configParent, dir, 'phpstorm.vmoptions'); + } catch { + return null; + } + }; + + await PluginTester.fullTest(pluginPath, [{ + type: 'phpstorm', + jvmMaxHeapSize: '2048m', + jvmMinHeapSize: '512m', + }], { + validateApply: async () => { + const vmOptionsPath = await findVmOptions(); + expect(vmOptionsPath).to.not.be.null; + const { data } = await testSpawn(`cat "${vmOptionsPath}"`); + expect(data).to.include('-Xmx2048m'); + expect(data).to.include('-Xms512m'); + }, + testModify: { + modifiedConfigs: [{ + type: 'phpstorm', + jvmMaxHeapSize: '4096m', + jvmMinHeapSize: '1024m', + }], + validateModify: async () => { + const vmOptionsPath = await findVmOptions(); + expect(vmOptionsPath).to.not.be.null; + const { data } = await testSpawn(`cat "${vmOptionsPath}"`); + expect(data).to.include('-Xmx4096m'); + expect(data).to.include('-Xms1024m'); + }, + }, + validateDestroy: async () => { + const vmOptionsPath = await findVmOptions(); + if (!vmOptionsPath) return; + try { + const content = await fs.readFile(vmOptionsPath, 'utf8'); + expect(content).not.to.include('-Xmx'); + expect(content).not.to.include('-Xms'); + } catch { /* file removed, that's fine */ } + }, + }); + }); + + it('Can install plugins', { timeout: 600_000 }, async () => { + await PluginTester.fullTest(pluginPath, [{ + type: 'phpstorm', + plugins: ['Docker'], + }]) + }) +}); diff --git a/test/jetbrains/pycharm/pycharm.test.ts b/test/jetbrains/pycharm/pycharm.test.ts new file mode 100644 index 00000000..075b0228 --- /dev/null +++ b/test/jetbrains/pycharm/pycharm.test.ts @@ -0,0 +1,126 @@ +import { Utils } from '@codifycli/plugin-core'; +import { PluginTester, testSpawn } from '@codifycli/plugin-test'; +import { expect, describe, it, beforeAll, afterAll } from 'vitest'; +import fs from 'node:fs/promises'; +import os from 'node:os'; +import path from 'node:path'; + +describe('PyCharm integration tests', async () => { + const pluginPath = path.resolve('./src/index.ts'); + + let xdgLine: string | null = null; + + beforeAll(async () => { + if (!Utils.isLinux()) return; + + // Wait for unattended-upgrades to release the dpkg lock before running any apt installs. + await testSpawn('systemctl stop unattended-upgrades || true', { requiresRoot: true }); + await testSpawn('flock /var/lib/dpkg/lock-frontend true', { requiresRoot: true }); + + const uid = process.getuid!(); + const xdgDir = `/tmp/xdg-runtime-${uid}`; + await fs.mkdir(xdgDir, { recursive: true }); + await fs.chmod(xdgDir, 0o700); + process.env.XDG_RUNTIME_DIR = xdgDir; + + const bashrc = path.join(os.homedir(), '.bashrc'); + const line = `export XDG_RUNTIME_DIR=${xdgDir}`; + const contents = await fs.readFile(bashrc, 'utf8').catch(() => ''); + if (!contents.includes(line)) { + await fs.appendFile(bashrc, `\n${line}\n`); + xdgLine = line; + } + }); + + afterAll(async () => { + if (!xdgLine) return; + + const bashrc = path.join(os.homedir(), '.bashrc'); + const contents = await fs.readFile(bashrc, 'utf8').catch(() => ''); + await fs.writeFile(bashrc, contents.replace(`\n${xdgLine}\n`, '')); + }); + + it('Can install PyCharm', { timeout: 600_000 }, async () => { + await PluginTester.fullTest(pluginPath, [{ type: 'pycharm' }], { + validateApply: async () => { + if (Utils.isMacOS()) { + const stat = await fs.lstat('/Applications/PyCharm.app'); + expect(stat.isDirectory()).to.be.true; + } else { + const { data } = await testSpawn('which pycharm-community'); + expect(data?.trim()).to.include('pycharm-community'); + } + }, + validateDestroy: async () => { + if (Utils.isMacOS()) { + const exists = await fs.access('/Applications/PyCharm.app').then(() => true).catch(() => false); + expect(exists).to.be.false; + } else { + const { data } = await testSpawn('which pycharm-community'); + expect(data?.trim() ?? '').not.to.include('pycharm-community'); + } + }, + }); + }); + + it('Can manage JVM heap size', { timeout: 600_000 }, async () => { + const configParent = Utils.isMacOS() + ? path.join(os.homedir(), 'Library', 'Application Support', 'JetBrains') + : path.join(os.homedir(), '.config', 'JetBrains'); + + const findVmOptions = async (): Promise => { + try { + const entries = await fs.readdir(configParent); + const dir = entries.filter((e) => e.startsWith('PyCharm')).sort().pop(); + if (!dir) return null; + return path.join(configParent, dir, 'pycharm.vmoptions'); + } catch { + return null; + } + }; + + await PluginTester.fullTest(pluginPath, [{ + type: 'pycharm', + jvmMaxHeapSize: '2048m', + jvmMinHeapSize: '512m', + }], { + validateApply: async () => { + const vmOptionsPath = await findVmOptions(); + expect(vmOptionsPath).to.not.be.null; + const { data } = await testSpawn(`cat "${vmOptionsPath}"`); + expect(data).to.include('-Xmx2048m'); + expect(data).to.include('-Xms512m'); + }, + testModify: { + modifiedConfigs: [{ + type: 'pycharm', + jvmMaxHeapSize: '4096m', + jvmMinHeapSize: '1024m', + }], + validateModify: async () => { + const vmOptionsPath = await findVmOptions(); + expect(vmOptionsPath).to.not.be.null; + const { data } = await testSpawn(`cat "${vmOptionsPath}"`); + expect(data).to.include('-Xmx4096m'); + expect(data).to.include('-Xms1024m'); + }, + }, + validateDestroy: async () => { + const vmOptionsPath = await findVmOptions(); + if (!vmOptionsPath) return; + try { + const content = await fs.readFile(vmOptionsPath, 'utf8'); + expect(content).not.to.include('-Xmx'); + expect(content).not.to.include('-Xms'); + } catch { /* file removed, that's fine */ } + }, + }); + }); + + it('Can install plugins', { timeout: 600_000 }, async () => { + await PluginTester.fullTest(pluginPath, [{ + type: 'pycharm', + plugins: ['Docker'], + }]) + }) +}); diff --git a/test/jetbrains/rider/rider.test.ts b/test/jetbrains/rider/rider.test.ts new file mode 100644 index 00000000..1ebdcb6f --- /dev/null +++ b/test/jetbrains/rider/rider.test.ts @@ -0,0 +1,126 @@ +import { Utils } from '@codifycli/plugin-core'; +import { PluginTester, testSpawn } from '@codifycli/plugin-test'; +import { expect, describe, it, beforeAll, afterAll } from 'vitest'; +import fs from 'node:fs/promises'; +import os from 'node:os'; +import path from 'node:path'; + +describe('Rider integration tests', async () => { + const pluginPath = path.resolve('./src/index.ts'); + + let xdgLine: string | null = null; + + beforeAll(async () => { + if (!Utils.isLinux()) return; + + // Wait for unattended-upgrades to release the dpkg lock before running any apt installs. + await testSpawn('systemctl stop unattended-upgrades || true', { requiresRoot: true }); + await testSpawn('flock /var/lib/dpkg/lock-frontend true', { requiresRoot: true }); + + const uid = process.getuid!(); + const xdgDir = `/tmp/xdg-runtime-${uid}`; + await fs.mkdir(xdgDir, { recursive: true }); + await fs.chmod(xdgDir, 0o700); + process.env.XDG_RUNTIME_DIR = xdgDir; + + const bashrc = path.join(os.homedir(), '.bashrc'); + const line = `export XDG_RUNTIME_DIR=${xdgDir}`; + const contents = await fs.readFile(bashrc, 'utf8').catch(() => ''); + if (!contents.includes(line)) { + await fs.appendFile(bashrc, `\n${line}\n`); + xdgLine = line; + } + }); + + afterAll(async () => { + if (!xdgLine) return; + + const bashrc = path.join(os.homedir(), '.bashrc'); + const contents = await fs.readFile(bashrc, 'utf8').catch(() => ''); + await fs.writeFile(bashrc, contents.replace(`\n${xdgLine}\n`, '')); + }); + + it('Can install Rider', { timeout: 600_000 }, async () => { + await PluginTester.fullTest(pluginPath, [{ type: 'rider' }], { + validateApply: async () => { + if (Utils.isMacOS()) { + const stat = await fs.lstat('/Applications/Rider.app'); + expect(stat.isDirectory()).to.be.true; + } else { + const { data } = await testSpawn('which rider'); + expect(data?.trim()).to.include('rider'); + } + }, + validateDestroy: async () => { + if (Utils.isMacOS()) { + const exists = await fs.access('/Applications/Rider.app').then(() => true).catch(() => false); + expect(exists).to.be.false; + } else { + const { data } = await testSpawn('which rider'); + expect(data?.trim() ?? '').not.to.include('rider'); + } + }, + }); + }); + + it('Can manage JVM heap size', { timeout: 600_000 }, async () => { + const configParent = Utils.isMacOS() + ? path.join(os.homedir(), 'Library', 'Application Support', 'JetBrains') + : path.join(os.homedir(), '.config', 'JetBrains'); + + const findVmOptions = async (): Promise => { + try { + const entries = await fs.readdir(configParent); + const dir = entries.filter((e) => e.startsWith('Rider')).sort().pop(); + if (!dir) return null; + return path.join(configParent, dir, 'rider.vmoptions'); + } catch { + return null; + } + }; + + await PluginTester.fullTest(pluginPath, [{ + type: 'rider', + jvmMaxHeapSize: '2048m', + jvmMinHeapSize: '512m', + }], { + validateApply: async () => { + const vmOptionsPath = await findVmOptions(); + expect(vmOptionsPath).to.not.be.null; + const { data } = await testSpawn(`cat "${vmOptionsPath}"`); + expect(data).to.include('-Xmx2048m'); + expect(data).to.include('-Xms512m'); + }, + testModify: { + modifiedConfigs: [{ + type: 'rider', + jvmMaxHeapSize: '4096m', + jvmMinHeapSize: '1024m', + }], + validateModify: async () => { + const vmOptionsPath = await findVmOptions(); + expect(vmOptionsPath).to.not.be.null; + const { data } = await testSpawn(`cat "${vmOptionsPath}"`); + expect(data).to.include('-Xmx4096m'); + expect(data).to.include('-Xms1024m'); + }, + }, + validateDestroy: async () => { + const vmOptionsPath = await findVmOptions(); + if (!vmOptionsPath) return; + try { + const content = await fs.readFile(vmOptionsPath, 'utf8'); + expect(content).not.to.include('-Xmx'); + expect(content).not.to.include('-Xms'); + } catch { /* file removed, that's fine */ } + }, + }); + }); + + it('Can install plugins', { timeout: 600_000 }, async () => { + await PluginTester.fullTest(pluginPath, [{ + type: 'rider', + plugins: ['Docker'], + }]) + }) +}); diff --git a/test/jetbrains/rubymine/rubymine.test.ts b/test/jetbrains/rubymine/rubymine.test.ts new file mode 100644 index 00000000..5c519479 --- /dev/null +++ b/test/jetbrains/rubymine/rubymine.test.ts @@ -0,0 +1,126 @@ +import { Utils } from '@codifycli/plugin-core'; +import { PluginTester, testSpawn } from '@codifycli/plugin-test'; +import { expect, describe, it, beforeAll, afterAll } from 'vitest'; +import fs from 'node:fs/promises'; +import os from 'node:os'; +import path from 'node:path'; + +describe('RubyMine integration tests', async () => { + const pluginPath = path.resolve('./src/index.ts'); + + let xdgLine: string | null = null; + + beforeAll(async () => { + if (!Utils.isLinux()) return; + + // Wait for unattended-upgrades to release the dpkg lock before running any apt installs. + await testSpawn('systemctl stop unattended-upgrades || true', { requiresRoot: true }); + await testSpawn('flock /var/lib/dpkg/lock-frontend true', { requiresRoot: true }); + + const uid = process.getuid!(); + const xdgDir = `/tmp/xdg-runtime-${uid}`; + await fs.mkdir(xdgDir, { recursive: true }); + await fs.chmod(xdgDir, 0o700); + process.env.XDG_RUNTIME_DIR = xdgDir; + + const bashrc = path.join(os.homedir(), '.bashrc'); + const line = `export XDG_RUNTIME_DIR=${xdgDir}`; + const contents = await fs.readFile(bashrc, 'utf8').catch(() => ''); + if (!contents.includes(line)) { + await fs.appendFile(bashrc, `\n${line}\n`); + xdgLine = line; + } + }); + + afterAll(async () => { + if (!xdgLine) return; + + const bashrc = path.join(os.homedir(), '.bashrc'); + const contents = await fs.readFile(bashrc, 'utf8').catch(() => ''); + await fs.writeFile(bashrc, contents.replace(`\n${xdgLine}\n`, '')); + }); + + it('Can install RubyMine', { timeout: 600_000 }, async () => { + await PluginTester.fullTest(pluginPath, [{ type: 'rubymine' }], { + validateApply: async () => { + if (Utils.isMacOS()) { + const stat = await fs.lstat('/Applications/RubyMine.app'); + expect(stat.isDirectory()).to.be.true; + } else { + const { data } = await testSpawn('which rubymine'); + expect(data?.trim()).to.include('rubymine'); + } + }, + validateDestroy: async () => { + if (Utils.isMacOS()) { + const exists = await fs.access('/Applications/RubyMine.app').then(() => true).catch(() => false); + expect(exists).to.be.false; + } else { + const { data } = await testSpawn('which rubymine'); + expect(data?.trim() ?? '').not.to.include('rubymine'); + } + }, + }); + }); + + it('Can manage JVM heap size', { timeout: 600_000 }, async () => { + const configParent = Utils.isMacOS() + ? path.join(os.homedir(), 'Library', 'Application Support', 'JetBrains') + : path.join(os.homedir(), '.config', 'JetBrains'); + + const findVmOptions = async (): Promise => { + try { + const entries = await fs.readdir(configParent); + const dir = entries.filter((e) => e.startsWith('RubyMine')).sort().pop(); + if (!dir) return null; + return path.join(configParent, dir, 'rubymine.vmoptions'); + } catch { + return null; + } + }; + + await PluginTester.fullTest(pluginPath, [{ + type: 'rubymine', + jvmMaxHeapSize: '2048m', + jvmMinHeapSize: '512m', + }], { + validateApply: async () => { + const vmOptionsPath = await findVmOptions(); + expect(vmOptionsPath).to.not.be.null; + const { data } = await testSpawn(`cat "${vmOptionsPath}"`); + expect(data).to.include('-Xmx2048m'); + expect(data).to.include('-Xms512m'); + }, + testModify: { + modifiedConfigs: [{ + type: 'rubymine', + jvmMaxHeapSize: '4096m', + jvmMinHeapSize: '1024m', + }], + validateModify: async () => { + const vmOptionsPath = await findVmOptions(); + expect(vmOptionsPath).to.not.be.null; + const { data } = await testSpawn(`cat "${vmOptionsPath}"`); + expect(data).to.include('-Xmx4096m'); + expect(data).to.include('-Xms1024m'); + }, + }, + validateDestroy: async () => { + const vmOptionsPath = await findVmOptions(); + if (!vmOptionsPath) return; + try { + const content = await fs.readFile(vmOptionsPath, 'utf8'); + expect(content).not.to.include('-Xmx'); + expect(content).not.to.include('-Xms'); + } catch { /* file removed, that's fine */ } + }, + }); + }); + + it('Can install plugins', { timeout: 600_000 }, async () => { + await PluginTester.fullTest(pluginPath, [{ + type: 'rubymine', + plugins: ['Docker'], + }]) + }) +}); diff --git a/test/jetbrains/rustrover/rustrover.test.ts b/test/jetbrains/rustrover/rustrover.test.ts new file mode 100644 index 00000000..a4b3733b --- /dev/null +++ b/test/jetbrains/rustrover/rustrover.test.ts @@ -0,0 +1,126 @@ +import { Utils } from '@codifycli/plugin-core'; +import { PluginTester, testSpawn } from '@codifycli/plugin-test'; +import { expect, describe, it, beforeAll, afterAll } from 'vitest'; +import fs from 'node:fs/promises'; +import os from 'node:os'; +import path from 'node:path'; + +describe('RustRover integration tests', async () => { + const pluginPath = path.resolve('./src/index.ts'); + + let xdgLine: string | null = null; + + beforeAll(async () => { + if (!Utils.isLinux()) return; + + // Wait for unattended-upgrades to release the dpkg lock before running any apt installs. + await testSpawn('systemctl stop unattended-upgrades || true', { requiresRoot: true }); + await testSpawn('flock /var/lib/dpkg/lock-frontend true', { requiresRoot: true }); + + const uid = process.getuid!(); + const xdgDir = `/tmp/xdg-runtime-${uid}`; + await fs.mkdir(xdgDir, { recursive: true }); + await fs.chmod(xdgDir, 0o700); + process.env.XDG_RUNTIME_DIR = xdgDir; + + const bashrc = path.join(os.homedir(), '.bashrc'); + const line = `export XDG_RUNTIME_DIR=${xdgDir}`; + const contents = await fs.readFile(bashrc, 'utf8').catch(() => ''); + if (!contents.includes(line)) { + await fs.appendFile(bashrc, `\n${line}\n`); + xdgLine = line; + } + }); + + afterAll(async () => { + if (!xdgLine) return; + + const bashrc = path.join(os.homedir(), '.bashrc'); + const contents = await fs.readFile(bashrc, 'utf8').catch(() => ''); + await fs.writeFile(bashrc, contents.replace(`\n${xdgLine}\n`, '')); + }); + + it('Can install RustRover', { timeout: 600_000 }, async () => { + await PluginTester.fullTest(pluginPath, [{ type: 'rustrover' }], { + validateApply: async () => { + if (Utils.isMacOS()) { + const stat = await fs.lstat('/Applications/RustRover.app'); + expect(stat.isDirectory()).to.be.true; + } else { + const { data } = await testSpawn('which rustrover'); + expect(data?.trim()).to.include('rustrover'); + } + }, + validateDestroy: async () => { + if (Utils.isMacOS()) { + const exists = await fs.access('/Applications/RustRover.app').then(() => true).catch(() => false); + expect(exists).to.be.false; + } else { + const { data } = await testSpawn('which rustrover'); + expect(data?.trim() ?? '').not.to.include('rustrover'); + } + }, + }); + }); + + it('Can manage JVM heap size', { timeout: 600_000 }, async () => { + const configParent = Utils.isMacOS() + ? path.join(os.homedir(), 'Library', 'Application Support', 'JetBrains') + : path.join(os.homedir(), '.config', 'JetBrains'); + + const findVmOptions = async (): Promise => { + try { + const entries = await fs.readdir(configParent); + const dir = entries.filter((e) => e.startsWith('RustRover')).sort().pop(); + if (!dir) return null; + return path.join(configParent, dir, 'rustrover.vmoptions'); + } catch { + return null; + } + }; + + await PluginTester.fullTest(pluginPath, [{ + type: 'rustrover', + jvmMaxHeapSize: '2048m', + jvmMinHeapSize: '512m', + }], { + validateApply: async () => { + const vmOptionsPath = await findVmOptions(); + expect(vmOptionsPath).to.not.be.null; + const { data } = await testSpawn(`cat "${vmOptionsPath}"`); + expect(data).to.include('-Xmx2048m'); + expect(data).to.include('-Xms512m'); + }, + testModify: { + modifiedConfigs: [{ + type: 'rustrover', + jvmMaxHeapSize: '4096m', + jvmMinHeapSize: '1024m', + }], + validateModify: async () => { + const vmOptionsPath = await findVmOptions(); + expect(vmOptionsPath).to.not.be.null; + const { data } = await testSpawn(`cat "${vmOptionsPath}"`); + expect(data).to.include('-Xmx4096m'); + expect(data).to.include('-Xms1024m'); + }, + }, + validateDestroy: async () => { + const vmOptionsPath = await findVmOptions(); + if (!vmOptionsPath) return; + try { + const content = await fs.readFile(vmOptionsPath, 'utf8'); + expect(content).not.to.include('-Xmx'); + expect(content).not.to.include('-Xms'); + } catch { /* file removed, that's fine */ } + }, + }); + }); + + it('Can install plugins', { timeout: 600_000 }, async () => { + await PluginTester.fullTest(pluginPath, [{ + type: 'rustrover', + plugins: ['Docker'], + }]) + }) +});