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.
- package/CHANGELOG.md +88 -0
- package/README.de.md +4 -2
- package/README.es.md +4 -2
- package/README.fr.md +4 -2
- package/README.hi.md +4 -2
- package/README.ja.md +4 -2
- package/README.ko.md +4 -2
- package/README.md +4 -2
- package/README.ru.md +4 -2
- package/README.vi.md +4 -2
- package/README.zh-CN.md +4 -2
- package/lib/env-parser.js +198 -4
- package/package.json +92 -92
- package/pass-json-validator/index.js +1 -1
- package/pass-prompts/templates/java-spring/pass1.md +10 -2
- package/pass-prompts/templates/java-spring/pass3.md +2 -1
- package/plan-installer/index.js +17 -1
- package/plan-installer/jvm-detect.js +562 -0
- package/plan-installer/scanners/scan-java.js +82 -14
- package/plan-installer/stack-detector.js +471 -34
|
@@ -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
|
-
|
|
501
|
-
|
|
502
|
-
|
|
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.
|
|
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 =
|
|
515
|
-
|| sc.match(/
|
|
516
|
-
if (sv) stack.frameworkVersion = sv
|
|
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
|
-
|
|
651
|
-
|
|
652
|
-
|
|
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
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
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
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
if (
|
|
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 = [
|