claudeos-core 2.4.4 → 2.5.1
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/CHANGELOG.md +125 -0
- package/README.de.md +12 -10
- package/README.es.md +12 -10
- package/README.fr.md +12 -10
- package/README.hi.md +12 -10
- package/README.ja.md +12 -10
- package/README.ko.md +12 -10
- package/README.md +12 -10
- package/README.ru.md +12 -10
- package/README.vi.md +12 -10
- package/README.zh-CN.md +12 -10
- package/bin/commands/init.js +121 -24
- package/bin/commands/lint.js +2 -0
- package/bin/commands/memory.js +10 -3
- package/content-validator/index.js +82 -13
- package/lib/env-parser.js +98 -12
- package/lib/memory-scaffold.js +35 -16
- package/manifest-generator/index.js +15 -4
- package/package.json +92 -92
- package/pass-json-validator/index.js +1 -1
- package/pass-prompts/templates/angular/pass3.md +2 -1
- package/pass-prompts/templates/common/claude-md-scaffold.md +1 -1
- package/pass-prompts/templates/common/pass3a-facts.md +11 -9
- package/pass-prompts/templates/common/pass4.md +3 -3
- package/pass-prompts/templates/java-spring/pass1.md +10 -2
- package/pass-prompts/templates/java-spring/pass3.md +5 -4
- package/pass-prompts/templates/kotlin-spring/pass3.md +2 -2
- package/pass-prompts/templates/node-express/pass3.md +1 -1
- package/pass-prompts/templates/node-fastify/pass3.md +1 -0
- package/pass-prompts/templates/node-nestjs/pass3.md +1 -0
- package/pass-prompts/templates/node-nextjs/pass3.md +1 -1
- package/pass-prompts/templates/node-vite/pass3.md +1 -0
- package/pass-prompts/templates/python-django/pass3.md +1 -1
- package/pass-prompts/templates/python-fastapi/pass3.md +1 -1
- package/pass-prompts/templates/python-flask/pass3.md +1 -0
- package/pass-prompts/templates/vue-nuxt/pass3.md +1 -0
- package/plan-installer/domain-grouper.js +4 -1
- package/plan-installer/index.js +26 -7
- package/plan-installer/jvm-detect.js +562 -0
- package/plan-installer/pass3-context-builder.js +10 -0
- package/plan-installer/prompt-generator.js +18 -2
- package/plan-installer/scanners/scan-frontend.js +67 -6
- package/plan-installer/scanners/scan-java.js +214 -15
- package/plan-installer/scanners/scan-kotlin.js +68 -3
- package/plan-installer/scanners/scan-node.js +115 -0
- package/plan-installer/scanners/scan-python.js +56 -0
- package/plan-installer/source-paths.js +61 -0
- package/plan-installer/stack-detector.js +726 -51
- package/plan-installer/structure-scanner.js +15 -4
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
const path = require("path");
|
|
10
10
|
const { glob } = require("glob");
|
|
11
11
|
const { readFileSafe, readJsonSafe, existsSafe } = require("../lib/safe-fs");
|
|
12
|
-
const { readStackEnvInfo } = require("../lib/env-parser");
|
|
12
|
+
const { readStackEnvInfo, extractPort } = require("../lib/env-parser");
|
|
13
|
+
const JVM = require("./jvm-detect");
|
|
13
14
|
|
|
14
15
|
// ─── Lookup tables ──────────────────────────────────────────────
|
|
15
16
|
|
|
@@ -60,11 +61,42 @@ const DB_KEYWORD_RULES = [
|
|
|
60
61
|
["oracle", "oracle"],
|
|
61
62
|
["mongodb", "mongodb"],
|
|
62
63
|
["sqlite", "sqlite"],
|
|
64
|
+
// v2.5.1 — JDBC coordinates common in enterprise / Korean-market deployments.
|
|
65
|
+
["mssql-jdbc", "mssql"], ["sqljdbc", "mssql"], ["jtds", "mssql"], ["sqlserver", "mssql"],
|
|
66
|
+
["com.ibm.db2", "db2"], ["db2jcc", "db2"],
|
|
67
|
+
["tibero", "tibero"], ["altibase", "altibase"], ["cubrid", "cubrid"],
|
|
63
68
|
];
|
|
64
69
|
|
|
65
70
|
// h2 needs word-boundary check (avoid oauth2, cache2k false positives)
|
|
66
71
|
const H2_REGEX = /\bh2\b/;
|
|
67
72
|
|
|
73
|
+
// Java version literals come in two spellings: modern `17` / `21` and the
|
|
74
|
+
// legacy dotted form `1.8` (Java 8 — still the norm in enterprise SI
|
|
75
|
+
// codebases). Normalize the legacy form so `1.8` → `8`; pass everything
|
|
76
|
+
// else through unchanged.
|
|
77
|
+
function normalizeJavaVersion(v) {
|
|
78
|
+
if (v == null) return v;
|
|
79
|
+
const m = String(v).match(/^1\.(\d+)$/);
|
|
80
|
+
return m ? m[1] : String(v);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// v2.5.0 — Source-language evidence. Build-file keywords alone are ambiguous:
|
|
84
|
+
// `buildSrc/build.gradle.kts` carries `kotlin-dsl` in pure-Java repos, and a
|
|
85
|
+
// Java catalog may pin `kotlin = "1.9.22"` only to settle kotlin-stdlib
|
|
86
|
+
// conflicts. When `.java` sources exist and no `.kt` sources do, a "kotlin"
|
|
87
|
+
// keyword must NOT flip the language — the Kotlin scanner would then find
|
|
88
|
+
// zero domains and `init` would abort. Memoized per detectStack() call.
|
|
89
|
+
async function hasJavaOnlySources(ROOT) {
|
|
90
|
+
// `buildSrc/` and `build-logic/` (Gradle's documented buildSrc replacement)
|
|
91
|
+
// hold convention plugins written in Kotlin DSL — build tooling, not
|
|
92
|
+
// application code.
|
|
93
|
+
const ignore = ["**/node_modules/**", "**/build/**", "**/target/**", "**/buildSrc/**", "**/build-logic/**", "**/gradle/plugins/**", "**/src/test/**", "**/.git/**"];
|
|
94
|
+
const kt = await glob("**/src/main/{java,kotlin}/**/*.kt", { cwd: ROOT, ignore });
|
|
95
|
+
if (kt.length > 0) return false;
|
|
96
|
+
const java = await glob("**/src/main/java/**/*.java", { cwd: ROOT, ignore });
|
|
97
|
+
return java.length > 0;
|
|
98
|
+
}
|
|
99
|
+
|
|
68
100
|
// ─── Helpers ────────────────────────────────────────────────────
|
|
69
101
|
|
|
70
102
|
function detectFirst(stack, field, content, rules) {
|
|
@@ -233,6 +265,16 @@ function detectDb(stack, content, rules) {
|
|
|
233
265
|
* @returns {Promise<object>} stack info
|
|
234
266
|
*/
|
|
235
267
|
async function detectStack(ROOT) {
|
|
268
|
+
// Lazily evaluated once per call; only consulted when a "kotlin" keyword
|
|
269
|
+
// would otherwise flip the language (see hasJavaOnlySources).
|
|
270
|
+
let javaOnlyMemo = null;
|
|
271
|
+
const isJavaOnly = async () => {
|
|
272
|
+
if (javaOnlyMemo === null) javaOnlyMemo = await hasJavaOnlySources(ROOT);
|
|
273
|
+
return javaOnlyMemo;
|
|
274
|
+
};
|
|
275
|
+
// Set when `language` was taken from a root package.json (see Node block);
|
|
276
|
+
// the Python block reclaims it when the backend turns out to be Python.
|
|
277
|
+
let languageFromPackageJson = false;
|
|
236
278
|
const stack = {
|
|
237
279
|
language: null, languageVersion: null,
|
|
238
280
|
framework: null, frameworkVersion: null,
|
|
@@ -259,6 +301,13 @@ async function detectStack(ROOT) {
|
|
|
259
301
|
loggingFrameworks: [],
|
|
260
302
|
frontend: null, frontendVersion: null,
|
|
261
303
|
packageManager: null, monorepo: null, workspaces: null,
|
|
304
|
+
// v2.5.x — JVM legacy support. `packaging` is what the build file
|
|
305
|
+
// DECLARES (Gradle `war`/`ear` plugin, Maven `<packaging>`, or a
|
|
306
|
+
// `WEB-INF/lib` tree) — null when nothing is declared, never an
|
|
307
|
+
// invented "jar". `springFrameworkVersion` is the org.springframework
|
|
308
|
+
// line when it is pinned explicitly; for Boot projects it is normally
|
|
309
|
+
// null because Boot manages it.
|
|
310
|
+
packaging: null, springFrameworkVersion: null,
|
|
262
311
|
detected: [],
|
|
263
312
|
};
|
|
264
313
|
|
|
@@ -266,6 +315,12 @@ async function detectStack(ROOT) {
|
|
|
266
315
|
const gradleFile = existsSafe(path.join(ROOT, "build.gradle.kts"))
|
|
267
316
|
? "build.gradle.kts"
|
|
268
317
|
: existsSafe(path.join(ROOT, "build.gradle")) ? "build.gradle" : null;
|
|
318
|
+
// v2.5.1 — a root holding only settings.gradle{,.kts} (modules carry the
|
|
319
|
+
// build files) is still a Gradle project; the sub-module sweep does the rest.
|
|
320
|
+
if (!gradleFile) {
|
|
321
|
+
const sg = ["settings.gradle.kts", "settings.gradle"].find(f => existsSafe(path.join(ROOT, f)));
|
|
322
|
+
if (sg) { stack.buildTool = "gradle"; stack.detected.push(sg); if (!stack.packageManager) stack.packageManager = "gradle"; }
|
|
323
|
+
}
|
|
269
324
|
if (gradleFile) {
|
|
270
325
|
const g = readFileSafe(path.join(ROOT, gradleFile));
|
|
271
326
|
if (g) {
|
|
@@ -274,7 +329,60 @@ async function detectStack(ROOT) {
|
|
|
274
329
|
// and downstream tooling don't show "PackageMgr: none" for a build tool
|
|
275
330
|
// that IS the package manager. Only set if not already detected.
|
|
276
331
|
if (!stack.packageManager) stack.packageManager = "gradle";
|
|
277
|
-
|
|
332
|
+
// v2.5.x — JVM plugin evidence first, independent of Spring Boot.
|
|
333
|
+
// `apply plugin: 'java'` / `'war'` / `'application'` / `plugins { java }`
|
|
334
|
+
// is proof of a Java project on its own. Before this, a legacy
|
|
335
|
+
// `apply plugin: 'java'` + `spring-webmvc:4.3.30.RELEASE` build reported
|
|
336
|
+
// `language: null` and the Java scanner never ran.
|
|
337
|
+
// v2.5.1 — gradle.properties is a second variable source for
|
|
338
|
+
// `${springVersion}` / `${springBootVersion}` references.
|
|
339
|
+
const gProps = JVM.parseGradleProperties(readFileSafe(path.join(ROOT, "gradle.properties")));
|
|
340
|
+
// v2.5.1 — variable definitions may live outside build.gradle:
|
|
341
|
+
// `apply from: 'gradle/dependencies.gradle'` scripts and buildSrc
|
|
342
|
+
// Kotlin constants (`object Versions { const val spring = "…" }`).
|
|
343
|
+
// Appended to the RESOLUTION text only; detection still reads `g`.
|
|
344
|
+
let gResolve = g;
|
|
345
|
+
for (const rel of JVM.gradleAppliedScripts(g).slice(0, 10)) {
|
|
346
|
+
const t = readFileSafe(path.join(ROOT, rel)); if (t) gResolve += "\n" + t;
|
|
347
|
+
}
|
|
348
|
+
if (existsSafe(path.join(ROOT, "buildSrc"))) {
|
|
349
|
+
const kts = await glob("buildSrc/src/main/{kotlin,java,groovy}/**/*.{kt,groovy,java}", { cwd: ROOT, nodir: true });
|
|
350
|
+
for (const f of kts.slice(0, 20)) { const t = readFileSafe(path.join(ROOT, f)); if (t) gResolve += "\n" + t; }
|
|
351
|
+
}
|
|
352
|
+
const gPlugins = JVM.gradleJvmPlugins(g);
|
|
353
|
+
const jvmByPlugin = JVM.gradleIsJvm(gPlugins) && !stack.language;
|
|
354
|
+
if (jvmByPlugin) stack.language = "java";
|
|
355
|
+
const gPack = JVM.gradlePackaging(gPlugins);
|
|
356
|
+
if (gPack) stack.packaging = gPack;
|
|
357
|
+
// `spring-boot` (starter coords) OR `org.springframework.boot` (plugin id —
|
|
358
|
+
// the only spelling present in a multi-module root that declares
|
|
359
|
+
// `id 'org.springframework.boot' version 'x' apply false`).
|
|
360
|
+
if (g.includes("spring-boot") || g.includes("org.springframework.boot")) {
|
|
361
|
+
stack.language = "java"; stack.framework = "spring-boot"; stack.detected.push("spring-boot");
|
|
362
|
+
} else if (jvmByPlugin) {
|
|
363
|
+
// Label the plugin evidence only when Boot is absent, so the
|
|
364
|
+
// `detected` array of every existing Boot project stays
|
|
365
|
+
// byte-identical to pre-v2.5.x output.
|
|
366
|
+
stack.detected.push("java (gradle plugin)");
|
|
367
|
+
}
|
|
368
|
+
// v2.5.x — Spring Framework WITHOUT Boot (group exactly
|
|
369
|
+
// org.springframework: spring-webmvc / spring-context / the 2.x
|
|
370
|
+
// single `spring` jar / spring-framework-bom). Boot wins when both
|
|
371
|
+
// appear because Boot manages the Framework version.
|
|
372
|
+
if (!stack.framework && JVM.gradleHasSpringFramework(g)) {
|
|
373
|
+
stack.language = "java"; stack.framework = "spring-framework"; stack.detected.push("spring-framework");
|
|
374
|
+
}
|
|
375
|
+
if (JVM.hasEgovframe(g)) {
|
|
376
|
+
const ev = JVM.egovframeVersion(g);
|
|
377
|
+
stack.detected.push(ev ? `egovframe ${ev}` : "egovframe");
|
|
378
|
+
if (!stack.framework) { stack.language = "java"; stack.framework = "spring-framework"; stack.detected.push("spring-framework"); }
|
|
379
|
+
}
|
|
380
|
+
const gSfv = JVM.gradleSpringFrameworkVersion(gResolve, gProps);
|
|
381
|
+
if (gSfv) stack.springFrameworkVersion = gSfv;
|
|
382
|
+
if (stack.framework === "spring-framework" && gSfv) stack.frameworkVersion = gSfv;
|
|
383
|
+
// Struts / JSF tags are legacy evidence; not pushed for Boot projects
|
|
384
|
+
// (their `detected` array must stay byte-identical to v2.5.0 output).
|
|
385
|
+
if (stack.framework !== "spring-boot") for (const t of JVM.legacyFrameworkTags(g)) stack.detected.push(t.version ? `${t.tag} ${t.version}` : t.tag);
|
|
278
386
|
const svPatterns = [
|
|
279
387
|
/org\.springframework\.boot.*version\s*['"]([^'"]+)['"]/,
|
|
280
388
|
/id\s*\(\s*["']org\.springframework\.boot["']\s*\)\s*version\s*["']([^"']+)["']/,
|
|
@@ -306,6 +414,15 @@ async function detectStack(ROOT) {
|
|
|
306
414
|
if (varVal) stack.frameworkVersion = varVal[1];
|
|
307
415
|
}
|
|
308
416
|
}
|
|
417
|
+
// v2.5.x — Boot version. The helper covers every form the loop above
|
|
418
|
+
// covered PLUS the Boot 1.x/2.x buildscript-classpath form
|
|
419
|
+
// (`classpath("org.springframework.boot:spring-boot-gradle-plugin:1.5.22.RELEASE")`,
|
|
420
|
+
// optionally via `${springBootVersion}`), which had no `version`
|
|
421
|
+
// keyword for the old regexes to anchor on.
|
|
422
|
+
if (stack.framework === "spring-boot" && !stack.frameworkVersion) {
|
|
423
|
+
const bv = JVM.gradleSpringBootVersion(gResolve, gProps);
|
|
424
|
+
if (bv) stack.frameworkVersion = bv;
|
|
425
|
+
}
|
|
309
426
|
// Java version — Gradle writes this in several forms. Try each
|
|
310
427
|
// pattern until one matches. Earlier patterns take precedence.
|
|
311
428
|
//
|
|
@@ -332,16 +449,19 @@ async function detectStack(ROOT) {
|
|
|
332
449
|
// Java 21).
|
|
333
450
|
const javaVersionPatterns = [
|
|
334
451
|
// (1) numeric literal on sourceCompatibility or targetCompatibility
|
|
335
|
-
|
|
336
|
-
/
|
|
452
|
+
// `(\d+(?:\.\d+)?)` — captures `1.8` whole instead of stopping at `1`
|
|
453
|
+
/sourceCompatibility\s*=\s*['"]?(\d+(?:\.\d+)?)['"]?/,
|
|
454
|
+
/targetCompatibility\s*=\s*['"]?(\d+(?:\.\d+)?)['"]?/,
|
|
337
455
|
// (2) JavaVersion enum — supports both VERSION_21 and VERSION_1_8
|
|
338
456
|
/JavaVersion\.VERSION_(?:1_)?(\d+)/,
|
|
339
457
|
// (3) toolchain block
|
|
340
458
|
/JavaLanguageVersion\.of\s*\(\s*(\d+)\s*\)/,
|
|
459
|
+
// (3b) v2.5.1 — `options.release = 17` / `options.release.set(17)`
|
|
460
|
+
/options\.release(?:\.set)?\s*[=(]\s*(\d+)/,
|
|
341
461
|
];
|
|
342
462
|
for (const pattern of javaVersionPatterns) {
|
|
343
463
|
const m = g.match(pattern);
|
|
344
|
-
if (m) { stack.languageVersion = m[1]; break; }
|
|
464
|
+
if (m) { stack.languageVersion = normalizeJavaVersion(m[1]); break; }
|
|
345
465
|
}
|
|
346
466
|
// (4) ext variable reference fallback — if the Compatibility
|
|
347
467
|
// assignment used "${varName}" we now resolve varName inside the
|
|
@@ -356,9 +476,9 @@ async function detectStack(ROOT) {
|
|
|
356
476
|
// defensive guard against unexpected characters, not a
|
|
357
477
|
// practical necessity for today's inputs.
|
|
358
478
|
const escapedVarName = varName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
359
|
-
const extAssign = new RegExp(`${escapedVarName}\\s*=\\s*['"]?(\\d+)['"]?`);
|
|
479
|
+
const extAssign = new RegExp(`${escapedVarName}\\s*=\\s*['"]?(\\d+(?:\\.\\d+)?)['"]?`);
|
|
360
480
|
const extVal = g.match(extAssign);
|
|
361
|
-
if (extVal) stack.languageVersion = extVal[1];
|
|
481
|
+
if (extVal) stack.languageVersion = normalizeJavaVersion(extVal[1]);
|
|
362
482
|
}
|
|
363
483
|
}
|
|
364
484
|
|
|
@@ -382,7 +502,8 @@ async function detectStack(ROOT) {
|
|
|
382
502
|
detectLogging(stack, g);
|
|
383
503
|
|
|
384
504
|
// Kotlin detection: override language if Kotlin plugin found
|
|
385
|
-
|
|
505
|
+
// (unless the source tree is Java-only — see hasJavaOnlySources).
|
|
506
|
+
if ((g.includes("kotlin") || g.includes("org.jetbrains.kotlin")) && !(await isJavaOnly())) {
|
|
386
507
|
stack.language = "kotlin"; stack.detected.push("kotlin");
|
|
387
508
|
const kvPatterns = [
|
|
388
509
|
/kotlin\S*\s*version\s*['"]([^'"]+)['"]/,
|
|
@@ -411,6 +532,22 @@ async function detectStack(ROOT) {
|
|
|
411
532
|
const sbMatch = vc.match(/spring-boot\s*=\s*["']([^"']+)["']/);
|
|
412
533
|
if (sbMatch) stack.frameworkVersion = sbMatch[1];
|
|
413
534
|
}
|
|
535
|
+
// v2.5.x — Spring Framework declared through the catalog
|
|
536
|
+
// (`spring-webmvc = { module = "org.springframework:spring-webmvc",
|
|
537
|
+
// version.ref = "spring" }`). The build file only shows
|
|
538
|
+
// `libs.spring.webmvc`, so this is the one place the coordinate and
|
|
539
|
+
// its version are visible.
|
|
540
|
+
if (!stack.framework && JVM.catalogHasSpringFramework(vc)) {
|
|
541
|
+
if (!stack.language) stack.language = "java";
|
|
542
|
+
stack.framework = "spring-framework"; stack.detected.push("spring-framework (catalog)");
|
|
543
|
+
}
|
|
544
|
+
if (!stack.springFrameworkVersion) {
|
|
545
|
+
const cSfv = JVM.catalogSpringFrameworkVersion(vc);
|
|
546
|
+
if (cSfv) {
|
|
547
|
+
stack.springFrameworkVersion = cSfv;
|
|
548
|
+
if (stack.framework === "spring-framework" && !stack.frameworkVersion) stack.frameworkVersion = cSfv;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
414
551
|
// Version catalog ORM (labels include " (catalog)" suffix)
|
|
415
552
|
if (!stack.orm && vc.includes("exposed")) { stack.orm = "exposed"; stack.detected.push("exposed (catalog)"); }
|
|
416
553
|
else if (!stack.orm && vc.includes("jooq")) { stack.orm = "jooq"; stack.detected.push("jooq (catalog)"); }
|
|
@@ -427,15 +564,77 @@ async function detectStack(ROOT) {
|
|
|
427
564
|
if (!stack.databases.includes(value)) stack.databases.push(value);
|
|
428
565
|
}
|
|
429
566
|
}
|
|
430
|
-
|
|
567
|
+
// A `kotlin = "x.y.z"` version entry or a Kotlin *plugin* coordinate in
|
|
568
|
+
// the catalog is decisive — it overrides the `java` default that the
|
|
569
|
+
// root build file's `org.springframework.boot` plugin id sets.
|
|
570
|
+
// Library coordinates (`org.jetbrains.kotlin:kotlin-stdlib`) are NOT a
|
|
571
|
+
// signal: Java projects pin them in the catalog to settle transitive
|
|
572
|
+
// version conflicts without writing a line of Kotlin.
|
|
573
|
+
const KOTLIN_CATALOG_RE = /(^\s*kotlin\s*=|org\.jetbrains\.kotlin\.(?:jvm|plugin|multiplatform|android|kapt)|kotlin-gradle-plugin)/m;
|
|
574
|
+
if (stack.language !== "kotlin" && KOTLIN_CATALOG_RE.test(vc) && !(await isJavaOnly())) {
|
|
431
575
|
stack.language = "kotlin"; stack.detected.push("kotlin (catalog)");
|
|
432
576
|
}
|
|
433
577
|
}
|
|
434
578
|
}
|
|
435
579
|
|
|
580
|
+
// ── Java: multi-module Gradle detection ──
|
|
581
|
+
// Root build.gradle of a multi-module project often holds only
|
|
582
|
+
// `allprojects { repositories {...} }` and no framework coords at all;
|
|
583
|
+
// the real declarations live in `api/build.gradle`, `core/build.gradle`, …
|
|
584
|
+
// Scan sub-module build files (same bound as the Kotlin block below) for
|
|
585
|
+
// the Java plugin / Spring Boot coords so the project isn't reported as
|
|
586
|
+
// "no language detected" and the Java scanner actually runs.
|
|
587
|
+
if (!stack.language && stack.buildTool === "gradle") {
|
|
588
|
+
const subBuildFiles = await glob("*/**/build.gradle{,.kts}", { cwd: ROOT, ignore: ["**/node_modules/**", "**/build/**", "**/buildSrc/**"] });
|
|
589
|
+
for (const sbf of subBuildFiles.slice(0, 30)) {
|
|
590
|
+
const sc = readFileSafe(path.join(ROOT, sbf));
|
|
591
|
+
if (!sc) continue;
|
|
592
|
+
if (sc.includes("kotlin") || sc.includes("org.jetbrains.kotlin")) continue; // handled by the Kotlin block
|
|
593
|
+
// v2.5.x — any JVM plugin or an org.springframework coordinate counts,
|
|
594
|
+
// not only java/java-library/Boot.
|
|
595
|
+
const scPlugins = JVM.gradleJvmPlugins(sc);
|
|
596
|
+
const isJava = JVM.gradleIsJvm(scPlugins)
|
|
597
|
+
|| sc.includes("spring-boot")
|
|
598
|
+
|| JVM.gradleHasSpringFramework(sc);
|
|
599
|
+
if (isJava) {
|
|
600
|
+
// Do not `break` on the first Java module: a `core` library module
|
|
601
|
+
// usually comes before the `api` module that actually declares
|
|
602
|
+
// Spring Boot. Set language once, keep sweeping for framework/versions.
|
|
603
|
+
if (stack.language !== "java") { stack.language = "java"; stack.detected.push("java (submodule)"); }
|
|
604
|
+
if (!stack.packaging) { const p = JVM.gradlePackaging(scPlugins); if (p) stack.packaging = p; }
|
|
605
|
+
if (!stack.framework && (sc.includes("spring-boot") || sc.includes("org.springframework.boot"))) {
|
|
606
|
+
stack.framework = "spring-boot"; stack.detected.push("spring-boot (submodule)");
|
|
607
|
+
}
|
|
608
|
+
if (!stack.framework && JVM.gradleHasSpringFramework(sc)) {
|
|
609
|
+
stack.framework = "spring-framework"; stack.detected.push("spring-framework (submodule)");
|
|
610
|
+
}
|
|
611
|
+
if (!stack.springFrameworkVersion) {
|
|
612
|
+
const sfv = JVM.gradleSpringFrameworkVersion(sc);
|
|
613
|
+
if (sfv) { stack.springFrameworkVersion = sfv; if (stack.framework === "spring-framework" && !stack.frameworkVersion) stack.frameworkVersion = sfv; }
|
|
614
|
+
}
|
|
615
|
+
if (!stack.frameworkVersion && stack.framework === "spring-boot") {
|
|
616
|
+
// `spring-boot-starter-web:2.7.18`, `spring-boot-dependencies:3.2.0`,
|
|
617
|
+
// or `id 'org.springframework.boot' version '3.2.5'` inside the module.
|
|
618
|
+
const sv = JVM.gradleSpringBootVersion(sc)
|
|
619
|
+
|| (sc.match(/spring-boot[\w-]*[:\s'"]+(\d+\.\d+\.\d+)/) || [])[1];
|
|
620
|
+
if (sv) stack.frameworkVersion = sv;
|
|
621
|
+
}
|
|
622
|
+
if (!stack.languageVersion) {
|
|
623
|
+
const jv = sc.match(/(?:sourceCompatibility|targetCompatibility)\s*=\s*['"]?(\d+(?:\.\d+)?)['"]?/)
|
|
624
|
+
|| sc.match(/JavaVersion\.VERSION_(?:1_)?(\d+)/)
|
|
625
|
+
|| sc.match(/JavaLanguageVersion\.of\s*\(\s*(\d+)\s*\)/);
|
|
626
|
+
if (jv) stack.languageVersion = normalizeJavaVersion(jv[1]);
|
|
627
|
+
}
|
|
628
|
+
if (stack.framework && stack.frameworkVersion && stack.languageVersion) break;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
436
633
|
// ── Kotlin: multi-module Gradle detection ──
|
|
437
|
-
|
|
438
|
-
|
|
634
|
+
// v2.5.0: `buildSrc/` is ignored (its `kotlin-dsl` plugin is not evidence of
|
|
635
|
+
// Kotlin application code) and a Java-only source tree never flips.
|
|
636
|
+
if (stack.language !== "kotlin" && stack.buildTool === "gradle" && !(await isJavaOnly())) {
|
|
637
|
+
const subBuildFiles = await glob("**/build.gradle{,.kts}", { cwd: ROOT, ignore: ["**/node_modules/**", "**/build/**", "**/buildSrc/**"] });
|
|
439
638
|
for (const sbf of subBuildFiles.slice(0, 5)) {
|
|
440
639
|
const sc = readFileSafe(path.join(ROOT, sbf));
|
|
441
640
|
if (sc && (sc.includes("kotlin") || sc.includes("org.jetbrains.kotlin"))) {
|
|
@@ -482,6 +681,39 @@ async function detectStack(ROOT) {
|
|
|
482
681
|
}
|
|
483
682
|
}
|
|
484
683
|
|
|
684
|
+
// v2.5.1 — Fold one child pom (Maven <module> or a depth-1 sibling
|
|
685
|
+
// project) into `stack`, filling only what is still null. `${prop}` in the
|
|
686
|
+
// child resolves against the child first, then `rootPom` (may be "").
|
|
687
|
+
const absorbMavenPom = (cp, rootPom, mod) => {
|
|
688
|
+
const cpClean = stripComments(cp);
|
|
689
|
+
const propsText = cp + "\n" + rootPom;
|
|
690
|
+
if (!stack.framework && cpClean.includes("spring-boot")) { stack.framework = "spring-boot"; stack.detected.push(`spring-boot (${mod})`); }
|
|
691
|
+
if (stack.framework === "spring-boot" && !stack.frameworkVersion) { const bv = JVM.mavenSpringBootVersion(propsText, cpClean); if (bv) stack.frameworkVersion = bv; }
|
|
692
|
+
if (!stack.framework && JVM.mavenHasSpringFramework(cpClean)) { stack.framework = "spring-framework"; stack.detected.push(`spring-framework (${mod})`); }
|
|
693
|
+
// Root <properties> may already have yielded the Framework version
|
|
694
|
+
// before any module declared the framework itself — link them.
|
|
695
|
+
if (stack.framework === "spring-framework" && !stack.frameworkVersion && stack.springFrameworkVersion) stack.frameworkVersion = stack.springFrameworkVersion;
|
|
696
|
+
if (!stack.springFrameworkVersion) {
|
|
697
|
+
const v = JVM.mavenSpringFrameworkVersion(propsText, cpClean);
|
|
698
|
+
if (v) { stack.springFrameworkVersion = v; if (stack.framework === "spring-framework" && !stack.frameworkVersion) stack.frameworkVersion = v; }
|
|
699
|
+
}
|
|
700
|
+
if (JVM.hasEgovframe(cpClean) && !stack.detected.some(d => d.startsWith("egovframe"))) {
|
|
701
|
+
const ev = JVM.egovframeVersion(cpClean, propsText);
|
|
702
|
+
stack.detected.push(ev ? `egovframe ${ev}` : "egovframe");
|
|
703
|
+
if (!stack.framework) { stack.framework = "spring-framework"; stack.detected.push("spring-framework"); }
|
|
704
|
+
}
|
|
705
|
+
if (!stack.orm) { if (IBATIS_REGEX.test(cpClean)) { stack.orm = "ibatis"; stack.detected.push(`ibatis (${mod})`); } else detectFirst(stack, "orm", cpClean, MAVEN_ORM_RULES); }
|
|
706
|
+
for (const [keyword, value] of DB_KEYWORD_RULES.filter(([kw]) => kw !== "postgres")) {
|
|
707
|
+
if (cpClean.includes(keyword)) { if (!stack.database) stack.database = value; if (!stack.databases.includes(value)) stack.databases.push(value); }
|
|
708
|
+
}
|
|
709
|
+
if (!stack.languageVersion) {
|
|
710
|
+
const jv = cp.match(/<java\.version>\s*(\d+(?:\.\d+)?)\s*<\/java\.version>/) || cp.match(/<maven\.compiler\.(?:source|release)>\s*(\d+(?:\.\d+)?)\s*</);
|
|
711
|
+
if (jv) stack.languageVersion = normalizeJavaVersion(jv[1]);
|
|
712
|
+
}
|
|
713
|
+
if (!stack.packaging) { const pk = JVM.mavenPackaging(cp); if (pk && pk !== "pom") stack.packaging = pk; }
|
|
714
|
+
if (stack.framework !== "spring-boot") for (const t of JVM.legacyFrameworkTags(cpClean)) { const tag = t.version ? `${t.tag} ${t.version}` : t.tag; if (!stack.detected.includes(tag)) stack.detected.push(tag); }
|
|
715
|
+
};
|
|
716
|
+
|
|
485
717
|
// ── Java: Maven ──
|
|
486
718
|
if (existsSafe(path.join(ROOT, "pom.xml"))) {
|
|
487
719
|
const pom = readFileSafe(path.join(ROOT, "pom.xml"));
|
|
@@ -489,6 +721,13 @@ async function detectStack(ROOT) {
|
|
|
489
721
|
if (!stack.buildTool) { stack.buildTool = "maven"; stack.language = "java"; stack.detected.push("pom.xml"); }
|
|
490
722
|
// v2.4.0 — JVM package manager (parallel to Gradle case above).
|
|
491
723
|
if (!stack.packageManager) stack.packageManager = "maven";
|
|
724
|
+
// v2.5.x — declared packaging (war / ear / jar / pom). Null when the
|
|
725
|
+
// pom is silent; Maven's implicit "jar" default is NOT written back.
|
|
726
|
+
const mPack = JVM.mavenPackaging(pom);
|
|
727
|
+
if (mPack && !stack.packaging) stack.packaging = mPack;
|
|
728
|
+
// Boot version: <spring-boot.version> property (the only form the
|
|
729
|
+
// old regex knew), the starter-parent <version>, or the
|
|
730
|
+
// spring-boot-dependencies BOM import.
|
|
492
731
|
const sv = pom.match(/<spring-boot[^>]*version>([^<]+)/);
|
|
493
732
|
if (sv) stack.frameworkVersion = sv[1];
|
|
494
733
|
// Java version — Maven commonly uses three patterns:
|
|
@@ -510,13 +749,15 @@ async function detectStack(ROOT) {
|
|
|
510
749
|
// pom.xml (cross-file resolution — parent pom, BOM — is out of
|
|
511
750
|
// scope; the resulting null falls through to LLM-side analysis).
|
|
512
751
|
const mvnJavaPatterns = [
|
|
513
|
-
/<java\.version>\s*(\d+)\s*<\/java\.version>/,
|
|
514
|
-
/<maven\.compiler\.source>\s*(\d+)\s*<\/maven\.compiler\.source>/,
|
|
515
|
-
/<maven\.compiler\.target>\s*(\d+)\s*<\/maven\.compiler\.target>/,
|
|
752
|
+
/<java\.version>\s*(\d+(?:\.\d+)?)\s*<\/java\.version>/,
|
|
753
|
+
/<maven\.compiler\.source>\s*(\d+(?:\.\d+)?)\s*<\/maven\.compiler\.source>/,
|
|
754
|
+
/<maven\.compiler\.target>\s*(\d+(?:\.\d+)?)\s*<\/maven\.compiler\.target>/,
|
|
755
|
+
// v2.5.1 — `<maven.compiler.release>17</maven.compiler.release>` (JEP 247 style)
|
|
756
|
+
/<maven\.compiler\.release>\s*(\d+)\s*<\/maven\.compiler\.release>/,
|
|
516
757
|
];
|
|
517
758
|
for (const pattern of mvnJavaPatterns) {
|
|
518
759
|
const m = pom.match(pattern);
|
|
519
|
-
if (m) { stack.languageVersion = m[1]; break; }
|
|
760
|
+
if (m) { stack.languageVersion = normalizeJavaVersion(m[1]); break; }
|
|
520
761
|
}
|
|
521
762
|
// Pattern 3 fallback: if <java.version> references a property,
|
|
522
763
|
// resolve it inside the same pom.
|
|
@@ -525,11 +766,19 @@ async function detectStack(ROOT) {
|
|
|
525
766
|
if (propRef) {
|
|
526
767
|
const propName = propRef[1].trim();
|
|
527
768
|
const escapedProp = propName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
528
|
-
const propDef = new RegExp(`<${escapedProp}>\\s*(\\d+)\\s*</${escapedProp}>`);
|
|
769
|
+
const propDef = new RegExp(`<${escapedProp}>\\s*(\\d+(?:\\.\\d+)?)\\s*</${escapedProp}>`);
|
|
529
770
|
const propVal = pom.match(propDef);
|
|
530
|
-
if (propVal) stack.languageVersion = propVal[1];
|
|
771
|
+
if (propVal) stack.languageVersion = normalizeJavaVersion(propVal[1]);
|
|
531
772
|
}
|
|
532
773
|
}
|
|
774
|
+
// v2.5.x — Pattern 4: pre-properties era. Maven 2 poms carried the
|
|
775
|
+
// level only inside the compiler plugin:
|
|
776
|
+
// <plugin><artifactId>maven-compiler-plugin</artifactId>
|
|
777
|
+
// <configuration><source>1.5</source><target>1.5</target></configuration>
|
|
778
|
+
if (!stack.languageVersion) {
|
|
779
|
+
const cps = JVM.mavenCompilerPluginSource(pom);
|
|
780
|
+
if (cps) stack.languageVersion = normalizeJavaVersion(cps);
|
|
781
|
+
}
|
|
533
782
|
// For dependency detection (framework, ORM, DB, logging), strip
|
|
534
783
|
// XML block comments first. A `<!-- <dependency>...</dependency> -->`
|
|
535
784
|
// block is a standard Maven pattern for disabling a dep during
|
|
@@ -541,6 +790,40 @@ async function detectStack(ROOT) {
|
|
|
541
790
|
// resolution already scopes itself to the declared property name.
|
|
542
791
|
const pomClean = stripComments(pom);
|
|
543
792
|
if (pomClean.includes("spring-boot") && !stack.framework) { stack.framework = "spring-boot"; stack.detected.push("spring-boot"); }
|
|
793
|
+
if (stack.framework === "spring-boot" && !stack.frameworkVersion) {
|
|
794
|
+
const bv = JVM.mavenSpringBootVersion(pom, pomClean);
|
|
795
|
+
if (bv) stack.frameworkVersion = bv;
|
|
796
|
+
}
|
|
797
|
+
// v2.5.x — Spring Framework without Boot. Only <dependency> blocks
|
|
798
|
+
// whose groupId is EXACTLY org.springframework count (comment-stripped
|
|
799
|
+
// text, so a `<!-- … -->`-disabled dependency is ignored). Version comes
|
|
800
|
+
// from spring-framework-bom, the first versioned Framework dependency,
|
|
801
|
+
// or a <spring.version>-style property — `${prop}` resolved in-file.
|
|
802
|
+
if (!stack.framework && JVM.mavenHasSpringFramework(pomClean)) {
|
|
803
|
+
stack.framework = "spring-framework"; stack.detected.push("spring-framework");
|
|
804
|
+
}
|
|
805
|
+
const mSfv = JVM.mavenSpringFrameworkVersion(pom, pomClean);
|
|
806
|
+
if (mSfv) {
|
|
807
|
+
stack.springFrameworkVersion = mSfv;
|
|
808
|
+
if (stack.framework === "spring-framework" && !stack.frameworkVersion) stack.frameworkVersion = mSfv;
|
|
809
|
+
}
|
|
810
|
+
// v2.5.1 — eGovFrame (전자정부 표준프레임워크): Spring MVC underneath.
|
|
811
|
+
if (JVM.hasEgovframe(pomClean)) {
|
|
812
|
+
const ev = JVM.egovframeVersion(pomClean, pom);
|
|
813
|
+
stack.detected.push(ev ? `egovframe ${ev}` : "egovframe");
|
|
814
|
+
if (!stack.framework) { stack.framework = "spring-framework"; stack.detected.push("spring-framework"); }
|
|
815
|
+
}
|
|
816
|
+
if (stack.framework !== "spring-boot") for (const t of JVM.legacyFrameworkTags(pomClean)) stack.detected.push(t.version ? `${t.tag} ${t.version}` : t.tag);
|
|
817
|
+
// v2.5.1 — Maven multi-module. A root `<packaging>pom</packaging>` with
|
|
818
|
+
// `<modules>` usually declares nothing but dependencyManagement; the
|
|
819
|
+
// Spring coordinates live in `web/pom.xml`. Sweep the listed modules
|
|
820
|
+
// (bounded) and fill only what the root left null. `${prop}` in a child
|
|
821
|
+
// resolves against the child first, then the root <properties>.
|
|
822
|
+
const moduleNames = [...pomClean.matchAll(/<module>\s*([^<]+?)\s*<\/module>/g)].map(m => m[1]);
|
|
823
|
+
for (const mod of moduleNames.slice(0, 30)) {
|
|
824
|
+
const cp = readFileSafe(path.join(ROOT, mod, "pom.xml"));
|
|
825
|
+
if (cp) absorbMavenPom(cp, pom, mod);
|
|
826
|
+
}
|
|
544
827
|
if (IBATIS_REGEX.test(pomClean)) {
|
|
545
828
|
stack.orm = "ibatis";
|
|
546
829
|
stack.detected.push("ibatis");
|
|
@@ -552,14 +835,9 @@ async function detectStack(ROOT) {
|
|
|
552
835
|
// (primary first-match → stack.database; every match →
|
|
553
836
|
// stack.databases). Maven original did not push to `detected`
|
|
554
837
|
// (unlike Gradle), so we preserve that omission here.
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
["mysql", "mysql"],
|
|
559
|
-
["oracle", "oracle"],
|
|
560
|
-
["mongodb", "mongodb"],
|
|
561
|
-
["sqlite", "sqlite"],
|
|
562
|
-
];
|
|
838
|
+
// "postgres" is excluded (substring of postgresql — Maven coords always
|
|
839
|
+
// spell it out) so the primary-DB race is identical to pre-v2.5.1.
|
|
840
|
+
const mvnDbRules = DB_KEYWORD_RULES.filter(([kw]) => kw !== "postgres");
|
|
563
841
|
for (const [keyword, value] of mvnDbRules) {
|
|
564
842
|
if (pomClean.includes(keyword)) {
|
|
565
843
|
if (!stack.database) stack.database = value;
|
|
@@ -628,7 +906,10 @@ async function detectStack(ROOT) {
|
|
|
628
906
|
}
|
|
629
907
|
}
|
|
630
908
|
|
|
631
|
-
|
|
909
|
+
// Provisional: a root package.json may exist only for frontend tooling
|
|
910
|
+
// (Tailwind/PostCSS) in a Python repo. The Python block below reclaims
|
|
911
|
+
// `language` when a Python framework is detected and no Node backend is.
|
|
912
|
+
if (!stack.language) { stack.language = deps.typescript ? "typescript" : "javascript"; languageFromPackageJson = true; }
|
|
632
913
|
if (deps.typescript) { stack.detected.push("typescript"); const tv = deps.typescript.match(/(\d+(?:\.\d+)*)/); if (tv) stack.languageVersion = tv[1]; }
|
|
633
914
|
|
|
634
915
|
// Frontend (Angular checked before React — Angular projects may include react in devDependencies)
|
|
@@ -666,6 +947,14 @@ async function detectStack(ROOT) {
|
|
|
666
947
|
stack.detected.push("vite");
|
|
667
948
|
stack.frameworkVersion = deps.vite.replace(/[^0-9.]/g, "");
|
|
668
949
|
}
|
|
950
|
+
// v2.5.0 — Record the frontend bundler independently of `framework`.
|
|
951
|
+
// When a backend framework occupies `stack.framework` (Spring + React/Vite
|
|
952
|
+
// in one repo), `framework === "vite"` can never be true, and
|
|
953
|
+
// selectTemplates() used to fall back to the Next.js template for a
|
|
954
|
+
// Vite SPA. `frontendBundler` carries that signal regardless of backend.
|
|
955
|
+
if (deps.vite && stack.frontend && stack.frontend !== "nextjs") {
|
|
956
|
+
stack.frontendBundler = "vite";
|
|
957
|
+
}
|
|
669
958
|
|
|
670
959
|
// ORM
|
|
671
960
|
for (const [depKeys, ormName] of NODE_ORM_RULES) {
|
|
@@ -698,18 +987,95 @@ async function detectStack(ROOT) {
|
|
|
698
987
|
}
|
|
699
988
|
}
|
|
700
989
|
|
|
990
|
+
let subDirSpa = null;
|
|
991
|
+
// ── Frontend in a sub-directory (v2.5.0) ──
|
|
992
|
+
// Spring/Django/... repos commonly keep the SPA in frontend/, client/, web/ or
|
|
993
|
+
// ui/ with its own package.json and no root package.json. Detect it there and
|
|
994
|
+
// record the sub-directory so the frontend scanner can be rooted at it.
|
|
995
|
+
if (!stack.frontend) {
|
|
996
|
+
for (const sub of ["frontend", "client", "web", "ui", "webapp", "front"]) {
|
|
997
|
+
const subDir = path.join(ROOT, sub);
|
|
998
|
+
const pj = path.join(subDir, "package.json");
|
|
999
|
+
const spkg = existsSafe(pj) ? readJsonSafe(pj) : null;
|
|
1000
|
+
const sdeps = spkg ? { ...(spkg.dependencies || {}), ...(spkg.devDependencies || {}) } : null;
|
|
1001
|
+
if (sdeps) {
|
|
1002
|
+
// Same precedence as the root package.json rules (Angular before React —
|
|
1003
|
+
// Angular projects may carry react in devDependencies). `nuxt` maps to
|
|
1004
|
+
// vue so a Nuxt app is not missed when `vue` is only transitive.
|
|
1005
|
+
const rules = [["next", "nextjs", "next.js"], ["@angular/core", "angular", "angular"], ["nuxt", "vue", "nuxt"], ["react", "react", "react"], ["vue", "vue", "vue"]];
|
|
1006
|
+
for (const [dep, name, label] of rules) {
|
|
1007
|
+
if (sdeps[dep]) {
|
|
1008
|
+
stack.frontend = name;
|
|
1009
|
+
// `frontendVersion` is the framework's version (Vue for a Nuxt app),
|
|
1010
|
+
// never the meta-framework's — a Nuxt 3.11 app is not "Vue 3.11".
|
|
1011
|
+
const verDep = dep === "nuxt" ? (sdeps.vue ? "vue" : null) : dep;
|
|
1012
|
+
stack.frontendVersion = verDep ? String(sdeps[verDep]).replace(/[^0-9.]/g, "") : null;
|
|
1013
|
+
stack.frontendRoot = sub;
|
|
1014
|
+
stack.detected.push(`${label} (${sub}/)`);
|
|
1015
|
+
break;
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
if (stack.frontend && sdeps.vite && stack.frontend !== "nextjs") stack.frontendBundler = "vite";
|
|
1019
|
+
}
|
|
1020
|
+
// Config-file fallback inside the sub-directory (mirrors the root fallback
|
|
1021
|
+
// below): a package.json without a recognizable framework dep, or none at all.
|
|
1022
|
+
if (!stack.frontend) {
|
|
1023
|
+
const subFallbacks = [
|
|
1024
|
+
[["next.config.js", "next.config.mjs", "next.config.ts"], "nextjs", null, "next.config"],
|
|
1025
|
+
[["vite.config.ts", "vite.config.js"], "react", "vite", "vite.config"],
|
|
1026
|
+
[["nuxt.config.ts", "nuxt.config.js"], "vue", null, "nuxt.config"],
|
|
1027
|
+
[["angular.json", ".angular.json"], "angular", null, "angular.json"],
|
|
1028
|
+
];
|
|
1029
|
+
for (const [files, frontendName, bundler, label] of subFallbacks) {
|
|
1030
|
+
if (files.some(f => existsSafe(path.join(subDir, f)))) {
|
|
1031
|
+
stack.frontend = frontendName;
|
|
1032
|
+
stack.frontendRoot = sub;
|
|
1033
|
+
if (bundler) stack.frontendBundler = bundler;
|
|
1034
|
+
stack.detected.push(`${label} (${sub}/, fallback)`);
|
|
1035
|
+
break;
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
if (stack.frontend) {
|
|
1040
|
+
// Remember the sub-directory; language / package manager are filled
|
|
1041
|
+
// AFTER every backend block has run (see "SPA-only sub-directory
|
|
1042
|
+
// repo" below). Filling them here would pre-empt the Python block's
|
|
1043
|
+
// `if (!stack.language)` and turn a Django + frontend/ repo into
|
|
1044
|
+
// "typescript", silently skipping the Python scanner.
|
|
1045
|
+
subDirSpa = { subDir, sdeps };
|
|
1046
|
+
break;
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
|
|
701
1051
|
// ── Python ──
|
|
702
1052
|
const hasPyproject = existsSafe(path.join(ROOT, "pyproject.toml"));
|
|
703
1053
|
const hasRequirements = existsSafe(path.join(ROOT, "requirements.txt"));
|
|
704
1054
|
if (hasPyproject || hasRequirements) {
|
|
705
|
-
|
|
1055
|
+
// v2.5.0 — a Python manifest at the root beats a `language` that came
|
|
1056
|
+
// from a root package.json with NO Node backend framework: that
|
|
1057
|
+
// package.json exists for Tailwind/PostCSS/ESLint tooling, and the
|
|
1058
|
+
// backend is Python. (A NestJS/Express/Fastify framework keeps Node.)
|
|
1059
|
+
const NODE_BACKENDS = ["nestjs", "express", "fastify"];
|
|
1060
|
+
if (!stack.language) {
|
|
1061
|
+
stack.language = "python";
|
|
1062
|
+
} else if (languageFromPackageJson && !NODE_BACKENDS.includes(stack.framework)) {
|
|
1063
|
+
stack.language = "python";
|
|
1064
|
+
stack.languageVersion = null; // was the TypeScript version; Python's is read below
|
|
1065
|
+
}
|
|
706
1066
|
stack.detected.push("python");
|
|
707
1067
|
|
|
708
1068
|
const pyFrameworkRules = [["django", "django"], ["fastapi", "fastapi"], ["flask", "flask"]];
|
|
709
1069
|
const pyOrmRules = [["sqlalchemy", "sqlalchemy"], ["tortoise", "tortoise-orm"]];
|
|
710
1070
|
|
|
1071
|
+
// v2.5.0 — keyword matching is case-insensitive. `pip freeze` and PyPI
|
|
1072
|
+
// canonical names are capitalized (`Django==5.0`, `Flask==3.0`,
|
|
1073
|
+
// `SQLAlchemy==2.0`); the previous case-sensitive `includes()` never
|
|
1074
|
+
// recognized Django/Flask from requirements.txt, and a Django project
|
|
1075
|
+
// then aborted `init` with "domain-groups.json has invalid totalGroups: 0".
|
|
711
1076
|
if (hasPyproject) {
|
|
712
|
-
const
|
|
1077
|
+
const ppRaw = readFileSafe(path.join(ROOT, "pyproject.toml"));
|
|
1078
|
+
const pp = ppRaw ? ppRaw.toLowerCase() : ppRaw;
|
|
713
1079
|
if (pp) {
|
|
714
1080
|
const pv = pp.match(/python\s*=\s*"[><=^~]*(\d+\.\d+)/);
|
|
715
1081
|
if (pv && !stack.languageVersion) stack.languageVersion = pv[1];
|
|
@@ -725,7 +1091,8 @@ async function detectStack(ROOT) {
|
|
|
725
1091
|
}
|
|
726
1092
|
|
|
727
1093
|
if (hasRequirements) {
|
|
728
|
-
const
|
|
1094
|
+
const rRaw = readFileSafe(path.join(ROOT, "requirements.txt"));
|
|
1095
|
+
const r = rRaw ? rRaw.toLowerCase() : rRaw;
|
|
729
1096
|
if (r) {
|
|
730
1097
|
for (const [kw, name] of pyFrameworkRules) {
|
|
731
1098
|
if (r.includes(kw) && !stack.framework) { stack.framework = name; stack.detected.push(name); break; }
|
|
@@ -752,6 +1119,224 @@ async function detectStack(ROOT) {
|
|
|
752
1119
|
}
|
|
753
1120
|
}
|
|
754
1121
|
|
|
1122
|
+
// ── Java: legacy evidence (runs AFTER Node/Python) ──
|
|
1123
|
+
// v2.5.1 — Everything below reads evidence weaker than a root build file:
|
|
1124
|
+
// sibling-directory build files, Ant / Eclipse / IntelliJ / NetBeans
|
|
1125
|
+
// metadata, jars on disk, `WEB-INF/web.xml`, Spring XSDs. It therefore runs
|
|
1126
|
+
// last and may only (a) fill a language nobody claimed, or (b) reclaim a
|
|
1127
|
+
// PROVISIONAL Node language — a root package.json that exists for gulp /
|
|
1128
|
+
// jQuery / Tailwind asset tooling, with NO Node framework detected — and
|
|
1129
|
+
// only on STRONG JVM evidence (build.xml, `.project` javanature, a sibling
|
|
1130
|
+
// pom/gradle, a WEB-INF/web.xml, or Spring jars beside *.java sources).
|
|
1131
|
+
// A Next.js / Express / Django project with a stray `.idea/misc.xml` or a
|
|
1132
|
+
// vendored `tools/lib/*.jar` is never flipped to Java.
|
|
1133
|
+
// `src/test/**` is excluded everywhere below: a test-resources `web.xml`
|
|
1134
|
+
// or a test fixture jar is not deployment evidence for the application.
|
|
1135
|
+
const jarIgnore = ["**/node_modules/**", "**/build/**", "**/target/**", "**/.git/**", "**/dist/**", "**/src/test/**"];
|
|
1136
|
+
const referencedJars = [];
|
|
1137
|
+
// Memoized: does the tree hold any *.java source at all? Shared by the
|
|
1138
|
+
// Ant / jar / last-resort decisions below so the walk happens at most once.
|
|
1139
|
+
let anyJavaMemo = null;
|
|
1140
|
+
const anyJavaSources = async () => {
|
|
1141
|
+
if (anyJavaMemo === null) anyJavaMemo = (await glob("**/*.java", { cwd: ROOT, ignore: jarIgnore, nodir: true })).length > 0;
|
|
1142
|
+
return anyJavaMemo;
|
|
1143
|
+
};
|
|
1144
|
+
let jvmMayClaim = !stack.language;
|
|
1145
|
+
// Provisional-language reclaim. The Node language is PARKED, not dropped:
|
|
1146
|
+
// if none of the JVM blocks below actually claims the project (a `build.xml`
|
|
1147
|
+
// that is not Ant — a Phing file or an empty stub — with no *.java anywhere),
|
|
1148
|
+
// the parked values are restored at the end of this section. A gulp-only
|
|
1149
|
+
// site therefore never ends up with `language: null`, and a reclaimed Java
|
|
1150
|
+
// project does not keep `packageManager: "npm"` from the asset tooling.
|
|
1151
|
+
let parked = null;
|
|
1152
|
+
if (!jvmMayClaim && languageFromPackageJson && !stack.framework && !stack.frontend && !stack.buildTool) {
|
|
1153
|
+
const strong =
|
|
1154
|
+
existsSafe(path.join(ROOT, "build.xml")) ||
|
|
1155
|
+
JVM.eclipseHasJavaNature(readFileSafe(path.join(ROOT, ".project"))) ||
|
|
1156
|
+
(await glob("*/{pom.xml,build.gradle,build.gradle.kts}", { cwd: ROOT, ignore: ["node_modules/**"] })).length > 0 ||
|
|
1157
|
+
(await glob("**/WEB-INF/web.xml", { cwd: ROOT, ignore: jarIgnore, nodir: true })).length > 0;
|
|
1158
|
+
if (strong) {
|
|
1159
|
+
jvmMayClaim = true;
|
|
1160
|
+
parked = { language: stack.language, languageVersion: stack.languageVersion, packageManager: stack.packageManager };
|
|
1161
|
+
stack.language = null; stack.languageVersion = null; stack.packageManager = null;
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
// ── Java: build files one directory down (no root build file) ──
|
|
1165
|
+
// v2.5.1 — SI repositories often hold sibling projects (`erp-web/pom.xml`,
|
|
1166
|
+
// `erp-batch/pom.xml`) with no aggregator at the root. Depth-1 poms are
|
|
1167
|
+
// absorbed with the same rules as Maven <modules>. Depth-1 Gradle files
|
|
1168
|
+
// without a root `settings.gradle` / `build.gradle` are NOT swept — they
|
|
1169
|
+
// count as strong evidence for the reclaim above and the `*.java` last
|
|
1170
|
+
// resort then sets the language, but framework/version stay null (a root
|
|
1171
|
+
// `settings.gradle` is what makes the Gradle sub-module sweep run).
|
|
1172
|
+
if (!stack.buildTool && jvmMayClaim) {
|
|
1173
|
+
const siblingPoms = (await glob("*/pom.xml", { cwd: ROOT, ignore: ["node_modules/**"] })).map(p => p.replace(/\\/g, "/")).sort();
|
|
1174
|
+
if (siblingPoms.length) {
|
|
1175
|
+
stack.buildTool = "maven"; stack.language = "java";
|
|
1176
|
+
stack.detected.push(`pom.xml (${siblingPoms.length} sibling project${siblingPoms.length > 1 ? "s" : ""})`);
|
|
1177
|
+
if (!stack.packageManager) stack.packageManager = "maven";
|
|
1178
|
+
for (const sp of siblingPoms.slice(0, 30)) {
|
|
1179
|
+
const cp = readFileSafe(path.join(ROOT, sp));
|
|
1180
|
+
if (cp) absorbMavenPom(cp, "", path.dirname(sp));
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
// ── Java: Ant / Eclipse WTP / no build tool (legacy) ──
|
|
1186
|
+
// v2.5.x — The shape of most pre-Maven enterprise code: a `build.xml`,
|
|
1187
|
+
// an Eclipse `.classpath`/`.project`, `WebContent/WEB-INF/lib/*.jar`, and
|
|
1188
|
+
// sources under `src/` (no `src/main/java`). Evidence, in order of
|
|
1189
|
+
// strength:
|
|
1190
|
+
// 1. build.xml → buildTool "ant", Java level from <javac source="">
|
|
1191
|
+
// 2. .classpath/.project → Java level from the JRE container, javanature
|
|
1192
|
+
// 3. **/WEB-INF/lib/*.jar → packaging "war"; Spring jars → framework +
|
|
1193
|
+
// version parsed from the jar NAME
|
|
1194
|
+
// (spring-webmvc-3.0.5.RELEASE.jar). An
|
|
1195
|
+
// unversioned `spring.jar` (2.0 era) reports the
|
|
1196
|
+
// framework with version null — never a guess.
|
|
1197
|
+
// 4. **/*.java → language "java" as a last resort
|
|
1198
|
+
// Only runs when no Gradle/Maven build file claimed the project.
|
|
1199
|
+
if (!stack.buildTool && jvmMayClaim) {
|
|
1200
|
+
const buildXml = path.join(ROOT, "build.xml");
|
|
1201
|
+
if (existsSafe(buildXml)) {
|
|
1202
|
+
const bx = readFileSafe(buildXml);
|
|
1203
|
+
// `<project>` alone is not Ant — Phing (PHP) and other XML build tools
|
|
1204
|
+
// use the same root element. Require a `<javac>` task or *.java sources.
|
|
1205
|
+
if (bx && /<project\b/.test(bx) && (/<javac\b/.test(bx) || await anyJavaSources())) {
|
|
1206
|
+
stack.buildTool = "ant"; stack.language = "java"; stack.detected.push("build.xml");
|
|
1207
|
+
if (!stack.packageManager) stack.packageManager = "ant";
|
|
1208
|
+
const src = JVM.antJavacSource(bx);
|
|
1209
|
+
if (src && !stack.languageVersion) stack.languageVersion = normalizeJavaVersion(src);
|
|
1210
|
+
// v2.5.1 — Ant + Ivy: ivy.xml is the dependency manifest.
|
|
1211
|
+
const ivy = readFileSafe(path.join(ROOT, "ivy.xml"));
|
|
1212
|
+
if (ivy) {
|
|
1213
|
+
stack.detected.push("ivy.xml");
|
|
1214
|
+
if (!stack.framework && JVM.ivyHasSpringFramework(ivy)) { stack.framework = "spring-framework"; stack.detected.push("spring-framework (ivy)"); }
|
|
1215
|
+
const iv = JVM.ivySpringFrameworkVersion(ivy);
|
|
1216
|
+
if (iv) { stack.springFrameworkVersion = iv; if (stack.framework === "spring-framework" && !stack.frameworkVersion) stack.frameworkVersion = iv; }
|
|
1217
|
+
if (!stack.orm) { if (IBATIS_REGEX.test(ivy)) { stack.orm = "ibatis"; stack.detected.push("ibatis (ivy)"); } else detectFirst(stack, "orm", ivy, GRADLE_ORM_RULES); }
|
|
1218
|
+
detectDb(stack, ivy, DB_KEYWORD_RULES.filter(([kw]) => !["postgres", "sqlite"].includes(kw)));
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
const classpathXml = readFileSafe(path.join(ROOT, ".classpath"));
|
|
1223
|
+
const projectXml = readFileSafe(path.join(ROOT, ".project"));
|
|
1224
|
+
const jdtPrefs = readFileSafe(path.join(ROOT, ".settings/org.eclipse.jdt.core.prefs"));
|
|
1225
|
+
if (classpathXml || projectXml || jdtPrefs) {
|
|
1226
|
+
if (JVM.eclipseHasJavaNature(projectXml) || (classpathXml && /JRE_CONTAINER|kind="src"/.test(classpathXml)) || jdtPrefs) {
|
|
1227
|
+
if (!stack.language) { stack.language = "java"; stack.detected.push(".classpath/.project"); }
|
|
1228
|
+
// `.settings/org.eclipse.jdt.core.prefs` compliance level is what the
|
|
1229
|
+
// compiler actually used — it outranks the JRE container name.
|
|
1230
|
+
const lvl = JVM.eclipseJdtPrefsLevel(jdtPrefs) || (classpathXml ? JVM.eclipseJreLevel(classpathXml) : null);
|
|
1231
|
+
if (lvl && !stack.languageVersion) stack.languageVersion = normalizeJavaVersion(lvl);
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
// IntelliJ / NetBeans project metadata.
|
|
1235
|
+
const ideaMisc = readFileSafe(path.join(ROOT, ".idea/misc.xml"));
|
|
1236
|
+
if (ideaMisc) {
|
|
1237
|
+
const lvl = JVM.intellijLanguageLevel(ideaMisc);
|
|
1238
|
+
if (lvl) { if (!stack.language) { stack.language = "java"; stack.detected.push(".idea/misc.xml"); } if (!stack.languageVersion) stack.languageVersion = normalizeJavaVersion(lvl); }
|
|
1239
|
+
}
|
|
1240
|
+
const nbProps = readFileSafe(path.join(ROOT, "nbproject/project.properties"));
|
|
1241
|
+
const nb = JVM.netbeansProject(nbProps);
|
|
1242
|
+
if (nbProps && (nb.level || nb.jars.length)) {
|
|
1243
|
+
if (!stack.language) { stack.language = "java"; stack.detected.push("nbproject"); }
|
|
1244
|
+
if (nb.level && !stack.languageVersion) stack.languageVersion = normalizeJavaVersion(nb.level);
|
|
1245
|
+
}
|
|
1246
|
+
// Jar names referenced by IDE metadata even when the jars themselves are
|
|
1247
|
+
// not committed (`.classpath kind="lib"/"var"`, NetBeans file.reference).
|
|
1248
|
+
// Fed into the same classifier as jars on disk, below.
|
|
1249
|
+
referencedJars.push(...JVM.eclipseClasspathJars(classpathXml), ...nb.jars);
|
|
1250
|
+
if (referencedJars.some(j => /WEB-INF\/lib\//.test(j)) && !stack.packaging) stack.packaging = "war";
|
|
1251
|
+
if (!stack.language && await anyJavaSources()) { stack.language = "java"; stack.detected.push("java sources"); }
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
// ── Java: jars on disk (any build tool, or none) ──
|
|
1255
|
+
// v2.5.1 — Also runs for Gradle/Maven projects that still resolve from
|
|
1256
|
+
// `fileTree(dir: 'WEB-INF/lib')` instead of coordinates — common in SI
|
|
1257
|
+
// codebases that adopted a build tool without migrating the jars. Fills
|
|
1258
|
+
// nulls only; coordinate-based answers above always win. Capped so a
|
|
1259
|
+
// vendored `lib/` with thousands of jars cannot stall detection.
|
|
1260
|
+
if (!stack.framework && (stack.language === "java" || (jvmMayClaim && !stack.buildTool))) {
|
|
1261
|
+
// Recursive under the lib roots: `lib/spring/*.jar`, `lib/db/*.jar` are
|
|
1262
|
+
// common hand-sorted layouts.
|
|
1263
|
+
const onDisk = await glob("**/{WEB-INF/lib,lib,libs}/**/*.jar", { cwd: ROOT, ignore: jarIgnore, nodir: true });
|
|
1264
|
+
const jars = [...onDisk.map(j => j.replace(/\\/g, "/")), ...referencedJars];
|
|
1265
|
+
if (jars.length) {
|
|
1266
|
+
if (jars.some(j => /WEB-INF\/lib\//.test(j)) && !stack.packaging) stack.packaging = "war";
|
|
1267
|
+
const cls = JVM.classifyJars(jars.slice(0, 500).map(j => path.basename(j)));
|
|
1268
|
+
// Jars alone are weak evidence — require *.java sources beside them.
|
|
1269
|
+
if (!stack.language && await anyJavaSources()) { stack.language = "java"; stack.detected.push("jars"); }
|
|
1270
|
+
// Everything below is dependency evidence for a JAVA project. A jar
|
|
1271
|
+
// directory with no sources is not a project and gets no DB / ORM /
|
|
1272
|
+
// framework either.
|
|
1273
|
+
if (stack.language === "java") {
|
|
1274
|
+
// JDBC drivers and ORM jars on disk are the only dependency evidence a
|
|
1275
|
+
// no-build-tool project has. Same dual output as every other DB source.
|
|
1276
|
+
for (const db of cls.databases) {
|
|
1277
|
+
if (!stack.database) stack.database = db;
|
|
1278
|
+
if (!stack.databases.includes(db)) stack.databases.push(db);
|
|
1279
|
+
}
|
|
1280
|
+
if (cls.orm && !stack.orm) { stack.orm = cls.orm; stack.detected.push(`${cls.orm} (jar)`); }
|
|
1281
|
+
if (cls.springBoot || cls.springFramework) {
|
|
1282
|
+
if (!stack.framework) {
|
|
1283
|
+
stack.framework = cls.springBoot ? "spring-boot" : "spring-framework";
|
|
1284
|
+
stack.detected.push(`${stack.framework} (jar)`);
|
|
1285
|
+
}
|
|
1286
|
+
if (cls.springFrameworkVersion && !stack.springFrameworkVersion) stack.springFrameworkVersion = cls.springFrameworkVersion;
|
|
1287
|
+
if (!stack.frameworkVersion) {
|
|
1288
|
+
if (stack.framework === "spring-boot" && cls.springBootVersion) stack.frameworkVersion = cls.springBootVersion;
|
|
1289
|
+
if (stack.framework === "spring-framework" && cls.springFrameworkVersion) stack.frameworkVersion = cls.springFrameworkVersion;
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
} // language === "java"
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
// ── Java: deployment descriptor + Spring XML schema evidence ──
|
|
1297
|
+
// v2.5.1 — For trees with no coordinates and no jars (jars gitignored,
|
|
1298
|
+
// IDE metadata absent): `WEB-INF/web.xml` naming DispatcherServlet /
|
|
1299
|
+
// ContextLoaderListener is Spring MVC evidence and a war signal; Spring XML
|
|
1300
|
+
// configs carry `spring-beans-3.0.xsd` — major.minor only, the
|
|
1301
|
+
// lowest-fidelity version source, consulted last and never overriding a
|
|
1302
|
+
// pinned version. Struts descriptors add a tag without setting a framework.
|
|
1303
|
+
// Spring Boot projects are skipped: Boot owns the servlet container, its
|
|
1304
|
+
// WAR packaging is declared in the build file, and the `detected` array of
|
|
1305
|
+
// a Boot project must stay byte-identical to v2.5.0 output.
|
|
1306
|
+
if (stack.framework !== "spring-boot" && (stack.language === "java" || (jvmMayClaim && !stack.buildTool))) {
|
|
1307
|
+
const webXmls = await glob("**/WEB-INF/web.xml", { cwd: ROOT, ignore: jarIgnore, nodir: true });
|
|
1308
|
+
for (const wx of webXmls.slice(0, 5)) {
|
|
1309
|
+
const facts = JVM.webXmlFacts(readFileSafe(path.join(ROOT, wx)));
|
|
1310
|
+
if (!stack.packaging) stack.packaging = "war";
|
|
1311
|
+
if (facts.spring) {
|
|
1312
|
+
if (!stack.language) stack.language = "java";
|
|
1313
|
+
if (!stack.framework) { stack.framework = "spring-framework"; stack.detected.push("spring-framework (web.xml)"); }
|
|
1314
|
+
}
|
|
1315
|
+
if (facts.struts && !stack.detected.some(d => d.startsWith(facts.struts))) stack.detected.push(`${facts.struts} (web.xml)`);
|
|
1316
|
+
if (facts.servletVersion && !stack.detected.some(d => d.startsWith("servlet "))) stack.detected.push(`servlet ${facts.servletVersion}`);
|
|
1317
|
+
if (!stack.language) { stack.language = "java"; stack.detected.push("web.xml"); }
|
|
1318
|
+
}
|
|
1319
|
+
if (stack.framework === "spring-framework" && !stack.frameworkVersion) {
|
|
1320
|
+
const xmls = await glob("**/{WEB-INF,resources,config,conf,spring}/**/*.xml", { cwd: ROOT, ignore: jarIgnore, nodir: true });
|
|
1321
|
+
let best = null;
|
|
1322
|
+
for (const x of xmls.slice(0, 50)) {
|
|
1323
|
+
const v = JVM.springXsdVersion(readFileSafe(path.join(ROOT, x)));
|
|
1324
|
+
if (v && (!best || parseFloat(v) > parseFloat(best))) best = v;
|
|
1325
|
+
}
|
|
1326
|
+
if (best) { stack.detected.push(`spring-xsd ${best}`); stack.frameworkVersion = best; if (!stack.springFrameworkVersion) stack.springFrameworkVersion = best; }
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
// Settle the provisional-language reclaim (see `parked` above).
|
|
1331
|
+
if (parked) {
|
|
1332
|
+
if (stack.language === "java") {
|
|
1333
|
+
stack.detected.push("java (reclaimed from provisional package.json language)");
|
|
1334
|
+
} else {
|
|
1335
|
+
stack.language = parked.language; stack.languageVersion = parked.languageVersion;
|
|
1336
|
+
if (!stack.packageManager) stack.packageManager = parked.packageManager;
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
|
|
755
1340
|
// ── DB from config files ──
|
|
756
1341
|
//
|
|
757
1342
|
// Glob covers Spring Boot's full configuration-file naming space:
|
|
@@ -868,29 +1453,41 @@ async function detectStack(ROOT) {
|
|
|
868
1453
|
}
|
|
869
1454
|
|
|
870
1455
|
// .env
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
1456
|
+
// .env: original checks postgres (not postgresql), no oracle/h2.
|
|
1457
|
+
// Preserve that semantics, but update both primary and array
|
|
1458
|
+
// outputs together.
|
|
1459
|
+
//
|
|
1460
|
+
// Returns true when a file in `files` actually DECLARED a DATABASE_URL —
|
|
1461
|
+
// not merely when a keyword matched. A runtime `.env` carrying a dialect
|
|
1462
|
+
// this keyword list does not cover (`jdbc:oracle:thin:@…`, `jdbc:sqlserver://…`)
|
|
1463
|
+
// still means the project answered the question, and a template must not
|
|
1464
|
+
// answer it differently.
|
|
1465
|
+
const detectDbFromEnvFiles = (files) => {
|
|
1466
|
+
let declared = false;
|
|
1467
|
+
for (const ef of files) {
|
|
1468
|
+
const ep = path.join(ROOT, ef);
|
|
1469
|
+
if (!existsSafe(ep)) continue;
|
|
874
1470
|
const ec = readFileSafe(ep);
|
|
875
|
-
if (ec
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
if (
|
|
887
|
-
if (!stack.database) stack.database = value;
|
|
888
|
-
if (!stack.databases.includes(value)) stack.databases.push(value);
|
|
889
|
-
}
|
|
1471
|
+
if (!ec || !ec.includes("DATABASE_URL")) continue;
|
|
1472
|
+
declared = true;
|
|
1473
|
+
const envDbs = [
|
|
1474
|
+
["postgres", "postgresql"],
|
|
1475
|
+
["mysql", "mysql"],
|
|
1476
|
+
["mongodb", "mongodb"],
|
|
1477
|
+
["sqlite", "sqlite"],
|
|
1478
|
+
];
|
|
1479
|
+
for (const [keyword, value] of envDbs) {
|
|
1480
|
+
if (ec.includes(keyword)) {
|
|
1481
|
+
if (!stack.database) stack.database = value;
|
|
1482
|
+
if (!stack.databases.includes(value)) stack.databases.push(value);
|
|
890
1483
|
}
|
|
891
1484
|
}
|
|
892
1485
|
}
|
|
893
|
-
|
|
1486
|
+
return declared;
|
|
1487
|
+
};
|
|
1488
|
+
|
|
1489
|
+
// Runtime env files first — they hold the real values. Unchanged behaviour.
|
|
1490
|
+
const runtimeEnvDeclared = detectDbFromEnvFiles([".env", ".env.local", ".env.development"]);
|
|
894
1491
|
|
|
895
1492
|
// Prisma schema
|
|
896
1493
|
const prismaSchema = path.join(ROOT, "prisma/schema.prisma");
|
|
@@ -905,6 +1502,30 @@ async function detectStack(ROOT) {
|
|
|
905
1502
|
}
|
|
906
1503
|
}
|
|
907
1504
|
|
|
1505
|
+
// v2.5.x — `.env` is gitignored in most repos, so on a fresh clone the
|
|
1506
|
+
// runtime files are absent and the DB type went undetected even though the
|
|
1507
|
+
// project committed a perfectly good `.env.example`. lib/env-parser.js has
|
|
1508
|
+
// always treated `.env.example` as the canonical "shape of truth" (it heads
|
|
1509
|
+
// ENV_FILE_ORDER); this aligns DB detection with that.
|
|
1510
|
+
//
|
|
1511
|
+
// POSITION MATTERS: this runs LAST, after every other DB source including
|
|
1512
|
+
// the Prisma block above. An `.env.example` value is a placeholder by
|
|
1513
|
+
// definition; `schema.prisma`, build files and `application.yml` are
|
|
1514
|
+
// declarative statements of intent. Hoisting this above them would let a
|
|
1515
|
+
// stale placeholder win the `if (!stack.database)` race against a real
|
|
1516
|
+
// declaration — worse than the null it replaces.
|
|
1517
|
+
//
|
|
1518
|
+
// Two guards keep it strictly additive — a template is consulted ONLY to
|
|
1519
|
+
// fill a total blank, never to contradict or pad an existing answer:
|
|
1520
|
+
// 1. no runtime env file declared a DATABASE_URL, and
|
|
1521
|
+
// 2. no other source (build.gradle / pom.xml / requirements.txt /
|
|
1522
|
+
// application.yml / schema.prisma) has identified a database.
|
|
1523
|
+
// So a placeholder `mysql://` in `.env.example` can never append a phantom
|
|
1524
|
+
// dialect to a project whose build file already said `oracle`.
|
|
1525
|
+
if (!runtimeEnvDeclared && stack.databases.length === 0 && !stack.database) {
|
|
1526
|
+
detectDbFromEnvFiles([".env.example", ".env.sample", ".env.template"]);
|
|
1527
|
+
}
|
|
1528
|
+
|
|
908
1529
|
// ── Config file fallback (monorepo) ──
|
|
909
1530
|
// [configFiles, frontendName, frameworkName (optional), label]
|
|
910
1531
|
const frontendFallbacks = [
|
|
@@ -922,6 +1543,8 @@ async function detectStack(ROOT) {
|
|
|
922
1543
|
stack.framework = frameworkName;
|
|
923
1544
|
stack.detected.push(frameworkName + " (fallback)");
|
|
924
1545
|
}
|
|
1546
|
+
// v2.5.0 — keep the bundler signal even when a backend owns `framework`.
|
|
1547
|
+
if (frameworkName === "vite") stack.frontendBundler = "vite";
|
|
925
1548
|
break;
|
|
926
1549
|
}
|
|
927
1550
|
}
|
|
@@ -932,13 +1555,65 @@ async function detectStack(ROOT) {
|
|
|
932
1555
|
// the project actually declares. This overrides framework-default guesses
|
|
933
1556
|
// in downstream code (plan-installer/index.js defaultPort) and exposes the
|
|
934
1557
|
// full variable map to Pass 3 prompts via project-analysis.json.
|
|
1558
|
+
// v2.5.0 — SPA-only sub-directory repo. Runs after EVERY backend block
|
|
1559
|
+
// (Gradle / Maven / Node / Python): a repo whose only application is the
|
|
1560
|
+
// SPA in `frontend/` must not be reported as "no language detected", but a
|
|
1561
|
+
// backend's language / package manager always takes precedence.
|
|
1562
|
+
if (subDirSpa && !stack.language) {
|
|
1563
|
+
const { subDir, sdeps } = subDirSpa;
|
|
1564
|
+
const ts = (sdeps && sdeps.typescript) || existsSafe(path.join(subDir, "tsconfig.json"));
|
|
1565
|
+
stack.language = ts ? "typescript" : "javascript";
|
|
1566
|
+
if (sdeps && sdeps.typescript && !stack.languageVersion) {
|
|
1567
|
+
const tv = String(sdeps.typescript).match(/(\d+(?:\.\d+)*)/);
|
|
1568
|
+
if (tv) stack.languageVersion = tv[1];
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
if (subDirSpa && !stack.packageManager) {
|
|
1572
|
+
const { subDir } = subDirSpa;
|
|
1573
|
+
stack.packageManager = existsSafe(path.join(subDir, "pnpm-lock.yaml")) ? "pnpm"
|
|
1574
|
+
: existsSafe(path.join(subDir, "yarn.lock")) ? "yarn"
|
|
1575
|
+
: existsSafe(path.join(subDir, "bun.lockb")) || existsSafe(path.join(subDir, "bun.lock")) ? "bun" : "npm";
|
|
1576
|
+
}
|
|
1577
|
+
|
|
935
1578
|
const envInfo = readStackEnvInfo(ROOT);
|
|
936
1579
|
if (envInfo) {
|
|
937
1580
|
stack.envInfo = envInfo;
|
|
938
1581
|
// Promote .env-declared port to stack.port if no earlier detection won
|
|
939
|
-
// (e.g., Spring application.yml parsing
|
|
940
|
-
|
|
941
|
-
|
|
1582
|
+
// (e.g., Spring application.yml parsing).
|
|
1583
|
+
//
|
|
1584
|
+
// v2.5.0 — a root `.env*` may carry BOTH a backend port and a frontend
|
|
1585
|
+
// dev-server port (`VITE_PORT` / `NEXT_PUBLIC_PORT` / `NUXT_PORT` /
|
|
1586
|
+
// `NG_PORT`). `extractPort()` prefers the frontend keys, so when a
|
|
1587
|
+
// backend exists the frontend key must go to `frontendPort`, never to the
|
|
1588
|
+
// backend's `stack.port`.
|
|
1589
|
+
const vars = envInfo.vars || {};
|
|
1590
|
+
const feKeys = Object.keys(vars).filter(k => /^(VITE_|NEXT_|NUXT_|NG_)\w*PORT$/.test(k));
|
|
1591
|
+
const backendOnlyVars = Object.fromEntries(Object.entries(vars).filter(([k]) => !feKeys.includes(k)));
|
|
1592
|
+
const backendPort = extractPort(backendOnlyVars);
|
|
1593
|
+
const frontendPort = feKeys.length ? extractPort(Object.fromEntries(feKeys.map(k => [k, vars[k]]))) : null;
|
|
1594
|
+
const hasBackend = (!!stack.framework && stack.framework !== "vite") || ["java", "kotlin", "python"].includes(stack.language);
|
|
1595
|
+
if (!stack.port) {
|
|
1596
|
+
const p = hasBackend ? backendPort : envInfo.port;
|
|
1597
|
+
if (p) stack.port = p;
|
|
1598
|
+
}
|
|
1599
|
+
if (frontendPort && stack.frontend && !stack.frontendRoot && !stack.frontendPort) stack.frontendPort = frontendPort;
|
|
1600
|
+
// Keep `stack.envInfo.port` consistent with the split: Pass 3 prompts read
|
|
1601
|
+
// `stack.envInfo.port` for the backend Server Port row, so it must never
|
|
1602
|
+
// carry the frontend dev-server value when a backend exists.
|
|
1603
|
+
envInfo.port = hasBackend ? (backendPort || null) : envInfo.port;
|
|
1604
|
+
if (frontendPort) envInfo.frontendPort = frontendPort;
|
|
1605
|
+
}
|
|
1606
|
+
// v2.5.0 — Sub-directory SPA: its own `.env*` (VITE_PORT, VITE_API_URL,
|
|
1607
|
+
// NEXT_PUBLIC_*) lives under `frontend/`, not at the project root. Read it
|
|
1608
|
+
// separately (same redaction/masking) so the dev-server port and API target
|
|
1609
|
+
// come from the project instead of framework-default guesses. Kept apart
|
|
1610
|
+
// from `stack.envInfo` / `stack.port` so a frontend `PORT=3000` never
|
|
1611
|
+
// overrides the backend's port.
|
|
1612
|
+
if (stack.frontendRoot) {
|
|
1613
|
+
const feEnv = readStackEnvInfo(path.join(ROOT, stack.frontendRoot));
|
|
1614
|
+
if (feEnv) {
|
|
1615
|
+
stack.frontendEnvInfo = { ...feEnv, source: `${stack.frontendRoot}/${feEnv.source}` };
|
|
1616
|
+
if (feEnv.port) stack.frontendPort = feEnv.port;
|
|
942
1617
|
}
|
|
943
1618
|
}
|
|
944
1619
|
|