@stackweld/cli 0.2.3 → 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.
- package/dist/commands/completion.js +49 -8
- package/dist/commands/init.d.ts +2 -1
- package/dist/commands/init.js +424 -75
- package/dist/index.js +1 -1
- package/package.json +2 -2
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
* stackweld completion — Generate shell completions.
|
|
3
3
|
*/
|
|
4
4
|
import { Command } from "commander";
|
|
5
|
+
const ALL_COMMANDS = "init create generate list info save delete clone export import import-url share compare browse doctor up down status logs scaffold template config completion ai score analyze env preview health migrate learn deploy lint benchmark cost plugin version";
|
|
5
6
|
const BASH_COMPLETION = `
|
|
6
7
|
# stackweld bash completion
|
|
7
8
|
_stackweld_completions() {
|
|
8
9
|
local cur="\${COMP_WORDS[COMP_CWORD]}"
|
|
9
|
-
local commands="
|
|
10
|
+
local commands="${ALL_COMMANDS}"
|
|
10
11
|
|
|
11
12
|
if [ "\${COMP_CWORD}" -eq 1 ]; then
|
|
12
13
|
COMPREPLY=( $(compgen -W "\${commands}" -- "\${cur}") )
|
|
@@ -20,23 +21,43 @@ const ZSH_COMPLETION = `
|
|
|
20
21
|
_stackweld() {
|
|
21
22
|
local -a commands
|
|
22
23
|
commands=(
|
|
23
|
-
'init:Create a new
|
|
24
|
+
'init:Create a new project interactively'
|
|
24
25
|
'create:Scaffold a project from a stack or template'
|
|
26
|
+
'generate:Create a fully scaffolded project'
|
|
25
27
|
'list:List all saved stacks'
|
|
26
28
|
'info:Show details about a stack or technology'
|
|
27
29
|
'save:Save a version snapshot of a stack'
|
|
30
|
+
'delete:Delete a saved stack'
|
|
31
|
+
'clone:Duplicate a saved stack'
|
|
28
32
|
'export:Export a stack definition to YAML or JSON'
|
|
29
33
|
'import:Import a stack definition from a file'
|
|
34
|
+
'import-url:Import a stack from a share URL'
|
|
35
|
+
'share:Generate a shareable URL for a stack'
|
|
36
|
+
'compare:Compare two saved stacks'
|
|
30
37
|
'browse:Browse the technology catalog and templates'
|
|
31
38
|
'doctor:Check system requirements and environment'
|
|
32
|
-
'delete:Delete a saved stack'
|
|
33
|
-
'version:Manage stack versions'
|
|
34
39
|
'up:Start Docker services'
|
|
35
40
|
'down:Stop Docker services'
|
|
36
41
|
'status:Show status of running services'
|
|
37
42
|
'logs:Show logs from Docker services'
|
|
38
43
|
'scaffold:Generate project files from a stack'
|
|
44
|
+
'template:Manage templates'
|
|
45
|
+
'config:Manage user preferences'
|
|
39
46
|
'completion:Generate shell completions'
|
|
47
|
+
'ai:AI-powered stack recommendations'
|
|
48
|
+
'score:Show compatibility score between technologies'
|
|
49
|
+
'analyze:Detect the technology stack of a project'
|
|
50
|
+
'env:Sync .env files and check for dangerous values'
|
|
51
|
+
'preview:Preview docker-compose.yml for a stack'
|
|
52
|
+
'health:Check project health and best practices'
|
|
53
|
+
'migrate:Generate a migration plan between technologies'
|
|
54
|
+
'learn:Show learning resources for a technology'
|
|
55
|
+
'deploy:Generate infrastructure-as-code files'
|
|
56
|
+
'lint:Validate a stack against team standards'
|
|
57
|
+
'benchmark:Show performance profile for a stack'
|
|
58
|
+
'cost:Estimate monthly hosting costs for a stack'
|
|
59
|
+
'plugin:Manage Stackweld plugins'
|
|
60
|
+
'version:Manage stack versions'
|
|
40
61
|
)
|
|
41
62
|
|
|
42
63
|
_describe 'command' commands
|
|
@@ -46,23 +67,43 @@ _stackweld
|
|
|
46
67
|
`.trim();
|
|
47
68
|
const FISH_COMPLETION = `
|
|
48
69
|
# stackweld fish completions
|
|
49
|
-
complete -c stackweld -n "__fish_use_subcommand" -a init -d "Create a new
|
|
50
|
-
complete -c stackweld -n "__fish_use_subcommand" -a create -d "Scaffold a project"
|
|
70
|
+
complete -c stackweld -n "__fish_use_subcommand" -a init -d "Create a new project interactively"
|
|
71
|
+
complete -c stackweld -n "__fish_use_subcommand" -a create -d "Scaffold a project from a stack or template"
|
|
72
|
+
complete -c stackweld -n "__fish_use_subcommand" -a generate -d "Create a fully scaffolded project"
|
|
51
73
|
complete -c stackweld -n "__fish_use_subcommand" -a list -d "List all saved stacks"
|
|
52
74
|
complete -c stackweld -n "__fish_use_subcommand" -a info -d "Show details about a stack or technology"
|
|
53
75
|
complete -c stackweld -n "__fish_use_subcommand" -a save -d "Save a version snapshot"
|
|
76
|
+
complete -c stackweld -n "__fish_use_subcommand" -a delete -d "Delete a saved stack"
|
|
77
|
+
complete -c stackweld -n "__fish_use_subcommand" -a clone -d "Duplicate a saved stack"
|
|
54
78
|
complete -c stackweld -n "__fish_use_subcommand" -a export -d "Export a stack definition"
|
|
55
79
|
complete -c stackweld -n "__fish_use_subcommand" -a import -d "Import a stack definition"
|
|
80
|
+
complete -c stackweld -n "__fish_use_subcommand" -a import-url -d "Import from a share URL"
|
|
81
|
+
complete -c stackweld -n "__fish_use_subcommand" -a share -d "Share a stack via URL"
|
|
82
|
+
complete -c stackweld -n "__fish_use_subcommand" -a compare -d "Compare two stacks"
|
|
56
83
|
complete -c stackweld -n "__fish_use_subcommand" -a browse -d "Browse the technology catalog"
|
|
57
84
|
complete -c stackweld -n "__fish_use_subcommand" -a doctor -d "Check system requirements"
|
|
58
|
-
complete -c stackweld -n "__fish_use_subcommand" -a delete -d "Delete a saved stack"
|
|
59
|
-
complete -c stackweld -n "__fish_use_subcommand" -a version -d "Manage stack versions"
|
|
60
85
|
complete -c stackweld -n "__fish_use_subcommand" -a up -d "Start Docker services"
|
|
61
86
|
complete -c stackweld -n "__fish_use_subcommand" -a down -d "Stop Docker services"
|
|
62
87
|
complete -c stackweld -n "__fish_use_subcommand" -a status -d "Show status of running services"
|
|
63
88
|
complete -c stackweld -n "__fish_use_subcommand" -a logs -d "Show logs from Docker services"
|
|
64
89
|
complete -c stackweld -n "__fish_use_subcommand" -a scaffold -d "Generate project files"
|
|
90
|
+
complete -c stackweld -n "__fish_use_subcommand" -a template -d "Manage templates"
|
|
91
|
+
complete -c stackweld -n "__fish_use_subcommand" -a config -d "Manage user preferences"
|
|
65
92
|
complete -c stackweld -n "__fish_use_subcommand" -a completion -d "Generate shell completions"
|
|
93
|
+
complete -c stackweld -n "__fish_use_subcommand" -a ai -d "AI-powered stack recommendations"
|
|
94
|
+
complete -c stackweld -n "__fish_use_subcommand" -a score -d "Compatibility score between technologies"
|
|
95
|
+
complete -c stackweld -n "__fish_use_subcommand" -a analyze -d "Detect project technology stack"
|
|
96
|
+
complete -c stackweld -n "__fish_use_subcommand" -a env -d "Sync .env files"
|
|
97
|
+
complete -c stackweld -n "__fish_use_subcommand" -a preview -d "Preview docker-compose.yml"
|
|
98
|
+
complete -c stackweld -n "__fish_use_subcommand" -a health -d "Check project health"
|
|
99
|
+
complete -c stackweld -n "__fish_use_subcommand" -a migrate -d "Migration plan between technologies"
|
|
100
|
+
complete -c stackweld -n "__fish_use_subcommand" -a learn -d "Learning resources for a technology"
|
|
101
|
+
complete -c stackweld -n "__fish_use_subcommand" -a deploy -d "Generate infrastructure-as-code"
|
|
102
|
+
complete -c stackweld -n "__fish_use_subcommand" -a lint -d "Validate against team standards"
|
|
103
|
+
complete -c stackweld -n "__fish_use_subcommand" -a benchmark -d "Performance profile for a stack"
|
|
104
|
+
complete -c stackweld -n "__fish_use_subcommand" -a cost -d "Estimate hosting costs"
|
|
105
|
+
complete -c stackweld -n "__fish_use_subcommand" -a plugin -d "Manage plugins"
|
|
106
|
+
complete -c stackweld -n "__fish_use_subcommand" -a version -d "Manage stack versions"
|
|
66
107
|
`.trim();
|
|
67
108
|
export const completionCommand = new Command("completion")
|
|
68
109
|
.description("Generate shell completion scripts")
|
package/dist/commands/init.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* stackweld init —
|
|
2
|
+
* stackweld init — Interactive project creation wizard.
|
|
3
|
+
* Creates the stack definition AND generates the full project.
|
|
3
4
|
*/
|
|
4
5
|
import { Command } from "commander";
|
|
5
6
|
export declare const initCommand: Command;
|
package/dist/commands/init.js
CHANGED
|
@@ -1,29 +1,375 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* stackweld init —
|
|
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
|
|
9
|
-
import {
|
|
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
|
|
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,
|
|
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
|
|
26
|
-
{ name: "From a 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,
|
|
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("
|
|
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,
|
|
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) =>
|
|
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
|
-
|
|
70
|
-
console.log(
|
|
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,
|
|
425
|
+
console.log(`\n${stepIndicator(2, 6, "Name your project")}`);
|
|
76
426
|
stackName = await input({
|
|
77
|
-
message: "
|
|
78
|
-
default: "my-
|
|
79
|
-
validate: (v) =>
|
|
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,
|
|
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
|
-
|
|
88
|
-
|
|
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,
|
|
110
|
-
|
|
111
|
-
|
|
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(`
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
package/dist/index.js
CHANGED
|
@@ -41,7 +41,7 @@ import { statusCommand } from "./commands/status.js";
|
|
|
41
41
|
import { templateCommand } from "./commands/template.js";
|
|
42
42
|
import { upCommand } from "./commands/up.js";
|
|
43
43
|
import { versionCommand } from "./commands/version-cmd.js";
|
|
44
|
-
const VERSION = "0.2.
|
|
44
|
+
const VERSION = "0.2.4";
|
|
45
45
|
const program = new Command();
|
|
46
46
|
program
|
|
47
47
|
.name("stackweld")
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stackweld/cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
4
4
|
"description": "The operating system for your dev stacks — define, validate, scaffold, and launch development environments from the terminal.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Orlando Fernandez <hello@xplustechnologies.com>",
|
|
@@ -40,8 +40,8 @@
|
|
|
40
40
|
"commander": "^13.0.0",
|
|
41
41
|
"ora": "^8.0.0",
|
|
42
42
|
"yaml": "^2.8.3",
|
|
43
|
-
"@stackweld/core": "0.2.1",
|
|
44
43
|
"@stackweld/registry": "0.2.1",
|
|
44
|
+
"@stackweld/core": "0.2.1",
|
|
45
45
|
"@stackweld/templates": "0.2.1"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|