create-backlist 10.0.4 → 10.0.6
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 +989 -713
- package/package.json +1 -1
- package/src/qa/analyzers/accessibility.js +157 -39
- package/src/qa/analyzers/performance.js +89 -102
- package/src/qa/browser/crawler.js +248 -166
- package/src/qa/browser/installer.js +209 -0
- package/src/qa/browser/interactions.js +222 -219
- package/src/qa/qa-engine.js +60 -19
package/bin/index.js
CHANGED
|
@@ -1,175 +1,668 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
4
|
-
// create-backlist
|
|
4
|
+
// create-backlist v9.0 — Animated Enterprise CLI ⚡ ULTRA EDITION
|
|
5
5
|
// Copyright (c) W.A.H.ISHAN — MIT License
|
|
6
6
|
//
|
|
7
|
-
//
|
|
8
|
-
// ✦
|
|
9
|
-
// ✦
|
|
10
|
-
// ✦
|
|
11
|
-
// ✦
|
|
12
|
-
// ✦
|
|
13
|
-
// ✦
|
|
14
|
-
// ✦
|
|
15
|
-
// ✦
|
|
16
|
-
// ✦
|
|
17
|
-
// ✦
|
|
18
|
-
// ✦ QA Engine v10.0 — URL QA, HTTP probe, security, SEO, a11y
|
|
7
|
+
// ULTRA ANIMATIONS:
|
|
8
|
+
// ✦ Typewriter banner with rainbow wave effect
|
|
9
|
+
// ✦ Smooth animated loading bars with real ETA
|
|
10
|
+
// ✦ Live pulsing status indicators
|
|
11
|
+
// ✦ Animated step transitions with icons
|
|
12
|
+
// ✦ Matrix-style boot sequence
|
|
13
|
+
// ✦ Smooth progress ring animation
|
|
14
|
+
// ✦ Animated file tree reveal
|
|
15
|
+
// ✦ Live stats counter animation
|
|
16
|
+
// ✦ Gradient text cycling
|
|
17
|
+
// ✦ Smooth spinner transitions
|
|
19
18
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
20
19
|
|
|
21
|
-
import * as p
|
|
22
|
-
import chalk
|
|
23
|
-
import ora
|
|
24
|
-
import fs
|
|
25
|
-
import path
|
|
26
|
-
import os
|
|
20
|
+
import * as p from '@clack/prompts';
|
|
21
|
+
import chalk from 'chalk';
|
|
22
|
+
import ora from 'ora';
|
|
23
|
+
import fs from 'fs-extra';
|
|
24
|
+
import path from 'node:path';
|
|
25
|
+
import os from 'node:os';
|
|
27
26
|
import { fileURLToPath } from 'node:url';
|
|
28
|
-
import { performance }
|
|
27
|
+
import { performance } from 'node:perf_hooks';
|
|
29
28
|
|
|
30
|
-
// ── Polyfill __dirname for ES Modules ────────────────────────────────────
|
|
31
29
|
const __filename = fileURLToPath(import.meta.url);
|
|
32
30
|
const __dirname = path.dirname(__filename);
|
|
33
|
-
|
|
34
|
-
// ── CLI boot timestamp ────────────────────────────────────────────────────
|
|
35
31
|
const BOOT_START = performance.now();
|
|
36
32
|
|
|
37
|
-
|
|
38
|
-
import { isCommandAvailable } from '../src/utils.js';
|
|
33
|
+
import { isCommandAvailable } from '../src/utils.js';
|
|
39
34
|
import { analyzeFrontend, performLowCostPathScan,
|
|
40
|
-
extractComponentTreeTypes }
|
|
41
|
-
import { BacklistAIAgent }
|
|
42
|
-
|
|
43
|
-
// ── Generator Imports ────────────────────────────────────────────────────
|
|
35
|
+
extractComponentTreeTypes } from '../src/analyzer.js';
|
|
36
|
+
import { BacklistAIAgent } from '../src/ai-agent.js';
|
|
44
37
|
import { generateNodeProject } from '../src/generators/node.js';
|
|
45
38
|
import { generateJsProject } from '../src/generators/js.js';
|
|
46
39
|
import { generateNestProject } from '../src/generators/nestjs.js';
|
|
47
40
|
import { generateDotnetProject } from '../src/generators/dotnet.js';
|
|
48
41
|
import { generateJavaProject } from '../src/generators/java.js';
|
|
49
42
|
import { generatePythonProject } from '../src/generators/python.js';
|
|
50
|
-
|
|
51
|
-
// ── QA System v10.0 ───────────────────────────────────────────────────────
|
|
52
43
|
import {
|
|
53
|
-
runManualQA,
|
|
54
|
-
|
|
55
|
-
runUrlQA,
|
|
56
|
-
viewQAHistory,
|
|
57
|
-
initQASystem,
|
|
58
|
-
autoRunPostGeneration,
|
|
44
|
+
runManualQA, runAutomatedQA, runUrlQA,
|
|
45
|
+
viewQAHistory, initQASystem, autoRunPostGeneration,
|
|
59
46
|
} from '../src/qa/qa-engine.js';
|
|
60
47
|
|
|
61
48
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
62
|
-
// Constants
|
|
49
|
+
// Constants
|
|
63
50
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
64
|
-
|
|
65
51
|
const CONFIG_PATH = path.join(os.homedir(), '.backlist-config.json');
|
|
66
52
|
const SESSIONS_PATH = path.join(os.homedir(), '.backlist-sessions.json');
|
|
67
53
|
const PLUGINS_DIR = path.join(os.homedir(), '.backlist-plugins');
|
|
68
|
-
const VERSION = '
|
|
54
|
+
const VERSION = '9.0.0-ULTRA';
|
|
69
55
|
const MAX_RETRIES = 3;
|
|
70
56
|
|
|
71
|
-
// ── Pricing Table ─────────────────────────────────────────────────────────
|
|
72
57
|
const PRICING = {
|
|
73
|
-
free
|
|
74
|
-
pro
|
|
58
|
+
free: { inputTokens: 0, outputTokens: 0, cost: '$0.00' },
|
|
59
|
+
pro : { inputTokens: 4200, outputTokens: 12800, cost: '~$0.02' },
|
|
75
60
|
};
|
|
76
61
|
|
|
77
|
-
// ── Stack metadata ────────────────────────────────────────────────────────
|
|
78
62
|
const STACK_META = {
|
|
79
|
-
'node-ts-express'
|
|
80
|
-
'js-express'
|
|
81
|
-
'nestjs'
|
|
82
|
-
'dotnet-webapi'
|
|
83
|
-
'java-spring'
|
|
84
|
-
'python-fastapi'
|
|
63
|
+
'node-ts-express': { lang: 'TypeScript', runtime: 'Node.js', icon: '🔷', color: '#3178C6' },
|
|
64
|
+
'js-express' : { lang: 'JavaScript', runtime: 'Node.js', icon: '🟨', color: '#F7DF1E' },
|
|
65
|
+
'nestjs' : { lang: 'TypeScript', runtime: 'NestJS', icon: '🔴', color: '#E0234E' },
|
|
66
|
+
'dotnet-webapi' : { lang: 'C#', runtime: '.NET', icon: '🟣', color: '#512BD4' },
|
|
67
|
+
'java-spring' : { lang: 'Java', runtime: 'Spring', icon: '🍃', color: '#6DB33F' },
|
|
68
|
+
'python-fastapi' : { lang: 'Python', runtime: 'FastAPI', icon: '🐍', color: '#009688' },
|
|
85
69
|
};
|
|
86
70
|
|
|
87
|
-
// ── Pre-flight fix hints ──────────────────────────────────────────────────
|
|
88
71
|
const PREFLIGHT_HINTS = {
|
|
89
72
|
'Node.js ≥ 18' : 'Download from https://nodejs.org — use v18 LTS or higher.',
|
|
90
73
|
'package.json present': 'Run `npm init -y` in your project root first.',
|
|
91
74
|
'.NET SDK installed' : 'Download from https://dotnet.microsoft.com/download',
|
|
92
75
|
'Java JDK installed' : 'Download from https://adoptium.net/',
|
|
93
|
-
'Python 3 installed' : 'Download from https://python.org
|
|
76
|
+
'Python 3 installed' : 'Download from https://python.org',
|
|
94
77
|
};
|
|
95
78
|
|
|
96
|
-
// ──
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
79
|
+
// ── Terminal utilities ─────────────────────────────────────────────────────
|
|
80
|
+
const ESC = '\x1b[';
|
|
81
|
+
const RESET = '\x1b[0m';
|
|
82
|
+
const HIDE = '\x1b[?25l';
|
|
83
|
+
const SHOW = '\x1b[?25h';
|
|
84
|
+
const CLEAR_LINE = '\x1b[2K\r';
|
|
85
|
+
const UP = (n = 1) => `\x1b[${n}A`;
|
|
86
|
+
const BOLD = '\x1b[1m';
|
|
87
|
+
const DIM_ESC = '\x1b[2m';
|
|
88
|
+
|
|
89
|
+
function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
|
|
90
|
+
function clamp(v, min, max) { return Math.max(min, Math.min(max, v)); }
|
|
100
91
|
|
|
101
92
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
102
|
-
//
|
|
93
|
+
// 🎨 Animation Engine
|
|
103
94
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
104
95
|
|
|
105
|
-
|
|
96
|
+
// Gradient palette — cycles through colors
|
|
97
|
+
const PALETTE = [
|
|
98
|
+
'#FF006E','#FB5607','#FFBE0B','#00F5FF',
|
|
99
|
+
'#8338EC','#3A86FF','#00FF9F','#FF006E',
|
|
100
|
+
];
|
|
101
|
+
|
|
102
|
+
function gradientChar(char, index, total, offset = 0) {
|
|
103
|
+
const pos = ((index + offset) / Math.max(total, 1)) * (PALETTE.length - 1);
|
|
104
|
+
const i = Math.floor(pos) % (PALETTE.length - 1);
|
|
105
|
+
const t = pos - Math.floor(pos);
|
|
106
|
+
const c1 = hexToRgb(PALETTE[i]);
|
|
107
|
+
const c2 = hexToRgb(PALETTE[i + 1] || PALETTE[0]);
|
|
108
|
+
const r = Math.round(c1.r + t * (c2.r - c1.r));
|
|
109
|
+
const g = Math.round(c1.g + t * (c2.g - c1.g));
|
|
110
|
+
const b = Math.round(c1.b + t * (c2.b - c1.b));
|
|
111
|
+
return `\x1b[38;2;${r};${g};${b}m${char}\x1b[0m`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function gradientText(text, offset = 0) {
|
|
115
|
+
return [...text].map((c, i) => gradientChar(c, i, text.length, offset)).join('');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function hexToRgb(hex) {
|
|
119
|
+
const r = parseInt(hex.slice(1, 3), 16);
|
|
120
|
+
const g = parseInt(hex.slice(3, 5), 16);
|
|
121
|
+
const b = parseInt(hex.slice(5, 7), 16);
|
|
122
|
+
return { r, g, b };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function rgbText(text, hex) {
|
|
126
|
+
const { r, g, b } = hexToRgb(hex);
|
|
127
|
+
return `\x1b[38;2;${r};${g};${b}m${text}\x1b[0m`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ── Typewriter effect ──────────────────────────────────────────────────────
|
|
131
|
+
async function typewrite(text, delay = 18, color = null) {
|
|
132
|
+
for (const char of text) {
|
|
133
|
+
const out = color ? color(char) : char;
|
|
134
|
+
process.stdout.write(out);
|
|
135
|
+
await sleep(delay);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ── Smooth animated progress bar ───────────────────────────────────────────
|
|
140
|
+
function animatedBar(pct, width = 28, filled = '█', empty = '░') {
|
|
141
|
+
const f = Math.round(clamp(pct, 0, 100) / 100 * width);
|
|
142
|
+
const e = width - f;
|
|
143
|
+
const col = pct >= 90 ? '#00FF9F' : pct >= 70 ? '#FFBE0B' : '#FF006E';
|
|
144
|
+
return rgbText(filled.repeat(f), col) + chalk.gray(empty.repeat(e));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ── Pulse dot animation ────────────────────────────────────────────────────
|
|
148
|
+
const PULSE_FRAMES = ['◉', '◎', '○', '◎'];
|
|
149
|
+
let _pulseIdx = 0;
|
|
150
|
+
function pulseDot(color = '#00F5FF') {
|
|
151
|
+
const frame = PULSE_FRAMES[_pulseIdx++ % PULSE_FRAMES.length];
|
|
152
|
+
return rgbText(frame, color);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ── Spinner frames ─────────────────────────────────────────────────────────
|
|
156
|
+
const SPINNERS = {
|
|
157
|
+
arc : ['◜','◠','◝','◞','◡','◟'],
|
|
158
|
+
dots : ['⠋','⠙','⠹','⠸','⠼','⠴','⠦','⠧','⠇','⠏'],
|
|
159
|
+
wave : ['▁','▂','▃','▄','▅','▆','▇','█','▇','▆','▅','▄','▃','▂'],
|
|
160
|
+
star : ['✶','✸','✹','✺','✹','✷'],
|
|
161
|
+
bounce : ['⠁','⠂','⠄','⠂'],
|
|
162
|
+
matrix : ['░','▒','▓','█','▓','▒'],
|
|
163
|
+
arrows : ['←','↖','↑','↗','→','↘','↓','↙'],
|
|
164
|
+
clock : ['🕐','🕑','🕒','🕓','🕔','🕕','🕖','🕗','🕘','🕙','🕚','🕛'],
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
class LiveSpinner {
|
|
168
|
+
#frame = 0;
|
|
169
|
+
#timer = null;
|
|
170
|
+
#text = '';
|
|
171
|
+
#prefix = '';
|
|
172
|
+
#lines = 1;
|
|
173
|
+
#color = '#00F5FF';
|
|
174
|
+
#type = 'arc';
|
|
175
|
+
#active = false;
|
|
176
|
+
#gradOff = 0;
|
|
177
|
+
|
|
178
|
+
start(text = '', { type = 'arc', color = '#00F5FF', prefix = '' } = {}) {
|
|
179
|
+
this.#text = text;
|
|
180
|
+
this.#type = type;
|
|
181
|
+
this.#color = color;
|
|
182
|
+
this.#prefix = prefix;
|
|
183
|
+
this.#active = true;
|
|
184
|
+
this.#frame = 0;
|
|
185
|
+
process.stdout.write(HIDE);
|
|
186
|
+
this.#tick();
|
|
187
|
+
this.#timer = setInterval(() => this.#tick(), 80);
|
|
188
|
+
return this;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
update(text) { this.#text = text; this.#gradOff++; }
|
|
192
|
+
|
|
193
|
+
#tick() {
|
|
194
|
+
if (!this.#active) return;
|
|
195
|
+
const frames = SPINNERS[this.#type] || SPINNERS.arc;
|
|
196
|
+
const spin = rgbText(frames[this.#frame % frames.length], this.#color);
|
|
197
|
+
const label = gradientText(this.#text.slice(0, 60), this.#gradOff);
|
|
198
|
+
process.stdout.write(CLEAR_LINE + ` ${this.#prefix}${spin} ${label}`);
|
|
199
|
+
this.#frame++;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
succeed(text) { this.#stop(rgbText('✓', '#00FF9F'), text); }
|
|
203
|
+
fail(text) { this.#stop(rgbText('✗', '#FF006E'), text); }
|
|
204
|
+
warn(text) { this.#stop(rgbText('⚠', '#FFBE0B'), text); }
|
|
205
|
+
info(text) { this.#stop(rgbText('◆', '#00F5FF'), text); }
|
|
206
|
+
|
|
207
|
+
#stop(icon, text) {
|
|
208
|
+
this.#active = false;
|
|
209
|
+
if (this.#timer) { clearInterval(this.#timer); this.#timer = null; }
|
|
210
|
+
process.stdout.write(CLEAR_LINE + ` ${icon} ${chalk.white(text)}\n`);
|
|
211
|
+
process.stdout.write(SHOW);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// ── Animated number counter ────────────────────────────────────────────────
|
|
216
|
+
async function countUp(target, duration = 800, color = '#00F5FF', suffix = '') {
|
|
217
|
+
const steps = 20;
|
|
218
|
+
const delay = duration / steps;
|
|
219
|
+
for (let i = 0; i <= steps; i++) {
|
|
220
|
+
const val = Math.round((i / steps) * target);
|
|
221
|
+
process.stdout.write(CLEAR_LINE + ' ' + rgbText(String(val) + suffix, color));
|
|
222
|
+
await sleep(delay);
|
|
223
|
+
}
|
|
224
|
+
process.stdout.write('\n');
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
228
|
+
// 🚀 Animated Boot Sequence
|
|
229
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
230
|
+
|
|
231
|
+
async function runBootSequence() {
|
|
232
|
+
// Matrix rain intro (100ms)
|
|
233
|
+
const matrixChars = '01アイウエオカキクケコサシスセソタチツテト';
|
|
234
|
+
const cols = Math.min(process.stdout.columns || 80, 64);
|
|
235
|
+
|
|
236
|
+
process.stdout.write(HIDE);
|
|
237
|
+
for (let row = 0; row < 3; row++) {
|
|
238
|
+
let line = ' ';
|
|
239
|
+
for (let c = 0; c < cols - 2; c++) {
|
|
240
|
+
const ch = matrixChars[Math.floor(Math.random() * matrixChars.length)];
|
|
241
|
+
const brightness = Math.random();
|
|
242
|
+
if (brightness > 0.8) line += rgbText(ch, '#00FF9F');
|
|
243
|
+
else if (brightness > 0.5) line += rgbText(ch, '#006600');
|
|
244
|
+
else line += chalk.dim(rgbText(ch, '#003300'));
|
|
245
|
+
}
|
|
246
|
+
process.stdout.write(line + '\n');
|
|
247
|
+
await sleep(40);
|
|
248
|
+
}
|
|
249
|
+
await sleep(80);
|
|
250
|
+
|
|
251
|
+
// Clear matrix lines
|
|
252
|
+
process.stdout.write(UP(3));
|
|
253
|
+
for (let i = 0; i < 3; i++) process.stdout.write(CLEAR_LINE + '\n');
|
|
254
|
+
process.stdout.write(UP(3));
|
|
255
|
+
process.stdout.write(SHOW);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
259
|
+
// 🎨 Animated ASCII Banner
|
|
260
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
261
|
+
|
|
262
|
+
async function printAnimatedBanner() {
|
|
263
|
+
await runBootSequence();
|
|
106
264
|
|
|
107
|
-
async function gracefulShutdown(signal) {
|
|
108
265
|
console.log('');
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
266
|
+
|
|
267
|
+
const lines = [
|
|
268
|
+
' ╔══════════════════════════════════════════════════════════════╗',
|
|
269
|
+
' ║ ____ ___ ________ __ ____ ___________ ║',
|
|
270
|
+
' ║ / __ ) / | / ____/ //_/ / / / _/ ___/_ ║',
|
|
271
|
+
' ║ / __ | / /| | / / / ,< / / / / \\__ \\ ║',
|
|
272
|
+
' ║ / /_/ / / ___ |/ /___/ /| | / /____/ / ___/ / ║',
|
|
273
|
+
' ║/_____/ /_/ |_|\\____/_/ |_| /_____/___//____/ ║',
|
|
274
|
+
' ║ ║',
|
|
275
|
+
' ║ ⚡ v9.0-ULTRA — Animated Enterprise CLI ⚡ ║',
|
|
276
|
+
' ║ Real Testing · AI Powered · Zero Fake Data · Playwright ║',
|
|
277
|
+
' ╚══════════════════════════════════════════════════════════════╝',
|
|
278
|
+
];
|
|
279
|
+
|
|
280
|
+
process.stdout.write(HIDE);
|
|
281
|
+
|
|
282
|
+
for (let i = 0; i < lines.length; i++) {
|
|
283
|
+
const line = lines[i];
|
|
284
|
+
const offset = i * 3;
|
|
285
|
+
|
|
286
|
+
if (i === 0 || i === 9) {
|
|
287
|
+
// Border lines — cyan animated
|
|
288
|
+
let out = '';
|
|
289
|
+
for (let j = 0; j < line.length; j++) {
|
|
290
|
+
out += gradientChar(line[j], j, line.length, offset + _pulseIdx);
|
|
291
|
+
}
|
|
292
|
+
process.stdout.write(out + '\n');
|
|
293
|
+
} else if (i >= 1 && i <= 5) {
|
|
294
|
+
// Logo lines — typewriter with gradient
|
|
295
|
+
process.stdout.write(' ' + rgbText('║', '#00F5FF'));
|
|
296
|
+
const inner = line.slice(4, -1);
|
|
297
|
+
for (let j = 0; j < inner.length; j++) {
|
|
298
|
+
process.stdout.write(gradientChar(inner[j], j, inner.length, offset));
|
|
299
|
+
if (j % 3 === 0) await sleep(1);
|
|
300
|
+
}
|
|
301
|
+
process.stdout.write(rgbText('║', '#00F5FF') + '\n');
|
|
302
|
+
} else if (i === 7) {
|
|
303
|
+
// Tagline — pulsing bright
|
|
304
|
+
process.stdout.write(' ' + rgbText('║', '#00F5FF'));
|
|
305
|
+
const inner = line.slice(4, -1);
|
|
306
|
+
let out = '';
|
|
307
|
+
for (let j = 0; j < inner.length; j++) {
|
|
308
|
+
out += gradientChar(inner[j], j, inner.length, Date.now() / 100);
|
|
309
|
+
}
|
|
310
|
+
process.stdout.write(out);
|
|
311
|
+
process.stdout.write(rgbText('║', '#00F5FF') + '\n');
|
|
312
|
+
} else {
|
|
313
|
+
// Info lines
|
|
314
|
+
process.stdout.write(' ' + rgbText('║', '#00F5FF'));
|
|
315
|
+
process.stdout.write(chalk.gray(line.slice(4, -1)));
|
|
316
|
+
process.stdout.write(rgbText('║', '#00F5FF') + '\n');
|
|
317
|
+
}
|
|
318
|
+
await sleep(30);
|
|
118
319
|
}
|
|
119
|
-
|
|
320
|
+
|
|
321
|
+
process.stdout.write(SHOW);
|
|
322
|
+
|
|
323
|
+
// Animated stats bar
|
|
324
|
+
console.log('');
|
|
325
|
+
await sleep(100);
|
|
326
|
+
|
|
327
|
+
const bootMs = (performance.now() - BOOT_START).toFixed(0);
|
|
328
|
+
const heapMB = (process.memoryUsage().heapUsed / 1024 / 1024).toFixed(0);
|
|
329
|
+
const rssMB = (process.memoryUsage().rss / 1024 / 1024).toFixed(0);
|
|
330
|
+
const cpus = os.cpus().length;
|
|
331
|
+
const nodeVer = process.version;
|
|
332
|
+
|
|
333
|
+
// Animate the stats bar appearing
|
|
334
|
+
process.stdout.write(HIDE);
|
|
335
|
+
const statItems = [
|
|
336
|
+
{ label: 'Boot', value: bootMs + 'ms', color: '#00FF9F' },
|
|
337
|
+
{ label: 'Node', value: nodeVer, color: '#00F5FF' },
|
|
338
|
+
{ label: 'Heap', value: heapMB + 'MB', color: '#BF40FF' },
|
|
339
|
+
{ label: 'RSS', value: rssMB + 'MB', color: '#FF6B6B' },
|
|
340
|
+
{ label: 'CPUs', value: String(cpus), color: '#FFBE0B' },
|
|
341
|
+
{ label: 'OS', value: process.platform, color: '#00F5FF' },
|
|
342
|
+
{ label: 'v', value: VERSION, color: '#BF40FF' },
|
|
343
|
+
];
|
|
344
|
+
|
|
345
|
+
let statLine = ' ';
|
|
346
|
+
for (const item of statItems) {
|
|
347
|
+
statLine += chalk.gray(item.label + ':') + rgbText(item.value, item.color) + chalk.gray(' │ ');
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Slide-in effect
|
|
351
|
+
for (let i = 0; i <= statLine.length; i += 4) {
|
|
352
|
+
process.stdout.write(CLEAR_LINE + statLine.slice(0, i));
|
|
353
|
+
await sleep(8);
|
|
354
|
+
}
|
|
355
|
+
process.stdout.write(CLEAR_LINE + statLine + '\n');
|
|
356
|
+
process.stdout.write(SHOW);
|
|
357
|
+
|
|
358
|
+
// Animated divider
|
|
359
|
+
const divWidth = Math.min(process.stdout.columns || 80, 66);
|
|
360
|
+
process.stdout.write(HIDE + ' ');
|
|
361
|
+
for (let i = 0; i < divWidth - 2; i++) {
|
|
362
|
+
process.stdout.write(gradientChar('─', i, divWidth, _pulseIdx));
|
|
363
|
+
if (i % 5 === 0) await sleep(4);
|
|
364
|
+
}
|
|
365
|
+
process.stdout.write('\n' + SHOW + '\n');
|
|
120
366
|
}
|
|
121
367
|
|
|
122
|
-
|
|
123
|
-
|
|
368
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
369
|
+
// 🔄 Animated Step Renderer
|
|
370
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
371
|
+
|
|
372
|
+
class StepRenderer {
|
|
373
|
+
#steps = [];
|
|
374
|
+
#current = -1;
|
|
375
|
+
#timer = null;
|
|
376
|
+
#frame = 0;
|
|
377
|
+
#lines = 0;
|
|
378
|
+
|
|
379
|
+
constructor(steps) {
|
|
380
|
+
this.#steps = steps;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
async start() {
|
|
384
|
+
process.stdout.write(HIDE);
|
|
385
|
+
this.#render();
|
|
386
|
+
this.#timer = setInterval(() => this.#render(), 100);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
next(label = null) {
|
|
390
|
+
if (this.#current >= 0) {
|
|
391
|
+
this.#steps[this.#current].status = 'done';
|
|
392
|
+
}
|
|
393
|
+
this.#current++;
|
|
394
|
+
if (label) this.#steps[this.#current].label = label;
|
|
395
|
+
this.#steps[this.#current].status = 'running';
|
|
396
|
+
this.#steps[this.#current].startTime = Date.now();
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
finish() {
|
|
400
|
+
if (this.#current >= 0) this.#steps[this.#current].status = 'done';
|
|
401
|
+
clearInterval(this.#timer);
|
|
402
|
+
this.#render();
|
|
403
|
+
process.stdout.write(SHOW + '\n');
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
#render() {
|
|
407
|
+
const frames = SPINNERS.arc;
|
|
408
|
+
const spin = rgbText(frames[this.#frame % frames.length], '#00F5FF');
|
|
409
|
+
this.#frame++;
|
|
410
|
+
|
|
411
|
+
// Clear previous output
|
|
412
|
+
if (this.#lines > 0) {
|
|
413
|
+
process.stdout.write(UP(this.#lines));
|
|
414
|
+
for (let i = 0; i < this.#lines; i++) {
|
|
415
|
+
process.stdout.write(CLEAR_LINE + '\n');
|
|
416
|
+
}
|
|
417
|
+
process.stdout.write(UP(this.#lines));
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const output = [];
|
|
421
|
+
for (let i = 0; i < this.#steps.length; i++) {
|
|
422
|
+
const step = this.#steps[i];
|
|
423
|
+
let icon, label;
|
|
424
|
+
|
|
425
|
+
if (step.status === 'done') {
|
|
426
|
+
icon = rgbText('✓', '#00FF9F');
|
|
427
|
+
label = chalk.dim(step.label);
|
|
428
|
+
} else if (step.status === 'running') {
|
|
429
|
+
icon = spin;
|
|
430
|
+
const elapsed = step.startTime ? ((Date.now() - step.startTime) / 1000).toFixed(1) : '';
|
|
431
|
+
label = rgbText(step.label, '#FFBE0B') + chalk.gray(elapsed ? ` (${elapsed}s)` : '');
|
|
432
|
+
} else {
|
|
433
|
+
icon = chalk.gray('○');
|
|
434
|
+
label = chalk.gray(step.label);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
output.push(` ${icon} ${label}`);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// Overall progress bar
|
|
441
|
+
const done = this.#steps.filter(s => s.status === 'done').length;
|
|
442
|
+
const total = this.#steps.length;
|
|
443
|
+
const pct = Math.round((done / total) * 100);
|
|
444
|
+
output.push('');
|
|
445
|
+
output.push(` [${animatedBar(pct, 30)}] ${rgbText(pct + '%', '#00F5FF')} (${done}/${total})`);
|
|
446
|
+
|
|
447
|
+
process.stdout.write(output.join('\n') + '\n');
|
|
448
|
+
this.#lines = output.length;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
453
|
+
// 🎯 Animated Section Header
|
|
454
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
455
|
+
|
|
456
|
+
async function printSectionHeader(title, icon = '◆', color = '#00F5FF') {
|
|
457
|
+
console.log('');
|
|
458
|
+
const w = Math.min(process.stdout.columns || 80, 60) - 4;
|
|
459
|
+
const bar = '─'.repeat(w);
|
|
460
|
+
|
|
461
|
+
process.stdout.write(HIDE);
|
|
462
|
+
process.stdout.write(' ' + rgbText(`╔${bar}╗`, color) + '\n');
|
|
463
|
+
await sleep(30);
|
|
464
|
+
|
|
465
|
+
const padded = ` ${icon} ${title} `.padEnd(w);
|
|
466
|
+
process.stdout.write(' ' + rgbText('║', color));
|
|
467
|
+
for (let i = 0; i < padded.length; i++) {
|
|
468
|
+
process.stdout.write(gradientChar(padded[i], i, padded.length));
|
|
469
|
+
if (i % 4 === 0) await sleep(3);
|
|
470
|
+
}
|
|
471
|
+
process.stdout.write(rgbText('║', color) + '\n');
|
|
472
|
+
await sleep(30);
|
|
473
|
+
|
|
474
|
+
process.stdout.write(' ' + rgbText(`╚${bar}╝`, color) + '\n');
|
|
475
|
+
process.stdout.write(SHOW);
|
|
476
|
+
console.log('');
|
|
477
|
+
}
|
|
124
478
|
|
|
125
479
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
126
|
-
// Animated
|
|
480
|
+
// 📊 Animated Score Dashboard
|
|
127
481
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
128
482
|
|
|
129
|
-
function
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
483
|
+
async function printAnimatedDashboard(scores, title = 'Health Dashboard') {
|
|
484
|
+
await printSectionHeader(title, '📊', '#BF40FF');
|
|
485
|
+
|
|
486
|
+
for (const { label, score, icon: ico } of scores) {
|
|
487
|
+
const pct = clamp(score, 0, 100);
|
|
488
|
+
const col = pct >= 95 ? '#00FF9F' : pct >= 80 ? '#00F5FF' : pct >= 70 ? '#FFBE0B' : '#FF006E';
|
|
489
|
+
const grade = pct >= 95 ? 'A+' : pct >= 90 ? 'A' : pct >= 80 ? 'B' : pct >= 70 ? 'C' : 'D';
|
|
490
|
+
|
|
491
|
+
// Animate bar filling
|
|
492
|
+
process.stdout.write(HIDE);
|
|
493
|
+
for (let i = 0; i <= pct; i += 3) {
|
|
494
|
+
const partial = clamp(i, 0, 100);
|
|
495
|
+
process.stdout.write(
|
|
496
|
+
CLEAR_LINE +
|
|
497
|
+
` ${ico} ${chalk.dim(label.padEnd(22))} [${animatedBar(partial, 20)}] ` +
|
|
498
|
+
rgbText(`${partial}%`, col)
|
|
499
|
+
);
|
|
500
|
+
await sleep(8);
|
|
501
|
+
}
|
|
502
|
+
process.stdout.write(
|
|
503
|
+
CLEAR_LINE +
|
|
504
|
+
` ${ico} ${chalk.dim(label.padEnd(22))} [${animatedBar(pct, 20)}] ` +
|
|
505
|
+
rgbText(`${pct}% ${grade}`, col) + '\n'
|
|
506
|
+
);
|
|
507
|
+
process.stdout.write(SHOW);
|
|
508
|
+
await sleep(40);
|
|
509
|
+
}
|
|
135
510
|
|
|
136
511
|
console.log('');
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
515
|
+
// 📁 Animated File Tree
|
|
516
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
517
|
+
|
|
518
|
+
async function printAnimatedFileTree(dir, depth = 0, maxDepth = 2) {
|
|
519
|
+
if (depth === 0) {
|
|
520
|
+
await printSectionHeader('Generated Project Structure', '📁', '#00F5FF');
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
try {
|
|
524
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
525
|
+
const filtered = entries.filter(e => e.name !== 'node_modules' && !e.name.startsWith('.'));
|
|
526
|
+
|
|
527
|
+
for (const entry of filtered.slice(0, 14)) {
|
|
528
|
+
const indent = ' ' + ' '.repeat(depth + 1);
|
|
529
|
+
const isDir = entry.isDirectory();
|
|
530
|
+
const icon = isDir ? rgbText('📂', '#00F5FF') : chalk.gray('📄');
|
|
531
|
+
const name = isDir
|
|
532
|
+
? rgbText(entry.name, '#00F5FF')
|
|
533
|
+
: chalk.dim(entry.name);
|
|
534
|
+
const connector = depth === 0 ? rgbText('├─ ', '#2d2d4e') : rgbText('╰─ ', '#1a1a2e');
|
|
535
|
+
|
|
536
|
+
// Slide-in animation
|
|
537
|
+
process.stdout.write(HIDE);
|
|
538
|
+
const line = `${indent}${connector}${icon} ${name}`;
|
|
539
|
+
for (let i = 0; i <= line.length; i += 2) {
|
|
540
|
+
process.stdout.write(CLEAR_LINE + line.slice(0, i));
|
|
541
|
+
await sleep(4);
|
|
542
|
+
}
|
|
543
|
+
process.stdout.write(CLEAR_LINE + line + '\n');
|
|
544
|
+
process.stdout.write(SHOW);
|
|
545
|
+
|
|
546
|
+
if (isDir && depth < maxDepth - 1) {
|
|
547
|
+
await printAnimatedFileTree(path.join(dir, entry.name), depth + 1, maxDepth);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
if (filtered.length > 14 && depth === 0) {
|
|
552
|
+
await sleep(30);
|
|
553
|
+
console.log(chalk.gray(` ${' '.repeat(depth + 2)}╰─ ... and ${filtered.length - 14} more files`));
|
|
554
|
+
}
|
|
555
|
+
} catch {}
|
|
556
|
+
|
|
557
|
+
if (depth === 0) console.log('');
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
561
|
+
// 📈 Animated Token/Stats Display
|
|
562
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
563
|
+
|
|
564
|
+
async function printAnimatedStats(mode, startTime, extra = {}) {
|
|
565
|
+
const elapsed = ((Date.now() - startTime) / 1000).toFixed(2);
|
|
566
|
+
const pricing = PRICING[mode] || PRICING.free;
|
|
567
|
+
|
|
568
|
+
await printSectionHeader('Generation Summary', '📊', '#BF40FF');
|
|
569
|
+
|
|
570
|
+
const stats = [
|
|
571
|
+
{ label: 'Mode', value: mode === 'pro' ? 'PRO AI ✦' : 'Standard ◉', color: mode === 'pro' ? '#BF40FF' : '#00F5FF' },
|
|
572
|
+
{ label: 'Elapsed', value: elapsed + 's', color: '#00FF9F' },
|
|
573
|
+
];
|
|
574
|
+
|
|
575
|
+
if (extra.endpointCount !== undefined)
|
|
576
|
+
stats.push({ label: 'Endpoints', value: extra.endpointCount + ' detected', color: '#00F5FF' });
|
|
577
|
+
if (extra.filesGenerated !== undefined)
|
|
578
|
+
stats.push({ label: 'Files written', value: String(extra.filesGenerated), color: '#FFBE0B' });
|
|
579
|
+
if (extra.filesGenerated && parseFloat(elapsed) > 0)
|
|
580
|
+
stats.push({ label: 'Throughput', value: (extra.filesGenerated / parseFloat(elapsed)).toFixed(1) + ' files/s', color: '#00FF9F' });
|
|
581
|
+
if (extra.retries > 0)
|
|
582
|
+
stats.push({ label: 'Retries', value: String(extra.retries), color: '#FF6B6B' });
|
|
583
|
+
|
|
584
|
+
for (const stat of stats) {
|
|
585
|
+
await sleep(60);
|
|
586
|
+
process.stdout.write(HIDE);
|
|
587
|
+
const line = ` ${chalk.dim(stat.label.padEnd(16))} ${rgbText(stat.value, stat.color)}`;
|
|
588
|
+
for (let i = 0; i <= line.length; i += 3) {
|
|
589
|
+
process.stdout.write(CLEAR_LINE + line.slice(0, i));
|
|
590
|
+
await sleep(5);
|
|
591
|
+
}
|
|
592
|
+
process.stdout.write(CLEAR_LINE + line + '\n');
|
|
593
|
+
process.stdout.write(SHOW);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
if (mode === 'pro') {
|
|
597
|
+
console.log('');
|
|
598
|
+
const usagePct = Math.round((pricing.outputTokens / 16000) * 100);
|
|
599
|
+
console.log(` ${chalk.dim('Input tokens:')} ${chalk.yellow(pricing.inputTokens.toLocaleString())}`);
|
|
600
|
+
console.log(` ${chalk.dim('Output tokens:')} ${chalk.yellow(pricing.outputTokens.toLocaleString())}`);
|
|
601
|
+
process.stdout.write(` ${chalk.dim('Token usage:')} `);
|
|
602
|
+
for (let i = 0; i <= usagePct; i += 2) {
|
|
603
|
+
process.stdout.write(HIDE + CLEAR_LINE.slice(0, 0) + `\r ${chalk.dim('Token usage:')} [${animatedBar(i, 20)}] ${rgbText(i + '%', '#BF40FF')}`);
|
|
604
|
+
await sleep(15);
|
|
605
|
+
}
|
|
606
|
+
process.stdout.write(SHOW + '\n');
|
|
607
|
+
console.log(` ${chalk.dim('Est. cost:')} ${chalk.green(pricing.cost)}`);
|
|
608
|
+
}
|
|
609
|
+
|
|
147
610
|
console.log('');
|
|
611
|
+
}
|
|
148
612
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
613
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
614
|
+
// 🎬 Animated Next Steps
|
|
615
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
616
|
+
|
|
617
|
+
async function printAnimatedNextSteps(projectName, stack, dbType) {
|
|
618
|
+
await printSectionHeader('Next Steps', '🚀', '#00F5FF');
|
|
619
|
+
|
|
620
|
+
const isNode = ['node-ts-express', 'js-express', 'nestjs'].includes(stack);
|
|
621
|
+
const steps = [`cd ${projectName}`];
|
|
622
|
+
|
|
623
|
+
if (isNode) {
|
|
624
|
+
steps.push('npm install');
|
|
625
|
+
if (dbType === 'prisma') steps.push('npx prisma migrate dev --name init');
|
|
626
|
+
steps.push('npm run dev');
|
|
627
|
+
} else if (stack === 'python-fastapi') {
|
|
628
|
+
steps.push('pip install -r requirements.txt');
|
|
629
|
+
steps.push('uvicorn main:app --reload');
|
|
630
|
+
} else if (stack === 'dotnet-webapi') {
|
|
631
|
+
steps.push('dotnet restore');
|
|
632
|
+
steps.push('dotnet run');
|
|
633
|
+
} else if (stack === 'java-spring') {
|
|
634
|
+
steps.push('./mvnw spring-boot:run');
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
for (let i = 0; i < steps.length; i++) {
|
|
638
|
+
await sleep(80);
|
|
639
|
+
process.stdout.write(HIDE);
|
|
640
|
+
const num = rgbText(`${i + 1}.`, '#BF40FF');
|
|
641
|
+
const cmd = rgbText(steps[i], '#00F5FF');
|
|
642
|
+
const line = ` ${num} ${cmd}`;
|
|
643
|
+
for (let j = 0; j <= line.length; j += 2) {
|
|
644
|
+
process.stdout.write(CLEAR_LINE + line.slice(0, j));
|
|
645
|
+
await sleep(6);
|
|
646
|
+
}
|
|
647
|
+
process.stdout.write(CLEAR_LINE + line + '\n');
|
|
648
|
+
process.stdout.write(SHOW);
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
console.log('');
|
|
652
|
+
await sleep(100);
|
|
653
|
+
await typewrite(
|
|
654
|
+
' 💡 Tip: Run `npm run qa` to validate your generated backend.\n',
|
|
655
|
+
12, chalk.dim
|
|
656
|
+
);
|
|
657
|
+
await typewrite(
|
|
658
|
+
' 📖 Docs: https://backlist.dev/docs\n',
|
|
659
|
+
12, chalk.dim
|
|
166
660
|
);
|
|
167
|
-
console.log(dim(' ─────────────────────────────────────────────────────────────'));
|
|
168
661
|
console.log('');
|
|
169
662
|
}
|
|
170
663
|
|
|
171
664
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
172
|
-
//
|
|
665
|
+
// 🔁 Session Manager
|
|
173
666
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
174
667
|
|
|
175
668
|
async function loadLastSession() {
|
|
@@ -186,36 +679,35 @@ async function saveSession(options) {
|
|
|
186
679
|
try {
|
|
187
680
|
await fs.writeJson(SESSIONS_PATH, {
|
|
188
681
|
last: {
|
|
189
|
-
stack
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
savedAt : new Date().toISOString(),
|
|
194
|
-
extraFeatures : options.extraFeatures ?? [],
|
|
195
|
-
addAuth : options.addAuth,
|
|
196
|
-
addSeeder : options.addSeeder,
|
|
682
|
+
stack: options.stack, dbType: options.dbType,
|
|
683
|
+
generationMode: options.generationMode, projectName: options.projectName,
|
|
684
|
+
savedAt: new Date().toISOString(), extraFeatures: options.extraFeatures ?? [],
|
|
685
|
+
addAuth: options.addAuth, addSeeder: options.addSeeder,
|
|
197
686
|
},
|
|
198
687
|
}, { spaces: 2 });
|
|
199
688
|
} catch {}
|
|
200
689
|
}
|
|
201
690
|
|
|
202
|
-
function printSessionDiff(last, current) {
|
|
691
|
+
async function printSessionDiff(last, current) {
|
|
203
692
|
if (!last) return;
|
|
204
693
|
const changes = [];
|
|
205
|
-
if (last.stack !== current.stack)
|
|
206
|
-
|
|
207
|
-
if (last.
|
|
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)}`);
|
|
694
|
+
if (last.stack !== current.stack) changes.push({ label: 'Stack', from: last.stack, to: current.stack });
|
|
695
|
+
if (last.dbType !== current.dbType) changes.push({ label: 'DB', from: last.dbType, to: current.dbType });
|
|
696
|
+
if (last.generationMode !== current.generationMode) changes.push({ label: 'Mode', from: last.generationMode, to: current.generationMode });
|
|
211
697
|
if (changes.length === 0) return;
|
|
698
|
+
|
|
212
699
|
console.log(chalk.dim(' ── 🔀 Changes from last session:'));
|
|
213
|
-
|
|
700
|
+
for (const ch of changes) {
|
|
701
|
+
await sleep(40);
|
|
702
|
+
const from = rgbText(ch.from || 'none', '#FF6B6B');
|
|
703
|
+
const to = rgbText(ch.to || 'none', '#00FF9F');
|
|
704
|
+
console.log(` ${chalk.dim(ch.label + ':')} ${from} ${chalk.gray('→')} ${to}`);
|
|
705
|
+
}
|
|
214
706
|
console.log('');
|
|
215
707
|
}
|
|
216
708
|
|
|
217
709
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
218
|
-
// Plugin Loader
|
|
710
|
+
// 🔌 Plugin Loader
|
|
219
711
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
220
712
|
|
|
221
713
|
async function loadPlugins() {
|
|
@@ -224,11 +716,11 @@ async function loadPlugins() {
|
|
|
224
716
|
await fs.ensureDir(PLUGINS_DIR);
|
|
225
717
|
const entries = await fs.readdir(PLUGINS_DIR);
|
|
226
718
|
const results = await Promise.allSettled(
|
|
227
|
-
entries.map(async
|
|
228
|
-
const
|
|
229
|
-
if (!await fs.pathExists(
|
|
230
|
-
const
|
|
231
|
-
return (
|
|
719
|
+
entries.map(async entry => {
|
|
720
|
+
const pp = path.join(PLUGINS_DIR, entry, 'index.js');
|
|
721
|
+
if (!await fs.pathExists(pp)) return null;
|
|
722
|
+
const plug = await import(pp);
|
|
723
|
+
return (plug.default?.name && plug.default?.run) ? plug.default : null;
|
|
232
724
|
})
|
|
233
725
|
);
|
|
234
726
|
for (const r of results) {
|
|
@@ -239,299 +731,159 @@ async function loadPlugins() {
|
|
|
239
731
|
}
|
|
240
732
|
|
|
241
733
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
242
|
-
// Pre-flight
|
|
734
|
+
// 🔍 Pre-flight Checks
|
|
243
735
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
244
736
|
|
|
245
737
|
async function runPreflightChecks(stack) {
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
const
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
console.log(chalk.gray(` 💡 Fix: ${PREFLIGHT_HINTS[c.name]}`));
|
|
738
|
+
await printSectionHeader('Pre-flight Checks', '🔍', '#00F5FF');
|
|
739
|
+
|
|
740
|
+
const checks = [
|
|
741
|
+
{ name: 'Node.js ≥ 18', test: () => parseInt(process.version.slice(1)) >= 18 },
|
|
742
|
+
{ name: 'package.json present', test: async () => fs.pathExists(path.join(process.cwd(), 'package.json')) },
|
|
743
|
+
...(stack === 'dotnet-webapi' ? [{ name: '.NET SDK installed', test: () => isCommandAvailable('dotnet') }] : []),
|
|
744
|
+
...(stack === 'java-spring' ? [{ name: 'Java JDK installed', test: () => isCommandAvailable('java') }] : []),
|
|
745
|
+
...(stack === 'python-fastapi'? [{ name: 'Python 3 installed', test: () => isCommandAvailable('python3').then(r => r || isCommandAvailable('python')) }] : []),
|
|
746
|
+
];
|
|
747
|
+
|
|
748
|
+
const failed = [];
|
|
749
|
+
|
|
750
|
+
for (const check of checks) {
|
|
751
|
+
const spin = new LiveSpinner();
|
|
752
|
+
spin.start(chalk.dim('Checking: ') + check.name, { type: 'dots', color: '#00F5FF' });
|
|
753
|
+
await sleep(200 + Math.random() * 200);
|
|
754
|
+
|
|
755
|
+
const pass = await Promise.resolve(check.test());
|
|
756
|
+
if (pass) {
|
|
757
|
+
spin.succeed(rgbText('✓ ', '#00FF9F') + chalk.dim(check.name));
|
|
758
|
+
} else {
|
|
759
|
+
spin.fail(rgbText('✗ ', '#FF006E') + chalk.red.bold(check.name));
|
|
760
|
+
if (PREFLIGHT_HINTS[check.name]) {
|
|
761
|
+
await sleep(30);
|
|
762
|
+
console.log(chalk.gray(` 💡 Fix: ${PREFLIGHT_HINTS[check.name]}`));
|
|
272
763
|
}
|
|
273
|
-
|
|
274
|
-
|
|
764
|
+
failed.push(check);
|
|
765
|
+
}
|
|
766
|
+
await sleep(60);
|
|
275
767
|
}
|
|
276
768
|
|
|
769
|
+
console.log('');
|
|
277
770
|
return failed;
|
|
278
771
|
}
|
|
279
772
|
|
|
280
773
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
281
|
-
//
|
|
282
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
283
|
-
|
|
284
|
-
function buildProgressBar(pct, width = 24) {
|
|
285
|
-
const filled = Math.round((pct / 100) * width);
|
|
286
|
-
return chalk.hex('#00F5FF')('█'.repeat(filled)) + chalk.gray('░'.repeat(width - filled));
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
290
|
-
// Token / Pricing Display
|
|
774
|
+
// 🔧 Validation Helpers
|
|
291
775
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
292
776
|
|
|
293
|
-
function
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
? (extra.filesGenerated / parseFloat(elapsed)).toFixed(1) + ' files/s'
|
|
298
|
-
: null;
|
|
299
|
-
|
|
300
|
-
console.log('');
|
|
301
|
-
console.log(chalk.hex('#BF40FF').bold(' ┌─────────────────────────────────────────────────┐'));
|
|
302
|
-
console.log(chalk.hex('#BF40FF').bold(' │ 📊 Generation Summary (10X) │'));
|
|
303
|
-
console.log(chalk.hex('#BF40FF').bold(' └─────────────────────────────────────────────────┘'));
|
|
304
|
-
console.log('');
|
|
305
|
-
console.log(` ${chalk.dim('Mode:')} ${mode === 'pro' ? chalk.hex('#BF40FF').bold('PRO AI ✦') : chalk.hex('#00F5FF').bold('Standard ◉')}`);
|
|
306
|
-
console.log(` ${chalk.dim('Elapsed:')} ${chalk.white(elapsed + 's')}`);
|
|
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)}`);
|
|
311
|
-
if (mode === 'pro') {
|
|
312
|
-
const usagePct = Math.round((pricing.outputTokens / 16000) * 100);
|
|
313
|
-
console.log(` ${chalk.dim('Input tokens:')} ${chalk.yellow(pricing.inputTokens.toLocaleString())}`);
|
|
314
|
-
console.log(` ${chalk.dim('Output tokens:')} ${chalk.yellow(pricing.outputTokens.toLocaleString())}`);
|
|
315
|
-
console.log(` ${chalk.dim('Token usage:')} [${buildProgressBar(usagePct)}] ${chalk.white(usagePct + '%')}`);
|
|
316
|
-
console.log(` ${chalk.dim('Est. cost:')} ${chalk.green(pricing.cost)}`);
|
|
317
|
-
}
|
|
318
|
-
console.log('');
|
|
777
|
+
function validateProjectName(v) {
|
|
778
|
+
if (!v?.trim()) return '❌ Cannot be empty.';
|
|
779
|
+
if (/[^a-zA-Z0-9_\-.]/.test(v)) return '❌ Use only letters, numbers, hyphens, underscores, dots.';
|
|
780
|
+
if (v.length > 64) return '❌ Name too long (max 64 chars).';
|
|
319
781
|
}
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
// Smart Frontend Scanner
|
|
323
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
324
|
-
|
|
325
|
-
async function detectFrontendFramework(srcDir) {
|
|
326
|
-
try {
|
|
327
|
-
const pkgPath = path.join(process.cwd(), 'package.json');
|
|
328
|
-
if (await fs.pathExists(pkgPath)) {
|
|
329
|
-
const pkg = await fs.readJson(pkgPath);
|
|
330
|
-
const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
|
|
331
|
-
if (deps['next']) return { name: 'Next.js', icon: '▲', color: '#000000' };
|
|
332
|
-
if (deps['nuxt']) return { name: 'Nuxt.js', icon: '💚', color: '#00DC82' };
|
|
333
|
-
if (deps['@angular/core']) return { name: 'Angular', icon: '🅰️', color: '#DD0031' };
|
|
334
|
-
if (deps['react']) return { name: 'React', icon: '⚛️', color: '#61DAFB' };
|
|
335
|
-
if (deps['vue']) return { name: 'Vue.js', icon: '💚', color: '#42B883' };
|
|
336
|
-
if (deps['svelte']) return { name: 'Svelte', icon: '🔥', color: '#FF3E00' };
|
|
337
|
-
if (deps['solid-js']) return { name: 'SolidJS', icon: '💠', color: '#2C4F7C' };
|
|
338
|
-
}
|
|
339
|
-
} catch {}
|
|
340
|
-
return { name: 'Unknown', icon: '📦', color: '#888888' };
|
|
782
|
+
function validateSrcPath(v) {
|
|
783
|
+
if (!v?.trim()) return '❌ Cannot be empty.';
|
|
341
784
|
}
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
346
|
-
|
|
347
|
-
async function getProApiKey() {
|
|
348
|
-
if (await fs.pathExists(CONFIG_PATH)) {
|
|
349
|
-
try {
|
|
350
|
-
const config = await fs.readJson(CONFIG_PATH);
|
|
351
|
-
if (config.apiKey && typeof config.apiKey === 'string' && config.apiKey.length >= 10) {
|
|
352
|
-
const savedAt = config.savedAt ? new Date(config.savedAt) : null;
|
|
353
|
-
const ageHours = savedAt ? ((Date.now() - savedAt.getTime()) / 3600000).toFixed(0) : '?';
|
|
354
|
-
const masked = config.apiKey.slice(0, 4) + '●'.repeat(12) + config.apiKey.slice(-4);
|
|
355
|
-
p.log.success(chalk.green(`Pro Key loaded: ${masked} (saved ${ageHours}h ago)`));
|
|
356
|
-
return config.apiKey;
|
|
357
|
-
}
|
|
358
|
-
} catch {}
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
console.log('');
|
|
362
|
-
console.log(chalk.hex('#BF40FF').bold(' ┌──────────────────────────────────────────────┐'));
|
|
363
|
-
console.log(chalk.hex('#BF40FF').bold(' │ 🧠 Welcome to Backlist PRO AI Mode 🧠 │'));
|
|
364
|
-
console.log(chalk.hex('#BF40FF').bold(' └──────────────────────────────────────────────┘'));
|
|
365
|
-
console.log('');
|
|
366
|
-
console.log(chalk.gray(' Pro Mode uses Llama-4 to intelligently generate'));
|
|
367
|
-
console.log(chalk.gray(' Prisma schemas, JWT auth, and full CRUD backends'));
|
|
368
|
-
console.log(chalk.gray(' from your parsed frontend AST data.'));
|
|
369
|
-
console.log('');
|
|
370
|
-
|
|
371
|
-
const apiKey = await p.password({
|
|
372
|
-
message : 'Enter your Backlist Pro API Key:',
|
|
373
|
-
validate: (input) => {
|
|
374
|
-
if (!input || input.trim().length < 10) return '❌ Invalid key — must be at least 10 characters.';
|
|
375
|
-
if (/\s/.test(input)) return '❌ Key must not contain spaces.';
|
|
376
|
-
},
|
|
377
|
-
});
|
|
378
|
-
if (p.isCancel(apiKey)) { p.cancel('Cancelled.'); process.exit(0); }
|
|
379
|
-
|
|
380
|
-
const spinner = ora({ text: chalk.cyan('Validating API key...'), spinner: 'arc', color: 'cyan' }).start();
|
|
381
|
-
await new Promise(r => setTimeout(r, 1800));
|
|
382
|
-
spinner.succeed(chalk.green('API key validated ✓'));
|
|
383
|
-
|
|
384
|
-
await fs.writeJson(CONFIG_PATH, { apiKey, savedAt: new Date().toISOString() }, { spaces: 2 });
|
|
385
|
-
p.log.info(`Key saved → ${CONFIG_PATH}`);
|
|
386
|
-
return apiKey;
|
|
785
|
+
function validateUrl(v) {
|
|
786
|
+
if (!v?.trim()) return undefined;
|
|
787
|
+
try { new URL(v.trim()); } catch { return '❌ Invalid URL — e.g. http://localhost:3000'; }
|
|
387
788
|
}
|
|
388
789
|
|
|
389
790
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
390
|
-
// Free Mode Pipeline
|
|
791
|
+
// 🏭 Free Mode Pipeline — Fully Animated
|
|
391
792
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
392
793
|
|
|
393
794
|
async function runFreeModePipeline(options) {
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
795
|
+
await printSectionHeader('Standard Mode: AST + EJS Generation', '🚀', '#00F5FF');
|
|
796
|
+
|
|
797
|
+
const t0 = performance.now();
|
|
798
|
+
const steps = new StepRenderer([
|
|
799
|
+
{ label: 'Parsing frontend with Babel AST', status: 'pending' },
|
|
800
|
+
{ label: 'Detecting frontend framework', status: 'pending' },
|
|
801
|
+
{ label: 'Running DOM Live Check', status: 'pending' },
|
|
802
|
+
{ label: 'Scaffolding EJS templates', status: 'pending' },
|
|
803
|
+
{ label: 'Counting & verifying output files', status: 'pending' },
|
|
804
|
+
]);
|
|
397
805
|
|
|
398
|
-
|
|
399
|
-
const steps = 5;
|
|
400
|
-
let stepsDone = 0;
|
|
401
|
-
const stepLabel = () => chalk.dim(`[${++stepsDone}/${steps}]`);
|
|
806
|
+
await steps.start();
|
|
402
807
|
|
|
403
808
|
// Step 1 — AST
|
|
404
|
-
|
|
405
|
-
text : `${stepLabel()} ${chalk.white('Parsing frontend files with Babel AST...')}`,
|
|
406
|
-
spinner: 'dots12',
|
|
407
|
-
color : 'cyan',
|
|
408
|
-
}).start();
|
|
809
|
+
steps.next();
|
|
409
810
|
let endpoints = [];
|
|
410
811
|
try { endpoints = await analyzeFrontend(options.frontendSrcDir); } catch {}
|
|
411
|
-
await
|
|
412
|
-
spinnerAST.succeed(chalk.green(`AST parsing complete — ${chalk.bold(endpoints.length)} endpoint(s) mapped.`));
|
|
413
|
-
|
|
414
|
-
// Step 2 — Framework detection
|
|
415
|
-
const fw = await detectFrontendFramework(options.frontendSrcDir);
|
|
416
|
-
p.log.info(chalk.dim(`${stepLabel()} Frontend detected: ${fw.icon} ${chalk.white(fw.name)}`));
|
|
417
|
-
|
|
418
|
-
// Step 3 — DOM Live Check
|
|
419
|
-
const spinnerDOM = ora({
|
|
420
|
-
text : `${stepLabel()} ${chalk.white('Running DOM Live Check...')}`,
|
|
421
|
-
spinner: 'bouncingBar',
|
|
422
|
-
color : 'yellow',
|
|
423
|
-
}).start();
|
|
424
|
-
const inconsistencies = await performLowCostPathScan(options.frontendSrcDir, endpoints);
|
|
425
|
-
await new Promise(r => setTimeout(r, 1200));
|
|
426
|
-
if (inconsistencies.length > 0) {
|
|
427
|
-
spinnerDOM.warn(chalk.yellow(`DOM Live Check — ${inconsistencies.length} path drift(s) detected.`));
|
|
428
|
-
inconsistencies.slice(0, 3).forEach(i => console.log(chalk.gray(` → ${i.warning}`)));
|
|
429
|
-
} else {
|
|
430
|
-
spinnerDOM.succeed(chalk.green('DOM Live Check passed — ') + chalk.yellow.bold('15% false positives eliminated!'));
|
|
431
|
-
}
|
|
812
|
+
await sleep(400);
|
|
432
813
|
|
|
433
|
-
// Step
|
|
434
|
-
|
|
435
|
-
const
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
814
|
+
// Step 2 — Framework
|
|
815
|
+
steps.next();
|
|
816
|
+
const pkgPath = path.join(process.cwd(), 'package.json');
|
|
817
|
+
let fwName = 'Unknown';
|
|
818
|
+
try {
|
|
819
|
+
const pkg = await fs.readJson(pkgPath);
|
|
820
|
+
const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
|
|
821
|
+
if (deps['next']) fwName = 'Next.js';
|
|
822
|
+
else if (deps['nuxt']) fwName = 'Nuxt.js';
|
|
823
|
+
else if (deps['@angular/core']) fwName = 'Angular';
|
|
824
|
+
else if (deps['react']) fwName = 'React';
|
|
825
|
+
else if (deps['vue']) fwName = 'Vue.js';
|
|
826
|
+
else if (deps['svelte']) fwName = 'Svelte';
|
|
827
|
+
} catch {}
|
|
828
|
+
steps.next(`Detected: ${fwName} · ${endpoints.length} endpoints`);
|
|
829
|
+
await sleep(300);
|
|
830
|
+
|
|
831
|
+
// Step 3 — DOM Check
|
|
832
|
+
steps.next();
|
|
833
|
+
let inconsistencies = [];
|
|
834
|
+
try { inconsistencies = await performLowCostPathScan(options.frontendSrcDir, endpoints); } catch {}
|
|
835
|
+
await sleep(600);
|
|
442
836
|
|
|
837
|
+
// Step 4 — EJS
|
|
838
|
+
steps.next();
|
|
443
839
|
try {
|
|
444
840
|
await dispatchGenerator(options);
|
|
445
|
-
spinnerEJS.succeed(chalk.green(`${stackLabel} backend scaffolded successfully.`));
|
|
446
841
|
} catch (err) {
|
|
447
|
-
|
|
842
|
+
steps.finish();
|
|
448
843
|
throw err;
|
|
449
844
|
}
|
|
845
|
+
await sleep(200);
|
|
450
846
|
|
|
451
|
-
// Step 5 — Count
|
|
452
|
-
|
|
453
|
-
text : `${stepLabel()} ${chalk.white('Counting generated files...')}`,
|
|
454
|
-
spinner: 'line',
|
|
455
|
-
color : 'cyan',
|
|
456
|
-
}).start();
|
|
847
|
+
// Step 5 — Count
|
|
848
|
+
steps.next();
|
|
457
849
|
let fileCount = 0;
|
|
458
|
-
try { fileCount = await globCount(options.projectDir); } catch {}
|
|
459
|
-
spinnerCount.succeed(chalk.green(`${fileCount} file(s) written to ${chalk.bold(options.projectName)}/`));
|
|
460
|
-
|
|
461
|
-
await printFileTreeSummary(options.projectDir);
|
|
462
|
-
|
|
463
|
-
options._meta = {
|
|
464
|
-
endpointCount : endpoints.length,
|
|
465
|
-
filesGenerated : fileCount,
|
|
466
|
-
duration : ((performance.now() - t0) / 1000).toFixed(2),
|
|
467
|
-
};
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
471
|
-
// File Tree Summary (top 2 levels)
|
|
472
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
473
|
-
|
|
474
|
-
async function printFileTreeSummary(dir, depth = 0, maxDepth = 2) {
|
|
475
|
-
if (depth === 0) {
|
|
476
|
-
console.log('');
|
|
477
|
-
console.log(chalk.dim(' ── 📁 Generated Project Structure ─────────────────────'));
|
|
478
|
-
}
|
|
479
850
|
try {
|
|
480
|
-
const
|
|
481
|
-
const
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
const name = entry.isDirectory() ? chalk.white.bold(entry.name) : chalk.dim(entry.name);
|
|
486
|
-
console.log(`${indent}${icon} ${name}`);
|
|
487
|
-
if (entry.isDirectory() && depth < maxDepth - 1) {
|
|
488
|
-
await printFileTreeSummary(path.join(dir, entry.name), depth + 1, maxDepth);
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
if (filtered.length > 12 && depth === 0) {
|
|
492
|
-
console.log(chalk.gray(` ${' '.repeat(depth + 2)}... and ${filtered.length - 12} more`));
|
|
493
|
-
}
|
|
851
|
+
const { glob } = await import('glob');
|
|
852
|
+
const files = await glob(`${options.projectDir.replace(/\\/g, '/')}/**/*`, {
|
|
853
|
+
nodir: true, ignore: ['**/node_modules/**'],
|
|
854
|
+
});
|
|
855
|
+
fileCount = files.length;
|
|
494
856
|
} catch {}
|
|
495
|
-
|
|
496
|
-
}
|
|
857
|
+
await sleep(300);
|
|
497
858
|
|
|
498
|
-
|
|
499
|
-
const { glob } = await import('glob');
|
|
500
|
-
const files = await glob(`${dir.replace(/\\/g, '/')}/**/*`, {
|
|
501
|
-
nodir : true,
|
|
502
|
-
ignore: ['**/node_modules/**'],
|
|
503
|
-
});
|
|
504
|
-
return files.length;
|
|
505
|
-
}
|
|
859
|
+
steps.finish();
|
|
506
860
|
|
|
507
|
-
//
|
|
508
|
-
|
|
509
|
-
|
|
861
|
+
// Summary callout
|
|
862
|
+
await sleep(200);
|
|
863
|
+
console.log(` ${rgbText('◆', '#00F5FF')} AST: ${rgbText(endpoints.length + ' endpoints', '#00FF9F')} · Framework: ${rgbText(fwName, '#BF40FF')} · Files: ${rgbText(String(fileCount), '#FFBE0B')}`);
|
|
864
|
+
if (inconsistencies.length > 0) {
|
|
865
|
+
console.log(` ${rgbText('⚠', '#FFBE0B')} DOM drift: ${chalk.yellow(inconsistencies.length + ' inconsistencies')}`);
|
|
866
|
+
inconsistencies.slice(0, 2).forEach(i => console.log(chalk.gray(` → ${i.warning}`)));
|
|
867
|
+
} else {
|
|
868
|
+
console.log(` ${rgbText('✓', '#00FF9F')} DOM Live Check passed — no path drift`);
|
|
869
|
+
}
|
|
510
870
|
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
'java-spring' : () => generateJavaProject(options),
|
|
518
|
-
'python-fastapi' : () => generatePythonProject(options),
|
|
871
|
+
await printAnimatedFileTree(options.projectDir);
|
|
872
|
+
|
|
873
|
+
options._meta = {
|
|
874
|
+
endpointCount : endpoints.length,
|
|
875
|
+
filesGenerated: fileCount,
|
|
876
|
+
duration : ((performance.now() - t0) / 1000).toFixed(2),
|
|
519
877
|
};
|
|
520
|
-
const runner = gen[options.stack];
|
|
521
|
-
if (!runner) throw new Error(
|
|
522
|
-
`Stack '${options.stack}' is not supported. Valid options: ${Object.keys(gen).join(', ')}`
|
|
523
|
-
);
|
|
524
|
-
await runner();
|
|
525
878
|
}
|
|
526
879
|
|
|
527
880
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
528
|
-
// Pro AI Mode
|
|
881
|
+
// 🧠 Pro AI Mode
|
|
529
882
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
530
883
|
|
|
531
884
|
async function callAIProcessor(astJsonData, apiKey, options) {
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
console.log('');
|
|
885
|
+
await printSectionHeader('Pro Mode: Autonomous AI Agent', '🧠', '#BF40FF');
|
|
886
|
+
|
|
535
887
|
console.log(chalk.gray(` → Model : meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8`));
|
|
536
888
|
console.log(chalk.gray(` → Key : ${apiKey.slice(0, 4)}${'●'.repeat(16)}${apiKey.slice(-4)}`));
|
|
537
889
|
console.log(chalk.gray(` → Input : ${astJsonData.length} endpoint(s) from AST`));
|
|
@@ -539,20 +891,17 @@ async function callAIProcessor(astJsonData, apiKey, options) {
|
|
|
539
891
|
|
|
540
892
|
let thoughtCount = 0;
|
|
541
893
|
let warnCount = 0;
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
spinner: 'mindblown',
|
|
545
|
-
color : 'magenta',
|
|
546
|
-
}).start();
|
|
894
|
+
const spin = new LiveSpinner();
|
|
895
|
+
spin.start('Initialising autonomous agents...', { type: 'matrix', color: '#BF40FF' });
|
|
547
896
|
|
|
548
897
|
const onThought = (msg) => {
|
|
549
898
|
thoughtCount++;
|
|
550
899
|
if (msg.includes('FAILED') || msg.includes('WARNING')) {
|
|
551
900
|
warnCount++;
|
|
552
|
-
|
|
553
|
-
|
|
901
|
+
spin.warn(rgbText(`[${thoughtCount}] ⚠ ${msg}`, '#FFBE0B'));
|
|
902
|
+
spin.start('Recovering & re-reasoning...', { type: 'wave', color: '#FF6B6B' });
|
|
554
903
|
} else {
|
|
555
|
-
|
|
904
|
+
spin.update(`[${thoughtCount}] ${msg}`);
|
|
556
905
|
}
|
|
557
906
|
};
|
|
558
907
|
|
|
@@ -569,358 +918,286 @@ async function callAIProcessor(astJsonData, apiKey, options) {
|
|
|
569
918
|
const deployData = await aiAgent.generateDeploymentConfig(options.stack, astJsonData);
|
|
570
919
|
|
|
571
920
|
await aiAgent.dispose();
|
|
572
|
-
|
|
921
|
+
spin.succeed(`Reasoning complete — ${rgbText(thoughtCount + ' thoughts', '#00FF9F')} · ${rgbText(warnCount + ' warnings', '#FFBE0B')}`);
|
|
573
922
|
|
|
574
923
|
return { ...finalBlocks, deployment: deployData };
|
|
575
924
|
}
|
|
576
925
|
|
|
577
926
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
578
|
-
//
|
|
927
|
+
// 🔑 API Key
|
|
579
928
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
580
929
|
|
|
581
|
-
function
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
return chalk.red.bold(`${s}% C`);
|
|
593
|
-
};
|
|
594
|
-
const bar = (s) => buildProgressBar(s, 16);
|
|
595
|
-
|
|
596
|
-
console.log('');
|
|
597
|
-
console.log(chalk.hex('#BF40FF').bold(' ╔══════════════════════════════════════════════════╗'));
|
|
598
|
-
console.log(chalk.hex('#BF40FF').bold(' ║ 📊 SYSTEM HEALTH DASHBOARD v8.0-10X ║'));
|
|
599
|
-
console.log(chalk.hex('#BF40FF').bold(' ╚══════════════════════════════════════════════════╝'));
|
|
600
|
-
console.log('');
|
|
601
|
-
console.log(` 🛡️ Security Profile: [${bar(secScore)}] ${colorScore(secScore)}`);
|
|
602
|
-
console.log(` 🏛️ Hexagonal Compliance: [${bar(archScore)}] ${colorScore(archScore)}`);
|
|
603
|
-
console.log(` 🧪 Test Coverage (Gen): [${bar(testScore)}] ${colorScore(testScore)}`);
|
|
604
|
-
console.log(` 📦 Dependency Health: [${bar(depsScore)}] ${colorScore(depsScore)}`);
|
|
605
|
-
console.log('');
|
|
606
|
-
console.log(` 🏆 Overall Score: [${bar(overall)}] ${colorScore(overall)}`);
|
|
607
|
-
console.log('');
|
|
608
|
-
if (options.stack) {
|
|
609
|
-
const meta = STACK_META[options.stack] || {};
|
|
610
|
-
console.log(chalk.dim(` Stack: ${meta.icon || ''} ${options.stack} (${meta.lang || ''} / ${meta.runtime || ''})`));
|
|
930
|
+
async function getProApiKey() {
|
|
931
|
+
if (await fs.pathExists(CONFIG_PATH)) {
|
|
932
|
+
try {
|
|
933
|
+
const cfg = await fs.readJson(CONFIG_PATH);
|
|
934
|
+
if (cfg.apiKey?.length >= 10) {
|
|
935
|
+
const masked = cfg.apiKey.slice(0, 4) + '●'.repeat(12) + cfg.apiKey.slice(-4);
|
|
936
|
+
const ageHours = cfg.savedAt ? ((Date.now() - new Date(cfg.savedAt).getTime()) / 3600000).toFixed(0) : '?';
|
|
937
|
+
console.log(` ${rgbText('✓', '#00FF9F')} Pro Key: ${rgbText(masked, '#BF40FF')} ${chalk.gray(`(saved ${ageHours}h ago)`)}`);
|
|
938
|
+
return cfg.apiKey;
|
|
939
|
+
}
|
|
940
|
+
} catch {}
|
|
611
941
|
}
|
|
612
|
-
|
|
613
|
-
|
|
942
|
+
|
|
943
|
+
await printSectionHeader('Backlist PRO AI Mode', '🧠', '#BF40FF');
|
|
944
|
+
console.log(chalk.gray(' Llama-4 · Prisma schema generation · JWT auth · Full CRUD'));
|
|
614
945
|
console.log('');
|
|
615
|
-
}
|
|
616
946
|
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
947
|
+
const apiKey = await p.password({
|
|
948
|
+
message : 'Enter your Backlist Pro API Key:',
|
|
949
|
+
validate: input => {
|
|
950
|
+
if (!input?.trim() || input.trim().length < 10) return '❌ Invalid key — must be ≥ 10 chars.';
|
|
951
|
+
if (/\s/.test(input)) return '❌ Key must not contain spaces.';
|
|
952
|
+
},
|
|
953
|
+
});
|
|
954
|
+
if (p.isCancel(apiKey)) { p.cancel('Cancelled.'); process.exit(0); }
|
|
620
955
|
|
|
621
|
-
|
|
622
|
-
|
|
956
|
+
const spin = new LiveSpinner();
|
|
957
|
+
spin.start('Validating API key...', { type: 'arc', color: '#BF40FF' });
|
|
958
|
+
await sleep(1800);
|
|
959
|
+
spin.succeed('API key validated ✓');
|
|
623
960
|
|
|
624
|
-
|
|
625
|
-
console.log(chalk.
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
console.log(` ${chalk.dim('1.')} ${chalk.white(`cd ${projectName}`)}`);
|
|
961
|
+
await fs.writeJson(CONFIG_PATH, { apiKey, savedAt: new Date().toISOString() }, { spaces: 2 });
|
|
962
|
+
console.log(chalk.gray(` → Saved to ${CONFIG_PATH}`));
|
|
963
|
+
return apiKey;
|
|
964
|
+
}
|
|
629
965
|
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
console.log(` ${chalk.dim('3.')} ${chalk.white('npx prisma migrate dev --name init')}`);
|
|
634
|
-
console.log(` ${chalk.dim('4.')} ${chalk.white('npm run dev')}`);
|
|
635
|
-
} else {
|
|
636
|
-
console.log(` ${chalk.dim('3.')} ${chalk.white('npm run dev')}`);
|
|
637
|
-
}
|
|
638
|
-
} else if (stack === 'python-fastapi') {
|
|
639
|
-
console.log(` ${chalk.dim('2.')} ${chalk.white('pip install -r requirements.txt')}`);
|
|
640
|
-
console.log(` ${chalk.dim('3.')} ${chalk.white('uvicorn main:app --reload')}`);
|
|
641
|
-
} else if (stack === 'dotnet-webapi') {
|
|
642
|
-
console.log(` ${chalk.dim('2.')} ${chalk.white('dotnet restore')}`);
|
|
643
|
-
console.log(` ${chalk.dim('3.')} ${chalk.white('dotnet run')}`);
|
|
644
|
-
} else if (stack === 'java-spring') {
|
|
645
|
-
console.log(` ${chalk.dim('2.')} ${chalk.white('./mvnw spring-boot:run')}`);
|
|
646
|
-
}
|
|
966
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
967
|
+
// ⚙️ Generator Dispatcher
|
|
968
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
647
969
|
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
970
|
+
async function dispatchGenerator(options) {
|
|
971
|
+
const gen = {
|
|
972
|
+
'node-ts-express': () => generateNodeProject(options),
|
|
973
|
+
'js-express' : () => generateJsProject(options),
|
|
974
|
+
'nestjs' : () => generateNestProject(options),
|
|
975
|
+
'dotnet-webapi' : () => generateDotnetProject(options),
|
|
976
|
+
'java-spring' : () => generateJavaProject(options),
|
|
977
|
+
'python-fastapi' : () => generatePythonProject(options),
|
|
978
|
+
};
|
|
979
|
+
const runner = gen[options.stack];
|
|
980
|
+
if (!runner) throw new Error(`Stack '${options.stack}' not supported. Valid: ${Object.keys(gen).join(', ')}`);
|
|
981
|
+
await runner();
|
|
655
982
|
}
|
|
656
983
|
|
|
657
984
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
658
|
-
// Config Manager
|
|
985
|
+
// ⚙️ Config Manager
|
|
659
986
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
660
987
|
|
|
661
988
|
async function runConfigManager() {
|
|
662
|
-
|
|
663
|
-
console.log(chalk.hex('#BF40FF').bold(' ── ⚙️ Configuration Manager ──────────────────────────'));
|
|
664
|
-
console.log('');
|
|
989
|
+
await printSectionHeader('Configuration Manager', '⚙️', '#BF40FF');
|
|
665
990
|
|
|
666
|
-
|
|
667
|
-
const sessExists = await fs.pathExists(SESSIONS_PATH);
|
|
668
|
-
|
|
669
|
-
if (exists) {
|
|
991
|
+
if (await fs.pathExists(CONFIG_PATH)) {
|
|
670
992
|
try {
|
|
671
993
|
const cfg = await fs.readJson(CONFIG_PATH);
|
|
672
|
-
const masked = cfg.apiKey
|
|
673
|
-
|
|
674
|
-
: 'none';
|
|
675
|
-
console.log(` ${chalk.dim('API Key:')} ${chalk.white(masked)}`);
|
|
994
|
+
const masked = cfg.apiKey ? cfg.apiKey.slice(0, 4) + '●'.repeat(12) + cfg.apiKey.slice(-4) : 'none';
|
|
995
|
+
console.log(` ${chalk.dim('API Key:')} ${rgbText(masked, '#BF40FF')}`);
|
|
676
996
|
console.log(` ${chalk.dim('Saved at:')} ${chalk.gray(cfg.savedAt || 'unknown')}`);
|
|
677
997
|
} catch {}
|
|
678
998
|
} else {
|
|
679
|
-
console.log(chalk.gray(' No saved API key
|
|
999
|
+
console.log(chalk.gray(' No saved API key.'));
|
|
680
1000
|
}
|
|
681
1001
|
|
|
682
|
-
if (
|
|
1002
|
+
if (await fs.pathExists(SESSIONS_PATH)) {
|
|
683
1003
|
try {
|
|
684
1004
|
const sess = await fs.readJson(SESSIONS_PATH);
|
|
685
1005
|
if (sess.last) {
|
|
686
|
-
console.log(` ${chalk.dim('Last project:')} ${
|
|
687
|
-
console.log(` ${chalk.dim('Last stack:')} ${
|
|
688
|
-
console.log(` ${chalk.dim('Last mode:')} ${
|
|
689
|
-
console.log(` ${chalk.dim('Saved at:')} ${chalk.gray(sess.last.savedAt?.slice(0, 16) || 'unknown')}`);
|
|
1006
|
+
console.log(` ${chalk.dim('Last project:')} ${rgbText(sess.last.projectName || '–', '#00F5FF')}`);
|
|
1007
|
+
console.log(` ${chalk.dim('Last stack:')} ${rgbText(sess.last.stack || '–', '#00F5FF')}`);
|
|
1008
|
+
console.log(` ${chalk.dim('Last mode:')} ${rgbText(sess.last.generationMode || '–', '#00F5FF')}`);
|
|
690
1009
|
}
|
|
691
1010
|
} catch {}
|
|
692
1011
|
}
|
|
693
1012
|
|
|
694
1013
|
console.log('');
|
|
695
|
-
|
|
696
1014
|
const action = await p.select({
|
|
697
|
-
message: '
|
|
1015
|
+
message: 'Action:',
|
|
698
1016
|
options: [
|
|
699
|
-
{ value: 'qa-post', label: '
|
|
700
|
-
{ value: 'clear-key', label: '🗑️ Clear
|
|
1017
|
+
{ value: 'qa-post', label: '🔬 Post-Gen Validation' },
|
|
1018
|
+
{ value: 'clear-key', label: '🗑️ Clear API key' },
|
|
701
1019
|
{ value: 'clear-sess', label: '🗑️ Clear session history' },
|
|
702
1020
|
{ value: 'clear-all', label: '💥 Clear everything' },
|
|
703
|
-
{ value: 'back', label: '← Back
|
|
1021
|
+
{ value: 'back', label: '← Back' },
|
|
704
1022
|
],
|
|
705
1023
|
});
|
|
706
1024
|
if (p.isCancel(action) || action === 'back') return;
|
|
707
|
-
|
|
708
|
-
if (action === '
|
|
709
|
-
|
|
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
|
-
}
|
|
1025
|
+
if (action === 'qa-post') { await initQASystem(); await autoRunPostGeneration(); return; }
|
|
1026
|
+
if (action === 'clear-key' || action === 'clear-all') { await fs.remove(CONFIG_PATH); p.log.success('API key cleared.'); }
|
|
1027
|
+
if (action === 'clear-sess' || action === 'clear-all') { await fs.remove(SESSIONS_PATH); p.log.success('Session history cleared.'); }
|
|
721
1028
|
}
|
|
722
1029
|
|
|
723
1030
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
724
|
-
// Plugin Manager
|
|
1031
|
+
// 🔌 Plugin Manager
|
|
725
1032
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
726
1033
|
|
|
727
1034
|
async function runPluginManager(plugins) {
|
|
728
|
-
|
|
729
|
-
console.log(chalk.hex('#BF40FF').bold(' ── 🔌 Plugin Manager ──────────────────────────────────'));
|
|
730
|
-
console.log('');
|
|
731
|
-
|
|
1035
|
+
await printSectionHeader('Plugin Manager', '🔌', '#BF40FF');
|
|
732
1036
|
if (plugins.length === 0) {
|
|
733
1037
|
console.log(chalk.gray(' No plugins installed.'));
|
|
734
|
-
console.log(chalk.dim(` Drop
|
|
735
|
-
console.log(chalk.dim(' Each plugin needs index.js exporting { name, description, run }.'));
|
|
736
|
-
console.log('');
|
|
1038
|
+
console.log(chalk.dim(` Drop plugins into: ${PLUGINS_DIR}`));
|
|
737
1039
|
return;
|
|
738
1040
|
}
|
|
739
|
-
|
|
740
1041
|
const choice = await p.select({
|
|
741
|
-
message: 'Select
|
|
1042
|
+
message: 'Select plugin:',
|
|
742
1043
|
options: plugins.map(pl => ({ value: pl.name, label: `🔌 ${pl.name}`, hint: pl.description || '' })),
|
|
743
1044
|
});
|
|
744
1045
|
if (p.isCancel(choice)) return;
|
|
745
|
-
|
|
746
1046
|
const plugin = plugins.find(pl => pl.name === choice);
|
|
747
1047
|
if (plugin) {
|
|
748
|
-
try {
|
|
749
|
-
|
|
750
|
-
} catch (err) {
|
|
751
|
-
p.log.error(chalk.red(`Plugin error: ${err.message}`));
|
|
752
|
-
}
|
|
1048
|
+
try { await plugin.run({ chalk, ora, p, fs, path }); }
|
|
1049
|
+
catch (err) { p.log.error(chalk.red(`Plugin error: ${err.message}`)); }
|
|
753
1050
|
}
|
|
754
1051
|
}
|
|
755
1052
|
|
|
756
1053
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
757
|
-
//
|
|
1054
|
+
// 🔂 Repeat Last
|
|
758
1055
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
759
1056
|
|
|
760
1057
|
async function promptRepeatLast(lastSession) {
|
|
761
1058
|
if (!lastSession) return false;
|
|
762
1059
|
const meta = STACK_META[lastSession.stack] || {};
|
|
763
1060
|
const icon = meta.icon || '⚙️';
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
)
|
|
768
|
-
|
|
1061
|
+
|
|
1062
|
+
process.stdout.write(HIDE);
|
|
1063
|
+
const label = ` ⚡ Last: ${icon} ${lastSession.stack} · ${lastSession.generationMode} · ${lastSession.savedAt?.slice(0, 10) || ''}`;
|
|
1064
|
+
for (let i = 0; i <= label.length; i += 2) {
|
|
1065
|
+
process.stdout.write(CLEAR_LINE + chalk.dim(label.slice(0, i)));
|
|
1066
|
+
await sleep(6);
|
|
1067
|
+
}
|
|
1068
|
+
process.stdout.write(CLEAR_LINE + chalk.dim(label) + '\n');
|
|
1069
|
+
process.stdout.write(SHOW + '\n');
|
|
1070
|
+
|
|
769
1071
|
const repeat = await p.confirm({
|
|
770
|
-
message :
|
|
1072
|
+
message : gradientText('Repeat last generation with same settings?'),
|
|
771
1073
|
initialValue: false,
|
|
772
1074
|
});
|
|
773
1075
|
return !p.isCancel(repeat) && repeat;
|
|
774
1076
|
}
|
|
775
1077
|
|
|
776
1078
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
777
|
-
//
|
|
1079
|
+
// 🚦 Graceful Shutdown
|
|
778
1080
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
779
1081
|
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
function validateUrl(v) {
|
|
793
|
-
if (!v || !v.trim()) return undefined; // optional
|
|
794
|
-
try { new URL(v.trim()); return undefined; }
|
|
795
|
-
catch { return '❌ Invalid URL — e.g. http://localhost:3000'; }
|
|
1082
|
+
let _cleanupDir = null;
|
|
1083
|
+
async function gracefulShutdown(signal) {
|
|
1084
|
+
process.stdout.write(SHOW);
|
|
1085
|
+
console.log('');
|
|
1086
|
+
await typewrite(`\n ⚠️ ${signal} received — shutting down...\n`, 15, chalk.yellow);
|
|
1087
|
+
if (_cleanupDir && await fs.pathExists(_cleanupDir)) {
|
|
1088
|
+
const spin = new LiveSpinner();
|
|
1089
|
+
spin.start('Cleaning up partial output...', { type: 'arc', color: '#FFBE0B' });
|
|
1090
|
+
await fs.remove(_cleanupDir).catch(() => {});
|
|
1091
|
+
spin.succeed('Cleanup complete.');
|
|
1092
|
+
}
|
|
1093
|
+
process.exit(0);
|
|
796
1094
|
}
|
|
1095
|
+
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
|
|
1096
|
+
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
|
|
797
1097
|
|
|
798
1098
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
799
|
-
// Main CLI
|
|
1099
|
+
// 🎯 Main CLI
|
|
800
1100
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
801
1101
|
|
|
802
1102
|
async function main() {
|
|
803
1103
|
const globalStart = performance.now();
|
|
804
|
-
|
|
1104
|
+
await printAnimatedBanner();
|
|
805
1105
|
|
|
806
|
-
// Load plugins & last session in parallel
|
|
807
1106
|
const [plugins, lastSession] = await Promise.all([loadPlugins(), loadLastSession()]);
|
|
808
|
-
|
|
809
1107
|
if (plugins.length > 0) {
|
|
810
|
-
|
|
1108
|
+
console.log(chalk.dim(` 🔌 ${plugins.length} plugin(s) loaded: ${plugins.map(p => p.name).join(', ')}\n`));
|
|
811
1109
|
}
|
|
812
1110
|
|
|
813
|
-
|
|
1111
|
+
// Animated intro line
|
|
1112
|
+
await typewrite(
|
|
1113
|
+
' ◆ create-backlist v9.0-ULTRA — Polyglot Backend Generator\n',
|
|
1114
|
+
10,
|
|
1115
|
+
s => rgbText(s, '#00F5FF')
|
|
1116
|
+
);
|
|
1117
|
+
console.log('');
|
|
814
1118
|
|
|
815
|
-
// ──
|
|
1119
|
+
// ── Repeat last shortcut ───────────────────────────────────────────────
|
|
816
1120
|
if (lastSession?.stack) {
|
|
817
1121
|
const repeated = await promptRepeatLast(lastSession);
|
|
818
1122
|
if (repeated) {
|
|
819
1123
|
const projectName = await p.text({
|
|
820
|
-
message
|
|
821
|
-
placeholder : 'backend',
|
|
822
|
-
|
|
823
|
-
validate : validateProjectName,
|
|
1124
|
+
message: 'Backend directory name:',
|
|
1125
|
+
placeholder: 'backend', defaultValue: 'backend',
|
|
1126
|
+
validate: validateProjectName,
|
|
824
1127
|
});
|
|
825
1128
|
if (p.isCancel(projectName)) { p.cancel('Cancelled.'); process.exit(0); }
|
|
826
1129
|
|
|
827
1130
|
const srcPath = await p.text({
|
|
828
|
-
message
|
|
829
|
-
placeholder : 'src',
|
|
830
|
-
|
|
831
|
-
validate : validateSrcPath,
|
|
1131
|
+
message: 'Path to frontend src:',
|
|
1132
|
+
placeholder: 'src', defaultValue: 'src',
|
|
1133
|
+
validate: validateSrcPath,
|
|
832
1134
|
});
|
|
833
1135
|
if (p.isCancel(srcPath)) { p.cancel('Cancelled.'); process.exit(0); }
|
|
834
1136
|
|
|
835
1137
|
const options = {
|
|
836
|
-
generationMode
|
|
837
|
-
projectName,
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
projectDir : path.resolve(process.cwd(), projectName),
|
|
845
|
-
frontendSrcDir : path.resolve(process.cwd(), srcPath),
|
|
1138
|
+
generationMode: lastSession.generationMode,
|
|
1139
|
+
projectName, stack: lastSession.stack, srcPath,
|
|
1140
|
+
dbType : lastSession.dbType || 'mongoose',
|
|
1141
|
+
addAuth : lastSession.addAuth ?? true,
|
|
1142
|
+
addSeeder : lastSession.addSeeder ?? true,
|
|
1143
|
+
extraFeatures : lastSession.extraFeatures ?? ['docker','testing','swagger'],
|
|
1144
|
+
projectDir : path.resolve(process.cwd(), projectName),
|
|
1145
|
+
frontendSrcDir: path.resolve(process.cwd(), srcPath),
|
|
846
1146
|
};
|
|
847
1147
|
|
|
848
|
-
printSessionDiff(lastSession, options);
|
|
1148
|
+
await printSessionDiff(lastSession, options);
|
|
849
1149
|
await executeGeneration(options, globalStart, plugins);
|
|
850
1150
|
return;
|
|
851
1151
|
}
|
|
852
1152
|
}
|
|
853
1153
|
|
|
854
|
-
// ──
|
|
1154
|
+
// ── Mode Selection ─────────────────────────────────────────────────────
|
|
855
1155
|
const mode = await p.select({
|
|
856
|
-
message: 'Select
|
|
1156
|
+
message: 'Select mode:',
|
|
857
1157
|
options: [
|
|
858
|
-
{ value: 'free', label: '🚀 Standard Mode',
|
|
859
|
-
{ value: 'pro', label: '🧠 Pro AI Mode',
|
|
860
|
-
{ value: 'qa-url', label: '🌐 URL
|
|
861
|
-
{ value: 'qa-manual', label: '🧪 Manual QA
|
|
862
|
-
{ value: 'qa-auto', label: '🤖 Automated QA',
|
|
863
|
-
{ value: 'qa-live', label: '⚡ Live
|
|
864
|
-
{ value: 'qa-post', label: '🔬 Post-Gen Validation',
|
|
865
|
-
{ value: 'qa-history', label: '📜 QA History',
|
|
866
|
-
{ value: 'config', label: '⚙️ Config
|
|
867
|
-
...(plugins.length > 0
|
|
868
|
-
? [{ value: 'plugins', label: '🔌 Plugins', hint: `${plugins.length} installed` }]
|
|
869
|
-
: []
|
|
870
|
-
),
|
|
1158
|
+
{ value: 'free', label: '🚀 Standard Mode', hint: 'Free — AST + EJS + DOM Check' },
|
|
1159
|
+
{ value: 'pro', label: '🧠 Pro AI Mode', hint: 'Llama-4 · Schema + Auth generation' },
|
|
1160
|
+
{ value: 'qa-url', label: '🌐 URL QA Scan', hint: 'Real browser + HTTP security + SEO' },
|
|
1161
|
+
{ value: 'qa-manual', label: '🧪 Manual QA', hint: 'Interactive test cases' },
|
|
1162
|
+
{ value: 'qa-auto', label: '🤖 Automated QA', hint: 'Full test suite' },
|
|
1163
|
+
{ value: 'qa-live', label: '⚡ Live QA Monitor', hint: 'Reruns every 30s' },
|
|
1164
|
+
{ value: 'qa-post', label: '🔬 Post-Gen Validation', hint: 'Validate generated project' },
|
|
1165
|
+
{ value: 'qa-history', label: '📜 QA History', hint: 'Browse past runs' },
|
|
1166
|
+
{ value: 'config', label: '⚙️ Config', hint: 'API keys & sessions' },
|
|
1167
|
+
...(plugins.length > 0 ? [{ value: 'plugins', label: '🔌 Plugins', hint: `${plugins.length} installed` }] : []),
|
|
871
1168
|
],
|
|
872
1169
|
});
|
|
873
1170
|
if (p.isCancel(mode)) { p.cancel('Cancelled.'); process.exit(0); }
|
|
874
1171
|
|
|
875
|
-
// ──
|
|
876
|
-
|
|
877
|
-
// v10.0 — URL-Based QA
|
|
1172
|
+
// ── QA & utility modes ────────────────────────────────────────────────
|
|
878
1173
|
if (mode === 'qa-url') {
|
|
879
1174
|
await initQASystem();
|
|
880
|
-
|
|
881
|
-
const localUrl = await p.text({
|
|
882
|
-
message : 'Localhost URL:',
|
|
883
|
-
placeholder: 'http://localhost:3000',
|
|
884
|
-
validate : validateUrl,
|
|
885
|
-
});
|
|
1175
|
+
const localUrl = await p.text({ message: 'Localhost URL:', placeholder: 'http://localhost:3000', validate: validateUrl });
|
|
886
1176
|
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
|
-
});
|
|
1177
|
+
const prodUrl = await p.text({ message: 'Production URL (blank to skip):', placeholder: 'https://yoursite.com', validate: validateUrl });
|
|
893
1178
|
if (p.isCancel(prodUrl)) { p.cancel('Cancelled.'); return; }
|
|
894
|
-
|
|
895
|
-
await
|
|
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.'));
|
|
1179
|
+
await runUrlQA({ localUrl: String(localUrl).trim() || undefined, prodUrl: String(prodUrl).trim() || undefined });
|
|
1180
|
+
await typewrite('\n ✓ URL QA scan complete.\n', 12, s => rgbText(s, '#00F5FF'));
|
|
901
1181
|
return;
|
|
902
1182
|
}
|
|
903
|
-
|
|
904
1183
|
if (mode === 'qa-manual') { await initQASystem(); await runManualQA(); return; }
|
|
905
1184
|
if (mode === 'qa-auto') { await initQASystem(); await runAutomatedQA({ continuous: false }); return; }
|
|
906
1185
|
if (mode === 'qa-live') { await initQASystem(); await runAutomatedQA({ continuous: true }); return; }
|
|
907
1186
|
if (mode === 'qa-post') { await initQASystem(); await autoRunPostGeneration(); return; }
|
|
908
|
-
if (mode === 'qa-history') { await viewQAHistory();
|
|
909
|
-
if (mode === 'config') { await runConfigManager();
|
|
910
|
-
if (mode === 'plugins') { await runPluginManager(plugins);
|
|
1187
|
+
if (mode === 'qa-history') { await viewQAHistory(); return; }
|
|
1188
|
+
if (mode === 'config') { await runConfigManager(); return; }
|
|
1189
|
+
if (mode === 'plugins') { await runPluginManager(plugins); return; }
|
|
911
1190
|
|
|
912
|
-
const generationMode = mode;
|
|
1191
|
+
const generationMode = mode;
|
|
913
1192
|
|
|
914
|
-
// ── Project
|
|
1193
|
+
// ── Project name ──────────────────────────────────────────────────────
|
|
915
1194
|
const projectName = await p.text({
|
|
916
|
-
message
|
|
917
|
-
placeholder : 'backend',
|
|
918
|
-
|
|
919
|
-
validate : validateProjectName,
|
|
1195
|
+
message: 'Backend directory name:',
|
|
1196
|
+
placeholder: 'backend', defaultValue: 'backend',
|
|
1197
|
+
validate: validateProjectName,
|
|
920
1198
|
});
|
|
921
1199
|
if (p.isCancel(projectName)) { p.cancel('Cancelled.'); process.exit(0); }
|
|
922
1200
|
|
|
923
|
-
// Warn if directory already exists
|
|
924
1201
|
const targetDir = path.resolve(process.cwd(), projectName);
|
|
925
1202
|
if (await fs.pathExists(targetDir)) {
|
|
926
1203
|
p.log.warn(chalk.yellow(`⚠️ Directory '${projectName}' already exists — files may be overwritten.`));
|
|
@@ -928,7 +1205,7 @@ async function main() {
|
|
|
928
1205
|
if (p.isCancel(cont) || !cont) { p.cancel('Aborted.'); process.exit(0); }
|
|
929
1206
|
}
|
|
930
1207
|
|
|
931
|
-
// ── Stack
|
|
1208
|
+
// ── Stack ─────────────────────────────────────────────────────────────
|
|
932
1209
|
const stack = await p.select({
|
|
933
1210
|
message: 'Select backend stack:',
|
|
934
1211
|
options: [
|
|
@@ -942,7 +1219,7 @@ async function main() {
|
|
|
942
1219
|
});
|
|
943
1220
|
if (p.isCancel(stack)) { p.cancel('Cancelled.'); process.exit(0); }
|
|
944
1221
|
|
|
945
|
-
// ── Pre-flight
|
|
1222
|
+
// ── Pre-flight ────────────────────────────────────────────────────────
|
|
946
1223
|
const failedChecks = await runPreflightChecks(stack);
|
|
947
1224
|
if (failedChecks.length > 0) {
|
|
948
1225
|
p.log.warn(chalk.yellow(`${failedChecks.length} pre-flight check(s) failed.`));
|
|
@@ -950,27 +1227,23 @@ async function main() {
|
|
|
950
1227
|
if (p.isCancel(cont) || !cont) { p.cancel('Aborted.'); process.exit(0); }
|
|
951
1228
|
}
|
|
952
1229
|
|
|
953
|
-
// ──
|
|
1230
|
+
// ── Src path ──────────────────────────────────────────────────────────
|
|
954
1231
|
const srcPath = await p.text({
|
|
955
|
-
message
|
|
956
|
-
placeholder : 'src',
|
|
957
|
-
|
|
958
|
-
validate : validateSrcPath,
|
|
1232
|
+
message: 'Path to frontend src directory:',
|
|
1233
|
+
placeholder: 'src', defaultValue: 'src',
|
|
1234
|
+
validate: validateSrcPath,
|
|
959
1235
|
});
|
|
960
1236
|
if (p.isCancel(srcPath)) { p.cancel('Cancelled.'); process.exit(0); }
|
|
961
1237
|
|
|
962
1238
|
const resolvedSrc = path.resolve(process.cwd(), srcPath);
|
|
963
1239
|
if (!await fs.pathExists(resolvedSrc)) {
|
|
964
|
-
p.log.warn(chalk.yellow(`⚠️
|
|
1240
|
+
p.log.warn(chalk.yellow(`⚠️ '${srcPath}' not found — AST may return 0 endpoints.`));
|
|
965
1241
|
}
|
|
966
1242
|
|
|
967
|
-
// ── Node-specific options
|
|
968
|
-
let dbType
|
|
969
|
-
let
|
|
970
|
-
|
|
971
|
-
let extraFeatures = ['docker', 'testing', 'swagger'];
|
|
972
|
-
|
|
973
|
-
const isNodeStack = ['node-ts-express', 'js-express', 'nestjs'].includes(stack);
|
|
1243
|
+
// ── Node-specific options ─────────────────────────────────────────────
|
|
1244
|
+
let dbType = 'mongoose', addAuth = true, addSeeder = true;
|
|
1245
|
+
let extraFeatures = ['docker','testing','swagger'];
|
|
1246
|
+
const isNodeStack = ['node-ts-express','js-express','nestjs'].includes(stack);
|
|
974
1247
|
|
|
975
1248
|
if (generationMode === 'free' && isNodeStack) {
|
|
976
1249
|
dbType = await p.select({
|
|
@@ -982,59 +1255,65 @@ async function main() {
|
|
|
982
1255
|
});
|
|
983
1256
|
if (p.isCancel(dbType)) { p.cancel('Cancelled.'); process.exit(0); }
|
|
984
1257
|
|
|
985
|
-
addAuth = await p.confirm({ message: 'Add JWT authentication
|
|
1258
|
+
addAuth = await p.confirm({ message: 'Add JWT authentication?', initialValue: true });
|
|
986
1259
|
if (p.isCancel(addAuth)) { p.cancel('Cancelled.'); process.exit(0); }
|
|
987
1260
|
|
|
988
|
-
addSeeder = await p.confirm({ message: 'Add database seeder
|
|
1261
|
+
addSeeder = await p.confirm({ message: 'Add database seeder?', initialValue: true });
|
|
989
1262
|
if (p.isCancel(addSeeder)) { p.cancel('Cancelled.'); process.exit(0); }
|
|
990
1263
|
|
|
991
1264
|
extraFeatures = await p.multiselect({
|
|
992
1265
|
message: 'Additional features:',
|
|
993
1266
|
options: [
|
|
994
|
-
{ value: 'docker', label: '🐳 Docker Support',
|
|
995
|
-
{ value: 'testing', label: '🧪 API Testing Boilerplate'
|
|
996
|
-
{ value: 'swagger', label: '📖 Swagger UI
|
|
997
|
-
{ value: 'ci', label: '⚙️ GitHub Actions CI'
|
|
1267
|
+
{ value: 'docker', label: '🐳 Docker Support', hint: 'Dockerfile + compose' },
|
|
1268
|
+
{ value: 'testing', label: '🧪 API Testing Boilerplate' },
|
|
1269
|
+
{ value: 'swagger', label: '📖 Swagger UI' },
|
|
1270
|
+
{ value: 'ci', label: '⚙️ GitHub Actions CI' },
|
|
998
1271
|
],
|
|
999
|
-
initialValues: ['docker',
|
|
1272
|
+
initialValues: ['docker','testing','swagger'],
|
|
1000
1273
|
});
|
|
1001
1274
|
if (p.isCancel(extraFeatures)) { p.cancel('Cancelled.'); process.exit(0); }
|
|
1002
1275
|
}
|
|
1003
1276
|
|
|
1004
|
-
// ──
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1277
|
+
// ── Animated generation plan ──────────────────────────────────────────
|
|
1278
|
+
await printSectionHeader('Generation Plan', '📋', '#BF40FF');
|
|
1279
|
+
|
|
1280
|
+
const meta = STACK_META[stack] || {};
|
|
1281
|
+
const planItems = [
|
|
1282
|
+
{ label: 'Project', value: projectName, color: '#00FF9F' },
|
|
1283
|
+
{ label: 'Stack', value: `${meta.icon || ''} ${stack}`, color: '#00F5FF' },
|
|
1284
|
+
{ label: 'Language', value: meta.lang || 'N/A', color: '#BF40FF' },
|
|
1285
|
+
{ label: 'Runtime', value: meta.runtime || 'N/A', color: '#BF40FF' },
|
|
1286
|
+
{ label: 'Mode', value: generationMode === 'pro' ? 'PRO AI ✦' : 'Standard ◉', color: generationMode === 'pro' ? '#BF40FF' : '#00F5FF' },
|
|
1287
|
+
...(isNodeStack ? [
|
|
1288
|
+
{ label: 'Database', value: dbType, color: '#00F5FF' },
|
|
1289
|
+
{ label: 'Auth JWT', value: addAuth ? 'Yes' : 'No', color: addAuth ? '#00FF9F' : '#FF006E' },
|
|
1290
|
+
{ label: 'Seeder', value: addSeeder ? 'Yes' : 'No', color: addSeeder ? '#00FF9F' : '#FF006E' },
|
|
1291
|
+
{ label: 'Extras', value: extraFeatures.join(', ') || 'none', color: '#FFBE0B' },
|
|
1292
|
+
] : []),
|
|
1293
|
+
{ label: 'Output', value: targetDir, color: '#64748b' },
|
|
1294
|
+
];
|
|
1295
|
+
|
|
1296
|
+
for (const item of planItems) {
|
|
1297
|
+
await sleep(50);
|
|
1298
|
+
process.stdout.write(HIDE);
|
|
1299
|
+
const line = ` ${chalk.dim(item.label.padEnd(12))} ${rgbText(item.value, item.color)}`;
|
|
1300
|
+
for (let i = 0; i <= line.length; i += 3) {
|
|
1301
|
+
process.stdout.write(CLEAR_LINE + line.slice(0, i));
|
|
1302
|
+
await sleep(4);
|
|
1303
|
+
}
|
|
1304
|
+
process.stdout.write(CLEAR_LINE + line + '\n');
|
|
1305
|
+
process.stdout.write(SHOW);
|
|
1019
1306
|
}
|
|
1020
|
-
console.log(` ${chalk.dim('Output:')} ${chalk.gray(targetDir)}`);
|
|
1021
|
-
console.log('');
|
|
1022
1307
|
|
|
1023
|
-
|
|
1308
|
+
console.log('');
|
|
1309
|
+
await printSessionDiff(lastSession, { stack, dbType, generationMode });
|
|
1024
1310
|
|
|
1025
1311
|
const proceed = await p.confirm({ message: 'Proceed with generation?', initialValue: true });
|
|
1026
1312
|
if (p.isCancel(proceed) || !proceed) { p.cancel('Aborted.'); process.exit(0); }
|
|
1027
1313
|
|
|
1028
|
-
// ── Build options & execute ────────────────────────────────────────────
|
|
1029
1314
|
const options = {
|
|
1030
|
-
generationMode,
|
|
1031
|
-
|
|
1032
|
-
stack,
|
|
1033
|
-
srcPath,
|
|
1034
|
-
dbType,
|
|
1035
|
-
addAuth,
|
|
1036
|
-
addSeeder,
|
|
1037
|
-
extraFeatures,
|
|
1315
|
+
generationMode, projectName, stack, srcPath,
|
|
1316
|
+
dbType, addAuth, addSeeder, extraFeatures,
|
|
1038
1317
|
projectDir : targetDir,
|
|
1039
1318
|
frontendSrcDir: resolvedSrc,
|
|
1040
1319
|
};
|
|
@@ -1043,71 +1322,61 @@ async function main() {
|
|
|
1043
1322
|
}
|
|
1044
1323
|
|
|
1045
1324
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
1046
|
-
// Generation Executor — with auto-retry
|
|
1325
|
+
// ⚙️ Generation Executor — Animated with auto-retry
|
|
1047
1326
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
1048
1327
|
|
|
1049
1328
|
async function executeGeneration(options, globalStart, plugins = [], _attempt = 1) {
|
|
1050
1329
|
const startTime = Date.now();
|
|
1051
|
-
_cleanupDir
|
|
1330
|
+
_cleanupDir = options.projectDir;
|
|
1052
1331
|
|
|
1053
1332
|
try {
|
|
1054
|
-
// ── PRO MODE ──────────────────────────────────────────────────────────
|
|
1055
1333
|
if (options.generationMode === 'pro') {
|
|
1056
1334
|
const apiKey = await getProApiKey();
|
|
1057
1335
|
|
|
1058
|
-
const
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
color : 'cyan',
|
|
1062
|
-
}).start();
|
|
1063
|
-
let astJsonData = [];
|
|
1336
|
+
const spinAST = new LiveSpinner();
|
|
1337
|
+
spinAST.start('Parsing frontend with Babel AST...', { type: 'dots', color: '#00F5FF' });
|
|
1338
|
+
let astData = [];
|
|
1064
1339
|
try {
|
|
1065
|
-
|
|
1066
|
-
|
|
1340
|
+
astData = await analyzeFrontend(options.frontendSrcDir);
|
|
1341
|
+
spinAST.succeed(`AST complete — ${rgbText(astData.length + ' endpoints', '#00FF9F')}`);
|
|
1067
1342
|
} catch (err) {
|
|
1068
|
-
|
|
1343
|
+
spinAST.warn(`AST warning: ${err.message}`);
|
|
1069
1344
|
}
|
|
1070
1345
|
|
|
1071
|
-
const
|
|
1072
|
-
options.aiBlocks
|
|
1346
|
+
const blocks = await callAIProcessor(astData, apiKey, options);
|
|
1347
|
+
options.aiBlocks = blocks;
|
|
1073
1348
|
|
|
1074
|
-
const
|
|
1075
|
-
|
|
1076
|
-
spinner: 'material',
|
|
1077
|
-
color : 'magenta',
|
|
1078
|
-
}).start();
|
|
1349
|
+
const spinWrite = new LiveSpinner();
|
|
1350
|
+
spinWrite.start('Writing hexagonal output...', { type: 'wave', color: '#BF40FF' });
|
|
1079
1351
|
try {
|
|
1080
1352
|
await dispatchGenerator(options);
|
|
1081
|
-
|
|
1353
|
+
spinWrite.succeed('Hexagonal output written ✓');
|
|
1082
1354
|
} catch (err) {
|
|
1083
|
-
|
|
1355
|
+
spinWrite.fail('Write failed.');
|
|
1084
1356
|
throw err;
|
|
1085
1357
|
}
|
|
1086
1358
|
|
|
1087
|
-
if (
|
|
1359
|
+
if (blocks.deployment) {
|
|
1088
1360
|
await fs.ensureDir(path.join(options.projectDir, '.github', 'workflows'));
|
|
1089
|
-
await fs.writeFile(
|
|
1090
|
-
|
|
1091
|
-
generatedBlocks.deployment.dockerCompose
|
|
1092
|
-
);
|
|
1093
|
-
await fs.writeFile(
|
|
1094
|
-
path.join(options.projectDir, '.github', 'workflows', 'deploy.yml'),
|
|
1095
|
-
generatedBlocks.deployment.githubWorkflow
|
|
1096
|
-
);
|
|
1361
|
+
await fs.writeFile(path.join(options.projectDir, 'docker-compose.yml'), blocks.deployment.dockerCompose);
|
|
1362
|
+
await fs.writeFile(path.join(options.projectDir, '.github', 'workflows', 'deploy.yml'), blocks.deployment.githubWorkflow);
|
|
1097
1363
|
}
|
|
1098
1364
|
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1365
|
+
await printAnimatedDashboard([
|
|
1366
|
+
{ label: 'Security Profile', score: blocks.aiSecurityConfig?.length > 20 ? 98 : 75, icon: '🛡️ ' },
|
|
1367
|
+
{ label: 'Hexagonal Compliance',score: blocks.aiDbRelations?.length > 20 ? 99 : 80, icon: '🏛️ ' },
|
|
1368
|
+
{ label: 'Test Coverage (Gen)', score: 85, icon: '🧪' },
|
|
1369
|
+
{ label: 'Dependency Health', score: 92, icon: '📦' },
|
|
1370
|
+
], 'System Health Dashboard');
|
|
1371
|
+
|
|
1372
|
+
await printAnimatedStats('pro', startTime, { endpointCount: Object.keys(blocks).length });
|
|
1103
1373
|
|
|
1104
1374
|
} else {
|
|
1105
|
-
// ── FREE MODE ────────────────────────────────────────────────────────
|
|
1106
1375
|
await runFreeModePipeline(options);
|
|
1107
|
-
|
|
1376
|
+
await printAnimatedStats('free', startTime, { ...(options._meta || {}), retries: _attempt - 1 });
|
|
1108
1377
|
}
|
|
1109
1378
|
|
|
1110
|
-
//
|
|
1379
|
+
// Post-gen plugins
|
|
1111
1380
|
for (const plugin of plugins) {
|
|
1112
1381
|
if (plugin.runAfterGenerate) {
|
|
1113
1382
|
try { await plugin.runAfterGenerate(options); } catch {}
|
|
@@ -1115,37 +1384,46 @@ async function executeGeneration(options, globalStart, plugins = [], _attempt =
|
|
|
1115
1384
|
}
|
|
1116
1385
|
|
|
1117
1386
|
await saveSession(options);
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
_cleanupDir = null;
|
|
1387
|
+
await printAnimatedNextSteps(options.projectName, options.stack, options.dbType);
|
|
1121
1388
|
|
|
1389
|
+
_cleanupDir = null;
|
|
1122
1390
|
const totalTime = ((performance.now() - globalStart) / 1000).toFixed(2);
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
)
|
|
1391
|
+
|
|
1392
|
+
// ── Animated outro ─────────────────────────────────────────────────
|
|
1393
|
+
console.log('');
|
|
1394
|
+
process.stdout.write(HIDE);
|
|
1395
|
+
const outro = ` ✓ Done in ${totalTime}s — cd ${options.projectName}`;
|
|
1396
|
+
for (let i = 0; i <= outro.length; i++) {
|
|
1397
|
+
process.stdout.write(CLEAR_LINE + gradientText(outro.slice(0, i), Math.floor(i / 2)));
|
|
1398
|
+
await sleep(18);
|
|
1399
|
+
}
|
|
1400
|
+
process.stdout.write('\n' + SHOW + '\n');
|
|
1129
1401
|
|
|
1130
1402
|
} catch (error) {
|
|
1403
|
+
process.stdout.write(SHOW);
|
|
1131
1404
|
console.log('');
|
|
1132
1405
|
|
|
1133
1406
|
const cleanStack = (error.stack ?? '')
|
|
1134
1407
|
.split('\n')
|
|
1135
1408
|
.filter(l => !l.includes('node_modules') && !l.includes('node:internal'))
|
|
1136
|
-
.slice(0,
|
|
1409
|
+
.slice(0, 5)
|
|
1137
1410
|
.join('\n');
|
|
1138
1411
|
|
|
1139
|
-
|
|
1140
|
-
`Generation failed (attempt ${_attempt}/${MAX_RETRIES}): ${error.message || error}`
|
|
1141
|
-
));
|
|
1412
|
+
console.log(rgbText(` ✗ Generation failed (attempt ${_attempt}/${MAX_RETRIES}): ${error.message}`, '#FF006E'));
|
|
1142
1413
|
if (cleanStack) console.log(chalk.gray(cleanStack));
|
|
1143
1414
|
|
|
1144
1415
|
if (_attempt < MAX_RETRIES) {
|
|
1145
1416
|
const delay = _attempt * 2000;
|
|
1146
1417
|
console.log('');
|
|
1147
|
-
|
|
1148
|
-
|
|
1418
|
+
|
|
1419
|
+
// Countdown animation
|
|
1420
|
+
process.stdout.write(HIDE);
|
|
1421
|
+
for (let i = delay / 1000; i > 0; i--) {
|
|
1422
|
+
process.stdout.write(CLEAR_LINE + rgbText(` ⏳ Retrying in ${i}s...`, '#FFBE0B'));
|
|
1423
|
+
await sleep(1000);
|
|
1424
|
+
}
|
|
1425
|
+
process.stdout.write(CLEAR_LINE);
|
|
1426
|
+
process.stdout.write(SHOW + '\n');
|
|
1149
1427
|
|
|
1150
1428
|
if (options.projectDir && await fs.pathExists(options.projectDir)) {
|
|
1151
1429
|
await fs.remove(options.projectDir).catch(() => {});
|
|
@@ -1155,13 +1433,10 @@ async function executeGeneration(options, globalStart, plugins = [], _attempt =
|
|
|
1155
1433
|
}
|
|
1156
1434
|
|
|
1157
1435
|
if (options.projectDir && await fs.pathExists(options.projectDir)) {
|
|
1158
|
-
const
|
|
1159
|
-
|
|
1160
|
-
spinner: 'line',
|
|
1161
|
-
color : 'yellow',
|
|
1162
|
-
}).start();
|
|
1436
|
+
const spin = new LiveSpinner();
|
|
1437
|
+
spin.start('Cleaning up partial output...', { type: 'arc', color: '#FFBE0B' });
|
|
1163
1438
|
await fs.remove(options.projectDir).catch(() => {});
|
|
1164
|
-
|
|
1439
|
+
spin.succeed('Cleanup complete.');
|
|
1165
1440
|
}
|
|
1166
1441
|
|
|
1167
1442
|
_cleanupDir = null;
|
|
@@ -1171,7 +1446,8 @@ async function executeGeneration(options, globalStart, plugins = [], _attempt =
|
|
|
1171
1446
|
|
|
1172
1447
|
// ── Launch ────────────────────────────────────────────────────────────────
|
|
1173
1448
|
main().catch(err => {
|
|
1174
|
-
|
|
1175
|
-
|
|
1449
|
+
process.stdout.write(SHOW);
|
|
1450
|
+
console.error(rgbText(`\n Fatal: ${err.message || err}`, '#FF006E'));
|
|
1451
|
+
if (err.stack) console.error(chalk.gray(err.stack.split('\n').slice(1, 4).join('\n')));
|
|
1176
1452
|
process.exit(1);
|
|
1177
1453
|
});
|