@jellylegsai/aether-cli 1.9.2 → 2.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.
@@ -0,0 +1,706 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * aether-cli network-diagnostics
4
+ *
5
+ * Comprehensive network diagnostics with automatic RPC failover,
6
+ * latency testing, and intelligent error recovery.
7
+ *
8
+ * Features:
9
+ * - Multi-RPC endpoint health checking with automatic failover
10
+ * - Latency benchmarking across endpoints
11
+ * - Connection quality scoring
12
+ * - Automatic RPC selection with --auto flag
13
+ * - Detailed error classification and recovery suggestions
14
+ *
15
+ * Usage:
16
+ * aether network-diagnostics # Test default RPC
17
+ * aether network-diagnostics --auto # Auto-select best RPC
18
+ * aether network-diagnostics --rpc <url> # Test specific RPC
19
+ * aether network-diagnostics --benchmark # Latency benchmark mode
20
+ * aether network-diagnostics --all # Test all known endpoints
21
+ *
22
+ * SDK wired to:
23
+ * - client.getHealth() → GET /v1/health
24
+ * - client.getSlot() → GET /v1/slot
25
+ * - client.getVersion() → GET /v1/version
26
+ * - client.getEpochInfo() → GET /v1/epoch
27
+ * - client.ping() → Latency test
28
+ */
29
+
30
+ const path = require('path');
31
+ const readline = require('readline');
32
+ const fs = require('fs');
33
+ const os = require('os');
34
+
35
+ // Import SDK
36
+ const sdkPath = path.join(__dirname, '..', 'sdk', 'index.js');
37
+ const aether = require(sdkPath);
38
+
39
+ // ANSI colours
40
+ const C = {
41
+ reset: '\x1b[0m',
42
+ bright: '\x1b[1m',
43
+ dim: '\x1b[2m',
44
+ red: '\x1b[31m',
45
+ green: '\x1b[32m',
46
+ yellow: '\x1b[33m',
47
+ cyan: '\x1b[36m',
48
+ magenta: '\x1b[35m',
49
+ blue: '\x1b[34m',
50
+ };
51
+
52
+ const CLI_VERSION = '1.0.0';
53
+
54
+ // Known RPC endpoints for failover
55
+ const KNOWN_RPC_ENDPOINTS = [
56
+ { url: 'http://127.0.0.1:8899', name: 'Local Node', priority: 1 },
57
+ { url: 'http://localhost:8899', name: 'Local Node (alt)', priority: 2 },
58
+ { url: process.env.AETHER_RPC, name: 'Env AETHER_RPC', priority: 0, conditional: true },
59
+ ];
60
+
61
+ // Filter out null/undefined endpoints
62
+ function getEffectiveEndpoints() {
63
+ const endpoints = [];
64
+ const seen = new Set();
65
+
66
+ for (const ep of KNOWN_RPC_ENDPOINTS) {
67
+ if (ep.conditional && !ep.url) continue;
68
+ if (!ep.url) continue;
69
+ if (seen.has(ep.url)) continue;
70
+ seen.add(ep.url);
71
+ endpoints.push(ep);
72
+ }
73
+
74
+ return endpoints.sort((a, b) => a.priority - b.priority);
75
+ }
76
+
77
+ // ============================================================================
78
+ // Config & Paths
79
+ // ============================================================================
80
+
81
+ function getAetherDir() {
82
+ return path.join(os.homedir(), '.aether');
83
+ }
84
+
85
+ function getConfigPath() {
86
+ return path.join(getAetherDir(), 'config.json');
87
+ }
88
+
89
+ function loadConfig() {
90
+ if (!fs.existsSync(getConfigPath())) {
91
+ return { preferredRpc: null, rpcHistory: [] };
92
+ }
93
+ try {
94
+ return JSON.parse(fs.readFileSync(getConfigPath(), 'utf8'));
95
+ } catch {
96
+ return { preferredRpc: null, rpcHistory: [] };
97
+ }
98
+ }
99
+
100
+ function saveConfig(cfg) {
101
+ if (!fs.existsSync(getAetherDir())) {
102
+ fs.mkdirSync(getAetherDir(), { recursive: true });
103
+ }
104
+ fs.writeFileSync(getConfigPath(), JSON.stringify(cfg, null, 2));
105
+ }
106
+
107
+ function updateRpcHistory(rpcUrl, latency, success) {
108
+ const cfg = loadConfig();
109
+ cfg.rpcHistory = cfg.rpcHistory || [];
110
+
111
+ // Add entry
112
+ cfg.rpcHistory.unshift({
113
+ url: rpcUrl,
114
+ latency,
115
+ success,
116
+ timestamp: new Date().toISOString(),
117
+ });
118
+
119
+ // Keep last 50 entries
120
+ cfg.rpcHistory = cfg.rpcHistory.slice(0, 50);
121
+
122
+ // Update preferred RPC if this one is good
123
+ if (success && latency < 100) {
124
+ cfg.preferredRpc = rpcUrl;
125
+ }
126
+
127
+ saveConfig(cfg);
128
+ }
129
+
130
+ // ============================================================================
131
+ // Argument Parsing
132
+ // ============================================================================
133
+
134
+ function parseArgs() {
135
+ const args = process.argv.slice(2);
136
+ const opts = {
137
+ rpc: process.env.AETHER_RPC || 'http://127.0.0.1:8899',
138
+ auto: false,
139
+ benchmark: false,
140
+ all: false,
141
+ json: false,
142
+ timeout: 10000,
143
+ help: false,
144
+ };
145
+
146
+ for (let i = 0; i < args.length; i++) {
147
+ switch (args[i]) {
148
+ case '--rpc':
149
+ case '-r':
150
+ opts.rpc = args[++i];
151
+ break;
152
+ case '--auto':
153
+ case '-a':
154
+ opts.auto = true;
155
+ break;
156
+ case '--benchmark':
157
+ case '-b':
158
+ opts.benchmark = true;
159
+ break;
160
+ case '--all':
161
+ opts.all = true;
162
+ break;
163
+ case '--json':
164
+ case '-j':
165
+ opts.json = true;
166
+ break;
167
+ case '--timeout':
168
+ case '-t':
169
+ opts.timeout = parseInt(args[++i], 10) || 10000;
170
+ break;
171
+ case '--help':
172
+ case '-h':
173
+ opts.help = true;
174
+ break;
175
+ }
176
+ }
177
+
178
+ return opts;
179
+ }
180
+
181
+ function showHelp() {
182
+ console.log(`
183
+ ${C.bright}${C.cyan}aether-cli network-diagnostics${C.reset} - Network health & RPC diagnostics
184
+
185
+ ${C.bright}USAGE${C.reset}
186
+ aether network-diagnostics [options]
187
+
188
+ ${C.bright}OPTIONS${C.reset}
189
+ --rpc <url> Test specific RPC endpoint
190
+ --auto, -a Auto-select best performing RPC
191
+ --benchmark, -b Run latency benchmark
192
+ --all Test all known RPC endpoints
193
+ --json, -j Output JSON
194
+ --timeout <ms> Request timeout (default: 10000)
195
+ --help, -h Show this help
196
+
197
+ ${C.bright}SDK METHODS USED${C.reset}
198
+ client.getHealth() → GET /v1/health
199
+ client.getSlot() → GET /v1/slot
200
+ client.getVersion() → GET /v1/version
201
+ client.getEpochInfo() → GET /v1/epoch
202
+ client.ping() → Latency measurement
203
+
204
+ ${C.bright}EXAMPLES${C.reset}
205
+ aether network-diagnostics # Test default RPC
206
+ aether network-diagnostics --auto # Find best RPC
207
+ aether network-diagnostics --benchmark # Latency test
208
+ aether network-diagnostics --all --json # Test all, JSON output
209
+ `);
210
+ }
211
+
212
+ // ============================================================================
213
+ // Diagnostics Logic
214
+ // ============================================================================
215
+
216
+ /**
217
+ * Test a single RPC endpoint comprehensively
218
+ */
219
+ async function testEndpoint(url, timeout = 10000) {
220
+ const results = {
221
+ url,
222
+ timestamp: new Date().toISOString(),
223
+ tests: {},
224
+ overall: {
225
+ success: false,
226
+ latency: null,
227
+ score: 0,
228
+ },
229
+ };
230
+
231
+ const client = new aether.AetherClient({ rpcUrl: url, timeoutMs: timeout });
232
+ const startTime = Date.now();
233
+
234
+ try {
235
+ // Test 1: Basic connectivity (ping)
236
+ const pingStart = Date.now();
237
+ const pingResult = await aether.ping(url);
238
+ results.tests.ping = {
239
+ success: pingResult.ok,
240
+ latency: pingResult.latency,
241
+ error: pingResult.ok ? null : pingResult.error,
242
+ };
243
+
244
+ if (!pingResult.ok) {
245
+ throw new Error(`Ping failed: ${pingResult.error}`);
246
+ }
247
+
248
+ // Test 2: Health check
249
+ const healthStart = Date.now();
250
+ try {
251
+ const health = await client.getHealth();
252
+ results.tests.health = {
253
+ success: true,
254
+ latency: Date.now() - healthStart,
255
+ status: health,
256
+ };
257
+ } catch (err) {
258
+ results.tests.health = {
259
+ success: false,
260
+ latency: Date.now() - healthStart,
261
+ error: err.message,
262
+ };
263
+ }
264
+
265
+ // Test 3: Slot query
266
+ const slotStart = Date.now();
267
+ try {
268
+ const slot = await client.getSlot();
269
+ results.tests.slot = {
270
+ success: true,
271
+ latency: Date.now() - slotStart,
272
+ slot,
273
+ };
274
+ } catch (err) {
275
+ results.tests.slot = {
276
+ success: false,
277
+ latency: Date.now() - slotStart,
278
+ error: err.message,
279
+ };
280
+ }
281
+
282
+ // Test 4: Version info
283
+ const versionStart = Date.now();
284
+ try {
285
+ const version = await client.getVersion();
286
+ results.tests.version = {
287
+ success: true,
288
+ latency: Date.now() - versionStart,
289
+ version,
290
+ };
291
+ } catch (err) {
292
+ results.tests.version = {
293
+ success: false,
294
+ latency: Date.now() - versionStart,
295
+ error: err.message,
296
+ };
297
+ }
298
+
299
+ // Test 5: Epoch info
300
+ const epochStart = Date.now();
301
+ try {
302
+ const epoch = await client.getEpochInfo();
303
+ results.tests.epoch = {
304
+ success: true,
305
+ latency: Date.now() - epochStart,
306
+ epoch,
307
+ };
308
+ } catch (err) {
309
+ results.tests.epoch = {
310
+ success: false,
311
+ latency: Date.now() - epochStart,
312
+ error: err.message,
313
+ };
314
+ }
315
+
316
+ // Calculate overall stats
317
+ const totalTime = Date.now() - startTime;
318
+ const successfulTests = Object.values(results.tests).filter(t => t.success).length;
319
+ const totalTests = Object.keys(results.tests).length;
320
+
321
+ results.overall = {
322
+ success: successfulTests === totalTests,
323
+ latency: totalTime,
324
+ score: Math.round((successfulTests / totalTests) * 100),
325
+ successfulTests,
326
+ totalTests,
327
+ };
328
+
329
+ // Update history
330
+ updateRpcHistory(url, totalTime, results.overall.success);
331
+
332
+ return results;
333
+
334
+ } catch (err) {
335
+ const totalTime = Date.now() - startTime;
336
+ results.overall = {
337
+ success: false,
338
+ latency: totalTime,
339
+ score: 0,
340
+ error: err.message,
341
+ };
342
+ updateRpcHistory(url, totalTime, false);
343
+ return results;
344
+ } finally {
345
+ client.destroy();
346
+ }
347
+ }
348
+
349
+ /**
350
+ * Run benchmark on an endpoint
351
+ */
352
+ async function benchmarkEndpoint(url, iterations = 5) {
353
+ const latencies = [];
354
+ const errors = [];
355
+
356
+ for (let i = 0; i < iterations; i++) {
357
+ const start = Date.now();
358
+ try {
359
+ const result = await aether.ping(url);
360
+ if (result.ok) {
361
+ latencies.push(result.latency);
362
+ } else {
363
+ errors.push(result.error);
364
+ }
365
+ } catch (err) {
366
+ errors.push(err.message);
367
+ }
368
+ // Small delay between pings
369
+ if (i < iterations - 1) {
370
+ await new Promise(r => setTimeout(r, 100));
371
+ }
372
+ }
373
+
374
+ if (latencies.length === 0) {
375
+ return {
376
+ url,
377
+ success: false,
378
+ errors,
379
+ };
380
+ }
381
+
382
+ const sorted = [...latencies].sort((a, b) => a - b);
383
+ const sum = latencies.reduce((a, b) => a + b, 0);
384
+
385
+ return {
386
+ url,
387
+ success: true,
388
+ iterations,
389
+ min: sorted[0],
390
+ max: sorted[sorted.length - 1],
391
+ avg: Math.round(sum / latencies.length),
392
+ median: sorted[Math.floor(sorted.length / 2)],
393
+ p95: sorted[Math.floor(sorted.length * 0.95)] || sorted[sorted.length - 1],
394
+ latencies,
395
+ errors: errors.length > 0 ? errors : undefined,
396
+ };
397
+ }
398
+
399
+ /**
400
+ * Find best RPC endpoint automatically
401
+ */
402
+ async function findBestEndpoint(endpoints, timeout = 10000) {
403
+ const results = [];
404
+
405
+ // Test all endpoints in parallel
406
+ const tests = endpoints.map(ep =>
407
+ testEndpoint(ep.url, timeout).then(result => ({
408
+ ...result,
409
+ name: ep.name,
410
+ }))
411
+ );
412
+
413
+ const settled = await Promise.allSettled(tests);
414
+
415
+ for (const result of settled) {
416
+ if (result.status === 'fulfilled') {
417
+ results.push(result.value);
418
+ }
419
+ }
420
+
421
+ // Sort by score (desc), then by latency (asc)
422
+ results.sort((a, b) => {
423
+ if (b.overall.score !== a.overall.score) {
424
+ return b.overall.score - a.overall.score;
425
+ }
426
+ return a.overall.latency - b.overall.latency;
427
+ });
428
+
429
+ return results;
430
+ }
431
+
432
+ // ============================================================================
433
+ // Output Formatters
434
+ // ============================================================================
435
+
436
+ function getScoreColor(score) {
437
+ if (score >= 80) return C.green;
438
+ if (score >= 50) return C.yellow;
439
+ return C.red;
440
+ }
441
+
442
+ function getLatencyColor(latency) {
443
+ if (latency < 50) return C.green;
444
+ if (latency < 200) return C.yellow;
445
+ return C.red;
446
+ }
447
+
448
+ function formatLatency(ms) {
449
+ if (ms === null || ms === undefined) return 'N/A';
450
+ if (ms < 1) return '<1ms';
451
+ return `${Math.round(ms)}ms`;
452
+ }
453
+
454
+ function printResult(result, verbose = false) {
455
+ const scoreColor = getScoreColor(result.overall.score);
456
+ const status = result.overall.success ? `${C.green}✓${C.reset}` : `${C.red}✗${C.reset}`;
457
+
458
+ console.log(`\n ${status} ${C.bright}${result.url}${C.reset}`);
459
+
460
+ if (result.name) {
461
+ console.log(` ${C.dim}Name: ${result.name}${C.reset}`);
462
+ }
463
+
464
+ console.log(` ${C.dim}Score:${C.reset} ${scoreColor}${result.overall.score}%${C.reset}`);
465
+ console.log(` ${C.dim}Total Time:${C.reset} ${getLatencyColor(result.overall.latency)}${formatLatency(result.overall.latency)}${C.reset}`);
466
+
467
+ if (result.overall.successfulTests !== undefined) {
468
+ console.log(` ${C.dim}Tests Passed:${C.reset} ${result.overall.successfulTests}/${result.overall.totalTests}`);
469
+ }
470
+
471
+ if (verbose && result.tests) {
472
+ console.log(`\n ${C.dim}── Test Details ──${C.reset}`);
473
+ for (const [name, test] of Object.entries(result.tests)) {
474
+ const testStatus = test.success ? `${C.green}✓${C.reset}` : `${C.red}✗${C.reset}`;
475
+ const latency = test.latency !== undefined ? ` (${formatLatency(test.latency)})` : '';
476
+ console.log(` ${testStatus} ${name}${latency}`);
477
+ if (test.error) {
478
+ console.log(` ${C.red}${test.error}${C.reset}`);
479
+ }
480
+ }
481
+ }
482
+
483
+ if (result.overall.error) {
484
+ console.log(` ${C.red}Error: ${result.overall.error}${C.reset}`);
485
+ }
486
+ }
487
+
488
+ function printBenchmarkResult(result) {
489
+ const status = result.success ? `${C.green}✓${C.reset}` : `${C.red}✗${C.reset}`;
490
+ console.log(`\n ${status} ${C.bright}${result.url}${C.reset}`);
491
+
492
+ if (!result.success) {
493
+ console.log(` ${C.red}Failed: ${result.errors?.join(', ') || 'Unknown error'}${C.reset}`);
494
+ return;
495
+ }
496
+
497
+ const avgColor = getLatencyColor(result.avg);
498
+ console.log(` ${C.dim}Iterations:${C.reset} ${result.iterations}`);
499
+ console.log(` ${C.dim}Min:${C.reset} ${getLatencyColor(result.min)}${formatLatency(result.min)}${C.reset}`);
500
+ console.log(` ${C.dim}Max:${C.reset} ${getLatencyColor(result.max)}${formatLatency(result.max)}${C.reset}`);
501
+ console.log(` ${C.dim}Avg:${C.reset} ${avgColor}${formatLatency(result.avg)}${C.reset}`);
502
+ console.log(` ${C.dim}Median:${C.reset} ${getLatencyColor(result.median)}${formatLatency(result.median)}${C.reset}`);
503
+ console.log(` ${C.dim}P95:${C.reset} ${getLatencyColor(result.p95)}${formatLatency(result.p95)}${C.reset}`);
504
+
505
+ if (result.errors && result.errors.length > 0) {
506
+ console.log(` ${C.yellow}Errors: ${result.errors.length}${C.reset}`);
507
+ }
508
+ }
509
+
510
+ function printRecommendations(results, bestResult) {
511
+ console.log(`\n${C.cyan}── Recommendations ──${C.reset}\n`);
512
+
513
+ if (!bestResult || !bestResult.overall.success) {
514
+ console.log(` ${C.red}⚠ No healthy RPC endpoints found${C.reset}\n`);
515
+ console.log(` ${C.dim}Suggestions:${C.reset}`);
516
+ console.log(` 1. Check if your local validator is running: ${C.cyan}aether validator start${C.reset}`);
517
+ console.log(` 2. Verify RPC URL is correct`);
518
+ console.log(` 3. Check firewall settings for port 8899`);
519
+ console.log(` 4. Set custom RPC: ${C.cyan}export AETHER_RPC=http://your-rpc:8899${C.reset}`);
520
+ return;
521
+ }
522
+
523
+ console.log(` ${C.green}✓ Best RPC:${C.reset} ${C.bright}${bestResult.url}${C.reset}`);
524
+ console.log(` ${C.dim}Latency: ${formatLatency(bestResult.overall.latency)} | Score: ${bestResult.overall.score}%${C.reset}\n`);
525
+
526
+ // Check for issues
527
+ const failedEndpoints = results.filter(r => !r.overall.success);
528
+ if (failedEndpoints.length > 0) {
529
+ console.log(` ${C.yellow}⚠ ${failedEndpoints.length} endpoint(s) unavailable${C.reset}`);
530
+ console.log(` ${C.dim}Run with --verbose for details${C.reset}\n`);
531
+ }
532
+
533
+ // Network health
534
+ const avgScore = results.reduce((sum, r) => sum + (r.overall.score || 0), 0) / results.length;
535
+ if (avgScore < 50) {
536
+ console.log(` ${C.yellow}⚠ Network health is degraded${C.reset}`);
537
+ console.log(` ${C.dim}Average score: ${avgScore.toFixed(0)}%${C.reset}\n`);
538
+ }
539
+ }
540
+
541
+ // ============================================================================
542
+ // Main Command
543
+ // ============================================================================
544
+
545
+ async function networkDiagnosticsCommand() {
546
+ const opts = parseArgs();
547
+
548
+ if (opts.help) {
549
+ showHelp();
550
+ return;
551
+ }
552
+
553
+ if (!opts.json) {
554
+ console.log(`\n${C.bright}${C.cyan}╔═══════════════════════════════════════════════════════════════╗`);
555
+ console.log(`${C.bright}${C.cyan}║ AETHER NETWORK DIAGNOSTICS ║`);
556
+ console.log(`${C.bright}${C.cyan}╚═══════════════════════════════════════════════════════════════╝${C.reset}\n`);
557
+ }
558
+
559
+ let results = [];
560
+ let bestResult = null;
561
+
562
+ try {
563
+ if (opts.benchmark) {
564
+ // Benchmark mode
565
+ if (!opts.json) {
566
+ console.log(`${C.dim}Running latency benchmark...${C.reset}\n`);
567
+ }
568
+
569
+ const endpoints = opts.all ? getEffectiveEndpoints() : [{ url: opts.rpc, name: 'Custom' }];
570
+ const benchmarks = [];
571
+
572
+ for (const ep of endpoints) {
573
+ if (!opts.json) {
574
+ process.stdout.write(` Testing ${ep.url}... `);
575
+ }
576
+ const result = await benchmarkEndpoint(ep.url);
577
+ benchmarks.push(result);
578
+ if (!opts.json) {
579
+ console.log(result.success ? `${C.green}✓${C.reset}` : `${C.red}✗${C.reset}`);
580
+ }
581
+ }
582
+
583
+ if (opts.json) {
584
+ console.log(JSON.stringify({ benchmarks, timestamp: new Date().toISOString() }, null, 2));
585
+ } else {
586
+ console.log(`\n${C.cyan}── Benchmark Results ──${C.reset}`);
587
+ for (const result of benchmarks) {
588
+ printBenchmarkResult(result);
589
+ }
590
+
591
+ // Summary
592
+ const successful = benchmarks.filter(b => b.success);
593
+ if (successful.length > 0) {
594
+ const fastest = successful.sort((a, b) => a.avg - b.avg)[0];
595
+ console.log(`\n${C.green}✓ Fastest RPC:${C.reset} ${C.bright}${fastest.url}${C.reset} (${formatLatency(fastest.avg)} avg)`);
596
+ }
597
+ }
598
+
599
+ return;
600
+ }
601
+
602
+ if (opts.auto) {
603
+ // Auto-select mode
604
+ const endpoints = getEffectiveEndpoints();
605
+
606
+ if (!opts.json) {
607
+ console.log(`${C.dim}Testing ${endpoints.length} RPC endpoint(s)...${C.reset}\n`);
608
+ }
609
+
610
+ results = await findBestEndpoint(endpoints, opts.timeout);
611
+
612
+ if (results.length > 0) {
613
+ bestResult = results[0];
614
+ }
615
+
616
+ if (opts.json) {
617
+ console.log(JSON.stringify({
618
+ results,
619
+ recommended: bestResult?.url || null,
620
+ timestamp: new Date().toISOString(),
621
+ }, null, 2));
622
+ } else {
623
+ console.log(`${C.cyan}── Test Results ──${C.reset}`);
624
+ for (const result of results) {
625
+ printResult(result);
626
+ }
627
+ printRecommendations(results, bestResult);
628
+ }
629
+
630
+ // Update config with best RPC
631
+ if (bestResult?.overall?.success) {
632
+ const cfg = loadConfig();
633
+ cfg.preferredRpc = bestResult.url;
634
+ saveConfig(cfg);
635
+
636
+ if (!opts.json) {
637
+ console.log(`\n ${C.green}✓ Saved preferred RPC to config${C.reset}`);
638
+ console.log(` ${C.dim}Set AETHER_RPC=${bestResult.url} to use this endpoint${C.reset}\n`);
639
+ }
640
+ }
641
+
642
+ return;
643
+ }
644
+
645
+ // Single RPC test mode
646
+ if (!opts.json) {
647
+ console.log(`${C.dim}Testing RPC endpoint...${C.reset}`);
648
+ console.log(` ${C.dim}URL: ${opts.rpc}${C.reset}`);
649
+ console.log(` ${C.dim}Timeout: ${opts.timeout}ms${C.reset}\n`);
650
+ }
651
+
652
+ const result = await testEndpoint(opts.rpc, opts.timeout);
653
+ results = [result];
654
+ bestResult = result.overall.success ? result : null;
655
+
656
+ if (opts.json) {
657
+ console.log(JSON.stringify(result, null, 2));
658
+ } else {
659
+ printResult(result, true);
660
+
661
+ // Health summary
662
+ console.log(`\n${C.cyan}── Health Summary ──${C.reset}\n`);
663
+ if (result.overall.success) {
664
+ console.log(` ${C.green}✓ RPC is healthy${C.reset}`);
665
+ console.log(` ${C.dim} Score: ${result.overall.score}%${C.reset}`);
666
+ console.log(` ${C.dim} Latency: ${formatLatency(result.overall.latency)}${C.reset}`);
667
+
668
+ if (result.tests?.slot?.slot !== undefined) {
669
+ console.log(` ${C.dim} Current slot: ${result.tests.slot.slot}${C.reset}`);
670
+ }
671
+ } else {
672
+ console.log(` ${C.red}✗ RPC is unhealthy${C.reset}`);
673
+ console.log(` ${C.dim} Error: ${result.overall.error || 'Unknown error'}${C.reset}`);
674
+ console.log(`\n ${C.dim}Troubleshooting:${C.reset}`);
675
+ console.log(` • Check if validator is running: ${C.cyan}aether validator status${C.reset}`);
676
+ console.log(` • Verify RPC URL: ${opts.rpc}`);
677
+ console.log(` • Check network connectivity`);
678
+ console.log(` • Try: ${C.cyan}aether network-diagnostics --auto${C.reset}`);
679
+ }
680
+ console.log();
681
+ }
682
+
683
+ } catch (err) {
684
+ if (opts.json) {
685
+ console.log(JSON.stringify({
686
+ error: err.message,
687
+ stack: err.stack,
688
+ timestamp: new Date().toISOString(),
689
+ }, null, 2));
690
+ } else {
691
+ console.log(`\n ${C.red}✗ Diagnostics failed:${C.reset} ${err.message}\n`);
692
+ }
693
+ process.exit(1);
694
+ }
695
+ }
696
+
697
+ // Export for module use
698
+ module.exports = { networkDiagnosticsCommand };
699
+
700
+ // Run if called directly
701
+ if (require.main === module) {
702
+ networkDiagnosticsCommand().catch(err => {
703
+ console.error(`${C.red}✗ Unexpected error:${C.reset}`, err.message);
704
+ process.exit(1);
705
+ });
706
+ }