thachvd-kit 1.0.2 → 1.0.4
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 +1 -0
- package/bin/cli.js +533 -96
- package/package.json +5 -2
- package/scripts/init.py +313 -101
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "thachvd-kit",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "Universal Agent Kit for Antigravity, Claude Code, and OpenCode",
|
|
5
5
|
"bin": {
|
|
6
6
|
"thachvd-kit": "./bin/cli.js"
|
|
@@ -22,7 +22,10 @@
|
|
|
22
22
|
"node": ">=16.7"
|
|
23
23
|
},
|
|
24
24
|
"scripts": {
|
|
25
|
-
"test": "echo \"Error: no test specified\" && exit 1"
|
|
25
|
+
"test": "echo \"Error: no test specified\" && exit 1",
|
|
26
|
+
"release:patch": "npm version patch",
|
|
27
|
+
"release:dry-run": "npm pack --dry-run",
|
|
28
|
+
"release:publish": "npm publish"
|
|
26
29
|
},
|
|
27
30
|
"keywords": [
|
|
28
31
|
"ai",
|
package/scripts/init.py
CHANGED
|
@@ -13,6 +13,7 @@ import sys
|
|
|
13
13
|
import os
|
|
14
14
|
import argparse
|
|
15
15
|
import json
|
|
16
|
+
import re
|
|
16
17
|
from pathlib import Path
|
|
17
18
|
from datetime import datetime
|
|
18
19
|
|
|
@@ -35,12 +36,13 @@ def ask(prompt, default="", choices=None):
|
|
|
35
36
|
value = input(f" {bold('→')} {prompt}{display_default}: ").strip()
|
|
36
37
|
return value if value else default
|
|
37
38
|
|
|
38
|
-
def ask_multi(prompt, choices, default=None):
|
|
39
|
-
"""Prompt for comma-separated selection from choices."""
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
39
|
+
def ask_multi(prompt, choices, default=None):
|
|
40
|
+
"""Prompt for comma-separated selection from choices."""
|
|
41
|
+
display_default = ", ".join(default) if isinstance(default, list) else (default or "none")
|
|
42
|
+
print(f"\n {cyan('Available:')} {', '.join(f'{i+1}) {c}' for i, c in enumerate(choices))}")
|
|
43
|
+
raw = input(f" {bold('→')} {prompt} (numbers or names, comma-separated) [{dim(display_default)}]: ").strip()
|
|
44
|
+
if not raw:
|
|
45
|
+
return default or []
|
|
44
46
|
selected = []
|
|
45
47
|
for part in raw.replace(",", " ").split():
|
|
46
48
|
part = part.strip()
|
|
@@ -71,32 +73,45 @@ def section(title):
|
|
|
71
73
|
|
|
72
74
|
# ─── Skill resolver ──────────────────────────────────────────────────────────
|
|
73
75
|
|
|
74
|
-
LANG_SKILLS = {
|
|
75
|
-
"typescript": ["nodejs-best-practices", "api-design"],
|
|
76
|
-
"javascript": ["nodejs-best-practices", "api-design"],
|
|
77
|
-
"python": ["python-patterns"],
|
|
78
|
-
"go": ["golang-patterns", "golang-testing"],
|
|
79
|
-
"php": ["
|
|
80
|
-
"rust": ["rust-pro"],
|
|
81
|
-
"java": ["api-design"],
|
|
82
|
-
"
|
|
83
|
-
"
|
|
84
|
-
"
|
|
85
|
-
|
|
76
|
+
LANG_SKILLS = {
|
|
77
|
+
"typescript": ["nodejs-best-practices", "api-design"],
|
|
78
|
+
"javascript": ["nodejs-best-practices", "api-design"],
|
|
79
|
+
"python": ["python-patterns"],
|
|
80
|
+
"go": ["golang-patterns", "golang-testing"],
|
|
81
|
+
"php": ["api-design"],
|
|
82
|
+
"rust": ["rust-pro"],
|
|
83
|
+
"java": ["api-design"],
|
|
84
|
+
"ruby": ["api-design"],
|
|
85
|
+
"swift": ["mobile-design"],
|
|
86
|
+
"kotlin": ["mobile-design"],
|
|
87
|
+
"csharp": ["api-design"],
|
|
88
|
+
}
|
|
86
89
|
|
|
87
90
|
FRAMEWORK_SKILLS = {
|
|
88
91
|
"vue": ["frontend-design", "tailwind-patterns"],
|
|
89
92
|
"react": ["react-frontend", "frontend-design"],
|
|
90
93
|
"next": ["nextjs-react-expert", "frontend-design"],
|
|
91
|
-
"nuxt": ["frontend-design"],
|
|
92
|
-
"svelte": ["frontend-design"],
|
|
93
|
-
"
|
|
94
|
-
"
|
|
95
|
-
"
|
|
96
|
-
"
|
|
97
|
-
"
|
|
98
|
-
"
|
|
94
|
+
"nuxt": ["frontend-design"],
|
|
95
|
+
"svelte": ["frontend-design"],
|
|
96
|
+
"angular": ["frontend-design"],
|
|
97
|
+
"laravel": ["laravel-patterns", "laravel-security", "laravel-tdd"],
|
|
98
|
+
"cakephp": ["api-design"],
|
|
99
|
+
"symfony": ["api-design"],
|
|
100
|
+
"codeigniter": ["api-design"],
|
|
101
|
+
"express": ["nodejs-best-practices", "api-design"],
|
|
102
|
+
"fastapi": ["python-patterns", "api-design"],
|
|
103
|
+
"django": ["python-patterns", "api-design"],
|
|
104
|
+
"flask": ["python-patterns", "api-design"],
|
|
105
|
+
"gin": ["golang-patterns", "golang-testing"],
|
|
106
|
+
"fiber": ["golang-patterns", "golang-testing"],
|
|
107
|
+
"echo": ["golang-patterns", "golang-testing"],
|
|
108
|
+
"nestjs": ["nodejs-best-practices", "api-design"],
|
|
99
109
|
"tauri": ["rust-pro", "desktop-design"],
|
|
110
|
+
"axum": ["rust-pro", "api-design"],
|
|
111
|
+
"actix": ["rust-pro", "api-design"],
|
|
112
|
+
"rails": ["api-design"],
|
|
113
|
+
"spring": ["api-design"],
|
|
114
|
+
"aspnet": ["api-design"],
|
|
100
115
|
"flutter": ["mobile-design"],
|
|
101
116
|
"react-native": ["mobile-design"],
|
|
102
117
|
}
|
|
@@ -128,14 +143,19 @@ def resolve_skills(data):
|
|
|
128
143
|
return sorted(skills)
|
|
129
144
|
|
|
130
145
|
FRAMEWORKS_ALL = ["vue", "react", "next", "nuxt", "svelte", "angular",
|
|
131
|
-
"laravel", "
|
|
132
|
-
"
|
|
133
|
-
|
|
146
|
+
"laravel", "cakephp", "symfony", "codeigniter",
|
|
147
|
+
"express", "fastapi", "django", "flask", "gin", "fiber", "echo", "nestjs",
|
|
148
|
+
"rails", "spring", "aspnet",
|
|
149
|
+
"tauri", "axum", "actix", "flutter", "react-native"]
|
|
150
|
+
LANGUAGES_ALL = ["typescript", "javascript", "python", "go", "php", "rust", "java", "ruby", "swift", "kotlin", "csharp", "c", "cpp"]
|
|
134
151
|
PROJECT_TYPES_ALL = ["web-app", "api", "mobile", "desktop", "cli", "library", "fullstack"]
|
|
135
152
|
DATABASES_ALL = ["postgres", "mysql", "sqlite", "mongodb", "redis", "dynamodb", "supabase", "prisma", "none"]
|
|
136
153
|
PACKAGE_MANAGERS_ALL = ["npm", "pnpm", "yarn", "bun", "pip", "uv", "poetry", "go mod", "cargo", "composer", "auto-detect"]
|
|
137
154
|
TEST_FRAMEWORKS_ALL = ["jest", "vitest", "pytest", "go test", "phpunit", "pest", "rspec", "junit", "auto-detect"]
|
|
138
155
|
CLOUDS_ALL = ["aws", "gcp", "azure", "vercel", "fly", "railway", "digitalocean", "none"]
|
|
156
|
+
FRONTEND_FRAMEWORKS = ["vue", "react", "next", "nuxt", "svelte", "angular"]
|
|
157
|
+
BACKEND_FRAMEWORKS = ["laravel", "cakephp", "symfony", "codeigniter", "express", "fastapi", "django", "flask", "gin", "fiber", "echo", "nestjs", "rails", "spring", "aspnet", "axum", "actix"]
|
|
158
|
+
MOBILE_DESKTOP_FRAMEWORKS = ["tauri", "flutter", "react-native"]
|
|
139
159
|
|
|
140
160
|
def read_text_if_exists(*parts):
|
|
141
161
|
path = Path.cwd().joinpath(*parts)
|
|
@@ -168,75 +188,236 @@ def detect_package_manager():
|
|
|
168
188
|
def uniq(values):
|
|
169
189
|
return list(dict.fromkeys(v for v in values if v))
|
|
170
190
|
|
|
191
|
+
def evidence_if(condition, label, weight=1):
|
|
192
|
+
return {"label": label, "weight": weight} if condition else None
|
|
193
|
+
|
|
194
|
+
def build_detection(id, evidence, **meta):
|
|
195
|
+
hits = [hit for hit in evidence if hit]
|
|
196
|
+
score = sum(hit["weight"] for hit in hits)
|
|
197
|
+
return {
|
|
198
|
+
"id": id,
|
|
199
|
+
"score": score,
|
|
200
|
+
"evidence": [hit["label"] for hit in hits],
|
|
201
|
+
**meta,
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
def confidence_label(score):
|
|
205
|
+
if score >= 6: return "high"
|
|
206
|
+
if score >= 3: return "medium"
|
|
207
|
+
return "low"
|
|
208
|
+
|
|
209
|
+
def has_dependency(dep_names, *names):
|
|
210
|
+
return any(name in dep_names for name in names)
|
|
211
|
+
|
|
212
|
+
def has_composer_dependency(composer_dep_names, *names):
|
|
213
|
+
return any(name in composer_dep_names for name in names)
|
|
214
|
+
|
|
215
|
+
def text_matches(text, pattern):
|
|
216
|
+
return bool(text and re.search(pattern, text, re.IGNORECASE | re.MULTILINE))
|
|
217
|
+
|
|
171
218
|
def scan_project():
|
|
172
219
|
pkg = read_json_if_exists("package.json") or {}
|
|
220
|
+
composer = read_json_if_exists("composer.json") or {}
|
|
221
|
+
root_entries = [p.name for p in Path.cwd().iterdir()] if Path.cwd().exists() else []
|
|
222
|
+
root_csproj = next((name for name in root_entries if name.endswith(".csproj")), "")
|
|
173
223
|
deps = {}
|
|
174
224
|
for key in ("dependencies", "devDependencies", "peerDependencies"):
|
|
175
225
|
deps.update(pkg.get(key, {}) or {})
|
|
176
226
|
dep_names = set(deps.keys())
|
|
177
|
-
|
|
227
|
+
composer_deps = {}
|
|
228
|
+
for key in ("require", "require-dev"):
|
|
229
|
+
composer_deps.update(composer.get(key, {}) or {})
|
|
230
|
+
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")
|
|
241
|
+
|
|
242
|
+
language_detections = [
|
|
243
|
+
build_detection("typescript", [
|
|
244
|
+
evidence_if(Path("tsconfig.json").exists(), "found tsconfig.json", 4),
|
|
245
|
+
evidence_if(has_dependency(dep_names, "typescript", "ts-node", "tsx"), "package.json -> typescript toolchain", 2),
|
|
246
|
+
]),
|
|
247
|
+
build_detection("javascript", [
|
|
248
|
+
evidence_if(Path("package.json").exists(), "found package.json", 2),
|
|
249
|
+
evidence_if(bool(dep_names), "package.json -> JavaScript dependency graph", 1),
|
|
250
|
+
]),
|
|
251
|
+
build_detection("python", [
|
|
252
|
+
evidence_if(Path("requirements.txt").exists(), "found requirements.txt", 3),
|
|
253
|
+
evidence_if(Path("pyproject.toml").exists(), "found pyproject.toml", 2),
|
|
254
|
+
evidence_if(text_matches(pyproject, r"\[tool\.(poetry|uv|hatch|pdm)\]"), "pyproject.toml -> Python tool section", 2),
|
|
255
|
+
]),
|
|
256
|
+
build_detection("go", [
|
|
257
|
+
evidence_if(Path("go.mod").exists(), "found go.mod", 4),
|
|
258
|
+
]),
|
|
259
|
+
build_detection("php", [
|
|
260
|
+
evidence_if(Path("composer.json").exists(), "found composer.json", 4),
|
|
261
|
+
evidence_if(bool(composer_dep_names), "composer.json -> PHP dependency graph", 1),
|
|
262
|
+
]),
|
|
263
|
+
build_detection("rust", [
|
|
264
|
+
evidence_if(Path("Cargo.toml").exists(), "found Cargo.toml", 4),
|
|
265
|
+
evidence_if(Path("src-tauri/Cargo.toml").exists(), "found src-tauri/Cargo.toml", 5),
|
|
266
|
+
]),
|
|
267
|
+
build_detection("java", [
|
|
268
|
+
evidence_if(Path("pom.xml").exists(), "found pom.xml", 4),
|
|
269
|
+
evidence_if(Path("build.gradle").exists() or Path("build.gradle.kts").exists(), "found Gradle build file", 3),
|
|
270
|
+
]),
|
|
271
|
+
build_detection("ruby", [
|
|
272
|
+
evidence_if(Path("Gemfile").exists(), "found Gemfile", 4),
|
|
273
|
+
evidence_if(Path("config.ru").exists(), "found config.ru", 1),
|
|
274
|
+
]),
|
|
275
|
+
build_detection("csharp", [
|
|
276
|
+
evidence_if(any(name.endswith(".sln") or name.endswith(".csproj") for name in root_entries), "found .sln/.csproj", 4),
|
|
277
|
+
evidence_if(Path("Program.cs").exists(), "found Program.cs", 1),
|
|
278
|
+
]),
|
|
279
|
+
]
|
|
280
|
+
language_detections = [item for item in language_detections if item["score"] >= 2]
|
|
281
|
+
language_detections.sort(key=lambda item: item["score"], reverse=True)
|
|
282
|
+
|
|
283
|
+
framework_details = [
|
|
284
|
+
{**build_detection("next", [
|
|
285
|
+
evidence_if(has_dependency(dep_names, "next"), "package.json -> next", 4),
|
|
286
|
+
evidence_if(Path("next.config.js").exists() or Path("next.config.mjs").exists() or Path("next.config.ts").exists(), "found next.config.*", 3),
|
|
287
|
+
evidence_if(Path("app").exists() or Path("pages").exists(), "found app/ or pages/", 1),
|
|
288
|
+
]), "kind": "frontend", "project_type": "web-app"},
|
|
289
|
+
{**build_detection("react", [
|
|
290
|
+
evidence_if(has_dependency(dep_names, "react"), "package.json -> react", 4),
|
|
291
|
+
evidence_if(has_dependency(dep_names, "react-dom"), "package.json -> react-dom", 2),
|
|
292
|
+
]), "kind": "frontend", "project_type": "web-app"},
|
|
293
|
+
{**build_detection("vue", [
|
|
294
|
+
evidence_if(has_dependency(dep_names, "vue"), "package.json -> vue", 4),
|
|
295
|
+
]), "kind": "frontend", "project_type": "web-app"},
|
|
296
|
+
{**build_detection("nuxt", [
|
|
297
|
+
evidence_if(has_dependency(dep_names, "nuxt"), "package.json -> nuxt", 4),
|
|
298
|
+
evidence_if(Path("nuxt.config.ts").exists() or Path("nuxt.config.js").exists(), "found nuxt.config.*", 3),
|
|
299
|
+
]), "kind": "frontend", "project_type": "web-app"},
|
|
300
|
+
{**build_detection("svelte", [
|
|
301
|
+
evidence_if(has_dependency(dep_names, "svelte", "@sveltejs/kit"), "package.json -> svelte", 4),
|
|
302
|
+
]), "kind": "frontend", "project_type": "web-app"},
|
|
303
|
+
{**build_detection("angular", [
|
|
304
|
+
evidence_if(has_dependency(dep_names, "@angular/core"), "package.json -> @angular/core", 4),
|
|
305
|
+
evidence_if(Path("angular.json").exists(), "found angular.json", 3),
|
|
306
|
+
]), "kind": "frontend", "project_type": "web-app"},
|
|
307
|
+
{**build_detection("express", [
|
|
308
|
+
evidence_if(has_dependency(dep_names, "express"), "package.json -> express", 4),
|
|
309
|
+
]), "kind": "backend", "project_type": "api", "language": "javascript"},
|
|
310
|
+
{**build_detection("nestjs", [
|
|
311
|
+
evidence_if(has_dependency(dep_names, "@nestjs/core"), "package.json -> @nestjs/core", 4),
|
|
312
|
+
]), "kind": "backend", "project_type": "api", "language": "javascript"},
|
|
313
|
+
{**build_detection("fastapi", [
|
|
314
|
+
evidence_if(text_matches(requirements, r"\bfastapi\b") or text_matches(pyproject, r"\bfastapi\b"), "Python deps -> fastapi", 4),
|
|
315
|
+
]), "kind": "backend", "project_type": "api", "language": "python"},
|
|
316
|
+
{**build_detection("django", [
|
|
317
|
+
evidence_if(text_matches(requirements, r"\bdjango\b") or text_matches(pyproject, r"\bdjango\b"), "Python deps -> django", 4),
|
|
318
|
+
evidence_if(Path("manage.py").exists(), "found manage.py", 2),
|
|
319
|
+
]), "kind": "backend", "project_type": "api", "language": "python"},
|
|
320
|
+
{**build_detection("flask", [
|
|
321
|
+
evidence_if(text_matches(requirements, r"\bflask\b") or text_matches(pyproject, r"\bflask\b"), "Python deps -> flask", 4),
|
|
322
|
+
]), "kind": "backend", "project_type": "api", "language": "python"},
|
|
323
|
+
{**build_detection("laravel", [
|
|
324
|
+
evidence_if(has_composer_dependency(composer_dep_names, "laravel/framework"), "composer.json -> laravel/framework", 4),
|
|
325
|
+
evidence_if(Path("artisan").exists(), "found artisan", 3),
|
|
326
|
+
]), "kind": "backend", "project_type": "api", "language": "php"},
|
|
327
|
+
{**build_detection("cakephp", [
|
|
328
|
+
evidence_if(has_composer_dependency(composer_dep_names, "cakephp/cakephp"), "composer.json -> cakephp/cakephp", 4),
|
|
329
|
+
evidence_if(Path("bin/cake").exists(), "found bin/cake", 3),
|
|
330
|
+
evidence_if(Path("config/app.php").exists(), "found config/app.php", 1),
|
|
331
|
+
]), "kind": "backend", "project_type": "api", "language": "php"},
|
|
332
|
+
{**build_detection("symfony", [
|
|
333
|
+
evidence_if(has_composer_dependency(composer_dep_names, "symfony/framework-bundle"), "composer.json -> symfony/framework-bundle", 4),
|
|
334
|
+
evidence_if(Path("bin/console").exists(), "found bin/console", 2),
|
|
335
|
+
evidence_if(Path("config/bundles.php").exists(), "found config/bundles.php", 1),
|
|
336
|
+
]), "kind": "backend", "project_type": "api", "language": "php"},
|
|
337
|
+
{**build_detection("codeigniter", [
|
|
338
|
+
evidence_if(has_composer_dependency(composer_dep_names, "codeigniter4/framework"), "composer.json -> codeigniter4/framework", 4),
|
|
339
|
+
evidence_if(Path("spark").exists(), "found spark", 2),
|
|
340
|
+
evidence_if(Path("app/Config/App.php").exists(), "found app/Config/App.php", 1),
|
|
341
|
+
]), "kind": "backend", "project_type": "api", "language": "php"},
|
|
342
|
+
{**build_detection("gin", [
|
|
343
|
+
evidence_if(text_matches(go_mod, r"\bgithub\.com\/gin-gonic\/gin\b"), "go.mod -> github.com/gin-gonic/gin", 4),
|
|
344
|
+
]), "kind": "backend", "project_type": "api", "language": "go"},
|
|
345
|
+
{**build_detection("fiber", [
|
|
346
|
+
evidence_if(text_matches(go_mod, r"\bgithub\.com\/gofiber\/fiber\b"), "go.mod -> github.com/gofiber/fiber", 4),
|
|
347
|
+
]), "kind": "backend", "project_type": "api", "language": "go"},
|
|
348
|
+
{**build_detection("echo", [
|
|
349
|
+
evidence_if(text_matches(go_mod, r"\bgithub\.com\/labstack\/echo\b"), "go.mod -> github.com/labstack/echo", 4),
|
|
350
|
+
]), "kind": "backend", "project_type": "api", "language": "go"},
|
|
351
|
+
{**build_detection("rails", [
|
|
352
|
+
evidence_if(text_matches(gemfile, r'^\s*gem\s+["\']rails["\']'), "Gemfile -> rails", 4),
|
|
353
|
+
evidence_if(Path("bin/rails").exists(), "found bin/rails", 2),
|
|
354
|
+
]), "kind": "backend", "project_type": "api", "language": "ruby"},
|
|
355
|
+
{**build_detection("spring", [
|
|
356
|
+
evidence_if(text_matches(pom, r"spring-boot") or text_matches(gradle, r"spring-boot"), "Java build -> spring-boot", 4),
|
|
357
|
+
]), "kind": "backend", "project_type": "api", "language": "java"},
|
|
358
|
+
{**build_detection("aspnet", [
|
|
359
|
+
evidence_if(text_matches(csproj, r"Microsoft\.NET\.Sdk\.Web"), ".csproj -> Microsoft.NET.Sdk.Web", 4),
|
|
360
|
+
evidence_if(text_matches(program_cs, r"WebApplication\.CreateBuilder"), "Program.cs -> WebApplication.CreateBuilder", 2),
|
|
361
|
+
]), "kind": "backend", "project_type": "api", "language": "csharp"},
|
|
362
|
+
{**build_detection("tauri", [
|
|
363
|
+
evidence_if(has_dependency(dep_names, "@tauri-apps/api"), "package.json -> @tauri-apps/api", 4),
|
|
364
|
+
evidence_if(Path("src-tauri/tauri.conf.json").exists() or Path("src-tauri/tauri.conf.json5").exists(), "found src-tauri/tauri.conf.*", 4),
|
|
365
|
+
evidence_if(Path("src-tauri/Cargo.toml").exists(), "found src-tauri/Cargo.toml", 2),
|
|
366
|
+
]), "kind": "desktop", "project_type": "desktop", "language": "rust"},
|
|
367
|
+
{**build_detection("axum", [
|
|
368
|
+
evidence_if(text_matches(cargo, r"\baxum\b"), "Cargo.toml -> axum", 4),
|
|
369
|
+
]), "kind": "backend", "project_type": "api", "language": "rust"},
|
|
370
|
+
{**build_detection("actix", [
|
|
371
|
+
evidence_if(text_matches(cargo, r"\bactix-web\b"), "Cargo.toml -> actix-web", 4),
|
|
372
|
+
]), "kind": "backend", "project_type": "api", "language": "rust"},
|
|
373
|
+
{**build_detection("flutter", [
|
|
374
|
+
evidence_if(text_matches(pubspec, r"^\s*flutter\s*:"), "pubspec.yaml -> flutter", 4),
|
|
375
|
+
]), "kind": "mobile", "project_type": "mobile"},
|
|
376
|
+
{**build_detection("react-native", [
|
|
377
|
+
evidence_if(has_dependency(dep_names, "react-native", "expo"), "package.json -> react-native/expo", 4),
|
|
378
|
+
]), "kind": "mobile", "project_type": "mobile"},
|
|
379
|
+
]
|
|
380
|
+
framework_details = [item for item in framework_details if item["score"] >= 3]
|
|
381
|
+
framework_details.sort(key=lambda item: item["score"], reverse=True)
|
|
382
|
+
for item in framework_details:
|
|
383
|
+
item["confidence"] = confidence_label(item["score"])
|
|
384
|
+
|
|
385
|
+
framework_language_scores = {}
|
|
386
|
+
for item in framework_details:
|
|
387
|
+
if item.get("language"):
|
|
388
|
+
framework_language_scores[item["language"]] = framework_language_scores.get(item["language"], 0) + item["score"]
|
|
389
|
+
primary_language = max(framework_language_scores, key=framework_language_scores.get) if framework_language_scores else (language_detections[0]["id"] if language_detections else "typescript")
|
|
390
|
+
frameworks = [item["id"] for item in framework_details if item["id"] in FRAMEWORKS_ALL]
|
|
391
|
+
frameworks = uniq(frameworks)
|
|
178
392
|
infrastructure = []
|
|
179
|
-
primary_language = ""
|
|
180
393
|
project_type = ""
|
|
181
394
|
database = "none"
|
|
182
395
|
test_framework = "auto-detect"
|
|
183
396
|
cloud = "none"
|
|
184
397
|
ci_cd = "none"
|
|
185
398
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
add_framework("react-native", "react-native" in dep_names or "expo" in dep_names)
|
|
207
|
-
add_framework("laravel", Path("artisan").exists())
|
|
208
|
-
add_framework("gin", "github.com/gin-gonic/gin" in go_mod.lower())
|
|
209
|
-
add_framework("fiber", "github.com/gofiber/fiber" in go_mod.lower())
|
|
210
|
-
|
|
211
|
-
if Path("tsconfig.json").exists() or "typescript" in dep_names: primary_language = "typescript"
|
|
212
|
-
elif Path("Cargo.toml").exists() or Path("src-tauri/Cargo.toml").exists(): primary_language = "rust"
|
|
213
|
-
elif Path("go.mod").exists(): primary_language = "go"
|
|
214
|
-
elif Path("requirements.txt").exists() or Path("pyproject.toml").exists(): primary_language = "python"
|
|
215
|
-
elif Path("composer.json").exists(): primary_language = "php"
|
|
216
|
-
elif dep_names or Path("package.json").exists(): primary_language = "javascript"
|
|
217
|
-
|
|
218
|
-
if "tauri" in frameworks: project_type = "desktop"
|
|
219
|
-
elif "flutter" in frameworks or "react-native" in frameworks: project_type = "mobile"
|
|
220
|
-
elif any(f in frameworks for f in ["express", "fastapi", "django", "laravel", "gin", "fiber", "nestjs"]):
|
|
221
|
-
project_type = "fullstack" if any(f in frameworks for f in ["react", "next", "vue", "nuxt", "svelte", "angular"]) else "api"
|
|
222
|
-
elif any(f in frameworks for f in ["react", "next", "vue", "nuxt", "svelte", "angular"]):
|
|
223
|
-
project_type = "web-app"
|
|
224
|
-
elif Path("package.json").exists() and pkg.get("bin"):
|
|
225
|
-
project_type = "cli"
|
|
226
|
-
|
|
227
|
-
if "pg" in dep_names or "postgres" in dep_names or "@prisma/client" in dep_names or "postgres" in json.dumps(pkg).lower(): database = "postgres"
|
|
228
|
-
elif "mysql" in dep_names or "mysql2" in dep_names: database = "mysql"
|
|
229
|
-
elif "sqlite3" in dep_names or "better-sqlite3" in dep_names or "rusqlite" in dep_names: database = "sqlite"
|
|
230
|
-
elif "mongodb" in dep_names or "mongoose" in dep_names: database = "mongodb"
|
|
231
|
-
elif "redis" in dep_names or "ioredis" in dep_names: database = "redis"
|
|
232
|
-
elif "@aws-sdk/client-dynamodb" in dep_names: database = "dynamodb"
|
|
233
|
-
|
|
234
|
-
if "vitest" in dep_names: test_framework = "vitest"
|
|
235
|
-
elif "jest" in dep_names: test_framework = "jest"
|
|
236
|
-
elif "pytest" in dep_names or "pytest" in requirements.lower(): test_framework = "pytest"
|
|
237
|
-
elif "phpunit" in dep_names: test_framework = "phpunit"
|
|
238
|
-
elif "pest" in dep_names: test_framework = "pest"
|
|
399
|
+
if any(item["project_type"] == "desktop" for item in framework_details): project_type = "desktop"
|
|
400
|
+
elif any(item["project_type"] == "mobile" for item in framework_details): project_type = "mobile"
|
|
401
|
+
elif any(item["kind"] == "frontend" for item in framework_details) and any(item["kind"] == "backend" for item in framework_details): project_type = "fullstack"
|
|
402
|
+
elif any(item["kind"] == "backend" for item in framework_details): project_type = "api"
|
|
403
|
+
elif any(item["kind"] == "frontend" for item in framework_details): project_type = "web-app"
|
|
404
|
+
elif Path("package.json").exists() and pkg.get("bin"): project_type = "cli"
|
|
405
|
+
elif primary_language in ["php", "python", "go", "java", "ruby", "csharp", "rust"]: project_type = "api"
|
|
406
|
+
|
|
407
|
+
if has_dependency(dep_names, "pg", "postgres", "@prisma/client") or "postgres" in json.dumps(pkg).lower(): database = "postgres"
|
|
408
|
+
elif has_dependency(dep_names, "mysql", "mysql2"): database = "mysql"
|
|
409
|
+
elif has_dependency(dep_names, "sqlite3", "better-sqlite3", "rusqlite"): database = "sqlite"
|
|
410
|
+
elif has_dependency(dep_names, "mongodb", "mongoose"): database = "mongodb"
|
|
411
|
+
elif has_dependency(dep_names, "redis", "ioredis"): database = "redis"
|
|
412
|
+
elif has_dependency(dep_names, "@aws-sdk/client-dynamodb"): database = "dynamodb"
|
|
413
|
+
|
|
414
|
+
if has_dependency(dep_names, "vitest"): test_framework = "vitest"
|
|
415
|
+
elif has_dependency(dep_names, "jest"): test_framework = "jest"
|
|
416
|
+
elif has_dependency(dep_names, "pytest") or text_matches(requirements, r"\bpytest\b") or text_matches(pyproject, r"\bpytest\b"): test_framework = "pytest"
|
|
417
|
+
elif has_composer_dependency(composer_dep_names, "phpunit/phpunit"): test_framework = "phpunit"
|
|
418
|
+
elif has_composer_dependency(composer_dep_names, "pestphp/pest"): test_framework = "pest"
|
|
239
419
|
elif Path("go.mod").exists(): test_framework = "go test"
|
|
420
|
+
elif text_matches(gemfile, r'^\s*gem\s+["\']rspec["\']'): test_framework = "rspec"
|
|
240
421
|
|
|
241
422
|
if Path("Dockerfile").exists() or Path("docker-compose.yml").exists() or Path("docker-compose.yaml").exists():
|
|
242
423
|
infrastructure.append("docker")
|
|
@@ -256,12 +437,19 @@ def scan_project():
|
|
|
256
437
|
elif Path(".circleci/config.yml").exists(): ci_cd = "circleci"
|
|
257
438
|
elif Path("Jenkinsfile").exists(): ci_cd = "jenkins"
|
|
258
439
|
|
|
440
|
+
scan_evidence = []
|
|
441
|
+
if language_detections:
|
|
442
|
+
scan_evidence.append(f"language:{language_detections[0]['id']} => {'; '.join(language_detections[0]['evidence'])}")
|
|
443
|
+
scan_evidence.extend(f"{item['id']} [{item['confidence']}] => {'; '.join(item['evidence'])}" for item in framework_details)
|
|
444
|
+
|
|
259
445
|
return {
|
|
260
446
|
"project_name": Path.cwd().name,
|
|
261
|
-
"description": pkg.get("description", "A software project"),
|
|
447
|
+
"description": pkg.get("description") or composer.get("description", "A software project"),
|
|
262
448
|
"project_type": project_type if project_type in PROJECT_TYPES_ALL else "web-app",
|
|
263
449
|
"primary_language": primary_language if primary_language in LANGUAGES_ALL else "typescript",
|
|
264
|
-
"frameworks":
|
|
450
|
+
"frameworks": frameworks,
|
|
451
|
+
"framework_details": framework_details,
|
|
452
|
+
"scan_evidence": scan_evidence,
|
|
265
453
|
"database": database if database in DATABASES_ALL else "none",
|
|
266
454
|
"package_manager": detect_package_manager(),
|
|
267
455
|
"uses_docker": "docker" in infrastructure,
|
|
@@ -283,7 +471,7 @@ def resolve_agent_table(data):
|
|
|
283
471
|
lang = data.get("primary_language", "").lower()
|
|
284
472
|
|
|
285
473
|
# Determine frontend agent
|
|
286
|
-
frontend_fws = [f for f in frameworks if f in
|
|
474
|
+
frontend_fws = [f for f in frameworks if f in FRONTEND_FRAMEWORKS]
|
|
287
475
|
if frontend_fws:
|
|
288
476
|
fe_skills = []
|
|
289
477
|
for fw in frontend_fws:
|
|
@@ -292,13 +480,13 @@ def resolve_agent_table(data):
|
|
|
292
480
|
|
|
293
481
|
# Backend
|
|
294
482
|
be_skills = list(LANG_SKILLS.get(lang, ["api-design"]))
|
|
295
|
-
backend_fws = [f for f in frameworks if f in
|
|
483
|
+
backend_fws = [f for f in frameworks if f in BACKEND_FRAMEWORKS]
|
|
296
484
|
for fw in backend_fws:
|
|
297
485
|
be_skills.extend(FRAMEWORK_SKILLS.get(fw, []))
|
|
298
486
|
rows.append(("Backend API", "backend-specialist", ", ".join(set(be_skills[:3]))))
|
|
299
487
|
|
|
300
488
|
# Mobile/Desktop
|
|
301
|
-
mobile_fws = [f for f in frameworks if f in
|
|
489
|
+
mobile_fws = [f for f in frameworks if f in MOBILE_DESKTOP_FRAMEWORKS]
|
|
302
490
|
if mobile_fws:
|
|
303
491
|
mobile_skills = []
|
|
304
492
|
for fw in mobile_fws:
|
|
@@ -316,20 +504,35 @@ def resolve_agent_table(data):
|
|
|
316
504
|
|
|
317
505
|
# ─── Generators ──────────────────────────────────────────────────────────────
|
|
318
506
|
|
|
319
|
-
def generate_project_context(data):
|
|
320
|
-
frameworks = data.get("frameworks", [])
|
|
321
|
-
infrastructure = data.get("infrastructure", [])
|
|
322
|
-
agent_table = resolve_agent_table(data)
|
|
323
|
-
auto_skills = resolve_skills(data)
|
|
507
|
+
def generate_project_context(data):
|
|
508
|
+
frameworks = data.get("frameworks", [])
|
|
509
|
+
infrastructure = data.get("infrastructure", [])
|
|
510
|
+
agent_table = resolve_agent_table(data)
|
|
511
|
+
auto_skills = resolve_skills(data)
|
|
512
|
+
scan_evidence = data.get("scan_evidence", [])
|
|
324
513
|
|
|
325
514
|
agent_rows = "\n".join(
|
|
326
515
|
f"| {task} | `{agent}` | {skills} |"
|
|
327
516
|
for task, agent, skills in agent_table
|
|
328
517
|
)
|
|
329
518
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
519
|
+
scan_evidence_block = ""
|
|
520
|
+
if scan_evidence:
|
|
521
|
+
scan_evidence_block = f"""
|
|
522
|
+
---
|
|
523
|
+
|
|
524
|
+
## Scan Evidence
|
|
525
|
+
|
|
526
|
+
These detections were inferred from the current codebase during `init --scan`:
|
|
527
|
+
|
|
528
|
+
```
|
|
529
|
+
{chr(10).join('- ' + line for line in scan_evidence)}
|
|
530
|
+
```
|
|
531
|
+
"""
|
|
532
|
+
|
|
533
|
+
return f"""# PROJECT_CONTEXT.md
|
|
534
|
+
# Generated by thachvd-kit init wizard on {datetime.now().strftime('%Y-%m-%d')}
|
|
535
|
+
# Edit freely — this is your project's brain.
|
|
333
536
|
|
|
334
537
|
## Project
|
|
335
538
|
|
|
@@ -349,9 +552,11 @@ stack:
|
|
|
349
552
|
infrastructure: [{', '.join(infrastructure)}]
|
|
350
553
|
package_manager: {data.get('package_manager', 'auto-detect')}
|
|
351
554
|
test_framework: {data.get('test_framework', 'auto-detect')}
|
|
352
|
-
```
|
|
353
|
-
|
|
354
|
-
|
|
555
|
+
```
|
|
556
|
+
|
|
557
|
+
{scan_evidence_block}
|
|
558
|
+
|
|
559
|
+
---
|
|
355
560
|
|
|
356
561
|
## Agent Routing
|
|
357
562
|
|
|
@@ -469,6 +674,11 @@ def main():
|
|
|
469
674
|
print(f" {dim('Scan mode: detecting stack from current project files...')}")
|
|
470
675
|
print(f" {bold('Scan result:')} {scanned['primary_language']} | {', '.join(scanned['frameworks']) or 'no framework'}")
|
|
471
676
|
print(f" {bold('Detected infra:')} {', '.join(scanned['infrastructure']) or 'none'}\n")
|
|
677
|
+
if scanned.get("scan_evidence"):
|
|
678
|
+
print(f" {bold('Evidence:')}")
|
|
679
|
+
for line in scanned["scan_evidence"]:
|
|
680
|
+
print(f" · {line}")
|
|
681
|
+
print()
|
|
472
682
|
elif not args.yes:
|
|
473
683
|
print(f" {dim('Manual mode: asking for project context explicitly.')}\n")
|
|
474
684
|
|
|
@@ -493,7 +703,7 @@ def main():
|
|
|
493
703
|
default=(scanned or {}).get("primary_language", "typescript"),
|
|
494
704
|
choices=LANGUAGES_ALL)
|
|
495
705
|
|
|
496
|
-
frameworks = ask_multi("Frameworks (select all that apply)", FRAMEWORKS_ALL, default=
|
|
706
|
+
frameworks = ask_multi("Frameworks (select all that apply)", FRAMEWORKS_ALL, default=(scanned or {}).get("frameworks", None))
|
|
497
707
|
data["frameworks"] = frameworks or []
|
|
498
708
|
|
|
499
709
|
data["database"] = ask("Database / ORM", default=(scanned or {}).get("database", "postgres"), choices=DATABASES_ALL)
|
|
@@ -547,6 +757,8 @@ def main():
|
|
|
547
757
|
data["test_framework"] = data.get("test_framework", (scanned or {}).get("test_framework", "auto-detect"))
|
|
548
758
|
data["max_file_lines"] = data.get("max_file_lines", (scanned or {}).get("max_file_lines", "300"))
|
|
549
759
|
data["code_style"] = data.get("code_style", (scanned or {}).get("code_style", "project-standard"))
|
|
760
|
+
data["framework_details"] = data.get("framework_details", (scanned or {}).get("framework_details", []))
|
|
761
|
+
data["scan_evidence"] = data.get("scan_evidence", (scanned or {}).get("scan_evidence", []))
|
|
550
762
|
data["architecture_notes"] = data.get("architecture_notes", (scanned or {}).get("architecture_notes", "<!-- Add notes -->"))
|
|
551
763
|
data["current_phase"] = data.get("current_phase", (scanned or {}).get("current_phase", "Initial setup"))
|
|
552
764
|
data["next_phase"] = data.get("next_phase", (scanned or {}).get("next_phase", "—"))
|