@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,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
+ }
@@ -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
+ }