claudeos-core 2.2.0 → 2.3.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 +1649 -907
- package/CONTRIBUTING.md +92 -92
- package/README.de.md +32 -0
- package/README.es.md +32 -0
- package/README.fr.md +32 -0
- package/README.hi.md +32 -0
- package/README.ja.md +32 -0
- package/README.ko.md +1018 -986
- package/README.md +1020 -987
- package/README.ru.md +32 -0
- package/README.vi.md +1019 -987
- package/README.zh-CN.md +32 -0
- package/bin/cli.js +152 -148
- package/bin/commands/init.js +1673 -1554
- package/bin/commands/lint.js +62 -0
- package/bin/commands/memory.js +438 -438
- package/bin/lib/cli-utils.js +206 -206
- package/claude-md-validator/index.js +184 -0
- package/claude-md-validator/reporter.js +66 -0
- package/claude-md-validator/structural-checks.js +528 -0
- package/content-validator/index.js +666 -441
- package/lib/expected-guides.js +23 -23
- package/lib/expected-outputs.js +90 -90
- package/lib/language-config.js +35 -35
- package/lib/memory-scaffold.js +1058 -1054
- package/lib/plan-parser.js +165 -165
- package/lib/staged-rules.js +118 -118
- package/manifest-generator/index.js +174 -174
- package/package.json +90 -87
- package/pass-json-validator/index.js +337 -337
- package/pass-prompts/templates/common/claude-md-scaffold.md +52 -10
- package/pass-prompts/templates/common/pass3-footer.md +402 -224
- package/pass-prompts/templates/common/pass3b-core-header.md +43 -0
- package/pass-prompts/templates/common/pass4.md +375 -305
- package/pass-prompts/templates/common/staging-override.md +26 -26
- package/pass-prompts/templates/node-vite/pass1.md +117 -117
- package/pass-prompts/templates/node-vite/pass2.md +78 -78
- package/pass-prompts/templates/python-flask/pass1.md +119 -119
- package/pass-prompts/templates/python-flask/pass2.md +85 -85
- package/plan-installer/domain-grouper.js +76 -76
- package/plan-installer/index.js +137 -137
- package/plan-installer/prompt-generator.js +188 -145
- package/plan-installer/scanners/scan-frontend.js +505 -473
- package/plan-installer/scanners/scan-java.js +226 -226
- package/plan-installer/scanners/scan-node.js +57 -57
- package/plan-installer/scanners/scan-python.js +85 -85
- package/plan-installer/stack-detector.js +482 -482
- package/plan-installer/structure-scanner.js +65 -65
- package/sync-checker/index.js +177 -177
|
@@ -1,482 +1,482 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ClaudeOS-Core — Stack Detector
|
|
3
|
-
*
|
|
4
|
-
* Detects project language, framework, build tool, database, ORM, and frontend.
|
|
5
|
-
* Supports: Java, Kotlin, TypeScript/JavaScript, Python
|
|
6
|
-
* Multi-stack aware (backend + frontend simultaneous detection).
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
const path = require("path");
|
|
10
|
-
const { glob } = require("glob");
|
|
11
|
-
const { readFileSafe, readJsonSafe, existsSafe } = require("../lib/safe-fs");
|
|
12
|
-
const { readStackEnvInfo } = require("../lib/env-parser");
|
|
13
|
-
|
|
14
|
-
// ─── Lookup tables ──────────────────────────────────────────────
|
|
15
|
-
|
|
16
|
-
// ORM detection rules: [keyword, ormName] (order = priority, first match wins)
|
|
17
|
-
const GRADLE_ORM_RULES = [
|
|
18
|
-
["mybatis", "mybatis"],
|
|
19
|
-
["jpa", "jpa"], ["hibernate", "jpa"],
|
|
20
|
-
["exposed", "exposed"],
|
|
21
|
-
["jooq", "jooq"],
|
|
22
|
-
["spring-data-jdbc", "spring-data-jdbc"],
|
|
23
|
-
["r2dbc", "r2dbc"],
|
|
24
|
-
];
|
|
25
|
-
|
|
26
|
-
const MAVEN_ORM_RULES = [
|
|
27
|
-
["mybatis", "mybatis"],
|
|
28
|
-
["jpa", "jpa"], ["hibernate", "jpa"],
|
|
29
|
-
["exposed", "exposed"],
|
|
30
|
-
["jooq", "jooq"],
|
|
31
|
-
];
|
|
32
|
-
|
|
33
|
-
const NODE_ORM_RULES = [
|
|
34
|
-
[["@prisma/client", "prisma"], "prisma"],
|
|
35
|
-
[["typeorm"], "typeorm"],
|
|
36
|
-
[["sequelize"], "sequelize"],
|
|
37
|
-
[["drizzle-orm"], "drizzle"],
|
|
38
|
-
[["knex"], "knex"],
|
|
39
|
-
];
|
|
40
|
-
|
|
41
|
-
// DB detection rules: [keyword, dbName]
|
|
42
|
-
const DB_KEYWORD_RULES = [
|
|
43
|
-
["postgresql", "postgresql"], ["postgres", "postgresql"],
|
|
44
|
-
["mysql", "mysql"],
|
|
45
|
-
["oracle", "oracle"],
|
|
46
|
-
["mongodb", "mongodb"],
|
|
47
|
-
["sqlite", "sqlite"],
|
|
48
|
-
];
|
|
49
|
-
|
|
50
|
-
// h2 needs word-boundary check (avoid oauth2, cache2k false positives)
|
|
51
|
-
const H2_REGEX = /\bh2\b/;
|
|
52
|
-
|
|
53
|
-
// ─── Helpers ────────────────────────────────────────────────────
|
|
54
|
-
|
|
55
|
-
function detectFirst(stack, field, content, rules) {
|
|
56
|
-
if (stack[field]) return;
|
|
57
|
-
for (const [keyword, value] of rules) {
|
|
58
|
-
if (content.includes(keyword)) {
|
|
59
|
-
stack[field] = value;
|
|
60
|
-
stack.detected.push(value);
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function detectDb(stack, content, rules) {
|
|
67
|
-
if (stack.database) return;
|
|
68
|
-
for (const [keyword, value] of rules) {
|
|
69
|
-
if (content.includes(keyword)) {
|
|
70
|
-
stack.database = value;
|
|
71
|
-
stack.detected.push(value);
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
// h2 with word boundary
|
|
76
|
-
if (!stack.database && H2_REGEX.test(content)) {
|
|
77
|
-
stack.database = "h2";
|
|
78
|
-
stack.detected.push("h2");
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Detect the project's technology stack.
|
|
85
|
-
* @param {string} ROOT - project root path
|
|
86
|
-
* @returns {Promise<object>} stack info
|
|
87
|
-
*/
|
|
88
|
-
async function detectStack(ROOT) {
|
|
89
|
-
const stack = {
|
|
90
|
-
language: null, languageVersion: null,
|
|
91
|
-
framework: null, frameworkVersion: null,
|
|
92
|
-
buildTool: null, database: null, orm: null,
|
|
93
|
-
frontend: null, frontendVersion: null,
|
|
94
|
-
packageManager: null, monorepo: null, workspaces: null,
|
|
95
|
-
detected: [],
|
|
96
|
-
};
|
|
97
|
-
|
|
98
|
-
// ── Java/Kotlin: Gradle ──
|
|
99
|
-
const gradleFile = existsSafe(path.join(ROOT, "build.gradle.kts"))
|
|
100
|
-
? "build.gradle.kts"
|
|
101
|
-
: existsSafe(path.join(ROOT, "build.gradle")) ? "build.gradle" : null;
|
|
102
|
-
if (gradleFile) {
|
|
103
|
-
const g = readFileSafe(path.join(ROOT, gradleFile));
|
|
104
|
-
if (g) {
|
|
105
|
-
stack.buildTool = "gradle"; stack.detected.push(gradleFile);
|
|
106
|
-
if (g.includes("spring-boot")) { stack.language = "java"; stack.framework = "spring-boot"; stack.detected.push("spring-boot"); }
|
|
107
|
-
const svPatterns = [
|
|
108
|
-
/org\.springframework\.boot.*version\s*['"]([^'"]+)['"]/,
|
|
109
|
-
/id\s*\(\s*["']org\.springframework\.boot["']\s*\)\s*version\s*["']([^"']+)["']/,
|
|
110
|
-
/spring-boot-dependencies:([^'")\s]+)/,
|
|
111
|
-
];
|
|
112
|
-
for (const pattern of svPatterns) {
|
|
113
|
-
const sv = g.match(pattern);
|
|
114
|
-
if (sv) { stack.frameworkVersion = sv[1]; break; }
|
|
115
|
-
}
|
|
116
|
-
const jv = g.match(/sourceCompatibility\s*=\s*['"]?(\d+)['"]?/);
|
|
117
|
-
if (jv) stack.languageVersion = jv[1];
|
|
118
|
-
|
|
119
|
-
detectFirst(stack, "orm", g, GRADLE_ORM_RULES);
|
|
120
|
-
// Exclude "postgres" (substring of postgresql — false positive on r2dbc-postgres) and "sqlite" (rare in Gradle deps)
|
|
121
|
-
// "postgresql" is still matched via DB_KEYWORD_RULES; h2 uses word-boundary check separately
|
|
122
|
-
detectDb(stack, g, DB_KEYWORD_RULES.filter(([kw]) => !["postgres", "sqlite"].includes(kw)));
|
|
123
|
-
|
|
124
|
-
// Kotlin detection: override language if Kotlin plugin found
|
|
125
|
-
if (g.includes("kotlin") || g.includes("org.jetbrains.kotlin")) {
|
|
126
|
-
stack.language = "kotlin"; stack.detected.push("kotlin");
|
|
127
|
-
const kvPatterns = [
|
|
128
|
-
/kotlin\S*\s*version\s*['"]([^'"]+)['"]/,
|
|
129
|
-
/org\.jetbrains\.kotlin\S*\s*version\s*['"]([^'"]+)['"]/,
|
|
130
|
-
/kotlin\("jvm"\)\s*version\s*["']([^"']+)["']/,
|
|
131
|
-
];
|
|
132
|
-
for (const pattern of kvPatterns) {
|
|
133
|
-
const match = g.match(pattern);
|
|
134
|
-
if (match) { stack.languageVersion = match[1]; break; }
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// ── Gradle: version catalogs (libs.versions.toml) ──
|
|
141
|
-
const versionCatalog = path.join(ROOT, "gradle/libs.versions.toml");
|
|
142
|
-
if (existsSafe(versionCatalog)) {
|
|
143
|
-
const vc = readFileSafe(versionCatalog);
|
|
144
|
-
if (vc) {
|
|
145
|
-
stack.detected.push("libs.versions.toml");
|
|
146
|
-
if (!stack.languageVersion) {
|
|
147
|
-
const kvMatch = vc.match(/kotlin\s*=\s*["']([^"']+)["']/);
|
|
148
|
-
if (kvMatch) stack.languageVersion = kvMatch[1];
|
|
149
|
-
}
|
|
150
|
-
if (!stack.frameworkVersion) {
|
|
151
|
-
const sbMatch = vc.match(/spring-boot\s*=\s*["']([^"']+)["']/);
|
|
152
|
-
if (sbMatch) stack.frameworkVersion = sbMatch[1];
|
|
153
|
-
}
|
|
154
|
-
// Version catalog ORM (labels include " (catalog)" suffix)
|
|
155
|
-
if (!stack.orm && vc.includes("exposed")) { stack.orm = "exposed"; stack.detected.push("exposed (catalog)"); }
|
|
156
|
-
else if (!stack.orm && vc.includes("jooq")) { stack.orm = "jooq"; stack.detected.push("jooq (catalog)"); }
|
|
157
|
-
else if (!stack.orm && (vc.includes("jpa") || vc.includes("hibernate"))) { stack.orm = "jpa"; stack.detected.push("jpa (catalog)"); }
|
|
158
|
-
if (!stack.database && vc.includes("postgresql")) { stack.database = "postgresql"; }
|
|
159
|
-
if (!stack.database && vc.includes("mysql")) { stack.database = "mysql"; }
|
|
160
|
-
if (!stack.database && vc.includes("mongodb")) { stack.database = "mongodb"; }
|
|
161
|
-
if (!stack.language && vc.includes("kotlin")) {
|
|
162
|
-
stack.language = "kotlin"; stack.detected.push("kotlin (catalog)");
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// ── Kotlin: multi-module Gradle detection ──
|
|
168
|
-
if (stack.language !== "kotlin" && stack.buildTool === "gradle") {
|
|
169
|
-
const subBuildFiles = await glob("**/build.gradle{,.kts}", { cwd: ROOT, ignore: ["**/node_modules/**", "**/build/**"] });
|
|
170
|
-
for (const sbf of subBuildFiles.slice(0, 5)) {
|
|
171
|
-
const sc = readFileSafe(path.join(ROOT, sbf));
|
|
172
|
-
if (sc && (sc.includes("kotlin") || sc.includes("org.jetbrains.kotlin"))) {
|
|
173
|
-
stack.language = "kotlin"; stack.detected.push("kotlin (submodule)");
|
|
174
|
-
const kv = sc.match(/kotlin\S*\s*version\s*['"]([^'"]+)['"]/);
|
|
175
|
-
if (kv) stack.languageVersion = kv[1];
|
|
176
|
-
break;
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// ── Kotlin: detect CQRS/multi-module from settings.gradle ──
|
|
182
|
-
const settingsFile = existsSafe(path.join(ROOT, "settings.gradle.kts"))
|
|
183
|
-
? "settings.gradle.kts"
|
|
184
|
-
: existsSafe(path.join(ROOT, "settings.gradle")) ? "settings.gradle" : null;
|
|
185
|
-
if (settingsFile && stack.language === "kotlin") {
|
|
186
|
-
const sg = readFileSafe(path.join(ROOT, settingsFile));
|
|
187
|
-
if (sg) {
|
|
188
|
-
const sgClean = sg.split("\n").filter(l => !l.trimStart().startsWith("//")).join("\n");
|
|
189
|
-
const includes = [];
|
|
190
|
-
const includeBlocks = [...sgClean.matchAll(/include\s*\(([^)]*)\)/gs)];
|
|
191
|
-
for (const block of includeBlocks) {
|
|
192
|
-
const quotedValues = [...block[1].matchAll(/["']([^"']+)["']/g)].map(m => m[1]);
|
|
193
|
-
includes.push(...quotedValues);
|
|
194
|
-
}
|
|
195
|
-
if (includes.length === 0) {
|
|
196
|
-
const groovyIncludes = [...sgClean.matchAll(/include\s+(.+)/g)];
|
|
197
|
-
for (const line of groovyIncludes) {
|
|
198
|
-
const quotedValues = [...line[1].matchAll(/['"]([^'"]+)['"]/g)].map(m => m[1]);
|
|
199
|
-
includes.push(...quotedValues);
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
const cleanModules = includes.map(m => m.replace(/^:/, ""));
|
|
203
|
-
if (cleanModules.length > 0) {
|
|
204
|
-
stack.multiModule = true;
|
|
205
|
-
stack.modules = cleanModules;
|
|
206
|
-
stack.detected.push(`multi-module (${cleanModules.length} modules)`);
|
|
207
|
-
const hasCommand = cleanModules.some(m => m.includes("command"));
|
|
208
|
-
const hasQuery = cleanModules.some(m => m.includes("query"));
|
|
209
|
-
const hasBff = cleanModules.some(m => m.includes("bff"));
|
|
210
|
-
if (hasCommand && hasQuery) { stack.architecture = "cqrs"; stack.detected.push("cqrs"); }
|
|
211
|
-
if (hasBff) { stack.detected.push("bff"); }
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// ── Java: Maven ──
|
|
217
|
-
if (existsSafe(path.join(ROOT, "pom.xml"))) {
|
|
218
|
-
const pom = readFileSafe(path.join(ROOT, "pom.xml"));
|
|
219
|
-
if (pom) {
|
|
220
|
-
if (!stack.buildTool) { stack.buildTool = "maven"; stack.language = "java"; stack.detected.push("pom.xml"); }
|
|
221
|
-
const sv = pom.match(/<spring-boot[^>]*version>([^<]+)/);
|
|
222
|
-
if (sv) stack.frameworkVersion = sv[1];
|
|
223
|
-
const jv = pom.match(/<java\.version>(\d+)/);
|
|
224
|
-
if (jv) stack.languageVersion = jv[1];
|
|
225
|
-
if (pom.includes("spring-boot") && !stack.framework) { stack.framework = "spring-boot"; stack.detected.push("spring-boot"); }
|
|
226
|
-
detectFirst(stack, "orm", pom, MAVEN_ORM_RULES);
|
|
227
|
-
// Maven DB: original does not push to detected (unlike Gradle)
|
|
228
|
-
if (!stack.database && pom.includes("postgresql")) stack.database = "postgresql";
|
|
229
|
-
if (!stack.database && pom.includes("mysql")) stack.database = "mysql";
|
|
230
|
-
if (!stack.database && pom.includes("oracle")) stack.database = "oracle";
|
|
231
|
-
if (!stack.database && pom.includes("mongodb")) stack.database = "mongodb";
|
|
232
|
-
if (!stack.database && H2_REGEX.test(pom)) stack.database = "h2";
|
|
233
|
-
if (!stack.database && pom.includes("sqlite")) stack.database = "sqlite";
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
// ── Node.js ──
|
|
238
|
-
if (existsSafe(path.join(ROOT, "package.json"))) {
|
|
239
|
-
const pkg = readJsonSafe(path.join(ROOT, "package.json"));
|
|
240
|
-
if (pkg) {
|
|
241
|
-
stack.detected.push("package.json");
|
|
242
|
-
|
|
243
|
-
// ── Monorepo detection ──
|
|
244
|
-
// Detect monorepo markers: turbo.json, pnpm-workspace.yaml, lerna.json, package.json#workspaces
|
|
245
|
-
if (existsSafe(path.join(ROOT, "turbo.json"))) { stack.monorepo = "turborepo"; stack.detected.push("turbo.json"); }
|
|
246
|
-
else if (existsSafe(path.join(ROOT, "pnpm-workspace.yaml"))) { stack.monorepo = "pnpm-workspace"; stack.detected.push("pnpm-workspace.yaml"); }
|
|
247
|
-
else if (existsSafe(path.join(ROOT, "lerna.json"))) { stack.monorepo = "lerna"; stack.detected.push("lerna.json"); }
|
|
248
|
-
else if (pkg.workspaces) { stack.monorepo = "npm-workspaces"; stack.detected.push("npm-workspaces"); }
|
|
249
|
-
if (stack.monorepo) {
|
|
250
|
-
// Resolve workspace paths from package.json#workspaces or pnpm-workspace.yaml
|
|
251
|
-
let wsPatterns = [];
|
|
252
|
-
if (Array.isArray(pkg.workspaces)) wsPatterns = pkg.workspaces;
|
|
253
|
-
else if (pkg.workspaces && Array.isArray(pkg.workspaces.packages)) wsPatterns = pkg.workspaces.packages;
|
|
254
|
-
if (wsPatterns.length === 0 && existsSafe(path.join(ROOT, "pnpm-workspace.yaml"))) {
|
|
255
|
-
const wy = readFileSafe(path.join(ROOT, "pnpm-workspace.yaml"));
|
|
256
|
-
if (wy) {
|
|
257
|
-
const wm = [...wy.matchAll(/- ['"]?([^'"#\n]+)['"]?/g)].map(m => m[1].trim());
|
|
258
|
-
if (wm.length > 0) wsPatterns = wm;
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
if (wsPatterns.length > 0) stack.workspaces = wsPatterns;
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
// Merge deps from root + sub-package package.json files (monorepo)
|
|
265
|
-
// Sub-packages provide framework/frontend/ORM that root may lack
|
|
266
|
-
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
267
|
-
if (stack.monorepo) {
|
|
268
|
-
const subPkgGlobs = ["{apps,packages}/*/package.json"];
|
|
269
|
-
if (stack.workspaces) {
|
|
270
|
-
for (const ws of stack.workspaces) {
|
|
271
|
-
const wsGlob = /[*?]/.test(ws)
|
|
272
|
-
? ws.replace(/\/?\*?\*?$/, "/*/package.json")
|
|
273
|
-
: `${ws.replace(/\/?$/, "")}/{,*/}package.json`;
|
|
274
|
-
if (!subPkgGlobs.includes(wsGlob)) subPkgGlobs.push(wsGlob);
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
for (const spg of subPkgGlobs) {
|
|
278
|
-
const subPkgs = await glob(spg, { cwd: ROOT, ignore: ["**/node_modules/**"] });
|
|
279
|
-
for (const sp of subPkgs) {
|
|
280
|
-
const sub = readJsonSafe(path.join(ROOT, sp));
|
|
281
|
-
if (sub) Object.assign(deps, sub.dependencies, sub.devDependencies);
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
if (!stack.language) stack.language = deps.typescript ? "typescript" : "javascript";
|
|
287
|
-
if (deps.typescript) { stack.detected.push("typescript"); const tv = deps.typescript.match(/(\d+(?:\.\d+)*)/); if (tv) stack.languageVersion = tv[1]; }
|
|
288
|
-
|
|
289
|
-
// Frontend (Angular checked before React — Angular projects may include react in devDependencies)
|
|
290
|
-
const frontendRules = [
|
|
291
|
-
["next", "nextjs", "next.js"],
|
|
292
|
-
["@angular/core", "angular", "angular"],
|
|
293
|
-
["react", "react", "react"],
|
|
294
|
-
["vue", "vue", "vue"],
|
|
295
|
-
];
|
|
296
|
-
for (const [dep, name, label] of frontendRules) {
|
|
297
|
-
if (deps[dep] && !stack.frontend) {
|
|
298
|
-
stack.frontend = name; stack.detected.push(label);
|
|
299
|
-
stack.frontendVersion = deps[dep].replace(/[^0-9.]/g, "");
|
|
300
|
-
break;
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
// Backend framework (NestJS > Fastify > Express — more specific first)
|
|
305
|
-
const frameworkRules = [
|
|
306
|
-
["@nestjs/core", "nestjs", "nestjs"],
|
|
307
|
-
["fastify", "fastify", "fastify"],
|
|
308
|
-
["express", "express", "express"],
|
|
309
|
-
];
|
|
310
|
-
for (const [dep, name, label] of frameworkRules) {
|
|
311
|
-
if (deps[dep] && !stack.framework) {
|
|
312
|
-
stack.framework = name; stack.detected.push(label);
|
|
313
|
-
if (dep !== "express") stack.frameworkVersion = deps[dep].replace(/[^0-9.]/g, "");
|
|
314
|
-
break;
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
// Vite as framework (only if no backend framework was detected — Vite is a build/dev tool for SPA)
|
|
319
|
-
if (deps.vite && !stack.framework) {
|
|
320
|
-
stack.framework = "vite";
|
|
321
|
-
stack.detected.push("vite");
|
|
322
|
-
stack.frameworkVersion = deps.vite.replace(/[^0-9.]/g, "");
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
// ORM
|
|
326
|
-
for (const [depKeys, ormName] of NODE_ORM_RULES) {
|
|
327
|
-
if (stack.orm) break;
|
|
328
|
-
if (depKeys.some(d => deps[d])) {
|
|
329
|
-
stack.orm = ormName; stack.detected.push(ormName);
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
if (deps.mongoose) { if (!stack.database) stack.database = "mongodb"; if (!stack.orm) stack.orm = "mongoose"; stack.detected.push("mongoose"); }
|
|
333
|
-
|
|
334
|
-
// DB
|
|
335
|
-
const nodeDbRules = [["pg", "postgresql"], ["mysql2", "mysql"], ["mongodb", "mongodb"]];
|
|
336
|
-
for (const [dep, db] of nodeDbRules) {
|
|
337
|
-
if (deps[dep] && !stack.database) { stack.database = db; break; }
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
// Package manager
|
|
341
|
-
stack.packageManager = existsSafe(path.join(ROOT, "pnpm-lock.yaml")) ? "pnpm"
|
|
342
|
-
: existsSafe(path.join(ROOT, "yarn.lock")) ? "yarn" : "npm";
|
|
343
|
-
|
|
344
|
-
if (pkg.engines && pkg.engines.node && !stack.languageVersion) {
|
|
345
|
-
const nv = pkg.engines.node.match(/(\d+(?:\.\d+)*)/);
|
|
346
|
-
if (nv) stack.languageVersion = nv[1];
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
// ── Python ──
|
|
352
|
-
const hasPyproject = existsSafe(path.join(ROOT, "pyproject.toml"));
|
|
353
|
-
const hasRequirements = existsSafe(path.join(ROOT, "requirements.txt"));
|
|
354
|
-
if (hasPyproject || hasRequirements) {
|
|
355
|
-
if (!stack.language) stack.language = "python";
|
|
356
|
-
stack.detected.push("python");
|
|
357
|
-
|
|
358
|
-
const pyFrameworkRules = [["django", "django"], ["fastapi", "fastapi"], ["flask", "flask"]];
|
|
359
|
-
const pyOrmRules = [["sqlalchemy", "sqlalchemy"], ["tortoise", "tortoise-orm"]];
|
|
360
|
-
|
|
361
|
-
if (hasPyproject) {
|
|
362
|
-
const pp = readFileSafe(path.join(ROOT, "pyproject.toml"));
|
|
363
|
-
if (pp) {
|
|
364
|
-
const pv = pp.match(/python\s*=\s*"[><=^~]*(\d+\.\d+)/);
|
|
365
|
-
if (pv && !stack.languageVersion) stack.languageVersion = pv[1];
|
|
366
|
-
for (const [kw, name] of pyFrameworkRules) {
|
|
367
|
-
if (pp.includes(kw) && !stack.framework) { stack.framework = name; stack.detected.push(name); break; }
|
|
368
|
-
}
|
|
369
|
-
for (const [kw, name] of pyOrmRules) {
|
|
370
|
-
if (pp.includes(kw) && !stack.orm) { stack.orm = name; stack.detected.push(name); break; }
|
|
371
|
-
}
|
|
372
|
-
if (pp.includes("poetry")) { stack.packageManager = "poetry"; }
|
|
373
|
-
if (pp.includes("pdm")) { stack.packageManager = "pdm"; }
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
if (hasRequirements) {
|
|
378
|
-
const r = readFileSafe(path.join(ROOT, "requirements.txt"));
|
|
379
|
-
if (r) {
|
|
380
|
-
for (const [kw, name] of pyFrameworkRules) {
|
|
381
|
-
if (r.includes(kw) && !stack.framework) { stack.framework = name; stack.detected.push(name); break; }
|
|
382
|
-
}
|
|
383
|
-
for (const [kw, name] of pyOrmRules) {
|
|
384
|
-
if (r.includes(kw) && !stack.orm) { stack.orm = name; break; }
|
|
385
|
-
}
|
|
386
|
-
if (r.includes("psycopg") && !stack.database) stack.database = "postgresql";
|
|
387
|
-
if (r.includes("mysqlclient") && !stack.database) stack.database = "mysql";
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
if (!stack.packageManager) {
|
|
392
|
-
stack.packageManager = existsSafe(path.join(ROOT, "Pipfile")) ? "pipenv"
|
|
393
|
-
: existsSafe(path.join(ROOT, "poetry.lock")) ? "poetry" : "pip";
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
// ── DB from config files ──
|
|
398
|
-
const ymls = await glob("**/application*.yml", { cwd: ROOT, absolute: true, ignore: ["**/node_modules/**", "**/build/**", "**/target/**", "**/.gradle/**"] });
|
|
399
|
-
for (const y of ymls) {
|
|
400
|
-
const c = readFileSafe(y);
|
|
401
|
-
if (!c) continue;
|
|
402
|
-
if (!stack.database && c.includes("postgresql")) stack.database = "postgresql";
|
|
403
|
-
if (!stack.database && c.includes("mysql")) stack.database = "mysql";
|
|
404
|
-
if (!stack.database && c.includes("oracle")) stack.database = "oracle";
|
|
405
|
-
if (!stack.database && c.includes("mongodb")) stack.database = "mongodb";
|
|
406
|
-
if (!stack.database && H2_REGEX.test(c)) stack.database = "h2";
|
|
407
|
-
if (!stack.database && c.includes("sqlite")) stack.database = "sqlite";
|
|
408
|
-
if (!stack.port) {
|
|
409
|
-
const pm = c.match(/server:\s*\n\s*port:\s*(\d+)/) || c.match(/server\.port\s*[=:]\s*(\d+)/);
|
|
410
|
-
if (pm) stack.port = parseInt(pm[1]);
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
// .env
|
|
415
|
-
for (const ef of [".env", ".env.local", ".env.development"]) {
|
|
416
|
-
const ep = path.join(ROOT, ef);
|
|
417
|
-
if (existsSafe(ep)) {
|
|
418
|
-
const ec = readFileSafe(ep);
|
|
419
|
-
if (ec && ec.includes("DATABASE_URL")) {
|
|
420
|
-
// .env: original checks postgres (not postgresql), no oracle/h2
|
|
421
|
-
if (ec.includes("postgres") && !stack.database) stack.database = "postgresql";
|
|
422
|
-
if (ec.includes("mysql") && !stack.database) stack.database = "mysql";
|
|
423
|
-
if (ec.includes("mongodb") && !stack.database) stack.database = "mongodb";
|
|
424
|
-
if (ec.includes("sqlite") && !stack.database) stack.database = "sqlite";
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
// Prisma schema
|
|
430
|
-
const prismaSchema = path.join(ROOT, "prisma/schema.prisma");
|
|
431
|
-
if (existsSafe(prismaSchema)) {
|
|
432
|
-
const ps = readFileSafe(prismaSchema);
|
|
433
|
-
if (ps) {
|
|
434
|
-
const prov = ps.match(/provider\s*=\s*"(\w+)"/);
|
|
435
|
-
if (prov && !stack.database) {
|
|
436
|
-
const db = { postgresql: "postgresql", mysql: "mysql", sqlite: "sqlite", mongodb: "mongodb" };
|
|
437
|
-
if (db[prov[1]]) stack.database = db[prov[1]];
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
// ── Config file fallback (monorepo) ──
|
|
443
|
-
// [configFiles, frontendName, frameworkName (optional), label]
|
|
444
|
-
const frontendFallbacks = [
|
|
445
|
-
[["next.config.js", "next.config.mjs", "next.config.ts"], "nextjs", null, "next.config (fallback)"],
|
|
446
|
-
[["vite.config.ts", "vite.config.js"], "react", "vite", "vite.config (fallback)"],
|
|
447
|
-
[["nuxt.config.ts", "nuxt.config.js"], "vue", null, "nuxt.config (fallback)"],
|
|
448
|
-
[["angular.json", ".angular.json"], "angular", null, "angular.json (fallback)"],
|
|
449
|
-
];
|
|
450
|
-
if (!stack.frontend) {
|
|
451
|
-
for (const [files, frontendName, frameworkName, label] of frontendFallbacks) {
|
|
452
|
-
if (files.some(f => existsSafe(path.join(ROOT, f)))) {
|
|
453
|
-
stack.frontend = frontendName; stack.detected.push(label);
|
|
454
|
-
if (!stack.language) stack.language = "typescript";
|
|
455
|
-
if (frameworkName && !stack.framework) {
|
|
456
|
-
stack.framework = frameworkName;
|
|
457
|
-
stack.detected.push(frameworkName + " (fallback)");
|
|
458
|
-
}
|
|
459
|
-
break;
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
// ── .env-derived factual config ──
|
|
465
|
-
// Read .env.example (preferred) or .env to capture ports/hosts/API targets
|
|
466
|
-
// the project actually declares. This overrides framework-default guesses
|
|
467
|
-
// in downstream code (plan-installer/index.js defaultPort) and exposes the
|
|
468
|
-
// full variable map to Pass 3 prompts via project-analysis.json.
|
|
469
|
-
const envInfo = readStackEnvInfo(ROOT);
|
|
470
|
-
if (envInfo) {
|
|
471
|
-
stack.envInfo = envInfo;
|
|
472
|
-
// Promote .env-declared port to stack.port if no earlier detection won
|
|
473
|
-
// (e.g., Spring application.yml parsing at line 407).
|
|
474
|
-
if (!stack.port && envInfo.port) {
|
|
475
|
-
stack.port = envInfo.port;
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
return stack;
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
module.exports = { detectStack };
|
|
1
|
+
/**
|
|
2
|
+
* ClaudeOS-Core — Stack Detector
|
|
3
|
+
*
|
|
4
|
+
* Detects project language, framework, build tool, database, ORM, and frontend.
|
|
5
|
+
* Supports: Java, Kotlin, TypeScript/JavaScript, Python
|
|
6
|
+
* Multi-stack aware (backend + frontend simultaneous detection).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const path = require("path");
|
|
10
|
+
const { glob } = require("glob");
|
|
11
|
+
const { readFileSafe, readJsonSafe, existsSafe } = require("../lib/safe-fs");
|
|
12
|
+
const { readStackEnvInfo } = require("../lib/env-parser");
|
|
13
|
+
|
|
14
|
+
// ─── Lookup tables ──────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
// ORM detection rules: [keyword, ormName] (order = priority, first match wins)
|
|
17
|
+
const GRADLE_ORM_RULES = [
|
|
18
|
+
["mybatis", "mybatis"],
|
|
19
|
+
["jpa", "jpa"], ["hibernate", "jpa"],
|
|
20
|
+
["exposed", "exposed"],
|
|
21
|
+
["jooq", "jooq"],
|
|
22
|
+
["spring-data-jdbc", "spring-data-jdbc"],
|
|
23
|
+
["r2dbc", "r2dbc"],
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
const MAVEN_ORM_RULES = [
|
|
27
|
+
["mybatis", "mybatis"],
|
|
28
|
+
["jpa", "jpa"], ["hibernate", "jpa"],
|
|
29
|
+
["exposed", "exposed"],
|
|
30
|
+
["jooq", "jooq"],
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
const NODE_ORM_RULES = [
|
|
34
|
+
[["@prisma/client", "prisma"], "prisma"],
|
|
35
|
+
[["typeorm"], "typeorm"],
|
|
36
|
+
[["sequelize"], "sequelize"],
|
|
37
|
+
[["drizzle-orm"], "drizzle"],
|
|
38
|
+
[["knex"], "knex"],
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
// DB detection rules: [keyword, dbName]
|
|
42
|
+
const DB_KEYWORD_RULES = [
|
|
43
|
+
["postgresql", "postgresql"], ["postgres", "postgresql"],
|
|
44
|
+
["mysql", "mysql"],
|
|
45
|
+
["oracle", "oracle"],
|
|
46
|
+
["mongodb", "mongodb"],
|
|
47
|
+
["sqlite", "sqlite"],
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
// h2 needs word-boundary check (avoid oauth2, cache2k false positives)
|
|
51
|
+
const H2_REGEX = /\bh2\b/;
|
|
52
|
+
|
|
53
|
+
// ─── Helpers ────────────────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
function detectFirst(stack, field, content, rules) {
|
|
56
|
+
if (stack[field]) return;
|
|
57
|
+
for (const [keyword, value] of rules) {
|
|
58
|
+
if (content.includes(keyword)) {
|
|
59
|
+
stack[field] = value;
|
|
60
|
+
stack.detected.push(value);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function detectDb(stack, content, rules) {
|
|
67
|
+
if (stack.database) return;
|
|
68
|
+
for (const [keyword, value] of rules) {
|
|
69
|
+
if (content.includes(keyword)) {
|
|
70
|
+
stack.database = value;
|
|
71
|
+
stack.detected.push(value);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// h2 with word boundary
|
|
76
|
+
if (!stack.database && H2_REGEX.test(content)) {
|
|
77
|
+
stack.database = "h2";
|
|
78
|
+
stack.detected.push("h2");
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Detect the project's technology stack.
|
|
85
|
+
* @param {string} ROOT - project root path
|
|
86
|
+
* @returns {Promise<object>} stack info
|
|
87
|
+
*/
|
|
88
|
+
async function detectStack(ROOT) {
|
|
89
|
+
const stack = {
|
|
90
|
+
language: null, languageVersion: null,
|
|
91
|
+
framework: null, frameworkVersion: null,
|
|
92
|
+
buildTool: null, database: null, orm: null,
|
|
93
|
+
frontend: null, frontendVersion: null,
|
|
94
|
+
packageManager: null, monorepo: null, workspaces: null,
|
|
95
|
+
detected: [],
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
// ── Java/Kotlin: Gradle ──
|
|
99
|
+
const gradleFile = existsSafe(path.join(ROOT, "build.gradle.kts"))
|
|
100
|
+
? "build.gradle.kts"
|
|
101
|
+
: existsSafe(path.join(ROOT, "build.gradle")) ? "build.gradle" : null;
|
|
102
|
+
if (gradleFile) {
|
|
103
|
+
const g = readFileSafe(path.join(ROOT, gradleFile));
|
|
104
|
+
if (g) {
|
|
105
|
+
stack.buildTool = "gradle"; stack.detected.push(gradleFile);
|
|
106
|
+
if (g.includes("spring-boot")) { stack.language = "java"; stack.framework = "spring-boot"; stack.detected.push("spring-boot"); }
|
|
107
|
+
const svPatterns = [
|
|
108
|
+
/org\.springframework\.boot.*version\s*['"]([^'"]+)['"]/,
|
|
109
|
+
/id\s*\(\s*["']org\.springframework\.boot["']\s*\)\s*version\s*["']([^"']+)["']/,
|
|
110
|
+
/spring-boot-dependencies:([^'")\s]+)/,
|
|
111
|
+
];
|
|
112
|
+
for (const pattern of svPatterns) {
|
|
113
|
+
const sv = g.match(pattern);
|
|
114
|
+
if (sv) { stack.frameworkVersion = sv[1]; break; }
|
|
115
|
+
}
|
|
116
|
+
const jv = g.match(/sourceCompatibility\s*=\s*['"]?(\d+)['"]?/);
|
|
117
|
+
if (jv) stack.languageVersion = jv[1];
|
|
118
|
+
|
|
119
|
+
detectFirst(stack, "orm", g, GRADLE_ORM_RULES);
|
|
120
|
+
// Exclude "postgres" (substring of postgresql — false positive on r2dbc-postgres) and "sqlite" (rare in Gradle deps)
|
|
121
|
+
// "postgresql" is still matched via DB_KEYWORD_RULES; h2 uses word-boundary check separately
|
|
122
|
+
detectDb(stack, g, DB_KEYWORD_RULES.filter(([kw]) => !["postgres", "sqlite"].includes(kw)));
|
|
123
|
+
|
|
124
|
+
// Kotlin detection: override language if Kotlin plugin found
|
|
125
|
+
if (g.includes("kotlin") || g.includes("org.jetbrains.kotlin")) {
|
|
126
|
+
stack.language = "kotlin"; stack.detected.push("kotlin");
|
|
127
|
+
const kvPatterns = [
|
|
128
|
+
/kotlin\S*\s*version\s*['"]([^'"]+)['"]/,
|
|
129
|
+
/org\.jetbrains\.kotlin\S*\s*version\s*['"]([^'"]+)['"]/,
|
|
130
|
+
/kotlin\("jvm"\)\s*version\s*["']([^"']+)["']/,
|
|
131
|
+
];
|
|
132
|
+
for (const pattern of kvPatterns) {
|
|
133
|
+
const match = g.match(pattern);
|
|
134
|
+
if (match) { stack.languageVersion = match[1]; break; }
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ── Gradle: version catalogs (libs.versions.toml) ──
|
|
141
|
+
const versionCatalog = path.join(ROOT, "gradle/libs.versions.toml");
|
|
142
|
+
if (existsSafe(versionCatalog)) {
|
|
143
|
+
const vc = readFileSafe(versionCatalog);
|
|
144
|
+
if (vc) {
|
|
145
|
+
stack.detected.push("libs.versions.toml");
|
|
146
|
+
if (!stack.languageVersion) {
|
|
147
|
+
const kvMatch = vc.match(/kotlin\s*=\s*["']([^"']+)["']/);
|
|
148
|
+
if (kvMatch) stack.languageVersion = kvMatch[1];
|
|
149
|
+
}
|
|
150
|
+
if (!stack.frameworkVersion) {
|
|
151
|
+
const sbMatch = vc.match(/spring-boot\s*=\s*["']([^"']+)["']/);
|
|
152
|
+
if (sbMatch) stack.frameworkVersion = sbMatch[1];
|
|
153
|
+
}
|
|
154
|
+
// Version catalog ORM (labels include " (catalog)" suffix)
|
|
155
|
+
if (!stack.orm && vc.includes("exposed")) { stack.orm = "exposed"; stack.detected.push("exposed (catalog)"); }
|
|
156
|
+
else if (!stack.orm && vc.includes("jooq")) { stack.orm = "jooq"; stack.detected.push("jooq (catalog)"); }
|
|
157
|
+
else if (!stack.orm && (vc.includes("jpa") || vc.includes("hibernate"))) { stack.orm = "jpa"; stack.detected.push("jpa (catalog)"); }
|
|
158
|
+
if (!stack.database && vc.includes("postgresql")) { stack.database = "postgresql"; }
|
|
159
|
+
if (!stack.database && vc.includes("mysql")) { stack.database = "mysql"; }
|
|
160
|
+
if (!stack.database && vc.includes("mongodb")) { stack.database = "mongodb"; }
|
|
161
|
+
if (!stack.language && vc.includes("kotlin")) {
|
|
162
|
+
stack.language = "kotlin"; stack.detected.push("kotlin (catalog)");
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ── Kotlin: multi-module Gradle detection ──
|
|
168
|
+
if (stack.language !== "kotlin" && stack.buildTool === "gradle") {
|
|
169
|
+
const subBuildFiles = await glob("**/build.gradle{,.kts}", { cwd: ROOT, ignore: ["**/node_modules/**", "**/build/**"] });
|
|
170
|
+
for (const sbf of subBuildFiles.slice(0, 5)) {
|
|
171
|
+
const sc = readFileSafe(path.join(ROOT, sbf));
|
|
172
|
+
if (sc && (sc.includes("kotlin") || sc.includes("org.jetbrains.kotlin"))) {
|
|
173
|
+
stack.language = "kotlin"; stack.detected.push("kotlin (submodule)");
|
|
174
|
+
const kv = sc.match(/kotlin\S*\s*version\s*['"]([^'"]+)['"]/);
|
|
175
|
+
if (kv) stack.languageVersion = kv[1];
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ── Kotlin: detect CQRS/multi-module from settings.gradle ──
|
|
182
|
+
const settingsFile = existsSafe(path.join(ROOT, "settings.gradle.kts"))
|
|
183
|
+
? "settings.gradle.kts"
|
|
184
|
+
: existsSafe(path.join(ROOT, "settings.gradle")) ? "settings.gradle" : null;
|
|
185
|
+
if (settingsFile && stack.language === "kotlin") {
|
|
186
|
+
const sg = readFileSafe(path.join(ROOT, settingsFile));
|
|
187
|
+
if (sg) {
|
|
188
|
+
const sgClean = sg.split("\n").filter(l => !l.trimStart().startsWith("//")).join("\n");
|
|
189
|
+
const includes = [];
|
|
190
|
+
const includeBlocks = [...sgClean.matchAll(/include\s*\(([^)]*)\)/gs)];
|
|
191
|
+
for (const block of includeBlocks) {
|
|
192
|
+
const quotedValues = [...block[1].matchAll(/["']([^"']+)["']/g)].map(m => m[1]);
|
|
193
|
+
includes.push(...quotedValues);
|
|
194
|
+
}
|
|
195
|
+
if (includes.length === 0) {
|
|
196
|
+
const groovyIncludes = [...sgClean.matchAll(/include\s+(.+)/g)];
|
|
197
|
+
for (const line of groovyIncludes) {
|
|
198
|
+
const quotedValues = [...line[1].matchAll(/['"]([^'"]+)['"]/g)].map(m => m[1]);
|
|
199
|
+
includes.push(...quotedValues);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
const cleanModules = includes.map(m => m.replace(/^:/, ""));
|
|
203
|
+
if (cleanModules.length > 0) {
|
|
204
|
+
stack.multiModule = true;
|
|
205
|
+
stack.modules = cleanModules;
|
|
206
|
+
stack.detected.push(`multi-module (${cleanModules.length} modules)`);
|
|
207
|
+
const hasCommand = cleanModules.some(m => m.includes("command"));
|
|
208
|
+
const hasQuery = cleanModules.some(m => m.includes("query"));
|
|
209
|
+
const hasBff = cleanModules.some(m => m.includes("bff"));
|
|
210
|
+
if (hasCommand && hasQuery) { stack.architecture = "cqrs"; stack.detected.push("cqrs"); }
|
|
211
|
+
if (hasBff) { stack.detected.push("bff"); }
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// ── Java: Maven ──
|
|
217
|
+
if (existsSafe(path.join(ROOT, "pom.xml"))) {
|
|
218
|
+
const pom = readFileSafe(path.join(ROOT, "pom.xml"));
|
|
219
|
+
if (pom) {
|
|
220
|
+
if (!stack.buildTool) { stack.buildTool = "maven"; stack.language = "java"; stack.detected.push("pom.xml"); }
|
|
221
|
+
const sv = pom.match(/<spring-boot[^>]*version>([^<]+)/);
|
|
222
|
+
if (sv) stack.frameworkVersion = sv[1];
|
|
223
|
+
const jv = pom.match(/<java\.version>(\d+)/);
|
|
224
|
+
if (jv) stack.languageVersion = jv[1];
|
|
225
|
+
if (pom.includes("spring-boot") && !stack.framework) { stack.framework = "spring-boot"; stack.detected.push("spring-boot"); }
|
|
226
|
+
detectFirst(stack, "orm", pom, MAVEN_ORM_RULES);
|
|
227
|
+
// Maven DB: original does not push to detected (unlike Gradle)
|
|
228
|
+
if (!stack.database && pom.includes("postgresql")) stack.database = "postgresql";
|
|
229
|
+
if (!stack.database && pom.includes("mysql")) stack.database = "mysql";
|
|
230
|
+
if (!stack.database && pom.includes("oracle")) stack.database = "oracle";
|
|
231
|
+
if (!stack.database && pom.includes("mongodb")) stack.database = "mongodb";
|
|
232
|
+
if (!stack.database && H2_REGEX.test(pom)) stack.database = "h2";
|
|
233
|
+
if (!stack.database && pom.includes("sqlite")) stack.database = "sqlite";
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// ── Node.js ──
|
|
238
|
+
if (existsSafe(path.join(ROOT, "package.json"))) {
|
|
239
|
+
const pkg = readJsonSafe(path.join(ROOT, "package.json"));
|
|
240
|
+
if (pkg) {
|
|
241
|
+
stack.detected.push("package.json");
|
|
242
|
+
|
|
243
|
+
// ── Monorepo detection ──
|
|
244
|
+
// Detect monorepo markers: turbo.json, pnpm-workspace.yaml, lerna.json, package.json#workspaces
|
|
245
|
+
if (existsSafe(path.join(ROOT, "turbo.json"))) { stack.monorepo = "turborepo"; stack.detected.push("turbo.json"); }
|
|
246
|
+
else if (existsSafe(path.join(ROOT, "pnpm-workspace.yaml"))) { stack.monorepo = "pnpm-workspace"; stack.detected.push("pnpm-workspace.yaml"); }
|
|
247
|
+
else if (existsSafe(path.join(ROOT, "lerna.json"))) { stack.monorepo = "lerna"; stack.detected.push("lerna.json"); }
|
|
248
|
+
else if (pkg.workspaces) { stack.monorepo = "npm-workspaces"; stack.detected.push("npm-workspaces"); }
|
|
249
|
+
if (stack.monorepo) {
|
|
250
|
+
// Resolve workspace paths from package.json#workspaces or pnpm-workspace.yaml
|
|
251
|
+
let wsPatterns = [];
|
|
252
|
+
if (Array.isArray(pkg.workspaces)) wsPatterns = pkg.workspaces;
|
|
253
|
+
else if (pkg.workspaces && Array.isArray(pkg.workspaces.packages)) wsPatterns = pkg.workspaces.packages;
|
|
254
|
+
if (wsPatterns.length === 0 && existsSafe(path.join(ROOT, "pnpm-workspace.yaml"))) {
|
|
255
|
+
const wy = readFileSafe(path.join(ROOT, "pnpm-workspace.yaml"));
|
|
256
|
+
if (wy) {
|
|
257
|
+
const wm = [...wy.matchAll(/- ['"]?([^'"#\n]+)['"]?/g)].map(m => m[1].trim());
|
|
258
|
+
if (wm.length > 0) wsPatterns = wm;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
if (wsPatterns.length > 0) stack.workspaces = wsPatterns;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Merge deps from root + sub-package package.json files (monorepo)
|
|
265
|
+
// Sub-packages provide framework/frontend/ORM that root may lack
|
|
266
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
267
|
+
if (stack.monorepo) {
|
|
268
|
+
const subPkgGlobs = ["{apps,packages}/*/package.json"];
|
|
269
|
+
if (stack.workspaces) {
|
|
270
|
+
for (const ws of stack.workspaces) {
|
|
271
|
+
const wsGlob = /[*?]/.test(ws)
|
|
272
|
+
? ws.replace(/\/?\*?\*?$/, "/*/package.json")
|
|
273
|
+
: `${ws.replace(/\/?$/, "")}/{,*/}package.json`;
|
|
274
|
+
if (!subPkgGlobs.includes(wsGlob)) subPkgGlobs.push(wsGlob);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
for (const spg of subPkgGlobs) {
|
|
278
|
+
const subPkgs = await glob(spg, { cwd: ROOT, ignore: ["**/node_modules/**"] });
|
|
279
|
+
for (const sp of subPkgs) {
|
|
280
|
+
const sub = readJsonSafe(path.join(ROOT, sp));
|
|
281
|
+
if (sub) Object.assign(deps, sub.dependencies, sub.devDependencies);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (!stack.language) stack.language = deps.typescript ? "typescript" : "javascript";
|
|
287
|
+
if (deps.typescript) { stack.detected.push("typescript"); const tv = deps.typescript.match(/(\d+(?:\.\d+)*)/); if (tv) stack.languageVersion = tv[1]; }
|
|
288
|
+
|
|
289
|
+
// Frontend (Angular checked before React — Angular projects may include react in devDependencies)
|
|
290
|
+
const frontendRules = [
|
|
291
|
+
["next", "nextjs", "next.js"],
|
|
292
|
+
["@angular/core", "angular", "angular"],
|
|
293
|
+
["react", "react", "react"],
|
|
294
|
+
["vue", "vue", "vue"],
|
|
295
|
+
];
|
|
296
|
+
for (const [dep, name, label] of frontendRules) {
|
|
297
|
+
if (deps[dep] && !stack.frontend) {
|
|
298
|
+
stack.frontend = name; stack.detected.push(label);
|
|
299
|
+
stack.frontendVersion = deps[dep].replace(/[^0-9.]/g, "");
|
|
300
|
+
break;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Backend framework (NestJS > Fastify > Express — more specific first)
|
|
305
|
+
const frameworkRules = [
|
|
306
|
+
["@nestjs/core", "nestjs", "nestjs"],
|
|
307
|
+
["fastify", "fastify", "fastify"],
|
|
308
|
+
["express", "express", "express"],
|
|
309
|
+
];
|
|
310
|
+
for (const [dep, name, label] of frameworkRules) {
|
|
311
|
+
if (deps[dep] && !stack.framework) {
|
|
312
|
+
stack.framework = name; stack.detected.push(label);
|
|
313
|
+
if (dep !== "express") stack.frameworkVersion = deps[dep].replace(/[^0-9.]/g, "");
|
|
314
|
+
break;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Vite as framework (only if no backend framework was detected — Vite is a build/dev tool for SPA)
|
|
319
|
+
if (deps.vite && !stack.framework) {
|
|
320
|
+
stack.framework = "vite";
|
|
321
|
+
stack.detected.push("vite");
|
|
322
|
+
stack.frameworkVersion = deps.vite.replace(/[^0-9.]/g, "");
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// ORM
|
|
326
|
+
for (const [depKeys, ormName] of NODE_ORM_RULES) {
|
|
327
|
+
if (stack.orm) break;
|
|
328
|
+
if (depKeys.some(d => deps[d])) {
|
|
329
|
+
stack.orm = ormName; stack.detected.push(ormName);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
if (deps.mongoose) { if (!stack.database) stack.database = "mongodb"; if (!stack.orm) stack.orm = "mongoose"; stack.detected.push("mongoose"); }
|
|
333
|
+
|
|
334
|
+
// DB
|
|
335
|
+
const nodeDbRules = [["pg", "postgresql"], ["mysql2", "mysql"], ["mongodb", "mongodb"]];
|
|
336
|
+
for (const [dep, db] of nodeDbRules) {
|
|
337
|
+
if (deps[dep] && !stack.database) { stack.database = db; break; }
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Package manager
|
|
341
|
+
stack.packageManager = existsSafe(path.join(ROOT, "pnpm-lock.yaml")) ? "pnpm"
|
|
342
|
+
: existsSafe(path.join(ROOT, "yarn.lock")) ? "yarn" : "npm";
|
|
343
|
+
|
|
344
|
+
if (pkg.engines && pkg.engines.node && !stack.languageVersion) {
|
|
345
|
+
const nv = pkg.engines.node.match(/(\d+(?:\.\d+)*)/);
|
|
346
|
+
if (nv) stack.languageVersion = nv[1];
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// ── Python ──
|
|
352
|
+
const hasPyproject = existsSafe(path.join(ROOT, "pyproject.toml"));
|
|
353
|
+
const hasRequirements = existsSafe(path.join(ROOT, "requirements.txt"));
|
|
354
|
+
if (hasPyproject || hasRequirements) {
|
|
355
|
+
if (!stack.language) stack.language = "python";
|
|
356
|
+
stack.detected.push("python");
|
|
357
|
+
|
|
358
|
+
const pyFrameworkRules = [["django", "django"], ["fastapi", "fastapi"], ["flask", "flask"]];
|
|
359
|
+
const pyOrmRules = [["sqlalchemy", "sqlalchemy"], ["tortoise", "tortoise-orm"]];
|
|
360
|
+
|
|
361
|
+
if (hasPyproject) {
|
|
362
|
+
const pp = readFileSafe(path.join(ROOT, "pyproject.toml"));
|
|
363
|
+
if (pp) {
|
|
364
|
+
const pv = pp.match(/python\s*=\s*"[><=^~]*(\d+\.\d+)/);
|
|
365
|
+
if (pv && !stack.languageVersion) stack.languageVersion = pv[1];
|
|
366
|
+
for (const [kw, name] of pyFrameworkRules) {
|
|
367
|
+
if (pp.includes(kw) && !stack.framework) { stack.framework = name; stack.detected.push(name); break; }
|
|
368
|
+
}
|
|
369
|
+
for (const [kw, name] of pyOrmRules) {
|
|
370
|
+
if (pp.includes(kw) && !stack.orm) { stack.orm = name; stack.detected.push(name); break; }
|
|
371
|
+
}
|
|
372
|
+
if (pp.includes("poetry")) { stack.packageManager = "poetry"; }
|
|
373
|
+
if (pp.includes("pdm")) { stack.packageManager = "pdm"; }
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (hasRequirements) {
|
|
378
|
+
const r = readFileSafe(path.join(ROOT, "requirements.txt"));
|
|
379
|
+
if (r) {
|
|
380
|
+
for (const [kw, name] of pyFrameworkRules) {
|
|
381
|
+
if (r.includes(kw) && !stack.framework) { stack.framework = name; stack.detected.push(name); break; }
|
|
382
|
+
}
|
|
383
|
+
for (const [kw, name] of pyOrmRules) {
|
|
384
|
+
if (r.includes(kw) && !stack.orm) { stack.orm = name; break; }
|
|
385
|
+
}
|
|
386
|
+
if (r.includes("psycopg") && !stack.database) stack.database = "postgresql";
|
|
387
|
+
if (r.includes("mysqlclient") && !stack.database) stack.database = "mysql";
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
if (!stack.packageManager) {
|
|
392
|
+
stack.packageManager = existsSafe(path.join(ROOT, "Pipfile")) ? "pipenv"
|
|
393
|
+
: existsSafe(path.join(ROOT, "poetry.lock")) ? "poetry" : "pip";
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// ── DB from config files ──
|
|
398
|
+
const ymls = await glob("**/application*.yml", { cwd: ROOT, absolute: true, ignore: ["**/node_modules/**", "**/build/**", "**/target/**", "**/.gradle/**"] });
|
|
399
|
+
for (const y of ymls) {
|
|
400
|
+
const c = readFileSafe(y);
|
|
401
|
+
if (!c) continue;
|
|
402
|
+
if (!stack.database && c.includes("postgresql")) stack.database = "postgresql";
|
|
403
|
+
if (!stack.database && c.includes("mysql")) stack.database = "mysql";
|
|
404
|
+
if (!stack.database && c.includes("oracle")) stack.database = "oracle";
|
|
405
|
+
if (!stack.database && c.includes("mongodb")) stack.database = "mongodb";
|
|
406
|
+
if (!stack.database && H2_REGEX.test(c)) stack.database = "h2";
|
|
407
|
+
if (!stack.database && c.includes("sqlite")) stack.database = "sqlite";
|
|
408
|
+
if (!stack.port) {
|
|
409
|
+
const pm = c.match(/server:\s*\n\s*port:\s*(\d+)/) || c.match(/server\.port\s*[=:]\s*(\d+)/);
|
|
410
|
+
if (pm) stack.port = parseInt(pm[1]);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// .env
|
|
415
|
+
for (const ef of [".env", ".env.local", ".env.development"]) {
|
|
416
|
+
const ep = path.join(ROOT, ef);
|
|
417
|
+
if (existsSafe(ep)) {
|
|
418
|
+
const ec = readFileSafe(ep);
|
|
419
|
+
if (ec && ec.includes("DATABASE_URL")) {
|
|
420
|
+
// .env: original checks postgres (not postgresql), no oracle/h2
|
|
421
|
+
if (ec.includes("postgres") && !stack.database) stack.database = "postgresql";
|
|
422
|
+
if (ec.includes("mysql") && !stack.database) stack.database = "mysql";
|
|
423
|
+
if (ec.includes("mongodb") && !stack.database) stack.database = "mongodb";
|
|
424
|
+
if (ec.includes("sqlite") && !stack.database) stack.database = "sqlite";
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// Prisma schema
|
|
430
|
+
const prismaSchema = path.join(ROOT, "prisma/schema.prisma");
|
|
431
|
+
if (existsSafe(prismaSchema)) {
|
|
432
|
+
const ps = readFileSafe(prismaSchema);
|
|
433
|
+
if (ps) {
|
|
434
|
+
const prov = ps.match(/provider\s*=\s*"(\w+)"/);
|
|
435
|
+
if (prov && !stack.database) {
|
|
436
|
+
const db = { postgresql: "postgresql", mysql: "mysql", sqlite: "sqlite", mongodb: "mongodb" };
|
|
437
|
+
if (db[prov[1]]) stack.database = db[prov[1]];
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// ── Config file fallback (monorepo) ──
|
|
443
|
+
// [configFiles, frontendName, frameworkName (optional), label]
|
|
444
|
+
const frontendFallbacks = [
|
|
445
|
+
[["next.config.js", "next.config.mjs", "next.config.ts"], "nextjs", null, "next.config (fallback)"],
|
|
446
|
+
[["vite.config.ts", "vite.config.js"], "react", "vite", "vite.config (fallback)"],
|
|
447
|
+
[["nuxt.config.ts", "nuxt.config.js"], "vue", null, "nuxt.config (fallback)"],
|
|
448
|
+
[["angular.json", ".angular.json"], "angular", null, "angular.json (fallback)"],
|
|
449
|
+
];
|
|
450
|
+
if (!stack.frontend) {
|
|
451
|
+
for (const [files, frontendName, frameworkName, label] of frontendFallbacks) {
|
|
452
|
+
if (files.some(f => existsSafe(path.join(ROOT, f)))) {
|
|
453
|
+
stack.frontend = frontendName; stack.detected.push(label);
|
|
454
|
+
if (!stack.language) stack.language = "typescript";
|
|
455
|
+
if (frameworkName && !stack.framework) {
|
|
456
|
+
stack.framework = frameworkName;
|
|
457
|
+
stack.detected.push(frameworkName + " (fallback)");
|
|
458
|
+
}
|
|
459
|
+
break;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// ── .env-derived factual config ──
|
|
465
|
+
// Read .env.example (preferred) or .env to capture ports/hosts/API targets
|
|
466
|
+
// the project actually declares. This overrides framework-default guesses
|
|
467
|
+
// in downstream code (plan-installer/index.js defaultPort) and exposes the
|
|
468
|
+
// full variable map to Pass 3 prompts via project-analysis.json.
|
|
469
|
+
const envInfo = readStackEnvInfo(ROOT);
|
|
470
|
+
if (envInfo) {
|
|
471
|
+
stack.envInfo = envInfo;
|
|
472
|
+
// Promote .env-declared port to stack.port if no earlier detection won
|
|
473
|
+
// (e.g., Spring application.yml parsing at line 407).
|
|
474
|
+
if (!stack.port && envInfo.port) {
|
|
475
|
+
stack.port = envInfo.port;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
return stack;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
module.exports = { detectStack };
|