openspec-playwright 0.3.73 → 0.3.75

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 (63) hide show
  1. package/README.md +18 -17
  2. package/README.zh-CN.md +19 -18
  3. package/dist/commands/doctor.d.ts +1 -0
  4. package/dist/commands/doctor.js +54 -23
  5. package/dist/commands/doctor.js.map +1 -1
  6. package/dist/commands/editors/adapters/claude.d.ts +5 -0
  7. package/dist/commands/editors/adapters/claude.js +79 -0
  8. package/dist/commands/editors/adapters/claude.js.map +1 -0
  9. package/dist/commands/editors/adapters/cline.d.ts +15 -0
  10. package/dist/commands/editors/adapters/cline.js +63 -0
  11. package/dist/commands/editors/adapters/cline.js.map +1 -0
  12. package/dist/commands/editors/adapters/cursor.d.ts +15 -0
  13. package/dist/commands/editors/adapters/cursor.js +79 -0
  14. package/dist/commands/editors/adapters/cursor.js.map +1 -0
  15. package/dist/commands/editors/adapters/dsh.d.ts +27 -0
  16. package/dist/commands/editors/adapters/dsh.js +67 -0
  17. package/dist/commands/editors/adapters/dsh.js.map +1 -0
  18. package/dist/commands/editors/adapters/omp.d.ts +24 -0
  19. package/dist/commands/editors/adapters/omp.js +73 -0
  20. package/dist/commands/editors/adapters/omp.js.map +1 -0
  21. package/dist/commands/editors/adapters/opencode.d.ts +8 -0
  22. package/dist/commands/editors/adapters/opencode.js +133 -0
  23. package/dist/commands/editors/adapters/opencode.js.map +1 -0
  24. package/dist/commands/editors/adapters/pi.d.ts +27 -0
  25. package/dist/commands/editors/adapters/pi.js +64 -0
  26. package/dist/commands/editors/adapters/pi.js.map +1 -0
  27. package/dist/commands/editors/project-rules.d.ts +69 -0
  28. package/dist/commands/editors/project-rules.js +246 -0
  29. package/dist/commands/editors/project-rules.js.map +1 -0
  30. package/dist/commands/editors/registry.d.ts +12 -0
  31. package/dist/commands/editors/registry.js +59 -0
  32. package/dist/commands/editors/registry.js.map +1 -0
  33. package/dist/commands/editors/shared.d.ts +37 -0
  34. package/dist/commands/editors/shared.js +92 -0
  35. package/dist/commands/editors/shared.js.map +1 -0
  36. package/dist/commands/editors/tool-selection.d.ts +17 -0
  37. package/dist/commands/editors/tool-selection.js +60 -0
  38. package/dist/commands/editors/tool-selection.js.map +1 -0
  39. package/dist/commands/editors/types.d.ts +82 -0
  40. package/dist/commands/editors/types.js +41 -0
  41. package/dist/commands/editors/types.js.map +1 -0
  42. package/dist/commands/editors.d.ts +32 -309
  43. package/dist/commands/editors.js +38 -960
  44. package/dist/commands/editors.js.map +1 -1
  45. package/dist/commands/explore.d.ts +2 -0
  46. package/dist/commands/explore.js +2 -2
  47. package/dist/commands/explore.js.map +1 -1
  48. package/dist/commands/init.js +15 -25
  49. package/dist/commands/init.js.map +1 -1
  50. package/dist/commands/uninstall.js +4 -2
  51. package/dist/commands/uninstall.js.map +1 -1
  52. package/dist/commands/update.d.ts +8 -0
  53. package/dist/commands/update.js +12 -8
  54. package/dist/commands/update.js.map +1 -1
  55. package/dist/shared/index.d.ts +1 -1
  56. package/dist/shared/index.js +1 -1
  57. package/dist/shared/index.js.map +1 -1
  58. package/dist/shared/mcp.d.ts +5 -2
  59. package/dist/shared/mcp.js +15 -7
  60. package/dist/shared/mcp.js.map +1 -1
  61. package/employee-standards.md +2 -0
  62. package/package.json +5 -5
  63. package/templates/e2e-command.md +6 -2
