@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.
- package/.turbo/turbo-build.log +4 -0
- package/.turbo/turbo-lint.log +498 -0
- package/.turbo/turbo-test.log +21 -0
- package/.turbo/turbo-typecheck.log +4 -0
- package/dist/__tests__/compatibility-scorer.test.d.ts +2 -0
- package/dist/__tests__/compatibility-scorer.test.d.ts.map +1 -0
- package/dist/__tests__/compatibility-scorer.test.js +226 -0
- package/dist/__tests__/compatibility-scorer.test.js.map +1 -0
- package/dist/__tests__/rules-engine.test.d.ts +2 -0
- package/dist/__tests__/rules-engine.test.d.ts.map +1 -0
- package/dist/__tests__/rules-engine.test.js +161 -0
- package/dist/__tests__/rules-engine.test.js.map +1 -0
- package/dist/__tests__/scaffold-orchestrator.test.d.ts +2 -0
- package/dist/__tests__/scaffold-orchestrator.test.d.ts.map +1 -0
- package/dist/__tests__/scaffold-orchestrator.test.js +149 -0
- package/dist/__tests__/scaffold-orchestrator.test.js.map +1 -0
- package/dist/__tests__/stack-engine.test.d.ts +2 -0
- package/dist/__tests__/stack-engine.test.d.ts.map +1 -0
- package/dist/__tests__/stack-engine.test.js +278 -0
- package/dist/__tests__/stack-engine.test.js.map +1 -0
- package/dist/db/database.d.ts +9 -0
- package/dist/db/database.d.ts.map +1 -0
- package/dist/db/database.js +106 -0
- package/dist/db/database.js.map +1 -0
- package/dist/db/index.d.ts +2 -0
- package/dist/db/index.d.ts.map +1 -0
- package/dist/db/index.js +2 -0
- package/dist/db/index.js.map +1 -0
- package/dist/engine/compatibility-scorer.d.ts +37 -0
- package/dist/engine/compatibility-scorer.d.ts.map +1 -0
- package/dist/engine/compatibility-scorer.js +178 -0
- package/dist/engine/compatibility-scorer.js.map +1 -0
- package/dist/engine/compose-generator.d.ts +35 -0
- package/dist/engine/compose-generator.d.ts.map +1 -0
- package/dist/engine/compose-generator.js +95 -0
- package/dist/engine/compose-generator.js.map +1 -0
- package/dist/engine/cost-estimator.d.ts +22 -0
- package/dist/engine/cost-estimator.d.ts.map +1 -0
- package/dist/engine/cost-estimator.js +451 -0
- package/dist/engine/cost-estimator.js.map +1 -0
- package/dist/engine/env-analyzer.d.ts +36 -0
- package/dist/engine/env-analyzer.d.ts.map +1 -0
- package/dist/engine/env-analyzer.js +111 -0
- package/dist/engine/env-analyzer.js.map +1 -0
- package/dist/engine/health-checker.d.ts +20 -0
- package/dist/engine/health-checker.d.ts.map +1 -0
- package/dist/engine/health-checker.js +377 -0
- package/dist/engine/health-checker.js.map +1 -0
- package/dist/engine/index.d.ts +11 -0
- package/dist/engine/index.d.ts.map +1 -0
- package/dist/engine/index.js +7 -0
- package/dist/engine/index.js.map +1 -0
- package/dist/engine/infra-generator.d.ts +26 -0
- package/dist/engine/infra-generator.d.ts.map +1 -0
- package/dist/engine/infra-generator.js +751 -0
- package/dist/engine/infra-generator.js.map +1 -0
- package/dist/engine/migration-planner.d.ts +34 -0
- package/dist/engine/migration-planner.d.ts.map +1 -0
- package/dist/engine/migration-planner.js +427 -0
- package/dist/engine/migration-planner.js.map +1 -0
- package/dist/engine/performance-profiler.d.ts +22 -0
- package/dist/engine/performance-profiler.d.ts.map +1 -0
- package/dist/engine/performance-profiler.js +292 -0
- package/dist/engine/performance-profiler.js.map +1 -0
- package/dist/engine/plugin-loader.d.ts +36 -0
- package/dist/engine/plugin-loader.d.ts.map +1 -0
- package/dist/engine/plugin-loader.js +157 -0
- package/dist/engine/plugin-loader.js.map +1 -0
- package/dist/engine/preferences.d.ts +24 -0
- package/dist/engine/preferences.d.ts.map +1 -0
- package/dist/engine/preferences.js +62 -0
- package/dist/engine/preferences.js.map +1 -0
- package/dist/engine/rules-engine.d.ts +31 -0
- package/dist/engine/rules-engine.d.ts.map +1 -0
- package/dist/engine/rules-engine.js +179 -0
- package/dist/engine/rules-engine.js.map +1 -0
- package/dist/engine/runtime-manager.d.ts +65 -0
- package/dist/engine/runtime-manager.d.ts.map +1 -0
- package/dist/engine/runtime-manager.js +181 -0
- package/dist/engine/runtime-manager.js.map +1 -0
- package/dist/engine/scaffold-orchestrator.d.ts +103 -0
- package/dist/engine/scaffold-orchestrator.d.ts.map +1 -0
- package/dist/engine/scaffold-orchestrator.js +934 -0
- package/dist/engine/scaffold-orchestrator.js.map +1 -0
- package/dist/engine/stack-detector.d.ts +21 -0
- package/dist/engine/stack-detector.d.ts.map +1 -0
- package/dist/engine/stack-detector.js +313 -0
- package/dist/engine/stack-detector.js.map +1 -0
- package/dist/engine/stack-differ.d.ts +26 -0
- package/dist/engine/stack-differ.d.ts.map +1 -0
- package/dist/engine/stack-differ.js +80 -0
- package/dist/engine/stack-differ.js.map +1 -0
- package/dist/engine/stack-engine.d.ts +54 -0
- package/dist/engine/stack-engine.d.ts.map +1 -0
- package/dist/engine/stack-engine.js +186 -0
- package/dist/engine/stack-engine.js.map +1 -0
- package/dist/engine/stack-serializer.d.ts +32 -0
- package/dist/engine/stack-serializer.d.ts.map +1 -0
- package/dist/engine/stack-serializer.js +75 -0
- package/dist/engine/stack-serializer.js.map +1 -0
- package/dist/engine/standards-linter.d.ts +34 -0
- package/dist/engine/standards-linter.d.ts.map +1 -0
- package/dist/engine/standards-linter.js +162 -0
- package/dist/engine/standards-linter.js.map +1 -0
- package/dist/engine/tech-installer.d.ts +37 -0
- package/dist/engine/tech-installer.d.ts.map +1 -0
- package/dist/engine/tech-installer.js +508 -0
- package/dist/engine/tech-installer.js.map +1 -0
- package/dist/index.d.ts +39 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +25 -0
- package/dist/index.js.map +1 -0
- package/dist/types/index.d.ts +6 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +2 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/project.d.ts +33 -0
- package/dist/types/project.d.ts.map +1 -0
- package/dist/types/project.js +6 -0
- package/dist/types/project.js.map +1 -0
- package/dist/types/stack.d.ts +29 -0
- package/dist/types/stack.d.ts.map +1 -0
- package/dist/types/stack.js +6 -0
- package/dist/types/stack.js.map +1 -0
- package/dist/types/technology.d.ts +47 -0
- package/dist/types/technology.d.ts.map +1 -0
- package/dist/types/technology.js +6 -0
- package/dist/types/technology.js.map +1 -0
- package/dist/types/template.d.ts +34 -0
- package/dist/types/template.d.ts.map +1 -0
- package/dist/types/template.js +6 -0
- package/dist/types/template.js.map +1 -0
- package/dist/types/validation.d.ts +20 -0
- package/dist/types/validation.d.ts.map +1 -0
- package/dist/types/validation.js +5 -0
- package/dist/types/validation.js.map +1 -0
- package/package.json +39 -0
- package/src/__tests__/compatibility-scorer.test.ts +264 -0
- package/src/__tests__/rules-engine.test.ts +170 -0
- package/src/__tests__/scaffold-orchestrator.test.ts +161 -0
- package/src/__tests__/stack-engine.test.ts +328 -0
- package/src/db/database.ts +112 -0
- package/src/db/index.ts +1 -0
- package/src/engine/compatibility-scorer.ts +222 -0
- package/src/engine/compose-generator.ts +134 -0
- package/src/engine/cost-estimator.ts +498 -0
- package/src/engine/env-analyzer.ts +156 -0
- package/src/engine/health-checker.ts +421 -0
- package/src/engine/index.ts +17 -0
- package/src/engine/infra-generator.ts +837 -0
- package/src/engine/migration-planner.ts +496 -0
- package/src/engine/performance-profiler.ts +354 -0
- package/src/engine/plugin-loader.ts +216 -0
- package/src/engine/preferences.ts +85 -0
- package/src/engine/rules-engine.ts +204 -0
- package/src/engine/runtime-manager.ts +207 -0
- package/src/engine/scaffold-orchestrator.ts +1052 -0
- package/src/engine/stack-detector.ts +345 -0
- package/src/engine/stack-differ.ts +118 -0
- package/src/engine/stack-engine.ts +258 -0
- package/src/engine/stack-serializer.ts +95 -0
- package/src/engine/standards-linter.ts +210 -0
- package/src/engine/tech-installer.ts +650 -0
- package/src/index.ts +78 -0
- package/src/types/index.ts +10 -0
- package/src/types/project.ts +36 -0
- package/src/types/stack.ts +32 -0
- package/src/types/technology.ts +58 -0
- package/src/types/template.ts +37 -0
- package/src/types/validation.ts +22 -0
- package/tsconfig.json +10 -0
- 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
|