pyre-cli 1.4.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -2,8 +2,9 @@
2
2
 
3
3
  // src/index.ts
4
4
  import fs2 from "fs";
5
+ import readline2 from "readline";
5
6
  import { Command } from "commander";
6
- import chalk7 from "chalk";
7
+ import chalk8 from "chalk";
7
8
 
8
9
  // src/monitors/collectors.ts
9
10
  import chalk from "chalk";
@@ -59,8 +60,39 @@ function parseSuffix(s) {
59
60
  }
60
61
 
61
62
  // src/monitors/collectors.ts
63
+ var BATTERY_WINDOW_MS = 15 * 60 * 1e3;
64
+ var MIN_ESTIMATE_WINDOW_MS = 30 * 1e3;
65
+ var prevBattery = { samples: [] };
66
+ function estimateBatteryLife(level, state2) {
67
+ const now = Date.now();
68
+ if (state2 === "charged" || state2 === "charging") {
69
+ prevBattery.samples = [];
70
+ return {};
71
+ }
72
+ prevBattery.samples.push({ level, ts: now });
73
+ prevBattery.samples = prevBattery.samples.filter((s) => now - s.ts <= BATTERY_WINDOW_MS);
74
+ const first = prevBattery.samples[0];
75
+ const elapsedMs = now - first.ts;
76
+ if (elapsedMs < MIN_ESTIMATE_WINDOW_MS) {
77
+ return {};
78
+ }
79
+ const dtHours = Math.max(elapsedMs / 1e3 / 3600, 1e-4);
80
+ const discharged = first.level - level;
81
+ if (discharged <= 0) {
82
+ return { estimatedTimeToEmpty: "calculating" };
83
+ }
84
+ const dischargeRate = discharged / dtHours;
85
+ const remainingLevel = Math.max(level, 0);
86
+ const hoursToEmpty = remainingLevel / dischargeRate;
87
+ const h = Math.floor(hoursToEmpty);
88
+ const m = Math.round((hoursToEmpty - h) * 60);
89
+ const estimatedTimeToEmpty = hoursToEmpty > 0 ? `${h}h ${m}m` : "calculating";
90
+ const ASSUMED_CAPACITY_WH = 55;
91
+ const powerWatts = Math.round(dischargeRate * (ASSUMED_CAPACITY_WH / 100) * 100) / 100;
92
+ return { estimatedTimeToEmpty, dischargeRatePerHour: Math.round(dischargeRate * 10) / 10, powerWatts };
93
+ }
62
94
  async function collectAll(opts2 = {}) {
63
- const [cpu, gpu, memory, disk, battery, thermal, network, processes, system, power] = await Promise.all([
95
+ const [cpu, gpu, memory, disk, battery, thermal, network, processes, system, power, packets, tasks] = await Promise.all([
64
96
  collectCpu(),
65
97
  collectGpu(opts2.detailed),
66
98
  collectMemory(),
@@ -68,9 +100,11 @@ async function collectAll(opts2 = {}) {
68
100
  collectBattery(),
69
101
  collectThermal(opts2.detailed),
70
102
  collectNetwork(),
71
- collectProcesses(opts2.processLimit ?? 10),
103
+ collectProcesses(opts2.processLimit),
72
104
  collectSystem(),
73
- collectPower()
105
+ collectPower(),
106
+ collectPackets(),
107
+ collectTasks()
74
108
  ]);
75
109
  return {
76
110
  header: {
@@ -88,17 +122,40 @@ async function collectAll(opts2 = {}) {
88
122
  network,
89
123
  processes,
90
124
  power,
91
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
125
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
126
+ packets,
127
+ tasks
92
128
  };
93
129
  }
94
130
  async function collectPower() {
95
131
  try {
96
132
  const { power } = await getSmcMetrics();
97
- if (power.cpu === void 0 && power.gpu === void 0 && power.combined === void 0) return null;
98
- return { cpuWatts: power.cpu, gpuWatts: power.gpu, combinedWatts: power.combined };
133
+ if (power.cpu !== void 0 || power.gpu !== void 0 || power.combined !== void 0) {
134
+ return { cpuWatts: power.cpu, gpuWatts: power.gpu, combinedWatts: power.combined };
135
+ }
99
136
  } catch {
100
- return null;
101
137
  }
138
+ try {
139
+ const ioreg = (await run("ioreg -rn AppleSmartBattery 2>/dev/null")).trim();
140
+ const ampMatch = ioreg.match(/"InstantAmperage"\s*=\s*(-?\d+)/);
141
+ const voltMatch = ioreg.match(/"Voltage"\s*=\s*(\d+)/);
142
+ if (ampMatch && voltMatch) {
143
+ let ampsBig = BigInt(ampMatch[1]);
144
+ if (!ampMatch[1].startsWith("-")) {
145
+ const UINT64_MAX_PLUS_1 = 1n << 64n;
146
+ const INT64_MAX = (1n << 63n) - 1n;
147
+ if (ampsBig > INT64_MAX) ampsBig -= UINT64_MAX_PLUS_1;
148
+ }
149
+ const amps = Number(ampsBig);
150
+ const volts = parseInt(voltMatch[1], 10) / 1e3;
151
+ const watts = Math.abs(amps / 1e3 * volts);
152
+ if (Number.isFinite(watts) && watts < 500) {
153
+ return { combinedWatts: Math.round(watts * 100) / 100 };
154
+ }
155
+ }
156
+ } catch {
157
+ }
158
+ return null;
102
159
  }
103
160
  async function collectGpu(detailed) {
104
161
  try {
@@ -252,7 +309,7 @@ async function collectBattery() {
252
309
  if (!raw.includes("Battery Power") && !raw.includes("AC Power")) return null;
253
310
  const levelMatch = raw.match(/(\d+)%/);
254
311
  const stateMatch = raw.match(/;\s*(charging|discharging|charged|finishing charge|attached to charger)/i);
255
- const timeMatch = raw.match(/:\s*([\d:]+)\s+remaining/i);
312
+ const timeMatch = raw.match(/:\s*([\d:]+)\s+remaining/i) || raw.match(/(\d+:\d+)\s+remaining/i);
256
313
  const powerPlugged = raw.includes("AC Power") || raw.includes("attached to charger");
257
314
  const level = levelMatch ? parseInt(levelMatch[1]) : 0;
258
315
  const state2 = powerPlugged ? "charged" : stateMatch ? stateMatch[1].toLowerCase() : "discharging";
@@ -285,6 +342,7 @@ async function collectBattery() {
285
342
  if (maxCapacityPercent !== void 0) healthParts.push(`${maxCapacityPercent}% capacity`);
286
343
  if (cycles !== void 0) healthParts.push(`${cycles} cycles`);
287
344
  const health = healthParts.length ? healthParts.join(", ") : "unknown";
345
+ const estimate = estimateBatteryLife(level, state2);
288
346
  return {
289
347
  level,
290
348
  state: state2,
@@ -293,7 +351,8 @@ async function collectBattery() {
293
351
  powerSource: powerPlugged ? "AC" : "Battery",
294
352
  cycles,
295
353
  condition,
296
- maxCapacityPercent
354
+ maxCapacityPercent,
355
+ ...estimate
297
356
  };
298
357
  } catch {
299
358
  return null;
@@ -398,9 +457,13 @@ async function collectNetwork() {
398
457
  return { interface: "unknown", ip: "0.0.0.0", rxBytes: 0, txBytes: 0, rxPackets: 0, txPackets: 0 };
399
458
  }
400
459
  }
401
- async function collectProcesses(limit = 10) {
460
+ async function collectProcesses(limit) {
402
461
  try {
403
- const raw = (await run(`ps -Ao pid,ppid,user,pcpu,pmem,state,threads,time,comm -r | head -n ${limit + 1}`)).trim();
462
+ const isMac = process.platform === "darwin";
463
+ const sortArg = isMac ? "-r" : "--sort=-pcpu";
464
+ const threadsArg = isMac ? "threads" : "nlwp";
465
+ const headClause = limit && limit > 0 ? ` | head -n ${limit + 1}` : "";
466
+ const raw = (await run(`ps -eo pid,ppid,user,pcpu,pmem,state,${threadsArg},time,comm ${sortArg}${headClause}`)).trim();
404
467
  const lines = raw.split("\n").slice(1);
405
468
  return lines.map((line) => {
406
469
  const parts = line.match(/\s*(\d+)\s+(\d+)\s+(\S+)\s+([\d.]+)\s+([\d.]+)\s+(\S+)\s+(\d+)\s+([\d:]+)\s+(.+)/);
@@ -412,6 +475,95 @@ async function collectProcesses(limit = 10) {
412
475
  return [];
413
476
  }
414
477
  }
478
+ async function collectPackets() {
479
+ try {
480
+ const netStat = (await run("netstat -ib")).trim();
481
+ const lines = netStat.split("\n").slice(1);
482
+ let totalRxPackets = 0;
483
+ let totalTxPackets = 0;
484
+ let totalRxBytes = 0;
485
+ let totalTxBytes = 0;
486
+ const ifaceStats = [];
487
+ for (const line of lines) {
488
+ const parts = line.split(/\s+/);
489
+ if (parts.length >= 11 && parts[0] && !parts[0].startsWith("Name")) {
490
+ const iface = parts[0];
491
+ const rxPackets = parseInt(parts[4]) || 0;
492
+ const rxBytes = parseInt(parts[5]) || 0;
493
+ const txPackets = parseInt(parts[6]) || 0;
494
+ const txBytes = parseInt(parts[7]) || 0;
495
+ totalRxPackets += rxPackets;
496
+ totalTxPackets += txPackets;
497
+ totalRxBytes += rxBytes;
498
+ totalTxBytes += txBytes;
499
+ if (iface && !iface.includes("lo")) {
500
+ ifaceStats.push({ iface, rxPackets, txPackets, rxBytes, txBytes });
501
+ }
502
+ }
503
+ }
504
+ let connections = 0;
505
+ try {
506
+ const tcpConns = (await run("netstat -an | grep ESTABLISHED | wc -l", "0")).trim();
507
+ connections = parseInt(tcpConns) || 0;
508
+ } catch {
509
+ connections = 0;
510
+ }
511
+ let topProcesses = [];
512
+ let allProcesses = [];
513
+ try {
514
+ const lsof = (await run("lsof -i -n -P -r 1 2>/dev/null | tail -n +2 | awk '{print $1, $2}' | sort | uniq -c | sort -rn", "")).trim();
515
+ const lsofLines = lsof.split("\n");
516
+ topProcesses = lsofLines.map((line) => {
517
+ const parts = line.trim().split(/\s+/);
518
+ if (parts.length >= 3) {
519
+ return { pid: parseInt(parts[2]) || 0, command: parts[1], rxBytes: 0, txBytes: parseInt(parts[0]) || 0 };
520
+ }
521
+ return { pid: 0, command: "", rxBytes: 0, txBytes: 0 };
522
+ }).filter((p) => p.pid > 0);
523
+ const allLsof = (await run("lsof -i -n -P 2>/dev/null | tail -n +2 | awk '{print $1, $2}' | sort | uniq -c | sort -rn", "")).trim();
524
+ const allLsofLines = allLsof.split("\n");
525
+ allProcesses = allLsofLines.map((line) => {
526
+ const parts = line.trim().split(/\s+/);
527
+ if (parts.length >= 3) {
528
+ return { pid: parseInt(parts[2]) || 0, command: parts[1], rxBytes: 0, txBytes: parseInt(parts[0]) || 0 };
529
+ }
530
+ return { pid: 0, command: "", rxBytes: 0, txBytes: 0 };
531
+ }).filter((p) => p.pid > 0);
532
+ } catch {
533
+ topProcesses = [];
534
+ allProcesses = [];
535
+ }
536
+ return {
537
+ totalPackets: totalRxPackets + totalTxPackets,
538
+ rxPackets: totalRxPackets,
539
+ txPackets: totalTxPackets,
540
+ rxRate: 0,
541
+ txRate: 0,
542
+ connections,
543
+ topProcesses,
544
+ interfaces: ifaceStats,
545
+ allProcesses
546
+ };
547
+ } catch {
548
+ return null;
549
+ }
550
+ }
551
+ async function collectTasks(limit = 12) {
552
+ try {
553
+ const isMac = process.platform === "darwin";
554
+ const sortArg = isMac ? "-r" : "--sort=-pcpu";
555
+ const raw = (await run(`ps -eo pid,user,pcpu,pmem,state,time,comm ${sortArg} | head -n ${limit + 1}`)).trim();
556
+ const lines = raw.split("\n").slice(1);
557
+ return lines.map((line) => {
558
+ const parts = line.match(/\s*(\d+)\s+(\S+)\s+([\d.]+)\s+([\d.]+)\s+(\S+)\s+([\d:]+)\s+(.+)/);
559
+ if (!parts) return null;
560
+ const runtimeSec = parts[6].split(":").reduce((acc, val, idx) => acc + parseInt(val) * Math.pow(60, 2 - idx), 0);
561
+ return { pid: parseInt(parts[1]), user: parts[2], cpu: parseFloat(parts[3]), mem: parseFloat(parts[4]), state: parts[5], runtime: runtimeSec, command: parts[7] };
562
+ }).filter((t) => t !== null && t.pid > 0);
563
+ } catch {
564
+ return [];
565
+ }
566
+ }
415
567
 
416
568
  // src/formatters/render.ts
417
569
  import chalk3 from "chalk";
@@ -610,11 +762,15 @@ function gpuCard(data, contentWidth) {
610
762
  return lines;
611
763
  }
612
764
  function powerCard(data) {
613
- if (!data.power) return null;
614
765
  const lines = [];
615
- if (data.power.cpuWatts !== void 0) lines.push(statRow("CPU", `${data.power.cpuWatts.toFixed(2)} W`));
616
- if (data.power.gpuWatts !== void 0) lines.push(statRow("GPU", `${data.power.gpuWatts.toFixed(2)} W`));
617
- if (data.power.combinedWatts !== void 0) lines.push(statRow("Total", `${data.power.combinedWatts.toFixed(2)} W`));
766
+ if (data.power) {
767
+ if (data.power.cpuWatts !== void 0) lines.push(statRow("CPU", `${data.power.cpuWatts.toFixed(2)} W`));
768
+ if (data.power.gpuWatts !== void 0) lines.push(statRow("GPU", `${data.power.gpuWatts.toFixed(2)} W`));
769
+ if (data.power.combinedWatts !== void 0) lines.push(statRow("Total", `${data.power.combinedWatts.toFixed(2)} W`));
770
+ }
771
+ if (data.battery?.powerWatts !== void 0 && data.battery.powerWatts > 0) {
772
+ lines.push(statRow("Battery", `${data.battery.powerWatts.toFixed(2)} W`));
773
+ }
618
774
  return lines.length ? lines : null;
619
775
  }
620
776
  function batteryCard(data, contentWidth) {
@@ -630,6 +786,12 @@ function batteryCard(data, contentWidth) {
630
786
  lines.push(statRow("Capacity", capacityColor(data.battery.maxCapacityPercent)(`${data.battery.maxCapacityPercent}%`)));
631
787
  }
632
788
  lines.push(statRow("Source", data.battery.powerSource));
789
+ if (data.battery.estimatedTimeToEmpty && data.battery.state !== "charged") {
790
+ lines.push(statRow("Est. Empty", data.battery.estimatedTimeToEmpty));
791
+ }
792
+ if (data.battery.dischargeRatePerHour !== void 0 && data.battery.state !== "charged") {
793
+ lines.push(statRow("Disch. Rate", `${data.battery.dischargeRatePerHour}%/h`));
794
+ }
633
795
  return lines;
634
796
  }
635
797
  function thermalCard(data, contentWidth) {
@@ -651,6 +813,33 @@ function networkCard(data, contentWidth) {
651
813
  statRow("TX", `${formatBytes(data.network.txBytes)} / ${data.network.txPackets} pkt`)
652
814
  ];
653
815
  }
816
+ function packetCard(data, contentWidth) {
817
+ if (!data.packets) return null;
818
+ const lines = [];
819
+ lines.push(statRow("Total", `${data.packets.totalPackets} pkt`));
820
+ lines.push(statRow("RX", `${data.packets.rxPackets} pkt`));
821
+ lines.push(statRow("TX", `${data.packets.txPackets} pkt`));
822
+ lines.push(statRow("Conns", `${data.packets.connections} TCP`));
823
+ if (data.packets.topProcesses && data.packets.topProcesses.length) {
824
+ lines.push(statRow("Top Proc", truncatePlain(data.packets.topProcesses[0].command, contentWidth - 10)));
825
+ }
826
+ if (data.packets.allProcesses && data.packets.allProcesses.length > 1) {
827
+ lines.push(statRow("Net Procs", `${data.packets.allProcesses.length} processes`));
828
+ }
829
+ if (data.packets.interfaces && data.packets.interfaces.length) {
830
+ lines.push(statRow("Ifaces", `${data.packets.interfaces.length} active`));
831
+ }
832
+ return lines;
833
+ }
834
+ function tasksCard(data, contentWidth) {
835
+ if (!data.tasks || !data.tasks.length) return null;
836
+ const lines = [];
837
+ for (const task of data.tasks.slice(0, 6)) {
838
+ const cmd = truncatePlain(task.command, Math.max(4, contentWidth - 28));
839
+ lines.push(statRow(`${task.pid}`, `${task.user.padEnd(8)} ${task.cpu.toFixed(1).padStart(5)}% ${task.mem.toFixed(1).padStart(5)}% ${task.state} ${cmd}`));
840
+ }
841
+ return lines;
842
+ }
654
843
  function header(data, width, badges = [], borderAccent = THEMES.default.border) {
655
844
  const now = (/* @__PURE__ */ new Date()).toLocaleTimeString();
656
845
  const left = `${chalk3.hex("#ff5500").bold(" PYRE ")} ${chalk3.bold(data.header.hostname)} ${chalk3.dim(data.header.os)} ${chalk3.dim("up " + data.header.uptime)}`;
@@ -659,6 +848,46 @@ function header(data, width, badges = [], borderAccent = THEMES.default.border)
659
848
  const line = left + " ".repeat(gap) + right;
660
849
  return [line, borderAccent("\u2500".repeat(width))];
661
850
  }
851
+ var TAB_DEFS = [
852
+ { id: "cpu", label: "CPU", key: "1" },
853
+ { id: "mem", label: "Memory", key: "2" },
854
+ { id: "gpu", label: "GPU", key: "3" },
855
+ { id: "power", label: "Power", key: "4" },
856
+ { id: "battery", label: "Battery", key: "5" },
857
+ { id: "thermal", label: "Thermal", key: "6" },
858
+ { id: "network", label: "Network", key: "7" },
859
+ { id: "packets", label: "Packets", key: "8" },
860
+ { id: "tasks", label: "Tasks", key: "9" },
861
+ { id: "disk", label: "Disk", key: "0" },
862
+ { id: "process", label: "Process", key: "p" }
863
+ ];
864
+ var TAB_BAR_INDENT = 2;
865
+ var TAB_SEPARATOR_WIDTH = 3;
866
+ function getTabHitboxes() {
867
+ const boxes = [];
868
+ let col = TAB_BAR_INDENT;
869
+ TAB_DEFS.forEach((t, i) => {
870
+ const text = `[${t.key}]${t.label}`;
871
+ const start = col;
872
+ const end = col + text.length;
873
+ boxes.push({ id: t.id, start, end });
874
+ col = end;
875
+ if (i < TAB_DEFS.length - 1) col += TAB_SEPARATOR_WIDTH;
876
+ });
877
+ return boxes;
878
+ }
879
+ var TAB_BAR_ROW = 4;
880
+ function tabBar(activePanel, width, theme) {
881
+ const rendered = TAB_DEFS.map((t) => {
882
+ const isActive = activePanel === t.id;
883
+ const label = isActive ? chalk3.bold(t.label) : chalk3.dim(t.label);
884
+ const keyHint = chalk3.dim(`[${t.key}]`);
885
+ return `${keyHint}${label}`;
886
+ });
887
+ const joined = rendered.join(chalk3.dim(" \u2502 "));
888
+ const padded = visLen(joined) < width ? joined + " ".repeat(width - visLen(joined)) : joined;
889
+ return chalk3.dim(" ") + padded;
890
+ }
662
891
  function sortProcesses(processes, sortBy) {
663
892
  const copy = [...processes];
664
893
  switch (sortBy) {
@@ -746,8 +975,85 @@ function diskTableLines(disks, contentWidth, borderAccent = THEMES.default.borde
746
975
  });
747
976
  return [head, borderAccent("\u2500".repeat(contentWidth)), ...rows];
748
977
  }
978
+ function detailPanelTitle(panel2) {
979
+ const map = {
980
+ cpu: "CPU",
981
+ mem: "Memory",
982
+ gpu: "GPU",
983
+ power: "Power",
984
+ battery: "Battery",
985
+ thermal: "Thermal",
986
+ network: "Network",
987
+ packets: "Packets",
988
+ tasks: "Tasks",
989
+ disk: "Disk",
990
+ process: "Processes"
991
+ };
992
+ return map[panel2] || panel2;
993
+ }
994
+ function activeDetailLines(data, activePanel, width) {
995
+ const contentWidth = width - 4;
996
+ switch (activePanel) {
997
+ case "cpu":
998
+ return cpuCard(data, contentWidth);
999
+ case "mem":
1000
+ return memCard(data, contentWidth);
1001
+ case "gpu": {
1002
+ const gpu = gpuCard(data, contentWidth);
1003
+ return gpu ?? ["No GPU data available"];
1004
+ }
1005
+ case "power": {
1006
+ const power = powerCard(data);
1007
+ if (!power && !data.battery) return ["No power data available"];
1008
+ const lines = [];
1009
+ if (power) lines.push(...power);
1010
+ if (data.battery?.powerWatts !== void 0 && data.battery.powerWatts > 0) {
1011
+ lines.push(statRow("Battery", `${data.battery.powerWatts.toFixed(2)} W`));
1012
+ }
1013
+ return lines.length ? lines : ["No power data available"];
1014
+ }
1015
+ case "battery":
1016
+ return data.battery ? batteryCard(data, contentWidth) : ["No battery data available"];
1017
+ case "thermal":
1018
+ return thermalCard(data, contentWidth);
1019
+ case "network":
1020
+ return networkCard(data, contentWidth);
1021
+ case "packets": {
1022
+ if (!data.packets) return ["No packet data available"];
1023
+ const lines = [];
1024
+ lines.push(statRow("Total", `${data.packets.totalPackets} pkt`));
1025
+ lines.push(statRow("RX", `${data.packets.rxPackets} pkt`));
1026
+ lines.push(statRow("TX", `${data.packets.txPackets} pkt`));
1027
+ lines.push(statRow("Conns", `${data.packets.connections} TCP`));
1028
+ if (data.packets.interfaces && data.packets.interfaces.length) {
1029
+ lines.push(statRow("Ifaces", `${data.packets.interfaces.length} active`));
1030
+ for (const iface of data.packets.interfaces.slice(0, 8)) {
1031
+ lines.push(statRow(` ${iface.iface}`, `RX ${formatBytes(iface.rxBytes)} / TX ${formatBytes(iface.txBytes)}`));
1032
+ }
1033
+ }
1034
+ if (data.packets.allProcesses && data.packets.allProcesses.length) {
1035
+ lines.push(statRow("Net Procs", `${data.packets.allProcesses.length} total`));
1036
+ for (const proc of data.packets.allProcesses.slice(0, 10)) {
1037
+ lines.push(statRow(` ${proc.pid}`, `${truncatePlain(proc.command, 20)} TX ${formatBytes(proc.txBytes)}`));
1038
+ }
1039
+ }
1040
+ return lines;
1041
+ }
1042
+ case "tasks":
1043
+ return data.tasks?.length ? tasksCard(data, contentWidth) : ["No task data available"];
1044
+ case "disk":
1045
+ return data.disk.length ? diskTableLines(data.disk, width - 4, THEMES.default.border) : ["No disk data available"];
1046
+ case "process": {
1047
+ const sorted = sortProcesses([...data.processes], "cpu");
1048
+ return processTableLines(sorted.slice(0, 50), width - 4, THEMES.default.border);
1049
+ }
1050
+ default:
1051
+ return null;
1052
+ }
1053
+ }
749
1054
  function formatTable(data, opts2 = {}) {
750
1055
  const width = clampWidth(opts2.width);
1056
+ const activePanel = opts2.activePanel || "grid";
751
1057
  const columns = gridColumns(width);
752
1058
  const gap = 2;
753
1059
  const cardWidth = Math.floor((width - gap * (columns - 1)) / columns);
@@ -758,6 +1064,15 @@ function formatTable(data, opts2 = {}) {
758
1064
  const out = [];
759
1065
  out.push(...header(data, width, [], theme.border));
760
1066
  out.push("");
1067
+ out.push(tabBar(activePanel, width, theme));
1068
+ out.push("");
1069
+ if (activePanel !== "grid") {
1070
+ const lines = activeDetailLines(data, activePanel, width);
1071
+ if (lines) {
1072
+ out.push(...panel(detailPanelTitle(activePanel), lines, width, theme[activePanel] ?? theme.cpu, theme.border));
1073
+ }
1074
+ return out.join("\n");
1075
+ }
761
1076
  const cards = [];
762
1077
  if (vis.cpu !== false) {
763
1078
  cards.push({ title: "CPU", accent: theme.cpu, lines: cpuCard(data, contentWidth) });
@@ -783,6 +1098,14 @@ function formatTable(data, opts2 = {}) {
783
1098
  if (vis.network !== false) {
784
1099
  cards.push({ title: "Network", accent: theme.network, lines: networkCard(data, contentWidth) });
785
1100
  }
1101
+ if (vis.packets !== false) {
1102
+ const packets = packetCard(data, contentWidth);
1103
+ if (packets) cards.push({ title: "Packets", accent: theme.network, lines: packets });
1104
+ }
1105
+ if (vis.tasks !== false) {
1106
+ const tasks = tasksCard(data, contentWidth);
1107
+ if (tasks) cards.push({ title: "Tasks", accent: theme.process, lines: tasks });
1108
+ }
786
1109
  for (let i = 0; i < cards.length; i += columns) {
787
1110
  const row = cards.slice(i, i + columns);
788
1111
  const rowHeight = Math.max(...row.map((c) => c.lines.length));
@@ -868,11 +1191,17 @@ function formatCsv(data) {
868
1191
  ],
869
1192
  ["network", "rxBytes", String(data.network.rxBytes)],
870
1193
  ["network", "txBytes", String(data.network.txBytes)],
1194
+ ["network", "rxPackets", String(data.network.rxPackets)],
1195
+ ["network", "txPackets", String(data.network.txPackets)],
1196
+ ["network", "connections", data.network.connections ? String(data.network.connections) : ""],
871
1197
  ["battery", "level", data.battery ? String(data.battery.level) : ""],
872
1198
  ["battery", "powerSource", data.battery ? data.battery.powerSource : ""],
873
1199
  ["battery", "condition", data.battery?.condition ?? ""],
874
1200
  ["battery", "maxCapacityPercent", data.battery?.maxCapacityPercent !== void 0 ? String(data.battery.maxCapacityPercent) : ""],
875
1201
  ["battery", "cycles", data.battery?.cycles !== void 0 ? String(data.battery.cycles) : ""],
1202
+ ["battery", "estimatedTimeToEmpty", data.battery?.estimatedTimeToEmpty ?? ""],
1203
+ ["battery", "dischargeRatePerHour", data.battery?.dischargeRatePerHour !== void 0 ? String(data.battery.dischargeRatePerHour) : ""],
1204
+ ["battery", "powerWatts", data.battery?.powerWatts !== void 0 ? String(data.battery.powerWatts) : ""],
876
1205
  ["power", "cpuWatts", data.power?.cpuWatts !== void 0 ? String(data.power.cpuWatts) : ""],
877
1206
  ["power", "gpuWatts", data.power?.gpuWatts !== void 0 ? String(data.power.gpuWatts) : ""],
878
1207
  ["power", "combinedWatts", data.power?.combinedWatts !== void 0 ? String(data.power.combinedWatts) : ""],
@@ -880,6 +1209,18 @@ function formatCsv(data) {
880
1209
  ["system", "uptime", data.header.uptime],
881
1210
  ["system", "timestamp", data.timestamp]
882
1211
  ];
1212
+ if (data.packets) {
1213
+ rows.push(["packets", "totalPackets", String(data.packets.totalPackets)]);
1214
+ rows.push(["packets", "rxPackets", String(data.packets.rxPackets)]);
1215
+ rows.push(["packets", "txPackets", String(data.packets.txPackets)]);
1216
+ rows.push(["packets", "connections", String(data.packets.connections)]);
1217
+ for (const iface of data.packets.interfaces || []) {
1218
+ rows.push(["packets_iface", iface.iface, `${iface.rxPackets},${iface.txPackets},${iface.rxBytes},${iface.txBytes}`]);
1219
+ }
1220
+ for (const proc of data.packets.allProcesses || []) {
1221
+ rows.push(["packets_proc", `pid=${proc.pid}`, `${proc.command},${proc.rxBytes},${proc.txBytes}`]);
1222
+ }
1223
+ }
883
1224
  if (data.gpu) {
884
1225
  rows.push(["gpu", "model", data.gpu.model]);
885
1226
  rows.push(["gpu", "memory", String(data.gpu.memory)]);
@@ -916,6 +1257,15 @@ function formatGraphs(history, width = 80, themeName = "default", graphMode = "s
916
1257
  );
917
1258
  lines.push(graphRow("Net RX/s", history.netRxRate, {}, (v) => `${formatBytes(v)}/s`, sparkWidth, graphMode));
918
1259
  lines.push(graphRow("Net TX/s", history.netTxRate, {}, (v) => `${formatBytes(v)}/s`, sparkWidth, graphMode));
1260
+ if (history.powerWatts.length) {
1261
+ lines.push(graphRow("Power W", history.powerWatts, {}, (v) => `${v.toFixed(1)} W`, sparkWidth, graphMode));
1262
+ }
1263
+ if (history.rxPacketRate.length) {
1264
+ lines.push(graphRow("RX pkt/s", history.rxPacketRate, {}, (v) => `${v.toFixed(0)} pkt/s`, sparkWidth, graphMode));
1265
+ }
1266
+ if (history.txPacketRate.length) {
1267
+ lines.push(graphRow("TX pkt/s", history.txPacketRate, {}, (v) => `${v.toFixed(0)} pkt/s`, sparkWidth, graphMode));
1268
+ }
919
1269
  const title = `Graphs \xB7 mode:${graphMode} \xB7 last ${sampleCount} sample${sampleCount === 1 ? "" : "s"}`;
920
1270
  return panel(title, lines, width, theme.graphs, theme.border).join("\n");
921
1271
  }
@@ -966,8 +1316,18 @@ var History = class {
966
1316
  netTxRate = [];
967
1317
  /** Rolling window of GPU utilization percentages (0–100). */
968
1318
  gpuUtil = [];
1319
+ /** Rolling window of combined power draw in watts. */
1320
+ powerWatts = [];
1321
+ /** Rolling window of network receive packet rates in packets/sec. */
1322
+ rxPacketRate = [];
1323
+ /** Rolling window of network transmit packet rates in packets/sec. */
1324
+ txPacketRate = [];
1325
+ /** Rolling window of active TCP connections. */
1326
+ connections = [];
969
1327
  lastRxBytes = 0;
970
1328
  lastTxBytes = 0;
1329
+ lastRxPackets = 0;
1330
+ lastTxPackets = 0;
971
1331
  lastTs = 0;
972
1332
  /**
973
1333
  * Create a new History instance.
@@ -987,13 +1347,23 @@ var History = class {
987
1347
  const now = Date.now();
988
1348
  let rxRate = 0;
989
1349
  let txRate = 0;
1350
+ let rxPktRate = 0;
1351
+ let txPktRate = 0;
990
1352
  if (this.lastTs > 0) {
991
1353
  const dtSeconds = Math.max((now - this.lastTs) / 1e3, 1e-3);
992
1354
  rxRate = Math.max(0, (sample.rxBytes - this.lastRxBytes) / dtSeconds);
993
1355
  txRate = Math.max(0, (sample.txBytes - this.lastTxBytes) / dtSeconds);
1356
+ if (sample.rxPackets !== void 0) {
1357
+ rxPktRate = Math.max(0, (sample.rxPackets - this.lastRxPackets) / dtSeconds);
1358
+ }
1359
+ if (sample.txPackets !== void 0) {
1360
+ txPktRate = Math.max(0, (sample.txPackets - this.lastTxPackets) / dtSeconds);
1361
+ }
994
1362
  }
995
1363
  this.lastRxBytes = sample.rxBytes;
996
1364
  this.lastTxBytes = sample.txBytes;
1365
+ if (sample.rxPackets !== void 0) this.lastRxPackets = sample.rxPackets;
1366
+ if (sample.txPackets !== void 0) this.lastTxPackets = sample.txPackets;
997
1367
  this.lastTs = now;
998
1368
  this.pushBounded(this.cpuUsage, sample.cpuUsage);
999
1369
  this.pushBounded(this.memUsage, sample.memUsage);
@@ -1001,6 +1371,10 @@ var History = class {
1001
1371
  this.pushBounded(this.netRxRate, rxRate);
1002
1372
  this.pushBounded(this.netTxRate, txRate);
1003
1373
  if (sample.gpuUtil !== void 0) this.pushBounded(this.gpuUtil, sample.gpuUtil);
1374
+ if (sample.powerWatts !== void 0) this.pushBounded(this.powerWatts, sample.powerWatts);
1375
+ if (sample.rxPackets !== void 0) this.pushBounded(this.rxPacketRate, rxPktRate);
1376
+ if (sample.txPackets !== void 0) this.pushBounded(this.txPacketRate, txPktRate);
1377
+ if (sample.connections !== void 0) this.pushBounded(this.connections, sample.connections);
1004
1378
  }
1005
1379
  /** Clear all rolling windows and reset cumulative byte counters. */
1006
1380
  reset() {
@@ -1010,8 +1384,14 @@ var History = class {
1010
1384
  this.netRxRate = [];
1011
1385
  this.netTxRate = [];
1012
1386
  this.gpuUtil = [];
1387
+ this.powerWatts = [];
1388
+ this.rxPacketRate = [];
1389
+ this.txPacketRate = [];
1390
+ this.connections = [];
1013
1391
  this.lastRxBytes = 0;
1014
1392
  this.lastTxBytes = 0;
1393
+ this.lastRxPackets = 0;
1394
+ this.lastTxPackets = 0;
1015
1395
  this.lastTs = 0;
1016
1396
  }
1017
1397
  /**
@@ -1021,7 +1401,7 @@ var History = class {
1021
1401
  */
1022
1402
  setMaxLen(n) {
1023
1403
  this.maxLen = Math.max(1, n);
1024
- for (const arr of [this.cpuUsage, this.memUsage, this.temp, this.netRxRate, this.netTxRate, this.gpuUtil]) {
1404
+ for (const arr of [this.cpuUsage, this.memUsage, this.temp, this.netRxRate, this.netTxRate, this.gpuUtil, this.powerWatts, this.rxPacketRate, this.txPacketRate, this.connections]) {
1025
1405
  while (arr.length > this.maxLen) arr.shift();
1026
1406
  }
1027
1407
  }
@@ -1047,10 +1427,13 @@ var state = {
1047
1427
  visiblePanels: {
1048
1428
  cpu: true,
1049
1429
  mem: true,
1430
+ gpu: true,
1050
1431
  power: true,
1051
1432
  battery: true,
1052
1433
  thermal: true,
1053
1434
  network: true,
1435
+ packets: true,
1436
+ tasks: true,
1054
1437
  disk: true,
1055
1438
  process: true
1056
1439
  },
@@ -1075,17 +1458,35 @@ var state = {
1075
1458
  "Graph Mode",
1076
1459
  "Toggle CPU",
1077
1460
  "Toggle Memory",
1461
+ "Toggle GPU",
1078
1462
  "Toggle Power",
1079
1463
  "Toggle Battery",
1080
1464
  "Toggle Thermal",
1081
1465
  "Toggle Network",
1466
+ "Toggle Packets",
1467
+ "Toggle Tasks",
1082
1468
  "Toggle Disk",
1083
1469
  "Toggle Processes",
1084
1470
  "Toggle Tree View"
1085
1471
  ],
1086
1472
  CPU_ALERT_PCT: 90,
1087
1473
  TEMP_ALERT_C: 95,
1088
- alerted: false
1474
+ alerted: false,
1475
+ activePanel: "grid",
1476
+ mouseEnabled: true,
1477
+ PANEL_TABS: [
1478
+ { id: "cpu", label: "CPU", key: "1" },
1479
+ { id: "mem", label: "Memory", key: "2" },
1480
+ { id: "gpu", label: "GPU", key: "3" },
1481
+ { id: "power", label: "Power", key: "4" },
1482
+ { id: "battery", label: "Battery", key: "5" },
1483
+ { id: "thermal", label: "Thermal", key: "6" },
1484
+ { id: "network", label: "Network", key: "7" },
1485
+ { id: "packets", label: "Packets", key: "8" },
1486
+ { id: "tasks", label: "Tasks", key: "9" },
1487
+ { id: "disk", label: "Disk", key: "0" },
1488
+ { id: "process", label: "Process", key: "P" }
1489
+ ]
1089
1490
  };
1090
1491
  function setStatus(msg, ms = 3e3) {
1091
1492
  state.statusMessage = msg;
@@ -1124,7 +1525,7 @@ function startLogging() {
1124
1525
  ensureDir(state.exportDir);
1125
1526
  const file = path.join(state.exportDir, `pyre-log-${timestampForFile()}.csv`);
1126
1527
  state.logStream = fs.createWriteStream(file, { flags: "a" });
1127
- state.logStream.write("timestamp,cpu_usage,mem_usage_percent,temp_c,net_rx_bytes,net_tx_bytes,thermal_state\n");
1528
+ 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");
1128
1529
  state.logging = true;
1129
1530
  setStatus(`Logging every tick -> ${file}`);
1130
1531
  } catch (err) {
@@ -1144,8 +1545,11 @@ function toggleLogging() {
1144
1545
  function writeLogRow(data) {
1145
1546
  if (!state.logging || !state.logStream) return;
1146
1547
  const temp = data.cpu.temperature ?? data.thermal.temperatures?.cpu_die ?? "";
1548
+ const rxPackets = data.network.rxPackets ?? 0;
1549
+ const txPackets = data.network.txPackets ?? 0;
1550
+ const connections = data.network.connections ?? 0;
1147
1551
  state.logStream.write(
1148
- `${data.timestamp},${data.cpu.usage},${data.memory.usagePercent},${temp},${data.network.rxBytes},${data.network.txBytes},${data.thermal.state}
1552
+ `${data.timestamp},${data.cpu.usage},${data.memory.usagePercent},${temp},${data.network.rxBytes},${data.network.txBytes},${rxPackets},${txPackets},${connections},${data.thermal.state}
1149
1553
  `
1150
1554
  );
1151
1555
  }
@@ -1154,7 +1558,8 @@ function writeLogRow(data) {
1154
1558
  import chalk5 from "chalk";
1155
1559
  function processRowBudget() {
1156
1560
  const columns = gridColumns(state.termWidth);
1157
- const cardRows = Math.ceil(6 / columns);
1561
+ const visibleCards = Object.values(state.visiblePanels).filter((v) => v !== false).length;
1562
+ const cardRows = Math.ceil(visibleCards / columns);
1158
1563
  const reserved = 2 + cardRows * 8 + 11 + (state.showGraphs ? 9 : 0) + 4;
1159
1564
  return Math.max(3, Math.min(40, state.termHeight - reserved));
1160
1565
  }
@@ -1193,7 +1598,8 @@ function render() {
1193
1598
  processLimit: processRowBudget(),
1194
1599
  theme: state.currentTheme,
1195
1600
  visible: state.visiblePanels,
1196
- treeView: state.treeView
1601
+ treeView: state.treeView,
1602
+ activePanel: state.activePanel
1197
1603
  })
1198
1604
  );
1199
1605
  if (state.showGraphs) {
@@ -1228,6 +1634,8 @@ function footerLine() {
1228
1634
  ["g", state.showGraphs ? "hide graphs" : "show graphs"],
1229
1635
  ["b", `graph:${state.graphMode}`],
1230
1636
  ["d", state.detailed ? "basic" : "detailed"],
1637
+ ["\u2190/\u2192", "tab"],
1638
+ ["1-9", "panels"],
1231
1639
  ["s", `sort:${state.sortMode}`],
1232
1640
  ["/", "filter"],
1233
1641
  ["k", "kill"],
@@ -1242,6 +1650,7 @@ function footerLine() {
1242
1650
  const badges = [];
1243
1651
  if (state.paused) badges.push(chalk5.yellow.bold("\u23F8 PAUSED"));
1244
1652
  if (state.logging) badges.push(chalk5.red.bold("\u25CF REC"));
1653
+ if (state.activePanel !== "grid") badges.push(chalk5.cyan.bold(`\u25C9 ${state.activePanel.toUpperCase()}`));
1245
1654
  const badgeStr = badges.join(" ");
1246
1655
  return badgeStr ? `${keyStr} ${badgeStr}` : keyStr;
1247
1656
  }
@@ -1363,7 +1772,11 @@ async function tick() {
1363
1772
  temp,
1364
1773
  rxBytes: data.network.rxBytes,
1365
1774
  txBytes: data.network.txBytes,
1366
- gpuUtil: data.gpu?.utilization
1775
+ gpuUtil: data.gpu?.utilization,
1776
+ powerWatts: data.power?.combinedWatts ?? data.power?.cpuWatts ?? 0,
1777
+ rxPackets: data.network.rxPackets,
1778
+ txPackets: data.network.txPackets,
1779
+ connections: data.network.connections ?? 0
1367
1780
  });
1368
1781
  writeLogRow(data);
1369
1782
  checkAlerts(data);
@@ -1386,6 +1799,9 @@ async function startLive(opts2) {
1386
1799
  process.stdout.write("\x1B[?25l");
1387
1800
  process.stdout.write("\x1B[2J\x1B[H");
1388
1801
  process.title = "pyre";
1802
+ if (state.mouseEnabled) {
1803
+ process.stdout.write("\x1B[?1000h");
1804
+ }
1389
1805
  await tick();
1390
1806
  restartTicker();
1391
1807
  if (opts2.autoLog) startLogging();
@@ -1403,6 +1819,18 @@ async function startLive(opts2) {
1403
1819
  handleInputModeKey(str, key);
1404
1820
  return;
1405
1821
  }
1822
+ if (key.mouse) {
1823
+ handleMouseClick(key.mouse);
1824
+ return;
1825
+ }
1826
+ if (key.sequence && key.sequence.startsWith("\x1B[M")) {
1827
+ handleNormalMouse(key.sequence);
1828
+ return;
1829
+ }
1830
+ if (key.sequence && key.sequence.startsWith("\x1B[<")) {
1831
+ handleSGRMouse(key.sequence);
1832
+ return;
1833
+ }
1406
1834
  switch (key.sequence) {
1407
1835
  case "+":
1408
1836
  state.interval += 1;
@@ -1419,11 +1847,69 @@ async function startLive(opts2) {
1419
1847
  state.inputBuffer = state.processFilter;
1420
1848
  render();
1421
1849
  return;
1850
+ case "0":
1851
+ case "1":
1852
+ case "2":
1853
+ case "3":
1854
+ case "4":
1855
+ case "5":
1856
+ case "6":
1857
+ case "7":
1858
+ case "8":
1859
+ case "9": {
1860
+ const tabMap = {
1861
+ "0": "disk",
1862
+ "1": "cpu",
1863
+ "2": "mem",
1864
+ "3": "gpu",
1865
+ "4": "power",
1866
+ "5": "battery",
1867
+ "6": "thermal",
1868
+ "7": "network",
1869
+ "8": "packets",
1870
+ "9": "tasks"
1871
+ };
1872
+ const panelId = tabMap[key.sequence];
1873
+ if (panelId) {
1874
+ state.activePanel = state.activePanel === panelId ? "grid" : panelId;
1875
+ setStatus(state.activePanel === "grid" ? "Grid view" : `${panelId} panel`);
1876
+ render();
1877
+ }
1878
+ return;
1879
+ }
1880
+ case "P": {
1881
+ state.activePanel = state.activePanel === "process" ? "grid" : "process";
1882
+ setStatus(state.activePanel === "grid" ? "Grid view" : "Process panel");
1883
+ render();
1884
+ return;
1885
+ }
1422
1886
  }
1423
1887
  switch (key.name) {
1424
1888
  case "q":
1425
1889
  stopLive();
1426
1890
  break;
1891
+ case "left": {
1892
+ const currentIdx = state.PANEL_TABS.findIndex((t) => t.id === state.activePanel);
1893
+ if (currentIdx > 0) {
1894
+ state.activePanel = state.PANEL_TABS[currentIdx - 1].id;
1895
+ } else if (currentIdx === -1) {
1896
+ state.activePanel = state.PANEL_TABS[state.PANEL_TABS.length - 1].id;
1897
+ }
1898
+ setStatus(`Panel: ${state.activePanel}`);
1899
+ render();
1900
+ break;
1901
+ }
1902
+ case "right": {
1903
+ const currentIdx = state.PANEL_TABS.findIndex((t) => t.id === state.activePanel);
1904
+ if (currentIdx < state.PANEL_TABS.length - 1) {
1905
+ state.activePanel = state.PANEL_TABS[currentIdx + 1].id;
1906
+ } else if (currentIdx === -1) {
1907
+ state.activePanel = state.PANEL_TABS[0].id;
1908
+ }
1909
+ setStatus(`Panel: ${state.activePanel}`);
1910
+ render();
1911
+ break;
1912
+ }
1427
1913
  case "c":
1428
1914
  state.inputMode = "customizer";
1429
1915
  state.customizerIndex = 0;
@@ -1482,11 +1968,70 @@ async function startLive(opts2) {
1482
1968
  setStatus(`Export format: ${state.exportFormat}`);
1483
1969
  render();
1484
1970
  break;
1971
+ case "tab":
1972
+ cycleTab(1);
1973
+ break;
1974
+ case "left":
1975
+ cycleTab(-1);
1976
+ break;
1977
+ case "right":
1978
+ cycleTab(1);
1979
+ break;
1980
+ default:
1981
+ if (str && str.length === 1) {
1982
+ const tabId = tabKeyToId(str);
1983
+ if (tabId) {
1984
+ state.activePanel = tabId;
1985
+ setStatus(`Panel: ${tabId.toUpperCase()}`);
1986
+ render();
1987
+ }
1988
+ }
1485
1989
  }
1486
1990
  };
1487
1991
  process.stdin.on("keypress", state.keypressHandler);
1488
1992
  process.once("SIGINT", () => stopLive());
1489
1993
  }
1994
+ function tabKeyToId(str) {
1995
+ const map = { "1": "cpu", "2": "mem", "3": "gpu", "4": "power", "5": "battery", "6": "thermal", "7": "network", "8": "packets", "9": "tasks", "0": "disk", "p": "process" };
1996
+ return map[str] ?? null;
1997
+ }
1998
+ function cycleTab(direction) {
1999
+ const tabs = state.PANEL_TABS.map((t) => t.id);
2000
+ const current = state.activePanel === "grid" ? -1 : tabs.indexOf(state.activePanel);
2001
+ const next = (current + direction + tabs.length) % tabs.length;
2002
+ state.activePanel = tabs[next];
2003
+ setStatus(`Panel: ${state.activePanel.toUpperCase()}`);
2004
+ render();
2005
+ }
2006
+ function handleNormalMouse(seq) {
2007
+ if (seq.length < 6) return;
2008
+ const cb = seq.charCodeAt(3) - 32;
2009
+ const cx = seq.charCodeAt(4) - 32;
2010
+ const cy = seq.charCodeAt(5) - 32;
2011
+ if (cb === 0 || cb === 1) {
2012
+ handleMouseClick(cy, cx);
2013
+ }
2014
+ }
2015
+ function handleSGRMouse(seq) {
2016
+ const match = seq.match(/^\x1b\[<(\d+);(\d+);(\d+)([Mm])/);
2017
+ if (!match) return;
2018
+ const button = parseInt(match[1]);
2019
+ const cx = parseInt(match[2]);
2020
+ const cy = parseInt(match[3]);
2021
+ if (button === 0 || button === 1) {
2022
+ handleMouseClick(cy, cx);
2023
+ }
2024
+ }
2025
+ function handleMouseClick(y, x) {
2026
+ if (y !== TAB_BAR_ROW) return;
2027
+ const hitboxes = getTabHitboxes();
2028
+ const hit = hitboxes.find((h) => x >= h.start && x < h.end);
2029
+ if (hit) {
2030
+ state.activePanel = state.activePanel === hit.id ? "grid" : hit.id;
2031
+ setStatus(state.activePanel === "grid" ? "Grid view" : `Panel: ${hit.id.toUpperCase()}`);
2032
+ render();
2033
+ }
2034
+ }
1490
2035
  function stopLive() {
1491
2036
  if (state.intervalHandle) {
1492
2037
  clearInterval(state.intervalHandle);
@@ -1509,6 +2054,9 @@ function stopLive() {
1509
2054
  state.logging = false;
1510
2055
  if (process.stdin.isTTY) process.stdin.setRawMode(false);
1511
2056
  process.stdin.pause();
2057
+ if (state.mouseEnabled) {
2058
+ process.stdout.write("\x1B[?1000l");
2059
+ }
1512
2060
  process.stdout.write("\x1B[?25h");
1513
2061
  process.stdout.write("\x1B[?1049l");
1514
2062
  process.stdout.write("\x1B[2J\x1B[H");
@@ -1626,9 +2174,375 @@ async function showSplash() {
1626
2174
  });
1627
2175
  }
1628
2176
 
2177
+ // src/p2p/server.ts
2178
+ import net from "net";
2179
+ import chalk7 from "chalk";
2180
+
2181
+ // src/p2p/protocol.ts
2182
+ var HEADER_SIZE = 4;
2183
+ var MAX_MESSAGE_SIZE = 16 * 1024 * 1024;
2184
+ function buildMessage(kind, payload) {
2185
+ const envelope = { kind, payload };
2186
+ const body = Buffer.from(JSON.stringify(envelope), "utf-8");
2187
+ const header2 = Buffer.alloc(HEADER_SIZE);
2188
+ header2.writeUInt32BE(body.length, 0);
2189
+ return Buffer.concat([header2, body]);
2190
+ }
2191
+ function encodeAuth(password) {
2192
+ return buildMessage("auth", { password });
2193
+ }
2194
+ function encodeAuthOk() {
2195
+ return buildMessage("auth-ok", { ok: true });
2196
+ }
2197
+ function encodeAuthFail(reason) {
2198
+ return buildMessage("auth-fail", { ok: false, reason });
2199
+ }
2200
+ function encodeData(stats) {
2201
+ return buildMessage("data", { stats });
2202
+ }
2203
+ function encodePing() {
2204
+ return buildMessage("ping", { ts: Date.now() });
2205
+ }
2206
+ function encodePong(ts) {
2207
+ return buildMessage("pong", { ts });
2208
+ }
2209
+ function encodeDisconnect(reason) {
2210
+ return buildMessage("disconnect", { reason });
2211
+ }
2212
+
2213
+ // src/p2p/server.ts
2214
+ var HEADER_SIZE2 = 4;
2215
+ function readMessageFromPeer(peer) {
2216
+ while (peer.readBuffer.length > 0) {
2217
+ if (peer.readExpectingHeader) {
2218
+ if (peer.readBuffer.length >= HEADER_SIZE2) {
2219
+ peer.readMessageLength = peer.readBuffer.readUInt32BE(0);
2220
+ peer.readBuffer = peer.readBuffer.subarray(HEADER_SIZE2);
2221
+ peer.readExpectingHeader = false;
2222
+ } else {
2223
+ break;
2224
+ }
2225
+ }
2226
+ if (!peer.readExpectingHeader) {
2227
+ if (peer.readBuffer.length >= peer.readMessageLength) {
2228
+ const body = peer.readBuffer.subarray(0, peer.readMessageLength);
2229
+ peer.readBuffer = peer.readBuffer.subarray(peer.readMessageLength);
2230
+ peer.readExpectingHeader = true;
2231
+ return body;
2232
+ } else {
2233
+ break;
2234
+ }
2235
+ }
2236
+ }
2237
+ return null;
2238
+ }
2239
+ var P2PServer = class {
2240
+ server = null;
2241
+ peers = /* @__PURE__ */ new Map();
2242
+ options;
2243
+ running = false;
2244
+ constructor(options) {
2245
+ this.options = options;
2246
+ }
2247
+ get info() {
2248
+ return {
2249
+ host: this.options.host,
2250
+ port: this.options.port,
2251
+ peersConnected: this.peers.size
2252
+ };
2253
+ }
2254
+ async start() {
2255
+ return new Promise((resolve, reject) => {
2256
+ this.server = net.createServer((socket) => this.onConnection(socket));
2257
+ this.server.on("error", (err) => reject(err));
2258
+ this.server.listen(this.options.port, this.options.host, () => {
2259
+ this.running = true;
2260
+ const addr = this.server?.address();
2261
+ const bound = addr ? typeof addr === "object" ? `${addr.address}:${addr.port}` : String(addr) : "unknown";
2262
+ console.log(chalk7.green(`P2P server listening on ${bound}`));
2263
+ console.log(chalk7.dim(` Password: ${this.options.password}`));
2264
+ console.log(chalk7.dim(` Interval: ${this.options.intervalMs}ms`));
2265
+ resolve();
2266
+ });
2267
+ });
2268
+ }
2269
+ stop() {
2270
+ this.running = false;
2271
+ for (const peer of this.peers.values()) {
2272
+ this.disconnectPeer(peer, "Server shutting down");
2273
+ }
2274
+ this.peers.clear();
2275
+ if (this.server) {
2276
+ this.server.close();
2277
+ this.server = null;
2278
+ }
2279
+ }
2280
+ onConnection(socket) {
2281
+ const peer = {
2282
+ socket,
2283
+ authenticated: false,
2284
+ intervalHandle: null,
2285
+ pingHandle: null,
2286
+ readBuffer: Buffer.alloc(0),
2287
+ readExpectingHeader: true,
2288
+ readMessageLength: 0
2289
+ };
2290
+ this.peers.set(socket, peer);
2291
+ const remoteAddr = socket.remoteAddress ? `${socket.remoteAddress}:${socket.remotePort}` : "unknown";
2292
+ console.log(chalk7.cyan(`Peer connected: ${remoteAddr}`));
2293
+ socket.on("data", (data) => this.onData(peer, data));
2294
+ socket.on("close", () => this.onPeerClose(peer));
2295
+ socket.on("error", (err) => this.onPeerError(peer, err));
2296
+ socket.setTimeout(3e4);
2297
+ socket.on("timeout", () => {
2298
+ if (!peer.authenticated) {
2299
+ this.disconnectPeer(peer, "Authentication timeout");
2300
+ }
2301
+ });
2302
+ }
2303
+ onData(peer, data) {
2304
+ peer.readBuffer = Buffer.concat([peer.readBuffer, data]);
2305
+ if (!peer.authenticated) {
2306
+ const body = readMessageFromPeer(peer);
2307
+ if (body) {
2308
+ this.handleAuthMessage(peer, body);
2309
+ }
2310
+ return;
2311
+ }
2312
+ }
2313
+ handleAuthMessage(peer, body) {
2314
+ try {
2315
+ const msg = JSON.parse(body.toString("utf-8"));
2316
+ if (msg.kind === "auth" && typeof msg.payload?.password === "string") {
2317
+ if (msg.payload.password === this.options.password) {
2318
+ peer.authenticated = true;
2319
+ peer.socket.write(encodeAuthOk());
2320
+ console.log(chalk7.green(`Peer authenticated: ${peer.socket.remoteAddress}`));
2321
+ this.startDataStream(peer);
2322
+ this.startPing(peer);
2323
+ } else {
2324
+ peer.socket.write(encodeAuthFail("Invalid password"));
2325
+ this.disconnectPeer(peer, "Authentication failed");
2326
+ }
2327
+ } else {
2328
+ peer.socket.write(encodeAuthFail("Expected auth message first"));
2329
+ this.disconnectPeer(peer, "Expected auth message");
2330
+ }
2331
+ } catch {
2332
+ peer.socket.write(encodeAuthFail("Invalid message format"));
2333
+ this.disconnectPeer(peer, "Invalid message format");
2334
+ }
2335
+ }
2336
+ startDataStream(peer) {
2337
+ const send = async () => {
2338
+ if (!peer.authenticated || peer.socket.destroyed) return;
2339
+ try {
2340
+ const data = await collectAll({ detailed: this.options.detailed });
2341
+ peer.socket.write(encodeData(data));
2342
+ } catch {
2343
+ }
2344
+ };
2345
+ send();
2346
+ peer.intervalHandle = setInterval(send, this.options.intervalMs);
2347
+ }
2348
+ startPing(peer) {
2349
+ peer.pingHandle = setInterval(() => {
2350
+ if (peer.socket.destroyed) return;
2351
+ peer.socket.write(encodePing());
2352
+ }, 5e3);
2353
+ }
2354
+ onPeerClose(peer) {
2355
+ this.cleanupPeer(peer);
2356
+ console.log(chalk7.yellow(`Peer disconnected: ${peer.socket.remoteAddress}`));
2357
+ }
2358
+ onPeerError(peer, err) {
2359
+ const code = err.code;
2360
+ if (code === "ECONNRESET") {
2361
+ console.log(chalk7.yellow(`Peer connection reset: ${peer.socket.remoteAddress}`));
2362
+ } else {
2363
+ console.log(chalk7.red(`Peer error: ${err.message}`));
2364
+ }
2365
+ this.cleanupPeer(peer);
2366
+ }
2367
+ disconnectPeer(peer, reason) {
2368
+ try {
2369
+ peer.socket.write(encodeDisconnect(reason));
2370
+ } catch {
2371
+ }
2372
+ peer.socket.destroy();
2373
+ }
2374
+ cleanupPeer(peer) {
2375
+ if (peer.intervalHandle) {
2376
+ clearInterval(peer.intervalHandle);
2377
+ peer.intervalHandle = null;
2378
+ }
2379
+ if (peer.pingHandle) {
2380
+ clearInterval(peer.pingHandle);
2381
+ peer.pingHandle = null;
2382
+ }
2383
+ this.peers.delete(peer.socket);
2384
+ }
2385
+ };
2386
+
2387
+ // src/p2p/client.ts
2388
+ import net2 from "net";
2389
+ var P2PClient = class {
2390
+ socket = null;
2391
+ options;
2392
+ running = false;
2393
+ readState = { buffer: Buffer.alloc(0), expectingHeader: true, messageLength: 0 };
2394
+ onDataCallback = null;
2395
+ onStatusCallback = null;
2396
+ reconnectTimer = null;
2397
+ pingHandle = null;
2398
+ constructor(options) {
2399
+ this.options = options;
2400
+ }
2401
+ setOnData(cb) {
2402
+ this.onDataCallback = cb;
2403
+ }
2404
+ setOnStatus(cb) {
2405
+ this.onStatusCallback = cb;
2406
+ }
2407
+ async connect() {
2408
+ return new Promise((resolve, reject) => {
2409
+ this.socket = new net2.Socket();
2410
+ this.socket.on("data", (data) => this.onData(data));
2411
+ this.socket.on("close", () => this.onClose());
2412
+ this.socket.on("error", (err) => this.onError(err));
2413
+ this.socket.on("timeout", () => this.onTimeout());
2414
+ this.socket.setTimeout(1e4);
2415
+ this.socket.connect(this.options.port, this.options.host, () => {
2416
+ this.running = true;
2417
+ this.socket?.setTimeout(0);
2418
+ this.socket?.write(encodeAuth(this.options.password));
2419
+ this.status("Connecting...");
2420
+ resolve();
2421
+ });
2422
+ this.socket.once("error", (err) => {
2423
+ reject(err);
2424
+ });
2425
+ });
2426
+ }
2427
+ disconnect() {
2428
+ this.running = false;
2429
+ if (this.reconnectTimer) {
2430
+ clearTimeout(this.reconnectTimer);
2431
+ this.reconnectTimer = null;
2432
+ }
2433
+ if (this.pingHandle) {
2434
+ clearInterval(this.pingHandle);
2435
+ this.pingHandle = null;
2436
+ }
2437
+ if (this.socket) {
2438
+ try {
2439
+ this.socket.write(encodeDisconnect("Client closing"));
2440
+ } catch {
2441
+ }
2442
+ this.socket.destroy();
2443
+ this.socket = null;
2444
+ }
2445
+ this.status("Disconnected");
2446
+ }
2447
+ onData(data) {
2448
+ this.readState.buffer = Buffer.concat([this.readState.buffer, data]);
2449
+ while (this.readState.buffer.length > 0) {
2450
+ if (this.readState.expectingHeader) {
2451
+ if (this.readState.buffer.length >= 4) {
2452
+ this.readState.messageLength = this.readState.buffer.readUInt32BE(0);
2453
+ this.readState.buffer = this.readState.buffer.subarray(4);
2454
+ this.readState.expectingHeader = false;
2455
+ } else {
2456
+ break;
2457
+ }
2458
+ }
2459
+ if (!this.readState.expectingHeader) {
2460
+ if (this.readState.buffer.length >= this.readState.messageLength) {
2461
+ const body = this.readState.buffer.subarray(0, this.readState.messageLength);
2462
+ this.readState.buffer = this.readState.buffer.subarray(this.readState.messageLength);
2463
+ this.readState.expectingHeader = true;
2464
+ this.handleMessage(body);
2465
+ } else {
2466
+ break;
2467
+ }
2468
+ }
2469
+ }
2470
+ }
2471
+ handleMessage(body) {
2472
+ try {
2473
+ const msg = JSON.parse(body.toString("utf-8"));
2474
+ switch (msg.kind) {
2475
+ case "auth-ok":
2476
+ this.status("Authenticated \u2014 receiving live data");
2477
+ this.startPing();
2478
+ break;
2479
+ case "auth-fail":
2480
+ this.status(`Authentication failed: ${msg.payload.reason}`);
2481
+ this.disconnect();
2482
+ break;
2483
+ case "data":
2484
+ if (this.onDataCallback && msg.payload?.stats) {
2485
+ this.onDataCallback(msg.payload.stats);
2486
+ }
2487
+ break;
2488
+ case "ping":
2489
+ if (this.socket && !this.socket.destroyed) {
2490
+ const ts = msg.payload.ts;
2491
+ this.socket.write(encodePong(ts));
2492
+ }
2493
+ break;
2494
+ case "pong":
2495
+ break;
2496
+ case "disconnect":
2497
+ this.status(`Server disconnected: ${msg.payload.reason}`);
2498
+ this.disconnect();
2499
+ break;
2500
+ default:
2501
+ break;
2502
+ }
2503
+ } catch {
2504
+ }
2505
+ }
2506
+ startPing() {
2507
+ this.pingHandle = setInterval(() => {
2508
+ if (this.socket && !this.socket.destroyed) {
2509
+ this.socket.write(encodePing());
2510
+ }
2511
+ }, 15e3);
2512
+ }
2513
+ onClose() {
2514
+ if (this.running) {
2515
+ this.status("Connection closed \u2014 reconnecting in 3s");
2516
+ this.reconnectTimer = setTimeout(() => {
2517
+ if (this.running) this.connect().catch(() => {
2518
+ });
2519
+ }, 3e3);
2520
+ }
2521
+ }
2522
+ onError(err) {
2523
+ const code = err.code;
2524
+ if (code === "ECONNREFUSED") {
2525
+ this.status("Connection refused \u2014 is the server running?");
2526
+ } else if (code === "ETIMEDOUT") {
2527
+ this.status("Connection timed out");
2528
+ } else {
2529
+ this.status(`Connection error: ${err.message}`);
2530
+ }
2531
+ }
2532
+ onTimeout() {
2533
+ this.status("Connection timed out");
2534
+ this.disconnect();
2535
+ }
2536
+ status(msg) {
2537
+ if (this.onStatusCallback) {
2538
+ this.onStatusCallback(msg);
2539
+ }
2540
+ }
2541
+ };
2542
+
1629
2543
  // src/index.ts
1630
2544
  var program = new Command();
1631
- program.name("pyre").version("2.0.0").description("Mac system monitoring CLI: interactive live dashboard, stats, graphs, export").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("--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");
2545
+ program.name("pyre").version("2.0.0").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("--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)", "0.0.0.0").option("--p2p-port <port>", "P2P port number", "9876").option("--p2p-password <password>", "Password for P2P authentication");
1632
2546
  program.parse(process.argv);
1633
2547
  var opts = program.opts();
1634
2548
  function isExportMode() {
@@ -1638,8 +2552,11 @@ function stripAnsi(s) {
1638
2552
  return s.replace(/\x1b\[[0-9;]*m/g, "");
1639
2553
  }
1640
2554
  async function main() {
1641
- const requestedLive = program.args[0] === "live" || !isExportMode() && !opts.once;
1642
- if (requestedLive) {
2555
+ const cmd = program.args[0];
2556
+ if (cmd === "live" || !isExportMode() && !opts.once && cmd !== "p2p") {
2557
+ if (cmd === "live") {
2558
+ program.args.shift();
2559
+ }
1643
2560
  await showSplash();
1644
2561
  const interval = parseFloat(opts.interval) || 2;
1645
2562
  await startLive({
@@ -1650,21 +2567,114 @@ async function main() {
1650
2567
  });
1651
2568
  return;
1652
2569
  }
1653
- const data = await collectAll({ detailed: opts.detailed });
2570
+ if (cmd === "p2p") {
2571
+ const subcommand = program.args[1];
2572
+ if (subcommand === "server") {
2573
+ await runP2PServer();
2574
+ return;
2575
+ } else if (subcommand === "connect") {
2576
+ await runP2PConnect();
2577
+ return;
2578
+ } else {
2579
+ console.log(chalk8.red("Usage: pyre p2p <server|connect> [options]"));
2580
+ process.exit(1);
2581
+ }
2582
+ }
2583
+ const requestedLimit = parseInt(opts.limit, 10);
2584
+ const processLimit = Number.isFinite(requestedLimit) && requestedLimit > 0 ? requestedLimit : void 0;
2585
+ const data = await collectAll({ detailed: opts.detailed, processLimit });
1654
2586
  let output;
1655
2587
  if (opts.json) output = formatJson(data);
1656
2588
  else if (opts.csv) output = formatCsv(data);
1657
2589
  else if (opts.tsv) output = formatTsv(data);
1658
- else output = formatTable(data, { width: process.stdout.columns || 80, sortBy: opts.sort, treeView: opts.tree });
2590
+ else output = formatTable(data, { width: process.stdout.columns || 80, sortBy: opts.sort, treeView: opts.tree, visible: { packets: opts.packets ? true : void 0 } });
1659
2591
  console.log(output);
1660
2592
  if (opts.out) {
1661
2593
  fs2.writeFileSync(opts.out, stripAnsi(output));
1662
- console.log(chalk7.dim(`
2594
+ console.log(chalk8.dim(`
1663
2595
  Saved to ${opts.out}`));
1664
2596
  }
1665
2597
  }
1666
2598
  main().catch((err) => {
1667
- console.error(chalk7.red(`Error: ${err.message}`));
1668
- stopLive();
2599
+ console.error(chalk8.red(`Error: ${err.message}`));
1669
2600
  process.exit(1);
1670
2601
  });
2602
+ async function runP2PServer() {
2603
+ const host = opts.p2pHost || "0.0.0.0";
2604
+ const port = parseInt(opts.p2pPort, 10) || 9876;
2605
+ const password = opts.p2pPassword || "";
2606
+ const interval = parseFloat(opts.interval) || 2;
2607
+ const detailed = !!opts.detailed;
2608
+ if (!password) {
2609
+ console.log(chalk8.red("Error: a password is required for P2P server mode. Use --p2p-password <password>"));
2610
+ process.exit(1);
2611
+ }
2612
+ const server = new P2PServer({ host, port, password, intervalMs: interval * 1e3, detailed });
2613
+ console.log(chalk8.bold(`
2614
+ pyre P2P Server`));
2615
+ console.log(chalk8.dim(` Host: ${host}`));
2616
+ console.log(chalk8.dim(` Port: ${port}`));
2617
+ console.log(chalk8.dim(` Password: ${password}`));
2618
+ console.log(chalk8.dim(` Interval: ${interval}s`));
2619
+ console.log(chalk8.dim(` Detailed: ${detailed ? "on" : "off"}`));
2620
+ console.log(chalk8.dim(` Waiting for peers...
2621
+ `));
2622
+ await server.start();
2623
+ const rl = readline2.createInterface({ input: process.stdin });
2624
+ rl.on("line", (line) => {
2625
+ const trimmed = line.trim().toLowerCase();
2626
+ if (trimmed === "q" || trimmed === "quit" || trimmed === "exit") {
2627
+ server.stop();
2628
+ rl.close();
2629
+ process.exit(0);
2630
+ } else if (trimmed === "status") {
2631
+ const info = server.info;
2632
+ console.log(chalk8.dim(` Peers connected: ${info.peersConnected}`));
2633
+ }
2634
+ });
2635
+ process.once("SIGINT", () => {
2636
+ server.stop();
2637
+ rl.close();
2638
+ process.exit(0);
2639
+ });
2640
+ }
2641
+ async function runP2PConnect() {
2642
+ const host = opts.p2pHost || "127.0.0.1";
2643
+ const port = parseInt(opts.p2pPort, 10) || 9876;
2644
+ const password = opts.p2pPassword || "";
2645
+ if (!password) {
2646
+ console.log(chalk8.red("Error: a password is required for P2P connect mode. Use --p2p-password <password>"));
2647
+ process.exit(1);
2648
+ }
2649
+ const client = new P2PClient({ host, port, password });
2650
+ client.setOnStatus((msg) => {
2651
+ process.stdout.write(`\r${chalk8.dim(msg)}${" ".repeat(40)}\r`);
2652
+ });
2653
+ client.setOnData((data) => {
2654
+ process.stdout.write("\x1B[2J\x1B[H");
2655
+ console.log(chalk8.bold(` pyre P2P Client \u2014 ${data.header.hostname}`));
2656
+ console.log(chalk8.dim(` ${data.timestamp}`));
2657
+ console.log();
2658
+ console.log(formatTable(data, {
2659
+ width: process.stdout.columns || 80,
2660
+ sortBy: "cpu",
2661
+ treeView: false,
2662
+ visible: { packets: true }
2663
+ }));
2664
+ });
2665
+ console.log(chalk8.bold(`
2666
+ pyre P2P Client`));
2667
+ console.log(chalk8.dim(` Connecting to ${host}:${port}...`));
2668
+ console.log(chalk8.dim(` Press Ctrl+C to disconnect
2669
+ `));
2670
+ try {
2671
+ await client.connect();
2672
+ } catch (err) {
2673
+ console.log(chalk8.red(`Connection failed: ${err.message}`));
2674
+ process.exit(1);
2675
+ }
2676
+ process.once("SIGINT", () => {
2677
+ client.disconnect();
2678
+ process.exit(0);
2679
+ });
2680
+ }