pyre-cli 5.0.0 → 5.1.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 +958 -81
- package/package.json +1 -1
- package/src/anomalies.ts +102 -0
- package/src/formatters/index.ts +3 -3
- package/src/formatters/output.ts +341 -8
- package/src/formatters/render.ts +53 -16
- package/src/formatters/types.ts +27 -16
- package/src/history.ts +57 -13
- package/src/index.ts +204 -11
- package/src/live/export.ts +64 -7
- package/src/live/input.ts +78 -34
- package/src/live/render.ts +114 -66
- package/src/live/state.ts +76 -70
- package/src/live/types.ts +4 -1
- package/src/monitors/collectors.ts +23 -1
- package/src/monitors/types.ts +1 -0
- package/src/state/config.ts +130 -0
package/dist/index.js
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import
|
|
5
|
-
import
|
|
4
|
+
import fs5 from "fs";
|
|
5
|
+
import os3 from "os";
|
|
6
|
+
import path3 from "path";
|
|
6
7
|
import readline2 from "readline";
|
|
7
8
|
import { Command } from "commander";
|
|
8
9
|
import chalk9 from "chalk";
|
|
9
10
|
|
|
10
11
|
// src/monitors/collectors.ts
|
|
11
12
|
import chalk from "chalk";
|
|
13
|
+
import os from "os";
|
|
12
14
|
|
|
13
15
|
// src/monitors/run.ts
|
|
14
16
|
import { exec } from "child_process";
|
|
@@ -65,6 +67,7 @@ function parseSuffix(s) {
|
|
|
65
67
|
// src/monitors/collectors.ts
|
|
66
68
|
var SP_TTL_MS = 1e4;
|
|
67
69
|
var NETSTAT_TTL_MS = 1e3;
|
|
70
|
+
var prevCpuTimes = null;
|
|
68
71
|
var ROUTE_TTL_MS = 5e3;
|
|
69
72
|
var SYSCTL_TTL_MS = 6e4;
|
|
70
73
|
var spGpuCache = null;
|
|
@@ -290,7 +293,25 @@ async function collectCpu() {
|
|
|
290
293
|
if (temps.cpu_die !== void 0) temperature = temps.cpu_die;
|
|
291
294
|
} catch {
|
|
292
295
|
}
|
|
293
|
-
|
|
296
|
+
let coreUsage = [];
|
|
297
|
+
try {
|
|
298
|
+
const cpus = os.cpus();
|
|
299
|
+
const current = cpus.map((cpu) => ({
|
|
300
|
+
total: cpu.times.user + cpu.times.nice + cpu.times.sys + cpu.times.idle + cpu.times.irq,
|
|
301
|
+
idle: cpu.times.idle
|
|
302
|
+
}));
|
|
303
|
+
if (prevCpuTimes) {
|
|
304
|
+
coreUsage = prevCpuTimes.map((prev, i) => {
|
|
305
|
+
const totalDelta = current[i].total - prev.total;
|
|
306
|
+
const idleDelta = current[i].idle - prev.idle;
|
|
307
|
+
if (totalDelta === 0) return 0;
|
|
308
|
+
return Math.round((totalDelta - idleDelta) / totalDelta * 100);
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
prevCpuTimes = current;
|
|
312
|
+
} catch {
|
|
313
|
+
}
|
|
314
|
+
return { brand, cores, physicalCores, frequency, usage, loadAvg, temperature, coreUsage };
|
|
294
315
|
}
|
|
295
316
|
async function collectMemory() {
|
|
296
317
|
const totalBytes = parseInt(await cachedSysctl("hw.memsize", "0")) || 0;
|
|
@@ -778,6 +799,18 @@ function cpuCard(data, contentWidth) {
|
|
|
778
799
|
gaugeRow("Usage", data.cpu.usage, contentWidth),
|
|
779
800
|
statRow("Load", data.cpu.loadAvg.map((l) => l.toFixed(2)).join(" "))
|
|
780
801
|
];
|
|
802
|
+
if (data.cpu.coreUsage && data.cpu.coreUsage.length) {
|
|
803
|
+
const slotW = 2;
|
|
804
|
+
const maxCores = Math.max(1, Math.floor((contentWidth - 10) / (slotW + 1)));
|
|
805
|
+
const cores = data.cpu.coreUsage.slice(0, maxCores);
|
|
806
|
+
const bars = cores.map((u) => {
|
|
807
|
+
const fill = Math.round(Math.min(100, u) / 100 * slotW);
|
|
808
|
+
const str = "\u2588".repeat(Math.max(0, fill)) + "\u2591".repeat(slotW - fill);
|
|
809
|
+
const color = u > 90 ? chalk3.red : u > 70 ? chalk3.yellow : chalk3.green;
|
|
810
|
+
return color(str);
|
|
811
|
+
});
|
|
812
|
+
lines.push(statRow("Cores", bars.join(" ")));
|
|
813
|
+
}
|
|
781
814
|
if (data.cpu.temperature) lines.push(statRow("Temp", formatTemp(data.cpu.temperature)));
|
|
782
815
|
return lines;
|
|
783
816
|
}
|
|
@@ -1115,6 +1148,15 @@ function activeDetailLines(data, activePanel, width, opts2 = {}) {
|
|
|
1115
1148
|
return null;
|
|
1116
1149
|
}
|
|
1117
1150
|
}
|
|
1151
|
+
function anomalyPanel(anomalies, width, theme) {
|
|
1152
|
+
const lines = [];
|
|
1153
|
+
for (const a of anomalies) {
|
|
1154
|
+
const color = a.severity === "critical" ? chalk3.red.bold : chalk3.yellow;
|
|
1155
|
+
const direction = a.zScore > 0 ? "\u2191 spike" : "\u2193 drop";
|
|
1156
|
+
lines.push(color(` \u26A0 ${a.metric}: ${a.value.toFixed(1)} (${direction}, \u03C3=${a.zScore.toFixed(1)})`));
|
|
1157
|
+
}
|
|
1158
|
+
return lines;
|
|
1159
|
+
}
|
|
1118
1160
|
function formatTable(data, opts2 = {}) {
|
|
1119
1161
|
const width = clampWidth(opts2.width);
|
|
1120
1162
|
const activePanel = opts2.activePanel || "grid";
|
|
@@ -1128,6 +1170,11 @@ function formatTable(data, opts2 = {}) {
|
|
|
1128
1170
|
const out = [];
|
|
1129
1171
|
out.push(...header(data, width, [], theme.border));
|
|
1130
1172
|
out.push("");
|
|
1173
|
+
const anomalies = opts2.anomalies ?? [];
|
|
1174
|
+
if (anomalies.length && activePanel === "grid") {
|
|
1175
|
+
out.push(...anomalyPanel(anomalies, width, theme));
|
|
1176
|
+
out.push("");
|
|
1177
|
+
}
|
|
1131
1178
|
out.push(tabBar(activePanel, width, theme));
|
|
1132
1179
|
out.push("");
|
|
1133
1180
|
if (activePanel !== "grid") {
|
|
@@ -1239,6 +1286,314 @@ function sparkline(values, opts2 = {}) {
|
|
|
1239
1286
|
function formatJson(data) {
|
|
1240
1287
|
return JSON.stringify(data, null, 2);
|
|
1241
1288
|
}
|
|
1289
|
+
function formatHtml(data) {
|
|
1290
|
+
const rows = (items) => items.map((i) => `<tr><td class="label">${escapeHtml(i.label)}</td><td class="value">${escapeHtml(i.value)}</td></tr>`).join("\n");
|
|
1291
|
+
const sections = [];
|
|
1292
|
+
sections.push(`<h2>System</h2>
|
|
1293
|
+
<table>
|
|
1294
|
+
${rows([
|
|
1295
|
+
{ label: "Hostname", value: data.header.hostname },
|
|
1296
|
+
{ label: "OS", value: data.header.os },
|
|
1297
|
+
{ label: "Uptime", value: data.header.uptime },
|
|
1298
|
+
{ label: "Timestamp", value: data.timestamp }
|
|
1299
|
+
]).trim()}
|
|
1300
|
+
</table>`);
|
|
1301
|
+
sections.push(`<h2>CPU</h2>
|
|
1302
|
+
<table>
|
|
1303
|
+
${rows([
|
|
1304
|
+
{ label: "Brand", value: data.cpu.brand },
|
|
1305
|
+
{ label: "Cores", value: `${data.cpu.physicalCores}/${data.cpu.cores}` },
|
|
1306
|
+
{ label: "Frequency", value: `${data.cpu.frequency} MHz` },
|
|
1307
|
+
{ label: "Usage", value: `${data.cpu.usage}%` },
|
|
1308
|
+
{ label: "Load Avg", value: data.cpu.loadAvg.map((l) => l.toFixed(2)).join(" ") },
|
|
1309
|
+
...data.cpu.temperature ? [{ label: "Temperature", value: `${data.cpu.temperature.toFixed(1)}\xB0C` }] : []
|
|
1310
|
+
]).trim()}
|
|
1311
|
+
</table>`);
|
|
1312
|
+
sections.push(`<h2>Memory</h2>
|
|
1313
|
+
<table>
|
|
1314
|
+
${rows([
|
|
1315
|
+
{ label: "Total", value: formatBytes(data.memory.total) },
|
|
1316
|
+
{ label: "Used", value: formatBytes(data.memory.used) },
|
|
1317
|
+
{ label: "Free", value: formatBytes(data.memory.free) },
|
|
1318
|
+
{ label: "Usage", value: `${data.memory.usagePercent}%` },
|
|
1319
|
+
{ label: "Swap", value: `${formatBytes(data.memory.swapUsed)} / ${formatBytes(data.memory.swapTotal)}` }
|
|
1320
|
+
]).trim()}
|
|
1321
|
+
</table>`);
|
|
1322
|
+
if (data.gpu) {
|
|
1323
|
+
sections.push(`<h2>GPU</h2>
|
|
1324
|
+
<table>
|
|
1325
|
+
${rows([
|
|
1326
|
+
{ label: "Model", value: data.gpu.model },
|
|
1327
|
+
{ label: "Memory", value: formatBytes(data.gpu.memory) },
|
|
1328
|
+
{ label: "Utilization", value: `${data.gpu.utilization}%` },
|
|
1329
|
+
...data.gpu.temperature ? [{ label: "Temperature", value: `${data.gpu.temperature.toFixed(1)}\xB0C` }] : [],
|
|
1330
|
+
{ label: "Processes", value: String(data.gpu.processes) }
|
|
1331
|
+
]).trim()}
|
|
1332
|
+
</table>`);
|
|
1333
|
+
}
|
|
1334
|
+
if (data.power) {
|
|
1335
|
+
const powerRows = [];
|
|
1336
|
+
if (data.power.cpuWatts !== void 0) powerRows.push({ label: "CPU", value: `${data.power.cpuWatts.toFixed(2)} W` });
|
|
1337
|
+
if (data.power.gpuWatts !== void 0) powerRows.push({ label: "GPU", value: `${data.power.gpuWatts.toFixed(2)} W` });
|
|
1338
|
+
if (data.power.combinedWatts !== void 0) powerRows.push({ label: "Combined", value: `${data.power.combinedWatts.toFixed(2)} W` });
|
|
1339
|
+
if (powerRows.length) {
|
|
1340
|
+
sections.push(`<h2>Power</h2>
|
|
1341
|
+
<table>
|
|
1342
|
+
${rows(powerRows).trim()}
|
|
1343
|
+
</table>`);
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
if (data.battery) {
|
|
1347
|
+
const battRows = [
|
|
1348
|
+
{ label: "Level", value: `${data.battery.level}%` },
|
|
1349
|
+
{ label: "State", value: data.battery.state },
|
|
1350
|
+
{ label: "Time Remaining", value: data.battery.timeRemaining },
|
|
1351
|
+
{ label: "Health", value: data.battery.health },
|
|
1352
|
+
{ label: "Power Source", value: data.battery.powerSource }
|
|
1353
|
+
];
|
|
1354
|
+
if (data.battery.condition) battRows.push({ label: "Condition", value: data.battery.condition });
|
|
1355
|
+
if (data.battery.maxCapacityPercent !== void 0) battRows.push({ label: "Max Capacity", value: `${data.battery.maxCapacityPercent}%` });
|
|
1356
|
+
if (data.battery.cycles !== void 0) battRows.push({ label: "Cycles", value: String(data.battery.cycles) });
|
|
1357
|
+
if (data.battery.estimatedTimeToEmpty) battRows.push({ label: "Est. Empty", value: data.battery.estimatedTimeToEmpty });
|
|
1358
|
+
if (data.battery.dischargeRatePerHour !== void 0) battRows.push({ label: "Discharge Rate", value: `${data.battery.dischargeRatePerHour}%/h` });
|
|
1359
|
+
sections.push(`<h2>Battery</h2>
|
|
1360
|
+
<table>
|
|
1361
|
+
${rows(battRows).trim()}
|
|
1362
|
+
</table>`);
|
|
1363
|
+
}
|
|
1364
|
+
sections.push(`<h2>Thermal</h2>
|
|
1365
|
+
<table>
|
|
1366
|
+
${rows([
|
|
1367
|
+
{ label: "State", value: data.thermal.state },
|
|
1368
|
+
{ label: "Pressure", value: ["Nominal", "Fair", "Serious", "Critical"][data.thermal.pressureLevel] || "Unknown" },
|
|
1369
|
+
...data.thermal.temperatures ? Object.entries(data.thermal.temperatures).flatMap(
|
|
1370
|
+
([k, v]) => v !== null && v !== void 0 ? [{ label: k.replace(/_/g, " "), value: `${v.toFixed(1)}\xB0C` }] : []
|
|
1371
|
+
) : []
|
|
1372
|
+
]).trim()}
|
|
1373
|
+
</table>`);
|
|
1374
|
+
sections.push(`<h2>Network</h2>
|
|
1375
|
+
<table>
|
|
1376
|
+
${rows([
|
|
1377
|
+
{ label: "Interface", value: `${data.network.interface} (${data.network.ip})` },
|
|
1378
|
+
{ label: "RX", value: `${formatBytes(data.network.rxBytes)} / ${data.network.rxPackets} pkt` },
|
|
1379
|
+
{ label: "TX", value: `${formatBytes(data.network.txBytes)} / ${data.network.txPackets} pkt` }
|
|
1380
|
+
]).trim()}
|
|
1381
|
+
</table>`);
|
|
1382
|
+
if (data.packets) {
|
|
1383
|
+
sections.push(`<h2>Packets</h2>
|
|
1384
|
+
<table>
|
|
1385
|
+
${rows([
|
|
1386
|
+
{ label: "Total", value: `${data.packets.totalPackets} pkt` },
|
|
1387
|
+
{ label: "RX", value: `${data.packets.rxPackets} pkt` },
|
|
1388
|
+
{ label: "TX", value: `${data.packets.txPackets} pkt` },
|
|
1389
|
+
{ label: "Connections", value: `${data.packets.connections} TCP` }
|
|
1390
|
+
]).trim()}
|
|
1391
|
+
</table>`);
|
|
1392
|
+
}
|
|
1393
|
+
if (data.tasks.length) {
|
|
1394
|
+
const taskRows = data.tasks.slice(0, 20).map((t) => ({
|
|
1395
|
+
label: `${t.pid}`,
|
|
1396
|
+
value: `${t.user.padEnd(10)} ${t.cpu.toFixed(1).padStart(6)}% ${t.mem.toFixed(1).padStart(6)}% ${t.state} ${t.command}`
|
|
1397
|
+
}));
|
|
1398
|
+
sections.push(`<h2>Tasks</h2>
|
|
1399
|
+
<table>
|
|
1400
|
+
${rows(taskRows).trim()}
|
|
1401
|
+
</table>`);
|
|
1402
|
+
}
|
|
1403
|
+
if (data.disk.length) {
|
|
1404
|
+
const diskRows = data.disk.map((d) => ({
|
|
1405
|
+
label: d.mountpoint,
|
|
1406
|
+
value: `${d.used} / ${d.size} (${d.capacity})`
|
|
1407
|
+
}));
|
|
1408
|
+
sections.push(`<h2>Disk</h2>
|
|
1409
|
+
<table>
|
|
1410
|
+
${rows(diskRows).trim()}
|
|
1411
|
+
</table>`);
|
|
1412
|
+
}
|
|
1413
|
+
if (data.processes.length) {
|
|
1414
|
+
const procRows = data.processes.slice(0, 50).map((p) => ({
|
|
1415
|
+
label: String(p.pid),
|
|
1416
|
+
value: `${p.user.padEnd(10)} ${p.cpu.toFixed(1).padStart(6)}% ${p.mem.toFixed(1).padStart(6)}% ${p.state} ${p.command}`
|
|
1417
|
+
}));
|
|
1418
|
+
sections.push(`<h2>Processes</h2>
|
|
1419
|
+
<table>
|
|
1420
|
+
${rows(procRows).trim()}
|
|
1421
|
+
</table>`);
|
|
1422
|
+
}
|
|
1423
|
+
return `<!DOCTYPE html>
|
|
1424
|
+
<html lang="en">
|
|
1425
|
+
<head>
|
|
1426
|
+
<meta charset="UTF-8">
|
|
1427
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1428
|
+
<title>pyre \u2014 ${data.header.hostname}</title>
|
|
1429
|
+
<style>
|
|
1430
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #1e1e2e; color: #cdd6f4; margin: 0; padding: 24px; }
|
|
1431
|
+
h1 { color: #ff6a39; margin-bottom: 4px; }
|
|
1432
|
+
.meta { color: #a6adc8; margin-bottom: 24px; }
|
|
1433
|
+
h2 { color: #89b4fa; margin-top: 28px; border-bottom: 1px solid #313244; padding-bottom: 6px; }
|
|
1434
|
+
table { width: 100%; border-collapse: collapse; margin-bottom: 16px; }
|
|
1435
|
+
td { padding: 6px 10px; border-bottom: 1px solid #313244; vertical-align: top; }
|
|
1436
|
+
td.label { color: #a6adc8; width: 22%; font-weight: 500; }
|
|
1437
|
+
td.value { color: #cdd6f4; }
|
|
1438
|
+
tr:last-child td { border-bottom: none; }
|
|
1439
|
+
</style>
|
|
1440
|
+
</head>
|
|
1441
|
+
<body>
|
|
1442
|
+
<h1>PYRE</h1>
|
|
1443
|
+
<div class="meta">${escapeHtml(data.header.hostname)} \xB7 ${escapeHtml(data.header.os)} \xB7 up ${escapeHtml(data.header.uptime)} \xB7 ${escapeHtml(data.timestamp)}</div>
|
|
1444
|
+
${sections.join("\n ")}
|
|
1445
|
+
</body>
|
|
1446
|
+
</html>`;
|
|
1447
|
+
}
|
|
1448
|
+
function formatMarkdown(data) {
|
|
1449
|
+
const lines = [];
|
|
1450
|
+
lines.push(`# pyre \u2014 ${data.header.hostname}`);
|
|
1451
|
+
lines.push("");
|
|
1452
|
+
lines.push(`**${data.header.os}** \xB7 up ${data.header.uptime} \xB7 ${data.timestamp}`);
|
|
1453
|
+
lines.push("");
|
|
1454
|
+
lines.push("## System");
|
|
1455
|
+
lines.push("");
|
|
1456
|
+
lines.push("| Property | Value |");
|
|
1457
|
+
lines.push("|---|---|");
|
|
1458
|
+
lines.push(`| Hostname | ${mdCell(data.header.hostname)} |`);
|
|
1459
|
+
lines.push(`| OS | ${mdCell(data.header.os)} |`);
|
|
1460
|
+
lines.push(`| Uptime | ${mdCell(data.header.uptime)} |`);
|
|
1461
|
+
lines.push("");
|
|
1462
|
+
lines.push("## CPU");
|
|
1463
|
+
lines.push("");
|
|
1464
|
+
lines.push("| Property | Value |");
|
|
1465
|
+
lines.push("|---|---|");
|
|
1466
|
+
lines.push(`| Brand | ${mdCell(data.cpu.brand)} |`);
|
|
1467
|
+
lines.push(`| Cores | ${mdCell(`${data.cpu.physicalCores}/${data.cpu.cores}`)} |`);
|
|
1468
|
+
lines.push(`| Frequency | ${mdCell(`${data.cpu.frequency} MHz`)} |`);
|
|
1469
|
+
lines.push(`| Usage | ${mdCell(`${data.cpu.usage}%`)} |`);
|
|
1470
|
+
lines.push(`| Load Avg | ${mdCell(data.cpu.loadAvg.map((l) => l.toFixed(2)).join(" "))} |`);
|
|
1471
|
+
if (data.cpu.temperature) lines.push(`| Temperature | ${mdCell(`${data.cpu.temperature.toFixed(1)}\xB0C`)} |`);
|
|
1472
|
+
lines.push("");
|
|
1473
|
+
lines.push("## Memory");
|
|
1474
|
+
lines.push("");
|
|
1475
|
+
lines.push("| Property | Value |");
|
|
1476
|
+
lines.push("|---|---|");
|
|
1477
|
+
lines.push(`| Total | ${mdCell(formatBytes(data.memory.total))} |`);
|
|
1478
|
+
lines.push(`| Used | ${mdCell(formatBytes(data.memory.used))} |`);
|
|
1479
|
+
lines.push(`| Free | ${mdCell(formatBytes(data.memory.free))} |`);
|
|
1480
|
+
lines.push(`| Usage | ${mdCell(`${data.memory.usagePercent}%`)} |`);
|
|
1481
|
+
lines.push(`| Swap | ${mdCell(`${formatBytes(data.memory.swapUsed)} / ${formatBytes(data.memory.swapTotal)}`)} |`);
|
|
1482
|
+
lines.push("");
|
|
1483
|
+
if (data.gpu) {
|
|
1484
|
+
lines.push("## GPU");
|
|
1485
|
+
lines.push("");
|
|
1486
|
+
lines.push("| Property | Value |");
|
|
1487
|
+
lines.push("|---|---|");
|
|
1488
|
+
lines.push(`| Model | ${mdCell(data.gpu.model)} |`);
|
|
1489
|
+
lines.push(`| Memory | ${mdCell(formatBytes(data.gpu.memory))} |`);
|
|
1490
|
+
lines.push(`| Utilization | ${mdCell(`${data.gpu.utilization}%`)} |`);
|
|
1491
|
+
if (data.gpu.temperature) lines.push(`| Temperature | ${mdCell(`${data.gpu.temperature.toFixed(1)}\xB0C`)} |`);
|
|
1492
|
+
lines.push(`| Processes | ${mdCell(String(data.gpu.processes))} |`);
|
|
1493
|
+
lines.push("");
|
|
1494
|
+
}
|
|
1495
|
+
if (data.power) {
|
|
1496
|
+
const pw = [];
|
|
1497
|
+
if (data.power.cpuWatts !== void 0) pw.push(`CPU ${data.power.cpuWatts.toFixed(2)} W`);
|
|
1498
|
+
if (data.power.gpuWatts !== void 0) pw.push(`GPU ${data.power.gpuWatts.toFixed(2)} W`);
|
|
1499
|
+
if (data.power.combinedWatts !== void 0) pw.push(`Combined ${data.power.combinedWatts.toFixed(2)} W`);
|
|
1500
|
+
if (pw.length) {
|
|
1501
|
+
lines.push("## Power");
|
|
1502
|
+
lines.push("");
|
|
1503
|
+
lines.push("| Metric | Value |");
|
|
1504
|
+
lines.push("|---|---|");
|
|
1505
|
+
if (data.power.cpuWatts !== void 0) lines.push(`| CPU | ${mdCell(`${data.power.cpuWatts.toFixed(2)} W`)} |`);
|
|
1506
|
+
if (data.power.gpuWatts !== void 0) lines.push(`| GPU | ${mdCell(`${data.power.gpuWatts.toFixed(2)} W`)} |`);
|
|
1507
|
+
if (data.power.combinedWatts !== void 0) lines.push(`| Combined | ${mdCell(`${data.power.combinedWatts.toFixed(2)} W`)} |`);
|
|
1508
|
+
lines.push("");
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
if (data.battery) {
|
|
1512
|
+
lines.push("## Battery");
|
|
1513
|
+
lines.push("");
|
|
1514
|
+
lines.push("| Property | Value |");
|
|
1515
|
+
lines.push("|---|---|");
|
|
1516
|
+
lines.push(`| Level | ${mdCell(`${data.battery.level}%`)} |`);
|
|
1517
|
+
lines.push(`| State | ${mdCell(data.battery.state)} |`);
|
|
1518
|
+
lines.push(`| Time Remaining | ${mdCell(data.battery.timeRemaining)} |`);
|
|
1519
|
+
lines.push(`| Health | ${mdCell(data.battery.health)} |`);
|
|
1520
|
+
lines.push(`| Power Source | ${mdCell(data.battery.powerSource)} |`);
|
|
1521
|
+
if (data.battery.condition) lines.push(`| Condition | ${mdCell(data.battery.condition)} |`);
|
|
1522
|
+
if (data.battery.maxCapacityPercent !== void 0) lines.push(`| Max Capacity | ${mdCell(`${data.battery.maxCapacityPercent}%`)} |`);
|
|
1523
|
+
if (data.battery.cycles !== void 0) lines.push(`| Cycles | ${mdCell(String(data.battery.cycles))} |`);
|
|
1524
|
+
if (data.battery.estimatedTimeToEmpty) lines.push(`| Est. Empty | ${mdCell(data.battery.estimatedTimeToEmpty)} |`);
|
|
1525
|
+
if (data.battery.dischargeRatePerHour !== void 0) lines.push(`| Discharge Rate | ${mdCell(`${data.battery.dischargeRatePerHour}%/h`)} |`);
|
|
1526
|
+
lines.push("");
|
|
1527
|
+
}
|
|
1528
|
+
lines.push("## Thermal");
|
|
1529
|
+
lines.push("");
|
|
1530
|
+
lines.push("| Property | Value |");
|
|
1531
|
+
lines.push("|---|---|");
|
|
1532
|
+
lines.push(`| State | ${mdCell(data.thermal.state)} |`);
|
|
1533
|
+
lines.push(`| Pressure | ${mdCell(["Nominal", "Fair", "Serious", "Critical"][data.thermal.pressureLevel] || "Unknown")} |`);
|
|
1534
|
+
if (data.thermal.temperatures) {
|
|
1535
|
+
for (const [k, v] of Object.entries(data.thermal.temperatures)) {
|
|
1536
|
+
if (v !== null && v !== void 0) lines.push(`| ${mdCell(k.replace(/_/g, " "))} | ${mdCell(`${v.toFixed(1)}\xB0C`)} |`);
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
lines.push("");
|
|
1540
|
+
lines.push("## Network");
|
|
1541
|
+
lines.push("");
|
|
1542
|
+
lines.push("| Property | Value |");
|
|
1543
|
+
lines.push("|---|---|");
|
|
1544
|
+
lines.push(`| Interface | ${mdCell(`${data.network.interface} (${data.network.ip})`)} |`);
|
|
1545
|
+
lines.push(`| RX | ${mdCell(`${formatBytes(data.network.rxBytes)} / ${data.network.rxPackets} pkt`)} |`);
|
|
1546
|
+
lines.push(`| TX | ${mdCell(`${formatBytes(data.network.txBytes)} / ${data.network.txPackets} pkt`)} |`);
|
|
1547
|
+
lines.push("");
|
|
1548
|
+
if (data.packets) {
|
|
1549
|
+
lines.push("## Packets");
|
|
1550
|
+
lines.push("");
|
|
1551
|
+
lines.push("| Property | Value |");
|
|
1552
|
+
lines.push("|---|---|");
|
|
1553
|
+
lines.push(`| Total | ${mdCell(`${data.packets.totalPackets} pkt`)} |`);
|
|
1554
|
+
lines.push(`| RX | ${mdCell(`${data.packets.rxPackets} pkt`)} |`);
|
|
1555
|
+
lines.push(`| TX | ${mdCell(`${data.packets.txPackets} pkt`)} |`);
|
|
1556
|
+
lines.push(`| Connections | ${mdCell(`${data.packets.connections} TCP`)} |`);
|
|
1557
|
+
lines.push("");
|
|
1558
|
+
}
|
|
1559
|
+
if (data.tasks.length) {
|
|
1560
|
+
lines.push("## Tasks");
|
|
1561
|
+
lines.push("");
|
|
1562
|
+
lines.push("| PID | User | CPU% | MEM% | State | Command |");
|
|
1563
|
+
lines.push("|---|---|---|---|---|---|");
|
|
1564
|
+
for (const t of data.tasks.slice(0, 20)) {
|
|
1565
|
+
lines.push(`| ${t.pid} | ${mdCell(t.user)} | ${t.cpu.toFixed(1)} | ${t.mem.toFixed(1)} | ${t.state} | ${mdCell(t.command)} |`);
|
|
1566
|
+
}
|
|
1567
|
+
lines.push("");
|
|
1568
|
+
}
|
|
1569
|
+
if (data.disk.length) {
|
|
1570
|
+
lines.push("## Disk");
|
|
1571
|
+
lines.push("");
|
|
1572
|
+
lines.push("| Mountpoint | Used / Size | Capacity |");
|
|
1573
|
+
lines.push("|---|---|---|");
|
|
1574
|
+
for (const d of data.disk) {
|
|
1575
|
+
lines.push(`| ${mdCell(d.mountpoint)} | ${mdCell(`${d.used} / ${d.size}`)} | ${mdCell(d.capacity)} |`);
|
|
1576
|
+
}
|
|
1577
|
+
lines.push("");
|
|
1578
|
+
}
|
|
1579
|
+
if (data.processes.length) {
|
|
1580
|
+
lines.push("## Processes");
|
|
1581
|
+
lines.push("");
|
|
1582
|
+
lines.push("| PID | User | CPU% | MEM% | State | Command |");
|
|
1583
|
+
lines.push("|---|---|---|---|---|---|");
|
|
1584
|
+
for (const p of data.processes.slice(0, 50)) {
|
|
1585
|
+
lines.push(`| ${p.pid} | ${mdCell(p.user)} | ${p.cpu.toFixed(1)} | ${p.mem.toFixed(1)} | ${p.state} | ${mdCell(p.command)} |`);
|
|
1586
|
+
}
|
|
1587
|
+
lines.push("");
|
|
1588
|
+
}
|
|
1589
|
+
return lines.join("\n");
|
|
1590
|
+
}
|
|
1591
|
+
function escapeHtml(s) {
|
|
1592
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
1593
|
+
}
|
|
1594
|
+
function mdCell(s) {
|
|
1595
|
+
return String(s).replace(/\|/g, "\\|");
|
|
1596
|
+
}
|
|
1242
1597
|
function formatCsv(data) {
|
|
1243
1598
|
const rows = [
|
|
1244
1599
|
["property", "key", "value"],
|
|
@@ -1247,6 +1602,9 @@ function formatCsv(data) {
|
|
|
1247
1602
|
["cpu", "loadAvg_1m", String(data.cpu.loadAvg[0])],
|
|
1248
1603
|
["cpu", "loadAvg_5m", String(data.cpu.loadAvg[1])],
|
|
1249
1604
|
["cpu", "loadAvg_15m", String(data.cpu.loadAvg[2])],
|
|
1605
|
+
["cpu", "cores", String(data.cpu.cores)],
|
|
1606
|
+
["cpu", "physicalCores", String(data.cpu.physicalCores)],
|
|
1607
|
+
["cpu", "frequency", String(data.cpu.frequency)],
|
|
1250
1608
|
["memory", "total", String(data.memory.total)],
|
|
1251
1609
|
["memory", "used", String(data.memory.used)],
|
|
1252
1610
|
["memory", "usagePercent", String(data.memory.usagePercent)],
|
|
@@ -1254,11 +1612,7 @@ function formatCsv(data) {
|
|
|
1254
1612
|
["memory", "swapTotal", String(data.memory.swapTotal)],
|
|
1255
1613
|
["thermal", "state", data.thermal.state],
|
|
1256
1614
|
["thermal", "cpuTempC", data.thermal.temperatures?.cpu_die ? String(data.thermal.temperatures.cpu_die) : ""],
|
|
1257
|
-
[
|
|
1258
|
-
"thermal",
|
|
1259
|
-
"cpuTempF",
|
|
1260
|
-
data.thermal.temperatures?.cpu_die != null ? String(celsiusToFahrenheit(data.thermal.temperatures.cpu_die)) : ""
|
|
1261
|
-
],
|
|
1615
|
+
["thermal", "cpuTempF", data.thermal.temperatures?.cpu_die != null ? String(celsiusToFahrenheit(data.thermal.temperatures.cpu_die)) : ""],
|
|
1262
1616
|
["network", "rxBytes", String(data.network.rxBytes)],
|
|
1263
1617
|
["network", "txBytes", String(data.network.txBytes)],
|
|
1264
1618
|
["network", "rxPackets", String(data.network.rxPackets)],
|
|
@@ -1279,6 +1633,9 @@ function formatCsv(data) {
|
|
|
1279
1633
|
["system", "uptime", data.header.uptime],
|
|
1280
1634
|
["system", "timestamp", data.timestamp]
|
|
1281
1635
|
];
|
|
1636
|
+
if (data.cpu.coreUsage && data.cpu.coreUsage.length) {
|
|
1637
|
+
data.cpu.coreUsage.forEach((u, i) => rows.push(["cpu", `coreUsage_${i}`, String(u)]));
|
|
1638
|
+
}
|
|
1282
1639
|
if (data.packets) {
|
|
1283
1640
|
rows.push(["packets", "totalPackets", String(data.packets.totalPackets)]);
|
|
1284
1641
|
rows.push(["packets", "rxPackets", String(data.packets.rxPackets)]);
|
|
@@ -1377,7 +1734,7 @@ function barGraph(values, bounds, width) {
|
|
|
1377
1734
|
import readline from "readline";
|
|
1378
1735
|
|
|
1379
1736
|
// src/history.ts
|
|
1380
|
-
var History = class {
|
|
1737
|
+
var History = class _History {
|
|
1381
1738
|
/** Maximum number of samples retained in the rolling window. */
|
|
1382
1739
|
maxLen;
|
|
1383
1740
|
/** Monotonically increasing version, bumped on every push. */
|
|
@@ -1414,6 +1771,46 @@ var History = class {
|
|
|
1414
1771
|
constructor(maxLen = 40) {
|
|
1415
1772
|
this.maxLen = maxLen;
|
|
1416
1773
|
}
|
|
1774
|
+
/**
|
|
1775
|
+
* Compute the arithmetic mean of a numeric array.
|
|
1776
|
+
* Returns 0 if the array is empty.
|
|
1777
|
+
*/
|
|
1778
|
+
static mean(arr) {
|
|
1779
|
+
if (!arr.length) return 0;
|
|
1780
|
+
return arr.reduce((s, v) => s + v, 0) / arr.length;
|
|
1781
|
+
}
|
|
1782
|
+
/**
|
|
1783
|
+
* Compute the population standard deviation of a numeric array.
|
|
1784
|
+
* Returns 0 if the array has fewer than 2 elements.
|
|
1785
|
+
*/
|
|
1786
|
+
static stdDev(arr) {
|
|
1787
|
+
if (arr.length < 2) return 0;
|
|
1788
|
+
const m = _History.mean(arr);
|
|
1789
|
+
const variance = arr.reduce((s, v) => s + (v - m) ** 2, 0) / arr.length;
|
|
1790
|
+
return Math.sqrt(variance);
|
|
1791
|
+
}
|
|
1792
|
+
/**
|
|
1793
|
+
* Compute the z-score of a value against a reference array.
|
|
1794
|
+
* Returns 0 when the array is empty or has zero std dev.
|
|
1795
|
+
*/
|
|
1796
|
+
static zScore(value, arr) {
|
|
1797
|
+
const m = _History.mean(arr);
|
|
1798
|
+
const sd = _History.stdDev(arr);
|
|
1799
|
+
if (sd === 0) return 0;
|
|
1800
|
+
return (value - m) / sd;
|
|
1801
|
+
}
|
|
1802
|
+
/**
|
|
1803
|
+
* Check whether a value is a statistical outlier against a
|
|
1804
|
+
* reference array.
|
|
1805
|
+
* @param value - The current reading
|
|
1806
|
+
* @param arr - The rolling history window
|
|
1807
|
+
* @param threshold - Z-score threshold (default 2.5)
|
|
1808
|
+
* @returns true if the value deviates from the baseline by more than `threshold` standard deviations
|
|
1809
|
+
*/
|
|
1810
|
+
static isAnomaly(value, arr, threshold = 2.5) {
|
|
1811
|
+
if (arr.length < 5) return false;
|
|
1812
|
+
return Math.abs(_History.zScore(value, arr)) > threshold;
|
|
1813
|
+
}
|
|
1417
1814
|
/**
|
|
1418
1815
|
* Push a new sample into the rolling window.
|
|
1419
1816
|
*
|
|
@@ -1491,20 +1888,31 @@ var History = class {
|
|
|
1491
1888
|
}
|
|
1492
1889
|
};
|
|
1493
1890
|
|
|
1494
|
-
// src/
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1891
|
+
// src/state/config.ts
|
|
1892
|
+
import fs from "fs";
|
|
1893
|
+
import os2 from "os";
|
|
1894
|
+
import path from "path";
|
|
1895
|
+
var CONFIG_DIR = path.join(os2.homedir(), ".config", "pyre");
|
|
1896
|
+
var CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
|
|
1897
|
+
var DEFAULT_CONFIG = {
|
|
1898
|
+
theme: "default",
|
|
1502
1899
|
interval: 2,
|
|
1503
|
-
showGraphs: true,
|
|
1504
|
-
graphMode: "spark",
|
|
1505
|
-
exportFormat: "json",
|
|
1506
1900
|
exportDir: "./pyre-exports",
|
|
1507
|
-
|
|
1901
|
+
detailed: false,
|
|
1902
|
+
sortMode: "cpu",
|
|
1903
|
+
treeView: false,
|
|
1904
|
+
graphMode: "spark",
|
|
1905
|
+
showGraphs: true,
|
|
1906
|
+
autoLog: false,
|
|
1907
|
+
mouseEnabled: true,
|
|
1908
|
+
cpuAlertPct: 90,
|
|
1909
|
+
tempAlertC: 95,
|
|
1910
|
+
notificationsEnabled: true,
|
|
1911
|
+
p2pPort: 9876,
|
|
1912
|
+
p2pPassword: "pyre",
|
|
1913
|
+
splashEnabled: true,
|
|
1914
|
+
splashColorScheme: "fire",
|
|
1915
|
+
splashAnimation: "classic",
|
|
1508
1916
|
visiblePanels: {
|
|
1509
1917
|
cpu: true,
|
|
1510
1918
|
mem: true,
|
|
@@ -1516,7 +1924,72 @@ var state = {
|
|
|
1516
1924
|
packets: true,
|
|
1517
1925
|
tasks: true,
|
|
1518
1926
|
disk: true,
|
|
1519
|
-
process: true
|
|
1927
|
+
process: true,
|
|
1928
|
+
tree: false
|
|
1929
|
+
}
|
|
1930
|
+
};
|
|
1931
|
+
function readConfig() {
|
|
1932
|
+
try {
|
|
1933
|
+
if (!fs.existsSync(CONFIG_FILE)) return { ...DEFAULT_CONFIG };
|
|
1934
|
+
const raw = fs.readFileSync(CONFIG_FILE, "utf-8");
|
|
1935
|
+
const parsed = JSON.parse(raw);
|
|
1936
|
+
return deepMerge(DEFAULT_CONFIG, parsed);
|
|
1937
|
+
} catch {
|
|
1938
|
+
return { ...DEFAULT_CONFIG };
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
function writeConfig(config3) {
|
|
1942
|
+
try {
|
|
1943
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
1944
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
1945
|
+
}
|
|
1946
|
+
const merged = deepMerge(readConfig(), config3);
|
|
1947
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(merged, null, 2) + "\n");
|
|
1948
|
+
} catch {
|
|
1949
|
+
}
|
|
1950
|
+
}
|
|
1951
|
+
function deepMerge(target, source) {
|
|
1952
|
+
const result = { ...target };
|
|
1953
|
+
for (const key of Object.keys(source)) {
|
|
1954
|
+
if (source[key] !== void 0 && source[key] !== null) {
|
|
1955
|
+
if (typeof source[key] === "object" && !Array.isArray(source[key]) && typeof target[key] === "object" && !Array.isArray(target[key])) {
|
|
1956
|
+
result[key] = deepMerge(target[key] || {}, source[key]);
|
|
1957
|
+
} else {
|
|
1958
|
+
result[key] = source[key];
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
return result;
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1965
|
+
// src/live/state.ts
|
|
1966
|
+
var SIGNAL_OPTIONS = ["SIGTERM", "SIGKILL", "SIGINT", "SIGHUP", "SIGSTOP", "SIGCONT"];
|
|
1967
|
+
var config = readConfig();
|
|
1968
|
+
var state = {
|
|
1969
|
+
intervalHandle: null,
|
|
1970
|
+
uiIntervalHandle: null,
|
|
1971
|
+
running: false,
|
|
1972
|
+
paused: false,
|
|
1973
|
+
detailed: config.detailed,
|
|
1974
|
+
interval: config.interval,
|
|
1975
|
+
showGraphs: config.showGraphs,
|
|
1976
|
+
graphMode: config.graphMode,
|
|
1977
|
+
exportFormat: "json",
|
|
1978
|
+
tempUnit: "c",
|
|
1979
|
+
exportDir: config.exportDir,
|
|
1980
|
+
currentTheme: config.theme,
|
|
1981
|
+
visiblePanels: {
|
|
1982
|
+
cpu: config.visiblePanels.cpu,
|
|
1983
|
+
mem: config.visiblePanels.mem,
|
|
1984
|
+
gpu: config.visiblePanels.gpu,
|
|
1985
|
+
power: config.visiblePanels.power,
|
|
1986
|
+
battery: config.visiblePanels.battery,
|
|
1987
|
+
thermal: config.visiblePanels.thermal,
|
|
1988
|
+
network: config.visiblePanels.network,
|
|
1989
|
+
packets: config.visiblePanels.packets,
|
|
1990
|
+
tasks: config.visiblePanels.tasks,
|
|
1991
|
+
disk: config.visiblePanels.disk,
|
|
1992
|
+
process: config.visiblePanels.process
|
|
1520
1993
|
},
|
|
1521
1994
|
logging: false,
|
|
1522
1995
|
logStream: null,
|
|
@@ -1527,18 +2000,23 @@ var state = {
|
|
|
1527
2000
|
keypressHandler: null,
|
|
1528
2001
|
termWidth: process.stdout.columns || 80,
|
|
1529
2002
|
termHeight: process.stdout.rows || 24,
|
|
1530
|
-
sortMode:
|
|
2003
|
+
sortMode: config.sortMode,
|
|
1531
2004
|
processFilter: "",
|
|
1532
2005
|
inputMode: null,
|
|
1533
2006
|
inputBuffer: "",
|
|
1534
2007
|
targetPid: "",
|
|
1535
2008
|
selectedSignal: "SIGTERM",
|
|
1536
|
-
treeView:
|
|
2009
|
+
treeView: config.treeView,
|
|
1537
2010
|
SIGNAL_OPTIONS,
|
|
1538
2011
|
customizerIndex: 0,
|
|
1539
2012
|
CUSTOMIZER_OPTIONS: [
|
|
1540
2013
|
"Theme",
|
|
1541
2014
|
"Graph Mode",
|
|
2015
|
+
"Splash Screen",
|
|
2016
|
+
"Splash Color",
|
|
2017
|
+
"Splash Animation",
|
|
2018
|
+
"Notifications",
|
|
2019
|
+
"Temperature Unit",
|
|
1542
2020
|
"Toggle CPU",
|
|
1543
2021
|
"Toggle Memory",
|
|
1544
2022
|
"Toggle GPU",
|
|
@@ -1550,16 +2028,14 @@ var state = {
|
|
|
1550
2028
|
"Toggle Tasks",
|
|
1551
2029
|
"Toggle Disk",
|
|
1552
2030
|
"Toggle Processes",
|
|
1553
|
-
"Toggle Tree View"
|
|
1554
|
-
"Splash Screen",
|
|
1555
|
-
"Splash Color",
|
|
1556
|
-
"Splash Animation"
|
|
2031
|
+
"Toggle Tree View"
|
|
1557
2032
|
],
|
|
1558
|
-
CPU_ALERT_PCT:
|
|
1559
|
-
TEMP_ALERT_C:
|
|
2033
|
+
CPU_ALERT_PCT: config.cpuAlertPct,
|
|
2034
|
+
TEMP_ALERT_C: config.tempAlertC,
|
|
2035
|
+
notificationsEnabled: config.notificationsEnabled,
|
|
1560
2036
|
alerted: false,
|
|
1561
2037
|
activePanel: "grid",
|
|
1562
|
-
mouseEnabled:
|
|
2038
|
+
mouseEnabled: config.mouseEnabled,
|
|
1563
2039
|
PANEL_TABS: [
|
|
1564
2040
|
{ id: "cpu", label: "CPU", key: "1" },
|
|
1565
2041
|
{ id: "mem", label: "Memory", key: "2" },
|
|
@@ -1578,11 +2054,11 @@ var state = {
|
|
|
1578
2054
|
p2pBind: "",
|
|
1579
2055
|
p2pServer: null,
|
|
1580
2056
|
p2pServerRunning: false,
|
|
1581
|
-
p2pPort:
|
|
1582
|
-
p2pPassword:
|
|
1583
|
-
splashEnabled:
|
|
1584
|
-
splashColorScheme:
|
|
1585
|
-
splashAnimation:
|
|
2057
|
+
p2pPort: config.p2pPort,
|
|
2058
|
+
p2pPassword: config.p2pPassword,
|
|
2059
|
+
splashEnabled: config.splashEnabled,
|
|
2060
|
+
splashColorScheme: config.splashColorScheme,
|
|
2061
|
+
splashAnimation: config.splashAnimation
|
|
1586
2062
|
};
|
|
1587
2063
|
function setStatus(msg, ms = 3e3) {
|
|
1588
2064
|
state.statusMessage = msg;
|
|
@@ -1610,10 +2086,10 @@ function getToggleKey(opt) {
|
|
|
1610
2086
|
}
|
|
1611
2087
|
|
|
1612
2088
|
// src/live/export.ts
|
|
1613
|
-
import
|
|
1614
|
-
import
|
|
2089
|
+
import fs2 from "fs";
|
|
2090
|
+
import path2 from "path";
|
|
1615
2091
|
function ensureDir(dir) {
|
|
1616
|
-
if (!
|
|
2092
|
+
if (!fs2.existsSync(dir)) fs2.mkdirSync(dir, { recursive: true });
|
|
1617
2093
|
}
|
|
1618
2094
|
function timestampForFile() {
|
|
1619
2095
|
return (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
@@ -1625,9 +2101,27 @@ function exportSnapshot() {
|
|
|
1625
2101
|
}
|
|
1626
2102
|
try {
|
|
1627
2103
|
ensureDir(state.exportDir);
|
|
1628
|
-
const
|
|
1629
|
-
const
|
|
1630
|
-
|
|
2104
|
+
const ext = state.exportFormat === "json" ? "json" : state.exportFormat === "csv" ? "csv" : state.exportFormat === "tsv" ? "tsv" : state.exportFormat === "html" ? "html" : "md";
|
|
2105
|
+
const file = path2.join(state.exportDir, `pyre-${timestampForFile()}.${ext}`);
|
|
2106
|
+
let content;
|
|
2107
|
+
switch (state.exportFormat) {
|
|
2108
|
+
case "json":
|
|
2109
|
+
content = formatJson(state.lastData);
|
|
2110
|
+
break;
|
|
2111
|
+
case "csv":
|
|
2112
|
+
content = formatCsv(state.lastData);
|
|
2113
|
+
break;
|
|
2114
|
+
case "tsv":
|
|
2115
|
+
content = formatTsv(state.lastData);
|
|
2116
|
+
break;
|
|
2117
|
+
case "html":
|
|
2118
|
+
content = formatHtml(state.lastData);
|
|
2119
|
+
break;
|
|
2120
|
+
case "md":
|
|
2121
|
+
content = formatMarkdown(state.lastData);
|
|
2122
|
+
break;
|
|
2123
|
+
}
|
|
2124
|
+
fs2.writeFileSync(file, content);
|
|
1631
2125
|
setStatus(`Exported snapshot -> ${file}`);
|
|
1632
2126
|
} catch (err) {
|
|
1633
2127
|
setStatus(`Export failed: ${err.message}`);
|
|
@@ -1636,8 +2130,9 @@ function exportSnapshot() {
|
|
|
1636
2130
|
function startLogging() {
|
|
1637
2131
|
try {
|
|
1638
2132
|
ensureDir(state.exportDir);
|
|
1639
|
-
|
|
1640
|
-
|
|
2133
|
+
rotateLogs();
|
|
2134
|
+
const file = path2.join(state.exportDir, `pyre-log-${timestampForFile()}.csv`);
|
|
2135
|
+
state.logStream = fs2.createWriteStream(file, { flags: "a" });
|
|
1641
2136
|
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");
|
|
1642
2137
|
state.logging = true;
|
|
1643
2138
|
setStatus(`Logging every tick -> ${file}`);
|
|
@@ -1645,6 +2140,52 @@ function startLogging() {
|
|
|
1645
2140
|
setStatus(`Logging failed to start: ${err.message}`);
|
|
1646
2141
|
}
|
|
1647
2142
|
}
|
|
2143
|
+
var MAX_LOG_AGE_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
2144
|
+
var MAX_LOG_COUNT = 20;
|
|
2145
|
+
var MAX_LOG_BYTES = 50 * 1024 * 1024;
|
|
2146
|
+
function rotateLogs() {
|
|
2147
|
+
try {
|
|
2148
|
+
const dir = state.exportDir;
|
|
2149
|
+
if (!fs2.existsSync(dir)) return;
|
|
2150
|
+
const files = fs2.readdirSync(dir).filter((f) => f.startsWith("pyre-log-") && f.endsWith(".csv")).map((f) => ({ name: f, path: path2.join(dir, f), mtime: fs2.statSync(path2.join(dir, f)).mtimeMs, size: fs2.statSync(path2.join(dir, f)).size })).sort((a, b) => a.mtime - b.mtime);
|
|
2151
|
+
const now = Date.now();
|
|
2152
|
+
for (const f of files) {
|
|
2153
|
+
if (now - f.mtime > MAX_LOG_AGE_MS) {
|
|
2154
|
+
try {
|
|
2155
|
+
fs2.unlinkSync(f.path);
|
|
2156
|
+
} catch {
|
|
2157
|
+
}
|
|
2158
|
+
}
|
|
2159
|
+
}
|
|
2160
|
+
const remaining = fs2.readdirSync(dir).filter((f) => f.startsWith("pyre-log-") && f.endsWith(".csv"));
|
|
2161
|
+
if (remaining.length > MAX_LOG_COUNT) {
|
|
2162
|
+
const toDelete = remaining.sort().slice(0, remaining.length - MAX_LOG_COUNT);
|
|
2163
|
+
for (const f of toDelete) {
|
|
2164
|
+
try {
|
|
2165
|
+
fs2.unlinkSync(path2.join(dir, f));
|
|
2166
|
+
} catch {
|
|
2167
|
+
}
|
|
2168
|
+
}
|
|
2169
|
+
}
|
|
2170
|
+
let totalSize = 0;
|
|
2171
|
+
for (const f of remaining) {
|
|
2172
|
+
totalSize += fs2.statSync(path2.join(dir, f)).size;
|
|
2173
|
+
}
|
|
2174
|
+
if (totalSize > MAX_LOG_BYTES) {
|
|
2175
|
+
const sorted = remaining.sort();
|
|
2176
|
+
for (const f of sorted) {
|
|
2177
|
+
const sz = fs2.statSync(path2.join(dir, f)).size;
|
|
2178
|
+
try {
|
|
2179
|
+
fs2.unlinkSync(path2.join(dir, f));
|
|
2180
|
+
} catch {
|
|
2181
|
+
}
|
|
2182
|
+
totalSize -= sz;
|
|
2183
|
+
if (totalSize <= MAX_LOG_BYTES * 0.8) break;
|
|
2184
|
+
}
|
|
2185
|
+
}
|
|
2186
|
+
} catch {
|
|
2187
|
+
}
|
|
2188
|
+
}
|
|
1648
2189
|
function stopLogging() {
|
|
1649
2190
|
state.logging = false;
|
|
1650
2191
|
state.logStream?.end();
|
|
@@ -1657,7 +2198,10 @@ function toggleLogging() {
|
|
|
1657
2198
|
}
|
|
1658
2199
|
function writeLogRow(data) {
|
|
1659
2200
|
if (!state.logging || !state.logStream) return;
|
|
1660
|
-
|
|
2201
|
+
let temp = data.cpu.temperature ?? data.thermal.temperatures?.cpu_die ?? "";
|
|
2202
|
+
if (temp !== "" && state.tempUnit === "f") {
|
|
2203
|
+
temp = (temp * 9 / 5 + 32).toFixed(1);
|
|
2204
|
+
}
|
|
1661
2205
|
const rxPackets = data.network.rxPackets ?? 0;
|
|
1662
2206
|
const txPackets = data.network.txPackets ?? 0;
|
|
1663
2207
|
const connections = data.network.connections ?? 0;
|
|
@@ -1668,7 +2212,101 @@ function writeLogRow(data) {
|
|
|
1668
2212
|
}
|
|
1669
2213
|
|
|
1670
2214
|
// src/live/render.ts
|
|
2215
|
+
import { execFile } from "child_process";
|
|
1671
2216
|
import chalk5 from "chalk";
|
|
2217
|
+
|
|
2218
|
+
// src/anomalies.ts
|
|
2219
|
+
var ZSCORE_THRESHOLD = 2.5;
|
|
2220
|
+
var CRITICAL_ZSCORE = 3.5;
|
|
2221
|
+
var MIN_SAMPLES = 5;
|
|
2222
|
+
function detectAnomalies(data, history) {
|
|
2223
|
+
const alerts = [];
|
|
2224
|
+
const cpuZ = History.zScore(data.cpu.usage, history.cpuUsage);
|
|
2225
|
+
if (Math.abs(cpuZ) > ZSCORE_THRESHOLD && history.cpuUsage.length >= MIN_SAMPLES) {
|
|
2226
|
+
alerts.push({
|
|
2227
|
+
metric: "CPU",
|
|
2228
|
+
value: data.cpu.usage,
|
|
2229
|
+
zScore: cpuZ,
|
|
2230
|
+
severity: Math.abs(cpuZ) > CRITICAL_ZSCORE ? "critical" : "warning"
|
|
2231
|
+
});
|
|
2232
|
+
}
|
|
2233
|
+
const memZ = History.zScore(data.memory.usagePercent, history.memUsage);
|
|
2234
|
+
if (Math.abs(memZ) > ZSCORE_THRESHOLD && history.memUsage.length >= MIN_SAMPLES) {
|
|
2235
|
+
alerts.push({
|
|
2236
|
+
metric: "Memory",
|
|
2237
|
+
value: data.memory.usagePercent,
|
|
2238
|
+
zScore: memZ,
|
|
2239
|
+
severity: Math.abs(memZ) > CRITICAL_ZSCORE ? "critical" : "warning"
|
|
2240
|
+
});
|
|
2241
|
+
}
|
|
2242
|
+
if (data.cpu.temperature !== void 0) {
|
|
2243
|
+
const tempZ = History.zScore(data.cpu.temperature, history.temp);
|
|
2244
|
+
if (Math.abs(tempZ) > ZSCORE_THRESHOLD && history.temp.length >= MIN_SAMPLES) {
|
|
2245
|
+
alerts.push({
|
|
2246
|
+
metric: "Temp",
|
|
2247
|
+
value: data.cpu.temperature,
|
|
2248
|
+
zScore: tempZ,
|
|
2249
|
+
severity: Math.abs(tempZ) > CRITICAL_ZSCORE ? "critical" : "warning"
|
|
2250
|
+
});
|
|
2251
|
+
}
|
|
2252
|
+
}
|
|
2253
|
+
const netRxZ = History.zScore(data.network.rxBytes, history.netRxRate);
|
|
2254
|
+
if (Math.abs(netRxZ) > ZSCORE_THRESHOLD && history.netRxRate.length >= MIN_SAMPLES) {
|
|
2255
|
+
alerts.push({
|
|
2256
|
+
metric: "Net RX",
|
|
2257
|
+
value: data.network.rxBytes,
|
|
2258
|
+
zScore: netRxZ,
|
|
2259
|
+
severity: Math.abs(netRxZ) > CRITICAL_ZSCORE ? "critical" : "warning"
|
|
2260
|
+
});
|
|
2261
|
+
}
|
|
2262
|
+
const netTxZ = History.zScore(data.network.txBytes, history.netTxRate);
|
|
2263
|
+
if (Math.abs(netTxZ) > ZSCORE_THRESHOLD && history.netTxRate.length >= MIN_SAMPLES) {
|
|
2264
|
+
alerts.push({
|
|
2265
|
+
metric: "Net TX",
|
|
2266
|
+
value: data.network.txBytes,
|
|
2267
|
+
zScore: netTxZ,
|
|
2268
|
+
severity: Math.abs(netTxZ) > CRITICAL_ZSCORE ? "critical" : "warning"
|
|
2269
|
+
});
|
|
2270
|
+
}
|
|
2271
|
+
if (data.power?.combinedWatts !== void 0) {
|
|
2272
|
+
const powerZ = History.zScore(data.power.combinedWatts, history.powerWatts);
|
|
2273
|
+
if (Math.abs(powerZ) > ZSCORE_THRESHOLD && history.powerWatts.length >= MIN_SAMPLES) {
|
|
2274
|
+
alerts.push({
|
|
2275
|
+
metric: "Power",
|
|
2276
|
+
value: data.power.combinedWatts,
|
|
2277
|
+
zScore: powerZ,
|
|
2278
|
+
severity: Math.abs(powerZ) > CRITICAL_ZSCORE ? "critical" : "warning"
|
|
2279
|
+
});
|
|
2280
|
+
}
|
|
2281
|
+
}
|
|
2282
|
+
if (data.gpu) {
|
|
2283
|
+
const gpuZ = History.zScore(data.gpu.utilization, history.gpuUtil);
|
|
2284
|
+
if (Math.abs(gpuZ) > ZSCORE_THRESHOLD && history.gpuUtil.length >= MIN_SAMPLES) {
|
|
2285
|
+
alerts.push({
|
|
2286
|
+
metric: "GPU",
|
|
2287
|
+
value: data.gpu.utilization,
|
|
2288
|
+
zScore: gpuZ,
|
|
2289
|
+
severity: Math.abs(gpuZ) > CRITICAL_ZSCORE ? "critical" : "warning"
|
|
2290
|
+
});
|
|
2291
|
+
}
|
|
2292
|
+
}
|
|
2293
|
+
return alerts;
|
|
2294
|
+
}
|
|
2295
|
+
|
|
2296
|
+
// src/live/render.ts
|
|
2297
|
+
function formatTempForUnit(c) {
|
|
2298
|
+
if (state.tempUnit === "f") {
|
|
2299
|
+
const f = c * (9 / 5) + 32;
|
|
2300
|
+
return `${f.toFixed(1)}\xB0F`;
|
|
2301
|
+
}
|
|
2302
|
+
return `${c.toFixed(1)}\xB0C`;
|
|
2303
|
+
}
|
|
2304
|
+
function applyTempUnit(text) {
|
|
2305
|
+
if (state.tempUnit === "f") {
|
|
2306
|
+
return text.replace(/([\d.]+)°C \/ ([\d.]+)°F/g, "$2\xB0F");
|
|
2307
|
+
}
|
|
2308
|
+
return text.replace(/([\d.]+)°C \/ ([\d.]+)°F/g, "$1\xB0C");
|
|
2309
|
+
}
|
|
1672
2310
|
var _cachedTable = "";
|
|
1673
2311
|
var _cachedTableData = null;
|
|
1674
2312
|
var _cachedTableParams = "";
|
|
@@ -1754,6 +2392,11 @@ function renderCustomizerOverlay() {
|
|
|
1754
2392
|
lines.push(`${prefix}${opt}: ${chalk5.bold.green(state.splashColorScheme)} [fire, ocean, forest, purple, monochrome]`);
|
|
1755
2393
|
} else if (opt === "Splash Animation") {
|
|
1756
2394
|
lines.push(`${prefix}${opt}: ${chalk5.bold.green(state.splashAnimation)} [classic, wave, sparks]`);
|
|
2395
|
+
} else if (opt === "Notifications") {
|
|
2396
|
+
const status = state.notificationsEnabled ? chalk5.green("[ON]") : chalk5.red("[OFF]");
|
|
2397
|
+
lines.push(`${prefix}${opt}: ${status}`);
|
|
2398
|
+
} else if (opt === "Temperature Unit") {
|
|
2399
|
+
lines.push(`${prefix}${opt}: ${chalk5.bold.green(state.tempUnit.toUpperCase())} [C, F]`);
|
|
1757
2400
|
} else {
|
|
1758
2401
|
const toggleKey = getToggleKey(opt);
|
|
1759
2402
|
const isVisible = toggleKey ? toggleKey === "tree" ? state.treeView : state.visiblePanels[toggleKey] !== false : true;
|
|
@@ -1789,7 +2432,10 @@ function render() {
|
|
|
1789
2432
|
const lines = [];
|
|
1790
2433
|
const theme = THEMES[state.currentTheme] || THEMES.default;
|
|
1791
2434
|
const params = tableCacheParams();
|
|
1792
|
-
const
|
|
2435
|
+
const anomalies = detectAnomalies(state.lastData, state.history);
|
|
2436
|
+
const anomalyCacheKey = anomalies.map((a) => `${a.metric}:${a.zScore.toFixed(2)}`).join(",");
|
|
2437
|
+
const cacheParams = params + "|" + anomalyCacheKey;
|
|
2438
|
+
const tableStr = _tableDataDirty || state.lastData !== _cachedTableData || cacheParams !== _cachedTableParams ? (() => {
|
|
1793
2439
|
const out = formatTable(state.lastData, {
|
|
1794
2440
|
width: state.termWidth,
|
|
1795
2441
|
sortBy: state.sortMode,
|
|
@@ -1798,13 +2444,15 @@ function render() {
|
|
|
1798
2444
|
theme: state.currentTheme,
|
|
1799
2445
|
visible: state.visiblePanels,
|
|
1800
2446
|
treeView: state.treeView,
|
|
1801
|
-
activePanel: state.activePanel
|
|
2447
|
+
activePanel: state.activePanel,
|
|
2448
|
+
anomalies,
|
|
2449
|
+
tempUnit: state.tempUnit
|
|
1802
2450
|
});
|
|
1803
2451
|
_cachedTableData = state.lastData;
|
|
1804
|
-
_cachedTableParams =
|
|
1805
|
-
_cachedTable = out;
|
|
2452
|
+
_cachedTableParams = cacheParams;
|
|
2453
|
+
_cachedTable = applyTempUnit(out);
|
|
1806
2454
|
_tableDataDirty = false;
|
|
1807
|
-
return
|
|
2455
|
+
return _cachedTable;
|
|
1808
2456
|
})() : _cachedTable;
|
|
1809
2457
|
const bodyLines = [];
|
|
1810
2458
|
if (state.activePanel === "p2p") {
|
|
@@ -1818,8 +2466,8 @@ function render() {
|
|
|
1818
2466
|
const out = formatGraphs(state.history, state.termWidth, state.currentTheme, state.graphMode);
|
|
1819
2467
|
_cachedGraphsVersion = state.history.version;
|
|
1820
2468
|
_cachedGraphsParams = graphsParams;
|
|
1821
|
-
_cachedGraphs = out;
|
|
1822
|
-
return
|
|
2469
|
+
_cachedGraphs = applyTempUnit(out);
|
|
2470
|
+
return _cachedGraphs;
|
|
1823
2471
|
})() : _cachedGraphs;
|
|
1824
2472
|
if (state.showGraphs && state.activePanel !== "p2p") {
|
|
1825
2473
|
bodyLines.push("");
|
|
@@ -1867,6 +2515,7 @@ function footerLine() {
|
|
|
1867
2515
|
["b", `graph:${state.graphMode}`],
|
|
1868
2516
|
["t", state.treeView ? "flat" : "tree"],
|
|
1869
2517
|
["d", state.detailed ? "basic" : "detailed"],
|
|
2518
|
+
["T", `temp:${state.tempUnit}`],
|
|
1870
2519
|
["p", state.paused ? "resume" : "pause"]
|
|
1871
2520
|
];
|
|
1872
2521
|
const row2 = [
|
|
@@ -1893,15 +2542,32 @@ function footerLine() {
|
|
|
1893
2542
|
${str2} ${badgeStr}` : `${str1}
|
|
1894
2543
|
${str2}`;
|
|
1895
2544
|
}
|
|
2545
|
+
function sendNotification(title, message) {
|
|
2546
|
+
if (!state.notificationsEnabled) return;
|
|
2547
|
+
execFile("osascript", ["-e", `display notification "${message}" with title "${title}"`], (err) => {
|
|
2548
|
+
if (err) {
|
|
2549
|
+
}
|
|
2550
|
+
});
|
|
2551
|
+
}
|
|
1896
2552
|
function checkAlerts(data) {
|
|
1897
2553
|
const temp = data.cpu.temperature ?? data.thermal.temperatures?.cpu_die ?? null;
|
|
1898
2554
|
const hot = data.cpu.usage >= state.CPU_ALERT_PCT || temp !== null && temp >= state.TEMP_ALERT_C;
|
|
1899
|
-
|
|
2555
|
+
const anomalies = detectAnomalies(data, state.history);
|
|
2556
|
+
const hasCriticalAnomaly = anomalies.some((a) => a.severity === "critical");
|
|
2557
|
+
const hasWarningAnomaly = anomalies.some((a) => a.severity === "warning");
|
|
2558
|
+
const anomalyTriggered = hasCriticalAnomaly || hasWarningAnomaly;
|
|
2559
|
+
if ((hot || anomalyTriggered) && !state.alerted) {
|
|
1900
2560
|
state.alerted = true;
|
|
1901
2561
|
process.stdout.write("\x07");
|
|
1902
|
-
const
|
|
1903
|
-
|
|
1904
|
-
|
|
2562
|
+
const reasons = [];
|
|
2563
|
+
if (data.cpu.usage >= state.CPU_ALERT_PCT) reasons.push(`CPU at ${data.cpu.usage}%`);
|
|
2564
|
+
if (temp !== null && temp >= state.TEMP_ALERT_C) reasons.push(`Temp at ${formatTempForUnit(temp)}`);
|
|
2565
|
+
for (const a of anomalies) {
|
|
2566
|
+
reasons.push(`${a.metric} anomaly (\u03C3=${a.zScore.toFixed(1)})`);
|
|
2567
|
+
}
|
|
2568
|
+
setStatus(chalk5.red(`\u26A0 Alert: ${reasons.join("; ")}`), 5e3);
|
|
2569
|
+
sendNotification("pyre Alert", reasons.join("; "));
|
|
2570
|
+
} else if (!hot && !anomalyTriggered) {
|
|
1905
2571
|
state.alerted = false;
|
|
1906
2572
|
}
|
|
1907
2573
|
}
|
|
@@ -1909,7 +2575,7 @@ function checkAlerts(data) {
|
|
|
1909
2575
|
// src/p2p/server.ts
|
|
1910
2576
|
import net from "net";
|
|
1911
2577
|
import tls from "tls";
|
|
1912
|
-
import
|
|
2578
|
+
import fs3 from "fs";
|
|
1913
2579
|
import crypto2 from "crypto";
|
|
1914
2580
|
import chalk6 from "chalk";
|
|
1915
2581
|
|
|
@@ -2054,9 +2720,9 @@ var P2PServer = class {
|
|
|
2054
2720
|
async start() {
|
|
2055
2721
|
if (this.options.auditLog) {
|
|
2056
2722
|
try {
|
|
2057
|
-
|
|
2723
|
+
fs3.mkdirSync(this.options.auditLog, { recursive: true });
|
|
2058
2724
|
const logFile = `${this.options.auditLog}/p2p-audit.log`;
|
|
2059
|
-
this.auditStream =
|
|
2725
|
+
this.auditStream = fs3.createWriteStream(logFile, { flags: "a" });
|
|
2060
2726
|
} catch {
|
|
2061
2727
|
this.log(chalk6.yellow("Warning: could not open audit log"));
|
|
2062
2728
|
}
|
|
@@ -2065,11 +2731,11 @@ var P2PServer = class {
|
|
|
2065
2731
|
const useTLS = !!(this.options.tlsCert && this.options.tlsKey);
|
|
2066
2732
|
if (useTLS) {
|
|
2067
2733
|
const tlsOptions = {
|
|
2068
|
-
key:
|
|
2069
|
-
cert:
|
|
2734
|
+
key: fs3.readFileSync(this.options.tlsKey),
|
|
2735
|
+
cert: fs3.readFileSync(this.options.tlsCert)
|
|
2070
2736
|
};
|
|
2071
2737
|
if (this.options.tlsCA) {
|
|
2072
|
-
tlsOptions.ca =
|
|
2738
|
+
tlsOptions.ca = fs3.readFileSync(this.options.tlsCA);
|
|
2073
2739
|
}
|
|
2074
2740
|
this.server = tls.createServer(tlsOptions, (socket) => this.onConnection(socket));
|
|
2075
2741
|
} else {
|
|
@@ -2337,7 +3003,7 @@ async function startP2PServer(options) {
|
|
|
2337
3003
|
// src/p2p/client.ts
|
|
2338
3004
|
import net2 from "net";
|
|
2339
3005
|
import tls2 from "tls";
|
|
2340
|
-
import
|
|
3006
|
+
import fs4 from "fs";
|
|
2341
3007
|
import crypto3 from "crypto";
|
|
2342
3008
|
import chalk7 from "chalk";
|
|
2343
3009
|
var P2PClient = class {
|
|
@@ -2365,9 +3031,9 @@ var P2PClient = class {
|
|
|
2365
3031
|
async connect() {
|
|
2366
3032
|
if (this.options.auditLog) {
|
|
2367
3033
|
try {
|
|
2368
|
-
|
|
3034
|
+
fs4.mkdirSync(this.options.auditLog, { recursive: true });
|
|
2369
3035
|
const logFile = `${this.options.auditLog}/p2p-client-audit.log`;
|
|
2370
|
-
this.auditStream =
|
|
3036
|
+
this.auditStream = fs4.createWriteStream(logFile, { flags: "a" });
|
|
2371
3037
|
} catch {
|
|
2372
3038
|
console.log(chalk7.yellow("Warning: could not open audit log"));
|
|
2373
3039
|
}
|
|
@@ -2382,7 +3048,7 @@ var P2PClient = class {
|
|
|
2382
3048
|
rejectUnauthorized: !this.options.tlsInsecure
|
|
2383
3049
|
};
|
|
2384
3050
|
if (this.options.tlsCA) {
|
|
2385
|
-
tlsOptions.ca =
|
|
3051
|
+
tlsOptions.ca = fs4.readFileSync(this.options.tlsCA);
|
|
2386
3052
|
}
|
|
2387
3053
|
this.socket = tls2.connect(tlsOptions, () => {
|
|
2388
3054
|
this.running = true;
|
|
@@ -2572,6 +3238,29 @@ function deriveHMACKey2(password) {
|
|
|
2572
3238
|
}
|
|
2573
3239
|
|
|
2574
3240
|
// src/live/input.ts
|
|
3241
|
+
function persistConfig() {
|
|
3242
|
+
writeConfig({
|
|
3243
|
+
theme: state.currentTheme,
|
|
3244
|
+
interval: state.interval,
|
|
3245
|
+
exportDir: state.exportDir,
|
|
3246
|
+
detailed: state.detailed,
|
|
3247
|
+
sortMode: state.sortMode,
|
|
3248
|
+
treeView: state.treeView,
|
|
3249
|
+
graphMode: state.graphMode,
|
|
3250
|
+
showGraphs: state.showGraphs,
|
|
3251
|
+
autoLog: state.logging,
|
|
3252
|
+
mouseEnabled: state.mouseEnabled,
|
|
3253
|
+
cpuAlertPct: state.CPU_ALERT_PCT,
|
|
3254
|
+
tempAlertC: state.TEMP_ALERT_C,
|
|
3255
|
+
p2pPort: state.p2pPort,
|
|
3256
|
+
p2pPassword: state.p2pPassword,
|
|
3257
|
+
splashEnabled: state.splashEnabled,
|
|
3258
|
+
splashColorScheme: state.splashColorScheme,
|
|
3259
|
+
splashAnimation: state.splashAnimation,
|
|
3260
|
+
visiblePanels: { ...state.visiblePanels },
|
|
3261
|
+
notificationsEnabled: state.notificationsEnabled
|
|
3262
|
+
});
|
|
3263
|
+
}
|
|
2575
3264
|
function isUpKey(key, str) {
|
|
2576
3265
|
return key.name === "up" || str === "k" || key.sequence === "\x1B[A" || key.sequence === "\x1BOA";
|
|
2577
3266
|
}
|
|
@@ -2617,6 +3306,12 @@ function handleInputModeKey(str, key) {
|
|
|
2617
3306
|
const curIdx = animations.indexOf(state.splashAnimation);
|
|
2618
3307
|
state.splashAnimation = animations[(curIdx + 1) % animations.length];
|
|
2619
3308
|
setStatus(`Splash animation: ${state.splashAnimation}`);
|
|
3309
|
+
} else if (selected === "Notifications") {
|
|
3310
|
+
state.notificationsEnabled = !state.notificationsEnabled;
|
|
3311
|
+
setStatus(`Notifications: ${state.notificationsEnabled ? "on" : "off"}`);
|
|
3312
|
+
} else if (selected === "Temperature Unit") {
|
|
3313
|
+
state.tempUnit = state.tempUnit === "c" ? "f" : "c";
|
|
3314
|
+
setStatus(`Temperature unit: ${state.tempUnit.toUpperCase()}`);
|
|
2620
3315
|
} else {
|
|
2621
3316
|
const toggleKey = getToggleKey(selected);
|
|
2622
3317
|
if (toggleKey) {
|
|
@@ -2631,6 +3326,7 @@ function handleInputModeKey(str, key) {
|
|
|
2631
3326
|
}
|
|
2632
3327
|
invalidateTableCache();
|
|
2633
3328
|
invalidateFrame();
|
|
3329
|
+
persistConfig();
|
|
2634
3330
|
}
|
|
2635
3331
|
render();
|
|
2636
3332
|
return;
|
|
@@ -2843,6 +3539,9 @@ async function startLive(opts2, splashPromise) {
|
|
|
2843
3539
|
if (opts2.theme) state.currentTheme = opts2.theme;
|
|
2844
3540
|
state.interval = opts2.interval;
|
|
2845
3541
|
if (opts2.exportDir) state.exportDir = opts2.exportDir;
|
|
3542
|
+
if (opts2.alertCpu !== void 0) state.CPU_ALERT_PCT = opts2.alertCpu;
|
|
3543
|
+
if (opts2.alertTemp !== void 0) state.TEMP_ALERT_C = opts2.alertTemp;
|
|
3544
|
+
if (opts2.tempUnit) state.tempUnit = opts2.tempUnit;
|
|
2846
3545
|
state.termWidth = process.stdout.columns || 80;
|
|
2847
3546
|
state.termHeight = process.stdout.rows || 24;
|
|
2848
3547
|
state.history.reset();
|
|
@@ -2989,6 +3688,12 @@ async function startLive(opts2, splashPromise) {
|
|
|
2989
3688
|
state.detailed = !state.detailed;
|
|
2990
3689
|
setStatus(`Detailed sensor mode: ${state.detailed ? "on" : "off"}`);
|
|
2991
3690
|
break;
|
|
3691
|
+
case "T":
|
|
3692
|
+
state.tempUnit = state.tempUnit === "c" ? "f" : "c";
|
|
3693
|
+
setStatus(`Temperature unit: ${state.tempUnit.toUpperCase()}`);
|
|
3694
|
+
invalidateTableCache();
|
|
3695
|
+
render();
|
|
3696
|
+
break;
|
|
2992
3697
|
case "s":
|
|
2993
3698
|
const sortCycle = ["cpu", "mem", "pid", "user", "command", "state", "threads", "runtime"];
|
|
2994
3699
|
const curIdx = sortCycle.indexOf(state.sortMode);
|
|
@@ -3020,7 +3725,9 @@ async function startLive(opts2, splashPromise) {
|
|
|
3020
3725
|
render();
|
|
3021
3726
|
break;
|
|
3022
3727
|
case "f":
|
|
3023
|
-
|
|
3728
|
+
const fmtCycle = ["json", "csv", "tsv", "html", "md"];
|
|
3729
|
+
const curFmt = fmtCycle.indexOf(state.exportFormat);
|
|
3730
|
+
state.exportFormat = fmtCycle[(curFmt + 1) % fmtCycle.length];
|
|
3024
3731
|
setStatus(`Export format: ${state.exportFormat}`);
|
|
3025
3732
|
render();
|
|
3026
3733
|
break;
|
|
@@ -3089,6 +3796,7 @@ function handleMouseClick(y, x) {
|
|
|
3089
3796
|
}
|
|
3090
3797
|
}
|
|
3091
3798
|
function stopLive() {
|
|
3799
|
+
persistConfig();
|
|
3092
3800
|
if (state.intervalHandle) {
|
|
3093
3801
|
clearInterval(state.intervalHandle);
|
|
3094
3802
|
state.intervalHandle = null;
|
|
@@ -3401,11 +4109,11 @@ function spreadFireSparks(fire, cols, rows) {
|
|
|
3401
4109
|
}
|
|
3402
4110
|
}
|
|
3403
4111
|
}
|
|
3404
|
-
async function showSplash(
|
|
3405
|
-
const enabled =
|
|
4112
|
+
async function showSplash(config3) {
|
|
4113
|
+
const enabled = config3?.enabled ?? true;
|
|
3406
4114
|
if (!enabled) return;
|
|
3407
|
-
const colorScheme =
|
|
3408
|
-
const animation =
|
|
4115
|
+
const colorScheme = config3?.colorScheme ?? "fire";
|
|
4116
|
+
const animation = config3?.animation ?? "classic";
|
|
3409
4117
|
const rgbPalette = getPalette(colorScheme);
|
|
3410
4118
|
const cols = 60;
|
|
3411
4119
|
const rows = 15;
|
|
@@ -3455,13 +4163,14 @@ async function showSplash(config) {
|
|
|
3455
4163
|
}
|
|
3456
4164
|
|
|
3457
4165
|
// src/index.ts
|
|
3458
|
-
var
|
|
4166
|
+
var config2 = readConfig();
|
|
4167
|
+
var pkg = JSON.parse(fs5.readFileSync(new URL("../package.json", import.meta.url), "utf-8"));
|
|
3459
4168
|
var program = new Command();
|
|
3460
|
-
program.name("pyre").version(pkg.version).description("Mac system monitoring CLI: interactive live dashboard, stats, graphs, export, packet monitor, battery predictor").option("-j, --json", "Output as JSON").option("-c, --csv", "Output as CSV").option("-t, --tsv", "Output as TSV").option("--detailed", "Include detailed system info and sensor readings").option("--theme <name>", "Default theme for live mode (default, dracula, cyberpunk, monochrome, nord, gruvbox)",
|
|
4169
|
+
program.name("pyre").version(pkg.version).description("Mac system monitoring CLI: interactive live dashboard, stats, graphs, export, packet monitor, battery predictor").option("-j, --json", "Output as JSON").option("--html", "Output as HTML").option("--md", "Output as Markdown").option("-c, --csv", "Output as CSV").option("-t, --tsv", "Output as TSV").option("--detailed", "Include detailed system info and sensor readings").option("--theme <name>", "Default theme for live mode (default, dracula, cyberpunk, monochrome, nord, gruvbox)", config2.theme).option("--interval <seconds>", "Refresh interval for live mode", String(config2.interval)).option("--once", "Show a single static snapshot instead of live feed").option("--out <file>", "Also write the snapshot output to a file (--once/--json/--csv/--tsv modes)").option("--export-dir <dir>", "Directory used for live-mode snapshot exports and logs", config2.exportDir).option("--log", "Start continuous CSV logging immediately when live mode starts").option("--tree", "Show process tree view instead of flat list").option("--sort <key>", "Sort processes by: cpu, mem, pid, user, command, state, threads, runtime", config2.sortMode).option("--packets", "Include packet monitor panel in output").option("--limit <n>", "Max number of processes to include in --once/--json/--csv/--tsv snapshots (0 = all)", "10").option("--alert-cpu <pct>", "CPU usage alert threshold (default: 90)", String(config2.cpuAlertPct)).option("--alert-temp <c>", "CPU temperature alert threshold in Celsius (default: 95)", String(config2.tempAlertC)).option("--temp-unit <unit>", "Temperature display unit: c or f (default: c)", "c").option("--p2p-host <host>", "P2P host address (server: bind address, client: server address)").option("--p2p-port <port>", "P2P port number", "9876").option("--p2p-password <password>", "Password for P2P authentication").option("--p2p-tls", "Enable TLS encryption for P2P connections").option("--p2p-cert <file>", "TLS certificate file (PEM) for P2P server").option("--p2p-key <file>", "TLS private key file (PEM) for P2P server").option("--p2p-ca <file>", "TLS CA certificate file (PEM) for P2P client").option("--p2p-insecure", "Skip TLS certificate verification (client only)").option("--p2p-rate-limit <n>", "Max auth attempts per IP per minute (default: 5)", "5").option("--p2p-allow <ips>", "Comma-separated list of allowed IPs (empty = all)").option("--p2p-deny <ips>", "Comma-separated list of denied IPs").option("--p2p-audit-log <dir>", "Directory for P2P audit logs").option("--p2p-hmac-key <key>", "HMAC key for message signing (default: derived from password)");
|
|
3461
4170
|
program.parse(process.argv);
|
|
3462
4171
|
var opts = program.opts();
|
|
3463
4172
|
function isExportMode() {
|
|
3464
|
-
return opts.json || opts.csv || opts.tsv;
|
|
4173
|
+
return opts.json || opts.csv || opts.tsv || opts.html || opts.md;
|
|
3465
4174
|
}
|
|
3466
4175
|
function stripAnsi(s) {
|
|
3467
4176
|
return s.replace(/\x1b\[[0-9;]*m/g, "");
|
|
@@ -3470,7 +4179,7 @@ function sanitizeHost2(host) {
|
|
|
3470
4179
|
return host.trim().replace(/%$/, "");
|
|
3471
4180
|
}
|
|
3472
4181
|
function detectLocalIP() {
|
|
3473
|
-
const ifaces =
|
|
4182
|
+
const ifaces = os3.networkInterfaces();
|
|
3474
4183
|
for (const [, addrs] of Object.entries(ifaces)) {
|
|
3475
4184
|
if (!addrs) continue;
|
|
3476
4185
|
for (const addr of addrs) {
|
|
@@ -3504,10 +4213,52 @@ async function runServerCommand() {
|
|
|
3504
4213
|
}
|
|
3505
4214
|
async function main() {
|
|
3506
4215
|
const cmd = program.args[0];
|
|
4216
|
+
if (cmd === "info") {
|
|
4217
|
+
await runInfoCommand();
|
|
4218
|
+
return;
|
|
4219
|
+
}
|
|
4220
|
+
if (cmd === "ssh") {
|
|
4221
|
+
const host = program.args[1];
|
|
4222
|
+
if (!host) {
|
|
4223
|
+
console.log(chalk9.red("Usage: pyre ssh <host>"));
|
|
4224
|
+
process.exit(1);
|
|
4225
|
+
}
|
|
4226
|
+
await runSshCommand(host);
|
|
4227
|
+
return;
|
|
4228
|
+
}
|
|
4229
|
+
if (cmd === "web") {
|
|
4230
|
+
await runWebCommand();
|
|
4231
|
+
return;
|
|
4232
|
+
}
|
|
4233
|
+
if (cmd === "bench") {
|
|
4234
|
+
const benchCmd = program.args.slice(1).join(" ");
|
|
4235
|
+
if (!benchCmd) {
|
|
4236
|
+
console.log(chalk9.red("Usage: pyre bench <command>"));
|
|
4237
|
+
process.exit(1);
|
|
4238
|
+
}
|
|
4239
|
+
await runBenchCommand(benchCmd);
|
|
4240
|
+
return;
|
|
4241
|
+
}
|
|
4242
|
+
if (cmd === "config") {
|
|
4243
|
+
const sub = program.args[1];
|
|
4244
|
+
if (sub === "show") {
|
|
4245
|
+
const cfg = readConfig();
|
|
4246
|
+
console.log(JSON.stringify(cfg, null, 2));
|
|
4247
|
+
} else if (sub === "reset") {
|
|
4248
|
+
fs5.rmSync(CONFIG_FILE, { force: true });
|
|
4249
|
+
console.log(chalk9.green("Config reset to defaults"));
|
|
4250
|
+
} else {
|
|
4251
|
+
console.log("Usage: pyre config <show|reset>");
|
|
4252
|
+
}
|
|
4253
|
+
return;
|
|
4254
|
+
}
|
|
3507
4255
|
if (cmd === "server") {
|
|
3508
4256
|
await runServerCommand();
|
|
3509
4257
|
return;
|
|
3510
4258
|
}
|
|
4259
|
+
const alertCpu = opts.alertCpu ? parseInt(opts.alertCpu, 10) : void 0;
|
|
4260
|
+
const alertTemp = opts.alertTemp ? parseInt(opts.alertTemp, 10) : void 0;
|
|
4261
|
+
const tempUnit = opts.tempUnit === "f" ? "f" : "c";
|
|
3511
4262
|
if (cmd === "live" || !isExportMode() && !opts.once && cmd !== "p2p") {
|
|
3512
4263
|
if (cmd === "live") {
|
|
3513
4264
|
program.args.shift();
|
|
@@ -3516,10 +4267,13 @@ async function main() {
|
|
|
3516
4267
|
const interval = parseFloat(opts.interval) || 2;
|
|
3517
4268
|
await startLive({
|
|
3518
4269
|
interval,
|
|
3519
|
-
detailed: opts.detailed,
|
|
3520
|
-
theme: opts.theme,
|
|
3521
|
-
exportDir: opts.exportDir,
|
|
3522
|
-
autoLog: opts.log
|
|
4270
|
+
detailed: opts.detailed ?? config2.detailed,
|
|
4271
|
+
theme: opts.theme || config2.theme,
|
|
4272
|
+
exportDir: opts.exportDir || config2.exportDir,
|
|
4273
|
+
autoLog: opts.log || config2.autoLog,
|
|
4274
|
+
alertCpu,
|
|
4275
|
+
alertTemp,
|
|
4276
|
+
tempUnit
|
|
3523
4277
|
}, splashPromise);
|
|
3524
4278
|
return;
|
|
3525
4279
|
}
|
|
@@ -3541,12 +4295,14 @@ async function main() {
|
|
|
3541
4295
|
const data = await collectAll({ detailed: opts.detailed, processLimit });
|
|
3542
4296
|
let output;
|
|
3543
4297
|
if (opts.json) output = formatJson(data);
|
|
4298
|
+
else if (opts.html) output = formatHtml(data);
|
|
4299
|
+
else if (opts.md) output = formatMarkdown(data);
|
|
3544
4300
|
else if (opts.csv) output = formatCsv(data);
|
|
3545
4301
|
else if (opts.tsv) output = formatTsv(data);
|
|
3546
|
-
else output = formatTable(data, { width: process.stdout.columns || 80, sortBy: opts.sort, treeView: opts.tree, visible: { packets: opts.packets ? true : void 0 } });
|
|
4302
|
+
else output = formatTable(data, { width: process.stdout.columns || 80, sortBy: opts.sort, treeView: opts.tree ?? config2.treeView, visible: { packets: opts.packets ? true : void 0 } });
|
|
3547
4303
|
console.log(output);
|
|
3548
4304
|
if (opts.out) {
|
|
3549
|
-
|
|
4305
|
+
fs5.writeFileSync(opts.out, stripAnsi(output));
|
|
3550
4306
|
console.log(chalk9.dim(`
|
|
3551
4307
|
Saved to ${opts.out}`));
|
|
3552
4308
|
}
|
|
@@ -3728,3 +4484,124 @@ async function runP2PConnect() {
|
|
|
3728
4484
|
process.exit(0);
|
|
3729
4485
|
});
|
|
3730
4486
|
}
|
|
4487
|
+
async function runInfoCommand() {
|
|
4488
|
+
const data = await collectAll({ detailed: true });
|
|
4489
|
+
const lines = [];
|
|
4490
|
+
lines.push(chalk9.bold(" pyre info"));
|
|
4491
|
+
lines.push("");
|
|
4492
|
+
lines.push(` Hostname: ${data.header.hostname}`);
|
|
4493
|
+
lines.push(` OS: ${data.header.os}`);
|
|
4494
|
+
lines.push(` Uptime: ${data.header.uptime}`);
|
|
4495
|
+
lines.push(` CPU: ${data.cpu.brand}`);
|
|
4496
|
+
lines.push(` Cores: ${data.cpu.physicalCores}/${data.cpu.cores} (phys/log)`);
|
|
4497
|
+
lines.push(` Frequency: ${data.cpu.frequency} MHz`);
|
|
4498
|
+
lines.push(` Memory: ${formatBytes(data.memory.total)}`);
|
|
4499
|
+
if (data.gpu) lines.push(` GPU: ${data.gpu.model} (${formatBytes(data.gpu.memory)})`);
|
|
4500
|
+
if (data.battery) {
|
|
4501
|
+
lines.push(` Battery: ${data.battery.level}% (${data.battery.state})`);
|
|
4502
|
+
lines.push(` Health: ${data.battery.health}`);
|
|
4503
|
+
}
|
|
4504
|
+
lines.push(` Thermal: ${data.thermal.state}`);
|
|
4505
|
+
console.log(lines.join("\n"));
|
|
4506
|
+
}
|
|
4507
|
+
async function runSshCommand(host) {
|
|
4508
|
+
const password = opts.p2pPassword || "mysecret";
|
|
4509
|
+
const cmd = `ssh -o LogLevel=QUIET ${host} "pyre --json --once"`;
|
|
4510
|
+
console.log(chalk9.bold(`
|
|
4511
|
+
pyre ssh \u2014 ${host}`));
|
|
4512
|
+
console.log(chalk9.dim(` Running: ${cmd}
|
|
4513
|
+
`));
|
|
4514
|
+
const { execFile: execFile2 } = await import("child_process");
|
|
4515
|
+
const child = execFile2("ssh", ["-o", "LogLevel=QUIET", host, "pyre", "--json", "--once"], (err, stdout, stderr) => {
|
|
4516
|
+
if (err) {
|
|
4517
|
+
console.log(chalk9.red(` SSH failed: ${err.message}`));
|
|
4518
|
+
console.log(chalk9.dim(" Ensure SSH keys are set up or the host is reachable."));
|
|
4519
|
+
process.exit(1);
|
|
4520
|
+
}
|
|
4521
|
+
if (stderr) console.log(chalk9.dim(stderr));
|
|
4522
|
+
try {
|
|
4523
|
+
const remote = JSON.parse(stdout);
|
|
4524
|
+
console.log(formatTable(remote, { width: process.stdout.columns || 80, sortBy: "cpu", treeView: false, visible: { packets: true } }));
|
|
4525
|
+
} catch {
|
|
4526
|
+
console.log(stdout);
|
|
4527
|
+
}
|
|
4528
|
+
process.exit(0);
|
|
4529
|
+
});
|
|
4530
|
+
child.stdin.end();
|
|
4531
|
+
process.once("SIGINT", () => {
|
|
4532
|
+
child.kill("SIGTERM");
|
|
4533
|
+
process.exit(0);
|
|
4534
|
+
});
|
|
4535
|
+
}
|
|
4536
|
+
async function runWebCommand() {
|
|
4537
|
+
const http = await import("http");
|
|
4538
|
+
const data = await collectAll({ detailed: true });
|
|
4539
|
+
const server = http.createServer((req, res) => {
|
|
4540
|
+
if (req.url === "/" || req.url === "/index.html") {
|
|
4541
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
4542
|
+
res.end(formatHtml(data));
|
|
4543
|
+
} else if (req.url === "/api") {
|
|
4544
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
4545
|
+
res.end(JSON.stringify(data, null, 2));
|
|
4546
|
+
} else {
|
|
4547
|
+
res.writeHead(404);
|
|
4548
|
+
res.end("Not found");
|
|
4549
|
+
}
|
|
4550
|
+
});
|
|
4551
|
+
server.listen(0, "127.0.0.1", async () => {
|
|
4552
|
+
const address = server.address();
|
|
4553
|
+
const port = typeof address === "object" && address ? address.port : 0;
|
|
4554
|
+
const url = `http://127.0.0.1:${port}`;
|
|
4555
|
+
console.log(chalk9.bold(`
|
|
4556
|
+
pyre web`));
|
|
4557
|
+
console.log(chalk9.dim(` Dashboard: ${url}`));
|
|
4558
|
+
console.log(chalk9.dim(` API: ${url}/api`));
|
|
4559
|
+
console.log(chalk9.dim(" Press Ctrl+C to stop\n"));
|
|
4560
|
+
process.once("SIGINT", () => {
|
|
4561
|
+
server.close();
|
|
4562
|
+
process.exit(0);
|
|
4563
|
+
});
|
|
4564
|
+
});
|
|
4565
|
+
}
|
|
4566
|
+
async function runBenchCommand(benchCmd) {
|
|
4567
|
+
const { spawn } = await import("child_process");
|
|
4568
|
+
const logDir = opts.exportDir || "./pyre-exports";
|
|
4569
|
+
const file = path3.join(logDir, `pyre-bench-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.csv`);
|
|
4570
|
+
if (!fs5.existsSync(logDir)) fs5.mkdirSync(logDir, { recursive: true });
|
|
4571
|
+
const stream = fs5.createWriteStream(file, { flags: "a" });
|
|
4572
|
+
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");
|
|
4573
|
+
console.log(chalk9.bold(`
|
|
4574
|
+
pyre bench`));
|
|
4575
|
+
console.log(chalk9.dim(` Command: ${benchCmd}`));
|
|
4576
|
+
console.log(chalk9.dim(` Log: ${file}
|
|
4577
|
+
`));
|
|
4578
|
+
const child = spawn("sh", ["-c", benchCmd]);
|
|
4579
|
+
child.stdout.on("data", (d) => process.stdout.write(d));
|
|
4580
|
+
child.stderr.on("data", (d) => process.stdout.write(d));
|
|
4581
|
+
const interval = parseFloat(opts.interval) || 2;
|
|
4582
|
+
const handle = setInterval(async () => {
|
|
4583
|
+
try {
|
|
4584
|
+
const data = await collectAll({ detailed: opts.detailed });
|
|
4585
|
+
const temp = data.cpu.temperature ?? data.thermal.temperatures?.cpu_die ?? "";
|
|
4586
|
+
const rxPackets = data.network.rxPackets ?? 0;
|
|
4587
|
+
const txPackets = data.network.txPackets ?? 0;
|
|
4588
|
+
const connections = data.network.connections ?? 0;
|
|
4589
|
+
stream.write(`${data.timestamp},${data.cpu.usage},${data.memory.usagePercent},${temp},${data.network.rxBytes},${data.network.txBytes},${rxPackets},${txPackets},${connections},${data.thermal.state}
|
|
4590
|
+
`);
|
|
4591
|
+
} catch {
|
|
4592
|
+
}
|
|
4593
|
+
}, interval * 1e3);
|
|
4594
|
+
child.on("close", () => {
|
|
4595
|
+
clearInterval(handle);
|
|
4596
|
+
stream.end();
|
|
4597
|
+
console.log(chalk9.green(`
|
|
4598
|
+
Bench complete. Log saved to ${file}`));
|
|
4599
|
+
process.exit(0);
|
|
4600
|
+
});
|
|
4601
|
+
process.once("SIGINT", () => {
|
|
4602
|
+
clearInterval(handle);
|
|
4603
|
+
child.kill("SIGTERM");
|
|
4604
|
+
stream.end();
|
|
4605
|
+
process.exit(0);
|
|
4606
|
+
});
|
|
4607
|
+
}
|