opencastle 0.5.1 → 0.7.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 (231) hide show
  1. package/README.md +5 -4
  2. package/dist/cli/adapters/claude-code.d.ts +2 -2
  3. package/dist/cli/adapters/claude-code.d.ts.map +1 -1
  4. package/dist/cli/adapters/claude-code.js +31 -4
  5. package/dist/cli/adapters/claude-code.js.map +1 -1
  6. package/dist/cli/adapters/cursor.d.ts +2 -2
  7. package/dist/cli/adapters/cursor.d.ts.map +1 -1
  8. package/dist/cli/adapters/cursor.js +28 -4
  9. package/dist/cli/adapters/cursor.js.map +1 -1
  10. package/dist/cli/adapters/opencode.d.ts +20 -0
  11. package/dist/cli/adapters/opencode.d.ts.map +1 -0
  12. package/dist/cli/adapters/opencode.js +265 -0
  13. package/dist/cli/adapters/opencode.js.map +1 -0
  14. package/dist/cli/adapters/vscode.d.ts +2 -2
  15. package/dist/cli/adapters/vscode.d.ts.map +1 -1
  16. package/dist/cli/adapters/vscode.js +38 -7
  17. package/dist/cli/adapters/vscode.js.map +1 -1
  18. package/dist/cli/copy.d.ts +12 -0
  19. package/dist/cli/copy.d.ts.map +1 -1
  20. package/dist/cli/copy.js +27 -0
  21. package/dist/cli/copy.js.map +1 -1
  22. package/dist/cli/detect.d.ts +18 -0
  23. package/dist/cli/detect.d.ts.map +1 -0
  24. package/dist/cli/detect.js +434 -0
  25. package/dist/cli/detect.js.map +1 -0
  26. package/dist/cli/gitignore.d.ts.map +1 -1
  27. package/dist/cli/gitignore.js +0 -2
  28. package/dist/cli/gitignore.js.map +1 -1
  29. package/dist/cli/init.d.ts.map +1 -1
  30. package/dist/cli/init.js +154 -91
  31. package/dist/cli/init.js.map +1 -1
  32. package/dist/cli/manifest.d.ts +1 -1
  33. package/dist/cli/manifest.d.ts.map +1 -1
  34. package/dist/cli/manifest.js +2 -1
  35. package/dist/cli/manifest.js.map +1 -1
  36. package/dist/cli/mcp.d.ts +6 -6
  37. package/dist/cli/mcp.d.ts.map +1 -1
  38. package/dist/cli/mcp.js +105 -34
  39. package/dist/cli/mcp.js.map +1 -1
  40. package/dist/cli/prompt.d.ts +22 -0
  41. package/dist/cli/prompt.d.ts.map +1 -1
  42. package/dist/cli/prompt.js +239 -0
  43. package/dist/cli/prompt.js.map +1 -1
  44. package/dist/cli/stack-config.d.ts +26 -3
  45. package/dist/cli/stack-config.d.ts.map +1 -1
  46. package/dist/cli/stack-config.js +140 -125
  47. package/dist/cli/stack-config.js.map +1 -1
  48. package/dist/cli/types.d.ts +46 -10
  49. package/dist/cli/types.d.ts.map +1 -1
  50. package/dist/cli/types.js +26 -1
  51. package/dist/cli/types.js.map +1 -1
  52. package/dist/cli/update.d.ts.map +1 -1
  53. package/dist/cli/update.js +66 -19
  54. package/dist/cli/update.js.map +1 -1
  55. package/dist/orchestrator/plugins/chrome-devtools/config.d.ts +3 -0
  56. package/dist/orchestrator/plugins/chrome-devtools/config.d.ts.map +1 -0
  57. package/dist/orchestrator/plugins/chrome-devtools/config.js +28 -0
  58. package/dist/orchestrator/plugins/chrome-devtools/config.js.map +1 -0
  59. package/dist/orchestrator/plugins/contentful/config.d.ts +3 -0
  60. package/dist/orchestrator/plugins/contentful/config.d.ts.map +1 -0
  61. package/dist/orchestrator/plugins/contentful/config.js +48 -0
  62. package/dist/orchestrator/plugins/contentful/config.js.map +1 -0
  63. package/dist/orchestrator/plugins/convex/config.d.ts +3 -0
  64. package/dist/orchestrator/plugins/convex/config.d.ts.map +1 -0
  65. package/dist/orchestrator/plugins/convex/config.js +32 -0
  66. package/dist/orchestrator/plugins/convex/config.js.map +1 -0
  67. package/dist/orchestrator/plugins/index.d.ts +28 -0
  68. package/dist/orchestrator/plugins/index.d.ts.map +1 -0
  69. package/dist/orchestrator/plugins/index.js +63 -0
  70. package/dist/orchestrator/plugins/index.js.map +1 -0
  71. package/dist/orchestrator/plugins/jira/config.d.ts +3 -0
  72. package/dist/orchestrator/plugins/jira/config.d.ts.map +1 -0
  73. package/dist/orchestrator/plugins/jira/config.js +29 -0
  74. package/dist/orchestrator/plugins/jira/config.js.map +1 -0
  75. package/dist/orchestrator/plugins/linear/config.d.ts +3 -0
  76. package/dist/orchestrator/plugins/linear/config.d.ts.map +1 -0
  77. package/dist/orchestrator/plugins/linear/config.js +33 -0
  78. package/dist/orchestrator/plugins/linear/config.js.map +1 -0
  79. package/dist/orchestrator/plugins/nx/config.d.ts +3 -0
  80. package/dist/orchestrator/plugins/nx/config.d.ts.map +1 -0
  81. package/dist/orchestrator/plugins/nx/config.js +28 -0
  82. package/dist/orchestrator/plugins/nx/config.js.map +1 -0
  83. package/dist/orchestrator/plugins/sanity/config.d.ts +3 -0
  84. package/dist/orchestrator/plugins/sanity/config.d.ts.map +1 -0
  85. package/dist/orchestrator/plugins/sanity/config.js +43 -0
  86. package/dist/orchestrator/plugins/sanity/config.js.map +1 -0
  87. package/dist/orchestrator/plugins/slack/config.d.ts +3 -0
  88. package/dist/orchestrator/plugins/slack/config.d.ts.map +1 -0
  89. package/dist/orchestrator/plugins/slack/config.js +34 -0
  90. package/dist/orchestrator/plugins/slack/config.js.map +1 -0
  91. package/dist/orchestrator/plugins/strapi/config.d.ts +3 -0
  92. package/dist/orchestrator/plugins/strapi/config.d.ts.map +1 -0
  93. package/dist/orchestrator/plugins/strapi/config.js +40 -0
  94. package/dist/orchestrator/plugins/strapi/config.js.map +1 -0
  95. package/dist/orchestrator/plugins/supabase/config.d.ts +3 -0
  96. package/dist/orchestrator/plugins/supabase/config.d.ts.map +1 -0
  97. package/dist/orchestrator/plugins/supabase/config.js +33 -0
  98. package/dist/orchestrator/plugins/supabase/config.js.map +1 -0
  99. package/dist/orchestrator/plugins/teams/config.d.ts +3 -0
  100. package/dist/orchestrator/plugins/teams/config.d.ts.map +1 -0
  101. package/dist/orchestrator/plugins/teams/config.js +43 -0
  102. package/dist/orchestrator/plugins/teams/config.js.map +1 -0
  103. package/dist/orchestrator/plugins/types.d.ts +61 -0
  104. package/dist/orchestrator/plugins/types.d.ts.map +1 -0
  105. package/dist/orchestrator/plugins/types.js +2 -0
  106. package/dist/orchestrator/plugins/types.js.map +1 -0
  107. package/dist/orchestrator/plugins/vercel/config.d.ts +3 -0
  108. package/dist/orchestrator/plugins/vercel/config.d.ts.map +1 -0
  109. package/dist/orchestrator/plugins/vercel/config.js +32 -0
  110. package/dist/orchestrator/plugins/vercel/config.js.map +1 -0
  111. package/package.json +1 -1
  112. package/src/cli/adapters/claude-code.ts +40 -6
  113. package/src/cli/adapters/cursor.ts +46 -6
  114. package/src/cli/adapters/opencode.ts +320 -0
  115. package/src/cli/adapters/vscode.ts +43 -9
  116. package/src/cli/copy.ts +32 -0
  117. package/src/cli/detect.ts +483 -0
  118. package/src/cli/gitignore.ts +0 -3
  119. package/src/cli/init.ts +169 -96
  120. package/src/cli/manifest.ts +2 -1
  121. package/src/cli/mcp.ts +131 -51
  122. package/src/cli/prompt.ts +299 -0
  123. package/src/cli/stack-config.ts +187 -145
  124. package/src/cli/types.ts +60 -9
  125. package/src/cli/update.ts +78 -20
  126. package/src/dashboard/node_modules/.vite/deps/_metadata.json +6 -6
  127. package/src/orchestrator/agent-workflows/README.md +1 -1
  128. package/src/orchestrator/agent-workflows/bug-fix.md +12 -12
  129. package/src/orchestrator/agent-workflows/data-pipeline.md +21 -20
  130. package/src/orchestrator/agent-workflows/database-migration.md +11 -11
  131. package/src/orchestrator/agent-workflows/feature-implementation.md +10 -10
  132. package/src/orchestrator/agent-workflows/performance-optimization.md +6 -6
  133. package/src/orchestrator/agent-workflows/refactoring.md +10 -10
  134. package/src/orchestrator/agent-workflows/schema-changes.md +8 -8
  135. package/src/orchestrator/agent-workflows/security-audit.md +12 -12
  136. package/src/orchestrator/agent-workflows/shared-delivery-phase.md +5 -5
  137. package/src/orchestrator/agents/api-designer.agent.md +2 -2
  138. package/src/orchestrator/agents/architect.agent.md +2 -2
  139. package/src/orchestrator/agents/content-engineer.agent.md +4 -4
  140. package/src/orchestrator/agents/copywriter.agent.md +2 -2
  141. package/src/orchestrator/agents/data-expert.agent.md +6 -6
  142. package/src/orchestrator/agents/database-engineer.agent.md +4 -4
  143. package/src/orchestrator/agents/developer.agent.md +5 -5
  144. package/src/orchestrator/agents/devops-expert.agent.md +5 -5
  145. package/src/orchestrator/agents/documentation-writer.agent.md +1 -1
  146. package/src/orchestrator/agents/performance-expert.agent.md +3 -3
  147. package/src/orchestrator/agents/release-manager.agent.md +4 -4
  148. package/src/orchestrator/agents/researcher.agent.md +19 -3
  149. package/src/orchestrator/agents/reviewer.agent.md +2 -4
  150. package/src/orchestrator/agents/security-expert.agent.md +4 -4
  151. package/src/orchestrator/agents/seo-specialist.agent.md +2 -2
  152. package/src/orchestrator/agents/team-lead.agent.md +97 -101
  153. package/src/orchestrator/agents/testing-expert.agent.md +5 -5
  154. package/src/orchestrator/agents/ui-ux-expert.agent.md +7 -7
  155. package/src/orchestrator/copilot-instructions.md +1 -1
  156. package/src/orchestrator/customizations/AGENT-FAILURES.md +1 -1
  157. package/src/orchestrator/customizations/AGENT-PERFORMANCE.md +12 -12
  158. package/src/orchestrator/customizations/DISPUTES.md +5 -5
  159. package/src/orchestrator/customizations/KNOWN-ISSUES.md +30 -0
  160. package/src/orchestrator/customizations/LESSONS-LEARNED.md +7 -7
  161. package/src/orchestrator/customizations/README.md +5 -2
  162. package/src/orchestrator/customizations/agents/agent-registry.md +1 -1
  163. package/src/orchestrator/customizations/agents/skill-matrix.md +12 -7
  164. package/src/orchestrator/customizations/logs/README.md +1 -1
  165. package/src/orchestrator/customizations/project/decisions.md +31 -0
  166. package/src/orchestrator/customizations/project/docs-structure.md +16 -5
  167. package/src/orchestrator/customizations/project/roadmap.md +24 -0
  168. package/src/orchestrator/customizations/project/tracker-config.md +1 -1
  169. package/src/orchestrator/customizations/stack/cms-config.md +1 -1
  170. package/src/orchestrator/customizations/stack/notifications-config.md +1 -1
  171. package/src/orchestrator/instructions/ai-optimization.instructions.md +2 -2
  172. package/src/orchestrator/instructions/general.instructions.md +102 -40
  173. package/src/orchestrator/{skills/browser-testing → plugins/chrome-devtools}/SKILL.md +1 -1
  174. package/src/orchestrator/plugins/chrome-devtools/config.ts +29 -0
  175. package/src/orchestrator/{skills/contentful-cms → plugins/contentful}/SKILL.md +1 -1
  176. package/src/orchestrator/plugins/contentful/config.ts +49 -0
  177. package/src/orchestrator/{skills/convex-database → plugins/convex}/SKILL.md +1 -1
  178. package/src/orchestrator/plugins/convex/config.ts +33 -0
  179. package/src/orchestrator/plugins/index.ts +85 -0
  180. package/src/orchestrator/{skills/jira-management → plugins/jira}/SKILL.md +3 -3
  181. package/src/orchestrator/plugins/jira/config.ts +30 -0
  182. package/src/orchestrator/{skills/task-management → plugins/linear}/SKILL.md +3 -3
  183. package/src/orchestrator/plugins/linear/config.ts +34 -0
  184. package/src/orchestrator/{skills/nx-workspace → plugins/nx}/SKILL.md +1 -1
  185. package/src/orchestrator/plugins/nx/config.ts +29 -0
  186. package/src/orchestrator/{skills/sanity-cms → plugins/sanity}/SKILL.md +1 -1
  187. package/src/orchestrator/plugins/sanity/config.ts +44 -0
  188. package/src/orchestrator/{skills/slack-notifications → plugins/slack}/SKILL.md +2 -2
  189. package/src/orchestrator/plugins/slack/config.ts +35 -0
  190. package/src/orchestrator/{skills/strapi-cms → plugins/strapi}/SKILL.md +1 -1
  191. package/src/orchestrator/plugins/strapi/config.ts +41 -0
  192. package/src/orchestrator/{skills/supabase-database → plugins/supabase}/SKILL.md +1 -1
  193. package/src/orchestrator/plugins/supabase/config.ts +34 -0
  194. package/src/orchestrator/{skills/teams-notifications → plugins/teams}/SKILL.md +2 -2
  195. package/src/orchestrator/plugins/teams/config.ts +44 -0
  196. package/src/orchestrator/plugins/types.ts +79 -0
  197. package/src/orchestrator/plugins/vercel/config.ts +33 -0
  198. package/src/orchestrator/prompts/bootstrap-customizations.prompt.md +59 -12
  199. package/src/orchestrator/prompts/brainstorm.prompt.md +3 -3
  200. package/src/orchestrator/prompts/bug-fix.prompt.md +18 -18
  201. package/src/orchestrator/prompts/create-skill.prompt.md +50 -32
  202. package/src/orchestrator/prompts/generate-task-spec.prompt.md +3 -3
  203. package/src/orchestrator/prompts/implement-feature.prompt.md +26 -26
  204. package/src/orchestrator/prompts/metrics-report.prompt.md +11 -11
  205. package/src/orchestrator/prompts/quick-refinement.prompt.md +16 -16
  206. package/src/orchestrator/prompts/resolve-pr-comments.prompt.md +2 -2
  207. package/src/orchestrator/skills/accessibility-standards/SKILL.md +1 -1
  208. package/src/orchestrator/skills/agent-hooks/SKILL.md +27 -18
  209. package/src/orchestrator/skills/agent-memory/SKILL.md +7 -7
  210. package/src/orchestrator/skills/api-patterns/SKILL.md +6 -6
  211. package/src/orchestrator/skills/code-commenting/SKILL.md +1 -1
  212. package/src/orchestrator/skills/context-map/SKILL.md +4 -4
  213. package/src/orchestrator/skills/data-engineering/SKILL.md +7 -4
  214. package/src/orchestrator/skills/deployment-infrastructure/SKILL.md +2 -2
  215. package/src/orchestrator/skills/documentation-standards/SKILL.md +1 -1
  216. package/src/orchestrator/skills/fast-review/SKILL.md +3 -3
  217. package/src/orchestrator/skills/frontend-design/SKILL.md +1 -1
  218. package/src/orchestrator/skills/memory-merger/SKILL.md +8 -8
  219. package/src/orchestrator/skills/nextjs-patterns/SKILL.md +1 -1
  220. package/src/orchestrator/skills/panel-majority-vote/SKILL.md +2 -2
  221. package/src/orchestrator/skills/panel-majority-vote/panel-report.template.md +1 -1
  222. package/src/orchestrator/skills/performance-optimization/SKILL.md +1 -1
  223. package/src/orchestrator/skills/react-development/SKILL.md +3 -3
  224. package/src/orchestrator/skills/security-hardening/SKILL.md +27 -27
  225. package/src/orchestrator/skills/self-improvement/SKILL.md +14 -13
  226. package/src/orchestrator/skills/seo-patterns/SKILL.md +1 -1
  227. package/src/orchestrator/skills/session-checkpoints/SKILL.md +19 -19
  228. package/src/orchestrator/skills/team-lead-reference/SKILL.md +9 -9
  229. package/src/orchestrator/skills/testing-workflow/SKILL.md +13 -13
  230. package/src/orchestrator/skills/validation-gates/SKILL.md +8 -15
  231. package/src/orchestrator/mcp.json +0 -61
