opencastle 0.5.0 → 0.6.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 (63) hide show
  1. package/dist/cli/adapters/claude-code.d.ts +2 -2
  2. package/dist/cli/adapters/claude-code.d.ts.map +1 -1
  3. package/dist/cli/adapters/claude-code.js +2 -2
  4. package/dist/cli/adapters/claude-code.js.map +1 -1
  5. package/dist/cli/adapters/cursor.d.ts +2 -2
  6. package/dist/cli/adapters/cursor.d.ts.map +1 -1
  7. package/dist/cli/adapters/cursor.js +2 -2
  8. package/dist/cli/adapters/cursor.js.map +1 -1
  9. package/dist/cli/adapters/vscode.d.ts +2 -2
  10. package/dist/cli/adapters/vscode.d.ts.map +1 -1
  11. package/dist/cli/adapters/vscode.js +2 -2
  12. package/dist/cli/adapters/vscode.js.map +1 -1
  13. package/dist/cli/detect.d.ts +18 -0
  14. package/dist/cli/detect.d.ts.map +1 -0
  15. package/dist/cli/detect.js +428 -0
  16. package/dist/cli/detect.js.map +1 -0
  17. package/dist/cli/gitignore.d.ts.map +1 -1
  18. package/dist/cli/gitignore.js +0 -2
  19. package/dist/cli/gitignore.js.map +1 -1
  20. package/dist/cli/init.d.ts.map +1 -1
  21. package/dist/cli/init.js +17 -3
  22. package/dist/cli/init.js.map +1 -1
  23. package/dist/cli/mcp.d.ts +2 -2
  24. package/dist/cli/mcp.d.ts.map +1 -1
  25. package/dist/cli/mcp.js +2 -2
  26. package/dist/cli/mcp.js.map +1 -1
  27. package/dist/cli/prompt.d.ts +3 -0
  28. package/dist/cli/prompt.d.ts.map +1 -1
  29. package/dist/cli/prompt.js +96 -0
  30. package/dist/cli/prompt.js.map +1 -1
  31. package/dist/cli/stack-config.d.ts +3 -3
  32. package/dist/cli/stack-config.d.ts.map +1 -1
  33. package/dist/cli/stack-config.js +16 -5
  34. package/dist/cli/stack-config.js.map +1 -1
  35. package/dist/cli/types.d.ts +20 -1
  36. package/dist/cli/types.d.ts.map +1 -1
  37. package/dist/cli/update.d.ts.map +1 -1
  38. package/dist/cli/update.js +6 -0
  39. package/dist/cli/update.js.map +1 -1
  40. package/package.json +1 -1
  41. package/src/cli/adapters/claude-code.ts +5 -3
  42. package/src/cli/adapters/cursor.ts +5 -3
  43. package/src/cli/adapters/vscode.ts +5 -3
  44. package/src/cli/detect.ts +483 -0
  45. package/src/cli/gitignore.ts +0 -3
  46. package/src/cli/init.ts +18 -3
  47. package/src/cli/mcp.ts +4 -3
  48. package/src/cli/prompt.ts +123 -0
  49. package/src/cli/stack-config.ts +19 -6
  50. package/src/cli/types.ts +21 -1
  51. package/src/cli/update.ts +7 -0
  52. package/src/dashboard/node_modules/.vite/deps/_metadata.json +6 -6
  53. package/src/orchestrator/agents/api-designer.agent.md +1 -1
  54. package/src/orchestrator/agents/content-engineer.agent.md +1 -1
  55. package/src/orchestrator/agents/database-engineer.agent.md +1 -1
  56. package/src/orchestrator/agents/developer.agent.md +1 -1
  57. package/src/orchestrator/agents/performance-expert.agent.md +1 -1
  58. package/src/orchestrator/agents/researcher.agent.md +16 -0
  59. package/src/orchestrator/agents/reviewer.agent.md +2 -4
  60. package/src/orchestrator/agents/team-lead.agent.md +41 -63
  61. package/src/orchestrator/agents/ui-ux-expert.agent.md +1 -1
  62. package/src/orchestrator/mcp.json +16 -8
  63. package/src/orchestrator/prompts/bootstrap-customizations.prompt.md +53 -6
