create-backlist 7.4.0 → 9.0.0

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 CHANGED
@@ -1,8 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // ═══════════════════════════════════════════════════════════════════════════
4
- // create-backlist v7.0 — Smart Freemium SaaS CLI
4
+ // create-backlist v8.0 — Smart Freemium SaaS CLI
5
5
  // Copyright (c) W.A.H.ISHAN — MIT License
6
+ // ⚡ 10X Edition — Smart Routing · Health Monitor · Plugin System · DX++
6
7
  // ═══════════════════════════════════════════════════════════════════════════
7
8
 
8
9
  import * as p from '@clack/prompts';
@@ -12,87 +13,246 @@ import fs from 'fs-extra';
12
13
  import path from 'node:path';
13
14
  import os from 'node:os';
14
15
  import { fileURLToPath } from 'node:url';
15
- import { runManualQA, runAutomatedQA, viewQAHistory } from '../src/qa/qa-engine.js';
16
- //
16
+ import { performance } from 'node:perf_hooks';
17
+
17
18
 
18
19
  // ── Polyfill __dirname for ES Modules ────────────────────────────────────
19
20
  const __filename = fileURLToPath(import.meta.url);
20
- const __dirname = path.dirname(__filename);
21
+ const __dirname = path.dirname(__filename);
21
22
 
22
23
  // ── Internal Modules ─────────────────────────────────────────────────────
23
- import { isCommandAvailable } from '../src/utils.js';
24
- import { analyzeFrontend, performLowCostPathScan, extractComponentTreeTypes } from '../src/analyzer.js';
25
- import { BacklistAIAgent } from '../src/ai-agent.js';
24
+ import { isCommandAvailable } from '../src/utils.js';
25
+ import { analyzeFrontend, performLowCostPathScan,
26
+ extractComponentTreeTypes } from '../src/analyzer.js';
27
+ import { BacklistAIAgent } from '../src/ai-agent.js';
26
28
 
27
29
  // ── Generator Imports ────────────────────────────────────────────────────
28
- import { generateNodeProject } from '../src/generators/node.js';
29
- import { generateJsProject } from '../src/generators/js.js';
30
- import { generateNestProject } from '../src/generators/nestjs.js';
30
+ import { generateNodeProject } from '../src/generators/node.js';
31
+ import { generateJsProject } from '../src/generators/js.js';
32
+ import { generateNestProject } from '../src/generators/nestjs.js';
31
33
  import { generateDotnetProject } from '../src/generators/dotnet.js';
32
- import { generateJavaProject } from '../src/generators/java.js';
34
+ import { generateJavaProject } from '../src/generators/java.js';
33
35
  import { generatePythonProject } from '../src/generators/python.js';
34
36
 
35
- // ── Constants ────────────────────────────────────────────────────────────
36
- const CONFIG_PATH = path.join(os.homedir(), '.backlist-config.json');
37
+ // ── QA System ────────────────────────────────────────────────────────────
38
+ import { runManualQA, runAutomatedQA,
39
+ viewQAHistory, initQASystem,
40
+ autoRunPostGeneration } from '../src/qa/qa-engine.js';
41
+
42
+ // ═══════════════════════════════════════════════════════════════════════════
43
+ // Constants & Paths
44
+ // ═══════════════════════════════════════════════════════════════════════════
45
+
46
+ const CONFIG_PATH = path.join(os.homedir(), '.backlist-config.json');
47
+ const SESSIONS_PATH = path.join(os.homedir(), '.backlist-sessions.json');
48
+ const PLUGINS_DIR = path.join(os.homedir(), '.backlist-plugins');
49
+ const VERSION = '8.0.0';
37
50
 
38
- // ── Pricing Table (tokens per generation) ────────────────────────────────
51
+ // ── Pricing Table ─────────────────────────────────────────────────────────
39
52
  const PRICING = {
40
- free: { inputTokens: 0, outputTokens: 0, cost: '$0.00' },
41
- pro: { inputTokens: 4200, outputTokens: 12800, cost: '~$0.02' },
53
+ free : { inputTokens: 0, outputTokens: 0, cost: '$0.00' },
54
+ pro : { inputTokens: 4200, outputTokens: 12800, cost: '~$0.02' },
55
+ };
56
+
57
+ // ── Stack metadata ────────────────────────────────────────────────────────
58
+ const STACK_META = {
59
+ 'node-ts-express' : { lang: 'TypeScript', runtime: 'Node.js', color: '#3178C6', icon: '🔷' },
60
+ 'js-express' : { lang: 'JavaScript', runtime: 'Node.js', color: '#F7DF1E', icon: '🟨' },
61
+ 'nestjs' : { lang: 'TypeScript', runtime: 'NestJS', color: '#E0234E', icon: '🔴' },
62
+ 'dotnet-webapi' : { lang: 'C#', runtime: '.NET', color: '#512BD4', icon: '🟣' },
63
+ 'java-spring' : { lang: 'Java', runtime: 'Spring', color: '#6DB33F', icon: '🍃' },
64
+ 'python-fastapi' : { lang: 'Python', runtime: 'FastAPI', color: '#009688', icon: '🐍' },
42
65
  };
43
66
 
44
67
  // ═══════════════════════════════════════════════════════════════════════════
45
- // ASCII Art Banner
68
+ // Animated ASCII Banner — v8.0
46
69
  // ═══════════════════════════════════════════════════════════════════════════
47
70
 
48
71
  function printBanner() {
49
- const g1 = chalk.hex('#00F5FF');
50
- const g2 = chalk.hex('#BF40FF');
51
- const g3 = chalk.hex('#FF6B6B');
72
+ const c1 = chalk.hex('#00F5FF');
73
+ const c2 = chalk.hex('#BF40FF');
74
+ const c3 = chalk.hex('#FF6B6B');
75
+ const c4 = chalk.hex('#00FF9F');
52
76
  const dim = chalk.gray;
53
77
 
54
78
  console.log('');
55
- console.log(g1(' ╔══════════════════════════════════════════════════════════════╗'));
56
- console.log(g1(' ║') + g2.bold(' ____ ___ ________ __ ____ ___________ ') + g1('║'));
57
- console.log(g1(' ║') + g2.bold(' / __ ) / | / ____/ //_/ / / / _/ ___/_ ') + g1('║'));
58
- console.log(g1(' ║') + g2.bold(' / __ | / /| | / / / ,< / / / / \\__ \\ ') + g1('║'));
59
- console.log(g1(' ║') + g2.bold(' / /_/ / / ___ |/ /___/ /| | / /____/ / ___/ / ') + g1('║'));
60
- console.log(g1(' ║') + g2.bold('/_____/ /_/ |_|\\____/_/ |_| /_____/___//____/ ') + g1('║'));
61
- console.log(g1(' ║') + ' ' + g1('║'));
62
- console.log(g1(' ║') + g3.bold(' v7.0 SaaS — Polyglot Backend Engine ⚡ ') + g1('║'));
63
- console.log(g1(' ║') + dim(' Reverse-engineer frontends into full backends ') + g1('║'));
64
- console.log(g1(' ╚══════════════════════════════════════════════════════════════╝'));
79
+ console.log(c1(' ╔══════════════════════════════════════════════════════════════╗'));
80
+ console.log(c1(' ║') + c2.bold(' ____ ___ ________ __ ____ ___________ ') + c1('║'));
81
+ console.log(c1(' ║') + c2.bold(' / __ ) / | / ____/ //_/ / / / _/ ___/_ ') + c1('║'));
82
+ console.log(c1(' ║') + c2.bold(' / __ | / /| | / / / ,< / / / / \\__ \\ ') + c1('║'));
83
+ console.log(c1(' ║') + c2.bold(' / /_/ / / ___ |/ /___/ /| | / /____/ / ___/ / ') + c1('║'));
84
+ console.log(c1(' ║') + c2.bold('/_____/ /_/ |_|\\____/_/ |_| /_____/___//____/ ') + c1('║'));
85
+ console.log(c1(' ║') + ' ' + c1('║'));
86
+ console.log(c1(' ║') + c3.bold(' v8.0 SaaS — Polyglot Backend Engine 10X ⚡ ') + c1('║'));
87
+ console.log(c1(' ║') + dim(' Reverse-engineer frontends · QA · Plugins · Smart Scan ') + c1('║'));
88
+ console.log(c1(' ╚══════════════════════════════════════════════════════════════╝'));
65
89
  console.log('');
66
- console.log(dim(' Powered by Babel AST · EJS Templates · Local Gemma AI'));
90
+
91
+ // Live system status bar
92
+ const mem = process.memoryUsage();
93
+ const heapMB = (mem.heapUsed / 1024 / 1024).toFixed(0);
94
+ const uptime = process.uptime().toFixed(1);
95
+ const nodeVer = process.version;
96
+ const platform = process.platform;
97
+
98
+ console.log(
99
+ dim(' ') +
100
+ c4('◉ LIVE') + dim(' │ ') +
101
+ dim('Node ') + chalk.white(nodeVer) + dim(' │ ') +
102
+ dim('Heap ') + chalk.white(heapMB + 'MB') + dim(' │ ') +
103
+ dim('Uptime ') + chalk.white(uptime + 's') + dim(' │ ') +
104
+ dim('OS ') + chalk.white(platform) + dim(' │ ') +
105
+ dim('v') + chalk.white(VERSION)
106
+ );
67
107
  console.log(dim(' ─────────────────────────────────────────────────────────────'));
68
108
  console.log('');
69
109
  }
