@jellylegsai/aether-cli 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +110 -0
  3. package/aether-cli-1.0.0.tgz +0 -0
  4. package/aether-cli-1.8.0.tgz +0 -0
  5. package/aether-hub-1.0.5.tgz +0 -0
  6. package/aether-hub-1.1.8.tgz +0 -0
  7. package/aether-hub-1.2.1.tgz +0 -0
  8. package/commands/account.js +280 -0
  9. package/commands/apy.js +499 -0
  10. package/commands/balance.js +241 -0
  11. package/commands/blockhash.js +181 -0
  12. package/commands/broadcast.js +387 -0
  13. package/commands/claim.js +490 -0
  14. package/commands/config.js +851 -0
  15. package/commands/delegations.js +582 -0
  16. package/commands/doctor.js +769 -0
  17. package/commands/emergency.js +667 -0
  18. package/commands/epoch.js +275 -0
  19. package/commands/fees.js +276 -0
  20. package/commands/index.js +78 -0
  21. package/commands/info.js +495 -0
  22. package/commands/init.js +816 -0
  23. package/commands/install.js +666 -0
  24. package/commands/kyc.js +272 -0
  25. package/commands/logs.js +315 -0
  26. package/commands/monitor.js +431 -0
  27. package/commands/multisig.js +701 -0
  28. package/commands/network.js +429 -0
  29. package/commands/nft.js +857 -0
  30. package/commands/ping.js +266 -0
  31. package/commands/price.js +253 -0
  32. package/commands/rewards.js +931 -0
  33. package/commands/sdk-test.js +477 -0
  34. package/commands/sdk.js +656 -0
  35. package/commands/slot.js +155 -0
  36. package/commands/snapshot.js +470 -0
  37. package/commands/stake-info.js +139 -0
  38. package/commands/stake-positions.js +205 -0
  39. package/commands/stake.js +516 -0
  40. package/commands/stats.js +396 -0
  41. package/commands/status.js +327 -0
  42. package/commands/supply.js +391 -0
  43. package/commands/tps.js +238 -0
  44. package/commands/transfer.js +495 -0
  45. package/commands/tx-history.js +346 -0
  46. package/commands/unstake.js +597 -0
  47. package/commands/validator-info.js +657 -0
  48. package/commands/validator-register.js +593 -0
  49. package/commands/validator-start.js +323 -0
  50. package/commands/validator-status.js +227 -0
  51. package/commands/validators.js +626 -0
  52. package/commands/wallet.js +1570 -0
  53. package/index.js +593 -0
  54. package/lib/errors.js +398 -0
  55. package/package.json +76 -0
  56. package/sdk/README.md +210 -0
  57. package/sdk/index.js +1639 -0
  58. package/sdk/package.json +34 -0
  59. package/sdk/rpc.js +254 -0
  60. package/sdk/test.js +85 -0
  61. package/test/doctor.test.js +76 -0
  62. package/validator-identity.json +4 -0
