@stackweld/core 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (172) hide show
  1. package/.turbo/turbo-build.log +4 -0
  2. package/.turbo/turbo-lint.log +498 -0
  3. package/.turbo/turbo-test.log +21 -0
  4. package/.turbo/turbo-typecheck.log +4 -0
  5. package/dist/__tests__/compatibility-scorer.test.d.ts +2 -0
  6. package/dist/__tests__/compatibility-scorer.test.d.ts.map +1 -0
  7. package/dist/__tests__/compatibility-scorer.test.js +226 -0
  8. package/dist/__tests__/compatibility-scorer.test.js.map +1 -0
  9. package/dist/__tests__/rules-engine.test.d.ts +2 -0
  10. package/dist/__tests__/rules-engine.test.d.ts.map +1 -0
  11. package/dist/__tests__/rules-engine.test.js +161 -0
  12. package/dist/__tests__/rules-engine.test.js.map +1 -0
  13. package/dist/__tests__/scaffold-orchestrator.test.d.ts +2 -0
  14. package/dist/__tests__/scaffold-orchestrator.test.d.ts.map +1 -0
  15. package/dist/__tests__/scaffold-orchestrator.test.js +149 -0
  16. package/dist/__tests__/scaffold-orchestrator.test.js.map +1 -0
  17. package/dist/__tests__/stack-engine.test.d.ts +2 -0
  18. package/dist/__tests__/stack-engine.test.d.ts.map +1 -0
  19. package/dist/__tests__/stack-engine.test.js +278 -0
  20. package/dist/__tests__/stack-engine.test.js.map +1 -0
  21. package/dist/db/database.d.ts +9 -0
  22. package/dist/db/database.d.ts.map +1 -0
  23. package/dist/db/database.js +106 -0
  24. package/dist/db/database.js.map +1 -0
  25. package/dist/db/index.d.ts +2 -0
  26. package/dist/db/index.d.ts.map +1 -0
  27. package/dist/db/index.js +2 -0
  28. package/dist/db/index.js.map +1 -0
  29. package/dist/engine/compatibility-scorer.d.ts +37 -0
  30. package/dist/engine/compatibility-scorer.d.ts.map +1 -0
  31. package/dist/engine/compatibility-scorer.js +178 -0
  32. package/dist/engine/compatibility-scorer.js.map +1 -0
  33. package/dist/engine/compose-generator.d.ts +35 -0
  34. package/dist/engine/compose-generator.d.ts.map +1 -0
  35. package/dist/engine/compose-generator.js +95 -0
  36. package/dist/engine/compose-generator.js.map +1 -0
  37. package/dist/engine/cost-estimator.d.ts +22 -0
  38. package/dist/engine/cost-estimator.d.ts.map +1 -0
  39. package/dist/engine/cost-estimator.js +451 -0
  40. package/dist/engine/cost-estimator.js.map +1 -0
  41. package/dist/engine/env-analyzer.d.ts +36 -0
  42. package/dist/engine/env-analyzer.d.ts.map +1 -0
  43. package/dist/engine/env-analyzer.js +111 -0
  44. package/dist/engine/env-analyzer.js.map +1 -0
  45. package/dist/engine/health-checker.d.ts +20 -0
  46. package/dist/engine/health-checker.d.ts.map +1 -0
  47. package/dist/engine/health-checker.js +377 -0
  48. package/dist/engine/health-checker.js.map +1 -0
  49. package/dist/engine/index.d.ts +11 -0
  50. package/dist/engine/index.d.ts.map +1 -0
  51. package/dist/engine/index.js +7 -0
  52. package/dist/engine/index.js.map +1 -0
  53. package/dist/engine/infra-generator.d.ts +26 -0
  54. package/dist/engine/infra-generator.d.ts.map +1 -0
  55. package/dist/engine/infra-generator.js +751 -0
  56. package/dist/engine/infra-generator.js.map +1 -0
  57. package/dist/engine/migration-planner.d.ts +34 -0
  58. package/dist/engine/migration-planner.d.ts.map +1 -0
  59. package/dist/engine/migration-planner.js +427 -0
  60. package/dist/engine/migration-planner.js.map +1 -0
  61. package/dist/engine/performance-profiler.d.ts +22 -0
  62. package/dist/engine/performance-profiler.d.ts.map +1 -0
  63. package/dist/engine/performance-profiler.js +292 -0
  64. package/dist/engine/performance-profiler.js.map +1 -0
  65. package/dist/engine/plugin-loader.d.ts +36 -0
  66. package/dist/engine/plugin-loader.d.ts.map +1 -0
  67. package/dist/engine/plugin-loader.js +157 -0
  68. package/dist/engine/plugin-loader.js.map +1 -0
  69. package/dist/engine/preferences.d.ts +24 -0
  70. package/dist/engine/preferences.d.ts.map +1 -0
  71. package/dist/engine/preferences.js +62 -0
  72. package/dist/engine/preferences.js.map +1 -0
  73. package/dist/engine/rules-engine.d.ts +31 -0
  74. package/dist/engine/rules-engine.d.ts.map +1 -0
  75. package/dist/engine/rules-engine.js +179 -0
  76. package/dist/engine/rules-engine.js.map +1 -0
  77. package/dist/engine/runtime-manager.d.ts +65 -0
  78. package/dist/engine/runtime-manager.d.ts.map +1 -0
  79. package/dist/engine/runtime-manager.js +181 -0
  80. package/dist/engine/runtime-manager.js.map +1 -0
  81. package/dist/engine/scaffold-orchestrator.d.ts +103 -0
  82. package/dist/engine/scaffold-orchestrator.d.ts.map +1 -0
  83. package/dist/engine/scaffold-orchestrator.js +934 -0
  84. package/dist/engine/scaffold-orchestrator.js.map +1 -0
  85. package/dist/engine/stack-detector.d.ts +21 -0
  86. package/dist/engine/stack-detector.d.ts.map +1 -0
  87. package/dist/engine/stack-detector.js +313 -0
  88. package/dist/engine/stack-detector.js.map +1 -0
  89. package/dist/engine/stack-differ.d.ts +26 -0
  90. package/dist/engine/stack-differ.d.ts.map +1 -0
  91. package/dist/engine/stack-differ.js +80 -0
  92. package/dist/engine/stack-differ.js.map +1 -0
  93. package/dist/engine/stack-engine.d.ts +54 -0
  94. package/dist/engine/stack-engine.d.ts.map +1 -0
  95. package/dist/engine/stack-engine.js +186 -0
  96. package/dist/engine/stack-engine.js.map +1 -0
  97. package/dist/engine/stack-serializer.d.ts +32 -0
  98. package/dist/engine/stack-serializer.d.ts.map +1 -0
  99. package/dist/engine/stack-serializer.js +75 -0
  100. package/dist/engine/stack-serializer.js.map +1 -0
  101. package/dist/engine/standards-linter.d.ts +34 -0
  102. package/dist/engine/standards-linter.d.ts.map +1 -0
  103. package/dist/engine/standards-linter.js +162 -0
  104. package/dist/engine/standards-linter.js.map +1 -0
  105. package/dist/engine/tech-installer.d.ts +37 -0
  106. package/dist/engine/tech-installer.d.ts.map +1 -0
  107. package/dist/engine/tech-installer.js +508 -0
  108. package/dist/engine/tech-installer.js.map +1 -0
  109. package/dist/index.d.ts +39 -0
  110. package/dist/index.d.ts.map +1 -0
  111. package/dist/index.js +25 -0
  112. package/dist/index.js.map +1 -0
  113. package/dist/types/index.d.ts +6 -0
  114. package/dist/types/index.d.ts.map +1 -0
  115. package/dist/types/index.js +2 -0
  116. package/dist/types/index.js.map +1 -0
  117. package/dist/types/project.d.ts +33 -0
  118. package/dist/types/project.d.ts.map +1 -0
  119. package/dist/types/project.js +6 -0
  120. package/dist/types/project.js.map +1 -0
  121. package/dist/types/stack.d.ts +29 -0
  122. package/dist/types/stack.d.ts.map +1 -0
  123. package/dist/types/stack.js +6 -0
  124. package/dist/types/stack.js.map +1 -0
  125. package/dist/types/technology.d.ts +47 -0
  126. package/dist/types/technology.d.ts.map +1 -0
  127. package/dist/types/technology.js +6 -0
  128. package/dist/types/technology.js.map +1 -0
  129. package/dist/types/template.d.ts +34 -0
  130. package/dist/types/template.d.ts.map +1 -0
  131. package/dist/types/template.js +6 -0
  132. package/dist/types/template.js.map +1 -0
  133. package/dist/types/validation.d.ts +20 -0
  134. package/dist/types/validation.d.ts.map +1 -0
  135. package/dist/types/validation.js +5 -0
  136. package/dist/types/validation.js.map +1 -0
  137. package/package.json +39 -0
  138. package/src/__tests__/compatibility-scorer.test.ts +264 -0
  139. package/src/__tests__/rules-engine.test.ts +170 -0
  140. package/src/__tests__/scaffold-orchestrator.test.ts +161 -0
  141. package/src/__tests__/stack-engine.test.ts +328 -0
  142. package/src/db/database.ts +112 -0
  143. package/src/db/index.ts +1 -0
  144. package/src/engine/compatibility-scorer.ts +222 -0
  145. package/src/engine/compose-generator.ts +134 -0
  146. package/src/engine/cost-estimator.ts +498 -0
  147. package/src/engine/env-analyzer.ts +156 -0
  148. package/src/engine/health-checker.ts +421 -0
  149. package/src/engine/index.ts +17 -0
  150. package/src/engine/infra-generator.ts +837 -0
  151. package/src/engine/migration-planner.ts +496 -0
  152. package/src/engine/performance-profiler.ts +354 -0
  153. package/src/engine/plugin-loader.ts +216 -0
  154. package/src/engine/preferences.ts +85 -0
  155. package/src/engine/rules-engine.ts +204 -0
  156. package/src/engine/runtime-manager.ts +207 -0
  157. package/src/engine/scaffold-orchestrator.ts +1052 -0
  158. package/src/engine/stack-detector.ts +345 -0
  159. package/src/engine/stack-differ.ts +118 -0
  160. package/src/engine/stack-engine.ts +258 -0
  161. package/src/engine/stack-serializer.ts +95 -0
  162. package/src/engine/standards-linter.ts +210 -0
  163. package/src/engine/tech-installer.ts +650 -0
  164. package/src/index.ts +78 -0
  165. package/src/types/index.ts +10 -0
  166. package/src/types/project.ts +36 -0
  167. package/src/types/stack.ts +32 -0
  168. package/src/types/technology.ts +58 -0
  169. package/src/types/template.ts +37 -0
  170. package/src/types/validation.ts +22 -0
  171. package/tsconfig.json +10 -0
  172. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,345 @@
