thachvd-kit 1.0.15 → 1.0.16

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
@@ -1,60 +1,60 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- const fs = require('fs');
4
- const path = require('path');
5
- const prompts = require('prompts');
6
- const pc = require('picocolors');
7
- const packageJson = require('../package.json');
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const prompts = require('prompts');
6
+ const pc = require('picocolors');
7
+ const packageJson = require('../package.json');
8
8
 
9
9
  const sourceDir = path.resolve(__dirname, '..');
10
10
  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: ["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
- };
27
-
28
- const FRAMEWORK_SKILLS = {
29
- vue: ["frontend-design", "tailwind-patterns"],
30
- react: ["react-frontend", "frontend-design"],
31
- next: ["nextjs-react-expert", "frontend-design"],
32
- nuxt: ["frontend-design"],
33
- svelte: ["frontend-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"],
50
- tauri: ["rust-pro", "desktop-design"],
51
- axum: ["rust-pro", "api-design"],
52
- actix: ["rust-pro", "api-design"],
53
- flutter: ["mobile-design"],
54
- "react-native": ["mobile-design"],
55
- };
56
-
57
- const INFRA_SKILLS = {
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
+ };
27
+
28
+ const FRAMEWORK_SKILLS = {
29
+ vue: ["frontend-design", "tailwind-patterns"],
30
+ react: ["react-frontend", "frontend-design"],
31
+ next: ["nextjs-react-expert", "frontend-design"],
32
+ nuxt: ["frontend-design"],
33
+ svelte: ["frontend-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"],
50
+ tauri: ["rust-pro", "desktop-design"],
51
+ axum: ["rust-pro", "api-design"],
52
+ actix: ["rust-pro", "api-design"],
53
+ flutter: ["mobile-design"],
54
+ "react-native": ["mobile-design"],
55
+ };
56
+
57
+ const INFRA_SKILLS = {
58
58
  docker: ["docker-patterns"],
59
59
  aws: ["deployment-procedures", "docker-patterns"],
60
60
  gcp: ["deployment-procedures", "docker-patterns"],
@@ -87,720 +87,723 @@ function resolveSkills(data) {
87
87
  skills.add("clean-code");
88
88
  skills.add("systematic-debugging");
89
89
 
90
- return Array.from(skills).sort();
91
- }
92
-
93
- const FRAMEWORK_CHOICES = [
94
- 'vue', 'react', 'next', 'nuxt', 'svelte', 'angular',
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'
99
- ];
100
-
101
- const LANGUAGE_CHOICES = ['typescript', 'javascript', 'python', 'go', 'php', 'rust', 'java', 'ruby', 'swift', 'kotlin', 'csharp', 'cpp'];
102
- const PROJECT_TYPE_CHOICES = ['web-app', 'api', 'mobile', 'desktop', 'cli', 'fullstack'];
103
- const DATABASE_CHOICES = ['postgres', 'mysql', 'sqlite', 'mongodb', 'redis', 'dynamodb', 'none'];
104
- const PACKAGE_MANAGER_CHOICES = ['npm', 'pnpm', 'yarn', 'bun', 'pip', 'uv', 'go mod', 'cargo', 'composer', 'auto-detect'];
105
- const TEST_FRAMEWORK_CHOICES = ['jest', 'vitest', 'pytest', 'go test', 'phpunit', 'pest', 'rspec', 'junit', 'auto-detect'];
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'];
110
- const IGNORED_SCAN_DIRS = new Set(['.git', 'node_modules', 'vendor', 'dist', 'build', 'coverage', '.next', '.nuxt', '.turbo', '.cache', 'storage', 'tmp']);
111
-
112
- function printHelp() {
113
- console.log(`
114
- ${pc.bold('Usage:')}
115
- thachvd-kit init [--scan] [--yes]
116
- thachvd-kit --version
117
- thachvd-kit --help
118
-
119
- ${pc.bold('Commands:')}
120
- init Run the project init wizard
121
-
122
- ${pc.bold('Options:')}
123
- --scan Scan the current project to auto-detect stack
124
- --yes Use detected/default values without prompting
125
- --version Show the installed CLI version
126
- --help Show this help message
127
-
128
- ${pc.bold('What gets generated:')}
129
- AGENTS.md Project context for AI agents (Codex / Claude Code / Cursor)
130
- .agent/ Skills, workflows, agents, and rules for your AI tools
131
- `);
132
- }
133
-
134
- function formatScanPreviewLines(scanned) {
135
- return [
136
- ['App root', scanned.app_root || '.'],
137
- ['Project type', scanned.project_type || 'unknown'],
138
- ['Primary language', scanned.primary_language || 'unknown'],
139
- ['Frameworks', (scanned.frameworks || []).join(', ') || 'no framework'],
140
- ['Database', scanned.database || 'none'],
141
- ['Package manager', scanned.package_manager || 'auto-detect'],
142
- ['Test framework', scanned.test_framework || 'auto-detect'],
143
- ['Cloud', scanned.cloud || 'none'],
144
- ['Infrastructure', (scanned.infrastructure || []).join(', ') || 'none'],
145
- ['CI/CD', scanned.ci_cd || 'none']
146
- ];
147
- }
148
-
149
- async function askQuestions(questions) {
150
- return prompts(questions, {
151
- onCancel: () => {
152
- console.log(pc.yellow('\nCancelled.'));
153
- process.exit(130);
154
- }
155
- });
156
- }
157
-
158
- function fileExists(...parts) {
159
- return fs.existsSync(path.join(targetDir, ...parts));
160
- }
161
-
162
- function fileExistsAt(baseDir, ...parts) {
163
- return fs.existsSync(path.join(baseDir, ...parts));
164
- }
165
-
166
- function readJsonIfExists(...parts) {
167
- const fullPath = path.join(targetDir, ...parts);
168
- if (!fs.existsSync(fullPath)) return null;
169
- try {
170
- return JSON.parse(fs.readFileSync(fullPath, 'utf8'));
171
- } catch {
172
- return null;
173
- }
174
- }
175
-
176
- function readJsonIfExistsAt(baseDir, ...parts) {
177
- const fullPath = path.join(baseDir, ...parts);
178
- if (!fs.existsSync(fullPath)) return null;
179
- try {
180
- return JSON.parse(fs.readFileSync(fullPath, 'utf8'));
181
- } catch {
182
- return null;
183
- }
184
- }
185
-
186
- function readTextIfExists(...parts) {
187
- const fullPath = path.join(targetDir, ...parts);
188
- if (!fs.existsSync(fullPath)) return '';
189
- try {
190
- return fs.readFileSync(fullPath, 'utf8');
191
- } catch {
192
- return '';
193
- }
194
- }
195
-
196
- function readTextIfExistsAt(baseDir, ...parts) {
197
- const fullPath = path.join(baseDir, ...parts);
198
- if (!fs.existsSync(fullPath)) return '';
199
- try {
200
- return fs.readFileSync(fullPath, 'utf8');
201
- } catch {
202
- return '';
203
- }
204
- }
205
-
206
- function detectPackageManager(baseDir = targetDir) {
207
- if (fileExistsAt(baseDir, 'pnpm-lock.yaml')) return 'pnpm';
208
- if (fileExistsAt(baseDir, 'yarn.lock')) return 'yarn';
209
- if (fileExistsAt(baseDir, 'bun.lockb') || fileExistsAt(baseDir, 'bun.lock')) return 'bun';
210
- if (fileExistsAt(baseDir, 'package-lock.json')) return 'npm';
211
- if (fileExistsAt(baseDir, 'uv.lock')) return 'uv';
212
- if (fileExistsAt(baseDir, 'poetry.lock')) return 'pip';
213
- if (fileExistsAt(baseDir, 'requirements.txt') || fileExistsAt(baseDir, 'pyproject.toml')) return 'pip';
214
- if (fileExistsAt(baseDir, 'go.mod')) return 'go mod';
215
- if (fileExistsAt(baseDir, 'Cargo.toml')) return 'cargo';
216
- if (fileExistsAt(baseDir, 'composer.json')) return 'composer';
217
- return 'auto-detect';
218
- }
219
-
220
- function uniq(values) {
221
- return Array.from(new Set(values.filter(Boolean)));
222
- }
223
-
224
- function detectAppRoot(baseDir) {
225
- const candidates = [baseDir];
226
- const queue = [{ dir: baseDir, depth: 0 }];
227
-
228
- while (queue.length > 0) {
229
- const { dir, depth } = queue.shift();
230
- if (depth >= 2) continue;
231
- let entries = [];
232
- try {
233
- entries = fs.readdirSync(dir, { withFileTypes: true });
234
- } catch {
235
- continue;
236
- }
237
- for (const entry of entries) {
238
- if (!entry.isDirectory()) continue;
239
- if (IGNORED_SCAN_DIRS.has(entry.name)) continue;
240
- const nextDir = path.join(dir, entry.name);
241
- candidates.push(nextDir);
242
- queue.push({ dir: nextDir, depth: depth + 1 });
243
- }
244
- }
245
-
246
- const scoreCandidate = dir => {
247
- let score = 0;
248
- if (fileExistsAt(dir, 'composer.json')) score += 5;
249
- if (fileExistsAt(dir, 'package.json')) score += 4;
250
- if (fileExistsAt(dir, 'Cargo.toml')) score += 4;
251
- if (fileExistsAt(dir, 'go.mod')) score += 4;
252
- if (fileExistsAt(dir, 'pyproject.toml') || fileExistsAt(dir, 'requirements.txt')) score += 4;
253
- if (fileExistsAt(dir, 'artisan')) score += 5;
254
- if (fileExistsAt(dir, 'bin', 'cake') || fileExistsAt(dir, 'app', 'Config', 'core.php')) score += 5;
255
- if (fileExistsAt(dir, 'src-tauri', 'tauri.conf.json') || fileExistsAt(dir, 'src-tauri', 'tauri.conf.json5')) score += 5;
256
- if (fileExistsAt(dir, 'manage.py')) score += 4;
257
- if (fileExistsAt(dir, 'Gemfile')) score += 4;
258
- if (fileExistsAt(dir, 'pom.xml') || fileExistsAt(dir, 'build.gradle') || fileExistsAt(dir, 'build.gradle.kts')) score += 4;
259
- if (fileExistsAt(dir, 'app', 'View') || fileExistsAt(dir, 'resources', 'views') || fileExistsAt(dir, 'templates') || fileExistsAt(dir, 'app', 'views')) score += 2;
260
- return score;
261
- };
262
-
263
- const ranked = candidates
264
- .map(dir => ({ dir, score: scoreCandidate(dir) }))
265
- .filter(item => item.score > 0)
266
- .sort((a, b) => (b.score - a.score) || (a.dir.length - b.dir.length));
267
-
268
- return ranked[0]?.dir || baseDir;
269
- }
270
-
271
- function repoHasAny(baseDir, relativePaths, maxDepth = 3) {
272
- const queue = [{ dir: baseDir, depth: 0 }];
273
- while (queue.length > 0) {
274
- const { dir, depth } = queue.shift();
275
- for (const relativePath of relativePaths) {
276
- if (fileExistsAt(dir, ...relativePath)) return true;
277
- }
278
- if (depth >= maxDepth) continue;
279
- let entries = [];
280
- try {
281
- entries = fs.readdirSync(dir, { withFileTypes: true });
282
- } catch {
283
- continue;
284
- }
285
- for (const entry of entries) {
286
- if (!entry.isDirectory()) continue;
287
- if (IGNORED_SCAN_DIRS.has(entry.name)) continue;
288
- queue.push({ dir: path.join(dir, entry.name), depth: depth + 1 });
289
- }
290
- }
291
- return false;
292
- }
293
-
294
- function evidenceIf(condition, label, weight = 1) {
295
- return condition ? { label, weight } : null;
296
- }
297
-
298
- function buildDetection(id, evidence, meta = {}) {
299
- const hits = evidence.filter(Boolean);
300
- const score = hits.reduce((sum, hit) => sum + hit.weight, 0);
301
- return {
302
- id,
303
- score,
304
- evidence: hits.map(hit => hit.label),
305
- ...meta
306
- };
307
- }
308
-
309
- function confidenceLabel(score) {
310
- if (score >= 6) return 'high';
311
- if (score >= 3) return 'medium';
312
- return 'low';
313
- }
314
-
315
- function hasDependency(depNames, ...names) {
316
- return names.some(name => depNames.has(name));
317
- }
318
-
319
- function hasComposerDependency(composerDepNames, ...names) {
320
- return names.some(name => composerDepNames.has(name));
321
- }
322
-
323
- function hasRootFile(rootEntries, ...filenames) {
324
- return filenames.some(name => rootEntries.has(name));
325
- }
326
-
327
- function textMatches(text, pattern) {
328
- return Boolean(text && pattern.test(text));
329
- }
330
-
331
- const LANGUAGE_DETECTORS = [
332
- {
333
- id: 'typescript',
334
- detect: ctx => buildDetection('typescript', [
335
- evidenceIf(ctx.fileExists('tsconfig.json'), 'found tsconfig.json', 4),
336
- evidenceIf(hasDependency(ctx.depNames, 'typescript', 'ts-node', 'tsx'), 'package.json -> typescript toolchain', 2)
337
- ])
338
- },
339
- {
340
- id: 'javascript',
341
- detect: ctx => buildDetection('javascript', [
342
- evidenceIf(ctx.fileExists('package.json'), 'found package.json', 2),
343
- evidenceIf(ctx.depNames.size > 0, 'package.json -> JavaScript dependency graph', 1)
344
- ])
345
- },
346
- {
347
- id: 'python',
348
- detect: ctx => buildDetection('python', [
349
- evidenceIf(ctx.fileExists('requirements.txt'), 'found requirements.txt', 3),
350
- evidenceIf(ctx.fileExists('pyproject.toml'), 'found pyproject.toml', 2),
351
- evidenceIf(textMatches(ctx.pyprojectText, /\[tool\.(poetry|uv|hatch|pdm)\]/i), 'pyproject.toml -> Python tool section', 2)
352
- ])
353
- },
354
- {
355
- id: 'go',
356
- detect: ctx => buildDetection('go', [
357
- evidenceIf(ctx.fileExists('go.mod'), 'found go.mod', 4)
358
- ])
359
- },
360
- {
361
- id: 'php',
362
- detect: ctx => buildDetection('php', [
363
- evidenceIf(ctx.fileExists('composer.json'), 'found composer.json', 4),
364
- evidenceIf(ctx.composerDepNames.size > 0, 'composer.json -> PHP dependency graph', 1)
365
- ])
366
- },
367
- {
368
- id: 'rust',
369
- detect: ctx => buildDetection('rust', [
370
- evidenceIf(ctx.fileExists('Cargo.toml'), 'found Cargo.toml', 4),
371
- evidenceIf(ctx.fileExists('src-tauri', 'Cargo.toml'), 'found src-tauri/Cargo.toml', 5)
372
- ])
373
- },
374
- {
375
- id: 'java',
376
- detect: ctx => buildDetection('java', [
377
- evidenceIf(ctx.fileExists('pom.xml'), 'found pom.xml', 4),
378
- evidenceIf(ctx.fileExists('build.gradle') || ctx.fileExists('build.gradle.kts'), 'found Gradle build file', 3)
379
- ])
380
- },
381
- {
382
- id: 'ruby',
383
- detect: ctx => buildDetection('ruby', [
384
- evidenceIf(ctx.fileExists('Gemfile'), 'found Gemfile', 4),
385
- evidenceIf(ctx.fileExists('config.ru'), 'found config.ru', 1)
386
- ])
387
- },
388
- {
389
- id: 'csharp',
390
- detect: ctx => buildDetection('csharp', [
391
- evidenceIf(ctx.rootEntries.some(name => name.endsWith('.sln') || name.endsWith('.csproj')), 'found .sln/.csproj', 4),
392
- evidenceIf(ctx.fileExists('Program.cs'), 'found Program.cs', 1)
393
- ])
394
- }
395
- ];
396
-
397
- const FRAMEWORK_DETECTORS = [
398
- {
399
- id: 'next',
400
- kind: 'frontend',
401
- projectType: 'web-app',
402
- detect: ctx => buildDetection('next', [
403
- evidenceIf(hasDependency(ctx.depNames, 'next'), 'package.json -> next', 4),
404
- evidenceIf(ctx.fileExists('next.config.js') || ctx.fileExists('next.config.mjs') || ctx.fileExists('next.config.ts'), 'found next.config.*', 3),
405
- evidenceIf(ctx.fileExists('app') || ctx.fileExists('pages'), 'found app/ or pages/', 1)
406
- ])
407
- },
408
- {
409
- id: 'react',
410
- kind: 'frontend',
411
- projectType: 'web-app',
412
- detect: ctx => buildDetection('react', [
413
- evidenceIf(hasDependency(ctx.depNames, 'react'), 'package.json -> react', 4),
414
- evidenceIf(hasDependency(ctx.depNames, 'react-dom'), 'package.json -> react-dom', 2)
415
- ])
416
- },
417
- {
418
- id: 'vue',
419
- kind: 'frontend',
420
- projectType: 'web-app',
421
- detect: ctx => buildDetection('vue', [
422
- evidenceIf(hasDependency(ctx.depNames, 'vue'), 'package.json -> vue', 4)
423
- ])
424
- },
425
- {
426
- id: 'nuxt',
427
- kind: 'frontend',
428
- projectType: 'web-app',
429
- detect: ctx => buildDetection('nuxt', [
430
- evidenceIf(hasDependency(ctx.depNames, 'nuxt'), 'package.json -> nuxt', 4),
431
- evidenceIf(ctx.fileExists('nuxt.config.ts') || ctx.fileExists('nuxt.config.js'), 'found nuxt.config.*', 3)
432
- ])
433
- },
434
- {
435
- id: 'svelte',
436
- kind: 'frontend',
437
- projectType: 'web-app',
438
- detect: ctx => buildDetection('svelte', [
439
- evidenceIf(hasDependency(ctx.depNames, 'svelte', '@sveltejs/kit'), 'package.json -> svelte', 4)
440
- ])
441
- },
442
- {
443
- id: 'angular',
444
- kind: 'frontend',
445
- projectType: 'web-app',
446
- detect: ctx => buildDetection('angular', [
447
- evidenceIf(hasDependency(ctx.depNames, '@angular/core'), 'package.json -> @angular/core', 4),
448
- evidenceIf(ctx.fileExists('angular.json'), 'found angular.json', 3)
449
- ])
450
- },
451
- {
452
- id: 'express',
453
- kind: 'backend',
454
- language: 'javascript',
455
- projectType: 'api',
456
- detect: ctx => buildDetection('express', [
457
- evidenceIf(hasDependency(ctx.depNames, 'express'), 'package.json -> express', 4)
458
- ])
459
- },
460
- {
461
- id: 'nestjs',
462
- kind: 'backend',
463
- language: 'javascript',
464
- projectType: 'api',
465
- detect: ctx => buildDetection('nestjs', [
466
- evidenceIf(hasDependency(ctx.depNames, '@nestjs/core'), 'package.json -> @nestjs/core', 4)
467
- ])
468
- },
469
- {
470
- id: 'fastapi',
471
- kind: 'backend',
472
- language: 'python',
473
- projectType: 'api',
474
- detect: ctx => buildDetection('fastapi', [
475
- evidenceIf(textMatches(ctx.requirementsText, /\bfastapi\b/i) || textMatches(ctx.pyprojectText, /\bfastapi\b/i), 'Python deps -> fastapi', 4)
476
- ])
477
- },
478
- {
479
- id: 'django',
480
- kind: 'backend',
481
- language: 'python',
482
- projectType: 'web-app',
483
- detect: ctx => buildDetection('django', [
484
- evidenceIf(textMatches(ctx.requirementsText, /\bdjango\b/i) || textMatches(ctx.pyprojectText, /\bdjango\b/i), 'Python deps -> django', 4),
485
- evidenceIf(ctx.fileExists('manage.py'), 'found manage.py', 2),
486
- evidenceIf(ctx.fileExists('templates'), 'found templates/', 1)
487
- ])
488
- },
489
- {
490
- id: 'flask',
491
- kind: 'backend',
492
- language: 'python',
493
- projectType: 'api',
494
- detect: ctx => buildDetection('flask', [
495
- evidenceIf(textMatches(ctx.requirementsText, /\bflask\b/i) || textMatches(ctx.pyprojectText, /\bflask\b/i), 'Python deps -> flask', 4)
496
- ])
497
- },
498
- {
499
- id: 'laravel',
500
- kind: 'backend',
501
- language: 'php',
502
- projectType: 'web-app',
503
- detect: ctx => buildDetection('laravel', [
504
- evidenceIf(hasComposerDependency(ctx.composerDepNames, 'laravel/framework'), 'composer.json -> laravel/framework', 4),
505
- evidenceIf(ctx.fileExists('artisan'), 'found artisan', 3),
506
- evidenceIf(ctx.fileExists('resources', 'views'), 'found resources/views', 1)
507
- ])
508
- },
509
- {
510
- id: 'cakephp',
511
- kind: 'backend',
512
- language: 'php',
513
- projectType: 'web-app',
514
- detect: ctx => buildDetection('cakephp', [
515
- evidenceIf(hasComposerDependency(ctx.composerDepNames, 'cakephp/cakephp'), 'composer.json -> cakephp/cakephp', 4),
516
- evidenceIf(ctx.fileExists('bin', 'cake'), 'found bin/cake', 3),
517
- evidenceIf(ctx.fileExists('config', 'app.php'), 'found config/app.php', 2),
518
- evidenceIf(ctx.fileExists('app', 'Config', 'core.php'), 'found app/Config/core.php (CakePHP legacy)', 3),
519
- evidenceIf(ctx.fileExists('app', 'Controller', 'AppController.php'), 'found app/Controller/AppController.php', 2),
520
- evidenceIf(ctx.fileExists('app', 'Model', 'AppModel.php'), 'found app/Model/AppModel.php', 2),
521
- evidenceIf(ctx.fileExists('lib', 'Cake'), 'found lib/Cake', 3),
522
- evidenceIf(ctx.fileExists('cake', 'console', 'cake'), 'found cake/console/cake', 3),
523
- evidenceIf(ctx.fileExists('app', 'View'), 'found app/View', 2),
524
- evidenceIf(textMatches(ctx.viewFilesText, /\.ctp$/im), 'found .ctp view templates', 2)
525
- ])
526
- },
527
- {
528
- id: 'symfony',
529
- kind: 'backend',
530
- language: 'php',
531
- projectType: 'web-app',
532
- detect: ctx => buildDetection('symfony', [
533
- evidenceIf(hasComposerDependency(ctx.composerDepNames, 'symfony/framework-bundle'), 'composer.json -> symfony/framework-bundle', 4),
534
- evidenceIf(ctx.fileExists('bin', 'console'), 'found bin/console', 2),
535
- evidenceIf(ctx.fileExists('config', 'bundles.php'), 'found config/bundles.php', 1),
536
- evidenceIf(ctx.fileExists('templates'), 'found templates/', 1)
537
- ])
538
- },
539
- {
540
- id: 'codeigniter',
541
- kind: 'backend',
542
- language: 'php',
543
- projectType: 'api',
544
- detect: ctx => buildDetection('codeigniter', [
545
- evidenceIf(hasComposerDependency(ctx.composerDepNames, 'codeigniter4/framework'), 'composer.json -> codeigniter4/framework', 4),
546
- evidenceIf(ctx.fileExists('spark'), 'found spark', 2),
547
- evidenceIf(ctx.fileExists('app', 'Config', 'App.php'), 'found app/Config/App.php', 1)
548
- ])
549
- },
550
- {
551
- id: 'gin',
552
- kind: 'backend',
553
- language: 'go',
554
- projectType: 'api',
555
- detect: ctx => buildDetection('gin', [
556
- evidenceIf(textMatches(ctx.goModText, /\bgithub\.com\/gin-gonic\/gin\b/i), 'go.mod -> github.com/gin-gonic/gin', 4)
557
- ])
558
- },
559
- {
560
- id: 'fiber',
561
- kind: 'backend',
562
- language: 'go',
563
- projectType: 'api',
564
- detect: ctx => buildDetection('fiber', [
565
- evidenceIf(textMatches(ctx.goModText, /\bgithub\.com\/gofiber\/fiber\b/i), 'go.mod -> github.com/gofiber/fiber', 4)
566
- ])
567
- },
568
- {
569
- id: 'echo',
570
- kind: 'backend',
571
- language: 'go',
572
- projectType: 'api',
573
- detect: ctx => buildDetection('echo', [
574
- evidenceIf(textMatches(ctx.goModText, /\bgithub\.com\/labstack\/echo\b/i), 'go.mod -> github.com/labstack/echo', 4)
575
- ])
576
- },
577
- {
578
- id: 'rails',
579
- kind: 'backend',
580
- language: 'ruby',
581
- projectType: 'web-app',
582
- detect: ctx => buildDetection('rails', [
583
- evidenceIf(textMatches(ctx.gemfileText, /^\s*gem\s+["']rails["']/im), 'Gemfile -> rails', 4),
584
- evidenceIf(ctx.fileExists('bin', 'rails'), 'found bin/rails', 2),
585
- evidenceIf(ctx.fileExists('app', 'views'), 'found app/views', 1)
586
- ])
587
- },
588
- {
589
- id: 'spring',
590
- kind: 'backend',
591
- language: 'java',
592
- projectType: 'api',
593
- detect: ctx => buildDetection('spring', [
594
- evidenceIf(textMatches(ctx.pomText, /spring-boot/i) || textMatches(ctx.gradleText, /spring-boot/i), 'Java build -> spring-boot', 4)
595
- ])
596
- },
597
- {
598
- id: 'aspnet',
599
- kind: 'backend',
600
- language: 'csharp',
601
- projectType: 'api',
602
- detect: ctx => buildDetection('aspnet', [
603
- evidenceIf(textMatches(ctx.csprojText, /Microsoft\.NET\.Sdk\.Web/i), '.csproj -> Microsoft.NET.Sdk.Web', 4),
604
- evidenceIf(textMatches(ctx.programCsText, /WebApplication\.CreateBuilder/i), 'Program.cs -> WebApplication.CreateBuilder', 2)
605
- ])
606
- },
607
- {
608
- id: 'tauri',
609
- kind: 'desktop',
610
- language: 'rust',
611
- projectType: 'desktop',
612
- detect: ctx => buildDetection('tauri', [
613
- evidenceIf(hasDependency(ctx.depNames, '@tauri-apps/api'), 'package.json -> @tauri-apps/api', 4),
614
- evidenceIf(ctx.fileExists('src-tauri', 'tauri.conf.json') || ctx.fileExists('src-tauri', 'tauri.conf.json5'), 'found src-tauri/tauri.conf.*', 4),
615
- evidenceIf(ctx.fileExists('src-tauri', 'Cargo.toml'), 'found src-tauri/Cargo.toml', 2)
616
- ])
617
- },
618
- {
619
- id: 'axum',
620
- kind: 'backend',
621
- language: 'rust',
622
- projectType: 'api',
623
- detect: ctx => buildDetection('axum', [
624
- evidenceIf(textMatches(ctx.cargoText, /\baxum\b/i), 'Cargo.toml -> axum', 4)
625
- ])
626
- },
627
- {
628
- id: 'actix',
629
- kind: 'backend',
630
- language: 'rust',
631
- projectType: 'api',
632
- detect: ctx => buildDetection('actix', [
633
- evidenceIf(textMatches(ctx.cargoText, /\bactix-web\b/i), 'Cargo.toml -> actix-web', 4)
634
- ])
635
- },
636
- {
637
- id: 'flutter',
638
- kind: 'mobile',
639
- projectType: 'mobile',
640
- detect: ctx => buildDetection('flutter', [
641
- evidenceIf(textMatches(ctx.pubspecText, /^\s*flutter\s*:/im), 'pubspec.yaml -> flutter', 4)
642
- ])
643
- },
644
- {
645
- id: 'react-native',
646
- kind: 'mobile',
647
- projectType: 'mobile',
648
- detect: ctx => buildDetection('react-native', [
649
- evidenceIf(hasDependency(ctx.depNames, 'react-native', 'expo'), 'package.json -> react-native/expo', 4)
650
- ])
651
- }
652
- ];
653
-
654
- function scanProject() {
655
- const appRoot = detectAppRoot(targetDir);
656
- const pkg = readJsonIfExistsAt(appRoot, 'package.json') || {};
657
- const composer = readJsonIfExistsAt(appRoot, 'composer.json') || {};
658
- const rootEntries = fs.existsSync(appRoot) ? fs.readdirSync(appRoot) : [];
659
- const rootCsproj = rootEntries.find(name => name.endsWith('.csproj')) || '';
660
- const deps = {
661
- ...(pkg.dependencies || {}),
662
- ...(pkg.devDependencies || {}),
663
- ...(pkg.peerDependencies || {})
664
- };
665
- const depNames = new Set(Object.keys(deps));
666
- const composerDeps = {
667
- ...(composer.require || {}),
668
- ...(composer['require-dev'] || {})
669
- };
670
- const composerDepNames = new Set(Object.keys(composerDeps));
671
- const ctx = {
672
- pkg,
673
- composer,
674
- depNames,
675
- composerDepNames,
676
- rootEntries,
677
- fileExists: (...parts) => fileExistsAt(appRoot, ...parts),
678
- requirementsText: readTextIfExistsAt(appRoot, 'requirements.txt'),
679
- pyprojectText: readTextIfExistsAt(appRoot, 'pyproject.toml'),
680
- goModText: readTextIfExistsAt(appRoot, 'go.mod'),
681
- pubspecText: readTextIfExistsAt(appRoot, 'pubspec.yaml'),
682
- gemfileText: readTextIfExistsAt(appRoot, 'Gemfile'),
683
- pomText: readTextIfExistsAt(appRoot, 'pom.xml'),
684
- gradleText: `${readTextIfExistsAt(appRoot, 'build.gradle')}\n${readTextIfExistsAt(appRoot, 'build.gradle.kts')}`,
685
- cargoText: `${readTextIfExistsAt(appRoot, 'Cargo.toml')}\n${readTextIfExistsAt(appRoot, 'src-tauri', 'Cargo.toml')}`,
686
- csprojText: rootCsproj ? readTextIfExistsAt(appRoot, rootCsproj) : '',
687
- programCsText: readTextIfExistsAt(appRoot, 'Program.cs'),
688
- viewFilesText: fs.existsSync(path.join(appRoot, 'app', 'View'))
689
- ? fs.readdirSync(path.join(appRoot, 'app', 'View'), { recursive: true }).join('\n')
690
- : ''
691
- };
692
-
693
- const languageDetections = LANGUAGE_DETECTORS
694
- .map(detector => detector.detect(ctx))
695
- .filter(result => result.score >= 2)
696
- .sort((a, b) => b.score - a.score);
697
-
698
- const frameworkDetails = FRAMEWORK_DETECTORS
699
- .map(detector => {
700
- const result = detector.detect(ctx);
701
- if (result.score < 3) return null;
702
- return {
703
- ...result,
704
- kind: detector.kind,
705
- language: detector.language,
706
- projectType: detector.projectType,
707
- confidence: confidenceLabel(result.score)
708
- };
709
- })
710
- .filter(Boolean)
711
- .sort((a, b) => b.score - a.score);
712
-
713
- const frameworkLanguageScores = frameworkDetails.reduce((acc, item) => {
714
- if (!item.language) return acc;
715
- acc[item.language] = (acc[item.language] || 0) + item.score;
716
- return acc;
717
- }, {});
718
- const primaryLanguage = Object.keys(frameworkLanguageScores).sort((a, b) => frameworkLanguageScores[b] - frameworkLanguageScores[a])[0]
719
- || languageDetections[0]?.id
720
- || 'typescript';
721
- const frameworks = uniq(frameworkDetails.map(item => item.id)).filter(name => FRAMEWORK_CHOICES.includes(name));
722
- const infrastructure = [];
723
- let projectType = '';
724
- let database = 'none';
725
- let testFramework = 'auto-detect';
726
- let cloud = 'none';
727
- let ciCd = 'none';
728
-
729
- if (frameworkDetails.some(item => item.projectType === 'desktop')) projectType = 'desktop';
730
- else if (frameworkDetails.some(item => item.projectType === 'mobile')) projectType = 'mobile';
731
- else if (frameworkDetails.some(item => item.kind === 'frontend') && frameworkDetails.some(item => item.kind === 'backend')) projectType = 'fullstack';
732
- else if (frameworkDetails.some(item => item.projectType === 'web-app')) projectType = 'web-app';
733
- else if (frameworkDetails.some(item => item.kind === 'backend')) projectType = 'api';
734
- else if (frameworkDetails.some(item => item.kind === 'frontend')) projectType = 'web-app';
735
- else if (fileExistsAt(appRoot, 'package.json') && pkg.bin) projectType = 'cli';
736
- else if (['php', 'python', 'go', 'java', 'ruby', 'csharp', 'rust'].includes(primaryLanguage)) projectType = 'api';
737
-
738
- if (hasDependency(depNames, 'pg', 'postgres', '@prisma/client') || /postgres/i.test(JSON.stringify(pkg))) database = 'postgres';
739
- else if (hasDependency(depNames, 'mysql', 'mysql2')) database = 'mysql';
740
- else if (hasDependency(depNames, 'sqlite3', 'better-sqlite3', 'rusqlite')) database = 'sqlite';
741
- else if (hasDependency(depNames, 'mongodb', 'mongoose')) database = 'mongodb';
742
- else if (hasDependency(depNames, 'redis', 'ioredis')) database = 'redis';
743
- else if (hasDependency(depNames, '@aws-sdk/client-dynamodb')) database = 'dynamodb';
744
-
745
- if (hasDependency(depNames, 'vitest')) testFramework = 'vitest';
746
- else if (hasDependency(depNames, 'jest')) testFramework = 'jest';
747
- else if (hasDependency(depNames, 'pytest') || textMatches(ctx.requirementsText, /\bpytest\b/i) || textMatches(ctx.pyprojectText, /\bpytest\b/i)) testFramework = 'pytest';
748
- else if (hasComposerDependency(composerDepNames, 'phpunit/phpunit')) testFramework = 'phpunit';
749
- else if (hasComposerDependency(composerDepNames, 'pestphp/pest')) testFramework = 'pest';
750
- else if (fileExistsAt(appRoot, 'go.mod')) testFramework = 'go test';
751
- else if (textMatches(ctx.gemfileText, /^\s*gem\s+["']rspec["']/im)) testFramework = 'rspec';
752
-
753
- if (repoHasAny(targetDir, [['Dockerfile'], ['docker-compose.yml'], ['docker-compose.yaml']])) {
754
- infrastructure.push('docker');
755
- }
756
-
757
- if (repoHasAny(targetDir, [['vercel.json']])) {
758
- cloud = 'vercel';
759
- infrastructure.push('vercel');
760
- } else if (repoHasAny(targetDir, [['fly.toml']])) {
761
- cloud = 'fly';
762
- infrastructure.push('fly');
763
- } else if (repoHasAny(targetDir, [['railway.json'], ['railway.toml']])) {
764
- cloud = 'railway';
765
- infrastructure.push('railway');
766
- }
767
-
768
- if (repoHasAny(targetDir, [['.github', 'workflows'], ['.gitlab-ci.yml'], ['.circleci', 'config.yml'], ['Jenkinsfile']], 2)) {
769
- if (repoHasAny(targetDir, [['.github', 'workflows']], 2)) ciCd = 'github-actions';
770
- else if (repoHasAny(targetDir, [['.gitlab-ci.yml']], 2)) ciCd = 'gitlab-ci';
771
- else if (repoHasAny(targetDir, [['.circleci', 'config.yml']], 2)) ciCd = 'circleci';
772
- else if (repoHasAny(targetDir, [['Jenkinsfile']], 2)) ciCd = 'jenkins';
773
- }
774
-
775
- const scanEvidence = [
776
- ...(languageDetections[0] ? [`language:${languageDetections[0].id} => ${languageDetections[0].evidence.join('; ')}`] : []),
777
- ...frameworkDetails.map(item => `${item.id} [${item.confidence}] => ${item.evidence.join('; ')}`)
778
- ];
779
-
780
- return {
781
- project_name: path.basename(targetDir),
782
- app_root: path.relative(targetDir, appRoot) || '.',
783
- description: pkg.description || composer.description || 'A software project',
784
- project_type: PROJECT_TYPE_CHOICES.includes(projectType) ? projectType : 'web-app',
785
- primary_language: LANGUAGE_CHOICES.includes(primaryLanguage) ? primaryLanguage : 'typescript',
786
- frameworks,
787
- framework_details: frameworkDetails,
788
- scan_evidence: scanEvidence,
789
- database: DATABASE_CHOICES.includes(database) ? database : 'none',
790
- package_manager: PACKAGE_MANAGER_CHOICES.includes(detectPackageManager(appRoot)) ? detectPackageManager(appRoot) : 'auto-detect',
791
- uses_docker: infrastructure.includes('docker'),
792
- cloud,
793
- infrastructure: uniq(infrastructure),
794
- ci_cd: ciCd,
795
- test_framework: TEST_FRAMEWORK_CHOICES.includes(testFramework) ? testFramework : 'auto-detect',
796
- max_file_lines: '300',
797
- code_style: 'project-standard',
798
- architecture_notes: '<!-- Add notes -->',
799
- current_phase: 'Initial setup',
800
- next_phase: '—',
801
- known_issues: '<!-- None yet -->'
802
- };
803
- }
90
+ return Array.from(skills).sort();
91
+ }
92
+
93
+ const FRAMEWORK_CHOICES = [
94
+ 'vue', 'react', 'next', 'nuxt', 'svelte', 'angular',
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'
99
+ ];
100
+
101
+ const LANGUAGE_CHOICES = ['typescript', 'javascript', 'python', 'go', 'php', 'rust', 'java', 'ruby', 'swift', 'kotlin', 'csharp', 'cpp'];
102
+ const PROJECT_TYPE_CHOICES = ['web-app', 'api', 'mobile', 'desktop', 'cli', 'fullstack'];
103
+ const DATABASE_CHOICES = ['postgres', 'mysql', 'sqlite', 'mongodb', 'redis', 'dynamodb', 'none'];
104
+ const PACKAGE_MANAGER_CHOICES = ['npm', 'pnpm', 'yarn', 'bun', 'pip', 'uv', 'go mod', 'cargo', 'composer', 'auto-detect'];
105
+ const TEST_FRAMEWORK_CHOICES = ['jest', 'vitest', 'pytest', 'go test', 'phpunit', 'pest', 'rspec', 'junit', 'auto-detect'];
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'];
110
+ const IGNORED_SCAN_DIRS = new Set(['.git', 'node_modules', 'vendor', 'dist', 'build', 'coverage', '.next', '.nuxt', '.turbo', '.cache', 'storage', 'tmp']);
111
+
112
+ function printHelp() {
113
+ console.log(`
114
+ ${pc.bold('Usage:')}
115
+ thachvd-kit init [--scan] [--yes]
116
+ thachvd-kit --version
117
+ thachvd-kit --help
118
+
119
+ ${pc.bold('Commands:')}
120
+ init Run the project init wizard
121
+
122
+ ${pc.bold('Options:')}
123
+ --scan Scan the current project to auto-detect stack
124
+ --yes Use detected/default values without prompting
125
+ --version Show the installed CLI version
126
+ --help Show this help message
127
+
128
+ ${pc.bold('What gets generated:')}
129
+ AGENTS.md Shared instructions for Codex, Antigravity, and Claude Code
130
+ CLAUDE.md Claude Code entry file that imports AGENTS.md
131
+ GEMINI.md Antigravity entry file that points to AGENTS.md
132
+ .agent/ Shared docs, skills, workflows, agents, and rules
133
+ `);
134
+
135
+ }
136
+
137
+ function formatScanPreviewLines(scanned) {
138
+ return [
139
+ ['App root', scanned.app_root || '.'],
140
+ ['Project type', scanned.project_type || 'unknown'],
141
+ ['Primary language', scanned.primary_language || 'unknown'],
142
+ ['Frameworks', (scanned.frameworks || []).join(', ') || 'no framework'],
143
+ ['Database', scanned.database || 'none'],
144
+ ['Package manager', scanned.package_manager || 'auto-detect'],
145
+ ['Test framework', scanned.test_framework || 'auto-detect'],
146
+ ['Cloud', scanned.cloud || 'none'],
147
+ ['Infrastructure', (scanned.infrastructure || []).join(', ') || 'none'],
148
+ ['CI/CD', scanned.ci_cd || 'none']
149
+ ];
150
+ }
151
+
152
+ async function askQuestions(questions) {
153
+ return prompts(questions, {
154
+ onCancel: () => {
155
+ console.log(pc.yellow('\nCancelled.'));
156
+ process.exit(130);
157
+ }
158
+ });
159
+ }
160
+
161
+ function fileExists(...parts) {
162
+ return fs.existsSync(path.join(targetDir, ...parts));
163
+ }
164
+
165
+ function fileExistsAt(baseDir, ...parts) {
166
+ return fs.existsSync(path.join(baseDir, ...parts));
167
+ }
168
+
169
+ function readJsonIfExists(...parts) {
170
+ const fullPath = path.join(targetDir, ...parts);
171
+ if (!fs.existsSync(fullPath)) return null;
172
+ try {
173
+ return JSON.parse(fs.readFileSync(fullPath, 'utf8'));
174
+ } catch {
175
+ return null;
176
+ }
177
+ }
178
+
179
+ function readJsonIfExistsAt(baseDir, ...parts) {
180
+ const fullPath = path.join(baseDir, ...parts);
181
+ if (!fs.existsSync(fullPath)) return null;
182
+ try {
183
+ return JSON.parse(fs.readFileSync(fullPath, 'utf8'));
184
+ } catch {
185
+ return null;
186
+ }
187
+ }
188
+
189
+ function readTextIfExists(...parts) {
190
+ const fullPath = path.join(targetDir, ...parts);
191
+ if (!fs.existsSync(fullPath)) return '';
192
+ try {
193
+ return fs.readFileSync(fullPath, 'utf8');
194
+ } catch {
195
+ return '';
196
+ }
197
+ }
198
+
199
+ function readTextIfExistsAt(baseDir, ...parts) {
200
+ const fullPath = path.join(baseDir, ...parts);
201
+ if (!fs.existsSync(fullPath)) return '';
202
+ try {
203
+ return fs.readFileSync(fullPath, 'utf8');
204
+ } catch {
205
+ return '';
206
+ }
207
+ }
208
+
209
+ function detectPackageManager(baseDir = targetDir) {
210
+ if (fileExistsAt(baseDir, 'pnpm-lock.yaml')) return 'pnpm';
211
+ if (fileExistsAt(baseDir, 'yarn.lock')) return 'yarn';
212
+ if (fileExistsAt(baseDir, 'bun.lockb') || fileExistsAt(baseDir, 'bun.lock')) return 'bun';
213
+ if (fileExistsAt(baseDir, 'package-lock.json')) return 'npm';
214
+ if (fileExistsAt(baseDir, 'uv.lock')) return 'uv';
215
+ if (fileExistsAt(baseDir, 'poetry.lock')) return 'pip';
216
+ if (fileExistsAt(baseDir, 'requirements.txt') || fileExistsAt(baseDir, 'pyproject.toml')) return 'pip';
217
+ if (fileExistsAt(baseDir, 'go.mod')) return 'go mod';
218
+ if (fileExistsAt(baseDir, 'Cargo.toml')) return 'cargo';
219
+ if (fileExistsAt(baseDir, 'composer.json')) return 'composer';
220
+ return 'auto-detect';
221
+ }
222
+
223
+ function uniq(values) {
224
+ return Array.from(new Set(values.filter(Boolean)));
225
+ }
226
+
227
+ function detectAppRoot(baseDir) {
228
+ const candidates = [baseDir];
229
+ const queue = [{ dir: baseDir, depth: 0 }];
230
+
231
+ while (queue.length > 0) {
232
+ const { dir, depth } = queue.shift();
233
+ if (depth >= 2) continue;
234
+ let entries = [];
235
+ try {
236
+ entries = fs.readdirSync(dir, { withFileTypes: true });
237
+ } catch {
238
+ continue;
239
+ }
240
+ for (const entry of entries) {
241
+ if (!entry.isDirectory()) continue;
242
+ if (IGNORED_SCAN_DIRS.has(entry.name)) continue;
243
+ const nextDir = path.join(dir, entry.name);
244
+ candidates.push(nextDir);
245
+ queue.push({ dir: nextDir, depth: depth + 1 });
246
+ }
247
+ }
248
+
249
+ const scoreCandidate = dir => {
250
+ let score = 0;
251
+ if (fileExistsAt(dir, 'composer.json')) score += 5;
252
+ if (fileExistsAt(dir, 'package.json')) score += 4;
253
+ if (fileExistsAt(dir, 'Cargo.toml')) score += 4;
254
+ if (fileExistsAt(dir, 'go.mod')) score += 4;
255
+ if (fileExistsAt(dir, 'pyproject.toml') || fileExistsAt(dir, 'requirements.txt')) score += 4;
256
+ if (fileExistsAt(dir, 'artisan')) score += 5;
257
+ if (fileExistsAt(dir, 'bin', 'cake') || fileExistsAt(dir, 'app', 'Config', 'core.php')) score += 5;
258
+ if (fileExistsAt(dir, 'src-tauri', 'tauri.conf.json') || fileExistsAt(dir, 'src-tauri', 'tauri.conf.json5')) score += 5;
259
+ if (fileExistsAt(dir, 'manage.py')) score += 4;
260
+ if (fileExistsAt(dir, 'Gemfile')) score += 4;
261
+ if (fileExistsAt(dir, 'pom.xml') || fileExistsAt(dir, 'build.gradle') || fileExistsAt(dir, 'build.gradle.kts')) score += 4;
262
+ if (fileExistsAt(dir, 'app', 'View') || fileExistsAt(dir, 'resources', 'views') || fileExistsAt(dir, 'templates') || fileExistsAt(dir, 'app', 'views')) score += 2;
263
+ return score;
264
+ };
265
+
266
+ const ranked = candidates
267
+ .map(dir => ({ dir, score: scoreCandidate(dir) }))
268
+ .filter(item => item.score > 0)
269
+ .sort((a, b) => (b.score - a.score) || (a.dir.length - b.dir.length));
270
+
271
+ return ranked[0]?.dir || baseDir;
272
+ }
273
+
274
+ function repoHasAny(baseDir, relativePaths, maxDepth = 3) {
275
+ const queue = [{ dir: baseDir, depth: 0 }];
276
+ while (queue.length > 0) {
277
+ const { dir, depth } = queue.shift();
278
+ for (const relativePath of relativePaths) {
279
+ if (fileExistsAt(dir, ...relativePath)) return true;
280
+ }
281
+ if (depth >= maxDepth) continue;
282
+ let entries = [];
283
+ try {
284
+ entries = fs.readdirSync(dir, { withFileTypes: true });
285
+ } catch {
286
+ continue;
287
+ }
288
+ for (const entry of entries) {
289
+ if (!entry.isDirectory()) continue;
290
+ if (IGNORED_SCAN_DIRS.has(entry.name)) continue;
291
+ queue.push({ dir: path.join(dir, entry.name), depth: depth + 1 });
292
+ }
293
+ }
294
+ return false;
295
+ }
296
+
297
+ function evidenceIf(condition, label, weight = 1) {
298
+ return condition ? { label, weight } : null;
299
+ }
300
+
301
+ function buildDetection(id, evidence, meta = {}) {
302
+ const hits = evidence.filter(Boolean);
303
+ const score = hits.reduce((sum, hit) => sum + hit.weight, 0);
304
+ return {
305
+ id,
306
+ score,
307
+ evidence: hits.map(hit => hit.label),
308
+ ...meta
309
+ };
310
+ }
311
+
312
+ function confidenceLabel(score) {
313
+ if (score >= 6) return 'high';
314
+ if (score >= 3) return 'medium';
315
+ return 'low';
316
+ }
317
+
318
+ function hasDependency(depNames, ...names) {
319
+ return names.some(name => depNames.has(name));
320
+ }
321
+
322
+ function hasComposerDependency(composerDepNames, ...names) {
323
+ return names.some(name => composerDepNames.has(name));
324
+ }
325
+
326
+ function hasRootFile(rootEntries, ...filenames) {
327
+ return filenames.some(name => rootEntries.has(name));
328
+ }
329
+
330
+ function textMatches(text, pattern) {
331
+ return Boolean(text && pattern.test(text));
332
+ }
333
+
334
+ const LANGUAGE_DETECTORS = [
335
+ {
336
+ id: 'typescript',
337
+ detect: ctx => buildDetection('typescript', [
338
+ evidenceIf(ctx.fileExists('tsconfig.json'), 'found tsconfig.json', 4),
339
+ evidenceIf(hasDependency(ctx.depNames, 'typescript', 'ts-node', 'tsx'), 'package.json -> typescript toolchain', 2)
340
+ ])
341
+ },
342
+ {
343
+ id: 'javascript',
344
+ detect: ctx => buildDetection('javascript', [
345
+ evidenceIf(ctx.fileExists('package.json'), 'found package.json', 2),
346
+ evidenceIf(ctx.depNames.size > 0, 'package.json -> JavaScript dependency graph', 1)
347
+ ])
348
+ },
349
+ {
350
+ id: 'python',
351
+ detect: ctx => buildDetection('python', [
352
+ evidenceIf(ctx.fileExists('requirements.txt'), 'found requirements.txt', 3),
353
+ evidenceIf(ctx.fileExists('pyproject.toml'), 'found pyproject.toml', 2),
354
+ evidenceIf(textMatches(ctx.pyprojectText, /\[tool\.(poetry|uv|hatch|pdm)\]/i), 'pyproject.toml -> Python tool section', 2)
355
+ ])
356
+ },
357
+ {
358
+ id: 'go',
359
+ detect: ctx => buildDetection('go', [
360
+ evidenceIf(ctx.fileExists('go.mod'), 'found go.mod', 4)
361
+ ])
362
+ },
363
+ {
364
+ id: 'php',
365
+ detect: ctx => buildDetection('php', [
366
+ evidenceIf(ctx.fileExists('composer.json'), 'found composer.json', 4),
367
+ evidenceIf(ctx.composerDepNames.size > 0, 'composer.json -> PHP dependency graph', 1)
368
+ ])
369
+ },
370
+ {
371
+ id: 'rust',
372
+ detect: ctx => buildDetection('rust', [
373
+ evidenceIf(ctx.fileExists('Cargo.toml'), 'found Cargo.toml', 4),
374
+ evidenceIf(ctx.fileExists('src-tauri', 'Cargo.toml'), 'found src-tauri/Cargo.toml', 5)
375
+ ])
376
+ },
377
+ {
378
+ id: 'java',
379
+ detect: ctx => buildDetection('java', [
380
+ evidenceIf(ctx.fileExists('pom.xml'), 'found pom.xml', 4),
381
+ evidenceIf(ctx.fileExists('build.gradle') || ctx.fileExists('build.gradle.kts'), 'found Gradle build file', 3)
382
+ ])
383
+ },
384
+ {
385
+ id: 'ruby',
386
+ detect: ctx => buildDetection('ruby', [
387
+ evidenceIf(ctx.fileExists('Gemfile'), 'found Gemfile', 4),
388
+ evidenceIf(ctx.fileExists('config.ru'), 'found config.ru', 1)
389
+ ])
390
+ },
391
+ {
392
+ id: 'csharp',
393
+ detect: ctx => buildDetection('csharp', [
394
+ evidenceIf(ctx.rootEntries.some(name => name.endsWith('.sln') || name.endsWith('.csproj')), 'found .sln/.csproj', 4),
395
+ evidenceIf(ctx.fileExists('Program.cs'), 'found Program.cs', 1)
396
+ ])
397
+ }
398
+ ];
399
+
400
+ const FRAMEWORK_DETECTORS = [
401
+ {
402
+ id: 'next',
403
+ kind: 'frontend',
404
+ projectType: 'web-app',
405
+ detect: ctx => buildDetection('next', [
406
+ evidenceIf(hasDependency(ctx.depNames, 'next'), 'package.json -> next', 4),
407
+ evidenceIf(ctx.fileExists('next.config.js') || ctx.fileExists('next.config.mjs') || ctx.fileExists('next.config.ts'), 'found next.config.*', 3),
408
+ evidenceIf(ctx.fileExists('app') || ctx.fileExists('pages'), 'found app/ or pages/', 1)
409
+ ])
410
+ },
411
+ {
412
+ id: 'react',
413
+ kind: 'frontend',
414
+ projectType: 'web-app',
415
+ detect: ctx => buildDetection('react', [
416
+ evidenceIf(hasDependency(ctx.depNames, 'react'), 'package.json -> react', 4),
417
+ evidenceIf(hasDependency(ctx.depNames, 'react-dom'), 'package.json -> react-dom', 2)
418
+ ])
419
+ },
420
+ {
421
+ id: 'vue',
422
+ kind: 'frontend',
423
+ projectType: 'web-app',
424
+ detect: ctx => buildDetection('vue', [
425
+ evidenceIf(hasDependency(ctx.depNames, 'vue'), 'package.json -> vue', 4)
426
+ ])
427
+ },
428
+ {
429
+ id: 'nuxt',
430
+ kind: 'frontend',
431
+ projectType: 'web-app',
432
+ detect: ctx => buildDetection('nuxt', [
433
+ evidenceIf(hasDependency(ctx.depNames, 'nuxt'), 'package.json -> nuxt', 4),
434
+ evidenceIf(ctx.fileExists('nuxt.config.ts') || ctx.fileExists('nuxt.config.js'), 'found nuxt.config.*', 3)
435
+ ])
436
+ },
437
+ {
438
+ id: 'svelte',
439
+ kind: 'frontend',
440
+ projectType: 'web-app',
441
+ detect: ctx => buildDetection('svelte', [
442
+ evidenceIf(hasDependency(ctx.depNames, 'svelte', '@sveltejs/kit'), 'package.json -> svelte', 4)
443
+ ])
444
+ },
445
+ {
446
+ id: 'angular',
447
+ kind: 'frontend',
448
+ projectType: 'web-app',
449
+ detect: ctx => buildDetection('angular', [
450
+ evidenceIf(hasDependency(ctx.depNames, '@angular/core'), 'package.json -> @angular/core', 4),
451
+ evidenceIf(ctx.fileExists('angular.json'), 'found angular.json', 3)
452
+ ])
453
+ },
454
+ {
455
+ id: 'express',
456
+ kind: 'backend',
457
+ language: 'javascript',
458
+ projectType: 'api',
459
+ detect: ctx => buildDetection('express', [
460
+ evidenceIf(hasDependency(ctx.depNames, 'express'), 'package.json -> express', 4)
461
+ ])
462
+ },
463
+ {
464
+ id: 'nestjs',
465
+ kind: 'backend',
466
+ language: 'javascript',
467
+ projectType: 'api',
468
+ detect: ctx => buildDetection('nestjs', [
469
+ evidenceIf(hasDependency(ctx.depNames, '@nestjs/core'), 'package.json -> @nestjs/core', 4)
470
+ ])
471
+ },
472
+ {
473
+ id: 'fastapi',
474
+ kind: 'backend',
475
+ language: 'python',
476
+ projectType: 'api',
477
+ detect: ctx => buildDetection('fastapi', [
478
+ evidenceIf(textMatches(ctx.requirementsText, /\bfastapi\b/i) || textMatches(ctx.pyprojectText, /\bfastapi\b/i), 'Python deps -> fastapi', 4)
479
+ ])
480
+ },
481
+ {
482
+ id: 'django',
483
+ kind: 'backend',
484
+ language: 'python',
485
+ projectType: 'web-app',
486
+ detect: ctx => buildDetection('django', [
487
+ evidenceIf(textMatches(ctx.requirementsText, /\bdjango\b/i) || textMatches(ctx.pyprojectText, /\bdjango\b/i), 'Python deps -> django', 4),
488
+ evidenceIf(ctx.fileExists('manage.py'), 'found manage.py', 2),
489
+ evidenceIf(ctx.fileExists('templates'), 'found templates/', 1)
490
+ ])
491
+ },
492
+ {
493
+ id: 'flask',
494
+ kind: 'backend',
495
+ language: 'python',
496
+ projectType: 'api',
497
+ detect: ctx => buildDetection('flask', [
498
+ evidenceIf(textMatches(ctx.requirementsText, /\bflask\b/i) || textMatches(ctx.pyprojectText, /\bflask\b/i), 'Python deps -> flask', 4)
499
+ ])
500
+ },
501
+ {
502
+ id: 'laravel',
503
+ kind: 'backend',
504
+ language: 'php',
505
+ projectType: 'web-app',
506
+ detect: ctx => buildDetection('laravel', [
507
+ evidenceIf(hasComposerDependency(ctx.composerDepNames, 'laravel/framework'), 'composer.json -> laravel/framework', 4),
508
+ evidenceIf(ctx.fileExists('artisan'), 'found artisan', 3),
509
+ evidenceIf(ctx.fileExists('resources', 'views'), 'found resources/views', 1)
510
+ ])
511
+ },
512
+ {
513
+ id: 'cakephp',
514
+ kind: 'backend',
515
+ language: 'php',
516
+ projectType: 'web-app',
517
+ detect: ctx => buildDetection('cakephp', [
518
+ evidenceIf(hasComposerDependency(ctx.composerDepNames, 'cakephp/cakephp'), 'composer.json -> cakephp/cakephp', 4),
519
+ evidenceIf(ctx.fileExists('bin', 'cake'), 'found bin/cake', 3),
520
+ evidenceIf(ctx.fileExists('config', 'app.php'), 'found config/app.php', 2),
521
+ evidenceIf(ctx.fileExists('app', 'Config', 'core.php'), 'found app/Config/core.php (CakePHP legacy)', 3),
522
+ evidenceIf(ctx.fileExists('app', 'Controller', 'AppController.php'), 'found app/Controller/AppController.php', 2),
523
+ evidenceIf(ctx.fileExists('app', 'Model', 'AppModel.php'), 'found app/Model/AppModel.php', 2),
524
+ evidenceIf(ctx.fileExists('lib', 'Cake'), 'found lib/Cake', 3),
525
+ evidenceIf(ctx.fileExists('cake', 'console', 'cake'), 'found cake/console/cake', 3),
526
+ evidenceIf(ctx.fileExists('app', 'View'), 'found app/View', 2),
527
+ evidenceIf(textMatches(ctx.viewFilesText, /\.ctp$/im), 'found .ctp view templates', 2)
528
+ ])
529
+ },
530
+ {
531
+ id: 'symfony',
532
+ kind: 'backend',
533
+ language: 'php',
534
+ projectType: 'web-app',
535
+ detect: ctx => buildDetection('symfony', [
536
+ evidenceIf(hasComposerDependency(ctx.composerDepNames, 'symfony/framework-bundle'), 'composer.json -> symfony/framework-bundle', 4),
537
+ evidenceIf(ctx.fileExists('bin', 'console'), 'found bin/console', 2),
538
+ evidenceIf(ctx.fileExists('config', 'bundles.php'), 'found config/bundles.php', 1),
539
+ evidenceIf(ctx.fileExists('templates'), 'found templates/', 1)
540
+ ])
541
+ },
542
+ {
543
+ id: 'codeigniter',
544
+ kind: 'backend',
545
+ language: 'php',
546
+ projectType: 'api',
547
+ detect: ctx => buildDetection('codeigniter', [
548
+ evidenceIf(hasComposerDependency(ctx.composerDepNames, 'codeigniter4/framework'), 'composer.json -> codeigniter4/framework', 4),
549
+ evidenceIf(ctx.fileExists('spark'), 'found spark', 2),
550
+ evidenceIf(ctx.fileExists('app', 'Config', 'App.php'), 'found app/Config/App.php', 1)
551
+ ])
552
+ },
553
+ {
554
+ id: 'gin',
555
+ kind: 'backend',
556
+ language: 'go',
557
+ projectType: 'api',
558
+ detect: ctx => buildDetection('gin', [
559
+ evidenceIf(textMatches(ctx.goModText, /\bgithub\.com\/gin-gonic\/gin\b/i), 'go.mod -> github.com/gin-gonic/gin', 4)
560
+ ])
561
+ },
562
+ {
563
+ id: 'fiber',
564
+ kind: 'backend',
565
+ language: 'go',
566
+ projectType: 'api',
567
+ detect: ctx => buildDetection('fiber', [
568
+ evidenceIf(textMatches(ctx.goModText, /\bgithub\.com\/gofiber\/fiber\b/i), 'go.mod -> github.com/gofiber/fiber', 4)
569
+ ])
570
+ },
571
+ {
572
+ id: 'echo',
573
+ kind: 'backend',
574
+ language: 'go',
575
+ projectType: 'api',
576
+ detect: ctx => buildDetection('echo', [
577
+ evidenceIf(textMatches(ctx.goModText, /\bgithub\.com\/labstack\/echo\b/i), 'go.mod -> github.com/labstack/echo', 4)
578
+ ])
579
+ },
580
+ {
581
+ id: 'rails',
582
+ kind: 'backend',
583
+ language: 'ruby',
584
+ projectType: 'web-app',
585
+ detect: ctx => buildDetection('rails', [
586
+ evidenceIf(textMatches(ctx.gemfileText, /^\s*gem\s+["']rails["']/im), 'Gemfile -> rails', 4),
587
+ evidenceIf(ctx.fileExists('bin', 'rails'), 'found bin/rails', 2),
588
+ evidenceIf(ctx.fileExists('app', 'views'), 'found app/views', 1)
589
+ ])
590
+ },
591
+ {
592
+ id: 'spring',
593
+ kind: 'backend',
594
+ language: 'java',
595
+ projectType: 'api',
596
+ detect: ctx => buildDetection('spring', [
597
+ evidenceIf(textMatches(ctx.pomText, /spring-boot/i) || textMatches(ctx.gradleText, /spring-boot/i), 'Java build -> spring-boot', 4)
598
+ ])
599
+ },
600
+ {
601
+ id: 'aspnet',
602
+ kind: 'backend',
603
+ language: 'csharp',
604
+ projectType: 'api',
605
+ detect: ctx => buildDetection('aspnet', [
606
+ evidenceIf(textMatches(ctx.csprojText, /Microsoft\.NET\.Sdk\.Web/i), '.csproj -> Microsoft.NET.Sdk.Web', 4),
607
+ evidenceIf(textMatches(ctx.programCsText, /WebApplication\.CreateBuilder/i), 'Program.cs -> WebApplication.CreateBuilder', 2)
608
+ ])
609
+ },
610
+ {
611
+ id: 'tauri',
612
+ kind: 'desktop',
613
+ language: 'rust',
614
+ projectType: 'desktop',
615
+ detect: ctx => buildDetection('tauri', [
616
+ evidenceIf(hasDependency(ctx.depNames, '@tauri-apps/api'), 'package.json -> @tauri-apps/api', 4),
617
+ evidenceIf(ctx.fileExists('src-tauri', 'tauri.conf.json') || ctx.fileExists('src-tauri', 'tauri.conf.json5'), 'found src-tauri/tauri.conf.*', 4),
618
+ evidenceIf(ctx.fileExists('src-tauri', 'Cargo.toml'), 'found src-tauri/Cargo.toml', 2)
619
+ ])
620
+ },
621
+ {
622
+ id: 'axum',
623
+ kind: 'backend',
624
+ language: 'rust',
625
+ projectType: 'api',
626
+ detect: ctx => buildDetection('axum', [
627
+ evidenceIf(textMatches(ctx.cargoText, /\baxum\b/i), 'Cargo.toml -> axum', 4)
628
+ ])
629
+ },
630
+ {
631
+ id: 'actix',
632
+ kind: 'backend',
633
+ language: 'rust',
634
+ projectType: 'api',
635
+ detect: ctx => buildDetection('actix', [
636
+ evidenceIf(textMatches(ctx.cargoText, /\bactix-web\b/i), 'Cargo.toml -> actix-web', 4)
637
+ ])
638
+ },
639
+ {
640
+ id: 'flutter',
641
+ kind: 'mobile',
642
+ projectType: 'mobile',
643
+ detect: ctx => buildDetection('flutter', [
644
+ evidenceIf(textMatches(ctx.pubspecText, /^\s*flutter\s*:/im), 'pubspec.yaml -> flutter', 4)
645
+ ])
646
+ },
647
+ {
648
+ id: 'react-native',
649
+ kind: 'mobile',
650
+ projectType: 'mobile',
651
+ detect: ctx => buildDetection('react-native', [
652
+ evidenceIf(hasDependency(ctx.depNames, 'react-native', 'expo'), 'package.json -> react-native/expo', 4)
653
+ ])
654
+ }
655
+ ];
656
+
657
+ function scanProject() {
658
+ const appRoot = detectAppRoot(targetDir);
659
+ const pkg = readJsonIfExistsAt(appRoot, 'package.json') || {};
660
+ const composer = readJsonIfExistsAt(appRoot, 'composer.json') || {};
661
+ const rootEntries = fs.existsSync(appRoot) ? fs.readdirSync(appRoot) : [];
662
+ const rootCsproj = rootEntries.find(name => name.endsWith('.csproj')) || '';
663
+ const deps = {
664
+ ...(pkg.dependencies || {}),
665
+ ...(pkg.devDependencies || {}),
666
+ ...(pkg.peerDependencies || {})
667
+ };
668
+ const depNames = new Set(Object.keys(deps));
669
+ const composerDeps = {
670
+ ...(composer.require || {}),
671
+ ...(composer['require-dev'] || {})
672
+ };
673
+ const composerDepNames = new Set(Object.keys(composerDeps));
674
+ const ctx = {
675
+ pkg,
676
+ composer,
677
+ depNames,
678
+ composerDepNames,
679
+ rootEntries,
680
+ fileExists: (...parts) => fileExistsAt(appRoot, ...parts),
681
+ requirementsText: readTextIfExistsAt(appRoot, 'requirements.txt'),
682
+ pyprojectText: readTextIfExistsAt(appRoot, 'pyproject.toml'),
683
+ goModText: readTextIfExistsAt(appRoot, 'go.mod'),
684
+ pubspecText: readTextIfExistsAt(appRoot, 'pubspec.yaml'),
685
+ gemfileText: readTextIfExistsAt(appRoot, 'Gemfile'),
686
+ pomText: readTextIfExistsAt(appRoot, 'pom.xml'),
687
+ gradleText: `${readTextIfExistsAt(appRoot, 'build.gradle')}\n${readTextIfExistsAt(appRoot, 'build.gradle.kts')}`,
688
+ cargoText: `${readTextIfExistsAt(appRoot, 'Cargo.toml')}\n${readTextIfExistsAt(appRoot, 'src-tauri', 'Cargo.toml')}`,
689
+ csprojText: rootCsproj ? readTextIfExistsAt(appRoot, rootCsproj) : '',
690
+ programCsText: readTextIfExistsAt(appRoot, 'Program.cs'),
691
+ viewFilesText: fs.existsSync(path.join(appRoot, 'app', 'View'))
692
+ ? fs.readdirSync(path.join(appRoot, 'app', 'View'), { recursive: true }).join('\n')
693
+ : ''
694
+ };
695
+
696
+ const languageDetections = LANGUAGE_DETECTORS
697
+ .map(detector => detector.detect(ctx))
698
+ .filter(result => result.score >= 2)
699
+ .sort((a, b) => b.score - a.score);
700
+
701
+ const frameworkDetails = FRAMEWORK_DETECTORS
702
+ .map(detector => {
703
+ const result = detector.detect(ctx);
704
+ if (result.score < 3) return null;
705
+ return {
706
+ ...result,
707
+ kind: detector.kind,
708
+ language: detector.language,
709
+ projectType: detector.projectType,
710
+ confidence: confidenceLabel(result.score)
711
+ };
712
+ })
713
+ .filter(Boolean)
714
+ .sort((a, b) => b.score - a.score);
715
+
716
+ const frameworkLanguageScores = frameworkDetails.reduce((acc, item) => {
717
+ if (!item.language) return acc;
718
+ acc[item.language] = (acc[item.language] || 0) + item.score;
719
+ return acc;
720
+ }, {});
721
+ const primaryLanguage = Object.keys(frameworkLanguageScores).sort((a, b) => frameworkLanguageScores[b] - frameworkLanguageScores[a])[0]
722
+ || languageDetections[0]?.id
723
+ || 'typescript';
724
+ const frameworks = uniq(frameworkDetails.map(item => item.id)).filter(name => FRAMEWORK_CHOICES.includes(name));
725
+ const infrastructure = [];
726
+ let projectType = '';
727
+ let database = 'none';
728
+ let testFramework = 'auto-detect';
729
+ let cloud = 'none';
730
+ let ciCd = 'none';
731
+
732
+ if (frameworkDetails.some(item => item.projectType === 'desktop')) projectType = 'desktop';
733
+ else if (frameworkDetails.some(item => item.projectType === 'mobile')) projectType = 'mobile';
734
+ else if (frameworkDetails.some(item => item.kind === 'frontend') && frameworkDetails.some(item => item.kind === 'backend')) projectType = 'fullstack';
735
+ else if (frameworkDetails.some(item => item.projectType === 'web-app')) projectType = 'web-app';
736
+ else if (frameworkDetails.some(item => item.kind === 'backend')) projectType = 'api';
737
+ else if (frameworkDetails.some(item => item.kind === 'frontend')) projectType = 'web-app';
738
+ else if (fileExistsAt(appRoot, 'package.json') && pkg.bin) projectType = 'cli';
739
+ else if (['php', 'python', 'go', 'java', 'ruby', 'csharp', 'rust'].includes(primaryLanguage)) projectType = 'api';
740
+
741
+ if (hasDependency(depNames, 'pg', 'postgres', '@prisma/client') || /postgres/i.test(JSON.stringify(pkg))) database = 'postgres';
742
+ else if (hasDependency(depNames, 'mysql', 'mysql2')) database = 'mysql';
743
+ else if (hasDependency(depNames, 'sqlite3', 'better-sqlite3', 'rusqlite')) database = 'sqlite';
744
+ else if (hasDependency(depNames, 'mongodb', 'mongoose')) database = 'mongodb';
745
+ else if (hasDependency(depNames, 'redis', 'ioredis')) database = 'redis';
746
+ else if (hasDependency(depNames, '@aws-sdk/client-dynamodb')) database = 'dynamodb';
747
+
748
+ if (hasDependency(depNames, 'vitest')) testFramework = 'vitest';
749
+ else if (hasDependency(depNames, 'jest')) testFramework = 'jest';
750
+ else if (hasDependency(depNames, 'pytest') || textMatches(ctx.requirementsText, /\bpytest\b/i) || textMatches(ctx.pyprojectText, /\bpytest\b/i)) testFramework = 'pytest';
751
+ else if (hasComposerDependency(composerDepNames, 'phpunit/phpunit')) testFramework = 'phpunit';
752
+ else if (hasComposerDependency(composerDepNames, 'pestphp/pest')) testFramework = 'pest';
753
+ else if (fileExistsAt(appRoot, 'go.mod')) testFramework = 'go test';
754
+ else if (textMatches(ctx.gemfileText, /^\s*gem\s+["']rspec["']/im)) testFramework = 'rspec';
755
+
756
+ if (repoHasAny(targetDir, [['Dockerfile'], ['docker-compose.yml'], ['docker-compose.yaml']])) {
757
+ infrastructure.push('docker');
758
+ }
759
+
760
+ if (repoHasAny(targetDir, [['vercel.json']])) {
761
+ cloud = 'vercel';
762
+ infrastructure.push('vercel');
763
+ } else if (repoHasAny(targetDir, [['fly.toml']])) {
764
+ cloud = 'fly';
765
+ infrastructure.push('fly');
766
+ } else if (repoHasAny(targetDir, [['railway.json'], ['railway.toml']])) {
767
+ cloud = 'railway';
768
+ infrastructure.push('railway');
769
+ }
770
+
771
+ if (repoHasAny(targetDir, [['.github', 'workflows'], ['.gitlab-ci.yml'], ['.circleci', 'config.yml'], ['Jenkinsfile']], 2)) {
772
+ if (repoHasAny(targetDir, [['.github', 'workflows']], 2)) ciCd = 'github-actions';
773
+ else if (repoHasAny(targetDir, [['.gitlab-ci.yml']], 2)) ciCd = 'gitlab-ci';
774
+ else if (repoHasAny(targetDir, [['.circleci', 'config.yml']], 2)) ciCd = 'circleci';
775
+ else if (repoHasAny(targetDir, [['Jenkinsfile']], 2)) ciCd = 'jenkins';
776
+ }
777
+
778
+ const scanEvidence = [
779
+ ...(languageDetections[0] ? [`language:${languageDetections[0].id} => ${languageDetections[0].evidence.join('; ')}`] : []),
780
+ ...frameworkDetails.map(item => `${item.id} [${item.confidence}] => ${item.evidence.join('; ')}`)
781
+ ];
782
+
783
+ return {
784
+ project_name: path.basename(targetDir),
785
+ app_root: path.relative(targetDir, appRoot) || '.',
786
+ description: pkg.description || composer.description || 'A software project',
787
+ project_type: PROJECT_TYPE_CHOICES.includes(projectType) ? projectType : 'web-app',
788
+ primary_language: LANGUAGE_CHOICES.includes(primaryLanguage) ? primaryLanguage : 'typescript',
789
+ frameworks,
790
+ framework_details: frameworkDetails,
791
+ scan_evidence: scanEvidence,
792
+ database: DATABASE_CHOICES.includes(database) ? database : 'none',
793
+ package_manager: PACKAGE_MANAGER_CHOICES.includes(detectPackageManager(appRoot)) ? detectPackageManager(appRoot) : 'auto-detect',
794
+ uses_docker: infrastructure.includes('docker'),
795
+ cloud,
796
+ infrastructure: uniq(infrastructure),
797
+ ci_cd: ciCd,
798
+ test_framework: TEST_FRAMEWORK_CHOICES.includes(testFramework) ? testFramework : 'auto-detect',
799
+ max_file_lines: '300',
800
+ code_style: 'project-standard',
801
+ architecture_notes: '<!-- Add notes -->',
802
+ current_phase: 'Initial setup',
803
+ next_phase: '—',
804
+ known_issues: '<!-- None yet -->'
805
+ };
806
+ }
804
807
 
805
808
  function resolveAgentTable(data) {
806
809
  const rows = [];
@@ -808,7 +811,7 @@ function resolveAgentTable(data) {
808
811
  const lang = (data.primary_language || "").toLowerCase();
809
812
 
810
813
  // Frontend
811
- const frontendFws = frameworks.filter(f => FRONTEND_FRAMEWORKS.includes(f));
814
+ const frontendFws = frameworks.filter(f => FRONTEND_FRAMEWORKS.includes(f));
812
815
  if (frontendFws.length > 0) {
813
816
  let feSkills = [];
814
817
  frontendFws.forEach(fw => {
@@ -819,14 +822,14 @@ function resolveAgentTable(data) {
819
822
 
820
823
  // Backend
821
824
  let beSkills = [...(LANG_SKILLS[lang] || ["api-design"])];
822
- const backendFws = frameworks.filter(f => BACKEND_FRAMEWORKS.includes(f));
825
+ const backendFws = frameworks.filter(f => BACKEND_FRAMEWORKS.includes(f));
823
826
  backendFws.forEach(fw => {
824
827
  beSkills.push(...(FRAMEWORK_SKILLS[fw] || []));
825
828
  });
826
829
  rows.push(["Backend API", "backend-specialist", Array.from(new Set(beSkills)).slice(0, 3).join(", ")]);
827
830
 
828
831
  // Mobile/Desktop
829
- const mobileFws = frameworks.filter(f => MOBILE_DESKTOP_FRAMEWORKS.includes(f));
832
+ const mobileFws = frameworks.filter(f => MOBILE_DESKTOP_FRAMEWORKS.includes(f));
830
833
  if (mobileFws.length > 0) {
831
834
  let mobileSkills = [];
832
835
  mobileFws.forEach(fw => {
@@ -849,659 +852,523 @@ function resolveAgentTable(data) {
849
852
 
850
853
  // --- File Generators ---
851
854
 
852
- function generateProjectContext(data) {
853
- const agentTable = resolveAgentTable(data);
854
- const autoSkills = resolveSkills(data);
855
- const scanEvidenceBlock = (data.scan_evidence || []).length > 0
856
- ? `
857
- ---
858
-
859
- ## Scan Evidence
860
-
861
- These detections were inferred from the current codebase during \`init --scan\`:
862
-
863
- \`\`\`text
864
- ${(data.scan_evidence || []).map(line => '- ' + line).join('\n')}
865
- \`\`\`
866
- `
867
- : '';
868
-
869
- const agentRows = agentTable.map(([task, agent, skills]) => `| ${task} | \`${agent}\` | ${skills} |`).join('\n');
870
- const autoSkillsList = autoSkills.map(s => `- ${s}`).join('\n');
871
-
872
- return `# PROJECT_CONTEXT.md
873
- # Generated by thachvd-kit init wizard on ${new Date().toISOString().split('T')[0]}
874
- # Edit freely — this is your project's brain.
875
-
876
- ## Project
877
-
878
- - **Name:** ${data.project_name}
879
- - **Description:** ${data.description}
880
- - **Type:** ${data.project_type}
881
-
882
- ---
883
-
884
- ## Context Status
885
-
886
- \`\`\`yaml
887
- context:
888
- status: ${data.context_status || 'baseline'}
889
- needs_ai_refinement: ${data.needs_ai_refinement ? 'true' : 'false'}
890
- last_refined_at: ${data.last_refined_at || 'not-yet-refined'}
891
- \`\`\`
892
-
893
- ---
894
-
895
- ## Stack
855
+ function generateSharedAgentsMd(data) {
856
+ return `# AGENTS.md
896
857
 
897
- \`\`\`yaml
898
- stack:
899
- app_root: ${data.app_root || '.'}
900
- primary_language: ${data.primary_language}
901
- frameworks: [${(data.frameworks || []).join(', ')}]
902
- database: ${data.database || 'none'}
903
- infrastructure: [${(data.infrastructure || []).join(', ')}]
904
- package_manager: ${data.package_manager || 'auto-detect'}
905
- test_framework: ${data.test_framework || 'auto-detect'}
906
- \`\`\`
907
-
908
- ${scanEvidenceBlock}
909
-
910
- ---
911
-
912
- ## Agent Routing
858
+ Shared operating instructions for Codex, Antigravity, and Claude Code.
913
859
 
914
- | Task Type | Agent | Primary Skills |
915
- |-----------|-------|----------------|
916
- ${agentRows}
860
+ ## Startup
917
861
 
918
- ---
862
+ 1. Read this file first.
863
+ 2. Read \`.agent/docs/project.md\` for detected stack, commands, and routing.
864
+ 3. Read \`.agent/docs/workflow.md\` for the task flow before editing.
865
+ 4. Read \`.agent/docs/architecture.md\` and \`.agent/docs/conventions.md\` before planning code changes.
866
+ 5. Load only the agent, skill, or workflow files that match the current task.
919
867
 
920
- ## Auto-Resolved Skills
921
-
922
- Based on your stack, these skills will be loaded automatically:
923
-
924
- \`\`\`text
925
- ${autoSkillsList}
926
- \`\`\`
868
+ If any \`.agent/docs/*.md\` file still contains \`TODO: refine\`, update the docs by scanning the project before making product code changes.
927
869
 
928
- ---
870
+ ## Task Flow
929
871
 
930
- ## Project Conventions
872
+ - Questions and analysis: answer directly, cite relevant files when useful, and do not edit code.
873
+ - Simple fix: inspect dependencies, make the smallest change, run focused verification.
874
+ - Feature or refactor: define success criteria, make a short plan, implement, then verify.
875
+ - Multi-domain work: use \`.agent/workflows/orchestrate.md\` and route to the relevant specialist docs.
876
+ - UI work: read \`.agent/agents/frontend-specialist.md\` and applicable design skills before editing.
931
877
 
932
- - **Max file size:** ${data.max_file_lines || '300'} lines (then modularize)
933
- - **Code style:** ${data.code_style || 'project-standard'}
878
+ ## Rules
934
879
 
935
- ---
880
+ - Respond in the user's language; keep code, identifiers, and code comments in English.
881
+ - State assumptions when the request is ambiguous.
882
+ - Prefer the existing project style over new abstractions.
883
+ - Keep changes surgical and remove only dead code introduced by your change.
884
+ - Tests or equivalent verification are mandatory before claiming done.
885
+ - Keep files under ${data.max_file_lines || '300'} lines unless the project already has a different standard in \`.agent/docs/conventions.md\`.
936
886
 
937
- ## Architecture Notes
887
+ ## Shared Knowledge
938
888
 
939
- <!-- Add architecture notes, key decisions, patterns used -->
889
+ - Project docs: \`.agent/docs/\`
890
+ - Specialist agents: \`.agent/agents/\`
891
+ - Skills: \`.agent/skills/\`
892
+ - Workflows: \`.agent/workflows/\`
893
+ - Antigravity mirror rules: \`.agent/rules/GEMINI.md\`
894
+ `;
895
+ }
940
896
 
941
- ---
897
+ function generateSharedClaudeMd() {
898
+ return `@AGENTS.md
942
899
 
943
- ## Current Phase
900
+ ## Claude Code
944
901
 
945
- - **Done:** —
946
- - **In Progress:** Initial setup
947
- - **Next:** —
902
+ This repository uses AGENTS.md as the shared cross-agent entry file. Follow the imported instructions and keep Claude-specific notes here only when they cannot apply to Codex or Antigravity.
903
+ `;
904
+ }
948
905
 
906
+ function generateSharedGeminiMd() {
907
+ return `# GEMINI.md
949
908
 
950
- ---
951
-
952
- ## Known Issues / Tasks
909
+ Antigravity entry file for this project.
953
910
 
954
- <!-- Add known issues or pending tasks -->
911
+ Read AGENTS.md first, then follow the shared docs under .agent/docs/.
955
912
  `;
956
913
  }
957
914
 
958
-
959
- function generateClaudeMd(data) {
960
- return `# CLAUDE.md — ${data.project_name}
961
- # Generated by thachvd-kit. See PROJECT_CONTEXT.md for full stack info.
962
- # Platform: Claude Code
963
-
964
- ## Project
965
- ${data.description}
966
- Stack: ${data.primary_language} | ${(data.frameworks || []).join(', ')}
967
-
968
- ## Rules
969
- - Read PROJECT_CONTEXT.md first for stack details and agent routing.
970
- - If PROJECT_CONTEXT.md is missing or stale, create/update it before major work.
971
- - Follow skills in .agent/skills/ (or .claude/skills/).
972
- - Tests are mandatory. Verify before claiming done.
973
- - Max ${data.max_file_lines || '300'} lines per file.
974
-
975
- ## Key Skills
976
- - Plan: \`plan-writing\` → \`executing-plans\`
977
- - Review: \`requesting-code-review\` / \`receiving-code-review\`
978
- - Quality: \`clean-code\`, \`verification-before-completion\`
979
- - Workflow: \`subagent-driven-development\`, \`finishing-a-development-branch\`
980
-
981
- ## Commands
982
- - \`/plan\` — Create implementation plan
983
- - \`/debug\` — Systematic debugging
984
- - \`/test\` — Run test suite
985
- - \`/deploy\` — Deploy checklist
986
-
987
- See PROJECT_CONTEXT.md for full agent routing table.
988
- `;
989
- }
990
-
991
- function generateAgentsMd(data) {
992
- const frameworks = (data.frameworks || []);
915
+ function resolveCommands(data) {
916
+ const frameworks = data.frameworks || [];
993
917
  const primaryLang = data.primary_language || 'javascript';
994
918
  const pkgManager = data.package_manager || 'npm';
995
919
  const testFramework = data.test_framework || 'auto-detect';
996
- const database = data.database || 'none';
997
- const cloud = data.cloud || 'none';
998
- const infrastructure = (data.infrastructure || []);
999
- const appRoot = data.app_root || '.';
1000
- const maxFileLines = data.max_file_lines || '300';
1001
920
 
1002
- // Build dev commands section based on detected package manager and framework
1003
- const isNode = ['javascript', 'typescript'].includes(primaryLang);
1004
921
  const isPython = primaryLang === 'python';
1005
922
  const isGo = primaryLang === 'go';
1006
923
  const isRust = primaryLang === 'rust';
1007
924
  const isPhp = primaryLang === 'php';
1008
925
 
1009
- let installCmd = 'npm install';
1010
- let devCmd = 'npm run dev';
1011
- let buildCmd = 'npm run build';
1012
- let testCmd = 'npm test';
1013
- let lintCmd = 'npm run lint';
1014
-
1015
- if (pkgManager === 'pnpm') { installCmd = 'pnpm install'; devCmd = 'pnpm dev'; buildCmd = 'pnpm build'; testCmd = 'pnpm test'; lintCmd = 'pnpm lint'; }
1016
- else if (pkgManager === 'yarn') { installCmd = 'yarn'; devCmd = 'yarn dev'; buildCmd = 'yarn build'; testCmd = 'yarn test'; lintCmd = 'yarn lint'; }
1017
- else if (pkgManager === 'bun') { installCmd = 'bun install'; devCmd = 'bun dev'; buildCmd = 'bun run build'; testCmd = 'bun test'; lintCmd = 'bun run lint'; }
1018
- else if (isPython) { installCmd = 'pip install -r requirements.txt'; devCmd = 'python manage.py runserver'; buildCmd = '# no build step'; testCmd = 'pytest'; lintCmd = 'ruff check .'; }
1019
- else if (isGo) { installCmd = 'go mod download'; devCmd = 'go run .'; buildCmd = 'go build ./...'; testCmd = 'go test ./...'; lintCmd = 'golangci-lint run'; }
1020
- else if (isRust) { installCmd = 'cargo build'; devCmd = 'cargo run'; buildCmd = 'cargo build --release'; testCmd = 'cargo test'; lintCmd = 'cargo clippy'; }
926
+ let install = 'npm install';
927
+ let dev = 'npm run dev';
928
+ let build = 'npm run build';
929
+ let test = 'npm test';
930
+ let lint = 'npm run lint';
931
+
932
+ if (pkgManager === 'pnpm') { install = 'pnpm install'; dev = 'pnpm dev'; build = 'pnpm build'; test = 'pnpm test'; lint = 'pnpm lint'; }
933
+ else if (pkgManager === 'yarn') { install = 'yarn'; dev = 'yarn dev'; build = 'yarn build'; test = 'yarn test'; lint = 'yarn lint'; }
934
+ else if (pkgManager === 'bun') { install = 'bun install'; dev = 'bun dev'; build = 'bun run build'; test = 'bun test'; lint = 'bun run lint'; }
935
+ else if (isPython) { install = 'pip install -r requirements.txt'; dev = 'python manage.py runserver'; build = '# no build step'; test = 'pytest'; lint = 'ruff check .'; }
936
+ else if (isGo) { install = 'go mod download'; dev = 'go run .'; build = 'go build ./...'; test = 'go test ./...'; lint = 'golangci-lint run'; }
937
+ else if (isRust) { install = 'cargo build'; dev = 'cargo run'; build = 'cargo build --release'; test = 'cargo test'; lint = 'cargo clippy'; }
1021
938
  else if (isPhp) {
1022
- installCmd = 'composer install';
1023
- buildCmd = '# no build step';
1024
- testCmd = testFramework === 'pest' ? './vendor/bin/pest' : './vendor/bin/phpunit';
939
+ install = 'composer install';
940
+ build = '# no build step';
941
+ test = testFramework === 'pest' ? './vendor/bin/pest' : './vendor/bin/phpunit';
1025
942
  const isCakePHP = frameworks.some(f => f.toLowerCase().includes('cakephp'));
1026
943
  const isSymfony = frameworks.some(f => f.toLowerCase().includes('symfony'));
1027
944
  const isCodeIgniter = frameworks.some(f => f.toLowerCase().includes('codeigniter'));
1028
945
  if (isCakePHP) {
1029
- devCmd = 'bin/cake server';
1030
- lintCmd = './vendor/bin/phpcs';
946
+ dev = 'bin/cake server';
947
+ lint = './vendor/bin/phpcs';
1031
948
  } else if (isSymfony) {
1032
- devCmd = 'bin/console server:run';
1033
- lintCmd = './vendor/bin/phpcs';
949
+ dev = 'bin/console server:run';
950
+ lint = './vendor/bin/phpcs';
1034
951
  } else if (isCodeIgniter) {
1035
- devCmd = 'php spark serve';
1036
- lintCmd = './vendor/bin/phpcs';
952
+ dev = 'php spark serve';
953
+ lint = './vendor/bin/phpcs';
1037
954
  } else {
1038
- // Default: Laravel
1039
- devCmd = 'php artisan serve';
1040
- lintCmd = 'php artisan pint';
955
+ dev = 'php artisan serve';
956
+ lint = 'php artisan pint';
1041
957
  }
1042
958
  }
1043
959
 
1044
- if (testFramework === 'vitest') testCmd = `${pkgManager === 'pnpm' ? 'pnpm' : pkgManager === 'yarn' ? 'yarn' : pkgManager === 'bun' ? 'bun run' : 'npx'} vitest`;
1045
- else if (testFramework === 'jest') testCmd = `${pkgManager === 'pnpm' ? 'pnpm' : pkgManager === 'yarn' ? 'yarn' : 'npx'} jest`;
1046
-
1047
- // Build stack overview section
1048
- const stackLines = [
1049
- `- **Language:** ${primaryLang}`,
1050
- frameworks.length > 0 ? `- **Frameworks:** ${frameworks.join(', ')}` : null,
1051
- database !== 'none' ? `- **Database:** ${database}` : null,
1052
- pkgManager !== 'auto-detect' ? `- **Package Manager:** ${pkgManager}` : null,
1053
- testFramework !== 'auto-detect' ? `- **Test Framework:** ${testFramework}` : null,
1054
- cloud !== 'none' ? `- **Cloud:** ${cloud}` : null,
1055
- infrastructure.length > 0 ? `- **Infrastructure:** ${infrastructure.join(', ')}` : null,
1056
- ].filter(Boolean).join('\n');
1057
-
1058
- // Build architecture notes hint
1059
- const archNotes = data.architecture_notes && !data.architecture_notes.includes('<!-- Add')
1060
- ? `\n${data.architecture_notes}`
1061
- : '';
1062
-
1063
- // Agent routing table
1064
- const agentTable = resolveAgentTable(data);
1065
- const agentRows = agentTable.map(([task, agent, skills]) => `| ${task} | \`${agent}\` | ${skills} |`).join('\n');
960
+ if (testFramework === 'vitest') test = `${pkgManager === 'pnpm' ? 'pnpm' : pkgManager === 'yarn' ? 'yarn' : pkgManager === 'bun' ? 'bun run' : 'npx'} vitest`;
961
+ else if (testFramework === 'jest') test = `${pkgManager === 'pnpm' ? 'pnpm' : pkgManager === 'yarn' ? 'yarn' : 'npx'} jest`;
1066
962
 
1067
- // Auto-resolved skills
1068
- const autoSkills = resolveSkills(data);
1069
- const skillsList = autoSkills.map(s => `- ${s}`).join('\n');
1070
-
1071
- return `# ${data.project_name}
1072
-
1073
- ${data.description}
1074
-
1075
- ## Project Overview
1076
-
1077
- ${stackLines}
1078
-
1079
- ---
1080
-
1081
- ## Core Architecture
1082
-
1083
- <!-- AI: Scan the project and replace this section with actual directory structure and key module descriptions -->
1084
- ${appRoot !== '.' ? `- **App Root:** \`${appRoot}\`` : ''}
1085
-
1086
- ---
1087
-
1088
- ## Development Workflow
1089
-
1090
- ### Prerequisites
1091
- - ${primaryLang.charAt(0).toUpperCase() + primaryLang.slice(1)} toolchain installed
1092
- ${pkgManager !== 'auto-detect' ? `- \`${pkgManager}\` (Package Manager)` : ''}
1093
-
1094
- ### Key Commands
1095
- - **Install Dependencies:** \`${installCmd}\`
1096
- - **Development Server:** \`${devCmd}\`
1097
- - **Build for Production:** \`${buildCmd}\`
1098
- - **Run Tests:** \`${testCmd}\`
1099
- - **Linting:** \`${lintCmd}\`
1100
-
1101
- ---
1102
-
1103
- ## Coding Conventions
1104
-
1105
- <!-- AI: Scan the project and replace this section with actual conventions (API patterns, state management, styling, naming, routing) -->
1106
-
1107
- ---
1108
-
1109
- ## Performance & Optimization
1110
-
1111
- <!-- AI: Scan vite.config / webpack / build config and replace with actual optimizations used -->
1112
-
1113
- ---
1114
-
1115
- ## Coding Rules
1116
-
1117
- **Don't assume. Don't hide confusion. Surface tradeoffs.**
1118
-
1119
- Before implementing:
1120
- - State your assumptions explicitly. If uncertain, ask.
1121
- - If multiple interpretations exist, present them — don't pick silently.
1122
- - If a simpler approach exists, say so. Push back when warranted.
1123
- - If something is unclear, stop. Name what's confusing. Ask.
1124
-
1125
- ### 1. Simplicity First
1126
-
1127
- **Minimum code that solves the problem. Nothing speculative.**
1128
-
1129
- - No features beyond what was asked.
1130
- - No abstractions for single-use code.
1131
- - No "flexibility" or "configurability" that wasn't requested.
1132
- - No error handling for impossible scenarios.
1133
- - If you write 200 lines and it could be 50, rewrite it.
1134
-
1135
- Ask yourself: "Would a senior engineer say this is overcomplicated?" If yes, simplify.
1136
-
1137
- ### 2. Surgical Changes
1138
-
1139
- **Touch only what you must. Clean up only your own mess.**
1140
-
1141
- When editing existing code:
1142
- - Don't "improve" adjacent code, comments, or formatting.
1143
- - Don't refactor things that aren't broken.
1144
- - Match existing style, even if you'd do it differently.
1145
- - If you notice unrelated dead code, mention it — don't delete it.
1146
-
1147
- When your changes create orphans:
1148
- - Remove imports/variables/functions that YOUR changes made unused.
1149
- - Don't remove pre-existing dead code unless asked.
1150
-
1151
- The test: Every changed line should trace directly to the user's request.
1152
-
1153
- ### 3. Goal-Driven Execution
1154
-
1155
- **Define success criteria. Loop until verified.**
1156
-
1157
- Transform tasks into verifiable goals:
1158
- - "Add validation" → "Write tests for invalid inputs, then make them pass"
1159
- - "Fix the bug" → "Write a test that reproduces it, then make it pass"
1160
- - "Refactor X" → "Ensure tests pass before and after"
1161
-
1162
- For multi-step tasks, state a brief plan:
1163
- \`\`\`
1164
- 1. [Step] → verify: [check]
1165
- 2. [Step] → verify: [check]
1166
- 3. [Step] → verify: [check]
1167
- \`\`\`
1168
-
1169
- Strong success criteria let you loop independently. Weak criteria ("make it work") require constant clarification.
1170
-
1171
- ### 4. Universal Standards
1172
-
1173
- - Respond in user's language; code/comments in English.
1174
- - \`clean-code\` applies to all code, all languages.
1175
- - Tests are mandatory.
1176
- - Verify with evidence before claiming done.
1177
- - Identify dependent files before modifying any.
1178
- - Keep files under ${maxFileLines} lines; split into modules if exceeded.
1179
- `;
963
+ return { install, dev, build, test, lint };
1180
964
  }
1181
965
 
1182
- function generateAgentsMdRefinementPrompt(data) {
1183
- const evidence = (data.scan_evidence || []).length > 0
1184
- ? (data.scan_evidence || []).map(line => `- ${line}`).join('\n')
1185
- : '- No scan evidence was captured during init.';
1186
-
1187
- return `# AGENTS.md Refinement Prompt
1188
-
1189
- Run this in your AI editor right after \`thachvd-kit init --scan\`.
1190
- The goal is to replace the placeholder sections in \`AGENTS.md\` with real project knowledge.
1191
-
1192
- ## Baseline Detected
1193
-
1194
- - **Project:** ${data.project_name}
1195
- - **Stack:** ${data.primary_language} | ${(data.frameworks || []).join(', ') || 'no framework'}
1196
- - **Database:** ${data.database || 'none'}
1197
- - **Package Manager:** ${data.package_manager || 'auto-detect'}
1198
-
1199
- ## Scan Evidence
1200
-
1201
- ${evidence}
1202
-
1203
- ## Your Task
1204
-
1205
- Update **only** the placeholder sections in \`AGENTS.md\` (marked with \`<!-- AI: ... --\`>):
1206
-
1207
- ### 1. Core Architecture
1208
- Scan the \`src/\` (or equivalent) directory. For each major folder, write one bullet:
1209
- \`- **\`src/folder/\`**: What it contains and its role\`
1210
- Keep it concise — one line per folder.
1211
-
1212
- ### 2. Coding Conventions
1213
- Inspect actual code patterns and write conventions grouped by topic:
1214
- - API integration patterns (how HTTP calls are made)
1215
- - State management patterns
1216
- - Styling conventions
1217
- - Component/file naming rules
1218
- - Routing and auth patterns
1219
-
1220
- ### 3. Performance & Optimization
1221
- Check \`vite.config.js\`, \`webpack.config.js\`, \`package.json\` scripts, or build configs. List actual optimizations:
1222
- - Code splitting, lazy loading, chunking
1223
- - Compression, caching strategies
1224
- - PWA, image optimization, etc.
1225
-
1226
- ## Rules
1227
-
1228
- - Replace placeholders with real content. Remove the \`<!-- AI: ... -->\` comments.
1229
- - Do NOT change \`## Project Overview\`, \`## Development Workflow\`, or \`## Coding Rules\` sections.
1230
- - Do NOT implement any product code.
1231
- - If a section is not applicable, write \`N/A\` and remove the placeholder.
1232
-
1233
- ## Files To Inspect
1234
-
1235
- - \`src/\` or \`app/\` directory structure
1236
- - Build config: \`vite.config.*\`, \`webpack.config.*\`, \`next.config.*\`
1237
- - Entry points: \`main.js\`, \`app.js\`, \`index.ts\`
1238
- - Router definition
1239
- - Example API service files
1240
- - Example store/state files
1241
- - README if present
1242
- `;
1243
- }
966
+ function generateProjectDoc(data) {
967
+ const commands = resolveCommands(data);
968
+ const agentRows = resolveAgentTable(data).map(([task, agent, skills]) => `| ${task} | \`${agent}\` | ${skills} |`).join('\n');
969
+ const skillsList = resolveSkills(data).map(s => `- ${s}`).join('\n');
970
+ const evidence = (data.scan_evidence || []).length > 0
971
+ ? (data.scan_evidence || []).map(line => `- ${line}`).join('\n')
972
+ : '- TODO: refine scan evidence by inspecting the project.';
1244
973
 
1245
- // --- Agent Folder Copy ---
1246
-
1247
- function copyAgentFolder() {
1248
- const srcAgent = path.join(sourceDir, '.agent');
1249
- if (!fs.existsSync(srcAgent)) return { copied: 0, skipped: 0 };
1250
-
1251
- const destAgent = path.join(targetDir, '.agent');
1252
- let copied = 0;
1253
- let skipped = 0;
1254
-
1255
- function copyDir(src, dest) {
1256
- fs.mkdirSync(dest, { recursive: true });
974
+ return `# Project Rules
975
+
976
+ Generated by thachvd-kit on ${new Date().toISOString().split('T')[0]}.
977
+
978
+ ## Summary
979
+
980
+ - Name: ${data.project_name}
981
+ - Description: ${data.description}
982
+ - Type: ${data.project_type}
983
+ - App root: ${data.app_root || '.'}
984
+
985
+ ## Stack
986
+
987
+ - Language: ${data.primary_language}
988
+ - Frameworks: ${(data.frameworks || []).join(', ') || 'none'}
989
+ - Database: ${data.database || 'none'}
990
+ - Infrastructure: ${(data.infrastructure || []).join(', ') || 'none'}
991
+ - Package manager: ${data.package_manager || 'auto-detect'}
992
+ - Test framework: ${data.test_framework || 'auto-detect'}
993
+
994
+ ## Commands
995
+
996
+ - Install: \`${commands.install}\`
997
+ - Dev: \`${commands.dev}\`
998
+ - Build: \`${commands.build}\`
999
+ - Test: \`${commands.test}\`
1000
+ - Lint: \`${commands.lint}\`
1001
+
1002
+ ## Agent Routing
1003
+
1004
+ | Task Type | Agent | Primary Skills |
1005
+ |-----------|-------|----------------|
1006
+ ${agentRows}
1007
+
1008
+ ## Auto-Resolved Skills
1009
+
1010
+ ${skillsList}
1011
+
1012
+ ## Scan Evidence
1013
+
1014
+ ${evidence}
1015
+ `;
1016
+ }
1017
+
1018
+ function generateArchitectureDoc(data) {
1019
+ const appRootLine = data.app_root && data.app_root !== '.'
1020
+ ? `- App root is currently detected as \`${data.app_root}\`.`
1021
+ : '- App root is the repository root unless refined below.';
1022
+ const evidence = (data.scan_evidence || []).length > 0
1023
+ ? (data.scan_evidence || []).map(line => `- ${line}`).join('\n')
1024
+ : '- TODO: refine architecture by scanning source directories.';
1025
+
1026
+ return `# Architecture Notes
1027
+
1028
+ ## Current Map
1029
+
1030
+ ${appRootLine}
1031
+ - TODO: refine major directories and responsibilities.
1032
+ - TODO: refine main entry points, routing, state boundaries, and integration boundaries.
1033
+
1034
+ ## Detected Evidence
1035
+
1036
+ ${evidence}
1037
+
1038
+ ## AI Maintenance Rule
1039
+
1040
+ Before non-trivial implementation work, inspect this file. If it is stale or still contains TODO items relevant to the task, update it from the code before editing product code.
1041
+ `;
1042
+ }
1043
+
1044
+ function generateConventionsDoc(data) {
1045
+ return `# Coding Conventions
1046
+
1047
+ ## Current Standards
1048
+
1049
+ - Maximum file length: ${data.max_file_lines || '300'} lines unless the existing project standard is stricter.
1050
+ - Code comments and identifiers should be written in English.
1051
+ - Keep changes scoped to the user request.
1052
+ - Prefer existing local patterns over introducing new abstractions.
1053
+ - TODO: refine naming, formatting, folder, API, state, styling, and testing conventions from the real codebase.
1054
+
1055
+ ## Verification
1056
+
1057
+ - Run the focused test or lint command that matches the touched area.
1058
+ - If no automated check exists, document the manual verification performed.
1059
+ - Do not claim completion without verification evidence.
1060
+ `;
1061
+ }
1062
+
1063
+ function generateWorkflowDoc() {
1064
+ return `# Agent Workflow
1065
+
1066
+ ## Before Every Task
1067
+
1068
+ 1. Read AGENTS.md.
1069
+ 2. Read .agent/docs/project.md.
1070
+ 3. Classify the request: question, survey, simple fix, feature, refactor, debug, UI, security, or deploy.
1071
+ 4. Read only the relevant docs under .agent/agents, .agent/skills, and .agent/workflows.
1072
+
1073
+ ## Implementation Flow
1074
+
1075
+ 1. State assumptions and success criteria when the task is not trivial.
1076
+ 2. Inspect dependent files before editing.
1077
+ 3. Make the smallest coherent change.
1078
+ 4. Add or update focused tests when behavior changes.
1079
+ 5. Run verification.
1080
+ 6. Summarize changed files and verification evidence.
1081
+
1082
+ ## When To Update Docs
1083
+
1084
+ - Update .agent/docs/architecture.md when structure, boundaries, or entry points change.
1085
+ - Update .agent/docs/conventions.md when repeated project patterns become clear.
1086
+ - Update .agent/docs/project.md when stack, scripts, app root, test tooling, or routing changes.
1087
+ - Keep AGENTS.md concise. Put project-specific detail in .agent/docs.
1088
+ `;
1089
+ }
1090
+
1091
+ // --- Agent Folder Copy ---
1092
+
1093
+ function copyAgentFolder() {
1094
+ const srcAgent = path.join(sourceDir, '.agent');
1095
+ if (!fs.existsSync(srcAgent)) return { copied: 0, skipped: 0 };
1096
+
1097
+ const destAgent = path.join(targetDir, '.agent');
1098
+ let copied = 0;
1099
+ let skipped = 0;
1100
+
1101
+ function copyDir(src, dest) {
1102
+ fs.mkdirSync(dest, { recursive: true });
1257
1103
  for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
1258
1104
  const srcPath = path.join(src, entry.name);
1259
1105
  const destPath = path.join(dest, entry.name);
1106
+ const relativeSrc = path.relative(srcAgent, srcPath).split(path.sep).join('/');
1107
+ if (relativeSrc === 'docs' || relativeSrc.startsWith('docs/')) {
1108
+ continue;
1109
+ }
1260
1110
  if (entry.isDirectory()) {
1261
1111
  copyDir(srcPath, destPath);
1262
1112
  } else {
1263
- if (fs.existsSync(destPath)) {
1264
- skipped++;
1265
- } else {
1266
- fs.copyFileSync(srcPath, destPath);
1267
- copied++;
1268
- }
1269
- }
1270
- }
1271
- }
1272
-
1273
- copyDir(srcAgent, destAgent);
1274
- return { copied, skipped };
1275
- }
1276
-
1277
- // --- Main CLI ---
1113
+ if (fs.existsSync(destPath)) {
1114
+ skipped++;
1115
+ } else {
1116
+ fs.copyFileSync(srcPath, destPath);
1117
+ copied++;
1118
+ }
1119
+ }
1120
+ }
1121
+ }
1278
1122
 
1279
- async function main() {
1280
- const rawArgs = process.argv.slice(2);
1281
- const command = rawArgs[0] && !rawArgs[0].startsWith('-') ? rawArgs[0] : 'init';
1282
- const args = command === 'init' ? rawArgs.slice(rawArgs[0] === 'init' ? 1 : 0) : rawArgs.slice(1);
1283
-
1284
- if (rawArgs.includes('--version') || rawArgs.includes('-v')) {
1285
- console.log(packageJson.version);
1286
- return;
1287
- }
1288
-
1289
- if (rawArgs.includes('--help') || rawArgs.includes('-h')) {
1290
- printHelp();
1291
- return;
1292
- }
1293
-
1294
- if (command !== 'init') {
1295
- console.error(pc.red(`Unknown command: ${command}`));
1296
- printHelp();
1297
- process.exit(1);
1298
- }
1299
-
1300
- let useScan = args.includes('--scan');
1301
- const assumeYes = args.includes('--yes') || args.includes('-y');
1302
-
1303
- console.log(pc.bold(pc.cyan('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')));
1304
- console.log(' ' + pc.bold('thachvd-kit') + ' — Project Init Wizard');
1305
- console.log(` Generates AGENTS.md + .agent/ folder${useScan ? ' (scan-assisted)' : ''}`);
1306
- console.log(pc.bold(pc.cyan('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')) + '\n');
1307
-
1308
- let projectSetup = 'new';
1309
- if (!useScan && !assumeYes) {
1310
- const modeAnswer = await askQuestions([
1311
- {
1312
- type: 'select',
1313
- name: 'project_setup',
1314
- message: 'Project already exists or are you starting fresh?',
1315
- choices: [
1316
- { title: 'Existing project', value: 'existing' },
1317
- { title: 'New project', value: 'new' }
1318
- ]
1319
- },
1320
- {
1321
- type: prev => prev === 'existing' ? 'select' : null,
1322
- name: 'existing_mode',
1323
- message: 'How should thachvd-kit build PROJECT_CONTEXT.md?',
1324
- choices: [
1325
- { title: 'Scan current project files', value: 'scan' },
1326
- { title: 'Enter context manually', value: 'manual' }
1327
- ],
1328
- initial: 0
1329
- }
1330
- ]);
1331
- projectSetup = modeAnswer.project_setup || 'new';
1332
- useScan = modeAnswer.existing_mode === 'scan';
1333
- }
1334
-
1335
- if (useScan) {
1336
- console.log(pc.dim(' Scan mode: detecting stack from current project files...\n'));
1337
- } else if (projectSetup === 'new') {
1338
- console.log(pc.dim(' Manual mode: asking for project context explicitly.\n'));
1339
- }
1340
-
1341
- if (fs.existsSync(path.join(targetDir, 'PROJECT_CONTEXT.md')) && !assumeYes) {
1342
- const res = await askQuestions({
1343
- type: 'confirm',
1344
- name: 'overwrite',
1345
- message: 'PROJECT_CONTEXT.md already exists in this folder. Overwrite?',
1123
+ copyDir(srcAgent, destAgent);
1124
+ return { copied, skipped };
1125
+ }
1126
+
1127
+ async function writeGeneratedFile(relativePath, content, assumeYes) {
1128
+ const outputPath = path.join(targetDir, relativePath);
1129
+ const exists = fs.existsSync(outputPath);
1130
+ if (exists && !assumeYes) {
1131
+ const res = await askQuestions({
1132
+ type: 'confirm',
1133
+ name: 'overwrite',
1134
+ message: `${relativePath} already exists. Overwrite?`,
1346
1135
  initial: false
1347
1136
  });
1348
- if (!res.overwrite) {
1349
- console.log(pc.yellow('\nAborted. Existing project context unchanged.'));
1350
- process.exit(0);
1351
- }
1352
- }
1353
-
1354
- const scanned = useScan ? scanProject() : null;
1355
- let reviewScannedFields = !useScan;
1356
- let requestAiRefinement = false;
1357
-
1358
- if (useScan) {
1359
- console.log(` ${pc.bold('Detected context:')}`);
1360
- formatScanPreviewLines(scanned).forEach(([label, value]) => {
1361
- console.log(` · ${label}: ${value}`);
1362
- });
1363
- console.log('');
1364
- if ((scanned.scan_evidence || []).length > 0) {
1365
- console.log(` ${pc.bold('Evidence:')}`);
1366
- scanned.scan_evidence.forEach(line => console.log(` · ${line}`));
1367
- console.log('');
1368
- }
1369
-
1370
- if (!assumeYes) {
1371
- const scanAction = await askQuestions({
1372
- type: 'select',
1373
- name: 'scan_action',
1374
- message: 'Use detected context as-is or review fields manually?',
1375
- choices: [
1376
- { title: 'Use detected context', value: 'accept' },
1377
- { title: 'Review detected fields', value: 'review' }
1378
- ],
1379
- initial: 0
1380
- });
1381
- reviewScannedFields = scanAction.scan_action === 'review';
1382
- }
1383
- }
1384
-
1385
- const { project_name, description, project_type } = await askQuestions([
1386
- { type: assumeYes || !reviewScannedFields ? null : 'text', name: 'project_name', message: 'Project name:', initial: scanned?.project_name || path.basename(targetDir) },
1387
- { type: assumeYes || !reviewScannedFields ? null : 'text', name: 'description', message: 'Short description:', initial: scanned?.description || 'A software project' },
1388
- {
1389
- type: assumeYes || !reviewScannedFields ? null : 'select', name: 'project_type', message: 'Project type:',
1390
- choices: PROJECT_TYPE_CHOICES.map(value => ({ title: value, value })),
1391
- initial: Math.max(PROJECT_TYPE_CHOICES.indexOf(scanned?.project_type || 'web-app'), 0)
1392
- }
1393
- ]);
1394
-
1395
- const { primary_language } = await askQuestions({
1396
- type: assumeYes || !reviewScannedFields ? null : 'select',
1397
- name: 'primary_language',
1398
- message: 'Primary language:',
1399
- choices: LANGUAGE_CHOICES.map(l => ({ title: l, value: l })),
1400
- initial: Math.max(LANGUAGE_CHOICES.indexOf(scanned?.primary_language || 'typescript'), 0)
1401
- });
1402
-
1403
- const { frameworks } = await askQuestions({
1404
- type: assumeYes || !reviewScannedFields ? null : 'multiselect',
1405
- name: 'frameworks',
1406
- message: 'Frameworks (Space to select, Enter to confirm):',
1407
- choices: FRAMEWORK_CHOICES.map(f => ({ title: f, value: f, selected: (scanned?.frameworks || []).includes(f) })),
1408
- min: 0
1409
- });
1410
-
1411
- const dbAndInfra = await askQuestions([
1412
- {
1413
- type: assumeYes || !reviewScannedFields ? null : 'select', name: 'database', message: 'Database / ORM:',
1414
- choices: DATABASE_CHOICES.map(d => ({ title: d, value: d })),
1415
- initial: Math.max(DATABASE_CHOICES.indexOf(scanned?.database || 'none'), 0)
1416
- },
1417
- { type: assumeYes || !reviewScannedFields ? null : 'confirm', name: 'uses_docker', message: 'Uses Docker / Docker Compose?', initial: scanned?.uses_docker ?? true },
1418
- {
1419
- type: assumeYes || !reviewScannedFields ? null : 'select', name: 'cloud', message: 'Cloud Provider:',
1420
- choices: CLOUD_CHOICES.map(c => ({ title: c, value: c })),
1421
- initial: Math.max(CLOUD_CHOICES.indexOf(scanned?.cloud || 'none'), 0)
1422
- }
1423
- ]);
1424
-
1425
- const { package_manager, test_framework, max_file_lines } = await askQuestions([
1426
- {
1427
- type: assumeYes || !reviewScannedFields ? null : 'select', name: 'package_manager', message: 'Package Manager:',
1428
- choices: PACKAGE_MANAGER_CHOICES.map(p => ({ title: p, value: p })),
1429
- initial: Math.max(PACKAGE_MANAGER_CHOICES.indexOf(scanned?.package_manager || 'auto-detect'), 0)
1430
- },
1431
- { type: assumeYes || !reviewScannedFields ? null : 'text', name: 'test_framework', message: 'Test Framework (jest, phpunit, etc):', initial: scanned?.test_framework || 'auto-detect' },
1432
- { type: assumeYes || !reviewScannedFields ? null : 'text', name: 'max_file_lines', message: 'Max lines per file:', initial: scanned?.max_file_lines || '300' }
1433
- ]);
1434
-
1435
- // Combine data
1436
- const data = {
1437
- project_name: project_name || scanned?.project_name || path.basename(targetDir),
1438
- description: description || scanned?.description || 'A software project',
1439
- project_type: project_type || scanned?.project_type || 'web-app',
1440
- primary_language: primary_language || scanned?.primary_language || 'typescript',
1441
- frameworks: frameworks || scanned?.frameworks || [],
1442
- ...dbAndInfra, package_manager, test_framework, max_file_lines
1443
- };
1444
-
1445
- data.database = data.database || scanned?.database || 'none';
1446
- data.uses_docker = typeof data.uses_docker === 'boolean' ? data.uses_docker : (scanned?.uses_docker ?? true);
1447
- data.cloud = data.cloud || scanned?.cloud || 'none';
1448
- data.package_manager = data.package_manager || scanned?.package_manager || 'auto-detect';
1449
- data.test_framework = data.test_framework || scanned?.test_framework || 'auto-detect';
1450
- data.max_file_lines = data.max_file_lines || scanned?.max_file_lines || '300';
1451
- data.framework_details = scanned?.framework_details || [];
1452
- data.scan_evidence = scanned?.scan_evidence || [];
1453
- data.app_root = scanned?.app_root || '.';
1454
-
1455
- data.infrastructure = [];
1456
- if (data.uses_docker) data.infrastructure.push('docker');
1457
- if (data.cloud !== 'none') data.infrastructure.push(data.cloud);
1458
-
1459
- console.log(`\n${pc.bold(pc.cyan('━━ Generating Files ━━'))}`);
1460
-
1461
- // Write AGENTS.md — the only output file
1462
- const agentsMdPath = path.join(targetDir, 'AGENTS.md');
1463
- const agentsMdExists = fs.existsSync(agentsMdPath);
1464
- if (agentsMdExists && !assumeYes) {
1465
- const res = await askQuestions({
1466
- type: 'confirm',
1467
- name: 'overwrite',
1468
- message: 'AGENTS.md already exists. Overwrite?',
1469
- initial: false
1470
- });
1471
- if (!res.overwrite) {
1472
- console.log(pc.yellow('\nAborted. Existing AGENTS.md unchanged.'));
1473
- process.exit(0);
1474
- }
1475
- }
1476
-
1477
- fs.writeFileSync(agentsMdPath, generateAgentsMd(data), 'utf8');
1478
- console.log(` ${pc.green('✓')} Generated ${pc.bold('AGENTS.md')}`);
1137
+ if (!res.overwrite) return false;
1138
+ }
1139
+
1140
+ fs.mkdirSync(path.dirname(outputPath), { recursive: true });
1141
+ fs.writeFileSync(outputPath, content, 'utf8');
1142
+ return true;
1143
+ }
1144
+
1145
+ // --- Main CLI ---
1146
+
1147
+ async function main() {
1148
+ const rawArgs = process.argv.slice(2);
1149
+ const command = rawArgs[0] && !rawArgs[0].startsWith('-') ? rawArgs[0] : 'init';
1150
+ const args = command === 'init' ? rawArgs.slice(rawArgs[0] === 'init' ? 1 : 0) : rawArgs.slice(1);
1151
+
1152
+ if (rawArgs.includes('--version') || rawArgs.includes('-v')) {
1153
+ console.log(packageJson.version);
1154
+ return;
1155
+ }
1156
+
1157
+ if (rawArgs.includes('--help') || rawArgs.includes('-h')) {
1158
+ printHelp();
1159
+ return;
1160
+ }
1161
+
1162
+ if (command !== 'init') {
1163
+ console.error(pc.red(`Unknown command: ${command}`));
1164
+ printHelp();
1165
+ process.exit(1);
1166
+ }
1167
+
1168
+ let useScan = args.includes('--scan');
1169
+ const assumeYes = args.includes('--yes') || args.includes('-y');
1170
+
1171
+ console.log(pc.bold(pc.cyan('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')));
1172
+ console.log(' ' + pc.bold('thachvd-kit') + ' — Project Init Wizard');
1173
+ console.log(` Generates AGENTS.md + .agent/ folder${useScan ? ' (scan-assisted)' : ''}`);
1174
+ console.log(pc.bold(pc.cyan('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')) + '\n');
1175
+
1176
+ let projectSetup = 'new';
1177
+ if (!useScan && !assumeYes) {
1178
+ const modeAnswer = await askQuestions([
1179
+ {
1180
+ type: 'select',
1181
+ name: 'project_setup',
1182
+ message: 'Project already exists or are you starting fresh?',
1183
+ choices: [
1184
+ { title: 'Existing project', value: 'existing' },
1185
+ { title: 'New project', value: 'new' }
1186
+ ]
1187
+ },
1188
+ {
1189
+ type: prev => prev === 'existing' ? 'select' : null,
1190
+ name: 'existing_mode',
1191
+ message: 'How should thachvd-kit build project rules?',
1192
+ choices: [
1193
+ { title: 'Scan current project files', value: 'scan' },
1194
+ { title: 'Enter context manually', value: 'manual' }
1195
+ ],
1196
+ initial: 0
1197
+ }
1198
+ ]);
1199
+ projectSetup = modeAnswer.project_setup || 'new';
1200
+ useScan = modeAnswer.existing_mode === 'scan';
1201
+ }
1202
+
1203
+ if (useScan) {
1204
+ console.log(pc.dim(' Scan mode: detecting stack from current project files...\n'));
1205
+ } else if (projectSetup === 'new') {
1206
+ console.log(pc.dim(' Manual mode: asking for project context explicitly.\n'));
1207
+ }
1208
+
1209
+ const scanned = useScan ? scanProject() : null;
1210
+ let reviewScannedFields = !useScan;
1211
+ let requestAiRefinement = false;
1212
+
1213
+ if (useScan) {
1214
+ console.log(` ${pc.bold('Detected context:')}`);
1215
+ formatScanPreviewLines(scanned).forEach(([label, value]) => {
1216
+ console.log(` · ${label}: ${value}`);
1217
+ });
1218
+ console.log('');
1219
+ if ((scanned.scan_evidence || []).length > 0) {
1220
+ console.log(` ${pc.bold('Evidence:')}`);
1221
+ scanned.scan_evidence.forEach(line => console.log(` · ${line}`));
1222
+ console.log('');
1223
+ }
1224
+
1225
+ if (!assumeYes) {
1226
+ const scanAction = await askQuestions({
1227
+ type: 'select',
1228
+ name: 'scan_action',
1229
+ message: 'Use detected context as-is or review fields manually?',
1230
+ choices: [
1231
+ { title: 'Use detected context', value: 'accept' },
1232
+ { title: 'Review detected fields', value: 'review' }
1233
+ ],
1234
+ initial: 0
1235
+ });
1236
+ reviewScannedFields = scanAction.scan_action === 'review';
1237
+ }
1238
+ }
1239
+
1240
+ const { project_name, description, project_type } = await askQuestions([
1241
+ { type: assumeYes || !reviewScannedFields ? null : 'text', name: 'project_name', message: 'Project name:', initial: scanned?.project_name || path.basename(targetDir) },
1242
+ { type: assumeYes || !reviewScannedFields ? null : 'text', name: 'description', message: 'Short description:', initial: scanned?.description || 'A software project' },
1243
+ {
1244
+ type: assumeYes || !reviewScannedFields ? null : 'select', name: 'project_type', message: 'Project type:',
1245
+ choices: PROJECT_TYPE_CHOICES.map(value => ({ title: value, value })),
1246
+ initial: Math.max(PROJECT_TYPE_CHOICES.indexOf(scanned?.project_type || 'web-app'), 0)
1247
+ }
1248
+ ]);
1249
+
1250
+ const { primary_language } = await askQuestions({
1251
+ type: assumeYes || !reviewScannedFields ? null : 'select',
1252
+ name: 'primary_language',
1253
+ message: 'Primary language:',
1254
+ choices: LANGUAGE_CHOICES.map(l => ({ title: l, value: l })),
1255
+ initial: Math.max(LANGUAGE_CHOICES.indexOf(scanned?.primary_language || 'typescript'), 0)
1256
+ });
1257
+
1258
+ const { frameworks } = await askQuestions({
1259
+ type: assumeYes || !reviewScannedFields ? null : 'multiselect',
1260
+ name: 'frameworks',
1261
+ message: 'Frameworks (Space to select, Enter to confirm):',
1262
+ choices: FRAMEWORK_CHOICES.map(f => ({ title: f, value: f, selected: (scanned?.frameworks || []).includes(f) })),
1263
+ min: 0
1264
+ });
1265
+
1266
+ const dbAndInfra = await askQuestions([
1267
+ {
1268
+ type: assumeYes || !reviewScannedFields ? null : 'select', name: 'database', message: 'Database / ORM:',
1269
+ choices: DATABASE_CHOICES.map(d => ({ title: d, value: d })),
1270
+ initial: Math.max(DATABASE_CHOICES.indexOf(scanned?.database || 'none'), 0)
1271
+ },
1272
+ { type: assumeYes || !reviewScannedFields ? null : 'confirm', name: 'uses_docker', message: 'Uses Docker / Docker Compose?', initial: scanned?.uses_docker ?? true },
1273
+ {
1274
+ type: assumeYes || !reviewScannedFields ? null : 'select', name: 'cloud', message: 'Cloud Provider:',
1275
+ choices: CLOUD_CHOICES.map(c => ({ title: c, value: c })),
1276
+ initial: Math.max(CLOUD_CHOICES.indexOf(scanned?.cloud || 'none'), 0)
1277
+ }
1278
+ ]);
1279
+
1280
+ const { package_manager, test_framework, max_file_lines } = await askQuestions([
1281
+ {
1282
+ type: assumeYes || !reviewScannedFields ? null : 'select', name: 'package_manager', message: 'Package Manager:',
1283
+ choices: PACKAGE_MANAGER_CHOICES.map(p => ({ title: p, value: p })),
1284
+ initial: Math.max(PACKAGE_MANAGER_CHOICES.indexOf(scanned?.package_manager || 'auto-detect'), 0)
1285
+ },
1286
+ { type: assumeYes || !reviewScannedFields ? null : 'text', name: 'test_framework', message: 'Test Framework (jest, phpunit, etc):', initial: scanned?.test_framework || 'auto-detect' },
1287
+ { type: assumeYes || !reviewScannedFields ? null : 'text', name: 'max_file_lines', message: 'Max lines per file:', initial: scanned?.max_file_lines || '300' }
1288
+ ]);
1289
+
1290
+ // Combine data
1291
+ const data = {
1292
+ project_name: project_name || scanned?.project_name || path.basename(targetDir),
1293
+ description: description || scanned?.description || 'A software project',
1294
+ project_type: project_type || scanned?.project_type || 'web-app',
1295
+ primary_language: primary_language || scanned?.primary_language || 'typescript',
1296
+ frameworks: frameworks || scanned?.frameworks || [],
1297
+ ...dbAndInfra, package_manager, test_framework, max_file_lines
1298
+ };
1299
+
1300
+ data.database = data.database || scanned?.database || 'none';
1301
+ data.uses_docker = typeof data.uses_docker === 'boolean' ? data.uses_docker : (scanned?.uses_docker ?? true);
1302
+ data.cloud = data.cloud || scanned?.cloud || 'none';
1303
+ data.package_manager = data.package_manager || scanned?.package_manager || 'auto-detect';
1304
+ data.test_framework = data.test_framework || scanned?.test_framework || 'auto-detect';
1305
+ data.max_file_lines = data.max_file_lines || scanned?.max_file_lines || '300';
1306
+ data.framework_details = scanned?.framework_details || [];
1307
+ data.scan_evidence = scanned?.scan_evidence || [];
1308
+ data.app_root = scanned?.app_root || '.';
1309
+
1310
+ data.infrastructure = [];
1311
+ if (data.uses_docker) data.infrastructure.push('docker');
1312
+ if (data.cloud !== 'none') data.infrastructure.push(data.cloud);
1313
+
1314
+ console.log(`\n${pc.bold(pc.cyan('Generating Files'))}`);
1315
+
1316
+ // Shared entry files are written after the .agent folder is available.
1479
1317
 
1480
1318
  // Copy .agent folder
1481
1319
  const { copied, skipped } = copyAgentFolder();
1482
1320
  if (copied > 0) {
1483
- console.log(` ${pc.green('✓')} Copied ${pc.bold('.agent/')} (${copied} file${copied !== 1 ? 's' : ''} added${skipped > 0 ? `, ${skipped} skipped` : ''})`);
1484
- } else if (skipped > 0) {
1485
- console.log(` ${pc.yellow('~')} ${pc.bold('.agent/')} already exists — ${skipped} file${skipped !== 1 ? 's' : ''} skipped (no overwrite)`);
1486
- }
1487
-
1488
- console.log(`\n${pc.bold(pc.green('━━ Done! ━━'))}
1489
-
1490
- ${pc.bold('Project:')} ${data.project_name}
1491
- ${pc.bold('Stack:')} ${data.primary_language} | ${(data.frameworks || []).join(', ') || 'no framework'}
1492
-
1493
- ${pc.bold('Generated:')}
1494
- · ${pc.bold('AGENTS.md')} — project context for AI agents
1495
- · ${pc.bold('.agent/')} — skills, workflows, agents, rules
1496
-
1321
+ console.log(` ${pc.green('OK')} Copied ${pc.bold('.agent/')} (${copied} file${copied !== 1 ? 's' : ''} added${skipped > 0 ? `, ${skipped} skipped` : ''})`);
1322
+ } else if (skipped > 0) {
1323
+ console.log(` ${pc.yellow('~')} ${pc.bold('.agent/')} already exists - ${skipped} file${skipped !== 1 ? 's' : ''} skipped (no overwrite)`);
1324
+ }
1325
+
1326
+
1327
+ const generatedFiles = [
1328
+ ['AGENTS.md', generateSharedAgentsMd(data)],
1329
+ ['CLAUDE.md', generateSharedClaudeMd()],
1330
+ ['GEMINI.md', generateSharedGeminiMd()],
1331
+ [path.join('.agent', 'docs', 'project.md'), generateProjectDoc(data)],
1332
+ [path.join('.agent', 'docs', 'architecture.md'), generateArchitectureDoc(data)],
1333
+ [path.join('.agent', 'docs', 'conventions.md'), generateConventionsDoc(data)],
1334
+ [path.join('.agent', 'docs', 'workflow.md'), generateWorkflowDoc()]
1335
+ ];
1336
+
1337
+ for (const [relativePath, content] of generatedFiles) {
1338
+ const wrote = await writeGeneratedFile(relativePath, content, assumeYes);
1339
+ if (wrote) console.log(` ${pc.green('OK')} Generated ${pc.bold(relativePath)}`);
1340
+ else console.log(` ${pc.yellow('~')} Skipped ${pc.bold(relativePath)}`);
1341
+ }
1342
+
1343
+ console.log(`\n${pc.bold(pc.green('Done!'))}
1344
+
1345
+ ${pc.bold('Project:')} ${data.project_name}
1346
+ ${pc.bold('Stack:')} ${data.primary_language} | ${(data.frameworks || []).join(', ') || 'no framework'}
1347
+
1348
+ ${pc.bold('Generated:')}
1349
+ - ${pc.bold('AGENTS.md')} shared entry for Codex, Antigravity, and Claude Code
1350
+ - ${pc.bold('CLAUDE.md')} Claude Code entry that imports AGENTS.md
1351
+ - ${pc.bold('GEMINI.md')} Antigravity entry
1352
+ - ${pc.bold('.agent/docs/')} scan-based project rules
1353
+ - ${pc.bold('.agent/')} skills, workflows, agents, and rules
1354
+
1497
1355
  ${pc.bold('Next steps:')}
1498
- 1. ${useScan ? `Open ${pc.bold('AGENTS.md')} — paste its content into your AI editor to fill in the placeholders` : `Edit ${pc.bold('AGENTS.md')} and fill in Core Architecture, Coding Conventions, Performance sections`}
1499
- 2. Commit ${pc.bold('AGENTS.md')} and ${pc.bold('.agent/')} to the repo
1500
- 3. Open project in Claude Code / Codex / Cursor / Antigravity — agent reads AGENTS.md automatically
1356
+ 1. Review ${pc.bold('.agent/docs/')} and replace any TODO: refine items with real project facts
1357
+ 2. Commit ${pc.bold('AGENTS.md')}, ${pc.bold('CLAUDE.md')}, ${pc.bold('GEMINI.md')}, and ${pc.bold('.agent/')}
1358
+ 3. Open the project in Codex, Antigravity, or Claude Code
1359
+
1360
+ ${pc.bold('Copy this prompt into your AI editor:')}
1361
+ ${pc.dim('---')}
1362
+ Read AGENTS.md and all files under .agent/docs/.
1363
+ Scan this repository.
1364
+ Update .agent/docs/project.md, .agent/docs/architecture.md, and .agent/docs/conventions.md with factual project-specific rules.
1365
+ Do not implement product code.
1366
+ Remove TODO: refine only when backed by evidence from the codebase.
1367
+ ${pc.dim('---')}
1501
1368
  `);
1502
1369
  }
1503
-
1504
- main().catch(err => {
1505
- console.error(pc.red('\nError during init:'), err);
1506
- process.exit(1);
1507
- });
1370
+
1371
+ main().catch(err => {
1372
+ console.error(pc.red('\nError during init:'), err);
1373
+ process.exit(1);
1374
+ });