thachvd-kit 1.0.3 → 1.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/bin/cli.js +513 -88
- package/package.json +1 -1
- package/scripts/init.py +318 -101
package/README.md
CHANGED
|
@@ -17,6 +17,7 @@ npx github:thachvd/thachvd-kit init
|
|
|
17
17
|
|
|
18
18
|
The interactive wizard will first ask whether this is a new project or an existing one.
|
|
19
19
|
If it is an existing project, it can scan the current codebase to prefill `PROJECT_CONTEXT.md`, or let you enter everything manually.
|
|
20
|
+
The scan mode uses evidence-based detectors and shows why a framework was inferred, so you can quickly confirm or correct it.
|
|
20
21
|
|
|
21
22
|
```bash
|
|
22
23
|
thachvd-kit init --scan
|
package/bin/cli.js
CHANGED
|
@@ -11,18 +11,19 @@ const targetDir = process.cwd();
|
|
|
11
11
|
|
|
12
12
|
// --- Skill Resolver ---
|
|
13
13
|
|
|
14
|
-
const LANG_SKILLS = {
|
|
15
|
-
javascript: ["nodejs-best-practices", "api-design"],
|
|
16
|
-
typescript: ["nodejs-best-practices", "api-design"],
|
|
17
|
-
python: ["python-patterns"],
|
|
18
|
-
go: ["golang-patterns", "golang-testing"],
|
|
19
|
-
php: ["
|
|
20
|
-
rust: ["rust-pro"],
|
|
21
|
-
java: ["api-design"],
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
14
|
+
const LANG_SKILLS = {
|
|
15
|
+
javascript: ["nodejs-best-practices", "api-design"],
|
|
16
|
+
typescript: ["nodejs-best-practices", "api-design"],
|
|
17
|
+
python: ["python-patterns"],
|
|
18
|
+
go: ["golang-patterns", "golang-testing"],
|
|
19
|
+
php: ["api-design"],
|
|
20
|
+
rust: ["rust-pro"],
|
|
21
|
+
java: ["api-design"],
|
|
22
|
+
ruby: ["api-design"],
|
|
23
|
+
swift: ["mobile-design"],
|
|
24
|
+
kotlin: ["mobile-design"],
|
|
25
|
+
csharp: ["api-design"],
|
|
26
|
+
};
|
|
26
27
|
|
|
27
28
|
const FRAMEWORK_SKILLS = {
|
|
28
29
|
vue: ["frontend-design", "tailwind-patterns"],
|
|
@@ -30,14 +31,25 @@ const FRAMEWORK_SKILLS = {
|
|
|
30
31
|
next: ["nextjs-react-expert", "frontend-design"],
|
|
31
32
|
nuxt: ["frontend-design"],
|
|
32
33
|
svelte: ["frontend-design"],
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
34
|
+
angular: ["frontend-design"],
|
|
35
|
+
laravel: ["laravel-patterns", "laravel-security", "laravel-tdd"],
|
|
36
|
+
cakephp: ["api-design"],
|
|
37
|
+
symfony: ["api-design"],
|
|
38
|
+
codeigniter: ["api-design"],
|
|
39
|
+
express: ["nodejs-best-practices", "api-design"],
|
|
40
|
+
fastapi: ["python-patterns", "api-design"],
|
|
41
|
+
django: ["python-patterns", "api-design"],
|
|
42
|
+
flask: ["python-patterns", "api-design"],
|
|
43
|
+
gin: ["golang-patterns", "golang-testing"],
|
|
44
|
+
fiber: ["golang-patterns", "golang-testing"],
|
|
45
|
+
echo: ["golang-patterns", "golang-testing"],
|
|
46
|
+
nestjs: ["nodejs-best-practices", "api-design"],
|
|
47
|
+
rails: ["api-design"],
|
|
48
|
+
spring: ["api-design"],
|
|
49
|
+
aspnet: ["api-design"],
|
|
40
50
|
tauri: ["rust-pro", "desktop-design"],
|
|
51
|
+
axum: ["rust-pro", "api-design"],
|
|
52
|
+
actix: ["rust-pro", "api-design"],
|
|
41
53
|
flutter: ["mobile-design"],
|
|
42
54
|
"react-native": ["mobile-design"],
|
|
43
55
|
};
|
|
@@ -80,16 +92,21 @@ function resolveSkills(data) {
|
|
|
80
92
|
|
|
81
93
|
const FRAMEWORK_CHOICES = [
|
|
82
94
|
'vue', 'react', 'next', 'nuxt', 'svelte', 'angular',
|
|
83
|
-
'laravel', '
|
|
84
|
-
'
|
|
95
|
+
'laravel', 'cakephp', 'symfony', 'codeigniter',
|
|
96
|
+
'express', 'fastapi', 'django', 'flask', 'gin', 'fiber', 'echo', 'nestjs',
|
|
97
|
+
'rails', 'spring', 'aspnet',
|
|
98
|
+
'tauri', 'axum', 'actix', 'flutter', 'react-native'
|
|
85
99
|
];
|
|
86
100
|
|
|
87
|
-
const LANGUAGE_CHOICES = ['typescript', 'javascript', 'python', 'go', 'php', 'rust', 'java', 'swift', 'kotlin', 'csharp', 'cpp'];
|
|
101
|
+
const LANGUAGE_CHOICES = ['typescript', 'javascript', 'python', 'go', 'php', 'rust', 'java', 'ruby', 'swift', 'kotlin', 'csharp', 'cpp'];
|
|
88
102
|
const PROJECT_TYPE_CHOICES = ['web-app', 'api', 'mobile', 'desktop', 'cli', 'fullstack'];
|
|
89
103
|
const DATABASE_CHOICES = ['postgres', 'mysql', 'sqlite', 'mongodb', 'redis', 'dynamodb', 'none'];
|
|
90
104
|
const PACKAGE_MANAGER_CHOICES = ['npm', 'pnpm', 'yarn', 'bun', 'pip', 'uv', 'go mod', 'cargo', 'composer', 'auto-detect'];
|
|
91
105
|
const TEST_FRAMEWORK_CHOICES = ['jest', 'vitest', 'pytest', 'go test', 'phpunit', 'pest', 'rspec', 'junit', 'auto-detect'];
|
|
92
106
|
const CLOUD_CHOICES = ['aws', 'gcp', 'azure', 'vercel', 'fly', 'railway', 'none'];
|
|
107
|
+
const FRONTEND_FRAMEWORKS = ['vue', 'react', 'next', 'nuxt', 'svelte', 'angular'];
|
|
108
|
+
const BACKEND_FRAMEWORKS = ['laravel', 'cakephp', 'symfony', 'codeigniter', 'express', 'fastapi', 'django', 'flask', 'gin', 'fiber', 'echo', 'nestjs', 'rails', 'spring', 'aspnet', 'axum', 'actix'];
|
|
109
|
+
const MOBILE_DESKTOP_FRAMEWORKS = ['tauri', 'flutter', 'react-native'];
|
|
93
110
|
|
|
94
111
|
function printHelp() {
|
|
95
112
|
console.log(`
|
|
@@ -160,74 +177,453 @@ function uniq(values) {
|
|
|
160
177
|
return Array.from(new Set(values.filter(Boolean)));
|
|
161
178
|
}
|
|
162
179
|
|
|
180
|
+
function evidenceIf(condition, label, weight = 1) {
|
|
181
|
+
return condition ? { label, weight } : null;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function buildDetection(id, evidence, meta = {}) {
|
|
185
|
+
const hits = evidence.filter(Boolean);
|
|
186
|
+
const score = hits.reduce((sum, hit) => sum + hit.weight, 0);
|
|
187
|
+
return {
|
|
188
|
+
id,
|
|
189
|
+
score,
|
|
190
|
+
evidence: hits.map(hit => hit.label),
|
|
191
|
+
...meta
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function confidenceLabel(score) {
|
|
196
|
+
if (score >= 6) return 'high';
|
|
197
|
+
if (score >= 3) return 'medium';
|
|
198
|
+
return 'low';
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function hasDependency(depNames, ...names) {
|
|
202
|
+
return names.some(name => depNames.has(name));
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function hasComposerDependency(composerDepNames, ...names) {
|
|
206
|
+
return names.some(name => composerDepNames.has(name));
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function hasRootFile(rootEntries, ...filenames) {
|
|
210
|
+
return filenames.some(name => rootEntries.has(name));
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function textMatches(text, pattern) {
|
|
214
|
+
return Boolean(text && pattern.test(text));
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const LANGUAGE_DETECTORS = [
|
|
218
|
+
{
|
|
219
|
+
id: 'typescript',
|
|
220
|
+
detect: ctx => buildDetection('typescript', [
|
|
221
|
+
evidenceIf(ctx.fileExists('tsconfig.json'), 'found tsconfig.json', 4),
|
|
222
|
+
evidenceIf(hasDependency(ctx.depNames, 'typescript', 'ts-node', 'tsx'), 'package.json -> typescript toolchain', 2)
|
|
223
|
+
])
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
id: 'javascript',
|
|
227
|
+
detect: ctx => buildDetection('javascript', [
|
|
228
|
+
evidenceIf(ctx.fileExists('package.json'), 'found package.json', 2),
|
|
229
|
+
evidenceIf(ctx.depNames.size > 0, 'package.json -> JavaScript dependency graph', 1)
|
|
230
|
+
])
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
id: 'python',
|
|
234
|
+
detect: ctx => buildDetection('python', [
|
|
235
|
+
evidenceIf(ctx.fileExists('requirements.txt'), 'found requirements.txt', 3),
|
|
236
|
+
evidenceIf(ctx.fileExists('pyproject.toml'), 'found pyproject.toml', 2),
|
|
237
|
+
evidenceIf(textMatches(ctx.pyprojectText, /\[tool\.(poetry|uv|hatch|pdm)\]/i), 'pyproject.toml -> Python tool section', 2)
|
|
238
|
+
])
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
id: 'go',
|
|
242
|
+
detect: ctx => buildDetection('go', [
|
|
243
|
+
evidenceIf(ctx.fileExists('go.mod'), 'found go.mod', 4)
|
|
244
|
+
])
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
id: 'php',
|
|
248
|
+
detect: ctx => buildDetection('php', [
|
|
249
|
+
evidenceIf(ctx.fileExists('composer.json'), 'found composer.json', 4),
|
|
250
|
+
evidenceIf(ctx.composerDepNames.size > 0, 'composer.json -> PHP dependency graph', 1)
|
|
251
|
+
])
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
id: 'rust',
|
|
255
|
+
detect: ctx => buildDetection('rust', [
|
|
256
|
+
evidenceIf(ctx.fileExists('Cargo.toml'), 'found Cargo.toml', 4),
|
|
257
|
+
evidenceIf(ctx.fileExists('src-tauri', 'Cargo.toml'), 'found src-tauri/Cargo.toml', 5)
|
|
258
|
+
])
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
id: 'java',
|
|
262
|
+
detect: ctx => buildDetection('java', [
|
|
263
|
+
evidenceIf(ctx.fileExists('pom.xml'), 'found pom.xml', 4),
|
|
264
|
+
evidenceIf(ctx.fileExists('build.gradle') || ctx.fileExists('build.gradle.kts'), 'found Gradle build file', 3)
|
|
265
|
+
])
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
id: 'ruby',
|
|
269
|
+
detect: ctx => buildDetection('ruby', [
|
|
270
|
+
evidenceIf(ctx.fileExists('Gemfile'), 'found Gemfile', 4),
|
|
271
|
+
evidenceIf(ctx.fileExists('config.ru'), 'found config.ru', 1)
|
|
272
|
+
])
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
id: 'csharp',
|
|
276
|
+
detect: ctx => buildDetection('csharp', [
|
|
277
|
+
evidenceIf(ctx.rootEntries.some(name => name.endsWith('.sln') || name.endsWith('.csproj')), 'found .sln/.csproj', 4),
|
|
278
|
+
evidenceIf(ctx.fileExists('Program.cs'), 'found Program.cs', 1)
|
|
279
|
+
])
|
|
280
|
+
}
|
|
281
|
+
];
|
|
282
|
+
|
|
283
|
+
const FRAMEWORK_DETECTORS = [
|
|
284
|
+
{
|
|
285
|
+
id: 'next',
|
|
286
|
+
kind: 'frontend',
|
|
287
|
+
projectType: 'web-app',
|
|
288
|
+
detect: ctx => buildDetection('next', [
|
|
289
|
+
evidenceIf(hasDependency(ctx.depNames, 'next'), 'package.json -> next', 4),
|
|
290
|
+
evidenceIf(ctx.fileExists('next.config.js') || ctx.fileExists('next.config.mjs') || ctx.fileExists('next.config.ts'), 'found next.config.*', 3),
|
|
291
|
+
evidenceIf(ctx.fileExists('app') || ctx.fileExists('pages'), 'found app/ or pages/', 1)
|
|
292
|
+
])
|
|
293
|
+
},
|
|
294
|
+
{
|
|
295
|
+
id: 'react',
|
|
296
|
+
kind: 'frontend',
|
|
297
|
+
projectType: 'web-app',
|
|
298
|
+
detect: ctx => buildDetection('react', [
|
|
299
|
+
evidenceIf(hasDependency(ctx.depNames, 'react'), 'package.json -> react', 4),
|
|
300
|
+
evidenceIf(hasDependency(ctx.depNames, 'react-dom'), 'package.json -> react-dom', 2)
|
|
301
|
+
])
|
|
302
|
+
},
|
|
303
|
+
{
|
|
304
|
+
id: 'vue',
|
|
305
|
+
kind: 'frontend',
|
|
306
|
+
projectType: 'web-app',
|
|
307
|
+
detect: ctx => buildDetection('vue', [
|
|
308
|
+
evidenceIf(hasDependency(ctx.depNames, 'vue'), 'package.json -> vue', 4)
|
|
309
|
+
])
|
|
310
|
+
},
|
|
311
|
+
{
|
|
312
|
+
id: 'nuxt',
|
|
313
|
+
kind: 'frontend',
|
|
314
|
+
projectType: 'web-app',
|
|
315
|
+
detect: ctx => buildDetection('nuxt', [
|
|
316
|
+
evidenceIf(hasDependency(ctx.depNames, 'nuxt'), 'package.json -> nuxt', 4),
|
|
317
|
+
evidenceIf(ctx.fileExists('nuxt.config.ts') || ctx.fileExists('nuxt.config.js'), 'found nuxt.config.*', 3)
|
|
318
|
+
])
|
|
319
|
+
},
|
|
320
|
+
{
|
|
321
|
+
id: 'svelte',
|
|
322
|
+
kind: 'frontend',
|
|
323
|
+
projectType: 'web-app',
|
|
324
|
+
detect: ctx => buildDetection('svelte', [
|
|
325
|
+
evidenceIf(hasDependency(ctx.depNames, 'svelte', '@sveltejs/kit'), 'package.json -> svelte', 4)
|
|
326
|
+
])
|
|
327
|
+
},
|
|
328
|
+
{
|
|
329
|
+
id: 'angular',
|
|
330
|
+
kind: 'frontend',
|
|
331
|
+
projectType: 'web-app',
|
|
332
|
+
detect: ctx => buildDetection('angular', [
|
|
333
|
+
evidenceIf(hasDependency(ctx.depNames, '@angular/core'), 'package.json -> @angular/core', 4),
|
|
334
|
+
evidenceIf(ctx.fileExists('angular.json'), 'found angular.json', 3)
|
|
335
|
+
])
|
|
336
|
+
},
|
|
337
|
+
{
|
|
338
|
+
id: 'express',
|
|
339
|
+
kind: 'backend',
|
|
340
|
+
language: 'javascript',
|
|
341
|
+
projectType: 'api',
|
|
342
|
+
detect: ctx => buildDetection('express', [
|
|
343
|
+
evidenceIf(hasDependency(ctx.depNames, 'express'), 'package.json -> express', 4)
|
|
344
|
+
])
|
|
345
|
+
},
|
|
346
|
+
{
|
|
347
|
+
id: 'nestjs',
|
|
348
|
+
kind: 'backend',
|
|
349
|
+
language: 'javascript',
|
|
350
|
+
projectType: 'api',
|
|
351
|
+
detect: ctx => buildDetection('nestjs', [
|
|
352
|
+
evidenceIf(hasDependency(ctx.depNames, '@nestjs/core'), 'package.json -> @nestjs/core', 4)
|
|
353
|
+
])
|
|
354
|
+
},
|
|
355
|
+
{
|
|
356
|
+
id: 'fastapi',
|
|
357
|
+
kind: 'backend',
|
|
358
|
+
language: 'python',
|
|
359
|
+
projectType: 'api',
|
|
360
|
+
detect: ctx => buildDetection('fastapi', [
|
|
361
|
+
evidenceIf(textMatches(ctx.requirementsText, /\bfastapi\b/i) || textMatches(ctx.pyprojectText, /\bfastapi\b/i), 'Python deps -> fastapi', 4)
|
|
362
|
+
])
|
|
363
|
+
},
|
|
364
|
+
{
|
|
365
|
+
id: 'django',
|
|
366
|
+
kind: 'backend',
|
|
367
|
+
language: 'python',
|
|
368
|
+
projectType: 'api',
|
|
369
|
+
detect: ctx => buildDetection('django', [
|
|
370
|
+
evidenceIf(textMatches(ctx.requirementsText, /\bdjango\b/i) || textMatches(ctx.pyprojectText, /\bdjango\b/i), 'Python deps -> django', 4),
|
|
371
|
+
evidenceIf(ctx.fileExists('manage.py'), 'found manage.py', 2)
|
|
372
|
+
])
|
|
373
|
+
},
|
|
374
|
+
{
|
|
375
|
+
id: 'flask',
|
|
376
|
+
kind: 'backend',
|
|
377
|
+
language: 'python',
|
|
378
|
+
projectType: 'api',
|
|
379
|
+
detect: ctx => buildDetection('flask', [
|
|
380
|
+
evidenceIf(textMatches(ctx.requirementsText, /\bflask\b/i) || textMatches(ctx.pyprojectText, /\bflask\b/i), 'Python deps -> flask', 4)
|
|
381
|
+
])
|
|
382
|
+
},
|
|
383
|
+
{
|
|
384
|
+
id: 'laravel',
|
|
385
|
+
kind: 'backend',
|
|
386
|
+
language: 'php',
|
|
387
|
+
projectType: 'api',
|
|
388
|
+
detect: ctx => buildDetection('laravel', [
|
|
389
|
+
evidenceIf(hasComposerDependency(ctx.composerDepNames, 'laravel/framework'), 'composer.json -> laravel/framework', 4),
|
|
390
|
+
evidenceIf(ctx.fileExists('artisan'), 'found artisan', 3)
|
|
391
|
+
])
|
|
392
|
+
},
|
|
393
|
+
{
|
|
394
|
+
id: 'cakephp',
|
|
395
|
+
kind: 'backend',
|
|
396
|
+
language: 'php',
|
|
397
|
+
projectType: 'api',
|
|
398
|
+
detect: ctx => buildDetection('cakephp', [
|
|
399
|
+
evidenceIf(hasComposerDependency(ctx.composerDepNames, 'cakephp/cakephp'), 'composer.json -> cakephp/cakephp', 4),
|
|
400
|
+
evidenceIf(ctx.fileExists('bin', 'cake'), 'found bin/cake', 3),
|
|
401
|
+
evidenceIf(ctx.fileExists('config', 'app.php'), 'found config/app.php', 2),
|
|
402
|
+
evidenceIf(ctx.fileExists('app', 'Config', 'core.php'), 'found app/Config/core.php (CakePHP legacy)', 3),
|
|
403
|
+
evidenceIf(ctx.fileExists('app', 'Controller', 'AppController.php'), 'found app/Controller/AppController.php', 2),
|
|
404
|
+
evidenceIf(ctx.fileExists('app', 'Model', 'AppModel.php'), 'found app/Model/AppModel.php', 2),
|
|
405
|
+
evidenceIf(ctx.fileExists('lib', 'Cake'), 'found lib/Cake', 3),
|
|
406
|
+
evidenceIf(ctx.fileExists('cake', 'console', 'cake'), 'found cake/console/cake', 3)
|
|
407
|
+
])
|
|
408
|
+
},
|
|
409
|
+
{
|
|
410
|
+
id: 'symfony',
|
|
411
|
+
kind: 'backend',
|
|
412
|
+
language: 'php',
|
|
413
|
+
projectType: 'api',
|
|
414
|
+
detect: ctx => buildDetection('symfony', [
|
|
415
|
+
evidenceIf(hasComposerDependency(ctx.composerDepNames, 'symfony/framework-bundle'), 'composer.json -> symfony/framework-bundle', 4),
|
|
416
|
+
evidenceIf(ctx.fileExists('bin', 'console'), 'found bin/console', 2),
|
|
417
|
+
evidenceIf(ctx.fileExists('config', 'bundles.php'), 'found config/bundles.php', 1)
|
|
418
|
+
])
|
|
419
|
+
},
|
|
420
|
+
{
|
|
421
|
+
id: 'codeigniter',
|
|
422
|
+
kind: 'backend',
|
|
423
|
+
language: 'php',
|
|
424
|
+
projectType: 'api',
|
|
425
|
+
detect: ctx => buildDetection('codeigniter', [
|
|
426
|
+
evidenceIf(hasComposerDependency(ctx.composerDepNames, 'codeigniter4/framework'), 'composer.json -> codeigniter4/framework', 4),
|
|
427
|
+
evidenceIf(ctx.fileExists('spark'), 'found spark', 2),
|
|
428
|
+
evidenceIf(ctx.fileExists('app', 'Config', 'App.php'), 'found app/Config/App.php', 1)
|
|
429
|
+
])
|
|
430
|
+
},
|
|
431
|
+
{
|
|
432
|
+
id: 'gin',
|
|
433
|
+
kind: 'backend',
|
|
434
|
+
language: 'go',
|
|
435
|
+
projectType: 'api',
|
|
436
|
+
detect: ctx => buildDetection('gin', [
|
|
437
|
+
evidenceIf(textMatches(ctx.goModText, /\bgithub\.com\/gin-gonic\/gin\b/i), 'go.mod -> github.com/gin-gonic/gin', 4)
|
|
438
|
+
])
|
|
439
|
+
},
|
|
440
|
+
{
|
|
441
|
+
id: 'fiber',
|
|
442
|
+
kind: 'backend',
|
|
443
|
+
language: 'go',
|
|
444
|
+
projectType: 'api',
|
|
445
|
+
detect: ctx => buildDetection('fiber', [
|
|
446
|
+
evidenceIf(textMatches(ctx.goModText, /\bgithub\.com\/gofiber\/fiber\b/i), 'go.mod -> github.com/gofiber/fiber', 4)
|
|
447
|
+
])
|
|
448
|
+
},
|
|
449
|
+
{
|
|
450
|
+
id: 'echo',
|
|
451
|
+
kind: 'backend',
|
|
452
|
+
language: 'go',
|
|
453
|
+
projectType: 'api',
|
|
454
|
+
detect: ctx => buildDetection('echo', [
|
|
455
|
+
evidenceIf(textMatches(ctx.goModText, /\bgithub\.com\/labstack\/echo\b/i), 'go.mod -> github.com/labstack/echo', 4)
|
|
456
|
+
])
|
|
457
|
+
},
|
|
458
|
+
{
|
|
459
|
+
id: 'rails',
|
|
460
|
+
kind: 'backend',
|
|
461
|
+
language: 'ruby',
|
|
462
|
+
projectType: 'api',
|
|
463
|
+
detect: ctx => buildDetection('rails', [
|
|
464
|
+
evidenceIf(textMatches(ctx.gemfileText, /^\s*gem\s+["']rails["']/im), 'Gemfile -> rails', 4),
|
|
465
|
+
evidenceIf(ctx.fileExists('bin', 'rails'), 'found bin/rails', 2)
|
|
466
|
+
])
|
|
467
|
+
},
|
|
468
|
+
{
|
|
469
|
+
id: 'spring',
|
|
470
|
+
kind: 'backend',
|
|
471
|
+
language: 'java',
|
|
472
|
+
projectType: 'api',
|
|
473
|
+
detect: ctx => buildDetection('spring', [
|
|
474
|
+
evidenceIf(textMatches(ctx.pomText, /spring-boot/i) || textMatches(ctx.gradleText, /spring-boot/i), 'Java build -> spring-boot', 4)
|
|
475
|
+
])
|
|
476
|
+
},
|
|
477
|
+
{
|
|
478
|
+
id: 'aspnet',
|
|
479
|
+
kind: 'backend',
|
|
480
|
+
language: 'csharp',
|
|
481
|
+
projectType: 'api',
|
|
482
|
+
detect: ctx => buildDetection('aspnet', [
|
|
483
|
+
evidenceIf(textMatches(ctx.csprojText, /Microsoft\.NET\.Sdk\.Web/i), '.csproj -> Microsoft.NET.Sdk.Web', 4),
|
|
484
|
+
evidenceIf(textMatches(ctx.programCsText, /WebApplication\.CreateBuilder/i), 'Program.cs -> WebApplication.CreateBuilder', 2)
|
|
485
|
+
])
|
|
486
|
+
},
|
|
487
|
+
{
|
|
488
|
+
id: 'tauri',
|
|
489
|
+
kind: 'desktop',
|
|
490
|
+
language: 'rust',
|
|
491
|
+
projectType: 'desktop',
|
|
492
|
+
detect: ctx => buildDetection('tauri', [
|
|
493
|
+
evidenceIf(hasDependency(ctx.depNames, '@tauri-apps/api'), 'package.json -> @tauri-apps/api', 4),
|
|
494
|
+
evidenceIf(ctx.fileExists('src-tauri', 'tauri.conf.json') || ctx.fileExists('src-tauri', 'tauri.conf.json5'), 'found src-tauri/tauri.conf.*', 4),
|
|
495
|
+
evidenceIf(ctx.fileExists('src-tauri', 'Cargo.toml'), 'found src-tauri/Cargo.toml', 2)
|
|
496
|
+
])
|
|
497
|
+
},
|
|
498
|
+
{
|
|
499
|
+
id: 'axum',
|
|
500
|
+
kind: 'backend',
|
|
501
|
+
language: 'rust',
|
|
502
|
+
projectType: 'api',
|
|
503
|
+
detect: ctx => buildDetection('axum', [
|
|
504
|
+
evidenceIf(textMatches(ctx.cargoText, /\baxum\b/i), 'Cargo.toml -> axum', 4)
|
|
505
|
+
])
|
|
506
|
+
},
|
|
507
|
+
{
|
|
508
|
+
id: 'actix',
|
|
509
|
+
kind: 'backend',
|
|
510
|
+
language: 'rust',
|
|
511
|
+
projectType: 'api',
|
|
512
|
+
detect: ctx => buildDetection('actix', [
|
|
513
|
+
evidenceIf(textMatches(ctx.cargoText, /\bactix-web\b/i), 'Cargo.toml -> actix-web', 4)
|
|
514
|
+
])
|
|
515
|
+
},
|
|
516
|
+
{
|
|
517
|
+
id: 'flutter',
|
|
518
|
+
kind: 'mobile',
|
|
519
|
+
projectType: 'mobile',
|
|
520
|
+
detect: ctx => buildDetection('flutter', [
|
|
521
|
+
evidenceIf(textMatches(ctx.pubspecText, /^\s*flutter\s*:/im), 'pubspec.yaml -> flutter', 4)
|
|
522
|
+
])
|
|
523
|
+
},
|
|
524
|
+
{
|
|
525
|
+
id: 'react-native',
|
|
526
|
+
kind: 'mobile',
|
|
527
|
+
projectType: 'mobile',
|
|
528
|
+
detect: ctx => buildDetection('react-native', [
|
|
529
|
+
evidenceIf(hasDependency(ctx.depNames, 'react-native', 'expo'), 'package.json -> react-native/expo', 4)
|
|
530
|
+
])
|
|
531
|
+
}
|
|
532
|
+
];
|
|
533
|
+
|
|
163
534
|
function scanProject() {
|
|
164
535
|
const pkg = readJsonIfExists('package.json') || {};
|
|
536
|
+
const composer = readJsonIfExists('composer.json') || {};
|
|
537
|
+
const rootEntries = fs.existsSync(targetDir) ? fs.readdirSync(targetDir) : [];
|
|
538
|
+
const rootCsproj = rootEntries.find(name => name.endsWith('.csproj')) || '';
|
|
165
539
|
const deps = {
|
|
166
540
|
...(pkg.dependencies || {}),
|
|
167
541
|
...(pkg.devDependencies || {}),
|
|
168
542
|
...(pkg.peerDependencies || {})
|
|
169
543
|
};
|
|
170
544
|
const depNames = new Set(Object.keys(deps));
|
|
171
|
-
const
|
|
545
|
+
const composerDeps = {
|
|
546
|
+
...(composer.require || {}),
|
|
547
|
+
...(composer['require-dev'] || {})
|
|
548
|
+
};
|
|
549
|
+
const composerDepNames = new Set(Object.keys(composerDeps));
|
|
550
|
+
const ctx = {
|
|
551
|
+
pkg,
|
|
552
|
+
composer,
|
|
553
|
+
depNames,
|
|
554
|
+
composerDepNames,
|
|
555
|
+
rootEntries,
|
|
556
|
+
fileExists,
|
|
557
|
+
requirementsText: readTextIfExists('requirements.txt'),
|
|
558
|
+
pyprojectText: readTextIfExists('pyproject.toml'),
|
|
559
|
+
goModText: readTextIfExists('go.mod'),
|
|
560
|
+
pubspecText: readTextIfExists('pubspec.yaml'),
|
|
561
|
+
gemfileText: readTextIfExists('Gemfile'),
|
|
562
|
+
pomText: readTextIfExists('pom.xml'),
|
|
563
|
+
gradleText: `${readTextIfExists('build.gradle')}\n${readTextIfExists('build.gradle.kts')}`,
|
|
564
|
+
cargoText: `${readTextIfExists('Cargo.toml')}\n${readTextIfExists('src-tauri', 'Cargo.toml')}`,
|
|
565
|
+
csprojText: rootCsproj ? readTextIfExists(rootCsproj) : '',
|
|
566
|
+
programCsText: readTextIfExists('Program.cs')
|
|
567
|
+
};
|
|
568
|
+
|
|
569
|
+
const languageDetections = LANGUAGE_DETECTORS
|
|
570
|
+
.map(detector => detector.detect(ctx))
|
|
571
|
+
.filter(result => result.score >= 2)
|
|
572
|
+
.sort((a, b) => b.score - a.score);
|
|
573
|
+
|
|
574
|
+
const frameworkDetails = FRAMEWORK_DETECTORS
|
|
575
|
+
.map(detector => {
|
|
576
|
+
const result = detector.detect(ctx);
|
|
577
|
+
if (result.score < 3) return null;
|
|
578
|
+
return {
|
|
579
|
+
...result,
|
|
580
|
+
kind: detector.kind,
|
|
581
|
+
language: detector.language,
|
|
582
|
+
projectType: detector.projectType,
|
|
583
|
+
confidence: confidenceLabel(result.score)
|
|
584
|
+
};
|
|
585
|
+
})
|
|
586
|
+
.filter(Boolean)
|
|
587
|
+
.sort((a, b) => b.score - a.score);
|
|
588
|
+
|
|
589
|
+
const frameworkLanguageScores = frameworkDetails.reduce((acc, item) => {
|
|
590
|
+
if (!item.language) return acc;
|
|
591
|
+
acc[item.language] = (acc[item.language] || 0) + item.score;
|
|
592
|
+
return acc;
|
|
593
|
+
}, {});
|
|
594
|
+
const primaryLanguage = Object.keys(frameworkLanguageScores).sort((a, b) => frameworkLanguageScores[b] - frameworkLanguageScores[a])[0]
|
|
595
|
+
|| languageDetections[0]?.id
|
|
596
|
+
|| 'typescript';
|
|
597
|
+
const frameworks = uniq(frameworkDetails.map(item => item.id)).filter(name => FRAMEWORK_CHOICES.includes(name));
|
|
172
598
|
const infrastructure = [];
|
|
173
|
-
let primaryLanguage = '';
|
|
174
599
|
let projectType = '';
|
|
175
600
|
let database = 'none';
|
|
176
601
|
let testFramework = 'auto-detect';
|
|
177
602
|
let cloud = 'none';
|
|
178
603
|
let ciCd = 'none';
|
|
179
604
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
605
|
+
if (frameworkDetails.some(item => item.projectType === 'desktop')) projectType = 'desktop';
|
|
606
|
+
else if (frameworkDetails.some(item => item.projectType === 'mobile')) projectType = 'mobile';
|
|
607
|
+
else if (frameworkDetails.some(item => item.kind === 'frontend') && frameworkDetails.some(item => item.kind === 'backend')) projectType = 'fullstack';
|
|
608
|
+
else if (frameworkDetails.some(item => item.kind === 'backend')) projectType = 'api';
|
|
609
|
+
else if (frameworkDetails.some(item => item.kind === 'frontend')) projectType = 'web-app';
|
|
610
|
+
else if (fileExists('package.json') && pkg.bin) projectType = 'cli';
|
|
611
|
+
else if (['php', 'python', 'go', 'java', 'ruby', 'csharp', 'rust'].includes(primaryLanguage)) projectType = 'api';
|
|
183
612
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
addFramework('express', depNames.has('express'));
|
|
191
|
-
addFramework('nestjs', depNames.has('@nestjs/core'));
|
|
192
|
-
addFramework('fastapi', fileExists('requirements.txt') && /fastapi/i.test(readTextIfExists('requirements.txt')));
|
|
193
|
-
addFramework('django', fileExists('requirements.txt') && /django/i.test(readTextIfExists('requirements.txt')));
|
|
194
|
-
addFramework('tauri', depNames.has('@tauri-apps/api') || fileExists('src-tauri', 'tauri.conf.json') || fileExists('src-tauri', 'tauri.conf.json5'));
|
|
195
|
-
addFramework('flutter', fileExists('pubspec.yaml') && /flutter:/i.test(readTextIfExists('pubspec.yaml')));
|
|
196
|
-
addFramework('react-native', depNames.has('react-native') || depNames.has('expo'));
|
|
197
|
-
addFramework('laravel', fileExists('artisan') || depNames.has('laravel'));
|
|
198
|
-
addFramework('gin', fileExists('go.mod') && /\bgithub\.com\/gin-gonic\/gin\b/i.test(readTextIfExists('go.mod')));
|
|
199
|
-
addFramework('fiber', fileExists('go.mod') && /\bgithub\.com\/gofiber\/fiber\b/i.test(readTextIfExists('go.mod')));
|
|
200
|
-
|
|
201
|
-
if (fileExists('tsconfig.json') || depNames.has('typescript')) primaryLanguage = 'typescript';
|
|
202
|
-
else if (fileExists('Cargo.toml') || fileExists('src-tauri', 'Cargo.toml')) primaryLanguage = 'rust';
|
|
203
|
-
else if (fileExists('go.mod')) primaryLanguage = 'go';
|
|
204
|
-
else if (fileExists('requirements.txt') || fileExists('pyproject.toml')) primaryLanguage = 'python';
|
|
205
|
-
else if (fileExists('composer.json')) primaryLanguage = 'php';
|
|
206
|
-
else if (depNames.size > 0 || fileExists('package.json')) primaryLanguage = 'javascript';
|
|
207
|
-
|
|
208
|
-
if (frameworks.includes('tauri')) projectType = 'desktop';
|
|
209
|
-
else if (frameworks.includes('flutter') || frameworks.includes('react-native')) projectType = 'mobile';
|
|
210
|
-
else if (frameworks.some(f => ['express', 'fastapi', 'django', 'laravel', 'gin', 'fiber', 'nestjs'].includes(f))) {
|
|
211
|
-
projectType = frameworks.some(f => ['react', 'next', 'vue', 'nuxt', 'svelte', 'angular'].includes(f)) ? 'fullstack' : 'api';
|
|
212
|
-
} else if (frameworks.some(f => ['react', 'next', 'vue', 'nuxt', 'svelte', 'angular'].includes(f))) {
|
|
213
|
-
projectType = 'web-app';
|
|
214
|
-
} else if (fileExists('package.json') && pkg.bin) {
|
|
215
|
-
projectType = 'cli';
|
|
216
|
-
}
|
|
613
|
+
if (hasDependency(depNames, 'pg', 'postgres', '@prisma/client') || /postgres/i.test(JSON.stringify(pkg))) database = 'postgres';
|
|
614
|
+
else if (hasDependency(depNames, 'mysql', 'mysql2')) database = 'mysql';
|
|
615
|
+
else if (hasDependency(depNames, 'sqlite3', 'better-sqlite3', 'rusqlite')) database = 'sqlite';
|
|
616
|
+
else if (hasDependency(depNames, 'mongodb', 'mongoose')) database = 'mongodb';
|
|
617
|
+
else if (hasDependency(depNames, 'redis', 'ioredis')) database = 'redis';
|
|
618
|
+
else if (hasDependency(depNames, '@aws-sdk/client-dynamodb')) database = 'dynamodb';
|
|
217
619
|
|
|
218
|
-
if (
|
|
219
|
-
else if (depNames
|
|
220
|
-
else if (depNames
|
|
221
|
-
else if (
|
|
222
|
-
else if (
|
|
223
|
-
else if (depNames.has('@aws-sdk/client-dynamodb')) database = 'dynamodb';
|
|
224
|
-
|
|
225
|
-
if (depNames.has('vitest')) testFramework = 'vitest';
|
|
226
|
-
else if (depNames.has('jest')) testFramework = 'jest';
|
|
227
|
-
else if (depNames.has('pytest') || /pytest/i.test(readTextIfExists('requirements.txt'))) testFramework = 'pytest';
|
|
228
|
-
else if (depNames.has('phpunit')) testFramework = 'phpunit';
|
|
229
|
-
else if (depNames.has('pest')) testFramework = 'pest';
|
|
620
|
+
if (hasDependency(depNames, 'vitest')) testFramework = 'vitest';
|
|
621
|
+
else if (hasDependency(depNames, 'jest')) testFramework = 'jest';
|
|
622
|
+
else if (hasDependency(depNames, 'pytest') || textMatches(ctx.requirementsText, /\bpytest\b/i) || textMatches(ctx.pyprojectText, /\bpytest\b/i)) testFramework = 'pytest';
|
|
623
|
+
else if (hasComposerDependency(composerDepNames, 'phpunit/phpunit')) testFramework = 'phpunit';
|
|
624
|
+
else if (hasComposerDependency(composerDepNames, 'pestphp/pest')) testFramework = 'pest';
|
|
230
625
|
else if (fileExists('go.mod')) testFramework = 'go test';
|
|
626
|
+
else if (textMatches(ctx.gemfileText, /^\s*gem\s+["']rspec["']/im)) testFramework = 'rspec';
|
|
231
627
|
|
|
232
628
|
if (fileExists('Dockerfile') || fileExists('docker-compose.yml') || fileExists('docker-compose.yaml')) {
|
|
233
629
|
infrastructure.push('docker');
|
|
@@ -249,12 +645,19 @@ function scanProject() {
|
|
|
249
645
|
else if (fileExists('.circleci', 'config.yml')) ciCd = 'circleci';
|
|
250
646
|
else if (fileExists('Jenkinsfile')) ciCd = 'jenkins';
|
|
251
647
|
|
|
648
|
+
const scanEvidence = [
|
|
649
|
+
...(languageDetections[0] ? [`language:${languageDetections[0].id} => ${languageDetections[0].evidence.join('; ')}`] : []),
|
|
650
|
+
...frameworkDetails.map(item => `${item.id} [${item.confidence}] => ${item.evidence.join('; ')}`)
|
|
651
|
+
];
|
|
652
|
+
|
|
252
653
|
return {
|
|
253
654
|
project_name: path.basename(targetDir),
|
|
254
|
-
description: pkg.description || 'A software project',
|
|
655
|
+
description: pkg.description || composer.description || 'A software project',
|
|
255
656
|
project_type: PROJECT_TYPE_CHOICES.includes(projectType) ? projectType : 'web-app',
|
|
256
657
|
primary_language: LANGUAGE_CHOICES.includes(primaryLanguage) ? primaryLanguage : 'typescript',
|
|
257
|
-
frameworks
|
|
658
|
+
frameworks,
|
|
659
|
+
framework_details: frameworkDetails,
|
|
660
|
+
scan_evidence: scanEvidence,
|
|
258
661
|
database: DATABASE_CHOICES.includes(database) ? database : 'none',
|
|
259
662
|
package_manager: PACKAGE_MANAGER_CHOICES.includes(detectPackageManager()) ? detectPackageManager() : 'auto-detect',
|
|
260
663
|
uses_docker: infrastructure.includes('docker'),
|
|
@@ -277,7 +680,7 @@ function resolveAgentTable(data) {
|
|
|
277
680
|
const lang = (data.primary_language || "").toLowerCase();
|
|
278
681
|
|
|
279
682
|
// Frontend
|
|
280
|
-
const frontendFws = frameworks.filter(f =>
|
|
683
|
+
const frontendFws = frameworks.filter(f => FRONTEND_FRAMEWORKS.includes(f));
|
|
281
684
|
if (frontendFws.length > 0) {
|
|
282
685
|
let feSkills = [];
|
|
283
686
|
frontendFws.forEach(fw => {
|
|
@@ -288,14 +691,14 @@ function resolveAgentTable(data) {
|
|
|
288
691
|
|
|
289
692
|
// Backend
|
|
290
693
|
let beSkills = [...(LANG_SKILLS[lang] || ["api-design"])];
|
|
291
|
-
const backendFws = frameworks.filter(f =>
|
|
694
|
+
const backendFws = frameworks.filter(f => BACKEND_FRAMEWORKS.includes(f));
|
|
292
695
|
backendFws.forEach(fw => {
|
|
293
696
|
beSkills.push(...(FRAMEWORK_SKILLS[fw] || []));
|
|
294
697
|
});
|
|
295
698
|
rows.push(["Backend API", "backend-specialist", Array.from(new Set(beSkills)).slice(0, 3).join(", ")]);
|
|
296
699
|
|
|
297
700
|
// Mobile/Desktop
|
|
298
|
-
const mobileFws = frameworks.filter(f =>
|
|
701
|
+
const mobileFws = frameworks.filter(f => MOBILE_DESKTOP_FRAMEWORKS.includes(f));
|
|
299
702
|
if (mobileFws.length > 0) {
|
|
300
703
|
let mobileSkills = [];
|
|
301
704
|
mobileFws.forEach(fw => {
|
|
@@ -318,12 +721,25 @@ function resolveAgentTable(data) {
|
|
|
318
721
|
|
|
319
722
|
// --- File Generators ---
|
|
320
723
|
|
|
321
|
-
function generateProjectContext(data) {
|
|
322
|
-
const agentTable = resolveAgentTable(data);
|
|
323
|
-
const autoSkills = resolveSkills(data);
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
724
|
+
function generateProjectContext(data) {
|
|
725
|
+
const agentTable = resolveAgentTable(data);
|
|
726
|
+
const autoSkills = resolveSkills(data);
|
|
727
|
+
const scanEvidenceBlock = (data.scan_evidence || []).length > 0
|
|
728
|
+
? `
|
|
729
|
+
---
|
|
730
|
+
|
|
731
|
+
## Scan Evidence
|
|
732
|
+
|
|
733
|
+
These detections were inferred from the current codebase during \`init --scan\`:
|
|
734
|
+
|
|
735
|
+
\`\`\`text
|
|
736
|
+
${(data.scan_evidence || []).map(line => '- ' + line).join('\n')}
|
|
737
|
+
\`\`\`
|
|
738
|
+
`
|
|
739
|
+
: '';
|
|
740
|
+
|
|
741
|
+
const agentRows = agentTable.map(([task, agent, skills]) => `| ${task} | \`${agent}\` | ${skills} |`).join('\n');
|
|
742
|
+
const autoSkillsList = autoSkills.map(s => `- ${s}`).join('\n');
|
|
327
743
|
|
|
328
744
|
return `# PROJECT_CONTEXT.md
|
|
329
745
|
# Generated by thachvd-kit init wizard on ${new Date().toISOString().split('T')[0]}
|
|
@@ -347,11 +763,13 @@ stack:
|
|
|
347
763
|
infrastructure: [${(data.infrastructure || []).join(', ')}]
|
|
348
764
|
package_manager: ${data.package_manager || 'auto-detect'}
|
|
349
765
|
test_framework: ${data.test_framework || 'auto-detect'}
|
|
350
|
-
\`\`\`
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
766
|
+
\`\`\`
|
|
767
|
+
|
|
768
|
+
${scanEvidenceBlock}
|
|
769
|
+
|
|
770
|
+
---
|
|
771
|
+
|
|
772
|
+
## Agent Routing
|
|
355
773
|
|
|
356
774
|
| Task Type | Agent | Primary Skills |
|
|
357
775
|
|-----------|-------|----------------|
|
|
@@ -510,6 +928,11 @@ async function main() {
|
|
|
510
928
|
if (useScan) {
|
|
511
929
|
console.log(`${pc.bold(' Scan result:')} ${scanned.primary_language} | ${scanned.frameworks.join(', ') || 'no framework'}`);
|
|
512
930
|
console.log(` ${pc.bold('Detected infra:')} ${scanned.infrastructure.join(', ') || 'none'}\n`);
|
|
931
|
+
if ((scanned.scan_evidence || []).length > 0) {
|
|
932
|
+
console.log(` ${pc.bold('Evidence:')}`);
|
|
933
|
+
scanned.scan_evidence.forEach(line => console.log(` · ${line}`));
|
|
934
|
+
console.log('');
|
|
935
|
+
}
|
|
513
936
|
}
|
|
514
937
|
|
|
515
938
|
const { project_name, description, project_type } = await askQuestions([
|
|
@@ -578,6 +1001,8 @@ async function main() {
|
|
|
578
1001
|
data.package_manager = data.package_manager || scanned?.package_manager || 'auto-detect';
|
|
579
1002
|
data.test_framework = data.test_framework || scanned?.test_framework || 'auto-detect';
|
|
580
1003
|
data.max_file_lines = data.max_file_lines || scanned?.max_file_lines || '300';
|
|
1004
|
+
data.framework_details = scanned?.framework_details || [];
|
|
1005
|
+
data.scan_evidence = scanned?.scan_evidence || [];
|
|
581
1006
|
data.infrastructure = [];
|
|
582
1007
|
if (data.uses_docker) data.infrastructure.push('docker');
|
|
583
1008
|
if (data.cloud !== 'none') data.infrastructure.push(data.cloud);
|
package/package.json
CHANGED
package/scripts/init.py
CHANGED
|
@@ -13,6 +13,7 @@ import sys
|
|
|
13
13
|
import os
|
|
14
14
|
import argparse
|
|
15
15
|
import json
|
|
16
|
+
import re
|
|
16
17
|
from pathlib import Path
|
|
17
18
|
from datetime import datetime
|
|
18
19
|
|
|
@@ -35,12 +36,13 @@ def ask(prompt, default="", choices=None):
|
|
|
35
36
|
value = input(f" {bold('→')} {prompt}{display_default}: ").strip()
|
|
36
37
|
return value if value else default
|
|
37
38
|
|
|
38
|
-
def ask_multi(prompt, choices, default=None):
|
|
39
|
-
"""Prompt for comma-separated selection from choices."""
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
39
|
+
def ask_multi(prompt, choices, default=None):
|
|
40
|
+
"""Prompt for comma-separated selection from choices."""
|
|
41
|
+
display_default = ", ".join(default) if isinstance(default, list) else (default or "none")
|
|
42
|
+
print(f"\n {cyan('Available:')} {', '.join(f'{i+1}) {c}' for i, c in enumerate(choices))}")
|
|
43
|
+
raw = input(f" {bold('→')} {prompt} (numbers or names, comma-separated) [{dim(display_default)}]: ").strip()
|
|
44
|
+
if not raw:
|
|
45
|
+
return default or []
|
|
44
46
|
selected = []
|
|
45
47
|
for part in raw.replace(",", " ").split():
|
|
46
48
|
part = part.strip()
|
|
@@ -71,32 +73,45 @@ def section(title):
|
|
|
71
73
|
|
|
72
74
|
# ─── Skill resolver ──────────────────────────────────────────────────────────
|
|
73
75
|
|
|
74
|
-
LANG_SKILLS = {
|
|
75
|
-
"typescript": ["nodejs-best-practices", "api-design"],
|
|
76
|
-
"javascript": ["nodejs-best-practices", "api-design"],
|
|
77
|
-
"python": ["python-patterns"],
|
|
78
|
-
"go": ["golang-patterns", "golang-testing"],
|
|
79
|
-
"php": ["
|
|
80
|
-
"rust": ["rust-pro"],
|
|
81
|
-
"java": ["api-design"],
|
|
82
|
-
"
|
|
83
|
-
"
|
|
84
|
-
"
|
|
85
|
-
|
|
76
|
+
LANG_SKILLS = {
|
|
77
|
+
"typescript": ["nodejs-best-practices", "api-design"],
|
|
78
|
+
"javascript": ["nodejs-best-practices", "api-design"],
|
|
79
|
+
"python": ["python-patterns"],
|
|
80
|
+
"go": ["golang-patterns", "golang-testing"],
|
|
81
|
+
"php": ["api-design"],
|
|
82
|
+
"rust": ["rust-pro"],
|
|
83
|
+
"java": ["api-design"],
|
|
84
|
+
"ruby": ["api-design"],
|
|
85
|
+
"swift": ["mobile-design"],
|
|
86
|
+
"kotlin": ["mobile-design"],
|
|
87
|
+
"csharp": ["api-design"],
|
|
88
|
+
}
|
|
86
89
|
|
|
87
90
|
FRAMEWORK_SKILLS = {
|
|
88
91
|
"vue": ["frontend-design", "tailwind-patterns"],
|
|
89
92
|
"react": ["react-frontend", "frontend-design"],
|
|
90
93
|
"next": ["nextjs-react-expert", "frontend-design"],
|
|
91
|
-
"nuxt": ["frontend-design"],
|
|
92
|
-
"svelte": ["frontend-design"],
|
|
93
|
-
"
|
|
94
|
-
"
|
|
95
|
-
"
|
|
96
|
-
"
|
|
97
|
-
"
|
|
98
|
-
"
|
|
94
|
+
"nuxt": ["frontend-design"],
|
|
95
|
+
"svelte": ["frontend-design"],
|
|
96
|
+
"angular": ["frontend-design"],
|
|
97
|
+
"laravel": ["laravel-patterns", "laravel-security", "laravel-tdd"],
|
|
98
|
+
"cakephp": ["api-design"],
|
|
99
|
+
"symfony": ["api-design"],
|
|
100
|
+
"codeigniter": ["api-design"],
|
|
101
|
+
"express": ["nodejs-best-practices", "api-design"],
|
|
102
|
+
"fastapi": ["python-patterns", "api-design"],
|
|
103
|
+
"django": ["python-patterns", "api-design"],
|
|
104
|
+
"flask": ["python-patterns", "api-design"],
|
|
105
|
+
"gin": ["golang-patterns", "golang-testing"],
|
|
106
|
+
"fiber": ["golang-patterns", "golang-testing"],
|
|
107
|
+
"echo": ["golang-patterns", "golang-testing"],
|
|
108
|
+
"nestjs": ["nodejs-best-practices", "api-design"],
|
|
99
109
|
"tauri": ["rust-pro", "desktop-design"],
|
|
110
|
+
"axum": ["rust-pro", "api-design"],
|
|
111
|
+
"actix": ["rust-pro", "api-design"],
|
|
112
|
+
"rails": ["api-design"],
|
|
113
|
+
"spring": ["api-design"],
|
|
114
|
+
"aspnet": ["api-design"],
|
|
100
115
|
"flutter": ["mobile-design"],
|
|
101
116
|
"react-native": ["mobile-design"],
|
|
102
117
|
}
|
|
@@ -128,14 +143,19 @@ def resolve_skills(data):
|
|
|
128
143
|
return sorted(skills)
|
|
129
144
|
|
|
130
145
|
FRAMEWORKS_ALL = ["vue", "react", "next", "nuxt", "svelte", "angular",
|
|
131
|
-
"laravel", "
|
|
132
|
-
"
|
|
133
|
-
|
|
146
|
+
"laravel", "cakephp", "symfony", "codeigniter",
|
|
147
|
+
"express", "fastapi", "django", "flask", "gin", "fiber", "echo", "nestjs",
|
|
148
|
+
"rails", "spring", "aspnet",
|
|
149
|
+
"tauri", "axum", "actix", "flutter", "react-native"]
|
|
150
|
+
LANGUAGES_ALL = ["typescript", "javascript", "python", "go", "php", "rust", "java", "ruby", "swift", "kotlin", "csharp", "c", "cpp"]
|
|
134
151
|
PROJECT_TYPES_ALL = ["web-app", "api", "mobile", "desktop", "cli", "library", "fullstack"]
|
|
135
152
|
DATABASES_ALL = ["postgres", "mysql", "sqlite", "mongodb", "redis", "dynamodb", "supabase", "prisma", "none"]
|
|
136
153
|
PACKAGE_MANAGERS_ALL = ["npm", "pnpm", "yarn", "bun", "pip", "uv", "poetry", "go mod", "cargo", "composer", "auto-detect"]
|
|
137
154
|
TEST_FRAMEWORKS_ALL = ["jest", "vitest", "pytest", "go test", "phpunit", "pest", "rspec", "junit", "auto-detect"]
|
|
138
155
|
CLOUDS_ALL = ["aws", "gcp", "azure", "vercel", "fly", "railway", "digitalocean", "none"]
|
|
156
|
+
FRONTEND_FRAMEWORKS = ["vue", "react", "next", "nuxt", "svelte", "angular"]
|
|
157
|
+
BACKEND_FRAMEWORKS = ["laravel", "cakephp", "symfony", "codeigniter", "express", "fastapi", "django", "flask", "gin", "fiber", "echo", "nestjs", "rails", "spring", "aspnet", "axum", "actix"]
|
|
158
|
+
MOBILE_DESKTOP_FRAMEWORKS = ["tauri", "flutter", "react-native"]
|
|
139
159
|
|
|
140
160
|
def read_text_if_exists(*parts):
|
|
141
161
|
path = Path.cwd().joinpath(*parts)
|
|
@@ -168,75 +188,241 @@ def detect_package_manager():
|
|
|
168
188
|
def uniq(values):
|
|
169
189
|
return list(dict.fromkeys(v for v in values if v))
|
|
170
190
|
|
|
191
|
+
def evidence_if(condition, label, weight=1):
|
|
192
|
+
return {"label": label, "weight": weight} if condition else None
|
|
193
|
+
|
|
194
|
+
def build_detection(id, evidence, **meta):
|
|
195
|
+
hits = [hit for hit in evidence if hit]
|
|
196
|
+
score = sum(hit["weight"] for hit in hits)
|
|
197
|
+
return {
|
|
198
|
+
"id": id,
|
|
199
|
+
"score": score,
|
|
200
|
+
"evidence": [hit["label"] for hit in hits],
|
|
201
|
+
**meta,
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
def confidence_label(score):
|
|
205
|
+
if score >= 6: return "high"
|
|
206
|
+
if score >= 3: return "medium"
|
|
207
|
+
return "low"
|
|
208
|
+
|
|
209
|
+
def has_dependency(dep_names, *names):
|
|
210
|
+
return any(name in dep_names for name in names)
|
|
211
|
+
|
|
212
|
+
def has_composer_dependency(composer_dep_names, *names):
|
|
213
|
+
return any(name in composer_dep_names for name in names)
|
|
214
|
+
|
|
215
|
+
def text_matches(text, pattern):
|
|
216
|
+
return bool(text and re.search(pattern, text, re.IGNORECASE | re.MULTILINE))
|
|
217
|
+
|
|
171
218
|
def scan_project():
|
|
172
219
|
pkg = read_json_if_exists("package.json") or {}
|
|
220
|
+
composer = read_json_if_exists("composer.json") or {}
|
|
221
|
+
root_entries = [p.name for p in Path.cwd().iterdir()] if Path.cwd().exists() else []
|
|
222
|
+
root_csproj = next((name for name in root_entries if name.endswith(".csproj")), "")
|
|
173
223
|
deps = {}
|
|
174
224
|
for key in ("dependencies", "devDependencies", "peerDependencies"):
|
|
175
225
|
deps.update(pkg.get(key, {}) or {})
|
|
176
226
|
dep_names = set(deps.keys())
|
|
177
|
-
|
|
227
|
+
composer_deps = {}
|
|
228
|
+
for key in ("require", "require-dev"):
|
|
229
|
+
composer_deps.update(composer.get(key, {}) or {})
|
|
230
|
+
composer_dep_names = set(composer_deps.keys())
|
|
231
|
+
requirements = read_text_if_exists("requirements.txt")
|
|
232
|
+
pyproject = read_text_if_exists("pyproject.toml")
|
|
233
|
+
go_mod = read_text_if_exists("go.mod")
|
|
234
|
+
pubspec = read_text_if_exists("pubspec.yaml")
|
|
235
|
+
gemfile = read_text_if_exists("Gemfile")
|
|
236
|
+
pom = read_text_if_exists("pom.xml")
|
|
237
|
+
gradle = f"{read_text_if_exists('build.gradle')}\n{read_text_if_exists('build.gradle.kts')}"
|
|
238
|
+
cargo = f"{read_text_if_exists('Cargo.toml')}\n{read_text_if_exists('src-tauri/Cargo.toml')}"
|
|
239
|
+
csproj = read_text_if_exists(root_csproj) if root_csproj else ""
|
|
240
|
+
program_cs = read_text_if_exists("Program.cs")
|
|
241
|
+
|
|
242
|
+
language_detections = [
|
|
243
|
+
build_detection("typescript", [
|
|
244
|
+
evidence_if(Path("tsconfig.json").exists(), "found tsconfig.json", 4),
|
|
245
|
+
evidence_if(has_dependency(dep_names, "typescript", "ts-node", "tsx"), "package.json -> typescript toolchain", 2),
|
|
246
|
+
]),
|
|
247
|
+
build_detection("javascript", [
|
|
248
|
+
evidence_if(Path("package.json").exists(), "found package.json", 2),
|
|
249
|
+
evidence_if(bool(dep_names), "package.json -> JavaScript dependency graph", 1),
|
|
250
|
+
]),
|
|
251
|
+
build_detection("python", [
|
|
252
|
+
evidence_if(Path("requirements.txt").exists(), "found requirements.txt", 3),
|
|
253
|
+
evidence_if(Path("pyproject.toml").exists(), "found pyproject.toml", 2),
|
|
254
|
+
evidence_if(text_matches(pyproject, r"\[tool\.(poetry|uv|hatch|pdm)\]"), "pyproject.toml -> Python tool section", 2),
|
|
255
|
+
]),
|
|
256
|
+
build_detection("go", [
|
|
257
|
+
evidence_if(Path("go.mod").exists(), "found go.mod", 4),
|
|
258
|
+
]),
|
|
259
|
+
build_detection("php", [
|
|
260
|
+
evidence_if(Path("composer.json").exists(), "found composer.json", 4),
|
|
261
|
+
evidence_if(bool(composer_dep_names), "composer.json -> PHP dependency graph", 1),
|
|
262
|
+
]),
|
|
263
|
+
build_detection("rust", [
|
|
264
|
+
evidence_if(Path("Cargo.toml").exists(), "found Cargo.toml", 4),
|
|
265
|
+
evidence_if(Path("src-tauri/Cargo.toml").exists(), "found src-tauri/Cargo.toml", 5),
|
|
266
|
+
]),
|
|
267
|
+
build_detection("java", [
|
|
268
|
+
evidence_if(Path("pom.xml").exists(), "found pom.xml", 4),
|
|
269
|
+
evidence_if(Path("build.gradle").exists() or Path("build.gradle.kts").exists(), "found Gradle build file", 3),
|
|
270
|
+
]),
|
|
271
|
+
build_detection("ruby", [
|
|
272
|
+
evidence_if(Path("Gemfile").exists(), "found Gemfile", 4),
|
|
273
|
+
evidence_if(Path("config.ru").exists(), "found config.ru", 1),
|
|
274
|
+
]),
|
|
275
|
+
build_detection("csharp", [
|
|
276
|
+
evidence_if(any(name.endswith(".sln") or name.endswith(".csproj") for name in root_entries), "found .sln/.csproj", 4),
|
|
277
|
+
evidence_if(Path("Program.cs").exists(), "found Program.cs", 1),
|
|
278
|
+
]),
|
|
279
|
+
]
|
|
280
|
+
language_detections = [item for item in language_detections if item["score"] >= 2]
|
|
281
|
+
language_detections.sort(key=lambda item: item["score"], reverse=True)
|
|
282
|
+
|
|
283
|
+
framework_details = [
|
|
284
|
+
{**build_detection("next", [
|
|
285
|
+
evidence_if(has_dependency(dep_names, "next"), "package.json -> next", 4),
|
|
286
|
+
evidence_if(Path("next.config.js").exists() or Path("next.config.mjs").exists() or Path("next.config.ts").exists(), "found next.config.*", 3),
|
|
287
|
+
evidence_if(Path("app").exists() or Path("pages").exists(), "found app/ or pages/", 1),
|
|
288
|
+
]), "kind": "frontend", "project_type": "web-app"},
|
|
289
|
+
{**build_detection("react", [
|
|
290
|
+
evidence_if(has_dependency(dep_names, "react"), "package.json -> react", 4),
|
|
291
|
+
evidence_if(has_dependency(dep_names, "react-dom"), "package.json -> react-dom", 2),
|
|
292
|
+
]), "kind": "frontend", "project_type": "web-app"},
|
|
293
|
+
{**build_detection("vue", [
|
|
294
|
+
evidence_if(has_dependency(dep_names, "vue"), "package.json -> vue", 4),
|
|
295
|
+
]), "kind": "frontend", "project_type": "web-app"},
|
|
296
|
+
{**build_detection("nuxt", [
|
|
297
|
+
evidence_if(has_dependency(dep_names, "nuxt"), "package.json -> nuxt", 4),
|
|
298
|
+
evidence_if(Path("nuxt.config.ts").exists() or Path("nuxt.config.js").exists(), "found nuxt.config.*", 3),
|
|
299
|
+
]), "kind": "frontend", "project_type": "web-app"},
|
|
300
|
+
{**build_detection("svelte", [
|
|
301
|
+
evidence_if(has_dependency(dep_names, "svelte", "@sveltejs/kit"), "package.json -> svelte", 4),
|
|
302
|
+
]), "kind": "frontend", "project_type": "web-app"},
|
|
303
|
+
{**build_detection("angular", [
|
|
304
|
+
evidence_if(has_dependency(dep_names, "@angular/core"), "package.json -> @angular/core", 4),
|
|
305
|
+
evidence_if(Path("angular.json").exists(), "found angular.json", 3),
|
|
306
|
+
]), "kind": "frontend", "project_type": "web-app"},
|
|
307
|
+
{**build_detection("express", [
|
|
308
|
+
evidence_if(has_dependency(dep_names, "express"), "package.json -> express", 4),
|
|
309
|
+
]), "kind": "backend", "project_type": "api", "language": "javascript"},
|
|
310
|
+
{**build_detection("nestjs", [
|
|
311
|
+
evidence_if(has_dependency(dep_names, "@nestjs/core"), "package.json -> @nestjs/core", 4),
|
|
312
|
+
]), "kind": "backend", "project_type": "api", "language": "javascript"},
|
|
313
|
+
{**build_detection("fastapi", [
|
|
314
|
+
evidence_if(text_matches(requirements, r"\bfastapi\b") or text_matches(pyproject, r"\bfastapi\b"), "Python deps -> fastapi", 4),
|
|
315
|
+
]), "kind": "backend", "project_type": "api", "language": "python"},
|
|
316
|
+
{**build_detection("django", [
|
|
317
|
+
evidence_if(text_matches(requirements, r"\bdjango\b") or text_matches(pyproject, r"\bdjango\b"), "Python deps -> django", 4),
|
|
318
|
+
evidence_if(Path("manage.py").exists(), "found manage.py", 2),
|
|
319
|
+
]), "kind": "backend", "project_type": "api", "language": "python"},
|
|
320
|
+
{**build_detection("flask", [
|
|
321
|
+
evidence_if(text_matches(requirements, r"\bflask\b") or text_matches(pyproject, r"\bflask\b"), "Python deps -> flask", 4),
|
|
322
|
+
]), "kind": "backend", "project_type": "api", "language": "python"},
|
|
323
|
+
{**build_detection("laravel", [
|
|
324
|
+
evidence_if(has_composer_dependency(composer_dep_names, "laravel/framework"), "composer.json -> laravel/framework", 4),
|
|
325
|
+
evidence_if(Path("artisan").exists(), "found artisan", 3),
|
|
326
|
+
]), "kind": "backend", "project_type": "api", "language": "php"},
|
|
327
|
+
{**build_detection("cakephp", [
|
|
328
|
+
evidence_if(has_composer_dependency(composer_dep_names, "cakephp/cakephp"), "composer.json -> cakephp/cakephp", 4),
|
|
329
|
+
evidence_if(Path("bin/cake").exists(), "found bin/cake", 3),
|
|
330
|
+
evidence_if(Path("config/app.php").exists(), "found config/app.php", 2),
|
|
331
|
+
evidence_if(Path("app/Config/core.php").exists(), "found app/Config/core.php (CakePHP legacy)", 3),
|
|
332
|
+
evidence_if(Path("app/Controller/AppController.php").exists(), "found app/Controller/AppController.php", 2),
|
|
333
|
+
evidence_if(Path("app/Model/AppModel.php").exists(), "found app/Model/AppModel.php", 2),
|
|
334
|
+
evidence_if(Path("lib/Cake").exists(), "found lib/Cake", 3),
|
|
335
|
+
evidence_if(Path("cake/console/cake").exists(), "found cake/console/cake", 3),
|
|
336
|
+
]), "kind": "backend", "project_type": "api", "language": "php"},
|
|
337
|
+
{**build_detection("symfony", [
|
|
338
|
+
evidence_if(has_composer_dependency(composer_dep_names, "symfony/framework-bundle"), "composer.json -> symfony/framework-bundle", 4),
|
|
339
|
+
evidence_if(Path("bin/console").exists(), "found bin/console", 2),
|
|
340
|
+
evidence_if(Path("config/bundles.php").exists(), "found config/bundles.php", 1),
|
|
341
|
+
]), "kind": "backend", "project_type": "api", "language": "php"},
|
|
342
|
+
{**build_detection("codeigniter", [
|
|
343
|
+
evidence_if(has_composer_dependency(composer_dep_names, "codeigniter4/framework"), "composer.json -> codeigniter4/framework", 4),
|
|
344
|
+
evidence_if(Path("spark").exists(), "found spark", 2),
|
|
345
|
+
evidence_if(Path("app/Config/App.php").exists(), "found app/Config/App.php", 1),
|
|
346
|
+
]), "kind": "backend", "project_type": "api", "language": "php"},
|
|
347
|
+
{**build_detection("gin", [
|
|
348
|
+
evidence_if(text_matches(go_mod, r"\bgithub\.com\/gin-gonic\/gin\b"), "go.mod -> github.com/gin-gonic/gin", 4),
|
|
349
|
+
]), "kind": "backend", "project_type": "api", "language": "go"},
|
|
350
|
+
{**build_detection("fiber", [
|
|
351
|
+
evidence_if(text_matches(go_mod, r"\bgithub\.com\/gofiber\/fiber\b"), "go.mod -> github.com/gofiber/fiber", 4),
|
|
352
|
+
]), "kind": "backend", "project_type": "api", "language": "go"},
|
|
353
|
+
{**build_detection("echo", [
|
|
354
|
+
evidence_if(text_matches(go_mod, r"\bgithub\.com\/labstack\/echo\b"), "go.mod -> github.com/labstack/echo", 4),
|
|
355
|
+
]), "kind": "backend", "project_type": "api", "language": "go"},
|
|
356
|
+
{**build_detection("rails", [
|
|
357
|
+
evidence_if(text_matches(gemfile, r'^\s*gem\s+["\']rails["\']'), "Gemfile -> rails", 4),
|
|
358
|
+
evidence_if(Path("bin/rails").exists(), "found bin/rails", 2),
|
|
359
|
+
]), "kind": "backend", "project_type": "api", "language": "ruby"},
|
|
360
|
+
{**build_detection("spring", [
|
|
361
|
+
evidence_if(text_matches(pom, r"spring-boot") or text_matches(gradle, r"spring-boot"), "Java build -> spring-boot", 4),
|
|
362
|
+
]), "kind": "backend", "project_type": "api", "language": "java"},
|
|
363
|
+
{**build_detection("aspnet", [
|
|
364
|
+
evidence_if(text_matches(csproj, r"Microsoft\.NET\.Sdk\.Web"), ".csproj -> Microsoft.NET.Sdk.Web", 4),
|
|
365
|
+
evidence_if(text_matches(program_cs, r"WebApplication\.CreateBuilder"), "Program.cs -> WebApplication.CreateBuilder", 2),
|
|
366
|
+
]), "kind": "backend", "project_type": "api", "language": "csharp"},
|
|
367
|
+
{**build_detection("tauri", [
|
|
368
|
+
evidence_if(has_dependency(dep_names, "@tauri-apps/api"), "package.json -> @tauri-apps/api", 4),
|
|
369
|
+
evidence_if(Path("src-tauri/tauri.conf.json").exists() or Path("src-tauri/tauri.conf.json5").exists(), "found src-tauri/tauri.conf.*", 4),
|
|
370
|
+
evidence_if(Path("src-tauri/Cargo.toml").exists(), "found src-tauri/Cargo.toml", 2),
|
|
371
|
+
]), "kind": "desktop", "project_type": "desktop", "language": "rust"},
|
|
372
|
+
{**build_detection("axum", [
|
|
373
|
+
evidence_if(text_matches(cargo, r"\baxum\b"), "Cargo.toml -> axum", 4),
|
|
374
|
+
]), "kind": "backend", "project_type": "api", "language": "rust"},
|
|
375
|
+
{**build_detection("actix", [
|
|
376
|
+
evidence_if(text_matches(cargo, r"\bactix-web\b"), "Cargo.toml -> actix-web", 4),
|
|
377
|
+
]), "kind": "backend", "project_type": "api", "language": "rust"},
|
|
378
|
+
{**build_detection("flutter", [
|
|
379
|
+
evidence_if(text_matches(pubspec, r"^\s*flutter\s*:"), "pubspec.yaml -> flutter", 4),
|
|
380
|
+
]), "kind": "mobile", "project_type": "mobile"},
|
|
381
|
+
{**build_detection("react-native", [
|
|
382
|
+
evidence_if(has_dependency(dep_names, "react-native", "expo"), "package.json -> react-native/expo", 4),
|
|
383
|
+
]), "kind": "mobile", "project_type": "mobile"},
|
|
384
|
+
]
|
|
385
|
+
framework_details = [item for item in framework_details if item["score"] >= 3]
|
|
386
|
+
framework_details.sort(key=lambda item: item["score"], reverse=True)
|
|
387
|
+
for item in framework_details:
|
|
388
|
+
item["confidence"] = confidence_label(item["score"])
|
|
389
|
+
|
|
390
|
+
framework_language_scores = {}
|
|
391
|
+
for item in framework_details:
|
|
392
|
+
if item.get("language"):
|
|
393
|
+
framework_language_scores[item["language"]] = framework_language_scores.get(item["language"], 0) + item["score"]
|
|
394
|
+
primary_language = max(framework_language_scores, key=framework_language_scores.get) if framework_language_scores else (language_detections[0]["id"] if language_detections else "typescript")
|
|
395
|
+
frameworks = [item["id"] for item in framework_details if item["id"] in FRAMEWORKS_ALL]
|
|
396
|
+
frameworks = uniq(frameworks)
|
|
178
397
|
infrastructure = []
|
|
179
|
-
primary_language = ""
|
|
180
398
|
project_type = ""
|
|
181
399
|
database = "none"
|
|
182
400
|
test_framework = "auto-detect"
|
|
183
401
|
cloud = "none"
|
|
184
402
|
ci_cd = "none"
|
|
185
403
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
add_framework("react-native", "react-native" in dep_names or "expo" in dep_names)
|
|
207
|
-
add_framework("laravel", Path("artisan").exists())
|
|
208
|
-
add_framework("gin", "github.com/gin-gonic/gin" in go_mod.lower())
|
|
209
|
-
add_framework("fiber", "github.com/gofiber/fiber" in go_mod.lower())
|
|
210
|
-
|
|
211
|
-
if Path("tsconfig.json").exists() or "typescript" in dep_names: primary_language = "typescript"
|
|
212
|
-
elif Path("Cargo.toml").exists() or Path("src-tauri/Cargo.toml").exists(): primary_language = "rust"
|
|
213
|
-
elif Path("go.mod").exists(): primary_language = "go"
|
|
214
|
-
elif Path("requirements.txt").exists() or Path("pyproject.toml").exists(): primary_language = "python"
|
|
215
|
-
elif Path("composer.json").exists(): primary_language = "php"
|
|
216
|
-
elif dep_names or Path("package.json").exists(): primary_language = "javascript"
|
|
217
|
-
|
|
218
|
-
if "tauri" in frameworks: project_type = "desktop"
|
|
219
|
-
elif "flutter" in frameworks or "react-native" in frameworks: project_type = "mobile"
|
|
220
|
-
elif any(f in frameworks for f in ["express", "fastapi", "django", "laravel", "gin", "fiber", "nestjs"]):
|
|
221
|
-
project_type = "fullstack" if any(f in frameworks for f in ["react", "next", "vue", "nuxt", "svelte", "angular"]) else "api"
|
|
222
|
-
elif any(f in frameworks for f in ["react", "next", "vue", "nuxt", "svelte", "angular"]):
|
|
223
|
-
project_type = "web-app"
|
|
224
|
-
elif Path("package.json").exists() and pkg.get("bin"):
|
|
225
|
-
project_type = "cli"
|
|
226
|
-
|
|
227
|
-
if "pg" in dep_names or "postgres" in dep_names or "@prisma/client" in dep_names or "postgres" in json.dumps(pkg).lower(): database = "postgres"
|
|
228
|
-
elif "mysql" in dep_names or "mysql2" in dep_names: database = "mysql"
|
|
229
|
-
elif "sqlite3" in dep_names or "better-sqlite3" in dep_names or "rusqlite" in dep_names: database = "sqlite"
|
|
230
|
-
elif "mongodb" in dep_names or "mongoose" in dep_names: database = "mongodb"
|
|
231
|
-
elif "redis" in dep_names or "ioredis" in dep_names: database = "redis"
|
|
232
|
-
elif "@aws-sdk/client-dynamodb" in dep_names: database = "dynamodb"
|
|
233
|
-
|
|
234
|
-
if "vitest" in dep_names: test_framework = "vitest"
|
|
235
|
-
elif "jest" in dep_names: test_framework = "jest"
|
|
236
|
-
elif "pytest" in dep_names or "pytest" in requirements.lower(): test_framework = "pytest"
|
|
237
|
-
elif "phpunit" in dep_names: test_framework = "phpunit"
|
|
238
|
-
elif "pest" in dep_names: test_framework = "pest"
|
|
404
|
+
if any(item["project_type"] == "desktop" for item in framework_details): project_type = "desktop"
|
|
405
|
+
elif any(item["project_type"] == "mobile" for item in framework_details): project_type = "mobile"
|
|
406
|
+
elif any(item["kind"] == "frontend" for item in framework_details) and any(item["kind"] == "backend" for item in framework_details): project_type = "fullstack"
|
|
407
|
+
elif any(item["kind"] == "backend" for item in framework_details): project_type = "api"
|
|
408
|
+
elif any(item["kind"] == "frontend" for item in framework_details): project_type = "web-app"
|
|
409
|
+
elif Path("package.json").exists() and pkg.get("bin"): project_type = "cli"
|
|
410
|
+
elif primary_language in ["php", "python", "go", "java", "ruby", "csharp", "rust"]: project_type = "api"
|
|
411
|
+
|
|
412
|
+
if has_dependency(dep_names, "pg", "postgres", "@prisma/client") or "postgres" in json.dumps(pkg).lower(): database = "postgres"
|
|
413
|
+
elif has_dependency(dep_names, "mysql", "mysql2"): database = "mysql"
|
|
414
|
+
elif has_dependency(dep_names, "sqlite3", "better-sqlite3", "rusqlite"): database = "sqlite"
|
|
415
|
+
elif has_dependency(dep_names, "mongodb", "mongoose"): database = "mongodb"
|
|
416
|
+
elif has_dependency(dep_names, "redis", "ioredis"): database = "redis"
|
|
417
|
+
elif has_dependency(dep_names, "@aws-sdk/client-dynamodb"): database = "dynamodb"
|
|
418
|
+
|
|
419
|
+
if has_dependency(dep_names, "vitest"): test_framework = "vitest"
|
|
420
|
+
elif has_dependency(dep_names, "jest"): test_framework = "jest"
|
|
421
|
+
elif has_dependency(dep_names, "pytest") or text_matches(requirements, r"\bpytest\b") or text_matches(pyproject, r"\bpytest\b"): test_framework = "pytest"
|
|
422
|
+
elif has_composer_dependency(composer_dep_names, "phpunit/phpunit"): test_framework = "phpunit"
|
|
423
|
+
elif has_composer_dependency(composer_dep_names, "pestphp/pest"): test_framework = "pest"
|
|
239
424
|
elif Path("go.mod").exists(): test_framework = "go test"
|
|
425
|
+
elif text_matches(gemfile, r'^\s*gem\s+["\']rspec["\']'): test_framework = "rspec"
|
|
240
426
|
|
|
241
427
|
if Path("Dockerfile").exists() or Path("docker-compose.yml").exists() or Path("docker-compose.yaml").exists():
|
|
242
428
|
infrastructure.append("docker")
|
|
@@ -256,12 +442,19 @@ def scan_project():
|
|
|
256
442
|
elif Path(".circleci/config.yml").exists(): ci_cd = "circleci"
|
|
257
443
|
elif Path("Jenkinsfile").exists(): ci_cd = "jenkins"
|
|
258
444
|
|
|
445
|
+
scan_evidence = []
|
|
446
|
+
if language_detections:
|
|
447
|
+
scan_evidence.append(f"language:{language_detections[0]['id']} => {'; '.join(language_detections[0]['evidence'])}")
|
|
448
|
+
scan_evidence.extend(f"{item['id']} [{item['confidence']}] => {'; '.join(item['evidence'])}" for item in framework_details)
|
|
449
|
+
|
|
259
450
|
return {
|
|
260
451
|
"project_name": Path.cwd().name,
|
|
261
|
-
"description": pkg.get("description", "A software project"),
|
|
452
|
+
"description": pkg.get("description") or composer.get("description", "A software project"),
|
|
262
453
|
"project_type": project_type if project_type in PROJECT_TYPES_ALL else "web-app",
|
|
263
454
|
"primary_language": primary_language if primary_language in LANGUAGES_ALL else "typescript",
|
|
264
|
-
"frameworks":
|
|
455
|
+
"frameworks": frameworks,
|
|
456
|
+
"framework_details": framework_details,
|
|
457
|
+
"scan_evidence": scan_evidence,
|
|
265
458
|
"database": database if database in DATABASES_ALL else "none",
|
|
266
459
|
"package_manager": detect_package_manager(),
|
|
267
460
|
"uses_docker": "docker" in infrastructure,
|
|
@@ -283,7 +476,7 @@ def resolve_agent_table(data):
|
|
|
283
476
|
lang = data.get("primary_language", "").lower()
|
|
284
477
|
|
|
285
478
|
# Determine frontend agent
|
|
286
|
-
frontend_fws = [f for f in frameworks if f in
|
|
479
|
+
frontend_fws = [f for f in frameworks if f in FRONTEND_FRAMEWORKS]
|
|
287
480
|
if frontend_fws:
|
|
288
481
|
fe_skills = []
|
|
289
482
|
for fw in frontend_fws:
|
|
@@ -292,13 +485,13 @@ def resolve_agent_table(data):
|
|
|
292
485
|
|
|
293
486
|
# Backend
|
|
294
487
|
be_skills = list(LANG_SKILLS.get(lang, ["api-design"]))
|
|
295
|
-
backend_fws = [f for f in frameworks if f in
|
|
488
|
+
backend_fws = [f for f in frameworks if f in BACKEND_FRAMEWORKS]
|
|
296
489
|
for fw in backend_fws:
|
|
297
490
|
be_skills.extend(FRAMEWORK_SKILLS.get(fw, []))
|
|
298
491
|
rows.append(("Backend API", "backend-specialist", ", ".join(set(be_skills[:3]))))
|
|
299
492
|
|
|
300
493
|
# Mobile/Desktop
|
|
301
|
-
mobile_fws = [f for f in frameworks if f in
|
|
494
|
+
mobile_fws = [f for f in frameworks if f in MOBILE_DESKTOP_FRAMEWORKS]
|
|
302
495
|
if mobile_fws:
|
|
303
496
|
mobile_skills = []
|
|
304
497
|
for fw in mobile_fws:
|
|
@@ -316,20 +509,35 @@ def resolve_agent_table(data):
|
|
|
316
509
|
|
|
317
510
|
# ─── Generators ──────────────────────────────────────────────────────────────
|
|
318
511
|
|
|
319
|
-
def generate_project_context(data):
|
|
320
|
-
frameworks = data.get("frameworks", [])
|
|
321
|
-
infrastructure = data.get("infrastructure", [])
|
|
322
|
-
agent_table = resolve_agent_table(data)
|
|
323
|
-
auto_skills = resolve_skills(data)
|
|
512
|
+
def generate_project_context(data):
|
|
513
|
+
frameworks = data.get("frameworks", [])
|
|
514
|
+
infrastructure = data.get("infrastructure", [])
|
|
515
|
+
agent_table = resolve_agent_table(data)
|
|
516
|
+
auto_skills = resolve_skills(data)
|
|
517
|
+
scan_evidence = data.get("scan_evidence", [])
|
|
324
518
|
|
|
325
519
|
agent_rows = "\n".join(
|
|
326
520
|
f"| {task} | `{agent}` | {skills} |"
|
|
327
521
|
for task, agent, skills in agent_table
|
|
328
522
|
)
|
|
329
523
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
524
|
+
scan_evidence_block = ""
|
|
525
|
+
if scan_evidence:
|
|
526
|
+
scan_evidence_block = f"""
|
|
527
|
+
---
|
|
528
|
+
|
|
529
|
+
## Scan Evidence
|
|
530
|
+
|
|
531
|
+
These detections were inferred from the current codebase during `init --scan`:
|
|
532
|
+
|
|
533
|
+
```
|
|
534
|
+
{chr(10).join('- ' + line for line in scan_evidence)}
|
|
535
|
+
```
|
|
536
|
+
"""
|
|
537
|
+
|
|
538
|
+
return f"""# PROJECT_CONTEXT.md
|
|
539
|
+
# Generated by thachvd-kit init wizard on {datetime.now().strftime('%Y-%m-%d')}
|
|
540
|
+
# Edit freely — this is your project's brain.
|
|
333
541
|
|
|
334
542
|
## Project
|
|
335
543
|
|
|
@@ -349,9 +557,11 @@ stack:
|
|
|
349
557
|
infrastructure: [{', '.join(infrastructure)}]
|
|
350
558
|
package_manager: {data.get('package_manager', 'auto-detect')}
|
|
351
559
|
test_framework: {data.get('test_framework', 'auto-detect')}
|
|
352
|
-
```
|
|
353
|
-
|
|
354
|
-
|
|
560
|
+
```
|
|
561
|
+
|
|
562
|
+
{scan_evidence_block}
|
|
563
|
+
|
|
564
|
+
---
|
|
355
565
|
|
|
356
566
|
## Agent Routing
|
|
357
567
|
|
|
@@ -469,6 +679,11 @@ def main():
|
|
|
469
679
|
print(f" {dim('Scan mode: detecting stack from current project files...')}")
|
|
470
680
|
print(f" {bold('Scan result:')} {scanned['primary_language']} | {', '.join(scanned['frameworks']) or 'no framework'}")
|
|
471
681
|
print(f" {bold('Detected infra:')} {', '.join(scanned['infrastructure']) or 'none'}\n")
|
|
682
|
+
if scanned.get("scan_evidence"):
|
|
683
|
+
print(f" {bold('Evidence:')}")
|
|
684
|
+
for line in scanned["scan_evidence"]:
|
|
685
|
+
print(f" · {line}")
|
|
686
|
+
print()
|
|
472
687
|
elif not args.yes:
|
|
473
688
|
print(f" {dim('Manual mode: asking for project context explicitly.')}\n")
|
|
474
689
|
|
|
@@ -493,7 +708,7 @@ def main():
|
|
|
493
708
|
default=(scanned or {}).get("primary_language", "typescript"),
|
|
494
709
|
choices=LANGUAGES_ALL)
|
|
495
710
|
|
|
496
|
-
frameworks = ask_multi("Frameworks (select all that apply)", FRAMEWORKS_ALL, default=
|
|
711
|
+
frameworks = ask_multi("Frameworks (select all that apply)", FRAMEWORKS_ALL, default=(scanned or {}).get("frameworks", None))
|
|
497
712
|
data["frameworks"] = frameworks or []
|
|
498
713
|
|
|
499
714
|
data["database"] = ask("Database / ORM", default=(scanned or {}).get("database", "postgres"), choices=DATABASES_ALL)
|
|
@@ -547,6 +762,8 @@ def main():
|
|
|
547
762
|
data["test_framework"] = data.get("test_framework", (scanned or {}).get("test_framework", "auto-detect"))
|
|
548
763
|
data["max_file_lines"] = data.get("max_file_lines", (scanned or {}).get("max_file_lines", "300"))
|
|
549
764
|
data["code_style"] = data.get("code_style", (scanned or {}).get("code_style", "project-standard"))
|
|
765
|
+
data["framework_details"] = data.get("framework_details", (scanned or {}).get("framework_details", []))
|
|
766
|
+
data["scan_evidence"] = data.get("scan_evidence", (scanned or {}).get("scan_evidence", []))
|
|
550
767
|
data["architecture_notes"] = data.get("architecture_notes", (scanned or {}).get("architecture_notes", "<!-- Add notes -->"))
|
|
551
768
|
data["current_phase"] = data.get("current_phase", (scanned or {}).get("current_phase", "Initial setup"))
|
|
552
769
|
data["next_phase"] = data.get("next_phase", (scanned or {}).get("next_phase", "—"))
|