pyre-cli 5.0.0 → 6.0.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/README.md +197 -255
- package/dist/index.js +1454 -281
- package/package.json +1 -1
- package/src/anomalies.ts +102 -0
- package/src/formatters/index.ts +3 -3
- package/src/formatters/output.ts +355 -21
- package/src/formatters/render.ts +316 -97
- package/src/formatters/types.ts +33 -17
- package/src/history.ts +57 -13
- package/src/index.ts +226 -13
- package/src/live/export.ts +64 -7
- package/src/live/input.ts +84 -39
- package/src/live/render.ts +175 -118
- package/src/live/state.ts +76 -70
- package/src/live/types.ts +10 -2
- package/src/monitors/collectors.ts +142 -35
- package/src/monitors/smc.ts +60 -12
- package/src/monitors/types.ts +1 -0
- package/src/p2p/server.test.ts +2 -2
- package/src/sparkline.ts +82 -13
- package/src/state/config.ts +130 -0
- package/Formula/pyre.rb +0 -19
package/src/index.ts
CHANGED
|
@@ -8,14 +8,19 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import fs from 'node:fs';
|
|
10
10
|
import os from 'node:os';
|
|
11
|
+
import path from 'node:path';
|
|
11
12
|
import readline from 'node:readline';
|
|
12
13
|
import { Command } from 'commander';
|
|
13
14
|
import chalk from 'chalk';
|
|
14
15
|
import { collectAll } from './monitors/index.js';
|
|
15
|
-
import { formatTable, formatJson, formatCsv, formatTsv } from './formatters/index.js';
|
|
16
|
+
import { formatTable, formatJson, formatCsv, formatTsv, formatHtml, formatMarkdown, formatBytes } from './formatters/index.js';
|
|
16
17
|
import { startLive, stopLive } from './live/index.js';
|
|
17
18
|
import { showSplash } from './splash.js';
|
|
18
19
|
import { P2PServer, P2PClient } from './p2p/index.js';
|
|
20
|
+
import { readConfig } from './state/config.js';
|
|
21
|
+
import { CONFIG_FILE } from './state/config.js';
|
|
22
|
+
|
|
23
|
+
const config = readConfig();
|
|
19
24
|
|
|
20
25
|
const pkg = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf-8'));
|
|
21
26
|
|
|
@@ -26,19 +31,24 @@ program
|
|
|
26
31
|
.version(pkg.version)
|
|
27
32
|
.description('Mac system monitoring CLI: interactive live dashboard, stats, graphs, export, packet monitor, battery predictor')
|
|
28
33
|
.option('-j, --json', 'Output as JSON')
|
|
34
|
+
.option('--html', 'Output as HTML')
|
|
35
|
+
.option('--md', 'Output as Markdown')
|
|
29
36
|
.option('-c, --csv', 'Output as CSV')
|
|
30
37
|
.option('-t, --tsv', 'Output as TSV')
|
|
31
38
|
.option('--detailed', 'Include detailed system info and sensor readings')
|
|
32
|
-
.option('--theme <name>', 'Default theme for live mode (default, dracula, cyberpunk, monochrome, nord, gruvbox)',
|
|
33
|
-
.option('--interval <seconds>', 'Refresh interval for live mode',
|
|
39
|
+
.option('--theme <name>', 'Default theme for live mode (default, dracula, cyberpunk, monochrome, nord, gruvbox)', config.theme)
|
|
40
|
+
.option('--interval <seconds>', 'Refresh interval for live mode', String(config.interval))
|
|
34
41
|
.option('--once', 'Show a single static snapshot instead of live feed')
|
|
35
42
|
.option('--out <file>', 'Also write the snapshot output to a file (--once/--json/--csv/--tsv modes)')
|
|
36
|
-
.option('--export-dir <dir>', 'Directory used for live-mode snapshot exports and logs',
|
|
43
|
+
.option('--export-dir <dir>', 'Directory used for live-mode snapshot exports and logs', config.exportDir)
|
|
37
44
|
.option('--log', 'Start continuous CSV logging immediately when live mode starts')
|
|
38
45
|
.option('--tree', 'Show process tree view instead of flat list')
|
|
39
|
-
.option('--sort <key>', 'Sort processes by: cpu, mem, pid, user, command, state, threads, runtime',
|
|
46
|
+
.option('--sort <key>', 'Sort processes by: cpu, mem, pid, user, command, state, threads, runtime', config.sortMode)
|
|
40
47
|
.option('--packets', 'Include packet monitor panel in output')
|
|
41
48
|
.option('--limit <n>', 'Max number of processes to include in --once/--json/--csv/--tsv snapshots (0 = all)', '10')
|
|
49
|
+
.option('--alert-cpu <pct>', 'CPU usage alert threshold (default: 90)', String(config.cpuAlertPct))
|
|
50
|
+
.option('--alert-temp <c>', 'CPU temperature alert threshold in Celsius (default: 95)', String(config.tempAlertC))
|
|
51
|
+
.option('--temp-unit <unit>', 'Temperature display unit: c or f (default: c)', 'c')
|
|
42
52
|
.option('--p2p-host <host>', 'P2P host address (server: bind address, client: server address)')
|
|
43
53
|
.option('--p2p-port <port>', 'P2P port number', '9876')
|
|
44
54
|
.option('--p2p-password <password>', 'Password for P2P authentication')
|
|
@@ -51,14 +61,15 @@ program
|
|
|
51
61
|
.option('--p2p-allow <ips>', 'Comma-separated list of allowed IPs (empty = all)')
|
|
52
62
|
.option('--p2p-deny <ips>', 'Comma-separated list of denied IPs')
|
|
53
63
|
.option('--p2p-audit-log <dir>', 'Directory for P2P audit logs')
|
|
54
|
-
.option('--p2p-hmac-key <key>', 'HMAC key for message signing (default: derived from password)')
|
|
64
|
+
.option('--p2p-hmac-key <key>', 'HMAC key for message signing (default: derived from password)')
|
|
65
|
+
.option('--port <port>', 'Port number for web server mode', '3000');
|
|
55
66
|
|
|
56
67
|
program.parse(process.argv);
|
|
57
68
|
|
|
58
69
|
const opts = program.opts();
|
|
59
70
|
|
|
60
71
|
function isExportMode() {
|
|
61
|
-
return opts.json || opts.csv || opts.tsv;
|
|
72
|
+
return opts.json || opts.csv || opts.tsv || opts.html || opts.md;
|
|
62
73
|
}
|
|
63
74
|
|
|
64
75
|
function stripAnsi(s: string): string {
|
|
@@ -112,23 +123,82 @@ async function runServerCommand(): Promise<void> {
|
|
|
112
123
|
async function main() {
|
|
113
124
|
const cmd = program.args[0];
|
|
114
125
|
|
|
126
|
+
if (cmd === 'info') {
|
|
127
|
+
await runInfoCommand();
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (cmd === 'ssh') {
|
|
132
|
+
const host = program.args[1];
|
|
133
|
+
if (!host) {
|
|
134
|
+
console.log(chalk.red('Usage: pyre ssh <host>'));
|
|
135
|
+
process.exit(1);
|
|
136
|
+
}
|
|
137
|
+
await runSshCommand(host);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (cmd === 'web') {
|
|
142
|
+
await runWebCommand();
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (cmd === 'bench') {
|
|
147
|
+
const benchCmd = program.args.slice(1).join(' ');
|
|
148
|
+
if (!benchCmd) {
|
|
149
|
+
console.log(chalk.red('Usage: pyre bench <command>'));
|
|
150
|
+
process.exit(1);
|
|
151
|
+
}
|
|
152
|
+
await runBenchCommand(benchCmd);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (cmd === 'config') {
|
|
157
|
+
const sub = program.args[1];
|
|
158
|
+
if (sub === 'show') {
|
|
159
|
+
const cfg = readConfig();
|
|
160
|
+
console.log(JSON.stringify(cfg, null, 2));
|
|
161
|
+
} else if (sub === 'reset') {
|
|
162
|
+
fs.rmSync(CONFIG_FILE, { force: true });
|
|
163
|
+
console.log(chalk.green('Config reset to defaults'));
|
|
164
|
+
} else {
|
|
165
|
+
console.log('Usage: pyre config <show|reset>');
|
|
166
|
+
}
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
115
170
|
if (cmd === 'server') {
|
|
116
171
|
await runServerCommand();
|
|
117
172
|
return;
|
|
118
173
|
}
|
|
119
174
|
|
|
175
|
+
const alertCpu = opts.alertCpu ? parseInt(opts.alertCpu, 10) : undefined;
|
|
176
|
+
const alertTemp = opts.alertTemp ? parseInt(opts.alertTemp, 10) : undefined;
|
|
177
|
+
const tempUnit = opts.tempUnit === 'f' ? 'f' : 'c';
|
|
178
|
+
|
|
120
179
|
if (cmd === 'live' || (!isExportMode() && !opts.once && cmd !== 'p2p')) {
|
|
180
|
+
if (!process.stdout.isTTY && cmd !== 'live') {
|
|
181
|
+
await runWebCommand();
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
121
184
|
if (cmd === 'live') {
|
|
122
185
|
program.args.shift();
|
|
123
186
|
}
|
|
124
|
-
const splashPromise = showSplash(
|
|
187
|
+
const splashPromise = showSplash({
|
|
188
|
+
enabled: config.splashEnabled,
|
|
189
|
+
colorScheme: config.splashColorScheme as any,
|
|
190
|
+
animation: config.splashAnimation as any,
|
|
191
|
+
});
|
|
125
192
|
const interval = parseFloat(opts.interval) || 2;
|
|
126
193
|
await startLive({
|
|
127
194
|
interval,
|
|
128
|
-
detailed: opts.detailed,
|
|
129
|
-
theme: opts.theme,
|
|
130
|
-
exportDir: opts.exportDir,
|
|
131
|
-
autoLog: opts.log,
|
|
195
|
+
detailed: opts.detailed ?? config.detailed,
|
|
196
|
+
theme: opts.theme || config.theme,
|
|
197
|
+
exportDir: opts.exportDir || config.exportDir,
|
|
198
|
+
autoLog: opts.log || config.autoLog,
|
|
199
|
+
alertCpu,
|
|
200
|
+
alertTemp,
|
|
201
|
+
tempUnit,
|
|
132
202
|
}, splashPromise);
|
|
133
203
|
return;
|
|
134
204
|
}
|
|
@@ -156,9 +226,11 @@ async function main() {
|
|
|
156
226
|
|
|
157
227
|
let output: string;
|
|
158
228
|
if (opts.json) output = formatJson(data);
|
|
229
|
+
else if (opts.html) output = formatHtml(data);
|
|
230
|
+
else if (opts.md) output = formatMarkdown(data);
|
|
159
231
|
else if (opts.csv) output = formatCsv(data);
|
|
160
232
|
else if (opts.tsv) output = formatTsv(data);
|
|
161
|
-
else output = formatTable(data, { width: process.stdout.columns || 80, sortBy: opts.sort, treeView: opts.tree, visible: { packets: opts.packets ? true : undefined } });
|
|
233
|
+
else output = formatTable(data, { width: process.stdout.columns || 80, sortBy: opts.sort, treeView: opts.tree ?? config.treeView, visible: { packets: opts.packets ? true : undefined } });
|
|
162
234
|
|
|
163
235
|
console.log(output);
|
|
164
236
|
|
|
@@ -363,4 +435,145 @@ async function runP2PConnect(): Promise<void> {
|
|
|
363
435
|
process.stdout.write('\x1b[?25h\x1b[2J\x1b[H');
|
|
364
436
|
process.exit(0);
|
|
365
437
|
});
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
async function runInfoCommand(): Promise<void> {
|
|
441
|
+
const data = await collectAll({ detailed: true });
|
|
442
|
+
const lines: string[] = [];
|
|
443
|
+
lines.push(chalk.bold(' pyre info'));
|
|
444
|
+
lines.push('');
|
|
445
|
+
lines.push(` Hostname: ${data.header.hostname}`);
|
|
446
|
+
lines.push(` OS: ${data.header.os}`);
|
|
447
|
+
lines.push(` Uptime: ${data.header.uptime}`);
|
|
448
|
+
lines.push(` CPU: ${data.cpu.brand}`);
|
|
449
|
+
lines.push(` Cores: ${data.cpu.physicalCores}/${data.cpu.cores} (phys/log)`);
|
|
450
|
+
lines.push(` Frequency: ${data.cpu.frequency} MHz`);
|
|
451
|
+
lines.push(` Memory: ${formatBytes(data.memory.total)}`);
|
|
452
|
+
if (data.gpu) lines.push(` GPU: ${data.gpu.model} (${formatBytes(data.gpu.memory)})`);
|
|
453
|
+
if (data.battery) {
|
|
454
|
+
lines.push(` Battery: ${data.battery.level}% (${data.battery.state})`);
|
|
455
|
+
lines.push(` Health: ${data.battery.health}`);
|
|
456
|
+
}
|
|
457
|
+
lines.push(` Thermal: ${data.thermal.state}`);
|
|
458
|
+
console.log(lines.join('\n'));
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
async function runSshCommand(host: string): Promise<void> {
|
|
462
|
+
const password = opts.p2pPassword || 'mysecret';
|
|
463
|
+
const cmd = `ssh -o LogLevel=QUIET ${host} "pyre --json --once"`;
|
|
464
|
+
console.log(chalk.bold(`\n pyre ssh — ${host}`));
|
|
465
|
+
console.log(chalk.dim(` Running: ${cmd}\n`));
|
|
466
|
+
|
|
467
|
+
const { execFile } = await import('node:child_process');
|
|
468
|
+
const child = execFile('ssh', ['-o', 'LogLevel=QUIET', host, 'pyre', '--json', '--once'], (err, stdout, stderr) => {
|
|
469
|
+
if (err) {
|
|
470
|
+
console.log(chalk.red(` SSH failed: ${err.message}`));
|
|
471
|
+
console.log(chalk.dim(' Ensure SSH keys are set up or the host is reachable.'));
|
|
472
|
+
process.exit(1);
|
|
473
|
+
}
|
|
474
|
+
if (stderr) console.log(chalk.dim(stderr));
|
|
475
|
+
try {
|
|
476
|
+
const remote = JSON.parse(stdout);
|
|
477
|
+
console.log(formatTable(remote, { width: process.stdout.columns || 80, sortBy: 'cpu', treeView: false, visible: { packets: true } }));
|
|
478
|
+
} catch {
|
|
479
|
+
console.log(stdout);
|
|
480
|
+
}
|
|
481
|
+
process.exit(0);
|
|
482
|
+
});
|
|
483
|
+
child.stdin?.end();
|
|
484
|
+
|
|
485
|
+
process.once('SIGINT', () => {
|
|
486
|
+
child.kill('SIGTERM');
|
|
487
|
+
process.exit(0);
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
async function runWebCommand(): Promise<void> {
|
|
492
|
+
const http = await import('node:http');
|
|
493
|
+
const port = parseInt(opts.port || process.env.PORT || '3000', 10) || 3000;
|
|
494
|
+
|
|
495
|
+
const server = http.createServer(async (req, res) => {
|
|
496
|
+
const url = req.url || '/';
|
|
497
|
+
if (url === '/' || url === '/index.html') {
|
|
498
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
499
|
+
const indexPath = path.join(process.cwd(), 'index.html');
|
|
500
|
+
let htmlContent = '';
|
|
501
|
+
if (fs.existsSync(indexPath)) {
|
|
502
|
+
htmlContent = fs.readFileSync(indexPath, 'utf-8');
|
|
503
|
+
} else {
|
|
504
|
+
const data = await collectAll({ detailed: true });
|
|
505
|
+
htmlContent = formatHtml(data);
|
|
506
|
+
}
|
|
507
|
+
res.end(htmlContent);
|
|
508
|
+
} else if (url === '/api' || url === '/api/stats' || url === '/data') {
|
|
509
|
+
const data = await collectAll({ detailed: true });
|
|
510
|
+
res.writeHead(200, {
|
|
511
|
+
'Content-Type': 'application/json',
|
|
512
|
+
'Access-Control-Allow-Origin': '*',
|
|
513
|
+
'Access-Control-Allow-Methods': 'GET, OPTIONS',
|
|
514
|
+
});
|
|
515
|
+
res.end(JSON.stringify(data, null, 2));
|
|
516
|
+
} else {
|
|
517
|
+
res.writeHead(404);
|
|
518
|
+
res.end('Not found');
|
|
519
|
+
}
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
server.listen(port, '0.0.0.0', () => {
|
|
523
|
+
console.log(chalk.bold(`\n pyre web server running on port ${port}`));
|
|
524
|
+
console.log(chalk.dim(` Dashboard: http://0.0.0.0:${port}/`));
|
|
525
|
+
console.log(chalk.dim(` API: http://0.0.0.0:${port}/api`));
|
|
526
|
+
console.log(chalk.dim(' Press Ctrl+C to stop\n'));
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
process.once('SIGINT', () => {
|
|
530
|
+
server.close();
|
|
531
|
+
process.exit(0);
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
async function runBenchCommand(benchCmd: string): Promise<void> {
|
|
536
|
+
const { spawn } = await import('node:child_process');
|
|
537
|
+
const logDir = opts.exportDir || './pyre-exports';
|
|
538
|
+
const file = path.join(logDir, `pyre-bench-${new Date().toISOString().replace(/[:.]/g, '-')}.csv`);
|
|
539
|
+
|
|
540
|
+
if (!fs.existsSync(logDir)) fs.mkdirSync(logDir, { recursive: true });
|
|
541
|
+
const stream = fs.createWriteStream(file, { flags: 'a' });
|
|
542
|
+
stream.write('timestamp,cpu_usage,mem_usage_percent,temp_c,net_rx_bytes,net_tx_bytes,net_rx_packets,net_tx_packets,connections,thermal_state\n');
|
|
543
|
+
|
|
544
|
+
console.log(chalk.bold(`\n pyre bench`));
|
|
545
|
+
console.log(chalk.dim(` Command: ${benchCmd}`));
|
|
546
|
+
console.log(chalk.dim(` Log: ${file}\n`));
|
|
547
|
+
|
|
548
|
+
const child = spawn('sh', ['-c', benchCmd]);
|
|
549
|
+
child.stdout.on('data', d => process.stdout.write(d));
|
|
550
|
+
child.stderr.on('data', d => process.stdout.write(d));
|
|
551
|
+
|
|
552
|
+
const interval = parseFloat(opts.interval) || 2;
|
|
553
|
+
const handle = setInterval(async () => {
|
|
554
|
+
try {
|
|
555
|
+
const data = await collectAll({ detailed: opts.detailed });
|
|
556
|
+
const temp = data.cpu.temperature ?? data.thermal.temperatures?.cpu_die ?? '';
|
|
557
|
+
const rxPackets = data.network.rxPackets ?? 0;
|
|
558
|
+
const txPackets = data.network.txPackets ?? 0;
|
|
559
|
+
const connections = data.network.connections ?? 0;
|
|
560
|
+
stream.write(`${data.timestamp},${data.cpu.usage},${data.memory.usagePercent},${temp},${data.network.rxBytes},${data.network.txBytes},${rxPackets},${txPackets},${connections},${data.thermal.state}\n`);
|
|
561
|
+
} catch {
|
|
562
|
+
// skip bad tick
|
|
563
|
+
}
|
|
564
|
+
}, interval * 1000);
|
|
565
|
+
|
|
566
|
+
child.on('close', () => {
|
|
567
|
+
clearInterval(handle);
|
|
568
|
+
stream.end();
|
|
569
|
+
console.log(chalk.green(`\n Bench complete. Log saved to ${file}`));
|
|
570
|
+
process.exit(0);
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
process.once('SIGINT', () => {
|
|
574
|
+
clearInterval(handle);
|
|
575
|
+
child.kill('SIGTERM');
|
|
576
|
+
stream.end();
|
|
577
|
+
process.exit(0);
|
|
578
|
+
});
|
|
366
579
|
}
|
package/src/live/export.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Live dashboard export and logging helpers.
|
|
3
3
|
*
|
|
4
|
-
* Handles snapshot export (JSON/CSV/TSV), continuous
|
|
5
|
-
* CSV logging, and the supporting file-system utilities.
|
|
4
|
+
* Handles snapshot export (JSON/CSV/TSV/HTML/Markdown), continuous
|
|
5
|
+
* CSV logging with rotation, and the supporting file-system utilities.
|
|
6
6
|
* All state variables are imported from `state.ts`.
|
|
7
7
|
*/
|
|
8
8
|
import fs from 'node:fs';
|
|
9
9
|
import path from 'node:path';
|
|
10
|
-
import { formatJson, formatCsv, formatTsv } from '../formatters/index.js';
|
|
10
|
+
import { formatJson, formatCsv, formatTsv, formatHtml, formatMarkdown } from '../formatters/index.js';
|
|
11
11
|
import { state, setStatus } from './state.js';
|
|
12
12
|
|
|
13
13
|
/** Ensure a directory exists, creating it recursively if needed. */
|
|
@@ -27,9 +27,16 @@ function exportSnapshot() {
|
|
|
27
27
|
}
|
|
28
28
|
try {
|
|
29
29
|
ensureDir(state.exportDir);
|
|
30
|
-
const
|
|
31
|
-
const
|
|
32
|
-
|
|
30
|
+
const ext = state.exportFormat === 'json' ? 'json' : state.exportFormat === 'csv' ? 'csv' : state.exportFormat === 'tsv' ? 'tsv' : state.exportFormat === 'html' ? 'html' : 'md';
|
|
31
|
+
const file = path.join(state.exportDir, `pyre-${timestampForFile()}.${ext}`);
|
|
32
|
+
let content: string;
|
|
33
|
+
switch (state.exportFormat) {
|
|
34
|
+
case 'json': content = formatJson(state.lastData); break;
|
|
35
|
+
case 'csv': content = formatCsv(state.lastData); break;
|
|
36
|
+
case 'tsv': content = formatTsv(state.lastData); break;
|
|
37
|
+
case 'html': content = formatHtml(state.lastData); break;
|
|
38
|
+
case 'md': content = formatMarkdown(state.lastData); break;
|
|
39
|
+
}
|
|
33
40
|
fs.writeFileSync(file, content);
|
|
34
41
|
setStatus(`Exported snapshot -> ${file}`);
|
|
35
42
|
} catch (err: any) {
|
|
@@ -40,6 +47,7 @@ function exportSnapshot() {
|
|
|
40
47
|
function startLogging() {
|
|
41
48
|
try {
|
|
42
49
|
ensureDir(state.exportDir);
|
|
50
|
+
rotateLogs();
|
|
43
51
|
const file = path.join(state.exportDir, `pyre-log-${timestampForFile()}.csv`);
|
|
44
52
|
state.logStream = fs.createWriteStream(file, { flags: 'a' });
|
|
45
53
|
state.logStream.write('timestamp,cpu_usage,mem_usage_percent,temp_c,net_rx_bytes,net_tx_bytes,net_rx_packets,net_tx_packets,connections,thermal_state\n');
|
|
@@ -50,6 +58,52 @@ function startLogging() {
|
|
|
50
58
|
}
|
|
51
59
|
}
|
|
52
60
|
|
|
61
|
+
const MAX_LOG_AGE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
62
|
+
const MAX_LOG_COUNT = 20;
|
|
63
|
+
const MAX_LOG_BYTES = 50 * 1024 * 1024;
|
|
64
|
+
|
|
65
|
+
function rotateLogs() {
|
|
66
|
+
try {
|
|
67
|
+
const dir = state.exportDir;
|
|
68
|
+
if (!fs.existsSync(dir)) return;
|
|
69
|
+
const files = fs.readdirSync(dir)
|
|
70
|
+
.filter(f => f.startsWith('pyre-log-') && f.endsWith('.csv'))
|
|
71
|
+
.map(f => ({ name: f, path: path.join(dir, f), mtime: fs.statSync(path.join(dir, f)).mtimeMs, size: fs.statSync(path.join(dir, f)).size }))
|
|
72
|
+
.sort((a, b) => a.mtime - b.mtime);
|
|
73
|
+
|
|
74
|
+
const now = Date.now();
|
|
75
|
+
for (const f of files) {
|
|
76
|
+
if (now - f.mtime > MAX_LOG_AGE_MS) {
|
|
77
|
+
try { fs.unlinkSync(f.path); } catch { /* ignore */ }
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const remaining = fs.readdirSync(dir).filter(f => f.startsWith('pyre-log-') && f.endsWith('.csv'));
|
|
82
|
+
if (remaining.length > MAX_LOG_COUNT) {
|
|
83
|
+
const toDelete = remaining.sort().slice(0, remaining.length - MAX_LOG_COUNT);
|
|
84
|
+
for (const f of toDelete) {
|
|
85
|
+
try { fs.unlinkSync(path.join(dir, f)); } catch { /* ignore */ }
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
let totalSize = 0;
|
|
90
|
+
for (const f of remaining) {
|
|
91
|
+
totalSize += fs.statSync(path.join(dir, f)).size;
|
|
92
|
+
}
|
|
93
|
+
if (totalSize > MAX_LOG_BYTES) {
|
|
94
|
+
const sorted = remaining.sort();
|
|
95
|
+
for (const f of sorted) {
|
|
96
|
+
const sz = fs.statSync(path.join(dir, f)).size;
|
|
97
|
+
try { fs.unlinkSync(path.join(dir, f)); } catch { /* ignore */ }
|
|
98
|
+
totalSize -= sz;
|
|
99
|
+
if (totalSize <= MAX_LOG_BYTES * 0.8) break;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
} catch {
|
|
103
|
+
// ignore rotation errors
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
53
107
|
function stopLogging() {
|
|
54
108
|
state.logging = false;
|
|
55
109
|
state.logStream?.end();
|
|
@@ -64,7 +118,10 @@ function toggleLogging() {
|
|
|
64
118
|
|
|
65
119
|
function writeLogRow(data: any) {
|
|
66
120
|
if (!state.logging || !state.logStream) return;
|
|
67
|
-
|
|
121
|
+
let temp = data.cpu.temperature ?? data.thermal.temperatures?.cpu_die ?? '';
|
|
122
|
+
if (temp !== '' && state.tempUnit === 'f') {
|
|
123
|
+
temp = ((temp * 9 / 5) + 32).toFixed(1);
|
|
124
|
+
}
|
|
68
125
|
const rxPackets = data.network.rxPackets ?? 0;
|
|
69
126
|
const txPackets = data.network.txPackets ?? 0;
|
|
70
127
|
const connections = data.network.connections ?? 0;
|
package/src/live/input.ts
CHANGED
|
@@ -13,10 +13,35 @@ import { THEMES, type ThemeName, type VisibleItems, getTabHitboxes, TAB_BAR_ROW
|
|
|
13
13
|
import { state, setStatus, getToggleKey, type SplashColorScheme, type SplashAnimation } from './state.js';
|
|
14
14
|
import { exportSnapshot, startLogging, stopLogging, toggleLogging, writeLogRow } from './export.js';
|
|
15
15
|
import { render, footerLine, checkAlerts, invalidateTableCache, invalidateFrame } from './render.js';
|
|
16
|
-
import type { LiveOptions, ExportFormat, InputMode, SortMode, GraphMode } from './types.js';
|
|
16
|
+
import type { LiveOptions, ExportFormat, InputMode, SortMode, GraphMode, ActivePanel } from './types.js';
|
|
17
17
|
import { startP2PServer } from '../p2p/index.js';
|
|
18
|
+
import { writeConfig } from '../state/config.js';
|
|
19
|
+
|
|
20
|
+
function persistConfig() {
|
|
21
|
+
writeConfig({
|
|
22
|
+
theme: state.currentTheme,
|
|
23
|
+
interval: state.interval,
|
|
24
|
+
exportDir: state.exportDir,
|
|
25
|
+
detailed: state.detailed,
|
|
26
|
+
sortMode: state.sortMode,
|
|
27
|
+
treeView: state.treeView,
|
|
28
|
+
graphMode: state.graphMode,
|
|
29
|
+
showGraphs: state.showGraphs,
|
|
30
|
+
autoLog: state.logging,
|
|
31
|
+
mouseEnabled: state.mouseEnabled,
|
|
32
|
+
cpuAlertPct: state.CPU_ALERT_PCT,
|
|
33
|
+
tempAlertC: state.TEMP_ALERT_C,
|
|
34
|
+
p2pPort: state.p2pPort,
|
|
35
|
+
p2pPassword: state.p2pPassword,
|
|
36
|
+
splashEnabled: state.splashEnabled,
|
|
37
|
+
splashColorScheme: state.splashColorScheme,
|
|
38
|
+
splashAnimation: state.splashAnimation,
|
|
39
|
+
visiblePanels: { ...state.visiblePanels },
|
|
40
|
+
notificationsEnabled: state.notificationsEnabled,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
18
43
|
|
|
19
|
-
function isUpKey(key: readline.Key, str?: string): boolean {
|
|
44
|
+
function isUpKey(key: readline.Key, str?: string): boolean {
|
|
20
45
|
return key.name === 'up' || str === 'k' || key.sequence === '\x1b[A' || key.sequence === '\x1bOA';
|
|
21
46
|
}
|
|
22
47
|
function isDownKey(key: readline.Key, str?: string): boolean {
|
|
@@ -59,28 +84,35 @@ function handleInputModeKey(str: string, key: readline.Key) {
|
|
|
59
84
|
const curIdx = schemes.indexOf(state.splashColorScheme);
|
|
60
85
|
state.splashColorScheme = schemes[(curIdx + 1) % schemes.length];
|
|
61
86
|
setStatus(`Splash color: ${state.splashColorScheme}`);
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
87
|
+
} else if (selected === 'Splash Animation') {
|
|
88
|
+
const animations: SplashAnimation[] = ['classic', 'wave', 'sparks'];
|
|
89
|
+
const curIdx = animations.indexOf(state.splashAnimation);
|
|
90
|
+
state.splashAnimation = animations[(curIdx + 1) % animations.length];
|
|
91
|
+
setStatus(`Splash animation: ${state.splashAnimation}`);
|
|
92
|
+
} else if (selected === 'Notifications') {
|
|
93
|
+
state.notificationsEnabled = !state.notificationsEnabled;
|
|
94
|
+
setStatus(`Notifications: ${state.notificationsEnabled ? 'on' : 'off'}`);
|
|
95
|
+
} else if (selected === 'Temperature Unit') {
|
|
96
|
+
state.tempUnit = state.tempUnit === 'c' ? 'f' : 'c';
|
|
97
|
+
setStatus(`Temperature unit: ${state.tempUnit.toUpperCase()}`);
|
|
98
|
+
} else {
|
|
99
|
+
const toggleKey = getToggleKey(selected);
|
|
100
|
+
if (toggleKey) {
|
|
101
|
+
if (toggleKey === 'tree') {
|
|
102
|
+
state.treeView = !state.treeView;
|
|
103
|
+
state.visiblePanels.tree = state.treeView;
|
|
104
|
+
} else {
|
|
105
|
+
const isVisible = state.visiblePanels[toggleKey] !== false;
|
|
106
|
+
state.visiblePanels[toggleKey] = !isVisible;
|
|
107
|
+
}
|
|
76
108
|
}
|
|
77
109
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
110
|
+
invalidateTableCache();
|
|
111
|
+
invalidateFrame();
|
|
112
|
+
persistConfig();
|
|
113
|
+
}
|
|
114
|
+
render();
|
|
115
|
+
return;
|
|
84
116
|
}
|
|
85
117
|
|
|
86
118
|
if (state.inputMode === 'signal') {
|
|
@@ -310,18 +342,21 @@ function killProcess(pidStr: string, signal: string = 'SIGTERM') {
|
|
|
310
342
|
* rendering interval. Idempotent — calling while already
|
|
311
343
|
* running is a no-op.
|
|
312
344
|
*/
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
345
|
+
export async function startLive(opts: LiveOptions, splashPromise?: Promise<void>) {
|
|
346
|
+
if (state.running) return;
|
|
347
|
+
state.running = true;
|
|
348
|
+
state.paused = false;
|
|
349
|
+
state.detailed = !!opts.detailed;
|
|
350
|
+
if (opts.theme) state.currentTheme = opts.theme;
|
|
351
|
+
state.interval = opts.interval;
|
|
352
|
+
if (opts.exportDir) state.exportDir = opts.exportDir;
|
|
353
|
+
if (opts.alertCpu !== undefined) state.CPU_ALERT_PCT = opts.alertCpu;
|
|
354
|
+
if (opts.alertTemp !== undefined) state.TEMP_ALERT_C = opts.alertTemp;
|
|
355
|
+
if (opts.tempUnit) state.tempUnit = opts.tempUnit;
|
|
356
|
+
state.termWidth = process.stdout.columns || 80;
|
|
357
|
+
state.termHeight = process.stdout.rows || 24;
|
|
358
|
+
state.history.reset();
|
|
359
|
+
state.history.setMaxLen(Math.max(20, Math.min(200, state.termWidth - 30)));
|
|
325
360
|
|
|
326
361
|
const warmupPromise = doWarmup();
|
|
327
362
|
|
|
@@ -369,8 +404,9 @@ function killProcess(pidStr: string, signal: string = 'SIGTERM') {
|
|
|
369
404
|
return;
|
|
370
405
|
}
|
|
371
406
|
|
|
372
|
-
|
|
373
|
-
|
|
407
|
+
const mouse = (key as { mouse?: { x: number; y: number } }).mouse;
|
|
408
|
+
if (mouse) {
|
|
409
|
+
handleMouseClick(mouse.y, mouse.x);
|
|
374
410
|
return;
|
|
375
411
|
}
|
|
376
412
|
|
|
@@ -402,7 +438,7 @@ function killProcess(pidStr: string, signal: string = 'SIGTERM') {
|
|
|
402
438
|
return;
|
|
403
439
|
case '0': case '1': case '2': case '3': case '4':
|
|
404
440
|
case '5': case '6': case '7': case '8': case '9': {
|
|
405
|
-
const tabMap: Record<string,
|
|
441
|
+
const tabMap: Record<string, ActivePanel> = {
|
|
406
442
|
'0': 'disk', '1': 'cpu', '2': 'mem', '3': 'gpu', '4': 'power',
|
|
407
443
|
'5': 'battery', '6': 'thermal', '7': 'network', '8': 'packets', '9': 'tasks'
|
|
408
444
|
};
|
|
@@ -467,6 +503,12 @@ function killProcess(pidStr: string, signal: string = 'SIGTERM') {
|
|
|
467
503
|
state.detailed = !state.detailed;
|
|
468
504
|
setStatus(`Detailed sensor mode: ${state.detailed ? 'on' : 'off'}`);
|
|
469
505
|
break;
|
|
506
|
+
case 'T':
|
|
507
|
+
state.tempUnit = state.tempUnit === 'c' ? 'f' : 'c';
|
|
508
|
+
setStatus(`Temperature unit: ${state.tempUnit.toUpperCase()}`);
|
|
509
|
+
invalidateTableCache();
|
|
510
|
+
render();
|
|
511
|
+
break;
|
|
470
512
|
case 's':
|
|
471
513
|
const sortCycle: SortMode[] = ['cpu', 'mem', 'pid', 'user', 'command', 'state', 'threads', 'runtime'];
|
|
472
514
|
const curIdx = sortCycle.indexOf(state.sortMode);
|
|
@@ -498,7 +540,9 @@ function killProcess(pidStr: string, signal: string = 'SIGTERM') {
|
|
|
498
540
|
render();
|
|
499
541
|
break;
|
|
500
542
|
case 'f':
|
|
501
|
-
|
|
543
|
+
const fmtCycle: ExportFormat[] = ['json', 'csv', 'tsv', 'html', 'md'];
|
|
544
|
+
const curFmt = fmtCycle.indexOf(state.exportFormat);
|
|
545
|
+
state.exportFormat = fmtCycle[(curFmt + 1) % fmtCycle.length];
|
|
502
546
|
setStatus(`Export format: ${state.exportFormat}`);
|
|
503
547
|
render();
|
|
504
548
|
break;
|
|
@@ -515,7 +559,7 @@ function killProcess(pidStr: string, signal: string = 'SIGTERM') {
|
|
|
515
559
|
if (str && str.length === 1) {
|
|
516
560
|
const tabId = tabKeyToId(str);
|
|
517
561
|
if (tabId) {
|
|
518
|
-
state.activePanel = tabId;
|
|
562
|
+
state.activePanel = tabId as ActivePanel;
|
|
519
563
|
setStatus(`Panel: ${tabId.toUpperCase()}`);
|
|
520
564
|
render();
|
|
521
565
|
}
|
|
@@ -574,6 +618,7 @@ function killProcess(pidStr: string, signal: string = 'SIGTERM') {
|
|
|
574
618
|
}
|
|
575
619
|
|
|
576
620
|
export function stopLive() {
|
|
621
|
+
persistConfig();
|
|
577
622
|
if (state.intervalHandle) {
|
|
578
623
|
clearInterval(state.intervalHandle);
|
|
579
624
|
state.intervalHandle = null;
|