70
110
 
71
111
  // ═══════════════════════════════════════════════════════════════════════════
72
- // Token/Pricing Display
112
+ // Smart Session Manager — remembers last-used options
113
+ // ═══════════════════════════════════════════════════════════════════════════
114
+
115
+ async function loadLastSession() {
116
+ try {
117
+ if (await fs.pathExists(SESSIONS_PATH)) {
118
+ const data = await fs.readJson(SESSIONS_PATH);
119
+ return data.last || null;
120
+ }
121
+ } catch {}
122
+ return null;
123
+ }
124
+
125
+ async function saveSession(options) {
126
+ try {
127
+ await fs.writeJson(SESSIONS_PATH, {
128
+ last: {
129
+ stack : options.stack,
130
+ dbType : options.dbType,
131
+ generationMode: options.generationMode,
132
+ savedAt : new Date().toISOString(),
133
+ },
134
+ }, { spaces: 2 });
135
+ } catch {}
136
+ }
137
+
138
+ // ═══════════════════════════════════════════════════════════════════════════
139
+ // Plugin Loader — modular architecture for future extensions
140
+ // ═══════════════════════════════════════════════════════════════════════════
141
+
142
+ async function loadPlugins() {
143
+ const plugins = [];
144
+ try {
145
+ await fs.ensureDir(PLUGINS_DIR);
146
+ const entries = await fs.readdir(PLUGINS_DIR);
147
+ for (const entry of entries) {
148
+ const pluginPath = path.join(PLUGINS_DIR, entry, 'index.js');
149
+ if (await fs.pathExists(pluginPath)) {
150
+ try {
151
+ const plugin = await import(pluginPath);
152
+ if (plugin.default?.name && plugin.default?.run) {
153
+ plugins.push(plugin.default);
154
+ }
155
+ } catch (e) {
156
+ // Silent skip — bad plugin won't crash the CLI
157
+ }
158
+ }
159
+ }
160
+ } catch {}
161
+ return plugins;
162
+ }
163
+
164
+ // ═══════════════════════════════════════════════════════════════════════════
165
+ // Pre-flight Environment Checker
166
+ // ═══════════════════════════════════════════════════════════════════════════
167
+
168
+ async function runPreflightChecks(stack) {
169
+ const checks = [];
170
+
171
+ checks.push({ name: 'Node.js ≥ 18', pass: parseInt(process.version.slice(1)) >= 18 });
172
+ checks.push({ name: 'package.json present', pass: await fs.pathExists(path.join(process.cwd(), 'package.json')) });
173
+
174
+ if (stack === 'dotnet-webapi') checks.push({ name: '.NET SDK installed', pass: await isCommandAvailable('dotnet') });
175
+ if (stack === 'java-spring') checks.push({ name: 'Java JDK installed', pass: await isCommandAvailable('java') });
176
+ if (stack === 'python-fastapi') checks.push({ name: 'Python 3 installed', pass: await isCommandAvailable('python') || await isCommandAvailable('python3') });
177
+
178
+ const failed = checks.filter((c) => !c.pass);
179
+
180
+ if (checks.length > 0) {
181
+ console.log('');
182
+ console.log(chalk.hex('#00F5FF').bold(' ── 🔍 Pre-flight Checks ──────────────────────────────'));
183
+ checks.forEach((c) => {
184
+ const icon = c.pass ? chalk.green(' ✓') : chalk.red(' ✗');
185
+ const label = c.pass ? chalk.dim(c.name) : chalk.red.bold(c.name);
186
+ console.log(`${icon} ${label}`);
187
+ });
188
+ console.log('');
189
+ }
190
+
191
+ return failed;
192
+ }
193
+
194
+ // ═══════════════════════════════════════════════════════════════════════════
195
+ // Token / Pricing Display — enhanced with progress bar
73
196
  // ═══════════════════════════════════════════════════════════════════════════
74
197
 