@@ -1,962 +1,40 @@
1
1
  /**
2
- * Editor adapter layer.
3
- *
4
- * Claude Code, OpenCode, Cline, and Cursor can host the /opsx:e2e command,
5
- * plus a project-level rules file (CLAUDE.md / AGENTS.md) and an MCP server
6
- * definition. Each editor has its own conventions — file path, frontmatter
7
- * shape, MCP install mechanism — so this module exposes a single
8
- * `EditorAdapter` interface and a registry that callers can iterate.
9
- *
10
- * Conventions follow the upstream OpenSpec reference implementation:
11
- * - Claude: `.claude/commands/opsx/<id>.md`, full frontmatter,
12
- * `claude mcp add …`, reads CLAUDE.md directly.
13
- * - OpenCode: `.opencode/commands/opsx-<id>.md`, description-only
14
- * frontmatter, edits `opencode.json(c)`, reads files listed
15
- * under `instructions` (CLAUDE.md is a built-in fallback).
16
- * - Cline: `.cline/skills/opsx-<id>/SKILL.md`, name+description
17
- * frontmatter, edits `.cline/mcp.json`, auto-detects AGENTS.md.
18
- * - Cursor: `.cursor/commands/opsx-<id>.md` (plain MD) +
19
- * `.cursor/skills/opsx-<id>/SKILL.md` (extraArtifacts),
20
- * edits `.cursor/mcp.json`, auto-detects AGENTS.md.
21
- * - Pi: `.pi/prompts/opsx-<id>.md` (prompt template), description +
22
- * argument-hint frontmatter, NO MCP client (supportsMcp:false),
23
- * auto-detects AGENTS.md. Detected via `.pi/` or `~/.pi/agent/`.
24
- * - Oh My Pi: `.omp/commands/opsx-<id>.md`, name+description frontmatter,
25
- * edits `.omp/mcp.json` (same shape as Cline/Cursor),
26
- * auto-detects AGENTS.md. Detected via `.omp/` or `~/.omp/agent/`.
27
- * - DeepSeek Harness: `.dsh/skills/opsx-<id>/SKILL.md` (project-dsh skill,
28
- * rank 100), name+description frontmatter, NO MCP client
29
- * (supportsMcp:false MCP is configured via cordis.yml plugin
30
- * config, not a simple file), auto-detects AGENTS.md. Detected
31
- * via `.dsh/` or `~/.dsh/` (DSH_HOME).
32
- */
33
- import { existsSync, lstatSync, mkdirSync, rmSync, writeFileSync, readFileSync, } from "fs";
34
- import { execFileSync } from "node:child_process";
35
- import { homedir } from "os";
36
- import { join, dirname, basename, resolve as pathResolve } from "path";
37
- import chalk from "chalk";
38
- import { modify, applyEdits, parseTree as parseJsonc, findNodeAtLocation, getNodeValue, } from "jsonc-parser";
39
- import { TIMEOUT } from "../shared/constants.js";
40
- import { needsShell } from "../shared/platform.js";
41
- // ─── YAML helpers (shared by Claude frontmatter and elsewhere) ───────────
42
- /** Escape a value for safe inclusion in a YAML frontmatter scalar. */
43
- export function escapeYamlValue(value) {
44
- const needsQuoting = /[:\n\r#{}[\],&*!|>'"%@`]|^\s|\s$/.test(value);
45
- if (needsQuoting) {
46
- const escaped = value
47
- .replace(/\\/g, "\\\\")
48
- .replace(/"/g, '\\"')
49
- .replace(/\n/g, "\\n");
50
- return `"${escaped}"`;
51
- }
52
- return value;
53
- }
54
- /** Format tags as a YAML inline array. */
55
- export function formatTagsArray(tags) {
56
- return `[${tags.map((t) => escapeYamlValue(t)).join(", ")}]`;
57
- }
58
- // ─── Body transform ──────────────────────────────────────────────────────
59
- /**
60
- * OpenCode slash-command names are hyphenated (`/opsx-e2e`), Claude's are
61
- * colon-prefixed (`/opsx:e2e`). Rewrite all `/opsx:` references in a
62
- * command body for OpenCode installation.
63
- */
64
- export function transformToHyphenCommands(text) {
65
- return text.replace(/\/opsx:/g, "/opsx-");
66
- }
67
- /** Build the command metadata for the /opsx:e2e command. */
68
- export function buildCommandMeta(body) {
69
- return {
70
- id: "e2e",
71
- name: "OPSX: E2E",
72
- description: "Run Playwright E2E verification for an OpenSpec change",
73
- category: "OpenSpec",
74
- tags: ["openspec", "playwright", "e2e", "testing"],
75
- body,
76
- };
77
- }
78
- const noop = () => { };
79
- const alwaysFalse = () => false;
80
- /**
81
- * Build an `EditorAdapter` from a partial init object. Fills in
82
- * defaults so each adapter only declares what's actually different.
83
- */
84
- export function defineAdapter(init) {
85
- return {
86
- id: init.id,
87
- label: init.label,
88
- displayName: init.displayName,
89
- detect: init.detect,
90
- commandFilePath: init.commandFilePath,
91
- formatCommand: init.formatCommand,
92
- supportsMcp: init.supportsMcp ?? true,
93
- projectRulesPath: init.projectRulesPath ?? ((root) => join(root, "AGENTS.md")),
94
- isMcpInstalled: init.isMcpInstalled ?? alwaysFalse,
95
- installMcp: init.installMcp ?? noop,
96
- removeMcp: init.removeMcp ?? noop,
97
- registerInstructions: init.registerInstructions,
98
- extraArtifacts: init.extraArtifacts,
99
- };
100
- }
101
- // ─── Claude Code adapter ─────────────────────────────────────────────────
102
- export function formatClaudeCommand(meta) {
103
- return `---
104
- name: ${escapeYamlValue(meta.name)}
105
- description: ${escapeYamlValue(meta.description)}
106
- category: ${escapeYamlValue(meta.category)}
107
- tags: ${formatTagsArray(meta.tags)}
108
- ---
109
-
110
- ${meta.body}
111
- `;
112
- }
113
- export function getClaudeCommandPath(id) {
114
- return join(".claude", "commands", "opsx", `${id}.md`);
115
- }
116
- export function hasClaudeCode(projectRoot) {
117
- return existsSync(join(projectRoot, ".claude"));
118
- }
119
- // ─── OpenCode adapter ────────────────────────────────────────────────────
120
- export function formatOpenCodeCommand(meta) {
121
- const body = transformToHyphenCommands(meta.body);
122
- return `---
123
- description: ${escapeYamlValue(meta.description)}
124
- ---
125
-
126
- ${body}
127
- `;
128
- }
129
- export function getOpenCodeCommandPath(id) {
130
- return join(".opencode", "commands", `opsx-${id}.md`);
131
- }
132
- export function hasOpenCode(projectRoot) {
133
- return existsSync(join(projectRoot, ".opencode"));
134
- }
135
- const JSONC_FORMAT = { tabSize: 2, insertSpaces: true };
136
- /** Find the first existing opencode.json(c), or null. */
137
- function findOpenCodeConfig(projectRoot) {
138
- for (const name of ["opencode.jsonc", "opencode.json"]) {
139
- const p = join(projectRoot, name);
140
- if (existsSync(p))
141
- return { path: p, text: readFileSync(p, "utf-8") };
142
- }
143
- return null;
144
- }
145
- /**
146
- * Set a value at a JSON path inside opencode.json(c), creating the file
147
- * with a `$schema` scaffold if it doesn't exist. Replaces any existing
148
- * value at the path (does not merge arrays).
149
- */
150
- function setOpenCodeValue(projectRoot, keySegments, value) {
151
- const existing = findOpenCodeConfig(projectRoot);
152
- const targetPath = existing?.path ?? join(projectRoot, "opencode.jsonc");
153
- if (!existing) {
154
- const scaffold = {
155
- $schema: "https://opencode.ai/config.json",
156
- };
157
- // Build nested scaffold for multi-segment keys (e.g. ["mcp", "playwright"])
158
- let cursor = scaffold;
159
- for (let i = 0; i < keySegments.length - 1; i++) {
160
- const k = String(keySegments[i]);
161
- cursor[k] = {};
162
- cursor = cursor[k];
163
- }
164
- cursor[String(keySegments[keySegments.length - 1])] = value;
165
- // ponytail: new file uses 2-space indent; modify branch preserves existing formatting.
166
- writeFileSync(targetPath, JSON.stringify(scaffold, null, 2) + "\n");
167
- return;
168
- }
169
- const edits = modify(existing.text, keySegments, value, {
170
- formattingOptions: JSONC_FORMAT,
171
- });
172
- writeFileSync(targetPath, applyEdits(existing.text, edits));
173
- }
174
- /** Read the current value at a JSON path (returns undefined if missing). */
175
- function readOpenCodeValue(text, keySegments) {
176
- try {
177
- const tree = parseJsonc(text);
178
- if (!tree)
179
- return undefined;
180
- const node = findNodeAtLocation(tree, keySegments);
181
- return node ? getNodeValue(node) : undefined;
182
- }
183
- catch {
184
- return undefined;
185
- }
186
- }
187
- /** Read the current `instructions` array from opencode.json(c), or undefined. */
188
- function readOpenCodeInstructions(projectRoot) {
189
- const config = findOpenCodeConfig(projectRoot);
190
- if (!config)
191
- return undefined;
192
- const value = readOpenCodeValue(config.text, ["instructions"]);
193
- if (Array.isArray(value) && value.every((v) => typeof v === "string")) {
194
- return value;
195
- }
196
- return undefined;
197
- }
198
- // ─── Cline adapter ───────────────────────────────────────────────────────
199
- /**
200
- * Cline stores project-level config in `.cline/` (skills/, rules/, mcp.json).
201
- * `.clinerules/` is the legacy rules-only directory, still auto-detected.
202
- *
203
- * Conventions follow the Cline documentation (2026):
204
- * - Skills: `.cline/skills/<name>/SKILL.md` with YAML frontmatter
205
- * (name + description). Triggered via `/<name>` slash command.
206
- * - MCP: `.cline/mcp.json` with `{ "mcpServers": { ... } }` structure.
207
- * - Rules: Cline auto-detects `AGENTS.md` — no wrapper file needed.
208
- */
209
- export function formatClineCommand(meta) {
210
- const body = transformToHyphenCommands(meta.body);
211
- const skillName = `opsx-${meta.id}`;
212
- return `---
213
- name: ${escapeYamlValue(skillName)}
214
- description: ${escapeYamlValue(meta.description)}
215
- ---
216
-
217
- ${body}
218
- `;
219
- }
220
- export function getClineCommandPath(id) {
221
- return join(".cline", "skills", `opsx-${id}`, "SKILL.md");
222
- }
223
- export function hasCline(projectRoot) {
224
- return (existsSync(join(projectRoot, ".cline")) ||
225
- existsSync(join(projectRoot, ".clinerules")));
226
- }
227
- /**
228
- * Read an MCP config file with a top-level `mcpServers` map, or null if
229
- * missing/unparseable. Preserves unknown top-level fields.
230
- */
231
- export function readMcpServersFile(configPath) {
232
- if (!existsSync(configPath))
233
- return null;
234
- try {
235
- const raw = JSON.parse(readFileSync(configPath, "utf-8"));
236
- if (raw.mcpServers && typeof raw.mcpServers === "object") {
237
- return raw;
238
- }
239
- raw.mcpServers = {};
240
- return raw;
241
- }
242
- catch {
243
- return null;
244
- }
245
- }
246
- /** Write an MCP config file, creating parent directories if needed. */
247
- export function writeMcpServersFile(configPath, config) {
248
- mkdirSync(dirname(configPath), { recursive: true });
249
- writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
250
- }
251
- export function isMcpServerInFile(configPath, serverName) {
252
- const config = readMcpServersFile(configPath);
253
- if (!config)
254
- return false;
255
- return config.mcpServers[serverName] !== undefined;
256
- }
257
- export function installMcpServerInFile(configPath, serverName, command) {
258
- const config = readMcpServersFile(configPath) ?? { mcpServers: {} };
259
- config.mcpServers[serverName] = {
260
- command: command[0],
261
- args: command.slice(1),
262
- };
263
- writeMcpServersFile(configPath, config);
264
- }
265
- export function removeMcpServerFromFile(configPath, serverName) {
266
- const config = readMcpServersFile(configPath);
267
- if (!config)
268
- return;
269
- if (config.mcpServers[serverName] === undefined)
270
- return;
271
- delete config.mcpServers[serverName];
272
- writeMcpServersFile(configPath, config);
273
- }
274
- /** Path to the project-level MCP config file for Cline. */
275
- function clineMcpPath(projectRoot) {
276
- return join(projectRoot, ".cline", "mcp.json");
277
- }
278
- /** Path to the project-level MCP config file for Cursor. */
279
- function cursorMcpPath(projectRoot) {
280
- return join(projectRoot, ".cursor", "mcp.json");
281
- }
282
- /** Path to the project-level MCP config file for Oh My Pi. */
283
- function ompMcpPath(projectRoot) {
284
- return join(projectRoot, ".omp", "mcp.json");
285
- }
286
- // ─── Cursor adapter (format / paths) ─────────────────────────────────────
287
- /**
288
- * Cursor slash commands are plain markdown (no frontmatter); the filename
289
- * is the command name. `$1` is the change-name argument.
290
- */
291
- export function formatCursorCommand(meta) {
292
- const body = transformToHyphenCommands(meta.body);
293
- return `<!-- Change name: $1 (e.g. /opsx-${meta.id} my-change) -->
294
-
295
- ${body}
296
- `;
297
- }
298
- export function getCursorCommandPath(id) {
299
- return join(".cursor", "commands", `opsx-${id}.md`);
300
- }
301
- export function getCursorSkillPath(id) {
302
- return join(".cursor", "skills", `opsx-${id}`, "SKILL.md");
303
- }
304
- /**
305
- * Cursor Agent Skill — explicit invocation only (`disable-model-invocation`).
306
- * No `$1` placeholders (those belong to the slash command file).
307
- */
308
- export function formatCursorSkill(meta) {
309
- const body = transformToHyphenCommands(meta.body);
310
- const skillName = `opsx-${meta.id}`;
311
- return `---
312
- name: ${escapeYamlValue(skillName)}
313
- description: ${escapeYamlValue(meta.description)}
314
- disable-model-invocation: true
315
- ---
316
-
317
- ${body}
318
- `;
319
- }
320
- export function hasCursor(projectRoot) {
321
- return existsSync(join(projectRoot, ".cursor"));
322
- }
323
- // ─── Pi adapter ──────────────────────────────────────────────────────────
324
- /**
325
- * Pi stores project resources in `.pi/` (skills/, prompts/, extensions/).
326
- * Prompt templates are Markdown with optional YAML frontmatter; the
327
- * filename becomes the slash command name (`opsx-e2e.md` → `/opsx-e2e`),
328
- * `description` feeds autocomplete, and `argument-hint` shows expected
329
- * arguments. `$1` / `$ARGUMENTS` placeholders expand at prompt time.
330
- *
331
- * Conventions follow the Pi docs (packages/coding-agent/docs):
332
- * - Prompts: `.pi/prompts/*.md`, invoked via `/name`.
333
- * - Skills: `.pi/skills/` (root `.md` files or `SKILL.md` dirs),
334
- * invoked via `/skill:name`.
335
- * - Rules: Pi loads `AGENTS.md` (or `CLAUDE.md`) walking up from cwd
336
- * natively — no wrapper file needed.
337
- * - MCP: Pi has NO MCP client (built-in tools only). The adapter
338
- * still implements the interface with no-ops and declares
339
- * `supportsMcp: false` so shared MCP phases skip it.
340
- */
341
- export function formatPiCommand(meta) {
342
- const body = transformToHyphenCommands(meta.body);
343
- return `---
344
- description: ${escapeYamlValue(meta.description)}
345
- argument-hint: "<change-name>"
346
- ---
347
-
348
- ${body}
349
- `;
350
- }
351
- export function getPiCommandPath(id) {
352
- return join(".pi", "prompts", `opsx-${id}.md`);
353
- }
354
- /**
355
- * True when Pi is in use: a project `.pi/` dir, or a global Pi config dir
356
- * (`~/.pi/agent/`, created by Pi on first run). The home signal lets
357
- * `openspec-pw init` detect Pi in a fresh project that has no `.pi/` yet.
358
- */
359
- export function hasPi(projectRoot, homeDir = homedir()) {
360
- return (existsSync(join(projectRoot, ".pi")) ||
361
- existsSync(join(homeDir, ".pi", "agent")));
362
- }
363
- // ─── Oh My Pi adapter ────────────────────────────────────────────────────
364
- /**
365
- * Oh My Pi (omp) stores project slash commands in `.omp/commands/*.md`
366
- * (non-recursive scan, project before user). Native commands are parsed
367
- * with FATAL frontmatter parsing, so the YAML must be valid; `name`
368
- * overrides the filename and `description` feeds autocomplete. `$1` /
369
- * `$ARGUMENTS` placeholders expand at prompt time.
370
- *
371
- * Conventions follow the omp docs (docs/slash-command-internals.md,
372
- * docs/mcp-config.md):
373
- * - Commands: `.omp/commands/*.md`, invoked via `/name`.
374
- * - MCP: `.omp/mcp.json` with `{ "mcpServers": { ... } }` — same
375
- * shape as Cline/Cursor. omp also inherits `.claude/` /
376
- * `.cursor/` / opencode MCP configs when present.
377
- * - Rules: omp reads `AGENTS.md` natively — no wrapper file needed.
378
- */
379
- export function formatOmpCommand(meta) {
380
- const body = transformToHyphenCommands(meta.body);
381
- const cmdName = `opsx-${meta.id}`;
382
- return `---
383
- name: ${escapeYamlValue(cmdName)}
384
- description: ${escapeYamlValue(meta.description)}
385
- ---
386
-
387
- ${body}
388
- `;
389
- }
390
- export function getOmpCommandPath(id) {
391
- return join(".omp", "commands", `opsx-${id}.md`);
392
- }
393
- /**
394
- * True when Oh My Pi is in use: a project `.omp/` dir, or a global omp
395
- * config dir (`~/.omp/agent/`, created by omp on first run).
396
- */
397
- export function hasOmp(projectRoot, homeDir = homedir()) {
398
- return (existsSync(join(projectRoot, ".omp")) ||
399
- existsSync(join(homeDir, ".omp", "agent")));
400
- }
401
- // ─── DeepSeek Harness adapter ────────────────────────────────────────────
402
- /**
403
- * DeepSeek Harness (dsh) stores project skills in `.dsh/skills/` — the
404
- * `project-dsh` local provider root, rank 100 (highest local priority).
405
- * Skills are directory bundles (`<name>/SKILL.md`) with YAML frontmatter
406
- * (name + description); the kebab-case name becomes the model-invocable
407
- * skill name (`opsx-e2e`).
408
- *
409
- * Conventions follow the dsh skills subsystem (docs/subsystems/skills.md):
410
- * - Skills: `.dsh/skills/<name>/SKILL.md`, invoked via the `skill` tool.
411
- * - Rules: dsh reads `AGENTS.md` / `CLAUDE.md` natively (default
412
- * instructionFileCandidates) — no wrapper file needed.
413
- * - MCP: dsh configures MCP via `cordis.yml` plugin config
414
- * (@deepseek-ai/dsh-mcp-client), not a simple mcpServers file —
415
- * the adapter declares `supportsMcp: false` so shared MCP phases
416
- * skip it (configure Playwright MCP manually in cordis.yml).
417
- * - Detected via `.dsh/` or the global `~/.dsh/` (DSH_HOME).
418
- */
419
- export function formatDshCommand(meta) {
420
- const body = transformToHyphenCommands(meta.body);
421
- const skillName = `opsx-${meta.id}`;
422
- return `---
423
- name: ${escapeYamlValue(skillName)}
424
- description: ${escapeYamlValue(meta.description)}
425
- ---
426
-
427
- ${body}
428
- `;
429
- }
430
- export function getDshCommandPath(id) {
431
- return join(".dsh", "skills", `opsx-${id}`, "SKILL.md");
432
- }
433
- /**
434
- * True when DeepSeek Harness is in use: a project `.dsh/` dir, or the global
435
- * dsh home (`~/.dsh/`, DSH_HOME). The home signal lets `openspec-pw init`
436
- * detect dsh in a fresh project that has no `.dsh/` yet.
437
- */
438
- export function hasDsh(projectRoot, homeDir = homedir()) {
439
- return (existsSync(join(projectRoot, ".dsh")) ||
440
- existsSync(join(homeDir, ".dsh")));
441
- }
442
- // ─── Registry ────────────────────────────────────────────────────────────
443
- const ADAPTERS = [
444
- // Adapters are registered after const declarations at the bottom of this file.
445
- ];
446
- // ─── Tool selection (--tools flag) ───────────────────────────────────────
447
- /** Aliases accepted by `--tools` in addition to canonical EditorIds. */
448
- const TOOL_ID_ALIASES = {
449
- "oh-my-pi": "omp",
450
- };
451
- const RESERVED_TOOLS = new Set(["all", "none"]);
452
- /**
453
- * Parse the `--tools` flag value for `openspec-pw init`.
454
- *
455
- * Returns the selected editor ids, or `null` when the flag was not provided.
456
- * Mirrors the upstream OpenSpec `openspec init --tools` semantics:
457
- * - "all" → every registered editor
458
- * - "none" → no editors
459
- * - comma-separated ids, case-insensitive; "oh-my-pi" aliases "omp"
460
- * - mixing "all"/"none" with specific ids, or unknown ids, throw
461
- * - duplicate ids are deduplicated preserving first-occurrence order
462
- */
463
- export function resolveToolsArg(toolsArg) {
464
- if (typeof toolsArg === "undefined")
465
- return null;
466
- const raw = toolsArg.trim();
467
- if (raw.length === 0) {
468
- throw new Error('The --tools option requires a value. Use "all", "none", or a comma-separated list of editor ids.');
469
- }
470
- const editorIds = ADAPTERS.map((a) => a.id);
471
- const availableList = ["all", "none", ...editorIds].join(", ");
472
- const lowerRaw = raw.toLowerCase();
473
- if (lowerRaw === "all")
474
- return [...editorIds];
475
- if (lowerRaw === "none")
476
- return [];
477
- const tokens = raw
478
- .split(",")
479
- .map((t) => t.trim())
480
- .filter((t) => t.length > 0);
481
- if (tokens.length === 0) {
482
- throw new Error('The --tools option requires at least one editor id when not using "all" or "none".');
483
- }
484
- const normalized = tokens.map((t) => {
485
- const lower = t.toLowerCase();
486
- return TOOL_ID_ALIASES[lower] ?? lower;
487
- });
488
- if (normalized.some((t) => RESERVED_TOOLS.has(t))) {
489
- throw new Error('Cannot combine reserved values "all" or "none" with specific editor ids.');
490
- }
491
- const invalid = normalized.filter((t) => !editorIds.includes(t));
492
- if (invalid.length > 0) {
493
- throw new Error(`Invalid tool id(s): ${invalid.join(", ")}. Available values: ${availableList}`);
494
- }
495
- // Deduplicate while preserving order
496
- const deduped = [];
497
- for (const id of normalized) {
498
- const editorId = id;
499
- if (!deduped.includes(editorId))
500
- deduped.push(editorId);
501
- }
502
- return deduped;
503
- }
504
- export function getAdapter(id) {
505
- return ADAPTERS.find((a) => a.id === id);
506
- }
507
- /** All registered editors, regardless of detection. */
508
- export function getAllAdapters() {
509
- return [...ADAPTERS];
510
- }
511
- export function detectAdapters(projectRoot, homeDir) {
512
- return ADAPTERS.filter((a) => a.detect(projectRoot, homeDir));
513
- }
514
- function registerAdapter(adapter) {
515
- ADAPTERS.push(adapter);
516
- }
517
- /** Slash-command hint for user-facing messages. */
518
- export function slashCommandForAdapter(adapter) {
519
- return adapter.id === "claude" ? "/opsx:e2e" : "/opsx-e2e";
520
- }
521
- /** Relative paths installCommand writes for this adapter + meta. */
522
- export function listCommandArtifactPaths(adapter, meta) {
523
- const paths = [adapter.commandFilePath(meta.id)];
524
- for (const extra of adapter.extraArtifacts?.(meta) ?? []) {
525
- paths.push(extra.relativePath);
526
- }
527
- return paths;
528
- }
529
- // ─── Install helpers ─────────────────────────────────────────────────────
530
- /** Install the command file (and optional extraArtifacts) for one adapter. */
531
- export function installCommand(adapter, meta, projectRoot) {
532
- const relPath = adapter.commandFilePath(meta.id);
533
- const absPath = pathResolve(projectRoot, relPath);
534
- mkdirSync(dirname(absPath), { recursive: true });
535
- writeFileSync(absPath, adapter.formatCommand(meta));
536
- console.log(chalk.green(` ✓ ${adapter.label}: ${relPath}`));
537
- for (const extra of adapter.extraArtifacts?.(meta) ?? []) {
538
- const extraAbs = pathResolve(projectRoot, extra.relativePath);
539
- mkdirSync(dirname(extraAbs), { recursive: true });
540
- writeFileSync(extraAbs, extra.contents);
541
- console.log(chalk.green(` ✓ ${adapter.label}: ${extra.relativePath}`));
542
- }
543
- }
544
- // ─── Project rules file (CLAUDE.md / AGENTS.md) ──────────────────────────
545
- /**
546
- * Read the OPENSPEC marker block from a rules file, or `null` when the file
547
- * is missing / has no markers. Used by drift detection and update to decide
548
- * whether a rules file needs rewriting.
549
- */
550
- export function readOpenSpecBlock(projectRoot, adapter) {
551
- const dest = adapter.projectRulesPath(projectRoot);
552
- if (!existsSync(dest))
553
- return null;
554
- const content = readFileSync(dest, "utf-8");
555
- const startIdx = content.indexOf("<!-- OPENSPEC:START -->");
556
- const endIdx = content.indexOf("<!-- OPENSPEC:END -->");
557
- if (startIdx === -1 || endIdx === -1)
558
- return null;
559
- return content.slice(startIdx + "<!-- OPENSPEC:START -->".length, endIdx).trim();
560
- }
561
- /**
562
- * Whether a rules file's OPENSPEC block matches the expected content.
563
- * A missing file or absent/truncated markers counts as "does not match"
564
- * (the caller will rewrite it), which keeps update idempotent but safe.
565
- */
566
- export function blockMatchesExpected(projectRoot, adapter, expected) {
567
- const block = readOpenSpecBlock(projectRoot, adapter);
568
- if (block === null)
569
- return false;
570
- return block === expected.trim();
571
- }
572
- /**
573
- * Install employee-grade standards into the editor's rules file
574
- * (CLAUDE.md for Claude, AGENTS.md for OpenCode, Cline, and Cursor). Wraps content in
575
- * `<!-- OPENSPEC:START -->` / `<!-- OPENSPEC:END -->` markers so future
576
- * updates can replace the block without touching the rest of the file.
577
- */
578
- export function installOpenSpecBlock(projectRoot, standardsContent, adapter = claudeAdapter) {
579
- const dest = adapter.projectRulesPath(projectRoot);
580
- const fileLabel = basename(dest);
581
- const markerStart = "<!-- OPENSPEC:START -->";
582
- const markerEnd = "<!-- OPENSPEC:END -->";
583
- if (!existsSync(dest)) {
584
- const projName = projectRoot.split("/").pop() ?? "Project";
585
- const content = `# ${projName}\n\n${markerStart}\n\n${standardsContent.trim()}\n\n${markerEnd}\n`;
586
- writeFileSync(dest, content);
587
- console.log(chalk.green(` ✓ ${fileLabel}: created with employee-grade standards`));
588
- return;
589
- }
590
- const existing = readFileSync(dest, "utf-8");
591
- const hasStart = existing.includes(markerStart);
592
- const hasEnd = existing.includes(markerEnd);
593
- if (hasStart && hasEnd) {
594
- const startIdx = existing.indexOf(markerStart);
595
- const endIdx = existing.indexOf(markerEnd) + markerEnd.length;
596
- const before = existing.slice(0, startIdx).trimEnd();
597
- const after = existing.slice(endIdx);
598
- const updated = before +
599
- "\n" +
600
- markerStart +
601
- "\n\n" +
602
- standardsContent.trim() +
603
- "\n\n" +
604
- markerEnd +
605
- after;
606
- writeFileSync(dest, updated);
607
- console.log(chalk.green(` ✓ ${fileLabel}: updated employee-grade standards (markers preserved, content refreshed)`));
608
- }
609
- else if (!hasStart && !hasEnd) {
610
- const updated = existing.trim() +
611
- "\n\n" +
612
- markerStart +
613
- "\n\n" +
614
- standardsContent.trim() +
615
- "\n\n" +
616
- markerEnd +
617
- "\n";
618
- writeFileSync(dest, updated);
619
- console.log(chalk.green(` ✓ ${fileLabel}: appended employee-grade standards with markers`));
620
- }
621
- else {
622
- // Incomplete markers (only START, or only END) — corrupted tool territory.
623
- // Keep everything before the first marker (user content), discard the
624
- // truncated tool output after it, and write a clean complete block so
625
- // `doctor`/`update` converge instead of dead-ending on a skipped file.
626
- const firstIdx = hasStart
627
- ? existing.indexOf(markerStart)
628
- : existing.indexOf(markerEnd);
629
- const header = existing.slice(0, firstIdx).trimEnd();
630
- const updated = header +
631
- "\n\n" +
632
- markerStart +
633
- "\n\n" +
634
- standardsContent.trim() +
635
- "\n\n" +
636
- markerEnd +
637
- "\n";
638
- writeFileSync(dest, updated);
639
- console.log(chalk.green(` ✓ ${fileLabel}: repaired incomplete OPENSPEC markers with employee-grade standards`));
640
- }
641
- }
642
- /**
643
- * CodeGraph-first guidance prepended to the Claude wrapper so the model sees
644
- * it in the main rules file instead of relying on the AGENTS.md import
645
- * (imported content ranks lower and is treated as optional by the model).
646
- */
647
- const CODE_GRAPH_FIRST_BLOCK = `## CodeGraph 优先 🔴
648
-
649
- 有 \`.codegraph/\` 时:结构性任务(定义/调用链/影响面/流程)默认第一步用 \`codegraph_explore\`,直接用结果回答,别先 grep/read(仅字面文本、已打开文件、结果不足时补查)。不派子 agent 重建索引。无 \`.codegraph/\` 跳过。`;
650
- /**
651
- * The expected OPENSPEC block content for a thin CLAUDE.md wrapper
652
- * (CodeGraph-first guidance + workflow hint + `@AGENTS.md` import). Exported
653
- * so drift detection / update can compare against it.
654
- *
655
- * The `@AGENTS.md` line is Claude Code's documented way to reuse AGENTS.md
656
- * inside CLAUDE.md — AGENTS.md is NOT read by default ("Claude Code reads
657
- * CLAUDE.md, not AGENTS.md"). Contract per
658
- * https://code.claude.com/docs/en/memory.md:
659
- * - Position is irrelevant — the doc says "@ ... anywhere in your CLAUDE.md"
660
- * (examples even inline it mid-sentence or in a list item). The one real
661
- * constraint: the `@` line must NOT sit inside a code span (backticks) or
662
- * a fenced code block — the resolver skips those. This wrapper keeps the
663
- * import at the end of the block as a bare line.
664
- * - The path resolves relative to the importing CLAUDE.md; import recursion
665
- * is capped at 4 hops.
666
- * - Block-level HTML comments (`<!-- ... -->`) are stripped before context
667
- * injection, so the OPENSPEC markers vanish while the live `@AGENTS.md`
668
- * line inside them is still honored.
669
- */
670
- export function claudeWrapperStandardsContent() {
671
- return `${CODE_GRAPH_FIRST_BLOCK}\n\n**工作流**:优先使用 OpenSpec 工作流(/opsx 命令),而非 plan mode。\n\n@AGENTS.md\n`;
672
- }
673
- /**
674
- * Install a thin CLAUDE.md that imports AGENTS.md.
675
- *
676
- * Uses the same OPENSPEC:START/END markers as the full standards block so
677
- * `cleanProjectRules` can remove it uniformly. The CodeGraph-first block is
678
- * written directly into CLAUDE.md (before the @AGENTS.md import) so Claude
679
- * Code picks it up without depending on the import.
680
- *
681
- * Also handles migration: if CLAUDE.md has an existing OPENSPEC:START block
682
- * (old format that wrote standards directly to CLAUDE.md), calling
683
- * `installOpenSpecBlock` replaces the content with the CodeGraph block +
684
- * `@AGENTS.md` import.
685
- */
686
- export function installClaudeWrapper(projectRoot) {
687
- const dest = join(projectRoot, "CLAUDE.md");
688
- // CLAUDE.md symlinked (typically → AGENTS.md, the officially documented
689
- // reuse pattern): AGENTS.md itself is what Claude Code reads, and
690
- // installProjectRules already keeps the full standards in it. Writing a
691
- // wrapper here would overwrite them through the symlink (and the wrapper's
692
- // @AGENTS.md import would self-reference). Skip instead.
693
- if (existsSync(dest)) {
694
- if (lstatSync(dest).isSymbolicLink()) {
695
- console.log(chalk.gray(" - CLAUDE.md is a symlink to AGENTS.md — standards live there, no wrapper needed"));
696
- return;
697
- }
698
- }
699
- // No-op if our full wrapper (CodeGraph block + @AGENTS.md import) is already
700
- // in place — content-equal, so a user edit inside the markers is detected.
701
- // A bare @AGENTS.md without our markers (added by openspec CLI or the user)
702
- // is left untouched — but tell the user CodeGraph-first won't be written.
703
- if (existsSync(dest)) {
704
- const existing = readFileSync(dest, "utf-8");
705
- const hasMarkers = existing.includes("<!-- OPENSPEC:START -->");
706
- if (!hasMarkers && /^@AGENTS\.md\r?$/m.test(existing)) {
707
- console.log(chalk.yellow(" ⚠ CLAUDE.md 是裸 @AGENTS.md 导入(无 OPENSPEC 标记),CodeGraph 优先约束未写入。如需启用:删除该行后重跑 openspec-pw update。"));
708
- return;
709
- }
710
- if (hasMarkers && blockMatchesExpected(projectRoot, claudeAdapter, claudeWrapperStandardsContent())) {
711
- return;
712
- }
713
- }
714
- // Delegate to installOpenSpecBlock which handles create/update/append
715
- // with OPENSPEC:START/END markers.
716
- installOpenSpecBlock(projectRoot, claudeWrapperStandardsContent(), claudeAdapter);
717
- }
718
- /**
719
- * Route employee-grade standards into project rules files.
720
- *
721
- * AGENTS.md is always the single source of truth, regardless of which
722
- * editors are detected. If Claude is in use, a thin CLAUDE.md wrapper
723
- * with `@AGENTS.md` import is created so Claude loads AGENTS.md as
724
- * its project rules. Cline and Cursor auto-detect AGENTS.md natively — no
725
- * wrapper needed.
726
- */
727
- export function installProjectRules(projectRoot, standardsContent, detected) {
728
- if (detected.length === 0)
729
- return;
730
- // AGENTS.md is always the single source of truth
731
- installOpenSpecBlock(projectRoot, standardsContent, opencodeAdapter);
732
- // Thin CLAUDE.md with @AGENTS.md import if Claude is in use
733
- if (detected.some((a) => a.id === "claude")) {
734
- installClaudeWrapper(projectRoot);
735
- }
736
- // Register AGENTS.md in opencode.json for OpenCode
737
- if (detected.some((a) => a.id === "opencode") && opencodeAdapter.registerInstructions) {
738
- const existing = readOpenCodeInstructions(projectRoot);
739
- const next = Array.from(new Set([...(existing ?? []), "AGENTS.md"]));
740
- opencodeAdapter.registerInstructions(projectRoot, next);
741
- }
742
- }
743
- /** Remove all OpenSpec marker blocks from AGENTS.md (always) and CLAUDE.md (for claude adapter). */
744
- export function cleanProjectRules(adapter, projectRoot) {
745
- // AGENTS.md always has the employee standards (SSOT)
746
- removeMarkersFromFile(join(projectRoot, "AGENTS.md"), "AGENTS.md");
747
- // CLAUDE.md may have the wrapper import if Claude is detected
748
- if (adapter.id === "claude") {
749
- removeMarkersFromFile(adapter.projectRulesPath(projectRoot), basename(adapter.projectRulesPath(projectRoot)));
750
- }
751
- }
752
- /** Remove OpenSpec marker blocks from a single file. Only edits within markers. */
753
- function removeMarkersFromFile(dest, fileLabel) {
754
- if (!existsSync(dest)) {
755
- console.log(chalk.gray(` - ${fileLabel} not found, skipping`));
756
- return;
757
- }
758
- const existing = readFileSync(dest, "utf-8");
759
- if (!existing.includes("<!-- OPENSPEC:START -->")) {
760
- console.log(chalk.gray(` - No OpenSpec markers found in ${fileLabel}`));
761
- return;
762
- }
763
- // Remove markers and their content, consuming surrounding whitespace.
764
- // Then collapse runs of 3+ blank lines to at most 2 for a clean result.
765
- let updated = existing.replace(/\s*<!-- OPENSPEC:START -->[\s\S]*?<!-- OPENSPEC:END -->\s*/g, "\n\n").replace(/\n{3,}/g, "\n\n").trim();
766
- // Delete empty file rather than leaving a ghost.
767
- if (updated === "") {
768
- rmSync(dest);
769
- console.log(chalk.green(` ✓ Removed empty ${fileLabel}`));
770
- return;
771
- }
772
- writeFileSync(dest, updated + "\n");
773
- console.log(chalk.green(` ✓ Removed OpenSpec markers from ${fileLabel}`));
774
- }
775
- /** Read the employee-grade standards source file (empty string if missing). */
776
- export function readEmployeeStandards(srcPath) {
777
- return existsSync(srcPath) ? readFileSync(srcPath, "utf-8") : "";
778
- }
779
- // ─── Adapter instances (registered after helpers above are defined) ──────
780
- //
781
- // We declare them here (not at the top) so they can reference the helper
782
- // functions defined in this same module. JS hoisting covers `function`
783
- // declarations; `const` arrows don't get hoisted, so the order matters.
784
- export const claudeAdapter = defineAdapter({
785
- id: "claude",
786
- label: "claude",
787
- displayName: "Claude Code",
788
- detect: hasClaudeCode,
789
- commandFilePath: getClaudeCommandPath,
790
- formatCommand: formatClaudeCommand,
791
- // Claude Code reads CLAUDE.md directly — override the AGENTS.md default.
792
- projectRulesPath: (root) => join(root, "CLAUDE.md"),
793
- isMcpInstalled(root, serverName) {
794
- // Project scope: Playwright MCP lives in the project-root .mcp.json.
795
- // Read the file directly instead of `claude mcp list` — zero CLI
796
- // dependency, testable, and never confuses a global user-scope entry
797
- // with a project-scoped one.
798
- try {
799
- const mcpJson = readFileSync(join(root, ".mcp.json"), "utf-8");
800
- const parsed = JSON.parse(mcpJson);
801
- return (typeof parsed === "object" &&
802
- parsed !== null &&
803
- typeof parsed.mcpServers === "object" &&
804
- parsed.mcpServers !== null &&
805
- Object.hasOwn(parsed.mcpServers, serverName));
806
- }
807
- catch {
808
- // Missing or unparseable file — server not installed
809
- return false;
810
- }
811
- },
812
- installMcp(_root, serverName, command) {
813
- execFileSync("claude", ["mcp", "add", "--scope", "project", serverName, ...command], {
814
- encoding: "utf-8",
815
- timeout: TIMEOUT.MCP_LIST,
816
- stdio: ["pipe", "pipe", "pipe"],
817
- shell: needsShell,
818
- });
819
- },
820
- removeMcp(_root, serverName) {
821
- execFileSync("claude", ["mcp", "remove", "--scope", "project", serverName], {
822
- encoding: "utf-8",
823
- timeout: TIMEOUT.MCP_LIST,
824
- stdio: ["pipe", "pipe", "pipe"],
825
- shell: needsShell,
826
- });
827
- },
828
- });
829
- export const opencodeAdapter = defineAdapter({
830
- id: "opencode",
831
- label: "opencode",
832
- displayName: "OpenCode",
833
- detect: hasOpenCode,
834
- commandFilePath: getOpenCodeCommandPath,
835
- formatCommand: formatOpenCodeCommand,
836
- // AGENTS.md is the default; not declared explicitly.
837
- isMcpInstalled(projectRoot, serverName) {
838
- const config = findOpenCodeConfig(projectRoot);
839
- if (!config)
840
- return false;
841
- const value = readOpenCodeValue(config.text, ["mcp", serverName]);
842
- return value !== undefined;
843
- },
844
- installMcp(projectRoot, serverName, command) {
845
- setOpenCodeValue(projectRoot, ["mcp", serverName], {
846
- type: "local",
847
- command,
848
- });
849
- },
850
- removeMcp(projectRoot, serverName) {
851
- // Read current mcp map, rebuild without this server, write back
852
- const config = findOpenCodeConfig(projectRoot);
853
- if (!config)
854
- return;
855
- const value = readOpenCodeValue(config.text, ["mcp", serverName]);
856
- if (value === undefined)
857
- return;
858
- const current = readOpenCodeValue(config.text, ["mcp"]);
859
- if (current && typeof current === "object") {
860
- const next = { ...current };
861
- delete next[serverName];
862
- setOpenCodeValue(projectRoot, ["mcp"], next);
863
- }
864
- },
865
- registerInstructions(projectRoot, instructions) {
866
- setOpenCodeValue(projectRoot, ["instructions"], instructions);
867
- },
868
- });
869
- export const clineAdapter = defineAdapter({
870
- id: "cline",
871
- label: "cline",
872
- displayName: "Cline",
873
- detect: hasCline,
874
- commandFilePath: getClineCommandPath,
875
- formatCommand: formatClineCommand,
876
- // Cline auto-detects AGENTS.md — that's the default.
877
- isMcpInstalled(projectRoot, serverName) {
878
- return isMcpServerInFile(clineMcpPath(projectRoot), serverName);
879
- },
880
- installMcp(projectRoot, serverName, command) {
881
- installMcpServerInFile(clineMcpPath(projectRoot), serverName, command);
882
- },
883
- removeMcp(projectRoot, serverName) {
884
- removeMcpServerFromFile(clineMcpPath(projectRoot), serverName);
885
- },
886
- });
887
- export const cursorAdapter = defineAdapter({
888
- id: "cursor",
889
- label: "cursor",
890
- displayName: "Cursor",
891
- detect: hasCursor,
892
- commandFilePath: getCursorCommandPath,
893
- formatCommand: formatCursorCommand,
894
- // Cursor auto-detects AGENTS.md — that's the default.
895
- isMcpInstalled(projectRoot, serverName) {
896
- return isMcpServerInFile(cursorMcpPath(projectRoot), serverName);
897
- },
898
- installMcp(projectRoot, serverName, command) {
899
- installMcpServerInFile(cursorMcpPath(projectRoot), serverName, command);
900
- },
901
- removeMcp(projectRoot, serverName) {
902
- removeMcpServerFromFile(cursorMcpPath(projectRoot), serverName);
903
- },
904
- extraArtifacts(meta) {
905
- return [
906
- {
907
- relativePath: getCursorSkillPath(meta.id),
908
- contents: formatCursorSkill(meta),
909
- },
910
- ];
911
- },
912
- });
913
- export const piAdapter = defineAdapter({
914
- id: "pi",
915
- label: "pi",
916
- displayName: "Pi",
917
- detect: hasPi,
918
- // Pi has no MCP client — shared MCP phases skip this adapter.
919
- supportsMcp: false,
920
- commandFilePath: getPiCommandPath,
921
- formatCommand: formatPiCommand,
922
- // Pi loads AGENTS.md natively (walking up from cwd) — that's the default.
923
- });
924
- export const ompAdapter = defineAdapter({
925
- id: "omp",
926
- label: "omp",
927
- displayName: "Oh My Pi",
928
- detect: hasOmp,
929
- commandFilePath: getOmpCommandPath,
930
- formatCommand: formatOmpCommand,
931
- // omp reads AGENTS.md natively — that's the default.
932
- isMcpInstalled(projectRoot, serverName) {
933
- return isMcpServerInFile(ompMcpPath(projectRoot), serverName);
934
- },
935
- installMcp(projectRoot, serverName, command) {
936
- installMcpServerInFile(ompMcpPath(projectRoot), serverName, command);
937
- },
938
- removeMcp(projectRoot, serverName) {
939
- removeMcpServerFromFile(ompMcpPath(projectRoot), serverName);
940
- },
941
- });
942
- export const dshAdapter = defineAdapter({
943
- id: "dsh",
944
- label: "dsh",
945
- displayName: "DeepSeek Harness",
946
- detect: hasDsh,
947
- // dsh configures MCP via cordis.yml plugin config, not a simple file —
948
- // shared MCP phases skip this adapter (configure Playwright MCP manually).
949
- supportsMcp: false,
950
- commandFilePath: getDshCommandPath,
951
- formatCommand: formatDshCommand,
952
- // dsh reads AGENTS.md natively — that's the default.
953
- });
954
- // Register the adapters now that the const arrows exist
955
- registerAdapter(claudeAdapter);
956
- registerAdapter(opencodeAdapter);
957
- registerAdapter(clineAdapter);
958
- registerAdapter(cursorAdapter);
959
- registerAdapter(piAdapter);
960
- registerAdapter(ompAdapter);
961
- registerAdapter(dshAdapter);
2
+ * ─────────────────────────────────────────────────────────────────────────
3
+ * FACADE FILE — implementation lives in ./editors/*
4
+ * ─────────────────────────────────────────────────────────────────────────
5
+ * This file only re-exports the public API of the editor adapter layer so
6
+ * every existing `import ... from ".../editors.js"` keeps working.
7
+ *
8
+ * Why a facade instead of letting callers import "./editors/index.js"?
9
+ * Node ESM and TypeScript nodenext do NOT resolve directory indexes:
10
+ * `"./editors.js"` with only `editors/index.ts` on disk fails (TS2307).
11
+ * Keeping this thin file preserves zero-churn imports for all callers.
12
+ *
13
+ * Do NOT add logic here. Add implementations under ./editors/ and
14
+ * re-export below.
15
+ *
16
+ * Re-export order matters for adapter self-registration: importing the
17
+ * adapter modules loads them, and each calls `registerAdapter()` — the
18
+ * order below fixes ADAPTERS registration order (claude opencode →
19
+ * cline → cursor → pi → omp → dsh), which `resolveToolsArg("all")`, the
20
+ * interactive prompt order, and tests/editors-tools.test.ts depend on.
21
+ */
22
+ // Types & factory (zero internal deps)
23
+ export { buildCommandMeta, defineAdapter, } from "./editors/types.js";
24
+ // Cross-editor shared helpers
25
+ export { escapeYamlValue, formatTagsArray, transformToHyphenCommands, readMcpServersFile, writeMcpServersFile, isMcpServerInFile, installMcpServerInFile, removeMcpServerFromFile, } from "./editors/shared.js";
26
+ // Registry (lookup / detection / install helpers) — no adapter imports here
27
+ export { registerAdapter, getAdapter, getAllAdapters, detectAdapters, slashCommandForAdapter, listCommandArtifactPaths, installCommand, } from "./editors/registry.js";
28
+ // Adapters import order = registration order (do not reorder)
29
+ export { formatClaudeCommand, getClaudeCommandPath, hasClaudeCode, claudeAdapter, } from "./editors/adapters/claude.js";
30
+ export { formatOpenCodeCommand, getOpenCodeCommandPath, hasOpenCode, opencodeAdapter, readOpenCodeInstructions, } from "./editors/adapters/opencode.js";
31
+ export { formatClineCommand, getClineCommandPath, hasCline, clineAdapter, } from "./editors/adapters/cline.js";
32
+ export { formatCursorCommand, getCursorCommandPath, getCursorSkillPath, formatCursorSkill, hasCursor, cursorAdapter, } from "./editors/adapters/cursor.js";
33
+ export { formatPiCommand, getPiCommandPath, hasPi, piAdapter, } from "./editors/adapters/pi.js";
34
+ export { formatOmpCommand, getOmpCommandPath, hasOmp, ompAdapter, } from "./editors/adapters/omp.js";
35
+ export { formatDshCommand, getDshCommandPath, hasDsh, dshAdapter, } from "./editors/adapters/dsh.js";
36
+ // Tool selection (--tools flag parsing)
37
+ export { resolveToolsArg } from "./editors/tool-selection.js";
38
+ // Project rules file management (AGENTS.md SSOT + CLAUDE.md wrapper)
39
+ export { readOpenSpecBlock, blockMatchesExpected, installOpenSpecBlock, claudeWrapperStandardsContent, installClaudeWrapper, installProjectRules, cleanProjectRules, readEmployeeStandards, } from "./editors/project-rules.js";
962
40
  //# sourceMappingURL=editors.js.map