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.
- package/dist/cli/adapters/claude-code.d.ts +2 -2
- package/dist/cli/adapters/claude-code.d.ts.map +1 -1
- package/dist/cli/adapters/claude-code.js +2 -2
- package/dist/cli/adapters/claude-code.js.map +1 -1
- package/dist/cli/adapters/cursor.d.ts +2 -2
- package/dist/cli/adapters/cursor.d.ts.map +1 -1
- package/dist/cli/adapters/cursor.js +2 -2
- package/dist/cli/adapters/cursor.js.map +1 -1
- package/dist/cli/adapters/vscode.d.ts +2 -2
- package/dist/cli/adapters/vscode.d.ts.map +1 -1
- package/dist/cli/adapters/vscode.js +2 -2
- package/dist/cli/adapters/vscode.js.map +1 -1
- package/dist/cli/detect.d.ts +18 -0
- package/dist/cli/detect.d.ts.map +1 -0
- package/dist/cli/detect.js +428 -0
- package/dist/cli/detect.js.map +1 -0
- package/dist/cli/gitignore.d.ts.map +1 -1
- package/dist/cli/gitignore.js +0 -2
- package/dist/cli/gitignore.js.map +1 -1
- package/dist/cli/init.d.ts.map +1 -1
- package/dist/cli/init.js +17 -3
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/mcp.d.ts +2 -2
- package/dist/cli/mcp.d.ts.map +1 -1
- package/dist/cli/mcp.js +2 -2
- package/dist/cli/mcp.js.map +1 -1
- package/dist/cli/prompt.d.ts +3 -0
- package/dist/cli/prompt.d.ts.map +1 -1
- package/dist/cli/prompt.js +96 -0
- package/dist/cli/prompt.js.map +1 -1
- package/dist/cli/stack-config.d.ts +3 -3
- package/dist/cli/stack-config.d.ts.map +1 -1
- package/dist/cli/stack-config.js +16 -5
- package/dist/cli/stack-config.js.map +1 -1
- package/dist/cli/types.d.ts +20 -1
- package/dist/cli/types.d.ts.map +1 -1
- package/dist/cli/update.d.ts.map +1 -1
- package/dist/cli/update.js +6 -0
- package/dist/cli/update.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/adapters/claude-code.ts +5 -3
- package/src/cli/adapters/cursor.ts +5 -3
- package/src/cli/adapters/vscode.ts +5 -3
- package/src/cli/detect.ts +483 -0
- package/src/cli/gitignore.ts +0 -3
- package/src/cli/init.ts +18 -3
- package/src/cli/mcp.ts +4 -3
- package/src/cli/prompt.ts +123 -0
- package/src/cli/stack-config.ts +19 -6
- package/src/cli/types.ts +21 -1
- package/src/cli/update.ts +7 -0
- package/src/dashboard/node_modules/.vite/deps/_metadata.json +6 -6
- package/src/orchestrator/agents/api-designer.agent.md +1 -1
- package/src/orchestrator/agents/content-engineer.agent.md +1 -1
- package/src/orchestrator/agents/database-engineer.agent.md +1 -1
- package/src/orchestrator/agents/developer.agent.md +1 -1
- package/src/orchestrator/agents/performance-expert.agent.md +1 -1
- package/src/orchestrator/agents/researcher.agent.md +16 -0
- package/src/orchestrator/agents/reviewer.agent.md +2 -4
- package/src/orchestrator/agents/team-lead.agent.md +41 -63
- package/src/orchestrator/agents/ui-ux-expert.agent.md +1 -1
- package/src/orchestrator/mcp.json +16 -8
- 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
|
+
}
|
package/src/cli/gitignore.ts
CHANGED
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
|
);
|