openspec-playwright 0.3.72 → 0.3.74

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.
Files changed (57) hide show
  1. package/README.md +11 -10
  2. package/README.zh-CN.md +5 -4
  3. package/dist/commands/doctor.js +65 -1
  4. package/dist/commands/doctor.js.map +1 -1
  5. package/dist/commands/editors/adapters/claude.d.ts +5 -0
  6. package/dist/commands/editors/adapters/claude.js +79 -0
  7. package/dist/commands/editors/adapters/claude.js.map +1 -0
  8. package/dist/commands/editors/adapters/cline.d.ts +15 -0
  9. package/dist/commands/editors/adapters/cline.js +63 -0
  10. package/dist/commands/editors/adapters/cline.js.map +1 -0
  11. package/dist/commands/editors/adapters/cursor.d.ts +15 -0
  12. package/dist/commands/editors/adapters/cursor.js +79 -0
  13. package/dist/commands/editors/adapters/cursor.js.map +1 -0
  14. package/dist/commands/editors/adapters/dsh.d.ts +27 -0
  15. package/dist/commands/editors/adapters/dsh.js +67 -0
  16. package/dist/commands/editors/adapters/dsh.js.map +1 -0
  17. package/dist/commands/editors/adapters/omp.d.ts +24 -0
  18. package/dist/commands/editors/adapters/omp.js +73 -0
  19. package/dist/commands/editors/adapters/omp.js.map +1 -0
  20. package/dist/commands/editors/adapters/opencode.d.ts +8 -0
  21. package/dist/commands/editors/adapters/opencode.js +133 -0
  22. package/dist/commands/editors/adapters/opencode.js.map +1 -0
  23. package/dist/commands/editors/adapters/pi.d.ts +27 -0
  24. package/dist/commands/editors/adapters/pi.js +64 -0
  25. package/dist/commands/editors/adapters/pi.js.map +1 -0
  26. package/dist/commands/editors/project-rules.d.ts +69 -0
  27. package/dist/commands/editors/project-rules.js +246 -0
  28. package/dist/commands/editors/project-rules.js.map +1 -0
  29. package/dist/commands/editors/registry.d.ts +12 -0
  30. package/dist/commands/editors/registry.js +59 -0
  31. package/dist/commands/editors/registry.js.map +1 -0
  32. package/dist/commands/editors/shared.d.ts +37 -0
  33. package/dist/commands/editors/shared.js +92 -0
  34. package/dist/commands/editors/shared.js.map +1 -0
  35. package/dist/commands/editors/tool-selection.d.ts +17 -0
  36. package/dist/commands/editors/tool-selection.js +60 -0
  37. package/dist/commands/editors/tool-selection.js.map +1 -0
  38. package/dist/commands/editors/types.d.ts +82 -0
  39. package/dist/commands/editors/types.js +41 -0
  40. package/dist/commands/editors/types.js.map +1 -0
  41. package/dist/commands/editors.d.ts +32 -309
  42. package/dist/commands/editors.js +38 -960
  43. package/dist/commands/editors.js.map +1 -1
  44. package/dist/commands/init.js +12 -15
  45. package/dist/commands/init.js.map +1 -1
  46. package/dist/commands/uninstall.js +7 -1
  47. package/dist/commands/uninstall.js.map +1 -1
  48. package/dist/commands/update.d.ts +8 -0
  49. package/dist/commands/update.js +13 -2
  50. package/dist/commands/update.js.map +1 -1
  51. package/dist/shared/codegraph.d.ts +20 -0
  52. package/dist/shared/codegraph.js +46 -0
  53. package/dist/shared/codegraph.js.map +1 -0
  54. package/dist/shared/index.d.ts +2 -0
  55. package/dist/shared/index.js +1 -0
  56. package/dist/shared/index.js.map +1 -1
  57. package/package.json +4 -3
