@stackweld/cli 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 (81) hide show
  1. package/dist/__tests__/commands.test.d.ts +2 -0
  2. package/dist/__tests__/commands.test.js +275 -0
  3. package/dist/commands/ai.d.ts +8 -0
  4. package/dist/commands/ai.js +167 -0
  5. package/dist/commands/analyze.d.ts +6 -0
  6. package/dist/commands/analyze.js +90 -0
  7. package/dist/commands/benchmark.d.ts +6 -0
  8. package/dist/commands/benchmark.js +86 -0
  9. package/dist/commands/browse.d.ts +6 -0
  10. package/dist/commands/browse.js +101 -0
  11. package/dist/commands/clone.d.ts +3 -0
  12. package/dist/commands/clone.js +37 -0
  13. package/dist/commands/compare.d.ts +6 -0
  14. package/dist/commands/compare.js +93 -0
  15. package/dist/commands/completion.d.ts +6 -0
  16. package/dist/commands/completion.js +86 -0
  17. package/dist/commands/config.d.ts +6 -0
  18. package/dist/commands/config.js +56 -0
  19. package/dist/commands/cost.d.ts +6 -0
  20. package/dist/commands/cost.js +101 -0
  21. package/dist/commands/create.d.ts +7 -0
  22. package/dist/commands/create.js +111 -0
  23. package/dist/commands/delete.d.ts +6 -0
  24. package/dist/commands/delete.js +33 -0
  25. package/dist/commands/deploy.d.ts +6 -0
  26. package/dist/commands/deploy.js +90 -0
  27. package/dist/commands/doctor.d.ts +6 -0
  28. package/dist/commands/doctor.js +144 -0
  29. package/dist/commands/down.d.ts +6 -0
  30. package/dist/commands/down.js +37 -0
  31. package/dist/commands/env.d.ts +6 -0
  32. package/dist/commands/env.js +129 -0
  33. package/dist/commands/export-stack.d.ts +6 -0
  34. package/dist/commands/export-stack.js +51 -0
  35. package/dist/commands/generate.d.ts +9 -0
  36. package/dist/commands/generate.js +542 -0
  37. package/dist/commands/health.d.ts +6 -0
  38. package/dist/commands/health.js +68 -0
  39. package/dist/commands/import-stack.d.ts +6 -0
  40. package/dist/commands/import-stack.js +68 -0
  41. package/dist/commands/info.d.ts +6 -0
  42. package/dist/commands/info.js +56 -0
  43. package/dist/commands/init.d.ts +6 -0
  44. package/dist/commands/init.js +186 -0
  45. package/dist/commands/learn.d.ts +6 -0
  46. package/dist/commands/learn.js +91 -0
  47. package/dist/commands/lint.d.ts +6 -0
  48. package/dist/commands/lint.js +193 -0
  49. package/dist/commands/list.d.ts +6 -0
  50. package/dist/commands/list.js +27 -0
  51. package/dist/commands/logs.d.ts +6 -0
  52. package/dist/commands/logs.js +37 -0
  53. package/dist/commands/migrate.d.ts +6 -0
  54. package/dist/commands/migrate.js +57 -0
  55. package/dist/commands/plugin.d.ts +8 -0
  56. package/dist/commands/plugin.js +131 -0
  57. package/dist/commands/preview.d.ts +7 -0
  58. package/dist/commands/preview.js +100 -0
  59. package/dist/commands/save.d.ts +6 -0
  60. package/dist/commands/save.js +32 -0
  61. package/dist/commands/scaffold.d.ts +7 -0
  62. package/dist/commands/scaffold.js +100 -0
  63. package/dist/commands/score.d.ts +9 -0
  64. package/dist/commands/score.js +111 -0
  65. package/dist/commands/share.d.ts +10 -0
  66. package/dist/commands/share.js +93 -0
  67. package/dist/commands/status.d.ts +6 -0
  68. package/dist/commands/status.js +39 -0
  69. package/dist/commands/template.d.ts +3 -0
  70. package/dist/commands/template.js +162 -0
  71. package/dist/commands/up.d.ts +6 -0
  72. package/dist/commands/up.js +54 -0
  73. package/dist/commands/version-cmd.d.ts +6 -0
  74. package/dist/commands/version-cmd.js +100 -0
  75. package/dist/index.d.ts +6 -0
  76. package/dist/index.js +160 -0
  77. package/dist/ui/context.d.ts +10 -0
  78. package/dist/ui/context.js +90 -0
  79. package/dist/ui/format.d.ts +59 -0
  80. package/dist/ui/format.js +295 -0
  81. package/package.json +52 -0
