openspec-playwright 0.1.39 → 0.1.40
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -21
- package/README.zh-CN.md +29 -12
- package/dist/commands/editors.d.ts +3 -5
- package/dist/commands/editors.js +348 -17
- package/dist/commands/editors.js.map +1 -1
- package/dist/commands/init.js +5 -3
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/update.js +4 -2
- package/dist/commands/update.js.map +1 -1
- package/openspec-playwright-0.1.40.tgz +0 -0
- package/package.json +1 -1
- package/release-notes.md +2 -2
- package/src/commands/editors.ts +395 -21
- package/src/commands/init.ts +5 -3
- package/src/commands/update.ts +4 -2
- package/openspec-playwright-0.1.39.tgz +0 -0
package/README.md
CHANGED
|
@@ -18,21 +18,35 @@ openspec init # Initialize OpenSpec
|
|
|
18
18
|
openspec-pw init # Install Playwright E2E integration
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
+
## Supported AI Coding Assistants
|
|
22
|
+
|
|
23
|
+
Auto-detects and installs commands for these editors:
|
|
24
|
+
|
|
25
|
+
| Editor | Command | Format |
|
|
26
|
+
|--------|---------|--------|
|
|
27
|
+
| Claude Code | `/opsx:e2e` | Skill + command + MCP |
|
|
28
|
+
| Cursor | `/opsx-e2e` | Command |
|
|
29
|
+
| Windsurf | `/opsx-e2e` | Workflow |
|
|
30
|
+
| Cline | `/opsx-e2e` | Workflow |
|
|
31
|
+
| Continue | `/opsx-e2e` | Prompt |
|
|
32
|
+
|
|
33
|
+
`openspec-pw init` detects which editors you have in your project and installs the appropriate files. Claude Code gets the full experience (skill + command + Playwright MCP). Other editors get command/workflow files with the complete E2E workflow.
|
|
34
|
+
|
|
21
35
|
## Usage
|
|
22
36
|
|
|
23
|
-
### In
|
|
37
|
+
### In Your AI Coding Assistant
|
|
24
38
|
|
|
25
39
|
```bash
|
|
26
|
-
/opsx:e2e my-feature #
|
|
27
|
-
/
|
|
40
|
+
/opsx:e2e my-feature # Claude Code
|
|
41
|
+
/opsx-e2e my-feature # Cursor, Windsurf, Cline, Continue
|
|
28
42
|
```
|
|
29
43
|
|
|
30
44
|
### CLI Commands
|
|
31
45
|
|
|
32
46
|
```bash
|
|
33
47
|
openspec-pw init # Initialize integration (one-time setup)
|
|
34
|
-
openspec-pw update
|
|
35
|
-
openspec-pw doctor
|
|
48
|
+
openspec-pw update # Update CLI and commands to latest version
|
|
49
|
+
openspec-pw doctor # Check prerequisites
|
|
36
50
|
```
|
|
37
51
|
|
|
38
52
|
## How It Works
|
|
@@ -62,13 +76,16 @@ openspec-pw doctor # Check prerequisites
|
|
|
62
76
|
|
|
63
77
|
1. **Node.js >= 20**
|
|
64
78
|
2. **OpenSpec** initialized: `npm install -g @fission-ai/openspec && openspec init`
|
|
65
|
-
3. **Claude Code
|
|
79
|
+
3. **One of**: Claude Code, Cursor, Windsurf, Cline, or Continue (auto-detected)
|
|
80
|
+
4. **Claude Code only**: Playwright MCP — `claude mcp add playwright npx @playwright/mcp@latest`
|
|
66
81
|
|
|
67
82
|
## What `openspec-pw init` Does
|
|
68
83
|
|
|
69
|
-
1.
|
|
70
|
-
2. Installs
|
|
71
|
-
3.
|
|
84
|
+
1. Detects installed AI coding assistants (Claude Code, Cursor, Windsurf, Cline, Continue)
|
|
85
|
+
2. Installs E2E command/workflow files for each detected editor
|
|
86
|
+
3. Installs `/openspec-e2e` skill for Claude Code
|
|
87
|
+
4. Installs Playwright MCP globally for Claude Code (via `claude mcp add`)
|
|
88
|
+
5. Generates `tests/playwright/seed.spec.ts`, `auth.setup.ts`, `credentials.yaml`
|
|
72
89
|
|
|
73
90
|
> **Note**: After running `openspec-pw init`, manually install Playwright browsers: `npx playwright install --with-deps`
|
|
74
91
|
|
|
@@ -109,23 +126,30 @@ Edit `tests/playwright/credentials.yaml`:
|
|
|
109
126
|
- Configure test user credentials
|
|
110
127
|
- Add multiple users for role-based tests
|
|
111
128
|
|
|
112
|
-
### MCP server
|
|
129
|
+
### MCP server (Claude Code only)
|
|
113
130
|
|
|
114
|
-
Playwright MCP is installed globally via `claude mcp add
|
|
131
|
+
Playwright MCP is installed globally via `claude mcp add` and enables the Healer Agent (auto-heals test failures via UI inspection). Restart Claude Code after setup to activate.
|
|
115
132
|
|
|
116
133
|
## Architecture
|
|
117
134
|
|
|
118
135
|
```
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
136
|
+
Schema (openspec/schemas/playwright-e2e/)
|
|
137
|
+
└── Templates: test-plan.md, report.md, playwright.config.ts
|
|
138
|
+
|
|
139
|
+
CLI (openspec-pw)
|
|
140
|
+
├── init → Installs commands for detected editors
|
|
141
|
+
├── update → Syncs commands + schema from npm
|
|
142
|
+
└── doctor → Checks prerequisites
|
|
143
|
+
|
|
144
|
+
Skill/Commands (per editor)
|
|
145
|
+
├── Claude Code → /openspec-e2e (skill) + /opsx:e2e (command) + MCP
|
|
146
|
+
├── Cursor → /opsx-e2e (command)
|
|
147
|
+
├── Windsurf → /opsx-e2e (workflow)
|
|
148
|
+
├── Cline → /opsx-e2e (workflow)
|
|
149
|
+
└── Continue → /opsx-e2e (prompt)
|
|
150
|
+
|
|
151
|
+
Healer Agent (Claude Code + MCP only)
|
|
152
|
+
└── browser_snapshot, browser_navigate, browser_run_code, etc.
|
|
129
153
|
```
|
|
130
154
|
|
|
131
155
|
## License
|
package/README.zh-CN.md
CHANGED
|
@@ -18,27 +18,41 @@ openspec init # 初始化 OpenSpec
|
|
|
18
18
|
openspec-pw init # 安装 Playwright E2E 集成
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
+
## 支持的 AI 编码助手
|
|
22
|
+
|
|
23
|
+
自动检测并安装以下编辑器的命令文件:
|
|
24
|
+
|
|
25
|
+
| 编辑器 | 命令 | 格式 |
|
|
26
|
+
|--------|------|------|
|
|
27
|
+
| Claude Code | `/opsx:e2e` | Skill + 命令 + MCP |
|
|
28
|
+
| Cursor | `/opsx-e2e` | 命令 |
|
|
29
|
+
| Windsurf | `/opsx-e2e` | 工作流 |
|
|
30
|
+
| Cline | `/opsx-e2e` | 工作流 |
|
|
31
|
+
| Continue | `/opsx-e2e` | Prompt |
|
|
32
|
+
|
|
33
|
+
`openspec-pw init` 会检测项目中安装了哪些编辑器并安装对应文件。Claude Code 获得完整体验(skill + 命令 + Playwright MCP)。其他编辑器获得包含完整 E2E 工作流的命令/工作流文件。
|
|
34
|
+
|
|
21
35
|
## 使用
|
|
22
36
|
|
|
23
|
-
### 在
|
|
37
|
+
### 在 AI 编码助手中
|
|
24
38
|
|
|
25
39
|
```bash
|
|
26
|
-
/opsx:e2e my-feature #
|
|
27
|
-
/
|
|
40
|
+
/opsx:e2e my-feature # Claude Code
|
|
41
|
+
/opsx-e2e my-feature # Cursor, Windsurf, Cline, Continue
|
|
28
42
|
```
|
|
29
43
|
|
|
30
44
|
### CLI 命令
|
|
31
45
|
|
|
32
46
|
```bash
|
|
33
47
|
openspec-pw init # 初始化集成(一次性设置)
|
|
34
|
-
openspec-pw update # 更新 CLI
|
|
48
|
+
openspec-pw update # 更新 CLI 和命令到最新版本
|
|
35
49
|
openspec-pw doctor # 检查前置条件
|
|
36
50
|
```
|
|
37
51
|
|
|
38
52
|
## 工作原理
|
|
39
53
|
|
|
40
54
|
```
|
|
41
|
-
/
|
|
55
|
+
/opsx:e2e <change-name>
|
|
42
56
|
│
|
|
43
57
|
├── 1. 从 openspec/changes/<name>/specs/ 读取 OpenSpec specs
|
|
44
58
|
│
|
|
@@ -46,7 +60,7 @@ openspec-pw doctor # 检查前置条件
|
|
|
46
60
|
│
|
|
47
61
|
├── 3. Generator Agent → 创建 tests/playwright/<name>.spec.ts
|
|
48
62
|
│
|
|
49
|
-
└── 4. Healer Agent → 运行测试 +
|
|
63
|
+
└── 4. Healer Agent → 运行测试 + 自动修复失败(Claude Code + MCP)
|
|
50
64
|
│
|
|
51
65
|
└── 报告: openspec/reports/playwright-e2e-<name>.md
|
|
52
66
|
```
|
|
@@ -62,13 +76,16 @@ openspec-pw doctor # 检查前置条件
|
|
|
62
76
|
|
|
63
77
|
1. **Node.js >= 20**
|
|
64
78
|
2. **OpenSpec** 已初始化: `npm install -g @fission-ai/openspec && openspec init`
|
|
65
|
-
3.
|
|
79
|
+
3. **任一**: Claude Code、Cursor、Windsurf、Cline 或 Continue(自动检测)
|
|
80
|
+
4. **仅 Claude Code**: Playwright MCP — `claude mcp add playwright npx @playwright/mcp@latest`
|
|
66
81
|
|
|
67
82
|
## `openspec-pw init` 做了什么
|
|
68
83
|
|
|
69
|
-
1.
|
|
70
|
-
2.
|
|
71
|
-
3.
|
|
84
|
+
1. 检测已安装的 AI 编码助手(Claude Code、Cursor、Windsurf、Cline、Continue)
|
|
85
|
+
2. 为每个检测到的编辑器安装 E2E 命令/工作流文件
|
|
86
|
+
3. 为 Claude Code 安装 `/openspec-e2e` skill
|
|
87
|
+
4. 为 Claude Code 全局安装 Playwright MCP(通过 `claude mcp add`)
|
|
88
|
+
5. 生成 `tests/playwright/seed.spec.ts`、`auth.setup.ts`、`credentials.yaml`
|
|
72
89
|
|
|
73
90
|
> **注意**:运行 `openspec-pw init` 后,手动安装 Playwright 浏览器:`npx playwright install --with-deps`
|
|
74
91
|
|
|
@@ -109,9 +126,9 @@ npx playwright test --project=setup
|
|
|
109
126
|
- 配置测试用户凭证
|
|
110
127
|
- 为角色测试添加多用户
|
|
111
128
|
|
|
112
|
-
### MCP
|
|
129
|
+
### MCP 服务器(仅 Claude Code)
|
|
113
130
|
|
|
114
|
-
Playwright MCP 通过 `claude mcp add`
|
|
131
|
+
Playwright MCP 通过 `claude mcp add` 全局安装,启用 Healer Agent(通过 UI 检查自动修复测试失败)。设置后需重启 Claude Code 生效。
|
|
115
132
|
|
|
116
133
|
## 许可
|
|
117
134
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** Shared YAML escape — matches OpenSpec's escape logic */
|
|
2
2
|
export declare function escapeYamlValue(value: string): string;
|
|
3
|
-
/** Format tags as YAML inline array */
|
|
3
|
+
/** Format tags as YAML inline array (escaped) */
|
|
4
4
|
export declare function formatTagsArray(tags: string[]): string;
|
|
5
5
|
/** Command metadata shared across editors */
|
|
6
6
|
export interface CommandMeta {
|
|
@@ -13,19 +13,17 @@ export interface CommandMeta {
|
|
|
13
13
|
}
|
|
14
14
|
/** Editor adapter — Strategy Pattern */
|
|
15
15
|
export interface EditorAdapter {
|
|
16
|
-
/** Tool identifier */
|
|
17
16
|
toolId: string;
|
|
18
|
-
/** Whether this editor supports SKILL.md */
|
|
19
17
|
hasSkill: boolean;
|
|
20
|
-
/** Get the command file path relative to project root */
|
|
21
18
|
getCommandPath(commandId: string): string;
|
|
22
|
-
/** Format the complete file content */
|
|
23
19
|
formatCommand(meta: CommandMeta): string;
|
|
24
20
|
}
|
|
25
21
|
/** Claude Code: .claude/commands/opsx/<id>.md + SKILL.md */
|
|
26
22
|
declare const claudeAdapter: EditorAdapter;
|
|
27
23
|
/** Detect which editors are installed by checking their config directories */
|
|
28
24
|
export declare function detectEditors(projectRoot: string): EditorAdapter[];
|
|
25
|
+
/** Detect Codex by checking if CODEX_HOME or ~/.codex exists */
|
|
26
|
+
export declare function detectCodex(): EditorAdapter | null;
|
|
29
27
|
/** Build the shared command metadata */
|
|
30
28
|
export declare function buildCommandMeta(body: string): CommandMeta;
|
|
31
29
|
/** Install command files for all detected editors */
|
package/dist/commands/editors.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, writeFileSync } from 'fs';
|
|
2
|
+
import { homedir } from 'os';
|
|
2
3
|
import { join, dirname } from 'path';
|
|
3
4
|
import chalk from 'chalk';
|
|
4
5
|
/** Shared YAML escape — matches OpenSpec's escape logic */
|
|
@@ -10,17 +11,24 @@ export function escapeYamlValue(value) {
|
|
|
10
11
|
}
|
|
11
12
|
return value;
|
|
12
13
|
}
|
|
13
|
-
/** Format tags as YAML inline array */
|
|
14
|
+
/** Format tags as YAML inline array (escaped) */
|
|
14
15
|
export function formatTagsArray(tags) {
|
|
15
16
|
return `[${tags.map(t => escapeYamlValue(t)).join(', ')}]`;
|
|
16
17
|
}
|
|
18
|
+
/** Format tags as YAML inline array (plain, no escaping) */
|
|
19
|
+
function formatTagsPlain(tags) {
|
|
20
|
+
return `[${tags.join(', ')}]`;
|
|
21
|
+
}
|
|
22
|
+
/** Transform /opsx: to /opsx- for OpenCode */
|
|
23
|
+
function transformToHyphenCommands(text) {
|
|
24
|
+
return text.replace(/\/opsx:/g, '/opsx-');
|
|
25
|
+
}
|
|
26
|
+
// ─── Claude Code ──────────────────────────────────────────────────────────────
|
|
17
27
|
/** Claude Code: .claude/commands/opsx/<id>.md + SKILL.md */
|
|
18
28
|
const claudeAdapter = {
|
|
19
29
|
toolId: 'claude',
|
|
20
30
|
hasSkill: true,
|
|
21
|
-
getCommandPath(id) {
|
|
22
|
-
return join('.claude', 'commands', 'opsx', `${id}.md`);
|
|
23
|
-
},
|
|
31
|
+
getCommandPath(id) { return join('.claude', 'commands', 'opsx', `${id}.md`); },
|
|
24
32
|
formatCommand(meta) {
|
|
25
33
|
return `---
|
|
26
34
|
name: ${escapeYamlValue(meta.name)}
|
|
@@ -33,13 +41,12 @@ ${meta.body}
|
|
|
33
41
|
`;
|
|
34
42
|
},
|
|
35
43
|
};
|
|
44
|
+
// ─── Cursor ─────────────────────────────────────────────────────────────────
|
|
36
45
|
/** Cursor: .cursor/commands/opsx-<id>.md */
|
|
37
46
|
const cursorAdapter = {
|
|
38
47
|
toolId: 'cursor',
|
|
39
48
|
hasSkill: false,
|
|
40
|
-
getCommandPath(id) {
|
|
41
|
-
return join('.cursor', 'commands', `opsx-${id}.md`);
|
|
42
|
-
},
|
|
49
|
+
getCommandPath(id) { return join('.cursor', 'commands', `opsx-${id}.md`); },
|
|
43
50
|
formatCommand(meta) {
|
|
44
51
|
return `---
|
|
45
52
|
name: /opsx-${meta.id}
|
|
@@ -52,13 +59,12 @@ ${meta.body}
|
|
|
52
59
|
`;
|
|
53
60
|
},
|
|
54
61
|
};
|
|
62
|
+
// ─── Windsurf ────────────────────────────────────────────────────────────────
|
|
55
63
|
/** Windsurf: .windsurf/workflows/opsx-<id>.md */
|
|
56
64
|
const windsurfAdapter = {
|
|
57
65
|
toolId: 'windsurf',
|
|
58
66
|
hasSkill: false,
|
|
59
|
-
getCommandPath(id) {
|
|
60
|
-
return join('.windsurf', 'workflows', `opsx-${id}.md`);
|
|
61
|
-
},
|
|
67
|
+
getCommandPath(id) { return join('.windsurf', 'workflows', `opsx-${id}.md`); },
|
|
62
68
|
formatCommand(meta) {
|
|
63
69
|
return `---
|
|
64
70
|
name: ${escapeYamlValue(meta.name)}
|
|
@@ -71,13 +77,12 @@ ${meta.body}
|
|
|
71
77
|
`;
|
|
72
78
|
},
|
|
73
79
|
};
|
|
80
|
+
// ─── Cline ──────────────────────────────────────────────────────────────────
|
|
74
81
|
/** Cline: .clinerules/workflows/opsx-<id>.md — markdown header only */
|
|
75
82
|
const clineAdapter = {
|
|
76
83
|
toolId: 'cline',
|
|
77
84
|
hasSkill: false,
|
|
78
|
-
getCommandPath(id) {
|
|
79
|
-
return join('.clinerules', 'workflows', `opsx-${id}.md`);
|
|
80
|
-
},
|
|
85
|
+
getCommandPath(id) { return join('.clinerules', 'workflows', `opsx-${id}.md`); },
|
|
81
86
|
formatCommand(meta) {
|
|
82
87
|
return `# ${meta.name}
|
|
83
88
|
|
|
@@ -87,13 +92,12 @@ ${meta.body}
|
|
|
87
92
|
`;
|
|
88
93
|
},
|
|
89
94
|
};
|
|
95
|
+
// ─── Continue ────────────────────────────────────────────────────────────────
|
|
90
96
|
/** Continue: .continue/prompts/opsx-<id>.prompt */
|
|
91
97
|
const continueAdapter = {
|
|
92
98
|
toolId: 'continue',
|
|
93
99
|
hasSkill: false,
|
|
94
|
-
getCommandPath(id) {
|
|
95
|
-
return join('.continue', 'prompts', `opsx-${id}.prompt`);
|
|
96
|
-
},
|
|
100
|
+
getCommandPath(id) { return join('.continue', 'prompts', `opsx-${id}.prompt`); },
|
|
97
101
|
formatCommand(meta) {
|
|
98
102
|
return `---
|
|
99
103
|
name: opsx-${meta.id}
|
|
@@ -105,13 +109,316 @@ ${meta.body}
|
|
|
105
109
|
`;
|
|
106
110
|
},
|
|
107
111
|
};
|
|
108
|
-
|
|
112
|
+
// ─── amazon-q ─────────────────────────────────────────────────────────────
|
|
113
|
+
/** Amazon Q: .amazonq/prompts/opsx-<id>.md */
|
|
114
|
+
const amazonqAdapter = {
|
|
115
|
+
toolId: 'amazon-q',
|
|
116
|
+
hasSkill: false,
|
|
117
|
+
getCommandPath(id) { return join('.amazonq', 'prompts', `opsx-${id}.md`); },
|
|
118
|
+
formatCommand(meta) {
|
|
119
|
+
return `---
|
|
120
|
+
description: ${escapeYamlValue(meta.description)}
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
${meta.body}
|
|
124
|
+
`;
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
// ─── antigravity ──────────────────────────────────────────────────────────
|
|
128
|
+
/** Antigravity: .agent/workflows/opsx-<id>.md */
|
|
129
|
+
const antigravityAdapter = {
|
|
130
|
+
toolId: 'antigravity',
|
|
131
|
+
hasSkill: false,
|
|
132
|
+
getCommandPath(id) { return join('.agent', 'workflows', `opsx-${id}.md`); },
|
|
133
|
+
formatCommand(meta) {
|
|
134
|
+
return `---
|
|
135
|
+
description: ${escapeYamlValue(meta.description)}
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
${meta.body}
|
|
139
|
+
`;
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
// ─── auggie ────────────────────────────────────────────────────────────────
|
|
143
|
+
/** Auggie: .augment/commands/opsx-<id>.md */
|
|
144
|
+
const auggieAdapter = {
|
|
145
|
+
toolId: 'auggie',
|
|
146
|
+
hasSkill: false,
|
|
147
|
+
getCommandPath(id) { return join('.augment', 'commands', `opsx-${id}.md`); },
|
|
148
|
+
formatCommand(meta) {
|
|
149
|
+
return `---
|
|
150
|
+
description: ${escapeYamlValue(meta.description)}
|
|
151
|
+
argument-hint: command arguments
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
${meta.body}
|
|
155
|
+
`;
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
// ─── codebuddy ─────────────────────────────────────────────────────────────
|
|
159
|
+
/** CodeBuddy: .codebuddy/commands/opsx/<id>.md */
|
|
160
|
+
const codebuddyAdapter = {
|
|
161
|
+
toolId: 'codebuddy',
|
|
162
|
+
hasSkill: false,
|
|
163
|
+
getCommandPath(id) { return join('.codebuddy', 'commands', 'opsx', `${id}.md`); },
|
|
164
|
+
formatCommand(meta) {
|
|
165
|
+
return `---
|
|
166
|
+
name: ${escapeYamlValue(meta.name)}
|
|
167
|
+
description: "${meta.description}"
|
|
168
|
+
argument-hint: "[command arguments]"
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
${meta.body}
|
|
172
|
+
`;
|
|
173
|
+
},
|
|
174
|
+
};
|
|
175
|
+
// ─── codex ────────────────────────────────────────────────────────────────
|
|
176
|
+
/** Codex: <CODEX_HOME>/prompts/opsx-<id>.md — global scope */
|
|
177
|
+
const codexAdapter = {
|
|
178
|
+
toolId: 'codex',
|
|
179
|
+
hasSkill: false,
|
|
180
|
+
getCommandPath(id) {
|
|
181
|
+
const codexHome = process.env.CODEX_HOME?.trim() || join(homedir(), '.codex');
|
|
182
|
+
return join(codexHome, 'prompts', `opsx-${id}.md`);
|
|
183
|
+
},
|
|
184
|
+
formatCommand(meta) {
|
|
185
|
+
return `---
|
|
186
|
+
description: ${escapeYamlValue(meta.description)}
|
|
187
|
+
argument-hint: command arguments
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
${meta.body}
|
|
191
|
+
`;
|
|
192
|
+
},
|
|
193
|
+
};
|
|
194
|
+
// ─── costrict ─────────────────────────────────────────────────────────────
|
|
195
|
+
/** CoStrict: .cospec/openspec/commands/opsx-<id>.md */
|
|
196
|
+
const costrictAdapter = {
|
|
197
|
+
toolId: 'costrict',
|
|
198
|
+
hasSkill: false,
|
|
199
|
+
getCommandPath(id) { return join('.cospec', 'openspec', 'commands', `opsx-${id}.md`); },
|
|
200
|
+
formatCommand(meta) {
|
|
201
|
+
return `---
|
|
202
|
+
description: "${meta.description}"
|
|
203
|
+
argument-hint: command arguments
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
${meta.body}
|
|
207
|
+
`;
|
|
208
|
+
},
|
|
209
|
+
};
|
|
210
|
+
// ─── crush ────────────────────────────────────────────────────────────────
|
|
211
|
+
/** Crush: .crush/commands/opsx/<id>.md — tags plain join */
|
|
212
|
+
const crushAdapter = {
|
|
213
|
+
toolId: 'crush',
|
|
214
|
+
hasSkill: false,
|
|
215
|
+
getCommandPath(id) { return join('.crush', 'commands', 'opsx', `${id}.md`); },
|
|
216
|
+
formatCommand(meta) {
|
|
217
|
+
return `---
|
|
218
|
+
name: ${escapeYamlValue(meta.name)}
|
|
219
|
+
description: ${escapeYamlValue(meta.description)}
|
|
220
|
+
category: ${escapeYamlValue(meta.category)}
|
|
221
|
+
tags: ${formatTagsPlain(meta.tags)}
|
|
222
|
+
---
|
|
223
|
+
|
|
224
|
+
${meta.body}
|
|
225
|
+
`;
|
|
226
|
+
},
|
|
227
|
+
};
|
|
228
|
+
// ─── factory ───────────────────────────────────────────────────────────────
|
|
229
|
+
/** Factory Droid: .factory/commands/opsx-<id>.md */
|
|
230
|
+
const factoryAdapter = {
|
|
231
|
+
toolId: 'factory',
|
|
232
|
+
hasSkill: false,
|
|
233
|
+
getCommandPath(id) { return join('.factory', 'commands', `opsx-${id}.md`); },
|
|
234
|
+
formatCommand(meta) {
|
|
235
|
+
return `---
|
|
236
|
+
description: ${escapeYamlValue(meta.description)}
|
|
237
|
+
argument-hint: command arguments
|
|
238
|
+
---
|
|
239
|
+
|
|
240
|
+
${meta.body}
|
|
241
|
+
`;
|
|
242
|
+
},
|
|
243
|
+
};
|
|
244
|
+
// ─── gemini ────────────────────────────────────────────────────────────────
|
|
245
|
+
/** Gemini CLI: .gemini/commands/opsx/<id>.toml */
|
|
246
|
+
const geminiAdapter = {
|
|
247
|
+
toolId: 'gemini',
|
|
248
|
+
hasSkill: false,
|
|
249
|
+
getCommandPath(id) { return join('.gemini', 'commands', 'opsx', `${id}.toml`); },
|
|
250
|
+
formatCommand(meta) {
|
|
251
|
+
return `description = "${meta.description}"
|
|
252
|
+
|
|
253
|
+
prompt = """
|
|
254
|
+
${meta.body}
|
|
255
|
+
"""
|
|
256
|
+
`;
|
|
257
|
+
},
|
|
258
|
+
};
|
|
259
|
+
// ─── github-copilot ────────────────────────────────────────────────────────
|
|
260
|
+
/** GitHub Copilot: .github/prompts/opsx-<id>.prompt.md */
|
|
261
|
+
const githubcopilotAdapter = {
|
|
262
|
+
toolId: 'github-copilot',
|
|
263
|
+
hasSkill: false,
|
|
264
|
+
getCommandPath(id) { return join('.github', 'prompts', `opsx-${id}.prompt.md`); },
|
|
265
|
+
formatCommand(meta) {
|
|
266
|
+
return `---
|
|
267
|
+
description: ${escapeYamlValue(meta.description)}
|
|
268
|
+
---
|
|
269
|
+
|
|
270
|
+
${meta.body}
|
|
271
|
+
`;
|
|
272
|
+
},
|
|
273
|
+
};
|
|
274
|
+
// ─── iflow ────────────────────────────────────────────────────────────────
|
|
275
|
+
/** iFlow: .iflow/commands/opsx-<id>.md */
|
|
276
|
+
const iflowAdapter = {
|
|
277
|
+
toolId: 'iflow',
|
|
278
|
+
hasSkill: false,
|
|
279
|
+
getCommandPath(id) { return join('.iflow', 'commands', `opsx-${id}.md`); },
|
|
280
|
+
formatCommand(meta) {
|
|
281
|
+
return `---
|
|
282
|
+
name: /opsx-${meta.id}
|
|
283
|
+
id: opsx-${meta.id}
|
|
284
|
+
category: ${escapeYamlValue(meta.category)}
|
|
285
|
+
description: ${escapeYamlValue(meta.description)}
|
|
286
|
+
---
|
|
287
|
+
|
|
288
|
+
${meta.body}
|
|
289
|
+
`;
|
|
290
|
+
},
|
|
291
|
+
};
|
|
292
|
+
// ─── kilocode ────────────────────────────────────────────────────────────
|
|
293
|
+
/** Kilo Code: .kilocode/workflows/opsx-<id>.md — body only */
|
|
294
|
+
const kilocodeAdapter = {
|
|
295
|
+
toolId: 'kilocode',
|
|
296
|
+
hasSkill: false,
|
|
297
|
+
getCommandPath(id) { return join('.kilocode', 'workflows', `opsx-${id}.md`); },
|
|
298
|
+
formatCommand(meta) {
|
|
299
|
+
return `${meta.body}
|
|
300
|
+
`;
|
|
301
|
+
},
|
|
302
|
+
};
|
|
303
|
+
// ─── kiro ─────────────────────────────────────────────────────────────────
|
|
304
|
+
/** Kiro: .kiro/prompts/opsx-<id>.prompt.md */
|
|
305
|
+
const kiroAdapter = {
|
|
306
|
+
toolId: 'kiro',
|
|
307
|
+
hasSkill: false,
|
|
308
|
+
getCommandPath(id) { return join('.kiro', 'prompts', `opsx-${id}.prompt.md`); },
|
|
309
|
+
formatCommand(meta) {
|
|
310
|
+
return `---
|
|
311
|
+
description: ${escapeYamlValue(meta.description)}
|
|
312
|
+
---
|
|
313
|
+
|
|
314
|
+
${meta.body}
|
|
315
|
+
`;
|
|
316
|
+
},
|
|
317
|
+
};
|
|
318
|
+
// ─── opencode ─────────────────────────────────────────────────────────────
|
|
319
|
+
/** OpenCode: .opencode/commands/opsx-<id>.md — transforms /opsx: to /opsx- */
|
|
320
|
+
const opencodeAdapter = {
|
|
321
|
+
toolId: 'opencode',
|
|
322
|
+
hasSkill: false,
|
|
323
|
+
getCommandPath(id) { return join('.opencode', 'commands', `opsx-${id}.md`); },
|
|
324
|
+
formatCommand(meta) {
|
|
325
|
+
const transformed = transformToHyphenCommands(meta.body);
|
|
326
|
+
return `---
|
|
327
|
+
description: ${escapeYamlValue(meta.description)}
|
|
328
|
+
---
|
|
329
|
+
|
|
330
|
+
${transformed}
|
|
331
|
+
`;
|
|
332
|
+
},
|
|
333
|
+
};
|
|
334
|
+
// ─── pi ──────────────────────────────────────────────────────────────────
|
|
335
|
+
/** Pi: .pi/prompts/opsx-<id>.md */
|
|
336
|
+
const piAdapter = {
|
|
337
|
+
toolId: 'pi',
|
|
338
|
+
hasSkill: false,
|
|
339
|
+
getCommandPath(id) { return join('.pi', 'prompts', `opsx-${id}.md`); },
|
|
340
|
+
formatCommand(meta) {
|
|
341
|
+
return `---
|
|
342
|
+
description: ${escapeYamlValue(meta.description)}
|
|
343
|
+
---
|
|
344
|
+
|
|
345
|
+
${meta.body}
|
|
346
|
+
`;
|
|
347
|
+
},
|
|
348
|
+
};
|
|
349
|
+
// ─── qoder ────────────────────────────────────────────────────────────────
|
|
350
|
+
/** Qoder: .qoder/commands/opsx/<id>.md — tags plain join */
|
|
351
|
+
const qoderAdapter = {
|
|
352
|
+
toolId: 'qoder',
|
|
353
|
+
hasSkill: false,
|
|
354
|
+
getCommandPath(id) { return join('.qoder', 'commands', 'opsx', `${id}.md`); },
|
|
355
|
+
formatCommand(meta) {
|
|
356
|
+
return `---
|
|
357
|
+
name: ${escapeYamlValue(meta.name)}
|
|
358
|
+
description: ${escapeYamlValue(meta.description)}
|
|
359
|
+
category: ${escapeYamlValue(meta.category)}
|
|
360
|
+
tags: ${formatTagsPlain(meta.tags)}
|
|
361
|
+
---
|
|
362
|
+
|
|
363
|
+
${meta.body}
|
|
364
|
+
`;
|
|
365
|
+
},
|
|
366
|
+
};
|
|
367
|
+
// ─── qwen ────────────────────────────────────────────────────────────────
|
|
368
|
+
/** Qwen Code: .qwen/commands/opsx-<id>.toml */
|
|
369
|
+
const qwenAdapter = {
|
|
370
|
+
toolId: 'qwen',
|
|
371
|
+
hasSkill: false,
|
|
372
|
+
getCommandPath(id) { return join('.qwen', 'commands', `opsx-${id}.toml`); },
|
|
373
|
+
formatCommand(meta) {
|
|
374
|
+
return `description = "${meta.description}"
|
|
375
|
+
|
|
376
|
+
prompt = """
|
|
377
|
+
${meta.body}
|
|
378
|
+
"""
|
|
379
|
+
`;
|
|
380
|
+
},
|
|
381
|
+
};
|
|
382
|
+
// ─── roocode ─────────────────────────────────────────────────────────────
|
|
383
|
+
/** RooCode: .roo/commands/opsx-<id>.md — markdown header */
|
|
384
|
+
const roocodeAdapter = {
|
|
385
|
+
toolId: 'roocode',
|
|
386
|
+
hasSkill: false,
|
|
387
|
+
getCommandPath(id) { return join('.roo', 'commands', `opsx-${id}.md`); },
|
|
388
|
+
formatCommand(meta) {
|
|
389
|
+
return `# ${meta.name}
|
|
390
|
+
|
|
391
|
+
${meta.description}
|
|
392
|
+
|
|
393
|
+
${meta.body}
|
|
394
|
+
`;
|
|
395
|
+
},
|
|
396
|
+
};
|
|
397
|
+
// ─── Detection map ───────────────────────────────────────────────────────
|
|
109
398
|
const ALL_ADAPTERS = [
|
|
110
399
|
claudeAdapter,
|
|
111
400
|
cursorAdapter,
|
|
112
401
|
windsurfAdapter,
|
|
113
402
|
clineAdapter,
|
|
114
403
|
continueAdapter,
|
|
404
|
+
amazonqAdapter,
|
|
405
|
+
antigravityAdapter,
|
|
406
|
+
auggieAdapter,
|
|
407
|
+
codebuddyAdapter,
|
|
408
|
+
codexAdapter,
|
|
409
|
+
costrictAdapter,
|
|
410
|
+
crushAdapter,
|
|
411
|
+
factoryAdapter,
|
|
412
|
+
geminiAdapter,
|
|
413
|
+
githubcopilotAdapter,
|
|
414
|
+
iflowAdapter,
|
|
415
|
+
kilocodeAdapter,
|
|
416
|
+
kiroAdapter,
|
|
417
|
+
opencodeAdapter,
|
|
418
|
+
piAdapter,
|
|
419
|
+
qoderAdapter,
|
|
420
|
+
qwenAdapter,
|
|
421
|
+
roocodeAdapter,
|
|
115
422
|
];
|
|
116
423
|
/** Detect which editors are installed by checking their config directories */
|
|
117
424
|
export function detectEditors(projectRoot) {
|
|
@@ -121,11 +428,35 @@ export function detectEditors(projectRoot) {
|
|
|
121
428
|
['.windsurf', windsurfAdapter],
|
|
122
429
|
['.clinerules', clineAdapter],
|
|
123
430
|
['.continue', continueAdapter],
|
|
431
|
+
['.amazonq', amazonqAdapter],
|
|
432
|
+
['.agent', antigravityAdapter],
|
|
433
|
+
['.augment', auggieAdapter],
|
|
434
|
+
['.codebuddy', codebuddyAdapter],
|
|
435
|
+
['.cospec', costrictAdapter],
|
|
436
|
+
['.crush', crushAdapter],
|
|
437
|
+
['.factory', factoryAdapter],
|
|
438
|
+
['.gemini', geminiAdapter],
|
|
439
|
+
['.github', githubcopilotAdapter],
|
|
440
|
+
['.iflow', iflowAdapter],
|
|
441
|
+
['.kilocode', kilocodeAdapter],
|
|
442
|
+
['.kiro', kiroAdapter],
|
|
443
|
+
['.opencode', opencodeAdapter],
|
|
444
|
+
['.pi', piAdapter],
|
|
445
|
+
['.qoder', qoderAdapter],
|
|
446
|
+
['.qwen', qwenAdapter],
|
|
447
|
+
['.roo', roocodeAdapter],
|
|
124
448
|
];
|
|
125
449
|
return checks
|
|
126
450
|
.filter(([dir]) => existsSync(join(projectRoot, dir)))
|
|
127
451
|
.map(([, adapter]) => adapter);
|
|
128
452
|
}
|
|
453
|
+
// ─── Codex is global — detect separately ─────────────────────────────────
|
|
454
|
+
/** Detect Codex by checking if CODEX_HOME or ~/.codex exists */
|
|
455
|
+
export function detectCodex() {
|
|
456
|
+
const codexHome = process.env.CODEX_HOME?.trim() || join(homedir(), '.codex');
|
|
457
|
+
return existsSync(codexHome) ? codexAdapter : null;
|
|
458
|
+
}
|
|
459
|
+
// ─── Install helpers ───────────────────────────────────────────────────────
|
|
129
460
|
/** Build the shared command metadata */
|
|
130
461
|
export function buildCommandMeta(body) {
|
|
131
462
|
return {
|