claudeos-core 1.0.7 → 1.2.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.
Potentially problematic release.
This version of claudeos-core might be problematic. Click here for more details.
- package/CHANGELOG.md +84 -1
- package/CONTRIBUTING.md +15 -4
- package/README.de.md +187 -11
- package/README.es.md +187 -11
- package/README.fr.md +187 -11
- package/README.hi.md +187 -11
- package/README.ja.md +186 -10
- package/README.ko.md +331 -364
- package/README.md +200 -11
- package/README.ru.md +187 -11
- package/README.vi.md +188 -12
- package/README.zh-CN.md +186 -10
- package/bin/cli.js +183 -61
- package/bootstrap.sh +128 -21
- package/content-validator/index.js +131 -60
- package/health-checker/index.js +29 -23
- package/import-linter/index.js +14 -8
- package/manifest-generator/index.js +26 -20
- package/package.json +84 -75
- package/pass-json-validator/index.js +92 -70
- package/pass-prompts/templates/common/header.md +4 -4
- package/pass-prompts/templates/common/lang-instructions.json +27 -0
- package/pass-prompts/templates/common/pass3-footer.md +2 -3
- package/pass-prompts/templates/java-spring/pass1.md +84 -81
- package/pass-prompts/templates/java-spring/pass2.md +66 -66
- package/pass-prompts/templates/java-spring/pass3.md +60 -60
- package/pass-prompts/templates/kotlin-spring/pass1.md +172 -0
- package/pass-prompts/templates/kotlin-spring/pass2.md +109 -0
- package/pass-prompts/templates/kotlin-spring/pass3.md +98 -0
- package/pass-prompts/templates/node-express/pass1.md +73 -73
- package/pass-prompts/templates/node-express/pass2.md +66 -66
- package/pass-prompts/templates/node-express/pass3.md +53 -53
- package/pass-prompts/templates/node-nextjs/pass1.md +68 -68
- package/pass-prompts/templates/node-nextjs/pass2.md +61 -61
- package/pass-prompts/templates/node-nextjs/pass3.md +48 -48
- package/pass-prompts/templates/python-django/pass1.md +78 -78
- package/pass-prompts/templates/python-django/pass2.md +69 -69
- package/pass-prompts/templates/python-django/pass3.md +45 -45
- package/pass-prompts/templates/python-fastapi/pass1.md +76 -76
- package/pass-prompts/templates/python-fastapi/pass2.md +67 -67
- package/pass-prompts/templates/python-fastapi/pass3.md +45 -45
- package/plan-installer/index.js +623 -97
- package/plan-validator/index.js +54 -23
- package/sync-checker/index.js +25 -14
package/plan-installer/index.js
CHANGED
|
@@ -3,17 +3,17 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* ClaudeOS-Core — plan-installer
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* - claudeos-core/generated/project-analysis.json (
|
|
9
|
-
* - claudeos-core/generated/domain-groups.json (Pass 1
|
|
10
|
-
* - claudeos-core/generated/pass1-backend-prompt.md (
|
|
11
|
-
* - claudeos-core/generated/pass1-frontend-prompt.md (
|
|
12
|
-
* - claudeos-core/generated/pass1-prompt.md (
|
|
6
|
+
* Role: Project analysis → Stack detection → Domain list → Auto group splitting → Dynamic prompt generation
|
|
7
|
+
* Output:
|
|
8
|
+
* - claudeos-core/generated/project-analysis.json (stack/structure info, multi-stack aware)
|
|
9
|
+
* - claudeos-core/generated/domain-groups.json (group splitting for Pass 1 execution, per type)
|
|
10
|
+
* - claudeos-core/generated/pass1-backend-prompt.md (backend analysis prompt)
|
|
11
|
+
* - claudeos-core/generated/pass1-frontend-prompt.md (frontend analysis prompt, if detected)
|
|
12
|
+
* - claudeos-core/generated/pass1-prompt.md (single-stack backward compat)
|
|
13
13
|
* - claudeos-core/generated/pass2-prompt.md
|
|
14
|
-
* - claudeos-core/generated/pass3-prompt.md (
|
|
14
|
+
* - claudeos-core/generated/pass3-prompt.md (combined prompt for multi-stack)
|
|
15
15
|
*
|
|
16
|
-
*
|
|
16
|
+
* Usage: npx claudeos-core <cmd> or node claudeos-core-tools/plan-installer/index.js
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
19
|
const fs = require("fs");
|
|
@@ -28,7 +28,7 @@ function ensureDir(dir) {
|
|
|
28
28
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
// ───
|
|
31
|
+
// ─── Stack detection (multi-stack) ────────────────────────────────
|
|
32
32
|
async function detectStack() {
|
|
33
33
|
const stack = {
|
|
34
34
|
language: null, languageVersion: null,
|
|
@@ -38,20 +38,137 @@ async function detectStack() {
|
|
|
38
38
|
packageManager: null, detected: [],
|
|
39
39
|
};
|
|
40
40
|
|
|
41
|
-
// ── Java: Gradle ──
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
41
|
+
// ── Java/Kotlin: Gradle ──
|
|
42
|
+
const gradleFile = fs.existsSync(path.join(ROOT, "build.gradle.kts"))
|
|
43
|
+
? "build.gradle.kts"
|
|
44
|
+
: fs.existsSync(path.join(ROOT, "build.gradle")) ? "build.gradle" : null;
|
|
45
|
+
if (gradleFile) {
|
|
46
|
+
const g = fs.readFileSync(path.join(ROOT, gradleFile), "utf-8");
|
|
47
|
+
stack.buildTool = "gradle"; stack.detected.push(gradleFile);
|
|
45
48
|
if (g.includes("spring-boot")) { stack.language = "java"; stack.framework = "spring-boot"; stack.detected.push("spring-boot"); }
|
|
46
|
-
const
|
|
47
|
-
|
|
49
|
+
const svPatterns = [
|
|
50
|
+
/org\.springframework\.boot.*version\s*['"]([^'"]+)['"]/,
|
|
51
|
+
/id\s*\(\s*["']org\.springframework\.boot["']\s*\)\s*version\s*["']([^"']+)["']/,
|
|
52
|
+
/spring-boot-dependencies:([^'")\s]+)/,
|
|
53
|
+
];
|
|
54
|
+
for (const pattern of svPatterns) {
|
|
55
|
+
const sv = g.match(pattern);
|
|
56
|
+
if (sv) { stack.frameworkVersion = sv[1]; break; }
|
|
57
|
+
}
|
|
48
58
|
const jv = g.match(/sourceCompatibility\s*=\s*['"]?(\d+)['"]?/);
|
|
49
59
|
if (jv) stack.languageVersion = jv[1];
|
|
50
|
-
|
|
51
|
-
if (
|
|
52
|
-
if (g.includes("
|
|
53
|
-
if (g.includes("
|
|
54
|
-
if (g.includes("
|
|
60
|
+
// ORM detection (first match wins — order by specificity)
|
|
61
|
+
if (!stack.orm && g.includes("mybatis")) { stack.orm = "mybatis"; stack.detected.push("mybatis"); }
|
|
62
|
+
if (!stack.orm && (g.includes("jpa") || g.includes("hibernate"))) { stack.orm = "jpa"; stack.detected.push("jpa"); }
|
|
63
|
+
if (!stack.orm && g.includes("exposed")) { stack.orm = "exposed"; stack.detected.push("exposed"); }
|
|
64
|
+
if (!stack.orm && g.includes("jooq")) { stack.orm = "jooq"; stack.detected.push("jooq"); }
|
|
65
|
+
if (!stack.orm && g.includes("spring-data-jdbc")) { stack.orm = "spring-data-jdbc"; stack.detected.push("spring-data-jdbc"); }
|
|
66
|
+
if (!stack.orm && g.includes("r2dbc")) { stack.orm = "r2dbc"; stack.detected.push("r2dbc"); }
|
|
67
|
+
// DB detection
|
|
68
|
+
if (!stack.database && g.includes("postgresql")) { stack.database = "postgresql"; stack.detected.push("postgresql"); }
|
|
69
|
+
if (!stack.database && g.includes("mysql")) { stack.database = "mysql"; stack.detected.push("mysql"); }
|
|
70
|
+
if (!stack.database && g.includes("oracle")) { stack.database = "oracle"; stack.detected.push("oracle"); }
|
|
71
|
+
if (!stack.database && g.includes("mongodb")) { stack.database = "mongodb"; stack.detected.push("mongodb"); }
|
|
72
|
+
if (!stack.database && g.includes("h2")) { stack.database = "h2"; stack.detected.push("h2"); }
|
|
73
|
+
// Kotlin detection: override language if Kotlin plugin found
|
|
74
|
+
if (g.includes("kotlin") || g.includes("org.jetbrains.kotlin")) {
|
|
75
|
+
stack.language = "kotlin"; stack.detected.push("kotlin");
|
|
76
|
+
// Try multiple patterns for Kotlin version extraction
|
|
77
|
+
const kvPatterns = [
|
|
78
|
+
/kotlin\S*\s*version\s*['"]([^'"]+)['"]/,
|
|
79
|
+
/org\.jetbrains\.kotlin\S*\s*version\s*['"]([^'"]+)['"]/,
|
|
80
|
+
/kotlin\("jvm"\)\s*version\s*["']([^"']+)["']/,
|
|
81
|
+
];
|
|
82
|
+
for (const pattern of kvPatterns) {
|
|
83
|
+
const match = g.match(pattern);
|
|
84
|
+
if (match) { stack.languageVersion = match[1]; break; }
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ── Gradle: version catalogs (libs.versions.toml) — fallback for versions & deps ──
|
|
90
|
+
const versionCatalog = path.join(ROOT, "gradle/libs.versions.toml");
|
|
91
|
+
if (fs.existsSync(versionCatalog)) {
|
|
92
|
+
const vc = fs.readFileSync(versionCatalog, "utf-8");
|
|
93
|
+
stack.detected.push("libs.versions.toml");
|
|
94
|
+
// Language version fallback
|
|
95
|
+
if (!stack.languageVersion) {
|
|
96
|
+
const kvMatch = vc.match(/kotlin\s*=\s*["']([^"']+)["']/);
|
|
97
|
+
if (kvMatch) stack.languageVersion = kvMatch[1];
|
|
98
|
+
}
|
|
99
|
+
if (!stack.frameworkVersion) {
|
|
100
|
+
const sbMatch = vc.match(/spring-boot\s*=\s*["']([^"']+)["']/);
|
|
101
|
+
if (sbMatch) stack.frameworkVersion = sbMatch[1];
|
|
102
|
+
}
|
|
103
|
+
// ORM/DB detection from version catalog libraries
|
|
104
|
+
if (!stack.orm && vc.includes("exposed")) { stack.orm = "exposed"; stack.detected.push("exposed (catalog)"); }
|
|
105
|
+
if (!stack.orm && vc.includes("jooq")) { stack.orm = "jooq"; stack.detected.push("jooq (catalog)"); }
|
|
106
|
+
if (!stack.orm && (vc.includes("jpa") || vc.includes("hibernate"))) { stack.orm = "jpa"; stack.detected.push("jpa (catalog)"); }
|
|
107
|
+
if (!stack.database && vc.includes("postgresql")) { stack.database = "postgresql"; }
|
|
108
|
+
if (!stack.database && vc.includes("mysql")) { stack.database = "mysql"; }
|
|
109
|
+
if (!stack.database && vc.includes("mongodb")) { stack.database = "mongodb"; }
|
|
110
|
+
// Kotlin detection from catalog
|
|
111
|
+
if (!stack.language && vc.includes("kotlin")) {
|
|
112
|
+
stack.language = "kotlin"; stack.detected.push("kotlin (catalog)");
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ── Kotlin: multi-module Gradle detection (check subproject build files) ──
|
|
117
|
+
if (stack.language !== "kotlin" && stack.buildTool === "gradle") {
|
|
118
|
+
const subBuildFiles = await glob("**/build.gradle{,.kts}", { cwd: ROOT, ignore: ["**/node_modules/**", "**/build/**"] });
|
|
119
|
+
for (const sbf of subBuildFiles.slice(0, 5)) {
|
|
120
|
+
const sc = fs.readFileSync(path.join(ROOT, sbf), "utf-8");
|
|
121
|
+
if (sc.includes("kotlin") || sc.includes("org.jetbrains.kotlin")) {
|
|
122
|
+
stack.language = "kotlin"; stack.detected.push("kotlin (submodule)");
|
|
123
|
+
const kv = sc.match(/kotlin\S*\s*version\s*['"]([^'"]+)['"]/);
|
|
124
|
+
if (kv) stack.languageVersion = kv[1];
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ── Kotlin: detect CQRS/multi-module architecture from settings.gradle ──
|
|
131
|
+
const settingsFile = fs.existsSync(path.join(ROOT, "settings.gradle.kts"))
|
|
132
|
+
? "settings.gradle.kts"
|
|
133
|
+
: fs.existsSync(path.join(ROOT, "settings.gradle")) ? "settings.gradle" : null;
|
|
134
|
+
if (settingsFile && stack.language === "kotlin") {
|
|
135
|
+
const sg = fs.readFileSync(path.join(ROOT, settingsFile), "utf-8");
|
|
136
|
+
|
|
137
|
+
// Extract all module names from include() calls
|
|
138
|
+
// Handles: include(":m1", ":m2"), include(":m1"), include ":m1", ":m2"
|
|
139
|
+
// Also strips leading ":" prefix (Gradle subproject path convention)
|
|
140
|
+
// Pre-filter: remove single-line comments to avoid matching commented-out includes
|
|
141
|
+
const sgClean = sg.split("\n").filter(l => !l.trimStart().startsWith("//")).join("\n");
|
|
142
|
+
const includes = [];
|
|
143
|
+
// Kotlin DSL: include(":m1", ":m2", ":m3") — may span multiple lines
|
|
144
|
+
const includeBlocks = [...sgClean.matchAll(/include\s*\(([^)]*)\)/gs)];
|
|
145
|
+
for (const block of includeBlocks) {
|
|
146
|
+
const quotedValues = [...block[1].matchAll(/["']([^"']+)["']/g)].map(m => m[1]);
|
|
147
|
+
includes.push(...quotedValues);
|
|
148
|
+
}
|
|
149
|
+
// Groovy DSL: include ':m1', ':m2', ':m3' — single line, comma-separated
|
|
150
|
+
if (includes.length === 0) {
|
|
151
|
+
const groovyIncludes = [...sgClean.matchAll(/include\s+(.+)/g)];
|
|
152
|
+
for (const line of groovyIncludes) {
|
|
153
|
+
const quotedValues = [...line[1].matchAll(/['"]([^'"]+)['"]/g)].map(m => m[1]);
|
|
154
|
+
includes.push(...quotedValues);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Strip leading ":" prefix from all module names (Gradle convention)
|
|
159
|
+
const cleanModules = includes.map(m => m.replace(/^:/, ""));
|
|
160
|
+
|
|
161
|
+
if (cleanModules.length > 0) {
|
|
162
|
+
stack.multiModule = true;
|
|
163
|
+
stack.modules = cleanModules;
|
|
164
|
+
stack.detected.push(`multi-module (${cleanModules.length} modules)`);
|
|
165
|
+
// Detect CQRS from module names
|
|
166
|
+
const hasCommand = cleanModules.some(m => m.includes("command"));
|
|
167
|
+
const hasQuery = cleanModules.some(m => m.includes("query"));
|
|
168
|
+
const hasBff = cleanModules.some(m => m.includes("bff"));
|
|
169
|
+
if (hasCommand && hasQuery) { stack.architecture = "cqrs"; stack.detected.push("cqrs"); }
|
|
170
|
+
if (hasBff) { stack.detected.push("bff"); }
|
|
171
|
+
}
|
|
55
172
|
}
|
|
56
173
|
|
|
57
174
|
// ── Java: Maven ──
|
|
@@ -63,28 +180,36 @@ async function detectStack() {
|
|
|
63
180
|
const jv = pom.match(/<java\.version>(\d+)/);
|
|
64
181
|
if (jv) stack.languageVersion = jv[1];
|
|
65
182
|
if (pom.includes("spring-boot") && !stack.framework) { stack.framework = "spring-boot"; stack.detected.push("spring-boot"); }
|
|
66
|
-
if (pom.includes("mybatis")
|
|
67
|
-
if (pom.includes("jpa") || pom.includes("hibernate")) {
|
|
68
|
-
if (pom.includes("
|
|
69
|
-
if (pom.includes("
|
|
70
|
-
if (pom.includes("
|
|
183
|
+
if (!stack.orm && pom.includes("mybatis")) { stack.orm = "mybatis"; stack.detected.push("mybatis"); }
|
|
184
|
+
if (!stack.orm && (pom.includes("jpa") || pom.includes("hibernate"))) { stack.orm = "jpa"; stack.detected.push("jpa"); }
|
|
185
|
+
if (!stack.orm && pom.includes("exposed")) { stack.orm = "exposed"; stack.detected.push("exposed"); }
|
|
186
|
+
if (!stack.orm && pom.includes("jooq")) { stack.orm = "jooq"; stack.detected.push("jooq"); }
|
|
187
|
+
if (!stack.database && pom.includes("postgresql")) stack.database = "postgresql";
|
|
188
|
+
if (!stack.database && pom.includes("mysql")) stack.database = "mysql";
|
|
189
|
+
if (!stack.database && pom.includes("oracle")) stack.database = "oracle";
|
|
190
|
+
if (!stack.database && pom.includes("mongodb")) stack.database = "mongodb";
|
|
191
|
+
if (!stack.database && pom.includes("h2")) stack.database = "h2";
|
|
192
|
+
if (!stack.database && pom.includes("sqlite")) stack.database = "sqlite";
|
|
71
193
|
}
|
|
72
194
|
|
|
73
195
|
// ── Node.js ──
|
|
74
196
|
if (fs.existsSync(path.join(ROOT, "package.json"))) {
|
|
75
|
-
|
|
197
|
+
let pkg;
|
|
198
|
+
try { pkg = JSON.parse(fs.readFileSync(path.join(ROOT, "package.json"), "utf-8")); }
|
|
199
|
+
catch { console.warn(" ⚠️ package.json parse error — skipping Node.js detection"); pkg = null; }
|
|
200
|
+
if (!pkg) return stack;
|
|
76
201
|
stack.detected.push("package.json");
|
|
77
202
|
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
78
203
|
|
|
79
204
|
if (!stack.language) stack.language = deps.typescript ? "typescript" : "javascript";
|
|
80
205
|
if (deps.typescript) { stack.detected.push("typescript"); stack.languageVersion = deps.typescript.replace(/[^0-9.]/g, ""); }
|
|
81
206
|
|
|
82
|
-
//
|
|
207
|
+
// Frontend
|
|
83
208
|
if (deps.next) { stack.frontend = "nextjs"; stack.detected.push("next.js"); stack.frontendVersion = deps.next.replace(/[^0-9.]/g, ""); }
|
|
84
209
|
else if (deps.react) { stack.frontend = "react"; stack.detected.push("react"); stack.frontendVersion = deps.react.replace(/[^0-9.]/g, ""); }
|
|
85
210
|
else if (deps.vue) { stack.frontend = "vue"; stack.detected.push("vue"); stack.frontendVersion = deps.vue.replace(/[^0-9.]/g, ""); }
|
|
86
211
|
|
|
87
|
-
//
|
|
212
|
+
// Backend framework
|
|
88
213
|
if (deps.express && !stack.framework) { stack.framework = "express"; stack.detected.push("express"); }
|
|
89
214
|
if (deps["@nestjs/core"]) { stack.framework = "nestjs"; stack.detected.push("nestjs"); stack.frameworkVersion = deps["@nestjs/core"].replace(/[^0-9.]/g, ""); }
|
|
90
215
|
|
|
@@ -101,7 +226,7 @@ async function detectStack() {
|
|
|
101
226
|
if (deps.mysql2 && !stack.database) stack.database = "mysql";
|
|
102
227
|
if (deps.mongodb && !stack.database) stack.database = "mongodb";
|
|
103
228
|
|
|
104
|
-
//
|
|
229
|
+
// Package manager
|
|
105
230
|
stack.packageManager = fs.existsSync(path.join(ROOT, "pnpm-lock.yaml")) ? "pnpm"
|
|
106
231
|
: fs.existsSync(path.join(ROOT, "yarn.lock")) ? "yarn" : "npm";
|
|
107
232
|
|
|
@@ -149,11 +274,16 @@ async function detectStack() {
|
|
|
149
274
|
const ymls = await glob("**/application*.yml", { cwd: ROOT, absolute: true, ignore: ["**/node_modules/**"] });
|
|
150
275
|
for (const y of ymls) {
|
|
151
276
|
const c = fs.readFileSync(y, "utf-8");
|
|
152
|
-
if (c.includes("postgresql")
|
|
153
|
-
if (c.includes("mysql")
|
|
154
|
-
if (c.includes("oracle")
|
|
155
|
-
|
|
156
|
-
if (
|
|
277
|
+
if (!stack.database && c.includes("postgresql")) stack.database = "postgresql";
|
|
278
|
+
if (!stack.database && c.includes("mysql")) stack.database = "mysql";
|
|
279
|
+
if (!stack.database && c.includes("oracle")) stack.database = "oracle";
|
|
280
|
+
if (!stack.database && c.includes("mongodb")) stack.database = "mongodb";
|
|
281
|
+
if (!stack.database && /\bh2\b/.test(c)) stack.database = "h2";
|
|
282
|
+
if (!stack.database && c.includes("sqlite")) stack.database = "sqlite";
|
|
283
|
+
if (!stack.port) {
|
|
284
|
+
const pm = c.match(/server:\s*\n\s*port:\s*(\d+)/) || c.match(/server\.port\s*[=:]\s*(\d+)/);
|
|
285
|
+
if (pm) stack.port = parseInt(pm[1]);
|
|
286
|
+
}
|
|
157
287
|
}
|
|
158
288
|
|
|
159
289
|
// .env
|
|
@@ -181,7 +311,7 @@ async function detectStack() {
|
|
|
181
311
|
}
|
|
182
312
|
}
|
|
183
313
|
|
|
184
|
-
// ── Config file fallback (
|
|
314
|
+
// ── Config file fallback (monorepo: detect from config files when not in package.json) ──
|
|
185
315
|
if (!stack.frontend) {
|
|
186
316
|
const nextConfigs = ["next.config.js", "next.config.mjs", "next.config.ts"];
|
|
187
317
|
if (nextConfigs.some(c => fs.existsSync(path.join(ROOT, c)))) {
|
|
@@ -205,7 +335,7 @@ async function detectStack() {
|
|
|
205
335
|
return stack;
|
|
206
336
|
}
|
|
207
337
|
|
|
208
|
-
// ───
|
|
338
|
+
// ─── Structure scan (multi-stack) ────────────────────────────────
|
|
209
339
|
async function scanStructure(stack) {
|
|
210
340
|
const backendDomains = [];
|
|
211
341
|
const frontendDomains = [];
|
|
@@ -221,7 +351,7 @@ async function scanStructure(stack) {
|
|
|
221
351
|
const domainMap = {};
|
|
222
352
|
let detectedPattern = null;
|
|
223
353
|
|
|
224
|
-
// Pattern A: controller/{domain}/*.java (
|
|
354
|
+
// Pattern A: controller/{domain}/*.java (layer-first — domain under controller)
|
|
225
355
|
const controllersA = await glob("src/main/java/**/controller/*/*.java", { cwd: ROOT });
|
|
226
356
|
for (const f of controllersA) {
|
|
227
357
|
const m = f.match(/controller\/([^/]+)\//);
|
|
@@ -233,8 +363,8 @@ async function scanStructure(stack) {
|
|
|
233
363
|
}
|
|
234
364
|
if (Object.keys(domainMap).length > 0) detectedPattern = "A";
|
|
235
365
|
|
|
236
|
-
// Pattern B/D: {domain}/controller/*.java (
|
|
237
|
-
// D
|
|
366
|
+
// Pattern B/D: {domain}/controller/*.java (domain-first — controller under domain)
|
|
367
|
+
// D extends B: {module}/{domain}/controller/ — auto-upgrade to module/domain on name conflict
|
|
238
368
|
if (!detectedPattern) {
|
|
239
369
|
const controllersB = await glob("src/main/java/**/*/controller/*.java", { cwd: ROOT });
|
|
240
370
|
const domainPaths = {};
|
|
@@ -249,11 +379,11 @@ async function scanStructure(stack) {
|
|
|
249
379
|
}
|
|
250
380
|
}
|
|
251
381
|
|
|
252
|
-
//
|
|
382
|
+
// If same domain name found in multiple modules, use module/domain form (Pattern D)
|
|
253
383
|
for (const [d, entries] of Object.entries(domainPaths)) {
|
|
254
384
|
const modules = [...new Set(entries.map(e => e.module).filter(Boolean))];
|
|
255
385
|
if (modules.length > 1) {
|
|
256
|
-
// Pattern D:
|
|
386
|
+
// Pattern D: conflict — register as module/domain
|
|
257
387
|
for (const entry of entries) {
|
|
258
388
|
const fullName = entry.module ? `${entry.module}/${d}` : d;
|
|
259
389
|
if (!domainMap[fullName]) domainMap[fullName] = { controllers: 0, services: 0, mappers: 0, dtos: 0, xmlMappers: 0, pattern: "D", modulePath: entry.module, domainName: d };
|
|
@@ -267,7 +397,7 @@ async function scanStructure(stack) {
|
|
|
267
397
|
if (Object.keys(domainMap).length > 0) detectedPattern = domainMap[Object.keys(domainMap)[0]].pattern;
|
|
268
398
|
}
|
|
269
399
|
|
|
270
|
-
// Pattern E: DDD
|
|
400
|
+
// Pattern E: DDD/Hexagonal — {domain}/adapter/in/web/*.java or {domain}/adapter/in/rest/*.java
|
|
271
401
|
if (!detectedPattern) {
|
|
272
402
|
const controllersE = await glob("src/main/java/**/adapter/in/{web,rest}/*.java", { cwd: ROOT });
|
|
273
403
|
for (const f of controllersE) {
|
|
@@ -281,7 +411,7 @@ async function scanStructure(stack) {
|
|
|
281
411
|
if (Object.keys(domainMap).length > 0) detectedPattern = "E";
|
|
282
412
|
}
|
|
283
413
|
|
|
284
|
-
// Pattern C:
|
|
414
|
+
// Pattern C: Flat structure — controller/*.java (no domain directory, extract domain from class name)
|
|
285
415
|
if (!detectedPattern) {
|
|
286
416
|
const controllersC = await glob("src/main/java/**/controller/*.java", { cwd: ROOT });
|
|
287
417
|
for (const f of controllersC) {
|
|
@@ -295,8 +425,8 @@ async function scanStructure(stack) {
|
|
|
295
425
|
if (Object.keys(domainMap).length > 0) detectedPattern = "C";
|
|
296
426
|
}
|
|
297
427
|
|
|
298
|
-
// ──
|
|
299
|
-
// core/delivery
|
|
428
|
+
// ── Supplementary scan: detect service-only domains without controllers ──
|
|
429
|
+
// Catch domains like core/delivery that have service/mapper but no controller
|
|
300
430
|
if (detectedPattern === "B" || detectedPattern === "D" || !detectedPattern) {
|
|
301
431
|
const serviceDirs = await glob("src/main/java/**/*/service/*.java", { cwd: ROOT });
|
|
302
432
|
const mapperDirs = await glob("src/main/java/**/*/{mapper,repository}/*.java", { cwd: ROOT });
|
|
@@ -313,7 +443,7 @@ async function scanStructure(stack) {
|
|
|
313
443
|
}
|
|
314
444
|
}
|
|
315
445
|
|
|
316
|
-
//
|
|
446
|
+
// Scan service/mapper/dto/xml files for each domain
|
|
317
447
|
for (const d of Object.keys(domainMap)) {
|
|
318
448
|
const p = domainMap[d].pattern;
|
|
319
449
|
const dn = domainMap[d].domainName || d;
|
|
@@ -332,7 +462,7 @@ async function scanStructure(stack) {
|
|
|
332
462
|
mprGlob = `src/main/java/**/${d}/adapter/out/{persistence,repository}/*.java`;
|
|
333
463
|
dtoGlob = `src/main/java/**/${d}/**/{dto,command,query}/**/*.java`;
|
|
334
464
|
} else {
|
|
335
|
-
// Pattern C:
|
|
465
|
+
// Pattern C: Flat — match domain name from file name
|
|
336
466
|
const cap = d.charAt(0).toUpperCase() + d.slice(1);
|
|
337
467
|
svcGlob = `src/main/java/**/service/${cap}*.java`;
|
|
338
468
|
mprGlob = `src/main/java/**/{mapper,repository}/${cap}*.java`;
|
|
@@ -351,7 +481,7 @@ async function scanStructure(stack) {
|
|
|
351
481
|
backendDomains.push({ name: d, type: "backend", ...domainMap[d], totalFiles: svc.length + mpr.length + dto.length + xml.length + domainMap[d].controllers });
|
|
352
482
|
}
|
|
353
483
|
|
|
354
|
-
// ── Java
|
|
484
|
+
// ── Java fallback: extract domains directly from all .java files when glob returns 0 ──
|
|
355
485
|
if (backendDomains.length === 0) {
|
|
356
486
|
const allJava = await glob("**/*.java", { cwd: ROOT, ignore: ["**/node_modules/**", "**/build/**", "**/target/**", "**/test/**", "**/generated/**"] });
|
|
357
487
|
const javaDomains = {};
|
|
@@ -363,11 +493,11 @@ async function scanStructure(stack) {
|
|
|
363
493
|
const parts = f.replace(/\\/g, "/").split("/");
|
|
364
494
|
for (let i = 0; i < parts.length - 1; i++) {
|
|
365
495
|
if (layerNames.includes(parts[i]) && i > 0) {
|
|
366
|
-
// Pattern A
|
|
496
|
+
// Pattern A detection: .../{domain}/controller/... or .../controller/{domain}/...
|
|
367
497
|
const prevDir = parts[i - 1];
|
|
368
498
|
const nextDir = parts[i + 1];
|
|
369
499
|
|
|
370
|
-
// {domain}/layer/
|
|
500
|
+
// {domain}/layer/ pattern (domain before layer)
|
|
371
501
|
if (!skipNames.includes(prevDir) && !layerNames.includes(prevDir) && !prevDir.includes(".") && !versionPattern.test(prevDir)) {
|
|
372
502
|
if (!javaDomains[prevDir]) javaDomains[prevDir] = { controllers: 0, services: 0, mappers: 0, dtos: 0, xmlMappers: 0, pattern: "B" };
|
|
373
503
|
if (parts[i] === "controller") javaDomains[prevDir].controllers++;
|
|
@@ -375,7 +505,7 @@ async function scanStructure(stack) {
|
|
|
375
505
|
else if (["mapper", "repository", "dao"].includes(parts[i])) javaDomains[prevDir].mappers++;
|
|
376
506
|
else if (["dto", "vo"].includes(parts[i])) javaDomains[prevDir].dtos++;
|
|
377
507
|
}
|
|
378
|
-
// layer/{domain}/
|
|
508
|
+
// layer/{domain}/ pattern (layer before domain)
|
|
379
509
|
if (nextDir && !nextDir.endsWith(".java") && !skipNames.includes(nextDir) && !layerNames.includes(nextDir) && !versionPattern.test(nextDir)) {
|
|
380
510
|
if (!javaDomains[nextDir]) javaDomains[nextDir] = { controllers: 0, services: 0, mappers: 0, dtos: 0, xmlMappers: 0, pattern: "A" };
|
|
381
511
|
if (parts[i] === "controller") javaDomains[nextDir].controllers++;
|
|
@@ -397,7 +527,191 @@ async function scanStructure(stack) {
|
|
|
397
527
|
}
|
|
398
528
|
}
|
|
399
529
|
|
|
400
|
-
// ──
|
|
530
|
+
// ── Kotlin (multi-module monorepo + CQRS support) ──
|
|
531
|
+
if (stack.language === "kotlin") {
|
|
532
|
+
// Scan all .kt files across all submodules
|
|
533
|
+
const ktFiles = await glob("**/src/main/kotlin/**/*.kt", {
|
|
534
|
+
cwd: ROOT,
|
|
535
|
+
ignore: ["**/node_modules/**", "**/build/**", "**/test/**", "**/generated/**"],
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
// Detect root package from first controller/service file
|
|
539
|
+
for (const f of ktFiles) {
|
|
540
|
+
const m = f.match(/src\/main\/kotlin\/(.+?)\/(controller|service|mapper|dto|entity|repository|adapter)/);
|
|
541
|
+
if (m) { rootPackage = m[1].replace(/\//g, "."); break; }
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// Detect modules from directory structure (servers/*/ or direct submodules)
|
|
545
|
+
const moduleGlobs = [
|
|
546
|
+
"servers/*/*/src/main/kotlin/", // servers/command/reservation-command-server/
|
|
547
|
+
"servers/*/src/main/kotlin/", // servers/iam-server/
|
|
548
|
+
"*/src/main/kotlin/", // shared-lib/ or integration-lib/
|
|
549
|
+
];
|
|
550
|
+
const moduleSet = {};
|
|
551
|
+
for (const mg of moduleGlobs) {
|
|
552
|
+
const moduleDirs = await glob(mg, { cwd: ROOT });
|
|
553
|
+
for (const md of moduleDirs) {
|
|
554
|
+
const parts = md.replace(/\/src\/main\/kotlin\/$/, "").split("/");
|
|
555
|
+
const moduleName = parts[parts.length - 1]; // e.g., "reservation-command-server"
|
|
556
|
+
if (moduleSet[moduleName]) {
|
|
557
|
+
// Name conflict: use full relative path as key to avoid silent loss
|
|
558
|
+
const fullKey = md.replace(/\/src\/main\/kotlin\/$/, "").replace(/\//g, "__");
|
|
559
|
+
moduleSet[fullKey] = md;
|
|
560
|
+
} else {
|
|
561
|
+
moduleSet[moduleName] = md;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// Categorize modules and extract domains
|
|
567
|
+
const skipModules = ["build", "gradle", "buildSrc", ".gradle", "node_modules"];
|
|
568
|
+
const serverTypes = { command: "command", query: "query", bff: "bff", integration: "integration" };
|
|
569
|
+
const domainMap = {};
|
|
570
|
+
|
|
571
|
+
for (const [moduleName, modulePath] of Object.entries(moduleSet)) {
|
|
572
|
+
if (skipModules.some(s => moduleName.includes(s))) continue;
|
|
573
|
+
|
|
574
|
+
// Determine server type from module name
|
|
575
|
+
let serverType = "standalone";
|
|
576
|
+
for (const [key, val] of Object.entries(serverTypes)) {
|
|
577
|
+
if (moduleName.includes(key)) { serverType = val; break; }
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// Extract domain name from module name
|
|
581
|
+
// e.g., "reservation-command-server" → "reservation"
|
|
582
|
+
// e.g., "pms-bff-server" → "pms"
|
|
583
|
+
// e.g., "iam-server" → "iam"
|
|
584
|
+
// e.g., "shared-lib" → "shared-lib"
|
|
585
|
+
// e.g., "common-query-server" → "common-query" (shared server, not a domain named "common")
|
|
586
|
+
let domainName = moduleName
|
|
587
|
+
.replace(/-(?:command|query|bff|integration|adapter)-server$/, "")
|
|
588
|
+
.replace(/-server$/, "")
|
|
589
|
+
.replace(/-lib$/, "-lib");
|
|
590
|
+
|
|
591
|
+
// Guard: if domain extraction resulted in a generic name that is likely a shared module
|
|
592
|
+
// (e.g., "common-query-server" → "common"), keep the full type qualifier
|
|
593
|
+
const genericNames = ["common", "shared", "base", "core", "global", "internal"];
|
|
594
|
+
if (genericNames.includes(domainName) && serverType !== "standalone") {
|
|
595
|
+
domainName = `${domainName}-${serverType}`;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// Scan files in this module (append "/" to prevent prefix overlap: e.g., reservation vs reservation-ext)
|
|
599
|
+
const modulePrefix = modulePath.replace(/\/$/, "").replace(/\/src\/main\/kotlin$/, "") + "/";
|
|
600
|
+
const moduleKtFiles = ktFiles.filter(f => f.startsWith(modulePrefix) || f === modulePrefix.slice(0, -1));
|
|
601
|
+
const controllers = moduleKtFiles.filter(f => /controller/i.test(f)).length;
|
|
602
|
+
const services = moduleKtFiles.filter(f => /service/i.test(f)).length;
|
|
603
|
+
const repositories = moduleKtFiles.filter(f => /repository|mapper/i.test(f)).length;
|
|
604
|
+
const dtos = moduleKtFiles.filter(f => /dto|vo|request|response|model/i.test(f) && !/repository|service|controller/i.test(f)).length;
|
|
605
|
+
const totalFiles = moduleKtFiles.length;
|
|
606
|
+
|
|
607
|
+
if (totalFiles === 0) continue;
|
|
608
|
+
|
|
609
|
+
const key = `${domainName}:${serverType}`;
|
|
610
|
+
domainMap[key] = {
|
|
611
|
+
name: domainName,
|
|
612
|
+
moduleName,
|
|
613
|
+
modulePath,
|
|
614
|
+
serverType,
|
|
615
|
+
controllers,
|
|
616
|
+
services,
|
|
617
|
+
repositories,
|
|
618
|
+
dtos,
|
|
619
|
+
totalFiles,
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// Group by domain: merge command/query/bff of same domain
|
|
624
|
+
const domainGroups = {};
|
|
625
|
+
for (const [key, info] of Object.entries(domainMap)) {
|
|
626
|
+
const { name } = info;
|
|
627
|
+
if (!domainGroups[name]) {
|
|
628
|
+
domainGroups[name] = { name, type: "backend", modules: [], controllers: 0, services: 0, repositories: 0, dtos: 0, totalFiles: 0, serverTypes: [] };
|
|
629
|
+
}
|
|
630
|
+
const dg = domainGroups[name];
|
|
631
|
+
dg.modules.push(info.moduleName);
|
|
632
|
+
dg.controllers += info.controllers;
|
|
633
|
+
dg.services += info.services;
|
|
634
|
+
dg.repositories += info.repositories;
|
|
635
|
+
dg.dtos += info.dtos;
|
|
636
|
+
dg.totalFiles += info.totalFiles;
|
|
637
|
+
if (!dg.serverTypes.includes(info.serverType)) dg.serverTypes.push(info.serverType);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// Push to backendDomains
|
|
641
|
+
for (const dg of Object.values(domainGroups)) {
|
|
642
|
+
backendDomains.push({
|
|
643
|
+
name: dg.name,
|
|
644
|
+
type: "backend",
|
|
645
|
+
controllers: dg.controllers,
|
|
646
|
+
services: dg.services,
|
|
647
|
+
mappers: dg.repositories,
|
|
648
|
+
dtos: dg.dtos,
|
|
649
|
+
totalFiles: dg.totalFiles,
|
|
650
|
+
modules: dg.modules,
|
|
651
|
+
serverTypes: dg.serverTypes,
|
|
652
|
+
pattern: "kotlin-multimodule",
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// Also scan shared libraries as special domains
|
|
657
|
+
const libDirs = await glob("{shared-lib,integration-lib,*-lib}/src/main/kotlin/", { cwd: ROOT });
|
|
658
|
+
for (const ld of libDirs) {
|
|
659
|
+
const libName = ld.split("/")[0];
|
|
660
|
+
if (domainGroups[libName]) continue; // Already captured
|
|
661
|
+
const libFiles = ktFiles.filter(f => f.startsWith(libName));
|
|
662
|
+
if (libFiles.length > 0) {
|
|
663
|
+
backendDomains.push({
|
|
664
|
+
name: libName,
|
|
665
|
+
type: "backend",
|
|
666
|
+
controllers: 0,
|
|
667
|
+
services: libFiles.filter(f => /service/i.test(f)).length,
|
|
668
|
+
mappers: 0,
|
|
669
|
+
dtos: libFiles.filter(f => /dto|vo|model/i.test(f)).length,
|
|
670
|
+
totalFiles: libFiles.length,
|
|
671
|
+
modules: [libName],
|
|
672
|
+
serverTypes: ["library"],
|
|
673
|
+
pattern: "kotlin-library",
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// Resolve shared query modules: redistribute internal domains to actual domain entries
|
|
679
|
+
resolveSharedQueryDomains(backendDomains, ktFiles);
|
|
680
|
+
|
|
681
|
+
// ── Kotlin single-module fallback (no multi-module structure detected) ──
|
|
682
|
+
if (backendDomains.length === 0 && ktFiles.length > 0) {
|
|
683
|
+
const ktDomains = {};
|
|
684
|
+
const skipNames = ["common", "config", "util", "utils", "base", "shared", "global", "framework", "infra", "main", "generated", "build"];
|
|
685
|
+
const layerKw = ["controller", "service", "repository", "mapper", "dao", "dto", "vo", "entity", "aggregate", "adapter"];
|
|
686
|
+
for (const f of ktFiles) {
|
|
687
|
+
const parts = f.replace(/\\/g, "/").split("/");
|
|
688
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
689
|
+
if (layerKw.includes(parts[i].toLowerCase())) {
|
|
690
|
+
// domain/layer/ pattern
|
|
691
|
+
if (i > 0) {
|
|
692
|
+
const d = parts[i - 1].toLowerCase();
|
|
693
|
+
if (!skipNames.includes(d) && !layerKw.includes(d) && d.length > 1 && d !== "kotlin") {
|
|
694
|
+
if (!ktDomains[d]) ktDomains[d] = { controllers: 0, services: 0, mappers: 0, dtos: 0, totalFiles: 0 };
|
|
695
|
+
if (parts[i] === "controller") ktDomains[d].controllers++;
|
|
696
|
+
else if (parts[i] === "service") ktDomains[d].services++;
|
|
697
|
+
else if (["repository", "mapper", "dao"].includes(parts[i])) ktDomains[d].mappers++;
|
|
698
|
+
else if (["dto", "vo", "entity"].includes(parts[i])) ktDomains[d].dtos++;
|
|
699
|
+
ktDomains[d].totalFiles++;
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
break;
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
for (const [d, data] of Object.entries(ktDomains)) {
|
|
707
|
+
if (data.totalFiles > 0) {
|
|
708
|
+
backendDomains.push({ name: d, type: "backend", ...data, pattern: "kotlin-single" });
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
// ── Node.js backend (Express/NestJS) — scan regardless of frontend presence ──
|
|
401
715
|
if ((stack.language === "typescript" || stack.language === "javascript") && stack.framework) {
|
|
402
716
|
const nestModules = await glob("src/modules/*/", { cwd: ROOT });
|
|
403
717
|
const srcDirs = nestModules.length > 0 ? nestModules : await glob("src/*/", { cwd: ROOT });
|
|
@@ -417,7 +731,7 @@ async function scanStructure(stack) {
|
|
|
417
731
|
|
|
418
732
|
// ── Next.js/React/Vue ──
|
|
419
733
|
if (stack.frontend === "nextjs" || stack.frontend === "react" || stack.frontend === "vue") {
|
|
420
|
-
// App Router / Pages Router
|
|
734
|
+
// App Router / Pages Router domains
|
|
421
735
|
const allDirs = [
|
|
422
736
|
...await glob("{app,src/app}/*/", { cwd: ROOT }),
|
|
423
737
|
...await glob("{pages,src/pages}/*/", { cwd: ROOT }),
|
|
@@ -456,7 +770,7 @@ async function scanStructure(stack) {
|
|
|
456
770
|
}
|
|
457
771
|
}
|
|
458
772
|
|
|
459
|
-
// components/* (
|
|
773
|
+
// components/* (existing)
|
|
460
774
|
const compDirs = await glob("{src/,}components/*/", { cwd: ROOT });
|
|
461
775
|
for (const dir of compDirs) {
|
|
462
776
|
const name = path.basename(dir);
|
|
@@ -467,7 +781,7 @@ async function scanStructure(stack) {
|
|
|
467
781
|
}
|
|
468
782
|
}
|
|
469
783
|
|
|
470
|
-
// ──
|
|
784
|
+
// ── Fallback: extract domains directly from page.tsx/index.tsx locations when scanners return 0 ──
|
|
471
785
|
if (frontendDomains.length === 0) {
|
|
472
786
|
const pageFiles = await glob("**/page.{tsx,jsx}", { cwd: ROOT, ignore: ["**/node_modules/**", "**/.next/**"] });
|
|
473
787
|
const domainSet = {};
|
|
@@ -486,7 +800,7 @@ async function scanStructure(stack) {
|
|
|
486
800
|
}
|
|
487
801
|
}
|
|
488
802
|
}
|
|
489
|
-
// client.tsx
|
|
803
|
+
// Count client.tsx as well
|
|
490
804
|
const clientFiles = await glob("**/client.{tsx,jsx}", { cwd: ROOT, ignore: ["**/node_modules/**", "**/.next/**"] });
|
|
491
805
|
for (const f of clientFiles) {
|
|
492
806
|
const parts = f.replace(/\\/g, "/").split("/");
|
|
@@ -507,7 +821,7 @@ async function scanStructure(stack) {
|
|
|
507
821
|
});
|
|
508
822
|
}
|
|
509
823
|
|
|
510
|
-
// widgets/features/entities
|
|
824
|
+
// Also scan widgets/features/entities directly (glob fallback)
|
|
511
825
|
for (const layer of ["widgets", "features", "entities"]) {
|
|
512
826
|
const layerFiles = await glob(`**/${layer}/*/**/*.{tsx,jsx,ts,js}`, { cwd: ROOT, ignore: ["**/node_modules/**", "**/.next/**", "**/*.spec.*", "**/*.test.*"] });
|
|
513
827
|
const layerDomains = {};
|
|
@@ -537,9 +851,9 @@ async function scanStructure(stack) {
|
|
|
537
851
|
if (dir === "." || dir.includes("venv")) continue;
|
|
538
852
|
const name = path.basename(dir);
|
|
539
853
|
const appFiles = await glob(`${dir}/*.py`, { cwd: ROOT });
|
|
540
|
-
const views = appFiles.filter(
|
|
541
|
-
const models = appFiles.filter(
|
|
542
|
-
const serializers = appFiles.filter(
|
|
854
|
+
const views = appFiles.filter(x => x.includes("views")).length;
|
|
855
|
+
const models = appFiles.filter(x => x.includes("models")).length;
|
|
856
|
+
const serializers = appFiles.filter(x => x.includes("serializers")).length;
|
|
543
857
|
backendDomains.push({ name, type: "backend", views, models, serializers, totalFiles: appFiles.length });
|
|
544
858
|
}
|
|
545
859
|
}
|
|
@@ -567,7 +881,7 @@ async function scanStructure(stack) {
|
|
|
567
881
|
}
|
|
568
882
|
}
|
|
569
883
|
|
|
570
|
-
//
|
|
884
|
+
// Frontend count
|
|
571
885
|
const frontend = { exists: false, components: 0, pages: 0, hooks: 0 };
|
|
572
886
|
if (stack.frontend) {
|
|
573
887
|
frontend.exists = true;
|
|
@@ -576,7 +890,7 @@ async function scanStructure(stack) {
|
|
|
576
890
|
frontend.hooks = (await glob("{src/,}**/hooks/**/*.{ts,js}", { cwd: ROOT, ignore: ["**/node_modules/**"] })).length;
|
|
577
891
|
}
|
|
578
892
|
|
|
579
|
-
// App Router RSC/Client
|
|
893
|
+
// App Router RSC/Client overall stats (for project-analysis.json)
|
|
580
894
|
if (stack.frontend === "nextjs") {
|
|
581
895
|
const allClientFiles = await glob("{app,src/app}/**/client.{tsx,ts,jsx,js}", { cwd: ROOT });
|
|
582
896
|
const allPageFiles = await glob("{app,src/app}/**/page.{tsx,ts,jsx,js}", { cwd: ROOT });
|
|
@@ -587,7 +901,7 @@ async function scanStructure(stack) {
|
|
|
587
901
|
frontend.rscPattern = allClientFiles.length > 0;
|
|
588
902
|
}
|
|
589
903
|
|
|
590
|
-
//
|
|
904
|
+
// All domains = backend + frontend (with type tags)
|
|
591
905
|
const allDomains = [
|
|
592
906
|
...backendDomains.sort((a, b) => b.totalFiles - a.totalFiles),
|
|
593
907
|
...frontendDomains.sort((a, b) => b.totalFiles - a.totalFiles),
|
|
@@ -596,7 +910,201 @@ async function scanStructure(stack) {
|
|
|
596
910
|
return { domains: allDomains, backendDomains, frontendDomains, rootPackage, frontend };
|
|
597
911
|
}
|
|
598
912
|
|
|
599
|
-
// ───
|
|
913
|
+
// ─── Resolve shared query modules ────────────────────────────────
|
|
914
|
+
// When a shared query module (e.g., common-query-server) contains controllers/services
|
|
915
|
+
// for multiple domains, extract the actual domains and merge them into existing entries.
|
|
916
|
+
// Supports two extraction patterns:
|
|
917
|
+
// A) Package-based: .../kotlin/com/company/.../DOMAIN/controller/XxxController.kt
|
|
918
|
+
// B) Class-name-based: .../controller/ReservationQueryController.kt → "reservation"
|
|
919
|
+
// Safety: if no shared modules are found, this function does nothing (no-op).
|
|
920
|
+
function resolveSharedQueryDomains(backendDomains, ktFiles) {
|
|
921
|
+
const genericNames = ["common", "shared", "base", "core", "global"];
|
|
922
|
+
const layerNames = new Set([
|
|
923
|
+
"controller", "service", "repository", "mapper", "api", "handler",
|
|
924
|
+
"dto", "model", "entity", "config", "configuration", "util", "utils",
|
|
925
|
+
"infra", "infrastructure", "framework", "common", "shared", "base",
|
|
926
|
+
"core", "global", "internal", "external", "exception", "interceptor",
|
|
927
|
+
"filter", "converter", "event", "listener", "client", "feign",
|
|
928
|
+
"adapter", "port", "domain", "application", "presentation",
|
|
929
|
+
"persistence", "web", "rest", "grpc", "query", "command",
|
|
930
|
+
"aggregate", "aggregator", "vo", "valueobject",
|
|
931
|
+
]);
|
|
932
|
+
const skipPackages = new Set([
|
|
933
|
+
"com", "org", "net", "io", "kr", "jp", "cn", "de", "fr", "uk", "us",
|
|
934
|
+
"dev", "app", "main", "server", "backend", "project", "kotlin",
|
|
935
|
+
]);
|
|
936
|
+
const classSuffixes = new Set([
|
|
937
|
+
"Controller", "Service", "Repository", "Mapper", "Handler", "Api",
|
|
938
|
+
"Query", "Command", "Read", "Write", "Get", "Find", "List", "Search",
|
|
939
|
+
"Admin", "Dto", "Request", "Response", "Entity", "Model", "Impl",
|
|
940
|
+
"Factory", "Builder", "Adapter", "Client", "Facade", "Provider",
|
|
941
|
+
]);
|
|
942
|
+
|
|
943
|
+
// Find shared modules: generic name + query server type
|
|
944
|
+
const sharedModules = backendDomains.filter(d =>
|
|
945
|
+
d.pattern === "kotlin-multimodule" &&
|
|
946
|
+
d.serverTypes && d.serverTypes.includes("query") &&
|
|
947
|
+
genericNames.some(g => d.name.startsWith(g))
|
|
948
|
+
);
|
|
949
|
+
if (sharedModules.length === 0) return;
|
|
950
|
+
|
|
951
|
+
// Existing domain names for Pattern B-1 matching (longest-first for greedy match)
|
|
952
|
+
const existingDomains = backendDomains
|
|
953
|
+
.filter(d => !sharedModules.includes(d) && d.pattern === "kotlin-multimodule")
|
|
954
|
+
.map(d => d.name)
|
|
955
|
+
.sort((a, b) => b.length - a.length);
|
|
956
|
+
|
|
957
|
+
for (const shared of sharedModules) {
|
|
958
|
+
const moduleNames = shared.modules || [];
|
|
959
|
+
const sharedKtFiles = ktFiles.filter(f =>
|
|
960
|
+
moduleNames.some(m => f.includes(`/${m}/`) || f.startsWith(`${m}/`))
|
|
961
|
+
);
|
|
962
|
+
if (sharedKtFiles.length === 0) continue;
|
|
963
|
+
|
|
964
|
+
const domainFileMap = {}; // { domainName: [filePaths] }
|
|
965
|
+
|
|
966
|
+
for (const filePath of sharedKtFiles) {
|
|
967
|
+
let domain = null;
|
|
968
|
+
const parts = filePath.replace(/\\/g, "/").split("/");
|
|
969
|
+
|
|
970
|
+
// ── Pattern A: Package-based ──
|
|
971
|
+
// Looks for: .../kotlin/com/company/[project/]DOMAIN/LAYER/File.kt
|
|
972
|
+
// Uses depth >= 3 from kotlin/ to skip base package segments
|
|
973
|
+
const kotlinIdx = parts.findIndex(p => p === "kotlin");
|
|
974
|
+
if (kotlinIdx >= 0) {
|
|
975
|
+
for (let i = kotlinIdx + 1; i < parts.length - 1; i++) {
|
|
976
|
+
if (layerNames.has(parts[i].toLowerCase())) {
|
|
977
|
+
if (i - 1 > kotlinIdx) {
|
|
978
|
+
const candidate = parts[i - 1].toLowerCase();
|
|
979
|
+
const depth = (i - 1) - kotlinIdx; // distance from kotlin/
|
|
980
|
+
if (
|
|
981
|
+
depth >= 3 &&
|
|
982
|
+
!layerNames.has(candidate) &&
|
|
983
|
+
!skipPackages.has(candidate) &&
|
|
984
|
+
candidate.length > 2
|
|
985
|
+
) {
|
|
986
|
+
domain = candidate;
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
break; // only check first layer directory encountered
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
// ── Pattern B-1: Match class name against existing domain names ──
|
|
995
|
+
if (!domain) {
|
|
996
|
+
const fileName = parts[parts.length - 1].replace(/\.kt$/, "").toLowerCase();
|
|
997
|
+
for (const ed of existingDomains) {
|
|
998
|
+
const normalized = ed.replace(/-/g, "");
|
|
999
|
+
if (fileName.startsWith(normalized) && fileName.length > normalized.length) {
|
|
1000
|
+
domain = ed;
|
|
1001
|
+
break;
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
// ── Pattern B-2: Extract from PascalCase class name ──
|
|
1007
|
+
if (!domain) {
|
|
1008
|
+
const fileName = parts[parts.length - 1].replace(/\.kt$/, "");
|
|
1009
|
+
const words = fileName.match(/[A-Z][a-z]+|[A-Z]+(?=[A-Z][a-z]|$)/g);
|
|
1010
|
+
if (words && words.length >= 2) {
|
|
1011
|
+
const domainWords = [];
|
|
1012
|
+
for (const w of words) {
|
|
1013
|
+
if (classSuffixes.has(w)) break;
|
|
1014
|
+
domainWords.push(w);
|
|
1015
|
+
}
|
|
1016
|
+
if (domainWords.length > 0) {
|
|
1017
|
+
const extracted = domainWords.join("").toLowerCase();
|
|
1018
|
+
if (
|
|
1019
|
+
!genericNames.includes(extracted) &&
|
|
1020
|
+
!skipPackages.has(extracted) &&
|
|
1021
|
+
extracted.length > 2
|
|
1022
|
+
) {
|
|
1023
|
+
domain = extracted;
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
if (domain) {
|
|
1030
|
+
if (!domainFileMap[domain]) domainFileMap[domain] = [];
|
|
1031
|
+
domainFileMap[domain].push(filePath);
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
// ── Merge extracted domains into backendDomains ──
|
|
1036
|
+
let distributedFiles = 0;
|
|
1037
|
+
for (const [domain, files] of Object.entries(domainFileMap)) {
|
|
1038
|
+
// Find matching existing domain (exact or normalized)
|
|
1039
|
+
const existing = backendDomains.find(d =>
|
|
1040
|
+
d !== shared &&
|
|
1041
|
+
(d.name === domain || d.name.replace(/-/g, "") === domain.replace(/-/g, ""))
|
|
1042
|
+
);
|
|
1043
|
+
|
|
1044
|
+
const ctrlCount = files.filter(f =>
|
|
1045
|
+
/\/controller\//i.test(f) || /Controller\.kt$/i.test(f)
|
|
1046
|
+
).length;
|
|
1047
|
+
const svcCount = files.filter(f =>
|
|
1048
|
+
/\/service\//i.test(f) || /Service\.kt$/i.test(f)
|
|
1049
|
+
).length;
|
|
1050
|
+
const repoCount = files.filter(f =>
|
|
1051
|
+
/\/repository\//i.test(f) || /\/mapper\//i.test(f) ||
|
|
1052
|
+
/Repository\.kt$/i.test(f) || /Mapper\.kt$/i.test(f)
|
|
1053
|
+
).length;
|
|
1054
|
+
const dtoCount = files.filter(f =>
|
|
1055
|
+
(/\/dto\//i.test(f) || /\/vo\//i.test(f) || /Dto\.kt$/i.test(f) ||
|
|
1056
|
+
/Vo\.kt$/i.test(f) || /Request\.kt$/i.test(f) || /Response\.kt$/i.test(f)) &&
|
|
1057
|
+
!/\/controller\//i.test(f) && !/\/service\//i.test(f) &&
|
|
1058
|
+
!/\/repository\//i.test(f)
|
|
1059
|
+
).length;
|
|
1060
|
+
|
|
1061
|
+
if (existing) {
|
|
1062
|
+
// Merge into existing domain
|
|
1063
|
+
if (!existing.modules) existing.modules = [];
|
|
1064
|
+
for (const m of moduleNames) {
|
|
1065
|
+
if (!existing.modules.includes(m)) existing.modules.push(m);
|
|
1066
|
+
}
|
|
1067
|
+
if (!existing.serverTypes) existing.serverTypes = [];
|
|
1068
|
+
if (!existing.serverTypes.includes("query")) existing.serverTypes.push("query");
|
|
1069
|
+
existing.controllers += ctrlCount;
|
|
1070
|
+
existing.services += svcCount;
|
|
1071
|
+
if (existing.mappers != null) existing.mappers += repoCount;
|
|
1072
|
+
existing.dtos += dtoCount;
|
|
1073
|
+
existing.totalFiles += files.length;
|
|
1074
|
+
} else {
|
|
1075
|
+
// Create new domain entry
|
|
1076
|
+
backendDomains.push({
|
|
1077
|
+
name: domain,
|
|
1078
|
+
type: "backend",
|
|
1079
|
+
controllers: ctrlCount,
|
|
1080
|
+
services: svcCount,
|
|
1081
|
+
mappers: repoCount,
|
|
1082
|
+
dtos: dtoCount,
|
|
1083
|
+
totalFiles: files.length,
|
|
1084
|
+
modules: [...moduleNames],
|
|
1085
|
+
serverTypes: ["query"],
|
|
1086
|
+
pattern: "kotlin-multimodule",
|
|
1087
|
+
resolvedFrom: shared.name,
|
|
1088
|
+
});
|
|
1089
|
+
}
|
|
1090
|
+
distributedFiles += files.length;
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
// Adjust shared module: keep only undistributed files
|
|
1094
|
+
if (distributedFiles > 0) {
|
|
1095
|
+
shared.totalFiles = Math.max(0, shared.totalFiles - distributedFiles);
|
|
1096
|
+
shared.resolvedDomains = Object.keys(domainFileMap);
|
|
1097
|
+
if (shared.totalFiles === 0) shared._resolved = true;
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
// Remove fully resolved shared entries (all files distributed)
|
|
1102
|
+
for (let i = backendDomains.length - 1; i >= 0; i--) {
|
|
1103
|
+
if (backendDomains[i]._resolved) backendDomains.splice(i, 1);
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
// ─── Domain group splitting (per type) ──────────────────────────
|
|
600
1108
|
function splitDomainGroups(domains, type, template) {
|
|
601
1109
|
const MAX_FILES_PER_GROUP = 40;
|
|
602
1110
|
const MAX_DOMAINS_PER_GROUP = 4;
|
|
@@ -620,7 +1128,7 @@ function splitDomainGroups(domains, type, template) {
|
|
|
620
1128
|
return groups;
|
|
621
1129
|
}
|
|
622
1130
|
|
|
623
|
-
// ───
|
|
1131
|
+
// ─── Determine active domains ───────────────────────────────────
|
|
624
1132
|
function determineActiveDomains(stack) {
|
|
625
1133
|
const isBackend = stack.framework && !["nextjs", "react", "vue"].includes(stack.framework);
|
|
626
1134
|
return {
|
|
@@ -634,24 +1142,25 @@ function determineActiveDomains(stack) {
|
|
|
634
1142
|
};
|
|
635
1143
|
}
|
|
636
1144
|
|
|
637
|
-
// ───
|
|
1145
|
+
// ─── Template selection (multi-stack) ──────────────────────────────
|
|
638
1146
|
function selectTemplates(stack) {
|
|
639
1147
|
const templates = { backend: null, frontend: null };
|
|
640
1148
|
|
|
641
|
-
//
|
|
642
|
-
if (stack.language === "
|
|
1149
|
+
// Backend template
|
|
1150
|
+
if (stack.language === "kotlin") templates.backend = "kotlin-spring";
|
|
1151
|
+
else if (stack.language === "java") templates.backend = "java-spring";
|
|
643
1152
|
else if (stack.framework === "express" || stack.framework === "nestjs") templates.backend = "node-express";
|
|
644
1153
|
else if (stack.framework === "django") templates.backend = "python-django";
|
|
645
1154
|
else if (stack.framework === "fastapi" || stack.framework === "flask") templates.backend = "python-fastapi";
|
|
646
1155
|
else if (stack.language === "typescript" || stack.language === "javascript") templates.backend = "node-express";
|
|
647
1156
|
else if (stack.language === "python") templates.backend = "python-fastapi";
|
|
648
1157
|
|
|
649
|
-
//
|
|
1158
|
+
// Frontend template
|
|
650
1159
|
if (stack.frontend === "nextjs" || stack.frontend === "react" || stack.frontend === "vue") {
|
|
651
1160
|
templates.frontend = "node-nextjs";
|
|
652
1161
|
}
|
|
653
1162
|
|
|
654
|
-
//
|
|
1163
|
+
// Pure frontend project with no backend
|
|
655
1164
|
if (!templates.backend && templates.frontend) {
|
|
656
1165
|
templates.backend = null;
|
|
657
1166
|
}
|
|
@@ -659,28 +1168,43 @@ function selectTemplates(stack) {
|
|
|
659
1168
|
return templates;
|
|
660
1169
|
}
|
|
661
1170
|
|
|
662
|
-
// ───
|
|
663
|
-
function generatePrompts(templates) {
|
|
1171
|
+
// ─── Dynamic prompt generation (multi-stack) ──────────────────────
|
|
1172
|
+
function generatePrompts(templates, lang) {
|
|
664
1173
|
const commonDir = path.join(TEMPLATES_DIR, "common");
|
|
665
1174
|
const headerPath = path.join(commonDir, "header.md");
|
|
666
1175
|
const footerPath = path.join(commonDir, "pass3-footer.md");
|
|
1176
|
+
const langPath = path.join(commonDir, "lang-instructions.json");
|
|
667
1177
|
|
|
668
1178
|
const header = fs.existsSync(headerPath) ? fs.readFileSync(headerPath, "utf-8") : "";
|
|
669
1179
|
const footer = fs.existsSync(footerPath) ? fs.readFileSync(footerPath, "utf-8") : "";
|
|
670
1180
|
|
|
1181
|
+
// Load language instruction for Pass 3 (analysis passes stay English)
|
|
1182
|
+
let langInstruction = "";
|
|
1183
|
+
if (lang && lang !== "en" && fs.existsSync(langPath)) {
|
|
1184
|
+
try {
|
|
1185
|
+
const langData = JSON.parse(fs.readFileSync(langPath, "utf-8"));
|
|
1186
|
+
langInstruction = langData.instructions[lang] || "";
|
|
1187
|
+
if (langInstruction) {
|
|
1188
|
+
console.log(` 🌐 Language: ${langData.labels[lang]} (Pass 3 output)`);
|
|
1189
|
+
}
|
|
1190
|
+
} catch (e) {
|
|
1191
|
+
console.warn(` ⚠️ lang-instructions.json parse error: ${e.message}`);
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
|
|
671
1195
|
function readTemplate(templateName, passName) {
|
|
672
1196
|
const src = path.join(TEMPLATES_DIR, templateName, `${passName}.md`);
|
|
673
1197
|
if (!fs.existsSync(src)) return null;
|
|
674
1198
|
let body = fs.readFileSync(src, "utf-8");
|
|
675
|
-
body = body.replace(
|
|
676
|
-
body = body.replace(/\
|
|
1199
|
+
body = body.replace(/^Project root path:.*\nInterpret all file paths.*\n\n?---\n\n?/s, "");
|
|
1200
|
+
body = body.replace(/\nAfter completion, run the following commands in order:[\s\S]*$/m, "");
|
|
677
1201
|
return body;
|
|
678
1202
|
}
|
|
679
1203
|
|
|
680
1204
|
const activeTemplates = [templates.backend, templates.frontend].filter(Boolean);
|
|
681
1205
|
const primaryTemplate = templates.backend || templates.frontend;
|
|
682
1206
|
|
|
683
|
-
// ─── Pass 1:
|
|
1207
|
+
// ─── Pass 1: Separate prompts per type ──────────────────────
|
|
684
1208
|
for (const tmpl of activeTemplates) {
|
|
685
1209
|
const type = tmpl === templates.frontend ? "frontend" : "backend";
|
|
686
1210
|
const body = readTemplate(tmpl, "pass1");
|
|
@@ -691,7 +1215,7 @@ function generatePrompts(templates) {
|
|
|
691
1215
|
}
|
|
692
1216
|
}
|
|
693
1217
|
|
|
694
|
-
//
|
|
1218
|
+
// Single-stack backward compat: also generate pass1-prompt.md (primary-based)
|
|
695
1219
|
if (primaryTemplate) {
|
|
696
1220
|
const body = readTemplate(primaryTemplate, "pass1");
|
|
697
1221
|
if (body) {
|
|
@@ -699,7 +1223,7 @@ function generatePrompts(templates) {
|
|
|
699
1223
|
}
|
|
700
1224
|
}
|
|
701
1225
|
|
|
702
|
-
// ─── Pass 2:
|
|
1226
|
+
// ─── Pass 2: Single prompt (primary-based, merges all pass1 results) ──
|
|
703
1227
|
if (primaryTemplate) {
|
|
704
1228
|
const body = readTemplate(primaryTemplate, "pass2");
|
|
705
1229
|
if (body) {
|
|
@@ -708,20 +1232,20 @@ function generatePrompts(templates) {
|
|
|
708
1232
|
}
|
|
709
1233
|
}
|
|
710
1234
|
|
|
711
|
-
// ─── Pass 3:
|
|
1235
|
+
// ─── Pass 3: Combined prompt (backend + frontend merged) ──
|
|
712
1236
|
if (primaryTemplate) {
|
|
713
1237
|
const primaryBody = readTemplate(primaryTemplate, "pass3");
|
|
714
1238
|
let combinedBody = primaryBody || "";
|
|
715
1239
|
|
|
716
|
-
//
|
|
1240
|
+
// Multi-stack: merge frontend-specific sections from frontend pass3 into backend pass3
|
|
717
1241
|
if (templates.backend && templates.frontend && templates.backend !== templates.frontend) {
|
|
718
1242
|
const frontendBody = readTemplate(templates.frontend, "pass3");
|
|
719
1243
|
if (frontendBody) {
|
|
720
1244
|
combinedBody += "\n\n---\n\n";
|
|
721
|
-
combinedBody += "#
|
|
722
|
-
combinedBody += "
|
|
723
|
-
combinedBody += "pass2-merged.json
|
|
724
|
-
//
|
|
1245
|
+
combinedBody += "# Additional: Frontend generation targets (auto-detected)\n\n";
|
|
1246
|
+
combinedBody += "In addition to the backend standards above, also generate the following frontend standards.\n";
|
|
1247
|
+
combinedBody += "Reference the frontend analysis results in pass2-merged.json.\n\n";
|
|
1248
|
+
// Extract only standard sections from frontend pass3 (exclude CLAUDE.md, guide duplicates)
|
|
725
1249
|
const frontendSections = frontendBody
|
|
726
1250
|
.split(/\n(?=\d+\.\s)/)
|
|
727
1251
|
.filter(s => /frontend|component|page|routing|data.fetch|state|styling/i.test(s))
|
|
@@ -733,12 +1257,12 @@ function generatePrompts(templates) {
|
|
|
733
1257
|
}
|
|
734
1258
|
|
|
735
1259
|
const dst = path.join(GENERATED_DIR, "pass3-prompt.md");
|
|
736
|
-
fs.writeFileSync(dst, header + combinedBody.trimEnd() + "\n" + footer);
|
|
1260
|
+
fs.writeFileSync(dst, header + langInstruction + combinedBody.trimEnd() + "\n" + footer);
|
|
737
1261
|
console.log(` ✅ pass3-prompt.md${templates.frontend && templates.backend ? " (multi-stack combined)" : ""}`);
|
|
738
1262
|
}
|
|
739
1263
|
}
|
|
740
1264
|
|
|
741
|
-
// ───
|
|
1265
|
+
// ─── Main ───────────────────────────────────────────────
|
|
742
1266
|
async function main() {
|
|
743
1267
|
console.log("\n╔══════════════════════════════════════╗");
|
|
744
1268
|
console.log("║ ClaudeOS-Core — Plan Installer ║");
|
|
@@ -746,7 +1270,7 @@ async function main() {
|
|
|
746
1270
|
|
|
747
1271
|
ensureDir(GENERATED_DIR);
|
|
748
1272
|
|
|
749
|
-
// Phase 1:
|
|
1273
|
+
// Phase 1: Stack detection
|
|
750
1274
|
console.log(" [Phase 1] Detecting stack...");
|
|
751
1275
|
const stack = await detectStack();
|
|
752
1276
|
console.log(` Language: ${stack.language || "unknown"} ${stack.languageVersion || ""}`);
|
|
@@ -756,7 +1280,7 @@ async function main() {
|
|
|
756
1280
|
console.log(` ORM: ${stack.orm || "none"}`);
|
|
757
1281
|
console.log(` PackageMgr: ${stack.packageManager || "none"}\n`);
|
|
758
1282
|
|
|
759
|
-
// Phase 2:
|
|
1283
|
+
// Phase 2: Structure scan
|
|
760
1284
|
console.log(" [Phase 2] Scanning structure...");
|
|
761
1285
|
const { domains, backendDomains, frontendDomains, rootPackage, frontend } = await scanStructure(stack);
|
|
762
1286
|
console.log(` Backend: ${backendDomains.length} domains`);
|
|
@@ -766,7 +1290,7 @@ async function main() {
|
|
|
766
1290
|
if (frontend.exists) console.log(` Components: ${frontend.components} components, ${frontend.pages} pages, ${frontend.hooks} hooks`);
|
|
767
1291
|
console.log();
|
|
768
1292
|
|
|
769
|
-
// Phase 3:
|
|
1293
|
+
// Phase 3: Template selection (multi-stack)
|
|
770
1294
|
console.log(" [Phase 3] Selecting templates...");
|
|
771
1295
|
const templates = selectTemplates(stack);
|
|
772
1296
|
const isMultiStack = !!(templates.backend && templates.frontend);
|
|
@@ -776,7 +1300,7 @@ async function main() {
|
|
|
776
1300
|
else console.log(` Mode: Single-stack`);
|
|
777
1301
|
console.log();
|
|
778
1302
|
|
|
779
|
-
// Phase 4:
|
|
1303
|
+
// Phase 4: Domain group splitting (per type)
|
|
780
1304
|
console.log(" [Phase 4] Splitting domain groups...");
|
|
781
1305
|
const allGroups = [];
|
|
782
1306
|
if (templates.backend && backendDomains.length > 0) {
|
|
@@ -787,7 +1311,7 @@ async function main() {
|
|
|
787
1311
|
const fg = splitDomainGroups(frontendDomains, "frontend", templates.frontend);
|
|
788
1312
|
allGroups.push(...fg);
|
|
789
1313
|
}
|
|
790
|
-
// passNum
|
|
1314
|
+
// Reassign passNum
|
|
791
1315
|
allGroups.forEach((g, i) => { g.passNum = i + 1; });
|
|
792
1316
|
allGroups.forEach((g, i) => {
|
|
793
1317
|
const icon = g.type === "backend" ? "⚙️" : "🎨";
|
|
@@ -795,7 +1319,7 @@ async function main() {
|
|
|
795
1319
|
});
|
|
796
1320
|
console.log();
|
|
797
1321
|
|
|
798
|
-
// Phase 5:
|
|
1322
|
+
// Phase 5: Active domains
|
|
799
1323
|
console.log(" [Phase 5] Active domains...");
|
|
800
1324
|
const active = determineActiveDomains(stack);
|
|
801
1325
|
Object.entries(active).forEach(([k, v]) => {
|
|
@@ -803,15 +1327,17 @@ async function main() {
|
|
|
803
1327
|
});
|
|
804
1328
|
console.log();
|
|
805
1329
|
|
|
806
|
-
// Phase 6:
|
|
807
|
-
|
|
808
|
-
|
|
1330
|
+
// Phase 6: Prompt generation (multi-stack)
|
|
1331
|
+
const lang = process.env.CLAUDEOS_LANG || "en";
|
|
1332
|
+
console.log(` [Phase 6] Generating prompts (lang: ${lang})...`);
|
|
1333
|
+
generatePrompts(templates, lang);
|
|
809
1334
|
console.log();
|
|
810
1335
|
|
|
811
|
-
//
|
|
1336
|
+
// Save: project-analysis.json
|
|
812
1337
|
const defaultPort = stack.framework === "fastapi" || stack.framework === "django" ? 8000 : 8080;
|
|
813
1338
|
const analysis = {
|
|
814
1339
|
analyzedAt: new Date().toISOString(),
|
|
1340
|
+
lang,
|
|
815
1341
|
stack: { ...stack, port: stack.port || defaultPort },
|
|
816
1342
|
templates,
|
|
817
1343
|
isMultiStack,
|
|
@@ -831,7 +1357,7 @@ async function main() {
|
|
|
831
1357
|
fs.writeFileSync(path.join(GENERATED_DIR, "project-analysis.json"), JSON.stringify(analysis, null, 2));
|
|
832
1358
|
console.log(" 💾 project-analysis.json saved");
|
|
833
1359
|
|
|
834
|
-
//
|
|
1360
|
+
// Save: domain-groups.json
|
|
835
1361
|
const domainGroups = {
|
|
836
1362
|
generatedAt: new Date().toISOString(),
|
|
837
1363
|
isMultiStack,
|