@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,851 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * aether-cli config
4
+ *
5
+ * Centralized configuration management for the Aether CLI.
6
+ * Persistently store settings like RPC URL, default wallet, preferences.
7
+ * All RPC URLs are validated with real HTTP calls before saving.
8
+ *
9
+ * Usage:
10
+ * aether config get <key> Get a config value
11
+ * aether config set <key> <value> Set a config value (validates RPC URLs)
12
+ * aether config list Show all config
13
+ * aether config init Create default config
14
+ * aether config reset Reset to defaults
15
+ * aether config validate Validate current config (test RPC, etc.)
16
+ * aether config import --file <path> Import config from JSON file
17
+ * aether config export --file <path> Export config to JSON file
18
+ *
19
+ * Config keys:
20
+ * rpc.url Default RPC endpoint (validates on set)
21
+ * rpc.backup Backup RPC endpoint
22
+ * wallet.default Default wallet address
23
+ * wallet.keypair Path to keypair file
24
+ * validator.tier Default validator tier (full|lite|observer)
25
+ * output.format Default output format (text|json)
26
+ * output.colors Enable/disable ANSI colors (true|false)
27
+ * network.timeout RPC timeout in ms (default: 10000)
28
+ *
29
+ * SDK wired to: GET /v1/health, GET /v1/slot, GET /v1/version
30
+ */
31
+
32
+ const fs = require('fs');
33
+ const path = require('path');
34
+ const os = require('os');
35
+ const readline = require('readline');
36
+
37
+ // Import SDK for RPC validation
38
+ const sdkPath = path.join(__dirname, '..', 'sdk', 'index.js');
39
+ const aether = require(sdkPath);
40
+
41
+ // ANSI colours
42
+ const C = {
43
+ reset: '\x1b[0m',
44
+ bright: '\x1b[1m',
45
+ dim: '\x1b[2m',
46
+ red: '\x1b[31m',
47
+ green: '\x1b[32m',
48
+ yellow: '\x1b[33m',
49
+ cyan: '\x1b[36m',
50
+ magenta: '\x1b[35m',
51
+ };
52
+
53
+ const CLI_VERSION = '1.3.0';
54
+ const CONFIG_VERSION = 2;
55
+
56
+ // ---------------------------------------------------------------------------
57
+ // Paths & Config Management
58
+ // ---------------------------------------------------------------------------
59
+
60
+ function getAetherDir() {
61
+ return path.join(os.homedir(), '.aether');
62
+ }
63
+
64
+ function getConfigPath() {
65
+ return path.join(getAetherDir(), 'config.json');
66
+ }
67
+
68
+ function ensureAetherDir() {
69
+ const dir = getAetherDir();
70
+ if (!fs.existsSync(dir)) {
71
+ fs.mkdirSync(dir, { recursive: true });
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Default configuration values
77
+ */
78
+ function getDefaultConfig() {
79
+ return {
80
+ version: CONFIG_VERSION,
81
+ created_at: new Date().toISOString(),
82
+ updated_at: new Date().toISOString(),
83
+ rpc: {
84
+ url: 'http://127.0.0.1:8899',
85
+ backup: null,
86
+ timeout: 10000,
87
+ },
88
+ wallet: {
89
+ default: null,
90
+ keypair: null,
91
+ },
92
+ validator: {
93
+ tier: 'full',
94
+ identity: null,
95
+ },
96
+ output: {
97
+ format: 'text',
98
+ colors: true,
99
+ },
100
+ network: {
101
+ explorer: 'https://explorer.aether.network',
102
+ faucet: null,
103
+ },
104
+ };
105
+ }
106
+
107
+ /**
108
+ * Load config from disk
109
+ */
110
+ function loadConfig() {
111
+ const configPath = getConfigPath();
112
+ if (!fs.existsSync(configPath)) {
113
+ return null;
114
+ }
115
+ try {
116
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
117
+ return migrateConfig(config);
118
+ } catch (err) {
119
+ return null;
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Save config to disk
125
+ */
126
+ function saveConfig(config) {
127
+ ensureAetherDir();
128
+ config.updated_at = new Date().toISOString();
129
+ fs.writeFileSync(getConfigPath(), JSON.stringify(config, null, 2));
130
+ return config;
131
+ }
132
+
133
+ /**
134
+ * Migrate old config versions
135
+ */
136
+ function migrateConfig(config) {
137
+ if (!config.version || config.version < CONFIG_VERSION) {
138
+ // Merge with defaults to add any missing keys
139
+ const defaults = getDefaultConfig();
140
+ const migrated = {
141
+ ...defaults,
142
+ ...config,
143
+ version: CONFIG_VERSION,
144
+ rpc: { ...defaults.rpc, ...config.rpc },
145
+ wallet: { ...defaults.wallet, ...config.wallet },
146
+ validator: { ...defaults.validator, ...config.validator },
147
+ output: { ...defaults.output, ...config.output },
148
+ network: { ...defaults.network, ...config.network },
149
+ };
150
+ return migrated;
151
+ }
152
+ return config;
153
+ }
154
+
155
+ // ---------------------------------------------------------------------------
156
+ // Validation Functions (with REAL RPC calls)
157
+ // ---------------------------------------------------------------------------
158
+
159
+ /**
160
+ * Validate an RPC URL with real HTTP call
161
+ */
162
+ async function validateRpcUrl(url) {
163
+ const errors = [];
164
+
165
+ // Basic URL validation
166
+ if (!url || typeof url !== 'string') {
167
+ return { valid: false, error: 'URL is required' };
168
+ }
169
+
170
+ let parsed;
171
+ try {
172
+ parsed = new URL(url);
173
+ } catch {
174
+ return { valid: false, error: 'Invalid URL format' };
175
+ }
176
+
177
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
178
+ return { valid: false, error: 'URL must be http:// or https://' };
179
+ }
180
+
181
+ // Real RPC validation via SDK
182
+ try {
183
+ const client = new aether.AetherClient({ rpcUrl: url, timeoutMs: 5000 });
184
+ const start = Date.now();
185
+ const [health, slot] = await Promise.all([
186
+ client.getHealth().catch(() => null),
187
+ client.getSlot().catch(() => null),
188
+ ]);
189
+ const latency = Date.now() - start;
190
+
191
+ if (slot === null && health === null) {
192
+ return {
193
+ valid: false,
194
+ error: 'RPC endpoint did not respond to health/slot checks',
195
+ url,
196
+ };
197
+ }
198
+
199
+ return {
200
+ valid: true,
201
+ url,
202
+ latency,
203
+ health: health || 'unknown',
204
+ slot,
205
+ };
206
+ } catch (err) {
207
+ return {
208
+ valid: false,
209
+ error: `RPC validation failed: ${err.message}`,
210
+ url,
211
+ };
212
+ }
213
+ }
214
+
215
+ /**
216
+ * Validate wallet address format
217
+ */
218
+ function validateAddress(addr) {
219
+ if (!addr || typeof addr !== 'string') {
220
+ return { valid: false, error: 'Address is required' };
221
+ }
222
+ if (!addr.startsWith('ATH')) {
223
+ return { valid: false, error: 'Address must start with ATH' };
224
+ }
225
+ if (addr.length < 36) {
226
+ return { valid: false, error: 'Address too short' };
227
+ }
228
+ return { valid: true, address: addr };
229
+ }
230
+
231
+ /**
232
+ * Validate tier value
233
+ */
234
+ function validateTier(tier) {
235
+ const validTiers = ['full', 'lite', 'observer'];
236
+ if (!tier || typeof tier !== 'string') {
237
+ return { valid: false, error: 'Tier is required' };
238
+ }
239
+ if (!validTiers.includes(tier.toLowerCase())) {
240
+ return { valid: false, error: `Tier must be one of: ${validTiers.join(', ')}` };
241
+ }
242
+ return { valid: true, tier: tier.toLowerCase() };
243
+ }
244
+
245
+ /**
246
+ * Validate output format
247
+ */
248
+ function validateFormat(format) {
249
+ const validFormats = ['text', 'json'];
250
+ if (!format || typeof format !== 'string') {
251
+ return { valid: false, error: 'Format is required' };
252
+ }
253
+ if (!validFormats.includes(format.toLowerCase())) {
254
+ return { valid: false, error: `Format must be one of: ${validFormats.join(', ')}` };
255
+ }
256
+ return { valid: true, format: format.toLowerCase() };
257
+ }
258
+
259
+ /**
260
+ * Validate boolean string
261
+ */
262
+ function validateBoolean(value) {
263
+ const truthy = ['true', 'yes', '1', 'on'];
264
+ const falsy = ['false', 'no', '0', 'off'];
265
+ const lower = String(value).toLowerCase();
266
+ if (truthy.includes(lower)) return { valid: true, value: true };
267
+ if (falsy.includes(lower)) return { valid: true, value: false };
268
+ return { valid: false, error: 'Must be true/false, yes/no, 1/0, or on/off' };
269
+ }
270
+
271
+ /**
272
+ * Validate timeout
273
+ */
274
+ function validateTimeout(value) {
275
+ const num = parseInt(value, 10);
276
+ if (isNaN(num) || num < 1000) {
277
+ return { valid: false, error: 'Timeout must be at least 1000ms' };
278
+ }
279
+ if (num > 60000) {
280
+ return { valid: false, error: 'Timeout cannot exceed 60000ms' };
281
+ }
282
+ return { valid: true, timeout: num };
283
+ }
284
+
285
+ // ---------------------------------------------------------------------------
286
+ // Config Value Getters/Setters
287
+ // ---------------------------------------------------------------------------
288
+
289
+ const CONFIG_SCHEMA = {
290
+ 'rpc.url': {
291
+ category: 'rpc',
292
+ key: 'url',
293
+ validate: validateRpcUrl,
294
+ description: 'Default RPC endpoint URL',
295
+ },
296
+ 'rpc.backup': {
297
+ category: 'rpc',
298
+ key: 'backup',
299
+ validate: validateRpcUrl,
300
+ description: 'Backup RPC endpoint URL',
301
+ },
302
+ 'rpc.timeout': {
303
+ category: 'rpc',
304
+ key: 'timeout',
305
+ validate: validateTimeout,
306
+ description: 'RPC timeout in milliseconds',
307
+ },
308
+ 'wallet.default': {
309
+ category: 'wallet',
310
+ key: 'default',
311
+ validate: validateAddress,
312
+ description: 'Default wallet address',
313
+ },
314
+ 'wallet.keypair': {
315
+ category: 'wallet',
316
+ key: 'keypair',
317
+ validate: (v) => ({ valid: fs.existsSync(v), path: v, error: fs.existsSync(v) ? null : 'File does not exist' }),
318
+ description: 'Path to keypair file',
319
+ },
320
+ 'validator.tier': {
321
+ category: 'validator',
322
+ key: 'tier',
323
+ validate: validateTier,
324
+ description: 'Default validator tier (full|lite|observer)',
325
+ },
326
+ 'validator.identity': {
327
+ category: 'validator',
328
+ key: 'identity',
329
+ validate: (v) => ({ valid: true, path: v }),
330
+ description: 'Path to validator identity file',
331
+ },
332
+ 'output.format': {
333
+ category: 'output',
334
+ key: 'format',
335
+ validate: validateFormat,
336
+ description: 'Default output format (text|json)',
337
+ },
338
+ 'output.colors': {
339
+ category: 'output',
340
+ key: 'colors',
341
+ validate: validateBoolean,
342
+ description: 'Enable ANSI colors (true|false)',
343
+ },
344
+ 'network.explorer': {
345
+ category: 'network',
346
+ key: 'explorer',
347
+ validate: (v) => ({ valid: true, url: v }),
348
+ description: 'Block explorer URL',
349
+ },
350
+ 'network.faucet': {
351
+ category: 'network',
352
+ key: 'faucet',
353
+ validate: (v) => ({ valid: true, url: v }),
354
+ description: 'Testnet faucet URL',
355
+ },
356
+ };
357
+
358
+ function getConfigValue(config, key) {
359
+ const schema = CONFIG_SCHEMA[key];
360
+ if (!schema) return { exists: false };
361
+
362
+ const value = config[schema.category]?.[schema.key];
363
+ return { exists: true, value, schema };
364
+ }
365
+
366
+ async function setConfigValue(config, key, value) {
367
+ const schema = CONFIG_SCHEMA[key];
368
+ if (!schema) {
369
+ return { success: false, error: `Unknown config key: ${key}` };
370
+ }
371
+
372
+ // Validate the value
373
+ const validation = await schema.validate(value);
374
+ if (!validation.valid) {
375
+ return { success: false, error: validation.error };
376
+ }
377
+
378
+ // Set the value
379
+ config[schema.category][schema.key] = validation[schema.key] ?? validation.value ?? validation.url ?? validation.path ?? value;
380
+ return { success: true, value: config[schema.category][schema.key] };
381
+ }
382
+
383
+ // ---------------------------------------------------------------------------
384
+ // Command Implementations
385
+ // ---------------------------------------------------------------------------
386
+
387
+ async function configGet(args) {
388
+ const key = args[0];
389
+ const config = loadConfig() || getDefaultConfig();
390
+
391
+ if (!key) {
392
+ console.log(`\n ${C.red}✗ Config key required${C.reset}`);
393
+ console.log(` ${C.dim}Usage: aether config get <key>${C.reset}`);
394
+ console.log(` ${C.dim}Run 'aether config list' to see all keys${C.reset}\n`);
395
+ return;
396
+ }
397
+
398
+ const result = getConfigValue(config, key);
399
+ if (!result.exists) {
400
+ console.log(`\n ${C.red}✗ Unknown config key:${C.reset} ${key}`);
401
+ console.log(` ${C.dim}Run 'aether config list' to see available keys${C.reset}\n`);
402
+ return;
403
+ }
404
+
405
+ console.log(`\n ${C.bright}${C.cyan}── Config Value ──${C.reset}\n`);
406
+ console.log(` ${C.cyan}Key:${C.reset} ${key}`);
407
+ console.log(` ${C.cyan}Description:${C.reset} ${result.schema.description}`);
408
+ console.log(` ${C.cyan}Value:${C.reset} ${result.value !== null ? C.bright + result.value + C.reset : C.dim + '(not set)' + C.reset}`);
409
+ console.log();
410
+ }
411
+
412
+ async function configSet(args) {
413
+ const key = args[0];
414
+ const value = args[1];
415
+
416
+ if (!key || value === undefined) {
417
+ console.log(`\n ${C.red}✗ Key and value required${C.reset}`);
418
+ console.log(` ${C.dim}Usage: aether config set <key> <value>${C.reset}\n`);
419
+ return;
420
+ }
421
+
422
+ let config = loadConfig();
423
+ if (!config) {
424
+ config = getDefaultConfig();
425
+ console.log(` ${C.yellow}⚠ No existing config. Creating new config file...${C.reset}`);
426
+ }
427
+
428
+ const schema = CONFIG_SCHEMA[key];
429
+ if (!schema) {
430
+ console.log(`\n ${C.red}✗ Unknown config key:${C.reset} ${key}`);
431
+ console.log(` ${C.dim}Run 'aether config list' to see available keys${C.reset}\n`);
432
+ return;
433
+ }
434
+
435
+ // Show validation message for RPC URLs
436
+ if (key.includes('rpc.')) {
437
+ console.log(`\n ${C.dim}Validating RPC endpoint...${C.reset}`);
438
+ }
439
+
440
+ const result = await setConfigValue(config, key, value);
441
+
442
+ if (!result.success) {
443
+ console.log(`\n ${C.red}✗ Validation failed:${C.reset} ${result.error}\n`);
444
+ return;
445
+ }
446
+
447
+ saveConfig(config);
448
+
449
+ console.log(`\n ${C.green}✓ Config updated${C.reset}\n`);
450
+ console.log(` ${C.cyan}Key:${C.reset} ${key}`);
451
+ console.log(` ${C.cyan}Value:${C.reset} ${C.bright}${result.value}${C.reset}`);
452
+
453
+ // Show extra info for RPC
454
+ if (key === 'rpc.url' && result.latency) {
455
+ const latencyColor = result.latency < 50 ? C.green : result.latency < 200 ? C.cyan : C.yellow;
456
+ console.log(` ${C.cyan}Health:${C.reset} ${C.green}✓${C.reset} Online (${latencyColor}${result.latency}ms${C.reset})`);
457
+ if (result.slot) {
458
+ console.log(` ${C.cyan}Slot:${C.reset} ${result.slot.toLocaleString()}`);
459
+ }
460
+ }
461
+
462
+ console.log();
463
+ }
464
+
465
+ async function configList(args, asJson = false) {
466
+ const config = loadConfig() || getDefaultConfig();
467
+ const isCompact = args.includes('--compact');
468
+
469
+ if (asJson) {
470
+ console.log(JSON.stringify(config, null, 2));
471
+ return;
472
+ }
473
+
474
+ console.log(`\n ${C.bright}${C.cyan}── Aether CLI Configuration ──${C.reset}\n`);
475
+ console.log(` ${C.dim}Config file: ${getConfigPath()}${C.reset}`);
476
+ console.log(` ${C.dim}Version: ${config.version}${C.reset}\n`);
477
+
478
+ // Group by category
479
+ const categories = {};
480
+ Object.entries(CONFIG_SCHEMA).forEach(([key, schema]) => {
481
+ if (!categories[schema.category]) {
482
+ categories[schema.category] = [];
483
+ }
484
+ const value = config[schema.category]?.[schema.key];
485
+ categories[schema.category].push({ key, value, schema });
486
+ });
487
+
488
+ for (const [category, items] of Object.entries(categories)) {
489
+ console.log(` ${C.bright}${C.cyan}[${category.toUpperCase()}]${C.reset}`);
490
+ for (const item of items) {
491
+ const displayValue = item.value !== null
492
+ ? C.bright + String(item.value) + C.reset
493
+ : C.dim + '(not set)' + C.reset;
494
+ if (isCompact) {
495
+ console.log(` ${C.dim}${item.key}:${C.reset} ${displayValue}`);
496
+ } else {
497
+ console.log(` ${C.cyan}${item.key}${C.reset}`);
498
+ console.log(` ${C.dim}${item.schema.description}${C.reset}`);
499
+ console.log(` ${C.dim}Value:${C.reset} ${displayValue}`);
500
+ }
501
+ }
502
+ console.log();
503
+ }
504
+
505
+ console.log(` ${C.dim}Tip: Use ${C.cyan}aether config set <key> <value>${C.reset}${C.dim} to change settings${C.reset}`);
506
+ console.log(` ${C.dim} Run ${C.cyan}aether config validate${C.reset}${C.dim} to test your configuration${C.reset}\n`);
507
+ }
508
+
509
+ async function configInit(args) {
510
+ const force = args.includes('--force');
511
+ const configPath = getConfigPath();
512
+
513
+ if (fs.existsSync(configPath) && !force) {
514
+ console.log(`\n ${C.yellow}⚠ Config file already exists${C.reset}`);
515
+ console.log(` ${C.dim}Path: ${configPath}${C.reset}`);
516
+ console.log(` ${C.dim}Use --force to overwrite${C.reset}\n`);
517
+ return;
518
+ }
519
+
520
+ const config = getDefaultConfig();
521
+ saveConfig(config);
522
+
523
+ console.log(`\n ${C.green}✓ Configuration initialized${C.reset}\n`);
524
+ console.log(` ${C.dim}Path: ${configPath}${C.reset}`);
525
+ console.log(` ${C.dim}Edit with:${C.reset} ${C.cyan}aether config set <key> <value>${C.reset}`);
526
+ console.log(` ${C.dim}View with:${C.reset} ${C.cyan}aether config list${C.reset}\n`);
527
+ }
528
+
529
+ async function configReset(args) {
530
+ const confirmed = args.includes('--yes');
531
+
532
+ if (!confirmed) {
533
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
534
+ const answer = await new Promise(resolve => {
535
+ rl.question(`\n ${C.yellow}⚠ This will reset all configuration to defaults.${C.reset}\n Continue? [y/N] `, resolve);
536
+ });
537
+ rl.close();
538
+ if (answer.toLowerCase() !== 'y' && answer.toLowerCase() !== 'yes') {
539
+ console.log(`\n ${C.dim}Cancelled.${C.reset}\n`);
540
+ return;
541
+ }
542
+ }
543
+
544
+ const config = getDefaultConfig();
545
+ saveConfig(config);
546
+
547
+ console.log(`\n ${C.green}✓ Configuration reset to defaults${C.reset}\n`);
548
+ console.log(` ${C.dim}Config file: ${getConfigPath()}${C.reset}\n`);
549
+ }
550
+
551
+ async function configValidate(args, asJson = false) {
552
+ const config = loadConfig();
553
+
554
+ if (!config) {
555
+ const error = { error: 'No configuration file found. Run: aether config init' };
556
+ if (asJson) {
557
+ console.log(JSON.stringify(error, null, 2));
558
+ } else {
559
+ console.log(`\n ${C.red}✗ No configuration found${C.reset}`);
560
+ console.log(` ${C.dim}Run: aether config init${C.reset}\n`);
561
+ }
562
+ return;
563
+ }
564
+
565
+ const results = {
566
+ config_valid: true,
567
+ checks: [],
568
+ timestamp: new Date().toISOString(),
569
+ };
570
+
571
+ if (!asJson) {
572
+ console.log(`\n ${C.bright}${C.cyan}── Configuration Validation ──${C.reset}\n`);
573
+ console.log(` ${C.dim}Testing RPC endpoints and settings...${C.reset}\n`);
574
+ }
575
+
576
+ // Test primary RPC
577
+ if (config.rpc?.url) {
578
+ if (!asJson) console.log(` ${C.dim}Testing primary RPC: ${config.rpc.url}${C.reset}`);
579
+ const rpcTest = await validateRpcUrl(config.rpc.url);
580
+ results.checks.push({
581
+ name: 'Primary RPC',
582
+ key: 'rpc.url',
583
+ passed: rpcTest.valid,
584
+ details: rpcTest.valid
585
+ ? { latency: rpcTest.latency, slot: rpcTest.slot, health: rpcTest.health }
586
+ : { error: rpcTest.error },
587
+ });
588
+ if (!asJson) {
589
+ const icon = rpcTest.valid ? `${C.green}✓` : `${C.red}✗`;
590
+ const status = rpcTest.valid ? `${C.green}Online` : `${C.red}Failed`;
591
+ console.log(` ${icon} Primary RPC: ${status}${C.reset}`);
592
+ if (rpcTest.valid) {
593
+ const latencyColor = rpcTest.latency < 50 ? C.green : rpcTest.latency < 200 ? C.cyan : C.yellow;
594
+ console.log(` ${C.dim}Latency: ${latencyColor}${rpcTest.latency}ms${C.reset}`);
595
+ console.log(` ${C.dim}Slot: ${rpcTest.slot?.toLocaleString()}${C.reset}`);
596
+ } else {
597
+ console.log(` ${C.red}Error: ${rpcTest.error}${C.reset}`);
598
+ results.config_valid = false;
599
+ }
600
+ } else if (!rpcTest.valid) {
601
+ results.config_valid = false;
602
+ }
603
+ }
604
+
605
+ // Test backup RPC if set
606
+ if (config.rpc?.backup) {
607
+ if (!asJson) console.log(`\n ${C.dim}Testing backup RPC: ${config.rpc.backup}${C.reset}`);
608
+ const backupTest = await validateRpcUrl(config.rpc.backup);
609
+ results.checks.push({
610
+ name: 'Backup RPC',
611
+ key: 'rpc.backup',
612
+ passed: backupTest.valid,
613
+ details: backupTest.valid
614
+ ? { latency: backupTest.latency, slot: backupTest.slot }
615
+ : { error: backupTest.error },
616
+ });
617
+ if (!asJson) {
618
+ const icon = backupTest.valid ? `${C.green}✓` : `${C.yellow}⚠`;
619
+ const status = backupTest.valid ? `${C.green}Online` : `${C.yellow}Unreachable`;
620
+ console.log(` ${icon} Backup RPC: ${status}${C.reset}`);
621
+ if (backupTest.valid) {
622
+ const latencyColor = backupTest.latency < 50 ? C.green : backupTest.latency < 200 ? C.cyan : C.yellow;
623
+ console.log(` ${C.dim}Latency: ${latencyColor}${backupTest.latency}ms${C.reset}`);
624
+ }
625
+ }
626
+ }
627
+
628
+ // Validate default wallet
629
+ if (config.wallet?.default) {
630
+ const addrTest = validateAddress(config.wallet.default);
631
+ results.checks.push({
632
+ name: 'Default Wallet',
633
+ key: 'wallet.default',
634
+ passed: addrTest.valid,
635
+ details: addrTest.valid ? { address: addrTest.address } : { error: addrTest.error },
636
+ });
637
+ if (!asJson) {
638
+ const icon = addrTest.valid ? `${C.green}✓` : `${C.yellow}⚠`;
639
+ const status = addrTest.valid ? `${C.green}Valid` : `${C.yellow}Invalid format`;
640
+ console.log(`\n ${icon} Default wallet: ${status}${C.reset}`);
641
+ if (!addrTest.valid) {
642
+ console.log(` ${C.red}Error: ${addrTest.error}${C.reset}`);
643
+ } else {
644
+ console.log(` ${C.dim}Address: ${config.wallet.default}${C.reset}`);
645
+ }
646
+ }
647
+ }
648
+
649
+ // Validate tier
650
+ if (config.validator?.tier) {
651
+ const tierTest = validateTier(config.validator.tier);
652
+ results.checks.push({
653
+ name: 'Validator Tier',
654
+ key: 'validator.tier',
655
+ passed: tierTest.valid,
656
+ details: tierTest.valid ? { tier: tierTest.tier } : { error: tierTest.error },
657
+ });
658
+ if (!asJson) {
659
+ const icon = tierTest.valid ? `${C.green}✓` : `${C.red}✗`;
660
+ console.log(`\n ${icon} Validator tier: ${tierTest.valid ? C.green + tierTest.tier : C.red + 'Invalid'}${C.reset}`);
661
+ }
662
+ }
663
+
664
+ // Summary
665
+ const passed = results.checks.filter(c => c.passed).length;
666
+ const total = results.checks.length;
667
+
668
+ if (asJson) {
669
+ results.summary = { passed, total, success_rate: Math.round((passed / total) * 100) };
670
+ console.log(JSON.stringify(results, null, 2));
671
+ } else {
672
+ console.log(`\n ${C.bright}Validation Summary:${C.reset} ${passed}/${total} checks passed`);
673
+ if (passed === total) {
674
+ console.log(`\n ${C.green}✓ Configuration is valid and RPC is reachable${C.reset}\n`);
675
+ } else if (results.config_valid) {
676
+ console.log(`\n ${C.yellow}⚠ Some optional checks failed, but config is usable${C.reset}\n`);
677
+ } else {
678
+ console.log(`\n ${C.red}✗ Configuration has errors that need to be fixed${C.reset}`);
679
+ console.log(` ${C.dim}Run: aether config set rpc.url <working-url>${C.reset}\n`);
680
+ }
681
+ }
682
+ }
683
+
684
+ async function configExport(args) {
685
+ const fileIdx = args.findIndex(a => a === '--file' || a === '-f');
686
+ const filePath = fileIdx !== -1 && args[fileIdx + 1] ? args[fileIdx + 1] : null;
687
+
688
+ if (!filePath) {
689
+ console.log(`\n ${C.red}✗ --file required${C.reset}`);
690
+ console.log(` ${C.dim}Usage: aether config export --file <path>${C.reset}\n`);
691
+ return;
692
+ }
693
+
694
+ const config = loadConfig() || getDefaultConfig();
695
+ const exportData = {
696
+ ...config,
697
+ exported_at: new Date().toISOString(),
698
+ cli_version: CLI_VERSION,
699
+ };
700
+
701
+ try {
702
+ fs.writeFileSync(filePath, JSON.stringify(exportData, null, 2));
703
+ console.log(`\n ${C.green}✓ Configuration exported${C.reset}\n`);
704
+ console.log(` ${C.dim}File: ${filePath}${C.reset}\n`);
705
+ } catch (err) {
706
+ console.log(`\n ${C.red}✗ Export failed:${C.reset} ${err.message}\n`);
707
+ }
708
+ }
709
+
710
+ async function configImport(args) {
711
+ const fileIdx = args.findIndex(a => a === '--file' || a === '-f');
712
+ const filePath = fileIdx !== -1 && args[fileIdx + 1] ? args[fileIdx + 1] : null;
713
+ const force = args.includes('--force');
714
+
715
+ if (!filePath) {
716
+ console.log(`\n ${C.red}✗ --file required${C.reset}`);
717
+ console.log(` ${C.dim}Usage: aether config import --file <path>${C.reset}\n`);
718
+ return;
719
+ }
720
+
721
+ if (!fs.existsSync(filePath)) {
722
+ console.log(`\n ${C.red}✗ File not found:${C.reset} ${filePath}\n`);
723
+ return;
724
+ }
725
+
726
+ try {
727
+ const imported = JSON.parse(fs.readFileSync(filePath, 'utf8'));
728
+ const configPath = getConfigPath();
729
+
730
+ if (fs.existsSync(configPath) && !force) {
731
+ console.log(`\n ${C.yellow}⚠ Existing config will be overwritten${C.reset}`);
732
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
733
+ const answer = await new Promise(resolve => {
734
+ rl.question(` Continue? [y/N] `, resolve);
735
+ });
736
+ rl.close();
737
+ if (answer.toLowerCase() !== 'y' && answer.toLowerCase() !== 'yes') {
738
+ console.log(`\n ${C.dim}Cancelled.${C.reset}\n`);
739
+ return;
740
+ }
741
+ }
742
+
743
+ const migrated = migrateConfig(imported);
744
+ saveConfig(migrated);
745
+
746
+ console.log(`\n ${C.green}✓ Configuration imported${C.reset}\n`);
747
+ console.log(` ${C.dim}From: ${filePath}${C.reset}`);
748
+ console.log(` ${C.dim}To: ${configPath}${C.reset}\n`);
749
+ } catch (err) {
750
+ console.log(`\n ${C.red}✗ Import failed:${C.reset} ${err.message}\n`);
751
+ }
752
+ }
753
+
754
+ // ---------------------------------------------------------------------------
755
+ // CLI Parser & Dispatcher
756
+ // ---------------------------------------------------------------------------
757
+
758
+ function parseArgs() {
759
+ // argv = [node, index.js, config, <subcmd>, ...]
760
+ return process.argv.slice(3);
761
+ }
762
+
763
+ function showHelp() {
764
+ console.log(`
765
+ ${C.bright}${C.cyan}aether-cli config${C.reset} — Configuration Management
766
+
767
+ ${C.bright}Usage:${C.reset}
768
+ aether config get <key> Get a config value
769
+ aether config set <key> <value> Set a config value (validates RPC URLs)
770
+ aether config list [--compact] Show all configuration
771
+ aether config init [--force] Create default config
772
+ aether config reset --yes Reset to defaults
773
+ aether config validate [--json] Validate config with real RPC calls
774
+ aether config export --file <path> Export config to JSON
775
+ aether config import --file <path> Import config from JSON
776
+
777
+ ${C.bright}Config Keys:${C.reset}
778
+ rpc.url Default RPC endpoint (validates on set)
779
+ rpc.backup Backup RPC endpoint
780
+ rpc.timeout RPC timeout in milliseconds
781
+ wallet.default Default wallet address
782
+ wallet.keypair Path to keypair file
783
+ validator.tier Default tier (full|lite|observer)
784
+ validator.identity Path to validator identity
785
+ output.format Output format (text|json)
786
+ output.colors Enable colors (true|false)
787
+ network.explorer Block explorer URL
788
+ network.faucet Testnet faucet URL
789
+
790
+ ${C.bright}Examples:${C.reset}
791
+ aether config set rpc.url http://localhost:8899
792
+ aether config set wallet.default ATHabc...
793
+ aether config set validator.tier full
794
+ aether config validate
795
+ aether config export --file ~/aether-config-backup.json
796
+
797
+ ${C.dim}SDK Validation:${C.reset}
798
+ Setting rpc.url performs a real HTTP health check via GET /v1/health
799
+ `);
800
+ }
801
+
802
+ async function configCommand() {
803
+ const args = parseArgs();
804
+ const subcmd = args[0];
805
+ const subargs = args.slice(1);
806
+ const asJson = args.includes('--json') || args.includes('-j');
807
+
808
+ switch (subcmd) {
809
+ case 'get':
810
+ await configGet(subargs);
811
+ break;
812
+ case 'set':
813
+ await configSet(subargs);
814
+ break;
815
+ case 'list':
816
+ await configList(subargs, asJson);
817
+ break;
818
+ case 'init':
819
+ await configInit(subargs);
820
+ break;
821
+ case 'reset':
822
+ await configReset(subargs);
823
+ break;
824
+ case 'validate':
825
+ await configValidate(subargs, asJson);
826
+ break;
827
+ case 'export':
828
+ await configExport(subargs);
829
+ break;
830
+ case 'import':
831
+ await configImport(subargs);
832
+ break;
833
+ case '--help':
834
+ case '-h':
835
+ case 'help':
836
+ default:
837
+ showHelp();
838
+ break;
839
+ }
840
+ }
841
+
842
+ // Export for module use
843
+ module.exports = { configCommand };
844
+
845
+ // Run if called directly
846
+ if (require.main === module) {
847
+ configCommand().catch(err => {
848
+ console.error(`\n${C.red}✗ Config command failed:${C.reset}`, err.message, '\n');
849
+ process.exit(1);
850
+ });
851
+ }