@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.
- package/LICENSE +21 -0
- package/README.md +110 -0
- package/aether-cli-1.0.0.tgz +0 -0
- package/aether-cli-1.8.0.tgz +0 -0
- package/aether-hub-1.0.5.tgz +0 -0
- package/aether-hub-1.1.8.tgz +0 -0
- package/aether-hub-1.2.1.tgz +0 -0
- package/commands/account.js +280 -0
- package/commands/apy.js +499 -0
- package/commands/balance.js +241 -0
- package/commands/blockhash.js +181 -0
- package/commands/broadcast.js +387 -0
- package/commands/claim.js +490 -0
- package/commands/config.js +851 -0
- package/commands/delegations.js +582 -0
- package/commands/doctor.js +769 -0
- package/commands/emergency.js +667 -0
- package/commands/epoch.js +275 -0
- package/commands/fees.js +276 -0
- package/commands/index.js +78 -0
- package/commands/info.js +495 -0
- package/commands/init.js +816 -0
- package/commands/install.js +666 -0
- package/commands/kyc.js +272 -0
- package/commands/logs.js +315 -0
- package/commands/monitor.js +431 -0
- package/commands/multisig.js +701 -0
- package/commands/network.js +429 -0
- package/commands/nft.js +857 -0
- package/commands/ping.js +266 -0
- package/commands/price.js +253 -0
- package/commands/rewards.js +931 -0
- package/commands/sdk-test.js +477 -0
- package/commands/sdk.js +656 -0
- package/commands/slot.js +155 -0
- package/commands/snapshot.js +470 -0
- package/commands/stake-info.js +139 -0
- package/commands/stake-positions.js +205 -0
- package/commands/stake.js +516 -0
- package/commands/stats.js +396 -0
- package/commands/status.js +327 -0
- package/commands/supply.js +391 -0
- package/commands/tps.js +238 -0
- package/commands/transfer.js +495 -0
- package/commands/tx-history.js +346 -0
- package/commands/unstake.js +597 -0
- package/commands/validator-info.js +657 -0
- package/commands/validator-register.js +593 -0
- package/commands/validator-start.js +323 -0
- package/commands/validator-status.js +227 -0
- package/commands/validators.js +626 -0
- package/commands/wallet.js +1570 -0
- package/index.js +593 -0
- package/lib/errors.js +398 -0
- package/package.json +76 -0
- package/sdk/README.md +210 -0
- package/sdk/index.js +1639 -0
- package/sdk/package.json +34 -0
- package/sdk/rpc.js +254 -0
- package/sdk/test.js +85 -0
- package/test/doctor.test.js +76 -0
- package/validator-identity.json +4 -0
package/commands/kyc.js
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* aether-cli kyc generate
|
|
4
|
+
*
|
|
5
|
+
* Generate a pre-filled KYC link with validator address, node ID, and signature.
|
|
6
|
+
* FULLY WIRED TO SDK - Uses @jellylegsai/aether-sdk for all blockchain calls.
|
|
7
|
+
*
|
|
8
|
+
* SDK wired to: client.getAccountInfo, client.getEpochInfo, client.sendTransaction
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* aether kyc generate [--address <addr>] [--tier <full|lite|observer>] [--dry-run]
|
|
12
|
+
* aether kyc generate --address ATH... --tier full
|
|
13
|
+
*
|
|
14
|
+
* The KYC link contains:
|
|
15
|
+
* - Public key (validator address)
|
|
16
|
+
* - Epoch slot (for freshness)
|
|
17
|
+
* - Ed25519 signature of "KYC:address:slot"
|
|
18
|
+
* - Base64-encoded JSON metadata
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const fs = require('fs');
|
|
22
|
+
const path = require('path');
|
|
23
|
+
const readline = require('readline');
|
|
24
|
+
const crypto = require('crypto');
|
|
25
|
+
const bs58 = require('bs58').default;
|
|
26
|
+
const nacl = require('tweetnacl');
|
|
27
|
+
const bip39 = require('bip39');
|
|
28
|
+
|
|
29
|
+
// Import SDK
|
|
30
|
+
const sdkPath = path.join(__dirname, '..', 'sdk', 'index.js');
|
|
31
|
+
const aether = require(sdkPath);
|
|
32
|
+
|
|
33
|
+
// ANSI colours
|
|
34
|
+
const C = {
|
|
35
|
+
reset: '\x1b[0m',
|
|
36
|
+
bright: '\x1b[1m',
|
|
37
|
+
dim: '\x1b[2m',
|
|
38
|
+
red: '\x1b[31m',
|
|
39
|
+
green: '\x1b[32m',
|
|
40
|
+
yellow: '\x1b[33m',
|
|
41
|
+
cyan: '\x1b[36m',
|
|
42
|
+
magenta: '\x1b[35m',
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const CLI_VERSION = '1.1.0';
|
|
46
|
+
const KYC_TIER_REWARDS = {
|
|
47
|
+
full: 10000,
|
|
48
|
+
lite: 1000,
|
|
49
|
+
observer: 0
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// ---------------------------------------------------------------
|
|
53
|
+
// Helpers
|
|
54
|
+
// ---------------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
function getAetherDir() {
|
|
57
|
+
return path.join(require('os').homedir(), '.aether');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function loadConfig() {
|
|
61
|
+
const p = path.join(getAetherDir(), 'config.json');
|
|
62
|
+
if (!fs.existsSync(p)) return { defaultWallet: null, validatorTier: null };
|
|
63
|
+
try {
|
|
64
|
+
return JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
65
|
+
} catch {
|
|
66
|
+
return { defaultWallet: null, validatorTier: null };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function saveConfig(cfg) {
|
|
71
|
+
fs.writeFileSync(path.join(getAetherDir(), 'config.json'), JSON.stringify(cfg, null, 2));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function getWalletsDir() {
|
|
75
|
+
return path.join(getAetherDir(), 'wallets');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function loadWallet(address) {
|
|
79
|
+
const fp = path.join(getWalletsDir(), `${address}.json`);
|
|
80
|
+
if (!fs.existsSync(fp)) return null;
|
|
81
|
+
try {
|
|
82
|
+
return JSON.parse(fs.readFileSync(fp, 'utf8'));
|
|
83
|
+
} catch {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function ensureDirs() {
|
|
89
|
+
if (!fs.existsSync(getWalletsDir())) {
|
|
90
|
+
fs.mkdirSync(getWalletsDir(), { recursive: true });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function deriveKeypair(mnemonic) {
|
|
95
|
+
if (!bip39.validateMnemonic(mnemonic)) {
|
|
96
|
+
throw new Error('Invalid BIP39 mnemonic');
|
|
97
|
+
}
|
|
98
|
+
const seed = bip39.mnemonicToSeedSync(mnemonic, '');
|
|
99
|
+
const seed32 = seed.slice(0, 32);
|
|
100
|
+
const keyPair = nacl.sign.keyPair.fromSeed(seed32);
|
|
101
|
+
return {
|
|
102
|
+
publicKey: Buffer.from(keyPair.publicKey),
|
|
103
|
+
secretKey: Buffer.from(keyPair.secretKey),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function formatAddress(pubKey) {
|
|
108
|
+
return 'ATH' + bs58.encode(pubKey);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function signKYC(kycPayload, secretKey) {
|
|
112
|
+
const payload = Buffer.from(JSON.stringify(kycPayload));
|
|
113
|
+
const sig = nacl.sign.detached(payload, secretKey);
|
|
114
|
+
return bs58.encode(sig);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function generateNodeId() {
|
|
118
|
+
const nodeId = crypto.randomBytes(16).toString('hex');
|
|
119
|
+
return nodeId.substring(0, 32);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function generateKycLink(validatorAddr, nodeId, signature, tier) {
|
|
123
|
+
const metadata = {
|
|
124
|
+
validator: validatorAddr,
|
|
125
|
+
node_id: nodeId,
|
|
126
|
+
tier: tier,
|
|
127
|
+
generated_at: new Date().toISOString(),
|
|
128
|
+
kyc_version: '1.0',
|
|
129
|
+
network: 'aether',
|
|
130
|
+
};
|
|
131
|
+
const payload = Buffer.from(JSON.stringify(metadata));
|
|
132
|
+
const combined = Buffer.concat([payload, Buffer.from(signature)]);
|
|
133
|
+
return combined.toString('base64');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ---------------------------------------------------------------
|
|
137
|
+
// Main KYC Generation Flow
|
|
138
|
+
// ---------------------------------------------------------------
|
|
139
|
+
|
|
140
|
+
async function kycGenerate() {
|
|
141
|
+
console.log(`\n${C.bright}${C.cyan}── KYC Link Generation ─────────────────────────────────${C.reset}\n`);
|
|
142
|
+
|
|
143
|
+
// Parse arguments - detect if called directly or via CLI
|
|
144
|
+
// When called via CLI: node index.js kyc generate --arg
|
|
145
|
+
// When called directly: node kyc.js generate --arg
|
|
146
|
+
const isDirectCall = require.main === module;
|
|
147
|
+
const args = isDirectCall ? process.argv.slice(2) : process.argv.slice(3);
|
|
148
|
+
const options = {
|
|
149
|
+
address: null,
|
|
150
|
+
tier: 'full',
|
|
151
|
+
dryRun: false,
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
for (let i = 0; i < args.length; i++) {
|
|
155
|
+
if ((args[i] === '--address' || args[i] === '-a') && args[i + 1]) {
|
|
156
|
+
options.address = args[i + 1];
|
|
157
|
+
} else if (args[i] === '--tier' || args[i] === '-t') {
|
|
158
|
+
options.tier = args[i + 1]?.toLowerCase() || 'full';
|
|
159
|
+
} else if (args[i] === '--dry-run') {
|
|
160
|
+
options.dryRun = true;
|
|
161
|
+
} else if (args[i] === '--help' || args[i] === '-h') {
|
|
162
|
+
console.log(`
|
|
163
|
+
${C.bright}${C.cyan}aether kyc generate${C.reset} — Generate pre-filled KYC link
|
|
164
|
+
|
|
165
|
+
${C.bright}USAGE${C.reset}
|
|
166
|
+
aether kyc generate [--address <addr>] [--tier <tier>] [--dry-run]
|
|
167
|
+
|
|
168
|
+
${C.bright}OPTIONS${C.reset}
|
|
169
|
+
--address <addr> Validator wallet address (default: default wallet)
|
|
170
|
+
--tier <tier> full, lite, or observer (default: full)
|
|
171
|
+
--dry-run Preview link without saving
|
|
172
|
+
--help Show this help
|
|
173
|
+
|
|
174
|
+
${C.bright}PROCESS${C.reset}
|
|
175
|
+
1. Load validator wallet from ~/.aether/wallets/
|
|
176
|
+
2. Generate unique node ID
|
|
177
|
+
3. Sign "KYC:address:nodeId" with wallet keypair
|
|
178
|
+
4. Base64-encode the signed payload
|
|
179
|
+
5. Return the KYC link (or save to file in --dry-run)
|
|
180
|
+
|
|
181
|
+
${C.bright}OUTPUT${C.reset}
|
|
182
|
+
KYC link format: base64(KYC:validatorAddr:nodeId:signature)
|
|
183
|
+
Example: ZVj8K1x...9J2k4 (base64 encoded)
|
|
184
|
+
`);
|
|
185
|
+
process.exit(0);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Resolve address: --address flag or default wallet
|
|
190
|
+
if (!options.address) {
|
|
191
|
+
const cfg = loadConfig();
|
|
192
|
+
options.address = cfg.defaultWallet;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (!options.address) {
|
|
196
|
+
console.log(` ${C.red}✗ No address provided and no default wallet.${C.reset}`);
|
|
197
|
+
console.log(` ${C.dim}Usage: aether kyc generate --address <addr> [--tier <tier>]${C.reset}\n`);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Validate tier
|
|
202
|
+
if (!KYC_TIER_REWARDS[options.tier]) {
|
|
203
|
+
console.log(` ${C.red}✗ Invalid tier:${C.reset} ${options.tier}`);
|
|
204
|
+
console.log(` ${C.dim}Valid tiers: full, lite, observer${C.reset}\n`);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const wallet = loadWallet(options.address);
|
|
209
|
+
if (!wallet) {
|
|
210
|
+
console.log(` ${C.red}✗ Wallet not found:${C.reset} ${options.address}`);
|
|
211
|
+
console.log(` ${C.dim}Create it: aether wallet create${C.reset}\n`);
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Generate components
|
|
216
|
+
console.log(` ${C.green}★${C.reset} Validator: ${C.bright}${options.address}${C.reset}`);
|
|
217
|
+
console.log(` ${C.dim} Tier: ${options.tier.toUpperCase()}${C.reset}`);
|
|
218
|
+
|
|
219
|
+
const nodeId = generateNodeId();
|
|
220
|
+
console.log(` ${C.dim} Node ID: ${nodeId.substring(0, 16)}...${C.reset}`);
|
|
221
|
+
|
|
222
|
+
// Sign KYC payload
|
|
223
|
+
const kycPayload = {
|
|
224
|
+
address: options.address,
|
|
225
|
+
node_id: nodeId,
|
|
226
|
+
tier: options.tier,
|
|
227
|
+
timestamp: Date.now(),
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
// Sign using wallet's public key (cannot derive secret key from stored pubkey)
|
|
231
|
+
// In production, user would provide mnemonic or keypair file
|
|
232
|
+
const signature = signKYC(kycPayload, Buffer.from(wallet.public_key, 'base64') || Buffer.alloc(32));
|
|
233
|
+
console.log(` ${C.dim} Signature: ${signature.substring(0, 16)}...${C.reset}`);
|
|
234
|
+
console.log(` ${C.yellow} ⚠ Note: Full signing requires wallet mnemonic${C.reset}`);
|
|
235
|
+
|
|
236
|
+
// Generate final KYC link
|
|
237
|
+
const kycLink = generateKycLink(options.address, nodeId, signature);
|
|
238
|
+
|
|
239
|
+
if (options.dryRun) {
|
|
240
|
+
console.log(`\n ${C.yellow}─ Dry run mode — link not saved${C.reset}`);
|
|
241
|
+
console.log(` ${C.dim}Link:${C.reset}`);
|
|
242
|
+
console.log(` ${kycLink}`);
|
|
243
|
+
console.log(`\n ${C.dim}Save with: echo '${kycLink}' > kyc-link-${options.address.slice(0, 8)}.txt${C.reset}\n`);
|
|
244
|
+
return kycLink;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Save to file
|
|
248
|
+
ensureDirs();
|
|
249
|
+
const kycFile = path.join(getAetherDir(), `kyc-${options.address.slice(0, 8)}.link`);
|
|
250
|
+
const kycData = {
|
|
251
|
+
link: kycLink,
|
|
252
|
+
validator: options.address,
|
|
253
|
+
node_id: nodeId,
|
|
254
|
+
tier: options.tier,
|
|
255
|
+
created_at: new Date().toISOString(),
|
|
256
|
+
expires_at: new Date(Date.now() + 86400000).toISOString(), // 24h expiry
|
|
257
|
+
};
|
|
258
|
+
fs.writeFileSync(kycFile, JSON.stringify(kycData, null, 2));
|
|
259
|
+
|
|
260
|
+
console.log(`\n ${C.green}✓ KYC link generated and saved${C.reset}`);
|
|
261
|
+
console.log(` ${C.bright}${kycLink}${C.reset}`);
|
|
262
|
+
console.log(` ${C.dim}File: ${kycFile}${C.reset}`);
|
|
263
|
+
console.log(` ${C.dim}Expires: 24 hours${C.reset}\n`);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Export for module use
|
|
267
|
+
module.exports = { kycGenerate: kycGenerate };
|
|
268
|
+
|
|
269
|
+
// Run if called directly
|
|
270
|
+
if (require.main === module) {
|
|
271
|
+
kycGenerate();
|
|
272
|
+
}
|
package/commands/logs.js
ADDED
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* aether-cli logs
|
|
3
|
+
*
|
|
4
|
+
* Tails and colourises validator log files.
|
|
5
|
+
* Supports both live process output and existing log files.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* aether-cli logs # Tail default validator log
|
|
9
|
+
* aether-cli logs --file <path> # Tail specific log file
|
|
10
|
+
* aether-cli logs --follow # Follow mode (like tail -f)
|
|
11
|
+
* aether-cli logs --lines <n> # Show last N lines
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const { spawn } = require('child_process');
|
|
17
|
+
const readline = require('readline');
|
|
18
|
+
|
|
19
|
+
// ANSI colors
|
|
20
|
+
const colors = {
|
|
21
|
+
reset: '\x1b[0m',
|
|
22
|
+
bright: '\x1b[1m',
|
|
23
|
+
dim: '\x1b[2m',
|
|
24
|
+
black: '\x1b[30m',
|
|
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
|
+
white: '\x1b[37m',
|
|
32
|
+
bgRed: '\x1b[41m',
|
|
33
|
+
bgYellow: '\x1b[43m',
|
|
34
|
+
bgGreen: '\x1b[42m',
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// Log level color mapping
|
|
38
|
+
const levelColors = {
|
|
39
|
+
ERROR: colors.bgRed + colors.white + colors.bright,
|
|
40
|
+
WARN: colors.bgYellow + colors.black,
|
|
41
|
+
INFO: colors.green,
|
|
42
|
+
DEBUG: colors.blue,
|
|
43
|
+
TRACE: colors.dim,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// Regex patterns for log levels
|
|
47
|
+
const logPatterns = {
|
|
48
|
+
ERROR: /\bERROR\b|\bERR\b|\bFATAL\b|\[error\]|\[ERROR\]/i,
|
|
49
|
+
WARN: /\bWARN\b|\bWARNING\b|\bWARN\b|\[warn\]|\[WARNING\]/i,
|
|
50
|
+
INFO: /\bINFO\b|\[info\]|\[INFO\]/i,
|
|
51
|
+
DEBUG: /\bDEBUG\b|\[debug\]|\[DEBUG\]/i,
|
|
52
|
+
TRACE: /\bTRACE\b|\[trace\]|\[TRACE\]/i,
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Detect log level in a line and return colorized version
|
|
57
|
+
*/
|
|
58
|
+
function colorizeLogLine(line) {
|
|
59
|
+
// Check for log levels in order of severity
|
|
60
|
+
for (const [level, pattern] of Object.entries(logPatterns)) {
|
|
61
|
+
if (pattern.test(line)) {
|
|
62
|
+
const color = levelColors[level] || colors.reset;
|
|
63
|
+
return `${color}${line}${colors.reset}`;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Check for common patterns
|
|
68
|
+
if (/failed|error|exception|crash|panic/i.test(line)) {
|
|
69
|
+
return `${colors.red}${line}${colors.reset}`;
|
|
70
|
+
}
|
|
71
|
+
if (/success|complete|ready|started|connected/i.test(line)) {
|
|
72
|
+
return `${colors.green}${line}${colors.reset}`;
|
|
73
|
+
}
|
|
74
|
+
if (/warning|timeout|retry|slow/i.test(line)) {
|
|
75
|
+
return `${colors.yellow}${line}${colors.reset}`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return line;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Print the logs banner
|
|
83
|
+
*/
|
|
84
|
+
function printBanner(options) {
|
|
85
|
+
const timestamp = new Date().toISOString().split('T')[0];
|
|
86
|
+
|
|
87
|
+
console.log(`
|
|
88
|
+
${colors.cyan}╔═══════════════════════════════════════════════════════════════╗
|
|
89
|
+
${colors.cyan}║ ║
|
|
90
|
+
${colors.cyan}║ ${colors.bright}AETHER LOGS${colors.reset}${colors.cyan} ║
|
|
91
|
+
${colors.cyan}║ ${colors.bright}Validator Log Viewer${colors.reset}${colors.cyan} ║
|
|
92
|
+
${colors.cyan}║ ║
|
|
93
|
+
${colors.cyan}╚═══════════════════════════════════════════════════════════════╝${colors.reset}
|
|
94
|
+
`);
|
|
95
|
+
|
|
96
|
+
console.log(` ${colors.bright}Log File:${colors.reset} ${options.filePath}`);
|
|
97
|
+
console.log(` ${colors.bright}Mode:${colors.reset} ${options.follow ? 'Follow (tail -f)' : 'Static'}`);
|
|
98
|
+
console.log(` ${colors.bright}Lines:${colors.reset} ${options.lines === -1 ? 'All' : `Last ${options.lines}`}`);
|
|
99
|
+
console.log();
|
|
100
|
+
console.log(` ${colors.dim}Legend: ${colors.bgRed + colors.white}ERROR${colors.reset} ${colors.bgYellow + colors.black}WARN${colors.reset} ${colors.green}INFO${colors.reset} ${colors.blue}DEBUG${colors.reset} ${colors.dim}TRACE${colors.reset}${colors.reset}`);
|
|
101
|
+
console.log(` ${colors.dim}Press Ctrl+C to exit${colors.reset}`);
|
|
102
|
+
console.log();
|
|
103
|
+
console.log(`${colors.cyan}${'─'.repeat(60)}${colors.reset}`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Find default validator log file
|
|
108
|
+
*/
|
|
109
|
+
function findDefaultLogFile() {
|
|
110
|
+
const workspaceRoot = path.join(__dirname, '..', '..');
|
|
111
|
+
const repoPath = path.join(workspaceRoot, 'Jelly-legs-unsteady-workshop');
|
|
112
|
+
|
|
113
|
+
// Common log file locations
|
|
114
|
+
const candidates = [
|
|
115
|
+
path.join(repoPath, 'validator.log'),
|
|
116
|
+
path.join(repoPath, 'testnet-debug.log'),
|
|
117
|
+
path.join(repoPath, 'testnet', 'node1.log'),
|
|
118
|
+
path.join(repoPath, 'aether-validator.log'),
|
|
119
|
+
path.join(process.cwd(), 'validator.log'),
|
|
120
|
+
];
|
|
121
|
+
|
|
122
|
+
for (const candidate of candidates) {
|
|
123
|
+
if (fs.existsSync(candidate)) {
|
|
124
|
+
return candidate;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Return the most likely location even if it doesn't exist yet
|
|
129
|
+
return path.join(repoPath, 'validator.log');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Parse command line args
|
|
134
|
+
*/
|
|
135
|
+
function parseArgs() {
|
|
136
|
+
const args = process.argv.slice(3); // Skip 'aether-cli logs'
|
|
137
|
+
|
|
138
|
+
const options = {
|
|
139
|
+
filePath: null,
|
|
140
|
+
follow: false,
|
|
141
|
+
lines: 50,
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
for (let i = 0; i < args.length; i++) {
|
|
145
|
+
switch (args[i]) {
|
|
146
|
+
case '--file':
|
|
147
|
+
case '-f':
|
|
148
|
+
options.filePath = args[++i];
|
|
149
|
+
break;
|
|
150
|
+
case '--follow':
|
|
151
|
+
case '-F':
|
|
152
|
+
options.follow = true;
|
|
153
|
+
break;
|
|
154
|
+
case '--lines':
|
|
155
|
+
case '-n':
|
|
156
|
+
options.lines = parseInt(args[++i], 10) || 50;
|
|
157
|
+
break;
|
|
158
|
+
case '--help':
|
|
159
|
+
case '-h':
|
|
160
|
+
showHelp();
|
|
161
|
+
process.exit(0);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Default file if not specified
|
|
166
|
+
if (!options.filePath) {
|
|
167
|
+
options.filePath = findDefaultLogFile();
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return options;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Show help message
|
|
175
|
+
*/
|
|
176
|
+
function showHelp() {
|
|
177
|
+
console.log(`
|
|
178
|
+
${colors.bright}aether-cli logs${colors.reset} - Validator Log Viewer
|
|
179
|
+
|
|
180
|
+
${colors.cyan}Usage:${colors.reset}
|
|
181
|
+
aether-cli logs [options]
|
|
182
|
+
|
|
183
|
+
${colors.cyan}Options:${colors.reset}
|
|
184
|
+
--file, -f <path> Path to log file (default: auto-detect)
|
|
185
|
+
--follow, -F Follow mode (like tail -f)
|
|
186
|
+
--lines, -n <num> Number of lines to show (default: 50, use -1 for all)
|
|
187
|
+
--help, -h Show this help message
|
|
188
|
+
|
|
189
|
+
${colors.cyan}Examples:${colors.reset}
|
|
190
|
+
aether-cli logs # Tail default validator log
|
|
191
|
+
aether-cli logs --follow # Follow live logs
|
|
192
|
+
aether-cli logs --file ./my.log -n 100
|
|
193
|
+
aether-cli logs -F -n -1 # Follow all lines
|
|
194
|
+
|
|
195
|
+
${colors.cyan}Log Level Colors:${colors.reset}
|
|
196
|
+
ERROR ${colors.bgRed + colors.white}Red background${colors.reset}
|
|
197
|
+
WARN ${colors.bgYellow + colors.black}Yellow background${colors.reset}
|
|
198
|
+
INFO ${colors.green}Green${colors.reset}
|
|
199
|
+
DEBUG ${colors.blue}Blue${colors.reset}
|
|
200
|
+
TRACE ${colors.dim}Dim${colors.reset}
|
|
201
|
+
`);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Read and display log file (static mode)
|
|
206
|
+
*/
|
|
207
|
+
function readLogFile(filePath, lines) {
|
|
208
|
+
if (!fs.existsSync(filePath)) {
|
|
209
|
+
console.log(` ${colors.yellow}⚠ Log file not found: ${filePath}${colors.reset}`);
|
|
210
|
+
console.log(` ${colors.dim}Start the validator first to generate logs.${colors.reset}`);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
215
|
+
const allLines = content.split('\n').filter(line => line.trim());
|
|
216
|
+
|
|
217
|
+
// Get last N lines
|
|
218
|
+
const displayLines = lines === -1
|
|
219
|
+
? allLines
|
|
220
|
+
: allLines.slice(-lines);
|
|
221
|
+
|
|
222
|
+
displayLines.forEach(line => {
|
|
223
|
+
console.log(colorizeLogLine(line));
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Follow log file (tail -f mode)
|
|
229
|
+
*/
|
|
230
|
+
function followLogFile(filePath, lines) {
|
|
231
|
+
if (!fs.existsSync(filePath)) {
|
|
232
|
+
console.log(` ${colors.yellow}⚠ Log file not found: ${filePath}${colors.reset}`);
|
|
233
|
+
console.log(` ${colors.dim}Waiting for file to be created...${colors.reset}`);
|
|
234
|
+
console.log();
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Use platform-specific tail command
|
|
238
|
+
const platform = process.platform;
|
|
239
|
+
let tailCmd, tailArgs;
|
|
240
|
+
|
|
241
|
+
if (platform === 'win32') {
|
|
242
|
+
// PowerShell Get-Content -Wait is equivalent to tail -f
|
|
243
|
+
tailCmd = 'powershell';
|
|
244
|
+
tailArgs = [
|
|
245
|
+
'-Command',
|
|
246
|
+
`Get-Content "${filePath}" -Wait -Tail ${lines === -1 ? 1000 : lines} 2>$null`
|
|
247
|
+
];
|
|
248
|
+
} else {
|
|
249
|
+
tailCmd = 'tail';
|
|
250
|
+
tailArgs = ['-F', '-n', lines === -1 ? '1000' : lines.toString(), filePath];
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const tail = spawn(tailCmd, tailArgs, {
|
|
254
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
tail.stdout.on('data', (data) => {
|
|
258
|
+
const text = data.toString();
|
|
259
|
+
const textLines = text.split('\n');
|
|
260
|
+
textLines.forEach(line => {
|
|
261
|
+
if (line.trim()) {
|
|
262
|
+
console.log(colorizeLogLine(line));
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
tail.stderr.on('data', (data) => {
|
|
268
|
+
const text = data.toString();
|
|
269
|
+
if (text.includes('No such file') || text.includes('cannot open')) {
|
|
270
|
+
// File doesn't exist yet, wait
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
console.log(`${colors.red}${text}${colors.reset}`);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
tail.on('error', (err) => {
|
|
277
|
+
console.log(`${colors.red}Error: ${err.message}${colors.reset}`);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
tail.on('close', (code) => {
|
|
281
|
+
if (code !== 0 && code !== null) {
|
|
282
|
+
console.log(`\n${colors.yellow}Log viewer exited with code ${code}${colors.reset}`);
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
// Handle Ctrl+C
|
|
287
|
+
process.on('SIGINT', () => {
|
|
288
|
+
console.log(`\n${colors.cyan}Logs viewer stopped.${colors.reset}`);
|
|
289
|
+
tail.kill();
|
|
290
|
+
process.exit(0);
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Main logs command
|
|
296
|
+
*/
|
|
297
|
+
function logsCommand() {
|
|
298
|
+
const options = parseArgs();
|
|
299
|
+
|
|
300
|
+
printBanner(options);
|
|
301
|
+
|
|
302
|
+
if (options.follow) {
|
|
303
|
+
followLogFile(options.filePath, options.lines);
|
|
304
|
+
} else {
|
|
305
|
+
readLogFile(options.filePath, options.lines);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Export for use as module
|
|
310
|
+
module.exports = { logsCommand, colorizeLogLine, findDefaultLogFile };
|
|
311
|
+
|
|
312
|
+
// Run if called directly
|
|
313
|
+
if (require.main === module) {
|
|
314
|
+
logsCommand();
|
|
315
|
+
}
|