@@ -0,0 +1,483 @@
1
+ import { resolve } from 'node:path';
2
+ import { readFile, readdir, access } from 'node:fs/promises';
3
+ import { existsSync } from 'node:fs';
4
+ import type { RepoInfo, StackConfig } from './types.js';
5
+
6
+ // ── Detection rules ───────────────────────────────────────────
7
+
8
+ interface DetectionRule {
9
+ /** Human-readable label stored in repoInfo */
10
+ label: string;
11
+ /** File patterns to check (relative to project root) */
12
+ files: string[];
13
+ /** Optional: glob-style directory check */
14
+ dirs?: string[];
15
+ }
16
+
17
+ const PACKAGE_MANAGERS: DetectionRule[] = [
18
+ { label: 'pnpm', files: ['pnpm-lock.yaml', 'pnpm-workspace.yaml'] },
19
+ { label: 'yarn', files: ['yarn.lock'] },
20
+ { label: 'bun', files: ['bun.lockb', 'bun.lock'] },
21
+ { label: 'npm', files: ['package-lock.json'] },
22
+ ];
23
+
24
+ const MONOREPO_TOOLS: DetectionRule[] = [
25
+ { label: 'nx', files: ['nx.json'] },
26
+ { label: 'turborepo', files: ['turbo.json'] },
27
+ { label: 'lerna', files: ['lerna.json'] },
28
+ { label: 'pnpm-workspaces', files: ['pnpm-workspace.yaml'] },
29
+ ];
30
+
31
+ const FRAMEWORKS: DetectionRule[] = [
32
+ { label: 'next', files: ['next.config.js', 'next.config.mjs', 'next.config.ts'] },
33
+ { label: 'nuxt', files: ['nuxt.config.js', 'nuxt.config.ts'] },
34
+ { label: 'astro', files: ['astro.config.mjs', 'astro.config.ts', 'astro.config.js'] },
35
+ { label: 'remix', files: ['remix.config.js', 'remix.config.ts'] },
36
+ { label: 'sveltekit', files: ['svelte.config.js', 'svelte.config.ts'] },
37
+ { label: 'vite', files: ['vite.config.js', 'vite.config.ts', 'vite.config.mjs'] },
38
+ { label: 'angular', files: ['angular.json'] },
39
+ { label: 'gatsby', files: ['gatsby-config.js', 'gatsby-config.ts'] },
40
+ { label: 'express', files: [] }, // detected via package.json
41
+ ];
42
+
43
+ const DATABASES: DetectionRule[] = [
44
+ { label: 'supabase', files: ['supabase/config.toml'], dirs: ['supabase/'] },
45
+ { label: 'prisma', files: ['prisma/schema.prisma'] },
46
+ { label: 'drizzle', files: ['drizzle.config.ts', 'drizzle.config.js'] },
47
+ { label: 'convex', files: ['convex/_generated'], dirs: ['convex/'] },
48
+ { label: 'mongoose', files: [] }, // detected via package.json
49
+ { label: 'typeorm', files: [] }, // detected via package.json
50
+ ];
51
+
52
+ const CMS_PLATFORMS: DetectionRule[] = [
53
+ { label: 'sanity', files: ['sanity.config.ts', 'sanity.config.js', 'sanity.config.mjs'] },
54
+ { label: 'contentful', files: ['.contentful.json', 'contentful.config.js'] },
55
+ { label: 'strapi', files: [] }, // detected via package.json
56
+ { label: 'payload', files: ['payload.config.ts', 'payload.config.js'] },
57
+ ];
58
+
59
+ const DEPLOYMENT: DetectionRule[] = [
60
+ { label: 'vercel', files: ['vercel.json'] },
61
+ { label: 'netlify', files: ['netlify.toml'] },
62
+ { label: 'docker', files: ['Dockerfile', 'docker-compose.yml', 'docker-compose.yaml', 'compose.yml', 'compose.yaml'] },
63
+ { label: 'railway', files: ['railway.json', 'railway.toml'] },
64
+ { label: 'fly', files: ['fly.toml'] },
65
+ { label: 'render', files: ['render.yaml'] },
66
+ { label: 'aws-cdk', files: ['cdk.json'] },
67
+ { label: 'terraform', files: [] , dirs: ['terraform/'] },
68
+ { label: 'pulumi', files: ['Pulumi.yaml'] },
69
+ ];
70
+
71
+ const TESTING: DetectionRule[] = [
72
+ { label: 'jest', files: ['jest.config.js', 'jest.config.ts', 'jest.config.mjs'] },
73
+ { label: 'vitest', files: ['vitest.config.ts', 'vitest.config.js', 'vitest.config.mjs'] },
74
+ { label: 'playwright', files: ['playwright.config.ts', 'playwright.config.js'] },
75
+ { label: 'cypress', files: ['cypress.config.ts', 'cypress.config.js'], dirs: ['cypress/'] },
76
+ ];
77
+
78
+ const CICD: DetectionRule[] = [
79
+ { label: 'github-actions', files: [], dirs: ['.github/workflows/'] },
80
+ { label: 'gitlab-ci', files: ['.gitlab-ci.yml'] },
81
+ { label: 'circleci', files: ['.circleci/config.yml'] },
82
+ { label: 'jenkins', files: ['Jenkinsfile'] },
83
+ { label: 'travis', files: ['.travis.yml'] },
84
+ ];
85
+
86
+ const STYLING: DetectionRule[] = [
87
+ { label: 'tailwind', files: ['tailwind.config.js', 'tailwind.config.ts', 'tailwind.config.mjs'] },
88
+ { label: 'sass', files: [] }, // detected via package.json
89
+ { label: 'styled-components', files: [] }, // detected via package.json
90
+ { label: 'emotion', files: [] }, // detected via package.json
91
+ { label: 'css-modules', files: [] }, // detected via file extensions
92
+ ];
93
+
94
+ const AUTH: DetectionRule[] = [
95
+ { label: 'next-auth', files: [] }, // detected via package.json
96
+ { label: 'clerk', files: [] }, // detected via package.json
97
+ { label: 'auth0', files: [] }, // detected via package.json
98
+ { label: 'supabase-auth', files: [] }, // detected via supabase presence
99
+ { label: 'lucia', files: [] }, // detected via package.json
100
+ { label: 'passport', files: [] }, // detected via package.json
101
+ ];
102
+
103
+ // Mapping of npm package names to detection labels
104
+ const PACKAGE_DETECTIONS: Record<string, { category: string; label: string }> = {
105
+ 'next': { category: 'frameworks', label: 'next' },
106
+ 'nuxt': { category: 'frameworks', label: 'nuxt' },
107
+ 'astro': { category: 'frameworks', label: 'astro' },
108
+ '@remix-run/node': { category: 'frameworks', label: 'remix' },
109
+ '@sveltejs/kit': { category: 'frameworks', label: 'sveltekit' },
110
+ 'express': { category: 'frameworks', label: 'express' },
111
+ 'fastify': { category: 'frameworks', label: 'fastify' },
112
+ 'hono': { category: 'frameworks', label: 'hono' },
113
+ 'mongoose': { category: 'databases', label: 'mongoose' },
114
+ 'typeorm': { category: 'databases', label: 'typeorm' },
115
+ '@supabase/supabase-js': { category: 'databases', label: 'supabase' },
116
+ '@prisma/client': { category: 'databases', label: 'prisma' },
117
+ 'drizzle-orm': { category: 'databases', label: 'drizzle' },
118
+ 'convex': { category: 'databases', label: 'convex' },
119
+ 'sanity': { category: 'cms', label: 'sanity' },
120
+ 'contentful': { category: 'cms', label: 'contentful' },
121
+ '@strapi/strapi': { category: 'cms', label: 'strapi' },
122
+ 'payload': { category: 'cms', label: 'payload' },
123
+ 'next-auth': { category: 'auth', label: 'next-auth' },
124
+ '@auth/core': { category: 'auth', label: 'next-auth' },
125
+ '@clerk/nextjs': { category: 'auth', label: 'clerk' },
126
+ '@clerk/clerk-sdk-node': { category: 'auth', label: 'clerk' },
127
+ '@auth0/nextjs-auth0': { category: 'auth', label: 'auth0' },
128
+ 'auth0': { category: 'auth', label: 'auth0' },
129
+ 'lucia': { category: 'auth', label: 'lucia' },
130
+ 'passport': { category: 'auth', label: 'passport' },
131
+ 'sass': { category: 'styling', label: 'sass' },
132
+ 'styled-components': { category: 'styling', label: 'styled-components' },
133
+ '@emotion/react': { category: 'styling', label: 'emotion' },
134
+ '@emotion/styled': { category: 'styling', label: 'emotion' },
135
+ 'tailwindcss': { category: 'styling', label: 'tailwind' },
136
+ 'jest': { category: 'testing', label: 'jest' },
137
+ 'vitest': { category: 'testing', label: 'vitest' },
138
+ '@playwright/test': { category: 'testing', label: 'playwright' },
139
+ 'cypress': { category: 'testing', label: 'cypress' },
140
+ };
141
+
142
+ // ── Helpers ───────────────────────────────────────────────────
143
+
144
+ async function fileExists(path: string): Promise<boolean> {
145
+ try {
146
+ await access(path);
147
+ return true;
148
+ } catch {
149
+ return false;
150
+ }
151
+ }
152
+
153
+ async function dirExists(path: string): Promise<boolean> {
154
+ try {
155
+ await access(path);
156
+ const entries = await readdir(path);
157
+ return entries.length >= 0; // exists as a directory
158
+ } catch {
159
+ return false;
160
+ }
161
+ }
162
+
163
+ function addUnique(arr: string[], value: string): void {
164
+ if (!arr.includes(value)) arr.push(value);
165
+ }
166
+
167
+ // ── Main detect function ──────────────────────────────────────
168
+
169
+ /** Internal type with required arrays for detection phase. */
170
+ interface RepoInfoInternal {
171
+ packageManager?: string;
172
+ monorepo?: string;
173
+ language?: string;
174
+ frameworks: string[];
175
+ databases: string[];
176
+ cms: string[];
177
+ deployment: string[];
178
+ testing: string[];
179
+ cicd: string[];
180
+ styling: string[];
181
+ auth: string[];
182
+ mcpConfig?: boolean;
183
+ configFiles: string[];
184
+ }
185
+
186
+ /**
187
+ * Perform repo research: scan the project root for config files,
188
+ * package.json dependencies, and directory structures to detect
189
+ * the project's tooling and tech stack.
190
+ */
191
+ export async function detectRepoInfo(projectRoot: string): Promise<RepoInfo> {
192
+ const info: RepoInfoInternal = {
193
+ frameworks: [],
194
+ databases: [],
195
+ cms: [],
196
+ deployment: [],
197
+ testing: [],
198
+ cicd: [],
199
+ styling: [],
200
+ auth: [],
201
+ configFiles: [],
202
+ };
203
+
204
+ // ── 1. Detect package manager ───────────────────────────────
205
+ for (const pm of PACKAGE_MANAGERS) {
206
+ const found = await checkFiles(projectRoot, pm.files);
207
+ if (found.length > 0) {
208
+ info.packageManager = pm.label;
209
+ info.configFiles.push(...found);
210
+ break; // first match wins (order = priority)
211
+ }
212
+ }
213
+
214
+ // ── 2. Detect monorepo tool ─────────────────────────────────
215
+ for (const tool of MONOREPO_TOOLS) {
216
+ const found = await checkFiles(projectRoot, tool.files);
217
+ if (found.length > 0) {
218
+ info.monorepo = tool.label;
219
+ info.configFiles.push(...found);
220
+ break;
221
+ }
222
+ }
223
+
224
+ // ── 3. Detect by config files ───────────────────────────────
225
+ await detectCategory(projectRoot, FRAMEWORKS, info, 'frameworks');
226
+ await detectCategory(projectRoot, DATABASES, info, 'databases');
227
+ await detectCategory(projectRoot, CMS_PLATFORMS, info, 'cms');
228
+ await detectCategory(projectRoot, DEPLOYMENT, info, 'deployment');
229
+ await detectCategory(projectRoot, TESTING, info, 'testing');
230
+ await detectCategory(projectRoot, CICD, info, 'cicd');
231
+ await detectCategory(projectRoot, STYLING, info, 'styling');
232
+ await detectCategory(projectRoot, AUTH, info, 'auth');
233
+
234
+ // ── 4. Detect from package.json deps ────────────────────────
235
+ await detectFromPackageJson(projectRoot, info);
236
+
237
+ // ── 5. Detect MCP config ────────────────────────────────────
238
+ const mcpPaths = [
239
+ '.vscode/mcp.json',
240
+ '.cursor/mcp.json',
241
+ '.claude/mcp.json',
242
+ 'mcp.json',
243
+ ];
244
+ for (const p of mcpPaths) {
245
+ if (await fileExists(resolve(projectRoot, p))) {
246
+ info.mcpConfig = true;
247
+ info.configFiles.push(p);
248
+ }
249
+ }
250
+
251
+ // ── 6. Check for TypeScript ─────────────────────────────────
252
+ const tsConfigPath = resolve(projectRoot, 'tsconfig.json');
253
+ if (await fileExists(tsConfigPath)) {
254
+ info.language = 'typescript';
255
+ info.configFiles.push('tsconfig.json');
256
+ } else {
257
+ const jsConfigPath = resolve(projectRoot, 'jsconfig.json');
258
+ if (await fileExists(jsConfigPath)) {
259
+ info.language = 'javascript';
260
+ info.configFiles.push('jsconfig.json');
261
+ }
262
+ }
263
+
264
+ // ── 7. Detect CSS modules via src scan ──────────────────────
265
+ if (!info.styling.includes('css-modules')) {
266
+ const hasCssModules = await scanForPattern(projectRoot, /\.module\.(css|scss|sass)$/);
267
+ if (hasCssModules) {
268
+ addUnique(info.styling, 'css-modules');
269
+ }
270
+ }
271
+
272
+ // ── 8. Detect supabase-auth if supabase is present ──────────
273
+ if (info.databases.includes('supabase') && !info.auth.includes('supabase-auth')) {
274
+ addUnique(info.auth, 'supabase-auth');
275
+ }
276
+
277
+ // Deduplicate configFiles
278
+ info.configFiles = [...new Set(info.configFiles)];
279
+
280
+ // Sort arrays for stable output
281
+ for (const key of ['frameworks', 'databases', 'cms', 'deployment', 'testing', 'cicd', 'styling', 'auth', 'configFiles'] as const) {
282
+ info[key].sort();
283
+ }
284
+
285
+ // Strip empty arrays for cleaner JSON and return as RepoInfo
286
+ return cleanEmpty(info);
287
+ }
288
+
289
+ // ── Internal helpers ──────────────────────────────────────────
290
+
291
+ async function checkFiles(root: string, files: string[]): Promise<string[]> {
292
+ const found: string[] = [];
293
+ for (const f of files) {
294
+ if (await fileExists(resolve(root, f))) {
295
+ found.push(f);
296
+ }
297
+ }
298
+ return found;
299
+ }
300
+
301
+ type CategoryKey = 'frameworks' | 'databases' | 'cms' | 'deployment' | 'testing' | 'cicd' | 'styling' | 'auth';
302
+
303
+ async function detectCategory(
304
+ root: string,
305
+ rules: DetectionRule[],
306
+ info: RepoInfoInternal,
307
+ category: CategoryKey,
308
+ ): Promise<void> {
309
+ for (const rule of rules) {
310
+ if (rule.files.length === 0 && !rule.dirs?.length) continue; // package.json-only detection
311
+
312
+ const foundFiles = await checkFiles(root, rule.files);
313
+ let foundDir = false;
314
+ if (rule.dirs) {
315
+ for (const d of rule.dirs) {
316
+ if (await dirExists(resolve(root, d))) {
317
+ foundDir = true;
318
+ break;
319
+ }
320
+ }
321
+ }
322
+
323
+ if (foundFiles.length > 0 || foundDir) {
324
+ addUnique(info[category], rule.label);
325
+ info.configFiles.push(...foundFiles);
326
+ }
327
+ }
328
+ }
329
+
330
+ async function detectFromPackageJson(root: string, info: RepoInfoInternal): Promise<void> {
331
+ const pkgPath = resolve(root, 'package.json');
332
+ if (!await fileExists(pkgPath)) return;
333
+
334
+ try {
335
+ const content = await readFile(pkgPath, 'utf8');
336
+ const pkg = JSON.parse(content) as {
337
+ dependencies?: Record<string, string>;
338
+ devDependencies?: Record<string, string>;
339
+ packageManager?: string;
340
+ };
341
+
342
+ // Detect from dependencies
343
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
344
+ for (const [pkgName, detection] of Object.entries(PACKAGE_DETECTIONS)) {
345
+ if (pkgName in allDeps) {
346
+ addUnique(info[detection.category as CategoryKey], detection.label);
347
+ }
348
+ }
349
+
350
+ // Detect package manager from packageManager field (corepack)
351
+ if (pkg.packageManager && !info.packageManager) {
352
+ const pm = pkg.packageManager.split('@')[0];
353
+ if (['pnpm', 'yarn', 'bun', 'npm'].includes(pm)) {
354
+ info.packageManager = pm;
355
+ }
356
+ }
357
+
358
+ // Track package.json itself
359
+ if (!info.configFiles.includes('package.json')) {
360
+ info.configFiles.push('package.json');
361
+ }
362
+ } catch {
363
+ // Malformed package.json — skip silently
364
+ }
365
+ }
366
+
367
+ /**
368
+ * Quick scan of src/ directory (1 level deep) for file name patterns.
369
+ * Used to detect CSS modules without walking the entire tree.
370
+ */
371
+ async function scanForPattern(root: string, pattern: RegExp): Promise<boolean> {
372
+ const srcDir = resolve(root, 'src');
373
+ if (!existsSync(srcDir)) return false;
374
+
375
+ try {
376
+ const queue = [srcDir];
377
+ let depth = 0;
378
+ const maxDepth = 3;
379
+
380
+ while (queue.length > 0 && depth < maxDepth) {
381
+ const nextQueue: string[] = [];
382
+ for (const dir of queue) {
383
+ const entries = await readdir(dir, { withFileTypes: true });
384
+ for (const entry of entries) {
385
+ if (entry.name.startsWith('.') || entry.name === 'node_modules') continue;
386
+ if (entry.isFile() && pattern.test(entry.name)) return true;
387
+ if (entry.isDirectory()) nextQueue.push(resolve(dir, entry.name));
388
+ }
389
+ }
390
+ queue.length = 0;
391
+ queue.push(...nextQueue);
392
+ depth++;
393
+ }
394
+ } catch {
395
+ // Permission or read error — skip
396
+ }
397
+
398
+ return false;
399
+ }
400
+
401
+ /**
402
+ * Remove empty arrays and undefined values, returning a clean RepoInfo.
403
+ */
404
+ function cleanEmpty(info: RepoInfoInternal): RepoInfo {
405
+ const result: RepoInfo = {};
406
+
407
+ if (info.packageManager) result.packageManager = info.packageManager;
408
+ if (info.monorepo) result.monorepo = info.monorepo;
409
+ if (info.language) result.language = info.language;
410
+ if (info.mcpConfig) result.mcpConfig = info.mcpConfig;
411
+ if (info.frameworks.length > 0) result.frameworks = info.frameworks;
412
+ if (info.databases.length > 0) result.databases = info.databases;
413
+ if (info.cms.length > 0) result.cms = info.cms;
414
+ if (info.deployment.length > 0) result.deployment = info.deployment;
415
+ if (info.testing.length > 0) result.testing = info.testing;
416
+ if (info.cicd.length > 0) result.cicd = info.cicd;
417
+ if (info.styling.length > 0) result.styling = info.styling;
418
+ if (info.auth.length > 0) result.auth = info.auth;
419
+ if (info.configFiles.length > 0) result.configFiles = info.configFiles;
420
+
421
+ return result;
422
+ }
423
+
424
+ /**
425
+ * Merge user-declared stack choices into the auto-detected repoInfo.
426
+ * Adds CMS, DB, PM, and notifications from the questionnaire so
427
+ * repoInfo becomes the single combined source of truth.
428
+ */
429
+ export function mergeStackIntoRepoInfo(info: RepoInfo, stack: StackConfig): RepoInfo {
430
+ const merged = { ...info };
431
+
432
+ // CMS
433
+ if (stack.cms !== 'none') {
434
+ merged.cms = addUniqueToArray(merged.cms, stack.cms);
435
+ }
436
+
437
+ // Database
438
+ if (stack.db !== 'none') {
439
+ merged.databases = addUniqueToArray(merged.databases, stack.db);
440
+ }
441
+
442
+ // Project management
443
+ if (stack.pm !== 'none') {
444
+ merged.pm = addUniqueToArray(merged.pm, stack.pm);
445
+ }
446
+
447
+ // Notifications
448
+ if (stack.notifications !== 'none') {
449
+ merged.notifications = addUniqueToArray(merged.notifications, stack.notifications);
450
+ }
451
+
452
+ return merged;
453
+ }
454
+
455
+ function addUniqueToArray(arr: string[] | undefined, value: string): string[] {
456
+ const result = arr ? [...arr] : [];
457
+ if (!result.includes(value)) result.push(value);
458
+ return result.sort();
459
+ }
460
+
461
+ /**
462
+ * Format the detected repo info for console display.
463
+ */
464
+ export function formatRepoInfo(info: RepoInfo): string {
465
+ const lines: string[] = [];
466
+
467
+ if (info.packageManager) lines.push(`Package manager: ${info.packageManager}`);
468
+ if (info.monorepo) lines.push(`Monorepo: ${info.monorepo}`);
469
+ if (info.language) lines.push(`Language: ${info.language}`);
470
+ if (info.frameworks?.length) lines.push(`Frameworks: ${info.frameworks.join(', ')}`);
471
+ if (info.databases?.length) lines.push(`Databases: ${info.databases.join(', ')}`);
472
+ if (info.cms?.length) lines.push(`CMS: ${info.cms.join(', ')}`);
473
+ if (info.auth?.length) lines.push(`Auth: ${info.auth.join(', ')}`);
474
+ if (info.pm?.length) lines.push(`Project management: ${info.pm.join(', ')}`);
475
+ if (info.notifications?.length) lines.push(`Notifications: ${info.notifications.join(', ')}`);
476
+ if (info.deployment?.length) lines.push(`Deployment: ${info.deployment.join(', ')}`);
477
+ if (info.testing?.length) lines.push(`Testing: ${info.testing.join(', ')}`);
478
+ if (info.cicd?.length) lines.push(`CI/CD: ${info.cicd.join(', ')}`);
479
+ if (info.styling?.length) lines.push(`Styling: ${info.styling.join(', ')}`);
480
+ if (info.mcpConfig) lines.push(`MCP config: found`);
481
+
482
+ return lines.map(l => ` ${l}`).join('\n');
483
+ }
@@ -21,9 +21,6 @@ function buildBlock(managed: ManagedPaths): string {
21
21
  lines.push(p)
22
22
  }
