claudeos-core 2.5.0 → 2.5.2

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.
@@ -10,6 +10,7 @@ const path = require("path");
10
10
  const { glob } = require("glob");
11
11
  const { readFileSafe, readJsonSafe, existsSafe } = require("../lib/safe-fs");
12
12
  const { readStackEnvInfo, extractPort } = require("../lib/env-parser");
13
+ const JVM = require("./jvm-detect");
13
14
 
14
15
  // ─── Lookup tables ──────────────────────────────────────────────
15
16
 
@@ -60,6 +61,10 @@ 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)
@@ -296,6 +301,13 @@ async function detectStack(ROOT) {
296
301
  loggingFrameworks: [],
297
302
  frontend: null, frontendVersion: null,
298
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,
299
311
  detected: [],
300
312
  };
301
313
 
@@ -303,6 +315,12 @@ async function detectStack(ROOT) {
303
315
  const gradleFile = existsSafe(path.join(ROOT, "build.gradle.kts"))
304
316
  ? "build.gradle.kts"
305
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
+ }
306
324
  if (gradleFile) {
307
325
  const g = readFileSafe(path.join(ROOT, gradleFile));
308
326
  if (g) {
@@ -311,12 +329,60 @@ async function detectStack(ROOT) {
311
329
  // and downstream tooling don't show "PackageMgr: none" for a build tool
312
330
  // that IS the package manager. Only set if not already detected.
313
331
  if (!stack.packageManager) stack.packageManager = "gradle";
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;
314
357
  // `spring-boot` (starter coords) OR `org.springframework.boot` (plugin id —
315
358
  // the only spelling present in a multi-module root that declares
316
359
  // `id 'org.springframework.boot' version 'x' apply false`).
317
360
  if (g.includes("spring-boot") || g.includes("org.springframework.boot")) {
318
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)");
319
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);
320
386
  const svPatterns = [
321
387
  /org\.springframework\.boot.*version\s*['"]([^'"]+)['"]/,
322
388
  /id\s*\(\s*["']org\.springframework\.boot["']\s*\)\s*version\s*["']([^"']+)["']/,
@@ -348,6 +414,15 @@ async function detectStack(ROOT) {
348
414
  if (varVal) stack.frameworkVersion = varVal[1];
349
415
  }
350
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
+ }
351
426
  // Java version — Gradle writes this in several forms. Try each
352
427
  // pattern until one matches. Earlier patterns take precedence.
353
428
  //
@@ -381,6 +456,8 @@ async function detectStack(ROOT) {
381
456
  /JavaVersion\.VERSION_(?:1_)?(\d+)/,
382
457
  // (3) toolchain block
383
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+)/,
384
461
  ];
385
462
  for (const pattern of javaVersionPatterns) {
386
463
  const m = g.match(pattern);
@@ -455,6 +532,22 @@ async function detectStack(ROOT) {
455
532
  const sbMatch = vc.match(/spring-boot\s*=\s*["']([^"']+)["']/);
456
533
  if (sbMatch) stack.frameworkVersion = sbMatch[1];
457
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
+ }
458
551
  // Version catalog ORM (labels include " (catalog)" suffix)
459
552
  if (!stack.orm && vc.includes("exposed")) { stack.orm = "exposed"; stack.detected.push("exposed (catalog)"); }
460
553
  else if (!stack.orm && vc.includes("jooq")) { stack.orm = "jooq"; stack.detected.push("jooq (catalog)"); }
@@ -497,23 +590,34 @@ async function detectStack(ROOT) {
497
590
  const sc = readFileSafe(path.join(ROOT, sbf));
498
591
  if (!sc) continue;
499
592
  if (sc.includes("kotlin") || sc.includes("org.jetbrains.kotlin")) continue; // handled by the Kotlin block
500
- const isJava = /\bid\s*\(?\s*['"](java|java-library|org\.springframework\.boot)['"]/.test(sc)
501
- || /apply\s+plugin:\s*['"](java|java-library)['"]/.test(sc)
502
- || sc.includes("spring-boot");
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);
503
599
  if (isJava) {
504
600
  // Do not `break` on the first Java module: a `core` library module
505
601
  // usually comes before the `api` module that actually declares
506
602
  // Spring Boot. Set language once, keep sweeping for framework/versions.
507
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; }
508
605
  if (!stack.framework && (sc.includes("spring-boot") || sc.includes("org.springframework.boot"))) {
509
606
  stack.framework = "spring-boot"; stack.detected.push("spring-boot (submodule)");
510
607
  }
511
- if (!stack.frameworkVersion) {
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") {
512
616
  // `spring-boot-starter-web:2.7.18`, `spring-boot-dependencies:3.2.0`,
513
617
  // or `id 'org.springframework.boot' version '3.2.5'` inside the module.
514
- const sv = sc.match(/spring-boot[\w-]*[:\s'"]+(\d+\.\d+\.\d+)/)
515
- || sc.match(/org\.springframework\.boot[^\n]*?version\s*\(?\s*['"](\d+\.\d+\.\d+)['"]/);
516
- if (sv) stack.frameworkVersion = sv[1];
618
+ const sv = JVM.gradleSpringBootVersion(sc)
619
+ || (sc.match(/spring-boot[\w-]*[:\s'"]+(\d+\.\d+\.\d+)/) || [])[1];
620
+ if (sv) stack.frameworkVersion = sv;
517
621
  }
518
622
  if (!stack.languageVersion) {
519
623
  const jv = sc.match(/(?:sourceCompatibility|targetCompatibility)\s*=\s*['"]?(\d+(?:\.\d+)?)['"]?/)
@@ -577,6 +681,39 @@ async function detectStack(ROOT) {
577
681
  }
578
682
  }
579
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
+
580
717
  // ── Java: Maven ──
581
718
  if (existsSafe(path.join(ROOT, "pom.xml"))) {
582
719
  const pom = readFileSafe(path.join(ROOT, "pom.xml"));
@@ -584,6 +721,13 @@ async function detectStack(ROOT) {
584
721
  if (!stack.buildTool) { stack.buildTool = "maven"; stack.language = "java"; stack.detected.push("pom.xml"); }
585
722
  // v2.4.0 — JVM package manager (parallel to Gradle case above).
586
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.
587
731
  const sv = pom.match(/<spring-boot[^>]*version>([^<]+)/);
588
732
  if (sv) stack.frameworkVersion = sv[1];
589
733
  // Java version — Maven commonly uses three patterns:
@@ -608,6 +752,8 @@ async function detectStack(ROOT) {
608
752
  /<java\.version>\s*(\d+(?:\.\d+)?)\s*<\/java\.version>/,
609
753
  /<maven\.compiler\.source>\s*(\d+(?:\.\d+)?)\s*<\/maven\.compiler\.source>/,
610
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>/,
611
757
  ];
612
758
  for (const pattern of mvnJavaPatterns) {
613
759
  const m = pom.match(pattern);
@@ -625,6 +771,14 @@ async function detectStack(ROOT) {
625
771
  if (propVal) stack.languageVersion = normalizeJavaVersion(propVal[1]);
626
772
  }
627
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
+ }
628
782
  // For dependency detection (framework, ORM, DB, logging), strip
629
783
  // XML block comments first. A `<!-- <dependency>...</dependency> -->`
630
784
  // block is a standard Maven pattern for disabling a dep during
@@ -636,6 +790,40 @@ async function detectStack(ROOT) {
636
790
  // resolution already scopes itself to the declared property name.
637
791
  const pomClean = stripComments(pom);
638
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
+ }
639
827
  if (IBATIS_REGEX.test(pomClean)) {
640
828
  stack.orm = "ibatis";
641
829
  stack.detected.push("ibatis");
@@ -647,14 +835,9 @@ async function detectStack(ROOT) {
647
835
  // (primary first-match → stack.database; every match →
648
836
  // stack.databases). Maven original did not push to `detected`
649
837
  // (unlike Gradle), so we preserve that omission here.
650
- const mvnDbRules = [
651
- ["postgresql", "postgresql"],
652
- ["mariadb", "mariadb"],
653
- ["mysql", "mysql"],
654
- ["oracle", "oracle"],
655
- ["mongodb", "mongodb"],
656
- ["sqlite", "sqlite"],
657
- ];
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");
658
841
  for (const [keyword, value] of mvnDbRules) {
659
842
  if (pomClean.includes(keyword)) {
660
843
  if (!stack.database) stack.database = value;
@@ -936,6 +1119,224 @@ async function detectStack(ROOT) {
936
1119
  }
937
1120
  }
938
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
+
939
1340
  // ── DB from config files ──
940
1341
  //
941
1342
  // Glob covers Spring Boot's full configuration-file naming space:
@@ -1052,29 +1453,41 @@ async function detectStack(ROOT) {
1052
1453
  }
1053
1454
 
1054
1455
  // .env
1055
- for (const ef of [".env", ".env.local", ".env.development"]) {
1056
- const ep = path.join(ROOT, ef);
1057
- if (existsSafe(ep)) {
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;
1058
1470
  const ec = readFileSafe(ep);
1059
- if (ec && ec.includes("DATABASE_URL")) {
1060
- // .env: original checks postgres (not postgresql), no oracle/h2.
1061
- // Preserve that semantics, but update both primary and array
1062
- // outputs together.
1063
- const envDbs = [
1064
- ["postgres", "postgresql"],
1065
- ["mysql", "mysql"],
1066
- ["mongodb", "mongodb"],
1067
- ["sqlite", "sqlite"],
1068
- ];
1069
- for (const [keyword, value] of envDbs) {
1070
- if (ec.includes(keyword)) {
1071
- if (!stack.database) stack.database = value;
1072
- if (!stack.databases.includes(value)) stack.databases.push(value);
1073
- }
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);
1074
1483
  }
1075
1484
  }
1076
1485
  }
1077
- }
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"]);
1078
1491
 
1079
1492
  // Prisma schema
1080
1493
  const prismaSchema = path.join(ROOT, "prisma/schema.prisma");
@@ -1089,6 +1502,30 @@ async function detectStack(ROOT) {
1089
1502
  }
1090
1503
  }
1091
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
+
1092
1529
  // ── Config file fallback (monorepo) ──
1093
1530
  // [configFiles, frontendName, frameworkName (optional), label]
1094
1531
  const frontendFallbacks = [