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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thachvd-kit",
3
- "version": "1.0.2",
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
- print(f"\n {cyan('Available:')} {', '.join(f'{i+1}) {c}' for i, c in enumerate(choices))}")
41
- raw = input(f" {bold('→')} {prompt} (numbers or names, comma-separated) [{dim(default or 'none')}]: ").strip()
42
- if not raw:
43
- return default or []
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": ["laravel-patterns", "laravel-security", "laravel-tdd"],
80
- "rust": ["rust-pro"],
81
- "java": ["api-design"],
82
- "swift": ["mobile-design"],
83
- "kotlin": ["mobile-design"],
84
- "csharp": ["api-design"],
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
- "laravel": ["laravel-patterns", "laravel-security", "laravel-tdd"],
94
- "express": ["nodejs-best-practices", "api-design"],
95
- "fastapi": ["python-patterns", "api-design"],
96
- "django": ["python-patterns", "api-design"],
97
- "gin": ["golang-patterns", "golang-testing"],
98
- "fiber": ["golang-patterns", "golang-testing"],
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", "express", "fastapi", "django", "gin", "fiber", "nestjs",
132
- "tauri", "flutter", "react-native"]
133
- LANGUAGES_ALL = ["typescript", "javascript", "python", "go", "php", "rust", "java", "swift", "kotlin", "csharp", "c", "cpp"]
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
- frameworks = []
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
- def add_framework(name, condition):
187
- if condition:
188
- frameworks.append(name)
189
-
190
- requirements = read_text_if_exists("requirements.txt")
191
- go_mod = read_text_if_exists("go.mod")
192
- pubspec = read_text_if_exists("pubspec.yaml")
193
-
194
- add_framework("next", "next" in dep_names)
195
- add_framework("react", "react" in dep_names)
196
- add_framework("vue", "vue" in dep_names)
197
- add_framework("nuxt", "nuxt" in dep_names)
198
- add_framework("svelte", "svelte" in dep_names or "@sveltejs/kit" in dep_names)
199
- add_framework("angular", "@angular/core" in dep_names)
200
- add_framework("express", "express" in dep_names)
201
- add_framework("nestjs", "@nestjs/core" in dep_names)
202
- add_framework("fastapi", "fastapi" in requirements.lower())
203
- add_framework("django", "django" in requirements.lower())
204
- add_framework("tauri", "@tauri-apps/api" in dep_names or Path("src-tauri/tauri.conf.json").exists() or Path("src-tauri/tauri.conf.json5").exists())
205
- add_framework("flutter", "flutter:" in pubspec.lower())
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": uniq(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 ["vue", "react", "next", "nuxt", "svelte", "angular"]]
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 ["laravel", "express", "fastapi", "django", "gin", "fiber"]]
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 ["tauri", "flutter", "react-native"]]
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
- return f"""# PROJECT_CONTEXT.md
331
- # Generated by thachvd-kit init wizard on {datetime.now().strftime('%Y-%m-%d')}
332
- # Edit freely — this is your project's brain.
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=", ".join((scanned or {}).get("frameworks", [])) or None)
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", "—"))