23
23
 
24
- // Manifest file
25
- lines.push('.opencastle.json')
26
-
27
24
  // Un-ignore customizable paths so they stay tracked
28
25
  for (const p of managed.customizable) {
29
26
  lines.push(`!${p}`)
package/src/cli/init.ts CHANGED
@@ -6,6 +6,7 @@ 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 { detectRepoInfo, mergeStackIntoRepoInfo, formatRepoInfo } from './detect.js'
9
10
  import type { CliContext, IdeAdapter, CmsChoice, DbChoice, PmChoice, NotifChoice, StackConfig } from './types.js'
10
11
 
11
12
  const ADAPTERS: Record<string, () => Promise<IdeAdapter>> = {
@@ -43,6 +44,16 @@ export default async function init({ pkgRoot, args }: CliContext): Promise<void>
43
44
  ' Multi-agent orchestration framework for AI coding assistants\n'
44
45
  )
45
46
 
47
+ // ── Repo research ───────────────────────────────────────────────
48
+ console.log(' Scanning repository...')
49
+ const repoInfo = await detectRepoInfo(projectRoot)
50
+ const summary = formatRepoInfo(repoInfo)
51
+ if (summary) {
52
+ console.log(' Detected:\n' + summary + '\n')
53
+ } else {
54
+ console.log(' No tooling detected (empty project?)\n')
55
+ }
56
+
46
57
  // ── IDE selection ───────────────────────────────────────────────
47
58
  const ide = await select('Which IDE are you using?', [
48
59
  {
@@ -93,6 +104,9 @@ export default async function init({ pkgRoot, args }: CliContext): Promise<void>
93
104
 
94
105
  const stack: StackConfig = { cms: cms as CmsChoice, db: db as DbChoice, pm: pm as PmChoice, notifications: notifications as NotifChoice }
95
106
 
107
+ // ── Merge user choices into detected info ────────────────────
108
+ const combinedRepoInfo = mergeStackIntoRepoInfo(repoInfo, stack)
109
+
96
110
  console.log(`\n Installing for ${ide}...`)
97
111
  console.log(` Stack: CMS=${stack.cms}, DB=${stack.db}, PM=${stack.pm}, Notifications=${stack.notifications}\n`)
98
112
 
@@ -141,12 +155,13 @@ export default async function init({ pkgRoot, args }: CliContext): Promise<void>
141
155
 
142
156
  // ── Run adapter ─────────────────────────────────────────────────
143
157
  const adapter = await ADAPTERS[ide]()
144
- const results = await adapter.install(pkgRoot, projectRoot, stack)
158
+ const results = await adapter.install(pkgRoot, projectRoot, stack, combinedRepoInfo)
145
159
 
146
160
  // ── Write manifest ──────────────────────────────────────────────
147
161
  const manifest = createManifest(pkg.version, ide)
148
162
  manifest.managedPaths = adapter.getManagedPaths()
149
163
  manifest.stack = stack
164
+ manifest.repoInfo = combinedRepoInfo
150
165
  await writeManifest(projectRoot, manifest)
151
166
 
152
167
  // ── Update .gitignore ───────────────────────────────────────────
@@ -168,7 +183,7 @@ export default async function init({ pkgRoot, args }: CliContext): Promise<void>
168
183
  }
169
184
 
170
185
  // ── Env var notice ──────────────────────────────────────────────
171
- const envVars = getRequiredMcpEnvVars(stack)
186
+ const envVars = getRequiredMcpEnvVars(stack, combinedRepoInfo)
172
187
  if (envVars.length > 0) {
173
188
  console.log(`\n ⚠ Required environment variables for MCP servers:\n`)
174
189
  for (const { envVar, hint } of envVars) {
@@ -186,7 +201,7 @@ export default async function init({ pkgRoot, args }: CliContext): Promise<void>
186
201
  console.log(`\n Next steps:`)
187
202
  if (ide === 'vscode') {
188
203
  console.log(
189
- ' 0. Reload VS Code window (Cmd+Shift+P → "Reload Window") to pick up agents'
204
+ ' 0. Reload VS Code window (Cmd+Shift+P → "Developer: Reload Window") to pick up agents'
190
205
  )
191
206
  } else if (ide === 'cursor') {
192
207
  console.log(
package/src/cli/mcp.ts CHANGED
@@ -3,7 +3,7 @@ import { mkdir, readFile, writeFile } from 'node:fs/promises';
3
3
  import { existsSync } from 'node:fs';
4
4
  import { getOrchestratorRoot } from './copy.js';
5
5
  import { getIncludedMcpServers } from './stack-config.js';
6
- import type { ScaffoldResult, StackConfig } from './types.js';
6
+ import type { ScaffoldResult, StackConfig, RepoInfo } from './types.js';
7
7
 
8
8
  /**
9
9
  * Scaffold or merge the MCP server config into the target project.
@@ -21,7 +21,8 @@ export async function scaffoldMcpConfig(
21
21
  pkgRoot: string,
22
22
  projectRoot: string,
23
23
  destRelPath: string,
24
- stack?: StackConfig
24
+ stack?: StackConfig,
25
+ repoInfo?: RepoInfo
25
26
  ): Promise<ScaffoldResult> {
26
27
  const destPath = resolve(projectRoot, destRelPath);
27
28
 
@@ -36,7 +37,7 @@ export async function scaffoldMcpConfig(
36
37
 
37
38
  // Filter servers based on stack config
38
39
  if (stack) {
39
- const included = getIncludedMcpServers(stack);
40
+ const included = getIncludedMcpServers(stack, repoInfo);
40
41
  template.servers = Object.fromEntries(
41
42
  Object.entries(template.servers).filter(([key]) => included.has(key))
42
43
  );