@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,495 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * aether-cli info
4
+ *
5
+ * Display a comprehensive snapshot of the local validator's identity,
6
+ * stake status, node version, sync state, and network connectivity.
7
+ *
8
+ * Usage:
9
+ * aether info Full info dump (all sections)
10
+ * aether info --identity Validator identity only
11
+ * aether info --stake Stake & delegation info only
12
+ * aether info --network Network/peer info only
13
+ * aether info --json JSON output for all sections
14
+ * aether info --rpc <url> Use custom RPC endpoint
15
+ *
16
+ * Requires AETHER_RPC env var (default: http://127.0.0.1:8899)
17
+ */
18
+
19
+ const fs = require('fs');
20
+ const path = require('path');
21
+ const os = require('os');
22
+
23
+ // Import SDK for real blockchain RPC calls
24
+ const sdkPath = path.join(__dirname, '..', 'sdk', 'index.js');
25
+ const aether = require(sdkPath);
26
+
27
+ // ANSI colours
28
+ const C = {
29
+ reset: '\x1b[0m',
30
+ bright: '\x1b[1m',
31
+ dim: '\x1b[2m',
32
+ red: '\x1b[31m',
33
+ green: '\x1b[32m',
34
+ yellow: '\x1b[33m',
35
+ cyan: '\x1b[36m',
36
+ magenta: '\x1b[35m',
37
+ };
38
+
39
+ const CLI_VERSION = '1.1.6';
40
+
41
+ // ---------------------------------------------------------------------------
42
+ // Paths
43
+ // ---------------------------------------------------------------------------
44
+
45
+ function getAetherDir() {
46
+ return path.join(os.homedir(), '.aether');
47
+ }
48
+
49
+ function getConfigPath() {
50
+ return path.join(getAetherDir(), 'config.json');
51
+ }
52
+
53
+ function getValidatorIdentityPath() {
54
+ return path.join(getAetherDir(), 'validator-identity.json');
55
+ }
56
+
57
+ function loadConfig() {
58
+ const p = getConfigPath();
59
+ if (!fs.existsSync(p)) return {};
60
+ try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch { return {}; }
61
+ }
62
+
63
+ // ---------------------------------------------------------------------------
64
+ // SDK helpers - Real blockchain RPC calls via Aether SDK
65
+ // ---------------------------------------------------------------------------
66
+
67
+ function getDefaultRpc() {
68
+ return process.env.AETHER_RPC || aether.DEFAULT_RPC_URL || 'http://127.0.0.1:8899';
69
+ }
70
+
71
+ function createClient(rpcUrl) {
72
+ return new aether.AetherClient({ rpcUrl });
73
+ }
74
+
75
+ function formatAether(lamports) {
76
+ if (lamports === undefined || lamports === null) return '—';
77
+ const aeth = Number(lamports) / 1e9;
78
+ if (aeth === 0) return '0 AETH';
79
+ return aeth.toFixed(4).replace(/\.?0+$/, '') + ' AETH';
80
+ }
81
+
82
+ // ---------------------------------------------------------------------------
83
+ // Parse args
84
+ // ---------------------------------------------------------------------------
85
+
86
+ function parseArgs() {
87
+ return process.argv.slice(2);
88
+ }
89
+
90
+ function hasFlag(args, ...flags) {
91
+ return flags.some(f => args.includes(f));
92
+ }
93
+
94
+ function getFlag(args, ...flags) {
95
+ for (const f of flags) {
96
+ const idx = args.indexOf(f);
97
+ if (idx !== -1 && args[idx + 1] && !args[idx + 1].startsWith('-')) return args[idx + 1];
98
+ }
99
+ return null;
100
+ }
101
+
102
+ // ---------------------------------------------------------------------------
103
+ // Section 1: Validator Identity
104
+ // ---------------------------------------------------------------------------
105
+
106
+ async function getIdentity(rpcUrl) {
107
+ const identityPath = getValidatorIdentityPath();
108
+ const identity = fs.existsSync(identityPath)
109
+ ? JSON.parse(fs.readFileSync(identityPath, 'utf8'))
110
+ : null;
111
+
112
+ const cfg = loadConfig();
113
+ const defaultWallet = cfg.defaultWallet || null;
114
+
115
+ let delegatedStake = 0;
116
+ let stakeStatus = 'unknown';
117
+
118
+ if (identity && identity.vote_account) {
119
+ try {
120
+ const client = createClient(rpcUrl);
121
+ const res = await client.getAccountInfo(identity.vote_account);
122
+ if (res && !res.error) {
123
+ delegatedStake = res.lamports || 0;
124
+ stakeStatus = 'active';
125
+ }
126
+ } catch { /* RPC not reachable */ }
127
+ }
128
+
129
+ return {
130
+ identity,
131
+ defaultWallet,
132
+ vote_account: identity?.vote_account || null,
133
+ node_id: identity?.node_id || identity?.identity || null,
134
+ stake_account: identity?.stake_account || null,
135
+ delegated_stake: delegatedStake,
136
+ delegated_stake_formatted: formatAether(delegatedStake),
137
+ stake_status: stakeStatus,
138
+ pid: identity?.pid || null,
139
+ uptime_str: identity?.pid ? `PID ${identity.pid}` : null,
140
+ cli_version: CLI_VERSION,
141
+ };
142
+ }
143
+
144
+ // ---------------------------------------------------------------------------
145
+ // Section 2: Sync & Network Status
146
+ // ---------------------------------------------------------------------------
147
+
148
+ async function getNetworkStatus(rpcUrl) {
149
+ let slot = null, blockHeight = null, epoch = null, slotIndex = null, slotsInEpoch = null;
150
+ let TPS = null, blockTime = null;
151
+ let peers = [];
152
+ let totalPeers = 0;
153
+
154
+ try {
155
+ const client = createClient(rpcUrl);
156
+ const results = await Promise.allSettled([
157
+ client.getSlot(),
158
+ client.getEpochInfo(),
159
+ client.getBlockHeight(),
160
+ client.getClusterPeers(),
161
+ { status: 'rejected', reason: new Error('Perf endpoint not in SDK') },
162
+ ]);
163
+
164
+ const [slotRes, epochRes, blockHeightRes, peersRes] = results;
165
+
166
+ if (slotRes.status === 'fulfilled' && slotRes.value !== null) {
167
+ slot = typeof slotRes.value === 'object' ? slotRes.value.slot : slotRes.value;
168
+ }
169
+ if (epochRes.status === 'fulfilled' && epochRes.value) {
170
+ const ei = epochRes.value;
171
+ epoch = ei.epoch;
172
+ slotIndex = ei.slotIndex ?? ei.slot_index;
173
+ slotsInEpoch = ei.slotsInEpoch ?? ei.slots_in_epoch;
174
+ blockTime = ei.blockTime ?? ei.block_time ?? null;
175
+ }
176
+ if (blockHeightRes.status === 'fulfilled' && blockHeightRes.value !== null) {
177
+ blockHeight = typeof blockHeightRes.value === 'object'
178
+ ? blockHeightRes.value.blockHeight
179
+ : blockHeightRes.value;
180
+ }
181
+ if (peersRes.status === 'fulfilled' && peersRes.value) {
182
+ peers = Array.isArray(peersRes.value) ? peersRes.value : (peersRes.value.peers || []);
183
+ totalPeers = peers.length;
184
+ }
185
+
186
+ // Get TPS from SDK
187
+ try {
188
+ TPS = await client.getTPS();
189
+ } catch { /* TPS not available */ }
190
+ } catch { /* Network info unavailable */ }
191
+
192
+ let syncState = 'unknown';
193
+ if (slot !== null && blockHeight !== null) {
194
+ const lag = slot - blockHeight;
195
+ if (lag <= 1) syncState = 'synced';
196
+ else if (lag <= 10) syncState = 'catching_up';
197
+ else syncState = 'behind';
198
+ } else if (slot !== null) {
199
+ syncState = 'synced';
200
+ }
201
+
202
+ let epochProgress = null;
203
+ if (slotIndex !== null && slotsInEpoch !== null && slotsInEpoch > 0) {
204
+ epochProgress = Math.min(100, Math.round((slotIndex / slotsInEpoch) * 100));
205
+ }
206
+
207
+ return {
208
+ slot,
209
+ block_height: blockHeight,
210
+ sync_state: syncState,
211
+ epoch,
212
+ slot_index: slotIndex,
213
+ slots_in_epoch: slotsInEpoch,
214
+ epoch_progress: epochProgress,
215
+ tps: TPS,
216
+ block_time_ms: blockTime,
217
+ peers,
218
+ total_peers: totalPeers,
219
+ rpc_url: rpcUrl,
220
+ };
221
+ }
222
+
223
+ // ---------------------------------------------------------------------------
224
+ // Section 3: Stake & Delegation Summary
225
+ // ---------------------------------------------------------------------------
226
+
227
+ async function getStakeSummary(rpcUrl) {
228
+ let totalDelegated = BigInt(0);
229
+ let activePositions = 0;
230
+ let deactivatingPositions = 0;
231
+ let positions = [];
232
+ let err = null;
233
+
234
+ try {
235
+ const cfg = loadConfig();
236
+ const walletAddr = cfg.defaultWallet;
237
+
238
+ if (walletAddr) {
239
+ const client = createClient(rpcUrl);
240
+ const rawAddr = walletAddr.startsWith('ATH') ? walletAddr.slice(3) : walletAddr;
241
+ const res = await client.getStakePositions(rawAddr);
242
+ if (res && !res.error) {
243
+ const accounts = Array.isArray(res) ? res : (res.accounts || res.stakes || res.delegations || []);
244
+ for (const acc of accounts) {
245
+ const lamports = BigInt(acc.stake_lamports || acc.lamports || acc.amount || 0);
246
+ const status = (acc.status || acc.state || 'active').toLowerCase();
247
+ const validator = acc.validator || acc.voter || acc.vote_account || 'unknown';
248
+
249
+ if (status === 'active') activePositions++;
250
+ else if (status.includes('deactivating') || status.includes('deactivated')) deactivatingPositions++;
251
+
252
+ totalDelegated += lamports;
253
+ positions.push({
254
+ stake_account: acc.pubkey || acc.publicKey || acc.account || acc.stakeAccount || 'unknown',
255
+ validator,
256
+ lamports: lamports.toString(),
257
+ lamports_formatted: formatAether(lamports.toString()),
258
+ status,
259
+ });
260
+ }
261
+ }
262
+ }
263
+ } catch (e) {
264
+ err = e.message;
265
+ }
266
+
267
+ return {
268
+ total_delegated: totalDelegated.toString(),
269
+ total_delegated_formatted: formatAether(totalDelegated.toString()),
270
+ active_positions: activePositions,
271
+ deactivating_positions: deactivatingPositions,
272
+ total_positions: positions.length,
273
+ positions,
274
+ error: err,
275
+ };
276
+ }
277
+
278
+ // ---------------------------------------------------------------------------
279
+ // Human-readable output
280
+ // ---------------------------------------------------------------------------
281
+
282
+ function printSectionDivider(title) {
283
+ const width = 56;
284
+ const dashes = width - title.length - 4;
285
+ console.log(`\n${C.bright}${C.cyan}── ${title} ${'─'.repeat(Math.max(0, dashes))}${C.reset}`);
286
+ }
287
+
288
+ function printRow(label, value, valueColor) {
289
+ const vc = valueColor || C.reset;
290
+ console.log(` ${C.dim}${label.padEnd(22)}${C.reset} ${vc}${value}${C.reset}`);
291
+ }
292
+
293
+ function printIdentitySection(id) {
294
+ printSectionDivider('Validator Identity');
295
+
296
+ if (!id.identity && !id.defaultWallet) {
297
+ console.log(` ${C.yellow}⚠ Validator identity file not found.${C.reset}`);
298
+ console.log(` ${C.dim} Run: aether init${C.reset}`);
299
+ console.log(` ${C.dim} Or: aether validator start${C.reset}\n`);
300
+ return;
301
+ }
302
+
303
+ if (id.node_id) printRow('Node ID', id.node_id, C.bright);
304
+ if (id.vote_account) printRow('Vote account', id.vote_account, C.bright);
305
+ if (id.stake_account) printRow('Stake account', id.stake_account, C.bright);
306
+ if (id.defaultWallet) printRow('Default wallet', id.defaultWallet, C.bright);
307
+ printRow('Delegated stake', id.delegated_stake_formatted, C.green);
308
+ printRow('Stake status', id.stake_status, id.stake_status === 'active' ? C.green : C.yellow);
309
+ if (id.pid) printRow('PID', id.pid.toString(), C.dim);
310
+ printRow('CLI version', id.cli_version, C.dim);
311
+ console.log();
312
+ }
313
+
314
+ function printNetworkSection(net) {
315
+ printSectionDivider('Network & Sync');
316
+
317
+ const syncColors = { synced: C.green, catching_up: C.yellow, behind: C.red, unknown: C.dim };
318
+ const syncLabels = { synced: '✓ Synced', catching_up: '⚠ Catching up', behind: '✗ Behind', unknown: '— Unknown' };
319
+
320
+ printRow('Sync state', syncLabels[net.sync_state] || net.sync_state, syncColors[net.sync_state] || C.reset);
321
+
322
+ if (net.slot !== null) printRow('Current slot', net.slot.toLocaleString(), C.bright);
323
+ if (net.block_height !== null) printRow('Block height', net.block_height.toLocaleString(), C.bright);
324
+ if (net.slot !== null && net.block_height !== null) {
325
+ const lag = net.slot - net.block_height;
326
+ printRow('Slot lag', lag.toLocaleString(), lag <= 1 ? C.green : lag <= 10 ? C.yellow : C.red);
327
+ }
328
+
329
+ console.log();
330
+
331
+ if (net.epoch !== null) printRow('Epoch', net.epoch.toString(), C.bright);
332
+ if (net.epoch_progress !== null) {
333
+ const filled = Math.floor(net.epoch_progress / 5);
334
+ const bar = '█'.repeat(filled) + '░'.repeat(20 - filled);
335
+ printRow('Epoch progress', `${bar} ${net.epoch_progress}%`, C.cyan);
336
+ }
337
+ if (net.slots_in_epoch !== null) printRow('Slots in epoch', net.slots_in_epoch.toLocaleString(), C.dim);
338
+ if (net.slot_index !== null) {
339
+ printRow('Slot in epoch', `${net.slot_index.toLocaleString()} / ${net.slots_in_epoch?.toLocaleString() || '?'}`, C.dim);
340
+ }
341
+
342
+ console.log();
343
+
344
+ if (net.tps !== null) printRow('TPS (est.)', net.tps.toLocaleString(), C.green);
345
+ if (net.block_time_ms !== null) printRow('Block time', `${(net.block_time_ms / 1000).toFixed(2)}s`, C.dim);
346
+ printRow('RPC endpoint', net.rpc_url, C.dim);
347
+
348
+ console.log();
349
+
350
+ if (net.total_peers > 0) {
351
+ printRow('Connected peers', net.total_peers.toString(), C.green);
352
+ const shown = net.peers.slice(0, 10);
353
+ for (const peer of shown) {
354
+ const pubkey = peer.pubkey || peer.identity || peer.id || 'unknown';
355
+ const ip = peer.ip || peer.remote || '—';
356
+ const version = peer.version || peer.agent || '';
357
+ const shortKey = pubkey.length > 16 ? pubkey.slice(0, 8) + '…' + pubkey.slice(-6) : pubkey;
358
+ console.log(` ${C.dim} ·${C.reset} ${C.cyan}${shortKey}${C.reset} ${C.dim}${ip} ${version}${C.reset}`);
359
+ }
360
+ if (net.peers.length > 10) {
361
+ console.log(` ${C.dim} … and ${net.peers.length - 10} more peers${C.reset}`);
362
+ }
363
+ } else {
364
+ printRow('Connected peers', '0 — not connected', C.yellow);
365
+ console.log(` ${C.dim} Validator may still be starting or network is unavailable.${C.reset}`);
366
+ }
367
+
368
+ console.log();
369
+ }
370
+
371
+ function printStakeSection(stake) {
372
+ printSectionDivider('Stake & Delegations');
373
+
374
+ if (stake.error) {
375
+ console.log(` ${C.yellow}⚠ Could not fetch stake info: ${stake.error}${C.reset}`);
376
+ console.log(` ${C.dim} Set AETHER_RPC to your validator's RPC endpoint.${C.reset}\n`);
377
+ return;
378
+ }
379
+
380
+ printRow('Total delegated', stake.total_delegated_formatted, C.green);
381
+ printRow('Active positions', stake.active_positions.toString(), stake.active_positions > 0 ? C.green : C.dim);
382
+ printRow('Deactivating', stake.deactivating_positions.toString(), stake.deactivating_positions > 0 ? C.yellow : C.dim);
383
+
384
+ console.log();
385
+
386
+ if (stake.positions.length === 0) {
387
+ console.log(` ${C.dim} No stake delegations found.${C.reset}`);
388
+ console.log(` ${C.dim} Delegate: aether stake --validator <addr> --amount <aeth>${C.reset}\n`);
389
+ return;
390
+ }
391
+
392
+ for (const pos of stake.positions) {
393
+ const shortAcct = pos.stake_account.length > 20
394
+ ? pos.stake_account.slice(0, 8) + '…' + pos.stake_account.slice(-8)
395
+ : pos.stake_account;
396
+ const shortVal = pos.validator.length > 20
397
+ ? pos.validator.slice(0, 8) + '…' + pos.validator.slice(-8)
398
+ : pos.validator;
399
+ const statusColor = pos.status === 'active' ? C.green : C.yellow;
400
+
401
+ console.log(` ${C.dim}┌─ ${shortAcct}${C.reset}`);
402
+ console.log(` │ ${C.dim}Validator:${C.reset} ${C.cyan}${shortVal}${C.reset}`);
403
+ console.log(` │ ${C.dim}Amount:${C.reset} ${C.bright}${pos.lamports_formatted}${C.reset}`);
404
+ console.log(` │ ${C.dim}Status:${C.reset} ${statusColor}${pos.status}${C.reset}`);
405
+ console.log(` ${C.dim}└${C.reset}`);
406
+ }
407
+ console.log();
408
+ }
409
+
410
+ // ---------------------------------------------------------------------------
411
+ // JSON output
412
+ // ---------------------------------------------------------------------------
413
+
414
+ function printJson(id, net, stake) {
415
+ console.log(JSON.stringify({
416
+ identity: {
417
+ node_id: id.node_id,
418
+ vote_account: id.vote_account,
419
+ stake_account: id.stake_account,
420
+ default_wallet: id.defaultWallet,
421
+ delegated_stake: id.delegated_stake,
422
+ delegated_stake_formatted: id.delegated_stake_formatted,
423
+ stake_status: id.stake_status,
424
+ pid: id.pid,
425
+ cli_version: id.cli_version,
426
+ },
427
+ network: {
428
+ slot: net.slot,
429
+ block_height: net.block_height,
430
+ sync_state: net.sync_state,
431
+ epoch: net.epoch,
432
+ slot_index: net.slot_index,
433
+ slots_in_epoch: net.slots_in_epoch,
434
+ epoch_progress: net.epoch_progress,
435
+ tps: net.tps,
436
+ block_time_ms: net.block_time_ms,
437
+ total_peers: net.total_peers,
438
+ peers: net.peers.map(p => ({ pubkey: p.pubkey || p.identity || p.id, ip: p.ip })),
439
+ rpc_url: net.rpc_url,
440
+ },
441
+ stake: {
442
+ total_delegated: stake.total_delegated,
443
+ total_delegated_formatted: stake.total_delegated_formatted,
444
+ active_positions: stake.active_positions,
445
+ deactivating_positions: stake.deactivating_positions,
446
+ total_positions: stake.total_positions,
447
+ positions: stake.positions,
448
+ error: stake.error,
449
+ },
450
+ fetched_at: new Date().toISOString(),
451
+ }, null, 2));
452
+ }
453
+
454
+ // ---------------------------------------------------------------------------
455
+ // Main
456
+ // ---------------------------------------------------------------------------
457
+
458
+ async function main() {
459
+ const args = parseArgs();
460
+ const asJson = hasFlag(args, '--json', '-j');
461
+ const showIdentity = hasFlag(args, '--identity', '-i');
462
+ const showNetwork = hasFlag(args, '--network', '-n');
463
+ const showStake = hasFlag(args, '--stake', '-s');
464
+ const rpcUrl = getFlag(args, '--rpc', '-r') || getDefaultRpc();
465
+ const showAll = !showIdentity && !showNetwork && !showStake;
466
+
467
+ const [identityData, networkData, stakeData] = await Promise.all([
468
+ getIdentity(rpcUrl),
469
+ getNetworkStatus(rpcUrl),
470
+ getStakeSummary(rpcUrl),
471
+ ]);
472
+
473
+ if (asJson) {
474
+ printJson(identityData, networkData, stakeData);
475
+ return;
476
+ }
477
+
478
+ console.log(`\n${C.bright}${C.cyan}╔════════════════════════════════════════════════════════════╗${C.reset}`);
479
+ console.log(`${C.bright}${C.cyan}║ AETHER VALIDATOR — Info ║${C.reset}`);
480
+ console.log(`${C.bright}${C.cyan}╚════════════════════════════════════════════════════════════╝${C.reset}`);
481
+ console.log(` ${C.dim}AETHER_RPC: ${rpcUrl}${C.reset}`);
482
+
483
+ if (showAll || showIdentity) printIdentitySection(identityData);
484
+ if (showAll || showNetwork) printNetworkSection(networkData);
485
+ if (showAll || showStake) printStakeSection(stakeData);
486
+
487
+ console.log(` ${C.dim}Run with --json for scripted output.${C.reset}\n`);
488
+ }
489
+
490
+ main().catch(err => {
491
+ console.error(`\n${C.red}✗ Info command failed:${C.reset} ${err.message}\n`);
492
+ process.exit(1);
493
+ });
494
+
495
+ module.exports = { infoCommand: main };