Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions docs/resources/(resources)/webstorm.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
---
title: webstorm
description: A reference page for the webstorm resource
---

The webstorm resource installs [JetBrains WebStorm](https://www.jetbrains.com/webstorm/), a JavaScript IDE. On macOS it is installed via Homebrew Cask (`brew install --cask webstorm`); on Linux via Snap (`snap install webstorm --classic`).

## Parameters

- **settingsZip** *(string, optional)* — Absolute path to a WebStorm settings ZIP file (exported via *File | Manage IDE Settings | Export Settings*) to import on first install. The archive is extracted directly into the WebStorm config directory, so all exported settings (keymaps, code styles, inspections, etc.) are applied before WebStorm 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. `"dev.blachut.svelte.lang"`, `"org.jetbrains.plugins.github"`). 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 WebStorm, e.g. `"2048m"` for 2 GB or `"4096m"` for 4 GB. Written to `webstorm.vmoptions` in the IDE config directory as `-Xmx<value>`.

- **jvmMinHeapSize** *(string, optional)* — Initial JVM heap allocated to WebStorm, e.g. `"512m"`. Written to `webstorm.vmoptions` as `-Xms<value>`. Typically set to half the max heap size.

## Example usage

### Install WebStorm with plugins

```json title="codify.jsonc"
[
{
"type": "webstorm",
"plugins": [
"dev.blachut.svelte.lang",
"org.jetbrains.plugins.github"
]
}
]
```

### Install WebStorm, import previous settings, and increase heap

```json title="codify.jsonc"
[
{
"type": "webstorm",
"settingsZip": "/path/to/webstorm-settings.zip",
"importSettings": true,
"jvmMaxHeapSize": "4096m",
"jvmMinHeapSize": "1024m",
"plugins": [
"dev.blachut.svelte.lang",
"org.jetbrains.plugins.github"
]
}
]
```

## Notes

- On macOS a CLI launcher symlink is created at `/usr/local/bin/webstorm` during install so that `webstorm` 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 `webstorm.vmoptions` in `~/Library/Application Support/JetBrains/WebStorm<version>/` on macOS and `~/.config/JetBrains/WebStorm<version>/` on Linux. If WebStorm 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.
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import { TartResource } from './resources/tart/tart.js';
import { TartVmResource } from './resources/tart/tart-vm.js';
import { TerraformResource } from './resources/terraform/terraform.js';
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';

Expand Down Expand Up @@ -79,6 +80,7 @@ runPlugin(Plugin.create(
new GoenvResource(),
new PgcliResource(),
new VscodeResource(),
new WebStormResource(),
new GitRepositoryResource(),
new GitRepositoriesResource(),
new AndroidStudioResource(),
Expand Down
15 changes: 15 additions & 0 deletions src/resources/webstorm/completions/webstorm.plugins.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export default async function loadWebStormPlugins(): Promise<string[]> {
const response = await fetch(
'https://plugins.jetbrains.com/api/plugins?build=WS&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);
}
145 changes: 145 additions & 0 deletions src/resources/webstorm/plugins-parameter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import { ArrayStatefulParameter, getPty, Plan, SpawnStatus, Utils } from '@codifycli/plugin-core';
import fs from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';

import { WebStormConfig } from './webstorm.js';

export const MACOS_APP_PATH = '/Applications/WebStorm.app';
export const MACOS_BINARY = `${MACOS_APP_PATH}/Contents/MacOS/webstorm`;

export function getWebStormBinary(): string {
return Utils.isMacOS() ? MACOS_BINARY : 'webstorm';
}

export async function findConfigDir(): Promise<string | null> {
const parentDir = Utils.isMacOS()
? path.join(os.homedir(), 'Library', 'Application Support', 'JetBrains')
: path.join(os.homedir(), '.config', 'JetBrains');

try {
const entries = await fs.readdir(parentDir);
const dirs = entries.filter((e) => e.startsWith('WebStorm')).sort();
return dirs.length > 0 ? path.join(parentDir, dirs[dirs.length - 1]) : null;
} catch {
return null;
}
}

export async function getOrCreateConfigDir(): Promise<string | null> {
const existing = await findConfigDir();
if (existing) return existing;

const version = await getWebStormMajorMinorVersion();
if (!version) return null;

const parentDir = Utils.isMacOS()
? path.join(os.homedir(), 'Library', 'Application Support', 'JetBrains')
: path.join(os.homedir(), '.config', 'JetBrains');

const configDir = path.join(parentDir, `WebStorm${version}`);
await fs.mkdir(configDir, { recursive: true });
return configDir;
}

async function getWebStormMajorMinorVersion(): Promise<string | null> {
const $ = getPty();

if (Utils.isMacOS()) {
const result = await $.spawnSafe(
`/usr/libexec/PlistBuddy -c "Print CFBundleShortVersionString" "${MACOS_APP_PATH}/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 webstorm');
if (result.status !== SpawnStatus.SUCCESS) return null;
const lines = result.data.split('\n');
const line = lines.find((l) => l.startsWith('webstorm'));
const match = line?.match(/(\d+\.\d+)/);
return match ? match[1] : null;
}

return null;
}

function getPluginsDir(configDir: string): string {
// macOS: plugins are in a `plugins/` subdir of the config dir
// Linux: plugins are in ~/.local/share/JetBrains/WebStorm<version>/ 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);
}

async function readPluginIdFromDir(pluginDir: string): Promise<string | null> {
const xmlPath = path.join(pluginDir, 'META-INF', 'plugin.xml');
try {
const content = await fs.readFile(xmlPath, 'utf8');
const match = content.match(/<id>([^<]+)<\/id>/);
return match ? match[1].trim() : null;
} catch {
return null;
}
}

export class PluginsParameter extends ArrayStatefulParameter<WebStormConfig, string> {
override getSettings() {
return {
type: 'array' as const,
isElementEqual: (desired: string, current: string) =>
desired.toLowerCase() === current.toLowerCase(),
};
}

override async refresh(_desired: string[] | null): Promise<string[] | null> {
const configDir = await findConfigDir();
if (!configDir) return null;

const pluginsDir = getPluginsDir(configDir);
try {
const entries = await fs.readdir(pluginsDir, { withFileTypes: true });
const ids: string[] = [];

for (const entry of entries) {
if (!entry.isDirectory()) continue;
const id = await readPluginIdFromDir(path.join(pluginsDir, entry.name));
if (id) ids.push(id);
}

return ids;
} catch {
return [];
}
}

async addItem(item: string, _plan: Plan<WebStormConfig>): Promise<void> {
const $ = getPty();
const binary = getWebStormBinary();
await $.spawn(`"${binary}" installPlugins ${item}`, { interactive: true });
}

async removeItem(item: string, _plan: Plan<WebStormConfig>): Promise<void> {
const configDir = await findConfigDir();
if (!configDir) return;

const pluginsDir = getPluginsDir(configDir);
try {
const entries = await fs.readdir(pluginsDir, { withFileTypes: true });

for (const entry of entries) {
if (!entry.isDirectory()) continue;
const id = await 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 */ }
}
}
Loading