opencastle 0.1.0

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 (224) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +215 -0
  3. package/bin/cli.mjs +69 -0
  4. package/dist/cli/adapters/claude-code.d.ts +22 -0
  5. package/dist/cli/adapters/claude-code.d.ts.map +1 -0
  6. package/dist/cli/adapters/claude-code.js +237 -0
  7. package/dist/cli/adapters/claude-code.js.map +1 -0
  8. package/dist/cli/adapters/cursor.d.ts +20 -0
  9. package/dist/cli/adapters/cursor.d.ts.map +1 -0
  10. package/dist/cli/adapters/cursor.js +231 -0
  11. package/dist/cli/adapters/cursor.js.map +1 -0
  12. package/dist/cli/adapters/vscode.d.ts +20 -0
  13. package/dist/cli/adapters/vscode.d.ts.map +1 -0
  14. package/dist/cli/adapters/vscode.js +132 -0
  15. package/dist/cli/adapters/vscode.js.map +1 -0
  16. package/dist/cli/copy.d.ts +14 -0
  17. package/dist/cli/copy.d.ts.map +1 -0
  18. package/dist/cli/copy.js +62 -0
  19. package/dist/cli/copy.js.map +1 -0
  20. package/dist/cli/dashboard.d.ts +3 -0
  21. package/dist/cli/dashboard.d.ts.map +1 -0
  22. package/dist/cli/dashboard.js +183 -0
  23. package/dist/cli/dashboard.js.map +1 -0
  24. package/dist/cli/diff.d.ts +3 -0
  25. package/dist/cli/diff.d.ts.map +1 -0
  26. package/dist/cli/diff.js +27 -0
  27. package/dist/cli/diff.js.map +1 -0
  28. package/dist/cli/eject.d.ts +3 -0
  29. package/dist/cli/eject.d.ts.map +1 -0
  30. package/dist/cli/eject.js +27 -0
  31. package/dist/cli/eject.js.map +1 -0
  32. package/dist/cli/init.d.ts +3 -0
  33. package/dist/cli/init.d.ts.map +1 -0
  34. package/dist/cli/init.js +92 -0
  35. package/dist/cli/init.js.map +1 -0
  36. package/dist/cli/manifest.d.ts +14 -0
  37. package/dist/cli/manifest.d.ts.map +1 -0
  38. package/dist/cli/manifest.js +34 -0
  39. package/dist/cli/manifest.js.map +1 -0
  40. package/dist/cli/mcp.d.ts +14 -0
  41. package/dist/cli/mcp.d.ts.map +1 -0
  42. package/dist/cli/mcp.js +35 -0
  43. package/dist/cli/mcp.js.map +1 -0
  44. package/dist/cli/prompt.d.ts +12 -0
  45. package/dist/cli/prompt.d.ts.map +1 -0
  46. package/dist/cli/prompt.js +104 -0
  47. package/dist/cli/prompt.js.map +1 -0
  48. package/dist/cli/run/adapters/claude-code.d.ts +16 -0
  49. package/dist/cli/run/adapters/claude-code.d.ts.map +1 -0
  50. package/dist/cli/run/adapters/claude-code.js +82 -0
  51. package/dist/cli/run/adapters/claude-code.js.map +1 -0
  52. package/dist/cli/run/adapters/copilot.d.ts +16 -0
  53. package/dist/cli/run/adapters/copilot.d.ts.map +1 -0
  54. package/dist/cli/run/adapters/copilot.js +84 -0
  55. package/dist/cli/run/adapters/copilot.js.map +1 -0
  56. package/dist/cli/run/adapters/cursor.d.ts +16 -0
  57. package/dist/cli/run/adapters/cursor.d.ts.map +1 -0
  58. package/dist/cli/run/adapters/cursor.js +81 -0
  59. package/dist/cli/run/adapters/cursor.js.map +1 -0
  60. package/dist/cli/run/adapters/index.d.ts +14 -0
  61. package/dist/cli/run/adapters/index.d.ts.map +1 -0
  62. package/dist/cli/run/adapters/index.js +35 -0
  63. package/dist/cli/run/adapters/index.js.map +1 -0
  64. package/dist/cli/run/executor.d.ts +15 -0
  65. package/dist/cli/run/executor.d.ts.map +1 -0
  66. package/dist/cli/run/executor.js +249 -0
  67. package/dist/cli/run/executor.js.map +1 -0
  68. package/dist/cli/run/reporter.d.ts +10 -0
  69. package/dist/cli/run/reporter.d.ts.map +1 -0
  70. package/dist/cli/run/reporter.js +112 -0
  71. package/dist/cli/run/reporter.js.map +1 -0
  72. package/dist/cli/run/schema.d.ts +28 -0
  73. package/dist/cli/run/schema.d.ts.map +1 -0
  74. package/dist/cli/run/schema.js +511 -0
  75. package/dist/cli/run/schema.js.map +1 -0
  76. package/dist/cli/run.d.ts +6 -0
  77. package/dist/cli/run.d.ts.map +1 -0
  78. package/dist/cli/run.js +123 -0
  79. package/dist/cli/run.js.map +1 -0
  80. package/dist/cli/stack-config.d.ts +12 -0
  81. package/dist/cli/stack-config.d.ts.map +1 -0
  82. package/dist/cli/stack-config.js +146 -0
  83. package/dist/cli/stack-config.js.map +1 -0
  84. package/dist/cli/types.d.ts +169 -0
  85. package/dist/cli/types.d.ts.map +1 -0
  86. package/dist/cli/types.js +2 -0
  87. package/dist/cli/types.js.map +1 -0
  88. package/dist/cli/update.d.ts +3 -0
  89. package/dist/cli/update.d.ts.map +1 -0
  90. package/dist/cli/update.js +50 -0
  91. package/dist/cli/update.js.map +1 -0
  92. package/package.json +48 -0
  93. package/src/cli/adapters/claude-code.ts +287 -0
  94. package/src/cli/adapters/cursor.ts +377 -0
  95. package/src/cli/adapters/vscode.ts +168 -0
  96. package/src/cli/copy.ts +79 -0
  97. package/src/cli/dashboard.ts +225 -0
  98. package/src/cli/diff.ts +44 -0
  99. package/src/cli/eject.ts +39 -0
  100. package/src/cli/init.ts +120 -0
  101. package/src/cli/manifest.ts +45 -0
  102. package/src/cli/mcp.ts +49 -0
  103. package/src/cli/prompt.ts +115 -0
  104. package/src/cli/run/adapters/claude-code.ts +95 -0
  105. package/src/cli/run/adapters/copilot.ts +97 -0
  106. package/src/cli/run/adapters/cursor.ts +94 -0
  107. package/src/cli/run/adapters/index.ts +40 -0
  108. package/src/cli/run/executor.ts +292 -0
  109. package/src/cli/run/reporter.ts +129 -0
  110. package/src/cli/run/schema.ts +595 -0
  111. package/src/cli/run.ts +137 -0
  112. package/src/cli/stack-config.ts +180 -0
  113. package/src/cli/types.ts +207 -0
  114. package/src/cli/update.ts +75 -0
  115. package/src/dashboard/astro.config.mjs +6 -0
  116. package/src/dashboard/package-lock.json +5455 -0
  117. package/src/dashboard/package.json +14 -0
  118. package/src/dashboard/public/data/delegations.ndjson +35 -0
  119. package/src/dashboard/public/data/panels.ndjson +13 -0
  120. package/src/dashboard/public/data/sessions.ndjson +50 -0
  121. package/src/dashboard/public/icon-192.png +0 -0
  122. package/src/dashboard/scripts/generate-seed-data.ts +355 -0
  123. package/src/dashboard/src/layouts/Layout.astro +25 -0
  124. package/src/dashboard/src/pages/index.astro +1070 -0
  125. package/src/dashboard/src/styles/dashboard.css +1078 -0
  126. package/src/dashboard/tsconfig.json +6 -0
  127. package/src/orchestrator/agent-workflows/README.md +22 -0
  128. package/src/orchestrator/agent-workflows/bug-fix.md +128 -0
  129. package/src/orchestrator/agent-workflows/data-pipeline.md +145 -0
  130. package/src/orchestrator/agent-workflows/database-migration.md +159 -0
  131. package/src/orchestrator/agent-workflows/feature-implementation.md +223 -0
  132. package/src/orchestrator/agent-workflows/performance-optimization.md +125 -0
  133. package/src/orchestrator/agent-workflows/refactoring.md +142 -0
  134. package/src/orchestrator/agent-workflows/schema-changes.md +164 -0
  135. package/src/orchestrator/agent-workflows/security-audit.md +148 -0
  136. package/src/orchestrator/agent-workflows/shared-delivery-phase.md +33 -0
  137. package/src/orchestrator/agents/api-designer.agent.md +68 -0
  138. package/src/orchestrator/agents/architect.agent.md +129 -0
  139. package/src/orchestrator/agents/content-engineer.agent.md +57 -0
  140. package/src/orchestrator/agents/copywriter.agent.md +95 -0
  141. package/src/orchestrator/agents/data-expert.agent.md +63 -0
  142. package/src/orchestrator/agents/database-engineer.agent.md +62 -0
  143. package/src/orchestrator/agents/developer.agent.md +66 -0
  144. package/src/orchestrator/agents/devops-expert.agent.md +57 -0
  145. package/src/orchestrator/agents/documentation-writer.agent.md +60 -0
  146. package/src/orchestrator/agents/performance-expert.agent.md +58 -0
  147. package/src/orchestrator/agents/release-manager.agent.md +72 -0
  148. package/src/orchestrator/agents/researcher.agent.md +145 -0
  149. package/src/orchestrator/agents/reviewer.agent.md +62 -0
  150. package/src/orchestrator/agents/security-expert.agent.md +64 -0
  151. package/src/orchestrator/agents/seo-specialist.agent.md +67 -0
  152. package/src/orchestrator/agents/team-lead.agent.md +644 -0
  153. package/src/orchestrator/agents/testing-expert.agent.md +85 -0
  154. package/src/orchestrator/agents/ui-ux-expert.agent.md +63 -0
  155. package/src/orchestrator/copilot-instructions.md +3 -0
  156. package/src/orchestrator/customizations/AGENT-EXPERTISE.md +325 -0
  157. package/src/orchestrator/customizations/AGENT-FAILURES.md +69 -0
  158. package/src/orchestrator/customizations/AGENT-PERFORMANCE.md +58 -0
  159. package/src/orchestrator/customizations/DISPUTES.md +162 -0
  160. package/src/orchestrator/customizations/KNOWLEDGE-GRAPH.md +10 -0
  161. package/src/orchestrator/customizations/LESSONS-LEARNED.md +70 -0
  162. package/src/orchestrator/customizations/README.md +59 -0
  163. package/src/orchestrator/customizations/agents/agent-registry.md +46 -0
  164. package/src/orchestrator/customizations/agents/skill-matrix.md +142 -0
  165. package/src/orchestrator/customizations/logs/README.md +181 -0
  166. package/src/orchestrator/customizations/logs/delegations.ndjson +1 -0
  167. package/src/orchestrator/customizations/logs/panels.ndjson +1 -0
  168. package/src/orchestrator/customizations/logs/sessions.ndjson +1 -0
  169. package/src/orchestrator/customizations/project/docs-structure.md +23 -0
  170. package/src/orchestrator/customizations/project/tracker-config.md +45 -0
  171. package/src/orchestrator/customizations/project.instructions.md +64 -0
  172. package/src/orchestrator/customizations/stack/api-config.md +37 -0
  173. package/src/orchestrator/customizations/stack/cms-config.md +26 -0
  174. package/src/orchestrator/customizations/stack/data-pipeline-config.md +41 -0
  175. package/src/orchestrator/customizations/stack/database-config.md +44 -0
  176. package/src/orchestrator/customizations/stack/deployment-config.md +45 -0
  177. package/src/orchestrator/customizations/stack/testing-config.md +56 -0
  178. package/src/orchestrator/instructions/ai-optimization.instructions.md +143 -0
  179. package/src/orchestrator/instructions/general.instructions.md +194 -0
  180. package/src/orchestrator/mcp.json +55 -0
  181. package/src/orchestrator/prompts/bootstrap-customizations.prompt.md +235 -0
  182. package/src/orchestrator/prompts/brainstorm.prompt.md +115 -0
  183. package/src/orchestrator/prompts/bug-fix.prompt.md +141 -0
  184. package/src/orchestrator/prompts/create-skill.prompt.md +103 -0
  185. package/src/orchestrator/prompts/generate-task-spec.prompt.md +154 -0
  186. package/src/orchestrator/prompts/implement-feature.prompt.md +124 -0
  187. package/src/orchestrator/prompts/metrics-report.prompt.md +142 -0
  188. package/src/orchestrator/prompts/quick-refinement.prompt.md +137 -0
  189. package/src/orchestrator/prompts/resolve-pr-comments.prompt.md +100 -0
  190. package/src/orchestrator/skills/accessibility-standards/SKILL.md +164 -0
  191. package/src/orchestrator/skills/agent-hooks/SKILL.md +147 -0
  192. package/src/orchestrator/skills/agent-memory/SKILL.md +144 -0
  193. package/src/orchestrator/skills/api-patterns/SKILL.md +106 -0
  194. package/src/orchestrator/skills/browser-testing/SKILL.md +203 -0
  195. package/src/orchestrator/skills/code-commenting/SKILL.md +133 -0
  196. package/src/orchestrator/skills/contentful-cms/SKILL.md +43 -0
  197. package/src/orchestrator/skills/context-map/SKILL.md +135 -0
  198. package/src/orchestrator/skills/convex-database/SKILL.md +80 -0
  199. package/src/orchestrator/skills/data-engineering/SKILL.md +99 -0
  200. package/src/orchestrator/skills/deployment-infrastructure/SKILL.md +49 -0
  201. package/src/orchestrator/skills/documentation-standards/SKILL.md +85 -0
  202. package/src/orchestrator/skills/fast-review/SKILL.md +327 -0
  203. package/src/orchestrator/skills/frontend-design/SKILL.md +42 -0
  204. package/src/orchestrator/skills/jira-management/SKILL.md +168 -0
  205. package/src/orchestrator/skills/memory-merger/SKILL.md +123 -0
  206. package/src/orchestrator/skills/nextjs-patterns/SKILL.md +75 -0
  207. package/src/orchestrator/skills/nx-workspace/SKILL.md +192 -0
  208. package/src/orchestrator/skills/panel-majority-vote/SKILL.md +184 -0
  209. package/src/orchestrator/skills/panel-majority-vote/panel-report.template.md +38 -0
  210. package/src/orchestrator/skills/performance-optimization/SKILL.md +101 -0
  211. package/src/orchestrator/skills/react-development/SKILL.md +117 -0
  212. package/src/orchestrator/skills/sanity-cms/SKILL.md +18 -0
  213. package/src/orchestrator/skills/security-hardening/SKILL.md +118 -0
  214. package/src/orchestrator/skills/self-improvement/SKILL.md +137 -0
  215. package/src/orchestrator/skills/seo-patterns/SKILL.md +40 -0
  216. package/src/orchestrator/skills/session-checkpoints/SKILL.md +205 -0
  217. package/src/orchestrator/skills/slack-notifications/SKILL.md +211 -0
  218. package/src/orchestrator/skills/strapi-cms/SKILL.md +43 -0
  219. package/src/orchestrator/skills/supabase-database/SKILL.md +24 -0
  220. package/src/orchestrator/skills/task-management/SKILL.md +143 -0
  221. package/src/orchestrator/skills/team-lead-reference/SKILL.md +317 -0
  222. package/src/orchestrator/skills/teams-notifications/SKILL.md +249 -0
  223. package/src/orchestrator/skills/testing-workflow/SKILL.md +134 -0
  224. package/src/orchestrator/skills/validation-gates/SKILL.md +100 -0
