openspec-playwright 0.3.73 → 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.
- package/dist/commands/editors/adapters/claude.d.ts +5 -0
- package/dist/commands/editors/adapters/claude.js +79 -0
- package/dist/commands/editors/adapters/claude.js.map +1 -0
- package/dist/commands/editors/adapters/cline.d.ts +15 -0
- package/dist/commands/editors/adapters/cline.js +63 -0
- package/dist/commands/editors/adapters/cline.js.map +1 -0
- package/dist/commands/editors/adapters/cursor.d.ts +15 -0
- package/dist/commands/editors/adapters/cursor.js +79 -0
- package/dist/commands/editors/adapters/cursor.js.map +1 -0
- package/dist/commands/editors/adapters/dsh.d.ts +27 -0
- package/dist/commands/editors/adapters/dsh.js +67 -0
- package/dist/commands/editors/adapters/dsh.js.map +1 -0
- package/dist/commands/editors/adapters/omp.d.ts +24 -0
- package/dist/commands/editors/adapters/omp.js +73 -0
- package/dist/commands/editors/adapters/omp.js.map +1 -0
- package/dist/commands/editors/adapters/opencode.d.ts +8 -0
- package/dist/commands/editors/adapters/opencode.js +133 -0
- package/dist/commands/editors/adapters/opencode.js.map +1 -0
- package/dist/commands/editors/adapters/pi.d.ts +27 -0
- package/dist/commands/editors/adapters/pi.js +64 -0
- package/dist/commands/editors/adapters/pi.js.map +1 -0
- package/dist/commands/editors/project-rules.d.ts +69 -0
- package/dist/commands/editors/project-rules.js +246 -0
- package/dist/commands/editors/project-rules.js.map +1 -0
- package/dist/commands/editors/registry.d.ts +12 -0
- package/dist/commands/editors/registry.js +59 -0
- package/dist/commands/editors/registry.js.map +1 -0
- package/dist/commands/editors/shared.d.ts +37 -0
- package/dist/commands/editors/shared.js +92 -0
- package/dist/commands/editors/shared.js.map +1 -0
- package/dist/commands/editors/tool-selection.d.ts +17 -0
- package/dist/commands/editors/tool-selection.js +60 -0
- package/dist/commands/editors/tool-selection.js.map +1 -0
- package/dist/commands/editors/types.d.ts +82 -0
- package/dist/commands/editors/types.js +41 -0
- package/dist/commands/editors/types.js.map +1 -0
- package/dist/commands/editors.d.ts +32 -309
- package/dist/commands/editors.js +38 -960
- package/dist/commands/editors.js.map +1 -1
- package/dist/commands/update.d.ts +8 -0
- package/dist/commands/update.js +1 -1
- package/dist/commands/update.js.map +1 -1
- package/package.json +5 -5
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type EditorAdapter, type CommandMeta } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Pi stores project resources in `.pi/` (skills/, prompts/, extensions/).
|
|
4
|
+
* Prompt templates are Markdown with optional YAML frontmatter; the
|
|
5
|
+
* filename becomes the slash command name (`opsx-e2e.md` → `/opsx-e2e`),
|
|
6
|
+
* `description` feeds autocomplete, and `argument-hint` shows expected
|
|
7
|
+
* arguments. `$1` / `$ARGUMENTS` placeholders expand at prompt time.
|
|
8
|
+
*
|
|
9
|
+
* Conventions follow the Pi docs (packages/coding-agent/docs):
|
|
10
|
+
* - Prompts: `.pi/prompts/*.md`, invoked via `/name`.
|
|
11
|
+
* - Skills: `.pi/skills/` (root `.md` files or `SKILL.md` dirs),
|
|
12
|
+
* invoked via `/skill:name`.
|
|
13
|
+
* - Rules: Pi loads `AGENTS.md` (or `CLAUDE.md`) walking up from cwd
|
|
14
|
+
* natively — no wrapper file needed.
|
|
15
|
+
* - MCP: Pi has NO MCP client (built-in tools only). The adapter
|
|
16
|
+
* still implements the interface with no-ops and declares
|
|
17
|
+
* `supportsMcp: false` so shared MCP phases skip it.
|
|
18
|
+
*/
|
|
19
|
+
export declare function formatPiCommand(meta: CommandMeta): string;
|
|
20
|
+
export declare function getPiCommandPath(id: string): string;
|
|
21
|
+
/**
|
|
22
|
+
* True when Pi is in use: a project `.pi/` dir, or a global Pi config dir
|
|
23
|
+
* (`~/.pi/agent/`, created by Pi on first run). The home signal lets
|
|
24
|
+
* `openspec-pw init` detect Pi in a fresh project that has no `.pi/` yet.
|
|
25
|
+
*/
|
|
26
|
+
export declare function hasPi(projectRoot: string, homeDir?: string): boolean;
|
|
27
|
+
export declare const piAdapter: EditorAdapter;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pi adapter: prompt template at `.pi/prompts/opsx-<id>.md` (filename =
|
|
3
|
+
* slash command). NO MCP client — `supportsMcp: false` makes shared MCP
|
|
4
|
+
* phases skip it. Detected via `.pi/` or global `~/.pi/agent/`.
|
|
5
|
+
*/
|
|
6
|
+
import { existsSync } from "fs";
|
|
7
|
+
import { homedir } from "os";
|
|
8
|
+
import { join } from "path";
|
|
9
|
+
import { defineAdapter } from "../types.js";
|
|
10
|
+
import { escapeYamlValue, transformToHyphenCommands } from "../shared.js";
|
|
11
|
+
import { registerAdapter } from "../registry.js";
|
|
12
|
+
// ─── Pi adapter ──────────────────────────────────────────────────────────
|
|
13
|
+
/**
|
|
14
|
+
* Pi stores project resources in `.pi/` (skills/, prompts/, extensions/).
|
|
15
|
+
* Prompt templates are Markdown with optional YAML frontmatter; the
|
|
16
|
+
* filename becomes the slash command name (`opsx-e2e.md` → `/opsx-e2e`),
|
|
17
|
+
* `description` feeds autocomplete, and `argument-hint` shows expected
|
|
18
|
+
* arguments. `$1` / `$ARGUMENTS` placeholders expand at prompt time.
|
|
19
|
+
*
|
|
20
|
+
* Conventions follow the Pi docs (packages/coding-agent/docs):
|
|
21
|
+
* - Prompts: `.pi/prompts/*.md`, invoked via `/name`.
|
|
22
|
+
* - Skills: `.pi/skills/` (root `.md` files or `SKILL.md` dirs),
|
|
23
|
+
* invoked via `/skill:name`.
|
|
24
|
+
* - Rules: Pi loads `AGENTS.md` (or `CLAUDE.md`) walking up from cwd
|
|
25
|
+
* natively — no wrapper file needed.
|
|
26
|
+
* - MCP: Pi has NO MCP client (built-in tools only). The adapter
|
|
27
|
+
* still implements the interface with no-ops and declares
|
|
28
|
+
* `supportsMcp: false` so shared MCP phases skip it.
|
|
29
|
+
*/
|
|
30
|
+
export function formatPiCommand(meta) {
|
|
31
|
+
const body = transformToHyphenCommands(meta.body);
|
|
32
|
+
return `---
|
|
33
|
+
description: ${escapeYamlValue(meta.description)}
|
|
34
|
+
argument-hint: "<change-name>"
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
${body}
|
|
38
|
+
`;
|
|
39
|
+
}
|
|
40
|
+
export function getPiCommandPath(id) {
|
|
41
|
+
return join(".pi", "prompts", `opsx-${id}.md`);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* True when Pi is in use: a project `.pi/` dir, or a global Pi config dir
|
|
45
|
+
* (`~/.pi/agent/`, created by Pi on first run). The home signal lets
|
|
46
|
+
* `openspec-pw init` detect Pi in a fresh project that has no `.pi/` yet.
|
|
47
|
+
*/
|
|
48
|
+
export function hasPi(projectRoot, homeDir = homedir()) {
|
|
49
|
+
return (existsSync(join(projectRoot, ".pi")) ||
|
|
50
|
+
existsSync(join(homeDir, ".pi", "agent")));
|
|
51
|
+
}
|
|
52
|
+
export const piAdapter = defineAdapter({
|
|
53
|
+
id: "pi",
|
|
54
|
+
label: "pi",
|
|
55
|
+
displayName: "Pi",
|
|
56
|
+
detect: hasPi,
|
|
57
|
+
// Pi has no MCP client — shared MCP phases skip this adapter.
|
|
58
|
+
supportsMcp: false,
|
|
59
|
+
commandFilePath: getPiCommandPath,
|
|
60
|
+
formatCommand: formatPiCommand,
|
|
61
|
+
// Pi loads AGENTS.md natively (walking up from cwd) — that's the default.
|
|
62
|
+
});
|
|
63
|
+
registerAdapter(piAdapter);
|
|
64
|
+
//# sourceMappingURL=pi.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pi.js","sourceRoot":"","sources":["../../../../src/commands/editors/adapters/pi.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,IAAI,CAAC;AAChC,OAAO,EAAE,OAAO,EAAE,MAAM,IAAI,CAAC;AAC7B,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAC5B,OAAO,EAAE,aAAa,EAAwC,MAAM,aAAa,CAAC;AAClF,OAAO,EAAE,eAAe,EAAE,yBAAyB,EAAE,MAAM,cAAc,CAAC;AAC1E,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAEjD,4EAA4E;AAE5E;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,eAAe,CAAC,IAAiB;IAC/C,MAAM,IAAI,GAAG,yBAAyB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAClD,OAAO;eACM,eAAe,CAAC,IAAI,CAAC,WAAW,CAAC;;;;EAI9C,IAAI;CACL,CAAC;AACF,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,EAAU;IACzC,OAAO,IAAI,CAAC,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,KAAK,CAAC,CAAC;AACjD,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,KAAK,CACnB,WAAmB,EACnB,OAAO,GAAG,OAAO,EAAE;IAEnB,OAAO,CACL,UAAU,CAAC,IAAI,CAAC,WAAW,EAAE,KAAK,CAAC,CAAC;QACpC,UAAU,CAAC,IAAI,CAAC,OAAO,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC,CAC1C,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,MAAM,SAAS,GAAkB,aAAa,CAAC;IACpD,EAAE,EAAE,IAAI;IACR,KAAK,EAAE,IAAI;IACX,WAAW,EAAE,IAAI;IACjB,MAAM,EAAE,KAAK;IACb,8DAA8D;IAC9D,WAAW,EAAE,KAAK;IAClB,eAAe,EAAE,gBAAgB;IACjC,aAAa,EAAE,eAAe;IAC9B,0EAA0E;CAC3E,CAAC,CAAC;AAEH,eAAe,CAAC,SAAS,CAAC,CAAC"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { EditorAdapter } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Read the OPENSPEC marker block from a rules file, or `null` when the file
|
|
4
|
+
* is missing / has no markers. Used by drift detection and update to decide
|
|
5
|
+
* whether a rules file needs rewriting.
|
|
6
|
+
*/
|
|
7
|
+
export declare function readOpenSpecBlock(projectRoot: string, adapter: EditorAdapter): string | null;
|
|
8
|
+
/**
|
|
9
|
+
* Whether a rules file's OPENSPEC block matches the expected content.
|
|
10
|
+
* A missing file or absent/truncated markers counts as "does not match"
|
|
11
|
+
* (the caller will rewrite it), which keeps update idempotent but safe.
|
|
12
|
+
*/
|
|
13
|
+
export declare function blockMatchesExpected(projectRoot: string, adapter: EditorAdapter, expected: string): boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Install employee-grade standards into the editor's rules file
|
|
16
|
+
* (CLAUDE.md for Claude, AGENTS.md for OpenCode, Cline, and Cursor). Wraps content in
|
|
17
|
+
* `<!-- OPENSPEC:START -->` / `<!-- OPENSPEC:END -->` markers so future
|
|
18
|
+
* updates can replace the block without touching the rest of the file.
|
|
19
|
+
*/
|
|
20
|
+
export declare function installOpenSpecBlock(projectRoot: string, standardsContent: string, adapter?: EditorAdapter): void;
|
|
21
|
+
/**
|
|
22
|
+
* The expected OPENSPEC block content for a thin CLAUDE.md wrapper
|
|
23
|
+
* (CodeGraph-first guidance + workflow hint + `@AGENTS.md` import). Exported
|
|
24
|
+
* so drift detection / update can compare against it.
|
|
25
|
+
*
|
|
26
|
+
* The `@AGENTS.md` line is Claude Code's documented way to reuse AGENTS.md
|
|
27
|
+
* inside CLAUDE.md — AGENTS.md is NOT read by default ("Claude Code reads
|
|
28
|
+
* CLAUDE.md, not AGENTS.md"). Contract per
|
|
29
|
+
* https://code.claude.com/docs/en/memory.md:
|
|
30
|
+
* - Position is irrelevant — the doc says "@ ... anywhere in your CLAUDE.md"
|
|
31
|
+
* (examples even inline it mid-sentence or in a list item). The one real
|
|
32
|
+
* constraint: the `@` line must NOT sit inside a code span (backticks) or
|
|
33
|
+
* a fenced code block — the resolver skips those. This wrapper keeps the
|
|
34
|
+
* import at the end of the block as a bare line.
|
|
35
|
+
* - The path resolves relative to the importing CLAUDE.md; import recursion
|
|
36
|
+
* is capped at 4 hops.
|
|
37
|
+
* - Block-level HTML comments (`<!-- ... -->`) are stripped before context
|
|
38
|
+
* injection, so the OPENSPEC markers vanish while the live `@AGENTS.md`
|
|
39
|
+
* line inside them is still honored.
|
|
40
|
+
*/
|
|
41
|
+
export declare function claudeWrapperStandardsContent(): string;
|
|
42
|
+
/**
|
|
43
|
+
* Install a thin CLAUDE.md that imports AGENTS.md.
|
|
44
|
+
*
|
|
45
|
+
* Uses the same OPENSPEC:START/END markers as the full standards block so
|
|
46
|
+
* `cleanProjectRules` can remove it uniformly. The CodeGraph-first block is
|
|
47
|
+
* written directly into CLAUDE.md (before the @AGENTS.md import) so Claude
|
|
48
|
+
* Code picks it up without depending on the import.
|
|
49
|
+
*
|
|
50
|
+
* Also handles migration: if CLAUDE.md has an existing OPENSPEC:START block
|
|
51
|
+
* (old format that wrote standards directly to CLAUDE.md), calling
|
|
52
|
+
* `installOpenSpecBlock` replaces the content with the CodeGraph block +
|
|
53
|
+
* `@AGENTS.md` import.
|
|
54
|
+
*/
|
|
55
|
+
export declare function installClaudeWrapper(projectRoot: string): void;
|
|
56
|
+
/**
|
|
57
|
+
* Route employee-grade standards into project rules files.
|
|
58
|
+
*
|
|
59
|
+
* AGENTS.md is always the single source of truth, regardless of which
|
|
60
|
+
* editors are detected. If Claude is in use, a thin CLAUDE.md wrapper
|
|
61
|
+
* with `@AGENTS.md` import is created so Claude loads AGENTS.md as
|
|
62
|
+
* its project rules. Cline and Cursor auto-detect AGENTS.md natively — no
|
|
63
|
+
* wrapper needed.
|
|
64
|
+
*/
|
|
65
|
+
export declare function installProjectRules(projectRoot: string, standardsContent: string, detected: EditorAdapter[]): void;
|
|
66
|
+
/** Remove all OpenSpec marker blocks from AGENTS.md (always) and CLAUDE.md (for claude adapter). */
|
|
67
|
+
export declare function cleanProjectRules(adapter: EditorAdapter, projectRoot: string): void;
|
|
68
|
+
/** Read the employee-grade standards source file (empty string if missing). */
|
|
69
|
+
export declare function readEmployeeStandards(srcPath: string): string;
|
|
@@ -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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"}
|