pyre-cli 4.17.0 → 5.1.0

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