opencastle 0.1.0 → 0.3.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 (132) hide show
  1. package/README.md +16 -7
  2. package/bin/cli.mjs +2 -3
  3. package/dist/cli/adapters/claude-code.d.ts.map +1 -1
  4. package/dist/cli/adapters/claude-code.js +7 -3
  5. package/dist/cli/adapters/claude-code.js.map +1 -1
  6. package/dist/cli/adapters/cursor.d.ts.map +1 -1
  7. package/dist/cli/adapters/cursor.js +27 -9
  8. package/dist/cli/adapters/cursor.js.map +1 -1
  9. package/dist/cli/dashboard.d.ts.map +1 -1
  10. package/dist/cli/dashboard.js +7 -4
  11. package/dist/cli/dashboard.js.map +1 -1
  12. package/dist/cli/eject.d.ts +1 -1
  13. package/dist/cli/eject.d.ts.map +1 -1
  14. package/dist/cli/eject.js +6 -1
  15. package/dist/cli/eject.js.map +1 -1
  16. package/dist/cli/gitignore.d.ts +11 -0
  17. package/dist/cli/gitignore.d.ts.map +1 -0
  18. package/dist/cli/gitignore.js +61 -0
  19. package/dist/cli/gitignore.js.map +1 -0
  20. package/dist/cli/init.d.ts +1 -1
  21. package/dist/cli/init.d.ts.map +1 -1
  22. package/dist/cli/init.js +81 -5
  23. package/dist/cli/init.js.map +1 -1
  24. package/dist/cli/mcp.d.ts +3 -2
  25. package/dist/cli/mcp.d.ts.map +1 -1
  26. package/dist/cli/mcp.js +23 -5
  27. package/dist/cli/mcp.js.map +1 -1
  28. package/dist/cli/run/schema.d.ts.map +1 -1
  29. package/dist/cli/run/schema.js +28 -1
  30. package/dist/cli/run/schema.js.map +1 -1
  31. package/dist/cli/run.d.ts.map +1 -1
  32. package/dist/cli/run.js +16 -0
  33. package/dist/cli/run.js.map +1 -1
  34. package/dist/cli/stack-config.d.ts +13 -0
  35. package/dist/cli/stack-config.d.ts.map +1 -1
  36. package/dist/cli/stack-config.js +20 -0
  37. package/dist/cli/stack-config.js.map +1 -1
  38. package/dist/cli/update.d.ts.map +1 -1
  39. package/dist/cli/update.js +16 -3
  40. package/dist/cli/update.js.map +1 -1
  41. package/package.json +1 -1
  42. package/src/cli/adapters/claude-code.ts +7 -5
  43. package/src/cli/adapters/cursor.ts +28 -13
  44. package/src/cli/dashboard.ts +9 -4
  45. package/src/cli/eject.ts +7 -1
  46. package/src/cli/gitignore.ts +77 -0
  47. package/src/cli/init.ts +88 -5
  48. package/src/cli/mcp.ts +31 -6
  49. package/src/cli/run/schema.ts +26 -1
  50. package/src/cli/run.ts +4 -0
  51. package/src/cli/stack-config.ts +33 -0
  52. package/src/cli/update.ts +18 -3
  53. package/src/orchestrator/agent-workflows/README.md +2 -0
  54. package/src/orchestrator/agent-workflows/bug-fix.md +2 -0
  55. package/src/orchestrator/agent-workflows/data-pipeline.md +2 -0
  56. package/src/orchestrator/agent-workflows/database-migration.md +2 -0
  57. package/src/orchestrator/agent-workflows/feature-implementation.md +2 -0
  58. package/src/orchestrator/agent-workflows/performance-optimization.md +2 -0
  59. package/src/orchestrator/agent-workflows/refactoring.md +2 -0
  60. package/src/orchestrator/agent-workflows/schema-changes.md +2 -0
  61. package/src/orchestrator/agent-workflows/security-audit.md +2 -0
  62. package/src/orchestrator/agent-workflows/shared-delivery-phase.md +2 -0
  63. package/src/orchestrator/agents/api-designer.agent.md +2 -0
  64. package/src/orchestrator/agents/architect.agent.md +2 -0
  65. package/src/orchestrator/agents/content-engineer.agent.md +2 -0
  66. package/src/orchestrator/agents/copywriter.agent.md +2 -0
  67. package/src/orchestrator/agents/data-expert.agent.md +2 -0
  68. package/src/orchestrator/agents/database-engineer.agent.md +2 -0
  69. package/src/orchestrator/agents/developer.agent.md +2 -0
  70. package/src/orchestrator/agents/devops-expert.agent.md +2 -0
  71. package/src/orchestrator/agents/documentation-writer.agent.md +2 -0
  72. package/src/orchestrator/agents/performance-expert.agent.md +2 -0
  73. package/src/orchestrator/agents/release-manager.agent.md +2 -0
  74. package/src/orchestrator/agents/researcher.agent.md +2 -0
  75. package/src/orchestrator/agents/reviewer.agent.md +2 -0
  76. package/src/orchestrator/agents/security-expert.agent.md +2 -0
  77. package/src/orchestrator/agents/seo-specialist.agent.md +2 -0
  78. package/src/orchestrator/agents/team-lead.agent.md +2 -0
  79. package/src/orchestrator/agents/testing-expert.agent.md +3 -1
  80. package/src/orchestrator/agents/ui-ux-expert.agent.md +5 -3
  81. package/src/orchestrator/copilot-instructions.md +2 -0
  82. package/src/orchestrator/instructions/ai-optimization.instructions.md +2 -0
  83. package/src/orchestrator/instructions/general.instructions.md +2 -0
  84. package/src/orchestrator/prompts/bootstrap-customizations.prompt.md +2 -0
  85. package/src/orchestrator/prompts/brainstorm.prompt.md +2 -0
  86. package/src/orchestrator/prompts/bug-fix.prompt.md +2 -0
  87. package/src/orchestrator/prompts/create-skill.prompt.md +2 -0
  88. package/src/orchestrator/prompts/generate-task-spec.prompt.md +2 -0
  89. package/src/orchestrator/prompts/implement-feature.prompt.md +2 -0
  90. package/src/orchestrator/prompts/metrics-report.prompt.md +2 -0
  91. package/src/orchestrator/prompts/quick-refinement.prompt.md +2 -0
  92. package/src/orchestrator/prompts/resolve-pr-comments.prompt.md +2 -0
  93. package/src/orchestrator/skills/accessibility-standards/SKILL.md +2 -0
  94. package/src/orchestrator/skills/agent-hooks/SKILL.md +2 -0
  95. package/src/orchestrator/skills/agent-memory/SKILL.md +2 -0
  96. package/src/orchestrator/skills/api-patterns/SKILL.md +2 -0
  97. package/src/orchestrator/skills/browser-testing/SKILL.md +14 -26
  98. package/src/orchestrator/skills/code-commenting/SKILL.md +2 -0
  99. package/src/orchestrator/skills/contentful-cms/SKILL.md +2 -0
  100. package/src/orchestrator/skills/context-map/SKILL.md +2 -0
  101. package/src/orchestrator/skills/convex-database/SKILL.md +2 -0
  102. package/src/orchestrator/skills/data-engineering/SKILL.md +2 -0
  103. package/src/orchestrator/skills/deployment-infrastructure/SKILL.md +2 -0
  104. package/src/orchestrator/skills/documentation-standards/SKILL.md +2 -0
  105. package/src/orchestrator/skills/fast-review/SKILL.md +2 -0
  106. package/src/orchestrator/skills/frontend-design/SKILL.md +2 -0
  107. package/src/orchestrator/skills/jira-management/SKILL.md +2 -0
  108. package/src/orchestrator/skills/memory-merger/SKILL.md +2 -0
  109. package/src/orchestrator/skills/nextjs-patterns/SKILL.md +2 -0
  110. package/src/orchestrator/skills/nx-workspace/SKILL.md +2 -0
  111. package/src/orchestrator/skills/panel-majority-vote/SKILL.md +2 -0
  112. package/src/orchestrator/skills/panel-majority-vote/panel-report.template.md +2 -0
  113. package/src/orchestrator/skills/performance-optimization/SKILL.md +2 -0
  114. package/src/orchestrator/skills/react-development/SKILL.md +2 -0
  115. package/src/orchestrator/skills/sanity-cms/SKILL.md +2 -0
  116. package/src/orchestrator/skills/security-hardening/SKILL.md +2 -0
  117. package/src/orchestrator/skills/self-improvement/SKILL.md +2 -0
  118. package/src/orchestrator/skills/seo-patterns/SKILL.md +2 -0
  119. package/src/orchestrator/skills/session-checkpoints/SKILL.md +2 -0
  120. package/src/orchestrator/skills/slack-notifications/SKILL.md +2 -0
  121. package/src/orchestrator/skills/strapi-cms/SKILL.md +2 -0
  122. package/src/orchestrator/skills/supabase-database/SKILL.md +2 -0
  123. package/src/orchestrator/skills/task-management/SKILL.md +2 -0
  124. package/src/orchestrator/skills/team-lead-reference/SKILL.md +2 -0
  125. package/src/orchestrator/skills/teams-notifications/SKILL.md +2 -0
  126. package/src/orchestrator/skills/testing-workflow/SKILL.md +4 -2
  127. package/src/orchestrator/skills/validation-gates/SKILL.md +4 -2
  128. package/dist/cli/diff.d.ts +0 -3
  129. package/dist/cli/diff.d.ts.map +0 -1
  130. package/dist/cli/diff.js +0 -27
  131. package/dist/cli/diff.js.map +0 -1
  132. package/src/cli/diff.ts +0 -44