@@ -0,0 +1,287 @@
1
+ import { resolve, basename } from 'node:path'
2
+ import { mkdir, writeFile, readdir, readFile, unlink, rm } from 'node:fs/promises'
3
+ import { existsSync } from 'node:fs'
4
+ import { copyDir, getOrchestratorRoot } from '../copy.js'
5
+ import { scaffoldMcpConfig } from '../mcp.js'
6
+ import { getExcludedSkills, getExcludedAgents, getCustomizationsTransform } from '../stack-config.js'
7
+ import type { CopyResults, ManagedPaths, StackConfig } from '../types.js'
8
+
9
+ /**
10
+ * Claude Code adapter.
11
+ *
12
+ * Generates CLAUDE.md (root instructions) and .claude/ structure.
13
+ *
14
+ * copilot-instructions.md → CLAUDE.md (combined with instructions/)
15
+ * skills/\*\/SKILL.md → .claude/skills/<name>.md
16
+ * agent-workflows/*.md → .claude/commands/workflow-<name>.md
17
+ * prompts/*.prompt.md → .claude/commands/<name>.md
18
+ * customizations/ → .claude/customizations/ (scaffolded once)
19
+ *
20
+ * Note: Claude Code has no "agents" concept. Agent definitions are embedded
21
+ * as reference sections within CLAUDE.md so Claude can adopt personas
22
+ * when asked.
23
+ */
24
+
25
+ export const IDE_ID = 'claude-code'
26
+ export const IDE_LABEL = 'Claude Code'
27
+
28
+ // ─── Helpers ──────────────────────────────────────────────────────
29
+
30
+ function stripFrontmatter(content: string): string {
31
+ const m = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/)
32
+ return m ? m[2].trim() : content.trim()
33
+ }
34
+
35
+ function parseFrontmatterMeta(content: string): Record<string, string> {
36
+ const m = content.match(/^---\n([\s\S]*?)\n---/)
37
+ if (!m) return {}
38
+ const meta: Record<string, string> = {}
39
+ for (const line of m[1].split('\n')) {
40
+ const kv = line.match(/^(\w[\w-]*):\s*['"]?(.+?)['"]?\s*$/)
41
+ if (kv) meta[kv[1]] = kv[2]
42
+ }
43
+ return meta
44
+ }
45
+
46
+ // ─── Install ──────────────────────────────────────────────────────
47
+
48
+ export async function install(
49
+ pkgRoot: string,
50
+ projectRoot: string,
51
+ stack?: StackConfig
52
+ ): Promise<CopyResults> {
53
+ const srcRoot = getOrchestratorRoot(pkgRoot)
54
+ const results: CopyResults = { copied: [], skipped: [], created: [] }
55
+
56
+ const excludedSkills = stack ? getExcludedSkills(stack) : new Set<string>()
57
+ const excludedAgents = stack ? getExcludedAgents(stack) : new Set<string>()
58
+
59
+ // 1. Build CLAUDE.md ← copilot-instructions + instructions/* + agent index + skill index
60
+ const claudeMd = resolve(projectRoot, 'CLAUDE.md')
61
+ if (!existsSync(claudeMd)) {
62
+ const sections: string[] = []
63
+
64
+ // Main instructions
65
+ const main = await readFile(
66
+ resolve(srcRoot, 'copilot-instructions.md'),
67
+ 'utf8'
68
+ )
69
+ sections.push(stripFrontmatter(main))
70
+
71
+ // Always-loaded instruction files
72
+ const instDir = resolve(srcRoot, 'instructions')
73
+ if (existsSync(instDir)) {
74
+ for (const file of (await readdir(instDir)).sort()) {
75
+ if (!file.endsWith('.md')) continue
76
+ const content = await readFile(resolve(instDir, file), 'utf8')
77
+ sections.push(
78
+ `\n---\n\n<!-- Source: instructions/${file} -->\n\n${stripFrontmatter(content)}`
79
+ )
80
+ }
81
+ }
82
+
83
+ // Agent reference (so Claude can adopt personas)
84
+ const agentsDir = resolve(srcRoot, 'agents')
85
+ if (existsSync(agentsDir)) {
86
+ const agentLines: string[] = ['\n---\n\n## Agent Definitions\n']
87
+ agentLines.push(
88
+ 'The following agent personas are available. Adopt the appropriate persona when asked.\n'
89
+ )
90
+ for (const file of (await readdir(agentsDir)).sort()) {
91
+ if (!file.endsWith('.md')) continue
92
+ if (excludedAgents.has(file)) continue
93
+ const meta = parseFrontmatterMeta(
94
+ await readFile(resolve(agentsDir, file), 'utf8')
95
+ )
96
+ const name = meta['name'] ?? basename(file, '.agent.md')
97
+ const desc = meta['description'] ?? ''
98
+ agentLines.push(`- **${name}**: ${desc}`)
99
+ }
100
+ agentLines.push(
101
+ '\nFull agent definitions are in `.claude/agents/`. Read the relevant file when adopting a persona.'
102
+ )
103
+ sections.push(agentLines.join('\n'))
104
+ }
105
+
106
+ // Skill index
107
+ const skillsDir = resolve(srcRoot, 'skills')
108
+ if (existsSync(skillsDir)) {
109
+ const skillLines: string[] = ['\n---\n\n## Available Skills\n']
110
+ skillLines.push(
111
+ 'Skills are on-demand knowledge files. Read the file when the task matches.\n'
112
+ )
113
+ const subdirs = (
114
+ await readdir(skillsDir, { withFileTypes: true })
115
+ ).filter((e) => e.isDirectory())
116
+ for (const entry of subdirs.sort((a, b) =>
117
+ a.name.localeCompare(b.name)
118
+ )) {
119
+ if (excludedSkills.has(entry.name)) continue
120
+ const skillFile = resolve(skillsDir, entry.name, 'SKILL.md')
121
+ if (!existsSync(skillFile)) continue
122
+ const meta = parseFrontmatterMeta(await readFile(skillFile, 'utf8'))
123
+ const desc = meta['description'] ?? ''
124
+ skillLines.push(
125
+ `- **${entry.name}** (\`.claude/skills/${entry.name}.md\`): ${desc}`
126
+ )
127
+ }
128
+ sections.push(skillLines.join('\n'))
129
+ }
130
+
131
+ await writeFile(claudeMd, sections.join('\n') + '\n')
132
+ results.created.push(claudeMd)
133
+ } else {
134
+ results.skipped.push(claudeMd)
135
+ }
136
+
137
+ const claudeDir = resolve(projectRoot, '.claude')
138
+
139
+ // 2. Agent definitions → .claude/agents/
140
+ const agentsDir = resolve(srcRoot, 'agents')
141
+ if (existsSync(agentsDir)) {
142
+ const destAgents = resolve(claudeDir, 'agents')
143
+ await mkdir(destAgents, { recursive: true })
144
+ for (const file of await readdir(agentsDir)) {
145
+ if (!file.endsWith('.md')) continue
146
+ if (excludedAgents.has(file)) continue
147
+ const destPath = resolve(destAgents, file)
148
+ if (existsSync(destPath)) {
149
+ results.skipped.push(destPath)
150
+ continue
151
+ }
152
+ const content = await readFile(resolve(agentsDir, file), 'utf8')
153
+ await writeFile(destPath, stripFrontmatter(content) + '\n')
154
+ results.created.push(destPath)
155
+ }
156
+ }
157
+
158
+ // 3. Skills → .claude/skills/<name>.md
159
+ const skillsDir = resolve(srcRoot, 'skills')
160
+ if (existsSync(skillsDir)) {
161
+ const destSkills = resolve(claudeDir, 'skills')
162
+ await mkdir(destSkills, { recursive: true })
163
+ const subdirs = (
164
+ await readdir(skillsDir, { withFileTypes: true })
165
+ ).filter((e) => e.isDirectory())
166
+ for (const entry of subdirs) {
167
+ if (excludedSkills.has(entry.name)) continue
168
+ const skillFile = resolve(skillsDir, entry.name, 'SKILL.md')
169
+ if (!existsSync(skillFile)) continue
170
+ const destPath = resolve(destSkills, `${entry.name}.md`)
171
+ if (existsSync(destPath)) {
172
+ results.skipped.push(destPath)
173
+ continue
174
+ }
175
+ const content = await readFile(skillFile, 'utf8')
176
+ await writeFile(destPath, stripFrontmatter(content) + '\n')
177
+ results.created.push(destPath)
178
+ }
179
+ }
180
+
181
+ // 4. Prompts → .claude/commands/<name>.md
182
+ const promptDir = resolve(srcRoot, 'prompts')
183
+ if (existsSync(promptDir)) {
184
+ const destCmds = resolve(claudeDir, 'commands')
185
+ await mkdir(destCmds, { recursive: true })
186
+ for (const file of await readdir(promptDir)) {
187
+ if (!file.endsWith('.md')) continue
188
+ const name = basename(file, '.prompt.md') || basename(file, '.md')
189
+ const destPath = resolve(destCmds, `${name}.md`)
190
+ if (existsSync(destPath)) {
191
+ results.skipped.push(destPath)
192
+ continue
193
+ }
194
+ const content = await readFile(resolve(promptDir, file), 'utf8')
195
+ await writeFile(destPath, stripFrontmatter(content) + '\n')
196
+ results.created.push(destPath)
197
+ }
198
+ }
199
+
200
+ // 5. Agent Workflows → .claude/commands/workflow-<name>.md
201
+ const wfDir = resolve(srcRoot, 'agent-workflows')
202
+ if (existsSync(wfDir)) {
203
+ const destCmds = resolve(claudeDir, 'commands')
204
+ await mkdir(destCmds, { recursive: true })
205
+ for (const file of await readdir(wfDir)) {
206
+ if (!file.endsWith('.md')) continue
207
+ const name = basename(file, '.md')
208
+ const destPath = resolve(destCmds, `workflow-${name}.md`)
209
+ if (existsSync(destPath)) {
210
+ results.skipped.push(destPath)
211
+ continue
212
+ }
213
+ const content = await readFile(resolve(wfDir, file), 'utf8')
214
+ await writeFile(destPath, stripFrontmatter(content) + '\n')
215
+ results.created.push(destPath)
216
+ }
217
+ }
218
+
219
+ // 6. Customizations (scaffold once, pre-populated with stack choices)
220
+ const custDir = resolve(srcRoot, 'customizations')
221
+ if (existsSync(custDir)) {
222
+ const destCust = resolve(claudeDir, 'customizations')
223
+ const custTransform = stack ? getCustomizationsTransform(stack) : undefined
224
+ const sub = await copyDir(custDir, destCust, { transform: custTransform })
225
+ results.created.push(...sub.created)
226
+ results.skipped.push(...sub.skipped)
227
+ }
228
+
229
+ // 7. MCP server config → .claude/mcp.json (scaffold once)
230
+ const mcpResult = await scaffoldMcpConfig(
231
+ pkgRoot,
232
+ projectRoot,
233
+ '.claude/mcp.json',
234
+ stack
235
+ )
236
+ results[mcpResult.action].push(mcpResult.path)
237
+
238
+ return results
239
+ }
240
+
241
+ // ─── Update ───────────────────────────────────────────────────────
242
+
243
+ export async function update(
244
+ pkgRoot: string,
245
+ projectRoot: string,
246
+ stack?: StackConfig
247
+ ): Promise<CopyResults> {
248
+ const results: CopyResults = { copied: [], skipped: [], created: [] }
249
+ const claudeDir = resolve(projectRoot, '.claude')
250
+
251
+ // 1. Regenerate CLAUDE.md (overwrite)
252
+ const claudeMd = resolve(projectRoot, 'CLAUDE.md')
253
+ if (existsSync(claudeMd)) {
254
+ await unlink(claudeMd)
255
+ }
256
+
257
+ // 2. Remove existing framework files so install() recreates them
258
+ const frameworkDirs = ['agents', 'skills', 'commands']
259
+ for (const dir of frameworkDirs) {
260
+ const dirPath = resolve(claudeDir, dir)
261
+ if (existsSync(dirPath)) {
262
+ await rm(dirPath, { recursive: true })
263
+ }
264
+ }
265
+
266
+ // 3. Re-run full install (CLAUDE.md + agents + skills + commands)
267
+ const installResult = await install(pkgRoot, projectRoot, stack)
268
+ // Everything install created is an "update" copy
269
+ results.copied.push(...installResult.created)
270
+ results.skipped.push(...installResult.skipped)
271
+
272
+ return results
273
+ }
274
+
275
+ // ─── Managed paths ────────────────────────────────────────────────
276
+
277
+ export function getManagedPaths(): ManagedPaths {
278
+ return {
279
+ framework: [
280
+ 'CLAUDE.md',
281
+ '.claude/agents/',
282
+ '.claude/skills/',
283
+ '.claude/commands/',
284
+ ],
285
+ customizable: ['.claude/customizations/', '.claude/mcp.json'],
286
+ }
287
+ }
@@ -0,0 +1,377 @@
1
+ import { resolve, basename } from 'node:path'
2
+ import { mkdir, writeFile, readdir, readFile } from 'node:fs/promises'
3
+ import { existsSync } from 'node:fs'
4
+ import { copyDir, getOrchestratorRoot, removeDirIfExists } from '../copy.js'
5
+ import { scaffoldMcpConfig } from '../mcp.js'
6
+ import { getExcludedSkills, getExcludedAgents, getCustomizationsTransform } from '../stack-config.js'
7
+ import type { CopyResults, ManagedPaths, StackConfig } from '../types.js'
8
+
9
+ /**
10
+ * Cursor adapter.
11
+ *
12
+ * Transforms Copilot-format orchestrator files into Cursor's .mdc rule format.
13
+ *
14
+ * copilot-instructions.md → .cursorrules
15
+ * instructions/*.md → .cursor/rules/*.mdc (alwaysApply: true)
16
+ * agents/*.agent.md → .cursor/rules/agents/*.mdc (description-triggered)
17
+ * skills/\*\/SKILL.md → .cursor/rules/skills/*.mdc (alwaysApply: false)
18
+ * agent-workflows/*.md → .cursor/rules/agent-workflows/*.mdc
19
+ * prompts/*.prompt.md → .cursor/rules/prompts/*.mdc
20
+ * customizations/ → .cursor/rules/customizations/ (scaffolded once)
21
+ */
22
+
23
+ export const IDE_ID = 'cursor'
24
+ export const IDE_LABEL = 'Cursor'
25
+
26
+ // ─── Helpers ──────────────────────────────────────────────────────
27
+
28
+ interface FrontmatterResult {
29
+ frontmatter: string
30
+ body: string
31
+ }
32
+
33
+ function stripFrontmatter(content: string): FrontmatterResult {
34
+ const m = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/)
35
+ return m
36
+ ? { frontmatter: m[1], body: m[2] }
37
+ : { frontmatter: '', body: content }
38
+ }
39
+
40
+ function parseFrontmatter(fm: string): Record<string, string> {
41
+ const result: Record<string, string> = {}
42
+ for (const line of fm.split('\n')) {
43
+ const m = line.match(/^(\w[\w-]*):\s*['"]?(.+?)['"]?\s*$/)
44
+ if (m) result[m[1]] = m[2]
45
+ }
46
+ return result
47
+ }
48
+
49
+ interface MdcOptions {
50
+ description?: string
51
+ globs?: string[]
52
+ alwaysApply: boolean
53
+ body: string
54
+ }
55
+
56
+ function toMdc({ description, globs, alwaysApply, body }: MdcOptions): string {
57
+ const lines = ['---']
58
+ if (description) lines.push(`description: "${description}"`)
59
+ if (globs) lines.push(`globs: ${JSON.stringify(globs)}`)
60
+ lines.push(`alwaysApply: ${alwaysApply ? 'true' : 'false'}`)
61
+ lines.push('---', '', body.trim(), '')
62
+ return lines.join('\n')
63
+ }
64
+
65
+ /** Convert a source filename to .mdc, stripping intermediate extensions. */
66
+ function mdcName(name: string): string {
67
+ // Handle compound extensions like .agent.md, .instructions.md, .prompt.md
68
+ const compound = name.replace(/\.(agent|instructions|prompt)\.md$/, '.mdc')
69
+ if (compound !== name) return compound
70
+ // Handle plain .md files
71
+ return name.replace(/\.md$/, '.mdc')
72
+ }
73
+
74
+ interface ConvertFileOptions {
75
+ alwaysApply?: boolean
76
+ descriptionFallback?: string
77
+ }
78
+
79
+ /** Read a source .md and produce an .mdc string. */
80
+ async function convertFile(
81
+ srcPath: string,
82
+ { alwaysApply = false, descriptionFallback = '' }: ConvertFileOptions = {}
83
+ ): Promise<string> {
84
+ const content = await readFile(srcPath, 'utf8')
85
+ const { frontmatter, body } = stripFrontmatter(content)
86
+ const meta = parseFrontmatter(frontmatter)
87
+
88
+ // Description: frontmatter > fallback > first heading
89
+ let description = meta['description'] ?? descriptionFallback
90
+ if (!description) {
91
+ const heading = body.match(/^#\s+(.+)/m)
92
+ if (heading) description = heading[1]
93
+ }
94
+
95
+ // If applyTo is '**' the rule should always apply
96
+ const globs = meta['applyTo'] ? [meta['applyTo']] : undefined
97
+ const apply = meta['applyTo'] === '**' ? true : alwaysApply
98
+
99
+ return toMdc({ description, globs, alwaysApply: apply, body: body.trim() })
100
+ }
101
+
102
+ /** Write a converted file; skip if it already exists (unless overwrite). */
103
+ async function writeConverted(
104
+ srcPath: string,
105
+ destPath: string,
106
+ opts: ConvertFileOptions,
107
+ results: CopyResults,
108
+ overwrite = false
109
+ ): Promise<void> {
110
+ if (!overwrite && existsSync(destPath)) {
111
+ results.skipped.push(destPath)
112
+ return
113
+ }
114
+ const existed = existsSync(destPath)
115
+ const mdc = await convertFile(srcPath, opts)
116
+ await writeFile(destPath, mdc)
117
+ results[existed ? 'copied' : 'created'].push(destPath)
118
+ }
119
+
120
+ // ─── Install ──────────────────────────────────────────────────────
121
+
122
+ export async function install(
123
+ pkgRoot: string,
124
+ projectRoot: string,
125
+ stack?: StackConfig
126
+ ): Promise<CopyResults> {
127
+ const srcRoot = getOrchestratorRoot(pkgRoot)
128
+ const results: CopyResults = { copied: [], skipped: [], created: [] }
129
+
130
+ const excludedSkills = stack ? getExcludedSkills(stack) : new Set<string>()
131
+ const excludedAgents = stack ? getExcludedAgents(stack) : new Set<string>()
132
+
133
+ // 1. .cursorrules ← copilot-instructions.md (body only)
134
+ const cursorrules = resolve(projectRoot, '.cursorrules')
135
+ if (!existsSync(cursorrules)) {
136
+ const { body } = stripFrontmatter(
137
+ await readFile(resolve(srcRoot, 'copilot-instructions.md'), 'utf8')
138
+ )
139
+ await writeFile(cursorrules, body.trim() + '\n')
140
+ results.created.push(cursorrules)
141
+ } else {
142
+ results.skipped.push(cursorrules)
143
+ }
144
+
145
+ const rulesRoot = resolve(projectRoot, '.cursor', 'rules')
146
+ await mkdir(rulesRoot, { recursive: true })
147
+
148
+ // 2. Instructions → .cursor/rules/*.mdc (alwaysApply: true)
149
+ await convertDir(srcRoot, 'instructions', rulesRoot, results, {
150
+ alwaysApply: true,
151
+ })
152
+
153
+ // 3. Agents → .cursor/rules/agents/*.mdc
154
+ await convertDir(srcRoot, 'agents', resolve(rulesRoot, 'agents'), results, {
155
+ descriptionPrefix: 'Agent: ',
156
+ removeExt: '.agent.md',
157
+ excludeFiles: excludedAgents,
158
+ })
159
+
160
+ // 4. Skills → .cursor/rules/skills/*.mdc
161
+ await convertSkills(srcRoot, resolve(rulesRoot, 'skills'), results, false, excludedSkills)
162
+
163
+ // 5. Agent Workflows → .cursor/rules/agent-workflows/*.mdc
164
+ await convertDir(
165
+ srcRoot,
166
+ 'agent-workflows',
167
+ resolve(rulesRoot, 'agent-workflows'),
168
+ results,
169
+ { descriptionPrefix: 'Workflow: ' }
170
+ )
171
+
172
+ // 6. Prompts → .cursor/rules/prompts/*.mdc
173
+ await convertDir(srcRoot, 'prompts', resolve(rulesRoot, 'prompts'), results, {
174
+ descriptionPrefix: 'Prompt: ',
175
+ removeExt: '.prompt.md',
176
+ })
177
+
178
+ // 7. Customizations (scaffold once, pre-populated with stack choices)
179
+ const custSrc = resolve(srcRoot, 'customizations')
180
+ if (existsSync(custSrc)) {
181
+ const custDest = resolve(rulesRoot, 'customizations')
182
+ const custTransform = stack ? getCustomizationsTransform(stack) : undefined
183
+ const sub = await copyDir(custSrc, custDest, { transform: custTransform })
184
+ results.created.push(...sub.created)
185
+ results.skipped.push(...sub.skipped)
186
+ }
187
+
188
+ // 8. MCP server config → .cursor/mcp.json (scaffold once)
189
+ const mcpResult = await scaffoldMcpConfig(
190
+ pkgRoot,
191
+ projectRoot,
192
+ '.cursor/mcp.json',
193
+ stack
194
+ )
195
+ results[mcpResult.action].push(mcpResult.path)
196
+
197
+ return results
198
+ }
199
+
200
+ // ─── Update ───────────────────────────────────────────────────────
201
+
202
+ export async function update(
203
+ pkgRoot: string,
204
+ projectRoot: string,
205
+ stack?: StackConfig
206
+ ): Promise<CopyResults> {
207
+ const srcRoot = getOrchestratorRoot(pkgRoot)
208
+ const results: CopyResults = { copied: [], skipped: [], created: [] }
209
+
210
+ const excludedSkills = stack ? getExcludedSkills(stack) : new Set<string>()
211
+ const excludedAgents = stack ? getExcludedAgents(stack) : new Set<string>()
212
+
213
+ // Overwrite .cursorrules
214
+ const { body } = stripFrontmatter(
215
+ await readFile(resolve(srcRoot, 'copilot-instructions.md'), 'utf8')
216
+ )
217
+ await writeFile(resolve(projectRoot, '.cursorrules'), body.trim() + '\n')
218
+ results.copied.push('.cursorrules')
219
+
220
+ const rulesRoot = resolve(projectRoot, '.cursor', 'rules')
221
+
222
+ // Remove existing framework rule directories to clear stale files
223
+ const FRAMEWORK_RULE_DIRS = ['agents', 'skills', 'agent-workflows', 'prompts']
224
+ for (const dir of FRAMEWORK_RULE_DIRS) {
225
+ await removeDirIfExists(resolve(rulesRoot, dir))
226
+ }
227
+
228
+ // Overwrite framework rules
229
+ await convertDir(srcRoot, 'instructions', rulesRoot, results, {
230
+ alwaysApply: true,
231
+ overwrite: true,
232
+ })
233
+ await convertDir(
234
+ srcRoot,
235
+ 'agents',
236
+ resolve(rulesRoot, 'agents'),
237
+ results,
238
+ { descriptionPrefix: 'Agent: ', removeExt: '.agent.md', overwrite: true, excludeFiles: excludedAgents }
239
+ )
240
+ await convertSkills(srcRoot, resolve(rulesRoot, 'skills'), results, true, excludedSkills)
241
+ await convertDir(
242
+ srcRoot,
243
+ 'agent-workflows',
244
+ resolve(rulesRoot, 'agent-workflows'),
245
+ results,
246
+ { descriptionPrefix: 'Workflow: ', overwrite: true }
247
+ )
248
+ await convertDir(
249
+ srcRoot,
250
+ 'prompts',
251
+ resolve(rulesRoot, 'prompts'),
252
+ results,
253
+ { descriptionPrefix: 'Prompt: ', removeExt: '.prompt.md', overwrite: true }
254
+ )
255
+
256
+ // Customizations are NEVER overwritten.
257
+
258
+ // All re-installed framework files count as "updated" (copied), not "created"
259
+ results.copied.push(...results.created)
260
+ results.created = []
261
+
262
+ return results
263
+ }
264
+
265
+ // ─── Managed paths ────────────────────────────────────────────────
266
+
267
+ export function getManagedPaths(): ManagedPaths {
268
+ return {
269
+ framework: [
270
+ '.cursorrules',
271
+ '.cursor/rules/agents/',
272
+ '.cursor/rules/skills/',
273
+ '.cursor/rules/agent-workflows/',
274
+ '.cursor/rules/prompts/',
275
+ '.cursor/rules/general.mdc',
276
+ '.cursor/rules/ai-optimization.mdc',
277
+ ],
278
+ customizable: [
279
+ '.cursor/rules/customizations/',
280
+ '.cursor/mcp.json',
281
+ ],
282
+ }
283
+ }
284
+
285
+ // ─── Internal helpers ─────────────────────────────────────────────
286
+
287
+ interface ConvertDirOptions {
288
+ alwaysApply?: boolean
289
+ descriptionPrefix?: string
290
+ removeExt?: string
291
+ overwrite?: boolean
292
+ excludeFiles?: Set<string>
293
+ }
294
+
295
+ async function convertDir(
296
+ srcRoot: string,
297
+ dirName: string,
298
+ destDir: string,
299
+ results: CopyResults,
300
+ {
301
+ alwaysApply,
302
+ descriptionPrefix,
303
+ removeExt,
304
+ overwrite,
305
+ excludeFiles,
306
+ }: ConvertDirOptions = {}
307
+ ): Promise<void> {
308
+ const srcDir = resolve(srcRoot, dirName)
309
+ if (!existsSync(srcDir)) return
310
+
311
+ await mkdir(destDir, { recursive: true })
312
+
313
+ for (const file of await readdir(srcDir)) {
314
+ if (!file.endsWith('.md')) continue
315
+ if (excludeFiles?.has(file)) continue
316
+ const fallback = descriptionPrefix
317
+ ? `${descriptionPrefix}${basename(file, removeExt ?? '.md')}`
318
+ : ''
319
+ const destPath = resolve(destDir, mdcName(file))
320
+ await writeConverted(
321
+ resolve(srcDir, file),
322
+ destPath,
323
+ { alwaysApply: alwaysApply ?? false, descriptionFallback: fallback },
324
+ results,
325
+ overwrite
326
+ )
327
+ }
328
+ }
329
+
330
+ async function convertSkills(
331
+ srcRoot: string,
332
+ destDir: string,
333
+ results: CopyResults,
334
+ overwrite = false,
335
+ excludedSkills?: Set<string>
336
+ ): Promise<void> {
337
+ const skillsDir = resolve(srcRoot, 'skills')
338
+ if (!existsSync(skillsDir)) return
339
+
340
+ await mkdir(destDir, { recursive: true })
341
+
342
+ const subdirs = await readdir(skillsDir, { withFileTypes: true })
343
+ for (const entry of subdirs) {
344
+ if (!entry.isDirectory()) continue
345
+ if (excludedSkills?.has(entry.name)) continue
346
+ const skillFile = resolve(skillsDir, entry.name, 'SKILL.md')
347
+ if (!existsSync(skillFile)) continue
348
+
349
+ // Main skill → skills/<name>.mdc
350
+ const destPath = resolve(destDir, `${entry.name}.mdc`)
351
+ await writeConverted(
352
+ skillFile,
353
+ destPath,
354
+ { descriptionFallback: `Skill: ${entry.name}` },
355
+ results,
356
+ overwrite
357
+ )
358
+
359
+ // Extra files in the skill directory (e.g. templates)
360
+ const files = await readdir(resolve(skillsDir, entry.name))
361
+ const extras = files.filter((f) => f !== 'SKILL.md' && f.endsWith('.md'))
362
+ if (extras.length > 0) {
363
+ const subDest = resolve(destDir, entry.name)
364
+ await mkdir(subDest, { recursive: true })
365
+ for (const file of extras) {
366
+ const extraDest = resolve(subDest, mdcName(file))
367
+ await writeConverted(
368
+ resolve(skillsDir, entry.name, file),
369
+ extraDest,
370
+ { descriptionFallback: `${entry.name}: ${basename(file, '.md')}` },
371
+ results,
372
+ overwrite
373
+ )
374
+ }
375
+ }
376
+ }
377
+ }