pyre-cli 6.1.0 → 7.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import fs8 from "fs";
5
- import os4 from "os";
6
- import path5 from "path";
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 chalk11 from "chalk";
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("\u250C" + topInner + "\u2510");
1003
- const bottom = borderAccent("\u2514" + "\u2500".repeat(innerW) + "\u2518");
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 filled = Math.round(pct / 100 * w);
1015
- const empty = w - filled;
1016
- const filledStr = "\u2588".repeat(filled);
1017
- const emptyStr = "\u2591".repeat(empty);
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(filledStr) + chalk3.dim(emptyStr);
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 filled = Math.round(Math.min(100, Math.max(0, percent)) / 100 * barW);
1051
- const empty = barW - filled;
1052
- const barStr = "\u2588".repeat(Math.max(0, filled)) + "\u2591".repeat(Math.max(0, empty));
1053
- const color = percent > 90 ? chalk3.red : percent > 70 ? chalk3.yellow : chalk3.green;
1054
- return `${chalk3.dim(label.padEnd(labelW))}${color(barStr)} ${pctText}`;
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
- lines.push(theme.cpu(sparkline(opts2.history.cpuUsage, { min: 0, max: 100 })));
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 memLines = [];
1510
- memLines.push(`RAM [${gaugeBar(data.memory.usagePercent, Math.max(4, contentWidth - 12))}] ${data.memory.usagePercent}%`);
1511
- memLines.push(`Used: ${formatBytes(data.memory.used)} / ${formatBytes(data.memory.total)}`);
1512
- memLines.push(`Free: ${formatBytes(data.memory.free)} \xB7 Cache: ${formatBytes(data.memory.total - data.memory.free - data.memory.used)}`);
1513
- if (data.memory.swapTotal > 0) {
1514
- const swapPct = Math.round(data.memory.swapUsed / data.memory.swapTotal * 100);
1515
- memLines.push(`Swap [${gaugeBar(swapPct, Math.max(4, contentWidth - 12))}] ${swapPct}%`);
1516
- }
1517
- out.push(...panel("2 mem", memLines, width, theme.mem, theme.border, memLines.length, `${formatBytes(data.memory.used)}`));
1518
- if (data.disk.length) {
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
- diskLines.push(`${mountShort} ${d.used}/${d.size} [${gaugeBar(capNum, Math.max(3, contentWidth - 18 - ioStr.length))}] ${d.capacity}${ioStr}`);
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: #1e1e2e; color: #cdd6f4; margin: 0; padding: 24px; }
1775
- h1 { color: #ff6a39; margin-bottom: 4px; }
1776
- .meta { color: #a6adc8; margin-bottom: 24px; }
1777
- h2 { color: #89b4fa; margin-top: 28px; border-bottom: 1px solid #313244; padding-bottom: 6px; }
1778
- table { width: 100%; border-collapse: collapse; margin-bottom: 16px; }
1779
- td { padding: 6px 10px; border-bottom: 1px solid #313244; vertical-align: top; }
1780
- td.label { color: #a6adc8; width: 22%; font-weight: 500; }
1781
- td.value { color: #cdd6f4; }
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 ${escapeHtml(data.timestamp)}</div>
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 + ' &nbsp; CPU: ' + p.cpu.toFixed(1) + '% &nbsp; MEM: ' + p.mem.toFixed(1) + '% &nbsp; ' + 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
- p2pPort: config.p2pPort,
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
  }
@@ -3646,10 +3841,10 @@ function persistConfig() {
3646
3841
  });
3647
3842
  }
3648
3843
  function isUpKey(key, str) {
3649
- return key.name === "up" || str === "k" || key.sequence === "\x1B[A" || key.sequence === "\x1BOA";
3844
+ return key.name === "up" || key.sequence === "\x1B[A" || key.sequence === "\x1BOA";
3650
3845
  }
3651
3846
  function isDownKey(key, str) {
3652
- return key.name === "down" || str === "j" || key.sequence === "\x1B[B" || key.sequence === "\x1BOB";
3847
+ return key.name === "down" || key.sequence === "\x1B[B" || key.sequence === "\x1BOB";
3653
3848
  }
3654
3849
  function isEnterKey(key, str) {
3655
3850
  return key.name === "return" || key.name === "enter" || key.sequence === "\r" || key.sequence === "\n" || str === "\r" || str === "\n";
@@ -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(fs8.readFileSync(new URL("../package.json", import.meta.url), "utf-8"));
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("--port <port>", "Port number for web server mode", "3000").option("--web-auth", "Enable Basic Authentication for web server").option("--web-user <user>", "Username for web server authentication", "admin").option("--web-pass <pass>", "Password for web server authentication", "pyre");
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 = os4.networkInterfaces();
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(chalk11.red("Could not detect a non-loopback IP address. Connect to a network first."));
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(chalk11.bold(`
4972
+ console.log(chalk13.bold(`
4690
4973
  pyre server`));
4691
- console.log(chalk11.dim(` Detected IP: ${ip}`));
4692
- console.log(chalk11.dim(` Port: ${port}`));
4693
- console.log(chalk11.dim(` Password: ${password}`));
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(chalk11.bold("Run this on THIS machine (server):"));
4696
- console.log(chalk11.hex("#ff6a39")(`pyre p2p server --p2p-host 0.0.0.0 --p2p-port ${port} --p2p-password ${password}`));
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(chalk11.bold("Run this on the OTHER machine (client):"));
4699
- console.log(chalk11.hex("#ff6a39")(`pyre p2p connect --p2p-host ${ip} --p2p-port ${port} --p2p-password ${password}`));
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(chalk11.red("Usage: pyre diff <snapshot1.json> <snapshot2.json>"));
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(chalk11.red("Usage: pyre ssh <host>"));
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(chalk11.red("Usage: pyre bench <command>"));
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
- fs8.rmSync(CONFIG_FILE, { force: true });
4749
- console.log(chalk11.green("Config reset to defaults"));
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
- const alertCpu = opts.alertCpu ? parseInt(opts.alertCpu, 10) : void 0;
4760
- const alertTemp = opts.alertTemp ? parseInt(opts.alertTemp, 10) : void 0;
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(chalk11.red("Usage: pyre p2p <server|connect> [options]"));
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
- fs8.writeFileSync(opts.out, stripAnsi(output));
4814
- console.log(chalk11.dim(`
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(chalk11.red(`Error: ${err.message}`));
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(chalk11.red("Error: a password is required for P2P server mode. Use --p2p-password <password>"));
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(chalk11.red("Error: --p2p-key is required when --p2p-cert is specified"));
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(chalk11.red("Error: --p2p-cert is required when --p2p-key is specified"));
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(chalk11.bold(`
5162
+ console.log(chalk13.bold(`
4864
5163
  pyre P2P Server`));
4865
- console.log(chalk11.dim(` Host: ${host}`));
4866
- console.log(chalk11.dim(` Port: ${port}`));
4867
- console.log(chalk11.dim(` Protocol: ${tlsEnabled ? "TLS" : "TCP"}`));
4868
- console.log(chalk11.dim(` Password: ${password}`));
4869
- console.log(chalk11.dim(` Interval: ${interval}s`));
4870
- console.log(chalk11.dim(` Detailed: ${detailed ? "on" : "off"}`));
4871
- console.log(chalk11.dim(` Rate limit: ${rateLimit} attempts/min`));
4872
- if (allowList.length > 0) console.log(chalk11.dim(` Allowed IPs: ${allowList.join(", ")}`));
4873
- if (denyList.length > 0) console.log(chalk11.dim(` Denied IPs: ${denyList.join(", ")}`));
4874
- console.log(chalk11.dim(` Waiting for peers...
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(chalk11.dim(` Peers connected: ${info.peersConnected}`));
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(chalk11.red("Error: a password is required for P2P connect mode. Use --p2p-password <password>"));
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${chalk11.dim(msg)}${" ".repeat(40)}\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(chalk11.bold(` pyre P2P Client \u2014 ${data.header.hostname}`));
4938
- lines.push(chalk11.dim(` ${data.timestamp}`));
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(chalk11.bold(`
5254
+ console.log(chalk13.bold(`
4956
5255
  pyre P2P Client`));
4957
- console.log(chalk11.dim(` Protocol: ${protocolLabel}`));
4958
- console.log(chalk11.dim(` Connecting to ${host}:${port}...`));
4959
- console.log(chalk11.dim(` Press Ctrl+C to disconnect
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(chalk11.red(`Connection failed: ${msg}`));
5266
+ console.log(chalk13.red(`Connection failed: ${msg}`));
4968
5267
  if (code === "ECONNREFUSED") {
4969
- console.log(chalk11.yellow(" Hints:"));
4970
- console.log(chalk11.dim(" 1. Start the server: pyre p2p server --p2p-host 0.0.0.0 --p2p-port " + port + " --p2p-password <password>"));
4971
- console.log(chalk11.dim(" 2. Check the server IP with: hostname -I"));
4972
- console.log(chalk11.dim(" 3. Open the port on the server firewall: sudo ufw allow " + port + "/tcp"));
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(chalk11.yellow(" Hints:"));
4975
- console.log(chalk11.dim(" 1. Verify the server IP is correct"));
4976
- console.log(chalk11.dim(" 2. Check that both machines are on the same network/subnet"));
4977
- console.log(chalk11.dim(" 3. Open the port on the server firewall"));
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(chalk11.yellow(" Hints:"));
4980
- console.log(chalk11.dim(" 1. The server at " + host + " is not responding"));
4981
- console.log(chalk11.dim(" 2. Start the server first: pyre p2p server --p2p-host 0.0.0.0 --p2p-port " + port + " --p2p-password <password>"));
4982
- console.log(chalk11.dim(" 3. Check connectivity: ping " + host));
4983
- console.log(chalk11.dim(" 4. Ensure both machines are on the same LAN (ping each other first)"));
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(chalk11.bold(`
5297
+ console.log(chalk13.bold(`
4999
5298
  pyre ssh \u2014 ${host}`));
5000
- console.log(chalk11.dim(` Running: ${cmd}
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(chalk11.red(` SSH failed: ${err.message}`));
5006
- console.log(chalk11.dim(" Ensure SSH keys are set up or the host is reachable."));
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(chalk11.dim(stderr));
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 indexPath = path5.join(process.cwd(), "index.html");
5084
- let htmlContent = "";
5085
- if (fs8.existsSync(indexPath)) {
5086
- htmlContent = fs8.readFileSync(indexPath, "utf-8");
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
- htmlContent = formatHtml(data);
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(chalk11.bold(`
5398
+ console.log(chalk13.bold(`
5107
5399
  pyre web server running on port ${port}`));
5108
- console.log(chalk11.dim(` Dashboard: http://0.0.0.0:${port}/`));
5109
- console.log(chalk11.dim(` API: http://0.0.0.0:${port}/api`));
5110
- console.log(chalk11.dim(` SSE Stream: http://0.0.0.0:${port}/api/stream`));
5111
- if (authEnabled) {
5112
- console.log(chalk11.yellow(` Authentication: Enabled (${authUser}:${authPass})`));
5113
- }
5114
- console.log(chalk11.dim(" Press Ctrl+C to stop\n"));
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 = path5.join(logDir, `pyre-bench-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.csv`);
5125
- if (!fs8.existsSync(logDir)) fs8.mkdirSync(logDir, { recursive: true });
5126
- const stream = fs8.createWriteStream(file, { flags: "a" });
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(chalk11.bold(`
5423
+ console.log(chalk13.bold(`
5129
5424
  pyre bench`));
5130
- console.log(chalk11.dim(` Command: ${benchCmd}`));
5131
- console.log(chalk11.dim(` Log: ${file}
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(chalk11.green(`
5447
+ console.log(chalk13.green(`
5153
5448
  Bench complete. Log saved to ${file}`));
5154
5449
  process.exit(0);
5155
5450
  });