create-backlist 10.0.1 → 10.0.3
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 +247 -156
- package/package.json +1 -1
- package/src/qa/qa-engine.js +2429 -787
package/bin/index.js
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
// ✦ Rich error diagnostics with stack-trace filtering
|
|
16
16
|
// ✦ Session diff — shows what changed since last run
|
|
17
17
|
// ✦ Post-gen file-tree summary
|
|
18
|
+
// ✦ QA Engine v10.0 — URL QA, HTTP probe, security, SEO, a11y
|
|
18
19
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
19
20
|
|
|
20
21
|
import * as p from '@clack/prompts';
|
|
@@ -30,7 +31,7 @@ import { performance } from 'node:perf_hooks';
|
|
|
30
31
|
const __filename = fileURLToPath(import.meta.url);
|
|
31
32
|
const __dirname = path.dirname(__filename);
|
|
32
33
|
|
|
33
|
-
// ── CLI boot timestamp
|
|
34
|
+
// ── CLI boot timestamp ────────────────────────────────────────────────────
|
|
34
35
|
const BOOT_START = performance.now();
|
|
35
36
|
|
|
36
37
|
// ── Internal Modules ─────────────────────────────────────────────────────
|
|
@@ -47,20 +48,25 @@ import { generateDotnetProject } from '../src/generators/dotnet.js';
|
|
|
47
48
|
import { generateJavaProject } from '../src/generators/java.js';
|
|
48
49
|
import { generatePythonProject } from '../src/generators/python.js';
|
|
49
50
|
|
|
50
|
-
// ── QA System
|
|
51
|
-
import {
|
|
52
|
-
|
|
53
|
-
|
|
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';
|
|
54
60
|
|
|
55
61
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
56
62
|
// Constants & Paths
|
|
57
63
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
58
64
|
|
|
59
|
-
const CONFIG_PATH
|
|
60
|
-
const SESSIONS_PATH
|
|
61
|
-
const PLUGINS_DIR
|
|
62
|
-
const VERSION
|
|
63
|
-
const MAX_RETRIES
|
|
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;
|
|
64
70
|
|
|
65
71
|
// ── Pricing Table ─────────────────────────────────────────────────────────
|
|
66
72
|
const PRICING = {
|
|
@@ -87,17 +93,26 @@ const PREFLIGHT_HINTS = {
|
|
|
87
93
|
'Python 3 installed' : 'Download from https://python.org or use `winget install Python.Python.3`',
|
|
88
94
|
};
|
|
89
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
|
+
|
|
90
101
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
91
|
-
//
|
|
102
|
+
// Graceful Shutdown Handler
|
|
92
103
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
93
104
|
|
|
94
|
-
let _cleanupDir = null;
|
|
105
|
+
let _cleanupDir = null;
|
|
95
106
|
|
|
96
107
|
async function gracefulShutdown(signal) {
|
|
97
108
|
console.log('');
|
|
98
109
|
console.log(chalk.yellow(`\n ⚠️ ${signal} received — shutting down gracefully...`));
|
|
99
110
|
if (_cleanupDir && await fs.pathExists(_cleanupDir)) {
|
|
100
|
-
const sc = ora({
|
|
111
|
+
const sc = ora({
|
|
112
|
+
text : chalk.yellow('Cleaning up partial output...'),
|
|
113
|
+
spinner: 'line',
|
|
114
|
+
color : 'yellow',
|
|
115
|
+
}).start();
|
|
101
116
|
await fs.remove(_cleanupDir).catch(() => {});
|
|
102
117
|
sc.succeed(chalk.yellow('Partial output removed.'));
|
|
103
118
|
}
|
|
@@ -131,11 +146,9 @@ function printBanner() {
|
|
|
131
146
|
console.log(c1(' ╚══════════════════════════════════════════════════════════════╝'));
|
|
132
147
|
console.log('');
|
|
133
148
|
|
|
134
|
-
// 10X: Boot time + live system status bar
|
|
135
149
|
const bootMs = (performance.now() - BOOT_START).toFixed(0);
|
|
136
150
|
const mem = process.memoryUsage();
|
|
137
151
|
const heapMB = (mem.heapUsed / 1024 / 1024).toFixed(0);
|
|
138
|
-
const rssMB = (mem.rss / 1024 / 1024).toFixed(0);
|
|
139
152
|
const uptime = process.uptime().toFixed(1);
|
|
140
153
|
const nodeVer = process.version;
|
|
141
154
|
const platform = process.platform;
|
|
@@ -144,24 +157,19 @@ function printBanner() {
|
|
|
144
157
|
console.log(
|
|
145
158
|
dim(' ') +
|
|
146
159
|
c4('◉ LIVE') + dim(' │ ') +
|
|
147
|
-
dim('Boot ')
|
|
148
|
-
dim('Node ')
|
|
149
|
-
dim('Heap ')
|
|
150
|
-
dim('CPUs ')
|
|
151
|
-
dim('OS ')
|
|
152
|
-
dim('v')
|
|
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)
|
|
153
166
|
);
|
|
154
167
|
console.log(dim(' ─────────────────────────────────────────────────────────────'));
|
|
155
168
|
console.log('');
|
|
156
169
|
}
|
|
157
170
|
|
|
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
|
-
|
|
163
171
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
164
|
-
//
|
|
172
|
+
// Smart Session Manager
|
|
165
173
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
166
174
|
|
|
167
175
|
async function loadLastSession() {
|
|
@@ -183,7 +191,6 @@ async function saveSession(options) {
|
|
|
183
191
|
generationMode: options.generationMode,
|
|
184
192
|
projectName : options.projectName,
|
|
185
193
|
savedAt : new Date().toISOString(),
|
|
186
|
-
// 10X: track extra features for diff
|
|
187
194
|
extraFeatures : options.extraFeatures ?? [],
|
|
188
195
|
addAuth : options.addAuth,
|
|
189
196
|
addSeeder : options.addSeeder,
|
|
@@ -192,13 +199,15 @@ async function saveSession(options) {
|
|
|
192
199
|
} catch {}
|
|
193
200
|
}
|
|
194
201
|
|
|
195
|
-
// 10X: Show what changed vs last session
|
|
196
202
|
function printSessionDiff(last, current) {
|
|
197
203
|
if (!last) return;
|
|
198
204
|
const changes = [];
|
|
199
|
-
if (last.stack !== current.stack)
|
|
200
|
-
|
|
201
|
-
if (last.
|
|
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)}`);
|
|
202
211
|
if (changes.length === 0) return;
|
|
203
212
|
console.log(chalk.dim(' ── 🔀 Changes from last session:'));
|
|
204
213
|
changes.forEach(c => console.log(chalk.dim(' ' + c)));
|
|
@@ -206,7 +215,7 @@ function printSessionDiff(last, current) {
|
|
|
206
215
|
}
|
|
207
216
|
|
|
208
217
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
209
|
-
// Plugin Loader — parallel loading
|
|
218
|
+
// Plugin Loader — parallel loading
|
|
210
219
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
211
220
|
|
|
212
221
|
async function loadPlugins() {
|
|
@@ -214,8 +223,6 @@ async function loadPlugins() {
|
|
|
214
223
|
try {
|
|
215
224
|
await fs.ensureDir(PLUGINS_DIR);
|
|
216
225
|
const entries = await fs.readdir(PLUGINS_DIR);
|
|
217
|
-
|
|
218
|
-
// 10X: load all plugins in parallel
|
|
219
226
|
const results = await Promise.allSettled(
|
|
220
227
|
entries.map(async (entry) => {
|
|
221
228
|
const pluginPath = path.join(PLUGINS_DIR, entry, 'index.js');
|
|
@@ -224,7 +231,6 @@ async function loadPlugins() {
|
|
|
224
231
|
return (plugin.default?.name && plugin.default?.run) ? plugin.default : null;
|
|
225
232
|
})
|
|
226
233
|
);
|
|
227
|
-
|
|
228
234
|
for (const r of results) {
|
|
229
235
|
if (r.status === 'fulfilled' && r.value) plugins.push(r.value);
|
|
230
236
|
}
|
|
@@ -233,7 +239,7 @@ async function loadPlugins() {
|
|
|
233
239
|
}
|
|
234
240
|
|
|
235
241
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
236
|
-
//
|
|
242
|
+
// Pre-flight Environment Checker
|
|
237
243
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
238
244
|
|
|
239
245
|
async function runPreflightChecks(stack) {
|
|
@@ -245,20 +251,22 @@ async function runPreflightChecks(stack) {
|
|
|
245
251
|
const pkgOk = await fs.pathExists(path.join(process.cwd(), 'package.json'));
|
|
246
252
|
checks.push({ name: 'package.json present', pass: pkgOk });
|
|
247
253
|
|
|
248
|
-
if (stack === 'dotnet-webapi')
|
|
249
|
-
|
|
250
|
-
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') });
|
|
251
260
|
|
|
252
|
-
const failed = checks.filter(
|
|
261
|
+
const failed = checks.filter(c => !c.pass);
|
|
253
262
|
|
|
254
263
|
if (checks.length > 0) {
|
|
255
264
|
console.log('');
|
|
256
265
|
console.log(chalk.hex('#00F5FF').bold(' ── 🔍 Pre-flight Checks ──────────────────────────────'));
|
|
257
|
-
checks.forEach(
|
|
266
|
+
checks.forEach(c => {
|
|
258
267
|
const icon = c.pass ? chalk.green(' ✓') : chalk.red(' ✗');
|
|
259
268
|
const label = c.pass ? chalk.dim(c.name) : chalk.red.bold(c.name);
|
|
260
269
|
console.log(`${icon} ${label}`);
|
|
261
|
-
// 10X: print fix hint for failed checks
|
|
262
270
|
if (!c.pass && PREFLIGHT_HINTS[c.name]) {
|
|
263
271
|
console.log(chalk.gray(` 💡 Fix: ${PREFLIGHT_HINTS[c.name]}`));
|
|
264
272
|
}
|
|
@@ -279,14 +287,12 @@ function buildProgressBar(pct, width = 24) {
|
|
|
279
287
|
}
|
|
280
288
|
|
|
281
289
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
282
|
-
//
|
|
290
|
+
// Token / Pricing Display
|
|
283
291
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
284
292
|
|
|
285
293
|
function printTokenUsage(mode, startTime, extra = {}) {
|
|
286
294
|
const elapsed = ((Date.now() - startTime) / 1000).toFixed(2);
|
|
287
295
|
const pricing = PRICING[mode] || PRICING.free;
|
|
288
|
-
|
|
289
|
-
// 10X: files/sec throughput
|
|
290
296
|
const throughput = extra.filesGenerated && elapsed > 0
|
|
291
297
|
? (extra.filesGenerated / parseFloat(elapsed)).toFixed(1) + ' files/s'
|
|
292
298
|
: null;
|
|
@@ -298,19 +304,10 @@ function printTokenUsage(mode, startTime, extra = {}) {
|
|
|
298
304
|
console.log('');
|
|
299
305
|
console.log(` ${chalk.dim('Mode:')} ${mode === 'pro' ? chalk.hex('#BF40FF').bold('PRO AI ✦') : chalk.hex('#00F5FF').bold('Standard ◉')}`);
|
|
300
306
|
console.log(` ${chalk.dim('Elapsed:')} ${chalk.white(elapsed + 's')}`);
|
|
301
|
-
|
|
302
|
-
if (extra.
|
|
303
|
-
|
|
304
|
-
}
|
|
305
|
-
if (extra.filesGenerated !== undefined) {
|
|
306
|
-
console.log(` ${chalk.dim('Files written:')} ${chalk.white(extra.filesGenerated)}`);
|
|
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
|
-
}
|
|
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)}`);
|
|
314
311
|
if (mode === 'pro') {
|
|
315
312
|
const usagePct = Math.round((pricing.outputTokens / 16000) * 100);
|
|
316
313
|
console.log(` ${chalk.dim('Input tokens:')} ${chalk.yellow(pricing.inputTokens.toLocaleString())}`);
|
|
@@ -344,7 +341,7 @@ async function detectFrontendFramework(srcDir) {
|
|
|
344
341
|
}
|
|
345
342
|
|
|
346
343
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
347
|
-
// API Key Management
|
|
344
|
+
// API Key Management
|
|
348
345
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
349
346
|
|
|
350
347
|
async function getProApiKey() {
|
|
@@ -375,13 +372,13 @@ async function getProApiKey() {
|
|
|
375
372
|
message : 'Enter your Backlist Pro API Key:',
|
|
376
373
|
validate: (input) => {
|
|
377
374
|
if (!input || input.trim().length < 10) return '❌ Invalid key — must be at least 10 characters.';
|
|
378
|
-
if (/\s/.test(input))
|
|
375
|
+
if (/\s/.test(input)) return '❌ Key must not contain spaces.';
|
|
379
376
|
},
|
|
380
377
|
});
|
|
381
378
|
if (p.isCancel(apiKey)) { p.cancel('Cancelled.'); process.exit(0); }
|
|
382
379
|
|
|
383
380
|
const spinner = ora({ text: chalk.cyan('Validating API key...'), spinner: 'arc', color: 'cyan' }).start();
|
|
384
|
-
await new Promise(
|
|
381
|
+
await new Promise(r => setTimeout(r, 1800));
|
|
385
382
|
spinner.succeed(chalk.green('API key validated ✓'));
|
|
386
383
|
|
|
387
384
|
await fs.writeJson(CONFIG_PATH, { apiKey, savedAt: new Date().toISOString() }, { spaces: 2 });
|
|
@@ -390,7 +387,7 @@ async function getProApiKey() {
|
|
|
390
387
|
}
|
|
391
388
|
|
|
392
389
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
393
|
-
//
|
|
390
|
+
// Free Mode Pipeline
|
|
394
391
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
395
392
|
|
|
396
393
|
async function runFreeModePipeline(options) {
|
|
@@ -398,17 +395,20 @@ async function runFreeModePipeline(options) {
|
|
|
398
395
|
console.log(chalk.hex('#00F5FF').bold(' ─── 🚀 Standard Mode: AST + EJS Static Generation ───'));
|
|
399
396
|
console.log('');
|
|
400
397
|
|
|
401
|
-
const t0
|
|
402
|
-
const steps
|
|
403
|
-
let stepsDone
|
|
404
|
-
|
|
398
|
+
const t0 = performance.now();
|
|
399
|
+
const steps = 5;
|
|
400
|
+
let stepsDone = 0;
|
|
405
401
|
const stepLabel = () => chalk.dim(`[${++stepsDone}/${steps}]`);
|
|
406
402
|
|
|
407
403
|
// Step 1 — AST
|
|
408
|
-
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();
|
|
409
409
|
let endpoints = [];
|
|
410
410
|
try { endpoints = await analyzeFrontend(options.frontendSrcDir); } catch {}
|
|
411
|
-
await new Promise(
|
|
411
|
+
await new Promise(r => setTimeout(r, 800));
|
|
412
412
|
spinnerAST.succeed(chalk.green(`AST parsing complete — ${chalk.bold(endpoints.length)} endpoint(s) mapped.`));
|
|
413
413
|
|
|
414
414
|
// Step 2 — Framework detection
|
|
@@ -416,12 +416,16 @@ async function runFreeModePipeline(options) {
|
|
|
416
416
|
p.log.info(chalk.dim(`${stepLabel()} Frontend detected: ${fw.icon} ${chalk.white(fw.name)}`));
|
|
417
417
|
|
|
418
418
|
// Step 3 — DOM Live Check
|
|
419
|
-
const spinnerDOM = ora({
|
|
419
|
+
const spinnerDOM = ora({
|
|
420
|
+
text : `${stepLabel()} ${chalk.white('Running DOM Live Check...')}`,
|
|
421
|
+
spinner: 'bouncingBar',
|
|
422
|
+
color : 'yellow',
|
|
423
|
+
}).start();
|
|
420
424
|
const inconsistencies = await performLowCostPathScan(options.frontendSrcDir, endpoints);
|
|
421
|
-
await new Promise(
|
|
425
|
+
await new Promise(r => setTimeout(r, 1200));
|
|
422
426
|
if (inconsistencies.length > 0) {
|
|
423
427
|
spinnerDOM.warn(chalk.yellow(`DOM Live Check — ${inconsistencies.length} path drift(s) detected.`));
|
|
424
|
-
inconsistencies.slice(0, 3).forEach(
|
|
428
|
+
inconsistencies.slice(0, 3).forEach(i => console.log(chalk.gray(` → ${i.warning}`)));
|
|
425
429
|
} else {
|
|
426
430
|
spinnerDOM.succeed(chalk.green('DOM Live Check passed — ') + chalk.yellow.bold('15% false positives eliminated!'));
|
|
427
431
|
}
|
|
@@ -429,8 +433,12 @@ async function runFreeModePipeline(options) {
|
|
|
429
433
|
// Step 4 — EJS Scaffolding
|
|
430
434
|
const meta = STACK_META[options.stack] || {};
|
|
431
435
|
const stackLabel = `${meta.icon || '⚙️'} ${options.stack}`;
|
|
432
|
-
const spinnerEJS = ora({
|
|
433
|
-
|
|
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));
|
|
434
442
|
|
|
435
443
|
try {
|
|
436
444
|
await dispatchGenerator(options);
|
|
@@ -440,13 +448,16 @@ async function runFreeModePipeline(options) {
|
|
|
440
448
|
throw err;
|
|
441
449
|
}
|
|
442
450
|
|
|
443
|
-
// Step 5 — Count generated files
|
|
444
|
-
const spinnerCount = ora({
|
|
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();
|
|
445
457
|
let fileCount = 0;
|
|
446
458
|
try { fileCount = await globCount(options.projectDir); } catch {}
|
|
447
459
|
spinnerCount.succeed(chalk.green(`${fileCount} file(s) written to ${chalk.bold(options.projectName)}/`));
|
|
448
460
|
|
|
449
|
-
// 10X: Print top-level file tree
|
|
450
461
|
await printFileTreeSummary(options.projectDir);
|
|
451
462
|
|
|
452
463
|
options._meta = {
|
|
@@ -457,7 +468,7 @@ async function runFreeModePipeline(options) {
|
|
|
457
468
|
}
|
|
458
469
|
|
|
459
470
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
460
|
-
//
|
|
471
|
+
// File Tree Summary (top 2 levels)
|
|
461
472
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
462
473
|
|
|
463
474
|
async function printFileTreeSummary(dir, depth = 0, maxDepth = 2) {
|
|
@@ -466,7 +477,7 @@ async function printFileTreeSummary(dir, depth = 0, maxDepth = 2) {
|
|
|
466
477
|
console.log(chalk.dim(' ── 📁 Generated Project Structure ─────────────────────'));
|
|
467
478
|
}
|
|
468
479
|
try {
|
|
469
|
-
const entries
|
|
480
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
470
481
|
const filtered = entries.filter(e => e.name !== 'node_modules' && !e.name.startsWith('.'));
|
|
471
482
|
for (const entry of filtered.slice(0, 12)) {
|
|
472
483
|
const indent = ' ' + ' '.repeat(depth + 1);
|
|
@@ -486,7 +497,10 @@ async function printFileTreeSummary(dir, depth = 0, maxDepth = 2) {
|
|
|
486
497
|
|
|
487
498
|
async function globCount(dir) {
|
|
488
499
|
const { glob } = await import('glob');
|
|
489
|
-
const files = await glob(`${dir.replace(/\\/g, '/')}/**/*`, {
|
|
500
|
+
const files = await glob(`${dir.replace(/\\/g, '/')}/**/*`, {
|
|
501
|
+
nodir : true,
|
|
502
|
+
ignore: ['**/node_modules/**'],
|
|
503
|
+
});
|
|
490
504
|
return files.length;
|
|
491
505
|
}
|
|
492
506
|
|
|
@@ -504,12 +518,14 @@ async function dispatchGenerator(options) {
|
|
|
504
518
|
'python-fastapi' : () => generatePythonProject(options),
|
|
505
519
|
};
|
|
506
520
|
const runner = gen[options.stack];
|
|
507
|
-
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
|
+
);
|
|
508
524
|
await runner();
|
|
509
525
|
}
|
|
510
526
|
|
|
511
527
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
512
|
-
// Pro AI Mode
|
|
528
|
+
// Pro AI Mode
|
|
513
529
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
514
530
|
|
|
515
531
|
async function callAIProcessor(astJsonData, apiKey, options) {
|
|
@@ -523,7 +539,11 @@ async function callAIProcessor(astJsonData, apiKey, options) {
|
|
|
523
539
|
|
|
524
540
|
let thoughtCount = 0;
|
|
525
541
|
let warnCount = 0;
|
|
526
|
-
let currentSpinner = ora({
|
|
542
|
+
let currentSpinner = ora({
|
|
543
|
+
text : chalk.cyan('Initialising autonomous agents...'),
|
|
544
|
+
spinner: 'mindblown',
|
|
545
|
+
color : 'magenta',
|
|
546
|
+
}).start();
|
|
527
547
|
|
|
528
548
|
const onThought = (msg) => {
|
|
529
549
|
thoughtCount++;
|
|
@@ -540,7 +560,7 @@ async function callAIProcessor(astJsonData, apiKey, options) {
|
|
|
540
560
|
await aiAgent.init();
|
|
541
561
|
|
|
542
562
|
let existingPrisma = null;
|
|
543
|
-
const prismaPath
|
|
563
|
+
const prismaPath = path.join(options.projectDir, 'prisma', 'schema.prisma');
|
|
544
564
|
if (await fs.pathExists(prismaPath)) existingPrisma = await fs.readFile(prismaPath, 'utf8');
|
|
545
565
|
|
|
546
566
|
const pass1Data = await aiAgent.generateBackendBlocks(astJsonData, existingPrisma);
|
|
@@ -555,12 +575,12 @@ async function callAIProcessor(astJsonData, apiKey, options) {
|
|
|
555
575
|
}
|
|
556
576
|
|
|
557
577
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
558
|
-
// Health Dashboard
|
|
578
|
+
// Health Dashboard
|
|
559
579
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
560
580
|
|
|
561
581
|
function printHealthDashboard(blocks, options = {}) {
|
|
562
582
|
const secScore = blocks.aiSecurityConfig && blocks.aiSecurityConfig.length > 20 ? 98 : 75;
|
|
563
|
-
const archScore = blocks.aiDbRelations && blocks.aiDbRelations.length
|
|
583
|
+
const archScore = blocks.aiDbRelations && blocks.aiDbRelations.length > 20 ? 99 : 80;
|
|
564
584
|
const testScore = 85;
|
|
565
585
|
const depsScore = 92;
|
|
566
586
|
const overall = Math.round((secScore + archScore + testScore + depsScore) / 4);
|
|
@@ -571,7 +591,6 @@ function printHealthDashboard(blocks, options = {}) {
|
|
|
571
591
|
if (s >= 70) return chalk.yellow.bold(`${s}% B`);
|
|
572
592
|
return chalk.red.bold(`${s}% C`);
|
|
573
593
|
};
|
|
574
|
-
|
|
575
594
|
const bar = (s) => buildProgressBar(s, 16);
|
|
576
595
|
|
|
577
596
|
console.log('');
|
|
@@ -586,7 +605,6 @@ function printHealthDashboard(blocks, options = {}) {
|
|
|
586
605
|
console.log('');
|
|
587
606
|
console.log(` 🏆 Overall Score: [${bar(overall)}] ${colorScore(overall)}`);
|
|
588
607
|
console.log('');
|
|
589
|
-
|
|
590
608
|
if (options.stack) {
|
|
591
609
|
const meta = STACK_META[options.stack] || {};
|
|
592
610
|
console.log(chalk.dim(` Stack: ${meta.icon || ''} ${options.stack} (${meta.lang || ''} / ${meta.runtime || ''})`));
|
|
@@ -597,7 +615,7 @@ function printHealthDashboard(blocks, options = {}) {
|
|
|
597
615
|
}
|
|
598
616
|
|
|
599
617
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
600
|
-
// Post-generation Next Steps
|
|
618
|
+
// Post-generation Next Steps
|
|
601
619
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
602
620
|
|
|
603
621
|
function printNextSteps(projectName, stack, dbType) {
|
|
@@ -628,7 +646,10 @@ function printNextSteps(projectName, stack, dbType) {
|
|
|
628
646
|
}
|
|
629
647
|
|
|
630
648
|
console.log('');
|
|
631
|
-
console.log(
|
|
649
|
+
console.log(
|
|
650
|
+
chalk.dim(' 💡 Tip: Run') + chalk.white(' npm run qa ') +
|
|
651
|
+
chalk.dim('to validate your generated backend.')
|
|
652
|
+
);
|
|
632
653
|
console.log(chalk.dim(' 📖 Docs: https://backlist.dev/docs'));
|
|
633
654
|
console.log('');
|
|
634
655
|
}
|
|
@@ -648,7 +669,9 @@ async function runConfigManager() {
|
|
|
648
669
|
if (exists) {
|
|
649
670
|
try {
|
|
650
671
|
const cfg = await fs.readJson(CONFIG_PATH);
|
|
651
|
-
const masked = cfg.apiKey
|
|
672
|
+
const masked = cfg.apiKey
|
|
673
|
+
? cfg.apiKey.slice(0, 4) + '●'.repeat(12) + cfg.apiKey.slice(-4)
|
|
674
|
+
: 'none';
|
|
652
675
|
console.log(` ${chalk.dim('API Key:')} ${chalk.white(masked)}`);
|
|
653
676
|
console.log(` ${chalk.dim('Saved at:')} ${chalk.gray(cfg.savedAt || 'unknown')}`);
|
|
654
677
|
} catch {}
|
|
@@ -674,17 +697,27 @@ async function runConfigManager() {
|
|
|
674
697
|
message: 'What would you like to do?',
|
|
675
698
|
options: [
|
|
676
699
|
{ value: 'qa-post', label: '🚀 Post-Gen Validation', hint: 'Validate a generated project right now' },
|
|
677
|
-
{ value: 'clear-key', label: '🗑️ Clear saved API key'
|
|
700
|
+
{ value: 'clear-key', label: '🗑️ Clear saved API key' },
|
|
678
701
|
{ value: 'clear-sess', label: '🗑️ Clear session history' },
|
|
679
|
-
{ value: 'clear-all', label: '💥 Clear everything'
|
|
680
|
-
{ value: 'back', label: '← Back to main menu'
|
|
702
|
+
{ value: 'clear-all', label: '💥 Clear everything' },
|
|
703
|
+
{ value: 'back', label: '← Back to main menu' },
|
|
681
704
|
],
|
|
682
705
|
});
|
|
683
706
|
if (p.isCancel(action) || action === 'back') return;
|
|
684
707
|
|
|
685
|
-
if (action === 'qa-post') {
|
|
686
|
-
|
|
687
|
-
|
|
708
|
+
if (action === 'qa-post') {
|
|
709
|
+
await initQASystem();
|
|
710
|
+
await autoRunPostGeneration();
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
if (action === 'clear-key' || action === 'clear-all') {
|
|
714
|
+
await fs.remove(CONFIG_PATH);
|
|
715
|
+
p.log.success('API key cleared.');
|
|
716
|
+
}
|
|
717
|
+
if (action === 'clear-sess' || action === 'clear-all') {
|
|
718
|
+
await fs.remove(SESSIONS_PATH);
|
|
719
|
+
p.log.success('Session history cleared.');
|
|
720
|
+
}
|
|
688
721
|
}
|
|
689
722
|
|
|
690
723
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -706,11 +739,11 @@ async function runPluginManager(plugins) {
|
|
|
706
739
|
|
|
707
740
|
const choice = await p.select({
|
|
708
741
|
message: 'Select a plugin to run:',
|
|
709
|
-
options: plugins.map(
|
|
742
|
+
options: plugins.map(pl => ({ value: pl.name, label: `🔌 ${pl.name}`, hint: pl.description || '' })),
|
|
710
743
|
});
|
|
711
744
|
if (p.isCancel(choice)) return;
|
|
712
745
|
|
|
713
|
-
const plugin = plugins.find(
|
|
746
|
+
const plugin = plugins.find(pl => pl.name === choice);
|
|
714
747
|
if (plugin) {
|
|
715
748
|
try {
|
|
716
749
|
await plugin.run({ chalk, ora, p, fs, path });
|
|
@@ -728,23 +761,26 @@ async function promptRepeatLast(lastSession) {
|
|
|
728
761
|
if (!lastSession) return false;
|
|
729
762
|
const meta = STACK_META[lastSession.stack] || {};
|
|
730
763
|
const icon = meta.icon || '⚙️';
|
|
731
|
-
console.log(chalk.dim(
|
|
764
|
+
console.log(chalk.dim(
|
|
765
|
+
` ⚡ Last session: ${icon} ${lastSession.stack} · ` +
|
|
766
|
+
`${lastSession.generationMode} mode · ${lastSession.savedAt?.slice(0, 10) || ''}`
|
|
767
|
+
));
|
|
732
768
|
console.log('');
|
|
733
769
|
const repeat = await p.confirm({
|
|
734
|
-
message
|
|
770
|
+
message : chalk.hex('#00F5FF')('Repeat last generation with same settings?'),
|
|
735
771
|
initialValue: false,
|
|
736
772
|
});
|
|
737
773
|
return !p.isCancel(repeat) && repeat;
|
|
738
774
|
}
|
|
739
775
|
|
|
740
776
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
741
|
-
//
|
|
777
|
+
// Input Validation Helpers
|
|
742
778
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
743
779
|
|
|
744
780
|
function validateProjectName(v) {
|
|
745
|
-
if (!v || !v.trim())
|
|
781
|
+
if (!v || !v.trim()) return '❌ Cannot be empty.';
|
|
746
782
|
if (/[^a-zA-Z0-9_\-.]/.test(v)) return '❌ Use only letters, numbers, hyphens, underscores, dots.';
|
|
747
|
-
if (v.length > 64)
|
|
783
|
+
if (v.length > 64) return '❌ Name too long (max 64 chars).';
|
|
748
784
|
return undefined;
|
|
749
785
|
}
|
|
750
786
|
|
|
@@ -753,19 +789,25 @@ function validateSrcPath(v) {
|
|
|
753
789
|
return undefined;
|
|
754
790
|
}
|
|
755
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
|
+
|
|
756
798
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
757
|
-
// Main CLI Flow — v8.0-10X
|
|
799
|
+
// Main CLI Flow — v8.0-10X + QA v10.0
|
|
758
800
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
759
801
|
|
|
760
802
|
async function main() {
|
|
761
803
|
const globalStart = performance.now();
|
|
762
804
|
printBanner();
|
|
763
805
|
|
|
764
|
-
//
|
|
806
|
+
// Load plugins & last session in parallel
|
|
765
807
|
const [plugins, lastSession] = await Promise.all([loadPlugins(), loadLastSession()]);
|
|
766
808
|
|
|
767
809
|
if (plugins.length > 0) {
|
|
768
|
-
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(', ')}`));
|
|
769
811
|
}
|
|
770
812
|
|
|
771
813
|
p.intro(chalk.hex('#00F5FF').bold(' create-backlist v8.0-10X — Polyglot Backend Generator '));
|
|
@@ -795,9 +837,9 @@ async function main() {
|
|
|
795
837
|
projectName,
|
|
796
838
|
stack : lastSession.stack,
|
|
797
839
|
srcPath,
|
|
798
|
-
dbType : lastSession.dbType
|
|
799
|
-
addAuth : lastSession.addAuth
|
|
800
|
-
addSeeder : lastSession.addSeeder
|
|
840
|
+
dbType : lastSession.dbType || 'mongoose',
|
|
841
|
+
addAuth : lastSession.addAuth ?? true,
|
|
842
|
+
addSeeder : lastSession.addSeeder ?? true,
|
|
801
843
|
extraFeatures : lastSession.extraFeatures ?? ['docker', 'testing', 'swagger'],
|
|
802
844
|
projectDir : path.resolve(process.cwd(), projectName),
|
|
803
845
|
frontendSrcDir : path.resolve(process.cwd(), srcPath),
|
|
@@ -813,20 +855,52 @@ async function main() {
|
|
|
813
855
|
const mode = await p.select({
|
|
814
856
|
message: 'Select your mode:',
|
|
815
857
|
options: [
|
|
816
|
-
{ value: 'free', label: '🚀 Standard Mode',
|
|
817
|
-
{ value: 'pro', label: '🧠 Pro AI Mode',
|
|
818
|
-
{ value: 'qa-
|
|
819
|
-
{ value: 'qa-
|
|
820
|
-
{ value: 'qa-
|
|
821
|
-
{ value: 'qa-
|
|
822
|
-
{ value: 'qa-
|
|
823
|
-
{ value: '
|
|
824
|
-
|
|
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
|
+
),
|
|
825
871
|
],
|
|
826
872
|
});
|
|
827
873
|
if (p.isCancel(mode)) { p.cancel('Cancelled.'); process.exit(0); }
|
|
828
874
|
|
|
829
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
|
+
|
|
830
904
|
if (mode === 'qa-manual') { await initQASystem(); await runManualQA(); return; }
|
|
831
905
|
if (mode === 'qa-auto') { await initQASystem(); await runAutomatedQA({ continuous: false }); return; }
|
|
832
906
|
if (mode === 'qa-live') { await initQASystem(); await runAutomatedQA({ continuous: true }); return; }
|
|
@@ -846,7 +920,7 @@ async function main() {
|
|
|
846
920
|
});
|
|
847
921
|
if (p.isCancel(projectName)) { p.cancel('Cancelled.'); process.exit(0); }
|
|
848
922
|
|
|
849
|
-
//
|
|
923
|
+
// Warn if directory already exists
|
|
850
924
|
const targetDir = path.resolve(process.cwd(), projectName);
|
|
851
925
|
if (await fs.pathExists(targetDir)) {
|
|
852
926
|
p.log.warn(chalk.yellow(`⚠️ Directory '${projectName}' already exists — files may be overwritten.`));
|
|
@@ -858,12 +932,12 @@ async function main() {
|
|
|
858
932
|
const stack = await p.select({
|
|
859
933
|
message: 'Select backend stack:',
|
|
860
934
|
options: [
|
|
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'
|
|
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' },
|
|
867
941
|
],
|
|
868
942
|
});
|
|
869
943
|
if (p.isCancel(stack)) { p.cancel('Cancelled.'); process.exit(0); }
|
|
@@ -885,7 +959,6 @@ async function main() {
|
|
|
885
959
|
});
|
|
886
960
|
if (p.isCancel(srcPath)) { p.cancel('Cancelled.'); process.exit(0); }
|
|
887
961
|
|
|
888
|
-
// 10X: warn if src directory not found
|
|
889
962
|
const resolvedSrc = path.resolve(process.cwd(), srcPath);
|
|
890
963
|
if (!await fs.pathExists(resolvedSrc)) {
|
|
891
964
|
p.log.warn(chalk.yellow(`⚠️ Directory '${srcPath}' not found — AST scan may return 0 endpoints.`));
|
|
@@ -903,7 +976,7 @@ async function main() {
|
|
|
903
976
|
dbType = await p.select({
|
|
904
977
|
message: 'Database type:',
|
|
905
978
|
options: [
|
|
906
|
-
{ value: 'mongoose', label: '🍃 NoSQL — MongoDB + Mongoose'
|
|
979
|
+
{ value: 'mongoose', label: '🍃 NoSQL — MongoDB + Mongoose' },
|
|
907
980
|
{ value: 'prisma', label: '🔺 SQL — PostgreSQL/MySQL + Prisma' },
|
|
908
981
|
],
|
|
909
982
|
});
|
|
@@ -919,9 +992,9 @@ async function main() {
|
|
|
919
992
|
message: 'Additional features:',
|
|
920
993
|
options: [
|
|
921
994
|
{ value: 'docker', label: '🐳 Docker Support', hint: 'Dockerfile + docker-compose.yml' },
|
|
922
|
-
{ value: 'testing', label: '🧪 API Testing Boilerplate'
|
|
923
|
-
{ value: 'swagger', label: '📖 Swagger UI (API Docs)'
|
|
924
|
-
{ value: 'ci', label: '⚙️ GitHub Actions CI', hint: 'Auto-deploy workflow'
|
|
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' },
|
|
925
998
|
],
|
|
926
999
|
initialValues: ['docker', 'testing', 'swagger'],
|
|
927
1000
|
});
|
|
@@ -947,7 +1020,6 @@ async function main() {
|
|
|
947
1020
|
console.log(` ${chalk.dim('Output:')} ${chalk.gray(targetDir)}`);
|
|
948
1021
|
console.log('');
|
|
949
1022
|
|
|
950
|
-
// 10X: show diff from last session
|
|
951
1023
|
printSessionDiff(lastSession, { stack, dbType, generationMode });
|
|
952
1024
|
|
|
953
1025
|
const proceed = await p.confirm({ message: 'Proceed with generation?', initialValue: true });
|
|
@@ -971,19 +1043,23 @@ async function main() {
|
|
|
971
1043
|
}
|
|
972
1044
|
|
|
973
1045
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
974
|
-
//
|
|
1046
|
+
// Generation Executor — with auto-retry
|
|
975
1047
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
976
1048
|
|
|
977
1049
|
async function executeGeneration(options, globalStart, plugins = [], _attempt = 1) {
|
|
978
1050
|
const startTime = Date.now();
|
|
979
|
-
_cleanupDir = options.projectDir;
|
|
1051
|
+
_cleanupDir = options.projectDir;
|
|
980
1052
|
|
|
981
1053
|
try {
|
|
982
|
-
// ── PRO MODE
|
|
1054
|
+
// ── PRO MODE ──────────────────────────────────────────────────────────
|
|
983
1055
|
if (options.generationMode === 'pro') {
|
|
984
1056
|
const apiKey = await getProApiKey();
|
|
985
1057
|
|
|
986
|
-
const spinnerParse = ora({
|
|
1058
|
+
const spinnerParse = ora({
|
|
1059
|
+
text : chalk.white('Parsing frontend with Babel AST...'),
|
|
1060
|
+
spinner: 'dots12',
|
|
1061
|
+
color : 'cyan',
|
|
1062
|
+
}).start();
|
|
987
1063
|
let astJsonData = [];
|
|
988
1064
|
try {
|
|
989
1065
|
astJsonData = await analyzeFrontend(options.frontendSrcDir);
|
|
@@ -993,9 +1069,13 @@ async function executeGeneration(options, globalStart, plugins = [], _attempt =
|
|
|
993
1069
|
}
|
|
994
1070
|
|
|
995
1071
|
const generatedBlocks = await callAIProcessor(astJsonData, apiKey, options);
|
|
996
|
-
options.aiBlocks
|
|
1072
|
+
options.aiBlocks = generatedBlocks;
|
|
997
1073
|
|
|
998
|
-
const spinnerGen = ora({
|
|
1074
|
+
const spinnerGen = ora({
|
|
1075
|
+
text : chalk.white('Writing hexagonal output...'),
|
|
1076
|
+
spinner: 'material',
|
|
1077
|
+
color : 'magenta',
|
|
1078
|
+
}).start();
|
|
999
1079
|
try {
|
|
1000
1080
|
await dispatchGenerator(options);
|
|
1001
1081
|
spinnerGen.succeed(chalk.green('Hexagonal auto-write complete.'));
|
|
@@ -1006,20 +1086,28 @@ async function executeGeneration(options, globalStart, plugins = [], _attempt =
|
|
|
1006
1086
|
|
|
1007
1087
|
if (generatedBlocks.deployment) {
|
|
1008
1088
|
await fs.ensureDir(path.join(options.projectDir, '.github', 'workflows'));
|
|
1009
|
-
await fs.writeFile(
|
|
1010
|
-
|
|
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
|
+
);
|
|
1011
1097
|
}
|
|
1012
1098
|
|
|
1013
1099
|
printHealthDashboard(generatedBlocks, options);
|
|
1014
|
-
printTokenUsage('pro', startTime, {
|
|
1100
|
+
printTokenUsage('pro', startTime, {
|
|
1101
|
+
endpointCount: options.aiBlocks ? Object.keys(options.aiBlocks).length : 0,
|
|
1102
|
+
});
|
|
1015
1103
|
|
|
1016
1104
|
} else {
|
|
1017
|
-
// ── FREE MODE
|
|
1105
|
+
// ── FREE MODE ────────────────────────────────────────────────────────
|
|
1018
1106
|
await runFreeModePipeline(options);
|
|
1019
1107
|
printTokenUsage('free', startTime, { ...(options._meta || {}), retries: _attempt - 1 });
|
|
1020
1108
|
}
|
|
1021
1109
|
|
|
1022
|
-
// ── Post-gen plugins
|
|
1110
|
+
// ── Post-gen plugins ──────────────────────────────────────────────────
|
|
1023
1111
|
for (const plugin of plugins) {
|
|
1024
1112
|
if (plugin.runAfterGenerate) {
|
|
1025
1113
|
try { await plugin.runAfterGenerate(options); } catch {}
|
|
@@ -1029,36 +1117,36 @@ async function executeGeneration(options, globalStart, plugins = [], _attempt =
|
|
|
1029
1117
|
await saveSession(options);
|
|
1030
1118
|
printNextSteps(options.projectName, options.stack, options.dbType);
|
|
1031
1119
|
|
|
1032
|
-
_cleanupDir = null;
|
|
1120
|
+
_cleanupDir = null;
|
|
1033
1121
|
|
|
1034
1122
|
const totalTime = ((performance.now() - globalStart) / 1000).toFixed(2);
|
|
1035
1123
|
p.outro(
|
|
1036
|
-
(options.generationMode === 'pro'
|
|
1037
|
-
|
|
1038
|
-
|
|
1124
|
+
(options.generationMode === 'pro'
|
|
1125
|
+
? chalk.hex('#BF40FF')
|
|
1126
|
+
: chalk.hex('#00F5FF')
|
|
1127
|
+
).bold(`✓ Done in ${totalTime}s — cd ${options.projectName}`)
|
|
1039
1128
|
);
|
|
1040
1129
|
|
|
1041
1130
|
} catch (error) {
|
|
1042
1131
|
console.log('');
|
|
1043
1132
|
|
|
1044
|
-
// 10X: filter noisy stack frames
|
|
1045
1133
|
const cleanStack = (error.stack ?? '')
|
|
1046
1134
|
.split('\n')
|
|
1047
1135
|
.filter(l => !l.includes('node_modules') && !l.includes('node:internal'))
|
|
1048
1136
|
.slice(0, 6)
|
|
1049
1137
|
.join('\n');
|
|
1050
1138
|
|
|
1051
|
-
p.log.error(chalk.red.bold(
|
|
1139
|
+
p.log.error(chalk.red.bold(
|
|
1140
|
+
`Generation failed (attempt ${_attempt}/${MAX_RETRIES}): ${error.message || error}`
|
|
1141
|
+
));
|
|
1052
1142
|
if (cleanStack) console.log(chalk.gray(cleanStack));
|
|
1053
1143
|
|
|
1054
|
-
// 10X: auto-retry
|
|
1055
1144
|
if (_attempt < MAX_RETRIES) {
|
|
1056
1145
|
const delay = _attempt * 2000;
|
|
1057
1146
|
console.log('');
|
|
1058
1147
|
p.log.warn(chalk.yellow(`⏳ Retrying in ${delay / 1000}s...`));
|
|
1059
|
-
await new Promise(
|
|
1148
|
+
await new Promise(r => setTimeout(r, delay));
|
|
1060
1149
|
|
|
1061
|
-
// Clean up partial output before retry
|
|
1062
1150
|
if (options.projectDir && await fs.pathExists(options.projectDir)) {
|
|
1063
1151
|
await fs.remove(options.projectDir).catch(() => {});
|
|
1064
1152
|
}
|
|
@@ -1066,9 +1154,12 @@ async function executeGeneration(options, globalStart, plugins = [], _attempt =
|
|
|
1066
1154
|
return executeGeneration(options, globalStart, plugins, _attempt + 1);
|
|
1067
1155
|
}
|
|
1068
1156
|
|
|
1069
|
-
// All retries exhausted — clean up
|
|
1070
1157
|
if (options.projectDir && await fs.pathExists(options.projectDir)) {
|
|
1071
|
-
const sc = ora({
|
|
1158
|
+
const sc = ora({
|
|
1159
|
+
text : chalk.yellow('Cleaning up partial output...'),
|
|
1160
|
+
spinner: 'line',
|
|
1161
|
+
color : 'yellow',
|
|
1162
|
+
}).start();
|
|
1072
1163
|
await fs.remove(options.projectDir).catch(() => {});
|
|
1073
1164
|
sc.succeed(chalk.yellow('Cleanup done.'));
|
|
1074
1165
|
}
|
|
@@ -1078,8 +1169,8 @@ async function executeGeneration(options, globalStart, plugins = [], _attempt =
|
|
|
1078
1169
|
}
|
|
1079
1170
|
}
|
|
1080
1171
|
|
|
1081
|
-
// ── Launch
|
|
1082
|
-
main().catch(
|
|
1172
|
+
// ── Launch ────────────────────────────────────────────────────────────────
|
|
1173
|
+
main().catch(err => {
|
|
1083
1174
|
console.error(chalk.red.bold(`\n Fatal: ${err.message || err}`));
|
|
1084
1175
|
if (err.stack) console.error(chalk.gray(err.stack.split('\n').slice(1, 5).join('\n')));
|
|
1085
1176
|
process.exit(1);
|