claudeos-core 2.1.1 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/CHANGELOG.md +1649 -481
  2. package/CONTRIBUTING.md +92 -92
  3. package/README.de.md +64 -5
  4. package/README.es.md +64 -5
  5. package/README.fr.md +64 -5
  6. package/README.hi.md +64 -5
  7. package/README.ja.md +64 -5
  8. package/README.ko.md +1018 -959
  9. package/README.md +1020 -960
  10. package/README.ru.md +66 -5
  11. package/README.vi.md +1019 -960
  12. package/README.zh-CN.md +64 -5
  13. package/bin/cli.js +152 -148
  14. package/bin/commands/init.js +1673 -1518
  15. package/bin/commands/lint.js +62 -0
  16. package/bin/commands/memory.js +438 -438
  17. package/bin/lib/cli-utils.js +206 -206
  18. package/claude-md-validator/index.js +184 -0
  19. package/claude-md-validator/reporter.js +66 -0
  20. package/claude-md-validator/structural-checks.js +528 -0
  21. package/content-validator/index.js +666 -436
  22. package/lib/env-parser.js +317 -0
  23. package/lib/expected-guides.js +23 -23
  24. package/lib/expected-outputs.js +90 -90
  25. package/lib/language-config.js +35 -35
  26. package/lib/memory-scaffold.js +1058 -1052
  27. package/lib/plan-parser.js +165 -165
  28. package/lib/staged-rules.js +118 -118
  29. package/manifest-generator/index.js +174 -174
  30. package/package.json +90 -87
  31. package/pass-json-validator/index.js +337 -337
  32. package/pass-prompts/templates/angular/pass3.md +28 -13
  33. package/pass-prompts/templates/common/claude-md-scaffold.md +686 -0
  34. package/pass-prompts/templates/common/pass3-footer.md +402 -39
  35. package/pass-prompts/templates/common/pass3b-core-header.md +43 -0
  36. package/pass-prompts/templates/common/pass4.md +375 -302
  37. package/pass-prompts/templates/common/staging-override.md +26 -26
  38. package/pass-prompts/templates/java-spring/pass3.md +31 -21
  39. package/pass-prompts/templates/kotlin-spring/pass3.md +34 -22
  40. package/pass-prompts/templates/node-express/pass3.md +30 -21
  41. package/pass-prompts/templates/node-fastify/pass3.md +28 -14
  42. package/pass-prompts/templates/node-nestjs/pass3.md +29 -14
  43. package/pass-prompts/templates/node-nextjs/pass3.md +34 -21
  44. package/pass-prompts/templates/node-vite/pass1.md +117 -117
  45. package/pass-prompts/templates/node-vite/pass2.md +78 -78
  46. package/pass-prompts/templates/node-vite/pass3.md +30 -13
  47. package/pass-prompts/templates/python-django/pass3.md +32 -21
  48. package/pass-prompts/templates/python-fastapi/pass3.md +33 -21
  49. package/pass-prompts/templates/python-flask/pass1.md +119 -119
  50. package/pass-prompts/templates/python-flask/pass2.md +85 -85
  51. package/pass-prompts/templates/python-flask/pass3.md +31 -13
  52. package/pass-prompts/templates/vue-nuxt/pass3.md +32 -13
  53. package/plan-installer/domain-grouper.js +76 -76
  54. package/plan-installer/index.js +137 -129
  55. package/plan-installer/prompt-generator.js +188 -128
  56. package/plan-installer/scanners/scan-frontend.js +505 -473
  57. package/plan-installer/scanners/scan-java.js +226 -226
  58. package/plan-installer/scanners/scan-node.js +57 -57
  59. package/plan-installer/scanners/scan-python.js +85 -85
  60. package/plan-installer/stack-detector.js +482 -466
  61. package/plan-installer/structure-scanner.js +65 -65
  62. package/sync-checker/index.js +177 -177
