ccraft 1.0.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 (40) hide show
  1. package/bin/claude-craft.js +85 -0
  2. package/package.json +39 -0
  3. package/src/commands/auth.js +43 -0
  4. package/src/commands/create.js +543 -0
  5. package/src/commands/install.js +480 -0
  6. package/src/commands/logout.js +24 -0
  7. package/src/commands/update.js +339 -0
  8. package/src/constants.js +299 -0
  9. package/src/generators/directories.js +30 -0
  10. package/src/generators/metadata.js +57 -0
  11. package/src/generators/security.js +39 -0
  12. package/src/prompts/gather.js +308 -0
  13. package/src/ui/brand.js +62 -0
  14. package/src/ui/cards.js +179 -0
  15. package/src/ui/format.js +55 -0
  16. package/src/ui/phase-header.js +20 -0
  17. package/src/ui/prompts.js +56 -0
  18. package/src/ui/tables.js +89 -0
  19. package/src/ui/tasks.js +258 -0
  20. package/src/ui/theme.js +83 -0
  21. package/src/utils/analysis-cache.js +519 -0
  22. package/src/utils/api-client.js +253 -0
  23. package/src/utils/api-file-writer.js +197 -0
  24. package/src/utils/bootstrap-runner.js +148 -0
  25. package/src/utils/claude-analyzer.js +255 -0
  26. package/src/utils/claude-optimizer.js +341 -0
  27. package/src/utils/claude-rewriter.js +553 -0
  28. package/src/utils/claude-scorer.js +101 -0
  29. package/src/utils/description-analyzer.js +116 -0
  30. package/src/utils/detect-project.js +1276 -0
  31. package/src/utils/existing-setup.js +341 -0
  32. package/src/utils/file-writer.js +64 -0
  33. package/src/utils/json-extract.js +56 -0
  34. package/src/utils/logger.js +27 -0
  35. package/src/utils/mcp-setup.js +461 -0
  36. package/src/utils/preflight.js +112 -0
  37. package/src/utils/prompt-api-key.js +59 -0
  38. package/src/utils/run-claude.js +152 -0
  39. package/src/utils/security.js +82 -0
  40. package/src/utils/toolkit-rule-generator.js +364 -0