package/src/cli/init.ts CHANGED
@@ -1,18 +1,30 @@
1
1
  import { resolve } from 'node:path'
2
2
  import { readFile, unlink } from 'node:fs/promises'
3
3
  import { existsSync } from 'node:fs'
4
- import { select, confirm, closePrompts } from './prompt.js'
4
+ import { multiselect, confirm, closePrompts, c } from './prompt.js'
5
5
  import { readManifest, writeManifest, createManifest } from './manifest.js'
6
6
  import { removeDirIfExists } from './copy.js'
7
7
  import { updateGitignore } from './gitignore.js'
8
8
  import { getRequiredMcpEnvVars } from './stack-config.js'
9
- import type { CliContext, IdeAdapter, CmsChoice, DbChoice, PmChoice, NotifChoice, StackConfig } from './types.js'
9
+ import { TECH_PLUGINS, TEAM_PLUGINS } from '../orchestrator/plugins/index.js'
10
+ import { detectRepoInfo, mergeStackIntoRepoInfo, formatRepoInfo } from './detect.js'
11
+ import type { CliContext, IdeAdapter, IdeChoice, TechTool, TeamTool, StackConfig } from './types.js'
10
12
 
11
13
  const ADAPTERS: Record<string, () => Promise<IdeAdapter>> = {
12
14
  vscode: () => import('./adapters/vscode.js') as Promise<IdeAdapter>,
13
15
  cursor: () => import('./adapters/cursor.js') as Promise<IdeAdapter>,
14
16
  'claude-code': () =>
15
17
  import('./adapters/claude-code.js') as Promise<IdeAdapter>,
18
+ opencode: () =>
19
+ import('./adapters/opencode.js') as Promise<IdeAdapter>,
20
+ }
21
+
22
+ /** IDE display labels */
23
+ const IDE_DISPLAY: Record<IdeChoice, string> = {
24
+ vscode: 'VS Code',
25
+ cursor: 'Cursor',
26
+ 'claude-code': 'Claude Code',
27
+ opencode: 'OpenCode',
16
28
  }
