pyre-cli 1.4.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +78 -11
- package/dist/index.js +1325 -33
- package/package.json +2 -2
- package/src/formatters/index.ts +1 -1
- package/src/formatters/output.ts +47 -19
- package/src/formatters/render.ts +207 -16
- package/src/formatters/types.ts +4 -0
- package/src/history.ts +85 -47
- package/src/index.ts +200 -20
- package/src/live/export.ts +5 -2
- package/src/live/input.ts +296 -164
- package/src/live/render.ts +32 -28
- package/src/live/state.ts +41 -21
- package/src/monitors/collectors.ts +220 -17
- package/src/monitors/index.ts +2 -2
- package/src/monitors/types.ts +42 -1
- package/src/p2p/client.ts +271 -0
- package/src/p2p/index.ts +3 -0
- package/src/p2p/protocol.ts +112 -0
- package/src/p2p/server.ts +390 -0
- package/src/p2p/types.ts +97 -0
package/dist/index.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import
|
|
4
|
+
import fs4 from "fs";
|
|
5
|
+
import readline2 from "readline";
|
|
5
6
|
import { Command } from "commander";
|
|
6
|
-
import
|
|
7
|
+
import chalk9 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
|
|
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
|
|
98
|
-
|
|
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
|
|
460
|
+
async function collectProcesses(limit) {
|
|
402
461
|
try {
|
|
403
|
-
const
|
|
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 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
|
|
616
|
-
|
|
617
|
-
|
|
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
|
|
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,609 @@ async function showSplash() {
|
|
|
1626
2174
|
});
|
|
1627
2175
|
}
|
|
1628
2176
|
|
|
2177
|
+
// src/p2p/server.ts
|
|
2178
|
+
import net from "net";
|
|
2179
|
+
import tls from "tls";
|
|
2180
|
+
import fs2 from "fs";
|
|
2181
|
+
import crypto2 from "crypto";
|
|
2182
|
+
import chalk7 from "chalk";
|
|
2183
|
+
|
|
2184
|
+
// src/p2p/protocol.ts
|
|
2185
|
+
import crypto from "crypto";
|
|
2186
|
+
var HEADER_SIZE = 4;
|
|
2187
|
+
var MAX_MESSAGE_SIZE = 16 * 1024 * 1024;
|
|
2188
|
+
var HMAC_ALGO = "sha256";
|
|
2189
|
+
function buildMessage(kind, payload, hmac) {
|
|
2190
|
+
const envelope = { kind, payload };
|
|
2191
|
+
if (hmac) envelope.hmac = hmac;
|
|
2192
|
+
const body = Buffer.from(JSON.stringify(envelope), "utf-8");
|
|
2193
|
+
const header2 = Buffer.alloc(HEADER_SIZE);
|
|
2194
|
+
header2.writeUInt32BE(body.length, 0);
|
|
2195
|
+
return Buffer.concat([header2, body]);
|
|
2196
|
+
}
|
|
2197
|
+
function generateNonce() {
|
|
2198
|
+
return crypto.randomBytes(32).toString("hex");
|
|
2199
|
+
}
|
|
2200
|
+
function hashPassword(password, nonce) {
|
|
2201
|
+
return crypto.createHash("sha256").update(`${password}:${nonce}`).digest("hex");
|
|
2202
|
+
}
|
|
2203
|
+
function computeHMAC(body, key) {
|
|
2204
|
+
if (!key) return void 0;
|
|
2205
|
+
return crypto.createHmac(HMAC_ALGO, key).update(body).digest("hex");
|
|
2206
|
+
}
|
|
2207
|
+
function verifyHMAC(body, expectedHmac, key) {
|
|
2208
|
+
if (!key) return expectedHmac === void 0;
|
|
2209
|
+
if (!expectedHmac) return false;
|
|
2210
|
+
try {
|
|
2211
|
+
const parsed = JSON.parse(body);
|
|
2212
|
+
const payloadWithoutHmac = { ...parsed, hmac: void 0 };
|
|
2213
|
+
const canonical = JSON.stringify(payloadWithoutHmac);
|
|
2214
|
+
const computed = crypto.createHmac(HMAC_ALGO, key).update(canonical).digest("hex");
|
|
2215
|
+
return crypto.timingSafeEqual(Buffer.from(computed), Buffer.from(expectedHmac));
|
|
2216
|
+
} catch {
|
|
2217
|
+
return false;
|
|
2218
|
+
}
|
|
2219
|
+
}
|
|
2220
|
+
function encodeDisconnect(reason) {
|
|
2221
|
+
return buildMessage("disconnect", { reason });
|
|
2222
|
+
}
|
|
2223
|
+
function decodeMessage(buf) {
|
|
2224
|
+
const str = buf.toString("utf-8");
|
|
2225
|
+
const msg = JSON.parse(str);
|
|
2226
|
+
return msg;
|
|
2227
|
+
}
|
|
2228
|
+
function signMessage(kind, payload, key) {
|
|
2229
|
+
const envelope = { kind, payload };
|
|
2230
|
+
const bodyJson = JSON.stringify(envelope);
|
|
2231
|
+
const hmac = computeHMAC(bodyJson, key);
|
|
2232
|
+
return buildMessage(kind, payload, hmac);
|
|
2233
|
+
}
|
|
2234
|
+
|
|
2235
|
+
// src/p2p/server.ts
|
|
2236
|
+
var HEADER_SIZE2 = 4;
|
|
2237
|
+
var DEFAULT_RATE_LIMIT_MAX = 5;
|
|
2238
|
+
var DEFAULT_RATE_LIMIT_WINDOW = 6e4;
|
|
2239
|
+
var HMAC_KEY_LENGTH = 32;
|
|
2240
|
+
function readMessageFromPeer(peer) {
|
|
2241
|
+
while (peer.readBuffer.length > 0) {
|
|
2242
|
+
if (peer.readExpectingHeader) {
|
|
2243
|
+
if (peer.readBuffer.length >= HEADER_SIZE2) {
|
|
2244
|
+
peer.readMessageLength = peer.readBuffer.readUInt32BE(0);
|
|
2245
|
+
peer.readBuffer = peer.readBuffer.subarray(HEADER_SIZE2);
|
|
2246
|
+
peer.readExpectingHeader = false;
|
|
2247
|
+
} else {
|
|
2248
|
+
break;
|
|
2249
|
+
}
|
|
2250
|
+
}
|
|
2251
|
+
if (!peer.readExpectingHeader) {
|
|
2252
|
+
if (peer.readBuffer.length >= peer.readMessageLength) {
|
|
2253
|
+
const body = peer.readBuffer.subarray(0, peer.readMessageLength);
|
|
2254
|
+
peer.readBuffer = peer.readBuffer.subarray(peer.readMessageLength);
|
|
2255
|
+
peer.readExpectingHeader = true;
|
|
2256
|
+
return body;
|
|
2257
|
+
} else {
|
|
2258
|
+
break;
|
|
2259
|
+
}
|
|
2260
|
+
}
|
|
2261
|
+
}
|
|
2262
|
+
return null;
|
|
2263
|
+
}
|
|
2264
|
+
function isIPAllowed(ip, allowedIPs, deniedIPs) {
|
|
2265
|
+
for (const denied of deniedIPs) {
|
|
2266
|
+
if (ip === denied || ip.startsWith(denied)) {
|
|
2267
|
+
return false;
|
|
2268
|
+
}
|
|
2269
|
+
}
|
|
2270
|
+
if (allowedIPs.length === 0) {
|
|
2271
|
+
return true;
|
|
2272
|
+
}
|
|
2273
|
+
for (const allowed of allowedIPs) {
|
|
2274
|
+
if (ip === allowed || ip.startsWith(allowed)) {
|
|
2275
|
+
return true;
|
|
2276
|
+
}
|
|
2277
|
+
}
|
|
2278
|
+
return false;
|
|
2279
|
+
}
|
|
2280
|
+
function deriveHMACKey(password) {
|
|
2281
|
+
return crypto2.createHash("sha256").update(password).digest("hex").slice(0, HMAC_KEY_LENGTH);
|
|
2282
|
+
}
|
|
2283
|
+
var P2PServer = class {
|
|
2284
|
+
server = null;
|
|
2285
|
+
peers = /* @__PURE__ */ new Map();
|
|
2286
|
+
options;
|
|
2287
|
+
running = false;
|
|
2288
|
+
rateLimitMap = /* @__PURE__ */ new Map();
|
|
2289
|
+
auditStream = null;
|
|
2290
|
+
hmacKey;
|
|
2291
|
+
constructor(options) {
|
|
2292
|
+
this.options = options;
|
|
2293
|
+
this.hmacKey = deriveHMACKey(options.password);
|
|
2294
|
+
}
|
|
2295
|
+
get info() {
|
|
2296
|
+
return {
|
|
2297
|
+
host: this.options.host,
|
|
2298
|
+
port: this.options.port,
|
|
2299
|
+
peersConnected: this.peers.size
|
|
2300
|
+
};
|
|
2301
|
+
}
|
|
2302
|
+
async start() {
|
|
2303
|
+
if (this.options.auditLog) {
|
|
2304
|
+
try {
|
|
2305
|
+
fs2.mkdirSync(this.options.auditLog, { recursive: true });
|
|
2306
|
+
const logFile = `${this.options.auditLog}/p2p-audit.log`;
|
|
2307
|
+
this.auditStream = fs2.createWriteStream(logFile, { flags: "a" });
|
|
2308
|
+
} catch {
|
|
2309
|
+
console.log(chalk7.yellow("Warning: could not open audit log"));
|
|
2310
|
+
}
|
|
2311
|
+
}
|
|
2312
|
+
return new Promise((resolve, reject) => {
|
|
2313
|
+
const useTLS = !!(this.options.tlsCert && this.options.tlsKey);
|
|
2314
|
+
if (useTLS) {
|
|
2315
|
+
const tlsOptions = {
|
|
2316
|
+
key: fs2.readFileSync(this.options.tlsKey),
|
|
2317
|
+
cert: fs2.readFileSync(this.options.tlsCert)
|
|
2318
|
+
};
|
|
2319
|
+
if (this.options.tlsCA) {
|
|
2320
|
+
tlsOptions.ca = fs2.readFileSync(this.options.tlsCA);
|
|
2321
|
+
}
|
|
2322
|
+
this.server = tls.createServer(tlsOptions, (socket) => this.onConnection(socket));
|
|
2323
|
+
} else {
|
|
2324
|
+
this.server = net.createServer((socket) => this.onConnection(socket));
|
|
2325
|
+
}
|
|
2326
|
+
this.server.on("error", (err) => reject(err));
|
|
2327
|
+
this.server.listen(this.options.port, this.options.host, () => {
|
|
2328
|
+
this.running = true;
|
|
2329
|
+
const addr = this.server?.address();
|
|
2330
|
+
const bound = addr ? typeof addr === "object" ? `${addr.address}:${addr.port}` : String(addr) : "unknown";
|
|
2331
|
+
const protocol = useTLS ? "TLS" : "TCP";
|
|
2332
|
+
console.log(chalk7.green(`P2P server listening on ${bound} (${protocol})`));
|
|
2333
|
+
console.log(chalk7.dim(` Password: ${this.options.password}`));
|
|
2334
|
+
console.log(chalk7.dim(` Interval: ${this.options.intervalMs}ms`));
|
|
2335
|
+
const allowedIPs = this.options.allowedIPs ?? [];
|
|
2336
|
+
const deniedIPs = this.options.deniedIPs ?? [];
|
|
2337
|
+
if (allowedIPs.length > 0) console.log(chalk7.dim(` Allowed IPs: ${allowedIPs.join(", ")}`));
|
|
2338
|
+
if (deniedIPs.length > 0) console.log(chalk7.dim(` Denied IPs: ${deniedIPs.join(", ")}`));
|
|
2339
|
+
resolve();
|
|
2340
|
+
});
|
|
2341
|
+
});
|
|
2342
|
+
}
|
|
2343
|
+
stop() {
|
|
2344
|
+
this.running = false;
|
|
2345
|
+
for (const peer of this.peers.values()) {
|
|
2346
|
+
this.disconnectPeer(peer, "Server shutting down");
|
|
2347
|
+
}
|
|
2348
|
+
this.peers.clear();
|
|
2349
|
+
this.rateLimitMap.clear();
|
|
2350
|
+
if (this.server) {
|
|
2351
|
+
this.server.close();
|
|
2352
|
+
this.server = null;
|
|
2353
|
+
}
|
|
2354
|
+
if (this.auditStream) {
|
|
2355
|
+
this.auditStream.end();
|
|
2356
|
+
this.auditStream = null;
|
|
2357
|
+
}
|
|
2358
|
+
}
|
|
2359
|
+
audit(ip, event, detail) {
|
|
2360
|
+
if (!this.auditStream) return;
|
|
2361
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
2362
|
+
this.auditStream.write(`${ts} ${ip} ${event} ${detail}
|
|
2363
|
+
`);
|
|
2364
|
+
}
|
|
2365
|
+
getPeerIP(socket) {
|
|
2366
|
+
const addr = socket.remoteAddress;
|
|
2367
|
+
if (!addr) return "unknown";
|
|
2368
|
+
if (addr.startsWith("::ffff:")) {
|
|
2369
|
+
return addr.slice(7);
|
|
2370
|
+
}
|
|
2371
|
+
return addr;
|
|
2372
|
+
}
|
|
2373
|
+
checkRateLimit(ip) {
|
|
2374
|
+
const maxAttempts = this.options.rateLimitMaxAttempts ?? DEFAULT_RATE_LIMIT_MAX;
|
|
2375
|
+
const windowMs = this.options.rateLimitWindowMs ?? DEFAULT_RATE_LIMIT_WINDOW;
|
|
2376
|
+
const now = Date.now();
|
|
2377
|
+
const entry = this.rateLimitMap.get(ip);
|
|
2378
|
+
if (!entry) {
|
|
2379
|
+
this.rateLimitMap.set(ip, { attempts: 1, firstAttempt: now });
|
|
2380
|
+
return true;
|
|
2381
|
+
}
|
|
2382
|
+
if (now - entry.firstAttempt > windowMs) {
|
|
2383
|
+
this.rateLimitMap.set(ip, { attempts: 1, firstAttempt: now });
|
|
2384
|
+
return true;
|
|
2385
|
+
}
|
|
2386
|
+
entry.attempts++;
|
|
2387
|
+
if (entry.attempts > maxAttempts) {
|
|
2388
|
+
return false;
|
|
2389
|
+
}
|
|
2390
|
+
return true;
|
|
2391
|
+
}
|
|
2392
|
+
cleanupRateLimit(ip) {
|
|
2393
|
+
this.rateLimitMap.delete(ip);
|
|
2394
|
+
}
|
|
2395
|
+
onConnection(socket) {
|
|
2396
|
+
const ip = this.getPeerIP(socket);
|
|
2397
|
+
const allowedIPs = this.options.allowedIPs ?? [];
|
|
2398
|
+
const deniedIPs = this.options.deniedIPs ?? [];
|
|
2399
|
+
if (!isIPAllowed(ip, allowedIPs, deniedIPs)) {
|
|
2400
|
+
this.audit(ip, "BLOCKED", "IP not allowed");
|
|
2401
|
+
console.log(chalk7.yellow(`Blocked IP: ${ip}`));
|
|
2402
|
+
socket.destroy();
|
|
2403
|
+
return;
|
|
2404
|
+
}
|
|
2405
|
+
if (!this.checkRateLimit(ip)) {
|
|
2406
|
+
this.audit(ip, "RATE_LIMITED", "Too many auth attempts");
|
|
2407
|
+
console.log(chalk7.yellow(`Rate limit exceeded for IP: ${ip}`));
|
|
2408
|
+
socket.destroy();
|
|
2409
|
+
return;
|
|
2410
|
+
}
|
|
2411
|
+
const peer = {
|
|
2412
|
+
socket,
|
|
2413
|
+
authenticated: false,
|
|
2414
|
+
intervalHandle: null,
|
|
2415
|
+
pingHandle: null,
|
|
2416
|
+
readBuffer: Buffer.alloc(0),
|
|
2417
|
+
readExpectingHeader: true,
|
|
2418
|
+
readMessageLength: 0,
|
|
2419
|
+
ip,
|
|
2420
|
+
pendingNonce: null
|
|
2421
|
+
};
|
|
2422
|
+
this.peers.set(socket, peer);
|
|
2423
|
+
const remoteAddr = socket.remoteAddress ? `${socket.remoteAddress}:${socket.remotePort}` : "unknown";
|
|
2424
|
+
console.log(chalk7.cyan(`Peer connected: ${remoteAddr}`));
|
|
2425
|
+
this.audit(ip, "CONNECT", remoteAddr);
|
|
2426
|
+
socket.on("data", (data) => this.onData(peer, data));
|
|
2427
|
+
socket.on("close", () => this.onPeerClose(peer));
|
|
2428
|
+
socket.on("error", (err) => this.onPeerError(peer, err));
|
|
2429
|
+
socket.setTimeout(3e4);
|
|
2430
|
+
socket.on("timeout", () => {
|
|
2431
|
+
if (!peer.authenticated) {
|
|
2432
|
+
this.disconnectPeer(peer, "Authentication timeout");
|
|
2433
|
+
}
|
|
2434
|
+
});
|
|
2435
|
+
const nonce = generateNonce();
|
|
2436
|
+
this.sendSigned(peer, signMessage("challenge", { nonce }, this.hmacKey));
|
|
2437
|
+
peer.pendingNonce = nonce;
|
|
2438
|
+
}
|
|
2439
|
+
onData(peer, data) {
|
|
2440
|
+
peer.readBuffer = Buffer.concat([peer.readBuffer, data]);
|
|
2441
|
+
if (!peer.authenticated) {
|
|
2442
|
+
const body = readMessageFromPeer(peer);
|
|
2443
|
+
if (body) {
|
|
2444
|
+
this.handleAuthMessage(peer, body);
|
|
2445
|
+
}
|
|
2446
|
+
return;
|
|
2447
|
+
}
|
|
2448
|
+
}
|
|
2449
|
+
handleAuthMessage(peer, body) {
|
|
2450
|
+
try {
|
|
2451
|
+
const raw = body.toString("utf-8");
|
|
2452
|
+
const msg = decodeMessage(body);
|
|
2453
|
+
if (msg.kind !== "auth") {
|
|
2454
|
+
peer.socket.write(signMessage("auth-fail", { ok: false, reason: "Expected auth message first" }, this.hmacKey));
|
|
2455
|
+
this.disconnectPeer(peer, "Expected auth message");
|
|
2456
|
+
return;
|
|
2457
|
+
}
|
|
2458
|
+
const authPayload = msg.payload;
|
|
2459
|
+
if (!authPayload.passwordHash || !authPayload.nonce) {
|
|
2460
|
+
peer.socket.write(signMessage("auth-fail", { ok: false, reason: "Invalid auth payload" }, this.hmacKey));
|
|
2461
|
+
this.disconnectPeer(peer, "Invalid auth payload");
|
|
2462
|
+
return;
|
|
2463
|
+
}
|
|
2464
|
+
if (peer.pendingNonce && authPayload.nonce !== peer.pendingNonce) {
|
|
2465
|
+
this.audit(peer.ip, "AUTH_FAIL", "Nonce mismatch");
|
|
2466
|
+
peer.socket.write(signMessage("auth-fail", { ok: false, reason: "Invalid nonce" }, this.hmacKey));
|
|
2467
|
+
this.disconnectPeer(peer, "Invalid nonce");
|
|
2468
|
+
return;
|
|
2469
|
+
}
|
|
2470
|
+
const expectedHash = hashPassword(this.options.password, authPayload.nonce);
|
|
2471
|
+
if (authPayload.passwordHash !== expectedHash) {
|
|
2472
|
+
this.audit(peer.ip, "AUTH_FAIL", "Invalid password hash");
|
|
2473
|
+
peer.socket.write(signMessage("auth-fail", { ok: false, reason: "Invalid password" }, this.hmacKey));
|
|
2474
|
+
this.disconnectPeer(peer, "Authentication failed");
|
|
2475
|
+
return;
|
|
2476
|
+
}
|
|
2477
|
+
peer.authenticated = true;
|
|
2478
|
+
this.cleanupRateLimit(peer.ip);
|
|
2479
|
+
peer.pendingNonce = null;
|
|
2480
|
+
peer.socket.write(signMessage("auth-ok", { ok: true }, this.hmacKey));
|
|
2481
|
+
console.log(chalk7.green(`Peer authenticated: ${peer.socket.remoteAddress}`));
|
|
2482
|
+
this.audit(peer.ip, "AUTH_OK", "Challenge-response successful");
|
|
2483
|
+
this.startDataStream(peer);
|
|
2484
|
+
this.startPing(peer);
|
|
2485
|
+
} catch {
|
|
2486
|
+
peer.socket.write(signMessage("auth-fail", { ok: false, reason: "Invalid message format" }, this.hmacKey));
|
|
2487
|
+
this.disconnectPeer(peer, "Invalid message format");
|
|
2488
|
+
}
|
|
2489
|
+
}
|
|
2490
|
+
sendSigned(peer, message) {
|
|
2491
|
+
if (!peer.socket.destroyed) {
|
|
2492
|
+
peer.socket.write(message);
|
|
2493
|
+
}
|
|
2494
|
+
}
|
|
2495
|
+
startDataStream(peer) {
|
|
2496
|
+
const send = async () => {
|
|
2497
|
+
if (!peer.authenticated || peer.socket.destroyed) return;
|
|
2498
|
+
try {
|
|
2499
|
+
const data = await collectAll({ detailed: this.options.detailed });
|
|
2500
|
+
this.sendSigned(peer, signMessage("data", { stats: data }, this.hmacKey));
|
|
2501
|
+
} catch {
|
|
2502
|
+
}
|
|
2503
|
+
};
|
|
2504
|
+
send();
|
|
2505
|
+
peer.intervalHandle = setInterval(send, this.options.intervalMs);
|
|
2506
|
+
}
|
|
2507
|
+
startPing(peer) {
|
|
2508
|
+
peer.pingHandle = setInterval(() => {
|
|
2509
|
+
if (peer.socket.destroyed) return;
|
|
2510
|
+
this.sendSigned(peer, signMessage("ping", { ts: Date.now() }, this.hmacKey));
|
|
2511
|
+
}, 5e3);
|
|
2512
|
+
}
|
|
2513
|
+
onPeerClose(peer) {
|
|
2514
|
+
this.cleanupPeer(peer);
|
|
2515
|
+
console.log(chalk7.yellow(`Peer disconnected: ${peer.socket.remoteAddress}`));
|
|
2516
|
+
this.audit(peer.ip, "DISCONNECT", "Peer closed connection");
|
|
2517
|
+
}
|
|
2518
|
+
onPeerError(peer, err) {
|
|
2519
|
+
const code = err.code;
|
|
2520
|
+
if (code === "ECONNRESET") {
|
|
2521
|
+
console.log(chalk7.yellow(`Peer connection reset: ${peer.socket.remoteAddress}`));
|
|
2522
|
+
this.audit(peer.ip, "ERROR", "Connection reset");
|
|
2523
|
+
} else {
|
|
2524
|
+
console.log(chalk7.red(`Peer error: ${err.message}`));
|
|
2525
|
+
this.audit(peer.ip, "ERROR", err.message);
|
|
2526
|
+
}
|
|
2527
|
+
this.cleanupPeer(peer);
|
|
2528
|
+
}
|
|
2529
|
+
disconnectPeer(peer, reason) {
|
|
2530
|
+
try {
|
|
2531
|
+
this.sendSigned(peer, signMessage("disconnect", { reason }, this.hmacKey));
|
|
2532
|
+
} catch {
|
|
2533
|
+
}
|
|
2534
|
+
peer.socket.destroy();
|
|
2535
|
+
}
|
|
2536
|
+
cleanupPeer(peer) {
|
|
2537
|
+
if (peer.intervalHandle) {
|
|
2538
|
+
clearInterval(peer.intervalHandle);
|
|
2539
|
+
peer.intervalHandle = null;
|
|
2540
|
+
}
|
|
2541
|
+
if (peer.pingHandle) {
|
|
2542
|
+
clearInterval(peer.pingHandle);
|
|
2543
|
+
peer.pingHandle = null;
|
|
2544
|
+
}
|
|
2545
|
+
this.peers.delete(peer.socket);
|
|
2546
|
+
}
|
|
2547
|
+
};
|
|
2548
|
+
|
|
2549
|
+
// src/p2p/client.ts
|
|
2550
|
+
import net2 from "net";
|
|
2551
|
+
import tls2 from "tls";
|
|
2552
|
+
import fs3 from "fs";
|
|
2553
|
+
import crypto3 from "crypto";
|
|
2554
|
+
import chalk8 from "chalk";
|
|
2555
|
+
var P2PClient = class {
|
|
2556
|
+
socket = null;
|
|
2557
|
+
options;
|
|
2558
|
+
running = false;
|
|
2559
|
+
readState = { buffer: Buffer.alloc(0), expectingHeader: true, messageLength: 0 };
|
|
2560
|
+
onDataCallback = null;
|
|
2561
|
+
onStatusCallback = null;
|
|
2562
|
+
reconnectTimer = null;
|
|
2563
|
+
pingHandle = null;
|
|
2564
|
+
pendingNonce = null;
|
|
2565
|
+
auditStream = null;
|
|
2566
|
+
hmacKey;
|
|
2567
|
+
constructor(options) {
|
|
2568
|
+
this.options = options;
|
|
2569
|
+
this.hmacKey = deriveHMACKey2(options.password);
|
|
2570
|
+
}
|
|
2571
|
+
setOnData(cb) {
|
|
2572
|
+
this.onDataCallback = cb;
|
|
2573
|
+
}
|
|
2574
|
+
setOnStatus(cb) {
|
|
2575
|
+
this.onStatusCallback = cb;
|
|
2576
|
+
}
|
|
2577
|
+
async connect() {
|
|
2578
|
+
if (this.options.auditLog) {
|
|
2579
|
+
try {
|
|
2580
|
+
fs3.mkdirSync(this.options.auditLog, { recursive: true });
|
|
2581
|
+
const logFile = `${this.options.auditLog}/p2p-client-audit.log`;
|
|
2582
|
+
this.auditStream = fs3.createWriteStream(logFile, { flags: "a" });
|
|
2583
|
+
} catch {
|
|
2584
|
+
console.log(chalk8.yellow("Warning: could not open audit log"));
|
|
2585
|
+
}
|
|
2586
|
+
}
|
|
2587
|
+
return new Promise((resolve, reject) => {
|
|
2588
|
+
const useTLS = !!this.options.tls;
|
|
2589
|
+
if (useTLS) {
|
|
2590
|
+
const tlsOptions = {
|
|
2591
|
+
host: this.options.host,
|
|
2592
|
+
port: this.options.port,
|
|
2593
|
+
rejectUnauthorized: !this.options.tlsInsecure
|
|
2594
|
+
};
|
|
2595
|
+
if (this.options.tlsCA) {
|
|
2596
|
+
tlsOptions.ca = fs3.readFileSync(this.options.tlsCA);
|
|
2597
|
+
}
|
|
2598
|
+
this.socket = tls2.connect(tlsOptions, () => {
|
|
2599
|
+
this.status("Connecting...");
|
|
2600
|
+
});
|
|
2601
|
+
} else {
|
|
2602
|
+
this.socket = new net2.Socket();
|
|
2603
|
+
this.socket.on("data", (data) => this.onData(data));
|
|
2604
|
+
this.socket.on("close", () => this.onClose());
|
|
2605
|
+
this.socket.on("error", (err) => this.onError(err));
|
|
2606
|
+
this.socket.on("timeout", () => this.onTimeout());
|
|
2607
|
+
this.socket.setTimeout(1e4);
|
|
2608
|
+
this.socket.connect(this.options.port, this.options.host, () => {
|
|
2609
|
+
this.status("Connecting...");
|
|
2610
|
+
});
|
|
2611
|
+
this.socket.once("error", (err) => {
|
|
2612
|
+
reject(err);
|
|
2613
|
+
});
|
|
2614
|
+
}
|
|
2615
|
+
this.socket.on("data", (data) => this.onData(data));
|
|
2616
|
+
this.socket.on("close", () => this.onClose());
|
|
2617
|
+
this.socket.on("error", (err) => this.onError(err));
|
|
2618
|
+
this.socket.on("timeout", () => this.onTimeout());
|
|
2619
|
+
});
|
|
2620
|
+
}
|
|
2621
|
+
disconnect() {
|
|
2622
|
+
this.running = false;
|
|
2623
|
+
if (this.reconnectTimer) {
|
|
2624
|
+
clearTimeout(this.reconnectTimer);
|
|
2625
|
+
this.reconnectTimer = null;
|
|
2626
|
+
}
|
|
2627
|
+
if (this.pingHandle) {
|
|
2628
|
+
clearInterval(this.pingHandle);
|
|
2629
|
+
this.pingHandle = null;
|
|
2630
|
+
}
|
|
2631
|
+
if (this.socket) {
|
|
2632
|
+
try {
|
|
2633
|
+
this.sendSigned(encodeDisconnect("Client closing"));
|
|
2634
|
+
} catch {
|
|
2635
|
+
}
|
|
2636
|
+
this.socket.destroy();
|
|
2637
|
+
this.socket = null;
|
|
2638
|
+
}
|
|
2639
|
+
if (this.auditStream) {
|
|
2640
|
+
this.auditStream.end();
|
|
2641
|
+
this.auditStream = null;
|
|
2642
|
+
}
|
|
2643
|
+
this.status("Disconnected");
|
|
2644
|
+
}
|
|
2645
|
+
onData(data) {
|
|
2646
|
+
this.readState.buffer = Buffer.concat([this.readState.buffer, data]);
|
|
2647
|
+
while (this.readState.buffer.length > 0) {
|
|
2648
|
+
if (this.readState.expectingHeader) {
|
|
2649
|
+
if (this.readState.buffer.length >= 4) {
|
|
2650
|
+
this.readState.messageLength = this.readState.buffer.readUInt32BE(0);
|
|
2651
|
+
this.readState.buffer = this.readState.buffer.subarray(4);
|
|
2652
|
+
this.readState.expectingHeader = false;
|
|
2653
|
+
} else {
|
|
2654
|
+
break;
|
|
2655
|
+
}
|
|
2656
|
+
}
|
|
2657
|
+
if (!this.readState.expectingHeader) {
|
|
2658
|
+
if (this.readState.buffer.length >= this.readState.messageLength) {
|
|
2659
|
+
const body = this.readState.buffer.subarray(0, this.readState.messageLength);
|
|
2660
|
+
this.readState.buffer = this.readState.buffer.subarray(this.readState.messageLength);
|
|
2661
|
+
this.readState.expectingHeader = true;
|
|
2662
|
+
this.handleMessage(body);
|
|
2663
|
+
} else {
|
|
2664
|
+
break;
|
|
2665
|
+
}
|
|
2666
|
+
}
|
|
2667
|
+
}
|
|
2668
|
+
}
|
|
2669
|
+
handleMessage(body) {
|
|
2670
|
+
try {
|
|
2671
|
+
const raw = body.toString("utf-8");
|
|
2672
|
+
const msg = decodeMessage(body);
|
|
2673
|
+
if (!verifyHMAC(raw, msg.hmac ?? "", this.hmacKey)) {
|
|
2674
|
+
this.status("Message integrity check failed");
|
|
2675
|
+
return;
|
|
2676
|
+
}
|
|
2677
|
+
switch (msg.kind) {
|
|
2678
|
+
case "challenge":
|
|
2679
|
+
const challengePayload = msg.payload;
|
|
2680
|
+
if (challengePayload.nonce) {
|
|
2681
|
+
this.pendingNonce = challengePayload.nonce;
|
|
2682
|
+
const passwordHash = hashPassword(this.options.password, challengePayload.nonce);
|
|
2683
|
+
this.sendSigned(signMessage("auth", { passwordHash, nonce: challengePayload.nonce }, this.hmacKey));
|
|
2684
|
+
this.status("Authenticating...");
|
|
2685
|
+
}
|
|
2686
|
+
break;
|
|
2687
|
+
case "auth-ok":
|
|
2688
|
+
this.status("Authenticated \u2014 receiving live data");
|
|
2689
|
+
this.startPing();
|
|
2690
|
+
break;
|
|
2691
|
+
case "auth-fail":
|
|
2692
|
+
const failPayload = msg.payload;
|
|
2693
|
+
this.status(`Authentication failed: ${failPayload.reason || "unknown"}`);
|
|
2694
|
+
this.disconnect();
|
|
2695
|
+
break;
|
|
2696
|
+
case "data":
|
|
2697
|
+
if (this.onDataCallback) {
|
|
2698
|
+
const dataPayload = msg.payload;
|
|
2699
|
+
if (dataPayload.stats) {
|
|
2700
|
+
this.onDataCallback(dataPayload.stats);
|
|
2701
|
+
}
|
|
2702
|
+
}
|
|
2703
|
+
break;
|
|
2704
|
+
case "ping":
|
|
2705
|
+
if (this.socket && !this.socket.destroyed) {
|
|
2706
|
+
const ts = msg.payload.ts;
|
|
2707
|
+
this.sendSigned(signMessage("pong", { ts }, this.hmacKey));
|
|
2708
|
+
}
|
|
2709
|
+
break;
|
|
2710
|
+
case "pong":
|
|
2711
|
+
break;
|
|
2712
|
+
case "disconnect":
|
|
2713
|
+
const disconnectPayload = msg.payload;
|
|
2714
|
+
this.status(`Server disconnected: ${disconnectPayload.reason || "unknown"}`);
|
|
2715
|
+
this.disconnect();
|
|
2716
|
+
break;
|
|
2717
|
+
default:
|
|
2718
|
+
break;
|
|
2719
|
+
}
|
|
2720
|
+
} catch {
|
|
2721
|
+
}
|
|
2722
|
+
}
|
|
2723
|
+
sendSigned(message) {
|
|
2724
|
+
if (this.socket && !this.socket.destroyed) {
|
|
2725
|
+
this.socket.write(message);
|
|
2726
|
+
}
|
|
2727
|
+
}
|
|
2728
|
+
startPing() {
|
|
2729
|
+
this.pingHandle = setInterval(() => {
|
|
2730
|
+
if (this.socket && !this.socket.destroyed) {
|
|
2731
|
+
this.sendSigned(signMessage("ping", { ts: Date.now() }, this.hmacKey));
|
|
2732
|
+
}
|
|
2733
|
+
}, 15e3);
|
|
2734
|
+
}
|
|
2735
|
+
onClose() {
|
|
2736
|
+
if (this.running) {
|
|
2737
|
+
this.status("Connection closed \u2014 reconnecting in 3s");
|
|
2738
|
+
this.reconnectTimer = setTimeout(() => {
|
|
2739
|
+
if (this.running) this.connect().catch(() => {
|
|
2740
|
+
});
|
|
2741
|
+
}, 3e3);
|
|
2742
|
+
}
|
|
2743
|
+
}
|
|
2744
|
+
onError(err) {
|
|
2745
|
+
const code = err.code;
|
|
2746
|
+
if (code === "ECONNREFUSED") {
|
|
2747
|
+
this.status("Connection refused \u2014 is the server running?");
|
|
2748
|
+
} else if (code === "ETIMEDOUT") {
|
|
2749
|
+
this.status("Connection timed out");
|
|
2750
|
+
} else if (code === "UNABLE_TO_VERIFY_LEAF_SIGNATURE") {
|
|
2751
|
+
this.status("TLS certificate verification failed \u2014 use --p2p-insecure to skip");
|
|
2752
|
+
} else {
|
|
2753
|
+
this.status(`Connection error: ${err.message}`);
|
|
2754
|
+
}
|
|
2755
|
+
}
|
|
2756
|
+
onTimeout() {
|
|
2757
|
+
this.status("Connection timed out");
|
|
2758
|
+
this.disconnect();
|
|
2759
|
+
}
|
|
2760
|
+
status(msg) {
|
|
2761
|
+
if (this.onStatusCallback) {
|
|
2762
|
+
this.onStatusCallback(msg);
|
|
2763
|
+
}
|
|
2764
|
+
}
|
|
2765
|
+
audit(event, detail) {
|
|
2766
|
+
if (!this.auditStream) return;
|
|
2767
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
2768
|
+
const ip = this.options.host;
|
|
2769
|
+
this.auditStream.write(`${ts} ${ip} ${event} ${detail}
|
|
2770
|
+
`);
|
|
2771
|
+
}
|
|
2772
|
+
};
|
|
2773
|
+
function deriveHMACKey2(password) {
|
|
2774
|
+
return crypto3.createHash("sha256").update(password).digest("hex").slice(0, 32);
|
|
2775
|
+
}
|
|
2776
|
+
|
|
1629
2777
|
// src/index.ts
|
|
1630
2778
|
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");
|
|
2779
|
+
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").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)");
|
|
1632
2780
|
program.parse(process.argv);
|
|
1633
2781
|
var opts = program.opts();
|
|
1634
2782
|
function isExportMode() {
|
|
@@ -1638,8 +2786,11 @@ function stripAnsi(s) {
|
|
|
1638
2786
|
return s.replace(/\x1b\[[0-9;]*m/g, "");
|
|
1639
2787
|
}
|
|
1640
2788
|
async function main() {
|
|
1641
|
-
const
|
|
1642
|
-
if (
|
|
2789
|
+
const cmd = program.args[0];
|
|
2790
|
+
if (cmd === "live" || !isExportMode() && !opts.once && cmd !== "p2p") {
|
|
2791
|
+
if (cmd === "live") {
|
|
2792
|
+
program.args.shift();
|
|
2793
|
+
}
|
|
1643
2794
|
await showSplash();
|
|
1644
2795
|
const interval = parseFloat(opts.interval) || 2;
|
|
1645
2796
|
await startLive({
|
|
@@ -1650,21 +2801,162 @@ async function main() {
|
|
|
1650
2801
|
});
|
|
1651
2802
|
return;
|
|
1652
2803
|
}
|
|
1653
|
-
|
|
2804
|
+
if (cmd === "p2p") {
|
|
2805
|
+
const subcommand = program.args[1];
|
|
2806
|
+
if (subcommand === "server") {
|
|
2807
|
+
await runP2PServer();
|
|
2808
|
+
return;
|
|
2809
|
+
} else if (subcommand === "connect") {
|
|
2810
|
+
await runP2PConnect();
|
|
2811
|
+
return;
|
|
2812
|
+
} else {
|
|
2813
|
+
console.log(chalk9.red("Usage: pyre p2p <server|connect> [options]"));
|
|
2814
|
+
process.exit(1);
|
|
2815
|
+
}
|
|
2816
|
+
}
|
|
2817
|
+
const requestedLimit = parseInt(opts.limit, 10);
|
|
2818
|
+
const processLimit = Number.isFinite(requestedLimit) && requestedLimit > 0 ? requestedLimit : void 0;
|
|
2819
|
+
const data = await collectAll({ detailed: opts.detailed, processLimit });
|
|
1654
2820
|
let output;
|
|
1655
2821
|
if (opts.json) output = formatJson(data);
|
|
1656
2822
|
else if (opts.csv) output = formatCsv(data);
|
|
1657
2823
|
else if (opts.tsv) output = formatTsv(data);
|
|
1658
|
-
else output = formatTable(data, { width: process.stdout.columns || 80, sortBy: opts.sort, treeView: opts.tree });
|
|
2824
|
+
else output = formatTable(data, { width: process.stdout.columns || 80, sortBy: opts.sort, treeView: opts.tree, visible: { packets: opts.packets ? true : void 0 } });
|
|
1659
2825
|
console.log(output);
|
|
1660
2826
|
if (opts.out) {
|
|
1661
|
-
|
|
1662
|
-
console.log(
|
|
2827
|
+
fs4.writeFileSync(opts.out, stripAnsi(output));
|
|
2828
|
+
console.log(chalk9.dim(`
|
|
1663
2829
|
Saved to ${opts.out}`));
|
|
1664
2830
|
}
|
|
1665
2831
|
}
|
|
1666
2832
|
main().catch((err) => {
|
|
1667
|
-
console.error(
|
|
1668
|
-
stopLive();
|
|
2833
|
+
console.error(chalk9.red(`Error: ${err.message}`));
|
|
1669
2834
|
process.exit(1);
|
|
1670
2835
|
});
|
|
2836
|
+
async function runP2PServer() {
|
|
2837
|
+
const host = opts.p2pHost || "0.0.0.0";
|
|
2838
|
+
const port = parseInt(opts.p2pPort, 10) || 9876;
|
|
2839
|
+
const password = opts.p2pPassword || "";
|
|
2840
|
+
const interval = parseFloat(opts.interval) || 2;
|
|
2841
|
+
const detailed = !!opts.detailed;
|
|
2842
|
+
const tlsCert = opts.p2pCert;
|
|
2843
|
+
const tlsKey = opts.p2pKey;
|
|
2844
|
+
const tlsCA = opts.p2pCA;
|
|
2845
|
+
const rateLimit = parseInt(opts.p2pRateLimit, 10) || 5;
|
|
2846
|
+
const allowList = opts.p2pAllow ? opts.p2pAllow.split(",").map((s) => s.trim()).filter(Boolean) : [];
|
|
2847
|
+
const denyList = opts.p2pDeny ? opts.p2pDeny.split(",").map((s) => s.trim()).filter(Boolean) : [];
|
|
2848
|
+
if (!password) {
|
|
2849
|
+
console.log(chalk9.red("Error: a password is required for P2P server mode. Use --p2p-password <password>"));
|
|
2850
|
+
process.exit(1);
|
|
2851
|
+
}
|
|
2852
|
+
if (tlsCert && !tlsKey) {
|
|
2853
|
+
console.log(chalk9.red("Error: --p2p-key is required when --p2p-cert is specified"));
|
|
2854
|
+
process.exit(1);
|
|
2855
|
+
}
|
|
2856
|
+
if (tlsKey && !tlsCert) {
|
|
2857
|
+
console.log(chalk9.red("Error: --p2p-cert is required when --p2p-key is specified"));
|
|
2858
|
+
process.exit(1);
|
|
2859
|
+
}
|
|
2860
|
+
const server = new P2PServer({
|
|
2861
|
+
host,
|
|
2862
|
+
port,
|
|
2863
|
+
password,
|
|
2864
|
+
intervalMs: interval * 1e3,
|
|
2865
|
+
detailed,
|
|
2866
|
+
tlsCert,
|
|
2867
|
+
tlsKey,
|
|
2868
|
+
tlsCA,
|
|
2869
|
+
rateLimitMaxAttempts: rateLimit,
|
|
2870
|
+
rateLimitWindowMs: 6e4,
|
|
2871
|
+
allowedIPs: allowList,
|
|
2872
|
+
deniedIPs: denyList,
|
|
2873
|
+
auditLog: opts.p2pAuditLog,
|
|
2874
|
+
hmacKey: opts.p2pHmacKey
|
|
2875
|
+
});
|
|
2876
|
+
const tlsEnabled = !!(tlsCert && tlsKey);
|
|
2877
|
+
console.log(chalk9.bold(`
|
|
2878
|
+
pyre P2P Server`));
|
|
2879
|
+
console.log(chalk9.dim(` Host: ${host}`));
|
|
2880
|
+
console.log(chalk9.dim(` Port: ${port}`));
|
|
2881
|
+
console.log(chalk9.dim(` Protocol: ${tlsEnabled ? "TLS" : "TCP"}`));
|
|
2882
|
+
console.log(chalk9.dim(` Password: ${password}`));
|
|
2883
|
+
console.log(chalk9.dim(` Interval: ${interval}s`));
|
|
2884
|
+
console.log(chalk9.dim(` Detailed: ${detailed ? "on" : "off"}`));
|
|
2885
|
+
console.log(chalk9.dim(` Rate limit: ${rateLimit} attempts/min`));
|
|
2886
|
+
if (allowList.length > 0) console.log(chalk9.dim(` Allowed IPs: ${allowList.join(", ")}`));
|
|
2887
|
+
if (denyList.length > 0) console.log(chalk9.dim(` Denied IPs: ${denyList.join(", ")}`));
|
|
2888
|
+
console.log(chalk9.dim(` Waiting for peers...
|
|
2889
|
+
`));
|
|
2890
|
+
await server.start();
|
|
2891
|
+
const rl = readline2.createInterface({ input: process.stdin });
|
|
2892
|
+
rl.on("line", (line) => {
|
|
2893
|
+
const trimmed = line.trim().toLowerCase();
|
|
2894
|
+
if (trimmed === "q" || trimmed === "quit" || trimmed === "exit") {
|
|
2895
|
+
server.stop();
|
|
2896
|
+
rl.close();
|
|
2897
|
+
process.exit(0);
|
|
2898
|
+
} else if (trimmed === "status") {
|
|
2899
|
+
const info = server.info;
|
|
2900
|
+
console.log(chalk9.dim(` Peers connected: ${info.peersConnected}`));
|
|
2901
|
+
}
|
|
2902
|
+
});
|
|
2903
|
+
process.once("SIGINT", () => {
|
|
2904
|
+
server.stop();
|
|
2905
|
+
rl.close();
|
|
2906
|
+
process.exit(0);
|
|
2907
|
+
});
|
|
2908
|
+
}
|
|
2909
|
+
async function runP2PConnect() {
|
|
2910
|
+
const host = opts.p2pHost || "127.0.0.1";
|
|
2911
|
+
const port = parseInt(opts.p2pPort, 10) || 9876;
|
|
2912
|
+
const password = opts.p2pPassword || "";
|
|
2913
|
+
const useTLS = !!opts.p2pTls;
|
|
2914
|
+
const tlsCA = opts.p2pCA;
|
|
2915
|
+
const tlsInsecure = !!opts.p2pInsecure;
|
|
2916
|
+
if (!password) {
|
|
2917
|
+
console.log(chalk9.red("Error: a password is required for P2P connect mode. Use --p2p-password <password>"));
|
|
2918
|
+
process.exit(1);
|
|
2919
|
+
}
|
|
2920
|
+
const client = new P2PClient({
|
|
2921
|
+
host,
|
|
2922
|
+
port,
|
|
2923
|
+
password,
|
|
2924
|
+
tls: useTLS,
|
|
2925
|
+
tlsCA,
|
|
2926
|
+
tlsInsecure,
|
|
2927
|
+
auditLog: opts.p2pAuditLog,
|
|
2928
|
+
hmacKey: opts.p2pHmacKey
|
|
2929
|
+
});
|
|
2930
|
+
client.setOnStatus((msg) => {
|
|
2931
|
+
process.stdout.write(`\r${chalk9.dim(msg)}${" ".repeat(40)}\r`);
|
|
2932
|
+
});
|
|
2933
|
+
client.setOnData((data) => {
|
|
2934
|
+
process.stdout.write("\x1B[2J\x1B[H");
|
|
2935
|
+
console.log(chalk9.bold(` pyre P2P Client \u2014 ${data.header.hostname}`));
|
|
2936
|
+
console.log(chalk9.dim(` ${data.timestamp}`));
|
|
2937
|
+
console.log();
|
|
2938
|
+
console.log(formatTable(data, {
|
|
2939
|
+
width: process.stdout.columns || 80,
|
|
2940
|
+
sortBy: "cpu",
|
|
2941
|
+
treeView: false,
|
|
2942
|
+
visible: { packets: true }
|
|
2943
|
+
}));
|
|
2944
|
+
});
|
|
2945
|
+
const protocolLabel = useTLS ? "TLS" : "TCP";
|
|
2946
|
+
console.log(chalk9.bold(`
|
|
2947
|
+
pyre P2P Client`));
|
|
2948
|
+
console.log(chalk9.dim(` Protocol: ${protocolLabel}`));
|
|
2949
|
+
console.log(chalk9.dim(` Connecting to ${host}:${port}...`));
|
|
2950
|
+
console.log(chalk9.dim(` Press Ctrl+C to disconnect
|
|
2951
|
+
`));
|
|
2952
|
+
try {
|
|
2953
|
+
await client.connect();
|
|
2954
|
+
} catch (err) {
|
|
2955
|
+
console.log(chalk9.red(`Connection failed: ${err.message}`));
|
|
2956
|
+
process.exit(1);
|
|
2957
|
+
}
|
|
2958
|
+
process.once("SIGINT", () => {
|
|
2959
|
+
client.disconnect();
|
|
2960
|
+
process.exit(0);
|
|
2961
|
+
});
|
|
2962
|
+
}
|