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.
- package/README.md +16 -7
- package/bin/cli.mjs +2 -3
- package/dist/cli/adapters/claude-code.d.ts.map +1 -1
- package/dist/cli/adapters/claude-code.js +7 -3
- package/dist/cli/adapters/claude-code.js.map +1 -1
- package/dist/cli/adapters/cursor.d.ts.map +1 -1
- package/dist/cli/adapters/cursor.js +27 -9
- package/dist/cli/adapters/cursor.js.map +1 -1
- package/dist/cli/dashboard.d.ts.map +1 -1
- package/dist/cli/dashboard.js +7 -4
- package/dist/cli/dashboard.js.map +1 -1
- package/dist/cli/eject.d.ts +1 -1
- package/dist/cli/eject.d.ts.map +1 -1
- package/dist/cli/eject.js +6 -1
- package/dist/cli/eject.js.map +1 -1
- package/dist/cli/gitignore.d.ts +11 -0
- package/dist/cli/gitignore.d.ts.map +1 -0
- package/dist/cli/gitignore.js +61 -0
- package/dist/cli/gitignore.js.map +1 -0
- package/dist/cli/init.d.ts +1 -1
- package/dist/cli/init.d.ts.map +1 -1
- package/dist/cli/init.js +81 -5
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/mcp.d.ts +3 -2
- package/dist/cli/mcp.d.ts.map +1 -1
- package/dist/cli/mcp.js +23 -5
- package/dist/cli/mcp.js.map +1 -1
- package/dist/cli/run/schema.d.ts.map +1 -1
- package/dist/cli/run/schema.js +28 -1
- package/dist/cli/run/schema.js.map +1 -1
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +16 -0
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/stack-config.d.ts +13 -0
- package/dist/cli/stack-config.d.ts.map +1 -1
- package/dist/cli/stack-config.js +20 -0
- package/dist/cli/stack-config.js.map +1 -1
- package/dist/cli/update.d.ts.map +1 -1
- package/dist/cli/update.js +16 -3
- package/dist/cli/update.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/adapters/claude-code.ts +7 -5
- package/src/cli/adapters/cursor.ts +28 -13
- package/src/cli/dashboard.ts +9 -4
- package/src/cli/eject.ts +7 -1
- package/src/cli/gitignore.ts +77 -0
- package/src/cli/init.ts +88 -5
- package/src/cli/mcp.ts +31 -6
- package/src/cli/run/schema.ts +26 -1
- package/src/cli/run.ts +4 -0
- package/src/cli/stack-config.ts +33 -0
- package/src/cli/update.ts +18 -3
- package/src/orchestrator/agent-workflows/README.md +2 -0
- package/src/orchestrator/agent-workflows/bug-fix.md +2 -0
- package/src/orchestrator/agent-workflows/data-pipeline.md +2 -0
- package/src/orchestrator/agent-workflows/database-migration.md +2 -0
- package/src/orchestrator/agent-workflows/feature-implementation.md +2 -0
- package/src/orchestrator/agent-workflows/performance-optimization.md +2 -0
- package/src/orchestrator/agent-workflows/refactoring.md +2 -0
- package/src/orchestrator/agent-workflows/schema-changes.md +2 -0
- package/src/orchestrator/agent-workflows/security-audit.md +2 -0
- package/src/orchestrator/agent-workflows/shared-delivery-phase.md +2 -0
- package/src/orchestrator/agents/api-designer.agent.md +2 -0
- package/src/orchestrator/agents/architect.agent.md +2 -0
- package/src/orchestrator/agents/content-engineer.agent.md +2 -0
- package/src/orchestrator/agents/copywriter.agent.md +2 -0
- package/src/orchestrator/agents/data-expert.agent.md +2 -0
- package/src/orchestrator/agents/database-engineer.agent.md +2 -0
- package/src/orchestrator/agents/developer.agent.md +2 -0
- package/src/orchestrator/agents/devops-expert.agent.md +2 -0
- package/src/orchestrator/agents/documentation-writer.agent.md +2 -0
- package/src/orchestrator/agents/performance-expert.agent.md +2 -0
- package/src/orchestrator/agents/release-manager.agent.md +2 -0
- package/src/orchestrator/agents/researcher.agent.md +2 -0
- package/src/orchestrator/agents/reviewer.agent.md +2 -0
- package/src/orchestrator/agents/security-expert.agent.md +2 -0
- package/src/orchestrator/agents/seo-specialist.agent.md +2 -0
- package/src/orchestrator/agents/team-lead.agent.md +2 -0
- package/src/orchestrator/agents/testing-expert.agent.md +3 -1
- package/src/orchestrator/agents/ui-ux-expert.agent.md +5 -3
- package/src/orchestrator/copilot-instructions.md +2 -0
- package/src/orchestrator/instructions/ai-optimization.instructions.md +2 -0
- package/src/orchestrator/instructions/general.instructions.md +2 -0
- package/src/orchestrator/prompts/bootstrap-customizations.prompt.md +2 -0
- package/src/orchestrator/prompts/brainstorm.prompt.md +2 -0
- package/src/orchestrator/prompts/bug-fix.prompt.md +2 -0
- package/src/orchestrator/prompts/create-skill.prompt.md +2 -0
- package/src/orchestrator/prompts/generate-task-spec.prompt.md +2 -0
- package/src/orchestrator/prompts/implement-feature.prompt.md +2 -0
- package/src/orchestrator/prompts/metrics-report.prompt.md +2 -0
- package/src/orchestrator/prompts/quick-refinement.prompt.md +2 -0
- package/src/orchestrator/prompts/resolve-pr-comments.prompt.md +2 -0
- package/src/orchestrator/skills/accessibility-standards/SKILL.md +2 -0
- package/src/orchestrator/skills/agent-hooks/SKILL.md +2 -0
- package/src/orchestrator/skills/agent-memory/SKILL.md +2 -0
- package/src/orchestrator/skills/api-patterns/SKILL.md +2 -0
- package/src/orchestrator/skills/browser-testing/SKILL.md +14 -26
- package/src/orchestrator/skills/code-commenting/SKILL.md +2 -0
- package/src/orchestrator/skills/contentful-cms/SKILL.md +2 -0
- package/src/orchestrator/skills/context-map/SKILL.md +2 -0
- package/src/orchestrator/skills/convex-database/SKILL.md +2 -0
- package/src/orchestrator/skills/data-engineering/SKILL.md +2 -0
- package/src/orchestrator/skills/deployment-infrastructure/SKILL.md +2 -0
- package/src/orchestrator/skills/documentation-standards/SKILL.md +2 -0
- package/src/orchestrator/skills/fast-review/SKILL.md +2 -0
- package/src/orchestrator/skills/frontend-design/SKILL.md +2 -0
- package/src/orchestrator/skills/jira-management/SKILL.md +2 -0
- package/src/orchestrator/skills/memory-merger/SKILL.md +2 -0
- package/src/orchestrator/skills/nextjs-patterns/SKILL.md +2 -0
- package/src/orchestrator/skills/nx-workspace/SKILL.md +2 -0
- package/src/orchestrator/skills/panel-majority-vote/SKILL.md +2 -0
- package/src/orchestrator/skills/panel-majority-vote/panel-report.template.md +2 -0
- package/src/orchestrator/skills/performance-optimization/SKILL.md +2 -0
- package/src/orchestrator/skills/react-development/SKILL.md +2 -0
- package/src/orchestrator/skills/sanity-cms/SKILL.md +2 -0
- package/src/orchestrator/skills/security-hardening/SKILL.md +2 -0
- package/src/orchestrator/skills/self-improvement/SKILL.md +2 -0
- package/src/orchestrator/skills/seo-patterns/SKILL.md +2 -0
- package/src/orchestrator/skills/session-checkpoints/SKILL.md +2 -0
- package/src/orchestrator/skills/slack-notifications/SKILL.md +2 -0
- package/src/orchestrator/skills/strapi-cms/SKILL.md +2 -0
- package/src/orchestrator/skills/supabase-database/SKILL.md +2 -0
- package/src/orchestrator/skills/task-management/SKILL.md +2 -0
- package/src/orchestrator/skills/team-lead-reference/SKILL.md +2 -0
- package/src/orchestrator/skills/teams-notifications/SKILL.md +2 -0
- package/src/orchestrator/skills/testing-workflow/SKILL.md +4 -2
- package/src/orchestrator/skills/validation-gates/SKILL.md +4 -2
- package/dist/cli/diff.d.ts +0 -3
- package/dist/cli/diff.d.ts.map +0 -1
- package/dist/cli/diff.js +0 -27
- package/dist/cli/diff.js.map +0 -1
- 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
|
|
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
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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,
|
package/src/cli/dashboard.ts
CHANGED
|
@@ -78,16 +78,21 @@ function tryListen(
|
|
|
78
78
|
let attempt = 0
|
|
79
79
|
|
|
80
80
|
function attemptListen(): void {
|
|
81
|
-
|
|
82
|
-
|
|
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
|
|
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
|
-
'
|
|
196
|
+
` ${envVars.length > 0 ? '2' : '1'}. Run the "Bootstrap Customizations" prompt to configure for your project`
|
|
114
197
|
)
|
|
115
|
-
console.log('
|
|
116
|
-
console.log('
|
|
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
|
-
*
|
|
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
|
|
package/src/cli/run/schema.ts
CHANGED
|
@@ -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
|
-
|
|
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':
|
package/src/cli/stack-config.ts
CHANGED
|
@@ -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
|
-
|
|
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: 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
|
# 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.
|