@@ -0,0 +1,1276 @@
1
+ import { pathExists, readJson } from 'fs-extra/esm';
2
+ import { join, basename } from 'path';
3
+ import { readdirSync, readFileSync, existsSync } from 'fs';
4
+
5
+ // ── Extension → language mapping for distribution estimation ──────────
6
+
7
+ const EXT_TO_LANGUAGE = {
8
+ '.js': 'JavaScript', '.jsx': 'JavaScript', '.mjs': 'JavaScript', '.cjs': 'JavaScript',
9
+ '.ts': 'TypeScript', '.tsx': 'TypeScript', '.mts': 'TypeScript',
10
+ '.py': 'Python',
11
+ '.go': 'Go',
12
+ '.rs': 'Rust',
13
+ '.java': 'Java', '.kt': 'Kotlin', '.kts': 'Kotlin',
14
+ '.cs': 'C#',
15
+ '.rb': 'Ruby',
16
+ '.php': 'PHP',
17
+ '.swift': 'Swift',
18
+ '.dart': 'Dart',
19
+ '.html': 'HTML', '.cshtml': 'C#', '.razor': 'C#',
20
+ '.css': 'CSS', '.scss': 'CSS', '.less': 'CSS',
21
+ '.vue': 'JavaScript', '.svelte': 'JavaScript',
22
+ '.c': 'C', '.h': 'C',
23
+ '.cpp': 'C++', '.cc': 'C++', '.cxx': 'C++', '.hpp': 'C++',
24
+ '.sql': 'SQL',
25
+ '.tf': 'HCL', '.hcl': 'HCL',
26
+ };
27
+
28
+ // ── Database detection helpers ────────────────────────────────────────
29
+
30
+ /**
31
+ * Detect databases from a dependency list using a package-to-database mapping.
32
+ * Returns deduplicated array of canonical database IDs.
33
+ */
34
+ function detectDatabases(deps, mapping) {
35
+ const databases = [];
36
+ for (const [pkg, dbId] of Object.entries(mapping)) {
37
+ if (deps.some((d) => d === pkg || d.startsWith(pkg + '/'))) {
38
+ if (!databases.includes(dbId)) databases.push(dbId);
39
+ }
40
+ }
41
+ return databases;
42
+ }
43
+
44
+ /**
45
+ * Detect databases from raw file content using substring matching.
46
+ * Returns deduplicated array of canonical database IDs.
47
+ */
48
+ function detectDatabasesFromContent(content, mapping) {
49
+ const databases = [];
50
+ for (const [pattern, dbId] of Object.entries(mapping)) {
51
+ if (content.includes(pattern) && !databases.includes(dbId)) {
52
+ databases.push(dbId);
53
+ }
54
+ }
55
+ return databases;
56
+ }
57
+
58
+ const SKIP_DIRS = new Set([
59
+ 'node_modules', '.git', 'dist', 'build', 'out', '.next', 'vendor',
60
+ '__pycache__', '.venv', 'venv', 'target', 'bin', 'obj', '.nuget',
61
+ 'coverage', '.cache', 'tmp', '.turbo', '.output',
62
+ 'templates', 'fixtures', 'testdata', 'test-fixtures',
63
+ ]);
64
+
65
+ /**
66
+ * Recursively find files matching a given extension, up to maxDepth levels.
67
+ * Returns array of absolute paths.
68
+ */
69
+ function findFilesRecursive(dir, ext, maxDepth, _depth = 0) {
70
+ if (_depth > maxDepth) return [];
71
+ const results = [];
72
+ let entries;
73
+ try { entries = readdirSync(dir, { withFileTypes: true }); } catch { return []; }
74
+ for (const entry of entries) {
75
+ if (entry.isFile() && entry.name.endsWith(ext)) {
76
+ results.push(join(dir, entry.name));
77
+ } else if (entry.isDirectory() && !SKIP_DIRS.has(entry.name) && !entry.name.startsWith('.')) {
78
+ results.push(...findFilesRecursive(join(dir, entry.name), ext, maxDepth, _depth + 1));
79
+ }
80
+ }
81
+ return results;
82
+ }
83
+
84
+ /**
85
+ * Estimate language distribution by counting source files per extension.
86
+ * Returns { "C#": 72, "JavaScript": 18, ... } or null if no source files found.
87
+ */
88
+ function estimateLanguageDistribution(dir) {
89
+ const counts = {};
90
+
91
+ function walkSync(currentDir, depth) {
92
+ if (depth > 6) return;
93
+ let entries;
94
+ try { entries = readdirSync(currentDir, { withFileTypes: true }); } catch { return; }
95
+ for (const entry of entries) {
96
+ if (entry.isDirectory()) {
97
+ if (!SKIP_DIRS.has(entry.name) && !entry.name.startsWith('.')) {
98
+ walkSync(join(currentDir, entry.name), depth + 1);
99
+ }
100
+ } else if (entry.isFile()) {
101
+ const dotIdx = entry.name.lastIndexOf('.');
102
+ if (dotIdx === -1) continue;
103
+ const ext = entry.name.slice(dotIdx);
104
+ const lang = EXT_TO_LANGUAGE[ext];
105
+ if (lang) {
106
+ counts[lang] = (counts[lang] || 0) + 1;
107
+ }
108
+ }
109
+ }
110
+ }
111
+
112
+ walkSync(dir, 0);
113
+
114
+ const total = Object.values(counts).reduce((s, c) => s + c, 0);
115
+ if (total === 0) return null;
116
+
117
+ const distribution = {};
118
+ for (const [lang, count] of Object.entries(counts)) {
119
+ const pct = Math.round((count / total) * 100);
120
+ if (pct >= 2) {
121
+ distribution[lang] = pct;
122
+ }
123
+ }
124
+
125
+ return Object.keys(distribution).length > 0 ? distribution : null;
126
+ }
127
+
128
+ // ── Language / framework detectors ────────────────────────────────────
129
+
130
+ const DETECTORS = [
131
+ {
132
+ file: 'package.json',
133
+ detect: async (dir) => {
134
+ const pkg = await readJson(join(dir, 'package.json')).catch(() => null);
135
+ if (!pkg) return null;
136
+ const result = { languages: [], frameworks: [], packageManager: 'npm' };
137
+
138
+ if (
139
+ pkg.devDependencies?.typescript ||
140
+ pkg.dependencies?.typescript ||
141
+ (await pathExists(join(dir, 'tsconfig.json')))
142
+ ) {
143
+ result.languages.push('TypeScript');
144
+ } else {
145
+ result.languages.push('JavaScript');
146
+ }
147
+
148
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
149
+ result._allDependencies = Object.keys(allDeps);
150
+
151
+ if (allDeps.next) result.frameworks.push('Next.js');
152
+ if (allDeps.react && !allDeps.next) result.frameworks.push('React');
153
+ if (allDeps.vue) result.frameworks.push('Vue');
154
+ if (allDeps['@angular/core']) result.frameworks.push('Angular');
155
+ if (allDeps.express) result.frameworks.push('Express');
156
+ if (allDeps.fastify) result.frameworks.push('Fastify');
157
+ if (allDeps.tailwindcss) result.frameworks.push('Tailwind CSS');
158
+
159
+ // Expanded framework detection
160
+ if (allDeps.svelte || allDeps['@sveltejs/kit']) result.frameworks.push('SvelteKit');
161
+ if (allDeps.nuxt) result.frameworks.push('Nuxt');
162
+ if (allDeps.astro) result.frameworks.push('Astro');
163
+ if (allDeps.gatsby) result.frameworks.push('Gatsby');
164
+ if (allDeps['@nestjs/core']) result.frameworks.push('NestJS');
165
+ if (allDeps['@remix-run/react']) result.frameworks.push('Remix');
166
+ if (allDeps['socket.io'] || allDeps.ws) result.frameworks.push('Socket.io');
167
+ if (allDeps.electron) result.frameworks.push('Electron');
168
+ if (allDeps['react-native']) result.frameworks.push('React Native');
169
+ if (allDeps.expo) result.frameworks.push('Expo');
170
+ if (allDeps['@tauri-apps/api']) result.frameworks.push('Tauri');
171
+ if (allDeps.hardhat) result.frameworks.push('Hardhat');
172
+ if (allDeps.ethers || allDeps.viem || allDeps.web3) result.frameworks.push('Web3');
173
+ if (allDeps['@apollo/server'] || allDeps['graphql-yoga'] || allDeps['type-graphql']) result.frameworks.push('GraphQL');
174
+ if (allDeps.langchain || allDeps['@langchain/core']) result.frameworks.push('LangChain');
175
+ if (allDeps.openai) result.frameworks.push('OpenAI SDK');
176
+ if (allDeps.phaser) result.frameworks.push('Phaser');
177
+ if (allDeps.three) result.frameworks.push('Three.js');
178
+ if (allDeps.prisma || allDeps['@prisma/client']) result.frameworks.push('Prisma');
179
+ if (allDeps['drizzle-orm']) result.frameworks.push('Drizzle');
180
+ if (allDeps['single-spa']) result.frameworks.push('Single-SPA');
181
+ if (allDeps.commander || allDeps.yargs) result.frameworks.push('CLI Framework');
182
+ if (allDeps['@playwright/test'] || allDeps.playwright) result.frameworks.push('Playwright');
183
+ if (allDeps['discord.js']) result.frameworks.push('Discord.js');
184
+ if (allDeps.ai) result.frameworks.push('Vercel AI SDK');
185
+ if (allDeps['@docusaurus/core']) result.frameworks.push('Docusaurus');
186
+ if (allDeps['@babylonjs/core'] || allDeps.babylonjs) result.frameworks.push('Babylon.js');
187
+
188
+ // CMS / E-commerce
189
+ if (allDeps['@wordpress/scripts'] || allDeps['@wordpress/blocks'] || allDeps['@wordpress/element']) result.frameworks.push('WordPress');
190
+ if (allDeps['@shopify/hydrogen']) result.frameworks.push('Shopify');
191
+ if (allDeps['@shopify/polaris'] || allDeps['@shopify/cli'] || allDeps['@shopify/theme']) result.frameworks.push('Shopify');
192
+
193
+ // Database detection from npm dependencies
194
+ const dbMap = {
195
+ pg: 'postgresql', postgres: 'postgresql', '@types/pg': 'postgresql',
196
+ mysql2: 'mysql', mysql: 'mysql',
197
+ mongodb: 'mongodb', mongoose: 'mongodb',
198
+ mssql: 'sql-server', tedious: 'sql-server',
199
+ 'better-sqlite3': 'sqlite', 'sql.js': 'sqlite',
200
+ redis: 'redis', ioredis: 'redis', '@upstash/redis': 'redis',
201
+ '@aws-sdk/client-dynamodb': 'dynamodb',
202
+ 'neo4j-driver': 'neo4j',
203
+ '@elastic/elasticsearch': 'elasticsearch',
204
+ };
205
+ result.databases = detectDatabases(Object.keys(allDeps), dbMap);
206
+
207
+ if (await pathExists(join(dir, 'pnpm-lock.yaml'))) result.packageManager = 'pnpm';
208
+ else if (await pathExists(join(dir, 'yarn.lock'))) result.packageManager = 'yarn';
209
+ else if (await pathExists(join(dir, 'bun.lockb'))) result.packageManager = 'bun';
210
+
211
+ return { name: pkg.name, description: pkg.description, ...result };
212
+ },
213
+ },
214
+ {
215
+ file: 'go.mod',
216
+ detect: async (dir) => {
217
+ const frameworks = [];
218
+ const _allDependencies = [];
219
+ try {
220
+ const content = readFileSync(join(dir, 'go.mod'), 'utf8');
221
+ // Extract module paths from require blocks
222
+ const reqMatches = content.matchAll(/^\s+([\w./-]+)\s/gm);
223
+ for (const m of reqMatches) _allDependencies.push(m[1]);
224
+ if (content.includes('github.com/gin-gonic/gin')) frameworks.push('Gin');
225
+ if (content.includes('github.com/labstack/echo')) frameworks.push('Echo');
226
+ if (content.includes('github.com/spf13/cobra')) frameworks.push('Cobra');
227
+ if (content.includes('github.com/gorilla/websocket')) frameworks.push('WebSocket');
228
+ if (content.includes('github.com/gofiber/fiber')) frameworks.push('Fiber');
229
+ } catch {}
230
+ // Database detection from Go modules
231
+ const databases = detectDatabasesFromContent(content, {
232
+ 'github.com/lib/pq': 'postgresql', 'github.com/jackc/pgx': 'postgresql',
233
+ 'go.mongodb.org/mongo-driver': 'mongodb',
234
+ 'github.com/go-sql-driver/mysql': 'mysql',
235
+ 'github.com/microsoft/go-mssqldb': 'sql-server', 'github.com/denisenkom/go-mssqldb': 'sql-server',
236
+ 'github.com/mattn/go-sqlite3': 'sqlite', 'modernc.org/sqlite': 'sqlite',
237
+ 'github.com/redis/go-redis': 'redis', 'github.com/go-redis/redis': 'redis',
238
+ });
239
+ return { languages: ['Go'], frameworks, _allDependencies, databases };
240
+ },
241
+ },
242
+ {
243
+ file: 'Cargo.toml',
244
+ detect: async (dir) => {
245
+ const frameworks = [];
246
+ const _allDependencies = [];
247
+ try {
248
+ const content = readFileSync(join(dir, 'Cargo.toml'), 'utf8');
249
+ // Extract crate names from [dependencies] section
250
+ const depMatches = content.matchAll(/^([\w-]+)\s*=/gm);
251
+ for (const m of depMatches) _allDependencies.push(m[1]);
252
+ if (content.includes('actix')) frameworks.push('Actix');
253
+ if (content.includes('tokio')) frameworks.push('Tokio');
254
+ if (content.includes('axum')) frameworks.push('Axum');
255
+ if (content.includes('warp')) frameworks.push('Warp');
256
+ if (content.includes('clap')) frameworks.push('Clap');
257
+ if (content.includes('tauri')) frameworks.push('Tauri');
258
+ } catch {}
259
+ // Database detection from Rust crates
260
+ const databases = detectDatabases(_allDependencies, {
261
+ 'tokio-postgres': 'postgresql', sqlx: 'postgresql',
262
+ mongodb: 'mongodb',
263
+ redis: 'redis',
264
+ rusqlite: 'sqlite',
265
+ diesel: 'postgresql',
266
+ });
267
+ return { languages: ['Rust'], frameworks, _allDependencies, databases };
268
+ },
269
+ },
270
+ {
271
+ file: 'pyproject.toml',
272
+ detect: async (dir) => {
273
+ const frameworks = [];
274
+ const _allDependencies = [];
275
+ try {
276
+ const content = readFileSync(join(dir, 'pyproject.toml'), 'utf8');
277
+ // Extract dependency names from dependencies array
278
+ const depMatches = content.matchAll(/["']([\w-]+)(?:[><=!~\s]|$)/gm);
279
+ for (const m of depMatches) _allDependencies.push(m[1].toLowerCase());
280
+ if (content.includes('django')) frameworks.push('Django');
281
+ if (content.includes('flask')) frameworks.push('Flask');
282
+ if (content.includes('fastapi')) frameworks.push('FastAPI');
283
+ if (/\btorch\b/.test(content) || content.includes('pytorch')) frameworks.push('PyTorch');
284
+ if (content.includes('tensorflow')) frameworks.push('TensorFlow');
285
+ if (content.includes('scikit-learn') || content.includes('sklearn')) frameworks.push('scikit-learn');
286
+ if (content.includes('langchain')) frameworks.push('LangChain');
287
+ if (content.includes('streamlit')) frameworks.push('Streamlit');
288
+ if (content.includes('airflow')) frameworks.push('Airflow');
289
+ if (content.includes('pandas')) frameworks.push('Pandas');
290
+ if (content.includes('pydantic')) frameworks.push('Pydantic');
291
+ if (content.includes('scrapy')) frameworks.push('Scrapy');
292
+ if (content.includes('click') || content.includes('typer')) frameworks.push('CLI Framework');
293
+ if (content.includes('huggingface-hub') || content.includes('transformers')) frameworks.push('Hugging Face');
294
+ if (content.includes('dbt-core')) frameworks.push('dbt');
295
+ if (content.includes('llama-index') || content.includes('llama_index')) frameworks.push('LlamaIndex');
296
+ if (/["']dash["']/.test(content)) frameworks.push('Dash');
297
+ if (content.includes('graphene')) frameworks.push('Graphene');
298
+ } catch {}
299
+ // Database detection from Python dependencies
300
+ const databases = detectDatabases(_allDependencies, {
301
+ psycopg2: 'postgresql', 'psycopg2-binary': 'postgresql', psycopg: 'postgresql', asyncpg: 'postgresql',
302
+ pymongo: 'mongodb', motor: 'mongodb',
303
+ 'mysql-connector-python': 'mysql', mysqlclient: 'mysql', pymysql: 'mysql',
304
+ pyodbc: 'sql-server', pymssql: 'sql-server',
305
+ redis: 'redis', aioredis: 'redis',
306
+ });
307
+ return { languages: ['Python'], frameworks, _allDependencies, databases };
308
+ },
309
+ },
310
+ {
311
+ file: 'requirements.txt',
312
+ detect: async (dir) => {
313
+ const frameworks = [];
314
+ const _allDependencies = [];
315
+ try {
316
+ const content = readFileSync(join(dir, 'requirements.txt'), 'utf8');
317
+ const lines = content.split('\n');
318
+ for (const line of lines) {
319
+ const name = line.trim().split(/[><=!~\s\[;#]/)[0];
320
+ if (name) _allDependencies.push(name.toLowerCase());
321
+ }
322
+ const lc = content.toLowerCase();
323
+ if (lc.includes('django')) frameworks.push('Django');
324
+ if (lc.includes('flask')) frameworks.push('Flask');
325
+ if (lc.includes('fastapi')) frameworks.push('FastAPI');
326
+ if (/\btorch\b/.test(lc) || lc.includes('pytorch')) frameworks.push('PyTorch');
327
+ if (lc.includes('tensorflow')) frameworks.push('TensorFlow');
328
+ if (lc.includes('scikit-learn') || lc.includes('sklearn')) frameworks.push('scikit-learn');
329
+ if (lc.includes('langchain')) frameworks.push('LangChain');
330
+ if (lc.includes('streamlit')) frameworks.push('Streamlit');
331
+ if (lc.includes('airflow')) frameworks.push('Airflow');
332
+ if (lc.includes('pandas')) frameworks.push('Pandas');
333
+ if (lc.includes('pydantic')) frameworks.push('Pydantic');
334
+ if (lc.includes('scrapy')) frameworks.push('Scrapy');
335
+ if (lc.includes('click') || lc.includes('typer')) frameworks.push('CLI Framework');
336
+ if (lc.includes('huggingface-hub') || lc.includes('transformers')) frameworks.push('Hugging Face');
337
+ if (lc.includes('dbt-core')) frameworks.push('dbt');
338
+ if (lc.includes('llama-index') || lc.includes('llama_index')) frameworks.push('LlamaIndex');
339
+ if (_allDependencies.includes('dash')) frameworks.push('Dash');
340
+ if (lc.includes('graphene')) frameworks.push('Graphene');
341
+ } catch {}
342
+ // Database detection from Python dependencies
343
+ const databases = detectDatabases(_allDependencies, {
344
+ psycopg2: 'postgresql', 'psycopg2-binary': 'postgresql', psycopg: 'postgresql', asyncpg: 'postgresql',
345
+ pymongo: 'mongodb', motor: 'mongodb',
346
+ 'mysql-connector-python': 'mysql', mysqlclient: 'mysql', pymysql: 'mysql',
347
+ pyodbc: 'sql-server', pymssql: 'sql-server',
348
+ redis: 'redis', aioredis: 'redis',
349
+ });
350
+ return { languages: ['Python'], frameworks, _allDependencies, databases };
351
+ },
352
+ },
353
+ {
354
+ file: 'pom.xml',
355
+ detect: async (dir) => {
356
+ const frameworks = ['Spring Boot'];
357
+ const databases = [];
358
+ try {
359
+ const content = readFileSync(join(dir, 'pom.xml'), 'utf8');
360
+ const dbMap = {
361
+ 'org.postgresql': 'postgresql', postgresql: 'postgresql',
362
+ 'com.mysql': 'mysql', 'mysql-connector': 'mysql',
363
+ 'com.microsoft.sqlserver': 'sql-server', 'mssql-jdbc': 'sql-server',
364
+ 'mongodb-driver': 'mongodb', 'spring-boot-starter-data-mongodb': 'mongodb',
365
+ 'spring-boot-starter-data-redis': 'redis', 'jedis': 'redis',
366
+ 'com.h2database': 'sqlite',
367
+ };
368
+ for (const [pattern, dbId] of Object.entries(dbMap)) {
369
+ if (content.includes(pattern) && !databases.includes(dbId)) databases.push(dbId);
370
+ }
371
+ } catch {}
372
+ return { languages: ['Java'], frameworks, databases };
373
+ },
374
+ },
375
+ {
376
+ file: 'build.gradle',
377
+ detect: async (dir) => {
378
+ const languages = ['Java'];
379
+ const frameworks = [];
380
+ const databases = [];
381
+ try {
382
+ const content = readFileSync(join(dir, 'build.gradle'), 'utf8');
383
+ if (content.includes('kotlin')) languages.push('Kotlin');
384
+ if (content.includes('spring')) frameworks.push('Spring Boot');
385
+ if (content.includes('compose')) frameworks.push('Jetpack Compose');
386
+ // Database detection
387
+ for (const [pattern, dbId] of Object.entries({
388
+ 'org.postgresql': 'postgresql', postgresql: 'postgresql',
389
+ 'com.mysql': 'mysql', 'mysql-connector': 'mysql',
390
+ 'com.microsoft.sqlserver': 'sql-server', 'mssql-jdbc': 'sql-server',
391
+ 'mongodb-driver': 'mongodb', 'spring-boot-starter-data-mongodb': 'mongodb',
392
+ 'spring-boot-starter-data-redis': 'redis', jedis: 'redis',
393
+ 'com.h2database': 'sqlite',
394
+ })) {
395
+ if (content.includes(pattern) && !databases.includes(dbId)) databases.push(dbId);
396
+ }
397
+ } catch {}
398
+ return { languages, frameworks, databases };
399
+ },
400
+ },
401
+ {
402
+ file: 'build.gradle.kts',
403
+ detect: async (dir) => {
404
+ const frameworks = [];
405
+ const databases = [];
406
+ try {
407
+ const content = readFileSync(join(dir, 'build.gradle.kts'), 'utf8');
408
+ if (content.includes('spring')) frameworks.push('Spring Boot');
409
+ if (content.includes('compose')) frameworks.push('Jetpack Compose');
410
+ // Database detection
411
+ for (const [pattern, dbId] of Object.entries({
412
+ 'org.postgresql': 'postgresql', postgresql: 'postgresql',
413
+ 'com.mysql': 'mysql', 'mysql-connector': 'mysql',
414
+ 'com.microsoft.sqlserver': 'sql-server', 'mssql-jdbc': 'sql-server',
415
+ 'mongodb-driver': 'mongodb', 'spring-boot-starter-data-mongodb': 'mongodb',
416
+ 'spring-boot-starter-data-redis': 'redis', jedis: 'redis',
417
+ 'com.h2database': 'sqlite',
418
+ })) {
419
+ if (content.includes(pattern) && !databases.includes(dbId)) databases.push(dbId);
420
+ }
421
+ } catch {}
422
+ return { languages: ['Kotlin', 'Java'], frameworks, databases };
423
+ },
424
+ },
425
+ {
426
+ file: '*.csproj',
427
+ detect: async (dir) => {
428
+ const result = { languages: ['C#'], frameworks: [], databases: [] };
429
+ try {
430
+ const csprojFiles = findFilesRecursive(dir, '.csproj', 3);
431
+ // Read ALL .csproj files (not just the first) to catch database packages in any project
432
+ for (const csprojPath of csprojFiles) {
433
+ try {
434
+ const content = readFileSync(csprojPath, 'utf8');
435
+ if (content.includes('Microsoft.AspNetCore') || content.includes('Microsoft.NET.Sdk.Web')) {
436
+ if (!result.frameworks.includes('ASP.NET Core')) result.frameworks.push('ASP.NET Core');
437
+ }
438
+ if ((content.includes('Maui') || content.includes('Microsoft.Maui')) && !result.frameworks.includes('.NET MAUI')) {
439
+ result.frameworks.push('.NET MAUI');
440
+ }
441
+ if (content.includes('HotChocolate') && !result.frameworks.includes('HotChocolate')) result.frameworks.push('HotChocolate');
442
+ if (content.includes('SignalR') && !result.frameworks.includes('SignalR')) result.frameworks.push('SignalR');
443
+
444
+ // Database detection from NuGet PackageReference elements
445
+ const dbDetected = detectDatabasesFromContent(content, {
446
+ 'EntityFrameworkCore.SqlServer': 'sql-server',
447
+ 'Microsoft.Data.SqlClient': 'sql-server',
448
+ 'System.Data.SqlClient': 'sql-server',
449
+ 'Npgsql': 'postgresql',
450
+ 'EntityFrameworkCore.PostgreSQL': 'postgresql',
451
+ 'Pomelo.EntityFrameworkCore.MySql': 'mysql',
452
+ 'MySql.Data': 'mysql',
453
+ 'MySqlConnector': 'mysql',
454
+ 'EntityFrameworkCore.Sqlite': 'sqlite',
455
+ 'Microsoft.Data.Sqlite': 'sqlite',
456
+ 'MongoDB.Driver': 'mongodb',
457
+ 'StackExchange.Redis': 'redis',
458
+ 'Microsoft.Extensions.Caching.StackExchangeRedis': 'redis',
459
+ 'EntityFrameworkCore.Cosmos': 'cosmosdb',
460
+ 'Microsoft.Azure.Cosmos': 'cosmosdb',
461
+ });
462
+ for (const db of dbDetected) {
463
+ if (!result.databases.includes(db)) result.databases.push(db);
464
+ }
465
+
466
+ // Detect Entity Framework Core ORM
467
+ if (content.includes('Microsoft.EntityFrameworkCore') && !result.frameworks.includes('Entity Framework Core')) {
468
+ result.frameworks.push('Entity Framework Core');
469
+ }
470
+ } catch {}
471
+ }
472
+ // Detect Razor / Blazor by scanning for .cshtml / .razor files
473
+ try {
474
+ const allFiles = readdirSync(dir, { recursive: true }).map(String);
475
+ if (allFiles.some((f) => f.endsWith('.cshtml'))) {
476
+ if (!result.frameworks.includes('ASP.NET Core')) result.frameworks.push('ASP.NET Core');
477
+ if (!result.frameworks.includes('Razor')) result.frameworks.push('Razor');
478
+ }
479
+ if (allFiles.some((f) => f.endsWith('.razor'))) {
480
+ if (!result.frameworks.includes('Blazor')) result.frameworks.push('Blazor');
481
+ }
482
+ } catch {}
483
+ } catch {}
484
+ return result;
485
+ },
486
+ checkExists: async (dir) => {
487
+ try { return findFilesRecursive(dir, '.csproj', 3).length > 0; } catch { return false; }
488
+ },
489
+ },
490
+ {
491
+ file: '*.sln',
492
+ detect: async (dir) => {
493
+ const result = { languages: ['C#'], frameworks: [], databases: [] };
494
+ try {
495
+ const csprojFiles = findFilesRecursive(dir, '.csproj', 3);
496
+ for (const csprojPath of csprojFiles) {
497
+ try {
498
+ const content = readFileSync(csprojPath, 'utf8');
499
+ if (content.includes('Microsoft.AspNetCore') || content.includes('Microsoft.NET.Sdk.Web')) {
500
+ if (!result.frameworks.includes('ASP.NET Core')) result.frameworks.push('ASP.NET Core');
501
+ }
502
+ if ((content.includes('Maui') || content.includes('Microsoft.Maui')) && !result.frameworks.includes('.NET MAUI')) {
503
+ result.frameworks.push('.NET MAUI');
504
+ }
505
+
506
+ // Database detection from NuGet PackageReference elements
507
+ const dbDetected = detectDatabasesFromContent(content, {
508
+ 'EntityFrameworkCore.SqlServer': 'sql-server',
509
+ 'Microsoft.Data.SqlClient': 'sql-server',
510
+ 'System.Data.SqlClient': 'sql-server',
511
+ 'Npgsql': 'postgresql',
512
+ 'EntityFrameworkCore.PostgreSQL': 'postgresql',
513
+ 'Pomelo.EntityFrameworkCore.MySql': 'mysql',
514
+ 'MySql.Data': 'mysql',
515
+ 'MySqlConnector': 'mysql',
516
+ 'EntityFrameworkCore.Sqlite': 'sqlite',
517
+ 'Microsoft.Data.Sqlite': 'sqlite',
518
+ 'MongoDB.Driver': 'mongodb',
519
+ 'StackExchange.Redis': 'redis',
520
+ 'Microsoft.Extensions.Caching.StackExchangeRedis': 'redis',
521
+ 'EntityFrameworkCore.Cosmos': 'cosmosdb',
522
+ 'Microsoft.Azure.Cosmos': 'cosmosdb',
523
+ });
524
+ for (const db of dbDetected) {
525
+ if (!result.databases.includes(db)) result.databases.push(db);
526
+ }
527
+
528
+ // Detect Entity Framework Core ORM
529
+ if (content.includes('Microsoft.EntityFrameworkCore') && !result.frameworks.includes('Entity Framework Core')) {
530
+ result.frameworks.push('Entity Framework Core');
531
+ }
532
+ } catch {}
533
+ }
534
+ // Detect Razor / Blazor by scanning for .cshtml / .razor files
535
+ try {
536
+ const allFiles = readdirSync(dir, { recursive: true }).map(String);
537
+ if (allFiles.some((f) => f.endsWith('.cshtml'))) {
538
+ if (!result.frameworks.includes('ASP.NET Core')) result.frameworks.push('ASP.NET Core');
539
+ if (!result.frameworks.includes('Razor')) result.frameworks.push('Razor');
540
+ }
541
+ if (allFiles.some((f) => f.endsWith('.razor'))) {
542
+ if (!result.frameworks.includes('Blazor')) result.frameworks.push('Blazor');
543
+ }
544
+ } catch {}
545
+ } catch {}
546
+ return result;
547
+ },
548
+ checkExists: async (dir) => {
549
+ try { return readdirSync(dir).some((f) => f.endsWith('.sln')); } catch { return false; }
550
+ },
551
+ },
552
+ {
553
+ file: 'Gemfile',
554
+ detect: async (dir) => {
555
+ const frameworks = [];
556
+ const databases = [];
557
+ try {
558
+ const content = readFileSync(join(dir, 'Gemfile'), 'utf8');
559
+ if (content.includes('rails')) frameworks.push('Rails');
560
+ // Database detection from Gemfile
561
+ const dbMap = {
562
+ "'pg'": 'postgresql', '"pg"': 'postgresql',
563
+ "'mysql2'": 'mysql', '"mysql2"': 'mysql',
564
+ "'sqlite3'": 'sqlite', '"sqlite3"': 'sqlite',
565
+ "'mongoid'": 'mongodb', '"mongoid"': 'mongodb', "'mongo'": 'mongodb', '"mongo"': 'mongodb',
566
+ "'redis'": 'redis', '"redis"': 'redis',
567
+ };
568
+ for (const [pattern, dbId] of Object.entries(dbMap)) {
569
+ if (content.includes(pattern) && !databases.includes(dbId)) databases.push(dbId);
570
+ }
571
+ } catch {}
572
+ return { languages: ['Ruby'], frameworks, databases };
573
+ },
574
+ },
575
+ {
576
+ file: 'composer.json',
577
+ detect: async (dir) => {
578
+ const frameworks = [];
579
+ const databases = [];
580
+ try {
581
+ const pkg = await readJson(join(dir, 'composer.json')).catch(() => null);
582
+ if (pkg) {
583
+ const allDeps = { ...pkg.require, ...pkg['require-dev'] };
584
+ if (allDeps['laravel/framework']) frameworks.push('Laravel');
585
+ if (pkg.type === 'wordpress-plugin' || pkg.type === 'wordpress-theme') frameworks.push('WordPress');
586
+ if (allDeps['wpackagist-plugin/woocommerce'] || allDeps['johnpbloch/wordpress-core'] || allDeps['roots/wordpress']) frameworks.push('WordPress');
587
+ // Database detection
588
+ const dbMap = {
589
+ 'predis/predis': 'redis', 'phpredis/phpredis': 'redis',
590
+ 'mongodb/mongodb': 'mongodb',
591
+ 'ext-pgsql': 'postgresql',
592
+ 'ext-mysqli': 'mysql',
593
+ 'ext-mongodb': 'mongodb',
594
+ 'ext-redis': 'redis',
595
+ 'ext-sqlite3': 'sqlite',
596
+ };
597
+ for (const [pkg, dbId] of Object.entries(dbMap)) {
598
+ if (allDeps[pkg] && !databases.includes(dbId)) databases.push(dbId);
599
+ }
600
+ }
601
+ } catch {}
602
+ return { languages: ['PHP'], frameworks, databases };
603
+ },
604
+ },
605
+ // ── New detectors ─────────────────────────────────────────────────────
606
+ {
607
+ file: 'pubspec.yaml',
608
+ detect: async (dir) => {
609
+ const frameworks = [];
610
+ try {
611
+ const content = readFileSync(join(dir, 'pubspec.yaml'), 'utf8');
612
+ if (content.includes('flutter')) frameworks.push('Flutter');
613
+ } catch {}
614
+ return { languages: ['Dart'], frameworks };
615
+ },
616
+ },
617
+ {
618
+ file: 'foundry.toml',
619
+ detect: async () => ({ languages: ['Solidity'], frameworks: ['Foundry'] }),
620
+ },
621
+ {
622
+ file: 'hardhat.config.js',
623
+ detect: async () => ({ languages: ['Solidity'], frameworks: ['Hardhat'] }),
624
+ checkExists: async (dir) => {
625
+ try {
626
+ return readdirSync(dir).some((f) => f.startsWith('hardhat.config'));
627
+ } catch { return false; }
628
+ },
629
+ },
630
+ {
631
+ file: 'serverless.yml',
632
+ detect: async () => ({ languages: [], frameworks: ['Serverless Framework'] }),
633
+ checkExists: async (dir) => {
634
+ try {
635
+ return readdirSync(dir).some((f) => f.startsWith('serverless.'));
636
+ } catch { return false; }
637
+ },
638
+ },
639
+ {
640
+ file: 'ProjectSettings/ProjectVersion.txt',
641
+ detect: async () => ({ languages: ['C#'], frameworks: ['Unity'] }),
642
+ },
643
+ {
644
+ file: 'manifest.json',
645
+ detect: async (dir) => {
646
+ try {
647
+ const manifest = await readJson(join(dir, 'manifest.json')).catch(() => null);
648
+ if (manifest && (manifest.content_scripts || manifest.browser_action || manifest.action)) {
649
+ return { languages: [], frameworks: ['Browser Extension'] };
650
+ }
651
+ } catch {}
652
+ return null;
653
+ },
654
+ },
655
+ {
656
+ file: '*.tf',
657
+ detect: async () => ({ languages: ['HCL'], frameworks: ['Terraform'] }),
658
+ checkExists: async (dir) => {
659
+ try { return readdirSync(dir).some((f) => f.endsWith('.tf')); } catch { return false; }
660
+ },
661
+ },
662
+ {
663
+ file: 'hugo.toml',
664
+ detect: async (dir) => {
665
+ return { languages: ['Go'], frameworks: ['Hugo'] };
666
+ },
667
+ checkExists: async (dir) => {
668
+ try {
669
+ if (existsSync(join(dir, 'hugo.toml'))) return true;
670
+ if (existsSync(join(dir, 'hugo.yaml'))) return true;
671
+ if (existsSync(join(dir, 'hugo.json'))) return true;
672
+ // config.toml with Hugo markers
673
+ if (existsSync(join(dir, 'config.toml'))) {
674
+ const content = readFileSync(join(dir, 'config.toml'), 'utf8');
675
+ if (content.includes('baseURL') && (content.includes('theme') || existsSync(join(dir, 'themes')))) return true;
676
+ }
677
+ return false;
678
+ } catch { return false; }
679
+ },
680
+ },
681
+ {
682
+ file: '_config.yml',
683
+ detect: async (dir) => {
684
+ try {
685
+ // Verify it's Jekyll: check Gemfile for jekyll or config for Jekyll-specific keys
686
+ let isJekyll = false;
687
+ if (existsSync(join(dir, 'Gemfile'))) {
688
+ const gemfile = readFileSync(join(dir, 'Gemfile'), 'utf8');
689
+ if (gemfile.includes('jekyll')) isJekyll = true;
690
+ }
691
+ if (!isJekyll) {
692
+ const config = readFileSync(join(dir, '_config.yml'), 'utf8');
693
+ if (config.includes('permalink') || config.includes('collections') || config.includes('kramdown')) isJekyll = true;
694
+ }
695
+ if (isJekyll) return { languages: ['Ruby'], frameworks: ['Jekyll'] };
696
+ } catch {}
697
+ return null;
698
+ },
699
+ },
700
+ {
701
+ file: 'dbt_project.yml',
702
+ detect: async () => ({ languages: ['SQL'], frameworks: ['dbt'] }),
703
+ },
704
+ {
705
+ file: 'project.godot',
706
+ detect: async () => ({ languages: ['GDScript'], frameworks: ['Godot'] }),
707
+ },
708
+ // ── WordPress detectors ──────────────────────────────────────────────
709
+ {
710
+ file: 'style.css',
711
+ detect: async (dir) => {
712
+ try {
713
+ const content = readFileSync(join(dir, 'style.css'), 'utf8');
714
+ // WordPress theme: style.css has "Theme Name:" in a comment header
715
+ if (/Theme Name\s*:/i.test(content.slice(0, 2000))) {
716
+ return { languages: ['PHP'], frameworks: ['WordPress'] };
717
+ }
718
+ } catch {}
719
+ return null;
720
+ },
721
+ },
722
+ {
723
+ file: 'functions.php',
724
+ detect: async (dir) => {
725
+ try {
726
+ const content = readFileSync(join(dir, 'functions.php'), 'utf8');
727
+ // Must contain WordPress-specific hooks to avoid generic PHP match
728
+ if ((content.includes('add_action') && content.includes('wp_enqueue')) || content.includes('add_theme_support')) {
729
+ return { languages: ['PHP'], frameworks: ['WordPress'] };
730
+ }
731
+ } catch {}
732
+ return null;
733
+ },
734
+ },
735
+ {
736
+ file: 'theme.json',
737
+ detect: async (dir) => {
738
+ try {
739
+ const content = readFileSync(join(dir, 'theme.json'), 'utf8');
740
+ if (content.includes('schemas.wp.org') || (content.includes('"version"') && content.includes('"settings"') && content.includes('"styles"'))) {
741
+ return { languages: ['PHP'], frameworks: ['WordPress'] };
742
+ }
743
+ } catch {}
744
+ return null;
745
+ },
746
+ },
747
+ // ── C / C++ detectors ────────────────────────────────────────────────
748
+ {
749
+ file: 'CMakeLists.txt',
750
+ detect: async (dir) => {
751
+ const languages = [];
752
+ const frameworks = ['CMake'];
753
+ try {
754
+ const content = readFileSync(join(dir, 'CMakeLists.txt'), 'utf8');
755
+ // Detect C++ vs C
756
+ if (/CXX|CMAKE_CXX_STANDARD|\.cpp|\.cc|\.cxx/i.test(content)) {
757
+ languages.push('C++');
758
+ // Also add C if explicitly listed
759
+ if (/\bC\b/.test(content.match(/project\s*\([^)]*LANGUAGES\s+([^)]*)\)/i)?.[1] || '')) {
760
+ languages.push('C');
761
+ }
762
+ } else {
763
+ languages.push('C');
764
+ }
765
+ // Detect frameworks from find_package / FetchContent_Declare
766
+ const pkgMatches = content.matchAll(/(?:find_package|FetchContent_Declare)\s*\(\s*(\w+)/gi);
767
+ const pkgNames = [...pkgMatches].map((m) => m[1].toLowerCase());
768
+ // Also check add_subdirectory for vendored deps
769
+ const subdirMatches = content.matchAll(/add_subdirectory\s*\(\s*(?:[\w/]*\/)?(\w+)/gi);
770
+ for (const m of subdirMatches) pkgNames.push(m[1].toLowerCase());
771
+
772
+ const fwMap = {
773
+ qt5: 'Qt', qt6: 'Qt', qt: 'Qt',
774
+ boost: 'Boost',
775
+ sdl2: 'SDL', sdl: 'SDL',
776
+ sfml: 'SFML',
777
+ opencv: 'OpenCV',
778
+ gtest: 'Google Test', googletest: 'Google Test',
779
+ catch2: 'Catch2',
780
+ grpc: 'gRPC', protobuf: 'gRPC',
781
+ gtk: 'GTK', 'gtk+': 'GTK', gtkmm: 'GTK',
782
+ wxwidgets: 'wxWidgets',
783
+ imgui: 'Dear ImGui',
784
+ drogon: 'Drogon',
785
+ crow: 'Crow',
786
+ freertos: 'FreeRTOS',
787
+ zephyr: 'Zephyr',
788
+ };
789
+ for (const pkg of pkgNames) {
790
+ const fw = fwMap[pkg];
791
+ if (fw && !frameworks.includes(fw)) frameworks.push(fw);
792
+ }
793
+ } catch {}
794
+ return { languages, frameworks };
795
+ },
796
+ },
797
+ {
798
+ file: 'meson.build',
799
+ detect: async (dir) => {
800
+ const languages = [];
801
+ const frameworks = ['Meson'];
802
+ try {
803
+ const content = readFileSync(join(dir, 'meson.build'), 'utf8');
804
+ // project('name', 'cpp') or project('name', 'c', 'cpp')
805
+ const projMatch = content.match(/project\s*\([^)]*$/m) ? null : content.match(/project\s*\(([^)]*)\)/);
806
+ const projArgs = projMatch ? projMatch[1] : '';
807
+ if (/['"]cpp['"]/.test(projArgs)) languages.push('C++');
808
+ if (/['"]c['"]/.test(projArgs)) languages.push('C');
809
+ if (languages.length === 0) languages.push('C');
810
+
811
+ // Parse dependency() calls
812
+ const depMatches = content.matchAll(/dependency\s*\(\s*'([^']+)'/g);
813
+ const depMap = {
814
+ 'qt5': 'Qt', 'qt6': 'Qt',
815
+ 'boost': 'Boost',
816
+ 'sdl2': 'SDL',
817
+ 'sfml': 'SFML',
818
+ 'opencv4': 'OpenCV', 'opencv': 'OpenCV',
819
+ 'gtest': 'Google Test', 'gtest_main': 'Google Test',
820
+ 'catch2': 'Catch2', 'catch2-with-main': 'Catch2',
821
+ 'grpc': 'gRPC', 'grpc++': 'gRPC',
822
+ 'gtk+-3.0': 'GTK', 'gtk4': 'GTK', 'gtkmm-3.0': 'GTK',
823
+ 'wxwidgets': 'wxWidgets',
824
+ 'drogon': 'Drogon',
825
+ 'freertos': 'FreeRTOS',
826
+ };
827
+ for (const m of depMatches) {
828
+ const fw = depMap[m[1]];
829
+ if (fw && !frameworks.includes(fw)) frameworks.push(fw);
830
+ }
831
+ } catch {}
832
+ return { languages, frameworks };
833
+ },
834
+ },
835
+ {
836
+ file: 'conanfile.txt',
837
+ detect: async (dir) => {
838
+ const frameworks = [];
839
+ const fwCheck = (lc) => {
840
+ if (lc.includes('boost')) frameworks.push('Boost');
841
+ if (/\bqt\b/.test(lc)) frameworks.push('Qt');
842
+ if (lc.includes('sdl')) frameworks.push('SDL');
843
+ if (lc.includes('sfml')) frameworks.push('SFML');
844
+ if (lc.includes('opencv')) frameworks.push('OpenCV');
845
+ if (lc.includes('gtest')) frameworks.push('Google Test');
846
+ if (lc.includes('catch2')) frameworks.push('Catch2');
847
+ if (lc.includes('grpc')) frameworks.push('gRPC');
848
+ if (lc.includes('gtk')) frameworks.push('GTK');
849
+ if (lc.includes('wxwidgets')) frameworks.push('wxWidgets');
850
+ if (lc.includes('imgui')) frameworks.push('Dear ImGui');
851
+ if (lc.includes('drogon')) frameworks.push('Drogon');
852
+ if (lc.includes('crow')) frameworks.push('Crow');
853
+ if (lc.includes('freertos')) frameworks.push('FreeRTOS');
854
+ };
855
+ try {
856
+ if (existsSync(join(dir, 'conanfile.txt'))) {
857
+ const content = readFileSync(join(dir, 'conanfile.txt'), 'utf8');
858
+ const reqSection = content.match(/\[requires\]([\s\S]*?)(?:\[|$)/);
859
+ if (reqSection) fwCheck(reqSection[1].toLowerCase());
860
+ } else if (existsSync(join(dir, 'conanfile.py'))) {
861
+ const content = readFileSync(join(dir, 'conanfile.py'), 'utf8');
862
+ fwCheck(content.toLowerCase());
863
+ }
864
+ } catch {}
865
+ return { languages: ['C++'], frameworks };
866
+ },
867
+ checkExists: async (dir) => {
868
+ try {
869
+ return existsSync(join(dir, 'conanfile.txt')) || existsSync(join(dir, 'conanfile.py'));
870
+ } catch { return false; }
871
+ },
872
+ },
873
+ {
874
+ file: 'vcpkg.json',
875
+ detect: async (dir) => {
876
+ const frameworks = [];
877
+ try {
878
+ const pkg = JSON.parse(readFileSync(join(dir, 'vcpkg.json'), 'utf8'));
879
+ const deps = (pkg.dependencies || []).map((d) =>
880
+ (typeof d === 'string' ? d : d.name || '').toLowerCase()
881
+ );
882
+ const fwMap = {
883
+ boost: 'Boost', qt: 'Qt', qt5: 'Qt', qt6: 'Qt',
884
+ sdl2: 'SDL', sfml: 'SFML', opencv: 'OpenCV', opencv4: 'OpenCV',
885
+ gtest: 'Google Test', catch2: 'Catch2',
886
+ grpc: 'gRPC', gtk: 'GTK', gtkmm: 'GTK',
887
+ wxwidgets: 'wxWidgets', imgui: 'Dear ImGui',
888
+ drogon: 'Drogon', crow: 'Crow',
889
+ };
890
+ for (const dep of deps) {
891
+ const fw = fwMap[dep];
892
+ if (fw && !frameworks.includes(fw)) frameworks.push(fw);
893
+ }
894
+ } catch {}
895
+ return { languages: ['C++'], frameworks };
896
+ },
897
+ },
898
+ {
899
+ file: 'platformio.ini',
900
+ detect: async (dir) => {
901
+ const frameworks = [];
902
+ try {
903
+ const content = readFileSync(join(dir, 'platformio.ini'), 'utf8');
904
+ const fwMatch = content.match(/framework\s*=\s*(.*)/i);
905
+ if (fwMatch) {
906
+ const fwLine = fwMatch[1].toLowerCase();
907
+ if (fwLine.includes('espidf')) frameworks.push('ESP-IDF');
908
+ if (fwLine.includes('freertos')) frameworks.push('FreeRTOS');
909
+ if (fwLine.includes('zephyr')) frameworks.push('Zephyr');
910
+ }
911
+ } catch {}
912
+ return { languages: ['C++', 'C'], frameworks };
913
+ },
914
+ },
915
+ {
916
+ file: '*.uproject',
917
+ detect: async () => ({ languages: ['C++'], frameworks: ['Unreal Engine'] }),
918
+ checkExists: async (dir) => {
919
+ try { return readdirSync(dir).some((f) => f.endsWith('.uproject')); } catch { return false; }
920
+ },
921
+ },
922
+ {
923
+ file: 'WORKSPACE',
924
+ detect: async (dir) => {
925
+ const languages = [];
926
+ const frameworks = ['Bazel'];
927
+ try {
928
+ // Check both WORKSPACE and WORKSPACE.bazel
929
+ let content = '';
930
+ if (existsSync(join(dir, 'WORKSPACE'))) content = readFileSync(join(dir, 'WORKSPACE'), 'utf8');
931
+ else if (existsSync(join(dir, 'WORKSPACE.bazel'))) content = readFileSync(join(dir, 'WORKSPACE.bazel'), 'utf8');
932
+ if (content.includes('rules_cc') || content.includes('cc_library') || content.includes('cc_binary')) {
933
+ languages.push('C++');
934
+ languages.push('C');
935
+ }
936
+ } catch {}
937
+ return { languages, frameworks };
938
+ },
939
+ checkExists: async (dir) => {
940
+ try {
941
+ return existsSync(join(dir, 'WORKSPACE')) || existsSync(join(dir, 'WORKSPACE.bazel'));
942
+ } catch { return false; }
943
+ },
944
+ },
945
+ // ── Shopify detectors ────────────────────────────────────────────────
946
+ {
947
+ file: 'shopify.app.toml',
948
+ detect: async () => ({ languages: [], frameworks: ['Shopify'] }),
949
+ },
950
+ {
951
+ file: 'config/settings_schema.json',
952
+ detect: async (dir) => {
953
+ try {
954
+ const data = await readJson(join(dir, 'config', 'settings_schema.json')).catch(() => null);
955
+ if (Array.isArray(data)) {
956
+ return { languages: [], frameworks: ['Shopify'] };
957
+ }
958
+ } catch {}
959
+ return null;
960
+ },
961
+ checkExists: async (dir) => existsSync(join(dir, 'config', 'settings_schema.json')),
962
+ },
963
+ ];
964
+
965
+ // ── Project type detection ────────────────────────────────────────────
966
+
967
+ async function detectProjectType(dir) {
968
+ // Monorepo indicators
969
+ const monorepoIndicators = [
970
+ 'lerna.json',
971
+ 'pnpm-workspace.yaml',
972
+ 'nx.json',
973
+ 'turbo.json',
974
+ 'rush.json',
975
+ ];
976
+
977
+ for (const indicator of monorepoIndicators) {
978
+ if (await pathExists(join(dir, indicator))) return 'monorepo';
979
+ }
980
+
981
+ // Check package.json workspaces
982
+ try {
983
+ const pkg = await readJson(join(dir, 'package.json')).catch(() => null);
984
+ if (pkg?.workspaces) return 'monorepo';
985
+ } catch {}
986
+
987
+ // Check for packages/ or apps/ directories with multiple sub-packages
988
+ for (const subdir of ['packages', 'apps', 'services', 'modules']) {
989
+ const subdirPath = join(dir, subdir);
990
+ if (await pathExists(subdirPath)) {
991
+ try {
992
+ const entries = readdirSync(subdirPath, { withFileTypes: true });
993
+ const subdirs = entries.filter((e) => e.isDirectory()).length;
994
+ if (subdirs >= 2) return 'monorepo';
995
+ } catch {}
996
+ }
997
+ }
998
+
999
+ // Microservice indicators
1000
+ if (await pathExists(join(dir, 'docker-compose.yml')) || await pathExists(join(dir, 'docker-compose.yaml'))) {
1001
+ try {
1002
+ const content = readFileSync(
1003
+ await pathExists(join(dir, 'docker-compose.yml'))
1004
+ ? join(dir, 'docker-compose.yml')
1005
+ : join(dir, 'docker-compose.yaml'),
1006
+ 'utf8'
1007
+ );
1008
+ const serviceCount = (content.match(/^\s{2}\w[\w-]*:/gm) || []).length;
1009
+ if (serviceCount >= 3) return 'microservice';
1010
+ } catch {}
1011
+ }
1012
+
1013
+ // Library indicators
1014
+ try {
1015
+ const pkg = await readJson(join(dir, 'package.json')).catch(() => null);
1016
+ if (pkg) {
1017
+ if (pkg.main || pkg.module || pkg.exports) {
1018
+ if (!pkg.bin && !pkg.scripts?.start) return 'library';
1019
+ }
1020
+ if (pkg.bin) return 'cli';
1021
+ }
1022
+ } catch {}
1023
+
1024
+ // Python library
1025
+ if (await pathExists(join(dir, 'setup.py')) || await pathExists(join(dir, 'setup.cfg'))) {
1026
+ return 'library';
1027
+ }
1028
+
1029
+ return 'monolith';
1030
+ }
1031
+
1032
+ // ── Code style detection ──────────────────────────────────────────────
1033
+
1034
+ async function detectCodeStyle(dir) {
1035
+ const styles = [];
1036
+
1037
+ const checks = [
1038
+ { files: ['.prettierrc', '.prettierrc.json', '.prettierrc.js', '.prettierrc.cjs', 'prettier.config.js', 'prettier.config.cjs'], name: 'prettier' },
1039
+ { files: ['.eslintrc', '.eslintrc.json', '.eslintrc.js', '.eslintrc.cjs', 'eslint.config.js', 'eslint.config.mjs'], name: 'eslint' },
1040
+ { files: ['.editorconfig'], name: 'editorconfig' },
1041
+ { files: ['.stylelintrc', '.stylelintrc.json', 'stylelint.config.js'], name: 'stylelint' },
1042
+ { files: ['.golangci.yml', '.golangci.yaml'], name: 'golangci-lint' },
1043
+ { files: ['.rubocop.yml'], name: 'rubocop' },
1044
+ { files: ['.php-cs-fixer.php', '.php-cs-fixer.dist.php'], name: 'php-cs-fixer' },
1045
+ { files: ['rustfmt.toml', '.rustfmt.toml'], name: 'rustfmt' },
1046
+ { files: ['checkstyle.xml'], name: 'checkstyle' },
1047
+ { files: ['.clang-format'], name: 'clang-format' },
1048
+ { files: ['.clang-tidy'], name: 'clang-tidy' },
1049
+ ];
1050
+
1051
+ for (const { files, name } of checks) {
1052
+ for (const f of files) {
1053
+ if (await pathExists(join(dir, f))) {
1054
+ styles.push(name);
1055
+ break;
1056
+ }
1057
+ }
1058
+ }
1059
+
1060
+ // Check pyproject.toml for Python tools
1061
+ if (await pathExists(join(dir, 'pyproject.toml'))) {
1062
+ try {
1063
+ const content = readFileSync(join(dir, 'pyproject.toml'), 'utf8');
1064
+ if (content.includes('[tool.ruff]') || content.includes('[tool.ruff.')) styles.push('ruff');
1065
+ if (content.includes('[tool.black]')) styles.push('black');
1066
+ if (content.includes('[tool.mypy]')) styles.push('mypy');
1067
+ if (content.includes('[tool.isort]')) styles.push('isort');
1068
+ } catch {}
1069
+ }
1070
+
1071
+ return styles;
1072
+ }
1073
+
1074
+ // ── Subproject detection (for monorepos) ──────────────────────────────
1075
+
1076
+ async function detectSubprojects(dir) {
1077
+ const subprojects = [];
1078
+ const scanDirs = ['packages', 'apps', 'services', 'modules', 'libs'];
1079
+
1080
+ for (const scanDir of scanDirs) {
1081
+ const scanPath = join(dir, scanDir);
1082
+ if (!(await pathExists(scanPath))) continue;
1083
+
1084
+ try {
1085
+ const entries = readdirSync(scanPath, { withFileTypes: true });
1086
+ for (const entry of entries) {
1087
+ if (!entry.isDirectory()) continue;
1088
+ const subPath = join(scanPath, entry.name);
1089
+ const subInfo = { name: entry.name, path: `${scanDir}/${entry.name}`, languages: [], frameworks: [], databases: [] };
1090
+
1091
+ // Run detectors on subproject
1092
+ for (const { file, detect, checkExists } of DETECTORS) {
1093
+ const exists = checkExists
1094
+ ? await checkExists(subPath)
1095
+ : await pathExists(join(subPath, file));
1096
+ if (exists) {
1097
+ const detected = await detect(subPath);
1098
+ if (detected) {
1099
+ for (const lang of detected.languages || []) {
1100
+ if (!subInfo.languages.includes(lang)) subInfo.languages.push(lang);
1101
+ }
1102
+ for (const fw of detected.frameworks || []) {
1103
+ if (!subInfo.frameworks.includes(fw)) subInfo.frameworks.push(fw);
1104
+ }
1105
+ for (const db of detected.databases || []) {
1106
+ if (!subInfo.databases.includes(db)) subInfo.databases.push(db);
1107
+ }
1108
+ }
1109
+ }
1110
+ }
1111
+
1112
+ if (subInfo.languages.length > 0 || subInfo.frameworks.length > 0) {
1113
+ subprojects.push(subInfo);
1114
+ }
1115
+ }
1116
+ } catch {}
1117
+ }
1118
+
1119
+ return subprojects;
1120
+ }
1121
+
1122
+ // ── CI/CD detection ───────────────────────────────────────────────────
1123
+
1124
+ async function detectCICD(dir) {
1125
+ const ci = [];
1126
+ const checks = [
1127
+ { path: '.github/workflows', name: 'GitHub Actions' },
1128
+ { path: '.gitlab-ci.yml', name: 'GitLab CI' },
1129
+ { path: 'Jenkinsfile', name: 'Jenkins' },
1130
+ { path: '.circleci', name: 'CircleCI' },
1131
+ { path: '.travis.yml', name: 'Travis CI' },
1132
+ { path: 'azure-pipelines.yml', name: 'Azure Pipelines' },
1133
+ { path: 'bitbucket-pipelines.yml', name: 'Bitbucket Pipelines' },
1134
+ { path: '.drone.yml', name: 'Drone CI' },
1135
+ ];
1136
+
1137
+ for (const { path, name } of checks) {
1138
+ if (await pathExists(join(dir, path))) ci.push(name);
1139
+ }
1140
+
1141
+ return ci;
1142
+ }
1143
+
1144
+ // ── Sensitive file detection ──────────────────────────────────────────
1145
+
1146
+ async function detectSensitiveFiles(dir) {
1147
+ const found = [];
1148
+ const patterns = [
1149
+ '.env', '.env.local', '.env.production', '.env.development',
1150
+ 'credentials.json', 'secrets.yaml', 'secrets.yml',
1151
+ 'serviceAccountKey.json',
1152
+ ];
1153
+
1154
+ for (const p of patterns) {
1155
+ if (await pathExists(join(dir, p))) found.push(p);
1156
+ }
1157
+
1158
+ // Check if .gitignore covers sensitive files
1159
+ let gitignoreCovers = false;
1160
+ try {
1161
+ const gitignore = readFileSync(join(dir, '.gitignore'), 'utf8');
1162
+ gitignoreCovers = gitignore.includes('.env') || gitignore.includes('*.env');
1163
+ } catch {}
1164
+
1165
+ return { found, gitignoreCovers };
1166
+ }
1167
+
1168
+ // ── Main detection entry point ────────────────────────────────────────
1169
+
1170
+ export async function detectProject(targetDir) {
1171
+ const result = {
1172
+ name: '',
1173
+ description: '',
1174
+ languages: [],
1175
+ frameworks: [],
1176
+ databases: [],
1177
+ _allDependencies: [],
1178
+ hasGit: false,
1179
+ packageManager: null,
1180
+ projectType: 'monolith',
1181
+ codeStyle: [],
1182
+ subprojects: [],
1183
+ cicd: [],
1184
+ sensitiveFiles: { found: [], gitignoreCovers: false },
1185
+ languageDistribution: null,
1186
+ };
1187
+
1188
+ // Check git
1189
+ result.hasGit = await pathExists(join(targetDir, '.git'));
1190
+
1191
+ // Run language/framework detectors
1192
+ for (const { file, detect, checkExists } of DETECTORS) {
1193
+ const exists = checkExists
1194
+ ? await checkExists(targetDir)
1195
+ : await pathExists(join(targetDir, file));
1196
+ if (exists) {
1197
+ const detected = await detect(targetDir);
1198
+ if (detected) {
1199
+ if (detected.name) result.name = detected.name;
1200
+ if (detected.description) result.description = detected.description;
1201
+ if (detected.packageManager) result.packageManager = detected.packageManager;
1202
+ for (const lang of detected.languages || []) {
1203
+ if (!result.languages.includes(lang)) result.languages.push(lang);
1204
+ }
1205
+ for (const fw of detected.frameworks || []) {
1206
+ if (!result.frameworks.includes(fw)) result.frameworks.push(fw);
1207
+ }
1208
+ for (const dep of detected._allDependencies || []) {
1209
+ if (!result._allDependencies.includes(dep)) result._allDependencies.push(dep);
1210
+ }
1211
+ for (const db of detected.databases || []) {
1212
+ if (!result.databases.includes(db)) result.databases.push(db);
1213
+ }
1214
+ }
1215
+ }
1216
+ }
1217
+
1218
+ // Detect project type
1219
+ result.projectType = await detectProjectType(targetDir);
1220
+
1221
+ // Detect code style tools
1222
+ result.codeStyle = await detectCodeStyle(targetDir);
1223
+
1224
+ // Detect subprojects if monorepo
1225
+ if (result.projectType === 'monorepo') {
1226
+ result.subprojects = await detectSubprojects(targetDir);
1227
+ // Merge subproject languages/frameworks/databases into root
1228
+ for (const sub of result.subprojects) {
1229
+ for (const lang of sub.languages) {
1230
+ if (!result.languages.includes(lang)) result.languages.push(lang);
1231
+ }
1232
+ for (const fw of sub.frameworks) {
1233
+ if (!result.frameworks.includes(fw)) result.frameworks.push(fw);
1234
+ }
1235
+ for (const db of sub.databases || []) {
1236
+ if (!result.databases.includes(db)) result.databases.push(db);
1237
+ }
1238
+ }
1239
+ }
1240
+
1241
+ // Detect CI/CD
1242
+ result.cicd = await detectCICD(targetDir);
1243
+
1244
+ // Detect sensitive files
1245
+ result.sensitiveFiles = await detectSensitiveFiles(targetDir);
1246
+
1247
+ // Collect root-level filenames for server-side stack pack file-pattern matching
1248
+ try {
1249
+ result._rootFiles = readdirSync(targetDir).filter((f) => {
1250
+ // Exclude hidden dirs like .git, node_modules
1251
+ if (f === 'node_modules' || f === '.git') return false;
1252
+ return true;
1253
+ });
1254
+ } catch {
1255
+ result._rootFiles = [];
1256
+ }
1257
+
1258
+ // Estimate language distribution from file counts
1259
+ result.languageDistribution = estimateLanguageDistribution(targetDir);
1260
+
1261
+ // Reconcile: add languages with >= 15% distribution missed by detectors
1262
+ if (result.languageDistribution) {
1263
+ for (const [lang, pct] of Object.entries(result.languageDistribution)) {
1264
+ if (pct >= 15 && !result.languages.includes(lang)) {
1265
+ result.languages.push(lang);
1266
+ }
1267
+ }
1268
+ }
1269
+
1270
+ // Fallback name from directory
1271
+ if (!result.name) {
1272
+ result.name = targetDir.split(/[/\\]/).filter(Boolean).pop() || 'my-project';
1273
+ }
1274
+
1275
+ return result;
1276
+ }