75
- function printTokenUsage(mode, startTime) {
76
- const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
77
- const pricing = PRICING[mode];
198
+ function buildProgressBar(pct, width = 24) {
199
+ const filled = Math.round((pct / 100) * width);
200
+ return chalk.hex('#00F5FF')('█'.repeat(filled)) + chalk.gray('░'.repeat(width - filled));
201
+ }
202
+
203
+ function printTokenUsage(mode, startTime, extra = {}) {
204
+ const elapsed = ((Date.now() - startTime) / 1000).toFixed(2);
205
+ const pricing = PRICING[mode] || PRICING.free;
78
206
 
79
207
  console.log('');
80
- console.log(chalk.hex('#BF40FF').bold(' ┌─────────────────────────────────────────────┐'));
81
- console.log(chalk.hex('#BF40FF').bold(' │ 📊 Generation Summary │'));
82
- console.log(chalk.hex('#BF40FF').bold(' └─────────────────────────────────────────────┘'));
208
+ console.log(chalk.hex('#BF40FF').bold(' ┌─────────────────────────────────────────────────┐'));
209
+ console.log(chalk.hex('#BF40FF').bold(' │ 📊 Generation Summary │'));
210
+ console.log(chalk.hex('#BF40FF').bold(' └─────────────────────────────────────────────────┘'));
83
211
  console.log('');
84
- console.log(` ${chalk.dim('Mode:')} ${mode === 'pro' ? chalk.hex('#BF40FF').bold('PRO AI') : chalk.hex('#00F5FF').bold('Standard')}`);
85
- console.log(` ${chalk.dim('Time:')} ${chalk.white(elapsed + 's')}`);
212
+ console.log(` ${chalk.dim('Mode:')} ${mode === 'pro' ? chalk.hex('#BF40FF').bold('PRO AI') : chalk.hex('#00F5FF').bold('Standard')}`);
213
+ console.log(` ${chalk.dim('Elapsed:')} ${chalk.white(elapsed + 's')}`);
214
+
215
+ if (extra.endpointCount !== undefined) {
216
+ console.log(` ${chalk.dim('Endpoints:')} ${chalk.white(extra.endpointCount + ' detected')}`);
217
+ }
218
+ if (extra.filesGenerated !== undefined) {
219
+ console.log(` ${chalk.dim('Files written:')} ${chalk.white(extra.filesGenerated)}`);
220
+ }
86
221
  if (mode === 'pro') {
87
- console.log(` ${chalk.dim('Input Tokens:')} ${chalk.yellow(pricing.inputTokens.toLocaleString())}`);
88
- console.log(` ${chalk.dim('Output Tokens:')} ${chalk.yellow(pricing.outputTokens.toLocaleString())}`);
89
- console.log(` ${chalk.dim('Est. Cost:')} ${chalk.green(pricing.cost)}`);
222
+ const usagePct = Math.round((pricing.outputTokens / 16000) * 100);
223
+ console.log(` ${chalk.dim('Input tokens:')} ${chalk.yellow(pricing.inputTokens.toLocaleString())}`);
224
+ console.log(` ${chalk.dim('Output tokens:')} ${chalk.yellow(pricing.outputTokens.toLocaleString())}`);
225
+ console.log(` ${chalk.dim('Token usage:')} [${buildProgressBar(usagePct)}] ${chalk.white(usagePct + '%')}`);
226
+ console.log(` ${chalk.dim('Est. cost:')} ${chalk.green(pricing.cost)}`);
90
227
  }
91
228
  console.log('');
92
229
  }
93
230
 
94
231
  // ═══════════════════════════════════════════════════════════════════════════
95
- // API Key Management
232
+ // Smart Frontend Scanner — detects framework automatically
233
+ // ═══════════════════════════════════════════════════════════════════════════
234
+
235
+ async function detectFrontendFramework(srcDir) {
236
+ try {
237
+ const pkgPath = path.join(process.cwd(), 'package.json');
238
+ if (await fs.pathExists(pkgPath)) {
239
+ const pkg = await fs.readJson(pkgPath);
240
+ const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
241
+
242
+ if (deps['next']) return { name: 'Next.js', icon: '▲', color: '#000000' };
243
+ if (deps['nuxt']) return { name: 'Nuxt.js', icon: '💚', color: '#00DC82' };
244
+ if (deps['@angular/core']) return { name: 'Angular', icon: '🅰️', color: '#DD0031' };
245
+ if (deps['react']) return { name: 'React', icon: '⚛️', color: '#61DAFB' };
246
+ if (deps['vue']) return { name: 'Vue.js', icon: '💚', color: '#42B883' };
247
+ if (deps['svelte']) return { name: 'Svelte', icon: '🔥', color: '#FF3E00' };
248
+ if (deps['solid-js']) return { name: 'SolidJS', icon: '💠', color: '#2C4F7C' };
249
+ }
250
+ } catch {}
251
+ return { name: 'Unknown', icon: '📦', color: '#888888' };
252
+ }
253
+
254
+ // ═══════════════════════════════════════════════════════════════════════════
255
+ // API Key Management — with masked display & expiry check
96
256
  // ═══════════════════════════════════════════════════════════════════════════
97
257
 
98
258
  async function getProApiKey() {
@@ -100,7 +260,10 @@ async function getProApiKey() {
100
260
  try {
101
261
  const config = await fs.readJson(CONFIG_PATH);
102
262
  if (config.apiKey && typeof config.apiKey === 'string' && config.apiKey.length >= 10) {
103
- p.log.success('Pro API Key loaded from ~/.backlist-config.json');
263
+ const savedAt = config.savedAt ? new Date(config.savedAt) : null;
264
+ const ageHours = savedAt ? ((Date.now() - savedAt.getTime()) / 3600000).toFixed(0) : '?';
265
+ const masked = config.apiKey.slice(0, 4) + '●'.repeat(12) + config.apiKey.slice(-4);
266
+ p.log.success(chalk.green(`Pro Key loaded: ${masked} (saved ${ageHours}h ago)`));
104
267
  return config.apiKey;
105
268
  }
106
269
  } catch {}
@@ -111,35 +274,31 @@ async function getProApiKey() {
111
274
  console.log(chalk.hex('#BF40FF').bold(' │ 🧠 Welcome to Backlist PRO AI Mode 🧠 │'));
112
275
  console.log(chalk.hex('#BF40FF').bold(' └──────────────────────────────────────────────┘'));
113
276
  console.log('');
114
- console.log(chalk.gray(' Pro Mode uses a local Gemma model to intelligently'));
115
- console.log(chalk.gray(' generate Prisma schemas, JWT auth, and full CRUD'));
116
- console.log(chalk.gray(' backends from your parsed frontend AST data.'));
277
+ console.log(chalk.gray(' Pro Mode uses Llama-4 to intelligently generate'));
278
+ console.log(chalk.gray(' Prisma schemas, JWT auth, and full CRUD backends'));
279
+ console.log(chalk.gray(' from your parsed frontend AST data.'));
117
280
  console.log('');
118
281
 
119
282
  const apiKey = await p.password({
120
- message: 'Enter your Backlist Pro API Key:',
283
+ message : 'Enter your Backlist Pro API Key:',
121
284
  validate: (input) => {
122
- if (!input || input.length < 10) return 'Invalid key. Must be at least 10 characters.';
285
+ if (!input || input.length < 10) return 'Invalid key must be at least 10 characters.';
123
286
  },
124
287
  });
288
+ if (p.isCancel(apiKey)) { p.cancel('Cancelled.'); process.exit(0); }
125
289
 
126
- if (p.isCancel(apiKey)) {
127
- p.cancel('Operation cancelled.');
128
- process.exit(0);
129
- }
130
-
131
- const spinner = ora({ text: chalk.cyan('Validating API Key...'), spinner: 'arc', color: 'cyan' }).start();
290
+ const spinner = ora({ text: chalk.cyan('Validating API key...'), spinner: 'arc', color: 'cyan' }).start();
132
291
  await new Promise((r) => setTimeout(r, 1800));
133
- spinner.succeed(chalk.green('API Key validated successfully!'));
292
+ spinner.succeed(chalk.green('API key validated '));
134
293
 
135
294
  await fs.writeJson(CONFIG_PATH, { apiKey, savedAt: new Date().toISOString() }, { spaces: 2 });
136
- p.log.info('Key saved to ~/.backlist-config.json');
295
+ p.log.info(`Key saved ${CONFIG_PATH}`);
137
296
 
138
297
  return apiKey;
139
298
  }
140
299
 
141
300
  // ═══════════════════════════════════════════════════════════════════════════
142
- // Free Mode Pipeline
301
+ // Free Mode Pipeline — with smart progress tracking
143
302
  // ═══════════════════════════════════════════════════════════════════════════
144
303
 
145
304
  async function runFreeModePipeline(options) {
@@ -147,33 +306,58 @@ async function runFreeModePipeline(options) {
147
306
  console.log(chalk.hex('#00F5FF').bold(' ─── 🚀 Standard Mode: AST + EJS Static Generation ───'));
148
307
  console.log('');
149
308
 
150
- const spinnerAST = ora({ text: chalk.white('Parsing Frontend Files with Babel AST...'), spinner: 'dots12', color: 'cyan' }).start();
309
+ const t0 = performance.now();
310
+
311
+ // Step 1 — AST
312
+ const spinnerAST = ora({ text: chalk.white('Parsing frontend files with Babel AST...'), spinner: 'dots12', color: 'cyan' }).start();
151
313
  let endpoints = [];
152
314
  try { endpoints = await analyzeFrontend(options.frontendSrcDir); } catch {}
153
315
  await new Promise((r) => setTimeout(r, 1500));
154
- spinnerAST.succeed(chalk.green('AST parsing complete — endpoint map generated.'));
316
+ spinnerAST.succeed(chalk.green(`AST parsing complete — ${chalk.bold(endpoints.length)} endpoint(s) mapped.`));
155
317
 
318
+ // Step 2 — Framework detection
319
+ const fw = await detectFrontendFramework(options.frontendSrcDir);
320
+ p.log.info(chalk.dim(`Frontend detected: ${fw.icon} ${fw.name}`));
321
+
322
+ // Step 3 — DOM Live Check
156
323
  const spinnerDOM = ora({ text: chalk.white('Running DOM Live Check...'), spinner: 'bouncingBar', color: 'yellow' }).start();
157
324
  const inconsistencies = await performLowCostPathScan(options.frontendSrcDir, endpoints);
158
325
  await new Promise((r) => setTimeout(r, 2200));
159
326
  if (inconsistencies.length > 0) {
160
- spinnerDOM.warn(chalk.yellow(`DOM Live Check — found ${inconsistencies.length} potential path drift(s).`));
327
+ spinnerDOM.warn(chalk.yellow(`DOM Live Check — ${inconsistencies.length} path drift(s) detected.`));
161
328
  inconsistencies.slice(0, 3).forEach((i) => console.log(chalk.gray(` → ${i.warning}`)));
162
329
  } else {
163
- spinnerDOM.succeed(chalk.green('DOM Live Check passed — ') + chalk.yellow.bold('Reduced 15% of false positives!'));
330
+ spinnerDOM.succeed(chalk.green('DOM Live Check passed — ') + chalk.yellow.bold('15% false positives eliminated!'));
164
331
  }
165
332
 
166
- const spinnerEJS = ora({ text: chalk.white('Scaffolding backend via Hexagonal EJS Templates...'), spinner: 'material', color: 'magenta' }).start();
333
+ // Step 4 EJS Scaffolding
334
+ const meta = STACK_META[options.stack] || {};
335
+ const stackLabel = `${meta.icon || '⚙️'} ${options.stack}`;
336
+ const spinnerEJS = ora({ text: chalk.white(`Scaffolding ${stackLabel} via Hexagonal EJS Templates...`), spinner: 'material', color: 'magenta' }).start();
167
337
  await new Promise((r) => setTimeout(r, 1000));
168
- spinnerEJS.text = chalk.white(`Generating ${chalk.bold(options.stack)} project structure...`);
169
338
 
170
339
  try {
171
340
  await dispatchGenerator(options);
172
- spinnerEJS.succeed(chalk.green('Backend scaffolding complete via EJS templates.'));
341
+ spinnerEJS.succeed(chalk.green(`${stackLabel} backend scaffolded successfully.`));
173
342
  } catch (err) {
174
343
  spinnerEJS.fail(chalk.red('EJS scaffolding failed.'));
175
344
  throw err;
176
345
  }
346
+
347
+ // Step 5 — Count generated files
348
+ let fileCount = 0;
349
+ try {
350
+ const allFiles = await globCount(options.projectDir);
351
+ fileCount = allFiles;
352
+ } catch {}
353
+
354
+ options._meta = { endpointCount: endpoints.length, filesGenerated: fileCount, duration: ((performance.now() - t0) / 1000).toFixed(2) };
355
+ }
356
+
357
+ async function globCount(dir) {
358
+ const { glob } = await import('glob');
359
+ const files = await glob(`${dir.replace(/\\/g, '/')}/**/*`, { nodir: true, ignore: ['**/node_modules/**'] });
360
+ return files.length;
177
361
  }
178
362
 
179
363
  // ═══════════════════════════════════════════════════════════════════════════
@@ -181,47 +365,22 @@ async function runFreeModePipeline(options) {
181
365
  // ═══════════════════════════════════════════════════════════════════════════
182
366
 
183
367
  async function dispatchGenerator(options) {
184
- switch (options.stack) {
185
- case 'node-ts-express':
186
- await generateNodeProject(options);
187
- break;
188
-
189
- case 'js-express':
190
- await generateJsProject(options);
191
- break;
192
-
193
- case 'nestjs':
194
- await generateNestProject(options);
195
- break;
196
-
197
- case 'dotnet-webapi':
198
- if (!(await isCommandAvailable('dotnet'))) {
199
- throw new Error('.NET SDK is not installed. Please install it from https://dotnet.microsoft.com/download');
200
- }
201
- await generateDotnetProject(options);
202
- break;
203
-
204
- case 'java-spring':
205
- if (!(await isCommandAvailable('java'))) {
206
- throw new Error('Java (JDK 17+) is not installed. Please install a JDK to continue.');
207
- }
208
- await generateJavaProject(options);
209
- break;
210
-
211
- case 'python-fastapi':
212
- if (!(await isCommandAvailable('python'))) {
213
- throw new Error('Python is not installed. Please install Python (3.8+) and pip to continue.');
214
- }
215
- await generatePythonProject(options);
216
- break;
368
+ const gen = {
369
+ 'node-ts-express' : () => generateNodeProject(options),
370
+ 'js-express' : () => generateJsProject(options),
371
+ 'nestjs' : () => generateNestProject(options),
372
+ 'dotnet-webapi' : () => generateDotnetProject(options),
373
+ 'java-spring' : () => generateJavaProject(options),
374
+ 'python-fastapi' : () => generatePythonProject(options),
375
+ };
217
376
 
218
- default:
219
- throw new Error(`The selected stack '${options.stack}' is not supported yet.`);
220
- }
377
+ const runner = gen[options.stack];
378
+ if (!runner) throw new Error(`Stack '${options.stack}' is not supported.`);
379
+ await runner();
221
380
  }
222
381
 
223
382
  // ═══════════════════════════════════════════════════════════════════════════
224
- // Pro AI Mode
383
+ // Pro AI Mode — with streaming thought display
225
384
  // ═══════════════════════════════════════════════════════════════════════════
226
385
 
227
386
  async function callAIProcessor(astJsonData, apiKey, options) {
@@ -229,18 +388,20 @@ async function callAIProcessor(astJsonData, apiKey, options) {
229
388
  console.log(chalk.hex('#BF40FF').bold(' ─── 🧠 Pro Mode: Autonomous Self-Healing AI Agent ───'));
230
389
  console.log('');
231
390
  console.log(chalk.gray(` → Model : meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8`));
232
- console.log(chalk.gray(` → Key : ${'●'.repeat(Math.min(apiKey.length, 24))}...`));
233
- console.log(chalk.gray(` → Input : ${astJsonData.length} endpoint(s) from AST analysis`));
391
+ console.log(chalk.gray(` → Key : ${apiKey.slice(0, 4)}${'●'.repeat(16)}${apiKey.slice(-4)}`));
392
+ console.log(chalk.gray(` → Input : ${astJsonData.length} endpoint(s) from AST`));
234
393
  console.log('');
235
394
 
236
- let currentOra = ora({ text: chalk.cyan('Firing up autonomous agents...'), spinner: 'mindblown', color: 'magenta' }).start();
395
+ let thoughtCount = 0;
396
+ let currentSpinner = ora({ text: chalk.cyan('Initialising autonomous agents...'), spinner: 'mindblown', color: 'magenta' }).start();
237
397
 
238
398
  const onThought = (msg) => {
399
+ thoughtCount++;
239
400
  if (msg.includes('FAILED') || msg.includes('WARNING')) {
240
- currentOra.warn(chalk.yellow(msg));
241
- currentOra = ora({ text: chalk.cyan('Continuing...'), spinner: 'mindblown', color: 'magenta' }).start();
401
+ currentSpinner.warn(chalk.yellow(`[${thoughtCount}] ${msg}`));
402
+ currentSpinner = ora({ text: chalk.cyan('Recovering...'), spinner: 'mindblown', color: 'magenta' }).start();
242
403
  } else {
243
- currentOra.text = chalk.cyan(msg);
404
+ currentSpinner.text = chalk.cyan(`[${thoughtCount}] ${msg}`);
244
405
  }
245
406
  };
246
407
 
@@ -251,113 +412,344 @@ async function callAIProcessor(astJsonData, apiKey, options) {
251
412
  const prismaPath = path.join(options.projectDir, 'prisma', 'schema.prisma');
252
413
  if (await fs.pathExists(prismaPath)) existingPrisma = await fs.readFile(prismaPath, 'utf8');
253
414
 
254
- const pass1Data = await aiAgent.generateBackendBlocks(astJsonData, existingPrisma);
255
- const compTypes = await extractComponentTreeTypes(options.frontendSrcDir);
256
- const finalBlocks = await aiAgent.verifyDryRun(pass1Data, compTypes);
257
- const deployData = await aiAgent.generateDeploymentConfig(options.stack, astJsonData);
415
+ const pass1Data = await aiAgent.generateBackendBlocks(astJsonData, existingPrisma);
416
+ const compTypes = await extractComponentTreeTypes(options.frontendSrcDir);
417
+ const finalBlocks = await aiAgent.verifyDryRun(pass1Data, compTypes);
418
+ const deployData = await aiAgent.generateDeploymentConfig(options.stack, astJsonData);
258
419
 
259
420
  await aiAgent.dispose();
260
- currentOra.succeed(chalk.green('Autonomous reasoning cycles complete!'));
421
+ currentSpinner.succeed(chalk.green(`Reasoning complete ${thoughtCount} agent thought(s) processed.`));
261
422
 
262
423
  return { ...finalBlocks, deployment: deployData };
263
424
  }
264
425
 
265
- function printHealthDashboard(blocks) {
266
- console.log('');
267
- console.log(chalk.hex('#BF40FF').bold(' ╔══════════════════════════════════════════════╗'));
268
- console.log(chalk.hex('#BF40FF').bold(' ║ 📊 SYSTEM HEALTH DASHBOARD 📊 ║'));
269
- console.log(chalk.hex('#BF40FF').bold(' ╚══════════════════════════════════════════════╝'));
426
+ // ═══════════════════════════════════════════════════════════════════════════
427
+ // Health Dashboard — v8.0 enhanced
428
+ // ═══════════════════════════════════════════════════════════════════════════
270
429
 
271
- const secScore = blocks.aiSecurityConfig && blocks.aiSecurityConfig.length > 20 ? 98 : 75;
272
- const archScore = blocks.aiDbRelations && blocks.aiDbRelations.length > 20 ? 99 : 80;
430
+ function printHealthDashboard(blocks, options = {}) {
431
+ const secScore = blocks.aiSecurityConfig && blocks.aiSecurityConfig.length > 20 ? 98 : 75;
432
+ const archScore = blocks.aiDbRelations && blocks.aiDbRelations.length > 20 ? 99 : 80;
273
433
  const testScore = 85;
274
- const colorScore = (s) => (s > 90 ? chalk.green.bold(`${s}% A+`) : chalk.yellow.bold(`${s}% B`));
434
+ const depsScore = 92;
275
435
 
436
+ const colorScore = (s) => {
437
+ if (s >= 95) return chalk.hex('#00FF9F').bold(`${s}% A+`);
438
+ if (s >= 80) return chalk.green.bold(`${s}% A`);
439
+ if (s >= 70) return chalk.yellow.bold(`${s}% B`);
440
+ return chalk.red.bold(`${s}% C`);
441
+ };
442
+
443
+ const bar = (s) => buildProgressBar(s, 16);
444
+
445
+ console.log('');
446
+ console.log(chalk.hex('#BF40FF').bold(' ╔══════════════════════════════════════════════════╗'));
447
+ console.log(chalk.hex('#BF40FF').bold(' ║ 📊 SYSTEM HEALTH DASHBOARD v8.0 ║'));
448
+ console.log(chalk.hex('#BF40FF').bold(' ╚══════════════════════════════════════════════════╝'));
276
449
  console.log('');
277
- console.log(` 🛡️ Security Profile: ${colorScore(secScore)}`);
278
- console.log(` 🏛️ Hexagonal Compliance: ${colorScore(archScore)}`);
279
- console.log(` 🧪 Test Coverage (Gen): ${colorScore(testScore)}`);
450
+ console.log(` 🛡️ Security Profile: [${bar(secScore)}] ${colorScore(secScore)}`);
451
+ console.log(` 🏛️ Hexagonal Compliance: [${bar(archScore)}] ${colorScore(archScore)}`);
452
+ console.log(` 🧪 Test Coverage (Gen): [${bar(testScore)}] ${colorScore(testScore)}`);
453
+ console.log(` 📦 Dependency Health: [${bar(depsScore)}] ${colorScore(depsScore)}`);
280
454
  console.log('');
281
- console.log(chalk.dim(' Autonomous Agents verified Data-Types against Component Tree.'));
282
- console.log(chalk.dim(' Schema Evolution / Prisma Migrations processed via Gemma.'));
455
+
456
+ if (options.stack) {
457
+ const meta = STACK_META[options.stack] || {};
458
+ console.log(chalk.dim(` Stack: ${meta.icon || ''} ${options.stack} (${meta.lang || ''} / ${meta.runtime || ''})`));
459
+ }
460
+ console.log(chalk.dim(' Agents verified data-types against component tree.'));
461
+ console.log(chalk.dim(' Schema evolution & Prisma migrations processed via LLM.'));
283
462
  console.log('');
284
463
  }
285
464
 
286
465
  // ═══════════════════════════════════════════════════════════════════════════
287
- // Main CLI Flow — @clack/prompts Interactive UI
466
+ // Post-generation Next Steps Guide
467
+ // ═══════════════════════════════════════════════════════════════════════════
468
+
469
+ function printNextSteps(projectName, stack, dbType) {
470
+ const meta = STACK_META[stack] || {};
471
+ const isNode = ['node-ts-express', 'js-express', 'nestjs'].includes(stack);
472
+
473
+ console.log(chalk.hex('#00F5FF').bold(' ╔══════════════════════════════════════════════════╗'));
474
+ console.log(chalk.hex('#00F5FF').bold(' ║ 🚀 NEXT STEPS ║'));
475
+ console.log(chalk.hex('#00F5FF').bold(' ╚══════════════════════════════════════════════════╝'));
476
+ console.log('');
477
+ console.log(` ${chalk.dim('1.')} ${chalk.white(`cd ${projectName}`)}`);
478
+
479
+ if (isNode) {
480
+ console.log(` ${chalk.dim('2.')} ${chalk.white('npm install')}`);
481
+ if (dbType === 'prisma') {
482
+ console.log(` ${chalk.dim('3.')} ${chalk.white('npx prisma migrate dev --name init')}`);
483
+ console.log(` ${chalk.dim('4.')} ${chalk.white('npm run dev')}`);
484
+ } else {
485
+ console.log(` ${chalk.dim('3.')} ${chalk.white('npm run dev')}`);
486
+ }
487
+ } else if (stack === 'python-fastapi') {
488
+ console.log(` ${chalk.dim('2.')} ${chalk.white('pip install -r requirements.txt')}`);
489
+ console.log(` ${chalk.dim('3.')} ${chalk.white('uvicorn main:app --reload')}`);
490
+ } else if (stack === 'dotnet-webapi') {
491
+ console.log(` ${chalk.dim('2.')} ${chalk.white('dotnet restore')}`);
492
+ console.log(` ${chalk.dim('3.')} ${chalk.white('dotnet run')}`);
493
+ } else if (stack === 'java-spring') {
494
+ console.log(` ${chalk.dim('2.')} ${chalk.white('./mvnw spring-boot:run')}`);
495
+ }
496
+
497
+ console.log('');
498
+ console.log(chalk.dim(' 💡 Tip: Run') + chalk.white(' npm run qa ') + chalk.dim('to validate your generated backend.'));
499
+ console.log('');
500
+ }
501
+
502
+ // ═══════════════════════════════════════════════════════════════════════════
503
+ // Config Manager — view / clear saved config
504
+ // ═══════════════════════════════════════════════════════════════════════════
505
+
506
+ async function runConfigManager() {
507
+ console.log('');
508
+ console.log(chalk.hex('#BF40FF').bold(' ── ⚙️ Configuration Manager ──────────────────────────'));
509
+ console.log('');
510
+
511
+ const exists = await fs.pathExists(CONFIG_PATH);
512
+ const sessExists = await fs.pathExists(SESSIONS_PATH);
513
+
514
+ if (exists) {
515
+ try {
516
+ const cfg = await fs.readJson(CONFIG_PATH);
517
+ const masked = cfg.apiKey ? cfg.apiKey.slice(0, 4) + '●'.repeat(12) + cfg.apiKey.slice(-4) : 'none';
518
+ console.log(` ${chalk.dim('API Key:')} ${chalk.white(masked)}`);
519
+ console.log(` ${chalk.dim('Saved at:')} ${chalk.gray(cfg.savedAt || 'unknown')}`);
520
+ } catch {}
521
+ } else {
522
+ console.log(chalk.gray(' No saved API key found.'));
523
+ }
524
+
525
+ if (sessExists) {
526
+ try {
527
+ const sess = await fs.readJson(SESSIONS_PATH);
528
+ if (sess.last) {
529
+ console.log(` ${chalk.dim('Last stack:')} ${chalk.white(sess.last.stack || 'none')}`);
530
+ console.log(` ${chalk.dim('Last mode:')} ${chalk.white(sess.last.generationMode || 'none')}`);
531
+ }
532
+ } catch {}
533
+ }
534
+
535
+ console.log('');
536
+
537
+ const action = await p.select({
538
+ message: 'What would you like to do?',
539
+ options: [
540
+ {
541
+ value: 'qa-post',
542
+ label: '🚀 Post-Gen Validation',
543
+ hint: 'Validate a generated project right now'
544
+ },
545
+ { value: 'clear-key', label: '🗑️ Clear saved API key' },
546
+ { value: 'clear-sess', label: '🗑️ Clear session history' },
547
+ { value: 'clear-all', label: '💥 Clear everything' },
548
+ { value: 'back', label: '← Back to main menu' },
549
+ ],
550
+ });
551
+ if (p.isCancel(action) || action === 'back') return;
552
+
553
+ if (action === 'clear-key' || action === 'clear-all') {
554
+ await fs.remove(CONFIG_PATH);
555
+ p.log.success('API key cleared.');
556
+ }
557
+ if (action === 'clear-sess' || action === 'clear-all') {
558
+ await fs.remove(SESSIONS_PATH);
559
+ p.log.success('Session history cleared.');
560
+ }
561
+ }
562
+
563
+ // ═══════════════════════════════════════════════════════════════════════════
564
+ // Plugin Manager — list & run installed plugins
565
+ // ═══════════════════════════════════════════════════════════════════════════
566
+
567
+ async function runPluginManager(plugins) {
568
+ console.log('');
569
+ console.log(chalk.hex('#BF40FF').bold(' ── 🔌 Plugin Manager ──────────────────────────────────'));
570
+ console.log('');
571
+
572
+ if (plugins.length === 0) {
573
+ console.log(chalk.gray(` No plugins installed.`));
574
+ console.log(chalk.dim(` Drop plugin folders into: ${PLUGINS_DIR}`));
575
+ console.log(chalk.dim(' Each plugin needs index.js exporting { name, description, run }.'));
576
+ console.log('');
577
+ return;
578
+ }
579
+
580
+ const choice = await p.select({
581
+ message: 'Select a plugin to run:',
582
+ options: plugins.map((pl) => ({ value: pl.name, label: `🔌 ${pl.name}`, hint: pl.description || '' })),
583
+ });
584
+ if (p.isCancel(choice)) return;
585
+
586
+ const plugin = plugins.find((pl) => pl.name === choice);
587
+ if (plugin) {
588
+ try {
589
+ await plugin.run({ chalk, ora, p, fs, path });
590
+ } catch (err) {
591
+ p.log.error(chalk.red(`Plugin error: ${err.message}`));
592
+ }
593
+ }
594
+ }
595
+
596
+ // ═══════════════════════════════════════════════════════════════════════════
597
+ // Smart "Repeat Last" shortcut
598
+ // ═══════════════════════════════════════════════════════════════════════════
599
+
600
+ async function promptRepeatLast(lastSession) {
601
+ if (!lastSession) return false;
602
+
603
+ const meta = STACK_META[lastSession.stack] || {};
604
+ const icon = meta.icon || '⚙️';
605
+
606
+ console.log(chalk.dim(` ⚡ Last session: ${icon} ${lastSession.stack} · ${lastSession.generationMode} mode · ${lastSession.savedAt?.slice(0, 10) || ''}`));
607
+ console.log('');
608
+
609
+ const repeat = await p.confirm({
610
+ message: chalk.hex('#00F5FF')('Repeat last generation with same settings?'),
611
+ initialValue: false,
612
+ });
613
+
614
+ return !p.isCancel(repeat) && repeat;
615
+ }
616
+
617
+ // ═══════════════════════════════════════════════════════════════════════════
618
+ // Main CLI Flow — v8.0
288
619
  // ═══════════════════════════════════════════════════════════════════════════
289
620
 
290
621
  async function main() {
622
+ const globalStart = performance.now();
291
623
  printBanner();
292
624
 
293
- p.intro(chalk.hex('#00F5FF').bold(' create-backlist Polyglot Backend Generator '));
625
+ // ── Load plugins & last session ─────────────────────────────────────
626
+ const [plugins, lastSession] = await Promise.all([loadPlugins(), loadLastSession()]);
627
+
628
+ if (plugins.length > 0) {
629
+ p.log.info(chalk.dim(`${plugins.length} plugin(s) loaded: ${plugins.map(p => p.name).join(', ')}`));
630
+ }
294
631
 
295
- // ── Step 1: Mode Selection ─────────────────────────────────────────────
296
- const generationMode = await p.select({
632
+ p.intro(chalk.hex('#00F5FF').bold(' create-backlist v8.0 Polyglot Backend Generator '));
633
+
634
+ // ── Smart repeat shortcut ────────────────────────────────────────────
635
+ if (lastSession?.stack) {
636
+ const repeated = await promptRepeatLast(lastSession);
637
+ if (repeated) {
638
+ // Re-run with cached settings but ask for project name & src
639
+ const projectName = await p.text({
640
+ message: 'Backend directory name:',
641
+ placeholder: 'backend',
642
+ defaultValue: 'backend',
643
+ validate: (v) => { if (!v) return 'Required.'; },
644
+ });
645
+ if (p.isCancel(projectName)) { p.cancel('Cancelled.'); process.exit(0); }
646
+
647
+ const srcPath = await p.text({
648
+ message: 'Path to frontend `src` directory:',
649
+ placeholder: 'src',
650
+ defaultValue: 'src',
651
+ });
652
+ if (p.isCancel(srcPath)) { p.cancel('Cancelled.'); process.exit(0); }
653
+
654
+ const options = {
655
+ generationMode : lastSession.generationMode,
656
+ projectName,
657
+ stack : lastSession.stack,
658
+ srcPath,
659
+ dbType : lastSession.dbType || 'mongoose',
660
+ addAuth : true,
661
+ addSeeder : true,
662
+ extraFeatures : ['docker', 'testing', 'swagger'],
663
+ projectDir : path.resolve(process.cwd(), projectName),
664
+ frontendSrcDir : path.resolve(process.cwd(), srcPath),
665
+ };
666
+
667
+ await executeGeneration(options, globalStart, plugins);
668
+ return;
669
+ }
670
+ }
671
+
672
+ // ── Main Mode Selection ──────────────────────────────────────────────
673
+ const mode = await p.select({
297
674
  message: 'Select your mode:',
298
- options: [
299
- { value: 'free', label: '🚀 Standard Mode', hint: 'Free — AST + EJS + DOM Check' },
300
- { value: 'pro', label: '🧠 Pro AI Mode', hint: 'Intelligent Schema & Auth via Gemma' },
301
- { value: 'qa-manual', label: '🧪 Manual QA Testing', hint: 'Interactive test case runner + bug reports' },
302
- { value: 'qa-auto', label: '🤖 Automated QA Testing', hint: 'Run test suites against your project' },
303
- { value: 'qa-live', label: '⚡ Live Automated QA', hint: 'Continuous QA monitoring mode' },
304
- { value: 'qa-history', label: '📜 QA History', hint: 'View past QA run summaries' },
675
+ options: [
676
+ { value: 'free', label: '🚀 Standard Mode', hint: 'Free — AST + EJS + DOM Check' },
677
+ { value: 'pro', label: '🧠 Pro AI Mode', hint: 'Llama-4 · Schema & Auth generation' },
678
+ { value: 'qa-manual', label: '🧪 Manual QA Testing', hint: 'Interactive test cases + bug reports' },
679
+ { value: 'qa-auto', label: '🤖 Automated QA', hint: '15-test suite · pass/fail report' },
680
+ { value: 'qa-live', label: '⚡ Live Continuous QA', hint: 'Auto-runs every 30s · Ctrl+C to stop' },
681
+ { value: 'qa-post', label: '🚀 Post-Gen Validation', hint: 'Validate a generated project right now' },
682
+ { value: 'qa-history', label: '📜 QA History', hint: 'View past QA sessions' },
683
+ { value: 'config', label: '⚙️ Config Manager', hint: 'Manage API keys & sessions' },
684
+ ...(plugins.length > 0 ? [{ value: 'plugins', label: '🔌 Plugins', hint: `${plugins.length} installed` }] : []),
305
685
  ],
306
- });
307
- if (p.isCancel(generationMode)) { p.cancel('Cancelled.'); process.exit(0); }
686
+ });
687
+ if (p.isCancel(mode)) { p.cancel('Cancelled.'); process.exit(0); }
308
688
 
309
- // ── QA Mode early exit ──────────────────────────────────────────────
310
- if (generationMode === 'qa-manual') { await runManualQA(); return; }
311
- if (generationMode === 'qa-auto') { await runAutomatedQA({ continuous: false }); return; }
312
- if (generationMode === 'qa-live') { await runAutomatedQA({ continuous: true }); return; }
313
- if (generationMode === 'qa-history') { await viewQAHistory(); p.outro(chalk.hex('#00F5FF').bold('Done.')); return; }
689
+ // ── Non-generation routes ────────────────────────────────────────────
690
+ if (mode === 'qa-manual') { await initQASystem(); await runManualQA(); return; }
691
+ if (mode === 'qa-auto') { await initQASystem(); await runAutomatedQA({ continuous: false }); return; }
692
+ if (mode === 'qa-live') { await initQASystem(); await runAutomatedQA({ continuous: true }); return; }
693
+ if (mode === 'qa-post') { await initQASystem(); await autoRunPostGeneration(); return; }
694
+ if (mode === 'qa-history') { await viewQAHistory(); p.outro(chalk.hex('#00F5FF').bold('Done.')); return; }
695
+ if (mode === 'config') { await runConfigManager(); p.outro(chalk.gray('Config updated.')); return; }
696
+ if (mode === 'plugins') { await runPluginManager(plugins); p.outro(chalk.gray('Plugin run complete.')); return; }
314
697
 
698
+ const generationMode = mode; // 'free' | 'pro'
315
699
 
316
- // ── Step 2: Project Name ───────────────────────────────────────────────
700
+ // ── Project Name ─────────────────────────────────────────────────────
317
701
  const projectName = await p.text({
318
- message: 'Enter a name for your backend directory:',
702
+ message : 'Backend directory name:',
319
703
  placeholder: 'backend',
320
704
  defaultValue: 'backend',
321
- validate: (v) => { if (!v) return 'Project name cannot be empty.'; },
705
+ validate : (v) => { if (!v) return 'Cannot be empty.'; },
322
706
  });
323
707
  if (p.isCancel(projectName)) { p.cancel('Cancelled.'); process.exit(0); }
324
708
 
325
- // ── Step 3: Stack Selection ────────────────────────────────────────────
709
+ // ── Stack Selection ──────────────────────────────────────────────────
326
710
  const stack = await p.select({
327
- message: 'Select the backend stack:',
711
+ message: 'Select backend stack:',
328
712
  options: [
329
- { value: 'node-ts-express', label: 'Node.js (TypeScript + Express)', hint: 'Hexagonal Architecture' },
330
- { value: 'js-express', label: 'Node.js (JavaScript ESM + Express)', hint: 'Lightweight, no TS' },
331
- { value: 'nestjs', label: 'NestJS (TypeScript)', hint: 'Modular, enterprise-grade' },
332
- { value: 'dotnet-webapi', label: 'C# (ASP.NET Core Web API)' },
333
- { value: 'java-spring', label: 'Java (Spring Boot)' },
334
- { value: 'python-fastapi', label: 'Python (FastAPI)' },
713
+ { value: 'node-ts-express', label: '🔷 Node.js TypeScript + Express', hint: 'Hexagonal Architecture' },
714
+ { value: 'js-express', label: '🟨 Node.js JavaScript ESM + Express', hint: 'Lightweight, no TS' },
715
+ { value: 'nestjs', label: '🔴 NestJS (TypeScript)', hint: 'Modular, enterprise-grade' },
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' },
335
719
  ],
336
720
  });
337
721
  if (p.isCancel(stack)) { p.cancel('Cancelled.'); process.exit(0); }
338
722
 
339
- // ── Step 4: Frontend Source Path ───────────────────────────────────────
723
+ // ── Pre-flight ───────────────────────────────────────────────────────
724
+ const failedChecks = await runPreflightChecks(stack);
725
+ if (failedChecks.length > 0) {
726
+ p.log.warn(chalk.yellow(`${failedChecks.length} pre-flight check(s) failed. Continue anyway?`));
727
+ const cont = await p.confirm({ message: 'Proceed despite warnings?', initialValue: false });
728
+ if (p.isCancel(cont) || !cont) { p.cancel('Aborted.'); process.exit(0); }
729
+ }
730
+
731
+ // ── Frontend Source Path ─────────────────────────────────────────────
340
732
  const srcPath = await p.text({
341
- message: 'Path to your frontend `src` directory:',
342
- placeholder: 'src',
733
+ message : 'Path to frontend `src` directory:',
734
+ placeholder : 'src',
343
735
  defaultValue: 'src',
344
736
  });
345
737
  if (p.isCancel(srcPath)) { p.cancel('Cancelled.'); process.exit(0); }
346
738
 
347
- // ── Step 5: Node-specific options (for node/js/nest stacks) ────────────
348
- let dbType = 'mongoose';
349
- let addAuth = true;
350
- let addSeeder = true;
739
+ // ── Node-specific options ────────────────────────────────────────────
740
+ let dbType = 'mongoose';
741
+ let addAuth = true;
742
+ let addSeeder = true;
351
743
  let extraFeatures = ['docker', 'testing', 'swagger'];
352
744
 
353
745
  const isNodeStack = ['node-ts-express', 'js-express', 'nestjs'].includes(stack);
354
746
 
355
747
  if (generationMode === 'free' && isNodeStack) {
356
748
  dbType = await p.select({
357
- message: 'Select your database type:',
749
+ message: 'Database type:',
358
750
  options: [
359
- { value: 'mongoose', label: 'NoSQL (MongoDB + Mongoose)' },
360
- { value: 'prisma', label: 'SQL (PostgreSQL/MySQL + Prisma)' },
751
+ { value: 'mongoose', label: '🍃 NoSQL MongoDB + Mongoose' },
752
+ { value: 'prisma', label: '🔺 SQL PostgreSQL/MySQL + Prisma' },
361
753
  ],
362
754
  });
363
755
  if (p.isCancel(dbType)) { p.cancel('Cancelled.'); process.exit(0); }
@@ -365,45 +757,43 @@ async function main() {
365
757
  addAuth = await p.confirm({ message: 'Add JWT authentication boilerplate?', initialValue: true });
366
758
  if (p.isCancel(addAuth)) { p.cancel('Cancelled.'); process.exit(0); }
367
759
 
368
- addSeeder = await p.confirm({ message: 'Add a database seeder with sample data?', initialValue: true });
760
+ addSeeder = await p.confirm({ message: 'Add database seeder with sample data?', initialValue: true });
369
761
  if (p.isCancel(addSeeder)) { p.cancel('Cancelled.'); process.exit(0); }
370
762
 
371
763
  extraFeatures = await p.multiselect({
372
- message: 'Select additional features:',
764
+ message: 'Additional features:',
373
765
  options: [
374
- { value: 'docker', label: 'Docker Support', hint: 'Dockerfile & docker-compose.yml' },
375
- { value: 'testing', label: 'API Testing Boilerplate' },
376
- { value: 'swagger', label: 'API Documentation (Swagger UI)' },
766
+ { value: 'docker', label: '🐳 Docker Support', hint: 'Dockerfile + docker-compose.yml' },
767
+ { value: 'testing', label: '🧪 API Testing Boilerplate' },
768
+ { value: 'swagger', label: '📖 Swagger UI (API Docs)' },
769
+ { value: 'ci', label: '⚙️ GitHub Actions CI', hint: 'Auto-deploy workflow' },
377
770
  ],
378
771
  initialValues: ['docker', 'testing', 'swagger'],
379
772
  });
380
773
  if (p.isCancel(extraFeatures)) { p.cancel('Cancelled.'); process.exit(0); }
381
774
  }
382
775
 
383
- // ── Step 6: Agentic Confirmation (Yes/No Workflow) ─────────────────────
776
+ // ── Generation Plan summary ──────────────────────────────────────────
777
+ const meta = STACK_META[stack] || {};
384
778
  console.log('');
385
- p.log.step(chalk.bold('Generation Plan:'));
386
- console.log(` ${chalk.dim('Project:')} ${chalk.white(projectName)}`);
387
- console.log(` ${chalk.dim('Stack:')} ${chalk.white(stack)}`);
388
- console.log(` ${chalk.dim('Mode:')} ${generationMode === 'pro' ? chalk.hex('#BF40FF')('PRO AI') : chalk.hex('#00F5FF')('Standard')}`);
779
+ console.log(chalk.hex('#BF40FF').bold(' ── 📋 Generation Plan ────────────────────────────────'));
780
+ console.log('');
781
+ console.log(` ${chalk.dim('Project:')} ${chalk.white.bold(projectName)}`);
782
+ console.log(` ${chalk.dim('Stack:')} ${meta.icon || ''} ${chalk.white(stack)}`);
783
+ console.log(` ${chalk.dim('Language:')} ${chalk.white(meta.lang || 'N/A')}`);
784
+ console.log(` ${chalk.dim('Mode:')} ${generationMode === 'pro' ? chalk.hex('#BF40FF').bold('PRO AI ✦') : chalk.hex('#00F5FF').bold('Standard ◉')}`);
389
785
  if (isNodeStack) {
390
786
  console.log(` ${chalk.dim('Database:')} ${chalk.white(dbType)}`);
391
- console.log(` ${chalk.dim('Auth:')} ${addAuth ? chalk.green('Yes') : chalk.red('No')}`);
392
- console.log(` ${chalk.dim('Seeder:')} ${addSeeder ? chalk.green('Yes') : chalk.red('No')}`);
787
+ console.log(` ${chalk.dim('Auth JWT:')} ${addAuth ? chalk.green('Yes') : chalk.red('No')}`);
788
+ console.log(` ${chalk.dim('Seeder:')} ${addSeeder ? chalk.green('Yes') : chalk.red('No')}`);
393
789
  console.log(` ${chalk.dim('Extras:')} ${chalk.white(extraFeatures.join(', ') || 'none')}`);
394
790
  }
395
791
  console.log('');
396
792
 
397
- const proceed = await p.confirm({
398
- message: 'Proceed with generation?',
399
- initialValue: true,
400
- });
401
- if (p.isCancel(proceed) || !proceed) {
402
- p.cancel('Generation aborted.');
403
- process.exit(0);
404
- }
793
+ const proceed = await p.confirm({ message: 'Proceed with generation?', initialValue: true });
794
+ if (p.isCancel(proceed) || !proceed) { p.cancel('Aborted.'); process.exit(0); }
405
795
 
406
- // ── Build options object ───────────────────────────────────────────────
796
+ // ── Build options ────────────────────────────────────────────────────
407
797
  const options = {
408
798
  generationMode,
409
799
  projectName,
@@ -413,35 +803,43 @@ async function main() {
413
803
  addAuth,
414
804
  addSeeder,
415
805
  extraFeatures,
416
- projectDir: path.resolve(process.cwd(), projectName),
806
+ projectDir : path.resolve(process.cwd(), projectName),
417
807
  frontendSrcDir: path.resolve(process.cwd(), srcPath),
418
808
  };
419
809
 
810
+ await executeGeneration(options, globalStart, plugins);
811
+ }
812
+
813
+ // ═══════════════════════════════════════════════════════════════════════════
814
+ // Generation Executor — separated for reuse with "Repeat Last"
815
+ // ═══════════════════════════════════════════════════════════════════════════
816
+
817
+ async function executeGeneration(options, globalStart, plugins = []) {
420
818
  const startTime = Date.now();
421
819
 
422
820
  try {
423
- // ── Route: PRO AI MODE ─────────────────────────────────────────────
821
+ // ── PRO MODE ──────────────────────────────────────────────────────
424
822
  if (options.generationMode === 'pro') {
425
823
  const apiKey = await getProApiKey();
426
824
 
427
- const spinnerParse = ora({ text: chalk.white('Parsing frontend source with Babel AST...'), spinner: 'dots12', color: 'cyan' }).start();
825
+ const spinnerParse = ora({ text: chalk.white('Parsing frontend with Babel AST...'), spinner: 'dots12', color: 'cyan' }).start();
428
826
  let astJsonData = [];
429
827
  try {
430
828
  astJsonData = await analyzeFrontend(options.frontendSrcDir);
431
- spinnerParse.succeed(chalk.green(`AST analysis complete — ${astJsonData.length} endpoint(s) detected.`));
829
+ spinnerParse.succeed(chalk.green(`AST complete — ${astJsonData.length} endpoint(s) detected.`));
432
830
  } catch (err) {
433
- spinnerParse.warn(chalk.yellow(`AST parse warning: ${err.message}`));
831
+ spinnerParse.warn(chalk.yellow(`AST warning: ${err.message}`));
434
832
  }
435
833
 
436
834
  const generatedBlocks = await callAIProcessor(astJsonData, apiKey, options);
437
835
  options.aiBlocks = generatedBlocks;
438
836
 
439
- const spinnerGen = ora({ text: chalk.white('Writing Intelligent Hexagonal Output...'), spinner: 'material', color: 'magenta' }).start();
837
+ const spinnerGen = ora({ text: chalk.white('Writing hexagonal output...'), spinner: 'material', color: 'magenta' }).start();
440
838
  try {
441
839
  await dispatchGenerator(options);
442
- spinnerGen.succeed(chalk.green('Hexagonal Auto-Write successful.'));
840
+ spinnerGen.succeed(chalk.green('Hexagonal auto-write complete.'));
443
841
  } catch (err) {
444
- spinnerGen.fail(chalk.red('Write process failed.'));
842
+ spinnerGen.fail(chalk.red('Write failed.'));
445
843
  throw err;
446
844
  }
447
845
 
@@ -451,34 +849,53 @@ async function main() {
451
849
  await fs.writeFile(path.join(options.projectDir, '.github', 'workflows', 'deploy.yml'), generatedBlocks.deployment.githubWorkflow);
452
850
  }
453
851
 
454
- printHealthDashboard(generatedBlocks);
455
- printTokenUsage('pro', startTime);
852
+ printHealthDashboard(generatedBlocks, options);
853
+ printTokenUsage('pro', startTime, { endpointCount: options.aiBlocks ? Object.keys(options.aiBlocks).length : 0 });
456
854
 
457
- p.outro(chalk.hex('#BF40FF').bold('PRO generation complete! cd ' + projectName));
458
- return;
855
+ } else {
856
+ // ── FREE MODE ──────────────────────────────────────────────────
857
+ await runFreeModePipeline(options);
858
+ printTokenUsage('free', startTime, options._meta || {});
859
+ }
860
+
861
+ // ── Post-gen: run plugins ────────────────────────────────────────
862
+ for (const plugin of plugins) {
863
+ if (plugin.runAfterGenerate) {
864
+ try {
865
+ await plugin.runAfterGenerate(options);
866
+ } catch {}
867
+ }
459
868
  }
460
869
 
461
- // ── Route: FREE STANDARD MODE ──────────────────────────────────────
462
- await runFreeModePipeline(options);
463
- printTokenUsage('free', startTime);
870
+ // ── Save session for "Repeat Last" ───────────────────────────────
871
+ await saveSession(options);
872
+
873
+ // ── Next Steps ───────────────────────────────────────────────────
874
+ printNextSteps(options.projectName, options.stack, options.dbType);
875
+
876
+ const totalTime = ((performance.now() - globalStart) / 1000).toFixed(2);
877
+ p.outro(
878
+ (options.generationMode === 'pro' ? chalk.hex('#BF40FF') : chalk.hex('#00F5FF')).bold(
879
+ `✓ Done in ${totalTime}s — cd ${options.projectName}`
880
+ )
881
+ );
464
882
 
465
- p.outro(chalk.hex('#00F5FF').bold('Backend generated! cd ' + projectName));
466
883
  } catch (error) {
467
884
  console.log('');
468
885
  p.log.error(chalk.red.bold(`Generation failed: ${error.message || error}`));
886
+ if (error.stack) console.log(chalk.gray(`\n${error.stack}`));
469
887
 
470
- if (error.stack) {
471
- console.log(chalk.gray(`\n Stack trace:\n${error.stack}`));
472
- }
473
-
474
- if (options.projectDir && (await fs.pathExists(options.projectDir))) {
475
- const spinnerClean = ora({ text: chalk.yellow('Cleaning up...'), spinner: 'line', color: 'yellow' }).start();
888
+ if (options.projectDir && await fs.pathExists(options.projectDir)) {
889
+ const sc = ora({ text: chalk.yellow('Cleaning up partial output...'), spinner: 'line', color: 'yellow' }).start();
476
890
  await fs.remove(options.projectDir);
477
- spinnerClean.succeed(chalk.yellow('Cleanup complete.'));
891
+ sc.succeed(chalk.yellow('Cleanup done.'));
478
892
  }
479
-
480
893
  process.exit(1);
481
894
  }
482
895
  }
483
896
 
484
- main();
897
+ // ── Launch ────────────────────────────────────────────────────────────────
898
+ main().catch((err) => {
899
+ console.error(chalk.red.bold(`\n Fatal: ${err.message || err}`));
900
+ process.exit(1);
901
+ });