@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,934 @@
1
+ /**
2
+ * Scaffold Orchestrator — Generates project files from a stack definition.
3
+ * Delegates to official CLI tools when available, fills the gaps with:
4
+ * - docker-compose.yml
5
+ * - .env.example
6
+ * - README.md
7
+ * - .gitignore (combined)
8
+ * - devcontainer.json
9
+ */
10
+ import { execSync } from "node:child_process";
11
+ export class ScaffoldOrchestrator {
12
+ technologies;
13
+ constructor(technologies) {
14
+ this.technologies = new Map(technologies.map((t) => [t.id, t]));
15
+ }
16
+ /**
17
+ * Generate all scaffold files for a stack.
18
+ */
19
+ generate(stack) {
20
+ const techs = stack.technologies
21
+ .map((st) => this.technologies.get(st.technologyId))
22
+ .filter((t) => t != null);
23
+ const dockerServices = techs.filter((t) => t.dockerImage && (t.category === "database" || t.category === "service"));
24
+ return {
25
+ dockerCompose: dockerServices.length > 0 ? this.generateDockerCompose(stack, techs) : null,
26
+ envExample: this.generateEnvExample(stack, techs),
27
+ readme: this.generateReadme(stack, techs),
28
+ gitignore: this.generateGitignore(techs),
29
+ devcontainer: this.generateDevcontainer(stack, techs),
30
+ devScript: this.generateDevScript(stack, techs),
31
+ setupScript: this.generateSetupScript(stack, techs),
32
+ makefile: this.generateMakefile(stack, techs),
33
+ vscodeSettings: this.generateVscodeSettings(techs),
34
+ ciWorkflow: this.generateCiWorkflow(stack, techs),
35
+ scaffoldCommands: this.getScaffoldCommands(stack, techs),
36
+ directories: this.getRequiredDirectories(techs),
37
+ };
38
+ }
39
+ /**
40
+ * Generate docker-compose.yml from stack technologies.
41
+ */
42
+ generateDockerCompose(stack, techs) {
43
+ const lines = ["services:"];
44
+ const volumes = [];
45
+ // Only containerize databases and services — runtimes run locally
46
+ const containerizable = ["database", "service"];
47
+ for (const tech of techs) {
48
+ if (!tech.dockerImage || !containerizable.includes(tech.category))
49
+ continue;
50
+ const stackTech = stack.technologies.find((st) => st.technologyId === tech.id);
51
+ const port = stackTech?.port ?? tech.defaultPort;
52
+ lines.push(` ${tech.id}:`);
53
+ lines.push(` image: ${tech.dockerImage}`);
54
+ lines.push(" restart: unless-stopped");
55
+ // Ports
56
+ if (port) {
57
+ lines.push(" ports:");
58
+ lines.push(` - "${port}:${port}"`);
59
+ }
60
+ // Environment variables
61
+ const envVars = Object.entries(tech.envVars);
62
+ if (envVars.length > 0) {
63
+ lines.push(" environment:");
64
+ for (const [key, value] of envVars) {
65
+ lines.push(` ${key}: "${value}"`);
66
+ }
67
+ }
68
+ // Health check
69
+ if (tech.healthCheck) {
70
+ lines.push(" healthcheck:");
71
+ if (tech.healthCheck.command) {
72
+ lines.push(` test: ["CMD-SHELL", "${tech.healthCheck.command}"]`);
73
+ }
74
+ else if (tech.healthCheck.endpoint) {
75
+ lines.push(` test: ["CMD-SHELL", "curl -f ${tech.healthCheck.endpoint} || exit 1"]`);
76
+ }
77
+ if (tech.healthCheck.interval) {
78
+ lines.push(` interval: ${tech.healthCheck.interval}`);
79
+ }
80
+ if (tech.healthCheck.timeout) {
81
+ lines.push(` timeout: ${tech.healthCheck.timeout}`);
82
+ }
83
+ if (tech.healthCheck.retries) {
84
+ lines.push(` retries: ${tech.healthCheck.retries}`);
85
+ }
86
+ }
87
+ // Volumes for databases
88
+ if (tech.category === "database") {
89
+ const volName = `${tech.id}_data`;
90
+ const mountPath = this.getDataMount(tech.id);
91
+ if (mountPath) {
92
+ lines.push(" volumes:");
93
+ lines.push(` - ${volName}:${mountPath}`);
94
+ volumes.push(volName);
95
+ }
96
+ }
97
+ lines.push("");
98
+ }
99
+ // Named volumes
100
+ if (volumes.length > 0) {
101
+ lines.push("volumes:");
102
+ for (const vol of volumes) {
103
+ lines.push(` ${vol}:`);
104
+ }
105
+ }
106
+ return `${lines.join("\n")}\n`;
107
+ }
108
+ /**
109
+ * Generate .env.example from all technologies' envVars.
110
+ */
111
+ generateEnvExample(stack, techs) {
112
+ const lines = [
113
+ `# ${stack.name} — Environment Variables`,
114
+ `# Generated by Stackweld`,
115
+ "",
116
+ ];
117
+ const seen = new Set();
118
+ for (const tech of techs) {
119
+ const entries = Object.entries(tech.envVars);
120
+ if (entries.length === 0)
121
+ continue;
122
+ lines.push(`# ${tech.name}`);
123
+ for (const [key, value] of entries) {
124
+ if (!seen.has(key)) {
125
+ lines.push(`${key}=${value}`);
126
+ seen.add(key);
127
+ }
128
+ }
129
+ lines.push("");
130
+ }
131
+ return lines.join("\n");
132
+ }
133
+ /**
134
+ * Generate README.md for the project.
135
+ */
136
+ generateReadme(stack, techs) {
137
+ const lines = [
138
+ `# ${stack.name}`,
139
+ "",
140
+ stack.description || "A project generated by Stackweld.",
141
+ "",
142
+ "## Stack",
143
+ "",
144
+ "| Technology | Version | Category | Port |",
145
+ "|------------|---------|----------|------|",
146
+ ];
147
+ for (const st of stack.technologies) {
148
+ const tech = this.technologies.get(st.technologyId);
149
+ if (tech) {
150
+ const port = st.port ?? tech.defaultPort ?? "—";
151
+ lines.push(`| ${tech.name} | ${st.version} | ${tech.category} | ${port} |`);
152
+ }
153
+ }
154
+ lines.push("");
155
+ lines.push("## Getting Started");
156
+ lines.push("");
157
+ lines.push("```bash");
158
+ lines.push("# Copy environment variables");
159
+ lines.push("cp .env.example .env");
160
+ lines.push("");
161
+ const dockerTechs = techs.filter((t) => t.dockerImage && (t.category === "database" || t.category === "service"));
162
+ if (dockerTechs.length > 0) {
163
+ lines.push("# Start services");
164
+ lines.push("docker compose up -d");
165
+ lines.push("");
166
+ }
167
+ const scaffoldCmds = this.getScaffoldCommands(stack, techs);
168
+ if (scaffoldCmds.length > 0) {
169
+ lines.push("# Install dependencies");
170
+ for (const cmd of scaffoldCmds) {
171
+ lines.push(`# ${cmd.name}`);
172
+ lines.push(cmd.command);
173
+ }
174
+ }
175
+ lines.push("```");
176
+ lines.push("");
177
+ lines.push("---");
178
+ lines.push("");
179
+ lines.push(`*Generated by [Stackweld](https://github.com/stackweld) — Profile: ${stack.profile}*`);
180
+ return `${lines.join("\n")}\n`;
181
+ }
182
+ /**
183
+ * Generate combined .gitignore for all technologies.
184
+ */
185
+ generateGitignore(techs) {
186
+ const patterns = new Set();
187
+ // Always include — universal patterns
188
+ patterns.add("node_modules/");
189
+ patterns.add(".env");
190
+ patterns.add(".env.local");
191
+ patterns.add(".env.*.local");
192
+ patterns.add("dist/");
193
+ patterns.add("build/");
194
+ patterns.add(".DS_Store");
195
+ patterns.add("Thumbs.db");
196
+ patterns.add("*.log");
197
+ patterns.add("*.pid");
198
+ patterns.add("*.seed");
199
+ patterns.add("coverage/");
200
+ patterns.add(".cache/");
201
+ patterns.add("tmp/");
202
+ patterns.add(".tmp/");
203
+ for (const tech of techs) {
204
+ switch (tech.category) {
205
+ case "runtime":
206
+ if (tech.id === "nodejs" || tech.id === "bun") {
207
+ patterns.add("node_modules/");
208
+ patterns.add(".next/");
209
+ patterns.add(".nuxt/");
210
+ patterns.add(".output/");
211
+ patterns.add(".turbo/");
212
+ patterns.add(".vercel/");
213
+ patterns.add(".npm/");
214
+ patterns.add("*.tsbuildinfo");
215
+ }
216
+ if (tech.id === "bun") {
217
+ patterns.add("bun.lockb");
218
+ }
219
+ if (tech.id === "python") {
220
+ patterns.add("__pycache__/");
221
+ patterns.add("*.pyc");
222
+ patterns.add("*.pyo");
223
+ patterns.add(".venv/");
224
+ patterns.add("venv/");
225
+ patterns.add("*.egg-info/");
226
+ patterns.add(".mypy_cache/");
227
+ patterns.add(".ruff_cache/");
228
+ patterns.add(".pytest_cache/");
229
+ patterns.add("htmlcov/");
230
+ }
231
+ if (tech.id === "go") {
232
+ patterns.add("/vendor/");
233
+ patterns.add("*.exe");
234
+ patterns.add("bin/");
235
+ }
236
+ if (tech.id === "rust") {
237
+ patterns.add("target/");
238
+ patterns.add("Cargo.lock");
239
+ }
240
+ if (tech.id === "deno") {
241
+ patterns.add(".deno/");
242
+ }
243
+ break;
244
+ case "database":
245
+ patterns.add("*.db");
246
+ patterns.add("*.sqlite");
247
+ patterns.add("*.sqlite3");
248
+ break;
249
+ case "devops":
250
+ if (tech.id === "docker") {
251
+ patterns.add(".docker/");
252
+ }
253
+ if (tech.id === "storybook") {
254
+ patterns.add("storybook-static/");
255
+ }
256
+ break;
257
+ case "frontend":
258
+ if (tech.id === "nextjs") {
259
+ patterns.add(".next/");
260
+ patterns.add("out/");
261
+ }
262
+ if (tech.id === "nuxt") {
263
+ patterns.add(".nuxt/");
264
+ patterns.add(".output/");
265
+ }
266
+ if (tech.id === "astro") {
267
+ patterns.add(".astro/");
268
+ }
269
+ if (tech.id === "sveltekit") {
270
+ patterns.add(".svelte-kit/");
271
+ }
272
+ break;
273
+ case "orm":
274
+ if (tech.id === "prisma") {
275
+ patterns.add("prisma/*.db");
276
+ patterns.add("prisma/*.db-journal");
277
+ }
278
+ break;
279
+ }
280
+ }
281
+ return `${[...patterns].sort().join("\n")}\n`;
282
+ }
283
+ /**
284
+ * Generate devcontainer.json.
285
+ */
286
+ generateDevcontainer(stack, techs) {
287
+ const features = {};
288
+ const forwardPorts = [];
289
+ for (const tech of techs) {
290
+ const st = stack.technologies.find((s) => s.technologyId === tech.id);
291
+ const port = st?.port ?? tech.defaultPort;
292
+ if (port)
293
+ forwardPorts.push(port);
294
+ switch (tech.id) {
295
+ case "nodejs":
296
+ features["ghcr.io/devcontainers/features/node:1"] = {
297
+ version: tech.defaultVersion,
298
+ };
299
+ break;
300
+ case "python":
301
+ features["ghcr.io/devcontainers/features/python:1"] = {
302
+ version: tech.defaultVersion,
303
+ };
304
+ break;
305
+ case "go":
306
+ features["ghcr.io/devcontainers/features/go:1"] = {
307
+ version: tech.defaultVersion,
308
+ };
309
+ break;
310
+ case "rust":
311
+ features["ghcr.io/devcontainers/features/rust:1"] = {
312
+ version: tech.defaultVersion,
313
+ };
314
+ break;
315
+ case "docker":
316
+ features["ghcr.io/devcontainers/features/docker-in-docker:2"] = {};
317
+ break;
318
+ }
319
+ }
320
+ const hasDocker = techs.some((t) => t.dockerImage);
321
+ const devcontainer = {
322
+ name: stack.name,
323
+ image: "mcr.microsoft.com/devcontainers/base:ubuntu",
324
+ features,
325
+ forwardPorts: [...new Set(forwardPorts)].sort((a, b) => a - b),
326
+ postCreateCommand: "echo 'Stackweld devcontainer ready'",
327
+ };
328
+ if (hasDocker) {
329
+ devcontainer.features = {
330
+ ...features,
331
+ "ghcr.io/devcontainers/features/docker-in-docker:2": {},
332
+ };
333
+ }
334
+ return `${JSON.stringify(devcontainer, null, 2)}\n`;
335
+ }
336
+ /**
337
+ * Get official scaffold commands for technologies that have them.
338
+ */
339
+ getScaffoldCommands(_stack, techs) {
340
+ const commands = [];
341
+ for (const tech of techs) {
342
+ if (tech.officialScaffold) {
343
+ commands.push({
344
+ name: `Initialize ${tech.name}`,
345
+ command: tech.officialScaffold,
346
+ });
347
+ }
348
+ }
349
+ return commands;
350
+ }
351
+ /**
352
+ * Initialize a Git repository in the project directory.
353
+ * Creates .gitignore, makes initial commit if requested.
354
+ */
355
+ initGit(projectDir, stack, initialCommit = true) {
356
+ try {
357
+ execSync("git init", { cwd: projectDir, stdio: "pipe" });
358
+ if (initialCommit) {
359
+ execSync("git add -A", { cwd: projectDir, stdio: "pipe" });
360
+ const msg = `Initial commit: ${stack.name} (${stack.profile})\n\nGenerated by Stackweld`;
361
+ execSync(`git commit -m "${msg}"`, {
362
+ cwd: projectDir,
363
+ stdio: "pipe",
364
+ });
365
+ }
366
+ return { success: true, message: "Git repository initialized" };
367
+ }
368
+ catch (err) {
369
+ return {
370
+ success: false,
371
+ message: err instanceof Error ? err.message : "Git init failed",
372
+ };
373
+ }
374
+ }
375
+ /**
376
+ * Generate scripts/dev.sh — starts docker services and dev server.
377
+ */
378
+ generateDevScript(stack, techs) {
379
+ const lines = [
380
+ "#!/usr/bin/env bash",
381
+ `# ${stack.name} — Development script`,
382
+ "# Generated by Stackweld",
383
+ "set -euo pipefail",
384
+ "",
385
+ ];
386
+ const hasDockerServices = techs.some((t) => t.dockerImage && (t.category === "database" || t.category === "service"));
387
+ if (hasDockerServices) {
388
+ lines.push("echo '🐳 Starting Docker services...'");
389
+ lines.push("docker compose up -d");
390
+ lines.push("");
391
+ }
392
+ // Detect main framework dev command
393
+ const devCmd = this.getDevCommand(techs);
394
+ if (devCmd) {
395
+ lines.push(`echo '🚀 Starting dev server...'`);
396
+ lines.push(devCmd);
397
+ }
398
+ else {
399
+ lines.push("echo '✅ Services ready.'");
400
+ }
401
+ lines.push("");
402
+ return lines.join("\n");
403
+ }
404
+ /**
405
+ * Generate scripts/setup.sh — first-time project setup.
406
+ */
407
+ generateSetupScript(stack, techs) {
408
+ const lines = [
409
+ "#!/usr/bin/env bash",
410
+ `# ${stack.name} — First-time setup`,
411
+ "# Generated by Stackweld",
412
+ "set -euo pipefail",
413
+ "",
414
+ "# Copy environment variables if not present",
415
+ "if [ ! -f .env ]; then",
416
+ " cp .env.example .env",
417
+ ' echo "✅ .env created from .env.example"',
418
+ "else",
419
+ ' echo "ℹ️ .env already exists, skipping"',
420
+ "fi",
421
+ "",
422
+ ];
423
+ const hasDockerServices = techs.some((t) => t.dockerImage && (t.category === "database" || t.category === "service"));
424
+ if (hasDockerServices) {
425
+ lines.push("# Start Docker services");
426
+ lines.push("echo '🐳 Starting Docker services...'");
427
+ lines.push("docker compose up -d");
428
+ lines.push("");
429
+ }
430
+ // Detect full-stack layout
431
+ const hasFrontend = techs.some((t) => t.category === "frontend");
432
+ const hasBackend = techs.some((t) => t.category === "backend");
433
+ const isFullStack = hasFrontend && hasBackend;
434
+ // Install commands based on runtime
435
+ const hasNode = techs.some((t) => t.id === "nodejs" || t.id === "bun");
436
+ const hasPython = techs.some((t) => t.id === "python");
437
+ const hasGo = techs.some((t) => t.id === "go");
438
+ const hasRust = techs.some((t) => t.id === "rust");
439
+ const hasBun = techs.some((t) => t.id === "bun");
440
+ if (isFullStack) {
441
+ // Full-stack: install deps in subdirectories
442
+ if (hasNode) {
443
+ const installCmd = hasBun ? "bun install" : "npm install";
444
+ lines.push("# Install frontend dependencies");
445
+ lines.push(`echo '📦 Installing frontend dependencies...'`);
446
+ lines.push(`cd frontend && ${installCmd} && cd ..`);
447
+ lines.push("");
448
+ }
449
+ if (hasPython) {
450
+ lines.push("# Install backend dependencies");
451
+ lines.push(`echo '🐍 Setting up backend Python environment...'`);
452
+ lines.push("cd backend && python3 -m venv .venv || true");
453
+ lines.push("source .venv/bin/activate");
454
+ lines.push("pip install -r requirements.txt 2>/dev/null || echo 'No requirements.txt found'");
455
+ lines.push("cd ..");
456
+ lines.push("");
457
+ }
458
+ }
459
+ else {
460
+ if (hasNode) {
461
+ lines.push("# Install Node.js dependencies");
462
+ const installCmd = hasBun ? "bun install" : "npm install";
463
+ lines.push(`echo '📦 Installing dependencies...'`);
464
+ lines.push(installCmd);
465
+ lines.push("");
466
+ }
467
+ if (hasPython) {
468
+ lines.push("# Install Python dependencies");
469
+ lines.push(`echo '🐍 Setting up Python environment...'`);
470
+ lines.push("python -m venv .venv || true");
471
+ lines.push("source .venv/bin/activate");
472
+ lines.push("pip install -r requirements.txt 2>/dev/null || echo 'No requirements.txt found'");
473
+ lines.push("");
474
+ }
475
+ }
476
+ if (hasGo) {
477
+ lines.push("# Install Go dependencies");
478
+ lines.push(`echo '🔧 Installing Go modules...'`);
479
+ lines.push(isFullStack ? "cd backend && go mod download && cd .." : "go mod download");
480
+ lines.push("");
481
+ }
482
+ if (hasRust) {
483
+ lines.push("# Build Rust project");
484
+ lines.push(`echo '🦀 Building Rust project...'`);
485
+ lines.push(isFullStack ? "cd backend && cargo build && cd .." : "cargo build");
486
+ lines.push("");
487
+ }
488
+ // Run migrations if ORM is present
489
+ const hasOrm = techs.some((t) => t.category === "orm");
490
+ if (hasOrm) {
491
+ const hasPrisma = techs.some((t) => t.id === "prisma");
492
+ const hasDrizzle = techs.some((t) => t.id === "drizzle");
493
+ const hasSqlalchemy = techs.some((t) => t.id === "sqlalchemy");
494
+ lines.push("# Run database migrations");
495
+ lines.push(`echo '🗃️ Running migrations...'`);
496
+ if (hasPrisma) {
497
+ lines.push("npx prisma migrate dev");
498
+ }
499
+ else if (hasDrizzle) {
500
+ lines.push("npx drizzle-kit push");
501
+ }
502
+ else if (hasSqlalchemy) {
503
+ lines.push("alembic upgrade head 2>/dev/null || echo 'No Alembic config found'");
504
+ }
505
+ lines.push("");
506
+ }
507
+ lines.push("echo '✅ Setup complete!'");
508
+ lines.push("");
509
+ return lines.join("\n");
510
+ }
511
+ /**
512
+ * Generate Makefile with common development targets.
513
+ */
514
+ generateMakefile(stack, techs) {
515
+ const lines = [
516
+ `# ${stack.name} — Makefile`,
517
+ "# Generated by Stackweld",
518
+ "",
519
+ ".PHONY: dev up down logs status test setup clean",
520
+ "",
521
+ ];
522
+ // dev target
523
+ const hasFrontend = techs.some((t) => t.category === "frontend");
524
+ const hasBackend = techs.some((t) => t.category === "backend");
525
+ const isFullStack = hasFrontend && hasBackend;
526
+ const devCmd = this.getDevCommand(techs);
527
+ lines.push("dev:");
528
+ if (techs.some((t) => t.dockerImage && (t.category === "database" || t.category === "service"))) {
529
+ lines.push("\tdocker compose up -d");
530
+ }
531
+ if (isFullStack) {
532
+ const frontendDevCmd = this.getFrontendDevCommand(techs);
533
+ const backendDevCmd = this.getBackendDevCommand(techs);
534
+ if (frontendDevCmd)
535
+ lines.push(`\tcd frontend && ${frontendDevCmd} &`);
536
+ if (backendDevCmd)
537
+ lines.push(`\tcd backend && ${backendDevCmd}`);
538
+ }
539
+ else {
540
+ lines.push(`\t${devCmd || "echo 'No dev server configured'"}`);
541
+ }
542
+ lines.push("");
543
+ // up / down / logs / status (docker)
544
+ lines.push("up:");
545
+ lines.push("\tdocker compose up -d");
546
+ lines.push("");
547
+ lines.push("down:");
548
+ lines.push("\tdocker compose down");
549
+ lines.push("");
550
+ lines.push("logs:");
551
+ lines.push("\tdocker compose logs -f");
552
+ lines.push("");
553
+ lines.push("status:");
554
+ lines.push("\tdocker compose ps");
555
+ lines.push("");
556
+ // test target
557
+ const testCmd = this.getTestCommand(techs);
558
+ lines.push("test:");
559
+ lines.push(`\t${testCmd}`);
560
+ lines.push("");
561
+ // setup target
562
+ lines.push("setup:");
563
+ lines.push("\tbash scripts/setup.sh");
564
+ lines.push("");
565
+ // clean target
566
+ lines.push("clean:");
567
+ lines.push("\tdocker compose down -v");
568
+ const hasNode = techs.some((t) => t.id === "nodejs" || t.id === "bun");
569
+ const hasPython = techs.some((t) => t.id === "python");
570
+ if (hasNode)
571
+ lines.push("\trm -rf node_modules dist .next .nuxt .output");
572
+ if (hasPython)
573
+ lines.push("\trm -rf .venv __pycache__");
574
+ if (techs.some((t) => t.id === "rust"))
575
+ lines.push("\tcargo clean");
576
+ lines.push("");
577
+ return lines.join("\n");
578
+ }
579
+ /**
580
+ * Generate .vscode/settings.json with formatter and extensions.
581
+ */
582
+ generateVscodeSettings(techs) {
583
+ const settings = {};
584
+ const recommendations = [];
585
+ // Formatter detection
586
+ const hasBiome = techs.some((t) => t.id === "biome");
587
+ const hasPrettier = techs.some((t) => t.id === "prettier");
588
+ const hasEslint = techs.some((t) => t.id === "eslint");
589
+ const hasPython = techs.some((t) => t.id === "python");
590
+ const hasGo = techs.some((t) => t.id === "go");
591
+ const hasRust = techs.some((t) => t.id === "rust");
592
+ const hasDocker = techs.some((t) => t.id === "docker" || t.dockerImage);
593
+ const hasTailwind = techs.some((t) => t.id === "tailwindcss");
594
+ const hasPrisma = techs.some((t) => t.id === "prisma");
595
+ if (hasBiome) {
596
+ settings["editor.defaultFormatter"] = "biomejs.biome";
597
+ settings["editor.formatOnSave"] = true;
598
+ recommendations.push("biomejs.biome");
599
+ }
600
+ else if (hasPrettier) {
601
+ settings["editor.defaultFormatter"] = "esbenp.prettier-vscode";
602
+ settings["editor.formatOnSave"] = true;
603
+ recommendations.push("esbenp.prettier-vscode");
604
+ }
605
+ if (hasEslint) {
606
+ settings["editor.codeActionsOnSave"] = {
607
+ "source.fixAll.eslint": "explicit",
608
+ };
609
+ recommendations.push("dbaeumer.vscode-eslint");
610
+ }
611
+ if (hasPython) {
612
+ settings["[python]"] = {
613
+ "editor.defaultFormatter": "ms-python.black-formatter",
614
+ };
615
+ recommendations.push("ms-python.python");
616
+ recommendations.push("ms-python.black-formatter");
617
+ }
618
+ if (hasGo) {
619
+ settings["[go]"] = {
620
+ "editor.defaultFormatter": "golang.go",
621
+ };
622
+ recommendations.push("golang.go");
623
+ }
624
+ if (hasRust) {
625
+ recommendations.push("rust-lang.rust-analyzer");
626
+ }
627
+ if (hasDocker) {
628
+ recommendations.push("ms-azuretools.vscode-docker");
629
+ }
630
+ if (hasTailwind) {
631
+ recommendations.push("bradlc.vscode-tailwindcss");
632
+ }
633
+ if (hasPrisma) {
634
+ recommendations.push("Prisma.prisma");
635
+ }
636
+ // Always recommend
637
+ recommendations.push("EditorConfig.EditorConfig");
638
+ const output = { ...settings };
639
+ const _result = `${JSON.stringify(output, null, 2)}\n`;
640
+ // We embed recommendations as a separate extensions.json-style comment
641
+ // but since .vscode/settings.json doesn't support recommendations,
642
+ // we'll return settings.json content and note the extensions
643
+ // The extensions will be written to .vscode/extensions.json by the CLI
644
+ return `${JSON.stringify({
645
+ ...output,
646
+ "stackweld.recommendedExtensions": recommendations,
647
+ }, null, 2)}\n`;
648
+ }
649
+ /**
650
+ * Generate .github/workflows/ci.yml for the user's project.
651
+ */
652
+ generateCiWorkflow(_stack, techs) {
653
+ const lines = [
654
+ `name: CI`,
655
+ "",
656
+ "on:",
657
+ " push:",
658
+ " branches: [main]",
659
+ " pull_request:",
660
+ " branches: [main]",
661
+ "",
662
+ "jobs:",
663
+ " ci:",
664
+ " runs-on: ubuntu-latest",
665
+ "",
666
+ ];
667
+ // Services (databases for CI)
668
+ const dbTechs = techs.filter((t) => t.category === "database" && t.dockerImage);
669
+ if (dbTechs.length > 0) {
670
+ lines.push(" services:");
671
+ for (const db of dbTechs) {
672
+ lines.push(` ${db.id}:`);
673
+ lines.push(` image: ${db.dockerImage}`);
674
+ const envEntries = Object.entries(db.envVars);
675
+ if (envEntries.length > 0) {
676
+ lines.push(" env:");
677
+ for (const [key, value] of envEntries) {
678
+ lines.push(` ${key}: ${value}`);
679
+ }
680
+ }
681
+ if (db.defaultPort) {
682
+ lines.push(" ports:");
683
+ lines.push(` - ${db.defaultPort}:${db.defaultPort}`);
684
+ }
685
+ if (db.healthCheck) {
686
+ lines.push(" options: >-");
687
+ if (db.healthCheck.command) {
688
+ lines.push(` --health-cmd "${db.healthCheck.command}"`);
689
+ }
690
+ lines.push(` --health-interval ${db.healthCheck.interval || "10s"}`);
691
+ lines.push(` --health-timeout ${db.healthCheck.timeout || "5s"}`);
692
+ lines.push(` --health-retries ${db.healthCheck.retries || 5}`);
693
+ }
694
+ }
695
+ lines.push("");
696
+ }
697
+ lines.push(" steps:");
698
+ lines.push(" - uses: actions/checkout@v4");
699
+ lines.push("");
700
+ // Setup runtime
701
+ const hasNode = techs.some((t) => t.id === "nodejs");
702
+ const hasBun = techs.some((t) => t.id === "bun");
703
+ const hasPython = techs.some((t) => t.id === "python");
704
+ const hasGo = techs.some((t) => t.id === "go");
705
+ const hasRust = techs.some((t) => t.id === "rust");
706
+ if (hasNode || hasBun) {
707
+ const nodeVersion = techs.find((t) => t.id === "nodejs")?.defaultVersion || "22";
708
+ lines.push(" - uses: actions/setup-node@v4");
709
+ lines.push(" with:");
710
+ lines.push(` node-version: "${nodeVersion}"`);
711
+ lines.push("");
712
+ }
713
+ if (hasBun) {
714
+ lines.push(" - uses: oven-sh/setup-bun@v2");
715
+ lines.push("");
716
+ }
717
+ if (hasPython) {
718
+ const pyVersion = techs.find((t) => t.id === "python")?.defaultVersion || "3.12";
719
+ lines.push(" - uses: actions/setup-python@v5");
720
+ lines.push(" with:");
721
+ lines.push(` python-version: "${pyVersion}"`);
722
+ lines.push("");
723
+ }
724
+ if (hasGo) {
725
+ const goVersion = techs.find((t) => t.id === "go")?.defaultVersion || "1.22";
726
+ lines.push(" - uses: actions/setup-go@v5");
727
+ lines.push(" with:");
728
+ lines.push(` go-version: "${goVersion}"`);
729
+ lines.push("");
730
+ }
731
+ if (hasRust) {
732
+ lines.push(" - uses: dtolnay/rust-toolchain@stable");
733
+ lines.push("");
734
+ }
735
+ // Install dependencies
736
+ if (hasNode || hasBun) {
737
+ const installCmd = hasBun ? "bun install" : "npm ci";
738
+ lines.push(` - name: Install dependencies`);
739
+ lines.push(` run: ${installCmd}`);
740
+ lines.push("");
741
+ }
742
+ if (hasPython) {
743
+ lines.push(" - name: Install dependencies");
744
+ lines.push(" run: pip install -r requirements.txt");
745
+ lines.push("");
746
+ }
747
+ if (hasGo) {
748
+ lines.push(" - name: Download modules");
749
+ lines.push(" run: go mod download");
750
+ lines.push("");
751
+ }
752
+ // Lint step
753
+ const hasBiome = techs.some((t) => t.id === "biome");
754
+ const hasEslint = techs.some((t) => t.id === "eslint");
755
+ if (hasBiome) {
756
+ lines.push(" - name: Lint");
757
+ lines.push(" run: npx biome check .");
758
+ lines.push("");
759
+ }
760
+ else if (hasEslint) {
761
+ lines.push(" - name: Lint");
762
+ lines.push(" run: npx eslint .");
763
+ lines.push("");
764
+ }
765
+ else if (hasGo) {
766
+ lines.push(" - name: Lint");
767
+ lines.push(" run: go vet ./...");
768
+ lines.push("");
769
+ }
770
+ else if (hasRust) {
771
+ lines.push(" - name: Lint");
772
+ lines.push(" run: cargo clippy -- -D warnings");
773
+ lines.push("");
774
+ }
775
+ // Test step
776
+ const testCmd = this.getTestCommand(techs);
777
+ lines.push(" - name: Test");
778
+ lines.push(` run: ${testCmd}`);
779
+ lines.push("");
780
+ // Build step
781
+ const buildCmd = this.getBuildCommand(techs);
782
+ if (buildCmd) {
783
+ lines.push(" - name: Build");
784
+ lines.push(` run: ${buildCmd}`);
785
+ lines.push("");
786
+ }
787
+ return lines.join("\n");
788
+ }
789
+ /**
790
+ * Determine all directories that should be created during scaffolding.
791
+ * Ensures frontend/, backend/, scripts/, .vscode/, .devcontainer/,
792
+ * .github/workflows/, src/, and tests/ directories are included as needed.
793
+ */
794
+ getRequiredDirectories(techs) {
795
+ const dirs = new Set();
796
+ // Always create these
797
+ dirs.add("scripts");
798
+ dirs.add(".vscode");
799
+ dirs.add(".devcontainer");
800
+ dirs.add(".github/workflows");
801
+ const hasFrontend = techs.some((t) => t.category === "frontend");
802
+ const hasBackend = techs.some((t) => t.category === "backend");
803
+ const isFullStack = hasFrontend && hasBackend;
804
+ if (isFullStack) {
805
+ // Full-stack layout: frontend/ and backend/ directories
806
+ dirs.add("frontend");
807
+ dirs.add("frontend/src");
808
+ dirs.add("frontend/tests");
809
+ dirs.add("backend");
810
+ dirs.add("backend/src");
811
+ dirs.add("backend/tests");
812
+ }
813
+ else {
814
+ // Single-app layout: src/ and tests/ at root
815
+ dirs.add("src");
816
+ dirs.add("tests");
817
+ if (hasFrontend) {
818
+ dirs.add("public");
819
+ }
820
+ }
821
+ return [...dirs].sort();
822
+ }
823
+ // ─── Private helpers ───────────────────────────────
824
+ getDataMount(techId) {
825
+ const mounts = {
826
+ postgresql: "/var/lib/postgresql/data",
827
+ mysql: "/var/lib/mysql",
828
+ mongodb: "/data/db",
829
+ redis: "/data",
830
+ };
831
+ return mounts[techId] ?? null;
832
+ }
833
+ getDevCommand(techs) {
834
+ const hasBun = techs.some((t) => t.id === "bun");
835
+ const hasNext = techs.some((t) => t.id === "nextjs");
836
+ const hasNuxt = techs.some((t) => t.id === "nuxt");
837
+ const hasVite = techs.some((t) => t.id === "vite");
838
+ const hasAstro = techs.some((t) => t.id === "astro");
839
+ const hasFastapi = techs.some((t) => t.id === "fastapi");
840
+ const hasDjango = techs.some((t) => t.id === "django");
841
+ const hasExpress = techs.some((t) => t.id === "express");
842
+ const hasGo = techs.some((t) => t.id === "go");
843
+ const hasRust = techs.some((t) => t.id === "rust");
844
+ if (hasNext)
845
+ return hasBun ? "bun run dev" : "npm run dev";
846
+ if (hasNuxt)
847
+ return hasBun ? "bun run dev" : "npm run dev";
848
+ if (hasVite)
849
+ return hasBun ? "bun run dev" : "npm run dev";
850
+ if (hasAstro)
851
+ return hasBun ? "bun run dev" : "npm run dev";
852
+ if (hasExpress)
853
+ return hasBun ? "bun run dev" : "npm run dev";
854
+ if (hasFastapi)
855
+ return "uvicorn main:app --reload";
856
+ if (hasDjango)
857
+ return "python manage.py runserver";
858
+ if (hasGo)
859
+ return "go run .";
860
+ if (hasRust)
861
+ return "cargo run";
862
+ return null;
863
+ }
864
+ getFrontendDevCommand(techs) {
865
+ const hasBun = techs.some((t) => t.id === "bun");
866
+ const frontendTechs = techs.filter((t) => t.category === "frontend");
867
+ for (const t of frontendTechs) {
868
+ if (["nextjs", "nuxt", "vite", "astro", "sveltekit", "remix"].includes(t.id)) {
869
+ return hasBun ? "bun run dev" : "npm run dev";
870
+ }
871
+ }
872
+ return hasBun ? "bun run dev" : "npm run dev";
873
+ }
874
+ getBackendDevCommand(techs) {
875
+ const hasBun = techs.some((t) => t.id === "bun");
876
+ const backendTechs = techs.filter((t) => t.category === "backend");
877
+ for (const t of backendTechs) {
878
+ if (t.id === "fastapi")
879
+ return "uvicorn config.main:app --reload";
880
+ if (t.id === "django")
881
+ return "python manage.py runserver";
882
+ if (t.id === "flask")
883
+ return "flask run --reload";
884
+ if (t.id === "express" || t.id === "nestjs" || t.id === "fastify" || t.id === "hono") {
885
+ return hasBun ? "bun run dev" : "npm run dev";
886
+ }
887
+ if (t.id === "gin" || t.id === "echo")
888
+ return "go run .";
889
+ }
890
+ return null;
891
+ }
892
+ getTestCommand(techs) {
893
+ const hasBun = techs.some((t) => t.id === "bun");
894
+ const hasVitest = techs.some((t) => t.id === "vitest");
895
+ const hasJest = techs.some((t) => t.id === "jest");
896
+ const hasPytest = techs.some((t) => t.id === "pytest");
897
+ const hasPython = techs.some((t) => t.id === "python");
898
+ const hasGo = techs.some((t) => t.id === "go");
899
+ const hasRust = techs.some((t) => t.id === "rust");
900
+ if (hasVitest)
901
+ return "npx vitest run";
902
+ if (hasJest)
903
+ return "npx jest";
904
+ if (hasPytest)
905
+ return "pytest";
906
+ if (hasPython)
907
+ return "python -m pytest";
908
+ if (hasGo)
909
+ return "go test ./...";
910
+ if (hasRust)
911
+ return "cargo test";
912
+ if (hasBun)
913
+ return "bun test";
914
+ return "npm test";
915
+ }
916
+ getBuildCommand(techs) {
917
+ const hasBun = techs.some((t) => t.id === "bun");
918
+ const hasNext = techs.some((t) => t.id === "nextjs");
919
+ const hasNuxt = techs.some((t) => t.id === "nuxt");
920
+ const hasVite = techs.some((t) => t.id === "vite");
921
+ const hasAstro = techs.some((t) => t.id === "astro");
922
+ const hasGo = techs.some((t) => t.id === "go");
923
+ const hasRust = techs.some((t) => t.id === "rust");
924
+ if (hasNext || hasNuxt || hasVite || hasAstro) {
925
+ return hasBun ? "bun run build" : "npm run build";
926
+ }
927
+ if (hasGo)
928
+ return "go build -o bin/app .";
929
+ if (hasRust)
930
+ return "cargo build --release";
931
+ return null;
932
+ }
933
+ }
934
+ //# sourceMappingURL=scaffold-orchestrator.js.map