claudeos-core 2.4.4 → 2.5.0
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 +81 -0
- package/README.de.md +9 -9
- package/README.es.md +9 -9
- package/README.fr.md +9 -9
- package/README.hi.md +9 -9
- package/README.ja.md +9 -9
- package/README.ko.md +9 -9
- package/README.md +9 -9
- package/README.ru.md +9 -9
- package/README.vi.md +9 -9
- package/README.zh-CN.md +9 -9
- 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 +50 -12
- package/lib/memory-scaffold.js +35 -16
- package/manifest-generator/index.js +15 -4
- package/package.json +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/pass3.md +3 -3
- 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/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 +145 -14
- 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 +262 -24
- package/plan-installer/structure-scanner.js +15 -4
|
@@ -234,9 +234,70 @@ function renderAllowedPathsSection(collected) {
|
|
|
234
234
|
return lines.join("\n");
|
|
235
235
|
}
|
|
236
236
|
|
|
237
|
+
/**
|
|
238
|
+
* Deterministically insert (or replace) the `## Allowed Source Paths` section
|
|
239
|
+
* in a pass3a-facts.md body. Called by the orchestrator right after Pass 3a
|
|
240
|
+
* finishes, so the allowlist reaches Pass 3b/3c/3d verbatim — without asking
|
|
241
|
+
* the LLM to hand-copy up to 500 paths out of pass3-context.json.
|
|
242
|
+
*
|
|
243
|
+
* - If a `## Allowed Source Paths` section already exists (LLM wrote one),
|
|
244
|
+
* it is replaced wholesale up to the next `## ` heading / EOF.
|
|
245
|
+
* - Otherwise the section is appended.
|
|
246
|
+
* - `collected` is the `allowedSourcePaths` object from project-analysis.json.
|
|
247
|
+
* A missing/empty allowlist yields the documented fallback line so
|
|
248
|
+
* downstream prompts see an explicit "unavailable" marker, not silence.
|
|
249
|
+
*
|
|
250
|
+
* Returns the new markdown string.
|
|
251
|
+
*/
|
|
252
|
+
function injectAllowedPathsSection(factsMd, collected) {
|
|
253
|
+
const heading = "## Allowed Source Paths";
|
|
254
|
+
const hasPaths = collected && Array.isArray(collected.paths) && collected.paths.length > 0;
|
|
255
|
+
const body = hasPaths
|
|
256
|
+
? renderAllowedPathsSection(collected)
|
|
257
|
+
: "(allowlist unavailable — fall back to pass2-merged.json verification per file)";
|
|
258
|
+
const section = `${heading}\n\n${body}\n`;
|
|
259
|
+
|
|
260
|
+
const src = typeof factsMd === "string" ? factsMd : "";
|
|
261
|
+
const lines = src.split(/\r?\n/);
|
|
262
|
+
// Locate EVERY existing section heading outside fenced blocks. A model on a
|
|
263
|
+
// retry/resume may have written the section twice; all copies are removed
|
|
264
|
+
// and exactly one is put back at the position of the first.
|
|
265
|
+
const ranges = []; // [start, end) line indices
|
|
266
|
+
let inFence = false, cur = -1;
|
|
267
|
+
for (let i = 0; i < lines.length; i++) {
|
|
268
|
+
if (/^(```|~~~)/.test(lines[i].trimStart())) inFence = !inFence;
|
|
269
|
+
if (inFence) continue;
|
|
270
|
+
const isTarget = /^##\s+Allowed Source Paths\b/i.test(lines[i]);
|
|
271
|
+
const isHeading = /^##\s+/.test(lines[i]);
|
|
272
|
+
if (cur >= 0 && isHeading) { ranges.push([cur, i]); cur = -1; }
|
|
273
|
+
if (cur < 0 && isTarget) cur = i;
|
|
274
|
+
}
|
|
275
|
+
if (cur >= 0) ranges.push([cur, lines.length]);
|
|
276
|
+
if (ranges.length > 0) {
|
|
277
|
+
const out = [];
|
|
278
|
+
let pos = 0;
|
|
279
|
+
ranges.forEach(([a, b], idx) => {
|
|
280
|
+
out.push(...lines.slice(pos, a));
|
|
281
|
+
if (idx === 0) out.push(...section.split("\n"));
|
|
282
|
+
pos = b;
|
|
283
|
+
});
|
|
284
|
+
out.push(...lines.slice(pos));
|
|
285
|
+
return out.join("\n");
|
|
286
|
+
}
|
|
287
|
+
// Rebuild from `lines` (not `src`) so a CRLF input comes out as consistent LF
|
|
288
|
+
// instead of a mixed-ending file (CRLF body + LF appended section).
|
|
289
|
+
let trimmed = lines.join("\n").replace(/\s+$/, "");
|
|
290
|
+
// If the file ends inside an unclosed fence (LLM forgot the closing ```),
|
|
291
|
+
// close it first — otherwise the appended heading would sit inside a code
|
|
292
|
+
// block and be invisible to fence-aware readers of this file.
|
|
293
|
+
if (inFence) trimmed += "\n```";
|
|
294
|
+
return (trimmed ? trimmed + "\n\n" : "") + section;
|
|
295
|
+
}
|
|
296
|
+
|
|
237
297
|
module.exports = {
|
|
238
298
|
collectSourcePaths,
|
|
239
299
|
renderAllowedPathsSection,
|
|
300
|
+
injectAllowedPathsSection,
|
|
240
301
|
// Exported for test visibility.
|
|
241
302
|
_constants: { SOURCE_EXTENSIONS, EXCLUDED_DIRS, MAX_PATHS, MIN_FILES_PER_DIR, MAX_DIRS },
|
|
242
303
|
};
|
|
@@ -9,7 +9,7 @@
|
|
|
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
13
|
|
|
14
14
|
// ─── Lookup tables ──────────────────────────────────────────────
|
|
15
15
|
|
|
@@ -65,6 +65,33 @@ const DB_KEYWORD_RULES = [
|
|
|
65
65
|
// h2 needs word-boundary check (avoid oauth2, cache2k false positives)
|
|
66
66
|
const H2_REGEX = /\bh2\b/;
|
|
67
67
|
|
|
68
|
+
// Java version literals come in two spellings: modern `17` / `21` and the
|
|
69
|
+
// legacy dotted form `1.8` (Java 8 — still the norm in enterprise SI
|
|
70
|
+
// codebases). Normalize the legacy form so `1.8` → `8`; pass everything
|
|
71
|
+
// else through unchanged.
|
|
72
|
+
function normalizeJavaVersion(v) {
|
|
73
|
+
if (v == null) return v;
|
|
74
|
+
const m = String(v).match(/^1\.(\d+)$/);
|
|
75
|
+
return m ? m[1] : String(v);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// v2.5.0 — Source-language evidence. Build-file keywords alone are ambiguous:
|
|
79
|
+
// `buildSrc/build.gradle.kts` carries `kotlin-dsl` in pure-Java repos, and a
|
|
80
|
+
// Java catalog may pin `kotlin = "1.9.22"` only to settle kotlin-stdlib
|
|
81
|
+
// conflicts. When `.java` sources exist and no `.kt` sources do, a "kotlin"
|
|
82
|
+
// keyword must NOT flip the language — the Kotlin scanner would then find
|
|
83
|
+
// zero domains and `init` would abort. Memoized per detectStack() call.
|
|
84
|
+
async function hasJavaOnlySources(ROOT) {
|
|
85
|
+
// `buildSrc/` and `build-logic/` (Gradle's documented buildSrc replacement)
|
|
86
|
+
// hold convention plugins written in Kotlin DSL — build tooling, not
|
|
87
|
+
// application code.
|
|
88
|
+
const ignore = ["**/node_modules/**", "**/build/**", "**/target/**", "**/buildSrc/**", "**/build-logic/**", "**/gradle/plugins/**", "**/src/test/**", "**/.git/**"];
|
|
89
|
+
const kt = await glob("**/src/main/{java,kotlin}/**/*.kt", { cwd: ROOT, ignore });
|
|
90
|
+
if (kt.length > 0) return false;
|
|
91
|
+
const java = await glob("**/src/main/java/**/*.java", { cwd: ROOT, ignore });
|
|
92
|
+
return java.length > 0;
|
|
93
|
+
}
|
|
94
|
+
|
|
68
95
|
// ─── Helpers ────────────────────────────────────────────────────
|
|
69
96
|
|
|
70
97
|
function detectFirst(stack, field, content, rules) {
|
|
@@ -233,6 +260,16 @@ function detectDb(stack, content, rules) {
|
|
|
233
260
|
* @returns {Promise<object>} stack info
|
|
234
261
|
*/
|
|
235
262
|
async function detectStack(ROOT) {
|
|
263
|
+
// Lazily evaluated once per call; only consulted when a "kotlin" keyword
|
|
264
|
+
// would otherwise flip the language (see hasJavaOnlySources).
|
|
265
|
+
let javaOnlyMemo = null;
|
|
266
|
+
const isJavaOnly = async () => {
|
|
267
|
+
if (javaOnlyMemo === null) javaOnlyMemo = await hasJavaOnlySources(ROOT);
|
|
268
|
+
return javaOnlyMemo;
|
|
269
|
+
};
|
|
270
|
+
// Set when `language` was taken from a root package.json (see Node block);
|
|
271
|
+
// the Python block reclaims it when the backend turns out to be Python.
|
|
272
|
+
let languageFromPackageJson = false;
|
|
236
273
|
const stack = {
|
|
237
274
|
language: null, languageVersion: null,
|
|
238
275
|
framework: null, frameworkVersion: null,
|
|
@@ -274,7 +311,12 @@ async function detectStack(ROOT) {
|
|
|
274
311
|
// and downstream tooling don't show "PackageMgr: none" for a build tool
|
|
275
312
|
// that IS the package manager. Only set if not already detected.
|
|
276
313
|
if (!stack.packageManager) stack.packageManager = "gradle";
|
|
277
|
-
|
|
314
|
+
// `spring-boot` (starter coords) OR `org.springframework.boot` (plugin id —
|
|
315
|
+
// the only spelling present in a multi-module root that declares
|
|
316
|
+
// `id 'org.springframework.boot' version 'x' apply false`).
|
|
317
|
+
if (g.includes("spring-boot") || g.includes("org.springframework.boot")) {
|
|
318
|
+
stack.language = "java"; stack.framework = "spring-boot"; stack.detected.push("spring-boot");
|
|
319
|
+
}
|
|
278
320
|
const svPatterns = [
|
|
279
321
|
/org\.springframework\.boot.*version\s*['"]([^'"]+)['"]/,
|
|
280
322
|
/id\s*\(\s*["']org\.springframework\.boot["']\s*\)\s*version\s*["']([^"']+)["']/,
|
|
@@ -332,8 +374,9 @@ async function detectStack(ROOT) {
|
|
|
332
374
|
// Java 21).
|
|
333
375
|
const javaVersionPatterns = [
|
|
334
376
|
// (1) numeric literal on sourceCompatibility or targetCompatibility
|
|
335
|
-
|
|
336
|
-
/
|
|
377
|
+
// `(\d+(?:\.\d+)?)` — captures `1.8` whole instead of stopping at `1`
|
|
378
|
+
/sourceCompatibility\s*=\s*['"]?(\d+(?:\.\d+)?)['"]?/,
|
|
379
|
+
/targetCompatibility\s*=\s*['"]?(\d+(?:\.\d+)?)['"]?/,
|
|
337
380
|
// (2) JavaVersion enum — supports both VERSION_21 and VERSION_1_8
|
|
338
381
|
/JavaVersion\.VERSION_(?:1_)?(\d+)/,
|
|
339
382
|
// (3) toolchain block
|
|
@@ -341,7 +384,7 @@ async function detectStack(ROOT) {
|
|
|
341
384
|
];
|
|
342
385
|
for (const pattern of javaVersionPatterns) {
|
|
343
386
|
const m = g.match(pattern);
|
|
344
|
-
if (m) { stack.languageVersion = m[1]; break; }
|
|
387
|
+
if (m) { stack.languageVersion = normalizeJavaVersion(m[1]); break; }
|
|
345
388
|
}
|
|
346
389
|
// (4) ext variable reference fallback — if the Compatibility
|
|
347
390
|
// assignment used "${varName}" we now resolve varName inside the
|
|
@@ -356,9 +399,9 @@ async function detectStack(ROOT) {
|
|
|
356
399
|
// defensive guard against unexpected characters, not a
|
|
357
400
|
// practical necessity for today's inputs.
|
|
358
401
|
const escapedVarName = varName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
359
|
-
const extAssign = new RegExp(`${escapedVarName}\\s*=\\s*['"]?(\\d+)['"]?`);
|
|
402
|
+
const extAssign = new RegExp(`${escapedVarName}\\s*=\\s*['"]?(\\d+(?:\\.\\d+)?)['"]?`);
|
|
360
403
|
const extVal = g.match(extAssign);
|
|
361
|
-
if (extVal) stack.languageVersion = extVal[1];
|
|
404
|
+
if (extVal) stack.languageVersion = normalizeJavaVersion(extVal[1]);
|
|
362
405
|
}
|
|
363
406
|
}
|
|
364
407
|
|
|
@@ -382,7 +425,8 @@ async function detectStack(ROOT) {
|
|
|
382
425
|
detectLogging(stack, g);
|
|
383
426
|
|
|
384
427
|
// Kotlin detection: override language if Kotlin plugin found
|
|
385
|
-
|
|
428
|
+
// (unless the source tree is Java-only — see hasJavaOnlySources).
|
|
429
|
+
if ((g.includes("kotlin") || g.includes("org.jetbrains.kotlin")) && !(await isJavaOnly())) {
|
|
386
430
|
stack.language = "kotlin"; stack.detected.push("kotlin");
|
|
387
431
|
const kvPatterns = [
|
|
388
432
|
/kotlin\S*\s*version\s*['"]([^'"]+)['"]/,
|
|
@@ -427,15 +471,66 @@ async function detectStack(ROOT) {
|
|
|
427
471
|
if (!stack.databases.includes(value)) stack.databases.push(value);
|
|
428
472
|
}
|
|
429
473
|
}
|
|
430
|
-
|
|
474
|
+
// A `kotlin = "x.y.z"` version entry or a Kotlin *plugin* coordinate in
|
|
475
|
+
// the catalog is decisive — it overrides the `java` default that the
|
|
476
|
+
// root build file's `org.springframework.boot` plugin id sets.
|
|
477
|
+
// Library coordinates (`org.jetbrains.kotlin:kotlin-stdlib`) are NOT a
|
|
478
|
+
// signal: Java projects pin them in the catalog to settle transitive
|
|
479
|
+
// version conflicts without writing a line of Kotlin.
|
|
480
|
+
const KOTLIN_CATALOG_RE = /(^\s*kotlin\s*=|org\.jetbrains\.kotlin\.(?:jvm|plugin|multiplatform|android|kapt)|kotlin-gradle-plugin)/m;
|
|
481
|
+
if (stack.language !== "kotlin" && KOTLIN_CATALOG_RE.test(vc) && !(await isJavaOnly())) {
|
|
431
482
|
stack.language = "kotlin"; stack.detected.push("kotlin (catalog)");
|
|
432
483
|
}
|
|
433
484
|
}
|
|
434
485
|
}
|
|
435
486
|
|
|
487
|
+
// ── Java: multi-module Gradle detection ──
|
|
488
|
+
// Root build.gradle of a multi-module project often holds only
|
|
489
|
+
// `allprojects { repositories {...} }` and no framework coords at all;
|
|
490
|
+
// the real declarations live in `api/build.gradle`, `core/build.gradle`, …
|
|
491
|
+
// Scan sub-module build files (same bound as the Kotlin block below) for
|
|
492
|
+
// the Java plugin / Spring Boot coords so the project isn't reported as
|
|
493
|
+
// "no language detected" and the Java scanner actually runs.
|
|
494
|
+
if (!stack.language && stack.buildTool === "gradle") {
|
|
495
|
+
const subBuildFiles = await glob("*/**/build.gradle{,.kts}", { cwd: ROOT, ignore: ["**/node_modules/**", "**/build/**", "**/buildSrc/**"] });
|
|
496
|
+
for (const sbf of subBuildFiles.slice(0, 30)) {
|
|
497
|
+
const sc = readFileSafe(path.join(ROOT, sbf));
|
|
498
|
+
if (!sc) continue;
|
|
499
|
+
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");
|
|
503
|
+
if (isJava) {
|
|
504
|
+
// Do not `break` on the first Java module: a `core` library module
|
|
505
|
+
// usually comes before the `api` module that actually declares
|
|
506
|
+
// Spring Boot. Set language once, keep sweeping for framework/versions.
|
|
507
|
+
if (stack.language !== "java") { stack.language = "java"; stack.detected.push("java (submodule)"); }
|
|
508
|
+
if (!stack.framework && (sc.includes("spring-boot") || sc.includes("org.springframework.boot"))) {
|
|
509
|
+
stack.framework = "spring-boot"; stack.detected.push("spring-boot (submodule)");
|
|
510
|
+
}
|
|
511
|
+
if (!stack.frameworkVersion) {
|
|
512
|
+
// `spring-boot-starter-web:2.7.18`, `spring-boot-dependencies:3.2.0`,
|
|
513
|
+
// 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];
|
|
517
|
+
}
|
|
518
|
+
if (!stack.languageVersion) {
|
|
519
|
+
const jv = sc.match(/(?:sourceCompatibility|targetCompatibility)\s*=\s*['"]?(\d+(?:\.\d+)?)['"]?/)
|
|
520
|
+
|| sc.match(/JavaVersion\.VERSION_(?:1_)?(\d+)/)
|
|
521
|
+
|| sc.match(/JavaLanguageVersion\.of\s*\(\s*(\d+)\s*\)/);
|
|
522
|
+
if (jv) stack.languageVersion = normalizeJavaVersion(jv[1]);
|
|
523
|
+
}
|
|
524
|
+
if (stack.framework && stack.frameworkVersion && stack.languageVersion) break;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
436
529
|
// ── Kotlin: multi-module Gradle detection ──
|
|
437
|
-
|
|
438
|
-
|
|
530
|
+
// v2.5.0: `buildSrc/` is ignored (its `kotlin-dsl` plugin is not evidence of
|
|
531
|
+
// Kotlin application code) and a Java-only source tree never flips.
|
|
532
|
+
if (stack.language !== "kotlin" && stack.buildTool === "gradle" && !(await isJavaOnly())) {
|
|
533
|
+
const subBuildFiles = await glob("**/build.gradle{,.kts}", { cwd: ROOT, ignore: ["**/node_modules/**", "**/build/**", "**/buildSrc/**"] });
|
|
439
534
|
for (const sbf of subBuildFiles.slice(0, 5)) {
|
|
440
535
|
const sc = readFileSafe(path.join(ROOT, sbf));
|
|
441
536
|
if (sc && (sc.includes("kotlin") || sc.includes("org.jetbrains.kotlin"))) {
|
|
@@ -510,13 +605,13 @@ async function detectStack(ROOT) {
|
|
|
510
605
|
// pom.xml (cross-file resolution — parent pom, BOM — is out of
|
|
511
606
|
// scope; the resulting null falls through to LLM-side analysis).
|
|
512
607
|
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>/,
|
|
608
|
+
/<java\.version>\s*(\d+(?:\.\d+)?)\s*<\/java\.version>/,
|
|
609
|
+
/<maven\.compiler\.source>\s*(\d+(?:\.\d+)?)\s*<\/maven\.compiler\.source>/,
|
|
610
|
+
/<maven\.compiler\.target>\s*(\d+(?:\.\d+)?)\s*<\/maven\.compiler\.target>/,
|
|
516
611
|
];
|
|
517
612
|
for (const pattern of mvnJavaPatterns) {
|
|
518
613
|
const m = pom.match(pattern);
|
|
519
|
-
if (m) { stack.languageVersion = m[1]; break; }
|
|
614
|
+
if (m) { stack.languageVersion = normalizeJavaVersion(m[1]); break; }
|
|
520
615
|
}
|
|
521
616
|
// Pattern 3 fallback: if <java.version> references a property,
|
|
522
617
|
// resolve it inside the same pom.
|
|
@@ -525,9 +620,9 @@ async function detectStack(ROOT) {
|
|
|
525
620
|
if (propRef) {
|
|
526
621
|
const propName = propRef[1].trim();
|
|
527
622
|
const escapedProp = propName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
528
|
-
const propDef = new RegExp(`<${escapedProp}>\\s*(\\d+)\\s*</${escapedProp}>`);
|
|
623
|
+
const propDef = new RegExp(`<${escapedProp}>\\s*(\\d+(?:\\.\\d+)?)\\s*</${escapedProp}>`);
|
|
529
624
|
const propVal = pom.match(propDef);
|
|
530
|
-
if (propVal) stack.languageVersion = propVal[1];
|
|
625
|
+
if (propVal) stack.languageVersion = normalizeJavaVersion(propVal[1]);
|
|
531
626
|
}
|
|
532
627
|
}
|
|
533
628
|
// For dependency detection (framework, ORM, DB, logging), strip
|
|
@@ -628,7 +723,10 @@ async function detectStack(ROOT) {
|
|
|
628
723
|
}
|
|
629
724
|
}
|
|
630
725
|
|
|
631
|
-
|
|
726
|
+
// Provisional: a root package.json may exist only for frontend tooling
|
|
727
|
+
// (Tailwind/PostCSS) in a Python repo. The Python block below reclaims
|
|
728
|
+
// `language` when a Python framework is detected and no Node backend is.
|
|
729
|
+
if (!stack.language) { stack.language = deps.typescript ? "typescript" : "javascript"; languageFromPackageJson = true; }
|
|
632
730
|
if (deps.typescript) { stack.detected.push("typescript"); const tv = deps.typescript.match(/(\d+(?:\.\d+)*)/); if (tv) stack.languageVersion = tv[1]; }
|
|
633
731
|
|
|
634
732
|
// Frontend (Angular checked before React — Angular projects may include react in devDependencies)
|
|
@@ -666,6 +764,14 @@ async function detectStack(ROOT) {
|
|
|
666
764
|
stack.detected.push("vite");
|
|
667
765
|
stack.frameworkVersion = deps.vite.replace(/[^0-9.]/g, "");
|
|
668
766
|
}
|
|
767
|
+
// v2.5.0 — Record the frontend bundler independently of `framework`.
|
|
768
|
+
// When a backend framework occupies `stack.framework` (Spring + React/Vite
|
|
769
|
+
// in one repo), `framework === "vite"` can never be true, and
|
|
770
|
+
// selectTemplates() used to fall back to the Next.js template for a
|
|
771
|
+
// Vite SPA. `frontendBundler` carries that signal regardless of backend.
|
|
772
|
+
if (deps.vite && stack.frontend && stack.frontend !== "nextjs") {
|
|
773
|
+
stack.frontendBundler = "vite";
|
|
774
|
+
}
|
|
669
775
|
|
|
670
776
|
// ORM
|
|
671
777
|
for (const [depKeys, ormName] of NODE_ORM_RULES) {
|
|
@@ -698,18 +804,95 @@ async function detectStack(ROOT) {
|
|
|
698
804
|
}
|
|
699
805
|
}
|
|
700
806
|
|
|
807
|
+
let subDirSpa = null;
|
|
808
|
+
// ── Frontend in a sub-directory (v2.5.0) ──
|
|
809
|
+
// Spring/Django/... repos commonly keep the SPA in frontend/, client/, web/ or
|
|
810
|
+
// ui/ with its own package.json and no root package.json. Detect it there and
|
|
811
|
+
// record the sub-directory so the frontend scanner can be rooted at it.
|
|
812
|
+
if (!stack.frontend) {
|
|
813
|
+
for (const sub of ["frontend", "client", "web", "ui", "webapp", "front"]) {
|
|
814
|
+
const subDir = path.join(ROOT, sub);
|
|
815
|
+
const pj = path.join(subDir, "package.json");
|
|
816
|
+
const spkg = existsSafe(pj) ? readJsonSafe(pj) : null;
|
|
817
|
+
const sdeps = spkg ? { ...(spkg.dependencies || {}), ...(spkg.devDependencies || {}) } : null;
|
|
818
|
+
if (sdeps) {
|
|
819
|
+
// Same precedence as the root package.json rules (Angular before React —
|
|
820
|
+
// Angular projects may carry react in devDependencies). `nuxt` maps to
|
|
821
|
+
// vue so a Nuxt app is not missed when `vue` is only transitive.
|
|
822
|
+
const rules = [["next", "nextjs", "next.js"], ["@angular/core", "angular", "angular"], ["nuxt", "vue", "nuxt"], ["react", "react", "react"], ["vue", "vue", "vue"]];
|
|
823
|
+
for (const [dep, name, label] of rules) {
|
|
824
|
+
if (sdeps[dep]) {
|
|
825
|
+
stack.frontend = name;
|
|
826
|
+
// `frontendVersion` is the framework's version (Vue for a Nuxt app),
|
|
827
|
+
// never the meta-framework's — a Nuxt 3.11 app is not "Vue 3.11".
|
|
828
|
+
const verDep = dep === "nuxt" ? (sdeps.vue ? "vue" : null) : dep;
|
|
829
|
+
stack.frontendVersion = verDep ? String(sdeps[verDep]).replace(/[^0-9.]/g, "") : null;
|
|
830
|
+
stack.frontendRoot = sub;
|
|
831
|
+
stack.detected.push(`${label} (${sub}/)`);
|
|
832
|
+
break;
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
if (stack.frontend && sdeps.vite && stack.frontend !== "nextjs") stack.frontendBundler = "vite";
|
|
836
|
+
}
|
|
837
|
+
// Config-file fallback inside the sub-directory (mirrors the root fallback
|
|
838
|
+
// below): a package.json without a recognizable framework dep, or none at all.
|
|
839
|
+
if (!stack.frontend) {
|
|
840
|
+
const subFallbacks = [
|
|
841
|
+
[["next.config.js", "next.config.mjs", "next.config.ts"], "nextjs", null, "next.config"],
|
|
842
|
+
[["vite.config.ts", "vite.config.js"], "react", "vite", "vite.config"],
|
|
843
|
+
[["nuxt.config.ts", "nuxt.config.js"], "vue", null, "nuxt.config"],
|
|
844
|
+
[["angular.json", ".angular.json"], "angular", null, "angular.json"],
|
|
845
|
+
];
|
|
846
|
+
for (const [files, frontendName, bundler, label] of subFallbacks) {
|
|
847
|
+
if (files.some(f => existsSafe(path.join(subDir, f)))) {
|
|
848
|
+
stack.frontend = frontendName;
|
|
849
|
+
stack.frontendRoot = sub;
|
|
850
|
+
if (bundler) stack.frontendBundler = bundler;
|
|
851
|
+
stack.detected.push(`${label} (${sub}/, fallback)`);
|
|
852
|
+
break;
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
if (stack.frontend) {
|
|
857
|
+
// Remember the sub-directory; language / package manager are filled
|
|
858
|
+
// AFTER every backend block has run (see "SPA-only sub-directory
|
|
859
|
+
// repo" below). Filling them here would pre-empt the Python block's
|
|
860
|
+
// `if (!stack.language)` and turn a Django + frontend/ repo into
|
|
861
|
+
// "typescript", silently skipping the Python scanner.
|
|
862
|
+
subDirSpa = { subDir, sdeps };
|
|
863
|
+
break;
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
|
|
701
868
|
// ── Python ──
|
|
702
869
|
const hasPyproject = existsSafe(path.join(ROOT, "pyproject.toml"));
|
|
703
870
|
const hasRequirements = existsSafe(path.join(ROOT, "requirements.txt"));
|
|
704
871
|
if (hasPyproject || hasRequirements) {
|
|
705
|
-
|
|
872
|
+
// v2.5.0 — a Python manifest at the root beats a `language` that came
|
|
873
|
+
// from a root package.json with NO Node backend framework: that
|
|
874
|
+
// package.json exists for Tailwind/PostCSS/ESLint tooling, and the
|
|
875
|
+
// backend is Python. (A NestJS/Express/Fastify framework keeps Node.)
|
|
876
|
+
const NODE_BACKENDS = ["nestjs", "express", "fastify"];
|
|
877
|
+
if (!stack.language) {
|
|
878
|
+
stack.language = "python";
|
|
879
|
+
} else if (languageFromPackageJson && !NODE_BACKENDS.includes(stack.framework)) {
|
|
880
|
+
stack.language = "python";
|
|
881
|
+
stack.languageVersion = null; // was the TypeScript version; Python's is read below
|
|
882
|
+
}
|
|
706
883
|
stack.detected.push("python");
|
|
707
884
|
|
|
708
885
|
const pyFrameworkRules = [["django", "django"], ["fastapi", "fastapi"], ["flask", "flask"]];
|
|
709
886
|
const pyOrmRules = [["sqlalchemy", "sqlalchemy"], ["tortoise", "tortoise-orm"]];
|
|
710
887
|
|
|
888
|
+
// v2.5.0 — keyword matching is case-insensitive. `pip freeze` and PyPI
|
|
889
|
+
// canonical names are capitalized (`Django==5.0`, `Flask==3.0`,
|
|
890
|
+
// `SQLAlchemy==2.0`); the previous case-sensitive `includes()` never
|
|
891
|
+
// recognized Django/Flask from requirements.txt, and a Django project
|
|
892
|
+
// then aborted `init` with "domain-groups.json has invalid totalGroups: 0".
|
|
711
893
|
if (hasPyproject) {
|
|
712
|
-
const
|
|
894
|
+
const ppRaw = readFileSafe(path.join(ROOT, "pyproject.toml"));
|
|
895
|
+
const pp = ppRaw ? ppRaw.toLowerCase() : ppRaw;
|
|
713
896
|
if (pp) {
|
|
714
897
|
const pv = pp.match(/python\s*=\s*"[><=^~]*(\d+\.\d+)/);
|
|
715
898
|
if (pv && !stack.languageVersion) stack.languageVersion = pv[1];
|
|
@@ -725,7 +908,8 @@ async function detectStack(ROOT) {
|
|
|
725
908
|
}
|
|
726
909
|
|
|
727
910
|
if (hasRequirements) {
|
|
728
|
-
const
|
|
911
|
+
const rRaw = readFileSafe(path.join(ROOT, "requirements.txt"));
|
|
912
|
+
const r = rRaw ? rRaw.toLowerCase() : rRaw;
|
|
729
913
|
if (r) {
|
|
730
914
|
for (const [kw, name] of pyFrameworkRules) {
|
|
731
915
|
if (r.includes(kw) && !stack.framework) { stack.framework = name; stack.detected.push(name); break; }
|
|
@@ -922,6 +1106,8 @@ async function detectStack(ROOT) {
|
|
|
922
1106
|
stack.framework = frameworkName;
|
|
923
1107
|
stack.detected.push(frameworkName + " (fallback)");
|
|
924
1108
|
}
|
|
1109
|
+
// v2.5.0 — keep the bundler signal even when a backend owns `framework`.
|
|
1110
|
+
if (frameworkName === "vite") stack.frontendBundler = "vite";
|
|
925
1111
|
break;
|
|
926
1112
|
}
|
|
927
1113
|
}
|
|
@@ -932,13 +1118,65 @@ async function detectStack(ROOT) {
|
|
|
932
1118
|
// the project actually declares. This overrides framework-default guesses
|
|
933
1119
|
// in downstream code (plan-installer/index.js defaultPort) and exposes the
|
|
934
1120
|
// full variable map to Pass 3 prompts via project-analysis.json.
|
|
1121
|
+
// v2.5.0 — SPA-only sub-directory repo. Runs after EVERY backend block
|
|
1122
|
+
// (Gradle / Maven / Node / Python): a repo whose only application is the
|
|
1123
|
+
// SPA in `frontend/` must not be reported as "no language detected", but a
|
|
1124
|
+
// backend's language / package manager always takes precedence.
|
|
1125
|
+
if (subDirSpa && !stack.language) {
|
|
1126
|
+
const { subDir, sdeps } = subDirSpa;
|
|
1127
|
+
const ts = (sdeps && sdeps.typescript) || existsSafe(path.join(subDir, "tsconfig.json"));
|
|
1128
|
+
stack.language = ts ? "typescript" : "javascript";
|
|
1129
|
+
if (sdeps && sdeps.typescript && !stack.languageVersion) {
|
|
1130
|
+
const tv = String(sdeps.typescript).match(/(\d+(?:\.\d+)*)/);
|
|
1131
|
+
if (tv) stack.languageVersion = tv[1];
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
if (subDirSpa && !stack.packageManager) {
|
|
1135
|
+
const { subDir } = subDirSpa;
|
|
1136
|
+
stack.packageManager = existsSafe(path.join(subDir, "pnpm-lock.yaml")) ? "pnpm"
|
|
1137
|
+
: existsSafe(path.join(subDir, "yarn.lock")) ? "yarn"
|
|
1138
|
+
: existsSafe(path.join(subDir, "bun.lockb")) || existsSafe(path.join(subDir, "bun.lock")) ? "bun" : "npm";
|
|
1139
|
+
}
|
|
1140
|
+
|
|
935
1141
|
const envInfo = readStackEnvInfo(ROOT);
|
|
936
1142
|
if (envInfo) {
|
|
937
1143
|
stack.envInfo = envInfo;
|
|
938
1144
|
// Promote .env-declared port to stack.port if no earlier detection won
|
|
939
|
-
// (e.g., Spring application.yml parsing
|
|
940
|
-
|
|
941
|
-
|
|
1145
|
+
// (e.g., Spring application.yml parsing).
|
|
1146
|
+
//
|
|
1147
|
+
// v2.5.0 — a root `.env*` may carry BOTH a backend port and a frontend
|
|
1148
|
+
// dev-server port (`VITE_PORT` / `NEXT_PUBLIC_PORT` / `NUXT_PORT` /
|
|
1149
|
+
// `NG_PORT`). `extractPort()` prefers the frontend keys, so when a
|
|
1150
|
+
// backend exists the frontend key must go to `frontendPort`, never to the
|
|
1151
|
+
// backend's `stack.port`.
|
|
1152
|
+
const vars = envInfo.vars || {};
|
|
1153
|
+
const feKeys = Object.keys(vars).filter(k => /^(VITE_|NEXT_|NUXT_|NG_)\w*PORT$/.test(k));
|
|
1154
|
+
const backendOnlyVars = Object.fromEntries(Object.entries(vars).filter(([k]) => !feKeys.includes(k)));
|
|
1155
|
+
const backendPort = extractPort(backendOnlyVars);
|
|
1156
|
+
const frontendPort = feKeys.length ? extractPort(Object.fromEntries(feKeys.map(k => [k, vars[k]]))) : null;
|
|
1157
|
+
const hasBackend = (!!stack.framework && stack.framework !== "vite") || ["java", "kotlin", "python"].includes(stack.language);
|
|
1158
|
+
if (!stack.port) {
|
|
1159
|
+
const p = hasBackend ? backendPort : envInfo.port;
|
|
1160
|
+
if (p) stack.port = p;
|
|
1161
|
+
}
|
|
1162
|
+
if (frontendPort && stack.frontend && !stack.frontendRoot && !stack.frontendPort) stack.frontendPort = frontendPort;
|
|
1163
|
+
// Keep `stack.envInfo.port` consistent with the split: Pass 3 prompts read
|
|
1164
|
+
// `stack.envInfo.port` for the backend Server Port row, so it must never
|
|
1165
|
+
// carry the frontend dev-server value when a backend exists.
|
|
1166
|
+
envInfo.port = hasBackend ? (backendPort || null) : envInfo.port;
|
|
1167
|
+
if (frontendPort) envInfo.frontendPort = frontendPort;
|
|
1168
|
+
}
|
|
1169
|
+
// v2.5.0 — Sub-directory SPA: its own `.env*` (VITE_PORT, VITE_API_URL,
|
|
1170
|
+
// NEXT_PUBLIC_*) lives under `frontend/`, not at the project root. Read it
|
|
1171
|
+
// separately (same redaction/masking) so the dev-server port and API target
|
|
1172
|
+
// come from the project instead of framework-default guesses. Kept apart
|
|
1173
|
+
// from `stack.envInfo` / `stack.port` so a frontend `PORT=3000` never
|
|
1174
|
+
// overrides the backend's port.
|
|
1175
|
+
if (stack.frontendRoot) {
|
|
1176
|
+
const feEnv = readStackEnvInfo(path.join(ROOT, stack.frontendRoot));
|
|
1177
|
+
if (feEnv) {
|
|
1178
|
+
stack.frontendEnvInfo = { ...feEnv, source: `${stack.frontendRoot}/${feEnv.source}` };
|
|
1179
|
+
if (feEnv.port) stack.frontendPort = feEnv.port;
|
|
942
1180
|
}
|
|
943
1181
|
}
|
|
944
1182
|
|
|
@@ -17,6 +17,7 @@ const { scanKotlinDomains, resolveSharedQueryDomains } = require("./scanners/sca
|
|
|
17
17
|
const { scanNodeDomains } = require("./scanners/scan-node");
|
|
18
18
|
const { scanPythonDomains } = require("./scanners/scan-python");
|
|
19
19
|
const { scanFrontendDomains, countFrontendStats } = require("./scanners/scan-frontend");
|
|
20
|
+
const path = require("path");
|
|
20
21
|
|
|
21
22
|
async function scanStructure(stack, ROOT) {
|
|
22
23
|
let backendDomains = [];
|
|
@@ -36,22 +37,32 @@ async function scanStructure(stack, ROOT) {
|
|
|
36
37
|
if (r.rootPackage) rootPackage = r.rootPackage;
|
|
37
38
|
}
|
|
38
39
|
|
|
39
|
-
|
|
40
|
+
// v2.5.0 — dispatch is framework-aware for Python. A Django/FastAPI/Flask
|
|
41
|
+
// repo may carry a package.json (tailwind/postcss tooling, or a SPA in a
|
|
42
|
+
// sub-directory) that sets `language` to typescript/javascript; the backend
|
|
43
|
+
// is still Python and must be scanned by the Python scanner, never by the
|
|
44
|
+
// Node scanner.
|
|
45
|
+
const PY_FRAMEWORKS = ["django", "fastapi", "flask"];
|
|
46
|
+
const isPythonBackend = stack.language === "python" || PY_FRAMEWORKS.includes(stack.framework);
|
|
47
|
+
|
|
48
|
+
if ((stack.language === "typescript" || stack.language === "javascript") && stack.framework && stack.framework !== "vite" && !isPythonBackend) {
|
|
40
49
|
const r = await scanNodeDomains(stack, ROOT);
|
|
41
50
|
backendDomains.push(...r.backendDomains);
|
|
42
51
|
}
|
|
43
52
|
|
|
44
|
-
if (
|
|
53
|
+
if (isPythonBackend) {
|
|
45
54
|
const r = await scanPythonDomains(stack, ROOT);
|
|
46
55
|
backendDomains.push(...r.backendDomains);
|
|
47
56
|
}
|
|
48
57
|
|
|
49
58
|
// ── Frontend scanner ──
|
|
50
|
-
|
|
59
|
+
// v2.5.0: when the SPA lives in frontend/ (stack.frontendRoot), scan there.
|
|
60
|
+
const FE_ROOT = stack.frontendRoot ? path.join(ROOT, stack.frontendRoot) : ROOT;
|
|
61
|
+
const fe = await scanFrontendDomains(stack, FE_ROOT, { projectRoot: ROOT });
|
|
51
62
|
frontendDomains.push(...fe.frontendDomains);
|
|
52
63
|
|
|
53
64
|
// ── Frontend stats ──
|
|
54
|
-
const frontend = await countFrontendStats(stack,
|
|
65
|
+
const frontend = await countFrontendStats(stack, FE_ROOT);
|
|
55
66
|
|
|
56
67
|
// ── Aggregate ──
|
|
57
68
|
const allDomains = [
|