@@ -0,0 +1,769 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * aether-cli doctor - System Requirements Checker
4
+ *
5
+ * Validates that a validator's hardware meets minimum requirements:
6
+ * - CPU: 8+ cores
7
+ * - RAM: 32GB+ total, 28GB+ available
8
+ * - Disk: 512GB+ SSD with 340GB+ free
9
+ * - Network: 100Mbps+ upload/download
10
+ * - Firewall: Required ports open
11
+ *
12
+ * @see docs/MINING_VALIDATOR_TOOLS.md for spec
13
+ */
14
+
15
+ const { execSync } = require('child_process');
16
+ const os = require('os');
17
+ const fs = require('fs');
18
+ const path = require('path');
19
+ const readline = require('readline');
20
+
21
+ // Import error handling utilities
22
+ const { withErrorHandling, C } = require('../lib/errors');
23
+
24
+ // ANSI colors for terminal output - now using centralized C object
25
+ const colors = {
26
+ reset: C.reset,
27
+ bright: C.bright,
28
+ red: C.red,
29
+ green: C.green,
30
+ yellow: C.yellow,
31
+ blue: C.blue,
32
+ magenta: C.magenta,
33
+ cyan: C.cyan,
34
+ };
35
+
36
+ // Minimum requirements per tier (from spec)
37
+ const TIER_REQUIREMENTS = {
38
+ full: {
39
+ badge: '[FULL]',
40
+ cpu: { minCores: 8 },
41
+ ram: { minTotalGB: 32, minAvailableGB: 28 },
42
+ disk: { minTotalGB: 512, minFreeGB: 340 },
43
+ network: { minSpeedMbps: 100 },
44
+ ports: { p2p: 8001, p2pNode: 8002, rpc: 8899, ssh: 22 },
45
+ stake: '10,000 AETH',
46
+ consensusWeight: '1.0x',
47
+ canProduceBlocks: true,
48
+ },
49
+ lite: {
50
+ badge: '[LITE]',
51
+ cpu: { minCores: 4 },
52
+ ram: { minTotalGB: 8, minAvailableGB: 6 },
53
+ disk: { minTotalGB: 100, minFreeGB: 50 },
54
+ network: { minSpeedMbps: 25 },
55
+ ports: { p2p: 8001, rpc: 8899, ssh: 22 },
56
+ stake: '1,000 AETH',
57
+ consensusWeight: 'stake/10000 (e.g., 0.1x at 1K AETH)',
58
+ canProduceBlocks: false,
59
+ },
60
+ observer: {
61
+ badge: '[OBSERVER]',
62
+ cpu: { minCores: 2 },
63
+ ram: { minTotalGB: 4, minAvailableGB: 3 },
64
+ disk: { minTotalGB: 50, minFreeGB: 25 },
65
+ network: { minSpeedMbps: 10 },
66
+ ports: { p2p: 8001, ssh: 22 }, // inbound only, no RPC
67
+ stake: '0 AETH',
68
+ consensusWeight: '0x (relay-only)',
69
+ canProduceBlocks: false,
70
+ },
71
+ };
72
+
73
+ // Default to full tier
74
+ const DEFAULT_TIER = 'full';
75
+
76
+ /**
77
+ * Execute shell command and return output
78
+ */
79
+ function runCommand(cmd, options = {}) {
80
+ try {
81
+ return execSync(cmd, {
82
+ encoding: 'utf-8',
83
+ stdio: ['pipe', 'pipe', 'pipe'],
84
+ ...options,
85
+ }).trim();
86
+ } catch (error) {
87
+ return options.allowFailure ? null : error.message;
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Check CPU specifications
93
+ */
94
+ function checkCPU(tier = DEFAULT_TIER) {
95
+ const reqs = TIER_REQUIREMENTS[tier];
96
+ const cpus = os.cpus();
97
+ const physicalCores = cpus.length / 2; // Hyperthreading aware
98
+ const model = cpus[0].model;
99
+ const speed = cpus[0].speed;
100
+
101
+ const passed = physicalCores >= reqs.cpu.minCores;
102
+
103
+ return {
104
+ section: 'CPU',
105
+ model,
106
+ physicalCores,
107
+ logicalCores: cpus.length,
108
+ frequency: `${speed} MHz`,
109
+ passed,
110
+ message: passed
111
+ ? `✅ PASS (${physicalCores} cores >= ${reqs.cpu.minCores} required)`
112
+ : `❌ FAIL (${physicalCores} cores < ${reqs.cpu.minCores} required)`,
113
+ fixable: false,
114
+ fixNote: 'CPU upgrade required - hardware limitation',
115
+ };
116
+ }
117
+
118
+ /**
119
+ * Check memory specifications
120
+ */
121
+ function checkMemory(tier = DEFAULT_TIER) {
122
+ const reqs = TIER_REQUIREMENTS[tier];
123
+ const totalGB = os.totalmem() / (1024 * 1024 * 1024);
124
+ const freeGB = os.freemem() / (1024 * 1024 * 1024);
125
+ const availableGB = freeGB; // Simplified - in production would check swap too
126
+
127
+ const totalPassed = totalGB >= reqs.ram.minTotalGB;
128
+ const availablePassed = availableGB >= reqs.ram.minAvailableGB;
129
+ const passed = totalPassed && availablePassed;
130
+
131
+ return {
132
+ section: 'Memory',
133
+ total: `${totalGB.toFixed(1)} GB`,
134
+ available: `${availableGB.toFixed(1)} GB`,
135
+ passed,
136
+ message: passed
137
+ ? `✅ PASS (${totalGB.toFixed(1)} GB total, ${availableGB.toFixed(1)} GB available)`
138
+ : `❌ FAIL (need ${reqs.ram.minTotalGB} GB total, ${reqs.ram.minAvailableGB} GB available)`,
139
+ fixable: false,
140
+ fixNote: 'RAM upgrade required or close memory-intensive applications',
141
+ };
142
+ }
143
+
144
+ /**
145
+ * Check disk specifications
146
+ */
147
+ function checkDisk(tier = DEFAULT_TIER) {
148
+ const reqs = TIER_REQUIREMENTS[tier];
149
+ // Get disk info for root partition (works on Linux/Mac)
150
+ let diskInfo = { mount: '/', type: 'SSD', total: 0, free: 0 };
151
+
152
+ try {
153
+ if (process.platform === 'win32') {
154
+ // Windows: use PowerShell to get disk info
155
+ const output = runCommand('powershell -c "Get-Volume -DriveType Fixed | Where-Object {$_.DriveLetter -eq (Get-Location).Drive.Name} | Select-Object Size,SizeRemaining"', { allowFailure: true });
156
+ if (output) {
157
+ // Parse PowerShell output (format: Size: 123456789012, SizeRemaining: 98765432100)
158
+ const lines = output.split('\n');
159
+ for (const line of lines) {
160
+ if (line.includes('Size') && !line.includes('SizeRemaining')) {
161
+ const sizeMatch = line.match(/(\d+)/);
162
+ if (sizeMatch) diskInfo.total = parseInt(sizeMatch[1]) / (1024 * 1024 * 1024);
163
+ }
164
+ if (line.includes('SizeRemaining')) {
165
+ const freeMatch = line.match(/(\d+)/);
166
+ if (freeMatch) diskInfo.free = parseInt(freeMatch[1]) / (1024 * 1024 * 1024);
167
+ }
168
+ }
169
+ }
170
+ // Fallback: use Get-PSDrive
171
+ if (diskInfo.total === 0) {
172
+ const psDrive = runCommand('powershell -c "(Get-PSDrive -Name (Get-Location).Drive.Name).Used / 1GB"', { allowFailure: true });
173
+ const psFree = runCommand('powershell -c "(Get-PSDrive -Name (Get-Location).Drive.Name).Free / 1GB"', { allowFailure: true });
174
+ if (psDrive && psFree) {
175
+ diskInfo.free = parseFloat(psFree);
176
+ diskInfo.total = parseFloat(psDrive) + parseFloat(psFree);
177
+ }
178
+ }
179
+ } else {
180
+ // Linux/Mac: use df
181
+ const output = runCommand('df -k / | tail -1');
182
+ const parts = output.split(/\s+/);
183
+ if (parts.length >= 4) {
184
+ diskInfo.total = parseInt(parts[1]) / (1024 * 1024);
185
+ diskInfo.free = parseInt(parts[3]) / (1024 * 1024);
186
+ }
187
+ }
188
+ } catch (e) {
189
+ // Fallback - mark as unknown
190
+ diskInfo.total = 0;
191
+ diskInfo.free = 0;
192
+ }
193
+
194
+ const totalPassed = diskInfo.total >= reqs.disk.minTotalGB;
195
+ const freePassed = diskInfo.free >= reqs.disk.minFreeGB;
196
+ const passed = totalPassed && freePassed;
197
+
198
+ return {
199
+ section: 'Disk',
200
+ mount: diskInfo.mount,
201
+ type: diskInfo.type,
202
+ total: `${diskInfo.total.toFixed(0)} GB`,
203
+ free: `${diskInfo.free.toFixed(0)} GB`,
204
+ passed,
205
+ message: passed
206
+ ? `✅ PASS (${diskInfo.total.toFixed(0)} GB total, ${diskInfo.free.toFixed(0)} GB free)`
207
+ : `❌ FAIL (need ${reqs.disk.minTotalGB} GB total, ${reqs.disk.minFreeGB} GB free)`,
208
+ fixable: !totalPassed ? false : true,
209
+ fixNote: totalPassed ? 'Free up disk space by removing old files or logs' : 'Larger disk required - hardware limitation',
210
+ };
211
+ }
212
+
213
+ /**
214
+ * Check network specifications
215
+ */
216
+ function checkNetwork(tier = DEFAULT_TIER) {
217
+ const reqs = TIER_REQUIREMENTS[tier];
218
+ // Try to get public IP
219
+ let publicIP = 'Unknown';
220
+ try {
221
+ publicIP = runCommand('curl -s ifconfig.me', { allowFailure: true }) ||
222
+ runCommand('curl -s icanhazip.com', { allowFailure: true }) ||
223
+ 'Unknown';
224
+ } catch (e) {
225
+ publicIP = 'Unknown';
226
+ }
227
+
228
+ // Network speed test would require external API
229
+ // For now, we check interface speed
230
+ let downloadSpeed = 'Unknown';
231
+ let uploadSpeed = 'Unknown';
232
+ let latency = 'Unknown';
233
+ let passed = true; // Assume pass if we can't test
234
+
235
+ // In production, integrate with speedtest CLI or similar
236
+ // For MVP, we'll show interface info
237
+ const interfaces = os.networkInterfaces();
238
+ const interfaceCount = Object.keys(interfaces).length;
239
+
240
+ return {
241
+ section: 'Network',
242
+ publicIP,
243
+ download: downloadSpeed,
244
+ upload: uploadSpeed,
245
+ latency,
246
+ interfaces: interfaceCount,
247
+ required: `${reqs.network.minSpeedMbps} Mbps`,
248
+ passed,
249
+ message: passed
250
+ ? `✅ PASS (Network interfaces detected, need ${reqs.network.minSpeedMbps} Mbps)`
251
+ : `❌ FAIL`,
252
+ fixable: false,
253
+ fixNote: 'Network connectivity is system-level',
254
+ };
255
+ }
256
+
257
+ /**
258
+ * Check firewall and port availability
259
+ */
260
+ function checkFirewall(tier = DEFAULT_TIER) {
261
+ const reqs = TIER_REQUIREMENTS[tier];
262
+ const results = { p2p: false, rpc: false, ssh: false };
263
+ const blockedPorts = [];
264
+
265
+ try {
266
+ if (process.platform === 'linux') {
267
+ // Check if ufw is active and ports are open
268
+ const ufwStatus = runCommand('ufw status 2>&1', { allowFailure: true });
269
+ if (ufwStatus && !ufwStatus.includes('inactive')) {
270
+ results.p2p = ufwStatus.includes(`${reqs.ports.p2p}`);
271
+ results.ssh = ufwStatus.includes(`${reqs.ports.ssh}`);
272
+ // RPC only required for full/lite tiers
273
+ if (reqs.ports.rpc) {
274
+ results.rpc = ufwStatus.includes(`${reqs.ports.rpc}`);
275
+ } else {
276
+ results.rpc = true; // observer doesn't need RPC
277
+ }
278
+
279
+ if (!results.p2p) blockedPorts.push(reqs.ports.p2p);
280
+ if (reqs.ports.rpc && !results.rpc) blockedPorts.push(reqs.ports.rpc);
281
+ if (!results.ssh) blockedPorts.push(reqs.ports.ssh);
282
+ } else {
283
+ // If firewall inactive, assume ports are accessible
284
+ results.p2p = true;
285
+ results.rpc = reqs.ports.rpc ? true : true;
286
+ results.ssh = true;
287
+ }
288
+ } else if (process.platform === 'win32') {
289
+ // Windows Firewall check - test each port
290
+ const testPort = (port) => {
291
+ try {
292
+ const result = runCommand(`powershell -c "Get-NetFirewallRule -DisplayName '*Aether*' -ErrorAction SilentlyContinue | Where-Object { $_.Enabled -eq True }"`, { allowFailure: true });
293
+ // Simplified: check if any aether rules exist
294
+ if (result && result.includes('Aether')) {
295
+ return true;
296
+ }
297
+ // Try to bind to port to test availability
298
+ const bindTest = runCommand(`powershell -c "$listener = [System.Net.Sockets.TcpListener]::new([System.Net.IPAddress]::Any, ${port}); $listener.Start(); $listener.Stop()"`, { allowFailure: true });
299
+ return bindTest === null || !bindTest.includes('error');
300
+ } catch {
301
+ return false;
302
+ }
303
+ };
304
+
305
+ results.p2p = testPort(reqs.ports.p2p);
306
+ results.ssh = testPort(reqs.ports.ssh);
307
+ if (reqs.ports.rpc) {
308
+ results.rpc = testPort(reqs.ports.rpc);
309
+ } else {
310
+ results.rpc = true; // observer doesn't need RPC
311
+ }
312
+
313
+ if (!results.p2p) blockedPorts.push(reqs.ports.p2p);
314
+ if (reqs.ports.rpc && !results.rpc) blockedPorts.push(reqs.ports.rpc);
315
+ if (!results.ssh) blockedPorts.push(reqs.ports.ssh);
316
+ } else {
317
+ // macOS / other - assume pass
318
+ results.p2p = true;
319
+ results.rpc = reqs.ports.rpc ? true : true;
320
+ results.ssh = true;
321
+ }
322
+ } catch (e) {
323
+ // Assume pass if we can't check
324
+ results.p2p = true;
325
+ results.rpc = reqs.ports.rpc ? true : true;
326
+ results.ssh = true;
327
+ }
328
+
329
+ const allPassed = results.p2p && results.rpc && results.ssh;
330
+
331
+ return {
332
+ section: 'Firewall',
333
+ p2p: results.p2p,
334
+ rpc: results.rpc,
335
+ ssh: results.ssh,
336
+ blockedPorts,
337
+ passed: allPassed,
338
+ message: allPassed
339
+ ? `✅ PASS (All required ports accessible)`
340
+ : `❌ FAIL (Ports ${blockedPorts.join(', ')} may be blocked)`,
341
+ fixable: blockedPorts.length > 0,
342
+ fixNote: blockedPorts.length > 0 ? `Add firewall rules for ports ${blockedPorts.join(', ')}` : '',
343
+ };
344
+ }
345
+
346
+ /**
347
+ * Check if validator binary exists
348
+ */
349
+ function checkValidatorBinary() {
350
+ const platform = os.platform();
351
+ const isWindows = platform === 'win32';
352
+ const binaryName = isWindows ? 'aether-validator.exe' : 'aether-validator';
353
+
354
+ // Check the expected location based on repo layout
355
+ // Resolve workspace root — walk up from CLI to find the actual repo
356
+ // Look for Jelly-legs-unsteady-workshop that has real workspace markers (src/, .github/)
357
+ // not the nested copy inside the npm package
358
+ const npmPackageDir = path.join(__dirname, '..', '..');
359
+ let workspaceRoot = npmPackageDir;
360
+
361
+ let dir = npmPackageDir;
362
+ for (let i = 0; i < 15; i++) {
363
+ const candidate = path.join(dir, 'Jelly-legs-unsteady-workshop');
364
+ // Must have real workspace markers (Cargo.toml or .github/) to distinguish from npm package nested copy
365
+ if (fs.existsSync(path.join(candidate, 'Cargo.toml')) || fs.existsSync(path.join(candidate, '.github'))) {
366
+ workspaceRoot = candidate;
367
+ break;
368
+ }
369
+ const parent = path.dirname(dir);
370
+ if (parent === dir) break;
371
+ dir = parent;
372
+ }
373
+
374
+ const repoPath = path.join(workspaceRoot, 'Jelly-legs-unsteady-workshop');
375
+ // Prefer release binary, fall back to debug
376
+ const releaseBinary = path.join(repoPath, 'target', 'release', binaryName);
377
+ const debugBinary = path.join(repoPath, 'target', 'debug', binaryName);
378
+ const binaryPath = fs.existsSync(releaseBinary) ? releaseBinary : debugBinary;
379
+
380
+ const exists = fs.existsSync(binaryPath);
381
+
382
+ return {
383
+ section: 'Validator Binary',
384
+ path: binaryPath,
385
+ exists,
386
+ passed: exists,
387
+ message: exists
388
+ ? `✅ PASS (Binary found at ${binaryPath})`
389
+ : `❌ FAIL (Binary not found at ${binaryPath})`,
390
+ fixable: true,
391
+ fixNote: exists ? '' : 'Run cargo build --bin aether-validator',
392
+ };
393
+ }
394
+
395
+ /**
396
+ * Get OS information
397
+ */
398
+ function getOSInfo() {
399
+ const platform = os.platform();
400
+ const arch = os.arch();
401
+ const release = os.release();
402
+
403
+ let osName = platform;
404
+ try {
405
+ if (platform === 'linux') {
406
+ const osRelease = fs.readFileSync('/etc/os-release', 'utf-8');
407
+ const nameMatch = osRelease.match(/^NAME="([^"]+)"/m);
408
+ const versionMatch = osRelease.match(/^VERSION_ID="([^"]+)"/m);
409
+ if (nameMatch) osName = nameMatch[1];
410
+ if (versionMatch) osName += ` ${versionMatch[1]}`;
411
+ } else if (platform === 'darwin') {
412
+ const darwinVersion = runCommand('sw_vers -productVersion', { allowFailure: true });
413
+ if (darwinVersion) osName = `macOS ${darwinVersion}`;
414
+ } else if (platform === 'win32') {
415
+ const winVersion = runCommand('powershell -c "(Get-CimInstance Win32_OperatingSystem).Caption"', { allowFailure: true });
416
+ if (winVersion) osName = winVersion;
417
+ }
418
+ } catch (e) {
419
+ // Use defaults
420
+ }
421
+
422
+ return { platform, arch, release, osName };
423
+ }
424
+
425
+ /**
426
+ * Print section header
427
+ */
428
+ function printSectionHeader(title) {
429
+ console.log(`\n${colors.bright}${colors.cyan}${title}${colors.reset}`);
430
+ console.log(`${colors.cyan}${'─'.repeat(60)}${colors.reset}`);
431
+ }
432
+
433
+ /**
434
+ * Print check result
435
+ */
436
+ function printCheckResult(check) {
437
+ console.log(`\n${colors.bright}${check.section}${colors.reset}`);
438
+ Object.entries(check).forEach(([key, value]) => {
439
+ if (['section', 'passed', 'message', 'fixable', 'fixNote'].includes(key)) return;
440
+ console.log(` ${key}: ${value}`);
441
+ });
442
+ console.log(` ${check.message}`);
443
+ }
444
+
445
+ /**
446
+ * Print ASCII art header
447
+ */
448
+ function printHeader(tier = DEFAULT_TIER) {
449
+ const reqs = TIER_REQUIREMENTS[tier];
450
+ const header = `
451
+ ${colors.bright}${colors.cyan}
452
+ ███╗ ███╗██╗███████╗███████╗██╗ ██████╗ ███╗ ██╗
453
+ ████╗ ████║██║██╔════╝██╔════╝██║██╔═══██╗████╗ ██║
454
+ ██╔████╔██║██║███████╗███████╗██║██║ ██║██╔██╗ ██║
455
+ ██║╚██╔╝██║██║╚════██║╚════██║██║██║ ██║██║╚██╗██║
456
+ ██║ ╚═╝ ██║██║███████║███████║██║╚██████╔╝██║ ╚████║
457
+ ╚═╝ ╚═╝╚═╝╚══════╝╚══════╝╚═╝ ╚═════╝ ╚═╝ ╚═══╝
458
+
459
+ ${colors.reset}${colors.bright}Validator System Check${colors.reset}
460
+ ${colors.yellow}v1.0.0${colors.reset}
461
+ ${new Date().toISOString().split('T')[0]}
462
+ ${colors.magenta}${reqs.badge}${colors.reset}
463
+ `.trim();
464
+ console.log(header);
465
+ console.log(`\n${colors.cyan}${'━'.repeat(60)}${colors.reset}`);
466
+
467
+ // Print tier summary
468
+ console.log(`\n${colors.bright}Tier Requirements:${colors.reset}`);
469
+ console.log(` ${colors.cyan}Stake:${colors.reset} ${reqs.stake}`);
470
+ console.log(` ${colors.cyan}Consensus Weight:${colors.reset} ${reqs.consensusWeight}`);
471
+ console.log(` ${colors.cyan}Block Production:${colors.reset} ${reqs.canProduceBlocks ? '✅ Yes' : '❌ No'}`);
472
+ console.log(` ${colors.cyan}CPU:${colors.reset} ${reqs.cpu.minCores}+ cores`);
473
+ console.log(` ${colors.cyan}RAM:${colors.reset} ${reqs.ram.minTotalGB}GB+ total, ${reqs.ram.minAvailableGB}GB+ available`);
474
+ console.log(` ${colors.cyan}Disk:${colors.reset} ${reqs.disk.minTotalGB}GB+ total, ${reqs.disk.minFreeGB}GB+ free`);
475
+ console.log(` ${colors.cyan}Network:${colors.reset} ${reqs.network.minSpeedMbps}+ Mbps`);
476
+ console.log(` ${colors.cyan}Ports:${colors.reset} ${Object.values(reqs.ports).join(', ')}`);
477
+ console.log(`\n${colors.cyan}${'━'.repeat(60)}${colors.reset}`);
478
+ }
479
+
480
+ /**
481
+ * Print summary
482
+ */
483
+ function printSummary(results, tier = DEFAULT_TIER) {
484
+ const reqs = TIER_REQUIREMENTS[tier];
485
+ const allPassed = results.every(r => r.passed);
486
+
487
+ console.log(`\n${colors.cyan}${'━'.repeat(60)}${colors.reset}`);
488
+ console.log(`\n${colors.bright}SUMMARY:${colors.reset} ${colors.magenta}${reqs.badge}${colors.reset}`);
489
+
490
+ if (allPassed) {
491
+ console.log(`\n${colors.bright}${colors.green}✅ All checks passed!${colors.reset}`);
492
+ console.log(`\n${colors.green}Your system is ready to run an AeTHer ${tier.toUpperCase()} validator.${colors.reset}`);
493
+ console.log(`\nNext steps:`);
494
+ console.log(` ${colors.bright}aether-cli validator start --tier ${tier}${colors.reset} # Start validating`);
495
+ console.log(` ${colors.bright}aether-cli validator status${colors.reset} # Check status`);
496
+ console.log(` ${colors.bright}aether-cli help${colors.reset} # View all commands`);
497
+ } else {
498
+ const failed = results.filter(r => !r.passed);
499
+ const fixable = failed.filter(r => r.fixable);
500
+
501
+ console.log(`\n${colors.bright}${colors.red}❌ ${failed.length} check(s) failed${colors.reset}`);
502
+
503
+ if (fixable.length > 0) {
504
+ console.log(`\n${colors.yellow}⚠ ${fixable.length} issue(s) can be auto-fixed:${colors.reset}`);
505
+ fixable.forEach(f => {
506
+ console.log(` ${colors.yellow}• ${f.section}: ${f.fixNote}${colors.reset}`);
507
+ });
508
+ }
509
+
510
+ const notFixable = failed.filter(r => !r.fixable);
511
+ if (notFixable.length > 0) {
512
+ console.log(`\n${colors.red}The following issues require manual action:${colors.reset}`);
513
+ notFixable.forEach(f => {
514
+ console.log(` ${colors.red}• ${f.section}: ${f.fixNote}${colors.reset}`);
515
+ });
516
+ }
517
+ }
518
+
519
+ console.log(`\n${colors.cyan}${'━'.repeat(60)}${colors.reset}\n`);
520
+ }
521
+
522
+ /**
523
+ * Generate fix command for a failed check
524
+ */
525
+ function getFixCommand(check) {
526
+ const platform = os.platform();
527
+
528
+ switch (check.section) {
529
+ case 'Firewall':
530
+ if (platform === 'win32') {
531
+ const ports = check.blockedPorts || [];
532
+ if (ports.length === 0) return null;
533
+ const rules = ports.map(port =>
534
+ `New-NetFirewallRule -DisplayName "Aether Port ${port}" -Direction Inbound -LocalPort ${port} -Protocol TCP -Action Allow`
535
+ ).join('; ');
536
+ return `powershell -c "${rules}"`;
537
+ } else if (platform === 'linux') {
538
+ const ports = check.blockedPorts || [];
539
+ if (ports.length === 0) return null;
540
+ const rules = ports.map(port => `sudo ufw allow ${port}/tcp`).join(' && ');
541
+ return rules;
542
+ }
543
+ return null;
544
+
545
+ case 'Validator Binary': {
546
+ // Use same resolution logic as checkValidatorBinary
547
+ const npmPackageDir = path.join(__dirname, '..', '..');
548
+ let workspaceRoot = npmPackageDir;
549
+ let dir = npmPackageDir;
550
+ for (let i = 0; i < 15; i++) {
551
+ const candidate = path.join(dir, 'Jelly-legs-unsteady-workshop');
552
+ if (fs.existsSync(path.join(candidate, 'src')) || fs.existsSync(path.join(candidate, '.github'))) {
553
+ workspaceRoot = candidate;
554
+ break;
555
+ }
556
+ const parent = path.dirname(dir);
557
+ if (parent === dir) break;
558
+ dir = parent;
559
+ }
560
+ const repoPath = path.join(workspaceRoot, 'Jelly-legs-unsteady-workshop');
561
+ return `cd "${repoPath}" && cargo build --bin aether-validator --release`;
562
+ }
563
+
564
+ case 'Disk':
565
+ // Can't auto-fix disk space, but can suggest cleanup
566
+ if (platform === 'win32') {
567
+ return 'powershell -c "Get-AppxPackage -AllUsers | Where-Object {$_.InstallLocation -like \'*WindowsApps*\'} | Select-Object Name, PackageFullName"';
568
+ } else {
569
+ return 'sudo du -sh /* 2>/dev/null | sort -hr | head -20';
570
+ }
571
+
572
+ default:
573
+ return null;
574
+ }
575
+ }
576
+
577
+ /**
578
+ * Ask user for confirmation
579
+ */
580
+ async function askConfirmation(question) {
581
+ const rl = readline.createInterface({
582
+ input: process.stdin,
583
+ output: process.stdout,
584
+ });
585
+
586
+ return new Promise((resolve) => {
587
+ rl.question(`${colors.yellow}${question}${colors.reset} [y/N] `, (answer) => {
588
+ rl.close();
589
+ resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
590
+ });
591
+ });
592
+ }
593
+
594
+ /**
595
+ * Apply a fix for a failed check
596
+ */
597
+ async function applyFix(check) {
598
+ const command = getFixCommand(check);
599
+
600
+ if (!command) {
601
+ console.log(` ${colors.red}✗ No automated fix available for ${check.section}${colors.reset}`);
602
+ return false;
603
+ }
604
+
605
+ console.log(`\n ${colors.cyan}Proposed fix:${colors.reset}`);
606
+ console.log(` ${colors.bright}${command}${colors.reset}`);
607
+ console.log();
608
+
609
+ const confirmed = await askConfirmation(' Apply this fix?');
610
+
611
+ if (!confirmed) {
612
+ console.log(` ${colors.yellow}Skipped.${colors.reset}`);
613
+ return false;
614
+ }
615
+
616
+ console.log(` ${colors.cyan}Applying fix...${colors.reset}`);
617
+
618
+ try {
619
+ execSync(command, {
620
+ stdio: 'inherit',
621
+ shell: true,
622
+ cwd: process.cwd(),
623
+ });
624
+
625
+ console.log(` ${colors.green}✓ Fix applied successfully!${colors.reset}`);
626
+
627
+ // Re-run the check to verify
628
+ console.log(` ${colors.cyan}Verifying...${colors.reset}`);
629
+ let verifyCheck;
630
+ switch (check.section) {
631
+ case 'Firewall':
632
+ verifyCheck = checkFirewall();
633
+ break;
634
+ case 'Validator Binary':
635
+ verifyCheck = checkValidatorBinary();
636
+ break;
637
+ default:
638
+ verifyCheck = check;
639
+ }
640
+
641
+ if (verifyCheck.passed) {
642
+ console.log(` ${colors.green}✓ Verification passed!${colors.reset}`);
643
+ return true;
644
+ } else {
645
+ console.log(` ${colors.yellow}⚠ Fix applied but check still failing. May require manual intervention.${colors.reset}`);
646
+ return false;
647
+ }
648
+ } catch (err) {
649
+ console.log(` ${colors.red}✗ Fix failed: ${err.message}${colors.reset}`);
650
+ return false;
651
+ }
652
+ }
653
+
654
+ /**
655
+ * Interactive fix mode
656
+ */
657
+ async function interactiveFixMode(results) {
658
+ const failed = results.filter(r => !r.passed);
659
+ const fixable = failed.filter(r => r.fixable);
660
+
661
+ if (fixable.length === 0) {
662
+ console.log(`\n${colors.yellow}No auto-fixable issues found.${colors.reset}`);
663
+ return;
664
+ }
665
+
666
+ console.log(`\n${colors.bright}${colors.cyan}Auto-Fix Mode${colors.reset}`);
667
+ console.log(`${colors.cyan}${'─'.repeat(60)}${colors.reset}`);
668
+ console.log(`\n${colors.yellow}Found ${fixable.length} issue(s) that can be fixed automatically:${colors.reset}\n`);
669
+
670
+ for (const check of fixable) {
671
+ console.log(`${colors.bright}${check.section}${colors.reset}`);
672
+ console.log(` Issue: ${check.fixNote}`);
673
+ console.log();
674
+
675
+ const fixed = await applyFix(check);
676
+
677
+ if (fixed) {
678
+ check.passed = true;
679
+ check.message = `✅ FIXED (was: ${check.message})`;
680
+ }
681
+
682
+ console.log();
683
+ }
684
+
685
+ // Print updated summary
686
+ const stillFailed = results.filter(r => !r.passed);
687
+ if (stillFailed.length === 0) {
688
+ console.log(`\n${colors.bright}${colors.green}🎉 All issues resolved!${colors.reset}`);
689
+ console.log(`\n${colors.green}Your system is now ready to run an AeTHer validator.${colors.reset}`);
690
+ } else {
691
+ console.log(`\n${colors.yellow}⚠ ${stillFailed.length} issue(s) remain unresolved.${colors.reset}`);
692
+ }
693
+ }
694
+
695
+ /**
696
+ * Main doctor command - wrapped with error handling
697
+ */
698
+ async function doctorCommandRaw(options = {}) {
699
+ const { autoFix = false, tier = DEFAULT_TIER } = options;
700
+
701
+ // Validate tier
702
+ if (!TIER_REQUIREMENTS[tier]) {
703
+ const error = new Error(`Invalid tier '${tier}'. Valid tiers: full, lite, observer`);
704
+ error.isValidationError = true;
705
+ throw error;
706
+ }
707
+
708
+ printHeader(tier);
709
+ console.log(`\n${colors.bright}Running system checks for ${tier.toUpperCase()} tier...${colors.reset}\n`);
710
+
711
+ const results = [
712
+ checkCPU(tier),
713
+ checkMemory(tier),
714
+ checkDisk(tier),
715
+ checkNetwork(tier),
716
+ checkFirewall(tier),
717
+ checkValidatorBinary(),
718
+ ];
719
+
720
+ results.forEach(printCheckResult);
721
+ printSummary(results, tier);
722
+
723
+ // If auto-fix mode or user requests it
724
+ const failed = results.filter(r => !r.passed);
725
+ const fixable = failed.filter(r => r.fixable);
726
+
727
+ if (fixable.length > 0) {
728
+ if (autoFix) {
729
+ console.log(`\n${colors.cyan}Auto-fix mode enabled. Attempting fixes...${colors.reset}\n`);
730
+ await interactiveFixMode(results);
731
+ } else {
732
+ console.log(`\n${colors.cyan}Tip: Run ${colors.bright}aether-cli doctor --fix${colors.reset}${colors.cyan} to auto-fix issues.${colors.reset}\n`);
733
+ }
734
+ }
735
+
736
+ // Return exit code based on results
737
+ const allPassed = results.every(r => r.passed);
738
+ return allPassed ? 0 : 1;
739
+ }
740
+
741
+ /**
742
+ * Main doctor command with error handling
743
+ */
744
+ async function doctorCommand(options = {}) {
745
+ return withErrorHandling(doctorCommandRaw, {
746
+ exit: false,
747
+ verbose: process.env.AETHER_VERBOSE === '1'
748
+ })(options);
749
+ }
750
+
751
+ // Export for use as module
752
+ module.exports = { doctorCommand, checkCPU, checkMemory, checkDisk, checkNetwork, checkFirewall, checkValidatorBinary };
753
+
754
+ // Run if called directly
755
+ if (require.main === module) {
756
+ const args = process.argv.slice(2);
757
+ const autoFix = args.includes('--fix') || args.includes('-f');
758
+
759
+ // Parse --tier flag
760
+ let tier = DEFAULT_TIER;
761
+ const tierIndex = args.findIndex(arg => arg === '--tier');
762
+ if (tierIndex !== -1 && args[tierIndex + 1]) {
763
+ tier = args[tierIndex + 1].toLowerCase();
764
+ }
765
+
766
+ doctorCommand({ autoFix, tier }).then(exitCode => {
767
+ process.exit(exitCode);
768
+ });
769
+ }