compound-workflow 1.8.0 → 1.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +62 -68
- package/package.json +2 -8
- package/scripts/check-pack-readme.mjs +1 -16
- package/scripts/check-workflow-contracts.mjs +34 -44
- package/scripts/install-cli.mjs +273 -555
- package/src/AGENTS.md +59 -192
- package/src/{.agents/agents → agents}/research/best-practices-researcher.md +2 -2
- package/src/{.agents/commands → commands}/assess.md +4 -4
- package/src/commands/install.md +43 -0
- package/src/{.agents/commands → commands}/metrics.md +1 -1
- package/src/{.agents/commands → commands}/test-browser.md +8 -8
- package/src/commands/workflow-agents.md +101 -0
- package/src/{.agents/commands → commands}/workflow-brainstorm.md +5 -5
- package/src/{.agents/commands → commands}/workflow-compound.md +1 -1
- package/src/{.agents/commands → commands}/workflow-plan.md +62 -85
- package/src/commands/workflow-work.md +839 -0
- package/src/{.agents/references → references}/README.md +1 -1
- package/src/{.agents/skills → skills}/capture-skill/SKILL.md +1 -1
- package/src/{.agents/skills → skills}/compound-docs/SKILL.md +6 -6
- package/src/{.agents/skills → skills}/compound-docs/references/yaml-schema.md +2 -2
- package/src/skills/setup-agents/SKILL.md +247 -0
- package/src/skills/standards/SKILL.md +79 -0
- package/src/skills/standards/references/architecture.md +228 -0
- package/src/skills/standards/references/code-quality.md +192 -0
- package/src/skills/standards/references/presentation.md +515 -0
- package/src/skills/standards/references/services.md +172 -0
- package/src/skills/standards/references/state-management.md +936 -0
- package/.claude-plugin/plugin.json +0 -7
- package/.cursor-plugin/plugin.json +0 -20
- package/.cursor-plugin/registration.json +0 -5
- package/scripts/check-version-parity.mjs +0 -36
- package/scripts/generate-platform-artifacts.mjs +0 -230
- package/src/.agents/commands/install.md +0 -51
- package/src/.agents/commands/workflow-work.md +0 -690
- package/src/.agents/registry.json +0 -48
- package/src/.agents/scripts/self-check.mjs +0 -227
- package/src/.agents/scripts/sync-opencode.mjs +0 -362
- package/src/.agents/skills/presentation-composability/SKILL.md +0 -72
- package/src/.agents/skills/react-ddd-mvc-frontend/SKILL.md +0 -51
- package/src/.agents/skills/react-ddd-mvc-frontend/references/feature-structure.md +0 -25
- package/src/.agents/skills/react-ddd-mvc-frontend/references/implementation-principles.md +0 -21
- package/src/.agents/skills/react-ddd-mvc-frontend/references/responsibility-gates.md +0 -41
- package/src/.agents/skills/react-ddd-mvc-frontend/references/source-map.md +0 -11
- package/src/.agents/skills/standards/SKILL.md +0 -747
- package/src/.agents/skills/xstate-actor-orchestration/SKILL.md +0 -197
- package/src/.agents/skills/xstate-actor-orchestration/agents/openai.yaml +0 -4
- package/src/.agents/skills/xstate-actor-orchestration/assets/statecharts/.gitkeep +0 -0
- package/src/.agents/skills/xstate-actor-orchestration/references/actor-system-patterns.md +0 -71
- package/src/.agents/skills/xstate-actor-orchestration/references/event-contracts.md +0 -73
- package/src/.agents/skills/xstate-actor-orchestration/references/functional-domain-patterns.md +0 -53
- package/src/.agents/skills/xstate-actor-orchestration/references/machine-structure-and-tags.md +0 -36
- package/src/.agents/skills/xstate-actor-orchestration/references/react-container-pattern.md +0 -45
- package/src/.agents/skills/xstate-actor-orchestration/references/reliability-observability.md +0 -39
- package/src/.agents/skills/xstate-actor-orchestration/references/skill-validation.md +0 -33
- package/src/.agents/skills/xstate-actor-orchestration/references/source-map.md +0 -44
- package/src/.agents/skills/xstate-actor-orchestration/references/statechart-review-and-signoff.md +0 -59
- package/src/.agents/skills/xstate-actor-orchestration/references/testing-strategy.md +0 -35
- package/src/.agents/skills/xstate-actor-orchestration/scripts/create-statechart-artifact.sh +0 -71
- package/src/.agents/skills/xstate-actor-orchestration/scripts/validate-skill.sh +0 -138
- package/src/generated/opencode.managed.json +0 -115
- /package/src/{.agents/agents → agents}/research/framework-docs-researcher.md +0 -0
- /package/src/{.agents/agents → agents}/research/git-history-analyzer.md +0 -0
- /package/src/{.agents/agents → agents}/research/learnings-researcher.md +0 -0
- /package/src/{.agents/agents → agents}/research/repo-research-analyst.md +0 -0
- /package/src/{.agents/agents → agents}/review/agent-native-reviewer.md +0 -0
- /package/src/{.agents/agents → agents}/review/planning-technical-reviewer.md +0 -0
- /package/src/{.agents/agents → agents}/workflow/bug-reproduction-validator.md +0 -0
- /package/src/{.agents/agents → agents}/workflow/lint.md +0 -0
- /package/src/{.agents/agents → agents}/workflow/spec-flow-analyzer.md +0 -0
- /package/src/{.agents/commands → commands}/workflow-review.md +0 -0
- /package/src/{.agents/commands → commands}/workflow-tech-review.md +0 -0
- /package/src/{.agents/commands → commands}/workflow-triage.md +0 -0
- /package/src/{.agents/references → references}/standards/README.md +0 -0
- /package/src/{.agents/skills → skills}/agent-browser/SKILL.md +0 -0
- /package/src/{.agents/skills → skills}/audit-traceability/SKILL.md +0 -0
- /package/src/{.agents/skills → skills}/brainstorming/SKILL.md +0 -0
- /package/src/{.agents/skills → skills}/compound-docs/assets/critical-pattern-template.md +0 -0
- /package/src/{.agents/skills → skills}/compound-docs/assets/resolution-template.md +0 -0
- /package/src/{.agents/skills → skills}/compound-docs/schema.project.yaml +0 -0
- /package/src/{.agents/skills → skills}/compound-docs/schema.yaml +0 -0
- /package/src/{.agents/skills → skills}/data-foundations/SKILL.md +0 -0
- /package/src/{.agents/skills → skills}/document-review/SKILL.md +0 -0
- /package/src/{.agents/skills → skills}/file-todos/SKILL.md +0 -0
- /package/src/{.agents/skills → skills}/file-todos/assets/todo-template.md +0 -0
- /package/src/{.agents/skills → skills}/financial-workflow-integrity/SKILL.md +0 -0
- /package/src/{.agents/skills → skills}/git-worktree/SKILL.md +0 -0
- /package/src/{.agents/skills → skills}/pii-protection-prisma/SKILL.md +0 -0
- /package/src/{.agents/skills → skills}/process-metrics/SKILL.md +0 -0
- /package/src/{.agents/skills → skills}/process-metrics/assets/daily-template.md +0 -0
- /package/src/{.agents/skills → skills}/process-metrics/assets/monthly-template.md +0 -0
- /package/src/{.agents/skills → skills}/process-metrics/assets/weekly-template.md +0 -0
- /package/src/{.agents/skills → skills}/technical-review/SKILL.md +0 -0
package/scripts/install-cli.mjs
CHANGED
|
@@ -2,195 +2,237 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* compound-workflow install
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* Copies agents, skills, and commands from the package into target platform dirs:
|
|
6
|
+
* .claude/agents/ — flat agent .md files (Claude Code)
|
|
7
|
+
* .cursor/agents/ — agents preserving subdirs (Cursor)
|
|
8
|
+
* .cursor/skills/ — skill dirs (Cursor)
|
|
9
|
+
* .cursor/commands/ — command .md files (Cursor)
|
|
10
|
+
* .agents/agents/ — agents (OpenCode / general)
|
|
11
|
+
* .agents/skills/ — skills (OpenCode / general)
|
|
12
|
+
* .agents/commands/ — commands (OpenCode / general)
|
|
7
13
|
*
|
|
8
|
-
*
|
|
9
|
-
* - Commands: add/remove .md under src/.agents/commands/ (frontmatter: invocation, name, description).
|
|
10
|
-
* Registry (src/.agents/registry.json) + generate-platform-artifacts → opencode.managed.json → install.
|
|
11
|
-
* - Agents: add/remove .md under src/.agents/agents/ (frontmatter: name, description). Same pipeline.
|
|
12
|
-
* - Skills: add/remove dir src/.agents/skills/<name>/SKILL.md. OpenCode uses skills path.
|
|
14
|
+
* Also writes opencode.json, AGENTS.md, and standard docs directories.
|
|
13
15
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* -
|
|
17
|
-
* - OpenCode: opencode.json with paths into package source.
|
|
18
|
-
* Run install (or npm install compound-workflow) after any change; no other registration needed.
|
|
16
|
+
* Usage:
|
|
17
|
+
* (automatic) npm install compound-workflow # runs via postinstall
|
|
18
|
+
* (manual) npx compound-workflow install [--root <projectDir>] [--dry-run]
|
|
19
19
|
*/
|
|
20
20
|
import fs from "node:fs";
|
|
21
21
|
import path from "node:path";
|
|
22
|
-
import os from "node:os";
|
|
23
22
|
import { fileURLToPath } from "node:url";
|
|
24
23
|
import { spawnSync } from "node:child_process";
|
|
25
24
|
|
|
26
25
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
27
26
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
(automatic) npm install compound-workflow # runs install via postinstall; no npx needed
|
|
32
|
-
(manual) npx compound-workflow install [--root <projectDir>] [--dry-run] [--no-config]
|
|
33
|
-
|
|
34
|
-
Install writes opencode.json (from package), merges AGENTS.md, creates standard
|
|
35
|
-
docs/todos directories, copies skills/commands/agents into .cursor/ for Cursor,
|
|
36
|
-
and registers the Claude Code plugin (project-scoped).
|
|
37
|
-
|
|
38
|
-
--root <dir> Project directory (default: cwd)
|
|
39
|
-
--dry-run Print planned changes only
|
|
40
|
-
--no-config Skip Repo Config Block reminder
|
|
41
|
-
`;
|
|
42
|
-
(exitCode === 0 ? console.log : console.error)(msg.trimStart());
|
|
43
|
-
process.exit(exitCode);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function parseArgs(argv) {
|
|
47
|
-
const out = { root: process.cwd(), dryRun: false, noConfig: false };
|
|
48
|
-
for (let i = 2; i < argv.length; i++) {
|
|
49
|
-
const arg = argv[i];
|
|
50
|
-
if (arg === "--dry-run") out.dryRun = true;
|
|
51
|
-
else if (arg === "--no-config") out.noConfig = true;
|
|
52
|
-
else if (arg === "--root") {
|
|
53
|
-
const value = argv[i + 1];
|
|
54
|
-
if (!value) usage(1);
|
|
55
|
-
out.root = value;
|
|
56
|
-
i++;
|
|
57
|
-
} else if (arg === "install") {
|
|
58
|
-
continue;
|
|
59
|
-
} else if (arg === "-h" || arg === "--help") usage(0);
|
|
60
|
-
else usage(1);
|
|
61
|
-
}
|
|
62
|
-
return out;
|
|
63
|
-
}
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// Utilities
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
64
30
|
|
|
65
31
|
function realpathSafe(p) {
|
|
66
|
-
try {
|
|
67
|
-
return fs.realpathSync(p);
|
|
68
|
-
} catch {
|
|
69
|
-
return path.resolve(p);
|
|
70
|
-
}
|
|
32
|
+
try { return fs.realpathSync(p); } catch { return path.resolve(p); }
|
|
71
33
|
}
|
|
72
34
|
|
|
35
|
+
const PACKAGE_ROOT = realpathSafe(path.join(__dirname, ".."));
|
|
36
|
+
|
|
73
37
|
function hasCommand(cmd) {
|
|
74
38
|
const checker = process.platform === "win32" ? "where" : "which";
|
|
75
|
-
|
|
76
|
-
return result.status === 0;
|
|
39
|
+
return spawnSync(checker, [cmd], { stdio: "ignore" }).status === 0;
|
|
77
40
|
}
|
|
78
41
|
|
|
79
42
|
function stripJsonc(input) {
|
|
80
|
-
let out = "";
|
|
81
|
-
let i = 0;
|
|
82
|
-
let inStr = false;
|
|
83
|
-
let strQuote = "";
|
|
84
|
-
let escape = false;
|
|
85
|
-
|
|
43
|
+
let out = "", i = 0, inStr = false, strQuote = "", escape = false;
|
|
86
44
|
while (i < input.length) {
|
|
87
|
-
const c = input[i];
|
|
88
|
-
const n = input[i + 1];
|
|
45
|
+
const c = input[i], n = input[i + 1];
|
|
89
46
|
if (inStr) {
|
|
90
47
|
out += c;
|
|
91
48
|
if (escape) escape = false;
|
|
92
49
|
else if (c === "\\") escape = true;
|
|
93
50
|
else if (c === strQuote) inStr = false;
|
|
94
|
-
i++;
|
|
95
|
-
continue;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
if (c === '"' || c === "'") {
|
|
99
|
-
inStr = true;
|
|
100
|
-
strQuote = c;
|
|
101
|
-
out += c;
|
|
102
|
-
i++;
|
|
103
|
-
continue;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
if (c === "/" && n === "/") {
|
|
107
|
-
while (i < input.length && input[i] !== "\n") i++;
|
|
108
|
-
continue;
|
|
51
|
+
i++; continue;
|
|
109
52
|
}
|
|
110
|
-
|
|
111
|
-
if (c === "/" && n === "
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
i += 2;
|
|
115
|
-
continue;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
out += c;
|
|
119
|
-
i++;
|
|
53
|
+
if (c === '"' || c === "'") { inStr = true; strQuote = c; out += c; i++; continue; }
|
|
54
|
+
if (c === "/" && n === "/") { while (i < input.length && input[i] !== "\n") i++; continue; }
|
|
55
|
+
if (c === "/" && n === "*") { i += 2; while (i < input.length && !(input[i] === "*" && input[i + 1] === "/")) i++; i += 2; continue; }
|
|
56
|
+
out += c; i++;
|
|
120
57
|
}
|
|
121
|
-
|
|
122
58
|
return out;
|
|
123
59
|
}
|
|
124
60
|
|
|
125
61
|
function readJsonMaybe(fileAbs) {
|
|
126
62
|
if (!fs.existsSync(fileAbs)) return null;
|
|
127
|
-
|
|
128
|
-
return JSON.parse(stripJsonc(raw));
|
|
63
|
+
return JSON.parse(stripJsonc(fs.readFileSync(fileAbs, "utf8")));
|
|
129
64
|
}
|
|
130
65
|
|
|
131
66
|
function ensureObject(v) {
|
|
132
67
|
return v && typeof v === "object" && !Array.isArray(v) ? v : {};
|
|
133
68
|
}
|
|
134
69
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
);
|
|
70
|
+
function parseFrontmatter(md) {
|
|
71
|
+
if (!md.startsWith("---\n") && !md.startsWith("---\r\n")) return {};
|
|
72
|
+
const end = md.indexOf("\n---", 4);
|
|
73
|
+
if (end === -1) return {};
|
|
74
|
+
const block = md.slice(4, end + 1);
|
|
75
|
+
const out = {};
|
|
76
|
+
for (const line of block.split(/\r?\n/)) {
|
|
77
|
+
const match = line.match(/^([A-Za-z0-9_-]+):\s*(.*)\s*$/);
|
|
78
|
+
if (!match) continue;
|
|
79
|
+
out[match[1]] = (match[2] ?? "").replace(/^"(.*)"$/, "$1").replace(/^'(.*)'$/, "$1");
|
|
144
80
|
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
81
|
+
return out;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function walkFiles(dirAbs, ext) {
|
|
85
|
+
const out = [];
|
|
86
|
+
const stack = [dirAbs];
|
|
87
|
+
while (stack.length) {
|
|
88
|
+
const cur = stack.pop();
|
|
89
|
+
let entries;
|
|
90
|
+
try { entries = fs.readdirSync(cur, { withFileTypes: true }); } catch { continue; }
|
|
91
|
+
for (const e of entries) {
|
|
92
|
+
const p = path.join(cur, e.name);
|
|
93
|
+
if (e.isDirectory()) stack.push(p);
|
|
94
|
+
else if (e.isFile() && (!ext || p.endsWith(ext))) out.push(p);
|
|
95
|
+
}
|
|
148
96
|
}
|
|
149
|
-
|
|
150
|
-
|
|
97
|
+
return out.sort();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function copyDirRecursive(srcDir, destDir) {
|
|
101
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
102
|
+
for (const entry of fs.readdirSync(srcDir, { withFileTypes: true })) {
|
|
103
|
+
const srcPath = path.join(srcDir, entry.name);
|
|
104
|
+
const destPath = path.join(destDir, entry.name);
|
|
105
|
+
if (entry.isDirectory()) {
|
|
106
|
+
copyDirRecursive(srcPath, destPath);
|
|
107
|
+
} else if (entry.isFile()) {
|
|
108
|
+
// Remove symlinks before copying so we write a real file, not through a broken link
|
|
109
|
+
try { if (fs.lstatSync(destPath).isSymbolicLink()) fs.rmSync(destPath, { force: true }); } catch { /* doesn't exist */ }
|
|
110
|
+
fs.copyFileSync(srcPath, destPath);
|
|
111
|
+
}
|
|
151
112
|
}
|
|
152
|
-
return manifest;
|
|
153
113
|
}
|
|
154
114
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
let PACKAGE_SKILL_ROOT;
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
// Copy operations
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
159
118
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
119
|
+
/**
|
|
120
|
+
* Copy all .md files from srcDir (recursively) into destDir (flat).
|
|
121
|
+
* Prunes .md files in destDir not in current source.
|
|
122
|
+
*/
|
|
123
|
+
function copyAgentsFlat(srcDir, destDir, dryRun, label) {
|
|
124
|
+
const files = walkFiles(srcDir, ".md");
|
|
125
|
+
const srcNames = new Set(files.map((f) => path.basename(f)));
|
|
126
|
+
if (dryRun) { console.log(`[dry-run] Would copy ${files.length} agents (flat) to ${label}`); return; }
|
|
127
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
128
|
+
try {
|
|
129
|
+
for (const e of fs.readdirSync(destDir, { withFileTypes: true })) {
|
|
130
|
+
if (e.name.endsWith(".md") && !srcNames.has(e.name)) fs.rmSync(path.join(destDir, e.name), { force: true });
|
|
131
|
+
}
|
|
132
|
+
} catch { /* ignore */ }
|
|
133
|
+
for (const f of files) {
|
|
134
|
+
const dest = path.join(destDir, path.basename(f));
|
|
135
|
+
try { if (fs.lstatSync(dest).isSymbolicLink()) fs.rmSync(dest, { force: true }); } catch { /* doesn't exist */ }
|
|
136
|
+
fs.copyFileSync(f, dest);
|
|
137
|
+
}
|
|
138
|
+
console.log(`Copied ${files.length} agents to ${label}`);
|
|
165
139
|
}
|
|
166
140
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
141
|
+
/**
|
|
142
|
+
* Copy srcDir recursively to destDir, pruning top-level subdirs no longer in source.
|
|
143
|
+
*/
|
|
144
|
+
function copyAgentsRecursive(srcDir, destDir, dryRun, label) {
|
|
145
|
+
const files = walkFiles(srcDir, ".md");
|
|
146
|
+
if (dryRun) { console.log(`[dry-run] Would copy ${files.length} agents (recursive) to ${label}`); return; }
|
|
147
|
+
const validSubdirs = new Set();
|
|
148
|
+
for (const f of files) {
|
|
149
|
+
const sub = path.relative(srcDir, path.dirname(f));
|
|
150
|
+
if (sub && sub !== ".") validSubdirs.add(sub.split(path.sep)[0]);
|
|
151
|
+
}
|
|
152
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
153
|
+
try {
|
|
154
|
+
for (const e of fs.readdirSync(destDir, { withFileTypes: true })) {
|
|
155
|
+
if (e.isDirectory() && !validSubdirs.has(e.name)) fs.rmSync(path.join(destDir, e.name), { recursive: true, force: true });
|
|
156
|
+
}
|
|
157
|
+
} catch { /* ignore */ }
|
|
158
|
+
copyDirRecursive(srcDir, destDir);
|
|
159
|
+
console.log(`Copied ${files.length} agents to ${label}`);
|
|
173
160
|
}
|
|
174
161
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
162
|
+
/**
|
|
163
|
+
* Copy skill directories (each containing SKILL.md) to destDir.
|
|
164
|
+
* Prunes skill dirs in destDir no longer in source.
|
|
165
|
+
*/
|
|
166
|
+
function copySkills(srcDir, destDir, dryRun, label) {
|
|
167
|
+
if (!fs.existsSync(srcDir)) return;
|
|
168
|
+
const skillDirs = fs.readdirSync(srcDir, { withFileTypes: true })
|
|
169
|
+
.filter((e) => e.isDirectory() && fs.existsSync(path.join(srcDir, e.name, "SKILL.md")))
|
|
170
|
+
.map((e) => e.name);
|
|
171
|
+
if (dryRun) { console.log(`[dry-run] Would copy ${skillDirs.length} skills to ${label}`); return; }
|
|
172
|
+
const skillSet = new Set(skillDirs);
|
|
173
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
174
|
+
try {
|
|
175
|
+
for (const e of fs.readdirSync(destDir, { withFileTypes: true })) {
|
|
176
|
+
if (e.isDirectory() && fs.existsSync(path.join(destDir, e.name, "SKILL.md")) && !skillSet.has(e.name))
|
|
177
|
+
fs.rmSync(path.join(destDir, e.name), { recursive: true, force: true });
|
|
178
|
+
}
|
|
179
|
+
} catch { /* ignore */ }
|
|
180
|
+
for (const name of skillDirs) {
|
|
181
|
+
const dest = path.join(destDir, name);
|
|
182
|
+
fs.rmSync(dest, { recursive: true, force: true });
|
|
183
|
+
copyDirRecursive(path.join(srcDir, name), dest);
|
|
184
|
+
}
|
|
185
|
+
console.log(`Copied ${skillDirs.length} skills to ${label}`);
|
|
180
186
|
}
|
|
181
187
|
|
|
182
|
-
|
|
183
|
-
|
|
188
|
+
/**
|
|
189
|
+
* Copy .md command files from srcDir to destDir (flat).
|
|
190
|
+
* Prunes .md files in destDir not in current source.
|
|
191
|
+
*/
|
|
192
|
+
function copyCommands(srcDir, destDir, dryRun, label) {
|
|
193
|
+
if (!fs.existsSync(srcDir)) return;
|
|
194
|
+
const files = fs.readdirSync(srcDir, { withFileTypes: true })
|
|
195
|
+
.filter((e) => e.isFile() && e.name.endsWith(".md"))
|
|
196
|
+
.map((e) => e.name);
|
|
197
|
+
if (dryRun) { console.log(`[dry-run] Would copy ${files.length} commands to ${label}`); return; }
|
|
198
|
+
const fileSet = new Set(files);
|
|
199
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
200
|
+
try {
|
|
201
|
+
for (const e of fs.readdirSync(destDir, { withFileTypes: true })) {
|
|
202
|
+
if (e.name.endsWith(".md") && !fileSet.has(e.name)) fs.rmSync(path.join(destDir, e.name), { force: true });
|
|
203
|
+
}
|
|
204
|
+
} catch { /* ignore */ }
|
|
205
|
+
for (const name of files) fs.copyFileSync(path.join(srcDir, name), path.join(destDir, name));
|
|
206
|
+
console.log(`Copied ${files.length} commands to ${label}`);
|
|
184
207
|
}
|
|
185
208
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
209
|
+
// ---------------------------------------------------------------------------
|
|
210
|
+
// opencode.json
|
|
211
|
+
// ---------------------------------------------------------------------------
|
|
212
|
+
|
|
213
|
+
function writeOpenCodeJson(targetRoot, srcRoot, dryRun) {
|
|
214
|
+
const commandsDir = path.join(srcRoot, "commands");
|
|
215
|
+
const agentsDir = path.join(srcRoot, "agents");
|
|
216
|
+
|
|
217
|
+
const commands = [];
|
|
218
|
+
if (fs.existsSync(commandsDir)) {
|
|
219
|
+
for (const f of walkFiles(commandsDir, ".md")) {
|
|
220
|
+
const rel = path.relative(commandsDir, f).replaceAll(path.sep, "/");
|
|
221
|
+
const fm = parseFrontmatter(fs.readFileSync(f, "utf8"));
|
|
222
|
+
const id = fm.invocation || fm.name || path.basename(f, ".md");
|
|
223
|
+
commands.push({ id: id.trim(), description: (fm.description || id).trim(), rel });
|
|
224
|
+
}
|
|
225
|
+
}
|
|
189
226
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
227
|
+
const agents = [];
|
|
228
|
+
if (fs.existsSync(agentsDir)) {
|
|
229
|
+
for (const f of walkFiles(agentsDir, ".md")) {
|
|
230
|
+
const rel = path.relative(agentsDir, f).replaceAll(path.sep, "/");
|
|
231
|
+
const fm = parseFrontmatter(fs.readFileSync(f, "utf8"));
|
|
232
|
+
const id = fm.name || path.basename(f, ".md");
|
|
233
|
+
agents.push({ id: id.trim(), description: (fm.description || id).trim(), rel });
|
|
234
|
+
}
|
|
235
|
+
}
|
|
194
236
|
|
|
195
237
|
const opencodeAbs = path.join(targetRoot, "opencode.json");
|
|
196
238
|
const existing = readJsonMaybe(opencodeAbs) ?? {};
|
|
@@ -199,112 +241,76 @@ function writeOpenCodeJson(targetRoot, dryRun, isSelfInstall) {
|
|
|
199
241
|
next.$schema = next.$schema || "https://opencode.ai/config.json";
|
|
200
242
|
next.skills = ensureObject(next.skills);
|
|
201
243
|
next.skills.paths = Array.isArray(next.skills.paths) ? next.skills.paths : [];
|
|
202
|
-
|
|
203
|
-
next.skills.paths = next.skills.paths.filter((p) => p
|
|
204
|
-
if (!next.skills.paths.includes(
|
|
205
|
-
next.skills.paths.unshift(skillRoot);
|
|
206
|
-
}
|
|
244
|
+
// Remove old package-relative paths, ensure .agents/skills is present
|
|
245
|
+
next.skills.paths = next.skills.paths.filter((p) => !p.includes("compound-workflow") && !p.includes("src/.agents"));
|
|
246
|
+
if (!next.skills.paths.includes(".agents/skills")) next.skills.paths.unshift(".agents/skills");
|
|
207
247
|
|
|
208
248
|
next.command = ensureObject(next.command);
|
|
209
249
|
next.agent = ensureObject(next.agent);
|
|
210
250
|
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
next.command[command.id] = {
|
|
216
|
-
...ensureObject(next.command[command.id]),
|
|
217
|
-
description: command.description,
|
|
251
|
+
for (const cmd of commands) {
|
|
252
|
+
next.command[cmd.id] = {
|
|
253
|
+
...ensureObject(next.command[cmd.id]),
|
|
254
|
+
description: cmd.description,
|
|
218
255
|
agent: "build",
|
|
219
|
-
template: `@AGENTS.md\n
|
|
256
|
+
template: `@AGENTS.md\n@.agents/commands/${cmd.rel}\nArguments: $ARGUMENTS\n`,
|
|
220
257
|
};
|
|
221
258
|
}
|
|
222
259
|
|
|
223
|
-
const
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
const commandPath = managedCommandPath(entry);
|
|
228
|
-
if (!commandPath || !isManagedCommandPath(commandPath)) continue;
|
|
229
|
-
if (!managedCommandTargets.has(commandPath)) delete next.command[id];
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
for (const agent of agents) {
|
|
233
|
-
next.agent[agent.id] = {
|
|
234
|
-
...ensureObject(next.agent[agent.id]),
|
|
235
|
-
description: agent.description,
|
|
260
|
+
for (const ag of agents) {
|
|
261
|
+
next.agent[ag.id] = {
|
|
262
|
+
...ensureObject(next.agent[ag.id]),
|
|
263
|
+
description: ag.description,
|
|
236
264
|
mode: "subagent",
|
|
237
|
-
prompt: `{file
|
|
238
|
-
permission: { ...ensureObject(next.agent[
|
|
265
|
+
prompt: `{file:.agents/agents/${ag.rel}}`,
|
|
266
|
+
permission: { ...ensureObject(next.agent[ag.id]?.permission), edit: "deny" },
|
|
239
267
|
};
|
|
240
268
|
}
|
|
241
269
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
);
|
|
245
|
-
for (const [id, entry] of Object.entries(next.agent)) {
|
|
246
|
-
const agentPath = managedAgentPath(entry);
|
|
247
|
-
if (!agentPath || !isManagedAgentPath(agentPath)) continue;
|
|
248
|
-
if (!managedAgentTargets.has(agentPath)) delete next.agent[id];
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
const out = JSON.stringify(next, null, 2) + "\n";
|
|
252
|
-
if (dryRun) {
|
|
253
|
-
console.log("[dry-run] Would write opencode.json:", opencodeAbs);
|
|
254
|
-
return;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
fs.writeFileSync(opencodeAbs, out, "utf8");
|
|
258
|
-
console.log("Wrote:", opencodeAbs);
|
|
270
|
+
if (dryRun) { console.log("[dry-run] Would write opencode.json"); return; }
|
|
271
|
+
fs.writeFileSync(opencodeAbs, JSON.stringify(next, null, 2) + "\n", "utf8");
|
|
272
|
+
console.log("Wrote: opencode.json");
|
|
259
273
|
}
|
|
260
274
|
|
|
275
|
+
// ---------------------------------------------------------------------------
|
|
276
|
+
// AGENTS.md merge
|
|
277
|
+
// ---------------------------------------------------------------------------
|
|
278
|
+
|
|
261
279
|
function extractRepoConfigBlock(md) {
|
|
262
280
|
const match = md.match(/(### Repo Config Block[^\n]*\n)?\s*```yaml\n([\s\S]*?)```/);
|
|
263
281
|
if (!match) return { block: null, rest: md };
|
|
264
|
-
const full = match[0];
|
|
265
282
|
const block = match[2].trim();
|
|
266
|
-
const rest = md.replace(
|
|
283
|
+
const rest = md.replace(match[0], "").replace(/\n{3,}/g, "\n\n").trim();
|
|
267
284
|
return { block, rest };
|
|
268
285
|
}
|
|
269
286
|
|
|
270
|
-
function
|
|
271
|
-
const
|
|
272
|
-
|
|
273
|
-
|
|
287
|
+
function writeAgentsMd(targetRoot, packageRoot, dryRun) {
|
|
288
|
+
const templatePath = path.join(packageRoot, "src", "AGENTS.md");
|
|
289
|
+
const targetPath = path.join(targetRoot, "AGENTS.md");
|
|
290
|
+
const templateMd = fs.readFileSync(templatePath, "utf8");
|
|
291
|
+
const existingMd = fs.existsSync(targetPath) ? fs.readFileSync(targetPath, "utf8") : null;
|
|
292
|
+
|
|
293
|
+
const { block: existingBlock } = existingMd ? extractRepoConfigBlock(existingMd) : { block: null };
|
|
274
294
|
const { rest: templateRest } = extractRepoConfigBlock(templateMd);
|
|
275
295
|
|
|
276
296
|
let out = templateRest;
|
|
277
297
|
if (existingBlock) {
|
|
278
298
|
const repoSection = `### Repo Config Block (Optional)\n\n\`\`\`yaml\n${existingBlock}\n\`\`\`\n`;
|
|
279
|
-
|
|
280
299
|
if (!out.includes("### Repo Config Block")) {
|
|
281
|
-
out = out.replace(
|
|
282
|
-
"## Repo Configuration (Optional)",
|
|
283
|
-
`## Repo Configuration (Optional)\n\n${repoSection}`
|
|
284
|
-
);
|
|
300
|
+
out = out.replace("## Repo Configuration (Optional)", `## Repo Configuration (Optional)\n\n${repoSection}`);
|
|
285
301
|
} else {
|
|
286
302
|
out = out.replace(/### Repo Config Block[^\n]*\n\s*```yaml\n[\s\S]*?```/, repoSection);
|
|
287
303
|
}
|
|
288
304
|
}
|
|
289
305
|
|
|
290
|
-
return
|
|
306
|
+
if (dryRun) { console.log("[dry-run] Would write AGENTS.md"); return; }
|
|
307
|
+
fs.writeFileSync(targetPath, out, "utf8");
|
|
308
|
+
console.log("Wrote: AGENTS.md");
|
|
291
309
|
}
|
|
292
310
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
const templateMd = fs.readFileSync(templatePath, "utf8");
|
|
297
|
-
const existingMd = fs.existsSync(targetPath) ? fs.readFileSync(targetPath, "utf8") : null;
|
|
298
|
-
const merged = mergeAgentsMd(templateMd, existingMd);
|
|
299
|
-
|
|
300
|
-
if (dryRun) {
|
|
301
|
-
console.log("[dry-run] Would write AGENTS.md:", targetPath);
|
|
302
|
-
return;
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
fs.writeFileSync(targetPath, merged, "utf8");
|
|
306
|
-
console.log("Wrote:", targetPath);
|
|
307
|
-
}
|
|
311
|
+
// ---------------------------------------------------------------------------
|
|
312
|
+
// Standard dirs
|
|
313
|
+
// ---------------------------------------------------------------------------
|
|
308
314
|
|
|
309
315
|
const DIRS = [
|
|
310
316
|
"docs/brainstorms",
|
|
@@ -319,378 +325,90 @@ const DIRS = [
|
|
|
319
325
|
function ensureDirs(targetRoot, dryRun) {
|
|
320
326
|
for (const d of DIRS) {
|
|
321
327
|
const abs = path.join(targetRoot, d);
|
|
322
|
-
if (
|
|
323
|
-
console.log("[dry-run] Would create:", d);
|
|
324
|
-
|
|
325
|
-
fs.mkdirSync(abs, { recursive: true });
|
|
326
|
-
console.log("Created:", d);
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
/**
|
|
332
|
-
* Writes .cursor-plugin/plugin.json and .claude-plugin/plugin.json at targetRoot.
|
|
333
|
-
* Paths (commands, agents, skills) in the written manifests are relative to project root
|
|
334
|
-
* (parent of .cursor-plugin / .claude-plugin) so Cursor/Claude resolve assets correctly.
|
|
335
|
-
*/
|
|
336
|
-
function writePluginManifests(targetRoot, dryRun, isSelfInstall) {
|
|
337
|
-
const pathsBase = isSelfInstall ? "./src/.agents" : "./node_modules/compound-workflow/src/.agents";
|
|
338
|
-
const cursorSrc = path.join(PACKAGE_ROOT, ".cursor-plugin", "plugin.json");
|
|
339
|
-
const claudeSrc = path.join(PACKAGE_ROOT, ".claude-plugin", "plugin.json");
|
|
340
|
-
const cursorManifest = readJsonMaybe(cursorSrc);
|
|
341
|
-
const claudeManifest = readJsonMaybe(claudeSrc);
|
|
342
|
-
if (!cursorManifest || !claudeManifest) return;
|
|
343
|
-
|
|
344
|
-
// All Cursor paths point directly at the package source — no symlink indirection.
|
|
345
|
-
// This ensures frontmatter (descriptions) is parsed correctly by Cursor for all components.
|
|
346
|
-
const cursorOut = {
|
|
347
|
-
...cursorManifest,
|
|
348
|
-
commands: `${pathsBase}/commands`,
|
|
349
|
-
agents: `${pathsBase}/agents`,
|
|
350
|
-
skills: `${pathsBase}/skills`,
|
|
351
|
-
};
|
|
352
|
-
// Claude Code only accepts name, description, author in plugin.json.
|
|
353
|
-
// Agents are discovered from the adjacent agents/ directory (flat .md files).
|
|
354
|
-
const claudeOut = {
|
|
355
|
-
name: claudeManifest.name,
|
|
356
|
-
description: claudeManifest.description,
|
|
357
|
-
author: claudeManifest.author,
|
|
358
|
-
};
|
|
359
|
-
const cursorDir = path.join(targetRoot, ".cursor-plugin");
|
|
360
|
-
const claudeDir = path.join(targetRoot, ".claude-plugin");
|
|
361
|
-
|
|
362
|
-
const installPathAbs = realpathSafe(targetRoot);
|
|
363
|
-
const registrationDescriptor = {
|
|
364
|
-
pluginId: "compound-workflow@local",
|
|
365
|
-
scope: "user",
|
|
366
|
-
installPath: installPathAbs,
|
|
367
|
-
};
|
|
368
|
-
|
|
369
|
-
if (dryRun) {
|
|
370
|
-
console.log("[dry-run] Would write .cursor-plugin/plugin.json, .claude-plugin/plugin.json, .cursor-plugin/registration.json" + (isSelfInstall ? "" : ", .claude-plugin/marketplace.json"));
|
|
371
|
-
return;
|
|
372
|
-
}
|
|
373
|
-
fs.mkdirSync(cursorDir, { recursive: true });
|
|
374
|
-
fs.mkdirSync(claudeDir, { recursive: true });
|
|
375
|
-
fs.writeFileSync(path.join(cursorDir, "plugin.json"), JSON.stringify(cursorOut, null, 2) + "\n", "utf8");
|
|
376
|
-
fs.writeFileSync(path.join(claudeDir, "plugin.json"), JSON.stringify(claudeOut, null, 2) + "\n", "utf8");
|
|
377
|
-
fs.writeFileSync(path.join(cursorDir, "registration.json"), JSON.stringify(registrationDescriptor, null, 2) + "\n", "utf8");
|
|
378
|
-
|
|
379
|
-
// Sync flat agent symlinks into .claude-plugin/agents/ so Claude Code discovers them.
|
|
380
|
-
// Claude Code only scans the root of the agents/ directory (not subdirectories).
|
|
381
|
-
const claudeAgentsDir = path.join(claudeDir, "agents");
|
|
382
|
-
const packageAgentsDirAbs = isSelfInstall
|
|
383
|
-
? path.join(PACKAGE_ROOT, "src", ".agents", "agents")
|
|
384
|
-
: path.join(targetRoot, "node_modules", "compound-workflow", "src", ".agents", "agents");
|
|
385
|
-
if (fs.existsSync(packageAgentsDirAbs)) {
|
|
386
|
-
fs.mkdirSync(claudeAgentsDir, { recursive: true });
|
|
387
|
-
const agentBasenames = new Set(GENERATED_MANIFEST.agents.map((a) => path.basename(a.rel)));
|
|
388
|
-
// Prune stale symlinks
|
|
389
|
-
try {
|
|
390
|
-
for (const entry of fs.readdirSync(claudeAgentsDir, { withFileTypes: true })) {
|
|
391
|
-
if (!agentBasenames.has(entry.name)) {
|
|
392
|
-
fs.rmSync(path.join(claudeAgentsDir, entry.name), { force: true });
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
} catch { /* ignore */ }
|
|
396
|
-
for (const agent of GENERATED_MANIFEST.agents) {
|
|
397
|
-
const linkPath = path.join(claudeAgentsDir, path.basename(agent.rel));
|
|
398
|
-
const targetPath = path.join(packageAgentsDirAbs, agent.rel);
|
|
399
|
-
try {
|
|
400
|
-
if (fs.lstatSync(linkPath)) fs.rmSync(linkPath, { force: true });
|
|
401
|
-
} catch { /* doesn't exist */ }
|
|
402
|
-
try {
|
|
403
|
-
fs.symlinkSync(targetPath, linkPath);
|
|
404
|
-
} catch (err) {
|
|
405
|
-
console.warn("[claude] Could not symlink agent", agent.id, err.message);
|
|
406
|
-
}
|
|
328
|
+
if (!fs.existsSync(abs)) {
|
|
329
|
+
if (dryRun) console.log("[dry-run] Would create:", d);
|
|
330
|
+
else { fs.mkdirSync(abs, { recursive: true }); console.log("Created:", d); }
|
|
407
331
|
}
|
|
408
332
|
}
|
|
409
|
-
|
|
410
|
-
// Claude Code 2.1.x+ no longer loads from installed_plugins.json; it requires marketplace flow.
|
|
411
|
-
// Write a project-level marketplace so user can: /plugin marketplace add . then /plugin install compound-workflow@compound-workflow-local
|
|
412
|
-
if (!isSelfInstall) {
|
|
413
|
-
const marketplaceManifest = {
|
|
414
|
-
name: "compound-workflow-local",
|
|
415
|
-
owner: { name: "Compound Workflow" },
|
|
416
|
-
plugins: [
|
|
417
|
-
{
|
|
418
|
-
name: "compound-workflow",
|
|
419
|
-
source: "./node_modules/compound-workflow",
|
|
420
|
-
description: claudeOut.description || "Clarify → plan → execute → verify → capture workflow.",
|
|
421
|
-
},
|
|
422
|
-
],
|
|
423
|
-
};
|
|
424
|
-
fs.writeFileSync(path.join(claudeDir, "marketplace.json"), JSON.stringify(marketplaceManifest, null, 2) + "\n", "utf8");
|
|
425
|
-
}
|
|
426
|
-
console.log("Wrote: .cursor-plugin/plugin.json, .claude-plugin/plugin.json, .cursor-plugin/registration.json" + (isSelfInstall ? "" : ", .claude-plugin/marketplace.json"));
|
|
427
333
|
}
|
|
428
334
|
|
|
335
|
+
// ---------------------------------------------------------------------------
|
|
336
|
+
// CLI args
|
|
337
|
+
// ---------------------------------------------------------------------------
|
|
429
338
|
|
|
339
|
+
function usage(exitCode = 0) {
|
|
340
|
+
const msg = `
|
|
341
|
+
Usage:
|
|
342
|
+
(automatic) npm install compound-workflow # runs install via postinstall
|
|
343
|
+
(manual) npx compound-workflow install [--root <projectDir>] [--dry-run]
|
|
430
344
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
for (const entry of fs.readdirSync(srcDir, { withFileTypes: true })) {
|
|
434
|
-
const srcPath = path.join(srcDir, entry.name);
|
|
435
|
-
const destPath = path.join(destDir, entry.name);
|
|
436
|
-
if (entry.isDirectory()) {
|
|
437
|
-
copyDirRecursive(srcPath, destPath);
|
|
438
|
-
} else if (entry.isFile()) {
|
|
439
|
-
fs.copyFileSync(srcPath, destPath);
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
/**
|
|
445
|
-
* Cursor discovers skills from .cursor/skills/ (each subdir with SKILL.md).
|
|
446
|
-
* Copy package skills so Cursor finds them with full frontmatter metadata.
|
|
447
|
-
*/
|
|
448
|
-
function copyCursorSkills(targetRoot, dryRun, isSelfInstall) {
|
|
449
|
-
const packageSkillsAbs = path.join(
|
|
450
|
-
isSelfInstall ? PACKAGE_ROOT : path.join(targetRoot, "node_modules", "compound-workflow"),
|
|
451
|
-
"src", ".agents", "skills"
|
|
452
|
-
);
|
|
453
|
-
if (!fs.existsSync(packageSkillsAbs)) return;
|
|
454
|
-
|
|
455
|
-
const cursorSkillsDir = path.join(targetRoot, ".cursor", "skills");
|
|
456
|
-
const skillDirs = fs.readdirSync(packageSkillsAbs, { withFileTypes: true })
|
|
457
|
-
.filter((e) => e.isDirectory() && fs.existsSync(path.join(packageSkillsAbs, e.name, "SKILL.md")))
|
|
458
|
-
.map((e) => e.name);
|
|
459
|
-
if (skillDirs.length === 0) return;
|
|
460
|
-
|
|
461
|
-
if (dryRun) {
|
|
462
|
-
console.log("[dry-run] Would copy", skillDirs.length, "skills into .cursor/skills/");
|
|
463
|
-
return;
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
fs.mkdirSync(cursorSkillsDir, { recursive: true });
|
|
467
|
-
const skillSet = new Set(skillDirs);
|
|
468
|
-
|
|
469
|
-
// Prune stale entries that we manage (contain SKILL.md) but are no longer in the package
|
|
470
|
-
try {
|
|
471
|
-
for (const entry of fs.readdirSync(cursorSkillsDir, { withFileTypes: true })) {
|
|
472
|
-
if (!entry.isDirectory()) continue;
|
|
473
|
-
const skillMd = path.join(cursorSkillsDir, entry.name, "SKILL.md");
|
|
474
|
-
if (!fs.existsSync(skillMd)) continue;
|
|
475
|
-
if (!skillSet.has(entry.name)) {
|
|
476
|
-
fs.rmSync(path.join(cursorSkillsDir, entry.name), { recursive: true, force: true });
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
} catch { /* ignore */ }
|
|
480
|
-
|
|
481
|
-
for (const name of skillDirs) {
|
|
482
|
-
const dest = path.join(cursorSkillsDir, name);
|
|
483
|
-
fs.rmSync(dest, { recursive: true, force: true });
|
|
484
|
-
copyDirRecursive(path.join(packageSkillsAbs, name), dest);
|
|
485
|
-
}
|
|
486
|
-
console.log("Copied", skillDirs.length, "skills to .cursor/skills/");
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
/**
|
|
490
|
-
* Cursor discovers commands from .cursor/commands/ (.md files).
|
|
491
|
-
* Copy package commands so Cursor finds them with full frontmatter metadata.
|
|
492
|
-
*/
|
|
493
|
-
function copyCursorCommands(targetRoot, dryRun, isSelfInstall) {
|
|
494
|
-
const packageCommandsAbs = path.join(
|
|
495
|
-
isSelfInstall ? PACKAGE_ROOT : path.join(targetRoot, "node_modules", "compound-workflow"),
|
|
496
|
-
"src", ".agents", "commands"
|
|
497
|
-
);
|
|
498
|
-
if (!fs.existsSync(packageCommandsAbs)) return;
|
|
499
|
-
|
|
500
|
-
const cursorCommandsDir = path.join(targetRoot, ".cursor", "commands");
|
|
501
|
-
const commandFiles = fs.readdirSync(packageCommandsAbs, { withFileTypes: true })
|
|
502
|
-
.filter((e) => e.isFile() && e.name.endsWith(".md"))
|
|
503
|
-
.map((e) => e.name);
|
|
504
|
-
if (commandFiles.length === 0) return;
|
|
505
|
-
|
|
506
|
-
if (dryRun) {
|
|
507
|
-
console.log("[dry-run] Would copy", commandFiles.length, "commands into .cursor/commands/");
|
|
508
|
-
return;
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
fs.mkdirSync(cursorCommandsDir, { recursive: true });
|
|
512
|
-
const commandSet = new Set(commandFiles);
|
|
513
|
-
|
|
514
|
-
// Prune stale .md files and old symlinks that we previously managed
|
|
515
|
-
try {
|
|
516
|
-
for (const entry of fs.readdirSync(cursorCommandsDir, { withFileTypes: true })) {
|
|
517
|
-
if (!entry.name.endsWith(".md")) continue;
|
|
518
|
-
if (entry.isSymbolicLink() || !commandSet.has(entry.name)) {
|
|
519
|
-
fs.rmSync(path.join(cursorCommandsDir, entry.name), { force: true });
|
|
520
|
-
}
|
|
521
|
-
}
|
|
522
|
-
} catch { /* ignore */ }
|
|
523
|
-
|
|
524
|
-
for (const name of commandFiles) {
|
|
525
|
-
fs.copyFileSync(path.join(packageCommandsAbs, name), path.join(cursorCommandsDir, name));
|
|
526
|
-
}
|
|
527
|
-
console.log("Copied", commandFiles.length, "commands to .cursor/commands/");
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
/**
|
|
531
|
-
* Cursor discovers agents from .cursor/agents/ (.md files, supports subdirs).
|
|
532
|
-
* Copy package agents preserving subdirectory structure (research/, review/, workflow/).
|
|
533
|
-
*/
|
|
534
|
-
function copyCursorAgents(targetRoot, dryRun, isSelfInstall) {
|
|
535
|
-
const packageAgentsAbs = path.join(
|
|
536
|
-
isSelfInstall ? PACKAGE_ROOT : path.join(targetRoot, "node_modules", "compound-workflow"),
|
|
537
|
-
"src", ".agents", "agents"
|
|
538
|
-
);
|
|
539
|
-
if (!fs.existsSync(packageAgentsAbs)) return;
|
|
540
|
-
|
|
541
|
-
const cursorAgentsDir = path.join(targetRoot, ".cursor", "agents");
|
|
542
|
-
const agentRels = GENERATED_MANIFEST.agents.map((a) => a.rel);
|
|
543
|
-
if (agentRels.length === 0) return;
|
|
544
|
-
|
|
545
|
-
if (dryRun) {
|
|
546
|
-
console.log("[dry-run] Would copy", agentRels.length, "agents into .cursor/agents/");
|
|
547
|
-
return;
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
fs.mkdirSync(cursorAgentsDir, { recursive: true });
|
|
551
|
-
|
|
552
|
-
// Collect valid subdirectories from manifest
|
|
553
|
-
const validSubdirs = new Set();
|
|
554
|
-
for (const rel of agentRels) {
|
|
555
|
-
const subdir = path.dirname(rel);
|
|
556
|
-
if (subdir !== ".") validSubdirs.add(subdir);
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
// Prune stale subdirs and files
|
|
560
|
-
try {
|
|
561
|
-
for (const entry of fs.readdirSync(cursorAgentsDir, { withFileTypes: true })) {
|
|
562
|
-
if (entry.isDirectory() && !validSubdirs.has(entry.name)) {
|
|
563
|
-
fs.rmSync(path.join(cursorAgentsDir, entry.name), { recursive: true, force: true });
|
|
564
|
-
}
|
|
565
|
-
}
|
|
566
|
-
} catch { /* ignore */ }
|
|
567
|
-
|
|
568
|
-
for (const rel of agentRels) {
|
|
569
|
-
const destPath = path.join(cursorAgentsDir, rel);
|
|
570
|
-
fs.mkdirSync(path.dirname(destPath), { recursive: true });
|
|
571
|
-
fs.copyFileSync(path.join(packageAgentsAbs, rel), destPath);
|
|
572
|
-
}
|
|
573
|
-
console.log("Copied", agentRels.length, "agents to .cursor/agents/");
|
|
574
|
-
}
|
|
345
|
+
Copies agents, skills, and commands into .claude/, .cursor/, and .agents/.
|
|
346
|
+
Also writes opencode.json, AGENTS.md, and standard docs directories.
|
|
575
347
|
|
|
576
|
-
|
|
577
|
-
|
|
348
|
+
--root <dir> Project directory (default: cwd)
|
|
349
|
+
--dry-run Print planned changes only
|
|
350
|
+
`;
|
|
351
|
+
(exitCode === 0 ? console.log : console.error)(msg.trimStart());
|
|
352
|
+
process.exit(exitCode);
|
|
578
353
|
}
|
|
579
354
|
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
console.log("[dry-run] Would register Claude plugin (project-scoped) at:", projectRoot);
|
|
590
|
-
return;
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
const projectSettingsPath = path.join(projectRoot, ".claude", "settings.json");
|
|
594
|
-
let projectSettings = {};
|
|
595
|
-
if (fs.existsSync(projectSettingsPath)) {
|
|
596
|
-
try { projectSettings = readJsonMaybe(projectSettingsPath) ?? {}; } catch { projectSettings = {}; }
|
|
597
|
-
}
|
|
598
|
-
projectSettings.enabledPlugins = ensureObject(projectSettings.enabledPlugins);
|
|
599
|
-
projectSettings.enabledPlugins[pluginId] = true;
|
|
600
|
-
if (projectSettings.extraKnownMarketplaces?.["compound-workflow"]) {
|
|
601
|
-
delete projectSettings.extraKnownMarketplaces["compound-workflow"];
|
|
602
|
-
}
|
|
603
|
-
projectSettings.extraKnownMarketplaces = ensureObject(projectSettings.extraKnownMarketplaces);
|
|
604
|
-
projectSettings.extraKnownMarketplaces["compound-workflow-local"] = {
|
|
605
|
-
source: { source: "file", path: "." },
|
|
606
|
-
};
|
|
607
|
-
fs.mkdirSync(path.join(projectRoot, ".claude"), { recursive: true });
|
|
608
|
-
fs.writeFileSync(projectSettingsPath, JSON.stringify(projectSettings, null, 2) + "\n", "utf8");
|
|
609
|
-
|
|
610
|
-
console.log("Registered compound-workflow with Claude Code (project-scoped).");
|
|
611
|
-
if (!isSelfInstall) {
|
|
612
|
-
console.log(" Claude Code 2.1+: open /plugin, go to Discover; install 'compound-workflow' from marketplace 'compound-workflow-local', or run: claude --plugin-dir ./node_modules/compound-workflow");
|
|
355
|
+
function parseArgs(argv) {
|
|
356
|
+
const out = { root: process.cwd(), dryRun: false };
|
|
357
|
+
for (let i = 2; i < argv.length; i++) {
|
|
358
|
+
const arg = argv[i];
|
|
359
|
+
if (arg === "--dry-run") out.dryRun = true;
|
|
360
|
+
else if (arg === "--root") { const v = argv[++i]; if (!v) usage(1); out.root = v; }
|
|
361
|
+
else if (arg === "install") { /* subcommand, ignore */ }
|
|
362
|
+
else if (arg === "-h" || arg === "--help") usage(0);
|
|
363
|
+
else usage(1);
|
|
613
364
|
}
|
|
614
|
-
|
|
365
|
+
return out;
|
|
615
366
|
}
|
|
616
367
|
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
return;
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
const opencodeAbs = path.join(targetRoot, "opencode.json");
|
|
624
|
-
const opencode = readJsonMaybe(opencodeAbs) ?? {};
|
|
625
|
-
const skillPaths = Array.isArray(opencode?.skills?.paths) ? opencode.skills.paths : [];
|
|
626
|
-
const hasSkillPath = skillPaths.includes(PACKAGE_SKILL_ROOT) || skillPaths.includes("src/.agents/skills");
|
|
627
|
-
|
|
628
|
-
console.log(
|
|
629
|
-
"OpenCode integration:",
|
|
630
|
-
hasSkillPath ? "ok" : "incomplete",
|
|
631
|
-
`(skills.path=${hasSkillPath ? "yes" : "no"})`
|
|
632
|
-
);
|
|
633
|
-
}
|
|
368
|
+
// ---------------------------------------------------------------------------
|
|
369
|
+
// Main
|
|
370
|
+
// ---------------------------------------------------------------------------
|
|
634
371
|
|
|
635
372
|
function main() {
|
|
636
373
|
const args = parseArgs(process.argv);
|
|
637
374
|
const targetRoot = realpathSafe(args.root);
|
|
375
|
+
const isSelfInstall = targetRoot === PACKAGE_ROOT;
|
|
638
376
|
|
|
639
|
-
const
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
const result = spawnSync(process.execPath, [genScript], {
|
|
643
|
-
cwd: PACKAGE_ROOT,
|
|
644
|
-
stdio: "pipe",
|
|
645
|
-
encoding: "utf8",
|
|
646
|
-
});
|
|
647
|
-
if (result.status !== 0) {
|
|
648
|
-
console.error("Failed to regenerate manifest:", result.stderr || result.error || "unknown");
|
|
649
|
-
process.exit(1);
|
|
650
|
-
}
|
|
651
|
-
}
|
|
377
|
+
const packageSrc = isSelfInstall
|
|
378
|
+
? path.join(PACKAGE_ROOT, "src")
|
|
379
|
+
: path.join(targetRoot, "node_modules", "compound-workflow", "src");
|
|
652
380
|
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
PACKAGE_COMMAND_ROOT = GENERATED_MANIFEST.commandRoot;
|
|
656
|
-
PACKAGE_AGENT_ROOT = GENERATED_MANIFEST.agentRoot;
|
|
657
|
-
PACKAGE_SKILL_ROOT = GENERATED_MANIFEST.skillsPath;
|
|
658
|
-
} catch (err) {
|
|
659
|
-
console.error("Error: OpenCode manifest not found. Run 'npm run generate:artifacts' in the package or reinstall compound-workflow.");
|
|
381
|
+
if (!isSelfInstall && !fs.existsSync(packageSrc) && !args.dryRun) {
|
|
382
|
+
console.error("Error: compound-workflow not found in project. Run: npm install compound-workflow");
|
|
660
383
|
process.exit(2);
|
|
661
384
|
}
|
|
662
385
|
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
}
|
|
386
|
+
const srcAgents = path.join(packageSrc, "agents");
|
|
387
|
+
const srcSkills = path.join(packageSrc, "skills");
|
|
388
|
+
const srcCommands = path.join(packageSrc, "commands");
|
|
667
389
|
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
console.error("Error: compound-workflow not found in project. Run: npm install compound-workflow");
|
|
672
|
-
process.exit(2);
|
|
673
|
-
}
|
|
390
|
+
console.log("Target:", targetRoot);
|
|
391
|
+
console.log("Package:", PACKAGE_ROOT);
|
|
392
|
+
console.log("OpenCode CLI:", hasCommand("opencode") ? "yes" : "no");
|
|
674
393
|
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
console.log("OpenCode CLI detected:", hasCommand("opencode") ? "yes" : "no");
|
|
678
|
-
|
|
679
|
-
writeOpenCodeJson(targetRoot, args.dryRun, isSelfInstall);
|
|
680
|
-
writePluginManifests(targetRoot, args.dryRun, isSelfInstall);
|
|
681
|
-
applyClaudeRegistration(targetRoot, args.dryRun, isSelfInstall);
|
|
682
|
-
copyCursorSkills(targetRoot, args.dryRun, isSelfInstall);
|
|
683
|
-
copyCursorCommands(targetRoot, args.dryRun, isSelfInstall);
|
|
684
|
-
copyCursorAgents(targetRoot, args.dryRun, isSelfInstall);
|
|
685
|
-
reportOpenCodeIntegration(targetRoot, args.dryRun);
|
|
686
|
-
writeAgentsMd(targetRoot, args.dryRun);
|
|
687
|
-
ensureDirs(targetRoot, args.dryRun);
|
|
394
|
+
// .claude/agents/ — flat (Claude Code requires flat .md files)
|
|
395
|
+
copyAgentsFlat(srcAgents, path.join(targetRoot, ".claude", "agents"), args.dryRun, ".claude/agents/");
|
|
688
396
|
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
397
|
+
// .cursor/agents/, .cursor/skills/, .cursor/commands/
|
|
398
|
+
copyAgentsRecursive(srcAgents, path.join(targetRoot, ".cursor", "agents"), args.dryRun, ".cursor/agents/");
|
|
399
|
+
copySkills(srcSkills, path.join(targetRoot, ".cursor", "skills"), args.dryRun, ".cursor/skills/");
|
|
400
|
+
copyCommands(srcCommands, path.join(targetRoot, ".cursor", "commands"), args.dryRun, ".cursor/commands/");
|
|
401
|
+
|
|
402
|
+
// .agents/agents/, .agents/skills/, .agents/commands/ (OpenCode / general)
|
|
403
|
+
copyAgentsRecursive(srcAgents, path.join(targetRoot, ".agents", "agents"), args.dryRun, ".agents/agents/");
|
|
404
|
+
copySkills(srcSkills, path.join(targetRoot, ".agents", "skills"), args.dryRun, ".agents/skills/");
|
|
405
|
+
copyCommands(srcCommands, path.join(targetRoot, ".agents", "commands"), args.dryRun, ".agents/commands/");
|
|
406
|
+
|
|
407
|
+
writeOpenCodeJson(targetRoot, packageSrc, args.dryRun);
|
|
408
|
+
writeAgentsMd(targetRoot, PACKAGE_ROOT, args.dryRun);
|
|
409
|
+
ensureDirs(targetRoot, args.dryRun);
|
|
692
410
|
|
|
693
|
-
console.log("\nDone.
|
|
411
|
+
console.log("\nDone.");
|
|
694
412
|
}
|
|
695
413
|
|
|
696
414
|
main();
|