thachvd-kit 1.0.7 โ 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 +213 -69
- package/package.json +3 -3
- package/rules/GEMINI.md +13 -10
- package/scripts/init.py +198 -88
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(`
|
|
@@ -126,6 +127,21 @@ ${pc.bold('Options:')}
|
|
|
126
127
|
`);
|
|
127
128
|
}
|
|
128
129
|
|
|
130
|
+
function formatScanPreviewLines(scanned) {
|
|
131
|
+
return [
|
|
132
|
+
['App root', scanned.app_root || '.'],
|
|
133
|
+
['Project type', scanned.project_type || 'unknown'],
|
|
134
|
+
['Primary language', scanned.primary_language || 'unknown'],
|
|
135
|
+
['Frameworks', (scanned.frameworks || []).join(', ') || 'no framework'],
|
|
136
|
+
['Database', scanned.database || 'none'],
|
|
137
|
+
['Package manager', scanned.package_manager || 'auto-detect'],
|
|
138
|
+
['Test framework', scanned.test_framework || 'auto-detect'],
|
|
139
|
+
['Cloud', scanned.cloud || 'none'],
|
|
140
|
+
['Infrastructure', (scanned.infrastructure || []).join(', ') || 'none'],
|
|
141
|
+
['CI/CD', scanned.ci_cd || 'none']
|
|
142
|
+
];
|
|
143
|
+
}
|
|
144
|
+
|
|
129
145
|
async function askQuestions(questions) {
|
|
130
146
|
return prompts(questions, {
|
|
131
147
|
onCancel: () => {
|
|
@@ -139,6 +155,10 @@ function fileExists(...parts) {
|
|
|
139
155
|
return fs.existsSync(path.join(targetDir, ...parts));
|
|
140
156
|
}
|
|
141
157
|
|
|
158
|
+
function fileExistsAt(baseDir, ...parts) {
|
|
159
|
+
return fs.existsSync(path.join(baseDir, ...parts));
|
|
160
|
+
}
|
|
161
|
+
|
|
142
162
|
function readJsonIfExists(...parts) {
|
|
143
163
|
const fullPath = path.join(targetDir, ...parts);
|
|
144
164
|
if (!fs.existsSync(fullPath)) return null;
|
|
@@ -149,6 +169,16 @@ function readJsonIfExists(...parts) {
|
|
|
149
169
|
}
|
|
150
170
|
}
|
|
151
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
|
+
|
|
152
182
|
function readTextIfExists(...parts) {
|
|
153
183
|
const fullPath = path.join(targetDir, ...parts);
|
|
154
184
|
if (!fs.existsSync(fullPath)) return '';
|
|
@@ -159,17 +189,27 @@ function readTextIfExists(...parts) {
|
|
|
159
189
|
}
|
|
160
190
|
}
|
|
161
191
|
|
|
162
|
-
function
|
|
163
|
-
|
|
164
|
-
if (
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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';
|
|
173
213
|
return 'auto-detect';
|
|
174
214
|
}
|
|
175
215
|
|
|
@@ -177,6 +217,76 @@ function uniq(values) {
|
|
|
177
217
|
return Array.from(new Set(values.filter(Boolean)));
|
|
178
218
|
}
|
|
179
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
|
+
|
|
180
290
|
function evidenceIf(condition, label, weight = 1) {
|
|
181
291
|
return condition ? { label, weight } : null;
|
|
182
292
|
}
|
|
@@ -365,10 +475,11 @@ const FRAMEWORK_DETECTORS = [
|
|
|
365
475
|
id: 'django',
|
|
366
476
|
kind: 'backend',
|
|
367
477
|
language: 'python',
|
|
368
|
-
projectType: '
|
|
478
|
+
projectType: 'web-app',
|
|
369
479
|
detect: ctx => buildDetection('django', [
|
|
370
480
|
evidenceIf(textMatches(ctx.requirementsText, /\bdjango\b/i) || textMatches(ctx.pyprojectText, /\bdjango\b/i), 'Python deps -> django', 4),
|
|
371
|
-
evidenceIf(ctx.fileExists('manage.py'), 'found manage.py', 2)
|
|
481
|
+
evidenceIf(ctx.fileExists('manage.py'), 'found manage.py', 2),
|
|
482
|
+
evidenceIf(ctx.fileExists('templates'), 'found templates/', 1)
|
|
372
483
|
])
|
|
373
484
|
},
|
|
374
485
|
{
|
|
@@ -384,17 +495,18 @@ const FRAMEWORK_DETECTORS = [
|
|
|
384
495
|
id: 'laravel',
|
|
385
496
|
kind: 'backend',
|
|
386
497
|
language: 'php',
|
|
387
|
-
projectType: '
|
|
498
|
+
projectType: 'web-app',
|
|
388
499
|
detect: ctx => buildDetection('laravel', [
|
|
389
500
|
evidenceIf(hasComposerDependency(ctx.composerDepNames, 'laravel/framework'), 'composer.json -> laravel/framework', 4),
|
|
390
|
-
evidenceIf(ctx.fileExists('artisan'), 'found artisan', 3)
|
|
501
|
+
evidenceIf(ctx.fileExists('artisan'), 'found artisan', 3),
|
|
502
|
+
evidenceIf(ctx.fileExists('resources', 'views'), 'found resources/views', 1)
|
|
391
503
|
])
|
|
392
504
|
},
|
|
393
505
|
{
|
|
394
506
|
id: 'cakephp',
|
|
395
507
|
kind: 'backend',
|
|
396
508
|
language: 'php',
|
|
397
|
-
projectType: '
|
|
509
|
+
projectType: 'web-app',
|
|
398
510
|
detect: ctx => buildDetection('cakephp', [
|
|
399
511
|
evidenceIf(hasComposerDependency(ctx.composerDepNames, 'cakephp/cakephp'), 'composer.json -> cakephp/cakephp', 4),
|
|
400
512
|
evidenceIf(ctx.fileExists('bin', 'cake'), 'found bin/cake', 3),
|
|
@@ -403,18 +515,21 @@ const FRAMEWORK_DETECTORS = [
|
|
|
403
515
|
evidenceIf(ctx.fileExists('app', 'Controller', 'AppController.php'), 'found app/Controller/AppController.php', 2),
|
|
404
516
|
evidenceIf(ctx.fileExists('app', 'Model', 'AppModel.php'), 'found app/Model/AppModel.php', 2),
|
|
405
517
|
evidenceIf(ctx.fileExists('lib', 'Cake'), 'found lib/Cake', 3),
|
|
406
|
-
evidenceIf(ctx.fileExists('cake', 'console', 'cake'), 'found cake/console/cake', 3)
|
|
518
|
+
evidenceIf(ctx.fileExists('cake', 'console', 'cake'), 'found cake/console/cake', 3),
|
|
519
|
+
evidenceIf(ctx.fileExists('app', 'View'), 'found app/View', 2),
|
|
520
|
+
evidenceIf(textMatches(ctx.viewFilesText, /\.ctp$/im), 'found .ctp view templates', 2)
|
|
407
521
|
])
|
|
408
522
|
},
|
|
409
523
|
{
|
|
410
524
|
id: 'symfony',
|
|
411
525
|
kind: 'backend',
|
|
412
526
|
language: 'php',
|
|
413
|
-
projectType: '
|
|
527
|
+
projectType: 'web-app',
|
|
414
528
|
detect: ctx => buildDetection('symfony', [
|
|
415
529
|
evidenceIf(hasComposerDependency(ctx.composerDepNames, 'symfony/framework-bundle'), 'composer.json -> symfony/framework-bundle', 4),
|
|
416
530
|
evidenceIf(ctx.fileExists('bin', 'console'), 'found bin/console', 2),
|
|
417
|
-
evidenceIf(ctx.fileExists('config', 'bundles.php'), 'found config/bundles.php', 1)
|
|
531
|
+
evidenceIf(ctx.fileExists('config', 'bundles.php'), 'found config/bundles.php', 1),
|
|
532
|
+
evidenceIf(ctx.fileExists('templates'), 'found templates/', 1)
|
|
418
533
|
])
|
|
419
534
|
},
|
|
420
535
|
{
|
|
@@ -459,10 +574,11 @@ const FRAMEWORK_DETECTORS = [
|
|
|
459
574
|
id: 'rails',
|
|
460
575
|
kind: 'backend',
|
|
461
576
|
language: 'ruby',
|
|
462
|
-
projectType: '
|
|
577
|
+
projectType: 'web-app',
|
|
463
578
|
detect: ctx => buildDetection('rails', [
|
|
464
579
|
evidenceIf(textMatches(ctx.gemfileText, /^\s*gem\s+["']rails["']/im), 'Gemfile -> rails', 4),
|
|
465
|
-
evidenceIf(ctx.fileExists('bin', 'rails'), 'found bin/rails', 2)
|
|
580
|
+
evidenceIf(ctx.fileExists('bin', 'rails'), 'found bin/rails', 2),
|
|
581
|
+
evidenceIf(ctx.fileExists('app', 'views'), 'found app/views', 1)
|
|
466
582
|
])
|
|
467
583
|
},
|
|
468
584
|
{
|
|
@@ -532,9 +648,10 @@ const FRAMEWORK_DETECTORS = [
|
|
|
532
648
|
];
|
|
533
649
|
|
|
534
650
|
function scanProject() {
|
|
535
|
-
const
|
|
536
|
-
const
|
|
537
|
-
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) : [];
|
|
538
655
|
const rootCsproj = rootEntries.find(name => name.endsWith('.csproj')) || '';
|
|
539
656
|
const deps = {
|
|
540
657
|
...(pkg.dependencies || {}),
|
|
@@ -553,17 +670,20 @@ function scanProject() {
|
|
|
553
670
|
depNames,
|
|
554
671
|
composerDepNames,
|
|
555
672
|
rootEntries,
|
|
556
|
-
fileExists,
|
|
557
|
-
requirementsText:
|
|
558
|
-
pyprojectText:
|
|
559
|
-
goModText:
|
|
560
|
-
pubspecText:
|
|
561
|
-
gemfileText:
|
|
562
|
-
pomText:
|
|
563
|
-
gradleText: `${
|
|
564
|
-
cargoText: `${
|
|
565
|
-
csprojText: rootCsproj ?
|
|
566
|
-
programCsText:
|
|
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')
|
|
686
|
+
: ''
|
|
567
687
|
};
|
|
568
688
|
|
|
569
689
|
const languageDetections = LANGUAGE_DETECTORS
|
|
@@ -605,9 +725,10 @@ function scanProject() {
|
|
|
605
725
|
if (frameworkDetails.some(item => item.projectType === 'desktop')) projectType = 'desktop';
|
|
606
726
|
else if (frameworkDetails.some(item => item.projectType === 'mobile')) projectType = 'mobile';
|
|
607
727
|
else if (frameworkDetails.some(item => item.kind === 'frontend') && frameworkDetails.some(item => item.kind === 'backend')) projectType = 'fullstack';
|
|
728
|
+
else if (frameworkDetails.some(item => item.projectType === 'web-app')) projectType = 'web-app';
|
|
608
729
|
else if (frameworkDetails.some(item => item.kind === 'backend')) projectType = 'api';
|
|
609
730
|
else if (frameworkDetails.some(item => item.kind === 'frontend')) projectType = 'web-app';
|
|
610
|
-
else if (
|
|
731
|
+
else if (fileExistsAt(appRoot, 'package.json') && pkg.bin) projectType = 'cli';
|
|
611
732
|
else if (['php', 'python', 'go', 'java', 'ruby', 'csharp', 'rust'].includes(primaryLanguage)) projectType = 'api';
|
|
612
733
|
|
|
613
734
|
if (hasDependency(depNames, 'pg', 'postgres', '@prisma/client') || /postgres/i.test(JSON.stringify(pkg))) database = 'postgres';
|
|
@@ -622,28 +743,30 @@ function scanProject() {
|
|
|
622
743
|
else if (hasDependency(depNames, 'pytest') || textMatches(ctx.requirementsText, /\bpytest\b/i) || textMatches(ctx.pyprojectText, /\bpytest\b/i)) testFramework = 'pytest';
|
|
623
744
|
else if (hasComposerDependency(composerDepNames, 'phpunit/phpunit')) testFramework = 'phpunit';
|
|
624
745
|
else if (hasComposerDependency(composerDepNames, 'pestphp/pest')) testFramework = 'pest';
|
|
625
|
-
else if (
|
|
746
|
+
else if (fileExistsAt(appRoot, 'go.mod')) testFramework = 'go test';
|
|
626
747
|
else if (textMatches(ctx.gemfileText, /^\s*gem\s+["']rspec["']/im)) testFramework = 'rspec';
|
|
627
748
|
|
|
628
|
-
if (
|
|
749
|
+
if (repoHasAny(targetDir, [['Dockerfile'], ['docker-compose.yml'], ['docker-compose.yaml']])) {
|
|
629
750
|
infrastructure.push('docker');
|
|
630
751
|
}
|
|
631
752
|
|
|
632
|
-
if (
|
|
753
|
+
if (repoHasAny(targetDir, [['vercel.json']])) {
|
|
633
754
|
cloud = 'vercel';
|
|
634
755
|
infrastructure.push('vercel');
|
|
635
|
-
} else if (
|
|
756
|
+
} else if (repoHasAny(targetDir, [['fly.toml']])) {
|
|
636
757
|
cloud = 'fly';
|
|
637
758
|
infrastructure.push('fly');
|
|
638
|
-
} else if (
|
|
759
|
+
} else if (repoHasAny(targetDir, [['railway.json'], ['railway.toml']])) {
|
|
639
760
|
cloud = 'railway';
|
|
640
761
|
infrastructure.push('railway');
|
|
641
762
|
}
|
|
642
763
|
|
|
643
|
-
if (
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
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
|
+
}
|
|
647
770
|
|
|
648
771
|
const scanEvidence = [
|
|
649
772
|
...(languageDetections[0] ? [`language:${languageDetections[0].id} => ${languageDetections[0].evidence.join('; ')}`] : []),
|
|
@@ -652,6 +775,7 @@ function scanProject() {
|
|
|
652
775
|
|
|
653
776
|
return {
|
|
654
777
|
project_name: path.basename(targetDir),
|
|
778
|
+
app_root: path.relative(targetDir, appRoot) || '.',
|
|
655
779
|
description: pkg.description || composer.description || 'A software project',
|
|
656
780
|
project_type: PROJECT_TYPE_CHOICES.includes(projectType) ? projectType : 'web-app',
|
|
657
781
|
primary_language: LANGUAGE_CHOICES.includes(primaryLanguage) ? primaryLanguage : 'typescript',
|
|
@@ -659,7 +783,7 @@ function scanProject() {
|
|
|
659
783
|
framework_details: frameworkDetails,
|
|
660
784
|
scan_evidence: scanEvidence,
|
|
661
785
|
database: DATABASE_CHOICES.includes(database) ? database : 'none',
|
|
662
|
-
package_manager: PACKAGE_MANAGER_CHOICES.includes(detectPackageManager()) ? detectPackageManager() : 'auto-detect',
|
|
786
|
+
package_manager: PACKAGE_MANAGER_CHOICES.includes(detectPackageManager(appRoot)) ? detectPackageManager(appRoot) : 'auto-detect',
|
|
663
787
|
uses_docker: infrastructure.includes('docker'),
|
|
664
788
|
cloud,
|
|
665
789
|
infrastructure: uniq(infrastructure),
|
|
@@ -745,21 +869,33 @@ ${(data.scan_evidence || []).map(line => '- ' + line).join('\n')}
|
|
|
745
869
|
# Generated by thachvd-kit init wizard on ${new Date().toISOString().split('T')[0]}
|
|
746
870
|
# Edit freely โ this is your project's brain.
|
|
747
871
|
|
|
748
|
-
## Project
|
|
749
|
-
|
|
750
|
-
- **Name:** ${data.project_name}
|
|
751
|
-
- **Description:** ${data.description}
|
|
752
|
-
- **Type:** ${data.project_type}
|
|
753
|
-
|
|
754
|
-
---
|
|
872
|
+
## Project
|
|
873
|
+
|
|
874
|
+
- **Name:** ${data.project_name}
|
|
875
|
+
- **Description:** ${data.description}
|
|
876
|
+
- **Type:** ${data.project_type}
|
|
877
|
+
|
|
878
|
+
---
|
|
879
|
+
|
|
880
|
+
## Context Status
|
|
881
|
+
|
|
882
|
+
\`\`\`yaml
|
|
883
|
+
context:
|
|
884
|
+
status: ${data.context_status || 'baseline'}
|
|
885
|
+
needs_ai_refinement: ${data.needs_ai_refinement ? 'true' : 'false'}
|
|
886
|
+
last_refined_at: ${data.last_refined_at || 'not-yet-refined'}
|
|
887
|
+
\`\`\`
|
|
888
|
+
|
|
889
|
+
---
|
|
755
890
|
|
|
756
891
|
## Stack
|
|
757
892
|
|
|
758
893
|
\`\`\`yaml
|
|
759
|
-
stack:
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
894
|
+
stack:
|
|
895
|
+
app_root: ${data.app_root || '.'}
|
|
896
|
+
primary_language: ${data.primary_language}
|
|
897
|
+
frameworks: [${(data.frameworks || []).join(', ')}]
|
|
898
|
+
database: ${data.database || 'none'}
|
|
763
899
|
infrastructure: [${(data.infrastructure || []).join(', ')}]
|
|
764
900
|
package_manager: ${data.package_manager || 'auto-detect'}
|
|
765
901
|
test_framework: ${data.test_framework || 'auto-detect'}
|
|
@@ -992,8 +1128,11 @@ async function main() {
|
|
|
992
1128
|
let requestAiRefinement = false;
|
|
993
1129
|
|
|
994
1130
|
if (useScan) {
|
|
995
|
-
console.log(
|
|
996
|
-
|
|
1131
|
+
console.log(` ${pc.bold('Detected context:')}`);
|
|
1132
|
+
formatScanPreviewLines(scanned).forEach(([label, value]) => {
|
|
1133
|
+
console.log(` ยท ${label}: ${value}`);
|
|
1134
|
+
});
|
|
1135
|
+
console.log('');
|
|
997
1136
|
if ((scanned.scan_evidence || []).length > 0) {
|
|
998
1137
|
console.log(` ${pc.bold('Evidence:')}`);
|
|
999
1138
|
scanned.scan_evidence.forEach(line => console.log(` ยท ${line}`));
|
|
@@ -1083,6 +1222,21 @@ async function main() {
|
|
|
1083
1222
|
data.max_file_lines = data.max_file_lines || scanned?.max_file_lines || '300';
|
|
1084
1223
|
data.framework_details = scanned?.framework_details || [];
|
|
1085
1224
|
data.scan_evidence = scanned?.scan_evidence || [];
|
|
1225
|
+
data.app_root = scanned?.app_root || '.';
|
|
1226
|
+
|
|
1227
|
+
if (!assumeYes) {
|
|
1228
|
+
const aiRefineAnswer = await askQuestions({
|
|
1229
|
+
type: 'confirm',
|
|
1230
|
+
name: 'ai_refine',
|
|
1231
|
+
message: 'Should the agent refine PROJECT_CONTEXT.md on first session start?',
|
|
1232
|
+
initial: useScan
|
|
1233
|
+
});
|
|
1234
|
+
requestAiRefinement = Boolean(aiRefineAnswer.ai_refine);
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
data.context_status = 'baseline';
|
|
1238
|
+
data.needs_ai_refinement = requestAiRefinement;
|
|
1239
|
+
data.last_refined_at = 'not-yet-refined';
|
|
1086
1240
|
data.infrastructure = [];
|
|
1087
1241
|
if (data.uses_docker) data.infrastructure.push('docker');
|
|
1088
1242
|
if (data.cloud !== 'none') data.infrastructure.push(data.cloud);
|
|
@@ -1118,16 +1272,6 @@ async function main() {
|
|
|
1118
1272
|
fs.writeFileSync(claudeMdPath, generateClaudeMd(data), 'utf8');
|
|
1119
1273
|
console.log(` ${pc.green('โ')} Generated ${pc.bold('CLAUDE.md')}`);
|
|
1120
1274
|
|
|
1121
|
-
if (!assumeYes) {
|
|
1122
|
-
const aiRefineAnswer = await askQuestions({
|
|
1123
|
-
type: 'confirm',
|
|
1124
|
-
name: 'ai_refine',
|
|
1125
|
-
message: 'Do you want an AI refinement prompt for PROJECT_CONTEXT.md?',
|
|
1126
|
-
initial: useScan
|
|
1127
|
-
});
|
|
1128
|
-
requestAiRefinement = Boolean(aiRefineAnswer.ai_refine);
|
|
1129
|
-
}
|
|
1130
|
-
|
|
1131
1275
|
let aiRefinementPath = null;
|
|
1132
1276
|
if (requestAiRefinement) {
|
|
1133
1277
|
aiRefinementPath = path.join(targetDir, 'AI_CONTEXT_REFINEMENT.md');
|
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/rules/GEMINI.md
CHANGED
|
@@ -10,16 +10,19 @@ trigger: always_on
|
|
|
10
10
|
|
|
11
11
|
## MANDATORY FIRST ACTION
|
|
12
12
|
|
|
13
|
-
**At session start โ before anything else:**
|
|
14
|
-
|
|
15
|
-
```
|
|
16
|
-
1. Read PROJECT_CONTEXT.md (if exists) โ load stack, agents, constraints
|
|
17
|
-
2. If missing โ run @[skills/project-onboarding] to create it
|
|
18
|
-
3.
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
13
|
+
**At session start โ before anything else:**
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
1. Read PROJECT_CONTEXT.md (if exists) โ load stack, agents, constraints
|
|
17
|
+
2. If missing โ run @[skills/project-onboarding] to create it
|
|
18
|
+
3. Read PROJECT_CONTEXT context.status and context.needs_ai_refinement
|
|
19
|
+
4. If needs_ai_refinement = true โ scan project with AI, update PROJECT_CONTEXT.md, set status=refined and needs_ai_refinement=false
|
|
20
|
+
5. Confirm: "Context loaded: [Project Name] | Stack: [Stack] | Ready."
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
> All routing decisions below depend on PROJECT_CONTEXT.md.
|
|
24
|
+
> Without it, ask user to run: `python scripts/init.py`
|
|
25
|
+
> Baseline context must be refined once when `context.needs_ai_refinement = true`.
|
|
23
26
|
|
|
24
27
|
---
|
|
25
28
|
|
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,34 +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
|
-
|
|
385
|
+
evidence_if(path_exists(app_root, "manage.py"), "found manage.py", 2),
|
|
386
|
+
evidence_if(path_exists(app_root, "templates"), "found templates/", 1),
|
|
387
|
+
]), "kind": "backend", "project_type": "web-app", "language": "python"},
|
|
320
388
|
{**build_detection("flask", [
|
|
321
389
|
evidence_if(text_matches(requirements, r"\bflask\b") or text_matches(pyproject, r"\bflask\b"), "Python deps -> flask", 4),
|
|
322
390
|
]), "kind": "backend", "project_type": "api", "language": "python"},
|
|
323
391
|
{**build_detection("laravel", [
|
|
324
392
|
evidence_if(has_composer_dependency(composer_dep_names, "laravel/framework"), "composer.json -> laravel/framework", 4),
|
|
325
|
-
evidence_if(
|
|
326
|
-
|
|
393
|
+
evidence_if(path_exists(app_root, "artisan"), "found artisan", 3),
|
|
394
|
+
evidence_if(path_exists(app_root, "resources", "views"), "found resources/views", 1),
|
|
395
|
+
]), "kind": "backend", "project_type": "web-app", "language": "php"},
|
|
327
396
|
{**build_detection("cakephp", [
|
|
328
397
|
evidence_if(has_composer_dependency(composer_dep_names, "cakephp/cakephp"), "composer.json -> cakephp/cakephp", 4),
|
|
329
|
-
evidence_if(
|
|
330
|
-
evidence_if(
|
|
331
|
-
evidence_if(
|
|
332
|
-
evidence_if(
|
|
333
|
-
evidence_if(
|
|
334
|
-
evidence_if(
|
|
335
|
-
evidence_if(
|
|
336
|
-
|
|
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),
|
|
407
|
+
]), "kind": "backend", "project_type": "web-app", "language": "php"},
|
|
337
408
|
{**build_detection("symfony", [
|
|
338
409
|
evidence_if(has_composer_dependency(composer_dep_names, "symfony/framework-bundle"), "composer.json -> symfony/framework-bundle", 4),
|
|
339
|
-
evidence_if(
|
|
340
|
-
evidence_if(
|
|
341
|
-
|
|
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),
|
|
413
|
+
]), "kind": "backend", "project_type": "web-app", "language": "php"},
|
|
342
414
|
{**build_detection("codeigniter", [
|
|
343
415
|
evidence_if(has_composer_dependency(composer_dep_names, "codeigniter4/framework"), "composer.json -> codeigniter4/framework", 4),
|
|
344
|
-
evidence_if(
|
|
345
|
-
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),
|
|
346
418
|
]), "kind": "backend", "project_type": "api", "language": "php"},
|
|
347
419
|
{**build_detection("gin", [
|
|
348
420
|
evidence_if(text_matches(go_mod, r"\bgithub\.com\/gin-gonic\/gin\b"), "go.mod -> github.com/gin-gonic/gin", 4),
|
|
@@ -355,8 +427,9 @@ def scan_project():
|
|
|
355
427
|
]), "kind": "backend", "project_type": "api", "language": "go"},
|
|
356
428
|
{**build_detection("rails", [
|
|
357
429
|
evidence_if(text_matches(gemfile, r'^\s*gem\s+["\']rails["\']'), "Gemfile -> rails", 4),
|
|
358
|
-
evidence_if(
|
|
359
|
-
|
|
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),
|
|
432
|
+
]), "kind": "backend", "project_type": "web-app", "language": "ruby"},
|
|
360
433
|
{**build_detection("spring", [
|
|
361
434
|
evidence_if(text_matches(pom, r"spring-boot") or text_matches(gradle, r"spring-boot"), "Java build -> spring-boot", 4),
|
|
362
435
|
]), "kind": "backend", "project_type": "api", "language": "java"},
|
|
@@ -366,8 +439,8 @@ def scan_project():
|
|
|
366
439
|
]), "kind": "backend", "project_type": "api", "language": "csharp"},
|
|
367
440
|
{**build_detection("tauri", [
|
|
368
441
|
evidence_if(has_dependency(dep_names, "@tauri-apps/api"), "package.json -> @tauri-apps/api", 4),
|
|
369
|
-
evidence_if(
|
|
370
|
-
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),
|
|
371
444
|
]), "kind": "desktop", "project_type": "desktop", "language": "rust"},
|
|
372
445
|
{**build_detection("axum", [
|
|
373
446
|
evidence_if(text_matches(cargo, r"\baxum\b"), "Cargo.toml -> axum", 4),
|
|
@@ -404,9 +477,10 @@ def scan_project():
|
|
|
404
477
|
if any(item["project_type"] == "desktop" for item in framework_details): project_type = "desktop"
|
|
405
478
|
elif any(item["project_type"] == "mobile" for item in framework_details): project_type = "mobile"
|
|
406
479
|
elif any(item["kind"] == "frontend" for item in framework_details) and any(item["kind"] == "backend" for item in framework_details): project_type = "fullstack"
|
|
480
|
+
elif any(item["project_type"] == "web-app" for item in framework_details): project_type = "web-app"
|
|
407
481
|
elif any(item["kind"] == "backend" for item in framework_details): project_type = "api"
|
|
408
482
|
elif any(item["kind"] == "frontend" for item in framework_details): project_type = "web-app"
|
|
409
|
-
elif
|
|
483
|
+
elif path_exists(app_root, "package.json") and pkg.get("bin"): project_type = "cli"
|
|
410
484
|
elif primary_language in ["php", "python", "go", "java", "ruby", "csharp", "rust"]: project_type = "api"
|
|
411
485
|
|
|
412
486
|
if has_dependency(dep_names, "pg", "postgres", "@prisma/client") or "postgres" in json.dumps(pkg).lower(): database = "postgres"
|
|
@@ -421,26 +495,26 @@ def scan_project():
|
|
|
421
495
|
elif has_dependency(dep_names, "pytest") or text_matches(requirements, r"\bpytest\b") or text_matches(pyproject, r"\bpytest\b"): test_framework = "pytest"
|
|
422
496
|
elif has_composer_dependency(composer_dep_names, "phpunit/phpunit"): test_framework = "phpunit"
|
|
423
497
|
elif has_composer_dependency(composer_dep_names, "pestphp/pest"): test_framework = "pest"
|
|
424
|
-
elif
|
|
498
|
+
elif path_exists(app_root, "go.mod"): test_framework = "go test"
|
|
425
499
|
elif text_matches(gemfile, r'^\s*gem\s+["\']rspec["\']'): test_framework = "rspec"
|
|
426
500
|
|
|
427
|
-
if
|
|
501
|
+
if repo_has_any(repo_root, [("Dockerfile",), ("docker-compose.yml",), ("docker-compose.yaml",)]):
|
|
428
502
|
infrastructure.append("docker")
|
|
429
503
|
|
|
430
|
-
if
|
|
504
|
+
if repo_has_any(repo_root, [("vercel.json",)]):
|
|
431
505
|
cloud = "vercel"
|
|
432
506
|
infrastructure.append("vercel")
|
|
433
|
-
elif
|
|
507
|
+
elif repo_has_any(repo_root, [("fly.toml",)]):
|
|
434
508
|
cloud = "fly"
|
|
435
509
|
infrastructure.append("fly")
|
|
436
|
-
elif
|
|
510
|
+
elif repo_has_any(repo_root, [("railway.json",), ("railway.toml",)]):
|
|
437
511
|
cloud = "railway"
|
|
438
512
|
infrastructure.append("railway")
|
|
439
513
|
|
|
440
|
-
if
|
|
441
|
-
elif
|
|
442
|
-
elif
|
|
443
|
-
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"
|
|
444
518
|
|
|
445
519
|
scan_evidence = []
|
|
446
520
|
if language_detections:
|
|
@@ -449,6 +523,7 @@ def scan_project():
|
|
|
449
523
|
|
|
450
524
|
return {
|
|
451
525
|
"project_name": Path.cwd().name,
|
|
526
|
+
"app_root": str(app_root.relative_to(repo_root)) if app_root != repo_root else ".",
|
|
452
527
|
"description": pkg.get("description") or composer.get("description", "A software project"),
|
|
453
528
|
"project_type": project_type if project_type in PROJECT_TYPES_ALL else "web-app",
|
|
454
529
|
"primary_language": primary_language if primary_language in LANGUAGES_ALL else "typescript",
|
|
@@ -456,7 +531,7 @@ def scan_project():
|
|
|
456
531
|
"framework_details": framework_details,
|
|
457
532
|
"scan_evidence": scan_evidence,
|
|
458
533
|
"database": database if database in DATABASES_ALL else "none",
|
|
459
|
-
"package_manager": detect_package_manager(),
|
|
534
|
+
"package_manager": detect_package_manager(app_root),
|
|
460
535
|
"uses_docker": "docker" in infrastructure,
|
|
461
536
|
"cloud": cloud,
|
|
462
537
|
"infrastructure": uniq(infrastructure),
|
|
@@ -539,21 +614,33 @@ These detections were inferred from the current codebase during `init --scan`:
|
|
|
539
614
|
# Generated by thachvd-kit init wizard on {datetime.now().strftime('%Y-%m-%d')}
|
|
540
615
|
# Edit freely โ this is your project's brain.
|
|
541
616
|
|
|
542
|
-
## Project
|
|
543
|
-
|
|
544
|
-
- **Name:** {data['project_name']}
|
|
545
|
-
- **Description:** {data['description']}
|
|
546
|
-
- **Type:** {data['project_type']}
|
|
547
|
-
|
|
548
|
-
---
|
|
617
|
+
## Project
|
|
618
|
+
|
|
619
|
+
- **Name:** {data['project_name']}
|
|
620
|
+
- **Description:** {data['description']}
|
|
621
|
+
- **Type:** {data['project_type']}
|
|
622
|
+
|
|
623
|
+
---
|
|
624
|
+
|
|
625
|
+
## Context Status
|
|
626
|
+
|
|
627
|
+
```yaml
|
|
628
|
+
context:
|
|
629
|
+
status: {data.get('context_status', 'baseline')}
|
|
630
|
+
needs_ai_refinement: {'true' if data.get('needs_ai_refinement') else 'false'}
|
|
631
|
+
last_refined_at: {data.get('last_refined_at', 'not-yet-refined')}
|
|
632
|
+
```
|
|
633
|
+
|
|
634
|
+
---
|
|
549
635
|
|
|
550
636
|
## Stack
|
|
551
637
|
|
|
552
638
|
```yaml
|
|
553
|
-
stack:
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
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')}
|
|
557
644
|
infrastructure: [{', '.join(infrastructure)}]
|
|
558
645
|
package_manager: {data.get('package_manager', 'auto-detect')}
|
|
559
646
|
test_framework: {data.get('test_framework', 'auto-detect')}
|
|
@@ -699,6 +786,20 @@ When done, summarize:
|
|
|
699
786
|
- what is still inferred
|
|
700
787
|
- which files provided the strongest evidence
|
|
701
788
|
"""
|
|
789
|
+
|
|
790
|
+
def format_scan_preview_lines(scanned):
|
|
791
|
+
return [
|
|
792
|
+
("App root", scanned.get("app_root", ".")),
|
|
793
|
+
("Project type", scanned.get("project_type", "unknown")),
|
|
794
|
+
("Primary language", scanned.get("primary_language", "unknown")),
|
|
795
|
+
("Frameworks", ", ".join(scanned.get("frameworks", [])) or "no framework"),
|
|
796
|
+
("Database", scanned.get("database", "none")),
|
|
797
|
+
("Package manager", scanned.get("package_manager", "auto-detect")),
|
|
798
|
+
("Test framework", scanned.get("test_framework", "auto-detect")),
|
|
799
|
+
("Cloud", scanned.get("cloud", "none")),
|
|
800
|
+
("Infrastructure", ", ".join(scanned.get("infrastructure", [])) or "none"),
|
|
801
|
+
("CI/CD", scanned.get("ci_cd", "none")),
|
|
802
|
+
]
|
|
702
803
|
|
|
703
804
|
# โโโ Main wizard โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
704
805
|
|
|
@@ -739,8 +840,10 @@ def main():
|
|
|
739
840
|
review_scanned_fields = not use_scan
|
|
740
841
|
if use_scan:
|
|
741
842
|
print(f" {dim('Scan mode: detecting stack from current project files...')}")
|
|
742
|
-
print(f" {bold('
|
|
743
|
-
|
|
843
|
+
print(f" {bold('Detected context:')}")
|
|
844
|
+
for label, value in format_scan_preview_lines(scanned):
|
|
845
|
+
print(f" ยท {label}: {value}")
|
|
846
|
+
print()
|
|
744
847
|
if scanned.get("scan_evidence"):
|
|
745
848
|
print(f" {bold('Evidence:')}")
|
|
746
849
|
for line in scanned["scan_evidence"]:
|
|
@@ -830,6 +933,10 @@ def main():
|
|
|
830
933
|
data["code_style"] = data.get("code_style", (scanned or {}).get("code_style", "project-standard"))
|
|
831
934
|
data["framework_details"] = data.get("framework_details", (scanned or {}).get("framework_details", []))
|
|
832
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", "."))
|
|
937
|
+
data["context_status"] = data.get("context_status", "baseline")
|
|
938
|
+
data["needs_ai_refinement"] = data.get("needs_ai_refinement", False)
|
|
939
|
+
data["last_refined_at"] = data.get("last_refined_at", "not-yet-refined")
|
|
833
940
|
data["architecture_notes"] = data.get("architecture_notes", (scanned or {}).get("architecture_notes", "<!-- Add notes -->"))
|
|
834
941
|
data["current_phase"] = data.get("current_phase", (scanned or {}).get("current_phase", "Initial setup"))
|
|
835
942
|
data["next_phase"] = data.get("next_phase", (scanned or {}).get("next_phase", "โ"))
|
|
@@ -838,6 +945,9 @@ def main():
|
|
|
838
945
|
if data["uses_docker"]: data["infrastructure"].append("docker")
|
|
839
946
|
if data["cloud"] != "none": data["infrastructure"].append(data["cloud"])
|
|
840
947
|
|
|
948
|
+
if not args.yes:
|
|
949
|
+
data["needs_ai_refinement"] = ask_bool("Should the agent refine PROJECT_CONTEXT.md on first session start?", default=use_scan)
|
|
950
|
+
|
|
841
951
|
# โโ Generate files โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
842
952
|
section("Generating Files")
|
|
843
953
|
|
|
@@ -853,7 +963,7 @@ def main():
|
|
|
853
963
|
print(f" {green('โ')} {bold('CLAUDE.md')} generated (Claude Code / Codex)")
|
|
854
964
|
|
|
855
965
|
ai_refine_path = None
|
|
856
|
-
if
|
|
966
|
+
if data.get("needs_ai_refinement"):
|
|
857
967
|
ai_refine_path = Path("AI_CONTEXT_REFINEMENT.md")
|
|
858
968
|
ai_refine_path.write_text(generate_ai_context_refinement_prompt(data), encoding="utf-8")
|
|
859
969
|
print(f" {green('โ')} {bold('AI_CONTEXT_REFINEMENT.md')} generated")
|