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