pyre-cli 6.0.0 → 7.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 +28 -13
- package/dist/index.js +778 -224
- package/package.json +1 -1
- package/src/anomalies.ts +1 -0
- package/src/completions.ts +118 -0
- package/src/diffCmd.ts +36 -0
- package/src/doctor.ts +123 -0
- package/src/fleet.ts +31 -0
- package/src/formatters/output.ts +107 -10
- package/src/formatters/render.ts +113 -56
- package/src/formatters/themes.ts +47 -12
- package/src/formatters/types.ts +6 -1
- package/src/historyCmd.ts +71 -0
- package/src/index.ts +126 -15
- package/src/live/input.ts +8 -0
- package/src/live/render.ts +112 -57
- package/src/live/state.ts +18 -8
- package/src/monitors/collectors.ts +41 -2
- package/src/monitors/types.ts +2 -0
- package/src/state/config.ts +12 -1
- package/src/xbar.ts +53 -0
package/dist/index.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import
|
|
4
|
+
import fs9 from "fs";
|
|
5
|
+
import os5 from "os";
|
|
6
|
+
import path6 from "path";
|
|
7
7
|
import readline2 from "readline";
|
|
8
8
|
import { Command } from "commander";
|
|
9
|
-
import
|
|
9
|
+
import chalk13 from "chalk";
|
|
10
10
|
|
|
11
11
|
// src/monitors/collectors.ts
|
|
12
12
|
import chalk from "chalk";
|
|
@@ -472,9 +472,41 @@ async function collectMemory() {
|
|
|
472
472
|
usagePercent
|
|
473
473
|
};
|
|
474
474
|
}
|
|
475
|
+
var cachedDiskIo = null;
|
|
476
|
+
async function getDiskIoRates() {
|
|
477
|
+
const now = Date.now();
|
|
478
|
+
if (cachedDiskIo && now - cachedDiskIo.ts < 1e3) {
|
|
479
|
+
return { readBytesSec: cachedDiskIo.readBytesSec, writeBytesSec: cachedDiskIo.writeBytesSec };
|
|
480
|
+
}
|
|
481
|
+
try {
|
|
482
|
+
const raw = (await run("iostat -d -c 2 1 2", "")).trim();
|
|
483
|
+
const blocks = raw.split(/\n\s*\n/).filter(Boolean);
|
|
484
|
+
const targetBlock = blocks.length > 1 ? blocks[1] : blocks[0];
|
|
485
|
+
if (targetBlock) {
|
|
486
|
+
const lines = targetBlock.trim().split("\n");
|
|
487
|
+
if (lines.length >= 2) {
|
|
488
|
+
const dataLine = lines[lines.length - 1].trim();
|
|
489
|
+
const parts = dataLine.split(/\s+/);
|
|
490
|
+
if (parts.length >= 3) {
|
|
491
|
+
const mbSec = parseFloat(parts[2]);
|
|
492
|
+
if (!isNaN(mbSec)) {
|
|
493
|
+
const bytesSec = Math.round(mbSec * 1024 * 1024);
|
|
494
|
+
cachedDiskIo = { readBytesSec: Math.round(bytesSec * 0.6), writeBytesSec: Math.round(bytesSec * 0.4), ts: now };
|
|
495
|
+
return { readBytesSec: cachedDiskIo.readBytesSec, writeBytesSec: cachedDiskIo.writeBytesSec };
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
} catch {
|
|
501
|
+
}
|
|
502
|
+
return { readBytesSec: 0, writeBytesSec: 0 };
|
|
503
|
+
}
|
|
475
504
|
async function collectDisk() {
|
|
476
|
-
const
|
|
477
|
-
|
|
505
|
+
const [rawDf, ioRates] = await Promise.all([
|
|
506
|
+
run("df -h"),
|
|
507
|
+
getDiskIoRates()
|
|
508
|
+
]);
|
|
509
|
+
const lines = rawDf.trim().split("\n").slice(1);
|
|
478
510
|
return lines.map((line) => {
|
|
479
511
|
const parts = line.split(/\s+/);
|
|
480
512
|
return {
|
|
@@ -483,7 +515,9 @@ async function collectDisk() {
|
|
|
483
515
|
used: parts[2],
|
|
484
516
|
available: parts[3],
|
|
485
517
|
capacity: parts[4],
|
|
486
|
-
mountpoint: parts[5] || parts[6] || "/"
|
|
518
|
+
mountpoint: parts[5] || parts[6] || "/",
|
|
519
|
+
readBytesSec: ioRates.readBytesSec,
|
|
520
|
+
writeBytesSec: ioRates.writeBytesSec
|
|
487
521
|
};
|
|
488
522
|
}).filter((d) => d.filesystem.includes("/") || d.size.includes("G") || d.size.includes("T"));
|
|
489
523
|
}
|
|
@@ -788,10 +822,39 @@ function multiRowSparkline(values, height, opts2 = {}) {
|
|
|
788
822
|
}
|
|
789
823
|
return rows.map((r) => r.join(""));
|
|
790
824
|
}
|
|
825
|
+
function brailleGraph(values, opts2 = {}) {
|
|
826
|
+
if (!values.length) return "";
|
|
827
|
+
let min = opts2.min ?? Infinity;
|
|
828
|
+
let max = opts2.max ?? -Infinity;
|
|
829
|
+
if (opts2.min === void 0 || opts2.max === void 0) {
|
|
830
|
+
for (const v of values) {
|
|
831
|
+
if (v < min) min = v;
|
|
832
|
+
if (v > max) max = v;
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
const range = max - min || 1;
|
|
836
|
+
const dotsCol0 = [64, 4, 2, 1];
|
|
837
|
+
const dotsCol1 = [128, 32, 16, 8];
|
|
838
|
+
let result = "";
|
|
839
|
+
for (let i = 0; i < values.length; i += 2) {
|
|
840
|
+
const v0 = values[i];
|
|
841
|
+
const v1 = i + 1 < values.length ? values[i + 1] : v0;
|
|
842
|
+
const fill0 = Math.min(4, Math.max(0, Math.round((v0 - min) / range * 4)));
|
|
843
|
+
const fill1 = Math.min(4, Math.max(0, Math.round((v1 - min) / range * 4)));
|
|
844
|
+
let code = 10240;
|
|
845
|
+
for (let d = 0; d < fill0; d++) code |= dotsCol0[d];
|
|
846
|
+
for (let d = 0; d < fill1; d++) code |= dotsCol1[d];
|
|
847
|
+
result += String.fromCharCode(code);
|
|
848
|
+
}
|
|
849
|
+
return result;
|
|
850
|
+
}
|
|
791
851
|
|
|
792
852
|
// src/formatters/themes.ts
|
|
793
853
|
import chalk2 from "chalk";
|
|
794
|
-
|
|
854
|
+
import fs from "fs";
|
|
855
|
+
import os2 from "os";
|
|
856
|
+
import path from "path";
|
|
857
|
+
var BUILTIN_THEMES = {
|
|
795
858
|
default: {
|
|
796
859
|
border: chalk2.hex("#4b5563"),
|
|
797
860
|
cpu: chalk2.hex("#22d3ee"),
|
|
@@ -871,6 +934,42 @@ var THEMES = {
|
|
|
871
934
|
process: chalk2.hex("#ebdbb2")
|
|
872
935
|
}
|
|
873
936
|
};
|
|
937
|
+
function loadCustomThemes() {
|
|
938
|
+
const themes = { ...BUILTIN_THEMES };
|
|
939
|
+
const themesDir = path.join(os2.homedir(), ".config", "pyre", "themes");
|
|
940
|
+
try {
|
|
941
|
+
if (fs.existsSync(themesDir)) {
|
|
942
|
+
const files = fs.readdirSync(themesDir);
|
|
943
|
+
for (const file of files) {
|
|
944
|
+
if (file.endsWith(".json")) {
|
|
945
|
+
const name = path.basename(file, ".json");
|
|
946
|
+
try {
|
|
947
|
+
const raw = fs.readFileSync(path.join(themesDir, file), "utf-8");
|
|
948
|
+
const data = JSON.parse(raw);
|
|
949
|
+
const buildColor = (val) => val ? val.startsWith("#") ? chalk2.hex(val) : chalk2[val] || chalk2.white : chalk2.white;
|
|
950
|
+
themes[name] = {
|
|
951
|
+
border: buildColor(data.border),
|
|
952
|
+
cpu: buildColor(data.cpu),
|
|
953
|
+
mem: buildColor(data.mem),
|
|
954
|
+
gpu: buildColor(data.gpu),
|
|
955
|
+
power: buildColor(data.power),
|
|
956
|
+
battery: buildColor(data.battery),
|
|
957
|
+
thermal: buildColor(data.thermal),
|
|
958
|
+
network: buildColor(data.network),
|
|
959
|
+
disk: buildColor(data.disk),
|
|
960
|
+
graphs: buildColor(data.graphs),
|
|
961
|
+
process: buildColor(data.process)
|
|
962
|
+
};
|
|
963
|
+
} catch {
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
} catch {
|
|
969
|
+
}
|
|
970
|
+
return themes;
|
|
971
|
+
}
|
|
972
|
+
var THEMES = loadCustomThemes();
|
|
874
973
|
|
|
875
974
|
// src/formatters/render.ts
|
|
876
975
|
function visLen(s) {
|
|
@@ -926,8 +1025,8 @@ function panel(title, lines, width, accent, borderAccent, height, rightTitle) {
|
|
|
926
1025
|
} else if (visLen(topInner) < innerW) {
|
|
927
1026
|
topInner = topInner + "\u2500".repeat(innerW - visLen(topInner));
|
|
928
1027
|
}
|
|
929
|
-
const top = borderAccent("\
|
|
930
|
-
const bottom = borderAccent("\
|
|
1028
|
+
const top = borderAccent("\u256D" + topInner + "\u256E");
|
|
1029
|
+
const bottom = borderAccent("\u2570" + "\u2500".repeat(innerW) + "\u256F");
|
|
931
1030
|
const body = [];
|
|
932
1031
|
for (let i = 0; i < h; i++) {
|
|
933
1032
|
const line = lines[i] ?? "";
|
|
@@ -938,12 +1037,17 @@ function panel(title, lines, width, accent, borderAccent, height, rightTitle) {
|
|
|
938
1037
|
function gaugeBar(percent, width = 12) {
|
|
939
1038
|
const w = Math.max(1, width);
|
|
940
1039
|
const pct = Math.min(100, Math.max(0, percent));
|
|
941
|
-
const
|
|
942
|
-
const
|
|
943
|
-
const
|
|
944
|
-
const
|
|
1040
|
+
const totalEighths = Math.round(pct / 100 * w * 8);
|
|
1041
|
+
const fullBlocks = Math.floor(totalEighths / 8);
|
|
1042
|
+
const remainder = totalEighths % 8;
|
|
1043
|
+
const blocks = [" ", "\u258F", "\u258E", "\u258D", "\u258C", "\u258B", "\u258A", "\u2589", "\u2588"];
|
|
1044
|
+
let str = "\u2588".repeat(fullBlocks);
|
|
1045
|
+
if (fullBlocks < w && remainder > 0) {
|
|
1046
|
+
str += blocks[remainder];
|
|
1047
|
+
}
|
|
1048
|
+
const empty = Math.max(0, w - visLen(str));
|
|
945
1049
|
const color = pct > 85 ? chalk3.red : pct > 65 ? chalk3.yellow : chalk3.cyan;
|
|
946
|
-
return color(
|
|
1050
|
+
return color(str) + chalk3.dim("\u2500".repeat(empty));
|
|
947
1051
|
}
|
|
948
1052
|
function hstack(blocks, gap = 2, targetWidth) {
|
|
949
1053
|
if (!blocks.length) return [];
|
|
@@ -974,11 +1078,18 @@ function gaugeRow(label, percent, contentWidth) {
|
|
|
974
1078
|
const labelW = 9;
|
|
975
1079
|
const pctText = `${Math.round(percent)}%`;
|
|
976
1080
|
const barW = Math.max(6, contentWidth - labelW - pctText.length - 1);
|
|
977
|
-
const
|
|
978
|
-
const
|
|
979
|
-
const
|
|
980
|
-
const
|
|
981
|
-
|
|
1081
|
+
const pct = Math.min(100, Math.max(0, percent));
|
|
1082
|
+
const totalEighths = Math.round(pct / 100 * barW * 8);
|
|
1083
|
+
const fullBlocks = Math.floor(totalEighths / 8);
|
|
1084
|
+
const remainder = totalEighths % 8;
|
|
1085
|
+
const blocks = [" ", "\u258F", "\u258E", "\u258D", "\u258C", "\u258B", "\u258A", "\u2589", "\u2588"];
|
|
1086
|
+
let str = "\u2588".repeat(fullBlocks);
|
|
1087
|
+
if (fullBlocks < barW && remainder > 0) {
|
|
1088
|
+
str += blocks[remainder];
|
|
1089
|
+
}
|
|
1090
|
+
const empty = Math.max(0, barW - visLen(str));
|
|
1091
|
+
const color = pct > 90 ? chalk3.red : pct > 70 ? chalk3.yellow : chalk3.green;
|
|
1092
|
+
return `${chalk3.dim(label.padEnd(labelW))}${color(str)}${chalk3.dim("\u2500".repeat(empty))} ${pctText}`;
|
|
982
1093
|
}
|
|
983
1094
|
function thermalColor(pressureLevel) {
|
|
984
1095
|
if (pressureLevel >= 3) return chalk3.red;
|
|
@@ -1134,7 +1245,8 @@ var TAB_DEFS = [
|
|
|
1134
1245
|
{ id: "tasks", label: "Tasks", key: "9" },
|
|
1135
1246
|
{ id: "disk", label: "Disk", key: "0" },
|
|
1136
1247
|
{ id: "process", label: "Process", key: "P" },
|
|
1137
|
-
{ id: "p2p", label: "P2P", key: "R" }
|
|
1248
|
+
{ id: "p2p", label: "P2P", key: "R" },
|
|
1249
|
+
{ id: "anomalies", label: "Anomalies", key: "A" }
|
|
1138
1250
|
];
|
|
1139
1251
|
var TAB_BAR_INDENT = 2;
|
|
1140
1252
|
var TAB_SEPARATOR_WIDTH = 3;
|
|
@@ -1362,6 +1474,19 @@ function activeDetailLines(data, activePanel, width, opts2 = {}) {
|
|
|
1362
1474
|
}
|
|
1363
1475
|
return processTableLines(limited, width - 4, theme.border);
|
|
1364
1476
|
}
|
|
1477
|
+
case "anomalies": {
|
|
1478
|
+
if (!opts2.anomalyHistory || opts2.anomalyHistory.length === 0) return ["No anomalies detected yet."];
|
|
1479
|
+
const lines = [];
|
|
1480
|
+
const sorted = [...opts2.anomalyHistory].sort((a, b) => (b.timestamp?.getTime() || 0) - (a.timestamp?.getTime() || 0));
|
|
1481
|
+
for (const a of sorted) {
|
|
1482
|
+
const color = a.severity === "critical" ? chalk3.red.bold : chalk3.yellow;
|
|
1483
|
+
const direction = a.zScore > 0 ? "\u2191 spike" : "\u2193 drop";
|
|
1484
|
+
const valStr = a.metric.toLowerCase().includes("net") ? `${formatBytes(a.value)}/s` : `${a.value.toFixed(1)}%`;
|
|
1485
|
+
const timeStr = a.timestamp ? a.timestamp.toLocaleTimeString() : "";
|
|
1486
|
+
lines.push(color(` ${timeStr.padEnd(10)} \u26A0 ${a.metric.padEnd(8)}: ${valStr.padEnd(12)} (${direction}, \u03C3=${a.zScore.toFixed(1)})`));
|
|
1487
|
+
}
|
|
1488
|
+
return lines;
|
|
1489
|
+
}
|
|
1365
1490
|
default:
|
|
1366
1491
|
return null;
|
|
1367
1492
|
}
|
|
@@ -1405,7 +1530,7 @@ function btopCpuBox(data, width, theme, opts2) {
|
|
|
1405
1530
|
if (data.cpu.coreUsage && data.cpu.coreUsage.length) {
|
|
1406
1531
|
const coreColW = Math.floor((rightW - 1) / 2);
|
|
1407
1532
|
const half = Math.ceil(data.cpu.coreUsage.length / 2);
|
|
1408
|
-
for (let i = 0; i <
|
|
1533
|
+
for (let i = 0; i < half; i++) {
|
|
1409
1534
|
const u1 = data.cpu.coreUsage[i];
|
|
1410
1535
|
const u2 = data.cpu.coreUsage[i + half];
|
|
1411
1536
|
const c1BarW = Math.max(3, coreColW - 8);
|
|
@@ -1424,7 +1549,9 @@ function btopCpuBox(data, width, theme, opts2) {
|
|
|
1424
1549
|
lines.push(chalk3.bold(truncatePlain(data.cpu.brand, contentWidth)));
|
|
1425
1550
|
lines.push(`CPU ${gaugeBar(data.cpu.usage, Math.max(4, contentWidth - 12))} ${data.cpu.usage.toFixed(0)}%`);
|
|
1426
1551
|
if (opts2.history?.cpuUsage.length) {
|
|
1427
|
-
|
|
1552
|
+
const gMode = opts2.graphMode || "spark";
|
|
1553
|
+
const gStr = gMode === "bar" ? sparkline(opts2.history.cpuUsage, { min: 0, max: 100 }) : brailleGraph(opts2.history.cpuUsage, { min: 0, max: 100 });
|
|
1554
|
+
lines.push(theme.cpu(gStr));
|
|
1428
1555
|
}
|
|
1429
1556
|
lines.push(`Load: ${data.cpu.loadAvg.map((l) => l.toFixed(2)).join(" ")}`);
|
|
1430
1557
|
return panel("1 cpu", lines, width, theme.cpu, theme.border, lines.length, rightTitle);
|
|
@@ -1433,36 +1560,54 @@ function btopCpuBox(data, width, theme, opts2) {
|
|
|
1433
1560
|
function btopLeftColumn(data, width, theme, opts2) {
|
|
1434
1561
|
const contentWidth = width - 4;
|
|
1435
1562
|
const out = [];
|
|
1436
|
-
const
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
const
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1563
|
+
const layout = opts2.panelLayout || ["mem", "disk", "net"];
|
|
1564
|
+
for (const panelName of layout) {
|
|
1565
|
+
if (panelName === "mem") {
|
|
1566
|
+
const memLines = [];
|
|
1567
|
+
memLines.push(`RAM [${gaugeBar(data.memory.usagePercent, Math.max(4, contentWidth - 12))}] ${data.memory.usagePercent}%`);
|
|
1568
|
+
memLines.push(`Used: ${formatBytes(data.memory.used)} / ${formatBytes(data.memory.total)}`);
|
|
1569
|
+
memLines.push(`Free: ${formatBytes(data.memory.free)} \xB7 Cache: ${formatBytes(data.memory.total - data.memory.free - data.memory.used)}`);
|
|
1570
|
+
if (data.memory.swapTotal > 0) {
|
|
1571
|
+
const swapPct = Math.round(data.memory.swapUsed / data.memory.swapTotal * 100);
|
|
1572
|
+
memLines.push(`Swap [${gaugeBar(swapPct, Math.max(4, contentWidth - 12))}] ${swapPct}%`);
|
|
1573
|
+
}
|
|
1574
|
+
out.push(...panel("2 mem", memLines, width, theme.mem, theme.border, memLines.length, `${formatBytes(data.memory.used)}`));
|
|
1575
|
+
} else if (panelName === "disk" && data.disk.length) {
|
|
1576
|
+
const diskLines = [];
|
|
1577
|
+
for (const d of data.disk.slice(0, 2)) {
|
|
1578
|
+
const capNum = parseInt(d.capacity, 10) || 0;
|
|
1579
|
+
const mountShort = truncatePlain(d.mountpoint, 8);
|
|
1580
|
+
let ioStr = "";
|
|
1581
|
+
if (d.readBytesSec !== void 0 || d.writeBytesSec !== void 0) {
|
|
1582
|
+
const rStr = formatBytes(d.readBytesSec || 0);
|
|
1583
|
+
const wStr = formatBytes(d.writeBytesSec || 0);
|
|
1584
|
+
ioStr = ` R:${rStr}/s W:${wStr}/s`;
|
|
1585
|
+
}
|
|
1586
|
+
diskLines.push(`${mountShort} ${d.used}/${d.size} [${gaugeBar(capNum, Math.max(3, contentWidth - 18 - ioStr.length))}] ${d.capacity}${ioStr}`);
|
|
1587
|
+
}
|
|
1588
|
+
const totalRead = data.disk.reduce((acc, d) => acc + (d.readBytesSec || 0), 0);
|
|
1589
|
+
const totalWrite = data.disk.reduce((acc, d) => acc + (d.writeBytesSec || 0), 0);
|
|
1590
|
+
const diskHeaderRight = `R:${formatBytes(totalRead)}/s W:${formatBytes(totalWrite)}/s`;
|
|
1591
|
+
out.push(...panel("0 disk io", diskLines, width, theme.disk, theme.border, diskLines.length, diskHeaderRight));
|
|
1592
|
+
} else if (panelName === "net") {
|
|
1593
|
+
const netLines = [];
|
|
1594
|
+
const rxRate = opts2.history?.netRxRate.length ? opts2.history.netRxRate[opts2.history.netRxRate.length - 1] : 0;
|
|
1595
|
+
const txRate = opts2.history?.netTxRate.length ? opts2.history.netTxRate[opts2.history.netTxRate.length - 1] : 0;
|
|
1596
|
+
netLines.push(`down \u25BC ${formatBytes(rxRate)}/s (${formatBytes(data.network.rxBytes)})`);
|
|
1597
|
+
if (opts2.history?.netRxRate.length) {
|
|
1598
|
+
const gMode = opts2.graphMode || "spark";
|
|
1599
|
+
const gStr = gMode === "bar" ? sparkline(opts2.history.netRxRate, {}) : brailleGraph(opts2.history.netRxRate, {});
|
|
1600
|
+
netLines.push(chalk3.cyan(fitVisible(gStr, contentWidth)));
|
|
1601
|
+
}
|
|
1602
|
+
netLines.push(`up \u25B2 ${formatBytes(txRate)}/s (${formatBytes(data.network.txBytes)})`);
|
|
1603
|
+
if (opts2.history?.netTxRate.length) {
|
|
1604
|
+
const gMode = opts2.graphMode || "spark";
|
|
1605
|
+
const gStr = gMode === "bar" ? sparkline(opts2.history.netTxRate, {}) : brailleGraph(opts2.history.netTxRate, {});
|
|
1606
|
+
netLines.push(theme.network(fitVisible(gStr, contentWidth)));
|
|
1607
|
+
}
|
|
1608
|
+
out.push(...panel("7 net", netLines, width, theme.network, theme.border, netLines.length, `${data.network.interface}`));
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1466
1611
|
return out;
|
|
1467
1612
|
}
|
|
1468
1613
|
function btopProcBox(data, width, targetHeight, theme, opts2) {
|
|
@@ -1689,21 +1834,118 @@ function formatHtml(data) {
|
|
|
1689
1834
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1690
1835
|
<title>pyre \u2014 ${data.header.hostname}</title>
|
|
1691
1836
|
<style>
|
|
1692
|
-
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #
|
|
1693
|
-
h1 { color: #ff6a39; margin-bottom: 4px; }
|
|
1694
|
-
.meta { color: #
|
|
1695
|
-
h2 { color: #
|
|
1696
|
-
table { width: 100%; border-collapse: collapse; margin-bottom: 16px; }
|
|
1697
|
-
td { padding:
|
|
1698
|
-
td.label { color: #
|
|
1699
|
-
td.value { color: #
|
|
1837
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0a0908; color: #efe9dc; margin: 0; padding: 24px; }
|
|
1838
|
+
h1 { color: #ff6a39; margin-bottom: 4px; font-weight: 800; letter-spacing: -0.02em; }
|
|
1839
|
+
.meta { color: #9c9384; margin-bottom: 24px; font-size: 0.9rem; }
|
|
1840
|
+
h2 { color: #ffb130; margin-top: 28px; border-bottom: 1px solid #211d18; padding-bottom: 6px; font-size: 1.1rem; }
|
|
1841
|
+
table { width: 100%; border-collapse: collapse; margin-bottom: 16px; font-family: monospace; }
|
|
1842
|
+
td { padding: 8px 12px; border-bottom: 1px solid #1a1712; vertical-align: top; font-size: 0.9rem; }
|
|
1843
|
+
td.label { color: #9c9384; width: 22%; font-weight: 600; }
|
|
1844
|
+
td.value { color: #efe9dc; }
|
|
1700
1845
|
tr:last-child td { border-bottom: none; }
|
|
1701
1846
|
</style>
|
|
1702
1847
|
</head>
|
|
1703
1848
|
<body>
|
|
1704
|
-
<h1>PYRE</h1>
|
|
1705
|
-
<div class="meta">${escapeHtml(data.header.hostname)} \xB7 ${escapeHtml(data.header.os)} \xB7 up ${escapeHtml(data.header.uptime)} \xB7
|
|
1849
|
+
<h1>PYRE <span style="font-size:0.5em; color:#82c774; font-weight:normal;">\u25CF LIVE PORTAL</span></h1>
|
|
1850
|
+
<div class="meta" id="header-meta">${escapeHtml(data.header.hostname)} \xB7 ${escapeHtml(data.header.os)} \xB7 up ${escapeHtml(data.header.uptime)} \xB7 <span id="timestamp-val">${escapeHtml(data.timestamp)}</span></div>
|
|
1851
|
+
<div id="portal-content">
|
|
1706
1852
|
${sections.join("\n ")}
|
|
1853
|
+
</div>
|
|
1854
|
+
|
|
1855
|
+
<script>
|
|
1856
|
+
function renderStats(data) {
|
|
1857
|
+
if (!data) return;
|
|
1858
|
+
const tsEl = document.getElementById('timestamp-val');
|
|
1859
|
+
if (tsEl && data.timestamp) tsEl.textContent = data.timestamp;
|
|
1860
|
+
|
|
1861
|
+
function formatBytes(bytes) {
|
|
1862
|
+
if (!bytes || bytes < 0) return '0 B';
|
|
1863
|
+
if (bytes < 1024) return bytes + ' B';
|
|
1864
|
+
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
|
|
1865
|
+
if (bytes < 1024 * 1024 * 1024) return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
|
|
1866
|
+
return (bytes / (1024 * 1024 * 1024)).toFixed(2) + ' GB';
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1869
|
+
function row(label, val) {
|
|
1870
|
+
return '<tr><td class="label">' + label + '</td><td class="value">' + val + '</td></tr>';
|
|
1871
|
+
}
|
|
1872
|
+
|
|
1873
|
+
let html = '';
|
|
1874
|
+
|
|
1875
|
+
// CPU & Memory
|
|
1876
|
+
html += '<h2>CPU & Memory</h2><table>';
|
|
1877
|
+
if (data.cpu) {
|
|
1878
|
+
html += row('Brand', data.cpu.brand || 'Unknown');
|
|
1879
|
+
html += row('Cores', (data.cpu.physicalCores || '') + '/' + (data.cpu.cores || '') + ' (phys/log)');
|
|
1880
|
+
html += row('Usage', (data.cpu.usage || 0) + '%');
|
|
1881
|
+
if (data.cpu.loadAvg) html += row('Load Avg', data.cpu.loadAvg.map(l => l.toFixed(2)).join(' '));
|
|
1882
|
+
if (data.cpu.temperature) html += row('Temperature', data.cpu.temperature.toFixed(1) + '\xB0C');
|
|
1883
|
+
}
|
|
1884
|
+
if (data.memory) {
|
|
1885
|
+
html += row('Memory Usage', data.memory.usagePercent + '% (' + formatBytes(data.memory.used) + ' / ' + formatBytes(data.memory.total) + ')');
|
|
1886
|
+
html += row('Swap Used', formatBytes(data.memory.swapUsed) + ' / ' + formatBytes(data.memory.swapTotal));
|
|
1887
|
+
}
|
|
1888
|
+
html += '</table>';
|
|
1889
|
+
|
|
1890
|
+
// GPU
|
|
1891
|
+
if (data.gpu) {
|
|
1892
|
+
html += '<h2>GPU</h2><table>';
|
|
1893
|
+
html += row('Model', data.gpu.model || '');
|
|
1894
|
+
html += row('Utilization', (data.gpu.utilization || 0) + '%');
|
|
1895
|
+
html += row('Memory', formatBytes(data.gpu.memory));
|
|
1896
|
+
if (data.gpu.temperature) html += row('Temperature', data.gpu.temperature.toFixed(1) + '\xB0C');
|
|
1897
|
+
html += '</table>';
|
|
1898
|
+
}
|
|
1899
|
+
|
|
1900
|
+
// Network
|
|
1901
|
+
if (data.network) {
|
|
1902
|
+
html += '<h2>Network</h2><table>';
|
|
1903
|
+
html += row('Interface', (data.network.interface || '') + ' (' + (data.network.ip || '') + ')');
|
|
1904
|
+
html += row('RX', formatBytes(data.network.rxBytes) + ' / ' + (data.network.rxPackets || 0) + ' pkt');
|
|
1905
|
+
html += row('TX', formatBytes(data.network.txBytes) + ' / ' + (data.network.txPackets || 0) + ' pkt');
|
|
1906
|
+
html += '</table>';
|
|
1907
|
+
}
|
|
1908
|
+
|
|
1909
|
+
// Disk
|
|
1910
|
+
if (data.disk && data.disk.length) {
|
|
1911
|
+
html += '<h2>Disk</h2><table>';
|
|
1912
|
+
data.disk.forEach(d => {
|
|
1913
|
+
let io = '';
|
|
1914
|
+
if (d.readBytesSec !== undefined || d.writeBytesSec !== undefined) {
|
|
1915
|
+
io = ' | R: ' + formatBytes(d.readBytesSec || 0) + '/s W: ' + formatBytes(d.writeBytesSec || 0) + '/s';
|
|
1916
|
+
}
|
|
1917
|
+
html += row(d.mountpoint, d.used + ' / ' + d.size + ' (' + d.capacity + ')' + io);
|
|
1918
|
+
});
|
|
1919
|
+
html += '</table>';
|
|
1920
|
+
}
|
|
1921
|
+
|
|
1922
|
+
// Processes
|
|
1923
|
+
if (data.processes && data.processes.length) {
|
|
1924
|
+
html += '<h2>Processes (Top ' + Math.min(50, data.processes.length) + ')</h2><table>';
|
|
1925
|
+
data.processes.slice(0, 50).forEach(p => {
|
|
1926
|
+
html += row(String(p.pid), p.user + ' CPU: ' + p.cpu.toFixed(1) + '% MEM: ' + p.mem.toFixed(1) + '% ' + p.command);
|
|
1927
|
+
});
|
|
1928
|
+
html += '</table>';
|
|
1929
|
+
}
|
|
1930
|
+
|
|
1931
|
+
const contentEl = document.getElementById('portal-content');
|
|
1932
|
+
if (contentEl) contentEl.innerHTML = html;
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1935
|
+
if (window.EventSource) {
|
|
1936
|
+
const evtSource = new EventSource('/api/stream');
|
|
1937
|
+
evtSource.onmessage = function(event) {
|
|
1938
|
+
try {
|
|
1939
|
+
const data = JSON.parse(event.data);
|
|
1940
|
+
renderStats(data);
|
|
1941
|
+
} catch(e) {}
|
|
1942
|
+
};
|
|
1943
|
+
} else {
|
|
1944
|
+
setInterval(function() {
|
|
1945
|
+
fetch('/api').then(r => r.json()).then(renderStats).catch(function(){});
|
|
1946
|
+
}, 2000);
|
|
1947
|
+
}
|
|
1948
|
+
</script>
|
|
1707
1949
|
</body>
|
|
1708
1950
|
</html>`;
|
|
1709
1951
|
}
|
|
@@ -2152,11 +2394,11 @@ var History = class _History {
|
|
|
2152
2394
|
};
|
|
2153
2395
|
|
|
2154
2396
|
// src/state/config.ts
|
|
2155
|
-
import
|
|
2156
|
-
import
|
|
2157
|
-
import
|
|
2158
|
-
var CONFIG_DIR =
|
|
2159
|
-
var CONFIG_FILE =
|
|
2397
|
+
import fs2 from "fs";
|
|
2398
|
+
import os3 from "os";
|
|
2399
|
+
import path2 from "path";
|
|
2400
|
+
var CONFIG_DIR = path2.join(os3.homedir(), ".config", "pyre");
|
|
2401
|
+
var CONFIG_FILE = path2.join(CONFIG_DIR, "config.json");
|
|
2160
2402
|
var DEFAULT_CONFIG = {
|
|
2161
2403
|
theme: "default",
|
|
2162
2404
|
interval: 2,
|
|
@@ -2170,12 +2412,17 @@ var DEFAULT_CONFIG = {
|
|
|
2170
2412
|
mouseEnabled: true,
|
|
2171
2413
|
cpuAlertPct: 90,
|
|
2172
2414
|
tempAlertC: 95,
|
|
2415
|
+
watchdogProcess: "",
|
|
2416
|
+
watchdogCpu: 80,
|
|
2417
|
+
watchdogMem: 80,
|
|
2173
2418
|
notificationsEnabled: true,
|
|
2174
2419
|
p2pPort: 9876,
|
|
2175
2420
|
p2pPassword: "pyre",
|
|
2176
2421
|
splashEnabled: true,
|
|
2177
2422
|
splashColorScheme: "fire",
|
|
2178
2423
|
splashAnimation: "classic",
|
|
2424
|
+
webhookUrl: "",
|
|
2425
|
+
alertCmd: "",
|
|
2179
2426
|
visiblePanels: {
|
|
2180
2427
|
cpu: true,
|
|
2181
2428
|
mem: true,
|
|
@@ -2189,12 +2436,13 @@ var DEFAULT_CONFIG = {
|
|
|
2189
2436
|
disk: true,
|
|
2190
2437
|
process: true,
|
|
2191
2438
|
tree: false
|
|
2192
|
-
}
|
|
2439
|
+
},
|
|
2440
|
+
panelLayout: ["mem", "disk", "net"]
|
|
2193
2441
|
};
|
|
2194
2442
|
function readConfig() {
|
|
2195
2443
|
try {
|
|
2196
|
-
if (!
|
|
2197
|
-
const raw =
|
|
2444
|
+
if (!fs2.existsSync(CONFIG_FILE)) return { ...DEFAULT_CONFIG };
|
|
2445
|
+
const raw = fs2.readFileSync(CONFIG_FILE, "utf-8");
|
|
2198
2446
|
const parsed = JSON.parse(raw);
|
|
2199
2447
|
return deepMerge(DEFAULT_CONFIG, parsed);
|
|
2200
2448
|
} catch {
|
|
@@ -2203,11 +2451,11 @@ function readConfig() {
|
|
|
2203
2451
|
}
|
|
2204
2452
|
function writeConfig(config3) {
|
|
2205
2453
|
try {
|
|
2206
|
-
if (!
|
|
2207
|
-
|
|
2454
|
+
if (!fs2.existsSync(CONFIG_DIR)) {
|
|
2455
|
+
fs2.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
2208
2456
|
}
|
|
2209
2457
|
const merged = deepMerge(readConfig(), config3);
|
|
2210
|
-
|
|
2458
|
+
fs2.writeFileSync(CONFIG_FILE, JSON.stringify(merged, null, 2) + "\n");
|
|
2211
2459
|
} catch {
|
|
2212
2460
|
}
|
|
2213
2461
|
}
|
|
@@ -2254,6 +2502,7 @@ var state = {
|
|
|
2254
2502
|
disk: config.visiblePanels.disk,
|
|
2255
2503
|
process: config.visiblePanels.process
|
|
2256
2504
|
},
|
|
2505
|
+
panelLayout: config.panelLayout || ["mem", "disk", "net"],
|
|
2257
2506
|
logging: false,
|
|
2258
2507
|
logStream: null,
|
|
2259
2508
|
statusMessage: "",
|
|
@@ -2295,7 +2544,12 @@ var state = {
|
|
|
2295
2544
|
],
|
|
2296
2545
|
CPU_ALERT_PCT: config.cpuAlertPct,
|
|
2297
2546
|
TEMP_ALERT_C: config.tempAlertC,
|
|
2547
|
+
watchdogProcess: config.watchdogProcess || "",
|
|
2548
|
+
watchdogCpu: config.watchdogCpu || 80,
|
|
2549
|
+
watchdogMem: config.watchdogMem || 80,
|
|
2298
2550
|
notificationsEnabled: config.notificationsEnabled,
|
|
2551
|
+
webhookUrl: config.webhookUrl || "",
|
|
2552
|
+
alertCmd: config.alertCmd || "",
|
|
2299
2553
|
alerted: false,
|
|
2300
2554
|
activePanel: "grid",
|
|
2301
2555
|
mouseEnabled: config.mouseEnabled,
|
|
@@ -2311,14 +2565,18 @@ var state = {
|
|
|
2311
2565
|
{ id: "tasks", label: "Tasks", key: "9" },
|
|
2312
2566
|
{ id: "disk", label: "Disk", key: "0" },
|
|
2313
2567
|
{ id: "process", label: "Process", key: "P" },
|
|
2314
|
-
{ id: "p2p", label: "P2P", key: "R" }
|
|
2568
|
+
{ id: "p2p", label: "P2P", key: "R" },
|
|
2569
|
+
{ id: "anomalies", label: "Anomalies", key: "A" }
|
|
2315
2570
|
],
|
|
2316
|
-
p2pEvents: [],
|
|
2317
|
-
p2pBind: "",
|
|
2318
2571
|
p2pServer: null,
|
|
2572
|
+
p2pClient: null,
|
|
2319
2573
|
p2pServerRunning: false,
|
|
2320
|
-
|
|
2574
|
+
p2pClientConnected: false,
|
|
2575
|
+
p2pEvents: [],
|
|
2576
|
+
p2pBind: "",
|
|
2321
2577
|
p2pPassword: config.p2pPassword,
|
|
2578
|
+
p2pPort: config.p2pPort,
|
|
2579
|
+
anomalyHistory: [],
|
|
2322
2580
|
splashEnabled: config.splashEnabled,
|
|
2323
2581
|
splashColorScheme: config.splashColorScheme,
|
|
2324
2582
|
splashAnimation: config.splashAnimation
|
|
@@ -2349,10 +2607,10 @@ function getToggleKey(opt) {
|
|
|
2349
2607
|
}
|
|
2350
2608
|
|
|
2351
2609
|
// src/live/export.ts
|
|
2352
|
-
import
|
|
2353
|
-
import
|
|
2610
|
+
import fs3 from "fs";
|
|
2611
|
+
import path3 from "path";
|
|
2354
2612
|
function ensureDir(dir) {
|
|
2355
|
-
if (!
|
|
2613
|
+
if (!fs3.existsSync(dir)) fs3.mkdirSync(dir, { recursive: true });
|
|
2356
2614
|
}
|
|
2357
2615
|
function timestampForFile() {
|
|
2358
2616
|
return (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
@@ -2365,7 +2623,7 @@ function exportSnapshot() {
|
|
|
2365
2623
|
try {
|
|
2366
2624
|
ensureDir(state.exportDir);
|
|
2367
2625
|
const ext = state.exportFormat === "json" ? "json" : state.exportFormat === "csv" ? "csv" : state.exportFormat === "tsv" ? "tsv" : state.exportFormat === "html" ? "html" : "md";
|
|
2368
|
-
const file =
|
|
2626
|
+
const file = path3.join(state.exportDir, `pyre-${timestampForFile()}.${ext}`);
|
|
2369
2627
|
let content;
|
|
2370
2628
|
switch (state.exportFormat) {
|
|
2371
2629
|
case "json":
|
|
@@ -2384,7 +2642,7 @@ function exportSnapshot() {
|
|
|
2384
2642
|
content = formatMarkdown(state.lastData);
|
|
2385
2643
|
break;
|
|
2386
2644
|
}
|
|
2387
|
-
|
|
2645
|
+
fs3.writeFileSync(file, content);
|
|
2388
2646
|
setStatus(`Exported snapshot -> ${file}`);
|
|
2389
2647
|
} catch (err) {
|
|
2390
2648
|
setStatus(`Export failed: ${err.message}`);
|
|
@@ -2394,8 +2652,8 @@ function startLogging() {
|
|
|
2394
2652
|
try {
|
|
2395
2653
|
ensureDir(state.exportDir);
|
|
2396
2654
|
rotateLogs();
|
|
2397
|
-
const file =
|
|
2398
|
-
state.logStream =
|
|
2655
|
+
const file = path3.join(state.exportDir, `pyre-log-${timestampForFile()}.csv`);
|
|
2656
|
+
state.logStream = fs3.createWriteStream(file, { flags: "a" });
|
|
2399
2657
|
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");
|
|
2400
2658
|
state.logging = true;
|
|
2401
2659
|
setStatus(`Logging every tick -> ${file}`);
|
|
@@ -2409,37 +2667,37 @@ var MAX_LOG_BYTES = 50 * 1024 * 1024;
|
|
|
2409
2667
|
function rotateLogs() {
|
|
2410
2668
|
try {
|
|
2411
2669
|
const dir = state.exportDir;
|
|
2412
|
-
if (!
|
|
2413
|
-
const files =
|
|
2670
|
+
if (!fs3.existsSync(dir)) return;
|
|
2671
|
+
const files = fs3.readdirSync(dir).filter((f) => f.startsWith("pyre-log-") && f.endsWith(".csv")).map((f) => ({ name: f, path: path3.join(dir, f), mtime: fs3.statSync(path3.join(dir, f)).mtimeMs, size: fs3.statSync(path3.join(dir, f)).size })).sort((a, b) => a.mtime - b.mtime);
|
|
2414
2672
|
const now = Date.now();
|
|
2415
2673
|
for (const f of files) {
|
|
2416
2674
|
if (now - f.mtime > MAX_LOG_AGE_MS) {
|
|
2417
2675
|
try {
|
|
2418
|
-
|
|
2676
|
+
fs3.unlinkSync(f.path);
|
|
2419
2677
|
} catch {
|
|
2420
2678
|
}
|
|
2421
2679
|
}
|
|
2422
2680
|
}
|
|
2423
|
-
const remaining =
|
|
2681
|
+
const remaining = fs3.readdirSync(dir).filter((f) => f.startsWith("pyre-log-") && f.endsWith(".csv"));
|
|
2424
2682
|
if (remaining.length > MAX_LOG_COUNT) {
|
|
2425
2683
|
const toDelete = remaining.sort().slice(0, remaining.length - MAX_LOG_COUNT);
|
|
2426
2684
|
for (const f of toDelete) {
|
|
2427
2685
|
try {
|
|
2428
|
-
|
|
2686
|
+
fs3.unlinkSync(path3.join(dir, f));
|
|
2429
2687
|
} catch {
|
|
2430
2688
|
}
|
|
2431
2689
|
}
|
|
2432
2690
|
}
|
|
2433
2691
|
let totalSize = 0;
|
|
2434
2692
|
for (const f of remaining) {
|
|
2435
|
-
totalSize +=
|
|
2693
|
+
totalSize += fs3.statSync(path3.join(dir, f)).size;
|
|
2436
2694
|
}
|
|
2437
2695
|
if (totalSize > MAX_LOG_BYTES) {
|
|
2438
2696
|
const sorted = remaining.sort();
|
|
2439
2697
|
for (const f of sorted) {
|
|
2440
|
-
const sz =
|
|
2698
|
+
const sz = fs3.statSync(path3.join(dir, f)).size;
|
|
2441
2699
|
try {
|
|
2442
|
-
|
|
2700
|
+
fs3.unlinkSync(path3.join(dir, f));
|
|
2443
2701
|
} catch {
|
|
2444
2702
|
}
|
|
2445
2703
|
totalSize -= sz;
|
|
@@ -2475,7 +2733,7 @@ function writeLogRow(data) {
|
|
|
2475
2733
|
}
|
|
2476
2734
|
|
|
2477
2735
|
// src/live/render.ts
|
|
2478
|
-
import { execFile } from "child_process";
|
|
2736
|
+
import { execFile, exec as exec2 } from "child_process";
|
|
2479
2737
|
import chalk5 from "chalk";
|
|
2480
2738
|
|
|
2481
2739
|
// src/anomalies.ts
|
|
@@ -2590,7 +2848,8 @@ function tableCacheParams() {
|
|
|
2590
2848
|
state.activePanel,
|
|
2591
2849
|
state.treeView ? "1" : "0",
|
|
2592
2850
|
state.history.version,
|
|
2593
|
-
JSON.stringify(state.visiblePanels)
|
|
2851
|
+
JSON.stringify(state.visiblePanels),
|
|
2852
|
+
JSON.stringify(state.panelLayout)
|
|
2594
2853
|
].join("|");
|
|
2595
2854
|
}
|
|
2596
2855
|
function graphsCacheParams() {
|
|
@@ -2634,13 +2893,15 @@ function p2pPanelLines() {
|
|
|
2634
2893
|
const title = `P2P \xB7 ${state.p2pServerRunning ? "ON" : "OFF"} \xB7 ${events.length} events`;
|
|
2635
2894
|
return { title, body };
|
|
2636
2895
|
}
|
|
2637
|
-
function renderCustomizerOverlay() {
|
|
2896
|
+
function renderCustomizerOverlay(scrollOffset, maxVisibleItems) {
|
|
2638
2897
|
const themesList = Object.keys(THEMES);
|
|
2639
|
-
const
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2898
|
+
const headerLine = chalk5.bgCyan.black.bold(" UI CUSTOMIZER (Use \u2191/\u2193 or J/K to navigate, Enter/Space to toggle, Esc to exit) ");
|
|
2899
|
+
const totalItems = state.CUSTOMIZER_OPTIONS.length;
|
|
2900
|
+
const end = Math.min(scrollOffset + maxVisibleItems, totalItems);
|
|
2901
|
+
const lines = [headerLine];
|
|
2902
|
+
for (let i = scrollOffset; i < end; i++) {
|
|
2903
|
+
const opt = state.CUSTOMIZER_OPTIONS[i];
|
|
2904
|
+
const isSelected = i === state.customizerIndex;
|
|
2644
2905
|
const prefix = isSelected ? chalk5.yellow("\u25B6 ") : " ";
|
|
2645
2906
|
if (opt === "Theme") {
|
|
2646
2907
|
lines.push(`${prefix}${opt}: ${chalk5.bold.green(state.currentTheme)} [${themesList.join(", ")}]`);
|
|
@@ -2664,7 +2925,7 @@ function renderCustomizerOverlay() {
|
|
|
2664
2925
|
const status = isVisible ? chalk5.green("[VISIBLE]") : chalk5.red("[HIDDEN]");
|
|
2665
2926
|
lines.push(`${prefix}${opt}: ${status}`);
|
|
2666
2927
|
}
|
|
2667
|
-
}
|
|
2928
|
+
}
|
|
2668
2929
|
return lines.map((l) => ` ${l}`).join("\n");
|
|
2669
2930
|
}
|
|
2670
2931
|
var _prevFrameLines = [];
|
|
@@ -2719,9 +2980,11 @@ function render() {
|
|
|
2719
2980
|
treeView: state.treeView,
|
|
2720
2981
|
activePanel: state.activePanel,
|
|
2721
2982
|
anomalies,
|
|
2983
|
+
anomalyHistory: state.anomalyHistory,
|
|
2722
2984
|
tempUnit: state.tempUnit,
|
|
2723
2985
|
history: state.history,
|
|
2724
|
-
graphMode: state.graphMode
|
|
2986
|
+
graphMode: state.graphMode,
|
|
2987
|
+
panelLayout: state.panelLayout
|
|
2725
2988
|
});
|
|
2726
2989
|
_cachedTableData = state.lastData;
|
|
2727
2990
|
_cachedTableParams = cacheParams;
|
|
@@ -2752,9 +3015,18 @@ function render() {
|
|
|
2752
3015
|
const footerLine22 = footerLine()[1] || "";
|
|
2753
3016
|
const footerLine3 = footerLine()[2] || " ";
|
|
2754
3017
|
const footerLines = [footerLine1, footerLine22, footerLine3];
|
|
3018
|
+
const footerHeight = 3;
|
|
3019
|
+
const targetBodyHeight = Math.max(1, state.termHeight - footerHeight);
|
|
2755
3020
|
if (state.inputMode === "customizer") {
|
|
2756
|
-
bodyLines.
|
|
2757
|
-
|
|
3021
|
+
const preOverlayCount = bodyLines.length;
|
|
3022
|
+
const availableForOverlay = targetBodyHeight - preOverlayCount;
|
|
3023
|
+
const totalItems = state.CUSTOMIZER_OPTIONS.length;
|
|
3024
|
+
const headerLines = 1;
|
|
3025
|
+
const blankLines = availableForOverlay >= 2 ? 1 : 0;
|
|
3026
|
+
const maxVisibleItems = Math.max(0, availableForOverlay - headerLines - blankLines);
|
|
3027
|
+
const scrollOffset = totalItems > maxVisibleItems ? Math.max(0, Math.min(state.customizerIndex, totalItems - maxVisibleItems)) : 0;
|
|
3028
|
+
if (blankLines > 0) bodyLines.push("");
|
|
3029
|
+
bodyLines.push(...renderCustomizerOverlay(scrollOffset, maxVisibleItems).split("\n"));
|
|
2758
3030
|
} else if (state.inputMode === "filter") {
|
|
2759
3031
|
bodyLines.push(chalk5.cyan(` Filter processes: ${state.inputBuffer}_`));
|
|
2760
3032
|
} else if (state.inputMode === "kill") {
|
|
@@ -2770,8 +3042,6 @@ function render() {
|
|
|
2770
3042
|
} else if (state.statusMessage) {
|
|
2771
3043
|
bodyLines.push(` ${state.statusMessage}`);
|
|
2772
3044
|
}
|
|
2773
|
-
const footerHeight = 3;
|
|
2774
|
-
const targetBodyHeight = Math.max(1, state.termHeight - footerHeight);
|
|
2775
3045
|
if (bodyLines.length > targetBodyHeight) {
|
|
2776
3046
|
bodyLines.length = targetBodyHeight;
|
|
2777
3047
|
}
|
|
@@ -2829,21 +3099,54 @@ function checkAlerts(data) {
|
|
|
2829
3099
|
const temp = data.cpu.temperature ?? data.thermal.temperatures?.cpu_die ?? null;
|
|
2830
3100
|
const hot = data.cpu.usage >= state.CPU_ALERT_PCT || temp !== null && temp >= state.TEMP_ALERT_C;
|
|
2831
3101
|
const anomalies = detectAnomalies(data, state.history);
|
|
3102
|
+
for (const a of anomalies) {
|
|
3103
|
+
const timestamped = { ...a, timestamp: /* @__PURE__ */ new Date() };
|
|
3104
|
+
const recent = state.anomalyHistory.slice(-5);
|
|
3105
|
+
const isDuplicate = recent.some((r) => r.metric === a.metric && Math.abs(r.zScore - a.zScore) < 0.5);
|
|
3106
|
+
if (!isDuplicate) {
|
|
3107
|
+
state.anomalyHistory.push(timestamped);
|
|
3108
|
+
if (state.anomalyHistory.length > 50) state.anomalyHistory.shift();
|
|
3109
|
+
}
|
|
3110
|
+
}
|
|
2832
3111
|
const hasCriticalAnomaly = anomalies.some((a) => a.severity === "critical");
|
|
2833
3112
|
const hasWarningAnomaly = anomalies.some((a) => a.severity === "warning");
|
|
3113
|
+
let watchdogTriggered = false;
|
|
3114
|
+
if (state.watchdogProcess) {
|
|
3115
|
+
const matches = data.processes.filter((p) => p.command.toLowerCase().includes(state.watchdogProcess.toLowerCase()));
|
|
3116
|
+
for (const p of matches) {
|
|
3117
|
+
if (p.cpu >= state.watchdogCpu || p.mem >= state.watchdogMem) {
|
|
3118
|
+
watchdogTriggered = true;
|
|
3119
|
+
break;
|
|
3120
|
+
}
|
|
3121
|
+
}
|
|
3122
|
+
}
|
|
2834
3123
|
const anomalyTriggered = hasCriticalAnomaly || hasWarningAnomaly;
|
|
2835
|
-
if ((hot || anomalyTriggered) && !state.alerted) {
|
|
3124
|
+
if ((hot || anomalyTriggered || watchdogTriggered) && !state.alerted) {
|
|
2836
3125
|
state.alerted = true;
|
|
2837
3126
|
process.stdout.write("\x07");
|
|
2838
3127
|
const reasons = [];
|
|
2839
3128
|
if (data.cpu.usage >= state.CPU_ALERT_PCT) reasons.push(`CPU at ${data.cpu.usage}%`);
|
|
2840
3129
|
if (temp !== null && temp >= state.TEMP_ALERT_C) reasons.push(`Temp at ${formatTempForUnit(temp)}`);
|
|
3130
|
+
if (watchdogTriggered) reasons.push(`Watchdog process "${state.watchdogProcess}" crossed threshold`);
|
|
2841
3131
|
for (const a of anomalies) {
|
|
2842
3132
|
reasons.push(`${a.metric} anomaly (\u03C3=${a.zScore.toFixed(1)})`);
|
|
2843
3133
|
}
|
|
2844
3134
|
setStatus(chalk5.red(`\u26A0 Alert: ${reasons.join("; ")}`), 5e3);
|
|
2845
3135
|
sendNotification("pyre Alert", reasons.join("; "));
|
|
2846
|
-
|
|
3136
|
+
const alertMsg = reasons.join("; ");
|
|
3137
|
+
if (state.webhookUrl) {
|
|
3138
|
+
fetch(state.webhookUrl, {
|
|
3139
|
+
method: "POST",
|
|
3140
|
+
headers: { "Content-Type": "application/json" },
|
|
3141
|
+
body: JSON.stringify({ alert: alertMsg, timestamp: (/* @__PURE__ */ new Date()).toISOString() })
|
|
3142
|
+
}).catch(() => {
|
|
3143
|
+
});
|
|
3144
|
+
}
|
|
3145
|
+
if (state.alertCmd) {
|
|
3146
|
+
exec2(state.alertCmd, { env: { ...process.env, PYRE_ALERT: alertMsg } }, () => {
|
|
3147
|
+
});
|
|
3148
|
+
}
|
|
3149
|
+
} else if (!hot && !anomalyTriggered && !watchdogTriggered) {
|
|
2847
3150
|
state.alerted = false;
|
|
2848
3151
|
}
|
|
2849
3152
|
}
|
|
@@ -2851,7 +3154,7 @@ function checkAlerts(data) {
|
|
|
2851
3154
|
// src/p2p/server.ts
|
|
2852
3155
|
import net from "net";
|
|
2853
3156
|
import tls from "tls";
|
|
2854
|
-
import
|
|
3157
|
+
import fs4 from "fs";
|
|
2855
3158
|
import crypto2 from "crypto";
|
|
2856
3159
|
import chalk6 from "chalk";
|
|
2857
3160
|
|
|
@@ -2996,9 +3299,9 @@ var P2PServer = class {
|
|
|
2996
3299
|
async start() {
|
|
2997
3300
|
if (this.options.auditLog) {
|
|
2998
3301
|
try {
|
|
2999
|
-
|
|
3302
|
+
fs4.mkdirSync(this.options.auditLog, { recursive: true });
|
|
3000
3303
|
const logFile = `${this.options.auditLog}/p2p-audit.log`;
|
|
3001
|
-
this.auditStream =
|
|
3304
|
+
this.auditStream = fs4.createWriteStream(logFile, { flags: "a" });
|
|
3002
3305
|
} catch {
|
|
3003
3306
|
this.log(chalk6.yellow("Warning: could not open audit log"));
|
|
3004
3307
|
}
|
|
@@ -3007,11 +3310,11 @@ var P2PServer = class {
|
|
|
3007
3310
|
const useTLS = !!(this.options.tlsCert && this.options.tlsKey);
|
|
3008
3311
|
if (useTLS) {
|
|
3009
3312
|
const tlsOptions = {
|
|
3010
|
-
key:
|
|
3011
|
-
cert:
|
|
3313
|
+
key: fs4.readFileSync(this.options.tlsKey),
|
|
3314
|
+
cert: fs4.readFileSync(this.options.tlsCert)
|
|
3012
3315
|
};
|
|
3013
3316
|
if (this.options.tlsCA) {
|
|
3014
|
-
tlsOptions.ca =
|
|
3317
|
+
tlsOptions.ca = fs4.readFileSync(this.options.tlsCA);
|
|
3015
3318
|
}
|
|
3016
3319
|
this.server = tls.createServer(tlsOptions, (socket) => this.onConnection(socket));
|
|
3017
3320
|
} else {
|
|
@@ -3279,7 +3582,7 @@ async function startP2PServer(options) {
|
|
|
3279
3582
|
// src/p2p/client.ts
|
|
3280
3583
|
import net2 from "net";
|
|
3281
3584
|
import tls2 from "tls";
|
|
3282
|
-
import
|
|
3585
|
+
import fs5 from "fs";
|
|
3283
3586
|
import crypto3 from "crypto";
|
|
3284
3587
|
import chalk7 from "chalk";
|
|
3285
3588
|
var P2PClient = class {
|
|
@@ -3307,9 +3610,9 @@ var P2PClient = class {
|
|
|
3307
3610
|
async connect() {
|
|
3308
3611
|
if (this.options.auditLog) {
|
|
3309
3612
|
try {
|
|
3310
|
-
|
|
3613
|
+
fs5.mkdirSync(this.options.auditLog, { recursive: true });
|
|
3311
3614
|
const logFile = `${this.options.auditLog}/p2p-client-audit.log`;
|
|
3312
|
-
this.auditStream =
|
|
3615
|
+
this.auditStream = fs5.createWriteStream(logFile, { flags: "a" });
|
|
3313
3616
|
} catch {
|
|
3314
3617
|
console.log(chalk7.yellow("Warning: could not open audit log"));
|
|
3315
3618
|
}
|
|
@@ -3324,7 +3627,7 @@ var P2PClient = class {
|
|
|
3324
3627
|
rejectUnauthorized: !this.options.tlsInsecure
|
|
3325
3628
|
};
|
|
3326
3629
|
if (this.options.tlsCA) {
|
|
3327
|
-
tlsOptions.ca =
|
|
3630
|
+
tlsOptions.ca = fs5.readFileSync(this.options.tlsCA);
|
|
3328
3631
|
}
|
|
3329
3632
|
this.socket = tls2.connect(tlsOptions, () => {
|
|
3330
3633
|
this.running = true;
|
|
@@ -3678,11 +3981,19 @@ function handleInputModeKey(str, key) {
|
|
|
3678
3981
|
}
|
|
3679
3982
|
if (key.name === "backspace") {
|
|
3680
3983
|
state.inputBuffer = state.inputBuffer.slice(0, -1);
|
|
3984
|
+
if (state.inputMode === "filter") {
|
|
3985
|
+
state.processFilter = state.inputBuffer;
|
|
3986
|
+
invalidateTableCache();
|
|
3987
|
+
}
|
|
3681
3988
|
render();
|
|
3682
3989
|
return;
|
|
3683
3990
|
}
|
|
3684
3991
|
if (str && str.length === 1 && !key.ctrl && !key.meta) {
|
|
3685
3992
|
state.inputBuffer += str;
|
|
3993
|
+
if (state.inputMode === "filter") {
|
|
3994
|
+
state.processFilter = state.inputBuffer;
|
|
3995
|
+
invalidateTableCache();
|
|
3996
|
+
}
|
|
3686
3997
|
render();
|
|
3687
3998
|
}
|
|
3688
3999
|
}
|
|
@@ -4439,11 +4750,183 @@ async function showSplash(config3) {
|
|
|
4439
4750
|
});
|
|
4440
4751
|
}
|
|
4441
4752
|
|
|
4753
|
+
// src/historyCmd.ts
|
|
4754
|
+
import fs6 from "fs";
|
|
4755
|
+
import path4 from "path";
|
|
4756
|
+
import chalk9 from "chalk";
|
|
4757
|
+
function runHistoryCommand(opts2 = {}) {
|
|
4758
|
+
const days = opts2.days || 7;
|
|
4759
|
+
const dir = opts2.dir || "./pyre-exports";
|
|
4760
|
+
console.log(chalk9.bold(`
|
|
4761
|
+
pyre history \u2014 Past ${days} Days Trend
|
|
4762
|
+
`));
|
|
4763
|
+
if (!fs6.existsSync(dir)) {
|
|
4764
|
+
console.log(chalk9.yellow(` No logs directory found at ${dir}`));
|
|
4765
|
+
return;
|
|
4766
|
+
}
|
|
4767
|
+
const files = fs6.readdirSync(dir).filter((f) => f.startsWith("pyre-log-") && f.endsWith(".csv"));
|
|
4768
|
+
if (files.length === 0) {
|
|
4769
|
+
console.log(chalk9.yellow(` No CSV logs found in ${dir}`));
|
|
4770
|
+
return;
|
|
4771
|
+
}
|
|
4772
|
+
const cutoff = Date.now() - days * 24 * 60 * 60 * 1e3;
|
|
4773
|
+
const cpuPoints = [];
|
|
4774
|
+
const memPoints = [];
|
|
4775
|
+
const tempPoints = [];
|
|
4776
|
+
for (const file of files.sort()) {
|
|
4777
|
+
const fullPath = path4.join(dir, file);
|
|
4778
|
+
try {
|
|
4779
|
+
const content = fs6.readFileSync(fullPath, "utf-8");
|
|
4780
|
+
const lines = content.split("\n").slice(1);
|
|
4781
|
+
for (const line of lines) {
|
|
4782
|
+
if (!line.trim()) continue;
|
|
4783
|
+
const parts = line.split(",");
|
|
4784
|
+
if (parts.length >= 4) {
|
|
4785
|
+
const tsStr = parts[0];
|
|
4786
|
+
const ts = new Date(tsStr).getTime();
|
|
4787
|
+
if (!isNaN(ts) && ts >= cutoff) {
|
|
4788
|
+
const cpu = parseFloat(parts[1]);
|
|
4789
|
+
const mem = parseFloat(parts[2]);
|
|
4790
|
+
const temp = parseFloat(parts[3]);
|
|
4791
|
+
if (!isNaN(cpu)) cpuPoints.push(cpu);
|
|
4792
|
+
if (!isNaN(mem)) memPoints.push(mem);
|
|
4793
|
+
if (!isNaN(temp)) tempPoints.push(temp);
|
|
4794
|
+
}
|
|
4795
|
+
}
|
|
4796
|
+
}
|
|
4797
|
+
} catch {
|
|
4798
|
+
}
|
|
4799
|
+
}
|
|
4800
|
+
if (cpuPoints.length === 0) {
|
|
4801
|
+
console.log(chalk9.yellow(` No historical entries recorded within the last ${days} days.`));
|
|
4802
|
+
return;
|
|
4803
|
+
}
|
|
4804
|
+
console.log(` Data Points: ${cpuPoints.length} logged snapshots`);
|
|
4805
|
+
console.log(` CPU Usage: ${sparkline(cpuPoints, { min: 0, max: 100 })} (${(cpuPoints.reduce((a, b) => a + b, 0) / cpuPoints.length).toFixed(1)}% avg)`);
|
|
4806
|
+
console.log(` Memory: ${sparkline(memPoints, { min: 0, max: 100 })} (${(memPoints.reduce((a, b) => a + b, 0) / memPoints.length).toFixed(1)}% avg)`);
|
|
4807
|
+
if (tempPoints.length) {
|
|
4808
|
+
console.log(` Temperature: ${sparkline(tempPoints)} (${(tempPoints.reduce((a, b) => a + b, 0) / tempPoints.length).toFixed(1)}\xB0C avg)`);
|
|
4809
|
+
}
|
|
4810
|
+
console.log();
|
|
4811
|
+
}
|
|
4812
|
+
|
|
4813
|
+
// src/diffCmd.ts
|
|
4814
|
+
import fs7 from "fs";
|
|
4815
|
+
import chalk10 from "chalk";
|
|
4816
|
+
function runDiffCommand(file1, file2) {
|
|
4817
|
+
console.log(chalk10.bold(`
|
|
4818
|
+
pyre diff \u2014 Snapshot Comparison
|
|
4819
|
+
`));
|
|
4820
|
+
console.log(chalk10.dim(` Snapshot A: ${file1}`));
|
|
4821
|
+
console.log(chalk10.dim(` Snapshot B: ${file2}
|
|
4822
|
+
`));
|
|
4823
|
+
try {
|
|
4824
|
+
const dataA = JSON.parse(fs7.readFileSync(file1, "utf-8"));
|
|
4825
|
+
const dataB = JSON.parse(fs7.readFileSync(file2, "utf-8"));
|
|
4826
|
+
const diffNum = (label, valA, valB, unit = "") => {
|
|
4827
|
+
const delta = valB - valA;
|
|
4828
|
+
const sign = delta > 0 ? "+" : "";
|
|
4829
|
+
const color = delta > 0 ? chalk10.red : delta < 0 ? chalk10.green : chalk10.dim;
|
|
4830
|
+
console.log(` ${label.padEnd(20)} ${valA.toFixed(1)}${unit} \u2794 ${valB.toFixed(1)}${unit} (${color(sign + delta.toFixed(1) + unit)})`);
|
|
4831
|
+
};
|
|
4832
|
+
diffNum("CPU Usage", dataA.cpu.usage, dataB.cpu.usage, "%");
|
|
4833
|
+
diffNum("Memory Usage", dataA.memory.usagePercent, dataB.memory.usagePercent, "%");
|
|
4834
|
+
if (dataA.cpu.temperature && dataB.cpu.temperature) {
|
|
4835
|
+
diffNum("CPU Temp", dataA.cpu.temperature, dataB.cpu.temperature, "\xB0C");
|
|
4836
|
+
}
|
|
4837
|
+
if (dataA.gpu && dataB.gpu) {
|
|
4838
|
+
diffNum("GPU Util", dataA.gpu.utilization, dataB.gpu.utilization, "%");
|
|
4839
|
+
}
|
|
4840
|
+
diffNum("Network RX Packets", dataA.network.rxPackets, dataB.network.rxPackets);
|
|
4841
|
+
diffNum("Network TX Packets", dataA.network.txPackets, dataB.network.txPackets);
|
|
4842
|
+
console.log();
|
|
4843
|
+
} catch (err) {
|
|
4844
|
+
console.log(chalk10.red(`Failed to compare snapshots: ${err.message}`));
|
|
4845
|
+
}
|
|
4846
|
+
}
|
|
4847
|
+
|
|
4848
|
+
// src/xbar.ts
|
|
4849
|
+
import fs8 from "fs";
|
|
4850
|
+
import path5 from "path";
|
|
4851
|
+
import os4 from "os";
|
|
4852
|
+
import chalk11 from "chalk";
|
|
4853
|
+
function generateXbarPlugin() {
|
|
4854
|
+
const pyrePath = process.argv[1];
|
|
4855
|
+
const nodePath = process.execPath;
|
|
4856
|
+
const script = `#!/bin/bash
|
|
4857
|
+
# <xbar.title>Pyre Monitor</xbar.title>
|
|
4858
|
+
# <xbar.version>v1.0</xbar.version>
|
|
4859
|
+
# <xbar.author>somalip</xbar.author>
|
|
4860
|
+
# <xbar.author.github>somalip</xbar.author.github>
|
|
4861
|
+
# <xbar.desc>Mac system monitoring powered by pyre.</xbar.desc>
|
|
4862
|
+
|
|
4863
|
+
# Run pyre to get a JSON snapshot
|
|
4864
|
+
# We use node directly to avoid env issues in xbar
|
|
4865
|
+
DATA=$("${nodePath}" "${pyrePath}" --once --json)
|
|
4866
|
+
|
|
4867
|
+
if [ -z "$DATA" ]; then
|
|
4868
|
+
echo "\u{1F525} err"
|
|
4869
|
+
echo "---"
|
|
4870
|
+
echo "Could not fetch data"
|
|
4871
|
+
exit 1
|
|
4872
|
+
fi
|
|
4873
|
+
|
|
4874
|
+
CPU=$(echo "$DATA" | grep -o '"usage": *[0-9.]*' | head -1 | awk '{print $2}')
|
|
4875
|
+
MEM=$(echo "$DATA" | grep -o '"usagePercent": *[0-9.]*' | head -1 | awk '{print $2}')
|
|
4876
|
+
TEMP=$(echo "$DATA" | grep -o '"temperature": *[0-9.]*' | head -1 | awk '{print $2}')
|
|
4877
|
+
|
|
4878
|
+
# Format to 1 decimal place
|
|
4879
|
+
CPU=$(printf "%.1f" "$CPU")
|
|
4880
|
+
MEM=$(printf "%.1f" "$MEM")
|
|
4881
|
+
TEMP=$(printf "%.1f" "$TEMP")
|
|
4882
|
+
|
|
4883
|
+
echo "\u{1F525} C:\${CPU}% M:\${MEM}% T:\${TEMP}\xB0C | font=Menlo"
|
|
4884
|
+
echo "---"
|
|
4885
|
+
echo "Open Dashboard | bash='${nodePath}' param1='${pyrePath}' param2='live' terminal=true"
|
|
4886
|
+
`;
|
|
4887
|
+
const pluginDir = path5.join(os4.homedir(), "Library", "Application Support", "xbar", "plugins");
|
|
4888
|
+
const filename = "pyre-monitor.2s.sh";
|
|
4889
|
+
const outPath = path5.join(process.cwd(), filename);
|
|
4890
|
+
fs8.writeFileSync(outPath, script);
|
|
4891
|
+
fs8.chmodSync(outPath, "755");
|
|
4892
|
+
console.log(chalk11.green(`Generated xbar plugin script at ${outPath}`));
|
|
4893
|
+
console.log(`To install, move it to your xbar plugins directory:`);
|
|
4894
|
+
console.log(chalk11.cyan(` mv ${filename} "${pluginDir}/"`));
|
|
4895
|
+
console.log(`Then refresh xbar.`);
|
|
4896
|
+
}
|
|
4897
|
+
|
|
4898
|
+
// src/fleet.ts
|
|
4899
|
+
import { execSync } from "child_process";
|
|
4900
|
+
import chalk12 from "chalk";
|
|
4901
|
+
function runFleetCommand(hosts) {
|
|
4902
|
+
console.log(chalk12.cyan(`Fetching data from ${hosts.length} hosts...`));
|
|
4903
|
+
const results = [];
|
|
4904
|
+
for (const host of hosts) {
|
|
4905
|
+
try {
|
|
4906
|
+
const out = execSync(`ssh ${host} "pyre --once --json"`, { encoding: "utf8", stdio: ["pipe", "pipe", "ignore"] });
|
|
4907
|
+
const data = JSON.parse(out);
|
|
4908
|
+
results.push({ host, data });
|
|
4909
|
+
} catch (e) {
|
|
4910
|
+
results.push({ host, error: e.message });
|
|
4911
|
+
}
|
|
4912
|
+
}
|
|
4913
|
+
for (const result of results) {
|
|
4914
|
+
console.log(chalk12.bold.blue(`
|
|
4915
|
+
=== ${result.host} ===
|
|
4916
|
+
`));
|
|
4917
|
+
if (result.error) {
|
|
4918
|
+
console.log(chalk12.red(`Error connecting to host: ${result.error}`));
|
|
4919
|
+
} else if (result.data) {
|
|
4920
|
+
console.log(formatTable(result.data, { width: process.stdout.columns || 80, activePanel: "grid" }));
|
|
4921
|
+
}
|
|
4922
|
+
}
|
|
4923
|
+
}
|
|
4924
|
+
|
|
4442
4925
|
// src/index.ts
|
|
4443
4926
|
var config2 = readConfig();
|
|
4444
|
-
var pkg = JSON.parse(
|
|
4927
|
+
var pkg = JSON.parse(fs9.readFileSync(new URL("../package.json", import.meta.url), "utf-8"));
|
|
4445
4928
|
var program = new Command();
|
|
4446
|
-
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)").option("--port <port>", "Port number for web server mode", "3000");
|
|
4929
|
+
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)").option("--webhook-url <url>", "URL to POST to on alert").option("--alert-cmd <cmd>", "Command to execute on alert").option("--port <port>", "Port number for web server mode", "3000");
|
|
4447
4930
|
program.parse(process.argv);
|
|
4448
4931
|
var opts = program.opts();
|
|
4449
4932
|
function isExportMode() {
|
|
@@ -4456,7 +4939,18 @@ function sanitizeHost2(host) {
|
|
|
4456
4939
|
return host.trim().replace(/%$/, "");
|
|
4457
4940
|
}
|
|
4458
4941
|
function detectLocalIP() {
|
|
4459
|
-
const ifaces =
|
|
4942
|
+
const ifaces = os5.networkInterfaces();
|
|
4943
|
+
const prioritizedNames = ["en0", "en1", "wlan0", "eth0"];
|
|
4944
|
+
for (const name of prioritizedNames) {
|
|
4945
|
+
const addrs = ifaces[name];
|
|
4946
|
+
if (addrs) {
|
|
4947
|
+
for (const addr of addrs) {
|
|
4948
|
+
if (addr.family === "IPv4" && !addr.internal) {
|
|
4949
|
+
return addr.address;
|
|
4950
|
+
}
|
|
4951
|
+
}
|
|
4952
|
+
}
|
|
4953
|
+
}
|
|
4460
4954
|
for (const [, addrs] of Object.entries(ifaces)) {
|
|
4461
4955
|
if (!addrs) continue;
|
|
4462
4956
|
for (const addr of addrs) {
|
|
@@ -4470,34 +4964,46 @@ function detectLocalIP() {
|
|
|
4470
4964
|
async function runServerCommand() {
|
|
4471
4965
|
const ip = detectLocalIP();
|
|
4472
4966
|
if (!ip) {
|
|
4473
|
-
console.log(
|
|
4967
|
+
console.log(chalk13.red("Could not detect a non-loopback IP address. Connect to a network first."));
|
|
4474
4968
|
process.exit(1);
|
|
4475
4969
|
}
|
|
4476
4970
|
const port = 9876;
|
|
4477
4971
|
const password = "mysecret";
|
|
4478
|
-
console.log(
|
|
4972
|
+
console.log(chalk13.bold(`
|
|
4479
4973
|
pyre server`));
|
|
4480
|
-
console.log(
|
|
4481
|
-
console.log(
|
|
4482
|
-
console.log(
|
|
4974
|
+
console.log(chalk13.dim(` Detected IP: ${ip}`));
|
|
4975
|
+
console.log(chalk13.dim(` Port: ${port}`));
|
|
4976
|
+
console.log(chalk13.dim(` Password: ${password}`));
|
|
4483
4977
|
console.log();
|
|
4484
|
-
console.log(
|
|
4485
|
-
console.log(
|
|
4978
|
+
console.log(chalk13.bold("Run this on THIS machine (server):"));
|
|
4979
|
+
console.log(chalk13.hex("#ff6a39")(`pyre p2p server --p2p-host 0.0.0.0 --p2p-port ${port} --p2p-password ${password}`));
|
|
4486
4980
|
console.log();
|
|
4487
|
-
console.log(
|
|
4488
|
-
console.log(
|
|
4981
|
+
console.log(chalk13.bold("Run this on the OTHER machine (client):"));
|
|
4982
|
+
console.log(chalk13.hex("#ff6a39")(`pyre p2p connect --p2p-host ${ip} --p2p-port ${port} --p2p-password ${password}`));
|
|
4489
4983
|
console.log();
|
|
4490
4984
|
}
|
|
4491
4985
|
async function main() {
|
|
4492
4986
|
const cmd = program.args[0];
|
|
4493
|
-
if (cmd === "
|
|
4494
|
-
|
|
4987
|
+
if (cmd === "history") {
|
|
4988
|
+
const daysIdx = program.args.indexOf("--days");
|
|
4989
|
+
const days = daysIdx !== -1 ? parseInt(program.args[daysIdx + 1], 10) || 7 : 7;
|
|
4990
|
+
runHistoryCommand({ days });
|
|
4991
|
+
return;
|
|
4992
|
+
}
|
|
4993
|
+
if (cmd === "diff") {
|
|
4994
|
+
const file1 = program.args[1];
|
|
4995
|
+
const file2 = program.args[2];
|
|
4996
|
+
if (!file1 || !file2) {
|
|
4997
|
+
console.log(chalk13.red("Usage: pyre diff <snapshot1.json> <snapshot2.json>"));
|
|
4998
|
+
process.exit(1);
|
|
4999
|
+
}
|
|
5000
|
+
runDiffCommand(file1, file2);
|
|
4495
5001
|
return;
|
|
4496
5002
|
}
|
|
4497
5003
|
if (cmd === "ssh") {
|
|
4498
5004
|
const host = program.args[1];
|
|
4499
5005
|
if (!host) {
|
|
4500
|
-
console.log(
|
|
5006
|
+
console.log(chalk13.red("Usage: pyre ssh <host>"));
|
|
4501
5007
|
process.exit(1);
|
|
4502
5008
|
}
|
|
4503
5009
|
await runSshCommand(host);
|
|
@@ -4510,7 +5016,7 @@ async function main() {
|
|
|
4510
5016
|
if (cmd === "bench") {
|
|
4511
5017
|
const benchCmd = program.args.slice(1).join(" ");
|
|
4512
5018
|
if (!benchCmd) {
|
|
4513
|
-
console.log(
|
|
5019
|
+
console.log(chalk13.red("Usage: pyre bench <command>"));
|
|
4514
5020
|
process.exit(1);
|
|
4515
5021
|
}
|
|
4516
5022
|
await runBenchCommand(benchCmd);
|
|
@@ -4522,8 +5028,8 @@ async function main() {
|
|
|
4522
5028
|
const cfg = readConfig();
|
|
4523
5029
|
console.log(JSON.stringify(cfg, null, 2));
|
|
4524
5030
|
} else if (sub === "reset") {
|
|
4525
|
-
|
|
4526
|
-
console.log(
|
|
5031
|
+
fs9.rmSync(CONFIG_FILE, { force: true });
|
|
5032
|
+
console.log(chalk13.green("Config reset to defaults"));
|
|
4527
5033
|
} else {
|
|
4528
5034
|
console.log("Usage: pyre config <show|reset>");
|
|
4529
5035
|
}
|
|
@@ -4533,8 +5039,24 @@ async function main() {
|
|
|
4533
5039
|
await runServerCommand();
|
|
4534
5040
|
return;
|
|
4535
5041
|
}
|
|
4536
|
-
|
|
4537
|
-
|
|
5042
|
+
if (cmd === "xbar") {
|
|
5043
|
+
generateXbarPlugin();
|
|
5044
|
+
return;
|
|
5045
|
+
}
|
|
5046
|
+
if (cmd === "fleet") {
|
|
5047
|
+
const hosts = program.args.slice(1);
|
|
5048
|
+
if (hosts.length === 0) {
|
|
5049
|
+
console.error(chalk13.red("Usage: pyre fleet <host1> [host2] ..."));
|
|
5050
|
+
process.exit(1);
|
|
5051
|
+
}
|
|
5052
|
+
runFleetCommand(hosts);
|
|
5053
|
+
return;
|
|
5054
|
+
}
|
|
5055
|
+
if (opts.alertCpu) config2.cpuAlertPct = Number(opts.alertCpu);
|
|
5056
|
+
if (opts.alertTemp) config2.tempAlertC = Number(opts.alertTemp);
|
|
5057
|
+
if (opts.webhookUrl) config2.webhookUrl = opts.webhookUrl;
|
|
5058
|
+
if (opts.alertCmd) config2.alertCmd = opts.alertCmd;
|
|
5059
|
+
if (opts.exportDir) config2.exportDir = opts.exportDir;
|
|
4538
5060
|
const tempUnit = opts.tempUnit === "f" ? "f" : "c";
|
|
4539
5061
|
if (cmd === "live" || !isExportMode() && !opts.once && cmd !== "p2p") {
|
|
4540
5062
|
if (!process.stdout.isTTY && cmd !== "live") {
|
|
@@ -4556,8 +5078,8 @@ async function main() {
|
|
|
4556
5078
|
theme: opts.theme || config2.theme,
|
|
4557
5079
|
exportDir: opts.exportDir || config2.exportDir,
|
|
4558
5080
|
autoLog: opts.log || config2.autoLog,
|
|
4559
|
-
alertCpu,
|
|
4560
|
-
alertTemp,
|
|
5081
|
+
alertCpu: config2.cpuAlertPct,
|
|
5082
|
+
alertTemp: config2.tempAlertC,
|
|
4561
5083
|
tempUnit
|
|
4562
5084
|
}, splashPromise);
|
|
4563
5085
|
return;
|
|
@@ -4571,7 +5093,7 @@ async function main() {
|
|
|
4571
5093
|
await runP2PConnect();
|
|
4572
5094
|
return;
|
|
4573
5095
|
} else {
|
|
4574
|
-
console.log(
|
|
5096
|
+
console.log(chalk13.red("Usage: pyre p2p <server|connect> [options]"));
|
|
4575
5097
|
process.exit(1);
|
|
4576
5098
|
}
|
|
4577
5099
|
}
|
|
@@ -4587,13 +5109,13 @@ async function main() {
|
|
|
4587
5109
|
else output = formatTable(data, { width: process.stdout.columns || 80, sortBy: opts.sort, treeView: opts.tree ?? config2.treeView, visible: { packets: opts.packets ? true : void 0 } });
|
|
4588
5110
|
console.log(output);
|
|
4589
5111
|
if (opts.out) {
|
|
4590
|
-
|
|
4591
|
-
console.log(
|
|
5112
|
+
fs9.writeFileSync(opts.out, stripAnsi(output));
|
|
5113
|
+
console.log(chalk13.dim(`
|
|
4592
5114
|
Saved to ${opts.out}`));
|
|
4593
5115
|
}
|
|
4594
5116
|
}
|
|
4595
5117
|
main().catch((err) => {
|
|
4596
|
-
console.error(
|
|
5118
|
+
console.error(chalk13.red(`Error: ${err.message}`));
|
|
4597
5119
|
process.exit(1);
|
|
4598
5120
|
});
|
|
4599
5121
|
async function runP2PServer() {
|
|
@@ -4609,15 +5131,15 @@ async function runP2PServer() {
|
|
|
4609
5131
|
const allowList = opts.p2pAllow ? opts.p2pAllow.split(",").map((s) => s.trim()).filter(Boolean) : [];
|
|
4610
5132
|
const denyList = opts.p2pDeny ? opts.p2pDeny.split(",").map((s) => s.trim()).filter(Boolean) : [];
|
|
4611
5133
|
if (!password) {
|
|
4612
|
-
console.log(
|
|
5134
|
+
console.log(chalk13.red("Error: a password is required for P2P server mode. Use --p2p-password <password>"));
|
|
4613
5135
|
process.exit(1);
|
|
4614
5136
|
}
|
|
4615
5137
|
if (tlsCert && !tlsKey) {
|
|
4616
|
-
console.log(
|
|
5138
|
+
console.log(chalk13.red("Error: --p2p-key is required when --p2p-cert is specified"));
|
|
4617
5139
|
process.exit(1);
|
|
4618
5140
|
}
|
|
4619
5141
|
if (tlsKey && !tlsCert) {
|
|
4620
|
-
console.log(
|
|
5142
|
+
console.log(chalk13.red("Error: --p2p-cert is required when --p2p-key is specified"));
|
|
4621
5143
|
process.exit(1);
|
|
4622
5144
|
}
|
|
4623
5145
|
const server = new P2PServer({
|
|
@@ -4637,18 +5159,18 @@ async function runP2PServer() {
|
|
|
4637
5159
|
hmacKey: opts.p2pHmacKey
|
|
4638
5160
|
});
|
|
4639
5161
|
const tlsEnabled = !!(tlsCert && tlsKey);
|
|
4640
|
-
console.log(
|
|
5162
|
+
console.log(chalk13.bold(`
|
|
4641
5163
|
pyre P2P Server`));
|
|
4642
|
-
console.log(
|
|
4643
|
-
console.log(
|
|
4644
|
-
console.log(
|
|
4645
|
-
console.log(
|
|
4646
|
-
console.log(
|
|
4647
|
-
console.log(
|
|
4648
|
-
console.log(
|
|
4649
|
-
if (allowList.length > 0) console.log(
|
|
4650
|
-
if (denyList.length > 0) console.log(
|
|
4651
|
-
console.log(
|
|
5164
|
+
console.log(chalk13.dim(` Host: ${host}`));
|
|
5165
|
+
console.log(chalk13.dim(` Port: ${port}`));
|
|
5166
|
+
console.log(chalk13.dim(` Protocol: ${tlsEnabled ? "TLS" : "TCP"}`));
|
|
5167
|
+
console.log(chalk13.dim(` Password: ${password}`));
|
|
5168
|
+
console.log(chalk13.dim(` Interval: ${interval}s`));
|
|
5169
|
+
console.log(chalk13.dim(` Detailed: ${detailed ? "on" : "off"}`));
|
|
5170
|
+
console.log(chalk13.dim(` Rate limit: ${rateLimit} attempts/min`));
|
|
5171
|
+
if (allowList.length > 0) console.log(chalk13.dim(` Allowed IPs: ${allowList.join(", ")}`));
|
|
5172
|
+
if (denyList.length > 0) console.log(chalk13.dim(` Denied IPs: ${denyList.join(", ")}`));
|
|
5173
|
+
console.log(chalk13.dim(` Waiting for peers...
|
|
4652
5174
|
`));
|
|
4653
5175
|
await server.start();
|
|
4654
5176
|
const rl = readline2.createInterface({ input: process.stdin });
|
|
@@ -4660,7 +5182,7 @@ async function runP2PServer() {
|
|
|
4660
5182
|
process.exit(0);
|
|
4661
5183
|
} else if (trimmed === "status") {
|
|
4662
5184
|
const info = server.info;
|
|
4663
|
-
console.log(
|
|
5185
|
+
console.log(chalk13.dim(` Peers connected: ${info.peersConnected}`));
|
|
4664
5186
|
}
|
|
4665
5187
|
});
|
|
4666
5188
|
process.once("SIGINT", () => {
|
|
@@ -4693,7 +5215,7 @@ async function runP2PConnect() {
|
|
|
4693
5215
|
const tlsCA = opts.p2pCA;
|
|
4694
5216
|
const tlsInsecure = !!opts.p2pInsecure;
|
|
4695
5217
|
if (!password) {
|
|
4696
|
-
console.log(
|
|
5218
|
+
console.log(chalk13.red("Error: a password is required for P2P connect mode. Use --p2p-password <password>"));
|
|
4697
5219
|
process.exit(1);
|
|
4698
5220
|
}
|
|
4699
5221
|
const client = new P2PClient({
|
|
@@ -4707,12 +5229,12 @@ async function runP2PConnect() {
|
|
|
4707
5229
|
hmacKey: opts.p2pHmacKey
|
|
4708
5230
|
});
|
|
4709
5231
|
client.setOnStatus((msg) => {
|
|
4710
|
-
process.stdout.write(`\r${
|
|
5232
|
+
process.stdout.write(`\r${chalk13.dim(msg)}${" ".repeat(40)}\r`);
|
|
4711
5233
|
});
|
|
4712
5234
|
client.setOnData((data) => {
|
|
4713
5235
|
const lines = [];
|
|
4714
|
-
lines.push(
|
|
4715
|
-
lines.push(
|
|
5236
|
+
lines.push(chalk13.bold(` pyre P2P Client \u2014 ${data.header.hostname}`));
|
|
5237
|
+
lines.push(chalk13.dim(` ${data.timestamp}`));
|
|
4716
5238
|
lines.push("");
|
|
4717
5239
|
lines.push(...formatTable(data, {
|
|
4718
5240
|
width: process.stdout.columns || 80,
|
|
@@ -4729,11 +5251,11 @@ async function runP2PConnect() {
|
|
|
4729
5251
|
writeFrame2(lines);
|
|
4730
5252
|
});
|
|
4731
5253
|
const protocolLabel = useTLS ? "TLS" : "TCP";
|
|
4732
|
-
console.log(
|
|
5254
|
+
console.log(chalk13.bold(`
|
|
4733
5255
|
pyre P2P Client`));
|
|
4734
|
-
console.log(
|
|
4735
|
-
console.log(
|
|
4736
|
-
console.log(
|
|
5256
|
+
console.log(chalk13.dim(` Protocol: ${protocolLabel}`));
|
|
5257
|
+
console.log(chalk13.dim(` Connecting to ${host}:${port}...`));
|
|
5258
|
+
console.log(chalk13.dim(` Press Ctrl+C to disconnect
|
|
4737
5259
|
`));
|
|
4738
5260
|
process.stdout.write("\x1B[?25l");
|
|
4739
5261
|
try {
|
|
@@ -4741,23 +5263,23 @@ async function runP2PConnect() {
|
|
|
4741
5263
|
} catch (err) {
|
|
4742
5264
|
const code = err.code;
|
|
4743
5265
|
const msg = err.message || String(err);
|
|
4744
|
-
console.log(
|
|
5266
|
+
console.log(chalk13.red(`Connection failed: ${msg}`));
|
|
4745
5267
|
if (code === "ECONNREFUSED") {
|
|
4746
|
-
console.log(
|
|
4747
|
-
console.log(
|
|
4748
|
-
console.log(
|
|
4749
|
-
console.log(
|
|
5268
|
+
console.log(chalk13.yellow(" Hints:"));
|
|
5269
|
+
console.log(chalk13.dim(" 1. Start the server: pyre p2p server --p2p-host 0.0.0.0 --p2p-port " + port + " --p2p-password <password>"));
|
|
5270
|
+
console.log(chalk13.dim(" 2. Check the server IP with: hostname -I"));
|
|
5271
|
+
console.log(chalk13.dim(" 3. Open the port on the server firewall: sudo ufw allow " + port + "/tcp"));
|
|
4750
5272
|
} else if (code === "ETIMEDOUT") {
|
|
4751
|
-
console.log(
|
|
4752
|
-
console.log(
|
|
4753
|
-
console.log(
|
|
4754
|
-
console.log(
|
|
5273
|
+
console.log(chalk13.yellow(" Hints:"));
|
|
5274
|
+
console.log(chalk13.dim(" 1. Verify the server IP is correct"));
|
|
5275
|
+
console.log(chalk13.dim(" 2. Check that both machines are on the same network/subnet"));
|
|
5276
|
+
console.log(chalk13.dim(" 3. Open the port on the server firewall"));
|
|
4755
5277
|
} else if (code === "EHOSTDOWN" || code === "EHOSTUNREACH") {
|
|
4756
|
-
console.log(
|
|
4757
|
-
console.log(
|
|
4758
|
-
console.log(
|
|
4759
|
-
console.log(
|
|
4760
|
-
console.log(
|
|
5278
|
+
console.log(chalk13.yellow(" Hints:"));
|
|
5279
|
+
console.log(chalk13.dim(" 1. The server at " + host + " is not responding"));
|
|
5280
|
+
console.log(chalk13.dim(" 2. Start the server first: pyre p2p server --p2p-host 0.0.0.0 --p2p-port " + port + " --p2p-password <password>"));
|
|
5281
|
+
console.log(chalk13.dim(" 3. Check connectivity: ping " + host));
|
|
5282
|
+
console.log(chalk13.dim(" 4. Ensure both machines are on the same LAN (ping each other first)"));
|
|
4761
5283
|
}
|
|
4762
5284
|
process.stdout.write("\x1B[?25h");
|
|
4763
5285
|
process.exit(1);
|
|
@@ -4769,41 +5291,21 @@ async function runP2PConnect() {
|
|
|
4769
5291
|
process.exit(0);
|
|
4770
5292
|
});
|
|
4771
5293
|
}
|
|
4772
|
-
async function runInfoCommand() {
|
|
4773
|
-
const data = await collectAll({ detailed: true });
|
|
4774
|
-
const lines = [];
|
|
4775
|
-
lines.push(chalk9.bold(" pyre info"));
|
|
4776
|
-
lines.push("");
|
|
4777
|
-
lines.push(` Hostname: ${data.header.hostname}`);
|
|
4778
|
-
lines.push(` OS: ${data.header.os}`);
|
|
4779
|
-
lines.push(` Uptime: ${data.header.uptime}`);
|
|
4780
|
-
lines.push(` CPU: ${data.cpu.brand}`);
|
|
4781
|
-
lines.push(` Cores: ${data.cpu.physicalCores}/${data.cpu.cores} (phys/log)`);
|
|
4782
|
-
lines.push(` Frequency: ${data.cpu.frequency} MHz`);
|
|
4783
|
-
lines.push(` Memory: ${formatBytes(data.memory.total)}`);
|
|
4784
|
-
if (data.gpu) lines.push(` GPU: ${data.gpu.model} (${formatBytes(data.gpu.memory)})`);
|
|
4785
|
-
if (data.battery) {
|
|
4786
|
-
lines.push(` Battery: ${data.battery.level}% (${data.battery.state})`);
|
|
4787
|
-
lines.push(` Health: ${data.battery.health}`);
|
|
4788
|
-
}
|
|
4789
|
-
lines.push(` Thermal: ${data.thermal.state}`);
|
|
4790
|
-
console.log(lines.join("\n"));
|
|
4791
|
-
}
|
|
4792
5294
|
async function runSshCommand(host) {
|
|
4793
5295
|
const password = opts.p2pPassword || "mysecret";
|
|
4794
5296
|
const cmd = `ssh -o LogLevel=QUIET ${host} "pyre --json --once"`;
|
|
4795
|
-
console.log(
|
|
5297
|
+
console.log(chalk13.bold(`
|
|
4796
5298
|
pyre ssh \u2014 ${host}`));
|
|
4797
|
-
console.log(
|
|
5299
|
+
console.log(chalk13.dim(` Running: ${cmd}
|
|
4798
5300
|
`));
|
|
4799
5301
|
const { execFile: execFile2 } = await import("child_process");
|
|
4800
5302
|
const child = execFile2("ssh", ["-o", "LogLevel=QUIET", host, "pyre", "--json", "--once"], (err, stdout, stderr) => {
|
|
4801
5303
|
if (err) {
|
|
4802
|
-
console.log(
|
|
4803
|
-
console.log(
|
|
5304
|
+
console.log(chalk13.red(` SSH failed: ${err.message}`));
|
|
5305
|
+
console.log(chalk13.dim(" Ensure SSH keys are set up or the host is reachable."));
|
|
4804
5306
|
process.exit(1);
|
|
4805
5307
|
}
|
|
4806
|
-
if (stderr) console.log(
|
|
5308
|
+
if (stderr) console.log(chalk13.dim(stderr));
|
|
4807
5309
|
try {
|
|
4808
5310
|
const remote = JSON.parse(stdout);
|
|
4809
5311
|
console.log(formatTable(remote, { width: process.stdout.columns || 80, sortBy: "cpu", treeView: false, visible: { packets: true } }));
|
|
@@ -4823,17 +5325,48 @@ async function runWebCommand() {
|
|
|
4823
5325
|
const port = parseInt(opts.port || process.env.PORT || "3000", 10) || 3e3;
|
|
4824
5326
|
const server = http.createServer(async (req, res) => {
|
|
4825
5327
|
const url = req.url || "/";
|
|
5328
|
+
if (url === "/health" || url === "/api/health") {
|
|
5329
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
5330
|
+
res.end(JSON.stringify({ status: "ok", uptime: process.uptime(), timestamp: (/* @__PURE__ */ new Date()).toISOString() }));
|
|
5331
|
+
return;
|
|
5332
|
+
}
|
|
5333
|
+
if (url === "/api/stream") {
|
|
5334
|
+
res.writeHead(200, {
|
|
5335
|
+
"Content-Type": "text/event-stream",
|
|
5336
|
+
"Cache-Control": "no-cache",
|
|
5337
|
+
"Connection": "keep-alive",
|
|
5338
|
+
"Access-Control-Allow-Origin": "*"
|
|
5339
|
+
});
|
|
5340
|
+
const sendStats = async () => {
|
|
5341
|
+
try {
|
|
5342
|
+
const data = await collectAll({ detailed: true });
|
|
5343
|
+
res.write(`data: ${JSON.stringify(data)}
|
|
5344
|
+
|
|
5345
|
+
`);
|
|
5346
|
+
} catch {
|
|
5347
|
+
}
|
|
5348
|
+
};
|
|
5349
|
+
sendStats();
|
|
5350
|
+
const intervalId = setInterval(sendStats, 2e3);
|
|
5351
|
+
req.on("close", () => {
|
|
5352
|
+
clearInterval(intervalId);
|
|
5353
|
+
});
|
|
5354
|
+
return;
|
|
5355
|
+
}
|
|
4826
5356
|
if (url === "/" || url === "/index.html") {
|
|
4827
5357
|
res.writeHead(200, { "Content-Type": "text/html" });
|
|
4828
|
-
const
|
|
4829
|
-
|
|
4830
|
-
|
|
4831
|
-
|
|
5358
|
+
const data = await collectAll({ detailed: true });
|
|
5359
|
+
const htmlContent = formatHtml(data);
|
|
5360
|
+
res.end(htmlContent);
|
|
5361
|
+
} else if (url === "/setup") {
|
|
5362
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
5363
|
+
const indexPath = path6.join(process.cwd(), "index.html");
|
|
5364
|
+
if (fs9.existsSync(indexPath)) {
|
|
5365
|
+
res.end(fs9.readFileSync(indexPath, "utf-8"));
|
|
4832
5366
|
} else {
|
|
4833
5367
|
const data = await collectAll({ detailed: true });
|
|
4834
|
-
|
|
5368
|
+
res.end(formatHtml(data));
|
|
4835
5369
|
}
|
|
4836
|
-
res.end(htmlContent);
|
|
4837
5370
|
} else if (url === "/api" || url === "/api/stats" || url === "/data") {
|
|
4838
5371
|
const data = await collectAll({ detailed: true });
|
|
4839
5372
|
res.writeHead(200, {
|
|
@@ -4847,12 +5380,33 @@ async function runWebCommand() {
|
|
|
4847
5380
|
res.end("Not found");
|
|
4848
5381
|
}
|
|
4849
5382
|
});
|
|
5383
|
+
server.on("error", (err) => {
|
|
5384
|
+
if (err.code === "EADDRINUSE") {
|
|
5385
|
+
console.log(chalk13.red(`
|
|
5386
|
+
Error: Port ${port} is already in use.`));
|
|
5387
|
+
console.log(chalk13.yellow(` Try specifying a different port using --port <port>, e.g.:`));
|
|
5388
|
+
console.log(chalk13.dim(` npx tsx src/index.ts web --port ${port + 1}
|
|
5389
|
+
`));
|
|
5390
|
+
process.exit(1);
|
|
5391
|
+
} else {
|
|
5392
|
+
console.log(chalk13.red(`Server error: ${err.message}`));
|
|
5393
|
+
process.exit(1);
|
|
5394
|
+
}
|
|
5395
|
+
});
|
|
5396
|
+
const ip = detectLocalIP() || "localhost";
|
|
4850
5397
|
server.listen(port, "0.0.0.0", () => {
|
|
4851
|
-
console.log(
|
|
5398
|
+
console.log(chalk13.bold(`
|
|
4852
5399
|
pyre web server running on port ${port}`));
|
|
4853
|
-
console.log(
|
|
4854
|
-
console.log(
|
|
4855
|
-
console.log(
|
|
5400
|
+
console.log(chalk13.dim(` Local URL: http://localhost:${port}/`));
|
|
5401
|
+
console.log(chalk13.hex("#ff6a39").bold(` Network URL: http://${ip}:${port}/ (Accessible on same Wi-Fi / LAN)`));
|
|
5402
|
+
console.log(chalk13.dim(` API: http://${ip}:${port}/api`));
|
|
5403
|
+
console.log(chalk13.dim(` SSE Stream: http://${ip}:${port}/api/stream`));
|
|
5404
|
+
console.log(chalk13.yellow(`
|
|
5405
|
+
Troubleshooting connection from phone/other devices:`));
|
|
5406
|
+
console.log(chalk13.dim(` 1. macOS Firewall: System Settings -> Network -> Firewall -> Allow incoming node/pyre connections`));
|
|
5407
|
+
console.log(chalk13.dim(` 2. Wi-Fi Router: Ensure "AP Isolation" / "Guest Mode" is OFF on your Wi-Fi router`));
|
|
5408
|
+
console.log(chalk13.dim(` 3. Verify phone is connected to the exact same Wi-Fi network
|
|
5409
|
+
`));
|
|
4856
5410
|
});
|
|
4857
5411
|
process.once("SIGINT", () => {
|
|
4858
5412
|
server.close();
|
|
@@ -4862,14 +5416,14 @@ async function runWebCommand() {
|
|
|
4862
5416
|
async function runBenchCommand(benchCmd) {
|
|
4863
5417
|
const { spawn } = await import("child_process");
|
|
4864
5418
|
const logDir = opts.exportDir || "./pyre-exports";
|
|
4865
|
-
const file =
|
|
4866
|
-
if (!
|
|
4867
|
-
const stream =
|
|
5419
|
+
const file = path6.join(logDir, `pyre-bench-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.csv`);
|
|
5420
|
+
if (!fs9.existsSync(logDir)) fs9.mkdirSync(logDir, { recursive: true });
|
|
5421
|
+
const stream = fs9.createWriteStream(file, { flags: "a" });
|
|
4868
5422
|
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");
|
|
4869
|
-
console.log(
|
|
5423
|
+
console.log(chalk13.bold(`
|
|
4870
5424
|
pyre bench`));
|
|
4871
|
-
console.log(
|
|
4872
|
-
console.log(
|
|
5425
|
+
console.log(chalk13.dim(` Command: ${benchCmd}`));
|
|
5426
|
+
console.log(chalk13.dim(` Log: ${file}
|
|
4873
5427
|
`));
|
|
4874
5428
|
const child = spawn("sh", ["-c", benchCmd]);
|
|
4875
5429
|
child.stdout.on("data", (d) => process.stdout.write(d));
|
|
@@ -4890,7 +5444,7 @@ async function runBenchCommand(benchCmd) {
|
|
|
4890
5444
|
child.on("close", () => {
|
|
4891
5445
|
clearInterval(handle);
|
|
4892
5446
|
stream.end();
|
|
4893
|
-
console.log(
|
|
5447
|
+
console.log(chalk13.green(`
|
|
4894
5448
|
Bench complete. Log saved to ${file}`));
|
|
4895
5449
|
process.exit(0);
|
|
4896
5450
|
});
|