@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.
- package/dist/__tests__/commands.test.d.ts +2 -0
- package/dist/__tests__/commands.test.js +275 -0
- package/dist/commands/ai.d.ts +8 -0
- package/dist/commands/ai.js +167 -0
- package/dist/commands/analyze.d.ts +6 -0
- package/dist/commands/analyze.js +90 -0
- package/dist/commands/benchmark.d.ts +6 -0
- package/dist/commands/benchmark.js +86 -0
- package/dist/commands/browse.d.ts +6 -0
- package/dist/commands/browse.js +101 -0
- package/dist/commands/clone.d.ts +3 -0
- package/dist/commands/clone.js +37 -0
- package/dist/commands/compare.d.ts +6 -0
- package/dist/commands/compare.js +93 -0
- package/dist/commands/completion.d.ts +6 -0
- package/dist/commands/completion.js +86 -0
- package/dist/commands/config.d.ts +6 -0
- package/dist/commands/config.js +56 -0
- package/dist/commands/cost.d.ts +6 -0
- package/dist/commands/cost.js +101 -0
- package/dist/commands/create.d.ts +7 -0
- package/dist/commands/create.js +111 -0
- package/dist/commands/delete.d.ts +6 -0
- package/dist/commands/delete.js +33 -0
- package/dist/commands/deploy.d.ts +6 -0
- package/dist/commands/deploy.js +90 -0
- package/dist/commands/doctor.d.ts +6 -0
- package/dist/commands/doctor.js +144 -0
- package/dist/commands/down.d.ts +6 -0
- package/dist/commands/down.js +37 -0
- package/dist/commands/env.d.ts +6 -0
- package/dist/commands/env.js +129 -0
- package/dist/commands/export-stack.d.ts +6 -0
- package/dist/commands/export-stack.js +51 -0
- package/dist/commands/generate.d.ts +9 -0
- package/dist/commands/generate.js +542 -0
- package/dist/commands/health.d.ts +6 -0
- package/dist/commands/health.js +68 -0
- package/dist/commands/import-stack.d.ts +6 -0
- package/dist/commands/import-stack.js +68 -0
- package/dist/commands/info.d.ts +6 -0
- package/dist/commands/info.js +56 -0
- package/dist/commands/init.d.ts +6 -0
- package/dist/commands/init.js +186 -0
- package/dist/commands/learn.d.ts +6 -0
- package/dist/commands/learn.js +91 -0
- package/dist/commands/lint.d.ts +6 -0
- package/dist/commands/lint.js +193 -0
- package/dist/commands/list.d.ts +6 -0
- package/dist/commands/list.js +27 -0
- package/dist/commands/logs.d.ts +6 -0
- package/dist/commands/logs.js +37 -0
- package/dist/commands/migrate.d.ts +6 -0
- package/dist/commands/migrate.js +57 -0
- package/dist/commands/plugin.d.ts +8 -0
- package/dist/commands/plugin.js +131 -0
- package/dist/commands/preview.d.ts +7 -0
- package/dist/commands/preview.js +100 -0
- package/dist/commands/save.d.ts +6 -0
- package/dist/commands/save.js +32 -0
- package/dist/commands/scaffold.d.ts +7 -0
- package/dist/commands/scaffold.js +100 -0
- package/dist/commands/score.d.ts +9 -0
- package/dist/commands/score.js +111 -0
- package/dist/commands/share.d.ts +10 -0
- package/dist/commands/share.js +93 -0
- package/dist/commands/status.d.ts +6 -0
- package/dist/commands/status.js +39 -0
- package/dist/commands/template.d.ts +3 -0
- package/dist/commands/template.js +162 -0
- package/dist/commands/up.d.ts +6 -0
- package/dist/commands/up.js +54 -0
- package/dist/commands/version-cmd.d.ts +6 -0
- package/dist/commands/version-cmd.js +100 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +160 -0
- package/dist/ui/context.d.ts +10 -0
- package/dist/ui/context.js +90 -0
- package/dist/ui/format.d.ts +59 -0
- package/dist/ui/format.js +295 -0
- 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,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
|