1
+ /**
2
+ * Stack Detector — Analyze a project directory to detect its technology stack.
3
+ */
4
+
5
+ import * as fs from "node:fs";
6
+ import * as path from "node:path";
7
+
8
+ export interface DetectedStack {
9
+ technologies: DetectedTech[];
10
+ confidence: number;
11
+ projectType: "frontend" | "backend" | "fullstack" | "monorepo" | "library" | "unknown";
12
+ packageManagers: string[];
13
+ }
14
+
15
+ export interface DetectedTech {
16
+ id: string;
17
+ name: string;
18
+ confidence: number;
19
+ detectedVia: string;
20
+ version?: string;
21
+ }
22
+
23
+ // ─── Dependency → Registry ID mappings ─────────────────
24
+
25
+ const NPM_DEPENDENCY_MAP: Record<string, { id: string; name: string }> = {
26
+ next: { id: "nextjs", name: "Next.js" },
27
+ react: { id: "react", name: "React" },
28
+ vue: { id: "vue", name: "Vue" },
29
+ "@angular/core": { id: "angular", name: "Angular" },
30
+ express: { id: "express", name: "Express" },
31
+ fastify: { id: "fastify", name: "Fastify" },
32
+ "@nestjs/core": { id: "nestjs", name: "NestJS" },
33
+ prisma: { id: "prisma", name: "Prisma" },
34
+ "drizzle-orm": { id: "drizzle", name: "Drizzle ORM" },
35
+ typeorm: { id: "typeorm", name: "TypeORM" },
36
+ tailwindcss: { id: "tailwindcss", name: "Tailwind CSS" },
37
+ "@chakra-ui/react": { id: "chakra-ui", name: "Chakra UI" },
38
+ typescript: { id: "typescript", name: "TypeScript" },
39
+ vitest: { id: "vitest", name: "Vitest" },
40
+ jest: { id: "jest", name: "Jest" },
41
+ "next-auth": { id: "nextauth", name: "NextAuth.js" },
42
+ "@clerk/nextjs": { id: "clerk", name: "Clerk" },
43
+ };
44
+
45
+ const PYTHON_DEPENDENCY_MAP: Record<string, { id: string; name: string }> = {
46
+ fastapi: { id: "fastapi", name: "FastAPI" },
47
+ django: { id: "django", name: "Django" },
48
+ flask: { id: "flask", name: "Flask" },
49
+ sqlalchemy: { id: "sqlalchemy", name: "SQLAlchemy" },
50
+ };
51
+
52
+ const GO_DEPENDENCY_MAP: Record<string, { id: string; name: string }> = {
53
+ "github.com/gin-gonic/gin": { id: "gin", name: "Gin" },
54
+ "github.com/labstack/echo": { id: "echo", name: "Echo" },
55
+ };
56
+
57
+ const DOCKER_IMAGE_MAP: Record<string, { id: string; name: string }> = {
58
+ postgres: { id: "postgresql", name: "PostgreSQL" },
59
+ mysql: { id: "mysql", name: "MySQL" },
60
+ mongo: { id: "mongodb", name: "MongoDB" },
61
+ redis: { id: "redis", name: "Redis" },
62
+ rabbitmq: { id: "rabbitmq", name: "RabbitMQ" },
63
+ nginx: { id: "nginx", name: "Nginx" },
64
+ };
65
+
66
+ // ─── Helpers ───────────────────────────────────────────
67
+
68
+ function readFileOr(filePath: string): string | null {
69
+ try {
70
+ if (fs.existsSync(filePath)) {
71
+ return fs.readFileSync(filePath, "utf-8");
72
+ }
73
+ } catch {
74
+ // Ignore read errors
75
+ }
76
+ return null;
77
+ }
78
+
79
+ function extractVersion(value: string): string | undefined {
80
+ const match = value.replace(/[\^~>=<]/g, "").match(/(\d+[.\d]*)/);
81
+ return match?.[1];
82
+ }
83
+
84
+ // ─── Main detection ────────────────────────────────────
85
+
86
+ /**
87
+ * Detect the technology stack of a project at the given path.
88
+ */
89
+ export function detectStack(projectPath: string): DetectedStack {
90
+ const techs: DetectedTech[] = [];
91
+ const packageManagers: string[] = [];
92
+ let hasFrontend = false;
93
+ let hasBackend = false;
94
+ let isMonorepo = false;
95
+
96
+ // ── Package managers ──
97
+ if (fs.existsSync(path.join(projectPath, "pnpm-lock.yaml"))) packageManagers.push("pnpm");
98
+ if (fs.existsSync(path.join(projectPath, "yarn.lock"))) packageManagers.push("yarn");
99
+ if (fs.existsSync(path.join(projectPath, "package-lock.json"))) packageManagers.push("npm");
100
+ if (fs.existsSync(path.join(projectPath, "bun.lockb"))) packageManagers.push("bun");
101
+
102
+ // ── 1. package.json ──
103
+ const pkgContent = readFileOr(path.join(projectPath, "package.json"));
104
+ if (pkgContent) {
105
+ try {
106
+ const pkg = JSON.parse(pkgContent);
107
+ const allDeps: Record<string, string> = {
108
+ ...(pkg.dependencies || {}),
109
+ ...(pkg.devDependencies || {}),
110
+ };
111
+
112
+ for (const [dep, version] of Object.entries(allDeps)) {
113
+ const mapping = NPM_DEPENDENCY_MAP[dep];
114
+ if (mapping) {
115
+ const isDevDep = dep in (pkg.devDependencies || {});
116
+ techs.push({
117
+ id: mapping.id,
118
+ name: mapping.name,
119
+ confidence: 95,
120
+ detectedVia: isDevDep ? "devDependencies" : "package.json",
121
+ version: extractVersion(version),
122
+ });
123
+
124
+ // Track frontend/backend
125
+ if (["react", "vue", "angular", "nextjs"].includes(mapping.id)) {
126
+ hasFrontend = true;
127
+ }
128
+ if (["express", "fastify", "nestjs"].includes(mapping.id)) {
129
+ hasBackend = true;
130
+ }
131
+ // Next.js is fullstack
132
+ if (mapping.id === "nextjs") {
133
+ hasBackend = true;
134
+ }
135
+ }
136
+ }
137
+ } catch {
138
+ // Invalid JSON, skip
139
+ }
140
+ }
141
+
142
+ // ── 2. requirements.txt ──
143
+ const reqContent = readFileOr(path.join(projectPath, "requirements.txt"));
144
+ if (reqContent) {
145
+ packageManagers.push("pip");
146
+ const lines = reqContent.split("\n");
147
+ for (const line of lines) {
148
+ const dep = line
149
+ .trim()
150
+ .split(/[=<>!~]/)[0]
151
+ .toLowerCase();
152
+ const mapping = PYTHON_DEPENDENCY_MAP[dep];
153
+ if (mapping) {
154
+ const versionMatch = line.match(/==(\d+[.\d]*)/);
155
+ techs.push({
156
+ id: mapping.id,
157
+ name: mapping.name,
158
+ confidence: 95,
159
+ detectedVia: "requirements.txt",
160
+ version: versionMatch?.[1],
161
+ });
162
+ hasBackend = true;
163
+ }
164
+ }
165
+ }
166
+
167
+ // ── 2b. pyproject.toml ──
168
+ const pyprojectContent = readFileOr(path.join(projectPath, "pyproject.toml"));
169
+ if (pyprojectContent) {
170
+ if (!packageManagers.includes("pip")) packageManagers.push("pip");
171
+ for (const [dep, mapping] of Object.entries(PYTHON_DEPENDENCY_MAP)) {
172
+ if (pyprojectContent.includes(`"${dep}`) || pyprojectContent.includes(`'${dep}`)) {
173
+ const versionMatch = pyprojectContent.match(new RegExp(`["']${dep}[><=~!]*([\\d.]+)?["']`));
174
+ techs.push({
175
+ id: mapping.id,
176
+ name: mapping.name,
177
+ confidence: 85,
178
+ detectedVia: "pyproject.toml",
179
+ version: versionMatch?.[1],
180
+ });
181
+ hasBackend = true;
182
+ }
183
+ }
184
+ }
185
+
186
+ // ── 3. go.mod ──
187
+ const goModContent = readFileOr(path.join(projectPath, "go.mod"));
188
+ if (goModContent) {
189
+ packageManagers.push("go");
190
+ for (const [dep, mapping] of Object.entries(GO_DEPENDENCY_MAP)) {
191
+ if (goModContent.includes(dep)) {
192
+ const versionMatch = goModContent.match(
193
+ new RegExp(`${dep.replace(/\//g, "\\/")}\\s+v(\\d+[.\\d]*)`),
194
+ );
195
+ techs.push({
196
+ id: mapping.id,
197
+ name: mapping.name,
198
+ confidence: 95,
199
+ detectedVia: "go.mod",
200
+ version: versionMatch?.[1],
201
+ });
202
+ hasBackend = true;
203
+ }
204
+ }
205
+ }
206
+
207
+ // ── 4. Cargo.toml ──
208
+ const cargoContent = readFileOr(path.join(projectPath, "Cargo.toml"));
209
+ if (cargoContent) {
210
+ packageManagers.push("cargo");
211
+ techs.push({
212
+ id: "rust",
213
+ name: "Rust",
214
+ confidence: 95,
215
+ detectedVia: "Cargo.toml",
216
+ });
217
+ hasBackend = true;
218
+ }
219
+
220
+ // ── 5. docker-compose.yml / docker-compose.yaml ──
221
+ for (const composeName of [
222
+ "docker-compose.yml",
223
+ "docker-compose.yaml",
224
+ "compose.yml",
225
+ "compose.yaml",
226
+ ]) {
227
+ const composeContent = readFileOr(path.join(projectPath, composeName));
228
+ if (composeContent) {
229
+ for (const [imageKey, mapping] of Object.entries(DOCKER_IMAGE_MAP)) {
230
+ const imagePattern = new RegExp(`image:\\s*["']?[^\\n]*${imageKey}`, "i");
231
+ if (imagePattern.test(composeContent)) {
232
+ const versionMatch = composeContent.match(new RegExp(`${imageKey}:(\\d+[.\\d]*)`, "i"));
233
+ techs.push({
234
+ id: mapping.id,
235
+ name: mapping.name,
236
+ confidence: 90,
237
+ detectedVia: composeName,
238
+ version: versionMatch?.[1],
239
+ });
240
+ }
241
+ }
242
+ break; // Only process the first compose file found
243
+ }
244
+ }
245
+
246
+ // ── 6. Dockerfile ──
247
+ const dockerfileContent = readFileOr(path.join(projectPath, "Dockerfile"));
248
+ if (dockerfileContent) {
249
+ techs.push({
250
+ id: "docker",
251
+ name: "Docker",
252
+ confidence: 95,
253
+ detectedVia: "Dockerfile",
254
+ });
255
+ }
256
+
257
+ // ── 7. .github/workflows ──
258
+ const workflowsDir = path.join(projectPath, ".github", "workflows");
259
+ try {
260
+ if (fs.existsSync(workflowsDir) && fs.statSync(workflowsDir).isDirectory()) {
261
+ const files = fs.readdirSync(workflowsDir);
262
+ const ymlFiles = files.filter((f) => f.endsWith(".yml") || f.endsWith(".yaml"));
263
+ if (ymlFiles.length > 0) {
264
+ techs.push({
265
+ id: "github-actions",
266
+ name: "GitHub Actions",
267
+ confidence: 95,
268
+ detectedVia: ".github/workflows",
269
+ });
270
+ }
271
+ }
272
+ } catch {
273
+ // Ignore
274
+ }
275
+
276
+ // ── 8. turbo.json ──
277
+ if (fs.existsSync(path.join(projectPath, "turbo.json"))) {
278
+ techs.push({
279
+ id: "turborepo",
280
+ name: "Turborepo",
281
+ confidence: 95,
282
+ detectedVia: "turbo.json",
283
+ });
284
+ isMonorepo = true;
285
+ }
286
+
287
+ // ── 9. pnpm-workspace.yaml ──
288
+ if (fs.existsSync(path.join(projectPath, "pnpm-workspace.yaml"))) {
289
+ isMonorepo = true;
290
+ // Only add if turborepo not already detected (they often coexist)
291
+ if (!techs.some((t) => t.id === "turborepo")) {
292
+ techs.push({
293
+ id: "pnpm-workspace",
294
+ name: "pnpm Workspace",
295
+ confidence: 90,
296
+ detectedVia: "pnpm-workspace.yaml",
297
+ });
298
+ }
299
+ }
300
+
301
+ // ── Determine project type ──
302
+ let projectType: DetectedStack["projectType"] = "unknown";
303
+ if (isMonorepo) {
304
+ projectType = "monorepo";
305
+ } else if (hasFrontend && hasBackend) {
306
+ projectType = "fullstack";
307
+ } else if (hasFrontend) {
308
+ projectType = "frontend";
309
+ } else if (hasBackend) {
310
+ projectType = "backend";
311
+ } else if (pkgContent) {
312
+ // Has package.json but no clear frontend/backend → likely a library
313
+ try {
314
+ const pkg = JSON.parse(pkgContent);
315
+ if (pkg.main || pkg.exports || pkg.types) {
316
+ projectType = "library";
317
+ }
318
+ } catch {
319
+ // Ignore
320
+ }
321
+ }
322
+
323
+ // ── Deduplicate techs (keep highest confidence) ──
324
+ const techMap = new Map<string, DetectedTech>();
325
+ for (const tech of techs) {
326
+ const existing = techMap.get(tech.id);
327
+ if (!existing || tech.confidence > existing.confidence) {
328
+ techMap.set(tech.id, tech);
329
+ }
330
+ }
331
+ const deduped = Array.from(techMap.values());
332
+
333
+ // ── Calculate overall confidence ──
334
+ const confidence =
335
+ deduped.length > 0
336
+ ? Math.round(deduped.reduce((sum, t) => sum + t.confidence, 0) / deduped.length)
337
+ : 0;
338
+
339
+ return {
340
+ technologies: deduped,
341
+ confidence,
342
+ projectType,
343
+ packageManagers: [...new Set(packageManagers)],
344
+ };
345
+ }
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Stack Comparison — Diff two stack definitions by technology.
3
+ */
4
+
5
+ import type { StackDefinition } from "../types/stack.js";
6
+
7
+ // ─── Types ────────────────────────────────────────────
8
+
9
+ export interface StackDiff {
10
+ added: DiffItem[];
11
+ removed: DiffItem[];
12
+ changed: DiffChange[];
13
+ unchanged: string[];
14
+ summary: string;
15
+ }
16
+
17
+ export interface DiffItem {
18
+ technologyId: string;
19
+ name: string;
20
+ version?: string;
21
+ port?: number;
22
+ }
23
+
24
+ export interface DiffChange {
25
+ technologyId: string;
26
+ name: string;
27
+ field: string;
28
+ from: string;
29
+ to: string;
30
+ }
31
+
32
+ // ─── Main Function ────────────────────────────────────
33
+
34
+ export function diffStacks(stackA: StackDefinition, stackB: StackDefinition): StackDiff {
35
+ const techMapA = new Map(stackA.technologies.map((t) => [t.technologyId, t]));
36
+ const techMapB = new Map(stackB.technologies.map((t) => [t.technologyId, t]));
37
+
38
+ const added: DiffItem[] = [];
39
+ const removed: DiffItem[] = [];
40
+ const changed: DiffChange[] = [];
41
+ const unchanged: string[] = [];
42
+
43
+ // Find removed (in A but not B) and changed/unchanged
44
+ for (const [id, techA] of techMapA) {
45
+ const techB = techMapB.get(id);
46
+ if (!techB) {
47
+ removed.push({
48
+ technologyId: id,
49
+ name: id,
50
+ version: techA.version,
51
+ port: techA.port,
52
+ });
53
+ continue;
54
+ }
55
+
56
+ let hasChanges = false;
57
+
58
+ if (techA.version !== techB.version) {
59
+ changed.push({
60
+ technologyId: id,
61
+ name: id,
62
+ field: "version",
63
+ from: techA.version,
64
+ to: techB.version,
65
+ });
66
+ hasChanges = true;
67
+ }
68
+
69
+ if (techA.port !== techB.port) {
70
+ changed.push({
71
+ technologyId: id,
72
+ name: id,
73
+ field: "port",
74
+ from: techA.port !== undefined ? String(techA.port) : "none",
75
+ to: techB.port !== undefined ? String(techB.port) : "none",
76
+ });
77
+ hasChanges = true;
78
+ }
79
+
80
+ const configA = JSON.stringify(techA.config ?? {});
81
+ const configB = JSON.stringify(techB.config ?? {});
82
+ if (configA !== configB) {
83
+ changed.push({
84
+ technologyId: id,
85
+ name: id,
86
+ field: "config",
87
+ from: configA,
88
+ to: configB,
89
+ });
90
+ hasChanges = true;
91
+ }
92
+
93
+ if (!hasChanges) {
94
+ unchanged.push(id);
95
+ }
96
+ }
97
+
98
+ // Find added (in B but not A)
99
+ for (const [id, techB] of techMapB) {
100
+ if (!techMapA.has(id)) {
101
+ added.push({
102
+ technologyId: id,
103
+ name: id,
104
+ version: techB.version,
105
+ port: techB.port,
106
+ });
107
+ }
108
+ }
109
+
110
+ const parts: string[] = [];
111
+ if (added.length > 0) parts.push(`+${added.length} added`);
112
+ if (removed.length > 0) parts.push(`-${removed.length} removed`);
113
+ if (changed.length > 0) parts.push(`~${changed.length} changed`);
114
+ if (unchanged.length > 0) parts.push(`${unchanged.length} unchanged`);
115
+ const summary = parts.join(", ");
116
+
117
+ return { added, removed, changed, unchanged, summary };
118
+ }