@@ -1,76 +1,76 @@
1
- /**
2
- * ClaudeOS-Core — Domain Grouper
3
- *
4
- * Splits domains into analysis groups, determines active domains,
5
- * and selects appropriate templates based on detected stack.
6
- */
7
-
8
- function splitDomainGroups(domains, type, template) {
9
- const MAX_FILES_PER_GROUP = 40;
10
- const MAX_DOMAINS_PER_GROUP = 4;
11
- const groups = [];
12
- let current = [];
13
- let fileCount = 0;
14
-
15
- for (const d of domains) {
16
- // Flush current group before adding if it would exceed limits
17
- if (current.length > 0 && (fileCount + d.totalFiles > MAX_FILES_PER_GROUP || current.length >= MAX_DOMAINS_PER_GROUP)) {
18
- groups.push({ type, template, domains: [...current], estimatedFiles: fileCount });
19
- current = [];
20
- fileCount = 0;
21
- }
22
- current.push(d.name);
23
- fileCount += d.totalFiles;
24
- }
25
- if (current.length > 0) {
26
- groups.push({ type, template, domains: [...current], estimatedFiles: fileCount });
27
- }
28
-
29
- return groups;
30
- }
31
-
32
- // ─── Determine active domains ───────────────────────────────────
33
- function determineActiveDomains(stack) {
34
- const isBackend = !!stack.framework && stack.framework !== "vite";
35
- return {
36
- "00.core": true,
37
- "10.backend": !!isBackend,
38
- "20.frontend": !!stack.frontend,
39
- "30.security-db": !!(stack.database || isBackend || stack.frontend),
40
- "40.infra": true,
41
- "50.verification": true,
42
- "90.optional": true,
43
- };
44
- }
45
-
46
- // ─── Template selection (multi-stack) ──────────────────────────────
47
- function selectTemplates(stack) {
48
- const templates = { backend: null, frontend: null };
49
-
50
- // Backend template (requires a backend framework; language-only fallback skipped for pure frontend projects)
51
- if (stack.language === "kotlin") templates.backend = "kotlin-spring";
52
- else if (stack.language === "java") templates.backend = "java-spring";
53
- else if (stack.framework === "nestjs") templates.backend = "node-nestjs";
54
- else if (stack.framework === "express") templates.backend = "node-express";
55
- else if (stack.framework === "fastify") templates.backend = "node-fastify";
56
- else if (stack.framework === "django") templates.backend = "python-django";
57
- else if (stack.framework === "fastapi") templates.backend = "python-fastapi";
58
- else if (stack.framework === "flask") templates.backend = "python-flask";
59
- else if ((stack.language === "typescript" || stack.language === "javascript") && stack.framework && stack.framework !== "vite") templates.backend = "node-express";
60
- else if (stack.language === "python" && stack.framework) templates.backend = "python-fastapi";
61
-
62
- // Frontend template
63
- if (stack.frontend === "nextjs") {
64
- templates.frontend = "node-nextjs";
65
- } else if (stack.frontend === "react") {
66
- templates.frontend = stack.framework === "vite" ? "node-vite" : "node-nextjs";
67
- } else if (stack.frontend === "vue") {
68
- templates.frontend = "vue-nuxt";
69
- } else if (stack.frontend === "angular") {
70
- templates.frontend = "angular";
71
- }
72
-
73
- return templates;
74
- }
75
-
76
- module.exports = { splitDomainGroups, determineActiveDomains, selectTemplates };
1
+ /**
2
+ * ClaudeOS-Core — Domain Grouper
3
+ *
4
+ * Splits domains into analysis groups, determines active domains,
5
+ * and selects appropriate templates based on detected stack.
6
+ */
7
+
8
+ function splitDomainGroups(domains, type, template) {
9
+ const MAX_FILES_PER_GROUP = 40;
10
+ const MAX_DOMAINS_PER_GROUP = 4;
11
+ const groups = [];
12
+ let current = [];
13
+ let fileCount = 0;
14
+
15
+ for (const d of domains) {
16
+ // Flush current group before adding if it would exceed limits
17
+ if (current.length > 0 && (fileCount + d.totalFiles > MAX_FILES_PER_GROUP || current.length >= MAX_DOMAINS_PER_GROUP)) {
18
+ groups.push({ type, template, domains: [...current], estimatedFiles: fileCount });
19
+ current = [];
20
+ fileCount = 0;
21
+ }
22
+ current.push(d.name);
23
+ fileCount += d.totalFiles;
24
+ }
25
+ if (current.length > 0) {
26
+ groups.push({ type, template, domains: [...current], estimatedFiles: fileCount });
27
+ }
28
+
29
+ return groups;
30
+ }
31
+
32
+ // ─── Determine active domains ───────────────────────────────────
33
+ function determineActiveDomains(stack) {
34
+ const isBackend = !!stack.framework && stack.framework !== "vite";
35
+ return {
36
+ "00.core": true,
37
+ "10.backend": !!isBackend,
38
+ "20.frontend": !!stack.frontend,
39
+ "30.security-db": !!(stack.database || isBackend || stack.frontend),
40
+ "40.infra": true,
41
+ "50.verification": true,
42
+ "90.optional": true,
43
+ };
44
+ }
45
+
46
+ // ─── Template selection (multi-stack) ──────────────────────────────
47
+ function selectTemplates(stack) {
48
+ const templates = { backend: null, frontend: null };
49
+
50
+ // Backend template (requires a backend framework; language-only fallback skipped for pure frontend projects)
51
+ if (stack.language === "kotlin") templates.backend = "kotlin-spring";
52
+ else if (stack.language === "java") templates.backend = "java-spring";
53
+ else if (stack.framework === "nestjs") templates.backend = "node-nestjs";
54
+ else if (stack.framework === "express") templates.backend = "node-express";
55
+ else if (stack.framework === "fastify") templates.backend = "node-fastify";
56
+ else if (stack.framework === "django") templates.backend = "python-django";
57
+ else if (stack.framework === "fastapi") templates.backend = "python-fastapi";
58
+ else if (stack.framework === "flask") templates.backend = "python-flask";
59
+ else if ((stack.language === "typescript" || stack.language === "javascript") && stack.framework && stack.framework !== "vite") templates.backend = "node-express";
60
+ else if (stack.language === "python" && stack.framework) templates.backend = "python-fastapi";
61
+
62
+ // Frontend template
63
+ if (stack.frontend === "nextjs") {
64
+ templates.frontend = "node-nextjs";
65
+ } else if (stack.frontend === "react") {
66
+ templates.frontend = stack.framework === "vite" ? "node-vite" : "node-nextjs";
67
+ } else if (stack.frontend === "vue") {
68
+ templates.frontend = "vue-nuxt";
69
+ } else if (stack.frontend === "angular") {
70
+ templates.frontend = "angular";
71
+ }
72
+
73
+ return templates;
74
+ }
75
+
76
+ module.exports = { splitDomainGroups, determineActiveDomains, selectTemplates };
@@ -1,129 +1,137 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * ClaudeOS-Core — plan-installer (orchestrator)
5
- *
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()
11
- */
12
-
13
- const path = require("path");
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");
19
-
20
- const ROOT = process.env.CLAUDEOS_ROOT || path.resolve(__dirname, "../..");
21
- const GENERATED_DIR = path.join(ROOT, "claudeos-core/generated");
22
- const TEMPLATES_DIR = path.join(__dirname, "../pass-prompts/templates");
23
-
24
- async function main() {
25
- console.log("\n╔═══════════════════════════════════════╗");
26
- console.log("║ ClaudeOS-Core — Plan Installer ║");
27
- console.log("╚═══════════════════════════════════════╝\n");
28
-
29
- ensureDir(GENERATED_DIR);
30
-
31
- // Phase 1: Stack detection
32
- console.log(" [Phase 1] Detecting stack...");
33
- const stack = await detectStack(ROOT);
34
- console.log(` Language: ${stack.language || "unknown"} ${stack.languageVersion || ""}`);
35
- console.log(` Framework: ${stack.framework || "none"} ${stack.frameworkVersion || ""}`);
36
- if (!stack.language && !stack.framework) {
37
- console.warn("\n ⚠️ No language or framework detected.");
38
- console.warn(" Supported: Java, Kotlin, TypeScript, JavaScript, Python");
39
- console.warn(" Ensure you have build.gradle, package.json, pyproject.toml, or requirements.txt in the project root.\n");
40
- }
41
- console.log(` Frontend: ${stack.frontend || "none"} ${stack.frontendVersion || ""}`);
42
- console.log(` Database: ${stack.database || "none"}`);
43
- console.log(` ORM: ${stack.orm || "none"}`);
44
- console.log(` PackageMgr: ${stack.packageManager || "none"}\n`);
45
-
46
- // Phase 2: Structure scan
47
- console.log(" [Phase 2] Scanning structure...");
48
- const { domains, backendDomains, frontendDomains, rootPackage, frontend } = await scanStructure(stack, ROOT);
49
- console.log(` Backend: ${backendDomains.length} domains`);
50
- console.log(` Frontend: ${frontendDomains.length} domains`);
51
- console.log(` Total: ${domains.length} domains`);
52
- if (rootPackage) console.log(` Package: ${rootPackage}`);
53
- if (frontend.exists) console.log(` Components: ${frontend.components} components, ${frontend.pages} pages, ${frontend.hooks} hooks`);
54
- if (backendDomains.length === 0 && frontendDomains.length === 0) {
55
- console.warn("\n ⚠️ No domains detected.");
56
- console.warn(" Pass 1 will be skipped. Generated output may be minimal.\n");
57
- }
58
- console.log();
59
-
60
- // Phase 3: Template selection
61
- console.log(" [Phase 3] Selecting templates...");
62
- const templates = selectTemplates(stack);
63
- const isMultiStack = !!(templates.backend && templates.frontend);
64
- if (templates.backend) console.log(` Backend: ${templates.backend}`);
65
- if (templates.frontend) console.log(` Frontend: ${templates.frontend}`);
66
- console.log(` Mode: ${isMultiStack ? "🔀 Multi-stack" : "Single-stack"}`);
67
- console.log();
68
-
69
- // Phase 4: Domain group splitting
70
- console.log(" [Phase 4] Splitting domain groups...");
71
- const allGroups = [];
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));
74
- allGroups.forEach((g, i) => { g.passNum = i + 1; });
75
- allGroups.forEach((g, i) => {
76
- const icon = g.type === "backend" ? "⚙️" : "🎨";
77
- console.log(` ${icon} Group ${i + 1}: [${g.domains.join(", ")}] (${g.type}, ~${g.estimatedFiles} files)`);
78
- });
79
- console.log();
80
-
81
- // Phase 5: Active domains
82
- console.log(" [Phase 5] Active domains...");
83
- const active = determineActiveDomains(stack);
84
- Object.entries(active).forEach(([k, v]) => console.log(` ${v ? "✅" : "⏭️"} ${k}`));
85
- console.log();
86
-
87
- // Phase 6: Prompt generation
88
- const lang = process.env.CLAUDEOS_LANG || "en";
89
- console.log(` [Phase 6] Generating prompts (lang: ${lang})...`);
90
- generatePrompts(templates, lang, TEMPLATES_DIR, GENERATED_DIR);
91
- console.log();
92
-
93
- // Save outputs
94
- const defaultPort = (stack.framework === "fastapi" || stack.framework === "django") ? 8000
95
- : stack.framework === "flask" ? 5000
96
- : stack.framework === "vite" ? 5173
97
- : stack.frontend === "angular" ? 4200
98
- : stack.frontend === "nextjs" ? 3000
99
- : (stack.framework === "express" || stack.framework === "nestjs" || stack.framework === "fastify") ? 3000 : 8080;
100
- const analysis = {
101
- analyzedAt: new Date().toISOString(), lang,
102
- stack: { ...stack, port: stack.port || defaultPort },
103
- templates, isMultiStack, rootPackage,
104
- domains, backendDomains, frontendDomains, frontend,
105
- activeDomains: active,
106
- summary: {
107
- totalDomains: domains.length, backendDomains: backendDomains.length,
108
- frontendDomains: frontendDomains.length,
109
- totalFiles: domains.reduce((s, d) => s + d.totalFiles, 0),
110
- },
111
- };
112
- writeFileSafe(path.join(GENERATED_DIR, "project-analysis.json"), JSON.stringify(analysis, null, 2));
113
- console.log(" 💾 project-analysis.json saved");
114
-
115
- const domainGroups = {
116
- generatedAt: new Date().toISOString(), isMultiStack, templates,
117
- totalDomains: domains.length, totalGroups: allGroups.length,
118
- maxDomainsPerGroup: 4, maxFilesPerGroup: 40, groups: allGroups,
119
- };
120
- writeFileSafe(path.join(GENERATED_DIR, "domain-groups.json"), JSON.stringify(domainGroups, null, 2));
121
- console.log(" 💾 domain-groups.json saved\n");
122
- console.log(" ✅ Plan Installer complete\n");
123
- }
124
-
125
- main().catch(e => {
126
- console.error(`\n ❌ Plan Installer failed: ${e.message || e}`);
127
- if (e.code === "EACCES" || e.code === "EPERM") console.error(" Check file/directory permissions.");
128
- process.exit(1);
129
- });
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * ClaudeOS-Core — plan-installer (orchestrator)
5
+ *
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()
11
+ */
12
+
13
+ const path = require("path");
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");
19
+
20
+ const ROOT = process.env.CLAUDEOS_ROOT || path.resolve(__dirname, "../..");
21
+ const GENERATED_DIR = path.join(ROOT, "claudeos-core/generated");
22
+ const TEMPLATES_DIR = path.join(__dirname, "../pass-prompts/templates");
23
+
24
+ async function main() {
25
+ console.log("\n╔═══════════════════════════════════════╗");
26
+ console.log("║ ClaudeOS-Core — Plan Installer ║");
27
+ console.log("╚═══════════════════════════════════════╝\n");
28
+
29
+ ensureDir(GENERATED_DIR);
30
+
31
+ // Phase 1: Stack detection
32
+ console.log(" [Phase 1] Detecting stack...");
33
+ const stack = await detectStack(ROOT);
34
+ console.log(` Language: ${stack.language || "unknown"} ${stack.languageVersion || ""}`);
35
+ console.log(` Framework: ${stack.framework || "none"} ${stack.frameworkVersion || ""}`);
36
+ if (!stack.language && !stack.framework) {
37
+ console.warn("\n ⚠️ No language or framework detected.");
38
+ console.warn(" Supported: Java, Kotlin, TypeScript, JavaScript, Python");
39
+ console.warn(" Ensure you have build.gradle, package.json, pyproject.toml, or requirements.txt in the project root.\n");
40
+ }
41
+ console.log(` Frontend: ${stack.frontend || "none"} ${stack.frontendVersion || ""}`);
42
+ console.log(` Database: ${stack.database || "none"}`);
43
+ console.log(` ORM: ${stack.orm || "none"}`);
44
+ console.log(` PackageMgr: ${stack.packageManager || "none"}\n`);
45
+
46
+ // Phase 2: Structure scan
47
+ console.log(" [Phase 2] Scanning structure...");
48
+ const { domains, backendDomains, frontendDomains, rootPackage, frontend } = await scanStructure(stack, ROOT);
49
+ console.log(` Backend: ${backendDomains.length} domains`);
50
+ console.log(` Frontend: ${frontendDomains.length} domains`);
51
+ console.log(` Total: ${domains.length} domains`);
52
+ if (rootPackage) console.log(` Package: ${rootPackage}`);
53
+ if (frontend.exists) console.log(` Components: ${frontend.components} components, ${frontend.pages} pages, ${frontend.hooks} hooks`);
54
+ if (backendDomains.length === 0 && frontendDomains.length === 0) {
55
+ console.warn("\n ⚠️ No domains detected.");
56
+ console.warn(" Pass 1 will be skipped. Generated output may be minimal.\n");
57
+ }
58
+ console.log();
59
+
60
+ // Phase 3: Template selection
61
+ console.log(" [Phase 3] Selecting templates...");
62
+ const templates = selectTemplates(stack);
63
+ const isMultiStack = !!(templates.backend && templates.frontend);
64
+ if (templates.backend) console.log(` Backend: ${templates.backend}`);
65
+ if (templates.frontend) console.log(` Frontend: ${templates.frontend}`);
66
+ console.log(` Mode: ${isMultiStack ? "🔀 Multi-stack" : "Single-stack"}`);
67
+ console.log();
68
+
69
+ // Phase 4: Domain group splitting
70
+ console.log(" [Phase 4] Splitting domain groups...");
71
+ const allGroups = [];
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));
74
+ allGroups.forEach((g, i) => { g.passNum = i + 1; });
75
+ allGroups.forEach((g, i) => {
76
+ const icon = g.type === "backend" ? "⚙️" : "🎨";
77
+ console.log(` ${icon} Group ${i + 1}: [${g.domains.join(", ")}] (${g.type}, ~${g.estimatedFiles} files)`);
78
+ });
79
+ console.log();
80
+
81
+ // Phase 5: Active domains
82
+ console.log(" [Phase 5] Active domains...");
83
+ const active = determineActiveDomains(stack);
84
+ Object.entries(active).forEach(([k, v]) => console.log(` ${v ? "✅" : "⏭️"} ${k}`));
85
+ console.log();
86
+
87
+ // Phase 6: Prompt generation
88
+ const lang = process.env.CLAUDEOS_LANG || "en";
89
+ console.log(` [Phase 6] Generating prompts (lang: ${lang})...`);
90
+ generatePrompts(templates, lang, TEMPLATES_DIR, GENERATED_DIR);
91
+ console.log();
92
+
93
+ // Save outputs
94
+ //
95
+ // Port resolution precedence (stack.port):
96
+ // 1. stack.port already set by stack-detector (Spring application.yml
97
+ // server.port, or .env file PORT variable) — highest authority.
98
+ // 2. defaultPort fallback below — framework convention, only used when
99
+ // the project declares no port of its own. This is a last-resort
100
+ // default; prefer that stack-detector extract it from .env.example
101
+ // to keep CLAUDE.md truthful to what the project actually runs.
102
+ const defaultPort = (stack.framework === "fastapi" || stack.framework === "django") ? 8000
103
+ : stack.framework === "flask" ? 5000
104
+ : stack.framework === "vite" ? 5173
105
+ : stack.frontend === "angular" ? 4200
106
+ : stack.frontend === "nextjs" ? 3000
107
+ : (stack.framework === "express" || stack.framework === "nestjs" || stack.framework === "fastify") ? 3000 : 8080;
108
+ const analysis = {
109
+ analyzedAt: new Date().toISOString(), lang,
110
+ stack: { ...stack, port: stack.port || defaultPort },
111
+ templates, isMultiStack, rootPackage,
112
+ domains, backendDomains, frontendDomains, frontend,
113
+ activeDomains: active,
114
+ summary: {
115
+ totalDomains: domains.length, backendDomains: backendDomains.length,
116
+ frontendDomains: frontendDomains.length,
117
+ totalFiles: domains.reduce((s, d) => s + d.totalFiles, 0),
118
+ },
119
+ };
120
+ writeFileSafe(path.join(GENERATED_DIR, "project-analysis.json"), JSON.stringify(analysis, null, 2));
121
+ console.log(" 💾 project-analysis.json saved");
122
+
123
+ const domainGroups = {
124
+ generatedAt: new Date().toISOString(), isMultiStack, templates,
125
+ totalDomains: domains.length, totalGroups: allGroups.length,
126
+ maxDomainsPerGroup: 4, maxFilesPerGroup: 40, groups: allGroups,
127
+ };
128
+ writeFileSafe(path.join(GENERATED_DIR, "domain-groups.json"), JSON.stringify(domainGroups, null, 2));
129
+ console.log(" 💾 domain-groups.json saved\n");
130
+ console.log(" ✅ Plan Installer complete\n");
131
+ }
132
+
133
+ main().catch(e => {
134
+ console.error(`\n ❌ Plan Installer failed: ${e.message || e}`);
135
+ if (e.code === "EACCES" || e.code === "EPERM") console.error(" Check file/directory permissions.");
136
+ process.exit(1);
137
+ });