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