thachvd-kit 1.0.2 → 1.0.4

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/cli.js CHANGED
@@ -4,24 +4,26 @@ const fs = require('fs');
4
4
  const path = require('path');
5
5
  const prompts = require('prompts');
6
6
  const pc = require('picocolors');
7
+ const packageJson = require('../package.json');
7
8
 
8
9
  const sourceDir = path.resolve(__dirname, '..');
9
10
  const targetDir = process.cwd();
10
11
 
11
12
  // --- Skill Resolver ---
12
13
 
13
- const LANG_SKILLS = {
14
- javascript: ["nodejs-best-practices", "api-design"],
15
- typescript: ["nodejs-best-practices", "api-design"],
16
- python: ["python-patterns"],
17
- go: ["golang-patterns", "golang-testing"],
18
- php: ["laravel-patterns", "laravel-security", "laravel-tdd"],
19
- rust: ["rust-pro"],
20
- java: ["api-design"],
21
- swift: ["mobile-design"],
22
- kotlin: ["mobile-design"],
23
- csharp: ["api-design"],
24
- };
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
+ };
25
27
 
26
28
  const FRAMEWORK_SKILLS = {
27
29
  vue: ["frontend-design", "tailwind-patterns"],
@@ -29,14 +31,25 @@ const FRAMEWORK_SKILLS = {
29
31
  next: ["nextjs-react-expert", "frontend-design"],
30
32
  nuxt: ["frontend-design"],
31
33
  svelte: ["frontend-design"],
32
- laravel: ["laravel-patterns", "laravel-security", "laravel-tdd"],
33
- express: ["nodejs-best-practices", "api-design"],
34
- fastapi: ["python-patterns", "api-design"],
35
- django: ["python-patterns", "api-design"],
36
- gin: ["golang-patterns", "golang-testing"],
37
- fiber: ["golang-patterns", "golang-testing"],
38
- nestjs: ["nodejs-best-practices", "api-design"],
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"],
39
50
  tauri: ["rust-pro", "desktop-design"],
51
+ axum: ["rust-pro", "api-design"],
52
+ actix: ["rust-pro", "api-design"],
40
53
  flutter: ["mobile-design"],
41
54
  "react-native": ["mobile-design"],
42
55
  };
@@ -79,21 +92,27 @@ function resolveSkills(data) {
79
92
 
80
93
  const FRAMEWORK_CHOICES = [
81
94
  'vue', 'react', 'next', 'nuxt', 'svelte', 'angular',
82
- 'laravel', 'express', 'fastapi', 'django', 'gin', 'fiber', 'nestjs',
83
- 'tauri', 'flutter', 'react-native'
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'
84
99
  ];
85
100
 
86
- 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'];
87
102
  const PROJECT_TYPE_CHOICES = ['web-app', 'api', 'mobile', 'desktop', 'cli', 'fullstack'];
88
103
  const DATABASE_CHOICES = ['postgres', 'mysql', 'sqlite', 'mongodb', 'redis', 'dynamodb', 'none'];
89
104
  const PACKAGE_MANAGER_CHOICES = ['npm', 'pnpm', 'yarn', 'bun', 'pip', 'uv', 'go mod', 'cargo', 'composer', 'auto-detect'];
90
105
  const TEST_FRAMEWORK_CHOICES = ['jest', 'vitest', 'pytest', 'go test', 'phpunit', 'pest', 'rspec', 'junit', 'auto-detect'];
91
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'];
92
110
 
93
111
  function printHelp() {
94
112
  console.log(`
95
113
  ${pc.bold('Usage:')}
96
114
  thachvd-kit init [--scan] [--yes]
115
+ thachvd-kit --version
97
116
  thachvd-kit --help
98
117
 
99
118
  ${pc.bold('Commands:')}
@@ -102,10 +121,20 @@ ${pc.bold('Commands:')}
102
121
  ${pc.bold('Options:')}
103
122
  --scan Scan the current project to prefill PROJECT_CONTEXT.md
104
123
  --yes Use detected/default values without prompting
124
+ --version Show the installed CLI version
105
125
  --help Show this help message
106
126
  `);
107
127
  }
108
128
 
129
+ async function askQuestions(questions) {
130
+ return prompts(questions, {
131
+ onCancel: () => {
132
+ console.log(pc.yellow('\nCancelled.'));
133
+ process.exit(130);
134
+ }
135
+ });
136
+ }
137
+
109
138
  function fileExists(...parts) {
110
139
  return fs.existsSync(path.join(targetDir, ...parts));
111
140
  }
@@ -148,74 +177,448 @@ function uniq(values) {
148
177
  return Array.from(new Set(values.filter(Boolean)));
149
178
  }
150
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', 1)
402
+ ])
403
+ },
404
+ {
405
+ id: 'symfony',
406
+ kind: 'backend',
407
+ language: 'php',
408
+ projectType: 'api',
409
+ detect: ctx => buildDetection('symfony', [
410
+ evidenceIf(hasComposerDependency(ctx.composerDepNames, 'symfony/framework-bundle'), 'composer.json -> symfony/framework-bundle', 4),
411
+ evidenceIf(ctx.fileExists('bin', 'console'), 'found bin/console', 2),
412
+ evidenceIf(ctx.fileExists('config', 'bundles.php'), 'found config/bundles.php', 1)
413
+ ])
414
+ },
415
+ {
416
+ id: 'codeigniter',
417
+ kind: 'backend',
418
+ language: 'php',
419
+ projectType: 'api',
420
+ detect: ctx => buildDetection('codeigniter', [
421
+ evidenceIf(hasComposerDependency(ctx.composerDepNames, 'codeigniter4/framework'), 'composer.json -> codeigniter4/framework', 4),
422
+ evidenceIf(ctx.fileExists('spark'), 'found spark', 2),
423
+ evidenceIf(ctx.fileExists('app', 'Config', 'App.php'), 'found app/Config/App.php', 1)
424
+ ])
425
+ },
426
+ {
427
+ id: 'gin',
428
+ kind: 'backend',
429
+ language: 'go',
430
+ projectType: 'api',
431
+ detect: ctx => buildDetection('gin', [
432
+ evidenceIf(textMatches(ctx.goModText, /\bgithub\.com\/gin-gonic\/gin\b/i), 'go.mod -> github.com/gin-gonic/gin', 4)
433
+ ])
434
+ },
435
+ {
436
+ id: 'fiber',
437
+ kind: 'backend',
438
+ language: 'go',
439
+ projectType: 'api',
440
+ detect: ctx => buildDetection('fiber', [
441
+ evidenceIf(textMatches(ctx.goModText, /\bgithub\.com\/gofiber\/fiber\b/i), 'go.mod -> github.com/gofiber/fiber', 4)
442
+ ])
443
+ },
444
+ {
445
+ id: 'echo',
446
+ kind: 'backend',
447
+ language: 'go',
448
+ projectType: 'api',
449
+ detect: ctx => buildDetection('echo', [
450
+ evidenceIf(textMatches(ctx.goModText, /\bgithub\.com\/labstack\/echo\b/i), 'go.mod -> github.com/labstack/echo', 4)
451
+ ])
452
+ },
453
+ {
454
+ id: 'rails',
455
+ kind: 'backend',
456
+ language: 'ruby',
457
+ projectType: 'api',
458
+ detect: ctx => buildDetection('rails', [
459
+ evidenceIf(textMatches(ctx.gemfileText, /^\s*gem\s+["']rails["']/im), 'Gemfile -> rails', 4),
460
+ evidenceIf(ctx.fileExists('bin', 'rails'), 'found bin/rails', 2)
461
+ ])
462
+ },
463
+ {
464
+ id: 'spring',
465
+ kind: 'backend',
466
+ language: 'java',
467
+ projectType: 'api',
468
+ detect: ctx => buildDetection('spring', [
469
+ evidenceIf(textMatches(ctx.pomText, /spring-boot/i) || textMatches(ctx.gradleText, /spring-boot/i), 'Java build -> spring-boot', 4)
470
+ ])
471
+ },
472
+ {
473
+ id: 'aspnet',
474
+ kind: 'backend',
475
+ language: 'csharp',
476
+ projectType: 'api',
477
+ detect: ctx => buildDetection('aspnet', [
478
+ evidenceIf(textMatches(ctx.csprojText, /Microsoft\.NET\.Sdk\.Web/i), '.csproj -> Microsoft.NET.Sdk.Web', 4),
479
+ evidenceIf(textMatches(ctx.programCsText, /WebApplication\.CreateBuilder/i), 'Program.cs -> WebApplication.CreateBuilder', 2)
480
+ ])
481
+ },
482
+ {
483
+ id: 'tauri',
484
+ kind: 'desktop',
485
+ language: 'rust',
486
+ projectType: 'desktop',
487
+ detect: ctx => buildDetection('tauri', [
488
+ evidenceIf(hasDependency(ctx.depNames, '@tauri-apps/api'), 'package.json -> @tauri-apps/api', 4),
489
+ evidenceIf(ctx.fileExists('src-tauri', 'tauri.conf.json') || ctx.fileExists('src-tauri', 'tauri.conf.json5'), 'found src-tauri/tauri.conf.*', 4),
490
+ evidenceIf(ctx.fileExists('src-tauri', 'Cargo.toml'), 'found src-tauri/Cargo.toml', 2)
491
+ ])
492
+ },
493
+ {
494
+ id: 'axum',
495
+ kind: 'backend',
496
+ language: 'rust',
497
+ projectType: 'api',
498
+ detect: ctx => buildDetection('axum', [
499
+ evidenceIf(textMatches(ctx.cargoText, /\baxum\b/i), 'Cargo.toml -> axum', 4)
500
+ ])
501
+ },
502
+ {
503
+ id: 'actix',
504
+ kind: 'backend',
505
+ language: 'rust',
506
+ projectType: 'api',
507
+ detect: ctx => buildDetection('actix', [
508
+ evidenceIf(textMatches(ctx.cargoText, /\bactix-web\b/i), 'Cargo.toml -> actix-web', 4)
509
+ ])
510
+ },
511
+ {
512
+ id: 'flutter',
513
+ kind: 'mobile',
514
+ projectType: 'mobile',
515
+ detect: ctx => buildDetection('flutter', [
516
+ evidenceIf(textMatches(ctx.pubspecText, /^\s*flutter\s*:/im), 'pubspec.yaml -> flutter', 4)
517
+ ])
518
+ },
519
+ {
520
+ id: 'react-native',
521
+ kind: 'mobile',
522
+ projectType: 'mobile',
523
+ detect: ctx => buildDetection('react-native', [
524
+ evidenceIf(hasDependency(ctx.depNames, 'react-native', 'expo'), 'package.json -> react-native/expo', 4)
525
+ ])
526
+ }
527
+ ];
528
+
151
529
  function scanProject() {
152
530
  const pkg = readJsonIfExists('package.json') || {};
531
+ const composer = readJsonIfExists('composer.json') || {};
532
+ const rootEntries = fs.existsSync(targetDir) ? fs.readdirSync(targetDir) : [];
533
+ const rootCsproj = rootEntries.find(name => name.endsWith('.csproj')) || '';
153
534
  const deps = {
154
535
  ...(pkg.dependencies || {}),
155
536
  ...(pkg.devDependencies || {}),
156
537
  ...(pkg.peerDependencies || {})
157
538
  };
158
539
  const depNames = new Set(Object.keys(deps));
159
- const frameworks = [];
540
+ const composerDeps = {
541
+ ...(composer.require || {}),
542
+ ...(composer['require-dev'] || {})
543
+ };
544
+ const composerDepNames = new Set(Object.keys(composerDeps));
545
+ const ctx = {
546
+ pkg,
547
+ composer,
548
+ depNames,
549
+ composerDepNames,
550
+ rootEntries,
551
+ fileExists,
552
+ requirementsText: readTextIfExists('requirements.txt'),
553
+ pyprojectText: readTextIfExists('pyproject.toml'),
554
+ goModText: readTextIfExists('go.mod'),
555
+ pubspecText: readTextIfExists('pubspec.yaml'),
556
+ gemfileText: readTextIfExists('Gemfile'),
557
+ pomText: readTextIfExists('pom.xml'),
558
+ gradleText: `${readTextIfExists('build.gradle')}\n${readTextIfExists('build.gradle.kts')}`,
559
+ cargoText: `${readTextIfExists('Cargo.toml')}\n${readTextIfExists('src-tauri', 'Cargo.toml')}`,
560
+ csprojText: rootCsproj ? readTextIfExists(rootCsproj) : '',
561
+ programCsText: readTextIfExists('Program.cs')
562
+ };
563
+
564
+ const languageDetections = LANGUAGE_DETECTORS
565
+ .map(detector => detector.detect(ctx))
566
+ .filter(result => result.score >= 2)
567
+ .sort((a, b) => b.score - a.score);
568
+
569
+ const frameworkDetails = FRAMEWORK_DETECTORS
570
+ .map(detector => {
571
+ const result = detector.detect(ctx);
572
+ if (result.score < 3) return null;
573
+ return {
574
+ ...result,
575
+ kind: detector.kind,
576
+ language: detector.language,
577
+ projectType: detector.projectType,
578
+ confidence: confidenceLabel(result.score)
579
+ };
580
+ })
581
+ .filter(Boolean)
582
+ .sort((a, b) => b.score - a.score);
583
+
584
+ const frameworkLanguageScores = frameworkDetails.reduce((acc, item) => {
585
+ if (!item.language) return acc;
586
+ acc[item.language] = (acc[item.language] || 0) + item.score;
587
+ return acc;
588
+ }, {});
589
+ const primaryLanguage = Object.keys(frameworkLanguageScores).sort((a, b) => frameworkLanguageScores[b] - frameworkLanguageScores[a])[0]
590
+ || languageDetections[0]?.id
591
+ || 'typescript';
592
+ const frameworks = uniq(frameworkDetails.map(item => item.id)).filter(name => FRAMEWORK_CHOICES.includes(name));
160
593
  const infrastructure = [];
161
- let primaryLanguage = '';
162
594
  let projectType = '';
163
595
  let database = 'none';
164
596
  let testFramework = 'auto-detect';
165
597
  let cloud = 'none';
166
598
  let ciCd = 'none';
167
599
 
168
- const addFramework = (name, condition) => {
169
- if (condition) frameworks.push(name);
170
- };
600
+ if (frameworkDetails.some(item => item.projectType === 'desktop')) projectType = 'desktop';
601
+ else if (frameworkDetails.some(item => item.projectType === 'mobile')) projectType = 'mobile';
602
+ else if (frameworkDetails.some(item => item.kind === 'frontend') && frameworkDetails.some(item => item.kind === 'backend')) projectType = 'fullstack';
603
+ else if (frameworkDetails.some(item => item.kind === 'backend')) projectType = 'api';
604
+ else if (frameworkDetails.some(item => item.kind === 'frontend')) projectType = 'web-app';
605
+ else if (fileExists('package.json') && pkg.bin) projectType = 'cli';
606
+ else if (['php', 'python', 'go', 'java', 'ruby', 'csharp', 'rust'].includes(primaryLanguage)) projectType = 'api';
171
607
 
172
- addFramework('next', depNames.has('next'));
173
- addFramework('react', depNames.has('react'));
174
- addFramework('vue', depNames.has('vue'));
175
- addFramework('nuxt', depNames.has('nuxt'));
176
- addFramework('svelte', depNames.has('svelte') || depNames.has('@sveltejs/kit'));
177
- addFramework('angular', depNames.has('@angular/core'));
178
- addFramework('express', depNames.has('express'));
179
- addFramework('nestjs', depNames.has('@nestjs/core'));
180
- addFramework('fastapi', fileExists('requirements.txt') && /fastapi/i.test(readTextIfExists('requirements.txt')));
181
- addFramework('django', fileExists('requirements.txt') && /django/i.test(readTextIfExists('requirements.txt')));
182
- addFramework('tauri', depNames.has('@tauri-apps/api') || fileExists('src-tauri', 'tauri.conf.json') || fileExists('src-tauri', 'tauri.conf.json5'));
183
- addFramework('flutter', fileExists('pubspec.yaml') && /flutter:/i.test(readTextIfExists('pubspec.yaml')));
184
- addFramework('react-native', depNames.has('react-native') || depNames.has('expo'));
185
- addFramework('laravel', fileExists('artisan') || depNames.has('laravel'));
186
- addFramework('gin', fileExists('go.mod') && /\bgithub\.com\/gin-gonic\/gin\b/i.test(readTextIfExists('go.mod')));
187
- addFramework('fiber', fileExists('go.mod') && /\bgithub\.com\/gofiber\/fiber\b/i.test(readTextIfExists('go.mod')));
188
-
189
- if (fileExists('tsconfig.json') || depNames.has('typescript')) primaryLanguage = 'typescript';
190
- else if (fileExists('Cargo.toml') || fileExists('src-tauri', 'Cargo.toml')) primaryLanguage = 'rust';
191
- else if (fileExists('go.mod')) primaryLanguage = 'go';
192
- else if (fileExists('requirements.txt') || fileExists('pyproject.toml')) primaryLanguage = 'python';
193
- else if (fileExists('composer.json')) primaryLanguage = 'php';
194
- else if (depNames.size > 0 || fileExists('package.json')) primaryLanguage = 'javascript';
195
-
196
- if (frameworks.includes('tauri')) projectType = 'desktop';
197
- else if (frameworks.includes('flutter') || frameworks.includes('react-native')) projectType = 'mobile';
198
- else if (frameworks.some(f => ['express', 'fastapi', 'django', 'laravel', 'gin', 'fiber', 'nestjs'].includes(f))) {
199
- projectType = frameworks.some(f => ['react', 'next', 'vue', 'nuxt', 'svelte', 'angular'].includes(f)) ? 'fullstack' : 'api';
200
- } else if (frameworks.some(f => ['react', 'next', 'vue', 'nuxt', 'svelte', 'angular'].includes(f))) {
201
- projectType = 'web-app';
202
- } else if (fileExists('package.json') && pkg.bin) {
203
- projectType = 'cli';
204
- }
608
+ if (hasDependency(depNames, 'pg', 'postgres', '@prisma/client') || /postgres/i.test(JSON.stringify(pkg))) database = 'postgres';
609
+ else if (hasDependency(depNames, 'mysql', 'mysql2')) database = 'mysql';
610
+ else if (hasDependency(depNames, 'sqlite3', 'better-sqlite3', 'rusqlite')) database = 'sqlite';
611
+ else if (hasDependency(depNames, 'mongodb', 'mongoose')) database = 'mongodb';
612
+ else if (hasDependency(depNames, 'redis', 'ioredis')) database = 'redis';
613
+ else if (hasDependency(depNames, '@aws-sdk/client-dynamodb')) database = 'dynamodb';
205
614
 
206
- if (depNames.has('pg') || depNames.has('postgres') || depNames.has('@prisma/client') || /postgres/i.test(JSON.stringify(pkg))) database = 'postgres';
207
- else if (depNames.has('mysql') || depNames.has('mysql2')) database = 'mysql';
208
- else if (depNames.has('sqlite3') || depNames.has('better-sqlite3') || depNames.has('rusqlite')) database = 'sqlite';
209
- else if (depNames.has('mongodb') || depNames.has('mongoose')) database = 'mongodb';
210
- else if (depNames.has('redis') || depNames.has('ioredis')) database = 'redis';
211
- else if (depNames.has('@aws-sdk/client-dynamodb')) database = 'dynamodb';
212
-
213
- if (depNames.has('vitest')) testFramework = 'vitest';
214
- else if (depNames.has('jest')) testFramework = 'jest';
215
- else if (depNames.has('pytest') || /pytest/i.test(readTextIfExists('requirements.txt'))) testFramework = 'pytest';
216
- else if (depNames.has('phpunit')) testFramework = 'phpunit';
217
- else if (depNames.has('pest')) testFramework = 'pest';
615
+ if (hasDependency(depNames, 'vitest')) testFramework = 'vitest';
616
+ else if (hasDependency(depNames, 'jest')) testFramework = 'jest';
617
+ else if (hasDependency(depNames, 'pytest') || textMatches(ctx.requirementsText, /\bpytest\b/i) || textMatches(ctx.pyprojectText, /\bpytest\b/i)) testFramework = 'pytest';
618
+ else if (hasComposerDependency(composerDepNames, 'phpunit/phpunit')) testFramework = 'phpunit';
619
+ else if (hasComposerDependency(composerDepNames, 'pestphp/pest')) testFramework = 'pest';
218
620
  else if (fileExists('go.mod')) testFramework = 'go test';
621
+ else if (textMatches(ctx.gemfileText, /^\s*gem\s+["']rspec["']/im)) testFramework = 'rspec';
219
622
 
220
623
  if (fileExists('Dockerfile') || fileExists('docker-compose.yml') || fileExists('docker-compose.yaml')) {
221
624
  infrastructure.push('docker');
@@ -237,12 +640,19 @@ function scanProject() {
237
640
  else if (fileExists('.circleci', 'config.yml')) ciCd = 'circleci';
238
641
  else if (fileExists('Jenkinsfile')) ciCd = 'jenkins';
239
642
 
643
+ const scanEvidence = [
644
+ ...(languageDetections[0] ? [`language:${languageDetections[0].id} => ${languageDetections[0].evidence.join('; ')}`] : []),
645
+ ...frameworkDetails.map(item => `${item.id} [${item.confidence}] => ${item.evidence.join('; ')}`)
646
+ ];
647
+
240
648
  return {
241
649
  project_name: path.basename(targetDir),
242
- description: pkg.description || 'A software project',
650
+ description: pkg.description || composer.description || 'A software project',
243
651
  project_type: PROJECT_TYPE_CHOICES.includes(projectType) ? projectType : 'web-app',
244
652
  primary_language: LANGUAGE_CHOICES.includes(primaryLanguage) ? primaryLanguage : 'typescript',
245
- frameworks: uniq(frameworks),
653
+ frameworks,
654
+ framework_details: frameworkDetails,
655
+ scan_evidence: scanEvidence,
246
656
  database: DATABASE_CHOICES.includes(database) ? database : 'none',
247
657
  package_manager: PACKAGE_MANAGER_CHOICES.includes(detectPackageManager()) ? detectPackageManager() : 'auto-detect',
248
658
  uses_docker: infrastructure.includes('docker'),
@@ -265,7 +675,7 @@ function resolveAgentTable(data) {
265
675
  const lang = (data.primary_language || "").toLowerCase();
266
676
 
267
677
  // Frontend
268
- const frontendFws = frameworks.filter(f => ["vue", "react", "next", "nuxt", "svelte", "angular"].includes(f));
678
+ const frontendFws = frameworks.filter(f => FRONTEND_FRAMEWORKS.includes(f));
269
679
  if (frontendFws.length > 0) {
270
680
  let feSkills = [];
271
681
  frontendFws.forEach(fw => {
@@ -276,14 +686,14 @@ function resolveAgentTable(data) {
276
686
 
277
687
  // Backend
278
688
  let beSkills = [...(LANG_SKILLS[lang] || ["api-design"])];
279
- const backendFws = frameworks.filter(f => ["laravel", "express", "fastapi", "django", "gin", "fiber", "nestjs"].includes(f));
689
+ const backendFws = frameworks.filter(f => BACKEND_FRAMEWORKS.includes(f));
280
690
  backendFws.forEach(fw => {
281
691
  beSkills.push(...(FRAMEWORK_SKILLS[fw] || []));
282
692
  });
283
693
  rows.push(["Backend API", "backend-specialist", Array.from(new Set(beSkills)).slice(0, 3).join(", ")]);
284
694
 
285
695
  // Mobile/Desktop
286
- const mobileFws = frameworks.filter(f => ["tauri", "flutter", "react-native"].includes(f));
696
+ const mobileFws = frameworks.filter(f => MOBILE_DESKTOP_FRAMEWORKS.includes(f));
287
697
  if (mobileFws.length > 0) {
288
698
  let mobileSkills = [];
289
699
  mobileFws.forEach(fw => {
@@ -306,12 +716,25 @@ function resolveAgentTable(data) {
306
716
 
307
717
  // --- File Generators ---
308
718
 
309
- function generateProjectContext(data) {
310
- const agentTable = resolveAgentTable(data);
311
- const autoSkills = resolveSkills(data);
312
-
313
- const agentRows = agentTable.map(([task, agent, skills]) => `| ${task} | \`${agent}\` | ${skills} |`).join('\n');
314
- const autoSkillsList = autoSkills.map(s => `- ${s}`).join('\n');
719
+ function generateProjectContext(data) {
720
+ const agentTable = resolveAgentTable(data);
721
+ const autoSkills = resolveSkills(data);
722
+ const scanEvidenceBlock = (data.scan_evidence || []).length > 0
723
+ ? `
724
+ ---
725
+
726
+ ## Scan Evidence
727
+
728
+ These detections were inferred from the current codebase during \`init --scan\`:
729
+
730
+ \`\`\`text
731
+ ${(data.scan_evidence || []).map(line => '- ' + line).join('\n')}
732
+ \`\`\`
733
+ `
734
+ : '';
735
+
736
+ const agentRows = agentTable.map(([task, agent, skills]) => `| ${task} | \`${agent}\` | ${skills} |`).join('\n');
737
+ const autoSkillsList = autoSkills.map(s => `- ${s}`).join('\n');
315
738
 
316
739
  return `# PROJECT_CONTEXT.md
317
740
  # Generated by thachvd-kit init wizard on ${new Date().toISOString().split('T')[0]}
@@ -335,11 +758,13 @@ stack:
335
758
  infrastructure: [${(data.infrastructure || []).join(', ')}]
336
759
  package_manager: ${data.package_manager || 'auto-detect'}
337
760
  test_framework: ${data.test_framework || 'auto-detect'}
338
- \`\`\`
339
-
340
- ---
341
-
342
- ## Agent Routing
761
+ \`\`\`
762
+
763
+ ${scanEvidenceBlock}
764
+
765
+ ---
766
+
767
+ ## Agent Routing
343
768
 
344
769
  | Task Type | Agent | Primary Skills |
345
770
  |-----------|-------|----------------|
@@ -423,6 +848,11 @@ async function main() {
423
848
  const command = rawArgs[0] && !rawArgs[0].startsWith('-') ? rawArgs[0] : 'init';
424
849
  const args = command === 'init' ? rawArgs.slice(rawArgs[0] === 'init' ? 1 : 0) : rawArgs.slice(1);
425
850
 
851
+ if (rawArgs.includes('--version') || rawArgs.includes('-v')) {
852
+ console.log(packageJson.version);
853
+ return;
854
+ }
855
+
426
856
  if (rawArgs.includes('--help') || rawArgs.includes('-h')) {
427
857
  printHelp();
428
858
  return;
@@ -444,7 +874,7 @@ async function main() {
444
874
 
445
875
  let projectSetup = 'new';
446
876
  if (!useScan && !assumeYes) {
447
- const modeAnswer = await prompts([
877
+ const modeAnswer = await askQuestions([
448
878
  {
449
879
  type: 'select',
450
880
  name: 'project_setup',
@@ -476,10 +906,10 @@ async function main() {
476
906
  }
477
907
 
478
908
  if (fs.existsSync(path.join(targetDir, 'PROJECT_CONTEXT.md')) && !assumeYes) {
479
- const res = await prompts({
909
+ const res = await askQuestions({
480
910
  type: 'confirm',
481
911
  name: 'overwrite',
482
- message: 'PROJECT_CONTEXT.md already exists in this folder. Overwrite?',
912
+ message: 'PROJECT_CONTEXT.md already exists in this folder. Overwrite?',
483
913
  initial: false
484
914
  });
485
915
  if (!res.overwrite) {
@@ -493,9 +923,14 @@ async function main() {
493
923
  if (useScan) {
494
924
  console.log(`${pc.bold(' Scan result:')} ${scanned.primary_language} | ${scanned.frameworks.join(', ') || 'no framework'}`);
495
925
  console.log(` ${pc.bold('Detected infra:')} ${scanned.infrastructure.join(', ') || 'none'}\n`);
926
+ if ((scanned.scan_evidence || []).length > 0) {
927
+ console.log(` ${pc.bold('Evidence:')}`);
928
+ scanned.scan_evidence.forEach(line => console.log(` · ${line}`));
929
+ console.log('');
930
+ }
496
931
  }
497
932
 
498
- const { project_name, description, project_type } = await prompts([
933
+ const { project_name, description, project_type } = await askQuestions([
499
934
  { type: assumeYes ? null : 'text', name: 'project_name', message: 'Project name:', initial: scanned?.project_name || path.basename(targetDir) },
500
935
  { type: assumeYes ? null : 'text', name: 'description', message: 'Short description:', initial: scanned?.description || 'A software project' },
501
936
  {
@@ -505,7 +940,7 @@ async function main() {
505
940
  }
506
941
  ]);
507
942
 
508
- const { primary_language } = await prompts({
943
+ const { primary_language } = await askQuestions({
509
944
  type: assumeYes ? null : 'select',
510
945
  name: 'primary_language',
511
946
  message: 'Primary language:',
@@ -513,7 +948,7 @@ async function main() {
513
948
  initial: Math.max(LANGUAGE_CHOICES.indexOf(scanned?.primary_language || 'typescript'), 0)
514
949
  });
515
950
 
516
- const { frameworks } = await prompts({
951
+ const { frameworks } = await askQuestions({
517
952
  type: assumeYes ? null : 'multiselect',
518
953
  name: 'frameworks',
519
954
  message: 'Frameworks (Space to select, Enter to confirm):',
@@ -521,7 +956,7 @@ async function main() {
521
956
  min: 0
522
957
  });
523
958
 
524
- const dbAndInfra = await prompts([
959
+ const dbAndInfra = await askQuestions([
525
960
  {
526
961
  type: assumeYes ? null : 'select', name: 'database', message: 'Database / ORM:',
527
962
  choices: DATABASE_CHOICES.map(d => ({ title: d, value: d })),
@@ -535,7 +970,7 @@ async function main() {
535
970
  }
536
971
  ]);
537
972
 
538
- const { package_manager, test_framework, max_file_lines } = await prompts([
973
+ const { package_manager, test_framework, max_file_lines } = await askQuestions([
539
974
  {
540
975
  type: assumeYes ? null : 'select', name: 'package_manager', message: 'Package Manager:',
541
976
  choices: PACKAGE_MANAGER_CHOICES.map(p => ({ title: p, value: p })),
@@ -561,6 +996,8 @@ async function main() {
561
996
  data.package_manager = data.package_manager || scanned?.package_manager || 'auto-detect';
562
997
  data.test_framework = data.test_framework || scanned?.test_framework || 'auto-detect';
563
998
  data.max_file_lines = data.max_file_lines || scanned?.max_file_lines || '300';
999
+ data.framework_details = scanned?.framework_details || [];
1000
+ data.scan_evidence = scanned?.scan_evidence || [];
564
1001
  data.infrastructure = [];
565
1002
  if (data.uses_docker) data.infrastructure.push('docker');
566
1003
  if (data.cloud !== 'none') data.infrastructure.push(data.cloud);