@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,170 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { RulesEngine } from "../engine/rules-engine.js";
3
+ import type { StackTechnology, Technology } from "../types/index.js";
4
+
5
+ const mockTechs: Technology[] = [
6
+ {
7
+ id: "nodejs",
8
+ name: "Node.js",
9
+ category: "runtime",
10
+ description: "",
11
+ website: "",
12
+ versions: [{ version: "22" }],
13
+ defaultVersion: "22",
14
+ defaultPort: 3000,
15
+ requires: [],
16
+ incompatibleWith: [],
17
+ suggestedWith: ["typescript"],
18
+ envVars: {},
19
+ configFiles: [],
20
+ lastVerified: "2026-03-22",
21
+ tags: [],
22
+ },
23
+ {
24
+ id: "nextjs",
25
+ name: "Next.js",
26
+ category: "frontend",
27
+ description: "",
28
+ website: "",
29
+ versions: [{ version: "15" }],
30
+ defaultVersion: "15",
31
+ defaultPort: 3000,
32
+ requires: ["nodejs", "react"],
33
+ incompatibleWith: ["nuxt"],
34
+ suggestedWith: ["typescript", "tailwindcss"],
35
+ envVars: {},
36
+ configFiles: [],
37
+ lastVerified: "2026-03-22",
38
+ tags: [],
39
+ },
40
+ {
41
+ id: "react",
42
+ name: "React",
43
+ category: "frontend",
44
+ description: "",
45
+ website: "",
46
+ versions: [{ version: "19" }],
47
+ defaultVersion: "19",
48
+ defaultPort: 3000,
49
+ requires: ["nodejs"],
50
+ incompatibleWith: [],
51
+ suggestedWith: [],
52
+ envVars: {},
53
+ configFiles: [],
54
+ lastVerified: "2026-03-22",
55
+ tags: [],
56
+ },
57
+ {
58
+ id: "nuxt",
59
+ name: "Nuxt",
60
+ category: "frontend",
61
+ description: "",
62
+ website: "",
63
+ versions: [{ version: "3" }],
64
+ defaultVersion: "3",
65
+ defaultPort: 3000,
66
+ requires: ["nodejs"],
67
+ incompatibleWith: ["nextjs"],
68
+ suggestedWith: [],
69
+ envVars: {},
70
+ configFiles: [],
71
+ lastVerified: "2026-03-22",
72
+ tags: [],
73
+ },
74
+ {
75
+ id: "postgresql",
76
+ name: "PostgreSQL",
77
+ category: "database",
78
+ description: "",
79
+ website: "",
80
+ versions: [{ version: "17" }],
81
+ defaultVersion: "17",
82
+ defaultPort: 5432,
83
+ requires: [],
84
+ incompatibleWith: [],
85
+ suggestedWith: [],
86
+ envVars: {},
87
+ configFiles: [],
88
+ lastVerified: "2026-03-22",
89
+ tags: [],
90
+ },
91
+ {
92
+ id: "typescript",
93
+ name: "TypeScript",
94
+ category: "devops",
95
+ description: "",
96
+ website: "",
97
+ versions: [{ version: "5" }],
98
+ defaultVersion: "5",
99
+ requires: [],
100
+ incompatibleWith: [],
101
+ suggestedWith: [],
102
+ envVars: {},
103
+ configFiles: [],
104
+ lastVerified: "2026-03-22",
105
+ tags: [],
106
+ },
107
+ ];
108
+
109
+ describe("RulesEngine", () => {
110
+ const engine = new RulesEngine(mockTechs);
111
+
112
+ it("validates a simple valid stack", () => {
113
+ const selected: StackTechnology[] = [
114
+ { technologyId: "nodejs", version: "22" },
115
+ { technologyId: "postgresql", version: "17" },
116
+ ];
117
+ const result = engine.validate(selected);
118
+ expect(result.valid).toBe(true);
119
+ expect(result.issues.filter((i) => i.severity === "error")).toHaveLength(0);
120
+ });
121
+
122
+ it("auto-resolves missing dependencies", () => {
123
+ const selected: StackTechnology[] = [{ technologyId: "nextjs", version: "15" }];
124
+ const result = engine.validate(selected);
125
+ expect(result.resolvedDependencies).toContain("nodejs");
126
+ expect(result.resolvedDependencies).toContain("react");
127
+ expect(result.issues.some((i) => i.code === "AUTO_DEPENDENCY")).toBe(true);
128
+ });
129
+
130
+ it("detects incompatible technologies", () => {
131
+ const selected: StackTechnology[] = [
132
+ { technologyId: "nodejs", version: "22" },
133
+ { technologyId: "nextjs", version: "15" },
134
+ { technologyId: "nuxt", version: "3" },
135
+ ];
136
+ const result = engine.validate(selected);
137
+ expect(result.valid).toBe(false);
138
+ expect(result.issues.some((i) => i.code === "INCOMPATIBLE")).toBe(true);
139
+ });
140
+
141
+ it("detects and resolves port conflicts", () => {
142
+ const selected: StackTechnology[] = [
143
+ { technologyId: "nodejs", version: "22" },
144
+ { technologyId: "react", version: "19" },
145
+ { technologyId: "nextjs", version: "15" },
146
+ ];
147
+ const result = engine.validate(selected);
148
+ const ports = Object.values(result.portAssignments);
149
+ const uniquePorts = new Set(ports);
150
+ expect(ports.length).toBe(uniquePorts.size); // No duplicates
151
+ });
152
+
153
+ it("detects unknown technologies", () => {
154
+ const selected: StackTechnology[] = [{ technologyId: "doesnotexist", version: "1" }];
155
+ const result = engine.validate(selected);
156
+ expect(result.valid).toBe(false);
157
+ expect(result.issues.some((i) => i.code === "UNKNOWN_TECHNOLOGY")).toBe(true);
158
+ });
159
+
160
+ it("returns suggestions for selected stack", () => {
161
+ const suggestions = engine.getSuggestions(["nodejs"]);
162
+ expect(suggestions.some((s) => s.id === "typescript")).toBe(true);
163
+ });
164
+
165
+ it("gets technologies by category", () => {
166
+ const frontends = engine.getByCategory("frontend");
167
+ expect(frontends.length).toBeGreaterThan(0);
168
+ expect(frontends.every((t) => t.category === "frontend")).toBe(true);
169
+ });
170
+ });
@@ -0,0 +1,161 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { ScaffoldOrchestrator } from "../engine/scaffold-orchestrator.js";
3
+ import type { StackDefinition, Technology } from "../types/index.js";
4
+
5
+ const mockTechs: Technology[] = [
6
+ {
7
+ id: "nodejs",
8
+ name: "Node.js",
9
+ category: "runtime",
10
+ description: "JavaScript runtime",
11
+ website: "https://nodejs.org",
12
+ versions: [{ version: "22" }],
13
+ defaultVersion: "22",
14
+ defaultPort: 3000,
15
+ requires: [],
16
+ incompatibleWith: [],
17
+ suggestedWith: [],
18
+ envVars: { NODE_ENV: "development", PORT: "3000" },
19
+ configFiles: ["package.json"],
20
+ lastVerified: "2026-03-22",
21
+ tags: ["nodejs"],
22
+ },
23
+ {
24
+ id: "postgresql",
25
+ name: "PostgreSQL",
26
+ category: "database",
27
+ description: "Relational database",
28
+ website: "https://postgresql.org",
29
+ versions: [{ version: "17" }],
30
+ defaultVersion: "17",
31
+ defaultPort: 5432,
32
+ requires: [],
33
+ incompatibleWith: [],
34
+ suggestedWith: [],
35
+ dockerImage: "postgres:17",
36
+ healthCheck: {
37
+ command: "pg_isready -U postgres",
38
+ interval: "5s",
39
+ timeout: "5s",
40
+ retries: 5,
41
+ },
42
+ envVars: {
43
+ POSTGRES_USER: "postgres",
44
+ POSTGRES_PASSWORD: "postgres",
45
+ POSTGRES_DB: "app",
46
+ },
47
+ configFiles: [],
48
+ lastVerified: "2026-03-22",
49
+ tags: ["postgresql"],
50
+ },
51
+ {
52
+ id: "redis",
53
+ name: "Redis",
54
+ category: "database",
55
+ description: "In-memory store",
56
+ website: "https://redis.io",
57
+ versions: [{ version: "7" }],
58
+ defaultVersion: "7",
59
+ defaultPort: 6379,
60
+ requires: [],
61
+ incompatibleWith: [],
62
+ suggestedWith: [],
63
+ dockerImage: "redis:7-alpine",
64
+ healthCheck: {
65
+ command: "redis-cli ping",
66
+ interval: "5s",
67
+ timeout: "5s",
68
+ retries: 5,
69
+ },
70
+ envVars: { REDIS_URL: "redis://localhost:6379/0" },
71
+ configFiles: [],
72
+ lastVerified: "2026-03-22",
73
+ tags: ["redis"],
74
+ },
75
+ ];
76
+
77
+ const mockStack: StackDefinition = {
78
+ id: "test-stack",
79
+ name: "Test Stack",
80
+ description: "A test stack",
81
+ profile: "standard",
82
+ technologies: [
83
+ { technologyId: "nodejs", version: "22", port: 3000 },
84
+ { technologyId: "postgresql", version: "17", port: 5432 },
85
+ { technologyId: "redis", version: "7", port: 6379 },
86
+ ],
87
+ createdAt: "2026-03-22T00:00:00Z",
88
+ updatedAt: "2026-03-22T00:00:00Z",
89
+ version: 1,
90
+ tags: ["test"],
91
+ };
92
+
93
+ describe("ScaffoldOrchestrator", () => {
94
+ const orchestrator = new ScaffoldOrchestrator(mockTechs);
95
+
96
+ it("generates all scaffold files", () => {
97
+ const output = orchestrator.generate(mockStack);
98
+ expect(output.dockerCompose).not.toBeNull();
99
+ expect(output.envExample).toBeTruthy();
100
+ expect(output.readme).toBeTruthy();
101
+ expect(output.gitignore).toBeTruthy();
102
+ expect(output.devcontainer).toBeTruthy();
103
+ });
104
+
105
+ it("generates docker-compose with correct services", () => {
106
+ const compose = orchestrator.generateDockerCompose(mockStack, mockTechs);
107
+ expect(compose).toContain("postgresql:");
108
+ expect(compose).toContain("image: postgres:17");
109
+ expect(compose).toContain("redis:");
110
+ expect(compose).toContain("image: redis:7-alpine");
111
+ expect(compose).toContain("5432:5432");
112
+ expect(compose).toContain("6379:6379");
113
+ expect(compose).toContain("pg_isready");
114
+ expect(compose).toContain("redis-cli ping");
115
+ expect(compose).toContain("volumes:");
116
+ // Should NOT include nodejs (no docker image)
117
+ expect(compose).not.toContain("nodejs:");
118
+ });
119
+
120
+ it("generates .env.example with all env vars", () => {
121
+ const env = orchestrator.generateEnvExample(mockStack, mockTechs);
122
+ expect(env).toContain("NODE_ENV=development");
123
+ expect(env).toContain("POSTGRES_USER=postgres");
124
+ expect(env).toContain("REDIS_URL=redis://localhost:6379/0");
125
+ });
126
+
127
+ it("generates README with stack table", () => {
128
+ const readme = orchestrator.generateReadme(mockStack, mockTechs);
129
+ expect(readme).toContain("# Test Stack");
130
+ expect(readme).toContain("Node.js");
131
+ expect(readme).toContain("PostgreSQL");
132
+ expect(readme).toContain("docker compose up");
133
+ });
134
+
135
+ it("generates .gitignore with relevant patterns", () => {
136
+ const gitignore = orchestrator.generateGitignore(mockTechs);
137
+ expect(gitignore).toContain("node_modules/");
138
+ expect(gitignore).toContain(".env");
139
+ expect(gitignore).toContain("*.db");
140
+ });
141
+
142
+ it("generates devcontainer.json", () => {
143
+ const devcontainer = JSON.parse(orchestrator.generateDevcontainer(mockStack, mockTechs));
144
+ expect(devcontainer.name).toBe("Test Stack");
145
+ expect(devcontainer.forwardPorts).toContain(3000);
146
+ expect(devcontainer.forwardPorts).toContain(5432);
147
+ expect(devcontainer.features).toHaveProperty("ghcr.io/devcontainers/features/node:1");
148
+ expect(devcontainer.features).toHaveProperty(
149
+ "ghcr.io/devcontainers/features/docker-in-docker:2",
150
+ );
151
+ });
152
+
153
+ it("returns null docker-compose when no docker services", () => {
154
+ const noDockerStack: StackDefinition = {
155
+ ...mockStack,
156
+ technologies: [{ technologyId: "nodejs", version: "22", port: 3000 }],
157
+ };
158
+ const output = orchestrator.generate(noDockerStack);
159
+ expect(output.dockerCompose).toBeNull();
160
+ });
161
+ });
@@ -0,0 +1,328 @@
1
+ import * as fs from "node:fs";
2
+ import * as os from "node:os";
3
+ import * as path from "node:path";
4
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
5
+ import { closeDatabase, getDatabase } from "../db/database.js";
6
+ import { RulesEngine } from "../engine/rules-engine.js";
7
+ import { StackEngine } from "../engine/stack-engine.js";
8
+ import type { Technology } from "../types/technology.js";
9
+
10
+ // Minimal technology fixtures for testing
11
+ function makeTech(overrides: Partial<Technology> & { id: string; name: string }): Technology {
12
+ return {
13
+ category: "backend",
14
+ description: "Test technology",
15
+ website: "https://example.com",
16
+ versions: [{ version: "1.0.0" }],
17
+ defaultVersion: "1.0.0",
18
+ requires: [],
19
+ incompatibleWith: [],
20
+ suggestedWith: [],
21
+ envVars: {},
22
+ configFiles: [],
23
+ lastVerified: "2026-01-01",
24
+ tags: [],
25
+ ...overrides,
26
+ };
27
+ }
28
+
29
+ const fakeTechs: Technology[] = [
30
+ makeTech({ id: "nodejs", name: "Node.js", category: "runtime" }),
31
+ makeTech({ id: "express", name: "Express", category: "backend", requires: ["nodejs"] }),
32
+ makeTech({ id: "postgresql", name: "PostgreSQL", category: "database", defaultPort: 5432 }),
33
+ makeTech({ id: "redis", name: "Redis", category: "service", defaultPort: 6379 }),
34
+ ];
35
+
36
+ let tmpDbPath: string;
37
+ let engine: StackEngine;
38
+
39
+ beforeEach(() => {
40
+ // Use a temp file for the database so tests are isolated
41
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "stackweld-test-"));
42
+ tmpDbPath = path.join(tmpDir, "test.db");
43
+ // Initialize the database at the temp path
44
+ getDatabase(tmpDbPath);
45
+ const rules = new RulesEngine(fakeTechs);
46
+ engine = new StackEngine(rules);
47
+ });
48
+
49
+ afterEach(() => {
50
+ closeDatabase();
51
+ // Clean up temp file
52
+ if (tmpDbPath && fs.existsSync(tmpDbPath)) {
53
+ const dir = path.dirname(tmpDbPath);
54
+ fs.rmSync(dir, { recursive: true, force: true });
55
+ }
56
+ });
57
+
58
+ describe("StackEngine", () => {
59
+ describe("create()", () => {
60
+ it("creates a stack and persists it", () => {
61
+ const { stack, validation } = engine.create({
62
+ name: "test-stack",
63
+ technologies: [{ technologyId: "nodejs", version: "1.0.0" }],
64
+ });
65
+
66
+ expect(validation.valid).toBe(true);
67
+ expect(stack.name).toBe("test-stack");
68
+ expect(stack.id).toBeTruthy();
69
+ expect(stack.version).toBe(1);
70
+
71
+ // Verify it's actually persisted
72
+ const retrieved = engine.get(stack.id);
73
+ expect(retrieved).not.toBeNull();
74
+ expect(retrieved?.name).toBe("test-stack");
75
+ });
76
+
77
+ it("auto-resolves dependencies", () => {
78
+ const { stack, validation } = engine.create({
79
+ name: "express-stack",
80
+ technologies: [{ technologyId: "express", version: "1.0.0" }],
81
+ });
82
+
83
+ expect(validation.valid).toBe(true);
84
+ // express requires nodejs, which should be auto-resolved
85
+ expect(validation.resolvedDependencies).toContain("nodejs");
86
+ const techIds = stack.technologies.map((t) => t.technologyId);
87
+ expect(techIds).toContain("express");
88
+ expect(techIds).toContain("nodejs");
89
+ });
90
+
91
+ it("assigns ports to technologies with defaultPort", () => {
92
+ const { stack, validation } = engine.create({
93
+ name: "db-stack",
94
+ technologies: [
95
+ { technologyId: "postgresql", version: "1.0.0" },
96
+ { technologyId: "redis", version: "1.0.0" },
97
+ ],
98
+ });
99
+
100
+ expect(validation.valid).toBe(true);
101
+ expect(validation.portAssignments.postgresql).toBe(5432);
102
+ expect(validation.portAssignments.redis).toBe(6379);
103
+ });
104
+
105
+ it("sets default profile to standard", () => {
106
+ const { stack } = engine.create({
107
+ name: "default-profile",
108
+ technologies: [{ technologyId: "nodejs", version: "1.0.0" }],
109
+ });
110
+
111
+ expect(stack.profile).toBe("standard");
112
+ });
113
+
114
+ it("sets tags when provided", () => {
115
+ const { stack } = engine.create({
116
+ name: "tagged-stack",
117
+ technologies: [{ technologyId: "nodejs", version: "1.0.0" }],
118
+ tags: ["test", "dev"],
119
+ });
120
+
121
+ expect(stack.tags).toEqual(["test", "dev"]);
122
+ });
123
+ });
124
+
125
+ describe("list()", () => {
126
+ it("returns empty array when no stacks exist", () => {
127
+ const stacks = engine.list();
128
+ expect(stacks).toEqual([]);
129
+ });
130
+
131
+ it("returns saved stacks", () => {
132
+ engine.create({
133
+ name: "stack-a",
134
+ technologies: [{ technologyId: "nodejs", version: "1.0.0" }],
135
+ });
136
+ engine.create({
137
+ name: "stack-b",
138
+ technologies: [{ technologyId: "redis", version: "1.0.0" }],
139
+ });
140
+
141
+ const stacks = engine.list();
142
+ expect(stacks.length).toBe(2);
143
+ const names = stacks.map((s) => s.name);
144
+ expect(names).toContain("stack-a");
145
+ expect(names).toContain("stack-b");
146
+ });
147
+ });
148
+
149
+ describe("get()", () => {
150
+ it("returns correct stack by ID", () => {
151
+ const { stack: created } = engine.create({
152
+ name: "get-test",
153
+ description: "Testing get",
154
+ technologies: [{ technologyId: "nodejs", version: "1.0.0" }],
155
+ });
156
+
157
+ const retrieved = engine.get(created.id);
158
+ expect(retrieved).not.toBeNull();
159
+ expect(retrieved?.id).toBe(created.id);
160
+ expect(retrieved?.name).toBe("get-test");
161
+ expect(retrieved?.description).toBe("Testing get");
162
+ });
163
+
164
+ it("returns null for non-existent ID", () => {
165
+ const result = engine.get("non-existent-uuid");
166
+ expect(result).toBeNull();
167
+ });
168
+
169
+ it("includes technologies in the returned stack", () => {
170
+ const { stack: created } = engine.create({
171
+ name: "tech-check",
172
+ technologies: [
173
+ { technologyId: "postgresql", version: "1.0.0" },
174
+ { technologyId: "redis", version: "1.0.0" },
175
+ ],
176
+ });
177
+
178
+ const retrieved = engine.get(created.id);
179
+ expect(retrieved?.technologies.length).toBe(2);
180
+ const ids = retrieved?.technologies.map((t) => t.technologyId);
181
+ expect(ids).toContain("postgresql");
182
+ expect(ids).toContain("redis");
183
+ });
184
+ });
185
+
186
+ describe("delete()", () => {
187
+ it("removes a stack", () => {
188
+ const { stack } = engine.create({
189
+ name: "to-delete",
190
+ technologies: [{ technologyId: "nodejs", version: "1.0.0" }],
191
+ });
192
+
193
+ const deleted = engine.delete(stack.id);
194
+ expect(deleted).toBe(true);
195
+
196
+ const retrieved = engine.get(stack.id);
197
+ expect(retrieved).toBeNull();
198
+ });
199
+
200
+ it("returns false for non-existent stack", () => {
201
+ const deleted = engine.delete("non-existent-uuid");
202
+ expect(deleted).toBe(false);
203
+ });
204
+
205
+ it("does not affect other stacks", () => {
206
+ const { stack: a } = engine.create({
207
+ name: "keep-me",
208
+ technologies: [{ technologyId: "nodejs", version: "1.0.0" }],
209
+ });
210
+ const { stack: b } = engine.create({
211
+ name: "delete-me",
212
+ technologies: [{ technologyId: "redis", version: "1.0.0" }],
213
+ });
214
+
215
+ engine.delete(b.id);
216
+
217
+ expect(engine.get(a.id)).not.toBeNull();
218
+ expect(engine.get(b.id)).toBeNull();
219
+ });
220
+ });
221
+
222
+ describe("update()", () => {
223
+ it("modifies stack fields", () => {
224
+ const { stack } = engine.create({
225
+ name: "original",
226
+ description: "original desc",
227
+ technologies: [{ technologyId: "nodejs", version: "1.0.0" }],
228
+ });
229
+
230
+ const result = engine.update(stack.id, {
231
+ name: "updated-name",
232
+ description: "new desc",
233
+ });
234
+
235
+ expect(result).not.toBeNull();
236
+ expect(result?.stack.name).toBe("updated-name");
237
+ expect(result?.stack.description).toBe("new desc");
238
+ });
239
+
240
+ it("increments version number on update", () => {
241
+ const { stack } = engine.create({
242
+ name: "versioned",
243
+ technologies: [{ technologyId: "nodejs", version: "1.0.0" }],
244
+ });
245
+
246
+ expect(stack.version).toBe(1);
247
+
248
+ const result = engine.update(stack.id, { name: "versioned-v2" });
249
+ expect(result?.stack.version).toBe(2);
250
+
251
+ const result2 = engine.update(stack.id, { name: "versioned-v3" });
252
+ expect(result2?.stack.version).toBe(3);
253
+ });
254
+
255
+ it("returns null for non-existent stack", () => {
256
+ const result = engine.update("non-existent", { name: "nope" });
257
+ expect(result).toBeNull();
258
+ });
259
+
260
+ it("updates technologies", () => {
261
+ const { stack } = engine.create({
262
+ name: "tech-update",
263
+ technologies: [{ technologyId: "nodejs", version: "1.0.0" }],
264
+ });
265
+
266
+ const result = engine.update(stack.id, {
267
+ technologies: [
268
+ { technologyId: "nodejs", version: "1.0.0" },
269
+ { technologyId: "redis", version: "1.0.0" },
270
+ ],
271
+ });
272
+
273
+ const techIds = result?.stack.technologies.map((t) => t.technologyId);
274
+ expect(techIds).toContain("nodejs");
275
+ expect(techIds).toContain("redis");
276
+ });
277
+ });
278
+
279
+ describe("version management", () => {
280
+ it("saves a version snapshot on create", () => {
281
+ const { stack } = engine.create({
282
+ name: "versioned-stack",
283
+ technologies: [{ technologyId: "nodejs", version: "1.0.0" }],
284
+ });
285
+
286
+ const history = engine.getVersionHistory(stack.id);
287
+ expect(history.length).toBe(1);
288
+ expect(history[0].version).toBe(1);
289
+ expect(history[0].changelog).toBe("Initial creation");
290
+ expect(history[0].snapshot.name).toBe("versioned-stack");
291
+ });
292
+
293
+ it("saves a version snapshot on update", () => {
294
+ const { stack } = engine.create({
295
+ name: "multi-version",
296
+ technologies: [{ technologyId: "nodejs", version: "1.0.0" }],
297
+ });
298
+
299
+ engine.update(stack.id, { name: "multi-version-v2" });
300
+
301
+ const history = engine.getVersionHistory(stack.id);
302
+ expect(history.length).toBe(2);
303
+ // Ordered by version DESC
304
+ expect(history[0].version).toBe(2);
305
+ expect(history[1].version).toBe(1);
306
+ });
307
+
308
+ it("returns empty history for non-existent stack", () => {
309
+ const history = engine.getVersionHistory("non-existent");
310
+ expect(history).toEqual([]);
311
+ });
312
+
313
+ it("version snapshot contains full stack data", () => {
314
+ const { stack } = engine.create({
315
+ name: "snapshot-check",
316
+ description: "checking snapshots",
317
+ technologies: [{ technologyId: "postgresql", version: "1.0.0" }],
318
+ tags: ["prod"],
319
+ });
320
+
321
+ const history = engine.getVersionHistory(stack.id);
322
+ const snapshot = history[0].snapshot;
323
+ expect(snapshot.name).toBe("snapshot-check");
324
+ expect(snapshot.description).toBe("checking snapshots");
325
+ expect(snapshot.tags).toEqual(["prod"]);
326
+ });
327
+ });
328
+ });