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.

Files changed (44) hide show
  1. package/CHANGELOG.md +76 -0
  2. package/README.de.md +50 -10
  3. package/README.es.md +51 -10
  4. package/README.fr.md +51 -10
  5. package/README.hi.md +51 -10
  6. package/README.ja.md +52 -10
  7. package/README.ko.md +51 -10
  8. package/README.md +62 -15
  9. package/README.ru.md +51 -10
  10. package/README.vi.md +51 -10
  11. package/README.zh-CN.md +51 -10
  12. package/bin/cli.js +171 -36
  13. package/bootstrap.sh +71 -23
  14. package/content-validator/index.js +16 -13
  15. package/health-checker/index.js +4 -3
  16. package/lib/safe-fs.js +110 -0
  17. package/manifest-generator/index.js +13 -7
  18. package/package.json +4 -2
  19. package/pass-json-validator/index.js +3 -5
  20. package/pass-prompts/templates/java-spring/pass1.md +4 -1
  21. package/pass-prompts/templates/java-spring/pass2.md +3 -3
  22. package/pass-prompts/templates/java-spring/pass3.md +42 -5
  23. package/pass-prompts/templates/kotlin-spring/pass1.md +4 -1
  24. package/pass-prompts/templates/kotlin-spring/pass2.md +5 -5
  25. package/pass-prompts/templates/kotlin-spring/pass3.md +42 -5
  26. package/pass-prompts/templates/node-express/pass1.md +4 -1
  27. package/pass-prompts/templates/node-express/pass2.md +4 -1
  28. package/pass-prompts/templates/node-express/pass3.md +44 -6
  29. package/pass-prompts/templates/node-nextjs/pass1.md +14 -4
  30. package/pass-prompts/templates/node-nextjs/pass2.md +6 -4
  31. package/pass-prompts/templates/node-nextjs/pass3.md +45 -6
  32. package/pass-prompts/templates/python-django/pass1.md +4 -2
  33. package/pass-prompts/templates/python-django/pass2.md +4 -4
  34. package/pass-prompts/templates/python-django/pass3.md +42 -5
  35. package/pass-prompts/templates/python-fastapi/pass1.md +4 -1
  36. package/pass-prompts/templates/python-fastapi/pass2.md +4 -4
  37. package/pass-prompts/templates/python-fastapi/pass3.md +42 -5
  38. package/plan-installer/domain-grouper.js +74 -0
  39. package/plan-installer/index.js +35 -1305
  40. package/plan-installer/prompt-generator.js +94 -0
  41. package/plan-installer/stack-detector.js +326 -0
  42. package/plan-installer/structure-scanner.js +783 -0
  43. package/plan-validator/index.js +84 -20
  44. package/sync-checker/index.js +7 -3
@@ -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
- * 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
- * - 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 { glob } = require("glob");
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("╚══════════════════════════════════════╝\n");
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 (multi-stack)
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
- if (isMultiStack) console.log(` Mode: 🔀 Multi-stack`);
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 (per type)
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
- const bg = splitDomainGroups(backendDomains, "backend", templates.backend);
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 (multi-stack)
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: project-analysis.json
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
- isMultiStack,
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
- fs.writeFileSync(path.join(GENERATED_DIR, "project-analysis.json"), JSON.stringify(analysis, null, 2));
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
- isMultiStack,
1375
- templates,
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
- fs.writeFileSync(path.join(GENERATED_DIR, "domain-groups.json"), JSON.stringify(domainGroups, null, 2));
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
  });