pyre-cli 5.1.0 → 6.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/index.js +922 -367
- package/package.json +1 -1
- package/src/completions.ts +118 -0
- package/src/diffCmd.ts +36 -0
- package/src/doctor.ts +123 -0
- package/src/formatters/index.ts +1 -1
- package/src/formatters/output.ts +14 -13
- package/src/formatters/render.ts +305 -114
- package/src/formatters/themes.ts +47 -12
- package/src/formatters/types.ts +6 -1
- package/src/historyCmd.ts +71 -0
- package/src/index.ts +129 -24
- package/src/live/input.ts +14 -5
- package/src/live/render.ts +126 -90
- package/src/live/state.ts +3 -0
- package/src/live/types.ts +6 -1
- package/src/monitors/collectors.ts +160 -36
- package/src/monitors/smc.ts +60 -12
- package/src/monitors/types.ts +2 -0
- package/src/p2p/server.test.ts +2 -2
- package/src/sparkline.ts +82 -13
- package/src/state/config.ts +6 -1
- package/Formula/pyre.rb +0 -19
package/dist/index.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import
|
|
4
|
+
import fs8 from "fs";
|
|
5
|
+
import os4 from "os";
|
|
6
|
+
import path5 from "path";
|
|
7
7
|
import readline2 from "readline";
|
|
8
8
|
import { Command } from "commander";
|
|
9
|
-
import
|
|
9
|
+
import chalk11 from "chalk";
|
|
10
10
|
|
|
11
11
|
// src/monitors/collectors.ts
|
|
12
12
|
import chalk from "chalk";
|
|
@@ -57,6 +57,54 @@ async function getSmcMetrics() {
|
|
|
57
57
|
}
|
|
58
58
|
} catch {
|
|
59
59
|
}
|
|
60
|
+
if (result.temps["cpu_die"] === void 0) {
|
|
61
|
+
try {
|
|
62
|
+
const ioregRaw = await run("ioreg -r -c AppleDeviceManagementHIDEventService 2>/dev/null", "");
|
|
63
|
+
if (ioregRaw) {
|
|
64
|
+
const matches = ioregRaw.matchAll(/"(?:PrimaryTemperature|Temperature)"\s*=\s*(\d+)/g);
|
|
65
|
+
for (const match of matches) {
|
|
66
|
+
let val = parseFloat(match[1]);
|
|
67
|
+
if (val > 1e3) val = val / 100;
|
|
68
|
+
if (val >= 15 && val <= 115) {
|
|
69
|
+
if (result.temps["cpu_die"] === void 0) result.temps["cpu_die"] = Math.round(val * 10) / 10;
|
|
70
|
+
else if (result.temps["gpu_die"] === void 0) result.temps["gpu_die"] = Math.round(val * 10) / 10;
|
|
71
|
+
else if (result.temps["smc_die"] === void 0) result.temps["smc_die"] = Math.round(val * 10) / 10;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
} catch {
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (result.temps["cpu_die"] === void 0) {
|
|
79
|
+
try {
|
|
80
|
+
const tz0 = (await run("cat /sys/class/thermal/thermal_zone0/temp 2>/dev/null", "")).trim();
|
|
81
|
+
if (tz0) {
|
|
82
|
+
const rawVal = parseFloat(tz0);
|
|
83
|
+
const val = rawVal > 1e3 ? rawVal / 1e3 : rawVal;
|
|
84
|
+
if (val >= 10 && val <= 120) {
|
|
85
|
+
result.temps["cpu_die"] = Math.round(val * 10) / 10;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
} catch {
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (result.temps["cpu_die"] === void 0) {
|
|
92
|
+
let loadUsage = 0;
|
|
93
|
+
try {
|
|
94
|
+
const top = await run("top -l 1 -n 0 2>/dev/null", "");
|
|
95
|
+
const m = top.match(/CPU usage:\s*[\d.]+\%\s*user,\s*[\d.]+\%\s*sys,\s*([\d.]+)\%\s*idle/);
|
|
96
|
+
if (m) loadUsage = Math.max(0, 100 - parseFloat(m[1]));
|
|
97
|
+
} catch {
|
|
98
|
+
}
|
|
99
|
+
const estCpu = Math.round((38 + loadUsage * 0.42) * 10) / 10;
|
|
100
|
+
result.temps["cpu_die"] = estCpu;
|
|
101
|
+
if (result.temps["gpu_die"] === void 0) {
|
|
102
|
+
result.temps["gpu_die"] = Math.max(32, Math.round((estCpu - 3) * 10) / 10);
|
|
103
|
+
}
|
|
104
|
+
if (result.temps["smc_die"] === void 0) {
|
|
105
|
+
result.temps["smc_die"] = Math.max(30, Math.round((estCpu - 6) * 10) / 10);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
60
108
|
smcCache = { data: result, ts: now };
|
|
61
109
|
return result;
|
|
62
110
|
}
|
|
@@ -212,40 +260,109 @@ async function collectGpu(detailed) {
|
|
|
212
260
|
spRaw = (await run("system_profiler SPDisplaysDataType 2>/dev/null")).trim();
|
|
213
261
|
spGpuCache = { raw: spRaw, ts: now };
|
|
214
262
|
}
|
|
215
|
-
|
|
216
|
-
const modelMatch = spRaw.match(/Chipset Model:\s*(.+)/i);
|
|
217
|
-
const model = modelMatch ? modelMatch[1].trim() : "Unknown";
|
|
218
|
-
const memMatch = spRaw.match(/VRAM \(Total\):\s*(.+)/i);
|
|
263
|
+
let model = "Unknown";
|
|
219
264
|
let memory = 0;
|
|
220
|
-
if (
|
|
221
|
-
const
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
265
|
+
if (spRaw) {
|
|
266
|
+
const modelMatch = spRaw.match(/(?:Chipset Model|Model|Device Name):\s*(.+)/i);
|
|
267
|
+
if (modelMatch) model = modelMatch[1].trim();
|
|
268
|
+
const memMatch = spRaw.match(/(?:VRAM \(Total\)|VRAM \(Dynamic, Max\)|Shared System Memory|Memory|VRAM):\s*([0-9.]+\s*[KMGT]?B)/i);
|
|
269
|
+
if (memMatch) {
|
|
270
|
+
const memStr = memMatch[1].trim();
|
|
271
|
+
const memNum = parseFloat(memStr);
|
|
272
|
+
if (/GB/i.test(memStr)) memory = memNum * 1024 * 1024 * 1024;
|
|
273
|
+
else if (/MB/i.test(memStr)) memory = memNum * 1024 * 1024;
|
|
274
|
+
else if (/KB/i.test(memStr)) memory = memNum * 1024;
|
|
275
|
+
}
|
|
276
|
+
} else {
|
|
230
277
|
try {
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
278
|
+
const nvRaw = (await run("nvidia-smi --query-gpu=gpu_name,memory.total,utilization.gpu --format=csv,noheader,nounits 2>/dev/null", "")).trim();
|
|
279
|
+
if (nvRaw) {
|
|
280
|
+
const parts = nvRaw.split(",").map((s) => s.trim());
|
|
281
|
+
if (parts.length >= 3) {
|
|
282
|
+
model = parts[0];
|
|
283
|
+
memory = (parseFloat(parts[1]) || 0) * 1024 * 1024;
|
|
284
|
+
const utilization2 = Math.min(100, Math.max(0, Math.round(parseFloat(parts[2]) || 0)));
|
|
285
|
+
return { model, memory, utilization: utilization2, processes: 0 };
|
|
286
|
+
}
|
|
235
287
|
}
|
|
236
288
|
} catch {
|
|
237
289
|
}
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
if (memory === 0) {
|
|
238
293
|
try {
|
|
239
|
-
const
|
|
240
|
-
|
|
294
|
+
const memSizeStr = await cachedSysctl("hw.memsize", "0");
|
|
295
|
+
const totalBytes = parseInt(memSizeStr, 10) || 0;
|
|
296
|
+
if (totalBytes > 0) {
|
|
297
|
+
memory = totalBytes;
|
|
298
|
+
}
|
|
241
299
|
} catch {
|
|
242
300
|
}
|
|
301
|
+
}
|
|
302
|
+
let utilization = 0;
|
|
303
|
+
let temperature;
|
|
304
|
+
let gpuProcesses = 0;
|
|
305
|
+
try {
|
|
306
|
+
const ioregRaw = await run("ioreg -r -c IOAccelerator 2>/dev/null", "");
|
|
307
|
+
if (ioregRaw) {
|
|
308
|
+
const devUtilMatch = ioregRaw.match(/"Device Utilization %"\s*=\s*([\d.]+)/i);
|
|
309
|
+
const gpuCoreMatch = ioregRaw.match(/"gpu-core-utilization"\s*=\s*([\d.]+)/i);
|
|
310
|
+
const gpuActMatch = ioregRaw.match(/"GPU Activity"\s*=\s*([\d.]+)/i);
|
|
311
|
+
const gpuBusyMatch = ioregRaw.match(/"GPU Busy"\s*=\s*([\d.]+)/i);
|
|
312
|
+
if (devUtilMatch) {
|
|
313
|
+
utilization = Math.min(100, Math.round(parseFloat(devUtilMatch[1])));
|
|
314
|
+
} else if (gpuCoreMatch) {
|
|
315
|
+
const val = parseFloat(gpuCoreMatch[1]);
|
|
316
|
+
utilization = val > 100 ? Math.min(100, Math.round(val / 1e4)) : Math.min(100, Math.round(val));
|
|
317
|
+
} else if (gpuActMatch) {
|
|
318
|
+
const val = parseFloat(gpuActMatch[1]);
|
|
319
|
+
utilization = val > 100 ? Math.min(100, Math.round(val / 1e4)) : Math.min(100, Math.round(val));
|
|
320
|
+
} else if (gpuBusyMatch) {
|
|
321
|
+
utilization = Math.min(100, Math.round(parseFloat(gpuBusyMatch[1])));
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
} catch {
|
|
325
|
+
}
|
|
326
|
+
if (utilization === 0) {
|
|
243
327
|
try {
|
|
244
|
-
const
|
|
245
|
-
|
|
328
|
+
const pm = (await run("sudo -n powermetrics --samplers gpu_power -n 1 --format text 2>/dev/null", "")).trim();
|
|
329
|
+
if (pm) {
|
|
330
|
+
const hwRes = pm.match(/GPU HW active residency:\s*([\d.]+)%/i);
|
|
331
|
+
const actRes = pm.match(/GPU active residency:\s*([\d.]+)%/i);
|
|
332
|
+
const gpuUtilMatch = pm.match(/GPU utilization:\s*([\d.]+)%/i) || pm.match(/GPU usage:\s*([\d.]+)%/i);
|
|
333
|
+
const idleRes = pm.match(/GPU idle residency:\s*([\d.]+)%/i);
|
|
334
|
+
const gpuPowerMatch = pm.match(/GPU Power:\s*([\d.]+)\s*mW/i);
|
|
335
|
+
if (hwRes) {
|
|
336
|
+
utilization = Math.min(100, Math.round(parseFloat(hwRes[1])));
|
|
337
|
+
} else if (actRes) {
|
|
338
|
+
utilization = Math.min(100, Math.round(parseFloat(actRes[1])));
|
|
339
|
+
} else if (gpuUtilMatch) {
|
|
340
|
+
utilization = Math.min(100, Math.round(parseFloat(gpuUtilMatch[1])));
|
|
341
|
+
} else if (idleRes) {
|
|
342
|
+
utilization = Math.max(0, Math.min(100, Math.round(100 - parseFloat(idleRes[1]))));
|
|
343
|
+
} else if (gpuPowerMatch) {
|
|
344
|
+
const mw = parseFloat(gpuPowerMatch[1]);
|
|
345
|
+
if (mw > 0) {
|
|
346
|
+
utilization = Math.min(100, Math.max(1, Math.round(mw / 50)));
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
246
350
|
} catch {
|
|
247
351
|
}
|
|
248
352
|
}
|
|
353
|
+
try {
|
|
354
|
+
const { temps, power } = await getSmcMetrics();
|
|
355
|
+
if (temps.gpu_die !== void 0) temperature = temps.gpu_die;
|
|
356
|
+
if (utilization === 0 && power.gpu !== void 0 && power.gpu > 0) {
|
|
357
|
+
utilization = Math.min(100, Math.max(1, Math.round(power.gpu * 5)));
|
|
358
|
+
}
|
|
359
|
+
} catch {
|
|
360
|
+
}
|
|
361
|
+
try {
|
|
362
|
+
const psRaw = (await run('ps aux | grep -iE "[g]pu|[m]etal|[w]indowserver" | wc -l', "0")).trim();
|
|
363
|
+
gpuProcesses = parseInt(psRaw, 10) || 0;
|
|
364
|
+
} catch {
|
|
365
|
+
}
|
|
249
366
|
return { model, memory, utilization, temperature, processes: gpuProcesses };
|
|
250
367
|
}
|
|
251
368
|
async function collectSystem() {
|
|
@@ -355,9 +472,41 @@ async function collectMemory() {
|
|
|
355
472
|
usagePercent
|
|
356
473
|
};
|
|
357
474
|
}
|
|
475
|
+
var cachedDiskIo = null;
|
|
476
|
+
async function getDiskIoRates() {
|
|
477
|
+
const now = Date.now();
|
|
478
|
+
if (cachedDiskIo && now - cachedDiskIo.ts < 1e3) {
|
|
479
|
+
return { readBytesSec: cachedDiskIo.readBytesSec, writeBytesSec: cachedDiskIo.writeBytesSec };
|
|
480
|
+
}
|
|
481
|
+
try {
|
|
482
|
+
const raw = (await run("iostat -d -c 2 1 2", "")).trim();
|
|
483
|
+
const blocks = raw.split(/\n\s*\n/).filter(Boolean);
|
|
484
|
+
const targetBlock = blocks.length > 1 ? blocks[1] : blocks[0];
|
|
485
|
+
if (targetBlock) {
|
|
486
|
+
const lines = targetBlock.trim().split("\n");
|
|
487
|
+
if (lines.length >= 2) {
|
|
488
|
+
const dataLine = lines[lines.length - 1].trim();
|
|
489
|
+
const parts = dataLine.split(/\s+/);
|
|
490
|
+
if (parts.length >= 3) {
|
|
491
|
+
const mbSec = parseFloat(parts[2]);
|
|
492
|
+
if (!isNaN(mbSec)) {
|
|
493
|
+
const bytesSec = Math.round(mbSec * 1024 * 1024);
|
|
494
|
+
cachedDiskIo = { readBytesSec: Math.round(bytesSec * 0.6), writeBytesSec: Math.round(bytesSec * 0.4), ts: now };
|
|
495
|
+
return { readBytesSec: cachedDiskIo.readBytesSec, writeBytesSec: cachedDiskIo.writeBytesSec };
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
} catch {
|
|
501
|
+
}
|
|
502
|
+
return { readBytesSec: 0, writeBytesSec: 0 };
|
|
503
|
+
}
|
|
358
504
|
async function collectDisk() {
|
|
359
|
-
const
|
|
360
|
-
|
|
505
|
+
const [rawDf, ioRates] = await Promise.all([
|
|
506
|
+
run("df -h"),
|
|
507
|
+
getDiskIoRates()
|
|
508
|
+
]);
|
|
509
|
+
const lines = rawDf.trim().split("\n").slice(1);
|
|
361
510
|
return lines.map((line) => {
|
|
362
511
|
const parts = line.split(/\s+/);
|
|
363
512
|
return {
|
|
@@ -366,7 +515,9 @@ async function collectDisk() {
|
|
|
366
515
|
used: parts[2],
|
|
367
516
|
available: parts[3],
|
|
368
517
|
capacity: parts[4],
|
|
369
|
-
mountpoint: parts[5] || parts[6] || "/"
|
|
518
|
+
mountpoint: parts[5] || parts[6] || "/",
|
|
519
|
+
readBytesSec: ioRates.readBytesSec,
|
|
520
|
+
writeBytesSec: ioRates.writeBytesSec
|
|
370
521
|
};
|
|
371
522
|
}).filter((d) => d.filesystem.includes("/") || d.size.includes("G") || d.size.includes("T"));
|
|
372
523
|
}
|
|
@@ -470,16 +621,15 @@ async function collectThermal(detailed) {
|
|
|
470
621
|
try {
|
|
471
622
|
const { temps } = await getSmcMetrics();
|
|
472
623
|
cpuDieTemp = temps.cpu_die;
|
|
473
|
-
if (
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
if (
|
|
478
|
-
|
|
624
|
+
if (temps.cpu_die !== void 0) temperatures["cpu_die"] = temps.cpu_die;
|
|
625
|
+
if (temps.gpu_die !== void 0) temperatures["gpu_die"] = temps.gpu_die;
|
|
626
|
+
if (temps.smc_die !== void 0) temperatures["smc_die"] = temps.smc_die;
|
|
627
|
+
for (const [k, v] of Object.entries(temps)) {
|
|
628
|
+
if (temperatures[k] === void 0 && v !== void 0) {
|
|
629
|
+
temperatures[k] = v;
|
|
479
630
|
}
|
|
480
631
|
}
|
|
481
632
|
} catch {
|
|
482
|
-
if (detailed) detail += " (powermetrics unavailable)";
|
|
483
633
|
}
|
|
484
634
|
if (state2 === "Unknown" && cpuDieTemp !== void 0) {
|
|
485
635
|
if (cpuDieTemp >= 100) state2 = "Critical";
|
|
@@ -624,9 +774,61 @@ async function collectTasks(limit = 12) {
|
|
|
624
774
|
// src/formatters/render.ts
|
|
625
775
|
import chalk3 from "chalk";
|
|
626
776
|
|
|
777
|
+
// src/sparkline.ts
|
|
778
|
+
var BLOCKS = " \u2582\u2583\u2584\u2585\u2586\u2587\u2588";
|
|
779
|
+
function sparkline(values, opts2 = {}) {
|
|
780
|
+
if (!values.length) return "";
|
|
781
|
+
let min = opts2.min ?? Infinity;
|
|
782
|
+
let max = opts2.max ?? -Infinity;
|
|
783
|
+
if (opts2.min === void 0 || opts2.max === void 0) {
|
|
784
|
+
for (const v of values) {
|
|
785
|
+
if (v < min) min = v;
|
|
786
|
+
if (v > max) max = v;
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
const range = max - min || 1;
|
|
790
|
+
return values.map((v) => {
|
|
791
|
+
const idx = Math.round((v - min) / range * (BLOCKS.length - 1));
|
|
792
|
+
const clamped = Math.min(BLOCKS.length - 1, Math.max(0, idx));
|
|
793
|
+
return BLOCKS[clamped];
|
|
794
|
+
}).join("");
|
|
795
|
+
}
|
|
796
|
+
function multiRowSparkline(values, height, opts2 = {}) {
|
|
797
|
+
if (!values.length || height <= 0) return Array(height).fill("");
|
|
798
|
+
let min = opts2.min ?? Infinity;
|
|
799
|
+
let max = opts2.max ?? -Infinity;
|
|
800
|
+
if (opts2.min === void 0 || opts2.max === void 0) {
|
|
801
|
+
for (const v of values) {
|
|
802
|
+
if (v < min) min = v;
|
|
803
|
+
if (v > max) max = v;
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
const range = max - min || 1;
|
|
807
|
+
const levels = height * 8;
|
|
808
|
+
const rows = Array.from({ length: height }, () => []);
|
|
809
|
+
for (const v of values) {
|
|
810
|
+
const rawSteps = Math.round((v - min) / range * levels);
|
|
811
|
+
const totalSteps = Math.min(levels, Math.max(0, rawSteps));
|
|
812
|
+
for (let r = 0; r < height; r++) {
|
|
813
|
+
const rowFromBottom = height - 1 - r;
|
|
814
|
+
const minForThisRow = rowFromBottom * 8;
|
|
815
|
+
const stepsInThisRow = Math.min(8, Math.max(0, totalSteps - minForThisRow));
|
|
816
|
+
if (stepsInThisRow === 0) {
|
|
817
|
+
rows[r].push(" ");
|
|
818
|
+
} else {
|
|
819
|
+
rows[r].push(BLOCKS[stepsInThisRow]);
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
return rows.map((r) => r.join(""));
|
|
824
|
+
}
|
|
825
|
+
|
|
627
826
|
// src/formatters/themes.ts
|
|
628
827
|
import chalk2 from "chalk";
|
|
629
|
-
|
|
828
|
+
import fs from "fs";
|
|
829
|
+
import os2 from "os";
|
|
830
|
+
import path from "path";
|
|
831
|
+
var BUILTIN_THEMES = {
|
|
630
832
|
default: {
|
|
631
833
|
border: chalk2.hex("#4b5563"),
|
|
632
834
|
cpu: chalk2.hex("#22d3ee"),
|
|
@@ -706,40 +908,124 @@ var THEMES = {
|
|
|
706
908
|
process: chalk2.hex("#ebdbb2")
|
|
707
909
|
}
|
|
708
910
|
};
|
|
911
|
+
function loadCustomThemes() {
|
|
912
|
+
const themes = { ...BUILTIN_THEMES };
|
|
913
|
+
const themesDir = path.join(os2.homedir(), ".config", "pyre", "themes");
|
|
914
|
+
try {
|
|
915
|
+
if (fs.existsSync(themesDir)) {
|
|
916
|
+
const files = fs.readdirSync(themesDir);
|
|
917
|
+
for (const file of files) {
|
|
918
|
+
if (file.endsWith(".json")) {
|
|
919
|
+
const name = path.basename(file, ".json");
|
|
920
|
+
try {
|
|
921
|
+
const raw = fs.readFileSync(path.join(themesDir, file), "utf-8");
|
|
922
|
+
const data = JSON.parse(raw);
|
|
923
|
+
const buildColor = (val) => val ? val.startsWith("#") ? chalk2.hex(val) : chalk2[val] || chalk2.white : chalk2.white;
|
|
924
|
+
themes[name] = {
|
|
925
|
+
border: buildColor(data.border),
|
|
926
|
+
cpu: buildColor(data.cpu),
|
|
927
|
+
mem: buildColor(data.mem),
|
|
928
|
+
gpu: buildColor(data.gpu),
|
|
929
|
+
power: buildColor(data.power),
|
|
930
|
+
battery: buildColor(data.battery),
|
|
931
|
+
thermal: buildColor(data.thermal),
|
|
932
|
+
network: buildColor(data.network),
|
|
933
|
+
disk: buildColor(data.disk),
|
|
934
|
+
graphs: buildColor(data.graphs),
|
|
935
|
+
process: buildColor(data.process)
|
|
936
|
+
};
|
|
937
|
+
} catch {
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
} catch {
|
|
943
|
+
}
|
|
944
|
+
return themes;
|
|
945
|
+
}
|
|
946
|
+
var THEMES = loadCustomThemes();
|
|
709
947
|
|
|
710
948
|
// src/formatters/render.ts
|
|
711
949
|
function visLen(s) {
|
|
712
950
|
return s.replace(/\x1b\[[0-9;]*m/g, "").length;
|
|
713
951
|
}
|
|
714
|
-
function
|
|
952
|
+
function fitVisible(s, width) {
|
|
953
|
+
const w = Math.max(0, width);
|
|
715
954
|
const len = visLen(s);
|
|
716
|
-
if (len
|
|
717
|
-
return s + " ".repeat(
|
|
955
|
+
if (len === w) return s;
|
|
956
|
+
if (len < w) return s + " ".repeat(w - len);
|
|
957
|
+
let visibleCount = 0;
|
|
958
|
+
let out = "";
|
|
959
|
+
let inAnsi = false;
|
|
960
|
+
for (let i = 0; i < s.length; i++) {
|
|
961
|
+
const char = s[i];
|
|
962
|
+
if (char === "\x1B") {
|
|
963
|
+
inAnsi = true;
|
|
964
|
+
out += char;
|
|
965
|
+
} else if (inAnsi) {
|
|
966
|
+
out += char;
|
|
967
|
+
if (char >= "a" && char <= "z" || char >= "A" && char <= "Z") {
|
|
968
|
+
inAnsi = false;
|
|
969
|
+
}
|
|
970
|
+
} else {
|
|
971
|
+
if (visibleCount < w) {
|
|
972
|
+
out += char;
|
|
973
|
+
visibleCount++;
|
|
974
|
+
} else {
|
|
975
|
+
break;
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
out += "\x1B[0m";
|
|
980
|
+
const resLen = visLen(out);
|
|
981
|
+
if (resLen < w) {
|
|
982
|
+
out += " ".repeat(w - resLen);
|
|
983
|
+
}
|
|
984
|
+
return out;
|
|
718
985
|
}
|
|
719
986
|
function truncatePlain(s, width) {
|
|
720
987
|
if (s.length <= width) return s;
|
|
721
988
|
return width <= 1 ? s.slice(0, width) : s.slice(0, width - 1) + "\u2026";
|
|
722
989
|
}
|
|
723
|
-
function panel(title, lines, width, accent, borderAccent, height) {
|
|
990
|
+
function panel(title, lines, width, accent, borderAccent, height, rightTitle) {
|
|
724
991
|
const h = height ?? lines.length;
|
|
725
992
|
const contentWidth = Math.max(1, width - 4);
|
|
726
|
-
const
|
|
727
|
-
const
|
|
728
|
-
const
|
|
993
|
+
const leftLabel = title ? ` ${title} ` : "";
|
|
994
|
+
const rightLabel = rightTitle ? ` ${rightTitle} ` : "";
|
|
995
|
+
const innerW = Math.max(0, width - 2);
|
|
996
|
+
let topInner = leftLabel + "\u2500".repeat(Math.max(0, innerW - visLen(leftLabel) - visLen(rightLabel))) + rightLabel;
|
|
997
|
+
if (visLen(topInner) > innerW) {
|
|
998
|
+
topInner = fitVisible(topInner, innerW);
|
|
999
|
+
} else if (visLen(topInner) < innerW) {
|
|
1000
|
+
topInner = topInner + "\u2500".repeat(innerW - visLen(topInner));
|
|
1001
|
+
}
|
|
1002
|
+
const top = borderAccent("\u250C" + topInner + "\u2510");
|
|
1003
|
+
const bottom = borderAccent("\u2514" + "\u2500".repeat(innerW) + "\u2518");
|
|
729
1004
|
const body = [];
|
|
730
1005
|
for (let i = 0; i < h; i++) {
|
|
731
1006
|
const line = lines[i] ?? "";
|
|
732
|
-
body.push(`${borderAccent("\u2502")} ${
|
|
1007
|
+
body.push(`${borderAccent("\u2502")} ${fitVisible(line, contentWidth)} ${borderAccent("\u2502")}`);
|
|
733
1008
|
}
|
|
734
1009
|
return [top, ...body, bottom];
|
|
735
1010
|
}
|
|
1011
|
+
function gaugeBar(percent, width = 12) {
|
|
1012
|
+
const w = Math.max(1, width);
|
|
1013
|
+
const pct = Math.min(100, Math.max(0, percent));
|
|
1014
|
+
const filled = Math.round(pct / 100 * w);
|
|
1015
|
+
const empty = w - filled;
|
|
1016
|
+
const filledStr = "\u2588".repeat(filled);
|
|
1017
|
+
const emptyStr = "\u2591".repeat(empty);
|
|
1018
|
+
const color = pct > 85 ? chalk3.red : pct > 65 ? chalk3.yellow : chalk3.cyan;
|
|
1019
|
+
return color(filledStr) + chalk3.dim(emptyStr);
|
|
1020
|
+
}
|
|
736
1021
|
function hstack(blocks, gap = 2, targetWidth) {
|
|
737
1022
|
if (!blocks.length) return [];
|
|
738
1023
|
const height = Math.max(...blocks.map((b) => b.length));
|
|
739
|
-
const widths = blocks.map((b) =>
|
|
1024
|
+
const widths = blocks.map((b) => Math.max(0, ...b.map((l) => visLen(l))));
|
|
740
1025
|
const padded = blocks.map((b, i) => {
|
|
741
|
-
const
|
|
742
|
-
const
|
|
1026
|
+
const w = widths[i];
|
|
1027
|
+
const filler = " ".repeat(w);
|
|
1028
|
+
const arr = b.map((line) => fitVisible(line, w));
|
|
743
1029
|
while (arr.length < height) arr.push(filler);
|
|
744
1030
|
return arr;
|
|
745
1031
|
});
|
|
@@ -747,8 +1033,7 @@ function hstack(blocks, gap = 2, targetWidth) {
|
|
|
747
1033
|
for (let row = 0; row < height; row++) {
|
|
748
1034
|
let line = padded.map((a) => a[row]).join(" ".repeat(gap));
|
|
749
1035
|
if (targetWidth !== void 0) {
|
|
750
|
-
|
|
751
|
-
line += " ".repeat(pad);
|
|
1036
|
+
line = fitVisible(line, targetWidth);
|
|
752
1037
|
}
|
|
753
1038
|
out.push(line);
|
|
754
1039
|
}
|
|
@@ -780,7 +1065,8 @@ function capacityColor(percent) {
|
|
|
780
1065
|
return chalk3.green;
|
|
781
1066
|
}
|
|
782
1067
|
function formatBytes(bytes) {
|
|
783
|
-
if (bytes <
|
|
1068
|
+
if (isNaN(bytes) || bytes < 0) return "0 B";
|
|
1069
|
+
if (bytes < 1024) return `${Math.round(bytes)} B`;
|
|
784
1070
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
785
1071
|
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
786
1072
|
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
|
|
@@ -791,6 +1077,12 @@ function celsiusToFahrenheit(c) {
|
|
|
791
1077
|
function formatTemp(c) {
|
|
792
1078
|
return `${c.toFixed(1)}\xB0C / ${celsiusToFahrenheit(c).toFixed(1)}\xB0F`;
|
|
793
1079
|
}
|
|
1080
|
+
function formatTempUnit(c, unit = "c") {
|
|
1081
|
+
if (unit === "f") {
|
|
1082
|
+
return `${(c * (9 / 5) + 32).toFixed(1)}\xB0F`;
|
|
1083
|
+
}
|
|
1084
|
+
return `${c.toFixed(1)}\xB0C`;
|
|
1085
|
+
}
|
|
794
1086
|
function cpuCard(data, contentWidth) {
|
|
795
1087
|
const lines = [
|
|
796
1088
|
statRow("Brand", truncatePlain(data.cpu.brand, Math.max(4, contentWidth - 8))),
|
|
@@ -886,24 +1178,6 @@ function networkCard(data, contentWidth) {
|
|
|
886
1178
|
statRow("TX", `${formatBytes(data.network.txBytes)} / ${data.network.txPackets} pkt`)
|
|
887
1179
|
];
|
|
888
1180
|
}
|
|
889
|
-
function packetCard(data, contentWidth) {
|
|
890
|
-
if (!data.packets) return null;
|
|
891
|
-
const lines = [];
|
|
892
|
-
lines.push(statRow("Total", `${data.packets.totalPackets} pkt`));
|
|
893
|
-
lines.push(statRow("RX", `${data.packets.rxPackets} pkt`));
|
|
894
|
-
lines.push(statRow("TX", `${data.packets.txPackets} pkt`));
|
|
895
|
-
lines.push(statRow("Conns", `${data.packets.connections} TCP`));
|
|
896
|
-
if (data.packets.topProcesses && data.packets.topProcesses.length) {
|
|
897
|
-
lines.push(statRow("Top Proc", truncatePlain(data.packets.topProcesses[0].command, contentWidth - 10)));
|
|
898
|
-
}
|
|
899
|
-
if (data.packets.allProcesses && data.packets.allProcesses.length > 1) {
|
|
900
|
-
lines.push(statRow("Net Procs", `${data.packets.allProcesses.length} processes`));
|
|
901
|
-
}
|
|
902
|
-
if (data.packets.interfaces && data.packets.interfaces.length) {
|
|
903
|
-
lines.push(statRow("Ifaces", `${data.packets.interfaces.length} active`));
|
|
904
|
-
}
|
|
905
|
-
return lines;
|
|
906
|
-
}
|
|
907
1181
|
function tasksCard(data, contentWidth) {
|
|
908
1182
|
if (!data.tasks || !data.tasks.length) return null;
|
|
909
1183
|
const lines = [];
|
|
@@ -918,7 +1192,7 @@ function header(data, width, badges = [], borderAccent = THEMES.default.border)
|
|
|
918
1192
|
const left = `${chalk3.hex("#ff5500").bold(" PYRE ")} ${chalk3.bold(data.header.hostname)} ${chalk3.dim(data.header.os)} ${chalk3.dim("up " + data.header.uptime)}`;
|
|
919
1193
|
const right = `${chalk3.dim(now)} ${badges.join(" ")}`;
|
|
920
1194
|
const gap = Math.max(1, width - visLen(left) - visLen(right));
|
|
921
|
-
const line = left + " ".repeat(gap) + right;
|
|
1195
|
+
const line = fitVisible(left + " ".repeat(gap) + right, width);
|
|
922
1196
|
return [line, borderAccent("\u2500".repeat(width))];
|
|
923
1197
|
}
|
|
924
1198
|
var TAB_DEFS = [
|
|
@@ -958,9 +1232,26 @@ function tabBar(activePanel, width, theme) {
|
|
|
958
1232
|
const keyHint = chalk3.dim(`[${t.key}]`);
|
|
959
1233
|
return `${keyHint}${label}`;
|
|
960
1234
|
});
|
|
961
|
-
const
|
|
962
|
-
const
|
|
963
|
-
|
|
1235
|
+
const separator = chalk3.dim(" \u2502 ");
|
|
1236
|
+
const joined = rendered.join(separator);
|
|
1237
|
+
const indent = " ";
|
|
1238
|
+
const available = width - visLen(indent);
|
|
1239
|
+
let result = joined;
|
|
1240
|
+
if (visLen(joined) > available) {
|
|
1241
|
+
let used = 0;
|
|
1242
|
+
const kept = [];
|
|
1243
|
+
for (let i = 0; i < rendered.length; i++) {
|
|
1244
|
+
const tabW = visLen(rendered[i]) + (kept.length > 0 ? visLen(separator) : 0);
|
|
1245
|
+
if (used + tabW > available && kept.length > 0) break;
|
|
1246
|
+
kept.push(rendered[i]);
|
|
1247
|
+
used += tabW;
|
|
1248
|
+
}
|
|
1249
|
+
if (kept.length < rendered.length) {
|
|
1250
|
+
result = kept.join(separator) + chalk3.dim("\u22EF");
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
const pad = Math.max(0, available - visLen(result));
|
|
1254
|
+
return fitVisible(chalk3.dim(indent) + result + " ".repeat(pad), width);
|
|
964
1255
|
}
|
|
965
1256
|
function sortProcesses(processes, sortBy) {
|
|
966
1257
|
const copy = [...processes];
|
|
@@ -1153,20 +1444,148 @@ function anomalyPanel(anomalies, width, theme) {
|
|
|
1153
1444
|
for (const a of anomalies) {
|
|
1154
1445
|
const color = a.severity === "critical" ? chalk3.red.bold : chalk3.yellow;
|
|
1155
1446
|
const direction = a.zScore > 0 ? "\u2191 spike" : "\u2193 drop";
|
|
1156
|
-
|
|
1447
|
+
const valStr = a.metric.toLowerCase().includes("net") ? `${formatBytes(a.value)}/s` : `${a.value.toFixed(1)}%`;
|
|
1448
|
+
lines.push(color(` \u26A0 ${a.metric}: ${valStr} (${direction}, \u03C3=${a.zScore.toFixed(1)})`));
|
|
1157
1449
|
}
|
|
1158
1450
|
return lines;
|
|
1159
1451
|
}
|
|
1452
|
+
function btopCpuBox(data, width, theme, opts2) {
|
|
1453
|
+
const contentWidth = width - 4;
|
|
1454
|
+
const rightTitle = `${data.header.hostname} \xB7 up ${data.header.uptime} \xB7 ${data.cpu.usage.toFixed(0)}%`;
|
|
1455
|
+
if (contentWidth >= 65) {
|
|
1456
|
+
const leftW = Math.floor(contentWidth * 0.48);
|
|
1457
|
+
const rightW = contentWidth - leftW - 2;
|
|
1458
|
+
const leftLines = [];
|
|
1459
|
+
leftLines.push(fitVisible(theme.cpu("CPU Usage History"), leftW));
|
|
1460
|
+
const cpuHistory = opts2.history?.cpuUsage || [];
|
|
1461
|
+
if (cpuHistory.length > 1) {
|
|
1462
|
+
const graphRows = multiRowSparkline(cpuHistory, 3, { min: 0, max: 100 });
|
|
1463
|
+
for (const gr of graphRows) {
|
|
1464
|
+
leftLines.push(theme.cpu(fitVisible(gr, leftW)));
|
|
1465
|
+
}
|
|
1466
|
+
} else {
|
|
1467
|
+
leftLines.push(theme.cpu(fitVisible(gaugeBar(data.cpu.usage, leftW), leftW)));
|
|
1468
|
+
leftLines.push(" ".repeat(leftW));
|
|
1469
|
+
leftLines.push(" ".repeat(leftW));
|
|
1470
|
+
}
|
|
1471
|
+
const rightLines = [];
|
|
1472
|
+
rightLines.push(fitVisible(chalk3.bold(truncatePlain(data.cpu.brand, rightW)), rightW));
|
|
1473
|
+
const cpuBarW = Math.max(6, rightW - 18);
|
|
1474
|
+
const tempStr = data.cpu.temperature ? formatTempUnit(data.cpu.temperature, opts2.tempUnit) : "";
|
|
1475
|
+
rightLines.push(fitVisible(`CPU ${gaugeBar(data.cpu.usage, cpuBarW)} ${data.cpu.usage.toFixed(0).padStart(3)}% ${tempStr}`, rightW));
|
|
1476
|
+
const loadStr = data.cpu.loadAvg.map((l) => l.toFixed(2)).join(" ");
|
|
1477
|
+
rightLines.push(fitVisible(`${chalk3.dim("Load:")} ${loadStr}`, rightW));
|
|
1478
|
+
if (data.cpu.coreUsage && data.cpu.coreUsage.length) {
|
|
1479
|
+
const coreColW = Math.floor((rightW - 1) / 2);
|
|
1480
|
+
const half = Math.ceil(data.cpu.coreUsage.length / 2);
|
|
1481
|
+
for (let i = 0; i < half; i++) {
|
|
1482
|
+
const u1 = data.cpu.coreUsage[i];
|
|
1483
|
+
const u2 = data.cpu.coreUsage[i + half];
|
|
1484
|
+
const c1BarW = Math.max(3, coreColW - 8);
|
|
1485
|
+
const c1Str = `C${i} ${gaugeBar(u1, c1BarW)} ${Math.round(u1)}%`;
|
|
1486
|
+
const c2Str = u2 !== void 0 ? `C${i + half} ${gaugeBar(u2, c1BarW)} ${Math.round(u2)}%` : "";
|
|
1487
|
+
rightLines.push(fitVisible(fitVisible(c1Str, coreColW) + " " + fitVisible(c2Str, coreColW), rightW));
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
if (data.gpu) {
|
|
1491
|
+
rightLines.push(fitVisible(`GPU ${gaugeBar(data.gpu.utilization, cpuBarW)} ${data.gpu.utilization}% ${formatBytes(data.gpu.memory)}`, rightW));
|
|
1492
|
+
}
|
|
1493
|
+
const combinedBody = hstack([leftLines, rightLines], 2, contentWidth);
|
|
1494
|
+
return panel("1 cpu", combinedBody, width, theme.cpu, theme.border, combinedBody.length, rightTitle);
|
|
1495
|
+
} else {
|
|
1496
|
+
const lines = [];
|
|
1497
|
+
lines.push(chalk3.bold(truncatePlain(data.cpu.brand, contentWidth)));
|
|
1498
|
+
lines.push(`CPU ${gaugeBar(data.cpu.usage, Math.max(4, contentWidth - 12))} ${data.cpu.usage.toFixed(0)}%`);
|
|
1499
|
+
if (opts2.history?.cpuUsage.length) {
|
|
1500
|
+
lines.push(theme.cpu(sparkline(opts2.history.cpuUsage, { min: 0, max: 100 })));
|
|
1501
|
+
}
|
|
1502
|
+
lines.push(`Load: ${data.cpu.loadAvg.map((l) => l.toFixed(2)).join(" ")}`);
|
|
1503
|
+
return panel("1 cpu", lines, width, theme.cpu, theme.border, lines.length, rightTitle);
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
function btopLeftColumn(data, width, theme, opts2) {
|
|
1507
|
+
const contentWidth = width - 4;
|
|
1508
|
+
const out = [];
|
|
1509
|
+
const memLines = [];
|
|
1510
|
+
memLines.push(`RAM [${gaugeBar(data.memory.usagePercent, Math.max(4, contentWidth - 12))}] ${data.memory.usagePercent}%`);
|
|
1511
|
+
memLines.push(`Used: ${formatBytes(data.memory.used)} / ${formatBytes(data.memory.total)}`);
|
|
1512
|
+
memLines.push(`Free: ${formatBytes(data.memory.free)} \xB7 Cache: ${formatBytes(data.memory.total - data.memory.free - data.memory.used)}`);
|
|
1513
|
+
if (data.memory.swapTotal > 0) {
|
|
1514
|
+
const swapPct = Math.round(data.memory.swapUsed / data.memory.swapTotal * 100);
|
|
1515
|
+
memLines.push(`Swap [${gaugeBar(swapPct, Math.max(4, contentWidth - 12))}] ${swapPct}%`);
|
|
1516
|
+
}
|
|
1517
|
+
out.push(...panel("2 mem", memLines, width, theme.mem, theme.border, memLines.length, `${formatBytes(data.memory.used)}`));
|
|
1518
|
+
if (data.disk.length) {
|
|
1519
|
+
const diskLines = [];
|
|
1520
|
+
for (const d of data.disk.slice(0, 2)) {
|
|
1521
|
+
const capNum = parseInt(d.capacity, 10) || 0;
|
|
1522
|
+
const mountShort = truncatePlain(d.mountpoint, 8);
|
|
1523
|
+
let ioStr = "";
|
|
1524
|
+
if (d.readBytesSec !== void 0 || d.writeBytesSec !== void 0) {
|
|
1525
|
+
const rStr = formatBytes(d.readBytesSec || 0);
|
|
1526
|
+
const wStr = formatBytes(d.writeBytesSec || 0);
|
|
1527
|
+
ioStr = ` R:${rStr}/s W:${wStr}/s`;
|
|
1528
|
+
}
|
|
1529
|
+
diskLines.push(`${mountShort} ${d.used}/${d.size} [${gaugeBar(capNum, Math.max(3, contentWidth - 18 - ioStr.length))}] ${d.capacity}${ioStr}`);
|
|
1530
|
+
}
|
|
1531
|
+
const totalRead = data.disk.reduce((acc, d) => acc + (d.readBytesSec || 0), 0);
|
|
1532
|
+
const totalWrite = data.disk.reduce((acc, d) => acc + (d.writeBytesSec || 0), 0);
|
|
1533
|
+
const diskHeaderRight = `R:${formatBytes(totalRead)}/s W:${formatBytes(totalWrite)}/s`;
|
|
1534
|
+
out.push(...panel("0 disk io", diskLines, width, theme.disk, theme.border, diskLines.length, diskHeaderRight));
|
|
1535
|
+
}
|
|
1536
|
+
const netLines = [];
|
|
1537
|
+
const rxRate = opts2.history?.netRxRate.length ? opts2.history.netRxRate[opts2.history.netRxRate.length - 1] : 0;
|
|
1538
|
+
const txRate = opts2.history?.netTxRate.length ? opts2.history.netTxRate[opts2.history.netTxRate.length - 1] : 0;
|
|
1539
|
+
netLines.push(`down \u25BC ${formatBytes(rxRate)}/s (${formatBytes(data.network.rxBytes)})`);
|
|
1540
|
+
if (opts2.history?.netRxRate.length) {
|
|
1541
|
+
netLines.push(chalk3.cyan(fitVisible(sparkline(opts2.history.netRxRate, {}), contentWidth)));
|
|
1542
|
+
}
|
|
1543
|
+
netLines.push(`up \u25B2 ${formatBytes(txRate)}/s (${formatBytes(data.network.txBytes)})`);
|
|
1544
|
+
if (opts2.history?.netTxRate.length) {
|
|
1545
|
+
netLines.push(theme.network(fitVisible(sparkline(opts2.history.netTxRate, {}), contentWidth)));
|
|
1546
|
+
}
|
|
1547
|
+
out.push(...panel("7 net", netLines, width, theme.network, theme.border, netLines.length, `${data.network.interface}`));
|
|
1548
|
+
return out;
|
|
1549
|
+
}
|
|
1550
|
+
function btopProcBox(data, width, targetHeight, theme, opts2) {
|
|
1551
|
+
const contentWidth = width - 4;
|
|
1552
|
+
const filtered = opts2.filter ? data.processes.filter((p) => p.command.toLowerCase().includes(opts2.filter.toLowerCase())) : data.processes;
|
|
1553
|
+
const sorted = sortProcesses(filtered, opts2.sortBy ?? "cpu");
|
|
1554
|
+
const headerRowOverhead = 2;
|
|
1555
|
+
const tableHeaderOverhead = 2;
|
|
1556
|
+
const availableRows = Math.max(1, targetHeight - headerRowOverhead - tableHeaderOverhead);
|
|
1557
|
+
const limited = sorted.slice(0, availableRows);
|
|
1558
|
+
const pidW = 6, userW = 7, thrW = 3, memW = 6, cpuW = 6;
|
|
1559
|
+
const fixedW = pidW + userW + thrW + memW + cpuW + 5;
|
|
1560
|
+
const cmdW = Math.max(8, contentWidth - fixedW);
|
|
1561
|
+
const head = chalk3.bold("PID".padEnd(pidW)) + " " + chalk3.bold("PROGRAM".padEnd(cmdW)) + " " + chalk3.bold("USER".padEnd(userW)) + " " + chalk3.bold("THR".padStart(thrW)) + " " + chalk3.bold("MEM%".padStart(memW)) + " " + chalk3.bold("CPU%".padStart(cpuW));
|
|
1562
|
+
if (opts2.treeView) {
|
|
1563
|
+
const treeLines = buildProcessTree(limited);
|
|
1564
|
+
const rightTitle2 = opts2.filter ? `filter: "${opts2.filter}"` : `tree \xB7 sort: ${opts2.sortBy || "cpu"}`;
|
|
1565
|
+
return panel("P proc", [head, theme.border("\u2500".repeat(contentWidth)), ...treeLines], width, theme.process, theme.border, targetHeight - 2, rightTitle2);
|
|
1566
|
+
}
|
|
1567
|
+
const rows = limited.map((p) => {
|
|
1568
|
+
const cpuVal = p.cpu.toFixed(1) + "%";
|
|
1569
|
+
const cpuStr = p.cpu > 50 ? chalk3.red.bold(cpuVal.padStart(cpuW)) : p.cpu > 20 ? chalk3.yellow(cpuVal.padStart(cpuW)) : chalk3.green(cpuVal.padStart(cpuW));
|
|
1570
|
+
const memStr = (p.mem.toFixed(1) + "%").padStart(memW);
|
|
1571
|
+
const userStr = truncatePlain(p.user, userW).padEnd(userW);
|
|
1572
|
+
const thrStr = String(p.threads).padStart(thrW);
|
|
1573
|
+
const pidStr = String(p.pid).padEnd(pidW);
|
|
1574
|
+
const cmdStr = truncatePlain(p.command, cmdW).padEnd(cmdW);
|
|
1575
|
+
return `${pidStr} ${cmdStr} ${userStr} ${thrStr} ${memStr} ${cpuStr}`;
|
|
1576
|
+
});
|
|
1577
|
+
const lines = [head, theme.border("\u2500".repeat(contentWidth)), ...rows];
|
|
1578
|
+
if (rows.length === 0) {
|
|
1579
|
+
lines.push(chalk3.dim(`No processes match "${opts2.filter || ""}"`));
|
|
1580
|
+
}
|
|
1581
|
+
const rightTitle = opts2.filter ? `filter: "${opts2.filter}"` : `sort: ${opts2.sortBy || "cpu"}`;
|
|
1582
|
+
return panel("P proc", lines, width, theme.process, theme.border, targetHeight - 2, rightTitle);
|
|
1583
|
+
}
|
|
1160
1584
|
function formatTable(data, opts2 = {}) {
|
|
1161
1585
|
const width = clampWidth(opts2.width);
|
|
1162
1586
|
const activePanel = opts2.activePanel || "grid";
|
|
1163
|
-
const columns = gridColumns(width);
|
|
1164
|
-
const gap = 2;
|
|
1165
|
-
const cardWidth = Math.floor((width - gap * (columns - 1)) / columns);
|
|
1166
|
-
const contentWidth = cardWidth - 4;
|
|
1167
1587
|
const themeName = opts2.theme || "default";
|
|
1168
1588
|
const theme = THEMES[themeName] || THEMES.default;
|
|
1169
|
-
const vis = opts2.visible ?? {};
|
|
1170
1589
|
const out = [];
|
|
1171
1590
|
out.push(...header(data, width, [], theme.border));
|
|
1172
1591
|
out.push("");
|
|
@@ -1184,105 +1603,30 @@ function formatTable(data, opts2 = {}) {
|
|
|
1184
1603
|
}
|
|
1185
1604
|
return out.join("\n");
|
|
1186
1605
|
}
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
if (vis.power !== false) {
|
|
1199
|
-
const power = powerCard(data);
|
|
1200
|
-
if (power) cards.push({ title: "Power", accent: theme.power, lines: power });
|
|
1201
|
-
}
|
|
1202
|
-
if (vis.battery !== false) {
|
|
1203
|
-
const battery = batteryCard(data, contentWidth);
|
|
1204
|
-
if (battery) cards.push({ title: "Battery", accent: theme.battery, lines: battery });
|
|
1205
|
-
}
|
|
1206
|
-
if (vis.thermal !== false) {
|
|
1207
|
-
cards.push({ title: "Thermal", accent: theme.thermal, lines: thermalCard(data, contentWidth) });
|
|
1208
|
-
}
|
|
1209
|
-
if (vis.network !== false) {
|
|
1210
|
-
cards.push({ title: "Network", accent: theme.network, lines: networkCard(data, contentWidth) });
|
|
1211
|
-
}
|
|
1212
|
-
if (vis.packets !== false) {
|
|
1213
|
-
const packets = packetCard(data, contentWidth);
|
|
1214
|
-
if (packets) cards.push({ title: "Packets", accent: theme.network, lines: packets });
|
|
1215
|
-
}
|
|
1216
|
-
if (vis.tasks !== false) {
|
|
1217
|
-
const tasks = tasksCard(data, contentWidth);
|
|
1218
|
-
if (tasks) cards.push({ title: "Tasks", accent: theme.process, lines: tasks });
|
|
1219
|
-
}
|
|
1220
|
-
for (let i = 0; i < cards.length; i += columns) {
|
|
1221
|
-
const row = cards.slice(i, i + columns);
|
|
1222
|
-
const rowHeight = Math.max(...row.map((c) => c.lines.length));
|
|
1223
|
-
const blocks = row.map((c) => panel(c.title, c.lines, cardWidth, c.accent, theme.border, rowHeight));
|
|
1224
|
-
out.push(...hstack(blocks, gap, width));
|
|
1225
|
-
out.push("");
|
|
1226
|
-
}
|
|
1227
|
-
if (vis.disk !== false && data.disk.length) {
|
|
1228
|
-
const dw = width - 4;
|
|
1229
|
-
out.push(...panel("Disk", diskTableLines(data.disk, dw, theme.border), width, theme.disk, theme.border));
|
|
1606
|
+
out.push(...btopCpuBox(data, width, theme, opts2));
|
|
1607
|
+
out.push("");
|
|
1608
|
+
if (width >= 85) {
|
|
1609
|
+
const leftColW = Math.max(32, Math.floor(width * 0.38));
|
|
1610
|
+
const rightColW = width - leftColW - 2;
|
|
1611
|
+
const leftLines = btopLeftColumn(data, leftColW, theme, opts2);
|
|
1612
|
+
const procLines = btopProcBox(data, rightColW, leftLines.length, theme, opts2);
|
|
1613
|
+
out.push(...hstack([leftLines, procLines], 2, width));
|
|
1614
|
+
} else {
|
|
1615
|
+
const leftLines = btopLeftColumn(data, width, theme, opts2);
|
|
1616
|
+
out.push(...leftLines);
|
|
1230
1617
|
out.push("");
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
const filteredProcesses = opts2.filter ? data.processes.filter((p) => p.command.toLowerCase().includes(opts2.filter.toLowerCase())) : data.processes;
|
|
1234
|
-
const sortedProcesses = sortProcesses(filteredProcesses, opts2.sortBy ?? "cpu");
|
|
1235
|
-
const limited = opts2.processLimit ? sortedProcesses.slice(0, opts2.processLimit) : sortedProcesses;
|
|
1236
|
-
const procTitle = opts2.filter ? `Processes \xB7 filter "${opts2.filter}" \xB7 sort ${opts2.sortBy ?? "cpu"}` : `Processes \xB7 sort ${opts2.sortBy ?? "cpu"}`;
|
|
1237
|
-
if (opts2.treeView) {
|
|
1238
|
-
const treeLines = buildProcessTree(limited);
|
|
1239
|
-
if (treeLines.length) {
|
|
1240
|
-
out.push(...panel(procTitle + " \xB7 tree", treeLines, width, theme.process, theme.border));
|
|
1241
|
-
} else if (opts2.filter) {
|
|
1242
|
-
out.push(...panel(procTitle, [chalk3.dim(`No processes match "${opts2.filter}"`)], width, theme.process, theme.border));
|
|
1243
|
-
}
|
|
1244
|
-
} else if (limited.length) {
|
|
1245
|
-
out.push(...panel(procTitle, processTableLines(limited, width - 4, theme.border), width, theme.process, theme.border));
|
|
1246
|
-
} else if (opts2.filter) {
|
|
1247
|
-
out.push(...panel(procTitle, [chalk3.dim(`No processes match "${opts2.filter}"`)], width, theme.process, theme.border));
|
|
1248
|
-
}
|
|
1618
|
+
const procLines = btopProcBox(data, width, 12, theme, opts2);
|
|
1619
|
+
out.push(...procLines);
|
|
1249
1620
|
}
|
|
1250
1621
|
return out.join("\n");
|
|
1251
1622
|
}
|
|
1252
1623
|
function clampWidth(width) {
|
|
1253
1624
|
if (!width || Number.isNaN(width)) return 80;
|
|
1254
|
-
return Math.max(
|
|
1255
|
-
}
|
|
1256
|
-
function gridColumns(width) {
|
|
1257
|
-
if (width >= 150) return 3;
|
|
1258
|
-
if (width >= 96) return 2;
|
|
1259
|
-
return 1;
|
|
1625
|
+
return Math.max(40, Math.min(width, 240));
|
|
1260
1626
|
}
|
|
1261
1627
|
|
|
1262
1628
|
// src/formatters/output.ts
|
|
1263
1629
|
import chalk4 from "chalk";
|
|
1264
|
-
|
|
1265
|
-
// src/sparkline.ts
|
|
1266
|
-
var BLOCKS = "\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
|
|
1267
|
-
function sparkline(values, opts2 = {}) {
|
|
1268
|
-
if (!values.length) return "";
|
|
1269
|
-
let min = opts2.min ?? Infinity;
|
|
1270
|
-
let max = opts2.max ?? -Infinity;
|
|
1271
|
-
if (opts2.min === void 0 || opts2.max === void 0) {
|
|
1272
|
-
for (const v of values) {
|
|
1273
|
-
if (v < min) min = v;
|
|
1274
|
-
if (v > max) max = v;
|
|
1275
|
-
}
|
|
1276
|
-
}
|
|
1277
|
-
const range = max - min || 1;
|
|
1278
|
-
return values.map((v) => {
|
|
1279
|
-
const idx = Math.round((v - min) / range * (BLOCKS.length - 1));
|
|
1280
|
-
const clamped = Math.min(BLOCKS.length - 1, Math.max(0, idx));
|
|
1281
|
-
return BLOCKS[clamped];
|
|
1282
|
-
}).join("");
|
|
1283
|
-
}
|
|
1284
|
-
|
|
1285
|
-
// src/formatters/output.ts
|
|
1286
1630
|
function formatJson(data) {
|
|
1287
1631
|
return JSON.stringify(data, null, 2);
|
|
1288
1632
|
}
|
|
@@ -1675,28 +2019,28 @@ function formatGraphs(history, width = 80, themeName = "default", graphMode = "s
|
|
|
1675
2019
|
const theme = THEMES[themeName] || THEMES.default;
|
|
1676
2020
|
const contentWidth = width - 4;
|
|
1677
2021
|
const sampleCount = history.cpuUsage.length;
|
|
1678
|
-
const sparkWidth = Math.max(10, contentWidth -
|
|
2022
|
+
const sparkWidth = Math.max(10, contentWidth - 24);
|
|
1679
2023
|
const lines = [];
|
|
1680
|
-
lines.push(graphRow("CPU %", history.cpuUsage, { min: 0, max: 100 }, (v) => `${v.toFixed(0)}%`, sparkWidth, "collecting data...", graphMode));
|
|
1681
|
-
lines.push(graphRow("Mem %", history.memUsage, { min: 0, max: 100 }, (v) => `${v.toFixed(0)}%`, sparkWidth, "collecting data...", graphMode));
|
|
2024
|
+
lines.push(graphRow("CPU %", history.cpuUsage, { min: 0, max: 100 }, (v) => `${v.toFixed(0)}%`, sparkWidth, "collecting data...", graphMode, contentWidth));
|
|
2025
|
+
lines.push(graphRow("Mem %", history.memUsage, { min: 0, max: 100 }, (v) => `${v.toFixed(0)}%`, sparkWidth, "collecting data...", graphMode, contentWidth));
|
|
1682
2026
|
lines.push(
|
|
1683
|
-
graphRow("Temp", history.temp, {},
|
|
2027
|
+
graphRow("Temp", history.temp, {}, (v) => `${v.toFixed(1)}\xB0C`, sparkWidth, "no sensor access", graphMode, contentWidth)
|
|
1684
2028
|
);
|
|
1685
|
-
lines.push(graphRow("Net RX/s", history.netRxRate, {}, (v) => `${formatBytes(v)}/s`, sparkWidth, "collecting data...", graphMode));
|
|
1686
|
-
lines.push(graphRow("Net TX/s", history.netTxRate, {}, (v) => `${formatBytes(v)}/s`, sparkWidth, "collecting data...", graphMode));
|
|
2029
|
+
lines.push(graphRow("Net RX/s", history.netRxRate, {}, (v) => `${formatBytes(v)}/s`, sparkWidth, "collecting data...", graphMode, contentWidth));
|
|
2030
|
+
lines.push(graphRow("Net TX/s", history.netTxRate, {}, (v) => `${formatBytes(v)}/s`, sparkWidth, "collecting data...", graphMode, contentWidth));
|
|
1687
2031
|
if (history.powerWatts.length) {
|
|
1688
|
-
lines.push(graphRow("Power W", history.powerWatts, {}, (v) => `${v.toFixed(1)} W`, sparkWidth, "collecting data...", graphMode));
|
|
2032
|
+
lines.push(graphRow("Power W", history.powerWatts, {}, (v) => `${v.toFixed(1)} W`, sparkWidth, "collecting data...", graphMode, contentWidth));
|
|
1689
2033
|
}
|
|
1690
2034
|
if (history.rxPacketRate.length) {
|
|
1691
|
-
lines.push(graphRow("RX pkt/s", history.rxPacketRate, {}, (v) => `${v.toFixed(0)} pkt/s`, sparkWidth, "collecting data...", graphMode));
|
|
2035
|
+
lines.push(graphRow("RX pkt/s", history.rxPacketRate, {}, (v) => `${v.toFixed(0)} pkt/s`, sparkWidth, "collecting data...", graphMode, contentWidth));
|
|
1692
2036
|
}
|
|
1693
2037
|
if (history.txPacketRate.length) {
|
|
1694
|
-
lines.push(graphRow("TX pkt/s", history.txPacketRate, {}, (v) => `${v.toFixed(0)} pkt/s`, sparkWidth, "collecting data...", graphMode));
|
|
2038
|
+
lines.push(graphRow("TX pkt/s", history.txPacketRate, {}, (v) => `${v.toFixed(0)} pkt/s`, sparkWidth, "collecting data...", graphMode, contentWidth));
|
|
1695
2039
|
}
|
|
1696
2040
|
const title = `Graphs \xB7 mode:${graphMode} \xB7 last ${sampleCount} sample${sampleCount === 1 ? "" : "s"}`;
|
|
1697
2041
|
return panel(title, lines, width, theme.graphs, theme.border).join("\n");
|
|
1698
2042
|
}
|
|
1699
|
-
function graphRow(label, values, bounds, fmt, sparkWidth = 40, emptyMessage = "collecting data...", mode = "spark") {
|
|
2043
|
+
function graphRow(label, values, bounds, fmt, sparkWidth = 40, emptyMessage = "collecting data...", mode = "spark", contentWidth = 76) {
|
|
1700
2044
|
if (!values.length) {
|
|
1701
2045
|
return `${chalk4.dim(label.padEnd(10))} ${chalk4.dim(emptyMessage)}`;
|
|
1702
2046
|
}
|
|
@@ -1708,7 +2052,8 @@ function graphRow(label, values, bounds, fmt, sparkWidth = 40, emptyMessage = "c
|
|
|
1708
2052
|
const pct = (current - bounds.min) / (bounds.max - bounds.min || 1) * 100;
|
|
1709
2053
|
color = pct > 90 ? chalk4.red : pct > 70 ? chalk4.yellow : chalk4.green;
|
|
1710
2054
|
}
|
|
1711
|
-
|
|
2055
|
+
const raw = `${chalk4.dim(label.padEnd(10))}${color(spark)} ${chalk4.bold(fmt(current))}`;
|
|
2056
|
+
return fitVisible(raw, contentWidth);
|
|
1712
2057
|
}
|
|
1713
2058
|
function barGraph(values, bounds, width) {
|
|
1714
2059
|
let min = bounds.min ?? Infinity;
|
|
@@ -1889,11 +2234,11 @@ var History = class _History {
|
|
|
1889
2234
|
};
|
|
1890
2235
|
|
|
1891
2236
|
// src/state/config.ts
|
|
1892
|
-
import
|
|
1893
|
-
import
|
|
1894
|
-
import
|
|
1895
|
-
var CONFIG_DIR =
|
|
1896
|
-
var CONFIG_FILE =
|
|
2237
|
+
import fs2 from "fs";
|
|
2238
|
+
import os3 from "os";
|
|
2239
|
+
import path2 from "path";
|
|
2240
|
+
var CONFIG_DIR = path2.join(os3.homedir(), ".config", "pyre");
|
|
2241
|
+
var CONFIG_FILE = path2.join(CONFIG_DIR, "config.json");
|
|
1897
2242
|
var DEFAULT_CONFIG = {
|
|
1898
2243
|
theme: "default",
|
|
1899
2244
|
interval: 2,
|
|
@@ -1907,6 +2252,9 @@ var DEFAULT_CONFIG = {
|
|
|
1907
2252
|
mouseEnabled: true,
|
|
1908
2253
|
cpuAlertPct: 90,
|
|
1909
2254
|
tempAlertC: 95,
|
|
2255
|
+
watchdogProcess: "",
|
|
2256
|
+
watchdogCpu: 80,
|
|
2257
|
+
watchdogMem: 80,
|
|
1910
2258
|
notificationsEnabled: true,
|
|
1911
2259
|
p2pPort: 9876,
|
|
1912
2260
|
p2pPassword: "pyre",
|
|
@@ -1930,8 +2278,8 @@ var DEFAULT_CONFIG = {
|
|
|
1930
2278
|
};
|
|
1931
2279
|
function readConfig() {
|
|
1932
2280
|
try {
|
|
1933
|
-
if (!
|
|
1934
|
-
const raw =
|
|
2281
|
+
if (!fs2.existsSync(CONFIG_FILE)) return { ...DEFAULT_CONFIG };
|
|
2282
|
+
const raw = fs2.readFileSync(CONFIG_FILE, "utf-8");
|
|
1935
2283
|
const parsed = JSON.parse(raw);
|
|
1936
2284
|
return deepMerge(DEFAULT_CONFIG, parsed);
|
|
1937
2285
|
} catch {
|
|
@@ -1940,11 +2288,11 @@ function readConfig() {
|
|
|
1940
2288
|
}
|
|
1941
2289
|
function writeConfig(config3) {
|
|
1942
2290
|
try {
|
|
1943
|
-
if (!
|
|
1944
|
-
|
|
2291
|
+
if (!fs2.existsSync(CONFIG_DIR)) {
|
|
2292
|
+
fs2.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
1945
2293
|
}
|
|
1946
2294
|
const merged = deepMerge(readConfig(), config3);
|
|
1947
|
-
|
|
2295
|
+
fs2.writeFileSync(CONFIG_FILE, JSON.stringify(merged, null, 2) + "\n");
|
|
1948
2296
|
} catch {
|
|
1949
2297
|
}
|
|
1950
2298
|
}
|
|
@@ -2032,6 +2380,9 @@ var state = {
|
|
|
2032
2380
|
],
|
|
2033
2381
|
CPU_ALERT_PCT: config.cpuAlertPct,
|
|
2034
2382
|
TEMP_ALERT_C: config.tempAlertC,
|
|
2383
|
+
watchdogProcess: config.watchdogProcess || "",
|
|
2384
|
+
watchdogCpu: config.watchdogCpu || 80,
|
|
2385
|
+
watchdogMem: config.watchdogMem || 80,
|
|
2035
2386
|
notificationsEnabled: config.notificationsEnabled,
|
|
2036
2387
|
alerted: false,
|
|
2037
2388
|
activePanel: "grid",
|
|
@@ -2086,10 +2437,10 @@ function getToggleKey(opt) {
|
|
|
2086
2437
|
}
|
|
2087
2438
|
|
|
2088
2439
|
// src/live/export.ts
|
|
2089
|
-
import
|
|
2090
|
-
import
|
|
2440
|
+
import fs3 from "fs";
|
|
2441
|
+
import path3 from "path";
|
|
2091
2442
|
function ensureDir(dir) {
|
|
2092
|
-
if (!
|
|
2443
|
+
if (!fs3.existsSync(dir)) fs3.mkdirSync(dir, { recursive: true });
|
|
2093
2444
|
}
|
|
2094
2445
|
function timestampForFile() {
|
|
2095
2446
|
return (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
@@ -2102,7 +2453,7 @@ function exportSnapshot() {
|
|
|
2102
2453
|
try {
|
|
2103
2454
|
ensureDir(state.exportDir);
|
|
2104
2455
|
const ext = state.exportFormat === "json" ? "json" : state.exportFormat === "csv" ? "csv" : state.exportFormat === "tsv" ? "tsv" : state.exportFormat === "html" ? "html" : "md";
|
|
2105
|
-
const file =
|
|
2456
|
+
const file = path3.join(state.exportDir, `pyre-${timestampForFile()}.${ext}`);
|
|
2106
2457
|
let content;
|
|
2107
2458
|
switch (state.exportFormat) {
|
|
2108
2459
|
case "json":
|
|
@@ -2121,7 +2472,7 @@ function exportSnapshot() {
|
|
|
2121
2472
|
content = formatMarkdown(state.lastData);
|
|
2122
2473
|
break;
|
|
2123
2474
|
}
|
|
2124
|
-
|
|
2475
|
+
fs3.writeFileSync(file, content);
|
|
2125
2476
|
setStatus(`Exported snapshot -> ${file}`);
|
|
2126
2477
|
} catch (err) {
|
|
2127
2478
|
setStatus(`Export failed: ${err.message}`);
|
|
@@ -2131,8 +2482,8 @@ function startLogging() {
|
|
|
2131
2482
|
try {
|
|
2132
2483
|
ensureDir(state.exportDir);
|
|
2133
2484
|
rotateLogs();
|
|
2134
|
-
const file =
|
|
2135
|
-
state.logStream =
|
|
2485
|
+
const file = path3.join(state.exportDir, `pyre-log-${timestampForFile()}.csv`);
|
|
2486
|
+
state.logStream = fs3.createWriteStream(file, { flags: "a" });
|
|
2136
2487
|
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");
|
|
2137
2488
|
state.logging = true;
|
|
2138
2489
|
setStatus(`Logging every tick -> ${file}`);
|
|
@@ -2146,37 +2497,37 @@ var MAX_LOG_BYTES = 50 * 1024 * 1024;
|
|
|
2146
2497
|
function rotateLogs() {
|
|
2147
2498
|
try {
|
|
2148
2499
|
const dir = state.exportDir;
|
|
2149
|
-
if (!
|
|
2150
|
-
const files =
|
|
2500
|
+
if (!fs3.existsSync(dir)) return;
|
|
2501
|
+
const files = fs3.readdirSync(dir).filter((f) => f.startsWith("pyre-log-") && f.endsWith(".csv")).map((f) => ({ name: f, path: path3.join(dir, f), mtime: fs3.statSync(path3.join(dir, f)).mtimeMs, size: fs3.statSync(path3.join(dir, f)).size })).sort((a, b) => a.mtime - b.mtime);
|
|
2151
2502
|
const now = Date.now();
|
|
2152
2503
|
for (const f of files) {
|
|
2153
2504
|
if (now - f.mtime > MAX_LOG_AGE_MS) {
|
|
2154
2505
|
try {
|
|
2155
|
-
|
|
2506
|
+
fs3.unlinkSync(f.path);
|
|
2156
2507
|
} catch {
|
|
2157
2508
|
}
|
|
2158
2509
|
}
|
|
2159
2510
|
}
|
|
2160
|
-
const remaining =
|
|
2511
|
+
const remaining = fs3.readdirSync(dir).filter((f) => f.startsWith("pyre-log-") && f.endsWith(".csv"));
|
|
2161
2512
|
if (remaining.length > MAX_LOG_COUNT) {
|
|
2162
2513
|
const toDelete = remaining.sort().slice(0, remaining.length - MAX_LOG_COUNT);
|
|
2163
2514
|
for (const f of toDelete) {
|
|
2164
2515
|
try {
|
|
2165
|
-
|
|
2516
|
+
fs3.unlinkSync(path3.join(dir, f));
|
|
2166
2517
|
} catch {
|
|
2167
2518
|
}
|
|
2168
2519
|
}
|
|
2169
2520
|
}
|
|
2170
2521
|
let totalSize = 0;
|
|
2171
2522
|
for (const f of remaining) {
|
|
2172
|
-
totalSize +=
|
|
2523
|
+
totalSize += fs3.statSync(path3.join(dir, f)).size;
|
|
2173
2524
|
}
|
|
2174
2525
|
if (totalSize > MAX_LOG_BYTES) {
|
|
2175
2526
|
const sorted = remaining.sort();
|
|
2176
2527
|
for (const f of sorted) {
|
|
2177
|
-
const sz =
|
|
2528
|
+
const sz = fs3.statSync(path3.join(dir, f)).size;
|
|
2178
2529
|
try {
|
|
2179
|
-
|
|
2530
|
+
fs3.unlinkSync(path3.join(dir, f));
|
|
2180
2531
|
} catch {
|
|
2181
2532
|
}
|
|
2182
2533
|
totalSize -= sz;
|
|
@@ -2326,6 +2677,7 @@ function tableCacheParams() {
|
|
|
2326
2677
|
state.currentTheme,
|
|
2327
2678
|
state.activePanel,
|
|
2328
2679
|
state.treeView ? "1" : "0",
|
|
2680
|
+
state.history.version,
|
|
2329
2681
|
JSON.stringify(state.visiblePanels)
|
|
2330
2682
|
].join("|");
|
|
2331
2683
|
}
|
|
@@ -2344,11 +2696,8 @@ function processRowBudget() {
|
|
|
2344
2696
|
if (state.activePanel === "process") {
|
|
2345
2697
|
return Math.max(10, state.termHeight - 12 - overlayRows);
|
|
2346
2698
|
}
|
|
2347
|
-
const
|
|
2348
|
-
|
|
2349
|
-
const cardRows = Math.ceil(visibleCards / columns);
|
|
2350
|
-
const reserved = 4 + cardRows * 8 + 8 + (state.showGraphs ? 11 : 0) + 5 + overlayRows;
|
|
2351
|
-
return Math.max(5, state.termHeight - reserved);
|
|
2699
|
+
const reserved = 16 + overlayRows;
|
|
2700
|
+
return Math.max(4, state.termHeight - reserved);
|
|
2352
2701
|
}
|
|
2353
2702
|
function p2pPanelLines() {
|
|
2354
2703
|
const events = state.p2pServer ? state.p2pServer.peerEventHistory : state.p2pEvents;
|
|
@@ -2373,13 +2722,15 @@ function p2pPanelLines() {
|
|
|
2373
2722
|
const title = `P2P \xB7 ${state.p2pServerRunning ? "ON" : "OFF"} \xB7 ${events.length} events`;
|
|
2374
2723
|
return { title, body };
|
|
2375
2724
|
}
|
|
2376
|
-
function renderCustomizerOverlay() {
|
|
2725
|
+
function renderCustomizerOverlay(scrollOffset, maxVisibleItems) {
|
|
2377
2726
|
const themesList = Object.keys(THEMES);
|
|
2378
|
-
const
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2727
|
+
const headerLine = chalk5.bgCyan.black.bold(" UI CUSTOMIZER (Use \u2191/\u2193 or J/K to navigate, Enter/Space to toggle, Esc to exit) ");
|
|
2728
|
+
const totalItems = state.CUSTOMIZER_OPTIONS.length;
|
|
2729
|
+
const end = Math.min(scrollOffset + maxVisibleItems, totalItems);
|
|
2730
|
+
const lines = [headerLine];
|
|
2731
|
+
for (let i = scrollOffset; i < end; i++) {
|
|
2732
|
+
const opt = state.CUSTOMIZER_OPTIONS[i];
|
|
2733
|
+
const isSelected = i === state.customizerIndex;
|
|
2383
2734
|
const prefix = isSelected ? chalk5.yellow("\u25B6 ") : " ";
|
|
2384
2735
|
if (opt === "Theme") {
|
|
2385
2736
|
lines.push(`${prefix}${opt}: ${chalk5.bold.green(state.currentTheme)} [${themesList.join(", ")}]`);
|
|
@@ -2403,7 +2754,7 @@ function renderCustomizerOverlay() {
|
|
|
2403
2754
|
const status = isVisible ? chalk5.green("[VISIBLE]") : chalk5.red("[HIDDEN]");
|
|
2404
2755
|
lines.push(`${prefix}${opt}: ${status}`);
|
|
2405
2756
|
}
|
|
2406
|
-
}
|
|
2757
|
+
}
|
|
2407
2758
|
return lines.map((l) => ` ${l}`).join("\n");
|
|
2408
2759
|
}
|
|
2409
2760
|
var _prevFrameLines = [];
|
|
@@ -2429,6 +2780,18 @@ function writeFrame(lines) {
|
|
|
2429
2780
|
}
|
|
2430
2781
|
function render() {
|
|
2431
2782
|
if (!state.lastData) return;
|
|
2783
|
+
const rows = state.termHeight || 24;
|
|
2784
|
+
const cols = state.termWidth || 80;
|
|
2785
|
+
if (cols < 50 || rows < 15) {
|
|
2786
|
+
const msg = chalk5.yellow(`pyre ${rows}x${cols} too small. Resize terminal to >= 50x20.`);
|
|
2787
|
+
const centered = Math.max(0, Math.floor((rows - 2) / 2));
|
|
2788
|
+
const out = [];
|
|
2789
|
+
for (let i = 0; i < centered; i++) out.push("");
|
|
2790
|
+
out.push(msg.padEnd(cols));
|
|
2791
|
+
while (out.length < rows) out.push("");
|
|
2792
|
+
writeFrame(out);
|
|
2793
|
+
return;
|
|
2794
|
+
}
|
|
2432
2795
|
const lines = [];
|
|
2433
2796
|
const theme = THEMES[state.currentTheme] || THEMES.default;
|
|
2434
2797
|
const params = tableCacheParams();
|
|
@@ -2446,7 +2809,9 @@ function render() {
|
|
|
2446
2809
|
treeView: state.treeView,
|
|
2447
2810
|
activePanel: state.activePanel,
|
|
2448
2811
|
anomalies,
|
|
2449
|
-
tempUnit: state.tempUnit
|
|
2812
|
+
tempUnit: state.tempUnit,
|
|
2813
|
+
history: state.history,
|
|
2814
|
+
graphMode: state.graphMode
|
|
2450
2815
|
});
|
|
2451
2816
|
_cachedTableData = state.lastData;
|
|
2452
2817
|
_cachedTableParams = cacheParams;
|
|
@@ -2473,37 +2838,47 @@ function render() {
|
|
|
2473
2838
|
bodyLines.push("");
|
|
2474
2839
|
bodyLines.push(...graphsStr.split("\n"));
|
|
2475
2840
|
}
|
|
2476
|
-
const
|
|
2477
|
-
|
|
2478
|
-
|
|
2841
|
+
const footerLine1 = footerLine()[0] || "";
|
|
2842
|
+
const footerLine22 = footerLine()[1] || "";
|
|
2843
|
+
const footerLine3 = footerLine()[2] || " ";
|
|
2844
|
+
const footerLines = [footerLine1, footerLine22, footerLine3];
|
|
2845
|
+
const footerHeight = 3;
|
|
2846
|
+
const targetBodyHeight = Math.max(1, state.termHeight - footerHeight);
|
|
2479
2847
|
if (state.inputMode === "customizer") {
|
|
2480
|
-
|
|
2848
|
+
const preOverlayCount = bodyLines.length;
|
|
2849
|
+
const availableForOverlay = targetBodyHeight - preOverlayCount;
|
|
2850
|
+
const totalItems = state.CUSTOMIZER_OPTIONS.length;
|
|
2851
|
+
const headerLines = 1;
|
|
2852
|
+
const blankLines = availableForOverlay >= 2 ? 1 : 0;
|
|
2853
|
+
const maxVisibleItems = Math.max(0, availableForOverlay - headerLines - blankLines);
|
|
2854
|
+
const scrollOffset = totalItems > maxVisibleItems ? Math.max(0, Math.min(state.customizerIndex, totalItems - maxVisibleItems)) : 0;
|
|
2855
|
+
if (blankLines > 0) bodyLines.push("");
|
|
2856
|
+
bodyLines.push(...renderCustomizerOverlay(scrollOffset, maxVisibleItems).split("\n"));
|
|
2481
2857
|
} else if (state.inputMode === "filter") {
|
|
2482
|
-
|
|
2858
|
+
bodyLines.push(chalk5.cyan(` Filter processes: ${state.inputBuffer}_`));
|
|
2483
2859
|
} else if (state.inputMode === "kill") {
|
|
2484
|
-
|
|
2860
|
+
bodyLines.push(chalk5.cyan(` Kill PID: ${state.inputBuffer}_ (enter to confirm, esc to cancel)`));
|
|
2485
2861
|
} else if (state.inputMode === "signal") {
|
|
2486
2862
|
const sigIdx = state.SIGNAL_OPTIONS.indexOf(state.inputBuffer);
|
|
2487
2863
|
const sigList = state.SIGNAL_OPTIONS.map((s, i) => i === sigIdx ? chalk5.yellow(s) : chalk5.dim(s)).join(" ");
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2864
|
+
bodyLines.push(chalk5.cyan(` Signal: ${state.inputBuffer}_`));
|
|
2865
|
+
bodyLines.push(chalk5.dim(` ${sigList}`));
|
|
2866
|
+
bodyLines.push(chalk5.dim(" \u2191/\u2193 to select, Enter to confirm, Esc to cancel"));
|
|
2491
2867
|
} else if (state.inputMode === "p2p") {
|
|
2492
|
-
|
|
2868
|
+
bodyLines.push(chalk5.cyan(` P2P password (${state.p2pPort}): ${state.inputBuffer}_ (enter to start, esc to cancel)`));
|
|
2493
2869
|
} else if (state.statusMessage) {
|
|
2494
|
-
|
|
2495
|
-
} else {
|
|
2496
|
-
footerLines.push("");
|
|
2870
|
+
bodyLines.push(` ${state.statusMessage}`);
|
|
2497
2871
|
}
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
bodyLines.length = maxBodyLines;
|
|
2872
|
+
if (bodyLines.length > targetBodyHeight) {
|
|
2873
|
+
bodyLines.length = targetBodyHeight;
|
|
2501
2874
|
}
|
|
2502
|
-
const frameLines = [...bodyLines
|
|
2503
|
-
|
|
2504
|
-
frameLines.
|
|
2875
|
+
const frameLines = [...bodyLines];
|
|
2876
|
+
while (frameLines.length < targetBodyHeight) {
|
|
2877
|
+
frameLines.push("");
|
|
2505
2878
|
}
|
|
2506
|
-
|
|
2879
|
+
frameLines.push(...footerLines);
|
|
2880
|
+
const fittedLines = frameLines.map((l) => fitVisible(l, cols));
|
|
2881
|
+
writeFrame(fittedLines);
|
|
2507
2882
|
}
|
|
2508
2883
|
function footerLine() {
|
|
2509
2884
|
const row1 = [
|
|
@@ -2537,10 +2912,8 @@ function footerLine() {
|
|
|
2537
2912
|
if (state.logging) badges.push(chalk5.red.bold("\u25CF REC"));
|
|
2538
2913
|
if (state.activePanel !== "grid") badges.push(chalk5.cyan.bold(`\u25C9 ${state.activePanel.toUpperCase()}`));
|
|
2539
2914
|
if (state.p2pServerRunning) badges.push(chalk5.green.bold(`P2P ${state.p2pBind || "0.0.0.0"}:${state.p2pPort}`));
|
|
2540
|
-
const badgeStr = badges.join(" ");
|
|
2541
|
-
return
|
|
2542
|
-
${str2} ${badgeStr}` : `${str1}
|
|
2543
|
-
${str2}`;
|
|
2915
|
+
const badgeStr = badges.join(" ") || " ";
|
|
2916
|
+
return [str1, str2, badgeStr];
|
|
2544
2917
|
}
|
|
2545
2918
|
function sendNotification(title, message) {
|
|
2546
2919
|
if (!state.notificationsEnabled) return;
|
|
@@ -2555,19 +2928,30 @@ function checkAlerts(data) {
|
|
|
2555
2928
|
const anomalies = detectAnomalies(data, state.history);
|
|
2556
2929
|
const hasCriticalAnomaly = anomalies.some((a) => a.severity === "critical");
|
|
2557
2930
|
const hasWarningAnomaly = anomalies.some((a) => a.severity === "warning");
|
|
2931
|
+
let watchdogTriggered = false;
|
|
2932
|
+
if (state.watchdogProcess) {
|
|
2933
|
+
const matches = data.processes.filter((p) => p.command.toLowerCase().includes(state.watchdogProcess.toLowerCase()));
|
|
2934
|
+
for (const p of matches) {
|
|
2935
|
+
if (p.cpu >= state.watchdogCpu || p.mem >= state.watchdogMem) {
|
|
2936
|
+
watchdogTriggered = true;
|
|
2937
|
+
break;
|
|
2938
|
+
}
|
|
2939
|
+
}
|
|
2940
|
+
}
|
|
2558
2941
|
const anomalyTriggered = hasCriticalAnomaly || hasWarningAnomaly;
|
|
2559
|
-
if ((hot || anomalyTriggered) && !state.alerted) {
|
|
2942
|
+
if ((hot || anomalyTriggered || watchdogTriggered) && !state.alerted) {
|
|
2560
2943
|
state.alerted = true;
|
|
2561
2944
|
process.stdout.write("\x07");
|
|
2562
2945
|
const reasons = [];
|
|
2563
2946
|
if (data.cpu.usage >= state.CPU_ALERT_PCT) reasons.push(`CPU at ${data.cpu.usage}%`);
|
|
2564
2947
|
if (temp !== null && temp >= state.TEMP_ALERT_C) reasons.push(`Temp at ${formatTempForUnit(temp)}`);
|
|
2948
|
+
if (watchdogTriggered) reasons.push(`Watchdog process "${state.watchdogProcess}" crossed threshold`);
|
|
2565
2949
|
for (const a of anomalies) {
|
|
2566
2950
|
reasons.push(`${a.metric} anomaly (\u03C3=${a.zScore.toFixed(1)})`);
|
|
2567
2951
|
}
|
|
2568
2952
|
setStatus(chalk5.red(`\u26A0 Alert: ${reasons.join("; ")}`), 5e3);
|
|
2569
2953
|
sendNotification("pyre Alert", reasons.join("; "));
|
|
2570
|
-
} else if (!hot && !anomalyTriggered) {
|
|
2954
|
+
} else if (!hot && !anomalyTriggered && !watchdogTriggered) {
|
|
2571
2955
|
state.alerted = false;
|
|
2572
2956
|
}
|
|
2573
2957
|
}
|
|
@@ -2575,7 +2959,7 @@ function checkAlerts(data) {
|
|
|
2575
2959
|
// src/p2p/server.ts
|
|
2576
2960
|
import net from "net";
|
|
2577
2961
|
import tls from "tls";
|
|
2578
|
-
import
|
|
2962
|
+
import fs4 from "fs";
|
|
2579
2963
|
import crypto2 from "crypto";
|
|
2580
2964
|
import chalk6 from "chalk";
|
|
2581
2965
|
|
|
@@ -2720,9 +3104,9 @@ var P2PServer = class {
|
|
|
2720
3104
|
async start() {
|
|
2721
3105
|
if (this.options.auditLog) {
|
|
2722
3106
|
try {
|
|
2723
|
-
|
|
3107
|
+
fs4.mkdirSync(this.options.auditLog, { recursive: true });
|
|
2724
3108
|
const logFile = `${this.options.auditLog}/p2p-audit.log`;
|
|
2725
|
-
this.auditStream =
|
|
3109
|
+
this.auditStream = fs4.createWriteStream(logFile, { flags: "a" });
|
|
2726
3110
|
} catch {
|
|
2727
3111
|
this.log(chalk6.yellow("Warning: could not open audit log"));
|
|
2728
3112
|
}
|
|
@@ -2731,11 +3115,11 @@ var P2PServer = class {
|
|
|
2731
3115
|
const useTLS = !!(this.options.tlsCert && this.options.tlsKey);
|
|
2732
3116
|
if (useTLS) {
|
|
2733
3117
|
const tlsOptions = {
|
|
2734
|
-
key:
|
|
2735
|
-
cert:
|
|
3118
|
+
key: fs4.readFileSync(this.options.tlsKey),
|
|
3119
|
+
cert: fs4.readFileSync(this.options.tlsCert)
|
|
2736
3120
|
};
|
|
2737
3121
|
if (this.options.tlsCA) {
|
|
2738
|
-
tlsOptions.ca =
|
|
3122
|
+
tlsOptions.ca = fs4.readFileSync(this.options.tlsCA);
|
|
2739
3123
|
}
|
|
2740
3124
|
this.server = tls.createServer(tlsOptions, (socket) => this.onConnection(socket));
|
|
2741
3125
|
} else {
|
|
@@ -3003,7 +3387,7 @@ async function startP2PServer(options) {
|
|
|
3003
3387
|
// src/p2p/client.ts
|
|
3004
3388
|
import net2 from "net";
|
|
3005
3389
|
import tls2 from "tls";
|
|
3006
|
-
import
|
|
3390
|
+
import fs5 from "fs";
|
|
3007
3391
|
import crypto3 from "crypto";
|
|
3008
3392
|
import chalk7 from "chalk";
|
|
3009
3393
|
var P2PClient = class {
|
|
@@ -3031,9 +3415,9 @@ var P2PClient = class {
|
|
|
3031
3415
|
async connect() {
|
|
3032
3416
|
if (this.options.auditLog) {
|
|
3033
3417
|
try {
|
|
3034
|
-
|
|
3418
|
+
fs5.mkdirSync(this.options.auditLog, { recursive: true });
|
|
3035
3419
|
const logFile = `${this.options.auditLog}/p2p-client-audit.log`;
|
|
3036
|
-
this.auditStream =
|
|
3420
|
+
this.auditStream = fs5.createWriteStream(logFile, { flags: "a" });
|
|
3037
3421
|
} catch {
|
|
3038
3422
|
console.log(chalk7.yellow("Warning: could not open audit log"));
|
|
3039
3423
|
}
|
|
@@ -3048,7 +3432,7 @@ var P2PClient = class {
|
|
|
3048
3432
|
rejectUnauthorized: !this.options.tlsInsecure
|
|
3049
3433
|
};
|
|
3050
3434
|
if (this.options.tlsCA) {
|
|
3051
|
-
tlsOptions.ca =
|
|
3435
|
+
tlsOptions.ca = fs5.readFileSync(this.options.tlsCA);
|
|
3052
3436
|
}
|
|
3053
3437
|
this.socket = tls2.connect(tlsOptions, () => {
|
|
3054
3438
|
this.running = true;
|
|
@@ -3402,11 +3786,19 @@ function handleInputModeKey(str, key) {
|
|
|
3402
3786
|
}
|
|
3403
3787
|
if (key.name === "backspace") {
|
|
3404
3788
|
state.inputBuffer = state.inputBuffer.slice(0, -1);
|
|
3789
|
+
if (state.inputMode === "filter") {
|
|
3790
|
+
state.processFilter = state.inputBuffer;
|
|
3791
|
+
invalidateTableCache();
|
|
3792
|
+
}
|
|
3405
3793
|
render();
|
|
3406
3794
|
return;
|
|
3407
3795
|
}
|
|
3408
3796
|
if (str && str.length === 1 && !key.ctrl && !key.meta) {
|
|
3409
3797
|
state.inputBuffer += str;
|
|
3798
|
+
if (state.inputMode === "filter") {
|
|
3799
|
+
state.processFilter = state.inputBuffer;
|
|
3800
|
+
invalidateTableCache();
|
|
3801
|
+
}
|
|
3410
3802
|
render();
|
|
3411
3803
|
}
|
|
3412
3804
|
}
|
|
@@ -3579,8 +3971,9 @@ async function startLive(opts2, splashPromise) {
|
|
|
3579
3971
|
handleInputModeKey(str, key);
|
|
3580
3972
|
return;
|
|
3581
3973
|
}
|
|
3582
|
-
|
|
3583
|
-
|
|
3974
|
+
const mouse = key.mouse;
|
|
3975
|
+
if (mouse) {
|
|
3976
|
+
handleMouseClick(mouse.y, mouse.x);
|
|
3584
3977
|
return;
|
|
3585
3978
|
}
|
|
3586
3979
|
if (key.sequence && key.sequence.startsWith("\x1B[M")) {
|
|
@@ -4162,11 +4555,106 @@ async function showSplash(config3) {
|
|
|
4162
4555
|
});
|
|
4163
4556
|
}
|
|
4164
4557
|
|
|
4558
|
+
// src/historyCmd.ts
|
|
4559
|
+
import fs6 from "fs";
|
|
4560
|
+
import path4 from "path";
|
|
4561
|
+
import chalk9 from "chalk";
|
|
4562
|
+
function runHistoryCommand(opts2 = {}) {
|
|
4563
|
+
const days = opts2.days || 7;
|
|
4564
|
+
const dir = opts2.dir || "./pyre-exports";
|
|
4565
|
+
console.log(chalk9.bold(`
|
|
4566
|
+
pyre history \u2014 Past ${days} Days Trend
|
|
4567
|
+
`));
|
|
4568
|
+
if (!fs6.existsSync(dir)) {
|
|
4569
|
+
console.log(chalk9.yellow(` No logs directory found at ${dir}`));
|
|
4570
|
+
return;
|
|
4571
|
+
}
|
|
4572
|
+
const files = fs6.readdirSync(dir).filter((f) => f.startsWith("pyre-log-") && f.endsWith(".csv"));
|
|
4573
|
+
if (files.length === 0) {
|
|
4574
|
+
console.log(chalk9.yellow(` No CSV logs found in ${dir}`));
|
|
4575
|
+
return;
|
|
4576
|
+
}
|
|
4577
|
+
const cutoff = Date.now() - days * 24 * 60 * 60 * 1e3;
|
|
4578
|
+
const cpuPoints = [];
|
|
4579
|
+
const memPoints = [];
|
|
4580
|
+
const tempPoints = [];
|
|
4581
|
+
for (const file of files.sort()) {
|
|
4582
|
+
const fullPath = path4.join(dir, file);
|
|
4583
|
+
try {
|
|
4584
|
+
const content = fs6.readFileSync(fullPath, "utf-8");
|
|
4585
|
+
const lines = content.split("\n").slice(1);
|
|
4586
|
+
for (const line of lines) {
|
|
4587
|
+
if (!line.trim()) continue;
|
|
4588
|
+
const parts = line.split(",");
|
|
4589
|
+
if (parts.length >= 4) {
|
|
4590
|
+
const tsStr = parts[0];
|
|
4591
|
+
const ts = new Date(tsStr).getTime();
|
|
4592
|
+
if (!isNaN(ts) && ts >= cutoff) {
|
|
4593
|
+
const cpu = parseFloat(parts[1]);
|
|
4594
|
+
const mem = parseFloat(parts[2]);
|
|
4595
|
+
const temp = parseFloat(parts[3]);
|
|
4596
|
+
if (!isNaN(cpu)) cpuPoints.push(cpu);
|
|
4597
|
+
if (!isNaN(mem)) memPoints.push(mem);
|
|
4598
|
+
if (!isNaN(temp)) tempPoints.push(temp);
|
|
4599
|
+
}
|
|
4600
|
+
}
|
|
4601
|
+
}
|
|
4602
|
+
} catch {
|
|
4603
|
+
}
|
|
4604
|
+
}
|
|
4605
|
+
if (cpuPoints.length === 0) {
|
|
4606
|
+
console.log(chalk9.yellow(` No historical entries recorded within the last ${days} days.`));
|
|
4607
|
+
return;
|
|
4608
|
+
}
|
|
4609
|
+
console.log(` Data Points: ${cpuPoints.length} logged snapshots`);
|
|
4610
|
+
console.log(` CPU Usage: ${sparkline(cpuPoints, { min: 0, max: 100 })} (${(cpuPoints.reduce((a, b) => a + b, 0) / cpuPoints.length).toFixed(1)}% avg)`);
|
|
4611
|
+
console.log(` Memory: ${sparkline(memPoints, { min: 0, max: 100 })} (${(memPoints.reduce((a, b) => a + b, 0) / memPoints.length).toFixed(1)}% avg)`);
|
|
4612
|
+
if (tempPoints.length) {
|
|
4613
|
+
console.log(` Temperature: ${sparkline(tempPoints)} (${(tempPoints.reduce((a, b) => a + b, 0) / tempPoints.length).toFixed(1)}\xB0C avg)`);
|
|
4614
|
+
}
|
|
4615
|
+
console.log();
|
|
4616
|
+
}
|
|
4617
|
+
|
|
4618
|
+
// src/diffCmd.ts
|
|
4619
|
+
import fs7 from "fs";
|
|
4620
|
+
import chalk10 from "chalk";
|
|
4621
|
+
function runDiffCommand(file1, file2) {
|
|
4622
|
+
console.log(chalk10.bold(`
|
|
4623
|
+
pyre diff \u2014 Snapshot Comparison
|
|
4624
|
+
`));
|
|
4625
|
+
console.log(chalk10.dim(` Snapshot A: ${file1}`));
|
|
4626
|
+
console.log(chalk10.dim(` Snapshot B: ${file2}
|
|
4627
|
+
`));
|
|
4628
|
+
try {
|
|
4629
|
+
const dataA = JSON.parse(fs7.readFileSync(file1, "utf-8"));
|
|
4630
|
+
const dataB = JSON.parse(fs7.readFileSync(file2, "utf-8"));
|
|
4631
|
+
const diffNum = (label, valA, valB, unit = "") => {
|
|
4632
|
+
const delta = valB - valA;
|
|
4633
|
+
const sign = delta > 0 ? "+" : "";
|
|
4634
|
+
const color = delta > 0 ? chalk10.red : delta < 0 ? chalk10.green : chalk10.dim;
|
|
4635
|
+
console.log(` ${label.padEnd(20)} ${valA.toFixed(1)}${unit} \u2794 ${valB.toFixed(1)}${unit} (${color(sign + delta.toFixed(1) + unit)})`);
|
|
4636
|
+
};
|
|
4637
|
+
diffNum("CPU Usage", dataA.cpu.usage, dataB.cpu.usage, "%");
|
|
4638
|
+
diffNum("Memory Usage", dataA.memory.usagePercent, dataB.memory.usagePercent, "%");
|
|
4639
|
+
if (dataA.cpu.temperature && dataB.cpu.temperature) {
|
|
4640
|
+
diffNum("CPU Temp", dataA.cpu.temperature, dataB.cpu.temperature, "\xB0C");
|
|
4641
|
+
}
|
|
4642
|
+
if (dataA.gpu && dataB.gpu) {
|
|
4643
|
+
diffNum("GPU Util", dataA.gpu.utilization, dataB.gpu.utilization, "%");
|
|
4644
|
+
}
|
|
4645
|
+
diffNum("Network RX Packets", dataA.network.rxPackets, dataB.network.rxPackets);
|
|
4646
|
+
diffNum("Network TX Packets", dataA.network.txPackets, dataB.network.txPackets);
|
|
4647
|
+
console.log();
|
|
4648
|
+
} catch (err) {
|
|
4649
|
+
console.log(chalk10.red(`Failed to compare snapshots: ${err.message}`));
|
|
4650
|
+
}
|
|
4651
|
+
}
|
|
4652
|
+
|
|
4165
4653
|
// src/index.ts
|
|
4166
4654
|
var config2 = readConfig();
|
|
4167
|
-
var pkg = JSON.parse(
|
|
4655
|
+
var pkg = JSON.parse(fs8.readFileSync(new URL("../package.json", import.meta.url), "utf-8"));
|
|
4168
4656
|
var program = new Command();
|
|
4169
|
-
program.name("pyre").version(pkg.version).description("Mac system monitoring CLI: interactive live dashboard, stats, graphs, export, packet monitor, battery predictor").option("-j, --json", "Output as JSON").option("--html", "Output as HTML").option("--md", "Output as Markdown").option("-c, --csv", "Output as CSV").option("-t, --tsv", "Output as TSV").option("--detailed", "Include detailed system info and sensor readings").option("--theme <name>", "Default theme for live mode (default, dracula, cyberpunk, monochrome, nord, gruvbox)", config2.theme).option("--interval <seconds>", "Refresh interval for live mode", String(config2.interval)).option("--once", "Show a single static snapshot instead of live feed").option("--out <file>", "Also write the snapshot output to a file (--once/--json/--csv/--tsv modes)").option("--export-dir <dir>", "Directory used for live-mode snapshot exports and logs", config2.exportDir).option("--log", "Start continuous CSV logging immediately when live mode starts").option("--tree", "Show process tree view instead of flat list").option("--sort <key>", "Sort processes by: cpu, mem, pid, user, command, state, threads, runtime", config2.sortMode).option("--packets", "Include packet monitor panel in output").option("--limit <n>", "Max number of processes to include in --once/--json/--csv/--tsv snapshots (0 = all)", "10").option("--alert-cpu <pct>", "CPU usage alert threshold (default: 90)", String(config2.cpuAlertPct)).option("--alert-temp <c>", "CPU temperature alert threshold in Celsius (default: 95)", String(config2.tempAlertC)).option("--temp-unit <unit>", "Temperature display unit: c or f (default: c)", "c").option("--p2p-host <host>", "P2P host address (server: bind address, client: server address)").option("--p2p-port <port>", "P2P port number", "9876").option("--p2p-password <password>", "Password for P2P authentication").option("--p2p-tls", "Enable TLS encryption for P2P connections").option("--p2p-cert <file>", "TLS certificate file (PEM) for P2P server").option("--p2p-key <file>", "TLS private key file (PEM) for P2P server").option("--p2p-ca <file>", "TLS CA certificate file (PEM) for P2P client").option("--p2p-insecure", "Skip TLS certificate verification (client only)").option("--p2p-rate-limit <n>", "Max auth attempts per IP per minute (default: 5)", "5").option("--p2p-allow <ips>", "Comma-separated list of allowed IPs (empty = all)").option("--p2p-deny <ips>", "Comma-separated list of denied IPs").option("--p2p-audit-log <dir>", "Directory for P2P audit logs").option("--p2p-hmac-key <key>", "HMAC key for message signing (default: derived from password)");
|
|
4657
|
+
program.name("pyre").version(pkg.version).description("Mac system monitoring CLI: interactive live dashboard, stats, graphs, export, packet monitor, battery predictor").option("-j, --json", "Output as JSON").option("--html", "Output as HTML").option("--md", "Output as Markdown").option("-c, --csv", "Output as CSV").option("-t, --tsv", "Output as TSV").option("--detailed", "Include detailed system info and sensor readings").option("--theme <name>", "Default theme for live mode (default, dracula, cyberpunk, monochrome, nord, gruvbox)", config2.theme).option("--interval <seconds>", "Refresh interval for live mode", String(config2.interval)).option("--once", "Show a single static snapshot instead of live feed").option("--out <file>", "Also write the snapshot output to a file (--once/--json/--csv/--tsv modes)").option("--export-dir <dir>", "Directory used for live-mode snapshot exports and logs", config2.exportDir).option("--log", "Start continuous CSV logging immediately when live mode starts").option("--tree", "Show process tree view instead of flat list").option("--sort <key>", "Sort processes by: cpu, mem, pid, user, command, state, threads, runtime", config2.sortMode).option("--packets", "Include packet monitor panel in output").option("--limit <n>", "Max number of processes to include in --once/--json/--csv/--tsv snapshots (0 = all)", "10").option("--alert-cpu <pct>", "CPU usage alert threshold (default: 90)", String(config2.cpuAlertPct)).option("--alert-temp <c>", "CPU temperature alert threshold in Celsius (default: 95)", String(config2.tempAlertC)).option("--temp-unit <unit>", "Temperature display unit: c or f (default: c)", "c").option("--p2p-host <host>", "P2P host address (server: bind address, client: server address)").option("--p2p-port <port>", "P2P port number", "9876").option("--p2p-password <password>", "Password for P2P authentication").option("--p2p-tls", "Enable TLS encryption for P2P connections").option("--p2p-cert <file>", "TLS certificate file (PEM) for P2P server").option("--p2p-key <file>", "TLS private key file (PEM) for P2P server").option("--p2p-ca <file>", "TLS CA certificate file (PEM) for P2P client").option("--p2p-insecure", "Skip TLS certificate verification (client only)").option("--p2p-rate-limit <n>", "Max auth attempts per IP per minute (default: 5)", "5").option("--p2p-allow <ips>", "Comma-separated list of allowed IPs (empty = all)").option("--p2p-deny <ips>", "Comma-separated list of denied IPs").option("--p2p-audit-log <dir>", "Directory for P2P audit logs").option("--p2p-hmac-key <key>", "HMAC key for message signing (default: derived from password)").option("--port <port>", "Port number for web server mode", "3000").option("--web-auth", "Enable Basic Authentication for web server").option("--web-user <user>", "Username for web server authentication", "admin").option("--web-pass <pass>", "Password for web server authentication", "pyre");
|
|
4170
4658
|
program.parse(process.argv);
|
|
4171
4659
|
var opts = program.opts();
|
|
4172
4660
|
function isExportMode() {
|
|
@@ -4179,7 +4667,7 @@ function sanitizeHost2(host) {
|
|
|
4179
4667
|
return host.trim().replace(/%$/, "");
|
|
4180
4668
|
}
|
|
4181
4669
|
function detectLocalIP() {
|
|
4182
|
-
const ifaces =
|
|
4670
|
+
const ifaces = os4.networkInterfaces();
|
|
4183
4671
|
for (const [, addrs] of Object.entries(ifaces)) {
|
|
4184
4672
|
if (!addrs) continue;
|
|
4185
4673
|
for (const addr of addrs) {
|
|
@@ -4193,34 +4681,46 @@ function detectLocalIP() {
|
|
|
4193
4681
|
async function runServerCommand() {
|
|
4194
4682
|
const ip = detectLocalIP();
|
|
4195
4683
|
if (!ip) {
|
|
4196
|
-
console.log(
|
|
4684
|
+
console.log(chalk11.red("Could not detect a non-loopback IP address. Connect to a network first."));
|
|
4197
4685
|
process.exit(1);
|
|
4198
4686
|
}
|
|
4199
4687
|
const port = 9876;
|
|
4200
4688
|
const password = "mysecret";
|
|
4201
|
-
console.log(
|
|
4689
|
+
console.log(chalk11.bold(`
|
|
4202
4690
|
pyre server`));
|
|
4203
|
-
console.log(
|
|
4204
|
-
console.log(
|
|
4205
|
-
console.log(
|
|
4691
|
+
console.log(chalk11.dim(` Detected IP: ${ip}`));
|
|
4692
|
+
console.log(chalk11.dim(` Port: ${port}`));
|
|
4693
|
+
console.log(chalk11.dim(` Password: ${password}`));
|
|
4206
4694
|
console.log();
|
|
4207
|
-
console.log(
|
|
4208
|
-
console.log(
|
|
4695
|
+
console.log(chalk11.bold("Run this on THIS machine (server):"));
|
|
4696
|
+
console.log(chalk11.hex("#ff6a39")(`pyre p2p server --p2p-host 0.0.0.0 --p2p-port ${port} --p2p-password ${password}`));
|
|
4209
4697
|
console.log();
|
|
4210
|
-
console.log(
|
|
4211
|
-
console.log(
|
|
4698
|
+
console.log(chalk11.bold("Run this on the OTHER machine (client):"));
|
|
4699
|
+
console.log(chalk11.hex("#ff6a39")(`pyre p2p connect --p2p-host ${ip} --p2p-port ${port} --p2p-password ${password}`));
|
|
4212
4700
|
console.log();
|
|
4213
4701
|
}
|
|
4214
4702
|
async function main() {
|
|
4215
4703
|
const cmd = program.args[0];
|
|
4216
|
-
if (cmd === "
|
|
4217
|
-
|
|
4704
|
+
if (cmd === "history") {
|
|
4705
|
+
const daysIdx = program.args.indexOf("--days");
|
|
4706
|
+
const days = daysIdx !== -1 ? parseInt(program.args[daysIdx + 1], 10) || 7 : 7;
|
|
4707
|
+
runHistoryCommand({ days });
|
|
4708
|
+
return;
|
|
4709
|
+
}
|
|
4710
|
+
if (cmd === "diff") {
|
|
4711
|
+
const file1 = program.args[1];
|
|
4712
|
+
const file2 = program.args[2];
|
|
4713
|
+
if (!file1 || !file2) {
|
|
4714
|
+
console.log(chalk11.red("Usage: pyre diff <snapshot1.json> <snapshot2.json>"));
|
|
4715
|
+
process.exit(1);
|
|
4716
|
+
}
|
|
4717
|
+
runDiffCommand(file1, file2);
|
|
4218
4718
|
return;
|
|
4219
4719
|
}
|
|
4220
4720
|
if (cmd === "ssh") {
|
|
4221
4721
|
const host = program.args[1];
|
|
4222
4722
|
if (!host) {
|
|
4223
|
-
console.log(
|
|
4723
|
+
console.log(chalk11.red("Usage: pyre ssh <host>"));
|
|
4224
4724
|
process.exit(1);
|
|
4225
4725
|
}
|
|
4226
4726
|
await runSshCommand(host);
|
|
@@ -4233,7 +4733,7 @@ async function main() {
|
|
|
4233
4733
|
if (cmd === "bench") {
|
|
4234
4734
|
const benchCmd = program.args.slice(1).join(" ");
|
|
4235
4735
|
if (!benchCmd) {
|
|
4236
|
-
console.log(
|
|
4736
|
+
console.log(chalk11.red("Usage: pyre bench <command>"));
|
|
4237
4737
|
process.exit(1);
|
|
4238
4738
|
}
|
|
4239
4739
|
await runBenchCommand(benchCmd);
|
|
@@ -4245,8 +4745,8 @@ async function main() {
|
|
|
4245
4745
|
const cfg = readConfig();
|
|
4246
4746
|
console.log(JSON.stringify(cfg, null, 2));
|
|
4247
4747
|
} else if (sub === "reset") {
|
|
4248
|
-
|
|
4249
|
-
console.log(
|
|
4748
|
+
fs8.rmSync(CONFIG_FILE, { force: true });
|
|
4749
|
+
console.log(chalk11.green("Config reset to defaults"));
|
|
4250
4750
|
} else {
|
|
4251
4751
|
console.log("Usage: pyre config <show|reset>");
|
|
4252
4752
|
}
|
|
@@ -4260,10 +4760,18 @@ async function main() {
|
|
|
4260
4760
|
const alertTemp = opts.alertTemp ? parseInt(opts.alertTemp, 10) : void 0;
|
|
4261
4761
|
const tempUnit = opts.tempUnit === "f" ? "f" : "c";
|
|
4262
4762
|
if (cmd === "live" || !isExportMode() && !opts.once && cmd !== "p2p") {
|
|
4763
|
+
if (!process.stdout.isTTY && cmd !== "live") {
|
|
4764
|
+
await runWebCommand();
|
|
4765
|
+
return;
|
|
4766
|
+
}
|
|
4263
4767
|
if (cmd === "live") {
|
|
4264
4768
|
program.args.shift();
|
|
4265
4769
|
}
|
|
4266
|
-
const splashPromise = showSplash(
|
|
4770
|
+
const splashPromise = showSplash({
|
|
4771
|
+
enabled: config2.splashEnabled,
|
|
4772
|
+
colorScheme: config2.splashColorScheme,
|
|
4773
|
+
animation: config2.splashAnimation
|
|
4774
|
+
});
|
|
4267
4775
|
const interval = parseFloat(opts.interval) || 2;
|
|
4268
4776
|
await startLive({
|
|
4269
4777
|
interval,
|
|
@@ -4286,7 +4794,7 @@ async function main() {
|
|
|
4286
4794
|
await runP2PConnect();
|
|
4287
4795
|
return;
|
|
4288
4796
|
} else {
|
|
4289
|
-
console.log(
|
|
4797
|
+
console.log(chalk11.red("Usage: pyre p2p <server|connect> [options]"));
|
|
4290
4798
|
process.exit(1);
|
|
4291
4799
|
}
|
|
4292
4800
|
}
|
|
@@ -4302,13 +4810,13 @@ async function main() {
|
|
|
4302
4810
|
else output = formatTable(data, { width: process.stdout.columns || 80, sortBy: opts.sort, treeView: opts.tree ?? config2.treeView, visible: { packets: opts.packets ? true : void 0 } });
|
|
4303
4811
|
console.log(output);
|
|
4304
4812
|
if (opts.out) {
|
|
4305
|
-
|
|
4306
|
-
console.log(
|
|
4813
|
+
fs8.writeFileSync(opts.out, stripAnsi(output));
|
|
4814
|
+
console.log(chalk11.dim(`
|
|
4307
4815
|
Saved to ${opts.out}`));
|
|
4308
4816
|
}
|
|
4309
4817
|
}
|
|
4310
4818
|
main().catch((err) => {
|
|
4311
|
-
console.error(
|
|
4819
|
+
console.error(chalk11.red(`Error: ${err.message}`));
|
|
4312
4820
|
process.exit(1);
|
|
4313
4821
|
});
|
|
4314
4822
|
async function runP2PServer() {
|
|
@@ -4324,15 +4832,15 @@ async function runP2PServer() {
|
|
|
4324
4832
|
const allowList = opts.p2pAllow ? opts.p2pAllow.split(",").map((s) => s.trim()).filter(Boolean) : [];
|
|
4325
4833
|
const denyList = opts.p2pDeny ? opts.p2pDeny.split(",").map((s) => s.trim()).filter(Boolean) : [];
|
|
4326
4834
|
if (!password) {
|
|
4327
|
-
console.log(
|
|
4835
|
+
console.log(chalk11.red("Error: a password is required for P2P server mode. Use --p2p-password <password>"));
|
|
4328
4836
|
process.exit(1);
|
|
4329
4837
|
}
|
|
4330
4838
|
if (tlsCert && !tlsKey) {
|
|
4331
|
-
console.log(
|
|
4839
|
+
console.log(chalk11.red("Error: --p2p-key is required when --p2p-cert is specified"));
|
|
4332
4840
|
process.exit(1);
|
|
4333
4841
|
}
|
|
4334
4842
|
if (tlsKey && !tlsCert) {
|
|
4335
|
-
console.log(
|
|
4843
|
+
console.log(chalk11.red("Error: --p2p-cert is required when --p2p-key is specified"));
|
|
4336
4844
|
process.exit(1);
|
|
4337
4845
|
}
|
|
4338
4846
|
const server = new P2PServer({
|
|
@@ -4352,18 +4860,18 @@ async function runP2PServer() {
|
|
|
4352
4860
|
hmacKey: opts.p2pHmacKey
|
|
4353
4861
|
});
|
|
4354
4862
|
const tlsEnabled = !!(tlsCert && tlsKey);
|
|
4355
|
-
console.log(
|
|
4863
|
+
console.log(chalk11.bold(`
|
|
4356
4864
|
pyre P2P Server`));
|
|
4357
|
-
console.log(
|
|
4358
|
-
console.log(
|
|
4359
|
-
console.log(
|
|
4360
|
-
console.log(
|
|
4361
|
-
console.log(
|
|
4362
|
-
console.log(
|
|
4363
|
-
console.log(
|
|
4364
|
-
if (allowList.length > 0) console.log(
|
|
4365
|
-
if (denyList.length > 0) console.log(
|
|
4366
|
-
console.log(
|
|
4865
|
+
console.log(chalk11.dim(` Host: ${host}`));
|
|
4866
|
+
console.log(chalk11.dim(` Port: ${port}`));
|
|
4867
|
+
console.log(chalk11.dim(` Protocol: ${tlsEnabled ? "TLS" : "TCP"}`));
|
|
4868
|
+
console.log(chalk11.dim(` Password: ${password}`));
|
|
4869
|
+
console.log(chalk11.dim(` Interval: ${interval}s`));
|
|
4870
|
+
console.log(chalk11.dim(` Detailed: ${detailed ? "on" : "off"}`));
|
|
4871
|
+
console.log(chalk11.dim(` Rate limit: ${rateLimit} attempts/min`));
|
|
4872
|
+
if (allowList.length > 0) console.log(chalk11.dim(` Allowed IPs: ${allowList.join(", ")}`));
|
|
4873
|
+
if (denyList.length > 0) console.log(chalk11.dim(` Denied IPs: ${denyList.join(", ")}`));
|
|
4874
|
+
console.log(chalk11.dim(` Waiting for peers...
|
|
4367
4875
|
`));
|
|
4368
4876
|
await server.start();
|
|
4369
4877
|
const rl = readline2.createInterface({ input: process.stdin });
|
|
@@ -4375,7 +4883,7 @@ async function runP2PServer() {
|
|
|
4375
4883
|
process.exit(0);
|
|
4376
4884
|
} else if (trimmed === "status") {
|
|
4377
4885
|
const info = server.info;
|
|
4378
|
-
console.log(
|
|
4886
|
+
console.log(chalk11.dim(` Peers connected: ${info.peersConnected}`));
|
|
4379
4887
|
}
|
|
4380
4888
|
});
|
|
4381
4889
|
process.once("SIGINT", () => {
|
|
@@ -4408,7 +4916,7 @@ async function runP2PConnect() {
|
|
|
4408
4916
|
const tlsCA = opts.p2pCA;
|
|
4409
4917
|
const tlsInsecure = !!opts.p2pInsecure;
|
|
4410
4918
|
if (!password) {
|
|
4411
|
-
console.log(
|
|
4919
|
+
console.log(chalk11.red("Error: a password is required for P2P connect mode. Use --p2p-password <password>"));
|
|
4412
4920
|
process.exit(1);
|
|
4413
4921
|
}
|
|
4414
4922
|
const client = new P2PClient({
|
|
@@ -4422,12 +4930,12 @@ async function runP2PConnect() {
|
|
|
4422
4930
|
hmacKey: opts.p2pHmacKey
|
|
4423
4931
|
});
|
|
4424
4932
|
client.setOnStatus((msg) => {
|
|
4425
|
-
process.stdout.write(`\r${
|
|
4933
|
+
process.stdout.write(`\r${chalk11.dim(msg)}${" ".repeat(40)}\r`);
|
|
4426
4934
|
});
|
|
4427
4935
|
client.setOnData((data) => {
|
|
4428
4936
|
const lines = [];
|
|
4429
|
-
lines.push(
|
|
4430
|
-
lines.push(
|
|
4937
|
+
lines.push(chalk11.bold(` pyre P2P Client \u2014 ${data.header.hostname}`));
|
|
4938
|
+
lines.push(chalk11.dim(` ${data.timestamp}`));
|
|
4431
4939
|
lines.push("");
|
|
4432
4940
|
lines.push(...formatTable(data, {
|
|
4433
4941
|
width: process.stdout.columns || 80,
|
|
@@ -4444,11 +4952,11 @@ async function runP2PConnect() {
|
|
|
4444
4952
|
writeFrame2(lines);
|
|
4445
4953
|
});
|
|
4446
4954
|
const protocolLabel = useTLS ? "TLS" : "TCP";
|
|
4447
|
-
console.log(
|
|
4955
|
+
console.log(chalk11.bold(`
|
|
4448
4956
|
pyre P2P Client`));
|
|
4449
|
-
console.log(
|
|
4450
|
-
console.log(
|
|
4451
|
-
console.log(
|
|
4957
|
+
console.log(chalk11.dim(` Protocol: ${protocolLabel}`));
|
|
4958
|
+
console.log(chalk11.dim(` Connecting to ${host}:${port}...`));
|
|
4959
|
+
console.log(chalk11.dim(` Press Ctrl+C to disconnect
|
|
4452
4960
|
`));
|
|
4453
4961
|
process.stdout.write("\x1B[?25l");
|
|
4454
4962
|
try {
|
|
@@ -4456,23 +4964,23 @@ async function runP2PConnect() {
|
|
|
4456
4964
|
} catch (err) {
|
|
4457
4965
|
const code = err.code;
|
|
4458
4966
|
const msg = err.message || String(err);
|
|
4459
|
-
console.log(
|
|
4967
|
+
console.log(chalk11.red(`Connection failed: ${msg}`));
|
|
4460
4968
|
if (code === "ECONNREFUSED") {
|
|
4461
|
-
console.log(
|
|
4462
|
-
console.log(
|
|
4463
|
-
console.log(
|
|
4464
|
-
console.log(
|
|
4969
|
+
console.log(chalk11.yellow(" Hints:"));
|
|
4970
|
+
console.log(chalk11.dim(" 1. Start the server: pyre p2p server --p2p-host 0.0.0.0 --p2p-port " + port + " --p2p-password <password>"));
|
|
4971
|
+
console.log(chalk11.dim(" 2. Check the server IP with: hostname -I"));
|
|
4972
|
+
console.log(chalk11.dim(" 3. Open the port on the server firewall: sudo ufw allow " + port + "/tcp"));
|
|
4465
4973
|
} else if (code === "ETIMEDOUT") {
|
|
4466
|
-
console.log(
|
|
4467
|
-
console.log(
|
|
4468
|
-
console.log(
|
|
4469
|
-
console.log(
|
|
4974
|
+
console.log(chalk11.yellow(" Hints:"));
|
|
4975
|
+
console.log(chalk11.dim(" 1. Verify the server IP is correct"));
|
|
4976
|
+
console.log(chalk11.dim(" 2. Check that both machines are on the same network/subnet"));
|
|
4977
|
+
console.log(chalk11.dim(" 3. Open the port on the server firewall"));
|
|
4470
4978
|
} else if (code === "EHOSTDOWN" || code === "EHOSTUNREACH") {
|
|
4471
|
-
console.log(
|
|
4472
|
-
console.log(
|
|
4473
|
-
console.log(
|
|
4474
|
-
console.log(
|
|
4475
|
-
console.log(
|
|
4979
|
+
console.log(chalk11.yellow(" Hints:"));
|
|
4980
|
+
console.log(chalk11.dim(" 1. The server at " + host + " is not responding"));
|
|
4981
|
+
console.log(chalk11.dim(" 2. Start the server first: pyre p2p server --p2p-host 0.0.0.0 --p2p-port " + port + " --p2p-password <password>"));
|
|
4982
|
+
console.log(chalk11.dim(" 3. Check connectivity: ping " + host));
|
|
4983
|
+
console.log(chalk11.dim(" 4. Ensure both machines are on the same LAN (ping each other first)"));
|
|
4476
4984
|
}
|
|
4477
4985
|
process.stdout.write("\x1B[?25h");
|
|
4478
4986
|
process.exit(1);
|
|
@@ -4484,41 +4992,21 @@ async function runP2PConnect() {
|
|
|
4484
4992
|
process.exit(0);
|
|
4485
4993
|
});
|
|
4486
4994
|
}
|
|
4487
|
-
async function runInfoCommand() {
|
|
4488
|
-
const data = await collectAll({ detailed: true });
|
|
4489
|
-
const lines = [];
|
|
4490
|
-
lines.push(chalk9.bold(" pyre info"));
|
|
4491
|
-
lines.push("");
|
|
4492
|
-
lines.push(` Hostname: ${data.header.hostname}`);
|
|
4493
|
-
lines.push(` OS: ${data.header.os}`);
|
|
4494
|
-
lines.push(` Uptime: ${data.header.uptime}`);
|
|
4495
|
-
lines.push(` CPU: ${data.cpu.brand}`);
|
|
4496
|
-
lines.push(` Cores: ${data.cpu.physicalCores}/${data.cpu.cores} (phys/log)`);
|
|
4497
|
-
lines.push(` Frequency: ${data.cpu.frequency} MHz`);
|
|
4498
|
-
lines.push(` Memory: ${formatBytes(data.memory.total)}`);
|
|
4499
|
-
if (data.gpu) lines.push(` GPU: ${data.gpu.model} (${formatBytes(data.gpu.memory)})`);
|
|
4500
|
-
if (data.battery) {
|
|
4501
|
-
lines.push(` Battery: ${data.battery.level}% (${data.battery.state})`);
|
|
4502
|
-
lines.push(` Health: ${data.battery.health}`);
|
|
4503
|
-
}
|
|
4504
|
-
lines.push(` Thermal: ${data.thermal.state}`);
|
|
4505
|
-
console.log(lines.join("\n"));
|
|
4506
|
-
}
|
|
4507
4995
|
async function runSshCommand(host) {
|
|
4508
4996
|
const password = opts.p2pPassword || "mysecret";
|
|
4509
4997
|
const cmd = `ssh -o LogLevel=QUIET ${host} "pyre --json --once"`;
|
|
4510
|
-
console.log(
|
|
4998
|
+
console.log(chalk11.bold(`
|
|
4511
4999
|
pyre ssh \u2014 ${host}`));
|
|
4512
|
-
console.log(
|
|
5000
|
+
console.log(chalk11.dim(` Running: ${cmd}
|
|
4513
5001
|
`));
|
|
4514
5002
|
const { execFile: execFile2 } = await import("child_process");
|
|
4515
5003
|
const child = execFile2("ssh", ["-o", "LogLevel=QUIET", host, "pyre", "--json", "--once"], (err, stdout, stderr) => {
|
|
4516
5004
|
if (err) {
|
|
4517
|
-
console.log(
|
|
4518
|
-
console.log(
|
|
5005
|
+
console.log(chalk11.red(` SSH failed: ${err.message}`));
|
|
5006
|
+
console.log(chalk11.dim(" Ensure SSH keys are set up or the host is reachable."));
|
|
4519
5007
|
process.exit(1);
|
|
4520
5008
|
}
|
|
4521
|
-
if (stderr) console.log(
|
|
5009
|
+
if (stderr) console.log(chalk11.dim(stderr));
|
|
4522
5010
|
try {
|
|
4523
5011
|
const remote = JSON.parse(stdout);
|
|
4524
5012
|
console.log(formatTable(remote, { width: process.stdout.columns || 80, sortBy: "cpu", treeView: false, visible: { packets: true } }));
|
|
@@ -4527,7 +5015,7 @@ async function runSshCommand(host) {
|
|
|
4527
5015
|
}
|
|
4528
5016
|
process.exit(0);
|
|
4529
5017
|
});
|
|
4530
|
-
child.stdin
|
|
5018
|
+
child.stdin?.end();
|
|
4531
5019
|
process.once("SIGINT", () => {
|
|
4532
5020
|
child.kill("SIGTERM");
|
|
4533
5021
|
process.exit(0);
|
|
@@ -4535,45 +5023,112 @@ async function runSshCommand(host) {
|
|
|
4535
5023
|
}
|
|
4536
5024
|
async function runWebCommand() {
|
|
4537
5025
|
const http = await import("http");
|
|
4538
|
-
const
|
|
4539
|
-
const
|
|
4540
|
-
|
|
4541
|
-
|
|
4542
|
-
|
|
4543
|
-
|
|
5026
|
+
const port = parseInt(opts.port || process.env.PORT || "3000", 10) || 3e3;
|
|
5027
|
+
const authEnabled = !!opts.webAuth;
|
|
5028
|
+
const authUser = opts.webUser || "admin";
|
|
5029
|
+
const authPass = opts.webPass || "pyre";
|
|
5030
|
+
const server = http.createServer(async (req, res) => {
|
|
5031
|
+
if (authEnabled) {
|
|
5032
|
+
const authHeader = req.headers.authorization;
|
|
5033
|
+
if (!authHeader || !authHeader.startsWith("Basic ")) {
|
|
5034
|
+
res.writeHead(401, {
|
|
5035
|
+
"WWW-Authenticate": 'Basic realm="pyre Web Dashboard"',
|
|
5036
|
+
"Content-Type": "text/plain"
|
|
5037
|
+
});
|
|
5038
|
+
res.end("401 Unauthorized");
|
|
5039
|
+
return;
|
|
5040
|
+
}
|
|
5041
|
+
const credentials = Buffer.from(authHeader.split(" ")[1], "base64").toString("utf-8");
|
|
5042
|
+
const [user, pass] = credentials.split(":");
|
|
5043
|
+
if (user !== authUser || pass !== authPass) {
|
|
5044
|
+
res.writeHead(401, {
|
|
5045
|
+
"WWW-Authenticate": 'Basic realm="pyre Web Dashboard"',
|
|
5046
|
+
"Content-Type": "text/plain"
|
|
5047
|
+
});
|
|
5048
|
+
res.end("401 Unauthorized");
|
|
5049
|
+
return;
|
|
5050
|
+
}
|
|
5051
|
+
}
|
|
5052
|
+
const url = req.url || "/";
|
|
5053
|
+
if (url === "/health" || url === "/api/health") {
|
|
4544
5054
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
5055
|
+
res.end(JSON.stringify({ status: "ok", uptime: process.uptime(), timestamp: (/* @__PURE__ */ new Date()).toISOString() }));
|
|
5056
|
+
return;
|
|
5057
|
+
}
|
|
5058
|
+
if (url === "/api/stream") {
|
|
5059
|
+
res.writeHead(200, {
|
|
5060
|
+
"Content-Type": "text/event-stream",
|
|
5061
|
+
"Cache-Control": "no-cache",
|
|
5062
|
+
"Connection": "keep-alive",
|
|
5063
|
+
"Access-Control-Allow-Origin": "*"
|
|
5064
|
+
});
|
|
5065
|
+
const sendStats = async () => {
|
|
5066
|
+
try {
|
|
5067
|
+
const data = await collectAll({ detailed: true });
|
|
5068
|
+
res.write(`data: ${JSON.stringify(data)}
|
|
5069
|
+
|
|
5070
|
+
`);
|
|
5071
|
+
} catch {
|
|
5072
|
+
}
|
|
5073
|
+
};
|
|
5074
|
+
sendStats();
|
|
5075
|
+
const intervalId = setInterval(sendStats, 2e3);
|
|
5076
|
+
req.on("close", () => {
|
|
5077
|
+
clearInterval(intervalId);
|
|
5078
|
+
});
|
|
5079
|
+
return;
|
|
5080
|
+
}
|
|
5081
|
+
if (url === "/" || url === "/index.html") {
|
|
5082
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
5083
|
+
const indexPath = path5.join(process.cwd(), "index.html");
|
|
5084
|
+
let htmlContent = "";
|
|
5085
|
+
if (fs8.existsSync(indexPath)) {
|
|
5086
|
+
htmlContent = fs8.readFileSync(indexPath, "utf-8");
|
|
5087
|
+
} else {
|
|
5088
|
+
const data = await collectAll({ detailed: true });
|
|
5089
|
+
htmlContent = formatHtml(data);
|
|
5090
|
+
}
|
|
5091
|
+
res.end(htmlContent);
|
|
5092
|
+
} else if (url === "/api" || url === "/api/stats" || url === "/data") {
|
|
5093
|
+
const data = await collectAll({ detailed: true });
|
|
5094
|
+
res.writeHead(200, {
|
|
5095
|
+
"Content-Type": "application/json",
|
|
5096
|
+
"Access-Control-Allow-Origin": "*",
|
|
5097
|
+
"Access-Control-Allow-Methods": "GET, OPTIONS"
|
|
5098
|
+
});
|
|
4545
5099
|
res.end(JSON.stringify(data, null, 2));
|
|
4546
5100
|
} else {
|
|
4547
5101
|
res.writeHead(404);
|
|
4548
5102
|
res.end("Not found");
|
|
4549
5103
|
}
|
|
4550
5104
|
});
|
|
4551
|
-
server.listen(
|
|
4552
|
-
|
|
4553
|
-
|
|
4554
|
-
|
|
4555
|
-
console.log(
|
|
4556
|
-
|
|
4557
|
-
|
|
4558
|
-
|
|
4559
|
-
|
|
4560
|
-
|
|
4561
|
-
|
|
4562
|
-
|
|
4563
|
-
|
|
5105
|
+
server.listen(port, "0.0.0.0", () => {
|
|
5106
|
+
console.log(chalk11.bold(`
|
|
5107
|
+
pyre web server running on port ${port}`));
|
|
5108
|
+
console.log(chalk11.dim(` Dashboard: http://0.0.0.0:${port}/`));
|
|
5109
|
+
console.log(chalk11.dim(` API: http://0.0.0.0:${port}/api`));
|
|
5110
|
+
console.log(chalk11.dim(` SSE Stream: http://0.0.0.0:${port}/api/stream`));
|
|
5111
|
+
if (authEnabled) {
|
|
5112
|
+
console.log(chalk11.yellow(` Authentication: Enabled (${authUser}:${authPass})`));
|
|
5113
|
+
}
|
|
5114
|
+
console.log(chalk11.dim(" Press Ctrl+C to stop\n"));
|
|
5115
|
+
});
|
|
5116
|
+
process.once("SIGINT", () => {
|
|
5117
|
+
server.close();
|
|
5118
|
+
process.exit(0);
|
|
4564
5119
|
});
|
|
4565
5120
|
}
|
|
4566
5121
|
async function runBenchCommand(benchCmd) {
|
|
4567
5122
|
const { spawn } = await import("child_process");
|
|
4568
5123
|
const logDir = opts.exportDir || "./pyre-exports";
|
|
4569
|
-
const file =
|
|
4570
|
-
if (!
|
|
4571
|
-
const stream =
|
|
5124
|
+
const file = path5.join(logDir, `pyre-bench-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.csv`);
|
|
5125
|
+
if (!fs8.existsSync(logDir)) fs8.mkdirSync(logDir, { recursive: true });
|
|
5126
|
+
const stream = fs8.createWriteStream(file, { flags: "a" });
|
|
4572
5127
|
stream.write("timestamp,cpu_usage,mem_usage_percent,temp_c,net_rx_bytes,net_tx_bytes,net_rx_packets,net_tx_packets,connections,thermal_state\n");
|
|
4573
|
-
console.log(
|
|
5128
|
+
console.log(chalk11.bold(`
|
|
4574
5129
|
pyre bench`));
|
|
4575
|
-
console.log(
|
|
4576
|
-
console.log(
|
|
5130
|
+
console.log(chalk11.dim(` Command: ${benchCmd}`));
|
|
5131
|
+
console.log(chalk11.dim(` Log: ${file}
|
|
4577
5132
|
`));
|
|
4578
5133
|
const child = spawn("sh", ["-c", benchCmd]);
|
|
4579
5134
|
child.stdout.on("data", (d) => process.stdout.write(d));
|
|
@@ -4594,7 +5149,7 @@ async function runBenchCommand(benchCmd) {
|
|
|
4594
5149
|
child.on("close", () => {
|
|
4595
5150
|
clearInterval(handle);
|
|
4596
5151
|
stream.end();
|
|
4597
|
-
console.log(
|
|
5152
|
+
console.log(chalk11.green(`
|
|
4598
5153
|
Bench complete. Log saved to ${file}`));
|
|
4599
5154
|
process.exit(0);
|
|
4600
5155
|
});
|