pyre-cli 6.1.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 +479 -184
- package/package.json +1 -1
- package/src/anomalies.ts +1 -0
- package/src/fleet.ts +31 -0
- package/src/formatters/output.ts +107 -10
- package/src/formatters/render.ts +111 -63
- package/src/formatters/types.ts +6 -1
- package/src/index.ts +73 -47
- package/src/live/render.ts +30 -2
- package/src/live/state.ts +15 -8
- package/src/state/config.ts +6 -0
- 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";
|
|
@@ -822,6 +822,32 @@ function multiRowSparkline(values, height, opts2 = {}) {
|
|
|
822
822
|
}
|
|
823
823
|
return rows.map((r) => r.join(""));
|
|
824
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
|
+
}
|
|
825
851
|
|
|
826
852
|
// src/formatters/themes.ts
|
|
827
853
|
import chalk2 from "chalk";
|
|
@@ -999,8 +1025,8 @@ function panel(title, lines, width, accent, borderAccent, height, rightTitle) {
|
|
|
999
1025
|
} else if (visLen(topInner) < innerW) {
|
|
1000
1026
|
topInner = topInner + "\u2500".repeat(innerW - visLen(topInner));
|
|
1001
1027
|
}
|
|
1002
|
-
const top = borderAccent("\
|
|
1003
|
-
const bottom = borderAccent("\
|
|
1028
|
+
const top = borderAccent("\u256D" + topInner + "\u256E");
|
|
1029
|
+
const bottom = borderAccent("\u2570" + "\u2500".repeat(innerW) + "\u256F");
|
|
1004
1030
|
const body = [];
|
|
1005
1031
|
for (let i = 0; i < h; i++) {
|
|
1006
1032
|
const line = lines[i] ?? "";
|
|
@@ -1011,12 +1037,17 @@ function panel(title, lines, width, accent, borderAccent, height, rightTitle) {
|
|
|
1011
1037
|
function gaugeBar(percent, width = 12) {
|
|
1012
1038
|
const w = Math.max(1, width);
|
|
1013
1039
|
const pct = Math.min(100, Math.max(0, percent));
|
|
1014
|
-
const
|
|
1015
|
-
const
|
|
1016
|
-
const
|
|
1017
|
-
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));
|
|
1018
1049
|
const color = pct > 85 ? chalk3.red : pct > 65 ? chalk3.yellow : chalk3.cyan;
|
|
1019
|
-
return color(
|
|
1050
|
+
return color(str) + chalk3.dim("\u2500".repeat(empty));
|
|
1020
1051
|
}
|
|
1021
1052
|
function hstack(blocks, gap = 2, targetWidth) {
|
|
1022
1053
|
if (!blocks.length) return [];
|
|
@@ -1047,11 +1078,18 @@ function gaugeRow(label, percent, contentWidth) {
|
|
|
1047
1078
|
const labelW = 9;
|
|
1048
1079
|
const pctText = `${Math.round(percent)}%`;
|
|
1049
1080
|
const barW = Math.max(6, contentWidth - labelW - pctText.length - 1);
|
|
1050
|
-
const
|
|
1051
|
-
const
|
|
1052
|
-
const
|
|
1053
|
-
const
|
|
1054
|
-
|
|
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}`;
|
|
1055
1093
|
}
|
|
1056
1094
|
function thermalColor(pressureLevel) {
|
|
1057
1095
|
if (pressureLevel >= 3) return chalk3.red;
|
|
@@ -1207,7 +1245,8 @@ var TAB_DEFS = [
|
|
|
1207
1245
|
{ id: "tasks", label: "Tasks", key: "9" },
|
|
1208
1246
|
{ id: "disk", label: "Disk", key: "0" },
|
|
1209
1247
|
{ id: "process", label: "Process", key: "P" },
|
|
1210
|
-
{ id: "p2p", label: "P2P", key: "R" }
|
|
1248
|
+
{ id: "p2p", label: "P2P", key: "R" },
|
|
1249
|
+
{ id: "anomalies", label: "Anomalies", key: "A" }
|
|
1211
1250
|
];
|
|
1212
1251
|
var TAB_BAR_INDENT = 2;
|
|
1213
1252
|
var TAB_SEPARATOR_WIDTH = 3;
|
|
@@ -1435,6 +1474,19 @@ function activeDetailLines(data, activePanel, width, opts2 = {}) {
|
|
|
1435
1474
|
}
|
|
1436
1475
|
return processTableLines(limited, width - 4, theme.border);
|
|
1437
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
|
+
}
|
|
1438
1490
|
default:
|
|
1439
1491
|
return null;
|
|
1440
1492
|
}
|
|
@@ -1497,7 +1549,9 @@ function btopCpuBox(data, width, theme, opts2) {
|
|
|
1497
1549
|
lines.push(chalk3.bold(truncatePlain(data.cpu.brand, contentWidth)));
|
|
1498
1550
|
lines.push(`CPU ${gaugeBar(data.cpu.usage, Math.max(4, contentWidth - 12))} ${data.cpu.usage.toFixed(0)}%`);
|
|
1499
1551
|
if (opts2.history?.cpuUsage.length) {
|
|
1500
|
-
|
|
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));
|
|
1501
1555
|
}
|
|
1502
1556
|
lines.push(`Load: ${data.cpu.loadAvg.map((l) => l.toFixed(2)).join(" ")}`);
|
|
1503
1557
|
return panel("1 cpu", lines, width, theme.cpu, theme.border, lines.length, rightTitle);
|
|
@@ -1506,45 +1560,54 @@ function btopCpuBox(data, width, theme, opts2) {
|
|
|
1506
1560
|
function btopLeftColumn(data, width, theme, opts2) {
|
|
1507
1561
|
const contentWidth = width - 4;
|
|
1508
1562
|
const out = [];
|
|
1509
|
-
const
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
const diskLines = [];
|
|
1520
|
-
for (const d of data.disk.slice(0, 2)) {
|
|
1521
|
-
const capNum = parseInt(d.capacity, 10) || 0;
|
|
1522
|
-
const mountShort = truncatePlain(d.mountpoint, 8);
|
|
1523
|
-
let ioStr = "";
|
|
1524
|
-
if (d.readBytesSec !== void 0 || d.writeBytesSec !== void 0) {
|
|
1525
|
-
const rStr = formatBytes(d.readBytesSec || 0);
|
|
1526
|
-
const wStr = formatBytes(d.writeBytesSec || 0);
|
|
1527
|
-
ioStr = ` R:${rStr}/s W:${wStr}/s`;
|
|
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}%`);
|
|
1528
1573
|
}
|
|
1529
|
-
|
|
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}`));
|
|
1530
1609
|
}
|
|
1531
|
-
const totalRead = data.disk.reduce((acc, d) => acc + (d.readBytesSec || 0), 0);
|
|
1532
|
-
const totalWrite = data.disk.reduce((acc, d) => acc + (d.writeBytesSec || 0), 0);
|
|
1533
|
-
const diskHeaderRight = `R:${formatBytes(totalRead)}/s W:${formatBytes(totalWrite)}/s`;
|
|
1534
|
-
out.push(...panel("0 disk io", diskLines, width, theme.disk, theme.border, diskLines.length, diskHeaderRight));
|
|
1535
1610
|
}
|
|
1536
|
-
const netLines = [];
|
|
1537
|
-
const rxRate = opts2.history?.netRxRate.length ? opts2.history.netRxRate[opts2.history.netRxRate.length - 1] : 0;
|
|
1538
|
-
const txRate = opts2.history?.netTxRate.length ? opts2.history.netTxRate[opts2.history.netTxRate.length - 1] : 0;
|
|
1539
|
-
netLines.push(`down \u25BC ${formatBytes(rxRate)}/s (${formatBytes(data.network.rxBytes)})`);
|
|
1540
|
-
if (opts2.history?.netRxRate.length) {
|
|
1541
|
-
netLines.push(chalk3.cyan(fitVisible(sparkline(opts2.history.netRxRate, {}), contentWidth)));
|
|
1542
|
-
}
|
|
1543
|
-
netLines.push(`up \u25B2 ${formatBytes(txRate)}/s (${formatBytes(data.network.txBytes)})`);
|
|
1544
|
-
if (opts2.history?.netTxRate.length) {
|
|
1545
|
-
netLines.push(theme.network(fitVisible(sparkline(opts2.history.netTxRate, {}), contentWidth)));
|
|
1546
|
-
}
|
|
1547
|
-
out.push(...panel("7 net", netLines, width, theme.network, theme.border, netLines.length, `${data.network.interface}`));
|
|
1548
1611
|
return out;
|
|
1549
1612
|
}
|
|
1550
1613
|
function btopProcBox(data, width, targetHeight, theme, opts2) {
|
|
@@ -1771,21 +1834,118 @@ function formatHtml(data) {
|
|
|
1771
1834
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1772
1835
|
<title>pyre \u2014 ${data.header.hostname}</title>
|
|
1773
1836
|
<style>
|
|
1774
|
-
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #
|
|
1775
|
-
h1 { color: #ff6a39; margin-bottom: 4px; }
|
|
1776
|
-
.meta { color: #
|
|
1777
|
-
h2 { color: #
|
|
1778
|
-
table { width: 100%; border-collapse: collapse; margin-bottom: 16px; }
|
|
1779
|
-
td { padding:
|
|
1780
|
-
td.label { color: #
|
|
1781
|
-
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; }
|
|
1782
1845
|
tr:last-child td { border-bottom: none; }
|
|
1783
1846
|
</style>
|
|
1784
1847
|
</head>
|
|
1785
1848
|
<body>
|
|
1786
|
-
<h1>PYRE</h1>
|
|
1787
|
-
<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">
|
|
1788
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>
|
|
1789
1949
|
</body>
|
|
1790
1950
|
</html>`;
|
|
1791
1951
|
}
|
|
@@ -2261,6 +2421,8 @@ var DEFAULT_CONFIG = {
|
|
|
2261
2421
|
splashEnabled: true,
|
|
2262
2422
|
splashColorScheme: "fire",
|
|
2263
2423
|
splashAnimation: "classic",
|
|
2424
|
+
webhookUrl: "",
|
|
2425
|
+
alertCmd: "",
|
|
2264
2426
|
visiblePanels: {
|
|
2265
2427
|
cpu: true,
|
|
2266
2428
|
mem: true,
|
|
@@ -2274,7 +2436,8 @@ var DEFAULT_CONFIG = {
|
|
|
2274
2436
|
disk: true,
|
|
2275
2437
|
process: true,
|
|
2276
2438
|
tree: false
|
|
2277
|
-
}
|
|
2439
|
+
},
|
|
2440
|
+
panelLayout: ["mem", "disk", "net"]
|
|
2278
2441
|
};
|
|
2279
2442
|
function readConfig() {
|
|
2280
2443
|
try {
|
|
@@ -2339,6 +2502,7 @@ var state = {
|
|
|
2339
2502
|
disk: config.visiblePanels.disk,
|
|
2340
2503
|
process: config.visiblePanels.process
|
|
2341
2504
|
},
|
|
2505
|
+
panelLayout: config.panelLayout || ["mem", "disk", "net"],
|
|
2342
2506
|
logging: false,
|
|
2343
2507
|
logStream: null,
|
|
2344
2508
|
statusMessage: "",
|
|
@@ -2384,6 +2548,8 @@ var state = {
|
|
|
2384
2548
|
watchdogCpu: config.watchdogCpu || 80,
|
|
2385
2549
|
watchdogMem: config.watchdogMem || 80,
|
|
2386
2550
|
notificationsEnabled: config.notificationsEnabled,
|
|
2551
|
+
webhookUrl: config.webhookUrl || "",
|
|
2552
|
+
alertCmd: config.alertCmd || "",
|
|
2387
2553
|
alerted: false,
|
|
2388
2554
|
activePanel: "grid",
|
|
2389
2555
|
mouseEnabled: config.mouseEnabled,
|
|
@@ -2399,14 +2565,18 @@ var state = {
|
|
|
2399
2565
|
{ id: "tasks", label: "Tasks", key: "9" },
|
|
2400
2566
|
{ id: "disk", label: "Disk", key: "0" },
|
|
2401
2567
|
{ id: "process", label: "Process", key: "P" },
|
|
2402
|
-
{ id: "p2p", label: "P2P", key: "R" }
|
|
2568
|
+
{ id: "p2p", label: "P2P", key: "R" },
|
|
2569
|
+
{ id: "anomalies", label: "Anomalies", key: "A" }
|
|
2403
2570
|
],
|
|
2404
|
-
p2pEvents: [],
|
|
2405
|
-
p2pBind: "",
|
|
2406
2571
|
p2pServer: null,
|
|
2572
|
+
p2pClient: null,
|
|
2407
2573
|
p2pServerRunning: false,
|
|
2408
|
-
|
|
2574
|
+
p2pClientConnected: false,
|
|
2575
|
+
p2pEvents: [],
|
|
2576
|
+
p2pBind: "",
|
|
2409
2577
|
p2pPassword: config.p2pPassword,
|
|
2578
|
+
p2pPort: config.p2pPort,
|
|
2579
|
+
anomalyHistory: [],
|
|
2410
2580
|
splashEnabled: config.splashEnabled,
|
|
2411
2581
|
splashColorScheme: config.splashColorScheme,
|
|
2412
2582
|
splashAnimation: config.splashAnimation
|
|
@@ -2563,7 +2733,7 @@ function writeLogRow(data) {
|
|
|
2563
2733
|
}
|
|
2564
2734
|
|
|
2565
2735
|
// src/live/render.ts
|
|
2566
|
-
import { execFile } from "child_process";
|
|
2736
|
+
import { execFile, exec as exec2 } from "child_process";
|
|
2567
2737
|
import chalk5 from "chalk";
|
|
2568
2738
|
|
|
2569
2739
|
// src/anomalies.ts
|
|
@@ -2678,7 +2848,8 @@ function tableCacheParams() {
|
|
|
2678
2848
|
state.activePanel,
|
|
2679
2849
|
state.treeView ? "1" : "0",
|
|
2680
2850
|
state.history.version,
|
|
2681
|
-
JSON.stringify(state.visiblePanels)
|
|
2851
|
+
JSON.stringify(state.visiblePanels),
|
|
2852
|
+
JSON.stringify(state.panelLayout)
|
|
2682
2853
|
].join("|");
|
|
2683
2854
|
}
|
|
2684
2855
|
function graphsCacheParams() {
|
|
@@ -2809,9 +2980,11 @@ function render() {
|
|
|
2809
2980
|
treeView: state.treeView,
|
|
2810
2981
|
activePanel: state.activePanel,
|
|
2811
2982
|
anomalies,
|
|
2983
|
+
anomalyHistory: state.anomalyHistory,
|
|
2812
2984
|
tempUnit: state.tempUnit,
|
|
2813
2985
|
history: state.history,
|
|
2814
|
-
graphMode: state.graphMode
|
|
2986
|
+
graphMode: state.graphMode,
|
|
2987
|
+
panelLayout: state.panelLayout
|
|
2815
2988
|
});
|
|
2816
2989
|
_cachedTableData = state.lastData;
|
|
2817
2990
|
_cachedTableParams = cacheParams;
|
|
@@ -2926,6 +3099,15 @@ function checkAlerts(data) {
|
|
|
2926
3099
|
const temp = data.cpu.temperature ?? data.thermal.temperatures?.cpu_die ?? null;
|
|
2927
3100
|
const hot = data.cpu.usage >= state.CPU_ALERT_PCT || temp !== null && temp >= state.TEMP_ALERT_C;
|
|
2928
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
|
+
}
|
|
2929
3111
|
const hasCriticalAnomaly = anomalies.some((a) => a.severity === "critical");
|
|
2930
3112
|
const hasWarningAnomaly = anomalies.some((a) => a.severity === "warning");
|
|
2931
3113
|
let watchdogTriggered = false;
|
|
@@ -2951,6 +3133,19 @@ function checkAlerts(data) {
|
|
|
2951
3133
|
}
|
|
2952
3134
|
setStatus(chalk5.red(`\u26A0 Alert: ${reasons.join("; ")}`), 5e3);
|
|
2953
3135
|
sendNotification("pyre Alert", reasons.join("; "));
|
|
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
|
+
}
|
|
2954
3149
|
} else if (!hot && !anomalyTriggered && !watchdogTriggered) {
|
|
2955
3150
|
state.alerted = false;
|
|
2956
3151
|
}
|
|
@@ -4650,11 +4845,88 @@ function runDiffCommand(file1, file2) {
|
|
|
4650
4845
|
}
|
|
4651
4846
|
}
|
|
4652
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
|
+
|
|
4653
4925
|
// src/index.ts
|
|
4654
4926
|
var config2 = readConfig();
|
|
4655
|
-
var pkg = JSON.parse(
|
|
4927
|
+
var pkg = JSON.parse(fs9.readFileSync(new URL("../package.json", import.meta.url), "utf-8"));
|
|
4656
4928
|
var program = new Command();
|
|
4657
|
-
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("--
|
|
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");
|
|
4658
4930
|
program.parse(process.argv);
|
|
4659
4931
|
var opts = program.opts();
|
|
4660
4932
|
function isExportMode() {
|
|
@@ -4667,7 +4939,18 @@ function sanitizeHost2(host) {
|
|
|
4667
4939
|
return host.trim().replace(/%$/, "");
|
|
4668
4940
|
}
|
|
4669
4941
|
function detectLocalIP() {
|
|
4670
|
-
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
|
+
}
|
|
4671
4954
|
for (const [, addrs] of Object.entries(ifaces)) {
|
|
4672
4955
|
if (!addrs) continue;
|
|
4673
4956
|
for (const addr of addrs) {
|
|
@@ -4681,22 +4964,22 @@ function detectLocalIP() {
|
|
|
4681
4964
|
async function runServerCommand() {
|
|
4682
4965
|
const ip = detectLocalIP();
|
|
4683
4966
|
if (!ip) {
|
|
4684
|
-
console.log(
|
|
4967
|
+
console.log(chalk13.red("Could not detect a non-loopback IP address. Connect to a network first."));
|
|
4685
4968
|
process.exit(1);
|
|
4686
4969
|
}
|
|
4687
4970
|
const port = 9876;
|
|
4688
4971
|
const password = "mysecret";
|
|
4689
|
-
console.log(
|
|
4972
|
+
console.log(chalk13.bold(`
|
|
4690
4973
|
pyre server`));
|
|
4691
|
-
console.log(
|
|
4692
|
-
console.log(
|
|
4693
|
-
console.log(
|
|
4974
|
+
console.log(chalk13.dim(` Detected IP: ${ip}`));
|
|
4975
|
+
console.log(chalk13.dim(` Port: ${port}`));
|
|
4976
|
+
console.log(chalk13.dim(` Password: ${password}`));
|
|
4694
4977
|
console.log();
|
|
4695
|
-
console.log(
|
|
4696
|
-
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}`));
|
|
4697
4980
|
console.log();
|
|
4698
|
-
console.log(
|
|
4699
|
-
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}`));
|
|
4700
4983
|
console.log();
|
|
4701
4984
|
}
|
|
4702
4985
|
async function main() {
|
|
@@ -4711,7 +4994,7 @@ async function main() {
|
|
|
4711
4994
|
const file1 = program.args[1];
|
|
4712
4995
|
const file2 = program.args[2];
|
|
4713
4996
|
if (!file1 || !file2) {
|
|
4714
|
-
console.log(
|
|
4997
|
+
console.log(chalk13.red("Usage: pyre diff <snapshot1.json> <snapshot2.json>"));
|
|
4715
4998
|
process.exit(1);
|
|
4716
4999
|
}
|
|
4717
5000
|
runDiffCommand(file1, file2);
|
|
@@ -4720,7 +5003,7 @@ async function main() {
|
|
|
4720
5003
|
if (cmd === "ssh") {
|
|
4721
5004
|
const host = program.args[1];
|
|
4722
5005
|
if (!host) {
|
|
4723
|
-
console.log(
|
|
5006
|
+
console.log(chalk13.red("Usage: pyre ssh <host>"));
|
|
4724
5007
|
process.exit(1);
|
|
4725
5008
|
}
|
|
4726
5009
|
await runSshCommand(host);
|
|
@@ -4733,7 +5016,7 @@ async function main() {
|
|
|
4733
5016
|
if (cmd === "bench") {
|
|
4734
5017
|
const benchCmd = program.args.slice(1).join(" ");
|
|
4735
5018
|
if (!benchCmd) {
|
|
4736
|
-
console.log(
|
|
5019
|
+
console.log(chalk13.red("Usage: pyre bench <command>"));
|
|
4737
5020
|
process.exit(1);
|
|
4738
5021
|
}
|
|
4739
5022
|
await runBenchCommand(benchCmd);
|
|
@@ -4745,8 +5028,8 @@ async function main() {
|
|
|
4745
5028
|
const cfg = readConfig();
|
|
4746
5029
|
console.log(JSON.stringify(cfg, null, 2));
|
|
4747
5030
|
} else if (sub === "reset") {
|
|
4748
|
-
|
|
4749
|
-
console.log(
|
|
5031
|
+
fs9.rmSync(CONFIG_FILE, { force: true });
|
|
5032
|
+
console.log(chalk13.green("Config reset to defaults"));
|
|
4750
5033
|
} else {
|
|
4751
5034
|
console.log("Usage: pyre config <show|reset>");
|
|
4752
5035
|
}
|
|
@@ -4756,8 +5039,24 @@ async function main() {
|
|
|
4756
5039
|
await runServerCommand();
|
|
4757
5040
|
return;
|
|
4758
5041
|
}
|
|
4759
|
-
|
|
4760
|
-
|
|
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;
|
|
4761
5060
|
const tempUnit = opts.tempUnit === "f" ? "f" : "c";
|
|
4762
5061
|
if (cmd === "live" || !isExportMode() && !opts.once && cmd !== "p2p") {
|
|
4763
5062
|
if (!process.stdout.isTTY && cmd !== "live") {
|
|
@@ -4779,8 +5078,8 @@ async function main() {
|
|
|
4779
5078
|
theme: opts.theme || config2.theme,
|
|
4780
5079
|
exportDir: opts.exportDir || config2.exportDir,
|
|
4781
5080
|
autoLog: opts.log || config2.autoLog,
|
|
4782
|
-
alertCpu,
|
|
4783
|
-
alertTemp,
|
|
5081
|
+
alertCpu: config2.cpuAlertPct,
|
|
5082
|
+
alertTemp: config2.tempAlertC,
|
|
4784
5083
|
tempUnit
|
|
4785
5084
|
}, splashPromise);
|
|
4786
5085
|
return;
|
|
@@ -4794,7 +5093,7 @@ async function main() {
|
|
|
4794
5093
|
await runP2PConnect();
|
|
4795
5094
|
return;
|
|
4796
5095
|
} else {
|
|
4797
|
-
console.log(
|
|
5096
|
+
console.log(chalk13.red("Usage: pyre p2p <server|connect> [options]"));
|
|
4798
5097
|
process.exit(1);
|
|
4799
5098
|
}
|
|
4800
5099
|
}
|
|
@@ -4810,13 +5109,13 @@ async function main() {
|
|
|
4810
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 } });
|
|
4811
5110
|
console.log(output);
|
|
4812
5111
|
if (opts.out) {
|
|
4813
|
-
|
|
4814
|
-
console.log(
|
|
5112
|
+
fs9.writeFileSync(opts.out, stripAnsi(output));
|
|
5113
|
+
console.log(chalk13.dim(`
|
|
4815
5114
|
Saved to ${opts.out}`));
|
|
4816
5115
|
}
|
|
4817
5116
|
}
|
|
4818
5117
|
main().catch((err) => {
|
|
4819
|
-
console.error(
|
|
5118
|
+
console.error(chalk13.red(`Error: ${err.message}`));
|
|
4820
5119
|
process.exit(1);
|
|
4821
5120
|
});
|
|
4822
5121
|
async function runP2PServer() {
|
|
@@ -4832,15 +5131,15 @@ async function runP2PServer() {
|
|
|
4832
5131
|
const allowList = opts.p2pAllow ? opts.p2pAllow.split(",").map((s) => s.trim()).filter(Boolean) : [];
|
|
4833
5132
|
const denyList = opts.p2pDeny ? opts.p2pDeny.split(",").map((s) => s.trim()).filter(Boolean) : [];
|
|
4834
5133
|
if (!password) {
|
|
4835
|
-
console.log(
|
|
5134
|
+
console.log(chalk13.red("Error: a password is required for P2P server mode. Use --p2p-password <password>"));
|
|
4836
5135
|
process.exit(1);
|
|
4837
5136
|
}
|
|
4838
5137
|
if (tlsCert && !tlsKey) {
|
|
4839
|
-
console.log(
|
|
5138
|
+
console.log(chalk13.red("Error: --p2p-key is required when --p2p-cert is specified"));
|
|
4840
5139
|
process.exit(1);
|
|
4841
5140
|
}
|
|
4842
5141
|
if (tlsKey && !tlsCert) {
|
|
4843
|
-
console.log(
|
|
5142
|
+
console.log(chalk13.red("Error: --p2p-cert is required when --p2p-key is specified"));
|
|
4844
5143
|
process.exit(1);
|
|
4845
5144
|
}
|
|
4846
5145
|
const server = new P2PServer({
|
|
@@ -4860,18 +5159,18 @@ async function runP2PServer() {
|
|
|
4860
5159
|
hmacKey: opts.p2pHmacKey
|
|
4861
5160
|
});
|
|
4862
5161
|
const tlsEnabled = !!(tlsCert && tlsKey);
|
|
4863
|
-
console.log(
|
|
5162
|
+
console.log(chalk13.bold(`
|
|
4864
5163
|
pyre P2P Server`));
|
|
4865
|
-
console.log(
|
|
4866
|
-
console.log(
|
|
4867
|
-
console.log(
|
|
4868
|
-
console.log(
|
|
4869
|
-
console.log(
|
|
4870
|
-
console.log(
|
|
4871
|
-
console.log(
|
|
4872
|
-
if (allowList.length > 0) console.log(
|
|
4873
|
-
if (denyList.length > 0) console.log(
|
|
4874
|
-
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...
|
|
4875
5174
|
`));
|
|
4876
5175
|
await server.start();
|
|
4877
5176
|
const rl = readline2.createInterface({ input: process.stdin });
|
|
@@ -4883,7 +5182,7 @@ async function runP2PServer() {
|
|
|
4883
5182
|
process.exit(0);
|
|
4884
5183
|
} else if (trimmed === "status") {
|
|
4885
5184
|
const info = server.info;
|
|
4886
|
-
console.log(
|
|
5185
|
+
console.log(chalk13.dim(` Peers connected: ${info.peersConnected}`));
|
|
4887
5186
|
}
|
|
4888
5187
|
});
|
|
4889
5188
|
process.once("SIGINT", () => {
|
|
@@ -4916,7 +5215,7 @@ async function runP2PConnect() {
|
|
|
4916
5215
|
const tlsCA = opts.p2pCA;
|
|
4917
5216
|
const tlsInsecure = !!opts.p2pInsecure;
|
|
4918
5217
|
if (!password) {
|
|
4919
|
-
console.log(
|
|
5218
|
+
console.log(chalk13.red("Error: a password is required for P2P connect mode. Use --p2p-password <password>"));
|
|
4920
5219
|
process.exit(1);
|
|
4921
5220
|
}
|
|
4922
5221
|
const client = new P2PClient({
|
|
@@ -4930,12 +5229,12 @@ async function runP2PConnect() {
|
|
|
4930
5229
|
hmacKey: opts.p2pHmacKey
|
|
4931
5230
|
});
|
|
4932
5231
|
client.setOnStatus((msg) => {
|
|
4933
|
-
process.stdout.write(`\r${
|
|
5232
|
+
process.stdout.write(`\r${chalk13.dim(msg)}${" ".repeat(40)}\r`);
|
|
4934
5233
|
});
|
|
4935
5234
|
client.setOnData((data) => {
|
|
4936
5235
|
const lines = [];
|
|
4937
|
-
lines.push(
|
|
4938
|
-
lines.push(
|
|
5236
|
+
lines.push(chalk13.bold(` pyre P2P Client \u2014 ${data.header.hostname}`));
|
|
5237
|
+
lines.push(chalk13.dim(` ${data.timestamp}`));
|
|
4939
5238
|
lines.push("");
|
|
4940
5239
|
lines.push(...formatTable(data, {
|
|
4941
5240
|
width: process.stdout.columns || 80,
|
|
@@ -4952,11 +5251,11 @@ async function runP2PConnect() {
|
|
|
4952
5251
|
writeFrame2(lines);
|
|
4953
5252
|
});
|
|
4954
5253
|
const protocolLabel = useTLS ? "TLS" : "TCP";
|
|
4955
|
-
console.log(
|
|
5254
|
+
console.log(chalk13.bold(`
|
|
4956
5255
|
pyre P2P Client`));
|
|
4957
|
-
console.log(
|
|
4958
|
-
console.log(
|
|
4959
|
-
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
|
|
4960
5259
|
`));
|
|
4961
5260
|
process.stdout.write("\x1B[?25l");
|
|
4962
5261
|
try {
|
|
@@ -4964,23 +5263,23 @@ async function runP2PConnect() {
|
|
|
4964
5263
|
} catch (err) {
|
|
4965
5264
|
const code = err.code;
|
|
4966
5265
|
const msg = err.message || String(err);
|
|
4967
|
-
console.log(
|
|
5266
|
+
console.log(chalk13.red(`Connection failed: ${msg}`));
|
|
4968
5267
|
if (code === "ECONNREFUSED") {
|
|
4969
|
-
console.log(
|
|
4970
|
-
console.log(
|
|
4971
|
-
console.log(
|
|
4972
|
-
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"));
|
|
4973
5272
|
} else if (code === "ETIMEDOUT") {
|
|
4974
|
-
console.log(
|
|
4975
|
-
console.log(
|
|
4976
|
-
console.log(
|
|
4977
|
-
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"));
|
|
4978
5277
|
} else if (code === "EHOSTDOWN" || code === "EHOSTUNREACH") {
|
|
4979
|
-
console.log(
|
|
4980
|
-
console.log(
|
|
4981
|
-
console.log(
|
|
4982
|
-
console.log(
|
|
4983
|
-
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)"));
|
|
4984
5283
|
}
|
|
4985
5284
|
process.stdout.write("\x1B[?25h");
|
|
4986
5285
|
process.exit(1);
|
|
@@ -4995,18 +5294,18 @@ async function runP2PConnect() {
|
|
|
4995
5294
|
async function runSshCommand(host) {
|
|
4996
5295
|
const password = opts.p2pPassword || "mysecret";
|
|
4997
5296
|
const cmd = `ssh -o LogLevel=QUIET ${host} "pyre --json --once"`;
|
|
4998
|
-
console.log(
|
|
5297
|
+
console.log(chalk13.bold(`
|
|
4999
5298
|
pyre ssh \u2014 ${host}`));
|
|
5000
|
-
console.log(
|
|
5299
|
+
console.log(chalk13.dim(` Running: ${cmd}
|
|
5001
5300
|
`));
|
|
5002
5301
|
const { execFile: execFile2 } = await import("child_process");
|
|
5003
5302
|
const child = execFile2("ssh", ["-o", "LogLevel=QUIET", host, "pyre", "--json", "--once"], (err, stdout, stderr) => {
|
|
5004
5303
|
if (err) {
|
|
5005
|
-
console.log(
|
|
5006
|
-
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."));
|
|
5007
5306
|
process.exit(1);
|
|
5008
5307
|
}
|
|
5009
|
-
if (stderr) console.log(
|
|
5308
|
+
if (stderr) console.log(chalk13.dim(stderr));
|
|
5010
5309
|
try {
|
|
5011
5310
|
const remote = JSON.parse(stdout);
|
|
5012
5311
|
console.log(formatTable(remote, { width: process.stdout.columns || 80, sortBy: "cpu", treeView: false, visible: { packets: true } }));
|
|
@@ -5024,31 +5323,7 @@ async function runSshCommand(host) {
|
|
|
5024
5323
|
async function runWebCommand() {
|
|
5025
5324
|
const http = await import("http");
|
|
5026
5325
|
const port = parseInt(opts.port || process.env.PORT || "3000", 10) || 3e3;
|
|
5027
|
-
const authEnabled = !!opts.webAuth;
|
|
5028
|
-
const authUser = opts.webUser || "admin";
|
|
5029
|
-
const authPass = opts.webPass || "pyre";
|
|
5030
5326
|
const server = http.createServer(async (req, res) => {
|
|
5031
|
-
if (authEnabled) {
|
|
5032
|
-
const authHeader = req.headers.authorization;
|
|
5033
|
-
if (!authHeader || !authHeader.startsWith("Basic ")) {
|
|
5034
|
-
res.writeHead(401, {
|
|
5035
|
-
"WWW-Authenticate": 'Basic realm="pyre Web Dashboard"',
|
|
5036
|
-
"Content-Type": "text/plain"
|
|
5037
|
-
});
|
|
5038
|
-
res.end("401 Unauthorized");
|
|
5039
|
-
return;
|
|
5040
|
-
}
|
|
5041
|
-
const credentials = Buffer.from(authHeader.split(" ")[1], "base64").toString("utf-8");
|
|
5042
|
-
const [user, pass] = credentials.split(":");
|
|
5043
|
-
if (user !== authUser || pass !== authPass) {
|
|
5044
|
-
res.writeHead(401, {
|
|
5045
|
-
"WWW-Authenticate": 'Basic realm="pyre Web Dashboard"',
|
|
5046
|
-
"Content-Type": "text/plain"
|
|
5047
|
-
});
|
|
5048
|
-
res.end("401 Unauthorized");
|
|
5049
|
-
return;
|
|
5050
|
-
}
|
|
5051
|
-
}
|
|
5052
5327
|
const url = req.url || "/";
|
|
5053
5328
|
if (url === "/health" || url === "/api/health") {
|
|
5054
5329
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
@@ -5080,15 +5355,18 @@ async function runWebCommand() {
|
|
|
5080
5355
|
}
|
|
5081
5356
|
if (url === "/" || url === "/index.html") {
|
|
5082
5357
|
res.writeHead(200, { "Content-Type": "text/html" });
|
|
5083
|
-
const
|
|
5084
|
-
|
|
5085
|
-
|
|
5086
|
-
|
|
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"));
|
|
5087
5366
|
} else {
|
|
5088
5367
|
const data = await collectAll({ detailed: true });
|
|
5089
|
-
|
|
5368
|
+
res.end(formatHtml(data));
|
|
5090
5369
|
}
|
|
5091
|
-
res.end(htmlContent);
|
|
5092
5370
|
} else if (url === "/api" || url === "/api/stats" || url === "/data") {
|
|
5093
5371
|
const data = await collectAll({ detailed: true });
|
|
5094
5372
|
res.writeHead(200, {
|
|
@@ -5102,16 +5380,33 @@ async function runWebCommand() {
|
|
|
5102
5380
|
res.end("Not found");
|
|
5103
5381
|
}
|
|
5104
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";
|
|
5105
5397
|
server.listen(port, "0.0.0.0", () => {
|
|
5106
|
-
console.log(
|
|
5398
|
+
console.log(chalk13.bold(`
|
|
5107
5399
|
pyre web server running on port ${port}`));
|
|
5108
|
-
console.log(
|
|
5109
|
-
console.log(
|
|
5110
|
-
console.log(
|
|
5111
|
-
|
|
5112
|
-
|
|
5113
|
-
|
|
5114
|
-
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
|
+
`));
|
|
5115
5410
|
});
|
|
5116
5411
|
process.once("SIGINT", () => {
|
|
5117
5412
|
server.close();
|
|
@@ -5121,14 +5416,14 @@ async function runWebCommand() {
|
|
|
5121
5416
|
async function runBenchCommand(benchCmd) {
|
|
5122
5417
|
const { spawn } = await import("child_process");
|
|
5123
5418
|
const logDir = opts.exportDir || "./pyre-exports";
|
|
5124
|
-
const file =
|
|
5125
|
-
if (!
|
|
5126
|
-
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" });
|
|
5127
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");
|
|
5128
|
-
console.log(
|
|
5423
|
+
console.log(chalk13.bold(`
|
|
5129
5424
|
pyre bench`));
|
|
5130
|
-
console.log(
|
|
5131
|
-
console.log(
|
|
5425
|
+
console.log(chalk13.dim(` Command: ${benchCmd}`));
|
|
5426
|
+
console.log(chalk13.dim(` Log: ${file}
|
|
5132
5427
|
`));
|
|
5133
5428
|
const child = spawn("sh", ["-c", benchCmd]);
|
|
5134
5429
|
child.stdout.on("data", (d) => process.stdout.write(d));
|
|
@@ -5149,7 +5444,7 @@ async function runBenchCommand(benchCmd) {
|
|
|
5149
5444
|
child.on("close", () => {
|
|
5150
5445
|
clearInterval(handle);
|
|
5151
5446
|
stream.end();
|
|
5152
|
-
console.log(
|
|
5447
|
+
console.log(chalk13.green(`
|
|
5153
5448
|
Bench complete. Log saved to ${file}`));
|
|
5154
5449
|
process.exit(0);
|
|
5155
5450
|
});
|