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.
Files changed (3) hide show
  1. package/bin/index.js +247 -156
  2. package/package.json +1 -1
  3. 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 (10X: measure cold-start time) ────────────────────
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 { runManualQA, runAutomatedQA,
52
- viewQAHistory, initQASystem,
53
- autoRunPostGeneration } from '../src/qa/qa-engine.js';
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 = path.join(os.homedir(), '.backlist-config.json');
60
- const SESSIONS_PATH = path.join(os.homedir(), '.backlist-sessions.json');
61
- const PLUGINS_DIR = path.join(os.homedir(), '.backlist-plugins');
62
- const VERSION = '8.0.0-10X';
63
- const MAX_RETRIES = 3; // 10X: auto-retry generation up to 3 times
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
- // 10X: Graceful Shutdown Handler
102
+ // Graceful Shutdown Handler
92
103
  // ═══════════════════════════════════════════════════════════════════════════
93
104
 
94
- let _cleanupDir = null; // set during generation so SIGINT can clean up
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({ text: chalk.yellow('Cleaning up partial output...'), spinner: 'line', color: 'yellow' }).start();
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 ') + chalk.white(bootMs + 'ms') + dim(' │ ') +
148
- dim('Node ') + chalk.white(nodeVer) + dim(' │ ') +
149
- dim('Heap ') + chalk.white(heapMB + 'MB') + dim('/') + chalk.gray(rssMP + 'MB RSS') + dim(' │ ') +
150
- dim('CPUs ') + chalk.white(cpus) + dim(' │ ') +
151
- dim('OS ') + chalk.white(platform) + dim(' │ ') +
152
- dim('v') + chalk.white(VERSION)
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
- // 10X: Smart Session Manager — with diff display
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) 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)}`);
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 (10X)
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
- // 10X: Pre-flight Environment Checker with auto-fix hints
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') 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') });
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((c) => !c.pass);
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((c) => {
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
- // 10X: Token / Pricing Display with ETA
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.endpointCount !== undefined) {
303
- console.log(` ${chalk.dim('Endpoints:')} ${chalk.white(extra.endpointCount + ' detected')}`);
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 — masked display & expiry check
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)) return '❌ Key must not contain spaces.';
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((r) => setTimeout(r, 1800));
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
- // 10X: Free Mode Pipeline with ETA tracker
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 = performance.now();
402
- const steps = 5;
403
- let stepsDone = 0;
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({ text: `${stepLabel()} ${chalk.white('Parsing frontend files with Babel AST...')}`, spinner: 'dots12', color: 'cyan' }).start();
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((r) => setTimeout(r, 800));
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({ text: `${stepLabel()} ${chalk.white('Running DOM Live Check...')}`, spinner: 'bouncingBar', color: 'yellow' }).start();
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((r) => setTimeout(r, 1200));
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((i) => console.log(chalk.gray(` → ${i.warning}`)));
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({ text: `${stepLabel()} ${chalk.white(`Scaffolding ${stackLabel} via Hexagonal EJS Templates...`)}`, spinner: 'material', color: 'magenta' }).start();
433
- await new Promise((r) => setTimeout(r, 600));
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 + print tree summary
444
- const spinnerCount = ora({ text: `${stepLabel()} ${chalk.white('Counting generated files...')}`, spinner: 'line', color: 'cyan' }).start();
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
- // 10X: File Tree Summary (top 2 levels)
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 = await fs.readdir(dir, { withFileTypes: true });
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, '/')}/**/*`, { nodir: true, ignore: ['**/node_modules/**'] });
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(`Stack '${options.stack}' is not supported. Valid options: ${Object.keys(gen).join(', ')}`);
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 — streaming thought display
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({ text: chalk.cyan('Initialising autonomous agents...'), spinner: 'mindblown', color: 'magenta' }).start();
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 = path.join(options.projectDir, 'prisma', 'schema.prisma');
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 — v8.0-10X
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 > 20 ? 99 : 80;
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 Guide
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(chalk.dim(' 💡 Tip: Run') + chalk.white(' npm run qa ') + chalk.dim('to validate your generated backend.'));
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 ? cfg.apiKey.slice(0, 4) + '●'.repeat(12) + cfg.apiKey.slice(-4) : 'none';
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') { 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.'); }
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((pl) => ({ value: pl.name, label: `🔌 ${pl.name}`, hint: pl.description || '' })),
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((pl) => pl.name === choice);
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(` ⚡ Last session: ${icon} ${lastSession.stack} · ${lastSession.generationMode} mode · ${lastSession.savedAt?.slice(0, 10) || ''}`));
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 : chalk.hex('#00F5FF')('Repeat last generation with same settings?'),
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
- // 10X: Input Validation Helpers
777
+ // Input Validation Helpers
742
778
  // ═══════════════════════════════════════════════════════════════════════════
743
779
 
744
780
  function validateProjectName(v) {
745
- if (!v || !v.trim()) return '❌ Cannot be empty.';
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) return '❌ Name too long (max 64 chars).';
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
- // 10X: load plugins & last session in parallel
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((pl) => pl.name).join(', ')}`));
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 || 'mongoose',
799
- addAuth : lastSession.addAuth ?? true,
800
- addSeeder : lastSession.addSeeder ?? true,
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', 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' },
824
- ...(plugins.length > 0 ? [{ value: 'plugins', label: '🔌 Plugins', hint: `${plugins.length} installed` }] : []),
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
- // 10X: warn if directory already exists
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
- // 10X: Generation Executor — with auto-retry
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; // register for SIGINT cleanup
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({ text: chalk.white('Parsing frontend with Babel AST...'), spinner: 'dots12', color: 'cyan' }).start();
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 = generatedBlocks;
1072
+ options.aiBlocks = generatedBlocks;
997
1073
 
998
- const spinnerGen = ora({ text: chalk.white('Writing hexagonal output...'), spinner: 'material', color: 'magenta' }).start();
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(path.join(options.projectDir, 'docker-compose.yml'), generatedBlocks.deployment.dockerCompose);
1010
- await fs.writeFile(path.join(options.projectDir, '.github', 'workflows', 'deploy.yml'), generatedBlocks.deployment.githubWorkflow);
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, { endpointCount: options.aiBlocks ? Object.keys(options.aiBlocks).length : 0 });
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; // generation succeeded — no cleanup needed
1120
+ _cleanupDir = null;
1033
1121
 
1034
1122
  const totalTime = ((performance.now() - globalStart) / 1000).toFixed(2);
1035
1123
  p.outro(
1036
- (options.generationMode === 'pro' ? chalk.hex('#BF40FF') : chalk.hex('#00F5FF')).bold(
1037
- `✓ Done in ${totalTime}s — cd ${options.projectName}`
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(`Generation failed (attempt ${_attempt}/${MAX_RETRIES}): ${error.message || error}`));
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((r) => setTimeout(r, delay));
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({ text: chalk.yellow('Cleaning up partial output...'), spinner: 'line', color: 'yellow' }).start();
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((err) => {
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);