@@ -0,0 +1,542 @@
1
+ /**
2
+ * stackweld generate — Smart project generator.
3
+ * Detects project type (full-stack, frontend-only, backend-only),
4
+ * creates proper directory structure, executes official scaffolds,
5
+ * generates per-directory .env files, and leaves everything ready to code.
6
+ */
7
+ import { execSync } from "node:child_process";
8
+ import * as fs from "node:fs";
9
+ import * as path from "node:path";
10
+ import { installTechnologies } from "@stackweld/core";
11
+ import chalk from "chalk";
12
+ import { Command } from "commander";
13
+ import ora from "ora";
14
+ import { getRulesEngine, getScaffoldOrchestrator, getStackEngine } from "../ui/context.js";
15
+ import { box, error, formatValidation, gradientHeader, nextSteps, stepIndicator, warning, } from "../ui/format.js";
16
+ // ─── Helpers ────────────────────────────────────────
17
+ function run(cmd, cwd, timeoutMs = 120_000) {
18
+ try {
19
+ const out = execSync(cmd, {
20
+ cwd,
21
+ stdio: "pipe",
22
+ timeout: timeoutMs,
23
+ }).toString();
24
+ return { success: true, output: out };
25
+ }
26
+ catch (e) {
27
+ return {
28
+ success: false,
29
+ output: e instanceof Error ? e.message.slice(0, 200) : String(e),
30
+ };
31
+ }
32
+ }
33
+ function writeFile(filePath, content) {
34
+ const dir = path.dirname(filePath);
35
+ if (!fs.existsSync(dir))
36
+ fs.mkdirSync(dir, { recursive: true });
37
+ fs.writeFileSync(filePath, content, "utf-8");
38
+ }
39
+ function makeExecutable(filePath) {
40
+ fs.chmodSync(filePath, 0o755);
41
+ }
42
+ // ─── Frontend scaffolders ───────────────────────────
43
+ function scaffoldFrontend(tech, frontendDir, parentDir, projectName) {
44
+ const log = [];
45
+ fs.mkdirSync(frontendDir, { recursive: true });
46
+ if (tech.id === "nextjs") {
47
+ const r = run(`npx create-next-app@latest frontend --typescript --tailwind --app --src-dir --import-alias "@/*" --no-git --yes`, parentDir);
48
+ log.push(r.success ? `Next.js scaffolded` : `Next.js (manual: npx create-next-app@latest)`);
49
+ }
50
+ else if (tech.id === "nuxt") {
51
+ const r = run(`npx nuxi@latest init frontend --no-git --force`, parentDir);
52
+ log.push(r.success ? `Nuxt scaffolded` : `Nuxt (manual: npx nuxi init)`);
53
+ }
54
+ else if (tech.id === "sveltekit") {
55
+ const r = run(`npx sv create frontend --template minimal --types ts`, parentDir);
56
+ log.push(r.success ? `SvelteKit scaffolded` : `SvelteKit (manual: npx sv create)`);
57
+ }
58
+ else if (tech.id === "astro") {
59
+ const r = run(`npm create astro@latest frontend -- --template minimal --install --no-git --typescript strict --yes`, parentDir);
60
+ log.push(r.success ? `Astro scaffolded` : `Astro (manual: npm create astro@latest)`);
61
+ }
62
+ else if (tech.id === "remix") {
63
+ const r = run(`npx create-remix@latest frontend --no-git --yes`, parentDir);
64
+ log.push(r.success ? `Remix scaffolded` : `Remix (manual: npx create-remix@latest)`);
65
+ }
66
+ else if (tech.id === "angular") {
67
+ const r = run(`npx @angular/cli new frontend --skip-git --defaults`, parentDir);
68
+ log.push(r.success ? `Angular scaffolded` : `Angular (manual: npx @angular/cli new)`);
69
+ }
70
+ else if (tech.id === "react" || tech.id === "solidjs" || tech.id === "qwik") {
71
+ const template = tech.id === "solidjs" ? "solid-ts" : tech.id === "qwik" ? "qwik-ts" : "react-ts";
72
+ const r = run(`npm create vite@latest frontend -- --template ${template}`, parentDir);
73
+ log.push(r.success
74
+ ? `${tech.name} (Vite) scaffolded`
75
+ : `${tech.name} (manual: npm create vite@latest)`);
76
+ }
77
+ else {
78
+ log.push(`${tech.name} (create manually in frontend/)`);
79
+ }
80
+ const envLines = [
81
+ `# ${projectName} — Frontend Environment Variables`,
82
+ "",
83
+ `NEXT_PUBLIC_API_URL=http://localhost:8000`,
84
+ `VITE_API_URL=http://localhost:8000`,
85
+ `PUBLIC_API_URL=http://localhost:8000`,
86
+ "",
87
+ ];
88
+ writeFile(path.join(frontendDir, ".env.example"), envLines.join("\n"));
89
+ log.push("frontend/.env.example created");
90
+ return log;
91
+ }
92
+ // ─── Backend scaffolders ────────────────────────────
93
+ function scaffoldBackend(tech, backendDir, _parentDir, projectName, allTechs) {
94
+ const log = [];
95
+ fs.mkdirSync(backendDir, { recursive: true });
96
+ const hasPostgres = allTechs.some((t) => t.id === "postgresql");
97
+ const hasRedis = allTechs.some((t) => t.id === "redis");
98
+ const hasSqlite = allTechs.some((t) => t.id === "sqlite");
99
+ const dbUrl = hasPostgres
100
+ ? `postgresql://postgres:postgres@localhost:5432/${projectName}`
101
+ : hasSqlite
102
+ ? "file:./dev.db"
103
+ : `mysql://root:root@localhost:3306/${projectName}`;
104
+ // Python backends
105
+ if (tech.id === "django" || tech.id === "fastapi" || tech.id === "flask") {
106
+ run(`python3 -m venv .venv`, backendDir, 30_000);
107
+ const pip = path.join(backendDir, ".venv/bin/pip");
108
+ if (tech.id === "django") {
109
+ run(`${pip} install django djangorestframework django-cors-headers python-dotenv psycopg2-binary`, backendDir, 90_000);
110
+ run(`${path.join(backendDir, ".venv/bin/django-admin")} startproject config .`, backendDir, 15_000);
111
+ writeFile(path.join(backendDir, "requirements.txt"), `${[
112
+ "django>=5.1",
113
+ "djangorestframework>=3.15",
114
+ "django-cors-headers>=4.4",
115
+ "python-dotenv>=1.0",
116
+ "psycopg2-binary>=2.9",
117
+ "gunicorn>=22.0",
118
+ hasRedis ? "django-redis>=5.4" : "",
119
+ ]
120
+ .filter(Boolean)
121
+ .join("\n")}\n`);
122
+ log.push("Django project created");
123
+ }
124
+ else if (tech.id === "fastapi") {
125
+ run(`${pip} install fastapi uvicorn sqlalchemy alembic python-dotenv psycopg2-binary`, backendDir, 90_000);
126
+ writeFile(path.join(backendDir, "main.py"), `${[
127
+ `from fastapi import FastAPI`,
128
+ `from fastapi.middleware.cors import CORSMiddleware`,
129
+ ``,
130
+ `app = FastAPI(title="${projectName}")`,
131
+ ``,
132
+ `app.add_middleware(`,
133
+ ` CORSMiddleware,`,
134
+ ` allow_origins=["http://localhost:3000", "http://localhost:5173"],`,
135
+ ` allow_credentials=True,`,
136
+ ` allow_methods=["*"],`,
137
+ ` allow_headers=["*"],`,
138
+ `)`,
139
+ ``,
140
+ `@app.get("/health")`,
141
+ `def health():`,
142
+ ` return {"status": "ok"}`,
143
+ ``,
144
+ `@app.get("/api")`,
145
+ `def root():`,
146
+ ` return {"message": "Welcome to ${projectName} API"}`,
147
+ ].join("\n")}\n`);
148
+ writeFile(path.join(backendDir, "requirements.txt"), `${[
149
+ "fastapi>=0.115",
150
+ "uvicorn[standard]>=0.30",
151
+ "sqlalchemy>=2.0",
152
+ "alembic>=1.13",
153
+ "python-dotenv>=1.0",
154
+ "psycopg2-binary>=2.9",
155
+ hasRedis ? "redis>=5.0" : "",
156
+ ]
157
+ .filter(Boolean)
158
+ .join("\n")}\n`);
159
+ log.push("FastAPI project created");
160
+ }
161
+ else if (tech.id === "flask") {
162
+ run(`${pip} install flask flask-cors python-dotenv`, backendDir, 60_000);
163
+ writeFile(path.join(backendDir, "app.py"), `${[
164
+ `from flask import Flask, jsonify`,
165
+ `from flask_cors import CORS`,
166
+ ``,
167
+ `app = Flask(__name__)`,
168
+ `CORS(app)`,
169
+ ``,
170
+ `@app.route("/health")`,
171
+ `def health():`,
172
+ ` return jsonify(status="ok")`,
173
+ ``,
174
+ `@app.route("/api")`,
175
+ `def root():`,
176
+ ` return jsonify(message="Welcome to ${projectName} API")`,
177
+ ].join("\n")}\n`);
178
+ writeFile(path.join(backendDir, "requirements.txt"), `${["flask>=3.0", "flask-cors>=4.0", "python-dotenv>=1.0"].join("\n")}\n`);
179
+ log.push("Flask project created");
180
+ }
181
+ writeFile(path.join(backendDir, ".env.example"), `${[
182
+ `# ${projectName} — Backend Environment Variables`,
183
+ ``,
184
+ `DEBUG=True`,
185
+ `SECRET_KEY=change-me-in-production`,
186
+ `DATABASE_URL=${dbUrl}`,
187
+ hasRedis ? `REDIS_URL=redis://localhost:6379/0` : "",
188
+ `ALLOWED_HOSTS=localhost,127.0.0.1`,
189
+ `CORS_ALLOWED_ORIGINS=http://localhost:3000,http://localhost:5173`,
190
+ `PORT=8000`,
191
+ ]
192
+ .filter(Boolean)
193
+ .join("\n")}\n`);
194
+ log.push("backend/.env.example created");
195
+ }
196
+ // Node.js backends
197
+ if (tech.id === "express" ||
198
+ tech.id === "fastify" ||
199
+ tech.id === "hono" ||
200
+ tech.id === "nestjs") {
201
+ if (tech.id === "nestjs") {
202
+ const r = run(`npx @nestjs/cli new backend --package-manager npm --skip-git`, backendDir.replace(/\/backend$/, ""), 120_000);
203
+ log.push(r.success ? "NestJS project created" : "NestJS (manual: npx @nestjs/cli new)");
204
+ }
205
+ else {
206
+ writeFile(path.join(backendDir, "package.json"), `${JSON.stringify({
207
+ name: `${projectName}-backend`,
208
+ version: "0.1.0",
209
+ private: true,
210
+ type: "module",
211
+ scripts: {
212
+ dev: "npx tsx watch src/index.ts",
213
+ build: "tsc",
214
+ start: "node dist/index.js",
215
+ },
216
+ }, null, 2)}\n`);
217
+ writeFile(path.join(backendDir, "tsconfig.json"), `${JSON.stringify({
218
+ compilerOptions: {
219
+ target: "ES2022",
220
+ module: "ESNext",
221
+ moduleResolution: "bundler",
222
+ outDir: "dist",
223
+ rootDir: "src",
224
+ strict: true,
225
+ esModuleInterop: true,
226
+ skipLibCheck: true,
227
+ },
228
+ include: ["src"],
229
+ }, null, 2)}\n`);
230
+ const serverCode = tech.id === "hono"
231
+ ? `import { Hono } from "hono";\nimport { cors } from "hono/cors";\nimport { serve } from "@hono/node-server";\n\nconst app = new Hono();\napp.use("*", cors());\n\napp.get("/health", (c) => c.json({ status: "ok" }));\napp.get("/api", (c) => c.json({ message: "Welcome to ${projectName} API" }));\n\nserve({ fetch: app.fetch, port: 8000 });\nconsole.log("Server running on http://localhost:8000");\n`
232
+ : tech.id === "fastify"
233
+ ? `import Fastify from "fastify";\nimport cors from "@fastify/cors";\n\nconst app = Fastify();\nawait app.register(cors);\n\napp.get("/health", async () => ({ status: "ok" }));\napp.get("/api", async () => ({ message: "Welcome to ${projectName} API" }));\n\nawait app.listen({ port: 8000, host: "0.0.0.0" });\nconsole.log("Server running on http://localhost:8000");\n`
234
+ : `import express from "express";\nimport cors from "cors";\n\nconst app = express();\napp.use(cors());\napp.use(express.json());\n\napp.get("/health", (_, res) => res.json({ status: "ok" }));\napp.get("/api", (_, res) => res.json({ message: "Welcome to ${projectName} API" }));\n\napp.listen(8000, () => console.log("Server running on http://localhost:8000"));\n`;
235
+ fs.mkdirSync(path.join(backendDir, "src"), { recursive: true });
236
+ writeFile(path.join(backendDir, "src/index.ts"), serverCode);
237
+ const deps = tech.id === "hono"
238
+ ? "hono @hono/node-server"
239
+ : tech.id === "fastify"
240
+ ? "fastify @fastify/cors"
241
+ : "express cors";
242
+ const devDeps = `typescript tsx @types/node${tech.id === "express" ? " @types/express @types/cors" : ""}`;
243
+ run(`npm install ${deps}`, backendDir, 60_000);
244
+ run(`npm install -D ${devDeps}`, backendDir, 60_000);
245
+ log.push(`${tech.name} project created`);
246
+ }
247
+ writeFile(path.join(backendDir, ".env.example"), `${[
248
+ `# ${projectName} — Backend Environment Variables`,
249
+ ``,
250
+ `NODE_ENV=development`,
251
+ `PORT=8000`,
252
+ `DATABASE_URL=${dbUrl}`,
253
+ hasRedis ? `REDIS_URL=redis://localhost:6379/0` : "",
254
+ `CORS_ORIGINS=http://localhost:3000,http://localhost:5173`,
255
+ ]
256
+ .filter(Boolean)
257
+ .join("\n")}\n`);
258
+ log.push("backend/.env.example created");
259
+ }
260
+ // Go backends
261
+ if (tech.id === "gin" || tech.id === "echo") {
262
+ run(`go mod init ${projectName}`, backendDir, 15_000);
263
+ const framework = tech.id === "gin" ? "github.com/gin-gonic/gin" : "github.com/labstack/echo/v4";
264
+ run(`go get ${framework}`, backendDir, 60_000);
265
+ const mainCode = tech.id === "gin"
266
+ ? `package main\n\nimport (\n\t"github.com/gin-gonic/gin"\n)\n\nfunc main() {\n\tr := gin.Default()\n\tr.GET("/health", func(c *gin.Context) { c.JSON(200, gin.H{"status": "ok"}) })\n\tr.GET("/api", func(c *gin.Context) { c.JSON(200, gin.H{"message": "Welcome to ${projectName} API"}) })\n\tr.Run(":8000")\n}\n`
267
+ : `package main\n\nimport (\n\t"net/http"\n\t"github.com/labstack/echo/v4"\n)\n\nfunc main() {\n\te := echo.New()\n\te.GET("/health", func(c echo.Context) error { return c.JSON(http.StatusOK, map[string]string{"status": "ok"}) })\n\te.GET("/api", func(c echo.Context) error { return c.JSON(http.StatusOK, map[string]string{"message": "Welcome to ${projectName} API"}) })\n\te.Start(":8000")\n}\n`;
268
+ writeFile(path.join(backendDir, "main.go"), mainCode);
269
+ writeFile(path.join(backendDir, ".env.example"), `${[
270
+ `# ${projectName} — Backend Environment Variables`,
271
+ `PORT=8000`,
272
+ `DATABASE_URL=${dbUrl}`,
273
+ hasRedis ? `REDIS_URL=redis://localhost:6379/0` : "",
274
+ ]
275
+ .filter(Boolean)
276
+ .join("\n")}\n`);
277
+ log.push(`${tech.name} project created`);
278
+ log.push("backend/.env.example created");
279
+ }
280
+ return log;
281
+ }
282
+ // ─── Command ────────────────────────────────────────
283
+ export const generateCommand = new Command("generate")
284
+ .description("Create a fully scaffolded project: directory structure, configs, dependencies, ready to code")
285
+ .requiredOption("--name <name>", "Project name")
286
+ .requiredOption("--path <dir>", "Parent directory")
287
+ .requiredOption("--techs <ids>", "Comma-separated technology IDs")
288
+ .option("--profile <profile>", "Stack profile", "standard")
289
+ .option("--git", "Initialize git repository", true)
290
+ .option("--json", "Output result as JSON")
291
+ .action((opts) => {
292
+ const engine = getStackEngine();
293
+ const orchestrator = getScaffoldOrchestrator();
294
+ const rules = getRulesEngine();
295
+ // ── Input validation ──
296
+ const techIds = opts.techs
297
+ .split(",")
298
+ .map((s) => s.trim())
299
+ .filter(Boolean);
300
+ if (techIds.length === 0) {
301
+ console.error(error("No technologies specified."));
302
+ console.error(chalk.dim(" Use --techs with comma-separated IDs: --techs nextjs,postgresql,prisma"));
303
+ process.exit(1);
304
+ }
305
+ if (!opts.name || opts.name.trim().length === 0) {
306
+ console.error(error("Project name is required."));
307
+ process.exit(1);
308
+ }
309
+ // SEC-004: Sanitize project name to prevent path traversal
310
+ const safeName = path.basename(opts.name);
311
+ if (safeName !== opts.name || !/^[a-zA-Z0-9_.-]+$/.test(safeName)) {
312
+ console.error(error("Project name must be alphanumeric (a-z, 0-9, -, _, .). No path separators."));
313
+ process.exit(1);
314
+ }
315
+ const parentDir = path.resolve(opts.path);
316
+ if (!fs.existsSync(parentDir)) {
317
+ console.error(error(`Parent directory does not exist: ${parentDir}`));
318
+ process.exit(1);
319
+ }
320
+ const targetDir = path.resolve(parentDir, safeName);
321
+ if (!targetDir.startsWith(parentDir)) {
322
+ console.error(error("Project path resolves outside the parent directory."));
323
+ process.exit(1);
324
+ }
325
+ if (fs.existsSync(targetDir) && fs.readdirSync(targetDir).length > 0) {
326
+ console.error(error(`Target directory already exists and is not empty: ${targetDir}`));
327
+ process.exit(1);
328
+ }
329
+ // Validate all tech IDs exist
330
+ const unknownTechs = techIds.filter((id) => !rules.getTechnology(id));
331
+ if (unknownTechs.length > 0) {
332
+ console.error(error(`Unknown technologies: ${unknownTechs.join(", ")}`));
333
+ console.error(chalk.dim(" Run `stackweld browse` to see available technologies."));
334
+ process.exit(1);
335
+ }
336
+ const projectName = opts.name;
337
+ if (!opts.json) {
338
+ console.log(`\n ${gradientHeader("Stackweld")} ${chalk.dim("/ Generate Project")}\n`);
339
+ }
340
+ // ── Step 1: Build technology list ──
341
+ const spinner1 = !opts.json
342
+ ? ora(stepIndicator(1, 5, "Building technology list")).start()
343
+ : null;
344
+ const technologies = techIds.map((id) => {
345
+ const tech = rules.getTechnology(id);
346
+ return { technologyId: id, version: tech?.defaultVersion || "latest" };
347
+ });
348
+ spinner1?.succeed(stepIndicator(1, 5, `${technologies.length} technologies resolved`));
349
+ // ── Step 2: Validate stack ──
350
+ const spinner2 = !opts.json
351
+ ? ora(stepIndicator(2, 5, "Validating stack compatibility")).start()
352
+ : null;
353
+ const { stack, validation } = engine.create({
354
+ name: projectName,
355
+ description: "Generated by Stackweld",
356
+ profile: opts.profile || "standard",
357
+ technologies,
358
+ tags: ["generated"],
359
+ });
360
+ if (!validation.valid) {
361
+ spinner2?.fail(stepIndicator(2, 5, "Validation failed"));
362
+ if (opts.json) {
363
+ console.log(JSON.stringify({ success: false, errors: validation.issues }, null, 2));
364
+ }
365
+ else {
366
+ console.log("");
367
+ console.log(formatValidation(validation));
368
+ }
369
+ process.exit(1);
370
+ }
371
+ spinner2?.succeed(stepIndicator(2, 5, "Stack is valid"));
372
+ if (!opts.json && validation.issues.length > 0) {
373
+ for (const issue of validation.issues) {
374
+ if (issue.severity === "warning") {
375
+ console.log(` ${warning(issue.message)}`);
376
+ }
377
+ }
378
+ }
379
+ // ── Step 3: Detect project type & scaffold ──
380
+ const allTechObjects = stack.technologies
381
+ .map((st) => rules.getTechnology(st.technologyId))
382
+ .filter((t) => t != null);
383
+ const frontendTech = allTechObjects.find((t) => t.category === "frontend");
384
+ const backendTech = allTechObjects.find((t) => t.category === "backend");
385
+ const isFullStack = !!frontendTech && !!backendTech;
386
+ const isFrontendOnly = !!frontendTech && !backendTech;
387
+ const isBackendOnly = !frontendTech && !!backendTech;
388
+ const projectType = isFullStack
389
+ ? "full-stack"
390
+ : isFrontendOnly
391
+ ? "frontend"
392
+ : isBackendOnly
393
+ ? "backend"
394
+ : "library";
395
+ const spinner3 = !opts.json
396
+ ? ora(stepIndicator(3, 5, `Generating ${projectType} project structure`)).start()
397
+ : null;
398
+ fs.mkdirSync(targetDir, { recursive: true });
399
+ const output = orchestrator.generate(stack);
400
+ const written = [];
401
+ const rootFiles = [
402
+ { p: "README.md", content: output.readme },
403
+ { p: ".gitignore", content: output.gitignore },
404
+ { p: "Makefile", content: output.makefile },
405
+ { p: "scripts/dev.sh", content: output.devScript },
406
+ { p: "scripts/setup.sh", content: output.setupScript },
407
+ { p: ".devcontainer/devcontainer.json", content: output.devcontainer },
408
+ { p: ".vscode/settings.json", content: output.vscodeSettings },
409
+ { p: ".github/workflows/ci.yml", content: output.ciWorkflow },
410
+ ];
411
+ if (output.dockerCompose) {
412
+ rootFiles.unshift({ p: "docker-compose.yml", content: output.dockerCompose });
413
+ }
414
+ rootFiles.push({ p: ".env.example", content: output.envExample });
415
+ for (const f of rootFiles) {
416
+ writeFile(path.join(targetDir, f.p), f.content);
417
+ if (f.p.endsWith(".sh"))
418
+ makeExecutable(path.join(targetDir, f.p));
419
+ written.push(f.p);
420
+ }
421
+ spinner3?.succeed(stepIndicator(3, 5, `${written.length} root files generated`));
422
+ // ── Step 4: Scaffold sub-projects ──
423
+ const scaffoldLog = [];
424
+ const spinner4 = !opts.json
425
+ ? ora(stepIndicator(4, 5, "Scaffolding applications")).start()
426
+ : null;
427
+ if (isFullStack) {
428
+ if (spinner4)
429
+ spinner4.text = stepIndicator(4, 5, `Scaffolding ${frontendTech?.name} frontend...`);
430
+ const fLog = scaffoldFrontend(frontendTech, path.join(targetDir, "frontend"), targetDir, projectName);
431
+ scaffoldLog.push(...fLog.map((l) => `frontend: ${l}`));
432
+ if (spinner4)
433
+ spinner4.text = stepIndicator(4, 5, `Scaffolding ${backendTech?.name} backend...`);
434
+ const bLog = scaffoldBackend(backendTech, path.join(targetDir, "backend"), targetDir, projectName, allTechObjects);
435
+ scaffoldLog.push(...bLog.map((l) => `backend: ${l}`));
436
+ }
437
+ else if (isFrontendOnly) {
438
+ if (frontendTech?.officialScaffold) {
439
+ if (spinner4)
440
+ spinner4.text = stepIndicator(4, 5, `Scaffolding ${frontendTech?.name}...`);
441
+ const tempName = ".scaffold-temp";
442
+ const r = run(`${frontendTech?.officialScaffold} ${tempName}`, targetDir);
443
+ if (r.success) {
444
+ const tempDir = path.join(targetDir, tempName);
445
+ if (fs.existsSync(tempDir)) {
446
+ for (const item of fs.readdirSync(tempDir)) {
447
+ const src = path.join(tempDir, item);
448
+ const dest = path.join(targetDir, item);
449
+ if (!fs.existsSync(dest)) {
450
+ fs.renameSync(src, dest);
451
+ }
452
+ }
453
+ fs.rmSync(tempDir, { recursive: true, force: true });
454
+ }
455
+ scaffoldLog.push(`${frontendTech?.name} scaffolded in project root`);
456
+ }
457
+ else {
458
+ scaffoldLog.push(`${frontendTech?.name} (run manually: ${frontendTech?.officialScaffold})`);
459
+ }
460
+ }
461
+ }
462
+ else if (isBackendOnly) {
463
+ if (spinner4)
464
+ spinner4.text = stepIndicator(4, 5, `Scaffolding ${backendTech?.name}...`);
465
+ const bLog = scaffoldBackend(backendTech, targetDir, targetDir, projectName, allTechObjects);
466
+ scaffoldLog.push(...bLog);
467
+ }
468
+ // Install additional technologies
469
+ if (spinner4)
470
+ spinner4.text = stepIndicator(4, 5, "Installing additional technologies (ORM, Auth, Styling, DevOps)...");
471
+ const installCtx = {
472
+ projectDir: targetDir,
473
+ frontendDir: isFullStack ? path.join(targetDir, "frontend") : null,
474
+ backendDir: isFullStack ? path.join(targetDir, "backend") : null,
475
+ projectName,
476
+ isFullStack,
477
+ allTechs: allTechObjects,
478
+ runtime: allTechObjects.find((t) => t.category === "runtime")?.id ?? null,
479
+ };
480
+ const installResults = installTechnologies(installCtx);
481
+ for (const r of installResults) {
482
+ if (r.success) {
483
+ scaffoldLog.push(`${r.techId}: ${r.message}`);
484
+ }
485
+ else {
486
+ scaffoldLog.push(`${r.techId}: FAILED \u2014 ${r.message}`);
487
+ }
488
+ }
489
+ spinner4?.succeed(stepIndicator(4, 5, `${scaffoldLog.length} scaffold operations completed`));
490
+ // ── Step 5: Git init ──
491
+ if (opts.git) {
492
+ const spinner5 = !opts.json
493
+ ? ora(stepIndicator(5, 5, "Initializing git repository")).start()
494
+ : null;
495
+ orchestrator.initGit(targetDir, stack);
496
+ scaffoldLog.push("Git initialized with initial commit");
497
+ spinner5?.succeed(stepIndicator(5, 5, "Git repository initialized"));
498
+ }
499
+ // ── Output ──
500
+ if (opts.json) {
501
+ console.log(JSON.stringify({
502
+ success: true,
503
+ stackId: stack.id,
504
+ stackName: stack.name,
505
+ projectType,
506
+ path: targetDir,
507
+ filesGenerated: written,
508
+ scaffoldLog,
509
+ validation: {
510
+ issues: validation.issues,
511
+ resolvedDependencies: validation.resolvedDependencies,
512
+ portAssignments: validation.portAssignments,
513
+ },
514
+ }, null, 2));
515
+ }
516
+ else {
517
+ const summaryContent = [
518
+ `${chalk.dim("Project:")} ${chalk.cyan.bold(projectName)}`,
519
+ `${chalk.dim("Type:")} ${projectType}`,
520
+ `${chalk.dim("Path:")} ${targetDir}`,
521
+ `${chalk.dim("Profile:")} ${opts.profile || "standard"}`,
522
+ `${chalk.dim("Files:")} ${written.length} root + ${scaffoldLog.length} scaffold ops`,
523
+ `${chalk.dim("Stack ID:")} ${chalk.dim(stack.id)}`,
524
+ ].join("\n");
525
+ console.log("");
526
+ console.log(box(summaryContent, "\u2714 Project Created"));
527
+ if (scaffoldLog.length > 0) {
528
+ console.log(chalk.bold("\n Scaffold log:"));
529
+ for (const l of scaffoldLog) {
530
+ const isFailure = l.includes("FAILED");
531
+ const icon = isFailure ? chalk.red("\u2716") : chalk.green("\u2714");
532
+ console.log(` ${icon} ${isFailure ? chalk.red(l) : chalk.dim(l)}`);
533
+ }
534
+ }
535
+ const steps = [`cd ${targetDir}`];
536
+ if (output.dockerCompose)
537
+ steps.push("docker compose up -d");
538
+ steps.push("make dev");
539
+ console.log(nextSteps(steps));
540
+ }
541
+ });
542
+ //# sourceMappingURL=generate.js.map
@@ -0,0 +1,6 @@
1
+ /**
2
+ * stackweld health [path] — Check project health across multiple dimensions.
3
+ */
4
+ import { Command } from "commander";
5
+ export declare const healthCommand: Command;
6
+ //# sourceMappingURL=health.d.ts.map
@@ -0,0 +1,68 @@
1
+ /**
2
+ * stackweld health [path] — Check project health across multiple dimensions.
3
+ */
4
+ import * as path from "node:path";
5
+ import { checkProjectHealth } from "@stackweld/core";
6
+ import chalk from "chalk";
7
+ import { Command } from "commander";
8
+ import { box, formatJson, gradientHeader } from "../ui/format.js";
9
+ function statusIcon(status) {
10
+ switch (status) {
11
+ case "pass":
12
+ return chalk.green("\u2713");
13
+ case "warn":
14
+ return chalk.yellow("\u26A0");
15
+ case "fail":
16
+ return chalk.red("\u2717");
17
+ }
18
+ }
19
+ function overallLabel(overall) {
20
+ switch (overall) {
21
+ case "healthy":
22
+ return chalk.green.bold("HEALTHY");
23
+ case "warning":
24
+ return chalk.yellow.bold("WARNING");
25
+ case "critical":
26
+ return chalk.red.bold("CRITICAL");
27
+ }
28
+ }
29
+ export const healthCommand = new Command("health")
30
+ .description("Check project health and best practices")
31
+ .argument("[path]", "Project path to check", ".")
32
+ .option("--json", "Output as JSON")
33
+ .action((targetPath, opts) => {
34
+ const report = checkProjectHealth(targetPath);
35
+ if (opts.json) {
36
+ console.log(formatJson(report));
37
+ return;
38
+ }
39
+ const resolvedPath = path.resolve(targetPath);
40
+ const { summary } = report;
41
+ const totalChecks = summary.passed + summary.warnings + summary.critical;
42
+ // Build content lines
43
+ const lines = [];
44
+ lines.push("");
45
+ lines.push(` Overall: ${overallLabel(report.overall)} (${summary.passed}/${totalChecks} passed)`);
46
+ lines.push("");
47
+ // Group: passes first, then warns, then fails
48
+ const sorted = [...report.checks].sort((a, b) => {
49
+ const order = { pass: 0, warn: 1, fail: 2 };
50
+ return order[a.status] - order[b.status];
51
+ });
52
+ for (const check of sorted) {
53
+ const icon = statusIcon(check.status);
54
+ lines.push(` ${icon} ${check.message}`);
55
+ if (check.suggestion) {
56
+ lines.push(` ${chalk.dim(check.suggestion)}`);
57
+ }
58
+ }
59
+ lines.push("");
60
+ lines.push(` ${chalk.green(`${String(summary.passed)} passed`)} ${chalk.dim("\u00B7")} ` +
61
+ `${chalk.yellow(`${String(summary.warnings)} warnings`)} ${chalk.dim("\u00B7")} ` +
62
+ `${chalk.red(`${String(summary.critical)} critical`)}`);
63
+ lines.push("");
64
+ console.log(`\n ${gradientHeader("Stackweld")} ${chalk.dim("/ Health Report")}\n`);
65
+ console.log(box(lines.join("\n"), `Health Report: ${resolvedPath}`));
66
+ console.log("");
67
+ });
68
+ //# sourceMappingURL=health.js.map
@@ -0,0 +1,6 @@
1
+ /**
2
+ * stackweld import <file> — Import a stack definition from YAML or JSON.
3
+ */
4
+ import { Command } from "commander";
5
+ export declare const importCommand: Command;
6
+ //# sourceMappingURL=import-stack.d.ts.map