openspec-playwright 0.1.39 → 0.1.41

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.
@@ -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
 
@@ -12,11 +13,21 @@ export function escapeYamlValue(value: string): string {
12
13
  return value;
13
14
  }
14
15
 
15
- /** Format tags as YAML inline array */
16
+ /** Format tags as YAML inline array (escaped) */
16
17
  export function formatTagsArray(tags: string[]): string {
17
18
  return `[${tags.map(t => escapeYamlValue(t)).join(', ')}]`;
18
19
  }
19
20
 
21
+ /** Format tags as YAML inline array (plain, no escaping) */
22
+ function formatTagsPlain(tags: string[]): string {
23
+ return `[${tags.join(', ')}]`;
24
+ }
25
+
26
+ /** Transform /opsx: to /opsx- for OpenCode */
27
+ function transformToHyphenCommands(text: string): string {
28
+ return text.replace(/\/opsx:/g, '/opsx-');
29
+ }
30
+
20
31
  /** Command metadata shared across editors */
21
32
  export interface CommandMeta {
22
33
  id: string;
@@ -29,23 +40,19 @@ export interface CommandMeta {
29
40
 
30
41
  /** Editor adapter — Strategy Pattern */
31
42
  export interface EditorAdapter {
32
- /** Tool identifier */
33
43
  toolId: string;
34
- /** Whether this editor supports SKILL.md */
35
44
  hasSkill: boolean;
36
- /** Get the command file path relative to project root */
37
45
  getCommandPath(commandId: string): string;
38
- /** Format the complete file content */
39
46
  formatCommand(meta: CommandMeta): string;
40
47
  }
41
48
 
49
+ // ─── Claude Code ──────────────────────────────────────────────────────────────
50
+
42
51
  /** Claude Code: .claude/commands/opsx/<id>.md + SKILL.md */
43
52
  const claudeAdapter: EditorAdapter = {
44
53
  toolId: 'claude',
45
54
  hasSkill: true,
46
- getCommandPath(id: string) {
47
- return join('.claude', 'commands', 'opsx', `${id}.md`);
48
- },
55
+ getCommandPath(id) { return join('.claude', 'commands', 'opsx', `${id}.md`); },
49
56
  formatCommand(meta) {
50
57
  return `---
51
58
  name: ${escapeYamlValue(meta.name)}
@@ -59,13 +66,13 @@ ${meta.body}
59
66
  },
60
67
  };
61
68
 
69
+ // ─── Cursor ─────────────────────────────────────────────────────────────────
70
+
62
71
  /** Cursor: .cursor/commands/opsx-<id>.md */
63
72
  const cursorAdapter: EditorAdapter = {
64
73
  toolId: 'cursor',
65
74
  hasSkill: false,
66
- getCommandPath(id: string) {
67
- return join('.cursor', 'commands', `opsx-${id}.md`);
68
- },
75
+ getCommandPath(id) { return join('.cursor', 'commands', `opsx-${id}.md`); },
69
76
  formatCommand(meta) {
70
77
  return `---
71
78
  name: /opsx-${meta.id}
@@ -79,13 +86,13 @@ ${meta.body}
79
86
  },
80
87
  };
81
88
 
89
+ // ─── Windsurf ────────────────────────────────────────────────────────────────
90
+
82
91
  /** Windsurf: .windsurf/workflows/opsx-<id>.md */
83
92
  const windsurfAdapter: EditorAdapter = {
84
93
  toolId: 'windsurf',
85
94
  hasSkill: false,
86
- getCommandPath(id: string) {
87
- return join('.windsurf', 'workflows', `opsx-${id}.md`);
88
- },
95
+ getCommandPath(id) { return join('.windsurf', 'workflows', `opsx-${id}.md`); },
89
96
  formatCommand(meta) {
90
97
  return `---
91
98
  name: ${escapeYamlValue(meta.name)}
@@ -99,13 +106,13 @@ ${meta.body}
99
106
  },
100
107
  };
101
108
 
109
+ // ─── Cline ──────────────────────────────────────────────────────────────────
110
+
102
111
  /** Cline: .clinerules/workflows/opsx-<id>.md — markdown header only */
103
112
  const clineAdapter: EditorAdapter = {
104
113
  toolId: 'cline',
105
114
  hasSkill: false,
106
- getCommandPath(id: string) {
107
- return join('.clinerules', 'workflows', `opsx-${id}.md`);
108
- },
115
+ getCommandPath(id) { return join('.clinerules', 'workflows', `opsx-${id}.md`); },
109
116
  formatCommand(meta) {
110
117
  return `# ${meta.name}
111
118
 
@@ -116,13 +123,13 @@ ${meta.body}
116
123
  },
117
124
  };
118
125
 
126
+ // ─── Continue ────────────────────────────────────────────────────────────────
127
+
119
128
  /** Continue: .continue/prompts/opsx-<id>.prompt */
120
129
  const continueAdapter: EditorAdapter = {
121
130
  toolId: 'continue',
122
131
  hasSkill: false,
123
- getCommandPath(id: string) {
124
- return join('.continue', 'prompts', `opsx-${id}.prompt`);
125
- },
132
+ getCommandPath(id) { return join('.continue', 'prompts', `opsx-${id}.prompt`); },
126
133
  formatCommand(meta) {
127
134
  return `---
128
135
  name: opsx-${meta.id}
@@ -135,13 +142,353 @@ ${meta.body}
135
142
  },
136
143
  };
137
144
 
138
- /** All supported adapters */
145
+ // ─── amazon-q ─────────────────────────────────────────────────────────────
146
+
147
+ /** Amazon Q: .amazonq/prompts/opsx-<id>.md */
148
+ const amazonqAdapter: EditorAdapter = {
149
+ toolId: 'amazon-q',
150
+ hasSkill: false,
151
+ getCommandPath(id) { return join('.amazonq', 'prompts', `opsx-${id}.md`); },
152
+ formatCommand(meta) {
153
+ return `---
154
+ description: ${meta.description}
155
+ ---
156
+
157
+ ${meta.body}
158
+ `;
159
+ },
160
+ };
161
+
162
+ // ─── antigravity ──────────────────────────────────────────────────────────
163
+
164
+ /** Antigravity: .agent/workflows/opsx-<id>.md */
165
+ const antigravityAdapter: EditorAdapter = {
166
+ toolId: 'antigravity',
167
+ hasSkill: false,
168
+ getCommandPath(id) { return join('.agent', 'workflows', `opsx-${id}.md`); },
169
+ formatCommand(meta) {
170
+ return `---
171
+ description: ${meta.description}
172
+ ---
173
+
174
+ ${meta.body}
175
+ `;
176
+ },
177
+ };
178
+
179
+ // ─── auggie ────────────────────────────────────────────────────────────────
180
+
181
+ /** Auggie: .augment/commands/opsx-<id>.md */
182
+ const auggieAdapter: EditorAdapter = {
183
+ toolId: 'auggie',
184
+ hasSkill: false,
185
+ getCommandPath(id) { return join('.augment', 'commands', `opsx-${id}.md`); },
186
+ formatCommand(meta) {
187
+ return `---
188
+ description: ${meta.description}
189
+ argument-hint: command arguments
190
+ ---
191
+
192
+ ${meta.body}
193
+ `;
194
+ },
195
+ };
196
+
197
+ // ─── codebuddy ─────────────────────────────────────────────────────────────
198
+
199
+ /** CodeBuddy: .codebuddy/commands/opsx/<id>.md */
200
+ const codebuddyAdapter: EditorAdapter = {
201
+ toolId: 'codebuddy',
202
+ hasSkill: false,
203
+ getCommandPath(id) { return join('.codebuddy', 'commands', 'opsx', `${id}.md`); },
204
+ formatCommand(meta) {
205
+ return `---
206
+ name: ${meta.name}
207
+ description: "${meta.description}"
208
+ argument-hint: "[command arguments]"
209
+ ---
210
+
211
+ ${meta.body}
212
+ `;
213
+ },
214
+ };
215
+
216
+ // ─── codex ────────────────────────────────────────────────────────────────
217
+
218
+ /** Codex: <CODEX_HOME>/prompts/opsx-<id>.md — global scope */
219
+ const codexAdapter: EditorAdapter = {
220
+ toolId: 'codex',
221
+ hasSkill: false,
222
+ getCommandPath(id) {
223
+ const codexHome = process.env.CODEX_HOME?.trim() || join(homedir(), '.codex');
224
+ return join(codexHome, 'prompts', `opsx-${id}.md`);
225
+ },
226
+ formatCommand(meta) {
227
+ return `---
228
+ description: ${meta.description}
229
+ argument-hint: command arguments
230
+ ---
231
+
232
+ ${meta.body}
233
+ `;
234
+ },
235
+ };
236
+
237
+ // ─── costrict ─────────────────────────────────────────────────────────────
238
+
239
+ /** CoStrict: .cospec/openspec/commands/opsx-<id>.md */
240
+ const costrictAdapter: EditorAdapter = {
241
+ toolId: 'costrict',
242
+ hasSkill: false,
243
+ getCommandPath(id) { return join('.cospec', 'openspec', 'commands', `opsx-${id}.md`); },
244
+ formatCommand(meta) {
245
+ return `---
246
+ description: "${meta.description}"
247
+ argument-hint: command arguments
248
+ ---
249
+
250
+ ${meta.body}
251
+ `;
252
+ },
253
+ };
254
+
255
+ // ─── crush ────────────────────────────────────────────────────────────────
256
+
257
+ /** Crush: .crush/commands/opsx/<id>.md — raw values, no escaping */
258
+ const crushAdapter: EditorAdapter = {
259
+ toolId: 'crush',
260
+ hasSkill: false,
261
+ getCommandPath(id) { return join('.crush', 'commands', 'opsx', `${id}.md`); },
262
+ formatCommand(meta) {
263
+ return `---
264
+ name: ${meta.name}
265
+ description: ${meta.description}
266
+ category: ${meta.category}
267
+ tags: ${formatTagsPlain(meta.tags)}
268
+ ---
269
+
270
+ ${meta.body}
271
+ `;
272
+ },
273
+ };
274
+
275
+ // ─── factory ───────────────────────────────────────────────────────────────
276
+
277
+ /** Factory Droid: .factory/commands/opsx-<id>.md */
278
+ const factoryAdapter: EditorAdapter = {
279
+ toolId: 'factory',
280
+ hasSkill: false,
281
+ getCommandPath(id) { return join('.factory', 'commands', `opsx-${id}.md`); },
282
+ formatCommand(meta) {
283
+ return `---
284
+ description: ${meta.description}
285
+ argument-hint: command arguments
286
+ ---
287
+
288
+ ${meta.body}
289
+ `;
290
+ },
291
+ };
292
+
293
+ // ─── gemini ────────────────────────────────────────────────────────────────
294
+
295
+ /** Gemini CLI: .gemini/commands/opsx/<id>.toml */
296
+ const geminiAdapter: EditorAdapter = {
297
+ toolId: 'gemini',
298
+ hasSkill: false,
299
+ getCommandPath(id) { return join('.gemini', 'commands', 'opsx', `${id}.toml`); },
300
+ formatCommand(meta) {
301
+ return `description = "${meta.description}"
302
+
303
+ prompt = """
304
+ ${meta.body}
305
+ """
306
+ `;
307
+ },
308
+ };
309
+
310
+ // ─── github-copilot ────────────────────────────────────────────────────────
311
+
312
+ /** GitHub Copilot: .github/prompts/opsx-<id>.prompt.md */
313
+ const githubcopilotAdapter: EditorAdapter = {
314
+ toolId: 'github-copilot',
315
+ hasSkill: false,
316
+ getCommandPath(id) { return join('.github', 'prompts', `opsx-${id}.prompt.md`); },
317
+ formatCommand(meta) {
318
+ return `---
319
+ description: ${meta.description}
320
+ ---
321
+
322
+ ${meta.body}
323
+ `;
324
+ },
325
+ };
326
+
327
+ // ─── iflow ────────────────────────────────────────────────────────────────
328
+
329
+ /** iFlow: .iflow/commands/opsx-<id>.md */
330
+ const iflowAdapter: EditorAdapter = {
331
+ toolId: 'iflow',
332
+ hasSkill: false,
333
+ getCommandPath(id) { return join('.iflow', 'commands', `opsx-${id}.md`); },
334
+ formatCommand(meta) {
335
+ return `---
336
+ name: /opsx-${meta.id}
337
+ id: opsx-${meta.id}
338
+ category: ${meta.category}
339
+ description: ${meta.description}
340
+ ---
341
+
342
+ ${meta.body}
343
+ `;
344
+ },
345
+ };
346
+
347
+ // ─── kilocode ────────────────────────────────────────────────────────────
348
+
349
+ /** Kilo Code: .kilocode/workflows/opsx-<id>.md — body only */
350
+ const kilocodeAdapter: EditorAdapter = {
351
+ toolId: 'kilocode',
352
+ hasSkill: false,
353
+ getCommandPath(id) { return join('.kilocode', 'workflows', `opsx-${id}.md`); },
354
+ formatCommand(meta) {
355
+ return `${meta.body}
356
+ `;
357
+ },
358
+ };
359
+
360
+ // ─── kiro ─────────────────────────────────────────────────────────────────
361
+
362
+ /** Kiro: .kiro/prompts/opsx-<id>.prompt.md */
363
+ const kiroAdapter: EditorAdapter = {
364
+ toolId: 'kiro',
365
+ hasSkill: false,
366
+ getCommandPath(id) { return join('.kiro', 'prompts', `opsx-${id}.prompt.md`); },
367
+ formatCommand(meta) {
368
+ return `---
369
+ description: ${meta.description}
370
+ ---
371
+
372
+ ${meta.body}
373
+ `;
374
+ },
375
+ };
376
+
377
+ // ─── opencode ─────────────────────────────────────────────────────────────
378
+
379
+ /** OpenCode: .opencode/commands/opsx-<id>.md — transforms /opsx: to /opsx- */
380
+ const opencodeAdapter: EditorAdapter = {
381
+ toolId: 'opencode',
382
+ hasSkill: false,
383
+ getCommandPath(id) { return join('.opencode', 'commands', `opsx-${id}.md`); },
384
+ formatCommand(meta) {
385
+ const transformed = transformToHyphenCommands(meta.body);
386
+ return `---
387
+ description: ${meta.description}
388
+ ---
389
+
390
+ ${transformed}
391
+ `;
392
+ },
393
+ };
394
+
395
+ // ─── pi ──────────────────────────────────────────────────────────────────
396
+
397
+ /** Pi: .pi/prompts/opsx-<id>.md */
398
+ const piAdapter: EditorAdapter = {
399
+ toolId: 'pi',
400
+ hasSkill: false,
401
+ getCommandPath(id) { return join('.pi', 'prompts', `opsx-${id}.md`); },
402
+ formatCommand(meta) {
403
+ return `---
404
+ description: ${escapeYamlValue(meta.description)}
405
+ ---
406
+
407
+ ${meta.body}
408
+ `;
409
+ },
410
+ };
411
+
412
+ // ─── qoder ────────────────────────────────────────────────────────────────
413
+
414
+ /** Qoder: .qoder/commands/opsx/<id>.md — raw values, no escaping */
415
+ const qoderAdapter: EditorAdapter = {
416
+ toolId: 'qoder',
417
+ hasSkill: false,
418
+ getCommandPath(id) { return join('.qoder', 'commands', 'opsx', `${id}.md`); },
419
+ formatCommand(meta) {
420
+ return `---
421
+ name: ${meta.name}
422
+ description: ${meta.description}
423
+ category: ${meta.category}
424
+ tags: ${formatTagsPlain(meta.tags)}
425
+ ---
426
+
427
+ ${meta.body}
428
+ `;
429
+ },
430
+ };
431
+
432
+ // ─── qwen ────────────────────────────────────────────────────────────────
433
+
434
+ /** Qwen Code: .qwen/commands/opsx-<id>.toml */
435
+ const qwenAdapter: EditorAdapter = {
436
+ toolId: 'qwen',
437
+ hasSkill: false,
438
+ getCommandPath(id) { return join('.qwen', 'commands', `opsx-${id}.toml`); },
439
+ formatCommand(meta) {
440
+ return `description = "${meta.description}"
441
+
442
+ prompt = """
443
+ ${meta.body}
444
+ """
445
+ `;
446
+ },
447
+ };
448
+
449
+ // ─── roocode ─────────────────────────────────────────────────────────────
450
+
451
+ /** RooCode: .roo/commands/opsx-<id>.md — markdown header */
452
+ const roocodeAdapter: EditorAdapter = {
453
+ toolId: 'roocode',
454
+ hasSkill: false,
455
+ getCommandPath(id) { return join('.roo', 'commands', `opsx-${id}.md`); },
456
+ formatCommand(meta) {
457
+ return `# ${meta.name}
458
+
459
+ ${meta.description}
460
+
461
+ ${meta.body}
462
+ `;
463
+ },
464
+ };
465
+
466
+ // ─── Detection map ───────────────────────────────────────────────────────
467
+
139
468
  const ALL_ADAPTERS: EditorAdapter[] = [
140
469
  claudeAdapter,
141
470
  cursorAdapter,
142
471
  windsurfAdapter,
143
472
  clineAdapter,
144
473
  continueAdapter,
474
+ amazonqAdapter,
475
+ antigravityAdapter,
476
+ auggieAdapter,
477
+ codebuddyAdapter,
478
+ codexAdapter,
479
+ costrictAdapter,
480
+ crushAdapter,
481
+ factoryAdapter,
482
+ geminiAdapter,
483
+ githubcopilotAdapter,
484
+ iflowAdapter,
485
+ kilocodeAdapter,
486
+ kiroAdapter,
487
+ opencodeAdapter,
488
+ piAdapter,
489
+ qoderAdapter,
490
+ qwenAdapter,
491
+ roocodeAdapter,
145
492
  ];
146
493
 
147
494
  /** Detect which editors are installed by checking their config directories */
@@ -152,6 +499,23 @@ export function detectEditors(projectRoot: string): EditorAdapter[] {
152
499
  ['.windsurf', windsurfAdapter],
153
500
  ['.clinerules', clineAdapter],
154
501
  ['.continue', continueAdapter],
502
+ ['.amazonq', amazonqAdapter],
503
+ ['.agent', antigravityAdapter],
504
+ ['.augment', auggieAdapter],
505
+ ['.codebuddy', codebuddyAdapter],
506
+ ['.cospec', costrictAdapter],
507
+ ['.crush', crushAdapter],
508
+ ['.factory', factoryAdapter],
509
+ ['.gemini', geminiAdapter],
510
+ ['.github', githubcopilotAdapter],
511
+ ['.iflow', iflowAdapter],
512
+ ['.kilocode', kilocodeAdapter],
513
+ ['.kiro', kiroAdapter],
514
+ ['.opencode', opencodeAdapter],
515
+ ['.pi', piAdapter],
516
+ ['.qoder', qoderAdapter],
517
+ ['.qwen', qwenAdapter],
518
+ ['.roo', roocodeAdapter],
155
519
  ];
156
520
 
157
521
  return checks
@@ -159,6 +523,16 @@ export function detectEditors(projectRoot: string): EditorAdapter[] {
159
523
  .map(([, adapter]) => adapter);
160
524
  }
161
525
 
526
+ // ─── Codex is global — detect separately ─────────────────────────────────
527
+
528
+ /** Detect Codex by checking if CODEX_HOME or ~/.codex exists */
529
+ export function detectCodex(): EditorAdapter | null {
530
+ const codexHome = process.env.CODEX_HOME?.trim() || join(homedir(), '.codex');
531
+ return existsSync(codexHome) ? codexAdapter : null;
532
+ }
533
+
534
+ // ─── Install helpers ───────────────────────────────────────────────────────
535
+
162
536
  /** Build the shared command metadata */
163
537
  export function buildCommandMeta(body: string): CommandMeta {
164
538
  return {
@@ -11,7 +11,7 @@ import { fileURLToPath } from 'url';
11
11
  import chalk from 'chalk';
12
12
  import { readFile } from 'fs/promises';
13
13
  import { syncMcpTools } from './mcpSync.js';
14
- import { detectEditors, installForAllEditors, installSkill, claudeAdapter } from './editors.js';
14
+ import { detectEditors, detectCodex, installForAllEditors, installSkill, claudeAdapter } from './editors.js';
15
15
 
16
16
  const TEMPLATE_DIR = fileURLToPath(new URL('../../templates', import.meta.url));
17
17
  const SCHEMA_DIR = fileURLToPath(new URL('../../schemas', import.meta.url));
@@ -85,9 +85,11 @@ export async function init(options: InitOptions) {
85
85
  // 4. Install E2E commands for detected editors
86
86
  console.log(chalk.blue('\n─── Installing E2E Commands ───'));
87
87
  const detected = detectEditors(projectRoot);
88
- if (detected.length > 0) {
88
+ const codex = detectCodex();
89
+ const adapters = codex ? [...detected, codex] : detected;
90
+ if (adapters.length > 0) {
89
91
  const body = await readFile(CMD_BODY_SRC, 'utf-8');
90
- installForAllEditors(body, detected, projectRoot);
92
+ installForAllEditors(body, adapters, projectRoot);
91
93
  } else {
92
94
  const body = await readFile(CMD_BODY_SRC, 'utf-8');
93
95
  installForAllEditors(body, [claudeAdapter], projectRoot);
@@ -14,7 +14,7 @@ import { fileURLToPath } from 'url';
14
14
  import chalk from 'chalk';
15
15
  import * as tar from 'tar';
16
16
  import { syncMcpTools } from './mcpSync.js';
17
- import { detectEditors, installForAllEditors, installSkill } from './editors.js';
17
+ import { detectEditors, detectCodex, installForAllEditors, installSkill } from './editors.js';
18
18
 
19
19
  const CMD_BODY_SRC = fileURLToPath(new URL('../../.claude/commands/opsx/e2e-body.md', import.meta.url));
20
20
  const SCHEMA_DIR = fileURLToPath(new URL('../../schemas', import.meta.url));
@@ -84,7 +84,9 @@ export async function update(options: UpdateOptions) {
84
84
  const schemaSrc = join(tmpDir, 'schemas', 'playwright-e2e');
85
85
 
86
86
  // Install commands for all detected editors
87
- const adapters = detectEditors(projectRoot);
87
+ const detected = detectEditors(projectRoot);
88
+ const codex = detectCodex();
89
+ const adapters = codex ? [...detected, codex] : detected;
88
90
  if (adapters.length > 0 && existsSync(bodySrc)) {
89
91
  const body = readFileSync(bodySrc, 'utf-8');
90
92
  installForAllEditors(body, adapters, projectRoot);
Binary file