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