blueprompt 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/cli/index.js ADDED
@@ -0,0 +1,700 @@
1
+ const chalk = require("chalk");
2
+ const boxen = require("boxen");
3
+ const gradient = require("gradient-string");
4
+ const ora = require("ora");
5
+ const inquirer = require("inquirer");
6
+ const figures = require("figures");
7
+ const fs = require("fs");
8
+ const path = require("path");
9
+ const { exec } = require("child_process");
10
+ const { stdin: input, stdout: output } = require("process");
11
+
12
+ const DEFAULT_BASE_URL =
13
+ process.env.REVERSE_ENGINEER_BASE_URL || "http://localhost:3000";
14
+ const DEFAULT_STYLE = "blueprint";
15
+ const DEFAULT_LANGUAGE = "Thai";
16
+ const DEFAULT_PROVIDER = process.env.DEFAULT_PROVIDER || "openai";
17
+ const DEFAULT_STREAMING = process.env.STREAMING_ENABLED !== "false"; // Default to true unless explicitly disabled
18
+
19
+ function updateEnv(key, value) {
20
+ const envPath = path.join(__dirname, "..", ".env");
21
+ let content = fs.existsSync(envPath) ? fs.readFileSync(envPath, "utf8") : "";
22
+ const lines = content.split("\n");
23
+ let found = false;
24
+
25
+ const newLines = lines.map((line) => {
26
+ if (line.startsWith(`${key}=`)) {
27
+ found = true;
28
+ return `${key}=${value}`;
29
+ }
30
+ return line;
31
+ });
32
+
33
+ if (!found) {
34
+ newLines.push(`${key}=${value}`);
35
+ }
36
+
37
+ fs.writeFileSync(envPath, newLines.join("\n"), "utf8");
38
+ }
39
+
40
+ const APP_NAME = "REVERSE ENGINEER CLI v1.0";
41
+ const LOGO_TEXT = `
42
+ ██████╗ ███████╗██╗ ██╗███████╗██████╗ ███████╗███████╗
43
+ ██╔══██╗██╔════╝██║ ██║██╔════╝██╔══██╗██╔════╝██╔════╝
44
+ ██████╔╝█████╗ ██║ ██║█████╗ ██████╔╝███████╗█████╗
45
+ ██╔══██╗██╔══╝ ╚██╗ ██╔╝██╔══╝ ██╔══██╗╚════██║██╔══╝
46
+ ██║ ██║███████╗ ╚████╔╝ ███████╗██║ ██║███████║███████╗
47
+ ╚═╝ ╚═╝╚══════╝ ╚═══╝ ╚══════╝╚═╝ ╚═╝╚══════╝╚══════╝
48
+ ███████╗███╗ ██╗ ██████╗ ██╗███╗ ██╗███████╗███████╗██████╗
49
+ ██╔════╝████╗ ██║██╔════╝ ██║████╗ ██║██╔════╝██╔════╝██╔══██╗
50
+ █████╗ ██╔██╗ ██║██║ ███╗██║██╔██╗ ██║█████╗ █████╗ ██████╔╝
51
+ ██╔══╝ ██║╚██╗██║██║ ██║██║██║╚██╗██║██╔══╝ ██╔══╝ ██╔══██╗
52
+ ███████╗██║ ╚████║╚██████╔╝██║██║ ╚████║███████╗███████╗██║ ██║
53
+ ╚══════╝╚═╝ ╚═══╝ ╚═════╝ ╚═╝╚═╝ ╚═══╝╚══════╝╚══════╝╚═╝ ╚═╝
54
+ `;
55
+
56
+ function parseArgs(argv) {
57
+ const args = {
58
+ baseUrl: DEFAULT_BASE_URL,
59
+ inspectOnly: false,
60
+ json: false,
61
+ };
62
+
63
+ // Check if the first argument is a URL (not a flag)
64
+ if (argv[0] && !argv[0].startsWith("-")) {
65
+ args.url = argv[0];
66
+ }
67
+
68
+ for (let index = 0; index < argv.length; index += 1) {
69
+ const token = argv[index];
70
+ const next = argv[index + 1];
71
+
72
+ switch (token) {
73
+ case "--url":
74
+ args.url = next;
75
+ index += 1;
76
+ break;
77
+ case "--goal":
78
+ args.goal = next;
79
+ index += 1;
80
+ break;
81
+ case "--style":
82
+ args.outputStyle = next;
83
+ index += 1;
84
+ break;
85
+ case "--language":
86
+ args.language = next;
87
+ index += 1;
88
+ break;
89
+ case "--provider":
90
+ args.provider = next;
91
+ index += 1;
92
+ break;
93
+ case "--model":
94
+ args.model = next;
95
+ index += 1;
96
+ break;
97
+ case "--extra":
98
+ args.extraContext = next;
99
+ index += 1;
100
+ break;
101
+ case "--base-url":
102
+ args.baseUrl = next;
103
+ index += 1;
104
+ break;
105
+ case "--inspect-only":
106
+ args.inspectOnly = true;
107
+ break;
108
+ case "--json":
109
+ args.json = true;
110
+ break;
111
+ case "--help":
112
+ args.help = true;
113
+ break;
114
+ }
115
+ }
116
+
117
+ return args;
118
+ }
119
+
120
+ function printBanner() {
121
+ const g = gradient.default || gradient;
122
+ const claudeOrange = g("#D97757", "#FCAB64", "#D97757");
123
+ console.log(claudeOrange.multiline(LOGO_TEXT));
124
+ console.log(
125
+ chalk.hex("#D97757").bold(
126
+ ` ${figures.star} Welcome to ${APP_NAME} ${figures.star}`,
127
+ ),
128
+ );
129
+ console.log(
130
+ chalk.dim(" Designed for Deep Repo Analysis and Engineering Insights\n"),
131
+ );
132
+ }
133
+
134
+ function printHelp() {
135
+ printBanner();
136
+ console.log(
137
+ boxen(
138
+ chalk.white(`Usage:
139
+ npm run tui
140
+ node cli/index.js --url <github-url> [options]
141
+
142
+ Options:
143
+ --goal <text> Target analysis goal
144
+ --provider <name> AI Provider (openai, anthropic, etc.)
145
+ --model <id> AI Model ID (optional)
146
+ --style <style> summary, deep, step-by-step, refactoring, security, perfection, blueprint
147
+ --language <lang> Thai, English, Bilingual
148
+ --inspect-only Skip AI analysis
149
+ --json Output raw JSON
150
+ --help Show this help message`),
151
+ { padding: 1, borderColor: "blue", borderStyle: "round" },
152
+ ),
153
+ );
154
+ }
155
+
156
+ async function fetchJson(url, options) {
157
+ const response = await fetch(url, options);
158
+ const data = await response.json().catch(() => ({}));
159
+
160
+ if (!response.ok) {
161
+ throw new Error(
162
+ data.error || `Request failed with status ${response.status}`,
163
+ );
164
+ }
165
+
166
+ return data;
167
+ }
168
+
169
+ function divider(step, title, color = "blue", total = 4) {
170
+ const stepText = chalk.bold(` [STEP ${step}/${total}] `);
171
+ const titleText = chalk[color].bold(` ${figures.pointerSmall} ${title} `);
172
+ const line = chalk.dim("─".repeat(Math.max(0, (process.stdout.columns || 80) - title.length - stepText.length - 15)));
173
+ console.log(`\n${chalk.bgCyan.black(stepText)}${titleText}${line}\n`);
174
+ }
175
+
176
+ function summaryBox(title, content, color = "cyan") {
177
+ console.log("\n" + boxen(chalk.white(content), {
178
+ title: chalk[color].bold(title),
179
+ titleAlignment: "left",
180
+ padding: { left: 1, right: 1, top: 0, bottom: 0 },
181
+ borderColor: color,
182
+ borderStyle: "round",
183
+ margin: { left: 2 }
184
+ }));
185
+ }
186
+
187
+ function renderMetadata(metadata) {
188
+ const content = [
189
+ `${chalk.cyan("Repo")} : ${chalk.white(`${metadata.owner}/${metadata.repo}`)}`,
190
+ `${chalk.cyan("Branch")} : ${chalk.white(metadata.branch)}`,
191
+ `${chalk.cyan("Type")} : ${chalk.white(metadata.type)}`,
192
+ `${chalk.cyan("Path")} : ${chalk.white(metadata.path)}`,
193
+ `${chalk.cyan("Private")} : ${metadata.private ? chalk.red("Yes") : chalk.green("No")}`,
194
+ `${chalk.cyan("URL")} : ${chalk.blue.underline(metadata.url)}`,
195
+ ].join("\n");
196
+
197
+ summaryBox("METADATA RECOVERY", content, "cyan");
198
+ }
199
+
200
+ function renderTree(tree) {
201
+ divider("REPO TREE PREVIEW", "yellow");
202
+ if (!tree.length) {
203
+ console.log(chalk.dim(" No tree entries available"));
204
+ return;
205
+ }
206
+
207
+ tree.slice(0, 20).forEach((item) => {
208
+ const icon =
209
+ item.type === "tree" || item.type === "dir"
210
+ ? chalk.yellow(figures.folder || "󰉋")
211
+ : chalk.blue(figures.file || "󰈚");
212
+ const suffix = item.size ? chalk.dim(` (${item.size} bytes)`) : "";
213
+ console.log(` ${icon} ${item.path}${suffix}`);
214
+ });
215
+
216
+ if (tree.length > 20) {
217
+ console.log(
218
+ chalk.dim(
219
+ ` ${figures.ellipsis || "..."} and ${tree.length - 20} more entries`,
220
+ ),
221
+ );
222
+ }
223
+ }
224
+
225
+ function renderFile(file) {
226
+ if (!file) return;
227
+
228
+ divider("FILE PREVIEW", "magenta");
229
+ console.log(
230
+ `${chalk.magenta("Path:")} ${chalk.white(file.path)} ${chalk.dim(`(${file.size} bytes)`)}`,
231
+ );
232
+
233
+ if (file.content) {
234
+ const lines = file.content.split("\n");
235
+ const preview = lines.slice(0, 30).join("\n");
236
+ const suffix =
237
+ lines.length > 30 ? chalk.dim(`\n...(total ${lines.length} lines)`) : "";
238
+
239
+ console.log(
240
+ boxen(chalk.white(preview + suffix), {
241
+ padding: 0.5,
242
+ borderColor: "gray",
243
+ borderStyle: "single",
244
+ backgroundColor: "#1e1e1e",
245
+ }),
246
+ );
247
+ }
248
+ }
249
+
250
+ function renderAnalysis(analysis) {
251
+ const content = analysis.text || "No analysis available";
252
+ console.log(
253
+ boxen(chalk.greenBright(content), {
254
+ padding: 1,
255
+ borderColor: "green",
256
+ borderStyle: "double",
257
+ title: "AI ENGINEERING INSIGHTS",
258
+ titleAlignment: "center",
259
+ }),
260
+ );
261
+ return content;
262
+ }
263
+
264
+ async function handleOutputAction(content, metadata, health, githubContext) {
265
+ const outputDir = path.join(__dirname, "..", "output");
266
+
267
+ const { action } = await inquirer.prompt([
268
+ {
269
+ type: "list",
270
+ name: "action",
271
+ message: "What would you like to do with the result?",
272
+ choices: [
273
+ { name: `${figures.pointer || ">>"} Copy to Clipboard`, value: "copy" },
274
+ { name: `${figures.folder || "[F]"} Export as Markdown (.md)`, value: "save-md" },
275
+ { name: `${figures.folder || "[F]"} Export as Plain Text (.txt)`, value: "save-txt" },
276
+ { name: `${figures.folder || "[F]"} Export as JSON (.json)`, value: "save-json" },
277
+ {
278
+ name: `${figures.settings || "[S]"} Change Provider/Settings`,
279
+ value: "config",
280
+ },
281
+ { name: `${figures.tick || "[OK]"} Done (Quit)`, value: "quit" },
282
+ ],
283
+ },
284
+ ]);
285
+
286
+ if (action === "copy") {
287
+ try {
288
+ const platform = process.platform;
289
+ let clipCmd;
290
+ if (platform === "win32") clipCmd = "clip";
291
+ else if (platform === "darwin") clipCmd = "pbcopy";
292
+ else clipCmd = "xclip -selection clipboard";
293
+
294
+ const proc = exec(clipCmd);
295
+ proc.stdin.write(content);
296
+ proc.stdin.end();
297
+ console.log(chalk.green(`\n${figures.tick} Content copied to clipboard!`));
298
+ } catch {
299
+ console.log(chalk.red(`\n${figures.cross} Clipboard not available on this platform. Use export instead.`));
300
+ }
301
+ return handleOutputAction(content, metadata, health, githubContext);
302
+ }
303
+
304
+ if (action.startsWith("save-")) {
305
+ if (!fs.existsSync(outputDir)) {
306
+ fs.mkdirSync(outputDir, { recursive: true });
307
+ }
308
+
309
+ const baseName = `${metadata.owner}-${metadata.repo}`;
310
+ const format = action.replace("save-", "");
311
+ let filename, fileContent;
312
+
313
+ switch (format) {
314
+ case "md": {
315
+ filename = `${baseName}-analysis.md`;
316
+ const header = [
317
+ "---",
318
+ `# Reverse Engineer — Analysis Export`,
319
+ `# Repo: ${metadata.owner}/${metadata.repo}`,
320
+ `# Branch: ${metadata.branch}`,
321
+ `# Type: ${metadata.type}`,
322
+ `# Path: ${metadata.path}`,
323
+ `# Date: ${new Date().toISOString()}`,
324
+ "---",
325
+ "",
326
+ ].join("\n");
327
+ fileContent = header + content;
328
+ break;
329
+ }
330
+ case "txt": {
331
+ filename = `${baseName}-analysis.txt`;
332
+ fileContent = content;
333
+ break;
334
+ }
335
+ case "json": {
336
+ filename = `${baseName}-analysis.json`;
337
+ fileContent = JSON.stringify({
338
+ meta: {
339
+ repo: `${metadata.owner}/${metadata.repo}`,
340
+ branch: metadata.branch,
341
+ type: metadata.type,
342
+ path: metadata.path,
343
+ url: metadata.url,
344
+ exportedAt: new Date().toISOString(),
345
+ },
346
+ analysis: content,
347
+ }, null, 2);
348
+ break;
349
+ }
350
+ }
351
+
352
+ const fullPath = path.join(outputDir, filename);
353
+ fs.writeFileSync(fullPath, fileContent, "utf8");
354
+ console.log(
355
+ chalk.green(`\n${figures.tick} Saved to: ${chalk.bold(`output/${filename}`)}`),
356
+ );
357
+ return handleOutputAction(content, metadata, health, githubContext);
358
+ }
359
+
360
+ if (action === "config") {
361
+ await configureProvider(health);
362
+ console.log(chalk.yellow("\nSettings updated. Please restart analysis."));
363
+ return;
364
+ }
365
+
366
+ return;
367
+ }
368
+
369
+ async function configureProvider(health) {
370
+ const providers = Object.entries(health.providers || {}).map(([id, p]) => ({
371
+ name: `${p.label} ${p.configured ? chalk.green(`(${figures.tick} Ready: ${p.model})`) : chalk.dim(`(${figures.cross} Not Configured)`)}`,
372
+ value: id,
373
+ configured: p.configured,
374
+ }));
375
+
376
+ const { providerId } = await inquirer.prompt([
377
+ {
378
+ type: "list",
379
+ name: "providerId",
380
+ message: "Select AI Provider to configure/use:",
381
+ choices: providers,
382
+ },
383
+ ]);
384
+
385
+ const p = health.providers[providerId];
386
+ if (!p.configured) {
387
+ const keyName = `${providerId.toUpperCase()}_API_KEY`;
388
+ const { apiKey } = await inquirer.prompt([
389
+ {
390
+ type: "password",
391
+ name: "apiKey",
392
+ message: `Enter API Key for ${p.label} (${keyName}):`,
393
+ mask: "*",
394
+ },
395
+ ]);
396
+
397
+ if (apiKey) {
398
+ updateEnv(keyName, apiKey);
399
+ console.log(chalk.green(`\n${figures.tick} API Key saved to .env!`));
400
+ p.configured = true;
401
+ }
402
+ }
403
+
404
+ const { model } = await inquirer.prompt([
405
+ {
406
+ type: "input",
407
+ name: "model",
408
+ message: `Preferred Model for ${p.label} (default: ${p.model}):`,
409
+ default: p.model,
410
+ },
411
+ ]);
412
+
413
+ updateEnv("DEFAULT_PROVIDER", providerId);
414
+ const modelKey = `${providerId.toUpperCase()}_MODEL`;
415
+ updateEnv(modelKey, model);
416
+
417
+ console.log(
418
+ chalk.cyan(
419
+ `\n${figures.star} Default provider set to: ${chalk.bold(providerId)} / ${chalk.bold(model)}`,
420
+ ),
421
+ );
422
+ return { provider: providerId, model };
423
+ }
424
+
425
+ async function promptForMissing(args, health = {}) {
426
+ if (args.url) return args;
427
+
428
+ printBanner();
429
+
430
+ const { choice } = await inquirer.prompt([
431
+ {
432
+ type: "list",
433
+ name: "choice",
434
+ message: "What would you like to do?",
435
+ choices: [
436
+ {
437
+ name: `${figures.play || "▶"} Analyze a Repository`,
438
+ value: "analyze",
439
+ },
440
+ {
441
+ name: `${figures.settings || "⚙"} Configure API Keys / Models`,
442
+ value: "config",
443
+ },
444
+ { name: `${figures.cross || "✖"} Exit`, value: "exit" },
445
+ ],
446
+ },
447
+ ]);
448
+
449
+ if (choice === "exit") process.exit(0);
450
+
451
+ if (choice === "config") {
452
+ await configureProvider(health);
453
+ return promptForMissing(args, health);
454
+ }
455
+
456
+ const primaryAnswer = await inquirer.prompt([
457
+ {
458
+ type: "input",
459
+ name: "url",
460
+ message: "Enter GitHub URL:",
461
+ validate: (input) => input.trim() !== "" || "URL is required",
462
+ },
463
+ ]);
464
+
465
+ args.url = primaryAnswer.url;
466
+
467
+ const { mode } = await inquirer.prompt([
468
+ {
469
+ type: "list",
470
+ name: "mode",
471
+ message: "Proceed with defaults or customize?",
472
+ choices: [
473
+ { name: `${figures.play} Just go! (Fast Track)`, value: "fast" },
474
+ { name: `${figures.settings} Customize options`, value: "custom" },
475
+ ],
476
+ default: "fast",
477
+ },
478
+ ]);
479
+
480
+ if (mode === "fast") {
481
+ args.outputStyle = args.outputStyle || DEFAULT_STYLE;
482
+ args.language = args.language || DEFAULT_LANGUAGE;
483
+ return args;
484
+ }
485
+
486
+ const questions = [];
487
+
488
+ if (!args.goal && !args.inspectOnly) {
489
+ questions.push({
490
+ type: "input",
491
+ name: "goal",
492
+ message: "What's the goal of this analysis? (optional):",
493
+ });
494
+ }
495
+
496
+ if (!args.outputStyle) {
497
+ questions.push({
498
+ type: "list",
499
+ name: "outputStyle",
500
+ message: "Choose analysis style:",
501
+ choices: ["summary", "deep", "step-by-step", "refactoring", "security", "perfection", "blueprint"],
502
+ default: DEFAULT_STYLE,
503
+ });
504
+ }
505
+
506
+ if (!args.language) {
507
+ questions.push({
508
+ type: "list",
509
+ name: "language",
510
+ message: "Choose output language:",
511
+ choices: ["Thai", "English", "Bilingual"],
512
+ default: DEFAULT_LANGUAGE,
513
+ });
514
+ }
515
+
516
+ if (!args.provider) {
517
+ questions.push({
518
+ type: "input",
519
+ name: "provider",
520
+ message: "AI Provider (default: openai):",
521
+ default: DEFAULT_PROVIDER,
522
+ });
523
+ }
524
+
525
+ const answers = await inquirer.prompt(questions);
526
+ return { ...args, ...answers };
527
+ }
528
+
529
+ async function main() {
530
+ const args = parseArgs(process.argv.slice(2));
531
+
532
+ if (args.help) {
533
+ printHelp();
534
+ return;
535
+ }
536
+
537
+ let health = {};
538
+ try {
539
+ health = await fetchJson(`${args.baseUrl}/api/health`);
540
+ } catch (e) {
541
+ console.log(chalk.yellow(`${figures.warning} Could not reach API server at ${args.baseUrl}. Some features may not work.`));
542
+ }
543
+
544
+ const finalArgs = await promptForMissing(args, health);
545
+
546
+ if (!finalArgs.url) {
547
+ console.log(chalk.red(`${figures.cross} Error: GitHub URL is required.`));
548
+ process.exit(1);
549
+ }
550
+
551
+ try {
552
+ divider(1, "Handshake & Engine Check", "blue");
553
+ const serverHealth = await fetchJson(`${finalArgs.baseUrl}/api/health`);
554
+
555
+ const serverInfo = [
556
+ `${chalk.cyan("API Gateway")} : ${chalk.white(finalArgs.baseUrl)}`,
557
+ `${chalk.cyan("AI Gateway")} : ${serverHealth.ok ? chalk.green("CONNECTED") : chalk.red("DISCONNECTED")}`,
558
+ `${chalk.cyan("Active Prov")} : ${chalk.yellow(serverHealth.defaultProvider)}`,
559
+ ].join("\n");
560
+
561
+ console.log(
562
+ boxen(serverInfo, {
563
+ padding: { left: 1, right: 1, top: 0, bottom: 0 },
564
+ borderColor: "blue",
565
+ borderStyle: "round",
566
+ title: "SYSTEM STATUS",
567
+ }),
568
+ );
569
+
570
+ divider(2, "Deep Repository Extraction", "cyan");
571
+ const inspectSpinner = ora(
572
+ `Mining data from: ${chalk.blue(finalArgs.url)}`,
573
+ ).start();
574
+ const githubContext = await fetchJson(
575
+ `${finalArgs.baseUrl}/api/github/inspect?url=${encodeURIComponent(finalArgs.url)}`,
576
+ );
577
+ inspectSpinner.succeed(chalk.green("Data extraction complete."));
578
+
579
+ if (finalArgs.json && finalArgs.inspectOnly) {
580
+ console.log(JSON.stringify(githubContext, null, 2));
581
+ return;
582
+ }
583
+
584
+ renderMetadata(githubContext.metadata);
585
+ renderTree(githubContext.tree || []);
586
+ renderFile(githubContext.file);
587
+
588
+ if (finalArgs.inspectOnly) {
589
+ console.log(
590
+ chalk.yellow(
591
+ `\n${figures.info} Inspect-only mode enabled. Execution finished at Phase 2.`,
592
+ ),
593
+ );
594
+ return;
595
+ }
596
+
597
+ divider(3, "AI Synthesis & Pattern Analysis", "magenta");
598
+ const analysisSpinner = ora({
599
+ text: `Syncing with code-neural networks via ${chalk.bold(serverHealth.defaultProvider)}...`,
600
+ color: "magenta",
601
+ }).start();
602
+
603
+ let selectedProvider = finalArgs.provider || serverHealth.defaultProvider;
604
+
605
+ if (
606
+ (!selectedProvider || selectedProvider === "openai") &&
607
+ !serverHealth.hasOpenAIKey
608
+ ) {
609
+ const firstConfigured = Object.entries(serverHealth.providers || {}).find(
610
+ ([_, p]) => p.configured,
611
+ );
612
+ if (firstConfigured) {
613
+ selectedProvider = firstConfigured[0];
614
+ }
615
+ }
616
+
617
+ const shouldStream = DEFAULT_STREAMING && !finalArgs.json;
618
+
619
+ const response = await fetch(`${finalArgs.baseUrl}/api/analyze/stream`, {
620
+ method: "POST",
621
+ headers: { "Content-Type": "application/json" },
622
+ body: JSON.stringify({
623
+ githubContext,
624
+ goal: finalArgs.goal || "",
625
+ outputStyle: finalArgs.outputStyle || DEFAULT_STYLE,
626
+ language: finalArgs.language || DEFAULT_LANGUAGE,
627
+ provider: selectedProvider,
628
+ model: finalArgs.model || "",
629
+ extraContext: finalArgs.extraContext || "",
630
+ stream: shouldStream
631
+ }),
632
+ });
633
+
634
+ if (!response.ok) throw new Error(`Analysis connection failed: ${response.status}`);
635
+
636
+ let finalContent = "";
637
+
638
+ if (shouldStream) {
639
+ analysisSpinner.succeed(chalk.green("Intelligence synthesis handshake successful. Receiving stream..."));
640
+ process.stdout.write("\n");
641
+
642
+ const reader = response.body.getReader();
643
+ const decoder = new TextDecoder();
644
+ let buffer = "";
645
+ while (true) {
646
+ const { done, value } = await reader.read();
647
+ if (done) break;
648
+
649
+ buffer += decoder.decode(value, { stream: true });
650
+ const lines = buffer.split("\n");
651
+ buffer = lines.pop() || "";
652
+
653
+ for (const line of lines) {
654
+ if (!line.trim().startsWith("data: ")) continue;
655
+ const dataStr = line.replace("data: ", "").trim();
656
+ if (dataStr === "[DONE]") continue;
657
+
658
+ try {
659
+ const data = JSON.parse(dataStr);
660
+ if (data.chunk) {
661
+ const part = data.chunk;
662
+ finalContent += part;
663
+ process.stdout.write(part);
664
+ }
665
+ } catch (e) {}
666
+ }
667
+ }
668
+ } else {
669
+ analysisSpinner.text = "Engaging long-form architectural synthesis... This may take up to 2 minutes.";
670
+ const data = await response.json();
671
+ if (data.error) throw new Error(data.error);
672
+ finalContent = data.text || "";
673
+ analysisSpinner.succeed(chalk.green("Deep synthesis complete."));
674
+ console.log(chalk.dim("\n --- ANALYSIS PREVIEW START --- \n"));
675
+ console.log(finalContent);
676
+ console.log(chalk.dim("\n --- ANALYSIS PREVIEW END --- \n"));
677
+ }
678
+ process.stdout.write("\n\n"); // End of stream spacing
679
+
680
+ if (finalArgs.json) {
681
+ console.log(JSON.stringify({ githubContext, analysis: { text: finalContent } }, null, 2));
682
+ return;
683
+ }
684
+
685
+ divider(4, "Insight Delivery & Export", "green");
686
+ await handleOutputAction(finalContent, githubContext.metadata, serverHealth, githubContext);
687
+
688
+ summaryBox("MISSION STATUS", `${figures.tick} REVERSE ENGINEERING SUCCESSFUL\n${figures.star} Insights ready for engineering team.`, "green");
689
+ } catch (error) {
690
+ console.error(
691
+ chalk.red(`\n${figures.warning} Critical Error: ${error.message}`),
692
+ );
693
+ process.exit(1);
694
+ }
695
+ }
696
+
697
+ main().catch((error) => {
698
+ console.error(chalk.red(`${figures.cross} Fatal Error: ${error.message}`));
699
+ process.exit(1);
700
+ });