Skip to content

Commit 0518ffb

Browse files
feat: Add webstorm resource (#42)
* feat: Add webstorm resource (auto-generated from issue #41) * fix: fixed on macos and linux * fix: fixed on macos and linux * feat: added better handling for install errors * fix: claude code and cursor tests * fix: cursor tests --------- Co-authored-by: kevinwang5658 <20214115+kevinwang5658@users.noreply.github.com> Co-authored-by: kevinwang <kevinwang5658@gmail.com>
1 parent 3e0ed14 commit 0518ffb

13 files changed

Lines changed: 813 additions & 25 deletions

File tree

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
---
2+
title: webstorm
3+
description: A reference page for the webstorm resource
4+
---
5+
6+
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`).
7+
8+
## Parameters
9+
10+
- **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.
11+
12+
- **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.
13+
14+
- **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.
15+
16+
- **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>`.
17+
18+
- **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.
19+
20+
## Example usage
21+
22+
### Install WebStorm with plugins
23+
24+
```json title="codify.jsonc"
25+
[
26+
{
27+
"type": "webstorm",
28+
"plugins": [
29+
"dev.blachut.svelte.lang",
30+
"org.jetbrains.plugins.github"
31+
]
32+
}
33+
]
34+
```
35+
36+
### Install WebStorm, import previous settings, and increase heap
37+
38+
```json title="codify.jsonc"
39+
[
40+
{
41+
"type": "webstorm",
42+
"settingsZip": "/path/to/webstorm-settings.zip",
43+
"importSettings": true,
44+
"jvmMaxHeapSize": "4096m",
45+
"jvmMinHeapSize": "1024m",
46+
"plugins": [
47+
"dev.blachut.svelte.lang",
48+
"org.jetbrains.plugins.github"
49+
]
50+
}
51+
]
52+
```
53+
54+
## Notes
55+
56+
- 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.
57+
- Plugin IDs must be exact JetBrains Marketplace IDs. You can find them on the plugin's Marketplace page under *Additional Information → Plugin ID*.
58+
- 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.
59+
- 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.
60+
- On Linux, Snap must be available. Codify will attempt to install `snapd` via the system package manager if it is not found.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "default",
3-
"version": "1.3.0",
3+
"version": "1.4.0-beta.5",
44
"description": "Default plugin for Codify - provides 50+ declarative resources for managing development tools and system configuration across macOS and Linux",
55
"main": "dist/index.js",
66
"scripts": {

src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ import { TartVmResource } from './resources/tart/tart-vm.js';
5454
import { TerraformResource } from './resources/terraform/terraform.js';
5555
import { CursorResource } from './resources/cursor/cursor.js';
5656
import { VscodeResource } from './resources/vscode/vscode.js';
57+
import { WebStormResource } from './resources/webstorm/webstorm.js';
5758
import { XcodeToolsResource } from './resources/xcode-tools/xcode-tools.js';
5859
import { YumResource } from './resources/yum/yum.js';
5960

@@ -83,6 +84,7 @@ runPlugin(Plugin.create(
8384
new PgcliResource(),
8485
new CursorResource(),
8586
new VscodeResource(),
87+
new WebStormResource(),
8688
new GitRepositoryResource(),
8789
new GitRepositoriesResource(),
8890
new AndroidStudioResource(),

src/resources/claude-code/claude-code-project.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ export class ClaudeCodeProjectResource extends Resource<ClaudeCodeProjectConfig>
141141
// preventing the framework from re-planning a CREATE on every validation pass.
142142
const result: Partial<ClaudeCodeProjectConfig> = { ...parameters };
143143

144-
if (parameters.claudeMd !== undefined) {
144+
if (parameters.claudeMd != null) {
145145
if (isRemoteUrl(parameters.claudeMd)) {
146146
// For remote URLs, keep the URL as-is so the framework compares URL vs URL.
147147
// Change detection for remote content is done via hash on apply.

src/resources/claude-code/claude-code.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ export class ClaudeCodeResource extends Resource<ClaudeCodeConfig> {
150150

151151
const result: Partial<ClaudeCodeConfig> = {};
152152

153-
if (parameters.globalClaudeMd !== undefined) {
153+
if (parameters.globalClaudeMd != null) {
154154
if (isRemoteUrl(parameters.globalClaudeMd)) {
155155
result.globalClaudeMd = parameters.globalClaudeMd;
156156
} else {

src/resources/cursor/cursor.ts

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -136,9 +136,16 @@ export class CursorResource extends Resource<CursorConfig> {
136136
const directory = plan.currentConfig.directory ?? '/Applications';
137137
await $.spawn(`rm -rf "${path.join(directory, CURSOR_APPLICATION_NAME)}"`);
138138
} else if (Utils.isLinux()) {
139-
const directory = plan.currentConfig.directory ?? CURSOR_LOCAL_BIN;
140-
await $.spawnSafe(`rm -f "${path.join(directory, 'cursor')}"`);
141-
await FileUtils.removeLineFromShellRc(CURSOR_LOCAL_BIN_EXPORT);
139+
const aptCheck = await $.spawnSafe('which apt-get');
140+
const dnfCheck = await $.spawnSafe('which dnf');
141+
const yumCheck = await $.spawnSafe('which yum');
142+
if (aptCheck.status === SpawnStatus.SUCCESS || dnfCheck.status === SpawnStatus.SUCCESS || yumCheck.status === SpawnStatus.SUCCESS) {
143+
await Utils.uninstallViaPkgMgr('cursor');
144+
} else {
145+
const directory = plan.currentConfig.directory ?? CURSOR_LOCAL_BIN;
146+
await $.spawnSafe(`rm -f "${path.join(directory, 'cursor')}"`);
147+
await FileUtils.removeLineFromShellRc(CURSOR_LOCAL_BIN_EXPORT);
148+
}
142149
}
143150
}
144151

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

169176
private async installLinux(plan: CreatePlan<CursorConfig>): Promise<void> {
170177
const $ = getPty();
178+
179+
const aptCheck = await $.spawnSafe('which apt-get');
180+
if (aptCheck.status === SpawnStatus.SUCCESS) {
181+
await $.spawn(
182+
'bash -c "curl -fsSL https://downloads.cursor.com/keys/anysphere.asc | gpg --dearmor | tee /etc/apt/keyrings/cursor.gpg > /dev/null"',
183+
{ requiresRoot: true },
184+
);
185+
await $.spawn(
186+
'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"',
187+
{ requiresRoot: true },
188+
);
189+
await Utils.installViaPkgMgr('cursor');
190+
return;
191+
}
192+
193+
const dnfCheck = await $.spawnSafe('which dnf');
194+
const yumCheck = await $.spawnSafe('which yum');
195+
if (dnfCheck.status === SpawnStatus.SUCCESS || yumCheck.status === SpawnStatus.SUCCESS) {
196+
const pkgMgr = dnfCheck.status === SpawnStatus.SUCCESS ? 'dnf' : 'yum';
197+
await $.spawn(
198+
`${pkgMgr} config-manager --add-repo https://downloads.cursor.com/yumrepo/cursor.repo`,
199+
{ requiresRoot: true },
200+
);
201+
await Utils.installViaPkgMgr('cursor');
202+
return;
203+
}
204+
205+
// Fallback: AppImage
171206
const isArm = await Utils.isArmArch();
172-
const downloadUrl = `https://downloader.cursor.sh/linux/appImage/${isArm ? 'arm64' : 'x64'}`;
207+
const downloadUrl = `https://api2.cursor.sh/updates/download/golden/linux-${isArm ? 'arm64' : 'x64'}/cursor/latest`;
173208
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'cursor-'));
174209
const tmpAppImage = path.join(tmpDir, 'cursor.AppImage');
175210

src/resources/cursor/extensions-parameter.ts

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,23 @@ function getCursorBinary(directory?: string | null): string {
1414
'Contents', 'Resources', 'app', 'bin', 'cursor',
1515
);
1616
}
17-
// On Linux, use the full path to the AppImage/binary so it works before PATH is sourced.
17+
// On Linux, prefer the directory-scoped path (AppImage install), but fall back to
18+
// the system PATH location (apt/dnf install puts it at /usr/bin/cursor).
1819
return path.join(directory ?? CURSOR_LOCAL_BIN, 'cursor');
1920
}
2021

22+
async function resolveCursorBinary(directory?: string | null): Promise<string> {
23+
if (Utils.isMacOS()) return getCursorBinary(directory);
24+
const candidate = getCursorBinary(directory);
25+
const $ = getPty();
26+
const check = await $.spawnSafe(`test -x "${candidate}"`);
27+
if (check.status === SpawnStatus.SUCCESS) return candidate;
28+
// Fall back to whatever is on PATH (e.g. /usr/bin/cursor from apt install)
29+
const which = await $.spawnSafe('which cursor');
30+
if (which.status === SpawnStatus.SUCCESS) return which.data.trim();
31+
return candidate;
32+
}
33+
2134
export class ExtensionsParameter extends StatefulParameter<CursorConfig, string[]> {
2235
getSettings(): ArrayParameterSetting {
2336
return {
@@ -30,7 +43,7 @@ export class ExtensionsParameter extends StatefulParameter<CursorConfig, string[
3043

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

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

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

5669
async remove(valueToRemove: string[], plan: Plan<CursorConfig>): Promise<void> {
5770
const $ = getPty();
58-
const cursor = getCursorBinary(plan.desiredConfig?.directory ?? plan.currentConfig?.directory);
71+
const cursor = await resolveCursorBinary(plan.desiredConfig?.directory ?? plan.currentConfig?.directory);
5972
for (const ext of valueToRemove) {
6073
await $.spawnSafe(`"${cursor}" --uninstall-extension ${ext}`);
6174
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
export default async function loadWebStormPlugins(): Promise<string[]> {
2+
const response = await fetch(
3+
'https://plugins.jetbrains.com/api/plugins?build=WS&orderBy=downloads&offset=0&limit=500',
4+
{ headers: { Accept: 'application/json' } }
5+
);
6+
7+
if (!response.ok) {
8+
return [];
9+
}
10+
11+
const data = await response.json() as Array<{ xmlId?: string }>;
12+
return data
13+
.map((p) => p.xmlId)
14+
.filter((id): id is string => typeof id === 'string' && id.length > 0);
15+
}

0 commit comments

Comments
 (0)