thachvd-kit 1.0.30 → 1.0.32
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/LICENSE +21 -21
- package/README.md +99 -95
- package/bin/cli.js +1278 -1844
- package/package.json +1 -1
package/bin/cli.js
CHANGED
|
@@ -1,143 +1,59 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
const fs = require('fs');
|
|
4
|
-
const path = require('path');
|
|
5
|
-
const { spawnSync } = require('child_process');
|
|
6
|
-
const prompts = require('prompts');
|
|
7
|
-
const pc = require('picocolors');
|
|
8
|
-
const packageJson = require('../package.json');
|
|
9
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { spawnSync } = require('child_process');
|
|
6
|
+
const prompts = require('prompts');
|
|
7
|
+
const pc = require('picocolors');
|
|
8
|
+
const packageJson = require('../package.json');
|
|
9
|
+
|
|
10
10
|
const sourceDir = path.resolve(__dirname, '..');
|
|
11
11
|
const targetDir = process.cwd();
|
|
12
12
|
const CODEBASE_MEMORY_PACKAGE = 'codebase-memory-mcp';
|
|
13
13
|
const CODEBASE_MEMORY_BINARY = 'codebase-memory-mcp';
|
|
14
14
|
const RTK_INSTALL_COMMAND = 'cargo install --git https://github.com/rtk-ai/rtk --branch master rtk';
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
next: ["nextjs-react-expert", "frontend-design"],
|
|
36
|
-
nuxt: ["frontend-design"],
|
|
37
|
-
svelte: ["frontend-design"],
|
|
38
|
-
angular: ["frontend-design"],
|
|
39
|
-
laravel: ["laravel-patterns", "laravel-security", "laravel-tdd"],
|
|
40
|
-
cakephp: ["api-design"],
|
|
41
|
-
symfony: ["api-design"],
|
|
42
|
-
codeigniter: ["api-design"],
|
|
43
|
-
express: ["nodejs-best-practices", "api-design"],
|
|
44
|
-
fastapi: ["python-patterns", "api-design"],
|
|
45
|
-
django: ["python-patterns", "api-design"],
|
|
46
|
-
flask: ["python-patterns", "api-design"],
|
|
47
|
-
gin: ["golang-patterns", "golang-testing"],
|
|
48
|
-
fiber: ["golang-patterns", "golang-testing"],
|
|
49
|
-
echo: ["golang-patterns", "golang-testing"],
|
|
50
|
-
nestjs: ["nodejs-best-practices", "api-design"],
|
|
51
|
-
rails: ["api-design"],
|
|
52
|
-
spring: ["api-design"],
|
|
53
|
-
aspnet: ["api-design"],
|
|
54
|
-
tauri: ["rust-pro", "desktop-design"],
|
|
55
|
-
axum: ["rust-pro", "api-design"],
|
|
56
|
-
actix: ["rust-pro", "api-design"],
|
|
57
|
-
flutter: ["mobile-design"],
|
|
58
|
-
"react-native": ["mobile-design"],
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
const INFRA_SKILLS = {
|
|
62
|
-
docker: ["docker-patterns"],
|
|
63
|
-
aws: ["deployment-procedures", "docker-patterns"],
|
|
64
|
-
gcp: ["deployment-procedures", "docker-patterns"],
|
|
65
|
-
azure: ["deployment-procedures", "docker-patterns"],
|
|
66
|
-
vercel: ["deployment-procedures"],
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
function resolveSkills(data) {
|
|
70
|
-
const skills = new Set();
|
|
71
|
-
const lang = (data.primary_language || "").toLowerCase();
|
|
72
|
-
|
|
73
|
-
if (LANG_SKILLS[lang]) {
|
|
74
|
-
LANG_SKILLS[lang].forEach(s => skills.add(s));
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
(data.frameworks || []).forEach(fw => {
|
|
78
|
-
const fwLower = fw.toLowerCase();
|
|
79
|
-
for (const [key, vals] of Object.entries(FRAMEWORK_SKILLS)) {
|
|
80
|
-
if (fwLower.includes(key)) vals.forEach(s => skills.add(s));
|
|
81
|
-
}
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
(data.infrastructure || []).forEach(infra => {
|
|
85
|
-
const infraLower = infra.toLowerCase();
|
|
86
|
-
for (const [key, vals] of Object.entries(INFRA_SKILLS)) {
|
|
87
|
-
if (infraLower.includes(key)) vals.forEach(s => skills.add(s));
|
|
88
|
-
}
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
skills.add("clean-code");
|
|
92
|
-
skills.add("systematic-debugging");
|
|
93
|
-
|
|
94
|
-
return Array.from(skills).sort();
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function resolveNativeSkills(data) {
|
|
98
|
-
return Array.from(new Set([
|
|
99
|
-
...resolveSkills(data),
|
|
100
|
-
'verification-before-completion',
|
|
101
|
-
'webapp-testing'
|
|
102
|
-
])).sort();
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
const FRAMEWORK_CHOICES = [
|
|
106
|
-
'vue', 'react', 'next', 'nuxt', 'svelte', 'angular',
|
|
107
|
-
'laravel', 'cakephp', 'symfony', 'codeigniter',
|
|
108
|
-
'express', 'fastapi', 'django', 'flask', 'gin', 'fiber', 'echo', 'nestjs',
|
|
109
|
-
'rails', 'spring', 'aspnet',
|
|
110
|
-
'tauri', 'axum', 'actix', 'flutter', 'react-native'
|
|
111
|
-
];
|
|
112
|
-
|
|
113
|
-
const LANGUAGE_CHOICES = ['typescript', 'javascript', 'python', 'go', 'php', 'rust', 'java', 'ruby', 'swift', 'kotlin', 'csharp', 'cpp'];
|
|
114
|
-
const PROJECT_TYPE_CHOICES = ['web-app', 'api', 'mobile', 'desktop', 'cli', 'fullstack'];
|
|
115
|
-
const DATABASE_CHOICES = ['postgres', 'mysql', 'sqlite', 'mongodb', 'redis', 'dynamodb', 'none'];
|
|
116
|
-
const PACKAGE_MANAGER_CHOICES = ['npm', 'pnpm', 'yarn', 'bun', 'pip', 'uv', 'go mod', 'cargo', 'composer', 'auto-detect'];
|
|
117
|
-
const TEST_FRAMEWORK_CHOICES = ['jest', 'vitest', 'pytest', 'go test', 'phpunit', 'pest', 'rspec', 'junit', 'auto-detect'];
|
|
118
|
-
const CLOUD_CHOICES = ['aws', 'gcp', 'azure', 'vercel', 'fly', 'railway', 'none'];
|
|
119
|
-
const FRONTEND_FRAMEWORKS = ['vue', 'react', 'next', 'nuxt', 'svelte', 'angular'];
|
|
120
|
-
const BACKEND_FRAMEWORKS = ['laravel', 'cakephp', 'symfony', 'codeigniter', 'express', 'fastapi', 'django', 'flask', 'gin', 'fiber', 'echo', 'nestjs', 'rails', 'spring', 'aspnet', 'axum', 'actix'];
|
|
121
|
-
const MOBILE_DESKTOP_FRAMEWORKS = ['tauri', 'flutter', 'react-native'];
|
|
122
|
-
const IGNORED_SCAN_DIRS = new Set(['.git', 'node_modules', 'vendor', 'dist', 'build', 'coverage', '.next', '.nuxt', '.turbo', '.cache', 'storage', 'tmp']);
|
|
123
|
-
|
|
15
|
+
|
|
16
|
+
const FRAMEWORK_CHOICES = [
|
|
17
|
+
'vue', 'react', 'next', 'nuxt', 'svelte', 'angular',
|
|
18
|
+
'laravel', 'cakephp', 'symfony', 'codeigniter',
|
|
19
|
+
'express', 'fastapi', 'django', 'flask', 'gin', 'fiber', 'echo', 'nestjs',
|
|
20
|
+
'rails', 'spring', 'aspnet',
|
|
21
|
+
'tauri', 'axum', 'actix', 'flutter', 'react-native'
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
const LANGUAGE_CHOICES = ['typescript', 'javascript', 'python', 'go', 'php', 'rust', 'java', 'ruby', 'swift', 'kotlin', 'csharp', 'cpp'];
|
|
25
|
+
const PROJECT_TYPE_CHOICES = ['web-app', 'api', 'mobile', 'desktop', 'cli', 'fullstack'];
|
|
26
|
+
const DATABASE_CHOICES = ['postgres', 'mysql', 'sqlite', 'mongodb', 'redis', 'dynamodb', 'none'];
|
|
27
|
+
const PACKAGE_MANAGER_CHOICES = ['npm', 'pnpm', 'yarn', 'bun', 'pip', 'uv', 'go mod', 'cargo', 'composer', 'auto-detect'];
|
|
28
|
+
const TEST_FRAMEWORK_CHOICES = ['jest', 'vitest', 'pytest', 'go test', 'phpunit', 'pest', 'rspec', 'junit', 'auto-detect'];
|
|
29
|
+
const CLOUD_CHOICES = ['aws', 'gcp', 'azure', 'vercel', 'fly', 'railway', 'none'];
|
|
30
|
+
const FRONTEND_FRAMEWORKS = ['vue', 'react', 'next', 'nuxt', 'svelte', 'angular'];
|
|
31
|
+
const BACKEND_FRAMEWORKS = ['laravel', 'cakephp', 'symfony', 'codeigniter', 'express', 'fastapi', 'django', 'flask', 'gin', 'fiber', 'echo', 'nestjs', 'rails', 'spring', 'aspnet', 'axum', 'actix'];
|
|
32
|
+
const MOBILE_DESKTOP_FRAMEWORKS = ['tauri', 'flutter', 'react-native'];
|
|
33
|
+
const IGNORED_SCAN_DIRS = new Set(['.git', 'node_modules', 'vendor', 'dist', 'build', 'coverage', '.next', '.nuxt', '.turbo', '.cache', 'storage', 'tmp']);
|
|
34
|
+
|
|
124
35
|
function printHelp() {
|
|
125
36
|
console.log(`
|
|
126
37
|
${pc.bold('Usage:')}
|
|
127
38
|
thachvd-kit init [--yes]
|
|
128
39
|
thachvd-kit setup [--yes]
|
|
129
40
|
thachvd-kit doctor
|
|
41
|
+
thachvd-kit prompt
|
|
130
42
|
thachvd-kit --version
|
|
131
43
|
thachvd-kit --help
|
|
132
44
|
|
|
133
45
|
${pc.bold('Commands:')}
|
|
134
46
|
init Generate project context files and documentation
|
|
135
|
-
setup Install/configure
|
|
47
|
+
setup Install/configure MCP and recommended local tooling
|
|
136
48
|
doctor Check project context and local integrations
|
|
49
|
+
prompt Print the saved project-indexing prompt again
|
|
137
50
|
|
|
138
51
|
${pc.bold('Options:')}
|
|
139
52
|
--yes Overwrite generated files or accept setup defaults
|
|
140
|
-
--no-setup-mcp
|
|
53
|
+
--no-setup-mcp Skip MCP setup (legacy init compatibility)
|
|
54
|
+
--no-setup-hook Skip the Claude Code Definition-of-Done Stop hook during setup
|
|
55
|
+
--no-install-rtk Skip auto-installing rtk via cargo during setup
|
|
56
|
+
--no-index Skip auto-indexing the repository with codebase-memory-mcp during setup
|
|
141
57
|
--version Show the installed CLI version
|
|
142
58
|
--help Show this help message
|
|
143
59
|
|
|
@@ -146,7 +62,7 @@ ${pc.bold('What gets generated:')}
|
|
|
146
62
|
CLAUDE.md Claude Code entry file that imports AGENTS.md
|
|
147
63
|
GEMINI.md Antigravity entry file that points to AGENTS.md
|
|
148
64
|
.cursorrules Cursor entry file that points to AGENTS.md
|
|
149
|
-
.agent/docs/ Project context, architecture,
|
|
65
|
+
.agent/docs/ Project context, architecture, workflow, tooling, and durable onboarding prompt
|
|
150
66
|
|
|
151
67
|
${pc.bold('Workflow guide:')}
|
|
152
68
|
Every task Superpowers Use the matching native Superpowers skill
|
|
@@ -163,734 +79,690 @@ ${pc.bold('Tip:')}
|
|
|
163
79
|
Run thachvd-kit setup for the integration checklist and doctor to verify it.
|
|
164
80
|
`);
|
|
165
81
|
}
|
|
166
|
-
|
|
167
|
-
function formatScanPreviewLines(scanned) {
|
|
168
|
-
return [
|
|
169
|
-
['App root', scanned.app_root || '.'],
|
|
170
|
-
['Project type', scanned.project_type || 'unknown'],
|
|
171
|
-
['Primary language', scanned.primary_language || 'unknown'],
|
|
172
|
-
['Frameworks', (scanned.frameworks || []).join(', ') || 'no framework'],
|
|
173
|
-
['Database', scanned.database || 'none'],
|
|
174
|
-
['Package manager', scanned.package_manager || 'auto-detect'],
|
|
175
|
-
['Test framework', scanned.test_framework || 'auto-detect'],
|
|
176
|
-
['Cloud', scanned.cloud || 'none'],
|
|
177
|
-
['Infrastructure', (scanned.infrastructure || []).join(', ') || 'none'],
|
|
178
|
-
['CI/CD', scanned.ci_cd || 'none']
|
|
179
|
-
];
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
async function askQuestions(questions) {
|
|
183
|
-
return prompts(questions, {
|
|
184
|
-
onCancel: () => {
|
|
185
|
-
console.log(pc.yellow('\nCancelled.'));
|
|
186
|
-
process.exit(130);
|
|
187
|
-
}
|
|
188
|
-
});
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
function fileExists(...parts) {
|
|
192
|
-
return fs.existsSync(path.join(targetDir, ...parts));
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
function fileExistsAt(baseDir, ...parts) {
|
|
196
|
-
return fs.existsSync(path.join(baseDir, ...parts));
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
function readJsonIfExists(...parts) {
|
|
200
|
-
const fullPath = path.join(targetDir, ...parts);
|
|
201
|
-
if (!fs.existsSync(fullPath)) return null;
|
|
202
|
-
try {
|
|
203
|
-
return JSON.parse(fs.readFileSync(fullPath, 'utf8'));
|
|
204
|
-
} catch {
|
|
205
|
-
return null;
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
function readJsonIfExistsAt(baseDir, ...parts) {
|
|
210
|
-
const fullPath = path.join(baseDir, ...parts);
|
|
211
|
-
if (!fs.existsSync(fullPath)) return null;
|
|
212
|
-
try {
|
|
213
|
-
return JSON.parse(fs.readFileSync(fullPath, 'utf8'));
|
|
214
|
-
} catch {
|
|
215
|
-
return null;
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
function readTextIfExists(...parts) {
|
|
220
|
-
const fullPath = path.join(targetDir, ...parts);
|
|
221
|
-
if (!fs.existsSync(fullPath)) return '';
|
|
222
|
-
try {
|
|
223
|
-
return fs.readFileSync(fullPath, 'utf8');
|
|
224
|
-
} catch {
|
|
225
|
-
return '';
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
function readTextIfExistsAt(baseDir, ...parts) {
|
|
230
|
-
const fullPath = path.join(baseDir, ...parts);
|
|
231
|
-
if (!fs.existsSync(fullPath)) return '';
|
|
232
|
-
try {
|
|
233
|
-
return fs.readFileSync(fullPath, 'utf8');
|
|
234
|
-
} catch {
|
|
235
|
-
return '';
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
function detectPackageManager(baseDir = targetDir) {
|
|
240
|
-
if (fileExistsAt(baseDir, 'pnpm-lock.yaml')) return 'pnpm';
|
|
241
|
-
if (fileExistsAt(baseDir, 'yarn.lock')) return 'yarn';
|
|
242
|
-
if (fileExistsAt(baseDir, 'bun.lockb') || fileExistsAt(baseDir, 'bun.lock')) return 'bun';
|
|
243
|
-
if (fileExistsAt(baseDir, 'package-lock.json')) return 'npm';
|
|
244
|
-
if (fileExistsAt(baseDir, 'uv.lock')) return 'uv';
|
|
245
|
-
if (fileExistsAt(baseDir, 'poetry.lock')) return 'pip';
|
|
246
|
-
if (fileExistsAt(baseDir, 'requirements.txt') || fileExistsAt(baseDir, 'pyproject.toml')) return 'pip';
|
|
247
|
-
if (fileExistsAt(baseDir, 'go.mod')) return 'go mod';
|
|
248
|
-
if (fileExistsAt(baseDir, 'Cargo.toml')) return 'cargo';
|
|
249
|
-
if (fileExistsAt(baseDir, 'composer.json')) return 'composer';
|
|
250
|
-
return 'auto-detect';
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
function uniq(values) {
|
|
254
|
-
return Array.from(new Set(values.filter(Boolean)));
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
function detectAppRoot(baseDir) {
|
|
258
|
-
const candidates = [baseDir];
|
|
259
|
-
const queue = [{ dir: baseDir, depth: 0 }];
|
|
260
|
-
|
|
261
|
-
while (queue.length > 0) {
|
|
262
|
-
const { dir, depth } = queue.shift();
|
|
263
|
-
if (depth >= 2) continue;
|
|
264
|
-
let entries = [];
|
|
265
|
-
try {
|
|
266
|
-
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
267
|
-
} catch {
|
|
268
|
-
continue;
|
|
269
|
-
}
|
|
270
|
-
for (const entry of entries) {
|
|
271
|
-
if (!entry.isDirectory()) continue;
|
|
272
|
-
if (IGNORED_SCAN_DIRS.has(entry.name)) continue;
|
|
273
|
-
const nextDir = path.join(dir, entry.name);
|
|
274
|
-
candidates.push(nextDir);
|
|
275
|
-
queue.push({ dir: nextDir, depth: depth + 1 });
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
const scoreCandidate = dir => {
|
|
280
|
-
let score = 0;
|
|
281
|
-
if (fileExistsAt(dir, 'composer.json')) score += 5;
|
|
282
|
-
if (fileExistsAt(dir, 'package.json')) score += 4;
|
|
283
|
-
if (fileExistsAt(dir, 'Cargo.toml')) score += 4;
|
|
284
|
-
if (fileExistsAt(dir, 'go.mod')) score += 4;
|
|
285
|
-
if (fileExistsAt(dir, 'pyproject.toml') || fileExistsAt(dir, 'requirements.txt')) score += 4;
|
|
286
|
-
if (fileExistsAt(dir, 'artisan')) score += 5;
|
|
287
|
-
if (fileExistsAt(dir, 'bin', 'cake') || fileExistsAt(dir, 'app', 'Config', 'core.php')) score += 5;
|
|
288
|
-
if (fileExistsAt(dir, 'src-tauri', 'tauri.conf.json') || fileExistsAt(dir, 'src-tauri', 'tauri.conf.json5')) score += 5;
|
|
289
|
-
if (fileExistsAt(dir, 'manage.py')) score += 4;
|
|
290
|
-
if (fileExistsAt(dir, 'Gemfile')) score += 4;
|
|
291
|
-
if (fileExistsAt(dir, 'pom.xml') || fileExistsAt(dir, 'build.gradle') || fileExistsAt(dir, 'build.gradle.kts')) score += 4;
|
|
292
|
-
if (fileExistsAt(dir, 'app', 'View') || fileExistsAt(dir, 'resources', 'views') || fileExistsAt(dir, 'templates') || fileExistsAt(dir, 'app', 'views')) score += 2;
|
|
293
|
-
return score;
|
|
294
|
-
};
|
|
295
|
-
|
|
296
|
-
const ranked = candidates
|
|
297
|
-
.map(dir => ({ dir, score: scoreCandidate(dir) }))
|
|
298
|
-
.filter(item => item.score > 0)
|
|
299
|
-
.sort((a, b) => (b.score - a.score) || (a.dir.length - b.dir.length));
|
|
300
|
-
|
|
301
|
-
return ranked[0]?.dir || baseDir;
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
function repoHasAny(baseDir, relativePaths, maxDepth = 3) {
|
|
305
|
-
const queue = [{ dir: baseDir, depth: 0 }];
|
|
306
|
-
while (queue.length > 0) {
|
|
307
|
-
const { dir, depth } = queue.shift();
|
|
308
|
-
for (const relativePath of relativePaths) {
|
|
309
|
-
if (fileExistsAt(dir, ...relativePath)) return true;
|
|
310
|
-
}
|
|
311
|
-
if (depth >= maxDepth) continue;
|
|
312
|
-
let entries = [];
|
|
313
|
-
try {
|
|
314
|
-
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
315
|
-
} catch {
|
|
316
|
-
continue;
|
|
317
|
-
}
|
|
318
|
-
for (const entry of entries) {
|
|
319
|
-
if (!entry.isDirectory()) continue;
|
|
320
|
-
if (IGNORED_SCAN_DIRS.has(entry.name)) continue;
|
|
321
|
-
queue.push({ dir: path.join(dir, entry.name), depth: depth + 1 });
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
return false;
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
function evidenceIf(condition, label, weight = 1) {
|
|
328
|
-
return condition ? { label, weight } : null;
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
function buildDetection(id, evidence, meta = {}) {
|
|
332
|
-
const hits = evidence.filter(Boolean);
|
|
333
|
-
const score = hits.reduce((sum, hit) => sum + hit.weight, 0);
|
|
334
|
-
return {
|
|
335
|
-
id,
|
|
336
|
-
score,
|
|
337
|
-
evidence: hits.map(hit => hit.label),
|
|
338
|
-
...meta
|
|
339
|
-
};
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
function confidenceLabel(score) {
|
|
343
|
-
if (score >= 6) return 'high';
|
|
344
|
-
if (score >= 3) return 'medium';
|
|
345
|
-
return 'low';
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
function hasDependency(depNames, ...names) {
|
|
349
|
-
return names.some(name => depNames.has(name));
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
function hasComposerDependency(composerDepNames, ...names) {
|
|
353
|
-
return names.some(name => composerDepNames.has(name));
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
function hasRootFile(rootEntries, ...filenames) {
|
|
357
|
-
return filenames.some(name => rootEntries.has(name));
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
function textMatches(text, pattern) {
|
|
361
|
-
return Boolean(text && pattern.test(text));
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
const LANGUAGE_DETECTORS = [
|
|
365
|
-
{
|
|
366
|
-
id: 'typescript',
|
|
367
|
-
detect: ctx => buildDetection('typescript', [
|
|
368
|
-
evidenceIf(ctx.fileExists('tsconfig.json'), 'found tsconfig.json', 4),
|
|
369
|
-
evidenceIf(hasDependency(ctx.depNames, 'typescript', 'ts-node', 'tsx'), 'package.json -> typescript toolchain', 2)
|
|
370
|
-
])
|
|
371
|
-
},
|
|
372
|
-
{
|
|
373
|
-
id: 'javascript',
|
|
374
|
-
detect: ctx => buildDetection('javascript', [
|
|
375
|
-
evidenceIf(ctx.fileExists('package.json'), 'found package.json', 2),
|
|
376
|
-
evidenceIf(ctx.depNames.size > 0, 'package.json -> JavaScript dependency graph', 1)
|
|
377
|
-
])
|
|
378
|
-
},
|
|
379
|
-
{
|
|
380
|
-
id: 'python',
|
|
381
|
-
detect: ctx => buildDetection('python', [
|
|
382
|
-
evidenceIf(ctx.fileExists('requirements.txt'), 'found requirements.txt', 3),
|
|
383
|
-
evidenceIf(ctx.fileExists('pyproject.toml'), 'found pyproject.toml', 2),
|
|
384
|
-
evidenceIf(textMatches(ctx.pyprojectText, /\[tool\.(poetry|uv|hatch|pdm)\]/i), 'pyproject.toml -> Python tool section', 2)
|
|
385
|
-
])
|
|
386
|
-
},
|
|
387
|
-
{
|
|
388
|
-
id: 'go',
|
|
389
|
-
detect: ctx => buildDetection('go', [
|
|
390
|
-
evidenceIf(ctx.fileExists('go.mod'), 'found go.mod', 4)
|
|
391
|
-
])
|
|
392
|
-
},
|
|
393
|
-
{
|
|
394
|
-
id: 'php',
|
|
395
|
-
detect: ctx => buildDetection('php', [
|
|
396
|
-
evidenceIf(ctx.fileExists('composer.json'), 'found composer.json', 4),
|
|
397
|
-
evidenceIf(ctx.composerDepNames.size > 0, 'composer.json -> PHP dependency graph', 1)
|
|
398
|
-
])
|
|
399
|
-
},
|
|
400
|
-
{
|
|
401
|
-
id: 'rust',
|
|
402
|
-
detect: ctx => buildDetection('rust', [
|
|
403
|
-
evidenceIf(ctx.fileExists('Cargo.toml'), 'found Cargo.toml', 4),
|
|
404
|
-
evidenceIf(ctx.fileExists('src-tauri', 'Cargo.toml'), 'found src-tauri/Cargo.toml', 5)
|
|
405
|
-
])
|
|
406
|
-
},
|
|
407
|
-
{
|
|
408
|
-
id: 'java',
|
|
409
|
-
detect: ctx => buildDetection('java', [
|
|
410
|
-
evidenceIf(ctx.fileExists('pom.xml'), 'found pom.xml', 4),
|
|
411
|
-
evidenceIf(ctx.fileExists('build.gradle') || ctx.fileExists('build.gradle.kts'), 'found Gradle build file', 3)
|
|
412
|
-
])
|
|
413
|
-
},
|
|
414
|
-
{
|
|
415
|
-
id: 'ruby',
|
|
416
|
-
detect: ctx => buildDetection('ruby', [
|
|
417
|
-
evidenceIf(ctx.fileExists('Gemfile'), 'found Gemfile', 4),
|
|
418
|
-
evidenceIf(ctx.fileExists('config.ru'), 'found config.ru', 1)
|
|
419
|
-
])
|
|
420
|
-
},
|
|
421
|
-
{
|
|
422
|
-
id: 'csharp',
|
|
423
|
-
detect: ctx => buildDetection('csharp', [
|
|
424
|
-
evidenceIf(ctx.rootEntries.some(name => name.endsWith('.sln') || name.endsWith('.csproj')), 'found .sln/.csproj', 4),
|
|
425
|
-
evidenceIf(ctx.fileExists('Program.cs'), 'found Program.cs', 1)
|
|
426
|
-
])
|
|
427
|
-
}
|
|
428
|
-
];
|
|
429
|
-
|
|
430
|
-
const FRAMEWORK_DETECTORS = [
|
|
431
|
-
{
|
|
432
|
-
id: 'next',
|
|
433
|
-
kind: 'frontend',
|
|
434
|
-
projectType: 'web-app',
|
|
435
|
-
detect: ctx => buildDetection('next', [
|
|
436
|
-
evidenceIf(hasDependency(ctx.depNames, 'next'), 'package.json -> next', 4),
|
|
437
|
-
evidenceIf(ctx.fileExists('next.config.js') || ctx.fileExists('next.config.mjs') || ctx.fileExists('next.config.ts'), 'found next.config.*', 3),
|
|
438
|
-
evidenceIf(ctx.fileExists('app') || ctx.fileExists('pages'), 'found app/ or pages/', 1)
|
|
439
|
-
])
|
|
440
|
-
},
|
|
441
|
-
{
|
|
442
|
-
id: 'react',
|
|
443
|
-
kind: 'frontend',
|
|
444
|
-
projectType: 'web-app',
|
|
445
|
-
detect: ctx => buildDetection('react', [
|
|
446
|
-
evidenceIf(hasDependency(ctx.depNames, 'react'), 'package.json -> react', 4),
|
|
447
|
-
evidenceIf(hasDependency(ctx.depNames, 'react-dom'), 'package.json -> react-dom', 2)
|
|
448
|
-
])
|
|
449
|
-
},
|
|
450
|
-
{
|
|
451
|
-
id: 'vue',
|
|
452
|
-
kind: 'frontend',
|
|
453
|
-
projectType: 'web-app',
|
|
454
|
-
detect: ctx => buildDetection('vue', [
|
|
455
|
-
evidenceIf(hasDependency(ctx.depNames, 'vue'), 'package.json -> vue', 4)
|
|
456
|
-
])
|
|
457
|
-
},
|
|
458
|
-
{
|
|
459
|
-
id: 'nuxt',
|
|
460
|
-
kind: 'frontend',
|
|
461
|
-
projectType: 'web-app',
|
|
462
|
-
detect: ctx => buildDetection('nuxt', [
|
|
463
|
-
evidenceIf(hasDependency(ctx.depNames, 'nuxt'), 'package.json -> nuxt', 4),
|
|
464
|
-
evidenceIf(ctx.fileExists('nuxt.config.ts') || ctx.fileExists('nuxt.config.js'), 'found nuxt.config.*', 3)
|
|
465
|
-
])
|
|
466
|
-
},
|
|
467
|
-
{
|
|
468
|
-
id: 'svelte',
|
|
469
|
-
kind: 'frontend',
|
|
470
|
-
projectType: 'web-app',
|
|
471
|
-
detect: ctx => buildDetection('svelte', [
|
|
472
|
-
evidenceIf(hasDependency(ctx.depNames, 'svelte', '@sveltejs/kit'), 'package.json -> svelte', 4)
|
|
473
|
-
])
|
|
474
|
-
},
|
|
475
|
-
{
|
|
476
|
-
id: 'angular',
|
|
477
|
-
kind: 'frontend',
|
|
478
|
-
projectType: 'web-app',
|
|
479
|
-
detect: ctx => buildDetection('angular', [
|
|
480
|
-
evidenceIf(hasDependency(ctx.depNames, '@angular/core'), 'package.json -> @angular/core', 4),
|
|
481
|
-
evidenceIf(ctx.fileExists('angular.json'), 'found angular.json', 3)
|
|
482
|
-
])
|
|
483
|
-
},
|
|
484
|
-
{
|
|
485
|
-
id: 'express',
|
|
486
|
-
kind: 'backend',
|
|
487
|
-
language: 'javascript',
|
|
488
|
-
projectType: 'api',
|
|
489
|
-
detect: ctx => buildDetection('express', [
|
|
490
|
-
evidenceIf(hasDependency(ctx.depNames, 'express'), 'package.json -> express', 4)
|
|
491
|
-
])
|
|
492
|
-
},
|
|
493
|
-
{
|
|
494
|
-
id: 'nestjs',
|
|
495
|
-
kind: 'backend',
|
|
496
|
-
language: 'javascript',
|
|
497
|
-
projectType: 'api',
|
|
498
|
-
detect: ctx => buildDetection('nestjs', [
|
|
499
|
-
evidenceIf(hasDependency(ctx.depNames, '@nestjs/core'), 'package.json -> @nestjs/core', 4)
|
|
500
|
-
])
|
|
501
|
-
},
|
|
502
|
-
{
|
|
503
|
-
id: 'fastapi',
|
|
504
|
-
kind: 'backend',
|
|
505
|
-
language: 'python',
|
|
506
|
-
projectType: 'api',
|
|
507
|
-
detect: ctx => buildDetection('fastapi', [
|
|
508
|
-
evidenceIf(textMatches(ctx.requirementsText, /\bfastapi\b/i) || textMatches(ctx.pyprojectText, /\bfastapi\b/i), 'Python deps -> fastapi', 4)
|
|
509
|
-
])
|
|
510
|
-
},
|
|
511
|
-
{
|
|
512
|
-
id: 'django',
|
|
513
|
-
kind: 'backend',
|
|
514
|
-
language: 'python',
|
|
515
|
-
projectType: 'web-app',
|
|
516
|
-
detect: ctx => buildDetection('django', [
|
|
517
|
-
evidenceIf(textMatches(ctx.requirementsText, /\bdjango\b/i) || textMatches(ctx.pyprojectText, /\bdjango\b/i), 'Python deps -> django', 4),
|
|
518
|
-
evidenceIf(ctx.fileExists('manage.py'), 'found manage.py', 2),
|
|
519
|
-
evidenceIf(ctx.fileExists('templates'), 'found templates/', 1)
|
|
520
|
-
])
|
|
521
|
-
},
|
|
522
|
-
{
|
|
523
|
-
id: 'flask',
|
|
524
|
-
kind: 'backend',
|
|
525
|
-
language: 'python',
|
|
526
|
-
projectType: 'api',
|
|
527
|
-
detect: ctx => buildDetection('flask', [
|
|
528
|
-
evidenceIf(textMatches(ctx.requirementsText, /\bflask\b/i) || textMatches(ctx.pyprojectText, /\bflask\b/i), 'Python deps -> flask', 4)
|
|
529
|
-
])
|
|
530
|
-
},
|
|
531
|
-
{
|
|
532
|
-
id: 'laravel',
|
|
533
|
-
kind: 'backend',
|
|
534
|
-
language: 'php',
|
|
535
|
-
projectType: 'web-app',
|
|
536
|
-
detect: ctx => buildDetection('laravel', [
|
|
537
|
-
evidenceIf(hasComposerDependency(ctx.composerDepNames, 'laravel/framework'), 'composer.json -> laravel/framework', 4),
|
|
538
|
-
evidenceIf(ctx.fileExists('artisan'), 'found artisan', 3),
|
|
539
|
-
evidenceIf(ctx.fileExists('resources', 'views'), 'found resources/views', 1)
|
|
540
|
-
])
|
|
541
|
-
},
|
|
542
|
-
{
|
|
543
|
-
id: 'cakephp',
|
|
544
|
-
kind: 'backend',
|
|
545
|
-
language: 'php',
|
|
546
|
-
projectType: 'web-app',
|
|
547
|
-
detect: ctx => buildDetection('cakephp', [
|
|
548
|
-
evidenceIf(hasComposerDependency(ctx.composerDepNames, 'cakephp/cakephp'), 'composer.json -> cakephp/cakephp', 4),
|
|
549
|
-
evidenceIf(ctx.fileExists('bin', 'cake'), 'found bin/cake', 3),
|
|
550
|
-
evidenceIf(ctx.fileExists('config', 'app.php'), 'found config/app.php', 2),
|
|
551
|
-
evidenceIf(ctx.fileExists('app', 'Config', 'core.php'), 'found app/Config/core.php (CakePHP legacy)', 3),
|
|
552
|
-
evidenceIf(ctx.fileExists('app', 'Controller', 'AppController.php'), 'found app/Controller/AppController.php', 2),
|
|
553
|
-
evidenceIf(ctx.fileExists('app', 'Model', 'AppModel.php'), 'found app/Model/AppModel.php', 2),
|
|
554
|
-
evidenceIf(ctx.fileExists('lib', 'Cake'), 'found lib/Cake', 3),
|
|
555
|
-
evidenceIf(ctx.fileExists('cake', 'console', 'cake'), 'found cake/console/cake', 3),
|
|
556
|
-
evidenceIf(ctx.fileExists('app', 'View'), 'found app/View', 2),
|
|
557
|
-
evidenceIf(textMatches(ctx.viewFilesText, /\.ctp$/im), 'found .ctp view templates', 2)
|
|
558
|
-
])
|
|
559
|
-
},
|
|
560
|
-
{
|
|
561
|
-
id: 'symfony',
|
|
562
|
-
kind: 'backend',
|
|
563
|
-
language: 'php',
|
|
564
|
-
projectType: 'web-app',
|
|
565
|
-
detect: ctx => buildDetection('symfony', [
|
|
566
|
-
evidenceIf(hasComposerDependency(ctx.composerDepNames, 'symfony/framework-bundle'), 'composer.json -> symfony/framework-bundle', 4),
|
|
567
|
-
evidenceIf(ctx.fileExists('bin', 'console'), 'found bin/console', 2),
|
|
568
|
-
evidenceIf(ctx.fileExists('config', 'bundles.php'), 'found config/bundles.php', 1),
|
|
569
|
-
evidenceIf(ctx.fileExists('templates'), 'found templates/', 1)
|
|
570
|
-
])
|
|
571
|
-
},
|
|
572
|
-
{
|
|
573
|
-
id: 'codeigniter',
|
|
574
|
-
kind: 'backend',
|
|
575
|
-
language: 'php',
|
|
576
|
-
projectType: 'api',
|
|
577
|
-
detect: ctx => buildDetection('codeigniter', [
|
|
578
|
-
evidenceIf(hasComposerDependency(ctx.composerDepNames, 'codeigniter4/framework'), 'composer.json -> codeigniter4/framework', 4),
|
|
579
|
-
evidenceIf(ctx.fileExists('spark'), 'found spark', 2),
|
|
580
|
-
evidenceIf(ctx.fileExists('app', 'Config', 'App.php'), 'found app/Config/App.php', 1)
|
|
581
|
-
])
|
|
582
|
-
},
|
|
583
|
-
{
|
|
584
|
-
id: 'gin',
|
|
585
|
-
kind: 'backend',
|
|
586
|
-
language: 'go',
|
|
587
|
-
projectType: 'api',
|
|
588
|
-
detect: ctx => buildDetection('gin', [
|
|
589
|
-
evidenceIf(textMatches(ctx.goModText, /\bgithub\.com\/gin-gonic\/gin\b/i), 'go.mod -> github.com/gin-gonic/gin', 4)
|
|
590
|
-
])
|
|
591
|
-
},
|
|
592
|
-
{
|
|
593
|
-
id: 'fiber',
|
|
594
|
-
kind: 'backend',
|
|
595
|
-
language: 'go',
|
|
596
|
-
projectType: 'api',
|
|
597
|
-
detect: ctx => buildDetection('fiber', [
|
|
598
|
-
evidenceIf(textMatches(ctx.goModText, /\bgithub\.com\/gofiber\/fiber\b/i), 'go.mod -> github.com/gofiber/fiber', 4)
|
|
599
|
-
])
|
|
600
|
-
},
|
|
601
|
-
{
|
|
602
|
-
id: 'echo',
|
|
603
|
-
kind: 'backend',
|
|
604
|
-
language: 'go',
|
|
605
|
-
projectType: 'api',
|
|
606
|
-
detect: ctx => buildDetection('echo', [
|
|
607
|
-
evidenceIf(textMatches(ctx.goModText, /\bgithub\.com\/labstack\/echo\b/i), 'go.mod -> github.com/labstack/echo', 4)
|
|
608
|
-
])
|
|
609
|
-
},
|
|
610
|
-
{
|
|
611
|
-
id: 'rails',
|
|
612
|
-
kind: 'backend',
|
|
613
|
-
language: 'ruby',
|
|
614
|
-
projectType: 'web-app',
|
|
615
|
-
detect: ctx => buildDetection('rails', [
|
|
616
|
-
evidenceIf(textMatches(ctx.gemfileText, /^\s*gem\s+["']rails["']/im), 'Gemfile -> rails', 4),
|
|
617
|
-
evidenceIf(ctx.fileExists('bin', 'rails'), 'found bin/rails', 2),
|
|
618
|
-
evidenceIf(ctx.fileExists('app', 'views'), 'found app/views', 1)
|
|
619
|
-
])
|
|
620
|
-
},
|
|
621
|
-
{
|
|
622
|
-
id: 'spring',
|
|
623
|
-
kind: 'backend',
|
|
624
|
-
language: 'java',
|
|
625
|
-
projectType: 'api',
|
|
626
|
-
detect: ctx => buildDetection('spring', [
|
|
627
|
-
evidenceIf(textMatches(ctx.pomText, /spring-boot/i) || textMatches(ctx.gradleText, /spring-boot/i), 'Java build -> spring-boot', 4)
|
|
628
|
-
])
|
|
629
|
-
},
|
|
630
|
-
{
|
|
631
|
-
id: 'aspnet',
|
|
632
|
-
kind: 'backend',
|
|
633
|
-
language: 'csharp',
|
|
634
|
-
projectType: 'api',
|
|
635
|
-
detect: ctx => buildDetection('aspnet', [
|
|
636
|
-
evidenceIf(textMatches(ctx.csprojText, /Microsoft\.NET\.Sdk\.Web/i), '.csproj -> Microsoft.NET.Sdk.Web', 4),
|
|
637
|
-
evidenceIf(textMatches(ctx.programCsText, /WebApplication\.CreateBuilder/i), 'Program.cs -> WebApplication.CreateBuilder', 2)
|
|
638
|
-
])
|
|
639
|
-
},
|
|
640
|
-
{
|
|
641
|
-
id: 'tauri',
|
|
642
|
-
kind: 'desktop',
|
|
643
|
-
language: 'rust',
|
|
644
|
-
projectType: 'desktop',
|
|
645
|
-
detect: ctx => buildDetection('tauri', [
|
|
646
|
-
evidenceIf(hasDependency(ctx.depNames, '@tauri-apps/api'), 'package.json -> @tauri-apps/api', 4),
|
|
647
|
-
evidenceIf(ctx.fileExists('src-tauri', 'tauri.conf.json') || ctx.fileExists('src-tauri', 'tauri.conf.json5'), 'found src-tauri/tauri.conf.*', 4),
|
|
648
|
-
evidenceIf(ctx.fileExists('src-tauri', 'Cargo.toml'), 'found src-tauri/Cargo.toml', 2)
|
|
649
|
-
])
|
|
650
|
-
},
|
|
651
|
-
{
|
|
652
|
-
id: 'axum',
|
|
653
|
-
kind: 'backend',
|
|
654
|
-
language: 'rust',
|
|
655
|
-
projectType: 'api',
|
|
656
|
-
detect: ctx => buildDetection('axum', [
|
|
657
|
-
evidenceIf(textMatches(ctx.cargoText, /\baxum\b/i), 'Cargo.toml -> axum', 4)
|
|
658
|
-
])
|
|
659
|
-
},
|
|
660
|
-
{
|
|
661
|
-
id: 'actix',
|
|
662
|
-
kind: 'backend',
|
|
663
|
-
language: 'rust',
|
|
664
|
-
projectType: 'api',
|
|
665
|
-
detect: ctx => buildDetection('actix', [
|
|
666
|
-
evidenceIf(textMatches(ctx.cargoText, /\bactix-web\b/i), 'Cargo.toml -> actix-web', 4)
|
|
667
|
-
])
|
|
668
|
-
},
|
|
669
|
-
{
|
|
670
|
-
id: 'flutter',
|
|
671
|
-
kind: 'mobile',
|
|
672
|
-
projectType: 'mobile',
|
|
673
|
-
detect: ctx => buildDetection('flutter', [
|
|
674
|
-
evidenceIf(textMatches(ctx.pubspecText, /^\s*flutter\s*:/im), 'pubspec.yaml -> flutter', 4)
|
|
675
|
-
])
|
|
676
|
-
},
|
|
677
|
-
{
|
|
678
|
-
id: 'react-native',
|
|
679
|
-
kind: 'mobile',
|
|
680
|
-
projectType: 'mobile',
|
|
681
|
-
detect: ctx => buildDetection('react-native', [
|
|
682
|
-
evidenceIf(hasDependency(ctx.depNames, 'react-native', 'expo'), 'package.json -> react-native/expo', 4)
|
|
683
|
-
])
|
|
684
|
-
}
|
|
685
|
-
];
|
|
686
|
-
|
|
687
|
-
function scanProject() {
|
|
688
|
-
const appRoot = detectAppRoot(targetDir);
|
|
689
|
-
const pkg = readJsonIfExistsAt(appRoot, 'package.json') || {};
|
|
690
|
-
const composer = readJsonIfExistsAt(appRoot, 'composer.json') || {};
|
|
691
|
-
const rootEntries = fs.existsSync(appRoot) ? fs.readdirSync(appRoot) : [];
|
|
692
|
-
const rootCsproj = rootEntries.find(name => name.endsWith('.csproj')) || '';
|
|
693
|
-
const deps = {
|
|
694
|
-
...(pkg.dependencies || {}),
|
|
695
|
-
...(pkg.devDependencies || {}),
|
|
696
|
-
...(pkg.peerDependencies || {})
|
|
697
|
-
};
|
|
698
|
-
const depNames = new Set(Object.keys(deps));
|
|
699
|
-
const composerDeps = {
|
|
700
|
-
...(composer.require || {}),
|
|
701
|
-
...(composer['require-dev'] || {})
|
|
702
|
-
};
|
|
703
|
-
const composerDepNames = new Set(Object.keys(composerDeps));
|
|
704
|
-
const ctx = {
|
|
705
|
-
pkg,
|
|
706
|
-
composer,
|
|
707
|
-
depNames,
|
|
708
|
-
composerDepNames,
|
|
709
|
-
rootEntries,
|
|
710
|
-
fileExists: (...parts) => fileExistsAt(appRoot, ...parts),
|
|
711
|
-
requirementsText: readTextIfExistsAt(appRoot, 'requirements.txt'),
|
|
712
|
-
pyprojectText: readTextIfExistsAt(appRoot, 'pyproject.toml'),
|
|
713
|
-
goModText: readTextIfExistsAt(appRoot, 'go.mod'),
|
|
714
|
-
pubspecText: readTextIfExistsAt(appRoot, 'pubspec.yaml'),
|
|
715
|
-
gemfileText: readTextIfExistsAt(appRoot, 'Gemfile'),
|
|
716
|
-
pomText: readTextIfExistsAt(appRoot, 'pom.xml'),
|
|
717
|
-
gradleText: `${readTextIfExistsAt(appRoot, 'build.gradle')}\n${readTextIfExistsAt(appRoot, 'build.gradle.kts')}`,
|
|
718
|
-
cargoText: `${readTextIfExistsAt(appRoot, 'Cargo.toml')}\n${readTextIfExistsAt(appRoot, 'src-tauri', 'Cargo.toml')}`,
|
|
719
|
-
csprojText: rootCsproj ? readTextIfExistsAt(appRoot, rootCsproj) : '',
|
|
720
|
-
programCsText: readTextIfExistsAt(appRoot, 'Program.cs'),
|
|
721
|
-
viewFilesText: fs.existsSync(path.join(appRoot, 'app', 'View'))
|
|
722
|
-
? fs.readdirSync(path.join(appRoot, 'app', 'View'), { recursive: true }).join('\n')
|
|
723
|
-
: ''
|
|
724
|
-
};
|
|
725
|
-
|
|
726
|
-
const languageDetections = LANGUAGE_DETECTORS
|
|
727
|
-
.map(detector => detector.detect(ctx))
|
|
728
|
-
.filter(result => result.score >= 2)
|
|
729
|
-
.sort((a, b) => b.score - a.score);
|
|
730
|
-
|
|
731
|
-
const frameworkDetails = FRAMEWORK_DETECTORS
|
|
732
|
-
.map(detector => {
|
|
733
|
-
const result = detector.detect(ctx);
|
|
734
|
-
if (result.score < 3) return null;
|
|
735
|
-
return {
|
|
736
|
-
...result,
|
|
737
|
-
kind: detector.kind,
|
|
738
|
-
language: detector.language,
|
|
739
|
-
projectType: detector.projectType,
|
|
740
|
-
confidence: confidenceLabel(result.score)
|
|
741
|
-
};
|
|
742
|
-
})
|
|
743
|
-
.filter(Boolean)
|
|
744
|
-
.sort((a, b) => b.score - a.score);
|
|
745
|
-
|
|
746
|
-
const frameworkLanguageScores = frameworkDetails.reduce((acc, item) => {
|
|
747
|
-
if (!item.language) return acc;
|
|
748
|
-
acc[item.language] = (acc[item.language] || 0) + item.score;
|
|
749
|
-
return acc;
|
|
750
|
-
}, {});
|
|
751
|
-
const primaryLanguage = Object.keys(frameworkLanguageScores).sort((a, b) => frameworkLanguageScores[b] - frameworkLanguageScores[a])[0]
|
|
752
|
-
|| languageDetections[0]?.id
|
|
753
|
-
|| 'typescript';
|
|
754
|
-
const frameworks = uniq(frameworkDetails.map(item => item.id)).filter(name => FRAMEWORK_CHOICES.includes(name));
|
|
755
|
-
const infrastructure = [];
|
|
756
|
-
let projectType = '';
|
|
757
|
-
let database = 'none';
|
|
758
|
-
let testFramework = 'auto-detect';
|
|
759
|
-
let cloud = 'none';
|
|
760
|
-
let ciCd = 'none';
|
|
761
|
-
|
|
762
|
-
if (frameworkDetails.some(item => item.projectType === 'desktop')) projectType = 'desktop';
|
|
763
|
-
else if (frameworkDetails.some(item => item.projectType === 'mobile')) projectType = 'mobile';
|
|
764
|
-
else if (frameworkDetails.some(item => item.kind === 'frontend') && frameworkDetails.some(item => item.kind === 'backend')) projectType = 'fullstack';
|
|
765
|
-
else if (frameworkDetails.some(item => item.projectType === 'web-app')) projectType = 'web-app';
|
|
766
|
-
else if (frameworkDetails.some(item => item.kind === 'backend')) projectType = 'api';
|
|
767
|
-
else if (frameworkDetails.some(item => item.kind === 'frontend')) projectType = 'web-app';
|
|
768
|
-
else if (fileExistsAt(appRoot, 'package.json') && pkg.bin) projectType = 'cli';
|
|
769
|
-
else if (['php', 'python', 'go', 'java', 'ruby', 'csharp', 'rust'].includes(primaryLanguage)) projectType = 'api';
|
|
770
|
-
|
|
771
|
-
if (hasDependency(depNames, 'pg', 'postgres', '@prisma/client') || /postgres/i.test(JSON.stringify(pkg))) database = 'postgres';
|
|
772
|
-
else if (hasDependency(depNames, 'mysql', 'mysql2')) database = 'mysql';
|
|
773
|
-
else if (hasDependency(depNames, 'sqlite3', 'better-sqlite3', 'rusqlite')) database = 'sqlite';
|
|
774
|
-
else if (hasDependency(depNames, 'mongodb', 'mongoose')) database = 'mongodb';
|
|
775
|
-
else if (hasDependency(depNames, 'redis', 'ioredis')) database = 'redis';
|
|
776
|
-
else if (hasDependency(depNames, '@aws-sdk/client-dynamodb')) database = 'dynamodb';
|
|
777
|
-
|
|
778
|
-
if (hasDependency(depNames, 'vitest')) testFramework = 'vitest';
|
|
779
|
-
else if (hasDependency(depNames, 'jest')) testFramework = 'jest';
|
|
780
|
-
else if (hasDependency(depNames, 'pytest') || textMatches(ctx.requirementsText, /\bpytest\b/i) || textMatches(ctx.pyprojectText, /\bpytest\b/i)) testFramework = 'pytest';
|
|
781
|
-
else if (hasComposerDependency(composerDepNames, 'phpunit/phpunit')) testFramework = 'phpunit';
|
|
782
|
-
else if (hasComposerDependency(composerDepNames, 'pestphp/pest')) testFramework = 'pest';
|
|
783
|
-
else if (fileExistsAt(appRoot, 'go.mod')) testFramework = 'go test';
|
|
784
|
-
else if (textMatches(ctx.gemfileText, /^\s*gem\s+["']rspec["']/im)) testFramework = 'rspec';
|
|
785
|
-
|
|
786
|
-
if (repoHasAny(targetDir, [['Dockerfile'], ['docker-compose.yml'], ['docker-compose.yaml']])) {
|
|
787
|
-
infrastructure.push('docker');
|
|
788
|
-
}
|
|
789
|
-
|
|
790
|
-
if (repoHasAny(targetDir, [['vercel.json']])) {
|
|
791
|
-
cloud = 'vercel';
|
|
792
|
-
infrastructure.push('vercel');
|
|
793
|
-
} else if (repoHasAny(targetDir, [['fly.toml']])) {
|
|
794
|
-
cloud = 'fly';
|
|
795
|
-
infrastructure.push('fly');
|
|
796
|
-
} else if (repoHasAny(targetDir, [['railway.json'], ['railway.toml']])) {
|
|
797
|
-
cloud = 'railway';
|
|
798
|
-
infrastructure.push('railway');
|
|
799
|
-
}
|
|
800
|
-
|
|
801
|
-
if (repoHasAny(targetDir, [['.github', 'workflows'], ['.gitlab-ci.yml'], ['.circleci', 'config.yml'], ['Jenkinsfile']], 2)) {
|
|
802
|
-
if (repoHasAny(targetDir, [['.github', 'workflows']], 2)) ciCd = 'github-actions';
|
|
803
|
-
else if (repoHasAny(targetDir, [['.gitlab-ci.yml']], 2)) ciCd = 'gitlab-ci';
|
|
804
|
-
else if (repoHasAny(targetDir, [['.circleci', 'config.yml']], 2)) ciCd = 'circleci';
|
|
805
|
-
else if (repoHasAny(targetDir, [['Jenkinsfile']], 2)) ciCd = 'jenkins';
|
|
806
|
-
}
|
|
807
|
-
|
|
808
|
-
const scanEvidence = [
|
|
809
|
-
...(languageDetections[0] ? [`language:${languageDetections[0].id} => ${languageDetections[0].evidence.join('; ')}`] : []),
|
|
810
|
-
...frameworkDetails.map(item => `${item.id} [${item.confidence}] => ${item.evidence.join('; ')}`)
|
|
811
|
-
];
|
|
812
|
-
|
|
813
|
-
return {
|
|
814
|
-
project_name: path.basename(targetDir),
|
|
815
|
-
app_root: path.relative(targetDir, appRoot) || '.',
|
|
816
|
-
description: pkg.description || composer.description || 'A software project',
|
|
817
|
-
project_type: PROJECT_TYPE_CHOICES.includes(projectType) ? projectType : 'web-app',
|
|
818
|
-
primary_language: LANGUAGE_CHOICES.includes(primaryLanguage) ? primaryLanguage : 'typescript',
|
|
819
|
-
frameworks,
|
|
820
|
-
framework_details: frameworkDetails,
|
|
821
|
-
scan_evidence: scanEvidence,
|
|
822
|
-
database: DATABASE_CHOICES.includes(database) ? database : 'none',
|
|
823
|
-
package_manager: PACKAGE_MANAGER_CHOICES.includes(detectPackageManager(appRoot)) ? detectPackageManager(appRoot) : 'auto-detect',
|
|
824
|
-
uses_docker: infrastructure.includes('docker'),
|
|
825
|
-
cloud,
|
|
826
|
-
infrastructure: uniq(infrastructure),
|
|
827
|
-
ci_cd: ciCd,
|
|
828
|
-
test_framework: TEST_FRAMEWORK_CHOICES.includes(testFramework) ? testFramework : 'auto-detect',
|
|
829
|
-
max_file_lines: '300',
|
|
830
|
-
code_style: 'project-standard',
|
|
831
|
-
architecture_notes: '<!-- Add notes -->',
|
|
832
|
-
current_phase: 'Initial setup',
|
|
833
|
-
next_phase: '-',
|
|
834
|
-
known_issues: '<!-- None yet -->'
|
|
835
|
-
};
|
|
836
|
-
}
|
|
837
|
-
|
|
838
|
-
function resolveAgentTable(data) {
|
|
839
|
-
const rows = [];
|
|
840
|
-
const frameworks = (data.frameworks || []).map(f => f.toLowerCase());
|
|
841
|
-
const lang = (data.primary_language || "").toLowerCase();
|
|
842
|
-
|
|
843
|
-
// Frontend
|
|
844
|
-
const frontendFws = frameworks.filter(f => FRONTEND_FRAMEWORKS.includes(f));
|
|
845
|
-
if (frontendFws.length > 0) {
|
|
846
|
-
let feSkills = [];
|
|
847
|
-
frontendFws.forEach(fw => {
|
|
848
|
-
feSkills.push(...(FRAMEWORK_SKILLS[fw] || []));
|
|
849
|
-
});
|
|
850
|
-
rows.push(["Frontend UI", "frontend-specialist", Array.from(new Set(feSkills)).slice(0, 3).join(", ")]);
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
// Backend
|
|
854
|
-
let beSkills = [...(LANG_SKILLS[lang] || ["api-design"])];
|
|
855
|
-
const backendFws = frameworks.filter(f => BACKEND_FRAMEWORKS.includes(f));
|
|
856
|
-
backendFws.forEach(fw => {
|
|
857
|
-
beSkills.push(...(FRAMEWORK_SKILLS[fw] || []));
|
|
858
|
-
});
|
|
859
|
-
rows.push(["Backend API", "backend-specialist", Array.from(new Set(beSkills)).slice(0, 3).join(", ")]);
|
|
860
|
-
|
|
861
|
-
// Mobile/Desktop
|
|
862
|
-
const mobileFws = frameworks.filter(f => MOBILE_DESKTOP_FRAMEWORKS.includes(f));
|
|
863
|
-
if (mobileFws.length > 0) {
|
|
864
|
-
let mobileSkills = [];
|
|
865
|
-
mobileFws.forEach(fw => {
|
|
866
|
-
mobileSkills.push(...(FRAMEWORK_SKILLS[fw] || []));
|
|
867
|
-
});
|
|
868
|
-
rows.push(["Mobile/Desktop", "mobile-developer", Array.from(new Set(mobileSkills)).slice(0, 3).join(", ")]);
|
|
869
|
-
}
|
|
870
|
-
|
|
871
|
-
// DevOps
|
|
872
|
-
if (data.uses_docker || data.cloud !== "none") {
|
|
873
|
-
rows.push(["DevOps/Infra", "devops-engineer", "docker-patterns, deployment-procedures"]);
|
|
874
|
-
}
|
|
875
|
-
|
|
876
|
-
rows.push(["Debug", "debugger", "systematic-debugging"]);
|
|
877
|
-
rows.push(["Security", "security-auditor", "vulnerability-scanner"]);
|
|
878
|
-
rows.push(["Planning", "project-planner", "brainstorming, plan-writing"]);
|
|
879
|
-
|
|
880
82
|
return rows;
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
83
|
+
|
|
84
|
+
function formatScanPreviewLines(scanned) {
|
|
85
|
+
return [
|
|
86
|
+
['App root', scanned.app_root || '.'],
|
|
87
|
+
['Project type', scanned.project_type || 'unknown'],
|
|
88
|
+
['Primary language', scanned.primary_language || 'unknown'],
|
|
89
|
+
['Frameworks', (scanned.frameworks || []).join(', ') || 'no framework'],
|
|
90
|
+
['Database', scanned.database || 'none'],
|
|
91
|
+
['Package manager', scanned.package_manager || 'auto-detect'],
|
|
92
|
+
['Test framework', scanned.test_framework || 'auto-detect'],
|
|
93
|
+
['Cloud', scanned.cloud || 'none'],
|
|
94
|
+
['Infrastructure', (scanned.infrastructure || []).join(', ') || 'none'],
|
|
95
|
+
['CI/CD', scanned.ci_cd || 'none']
|
|
96
|
+
];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function askQuestions(questions) {
|
|
100
|
+
return prompts(questions, {
|
|
101
|
+
onCancel: () => {
|
|
102
|
+
console.log(pc.yellow('\nCancelled.'));
|
|
103
|
+
process.exit(130);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function fileExists(...parts) {
|
|
109
|
+
return fs.existsSync(path.join(targetDir, ...parts));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function fileExistsAt(baseDir, ...parts) {
|
|
113
|
+
return fs.existsSync(path.join(baseDir, ...parts));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function readJsonIfExists(...parts) {
|
|
117
|
+
const fullPath = path.join(targetDir, ...parts);
|
|
118
|
+
if (!fs.existsSync(fullPath)) return null;
|
|
119
|
+
try {
|
|
120
|
+
return JSON.parse(fs.readFileSync(fullPath, 'utf8'));
|
|
121
|
+
} catch {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function readJsonIfExistsAt(baseDir, ...parts) {
|
|
127
|
+
const fullPath = path.join(baseDir, ...parts);
|
|
128
|
+
if (!fs.existsSync(fullPath)) return null;
|
|
129
|
+
try {
|
|
130
|
+
return JSON.parse(fs.readFileSync(fullPath, 'utf8'));
|
|
131
|
+
} catch {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function readTextIfExists(...parts) {
|
|
137
|
+
const fullPath = path.join(targetDir, ...parts);
|
|
138
|
+
if (!fs.existsSync(fullPath)) return '';
|
|
139
|
+
try {
|
|
140
|
+
return fs.readFileSync(fullPath, 'utf8');
|
|
141
|
+
} catch {
|
|
142
|
+
return '';
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function readTextIfExistsAt(baseDir, ...parts) {
|
|
147
|
+
const fullPath = path.join(baseDir, ...parts);
|
|
148
|
+
if (!fs.existsSync(fullPath)) return '';
|
|
149
|
+
try {
|
|
150
|
+
return fs.readFileSync(fullPath, 'utf8');
|
|
151
|
+
} catch {
|
|
152
|
+
return '';
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function detectPackageManager(baseDir = targetDir) {
|
|
157
|
+
if (fileExistsAt(baseDir, 'pnpm-lock.yaml')) return 'pnpm';
|
|
158
|
+
if (fileExistsAt(baseDir, 'yarn.lock')) return 'yarn';
|
|
159
|
+
if (fileExistsAt(baseDir, 'bun.lockb') || fileExistsAt(baseDir, 'bun.lock')) return 'bun';
|
|
160
|
+
if (fileExistsAt(baseDir, 'package-lock.json')) return 'npm';
|
|
161
|
+
if (fileExistsAt(baseDir, 'uv.lock')) return 'uv';
|
|
162
|
+
if (fileExistsAt(baseDir, 'poetry.lock')) return 'pip';
|
|
163
|
+
if (fileExistsAt(baseDir, 'requirements.txt') || fileExistsAt(baseDir, 'pyproject.toml')) return 'pip';
|
|
164
|
+
if (fileExistsAt(baseDir, 'go.mod')) return 'go mod';
|
|
165
|
+
if (fileExistsAt(baseDir, 'Cargo.toml')) return 'cargo';
|
|
166
|
+
if (fileExistsAt(baseDir, 'composer.json')) return 'composer';
|
|
167
|
+
return 'auto-detect';
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function uniq(values) {
|
|
171
|
+
return Array.from(new Set(values.filter(Boolean)));
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function detectAppRoot(baseDir) {
|
|
175
|
+
const candidates = [baseDir];
|
|
176
|
+
const queue = [{ dir: baseDir, depth: 0 }];
|
|
177
|
+
|
|
178
|
+
while (queue.length > 0) {
|
|
179
|
+
const { dir, depth } = queue.shift();
|
|
180
|
+
if (depth >= 2) continue;
|
|
181
|
+
let entries = [];
|
|
182
|
+
try {
|
|
183
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
184
|
+
} catch {
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
for (const entry of entries) {
|
|
188
|
+
if (!entry.isDirectory()) continue;
|
|
189
|
+
if (IGNORED_SCAN_DIRS.has(entry.name)) continue;
|
|
190
|
+
const nextDir = path.join(dir, entry.name);
|
|
191
|
+
candidates.push(nextDir);
|
|
192
|
+
queue.push({ dir: nextDir, depth: depth + 1 });
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const scoreCandidate = dir => {
|
|
197
|
+
let score = 0;
|
|
198
|
+
if (fileExistsAt(dir, 'composer.json')) score += 5;
|
|
199
|
+
if (fileExistsAt(dir, 'package.json')) score += 4;
|
|
200
|
+
if (fileExistsAt(dir, 'Cargo.toml')) score += 4;
|
|
201
|
+
if (fileExistsAt(dir, 'go.mod')) score += 4;
|
|
202
|
+
if (fileExistsAt(dir, 'pyproject.toml') || fileExistsAt(dir, 'requirements.txt')) score += 4;
|
|
203
|
+
if (fileExistsAt(dir, 'artisan')) score += 5;
|
|
204
|
+
if (fileExistsAt(dir, 'bin', 'cake') || fileExistsAt(dir, 'app', 'Config', 'core.php')) score += 5;
|
|
205
|
+
if (fileExistsAt(dir, 'src-tauri', 'tauri.conf.json') || fileExistsAt(dir, 'src-tauri', 'tauri.conf.json5')) score += 5;
|
|
206
|
+
if (fileExistsAt(dir, 'manage.py')) score += 4;
|
|
207
|
+
if (fileExistsAt(dir, 'Gemfile')) score += 4;
|
|
208
|
+
if (fileExistsAt(dir, 'pom.xml') || fileExistsAt(dir, 'build.gradle') || fileExistsAt(dir, 'build.gradle.kts')) score += 4;
|
|
209
|
+
if (fileExistsAt(dir, 'app', 'View') || fileExistsAt(dir, 'resources', 'views') || fileExistsAt(dir, 'templates') || fileExistsAt(dir, 'app', 'views')) score += 2;
|
|
210
|
+
return score;
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const ranked = candidates
|
|
214
|
+
.map(dir => ({ dir, score: scoreCandidate(dir) }))
|
|
215
|
+
.filter(item => item.score > 0)
|
|
216
|
+
.sort((a, b) => (b.score - a.score) || (a.dir.length - b.dir.length));
|
|
217
|
+
|
|
218
|
+
return ranked[0]?.dir || baseDir;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function repoHasAny(baseDir, relativePaths, maxDepth = 3) {
|
|
222
|
+
const queue = [{ dir: baseDir, depth: 0 }];
|
|
223
|
+
while (queue.length > 0) {
|
|
224
|
+
const { dir, depth } = queue.shift();
|
|
225
|
+
for (const relativePath of relativePaths) {
|
|
226
|
+
if (fileExistsAt(dir, ...relativePath)) return true;
|
|
227
|
+
}
|
|
228
|
+
if (depth >= maxDepth) continue;
|
|
229
|
+
let entries = [];
|
|
230
|
+
try {
|
|
231
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
232
|
+
} catch {
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
for (const entry of entries) {
|
|
236
|
+
if (!entry.isDirectory()) continue;
|
|
237
|
+
if (IGNORED_SCAN_DIRS.has(entry.name)) continue;
|
|
238
|
+
queue.push({ dir: path.join(dir, entry.name), depth: depth + 1 });
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function evidenceIf(condition, label, weight = 1) {
|
|
245
|
+
return condition ? { label, weight } : null;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function buildDetection(id, evidence, meta = {}) {
|
|
249
|
+
const hits = evidence.filter(Boolean);
|
|
250
|
+
const score = hits.reduce((sum, hit) => sum + hit.weight, 0);
|
|
251
|
+
return {
|
|
252
|
+
id,
|
|
253
|
+
score,
|
|
254
|
+
evidence: hits.map(hit => hit.label),
|
|
255
|
+
...meta
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function confidenceLabel(score) {
|
|
260
|
+
if (score >= 6) return 'high';
|
|
261
|
+
if (score >= 3) return 'medium';
|
|
262
|
+
return 'low';
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function hasDependency(depNames, ...names) {
|
|
266
|
+
return names.some(name => depNames.has(name));
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function hasComposerDependency(composerDepNames, ...names) {
|
|
270
|
+
return names.some(name => composerDepNames.has(name));
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function hasRootFile(rootEntries, ...filenames) {
|
|
274
|
+
return filenames.some(name => rootEntries.has(name));
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function textMatches(text, pattern) {
|
|
278
|
+
return Boolean(text && pattern.test(text));
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const LANGUAGE_DETECTORS = [
|
|
282
|
+
{
|
|
283
|
+
id: 'typescript',
|
|
284
|
+
detect: ctx => buildDetection('typescript', [
|
|
285
|
+
evidenceIf(ctx.fileExists('tsconfig.json'), 'found tsconfig.json', 4),
|
|
286
|
+
evidenceIf(hasDependency(ctx.depNames, 'typescript', 'ts-node', 'tsx'), 'package.json -> typescript toolchain', 2)
|
|
287
|
+
])
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
id: 'javascript',
|
|
291
|
+
detect: ctx => buildDetection('javascript', [
|
|
292
|
+
evidenceIf(ctx.fileExists('package.json'), 'found package.json', 2),
|
|
293
|
+
evidenceIf(ctx.depNames.size > 0, 'package.json -> JavaScript dependency graph', 1)
|
|
294
|
+
])
|
|
295
|
+
},
|
|
296
|
+
{
|
|
297
|
+
id: 'python',
|
|
298
|
+
detect: ctx => buildDetection('python', [
|
|
299
|
+
evidenceIf(ctx.fileExists('requirements.txt'), 'found requirements.txt', 3),
|
|
300
|
+
evidenceIf(ctx.fileExists('pyproject.toml'), 'found pyproject.toml', 2),
|
|
301
|
+
evidenceIf(textMatches(ctx.pyprojectText, /\[tool\.(poetry|uv|hatch|pdm)\]/i), 'pyproject.toml -> Python tool section', 2)
|
|
302
|
+
])
|
|
303
|
+
},
|
|
304
|
+
{
|
|
305
|
+
id: 'go',
|
|
306
|
+
detect: ctx => buildDetection('go', [
|
|
307
|
+
evidenceIf(ctx.fileExists('go.mod'), 'found go.mod', 4)
|
|
308
|
+
])
|
|
309
|
+
},
|
|
310
|
+
{
|
|
311
|
+
id: 'php',
|
|
312
|
+
detect: ctx => buildDetection('php', [
|
|
313
|
+
evidenceIf(ctx.fileExists('composer.json'), 'found composer.json', 4),
|
|
314
|
+
evidenceIf(ctx.composerDepNames.size > 0, 'composer.json -> PHP dependency graph', 1)
|
|
315
|
+
])
|
|
316
|
+
},
|
|
317
|
+
{
|
|
318
|
+
id: 'rust',
|
|
319
|
+
detect: ctx => buildDetection('rust', [
|
|
320
|
+
evidenceIf(ctx.fileExists('Cargo.toml'), 'found Cargo.toml', 4),
|
|
321
|
+
evidenceIf(ctx.fileExists('src-tauri', 'Cargo.toml'), 'found src-tauri/Cargo.toml', 5)
|
|
322
|
+
])
|
|
323
|
+
},
|
|
324
|
+
{
|
|
325
|
+
id: 'java',
|
|
326
|
+
detect: ctx => buildDetection('java', [
|
|
327
|
+
evidenceIf(ctx.fileExists('pom.xml'), 'found pom.xml', 4),
|
|
328
|
+
evidenceIf(ctx.fileExists('build.gradle') || ctx.fileExists('build.gradle.kts'), 'found Gradle build file', 3)
|
|
329
|
+
])
|
|
330
|
+
},
|
|
331
|
+
{
|
|
332
|
+
id: 'ruby',
|
|
333
|
+
detect: ctx => buildDetection('ruby', [
|
|
334
|
+
evidenceIf(ctx.fileExists('Gemfile'), 'found Gemfile', 4),
|
|
335
|
+
evidenceIf(ctx.fileExists('config.ru'), 'found config.ru', 1)
|
|
336
|
+
])
|
|
337
|
+
},
|
|
338
|
+
{
|
|
339
|
+
id: 'csharp',
|
|
340
|
+
detect: ctx => buildDetection('csharp', [
|
|
341
|
+
evidenceIf(ctx.rootEntries.some(name => name.endsWith('.sln') || name.endsWith('.csproj')), 'found .sln/.csproj', 4),
|
|
342
|
+
evidenceIf(ctx.fileExists('Program.cs'), 'found Program.cs', 1)
|
|
343
|
+
])
|
|
344
|
+
}
|
|
345
|
+
];
|
|
346
|
+
|
|
347
|
+
const FRAMEWORK_DETECTORS = [
|
|
348
|
+
{
|
|
349
|
+
id: 'next',
|
|
350
|
+
kind: 'frontend',
|
|
351
|
+
projectType: 'web-app',
|
|
352
|
+
detect: ctx => buildDetection('next', [
|
|
353
|
+
evidenceIf(hasDependency(ctx.depNames, 'next'), 'package.json -> next', 4),
|
|
354
|
+
evidenceIf(ctx.fileExists('next.config.js') || ctx.fileExists('next.config.mjs') || ctx.fileExists('next.config.ts'), 'found next.config.*', 3),
|
|
355
|
+
evidenceIf(ctx.fileExists('app') || ctx.fileExists('pages'), 'found app/ or pages/', 1)
|
|
356
|
+
])
|
|
357
|
+
},
|
|
358
|
+
{
|
|
359
|
+
id: 'react',
|
|
360
|
+
kind: 'frontend',
|
|
361
|
+
projectType: 'web-app',
|
|
362
|
+
detect: ctx => buildDetection('react', [
|
|
363
|
+
evidenceIf(hasDependency(ctx.depNames, 'react'), 'package.json -> react', 4),
|
|
364
|
+
evidenceIf(hasDependency(ctx.depNames, 'react-dom'), 'package.json -> react-dom', 2)
|
|
365
|
+
])
|
|
366
|
+
},
|
|
367
|
+
{
|
|
368
|
+
id: 'vue',
|
|
369
|
+
kind: 'frontend',
|
|
370
|
+
projectType: 'web-app',
|
|
371
|
+
detect: ctx => buildDetection('vue', [
|
|
372
|
+
evidenceIf(hasDependency(ctx.depNames, 'vue'), 'package.json -> vue', 4)
|
|
373
|
+
])
|
|
374
|
+
},
|
|
375
|
+
{
|
|
376
|
+
id: 'nuxt',
|
|
377
|
+
kind: 'frontend',
|
|
378
|
+
projectType: 'web-app',
|
|
379
|
+
detect: ctx => buildDetection('nuxt', [
|
|
380
|
+
evidenceIf(hasDependency(ctx.depNames, 'nuxt'), 'package.json -> nuxt', 4),
|
|
381
|
+
evidenceIf(ctx.fileExists('nuxt.config.ts') || ctx.fileExists('nuxt.config.js'), 'found nuxt.config.*', 3)
|
|
382
|
+
])
|
|
383
|
+
},
|
|
384
|
+
{
|
|
385
|
+
id: 'svelte',
|
|
386
|
+
kind: 'frontend',
|
|
387
|
+
projectType: 'web-app',
|
|
388
|
+
detect: ctx => buildDetection('svelte', [
|
|
389
|
+
evidenceIf(hasDependency(ctx.depNames, 'svelte', '@sveltejs/kit'), 'package.json -> svelte', 4)
|
|
390
|
+
])
|
|
391
|
+
},
|
|
392
|
+
{
|
|
393
|
+
id: 'angular',
|
|
394
|
+
kind: 'frontend',
|
|
395
|
+
projectType: 'web-app',
|
|
396
|
+
detect: ctx => buildDetection('angular', [
|
|
397
|
+
evidenceIf(hasDependency(ctx.depNames, '@angular/core'), 'package.json -> @angular/core', 4),
|
|
398
|
+
evidenceIf(ctx.fileExists('angular.json'), 'found angular.json', 3)
|
|
399
|
+
])
|
|
400
|
+
},
|
|
401
|
+
{
|
|
402
|
+
id: 'express',
|
|
403
|
+
kind: 'backend',
|
|
404
|
+
language: 'javascript',
|
|
405
|
+
projectType: 'api',
|
|
406
|
+
detect: ctx => buildDetection('express', [
|
|
407
|
+
evidenceIf(hasDependency(ctx.depNames, 'express'), 'package.json -> express', 4)
|
|
408
|
+
])
|
|
409
|
+
},
|
|
410
|
+
{
|
|
411
|
+
id: 'nestjs',
|
|
412
|
+
kind: 'backend',
|
|
413
|
+
language: 'javascript',
|
|
414
|
+
projectType: 'api',
|
|
415
|
+
detect: ctx => buildDetection('nestjs', [
|
|
416
|
+
evidenceIf(hasDependency(ctx.depNames, '@nestjs/core'), 'package.json -> @nestjs/core', 4)
|
|
417
|
+
])
|
|
418
|
+
},
|
|
419
|
+
{
|
|
420
|
+
id: 'fastapi',
|
|
421
|
+
kind: 'backend',
|
|
422
|
+
language: 'python',
|
|
423
|
+
projectType: 'api',
|
|
424
|
+
detect: ctx => buildDetection('fastapi', [
|
|
425
|
+
evidenceIf(textMatches(ctx.requirementsText, /\bfastapi\b/i) || textMatches(ctx.pyprojectText, /\bfastapi\b/i), 'Python deps -> fastapi', 4)
|
|
426
|
+
])
|
|
427
|
+
},
|
|
428
|
+
{
|
|
429
|
+
id: 'django',
|
|
430
|
+
kind: 'backend',
|
|
431
|
+
language: 'python',
|
|
432
|
+
projectType: 'web-app',
|
|
433
|
+
detect: ctx => buildDetection('django', [
|
|
434
|
+
evidenceIf(textMatches(ctx.requirementsText, /\bdjango\b/i) || textMatches(ctx.pyprojectText, /\bdjango\b/i), 'Python deps -> django', 4),
|
|
435
|
+
evidenceIf(ctx.fileExists('manage.py'), 'found manage.py', 2),
|
|
436
|
+
evidenceIf(ctx.fileExists('templates'), 'found templates/', 1)
|
|
437
|
+
])
|
|
438
|
+
},
|
|
439
|
+
{
|
|
440
|
+
id: 'flask',
|
|
441
|
+
kind: 'backend',
|
|
442
|
+
language: 'python',
|
|
443
|
+
projectType: 'api',
|
|
444
|
+
detect: ctx => buildDetection('flask', [
|
|
445
|
+
evidenceIf(textMatches(ctx.requirementsText, /\bflask\b/i) || textMatches(ctx.pyprojectText, /\bflask\b/i), 'Python deps -> flask', 4)
|
|
446
|
+
])
|
|
447
|
+
},
|
|
448
|
+
{
|
|
449
|
+
id: 'laravel',
|
|
450
|
+
kind: 'backend',
|
|
451
|
+
language: 'php',
|
|
452
|
+
projectType: 'web-app',
|
|
453
|
+
detect: ctx => buildDetection('laravel', [
|
|
454
|
+
evidenceIf(hasComposerDependency(ctx.composerDepNames, 'laravel/framework'), 'composer.json -> laravel/framework', 4),
|
|
455
|
+
evidenceIf(ctx.fileExists('artisan'), 'found artisan', 3),
|
|
456
|
+
evidenceIf(ctx.fileExists('resources', 'views'), 'found resources/views', 1)
|
|
457
|
+
])
|
|
458
|
+
},
|
|
459
|
+
{
|
|
460
|
+
id: 'cakephp',
|
|
461
|
+
kind: 'backend',
|
|
462
|
+
language: 'php',
|
|
463
|
+
projectType: 'web-app',
|
|
464
|
+
detect: ctx => buildDetection('cakephp', [
|
|
465
|
+
evidenceIf(hasComposerDependency(ctx.composerDepNames, 'cakephp/cakephp'), 'composer.json -> cakephp/cakephp', 4),
|
|
466
|
+
evidenceIf(ctx.fileExists('bin', 'cake'), 'found bin/cake', 3),
|
|
467
|
+
evidenceIf(ctx.fileExists('config', 'app.php'), 'found config/app.php', 2),
|
|
468
|
+
evidenceIf(ctx.fileExists('app', 'Config', 'core.php'), 'found app/Config/core.php (CakePHP legacy)', 3),
|
|
469
|
+
evidenceIf(ctx.fileExists('app', 'Controller', 'AppController.php'), 'found app/Controller/AppController.php', 2),
|
|
470
|
+
evidenceIf(ctx.fileExists('app', 'Model', 'AppModel.php'), 'found app/Model/AppModel.php', 2),
|
|
471
|
+
evidenceIf(ctx.fileExists('lib', 'Cake'), 'found lib/Cake', 3),
|
|
472
|
+
evidenceIf(ctx.fileExists('cake', 'console', 'cake'), 'found cake/console/cake', 3),
|
|
473
|
+
evidenceIf(ctx.fileExists('app', 'View'), 'found app/View', 2),
|
|
474
|
+
evidenceIf(textMatches(ctx.viewFilesText, /\.ctp$/im), 'found .ctp view templates', 2)
|
|
475
|
+
])
|
|
476
|
+
},
|
|
477
|
+
{
|
|
478
|
+
id: 'symfony',
|
|
479
|
+
kind: 'backend',
|
|
480
|
+
language: 'php',
|
|
481
|
+
projectType: 'web-app',
|
|
482
|
+
detect: ctx => buildDetection('symfony', [
|
|
483
|
+
evidenceIf(hasComposerDependency(ctx.composerDepNames, 'symfony/framework-bundle'), 'composer.json -> symfony/framework-bundle', 4),
|
|
484
|
+
evidenceIf(ctx.fileExists('bin', 'console'), 'found bin/console', 2),
|
|
485
|
+
evidenceIf(ctx.fileExists('config', 'bundles.php'), 'found config/bundles.php', 1),
|
|
486
|
+
evidenceIf(ctx.fileExists('templates'), 'found templates/', 1)
|
|
487
|
+
])
|
|
488
|
+
},
|
|
489
|
+
{
|
|
490
|
+
id: 'codeigniter',
|
|
491
|
+
kind: 'backend',
|
|
492
|
+
language: 'php',
|
|
493
|
+
projectType: 'api',
|
|
494
|
+
detect: ctx => buildDetection('codeigniter', [
|
|
495
|
+
evidenceIf(hasComposerDependency(ctx.composerDepNames, 'codeigniter4/framework'), 'composer.json -> codeigniter4/framework', 4),
|
|
496
|
+
evidenceIf(ctx.fileExists('spark'), 'found spark', 2),
|
|
497
|
+
evidenceIf(ctx.fileExists('app', 'Config', 'App.php'), 'found app/Config/App.php', 1)
|
|
498
|
+
])
|
|
499
|
+
},
|
|
500
|
+
{
|
|
501
|
+
id: 'gin',
|
|
502
|
+
kind: 'backend',
|
|
503
|
+
language: 'go',
|
|
504
|
+
projectType: 'api',
|
|
505
|
+
detect: ctx => buildDetection('gin', [
|
|
506
|
+
evidenceIf(textMatches(ctx.goModText, /\bgithub\.com\/gin-gonic\/gin\b/i), 'go.mod -> github.com/gin-gonic/gin', 4)
|
|
507
|
+
])
|
|
508
|
+
},
|
|
509
|
+
{
|
|
510
|
+
id: 'fiber',
|
|
511
|
+
kind: 'backend',
|
|
512
|
+
language: 'go',
|
|
513
|
+
projectType: 'api',
|
|
514
|
+
detect: ctx => buildDetection('fiber', [
|
|
515
|
+
evidenceIf(textMatches(ctx.goModText, /\bgithub\.com\/gofiber\/fiber\b/i), 'go.mod -> github.com/gofiber/fiber', 4)
|
|
516
|
+
])
|
|
517
|
+
},
|
|
518
|
+
{
|
|
519
|
+
id: 'echo',
|
|
520
|
+
kind: 'backend',
|
|
521
|
+
language: 'go',
|
|
522
|
+
projectType: 'api',
|
|
523
|
+
detect: ctx => buildDetection('echo', [
|
|
524
|
+
evidenceIf(textMatches(ctx.goModText, /\bgithub\.com\/labstack\/echo\b/i), 'go.mod -> github.com/labstack/echo', 4)
|
|
525
|
+
])
|
|
526
|
+
},
|
|
527
|
+
{
|
|
528
|
+
id: 'rails',
|
|
529
|
+
kind: 'backend',
|
|
530
|
+
language: 'ruby',
|
|
531
|
+
projectType: 'web-app',
|
|
532
|
+
detect: ctx => buildDetection('rails', [
|
|
533
|
+
evidenceIf(textMatches(ctx.gemfileText, /^\s*gem\s+["']rails["']/im), 'Gemfile -> rails', 4),
|
|
534
|
+
evidenceIf(ctx.fileExists('bin', 'rails'), 'found bin/rails', 2),
|
|
535
|
+
evidenceIf(ctx.fileExists('app', 'views'), 'found app/views', 1)
|
|
536
|
+
])
|
|
537
|
+
},
|
|
538
|
+
{
|
|
539
|
+
id: 'spring',
|
|
540
|
+
kind: 'backend',
|
|
541
|
+
language: 'java',
|
|
542
|
+
projectType: 'api',
|
|
543
|
+
detect: ctx => buildDetection('spring', [
|
|
544
|
+
evidenceIf(textMatches(ctx.pomText, /spring-boot/i) || textMatches(ctx.gradleText, /spring-boot/i), 'Java build -> spring-boot', 4)
|
|
545
|
+
])
|
|
546
|
+
},
|
|
547
|
+
{
|
|
548
|
+
id: 'aspnet',
|
|
549
|
+
kind: 'backend',
|
|
550
|
+
language: 'csharp',
|
|
551
|
+
projectType: 'api',
|
|
552
|
+
detect: ctx => buildDetection('aspnet', [
|
|
553
|
+
evidenceIf(textMatches(ctx.csprojText, /Microsoft\.NET\.Sdk\.Web/i), '.csproj -> Microsoft.NET.Sdk.Web', 4),
|
|
554
|
+
evidenceIf(textMatches(ctx.programCsText, /WebApplication\.CreateBuilder/i), 'Program.cs -> WebApplication.CreateBuilder', 2)
|
|
555
|
+
])
|
|
556
|
+
},
|
|
557
|
+
{
|
|
558
|
+
id: 'tauri',
|
|
559
|
+
kind: 'desktop',
|
|
560
|
+
language: 'rust',
|
|
561
|
+
projectType: 'desktop',
|
|
562
|
+
detect: ctx => buildDetection('tauri', [
|
|
563
|
+
evidenceIf(hasDependency(ctx.depNames, '@tauri-apps/api'), 'package.json -> @tauri-apps/api', 4),
|
|
564
|
+
evidenceIf(ctx.fileExists('src-tauri', 'tauri.conf.json') || ctx.fileExists('src-tauri', 'tauri.conf.json5'), 'found src-tauri/tauri.conf.*', 4),
|
|
565
|
+
evidenceIf(ctx.fileExists('src-tauri', 'Cargo.toml'), 'found src-tauri/Cargo.toml', 2)
|
|
566
|
+
])
|
|
567
|
+
},
|
|
568
|
+
{
|
|
569
|
+
id: 'axum',
|
|
570
|
+
kind: 'backend',
|
|
571
|
+
language: 'rust',
|
|
572
|
+
projectType: 'api',
|
|
573
|
+
detect: ctx => buildDetection('axum', [
|
|
574
|
+
evidenceIf(textMatches(ctx.cargoText, /\baxum\b/i), 'Cargo.toml -> axum', 4)
|
|
575
|
+
])
|
|
576
|
+
},
|
|
577
|
+
{
|
|
578
|
+
id: 'actix',
|
|
579
|
+
kind: 'backend',
|
|
580
|
+
language: 'rust',
|
|
581
|
+
projectType: 'api',
|
|
582
|
+
detect: ctx => buildDetection('actix', [
|
|
583
|
+
evidenceIf(textMatches(ctx.cargoText, /\bactix-web\b/i), 'Cargo.toml -> actix-web', 4)
|
|
584
|
+
])
|
|
585
|
+
},
|
|
586
|
+
{
|
|
587
|
+
id: 'flutter',
|
|
588
|
+
kind: 'mobile',
|
|
589
|
+
projectType: 'mobile',
|
|
590
|
+
detect: ctx => buildDetection('flutter', [
|
|
591
|
+
evidenceIf(textMatches(ctx.pubspecText, /^\s*flutter\s*:/im), 'pubspec.yaml -> flutter', 4)
|
|
592
|
+
])
|
|
593
|
+
},
|
|
594
|
+
{
|
|
595
|
+
id: 'react-native',
|
|
596
|
+
kind: 'mobile',
|
|
597
|
+
projectType: 'mobile',
|
|
598
|
+
detect: ctx => buildDetection('react-native', [
|
|
599
|
+
evidenceIf(hasDependency(ctx.depNames, 'react-native', 'expo'), 'package.json -> react-native/expo', 4)
|
|
600
|
+
])
|
|
601
|
+
}
|
|
602
|
+
];
|
|
603
|
+
|
|
604
|
+
function scanProject() {
|
|
605
|
+
const appRoot = detectAppRoot(targetDir);
|
|
606
|
+
const pkg = readJsonIfExistsAt(appRoot, 'package.json') || {};
|
|
607
|
+
const composer = readJsonIfExistsAt(appRoot, 'composer.json') || {};
|
|
608
|
+
const rootEntries = fs.existsSync(appRoot) ? fs.readdirSync(appRoot) : [];
|
|
609
|
+
const rootCsproj = rootEntries.find(name => name.endsWith('.csproj')) || '';
|
|
610
|
+
const deps = {
|
|
611
|
+
...(pkg.dependencies || {}),
|
|
612
|
+
...(pkg.devDependencies || {}),
|
|
613
|
+
...(pkg.peerDependencies || {})
|
|
614
|
+
};
|
|
615
|
+
const depNames = new Set(Object.keys(deps));
|
|
616
|
+
const composerDeps = {
|
|
617
|
+
...(composer.require || {}),
|
|
618
|
+
...(composer['require-dev'] || {})
|
|
619
|
+
};
|
|
620
|
+
const composerDepNames = new Set(Object.keys(composerDeps));
|
|
621
|
+
const ctx = {
|
|
622
|
+
pkg,
|
|
623
|
+
composer,
|
|
624
|
+
depNames,
|
|
625
|
+
composerDepNames,
|
|
626
|
+
rootEntries,
|
|
627
|
+
fileExists: (...parts) => fileExistsAt(appRoot, ...parts),
|
|
628
|
+
requirementsText: readTextIfExistsAt(appRoot, 'requirements.txt'),
|
|
629
|
+
pyprojectText: readTextIfExistsAt(appRoot, 'pyproject.toml'),
|
|
630
|
+
goModText: readTextIfExistsAt(appRoot, 'go.mod'),
|
|
631
|
+
pubspecText: readTextIfExistsAt(appRoot, 'pubspec.yaml'),
|
|
632
|
+
gemfileText: readTextIfExistsAt(appRoot, 'Gemfile'),
|
|
633
|
+
pomText: readTextIfExistsAt(appRoot, 'pom.xml'),
|
|
634
|
+
gradleText: `${readTextIfExistsAt(appRoot, 'build.gradle')}\n${readTextIfExistsAt(appRoot, 'build.gradle.kts')}`,
|
|
635
|
+
cargoText: `${readTextIfExistsAt(appRoot, 'Cargo.toml')}\n${readTextIfExistsAt(appRoot, 'src-tauri', 'Cargo.toml')}`,
|
|
636
|
+
csprojText: rootCsproj ? readTextIfExistsAt(appRoot, rootCsproj) : '',
|
|
637
|
+
programCsText: readTextIfExistsAt(appRoot, 'Program.cs'),
|
|
638
|
+
viewFilesText: fs.existsSync(path.join(appRoot, 'app', 'View'))
|
|
639
|
+
? fs.readdirSync(path.join(appRoot, 'app', 'View'), { recursive: true }).join('\n')
|
|
640
|
+
: ''
|
|
641
|
+
};
|
|
642
|
+
|
|
643
|
+
const languageDetections = LANGUAGE_DETECTORS
|
|
644
|
+
.map(detector => detector.detect(ctx))
|
|
645
|
+
.filter(result => result.score >= 2)
|
|
646
|
+
.sort((a, b) => b.score - a.score);
|
|
647
|
+
|
|
648
|
+
const frameworkDetails = FRAMEWORK_DETECTORS
|
|
649
|
+
.map(detector => {
|
|
650
|
+
const result = detector.detect(ctx);
|
|
651
|
+
if (result.score < 3) return null;
|
|
652
|
+
return {
|
|
653
|
+
...result,
|
|
654
|
+
kind: detector.kind,
|
|
655
|
+
language: detector.language,
|
|
656
|
+
projectType: detector.projectType,
|
|
657
|
+
confidence: confidenceLabel(result.score)
|
|
658
|
+
};
|
|
659
|
+
})
|
|
660
|
+
.filter(Boolean)
|
|
661
|
+
.sort((a, b) => b.score - a.score);
|
|
662
|
+
|
|
663
|
+
const frameworkLanguageScores = frameworkDetails.reduce((acc, item) => {
|
|
664
|
+
if (!item.language) return acc;
|
|
665
|
+
acc[item.language] = (acc[item.language] || 0) + item.score;
|
|
666
|
+
return acc;
|
|
667
|
+
}, {});
|
|
668
|
+
const primaryLanguage = Object.keys(frameworkLanguageScores).sort((a, b) => frameworkLanguageScores[b] - frameworkLanguageScores[a])[0]
|
|
669
|
+
|| languageDetections[0]?.id
|
|
670
|
+
|| 'typescript';
|
|
671
|
+
const frameworks = uniq(frameworkDetails.map(item => item.id)).filter(name => FRAMEWORK_CHOICES.includes(name));
|
|
672
|
+
const infrastructure = [];
|
|
673
|
+
let projectType = '';
|
|
674
|
+
let database = 'none';
|
|
675
|
+
let testFramework = 'auto-detect';
|
|
676
|
+
let cloud = 'none';
|
|
677
|
+
let ciCd = 'none';
|
|
678
|
+
|
|
679
|
+
if (frameworkDetails.some(item => item.projectType === 'desktop')) projectType = 'desktop';
|
|
680
|
+
else if (frameworkDetails.some(item => item.projectType === 'mobile')) projectType = 'mobile';
|
|
681
|
+
else if (frameworkDetails.some(item => item.kind === 'frontend') && frameworkDetails.some(item => item.kind === 'backend')) projectType = 'fullstack';
|
|
682
|
+
else if (frameworkDetails.some(item => item.projectType === 'web-app')) projectType = 'web-app';
|
|
683
|
+
else if (frameworkDetails.some(item => item.kind === 'backend')) projectType = 'api';
|
|
684
|
+
else if (frameworkDetails.some(item => item.kind === 'frontend')) projectType = 'web-app';
|
|
685
|
+
else if (fileExistsAt(appRoot, 'package.json') && pkg.bin) projectType = 'cli';
|
|
686
|
+
else if (['php', 'python', 'go', 'java', 'ruby', 'csharp', 'rust'].includes(primaryLanguage)) projectType = 'api';
|
|
687
|
+
|
|
688
|
+
if (hasDependency(depNames, 'pg', 'postgres', '@prisma/client') || /postgres/i.test(JSON.stringify(pkg))) database = 'postgres';
|
|
689
|
+
else if (hasDependency(depNames, 'mysql', 'mysql2')) database = 'mysql';
|
|
690
|
+
else if (hasDependency(depNames, 'sqlite3', 'better-sqlite3', 'rusqlite')) database = 'sqlite';
|
|
691
|
+
else if (hasDependency(depNames, 'mongodb', 'mongoose')) database = 'mongodb';
|
|
692
|
+
else if (hasDependency(depNames, 'redis', 'ioredis')) database = 'redis';
|
|
693
|
+
else if (hasDependency(depNames, '@aws-sdk/client-dynamodb')) database = 'dynamodb';
|
|
694
|
+
|
|
695
|
+
if (hasDependency(depNames, 'vitest')) testFramework = 'vitest';
|
|
696
|
+
else if (hasDependency(depNames, 'jest')) testFramework = 'jest';
|
|
697
|
+
else if (hasDependency(depNames, 'pytest') || textMatches(ctx.requirementsText, /\bpytest\b/i) || textMatches(ctx.pyprojectText, /\bpytest\b/i)) testFramework = 'pytest';
|
|
698
|
+
else if (hasComposerDependency(composerDepNames, 'phpunit/phpunit')) testFramework = 'phpunit';
|
|
699
|
+
else if (hasComposerDependency(composerDepNames, 'pestphp/pest')) testFramework = 'pest';
|
|
700
|
+
else if (fileExistsAt(appRoot, 'go.mod')) testFramework = 'go test';
|
|
701
|
+
else if (textMatches(ctx.gemfileText, /^\s*gem\s+["']rspec["']/im)) testFramework = 'rspec';
|
|
702
|
+
|
|
703
|
+
if (repoHasAny(targetDir, [['Dockerfile'], ['docker-compose.yml'], ['docker-compose.yaml']])) {
|
|
704
|
+
infrastructure.push('docker');
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
if (repoHasAny(targetDir, [['vercel.json']])) {
|
|
708
|
+
cloud = 'vercel';
|
|
709
|
+
infrastructure.push('vercel');
|
|
710
|
+
} else if (repoHasAny(targetDir, [['fly.toml']])) {
|
|
711
|
+
cloud = 'fly';
|
|
712
|
+
infrastructure.push('fly');
|
|
713
|
+
} else if (repoHasAny(targetDir, [['railway.json'], ['railway.toml']])) {
|
|
714
|
+
cloud = 'railway';
|
|
715
|
+
infrastructure.push('railway');
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
if (repoHasAny(targetDir, [['.github', 'workflows'], ['.gitlab-ci.yml'], ['.circleci', 'config.yml'], ['Jenkinsfile']], 2)) {
|
|
719
|
+
if (repoHasAny(targetDir, [['.github', 'workflows']], 2)) ciCd = 'github-actions';
|
|
720
|
+
else if (repoHasAny(targetDir, [['.gitlab-ci.yml']], 2)) ciCd = 'gitlab-ci';
|
|
721
|
+
else if (repoHasAny(targetDir, [['.circleci', 'config.yml']], 2)) ciCd = 'circleci';
|
|
722
|
+
else if (repoHasAny(targetDir, [['Jenkinsfile']], 2)) ciCd = 'jenkins';
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
const scanEvidence = [
|
|
726
|
+
...(languageDetections[0] ? [`language:${languageDetections[0].id} => ${languageDetections[0].evidence.join('; ')}`] : []),
|
|
727
|
+
...frameworkDetails.map(item => `${item.id} [${item.confidence}] => ${item.evidence.join('; ')}`)
|
|
728
|
+
];
|
|
729
|
+
|
|
730
|
+
return {
|
|
731
|
+
project_name: path.basename(targetDir),
|
|
732
|
+
app_root: path.relative(targetDir, appRoot) || '.',
|
|
733
|
+
description: pkg.description || composer.description || 'A software project',
|
|
734
|
+
project_type: PROJECT_TYPE_CHOICES.includes(projectType) ? projectType : 'web-app',
|
|
735
|
+
primary_language: LANGUAGE_CHOICES.includes(primaryLanguage) ? primaryLanguage : 'typescript',
|
|
736
|
+
frameworks,
|
|
737
|
+
framework_details: frameworkDetails,
|
|
738
|
+
scan_evidence: scanEvidence,
|
|
739
|
+
database: DATABASE_CHOICES.includes(database) ? database : 'none',
|
|
740
|
+
package_manager: PACKAGE_MANAGER_CHOICES.includes(detectPackageManager(appRoot)) ? detectPackageManager(appRoot) : 'auto-detect',
|
|
741
|
+
uses_docker: infrastructure.includes('docker'),
|
|
742
|
+
cloud,
|
|
743
|
+
infrastructure: uniq(infrastructure),
|
|
744
|
+
ci_cd: ciCd,
|
|
745
|
+
test_framework: TEST_FRAMEWORK_CHOICES.includes(testFramework) ? testFramework : 'auto-detect',
|
|
746
|
+
max_file_lines: '300',
|
|
747
|
+
code_style: 'project-standard',
|
|
748
|
+
architecture_notes: '<!-- Add notes -->',
|
|
749
|
+
current_phase: 'Initial setup',
|
|
750
|
+
next_phase: '-',
|
|
751
|
+
known_issues: '<!-- None yet -->'
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
// --- File Generators ---
|
|
755
|
+
|
|
884
756
|
function generateSharedAgentsMd(data) {
|
|
885
757
|
return `# AGENTS.md
|
|
886
758
|
|
|
887
759
|
Shared operating instructions for Codex, Antigravity, Claude Code, and Cursor.
|
|
888
|
-
|
|
889
|
-
## Startup
|
|
890
|
-
|
|
891
|
-
1. Read this file first.
|
|
892
|
-
2. Read \`.agent/docs/project.md\` for detected stack, commands, and routing.
|
|
893
|
-
3. Read \`.agent/docs/workflow.md\` for the task flow before editing.
|
|
894
|
-
4. Read \`.agent/docs/architecture.md\` and \`.agent/docs/conventions.md\` before planning code changes.
|
|
760
|
+
|
|
761
|
+
## Startup
|
|
762
|
+
|
|
763
|
+
1. Read this file first.
|
|
764
|
+
2. Read \`.agent/docs/project.md\` for detected stack, commands, and routing.
|
|
765
|
+
3. Read \`.agent/docs/workflow.md\` for the task flow before editing.
|
|
766
|
+
4. Read \`.agent/docs/architecture.md\` and \`.agent/docs/conventions.md\` before planning code changes.
|
|
895
767
|
5. Read \`.agent/docs/tooling.md\` when the task needs MCP, RTK, or browser verification.
|
|
896
768
|
|
|
897
769
|
If any \`.agent/docs/*.md\` file still contains \`TODO: refine\`, update the docs by scanning the project before making product code changes.
|
|
@@ -909,208 +781,230 @@ Superpowers is the primary workflow backend. Use its native skills when availabl
|
|
|
909
781
|
Questions and research do not edit product code. A simple fix may use a fast path only when it is one-file, unambiguous, and changes no behavior or contract. State the scope and verification before editing. If the scope grows, switch to the full Superpowers flow.
|
|
910
782
|
|
|
911
783
|
If Superpowers is unavailable, follow the plain-language flow in \`.agent/docs/workflow.md\`; do not expect this kit to provide a second copy of the workflow engine.
|
|
912
|
-
|
|
913
|
-
## Rules
|
|
914
|
-
|
|
915
|
-
- Respond in the user's language; keep code, identifiers, and code comments in English.
|
|
916
|
-
- State assumptions when the request is ambiguous.
|
|
917
|
-
- Prefer the existing project style over new abstractions.
|
|
918
|
-
- Keep changes surgical and remove only dead code introduced by your change.
|
|
784
|
+
|
|
785
|
+
## Rules
|
|
786
|
+
|
|
787
|
+
- Respond in the user's language; keep code, identifiers, and code comments in English.
|
|
788
|
+
- State assumptions when the request is ambiguous.
|
|
789
|
+
- Prefer the existing project style over new abstractions.
|
|
790
|
+
- Keep changes surgical and remove only dead code introduced by your change.
|
|
919
791
|
- **MCP First**: Prioritize \`codebase-memory-mcp\` for symbols, call paths, architecture, and impact; use \`context7\` for API/docs queries and \`playwright\` for browser/UI verification. Fall back to normal file search when an integration is unavailable.
|
|
920
|
-
-
|
|
921
|
-
-
|
|
922
|
-
|
|
792
|
+
- Prefer \`rtk\` by default for verbose shell commands such as \`rtk git status\`, \`rtk npm test\`, and \`rtk git log\`. Run the underlying command directly only when RTK is unavailable or incompatible.
|
|
793
|
+
- Tests or equivalent verification are mandatory before claiming done.
|
|
794
|
+
- Keep files under ${data.max_file_lines || '300'} lines unless the project already has a different standard in \`.agent/docs/conventions.md\`.
|
|
795
|
+
|
|
923
796
|
## Shared Knowledge
|
|
924
797
|
|
|
925
798
|
- Project context: \`.agent/docs/\`
|
|
926
799
|
- Tooling setup: \`.agent/docs/tooling.md\`
|
|
927
800
|
- Superpowers: installed through the AI client's official plugin surface
|
|
928
|
-
- RTK:
|
|
801
|
+
- RTK: recommended local CLI for compact command output
|
|
929
802
|
- Antigravity and Cursor entry rules: \`GEMINI.md\` and \`.cursorrules\`
|
|
930
803
|
`;
|
|
931
804
|
}
|
|
932
|
-
|
|
933
|
-
function
|
|
934
|
-
return
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
805
|
+
|
|
806
|
+
function generateIndexProjectPrompt() {
|
|
807
|
+
return `# Project Indexing Prompt
|
|
808
|
+
|
|
809
|
+
Read CLAUDE.md (if using Claude Code), AGENTS.md, and all files under .agent/docs/.
|
|
810
|
+
Use Superpowers for the workflow and use codebase-memory MCP for structural code discovery when available.
|
|
811
|
+
Ask me what this project does and any important conventions I want preserved.
|
|
812
|
+
Then scan this repository and refine the generated context docs with factual rules.
|
|
813
|
+
Do not implement product code during onboarding.
|
|
814
|
+
`;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
function printProjectPrompt() {
|
|
818
|
+
const promptPath = path.join(targetDir, '.agent', 'docs', 'index-project-prompt.md');
|
|
819
|
+
if (!fs.existsSync(promptPath)) {
|
|
820
|
+
console.error(pc.yellow('No saved project prompt found. Run thachvd-kit init first.'));
|
|
821
|
+
process.exitCode = 1;
|
|
822
|
+
return;
|
|
823
|
+
}
|
|
824
|
+
console.log(fs.readFileSync(promptPath, 'utf8'));
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
function generateSharedClaudeMd() {
|
|
828
|
+
return `@AGENTS.md
|
|
829
|
+
|
|
830
|
+
## Claude Code
|
|
831
|
+
|
|
832
|
+
This repository uses AGENTS.md as the shared cross-agent entry file. Follow the imported instructions and keep Claude-specific notes here only when they cannot apply to Codex or Antigravity.
|
|
833
|
+
`;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
function generateSharedGeminiMd() {
|
|
837
|
+
return `# GEMINI.md
|
|
838
|
+
|
|
839
|
+
Antigravity entry file for this project.
|
|
840
|
+
|
|
841
|
+
Read AGENTS.md first, then follow the shared docs under .agent/docs/.
|
|
842
|
+
`;
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
function generateSharedCursorrules(data) {
|
|
952
846
|
return `# Cursor Rules
|
|
953
|
-
|
|
954
|
-
This repository uses AGENTS.md as the shared cross-agent entry file. Follow the instructions in AGENTS.md and keep Cursor-specific notes here only when they cannot apply to Codex, Antigravity, or Claude Code.
|
|
955
|
-
|
|
956
|
-
Read AGENTS.md first, then follow the shared docs under .agent/docs/.
|
|
957
|
-
|
|
958
|
-
---
|
|
959
|
-
|
|
847
|
+
|
|
848
|
+
This repository uses AGENTS.md as the shared cross-agent entry file. Follow the instructions in AGENTS.md and keep Cursor-specific notes here only when they cannot apply to Codex, Antigravity, or Claude Code.
|
|
849
|
+
|
|
850
|
+
Read AGENTS.md first, then follow the shared docs under .agent/docs/.
|
|
851
|
+
|
|
852
|
+
---
|
|
853
|
+
|
|
960
854
|
${generateSharedAgentsMd(data).trimEnd()}
|
|
961
855
|
`;
|
|
962
|
-
}
|
|
963
|
-
|
|
964
|
-
function resolveCommands(data) {
|
|
965
|
-
const frameworks = data.frameworks || [];
|
|
966
|
-
const primaryLang = data.primary_language || 'javascript';
|
|
967
|
-
const pkgManager = data.package_manager || 'npm';
|
|
968
|
-
const testFramework = data.test_framework || 'auto-detect';
|
|
969
|
-
|
|
970
|
-
const isPython = primaryLang === 'python';
|
|
971
|
-
const isGo = primaryLang === 'go';
|
|
972
|
-
const isRust = primaryLang === 'rust';
|
|
973
|
-
const isPhp = primaryLang === 'php';
|
|
974
|
-
|
|
975
|
-
let install = 'npm install';
|
|
976
|
-
let dev = 'npm run dev';
|
|
977
|
-
let build = 'npm run build';
|
|
978
|
-
let test = 'npm test';
|
|
979
|
-
let lint = 'npm run lint';
|
|
980
|
-
|
|
981
|
-
if (pkgManager === 'pnpm') { install = 'pnpm install'; dev = 'pnpm dev'; build = 'pnpm build'; test = 'pnpm test'; lint = 'pnpm lint'; }
|
|
982
|
-
else if (pkgManager === 'yarn') { install = 'yarn'; dev = 'yarn dev'; build = 'yarn build'; test = 'yarn test'; lint = 'yarn lint'; }
|
|
983
|
-
else if (pkgManager === 'bun') { install = 'bun install'; dev = 'bun dev'; build = 'bun run build'; test = 'bun test'; lint = 'bun run lint'; }
|
|
984
|
-
else if (isPython) { install = 'pip install -r requirements.txt'; dev = 'python manage.py runserver'; build = '# no build step'; test = 'pytest'; lint = 'ruff check .'; }
|
|
985
|
-
else if (isGo) { install = 'go mod download'; dev = 'go run .'; build = 'go build ./...'; test = 'go test ./...'; lint = 'golangci-lint run'; }
|
|
986
|
-
else if (isRust) { install = 'cargo build'; dev = 'cargo run'; build = 'cargo build --release'; test = 'cargo test'; lint = 'cargo clippy'; }
|
|
987
|
-
else if (isPhp) {
|
|
988
|
-
install = 'composer install';
|
|
989
|
-
build = '# no build step';
|
|
990
|
-
test = testFramework === 'pest' ? './vendor/bin/pest' : './vendor/bin/phpunit';
|
|
991
|
-
const isCakePHP = frameworks.some(f => f.toLowerCase().includes('cakephp'));
|
|
992
|
-
const isSymfony = frameworks.some(f => f.toLowerCase().includes('symfony'));
|
|
993
|
-
const isCodeIgniter = frameworks.some(f => f.toLowerCase().includes('codeigniter'));
|
|
994
|
-
if (isCakePHP) {
|
|
995
|
-
dev = 'bin/cake server';
|
|
996
|
-
lint = './vendor/bin/phpcs';
|
|
997
|
-
} else if (isSymfony) {
|
|
998
|
-
dev = 'bin/console server:run';
|
|
999
|
-
lint = './vendor/bin/phpcs';
|
|
1000
|
-
} else if (isCodeIgniter) {
|
|
1001
|
-
dev = 'php spark serve';
|
|
1002
|
-
lint = './vendor/bin/phpcs';
|
|
1003
|
-
} else {
|
|
1004
|
-
dev = 'php artisan serve';
|
|
1005
|
-
lint = 'php artisan pint';
|
|
1006
|
-
}
|
|
1007
|
-
}
|
|
1008
|
-
|
|
1009
|
-
if (testFramework === 'vitest') test = `${pkgManager === 'pnpm' ? 'pnpm' : pkgManager === 'yarn' ? 'yarn' : pkgManager === 'bun' ? 'bun run' : 'npx'} vitest`;
|
|
1010
|
-
else if (testFramework === 'jest') test = `${pkgManager === 'pnpm' ? 'pnpm' : pkgManager === 'yarn' ? 'yarn' : 'npx'} jest`;
|
|
1011
|
-
|
|
1012
|
-
return { install, dev, build, test, lint };
|
|
1013
|
-
}
|
|
1014
|
-
|
|
1015
|
-
function generateProjectDoc(data) {
|
|
1016
|
-
const commands = resolveCommands(data);
|
|
1017
|
-
const evidence = (data.scan_evidence || []).length > 0
|
|
1018
|
-
? (data.scan_evidence || []).map(line => `- ${line}`).join('\n')
|
|
1019
|
-
: '- TODO: refine scan evidence by inspecting the project.';
|
|
1020
|
-
|
|
1021
|
-
return `# Project Rules
|
|
1022
|
-
|
|
1023
|
-
Generated by thachvd-kit on ${new Date().toISOString().split('T')[0]}.
|
|
1024
|
-
|
|
1025
|
-
## Summary
|
|
1026
|
-
|
|
1027
|
-
- Name: ${data.project_name}
|
|
1028
|
-
- Description: ${data.description}
|
|
1029
|
-
- Type: ${data.project_type}
|
|
1030
|
-
- App root: ${data.app_root || '.'}
|
|
1031
|
-
|
|
1032
|
-
## Stack
|
|
1033
|
-
|
|
1034
|
-
- Language: ${data.primary_language}
|
|
1035
|
-
- Frameworks: ${(data.frameworks || []).join(', ') || 'none'}
|
|
1036
|
-
- Database: ${data.database || 'none'}
|
|
1037
|
-
- Infrastructure: ${(data.infrastructure || []).join(', ') || 'none'}
|
|
1038
|
-
- Package manager: ${data.package_manager || 'auto-detect'}
|
|
1039
|
-
- Test framework: ${data.test_framework || 'auto-detect'}
|
|
1040
|
-
|
|
1041
|
-
## Commands
|
|
1042
|
-
|
|
1043
|
-
- Install: \`${commands.install}\`
|
|
1044
|
-
- Dev: \`${commands.dev}\`
|
|
1045
|
-
- Build: \`${commands.build}\`
|
|
1046
|
-
- Test: \`${commands.test}\`
|
|
1047
|
-
- Lint: \`${commands.lint}\`
|
|
1048
|
-
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
function resolveCommands(data) {
|
|
859
|
+
const frameworks = data.frameworks || [];
|
|
860
|
+
const primaryLang = data.primary_language || 'javascript';
|
|
861
|
+
const pkgManager = data.package_manager || 'npm';
|
|
862
|
+
const testFramework = data.test_framework || 'auto-detect';
|
|
863
|
+
|
|
864
|
+
const isPython = primaryLang === 'python';
|
|
865
|
+
const isGo = primaryLang === 'go';
|
|
866
|
+
const isRust = primaryLang === 'rust';
|
|
867
|
+
const isPhp = primaryLang === 'php';
|
|
868
|
+
|
|
869
|
+
let install = 'npm install';
|
|
870
|
+
let dev = 'npm run dev';
|
|
871
|
+
let build = 'npm run build';
|
|
872
|
+
let test = 'npm test';
|
|
873
|
+
let lint = 'npm run lint';
|
|
874
|
+
|
|
875
|
+
if (pkgManager === 'pnpm') { install = 'pnpm install'; dev = 'pnpm dev'; build = 'pnpm build'; test = 'pnpm test'; lint = 'pnpm lint'; }
|
|
876
|
+
else if (pkgManager === 'yarn') { install = 'yarn'; dev = 'yarn dev'; build = 'yarn build'; test = 'yarn test'; lint = 'yarn lint'; }
|
|
877
|
+
else if (pkgManager === 'bun') { install = 'bun install'; dev = 'bun dev'; build = 'bun run build'; test = 'bun test'; lint = 'bun run lint'; }
|
|
878
|
+
else if (isPython) { install = 'pip install -r requirements.txt'; dev = 'python manage.py runserver'; build = '# no build step'; test = 'pytest'; lint = 'ruff check .'; }
|
|
879
|
+
else if (isGo) { install = 'go mod download'; dev = 'go run .'; build = 'go build ./...'; test = 'go test ./...'; lint = 'golangci-lint run'; }
|
|
880
|
+
else if (isRust) { install = 'cargo build'; dev = 'cargo run'; build = 'cargo build --release'; test = 'cargo test'; lint = 'cargo clippy'; }
|
|
881
|
+
else if (isPhp) {
|
|
882
|
+
install = 'composer install';
|
|
883
|
+
build = '# no build step';
|
|
884
|
+
test = testFramework === 'pest' ? './vendor/bin/pest' : './vendor/bin/phpunit';
|
|
885
|
+
const isCakePHP = frameworks.some(f => f.toLowerCase().includes('cakephp'));
|
|
886
|
+
const isSymfony = frameworks.some(f => f.toLowerCase().includes('symfony'));
|
|
887
|
+
const isCodeIgniter = frameworks.some(f => f.toLowerCase().includes('codeigniter'));
|
|
888
|
+
if (isCakePHP) {
|
|
889
|
+
dev = 'bin/cake server';
|
|
890
|
+
lint = './vendor/bin/phpcs';
|
|
891
|
+
} else if (isSymfony) {
|
|
892
|
+
dev = 'bin/console server:run';
|
|
893
|
+
lint = './vendor/bin/phpcs';
|
|
894
|
+
} else if (isCodeIgniter) {
|
|
895
|
+
dev = 'php spark serve';
|
|
896
|
+
lint = './vendor/bin/phpcs';
|
|
897
|
+
} else {
|
|
898
|
+
dev = 'php artisan serve';
|
|
899
|
+
lint = 'php artisan pint';
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
if (testFramework === 'vitest') test = `${pkgManager === 'pnpm' ? 'pnpm' : pkgManager === 'yarn' ? 'yarn' : pkgManager === 'bun' ? 'bun run' : 'npx'} vitest`;
|
|
904
|
+
else if (testFramework === 'jest') test = `${pkgManager === 'pnpm' ? 'pnpm' : pkgManager === 'yarn' ? 'yarn' : 'npx'} jest`;
|
|
905
|
+
|
|
906
|
+
return { install, dev, build, test, lint };
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
function generateProjectDoc(data) {
|
|
910
|
+
const commands = resolveCommands(data);
|
|
911
|
+
const evidence = (data.scan_evidence || []).length > 0
|
|
912
|
+
? (data.scan_evidence || []).map(line => `- ${line}`).join('\n')
|
|
913
|
+
: '- TODO: refine scan evidence by inspecting the project.';
|
|
914
|
+
|
|
915
|
+
return `# Project Rules
|
|
916
|
+
|
|
917
|
+
Generated by thachvd-kit on ${new Date().toISOString().split('T')[0]}.
|
|
918
|
+
|
|
919
|
+
## Summary
|
|
920
|
+
|
|
921
|
+
- Name: ${data.project_name}
|
|
922
|
+
- Description: ${data.description}
|
|
923
|
+
- Type: ${data.project_type}
|
|
924
|
+
- App root: ${data.app_root || '.'}
|
|
925
|
+
|
|
926
|
+
## Stack
|
|
927
|
+
|
|
928
|
+
- Language: ${data.primary_language}
|
|
929
|
+
- Frameworks: ${(data.frameworks || []).join(', ') || 'none'}
|
|
930
|
+
- Database: ${data.database || 'none'}
|
|
931
|
+
- Infrastructure: ${(data.infrastructure || []).join(', ') || 'none'}
|
|
932
|
+
- Package manager: ${data.package_manager || 'auto-detect'}
|
|
933
|
+
- Test framework: ${data.test_framework || 'auto-detect'}
|
|
934
|
+
|
|
935
|
+
## Commands
|
|
936
|
+
|
|
937
|
+
- Install: \`${commands.install}\`
|
|
938
|
+
- Dev: \`${commands.dev}\`
|
|
939
|
+
- Build: \`${commands.build}\`
|
|
940
|
+
- Test: \`${commands.test}\`
|
|
941
|
+
- Lint: \`${commands.lint}\`
|
|
942
|
+
|
|
1049
943
|
## Available Integrations
|
|
1050
|
-
|
|
944
|
+
|
|
1051
945
|
Use these integrations when available. Run thachvd-kit doctor to check the current machine.
|
|
1052
|
-
|
|
1053
|
-
| MCP | When to Use |
|
|
1054
|
-
|-----|-------------|
|
|
946
|
+
|
|
947
|
+
| MCP | When to Use |
|
|
948
|
+
|-----|-------------|
|
|
1055
949
|
| \`codebase-memory-mcp\` | Explore symbols, trace call chains, architecture, and impact |
|
|
1056
|
-
| \`context7\` | Look up library/framework docs, API signatures, migration guides |
|
|
950
|
+
| \`context7\` | Look up library/framework docs, API signatures, migration guides |
|
|
1057
951
|
| \`playwright\` | Verify UI behavior, take screenshots, test forms and navigation |
|
|
1058
|
-
| \`rtk\` |
|
|
1059
|
-
|
|
1060
|
-
> Check \`.agent/docs/tooling.md\` for setup details and additional MCP servers.
|
|
1061
|
-
|
|
952
|
+
| \`rtk\` | Recommended proxy for noisy git, tests, builds, and package commands |
|
|
953
|
+
|
|
954
|
+
> Check \`.agent/docs/tooling.md\` for setup details and additional MCP servers.
|
|
955
|
+
|
|
1062
956
|
## Scan Evidence
|
|
1063
|
-
|
|
1064
|
-
${evidence}
|
|
1065
|
-
`;
|
|
1066
|
-
}
|
|
1067
|
-
|
|
1068
|
-
function generateArchitectureDoc(data) {
|
|
1069
|
-
const appRootLine = data.app_root && data.app_root !== '.'
|
|
1070
|
-
? `- App root is currently detected as \`${data.app_root}\`.`
|
|
1071
|
-
: '- App root is the repository root unless refined below.';
|
|
1072
|
-
const evidence = (data.scan_evidence || []).length > 0
|
|
1073
|
-
? (data.scan_evidence || []).map(line => `- ${line}`).join('\n')
|
|
1074
|
-
: '- TODO: refine architecture by scanning source directories.';
|
|
1075
|
-
|
|
1076
|
-
return `# Architecture Notes
|
|
1077
|
-
|
|
1078
|
-
## Current Map
|
|
1079
|
-
|
|
1080
|
-
${appRootLine}
|
|
1081
|
-
- TODO: refine major directories and responsibilities.
|
|
1082
|
-
- TODO: refine main entry points, routing, state boundaries, and integration boundaries.
|
|
1083
|
-
|
|
1084
|
-
## Detected Evidence
|
|
1085
|
-
|
|
1086
|
-
${evidence}
|
|
1087
|
-
|
|
1088
|
-
## AI Maintenance Rule
|
|
1089
|
-
|
|
1090
|
-
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.
|
|
1091
|
-
`;
|
|
1092
|
-
}
|
|
1093
|
-
|
|
1094
|
-
function generateConventionsDoc(data) {
|
|
1095
|
-
return `# Coding Conventions
|
|
1096
|
-
|
|
1097
|
-
## Current Standards
|
|
1098
|
-
|
|
1099
|
-
- Maximum file length: ${data.max_file_lines || '300'} lines unless the existing project standard is stricter.
|
|
1100
|
-
- Code comments and identifiers should be written in English.
|
|
1101
|
-
- Keep changes scoped to the user request.
|
|
1102
|
-
- Prefer existing local patterns over introducing new abstractions.
|
|
1103
|
-
- TODO: refine naming, formatting, folder, API, state, styling, and testing conventions from the real codebase.
|
|
1104
|
-
|
|
1105
|
-
## Verification
|
|
1106
|
-
|
|
1107
|
-
- Run the focused test or lint command that matches the touched area.
|
|
1108
|
-
- If no automated check exists, document the manual verification performed.
|
|
1109
|
-
- For UI/web changes, prioritize using Playwright MCP to automatically verify client-side behavior and capture screenshots.
|
|
1110
|
-
- Do not claim completion without verification evidence.
|
|
1111
|
-
`;
|
|
1112
|
-
}
|
|
1113
|
-
|
|
957
|
+
|
|
958
|
+
${evidence}
|
|
959
|
+
`;
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
function generateArchitectureDoc(data) {
|
|
963
|
+
const appRootLine = data.app_root && data.app_root !== '.'
|
|
964
|
+
? `- App root is currently detected as \`${data.app_root}\`.`
|
|
965
|
+
: '- App root is the repository root unless refined below.';
|
|
966
|
+
const evidence = (data.scan_evidence || []).length > 0
|
|
967
|
+
? (data.scan_evidence || []).map(line => `- ${line}`).join('\n')
|
|
968
|
+
: '- TODO: refine architecture by scanning source directories.';
|
|
969
|
+
|
|
970
|
+
return `# Architecture Notes
|
|
971
|
+
|
|
972
|
+
## Current Map
|
|
973
|
+
|
|
974
|
+
${appRootLine}
|
|
975
|
+
- TODO: refine major directories and responsibilities.
|
|
976
|
+
- TODO: refine main entry points, routing, state boundaries, and integration boundaries.
|
|
977
|
+
|
|
978
|
+
## Detected Evidence
|
|
979
|
+
|
|
980
|
+
${evidence}
|
|
981
|
+
|
|
982
|
+
## AI Maintenance Rule
|
|
983
|
+
|
|
984
|
+
Before non-trivial implementation work, inspect this file. If it is stale or still contains TODO items relevant to the task, update it from the code before editing product code.
|
|
985
|
+
`;
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
function generateConventionsDoc(data) {
|
|
989
|
+
return `# Coding Conventions
|
|
990
|
+
|
|
991
|
+
## Current Standards
|
|
992
|
+
|
|
993
|
+
- Maximum file length: ${data.max_file_lines || '300'} lines unless the existing project standard is stricter.
|
|
994
|
+
- Code comments and identifiers should be written in English.
|
|
995
|
+
- Keep changes scoped to the user request.
|
|
996
|
+
- Prefer existing local patterns over introducing new abstractions.
|
|
997
|
+
- TODO: refine naming, formatting, folder, API, state, styling, and testing conventions from the real codebase.
|
|
998
|
+
|
|
999
|
+
## Verification
|
|
1000
|
+
|
|
1001
|
+
- Run the focused test or lint command that matches the touched area.
|
|
1002
|
+
- If no automated check exists, document the manual verification performed.
|
|
1003
|
+
- For UI/web changes, prioritize using Playwright MCP to automatically verify client-side behavior and capture screenshots.
|
|
1004
|
+
- Do not claim completion without verification evidence.
|
|
1005
|
+
`;
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1114
1008
|
function generateWorkflowDoc() {
|
|
1115
1009
|
return `# Agent Workflow
|
|
1116
1010
|
|
|
@@ -1150,89 +1044,8 @@ Use it only for one-file, unambiguous changes with no behavior, API, schema, sec
|
|
|
1150
1044
|
- Update project.md when stack, scripts, or test tooling changes.
|
|
1151
1045
|
- Keep AGENTS.md concise and put project-specific detail under .agent/docs/.
|
|
1152
1046
|
`;
|
|
1153
|
-
|
|
1154
|
-
return `# Agent Workflow
|
|
1155
|
-
|
|
1156
|
-
## Before Every Task
|
|
1157
|
-
|
|
1158
|
-
1. Read AGENTS.md.
|
|
1159
|
-
2. Read .agent/docs/project.md.
|
|
1160
|
-
3. Use the matching native Superpowers skill for the task.
|
|
1161
|
-
4. Read only the relevant docs under .agent/agents, .agent/skills, .claude/skills, and .agent/workflows.
|
|
1162
|
-
5. Use codebase-memory MCP for structural discovery when available, then verify the actual files before editing.
|
|
1163
|
-
|
|
1164
|
-
## Route Matrix
|
|
1047
|
+
}
|
|
1165
1048
|
|
|
1166
|
-
| Situation | Command | Workflow / gate |
|
|
1167
|
-
|---|---|---|
|
|
1168
|
-
| Question or analysis only | direct answer | no edits |
|
|
1169
|
-
| One obvious local fix | fast path | inspect -> edit -> focused verify |
|
|
1170
|
-
| Bug, regression, failing test, or unknown error | systematic-debugging | reproduce -> root cause -> regression test -> fix -> verify |
|
|
1171
|
-
| Research request | direct research | evidence report, no edits |
|
|
1172
|
-
| New behavior, multi-file change, or unclear scope | brainstorming -> writing-plans -> executing-plans | explicit approval before implementation |
|
|
1173
|
-
| Pre-merge review | requesting-code-review | blocking findings resolved |
|
|
1174
|
-
| Several specialist domains | subagent-driven-development | task review and final review |
|
|
1175
|
-
|
|
1176
|
-
If a task is not clearly simple, use the standard flow. Never use the fast path for behavior, API, schema, security, CI, dependency, workflow, or release changes.
|
|
1177
|
-
|
|
1178
|
-
## Superpowers Skills
|
|
1179
|
-
|
|
1180
|
-
- Use brainstorming for unclear requirements.
|
|
1181
|
-
- Use using-git-worktrees and writing-plans before non-trivial implementation.
|
|
1182
|
-
- Use test-driven-development, executing-plans or subagent-driven-development during implementation.
|
|
1183
|
-
- Use systematic-debugging for bugs and requesting-code-review before completion.
|
|
1184
|
-
- thachvd-kit does not copy or replace Superpowers skills.
|
|
1185
|
-
- Do not load skill bodies by default.
|
|
1186
|
-
- Load a skill when the user mentions it, the task clearly matches its SKILL.md description, or .agent/docs/project.md routes the task to it.
|
|
1187
|
-
- Use explicit skill names in prompts for predictable behavior, for example $webapp-testing or $clean-code.
|
|
1188
|
-
- Run thachvd-kit --help to see common workflow and skill recommendations.
|
|
1189
|
-
|
|
1190
|
-
## Implementation Flow
|
|
1191
|
-
|
|
1192
|
-
For \`/forge-fast\` fixes, state the fast-path decision before editing:
|
|
1193
|
-
|
|
1194
|
-
\`\`\`text
|
|
1195
|
-
FAST_PATH: simple-fix
|
|
1196
|
-
REASON: [why Spec/Plan/Review are not needed]
|
|
1197
|
-
SCOPE: [file or narrow area]
|
|
1198
|
-
\`\`\`
|
|
1199
|
-
|
|
1200
|
-
Then:
|
|
1201
|
-
|
|
1202
|
-
1. State assumptions and success criteria when the task is not trivial.
|
|
1203
|
-
2. Inspect dependent files before editing.
|
|
1204
|
-
3. Make the smallest coherent change.
|
|
1205
|
-
4. Add or update focused tests when behavior changes.
|
|
1206
|
-
5. Run verification (prioritize using Playwright MCP for web/UI changes to automate verification and capture screenshots).
|
|
1207
|
-
6. Summarize changed files and verification evidence.
|
|
1208
|
-
|
|
1209
|
-
For bugs, use \`/forge-debug\` with Superpowers' \`systematic-debugging\` when available, otherwise the matching local skill, plus the \`debugger\` agent and \`testing-patterns\`: reproduce, isolate the root cause, add regression protection, make the smallest fix, and verify.
|
|
1210
|
-
|
|
1211
|
-
For features or refactors, prefer the Superpowers chain when available: brainstorm/spec -> worktree -> detailed plan -> approval -> TDD/task execution -> per-task review -> final review -> finish branch. Otherwise use the local gated flow defined in AGENTS.md (spec -> plan -> implement -> review). Steps 3-5 above are the "Implement" phase, and step 6 is not a substitute for the mandatory review gate.
|
|
1212
|
-
|
|
1213
|
-
## Definition of Done (feature or refactor)
|
|
1214
|
-
|
|
1215
|
-
A feature/refactor task is only complete when all of the following hold — not when code compiles or tests pass:
|
|
1216
|
-
|
|
1217
|
-
The Spec checkbox may be skipped only when the documented \`/forge-fast\` protocol is stated and its criteria are met.
|
|
1218
|
-
|
|
1219
|
-
- [ ] \`.agent/workflows/spec.md\` exit criteria met (or explicitly skipped as a trivial/self-contained change)
|
|
1220
|
-
- [ ] \`.agent/workflows/plan.md\` produced a plan file, approved by the user
|
|
1221
|
-
- [ ] Tests added/updated for the changed behavior and passing
|
|
1222
|
-
- [ ] \`.agent/workflows/review.md\` five-axis review completed with all 🔴 BLOCKING items resolved or accepted with rationale
|
|
1223
|
-
- [ ] Changed files and verification evidence summarized to the user
|
|
1224
|
-
|
|
1225
|
-
Do not report a feature/refactor task as done if any box above is unchecked — say explicitly which gate is still open.
|
|
1226
|
-
|
|
1227
|
-
## When To Update Docs
|
|
1228
|
-
|
|
1229
|
-
- Update .agent/docs/architecture.md when structure, boundaries, or entry points change.
|
|
1230
|
-
- Update .agent/docs/conventions.md when repeated project patterns become clear.
|
|
1231
|
-
- Update .agent/docs/project.md when stack, scripts, app root, test tooling, or routing changes.
|
|
1232
|
-
- Keep AGENTS.md concise. Put project-specific detail in .agent/docs.
|
|
1233
|
-
`;
|
|
1234
|
-
}
|
|
1235
|
-
|
|
1236
1049
|
function generateGettingStartedDoc() {
|
|
1237
1050
|
return `# Getting Started
|
|
1238
1051
|
|
|
@@ -1241,8 +1054,8 @@ thachvd-kit creates project context. Superpowers owns the development workflow.
|
|
|
1241
1054
|
## Setup
|
|
1242
1055
|
|
|
1243
1056
|
1. Run \`thachvd-kit init\` in the repository.
|
|
1244
|
-
2.
|
|
1245
|
-
3.
|
|
1057
|
+
2. Open or print \`.agent/docs/index-project-prompt.md\` with \`thachvd-kit prompt\`.
|
|
1058
|
+
3. Install Superpowers and RTK, then run \`thachvd-kit setup\`.
|
|
1246
1059
|
4. Run \`thachvd-kit doctor\` and restart the AI client.
|
|
1247
1060
|
|
|
1248
1061
|
## Standard Flow
|
|
@@ -1264,124 +1077,26 @@ Use \`systematic-debugging\`: reproduce the symptom, inspect the path with codeb
|
|
|
1264
1077
|
|
|
1265
1078
|
Use a fast path only for one-file, unambiguous changes with no behavior or contract change. State the scope and verification before editing. If the scope expands, return to brainstorming and writing-plans.
|
|
1266
1079
|
`;
|
|
1267
|
-
|
|
1268
|
-
return `# Getting Started With This Kit
|
|
1269
|
-
|
|
1270
|
-
Read this after \`thachvd-kit init\`. The kit gives every member one entry point, then selects only the process and expertise the task needs.
|
|
1271
|
-
|
|
1272
|
-
## Forge Command Map
|
|
1273
|
-
|
|
1274
|
-
| Client | Task | Fast | Debug | Research | Review |
|
|
1275
|
-
|---|---|---|---|---|---|
|
|
1276
|
-
| Codex | \`$forge-task\` | \`$forge-fast\` | \`$forge-debug\` | \`$forge-research\` | \`$forge-review\` |
|
|
1277
|
-
| Antigravity | \`/forge-task\` | \`/forge-fast\` | \`/forge-debug\` | \`/forge-research\` | \`/forge-review\` |
|
|
1278
|
-
| Claude Code | \`/forge-task\` | \`/forge-fast\` | \`/forge-debug\` | \`/forge-research\` | \`/forge-review\` |
|
|
1279
|
-
| Cursor | \`/forge-task\` | \`/forge-fast\` | \`/forge-debug\` | \`/forge-research\` | \`/forge-review\` |
|
|
1280
|
-
|
|
1281
|
-
Forge commands accept free-form text after the command and continue the selected workflow in the same turn. Plain questions remain normal chat; edit requests without a command are implicitly routed as \`forge-task\`.
|
|
1282
|
-
|
|
1283
|
-
Forge is a routing layer. For feature/refactor work, prefer native Superpowers skills when available: brainstorming, using-git-worktrees, writing-plans, subagent-driven-development or executing-plans, test-driven-development, requesting-code-review, and finishing-a-development-branch. If Superpowers is not installed, use the matching \`.agent/skills/\` and \`.agent/workflows/\` fallback; do not maintain a competing duplicate methodology. Install the full execution backend from https://github.com/obra/superpowers#quickstart.
|
|
1284
|
-
|
|
1285
|
-
Codex Forge skills live in \`~/.codex/skills/forge-*/SKILL.md\`. Antigravity Forge workflows live in \`.agent/workflows/forge-*.md\`. Claude Code Forge commands live in \`.claude/commands/forge-*.md\`. Cursor Forge commands live in \`.cursor/commands/forge-*.md\`.
|
|
1286
|
-
|
|
1287
|
-
## Start Every Task With Forge
|
|
1288
|
-
|
|
1289
|
-
Use the matching Forge command before editing whenever the request is not a simple question. \`forge-task\` must route internally and continue; it must not stop by asking the member to type a second command.
|
|
1290
|
-
|
|
1291
|
-
\`\`\`text
|
|
1292
|
-
ROUTE: question | fast | feature | bug | research | review | multi-domain
|
|
1293
|
-
WORKFLOW: [exact .agent/workflows/<name>.md file]
|
|
1294
|
-
SKILLS: [matching skills, or none]
|
|
1295
|
-
AGENTS: [matching specialist agents, or none]
|
|
1296
|
-
GATE: fast-path | standard | human-approval
|
|
1297
|
-
BACKEND: superpowers-native | forge-fallback
|
|
1298
|
-
ACTION: [what the agent is doing next in this turn]
|
|
1299
|
-
\`\`\`
|
|
1300
|
-
|
|
1301
|
-
A workflow is the process and its gates. A skill is the method or checklist. An agent is the specialist role. Chat alone is not a route: the member must select the route and invoke the matching workflow or explicitly choose the question path.
|
|
1302
|
-
|
|
1303
|
-
## Route Matrix
|
|
1304
|
-
|
|
1305
|
-
| Situation | Command | Gate | Required next step |
|
|
1306
|
-
|---|---|---|---|
|
|
1307
|
-
| Explain, inspect, or brainstorm only | direct answer or \`/brainstorm\` | none | no code until the request becomes actionable |
|
|
1308
|
-
| One obvious local fix | \`/forge-fast <request>\` | fast-path | inspect -> edit -> focused verify |
|
|
1309
|
-
| New behavior, multi-file change, unclear scope, or >~30 min | \`/forge-task <request>\` | standard | route -> research/spec/plan -> approval when required -> continue |
|
|
1310
|
-
| Bug, regression, failing test, or unknown error | \`/forge-debug <symptom>\` | bug | reproduce -> isolate -> regression test -> fix -> verify |
|
|
1311
|
-
| Research request | \`/forge-research <question>\` | research | evidence report, no edits |
|
|
1312
|
-
| Pre-merge or independent review | \`/forge-review\` | review | five-axis review; resolve blocking findings |
|
|
1313
|
-
| Frontend + backend + data/security together | \`/orchestrate\` | standard | plan -> approval -> specialist work -> integration verify |
|
|
1314
|
-
|
|
1315
|
-
## Standard Feature Flow
|
|
1316
|
-
|
|
1317
|
-
The gated sequence is Spec -> Plan -> Implement -> Review. \`/forge-task\` manages this sequence internally and pauses only at the explicit user approval gate before implementation.
|
|
1318
|
-
|
|
1319
|
-
The plan is a gate, not a suggestion. Do not start product-code implementation before explicit approval. Load only the relevant specialist agent and skills, such as \`$frontend-design\`, \`$api-design\`, or \`$webapp-testing\`.
|
|
1320
|
-
|
|
1321
|
-
## Bug Fix Flow
|
|
1322
|
-
|
|
1323
|
-
Use \`/forge-debug <symptom>\`, not the feature flow, when something is failing or the root cause is unknown:
|
|
1324
|
-
|
|
1325
|
-
1. Capture the exact symptom, environment, reproduction, and expected result.
|
|
1326
|
-
2. Inspect the relevant code path with MCP/codegraph when available.
|
|
1327
|
-
3. Form and test a small number of hypotheses; do not patch by guesswork.
|
|
1328
|
-
4. Add or update a regression test that fails before the fix when practical.
|
|
1329
|
-
5. Make the smallest root-cause fix.
|
|
1330
|
-
6. Run focused tests, then broader verification when the blast radius is shared.
|
|
1331
|
-
7. Use \`/review\` when the fix touches multiple files, contracts, security, or release behavior.
|
|
1332
|
-
|
|
1333
|
-
Load Superpowers' \`systematic-debugging\` when available, otherwise the matching local skill, plus the \`debugger\` agent and \`testing-patterns\` for this route.
|
|
1334
|
-
|
|
1335
|
-
## Simple Fast Path
|
|
1336
|
-
|
|
1337
|
-
Use \`/forge-fast <request>\` only when the change is one obvious file, the result is unambiguous, and it changes no behavior, API, schema, security, CI, dependency, workflow, or release contract. Before editing, state:
|
|
1338
|
-
|
|
1339
|
-
\`\`\`text
|
|
1340
|
-
FAST_PATH: simple-fix
|
|
1341
|
-
REASON: [why Spec/Plan/Review are not needed]
|
|
1342
|
-
SCOPE: [file or narrow area]
|
|
1343
|
-
\`\`\`
|
|
1344
|
-
|
|
1345
|
-
This bypasses only Spec, Plan, and the feature/refactor Review gate. It never bypasses inspection, focused verification, or reporting evidence. If the scope expands, stop and re-route internally through \`forge-task\`.
|
|
1346
|
-
|
|
1347
|
-
## Command And Skill Map
|
|
1348
|
-
|
|
1349
|
-
- \`/brainstorm\`: clarify intent and tradeoffs; no code.
|
|
1350
|
-
- \`forge-research\`: evidence-based codebase/technical/domain research; no product-code edits.
|
|
1351
|
-
- \`/plan\`: create an approved implementation plan; no code before approval.
|
|
1352
|
-
- \`/enhance\` or \`/create\`: implement an approved feature with the relevant specialist agent.
|
|
1353
|
-
- \`forge-debug\`: systematic root-cause investigation and regression protection.
|
|
1354
|
-
- \`/test\`: add or run tests and assess coverage.
|
|
1355
|
-
- \`/review\`: five-axis review before completion or merge.
|
|
1356
|
-
- \`$clean-code\` and \`$verification-before-completion\`: default implementation hygiene.
|
|
1357
|
-
- \`$webapp-testing\`: browser/UI verification; use Playwright when available.
|
|
1358
|
-
|
|
1359
|
-
## Enforcement
|
|
1360
|
-
|
|
1361
|
-
Claude Code has a project Stop hook in \`.claude/settings.json\` for standard feature/refactor work. An explicit \`FAST_PATH: simple-fix\` is accepted only when the simple criteria are met; it still requires verification. Codex, Antigravity, and Cursor use the same written rules through \`AGENTS.md\`, \`.cursorrules\`, and \`.agent/rules/GEMINI.md\`.
|
|
1080
|
+
}
|
|
1362
1081
|
|
|
1363
|
-
|
|
1364
|
-
|
|
1082
|
+
function resolveToolingSetup(data) {
|
|
1083
|
+
const primaryLang = data.primary_language || 'javascript';
|
|
1084
|
+
const packageManager = data.package_manager || 'npm';
|
|
1085
|
+
const isPython = primaryLang === 'python';
|
|
1086
|
+
const runner = packageManager === 'pnpm'
|
|
1087
|
+
? 'pnpm dlx'
|
|
1088
|
+
: packageManager === 'yarn'
|
|
1089
|
+
? 'yarn dlx'
|
|
1090
|
+
: packageManager === 'bun'
|
|
1091
|
+
? 'bunx'
|
|
1092
|
+
: 'npx';
|
|
1093
|
+
const playwrightCommand = isPython
|
|
1094
|
+
? 'pip install playwright && playwright install chromium'
|
|
1095
|
+
: `${runner} playwright install`;
|
|
1096
|
+
|
|
1097
|
+
return { runner, playwrightCommand };
|
|
1365
1098
|
}
|
|
1366
1099
|
|
|
1367
|
-
function resolveToolingSetup(data) {
|
|
1368
|
-
const primaryLang = data.primary_language || 'javascript';
|
|
1369
|
-
const packageManager = data.package_manager || 'npm';
|
|
1370
|
-
const isPython = primaryLang === 'python';
|
|
1371
|
-
const runner = packageManager === 'pnpm'
|
|
1372
|
-
? 'pnpm dlx'
|
|
1373
|
-
: packageManager === 'yarn'
|
|
1374
|
-
? 'yarn dlx'
|
|
1375
|
-
: packageManager === 'bun'
|
|
1376
|
-
? 'bunx'
|
|
1377
|
-
: 'npx';
|
|
1378
|
-
const playwrightCommand = isPython
|
|
1379
|
-
? 'pip install playwright && playwright install chromium'
|
|
1380
|
-
: `${runner} playwright install`;
|
|
1381
|
-
|
|
1382
|
-
return { runner, playwrightCommand };
|
|
1383
|
-
}
|
|
1384
|
-
|
|
1385
1100
|
function generateToolingDoc(data) {
|
|
1386
1101
|
const { runner, playwrightCommand } = resolveToolingSetup(data);
|
|
1387
1102
|
|
|
@@ -1411,10 +1126,11 @@ Use \`codebase-memory-mcp\` for symbol search, call paths, architecture, and imp
|
|
|
1411
1126
|
|
|
1412
1127
|
## RTK
|
|
1413
1128
|
|
|
1414
|
-
RTK is
|
|
1129
|
+
RTK is the recommended command-output layer, not a workflow engine. Prefer it by default for verbose shell commands.
|
|
1415
1130
|
|
|
1416
1131
|
- Install: \`cargo install --git https://github.com/rtk-ai/rtk --branch master rtk\`
|
|
1417
|
-
-
|
|
1132
|
+
- Examples: \`rtk git status\`, \`rtk npm test\`, \`rtk git log\`.
|
|
1133
|
+
- If it is unavailable, run the underlying command directly.
|
|
1418
1134
|
|
|
1419
1135
|
## Playwright
|
|
1420
1136
|
|
|
@@ -1422,368 +1138,33 @@ RTK is optional command-output compression, not a workflow engine.
|
|
|
1422
1138
|
- Browser install: \`${playwrightCommand}\`
|
|
1423
1139
|
- Use the project's existing Playwright scripts when available.
|
|
1424
1140
|
`;
|
|
1425
|
-
/* Legacy tooling text below remains unreachable for source compatibility. */
|
|
1426
|
-
|
|
1427
|
-
return `# Optional Tooling
|
|
1428
|
-
|
|
1429
|
-
This kit keeps tool setup explicit. Do not assume these tools are available until you verify them in the current environment.
|
|
1430
|
-
|
|
1431
|
-
## Playwright
|
|
1432
|
-
|
|
1433
|
-
Use Playwright for browser and UI verification when a task touches web behavior.
|
|
1434
|
-
|
|
1435
|
-
- Check availability: \`${runner} playwright --version\`
|
|
1436
|
-
- Install browsers: \`${playwrightCommand}\`
|
|
1437
|
-
- Codex MCP CLI setup: \`codex mcp add playwright -- playwright-mcp\`
|
|
1438
|
-
- Auto setup: \`thachvd-kit\`
|
|
1439
|
-
- Kit helper: \`python .agent/skills/webapp-testing/scripts/playwright_runner.py <url> --screenshot\`
|
|
1440
|
-
|
|
1441
|
-
If the project already has Playwright configured, prefer the project's existing scripts.
|
|
1442
|
-
|
|
1443
|
-
Codex \`config.toml\` example:
|
|
1444
|
-
|
|
1445
|
-
\`\`\`toml
|
|
1446
|
-
[mcp_servers.context7]
|
|
1447
|
-
command = "context7-mcp"
|
|
1448
|
-
startup_timeout_sec = 20
|
|
1449
|
-
tool_timeout_sec = 120
|
|
1450
|
-
|
|
1451
|
-
[mcp_servers.filesystem]
|
|
1452
|
-
command = "npx"
|
|
1453
|
-
args = ["-y", "@modelcontextprotocol/server-filesystem", "<projectPath>"]
|
|
1454
|
-
startup_timeout_sec = 20
|
|
1455
|
-
tool_timeout_sec = 120
|
|
1456
|
-
|
|
1457
|
-
[mcp_servers.playwright]
|
|
1458
|
-
command = "playwright-mcp"
|
|
1459
|
-
startup_timeout_sec = 20
|
|
1460
|
-
tool_timeout_sec = 120
|
|
1461
|
-
\`\`\`
|
|
1462
|
-
|
|
1463
|
-
## Codegraph
|
|
1464
|
-
|
|
1465
|
-
Use codegraph for codebase exploration when the environment exposes it. The index is local state and should not be committed.
|
|
1466
|
-
|
|
1467
|
-
- Check for an index: look for \`.codegraph/\`
|
|
1468
|
-
- Check CLI availability: \`codegraph --help\`
|
|
1469
|
-
- Install CLI if needed: \`npm install -g @colbymchenry/codegraph\`
|
|
1470
|
-
- Codex MCP CLI setup: \`codex mcp add codegraph -- codegraph serve --mcp\`
|
|
1471
|
-
- Auto setup: \`thachvd-kit\`
|
|
1472
|
-
- If your Codex environment provides the codegraph CLI, run its project indexing step from the repository root.
|
|
1473
|
-
- Keep \`.codegraph/\` ignored in git.
|
|
1474
|
-
|
|
1475
|
-
Codex \`config.toml\` example:
|
|
1476
|
-
|
|
1477
|
-
\`\`\`toml
|
|
1478
|
-
[mcp_servers.codegraph]
|
|
1479
|
-
command = "codegraph"
|
|
1480
|
-
args = ["serve", "--mcp"]
|
|
1481
|
-
startup_timeout_sec = 20
|
|
1482
|
-
tool_timeout_sec = 120
|
|
1483
|
-
\`\`\`
|
|
1484
|
-
|
|
1485
|
-
Codex reads MCP servers from \`~/.codex/config.toml\` by default. For repo-local setup, use \`.codex/config.toml\`; Codex loads project config only for trusted projects.
|
|
1486
|
-
|
|
1487
|
-
Gemini CLI / Antigravity \`mcp_config.json\` example:
|
|
1488
|
-
|
|
1489
|
-
\`\`\`json
|
|
1490
|
-
{
|
|
1491
|
-
"mcpServers": {
|
|
1492
|
-
"codegraph": {
|
|
1493
|
-
"command": "codegraph",
|
|
1494
|
-
"args": ["serve", "--mcp"]
|
|
1495
|
-
},
|
|
1496
|
-
"context7": {
|
|
1497
|
-
"command": "context7-mcp"
|
|
1498
|
-
},
|
|
1499
|
-
"filesystem": {
|
|
1500
|
-
"command": "npx",
|
|
1501
|
-
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/project"]
|
|
1502
|
-
},
|
|
1503
|
-
"playwright": {
|
|
1504
|
-
"command": "playwright-mcp"
|
|
1505
|
-
}
|
|
1506
|
-
}
|
|
1507
|
-
}
|
|
1508
|
-
\`\`\`
|
|
1509
|
-
|
|
1510
|
-
Common local paths:
|
|
1511
|
-
|
|
1512
|
-
- Gemini CLI: \`~/.gemini/config/mcp_config.json\`
|
|
1513
|
-
- Antigravity IDE: \`~/.gemini/antigravity-ide/mcp_config.json\`
|
|
1514
|
-
- Older Antigravity setups may use \`~/.gemini/antigravity/mcp_config.json\`
|
|
1515
|
-
|
|
1516
|
-
Claude Code user config example:
|
|
1517
|
-
|
|
1518
|
-
\`\`\`json
|
|
1519
|
-
{
|
|
1520
|
-
"mcpServers": {
|
|
1521
|
-
"codegraph": {
|
|
1522
|
-
"type": "stdio",
|
|
1523
|
-
"command": "codegraph",
|
|
1524
|
-
"args": ["serve", "--mcp"]
|
|
1525
|
-
},
|
|
1526
|
-
"context7": {
|
|
1527
|
-
"type": "stdio",
|
|
1528
|
-
"command": "context7-mcp"
|
|
1529
|
-
},
|
|
1530
|
-
"filesystem": {
|
|
1531
|
-
"type": "stdio",
|
|
1532
|
-
"command": "npx",
|
|
1533
|
-
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/project"]
|
|
1534
|
-
},
|
|
1535
|
-
"playwright": {
|
|
1536
|
-
"type": "stdio",
|
|
1537
|
-
"command": "playwright-mcp"
|
|
1538
|
-
}
|
|
1539
|
-
}
|
|
1540
|
-
}
|
|
1541
|
-
\`\`\`
|
|
1542
|
-
|
|
1543
|
-
Common local path:
|
|
1544
|
-
|
|
1545
|
-
- Claude Code: \`~/.claude.json\` or \`~/.claude/settings.json\`
|
|
1546
|
-
|
|
1547
|
-
## Native Skill Folders
|
|
1548
|
-
|
|
1549
|
-
The shared kit skills live in \`.agent/skills/\`. Antigravity reads that folder directly. Codex shows skills from \`~/.codex/skills/\` in the \`$\` menu. Claude Code reads project skills from \`.claude/skills/\`.
|
|
1550
|
-
|
|
1551
|
-
- Codex global skills: \`~/.codex/skills/\` — installed by \`thachvd-kit init\`, visible in Codex \`$\` menu
|
|
1552
|
-
- Claude Code project skills: \`.claude/skills/\`
|
|
1553
|
-
- Antigravity: reads \`.agent/skills/\` directly (no copy needed)
|
|
1554
|
-
- Keep \`.agent/skills/\` as the full shared source of truth committed with the repo.
|
|
1555
|
-
- Do not commit \`~/.codex/skills/\`; it is local user state.
|
|
1556
|
-
|
|
1557
|
-
Recommended candidates to copy first:
|
|
1558
|
-
|
|
1559
|
-
- \`clean-code\`
|
|
1560
|
-
- \`systematic-debugging\`
|
|
1561
|
-
- \`verification-before-completion\`
|
|
1562
|
-
- Stack-specific skills listed in \`.agent/docs/project.md\`
|
|
1563
|
-
`;
|
|
1564
|
-
}
|
|
1565
|
-
|
|
1566
|
-
// --- Agent Folder Copy ---
|
|
1567
|
-
|
|
1568
|
-
function copyAgentFolder() {
|
|
1569
|
-
const srcAgent = path.join(sourceDir, '.agent');
|
|
1570
|
-
if (!fs.existsSync(srcAgent)) return { copied: 0, skipped: 0 };
|
|
1571
|
-
|
|
1572
|
-
const destAgent = path.join(targetDir, '.agent');
|
|
1573
|
-
let copied = 0;
|
|
1574
|
-
|
|
1575
|
-
function copyDir(src, dest) {
|
|
1576
|
-
fs.mkdirSync(dest, { recursive: true });
|
|
1577
|
-
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
1578
|
-
const srcPath = path.join(src, entry.name);
|
|
1579
|
-
const destPath = path.join(dest, entry.name);
|
|
1580
|
-
const relativeSrc = path.relative(srcAgent, srcPath).split(path.sep).join('/');
|
|
1581
|
-
if (relativeSrc === 'docs' || relativeSrc.startsWith('docs/')) {
|
|
1582
|
-
continue;
|
|
1583
|
-
}
|
|
1584
|
-
if (entry.isDirectory()) {
|
|
1585
|
-
copyDir(srcPath, destPath);
|
|
1586
|
-
} else {
|
|
1587
|
-
fs.copyFileSync(srcPath, destPath);
|
|
1588
|
-
copied++;
|
|
1589
|
-
}
|
|
1590
|
-
}
|
|
1591
|
-
}
|
|
1592
|
-
|
|
1593
|
-
copyDir(srcAgent, destAgent);
|
|
1594
|
-
return { copied, skipped: 0 };
|
|
1595
|
-
}
|
|
1596
|
-
|
|
1597
|
-
function copySelectedSkillFolders(data, assumeYes) {
|
|
1598
|
-
const srcSkills = path.join(sourceDir, '.agent', 'skills');
|
|
1599
|
-
if (!fs.existsSync(srcSkills)) return { copied: 0, skipped: 0, missing: 0, skills: [] };
|
|
1600
|
-
|
|
1601
|
-
const selectedSkills = resolveNativeSkills(data);
|
|
1602
|
-
|
|
1603
|
-
// ~/.codex/skills/ — global Codex user skills dir, always visible in the $ menu
|
|
1604
|
-
const codexSkillsDir = homePath('.codex', 'skills');
|
|
1605
|
-
// .claude/skills/ — project-level Claude Code skills
|
|
1606
|
-
const claudeSkillsDir = path.join(targetDir, '.claude', 'skills');
|
|
1607
|
-
|
|
1608
|
-
const destinations = [claudeSkillsDir];
|
|
1609
|
-
if (codexSkillsDir) destinations.push(codexSkillsDir);
|
|
1610
|
-
|
|
1611
|
-
let copied = 0;
|
|
1612
|
-
let missing = 0;
|
|
1613
|
-
|
|
1614
|
-
function copyDir(src, dest) {
|
|
1615
|
-
fs.mkdirSync(dest, { recursive: true });
|
|
1616
|
-
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
1617
|
-
const srcPath = path.join(src, entry.name);
|
|
1618
|
-
const destPath = path.join(dest, entry.name);
|
|
1619
|
-
if (entry.isDirectory()) {
|
|
1620
|
-
copyDir(srcPath, destPath);
|
|
1621
|
-
} else {
|
|
1622
|
-
fs.mkdirSync(path.dirname(destPath), { recursive: true });
|
|
1623
|
-
fs.copyFileSync(srcPath, destPath);
|
|
1624
|
-
copied++;
|
|
1625
|
-
}
|
|
1626
|
-
}
|
|
1627
|
-
}
|
|
1628
|
-
|
|
1629
|
-
for (const skill of selectedSkills) {
|
|
1630
|
-
const srcSkill = path.join(srcSkills, skill);
|
|
1631
|
-
if (!fs.existsSync(srcSkill)) {
|
|
1632
|
-
missing++;
|
|
1633
|
-
continue;
|
|
1634
|
-
}
|
|
1635
|
-
for (const destSkills of destinations) {
|
|
1636
|
-
copyDir(srcSkill, path.join(destSkills, skill));
|
|
1637
|
-
}
|
|
1638
|
-
}
|
|
1639
|
-
|
|
1640
|
-
return { copied, skipped: 0, missing, skills: selectedSkills };
|
|
1641
|
-
}
|
|
1642
|
-
|
|
1643
|
-
function copyCodexPrompts() {
|
|
1644
|
-
const srcPrompts = path.join(sourceDir, 'prompts');
|
|
1645
|
-
const codexPromptsDir = homePath('.codex', 'prompts');
|
|
1646
|
-
if (!fs.existsSync(srcPrompts) || !codexPromptsDir) {
|
|
1647
|
-
return { copied: 0, prompts: [] };
|
|
1648
|
-
}
|
|
1649
|
-
|
|
1650
|
-
let copied = 0;
|
|
1651
|
-
const prompts = [];
|
|
1652
|
-
fs.mkdirSync(codexPromptsDir, { recursive: true });
|
|
1653
|
-
for (const entry of fs.readdirSync(srcPrompts, { withFileTypes: true })) {
|
|
1654
|
-
if (!entry.isFile() || !entry.name.endsWith('.md')) continue;
|
|
1655
|
-
fs.copyFileSync(path.join(srcPrompts, entry.name), path.join(codexPromptsDir, entry.name));
|
|
1656
|
-
copied++;
|
|
1657
|
-
prompts.push(path.basename(entry.name, '.md'));
|
|
1658
|
-
}
|
|
1659
|
-
return { copied, prompts };
|
|
1660
|
-
}
|
|
1661
|
-
|
|
1662
|
-
function copyClientWorkflowCommands() {
|
|
1663
|
-
const srcPrompts = path.join(sourceDir, 'prompts');
|
|
1664
|
-
const claudeSkillsDir = path.join(targetDir, '.claude', 'skills');
|
|
1665
|
-
const cursorCommandsDir = path.join(targetDir, '.cursor', 'commands');
|
|
1666
|
-
if (!fs.existsSync(srcPrompts)) {
|
|
1667
|
-
return { claude: 0, cursor: 0, prompts: [] };
|
|
1668
|
-
}
|
|
1669
|
-
|
|
1670
|
-
let claude = 0;
|
|
1671
|
-
let cursor = 0;
|
|
1672
|
-
const prompts = [];
|
|
1673
|
-
for (const entry of fs.readdirSync(srcPrompts, { withFileTypes: true })) {
|
|
1674
|
-
if (!entry.isFile() || !entry.name.endsWith('.md')) continue;
|
|
1675
|
-
const name = path.basename(entry.name, '.md');
|
|
1676
|
-
const sourcePath = path.join(srcPrompts, entry.name);
|
|
1677
|
-
const sourceContent = fs.readFileSync(sourcePath, 'utf8');
|
|
1678
|
-
const cursorContent = sourceContent.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n/, '');
|
|
1679
|
-
fs.mkdirSync(path.join(claudeSkillsDir, name), { recursive: true });
|
|
1680
|
-
fs.copyFileSync(sourcePath, path.join(claudeSkillsDir, name, 'SKILL.md'));
|
|
1681
|
-
fs.mkdirSync(cursorCommandsDir, { recursive: true });
|
|
1682
|
-
fs.writeFileSync(path.join(cursorCommandsDir, entry.name), cursorContent, 'utf8');
|
|
1683
|
-
claude++;
|
|
1684
|
-
cursor++;
|
|
1685
|
-
prompts.push(name);
|
|
1686
|
-
}
|
|
1687
|
-
return { claude, cursor, prompts };
|
|
1688
1141
|
}
|
|
1689
1142
|
|
|
1690
|
-
function
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
claude: path.join(targetDir, '.claude', 'commands'),
|
|
1704
|
-
cursor: path.join(targetDir, '.cursor', 'commands')
|
|
1705
|
-
};
|
|
1706
|
-
|
|
1707
|
-
for (const commandName of REMOVED_FORGE_COMMANDS) {
|
|
1708
|
-
const filename = `${commandName}.md`;
|
|
1709
|
-
for (const destination of Object.values(destinations)) {
|
|
1710
|
-
const commandPath = path.join(destination, filename);
|
|
1711
|
-
if (fs.existsSync(commandPath)) fs.rmSync(commandPath, { force: true });
|
|
1143
|
+
async function writeGeneratedFile(relativePath, content, assumeYes) {
|
|
1144
|
+
const outputPath = path.join(targetDir, relativePath);
|
|
1145
|
+
const exists = fs.existsSync(outputPath);
|
|
1146
|
+
if (exists && !assumeYes) {
|
|
1147
|
+
const response = await prompts({
|
|
1148
|
+
type: 'confirm',
|
|
1149
|
+
name: 'overwrite',
|
|
1150
|
+
message: `File ${pc.bold(relativePath)} already exists. Do you want to overwrite it?`,
|
|
1151
|
+
initial: false
|
|
1152
|
+
});
|
|
1153
|
+
if (response.overwrite === undefined) {
|
|
1154
|
+
console.log(pc.yellow('\nCancelled.'));
|
|
1155
|
+
process.exit(130);
|
|
1712
1156
|
}
|
|
1713
|
-
if (
|
|
1714
|
-
const codexSkillPath = path.join(codexSkillsDir, commandName);
|
|
1715
|
-
if (fs.existsSync(codexSkillPath)) fs.rmSync(codexSkillPath, { recursive: true, force: true });
|
|
1716
|
-
}
|
|
1717
|
-
}
|
|
1718
|
-
|
|
1719
|
-
let antigravity = 0;
|
|
1720
|
-
let claude = 0;
|
|
1721
|
-
let cursor = 0;
|
|
1722
|
-
let codex = 0;
|
|
1723
|
-
const commands = [];
|
|
1724
|
-
|
|
1725
|
-
for (const commandName of FORGE_COMMANDS) {
|
|
1726
|
-
const filename = `${commandName}.md`;
|
|
1727
|
-
const sourcePath = path.join(srcCommands, filename);
|
|
1728
|
-
if (!fs.existsSync(sourcePath)) continue;
|
|
1729
|
-
const sourceContent = fs.readFileSync(sourcePath, 'utf8');
|
|
1730
|
-
const body = stripFrontmatter(sourceContent);
|
|
1731
|
-
|
|
1732
|
-
fs.mkdirSync(destinations.antigravity, { recursive: true });
|
|
1733
|
-
fs.copyFileSync(sourcePath, path.join(destinations.antigravity, filename));
|
|
1734
|
-
antigravity++;
|
|
1735
|
-
|
|
1736
|
-
fs.mkdirSync(destinations.claude, { recursive: true });
|
|
1737
|
-
fs.copyFileSync(sourcePath, path.join(destinations.claude, filename));
|
|
1738
|
-
claude++;
|
|
1739
|
-
|
|
1740
|
-
fs.mkdirSync(destinations.cursor, { recursive: true });
|
|
1741
|
-
fs.writeFileSync(path.join(destinations.cursor, filename), body, 'utf8');
|
|
1742
|
-
cursor++;
|
|
1743
|
-
|
|
1744
|
-
if (codexSkillsDir) {
|
|
1745
|
-
const skillDir = path.join(codexSkillsDir, commandName);
|
|
1746
|
-
fs.mkdirSync(skillDir, { recursive: true });
|
|
1747
|
-
const skillBody = body.replace(/\$ARGUMENTS/g, 'the request supplied with this skill');
|
|
1748
|
-
fs.writeFileSync(
|
|
1749
|
-
path.join(skillDir, 'SKILL.md'),
|
|
1750
|
-
`---\nname: ${commandName}\ndescription: ${sourceContent.match(/^description:\s*(.+)$/m)?.[1] || `Run ${commandName}`}\n---\n\nThis skill is invoked as $${commandName}. Treat the user's text supplied with the skill as the request.\n\n${skillBody}`,
|
|
1751
|
-
'utf8'
|
|
1752
|
-
);
|
|
1753
|
-
codex++;
|
|
1754
|
-
}
|
|
1755
|
-
|
|
1756
|
-
commands.push(commandName);
|
|
1157
|
+
if (!response.overwrite) return false;
|
|
1757
1158
|
}
|
|
1758
1159
|
|
|
1759
|
-
|
|
1160
|
+
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
|
1161
|
+
fs.writeFileSync(outputPath, content, 'utf8');
|
|
1162
|
+
return true;
|
|
1760
1163
|
}
|
|
1761
|
-
|
|
1762
|
-
async function writeGeneratedFile(relativePath, content, assumeYes) {
|
|
1763
|
-
const outputPath = path.join(targetDir, relativePath);
|
|
1764
|
-
const exists = fs.existsSync(outputPath);
|
|
1765
|
-
if (exists && !assumeYes) {
|
|
1766
|
-
const response = await prompts({
|
|
1767
|
-
type: 'confirm',
|
|
1768
|
-
name: 'overwrite',
|
|
1769
|
-
message: `File ${pc.bold(relativePath)} already exists. Do you want to overwrite it?`,
|
|
1770
|
-
initial: false
|
|
1771
|
-
});
|
|
1772
|
-
if (response.overwrite === undefined) {
|
|
1773
|
-
console.log(pc.yellow('\nCancelled.'));
|
|
1774
|
-
process.exit(130);
|
|
1775
|
-
}
|
|
1776
|
-
if (!response.overwrite) return false;
|
|
1777
|
-
}
|
|
1778
|
-
|
|
1779
|
-
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
|
1780
|
-
fs.writeFileSync(outputPath, content, 'utf8');
|
|
1781
|
-
return true;
|
|
1782
|
-
}
|
|
1783
|
-
|
|
1164
|
+
|
|
1784
1165
|
function homePath(...parts) {
|
|
1785
|
-
const home = process.env.USERPROFILE || process.env.HOME;
|
|
1786
|
-
return home ? path.join(home, ...parts) : null;
|
|
1166
|
+
const home = process.env.USERPROFILE || process.env.HOME;
|
|
1167
|
+
return home ? path.join(home, ...parts) : null;
|
|
1787
1168
|
}
|
|
1788
1169
|
|
|
1789
1170
|
function printSuperpowersInstallGuide() {
|
|
@@ -1810,23 +1191,26 @@ function runDoctor() {
|
|
|
1810
1191
|
|
|
1811
1192
|
const tools = [
|
|
1812
1193
|
[CODEBASE_MEMORY_BINARY, `npm install -g ${CODEBASE_MEMORY_PACKAGE}`],
|
|
1813
|
-
['rtk', RTK_INSTALL_COMMAND],
|
|
1194
|
+
['rtk', `recommended: ${RTK_INSTALL_COMMAND}`],
|
|
1814
1195
|
['agy', 'install Antigravity before running its Superpowers plugin command'],
|
|
1815
1196
|
['claude', 'install Claude Code before installing its Superpowers plugin'],
|
|
1816
1197
|
['codex', 'install Codex before using its Superpowers plugin']
|
|
1817
1198
|
];
|
|
1818
|
-
console.log(`\n${pc.bold('Local integrations')}`);
|
|
1199
|
+
console.log(`\n${pc.bold('Local integrations (RTK recommended)')}`);
|
|
1819
1200
|
for (const [binary, hint] of tools) {
|
|
1820
1201
|
const ok = commandExists(binary);
|
|
1821
1202
|
console.log(` ${ok ? pc.green('OK') : pc.yellow('WARN')} ${binary}${ok ? '' : ` - ${hint}`}`);
|
|
1822
1203
|
}
|
|
1823
1204
|
|
|
1824
1205
|
console.log(`\n${pc.bold('Next step')}`);
|
|
1825
|
-
console.log(' Run thachvd-kit
|
|
1206
|
+
console.log(' Run thachvd-kit prompt to print the saved project-indexing prompt.');
|
|
1826
1207
|
}
|
|
1827
1208
|
|
|
1828
1209
|
async function runSetup(args) {
|
|
1829
1210
|
const skipMcpSetup = args.includes('--no-setup-mcp');
|
|
1211
|
+
const skipHookSetup = args.includes('--no-setup-hook');
|
|
1212
|
+
const skipRtkInstall = args.includes('--no-install-rtk');
|
|
1213
|
+
const skipIndex = args.includes('--no-index');
|
|
1830
1214
|
console.log(pc.bold(pc.cyan('thachvd-kit setup')));
|
|
1831
1215
|
printSuperpowersInstallGuide();
|
|
1832
1216
|
if (!skipMcpSetup) {
|
|
@@ -1835,23 +1219,35 @@ async function runSetup(args) {
|
|
|
1835
1219
|
const marker = result.ok ? pc.green('OK') : pc.yellow('WARN');
|
|
1836
1220
|
console.log(` ${marker} ${result.message}`);
|
|
1837
1221
|
}
|
|
1222
|
+
if (!skipIndex) {
|
|
1223
|
+
const indexResult = indexRepository();
|
|
1224
|
+
const marker = indexResult.ok ? pc.green('OK') : pc.yellow('WARN');
|
|
1225
|
+
console.log(` ${marker} ${indexResult.message}`);
|
|
1226
|
+
}
|
|
1838
1227
|
}
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1228
|
+
if (!skipHookSetup) {
|
|
1229
|
+
console.log(`\n${pc.bold('Claude Code Definition-of-Done hook')}`);
|
|
1230
|
+
const claudeProjectSettings = path.join(targetDir, '.claude', 'settings.json');
|
|
1231
|
+
const result = mergeClaudeProjectHooks(claudeProjectSettings);
|
|
1232
|
+
const marker = result.ok ? pc.green('OK') : pc.yellow('WARN');
|
|
1233
|
+
console.log(` ${marker} ${result.message}`);
|
|
1234
|
+
console.log(' This hook only checks for plan/review/verification evidence; it never prescribes which skill produces it.');
|
|
1844
1235
|
}
|
|
1236
|
+
console.log(`\n${pc.bold('RTK (recommended)')}`);
|
|
1237
|
+
const rtkResult = ensureRtkInstalled(!skipRtkInstall);
|
|
1238
|
+
const rtkMarker = rtkResult.ok ? pc.green('OK') : pc.yellow('WARN');
|
|
1239
|
+
console.log(` ${rtkMarker} ${rtkResult.message}`);
|
|
1845
1240
|
console.log('\nRestart your AI client after installing plugins or changing MCP configuration.');
|
|
1241
|
+
console.log('Your onboarding prompt is saved at .agent/docs/index-project-prompt.md. Run thachvd-kit prompt to print it again.');
|
|
1846
1242
|
}
|
|
1847
1243
|
|
|
1848
1244
|
function commandExists(command) {
|
|
1849
|
-
const checker = process.platform === 'win32' ? 'where' : 'command';
|
|
1850
|
-
const args = process.platform === 'win32' ? [command] : ['-v', command];
|
|
1851
|
-
const result = spawnSync(checker, args, { encoding: 'utf8', shell: process.platform !== 'win32' });
|
|
1852
|
-
return result.status === 0;
|
|
1853
|
-
}
|
|
1854
|
-
|
|
1245
|
+
const checker = process.platform === 'win32' ? 'where' : 'command';
|
|
1246
|
+
const args = process.platform === 'win32' ? [command] : ['-v', command];
|
|
1247
|
+
const result = spawnSync(checker, args, { encoding: 'utf8', shell: process.platform !== 'win32' });
|
|
1248
|
+
return result.status === 0;
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1855
1251
|
function ensureGlobalNpmPackage(binaryName, packageName, install = false) {
|
|
1856
1252
|
if (commandExists(binaryName)) {
|
|
1857
1253
|
return { ok: true, message: `${binaryName} already installed` };
|
|
@@ -1860,113 +1256,149 @@ function ensureGlobalNpmPackage(binaryName, packageName, install = false) {
|
|
|
1860
1256
|
return { ok: false, message: `${binaryName} is missing; run: npm install -g ${packageName}` };
|
|
1861
1257
|
}
|
|
1862
1258
|
if (!commandExists('npm')) {
|
|
1863
|
-
return { ok: false, message: `npm not found; run: npm install -g ${packageName}` };
|
|
1864
|
-
}
|
|
1865
|
-
const result = spawnSync('npm', ['install', '-g', packageName], {
|
|
1866
|
-
encoding: 'utf8',
|
|
1867
|
-
stdio: 'pipe'
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
}
|
|
1874
|
-
|
|
1259
|
+
return { ok: false, message: `npm not found; run: npm install -g ${packageName}` };
|
|
1260
|
+
}
|
|
1261
|
+
const result = spawnSync('npm', ['install', '-g', packageName], {
|
|
1262
|
+
encoding: 'utf8',
|
|
1263
|
+
stdio: 'pipe',
|
|
1264
|
+
shell: process.platform === 'win32'
|
|
1265
|
+
});
|
|
1266
|
+
if (result.status !== 0) {
|
|
1267
|
+
return { ok: false, message: `failed to install ${binaryName}; run: npm install -g ${packageName}` };
|
|
1268
|
+
}
|
|
1269
|
+
return { ok: true, message: `installed ${binaryName}` };
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1875
1272
|
function ensureCodebaseMemoryCli(install = false) {
|
|
1876
1273
|
return ensureGlobalNpmPackage(CODEBASE_MEMORY_BINARY, CODEBASE_MEMORY_PACKAGE, install);
|
|
1877
1274
|
}
|
|
1878
|
-
|
|
1879
|
-
function
|
|
1880
|
-
if (
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
return
|
|
1885
|
-
}
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
if (
|
|
1896
|
-
return { ok: false, message:
|
|
1897
|
-
}
|
|
1898
|
-
|
|
1275
|
+
|
|
1276
|
+
function ensureRtkInstalled(install = false) {
|
|
1277
|
+
if (commandExists('rtk')) {
|
|
1278
|
+
return { ok: true, message: 'rtk already installed' };
|
|
1279
|
+
}
|
|
1280
|
+
if (!install) {
|
|
1281
|
+
return { ok: false, message: `rtk is missing; run: ${RTK_INSTALL_COMMAND}` };
|
|
1282
|
+
}
|
|
1283
|
+
if (!commandExists('cargo')) {
|
|
1284
|
+
return { ok: false, message: `cargo not found; install Rust, then run: ${RTK_INSTALL_COMMAND}` };
|
|
1285
|
+
}
|
|
1286
|
+
console.log(pc.dim(' Installing rtk via cargo (compiles from source; this can take a few minutes)...'));
|
|
1287
|
+
const result = spawnSync('cargo', ['install', '--git', 'https://github.com/rtk-ai/rtk', '--branch', 'master', 'rtk'], {
|
|
1288
|
+
encoding: 'utf8',
|
|
1289
|
+
stdio: 'inherit',
|
|
1290
|
+
shell: process.platform === 'win32'
|
|
1291
|
+
});
|
|
1292
|
+
if (result.status !== 0) {
|
|
1293
|
+
return { ok: false, message: `failed to install rtk; run: ${RTK_INSTALL_COMMAND}` };
|
|
1294
|
+
}
|
|
1295
|
+
return { ok: true, message: 'installed rtk' };
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
function indexRepository() {
|
|
1299
|
+
if (!commandExists(CODEBASE_MEMORY_BINARY)) {
|
|
1300
|
+
return { ok: false, message: `${CODEBASE_MEMORY_BINARY} is not installed; skipped indexing` };
|
|
1301
|
+
}
|
|
1302
|
+
console.log(pc.dim(` Running ${CODEBASE_MEMORY_BINARY} cli index_repository --repo-path . (first index of a large repo can take a while)...`));
|
|
1303
|
+
const result = spawnSync(CODEBASE_MEMORY_BINARY, ['cli', 'index_repository', '--repo-path', '.'], {
|
|
1304
|
+
cwd: targetDir,
|
|
1305
|
+
encoding: 'utf8',
|
|
1306
|
+
stdio: 'inherit',
|
|
1307
|
+
shell: process.platform === 'win32'
|
|
1308
|
+
});
|
|
1309
|
+
if (result.status !== 0) {
|
|
1310
|
+
return { ok: false, message: `indexing failed; run manually: ${CODEBASE_MEMORY_BINARY} cli index_repository --repo-path .` };
|
|
1311
|
+
}
|
|
1312
|
+
return { ok: true, message: 'indexed repository with codebase-memory-mcp' };
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
function readJsonFile(filePath) {
|
|
1316
|
+
if (!fs.existsSync(filePath)) return {};
|
|
1317
|
+
try {
|
|
1318
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
1319
|
+
} catch {
|
|
1320
|
+
return null;
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
function writeJsonFile(filePath, data) {
|
|
1325
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
1326
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n', 'utf8');
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
function mergeMcpJson(filePath, withType = false) {
|
|
1330
|
+
const data = readJsonFile(filePath);
|
|
1331
|
+
if (data === null) {
|
|
1332
|
+
return { ok: false, message: `${filePath} is not valid JSON; skipped` };
|
|
1333
|
+
}
|
|
1334
|
+
data.mcpServers = data.mcpServers || {};
|
|
1899
1335
|
const baseServers = {
|
|
1900
1336
|
'codebase-memory-mcp': { command: CODEBASE_MEMORY_BINARY, args: [] },
|
|
1901
1337
|
playwright: { command: 'playwright-mcp' },
|
|
1902
1338
|
context7: { command: 'context7-mcp' }
|
|
1903
|
-
};
|
|
1904
|
-
if (data.permissions && Array.isArray(data.permissions.allow)) {
|
|
1905
|
-
const requiredPermissions = [
|
|
1906
|
-
"mcp__context7__resolve-library-id",
|
|
1907
|
-
"mcp__context7__query-docs"
|
|
1908
|
-
];
|
|
1909
|
-
for (const perm of requiredPermissions) {
|
|
1910
|
-
if (!data.permissions.allow.includes(perm)) {
|
|
1911
|
-
data.permissions.allow.push(perm);
|
|
1912
|
-
}
|
|
1913
|
-
}
|
|
1914
|
-
}
|
|
1915
|
-
for (const [name, server] of Object.entries(baseServers)) {
|
|
1916
|
-
if (!data.mcpServers[name]) {
|
|
1917
|
-
data.mcpServers[name] = withType ? { type: 'stdio', ...server } : server;
|
|
1918
|
-
}
|
|
1919
|
-
}
|
|
1920
|
-
writeJsonFile(filePath, data);
|
|
1921
|
-
const tools = Object.keys(baseServers).join(', ');
|
|
1922
|
-
return { ok: true, message: `configured ${tools} MCP in ${filePath}` };
|
|
1923
|
-
}
|
|
1924
|
-
|
|
1925
|
-
function appendMcpTomlIfMissing(filePath) {
|
|
1926
|
-
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
1927
|
-
const existing = fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf8') : '';
|
|
1928
|
-
const blocks = [];
|
|
1929
|
-
if (!/\[mcp_servers\.context7\]/.test(existing)) {
|
|
1930
|
-
blocks.push(`[mcp_servers.context7]
|
|
1931
|
-
command = "context7-mcp"
|
|
1932
|
-
startup_timeout_sec = 20
|
|
1933
|
-
tool_timeout_sec = 120
|
|
1934
|
-
`);
|
|
1935
|
-
}
|
|
1339
|
+
};
|
|
1340
|
+
if (data.permissions && Array.isArray(data.permissions.allow)) {
|
|
1341
|
+
const requiredPermissions = [
|
|
1342
|
+
"mcp__context7__resolve-library-id",
|
|
1343
|
+
"mcp__context7__query-docs"
|
|
1344
|
+
];
|
|
1345
|
+
for (const perm of requiredPermissions) {
|
|
1346
|
+
if (!data.permissions.allow.includes(perm)) {
|
|
1347
|
+
data.permissions.allow.push(perm);
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
for (const [name, server] of Object.entries(baseServers)) {
|
|
1352
|
+
if (!data.mcpServers[name]) {
|
|
1353
|
+
data.mcpServers[name] = withType ? { type: 'stdio', ...server } : server;
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
writeJsonFile(filePath, data);
|
|
1357
|
+
const tools = Object.keys(baseServers).join(', ');
|
|
1358
|
+
return { ok: true, message: `configured ${tools} MCP in ${filePath}` };
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
function appendMcpTomlIfMissing(filePath) {
|
|
1362
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
1363
|
+
const existing = fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf8') : '';
|
|
1364
|
+
const blocks = [];
|
|
1365
|
+
if (!/\[mcp_servers\.context7\]/.test(existing)) {
|
|
1366
|
+
blocks.push(`[mcp_servers.context7]
|
|
1367
|
+
command = "context7-mcp"
|
|
1368
|
+
startup_timeout_sec = 20
|
|
1369
|
+
tool_timeout_sec = 120
|
|
1370
|
+
`);
|
|
1371
|
+
}
|
|
1936
1372
|
if (!/\[mcp_servers\.codebase-memory-mcp\]/.test(existing)) {
|
|
1937
1373
|
blocks.push(`[mcp_servers.codebase-memory-mcp]
|
|
1938
1374
|
command = "${CODEBASE_MEMORY_BINARY}"
|
|
1939
1375
|
startup_timeout_sec = 20
|
|
1940
1376
|
tool_timeout_sec = 120
|
|
1941
1377
|
`);
|
|
1942
|
-
}
|
|
1943
|
-
if (!/\[mcp_servers\.playwright\]/.test(existing)) {
|
|
1944
|
-
blocks.push(`[mcp_servers.playwright]
|
|
1945
|
-
command = "playwright-mcp"
|
|
1946
|
-
startup_timeout_sec = 20
|
|
1947
|
-
tool_timeout_sec = 120
|
|
1948
|
-
`);
|
|
1949
|
-
}
|
|
1950
|
-
if (blocks.length === 0) { return { ok: true, message: `already configured ${filePath}` };
|
|
1951
|
-
}
|
|
1952
|
-
const separator = existing && !existing.endsWith('\n') ? '\n\n' : existing ? '\n' : '';
|
|
1953
|
-
fs.writeFileSync(filePath, existing + separator + blocks.join('\n'), 'utf8');
|
|
1378
|
+
}
|
|
1379
|
+
if (!/\[mcp_servers\.playwright\]/.test(existing)) {
|
|
1380
|
+
blocks.push(`[mcp_servers.playwright]
|
|
1381
|
+
command = "playwright-mcp"
|
|
1382
|
+
startup_timeout_sec = 20
|
|
1383
|
+
tool_timeout_sec = 120
|
|
1384
|
+
`);
|
|
1385
|
+
}
|
|
1386
|
+
if (blocks.length === 0) { return { ok: true, message: `already configured ${filePath}` };
|
|
1387
|
+
}
|
|
1388
|
+
const separator = existing && !existing.endsWith('\n') ? '\n\n' : existing ? '\n' : '';
|
|
1389
|
+
fs.writeFileSync(filePath, existing + separator + blocks.join('\n'), 'utf8');
|
|
1954
1390
|
return { ok: true, message: `configured codebase-memory-mcp, context7, playwright MCP in ${filePath}` };
|
|
1955
|
-
}
|
|
1956
|
-
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1957
1393
|
const DOD_HOOK_MARKER = 'feature/refactor Definition-of-Done gate';
|
|
1958
1394
|
|
|
1959
1395
|
function generateDodStopHookPrompt() {
|
|
1960
|
-
return `Input JSON (Stop hook payload): $ARGUMENTS\n\nCheck this project's ${DOD_HOOK_MARKER}
|
|
1396
|
+
return `Input JSON (Stop hook payload): $ARGUMENTS\n\nCheck this project's ${DOD_HOOK_MARKER}. This kit does not define its own workflow steps; it only verifies that evidence of the gate exists, regardless of which skill produced it (a Superpowers skill such as \`writing-plans\`/\`requesting-code-review\`, or an equivalent documented step). Use the \`last_assistant_message\` field as the authoritative final response. Use \`transcript_path\` only for additional context because the transcript may not contain the final response yet.\n\nFor a feature or refactor (multi-file change, new behavior, or non-trivial refactor), completion requires evidence of:\n- Plan: a plan was written and explicitly approved by the human before implementation started.\n- Review: the change was reviewed (correctness, simplicity, architecture, security, performance) and blocking issues were resolved.\n- Verification: tests or equivalent verification ran and passed.\n\nA one-file, unambiguous fix with no behavior/contract change is exempt from the Plan and Review evidence only when the final response states FAST_PATH: simple-fix, its reason, and its narrow scope. Verification is still mandatory in that case.\n\nSteps:\n1. If the input JSON has \`stop_hook_active: true\`, allow stopping immediately (already checked once this cycle; never block twice in a row).\n2. Inspect the changed files and the \`last_assistant_message\` field.\n3. If no source files changed, the final response does not claim the task is done/complete, or the final response explicitly contains FAST_PATH: simple-fix and the changed scope is clearly a simple/self-contained fix, allow stopping.\n4. If it is a feature/refactor-scale change and the final response claims completion, verify that the response (or the visible transcript) shows evidence of an approved plan and a completed review. If either is missing, block; a generic statement that the task was simple is not enough.\n5. Return exactly JSON: {"ok": true} to allow stopping, or {"ok": false, "reason": "..."} to block.\n\nWhen blocking, name exactly which gate is missing without prescribing which skill or workflow must produce it.`;
|
|
1961
1397
|
}
|
|
1962
1398
|
|
|
1963
1399
|
function isDefinitionOfDoneHook(hook) {
|
|
1964
1400
|
const prompt = hook && typeof hook.prompt === 'string' ? hook.prompt : '';
|
|
1965
|
-
return prompt.includes(DOD_HOOK_MARKER)
|
|
1966
|
-
prompt.includes('Input JSON (Stop hook payload)') &&
|
|
1967
|
-
prompt.includes('.agent/workflows/spec.md') &&
|
|
1968
|
-
prompt.includes('.agent/workflows/review.md')
|
|
1969
|
-
);
|
|
1401
|
+
return prompt.includes(DOD_HOOK_MARKER);
|
|
1970
1402
|
}
|
|
1971
1403
|
|
|
1972
1404
|
function mergeClaudeProjectHooks(filePath) {
|
|
@@ -2032,42 +1464,42 @@ function setupMcpServers(options = {}) {
|
|
|
2032
1464
|
results.push(ensureCodebaseMemoryCli(install));
|
|
2033
1465
|
results.push(ensureGlobalNpmPackage('context7-mcp', '@upstash/context7-mcp', install));
|
|
2034
1466
|
results.push(ensureGlobalNpmPackage('playwright-mcp', '@playwright/mcp', install));
|
|
2035
|
-
|
|
2036
|
-
const codexConfig = homePath('.codex', 'config.toml');
|
|
2037
|
-
if (codexConfig) results.push(appendMcpTomlIfMissing(codexConfig));
|
|
2038
|
-
|
|
2039
|
-
const geminiConfig = homePath('.gemini', 'config', 'mcp_config.json');
|
|
2040
|
-
if (geminiConfig) results.push(mergeMcpJson(geminiConfig, false));
|
|
2041
|
-
|
|
2042
|
-
const antigravityIdeConfig = homePath('.gemini', 'antigravity-ide', 'mcp_config.json');
|
|
2043
|
-
if (antigravityIdeConfig) results.push(mergeMcpJson(antigravityIdeConfig, false));
|
|
2044
|
-
|
|
2045
|
-
const antigravityConfig = homePath('.gemini', 'antigravity', 'mcp_config.json');
|
|
2046
|
-
if (antigravityConfig && fs.existsSync(path.dirname(antigravityConfig))) {
|
|
2047
|
-
results.push(mergeMcpJson(antigravityConfig, false));
|
|
2048
|
-
}
|
|
2049
|
-
|
|
2050
|
-
const claudeConfig = homePath('.claude.json');
|
|
2051
|
-
if (claudeConfig) results.push(mergeMcpJson(claudeConfig, true));
|
|
2052
|
-
|
|
2053
|
-
const claudeSettings = homePath('.claude', 'settings.json');
|
|
2054
|
-
if (claudeSettings) results.push(mergeMcpJson(claudeSettings, true));
|
|
2055
|
-
|
|
2056
|
-
return results;
|
|
2057
|
-
}
|
|
2058
|
-
|
|
2059
|
-
// --- Main CLI ---
|
|
2060
|
-
|
|
2061
|
-
async function main() {
|
|
2062
|
-
const rawArgs = process.argv.slice(2);
|
|
2063
|
-
const command = rawArgs[0] && !rawArgs[0].startsWith('-') ? rawArgs[0] : 'init';
|
|
2064
|
-
const args = command === 'init' ? rawArgs.slice(rawArgs[0] === 'init' ? 1 : 0) : rawArgs.slice(1);
|
|
2065
|
-
|
|
2066
|
-
if (rawArgs.includes('--version') || rawArgs.includes('-v')) {
|
|
2067
|
-
console.log(packageJson.version);
|
|
2068
|
-
return;
|
|
2069
|
-
}
|
|
2070
|
-
|
|
1467
|
+
|
|
1468
|
+
const codexConfig = homePath('.codex', 'config.toml');
|
|
1469
|
+
if (codexConfig) results.push(appendMcpTomlIfMissing(codexConfig));
|
|
1470
|
+
|
|
1471
|
+
const geminiConfig = homePath('.gemini', 'config', 'mcp_config.json');
|
|
1472
|
+
if (geminiConfig) results.push(mergeMcpJson(geminiConfig, false));
|
|
1473
|
+
|
|
1474
|
+
const antigravityIdeConfig = homePath('.gemini', 'antigravity-ide', 'mcp_config.json');
|
|
1475
|
+
if (antigravityIdeConfig) results.push(mergeMcpJson(antigravityIdeConfig, false));
|
|
1476
|
+
|
|
1477
|
+
const antigravityConfig = homePath('.gemini', 'antigravity', 'mcp_config.json');
|
|
1478
|
+
if (antigravityConfig && fs.existsSync(path.dirname(antigravityConfig))) {
|
|
1479
|
+
results.push(mergeMcpJson(antigravityConfig, false));
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
const claudeConfig = homePath('.claude.json');
|
|
1483
|
+
if (claudeConfig) results.push(mergeMcpJson(claudeConfig, true));
|
|
1484
|
+
|
|
1485
|
+
const claudeSettings = homePath('.claude', 'settings.json');
|
|
1486
|
+
if (claudeSettings) results.push(mergeMcpJson(claudeSettings, true));
|
|
1487
|
+
|
|
1488
|
+
return results;
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
// --- Main CLI ---
|
|
1492
|
+
|
|
1493
|
+
async function main() {
|
|
1494
|
+
const rawArgs = process.argv.slice(2);
|
|
1495
|
+
const command = rawArgs[0] && !rawArgs[0].startsWith('-') ? rawArgs[0] : 'init';
|
|
1496
|
+
const args = command === 'init' ? rawArgs.slice(rawArgs[0] === 'init' ? 1 : 0) : rawArgs.slice(1);
|
|
1497
|
+
|
|
1498
|
+
if (rawArgs.includes('--version') || rawArgs.includes('-v')) {
|
|
1499
|
+
console.log(packageJson.version);
|
|
1500
|
+
return;
|
|
1501
|
+
}
|
|
1502
|
+
|
|
2071
1503
|
if (rawArgs.includes('--help') || rawArgs.includes('-h')) {
|
|
2072
1504
|
printHelp();
|
|
2073
1505
|
return;
|
|
@@ -2078,140 +1510,142 @@ async function main() {
|
|
|
2078
1510
|
return;
|
|
2079
1511
|
}
|
|
2080
1512
|
|
|
1513
|
+
if (command === 'prompt') {
|
|
1514
|
+
printProjectPrompt();
|
|
1515
|
+
return;
|
|
1516
|
+
}
|
|
1517
|
+
|
|
2081
1518
|
if (command === 'setup') {
|
|
2082
1519
|
await runSetup(rawArgs.slice(1));
|
|
2083
1520
|
return;
|
|
2084
1521
|
}
|
|
2085
1522
|
|
|
2086
1523
|
if (command !== 'init') {
|
|
2087
|
-
console.error(pc.red(`Unknown command: ${command}`));
|
|
2088
|
-
printHelp();
|
|
2089
|
-
process.exit(1);
|
|
2090
|
-
}
|
|
2091
|
-
|
|
2092
|
-
const assumeYes = args.includes('--yes') || args.includes('-y');
|
|
2093
|
-
const skipMcpSetup = args.includes('--no-setup-mcp') || process.env.THACHVD_KIT_SKIP_MCP_SETUP === '1';
|
|
2094
|
-
|
|
2095
|
-
console.log(pc.bold(pc.cyan('='.repeat(50))));
|
|
2096
|
-
console.log(' ' + pc.bold('thachvd-kit') + ' - AI Project Bootstrap');
|
|
2097
|
-
console.log(' Generates shared agent rules and configures MCP tooling');
|
|
2098
|
-
console.log(pc.bold(pc.cyan('='.repeat(50))));
|
|
2099
|
-
|
|
2100
|
-
console.log(pc.dim(' Scanning current project files for a baseline...\n'));
|
|
2101
|
-
const scanned = scanProject();
|
|
2102
|
-
|
|
2103
|
-
console.log(` ${pc.bold('Detected context:')}`);
|
|
2104
|
-
formatScanPreviewLines(scanned).forEach(([label, value]) => {
|
|
2105
|
-
console.log(` - ${label}: ${value}`);
|
|
2106
|
-
});
|
|
2107
|
-
console.log('');
|
|
2108
|
-
if ((scanned.scan_evidence || []).length > 0) {
|
|
2109
|
-
console.log(` ${pc.bold('Evidence:')}`);
|
|
2110
|
-
scanned.scan_evidence.forEach(line => console.log(` - ${line}`));
|
|
2111
|
-
console.log('');
|
|
2112
|
-
}
|
|
2113
|
-
|
|
2114
|
-
// Combine data
|
|
2115
|
-
const data = {
|
|
2116
|
-
project_name: scanned?.project_name || path.basename(targetDir),
|
|
2117
|
-
description: scanned?.description || 'A software project',
|
|
2118
|
-
project_type: scanned?.project_type || 'web-app',
|
|
2119
|
-
primary_language: scanned?.primary_language || 'typescript',
|
|
2120
|
-
frameworks: scanned?.frameworks || [],
|
|
2121
|
-
database: scanned?.database || 'none',
|
|
2122
|
-
uses_docker: scanned?.uses_docker ?? true,
|
|
2123
|
-
cloud: scanned?.cloud || 'none',
|
|
2124
|
-
package_manager: scanned?.package_manager || 'auto-detect',
|
|
2125
|
-
test_framework: scanned?.test_framework || 'auto-detect',
|
|
2126
|
-
max_file_lines: scanned?.max_file_lines || '300'
|
|
2127
|
-
};
|
|
2128
|
-
|
|
2129
|
-
data.database = data.database || scanned?.database || 'none';
|
|
2130
|
-
data.uses_docker = typeof data.uses_docker === 'boolean' ? data.uses_docker : (scanned?.uses_docker ?? true);
|
|
2131
|
-
data.cloud = data.cloud || scanned?.cloud || 'none';
|
|
2132
|
-
data.package_manager = data.package_manager || scanned?.package_manager || 'auto-detect';
|
|
2133
|
-
data.test_framework = data.test_framework || scanned?.test_framework || 'auto-detect';
|
|
2134
|
-
data.max_file_lines = data.max_file_lines || scanned?.max_file_lines || '300';
|
|
2135
|
-
data.framework_details = scanned?.framework_details || [];
|
|
2136
|
-
data.scan_evidence = scanned?.scan_evidence || [];
|
|
2137
|
-
data.app_root = scanned?.app_root || '.';
|
|
2138
|
-
|
|
2139
|
-
data.infrastructure = [];
|
|
2140
|
-
if (data.uses_docker) data.infrastructure.push('docker');
|
|
2141
|
-
if (data.cloud !== 'none') data.infrastructure.push(data.cloud);
|
|
2142
|
-
|
|
1524
|
+
console.error(pc.red(`Unknown command: ${command}`));
|
|
1525
|
+
printHelp();
|
|
1526
|
+
process.exit(1);
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
const assumeYes = args.includes('--yes') || args.includes('-y');
|
|
1530
|
+
const skipMcpSetup = args.includes('--no-setup-mcp') || process.env.THACHVD_KIT_SKIP_MCP_SETUP === '1';
|
|
1531
|
+
|
|
1532
|
+
console.log(pc.bold(pc.cyan('='.repeat(50))));
|
|
1533
|
+
console.log(' ' + pc.bold('thachvd-kit') + ' - AI Project Bootstrap');
|
|
1534
|
+
console.log(' Generates shared agent rules and configures MCP tooling');
|
|
1535
|
+
console.log(pc.bold(pc.cyan('='.repeat(50))));
|
|
1536
|
+
|
|
1537
|
+
console.log(pc.dim(' Scanning current project files for a baseline...\n'));
|
|
1538
|
+
const scanned = scanProject();
|
|
1539
|
+
|
|
1540
|
+
console.log(` ${pc.bold('Detected context:')}`);
|
|
1541
|
+
formatScanPreviewLines(scanned).forEach(([label, value]) => {
|
|
1542
|
+
console.log(` - ${label}: ${value}`);
|
|
1543
|
+
});
|
|
1544
|
+
console.log('');
|
|
1545
|
+
if ((scanned.scan_evidence || []).length > 0) {
|
|
1546
|
+
console.log(` ${pc.bold('Evidence:')}`);
|
|
1547
|
+
scanned.scan_evidence.forEach(line => console.log(` - ${line}`));
|
|
1548
|
+
console.log('');
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
// Combine data
|
|
1552
|
+
const data = {
|
|
1553
|
+
project_name: scanned?.project_name || path.basename(targetDir),
|
|
1554
|
+
description: scanned?.description || 'A software project',
|
|
1555
|
+
project_type: scanned?.project_type || 'web-app',
|
|
1556
|
+
primary_language: scanned?.primary_language || 'typescript',
|
|
1557
|
+
frameworks: scanned?.frameworks || [],
|
|
1558
|
+
database: scanned?.database || 'none',
|
|
1559
|
+
uses_docker: scanned?.uses_docker ?? true,
|
|
1560
|
+
cloud: scanned?.cloud || 'none',
|
|
1561
|
+
package_manager: scanned?.package_manager || 'auto-detect',
|
|
1562
|
+
test_framework: scanned?.test_framework || 'auto-detect',
|
|
1563
|
+
max_file_lines: scanned?.max_file_lines || '300'
|
|
1564
|
+
};
|
|
1565
|
+
|
|
1566
|
+
data.database = data.database || scanned?.database || 'none';
|
|
1567
|
+
data.uses_docker = typeof data.uses_docker === 'boolean' ? data.uses_docker : (scanned?.uses_docker ?? true);
|
|
1568
|
+
data.cloud = data.cloud || scanned?.cloud || 'none';
|
|
1569
|
+
data.package_manager = data.package_manager || scanned?.package_manager || 'auto-detect';
|
|
1570
|
+
data.test_framework = data.test_framework || scanned?.test_framework || 'auto-detect';
|
|
1571
|
+
data.max_file_lines = data.max_file_lines || scanned?.max_file_lines || '300';
|
|
1572
|
+
data.framework_details = scanned?.framework_details || [];
|
|
1573
|
+
data.scan_evidence = scanned?.scan_evidence || [];
|
|
1574
|
+
data.app_root = scanned?.app_root || '.';
|
|
1575
|
+
|
|
1576
|
+
data.infrastructure = [];
|
|
1577
|
+
if (data.uses_docker) data.infrastructure.push('docker');
|
|
1578
|
+
if (data.cloud !== 'none') data.infrastructure.push(data.cloud);
|
|
1579
|
+
|
|
2143
1580
|
const shouldSetupMcp = args.includes('--setup-mcp') && !skipMcpSetup;
|
|
2144
|
-
|
|
2145
|
-
console.log(`\n${pc.bold(pc.cyan('Generating Files'))}`);
|
|
2146
|
-
|
|
2147
|
-
// Shared entry files are written after the .agent folder is available.
|
|
2148
|
-
|
|
1581
|
+
|
|
1582
|
+
console.log(`\n${pc.bold(pc.cyan('Generating Files'))}`);
|
|
1583
|
+
|
|
1584
|
+
// Shared entry files are written after the .agent folder is available.
|
|
1585
|
+
|
|
2149
1586
|
console.log(` ${pc.green('OK')} Project context generation enabled; workflow skills remain client-managed by Superpowers`);
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
const generatedFiles = [
|
|
2153
|
-
['AGENTS.md', generateSharedAgentsMd(data)],
|
|
2154
|
-
['CLAUDE.md', generateSharedClaudeMd()],
|
|
2155
|
-
['GEMINI.md', generateSharedGeminiMd()],
|
|
2156
|
-
['.cursorrules', generateSharedCursorrules(data)],
|
|
2157
|
-
[path.join('.agent', 'docs', 'project.md'), generateProjectDoc(data)],
|
|
2158
|
-
[path.join('.agent', 'docs', 'architecture.md'), generateArchitectureDoc(data)],
|
|
2159
|
-
[path.join('.agent', 'docs', 'conventions.md'), generateConventionsDoc(data)],
|
|
2160
|
-
[path.join('.agent', 'docs', 'workflow.md'), generateWorkflowDoc()],
|
|
1587
|
+
|
|
1588
|
+
|
|
1589
|
+
const generatedFiles = [
|
|
1590
|
+
['AGENTS.md', generateSharedAgentsMd(data)],
|
|
1591
|
+
['CLAUDE.md', generateSharedClaudeMd()],
|
|
1592
|
+
['GEMINI.md', generateSharedGeminiMd()],
|
|
1593
|
+
['.cursorrules', generateSharedCursorrules(data)],
|
|
1594
|
+
[path.join('.agent', 'docs', 'project.md'), generateProjectDoc(data)],
|
|
1595
|
+
[path.join('.agent', 'docs', 'architecture.md'), generateArchitectureDoc(data)],
|
|
1596
|
+
[path.join('.agent', 'docs', 'conventions.md'), generateConventionsDoc(data)],
|
|
1597
|
+
[path.join('.agent', 'docs', 'workflow.md'), generateWorkflowDoc()],
|
|
2161
1598
|
[path.join('.agent', 'docs', 'tooling.md'), generateToolingDoc(data)],
|
|
2162
|
-
[path.join('.agent', 'docs', 'getting-started.md'), generateGettingStartedDoc()]
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
1599
|
+
[path.join('.agent', 'docs', 'getting-started.md'), generateGettingStartedDoc()],
|
|
1600
|
+
[path.join('.agent', 'docs', 'index-project-prompt.md'), generateIndexProjectPrompt()]
|
|
1601
|
+
];
|
|
1602
|
+
|
|
1603
|
+
for (const [relativePath, content] of generatedFiles) {
|
|
1604
|
+
const wrote = await writeGeneratedFile(relativePath, content, assumeYes);
|
|
1605
|
+
if (wrote) console.log(` ${pc.green('OK')} Generated ${pc.bold(relativePath)}`);
|
|
1606
|
+
else console.log(` ${pc.yellow('~')} Skipped ${pc.bold(relativePath)}`);
|
|
1607
|
+
}
|
|
1608
|
+
|
|
2171
1609
|
if (shouldSetupMcp) {
|
|
2172
|
-
console.log(`\n${pc.bold(pc.cyan('Configuring MCP'))}`);
|
|
2173
|
-
for (const result of setupMcpServers()) {
|
|
2174
|
-
const marker = result.ok ? pc.green('OK') : pc.yellow('~');
|
|
2175
|
-
console.log(` ${marker} ${result.message}`);
|
|
2176
|
-
}
|
|
2177
|
-
}
|
|
2178
|
-
|
|
2179
|
-
console.log(`\n${pc.bold(pc.green('Done!'))}
|
|
2180
|
-
|
|
2181
|
-
${pc.bold('Project:')} ${data.project_name}
|
|
2182
|
-
${pc.bold('Stack:')} ${data.primary_language} | ${(data.frameworks || []).join(', ') || 'no framework'}
|
|
2183
|
-
|
|
2184
|
-
${pc.bold('Generated:')}
|
|
2185
|
-
- ${pc.bold('AGENTS.md')} shared entry for Codex, Antigravity, Claude Code, and Cursor
|
|
2186
|
-
- ${pc.bold('CLAUDE.md')} Claude Code entry that imports AGENTS.md
|
|
2187
|
-
- ${pc.bold('GEMINI.md')} Antigravity entry
|
|
2188
|
-
- ${pc.bold('.cursorrules')} Cursor entry
|
|
2189
|
-
- ${pc.bold('.agent/docs/')} scan-based project rules
|
|
2190
|
-
- ${pc.bold('.agent/docs/')}
|
|
2191
|
-
|
|
2192
|
-
${pc.bold('MCP/tooling setup:')}
|
|
1610
|
+
console.log(`\n${pc.bold(pc.cyan('Configuring MCP'))}`);
|
|
1611
|
+
for (const result of setupMcpServers()) {
|
|
1612
|
+
const marker = result.ok ? pc.green('OK') : pc.yellow('~');
|
|
1613
|
+
console.log(` ${marker} ${result.message}`);
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
console.log(`\n${pc.bold(pc.green('Done!'))}
|
|
1618
|
+
|
|
1619
|
+
${pc.bold('Project:')} ${data.project_name}
|
|
1620
|
+
${pc.bold('Stack:')} ${data.primary_language} | ${(data.frameworks || []).join(', ') || 'no framework'}
|
|
1621
|
+
|
|
1622
|
+
${pc.bold('Generated:')}
|
|
1623
|
+
- ${pc.bold('AGENTS.md')} shared entry for Codex, Antigravity, Claude Code, and Cursor
|
|
1624
|
+
- ${pc.bold('CLAUDE.md')} Claude Code entry that imports AGENTS.md
|
|
1625
|
+
- ${pc.bold('GEMINI.md')} Antigravity entry
|
|
1626
|
+
- ${pc.bold('.cursorrules')} Cursor entry
|
|
1627
|
+
- ${pc.bold('.agent/docs/')} scan-based project rules and local operating notes
|
|
1628
|
+
- ${pc.bold('.agent/docs/index-project-prompt.md')} durable onboarding prompt
|
|
1629
|
+
|
|
1630
|
+
${pc.bold('MCP/tooling setup:')}
|
|
2193
1631
|
- Superpowers is the workflow backend; install it from the official client/plugin surface
|
|
2194
|
-
- Run ${pc.bold('thachvd-kit setup')} to configure codebase-memory MCP and
|
|
2195
|
-
|
|
2196
|
-
${pc.bold('Next steps:')}
|
|
2197
|
-
1.
|
|
2198
|
-
2.
|
|
1632
|
+
- Run ${pc.bold('thachvd-kit setup')} to configure codebase-memory MCP and recommended RTK
|
|
1633
|
+
|
|
1634
|
+
${pc.bold('Next steps:')}
|
|
1635
|
+
1. Run ${pc.bold('thachvd-kit prompt')} and paste the saved prompt into your AI editor
|
|
1636
|
+
2. Install Superpowers and RTK, then run ${pc.bold('thachvd-kit setup')}
|
|
2199
1637
|
3. Run ${pc.bold('thachvd-kit doctor')} to verify the machine
|
|
2200
|
-
4.
|
|
2201
|
-
|
|
2202
|
-
${pc.bold('
|
|
2203
|
-
${pc.dim('---')}
|
|
2204
|
-
|
|
2205
|
-
Use Superpowers for the workflow and use codebase-memory MCP for structural code discovery when available.
|
|
2206
|
-
Ask me what this project does and any important conventions I want preserved.
|
|
2207
|
-
Then scan this repository and refine the generated context docs with factual rules.
|
|
2208
|
-
Do not implement product code during onboarding.
|
|
1638
|
+
4. Re-run ${pc.bold('thachvd-kit prompt')} whenever you need the onboarding prompt again
|
|
1639
|
+
|
|
1640
|
+
${pc.bold('Onboarding prompt saved to .agent/docs/index-project-prompt.md:')}
|
|
1641
|
+
${pc.dim('---')}
|
|
1642
|
+
${generateIndexProjectPrompt().trim()}
|
|
2209
1643
|
${pc.dim('---')}
|
|
2210
1644
|
`);
|
|
2211
1645
|
}
|
|
2212
|
-
|
|
2213
|
-
main().catch(err => {
|
|
2214
|
-
console.error(pc.red('\nError during init:'), err);
|
|
2215
|
-
process.exit(1);
|
|
2216
|
-
});
|
|
2217
|
-
|
|
1646
|
+
|
|
1647
|
+
main().catch(err => {
|
|
1648
|
+
console.error(pc.red('\nError during init:'), err);
|
|
1649
|
+
process.exit(1);
|
|
1650
|
+
});
|
|
1651
|
+
|