aether-hub 1.0.3
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/commands/doctor.js +720 -0
- package/commands/init.js +685 -0
- package/commands/logs.js +315 -0
- package/commands/monitor.js +431 -0
- package/commands/sdk.js +381 -0
- package/commands/validator-start.js +290 -0
- package/commands/validator-status.js +268 -0
- package/index.js +275 -0
- package/package.json +51 -0
- package/test/doctor.test.js +76 -0
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
|
+
}
|