thachvd-kit 1.0.33 → 1.0.34

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,1651 +1,1651 @@
1
- #!/usr/bin/env node
2
-
3
- const fs = require('fs');
4
- const path = require('path');
5
- const { spawnSync } = require('child_process');
6
- const prompts = require('prompts');
7
- const pc = require('picocolors');
8
- const packageJson = require('../package.json');
9
-
10
- const sourceDir = path.resolve(__dirname, '..');
11
- const targetDir = process.cwd();
12
- const CODEBASE_MEMORY_PACKAGE = 'codebase-memory-mcp';
13
- const CODEBASE_MEMORY_BINARY = 'codebase-memory-mcp';
14
- const RTK_INSTALL_COMMAND = 'cargo install --git https://github.com/rtk-ai/rtk --branch master rtk';
15
-
16
- const FRAMEWORK_CHOICES = [
17
- 'vue', 'react', 'next', 'nuxt', 'svelte', 'angular',
18
- 'laravel', 'cakephp', 'symfony', 'codeigniter',
19
- 'express', 'fastapi', 'django', 'flask', 'gin', 'fiber', 'echo', 'nestjs',
20
- 'rails', 'spring', 'aspnet',
21
- 'tauri', 'axum', 'actix', 'flutter', 'react-native'
22
- ];
23
-
24
- const LANGUAGE_CHOICES = ['typescript', 'javascript', 'python', 'go', 'php', 'rust', 'java', 'ruby', 'swift', 'kotlin', 'csharp', 'cpp'];
25
- const PROJECT_TYPE_CHOICES = ['web-app', 'api', 'mobile', 'desktop', 'cli', 'fullstack'];
26
- const DATABASE_CHOICES = ['postgres', 'mysql', 'sqlite', 'mongodb', 'redis', 'dynamodb', 'none'];
27
- const PACKAGE_MANAGER_CHOICES = ['npm', 'pnpm', 'yarn', 'bun', 'pip', 'uv', 'go mod', 'cargo', 'composer', 'auto-detect'];
28
- const TEST_FRAMEWORK_CHOICES = ['jest', 'vitest', 'pytest', 'go test', 'phpunit', 'pest', 'rspec', 'junit', 'auto-detect'];
29
- const CLOUD_CHOICES = ['aws', 'gcp', 'azure', 'vercel', 'fly', 'railway', 'none'];
30
- const FRONTEND_FRAMEWORKS = ['vue', 'react', 'next', 'nuxt', 'svelte', 'angular'];
31
- const BACKEND_FRAMEWORKS = ['laravel', 'cakephp', 'symfony', 'codeigniter', 'express', 'fastapi', 'django', 'flask', 'gin', 'fiber', 'echo', 'nestjs', 'rails', 'spring', 'aspnet', 'axum', 'actix'];
32
- const MOBILE_DESKTOP_FRAMEWORKS = ['tauri', 'flutter', 'react-native'];
33
- const IGNORED_SCAN_DIRS = new Set(['.git', 'node_modules', 'vendor', 'dist', 'build', 'coverage', '.next', '.nuxt', '.turbo', '.cache', 'storage', 'tmp']);
34
-
35
- function printHelp() {
36
- console.log(`
37
- ${pc.bold('Usage:')}
38
- thachvd-kit init [--yes]
39
- thachvd-kit setup [--yes]
40
- thachvd-kit doctor
41
- thachvd-kit prompt
42
- thachvd-kit --version
43
- thachvd-kit --help
44
-
45
- ${pc.bold('Commands:')}
46
- init Generate project context files and documentation
47
- setup Install/configure MCP and recommended local tooling
48
- doctor Check project context and local integrations
49
- prompt Print the saved project-indexing prompt again
50
-
51
- ${pc.bold('Options:')}
52
- --yes Overwrite generated files or accept setup defaults
53
- --no-setup-mcp Skip MCP setup (legacy init compatibility)
54
- --no-setup-hook Skip the Claude Code Definition-of-Done Stop hook during setup
55
- --no-install-rtk Skip auto-installing rtk via cargo during setup
56
- --no-index Skip auto-indexing the repository with codebase-memory-mcp during setup
57
- --version Show the installed CLI version
58
- --help Show this help message
59
-
60
- ${pc.bold('What gets generated:')}
61
- AGENTS.md Shared instructions for Codex, Antigravity, Claude Code, and Cursor
62
- CLAUDE.md Claude Code entry file that imports AGENTS.md
63
- GEMINI.md Antigravity entry file that points to AGENTS.md
64
- .cursorrules Cursor entry file that points to AGENTS.md
65
- .agent/docs/ Project context, architecture, workflow, tooling, and durable onboarding prompt
66
-
67
- ${pc.bold('Workflow guide:')}
68
- Every task Superpowers Use the matching native Superpowers skill
69
- Idea is fuzzy brainstorming Clarify intent and tradeoffs before coding
70
- Scope needs a contract writing-plans Create a detailed plan before implementation
71
- Execute an approved plan executing-plans Follow the approved plan with verification
72
- Bug or failing behavior systematic-debugging Find the root cause before editing
73
- Pre-merge review requesting-code-review Review before completion or merge
74
- UI / UX work $frontend-design, $webapp-testing
75
- Project state thachvd-kit doctor
76
-
77
- ${pc.bold('Tip:')}
78
- Install Superpowers separately for each AI client from its official plugin surface.
79
- Run thachvd-kit setup for the integration checklist and doctor to verify it.
80
- `);
81
- }
82
-
83
- function formatScanPreviewLines(scanned) {
84
- return [
85
- ['App root', scanned.app_root || '.'],
86
- ['Project type', scanned.project_type || 'unknown'],
87
- ['Primary language', scanned.primary_language || 'unknown'],
88
- ['Frameworks', (scanned.frameworks || []).join(', ') || 'no framework'],
89
- ['Database', scanned.database || 'none'],
90
- ['Package manager', scanned.package_manager || 'auto-detect'],
91
- ['Test framework', scanned.test_framework || 'auto-detect'],
92
- ['Cloud', scanned.cloud || 'none'],
93
- ['Infrastructure', (scanned.infrastructure || []).join(', ') || 'none'],
94
- ['CI/CD', scanned.ci_cd || 'none']
95
- ];
96
- }
97
-
98
- async function askQuestions(questions) {
99
- return prompts(questions, {
100
- onCancel: () => {
101
- console.log(pc.yellow('\nCancelled.'));
102
- process.exit(130);
103
- }
104
- });
105
- }
106
-
107
- function fileExists(...parts) {
108
- return fs.existsSync(path.join(targetDir, ...parts));
109
- }
110
-
111
- function fileExistsAt(baseDir, ...parts) {
112
- return fs.existsSync(path.join(baseDir, ...parts));
113
- }
114
-
115
- function readJsonIfExists(...parts) {
116
- const fullPath = path.join(targetDir, ...parts);
117
- if (!fs.existsSync(fullPath)) return null;
118
- try {
119
- return JSON.parse(fs.readFileSync(fullPath, 'utf8'));
120
- } catch {
121
- return null;
122
- }
123
- }
124
-
125
- function readJsonIfExistsAt(baseDir, ...parts) {
126
- const fullPath = path.join(baseDir, ...parts);
127
- if (!fs.existsSync(fullPath)) return null;
128
- try {
129
- return JSON.parse(fs.readFileSync(fullPath, 'utf8'));
130
- } catch {
131
- return null;
132
- }
133
- }
134
-
135
- function readTextIfExists(...parts) {
136
- const fullPath = path.join(targetDir, ...parts);
137
- if (!fs.existsSync(fullPath)) return '';
138
- try {
139
- return fs.readFileSync(fullPath, 'utf8');
140
- } catch {
141
- return '';
142
- }
143
- }
144
-
145
- function readTextIfExistsAt(baseDir, ...parts) {
146
- const fullPath = path.join(baseDir, ...parts);
147
- if (!fs.existsSync(fullPath)) return '';
148
- try {
149
- return fs.readFileSync(fullPath, 'utf8');
150
- } catch {
151
- return '';
152
- }
153
- }
154
-
155
- function detectPackageManager(baseDir = targetDir) {
156
- if (fileExistsAt(baseDir, 'pnpm-lock.yaml')) return 'pnpm';
157
- if (fileExistsAt(baseDir, 'yarn.lock')) return 'yarn';
158
- if (fileExistsAt(baseDir, 'bun.lockb') || fileExistsAt(baseDir, 'bun.lock')) return 'bun';
159
- if (fileExistsAt(baseDir, 'package-lock.json')) return 'npm';
160
- if (fileExistsAt(baseDir, 'uv.lock')) return 'uv';
161
- if (fileExistsAt(baseDir, 'poetry.lock')) return 'pip';
162
- if (fileExistsAt(baseDir, 'requirements.txt') || fileExistsAt(baseDir, 'pyproject.toml')) return 'pip';
163
- if (fileExistsAt(baseDir, 'go.mod')) return 'go mod';
164
- if (fileExistsAt(baseDir, 'Cargo.toml')) return 'cargo';
165
- if (fileExistsAt(baseDir, 'composer.json')) return 'composer';
166
- return 'auto-detect';
167
- }
168
-
169
- function uniq(values) {
170
- return Array.from(new Set(values.filter(Boolean)));
171
- }
172
-
173
- function detectAppRoot(baseDir) {
174
- const candidates = [baseDir];
175
- const queue = [{ dir: baseDir, depth: 0 }];
176
-
177
- while (queue.length > 0) {
178
- const { dir, depth } = queue.shift();
179
- if (depth >= 2) continue;
180
- let entries = [];
181
- try {
182
- entries = fs.readdirSync(dir, { withFileTypes: true });
183
- } catch {
184
- continue;
185
- }
186
- for (const entry of entries) {
187
- if (!entry.isDirectory()) continue;
188
- if (IGNORED_SCAN_DIRS.has(entry.name)) continue;
189
- const nextDir = path.join(dir, entry.name);
190
- candidates.push(nextDir);
191
- queue.push({ dir: nextDir, depth: depth + 1 });
192
- }
193
- }
194
-
195
- const scoreCandidate = dir => {
196
- let score = 0;
197
- if (fileExistsAt(dir, 'composer.json')) score += 5;
198
- if (fileExistsAt(dir, 'package.json')) score += 4;
199
- if (fileExistsAt(dir, 'Cargo.toml')) score += 4;
200
- if (fileExistsAt(dir, 'go.mod')) score += 4;
201
- if (fileExistsAt(dir, 'pyproject.toml') || fileExistsAt(dir, 'requirements.txt')) score += 4;
202
- if (fileExistsAt(dir, 'artisan')) score += 5;
203
- if (fileExistsAt(dir, 'bin', 'cake') || fileExistsAt(dir, 'app', 'Config', 'core.php')) score += 5;
204
- if (fileExistsAt(dir, 'src-tauri', 'tauri.conf.json') || fileExistsAt(dir, 'src-tauri', 'tauri.conf.json5')) score += 5;
205
- if (fileExistsAt(dir, 'manage.py')) score += 4;
206
- if (fileExistsAt(dir, 'Gemfile')) score += 4;
207
- if (fileExistsAt(dir, 'pom.xml') || fileExistsAt(dir, 'build.gradle') || fileExistsAt(dir, 'build.gradle.kts')) score += 4;
208
- if (fileExistsAt(dir, 'app', 'View') || fileExistsAt(dir, 'resources', 'views') || fileExistsAt(dir, 'templates') || fileExistsAt(dir, 'app', 'views')) score += 2;
209
- return score;
210
- };
211
-
212
- const ranked = candidates
213
- .map(dir => ({ dir, score: scoreCandidate(dir) }))
214
- .filter(item => item.score > 0)
215
- .sort((a, b) => (b.score - a.score) || (a.dir.length - b.dir.length));
216
-
217
- return ranked[0]?.dir || baseDir;
218
- }
219
-
220
- function repoHasAny(baseDir, relativePaths, maxDepth = 3) {
221
- const queue = [{ dir: baseDir, depth: 0 }];
222
- while (queue.length > 0) {
223
- const { dir, depth } = queue.shift();
224
- for (const relativePath of relativePaths) {
225
- if (fileExistsAt(dir, ...relativePath)) return true;
226
- }
227
- if (depth >= maxDepth) continue;
228
- let entries = [];
229
- try {
230
- entries = fs.readdirSync(dir, { withFileTypes: true });
231
- } catch {
232
- continue;
233
- }
234
- for (const entry of entries) {
235
- if (!entry.isDirectory()) continue;
236
- if (IGNORED_SCAN_DIRS.has(entry.name)) continue;
237
- queue.push({ dir: path.join(dir, entry.name), depth: depth + 1 });
238
- }
239
- }
240
- return false;
241
- }
242
-
243
- function evidenceIf(condition, label, weight = 1) {
244
- return condition ? { label, weight } : null;
245
- }
246
-
247
- function buildDetection(id, evidence, meta = {}) {
248
- const hits = evidence.filter(Boolean);
249
- const score = hits.reduce((sum, hit) => sum + hit.weight, 0);
250
- return {
251
- id,
252
- score,
253
- evidence: hits.map(hit => hit.label),
254
- ...meta
255
- };
256
- }
257
-
258
- function confidenceLabel(score) {
259
- if (score >= 6) return 'high';
260
- if (score >= 3) return 'medium';
261
- return 'low';
262
- }
263
-
264
- function hasDependency(depNames, ...names) {
265
- return names.some(name => depNames.has(name));
266
- }
267
-
268
- function hasComposerDependency(composerDepNames, ...names) {
269
- return names.some(name => composerDepNames.has(name));
270
- }
271
-
272
- function hasRootFile(rootEntries, ...filenames) {
273
- return filenames.some(name => rootEntries.has(name));
274
- }
275
-
276
- function textMatches(text, pattern) {
277
- return Boolean(text && pattern.test(text));
278
- }
279
-
280
- const LANGUAGE_DETECTORS = [
281
- {
282
- id: 'typescript',
283
- detect: ctx => buildDetection('typescript', [
284
- evidenceIf(ctx.fileExists('tsconfig.json'), 'found tsconfig.json', 4),
285
- evidenceIf(hasDependency(ctx.depNames, 'typescript', 'ts-node', 'tsx'), 'package.json -> typescript toolchain', 2)
286
- ])
287
- },
288
- {
289
- id: 'javascript',
290
- detect: ctx => buildDetection('javascript', [
291
- evidenceIf(ctx.fileExists('package.json'), 'found package.json', 2),
292
- evidenceIf(ctx.depNames.size > 0, 'package.json -> JavaScript dependency graph', 1)
293
- ])
294
- },
295
- {
296
- id: 'python',
297
- detect: ctx => buildDetection('python', [
298
- evidenceIf(ctx.fileExists('requirements.txt'), 'found requirements.txt', 3),
299
- evidenceIf(ctx.fileExists('pyproject.toml'), 'found pyproject.toml', 2),
300
- evidenceIf(textMatches(ctx.pyprojectText, /\[tool\.(poetry|uv|hatch|pdm)\]/i), 'pyproject.toml -> Python tool section', 2)
301
- ])
302
- },
303
- {
304
- id: 'go',
305
- detect: ctx => buildDetection('go', [
306
- evidenceIf(ctx.fileExists('go.mod'), 'found go.mod', 4)
307
- ])
308
- },
309
- {
310
- id: 'php',
311
- detect: ctx => buildDetection('php', [
312
- evidenceIf(ctx.fileExists('composer.json'), 'found composer.json', 4),
313
- evidenceIf(ctx.composerDepNames.size > 0, 'composer.json -> PHP dependency graph', 1)
314
- ])
315
- },
316
- {
317
- id: 'rust',
318
- detect: ctx => buildDetection('rust', [
319
- evidenceIf(ctx.fileExists('Cargo.toml'), 'found Cargo.toml', 4),
320
- evidenceIf(ctx.fileExists('src-tauri', 'Cargo.toml'), 'found src-tauri/Cargo.toml', 5)
321
- ])
322
- },
323
- {
324
- id: 'java',
325
- detect: ctx => buildDetection('java', [
326
- evidenceIf(ctx.fileExists('pom.xml'), 'found pom.xml', 4),
327
- evidenceIf(ctx.fileExists('build.gradle') || ctx.fileExists('build.gradle.kts'), 'found Gradle build file', 3)
328
- ])
329
- },
330
- {
331
- id: 'ruby',
332
- detect: ctx => buildDetection('ruby', [
333
- evidenceIf(ctx.fileExists('Gemfile'), 'found Gemfile', 4),
334
- evidenceIf(ctx.fileExists('config.ru'), 'found config.ru', 1)
335
- ])
336
- },
337
- {
338
- id: 'csharp',
339
- detect: ctx => buildDetection('csharp', [
340
- evidenceIf(ctx.rootEntries.some(name => name.endsWith('.sln') || name.endsWith('.csproj')), 'found .sln/.csproj', 4),
341
- evidenceIf(ctx.fileExists('Program.cs'), 'found Program.cs', 1)
342
- ])
343
- }
344
- ];
345
-
346
- const FRAMEWORK_DETECTORS = [
347
- {
348
- id: 'next',
349
- kind: 'frontend',
350
- projectType: 'web-app',
351
- detect: ctx => buildDetection('next', [
352
- evidenceIf(hasDependency(ctx.depNames, 'next'), 'package.json -> next', 4),
353
- evidenceIf(ctx.fileExists('next.config.js') || ctx.fileExists('next.config.mjs') || ctx.fileExists('next.config.ts'), 'found next.config.*', 3),
354
- evidenceIf(ctx.fileExists('app') || ctx.fileExists('pages'), 'found app/ or pages/', 1)
355
- ])
356
- },
357
- {
358
- id: 'react',
359
- kind: 'frontend',
360
- projectType: 'web-app',
361
- detect: ctx => buildDetection('react', [
362
- evidenceIf(hasDependency(ctx.depNames, 'react'), 'package.json -> react', 4),
363
- evidenceIf(hasDependency(ctx.depNames, 'react-dom'), 'package.json -> react-dom', 2)
364
- ])
365
- },
366
- {
367
- id: 'vue',
368
- kind: 'frontend',
369
- projectType: 'web-app',
370
- detect: ctx => buildDetection('vue', [
371
- evidenceIf(hasDependency(ctx.depNames, 'vue'), 'package.json -> vue', 4)
372
- ])
373
- },
374
- {
375
- id: 'nuxt',
376
- kind: 'frontend',
377
- projectType: 'web-app',
378
- detect: ctx => buildDetection('nuxt', [
379
- evidenceIf(hasDependency(ctx.depNames, 'nuxt'), 'package.json -> nuxt', 4),
380
- evidenceIf(ctx.fileExists('nuxt.config.ts') || ctx.fileExists('nuxt.config.js'), 'found nuxt.config.*', 3)
381
- ])
382
- },
383
- {
384
- id: 'svelte',
385
- kind: 'frontend',
386
- projectType: 'web-app',
387
- detect: ctx => buildDetection('svelte', [
388
- evidenceIf(hasDependency(ctx.depNames, 'svelte', '@sveltejs/kit'), 'package.json -> svelte', 4)
389
- ])
390
- },
391
- {
392
- id: 'angular',
393
- kind: 'frontend',
394
- projectType: 'web-app',
395
- detect: ctx => buildDetection('angular', [
396
- evidenceIf(hasDependency(ctx.depNames, '@angular/core'), 'package.json -> @angular/core', 4),
397
- evidenceIf(ctx.fileExists('angular.json'), 'found angular.json', 3)
398
- ])
399
- },
400
- {
401
- id: 'express',
402
- kind: 'backend',
403
- language: 'javascript',
404
- projectType: 'api',
405
- detect: ctx => buildDetection('express', [
406
- evidenceIf(hasDependency(ctx.depNames, 'express'), 'package.json -> express', 4)
407
- ])
408
- },
409
- {
410
- id: 'nestjs',
411
- kind: 'backend',
412
- language: 'javascript',
413
- projectType: 'api',
414
- detect: ctx => buildDetection('nestjs', [
415
- evidenceIf(hasDependency(ctx.depNames, '@nestjs/core'), 'package.json -> @nestjs/core', 4)
416
- ])
417
- },
418
- {
419
- id: 'fastapi',
420
- kind: 'backend',
421
- language: 'python',
422
- projectType: 'api',
423
- detect: ctx => buildDetection('fastapi', [
424
- evidenceIf(textMatches(ctx.requirementsText, /\bfastapi\b/i) || textMatches(ctx.pyprojectText, /\bfastapi\b/i), 'Python deps -> fastapi', 4)
425
- ])
426
- },
427
- {
428
- id: 'django',
429
- kind: 'backend',
430
- language: 'python',
431
- projectType: 'web-app',
432
- detect: ctx => buildDetection('django', [
433
- evidenceIf(textMatches(ctx.requirementsText, /\bdjango\b/i) || textMatches(ctx.pyprojectText, /\bdjango\b/i), 'Python deps -> django', 4),
434
- evidenceIf(ctx.fileExists('manage.py'), 'found manage.py', 2),
435
- evidenceIf(ctx.fileExists('templates'), 'found templates/', 1)
436
- ])
437
- },
438
- {
439
- id: 'flask',
440
- kind: 'backend',
441
- language: 'python',
442
- projectType: 'api',
443
- detect: ctx => buildDetection('flask', [
444
- evidenceIf(textMatches(ctx.requirementsText, /\bflask\b/i) || textMatches(ctx.pyprojectText, /\bflask\b/i), 'Python deps -> flask', 4)
445
- ])
446
- },
447
- {
448
- id: 'laravel',
449
- kind: 'backend',
450
- language: 'php',
451
- projectType: 'web-app',
452
- detect: ctx => buildDetection('laravel', [
453
- evidenceIf(hasComposerDependency(ctx.composerDepNames, 'laravel/framework'), 'composer.json -> laravel/framework', 4),
454
- evidenceIf(ctx.fileExists('artisan'), 'found artisan', 3),
455
- evidenceIf(ctx.fileExists('resources', 'views'), 'found resources/views', 1)
456
- ])
457
- },
458
- {
459
- id: 'cakephp',
460
- kind: 'backend',
461
- language: 'php',
462
- projectType: 'web-app',
463
- detect: ctx => buildDetection('cakephp', [
464
- evidenceIf(hasComposerDependency(ctx.composerDepNames, 'cakephp/cakephp'), 'composer.json -> cakephp/cakephp', 4),
465
- evidenceIf(ctx.fileExists('bin', 'cake'), 'found bin/cake', 3),
466
- evidenceIf(ctx.fileExists('config', 'app.php'), 'found config/app.php', 2),
467
- evidenceIf(ctx.fileExists('app', 'Config', 'core.php'), 'found app/Config/core.php (CakePHP legacy)', 3),
468
- evidenceIf(ctx.fileExists('app', 'Controller', 'AppController.php'), 'found app/Controller/AppController.php', 2),
469
- evidenceIf(ctx.fileExists('app', 'Model', 'AppModel.php'), 'found app/Model/AppModel.php', 2),
470
- evidenceIf(ctx.fileExists('lib', 'Cake'), 'found lib/Cake', 3),
471
- evidenceIf(ctx.fileExists('cake', 'console', 'cake'), 'found cake/console/cake', 3),
472
- evidenceIf(ctx.fileExists('app', 'View'), 'found app/View', 2),
473
- evidenceIf(textMatches(ctx.viewFilesText, /\.ctp$/im), 'found .ctp view templates', 2)
474
- ])
475
- },
476
- {
477
- id: 'symfony',
478
- kind: 'backend',
479
- language: 'php',
480
- projectType: 'web-app',
481
- detect: ctx => buildDetection('symfony', [
482
- evidenceIf(hasComposerDependency(ctx.composerDepNames, 'symfony/framework-bundle'), 'composer.json -> symfony/framework-bundle', 4),
483
- evidenceIf(ctx.fileExists('bin', 'console'), 'found bin/console', 2),
484
- evidenceIf(ctx.fileExists('config', 'bundles.php'), 'found config/bundles.php', 1),
485
- evidenceIf(ctx.fileExists('templates'), 'found templates/', 1)
486
- ])
487
- },
488
- {
489
- id: 'codeigniter',
490
- kind: 'backend',
491
- language: 'php',
492
- projectType: 'api',
493
- detect: ctx => buildDetection('codeigniter', [
494
- evidenceIf(hasComposerDependency(ctx.composerDepNames, 'codeigniter4/framework'), 'composer.json -> codeigniter4/framework', 4),
495
- evidenceIf(ctx.fileExists('spark'), 'found spark', 2),
496
- evidenceIf(ctx.fileExists('app', 'Config', 'App.php'), 'found app/Config/App.php', 1)
497
- ])
498
- },
499
- {
500
- id: 'gin',
501
- kind: 'backend',
502
- language: 'go',
503
- projectType: 'api',
504
- detect: ctx => buildDetection('gin', [
505
- evidenceIf(textMatches(ctx.goModText, /\bgithub\.com\/gin-gonic\/gin\b/i), 'go.mod -> github.com/gin-gonic/gin', 4)
506
- ])
507
- },
508
- {
509
- id: 'fiber',
510
- kind: 'backend',
511
- language: 'go',
512
- projectType: 'api',
513
- detect: ctx => buildDetection('fiber', [
514
- evidenceIf(textMatches(ctx.goModText, /\bgithub\.com\/gofiber\/fiber\b/i), 'go.mod -> github.com/gofiber/fiber', 4)
515
- ])
516
- },
517
- {
518
- id: 'echo',
519
- kind: 'backend',
520
- language: 'go',
521
- projectType: 'api',
522
- detect: ctx => buildDetection('echo', [
523
- evidenceIf(textMatches(ctx.goModText, /\bgithub\.com\/labstack\/echo\b/i), 'go.mod -> github.com/labstack/echo', 4)
524
- ])
525
- },
526
- {
527
- id: 'rails',
528
- kind: 'backend',
529
- language: 'ruby',
530
- projectType: 'web-app',
531
- detect: ctx => buildDetection('rails', [
532
- evidenceIf(textMatches(ctx.gemfileText, /^\s*gem\s+["']rails["']/im), 'Gemfile -> rails', 4),
533
- evidenceIf(ctx.fileExists('bin', 'rails'), 'found bin/rails', 2),
534
- evidenceIf(ctx.fileExists('app', 'views'), 'found app/views', 1)
535
- ])
536
- },
537
- {
538
- id: 'spring',
539
- kind: 'backend',
540
- language: 'java',
541
- projectType: 'api',
542
- detect: ctx => buildDetection('spring', [
543
- evidenceIf(textMatches(ctx.pomText, /spring-boot/i) || textMatches(ctx.gradleText, /spring-boot/i), 'Java build -> spring-boot', 4)
544
- ])
545
- },
546
- {
547
- id: 'aspnet',
548
- kind: 'backend',
549
- language: 'csharp',
550
- projectType: 'api',
551
- detect: ctx => buildDetection('aspnet', [
552
- evidenceIf(textMatches(ctx.csprojText, /Microsoft\.NET\.Sdk\.Web/i), '.csproj -> Microsoft.NET.Sdk.Web', 4),
553
- evidenceIf(textMatches(ctx.programCsText, /WebApplication\.CreateBuilder/i), 'Program.cs -> WebApplication.CreateBuilder', 2)
554
- ])
555
- },
556
- {
557
- id: 'tauri',
558
- kind: 'desktop',
559
- language: 'rust',
560
- projectType: 'desktop',
561
- detect: ctx => buildDetection('tauri', [
562
- evidenceIf(hasDependency(ctx.depNames, '@tauri-apps/api'), 'package.json -> @tauri-apps/api', 4),
563
- evidenceIf(ctx.fileExists('src-tauri', 'tauri.conf.json') || ctx.fileExists('src-tauri', 'tauri.conf.json5'), 'found src-tauri/tauri.conf.*', 4),
564
- evidenceIf(ctx.fileExists('src-tauri', 'Cargo.toml'), 'found src-tauri/Cargo.toml', 2)
565
- ])
566
- },
567
- {
568
- id: 'axum',
569
- kind: 'backend',
570
- language: 'rust',
571
- projectType: 'api',
572
- detect: ctx => buildDetection('axum', [
573
- evidenceIf(textMatches(ctx.cargoText, /\baxum\b/i), 'Cargo.toml -> axum', 4)
574
- ])
575
- },
576
- {
577
- id: 'actix',
578
- kind: 'backend',
579
- language: 'rust',
580
- projectType: 'api',
581
- detect: ctx => buildDetection('actix', [
582
- evidenceIf(textMatches(ctx.cargoText, /\bactix-web\b/i), 'Cargo.toml -> actix-web', 4)
583
- ])
584
- },
585
- {
586
- id: 'flutter',
587
- kind: 'mobile',
588
- projectType: 'mobile',
589
- detect: ctx => buildDetection('flutter', [
590
- evidenceIf(textMatches(ctx.pubspecText, /^\s*flutter\s*:/im), 'pubspec.yaml -> flutter', 4)
591
- ])
592
- },
593
- {
594
- id: 'react-native',
595
- kind: 'mobile',
596
- projectType: 'mobile',
597
- detect: ctx => buildDetection('react-native', [
598
- evidenceIf(hasDependency(ctx.depNames, 'react-native', 'expo'), 'package.json -> react-native/expo', 4)
599
- ])
600
- }
601
- ];
602
-
603
- function scanProject() {
604
- const appRoot = detectAppRoot(targetDir);
605
- const pkg = readJsonIfExistsAt(appRoot, 'package.json') || {};
606
- const composer = readJsonIfExistsAt(appRoot, 'composer.json') || {};
607
- const rootEntries = fs.existsSync(appRoot) ? fs.readdirSync(appRoot) : [];
608
- const rootCsproj = rootEntries.find(name => name.endsWith('.csproj')) || '';
609
- const deps = {
610
- ...(pkg.dependencies || {}),
611
- ...(pkg.devDependencies || {}),
612
- ...(pkg.peerDependencies || {})
613
- };
614
- const depNames = new Set(Object.keys(deps));
615
- const composerDeps = {
616
- ...(composer.require || {}),
617
- ...(composer['require-dev'] || {})
618
- };
619
- const composerDepNames = new Set(Object.keys(composerDeps));
620
- const ctx = {
621
- pkg,
622
- composer,
623
- depNames,
624
- composerDepNames,
625
- rootEntries,
626
- fileExists: (...parts) => fileExistsAt(appRoot, ...parts),
627
- requirementsText: readTextIfExistsAt(appRoot, 'requirements.txt'),
628
- pyprojectText: readTextIfExistsAt(appRoot, 'pyproject.toml'),
629
- goModText: readTextIfExistsAt(appRoot, 'go.mod'),
630
- pubspecText: readTextIfExistsAt(appRoot, 'pubspec.yaml'),
631
- gemfileText: readTextIfExistsAt(appRoot, 'Gemfile'),
632
- pomText: readTextIfExistsAt(appRoot, 'pom.xml'),
633
- gradleText: `${readTextIfExistsAt(appRoot, 'build.gradle')}\n${readTextIfExistsAt(appRoot, 'build.gradle.kts')}`,
634
- cargoText: `${readTextIfExistsAt(appRoot, 'Cargo.toml')}\n${readTextIfExistsAt(appRoot, 'src-tauri', 'Cargo.toml')}`,
635
- csprojText: rootCsproj ? readTextIfExistsAt(appRoot, rootCsproj) : '',
636
- programCsText: readTextIfExistsAt(appRoot, 'Program.cs'),
637
- viewFilesText: fs.existsSync(path.join(appRoot, 'app', 'View'))
638
- ? fs.readdirSync(path.join(appRoot, 'app', 'View'), { recursive: true }).join('\n')
639
- : ''
640
- };
641
-
642
- const languageDetections = LANGUAGE_DETECTORS
643
- .map(detector => detector.detect(ctx))
644
- .filter(result => result.score >= 2)
645
- .sort((a, b) => b.score - a.score);
646
-
647
- const frameworkDetails = FRAMEWORK_DETECTORS
648
- .map(detector => {
649
- const result = detector.detect(ctx);
650
- if (result.score < 3) return null;
651
- return {
652
- ...result,
653
- kind: detector.kind,
654
- language: detector.language,
655
- projectType: detector.projectType,
656
- confidence: confidenceLabel(result.score)
657
- };
658
- })
659
- .filter(Boolean)
660
- .sort((a, b) => b.score - a.score);
661
-
662
- const frameworkLanguageScores = frameworkDetails.reduce((acc, item) => {
663
- if (!item.language) return acc;
664
- acc[item.language] = (acc[item.language] || 0) + item.score;
665
- return acc;
666
- }, {});
667
- const primaryLanguage = Object.keys(frameworkLanguageScores).sort((a, b) => frameworkLanguageScores[b] - frameworkLanguageScores[a])[0]
668
- || languageDetections[0]?.id
669
- || 'typescript';
670
- const frameworks = uniq(frameworkDetails.map(item => item.id)).filter(name => FRAMEWORK_CHOICES.includes(name));
671
- const infrastructure = [];
672
- let projectType = '';
673
- let database = 'none';
674
- let testFramework = 'auto-detect';
675
- let cloud = 'none';
676
- let ciCd = 'none';
677
-
678
- if (frameworkDetails.some(item => item.projectType === 'desktop')) projectType = 'desktop';
679
- else if (frameworkDetails.some(item => item.projectType === 'mobile')) projectType = 'mobile';
680
- else if (frameworkDetails.some(item => item.kind === 'frontend') && frameworkDetails.some(item => item.kind === 'backend')) projectType = 'fullstack';
681
- else if (frameworkDetails.some(item => item.projectType === 'web-app')) projectType = 'web-app';
682
- else if (frameworkDetails.some(item => item.kind === 'backend')) projectType = 'api';
683
- else if (frameworkDetails.some(item => item.kind === 'frontend')) projectType = 'web-app';
684
- else if (fileExistsAt(appRoot, 'package.json') && pkg.bin) projectType = 'cli';
685
- else if (['php', 'python', 'go', 'java', 'ruby', 'csharp', 'rust'].includes(primaryLanguage)) projectType = 'api';
686
-
687
- if (hasDependency(depNames, 'pg', 'postgres', '@prisma/client') || /postgres/i.test(JSON.stringify(pkg))) database = 'postgres';
688
- else if (hasDependency(depNames, 'mysql', 'mysql2')) database = 'mysql';
689
- else if (hasDependency(depNames, 'sqlite3', 'better-sqlite3', 'rusqlite')) database = 'sqlite';
690
- else if (hasDependency(depNames, 'mongodb', 'mongoose')) database = 'mongodb';
691
- else if (hasDependency(depNames, 'redis', 'ioredis')) database = 'redis';
692
- else if (hasDependency(depNames, '@aws-sdk/client-dynamodb')) database = 'dynamodb';
693
-
694
- if (hasDependency(depNames, 'vitest')) testFramework = 'vitest';
695
- else if (hasDependency(depNames, 'jest')) testFramework = 'jest';
696
- else if (hasDependency(depNames, 'pytest') || textMatches(ctx.requirementsText, /\bpytest\b/i) || textMatches(ctx.pyprojectText, /\bpytest\b/i)) testFramework = 'pytest';
697
- else if (hasComposerDependency(composerDepNames, 'phpunit/phpunit')) testFramework = 'phpunit';
698
- else if (hasComposerDependency(composerDepNames, 'pestphp/pest')) testFramework = 'pest';
699
- else if (fileExistsAt(appRoot, 'go.mod')) testFramework = 'go test';
700
- else if (textMatches(ctx.gemfileText, /^\s*gem\s+["']rspec["']/im)) testFramework = 'rspec';
701
-
702
- if (repoHasAny(targetDir, [['Dockerfile'], ['docker-compose.yml'], ['docker-compose.yaml']])) {
703
- infrastructure.push('docker');
704
- }
705
-
706
- if (repoHasAny(targetDir, [['vercel.json']])) {
707
- cloud = 'vercel';
708
- infrastructure.push('vercel');
709
- } else if (repoHasAny(targetDir, [['fly.toml']])) {
710
- cloud = 'fly';
711
- infrastructure.push('fly');
712
- } else if (repoHasAny(targetDir, [['railway.json'], ['railway.toml']])) {
713
- cloud = 'railway';
714
- infrastructure.push('railway');
715
- }
716
-
717
- if (repoHasAny(targetDir, [['.github', 'workflows'], ['.gitlab-ci.yml'], ['.circleci', 'config.yml'], ['Jenkinsfile']], 2)) {
718
- if (repoHasAny(targetDir, [['.github', 'workflows']], 2)) ciCd = 'github-actions';
719
- else if (repoHasAny(targetDir, [['.gitlab-ci.yml']], 2)) ciCd = 'gitlab-ci';
720
- else if (repoHasAny(targetDir, [['.circleci', 'config.yml']], 2)) ciCd = 'circleci';
721
- else if (repoHasAny(targetDir, [['Jenkinsfile']], 2)) ciCd = 'jenkins';
722
- }
723
-
724
- const scanEvidence = [
725
- ...(languageDetections[0] ? [`language:${languageDetections[0].id} => ${languageDetections[0].evidence.join('; ')}`] : []),
726
- ...frameworkDetails.map(item => `${item.id} [${item.confidence}] => ${item.evidence.join('; ')}`)
727
- ];
728
-
729
- return {
730
- project_name: path.basename(targetDir),
731
- app_root: path.relative(targetDir, appRoot) || '.',
732
- description: pkg.description || composer.description || 'A software project',
733
- project_type: PROJECT_TYPE_CHOICES.includes(projectType) ? projectType : 'web-app',
734
- primary_language: LANGUAGE_CHOICES.includes(primaryLanguage) ? primaryLanguage : 'typescript',
735
- frameworks,
736
- framework_details: frameworkDetails,
737
- scan_evidence: scanEvidence,
738
- database: DATABASE_CHOICES.includes(database) ? database : 'none',
739
- package_manager: PACKAGE_MANAGER_CHOICES.includes(detectPackageManager(appRoot)) ? detectPackageManager(appRoot) : 'auto-detect',
740
- uses_docker: infrastructure.includes('docker'),
741
- cloud,
742
- infrastructure: uniq(infrastructure),
743
- ci_cd: ciCd,
744
- test_framework: TEST_FRAMEWORK_CHOICES.includes(testFramework) ? testFramework : 'auto-detect',
745
- max_file_lines: '300',
746
- code_style: 'project-standard',
747
- architecture_notes: '<!-- Add notes -->',
748
- current_phase: 'Initial setup',
749
- next_phase: '-',
750
- known_issues: '<!-- None yet -->'
751
- };
752
- }
753
- // --- File Generators ---
754
-
755
- function generateSharedAgentsMd(data) {
756
- return `# AGENTS.md
757
-
758
- Shared operating instructions for Codex, Antigravity, Claude Code, and Cursor.
759
-
760
- ## Startup
761
-
762
- 1. Read this file first.
763
- 2. Read \`.agent/docs/project.md\` for detected stack, commands, and routing.
764
- 3. Read \`.agent/docs/workflow.md\` for the task flow before editing.
765
- 4. Read \`.agent/docs/architecture.md\` and \`.agent/docs/conventions.md\` before planning code changes.
766
- 5. Read \`.agent/docs/tooling.md\` when the task needs MCP, RTK, or browser verification.
767
-
768
- If any \`.agent/docs/*.md\` file still contains \`TODO: refine\`, update the docs by scanning the project before making product code changes.
769
-
770
- ## Workflow
771
-
772
- Superpowers is the primary workflow backend. Use its native skills when available:
773
-
774
- - New idea or unclear requirement: \`brainstorming\`.
775
- - Feature/refactor: \`using-git-worktrees\` -> \`writing-plans\` -> explicit approval -> \`executing-plans\` or \`subagent-driven-development\`.
776
- - Implementation: \`test-driven-development\` and focused verification.
777
- - Bug or failing behavior: \`systematic-debugging\` followed by a regression test.
778
- - Before completion: \`requesting-code-review\` and \`verification-before-completion\`.
779
- - Branch completion: \`finishing-a-development-branch\`.
780
-
781
- Questions and research do not edit product code. A simple fix may use a fast path only when it is one-file, unambiguous, and changes no behavior or contract. State the scope and verification before editing. If the scope grows, switch to the full Superpowers flow.
782
-
783
- If Superpowers is unavailable, follow the plain-language flow in \`.agent/docs/workflow.md\`; do not expect this kit to provide a second copy of the workflow engine.
784
-
785
- ## Rules
786
-
787
- - Respond in the user's language; keep code, identifiers, and code comments in English.
788
- - State assumptions when the request is ambiguous.
789
- - Prefer the existing project style over new abstractions.
790
- - Keep changes surgical and remove only dead code introduced by your change.
791
- - **MCP First**: Prioritize \`codebase-memory-mcp\` for symbols, call paths, architecture, and impact; use \`context7\` for API/docs queries and \`playwright\` for browser/UI verification. Fall back to normal file search when an integration is unavailable.
792
- - Prefer \`rtk\` by default for verbose shell commands such as \`rtk git status\`, \`rtk npm test\`, and \`rtk git log\`. Run the underlying command directly only when RTK is unavailable or incompatible.
793
- - Tests or equivalent verification are mandatory before claiming done.
794
- - Keep files under ${data.max_file_lines || '300'} lines unless the project already has a different standard in \`.agent/docs/conventions.md\`.
795
-
796
- ## Shared Knowledge
797
-
798
- - Project context: \`.agent/docs/\`
799
- - Tooling setup: \`.agent/docs/tooling.md\`
800
- - Superpowers: installed through the AI client's official plugin surface
801
- - RTK: recommended local CLI for compact command output
802
- - Antigravity and Cursor entry rules: \`GEMINI.md\` and \`.cursorrules\`
803
- `;
804
- }
805
-
806
- function generateIndexProjectPrompt() {
807
- return `# Project Indexing Prompt
808
-
809
- Read CLAUDE.md (if using Claude Code), AGENTS.md, and all files under .agent/docs/.
810
- Use Superpowers for the workflow and use codebase-memory MCP for structural code discovery when available.
811
- Ask me what this project does and any important conventions I want preserved.
812
- Then scan this repository and refine the generated context docs with factual rules.
813
- Do not implement product code during onboarding.
814
- `;
815
- }
816
-
817
- function printProjectPrompt() {
818
- const promptPath = path.join(targetDir, '.agent', 'docs', 'index-project-prompt.md');
819
- if (!fs.existsSync(promptPath)) {
820
- console.error(pc.yellow('No saved project prompt found. Run thachvd-kit init first.'));
821
- process.exitCode = 1;
822
- return;
823
- }
824
- console.log(fs.readFileSync(promptPath, 'utf8'));
825
- }
826
-
827
- function generateSharedClaudeMd() {
828
- return `@AGENTS.md
829
-
830
- ## Claude Code
831
-
832
- 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.
833
- `;
834
- }
835
-
836
- function generateSharedGeminiMd() {
837
- return `# GEMINI.md
838
-
839
- Antigravity entry file for this project.
840
-
841
- Read AGENTS.md first, then follow the shared docs under .agent/docs/.
842
- `;
843
- }
844
-
845
- function generateSharedCursorrules(data) {
846
- return `# Cursor Rules
847
-
848
- This repository uses AGENTS.md as the shared cross-agent entry file. Follow the instructions in AGENTS.md and keep Cursor-specific notes here only when they cannot apply to Codex, Antigravity, or Claude Code.
849
-
850
- Read AGENTS.md first, then follow the shared docs under .agent/docs/.
851
-
852
- ---
853
-
854
- ${generateSharedAgentsMd(data).trimEnd()}
855
- `;
856
- }
857
-
858
- function resolveCommands(data) {
859
- const frameworks = data.frameworks || [];
860
- const primaryLang = data.primary_language || 'javascript';
861
- const pkgManager = data.package_manager || 'npm';
862
- const testFramework = data.test_framework || 'auto-detect';
863
-
864
- const isPython = primaryLang === 'python';
865
- const isGo = primaryLang === 'go';
866
- const isRust = primaryLang === 'rust';
867
- const isPhp = primaryLang === 'php';
868
-
869
- let install = 'npm install';
870
- let dev = 'npm run dev';
871
- let build = 'npm run build';
872
- let test = 'npm test';
873
- let lint = 'npm run lint';
874
-
875
- if (pkgManager === 'pnpm') { install = 'pnpm install'; dev = 'pnpm dev'; build = 'pnpm build'; test = 'pnpm test'; lint = 'pnpm lint'; }
876
- else if (pkgManager === 'yarn') { install = 'yarn'; dev = 'yarn dev'; build = 'yarn build'; test = 'yarn test'; lint = 'yarn lint'; }
877
- else if (pkgManager === 'bun') { install = 'bun install'; dev = 'bun dev'; build = 'bun run build'; test = 'bun test'; lint = 'bun run lint'; }
878
- else if (isPython) { install = 'pip install -r requirements.txt'; dev = 'python manage.py runserver'; build = '# no build step'; test = 'pytest'; lint = 'ruff check .'; }
879
- else if (isGo) { install = 'go mod download'; dev = 'go run .'; build = 'go build ./...'; test = 'go test ./...'; lint = 'golangci-lint run'; }
880
- else if (isRust) { install = 'cargo build'; dev = 'cargo run'; build = 'cargo build --release'; test = 'cargo test'; lint = 'cargo clippy'; }
881
- else if (isPhp) {
882
- install = 'composer install';
883
- build = '# no build step';
884
- test = testFramework === 'pest' ? './vendor/bin/pest' : './vendor/bin/phpunit';
885
- const isCakePHP = frameworks.some(f => f.toLowerCase().includes('cakephp'));
886
- const isSymfony = frameworks.some(f => f.toLowerCase().includes('symfony'));
887
- const isCodeIgniter = frameworks.some(f => f.toLowerCase().includes('codeigniter'));
888
- if (isCakePHP) {
889
- dev = 'bin/cake server';
890
- lint = './vendor/bin/phpcs';
891
- } else if (isSymfony) {
892
- dev = 'bin/console server:run';
893
- lint = './vendor/bin/phpcs';
894
- } else if (isCodeIgniter) {
895
- dev = 'php spark serve';
896
- lint = './vendor/bin/phpcs';
897
- } else {
898
- dev = 'php artisan serve';
899
- lint = 'php artisan pint';
900
- }
901
- }
902
-
903
- if (testFramework === 'vitest') test = `${pkgManager === 'pnpm' ? 'pnpm' : pkgManager === 'yarn' ? 'yarn' : pkgManager === 'bun' ? 'bun run' : 'npx'} vitest`;
904
- else if (testFramework === 'jest') test = `${pkgManager === 'pnpm' ? 'pnpm' : pkgManager === 'yarn' ? 'yarn' : 'npx'} jest`;
905
-
906
- return { install, dev, build, test, lint };
907
- }
908
-
909
- function generateProjectDoc(data) {
910
- const commands = resolveCommands(data);
911
- const evidence = (data.scan_evidence || []).length > 0
912
- ? (data.scan_evidence || []).map(line => `- ${line}`).join('\n')
913
- : '- TODO: refine scan evidence by inspecting the project.';
914
-
915
- return `# Project Rules
916
-
917
- Generated by thachvd-kit on ${new Date().toISOString().split('T')[0]}.
918
-
919
- ## Summary
920
-
921
- - Name: ${data.project_name}
922
- - Description: ${data.description}
923
- - Type: ${data.project_type}
924
- - App root: ${data.app_root || '.'}
925
-
926
- ## Stack
927
-
928
- - Language: ${data.primary_language}
929
- - Frameworks: ${(data.frameworks || []).join(', ') || 'none'}
930
- - Database: ${data.database || 'none'}
931
- - Infrastructure: ${(data.infrastructure || []).join(', ') || 'none'}
932
- - Package manager: ${data.package_manager || 'auto-detect'}
933
- - Test framework: ${data.test_framework || 'auto-detect'}
934
-
935
- ## Commands
936
-
937
- - Install: \`${commands.install}\`
938
- - Dev: \`${commands.dev}\`
939
- - Build: \`${commands.build}\`
940
- - Test: \`${commands.test}\`
941
- - Lint: \`${commands.lint}\`
942
-
943
- ## Available Integrations
944
-
945
- Use these integrations when available. Run thachvd-kit doctor to check the current machine.
946
-
947
- | MCP | When to Use |
948
- |-----|-------------|
949
- | \`codebase-memory-mcp\` | Explore symbols, trace call chains, architecture, and impact |
950
- | \`context7\` | Look up library/framework docs, API signatures, migration guides |
951
- | \`playwright\` | Verify UI behavior, take screenshots, test forms and navigation |
952
- | \`rtk\` | Recommended proxy for noisy git, tests, builds, and package commands |
953
-
954
- > Check \`.agent/docs/tooling.md\` for setup details and additional MCP servers.
955
-
956
- ## Scan Evidence
957
-
958
- ${evidence}
959
- `;
960
- }
961
-
962
- function generateArchitectureDoc(data) {
963
- const appRootLine = data.app_root && data.app_root !== '.'
964
- ? `- App root is currently detected as \`${data.app_root}\`.`
965
- : '- App root is the repository root unless refined below.';
966
- const evidence = (data.scan_evidence || []).length > 0
967
- ? (data.scan_evidence || []).map(line => `- ${line}`).join('\n')
968
- : '- TODO: refine architecture by scanning source directories.';
969
-
970
- return `# Architecture Notes
971
-
972
- ## Current Map
973
-
974
- ${appRootLine}
975
- - TODO: refine major directories and responsibilities.
976
- - TODO: refine main entry points, routing, state boundaries, and integration boundaries.
977
-
978
- ## Detected Evidence
979
-
980
- ${evidence}
981
-
982
- ## AI Maintenance Rule
983
-
984
- 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.
985
- `;
986
- }
987
-
988
- function generateConventionsDoc(data) {
989
- return `# Coding Conventions
990
-
991
- ## Current Standards
992
-
993
- - Maximum file length: ${data.max_file_lines || '300'} lines unless the existing project standard is stricter.
994
- - Code comments and identifiers should be written in English.
995
- - Keep changes scoped to the user request.
996
- - Prefer existing local patterns over introducing new abstractions.
997
- - TODO: refine naming, formatting, folder, API, state, styling, and testing conventions from the real codebase.
998
-
999
- ## Verification
1000
-
1001
- - Run the focused test or lint command that matches the touched area.
1002
- - If no automated check exists, document the manual verification performed.
1003
- - For UI/web changes, prioritize using Playwright MCP to automatically verify client-side behavior and capture screenshots.
1004
- - Do not claim completion without verification evidence.
1005
- `;
1006
- }
1007
-
1008
- function generateWorkflowDoc() {
1009
- return `# Agent Workflow
1010
-
1011
- This project uses Superpowers as the workflow backend. thachvd-kit only provides project context and integration setup.
1012
-
1013
- ## Before Every Task
1014
-
1015
- 1. Read AGENTS.md and the relevant files under .agent/docs/.
1016
- 2. Use the native Superpowers skill that matches the request.
1017
- 3. Use codebase-memory MCP for structural discovery when available.
1018
- 4. Make changes only after the required planning gate.
1019
- 5. Verify the change and request review before completion.
1020
-
1021
- ## Route Matrix
1022
-
1023
- | Situation | Superpowers path | Gate |
1024
- |---|---|---|
1025
- | Question or research only | direct answer or research | no product-code edits |
1026
- | One obvious local fix | fast path | inspect -> edit -> focused verify |
1027
- | Bug or failing behavior | systematic-debugging | reproduce -> root cause -> regression test -> fix -> verify |
1028
- | New behavior or refactor | brainstorming -> writing-plans -> executing-plans | explicit plan approval before implementation |
1029
- | Multi-part implementation | subagent-driven-development | task review and final review |
1030
- | Pre-merge | requesting-code-review | blocking findings resolved |
1031
-
1032
- ## Standard Feature Flow
1033
-
1034
- brainstorming -> using-git-worktrees -> writing-plans -> user approval -> test-driven-development -> executing-plans or subagent-driven-development -> requesting-code-review -> verification-before-completion -> finishing-a-development-branch.
1035
-
1036
- ## Fast Path
1037
-
1038
- Use it only for one-file, unambiguous changes with no behavior, API, schema, security, dependency, CI, workflow, or release contract change. State the narrow scope and verification before editing. If the scope grows, switch to brainstorming and writing-plans.
1039
-
1040
- ## Documentation Maintenance
1041
-
1042
- - Update architecture.md when structure or boundaries change.
1043
- - Update conventions.md when repeated project patterns become clear.
1044
- - Update project.md when stack, scripts, or test tooling changes.
1045
- - Keep AGENTS.md concise and put project-specific detail under .agent/docs/.
1046
- `;
1047
- }
1048
-
1049
- function generateGettingStartedDoc() {
1050
- return `# Getting Started
1051
-
1052
- thachvd-kit creates project context. Superpowers owns the development workflow.
1053
-
1054
- ## Setup
1055
-
1056
- 1. Run \`thachvd-kit init\` in the repository.
1057
- 2. Open or print \`.agent/docs/index-project-prompt.md\` with \`thachvd-kit prompt\`.
1058
- 3. Install Superpowers and RTK, then run \`thachvd-kit setup\`.
1059
- 4. Run \`thachvd-kit doctor\` and restart the AI client.
1060
-
1061
- ## Standard Flow
1062
-
1063
- Use Superpowers native skills:
1064
-
1065
- 1. \`brainstorming\` for unclear requirements.
1066
- 2. \`using-git-worktrees\` for isolated work.
1067
- 3. \`writing-plans\`, then wait for explicit approval.
1068
- 4. \`test-driven-development\` and \`executing-plans\` or \`subagent-driven-development\`.
1069
- 5. \`requesting-code-review\` and \`verification-before-completion\`.
1070
- 6. \`finishing-a-development-branch\`.
1071
-
1072
- ## Bug Flow
1073
-
1074
- Use \`systematic-debugging\`: reproduce the symptom, inspect the path with codebase-memory MCP, test hypotheses, add regression protection, fix the root cause, and verify.
1075
-
1076
- ## Fast Path
1077
-
1078
- Use a fast path only for one-file, unambiguous changes with no behavior or contract change. State the scope and verification before editing. If the scope expands, return to brainstorming and writing-plans.
1079
- `;
1080
- }
1081
-
1082
- function resolveToolingSetup(data) {
1083
- const primaryLang = data.primary_language || 'javascript';
1084
- const packageManager = data.package_manager || 'npm';
1085
- const isPython = primaryLang === 'python';
1086
- const runner = packageManager === 'pnpm'
1087
- ? 'pnpm dlx'
1088
- : packageManager === 'yarn'
1089
- ? 'yarn dlx'
1090
- : packageManager === 'bun'
1091
- ? 'bunx'
1092
- : 'npx';
1093
- const playwrightCommand = isPython
1094
- ? 'pip install playwright && playwright install chromium'
1095
- : `${runner} playwright install`;
1096
-
1097
- return { runner, playwrightCommand };
1098
- }
1099
-
1100
- function generateToolingDoc(data) {
1101
- const { runner, playwrightCommand } = resolveToolingSetup(data);
1102
-
1103
- return `# Optional Tooling
1104
-
1105
- Run \`thachvd-kit doctor\` to check the current machine. thachvd-kit does not install workflow skills or copy agent folders.
1106
-
1107
- ## Superpowers
1108
-
1109
- Install Superpowers separately for each AI client from its official plugin surface:
1110
-
1111
- - Claude Code: \`/plugin install superpowers@claude-plugins-official\`
1112
- - Antigravity: \`agy plugin install https://github.com/obra/superpowers\`
1113
- - Codex App: install it from the Plugins sidebar.
1114
- - Codex CLI: open \`/plugins\` and install Superpowers.
1115
- - Cursor: \`/add-plugin superpowers\`
1116
-
1117
- Guide: https://github.com/obra/superpowers#installation
1118
-
1119
- ## Codebase Memory MCP
1120
-
1121
- Use \`codebase-memory-mcp\` for symbol search, call paths, architecture, and impact analysis.
1122
-
1123
- - Install: \`npm install -g codebase-memory-mcp\`
1124
- - Index a repository: \`codebase-memory-mcp cli index_repository --repo-path .\`
1125
- - Keep local index state out of git unless the team explicitly chooses a shared snapshot.
1126
-
1127
- ## RTK
1128
-
1129
- RTK is the recommended command-output layer, not a workflow engine. Prefer it by default for verbose shell commands.
1130
-
1131
- - Install: \`cargo install --git https://github.com/rtk-ai/rtk --branch master rtk\`
1132
- - Examples: \`rtk git status\`, \`rtk npm test\`, \`rtk git log\`.
1133
- - If it is unavailable, run the underlying command directly.
1134
-
1135
- ## Playwright
1136
-
1137
- - Check: \`${runner} playwright --version\`
1138
- - Browser install: \`${playwrightCommand}\`
1139
- - Use the project's existing Playwright scripts when available.
1140
- `;
1141
- }
1142
-
1143
- async function writeGeneratedFile(relativePath, content, assumeYes) {
1144
- const outputPath = path.join(targetDir, relativePath);
1145
- const exists = fs.existsSync(outputPath);
1146
- if (exists && !assumeYes) {
1147
- const response = await prompts({
1148
- type: 'confirm',
1149
- name: 'overwrite',
1150
- message: `File ${pc.bold(relativePath)} already exists. Do you want to overwrite it?`,
1151
- initial: false
1152
- });
1153
- if (response.overwrite === undefined) {
1154
- console.log(pc.yellow('\nCancelled.'));
1155
- process.exit(130);
1156
- }
1157
- if (!response.overwrite) return false;
1158
- }
1159
-
1160
- fs.mkdirSync(path.dirname(outputPath), { recursive: true });
1161
- fs.writeFileSync(outputPath, content, 'utf8');
1162
- return true;
1163
- }
1164
-
1165
- function homePath(...parts) {
1166
- const home = process.env.USERPROFILE || process.env.HOME;
1167
- return home ? path.join(home, ...parts) : null;
1168
- }
1169
-
1170
- function printSuperpowersInstallGuide() {
1171
- console.log(`
1172
- ${pc.bold('Superpowers setup')} (official client integrations)
1173
- Claude Code: /plugin install superpowers@claude-plugins-official
1174
- Antigravity: agy plugin install https://github.com/obra/superpowers
1175
- Codex App: install Superpowers from the Plugins sidebar
1176
- Codex CLI: open /plugins, search Superpowers, then Install Plugin
1177
- Cursor: /add-plugin superpowers
1178
-
1179
- Full guide: https://github.com/obra/superpowers#installation
1180
- `);
1181
- }
1182
-
1183
- function runDoctor() {
1184
- console.log(pc.bold(pc.cyan('thachvd-kit doctor')));
1185
- const requiredFiles = ['AGENTS.md', 'CLAUDE.md', 'GEMINI.md', '.cursorrules',
1186
- path.join('.agent', 'docs', 'project.md'), path.join('.agent', 'docs', 'workflow.md')];
1187
- for (const relativePath of requiredFiles) {
1188
- const ok = fs.existsSync(path.join(targetDir, relativePath));
1189
- console.log(` ${ok ? pc.green('OK') : pc.yellow('WARN')} ${relativePath}`);
1190
- }
1191
-
1192
- const tools = [
1193
- [CODEBASE_MEMORY_BINARY, `npm install -g ${CODEBASE_MEMORY_PACKAGE}`],
1194
- ['rtk', `recommended: ${RTK_INSTALL_COMMAND}`],
1195
- ['agy', 'install Antigravity before running its Superpowers plugin command'],
1196
- ['claude', 'install Claude Code before installing its Superpowers plugin'],
1197
- ['codex', 'install Codex before using its Superpowers plugin']
1198
- ];
1199
- console.log(`\n${pc.bold('Local integrations (RTK recommended)')}`);
1200
- for (const [binary, hint] of tools) {
1201
- const ok = commandExists(binary);
1202
- console.log(` ${ok ? pc.green('OK') : pc.yellow('WARN')} ${binary}${ok ? '' : ` - ${hint}`}`);
1203
- }
1204
-
1205
- console.log(`\n${pc.bold('Next step')}`);
1206
- console.log(' Run thachvd-kit prompt to print the saved project-indexing prompt.');
1207
- }
1208
-
1209
- async function runSetup(args) {
1210
- const skipMcpSetup = args.includes('--no-setup-mcp');
1211
- const skipHookSetup = args.includes('--no-setup-hook');
1212
- const skipRtkInstall = args.includes('--no-install-rtk');
1213
- const skipIndex = args.includes('--no-index');
1214
- console.log(pc.bold(pc.cyan('thachvd-kit setup')));
1215
- printSuperpowersInstallGuide();
1216
- if (!skipMcpSetup) {
1217
- console.log(pc.bold('MCP setup'));
1218
- for (const result of setupMcpServers({ install: true })) {
1219
- const marker = result.ok ? pc.green('OK') : pc.yellow('WARN');
1220
- console.log(` ${marker} ${result.message}`);
1221
- }
1222
- if (!skipIndex) {
1223
- const indexResult = indexRepository();
1224
- const marker = indexResult.ok ? pc.green('OK') : pc.yellow('WARN');
1225
- console.log(` ${marker} ${indexResult.message}`);
1226
- }
1227
- }
1228
- if (!skipHookSetup) {
1229
- console.log(`\n${pc.bold('Claude Code Definition-of-Done hook')}`);
1230
- const claudeProjectSettings = path.join(targetDir, '.claude', 'settings.json');
1231
- const result = mergeClaudeProjectHooks(claudeProjectSettings);
1232
- const marker = result.ok ? pc.green('OK') : pc.yellow('WARN');
1233
- console.log(` ${marker} ${result.message}`);
1234
- console.log(' This hook only checks for plan/review/verification evidence; it never prescribes which skill produces it.');
1235
- }
1236
- console.log(`\n${pc.bold('RTK (recommended)')}`);
1237
- const rtkResult = ensureRtkInstalled(!skipRtkInstall);
1238
- const rtkMarker = rtkResult.ok ? pc.green('OK') : pc.yellow('WARN');
1239
- console.log(` ${rtkMarker} ${rtkResult.message}`);
1240
- console.log('\nRestart your AI client after installing plugins or changing MCP configuration.');
1241
- console.log('Your onboarding prompt is saved at .agent/docs/index-project-prompt.md. Run thachvd-kit prompt to print it again.');
1242
- }
1243
-
1244
- function commandExists(command) {
1245
- const checker = process.platform === 'win32' ? 'where' : 'command';
1246
- const args = process.platform === 'win32' ? [command] : ['-v', command];
1247
- const result = spawnSync(checker, args, { encoding: 'utf8', shell: process.platform !== 'win32' });
1248
- return result.status === 0;
1249
- }
1250
-
1251
- function ensureGlobalNpmPackage(binaryName, packageName, install = false) {
1252
- if (commandExists(binaryName)) {
1253
- return { ok: true, message: `${binaryName} already installed` };
1254
- }
1255
- if (!install) {
1256
- return { ok: false, message: `${binaryName} is missing; run: npm install -g ${packageName}` };
1257
- }
1258
- if (!commandExists('npm')) {
1259
- return { ok: false, message: `npm not found; run: npm install -g ${packageName}` };
1260
- }
1261
- const result = spawnSync('npm', ['install', '-g', packageName], {
1262
- encoding: 'utf8',
1263
- stdio: 'pipe',
1264
- shell: process.platform === 'win32'
1265
- });
1266
- if (result.status !== 0) {
1267
- return { ok: false, message: `failed to install ${binaryName}; run: npm install -g ${packageName}` };
1268
- }
1269
- return { ok: true, message: `installed ${binaryName}` };
1270
- }
1271
-
1272
- function ensureCodebaseMemoryCli(install = false) {
1273
- return ensureGlobalNpmPackage(CODEBASE_MEMORY_BINARY, CODEBASE_MEMORY_PACKAGE, install);
1274
- }
1275
-
1276
- function ensureRtkInstalled(install = false) {
1277
- if (commandExists('rtk')) {
1278
- return { ok: true, message: 'rtk already installed' };
1279
- }
1280
- if (!install) {
1281
- return { ok: false, message: `rtk is missing; run: ${RTK_INSTALL_COMMAND}` };
1282
- }
1283
- if (!commandExists('cargo')) {
1284
- return { ok: false, message: `cargo not found; install Rust, then run: ${RTK_INSTALL_COMMAND}` };
1285
- }
1286
- console.log(pc.dim(' Installing rtk via cargo (compiles from source; this can take a few minutes)...'));
1287
- const result = spawnSync('cargo', ['install', '--git', 'https://github.com/rtk-ai/rtk', '--branch', 'master', 'rtk'], {
1288
- encoding: 'utf8',
1289
- stdio: 'inherit',
1290
- shell: process.platform === 'win32'
1291
- });
1292
- if (result.status !== 0) {
1293
- return { ok: false, message: `failed to install rtk; run: ${RTK_INSTALL_COMMAND}` };
1294
- }
1295
- return { ok: true, message: 'installed rtk' };
1296
- }
1297
-
1298
- function indexRepository() {
1299
- if (!commandExists(CODEBASE_MEMORY_BINARY)) {
1300
- return { ok: false, message: `${CODEBASE_MEMORY_BINARY} is not installed; skipped indexing` };
1301
- }
1302
- console.log(pc.dim(` Running ${CODEBASE_MEMORY_BINARY} cli index_repository --repo-path . (first index of a large repo can take a while)...`));
1303
- const result = spawnSync(CODEBASE_MEMORY_BINARY, ['cli', 'index_repository', '--repo-path', '.'], {
1304
- cwd: targetDir,
1305
- encoding: 'utf8',
1306
- stdio: 'inherit',
1307
- shell: process.platform === 'win32'
1308
- });
1309
- if (result.status !== 0) {
1310
- return { ok: false, message: `indexing failed; run manually: ${CODEBASE_MEMORY_BINARY} cli index_repository --repo-path .` };
1311
- }
1312
- return { ok: true, message: 'indexed repository with codebase-memory-mcp' };
1313
- }
1314
-
1315
- function readJsonFile(filePath) {
1316
- if (!fs.existsSync(filePath)) return {};
1317
- try {
1318
- return JSON.parse(fs.readFileSync(filePath, 'utf8'));
1319
- } catch {
1320
- return null;
1321
- }
1322
- }
1323
-
1324
- function writeJsonFile(filePath, data) {
1325
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
1326
- fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n', 'utf8');
1327
- }
1328
-
1329
- function mergeMcpJson(filePath, withType = false) {
1330
- const data = readJsonFile(filePath);
1331
- if (data === null) {
1332
- return { ok: false, message: `${filePath} is not valid JSON; skipped` };
1333
- }
1334
- data.mcpServers = data.mcpServers || {};
1335
- const baseServers = {
1336
- 'codebase-memory-mcp': { command: CODEBASE_MEMORY_BINARY, args: [] },
1337
- playwright: { command: 'playwright-mcp' },
1338
- context7: { command: 'context7-mcp' }
1339
- };
1340
- if (data.permissions && Array.isArray(data.permissions.allow)) {
1341
- const requiredPermissions = [
1342
- "mcp__context7__resolve-library-id",
1343
- "mcp__context7__query-docs"
1344
- ];
1345
- for (const perm of requiredPermissions) {
1346
- if (!data.permissions.allow.includes(perm)) {
1347
- data.permissions.allow.push(perm);
1348
- }
1349
- }
1350
- }
1351
- for (const [name, server] of Object.entries(baseServers)) {
1352
- if (!data.mcpServers[name]) {
1353
- data.mcpServers[name] = withType ? { type: 'stdio', ...server } : server;
1354
- }
1355
- }
1356
- writeJsonFile(filePath, data);
1357
- const tools = Object.keys(baseServers).join(', ');
1358
- return { ok: true, message: `configured ${tools} MCP in ${filePath}` };
1359
- }
1360
-
1361
- function appendMcpTomlIfMissing(filePath) {
1362
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
1363
- const existing = fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf8') : '';
1364
- const blocks = [];
1365
- if (!/\[mcp_servers\.context7\]/.test(existing)) {
1366
- blocks.push(`[mcp_servers.context7]
1367
- command = "context7-mcp"
1368
- startup_timeout_sec = 20
1369
- tool_timeout_sec = 120
1370
- `);
1371
- }
1372
- if (!/\[mcp_servers\.codebase-memory-mcp\]/.test(existing)) {
1373
- blocks.push(`[mcp_servers.codebase-memory-mcp]
1374
- command = "${CODEBASE_MEMORY_BINARY}"
1375
- startup_timeout_sec = 20
1376
- tool_timeout_sec = 120
1377
- `);
1378
- }
1379
- if (!/\[mcp_servers\.playwright\]/.test(existing)) {
1380
- blocks.push(`[mcp_servers.playwright]
1381
- command = "playwright-mcp"
1382
- startup_timeout_sec = 20
1383
- tool_timeout_sec = 120
1384
- `);
1385
- }
1386
- if (blocks.length === 0) { return { ok: true, message: `already configured ${filePath}` };
1387
- }
1388
- const separator = existing && !existing.endsWith('\n') ? '\n\n' : existing ? '\n' : '';
1389
- fs.writeFileSync(filePath, existing + separator + blocks.join('\n'), 'utf8');
1390
- return { ok: true, message: `configured codebase-memory-mcp, context7, playwright MCP in ${filePath}` };
1391
- }
1392
-
1393
- const DOD_HOOK_MARKER = 'feature/refactor Definition-of-Done gate';
1394
-
1395
- function generateDodStopHookPrompt() {
1396
- return `Input JSON (Stop hook payload): $ARGUMENTS\n\nCheck this project's ${DOD_HOOK_MARKER}. This kit does not define its own workflow steps; it only verifies that evidence of the gate exists, regardless of which skill produced it (a Superpowers skill such as \`writing-plans\`/\`requesting-code-review\`, or an equivalent documented step). Use the \`last_assistant_message\` field as the authoritative final response. Use \`transcript_path\` only for additional context because the transcript may not contain the final response yet.\n\nFor a feature or refactor (multi-file change, new behavior, or non-trivial refactor), completion requires evidence of:\n- Plan: a plan was written and explicitly approved by the human before implementation started.\n- Review: the change was reviewed (correctness, simplicity, architecture, security, performance) and blocking issues were resolved.\n- Verification: tests or equivalent verification ran and passed.\n\nA one-file, unambiguous fix with no behavior/contract change is exempt from the Plan and Review evidence only when the final response states FAST_PATH: simple-fix, its reason, and its narrow scope. Verification is still mandatory in that case.\n\nSteps:\n1. If the input JSON has \`stop_hook_active: true\`, allow stopping immediately (already checked once this cycle; never block twice in a row).\n2. Inspect the changed files and the \`last_assistant_message\` field.\n3. If no source files changed, the final response does not claim the task is done/complete, or the final response explicitly contains FAST_PATH: simple-fix and the changed scope is clearly a simple/self-contained fix, allow stopping.\n4. If it is a feature/refactor-scale change and the final response claims completion, verify that the response (or the visible transcript) shows evidence of an approved plan and a completed review. If either is missing, block; a generic statement that the task was simple is not enough.\n5. Return exactly JSON: {"ok": true} to allow stopping, or {"ok": false, "reason": "..."} to block.\n\nWhen blocking, name exactly which gate is missing without prescribing which skill or workflow must produce it.`;
1397
- }
1398
-
1399
- function isDefinitionOfDoneHook(hook) {
1400
- const prompt = hook && typeof hook.prompt === 'string' ? hook.prompt : '';
1401
- return prompt.includes(DOD_HOOK_MARKER);
1402
- }
1403
-
1404
- function mergeClaudeProjectHooks(filePath) {
1405
- const data = readJsonFile(filePath);
1406
- if (data === null) {
1407
- return { ok: false, message: `${filePath} is not valid JSON; skipped hook setup` };
1408
- }
1409
- if (!data.hooks || typeof data.hooks !== 'object') data.hooks = {};
1410
- if (!Array.isArray(data.hooks.Stop)) data.hooks.Stop = [];
1411
-
1412
- let primaryHook = null;
1413
- let definitionHookCount = 0;
1414
- for (const entry of data.hooks.Stop) {
1415
- for (const hook of Array.isArray(entry?.hooks) ? entry.hooks : []) {
1416
- if (isDefinitionOfDoneHook(hook)) {
1417
- definitionHookCount++;
1418
- if (!primaryHook) primaryHook = hook;
1419
- }
1420
- }
1421
- }
1422
-
1423
- if (primaryHook) {
1424
- primaryHook.prompt = generateDodStopHookPrompt();
1425
- primaryHook.timeout = 60;
1426
- primaryHook.statusMessage = 'Checking feature/refactor Definition-of-Done gate...';
1427
-
1428
- if (definitionHookCount > 1) {
1429
- data.hooks.Stop = data.hooks.Stop
1430
- .map(entry => ({
1431
- ...entry,
1432
- hooks: (Array.isArray(entry?.hooks) ? entry.hooks : [])
1433
- .filter(hook => !isDefinitionOfDoneHook(hook) || hook === primaryHook)
1434
- }))
1435
- .filter(entry => entry.hooks.length > 0);
1436
- }
1437
-
1438
- writeJsonFile(filePath, data);
1439
- return {
1440
- ok: true,
1441
- message: definitionHookCount > 1
1442
- ? `consolidated Definition-of-Done Stop hooks in ${filePath}`
1443
- : `updated Definition-of-Done Stop hook in ${filePath}`
1444
- };
1445
- }
1446
-
1447
- data.hooks.Stop.push({
1448
- hooks: [
1449
- {
1450
- type: 'agent',
1451
- prompt: generateDodStopHookPrompt(),
1452
- timeout: 60,
1453
- statusMessage: 'Checking feature/refactor Definition-of-Done gate...'
1454
- }
1455
- ]
1456
- });
1457
- writeJsonFile(filePath, data);
1458
- return { ok: true, message: `configured Definition-of-Done Stop hook in ${filePath}` };
1459
- }
1460
-
1461
- function setupMcpServers(options = {}) {
1462
- const install = options.install === true;
1463
- const results = [];
1464
- results.push(ensureCodebaseMemoryCli(install));
1465
- results.push(ensureGlobalNpmPackage('context7-mcp', '@upstash/context7-mcp', install));
1466
- results.push(ensureGlobalNpmPackage('playwright-mcp', '@playwright/mcp', install));
1467
-
1468
- const codexConfig = homePath('.codex', 'config.toml');
1469
- if (codexConfig) results.push(appendMcpTomlIfMissing(codexConfig));
1470
-
1471
- const geminiConfig = homePath('.gemini', 'config', 'mcp_config.json');
1472
- if (geminiConfig) results.push(mergeMcpJson(geminiConfig, false));
1473
-
1474
- const antigravityIdeConfig = homePath('.gemini', 'antigravity-ide', 'mcp_config.json');
1475
- if (antigravityIdeConfig) results.push(mergeMcpJson(antigravityIdeConfig, false));
1476
-
1477
- const antigravityConfig = homePath('.gemini', 'antigravity', 'mcp_config.json');
1478
- if (antigravityConfig && fs.existsSync(path.dirname(antigravityConfig))) {
1479
- results.push(mergeMcpJson(antigravityConfig, false));
1480
- }
1481
-
1482
- const claudeConfig = homePath('.claude.json');
1483
- if (claudeConfig) results.push(mergeMcpJson(claudeConfig, true));
1484
-
1485
- const claudeSettings = homePath('.claude', 'settings.json');
1486
- if (claudeSettings) results.push(mergeMcpJson(claudeSettings, true));
1487
-
1488
- return results;
1489
- }
1490
-
1491
- // --- Main CLI ---
1492
-
1493
- async function main() {
1494
- const rawArgs = process.argv.slice(2);
1495
- const command = rawArgs[0] && !rawArgs[0].startsWith('-') ? rawArgs[0] : 'init';
1496
- const args = command === 'init' ? rawArgs.slice(rawArgs[0] === 'init' ? 1 : 0) : rawArgs.slice(1);
1497
-
1498
- if (rawArgs.includes('--version') || rawArgs.includes('-v')) {
1499
- console.log(packageJson.version);
1500
- return;
1501
- }
1502
-
1503
- if (rawArgs.includes('--help') || rawArgs.includes('-h')) {
1504
- printHelp();
1505
- return;
1506
- }
1507
-
1508
- if (command === 'doctor') {
1509
- runDoctor();
1510
- return;
1511
- }
1512
-
1513
- if (command === 'prompt') {
1514
- printProjectPrompt();
1515
- return;
1516
- }
1517
-
1518
- if (command === 'setup') {
1519
- await runSetup(rawArgs.slice(1));
1520
- return;
1521
- }
1522
-
1523
- if (command !== 'init') {
1524
- console.error(pc.red(`Unknown command: ${command}`));
1525
- printHelp();
1526
- process.exit(1);
1527
- }
1528
-
1529
- const assumeYes = args.includes('--yes') || args.includes('-y');
1530
- const skipMcpSetup = args.includes('--no-setup-mcp') || process.env.THACHVD_KIT_SKIP_MCP_SETUP === '1';
1531
-
1532
- console.log(pc.bold(pc.cyan('='.repeat(50))));
1533
- console.log(' ' + pc.bold('thachvd-kit') + ' - AI Project Bootstrap');
1534
- console.log(' Generates shared agent rules and configures MCP tooling');
1535
- console.log(pc.bold(pc.cyan('='.repeat(50))));
1536
-
1537
- console.log(pc.dim(' Scanning current project files for a baseline...\n'));
1538
- const scanned = scanProject();
1539
-
1540
- console.log(` ${pc.bold('Detected context:')}`);
1541
- formatScanPreviewLines(scanned).forEach(([label, value]) => {
1542
- console.log(` - ${label}: ${value}`);
1543
- });
1544
- console.log('');
1545
- if ((scanned.scan_evidence || []).length > 0) {
1546
- console.log(` ${pc.bold('Evidence:')}`);
1547
- scanned.scan_evidence.forEach(line => console.log(` - ${line}`));
1548
- console.log('');
1549
- }
1550
-
1551
- // Combine data
1552
- const data = {
1553
- project_name: scanned?.project_name || path.basename(targetDir),
1554
- description: scanned?.description || 'A software project',
1555
- project_type: scanned?.project_type || 'web-app',
1556
- primary_language: scanned?.primary_language || 'typescript',
1557
- frameworks: scanned?.frameworks || [],
1558
- database: scanned?.database || 'none',
1559
- uses_docker: scanned?.uses_docker ?? true,
1560
- cloud: scanned?.cloud || 'none',
1561
- package_manager: scanned?.package_manager || 'auto-detect',
1562
- test_framework: scanned?.test_framework || 'auto-detect',
1563
- max_file_lines: scanned?.max_file_lines || '300'
1564
- };
1565
-
1566
- data.database = data.database || scanned?.database || 'none';
1567
- data.uses_docker = typeof data.uses_docker === 'boolean' ? data.uses_docker : (scanned?.uses_docker ?? true);
1568
- data.cloud = data.cloud || scanned?.cloud || 'none';
1569
- data.package_manager = data.package_manager || scanned?.package_manager || 'auto-detect';
1570
- data.test_framework = data.test_framework || scanned?.test_framework || 'auto-detect';
1571
- data.max_file_lines = data.max_file_lines || scanned?.max_file_lines || '300';
1572
- data.framework_details = scanned?.framework_details || [];
1573
- data.scan_evidence = scanned?.scan_evidence || [];
1574
- data.app_root = scanned?.app_root || '.';
1575
-
1576
- data.infrastructure = [];
1577
- if (data.uses_docker) data.infrastructure.push('docker');
1578
- if (data.cloud !== 'none') data.infrastructure.push(data.cloud);
1579
-
1580
- const shouldSetupMcp = args.includes('--setup-mcp') && !skipMcpSetup;
1581
-
1582
- console.log(`\n${pc.bold(pc.cyan('Generating Files'))}`);
1583
-
1584
- // Shared entry files are written after the .agent folder is available.
1585
-
1586
- console.log(` ${pc.green('OK')} Project context generation enabled; workflow skills remain client-managed by Superpowers`);
1587
-
1588
-
1589
- const generatedFiles = [
1590
- ['AGENTS.md', generateSharedAgentsMd(data)],
1591
- ['CLAUDE.md', generateSharedClaudeMd()],
1592
- ['GEMINI.md', generateSharedGeminiMd()],
1593
- ['.cursorrules', generateSharedCursorrules(data)],
1594
- [path.join('.agent', 'docs', 'project.md'), generateProjectDoc(data)],
1595
- [path.join('.agent', 'docs', 'architecture.md'), generateArchitectureDoc(data)],
1596
- [path.join('.agent', 'docs', 'conventions.md'), generateConventionsDoc(data)],
1597
- [path.join('.agent', 'docs', 'workflow.md'), generateWorkflowDoc()],
1598
- [path.join('.agent', 'docs', 'tooling.md'), generateToolingDoc(data)],
1599
- [path.join('.agent', 'docs', 'getting-started.md'), generateGettingStartedDoc()],
1600
- [path.join('.agent', 'docs', 'index-project-prompt.md'), generateIndexProjectPrompt()]
1601
- ];
1602
-
1603
- for (const [relativePath, content] of generatedFiles) {
1604
- const wrote = await writeGeneratedFile(relativePath, content, assumeYes);
1605
- if (wrote) console.log(` ${pc.green('OK')} Generated ${pc.bold(relativePath)}`);
1606
- else console.log(` ${pc.yellow('~')} Skipped ${pc.bold(relativePath)}`);
1607
- }
1608
-
1609
- if (shouldSetupMcp) {
1610
- console.log(`\n${pc.bold(pc.cyan('Configuring MCP'))}`);
1611
- for (const result of setupMcpServers()) {
1612
- const marker = result.ok ? pc.green('OK') : pc.yellow('~');
1613
- console.log(` ${marker} ${result.message}`);
1614
- }
1615
- }
1616
-
1617
- console.log(`\n${pc.bold(pc.green('Done!'))}
1618
-
1619
- ${pc.bold('Project:')} ${data.project_name}
1620
- ${pc.bold('Stack:')} ${data.primary_language} | ${(data.frameworks || []).join(', ') || 'no framework'}
1621
-
1622
- ${pc.bold('Generated:')}
1623
- - ${pc.bold('AGENTS.md')} shared entry for Codex, Antigravity, Claude Code, and Cursor
1624
- - ${pc.bold('CLAUDE.md')} Claude Code entry that imports AGENTS.md
1625
- - ${pc.bold('GEMINI.md')} Antigravity entry
1626
- - ${pc.bold('.cursorrules')} Cursor entry
1627
- - ${pc.bold('.agent/docs/')} scan-based project rules and local operating notes
1628
- - ${pc.bold('.agent/docs/index-project-prompt.md')} durable onboarding prompt
1629
-
1630
- ${pc.bold('MCP/tooling setup:')}
1631
- - Superpowers is the workflow backend; install it from the official client/plugin surface
1632
- - Run ${pc.bold('thachvd-kit setup')} to configure codebase-memory MCP and recommended RTK
1633
-
1634
- ${pc.bold('Next steps:')}
1635
- 1. Run ${pc.bold('thachvd-kit prompt')} and paste the saved prompt into your AI editor
1636
- 2. Install Superpowers and RTK, then run ${pc.bold('thachvd-kit setup')}
1637
- 3. Run ${pc.bold('thachvd-kit doctor')} to verify the machine
1638
- 4. Re-run ${pc.bold('thachvd-kit prompt')} whenever you need the onboarding prompt again
1639
-
1640
- ${pc.bold('Onboarding prompt saved to .agent/docs/index-project-prompt.md:')}
1641
- ${pc.dim('---')}
1642
- ${generateIndexProjectPrompt().trim()}
1643
- ${pc.dim('---')}
1644
- `);
1645
- }
1646
-
1647
- main().catch(err => {
1648
- console.error(pc.red('\nError during init:'), err);
1649
- process.exit(1);
1650
- });
1651
-
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { spawnSync } = require('child_process');
6
+ const prompts = require('prompts');
7
+ const pc = require('picocolors');
8
+ const packageJson = require('../package.json');
9
+
10
+ const sourceDir = path.resolve(__dirname, '..');
11
+ const targetDir = process.cwd();
12
+ const CODEBASE_MEMORY_PACKAGE = 'codebase-memory-mcp';
13
+ const CODEBASE_MEMORY_BINARY = 'codebase-memory-mcp';
14
+ const RTK_INSTALL_COMMAND = 'cargo install --git https://github.com/rtk-ai/rtk --branch master rtk';
15
+
16
+ const FRAMEWORK_CHOICES = [
17
+ 'vue', 'react', 'next', 'nuxt', 'svelte', 'angular',
18
+ 'laravel', 'cakephp', 'symfony', 'codeigniter',
19
+ 'express', 'fastapi', 'django', 'flask', 'gin', 'fiber', 'echo', 'nestjs',
20
+ 'rails', 'spring', 'aspnet',
21
+ 'tauri', 'axum', 'actix', 'flutter', 'react-native'
22
+ ];
23
+
24
+ const LANGUAGE_CHOICES = ['typescript', 'javascript', 'python', 'go', 'php', 'rust', 'java', 'ruby', 'swift', 'kotlin', 'csharp', 'cpp'];
25
+ const PROJECT_TYPE_CHOICES = ['web-app', 'api', 'mobile', 'desktop', 'cli', 'fullstack'];
26
+ const DATABASE_CHOICES = ['postgres', 'mysql', 'sqlite', 'mongodb', 'redis', 'dynamodb', 'none'];
27
+ const PACKAGE_MANAGER_CHOICES = ['npm', 'pnpm', 'yarn', 'bun', 'pip', 'uv', 'go mod', 'cargo', 'composer', 'auto-detect'];
28
+ const TEST_FRAMEWORK_CHOICES = ['jest', 'vitest', 'pytest', 'go test', 'phpunit', 'pest', 'rspec', 'junit', 'auto-detect'];
29
+ const CLOUD_CHOICES = ['aws', 'gcp', 'azure', 'vercel', 'fly', 'railway', 'none'];
30
+ const FRONTEND_FRAMEWORKS = ['vue', 'react', 'next', 'nuxt', 'svelte', 'angular'];
31
+ const BACKEND_FRAMEWORKS = ['laravel', 'cakephp', 'symfony', 'codeigniter', 'express', 'fastapi', 'django', 'flask', 'gin', 'fiber', 'echo', 'nestjs', 'rails', 'spring', 'aspnet', 'axum', 'actix'];
32
+ const MOBILE_DESKTOP_FRAMEWORKS = ['tauri', 'flutter', 'react-native'];
33
+ const IGNORED_SCAN_DIRS = new Set(['.git', 'node_modules', 'vendor', 'dist', 'build', 'coverage', '.next', '.nuxt', '.turbo', '.cache', 'storage', 'tmp']);
34
+
35
+ function printHelp() {
36
+ console.log(`
37
+ ${pc.bold('Usage:')}
38
+ thachvd-kit init [--yes]
39
+ thachvd-kit setup [--yes]
40
+ thachvd-kit doctor
41
+ thachvd-kit prompt
42
+ thachvd-kit --version
43
+ thachvd-kit --help
44
+
45
+ ${pc.bold('Commands:')}
46
+ init Generate project context files and documentation
47
+ setup Install/configure MCP and recommended local tooling
48
+ doctor Check project context and local integrations
49
+ prompt Print the saved project-indexing prompt again
50
+
51
+ ${pc.bold('Options:')}
52
+ --yes Overwrite generated files or accept setup defaults
53
+ --no-setup-mcp Skip MCP setup (legacy init compatibility)
54
+ --no-setup-hook Skip the Claude Code Definition-of-Done Stop hook during setup
55
+ --no-install-rtk Skip auto-installing rtk via cargo during setup
56
+ --no-index Skip auto-indexing the repository with codebase-memory-mcp during setup
57
+ --version Show the installed CLI version
58
+ --help Show this help message
59
+
60
+ ${pc.bold('What gets generated:')}
61
+ AGENTS.md Shared instructions for Codex, Antigravity, Claude Code, and Cursor
62
+ CLAUDE.md Claude Code entry file that imports AGENTS.md
63
+ GEMINI.md Antigravity entry file that points to AGENTS.md
64
+ .cursorrules Cursor entry file that points to AGENTS.md
65
+ .agent/docs/ Project context, architecture, workflow, tooling, and durable onboarding prompt
66
+
67
+ ${pc.bold('Workflow guide:')}
68
+ Every task Superpowers Use the matching native Superpowers skill
69
+ Idea is fuzzy brainstorming Clarify intent and tradeoffs before coding
70
+ Scope needs a contract writing-plans Create a detailed plan before implementation
71
+ Execute an approved plan executing-plans Follow the approved plan with verification
72
+ Bug or failing behavior systematic-debugging Find the root cause before editing
73
+ Pre-merge review requesting-code-review Review before completion or merge
74
+ UI / UX work $frontend-design, $webapp-testing
75
+ Project state thachvd-kit doctor
76
+
77
+ ${pc.bold('Tip:')}
78
+ Install Superpowers separately for each AI client from its official plugin surface.
79
+ Run thachvd-kit setup for the integration checklist and doctor to verify it.
80
+ `);
81
+ }
82
+
83
+ function formatScanPreviewLines(scanned) {
84
+ return [
85
+ ['App root', scanned.app_root || '.'],
86
+ ['Project type', scanned.project_type || 'unknown'],
87
+ ['Primary language', scanned.primary_language || 'unknown'],
88
+ ['Frameworks', (scanned.frameworks || []).join(', ') || 'no framework'],
89
+ ['Database', scanned.database || 'none'],
90
+ ['Package manager', scanned.package_manager || 'auto-detect'],
91
+ ['Test framework', scanned.test_framework || 'auto-detect'],
92
+ ['Cloud', scanned.cloud || 'none'],
93
+ ['Infrastructure', (scanned.infrastructure || []).join(', ') || 'none'],
94
+ ['CI/CD', scanned.ci_cd || 'none']
95
+ ];
96
+ }
97
+
98
+ async function askQuestions(questions) {
99
+ return prompts(questions, {
100
+ onCancel: () => {
101
+ console.log(pc.yellow('\nCancelled.'));
102
+ process.exit(130);
103
+ }
104
+ });
105
+ }
106
+
107
+ function fileExists(...parts) {
108
+ return fs.existsSync(path.join(targetDir, ...parts));
109
+ }
110
+
111
+ function fileExistsAt(baseDir, ...parts) {
112
+ return fs.existsSync(path.join(baseDir, ...parts));
113
+ }
114
+
115
+ function readJsonIfExists(...parts) {
116
+ const fullPath = path.join(targetDir, ...parts);
117
+ if (!fs.existsSync(fullPath)) return null;
118
+ try {
119
+ return JSON.parse(fs.readFileSync(fullPath, 'utf8'));
120
+ } catch {
121
+ return null;
122
+ }
123
+ }
124
+
125
+ function readJsonIfExistsAt(baseDir, ...parts) {
126
+ const fullPath = path.join(baseDir, ...parts);
127
+ if (!fs.existsSync(fullPath)) return null;
128
+ try {
129
+ return JSON.parse(fs.readFileSync(fullPath, 'utf8'));
130
+ } catch {
131
+ return null;
132
+ }
133
+ }
134
+
135
+ function readTextIfExists(...parts) {
136
+ const fullPath = path.join(targetDir, ...parts);
137
+ if (!fs.existsSync(fullPath)) return '';
138
+ try {
139
+ return fs.readFileSync(fullPath, 'utf8');
140
+ } catch {
141
+ return '';
142
+ }
143
+ }
144
+
145
+ function readTextIfExistsAt(baseDir, ...parts) {
146
+ const fullPath = path.join(baseDir, ...parts);
147
+ if (!fs.existsSync(fullPath)) return '';
148
+ try {
149
+ return fs.readFileSync(fullPath, 'utf8');
150
+ } catch {
151
+ return '';
152
+ }
153
+ }
154
+
155
+ function detectPackageManager(baseDir = targetDir) {
156
+ if (fileExistsAt(baseDir, 'pnpm-lock.yaml')) return 'pnpm';
157
+ if (fileExistsAt(baseDir, 'yarn.lock')) return 'yarn';
158
+ if (fileExistsAt(baseDir, 'bun.lockb') || fileExistsAt(baseDir, 'bun.lock')) return 'bun';
159
+ if (fileExistsAt(baseDir, 'package-lock.json')) return 'npm';
160
+ if (fileExistsAt(baseDir, 'uv.lock')) return 'uv';
161
+ if (fileExistsAt(baseDir, 'poetry.lock')) return 'pip';
162
+ if (fileExistsAt(baseDir, 'requirements.txt') || fileExistsAt(baseDir, 'pyproject.toml')) return 'pip';
163
+ if (fileExistsAt(baseDir, 'go.mod')) return 'go mod';
164
+ if (fileExistsAt(baseDir, 'Cargo.toml')) return 'cargo';
165
+ if (fileExistsAt(baseDir, 'composer.json')) return 'composer';
166
+ return 'auto-detect';
167
+ }
168
+
169
+ function uniq(values) {
170
+ return Array.from(new Set(values.filter(Boolean)));
171
+ }
172
+
173
+ function detectAppRoot(baseDir) {
174
+ const candidates = [baseDir];
175
+ const queue = [{ dir: baseDir, depth: 0 }];
176
+
177
+ while (queue.length > 0) {
178
+ const { dir, depth } = queue.shift();
179
+ if (depth >= 2) continue;
180
+ let entries = [];
181
+ try {
182
+ entries = fs.readdirSync(dir, { withFileTypes: true });
183
+ } catch {
184
+ continue;
185
+ }
186
+ for (const entry of entries) {
187
+ if (!entry.isDirectory()) continue;
188
+ if (IGNORED_SCAN_DIRS.has(entry.name)) continue;
189
+ const nextDir = path.join(dir, entry.name);
190
+ candidates.push(nextDir);
191
+ queue.push({ dir: nextDir, depth: depth + 1 });
192
+ }
193
+ }
194
+
195
+ const scoreCandidate = dir => {
196
+ let score = 0;
197
+ if (fileExistsAt(dir, 'composer.json')) score += 5;
198
+ if (fileExistsAt(dir, 'package.json')) score += 4;
199
+ if (fileExistsAt(dir, 'Cargo.toml')) score += 4;
200
+ if (fileExistsAt(dir, 'go.mod')) score += 4;
201
+ if (fileExistsAt(dir, 'pyproject.toml') || fileExistsAt(dir, 'requirements.txt')) score += 4;
202
+ if (fileExistsAt(dir, 'artisan')) score += 5;
203
+ if (fileExistsAt(dir, 'bin', 'cake') || fileExistsAt(dir, 'app', 'Config', 'core.php')) score += 5;
204
+ if (fileExistsAt(dir, 'src-tauri', 'tauri.conf.json') || fileExistsAt(dir, 'src-tauri', 'tauri.conf.json5')) score += 5;
205
+ if (fileExistsAt(dir, 'manage.py')) score += 4;
206
+ if (fileExistsAt(dir, 'Gemfile')) score += 4;
207
+ if (fileExistsAt(dir, 'pom.xml') || fileExistsAt(dir, 'build.gradle') || fileExistsAt(dir, 'build.gradle.kts')) score += 4;
208
+ if (fileExistsAt(dir, 'app', 'View') || fileExistsAt(dir, 'resources', 'views') || fileExistsAt(dir, 'templates') || fileExistsAt(dir, 'app', 'views')) score += 2;
209
+ return score;
210
+ };
211
+
212
+ const ranked = candidates
213
+ .map(dir => ({ dir, score: scoreCandidate(dir) }))
214
+ .filter(item => item.score > 0)
215
+ .sort((a, b) => (b.score - a.score) || (a.dir.length - b.dir.length));
216
+
217
+ return ranked[0]?.dir || baseDir;
218
+ }
219
+
220
+ function repoHasAny(baseDir, relativePaths, maxDepth = 3) {
221
+ const queue = [{ dir: baseDir, depth: 0 }];
222
+ while (queue.length > 0) {
223
+ const { dir, depth } = queue.shift();
224
+ for (const relativePath of relativePaths) {
225
+ if (fileExistsAt(dir, ...relativePath)) return true;
226
+ }
227
+ if (depth >= maxDepth) continue;
228
+ let entries = [];
229
+ try {
230
+ entries = fs.readdirSync(dir, { withFileTypes: true });
231
+ } catch {
232
+ continue;
233
+ }
234
+ for (const entry of entries) {
235
+ if (!entry.isDirectory()) continue;
236
+ if (IGNORED_SCAN_DIRS.has(entry.name)) continue;
237
+ queue.push({ dir: path.join(dir, entry.name), depth: depth + 1 });
238
+ }
239
+ }
240
+ return false;
241
+ }
242
+
243
+ function evidenceIf(condition, label, weight = 1) {
244
+ return condition ? { label, weight } : null;
245
+ }
246
+
247
+ function buildDetection(id, evidence, meta = {}) {
248
+ const hits = evidence.filter(Boolean);
249
+ const score = hits.reduce((sum, hit) => sum + hit.weight, 0);
250
+ return {
251
+ id,
252
+ score,
253
+ evidence: hits.map(hit => hit.label),
254
+ ...meta
255
+ };
256
+ }
257
+
258
+ function confidenceLabel(score) {
259
+ if (score >= 6) return 'high';
260
+ if (score >= 3) return 'medium';
261
+ return 'low';
262
+ }
263
+
264
+ function hasDependency(depNames, ...names) {
265
+ return names.some(name => depNames.has(name));
266
+ }
267
+
268
+ function hasComposerDependency(composerDepNames, ...names) {
269
+ return names.some(name => composerDepNames.has(name));
270
+ }
271
+
272
+ function hasRootFile(rootEntries, ...filenames) {
273
+ return filenames.some(name => rootEntries.has(name));
274
+ }
275
+
276
+ function textMatches(text, pattern) {
277
+ return Boolean(text && pattern.test(text));
278
+ }
279
+
280
+ const LANGUAGE_DETECTORS = [
281
+ {
282
+ id: 'typescript',
283
+ detect: ctx => buildDetection('typescript', [
284
+ evidenceIf(ctx.fileExists('tsconfig.json'), 'found tsconfig.json', 4),
285
+ evidenceIf(hasDependency(ctx.depNames, 'typescript', 'ts-node', 'tsx'), 'package.json -> typescript toolchain', 2)
286
+ ])
287
+ },
288
+ {
289
+ id: 'javascript',
290
+ detect: ctx => buildDetection('javascript', [
291
+ evidenceIf(ctx.fileExists('package.json'), 'found package.json', 2),
292
+ evidenceIf(ctx.depNames.size > 0, 'package.json -> JavaScript dependency graph', 1)
293
+ ])
294
+ },
295
+ {
296
+ id: 'python',
297
+ detect: ctx => buildDetection('python', [
298
+ evidenceIf(ctx.fileExists('requirements.txt'), 'found requirements.txt', 3),
299
+ evidenceIf(ctx.fileExists('pyproject.toml'), 'found pyproject.toml', 2),
300
+ evidenceIf(textMatches(ctx.pyprojectText, /\[tool\.(poetry|uv|hatch|pdm)\]/i), 'pyproject.toml -> Python tool section', 2)
301
+ ])
302
+ },
303
+ {
304
+ id: 'go',
305
+ detect: ctx => buildDetection('go', [
306
+ evidenceIf(ctx.fileExists('go.mod'), 'found go.mod', 4)
307
+ ])
308
+ },
309
+ {
310
+ id: 'php',
311
+ detect: ctx => buildDetection('php', [
312
+ evidenceIf(ctx.fileExists('composer.json'), 'found composer.json', 4),
313
+ evidenceIf(ctx.composerDepNames.size > 0, 'composer.json -> PHP dependency graph', 1)
314
+ ])
315
+ },
316
+ {
317
+ id: 'rust',
318
+ detect: ctx => buildDetection('rust', [
319
+ evidenceIf(ctx.fileExists('Cargo.toml'), 'found Cargo.toml', 4),
320
+ evidenceIf(ctx.fileExists('src-tauri', 'Cargo.toml'), 'found src-tauri/Cargo.toml', 5)
321
+ ])
322
+ },
323
+ {
324
+ id: 'java',
325
+ detect: ctx => buildDetection('java', [
326
+ evidenceIf(ctx.fileExists('pom.xml'), 'found pom.xml', 4),
327
+ evidenceIf(ctx.fileExists('build.gradle') || ctx.fileExists('build.gradle.kts'), 'found Gradle build file', 3)
328
+ ])
329
+ },
330
+ {
331
+ id: 'ruby',
332
+ detect: ctx => buildDetection('ruby', [
333
+ evidenceIf(ctx.fileExists('Gemfile'), 'found Gemfile', 4),
334
+ evidenceIf(ctx.fileExists('config.ru'), 'found config.ru', 1)
335
+ ])
336
+ },
337
+ {
338
+ id: 'csharp',
339
+ detect: ctx => buildDetection('csharp', [
340
+ evidenceIf(ctx.rootEntries.some(name => name.endsWith('.sln') || name.endsWith('.csproj')), 'found .sln/.csproj', 4),
341
+ evidenceIf(ctx.fileExists('Program.cs'), 'found Program.cs', 1)
342
+ ])
343
+ }
344
+ ];
345
+
346
+ const FRAMEWORK_DETECTORS = [
347
+ {
348
+ id: 'next',
349
+ kind: 'frontend',
350
+ projectType: 'web-app',
351
+ detect: ctx => buildDetection('next', [
352
+ evidenceIf(hasDependency(ctx.depNames, 'next'), 'package.json -> next', 4),
353
+ evidenceIf(ctx.fileExists('next.config.js') || ctx.fileExists('next.config.mjs') || ctx.fileExists('next.config.ts'), 'found next.config.*', 3),
354
+ evidenceIf(ctx.fileExists('app') || ctx.fileExists('pages'), 'found app/ or pages/', 1)
355
+ ])
356
+ },
357
+ {
358
+ id: 'react',
359
+ kind: 'frontend',
360
+ projectType: 'web-app',
361
+ detect: ctx => buildDetection('react', [
362
+ evidenceIf(hasDependency(ctx.depNames, 'react'), 'package.json -> react', 4),
363
+ evidenceIf(hasDependency(ctx.depNames, 'react-dom'), 'package.json -> react-dom', 2)
364
+ ])
365
+ },
366
+ {
367
+ id: 'vue',
368
+ kind: 'frontend',
369
+ projectType: 'web-app',
370
+ detect: ctx => buildDetection('vue', [
371
+ evidenceIf(hasDependency(ctx.depNames, 'vue'), 'package.json -> vue', 4)
372
+ ])
373
+ },
374
+ {
375
+ id: 'nuxt',
376
+ kind: 'frontend',
377
+ projectType: 'web-app',
378
+ detect: ctx => buildDetection('nuxt', [
379
+ evidenceIf(hasDependency(ctx.depNames, 'nuxt'), 'package.json -> nuxt', 4),
380
+ evidenceIf(ctx.fileExists('nuxt.config.ts') || ctx.fileExists('nuxt.config.js'), 'found nuxt.config.*', 3)
381
+ ])
382
+ },
383
+ {
384
+ id: 'svelte',
385
+ kind: 'frontend',
386
+ projectType: 'web-app',
387
+ detect: ctx => buildDetection('svelte', [
388
+ evidenceIf(hasDependency(ctx.depNames, 'svelte', '@sveltejs/kit'), 'package.json -> svelte', 4)
389
+ ])
390
+ },
391
+ {
392
+ id: 'angular',
393
+ kind: 'frontend',
394
+ projectType: 'web-app',
395
+ detect: ctx => buildDetection('angular', [
396
+ evidenceIf(hasDependency(ctx.depNames, '@angular/core'), 'package.json -> @angular/core', 4),
397
+ evidenceIf(ctx.fileExists('angular.json'), 'found angular.json', 3)
398
+ ])
399
+ },
400
+ {
401
+ id: 'express',
402
+ kind: 'backend',
403
+ language: 'javascript',
404
+ projectType: 'api',
405
+ detect: ctx => buildDetection('express', [
406
+ evidenceIf(hasDependency(ctx.depNames, 'express'), 'package.json -> express', 4)
407
+ ])
408
+ },
409
+ {
410
+ id: 'nestjs',
411
+ kind: 'backend',
412
+ language: 'javascript',
413
+ projectType: 'api',
414
+ detect: ctx => buildDetection('nestjs', [
415
+ evidenceIf(hasDependency(ctx.depNames, '@nestjs/core'), 'package.json -> @nestjs/core', 4)
416
+ ])
417
+ },
418
+ {
419
+ id: 'fastapi',
420
+ kind: 'backend',
421
+ language: 'python',
422
+ projectType: 'api',
423
+ detect: ctx => buildDetection('fastapi', [
424
+ evidenceIf(textMatches(ctx.requirementsText, /\bfastapi\b/i) || textMatches(ctx.pyprojectText, /\bfastapi\b/i), 'Python deps -> fastapi', 4)
425
+ ])
426
+ },
427
+ {
428
+ id: 'django',
429
+ kind: 'backend',
430
+ language: 'python',
431
+ projectType: 'web-app',
432
+ detect: ctx => buildDetection('django', [
433
+ evidenceIf(textMatches(ctx.requirementsText, /\bdjango\b/i) || textMatches(ctx.pyprojectText, /\bdjango\b/i), 'Python deps -> django', 4),
434
+ evidenceIf(ctx.fileExists('manage.py'), 'found manage.py', 2),
435
+ evidenceIf(ctx.fileExists('templates'), 'found templates/', 1)
436
+ ])
437
+ },
438
+ {
439
+ id: 'flask',
440
+ kind: 'backend',
441
+ language: 'python',
442
+ projectType: 'api',
443
+ detect: ctx => buildDetection('flask', [
444
+ evidenceIf(textMatches(ctx.requirementsText, /\bflask\b/i) || textMatches(ctx.pyprojectText, /\bflask\b/i), 'Python deps -> flask', 4)
445
+ ])
446
+ },
447
+ {
448
+ id: 'laravel',
449
+ kind: 'backend',
450
+ language: 'php',
451
+ projectType: 'web-app',
452
+ detect: ctx => buildDetection('laravel', [
453
+ evidenceIf(hasComposerDependency(ctx.composerDepNames, 'laravel/framework'), 'composer.json -> laravel/framework', 4),
454
+ evidenceIf(ctx.fileExists('artisan'), 'found artisan', 3),
455
+ evidenceIf(ctx.fileExists('resources', 'views'), 'found resources/views', 1)
456
+ ])
457
+ },
458
+ {
459
+ id: 'cakephp',
460
+ kind: 'backend',
461
+ language: 'php',
462
+ projectType: 'web-app',
463
+ detect: ctx => buildDetection('cakephp', [
464
+ evidenceIf(hasComposerDependency(ctx.composerDepNames, 'cakephp/cakephp'), 'composer.json -> cakephp/cakephp', 4),
465
+ evidenceIf(ctx.fileExists('bin', 'cake'), 'found bin/cake', 3),
466
+ evidenceIf(ctx.fileExists('config', 'app.php'), 'found config/app.php', 2),
467
+ evidenceIf(ctx.fileExists('app', 'Config', 'core.php'), 'found app/Config/core.php (CakePHP legacy)', 3),
468
+ evidenceIf(ctx.fileExists('app', 'Controller', 'AppController.php'), 'found app/Controller/AppController.php', 2),
469
+ evidenceIf(ctx.fileExists('app', 'Model', 'AppModel.php'), 'found app/Model/AppModel.php', 2),
470
+ evidenceIf(ctx.fileExists('lib', 'Cake'), 'found lib/Cake', 3),
471
+ evidenceIf(ctx.fileExists('cake', 'console', 'cake'), 'found cake/console/cake', 3),
472
+ evidenceIf(ctx.fileExists('app', 'View'), 'found app/View', 2),
473
+ evidenceIf(textMatches(ctx.viewFilesText, /\.ctp$/im), 'found .ctp view templates', 2)
474
+ ])
475
+ },
476
+ {
477
+ id: 'symfony',
478
+ kind: 'backend',
479
+ language: 'php',
480
+ projectType: 'web-app',
481
+ detect: ctx => buildDetection('symfony', [
482
+ evidenceIf(hasComposerDependency(ctx.composerDepNames, 'symfony/framework-bundle'), 'composer.json -> symfony/framework-bundle', 4),
483
+ evidenceIf(ctx.fileExists('bin', 'console'), 'found bin/console', 2),
484
+ evidenceIf(ctx.fileExists('config', 'bundles.php'), 'found config/bundles.php', 1),
485
+ evidenceIf(ctx.fileExists('templates'), 'found templates/', 1)
486
+ ])
487
+ },
488
+ {
489
+ id: 'codeigniter',
490
+ kind: 'backend',
491
+ language: 'php',
492
+ projectType: 'api',
493
+ detect: ctx => buildDetection('codeigniter', [
494
+ evidenceIf(hasComposerDependency(ctx.composerDepNames, 'codeigniter4/framework'), 'composer.json -> codeigniter4/framework', 4),
495
+ evidenceIf(ctx.fileExists('spark'), 'found spark', 2),
496
+ evidenceIf(ctx.fileExists('app', 'Config', 'App.php'), 'found app/Config/App.php', 1)
497
+ ])
498
+ },
499
+ {
500
+ id: 'gin',
501
+ kind: 'backend',
502
+ language: 'go',
503
+ projectType: 'api',
504
+ detect: ctx => buildDetection('gin', [
505
+ evidenceIf(textMatches(ctx.goModText, /\bgithub\.com\/gin-gonic\/gin\b/i), 'go.mod -> github.com/gin-gonic/gin', 4)
506
+ ])
507
+ },
508
+ {
509
+ id: 'fiber',
510
+ kind: 'backend',
511
+ language: 'go',
512
+ projectType: 'api',
513
+ detect: ctx => buildDetection('fiber', [
514
+ evidenceIf(textMatches(ctx.goModText, /\bgithub\.com\/gofiber\/fiber\b/i), 'go.mod -> github.com/gofiber/fiber', 4)
515
+ ])
516
+ },
517
+ {
518
+ id: 'echo',
519
+ kind: 'backend',
520
+ language: 'go',
521
+ projectType: 'api',
522
+ detect: ctx => buildDetection('echo', [
523
+ evidenceIf(textMatches(ctx.goModText, /\bgithub\.com\/labstack\/echo\b/i), 'go.mod -> github.com/labstack/echo', 4)
524
+ ])
525
+ },
526
+ {
527
+ id: 'rails',
528
+ kind: 'backend',
529
+ language: 'ruby',
530
+ projectType: 'web-app',
531
+ detect: ctx => buildDetection('rails', [
532
+ evidenceIf(textMatches(ctx.gemfileText, /^\s*gem\s+["']rails["']/im), 'Gemfile -> rails', 4),
533
+ evidenceIf(ctx.fileExists('bin', 'rails'), 'found bin/rails', 2),
534
+ evidenceIf(ctx.fileExists('app', 'views'), 'found app/views', 1)
535
+ ])
536
+ },
537
+ {
538
+ id: 'spring',
539
+ kind: 'backend',
540
+ language: 'java',
541
+ projectType: 'api',
542
+ detect: ctx => buildDetection('spring', [
543
+ evidenceIf(textMatches(ctx.pomText, /spring-boot/i) || textMatches(ctx.gradleText, /spring-boot/i), 'Java build -> spring-boot', 4)
544
+ ])
545
+ },
546
+ {
547
+ id: 'aspnet',
548
+ kind: 'backend',
549
+ language: 'csharp',
550
+ projectType: 'api',
551
+ detect: ctx => buildDetection('aspnet', [
552
+ evidenceIf(textMatches(ctx.csprojText, /Microsoft\.NET\.Sdk\.Web/i), '.csproj -> Microsoft.NET.Sdk.Web', 4),
553
+ evidenceIf(textMatches(ctx.programCsText, /WebApplication\.CreateBuilder/i), 'Program.cs -> WebApplication.CreateBuilder', 2)
554
+ ])
555
+ },
556
+ {
557
+ id: 'tauri',
558
+ kind: 'desktop',
559
+ language: 'rust',
560
+ projectType: 'desktop',
561
+ detect: ctx => buildDetection('tauri', [
562
+ evidenceIf(hasDependency(ctx.depNames, '@tauri-apps/api'), 'package.json -> @tauri-apps/api', 4),
563
+ evidenceIf(ctx.fileExists('src-tauri', 'tauri.conf.json') || ctx.fileExists('src-tauri', 'tauri.conf.json5'), 'found src-tauri/tauri.conf.*', 4),
564
+ evidenceIf(ctx.fileExists('src-tauri', 'Cargo.toml'), 'found src-tauri/Cargo.toml', 2)
565
+ ])
566
+ },
567
+ {
568
+ id: 'axum',
569
+ kind: 'backend',
570
+ language: 'rust',
571
+ projectType: 'api',
572
+ detect: ctx => buildDetection('axum', [
573
+ evidenceIf(textMatches(ctx.cargoText, /\baxum\b/i), 'Cargo.toml -> axum', 4)
574
+ ])
575
+ },
576
+ {
577
+ id: 'actix',
578
+ kind: 'backend',
579
+ language: 'rust',
580
+ projectType: 'api',
581
+ detect: ctx => buildDetection('actix', [
582
+ evidenceIf(textMatches(ctx.cargoText, /\bactix-web\b/i), 'Cargo.toml -> actix-web', 4)
583
+ ])
584
+ },
585
+ {
586
+ id: 'flutter',
587
+ kind: 'mobile',
588
+ projectType: 'mobile',
589
+ detect: ctx => buildDetection('flutter', [
590
+ evidenceIf(textMatches(ctx.pubspecText, /^\s*flutter\s*:/im), 'pubspec.yaml -> flutter', 4)
591
+ ])
592
+ },
593
+ {
594
+ id: 'react-native',
595
+ kind: 'mobile',
596
+ projectType: 'mobile',
597
+ detect: ctx => buildDetection('react-native', [
598
+ evidenceIf(hasDependency(ctx.depNames, 'react-native', 'expo'), 'package.json -> react-native/expo', 4)
599
+ ])
600
+ }
601
+ ];
602
+
603
+ function scanProject() {
604
+ const appRoot = detectAppRoot(targetDir);
605
+ const pkg = readJsonIfExistsAt(appRoot, 'package.json') || {};
606
+ const composer = readJsonIfExistsAt(appRoot, 'composer.json') || {};
607
+ const rootEntries = fs.existsSync(appRoot) ? fs.readdirSync(appRoot) : [];
608
+ const rootCsproj = rootEntries.find(name => name.endsWith('.csproj')) || '';
609
+ const deps = {
610
+ ...(pkg.dependencies || {}),
611
+ ...(pkg.devDependencies || {}),
612
+ ...(pkg.peerDependencies || {})
613
+ };
614
+ const depNames = new Set(Object.keys(deps));
615
+ const composerDeps = {
616
+ ...(composer.require || {}),
617
+ ...(composer['require-dev'] || {})
618
+ };
619
+ const composerDepNames = new Set(Object.keys(composerDeps));
620
+ const ctx = {
621
+ pkg,
622
+ composer,
623
+ depNames,
624
+ composerDepNames,
625
+ rootEntries,
626
+ fileExists: (...parts) => fileExistsAt(appRoot, ...parts),
627
+ requirementsText: readTextIfExistsAt(appRoot, 'requirements.txt'),
628
+ pyprojectText: readTextIfExistsAt(appRoot, 'pyproject.toml'),
629
+ goModText: readTextIfExistsAt(appRoot, 'go.mod'),
630
+ pubspecText: readTextIfExistsAt(appRoot, 'pubspec.yaml'),
631
+ gemfileText: readTextIfExistsAt(appRoot, 'Gemfile'),
632
+ pomText: readTextIfExistsAt(appRoot, 'pom.xml'),
633
+ gradleText: `${readTextIfExistsAt(appRoot, 'build.gradle')}\n${readTextIfExistsAt(appRoot, 'build.gradle.kts')}`,
634
+ cargoText: `${readTextIfExistsAt(appRoot, 'Cargo.toml')}\n${readTextIfExistsAt(appRoot, 'src-tauri', 'Cargo.toml')}`,
635
+ csprojText: rootCsproj ? readTextIfExistsAt(appRoot, rootCsproj) : '',
636
+ programCsText: readTextIfExistsAt(appRoot, 'Program.cs'),
637
+ viewFilesText: fs.existsSync(path.join(appRoot, 'app', 'View'))
638
+ ? fs.readdirSync(path.join(appRoot, 'app', 'View'), { recursive: true }).join('\n')
639
+ : ''
640
+ };
641
+
642
+ const languageDetections = LANGUAGE_DETECTORS
643
+ .map(detector => detector.detect(ctx))
644
+ .filter(result => result.score >= 2)
645
+ .sort((a, b) => b.score - a.score);
646
+
647
+ const frameworkDetails = FRAMEWORK_DETECTORS
648
+ .map(detector => {
649
+ const result = detector.detect(ctx);
650
+ if (result.score < 3) return null;
651
+ return {
652
+ ...result,
653
+ kind: detector.kind,
654
+ language: detector.language,
655
+ projectType: detector.projectType,
656
+ confidence: confidenceLabel(result.score)
657
+ };
658
+ })
659
+ .filter(Boolean)
660
+ .sort((a, b) => b.score - a.score);
661
+
662
+ const frameworkLanguageScores = frameworkDetails.reduce((acc, item) => {
663
+ if (!item.language) return acc;
664
+ acc[item.language] = (acc[item.language] || 0) + item.score;
665
+ return acc;
666
+ }, {});
667
+ const primaryLanguage = Object.keys(frameworkLanguageScores).sort((a, b) => frameworkLanguageScores[b] - frameworkLanguageScores[a])[0]
668
+ || languageDetections[0]?.id
669
+ || 'typescript';
670
+ const frameworks = uniq(frameworkDetails.map(item => item.id)).filter(name => FRAMEWORK_CHOICES.includes(name));
671
+ const infrastructure = [];
672
+ let projectType = '';
673
+ let database = 'none';
674
+ let testFramework = 'auto-detect';
675
+ let cloud = 'none';
676
+ let ciCd = 'none';
677
+
678
+ if (frameworkDetails.some(item => item.projectType === 'desktop')) projectType = 'desktop';
679
+ else if (frameworkDetails.some(item => item.projectType === 'mobile')) projectType = 'mobile';
680
+ else if (frameworkDetails.some(item => item.kind === 'frontend') && frameworkDetails.some(item => item.kind === 'backend')) projectType = 'fullstack';
681
+ else if (frameworkDetails.some(item => item.projectType === 'web-app')) projectType = 'web-app';
682
+ else if (frameworkDetails.some(item => item.kind === 'backend')) projectType = 'api';
683
+ else if (frameworkDetails.some(item => item.kind === 'frontend')) projectType = 'web-app';
684
+ else if (fileExistsAt(appRoot, 'package.json') && pkg.bin) projectType = 'cli';
685
+ else if (['php', 'python', 'go', 'java', 'ruby', 'csharp', 'rust'].includes(primaryLanguage)) projectType = 'api';
686
+
687
+ if (hasDependency(depNames, 'pg', 'postgres', '@prisma/client') || /postgres/i.test(JSON.stringify(pkg))) database = 'postgres';
688
+ else if (hasDependency(depNames, 'mysql', 'mysql2')) database = 'mysql';
689
+ else if (hasDependency(depNames, 'sqlite3', 'better-sqlite3', 'rusqlite')) database = 'sqlite';
690
+ else if (hasDependency(depNames, 'mongodb', 'mongoose')) database = 'mongodb';
691
+ else if (hasDependency(depNames, 'redis', 'ioredis')) database = 'redis';
692
+ else if (hasDependency(depNames, '@aws-sdk/client-dynamodb')) database = 'dynamodb';
693
+
694
+ if (hasDependency(depNames, 'vitest')) testFramework = 'vitest';
695
+ else if (hasDependency(depNames, 'jest')) testFramework = 'jest';
696
+ else if (hasDependency(depNames, 'pytest') || textMatches(ctx.requirementsText, /\bpytest\b/i) || textMatches(ctx.pyprojectText, /\bpytest\b/i)) testFramework = 'pytest';
697
+ else if (hasComposerDependency(composerDepNames, 'phpunit/phpunit')) testFramework = 'phpunit';
698
+ else if (hasComposerDependency(composerDepNames, 'pestphp/pest')) testFramework = 'pest';
699
+ else if (fileExistsAt(appRoot, 'go.mod')) testFramework = 'go test';
700
+ else if (textMatches(ctx.gemfileText, /^\s*gem\s+["']rspec["']/im)) testFramework = 'rspec';
701
+
702
+ if (repoHasAny(targetDir, [['Dockerfile'], ['docker-compose.yml'], ['docker-compose.yaml']])) {
703
+ infrastructure.push('docker');
704
+ }
705
+
706
+ if (repoHasAny(targetDir, [['vercel.json']])) {
707
+ cloud = 'vercel';
708
+ infrastructure.push('vercel');
709
+ } else if (repoHasAny(targetDir, [['fly.toml']])) {
710
+ cloud = 'fly';
711
+ infrastructure.push('fly');
712
+ } else if (repoHasAny(targetDir, [['railway.json'], ['railway.toml']])) {
713
+ cloud = 'railway';
714
+ infrastructure.push('railway');
715
+ }
716
+
717
+ if (repoHasAny(targetDir, [['.github', 'workflows'], ['.gitlab-ci.yml'], ['.circleci', 'config.yml'], ['Jenkinsfile']], 2)) {
718
+ if (repoHasAny(targetDir, [['.github', 'workflows']], 2)) ciCd = 'github-actions';
719
+ else if (repoHasAny(targetDir, [['.gitlab-ci.yml']], 2)) ciCd = 'gitlab-ci';
720
+ else if (repoHasAny(targetDir, [['.circleci', 'config.yml']], 2)) ciCd = 'circleci';
721
+ else if (repoHasAny(targetDir, [['Jenkinsfile']], 2)) ciCd = 'jenkins';
722
+ }
723
+
724
+ const scanEvidence = [
725
+ ...(languageDetections[0] ? [`language:${languageDetections[0].id} => ${languageDetections[0].evidence.join('; ')}`] : []),
726
+ ...frameworkDetails.map(item => `${item.id} [${item.confidence}] => ${item.evidence.join('; ')}`)
727
+ ];
728
+
729
+ return {
730
+ project_name: path.basename(targetDir),
731
+ app_root: path.relative(targetDir, appRoot) || '.',
732
+ description: pkg.description || composer.description || 'A software project',
733
+ project_type: PROJECT_TYPE_CHOICES.includes(projectType) ? projectType : 'web-app',
734
+ primary_language: LANGUAGE_CHOICES.includes(primaryLanguage) ? primaryLanguage : 'typescript',
735
+ frameworks,
736
+ framework_details: frameworkDetails,
737
+ scan_evidence: scanEvidence,
738
+ database: DATABASE_CHOICES.includes(database) ? database : 'none',
739
+ package_manager: PACKAGE_MANAGER_CHOICES.includes(detectPackageManager(appRoot)) ? detectPackageManager(appRoot) : 'auto-detect',
740
+ uses_docker: infrastructure.includes('docker'),
741
+ cloud,
742
+ infrastructure: uniq(infrastructure),
743
+ ci_cd: ciCd,
744
+ test_framework: TEST_FRAMEWORK_CHOICES.includes(testFramework) ? testFramework : 'auto-detect',
745
+ max_file_lines: '300',
746
+ code_style: 'project-standard',
747
+ architecture_notes: '<!-- Add notes -->',
748
+ current_phase: 'Initial setup',
749
+ next_phase: '-',
750
+ known_issues: '<!-- None yet -->'
751
+ };
752
+ }
753
+ // --- File Generators ---
754
+
755
+ function generateSharedAgentsMd(data) {
756
+ return `# AGENTS.md
757
+
758
+ Shared operating instructions for Codex, Antigravity, Claude Code, and Cursor.
759
+
760
+ ## Startup
761
+
762
+ 1. Read this file first.
763
+ 2. Read \`.agent/docs/project.md\` for detected stack, commands, and routing.
764
+ 3. Read \`.agent/docs/workflow.md\` for the task flow before editing.
765
+ 4. Read \`.agent/docs/architecture.md\` and \`.agent/docs/conventions.md\` before planning code changes.
766
+ 5. Read \`.agent/docs/tooling.md\` when the task needs MCP, RTK, or browser verification.
767
+
768
+ If any \`.agent/docs/*.md\` file still contains \`TODO: refine\`, update the docs by scanning the project before making product code changes.
769
+
770
+ ## Workflow
771
+
772
+ Superpowers is the primary workflow backend. Use its native skills when available:
773
+
774
+ - New idea or unclear requirement: \`brainstorming\`.
775
+ - Feature/refactor: \`using-git-worktrees\` -> \`writing-plans\` -> explicit approval -> \`executing-plans\` or \`subagent-driven-development\`.
776
+ - Implementation: \`test-driven-development\` and focused verification.
777
+ - Bug or failing behavior: \`systematic-debugging\` followed by a regression test.
778
+ - Before completion: \`requesting-code-review\` and \`verification-before-completion\`.
779
+ - Branch completion: \`finishing-a-development-branch\`.
780
+
781
+ Questions and research do not edit product code. A simple fix may use a fast path only when it is one-file, unambiguous, and changes no behavior or contract. State the scope and verification before editing. If the scope grows, switch to the full Superpowers flow.
782
+
783
+ If Superpowers is unavailable, follow the plain-language flow in \`.agent/docs/workflow.md\`; do not expect this kit to provide a second copy of the workflow engine.
784
+
785
+ ## Rules
786
+
787
+ - Respond in the user's language; keep code, identifiers, and code comments in English.
788
+ - State assumptions when the request is ambiguous.
789
+ - Prefer the existing project style over new abstractions.
790
+ - Keep changes surgical and remove only dead code introduced by your change.
791
+ - **MCP First**: Prioritize \`codebase-memory-mcp\` for symbols, call paths, architecture, and impact; use \`context7\` for API/docs queries and \`playwright\` for browser/UI verification. Fall back to normal file search when an integration is unavailable.
792
+ - Prefer \`rtk\` by default for verbose shell commands such as \`rtk git status\`, \`rtk npm test\`, and \`rtk git log\`. Run the underlying command directly only when RTK is unavailable or incompatible.
793
+ - Tests or equivalent verification are mandatory before claiming done.
794
+ - Keep files under ${data.max_file_lines || '300'} lines unless the project already has a different standard in \`.agent/docs/conventions.md\`.
795
+
796
+ ## Shared Knowledge
797
+
798
+ - Project context: \`.agent/docs/\`
799
+ - Tooling setup: \`.agent/docs/tooling.md\`
800
+ - Superpowers: installed through the AI client's official plugin surface
801
+ - RTK: recommended local CLI for compact command output
802
+ - Antigravity and Cursor entry rules: \`GEMINI.md\` and \`.cursorrules\`
803
+ `;
804
+ }
805
+
806
+ function generateIndexProjectPrompt() {
807
+ return `# Project Indexing Prompt
808
+
809
+ Read CLAUDE.md (if using Claude Code), AGENTS.md, and all files under .agent/docs/.
810
+ Use Superpowers for the workflow and use codebase-memory MCP for structural code discovery when available.
811
+ Ask me what this project does and any important conventions I want preserved.
812
+ Then scan this repository and refine the generated context docs with factual rules.
813
+ Do not implement product code during onboarding.
814
+ `;
815
+ }
816
+
817
+ function printProjectPrompt() {
818
+ const promptPath = path.join(targetDir, '.agent', 'docs', 'index-project-prompt.md');
819
+ if (!fs.existsSync(promptPath)) {
820
+ console.error(pc.yellow('No saved project prompt found. Run thachvd-kit init first.'));
821
+ process.exitCode = 1;
822
+ return;
823
+ }
824
+ console.log(fs.readFileSync(promptPath, 'utf8'));
825
+ }
826
+
827
+ function generateSharedClaudeMd() {
828
+ return `@AGENTS.md
829
+
830
+ ## Claude Code
831
+
832
+ 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.
833
+ `;
834
+ }
835
+
836
+ function generateSharedGeminiMd() {
837
+ return `# GEMINI.md
838
+
839
+ Antigravity entry file for this project.
840
+
841
+ Read AGENTS.md first, then follow the shared docs under .agent/docs/.
842
+ `;
843
+ }
844
+
845
+ function generateSharedCursorrules(data) {
846
+ return `# Cursor Rules
847
+
848
+ This repository uses AGENTS.md as the shared cross-agent entry file. Follow the instructions in AGENTS.md and keep Cursor-specific notes here only when they cannot apply to Codex, Antigravity, or Claude Code.
849
+
850
+ Read AGENTS.md first, then follow the shared docs under .agent/docs/.
851
+
852
+ ---
853
+
854
+ ${generateSharedAgentsMd(data).trimEnd()}
855
+ `;
856
+ }
857
+
858
+ function resolveCommands(data) {
859
+ const frameworks = data.frameworks || [];
860
+ const primaryLang = data.primary_language || 'javascript';
861
+ const pkgManager = data.package_manager || 'npm';
862
+ const testFramework = data.test_framework || 'auto-detect';
863
+
864
+ const isPython = primaryLang === 'python';
865
+ const isGo = primaryLang === 'go';
866
+ const isRust = primaryLang === 'rust';
867
+ const isPhp = primaryLang === 'php';
868
+
869
+ let install = 'npm install';
870
+ let dev = 'npm run dev';
871
+ let build = 'npm run build';
872
+ let test = 'npm test';
873
+ let lint = 'npm run lint';
874
+
875
+ if (pkgManager === 'pnpm') { install = 'pnpm install'; dev = 'pnpm dev'; build = 'pnpm build'; test = 'pnpm test'; lint = 'pnpm lint'; }
876
+ else if (pkgManager === 'yarn') { install = 'yarn'; dev = 'yarn dev'; build = 'yarn build'; test = 'yarn test'; lint = 'yarn lint'; }
877
+ else if (pkgManager === 'bun') { install = 'bun install'; dev = 'bun dev'; build = 'bun run build'; test = 'bun test'; lint = 'bun run lint'; }
878
+ else if (isPython) { install = 'pip install -r requirements.txt'; dev = 'python manage.py runserver'; build = '# no build step'; test = 'pytest'; lint = 'ruff check .'; }
879
+ else if (isGo) { install = 'go mod download'; dev = 'go run .'; build = 'go build ./...'; test = 'go test ./...'; lint = 'golangci-lint run'; }
880
+ else if (isRust) { install = 'cargo build'; dev = 'cargo run'; build = 'cargo build --release'; test = 'cargo test'; lint = 'cargo clippy'; }
881
+ else if (isPhp) {
882
+ install = 'composer install';
883
+ build = '# no build step';
884
+ test = testFramework === 'pest' ? './vendor/bin/pest' : './vendor/bin/phpunit';
885
+ const isCakePHP = frameworks.some(f => f.toLowerCase().includes('cakephp'));
886
+ const isSymfony = frameworks.some(f => f.toLowerCase().includes('symfony'));
887
+ const isCodeIgniter = frameworks.some(f => f.toLowerCase().includes('codeigniter'));
888
+ if (isCakePHP) {
889
+ dev = 'bin/cake server';
890
+ lint = './vendor/bin/phpcs';
891
+ } else if (isSymfony) {
892
+ dev = 'bin/console server:run';
893
+ lint = './vendor/bin/phpcs';
894
+ } else if (isCodeIgniter) {
895
+ dev = 'php spark serve';
896
+ lint = './vendor/bin/phpcs';
897
+ } else {
898
+ dev = 'php artisan serve';
899
+ lint = 'php artisan pint';
900
+ }
901
+ }
902
+
903
+ if (testFramework === 'vitest') test = `${pkgManager === 'pnpm' ? 'pnpm' : pkgManager === 'yarn' ? 'yarn' : pkgManager === 'bun' ? 'bun run' : 'npx'} vitest`;
904
+ else if (testFramework === 'jest') test = `${pkgManager === 'pnpm' ? 'pnpm' : pkgManager === 'yarn' ? 'yarn' : 'npx'} jest`;
905
+
906
+ return { install, dev, build, test, lint };
907
+ }
908
+
909
+ function generateProjectDoc(data) {
910
+ const commands = resolveCommands(data);
911
+ const evidence = (data.scan_evidence || []).length > 0
912
+ ? (data.scan_evidence || []).map(line => `- ${line}`).join('\n')
913
+ : '- TODO: refine scan evidence by inspecting the project.';
914
+
915
+ return `# Project Rules
916
+
917
+ Generated by thachvd-kit on ${new Date().toISOString().split('T')[0]}.
918
+
919
+ ## Summary
920
+
921
+ - Name: ${data.project_name}
922
+ - Description: ${data.description}
923
+ - Type: ${data.project_type}
924
+ - App root: ${data.app_root || '.'}
925
+
926
+ ## Stack
927
+
928
+ - Language: ${data.primary_language}
929
+ - Frameworks: ${(data.frameworks || []).join(', ') || 'none'}
930
+ - Database: ${data.database || 'none'}
931
+ - Infrastructure: ${(data.infrastructure || []).join(', ') || 'none'}
932
+ - Package manager: ${data.package_manager || 'auto-detect'}
933
+ - Test framework: ${data.test_framework || 'auto-detect'}
934
+
935
+ ## Commands
936
+
937
+ - Install: \`${commands.install}\`
938
+ - Dev: \`${commands.dev}\`
939
+ - Build: \`${commands.build}\`
940
+ - Test: \`${commands.test}\`
941
+ - Lint: \`${commands.lint}\`
942
+
943
+ ## Available Integrations
944
+
945
+ Use these integrations when available. Run thachvd-kit doctor to check the current machine.
946
+
947
+ | MCP | When to Use |
948
+ |-----|-------------|
949
+ | \`codebase-memory-mcp\` | Explore symbols, trace call chains, architecture, and impact |
950
+ | \`context7\` | Look up library/framework docs, API signatures, migration guides |
951
+ | \`playwright\` | Verify UI behavior, take screenshots, test forms and navigation |
952
+ | \`rtk\` | Recommended proxy for noisy git, tests, builds, and package commands |
953
+
954
+ > Check \`.agent/docs/tooling.md\` for setup details and additional MCP servers.
955
+
956
+ ## Scan Evidence
957
+
958
+ ${evidence}
959
+ `;
960
+ }
961
+
962
+ function generateArchitectureDoc(data) {
963
+ const appRootLine = data.app_root && data.app_root !== '.'
964
+ ? `- App root is currently detected as \`${data.app_root}\`.`
965
+ : '- App root is the repository root unless refined below.';
966
+ const evidence = (data.scan_evidence || []).length > 0
967
+ ? (data.scan_evidence || []).map(line => `- ${line}`).join('\n')
968
+ : '- TODO: refine architecture by scanning source directories.';
969
+
970
+ return `# Architecture Notes
971
+
972
+ ## Current Map
973
+
974
+ ${appRootLine}
975
+ - TODO: refine major directories and responsibilities.
976
+ - TODO: refine main entry points, routing, state boundaries, and integration boundaries.
977
+
978
+ ## Detected Evidence
979
+
980
+ ${evidence}
981
+
982
+ ## AI Maintenance Rule
983
+
984
+ 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.
985
+ `;
986
+ }
987
+
988
+ function generateConventionsDoc(data) {
989
+ return `# Coding Conventions
990
+
991
+ ## Current Standards
992
+
993
+ - Maximum file length: ${data.max_file_lines || '300'} lines unless the existing project standard is stricter.
994
+ - Code comments and identifiers should be written in English.
995
+ - Keep changes scoped to the user request.
996
+ - Prefer existing local patterns over introducing new abstractions.
997
+ - TODO: refine naming, formatting, folder, API, state, styling, and testing conventions from the real codebase.
998
+
999
+ ## Verification
1000
+
1001
+ - Run the focused test or lint command that matches the touched area.
1002
+ - If no automated check exists, document the manual verification performed.
1003
+ - For UI/web changes, prioritize using Playwright MCP to automatically verify client-side behavior and capture screenshots.
1004
+ - Do not claim completion without verification evidence.
1005
+ `;
1006
+ }
1007
+
1008
+ function generateWorkflowDoc() {
1009
+ return `# Agent Workflow
1010
+
1011
+ This project uses Superpowers as the workflow backend. thachvd-kit only provides project context and integration setup.
1012
+
1013
+ ## Before Every Task
1014
+
1015
+ 1. Read AGENTS.md and the relevant files under .agent/docs/.
1016
+ 2. Use the native Superpowers skill that matches the request.
1017
+ 3. Use codebase-memory MCP for structural discovery when available.
1018
+ 4. Make changes only after the required planning gate.
1019
+ 5. Verify the change and request review before completion.
1020
+
1021
+ ## Route Matrix
1022
+
1023
+ | Situation | Superpowers path | Gate |
1024
+ |---|---|---|
1025
+ | Question or research only | direct answer or research | no product-code edits |
1026
+ | One obvious local fix | fast path | inspect -> edit -> focused verify |
1027
+ | Bug or failing behavior | systematic-debugging | reproduce -> root cause -> regression test -> fix -> verify |
1028
+ | New behavior or refactor | brainstorming -> writing-plans -> executing-plans | explicit plan approval before implementation |
1029
+ | Multi-part implementation | subagent-driven-development | task review and final review |
1030
+ | Pre-merge | requesting-code-review | blocking findings resolved |
1031
+
1032
+ ## Standard Feature Flow
1033
+
1034
+ brainstorming -> using-git-worktrees -> writing-plans -> user approval -> test-driven-development -> executing-plans or subagent-driven-development -> requesting-code-review -> verification-before-completion -> finishing-a-development-branch.
1035
+
1036
+ ## Fast Path
1037
+
1038
+ Use it only for one-file, unambiguous changes with no behavior, API, schema, security, dependency, CI, workflow, or release contract change. State the narrow scope and verification before editing. If the scope grows, switch to brainstorming and writing-plans.
1039
+
1040
+ ## Documentation Maintenance
1041
+
1042
+ - Update architecture.md when structure or boundaries change.
1043
+ - Update conventions.md when repeated project patterns become clear.
1044
+ - Update project.md when stack, scripts, or test tooling changes.
1045
+ - Keep AGENTS.md concise and put project-specific detail under .agent/docs/.
1046
+ `;
1047
+ }
1048
+
1049
+ function generateGettingStartedDoc() {
1050
+ return `# Getting Started
1051
+
1052
+ thachvd-kit creates project context. Superpowers owns the development workflow.
1053
+
1054
+ ## Setup
1055
+
1056
+ 1. Run \`thachvd-kit init\` in the repository.
1057
+ 2. Open or print \`.agent/docs/index-project-prompt.md\` with \`thachvd-kit prompt\`.
1058
+ 3. Install Superpowers and RTK, then run \`thachvd-kit setup\`.
1059
+ 4. Run \`thachvd-kit doctor\` and restart the AI client.
1060
+
1061
+ ## Standard Flow
1062
+
1063
+ Use Superpowers native skills:
1064
+
1065
+ 1. \`brainstorming\` for unclear requirements.
1066
+ 2. \`using-git-worktrees\` for isolated work.
1067
+ 3. \`writing-plans\`, then wait for explicit approval.
1068
+ 4. \`test-driven-development\` and \`executing-plans\` or \`subagent-driven-development\`.
1069
+ 5. \`requesting-code-review\` and \`verification-before-completion\`.
1070
+ 6. \`finishing-a-development-branch\`.
1071
+
1072
+ ## Bug Flow
1073
+
1074
+ Use \`systematic-debugging\`: reproduce the symptom, inspect the path with codebase-memory MCP, test hypotheses, add regression protection, fix the root cause, and verify.
1075
+
1076
+ ## Fast Path
1077
+
1078
+ Use a fast path only for one-file, unambiguous changes with no behavior or contract change. State the scope and verification before editing. If the scope expands, return to brainstorming and writing-plans.
1079
+ `;
1080
+ }
1081
+
1082
+ function resolveToolingSetup(data) {
1083
+ const primaryLang = data.primary_language || 'javascript';
1084
+ const packageManager = data.package_manager || 'npm';
1085
+ const isPython = primaryLang === 'python';
1086
+ const runner = packageManager === 'pnpm'
1087
+ ? 'pnpm dlx'
1088
+ : packageManager === 'yarn'
1089
+ ? 'yarn dlx'
1090
+ : packageManager === 'bun'
1091
+ ? 'bunx'
1092
+ : 'npx';
1093
+ const playwrightCommand = isPython
1094
+ ? 'pip install playwright && playwright install chromium'
1095
+ : `${runner} playwright install`;
1096
+
1097
+ return { runner, playwrightCommand };
1098
+ }
1099
+
1100
+ function generateToolingDoc(data) {
1101
+ const { runner, playwrightCommand } = resolveToolingSetup(data);
1102
+
1103
+ return `# Optional Tooling
1104
+
1105
+ Run \`thachvd-kit doctor\` to check the current machine. thachvd-kit does not install workflow skills or copy agent folders.
1106
+
1107
+ ## Superpowers
1108
+
1109
+ Install Superpowers separately for each AI client from its official plugin surface:
1110
+
1111
+ - Claude Code: \`/plugin install superpowers@claude-plugins-official\`
1112
+ - Antigravity: \`agy plugin install https://github.com/obra/superpowers\`
1113
+ - Codex App: install it from the Plugins sidebar.
1114
+ - Codex CLI: open \`/plugins\` and install Superpowers.
1115
+ - Cursor: \`/add-plugin superpowers\`
1116
+
1117
+ Guide: https://github.com/obra/superpowers#installation
1118
+
1119
+ ## Codebase Memory MCP
1120
+
1121
+ Use \`codebase-memory-mcp\` for symbol search, call paths, architecture, and impact analysis.
1122
+
1123
+ - Install: \`npm install -g codebase-memory-mcp\`
1124
+ - Index a repository: \`codebase-memory-mcp cli index_repository --repo-path .\`
1125
+ - Keep local index state out of git unless the team explicitly chooses a shared snapshot.
1126
+
1127
+ ## RTK
1128
+
1129
+ RTK is the recommended command-output layer, not a workflow engine. Prefer it by default for verbose shell commands.
1130
+
1131
+ - Install: \`cargo install --git https://github.com/rtk-ai/rtk --branch master rtk\`
1132
+ - Examples: \`rtk git status\`, \`rtk npm test\`, \`rtk git log\`.
1133
+ - If it is unavailable, run the underlying command directly.
1134
+
1135
+ ## Playwright
1136
+
1137
+ - Check: \`${runner} playwright --version\`
1138
+ - Browser install: \`${playwrightCommand}\`
1139
+ - Use the project's existing Playwright scripts when available.
1140
+ `;
1141
+ }
1142
+
1143
+ async function writeGeneratedFile(relativePath, content, assumeYes) {
1144
+ const outputPath = path.join(targetDir, relativePath);
1145
+ const exists = fs.existsSync(outputPath);
1146
+ if (exists && !assumeYes) {
1147
+ const response = await prompts({
1148
+ type: 'confirm',
1149
+ name: 'overwrite',
1150
+ message: `File ${pc.bold(relativePath)} already exists. Do you want to overwrite it?`,
1151
+ initial: false
1152
+ });
1153
+ if (response.overwrite === undefined) {
1154
+ console.log(pc.yellow('\nCancelled.'));
1155
+ process.exit(130);
1156
+ }
1157
+ if (!response.overwrite) return false;
1158
+ }
1159
+
1160
+ fs.mkdirSync(path.dirname(outputPath), { recursive: true });
1161
+ fs.writeFileSync(outputPath, content, 'utf8');
1162
+ return true;
1163
+ }
1164
+
1165
+ function homePath(...parts) {
1166
+ const home = process.env.USERPROFILE || process.env.HOME;
1167
+ return home ? path.join(home, ...parts) : null;
1168
+ }
1169
+
1170
+ function printSuperpowersInstallGuide() {
1171
+ console.log(`
1172
+ ${pc.bold('Superpowers setup')} (official client integrations)
1173
+ Claude Code: /plugin install superpowers@claude-plugins-official
1174
+ Antigravity: agy plugin install https://github.com/obra/superpowers
1175
+ Codex App: install Superpowers from the Plugins sidebar
1176
+ Codex CLI: open /plugins, search Superpowers, then Install Plugin
1177
+ Cursor: /add-plugin superpowers
1178
+
1179
+ Full guide: https://github.com/obra/superpowers#installation
1180
+ `);
1181
+ }
1182
+
1183
+ function runDoctor() {
1184
+ console.log(pc.bold(pc.cyan('thachvd-kit doctor')));
1185
+ const requiredFiles = ['AGENTS.md', 'CLAUDE.md', 'GEMINI.md', '.cursorrules',
1186
+ path.join('.agent', 'docs', 'project.md'), path.join('.agent', 'docs', 'workflow.md')];
1187
+ for (const relativePath of requiredFiles) {
1188
+ const ok = fs.existsSync(path.join(targetDir, relativePath));
1189
+ console.log(` ${ok ? pc.green('OK') : pc.yellow('WARN')} ${relativePath}`);
1190
+ }
1191
+
1192
+ const tools = [
1193
+ [CODEBASE_MEMORY_BINARY, `npm install -g ${CODEBASE_MEMORY_PACKAGE}`],
1194
+ ['rtk', `recommended: ${RTK_INSTALL_COMMAND}`],
1195
+ ['agy', 'install Antigravity before running its Superpowers plugin command'],
1196
+ ['claude', 'install Claude Code before installing its Superpowers plugin'],
1197
+ ['codex', 'install Codex before using its Superpowers plugin']
1198
+ ];
1199
+ console.log(`\n${pc.bold('Local integrations (RTK recommended)')}`);
1200
+ for (const [binary, hint] of tools) {
1201
+ const ok = commandExists(binary);
1202
+ console.log(` ${ok ? pc.green('OK') : pc.yellow('WARN')} ${binary}${ok ? '' : ` - ${hint}`}`);
1203
+ }
1204
+
1205
+ console.log(`\n${pc.bold('Next step')}`);
1206
+ console.log(' Run thachvd-kit prompt to print the saved project-indexing prompt.');
1207
+ }
1208
+
1209
+ async function runSetup(args) {
1210
+ const skipMcpSetup = args.includes('--no-setup-mcp');
1211
+ const skipHookSetup = args.includes('--no-setup-hook');
1212
+ const skipRtkInstall = args.includes('--no-install-rtk');
1213
+ const skipIndex = args.includes('--no-index');
1214
+ console.log(pc.bold(pc.cyan('thachvd-kit setup')));
1215
+ printSuperpowersInstallGuide();
1216
+ if (!skipMcpSetup) {
1217
+ console.log(pc.bold('MCP setup'));
1218
+ for (const result of setupMcpServers({ install: true })) {
1219
+ const marker = result.ok ? pc.green('OK') : pc.yellow('WARN');
1220
+ console.log(` ${marker} ${result.message}`);
1221
+ }
1222
+ if (!skipIndex) {
1223
+ const indexResult = indexRepository();
1224
+ const marker = indexResult.ok ? pc.green('OK') : pc.yellow('WARN');
1225
+ console.log(` ${marker} ${indexResult.message}`);
1226
+ }
1227
+ }
1228
+ if (!skipHookSetup) {
1229
+ console.log(`\n${pc.bold('Claude Code Definition-of-Done hook')}`);
1230
+ const claudeProjectSettings = path.join(targetDir, '.claude', 'settings.json');
1231
+ const result = mergeClaudeProjectHooks(claudeProjectSettings);
1232
+ const marker = result.ok ? pc.green('OK') : pc.yellow('WARN');
1233
+ console.log(` ${marker} ${result.message}`);
1234
+ console.log(' This hook only checks for plan/review/verification evidence; it never prescribes which skill produces it.');
1235
+ }
1236
+ console.log(`\n${pc.bold('RTK (recommended)')}`);
1237
+ const rtkResult = ensureRtkInstalled(!skipRtkInstall);
1238
+ const rtkMarker = rtkResult.ok ? pc.green('OK') : pc.yellow('WARN');
1239
+ console.log(` ${rtkMarker} ${rtkResult.message}`);
1240
+ console.log('\nRestart your AI client after installing plugins or changing MCP configuration.');
1241
+ console.log('Your onboarding prompt is saved at .agent/docs/index-project-prompt.md. Run thachvd-kit prompt to print it again.');
1242
+ }
1243
+
1244
+ function commandExists(command) {
1245
+ const checker = process.platform === 'win32' ? 'where' : 'command';
1246
+ const args = process.platform === 'win32' ? [command] : ['-v', command];
1247
+ const result = spawnSync(checker, args, { encoding: 'utf8', shell: process.platform !== 'win32' });
1248
+ return result.status === 0;
1249
+ }
1250
+
1251
+ function ensureGlobalNpmPackage(binaryName, packageName, install = false) {
1252
+ if (commandExists(binaryName)) {
1253
+ return { ok: true, message: `${binaryName} already installed` };
1254
+ }
1255
+ if (!install) {
1256
+ return { ok: false, message: `${binaryName} is missing; run: npm install -g ${packageName}` };
1257
+ }
1258
+ if (!commandExists('npm')) {
1259
+ return { ok: false, message: `npm not found; run: npm install -g ${packageName}` };
1260
+ }
1261
+ const result = spawnSync('npm', ['install', '-g', packageName], {
1262
+ encoding: 'utf8',
1263
+ stdio: 'pipe',
1264
+ shell: process.platform === 'win32'
1265
+ });
1266
+ if (result.status !== 0) {
1267
+ return { ok: false, message: `failed to install ${binaryName}; run: npm install -g ${packageName}` };
1268
+ }
1269
+ return { ok: true, message: `installed ${binaryName}` };
1270
+ }
1271
+
1272
+ function ensureCodebaseMemoryCli(install = false) {
1273
+ return ensureGlobalNpmPackage(CODEBASE_MEMORY_BINARY, CODEBASE_MEMORY_PACKAGE, install);
1274
+ }
1275
+
1276
+ function ensureRtkInstalled(install = false) {
1277
+ if (commandExists('rtk')) {
1278
+ return { ok: true, message: 'rtk already installed' };
1279
+ }
1280
+ if (!install) {
1281
+ return { ok: false, message: `rtk is missing; run: ${RTK_INSTALL_COMMAND}` };
1282
+ }
1283
+ if (!commandExists('cargo')) {
1284
+ return { ok: false, message: `cargo not found; install Rust, then run: ${RTK_INSTALL_COMMAND}` };
1285
+ }
1286
+ console.log(pc.dim(' Installing rtk via cargo (compiles from source; this can take a few minutes)...'));
1287
+ const result = spawnSync('cargo', ['install', '--git', 'https://github.com/rtk-ai/rtk', '--branch', 'master', 'rtk'], {
1288
+ encoding: 'utf8',
1289
+ stdio: 'inherit',
1290
+ shell: process.platform === 'win32'
1291
+ });
1292
+ if (result.status !== 0) {
1293
+ return { ok: false, message: `failed to install rtk; run: ${RTK_INSTALL_COMMAND}` };
1294
+ }
1295
+ return { ok: true, message: 'installed rtk' };
1296
+ }
1297
+
1298
+ function indexRepository() {
1299
+ if (!commandExists(CODEBASE_MEMORY_BINARY)) {
1300
+ return { ok: false, message: `${CODEBASE_MEMORY_BINARY} is not installed; skipped indexing` };
1301
+ }
1302
+ console.log(pc.dim(` Running ${CODEBASE_MEMORY_BINARY} cli index_repository --repo-path . (first index of a large repo can take a while)...`));
1303
+ const result = spawnSync(CODEBASE_MEMORY_BINARY, ['cli', 'index_repository', '--repo-path', '.'], {
1304
+ cwd: targetDir,
1305
+ encoding: 'utf8',
1306
+ stdio: 'inherit',
1307
+ shell: process.platform === 'win32'
1308
+ });
1309
+ if (result.status !== 0) {
1310
+ return { ok: false, message: `indexing failed; run manually: ${CODEBASE_MEMORY_BINARY} cli index_repository --repo-path .` };
1311
+ }
1312
+ return { ok: true, message: 'indexed repository with codebase-memory-mcp' };
1313
+ }
1314
+
1315
+ function readJsonFile(filePath) {
1316
+ if (!fs.existsSync(filePath)) return {};
1317
+ try {
1318
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
1319
+ } catch {
1320
+ return null;
1321
+ }
1322
+ }
1323
+
1324
+ function writeJsonFile(filePath, data) {
1325
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
1326
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n', 'utf8');
1327
+ }
1328
+
1329
+ function mergeMcpJson(filePath, withType = false) {
1330
+ const data = readJsonFile(filePath);
1331
+ if (data === null) {
1332
+ return { ok: false, message: `${filePath} is not valid JSON; skipped` };
1333
+ }
1334
+ data.mcpServers = data.mcpServers || {};
1335
+ const baseServers = {
1336
+ 'codebase-memory-mcp': { command: CODEBASE_MEMORY_BINARY, args: [] },
1337
+ playwright: { command: 'playwright-mcp' },
1338
+ context7: { command: 'context7-mcp' }
1339
+ };
1340
+ if (data.permissions && Array.isArray(data.permissions.allow)) {
1341
+ const requiredPermissions = [
1342
+ "mcp__context7__resolve-library-id",
1343
+ "mcp__context7__query-docs"
1344
+ ];
1345
+ for (const perm of requiredPermissions) {
1346
+ if (!data.permissions.allow.includes(perm)) {
1347
+ data.permissions.allow.push(perm);
1348
+ }
1349
+ }
1350
+ }
1351
+ for (const [name, server] of Object.entries(baseServers)) {
1352
+ if (!data.mcpServers[name]) {
1353
+ data.mcpServers[name] = withType ? { type: 'stdio', ...server } : server;
1354
+ }
1355
+ }
1356
+ writeJsonFile(filePath, data);
1357
+ const tools = Object.keys(baseServers).join(', ');
1358
+ return { ok: true, message: `configured ${tools} MCP in ${filePath}` };
1359
+ }
1360
+
1361
+ function appendMcpTomlIfMissing(filePath) {
1362
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
1363
+ const existing = fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf8') : '';
1364
+ const blocks = [];
1365
+ if (!/\[mcp_servers\.context7\]/.test(existing)) {
1366
+ blocks.push(`[mcp_servers.context7]
1367
+ command = "context7-mcp"
1368
+ startup_timeout_sec = 20
1369
+ tool_timeout_sec = 120
1370
+ `);
1371
+ }
1372
+ if (!/\[mcp_servers\.codebase-memory-mcp\]/.test(existing)) {
1373
+ blocks.push(`[mcp_servers.codebase-memory-mcp]
1374
+ command = "${CODEBASE_MEMORY_BINARY}"
1375
+ startup_timeout_sec = 20
1376
+ tool_timeout_sec = 120
1377
+ `);
1378
+ }
1379
+ if (!/\[mcp_servers\.playwright\]/.test(existing)) {
1380
+ blocks.push(`[mcp_servers.playwright]
1381
+ command = "playwright-mcp"
1382
+ startup_timeout_sec = 20
1383
+ tool_timeout_sec = 120
1384
+ `);
1385
+ }
1386
+ if (blocks.length === 0) { return { ok: true, message: `already configured ${filePath}` };
1387
+ }
1388
+ const separator = existing && !existing.endsWith('\n') ? '\n\n' : existing ? '\n' : '';
1389
+ fs.writeFileSync(filePath, existing + separator + blocks.join('\n'), 'utf8');
1390
+ return { ok: true, message: `configured codebase-memory-mcp, context7, playwright MCP in ${filePath}` };
1391
+ }
1392
+
1393
+ const DOD_HOOK_MARKER = 'feature/refactor Definition-of-Done gate';
1394
+
1395
+ function generateDodStopHookPrompt() {
1396
+ return `Input JSON (Stop hook payload): $ARGUMENTS\n\nCheck this project's ${DOD_HOOK_MARKER}. This kit does not define its own workflow steps; it only verifies that evidence of the gate exists, regardless of which skill produced it (a Superpowers skill such as \`writing-plans\`/\`requesting-code-review\`, or an equivalent documented step). Use the \`last_assistant_message\` field as the authoritative final response. Use \`transcript_path\` only for additional context because the transcript may not contain the final response yet.\n\nFor a feature or refactor (multi-file change, new behavior, or non-trivial refactor), completion requires evidence of:\n- Plan: a plan was written and explicitly approved by the human before implementation started.\n- Review: the change was reviewed (correctness, simplicity, architecture, security, performance) and blocking issues were resolved.\n- Verification: tests or equivalent verification ran and passed.\n\nA one-file, unambiguous fix with no behavior/contract change is exempt from the Plan and Review evidence only when the final response states FAST_PATH: simple-fix, its reason, and its narrow scope. Verification is still mandatory in that case.\n\nSteps:\n1. If the input JSON has \`stop_hook_active: true\`, allow stopping immediately (already checked once this cycle; never block twice in a row).\n2. Inspect the changed files and the \`last_assistant_message\` field.\n3. If no source files changed, the final response does not claim the task is done/complete, or the final response explicitly contains FAST_PATH: simple-fix and the changed scope is clearly a simple/self-contained fix, allow stopping.\n4. If it is a feature/refactor-scale change and the final response claims completion, verify that the response (or the visible transcript) shows evidence of an approved plan and a completed review. If either is missing, block; a generic statement that the task was simple is not enough.\n5. Return exactly JSON: {"ok": true} to allow stopping, or {"ok": false, "reason": "..."} to block.\n\nWhen blocking, name exactly which gate is missing without prescribing which skill or workflow must produce it.`;
1397
+ }
1398
+
1399
+ function isDefinitionOfDoneHook(hook) {
1400
+ const prompt = hook && typeof hook.prompt === 'string' ? hook.prompt : '';
1401
+ return prompt.includes(DOD_HOOK_MARKER);
1402
+ }
1403
+
1404
+ function mergeClaudeProjectHooks(filePath) {
1405
+ const data = readJsonFile(filePath);
1406
+ if (data === null) {
1407
+ return { ok: false, message: `${filePath} is not valid JSON; skipped hook setup` };
1408
+ }
1409
+ if (!data.hooks || typeof data.hooks !== 'object') data.hooks = {};
1410
+ if (!Array.isArray(data.hooks.Stop)) data.hooks.Stop = [];
1411
+
1412
+ let primaryHook = null;
1413
+ let definitionHookCount = 0;
1414
+ for (const entry of data.hooks.Stop) {
1415
+ for (const hook of Array.isArray(entry?.hooks) ? entry.hooks : []) {
1416
+ if (isDefinitionOfDoneHook(hook)) {
1417
+ definitionHookCount++;
1418
+ if (!primaryHook) primaryHook = hook;
1419
+ }
1420
+ }
1421
+ }
1422
+
1423
+ if (primaryHook) {
1424
+ primaryHook.prompt = generateDodStopHookPrompt();
1425
+ primaryHook.timeout = 60;
1426
+ primaryHook.statusMessage = 'Checking feature/refactor Definition-of-Done gate...';
1427
+
1428
+ if (definitionHookCount > 1) {
1429
+ data.hooks.Stop = data.hooks.Stop
1430
+ .map(entry => ({
1431
+ ...entry,
1432
+ hooks: (Array.isArray(entry?.hooks) ? entry.hooks : [])
1433
+ .filter(hook => !isDefinitionOfDoneHook(hook) || hook === primaryHook)
1434
+ }))
1435
+ .filter(entry => entry.hooks.length > 0);
1436
+ }
1437
+
1438
+ writeJsonFile(filePath, data);
1439
+ return {
1440
+ ok: true,
1441
+ message: definitionHookCount > 1
1442
+ ? `consolidated Definition-of-Done Stop hooks in ${filePath}`
1443
+ : `updated Definition-of-Done Stop hook in ${filePath}`
1444
+ };
1445
+ }
1446
+
1447
+ data.hooks.Stop.push({
1448
+ hooks: [
1449
+ {
1450
+ type: 'agent',
1451
+ prompt: generateDodStopHookPrompt(),
1452
+ timeout: 60,
1453
+ statusMessage: 'Checking feature/refactor Definition-of-Done gate...'
1454
+ }
1455
+ ]
1456
+ });
1457
+ writeJsonFile(filePath, data);
1458
+ return { ok: true, message: `configured Definition-of-Done Stop hook in ${filePath}` };
1459
+ }
1460
+
1461
+ function setupMcpServers(options = {}) {
1462
+ const install = options.install === true;
1463
+ const results = [];
1464
+ results.push(ensureCodebaseMemoryCli(install));
1465
+ results.push(ensureGlobalNpmPackage('context7-mcp', '@upstash/context7-mcp', install));
1466
+ results.push(ensureGlobalNpmPackage('playwright-mcp', '@playwright/mcp', install));
1467
+
1468
+ const codexConfig = homePath('.codex', 'config.toml');
1469
+ if (codexConfig) results.push(appendMcpTomlIfMissing(codexConfig));
1470
+
1471
+ const geminiConfig = homePath('.gemini', 'config', 'mcp_config.json');
1472
+ if (geminiConfig) results.push(mergeMcpJson(geminiConfig, false));
1473
+
1474
+ const antigravityIdeConfig = homePath('.gemini', 'antigravity-ide', 'mcp_config.json');
1475
+ if (antigravityIdeConfig) results.push(mergeMcpJson(antigravityIdeConfig, false));
1476
+
1477
+ const antigravityConfig = homePath('.gemini', 'antigravity', 'mcp_config.json');
1478
+ if (antigravityConfig && fs.existsSync(path.dirname(antigravityConfig))) {
1479
+ results.push(mergeMcpJson(antigravityConfig, false));
1480
+ }
1481
+
1482
+ const claudeConfig = homePath('.claude.json');
1483
+ if (claudeConfig) results.push(mergeMcpJson(claudeConfig, true));
1484
+
1485
+ const claudeSettings = homePath('.claude', 'settings.json');
1486
+ if (claudeSettings) results.push(mergeMcpJson(claudeSettings, true));
1487
+
1488
+ return results;
1489
+ }
1490
+
1491
+ // --- Main CLI ---
1492
+
1493
+ async function main() {
1494
+ const rawArgs = process.argv.slice(2);
1495
+ const command = rawArgs[0] && !rawArgs[0].startsWith('-') ? rawArgs[0] : 'init';
1496
+ const args = command === 'init' ? rawArgs.slice(rawArgs[0] === 'init' ? 1 : 0) : rawArgs.slice(1);
1497
+
1498
+ if (rawArgs.includes('--version') || rawArgs.includes('-v')) {
1499
+ console.log(packageJson.version);
1500
+ return;
1501
+ }
1502
+
1503
+ if (rawArgs.includes('--help') || rawArgs.includes('-h')) {
1504
+ printHelp();
1505
+ return;
1506
+ }
1507
+
1508
+ if (command === 'doctor') {
1509
+ runDoctor();
1510
+ return;
1511
+ }
1512
+
1513
+ if (command === 'prompt') {
1514
+ printProjectPrompt();
1515
+ return;
1516
+ }
1517
+
1518
+ if (command === 'setup') {
1519
+ await runSetup(rawArgs.slice(1));
1520
+ return;
1521
+ }
1522
+
1523
+ if (command !== 'init') {
1524
+ console.error(pc.red(`Unknown command: ${command}`));
1525
+ printHelp();
1526
+ process.exit(1);
1527
+ }
1528
+
1529
+ const assumeYes = args.includes('--yes') || args.includes('-y');
1530
+ const skipMcpSetup = args.includes('--no-setup-mcp') || process.env.THACHVD_KIT_SKIP_MCP_SETUP === '1';
1531
+
1532
+ console.log(pc.bold(pc.cyan('='.repeat(50))));
1533
+ console.log(' ' + pc.bold('thachvd-kit') + ' - AI Project Bootstrap');
1534
+ console.log(' Generates shared agent rules and configures MCP tooling');
1535
+ console.log(pc.bold(pc.cyan('='.repeat(50))));
1536
+
1537
+ console.log(pc.dim(' Scanning current project files for a baseline...\n'));
1538
+ const scanned = scanProject();
1539
+
1540
+ console.log(` ${pc.bold('Detected context:')}`);
1541
+ formatScanPreviewLines(scanned).forEach(([label, value]) => {
1542
+ console.log(` - ${label}: ${value}`);
1543
+ });
1544
+ console.log('');
1545
+ if ((scanned.scan_evidence || []).length > 0) {
1546
+ console.log(` ${pc.bold('Evidence:')}`);
1547
+ scanned.scan_evidence.forEach(line => console.log(` - ${line}`));
1548
+ console.log('');
1549
+ }
1550
+
1551
+ // Combine data
1552
+ const data = {
1553
+ project_name: scanned?.project_name || path.basename(targetDir),
1554
+ description: scanned?.description || 'A software project',
1555
+ project_type: scanned?.project_type || 'web-app',
1556
+ primary_language: scanned?.primary_language || 'typescript',
1557
+ frameworks: scanned?.frameworks || [],
1558
+ database: scanned?.database || 'none',
1559
+ uses_docker: scanned?.uses_docker ?? true,
1560
+ cloud: scanned?.cloud || 'none',
1561
+ package_manager: scanned?.package_manager || 'auto-detect',
1562
+ test_framework: scanned?.test_framework || 'auto-detect',
1563
+ max_file_lines: scanned?.max_file_lines || '300'
1564
+ };
1565
+
1566
+ data.database = data.database || scanned?.database || 'none';
1567
+ data.uses_docker = typeof data.uses_docker === 'boolean' ? data.uses_docker : (scanned?.uses_docker ?? true);
1568
+ data.cloud = data.cloud || scanned?.cloud || 'none';
1569
+ data.package_manager = data.package_manager || scanned?.package_manager || 'auto-detect';
1570
+ data.test_framework = data.test_framework || scanned?.test_framework || 'auto-detect';
1571
+ data.max_file_lines = data.max_file_lines || scanned?.max_file_lines || '300';
1572
+ data.framework_details = scanned?.framework_details || [];
1573
+ data.scan_evidence = scanned?.scan_evidence || [];
1574
+ data.app_root = scanned?.app_root || '.';
1575
+
1576
+ data.infrastructure = [];
1577
+ if (data.uses_docker) data.infrastructure.push('docker');
1578
+ if (data.cloud !== 'none') data.infrastructure.push(data.cloud);
1579
+
1580
+ const shouldSetupMcp = args.includes('--setup-mcp') && !skipMcpSetup;
1581
+
1582
+ console.log(`\n${pc.bold(pc.cyan('Generating Files'))}`);
1583
+
1584
+ // Shared entry files are written after the .agent folder is available.
1585
+
1586
+ console.log(` ${pc.green('OK')} Project context generation enabled; workflow skills remain client-managed by Superpowers`);
1587
+
1588
+
1589
+ const generatedFiles = [
1590
+ ['AGENTS.md', generateSharedAgentsMd(data)],
1591
+ ['CLAUDE.md', generateSharedClaudeMd()],
1592
+ ['GEMINI.md', generateSharedGeminiMd()],
1593
+ ['.cursorrules', generateSharedCursorrules(data)],
1594
+ [path.join('.agent', 'docs', 'project.md'), generateProjectDoc(data)],
1595
+ [path.join('.agent', 'docs', 'architecture.md'), generateArchitectureDoc(data)],
1596
+ [path.join('.agent', 'docs', 'conventions.md'), generateConventionsDoc(data)],
1597
+ [path.join('.agent', 'docs', 'workflow.md'), generateWorkflowDoc()],
1598
+ [path.join('.agent', 'docs', 'tooling.md'), generateToolingDoc(data)],
1599
+ [path.join('.agent', 'docs', 'getting-started.md'), generateGettingStartedDoc()],
1600
+ [path.join('.agent', 'docs', 'index-project-prompt.md'), generateIndexProjectPrompt()]
1601
+ ];
1602
+
1603
+ for (const [relativePath, content] of generatedFiles) {
1604
+ const wrote = await writeGeneratedFile(relativePath, content, assumeYes);
1605
+ if (wrote) console.log(` ${pc.green('OK')} Generated ${pc.bold(relativePath)}`);
1606
+ else console.log(` ${pc.yellow('~')} Skipped ${pc.bold(relativePath)}`);
1607
+ }
1608
+
1609
+ if (shouldSetupMcp) {
1610
+ console.log(`\n${pc.bold(pc.cyan('Configuring MCP'))}`);
1611
+ for (const result of setupMcpServers()) {
1612
+ const marker = result.ok ? pc.green('OK') : pc.yellow('~');
1613
+ console.log(` ${marker} ${result.message}`);
1614
+ }
1615
+ }
1616
+
1617
+ console.log(`\n${pc.bold(pc.green('Done!'))}
1618
+
1619
+ ${pc.bold('Project:')} ${data.project_name}
1620
+ ${pc.bold('Stack:')} ${data.primary_language} | ${(data.frameworks || []).join(', ') || 'no framework'}
1621
+
1622
+ ${pc.bold('Generated:')}
1623
+ - ${pc.bold('AGENTS.md')} shared entry for Codex, Antigravity, Claude Code, and Cursor
1624
+ - ${pc.bold('CLAUDE.md')} Claude Code entry that imports AGENTS.md
1625
+ - ${pc.bold('GEMINI.md')} Antigravity entry
1626
+ - ${pc.bold('.cursorrules')} Cursor entry
1627
+ - ${pc.bold('.agent/docs/')} scan-based project rules and local operating notes
1628
+ - ${pc.bold('.agent/docs/index-project-prompt.md')} durable onboarding prompt
1629
+
1630
+ ${pc.bold('MCP/tooling setup:')}
1631
+ - Superpowers is the workflow backend; install it from the official client/plugin surface
1632
+ - Run ${pc.bold('thachvd-kit setup')} to configure codebase-memory MCP and recommended RTK
1633
+
1634
+ ${pc.bold('Next steps:')}
1635
+ 1. Run ${pc.bold('thachvd-kit prompt')} and paste the saved prompt into your AI editor
1636
+ 2. Install Superpowers and RTK, then run ${pc.bold('thachvd-kit setup')}
1637
+ 3. Run ${pc.bold('thachvd-kit doctor')} to verify the machine
1638
+ 4. Re-run ${pc.bold('thachvd-kit prompt')} whenever you need the onboarding prompt again
1639
+
1640
+ ${pc.bold('Onboarding prompt saved to .agent/docs/index-project-prompt.md:')}
1641
+ ${pc.dim('---')}
1642
+ ${generateIndexProjectPrompt().trim()}
1643
+ ${pc.dim('---')}
1644
+ `);
1645
+ }
1646
+
1647
+ main().catch(err => {
1648
+ console.error(pc.red('\nError during init:'), err);
1649
+ process.exit(1);
1650
+ });
1651
+