17
29
 
18
30
  export default async function init({ pkgRoot, args }: CliContext): Promise<void> {
@@ -24,7 +36,7 @@ export default async function init({ pkgRoot, args }: CliContext): Promise<void>
24
36
  let isReinit = false
25
37
  if (existing) {
26
38
  const proceed = await confirm(
27
- `OpenCastle already installed (v${existing.version}, ${existing.ide}). Re-initialize?`,
39
+ `OpenCastle already installed (v${existing.version}). Re-initialize?`,
28
40
  false
29
41
  )
30
42
  if (!proceed) {
@@ -38,78 +50,117 @@ export default async function init({ pkgRoot, args }: CliContext): Promise<void>
38
50
  await readFile(resolve(pkgRoot, 'package.json'), 'utf8')
39
51
  ) as { version: string }
40
52
 
41
- console.log(`\n 🏰 OpenCastle v${pkg.version}`)
53
+ console.log(`\n 🏰 ${c.bold('OpenCastle')} ${c.dim(`v${pkg.version}`)}`)
42
54
  console.log(
43
- ' Multi-agent orchestration framework for AI coding assistants\n'
55
+ ` ${c.dim('Multi-agent orchestration framework for AI coding assistants')}\n`
44
56
  )
45
57
 
46
- // ── IDE selection ───────────────────────────────────────────────
47
- const ide = await select('Which IDE are you using?', [
48
- {
49
- label: 'VS Code',
50
- hint: 'GitHub Copilot — .github/ agents, instructions, skills',
51
- value: 'vscode',
52
- },
53
- {
54
- label: 'Cursor',
55
- hint: '.cursorrules & .cursor/rules/*.mdc',
56
- value: 'cursor',
57
- },
58
- {
59
- label: 'Claude Code',
60
- hint: 'CLAUDE.md & .claude/ commands, skills',
61
- value: 'claude-code',
62
- },
63
- ])
58
+ // ── Repo research ───────────────────────────────────────────────
59
+ console.log(` ${c.dim('Scanning repository...')}`)
60
+ const repoInfo = await detectRepoInfo(projectRoot)
61
+ const summary = formatRepoInfo(repoInfo)
62
+ if (summary) {
63
+ console.log(` ${c.green('Detected:')}\n` + summary + '\n')
64
+ } else {
65
+ console.log(` ${c.dim('No tooling detected (empty project?)')}\n`)
66
+ }
64
67
 
65
- // ── CMS selection ───────────────────────────────────────────────
66
- const cms = await select('Which CMS are you using?', [
67
- { label: 'Sanity', hint: 'GROQ queries, real-time collaboration', value: 'sanity' },
68
- { label: 'Contentful', hint: 'GraphQL / REST API, structured content', value: 'contentful' },
69
- { label: 'Strapi', hint: 'Open-source headless CMS', value: 'strapi' },
70
- { label: 'None', hint: 'No CMS — skip CMS skills and agents', value: 'none' },
71
- ])
68
+ // ── IDEs (multiselect, at least 1) ─────────────────────────────
69
+ console.log(` ${c.bold('── IDEs ──────────────────────────────────────')}`)
70
+ let ides: string[] = []
71
+ while (ides.length === 0) {
72
+ ides = await multiselect('Which IDEs do you use?', [
73
+ {
74
+ label: 'VS Code',
75
+ hint: 'GitHub Copilot agents, instructions, skills',
76
+ value: 'vscode',
77
+ },
78
+ {
79
+ label: 'Cursor',
80
+ hint: '.cursorrules & .cursor/rules/*.mdc',
81
+ value: 'cursor',
82
+ },
83
+ {
84
+ label: 'Claude Code',
85
+ hint: 'CLAUDE.md & .claude/ commands, skills',
86
+ value: 'claude-code',
87
+ },
88
+ {
89
+ label: 'OpenCode',
90
+ hint: 'AGENTS.md & opencode.json',
91
+ value: 'opencode',
92
+ },
93
+ ])
94
+ if (ides.length === 0) {
95
+ console.log(` ${c.yellow('Please select at least one IDE.')}`)
96
+ }
97
+ }
72
98
 
73
- // ── Database selection ──────────────────────────────────────────
74
- const db = await select('Which database are you using?', [
75
- { label: 'Supabase', hint: 'Postgres + Auth + RLS + Edge Functions', value: 'supabase' },
76
- { label: 'Convex', hint: 'Reactive backend with real-time sync', value: 'convex' },
77
- { label: 'None', hint: 'No database — skip DB skills and agents', value: 'none' },
99
+ // ── Tech Tools (multiselect, 0-N) ──────────────────────────────
100
+ // Pre-select tools already detected in the repo
101
+ const detectedTools = new Set([
102
+ ...(repoInfo.cms ?? []),
103
+ ...(repoInfo.databases ?? []),
104
+ ...(repoInfo.deployment ?? []),
105
+ ...(repoInfo.monorepo ? [repoInfo.monorepo] : []),
78
106
  ])
79
107
 
80
- // ── Project management selection ────────────────────────────────
81
- const pm = await select('Which project management tool are you using?', [
82
- { label: 'Linear', hint: 'Issue tracking with MCP integration', value: 'linear' },
83
- { label: 'Jira', hint: 'Atlassian issue tracking via Rovo MCP', value: 'jira' },
84
- { label: 'None', hint: 'No project management — skip PM skills', value: 'none' },
85
- ])
108
+ console.log(` ${c.bold('── Tech Tools ────────────────────────────────')}`)
109
+ const techTools = await multiselect('Which tools does your project use?',
110
+ TECH_PLUGINS.map((p) => ({
111
+ label: p.label,
112
+ hint: p.hint,
113
+ value: p.id,
114
+ ...((p.preselected || detectedTools.has(p.id)) && { selected: true }),
115
+ }))
116
+ )
86
117
 
87
- // ── Notifications selection ────────────────────────────────────
88
- const notifications = await select('Which notifications tool are you using?', [
89
- { label: 'Slack', hint: 'Agent notifications and bi-directional communication', value: 'slack' },
90
- { label: 'Microsoft Teams', hint: 'Agent notifications via Teams channels', value: 'teams' },
91
- { label: 'None', hint: 'No notifications — skip messaging skills', value: 'none' },
92
- ])
118
+ // ── Team Tools (multiselect, 0-N) ──────────────────────────────
119
+ console.log(` ${c.bold('── Team Tools ────────────────────────────────')}`)
120
+ const teamTools = await multiselect('Which team tools do you use?',
121
+ TEAM_PLUGINS.map((p) => ({
122
+ label: p.label,
123
+ hint: p.hint,
124
+ value: p.id,
125
+ ...(p.preselected && { selected: true }),
126
+ }))
127
+ )
93
128
 
94
- const stack: StackConfig = { cms: cms as CmsChoice, db: db as DbChoice, pm: pm as PmChoice, notifications: notifications as NotifChoice }
129
+ const stack: StackConfig = {
130
+ ides: ides as IdeChoice[],
131
+ techTools: techTools as TechTool[],
132
+ teamTools: teamTools as TeamTool[],
133
+ }
95
134
 
96
- console.log(`\n Installing for ${ide}...`)
97
- console.log(` Stack: CMS=${stack.cms}, DB=${stack.db}, PM=${stack.pm}, Notifications=${stack.notifications}\n`)
135
+ // ── Merge user choices into detected info ────────────────────
136
+ const combinedRepoInfo = mergeStackIntoRepoInfo(repoInfo, stack)
137
+
138
+ const ideNames = ides.map((id) => IDE_DISPLAY[id as IdeChoice]).join(', ')
139
+ console.log(`\n Installing for ${c.cyan(ideNames)}...`)
140
+ if (techTools.length > 0) {
141
+ console.log(` Tech: ${c.green(techTools.join(', '))}`)
142
+ }
143
+ if (teamTools.length > 0) {
144
+ console.log(` Team: ${c.green(teamTools.join(', '))}`)
145
+ }
146
+ console.log()
98
147
 
99
148
  // ── Dry run ─────────────────────────────────────────────────────
100
149
  if (dryRun) {
101
- const adapter = await ADAPTERS[ide]()
102
- const managed = adapter.getManagedPaths()
103
- console.log(' [dry-run] Files that would be created:\n')
104
- for (const p of managed.framework) {
105
- console.log(` + ${p}`)
106
- }
107
- for (const p of managed.customizable) {
108
- console.log(` + ${p}`)
150
+ for (const ide of ides) {
151
+ const adapter = await ADAPTERS[ide]()
152
+ const managed = adapter.getManagedPaths()
153
+ console.log(` ${c.dim(`[dry-run] ${IDE_DISPLAY[ide as IdeChoice]} files:`)}\n`)
154
+ for (const p of managed.framework) {
155
+ console.log(` ${c.green('+')} ${p}`)
156
+ }
157
+ for (const p of managed.customizable) {
158
+ console.log(` ${c.green('+')} ${p}`)
159
+ }
109
160
  }
110
- console.log(` + .opencastle.json`)
111
- console.log(` + .gitignore (OpenCastle entries)`)
112
- console.log('\n No files were written.\n')
161
+ console.log(` ${c.green('+')} .opencastle.json`)
162
+ console.log(` ${c.green('+')} .gitignore (OpenCastle entries)`)
163
+ console.log(`\n ${c.dim('No files were written.')}\n`)
113
164
  closePrompts()
114
165
  return
115
166
  }
@@ -125,11 +176,12 @@ export default async function init({ pkgRoot, args }: CliContext): Promise<void>
125
176
  await unlink(fullPath)
126
177
  }
127
178
  }
128
- // Remove MCP config so it gets regenerated with new stack
179
+ // Remove MCP configs so they get regenerated with new stack
129
180
  const mcpCandidates = [
130
181
  '.vscode/mcp.json',
131
182
  '.cursor/mcp.json',
132
183
  '.claude/mcp.json',
184
+ 'opencode.json',
133
185
  ]
134
186
  for (const mcpPath of mcpCandidates) {
135
187
  const fullPath = resolve(projectRoot, mcpPath)
@@ -139,69 +191,90 @@ export default async function init({ pkgRoot, args }: CliContext): Promise<void>
139
191
  }
140
192
  }
141
193
 
142
- // ── Run adapter ─────────────────────────────────────────────────
143
- const adapter = await ADAPTERS[ide]()
144
- const results = await adapter.install(pkgRoot, projectRoot, stack)
194
+ // ── Run adapters for each selected IDE ──────────────────────────
195
+ let totalCreated = 0
196
+ let totalSkipped = 0
197
+ const allManagedPaths = { framework: [] as string[], customizable: [] as string[] }
198
+
199
+ for (const ide of ides) {
200
+ const adapter = await ADAPTERS[ide]()
201
+ const results = await adapter.install(pkgRoot, projectRoot, stack, combinedRepoInfo)
202
+ totalCreated += results.created.length
203
+ totalSkipped += results.skipped.length
204
+
205
+ const managed = adapter.getManagedPaths()
206
+ allManagedPaths.framework.push(...managed.framework)
207
+ allManagedPaths.customizable.push(...managed.customizable)
208
+ }
145
209
 
146
210
  // ── Write manifest ──────────────────────────────────────────────
147
- const manifest = createManifest(pkg.version, ide)
148
- manifest.managedPaths = adapter.getManagedPaths()
211
+ const manifest = createManifest(pkg.version, ides[0], ides)
212
+ manifest.managedPaths = allManagedPaths
149
213
  manifest.stack = stack
214
+ manifest.repoInfo = combinedRepoInfo
150
215
  await writeManifest(projectRoot, manifest)
151
216
 
152
217
  // ── Update .gitignore ───────────────────────────────────────────
153
- const managedPaths = adapter.getManagedPaths()
154
- const gitignoreResult = await updateGitignore(projectRoot, managedPaths)
218
+ const gitignoreResult = await updateGitignore(projectRoot, allManagedPaths)
155
219
 
156
220
  // ── Summary ─────────────────────────────────────────────────────
157
- const created = results.created.length
158
- const skipped = results.skipped.length
159
-
160
- console.log(` ✓ Created ${created} files`)
221
+ console.log(` ${c.green('✓')} Created ${c.bold(String(totalCreated))} files`)
161
222
  if (gitignoreResult === 'created') {
162
- console.log(' ✓ Created .gitignore with OpenCastle entries')
223
+ console.log(` ${c.green('✓')} Created .gitignore with OpenCastle entries`)
163
224
  } else if (gitignoreResult === 'updated') {
164
- console.log(' ✓ Updated .gitignore with OpenCastle entries')
225
+ console.log(` ${c.green('✓')} Updated .gitignore with OpenCastle entries`)
165
226
  }
166
- if (skipped > 0) {
167
- console.log(` → Skipped ${skipped} existing files`)
227
+ if (totalSkipped > 0) {
228
+ console.log(` ${c.dim('')} Skipped ${totalSkipped} existing files`)
168
229
  }
169
230
 
170
231
  // ── Env var notice ──────────────────────────────────────────────
171
- const envVars = getRequiredMcpEnvVars(stack)
232
+ const envVars = getRequiredMcpEnvVars(stack, combinedRepoInfo)
172
233
  if (envVars.length > 0) {
173
- console.log(`\n ⚠ Required environment variables for MCP servers:\n`)
234
+ console.log(`\n ${c.yellow('')} Required environment variables for MCP servers:\n`)
174
235
  for (const { envVar, hint } of envVars) {
175
- console.log(` ${envVar}`)
176
- console.log(` └ ${hint}\n`)
236
+ console.log(` ${c.bold(envVar)}`)
237
+ console.log(` ${c.dim('')} ${c.dim(hint)}\n`)
177
238
  }
178
239
  }
179
240
 
180
241
  // ── OAuth setup guides ────────────────────────────────────────
181
- if (stack.notifications === 'slack') {
182
- console.log(` 📖 Slack MCP requires a Slack App with a bot token.`)
183
- console.log(` Setup guide: https://www.opencastle.dev/guides/slack-mcp-setup\n`)
242
+ if (teamTools.includes('slack')) {
243
+ console.log(` ${c.cyan('📖')} Slack MCP requires a Slack App with a bot token.`)
244
+ console.log(` Setup guide: ${c.cyan('https://www.opencastle.dev/guides/plugins#slack')}\n`)
184
245
  }
185
246
 
186
- console.log(`\n Next steps:`)
187
- if (ide === 'vscode') {
188
- console.log(
189
- ' 0. Reload VS Code window (Cmd+Shift+P "Reload Window") to pick up agents'
190
- )
191
- } else if (ide === 'cursor') {
192
- console.log(
193
- ' 0. Reload Cursor window to pick up the new rule files'
194
- )
247
+ console.log(`\n ${c.bold('Next steps:')}`)
248
+
249
+ let step = 0
250
+ // Reload window messages for relevant IDEs
251
+ const needsReload = ides.filter((id) => ['vscode', 'cursor'].includes(id))
252
+ if (needsReload.length > 0) {
253
+ step++
254
+ if (needsReload.includes('vscode')) {
255
+ console.log(
256
+ ` ${step}. ${c.yellow('Reload VS Code window')} (Cmd+Shift+P → "Developer: Reload Window")`
257
+ )
258
+ }
259
+ if (needsReload.includes('cursor')) {
260
+ console.log(
261
+ ` ${step}. ${c.yellow('Reload Cursor window')} to pick up the new rule files`
262
+ )
263
+ }
195
264
  }
265
+
196
266
  if (envVars.length > 0) {
267
+ step++
197
268
  console.log(
198
- ` 1. Set the environment variable${envVars.length > 1 ? 's' : ''} listed above`
269
+ ` ${step}. Set the environment variable${envVars.length > 1 ? 's' : ''} listed above`
199
270
  )
200
271
  }
272
+ step++
201
273
  console.log(
202
- ` ${envVars.length > 0 ? '2' : '1'}. Run the "Bootstrap Customizations" prompt to configure for your project`
274
+ ` ${step}. Run the ${c.cyan('"Bootstrap Customizations"')} prompt to configure for your project`
203
275
  )
204
- console.log(` ${envVars.length > 0 ? '3' : '2'}. Commit the customizations/ folder to your repository`)
276
+ step++
277
+ console.log(` ${step}. Commit the customizations/ folder to your repository`)
205
278
  console.log()
206
279
 
207
280
  closePrompts()
@@ -35,10 +35,11 @@ export async function writeManifest(
35
35
  /**
36
36
  * Create a fresh manifest object.
37
37
  */
38
- export function createManifest(version: string, ide: string): Manifest {
38
+ export function createManifest(version: string, ide: string, ides?: string[]): Manifest {
39
39
  return {
40
40
  version,
41
41
  ide,
42
+ ides: ides ?? [ide],
42
43
  installedAt: new Date().toISOString(),
43
44
  updatedAt: new Date().toISOString(),
44
45
  };
package/src/cli/mcp.ts CHANGED
@@ -1,91 +1,171 @@
1
1
  import { resolve, dirname } from 'node:path';
2
2
  import { mkdir, readFile, writeFile } from 'node:fs/promises';
3
3
  import { existsSync } from 'node:fs';
4
- import { getOrchestratorRoot } from './copy.js';
5
4
  import { getIncludedMcpServers } from './stack-config.js';
6
- import type { ScaffoldResult, StackConfig } from './types.js';
5
+ import { PLUGINS } from '../orchestrator/plugins/index.js';
6
+ import type { McpInput } from '../orchestrator/plugins/types.js';
7
+ import type { ScaffoldResult, StackConfig, RepoInfo, IdeChoice } from './types.js';
8
+
9
+ // ── IDE-specific MCP format transformation ────────────────────
10
+
11
+ interface VsCodeServer {
12
+ type: 'stdio' | 'http';
13
+ command?: string;
14
+ args?: string[];
15
+ url?: string;
16
+ env?: Record<string, string>;
17
+ envFile?: string;
18
+ }
19
+
20
+ /**
21
+ * Transform a VS Code–format MCP config into the format
22
+ * expected by the given IDE.
23
+ */
24
+ function transformMcpForIde(
25
+ ide: IdeChoice,
26
+ servers: Record<string, VsCodeServer>,
27
+ inputs?: McpInput[]
28
+ ): Record<string, unknown> {
29
+ switch (ide) {
30
+ case 'cursor':
31
+ case 'claude-code': {
32
+ // mcpServers format — no 'type' field
33
+ const mcpServers: Record<string, unknown> = {};
34
+ for (const [name, server] of Object.entries(servers)) {
35
+ if (server.type === 'stdio') {
36
+ mcpServers[name] = {
37
+ command: server.command,
38
+ args: server.args,
39
+ ...(server.env && { env: server.env }),
40
+ };
41
+ } else if (server.type === 'http') {
42
+ // Strip VS Code ${input:...} placeholders for non-VS Code IDEs
43
+ let url = server.url ?? '';
44
+ url = url.replace(/\$\{input:\w+\}/g, 'REPLACE_ME');
45
+ mcpServers[name] = { url };
46
+ }
47
+ }
48
+ return { mcpServers };
49
+ }
50
+
51
+ case 'opencode': {
52
+ // OpenCode format — type: "local"/"remote", command as array
53
+ const mcp: Record<string, unknown> = {};
54
+ for (const [name, server] of Object.entries(servers)) {
55
+ if (server.type === 'stdio') {
56
+ mcp[name] = {
57
+ type: 'local',
58
+ command: [server.command, ...(server.args ?? [])],
59
+ ...(server.env && { environment: server.env }),
60
+ };
61
+ } else if (server.type === 'http') {
62
+ let url = server.url ?? '';
63
+ url = url.replace(/\$\{input:\w+\}/g, 'REPLACE_ME');
64
+ mcp[name] = {
65
+ type: 'remote',
66
+ url,
67
+ };
68
+ }
69
+ }
70
+ return { mcp };
71
+ }
72
+
73
+ default: {
74
+ // VS Code — return as-is (keep type, inputs, envFile)
75
+ const result: Record<string, unknown> = { servers };
76
+ if (inputs && inputs.length > 0) {
77
+ result.inputs = inputs;
78
+ }
79
+ return result;
80
+ }
81
+ }
82
+ }
7
83
 
8
84
  /**
9
85
  * Scaffold or merge the MCP server config into the target project.
10
86
  *
11
- * Reads the template from `opencastle/src/orchestrator/mcp.json`,
12
- * writes it to `<projectRoot>/<destRelPath>` (e.g. `.vscode/mcp.json`).
87
+ * Builds the server list from plugin configs based on the user's
88
+ * stack selection. Writes to `<projectRoot>/<destRelPath>`
89
+ * (e.g. `.vscode/mcp.json`).
13
90
  *
14
- * When a StackConfig is provided, only servers relevant to the chosen
15
- * CMS/DB stack (plus core servers) are included.
91
+ * The output format is adapted to match the target IDE's expectations.
16
92
  *
17
93
  * If the file already exists, missing servers are merged in without
18
94
  * overwriting any existing server configs.
19
95
  */
20
96
  export async function scaffoldMcpConfig(
21
- pkgRoot: string,
22
97
  projectRoot: string,
23
98
  destRelPath: string,
24
- stack?: StackConfig
99
+ stack?: StackConfig,
100
+ repoInfo?: RepoInfo,
101
+ ide?: IdeChoice
25
102
  ): Promise<ScaffoldResult> {
26
103
  const destPath = resolve(projectRoot, destRelPath);
27
104
 
28
- const srcRoot = getOrchestratorRoot(pkgRoot);
29
- const templatePath = resolve(srcRoot, 'mcp.json');
30
- const content = await readFile(templatePath, 'utf8');
31
-
32
- const template = JSON.parse(content) as {
33
- servers: Record<string, unknown>;
34
- inputs?: Array<{ id: string; [key: string]: unknown }>;
35
- };
105
+ // Build server list from plugin configs
106
+ const servers: Record<string, VsCodeServer> = {};
107
+ let inputs: McpInput[] = [];
36
108
 
37
- // Filter servers based on stack config
38
109
  if (stack) {
39
- const included = getIncludedMcpServers(stack);
40
- template.servers = Object.fromEntries(
41
- Object.entries(template.servers).filter(([key]) => included.has(key))
42
- );
43
-
44
- // Filter inputs to only include those referenced by included servers
45
- if (template.inputs) {
46
- const serverJson = JSON.stringify(template.servers);
47
- template.inputs = template.inputs.filter(
48
- (input) => serverJson.includes(`\${input:${input.id}}`)
49
- );
50
- if (template.inputs.length === 0) {
51
- delete template.inputs;
110
+ const included = getIncludedMcpServers(stack, repoInfo);
111
+
112
+ for (const plugin of Object.values(PLUGINS)) {
113
+ if (plugin.mcpServerKey && included.has(plugin.mcpServerKey)) {
114
+ servers[plugin.mcpServerKey] = plugin.mcpConfig as VsCodeServer;
115
+ if (plugin.mcpInputs) {
116
+ inputs.push(...plugin.mcpInputs);
117
+ }
52
118
  }
53
119
  }
54
120
  }
55
121
 
122
+ // Transform to IDE-specific format
123
+ const resolvedIde = ide ?? 'vscode';
124
+ const output = transformMcpForIde(resolvedIde, servers, inputs.length > 0 ? inputs : undefined);
125
+
56
126
  if (existsSync(destPath)) {
57
127
  // Merge: add missing servers without overwriting existing ones
58
128
  const existingContent = await readFile(destPath, 'utf8');
59
- const existing = JSON.parse(existingContent) as {
60
- servers?: Record<string, unknown>;
61
- inputs?: Array<{ id: string; [key: string]: unknown }>;
62
- [key: string]: unknown;
63
- };
64
-
65
- if (!existing.servers) {
66
- existing.servers = {};
129
+ const existing = JSON.parse(existingContent) as Record<string, unknown>;
130
+
131
+ // Determine the server container key for this IDE
132
+ const containerKey = resolvedIde === 'opencode'
133
+ ? 'mcp'
134
+ : resolvedIde === 'vscode'
135
+ ? 'servers'
136
+ : 'mcpServers';
137
+
138
+ if (!existing[containerKey]) {
139
+ existing[containerKey] = {};
67
140
  }
68
141
 
142
+ const existingServers = existing[containerKey] as Record<string, unknown>;
143
+ const newServers = (output as Record<string, unknown>)[containerKey] as Record<string, unknown> | undefined;
144
+
69
145
  let added = 0;
70
- for (const [key, value] of Object.entries(template.servers)) {
71
- if (!(key in existing.servers)) {
72
- existing.servers[key] = value;
73
- added++;
146
+ if (newServers) {
147
+ for (const [key, value] of Object.entries(newServers)) {
148
+ if (!(key in existingServers)) {
149
+ existingServers[key] = value;
150
+ added++;
151
+ }
74
152
  }
75
153
  }
76
154
 
77
- // Merge inputs: add missing input definitions
78
- if (template.inputs && template.inputs.length > 0) {
79
- if (!existing.inputs) {
80
- existing.inputs = [];
81
- }
82
- const existingIds = new Set(existing.inputs.map((i) => i.id));
83
- for (const input of template.inputs) {
155
+ // For VS Code: merge inputs
156
+ if (resolvedIde === 'vscode' && output.inputs) {
157
+ const existingInputs = (existing.inputs as McpInput[]) ?? [];
158
+ const existingIds = new Set(existingInputs.map((i) => i.id));
159
+ const newInputs = output.inputs as McpInput[];
160
+ for (const input of newInputs) {
84
161
  if (!existingIds.has(input.id)) {
85
- existing.inputs.push(input);
162
+ existingInputs.push(input);
86
163
  added++;
87
164
  }
88
165
  }
166
+ if (existingInputs.length > 0) {
167
+ existing.inputs = existingInputs;
168
+ }
89
169
  }
90
170
 
91
171
  if (added === 0) {
@@ -97,7 +177,7 @@ export async function scaffoldMcpConfig(
97
177
  }
98
178
 
99
179
  await mkdir(dirname(destPath), { recursive: true });
100
- await writeFile(destPath, JSON.stringify(template, null, 2) + '\n');
180
+ await writeFile(destPath, JSON.stringify(output, null, 2) + '\n');
101
181
 
102
182
  return { path: destPath, action: 'created' };
103
183
  }