@stackweld/cli 0.2.2 → 0.2.4

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.
@@ -1,29 +1,375 @@
1
1
  /**
2
- * stackweld init — Initialize Stackweld in the current directory or interactively create a new stack.
2
+ * stackweld init — Interactive project creation wizard.
3
+ * Creates the stack definition AND generates the full project.
3
4
  */
5
+ import { execSync } from "node:child_process";
6
+ import * as fs from "node:fs";
7
+ import * as path from "node:path";
4
8
  import { checkbox, confirm, input, select } from "@inquirer/prompts";
9
+ import { installTechnologies } from "@stackweld/core";
5
10
  import { getAllTemplates } from "@stackweld/templates";
6
11
  import chalk from "chalk";
7
12
  import { Command } from "commander";
8
- import { getRulesEngine, getStackEngine } from "../ui/context.js";
9
- import { formatStackSummary, formatValidation, gradientHeader, nextSteps, stepIndicator, warning, } from "../ui/format.js";
13
+ import ora from "ora";
14
+ import { getRulesEngine, getScaffoldOrchestrator, getStackEngine } from "../ui/context.js";
15
+ import { box, formatStackSummary, formatValidation, gradientHeader, nextSteps, stepIndicator, warning, } from "../ui/format.js";
16
+ // ─── Helpers ───────────────────────────────────────
17
+ function runCmd(cmd, cwd, timeoutMs = 120_000) {
18
+ try {
19
+ const out = execSync(cmd, { cwd, stdio: "pipe", timeout: timeoutMs }).toString();
20
+ return { success: true, output: out };
21
+ }
22
+ catch (e) {
23
+ return { success: false, output: e instanceof Error ? e.message.slice(0, 200) : String(e) };
24
+ }
25
+ }
26
+ function writeFile(filePath, content) {
27
+ const dir = path.dirname(filePath);
28
+ if (!fs.existsSync(dir))
29
+ fs.mkdirSync(dir, { recursive: true });
30
+ fs.writeFileSync(filePath, content, "utf-8");
31
+ }
32
+ function makeExecutable(filePath) {
33
+ fs.chmodSync(filePath, 0o755);
34
+ }
35
+ // ─── Frontend scaffolders ──────────────────────────
36
+ function scaffoldFrontend(tech, frontendDir, parentDir, projectName) {
37
+ const log = [];
38
+ fs.mkdirSync(frontendDir, { recursive: true });
39
+ if (tech.id === "nextjs") {
40
+ const r = runCmd(`npx create-next-app@latest frontend --typescript --tailwind --app --src-dir --import-alias "@/*" --no-git --yes`, parentDir);
41
+ log.push(r.success ? "Next.js scaffolded" : "Next.js (manual: npx create-next-app@latest)");
42
+ }
43
+ else if (tech.id === "nuxt") {
44
+ const r = runCmd(`npx nuxi@latest init frontend --no-git --force`, parentDir);
45
+ log.push(r.success ? "Nuxt scaffolded" : "Nuxt (manual: npx nuxi init)");
46
+ }
47
+ else if (tech.id === "sveltekit") {
48
+ const r = runCmd(`npx sv create frontend --template minimal --types ts`, parentDir);
49
+ log.push(r.success ? "SvelteKit scaffolded" : "SvelteKit (manual: npx sv create)");
50
+ }
51
+ else if (tech.id === "astro") {
52
+ const r = runCmd(`npm create astro@latest frontend -- --template minimal --install --no-git --typescript strict --yes`, parentDir);
53
+ log.push(r.success ? "Astro scaffolded" : "Astro (manual: npm create astro@latest)");
54
+ }
55
+ else if (tech.id === "remix") {
56
+ const r = runCmd(`npx create-remix@latest frontend --no-git --yes`, parentDir);
57
+ log.push(r.success ? "Remix scaffolded" : "Remix (manual: npx create-remix@latest)");
58
+ }
59
+ else if (tech.id === "angular") {
60
+ const r = runCmd(`npx @angular/cli new frontend --skip-git --defaults`, parentDir);
61
+ log.push(r.success ? "Angular scaffolded" : "Angular (manual: npx @angular/cli new)");
62
+ }
63
+ else if (tech.id === "react" || tech.id === "solidjs" || tech.id === "qwik") {
64
+ const template = tech.id === "solidjs" ? "solid-ts" : tech.id === "qwik" ? "qwik-ts" : "react-ts";
65
+ const r = runCmd(`npm create vite@latest frontend -- --template ${template}`, parentDir);
66
+ log.push(r.success ? `${tech.name} (Vite) scaffolded` : `${tech.name} (manual: npm create vite@latest)`);
67
+ }
68
+ else {
69
+ log.push(`${tech.name} (create manually in frontend/)`);
70
+ }
71
+ writeFile(path.join(frontendDir, ".env.example"), [
72
+ `# ${projectName} — Frontend Environment Variables`,
73
+ "",
74
+ "NEXT_PUBLIC_API_URL=http://localhost:8000",
75
+ "VITE_API_URL=http://localhost:8000",
76
+ "PUBLIC_API_URL=http://localhost:8000",
77
+ "",
78
+ ].join("\n"));
79
+ log.push("frontend/.env.example created");
80
+ return log;
81
+ }
82
+ // ─── Backend scaffolders ───────────────────────────
83
+ function scaffoldBackend(tech, backendDir, _parentDir, projectName, allTechs) {
84
+ const log = [];
85
+ fs.mkdirSync(backendDir, { recursive: true });
86
+ const hasPostgres = allTechs.some((t) => t.id === "postgresql");
87
+ const hasRedis = allTechs.some((t) => t.id === "redis");
88
+ const hasSqlite = allTechs.some((t) => t.id === "sqlite");
89
+ const dbUrl = hasPostgres
90
+ ? `postgresql://postgres:postgres@localhost:5432/${projectName}`
91
+ : hasSqlite
92
+ ? "file:./dev.db"
93
+ : `mysql://root:root@localhost:3306/${projectName}`;
94
+ if (tech.id === "django" || tech.id === "fastapi" || tech.id === "flask") {
95
+ runCmd("python3 -m venv .venv", backendDir, 30_000);
96
+ const pip = path.join(backendDir, ".venv/bin/pip");
97
+ if (tech.id === "django") {
98
+ runCmd(`${pip} install django djangorestframework django-cors-headers python-dotenv psycopg2-binary`, backendDir, 90_000);
99
+ runCmd(`${path.join(backendDir, ".venv/bin/django-admin")} startproject config .`, backendDir, 15_000);
100
+ writeFile(path.join(backendDir, "requirements.txt"), [
101
+ "django>=5.1", "djangorestframework>=3.15", "django-cors-headers>=4.4",
102
+ "python-dotenv>=1.0", "psycopg2-binary>=2.9", "gunicorn>=22.0",
103
+ ...(hasRedis ? ["django-redis>=5.4"] : []),
104
+ ].join("\n") + "\n");
105
+ log.push("Django project created");
106
+ }
107
+ else if (tech.id === "fastapi") {
108
+ runCmd(`${pip} install fastapi uvicorn sqlalchemy alembic python-dotenv psycopg2-binary`, backendDir, 90_000);
109
+ writeFile(path.join(backendDir, "main.py"), [
110
+ 'from fastapi import FastAPI',
111
+ 'from fastapi.middleware.cors import CORSMiddleware',
112
+ '',
113
+ `app = FastAPI(title="${projectName}")`,
114
+ '',
115
+ 'app.add_middleware(',
116
+ ' CORSMiddleware,',
117
+ ' allow_origins=["http://localhost:3000", "http://localhost:5173"],',
118
+ ' allow_credentials=True,',
119
+ ' allow_methods=["*"],',
120
+ ' allow_headers=["*"],',
121
+ ')',
122
+ '',
123
+ '@app.get("/health")',
124
+ 'def health():',
125
+ ' return {"status": "ok"}',
126
+ '',
127
+ '@app.get("/api")',
128
+ 'def root():',
129
+ ` return {"message": "Welcome to ${projectName} API"}`,
130
+ ].join("\n") + "\n");
131
+ writeFile(path.join(backendDir, "requirements.txt"), [
132
+ "fastapi>=0.115", "uvicorn[standard]>=0.30", "sqlalchemy>=2.0", "alembic>=1.13",
133
+ "python-dotenv>=1.0", "psycopg2-binary>=2.9",
134
+ ...(hasRedis ? ["redis>=5.0"] : []),
135
+ ].join("\n") + "\n");
136
+ log.push("FastAPI project created");
137
+ }
138
+ else if (tech.id === "flask") {
139
+ runCmd(`${pip} install flask flask-cors python-dotenv`, backendDir, 60_000);
140
+ writeFile(path.join(backendDir, "app.py"), [
141
+ 'from flask import Flask, jsonify',
142
+ 'from flask_cors import CORS',
143
+ '',
144
+ 'app = Flask(__name__)',
145
+ 'CORS(app)',
146
+ '',
147
+ '@app.route("/health")',
148
+ 'def health():',
149
+ ' return jsonify(status="ok")',
150
+ '',
151
+ '@app.route("/api")',
152
+ 'def root():',
153
+ ` return jsonify(message="Welcome to ${projectName} API")`,
154
+ ].join("\n") + "\n");
155
+ writeFile(path.join(backendDir, "requirements.txt"), [
156
+ "flask>=3.0", "flask-cors>=4.0", "python-dotenv>=1.0",
157
+ ].join("\n") + "\n");
158
+ log.push("Flask project created");
159
+ }
160
+ writeFile(path.join(backendDir, ".env.example"), [
161
+ `# ${projectName} — Backend Environment Variables`, "",
162
+ "DEBUG=True", "SECRET_KEY=change-me-in-production", `DATABASE_URL=${dbUrl}`,
163
+ ...(hasRedis ? ["REDIS_URL=redis://localhost:6379/0"] : []),
164
+ "ALLOWED_HOSTS=localhost,127.0.0.1",
165
+ "CORS_ALLOWED_ORIGINS=http://localhost:3000,http://localhost:5173",
166
+ "PORT=8000",
167
+ ].join("\n") + "\n");
168
+ log.push("backend/.env.example created");
169
+ }
170
+ if (tech.id === "express" || tech.id === "fastify" || tech.id === "hono" || tech.id === "nestjs") {
171
+ if (tech.id === "nestjs") {
172
+ const r = runCmd(`npx @nestjs/cli new backend --package-manager npm --skip-git`, backendDir.replace(/\/backend$/, ""), 120_000);
173
+ log.push(r.success ? "NestJS project created" : "NestJS (manual: npx @nestjs/cli new)");
174
+ }
175
+ else {
176
+ writeFile(path.join(backendDir, "package.json"), JSON.stringify({
177
+ name: `${projectName}-backend`, version: "0.1.0", private: true, type: "module",
178
+ scripts: { dev: "npx tsx watch src/index.ts", build: "tsc", start: "node dist/index.js" },
179
+ }, null, 2) + "\n");
180
+ writeFile(path.join(backendDir, "tsconfig.json"), JSON.stringify({
181
+ compilerOptions: {
182
+ target: "ES2022", module: "ESNext", moduleResolution: "bundler",
183
+ outDir: "dist", rootDir: "src", strict: true, esModuleInterop: true, skipLibCheck: true,
184
+ },
185
+ include: ["src"],
186
+ }, null, 2) + "\n");
187
+ const serverCode = tech.id === "hono"
188
+ ? `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`
189
+ : tech.id === "fastify"
190
+ ? `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`
191
+ : `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`;
192
+ fs.mkdirSync(path.join(backendDir, "src"), { recursive: true });
193
+ writeFile(path.join(backendDir, "src/index.ts"), serverCode);
194
+ const deps = tech.id === "hono" ? "hono @hono/node-server" : tech.id === "fastify" ? "fastify @fastify/cors" : "express cors";
195
+ const devDeps = `typescript tsx @types/node${tech.id === "express" ? " @types/express @types/cors" : ""}`;
196
+ runCmd(`npm install ${deps}`, backendDir, 60_000);
197
+ runCmd(`npm install -D ${devDeps}`, backendDir, 60_000);
198
+ log.push(`${tech.name} project created`);
199
+ }
200
+ writeFile(path.join(backendDir, ".env.example"), [
201
+ `# ${projectName} — Backend Environment Variables`, "",
202
+ "NODE_ENV=development", "PORT=8000", `DATABASE_URL=${dbUrl}`,
203
+ ...(hasRedis ? ["REDIS_URL=redis://localhost:6379/0"] : []),
204
+ "CORS_ORIGINS=http://localhost:3000,http://localhost:5173",
205
+ ].join("\n") + "\n");
206
+ log.push("backend/.env.example created");
207
+ }
208
+ if (tech.id === "gin" || tech.id === "echo") {
209
+ runCmd(`go mod init ${projectName}`, backendDir, 15_000);
210
+ const framework = tech.id === "gin" ? "github.com/gin-gonic/gin" : "github.com/labstack/echo/v4";
211
+ runCmd(`go get ${framework}`, backendDir, 60_000);
212
+ const mainCode = tech.id === "gin"
213
+ ? `package main\n\nimport "github.com/gin-gonic/gin"\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`
214
+ : `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`;
215
+ writeFile(path.join(backendDir, "main.go"), mainCode);
216
+ writeFile(path.join(backendDir, ".env.example"), [
217
+ `# ${projectName} — Backend Environment Variables`,
218
+ "PORT=8000", `DATABASE_URL=${dbUrl}`,
219
+ ...(hasRedis ? ["REDIS_URL=redis://localhost:6379/0"] : []),
220
+ ].join("\n") + "\n");
221
+ log.push(`${tech.name} project created`);
222
+ log.push("backend/.env.example created");
223
+ }
224
+ return log;
225
+ }
226
+ // ─── Generate project from stack ───────────────────
227
+ function generateProject(stack, targetDir, projectName, rules, orchestrator, json) {
228
+ const allTechObjects = stack.technologies
229
+ .map((st) => rules.getTechnology(st.technologyId))
230
+ .filter((t) => t != null);
231
+ const frontendTech = allTechObjects.find((t) => t.category === "frontend");
232
+ const backendTech = allTechObjects.find((t) => t.category === "backend");
233
+ const isFullStack = !!frontendTech && !!backendTech;
234
+ const isFrontendOnly = !!frontendTech && !backendTech;
235
+ const isBackendOnly = !frontendTech && !!backendTech;
236
+ const projectType = isFullStack ? "full-stack" : isFrontendOnly ? "frontend" : isBackendOnly ? "backend" : "library";
237
+ const spinner1 = !json ? ora(` Generating ${projectType} project structure...`).start() : null;
238
+ fs.mkdirSync(targetDir, { recursive: true });
239
+ const output = orchestrator.generate(stack);
240
+ const written = [];
241
+ const rootFiles = [
242
+ { p: "README.md", content: output.readme },
243
+ { p: ".gitignore", content: output.gitignore },
244
+ { p: "Makefile", content: output.makefile },
245
+ { p: "scripts/dev.sh", content: output.devScript },
246
+ { p: "scripts/setup.sh", content: output.setupScript },
247
+ { p: ".devcontainer/devcontainer.json", content: output.devcontainer },
248
+ { p: ".vscode/settings.json", content: output.vscodeSettings },
249
+ { p: ".github/workflows/ci.yml", content: output.ciWorkflow },
250
+ ];
251
+ if (output.dockerCompose) {
252
+ rootFiles.unshift({ p: "docker-compose.yml", content: output.dockerCompose });
253
+ }
254
+ rootFiles.push({ p: ".env.example", content: output.envExample });
255
+ for (const f of rootFiles) {
256
+ writeFile(path.join(targetDir, f.p), f.content);
257
+ if (f.p.endsWith(".sh"))
258
+ makeExecutable(path.join(targetDir, f.p));
259
+ written.push(f.p);
260
+ }
261
+ spinner1?.succeed(` ${written.length} root files generated`);
262
+ // Scaffold sub-projects
263
+ const scaffoldLog = [];
264
+ const spinner2 = !json ? ora(" Scaffolding applications...").start() : null;
265
+ if (isFullStack) {
266
+ if (spinner2)
267
+ spinner2.text = ` Scaffolding ${frontendTech.name} frontend...`;
268
+ scaffoldLog.push(...scaffoldFrontend(frontendTech, path.join(targetDir, "frontend"), targetDir, projectName).map((l) => `frontend: ${l}`));
269
+ if (spinner2)
270
+ spinner2.text = ` Scaffolding ${backendTech.name} backend...`;
271
+ scaffoldLog.push(...scaffoldBackend(backendTech, path.join(targetDir, "backend"), targetDir, projectName, allTechObjects).map((l) => `backend: ${l}`));
272
+ }
273
+ else if (isFrontendOnly && frontendTech?.officialScaffold) {
274
+ if (spinner2)
275
+ spinner2.text = ` Scaffolding ${frontendTech.name}...`;
276
+ const tempName = ".scaffold-temp";
277
+ const r = runCmd(`${frontendTech.officialScaffold} ${tempName}`, targetDir);
278
+ if (r.success) {
279
+ const tempDir = path.join(targetDir, tempName);
280
+ if (fs.existsSync(tempDir)) {
281
+ for (const item of fs.readdirSync(tempDir)) {
282
+ const src = path.join(tempDir, item);
283
+ const dest = path.join(targetDir, item);
284
+ if (!fs.existsSync(dest))
285
+ fs.renameSync(src, dest);
286
+ }
287
+ fs.rmSync(tempDir, { recursive: true, force: true });
288
+ }
289
+ scaffoldLog.push(`${frontendTech.name} scaffolded in project root`);
290
+ }
291
+ else {
292
+ scaffoldLog.push(`${frontendTech.name} (run manually: ${frontendTech.officialScaffold})`);
293
+ }
294
+ }
295
+ else if (isBackendOnly && backendTech) {
296
+ if (spinner2)
297
+ spinner2.text = ` Scaffolding ${backendTech.name}...`;
298
+ scaffoldLog.push(...scaffoldBackend(backendTech, targetDir, targetDir, projectName, allTechObjects));
299
+ }
300
+ // Install additional technologies (ORM, Auth, Styling, DevOps)
301
+ if (spinner2)
302
+ spinner2.text = " Installing additional technologies...";
303
+ const installCtx = {
304
+ projectDir: targetDir,
305
+ frontendDir: isFullStack ? path.join(targetDir, "frontend") : null,
306
+ backendDir: isFullStack ? path.join(targetDir, "backend") : null,
307
+ projectName,
308
+ isFullStack,
309
+ allTechs: allTechObjects,
310
+ runtime: allTechObjects.find((t) => t.category === "runtime")?.id ?? null,
311
+ };
312
+ const installResults = installTechnologies(installCtx);
313
+ for (const r of installResults) {
314
+ scaffoldLog.push(r.success ? `${r.techId}: ${r.message}` : `${r.techId}: FAILED — ${r.message}`);
315
+ }
316
+ spinner2?.succeed(` ${scaffoldLog.length} scaffold operations completed`);
317
+ // Git init
318
+ const spinner3 = !json ? ora(" Initializing git repository...").start() : null;
319
+ orchestrator.initGit(targetDir, stack);
320
+ spinner3?.succeed(" Git repository initialized");
321
+ // Output
322
+ if (json) {
323
+ console.log(JSON.stringify({
324
+ success: true, stackId: stack.id, stackName: stack.name,
325
+ projectType, path: targetDir, filesGenerated: written, scaffoldLog,
326
+ }, null, 2));
327
+ }
328
+ else {
329
+ const summaryContent = [
330
+ `${chalk.dim("Project:")} ${chalk.cyan.bold(projectName)}`,
331
+ `${chalk.dim("Type:")} ${projectType}`,
332
+ `${chalk.dim("Path:")} ${targetDir}`,
333
+ `${chalk.dim("Profile:")} ${stack.profile}`,
334
+ `${chalk.dim("Files:")} ${written.length} root + ${scaffoldLog.length} scaffold ops`,
335
+ `${chalk.dim("Stack ID:")} ${chalk.dim(stack.id)}`,
336
+ ].join("\n");
337
+ console.log("");
338
+ console.log(box(summaryContent, "\u2714 Project Created"));
339
+ if (scaffoldLog.length > 0) {
340
+ console.log(chalk.bold("\n Scaffold log:"));
341
+ for (const l of scaffoldLog) {
342
+ const isFailure = l.includes("FAILED");
343
+ const icon = isFailure ? chalk.red("\u2716") : chalk.green("\u2714");
344
+ console.log(` ${icon} ${isFailure ? chalk.red(l) : chalk.dim(l)}`);
345
+ }
346
+ }
347
+ const steps = [`cd ${targetDir}`];
348
+ if (output.dockerCompose)
349
+ steps.push("docker compose up -d");
350
+ steps.push("make dev");
351
+ console.log(nextSteps(steps));
352
+ }
353
+ }
354
+ // ─── Command ───────────────────────────────────────
10
355
  export const initCommand = new Command("init")
