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.
- package/bin/claude-craft.js +85 -0
- package/package.json +39 -0
- package/src/commands/auth.js +43 -0
- package/src/commands/create.js +543 -0
- package/src/commands/install.js +480 -0
- package/src/commands/logout.js +24 -0
- package/src/commands/update.js +339 -0
- package/src/constants.js +299 -0
- package/src/generators/directories.js +30 -0
- package/src/generators/metadata.js +57 -0
- package/src/generators/security.js +39 -0
- package/src/prompts/gather.js +308 -0
- package/src/ui/brand.js +62 -0
- package/src/ui/cards.js +179 -0
- package/src/ui/format.js +55 -0
- package/src/ui/phase-header.js +20 -0
- package/src/ui/prompts.js +56 -0
- package/src/ui/tables.js +89 -0
- package/src/ui/tasks.js +258 -0
- package/src/ui/theme.js +83 -0
- package/src/utils/analysis-cache.js +519 -0
- package/src/utils/api-client.js +253 -0
- package/src/utils/api-file-writer.js +197 -0
- package/src/utils/bootstrap-runner.js +148 -0
- package/src/utils/claude-analyzer.js +255 -0
- package/src/utils/claude-optimizer.js +341 -0
- package/src/utils/claude-rewriter.js +553 -0
- package/src/utils/claude-scorer.js +101 -0
- package/src/utils/description-analyzer.js +116 -0
- package/src/utils/detect-project.js +1276 -0
- package/src/utils/existing-setup.js +341 -0
- package/src/utils/file-writer.js +64 -0
- package/src/utils/json-extract.js +56 -0
- package/src/utils/logger.js +27 -0
- package/src/utils/mcp-setup.js +461 -0
- package/src/utils/preflight.js +112 -0
- package/src/utils/prompt-api-key.js +59 -0
- package/src/utils/run-claude.js +152 -0
- package/src/utils/security.js +82 -0
- 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
|
+
}
|