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 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 Claude Code
37
+ ### In Your AI Coding Assistant
24
38
 
25
39
  ```bash
26
- /opsx:e2e my-feature # Primary command (OpenSpec convention)
27
- /openspec-e2e # Alternative from skill
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 # Update CLI and skill to latest version
35
- openspec-pw doctor # Check prerequisites
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** with Playwright MCP configured
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. Installs Playwright MCP globally via `claude mcp add`
70
- 2. Installs `/opsx:e2e` command and `/openspec-e2e` skill
71
- 3. Generates `tests/playwright/seed.spec.ts`, `auth.setup.ts`, `credentials.yaml`
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`. Restart Claude Code after setup to activate.
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
- openspec-pw (CLI - setup only)
120
- ├── Installs Playwright agents (.github/)
121
- ├── Installs Playwright MCP globally via claude mcp add
122
- ├── Installs Claude Code skill (/openspec-e2e)
123
- └── Installs command (/opsx:e2e)
124
-
125
- /openspec-e2e (Claude Code skill - runs in Claude)
126
- ├── Reads OpenSpec specs
127
- ├── Triggers Playwright agents via MCP
128
- └── Generates E2E verification report
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
- ### 在 Claude Code 中
37
+ ### 在 AI 编码助手中
24
38
 
25
39
  ```bash
26
- /opsx:e2e my-feature # 主命令(遵循 OpenSpec 规范)
27
- /openspec-e2e # 来自 skill 的别名
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 和 skill 到最新版本
48
+ openspec-pw update # 更新 CLI 和命令到最新版本
35
49
  openspec-pw doctor # 检查前置条件
36
50
  ```
37
51
 
38
52
  ## 工作原理
39
53
 
40
54
  ```
41
- /openspec-e2e <change-name>
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. **Claude Code** 已配置 Playwright MCP
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. 通过 `claude mcp add` 全局安装 Playwright MCP
70
- 2. 安装 `/opsx:e2e` 命令和 `/openspec-e2e` skill
71
- 3. 生成 `tests/playwright/seed.spec.ts`、`auth.setup.ts`、`credentials.yaml`
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` 全局安装。设置后需重启 Claude Code 生效。
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 */
@@ -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
- /** All supported adapters */
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 {