create-backlist 9.0.1 → 10.0.2
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/bin/index.js +486 -210
- package/bin/qa.js +112 -53
- package/package.json +1 -1
- package/src/qa/qa-engine.js +664 -401
package/bin/index.js
CHANGED
|
@@ -1,9 +1,21 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
4
|
-
// create-backlist v8.0 — Smart Freemium SaaS CLI
|
|
4
|
+
// create-backlist v8.0 — Smart Freemium SaaS CLI ⚡ 10X EDITION
|
|
5
5
|
// Copyright (c) W.A.H.ISHAN — MIT License
|
|
6
|
-
//
|
|
6
|
+
//
|
|
7
|
+
// 10X UPGRADES:
|
|
8
|
+
// ✦ Startup performance timer & memory profiler
|
|
9
|
+
// ✦ Auto-retry on generation failure (up to 3 attempts)
|
|
10
|
+
// ✦ Enhanced progress tracking with ETA
|
|
11
|
+
// ✦ Stronger input validation with helpful hints
|
|
12
|
+
// ✦ Smarter pre-flight checks with auto-fix suggestions
|
|
13
|
+
// ✦ Graceful SIGINT / SIGTERM shutdown handler
|
|
14
|
+
// ✦ Parallel plugin loading + generator warmup
|
|
15
|
+
// ✦ Rich error diagnostics with stack-trace filtering
|
|
16
|
+
// ✦ Session diff — shows what changed since last run
|
|
17
|
+
// ✦ Post-gen file-tree summary
|
|
18
|
+
// ✦ QA Engine v10.0 — URL QA, HTTP probe, security, SEO, a11y
|
|
7
19
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
8
20
|
|
|
9
21
|
import * as p from '@clack/prompts';
|
|
@@ -15,11 +27,13 @@ import os from 'node:os';
|
|
|
15
27
|
import { fileURLToPath } from 'node:url';
|
|
16
28
|
import { performance } from 'node:perf_hooks';
|
|
17
29
|
|
|
18
|
-
|
|
19
30
|
// ── Polyfill __dirname for ES Modules ────────────────────────────────────
|
|
20
31
|
const __filename = fileURLToPath(import.meta.url);
|
|
21
32
|
const __dirname = path.dirname(__filename);
|
|
22
33
|
|
|
34
|
+
// ── CLI boot timestamp ────────────────────────────────────────────────────
|
|
35
|
+
const BOOT_START = performance.now();
|
|
36
|
+
|
|
23
37
|
// ── Internal Modules ─────────────────────────────────────────────────────
|
|
24
38
|
import { isCommandAvailable } from '../src/utils.js';
|
|
25
39
|
import { analyzeFrontend, performLowCostPathScan,
|
|
@@ -34,19 +48,25 @@ import { generateDotnetProject } from '../src/generators/dotnet.js';
|
|
|
34
48
|
import { generateJavaProject } from '../src/generators/java.js';
|
|
35
49
|
import { generatePythonProject } from '../src/generators/python.js';
|
|
36
50
|
|
|
37
|
-
// ── QA System
|
|
38
|
-
import {
|
|
39
|
-
|
|
40
|
-
|
|
51
|
+
// ── QA System v10.0 ───────────────────────────────────────────────────────
|
|
52
|
+
import {
|
|
53
|
+
runManualQA,
|
|
54
|
+
runAutomatedQA,
|
|
55
|
+
runUrlQA,
|
|
56
|
+
viewQAHistory,
|
|
57
|
+
initQASystem,
|
|
58
|
+
autoRunPostGeneration,
|
|
59
|
+
} from '../src/qa/qa-engine.js';
|
|
41
60
|
|
|
42
61
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
43
62
|
// Constants & Paths
|
|
44
63
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
45
64
|
|
|
46
|
-
const CONFIG_PATH
|
|
47
|
-
const SESSIONS_PATH
|
|
48
|
-
const PLUGINS_DIR
|
|
49
|
-
const VERSION
|
|
65
|
+
const CONFIG_PATH = path.join(os.homedir(), '.backlist-config.json');
|
|
66
|
+
const SESSIONS_PATH = path.join(os.homedir(), '.backlist-sessions.json');
|
|
67
|
+
const PLUGINS_DIR = path.join(os.homedir(), '.backlist-plugins');
|
|
68
|
+
const VERSION = '8.0.0-10X';
|
|
69
|
+
const MAX_RETRIES = 3;
|
|
50
70
|
|
|
51
71
|
// ── Pricing Table ─────────────────────────────────────────────────────────
|
|
52
72
|
const PRICING = {
|
|
@@ -64,8 +84,46 @@ const STACK_META = {
|
|
|
64
84
|
'python-fastapi' : { lang: 'Python', runtime: 'FastAPI', color: '#009688', icon: '🐍' },
|
|
65
85
|
};
|
|
66
86
|
|
|
87
|
+
// ── Pre-flight fix hints ──────────────────────────────────────────────────
|
|
88
|
+
const PREFLIGHT_HINTS = {
|
|
89
|
+
'Node.js ≥ 18' : 'Download from https://nodejs.org — use v18 LTS or higher.',
|
|
90
|
+
'package.json present': 'Run `npm init -y` in your project root first.',
|
|
91
|
+
'.NET SDK installed' : 'Download from https://dotnet.microsoft.com/download',
|
|
92
|
+
'Java JDK installed' : 'Download from https://adoptium.net/',
|
|
93
|
+
'Python 3 installed' : 'Download from https://python.org or use `winget install Python.Python.3`',
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
// ── RSS memory helper (used in banner) ───────────────────────────────────
|
|
97
|
+
const rssMB = (() => {
|
|
98
|
+
try { return (process.memoryUsage().rss / 1024 / 1024).toFixed(0); } catch { return '?'; }
|
|
99
|
+
})();
|
|
100
|
+
|
|
67
101
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
68
|
-
//
|
|
102
|
+
// Graceful Shutdown Handler
|
|
103
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
104
|
+
|
|
105
|
+
let _cleanupDir = null;
|
|
106
|
+
|
|
107
|
+
async function gracefulShutdown(signal) {
|
|
108
|
+
console.log('');
|
|
109
|
+
console.log(chalk.yellow(`\n ⚠️ ${signal} received — shutting down gracefully...`));
|
|
110
|
+
if (_cleanupDir && await fs.pathExists(_cleanupDir)) {
|
|
111
|
+
const sc = ora({
|
|
112
|
+
text : chalk.yellow('Cleaning up partial output...'),
|
|
113
|
+
spinner: 'line',
|
|
114
|
+
color : 'yellow',
|
|
115
|
+
}).start();
|
|
116
|
+
await fs.remove(_cleanupDir).catch(() => {});
|
|
117
|
+
sc.succeed(chalk.yellow('Partial output removed.'));
|
|
118
|
+
}
|
|
119
|
+
process.exit(0);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
|
|
123
|
+
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
|
|
124
|
+
|
|
125
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
126
|
+
// Animated ASCII Banner — v8.0 10X
|
|
69
127
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
70
128
|
|
|
71
129
|
function printBanner() {
|
|
@@ -83,33 +141,35 @@ function printBanner() {
|
|
|
83
141
|
console.log(c1(' ║') + c2.bold(' / /_/ / / ___ |/ /___/ /| | / /____/ / ___/ / ') + c1('║'));
|
|
84
142
|
console.log(c1(' ║') + c2.bold('/_____/ /_/ |_|\\____/_/ |_| /_____/___//____/ ') + c1('║'));
|
|
85
143
|
console.log(c1(' ║') + ' ' + c1('║'));
|
|
86
|
-
console.log(c1(' ║') + c3.bold('
|
|
87
|
-
console.log(c1(' ║') + dim('
|
|
144
|
+
console.log(c1(' ║') + c3.bold(' ⚡ v8.0-10X SaaS — Polyglot Backend Engine ⚡ ') + c1('║'));
|
|
145
|
+
console.log(c1(' ║') + dim(' Smart Routing · Auto-Retry · Health Monitor · Plugin System ') + c1('║'));
|
|
88
146
|
console.log(c1(' ╚══════════════════════════════════════════════════════════════╝'));
|
|
89
147
|
console.log('');
|
|
90
148
|
|
|
91
|
-
|
|
149
|
+
const bootMs = (performance.now() - BOOT_START).toFixed(0);
|
|
92
150
|
const mem = process.memoryUsage();
|
|
93
151
|
const heapMB = (mem.heapUsed / 1024 / 1024).toFixed(0);
|
|
94
152
|
const uptime = process.uptime().toFixed(1);
|
|
95
153
|
const nodeVer = process.version;
|
|
96
154
|
const platform = process.platform;
|
|
155
|
+
const cpus = os.cpus().length;
|
|
97
156
|
|
|
98
157
|
console.log(
|
|
99
158
|
dim(' ') +
|
|
100
159
|
c4('◉ LIVE') + dim(' │ ') +
|
|
101
|
-
dim('
|
|
102
|
-
dim('
|
|
103
|
-
dim('
|
|
104
|
-
dim('
|
|
105
|
-
dim('
|
|
160
|
+
dim('Boot ') + chalk.white(bootMs + 'ms') + dim(' │ ') +
|
|
161
|
+
dim('Node ') + chalk.white(nodeVer) + dim(' │ ') +
|
|
162
|
+
dim('Heap ') + chalk.white(heapMB + 'MB') + dim('/') + chalk.gray(rssMB + 'MB RSS') + dim(' │ ') +
|
|
163
|
+
dim('CPUs ') + chalk.white(cpus) + dim(' │ ') +
|
|
164
|
+
dim('OS ') + chalk.white(platform) + dim(' │ ') +
|
|
165
|
+
dim('v') + chalk.white(VERSION)
|
|
106
166
|
);
|
|
107
167
|
console.log(dim(' ─────────────────────────────────────────────────────────────'));
|
|
108
168
|
console.log('');
|
|
109
169
|
}
|
|
110
170
|
|
|
111
171
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
112
|
-
// Smart Session Manager
|
|
172
|
+
// Smart Session Manager
|
|
113
173
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
114
174
|
|
|
115
175
|
async function loadLastSession() {
|
|
@@ -129,14 +189,33 @@ async function saveSession(options) {
|
|
|
129
189
|
stack : options.stack,
|
|
130
190
|
dbType : options.dbType,
|
|
131
191
|
generationMode: options.generationMode,
|
|
192
|
+
projectName : options.projectName,
|
|
132
193
|
savedAt : new Date().toISOString(),
|
|
194
|
+
extraFeatures : options.extraFeatures ?? [],
|
|
195
|
+
addAuth : options.addAuth,
|
|
196
|
+
addSeeder : options.addSeeder,
|
|
133
197
|
},
|
|
134
198
|
}, { spaces: 2 });
|
|
135
199
|
} catch {}
|
|
136
200
|
}
|
|
137
201
|
|
|
202
|
+
function printSessionDiff(last, current) {
|
|
203
|
+
if (!last) return;
|
|
204
|
+
const changes = [];
|
|
205
|
+
if (last.stack !== current.stack)
|
|
206
|
+
changes.push(`Stack: ${chalk.red(last.stack)} → ${chalk.green(current.stack)}`);
|
|
207
|
+
if (last.dbType !== current.dbType)
|
|
208
|
+
changes.push(`DB: ${chalk.red(last.dbType)} → ${chalk.green(current.dbType)}`);
|
|
209
|
+
if (last.generationMode !== current.generationMode)
|
|
210
|
+
changes.push(`Mode: ${chalk.red(last.generationMode)} → ${chalk.green(current.generationMode)}`);
|
|
211
|
+
if (changes.length === 0) return;
|
|
212
|
+
console.log(chalk.dim(' ── 🔀 Changes from last session:'));
|
|
213
|
+
changes.forEach(c => console.log(chalk.dim(' ' + c)));
|
|
214
|
+
console.log('');
|
|
215
|
+
}
|
|
216
|
+
|
|
138
217
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
139
|
-
// Plugin Loader —
|
|
218
|
+
// Plugin Loader — parallel loading
|
|
140
219
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
141
220
|
|
|
142
221
|
async function loadPlugins() {
|
|
@@ -144,18 +223,16 @@ async function loadPlugins() {
|
|
|
144
223
|
try {
|
|
145
224
|
await fs.ensureDir(PLUGINS_DIR);
|
|
146
225
|
const entries = await fs.readdir(PLUGINS_DIR);
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
}
|
|
158
|
-
}
|
|
226
|
+
const results = await Promise.allSettled(
|
|
227
|
+
entries.map(async (entry) => {
|
|
228
|
+
const pluginPath = path.join(PLUGINS_DIR, entry, 'index.js');
|
|
229
|
+
if (!await fs.pathExists(pluginPath)) return null;
|
|
230
|
+
const plugin = await import(pluginPath);
|
|
231
|
+
return (plugin.default?.name && plugin.default?.run) ? plugin.default : null;
|
|
232
|
+
})
|
|
233
|
+
);
|
|
234
|
+
for (const r of results) {
|
|
235
|
+
if (r.status === 'fulfilled' && r.value) plugins.push(r.value);
|
|
159
236
|
}
|
|
160
237
|
} catch {}
|
|
161
238
|
return plugins;
|
|
@@ -168,22 +245,31 @@ async function loadPlugins() {
|
|
|
168
245
|
async function runPreflightChecks(stack) {
|
|
169
246
|
const checks = [];
|
|
170
247
|
|
|
171
|
-
|
|
172
|
-
checks.push({ name: '
|
|
248
|
+
const nodeOk = parseInt(process.version.slice(1)) >= 18;
|
|
249
|
+
checks.push({ name: 'Node.js ≥ 18', pass: nodeOk });
|
|
250
|
+
|
|
251
|
+
const pkgOk = await fs.pathExists(path.join(process.cwd(), 'package.json'));
|
|
252
|
+
checks.push({ name: 'package.json present', pass: pkgOk });
|
|
173
253
|
|
|
174
|
-
if (stack === 'dotnet-webapi')
|
|
175
|
-
|
|
176
|
-
if (stack === '
|
|
254
|
+
if (stack === 'dotnet-webapi')
|
|
255
|
+
checks.push({ name: '.NET SDK installed', pass: await isCommandAvailable('dotnet') });
|
|
256
|
+
if (stack === 'java-spring')
|
|
257
|
+
checks.push({ name: 'Java JDK installed', pass: await isCommandAvailable('java') });
|
|
258
|
+
if (stack === 'python-fastapi')
|
|
259
|
+
checks.push({ name: 'Python 3 installed', pass: await isCommandAvailable('python3') || await isCommandAvailable('python') });
|
|
177
260
|
|
|
178
|
-
const failed = checks.filter(
|
|
261
|
+
const failed = checks.filter(c => !c.pass);
|
|
179
262
|
|
|
180
263
|
if (checks.length > 0) {
|
|
181
264
|
console.log('');
|
|
182
265
|
console.log(chalk.hex('#00F5FF').bold(' ── 🔍 Pre-flight Checks ──────────────────────────────'));
|
|
183
|
-
checks.forEach(
|
|
184
|
-
const icon
|
|
266
|
+
checks.forEach(c => {
|
|
267
|
+
const icon = c.pass ? chalk.green(' ✓') : chalk.red(' ✗');
|
|
185
268
|
const label = c.pass ? chalk.dim(c.name) : chalk.red.bold(c.name);
|
|
186
269
|
console.log(`${icon} ${label}`);
|
|
270
|
+
if (!c.pass && PREFLIGHT_HINTS[c.name]) {
|
|
271
|
+
console.log(chalk.gray(` 💡 Fix: ${PREFLIGHT_HINTS[c.name]}`));
|
|
272
|
+
}
|
|
187
273
|
});
|
|
188
274
|
console.log('');
|
|
189
275
|
}
|
|
@@ -192,7 +278,7 @@ async function runPreflightChecks(stack) {
|
|
|
192
278
|
}
|
|
193
279
|
|
|
194
280
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
195
|
-
//
|
|
281
|
+
// Progress Bar
|
|
196
282
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
197
283
|
|
|
198
284
|
function buildProgressBar(pct, width = 24) {
|
|
@@ -200,24 +286,28 @@ function buildProgressBar(pct, width = 24) {
|
|
|
200
286
|
return chalk.hex('#00F5FF')('█'.repeat(filled)) + chalk.gray('░'.repeat(width - filled));
|
|
201
287
|
}
|
|
202
288
|
|
|
289
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
290
|
+
// Token / Pricing Display
|
|
291
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
292
|
+
|
|
203
293
|
function printTokenUsage(mode, startTime, extra = {}) {
|
|
204
|
-
const elapsed
|
|
294
|
+
const elapsed = ((Date.now() - startTime) / 1000).toFixed(2);
|
|
205
295
|
const pricing = PRICING[mode] || PRICING.free;
|
|
296
|
+
const throughput = extra.filesGenerated && elapsed > 0
|
|
297
|
+
? (extra.filesGenerated / parseFloat(elapsed)).toFixed(1) + ' files/s'
|
|
298
|
+
: null;
|
|
206
299
|
|
|
207
300
|
console.log('');
|
|
208
301
|
console.log(chalk.hex('#BF40FF').bold(' ┌─────────────────────────────────────────────────┐'));
|
|
209
|
-
console.log(chalk.hex('#BF40FF').bold(' │ 📊 Generation Summary
|
|
302
|
+
console.log(chalk.hex('#BF40FF').bold(' │ 📊 Generation Summary (10X) │'));
|
|
210
303
|
console.log(chalk.hex('#BF40FF').bold(' └─────────────────────────────────────────────────┘'));
|
|
211
304
|
console.log('');
|
|
212
305
|
console.log(` ${chalk.dim('Mode:')} ${mode === 'pro' ? chalk.hex('#BF40FF').bold('PRO AI ✦') : chalk.hex('#00F5FF').bold('Standard ◉')}`);
|
|
213
306
|
console.log(` ${chalk.dim('Elapsed:')} ${chalk.white(elapsed + 's')}`);
|
|
214
|
-
|
|
215
|
-
if (extra.
|
|
216
|
-
|
|
217
|
-
}
|
|
218
|
-
if (extra.filesGenerated !== undefined) {
|
|
219
|
-
console.log(` ${chalk.dim('Files written:')} ${chalk.white(extra.filesGenerated)}`);
|
|
220
|
-
}
|
|
307
|
+
if (extra.endpointCount !== undefined) console.log(` ${chalk.dim('Endpoints:')} ${chalk.white(extra.endpointCount + ' detected')}`);
|
|
308
|
+
if (extra.filesGenerated !== undefined) console.log(` ${chalk.dim('Files written:')} ${chalk.white(extra.filesGenerated)}`);
|
|
309
|
+
if (throughput) console.log(` ${chalk.dim('Throughput:')} ${chalk.cyan(throughput)}`);
|
|
310
|
+
if (extra.retries && extra.retries > 0) console.log(` ${chalk.dim('Retries:')} ${chalk.yellow(extra.retries)}`);
|
|
221
311
|
if (mode === 'pro') {
|
|
222
312
|
const usagePct = Math.round((pricing.outputTokens / 16000) * 100);
|
|
223
313
|
console.log(` ${chalk.dim('Input tokens:')} ${chalk.yellow(pricing.inputTokens.toLocaleString())}`);
|
|
@@ -229,7 +319,7 @@ function printTokenUsage(mode, startTime, extra = {}) {
|
|
|
229
319
|
}
|
|
230
320
|
|
|
231
321
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
232
|
-
// Smart Frontend Scanner
|
|
322
|
+
// Smart Frontend Scanner
|
|
233
323
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
234
324
|
|
|
235
325
|
async function detectFrontendFramework(srcDir) {
|
|
@@ -238,21 +328,20 @@ async function detectFrontendFramework(srcDir) {
|
|
|
238
328
|
if (await fs.pathExists(pkgPath)) {
|
|
239
329
|
const pkg = await fs.readJson(pkgPath);
|
|
240
330
|
const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
|
|
241
|
-
|
|
242
|
-
if (deps['
|
|
243
|
-
if (deps['
|
|
244
|
-
if (deps['
|
|
245
|
-
if (deps['
|
|
246
|
-
if (deps['
|
|
247
|
-
if (deps['
|
|
248
|
-
if (deps['solid-js']) return { name: 'SolidJS', icon: '💠', color: '#2C4F7C' };
|
|
331
|
+
if (deps['next']) return { name: 'Next.js', icon: '▲', color: '#000000' };
|
|
332
|
+
if (deps['nuxt']) return { name: 'Nuxt.js', icon: '💚', color: '#00DC82' };
|
|
333
|
+
if (deps['@angular/core']) return { name: 'Angular', icon: '🅰️', color: '#DD0031' };
|
|
334
|
+
if (deps['react']) return { name: 'React', icon: '⚛️', color: '#61DAFB' };
|
|
335
|
+
if (deps['vue']) return { name: 'Vue.js', icon: '💚', color: '#42B883' };
|
|
336
|
+
if (deps['svelte']) return { name: 'Svelte', icon: '🔥', color: '#FF3E00' };
|
|
337
|
+
if (deps['solid-js']) return { name: 'SolidJS', icon: '💠', color: '#2C4F7C' };
|
|
249
338
|
}
|
|
250
339
|
} catch {}
|
|
251
340
|
return { name: 'Unknown', icon: '📦', color: '#888888' };
|
|
252
341
|
}
|
|
253
342
|
|
|
254
343
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
255
|
-
// API Key Management
|
|
344
|
+
// API Key Management
|
|
256
345
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
257
346
|
|
|
258
347
|
async function getProApiKey() {
|
|
@@ -260,7 +349,7 @@ async function getProApiKey() {
|
|
|
260
349
|
try {
|
|
261
350
|
const config = await fs.readJson(CONFIG_PATH);
|
|
262
351
|
if (config.apiKey && typeof config.apiKey === 'string' && config.apiKey.length >= 10) {
|
|
263
|
-
const savedAt
|
|
352
|
+
const savedAt = config.savedAt ? new Date(config.savedAt) : null;
|
|
264
353
|
const ageHours = savedAt ? ((Date.now() - savedAt.getTime()) / 3600000).toFixed(0) : '?';
|
|
265
354
|
const masked = config.apiKey.slice(0, 4) + '●'.repeat(12) + config.apiKey.slice(-4);
|
|
266
355
|
p.log.success(chalk.green(`Pro Key loaded: ${masked} (saved ${ageHours}h ago)`));
|
|
@@ -282,23 +371,23 @@ async function getProApiKey() {
|
|
|
282
371
|
const apiKey = await p.password({
|
|
283
372
|
message : 'Enter your Backlist Pro API Key:',
|
|
284
373
|
validate: (input) => {
|
|
285
|
-
if (!input || input.length < 10) return 'Invalid key — must be at least 10 characters.';
|
|
374
|
+
if (!input || input.trim().length < 10) return '❌ Invalid key — must be at least 10 characters.';
|
|
375
|
+
if (/\s/.test(input)) return '❌ Key must not contain spaces.';
|
|
286
376
|
},
|
|
287
377
|
});
|
|
288
378
|
if (p.isCancel(apiKey)) { p.cancel('Cancelled.'); process.exit(0); }
|
|
289
379
|
|
|
290
380
|
const spinner = ora({ text: chalk.cyan('Validating API key...'), spinner: 'arc', color: 'cyan' }).start();
|
|
291
|
-
await new Promise(
|
|
381
|
+
await new Promise(r => setTimeout(r, 1800));
|
|
292
382
|
spinner.succeed(chalk.green('API key validated ✓'));
|
|
293
383
|
|
|
294
384
|
await fs.writeJson(CONFIG_PATH, { apiKey, savedAt: new Date().toISOString() }, { spaces: 2 });
|
|
295
385
|
p.log.info(`Key saved → ${CONFIG_PATH}`);
|
|
296
|
-
|
|
297
386
|
return apiKey;
|
|
298
387
|
}
|
|
299
388
|
|
|
300
389
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
301
|
-
// Free Mode Pipeline
|
|
390
|
+
// Free Mode Pipeline
|
|
302
391
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
303
392
|
|
|
304
393
|
async function runFreeModePipeline(options) {
|
|
@@ -306,35 +395,50 @@ async function runFreeModePipeline(options) {
|
|
|
306
395
|
console.log(chalk.hex('#00F5FF').bold(' ─── 🚀 Standard Mode: AST + EJS Static Generation ───'));
|
|
307
396
|
console.log('');
|
|
308
397
|
|
|
309
|
-
const t0
|
|
398
|
+
const t0 = performance.now();
|
|
399
|
+
const steps = 5;
|
|
400
|
+
let stepsDone = 0;
|
|
401
|
+
const stepLabel = () => chalk.dim(`[${++stepsDone}/${steps}]`);
|
|
310
402
|
|
|
311
403
|
// Step 1 — AST
|
|
312
|
-
const spinnerAST = ora({
|
|
404
|
+
const spinnerAST = ora({
|
|
405
|
+
text : `${stepLabel()} ${chalk.white('Parsing frontend files with Babel AST...')}`,
|
|
406
|
+
spinner: 'dots12',
|
|
407
|
+
color : 'cyan',
|
|
408
|
+
}).start();
|
|
313
409
|
let endpoints = [];
|
|
314
410
|
try { endpoints = await analyzeFrontend(options.frontendSrcDir); } catch {}
|
|
315
|
-
await new Promise(
|
|
411
|
+
await new Promise(r => setTimeout(r, 800));
|
|
316
412
|
spinnerAST.succeed(chalk.green(`AST parsing complete — ${chalk.bold(endpoints.length)} endpoint(s) mapped.`));
|
|
317
413
|
|
|
318
414
|
// Step 2 — Framework detection
|
|
319
415
|
const fw = await detectFrontendFramework(options.frontendSrcDir);
|
|
320
|
-
p.log.info(chalk.dim(
|
|
416
|
+
p.log.info(chalk.dim(`${stepLabel()} Frontend detected: ${fw.icon} ${chalk.white(fw.name)}`));
|
|
321
417
|
|
|
322
418
|
// Step 3 — DOM Live Check
|
|
323
|
-
const spinnerDOM = ora({
|
|
419
|
+
const spinnerDOM = ora({
|
|
420
|
+
text : `${stepLabel()} ${chalk.white('Running DOM Live Check...')}`,
|
|
421
|
+
spinner: 'bouncingBar',
|
|
422
|
+
color : 'yellow',
|
|
423
|
+
}).start();
|
|
324
424
|
const inconsistencies = await performLowCostPathScan(options.frontendSrcDir, endpoints);
|
|
325
|
-
await new Promise(
|
|
425
|
+
await new Promise(r => setTimeout(r, 1200));
|
|
326
426
|
if (inconsistencies.length > 0) {
|
|
327
427
|
spinnerDOM.warn(chalk.yellow(`DOM Live Check — ${inconsistencies.length} path drift(s) detected.`));
|
|
328
|
-
inconsistencies.slice(0, 3).forEach(
|
|
428
|
+
inconsistencies.slice(0, 3).forEach(i => console.log(chalk.gray(` → ${i.warning}`)));
|
|
329
429
|
} else {
|
|
330
430
|
spinnerDOM.succeed(chalk.green('DOM Live Check passed — ') + chalk.yellow.bold('15% false positives eliminated!'));
|
|
331
431
|
}
|
|
332
432
|
|
|
333
433
|
// Step 4 — EJS Scaffolding
|
|
334
|
-
const meta
|
|
434
|
+
const meta = STACK_META[options.stack] || {};
|
|
335
435
|
const stackLabel = `${meta.icon || '⚙️'} ${options.stack}`;
|
|
336
|
-
const spinnerEJS = ora({
|
|
337
|
-
|
|
436
|
+
const spinnerEJS = ora({
|
|
437
|
+
text : `${stepLabel()} ${chalk.white(`Scaffolding ${stackLabel} via Hexagonal EJS Templates...`)}`,
|
|
438
|
+
spinner: 'material',
|
|
439
|
+
color : 'magenta',
|
|
440
|
+
}).start();
|
|
441
|
+
await new Promise(r => setTimeout(r, 600));
|
|
338
442
|
|
|
339
443
|
try {
|
|
340
444
|
await dispatchGenerator(options);
|
|
@@ -345,18 +449,58 @@ async function runFreeModePipeline(options) {
|
|
|
345
449
|
}
|
|
346
450
|
|
|
347
451
|
// Step 5 — Count generated files
|
|
452
|
+
const spinnerCount = ora({
|
|
453
|
+
text : `${stepLabel()} ${chalk.white('Counting generated files...')}`,
|
|
454
|
+
spinner: 'line',
|
|
455
|
+
color : 'cyan',
|
|
456
|
+
}).start();
|
|
348
457
|
let fileCount = 0;
|
|
458
|
+
try { fileCount = await globCount(options.projectDir); } catch {}
|
|
459
|
+
spinnerCount.succeed(chalk.green(`${fileCount} file(s) written to ${chalk.bold(options.projectName)}/`));
|
|
460
|
+
|
|
461
|
+
await printFileTreeSummary(options.projectDir);
|
|
462
|
+
|
|
463
|
+
options._meta = {
|
|
464
|
+
endpointCount : endpoints.length,
|
|
465
|
+
filesGenerated : fileCount,
|
|
466
|
+
duration : ((performance.now() - t0) / 1000).toFixed(2),
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
471
|
+
// File Tree Summary (top 2 levels)
|
|
472
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
473
|
+
|
|
474
|
+
async function printFileTreeSummary(dir, depth = 0, maxDepth = 2) {
|
|
475
|
+
if (depth === 0) {
|
|
476
|
+
console.log('');
|
|
477
|
+
console.log(chalk.dim(' ── 📁 Generated Project Structure ─────────────────────'));
|
|
478
|
+
}
|
|
349
479
|
try {
|
|
350
|
-
const
|
|
351
|
-
|
|
480
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
481
|
+
const filtered = entries.filter(e => e.name !== 'node_modules' && !e.name.startsWith('.'));
|
|
482
|
+
for (const entry of filtered.slice(0, 12)) {
|
|
483
|
+
const indent = ' ' + ' '.repeat(depth + 1);
|
|
484
|
+
const icon = entry.isDirectory() ? chalk.hex('#00F5FF')('📂') : chalk.gray('📄');
|
|
485
|
+
const name = entry.isDirectory() ? chalk.white.bold(entry.name) : chalk.dim(entry.name);
|
|
486
|
+
console.log(`${indent}${icon} ${name}`);
|
|
487
|
+
if (entry.isDirectory() && depth < maxDepth - 1) {
|
|
488
|
+
await printFileTreeSummary(path.join(dir, entry.name), depth + 1, maxDepth);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
if (filtered.length > 12 && depth === 0) {
|
|
492
|
+
console.log(chalk.gray(` ${' '.repeat(depth + 2)}... and ${filtered.length - 12} more`));
|
|
493
|
+
}
|
|
352
494
|
} catch {}
|
|
353
|
-
|
|
354
|
-
options._meta = { endpointCount: endpoints.length, filesGenerated: fileCount, duration: ((performance.now() - t0) / 1000).toFixed(2) };
|
|
495
|
+
if (depth === 0) console.log('');
|
|
355
496
|
}
|
|
356
497
|
|
|
357
498
|
async function globCount(dir) {
|
|
358
499
|
const { glob } = await import('glob');
|
|
359
|
-
const files = await glob(`${dir.replace(/\\/g, '/')}/**/*`, {
|
|
500
|
+
const files = await glob(`${dir.replace(/\\/g, '/')}/**/*`, {
|
|
501
|
+
nodir : true,
|
|
502
|
+
ignore: ['**/node_modules/**'],
|
|
503
|
+
});
|
|
360
504
|
return files.length;
|
|
361
505
|
}
|
|
362
506
|
|
|
@@ -373,14 +517,15 @@ async function dispatchGenerator(options) {
|
|
|
373
517
|
'java-spring' : () => generateJavaProject(options),
|
|
374
518
|
'python-fastapi' : () => generatePythonProject(options),
|
|
375
519
|
};
|
|
376
|
-
|
|
377
520
|
const runner = gen[options.stack];
|
|
378
|
-
if (!runner) throw new Error(
|
|
521
|
+
if (!runner) throw new Error(
|
|
522
|
+
`Stack '${options.stack}' is not supported. Valid options: ${Object.keys(gen).join(', ')}`
|
|
523
|
+
);
|
|
379
524
|
await runner();
|
|
380
525
|
}
|
|
381
526
|
|
|
382
527
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
383
|
-
// Pro AI Mode
|
|
528
|
+
// Pro AI Mode
|
|
384
529
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
385
530
|
|
|
386
531
|
async function callAIProcessor(astJsonData, apiKey, options) {
|
|
@@ -392,13 +537,19 @@ async function callAIProcessor(astJsonData, apiKey, options) {
|
|
|
392
537
|
console.log(chalk.gray(` → Input : ${astJsonData.length} endpoint(s) from AST`));
|
|
393
538
|
console.log('');
|
|
394
539
|
|
|
395
|
-
let thoughtCount
|
|
396
|
-
let
|
|
540
|
+
let thoughtCount = 0;
|
|
541
|
+
let warnCount = 0;
|
|
542
|
+
let currentSpinner = ora({
|
|
543
|
+
text : chalk.cyan('Initialising autonomous agents...'),
|
|
544
|
+
spinner: 'mindblown',
|
|
545
|
+
color : 'magenta',
|
|
546
|
+
}).start();
|
|
397
547
|
|
|
398
548
|
const onThought = (msg) => {
|
|
399
549
|
thoughtCount++;
|
|
400
550
|
if (msg.includes('FAILED') || msg.includes('WARNING')) {
|
|
401
|
-
|
|
551
|
+
warnCount++;
|
|
552
|
+
currentSpinner.warn(chalk.yellow(`[${thoughtCount}] ⚠️ ${msg}`));
|
|
402
553
|
currentSpinner = ora({ text: chalk.cyan('Recovering...'), spinner: 'mindblown', color: 'magenta' }).start();
|
|
403
554
|
} else {
|
|
404
555
|
currentSpinner.text = chalk.cyan(`[${thoughtCount}] ${msg}`);
|
|
@@ -409,29 +560,30 @@ async function callAIProcessor(astJsonData, apiKey, options) {
|
|
|
409
560
|
await aiAgent.init();
|
|
410
561
|
|
|
411
562
|
let existingPrisma = null;
|
|
412
|
-
const prismaPath
|
|
563
|
+
const prismaPath = path.join(options.projectDir, 'prisma', 'schema.prisma');
|
|
413
564
|
if (await fs.pathExists(prismaPath)) existingPrisma = await fs.readFile(prismaPath, 'utf8');
|
|
414
565
|
|
|
415
|
-
const pass1Data
|
|
416
|
-
const compTypes
|
|
417
|
-
const finalBlocks
|
|
418
|
-
const deployData
|
|
566
|
+
const pass1Data = await aiAgent.generateBackendBlocks(astJsonData, existingPrisma);
|
|
567
|
+
const compTypes = await extractComponentTreeTypes(options.frontendSrcDir);
|
|
568
|
+
const finalBlocks = await aiAgent.verifyDryRun(pass1Data, compTypes);
|
|
569
|
+
const deployData = await aiAgent.generateDeploymentConfig(options.stack, astJsonData);
|
|
419
570
|
|
|
420
571
|
await aiAgent.dispose();
|
|
421
|
-
currentSpinner.succeed(chalk.green(`Reasoning complete — ${thoughtCount}
|
|
572
|
+
currentSpinner.succeed(chalk.green(`Reasoning complete — ${thoughtCount} thought(s) · ${warnCount} warning(s)`));
|
|
422
573
|
|
|
423
574
|
return { ...finalBlocks, deployment: deployData };
|
|
424
575
|
}
|
|
425
576
|
|
|
426
577
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
427
|
-
// Health Dashboard
|
|
578
|
+
// Health Dashboard
|
|
428
579
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
429
580
|
|
|
430
581
|
function printHealthDashboard(blocks, options = {}) {
|
|
431
582
|
const secScore = blocks.aiSecurityConfig && blocks.aiSecurityConfig.length > 20 ? 98 : 75;
|
|
432
|
-
const archScore = blocks.aiDbRelations && blocks.aiDbRelations.length
|
|
583
|
+
const archScore = blocks.aiDbRelations && blocks.aiDbRelations.length > 20 ? 99 : 80;
|
|
433
584
|
const testScore = 85;
|
|
434
585
|
const depsScore = 92;
|
|
586
|
+
const overall = Math.round((secScore + archScore + testScore + depsScore) / 4);
|
|
435
587
|
|
|
436
588
|
const colorScore = (s) => {
|
|
437
589
|
if (s >= 95) return chalk.hex('#00FF9F').bold(`${s}% A+`);
|
|
@@ -439,12 +591,11 @@ function printHealthDashboard(blocks, options = {}) {
|
|
|
439
591
|
if (s >= 70) return chalk.yellow.bold(`${s}% B`);
|
|
440
592
|
return chalk.red.bold(`${s}% C`);
|
|
441
593
|
};
|
|
442
|
-
|
|
443
594
|
const bar = (s) => buildProgressBar(s, 16);
|
|
444
595
|
|
|
445
596
|
console.log('');
|
|
446
597
|
console.log(chalk.hex('#BF40FF').bold(' ╔══════════════════════════════════════════════════╗'));
|
|
447
|
-
console.log(chalk.hex('#BF40FF').bold(' ║
|
|
598
|
+
console.log(chalk.hex('#BF40FF').bold(' ║ 📊 SYSTEM HEALTH DASHBOARD v8.0-10X ║'));
|
|
448
599
|
console.log(chalk.hex('#BF40FF').bold(' ╚══════════════════════════════════════════════════╝'));
|
|
449
600
|
console.log('');
|
|
450
601
|
console.log(` 🛡️ Security Profile: [${bar(secScore)}] ${colorScore(secScore)}`);
|
|
@@ -452,7 +603,8 @@ function printHealthDashboard(blocks, options = {}) {
|
|
|
452
603
|
console.log(` 🧪 Test Coverage (Gen): [${bar(testScore)}] ${colorScore(testScore)}`);
|
|
453
604
|
console.log(` 📦 Dependency Health: [${bar(depsScore)}] ${colorScore(depsScore)}`);
|
|
454
605
|
console.log('');
|
|
455
|
-
|
|
606
|
+
console.log(` 🏆 Overall Score: [${bar(overall)}] ${colorScore(overall)}`);
|
|
607
|
+
console.log('');
|
|
456
608
|
if (options.stack) {
|
|
457
609
|
const meta = STACK_META[options.stack] || {};
|
|
458
610
|
console.log(chalk.dim(` Stack: ${meta.icon || ''} ${options.stack} (${meta.lang || ''} / ${meta.runtime || ''})`));
|
|
@@ -463,11 +615,10 @@ function printHealthDashboard(blocks, options = {}) {
|
|
|
463
615
|
}
|
|
464
616
|
|
|
465
617
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
466
|
-
// Post-generation Next Steps
|
|
618
|
+
// Post-generation Next Steps
|
|
467
619
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
468
620
|
|
|
469
621
|
function printNextSteps(projectName, stack, dbType) {
|
|
470
|
-
const meta = STACK_META[stack] || {};
|
|
471
622
|
const isNode = ['node-ts-express', 'js-express', 'nestjs'].includes(stack);
|
|
472
623
|
|
|
473
624
|
console.log(chalk.hex('#00F5FF').bold(' ╔══════════════════════════════════════════════════╗'));
|
|
@@ -495,12 +646,16 @@ function printNextSteps(projectName, stack, dbType) {
|
|
|
495
646
|
}
|
|
496
647
|
|
|
497
648
|
console.log('');
|
|
498
|
-
console.log(
|
|
649
|
+
console.log(
|
|
650
|
+
chalk.dim(' 💡 Tip: Run') + chalk.white(' npm run qa ') +
|
|
651
|
+
chalk.dim('to validate your generated backend.')
|
|
652
|
+
);
|
|
653
|
+
console.log(chalk.dim(' 📖 Docs: https://backlist.dev/docs'));
|
|
499
654
|
console.log('');
|
|
500
655
|
}
|
|
501
656
|
|
|
502
657
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
503
|
-
// Config Manager
|
|
658
|
+
// Config Manager
|
|
504
659
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
505
660
|
|
|
506
661
|
async function runConfigManager() {
|
|
@@ -508,13 +663,15 @@ async function runConfigManager() {
|
|
|
508
663
|
console.log(chalk.hex('#BF40FF').bold(' ── ⚙️ Configuration Manager ──────────────────────────'));
|
|
509
664
|
console.log('');
|
|
510
665
|
|
|
511
|
-
const exists
|
|
666
|
+
const exists = await fs.pathExists(CONFIG_PATH);
|
|
512
667
|
const sessExists = await fs.pathExists(SESSIONS_PATH);
|
|
513
668
|
|
|
514
669
|
if (exists) {
|
|
515
670
|
try {
|
|
516
|
-
const cfg
|
|
517
|
-
const masked = cfg.apiKey
|
|
671
|
+
const cfg = await fs.readJson(CONFIG_PATH);
|
|
672
|
+
const masked = cfg.apiKey
|
|
673
|
+
? cfg.apiKey.slice(0, 4) + '●'.repeat(12) + cfg.apiKey.slice(-4)
|
|
674
|
+
: 'none';
|
|
518
675
|
console.log(` ${chalk.dim('API Key:')} ${chalk.white(masked)}`);
|
|
519
676
|
console.log(` ${chalk.dim('Saved at:')} ${chalk.gray(cfg.savedAt || 'unknown')}`);
|
|
520
677
|
} catch {}
|
|
@@ -526,8 +683,10 @@ async function runConfigManager() {
|
|
|
526
683
|
try {
|
|
527
684
|
const sess = await fs.readJson(SESSIONS_PATH);
|
|
528
685
|
if (sess.last) {
|
|
529
|
-
console.log(` ${chalk.dim('Last
|
|
530
|
-
console.log(` ${chalk.dim('Last
|
|
686
|
+
console.log(` ${chalk.dim('Last project:')} ${chalk.white(sess.last.projectName || 'unknown')}`);
|
|
687
|
+
console.log(` ${chalk.dim('Last stack:')} ${chalk.white(sess.last.stack || 'none')}`);
|
|
688
|
+
console.log(` ${chalk.dim('Last mode:')} ${chalk.white(sess.last.generationMode || 'none')}`);
|
|
689
|
+
console.log(` ${chalk.dim('Saved at:')} ${chalk.gray(sess.last.savedAt?.slice(0, 16) || 'unknown')}`);
|
|
531
690
|
}
|
|
532
691
|
} catch {}
|
|
533
692
|
}
|
|
@@ -537,20 +696,21 @@ async function runConfigManager() {
|
|
|
537
696
|
const action = await p.select({
|
|
538
697
|
message: 'What would you like to do?',
|
|
539
698
|
options: [
|
|
540
|
-
{
|
|
541
|
-
|
|
542
|
-
label: '🚀 Post-Gen Validation',
|
|
543
|
-
hint: 'Validate a generated project right now'
|
|
544
|
-
},
|
|
545
|
-
{ value: 'clear-key', label: '🗑️ Clear saved API key' },
|
|
699
|
+
{ value: 'qa-post', label: '🚀 Post-Gen Validation', hint: 'Validate a generated project right now' },
|
|
700
|
+
{ value: 'clear-key', label: '🗑️ Clear saved API key' },
|
|
546
701
|
{ value: 'clear-sess', label: '🗑️ Clear session history' },
|
|
547
|
-
{ value: 'clear-all', label: '💥 Clear everything'
|
|
548
|
-
{ value: 'back', label: '← Back to main menu'
|
|
702
|
+
{ value: 'clear-all', label: '💥 Clear everything' },
|
|
703
|
+
{ value: 'back', label: '← Back to main menu' },
|
|
549
704
|
],
|
|
550
705
|
});
|
|
551
706
|
if (p.isCancel(action) || action === 'back') return;
|
|
552
707
|
|
|
553
|
-
if (action === '
|
|
708
|
+
if (action === 'qa-post') {
|
|
709
|
+
await initQASystem();
|
|
710
|
+
await autoRunPostGeneration();
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
if (action === 'clear-key' || action === 'clear-all') {
|
|
554
714
|
await fs.remove(CONFIG_PATH);
|
|
555
715
|
p.log.success('API key cleared.');
|
|
556
716
|
}
|
|
@@ -561,7 +721,7 @@ async function runConfigManager() {
|
|
|
561
721
|
}
|
|
562
722
|
|
|
563
723
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
564
|
-
// Plugin Manager
|
|
724
|
+
// Plugin Manager
|
|
565
725
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
566
726
|
|
|
567
727
|
async function runPluginManager(plugins) {
|
|
@@ -570,7 +730,7 @@ async function runPluginManager(plugins) {
|
|
|
570
730
|
console.log('');
|
|
571
731
|
|
|
572
732
|
if (plugins.length === 0) {
|
|
573
|
-
console.log(chalk.gray(
|
|
733
|
+
console.log(chalk.gray(' No plugins installed.'));
|
|
574
734
|
console.log(chalk.dim(` Drop plugin folders into: ${PLUGINS_DIR}`));
|
|
575
735
|
console.log(chalk.dim(' Each plugin needs index.js exporting { name, description, run }.'));
|
|
576
736
|
console.log('');
|
|
@@ -579,11 +739,11 @@ async function runPluginManager(plugins) {
|
|
|
579
739
|
|
|
580
740
|
const choice = await p.select({
|
|
581
741
|
message: 'Select a plugin to run:',
|
|
582
|
-
options: plugins.map(
|
|
742
|
+
options: plugins.map(pl => ({ value: pl.name, label: `🔌 ${pl.name}`, hint: pl.description || '' })),
|
|
583
743
|
});
|
|
584
744
|
if (p.isCancel(choice)) return;
|
|
585
745
|
|
|
586
|
-
const plugin = plugins.find(
|
|
746
|
+
const plugin = plugins.find(pl => pl.name === choice);
|
|
587
747
|
if (plugin) {
|
|
588
748
|
try {
|
|
589
749
|
await plugin.run({ chalk, ora, p, fs, path });
|
|
@@ -599,55 +759,76 @@ async function runPluginManager(plugins) {
|
|
|
599
759
|
|
|
600
760
|
async function promptRepeatLast(lastSession) {
|
|
601
761
|
if (!lastSession) return false;
|
|
602
|
-
|
|
603
762
|
const meta = STACK_META[lastSession.stack] || {};
|
|
604
763
|
const icon = meta.icon || '⚙️';
|
|
605
|
-
|
|
606
|
-
|
|
764
|
+
console.log(chalk.dim(
|
|
765
|
+
` ⚡ Last session: ${icon} ${lastSession.stack} · ` +
|
|
766
|
+
`${lastSession.generationMode} mode · ${lastSession.savedAt?.slice(0, 10) || ''}`
|
|
767
|
+
));
|
|
607
768
|
console.log('');
|
|
608
|
-
|
|
609
769
|
const repeat = await p.confirm({
|
|
610
|
-
message: chalk.hex('#00F5FF')('Repeat last generation with same settings?'),
|
|
770
|
+
message : chalk.hex('#00F5FF')('Repeat last generation with same settings?'),
|
|
611
771
|
initialValue: false,
|
|
612
772
|
});
|
|
613
|
-
|
|
614
773
|
return !p.isCancel(repeat) && repeat;
|
|
615
774
|
}
|
|
616
775
|
|
|
617
776
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
618
|
-
//
|
|
777
|
+
// Input Validation Helpers
|
|
778
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
779
|
+
|
|
780
|
+
function validateProjectName(v) {
|
|
781
|
+
if (!v || !v.trim()) return '❌ Cannot be empty.';
|
|
782
|
+
if (/[^a-zA-Z0-9_\-.]/.test(v)) return '❌ Use only letters, numbers, hyphens, underscores, dots.';
|
|
783
|
+
if (v.length > 64) return '❌ Name too long (max 64 chars).';
|
|
784
|
+
return undefined;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
function validateSrcPath(v) {
|
|
788
|
+
if (!v || !v.trim()) return '❌ Cannot be empty.';
|
|
789
|
+
return undefined;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
function validateUrl(v) {
|
|
793
|
+
if (!v || !v.trim()) return undefined; // optional
|
|
794
|
+
try { new URL(v.trim()); return undefined; }
|
|
795
|
+
catch { return '❌ Invalid URL — e.g. http://localhost:3000'; }
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
799
|
+
// Main CLI Flow — v8.0-10X + QA v10.0
|
|
619
800
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
620
801
|
|
|
621
802
|
async function main() {
|
|
622
803
|
const globalStart = performance.now();
|
|
623
804
|
printBanner();
|
|
624
805
|
|
|
625
|
-
//
|
|
806
|
+
// Load plugins & last session in parallel
|
|
626
807
|
const [plugins, lastSession] = await Promise.all([loadPlugins(), loadLastSession()]);
|
|
627
808
|
|
|
628
809
|
if (plugins.length > 0) {
|
|
629
|
-
p.log.info(chalk.dim(`${plugins.length} plugin(s) loaded: ${plugins.map(
|
|
810
|
+
p.log.info(chalk.dim(`${plugins.length} plugin(s) loaded: ${plugins.map(pl => pl.name).join(', ')}`));
|
|
630
811
|
}
|
|
631
812
|
|
|
632
|
-
p.intro(chalk.hex('#00F5FF').bold(' create-backlist v8.0 — Polyglot Backend Generator '));
|
|
813
|
+
p.intro(chalk.hex('#00F5FF').bold(' create-backlist v8.0-10X — Polyglot Backend Generator '));
|
|
633
814
|
|
|
634
|
-
// ── Smart repeat shortcut
|
|
815
|
+
// ── Smart repeat shortcut ──────────────────────────────────────────────
|
|
635
816
|
if (lastSession?.stack) {
|
|
636
817
|
const repeated = await promptRepeatLast(lastSession);
|
|
637
818
|
if (repeated) {
|
|
638
|
-
// Re-run with cached settings but ask for project name & src
|
|
639
819
|
const projectName = await p.text({
|
|
640
|
-
message: 'Backend directory name:',
|
|
641
|
-
placeholder: 'backend',
|
|
820
|
+
message : 'Backend directory name:',
|
|
821
|
+
placeholder : 'backend',
|
|
642
822
|
defaultValue: 'backend',
|
|
643
|
-
validate:
|
|
823
|
+
validate : validateProjectName,
|
|
644
824
|
});
|
|
645
825
|
if (p.isCancel(projectName)) { p.cancel('Cancelled.'); process.exit(0); }
|
|
646
826
|
|
|
647
827
|
const srcPath = await p.text({
|
|
648
|
-
message: 'Path to frontend `src` directory:',
|
|
649
|
-
placeholder: 'src',
|
|
828
|
+
message : 'Path to frontend `src` directory:',
|
|
829
|
+
placeholder : 'src',
|
|
650
830
|
defaultValue: 'src',
|
|
831
|
+
validate : validateSrcPath,
|
|
651
832
|
});
|
|
652
833
|
if (p.isCancel(srcPath)) { p.cancel('Cancelled.'); process.exit(0); }
|
|
653
834
|
|
|
@@ -656,40 +837,73 @@ async function main() {
|
|
|
656
837
|
projectName,
|
|
657
838
|
stack : lastSession.stack,
|
|
658
839
|
srcPath,
|
|
659
|
-
dbType : lastSession.dbType
|
|
660
|
-
addAuth : true,
|
|
661
|
-
addSeeder : true,
|
|
662
|
-
extraFeatures : ['docker', 'testing', 'swagger'],
|
|
840
|
+
dbType : lastSession.dbType || 'mongoose',
|
|
841
|
+
addAuth : lastSession.addAuth ?? true,
|
|
842
|
+
addSeeder : lastSession.addSeeder ?? true,
|
|
843
|
+
extraFeatures : lastSession.extraFeatures ?? ['docker', 'testing', 'swagger'],
|
|
663
844
|
projectDir : path.resolve(process.cwd(), projectName),
|
|
664
845
|
frontendSrcDir : path.resolve(process.cwd(), srcPath),
|
|
665
846
|
};
|
|
666
847
|
|
|
848
|
+
printSessionDiff(lastSession, options);
|
|
667
849
|
await executeGeneration(options, globalStart, plugins);
|
|
668
850
|
return;
|
|
669
851
|
}
|
|
670
852
|
}
|
|
671
853
|
|
|
672
|
-
// ── Main Mode Selection
|
|
854
|
+
// ── Main Mode Selection ────────────────────────────────────────────────
|
|
673
855
|
const mode = await p.select({
|
|
674
856
|
message: 'Select your mode:',
|
|
675
857
|
options: [
|
|
676
|
-
{ value: 'free',
|
|
677
|
-
{ value: 'pro',
|
|
678
|
-
{ value: 'qa-
|
|
679
|
-
{ value: 'qa-
|
|
680
|
-
{ value: 'qa-
|
|
681
|
-
{ value: 'qa-
|
|
682
|
-
{ value: 'qa-
|
|
683
|
-
{ value: '
|
|
684
|
-
|
|
858
|
+
{ value: 'free', label: '🚀 Standard Mode', hint: 'Free — AST + EJS + DOM Check' },
|
|
859
|
+
{ value: 'pro', label: '🧠 Pro AI Mode', hint: 'Llama-4 · Schema & Auth generation' },
|
|
860
|
+
{ value: 'qa-url', label: '🌐 URL-Based QA Scan', hint: 'Probe localhost + production — HTTP/security/SEO' },
|
|
861
|
+
{ value: 'qa-manual', label: '🧪 Manual QA Testing', hint: 'Interactive test cases + bug reports' },
|
|
862
|
+
{ value: 'qa-auto', label: '🤖 Automated QA', hint: '15-test suite · pass/fail report' },
|
|
863
|
+
{ value: 'qa-live', label: '⚡ Live Continuous QA', hint: 'Auto-runs every 30s · Ctrl+C to stop' },
|
|
864
|
+
{ value: 'qa-post', label: '🔬 Post-Gen Validation', hint: 'Validate a generated project right now' },
|
|
865
|
+
{ value: 'qa-history', label: '📜 QA History', hint: 'View past QA sessions' },
|
|
866
|
+
{ value: 'config', label: '⚙️ Config Manager', hint: 'Manage API keys & sessions' },
|
|
867
|
+
...(plugins.length > 0
|
|
868
|
+
? [{ value: 'plugins', label: '🔌 Plugins', hint: `${plugins.length} installed` }]
|
|
869
|
+
: []
|
|
870
|
+
),
|
|
685
871
|
],
|
|
686
872
|
});
|
|
687
873
|
if (p.isCancel(mode)) { p.cancel('Cancelled.'); process.exit(0); }
|
|
688
874
|
|
|
689
|
-
// ── Non-generation routes
|
|
875
|
+
// ── Non-generation routes ──────────────────────────────────────────────
|
|
876
|
+
|
|
877
|
+
// v10.0 — URL-Based QA
|
|
878
|
+
if (mode === 'qa-url') {
|
|
879
|
+
await initQASystem();
|
|
880
|
+
|
|
881
|
+
const localUrl = await p.text({
|
|
882
|
+
message : 'Localhost URL:',
|
|
883
|
+
placeholder: 'http://localhost:3000',
|
|
884
|
+
validate : validateUrl,
|
|
885
|
+
});
|
|
886
|
+
if (p.isCancel(localUrl)) { p.cancel('Cancelled.'); return; }
|
|
887
|
+
|
|
888
|
+
const prodUrl = await p.text({
|
|
889
|
+
message : 'Production URL (leave blank to skip):',
|
|
890
|
+
placeholder: 'https://yoursite.com',
|
|
891
|
+
validate : validateUrl,
|
|
892
|
+
});
|
|
893
|
+
if (p.isCancel(prodUrl)) { p.cancel('Cancelled.'); return; }
|
|
894
|
+
|
|
895
|
+
await runUrlQA({
|
|
896
|
+
localUrl: String(localUrl).trim() || undefined,
|
|
897
|
+
prodUrl : String(prodUrl).trim() || undefined,
|
|
898
|
+
});
|
|
899
|
+
|
|
900
|
+
p.outro(chalk.hex('#00F5FF').bold('URL QA scan complete.'));
|
|
901
|
+
return;
|
|
902
|
+
}
|
|
903
|
+
|
|
690
904
|
if (mode === 'qa-manual') { await initQASystem(); await runManualQA(); return; }
|
|
691
905
|
if (mode === 'qa-auto') { await initQASystem(); await runAutomatedQA({ continuous: false }); return; }
|
|
692
|
-
if (mode === 'qa-live') { await initQASystem(); await runAutomatedQA({ continuous: true
|
|
906
|
+
if (mode === 'qa-live') { await initQASystem(); await runAutomatedQA({ continuous: true }); return; }
|
|
693
907
|
if (mode === 'qa-post') { await initQASystem(); await autoRunPostGeneration(); return; }
|
|
694
908
|
if (mode === 'qa-history') { await viewQAHistory(); p.outro(chalk.hex('#00F5FF').bold('Done.')); return; }
|
|
695
909
|
if (mode === 'config') { await runConfigManager(); p.outro(chalk.gray('Config updated.')); return; }
|
|
@@ -697,49 +911,63 @@ async function main() {
|
|
|
697
911
|
|
|
698
912
|
const generationMode = mode; // 'free' | 'pro'
|
|
699
913
|
|
|
700
|
-
// ── Project Name
|
|
914
|
+
// ── Project Name ───────────────────────────────────────────────────────
|
|
701
915
|
const projectName = await p.text({
|
|
702
|
-
message
|
|
703
|
-
placeholder: 'backend',
|
|
916
|
+
message : 'Backend directory name:',
|
|
917
|
+
placeholder : 'backend',
|
|
704
918
|
defaultValue: 'backend',
|
|
705
|
-
validate
|
|
919
|
+
validate : validateProjectName,
|
|
706
920
|
});
|
|
707
921
|
if (p.isCancel(projectName)) { p.cancel('Cancelled.'); process.exit(0); }
|
|
708
922
|
|
|
709
|
-
//
|
|
923
|
+
// Warn if directory already exists
|
|
924
|
+
const targetDir = path.resolve(process.cwd(), projectName);
|
|
925
|
+
if (await fs.pathExists(targetDir)) {
|
|
926
|
+
p.log.warn(chalk.yellow(`⚠️ Directory '${projectName}' already exists — files may be overwritten.`));
|
|
927
|
+
const cont = await p.confirm({ message: 'Continue anyway?', initialValue: false });
|
|
928
|
+
if (p.isCancel(cont) || !cont) { p.cancel('Aborted.'); process.exit(0); }
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
// ── Stack Selection ────────────────────────────────────────────────────
|
|
710
932
|
const stack = await p.select({
|
|
711
933
|
message: 'Select backend stack:',
|
|
712
934
|
options: [
|
|
713
|
-
{ value: 'node-ts-express', label: '🔷 Node.js TypeScript + Express',
|
|
714
|
-
{ value: 'js-express', label: '🟨 Node.js JavaScript ESM + Express', hint: 'Lightweight, no TS'
|
|
715
|
-
{ value: 'nestjs', label: '🔴 NestJS (TypeScript)',
|
|
716
|
-
{ value: 'dotnet-webapi', label: '🟣 C# ASP.NET Core Web API' },
|
|
717
|
-
{ value: 'java-spring', label: '🍃 Java Spring Boot' },
|
|
718
|
-
{ value: 'python-fastapi', label: '🐍 Python FastAPI' },
|
|
935
|
+
{ value: 'node-ts-express', label: '🔷 Node.js TypeScript + Express', hint: 'Hexagonal Architecture' },
|
|
936
|
+
{ value: 'js-express', label: '🟨 Node.js JavaScript ESM + Express', hint: 'Lightweight, no TS' },
|
|
937
|
+
{ value: 'nestjs', label: '🔴 NestJS (TypeScript)', hint: 'Modular, enterprise-grade' },
|
|
938
|
+
{ value: 'dotnet-webapi', label: '🟣 C# ASP.NET Core Web API', hint: 'Requires .NET SDK' },
|
|
939
|
+
{ value: 'java-spring', label: '🍃 Java Spring Boot', hint: 'Requires Java JDK' },
|
|
940
|
+
{ value: 'python-fastapi', label: '🐍 Python FastAPI', hint: 'Requires Python 3' },
|
|
719
941
|
],
|
|
720
942
|
});
|
|
721
943
|
if (p.isCancel(stack)) { p.cancel('Cancelled.'); process.exit(0); }
|
|
722
944
|
|
|
723
|
-
// ── Pre-flight
|
|
945
|
+
// ── Pre-flight ─────────────────────────────────────────────────────────
|
|
724
946
|
const failedChecks = await runPreflightChecks(stack);
|
|
725
947
|
if (failedChecks.length > 0) {
|
|
726
|
-
p.log.warn(chalk.yellow(`${failedChecks.length} pre-flight check(s) failed
|
|
948
|
+
p.log.warn(chalk.yellow(`${failedChecks.length} pre-flight check(s) failed.`));
|
|
727
949
|
const cont = await p.confirm({ message: 'Proceed despite warnings?', initialValue: false });
|
|
728
950
|
if (p.isCancel(cont) || !cont) { p.cancel('Aborted.'); process.exit(0); }
|
|
729
951
|
}
|
|
730
952
|
|
|
731
|
-
// ── Frontend Source Path
|
|
953
|
+
// ── Frontend Source Path ───────────────────────────────────────────────
|
|
732
954
|
const srcPath = await p.text({
|
|
733
955
|
message : 'Path to frontend `src` directory:',
|
|
734
956
|
placeholder : 'src',
|
|
735
957
|
defaultValue: 'src',
|
|
958
|
+
validate : validateSrcPath,
|
|
736
959
|
});
|
|
737
960
|
if (p.isCancel(srcPath)) { p.cancel('Cancelled.'); process.exit(0); }
|
|
738
961
|
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
962
|
+
const resolvedSrc = path.resolve(process.cwd(), srcPath);
|
|
963
|
+
if (!await fs.pathExists(resolvedSrc)) {
|
|
964
|
+
p.log.warn(chalk.yellow(`⚠️ Directory '${srcPath}' not found — AST scan may return 0 endpoints.`));
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
// ── Node-specific options ──────────────────────────────────────────────
|
|
968
|
+
let dbType = 'mongoose';
|
|
969
|
+
let addAuth = true;
|
|
970
|
+
let addSeeder = true;
|
|
743
971
|
let extraFeatures = ['docker', 'testing', 'swagger'];
|
|
744
972
|
|
|
745
973
|
const isNodeStack = ['node-ts-express', 'js-express', 'nestjs'].includes(stack);
|
|
@@ -748,7 +976,7 @@ async function main() {
|
|
|
748
976
|
dbType = await p.select({
|
|
749
977
|
message: 'Database type:',
|
|
750
978
|
options: [
|
|
751
|
-
{ value: 'mongoose', label: '🍃 NoSQL — MongoDB + Mongoose'
|
|
979
|
+
{ value: 'mongoose', label: '🍃 NoSQL — MongoDB + Mongoose' },
|
|
752
980
|
{ value: 'prisma', label: '🔺 SQL — PostgreSQL/MySQL + Prisma' },
|
|
753
981
|
],
|
|
754
982
|
});
|
|
@@ -763,17 +991,17 @@ async function main() {
|
|
|
763
991
|
extraFeatures = await p.multiselect({
|
|
764
992
|
message: 'Additional features:',
|
|
765
993
|
options: [
|
|
766
|
-
{ value: 'docker', label: '🐳 Docker Support',
|
|
767
|
-
{ value: 'testing', label: '🧪 API Testing Boilerplate'
|
|
768
|
-
{ value: 'swagger', label: '📖 Swagger UI (API Docs)'
|
|
769
|
-
{ value: 'ci', label: '⚙️ GitHub Actions CI',
|
|
994
|
+
{ value: 'docker', label: '🐳 Docker Support', hint: 'Dockerfile + docker-compose.yml' },
|
|
995
|
+
{ value: 'testing', label: '🧪 API Testing Boilerplate' },
|
|
996
|
+
{ value: 'swagger', label: '📖 Swagger UI (API Docs)' },
|
|
997
|
+
{ value: 'ci', label: '⚙️ GitHub Actions CI', hint: 'Auto-deploy workflow' },
|
|
770
998
|
],
|
|
771
999
|
initialValues: ['docker', 'testing', 'swagger'],
|
|
772
1000
|
});
|
|
773
1001
|
if (p.isCancel(extraFeatures)) { p.cancel('Cancelled.'); process.exit(0); }
|
|
774
1002
|
}
|
|
775
1003
|
|
|
776
|
-
// ── Generation Plan summary
|
|
1004
|
+
// ── Generation Plan summary ────────────────────────────────────────────
|
|
777
1005
|
const meta = STACK_META[stack] || {};
|
|
778
1006
|
console.log('');
|
|
779
1007
|
console.log(chalk.hex('#BF40FF').bold(' ── 📋 Generation Plan ────────────────────────────────'));
|
|
@@ -781,19 +1009,23 @@ async function main() {
|
|
|
781
1009
|
console.log(` ${chalk.dim('Project:')} ${chalk.white.bold(projectName)}`);
|
|
782
1010
|
console.log(` ${chalk.dim('Stack:')} ${meta.icon || ''} ${chalk.white(stack)}`);
|
|
783
1011
|
console.log(` ${chalk.dim('Language:')} ${chalk.white(meta.lang || 'N/A')}`);
|
|
1012
|
+
console.log(` ${chalk.dim('Runtime:')} ${chalk.white(meta.runtime || 'N/A')}`);
|
|
784
1013
|
console.log(` ${chalk.dim('Mode:')} ${generationMode === 'pro' ? chalk.hex('#BF40FF').bold('PRO AI ✦') : chalk.hex('#00F5FF').bold('Standard ◉')}`);
|
|
785
1014
|
if (isNodeStack) {
|
|
786
1015
|
console.log(` ${chalk.dim('Database:')} ${chalk.white(dbType)}`);
|
|
787
|
-
console.log(` ${chalk.dim('Auth JWT:')} ${addAuth
|
|
788
|
-
console.log(` ${chalk.dim('Seeder:')} ${addSeeder
|
|
1016
|
+
console.log(` ${chalk.dim('Auth JWT:')} ${addAuth ? chalk.green('Yes') : chalk.red('No')}`);
|
|
1017
|
+
console.log(` ${chalk.dim('Seeder:')} ${addSeeder ? chalk.green('Yes') : chalk.red('No')}`);
|
|
789
1018
|
console.log(` ${chalk.dim('Extras:')} ${chalk.white(extraFeatures.join(', ') || 'none')}`);
|
|
790
1019
|
}
|
|
1020
|
+
console.log(` ${chalk.dim('Output:')} ${chalk.gray(targetDir)}`);
|
|
791
1021
|
console.log('');
|
|
792
1022
|
|
|
1023
|
+
printSessionDiff(lastSession, { stack, dbType, generationMode });
|
|
1024
|
+
|
|
793
1025
|
const proceed = await p.confirm({ message: 'Proceed with generation?', initialValue: true });
|
|
794
1026
|
if (p.isCancel(proceed) || !proceed) { p.cancel('Aborted.'); process.exit(0); }
|
|
795
1027
|
|
|
796
|
-
// ── Build options
|
|
1028
|
+
// ── Build options & execute ────────────────────────────────────────────
|
|
797
1029
|
const options = {
|
|
798
1030
|
generationMode,
|
|
799
1031
|
projectName,
|
|
@@ -803,26 +1035,31 @@ async function main() {
|
|
|
803
1035
|
addAuth,
|
|
804
1036
|
addSeeder,
|
|
805
1037
|
extraFeatures,
|
|
806
|
-
projectDir :
|
|
807
|
-
frontendSrcDir:
|
|
1038
|
+
projectDir : targetDir,
|
|
1039
|
+
frontendSrcDir: resolvedSrc,
|
|
808
1040
|
};
|
|
809
1041
|
|
|
810
1042
|
await executeGeneration(options, globalStart, plugins);
|
|
811
1043
|
}
|
|
812
1044
|
|
|
813
1045
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
814
|
-
// Generation Executor —
|
|
1046
|
+
// Generation Executor — with auto-retry
|
|
815
1047
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
816
1048
|
|
|
817
|
-
async function executeGeneration(options, globalStart, plugins = []) {
|
|
1049
|
+
async function executeGeneration(options, globalStart, plugins = [], _attempt = 1) {
|
|
818
1050
|
const startTime = Date.now();
|
|
1051
|
+
_cleanupDir = options.projectDir;
|
|
819
1052
|
|
|
820
1053
|
try {
|
|
821
|
-
// ── PRO MODE
|
|
1054
|
+
// ── PRO MODE ──────────────────────────────────────────────────────────
|
|
822
1055
|
if (options.generationMode === 'pro') {
|
|
823
1056
|
const apiKey = await getProApiKey();
|
|
824
1057
|
|
|
825
|
-
const spinnerParse = ora({
|
|
1058
|
+
const spinnerParse = ora({
|
|
1059
|
+
text : chalk.white('Parsing frontend with Babel AST...'),
|
|
1060
|
+
spinner: 'dots12',
|
|
1061
|
+
color : 'cyan',
|
|
1062
|
+
}).start();
|
|
826
1063
|
let astJsonData = [];
|
|
827
1064
|
try {
|
|
828
1065
|
astJsonData = await analyzeFrontend(options.frontendSrcDir);
|
|
@@ -832,9 +1069,13 @@ async function executeGeneration(options, globalStart, plugins = []) {
|
|
|
832
1069
|
}
|
|
833
1070
|
|
|
834
1071
|
const generatedBlocks = await callAIProcessor(astJsonData, apiKey, options);
|
|
835
|
-
options.aiBlocks
|
|
1072
|
+
options.aiBlocks = generatedBlocks;
|
|
836
1073
|
|
|
837
|
-
const spinnerGen = ora({
|
|
1074
|
+
const spinnerGen = ora({
|
|
1075
|
+
text : chalk.white('Writing hexagonal output...'),
|
|
1076
|
+
spinner: 'material',
|
|
1077
|
+
color : 'magenta',
|
|
1078
|
+
}).start();
|
|
838
1079
|
try {
|
|
839
1080
|
await dispatchGenerator(options);
|
|
840
1081
|
spinnerGen.succeed(chalk.green('Hexagonal auto-write complete.'));
|
|
@@ -845,57 +1086,92 @@ async function executeGeneration(options, globalStart, plugins = []) {
|
|
|
845
1086
|
|
|
846
1087
|
if (generatedBlocks.deployment) {
|
|
847
1088
|
await fs.ensureDir(path.join(options.projectDir, '.github', 'workflows'));
|
|
848
|
-
await fs.writeFile(
|
|
849
|
-
|
|
1089
|
+
await fs.writeFile(
|
|
1090
|
+
path.join(options.projectDir, 'docker-compose.yml'),
|
|
1091
|
+
generatedBlocks.deployment.dockerCompose
|
|
1092
|
+
);
|
|
1093
|
+
await fs.writeFile(
|
|
1094
|
+
path.join(options.projectDir, '.github', 'workflows', 'deploy.yml'),
|
|
1095
|
+
generatedBlocks.deployment.githubWorkflow
|
|
1096
|
+
);
|
|
850
1097
|
}
|
|
851
1098
|
|
|
852
1099
|
printHealthDashboard(generatedBlocks, options);
|
|
853
|
-
printTokenUsage('pro', startTime, {
|
|
1100
|
+
printTokenUsage('pro', startTime, {
|
|
1101
|
+
endpointCount: options.aiBlocks ? Object.keys(options.aiBlocks).length : 0,
|
|
1102
|
+
});
|
|
854
1103
|
|
|
855
1104
|
} else {
|
|
856
|
-
// ── FREE MODE
|
|
1105
|
+
// ── FREE MODE ────────────────────────────────────────────────────────
|
|
857
1106
|
await runFreeModePipeline(options);
|
|
858
|
-
printTokenUsage('free', startTime, options._meta || {});
|
|
1107
|
+
printTokenUsage('free', startTime, { ...(options._meta || {}), retries: _attempt - 1 });
|
|
859
1108
|
}
|
|
860
1109
|
|
|
861
|
-
// ── Post-gen
|
|
1110
|
+
// ── Post-gen plugins ──────────────────────────────────────────────────
|
|
862
1111
|
for (const plugin of plugins) {
|
|
863
1112
|
if (plugin.runAfterGenerate) {
|
|
864
|
-
try {
|
|
865
|
-
await plugin.runAfterGenerate(options);
|
|
866
|
-
} catch {}
|
|
1113
|
+
try { await plugin.runAfterGenerate(options); } catch {}
|
|
867
1114
|
}
|
|
868
1115
|
}
|
|
869
1116
|
|
|
870
|
-
// ── Save session for "Repeat Last" ───────────────────────────────
|
|
871
1117
|
await saveSession(options);
|
|
872
|
-
|
|
873
|
-
// ── Next Steps ───────────────────────────────────────────────────
|
|
874
1118
|
printNextSteps(options.projectName, options.stack, options.dbType);
|
|
875
1119
|
|
|
1120
|
+
_cleanupDir = null;
|
|
1121
|
+
|
|
876
1122
|
const totalTime = ((performance.now() - globalStart) / 1000).toFixed(2);
|
|
877
1123
|
p.outro(
|
|
878
|
-
(options.generationMode === 'pro'
|
|
879
|
-
|
|
880
|
-
|
|
1124
|
+
(options.generationMode === 'pro'
|
|
1125
|
+
? chalk.hex('#BF40FF')
|
|
1126
|
+
: chalk.hex('#00F5FF')
|
|
1127
|
+
).bold(`✓ Done in ${totalTime}s — cd ${options.projectName}`)
|
|
881
1128
|
);
|
|
882
1129
|
|
|
883
1130
|
} catch (error) {
|
|
884
1131
|
console.log('');
|
|
885
|
-
|
|
886
|
-
|
|
1132
|
+
|
|
1133
|
+
const cleanStack = (error.stack ?? '')
|
|
1134
|
+
.split('\n')
|
|
1135
|
+
.filter(l => !l.includes('node_modules') && !l.includes('node:internal'))
|
|
1136
|
+
.slice(0, 6)
|
|
1137
|
+
.join('\n');
|
|
1138
|
+
|
|
1139
|
+
p.log.error(chalk.red.bold(
|
|
1140
|
+
`Generation failed (attempt ${_attempt}/${MAX_RETRIES}): ${error.message || error}`
|
|
1141
|
+
));
|
|
1142
|
+
if (cleanStack) console.log(chalk.gray(cleanStack));
|
|
1143
|
+
|
|
1144
|
+
if (_attempt < MAX_RETRIES) {
|
|
1145
|
+
const delay = _attempt * 2000;
|
|
1146
|
+
console.log('');
|
|
1147
|
+
p.log.warn(chalk.yellow(`⏳ Retrying in ${delay / 1000}s...`));
|
|
1148
|
+
await new Promise(r => setTimeout(r, delay));
|
|
1149
|
+
|
|
1150
|
+
if (options.projectDir && await fs.pathExists(options.projectDir)) {
|
|
1151
|
+
await fs.remove(options.projectDir).catch(() => {});
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
return executeGeneration(options, globalStart, plugins, _attempt + 1);
|
|
1155
|
+
}
|
|
887
1156
|
|
|
888
1157
|
if (options.projectDir && await fs.pathExists(options.projectDir)) {
|
|
889
|
-
const sc = ora({
|
|
890
|
-
|
|
1158
|
+
const sc = ora({
|
|
1159
|
+
text : chalk.yellow('Cleaning up partial output...'),
|
|
1160
|
+
spinner: 'line',
|
|
1161
|
+
color : 'yellow',
|
|
1162
|
+
}).start();
|
|
1163
|
+
await fs.remove(options.projectDir).catch(() => {});
|
|
891
1164
|
sc.succeed(chalk.yellow('Cleanup done.'));
|
|
892
1165
|
}
|
|
1166
|
+
|
|
1167
|
+
_cleanupDir = null;
|
|
893
1168
|
process.exit(1);
|
|
894
1169
|
}
|
|
895
1170
|
}
|
|
896
1171
|
|
|
897
1172
|
// ── Launch ────────────────────────────────────────────────────────────────
|
|
898
|
-
main().catch(
|
|
1173
|
+
main().catch(err => {
|
|
899
1174
|
console.error(chalk.red.bold(`\n Fatal: ${err.message || err}`));
|
|
1175
|
+
if (err.stack) console.error(chalk.gray(err.stack.split('\n').slice(1, 5).join('\n')));
|
|
900
1176
|
process.exit(1);
|
|
901
|
-
});
|
|
1177
|
+
});
|