thachvd-kit 1.0.8 → 1.0.10
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/README.md +100 -71
- package/bin/cli.js +144 -42
- package/package.json +3 -3
- package/scripts/init.py +150 -79
package/README.md
CHANGED
|
@@ -1,82 +1,111 @@
|
|
|
1
|
-
# thachvd-kit
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
Simply run the wizard:
|
|
1
|
+
# thachvd-kit
|
|
2
|
+
|
|
3
|
+
`thachvd-kit` bootstraps project context for AI coding agents such as Antigravity, Claude Code, and Codex.
|
|
4
|
+
|
|
5
|
+
It installs a local `.agent/` kit, generates `PROJECT_CONTEXT.md`, and can scan an existing codebase to infer stack, framework, database, and infra signals before your agent starts working.
|
|
6
|
+
|
|
7
|
+
## Quick Start
|
|
8
|
+
|
|
9
|
+
Install globally:
|
|
11
10
|
|
|
12
11
|
```bash
|
|
13
|
-
|
|
14
|
-
# or
|
|
15
|
-
npx github:thachvd/thachvd-kit init
|
|
12
|
+
npm install -g thachvd-kit
|
|
16
13
|
```
|
|
17
14
|
|
|
18
|
-
|
|
19
|
-
If it is an existing project, it can scan the current codebase to prefill `PROJECT_CONTEXT.md`, or let you enter everything manually.
|
|
20
|
-
The scan mode uses evidence-based detectors and shows why a framework was inferred, so you can quickly confirm or correct it.
|
|
21
|
-
After init, you can also generate an AI refinement prompt to let your editor/agent deepen the project context.
|
|
15
|
+
Then run inside any project folder:
|
|
22
16
|
|
|
23
17
|
```bash
|
|
24
|
-
thachvd-kit init
|
|
18
|
+
thachvd-kit init
|
|
25
19
|
```
|
|
26
20
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
The wizard will then ask you a few questions about your project (frontend, backend, database, infrastructure, etc.).
|
|
30
|
-
Based on your answers, it will:
|
|
31
|
-
1. **Copy** the highly optimized agents, skills, and rules to a `.agent` folder inside your project.
|
|
32
|
-
2. **Generate** a smart `PROJECT_CONTEXT.md` file tailored specifically to your chosen stack.
|
|
33
|
-
3. **Generate** a `CLAUDE.md` to ensure maximum cross-platform compatibility.
|
|
34
|
-
|
|
35
|
-
### Global Installation (Like `agkit-init`)
|
|
36
|
-
|
|
37
|
-
If you want to install it globally on your machine:
|
|
38
|
-
|
|
39
|
-
```bash
|
|
40
|
-
npm install -g github:thachvd/thachvd-kit
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
Then, whenever you start a new project, just type:
|
|
21
|
+
You can also skip directly to scan mode:
|
|
44
22
|
|
|
45
23
|
```bash
|
|
46
|
-
thachvd-kit init
|
|
24
|
+
thachvd-kit init --scan
|
|
47
25
|
```
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
-
|
|
60
|
-
-
|
|
61
|
-
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
.
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
26
|
+
|
|
27
|
+
## What `init` Does
|
|
28
|
+
|
|
29
|
+
`thachvd-kit init` now supports both new and existing projects:
|
|
30
|
+
|
|
31
|
+
- New project: enter context manually.
|
|
32
|
+
- Existing project: choose scan mode or manual mode.
|
|
33
|
+
- Scan mode: detect stack from the codebase, show evidence, then either accept detected values or review/edit them.
|
|
34
|
+
|
|
35
|
+
Generated files:
|
|
36
|
+
|
|
37
|
+
- `.agent/agents`
|
|
38
|
+
- `.agent/rules`
|
|
39
|
+
- `.agent/skills`
|
|
40
|
+
- `.agent/workflows`
|
|
41
|
+
- `PROJECT_CONTEXT.md`
|
|
42
|
+
- `CLAUDE.md`
|
|
43
|
+
- `AI_CONTEXT_REFINEMENT.md` when first-session AI refinement is enabled
|
|
44
|
+
|
|
45
|
+
## Scan Behavior
|
|
46
|
+
|
|
47
|
+
The scanner is hybrid and practical:
|
|
48
|
+
|
|
49
|
+
- App stack detection uses an inferred `app_root` so nested apps like `src/` can still resolve Laravel, CakePHP, Next.js, and similar stacks correctly.
|
|
50
|
+
- Infra detection stays repo-wide, so folders like `dev/`, `stg/`, `prod/` can still contribute Docker and CI evidence.
|
|
51
|
+
- Noise folders such as `vendor`, `node_modules`, `.git`, `dist`, and `build` are ignored during recursive scan.
|
|
52
|
+
|
|
53
|
+
The scan preview surfaces:
|
|
54
|
+
|
|
55
|
+
- app root
|
|
56
|
+
- project type
|
|
57
|
+
- primary language
|
|
58
|
+
- frameworks
|
|
59
|
+
- database
|
|
60
|
+
- package manager
|
|
61
|
+
- test framework
|
|
62
|
+
- cloud
|
|
63
|
+
- infrastructure
|
|
64
|
+
- CI/CD
|
|
65
|
+
- evidence for each inferred framework
|
|
66
|
+
|
|
67
|
+
## AI Refinement Flow
|
|
68
|
+
|
|
69
|
+
`init` creates a baseline `PROJECT_CONTEXT.md`.
|
|
70
|
+
|
|
71
|
+
If you enable first-session AI refinement, the generated context is marked with:
|
|
72
|
+
|
|
73
|
+
- `context.status: baseline`
|
|
74
|
+
- `context.needs_ai_refinement: true`
|
|
75
|
+
|
|
76
|
+
Then, when your agent opens the project, `GEMINI.md` / `CLAUDE.md` instruct it to:
|
|
77
|
+
|
|
78
|
+
1. Read `PROJECT_CONTEXT.md`
|
|
79
|
+
2. Refine the context once if `needs_ai_refinement` is `true`
|
|
80
|
+
3. Update the file to `context.status: refined`
|
|
81
|
+
4. Set `context.needs_ai_refinement: false`
|
|
82
|
+
|
|
83
|
+
This keeps `init` simple while still allowing a deeper AI pass without requiring API-key integration inside the CLI.
|
|
84
|
+
|
|
85
|
+
## Skill Resolution
|
|
86
|
+
|
|
87
|
+
The kit auto-loads generic and framework-specific skills based on detected context.
|
|
88
|
+
|
|
89
|
+
Examples:
|
|
90
|
+
|
|
91
|
+
- React -> `react-frontend`, `frontend-design`
|
|
92
|
+
- Next.js -> `nextjs-react-expert`, `frontend-design`
|
|
93
|
+
- Laravel -> `laravel-patterns`, `laravel-security`, `laravel-tdd`
|
|
94
|
+
- Tauri -> `rust-pro`, `desktop-design`
|
|
95
|
+
|
|
96
|
+
Generic quality skills such as `clean-code` and `systematic-debugging` are always included.
|
|
97
|
+
|
|
98
|
+
## Directory Layout
|
|
99
|
+
|
|
100
|
+
```text
|
|
101
|
+
.agent/
|
|
102
|
+
├── agents/
|
|
103
|
+
├── rules/
|
|
104
|
+
├── skills/
|
|
105
|
+
└── workflows/
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Notes
|
|
109
|
+
|
|
110
|
+
- `thachvd-kit` is compatible with Codex workflows through `PROJECT_CONTEXT.md` and `CLAUDE.md`; it does not directly control Codex sessions.
|
|
111
|
+
- The npm package page updates only after a new `npm publish`, not just after pushing to GitHub.
|
package/bin/cli.js
CHANGED
|
@@ -107,6 +107,7 @@ const CLOUD_CHOICES = ['aws', 'gcp', 'azure', 'vercel', 'fly', 'railway', 'none'
|
|
|
107
107
|
const FRONTEND_FRAMEWORKS = ['vue', 'react', 'next', 'nuxt', 'svelte', 'angular'];
|
|
108
108
|
const BACKEND_FRAMEWORKS = ['laravel', 'cakephp', 'symfony', 'codeigniter', 'express', 'fastapi', 'django', 'flask', 'gin', 'fiber', 'echo', 'nestjs', 'rails', 'spring', 'aspnet', 'axum', 'actix'];
|
|
109
109
|
const MOBILE_DESKTOP_FRAMEWORKS = ['tauri', 'flutter', 'react-native'];
|
|
110
|
+
const IGNORED_SCAN_DIRS = new Set(['.git', 'node_modules', 'vendor', 'dist', 'build', 'coverage', '.next', '.nuxt', '.turbo', '.cache', 'storage', 'tmp']);
|
|
110
111
|
|
|
111
112
|
function printHelp() {
|
|
112
113
|
console.log(`
|
|
@@ -128,6 +129,7 @@ ${pc.bold('Options:')}
|
|
|
128
129
|
|
|
129
130
|
function formatScanPreviewLines(scanned) {
|
|
130
131
|
return [
|
|
132
|
+
['App root', scanned.app_root || '.'],
|
|
131
133
|
['Project type', scanned.project_type || 'unknown'],
|
|
132
134
|
['Primary language', scanned.primary_language || 'unknown'],
|
|
133
135
|
['Frameworks', (scanned.frameworks || []).join(', ') || 'no framework'],
|
|
@@ -153,6 +155,10 @@ function fileExists(...parts) {
|
|
|
153
155
|
return fs.existsSync(path.join(targetDir, ...parts));
|
|
154
156
|
}
|
|
155
157
|
|
|
158
|
+
function fileExistsAt(baseDir, ...parts) {
|
|
159
|
+
return fs.existsSync(path.join(baseDir, ...parts));
|
|
160
|
+
}
|
|
161
|
+
|
|
156
162
|
function readJsonIfExists(...parts) {
|
|
157
163
|
const fullPath = path.join(targetDir, ...parts);
|
|
158
164
|
if (!fs.existsSync(fullPath)) return null;
|
|
@@ -163,6 +169,16 @@ function readJsonIfExists(...parts) {
|
|
|
163
169
|
}
|
|
164
170
|
}
|
|
165
171
|
|
|
172
|
+
function readJsonIfExistsAt(baseDir, ...parts) {
|
|
173
|
+
const fullPath = path.join(baseDir, ...parts);
|
|
174
|
+
if (!fs.existsSync(fullPath)) return null;
|
|
175
|
+
try {
|
|
176
|
+
return JSON.parse(fs.readFileSync(fullPath, 'utf8'));
|
|
177
|
+
} catch {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
166
182
|
function readTextIfExists(...parts) {
|
|
167
183
|
const fullPath = path.join(targetDir, ...parts);
|
|
168
184
|
if (!fs.existsSync(fullPath)) return '';
|
|
@@ -173,17 +189,27 @@ function readTextIfExists(...parts) {
|
|
|
173
189
|
}
|
|
174
190
|
}
|
|
175
191
|
|
|
176
|
-
function
|
|
177
|
-
|
|
178
|
-
if (
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
192
|
+
function readTextIfExistsAt(baseDir, ...parts) {
|
|
193
|
+
const fullPath = path.join(baseDir, ...parts);
|
|
194
|
+
if (!fs.existsSync(fullPath)) return '';
|
|
195
|
+
try {
|
|
196
|
+
return fs.readFileSync(fullPath, 'utf8');
|
|
197
|
+
} catch {
|
|
198
|
+
return '';
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function detectPackageManager(baseDir = targetDir) {
|
|
203
|
+
if (fileExistsAt(baseDir, 'pnpm-lock.yaml')) return 'pnpm';
|
|
204
|
+
if (fileExistsAt(baseDir, 'yarn.lock')) return 'yarn';
|
|
205
|
+
if (fileExistsAt(baseDir, 'bun.lockb') || fileExistsAt(baseDir, 'bun.lock')) return 'bun';
|
|
206
|
+
if (fileExistsAt(baseDir, 'package-lock.json')) return 'npm';
|
|
207
|
+
if (fileExistsAt(baseDir, 'uv.lock')) return 'uv';
|
|
208
|
+
if (fileExistsAt(baseDir, 'poetry.lock')) return 'pip';
|
|
209
|
+
if (fileExistsAt(baseDir, 'requirements.txt') || fileExistsAt(baseDir, 'pyproject.toml')) return 'pip';
|
|
210
|
+
if (fileExistsAt(baseDir, 'go.mod')) return 'go mod';
|
|
211
|
+
if (fileExistsAt(baseDir, 'Cargo.toml')) return 'cargo';
|
|
212
|
+
if (fileExistsAt(baseDir, 'composer.json')) return 'composer';
|
|
187
213
|
return 'auto-detect';
|
|
188
214
|
}
|
|
189
215
|
|
|
@@ -191,6 +217,76 @@ function uniq(values) {
|
|
|
191
217
|
return Array.from(new Set(values.filter(Boolean)));
|
|
192
218
|
}
|
|
193
219
|
|
|
220
|
+
function detectAppRoot(baseDir) {
|
|
221
|
+
const candidates = [baseDir];
|
|
222
|
+
const queue = [{ dir: baseDir, depth: 0 }];
|
|
223
|
+
|
|
224
|
+
while (queue.length > 0) {
|
|
225
|
+
const { dir, depth } = queue.shift();
|
|
226
|
+
if (depth >= 2) continue;
|
|
227
|
+
let entries = [];
|
|
228
|
+
try {
|
|
229
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
230
|
+
} catch {
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
for (const entry of entries) {
|
|
234
|
+
if (!entry.isDirectory()) continue;
|
|
235
|
+
if (IGNORED_SCAN_DIRS.has(entry.name)) continue;
|
|
236
|
+
const nextDir = path.join(dir, entry.name);
|
|
237
|
+
candidates.push(nextDir);
|
|
238
|
+
queue.push({ dir: nextDir, depth: depth + 1 });
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const scoreCandidate = dir => {
|
|
243
|
+
let score = 0;
|
|
244
|
+
if (fileExistsAt(dir, 'composer.json')) score += 5;
|
|
245
|
+
if (fileExistsAt(dir, 'package.json')) score += 4;
|
|
246
|
+
if (fileExistsAt(dir, 'Cargo.toml')) score += 4;
|
|
247
|
+
if (fileExistsAt(dir, 'go.mod')) score += 4;
|
|
248
|
+
if (fileExistsAt(dir, 'pyproject.toml') || fileExistsAt(dir, 'requirements.txt')) score += 4;
|
|
249
|
+
if (fileExistsAt(dir, 'artisan')) score += 5;
|
|
250
|
+
if (fileExistsAt(dir, 'bin', 'cake') || fileExistsAt(dir, 'app', 'Config', 'core.php')) score += 5;
|
|
251
|
+
if (fileExistsAt(dir, 'src-tauri', 'tauri.conf.json') || fileExistsAt(dir, 'src-tauri', 'tauri.conf.json5')) score += 5;
|
|
252
|
+
if (fileExistsAt(dir, 'manage.py')) score += 4;
|
|
253
|
+
if (fileExistsAt(dir, 'Gemfile')) score += 4;
|
|
254
|
+
if (fileExistsAt(dir, 'pom.xml') || fileExistsAt(dir, 'build.gradle') || fileExistsAt(dir, 'build.gradle.kts')) score += 4;
|
|
255
|
+
if (fileExistsAt(dir, 'app', 'View') || fileExistsAt(dir, 'resources', 'views') || fileExistsAt(dir, 'templates') || fileExistsAt(dir, 'app', 'views')) score += 2;
|
|
256
|
+
return score;
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
const ranked = candidates
|
|
260
|
+
.map(dir => ({ dir, score: scoreCandidate(dir) }))
|
|
261
|
+
.filter(item => item.score > 0)
|
|
262
|
+
.sort((a, b) => (b.score - a.score) || (a.dir.length - b.dir.length));
|
|
263
|
+
|
|
264
|
+
return ranked[0]?.dir || baseDir;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function repoHasAny(baseDir, relativePaths, maxDepth = 3) {
|
|
268
|
+
const queue = [{ dir: baseDir, depth: 0 }];
|
|
269
|
+
while (queue.length > 0) {
|
|
270
|
+
const { dir, depth } = queue.shift();
|
|
271
|
+
for (const relativePath of relativePaths) {
|
|
272
|
+
if (fileExistsAt(dir, ...relativePath)) return true;
|
|
273
|
+
}
|
|
274
|
+
if (depth >= maxDepth) continue;
|
|
275
|
+
let entries = [];
|
|
276
|
+
try {
|
|
277
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
278
|
+
} catch {
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
for (const entry of entries) {
|
|
282
|
+
if (!entry.isDirectory()) continue;
|
|
283
|
+
if (IGNORED_SCAN_DIRS.has(entry.name)) continue;
|
|
284
|
+
queue.push({ dir: path.join(dir, entry.name), depth: depth + 1 });
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
289
|
+
|
|
194
290
|
function evidenceIf(condition, label, weight = 1) {
|
|
195
291
|
return condition ? { label, weight } : null;
|
|
196
292
|
}
|
|
@@ -552,9 +648,10 @@ const FRAMEWORK_DETECTORS = [
|
|
|
552
648
|
];
|
|
553
649
|
|
|
554
650
|
function scanProject() {
|
|
555
|
-
const
|
|
556
|
-
const
|
|
557
|
-
const
|
|
651
|
+
const appRoot = detectAppRoot(targetDir);
|
|
652
|
+
const pkg = readJsonIfExistsAt(appRoot, 'package.json') || {};
|
|
653
|
+
const composer = readJsonIfExistsAt(appRoot, 'composer.json') || {};
|
|
654
|
+
const rootEntries = fs.existsSync(appRoot) ? fs.readdirSync(appRoot) : [];
|
|
558
655
|
const rootCsproj = rootEntries.find(name => name.endsWith('.csproj')) || '';
|
|
559
656
|
const deps = {
|
|
560
657
|
...(pkg.dependencies || {}),
|
|
@@ -573,19 +670,19 @@ function scanProject() {
|
|
|
573
670
|
depNames,
|
|
574
671
|
composerDepNames,
|
|
575
672
|
rootEntries,
|
|
576
|
-
fileExists,
|
|
577
|
-
requirementsText:
|
|
578
|
-
pyprojectText:
|
|
579
|
-
goModText:
|
|
580
|
-
pubspecText:
|
|
581
|
-
gemfileText:
|
|
582
|
-
pomText:
|
|
583
|
-
gradleText: `${
|
|
584
|
-
cargoText: `${
|
|
585
|
-
csprojText: rootCsproj ?
|
|
586
|
-
programCsText:
|
|
587
|
-
viewFilesText: fs.existsSync(path.join(
|
|
588
|
-
? fs.readdirSync(path.join(
|
|
673
|
+
fileExists: (...parts) => fileExistsAt(appRoot, ...parts),
|
|
674
|
+
requirementsText: readTextIfExistsAt(appRoot, 'requirements.txt'),
|
|
675
|
+
pyprojectText: readTextIfExistsAt(appRoot, 'pyproject.toml'),
|
|
676
|
+
goModText: readTextIfExistsAt(appRoot, 'go.mod'),
|
|
677
|
+
pubspecText: readTextIfExistsAt(appRoot, 'pubspec.yaml'),
|
|
678
|
+
gemfileText: readTextIfExistsAt(appRoot, 'Gemfile'),
|
|
679
|
+
pomText: readTextIfExistsAt(appRoot, 'pom.xml'),
|
|
680
|
+
gradleText: `${readTextIfExistsAt(appRoot, 'build.gradle')}\n${readTextIfExistsAt(appRoot, 'build.gradle.kts')}`,
|
|
681
|
+
cargoText: `${readTextIfExistsAt(appRoot, 'Cargo.toml')}\n${readTextIfExistsAt(appRoot, 'src-tauri', 'Cargo.toml')}`,
|
|
682
|
+
csprojText: rootCsproj ? readTextIfExistsAt(appRoot, rootCsproj) : '',
|
|
683
|
+
programCsText: readTextIfExistsAt(appRoot, 'Program.cs'),
|
|
684
|
+
viewFilesText: fs.existsSync(path.join(appRoot, 'app', 'View'))
|
|
685
|
+
? fs.readdirSync(path.join(appRoot, 'app', 'View'), { recursive: true }).join('\n')
|
|
589
686
|
: ''
|
|
590
687
|
};
|
|
591
688
|
|
|
@@ -631,7 +728,7 @@ function scanProject() {
|
|
|
631
728
|
else if (frameworkDetails.some(item => item.projectType === 'web-app')) projectType = 'web-app';
|
|
632
729
|
else if (frameworkDetails.some(item => item.kind === 'backend')) projectType = 'api';
|
|
633
730
|
else if (frameworkDetails.some(item => item.kind === 'frontend')) projectType = 'web-app';
|
|
634
|
-
else if (
|
|
731
|
+
else if (fileExistsAt(appRoot, 'package.json') && pkg.bin) projectType = 'cli';
|
|
635
732
|
else if (['php', 'python', 'go', 'java', 'ruby', 'csharp', 'rust'].includes(primaryLanguage)) projectType = 'api';
|
|
636
733
|
|
|
637
734
|
if (hasDependency(depNames, 'pg', 'postgres', '@prisma/client') || /postgres/i.test(JSON.stringify(pkg))) database = 'postgres';
|
|
@@ -646,28 +743,30 @@ function scanProject() {
|
|
|
646
743
|
else if (hasDependency(depNames, 'pytest') || textMatches(ctx.requirementsText, /\bpytest\b/i) || textMatches(ctx.pyprojectText, /\bpytest\b/i)) testFramework = 'pytest';
|
|
647
744
|
else if (hasComposerDependency(composerDepNames, 'phpunit/phpunit')) testFramework = 'phpunit';
|
|
648
745
|
else if (hasComposerDependency(composerDepNames, 'pestphp/pest')) testFramework = 'pest';
|
|
649
|
-
else if (
|
|
746
|
+
else if (fileExistsAt(appRoot, 'go.mod')) testFramework = 'go test';
|
|
650
747
|
else if (textMatches(ctx.gemfileText, /^\s*gem\s+["']rspec["']/im)) testFramework = 'rspec';
|
|
651
748
|
|
|
652
|
-
if (
|
|
749
|
+
if (repoHasAny(targetDir, [['Dockerfile'], ['docker-compose.yml'], ['docker-compose.yaml']])) {
|
|
653
750
|
infrastructure.push('docker');
|
|
654
751
|
}
|
|
655
752
|
|
|
656
|
-
if (
|
|
753
|
+
if (repoHasAny(targetDir, [['vercel.json']])) {
|
|
657
754
|
cloud = 'vercel';
|
|
658
755
|
infrastructure.push('vercel');
|
|
659
|
-
} else if (
|
|
756
|
+
} else if (repoHasAny(targetDir, [['fly.toml']])) {
|
|
660
757
|
cloud = 'fly';
|
|
661
758
|
infrastructure.push('fly');
|
|
662
|
-
} else if (
|
|
759
|
+
} else if (repoHasAny(targetDir, [['railway.json'], ['railway.toml']])) {
|
|
663
760
|
cloud = 'railway';
|
|
664
761
|
infrastructure.push('railway');
|
|
665
762
|
}
|
|
666
763
|
|
|
667
|
-
if (
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
764
|
+
if (repoHasAny(targetDir, [['.github', 'workflows'], ['.gitlab-ci.yml'], ['.circleci', 'config.yml'], ['Jenkinsfile']], 2)) {
|
|
765
|
+
if (repoHasAny(targetDir, [['.github', 'workflows']], 2)) ciCd = 'github-actions';
|
|
766
|
+
else if (repoHasAny(targetDir, [['.gitlab-ci.yml']], 2)) ciCd = 'gitlab-ci';
|
|
767
|
+
else if (repoHasAny(targetDir, [['.circleci', 'config.yml']], 2)) ciCd = 'circleci';
|
|
768
|
+
else if (repoHasAny(targetDir, [['Jenkinsfile']], 2)) ciCd = 'jenkins';
|
|
769
|
+
}
|
|
671
770
|
|
|
672
771
|
const scanEvidence = [
|
|
673
772
|
...(languageDetections[0] ? [`language:${languageDetections[0].id} => ${languageDetections[0].evidence.join('; ')}`] : []),
|
|
@@ -676,6 +775,7 @@ function scanProject() {
|
|
|
676
775
|
|
|
677
776
|
return {
|
|
678
777
|
project_name: path.basename(targetDir),
|
|
778
|
+
app_root: path.relative(targetDir, appRoot) || '.',
|
|
679
779
|
description: pkg.description || composer.description || 'A software project',
|
|
680
780
|
project_type: PROJECT_TYPE_CHOICES.includes(projectType) ? projectType : 'web-app',
|
|
681
781
|
primary_language: LANGUAGE_CHOICES.includes(primaryLanguage) ? primaryLanguage : 'typescript',
|
|
@@ -683,7 +783,7 @@ function scanProject() {
|
|
|
683
783
|
framework_details: frameworkDetails,
|
|
684
784
|
scan_evidence: scanEvidence,
|
|
685
785
|
database: DATABASE_CHOICES.includes(database) ? database : 'none',
|
|
686
|
-
package_manager: PACKAGE_MANAGER_CHOICES.includes(detectPackageManager()) ? detectPackageManager() : 'auto-detect',
|
|
786
|
+
package_manager: PACKAGE_MANAGER_CHOICES.includes(detectPackageManager(appRoot)) ? detectPackageManager(appRoot) : 'auto-detect',
|
|
687
787
|
uses_docker: infrastructure.includes('docker'),
|
|
688
788
|
cloud,
|
|
689
789
|
infrastructure: uniq(infrastructure),
|
|
@@ -791,10 +891,11 @@ context:
|
|
|
791
891
|
## Stack
|
|
792
892
|
|
|
793
893
|
\`\`\`yaml
|
|
794
|
-
stack:
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
894
|
+
stack:
|
|
895
|
+
app_root: ${data.app_root || '.'}
|
|
896
|
+
primary_language: ${data.primary_language}
|
|
897
|
+
frameworks: [${(data.frameworks || []).join(', ')}]
|
|
898
|
+
database: ${data.database || 'none'}
|
|
798
899
|
infrastructure: [${(data.infrastructure || []).join(', ')}]
|
|
799
900
|
package_manager: ${data.package_manager || 'auto-detect'}
|
|
800
901
|
test_framework: ${data.test_framework || 'auto-detect'}
|
|
@@ -1121,6 +1222,7 @@ async function main() {
|
|
|
1121
1222
|
data.max_file_lines = data.max_file_lines || scanned?.max_file_lines || '300';
|
|
1122
1223
|
data.framework_details = scanned?.framework_details || [];
|
|
1123
1224
|
data.scan_evidence = scanned?.scan_evidence || [];
|
|
1225
|
+
data.app_root = scanned?.app_root || '.';
|
|
1124
1226
|
|
|
1125
1227
|
if (!assumeYes) {
|
|
1126
1228
|
const aiRefineAnswer = await askQuestions({
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "thachvd-kit",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.0.10",
|
|
4
|
+
"description": "Project context bootstrap kit for Antigravity, Claude Code, and Codex",
|
|
5
5
|
"bin": {
|
|
6
6
|
"thachvd-kit": "./bin/cli.js"
|
|
7
7
|
},
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"license": "MIT",
|
|
39
39
|
"repository": {
|
|
40
40
|
"type": "git",
|
|
41
|
-
"url": "
|
|
41
|
+
"url": "https://github.com/holdon1996/thachvd-kit.git"
|
|
42
42
|
},
|
|
43
43
|
"homepage": "https://github.com/holdon1996/thachvd-kit",
|
|
44
44
|
"bugs": {
|
package/scripts/init.py
CHANGED
|
@@ -156,6 +156,7 @@ CLOUDS_ALL = ["aws", "gcp", "azure", "vercel", "fly", "railway", "digitalocean",
|
|
|
156
156
|
FRONTEND_FRAMEWORKS = ["vue", "react", "next", "nuxt", "svelte", "angular"]
|
|
157
157
|
BACKEND_FRAMEWORKS = ["laravel", "cakephp", "symfony", "codeigniter", "express", "fastapi", "django", "flask", "gin", "fiber", "echo", "nestjs", "rails", "spring", "aspnet", "axum", "actix"]
|
|
158
158
|
MOBILE_DESKTOP_FRAMEWORKS = ["tauri", "flutter", "react-native"]
|
|
159
|
+
IGNORED_SCAN_DIRS = {".git", "node_modules", "vendor", "dist", "build", "coverage", ".next", ".nuxt", ".turbo", ".cache", "storage", "tmp"}
|
|
159
160
|
|
|
160
161
|
def read_text_if_exists(*parts):
|
|
161
162
|
path = Path.cwd().joinpath(*parts)
|
|
@@ -171,23 +172,26 @@ def read_json_if_exists(*parts):
|
|
|
171
172
|
except Exception:
|
|
172
173
|
return None
|
|
173
174
|
|
|
174
|
-
def detect_package_manager():
|
|
175
|
-
|
|
176
|
-
if (
|
|
177
|
-
if (
|
|
178
|
-
if (
|
|
179
|
-
if (
|
|
180
|
-
if (
|
|
181
|
-
if (
|
|
182
|
-
if (
|
|
183
|
-
if (
|
|
184
|
-
if (
|
|
185
|
-
if (
|
|
175
|
+
def detect_package_manager(base_dir=None):
|
|
176
|
+
base_dir = base_dir or Path.cwd()
|
|
177
|
+
if path_exists(base_dir, "pnpm-lock.yaml"): return "pnpm"
|
|
178
|
+
if path_exists(base_dir, "yarn.lock"): return "yarn"
|
|
179
|
+
if path_exists(base_dir, "bun.lockb") or path_exists(base_dir, "bun.lock"): return "bun"
|
|
180
|
+
if path_exists(base_dir, "package-lock.json"): return "npm"
|
|
181
|
+
if path_exists(base_dir, "uv.lock"): return "uv"
|
|
182
|
+
if path_exists(base_dir, "poetry.lock"): return "poetry"
|
|
183
|
+
if path_exists(base_dir, "requirements.txt") or path_exists(base_dir, "pyproject.toml"): return "pip"
|
|
184
|
+
if path_exists(base_dir, "go.mod"): return "go mod"
|
|
185
|
+
if path_exists(base_dir, "Cargo.toml"): return "cargo"
|
|
186
|
+
if path_exists(base_dir, "composer.json"): return "composer"
|
|
186
187
|
return "auto-detect"
|
|
187
188
|
|
|
188
189
|
def uniq(values):
|
|
189
190
|
return list(dict.fromkeys(v for v in values if v))
|
|
190
191
|
|
|
192
|
+
def path_exists(base_dir, *parts):
|
|
193
|
+
return base_dir.joinpath(*parts).exists()
|
|
194
|
+
|
|
191
195
|
def evidence_if(condition, label, weight=1):
|
|
192
196
|
return {"label": label, "weight": weight} if condition else None
|
|
193
197
|
|
|
@@ -215,10 +219,73 @@ def has_composer_dependency(composer_dep_names, *names):
|
|
|
215
219
|
def text_matches(text, pattern):
|
|
216
220
|
return bool(text and re.search(pattern, text, re.IGNORECASE | re.MULTILINE))
|
|
217
221
|
|
|
222
|
+
def detect_app_root(base_dir):
|
|
223
|
+
candidates = [base_dir]
|
|
224
|
+
queue = [(base_dir, 0)]
|
|
225
|
+
while queue:
|
|
226
|
+
current, depth = queue.pop(0)
|
|
227
|
+
if depth >= 2:
|
|
228
|
+
continue
|
|
229
|
+
try:
|
|
230
|
+
entries = list(current.iterdir())
|
|
231
|
+
except Exception:
|
|
232
|
+
continue
|
|
233
|
+
for entry in entries:
|
|
234
|
+
if not entry.is_dir():
|
|
235
|
+
continue
|
|
236
|
+
if entry.name in IGNORED_SCAN_DIRS:
|
|
237
|
+
continue
|
|
238
|
+
candidates.append(entry)
|
|
239
|
+
queue.append((entry, depth + 1))
|
|
240
|
+
|
|
241
|
+
def score_candidate(path):
|
|
242
|
+
score = 0
|
|
243
|
+
if path_exists(path, "composer.json"): score += 5
|
|
244
|
+
if path_exists(path, "package.json"): score += 4
|
|
245
|
+
if path_exists(path, "Cargo.toml"): score += 4
|
|
246
|
+
if path_exists(path, "go.mod"): score += 4
|
|
247
|
+
if path_exists(path, "pyproject.toml") or path_exists(path, "requirements.txt"): score += 4
|
|
248
|
+
if path_exists(path, "artisan"): score += 5
|
|
249
|
+
if path_exists(path, "bin", "cake") or path_exists(path, "app", "Config", "core.php"): score += 5
|
|
250
|
+
if path_exists(path, "src-tauri", "tauri.conf.json") or path_exists(path, "src-tauri", "tauri.conf.json5"): score += 5
|
|
251
|
+
if path_exists(path, "manage.py"): score += 4
|
|
252
|
+
if path_exists(path, "Gemfile"): score += 4
|
|
253
|
+
if path_exists(path, "pom.xml") or path_exists(path, "build.gradle") or path_exists(path, "build.gradle.kts"): score += 4
|
|
254
|
+
if path_exists(path, "app", "View") or path_exists(path, "resources", "views") or path_exists(path, "templates") or path_exists(path, "app", "views"): score += 2
|
|
255
|
+
return score
|
|
256
|
+
|
|
257
|
+
ranked = [{"path": path, "score": score_candidate(path)} for path in candidates]
|
|
258
|
+
ranked = [item for item in ranked if item["score"] > 0]
|
|
259
|
+
ranked.sort(key=lambda item: (-item["score"], len(str(item["path"]))))
|
|
260
|
+
return ranked[0]["path"] if ranked else base_dir
|
|
261
|
+
|
|
262
|
+
def repo_has_any(base_dir, relative_paths, max_depth=3):
|
|
263
|
+
queue = [(base_dir, 0)]
|
|
264
|
+
while queue:
|
|
265
|
+
current, depth = queue.pop(0)
|
|
266
|
+
for rel_parts in relative_paths:
|
|
267
|
+
if path_exists(current, *rel_parts):
|
|
268
|
+
return True
|
|
269
|
+
if depth >= max_depth:
|
|
270
|
+
continue
|
|
271
|
+
try:
|
|
272
|
+
entries = list(current.iterdir())
|
|
273
|
+
except Exception:
|
|
274
|
+
continue
|
|
275
|
+
for entry in entries:
|
|
276
|
+
if not entry.is_dir():
|
|
277
|
+
continue
|
|
278
|
+
if entry.name in IGNORED_SCAN_DIRS:
|
|
279
|
+
continue
|
|
280
|
+
queue.append((entry, depth + 1))
|
|
281
|
+
return False
|
|
282
|
+
|
|
218
283
|
def scan_project():
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
284
|
+
repo_root = Path.cwd()
|
|
285
|
+
app_root = detect_app_root(repo_root)
|
|
286
|
+
pkg = read_json_if_exists(app_root, "package.json") or {}
|
|
287
|
+
composer = read_json_if_exists(app_root, "composer.json") or {}
|
|
288
|
+
root_entries = [p.name for p in app_root.iterdir()] if app_root.exists() else []
|
|
222
289
|
root_csproj = next((name for name in root_entries if name.endswith(".csproj")), "")
|
|
223
290
|
deps = {}
|
|
224
291
|
for key in ("dependencies", "devDependencies", "peerDependencies"):
|
|
@@ -228,53 +295,53 @@ def scan_project():
|
|
|
228
295
|
for key in ("require", "require-dev"):
|
|
229
296
|
composer_deps.update(composer.get(key, {}) or {})
|
|
230
297
|
composer_dep_names = set(composer_deps.keys())
|
|
231
|
-
requirements = read_text_if_exists("requirements.txt")
|
|
232
|
-
pyproject = read_text_if_exists("pyproject.toml")
|
|
233
|
-
go_mod = read_text_if_exists("go.mod")
|
|
234
|
-
pubspec = read_text_if_exists("pubspec.yaml")
|
|
235
|
-
gemfile = read_text_if_exists("Gemfile")
|
|
236
|
-
pom = read_text_if_exists("pom.xml")
|
|
237
|
-
gradle = f"{read_text_if_exists('build.gradle')}\n{read_text_if_exists('build.gradle.kts')}"
|
|
238
|
-
cargo = f"{read_text_if_exists('Cargo.toml')}\n{read_text_if_exists('src-tauri/Cargo.toml')}"
|
|
239
|
-
csproj = read_text_if_exists(root_csproj) if root_csproj else ""
|
|
240
|
-
program_cs = read_text_if_exists("Program.cs")
|
|
298
|
+
requirements = read_text_if_exists(app_root, "requirements.txt")
|
|
299
|
+
pyproject = read_text_if_exists(app_root, "pyproject.toml")
|
|
300
|
+
go_mod = read_text_if_exists(app_root, "go.mod")
|
|
301
|
+
pubspec = read_text_if_exists(app_root, "pubspec.yaml")
|
|
302
|
+
gemfile = read_text_if_exists(app_root, "Gemfile")
|
|
303
|
+
pom = read_text_if_exists(app_root, "pom.xml")
|
|
304
|
+
gradle = f"{read_text_if_exists(app_root, 'build.gradle')}\n{read_text_if_exists(app_root, 'build.gradle.kts')}"
|
|
305
|
+
cargo = f"{read_text_if_exists(app_root, 'Cargo.toml')}\n{read_text_if_exists(app_root, 'src-tauri/Cargo.toml')}"
|
|
306
|
+
csproj = read_text_if_exists(app_root, root_csproj) if root_csproj else ""
|
|
307
|
+
program_cs = read_text_if_exists(app_root, "Program.cs")
|
|
241
308
|
|
|
242
309
|
language_detections = [
|
|
243
310
|
build_detection("typescript", [
|
|
244
|
-
evidence_if(
|
|
311
|
+
evidence_if(path_exists(app_root, "tsconfig.json"), "found tsconfig.json", 4),
|
|
245
312
|
evidence_if(has_dependency(dep_names, "typescript", "ts-node", "tsx"), "package.json -> typescript toolchain", 2),
|
|
246
313
|
]),
|
|
247
314
|
build_detection("javascript", [
|
|
248
|
-
evidence_if(
|
|
315
|
+
evidence_if(path_exists(app_root, "package.json"), "found package.json", 2),
|
|
249
316
|
evidence_if(bool(dep_names), "package.json -> JavaScript dependency graph", 1),
|
|
250
317
|
]),
|
|
251
318
|
build_detection("python", [
|
|
252
|
-
evidence_if(
|
|
253
|
-
evidence_if(
|
|
319
|
+
evidence_if(path_exists(app_root, "requirements.txt"), "found requirements.txt", 3),
|
|
320
|
+
evidence_if(path_exists(app_root, "pyproject.toml"), "found pyproject.toml", 2),
|
|
254
321
|
evidence_if(text_matches(pyproject, r"\[tool\.(poetry|uv|hatch|pdm)\]"), "pyproject.toml -> Python tool section", 2),
|
|
255
322
|
]),
|
|
256
323
|
build_detection("go", [
|
|
257
|
-
evidence_if(
|
|
324
|
+
evidence_if(path_exists(app_root, "go.mod"), "found go.mod", 4),
|
|
258
325
|
]),
|
|
259
326
|
build_detection("php", [
|
|
260
|
-
evidence_if(
|
|
327
|
+
evidence_if(path_exists(app_root, "composer.json"), "found composer.json", 4),
|
|
261
328
|
evidence_if(bool(composer_dep_names), "composer.json -> PHP dependency graph", 1),
|
|
262
329
|
]),
|
|
263
330
|
build_detection("rust", [
|
|
264
|
-
evidence_if(
|
|
265
|
-
evidence_if(
|
|
331
|
+
evidence_if(path_exists(app_root, "Cargo.toml"), "found Cargo.toml", 4),
|
|
332
|
+
evidence_if(path_exists(app_root, "src-tauri", "Cargo.toml"), "found src-tauri/Cargo.toml", 5),
|
|
266
333
|
]),
|
|
267
334
|
build_detection("java", [
|
|
268
|
-
evidence_if(
|
|
269
|
-
evidence_if(
|
|
335
|
+
evidence_if(path_exists(app_root, "pom.xml"), "found pom.xml", 4),
|
|
336
|
+
evidence_if(path_exists(app_root, "build.gradle") or path_exists(app_root, "build.gradle.kts"), "found Gradle build file", 3),
|
|
270
337
|
]),
|
|
271
338
|
build_detection("ruby", [
|
|
272
|
-
evidence_if(
|
|
273
|
-
evidence_if(
|
|
339
|
+
evidence_if(path_exists(app_root, "Gemfile"), "found Gemfile", 4),
|
|
340
|
+
evidence_if(path_exists(app_root, "config.ru"), "found config.ru", 1),
|
|
274
341
|
]),
|
|
275
342
|
build_detection("csharp", [
|
|
276
343
|
evidence_if(any(name.endswith(".sln") or name.endswith(".csproj") for name in root_entries), "found .sln/.csproj", 4),
|
|
277
|
-
evidence_if(
|
|
344
|
+
evidence_if(path_exists(app_root, "Program.cs"), "found Program.cs", 1),
|
|
278
345
|
]),
|
|
279
346
|
]
|
|
280
347
|
language_detections = [item for item in language_detections if item["score"] >= 2]
|
|
@@ -283,8 +350,8 @@ def scan_project():
|
|
|
283
350
|
framework_details = [
|
|
284
351
|
{**build_detection("next", [
|
|
285
352
|
evidence_if(has_dependency(dep_names, "next"), "package.json -> next", 4),
|
|
286
|
-
evidence_if(
|
|
287
|
-
evidence_if(
|
|
353
|
+
evidence_if(path_exists(app_root, "next.config.js") or path_exists(app_root, "next.config.mjs") or path_exists(app_root, "next.config.ts"), "found next.config.*", 3),
|
|
354
|
+
evidence_if(path_exists(app_root, "app") or path_exists(app_root, "pages"), "found app/ or pages/", 1),
|
|
288
355
|
]), "kind": "frontend", "project_type": "web-app"},
|
|
289
356
|
{**build_detection("react", [
|
|
290
357
|
evidence_if(has_dependency(dep_names, "react"), "package.json -> react", 4),
|
|
@@ -295,14 +362,14 @@ def scan_project():
|
|
|
295
362
|
]), "kind": "frontend", "project_type": "web-app"},
|
|
296
363
|
{**build_detection("nuxt", [
|
|
297
364
|
evidence_if(has_dependency(dep_names, "nuxt"), "package.json -> nuxt", 4),
|
|
298
|
-
evidence_if(
|
|
365
|
+
evidence_if(path_exists(app_root, "nuxt.config.ts") or path_exists(app_root, "nuxt.config.js"), "found nuxt.config.*", 3),
|
|
299
366
|
]), "kind": "frontend", "project_type": "web-app"},
|
|
300
367
|
{**build_detection("svelte", [
|
|
301
368
|
evidence_if(has_dependency(dep_names, "svelte", "@sveltejs/kit"), "package.json -> svelte", 4),
|
|
302
369
|
]), "kind": "frontend", "project_type": "web-app"},
|
|
303
370
|
{**build_detection("angular", [
|
|
304
371
|
evidence_if(has_dependency(dep_names, "@angular/core"), "package.json -> @angular/core", 4),
|
|
305
|
-
evidence_if(
|
|
372
|
+
evidence_if(path_exists(app_root, "angular.json"), "found angular.json", 3),
|
|
306
373
|
]), "kind": "frontend", "project_type": "web-app"},
|
|
307
374
|
{**build_detection("express", [
|
|
308
375
|
evidence_if(has_dependency(dep_names, "express"), "package.json -> express", 4),
|
|
@@ -315,39 +382,39 @@ def scan_project():
|
|
|
315
382
|
]), "kind": "backend", "project_type": "api", "language": "python"},
|
|
316
383
|
{**build_detection("django", [
|
|
317
384
|
evidence_if(text_matches(requirements, r"\bdjango\b") or text_matches(pyproject, r"\bdjango\b"), "Python deps -> django", 4),
|
|
318
|
-
evidence_if(
|
|
319
|
-
evidence_if(
|
|
385
|
+
evidence_if(path_exists(app_root, "manage.py"), "found manage.py", 2),
|
|
386
|
+
evidence_if(path_exists(app_root, "templates"), "found templates/", 1),
|
|
320
387
|
]), "kind": "backend", "project_type": "web-app", "language": "python"},
|
|
321
388
|
{**build_detection("flask", [
|
|
322
389
|
evidence_if(text_matches(requirements, r"\bflask\b") or text_matches(pyproject, r"\bflask\b"), "Python deps -> flask", 4),
|
|
323
390
|
]), "kind": "backend", "project_type": "api", "language": "python"},
|
|
324
391
|
{**build_detection("laravel", [
|
|
325
392
|
evidence_if(has_composer_dependency(composer_dep_names, "laravel/framework"), "composer.json -> laravel/framework", 4),
|
|
326
|
-
evidence_if(
|
|
327
|
-
evidence_if(
|
|
393
|
+
evidence_if(path_exists(app_root, "artisan"), "found artisan", 3),
|
|
394
|
+
evidence_if(path_exists(app_root, "resources", "views"), "found resources/views", 1),
|
|
328
395
|
]), "kind": "backend", "project_type": "web-app", "language": "php"},
|
|
329
396
|
{**build_detection("cakephp", [
|
|
330
397
|
evidence_if(has_composer_dependency(composer_dep_names, "cakephp/cakephp"), "composer.json -> cakephp/cakephp", 4),
|
|
331
|
-
evidence_if(
|
|
332
|
-
evidence_if(
|
|
333
|
-
evidence_if(
|
|
334
|
-
evidence_if(
|
|
335
|
-
evidence_if(
|
|
336
|
-
evidence_if(
|
|
337
|
-
evidence_if(
|
|
338
|
-
evidence_if(
|
|
339
|
-
evidence_if(any(p.suffix.lower() == ".ctp" for p in
|
|
398
|
+
evidence_if(path_exists(app_root, "bin", "cake"), "found bin/cake", 3),
|
|
399
|
+
evidence_if(path_exists(app_root, "config", "app.php"), "found config/app.php", 2),
|
|
400
|
+
evidence_if(path_exists(app_root, "app", "Config", "core.php"), "found app/Config/core.php (CakePHP legacy)", 3),
|
|
401
|
+
evidence_if(path_exists(app_root, "app", "Controller", "AppController.php"), "found app/Controller/AppController.php", 2),
|
|
402
|
+
evidence_if(path_exists(app_root, "app", "Model", "AppModel.php"), "found app/Model/AppModel.php", 2),
|
|
403
|
+
evidence_if(path_exists(app_root, "lib", "Cake"), "found lib/Cake", 3),
|
|
404
|
+
evidence_if(path_exists(app_root, "cake", "console", "cake"), "found cake/console/cake", 3),
|
|
405
|
+
evidence_if(path_exists(app_root, "app", "View"), "found app/View", 2),
|
|
406
|
+
evidence_if(any(p.suffix.lower() == ".ctp" for p in app_root.joinpath("app", "View").rglob("*.ctp")) if path_exists(app_root, "app", "View") else False, "found .ctp view templates", 2),
|
|
340
407
|
]), "kind": "backend", "project_type": "web-app", "language": "php"},
|
|
341
408
|
{**build_detection("symfony", [
|
|
342
409
|
evidence_if(has_composer_dependency(composer_dep_names, "symfony/framework-bundle"), "composer.json -> symfony/framework-bundle", 4),
|
|
343
|
-
evidence_if(
|
|
344
|
-
evidence_if(
|
|
345
|
-
evidence_if(
|
|
410
|
+
evidence_if(path_exists(app_root, "bin", "console"), "found bin/console", 2),
|
|
411
|
+
evidence_if(path_exists(app_root, "config", "bundles.php"), "found config/bundles.php", 1),
|
|
412
|
+
evidence_if(path_exists(app_root, "templates"), "found templates/", 1),
|
|
346
413
|
]), "kind": "backend", "project_type": "web-app", "language": "php"},
|
|
347
414
|
{**build_detection("codeigniter", [
|
|
348
415
|
evidence_if(has_composer_dependency(composer_dep_names, "codeigniter4/framework"), "composer.json -> codeigniter4/framework", 4),
|
|
349
|
-
evidence_if(
|
|
350
|
-
evidence_if(
|
|
416
|
+
evidence_if(path_exists(app_root, "spark"), "found spark", 2),
|
|
417
|
+
evidence_if(path_exists(app_root, "app", "Config", "App.php"), "found app/Config/App.php", 1),
|
|
351
418
|
]), "kind": "backend", "project_type": "api", "language": "php"},
|
|
352
419
|
{**build_detection("gin", [
|
|
353
420
|
evidence_if(text_matches(go_mod, r"\bgithub\.com\/gin-gonic\/gin\b"), "go.mod -> github.com/gin-gonic/gin", 4),
|
|
@@ -360,8 +427,8 @@ def scan_project():
|
|
|
360
427
|
]), "kind": "backend", "project_type": "api", "language": "go"},
|
|
361
428
|
{**build_detection("rails", [
|
|
362
429
|
evidence_if(text_matches(gemfile, r'^\s*gem\s+["\']rails["\']'), "Gemfile -> rails", 4),
|
|
363
|
-
evidence_if(
|
|
364
|
-
evidence_if(
|
|
430
|
+
evidence_if(path_exists(app_root, "bin", "rails"), "found bin/rails", 2),
|
|
431
|
+
evidence_if(path_exists(app_root, "app", "views"), "found app/views", 1),
|
|
365
432
|
]), "kind": "backend", "project_type": "web-app", "language": "ruby"},
|
|
366
433
|
{**build_detection("spring", [
|
|
367
434
|
evidence_if(text_matches(pom, r"spring-boot") or text_matches(gradle, r"spring-boot"), "Java build -> spring-boot", 4),
|
|
@@ -372,8 +439,8 @@ def scan_project():
|
|
|
372
439
|
]), "kind": "backend", "project_type": "api", "language": "csharp"},
|
|
373
440
|
{**build_detection("tauri", [
|
|
374
441
|
evidence_if(has_dependency(dep_names, "@tauri-apps/api"), "package.json -> @tauri-apps/api", 4),
|
|
375
|
-
evidence_if(
|
|
376
|
-
evidence_if(
|
|
442
|
+
evidence_if(path_exists(app_root, "src-tauri", "tauri.conf.json") or path_exists(app_root, "src-tauri", "tauri.conf.json5"), "found src-tauri/tauri.conf.*", 4),
|
|
443
|
+
evidence_if(path_exists(app_root, "src-tauri", "Cargo.toml"), "found src-tauri/Cargo.toml", 2),
|
|
377
444
|
]), "kind": "desktop", "project_type": "desktop", "language": "rust"},
|
|
378
445
|
{**build_detection("axum", [
|
|
379
446
|
evidence_if(text_matches(cargo, r"\baxum\b"), "Cargo.toml -> axum", 4),
|
|
@@ -413,7 +480,7 @@ def scan_project():
|
|
|
413
480
|
elif any(item["project_type"] == "web-app" for item in framework_details): project_type = "web-app"
|
|
414
481
|
elif any(item["kind"] == "backend" for item in framework_details): project_type = "api"
|
|
415
482
|
elif any(item["kind"] == "frontend" for item in framework_details): project_type = "web-app"
|
|
416
|
-
elif
|
|
483
|
+
elif path_exists(app_root, "package.json") and pkg.get("bin"): project_type = "cli"
|
|
417
484
|
elif primary_language in ["php", "python", "go", "java", "ruby", "csharp", "rust"]: project_type = "api"
|
|
418
485
|
|
|
419
486
|
if has_dependency(dep_names, "pg", "postgres", "@prisma/client") or "postgres" in json.dumps(pkg).lower(): database = "postgres"
|
|
@@ -428,26 +495,26 @@ def scan_project():
|
|
|
428
495
|
elif has_dependency(dep_names, "pytest") or text_matches(requirements, r"\bpytest\b") or text_matches(pyproject, r"\bpytest\b"): test_framework = "pytest"
|
|
429
496
|
elif has_composer_dependency(composer_dep_names, "phpunit/phpunit"): test_framework = "phpunit"
|
|
430
497
|
elif has_composer_dependency(composer_dep_names, "pestphp/pest"): test_framework = "pest"
|
|
431
|
-
elif
|
|
498
|
+
elif path_exists(app_root, "go.mod"): test_framework = "go test"
|
|
432
499
|
elif text_matches(gemfile, r'^\s*gem\s+["\']rspec["\']'): test_framework = "rspec"
|
|
433
500
|
|
|
434
|
-
if
|
|
501
|
+
if repo_has_any(repo_root, [("Dockerfile",), ("docker-compose.yml",), ("docker-compose.yaml",)]):
|
|
435
502
|
infrastructure.append("docker")
|
|
436
503
|
|
|
437
|
-
if
|
|
504
|
+
if repo_has_any(repo_root, [("vercel.json",)]):
|
|
438
505
|
cloud = "vercel"
|
|
439
506
|
infrastructure.append("vercel")
|
|
440
|
-
elif
|
|
507
|
+
elif repo_has_any(repo_root, [("fly.toml",)]):
|
|
441
508
|
cloud = "fly"
|
|
442
509
|
infrastructure.append("fly")
|
|
443
|
-
elif
|
|
510
|
+
elif repo_has_any(repo_root, [("railway.json",), ("railway.toml",)]):
|
|
444
511
|
cloud = "railway"
|
|
445
512
|
infrastructure.append("railway")
|
|
446
513
|
|
|
447
|
-
if
|
|
448
|
-
elif
|
|
449
|
-
elif
|
|
450
|
-
elif
|
|
514
|
+
if repo_has_any(repo_root, [(".github", "workflows")], max_depth=2): ci_cd = "github-actions"
|
|
515
|
+
elif repo_has_any(repo_root, [(".gitlab-ci.yml",)], max_depth=2): ci_cd = "gitlab-ci"
|
|
516
|
+
elif repo_has_any(repo_root, [(".circleci", "config.yml")], max_depth=2): ci_cd = "circleci"
|
|
517
|
+
elif repo_has_any(repo_root, [("Jenkinsfile",)], max_depth=2): ci_cd = "jenkins"
|
|
451
518
|
|
|
452
519
|
scan_evidence = []
|
|
453
520
|
if language_detections:
|
|
@@ -456,6 +523,7 @@ def scan_project():
|
|
|
456
523
|
|
|
457
524
|
return {
|
|
458
525
|
"project_name": Path.cwd().name,
|
|
526
|
+
"app_root": str(app_root.relative_to(repo_root)) if app_root != repo_root else ".",
|
|
459
527
|
"description": pkg.get("description") or composer.get("description", "A software project"),
|
|
460
528
|
"project_type": project_type if project_type in PROJECT_TYPES_ALL else "web-app",
|
|
461
529
|
"primary_language": primary_language if primary_language in LANGUAGES_ALL else "typescript",
|
|
@@ -463,7 +531,7 @@ def scan_project():
|
|
|
463
531
|
"framework_details": framework_details,
|
|
464
532
|
"scan_evidence": scan_evidence,
|
|
465
533
|
"database": database if database in DATABASES_ALL else "none",
|
|
466
|
-
"package_manager": detect_package_manager(),
|
|
534
|
+
"package_manager": detect_package_manager(app_root),
|
|
467
535
|
"uses_docker": "docker" in infrastructure,
|
|
468
536
|
"cloud": cloud,
|
|
469
537
|
"infrastructure": uniq(infrastructure),
|
|
@@ -568,10 +636,11 @@ context:
|
|
|
568
636
|
## Stack
|
|
569
637
|
|
|
570
638
|
```yaml
|
|
571
|
-
stack:
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
639
|
+
stack:
|
|
640
|
+
app_root: {data.get('app_root', '.')}
|
|
641
|
+
primary_language: {data['primary_language']}
|
|
642
|
+
frameworks: [{', '.join(frameworks)}]
|
|
643
|
+
database: {data.get('database', 'none')}
|
|
575
644
|
infrastructure: [{', '.join(infrastructure)}]
|
|
576
645
|
package_manager: {data.get('package_manager', 'auto-detect')}
|
|
577
646
|
test_framework: {data.get('test_framework', 'auto-detect')}
|
|
@@ -720,6 +789,7 @@ When done, summarize:
|
|
|
720
789
|
|
|
721
790
|
def format_scan_preview_lines(scanned):
|
|
722
791
|
return [
|
|
792
|
+
("App root", scanned.get("app_root", ".")),
|
|
723
793
|
("Project type", scanned.get("project_type", "unknown")),
|
|
724
794
|
("Primary language", scanned.get("primary_language", "unknown")),
|
|
725
795
|
("Frameworks", ", ".join(scanned.get("frameworks", [])) or "no framework"),
|
|
@@ -863,6 +933,7 @@ def main():
|
|
|
863
933
|
data["code_style"] = data.get("code_style", (scanned or {}).get("code_style", "project-standard"))
|
|
864
934
|
data["framework_details"] = data.get("framework_details", (scanned or {}).get("framework_details", []))
|
|
865
935
|
data["scan_evidence"] = data.get("scan_evidence", (scanned or {}).get("scan_evidence", []))
|
|
936
|
+
data["app_root"] = data.get("app_root", (scanned or {}).get("app_root", "."))
|
|
866
937
|
data["context_status"] = data.get("context_status", "baseline")
|
|
867
938
|
data["needs_ai_refinement"] = data.get("needs_ai_refinement", False)
|
|
868
939
|
data["last_refined_at"] = data.get("last_refined_at", "not-yet-refined")
|