Skip to content
Merged
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: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "default",
"version": "1.3.0",
"version": "1.4.0-beta.5",
"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": {
Expand Down
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ import { TartVmResource } from './resources/tart/tart-vm.js';
import { TerraformResource } from './resources/terraform/terraform.js';
import { CursorResource } from './resources/cursor/cursor.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 @@ -83,6 +84,7 @@ runPlugin(Plugin.create(
new PgcliResource(),
new CursorResource(),
new VscodeResource(),
new WebStormResource(),
new GitRepositoryResource(),
new GitRepositoriesResource(),
new AndroidStudioResource(),
Expand Down
2 changes: 1 addition & 1 deletion src/resources/claude-code/claude-code-project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ export class ClaudeCodeProjectResource extends Resource<ClaudeCodeProjectConfig>
// preventing the framework from re-planning a CREATE on every validation pass.
const result: Partial<ClaudeCodeProjectConfig> = { ...parameters };

if (parameters.claudeMd !== undefined) {
if (parameters.claudeMd != null) {
if (isRemoteUrl(parameters.claudeMd)) {
// For remote URLs, keep the URL as-is so the framework compares URL vs URL.
// Change detection for remote content is done via hash on apply.
Expand Down
2 changes: 1 addition & 1 deletion src/resources/claude-code/claude-code.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ export class ClaudeCodeResource extends Resource<ClaudeCodeConfig> {

const result: Partial<ClaudeCodeConfig> = {};

if (parameters.globalClaudeMd !== undefined) {
if (parameters.globalClaudeMd != null) {
if (isRemoteUrl(parameters.globalClaudeMd)) {
result.globalClaudeMd = parameters.globalClaudeMd;
} else {
Expand Down
43 changes: 39 additions & 4 deletions src/resources/cursor/cursor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,9 +136,16 @@ export class CursorResource extends Resource<CursorConfig> {
const directory = plan.currentConfig.directory ?? '/Applications';
await $.spawn(`rm -rf "${path.join(directory, CURSOR_APPLICATION_NAME)}"`);
} else if (Utils.isLinux()) {
const directory = plan.currentConfig.directory ?? CURSOR_LOCAL_BIN;
await $.spawnSafe(`rm -f "${path.join(directory, 'cursor')}"`);
await FileUtils.removeLineFromShellRc(CURSOR_LOCAL_BIN_EXPORT);
const aptCheck = await $.spawnSafe('which apt-get');
const dnfCheck = await $.spawnSafe('which dnf');
const yumCheck = await $.spawnSafe('which yum');
if (aptCheck.status === SpawnStatus.SUCCESS || dnfCheck.status === SpawnStatus.SUCCESS || yumCheck.status === SpawnStatus.SUCCESS) {
await Utils.uninstallViaPkgMgr('cursor');
} else {
const directory = plan.currentConfig.directory ?? CURSOR_LOCAL_BIN;
await $.spawnSafe(`rm -f "${path.join(directory, 'cursor')}"`);
await FileUtils.removeLineFromShellRc(CURSOR_LOCAL_BIN_EXPORT);
}
}
}

Expand Down Expand Up @@ -168,8 +175,36 @@ export class CursorResource extends Resource<CursorConfig> {

private async installLinux(plan: CreatePlan<CursorConfig>): Promise<void> {
const $ = getPty();

const aptCheck = await $.spawnSafe('which apt-get');
if (aptCheck.status === SpawnStatus.SUCCESS) {
await $.spawn(
'bash -c "curl -fsSL https://downloads.cursor.com/keys/anysphere.asc | gpg --dearmor | tee /etc/apt/keyrings/cursor.gpg > /dev/null"',
{ requiresRoot: true },
);
await $.spawn(
'bash -c "echo \\"deb [arch=amd64,arm64 signed-by=/etc/apt/keyrings/cursor.gpg] https://downloads.cursor.com/aptrepo stable main\\" | tee /etc/apt/sources.list.d/cursor.list > /dev/null"',
{ requiresRoot: true },
);
await Utils.installViaPkgMgr('cursor');
return;
}

const dnfCheck = await $.spawnSafe('which dnf');
const yumCheck = await $.spawnSafe('which yum');
if (dnfCheck.status === SpawnStatus.SUCCESS || yumCheck.status === SpawnStatus.SUCCESS) {
const pkgMgr = dnfCheck.status === SpawnStatus.SUCCESS ? 'dnf' : 'yum';
await $.spawn(
`${pkgMgr} config-manager --add-repo https://downloads.cursor.com/yumrepo/cursor.repo`,
{ requiresRoot: true },
);
await Utils.installViaPkgMgr('cursor');
return;
}

// Fallback: AppImage
const isArm = await Utils.isArmArch();
const downloadUrl = `https://downloader.cursor.sh/linux/appImage/${isArm ? 'arm64' : 'x64'}`;
const downloadUrl = `https://api2.cursor.sh/updates/download/golden/linux-${isArm ? 'arm64' : 'x64'}/cursor/latest`;
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'cursor-'));
const tmpAppImage = path.join(tmpDir, 'cursor.AppImage');

Expand Down
23 changes: 18 additions & 5 deletions src/resources/cursor/extensions-parameter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,23 @@ function getCursorBinary(directory?: string | null): string {
'Contents', 'Resources', 'app', 'bin', 'cursor',
);
}
// On Linux, use the full path to the AppImage/binary so it works before PATH is sourced.
// On Linux, prefer the directory-scoped path (AppImage install), but fall back to
// the system PATH location (apt/dnf install puts it at /usr/bin/cursor).
return path.join(directory ?? CURSOR_LOCAL_BIN, 'cursor');
}

async function resolveCursorBinary(directory?: string | null): Promise<string> {
if (Utils.isMacOS()) return getCursorBinary(directory);
const candidate = getCursorBinary(directory);
const $ = getPty();
const check = await $.spawnSafe(`test -x "${candidate}"`);
if (check.status === SpawnStatus.SUCCESS) return candidate;
// Fall back to whatever is on PATH (e.g. /usr/bin/cursor from apt install)
const which = await $.spawnSafe('which cursor');
if (which.status === SpawnStatus.SUCCESS) return which.data.trim();
return candidate;
}

export class ExtensionsParameter extends StatefulParameter<CursorConfig, string[]> {
getSettings(): ArrayParameterSetting {
return {
Expand All @@ -30,7 +43,7 @@ export class ExtensionsParameter extends StatefulParameter<CursorConfig, string[

override async refresh(desired: string[] | null, config: Partial<CursorConfig>): Promise<string[] | null> {
const $ = getPty();
const cursor = getCursorBinary(config.directory);
const cursor = await resolveCursorBinary(config.directory);
const result = await $.spawnSafe(`"${cursor}" --list-extensions`);
if (result.status !== SpawnStatus.SUCCESS || result.data == null) {
return null;
Expand All @@ -40,9 +53,9 @@ export class ExtensionsParameter extends StatefulParameter<CursorConfig, string[

async add(valueToAdd: string[], plan: Plan<CursorConfig>): Promise<void> {
const $ = getPty();
const cursor = getCursorBinary(plan.desiredConfig?.directory);
const cursor = await resolveCursorBinary(plan.desiredConfig?.directory);
for (const ext of valueToAdd) {
await $.spawn(`"${cursor}" --install-extension ${ext} --force`, { interactive: true });
await $.spawn(`"${cursor}" --install-extension ${ext}`, { interactive: true });
}
}

Expand All @@ -55,7 +68,7 @@ export class ExtensionsParameter extends StatefulParameter<CursorConfig, string[

async remove(valueToRemove: string[], plan: Plan<CursorConfig>): Promise<void> {
const $ = getPty();
const cursor = getCursorBinary(plan.desiredConfig?.directory ?? plan.currentConfig?.directory);
const cursor = await resolveCursorBinary(plan.desiredConfig?.directory ?? plan.currentConfig?.directory);
for (const ext of valueToRemove) {
await $.spawnSafe(`"${cursor}" --uninstall-extension ${ext}`);
}
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);
}
Loading
Loading