11
356
  .description("Create a new stack interactively")
12
357
  .option("--template <id>", "Start from a template")
13
358
  .option("--json", "Output result as JSON")
14
359
  .action(async (opts) => {
15
- console.log(`\n ${gradientHeader("Stackweld")} ${chalk.dim("/ New Stack Wizard")}\n`);
360
+ console.log(`\n ${gradientHeader("Stackweld")} ${chalk.dim("/ New Project Wizard")}\n`);
16
361
  const rules = getRulesEngine();
17
362
  const engine = getStackEngine();
18
363
  // ── Step 1: Choose mode ──
19
- console.log(stepIndicator(1, 5, "Choose how to start"));
364
+ console.log(stepIndicator(1, 6, "Choose how to start"));
20
365
  const mode = opts.template
21
366
  ? "template"
22
367
  : await select({
23
368
  message: "How do you want to start?",
369
+ loop: false,
24
370
  choices: [
25
- { name: "From scratch pick technologies one by one", value: "scratch" },
26
- { name: "From a template use a pre-built stack", value: "template" },
371
+ { name: "From scratch \u2014 pick technologies one by one", value: "scratch" },
372
+ { name: "From a template \u2014 use a pre-built stack", value: "template" },
27
373
  ],
28
374
  });
29
375
  let technologies = [];
@@ -31,7 +377,7 @@ export const initCommand = new Command("init")
31
377
  let stackName = "";
32
378
  if (mode === "template") {
33
379
  // ── Step 2: Select template ──
34
- console.log(`\n${stepIndicator(2, 5, "Select template")}`);
380
+ console.log(`\n${stepIndicator(2, 6, "Select template")}`);
35
381
  const templates = getAllTemplates();
36
382
  if (templates.length === 0) {
37
383
  console.log(warning("No templates available. Run from scratch instead."));
@@ -40,8 +386,10 @@ export const initCommand = new Command("init")
40
386
  const templateId = opts.template ||
41
387
  (await select({
42
388
  message: "Choose a template:",
389
+ loop: false,
390
+ pageSize: 15,
43
391
  choices: templates.map((t) => ({
44
- name: `${chalk.cyan(t.name)} ${chalk.dim("")} ${t.description}`,
392
+ name: `${chalk.cyan(t.name)} ${chalk.dim("\u2014")} ${t.description}`,
45
393
  value: t.id,
46
394
  })),
47
395
  }));
@@ -52,101 +400,84 @@ export const initCommand = new Command("init")
52
400
  process.exit(1);
53
401
  }
54
402
  // ── Step 3: Name ──
55
- console.log(`\n${stepIndicator(3, 5, "Name your project")}`);
403
+ console.log(`\n${stepIndicator(3, 6, "Name your project")}`);
56
404
  stackName = await input({
57
405
  message: "Project name:",
58
406
  default: template.variables.projectName || "my-project",
59
- validate: (v) => (v.trim().length > 0 ? true : "Name cannot be empty"),
407
+ validate: (v) => {
408
+ if (v.trim().length === 0)
409
+ return "Name cannot be empty";
410
+ if (!/^[a-zA-Z0-9_.-]+$/.test(v.trim()))
411
+ return "Only letters, numbers, hyphens, dots, and underscores";
412
+ return true;
413
+ },
60
414
  });
61
415
  technologies = template.technologyIds.map((tid) => {
62
416
  const tech = rules.getTechnology(tid);
63
- return {
64
- technologyId: tid,
65
- version: tech?.defaultVersion || "latest",
66
- };
417
+ return { technologyId: tid, version: tech?.defaultVersion || "latest" };
67
418
  });
68
419
  profile = template.profile;
69
- // Steps 4-5 skipped for template mode
70
- console.log(`\n${stepIndicator(4, 5, chalk.dim("Profile: ") + profile)}`);
71
- console.log(`${stepIndicator(5, 5, `${chalk.dim("Technologies: ") + technologies.length} from template`)}`);
420
+ console.log(`\n${stepIndicator(4, 6, chalk.dim("Profile: ") + profile)}`);
421
+ console.log(`${stepIndicator(5, 6, `${chalk.dim("Technologies: ") + technologies.length} from template`)}`);
72
422
  }
73
423
  else {
74
424
  // ── Step 2: Name ──
75
- console.log(`\n${stepIndicator(2, 5, "Name your stack")}`);
425
+ console.log(`\n${stepIndicator(2, 6, "Name your project")}`);
76
426
  stackName = await input({
77
- message: "Stack name:",
78
- default: "my-stack",
79
- validate: (v) => (v.trim().length > 0 ? true : "Name cannot be empty"),
427
+ message: "Project name:",
428
+ default: "my-project",
429
+ validate: (v) => {
430
+ if (v.trim().length === 0)
431
+ return "Name cannot be empty";
432
+ if (!/^[a-zA-Z0-9_.-]+$/.test(v.trim()))
433
+ return "Only letters, numbers, hyphens, dots, and underscores";
434
+ return true;
435
+ },
80
436
  });
81
437
  // ── Step 3: Profile ──
82
- console.log(`\n${stepIndicator(3, 5, "Choose a project profile")}`);
438
+ console.log(`\n${stepIndicator(3, 6, "Choose a project profile")}`);
83
439
  profile = (await select({
84
440
  message: "Project profile:",
441
+ loop: false,
85
442
  choices: [
86
- {
87
- name: `${chalk.cyan("Rapid")} ${chalk.dim(" Quick prototyping, minimal config")}`,
88
- value: "rapid",
89
- },
90
- {
91
- name: `${chalk.cyan("Standard")} ${chalk.dim("— Balanced defaults for most projects")}`,
92
- value: "standard",
93
- },
94
- {
95
- name: `${chalk.cyan("Production")} ${chalk.dim("— Battle-tested, monitoring included")}`,
96
- value: "production",
97
- },
98
- {
99
- name: `${chalk.cyan("Enterprise")} ${chalk.dim("— Full compliance, audit, security")}`,
100
- value: "enterprise",
101
- },
102
- {
103
- name: `${chalk.cyan("Lightweight")}${chalk.dim("— Minimal footprint")}`,
104
- value: "lightweight",
105
- },
443
+ { name: `${chalk.cyan("Rapid")} ${chalk.dim("\u2014 Quick prototyping, minimal config")}`, value: "rapid" },
444
+ { name: `${chalk.cyan("Standard")} ${chalk.dim("\u2014 Balanced defaults for most projects")}`, value: "standard" },
445
+ { name: `${chalk.cyan("Production")} ${chalk.dim("\u2014 Battle-tested, monitoring included")}`, value: "production" },
446
+ { name: `${chalk.cyan("Enterprise")} ${chalk.dim("\u2014 Full compliance, audit, security")}`, value: "enterprise" },
447
+ { name: `${chalk.cyan("Lightweight")} ${chalk.dim("\u2014 Minimal footprint")}`, value: "lightweight" },
106
448
  ],
107
449
  }));
108
450
  // ── Step 4: Technologies ──
109
- console.log(`\n${stepIndicator(4, 5, "Select technologies by category")}`);
110
- const categories = [
111
- "runtime",
112
- "frontend",
113
- "backend",
114
- "database",
115
- "orm",
116
- "auth",
117
- "styling",
118
- "service",
119
- "devops",
120
- ];
451
+ console.log(`\n${stepIndicator(4, 6, "Select technologies by category")}`);
452
+ console.log(chalk.dim(" Use arrow keys + space to select, Enter to confirm each category\n"));
453
+ const categories = ["runtime", "frontend", "backend", "database", "orm", "auth", "styling", "service", "devops"];
121
454
  for (const category of categories) {
122
455
  const available = rules.getByCategory(category);
123
456
  if (available.length === 0)
124
457
  continue;
125
458
  const selected = await checkbox({
126
- message: `${chalk.cyan(category)} technologies:`,
459
+ message: `${chalk.cyan(category.charAt(0).toUpperCase() + category.slice(1))} technologies:`,
460
+ pageSize: 15,
127
461
  choices: available.map((t) => ({
128
- name: `${t.name} ${chalk.dim(`(${t.defaultVersion})`)}`,
462
+ name: `${t.name} ${chalk.dim(`v${t.defaultVersion}`)} ${t.description ? chalk.dim(`\u2014 ${t.description}`) : ""}`,
129
463
  value: t.id,
130
464
  })),
131
465
  });
132
466
  for (const id of selected) {
133
467
  const tech = rules.getTechnology(id);
134
468
  if (tech) {
135
- technologies.push({
136
- technologyId: id,
137
- version: tech.defaultVersion,
138
- });
469
+ technologies.push({ technologyId: id, version: tech.defaultVersion });
139
470
  }
140
471
  }
141
472
  }
142
473
  // ── Step 5: Confirm ──
143
- console.log(`\n${stepIndicator(5, 5, "Review and confirm")}`);
474
+ console.log(`\n${stepIndicator(5, 6, "Review and confirm")}`);
144
475
  }
145
476
  if (technologies.length === 0) {
146
477
  console.log(warning("No technologies selected. Aborting."));
147
478
  return;
148
479
  }
149
- // Show summary before creating
480
+ // Show summary
150
481
  console.log("");
151
482
  console.log(chalk.bold(" Summary:"));
152
483
  console.log(` ${chalk.dim("Name:")} ${chalk.cyan(stackName)}`);
@@ -154,33 +485,51 @@ export const initCommand = new Command("init")
154
485
  console.log(` ${chalk.dim("Techs:")} ${technologies.map((t) => t.technologyId).join(", ")}`);
155
486
  console.log("");
156
487
  if (mode !== "template") {
157
- const proceed = await confirm({
158
- message: "Create this stack?",
159
- default: true,
160
- });
488
+ const proceed = await confirm({ message: "Create this stack?", default: true });
161
489
  if (!proceed) {
162
490
  console.log(chalk.dim(" Cancelled."));
163
491
  return;
164
492
  }
165
493
  }
166
- const { stack, validation } = engine.create({
167
- name: stackName,
168
- profile,
169
- technologies,
170
- });
494
+ const { stack, validation } = engine.create({ name: stackName, profile, technologies });
171
495
  console.log("");
172
496
  console.log(formatValidation(validation));
173
- console.log("");
174
497
  if (opts.json) {
175
498
  console.log(JSON.stringify({ stack, validation }, null, 2));
499
+ return;
176
500
  }
177
- else {
501
+ // ── Step 6: Generate project ──
502
+ console.log(`\n${stepIndicator(6, 6, "Generate project files")}`);
503
+ const shouldGenerate = await confirm({
504
+ message: "Generate the project files now?",
505
+ default: true,
506
+ });
507
+ if (!shouldGenerate) {
508
+ console.log("");
178
509
  console.log(formatStackSummary(stack));
179
510
  console.log(nextSteps([
180
- `stackweld scaffold ${stack.id} --path .`,
181
511
  `stackweld generate --name ${stackName} --path . --techs ${technologies.map((t) => t.technologyId).join(",")}`,
182
512
  `stackweld info ${stack.id}`,
183
513
  ]));
514
+ return;
515
+ }
516
+ const projectPath = await input({
517
+ message: "Where to create the project?",
518
+ default: process.cwd(),
519
+ validate: (v) => {
520
+ const resolved = path.resolve(v.trim());
521
+ if (!fs.existsSync(resolved))
522
+ return `Directory does not exist: ${resolved}`;
523
+ return true;
524
+ },
525
+ });
526
+ const targetDir = path.resolve(projectPath.trim(), stackName);
527
+ if (fs.existsSync(targetDir) && fs.readdirSync(targetDir).length > 0) {
528
+ console.log(chalk.red(`\n \u2716 Directory already exists and is not empty: ${targetDir}`));
529
+ return;
184
530
  }
531
+ console.log("");
532
+ const orchestrator = getScaffoldOrchestrator();
533
+ generateProject(stack, targetDir, stackName, rules, orchestrator, !!opts.json);
185
534
  });
186
535
  //# sourceMappingURL=init.js.map