claudeos-core 1.2.4 → 1.3.1
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 +76 -0
- package/README.de.md +50 -10
- package/README.es.md +51 -10
- package/README.fr.md +51 -10
- package/README.hi.md +51 -10
- package/README.ja.md +52 -10
- package/README.ko.md +51 -10
- package/README.md +62 -15
- package/README.ru.md +51 -10
- package/README.vi.md +51 -10
- package/README.zh-CN.md +51 -10
- package/bin/cli.js +171 -36
- package/bootstrap.sh +71 -23
- package/content-validator/index.js +16 -13
- package/health-checker/index.js +4 -3
- package/lib/safe-fs.js +110 -0
- package/manifest-generator/index.js +13 -7
- package/package.json +4 -2
- package/pass-json-validator/index.js +3 -5
- package/pass-prompts/templates/java-spring/pass1.md +4 -1
- package/pass-prompts/templates/java-spring/pass2.md +3 -3
- package/pass-prompts/templates/java-spring/pass3.md +42 -5
- package/pass-prompts/templates/kotlin-spring/pass1.md +4 -1
- package/pass-prompts/templates/kotlin-spring/pass2.md +5 -5
- package/pass-prompts/templates/kotlin-spring/pass3.md +42 -5
- package/pass-prompts/templates/node-express/pass1.md +4 -1
- package/pass-prompts/templates/node-express/pass2.md +4 -1
- package/pass-prompts/templates/node-express/pass3.md +44 -6
- package/pass-prompts/templates/node-nextjs/pass1.md +14 -4
- package/pass-prompts/templates/node-nextjs/pass2.md +6 -4
- package/pass-prompts/templates/node-nextjs/pass3.md +45 -6
- package/pass-prompts/templates/python-django/pass1.md +4 -2
- package/pass-prompts/templates/python-django/pass2.md +4 -4
- package/pass-prompts/templates/python-django/pass3.md +42 -5
- package/pass-prompts/templates/python-fastapi/pass1.md +4 -1
- package/pass-prompts/templates/python-fastapi/pass2.md +4 -4
- package/pass-prompts/templates/python-fastapi/pass3.md +42 -5
- package/plan-installer/domain-grouper.js +74 -0
- package/plan-installer/index.js +35 -1305
- package/plan-installer/prompt-generator.js +94 -0
- package/plan-installer/stack-detector.js +326 -0
- package/plan-installer/structure-scanner.js +783 -0
- package/plan-validator/index.js +84 -20
- package/sync-checker/index.js +7 -3
package/plan-installer/index.js
CHANGED
|
@@ -1,1279 +1,36 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* ClaudeOS-Core — plan-installer
|
|
4
|
+
* ClaudeOS-Core — plan-installer (orchestrator)
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* -
|
|
9
|
-
* -
|
|
10
|
-
* -
|
|
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
|
-
* - claudeos-core/generated/pass2-prompt.md
|
|
14
|
-
* - claudeos-core/generated/pass3-prompt.md (combined prompt for multi-stack)
|
|
15
|
-
*
|
|
16
|
-
* Usage: npx claudeos-core <cmd> or node claudeos-core-tools/plan-installer/index.js
|
|
6
|
+
* Modules:
|
|
7
|
+
* - stack-detector.js — detectStack()
|
|
8
|
+
* - structure-scanner.js — scanStructure(), resolveSharedQueryDomains()
|
|
9
|
+
* - domain-grouper.js — splitDomainGroups(), determineActiveDomains(), selectTemplates()
|
|
10
|
+
* - prompt-generator.js — generatePrompts()
|
|
17
11
|
*/
|
|
18
12
|
|
|
19
|
-
const fs = require("fs");
|
|
20
13
|
const path = require("path");
|
|
21
|
-
const {
|
|
14
|
+
const { ensureDir, writeFileSafe } = require("../lib/safe-fs");
|
|
15
|
+
const { detectStack } = require("./stack-detector");
|
|
16
|
+
const { scanStructure } = require("./structure-scanner");
|
|
17
|
+
const { splitDomainGroups, determineActiveDomains, selectTemplates } = require("./domain-grouper");
|
|
18
|
+
const { generatePrompts } = require("./prompt-generator");
|
|
22
19
|
|
|
23
20
|
const ROOT = process.env.CLAUDEOS_ROOT || path.resolve(__dirname, "../..");
|
|
24
21
|
const GENERATED_DIR = path.join(ROOT, "claudeos-core/generated");
|
|
25
22
|
const TEMPLATES_DIR = path.join(__dirname, "../pass-prompts/templates");
|
|
26
23
|
|
|
27
|
-
function ensureDir(dir) {
|
|
28
|
-
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
// ─── Stack detection (multi-stack) ────────────────────────────────
|
|
32
|
-
async function detectStack() {
|
|
33
|
-
const stack = {
|
|
34
|
-
language: null, languageVersion: null,
|
|
35
|
-
framework: null, frameworkVersion: null,
|
|
36
|
-
buildTool: null, database: null, orm: null,
|
|
37
|
-
frontend: null, frontendVersion: null,
|
|
38
|
-
packageManager: null, detected: [],
|
|
39
|
-
};
|
|
40
|
-
|
|
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);
|
|
48
|
-
if (g.includes("spring-boot")) { stack.language = "java"; stack.framework = "spring-boot"; stack.detected.push("spring-boot"); }
|
|
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
|
-
}
|
|
58
|
-
const jv = g.match(/sourceCompatibility\s*=\s*['"]?(\d+)['"]?/);
|
|
59
|
-
if (jv) stack.languageVersion = jv[1];
|
|
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
|
-
}
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
// ── Java: Maven ──
|
|
175
|
-
if (fs.existsSync(path.join(ROOT, "pom.xml"))) {
|
|
176
|
-
const pom = fs.readFileSync(path.join(ROOT, "pom.xml"), "utf-8");
|
|
177
|
-
if (!stack.buildTool) { stack.buildTool = "maven"; stack.language = "java"; stack.detected.push("pom.xml"); }
|
|
178
|
-
const sv = pom.match(/<spring-boot[^>]*version>([^<]+)/);
|
|
179
|
-
if (sv) stack.frameworkVersion = sv[1];
|
|
180
|
-
const jv = pom.match(/<java\.version>(\d+)/);
|
|
181
|
-
if (jv) stack.languageVersion = jv[1];
|
|
182
|
-
if (pom.includes("spring-boot") && !stack.framework) { stack.framework = "spring-boot"; stack.detected.push("spring-boot"); }
|
|
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";
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// ── Node.js ──
|
|
196
|
-
if (fs.existsSync(path.join(ROOT, "package.json"))) {
|
|
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) {
|
|
201
|
-
stack.detected.push("package.json");
|
|
202
|
-
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
203
|
-
|
|
204
|
-
if (!stack.language) stack.language = deps.typescript ? "typescript" : "javascript";
|
|
205
|
-
if (deps.typescript) { stack.detected.push("typescript"); stack.languageVersion = deps.typescript.replace(/[^0-9.]/g, ""); }
|
|
206
|
-
|
|
207
|
-
// Frontend
|
|
208
|
-
if (deps.next) { stack.frontend = "nextjs"; stack.detected.push("next.js"); stack.frontendVersion = deps.next.replace(/[^0-9.]/g, ""); }
|
|
209
|
-
else if (deps.react) { stack.frontend = "react"; stack.detected.push("react"); stack.frontendVersion = deps.react.replace(/[^0-9.]/g, ""); }
|
|
210
|
-
else if (deps.vue) { stack.frontend = "vue"; stack.detected.push("vue"); stack.frontendVersion = deps.vue.replace(/[^0-9.]/g, ""); }
|
|
211
|
-
|
|
212
|
-
// Backend framework
|
|
213
|
-
if (deps.express && !stack.framework) { stack.framework = "express"; stack.detected.push("express"); }
|
|
214
|
-
if (deps["@nestjs/core"]) { stack.framework = "nestjs"; stack.detected.push("nestjs"); stack.frameworkVersion = deps["@nestjs/core"].replace(/[^0-9.]/g, ""); }
|
|
215
|
-
|
|
216
|
-
// ORM
|
|
217
|
-
if ((deps["@prisma/client"] || deps.prisma) && !stack.orm) { stack.orm = "prisma"; stack.detected.push("prisma"); }
|
|
218
|
-
if (deps.typeorm && !stack.orm) { stack.orm = "typeorm"; stack.detected.push("typeorm"); }
|
|
219
|
-
if (deps.sequelize && !stack.orm) { stack.orm = "sequelize"; stack.detected.push("sequelize"); }
|
|
220
|
-
if (deps["drizzle-orm"] && !stack.orm) { stack.orm = "drizzle"; stack.detected.push("drizzle"); }
|
|
221
|
-
if (deps.knex && !stack.orm) { stack.orm = "knex"; stack.detected.push("knex"); }
|
|
222
|
-
if (deps.mongoose) { if (!stack.database) stack.database = "mongodb"; if (!stack.orm) stack.orm = "mongoose"; stack.detected.push("mongoose"); }
|
|
223
|
-
|
|
224
|
-
// DB
|
|
225
|
-
if (deps.pg && !stack.database) stack.database = "postgresql";
|
|
226
|
-
if (deps.mysql2 && !stack.database) stack.database = "mysql";
|
|
227
|
-
if (deps.mongodb && !stack.database) stack.database = "mongodb";
|
|
228
|
-
|
|
229
|
-
// Package manager
|
|
230
|
-
stack.packageManager = fs.existsSync(path.join(ROOT, "pnpm-lock.yaml")) ? "pnpm"
|
|
231
|
-
: fs.existsSync(path.join(ROOT, "yarn.lock")) ? "yarn" : "npm";
|
|
232
|
-
|
|
233
|
-
if (pkg.engines && pkg.engines.node && !stack.languageVersion) stack.languageVersion = pkg.engines.node;
|
|
234
|
-
} // end if (pkg)
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
// ── Python ──
|
|
238
|
-
const hasPyproject = fs.existsSync(path.join(ROOT, "pyproject.toml"));
|
|
239
|
-
const hasRequirements = fs.existsSync(path.join(ROOT, "requirements.txt"));
|
|
240
|
-
if (hasPyproject || hasRequirements) {
|
|
241
|
-
if (!stack.language) stack.language = "python";
|
|
242
|
-
stack.detected.push("python");
|
|
243
|
-
|
|
244
|
-
if (hasPyproject) {
|
|
245
|
-
const pp = fs.readFileSync(path.join(ROOT, "pyproject.toml"), "utf-8");
|
|
246
|
-
const pv = pp.match(/python\s*=\s*"[><=^~]*(\d+\.\d+)/);
|
|
247
|
-
if (pv && !stack.languageVersion) stack.languageVersion = pv[1];
|
|
248
|
-
if (pp.includes("django") && !stack.framework) { stack.framework = "django"; stack.detected.push("django"); }
|
|
249
|
-
if (pp.includes("fastapi") && !stack.framework) { stack.framework = "fastapi"; stack.detected.push("fastapi"); }
|
|
250
|
-
if (pp.includes("flask") && !stack.framework) { stack.framework = "flask"; stack.detected.push("flask"); }
|
|
251
|
-
if (pp.includes("sqlalchemy") && !stack.orm) { stack.orm = "sqlalchemy"; stack.detected.push("sqlalchemy"); }
|
|
252
|
-
if (pp.includes("tortoise") && !stack.orm) { stack.orm = "tortoise-orm"; stack.detected.push("tortoise-orm"); }
|
|
253
|
-
if (pp.includes("poetry")) { stack.packageManager = "poetry"; }
|
|
254
|
-
if (pp.includes("pdm")) { stack.packageManager = "pdm"; }
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
if (hasRequirements) {
|
|
258
|
-
const r = fs.readFileSync(path.join(ROOT, "requirements.txt"), "utf-8");
|
|
259
|
-
if (r.includes("django") && !stack.framework) { stack.framework = "django"; stack.detected.push("django"); }
|
|
260
|
-
if (r.includes("fastapi") && !stack.framework) { stack.framework = "fastapi"; stack.detected.push("fastapi"); }
|
|
261
|
-
if (r.includes("flask") && !stack.framework) { stack.framework = "flask"; stack.detected.push("flask"); }
|
|
262
|
-
if (r.includes("sqlalchemy") && !stack.orm) { stack.orm = "sqlalchemy"; }
|
|
263
|
-
if (r.includes("tortoise") && !stack.orm) { stack.orm = "tortoise-orm"; }
|
|
264
|
-
if (r.includes("psycopg") && !stack.database) stack.database = "postgresql";
|
|
265
|
-
if (r.includes("mysqlclient") && !stack.database) stack.database = "mysql";
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
if (!stack.packageManager) {
|
|
269
|
-
stack.packageManager = fs.existsSync(path.join(ROOT, "Pipfile")) ? "pipenv"
|
|
270
|
-
: fs.existsSync(path.join(ROOT, "poetry.lock")) ? "poetry" : "pip";
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
// ── DB from config files ──
|
|
275
|
-
const ymls = await glob("**/application*.yml", { cwd: ROOT, absolute: true, ignore: ["**/node_modules/**"] });
|
|
276
|
-
for (const y of ymls) {
|
|
277
|
-
const c = fs.readFileSync(y, "utf-8");
|
|
278
|
-
if (!stack.database && c.includes("postgresql")) stack.database = "postgresql";
|
|
279
|
-
if (!stack.database && c.includes("mysql")) stack.database = "mysql";
|
|
280
|
-
if (!stack.database && c.includes("oracle")) stack.database = "oracle";
|
|
281
|
-
if (!stack.database && c.includes("mongodb")) stack.database = "mongodb";
|
|
282
|
-
if (!stack.database && /\bh2\b/.test(c)) stack.database = "h2";
|
|
283
|
-
if (!stack.database && c.includes("sqlite")) stack.database = "sqlite";
|
|
284
|
-
if (!stack.port) {
|
|
285
|
-
const pm = c.match(/server:\s*\n\s*port:\s*(\d+)/) || c.match(/server\.port\s*[=:]\s*(\d+)/);
|
|
286
|
-
if (pm) stack.port = parseInt(pm[1]);
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
// .env
|
|
291
|
-
for (const ef of [".env", ".env.local", ".env.development"]) {
|
|
292
|
-
const ep = path.join(ROOT, ef);
|
|
293
|
-
if (fs.existsSync(ep)) {
|
|
294
|
-
const ec = fs.readFileSync(ep, "utf-8");
|
|
295
|
-
if (ec.includes("DATABASE_URL")) {
|
|
296
|
-
if (ec.includes("postgres") && !stack.database) stack.database = "postgresql";
|
|
297
|
-
if (ec.includes("mysql") && !stack.database) stack.database = "mysql";
|
|
298
|
-
if (ec.includes("mongodb") && !stack.database) stack.database = "mongodb";
|
|
299
|
-
if (ec.includes("sqlite") && !stack.database) stack.database = "sqlite";
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
// Prisma schema
|
|
305
|
-
const prismaSchema = path.join(ROOT, "prisma/schema.prisma");
|
|
306
|
-
if (fs.existsSync(prismaSchema)) {
|
|
307
|
-
const ps = fs.readFileSync(prismaSchema, "utf-8");
|
|
308
|
-
const prov = ps.match(/provider\s*=\s*"(\w+)"/);
|
|
309
|
-
if (prov && !stack.database) {
|
|
310
|
-
const db = { postgresql: "postgresql", mysql: "mysql", sqlite: "sqlite", mongodb: "mongodb" };
|
|
311
|
-
if (db[prov[1]]) stack.database = db[prov[1]];
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
// ── Config file fallback (monorepo: detect from config files when not in package.json) ──
|
|
316
|
-
if (!stack.frontend) {
|
|
317
|
-
const nextConfigs = ["next.config.js", "next.config.mjs", "next.config.ts"];
|
|
318
|
-
if (nextConfigs.some(c => fs.existsSync(path.join(ROOT, c)))) {
|
|
319
|
-
stack.frontend = "nextjs"; stack.detected.push("next.config (fallback)");
|
|
320
|
-
if (!stack.language) stack.language = "typescript";
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
if (!stack.frontend) {
|
|
324
|
-
if (fs.existsSync(path.join(ROOT, "vite.config.ts")) || fs.existsSync(path.join(ROOT, "vite.config.js"))) {
|
|
325
|
-
if (!stack.frontend) { stack.frontend = "react"; stack.detected.push("vite.config (fallback)"); }
|
|
326
|
-
if (!stack.language) stack.language = "typescript";
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
if (!stack.frontend) {
|
|
330
|
-
if (fs.existsSync(path.join(ROOT, "nuxt.config.ts")) || fs.existsSync(path.join(ROOT, "nuxt.config.js"))) {
|
|
331
|
-
stack.frontend = "vue"; stack.detected.push("nuxt.config (fallback)");
|
|
332
|
-
if (!stack.language) stack.language = "typescript";
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
return stack;
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
// ─── Structure scan (multi-stack) ────────────────────────────────
|
|
340
|
-
async function scanStructure(stack) {
|
|
341
|
-
const backendDomains = [];
|
|
342
|
-
const frontendDomains = [];
|
|
343
|
-
let rootPackage = null;
|
|
344
|
-
|
|
345
|
-
// ── Java ──
|
|
346
|
-
if (stack.language === "java") {
|
|
347
|
-
const javaFiles = await glob("src/main/java/**/*.java", { cwd: ROOT });
|
|
348
|
-
for (const f of javaFiles) {
|
|
349
|
-
const m = f.match(/src\/main\/java\/(.+?)\/(controller|service|mapper|dto|entity|repository|adapter)/);
|
|
350
|
-
if (m) { rootPackage = m[1].replace(/\//g, "."); break; }
|
|
351
|
-
}
|
|
352
|
-
const domainMap = {};
|
|
353
|
-
let detectedPattern = null;
|
|
354
|
-
|
|
355
|
-
// Pattern A: controller/{domain}/*.java (layer-first — domain under controller)
|
|
356
|
-
const controllersA = await glob("src/main/java/**/controller/*/*.java", { cwd: ROOT });
|
|
357
|
-
for (const f of controllersA) {
|
|
358
|
-
const m = f.match(/controller\/([^/]+)\//);
|
|
359
|
-
if (m) {
|
|
360
|
-
const d = m[1];
|
|
361
|
-
if (!domainMap[d]) domainMap[d] = { controllers: 0, services: 0, mappers: 0, dtos: 0, xmlMappers: 0, pattern: "A" };
|
|
362
|
-
domainMap[d].controllers++;
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
if (Object.keys(domainMap).length > 0) detectedPattern = "A";
|
|
366
|
-
|
|
367
|
-
// Pattern B/D: {domain}/controller/*.java (domain-first — controller under domain)
|
|
368
|
-
// D extends B: {module}/{domain}/controller/ — auto-upgrade to module/domain on name conflict
|
|
369
|
-
if (!detectedPattern) {
|
|
370
|
-
const controllersB = await glob("src/main/java/**/*/controller/*.java", { cwd: ROOT });
|
|
371
|
-
const domainPaths = {};
|
|
372
|
-
for (const f of controllersB) {
|
|
373
|
-
const m = f.match(/\/([^/]+)\/controller\/[^/]+\.java$/);
|
|
374
|
-
if (m) {
|
|
375
|
-
const d = m[1];
|
|
376
|
-
const parentMatch = f.match(/\/([^/]+)\/([^/]+)\/controller\//);
|
|
377
|
-
const parentModule = parentMatch ? parentMatch[1] : null;
|
|
378
|
-
if (!domainPaths[d]) domainPaths[d] = [];
|
|
379
|
-
domainPaths[d].push({ file: f, module: parentModule });
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
// If same domain name found in multiple modules, use module/domain form (Pattern D)
|
|
384
|
-
for (const [d, entries] of Object.entries(domainPaths)) {
|
|
385
|
-
const modules = [...new Set(entries.map(e => e.module).filter(Boolean))];
|
|
386
|
-
if (modules.length > 1) {
|
|
387
|
-
// Pattern D: conflict — register as module/domain
|
|
388
|
-
for (const entry of entries) {
|
|
389
|
-
const fullName = entry.module ? `${entry.module}/${d}` : d;
|
|
390
|
-
if (!domainMap[fullName]) domainMap[fullName] = { controllers: 0, services: 0, mappers: 0, dtos: 0, xmlMappers: 0, pattern: "D", modulePath: entry.module, domainName: d };
|
|
391
|
-
domainMap[fullName].controllers++;
|
|
392
|
-
}
|
|
393
|
-
} else {
|
|
394
|
-
if (!domainMap[d]) domainMap[d] = { controllers: 0, services: 0, mappers: 0, dtos: 0, xmlMappers: 0, pattern: "B" };
|
|
395
|
-
domainMap[d].controllers += entries.length;
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
if (Object.keys(domainMap).length > 0) detectedPattern = domainMap[Object.keys(domainMap)[0]].pattern;
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
// Pattern E: DDD/Hexagonal — {domain}/adapter/in/web/*.java or {domain}/adapter/in/rest/*.java
|
|
402
|
-
if (!detectedPattern) {
|
|
403
|
-
const controllersE = await glob("src/main/java/**/adapter/in/{web,rest}/*.java", { cwd: ROOT });
|
|
404
|
-
for (const f of controllersE) {
|
|
405
|
-
const m = f.match(/\/([^/]+)\/adapter\/in\/(web|rest)\/[^/]+\.java$/);
|
|
406
|
-
if (m) {
|
|
407
|
-
const d = m[1];
|
|
408
|
-
if (!domainMap[d]) domainMap[d] = { controllers: 0, services: 0, mappers: 0, dtos: 0, xmlMappers: 0, pattern: "E" };
|
|
409
|
-
domainMap[d].controllers++;
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
if (Object.keys(domainMap).length > 0) detectedPattern = "E";
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
// Pattern C: Flat structure — controller/*.java (no domain directory, extract domain from class name)
|
|
416
|
-
if (!detectedPattern) {
|
|
417
|
-
const controllersC = await glob("src/main/java/**/controller/*.java", { cwd: ROOT });
|
|
418
|
-
for (const f of controllersC) {
|
|
419
|
-
const m = f.match(/\/([A-Z][a-zA-Z]*)Controller\.java$/);
|
|
420
|
-
if (m) {
|
|
421
|
-
const d = m[1].toLowerCase();
|
|
422
|
-
if (!domainMap[d]) domainMap[d] = { controllers: 0, services: 0, mappers: 0, dtos: 0, xmlMappers: 0, pattern: "C" };
|
|
423
|
-
domainMap[d].controllers++;
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
if (Object.keys(domainMap).length > 0) detectedPattern = "C";
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
// ── Supplementary scan: detect service-only domains without controllers ──
|
|
430
|
-
// Catch domains like core/delivery that have service/mapper but no controller
|
|
431
|
-
if (detectedPattern === "B" || detectedPattern === "D" || !detectedPattern) {
|
|
432
|
-
const serviceDirs = await glob("src/main/java/**/*/service/*.java", { cwd: ROOT });
|
|
433
|
-
const mapperDirs = await glob("src/main/java/**/*/{mapper,repository}/*.java", { cwd: ROOT });
|
|
434
|
-
const allServiceFiles = [...serviceDirs, ...mapperDirs];
|
|
435
|
-
const skipDomains = ["common", "config", "util", "utils", "base", "core", "shared", "global", "framework", "infra", "front", "admin", "back", "internal", "external", "web", "app", "test", "tests", "main", "generated", "build"];
|
|
436
|
-
for (const f of allServiceFiles) {
|
|
437
|
-
const m = f.match(/\/([^/]+)\/(service|mapper|repository)\/[^/]+\.java$/);
|
|
438
|
-
if (m) {
|
|
439
|
-
const d = m[1];
|
|
440
|
-
if (!domainMap[d] && !skipDomains.includes(d) && !/^v\d+$/.test(d)) {
|
|
441
|
-
domainMap[d] = { controllers: 0, services: 0, mappers: 0, dtos: 0, xmlMappers: 0, pattern: detectedPattern || "B" };
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
// Scan service/mapper/dto/xml files for each domain
|
|
448
|
-
for (const d of Object.keys(domainMap)) {
|
|
449
|
-
const p = domainMap[d].pattern;
|
|
450
|
-
const dn = domainMap[d].domainName || d;
|
|
451
|
-
let svcGlob, mprGlob, dtoGlob;
|
|
452
|
-
|
|
453
|
-
if (p === "A") {
|
|
454
|
-
svcGlob = `src/main/java/**/service/${d}/*.java`;
|
|
455
|
-
mprGlob = `src/main/java/**/{mapper,repository}/${d}/*.java`;
|
|
456
|
-
dtoGlob = `src/main/java/**/dto/${d}/**/*.java`;
|
|
457
|
-
} else if (p === "B" || p === "D") {
|
|
458
|
-
svcGlob = `src/main/java/**/${dn}/service/*.java`;
|
|
459
|
-
mprGlob = `src/main/java/**/${dn}/{mapper,repository}/*.java`;
|
|
460
|
-
dtoGlob = `src/main/java/**/${dn}/dto/**/*.java`;
|
|
461
|
-
} else if (p === "E") {
|
|
462
|
-
svcGlob = `src/main/java/**/${d}/{application,domain}/**/*.java`;
|
|
463
|
-
mprGlob = `src/main/java/**/${d}/adapter/out/{persistence,repository}/*.java`;
|
|
464
|
-
dtoGlob = `src/main/java/**/${d}/**/{dto,command,query}/**/*.java`;
|
|
465
|
-
} else {
|
|
466
|
-
// Pattern C: Flat — match domain name from file name
|
|
467
|
-
const cap = d.charAt(0).toUpperCase() + d.slice(1);
|
|
468
|
-
svcGlob = `src/main/java/**/service/${cap}*.java`;
|
|
469
|
-
mprGlob = `src/main/java/**/{mapper,repository}/${cap}*.java`;
|
|
470
|
-
dtoGlob = `src/main/java/**/dto/${cap}*.java`;
|
|
471
|
-
}
|
|
472
|
-
const xmlGlob = `src/main/resources/mapper/**/${dn}/*.xml`;
|
|
473
|
-
|
|
474
|
-
const svc = await glob(svcGlob, { cwd: ROOT });
|
|
475
|
-
const mpr = await glob(mprGlob, { cwd: ROOT });
|
|
476
|
-
const dto = await glob(dtoGlob, { cwd: ROOT });
|
|
477
|
-
const xml = await glob(xmlGlob, { cwd: ROOT });
|
|
478
|
-
domainMap[d].services = svc.length;
|
|
479
|
-
domainMap[d].mappers = mpr.length;
|
|
480
|
-
domainMap[d].dtos = dto.length;
|
|
481
|
-
domainMap[d].xmlMappers = xml.length;
|
|
482
|
-
backendDomains.push({ name: d, type: "backend", ...domainMap[d], totalFiles: svc.length + mpr.length + dto.length + xml.length + domainMap[d].controllers });
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
// ── Java fallback: extract domains directly from all .java files when glob returns 0 ──
|
|
486
|
-
if (backendDomains.length === 0) {
|
|
487
|
-
const allJava = await glob("**/*.java", { cwd: ROOT, ignore: ["**/node_modules/**", "**/build/**", "**/target/**", "**/test/**", "**/generated/**"] });
|
|
488
|
-
const javaDomains = {};
|
|
489
|
-
const skipNames = ["common", "config", "util", "utils", "base", "shared", "global", "framework", "infra", "api", "main", "front", "admin", "back", "internal", "external", "web", "app", "test", "tests", "generated", "build"];
|
|
490
|
-
const versionPattern = /^v\d+$/;
|
|
491
|
-
const layerNames = ["controller", "service", "mapper", "repository", "dao", "dto", "vo", "entity", "aggregator", "adapter"];
|
|
492
|
-
|
|
493
|
-
for (const f of allJava) {
|
|
494
|
-
const parts = f.replace(/\\/g, "/").split("/");
|
|
495
|
-
for (let i = 0; i < parts.length - 1; i++) {
|
|
496
|
-
if (layerNames.includes(parts[i]) && i > 0) {
|
|
497
|
-
// Pattern A detection: .../{domain}/controller/... or .../controller/{domain}/...
|
|
498
|
-
const prevDir = parts[i - 1];
|
|
499
|
-
const nextDir = parts[i + 1];
|
|
500
|
-
|
|
501
|
-
// {domain}/layer/ pattern (domain before layer)
|
|
502
|
-
if (!skipNames.includes(prevDir) && !layerNames.includes(prevDir) && !prevDir.includes(".") && !versionPattern.test(prevDir)) {
|
|
503
|
-
if (!javaDomains[prevDir]) javaDomains[prevDir] = { controllers: 0, services: 0, mappers: 0, dtos: 0, xmlMappers: 0, pattern: "B" };
|
|
504
|
-
if (parts[i] === "controller") javaDomains[prevDir].controllers++;
|
|
505
|
-
else if (parts[i] === "service") javaDomains[prevDir].services++;
|
|
506
|
-
else if (["mapper", "repository", "dao"].includes(parts[i])) javaDomains[prevDir].mappers++;
|
|
507
|
-
else if (["dto", "vo"].includes(parts[i])) javaDomains[prevDir].dtos++;
|
|
508
|
-
}
|
|
509
|
-
// layer/{domain}/ pattern (layer before domain)
|
|
510
|
-
if (nextDir && !nextDir.endsWith(".java") && !skipNames.includes(nextDir) && !layerNames.includes(nextDir) && !versionPattern.test(nextDir)) {
|
|
511
|
-
if (!javaDomains[nextDir]) javaDomains[nextDir] = { controllers: 0, services: 0, mappers: 0, dtos: 0, xmlMappers: 0, pattern: "A" };
|
|
512
|
-
if (parts[i] === "controller") javaDomains[nextDir].controllers++;
|
|
513
|
-
else if (parts[i] === "service") javaDomains[nextDir].services++;
|
|
514
|
-
else if (["mapper", "repository", "dao"].includes(parts[i])) javaDomains[nextDir].mappers++;
|
|
515
|
-
else if (["dto", "vo"].includes(parts[i])) javaDomains[nextDir].dtos++;
|
|
516
|
-
}
|
|
517
|
-
break;
|
|
518
|
-
}
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
for (const [d, data] of Object.entries(javaDomains)) {
|
|
523
|
-
const total = data.controllers + data.services + data.mappers + data.dtos;
|
|
524
|
-
if (total > 0) {
|
|
525
|
-
backendDomains.push({ name: d, type: "backend", ...data, totalFiles: total });
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
}
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
// ── Kotlin (multi-module monorepo + CQRS support) ──
|
|
532
|
-
if (stack.language === "kotlin") {
|
|
533
|
-
// Scan all .kt files across all submodules
|
|
534
|
-
const ktFiles = await glob("**/src/main/kotlin/**/*.kt", {
|
|
535
|
-
cwd: ROOT,
|
|
536
|
-
ignore: ["**/node_modules/**", "**/build/**", "**/test/**", "**/generated/**"],
|
|
537
|
-
});
|
|
538
|
-
|
|
539
|
-
// Detect root package from first controller/service file
|
|
540
|
-
for (const f of ktFiles) {
|
|
541
|
-
const m = f.match(/src\/main\/kotlin\/(.+?)\/(controller|service|mapper|dto|entity|repository|adapter)/);
|
|
542
|
-
if (m) { rootPackage = m[1].replace(/\//g, "."); break; }
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
// Detect modules from directory structure (servers/*/ or direct submodules)
|
|
546
|
-
const moduleGlobs = [
|
|
547
|
-
"servers/*/*/src/main/kotlin/", // servers/command/reservation-command-server/
|
|
548
|
-
"servers/*/src/main/kotlin/", // servers/iam-server/
|
|
549
|
-
"*/src/main/kotlin/", // shared-lib/ or integration-lib/
|
|
550
|
-
];
|
|
551
|
-
const moduleSet = {};
|
|
552
|
-
for (const mg of moduleGlobs) {
|
|
553
|
-
const moduleDirs = await glob(mg, { cwd: ROOT });
|
|
554
|
-
for (const md of moduleDirs) {
|
|
555
|
-
const parts = md.replace(/\/src\/main\/kotlin\/$/, "").split("/");
|
|
556
|
-
const moduleName = parts[parts.length - 1]; // e.g., "reservation-command-server"
|
|
557
|
-
if (moduleSet[moduleName]) {
|
|
558
|
-
// Name conflict: use full relative path as key to avoid silent loss
|
|
559
|
-
const fullKey = md.replace(/\/src\/main\/kotlin\/$/, "").replace(/\//g, "__");
|
|
560
|
-
moduleSet[fullKey] = md;
|
|
561
|
-
} else {
|
|
562
|
-
moduleSet[moduleName] = md;
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
// Categorize modules and extract domains
|
|
568
|
-
const skipModules = ["build", "gradle", "buildSrc", ".gradle", "node_modules"];
|
|
569
|
-
const serverTypes = { command: "command", query: "query", bff: "bff", integration: "integration" };
|
|
570
|
-
const domainMap = {};
|
|
571
|
-
|
|
572
|
-
for (const [moduleName, modulePath] of Object.entries(moduleSet)) {
|
|
573
|
-
if (skipModules.some(s => moduleName.includes(s))) continue;
|
|
574
|
-
|
|
575
|
-
// Determine server type from module name
|
|
576
|
-
let serverType = "standalone";
|
|
577
|
-
for (const [key, val] of Object.entries(serverTypes)) {
|
|
578
|
-
if (moduleName.includes(key)) { serverType = val; break; }
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
// Extract domain name from module name
|
|
582
|
-
// e.g., "reservation-command-server" → "reservation"
|
|
583
|
-
// e.g., "pms-bff-server" → "pms"
|
|
584
|
-
// e.g., "iam-server" → "iam"
|
|
585
|
-
// e.g., "shared-lib" → "shared-lib"
|
|
586
|
-
// e.g., "common-query-server" → "common-query" (shared server, not a domain named "common")
|
|
587
|
-
let domainName = moduleName
|
|
588
|
-
.replace(/-(?:command|query|bff|integration|adapter)-server$/, "")
|
|
589
|
-
.replace(/-server$/, "")
|
|
590
|
-
.replace(/-lib$/, "-lib");
|
|
591
|
-
|
|
592
|
-
// Guard: if domain extraction resulted in a generic name that is likely a shared module
|
|
593
|
-
// (e.g., "common-query-server" → "common"), keep the full type qualifier
|
|
594
|
-
const genericNames = ["common", "shared", "base", "core", "global", "internal"];
|
|
595
|
-
if (genericNames.includes(domainName) && serverType !== "standalone") {
|
|
596
|
-
domainName = `${domainName}-${serverType}`;
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
// Scan files in this module (append "/" to prevent prefix overlap: e.g., reservation vs reservation-ext)
|
|
600
|
-
const modulePrefix = modulePath.replace(/\/$/, "").replace(/\/src\/main\/kotlin$/, "") + "/";
|
|
601
|
-
const moduleKtFiles = ktFiles.filter(f => f.startsWith(modulePrefix) || f === modulePrefix.slice(0, -1));
|
|
602
|
-
const controllers = moduleKtFiles.filter(f => /controller/i.test(f)).length;
|
|
603
|
-
const services = moduleKtFiles.filter(f => /service/i.test(f)).length;
|
|
604
|
-
const repositories = moduleKtFiles.filter(f => /repository|mapper/i.test(f)).length;
|
|
605
|
-
const dtos = moduleKtFiles.filter(f => /dto|vo|request|response|model/i.test(f) && !/repository|service|controller/i.test(f)).length;
|
|
606
|
-
const totalFiles = moduleKtFiles.length;
|
|
607
|
-
|
|
608
|
-
if (totalFiles === 0) continue;
|
|
609
|
-
|
|
610
|
-
const key = `${domainName}:${serverType}`;
|
|
611
|
-
domainMap[key] = {
|
|
612
|
-
name: domainName,
|
|
613
|
-
moduleName,
|
|
614
|
-
modulePath,
|
|
615
|
-
serverType,
|
|
616
|
-
controllers,
|
|
617
|
-
services,
|
|
618
|
-
repositories,
|
|
619
|
-
dtos,
|
|
620
|
-
totalFiles,
|
|
621
|
-
};
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
// Group by domain: merge command/query/bff of same domain
|
|
625
|
-
const domainGroups = {};
|
|
626
|
-
for (const [key, info] of Object.entries(domainMap)) {
|
|
627
|
-
const { name } = info;
|
|
628
|
-
if (!domainGroups[name]) {
|
|
629
|
-
domainGroups[name] = { name, type: "backend", modules: [], controllers: 0, services: 0, repositories: 0, dtos: 0, totalFiles: 0, serverTypes: [] };
|
|
630
|
-
}
|
|
631
|
-
const dg = domainGroups[name];
|
|
632
|
-
dg.modules.push(info.moduleName);
|
|
633
|
-
dg.controllers += info.controllers;
|
|
634
|
-
dg.services += info.services;
|
|
635
|
-
dg.repositories += info.repositories;
|
|
636
|
-
dg.dtos += info.dtos;
|
|
637
|
-
dg.totalFiles += info.totalFiles;
|
|
638
|
-
if (!dg.serverTypes.includes(info.serverType)) dg.serverTypes.push(info.serverType);
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
// Push to backendDomains
|
|
642
|
-
for (const dg of Object.values(domainGroups)) {
|
|
643
|
-
backendDomains.push({
|
|
644
|
-
name: dg.name,
|
|
645
|
-
type: "backend",
|
|
646
|
-
controllers: dg.controllers,
|
|
647
|
-
services: dg.services,
|
|
648
|
-
mappers: dg.repositories,
|
|
649
|
-
dtos: dg.dtos,
|
|
650
|
-
totalFiles: dg.totalFiles,
|
|
651
|
-
modules: dg.modules,
|
|
652
|
-
serverTypes: dg.serverTypes,
|
|
653
|
-
pattern: "kotlin-multimodule",
|
|
654
|
-
});
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
// Also scan shared libraries as special domains
|
|
658
|
-
const libDirs = await glob("{shared-lib,integration-lib,*-lib}/src/main/kotlin/", { cwd: ROOT });
|
|
659
|
-
for (const ld of libDirs) {
|
|
660
|
-
const libName = ld.split("/")[0];
|
|
661
|
-
if (domainGroups[libName]) continue; // Already captured
|
|
662
|
-
const libFiles = ktFiles.filter(f => f.startsWith(libName));
|
|
663
|
-
if (libFiles.length > 0) {
|
|
664
|
-
backendDomains.push({
|
|
665
|
-
name: libName,
|
|
666
|
-
type: "backend",
|
|
667
|
-
controllers: 0,
|
|
668
|
-
services: libFiles.filter(f => /service/i.test(f)).length,
|
|
669
|
-
mappers: 0,
|
|
670
|
-
dtos: libFiles.filter(f => /dto|vo|model/i.test(f)).length,
|
|
671
|
-
totalFiles: libFiles.length,
|
|
672
|
-
modules: [libName],
|
|
673
|
-
serverTypes: ["library"],
|
|
674
|
-
pattern: "kotlin-library",
|
|
675
|
-
});
|
|
676
|
-
}
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
// Resolve shared query modules: redistribute internal domains to actual domain entries
|
|
680
|
-
resolveSharedQueryDomains(backendDomains, ktFiles);
|
|
681
|
-
|
|
682
|
-
// ── Kotlin single-module fallback (no multi-module structure detected) ──
|
|
683
|
-
if (backendDomains.length === 0 && ktFiles.length > 0) {
|
|
684
|
-
const ktDomains = {};
|
|
685
|
-
const skipNames = ["common", "config", "util", "utils", "base", "shared", "global", "framework", "infra", "main", "generated", "build"];
|
|
686
|
-
const layerKw = ["controller", "service", "repository", "mapper", "dao", "dto", "vo", "entity", "aggregate", "adapter"];
|
|
687
|
-
for (const f of ktFiles) {
|
|
688
|
-
const parts = f.replace(/\\/g, "/").split("/");
|
|
689
|
-
for (let i = 0; i < parts.length - 1; i++) {
|
|
690
|
-
if (layerKw.includes(parts[i].toLowerCase())) {
|
|
691
|
-
// domain/layer/ pattern
|
|
692
|
-
if (i > 0) {
|
|
693
|
-
const d = parts[i - 1].toLowerCase();
|
|
694
|
-
if (!skipNames.includes(d) && !layerKw.includes(d) && d.length > 1 && d !== "kotlin") {
|
|
695
|
-
if (!ktDomains[d]) ktDomains[d] = { controllers: 0, services: 0, mappers: 0, dtos: 0, totalFiles: 0 };
|
|
696
|
-
if (parts[i] === "controller") ktDomains[d].controllers++;
|
|
697
|
-
else if (parts[i] === "service") ktDomains[d].services++;
|
|
698
|
-
else if (["repository", "mapper", "dao"].includes(parts[i])) ktDomains[d].mappers++;
|
|
699
|
-
else if (["dto", "vo", "entity"].includes(parts[i])) ktDomains[d].dtos++;
|
|
700
|
-
ktDomains[d].totalFiles++;
|
|
701
|
-
}
|
|
702
|
-
}
|
|
703
|
-
break;
|
|
704
|
-
}
|
|
705
|
-
}
|
|
706
|
-
}
|
|
707
|
-
for (const [d, data] of Object.entries(ktDomains)) {
|
|
708
|
-
if (data.totalFiles > 0) {
|
|
709
|
-
backendDomains.push({ name: d, type: "backend", ...data, pattern: "kotlin-single" });
|
|
710
|
-
}
|
|
711
|
-
}
|
|
712
|
-
}
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
// ── Node.js backend (Express/NestJS) — scan regardless of frontend presence ──
|
|
716
|
-
if ((stack.language === "typescript" || stack.language === "javascript") && stack.framework) {
|
|
717
|
-
const nestModules = await glob("src/modules/*/", { cwd: ROOT });
|
|
718
|
-
const srcDirs = nestModules.length > 0 ? nestModules : await glob("src/*/", { cwd: ROOT });
|
|
719
|
-
const skipDirs = ["common", "shared", "config", "utils", "lib", "core", "main", "interfaces", "types", "constants", "guards", "decorators", "pipes", "filters", "interceptors"];
|
|
720
|
-
for (const dir of srcDirs) {
|
|
721
|
-
const name = path.basename(dir);
|
|
722
|
-
if (skipDirs.includes(name)) continue;
|
|
723
|
-
const files = await glob(`${dir}**/*.{ts,js}`, { cwd: ROOT, ignore: ["**/*.spec.*", "**/*.test.*"] });
|
|
724
|
-
if (files.length > 0) {
|
|
725
|
-
const controllers = files.filter(f => /controller|router|route/.test(f)).length;
|
|
726
|
-
const services = files.filter(f => /service/.test(f)).length;
|
|
727
|
-
const dtos = files.filter(f => /dto|schema|type/.test(f)).length;
|
|
728
|
-
backendDomains.push({ name, type: "backend", controllers, services, dtos, totalFiles: files.length });
|
|
729
|
-
}
|
|
730
|
-
}
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
// ── Next.js/React/Vue ──
|
|
734
|
-
if (stack.frontend === "nextjs" || stack.frontend === "react" || stack.frontend === "vue") {
|
|
735
|
-
// App Router / Pages Router domains
|
|
736
|
-
const allDirs = [
|
|
737
|
-
...await glob("{app,src/app}/*/", { cwd: ROOT }),
|
|
738
|
-
...await glob("{pages,src/pages}/*/", { cwd: ROOT }),
|
|
739
|
-
];
|
|
740
|
-
const skipPages = ["api", "_app", "_document", "fonts"];
|
|
741
|
-
for (const dir of allDirs) {
|
|
742
|
-
const name = path.basename(dir);
|
|
743
|
-
if (skipPages.includes(name) || name.startsWith("(") || name.startsWith("_") || name.startsWith(".")) continue;
|
|
744
|
-
const files = await glob(`${dir}**/*.{tsx,jsx,ts,js}`, { cwd: ROOT });
|
|
745
|
-
if (files.length > 0) {
|
|
746
|
-
const pages = files.filter(f => /page\.|index\./.test(f)).length;
|
|
747
|
-
const layouts = files.filter(f => /layout\./.test(f)).length;
|
|
748
|
-
const clientFiles = files.filter(f => /client\./.test(f)).length;
|
|
749
|
-
const serverFiles = pages + layouts;
|
|
750
|
-
const components = files.filter(f => !/page\.|layout\.|index\.|client\./.test(f)).length;
|
|
751
|
-
frontendDomains.push({
|
|
752
|
-
name, type: "frontend", pages, layouts, clientFiles, serverFiles, components, totalFiles: files.length,
|
|
753
|
-
rscPattern: clientFiles > 0 ? "RSC+Client split" : "default",
|
|
754
|
-
});
|
|
755
|
-
}
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
// FSD (Feature-Sliced Design): features/*, widgets/*, entities/*
|
|
759
|
-
const fsdLayers = ["features", "widgets", "entities"];
|
|
760
|
-
for (const layer of fsdLayers) {
|
|
761
|
-
const fsdDirs = await glob(`{${layer},src/${layer}}/*/`, { cwd: ROOT });
|
|
762
|
-
for (const dir of fsdDirs) {
|
|
763
|
-
const name = path.basename(dir);
|
|
764
|
-
if (["ui", "common", "shared", "lib", "config", "index"].includes(name)) continue;
|
|
765
|
-
const files = await glob(`${dir}**/*.{tsx,jsx,ts,js}`, { cwd: ROOT, ignore: ["**/*.spec.*", "**/*.test.*", "**/*.stories.*"] });
|
|
766
|
-
if (files.length > 0) {
|
|
767
|
-
const uiFiles = files.filter(f => /\bui\b/.test(f)).length;
|
|
768
|
-
const modelFiles = files.filter(f => /model|store|hook/.test(f)).length;
|
|
769
|
-
frontendDomains.push({ name: `${layer}/${name}`, type: "frontend", components: uiFiles, models: modelFiles, totalFiles: files.length });
|
|
770
|
-
}
|
|
771
|
-
}
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
// components/* (existing)
|
|
775
|
-
const compDirs = await glob("{src/,}components/*/", { cwd: ROOT });
|
|
776
|
-
for (const dir of compDirs) {
|
|
777
|
-
const name = path.basename(dir);
|
|
778
|
-
if (["ui", "common", "shared", "layout", "icons"].includes(name)) continue;
|
|
779
|
-
const files = await glob(`${dir}**/*.{tsx,jsx}`, { cwd: ROOT });
|
|
780
|
-
if (files.length >= 2) {
|
|
781
|
-
frontendDomains.push({ name: `comp-${name}`, type: "frontend", components: files.length, totalFiles: files.length });
|
|
782
|
-
}
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
// ── Fallback: extract domains directly from page.tsx/index.tsx locations when scanners return 0 ──
|
|
786
|
-
if (frontendDomains.length === 0) {
|
|
787
|
-
const pageFiles = await glob("**/page.{tsx,jsx}", { cwd: ROOT, ignore: ["**/node_modules/**", "**/.next/**"] });
|
|
788
|
-
const domainSet = {};
|
|
789
|
-
const skipNames = ["app", "src", "pages", "api", "_app", "_document"];
|
|
790
|
-
for (const f of pageFiles) {
|
|
791
|
-
const parts = f.replace(/\\/g, "/").split("/");
|
|
792
|
-
const appIdx = parts.indexOf("app");
|
|
793
|
-
const pagesIdx = parts.indexOf("pages");
|
|
794
|
-
const baseIdx = appIdx >= 0 ? appIdx : pagesIdx;
|
|
795
|
-
if (baseIdx >= 0 && baseIdx + 1 < parts.length - 1) {
|
|
796
|
-
const domain = parts[baseIdx + 1];
|
|
797
|
-
if (!skipNames.includes(domain) && !domain.startsWith("_") && !domain.startsWith("(") && !domain.startsWith(".")) {
|
|
798
|
-
if (!domainSet[domain]) domainSet[domain] = { pages: 0, clientFiles: 0, totalFiles: 0 };
|
|
799
|
-
domainSet[domain].pages++;
|
|
800
|
-
domainSet[domain].totalFiles++;
|
|
801
|
-
}
|
|
802
|
-
}
|
|
803
|
-
}
|
|
804
|
-
// Count client.tsx as well
|
|
805
|
-
const clientFiles = await glob("**/client.{tsx,jsx}", { cwd: ROOT, ignore: ["**/node_modules/**", "**/.next/**"] });
|
|
806
|
-
for (const f of clientFiles) {
|
|
807
|
-
const parts = f.replace(/\\/g, "/").split("/");
|
|
808
|
-
const appIdx = parts.indexOf("app");
|
|
809
|
-
const baseIdx = appIdx >= 0 ? appIdx : -1;
|
|
810
|
-
if (baseIdx >= 0 && baseIdx + 1 < parts.length - 1) {
|
|
811
|
-
const domain = parts[baseIdx + 1];
|
|
812
|
-
if (domainSet[domain]) {
|
|
813
|
-
domainSet[domain].clientFiles++;
|
|
814
|
-
domainSet[domain].totalFiles++;
|
|
815
|
-
}
|
|
816
|
-
}
|
|
817
|
-
}
|
|
818
|
-
for (const [name, data] of Object.entries(domainSet)) {
|
|
819
|
-
frontendDomains.push({
|
|
820
|
-
name, type: "frontend", pages: data.pages, clientFiles: data.clientFiles, totalFiles: data.totalFiles,
|
|
821
|
-
rscPattern: data.clientFiles > 0 ? "RSC+Client split" : "default",
|
|
822
|
-
});
|
|
823
|
-
}
|
|
824
|
-
|
|
825
|
-
// Also scan widgets/features/entities directly (glob fallback)
|
|
826
|
-
for (const layer of ["widgets", "features", "entities"]) {
|
|
827
|
-
const layerFiles = await glob(`**/${layer}/*/**/*.{tsx,jsx,ts,js}`, { cwd: ROOT, ignore: ["**/node_modules/**", "**/.next/**", "**/*.spec.*", "**/*.test.*"] });
|
|
828
|
-
const layerDomains = {};
|
|
829
|
-
for (const f of layerFiles) {
|
|
830
|
-
const parts = f.replace(/\\/g, "/").split("/");
|
|
831
|
-
const layerIdx = parts.indexOf(layer);
|
|
832
|
-
if (layerIdx >= 0 && layerIdx + 1 < parts.length) {
|
|
833
|
-
const domain = parts[layerIdx + 1];
|
|
834
|
-
if (!["ui", "common", "shared", "lib", "config"].includes(domain)) {
|
|
835
|
-
if (!layerDomains[domain]) layerDomains[domain] = 0;
|
|
836
|
-
layerDomains[domain]++;
|
|
837
|
-
}
|
|
838
|
-
}
|
|
839
|
-
}
|
|
840
|
-
for (const [name, count] of Object.entries(layerDomains)) {
|
|
841
|
-
frontendDomains.push({ name: `${layer}/${name}`, type: "frontend", totalFiles: count });
|
|
842
|
-
}
|
|
843
|
-
}
|
|
844
|
-
}
|
|
845
|
-
}
|
|
846
|
-
|
|
847
|
-
// ── Python/Django ──
|
|
848
|
-
if (stack.framework === "django") {
|
|
849
|
-
const candidates = await glob("**/models.py", { cwd: ROOT, ignore: ["**/node_modules/**", "**/venv/**", "**/.venv/**", "**/env/**", "**/migrations/**"] });
|
|
850
|
-
for (const f of candidates) {
|
|
851
|
-
const dir = path.dirname(f);
|
|
852
|
-
if (dir === "." || dir.includes("venv")) continue;
|
|
853
|
-
const name = path.basename(dir);
|
|
854
|
-
const appFiles = await glob(`${dir}/*.py`, { cwd: ROOT });
|
|
855
|
-
const views = appFiles.filter(x => x.includes("views")).length;
|
|
856
|
-
const models = appFiles.filter(x => x.includes("models")).length;
|
|
857
|
-
const serializers = appFiles.filter(x => x.includes("serializers")).length;
|
|
858
|
-
backendDomains.push({ name, type: "backend", views, models, serializers, totalFiles: appFiles.length });
|
|
859
|
-
}
|
|
860
|
-
}
|
|
861
|
-
|
|
862
|
-
// ── Python/FastAPI ──
|
|
863
|
-
if (stack.framework === "fastapi" || stack.framework === "flask") {
|
|
864
|
-
const routerFiles = await glob("**/{router,routes,endpoints}*.py", { cwd: ROOT, ignore: ["**/venv/**", "**/.venv/**"] });
|
|
865
|
-
const seen = new Set();
|
|
866
|
-
for (const f of routerFiles) {
|
|
867
|
-
const dir = path.dirname(f);
|
|
868
|
-
const name = path.basename(dir);
|
|
869
|
-
if (seen.has(name) || ["venv", ".venv", "__pycache__"].includes(name)) continue;
|
|
870
|
-
seen.add(name);
|
|
871
|
-
const appFiles = await glob(`${dir}/*.py`, { cwd: ROOT });
|
|
872
|
-
backendDomains.push({ name, type: "backend", totalFiles: appFiles.length });
|
|
873
|
-
}
|
|
874
|
-
if (backendDomains.filter(d => d.type === "backend").length === 0) {
|
|
875
|
-
const appDirs = await glob("app/*/", { cwd: ROOT });
|
|
876
|
-
for (const dir of appDirs) {
|
|
877
|
-
const name = path.basename(dir);
|
|
878
|
-
if (["core", "common", "utils", "__pycache__"].includes(name)) continue;
|
|
879
|
-
const files = await glob(`${dir}*.py`, { cwd: ROOT });
|
|
880
|
-
if (files.length > 0) backendDomains.push({ name, type: "backend", totalFiles: files.length });
|
|
881
|
-
}
|
|
882
|
-
}
|
|
883
|
-
}
|
|
884
|
-
|
|
885
|
-
// Frontend count
|
|
886
|
-
const frontend = { exists: false, components: 0, pages: 0, hooks: 0 };
|
|
887
|
-
if (stack.frontend) {
|
|
888
|
-
frontend.exists = true;
|
|
889
|
-
frontend.components = (await glob("{src/,}**/components/**/*.{tsx,jsx,vue}", { cwd: ROOT, ignore: ["**/node_modules/**"] })).length;
|
|
890
|
-
frontend.pages = (await glob("{src/,}{app,pages}/**/{page,index}.{tsx,jsx,vue}", { cwd: ROOT, ignore: ["**/node_modules/**"] })).length;
|
|
891
|
-
frontend.hooks = (await glob("{src/,}**/hooks/**/*.{ts,js}", { cwd: ROOT, ignore: ["**/node_modules/**"] })).length;
|
|
892
|
-
}
|
|
893
|
-
|
|
894
|
-
// App Router RSC/Client overall stats (for project-analysis.json)
|
|
895
|
-
if (stack.frontend === "nextjs") {
|
|
896
|
-
const allClientFiles = await glob("{app,src/app}/**/client.{tsx,ts,jsx,js}", { cwd: ROOT });
|
|
897
|
-
const allPageFiles = await glob("{app,src/app}/**/page.{tsx,ts,jsx,js}", { cwd: ROOT });
|
|
898
|
-
const allLayoutFiles = await glob("{app,src/app}/**/layout.{tsx,ts,jsx,js}", { cwd: ROOT });
|
|
899
|
-
frontend.clientComponents = allClientFiles.length;
|
|
900
|
-
frontend.serverPages = allPageFiles.length;
|
|
901
|
-
frontend.layouts = allLayoutFiles.length;
|
|
902
|
-
frontend.rscPattern = allClientFiles.length > 0;
|
|
903
|
-
}
|
|
904
|
-
|
|
905
|
-
// All domains = backend + frontend (with type tags)
|
|
906
|
-
const allDomains = [
|
|
907
|
-
...backendDomains.sort((a, b) => b.totalFiles - a.totalFiles),
|
|
908
|
-
...frontendDomains.sort((a, b) => b.totalFiles - a.totalFiles),
|
|
909
|
-
];
|
|
910
|
-
|
|
911
|
-
return { domains: allDomains, backendDomains, frontendDomains, rootPackage, frontend };
|
|
912
|
-
}
|
|
913
|
-
|
|
914
|
-
// ─── Resolve shared query modules ────────────────────────────────
|
|
915
|
-
// When a shared query module (e.g., common-query-server) contains controllers/services
|
|
916
|
-
// for multiple domains, extract the actual domains and merge them into existing entries.
|
|
917
|
-
// Supports two extraction patterns:
|
|
918
|
-
// A) Package-based: .../kotlin/com/company/.../DOMAIN/controller/XxxController.kt
|
|
919
|
-
// B) Class-name-based: .../controller/ReservationQueryController.kt → "reservation"
|
|
920
|
-
// Safety: if no shared modules are found, this function does nothing (no-op).
|
|
921
|
-
function resolveSharedQueryDomains(backendDomains, ktFiles) {
|
|
922
|
-
const genericNames = ["common", "shared", "base", "core", "global"];
|
|
923
|
-
const layerNames = new Set([
|
|
924
|
-
"controller", "service", "repository", "mapper", "api", "handler",
|
|
925
|
-
"dto", "model", "entity", "config", "configuration", "util", "utils",
|
|
926
|
-
"infra", "infrastructure", "framework", "common", "shared", "base",
|
|
927
|
-
"core", "global", "internal", "external", "exception", "interceptor",
|
|
928
|
-
"filter", "converter", "event", "listener", "client", "feign",
|
|
929
|
-
"adapter", "port", "domain", "application", "presentation",
|
|
930
|
-
"persistence", "web", "rest", "grpc", "query", "command",
|
|
931
|
-
"aggregate", "aggregator", "vo", "valueobject",
|
|
932
|
-
]);
|
|
933
|
-
const skipPackages = new Set([
|
|
934
|
-
"com", "org", "net", "io", "kr", "jp", "cn", "de", "fr", "uk", "us",
|
|
935
|
-
"dev", "app", "main", "server", "backend", "project", "kotlin",
|
|
936
|
-
]);
|
|
937
|
-
const classSuffixes = new Set([
|
|
938
|
-
"Controller", "Service", "Repository", "Mapper", "Handler", "Api",
|
|
939
|
-
"Query", "Command", "Read", "Write", "Get", "Find", "List", "Search",
|
|
940
|
-
"Admin", "Dto", "Request", "Response", "Entity", "Model", "Impl",
|
|
941
|
-
"Factory", "Builder", "Adapter", "Client", "Facade", "Provider",
|
|
942
|
-
]);
|
|
943
|
-
|
|
944
|
-
// Find shared modules: generic name + query server type
|
|
945
|
-
const sharedModules = backendDomains.filter(d =>
|
|
946
|
-
d.pattern === "kotlin-multimodule" &&
|
|
947
|
-
d.serverTypes && d.serverTypes.includes("query") &&
|
|
948
|
-
genericNames.some(g => d.name.startsWith(g))
|
|
949
|
-
);
|
|
950
|
-
if (sharedModules.length === 0) return;
|
|
951
|
-
|
|
952
|
-
// Existing domain names for Pattern B-1 matching (longest-first for greedy match)
|
|
953
|
-
const existingDomains = backendDomains
|
|
954
|
-
.filter(d => !sharedModules.includes(d) && d.pattern === "kotlin-multimodule")
|
|
955
|
-
.map(d => d.name)
|
|
956
|
-
.sort((a, b) => b.length - a.length);
|
|
957
|
-
|
|
958
|
-
for (const shared of sharedModules) {
|
|
959
|
-
const moduleNames = shared.modules || [];
|
|
960
|
-
const sharedKtFiles = ktFiles.filter(f =>
|
|
961
|
-
moduleNames.some(m => f.includes(`/${m}/`) || f.startsWith(`${m}/`))
|
|
962
|
-
);
|
|
963
|
-
if (sharedKtFiles.length === 0) continue;
|
|
964
|
-
|
|
965
|
-
const domainFileMap = {}; // { domainName: [filePaths] }
|
|
966
|
-
|
|
967
|
-
for (const filePath of sharedKtFiles) {
|
|
968
|
-
let domain = null;
|
|
969
|
-
const parts = filePath.replace(/\\/g, "/").split("/");
|
|
970
|
-
|
|
971
|
-
// ── Pattern A: Package-based ──
|
|
972
|
-
// Looks for: .../kotlin/com/company/[project/]DOMAIN/LAYER/File.kt
|
|
973
|
-
// Uses depth >= 3 from kotlin/ to skip base package segments
|
|
974
|
-
const kotlinIdx = parts.findIndex(p => p === "kotlin");
|
|
975
|
-
if (kotlinIdx >= 0) {
|
|
976
|
-
for (let i = kotlinIdx + 1; i < parts.length - 1; i++) {
|
|
977
|
-
if (layerNames.has(parts[i].toLowerCase())) {
|
|
978
|
-
if (i - 1 > kotlinIdx) {
|
|
979
|
-
const candidate = parts[i - 1].toLowerCase();
|
|
980
|
-
const depth = (i - 1) - kotlinIdx; // distance from kotlin/
|
|
981
|
-
if (
|
|
982
|
-
depth >= 3 &&
|
|
983
|
-
!layerNames.has(candidate) &&
|
|
984
|
-
!skipPackages.has(candidate) &&
|
|
985
|
-
candidate.length > 2
|
|
986
|
-
) {
|
|
987
|
-
domain = candidate;
|
|
988
|
-
}
|
|
989
|
-
}
|
|
990
|
-
break; // only check first layer directory encountered
|
|
991
|
-
}
|
|
992
|
-
}
|
|
993
|
-
}
|
|
994
|
-
|
|
995
|
-
// ── Pattern B-1: Match class name against existing domain names ──
|
|
996
|
-
if (!domain) {
|
|
997
|
-
const fileName = parts[parts.length - 1].replace(/\.kt$/, "").toLowerCase();
|
|
998
|
-
for (const ed of existingDomains) {
|
|
999
|
-
const normalized = ed.replace(/-/g, "");
|
|
1000
|
-
if (fileName.startsWith(normalized) && fileName.length > normalized.length) {
|
|
1001
|
-
domain = ed;
|
|
1002
|
-
break;
|
|
1003
|
-
}
|
|
1004
|
-
}
|
|
1005
|
-
}
|
|
1006
|
-
|
|
1007
|
-
// ── Pattern B-2: Extract from PascalCase class name ──
|
|
1008
|
-
if (!domain) {
|
|
1009
|
-
const fileName = parts[parts.length - 1].replace(/\.kt$/, "");
|
|
1010
|
-
const words = fileName.match(/[A-Z][a-z]+|[A-Z]+(?=[A-Z][a-z]|$)/g);
|
|
1011
|
-
if (words && words.length >= 2) {
|
|
1012
|
-
const domainWords = [];
|
|
1013
|
-
for (const w of words) {
|
|
1014
|
-
if (classSuffixes.has(w)) break;
|
|
1015
|
-
domainWords.push(w);
|
|
1016
|
-
}
|
|
1017
|
-
if (domainWords.length > 0) {
|
|
1018
|
-
const extracted = domainWords.join("").toLowerCase();
|
|
1019
|
-
if (
|
|
1020
|
-
!genericNames.includes(extracted) &&
|
|
1021
|
-
!skipPackages.has(extracted) &&
|
|
1022
|
-
extracted.length > 2
|
|
1023
|
-
) {
|
|
1024
|
-
domain = extracted;
|
|
1025
|
-
}
|
|
1026
|
-
}
|
|
1027
|
-
}
|
|
1028
|
-
}
|
|
1029
|
-
|
|
1030
|
-
if (domain) {
|
|
1031
|
-
if (!domainFileMap[domain]) domainFileMap[domain] = [];
|
|
1032
|
-
domainFileMap[domain].push(filePath);
|
|
1033
|
-
}
|
|
1034
|
-
}
|
|
1035
|
-
|
|
1036
|
-
// ── Merge extracted domains into backendDomains ──
|
|
1037
|
-
let distributedFiles = 0;
|
|
1038
|
-
for (const [domain, files] of Object.entries(domainFileMap)) {
|
|
1039
|
-
// Find matching existing domain (exact or normalized)
|
|
1040
|
-
const existing = backendDomains.find(d =>
|
|
1041
|
-
d !== shared &&
|
|
1042
|
-
(d.name === domain || d.name.replace(/-/g, "") === domain.replace(/-/g, ""))
|
|
1043
|
-
);
|
|
1044
|
-
|
|
1045
|
-
const ctrlCount = files.filter(f =>
|
|
1046
|
-
/\/controller\//i.test(f) || /Controller\.kt$/i.test(f)
|
|
1047
|
-
).length;
|
|
1048
|
-
const svcCount = files.filter(f =>
|
|
1049
|
-
/\/service\//i.test(f) || /Service\.kt$/i.test(f)
|
|
1050
|
-
).length;
|
|
1051
|
-
const repoCount = files.filter(f =>
|
|
1052
|
-
/\/repository\//i.test(f) || /\/mapper\//i.test(f) ||
|
|
1053
|
-
/Repository\.kt$/i.test(f) || /Mapper\.kt$/i.test(f)
|
|
1054
|
-
).length;
|
|
1055
|
-
const dtoCount = files.filter(f =>
|
|
1056
|
-
(/\/dto\//i.test(f) || /\/vo\//i.test(f) || /Dto\.kt$/i.test(f) ||
|
|
1057
|
-
/Vo\.kt$/i.test(f) || /Request\.kt$/i.test(f) || /Response\.kt$/i.test(f)) &&
|
|
1058
|
-
!/\/controller\//i.test(f) && !/\/service\//i.test(f) &&
|
|
1059
|
-
!/\/repository\//i.test(f)
|
|
1060
|
-
).length;
|
|
1061
|
-
|
|
1062
|
-
if (existing) {
|
|
1063
|
-
// Merge into existing domain
|
|
1064
|
-
if (!existing.modules) existing.modules = [];
|
|
1065
|
-
for (const m of moduleNames) {
|
|
1066
|
-
if (!existing.modules.includes(m)) existing.modules.push(m);
|
|
1067
|
-
}
|
|
1068
|
-
if (!existing.serverTypes) existing.serverTypes = [];
|
|
1069
|
-
if (!existing.serverTypes.includes("query")) existing.serverTypes.push("query");
|
|
1070
|
-
existing.controllers += ctrlCount;
|
|
1071
|
-
existing.services += svcCount;
|
|
1072
|
-
if (existing.mappers != null) existing.mappers += repoCount;
|
|
1073
|
-
existing.dtos += dtoCount;
|
|
1074
|
-
existing.totalFiles += files.length;
|
|
1075
|
-
} else {
|
|
1076
|
-
// Create new domain entry
|
|
1077
|
-
backendDomains.push({
|
|
1078
|
-
name: domain,
|
|
1079
|
-
type: "backend",
|
|
1080
|
-
controllers: ctrlCount,
|
|
1081
|
-
services: svcCount,
|
|
1082
|
-
mappers: repoCount,
|
|
1083
|
-
dtos: dtoCount,
|
|
1084
|
-
totalFiles: files.length,
|
|
1085
|
-
modules: [...moduleNames],
|
|
1086
|
-
serverTypes: ["query"],
|
|
1087
|
-
pattern: "kotlin-multimodule",
|
|
1088
|
-
resolvedFrom: shared.name,
|
|
1089
|
-
});
|
|
1090
|
-
}
|
|
1091
|
-
distributedFiles += files.length;
|
|
1092
|
-
}
|
|
1093
|
-
|
|
1094
|
-
// Adjust shared module: keep only undistributed files
|
|
1095
|
-
if (distributedFiles > 0) {
|
|
1096
|
-
shared.totalFiles = Math.max(0, shared.totalFiles - distributedFiles);
|
|
1097
|
-
shared.resolvedDomains = Object.keys(domainFileMap);
|
|
1098
|
-
if (shared.totalFiles === 0) shared._resolved = true;
|
|
1099
|
-
}
|
|
1100
|
-
}
|
|
1101
|
-
|
|
1102
|
-
// Remove fully resolved shared entries (all files distributed)
|
|
1103
|
-
for (let i = backendDomains.length - 1; i >= 0; i--) {
|
|
1104
|
-
if (backendDomains[i]._resolved) backendDomains.splice(i, 1);
|
|
1105
|
-
}
|
|
1106
|
-
}
|
|
1107
|
-
|
|
1108
|
-
// ─── Domain group splitting (per type) ──────────────────────────
|
|
1109
|
-
function splitDomainGroups(domains, type, template) {
|
|
1110
|
-
const MAX_FILES_PER_GROUP = 40;
|
|
1111
|
-
const MAX_DOMAINS_PER_GROUP = 4;
|
|
1112
|
-
const groups = [];
|
|
1113
|
-
let current = [];
|
|
1114
|
-
let fileCount = 0;
|
|
1115
|
-
|
|
1116
|
-
for (const d of domains) {
|
|
1117
|
-
current.push(d.name);
|
|
1118
|
-
fileCount += d.totalFiles;
|
|
1119
|
-
if (fileCount >= MAX_FILES_PER_GROUP || current.length >= MAX_DOMAINS_PER_GROUP) {
|
|
1120
|
-
groups.push({ type, template, domains: [...current], estimatedFiles: fileCount });
|
|
1121
|
-
current = [];
|
|
1122
|
-
fileCount = 0;
|
|
1123
|
-
}
|
|
1124
|
-
}
|
|
1125
|
-
if (current.length > 0) {
|
|
1126
|
-
groups.push({ type, template, domains: [...current], estimatedFiles: fileCount });
|
|
1127
|
-
}
|
|
1128
|
-
|
|
1129
|
-
return groups;
|
|
1130
|
-
}
|
|
1131
|
-
|
|
1132
|
-
// ─── Determine active domains ───────────────────────────────────
|
|
1133
|
-
function determineActiveDomains(stack) {
|
|
1134
|
-
const isBackend = stack.framework && !["nextjs", "react", "vue"].includes(stack.framework);
|
|
1135
|
-
return {
|
|
1136
|
-
"00.core": true,
|
|
1137
|
-
"10.backend": !!isBackend,
|
|
1138
|
-
"20.frontend": !!stack.frontend,
|
|
1139
|
-
"30.security-db": !!(stack.database || stack.framework),
|
|
1140
|
-
"40.infra": true,
|
|
1141
|
-
"50.verification": true,
|
|
1142
|
-
"90.optional": true,
|
|
1143
|
-
};
|
|
1144
|
-
}
|
|
1145
|
-
|
|
1146
|
-
// ─── Template selection (multi-stack) ──────────────────────────────
|
|
1147
|
-
function selectTemplates(stack) {
|
|
1148
|
-
const templates = { backend: null, frontend: null };
|
|
1149
|
-
|
|
1150
|
-
// Backend template
|
|
1151
|
-
if (stack.language === "kotlin") templates.backend = "kotlin-spring";
|
|
1152
|
-
else if (stack.language === "java") templates.backend = "java-spring";
|
|
1153
|
-
else if (stack.framework === "express" || stack.framework === "nestjs") templates.backend = "node-express";
|
|
1154
|
-
else if (stack.framework === "django") templates.backend = "python-django";
|
|
1155
|
-
else if (stack.framework === "fastapi" || stack.framework === "flask") templates.backend = "python-fastapi";
|
|
1156
|
-
else if (stack.language === "typescript" || stack.language === "javascript") templates.backend = "node-express";
|
|
1157
|
-
else if (stack.language === "python") templates.backend = "python-fastapi";
|
|
1158
|
-
|
|
1159
|
-
// Frontend template
|
|
1160
|
-
if (stack.frontend === "nextjs" || stack.frontend === "react" || stack.frontend === "vue") {
|
|
1161
|
-
templates.frontend = "node-nextjs";
|
|
1162
|
-
}
|
|
1163
|
-
|
|
1164
|
-
// Pure frontend project with no backend
|
|
1165
|
-
if (!templates.backend && templates.frontend) {
|
|
1166
|
-
templates.backend = null;
|
|
1167
|
-
}
|
|
1168
|
-
|
|
1169
|
-
return templates;
|
|
1170
|
-
}
|
|
1171
|
-
|
|
1172
|
-
// ─── Dynamic prompt generation (multi-stack) ──────────────────────
|
|
1173
|
-
function generatePrompts(templates, lang) {
|
|
1174
|
-
const commonDir = path.join(TEMPLATES_DIR, "common");
|
|
1175
|
-
const headerPath = path.join(commonDir, "header.md");
|
|
1176
|
-
const footerPath = path.join(commonDir, "pass3-footer.md");
|
|
1177
|
-
const langPath = path.join(commonDir, "lang-instructions.json");
|
|
1178
|
-
|
|
1179
|
-
const header = fs.existsSync(headerPath) ? fs.readFileSync(headerPath, "utf-8") : "";
|
|
1180
|
-
const footer = fs.existsSync(footerPath) ? fs.readFileSync(footerPath, "utf-8") : "";
|
|
1181
|
-
|
|
1182
|
-
// Load language instruction for Pass 3 (analysis passes stay English)
|
|
1183
|
-
let langInstruction = "";
|
|
1184
|
-
if (lang && lang !== "en" && fs.existsSync(langPath)) {
|
|
1185
|
-
try {
|
|
1186
|
-
const langData = JSON.parse(fs.readFileSync(langPath, "utf-8"));
|
|
1187
|
-
langInstruction = langData.instructions[lang] || "";
|
|
1188
|
-
if (langInstruction) {
|
|
1189
|
-
console.log(` 🌐 Language: ${langData.labels[lang]} (Pass 3 output)`);
|
|
1190
|
-
}
|
|
1191
|
-
} catch (e) {
|
|
1192
|
-
console.warn(` ⚠️ lang-instructions.json parse error: ${e.message}`);
|
|
1193
|
-
}
|
|
1194
|
-
}
|
|
1195
|
-
|
|
1196
|
-
function readTemplate(templateName, passName) {
|
|
1197
|
-
const src = path.join(TEMPLATES_DIR, templateName, `${passName}.md`);
|
|
1198
|
-
if (!fs.existsSync(src)) return null;
|
|
1199
|
-
let body = fs.readFileSync(src, "utf-8");
|
|
1200
|
-
body = body.replace(/^Project root path:.*\nInterpret all file paths.*\n\n?---\n\n?/s, "");
|
|
1201
|
-
body = body.replace(/\nAfter completion, run the following commands in order:[\s\S]*$/m, "");
|
|
1202
|
-
return body;
|
|
1203
|
-
}
|
|
1204
|
-
|
|
1205
|
-
const activeTemplates = [templates.backend, templates.frontend].filter(Boolean);
|
|
1206
|
-
const primaryTemplate = templates.backend || templates.frontend;
|
|
1207
|
-
|
|
1208
|
-
// ─── Pass 1: Separate prompts per type ──────────────────────
|
|
1209
|
-
for (const tmpl of activeTemplates) {
|
|
1210
|
-
const type = tmpl === templates.frontend ? "frontend" : "backend";
|
|
1211
|
-
const body = readTemplate(tmpl, "pass1");
|
|
1212
|
-
if (body) {
|
|
1213
|
-
const dst = path.join(GENERATED_DIR, `pass1-${type}-prompt.md`);
|
|
1214
|
-
fs.writeFileSync(dst, header + body);
|
|
1215
|
-
console.log(` ✅ pass1-${type}-prompt.md (${tmpl})`);
|
|
1216
|
-
}
|
|
1217
|
-
}
|
|
1218
|
-
|
|
1219
|
-
// Single-stack backward compat: also generate pass1-prompt.md (primary-based)
|
|
1220
|
-
if (primaryTemplate) {
|
|
1221
|
-
const body = readTemplate(primaryTemplate, "pass1");
|
|
1222
|
-
if (body) {
|
|
1223
|
-
fs.writeFileSync(path.join(GENERATED_DIR, "pass1-prompt.md"), header + body);
|
|
1224
|
-
}
|
|
1225
|
-
}
|
|
1226
|
-
|
|
1227
|
-
// ─── Pass 2: Single prompt (primary-based, merges all pass1 results) ──
|
|
1228
|
-
if (primaryTemplate) {
|
|
1229
|
-
const body = readTemplate(primaryTemplate, "pass2");
|
|
1230
|
-
if (body) {
|
|
1231
|
-
fs.writeFileSync(path.join(GENERATED_DIR, "pass2-prompt.md"), header + body);
|
|
1232
|
-
console.log(` ✅ pass2-prompt.md (${primaryTemplate})`);
|
|
1233
|
-
}
|
|
1234
|
-
}
|
|
1235
|
-
|
|
1236
|
-
// ─── Pass 3: Combined prompt (backend + frontend merged) ──
|
|
1237
|
-
if (primaryTemplate) {
|
|
1238
|
-
const primaryBody = readTemplate(primaryTemplate, "pass3");
|
|
1239
|
-
let combinedBody = primaryBody || "";
|
|
1240
|
-
|
|
1241
|
-
// Multi-stack: merge frontend-specific sections from frontend pass3 into backend pass3
|
|
1242
|
-
if (templates.backend && templates.frontend && templates.backend !== templates.frontend) {
|
|
1243
|
-
const frontendBody = readTemplate(templates.frontend, "pass3");
|
|
1244
|
-
if (frontendBody) {
|
|
1245
|
-
combinedBody += "\n\n---\n\n";
|
|
1246
|
-
combinedBody += "# Additional: Frontend generation targets (auto-detected)\n\n";
|
|
1247
|
-
combinedBody += "In addition to the backend standards above, also generate the following frontend standards.\n";
|
|
1248
|
-
combinedBody += "Reference the frontend analysis results in pass2-merged.json.\n\n";
|
|
1249
|
-
// Extract only standard sections from frontend pass3 (exclude CLAUDE.md, guide duplicates)
|
|
1250
|
-
const frontendSections = frontendBody
|
|
1251
|
-
.split(/\n(?=\d+\.\s)/)
|
|
1252
|
-
.filter(s => /frontend|component|page|routing|data.fetch|state|styling/i.test(s))
|
|
1253
|
-
.join("\n");
|
|
1254
|
-
if (frontendSections.trim()) {
|
|
1255
|
-
combinedBody += frontendSections;
|
|
1256
|
-
}
|
|
1257
|
-
}
|
|
1258
|
-
}
|
|
1259
|
-
|
|
1260
|
-
const dst = path.join(GENERATED_DIR, "pass3-prompt.md");
|
|
1261
|
-
fs.writeFileSync(dst, header + langInstruction + combinedBody.trimEnd() + "\n" + footer);
|
|
1262
|
-
console.log(` ✅ pass3-prompt.md${templates.frontend && templates.backend ? " (multi-stack combined)" : ""}`);
|
|
1263
|
-
}
|
|
1264
|
-
}
|
|
1265
|
-
|
|
1266
|
-
// ─── Main ───────────────────────────────────────────────
|
|
1267
24
|
async function main() {
|
|
1268
|
-
console.log("\n
|
|
25
|
+
console.log("\n╔═══════════════════════════════════════╗");
|
|
1269
26
|
console.log("║ ClaudeOS-Core — Plan Installer ║");
|
|
1270
|
-
console.log("
|
|
27
|
+
console.log("╚═══════════════════════════════════════╝\n");
|
|
1271
28
|
|
|
1272
29
|
ensureDir(GENERATED_DIR);
|
|
1273
30
|
|
|
1274
31
|
// Phase 1: Stack detection
|
|
1275
32
|
console.log(" [Phase 1] Detecting stack...");
|
|
1276
|
-
const stack = await detectStack();
|
|
33
|
+
const stack = await detectStack(ROOT);
|
|
1277
34
|
console.log(` Language: ${stack.language || "unknown"} ${stack.languageVersion || ""}`);
|
|
1278
35
|
console.log(` Framework: ${stack.framework || "none"} ${stack.frameworkVersion || ""}`);
|
|
1279
36
|
if (!stack.language && !stack.framework) {
|
|
@@ -1288,7 +45,7 @@ async function main() {
|
|
|
1288
45
|
|
|
1289
46
|
// Phase 2: Structure scan
|
|
1290
47
|
console.log(" [Phase 2] Scanning structure...");
|
|
1291
|
-
const { domains, backendDomains, frontendDomains, rootPackage, frontend } = await scanStructure(stack);
|
|
48
|
+
const { domains, backendDomains, frontendDomains, rootPackage, frontend } = await scanStructure(stack, ROOT);
|
|
1292
49
|
console.log(` Backend: ${backendDomains.length} domains`);
|
|
1293
50
|
console.log(` Frontend: ${frontendDomains.length} domains`);
|
|
1294
51
|
console.log(` Total: ${domains.length} domains`);
|
|
@@ -1296,33 +53,24 @@ async function main() {
|
|
|
1296
53
|
if (frontend.exists) console.log(` Components: ${frontend.components} components, ${frontend.pages} pages, ${frontend.hooks} hooks`);
|
|
1297
54
|
if (backendDomains.length === 0 && frontendDomains.length === 0) {
|
|
1298
55
|
console.warn("\n ⚠️ No domains detected.");
|
|
1299
|
-
console.warn(" Pass 1 will be skipped. Generated output may be minimal
|
|
1300
|
-
console.warn(" Check your project structure — see README for supported patterns.\n");
|
|
56
|
+
console.warn(" Pass 1 will be skipped. Generated output may be minimal.\n");
|
|
1301
57
|
}
|
|
1302
58
|
console.log();
|
|
1303
59
|
|
|
1304
|
-
// Phase 3: Template selection
|
|
60
|
+
// Phase 3: Template selection
|
|
1305
61
|
console.log(" [Phase 3] Selecting templates...");
|
|
1306
62
|
const templates = selectTemplates(stack);
|
|
1307
63
|
const isMultiStack = !!(templates.backend && templates.frontend);
|
|
1308
64
|
if (templates.backend) console.log(` Backend: ${templates.backend}`);
|
|
1309
65
|
if (templates.frontend) console.log(` Frontend: ${templates.frontend}`);
|
|
1310
|
-
|
|
1311
|
-
else console.log(` Mode: Single-stack`);
|
|
66
|
+
console.log(` Mode: ${isMultiStack ? "🔀 Multi-stack" : "Single-stack"}`);
|
|
1312
67
|
console.log();
|
|
1313
68
|
|
|
1314
|
-
// Phase 4: Domain group splitting
|
|
69
|
+
// Phase 4: Domain group splitting
|
|
1315
70
|
console.log(" [Phase 4] Splitting domain groups...");
|
|
1316
71
|
const allGroups = [];
|
|
1317
|
-
if (templates.backend && backendDomains.length > 0)
|
|
1318
|
-
|
|
1319
|
-
allGroups.push(...bg);
|
|
1320
|
-
}
|
|
1321
|
-
if (templates.frontend && frontendDomains.length > 0) {
|
|
1322
|
-
const fg = splitDomainGroups(frontendDomains, "frontend", templates.frontend);
|
|
1323
|
-
allGroups.push(...fg);
|
|
1324
|
-
}
|
|
1325
|
-
// Reassign passNum
|
|
72
|
+
if (templates.backend && backendDomains.length > 0) allGroups.push(...splitDomainGroups(backendDomains, "backend", templates.backend));
|
|
73
|
+
if (templates.frontend && frontendDomains.length > 0) allGroups.push(...splitDomainGroups(frontendDomains, "frontend", templates.frontend));
|
|
1326
74
|
allGroups.forEach((g, i) => { g.passNum = i + 1; });
|
|
1327
75
|
allGroups.forEach((g, i) => {
|
|
1328
76
|
const icon = g.type === "backend" ? "⚙️" : "🎨";
|
|
@@ -1333,62 +81,44 @@ async function main() {
|
|
|
1333
81
|
// Phase 5: Active domains
|
|
1334
82
|
console.log(" [Phase 5] Active domains...");
|
|
1335
83
|
const active = determineActiveDomains(stack);
|
|
1336
|
-
Object.entries(active).forEach(([k, v]) => {
|
|
1337
|
-
console.log(` ${v ? "✅" : "⏭️"} ${k}`);
|
|
1338
|
-
});
|
|
84
|
+
Object.entries(active).forEach(([k, v]) => console.log(` ${v ? "✅" : "⏭️"} ${k}`));
|
|
1339
85
|
console.log();
|
|
1340
86
|
|
|
1341
|
-
// Phase 6: Prompt generation
|
|
87
|
+
// Phase 6: Prompt generation
|
|
1342
88
|
const lang = process.env.CLAUDEOS_LANG || "en";
|
|
1343
89
|
console.log(` [Phase 6] Generating prompts (lang: ${lang})...`);
|
|
1344
|
-
generatePrompts(templates, lang);
|
|
90
|
+
generatePrompts(templates, lang, TEMPLATES_DIR, GENERATED_DIR);
|
|
1345
91
|
console.log();
|
|
1346
92
|
|
|
1347
|
-
// Save
|
|
93
|
+
// Save outputs
|
|
1348
94
|
const defaultPort = stack.framework === "fastapi" || stack.framework === "django" ? 8000 : 8080;
|
|
1349
95
|
const analysis = {
|
|
1350
|
-
analyzedAt: new Date().toISOString(),
|
|
1351
|
-
lang,
|
|
96
|
+
analyzedAt: new Date().toISOString(), lang,
|
|
1352
97
|
stack: { ...stack, port: stack.port || defaultPort },
|
|
1353
|
-
templates,
|
|
1354
|
-
|
|
1355
|
-
rootPackage,
|
|
1356
|
-
domains,
|
|
1357
|
-
backendDomains,
|
|
1358
|
-
frontendDomains,
|
|
1359
|
-
frontend,
|
|
98
|
+
templates, isMultiStack, rootPackage,
|
|
99
|
+
domains, backendDomains, frontendDomains, frontend,
|
|
1360
100
|
activeDomains: active,
|
|
1361
101
|
summary: {
|
|
1362
|
-
totalDomains: domains.length,
|
|
1363
|
-
backendDomains: backendDomains.length,
|
|
102
|
+
totalDomains: domains.length, backendDomains: backendDomains.length,
|
|
1364
103
|
frontendDomains: frontendDomains.length,
|
|
1365
104
|
totalFiles: domains.reduce((s, d) => s + d.totalFiles, 0),
|
|
1366
105
|
},
|
|
1367
106
|
};
|
|
1368
|
-
|
|
107
|
+
writeFileSafe(path.join(GENERATED_DIR, "project-analysis.json"), JSON.stringify(analysis, null, 2));
|
|
1369
108
|
console.log(" 💾 project-analysis.json saved");
|
|
1370
109
|
|
|
1371
|
-
// Save: domain-groups.json
|
|
1372
110
|
const domainGroups = {
|
|
1373
|
-
generatedAt: new Date().toISOString(),
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
totalDomains: domains.length,
|
|
1377
|
-
totalGroups: allGroups.length,
|
|
1378
|
-
maxDomainsPerGroup: 4,
|
|
1379
|
-
maxFilesPerGroup: 40,
|
|
1380
|
-
groups: allGroups,
|
|
111
|
+
generatedAt: new Date().toISOString(), isMultiStack, templates,
|
|
112
|
+
totalDomains: domains.length, totalGroups: allGroups.length,
|
|
113
|
+
maxDomainsPerGroup: 4, maxFilesPerGroup: 40, groups: allGroups,
|
|
1381
114
|
};
|
|
1382
|
-
|
|
115
|
+
writeFileSafe(path.join(GENERATED_DIR, "domain-groups.json"), JSON.stringify(domainGroups, null, 2));
|
|
1383
116
|
console.log(" 💾 domain-groups.json saved\n");
|
|
1384
|
-
|
|
1385
117
|
console.log(" ✅ Plan Installer complete\n");
|
|
1386
118
|
}
|
|
1387
119
|
|
|
1388
120
|
main().catch(e => {
|
|
1389
121
|
console.error(`\n ❌ Plan Installer failed: ${e.message || e}`);
|
|
1390
|
-
if (e.code === "EACCES" || e.code === "EPERM")
|
|
1391
|
-
console.error(" Check file/directory permissions.");
|
|
1392
|
-
}
|
|
122
|
+
if (e.code === "EACCES" || e.code === "EPERM") console.error(" Check file/directory permissions.");
|
|
1393
123
|
process.exit(1);
|
|
1394
124
|
});
|