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