@@ -1,5 +1,5 @@
1
1
  import { resolve, basename } from 'node:path'
2
- import { mkdir, writeFile, readdir, readFile } from 'node:fs/promises'
2
+ import { mkdir, writeFile, readdir, readFile, unlink } from 'node:fs/promises'
3
3
  import { existsSync } from 'node:fs'
4
4
  import { copyDir, getOrchestratorRoot, removeDirIfExists } from '../copy.js'
5
5
  import { scaffoldMcpConfig } from '../mcp.js'
@@ -130,13 +130,16 @@ export async function install(
130
130
  const excludedSkills = stack ? getExcludedSkills(stack) : new Set<string>()
131
131
  const excludedAgents = stack ? getExcludedAgents(stack) : new Set<string>()
132
132
 
133
- // 1. .cursorrules ← copilot-instructions.md (body only)
133
+ // 1. .cursorrules ← Cursor-specific intro (not copilot-instructions.md verbatim)
134
134
  const cursorrules = resolve(projectRoot, '.cursorrules')
135
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')
136
+ const cursorIntro = [
137
+ '# Project Instructions',
138
+ '',
139
+ 'All conventions, architecture, and project context live in `.cursor/rules/`. Read those files before making changes.',
140
+ '',
141
+ ].join('\n')
142
+ await writeFile(cursorrules, cursorIntro)
140
143
  results.created.push(cursorrules)
141
144
  } else {
142
145
  results.skipped.push(cursorrules)
@@ -166,7 +169,7 @@ export async function install(
166
169
  'agent-workflows',
167
170
  resolve(rulesRoot, 'agent-workflows'),
168
171
  results,
169
- { descriptionPrefix: 'Workflow: ' }
172
+ { descriptionPrefix: 'Workflow: ', excludeFiles: new Set(['README.md']) }
170
173
  )
171
174
 
172
175
  // 6. Prompts → .cursor/rules/prompts/*.mdc
@@ -210,11 +213,14 @@ export async function update(
210
213
  const excludedSkills = stack ? getExcludedSkills(stack) : new Set<string>()
211
214
  const excludedAgents = stack ? getExcludedAgents(stack) : new Set<string>()
212
215
 
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')
216
+ // Overwrite .cursorrules with Cursor-specific intro
217
+ const cursorIntro = [
218
+ '# Project Instructions',
219
+ '',
220
+ 'All conventions, architecture, and project context live in `.cursor/rules/`. Read those files before making changes.',
221
+ '',
222
+ ].join('\n')
223
+ await writeFile(resolve(projectRoot, '.cursorrules'), cursorIntro)
218
224
  results.copied.push('.cursorrules')
219
225
 
220
226
  const rulesRoot = resolve(projectRoot, '.cursor', 'rules')
@@ -225,6 +231,15 @@ export async function update(
225
231
  await removeDirIfExists(resolve(rulesRoot, dir))
226
232
  }
227
233
 
234
+ // Remove stale root-level instruction .mdc files (in case instructions were renamed)
235
+ if (existsSync(rulesRoot)) {
236
+ for (const file of await readdir(rulesRoot)) {
237
+ if (file.endsWith('.mdc')) {
238
+ await unlink(resolve(rulesRoot, file))
239
+ }
240
+ }
241
+ }
242
+
228
243
  // Overwrite framework rules
229
244
  await convertDir(srcRoot, 'instructions', rulesRoot, results, {
230
245
  alwaysApply: true,
@@ -243,7 +258,7 @@ export async function update(
243
258
  'agent-workflows',
244
259
  resolve(rulesRoot, 'agent-workflows'),
245
260
  results,
246
- { descriptionPrefix: 'Workflow: ', overwrite: true }
261
+ { descriptionPrefix: 'Workflow: ', overwrite: true, excludeFiles: new Set(['README.md']) }
247
262
  )
248
263
  await convertDir(
249
264
  srcRoot,
@@ -78,16 +78,21 @@ function tryListen(
78
78
  let attempt = 0
79
79
 
80
80
  function attemptListen(): void {
81
- server.listen(port + attempt, '127.0.0.1', () => {
82
- res(port + attempt)
83
- })
84
- server.once('error', (err: Error & { code?: string }) => {
81
+ const currentPort = port + attempt
82
+
83
+ const onError = (err: Error & { code?: string }): void => {
85
84
  if (err.code === 'EADDRINUSE' && attempt < maxAttempts) {
86
85
  attempt++
87
86
  attemptListen()
88
87
  } else {
89
88
  rej(err)
90
89
  }
90
+ }
91
+
92
+ server.once('error', onError)
93
+ server.listen(currentPort, '127.0.0.1', () => {
94
+ server.removeListener('error', onError)
95
+ res(currentPort)
91
96
  })
92
97
  }
93
98
 
package/src/cli/eject.ts CHANGED
@@ -6,9 +6,10 @@ import type { CliContext } from './types.js'
6
6
 
7
7
  export default async function eject({
8
8
  pkgRoot: _pkgRoot,
9
- args: _args,
9
+ args,
10
10
  }: CliContext): Promise<void> {
11
11
  const projectRoot = process.cwd()
12
+ const dryRun = args.includes('--dry-run')
12
13
 
13
14
  const manifest = await readManifest(projectRoot)
14
15
  if (!manifest) {
@@ -24,6 +25,11 @@ export default async function eject({
24
25
  ' • You can safely uninstall the opencastle package after this\n'
25
26
  )
26
27
 
28
+ if (dryRun) {
29
+ console.log(' [dry-run] No files were changed.\n')
30
+ return
31
+ }
32
+
27
33
  const proceed = await confirm('Continue?')
28
34
  if (!proceed) {
29
35
  console.log(' Aborted.')
@@ -0,0 +1,77 @@
1
+ import { resolve } from 'node:path'
2
+ import { readFile, writeFile } from 'node:fs/promises'
3
+ import { existsSync } from 'node:fs'
4
+ import type { ManagedPaths } from './types.js'
5
+
6
+ const START_MARKER = '# >>> OpenCastle managed (do not edit) >>>'
7
+ const END_MARKER = '# <<< OpenCastle managed <<<'
8
+
9
+ /**
10
+ * Build the gitignore block for OpenCastle-managed files.
11
+ *
12
+ * Ignores all framework-managed paths plus the manifest file.
13
+ * Explicitly un-ignores customizable directories so user edits
14
+ * are committed even when a parent directory is ignored.
15
+ */
16
+ function buildBlock(managed: ManagedPaths): string {
17
+ const lines: string[] = [START_MARKER]
18
+
19
+ // Framework-managed paths (overwritten on `opencastle update`)
20
+ for (const p of managed.framework) {
21
+ lines.push(p)
22
+ }
23
+
24
+ // Manifest file
25
+ lines.push('.opencastle.json')
26
+
27
+ // Un-ignore customizable paths so they stay tracked
28
+ for (const p of managed.customizable) {
29
+ lines.push(`!${p}`)
30
+ }
31
+
32
+ lines.push(END_MARKER)
33
+ return lines.join('\n')
34
+ }
35
+
36
+ /**
37
+ * Create or update the project's `.gitignore` with OpenCastle entries.
38
+ *
39
+ * - If no `.gitignore` exists, creates one with the managed block.
40
+ * - If `.gitignore` exists but has no OpenCastle block, appends it.
41
+ * - If `.gitignore` already contains an OpenCastle block, replaces it
42
+ * (handles re-init or IDE switch cleanly).
43
+ */
44
+ export async function updateGitignore(
45
+ projectRoot: string,
46
+ managed: ManagedPaths
47
+ ): Promise<'created' | 'updated' | 'unchanged'> {
48
+ const gitignorePath = resolve(projectRoot, '.gitignore')
49
+ const block = buildBlock(managed)
50
+
51
+ if (!existsSync(gitignorePath)) {
52
+ await writeFile(gitignorePath, block + '\n', 'utf8')
53
+ return 'created'
54
+ }
55
+
56
+ const existing = await readFile(gitignorePath, 'utf8')
57
+
58
+ // Replace existing block
59
+ const startIdx = existing.indexOf(START_MARKER)
60
+ const endIdx = existing.indexOf(END_MARKER)
61
+
62
+ if (startIdx !== -1 && endIdx !== -1) {
63
+ const before = existing.slice(0, startIdx)
64
+ const after = existing.slice(endIdx + END_MARKER.length)
65
+ const updated = before + block + after
66
+
67
+ if (updated === existing) return 'unchanged'
68
+
69
+ await writeFile(gitignorePath, updated, 'utf8')
70
+ return 'updated'
71
+ }
72
+
73
+ // Append block to existing file
74
+ const separator = existing.endsWith('\n') ? '\n' : '\n\n'
75
+ await writeFile(gitignorePath, existing + separator + block + '\n', 'utf8')
76
+ return 'updated'
77
+ }
package/src/cli/init.ts CHANGED
@@ -1,7 +1,11 @@
1
1
  import { resolve } from 'node:path'
2
- import { readFile } from 'node:fs/promises'
2
+ import { readFile, unlink } from 'node:fs/promises'
3
+ import { existsSync } from 'node:fs'
3
4
  import { select, confirm, closePrompts } from './prompt.js'
4
5
  import { readManifest, writeManifest, createManifest } from './manifest.js'
6
+ import { removeDirIfExists } from './copy.js'
7
+ import { updateGitignore } from './gitignore.js'
8
+ import { getRequiredMcpEnvVars } from './stack-config.js'
5
9
  import type { CliContext, IdeAdapter, CmsChoice, DbChoice, PmChoice, NotifChoice, StackConfig } from './types.js'
6
10
 
7
11
  const ADAPTERS: Record<string, () => Promise<IdeAdapter>> = {
@@ -11,11 +15,13 @@ const ADAPTERS: Record<string, () => Promise<IdeAdapter>> = {
11
15
  import('./adapters/claude-code.js') as Promise<IdeAdapter>,
12
16
  }
13
17
 
14
- export default async function init({ pkgRoot }: CliContext): Promise<void> {
18
+ export default async function init({ pkgRoot, args }: CliContext): Promise<void> {
15
19
  const projectRoot = process.cwd()
20
+ const dryRun = args.includes('--dry-run')
16
21
 
17
22
  // Check for existing installation
18
23
  const existing = await readManifest(projectRoot)
24
+ let isReinit = false
19
25
  if (existing) {
20
26
  const proceed = await confirm(
21
27
  `OpenCastle already installed (v${existing.version}, ${existing.ide}). Re-initialize?`,
@@ -25,6 +31,7 @@ export default async function init({ pkgRoot }: CliContext): Promise<void> {
25
31
  console.log(' Aborted.')
26
32
  return
27
33
  }
34
+ isReinit = true
28
35
  }
29
36
 
30
37
  const pkg = JSON.parse(
@@ -89,6 +96,49 @@ export default async function init({ pkgRoot }: CliContext): Promise<void> {
89
96
  console.log(`\n Installing for ${ide}...`)
90
97
  console.log(` Stack: CMS=${stack.cms}, DB=${stack.db}, PM=${stack.pm}, Notifications=${stack.notifications}\n`)
91
98
 
99
+ // ── Dry run ─────────────────────────────────────────────────────
100
+ 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}`)
109
+ }
110
+ console.log(` + .opencastle.json`)
111
+ console.log(` + .gitignore (OpenCastle entries)`)
112
+ console.log('\n No files were written.\n')
113
+ closePrompts()
114
+ return
115
+ }
116
+
117
+ // ── Clean up previous installation on re-init ────────────────
118
+ if (isReinit && existing) {
119
+ const frameworkPaths = existing.managedPaths?.framework ?? []
120
+ for (const p of frameworkPaths) {
121
+ const fullPath = resolve(projectRoot, p)
122
+ if (p.endsWith('/')) {
123
+ await removeDirIfExists(fullPath)
124
+ } else if (existsSync(fullPath)) {
125
+ await unlink(fullPath)
126
+ }
127
+ }
128
+ // Remove MCP config so it gets regenerated with new stack
129
+ const mcpCandidates = [
130
+ '.vscode/mcp.json',
131
+ '.cursor/mcp.json',
132
+ '.claude/mcp.json',
133
+ ]
134
+ for (const mcpPath of mcpCandidates) {
135
+ const fullPath = resolve(projectRoot, mcpPath)
136
+ if (existsSync(fullPath)) {
137
+ await unlink(fullPath)
138
+ }
139
+ }
140
+ }
141
+
92
142
  // ── Run adapter ─────────────────────────────────────────────────
93
143
  const adapter = await ADAPTERS[ide]()
94
144
  const results = await adapter.install(pkgRoot, projectRoot, stack)
@@ -99,21 +149,54 @@ export default async function init({ pkgRoot }: CliContext): Promise<void> {
99
149
  manifest.stack = stack
100
150
  await writeManifest(projectRoot, manifest)
101
151
 
152
+ // ── Update .gitignore ───────────────────────────────────────────
153
+ const managedPaths = adapter.getManagedPaths()
154
+ const gitignoreResult = await updateGitignore(projectRoot, managedPaths)
155
+
102
156
  // ── Summary ─────────────────────────────────────────────────────
103
157
  const created = results.created.length
104
158
  const skipped = results.skipped.length
105
159
 
106
160
  console.log(` ✓ Created ${created} files`)
161
+ if (gitignoreResult === 'created') {
162
+ console.log(' ✓ Created .gitignore with OpenCastle entries')
163
+ } else if (gitignoreResult === 'updated') {
164
+ console.log(' ✓ Updated .gitignore with OpenCastle entries')
165
+ }
107
166
  if (skipped > 0) {
108
167
  console.log(` → Skipped ${skipped} existing files`)
109
168
  }
110
169
 
170
+ // ── Env var notice ──────────────────────────────────────────────
171
+ const envVars = getRequiredMcpEnvVars(stack)
172
+ if (envVars.length > 0) {
173
+ console.log(`\n ⚠ Required environment variables for MCP servers:\n`)
174
+ for (const { envVar, hint } of envVars) {
175
+ console.log(` ${envVar}`)
176
+ console.log(` └ ${hint}\n`)
177
+ }
178
+ }
179
+
111
180
  console.log(`\n Next steps:`)
181
+ if (ide === 'vscode') {
182
+ console.log(
183
+ ' 0. Reload VS Code window (Cmd+Shift+P → "Reload Window") to pick up agents'
184
+ )
185
+ } else if (ide === 'cursor') {
186
+ console.log(
187
+ ' 0. Reload Cursor window to pick up the new rule files'
188
+ )
189
+ }
190
+ if (envVars.length > 0) {
191
+ console.log(
192
+ ` 1. Set the environment variable${envVars.length > 1 ? 's' : ''} listed above`
193
+ )
194
+ }
112
195
  console.log(
113
- ' 1. Run the "Bootstrap Customizations" prompt to configure for your project'
196
+ ` ${envVars.length > 0 ? '2' : '1'}. Run the "Bootstrap Customizations" prompt to configure for your project`
114
197
  )
115
- console.log(' 2. Customize agent definitions for your tech stack')
116
- console.log(' 3. Commit the generated files to your repository')
198
+ console.log(` ${envVars.length > 0 ? '3' : '2'}. Customize agent definitions for your tech stack`)
199
+ console.log(` ${envVars.length > 0 ? '4' : '3'}. Commit the generated files to your repository`)
117
200
  console.log()
118
201
 
119
202
  closePrompts()
package/src/cli/mcp.ts CHANGED
@@ -6,7 +6,7 @@ import { getIncludedMcpServers } from './stack-config.js';
6
6
  import type { ScaffoldResult, StackConfig } from './types.js';
7
7
 
8
8
  /**
9
- * Scaffold the MCP server config into the target project.
9
+ * Scaffold or merge the MCP server config into the target project.
10
10
  *
11
11
  * Reads the template from `opencastle/src/orchestrator/mcp.json`,
12
12
  * writes it to `<projectRoot>/<destRelPath>` (e.g. `.vscode/mcp.json`).
@@ -14,7 +14,8 @@ import type { ScaffoldResult, StackConfig } from './types.js';
14
14
  * When a StackConfig is provided, only servers relevant to the chosen
15
15
  * CMS/DB stack (plus core servers) are included.
16
16
  *
17
- * This is a customizable file scaffolded once, never overwritten on update.
17
+ * If the file already exists, missing servers are merged in without
18
+ * overwriting any existing server configs.
18
19
  */
19
20
  export async function scaffoldMcpConfig(
20
21
  pkgRoot: string,
@@ -24,10 +25,6 @@ export async function scaffoldMcpConfig(
24
25
  ): Promise<ScaffoldResult> {
25
26
  const destPath = resolve(projectRoot, destRelPath);
26
27
 
27
- if (existsSync(destPath)) {
28
- return { path: destPath, action: 'skipped' };
29
- }
30
-
31
28
  const srcRoot = getOrchestratorRoot(pkgRoot);
32
29
  const templatePath = resolve(srcRoot, 'mcp.json');
33
30
  const content = await readFile(templatePath, 'utf8');
@@ -42,6 +39,34 @@ export async function scaffoldMcpConfig(
42
39
  );
43
40
  }
44
41
 
42
+ if (existsSync(destPath)) {
43
+ // Merge: add missing servers without overwriting existing ones
44
+ const existingContent = await readFile(destPath, 'utf8');
45
+ const existing = JSON.parse(existingContent) as {
46
+ servers?: Record<string, unknown>;
47
+ [key: string]: unknown;
48
+ };
49
+
50
+ if (!existing.servers) {
51
+ existing.servers = {};
52
+ }
53
+
54
+ let added = 0;
55
+ for (const [key, value] of Object.entries(template.servers)) {
56
+ if (!(key in existing.servers)) {
57
+ existing.servers[key] = value;
58
+ added++;
59
+ }
60
+ }
61
+
62
+ if (added === 0) {
63
+ return { path: destPath, action: 'skipped' };
64
+ }
65
+
66
+ await writeFile(destPath, JSON.stringify(existing, null, 2) + '\n');
67
+ return { path: destPath, action: 'created' };
68
+ }
69
+
45
70
  await mkdir(dirname(destPath), { recursive: true });
46
71
  await writeFile(destPath, JSON.stringify(template, null, 2) + '\n');
47
72
 
@@ -340,11 +340,36 @@ function parseBlockScalar(lines: string[], startIdx: number, parentIndent: numbe
340
340
 
341
341
  /**
342
342
  * Parse a flow sequence: [item1, item2, item3]
343
+ * Handles quoted strings that may contain commas.
343
344
  */
344
345
  function parseFlowSequence(text: string): Array<string | number | boolean | null> {
345
346
  const inner = text.slice(1, -1).trim()
346
347
  if (inner === '') return []
347
- return inner.split(',').map((s) => castScalar(s.trim()))
348
+
349
+ const items: string[] = []
350
+ let current = ''
351
+ let inQuote: string | null = null
352
+
353
+ for (let i = 0; i < inner.length; i++) {
354
+ const ch = inner[i]
355
+ if (inQuote) {
356
+ if (ch === inQuote) {
357
+ inQuote = null
358
+ } else {
359
+ current += ch
360
+ }
361
+ } else if (ch === '"' || ch === "'") {
362
+ inQuote = ch
363
+ } else if (ch === ',') {
364
+ items.push(current.trim())
365
+ current = ''
366
+ } else {
367
+ current += ch
368
+ }
369
+ }
370
+ if (current.trim()) items.push(current.trim())
371
+
372
+ return items.map(castScalar)
348
373
  }
349
374
 
350
375
  // ── Schema validation ──────────────────────────────────────────────
package/src/cli/run.ts CHANGED
@@ -43,6 +43,7 @@ function parseArgs(args: string[]): RunOptions {
43
43
  break
44
44
  case '--file':
45
45
  case '-f':
46
+ if (i + 1 >= args.length) { console.error(' \u2717 --file requires a path'); process.exit(1) }
46
47
  opts.file = args[++i]
47
48
  break
48
49
  case '--dry-run':
@@ -50,6 +51,7 @@ function parseArgs(args: string[]): RunOptions {
50
51
  break
51
52
  case '--concurrency':
52
53
  case '-c': {
54
+ if (i + 1 >= args.length) { console.error(' \u2717 --concurrency requires a number'); process.exit(1) }
53
55
  const val = parseInt(args[++i], 10)
54
56
  if (!Number.isFinite(val) || val < 1) {
55
57
  console.error(` ✗ --concurrency must be an integer >= 1`)
@@ -60,9 +62,11 @@ function parseArgs(args: string[]): RunOptions {
60
62
  }
61
63
  case '--adapter':
62
64
  case '-a':
65
+ if (i + 1 >= args.length) { console.error(' \u2717 --adapter requires a name'); process.exit(1) }
63
66
  opts.adapter = args[++i]
64
67
  break
65
68
  case '--report-dir':
69
+ if (i + 1 >= args.length) { console.error(' \u2717 --report-dir requires a path'); process.exit(1) }
66
70
  opts.reportDir = args[++i]
67
71
  break
68
72
  case '--verbose':
@@ -105,6 +105,30 @@ const NOTIF_MCP_MAP: Record<NotifChoice, string[]> = {
105
105
  /** Always-included MCP servers */
106
106
  const CORE_MCP_SERVERS = ['chrome-devtools', 'Vercel'];
107
107
 
108
+ // ── MCP environment variable requirements ─────────────────────
109
+
110
+ export interface McpEnvRequirement {
111
+ /** MCP server key (must match mcp.json) */
112
+ server: string;
113
+ /** Environment variable name */
114
+ envVar: string;
115
+ /** Short description of where to get the key */
116
+ hint: string;
117
+ }
118
+
119
+ /**
120
+ * Registry of MCP servers that require API keys via environment variables.
121
+ * Only servers with `env` fields in mcp.json need to be listed here.
122
+ * HTTP-based servers (Sanity, Slack, Vercel, etc.) handle auth via OAuth.
123
+ */
124
+ const MCP_ENV_REQUIREMENTS: McpEnvRequirement[] = [
125
+ {
126
+ server: 'Linear',
127
+ envVar: 'LINEAR_API_KEY',
128
+ hint: 'Create at linear.app → Settings → API → Personal API keys',
129
+ },
130
+ ];
131
+
108
132
  export function getExcludedSkills(stack: StackConfig): Set<string> {
109
133
  return new Set([
110
134
  ...CMS_SKILL_MAP[stack.cms],
@@ -131,6 +155,15 @@ export function getIncludedMcpServers(stack: StackConfig): Set<string> {
131
155
  ]);
132
156
  }
133
157
 
158
+ /**
159
+ * Returns env var requirements for the MCP servers included in the stack.
160
+ * Only returns entries for servers that actually need API keys.
161
+ */
162
+ export function getRequiredMcpEnvVars(stack: StackConfig): McpEnvRequirement[] {
163
+ const included = getIncludedMcpServers(stack);
164
+ return MCP_ENV_REQUIREMENTS.filter((req) => included.has(req.server));
165
+ }
166
+
134
167
  // ── Customization file transforms ─────────────────────────────
135
168
 
136
169
  /**
package/src/cli/update.ts CHANGED
@@ -38,18 +38,33 @@ export default async function update({
38
38
  await readFile(resolve(pkgRoot, 'package.json'), 'utf8')
39
39
  ) as { version: string }
40
40
 
41
- if (manifest.version === pkg.version && !args.includes('--force')) {
41
+ const dryRun = args.includes('--dry-run')
42
+
43
+ if (manifest.version === pkg.version && !args.includes('--force') && !dryRun) {
42
44
  console.log(` Already up to date (v${pkg.version}).`)
43
45
  return
44
46
  }
45
47
 
46
48
  console.log(
47
- `\n 🏰 OpenCastle update: v${manifest.version} → v${pkg.version}\n`
49
+ `\n 🏰 OpenCastle ${dryRun ? 'dry-run' : 'update'}: v${manifest.version} → v${pkg.version}\n`
48
50
  )
49
51
  console.log(` IDE: ${manifest.ide}`)
50
52
  console.log(' Framework files will be overwritten.')
51
53
  console.log(' Customization files will be preserved.\n')
52
54
 
55
+ if (dryRun) {
56
+ console.log(' [dry-run] Framework files that would be updated:\n')
57
+ for (const p of manifest.managedPaths?.framework ?? []) {
58
+ console.log(` ↻ ${p}`)
59
+ }
60
+ console.log('\n Customization files that would be preserved:\n')
61
+ for (const p of manifest.managedPaths?.customizable ?? []) {
62
+ console.log(` ✓ ${p}`)
63
+ }
64
+ console.log('\n No files were written.\n')
65
+ return
66
+ }
67
+
53
68
  const proceed = await confirm('Proceed with update?')
54
69
  if (!proceed) {
55
70
  console.log(' Aborted.')
@@ -57,7 +72,7 @@ export default async function update({
57
72
  }
58
73
 
59
74
  const adapter = await ADAPTERS[manifest.ide]()
60
- const results = await adapter.update(pkgRoot, projectRoot)
75
+ const results = await adapter.update(pkgRoot, projectRoot, manifest.stack)
61
76
 
62
77
  // Update manifest
63
78
  manifest.version = pkg.version
@@ -1,3 +1,5 @@
1
+ <!-- ⚠️ This file is managed by OpenCastle. Edits will be overwritten on update. Customize in the customizations/ directory instead. -->
2
+
1
3
  # Workflow Templates
2
4
 
3
5
  Declarative workflow templates for common orchestration patterns. Inspired by Sandcastle's YAML workflow engine, these templates provide reproducible execution plans that the Team Lead and prompts can reference.
@@ -1,3 +1,5 @@
1
+ <!-- ⚠️ This file is managed by OpenCastle. Edits will be overwritten on update. Customize in the customizations/ directory instead. -->
2
+
1
3
  # Workflow: Bug Fix
2
4
 
3
5
  Structured workflow for investigating and fixing reported bugs.
@@ -1,3 +1,5 @@
1
+ <!-- ⚠️ This file is managed by OpenCastle. Edits will be overwritten on update. Customize in the customizations/ directory instead. -->
2
+
1
3
  # Workflow: Data Pipeline
2
4
 
3
5
  Standard execution plan for scraping, processing, and importing data.
@@ -1,3 +1,5 @@
1
+ <!-- ⚠️ This file is managed by OpenCastle. Edits will be overwritten on update. Customize in the customizations/ directory instead. -->
2
+
1
3
  # Workflow: Database Migration
2
4
 
3
5
  Structured workflow for database schema changes, RLS policies, and data migrations.
@@ -1,3 +1,5 @@
1
+ <!-- ⚠️ This file is managed by OpenCastle. Edits will be overwritten on update. Customize in the customizations/ directory instead. -->
2
+
1
3
  # Workflow: Feature Implementation
2
4
 
3
5
  Standard execution plan for multi-layer features. Customize file paths, agents, and criteria per task.
@@ -1,3 +1,5 @@
1
+ <!-- ⚠️ This file is managed by OpenCastle. Edits will be overwritten on update. Customize in the customizations/ directory instead. -->
2
+
1
3
  # Workflow: Performance Optimization
2
4
 
3
5
  Measure-first optimization workflow. Never optimize without profiling data.
@@ -1,3 +1,5 @@
1
+ <!-- ⚠️ This file is managed by OpenCastle. Edits will be overwritten on update. Customize in the customizations/ directory instead. -->
2
+
1
3
  # Workflow: Code Refactoring
2
4
 
3
5
  Structured workflow for safe code refactoring — improving code quality without changing behavior.
@@ -1,3 +1,5 @@
1
+ <!-- ⚠️ This file is managed by OpenCastle. Edits will be overwritten on update. Customize in the customizations/ directory instead. -->
2
+
1
3
  # Workflow: Schema / CMS Changes
2
4
 
3
5
  Structured workflow for CMS schema modifications, query updates, and content model changes.
@@ -1,3 +1,5 @@
1
+ <!-- ⚠️ This file is managed by OpenCastle. Edits will be overwritten on update. Customize in the customizations/ directory instead. -->
2
+
1
3
  # Workflow: Security Audit
2
4
 
3
5
  Comprehensive security review workflow for auth, RLS, headers, and API security.
@@ -1,3 +1,5 @@
1
+ <!-- ⚠️ This file is managed by OpenCastle. Edits will be overwritten on update. Customize in the customizations/ directory instead. -->
2
+
1
3
  # Shared Delivery Phase
2
4
 
3
5
  This phase is referenced by all workflow templates. It covers the final delivery steps after all implementation and verification is complete.
@@ -5,6 +5,8 @@ model: Gemini 3.1 Pro
5
5
  tools: ['search/changes', 'search/codebase', 'edit/editFiles', 'web/fetch', 'read/problems', 'execute/getTerminalOutput', 'execute/runInTerminal', 'read/terminalLastCommand', 'read/terminalSelection', 'search', 'execute/testFailure', 'search/usages']
6
6
  ---
7
7
 
8
+ <!-- ⚠️ This file is managed by OpenCastle. Edits will be overwritten on update. Customize in the customizations/ directory instead. -->
9
+
8
10
  # API Designer
9
11
 
10
12
  You are an API designer specializing in route architecture, endpoint conventions, request/response schemas, versioning, error handling patterns, and API documentation.