aether-hub 1.1.7 → 1.1.9

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.
@@ -556,7 +556,8 @@ function getFixCommand(check) {
556
556
  }
557
557
  const repoPath = path.join(workspaceRoot, 'Jelly-legs-unsteady-workshop');
558
558
  return `cd "${repoPath}" && cargo build --bin aether-validator --release`;
559
-
559
+ }
560
+
560
561
  case 'Disk':
561
562
  // Can't auto-fix disk space, but can suggest cleanup
562
563
  if (platform === 'win32') {
@@ -0,0 +1,320 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * aether-cli ping
4
+ *
5
+ * Quick RPC health check — measures latency, verifies connectivity,
6
+ * and reports node version and slot info.
7
+ *
8
+ * Usage:
9
+ * aether ping Ping default RPC (AETHER_RPC or localhost:8899)
10
+ * aether ping --rpc <url> Ping a specific RPC endpoint
11
+ * aether ping --count <n> Run <n> pings and show avg/min/max (default: 1, max 20)
12
+ * aether ping --json JSON output for scripting/monitoring
13
+ *
14
+ * Examples:
15
+ * aether ping # Single ping, default RPC
16
+ * aether ping --rpc https://rpc.example.com # Ping specific endpoint
17
+ * aether ping --count 5 --json # 5 pings, JSON output for alerting
18
+ */
19
+
20
+ const http = require('http');
21
+ const https = require('https');
22
+
23
+ // ANSI colours
24
+ const C = {
25
+ reset: '\x1b[0m',
26
+ bright: '\x1b[1m',
27
+ dim: '\x1b[2m',
28
+ red: '\x1b[31m',
29
+ green: '\x1b[32m',
30
+ yellow: '\x1b[33m',
31
+ cyan: '\x1b[36m',
32
+ };
33
+
34
+ const CLI_VERSION = '1.0.6';
35
+
36
+ // ---------------------------------------------------------------------------
37
+ // Helpers
38
+ // ---------------------------------------------------------------------------
39
+
40
+ function getDefaultRpc() {
41
+ return process.env.AETHER_RPC || 'http://127.0.0.1:8899';
42
+ }
43
+
44
+ function httpRequest(rpcUrl, pathStr, timeoutMs = 8000) {
45
+ return new Promise((resolve, reject) => {
46
+ const url = new URL(pathStr, rpcUrl);
47
+ const lib = url.protocol === 'https:' ? https : http;
48
+ const req = lib.request({
49
+ hostname: url.hostname,
50
+ port: url.port || (url.protocol === 'https:' ? 443 : 80),
51
+ path: url.pathname + url.search,
52
+ method: 'GET',
53
+ timeout: timeoutMs,
54
+ headers: { 'Content-Type': 'application/json' },
55
+ }, (res) => {
56
+ let data = '';
57
+ res.on('data', (chunk) => data += chunk);
58
+ res.on('end', () => {
59
+ try { resolve(JSON.parse(data)); }
60
+ catch { resolve({ raw: data }); }
61
+ });
62
+ });
63
+ req.on('error', reject);
64
+ req.on('timeout', () => { req.destroy(); reject(new Error('Request timeout')); });
65
+ req.end();
66
+ });
67
+ }
68
+
69
+ function httpPost(rpcUrl, pathStr, body, timeoutMs = 8000) {
70
+ return new Promise((resolve, reject) => {
71
+ const url = new URL(pathStr, rpcUrl);
72
+ const lib = url.protocol === 'https:' ? https : http;
73
+ const bodyStr = JSON.stringify(body);
74
+ const req = lib.request({
75
+ hostname: url.hostname,
76
+ port: url.port || (url.protocol === 'https:' ? 443 : 80),
77
+ path: url.pathname + url.search,
78
+ method: 'POST',
79
+ timeout: timeoutMs,
80
+ headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(bodyStr) },
81
+ }, (res) => {
82
+ let data = '';
83
+ res.on('data', (chunk) => data += chunk);
84
+ res.on('end', () => {
85
+ try { resolve(JSON.parse(data)); }
86
+ catch { resolve({ raw: data }); }
87
+ });
88
+ });
89
+ req.on('error', reject);
90
+ req.on('timeout', () => { req.destroy(); reject(new Error('Request timeout')); });
91
+ req.write(bodyStr);
92
+ req.end();
93
+ });
94
+ }
95
+
96
+ // ---------------------------------------------------------------------------
97
+ // Single ping: measure latency to /v1/slot
98
+ // ---------------------------------------------------------------------------
99
+
100
+ async function pingOnce(rpcUrl) {
101
+ const start = Date.now();
102
+ let slot = null;
103
+ let error = null;
104
+ let latencyMs = null;
105
+
106
+ try {
107
+ // Use POST to /v1/slot (some nodes only support POST)
108
+ const result = await httpPost(rpcUrl, '/v1/slot', {}, 8000);
109
+ latencyMs = Date.now() - start;
110
+
111
+ if (result && result.error) {
112
+ error = result.error;
113
+ } else {
114
+ slot = result.slot ?? result;
115
+ if (typeof slot === 'object') slot = slot.slot;
116
+ }
117
+ } catch (err) {
118
+ latencyMs = Date.now() - start;
119
+ error = err.message;
120
+ }
121
+
122
+ return { latencyMs, slot, error, rpcUrl };
123
+ }
124
+
125
+ // ---------------------------------------------------------------------------
126
+ // Multi-ping: run N pings and aggregate
127
+ // ---------------------------------------------------------------------------
128
+
129
+ async function pingMulti(rpcUrl, count) {
130
+ const results = [];
131
+ for (let i = 0; i < count; i++) {
132
+ results.push(await pingOnce(rpcUrl));
133
+ if (i < count - 1) await new Promise(r => setTimeout(r, 100));
134
+ }
135
+ return results;
136
+ }
137
+
138
+ // ---------------------------------------------------------------------------
139
+ // Colour helpers
140
+ // ---------------------------------------------------------------------------
141
+
142
+ function latencyColor(ms) {
143
+ if (ms === null) return C.red;
144
+ if (ms < 50) return C.green;
145
+ if (ms < 200) return C.cyan;
146
+ if (ms < 500) return C.yellow;
147
+ return C.red;
148
+ }
149
+
150
+ function latencyLabel(ms) {
151
+ if (ms === null) return '✗ unreachable';
152
+ if (ms < 50) return `● ${ms}ms (excellent)`;
153
+ if (ms < 200) return `● ${ms}ms (good)`;
154
+ if (ms < 500) return `○ ${ms}ms (fair)`;
155
+ return `○ ${ms}ms (slow)`;
156
+ }
157
+
158
+ // ---------------------------------------------------------------------------
159
+ // Output formatters
160
+ // ---------------------------------------------------------------------------
161
+
162
+ function printResult(ping, asJson) {
163
+ const { latencyMs, slot, error, rpcUrl } = ping;
164
+
165
+ if (asJson) {
166
+ console.log(JSON.stringify({
167
+ rpc: rpcUrl,
168
+ online: error === null,
169
+ latency_ms: latencyMs,
170
+ slot,
171
+ error: error || null,
172
+ cli_version: CLI_VERSION,
173
+ timestamp: new Date().toISOString(),
174
+ }));
175
+ return;
176
+ }
177
+
178
+ const lc = latencyColor(latencyMs);
179
+ const bar = latencyMs !== null ? '█'.repeat(Math.min(10, Math.floor(latencyMs / 50))) : '▒';
180
+
181
+ console.log(` ${lc}${bar}${C.reset} ${C.bright}${latencyLabel(latencyMs)}${C.reset}`);
182
+ if (slot !== null) {
183
+ console.log(` ${C.dim} slot: ${C.reset}${C.cyan}${slot.toLocaleString()}${C.reset}`);
184
+ }
185
+ if (error) {
186
+ console.log(` ${C.red} ✗ ${error}${C.reset}`);
187
+ }
188
+ console.log(` ${C.dim} rpc: ${rpcUrl}${C.reset}`);
189
+ }
190
+
191
+ function printAggregated(results, rpcUrl, asJson) {
192
+ const online = results.filter(r => r.error === null);
193
+ const failed = results.filter(r => r.error !== null);
194
+
195
+ if (asJson) {
196
+ const latencies = online.map(r => r.latencyMs).filter(Boolean);
197
+ const avg = latencies.length > 0 ? Math.round(latencies.reduce((a, b) => a + b, 0) / latencies.length) : null;
198
+ const min = latencies.length > 0 ? Math.min(...latencies) : null;
199
+ const max = latencies.length > 0 ? Math.max(...latencies) : null;
200
+
201
+ console.log(JSON.stringify({
202
+ rpc: rpcUrl,
203
+ count: results.length,
204
+ online: online.length,
205
+ failed: failed.length,
206
+ latency_ms: { avg, min, max },
207
+ slots: online.map(r => r.slot).filter(Boolean),
208
+ errors: failed.map(r => r.error),
209
+ cli_version: CLI_VERSION,
210
+ timestamp: new Date().toISOString(),
211
+ }));
212
+ return;
213
+ }
214
+
215
+ console.log(`\n${C.bright}${C.cyan}── Ping Results: ${rpcUrl} ──${C.reset}\n`);
216
+
217
+ const latencies = online.map(r => r.latencyMs).filter(Boolean);
218
+
219
+ if (latencies.length > 0) {
220
+ const avg = Math.round(latencies.reduce((a, b) => a + b, 0) / latencies.length);
221
+ const min = Math.min(...latencies);
222
+ const max = Math.max(...latencies);
223
+
224
+ console.log(` ${C.green}✓${C.reset} ${online.length}/${results.length} successful\n`);
225
+
226
+ // Per-ping bars
227
+ for (let i = 0; i < online.length; i++) {
228
+ const r = online[i];
229
+ const lc = latencyColor(r.latencyMs);
230
+ const bar = '█'.repeat(Math.min(10, Math.floor(r.latencyMs / 50)));
231
+ const slotStr = r.slot !== null ? ` slot=${C.cyan}${r.slot.toLocaleString()}${C.reset}` : '';
232
+ console.log(` ${lc}${bar}${C.reset} ${r.latencyMs}ms${slotStr}`);
233
+ }
234
+
235
+ console.log();
236
+ console.log(` ${C.bright}Latency:${C.reset} avg=${latencyColor(avg)}${avg}ms${C.reset} min=${latencyColor(min)}${min}ms${C.reset} max=${latencyColor(max)}${max}ms${C.reset}`);
237
+ console.log(` ${C.dim} Packets: ${results.length} Lost: ${failed.length}${C.reset}`);
238
+
239
+ // Health assessment
240
+ const healthPct = (online.length / results.length) * 100;
241
+ if (healthPct === 100 && avg < 50) {
242
+ console.log(` ${C.green} Health: excellent${C.reset}`);
243
+ } else if (healthPct >= 80 && avg < 200) {
244
+ console.log(` ${C.cyan} Health: good${C.reset}`);
245
+ } else if (healthPct >= 60) {
246
+ console.log(` ${C.yellow} Health: degraded${C.reset}`);
247
+ } else {
248
+ console.log(` ${C.red} Health: poor${C.reset}`);
249
+ }
250
+ } else {
251
+ console.log(` ${C.red}✗ All pings failed${C.reset}`);
252
+ for (const r of failed) {
253
+ console.log(` ${C.red}✗ ${r.error}${C.reset}`);
254
+ }
255
+ }
256
+
257
+ if (failed.length > 0 && online.length > 0) {
258
+ console.log();
259
+ console.log(` ${C.yellow}⚠ ${failed.length} pings failed:${C.reset}`);
260
+ for (const r of failed) {
261
+ console.log(` ${C.red}✗ ${r.error}${C.reset}`);
262
+ }
263
+ }
264
+ console.log();
265
+ }
266
+
267
+ // ---------------------------------------------------------------------------
268
+ // Main
269
+ // ---------------------------------------------------------------------------
270
+
271
+ async function main() {
272
+ const args = process.argv.slice(3); // [node, index.js, ping, ...]
273
+
274
+ const rpcIndex = args.findIndex(a => a === '--rpc' || a === '-r');
275
+ const rpcUrl = rpcIndex !== -1 && args[rpcIndex + 1] && !args[rpcIndex + 1].startsWith('-')
276
+ ? args[rpcIndex + 1]
277
+ : getDefaultRpc();
278
+
279
+ const countIndex = args.findIndex(a => a === '--count' || a === '-c');
280
+ const countRaw = countIndex !== -1 && args[countIndex + 1] && !args[countIndex + 1].startsWith('-')
281
+ ? parseInt(args[countIndex + 1], 10)
282
+ : 1;
283
+ const count = Math.min(Math.max(1, countRaw || 1), 20);
284
+
285
+ const asJson = args.includes('--json') || args.includes('-j');
286
+
287
+ if (!asJson) {
288
+ console.log(`\n${C.bright}${C.cyan}── Aether RPC Ping ──────────────────────────────────────${C.reset}`);
289
+ if (count > 1) {
290
+ console.log(` ${C.dim}Running ${count} pings against ${rpcUrl}…${C.reset}`);
291
+ } else {
292
+ console.log(` ${C.dim}RPC: ${rpcUrl}${C.reset}`);
293
+ }
294
+ console.log();
295
+ }
296
+
297
+ if (count === 1) {
298
+ const result = await pingOnce(rpcUrl);
299
+ printResult(result, asJson);
300
+ if (!asJson) console.log();
301
+ // Exit 1 if unreachable
302
+ if (result.error) process.exit(1);
303
+ } else {
304
+ const results = await pingMulti(rpcUrl, count);
305
+ printAggregated(results, rpcUrl, asJson);
306
+ // Exit 1 if all failed
307
+ if (results.every(r => r.error)) process.exit(1);
308
+ }
309
+ }
310
+
311
+ main().catch(err => {
312
+ console.error(`\n${C.red}✗ Ping failed:${C.reset} ${err.message}\n`);
313
+ process.exit(1);
314
+ });
315
+
316
+ module.exports = { pingCommand: main };
317
+
318
+ if (require.main === module) {
319
+ main();
320
+ }
@@ -0,0 +1,253 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * aether-cli price
4
+ *
5
+ * Show real-time AETH/USD price from free public crypto APIs.
6
+ * Supports CoinGecko (free tier), and falls back to simulated data
7
+ * if no API key is available.
8
+ *
9
+ * Usage:
10
+ * aether price Show current AETH/USD price
11
+ * aether price --pair AETH/USD Specify trading pair (default: AETH/USD)
12
+ * aether price --json JSON output for scripting
13
+ * aether price --source coingecko Fallback to CoinGecko (no API key needed)
14
+ */
15
+
16
+ const https = require('https');
17
+ const http = require('http');
18
+
19
+ const C = {
20
+ reset: '\x1b[0m',
21
+ bright: '\x1b[1m',
22
+ dim: '\x1b[2m',
23
+ red: '\x1b[31m',
24
+ green: '\x1b[32m',
25
+ yellow: '\x1b[33m',
26
+ cyan: '\x1b[36m',
27
+ magenta: '\x1b[35m',
28
+ };
29
+
30
+ const AETHER_CONTRACT = 'ATH'; // Aether token contract (hypothetical)
31
+ const DEFAULT_PAIR = 'AETH/USD';
32
+
33
+ function httpGet(url) {
34
+ return new Promise((resolve, reject) => {
35
+ const lib = url.startsWith('https') ? https : http;
36
+ const parsed = new URL(url);
37
+ const req = lib.request({
38
+ hostname: parsed.hostname,
39
+ port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80),
40
+ path: parsed.pathname + parsed.search,
41
+ method: 'GET',
42
+ timeout: 10000,
43
+ headers: { 'Accept': 'application/json', 'User-Agent': 'Aether-CLI/1.0' },
44
+ }, (res) => {
45
+ let data = '';
46
+ res.on('data', (chunk) => data += chunk);
47
+ res.on('end', () => {
48
+ try { resolve(JSON.parse(data)); }
49
+ catch { resolve({ _raw: data }); }
50
+ });
51
+ });
52
+ req.on('error', reject);
53
+ req.on('timeout', () => { req.destroy(); reject(new Error('Request timeout')); });
54
+ req.end();
55
+ });
56
+ }
57
+
58
+ function formatPrice(num, decimals = 4) {
59
+ if (num === null || num === undefined || isNaN(num)) return '—';
60
+ return num.toFixed(decimals);
61
+ }
62
+
63
+ function formatTime(date) {
64
+ return date.toISOString().replace('T', ' ').substring(0, 19) + ' UTC';
65
+ }
66
+
67
+ /**
68
+ * Fetch AETH price from CoinGecko (free, no API key).
69
+ * Coins API: /coins/list → find ATH/AETH → /coins/{id}/market_chart
70
+ * Fallback: try known Aether token addresses on major DEXes via /simple/price
71
+ */
72
+ async function fetchFromCoinGecko() {
73
+ try {
74
+ // Try CoinGecko simple price for AETH token
75
+ // We'll try a few known Aether token addresses on Ethereum mainnet as a proxy
76
+ const url = 'https://api.coingecko.com/api/v3/simple/price?vs_currencies=usd&ids=aether,ath,ath-token,aether-network';
77
+ const data = await httpGet(url);
78
+
79
+ if (data && !data.error) {
80
+ // Find first available AETH quote
81
+ const pairs = [
82
+ { key: 'aether-network', name: 'AETH' },
83
+ { key: 'aether', name: 'AETH' },
84
+ { key: 'ath-token', name: 'ATH' },
85
+ { key: 'ath', name: 'ATH' },
86
+ ];
87
+
88
+ for (const { key, name } of pairs) {
89
+ if (data[key] && data[key].usd !== undefined) {
90
+ return {
91
+ source: 'CoinGecko',
92
+ symbol: name,
93
+ price: data[key].usd,
94
+ currency: 'USD',
95
+ timestamp: new Date(),
96
+ };
97
+ }
98
+ }
99
+ }
100
+
101
+ return null;
102
+ } catch {
103
+ return null;
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Fetch price data from a public DEX aggregator or mock Aether RPC.
109
+ * Primary: use the Aether chain's own price oracle if available.
110
+ * Fallback: use CoinGecko.
111
+ */
112
+ async function fetchAetherPrice() {
113
+ // Try Aether chain's built-in price oracle (if validator is running)
114
+ const rpcUrl = process.env.AETHER_RPC || 'http://127.0.0.1:8899';
115
+ try {
116
+ const res = await httpGet(`${rpcUrl}/v1/price`);
117
+ if (res && !res.error && res.price !== undefined) {
118
+ return {
119
+ source: 'Aether Oracle',
120
+ symbol: 'AETH',
121
+ price: parseFloat(res.price),
122
+ currency: res.currency || 'USD',
123
+ timestamp: new Date(),
124
+ change_24h: res.change_24h || null,
125
+ volume_24h: res.volume_24h || null,
126
+ };
127
+ }
128
+ } catch { /* chain oracle not available */ }
129
+
130
+ // Try CoinGecko
131
+ const cg = await fetchFromCoinGecko();
132
+ if (cg) return cg;
133
+
134
+ return null;
135
+ }
136
+
137
+ /**
138
+ * Fetch 24h price change using CoinGecko market chart.
139
+ */
140
+ async function fetchPriceChange24h(symbol) {
141
+ try {
142
+ const idMap = {
143
+ 'AETH': 'aether-network',
144
+ 'ATH': 'ath-token',
145
+ };
146
+ const id = idMap[symbol] || 'aether-network';
147
+ const url = `https://api.coingecko.com/api/v3/coins/${id}/market_chart?vs_currency=usd&days=1`;
148
+ const data = await httpGet(url);
149
+
150
+ if (data && data.prices && data.prices.length >= 2) {
151
+ const latest = data.prices[data.prices.length - 1][1];
152
+ const yesterday = data.prices[0][1];
153
+ const change = ((latest - yesterday) / yesterday) * 100;
154
+ const volume = data.total_volumes ? data.total_volumes[data.total_volumes.length - 1][1] : null;
155
+ return {
156
+ change_24h: change,
157
+ volume_24h: volume,
158
+ };
159
+ }
160
+ } catch { /* ignore */ }
161
+ return { change_24h: null, volume_24h: null };
162
+ }
163
+
164
+ async function priceCommand() {
165
+ const args = process.argv.slice(2);
166
+ const asJson = args.includes('--json') || args.includes('-j');
167
+ const pair = args.includes('--pair')
168
+ ? args[args.indexOf('--pair') + 1] || DEFAULT_PAIR
169
+ : DEFAULT_PAIR;
170
+ const source = args.includes('--source') ? args[args.indexOf('--source') + 1] : null;
171
+
172
+ // Parse pair
173
+ const [fromSymbol, toSymbol = 'USD'] = pair.split('/');
174
+ const symbol = fromSymbol.toUpperCase();
175
+
176
+ console.log(`\n${C.bright}${C.cyan}── Aether Price ───────────────────────────────────────${C.reset}\n`);
177
+
178
+ let priceData;
179
+ if (source === 'coingecko') {
180
+ priceData = await fetchFromCoinGecko();
181
+ } else {
182
+ priceData = await fetchAetherPrice();
183
+ }
184
+
185
+ if (!priceData) {
186
+ if (asJson) {
187
+ console.log(JSON.stringify({ error: 'Price data unavailable', symbol, pair }, null, 2));
188
+ } else {
189
+ console.log(` ${C.yellow}⚠ Price data temporarily unavailable.${C.reset}`);
190
+ console.log(` ${C.dim}Make sure your validator is running or check network connectivity.${C.reset}`);
191
+ console.log(` ${C.dim}Set AETHER_RPC env var to your validator's RPC address.${C.reset}`);
192
+ console.log(` ${C.dim}Fallback: aether price --source coingecko${C.reset}\n`);
193
+ }
194
+ return;
195
+ }
196
+
197
+ // Fetch 24h change if available
198
+ const changeData = await fetchPriceChange24h(symbol);
199
+ const priceInfo = { ...priceData, ...changeData };
200
+
201
+ if (asJson) {
202
+ console.log(JSON.stringify({
203
+ symbol: priceInfo.symbol,
204
+ pair: `${symbol}/USD`,
205
+ price_usd: priceInfo.price,
206
+ change_24h_pct: priceInfo.change_24h !== null ? parseFloat(priceInfo.change_24h.toFixed(4)) : null,
207
+ volume_24h_usd: priceInfo.volume_24h,
208
+ source: priceInfo.source,
209
+ timestamp: formatTime(priceInfo.timestamp),
210
+ }, null, 2));
211
+ return;
212
+ }
213
+
214
+ // Human-readable output
215
+ const change = priceInfo.change_24h;
216
+ const changeColor = change === null ? C.dim : change >= 0 ? C.green : C.red;
217
+ const changeStr = change !== null
218
+ ? `${change >= 0 ? '+' : ''}${change.toFixed(2)}%`
219
+ : '—';
220
+
221
+ const arrow = change === null ? ' ' : change >= 0 ? '▲' : '▼';
222
+ const volumeStr = priceInfo.volume_24h !== null
223
+ ? `$${(priceInfo.volume_24h / 1e6).toFixed(2)}M`
224
+ : null;
225
+
226
+ console.log(` ${C.dim}Pair:${C.reset} ${C.bright}${symbol}/USD${C.reset}`);
227
+ console.log(` ${C.dim}Source:${C.reset} ${C.bright}${priceInfo.source}${C.reset}`);
228
+ console.log(` ${C.dim}Updated:${C.reset} ${formatTime(priceInfo.timestamp)}`);
229
+ console.log();
230
+ console.log(` ${C.bright}${C.green}$${formatPrice(priceInfo.price)}${C.reset} ${C.dim}USD${C.reset}`);
231
+ console.log(` ${C.dim}24h change: ${changeColor}${arrow} ${changeStr}${C.reset}`);
232
+ if (volumeStr) {
233
+ console.log(` ${C.dim}24h volume: ${volumeStr}${C.reset}`);
234
+ }
235
+ console.log();
236
+
237
+ // ASCII box
238
+ const barLen = 40;
239
+ const fillLen = change !== null ? Math.min(barLen, Math.round(Math.abs(change) / 2)) : 0;
240
+ const barColor = change !== null && change < 0 ? C.red : C.green;
241
+ const bar = barColor + '█'.repeat(fillLen) + C.dim + '░'.repeat(barLen - fillLen) + C.reset;
242
+ console.log(` ${C.dim}[${bar}]${C.reset}`);
243
+ console.log();
244
+ console.log(` ${C.dim}Run with ${C.cyan}--json${C.reset}${C.dim} for scripted integrations.${C.reset}`);
245
+ console.log(` ${C.dim}Refreshes on each call — set up a cron job for live monitoring.${C.reset}\n`);
246
+ }
247
+
248
+ priceCommand().catch(err => {
249
+ console.error(`\n${C.red}Price error:${C.reset}`, err.message, '\n');
250
+ process.exit(1);
251
+ });
252
+
253
+ module.exports = { priceCommand };