@@ -0,0 +1,246 @@
1
+ /**
2
+ * Project rules file management: OPENSPEC marker blocks inside
3
+ * AGENTS.md (single source of truth) and the thin CLAUDE.md wrapper for
4
+ * Claude Code (@AGENTS.md import + CodeGraph-first guidance).
5
+ */
6
+ import { existsSync, lstatSync, rmSync, readFileSync, writeFileSync, } from "fs";
7
+ import { join, basename } from "path";
8
+ import chalk from "chalk";
9
+ import { claudeAdapter } from "./adapters/claude.js";
10
+ import { opencodeAdapter, readOpenCodeInstructions } from "./adapters/opencode.js";
11
+ // ─── Project rules file (CLAUDE.md / AGENTS.md) ──────────────────────────
12
+ /**
13
+ * Read the OPENSPEC marker block from a rules file, or `null` when the file
14
+ * is missing / has no markers. Used by drift detection and update to decide
15
+ * whether a rules file needs rewriting.
16
+ */
17
+ export function readOpenSpecBlock(projectRoot, adapter) {
18
+ const dest = adapter.projectRulesPath(projectRoot);
19
+ if (!existsSync(dest))
20
+ return null;
21
+ const content = readFileSync(dest, "utf-8");
22
+ const startIdx = content.indexOf("<!-- OPENSPEC:START -->");
23
+ const endIdx = content.indexOf("<!-- OPENSPEC:END -->");
24
+ if (startIdx === -1 || endIdx === -1)
25
+ return null;
26
+ return content.slice(startIdx + "<!-- OPENSPEC:START -->".length, endIdx).trim();
27
+ }
28
+ /**
29
+ * Whether a rules file's OPENSPEC block matches the expected content.
30
+ * A missing file or absent/truncated markers counts as "does not match"
31
+ * (the caller will rewrite it), which keeps update idempotent but safe.
32
+ */
33
+ export function blockMatchesExpected(projectRoot, adapter, expected) {
34
+ const block = readOpenSpecBlock(projectRoot, adapter);
35
+ if (block === null)
36
+ return false;
37
+ return block === expected.trim();
38
+ }
39
+ /**
40
+ * Install employee-grade standards into the editor's rules file
41
+ * (CLAUDE.md for Claude, AGENTS.md for OpenCode, Cline, and Cursor). Wraps content in
42
+ * `<!-- OPENSPEC:START -->` / `<!-- OPENSPEC:END -->` markers so future
43
+ * updates can replace the block without touching the rest of the file.
44
+ */
45
+ export function installOpenSpecBlock(projectRoot, standardsContent, adapter = claudeAdapter) {
46
+ const dest = adapter.projectRulesPath(projectRoot);
47
+ const fileLabel = basename(dest);
48
+ const markerStart = "<!-- OPENSPEC:START -->";
49
+ const markerEnd = "<!-- OPENSPEC:END -->";
50
+ if (!existsSync(dest)) {
51
+ const projName = projectRoot.split("/").pop() ?? "Project";
52
+ const content = `# ${projName}\n\n${markerStart}\n\n${standardsContent.trim()}\n\n${markerEnd}\n`;
53
+ writeFileSync(dest, content);
54
+ console.log(chalk.green(` ✓ ${fileLabel}: created with employee-grade standards`));
55
+ return;
56
+ }
57
+ const existing = readFileSync(dest, "utf-8");
58
+ const hasStart = existing.includes(markerStart);
59
+ const hasEnd = existing.includes(markerEnd);
60
+ if (hasStart && hasEnd) {
61
+ const startIdx = existing.indexOf(markerStart);
62
+ const endIdx = existing.indexOf(markerEnd) + markerEnd.length;
63
+ const before = existing.slice(0, startIdx).trimEnd();
64
+ const after = existing.slice(endIdx);
65
+ const updated = before +
66
+ "\n" +
67
+ markerStart +
68
+ "\n\n" +
69
+ standardsContent.trim() +
70
+ "\n\n" +
71
+ markerEnd +
72
+ after;
73
+ writeFileSync(dest, updated);
74
+ console.log(chalk.green(` ✓ ${fileLabel}: updated employee-grade standards (markers preserved, content refreshed)`));
75
+ }
76
+ else if (!hasStart && !hasEnd) {
77
+ const updated = existing.trim() +
78
+ "\n\n" +
79
+ markerStart +
80
+ "\n\n" +
81
+ standardsContent.trim() +
82
+ "\n\n" +
83
+ markerEnd +
84
+ "\n";
85
+ writeFileSync(dest, updated);
86
+ console.log(chalk.green(` ✓ ${fileLabel}: appended employee-grade standards with markers`));
87
+ }
88
+ else {
89
+ // Incomplete markers (only START, or only END) — corrupted tool territory.
90
+ // Keep everything before the first marker (user content), discard the
91
+ // truncated tool output after it, and write a clean complete block so
92
+ // `doctor`/`update` converge instead of dead-ending on a skipped file.
93
+ const firstIdx = hasStart
94
+ ? existing.indexOf(markerStart)
95
+ : existing.indexOf(markerEnd);
96
+ const header = existing.slice(0, firstIdx).trimEnd();
97
+ const updated = header +
98
+ "\n\n" +
99
+ markerStart +
100
+ "\n\n" +
101
+ standardsContent.trim() +
102
+ "\n\n" +
103
+ markerEnd +
104
+ "\n";
105
+ writeFileSync(dest, updated);
106
+ console.log(chalk.green(` ✓ ${fileLabel}: repaired incomplete OPENSPEC markers with employee-grade standards`));
107
+ }
108
+ }
109
+ /**
110
+ * CodeGraph-first guidance prepended to the Claude wrapper so the model sees
111
+ * it in the main rules file instead of relying on the AGENTS.md import
112
+ * (imported content ranks lower and is treated as optional by the model).
113
+ */
114
+ const CODE_GRAPH_FIRST_BLOCK = `## CodeGraph 优先 🔴
115
+
116
+ 有 \`.codegraph/\` 时:结构性任务(定义/调用链/影响面/流程)默认第一步用 \`codegraph_explore\`,直接用结果回答,别先 grep/read(仅字面文本、已打开文件、结果不足时补查)。不派子 agent 重建索引。无 \`.codegraph/\` 跳过。`;
117
+ /**
118
+ * The expected OPENSPEC block content for a thin CLAUDE.md wrapper
119
+ * (CodeGraph-first guidance + workflow hint + `@AGENTS.md` import). Exported
120
+ * so drift detection / update can compare against it.
121
+ *
122
+ * The `@AGENTS.md` line is Claude Code's documented way to reuse AGENTS.md
123
+ * inside CLAUDE.md — AGENTS.md is NOT read by default ("Claude Code reads
124
+ * CLAUDE.md, not AGENTS.md"). Contract per
125
+ * https://code.claude.com/docs/en/memory.md:
126
+ * - Position is irrelevant — the doc says "@ ... anywhere in your CLAUDE.md"
127
+ * (examples even inline it mid-sentence or in a list item). The one real
128
+ * constraint: the `@` line must NOT sit inside a code span (backticks) or
129
+ * a fenced code block — the resolver skips those. This wrapper keeps the
130
+ * import at the end of the block as a bare line.
131
+ * - The path resolves relative to the importing CLAUDE.md; import recursion
132
+ * is capped at 4 hops.
133
+ * - Block-level HTML comments (`<!-- ... -->`) are stripped before context
134
+ * injection, so the OPENSPEC markers vanish while the live `@AGENTS.md`
135
+ * line inside them is still honored.
136
+ */
137
+ export function claudeWrapperStandardsContent() {
138
+ return `${CODE_GRAPH_FIRST_BLOCK}\n\n**工作流**:优先使用 OpenSpec 工作流(/opsx 命令),而非 plan mode。\n\n@AGENTS.md\n`;
139
+ }
140
+ /**
141
+ * Install a thin CLAUDE.md that imports AGENTS.md.
142
+ *
143
+ * Uses the same OPENSPEC:START/END markers as the full standards block so
144
+ * `cleanProjectRules` can remove it uniformly. The CodeGraph-first block is
145
+ * written directly into CLAUDE.md (before the @AGENTS.md import) so Claude
146
+ * Code picks it up without depending on the import.
147
+ *
148
+ * Also handles migration: if CLAUDE.md has an existing OPENSPEC:START block
149
+ * (old format that wrote standards directly to CLAUDE.md), calling
150
+ * `installOpenSpecBlock` replaces the content with the CodeGraph block +
151
+ * `@AGENTS.md` import.
152
+ */
153
+ export function installClaudeWrapper(projectRoot) {
154
+ const dest = join(projectRoot, "CLAUDE.md");
155
+ // CLAUDE.md symlinked (typically → AGENTS.md, the officially documented
156
+ // reuse pattern): AGENTS.md itself is what Claude Code reads, and
157
+ // installProjectRules already keeps the full standards in it. Writing a
158
+ // wrapper here would overwrite them through the symlink (and the wrapper's
159
+ // @AGENTS.md import would self-reference). Skip instead.
160
+ if (existsSync(dest)) {
161
+ if (lstatSync(dest).isSymbolicLink()) {
162
+ console.log(chalk.gray(" - CLAUDE.md is a symlink to AGENTS.md — standards live there, no wrapper needed"));
163
+ return;
164
+ }
165
+ }
166
+ // No-op if our full wrapper (CodeGraph block + @AGENTS.md import) is already
167
+ // in place — content-equal, so a user edit inside the markers is detected.
168
+ // A bare @AGENTS.md without our markers (added by openspec CLI or the user)
169
+ // is left untouched — but tell the user CodeGraph-first won't be written.
170
+ if (existsSync(dest)) {
171
+ const existing = readFileSync(dest, "utf-8");
172
+ const hasMarkers = existing.includes("<!-- OPENSPEC:START -->");
173
+ if (!hasMarkers && /^@AGENTS\.md\r?$/m.test(existing)) {
174
+ console.log(chalk.yellow(" ⚠ CLAUDE.md 是裸 @AGENTS.md 导入(无 OPENSPEC 标记),CodeGraph 优先约束未写入。如需启用:删除该行后重跑 openspec-pw update。"));
175
+ return;
176
+ }
177
+ if (hasMarkers && blockMatchesExpected(projectRoot, claudeAdapter, claudeWrapperStandardsContent())) {
178
+ return;
179
+ }
180
+ }
181
+ // Delegate to installOpenSpecBlock which handles create/update/append
182
+ // with OPENSPEC:START/END markers.
183
+ installOpenSpecBlock(projectRoot, claudeWrapperStandardsContent(), claudeAdapter);
184
+ }
185
+ /**
186
+ * Route employee-grade standards into project rules files.
187
+ *
188
+ * AGENTS.md is always the single source of truth, regardless of which
189
+ * editors are detected. If Claude is in use, a thin CLAUDE.md wrapper
190
+ * with `@AGENTS.md` import is created so Claude loads AGENTS.md as
191
+ * its project rules. Cline and Cursor auto-detect AGENTS.md natively — no
192
+ * wrapper needed.
193
+ */
194
+ export function installProjectRules(projectRoot, standardsContent, detected) {
195
+ if (detected.length === 0)
196
+ return;
197
+ // AGENTS.md is always the single source of truth
198
+ installOpenSpecBlock(projectRoot, standardsContent, opencodeAdapter);
199
+ // Thin CLAUDE.md with @AGENTS.md import if Claude is in use
200
+ if (detected.some((a) => a.id === "claude")) {
201
+ installClaudeWrapper(projectRoot);
202
+ }
203
+ // Register AGENTS.md in opencode.json for OpenCode
204
+ if (detected.some((a) => a.id === "opencode") && opencodeAdapter.registerInstructions) {
205
+ const existing = readOpenCodeInstructions(projectRoot);
206
+ const next = Array.from(new Set([...(existing ?? []), "AGENTS.md"]));
207
+ opencodeAdapter.registerInstructions(projectRoot, next);
208
+ }
209
+ }
210
+ /** Remove all OpenSpec marker blocks from AGENTS.md (always) and CLAUDE.md (for claude adapter). */
211
+ export function cleanProjectRules(adapter, projectRoot) {
212
+ // AGENTS.md always has the employee standards (SSOT)
213
+ removeMarkersFromFile(join(projectRoot, "AGENTS.md"), "AGENTS.md");
214
+ // CLAUDE.md may have the wrapper import if Claude is detected
215
+ if (adapter.id === "claude") {
216
+ removeMarkersFromFile(adapter.projectRulesPath(projectRoot), basename(adapter.projectRulesPath(projectRoot)));
217
+ }
218
+ }
219
+ /** Remove OpenSpec marker blocks from a single file. Only edits within markers. */
220
+ function removeMarkersFromFile(dest, fileLabel) {
221
+ if (!existsSync(dest)) {
222
+ console.log(chalk.gray(` - ${fileLabel} not found, skipping`));
223
+ return;
224
+ }
225
+ const existing = readFileSync(dest, "utf-8");
226
+ if (!existing.includes("<!-- OPENSPEC:START -->")) {
227
+ console.log(chalk.gray(` - No OpenSpec markers found in ${fileLabel}`));
228
+ return;
229
+ }
230
+ // Remove markers and their content, consuming surrounding whitespace.
231
+ // Then collapse runs of 3+ blank lines to at most 2 for a clean result.
232
+ let updated = existing.replace(/\s*<!-- OPENSPEC:START -->[\s\S]*?<!-- OPENSPEC:END -->\s*/g, "\n\n").replace(/\n{3,}/g, "\n\n").trim();
233
+ // Delete empty file rather than leaving a ghost.
234
+ if (updated === "") {
235
+ rmSync(dest);
236
+ console.log(chalk.green(` ✓ Removed empty ${fileLabel}`));
237
+ return;
238
+ }
239
+ writeFileSync(dest, updated + "\n");
240
+ console.log(chalk.green(` ✓ Removed OpenSpec markers from ${fileLabel}`));
241
+ }
242
+ /** Read the employee-grade standards source file (empty string if missing). */
243
+ export function readEmployeeStandards(srcPath) {
244
+ return existsSync(srcPath) ? readFileSync(srcPath, "utf-8") : "";
245
+ }
246
+ //# sourceMappingURL=project-rules.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"project-rules.js","sourceRoot":"","sources":["../../../src/commands/editors/project-rules.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ import type { CommandMeta, EditorAdapter, EditorId } from "./types.js";
2
+ export declare function getAdapter(id: EditorId): EditorAdapter | undefined;
3
+ /** All registered editors, regardless of detection. */
4
+ export declare function getAllAdapters(): EditorAdapter[];
5
+ export declare function detectAdapters(projectRoot: string, homeDir?: string): EditorAdapter[];
6
+ export declare function registerAdapter(adapter: EditorAdapter): void;
7
+ /** Slash-command hint for user-facing messages. */
8
+ export declare function slashCommandForAdapter(adapter: EditorAdapter): string;
9
+ /** Relative paths installCommand writes for this adapter + meta. */
10
+ export declare function listCommandArtifactPaths(adapter: EditorAdapter, meta: CommandMeta): string[];
11
+ /** Install the command file (and optional extraArtifacts) for one adapter. */
12
+ export declare function installCommand(adapter: EditorAdapter, meta: CommandMeta, projectRoot: string): void;
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Adapter registry: the ADAPTERS array plus lookup/detection/install
3
+ * helpers. Adapters self-register on module load (each adapters/*.ts
4
+ * calls registerAdapter); this module deliberately imports no adapter so
5
+ * the dependency graph stays acyclic (adapters -> registry).
6
+ *
7
+ * Registration order is load order and MUST stay:
8
+ * claude, opencode, cline, cursor, pi, omp, dsh
9
+ * (editors.ts re-exports the adapter modules in exactly that order;
10
+ * tests/editors-tools.test.ts asserts it.)
11
+ */
12
+ import { mkdirSync, writeFileSync } from "fs";
13
+ import { dirname, resolve as pathResolve } from "path";
14
+ import chalk from "chalk";
15
+ // ─── Registry ────────────────────────────────────────────────────────────
16
+ const ADAPTERS = [
17
+ // Adapters are registered after const declarations at the bottom of this file.
18
+ ];
19
+ export function getAdapter(id) {
20
+ return ADAPTERS.find((a) => a.id === id);
21
+ }
22
+ /** All registered editors, regardless of detection. */
23
+ export function getAllAdapters() {
24
+ return [...ADAPTERS];
25
+ }
26
+ export function detectAdapters(projectRoot, homeDir) {
27
+ return ADAPTERS.filter((a) => a.detect(projectRoot, homeDir));
28
+ }
29
+ export function registerAdapter(adapter) {
30
+ ADAPTERS.push(adapter);
31
+ }
32
+ /** Slash-command hint for user-facing messages. */
33
+ export function slashCommandForAdapter(adapter) {
34
+ return adapter.id === "claude" ? "/opsx:e2e" : "/opsx-e2e";
35
+ }
36
+ /** Relative paths installCommand writes for this adapter + meta. */
37
+ export function listCommandArtifactPaths(adapter, meta) {
38
+ const paths = [adapter.commandFilePath(meta.id)];
39
+ for (const extra of adapter.extraArtifacts?.(meta) ?? []) {
40
+ paths.push(extra.relativePath);
41
+ }
42
+ return paths;
43
+ }
44
+ // ─── Install helpers ─────────────────────────────────────────────────────
45
+ /** Install the command file (and optional extraArtifacts) for one adapter. */
46
+ export function installCommand(adapter, meta, projectRoot) {
47
+ const relPath = adapter.commandFilePath(meta.id);
48
+ const absPath = pathResolve(projectRoot, relPath);
49
+ mkdirSync(dirname(absPath), { recursive: true });
50
+ writeFileSync(absPath, adapter.formatCommand(meta));
51
+ console.log(chalk.green(` ✓ ${adapter.label}: ${relPath}`));
52
+ for (const extra of adapter.extraArtifacts?.(meta) ?? []) {
53
+ const extraAbs = pathResolve(projectRoot, extra.relativePath);
54
+ mkdirSync(dirname(extraAbs), { recursive: true });
55
+ writeFileSync(extraAbs, extra.contents);
56
+ console.log(chalk.green(` ✓ ${adapter.label}: ${extra.relativePath}`));
57
+ }
58
+ }
59
+ //# sourceMappingURL=registry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.js","sourceRoot":"","sources":["../../../src/commands/editors/registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,IAAI,CAAC;AAC9C,OAAO,EAAE,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,MAAM,CAAC;AACvD,OAAO,KAAK,MAAM,OAAO,CAAC;AAG1B,4EAA4E;AAE5E,MAAM,QAAQ,GAAoB;AAChC,+EAA+E;CAChF,CAAC;AAEF,MAAM,UAAU,UAAU,CAAC,EAAY;IACrC,OAAO,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC;AAC3C,CAAC;AAED,uDAAuD;AACvD,MAAM,UAAU,cAAc;IAC5B,OAAO,CAAC,GAAG,QAAQ,CAAC,CAAC;AACvB,CAAC;AAED,MAAM,UAAU,cAAc,CAC5B,WAAmB,EACnB,OAAgB;IAEhB,OAAO,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC,CAAC;AAChE,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,OAAsB;IACpD,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;AACzB,CAAC;AAED,mDAAmD;AACnD,MAAM,UAAU,sBAAsB,CAAC,OAAsB;IAC3D,OAAO,OAAO,CAAC,EAAE,KAAK,QAAQ,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,WAAW,CAAC;AAC7D,CAAC;AAED,oEAAoE;AACpE,MAAM,UAAU,wBAAwB,CACtC,OAAsB,EACtB,IAAiB;IAEjB,MAAM,KAAK,GAAG,CAAC,OAAO,CAAC,eAAe,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC;IACjD,KAAK,MAAM,KAAK,IAAI,OAAO,CAAC,cAAc,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,EAAE,CAAC;QACzD,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC;IACjC,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,4EAA4E;AAE5E,8EAA8E;AAC9E,MAAM,UAAU,cAAc,CAC5B,OAAsB,EACtB,IAAiB,EACjB,WAAmB;IAEnB,MAAM,OAAO,GAAG,OAAO,CAAC,eAAe,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACjD,MAAM,OAAO,GAAG,WAAW,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC;IAClD,SAAS,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACjD,aAAa,CAAC,OAAO,EAAE,OAAO,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC,CAAC;IACpD,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,OAAO,OAAO,CAAC,KAAK,KAAK,OAAO,EAAE,CAAC,CAAC,CAAC;IAE7D,KAAK,MAAM,KAAK,IAAI,OAAO,CAAC,cAAc,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,EAAE,CAAC;QACzD,MAAM,QAAQ,GAAG,WAAW,CAAC,WAAW,EAAE,KAAK,CAAC,YAAY,CAAC,CAAC;QAC9D,SAAS,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAClD,aAAa,CAAC,QAAQ,EAAE,KAAK,CAAC,QAAQ,CAAC,CAAC;QACxC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,OAAO,OAAO,CAAC,KAAK,KAAK,KAAK,CAAC,YAAY,EAAE,CAAC,CAAC,CAAC;IAC1E,CAAC;AACH,CAAC"}
@@ -0,0 +1,37 @@
1
+ /** Escape a value for safe inclusion in a YAML frontmatter scalar. */
2
+ export declare function escapeYamlValue(value: string): string;
3
+ /** Format tags as a YAML inline array. */
4
+ export declare function formatTagsArray(tags: string[]): string;
5
+ /**
6
+ * OpenCode slash-command names are hyphenated (`/opsx-e2e`), Claude's are
7
+ * colon-prefixed (`/opsx:e2e`). Rewrite all `/opsx:` references in a
8
+ * command body for OpenCode installation.
9
+ */
10
+ export declare function transformToHyphenCommands(text: string): string;
11
+ export interface CommandMeta {
12
+ id: string;
13
+ name: string;
14
+ description: string;
15
+ category: string;
16
+ tags: string[];
17
+ body: string;
18
+ }
19
+ /** Build the command metadata for the /opsx:e2e command. */
20
+ export declare function buildCommandMeta(body: string): CommandMeta;
21
+ export interface McpStdioServer {
22
+ command: string;
23
+ args: string[];
24
+ }
25
+ export type McpServersFile = Record<string, unknown> & {
26
+ mcpServers: Record<string, McpStdioServer>;
27
+ };
28
+ /**
29
+ * Read an MCP config file with a top-level `mcpServers` map, or null if
30
+ * missing/unparseable. Preserves unknown top-level fields.
31
+ */
32
+ export declare function readMcpServersFile(configPath: string): McpServersFile | null;
33
+ /** Write an MCP config file, creating parent directories if needed. */
34
+ export declare function writeMcpServersFile(configPath: string, config: McpServersFile): void;
35
+ export declare function isMcpServerInFile(configPath: string, serverName: string): boolean;
36
+ export declare function installMcpServerInFile(configPath: string, serverName: string, command: string[]): void;
37
+ export declare function removeMcpServerFromFile(configPath: string, serverName: string): void;
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Cross-editor shared helpers: YAML frontmatter escaping, command body
3
+ * transforms, command metadata, and the mcpServers JSON file family
4
+ * (Cline / Cursor / Oh My Pi).
5
+ */
6
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
7
+ import { dirname } from "path";
8
+ // ─── YAML helpers (shared by Claude frontmatter and elsewhere) ───────────
9
+ /** Escape a value for safe inclusion in a YAML frontmatter scalar. */
10
+ export function escapeYamlValue(value) {
11
+ const needsQuoting = /[:\n\r#{}[\],&*!|>'"%@`]|^\s|\s$/.test(value);
12
+ if (needsQuoting) {
13
+ const escaped = value
14
+ .replace(/\\/g, "\\\\")
15
+ .replace(/"/g, '\\"')
16
+ .replace(/\n/g, "\\n");
17
+ return `"${escaped}"`;
18
+ }
19
+ return value;
20
+ }
21
+ /** Format tags as a YAML inline array. */
22
+ export function formatTagsArray(tags) {
23
+ return `[${tags.map((t) => escapeYamlValue(t)).join(", ")}]`;
24
+ }
25
+ // ─── Body transform ──────────────────────────────────────────────────────
26
+ /**
27
+ * OpenCode slash-command names are hyphenated (`/opsx-e2e`), Claude's are
28
+ * colon-prefixed (`/opsx:e2e`). Rewrite all `/opsx:` references in a
29
+ * command body for OpenCode installation.
30
+ */
31
+ export function transformToHyphenCommands(text) {
32
+ return text.replace(/\/opsx:/g, "/opsx-");
33
+ }
34
+ /** Build the command metadata for the /opsx:e2e command. */
35
+ export function buildCommandMeta(body) {
36
+ return {
37
+ id: "e2e",
38
+ name: "OPSX: E2E",
39
+ description: "Run Playwright E2E verification for an OpenSpec change",
40
+ category: "OpenSpec",
41
+ tags: ["openspec", "playwright", "e2e", "testing"],
42
+ body,
43
+ };
44
+ }
45
+ /**
46
+ * Read an MCP config file with a top-level `mcpServers` map, or null if
47
+ * missing/unparseable. Preserves unknown top-level fields.
48
+ */
49
+ export function readMcpServersFile(configPath) {
50
+ if (!existsSync(configPath))
51
+ return null;
52
+ try {
53
+ const raw = JSON.parse(readFileSync(configPath, "utf-8"));
54
+ if (raw.mcpServers && typeof raw.mcpServers === "object") {
55
+ return raw;
56
+ }
57
+ raw.mcpServers = {};
58
+ return raw;
59
+ }
60
+ catch {
61
+ return null;
62
+ }
63
+ }
64
+ /** Write an MCP config file, creating parent directories if needed. */
65
+ export function writeMcpServersFile(configPath, config) {
66
+ mkdirSync(dirname(configPath), { recursive: true });
67
+ writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
68
+ }
69
+ export function isMcpServerInFile(configPath, serverName) {
70
+ const config = readMcpServersFile(configPath);
71
+ if (!config)
72
+ return false;
73
+ return config.mcpServers[serverName] !== undefined;
74
+ }
75
+ export function installMcpServerInFile(configPath, serverName, command) {
76
+ const config = readMcpServersFile(configPath) ?? { mcpServers: {} };
77
+ config.mcpServers[serverName] = {
78
+ command: command[0],
79
+ args: command.slice(1),
80
+ };
81
+ writeMcpServersFile(configPath, config);
82
+ }
83
+ export function removeMcpServerFromFile(configPath, serverName) {
84
+ const config = readMcpServersFile(configPath);
85
+ if (!config)
86
+ return;
87
+ if (config.mcpServers[serverName] === undefined)
88
+ return;
89
+ delete config.mcpServers[serverName];
90
+ writeMcpServersFile(configPath, config);
91
+ }
92
+ //# sourceMappingURL=shared.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shared.js","sourceRoot":"","sources":["../../../src/commands/editors/shared.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ /**
2
+ * `--tools` flag parsing for `openspec-pw init` — mirrors upstream
3
+ * OpenSpec semantics (all | none | comma-separated ids).
4
+ */
5
+ import type { EditorId } from "./types.js";
6
+ /**
7
+ * Parse the `--tools` flag value for `openspec-pw init`.
8
+ *
9
+ * Returns the selected editor ids, or `null` when the flag was not provided.
10
+ * Mirrors the upstream OpenSpec `openspec init --tools` semantics:
11
+ * - "all" → every registered editor
12
+ * - "none" → no editors
13
+ * - comma-separated ids, case-insensitive; "oh-my-pi" aliases "omp"
14
+ * - mixing "all"/"none" with specific ids, or unknown ids, throw
15
+ * - duplicate ids are deduplicated preserving first-occurrence order
16
+ */
17
+ export declare function resolveToolsArg(toolsArg: string | undefined): EditorId[] | null;
@@ -0,0 +1,60 @@
1
+ import { getAllAdapters } from "./registry.js";
2
+ // ─── Tool selection (--tools flag) ───────────────────────────────────────
3
+ /** Aliases accepted by `--tools` in addition to canonical EditorIds. */
4
+ const TOOL_ID_ALIASES = {
5
+ "oh-my-pi": "omp",
6
+ };
7
+ const RESERVED_TOOLS = new Set(["all", "none"]);
8
+ /**
9
+ * Parse the `--tools` flag value for `openspec-pw init`.
10
+ *
11
+ * Returns the selected editor ids, or `null` when the flag was not provided.
12
+ * Mirrors the upstream OpenSpec `openspec init --tools` semantics:
13
+ * - "all" → every registered editor
14
+ * - "none" → no editors
15
+ * - comma-separated ids, case-insensitive; "oh-my-pi" aliases "omp"
16
+ * - mixing "all"/"none" with specific ids, or unknown ids, throw
17
+ * - duplicate ids are deduplicated preserving first-occurrence order
18
+ */
19
+ export function resolveToolsArg(toolsArg) {
20
+ if (typeof toolsArg === "undefined")
21
+ return null;
22
+ const raw = toolsArg.trim();
23
+ if (raw.length === 0) {
24
+ throw new Error('The --tools option requires a value. Use "all", "none", or a comma-separated list of editor ids.');
25
+ }
26
+ const editorIds = getAllAdapters().map((a) => a.id);
27
+ const availableList = ["all", "none", ...editorIds].join(", ");
28
+ const lowerRaw = raw.toLowerCase();
29
+ if (lowerRaw === "all")
30
+ return [...editorIds];
31
+ if (lowerRaw === "none")
32
+ return [];
33
+ const tokens = raw
34
+ .split(",")
35
+ .map((t) => t.trim())
36
+ .filter((t) => t.length > 0);
37
+ if (tokens.length === 0) {
38
+ throw new Error('The --tools option requires at least one editor id when not using "all" or "none".');
39
+ }
40
+ const normalized = tokens.map((t) => {
41
+ const lower = t.toLowerCase();
42
+ return TOOL_ID_ALIASES[lower] ?? lower;
43
+ });
44
+ if (normalized.some((t) => RESERVED_TOOLS.has(t))) {
45
+ throw new Error('Cannot combine reserved values "all" or "none" with specific editor ids.');
46
+ }
47
+ const invalid = normalized.filter((t) => !editorIds.includes(t));
48
+ if (invalid.length > 0) {
49
+ throw new Error(`Invalid tool id(s): ${invalid.join(", ")}. Available values: ${availableList}`);
50
+ }
51
+ // Deduplicate while preserving order
52
+ const deduped = [];
53
+ for (const id of normalized) {
54
+ const editorId = id;
55
+ if (!deduped.includes(editorId))
56
+ deduped.push(editorId);
57
+ }
58
+ return deduped;
59
+ }
60
+ //# sourceMappingURL=tool-selection.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tool-selection.js","sourceRoot":"","sources":["../../../src/commands/editors/tool-selection.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAE/C,4EAA4E;AAE5E,wEAAwE;AACxE,MAAM,eAAe,GAA6B;IAChD,UAAU,EAAE,KAAK;CAClB,CAAC;AAEF,MAAM,cAAc,GAAG,IAAI,GAAG,CAAC,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC;AAEhD;;;;;;;;;;GAUG;AACH,MAAM,UAAU,eAAe,CAC7B,QAA4B;IAE5B,IAAI,OAAO,QAAQ,KAAK,WAAW;QAAE,OAAO,IAAI,CAAC;IAEjD,MAAM,GAAG,GAAG,QAAQ,CAAC,IAAI,EAAE,CAAC;IAC5B,IAAI,GAAG,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACrB,MAAM,IAAI,KAAK,CACb,kGAAkG,CACnG,CAAC;IACJ,CAAC;IAED,MAAM,SAAS,GAAG,cAAc,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;IACpD,MAAM,aAAa,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,SAAS,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAE/D,MAAM,QAAQ,GAAG,GAAG,CAAC,WAAW,EAAE,CAAC;IACnC,IAAI,QAAQ,KAAK,KAAK;QAAE,OAAO,CAAC,GAAG,SAAS,CAAC,CAAC;IAC9C,IAAI,QAAQ,KAAK,MAAM;QAAE,OAAO,EAAE,CAAC;IAEnC,MAAM,MAAM,GAAG,GAAG;SACf,KAAK,CAAC,GAAG,CAAC;SACV,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;SACpB,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IAC/B,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACxB,MAAM,IAAI,KAAK,CACb,oFAAoF,CACrF,CAAC;IACJ,CAAC;IAED,MAAM,UAAU,GAAa,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE;QAC5C,MAAM,KAAK,GAAG,CAAC,CAAC,WAAW,EAAE,CAAC;QAC9B,OAAO,eAAe,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC;IACzC,CAAC,CAAC,CAAC;IAEH,IAAI,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,cAAc,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QAClD,MAAM,IAAI,KAAK,CACb,0EAA0E,CAC3E,CAAC;IACJ,CAAC;IAED,MAAM,OAAO,GAAG,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAa,CAAC,CAAC,CAAC;IAC7E,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACvB,MAAM,IAAI,KAAK,CACb,uBAAuB,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,uBAAuB,aAAa,EAAE,CAChF,CAAC;IACJ,CAAC;IAED,qCAAqC;IACrC,MAAM,OAAO,GAAe,EAAE,CAAC;IAC/B,KAAK,MAAM,EAAE,IAAI,UAAU,EAAE,CAAC;QAC5B,MAAM,QAAQ,GAAG,EAAc,CAAC;QAChC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC;YAAE,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IAC1D,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC"}
@@ -0,0 +1,82 @@
1
+ export interface CommandMeta {
2
+ id: string;
3
+ name: string;
4
+ description: string;
5
+ category: string;
6
+ tags: string[];
7
+ body: string;
8
+ }
9
+ /** Build the command metadata for the /opsx:e2e command. */
10
+ export declare function buildCommandMeta(body: string): CommandMeta;
11
+ export type EditorId = "claude" | "opencode" | "cline" | "cursor" | "pi" | "omp" | "dsh";
12
+ export interface ExtraArtifact {
13
+ relativePath: string;
14
+ contents: string;
15
+ }
16
+ export interface EditorAdapter {
17
+ id: EditorId;
18
+ /** Short label used in log messages. */
19
+ label: string;
20
+ /** Human-readable name used in user-facing messages. */
21
+ displayName: string;
22
+ /**
23
+ * True if this editor's config dir is present in the project.
24
+ * Some adapters (Pi, Oh My Pi) also treat a global config dir in the
25
+ * user's home as a detection signal — `homeDir` lets tests inject a
26
+ * fake home so detection stays hermetic.
27
+ */
28
+ detect(projectRoot: string, homeDir?: string): boolean;
29
+ /**
30
+ * True when this editor has an MCP client to configure. False skips all
31
+ * MCP install/check/remove phases (Pi has no MCP client).
32
+ */
33
+ supportsMcp?: boolean;
34
+ /** Relative path of the command file inside the project. */
35
+ commandFilePath(id: string): string;
36
+ /** Format command file contents (frontmatter + body). */
37
+ formatCommand(meta: CommandMeta): string;
38
+ /** Absolute path of the project rules file. */
39
+ projectRulesPath(projectRoot: string): string;
40
+ /** True if MCP server `serverName` is already configured. */
41
+ isMcpInstalled(projectRoot: string, serverName: string): boolean;
42
+ /** Install MCP server config in this editor. */
43
+ installMcp(projectRoot: string, serverName: string, command: string[]): void;
44
+ /** Remove MCP server config from this editor. */
45
+ removeMcp(projectRoot: string, serverName: string): void;
46
+ /** Optional: register project rules file path in editor config. */
47
+ registerInstructions?(projectRoot: string, instructions: string[]): void;
48
+ /** Optional: secondary files written alongside commandFilePath (Cursor skill). */
49
+ extraArtifacts?(meta: CommandMeta): ExtraArtifact[];
50
+ }
51
+ /**
52
+ * Input shape for `defineAdapter` — declares the contract for an editor
53
+ * adapter with sensible defaults for the no-MCP-client case (Pi, dsh).
54
+ * All required-by-behavior fields are still required; optional ones
55
+ * (supportsMcp, projectRulesPath, isMcpInstalled, installMcp, removeMcp)
56
+ * fall back to defaults.
57
+ */
58
+ export interface EditorAdapterInit {
59
+ id: EditorId;
60
+ label: string;
61
+ displayName: string;
62
+ detect: EditorAdapter["detect"];
63
+ commandFilePath: EditorAdapter["commandFilePath"];
64
+ formatCommand: EditorAdapter["formatCommand"];
65
+ /** Optional: true by default; set false for editors without an MCP client. */
66
+ supportsMcp?: boolean;
67
+ /** Optional: defaults to `<root>/AGENTS.md`. Override for Claude (CLAUDE.md). */
68
+ projectRulesPath?: EditorAdapter["projectRulesPath"];
69
+ /** Required when supportsMcp !== false. Defaults to `() => false`. */
70
+ isMcpInstalled?: EditorAdapter["isMcpInstalled"];
71
+ /** Required when supportsMcp !== false. Defaults to a no-op. */
72
+ installMcp?: EditorAdapter["installMcp"];
73
+ /** Required when supportsMcp !== false. Defaults to a no-op. */
74
+ removeMcp?: EditorAdapter["removeMcp"];
75
+ registerInstructions?: EditorAdapter["registerInstructions"];
76
+ extraArtifacts?: EditorAdapter["extraArtifacts"];
77
+ }
78
+ /**
79
+ * Build an `EditorAdapter` from a partial init object. Fills in
80
+ * defaults so each adapter only declares what's actually different.
81
+ */
82
+ export declare function defineAdapter(init: EditorAdapterInit): EditorAdapter;