pyre-cli 5.0.0 → 6.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +197 -255
- package/dist/index.js +1454 -281
- package/package.json +1 -1
- package/src/anomalies.ts +102 -0
- package/src/formatters/index.ts +3 -3
- package/src/formatters/output.ts +355 -21
- package/src/formatters/render.ts +316 -97
- package/src/formatters/types.ts +33 -17
- package/src/history.ts +57 -13
- package/src/index.ts +226 -13
- package/src/live/export.ts +64 -7
- package/src/live/input.ts +84 -39
- package/src/live/render.ts +175 -118
- package/src/live/state.ts +76 -70
- package/src/live/types.ts +10 -2
- package/src/monitors/collectors.ts +142 -35
- package/src/monitors/smc.ts +60 -12
- package/src/monitors/types.ts +1 -0
- package/src/p2p/server.test.ts +2 -2
- package/src/sparkline.ts +82 -13
- package/src/state/config.ts +130 -0
- package/Formula/pyre.rb +0 -19
package/dist/index.js
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import
|
|
5
|
-
import
|
|
4
|
+
import fs5 from "fs";
|
|
5
|
+
import os3 from "os";
|
|
6
|
+
import path3 from "path";
|
|
6
7
|
import readline2 from "readline";
|
|
7
8
|
import { Command } from "commander";
|
|
8
9
|
import chalk9 from "chalk";
|
|
9
10
|
|
|
10
11
|
// src/monitors/collectors.ts
|
|
11
12
|
import chalk from "chalk";
|
|
13
|
+
import os from "os";
|
|
12
14
|
|
|
13
15
|
// src/monitors/run.ts
|
|
14
16
|
import { exec } from "child_process";
|
|
@@ -55,6 +57,54 @@ async function getSmcMetrics() {
|
|
|
55
57
|
}
|
|
56
58
|
} catch {
|
|
57
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
|
+
}
|
|
58
108
|
smcCache = { data: result, ts: now };
|
|
59
109
|
return result;
|
|
60
110
|
}
|
|
@@ -65,6 +115,7 @@ function parseSuffix(s) {
|
|
|
65
115
|
// src/monitors/collectors.ts
|
|
66
116
|
var SP_TTL_MS = 1e4;
|
|
67
117
|
var NETSTAT_TTL_MS = 1e3;
|
|
118
|
+
var prevCpuTimes = null;
|
|
68
119
|
var ROUTE_TTL_MS = 5e3;
|
|
69
120
|
var SYSCTL_TTL_MS = 6e4;
|
|
70
121
|
var spGpuCache = null;
|
|
@@ -209,40 +260,109 @@ async function collectGpu(detailed) {
|
|
|
209
260
|
spRaw = (await run("system_profiler SPDisplaysDataType 2>/dev/null")).trim();
|
|
210
261
|
spGpuCache = { raw: spRaw, ts: now };
|
|
211
262
|
}
|
|
212
|
-
|
|
213
|
-
const modelMatch = spRaw.match(/Chipset Model:\s*(.+)/i);
|
|
214
|
-
const model = modelMatch ? modelMatch[1].trim() : "Unknown";
|
|
215
|
-
const memMatch = spRaw.match(/VRAM \(Total\):\s*(.+)/i);
|
|
263
|
+
let model = "Unknown";
|
|
216
264
|
let memory = 0;
|
|
217
|
-
if (
|
|
218
|
-
const
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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 {
|
|
227
277
|
try {
|
|
228
|
-
const
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
+
}
|
|
232
287
|
}
|
|
233
288
|
} catch {
|
|
234
289
|
}
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
if (memory === 0) {
|
|
235
293
|
try {
|
|
236
|
-
const
|
|
237
|
-
|
|
294
|
+
const memSizeStr = await cachedSysctl("hw.memsize", "0");
|
|
295
|
+
const totalBytes = parseInt(memSizeStr, 10) || 0;
|
|
296
|
+
if (totalBytes > 0) {
|
|
297
|
+
memory = totalBytes;
|
|
298
|
+
}
|
|
238
299
|
} catch {
|
|
239
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) {
|
|
240
327
|
try {
|
|
241
|
-
const
|
|
242
|
-
|
|
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
|
+
}
|
|
243
350
|
} catch {
|
|
244
351
|
}
|
|
245
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
|
+
}
|
|
246
366
|
return { model, memory, utilization, temperature, processes: gpuProcesses };
|
|
247
367
|
}
|
|
248
368
|
async function collectSystem() {
|
|
@@ -290,7 +410,25 @@ async function collectCpu() {
|
|
|
290
410
|
if (temps.cpu_die !== void 0) temperature = temps.cpu_die;
|
|
291
411
|
} catch {
|
|
292
412
|
}
|
|
293
|
-
|
|
413
|
+
let coreUsage = [];
|
|
414
|
+
try {
|
|
415
|
+
const cpus = os.cpus();
|
|
416
|
+
const current = cpus.map((cpu) => ({
|
|
417
|
+
total: cpu.times.user + cpu.times.nice + cpu.times.sys + cpu.times.idle + cpu.times.irq,
|
|
418
|
+
idle: cpu.times.idle
|
|
419
|
+
}));
|
|
420
|
+
if (prevCpuTimes) {
|
|
421
|
+
coreUsage = prevCpuTimes.map((prev, i) => {
|
|
422
|
+
const totalDelta = current[i].total - prev.total;
|
|
423
|
+
const idleDelta = current[i].idle - prev.idle;
|
|
424
|
+
if (totalDelta === 0) return 0;
|
|
425
|
+
return Math.round((totalDelta - idleDelta) / totalDelta * 100);
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
prevCpuTimes = current;
|
|
429
|
+
} catch {
|
|
430
|
+
}
|
|
431
|
+
return { brand, cores, physicalCores, frequency, usage, loadAvg, temperature, coreUsage };
|
|
294
432
|
}
|
|
295
433
|
async function collectMemory() {
|
|
296
434
|
const totalBytes = parseInt(await cachedSysctl("hw.memsize", "0")) || 0;
|
|
@@ -449,16 +587,15 @@ async function collectThermal(detailed) {
|
|
|
449
587
|
try {
|
|
450
588
|
const { temps } = await getSmcMetrics();
|
|
451
589
|
cpuDieTemp = temps.cpu_die;
|
|
452
|
-
if (
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
if (
|
|
457
|
-
|
|
590
|
+
if (temps.cpu_die !== void 0) temperatures["cpu_die"] = temps.cpu_die;
|
|
591
|
+
if (temps.gpu_die !== void 0) temperatures["gpu_die"] = temps.gpu_die;
|
|
592
|
+
if (temps.smc_die !== void 0) temperatures["smc_die"] = temps.smc_die;
|
|
593
|
+
for (const [k, v] of Object.entries(temps)) {
|
|
594
|
+
if (temperatures[k] === void 0 && v !== void 0) {
|
|
595
|
+
temperatures[k] = v;
|
|
458
596
|
}
|
|
459
597
|
}
|
|
460
598
|
} catch {
|
|
461
|
-
if (detailed) detail += " (powermetrics unavailable)";
|
|
462
599
|
}
|
|
463
600
|
if (state2 === "Unknown" && cpuDieTemp !== void 0) {
|
|
464
601
|
if (cpuDieTemp >= 100) state2 = "Critical";
|
|
@@ -603,6 +740,55 @@ async function collectTasks(limit = 12) {
|
|
|
603
740
|
// src/formatters/render.ts
|
|
604
741
|
import chalk3 from "chalk";
|
|
605
742
|
|
|
743
|
+
// src/sparkline.ts
|
|
744
|
+
var BLOCKS = " \u2582\u2583\u2584\u2585\u2586\u2587\u2588";
|
|
745
|
+
function sparkline(values, opts2 = {}) {
|
|
746
|
+
if (!values.length) return "";
|
|
747
|
+
let min = opts2.min ?? Infinity;
|
|
748
|
+
let max = opts2.max ?? -Infinity;
|
|
749
|
+
if (opts2.min === void 0 || opts2.max === void 0) {
|
|
750
|
+
for (const v of values) {
|
|
751
|
+
if (v < min) min = v;
|
|
752
|
+
if (v > max) max = v;
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
const range = max - min || 1;
|
|
756
|
+
return values.map((v) => {
|
|
757
|
+
const idx = Math.round((v - min) / range * (BLOCKS.length - 1));
|
|
758
|
+
const clamped = Math.min(BLOCKS.length - 1, Math.max(0, idx));
|
|
759
|
+
return BLOCKS[clamped];
|
|
760
|
+
}).join("");
|
|
761
|
+
}
|
|
762
|
+
function multiRowSparkline(values, height, opts2 = {}) {
|
|
763
|
+
if (!values.length || height <= 0) return Array(height).fill("");
|
|
764
|
+
let min = opts2.min ?? Infinity;
|
|
765
|
+
let max = opts2.max ?? -Infinity;
|
|
766
|
+
if (opts2.min === void 0 || opts2.max === void 0) {
|
|
767
|
+
for (const v of values) {
|
|
768
|
+
if (v < min) min = v;
|
|
769
|
+
if (v > max) max = v;
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
const range = max - min || 1;
|
|
773
|
+
const levels = height * 8;
|
|
774
|
+
const rows = Array.from({ length: height }, () => []);
|
|
775
|
+
for (const v of values) {
|
|
776
|
+
const rawSteps = Math.round((v - min) / range * levels);
|
|
777
|
+
const totalSteps = Math.min(levels, Math.max(0, rawSteps));
|
|
778
|
+
for (let r = 0; r < height; r++) {
|
|
779
|
+
const rowFromBottom = height - 1 - r;
|
|
780
|
+
const minForThisRow = rowFromBottom * 8;
|
|
781
|
+
const stepsInThisRow = Math.min(8, Math.max(0, totalSteps - minForThisRow));
|
|
782
|
+
if (stepsInThisRow === 0) {
|
|
783
|
+
rows[r].push(" ");
|
|
784
|
+
} else {
|
|
785
|
+
rows[r].push(BLOCKS[stepsInThisRow]);
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
return rows.map((r) => r.join(""));
|
|
790
|
+
}
|
|
791
|
+
|
|
606
792
|
// src/formatters/themes.ts
|
|
607
793
|
import chalk2 from "chalk";
|
|
608
794
|
var THEMES = {
|
|
@@ -690,35 +876,83 @@ var THEMES = {
|
|
|
690
876
|
function visLen(s) {
|
|
691
877
|
return s.replace(/\x1b\[[0-9;]*m/g, "").length;
|
|
692
878
|
}
|
|
693
|
-
function
|
|
879
|
+
function fitVisible(s, width) {
|
|
880
|
+
const w = Math.max(0, width);
|
|
694
881
|
const len = visLen(s);
|
|
695
|
-
if (len
|
|
696
|
-
return s + " ".repeat(
|
|
882
|
+
if (len === w) return s;
|
|
883
|
+
if (len < w) return s + " ".repeat(w - len);
|
|
884
|
+
let visibleCount = 0;
|
|
885
|
+
let out = "";
|
|
886
|
+
let inAnsi = false;
|
|
887
|
+
for (let i = 0; i < s.length; i++) {
|
|
888
|
+
const char = s[i];
|
|
889
|
+
if (char === "\x1B") {
|
|
890
|
+
inAnsi = true;
|
|
891
|
+
out += char;
|
|
892
|
+
} else if (inAnsi) {
|
|
893
|
+
out += char;
|
|
894
|
+
if (char >= "a" && char <= "z" || char >= "A" && char <= "Z") {
|
|
895
|
+
inAnsi = false;
|
|
896
|
+
}
|
|
897
|
+
} else {
|
|
898
|
+
if (visibleCount < w) {
|
|
899
|
+
out += char;
|
|
900
|
+
visibleCount++;
|
|
901
|
+
} else {
|
|
902
|
+
break;
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
out += "\x1B[0m";
|
|
907
|
+
const resLen = visLen(out);
|
|
908
|
+
if (resLen < w) {
|
|
909
|
+
out += " ".repeat(w - resLen);
|
|
910
|
+
}
|
|
911
|
+
return out;
|
|
697
912
|
}
|
|
698
913
|
function truncatePlain(s, width) {
|
|
699
914
|
if (s.length <= width) return s;
|
|
700
915
|
return width <= 1 ? s.slice(0, width) : s.slice(0, width - 1) + "\u2026";
|
|
701
916
|
}
|
|
702
|
-
function panel(title, lines, width, accent, borderAccent, height) {
|
|
917
|
+
function panel(title, lines, width, accent, borderAccent, height, rightTitle) {
|
|
703
918
|
const h = height ?? lines.length;
|
|
704
919
|
const contentWidth = Math.max(1, width - 4);
|
|
705
|
-
const
|
|
706
|
-
const
|
|
707
|
-
const
|
|
920
|
+
const leftLabel = title ? ` ${title} ` : "";
|
|
921
|
+
const rightLabel = rightTitle ? ` ${rightTitle} ` : "";
|
|
922
|
+
const innerW = Math.max(0, width - 2);
|
|
923
|
+
let topInner = leftLabel + "\u2500".repeat(Math.max(0, innerW - visLen(leftLabel) - visLen(rightLabel))) + rightLabel;
|
|
924
|
+
if (visLen(topInner) > innerW) {
|
|
925
|
+
topInner = fitVisible(topInner, innerW);
|
|
926
|
+
} else if (visLen(topInner) < innerW) {
|
|
927
|
+
topInner = topInner + "\u2500".repeat(innerW - visLen(topInner));
|
|
928
|
+
}
|
|
929
|
+
const top = borderAccent("\u250C" + topInner + "\u2510");
|
|
930
|
+
const bottom = borderAccent("\u2514" + "\u2500".repeat(innerW) + "\u2518");
|
|
708
931
|
const body = [];
|
|
709
932
|
for (let i = 0; i < h; i++) {
|
|
710
933
|
const line = lines[i] ?? "";
|
|
711
|
-
body.push(`${borderAccent("\u2502")} ${
|
|
934
|
+
body.push(`${borderAccent("\u2502")} ${fitVisible(line, contentWidth)} ${borderAccent("\u2502")}`);
|
|
712
935
|
}
|
|
713
936
|
return [top, ...body, bottom];
|
|
714
937
|
}
|
|
938
|
+
function gaugeBar(percent, width = 12) {
|
|
939
|
+
const w = Math.max(1, width);
|
|
940
|
+
const pct = Math.min(100, Math.max(0, percent));
|
|
941
|
+
const filled = Math.round(pct / 100 * w);
|
|
942
|
+
const empty = w - filled;
|
|
943
|
+
const filledStr = "\u2588".repeat(filled);
|
|
944
|
+
const emptyStr = "\u2591".repeat(empty);
|
|
945
|
+
const color = pct > 85 ? chalk3.red : pct > 65 ? chalk3.yellow : chalk3.cyan;
|
|
946
|
+
return color(filledStr) + chalk3.dim(emptyStr);
|
|
947
|
+
}
|
|
715
948
|
function hstack(blocks, gap = 2, targetWidth) {
|
|
716
949
|
if (!blocks.length) return [];
|
|
717
950
|
const height = Math.max(...blocks.map((b) => b.length));
|
|
718
|
-
const widths = blocks.map((b) =>
|
|
951
|
+
const widths = blocks.map((b) => Math.max(0, ...b.map((l) => visLen(l))));
|
|
719
952
|
const padded = blocks.map((b, i) => {
|
|
720
|
-
const
|
|
721
|
-
const
|
|
953
|
+
const w = widths[i];
|
|
954
|
+
const filler = " ".repeat(w);
|
|
955
|
+
const arr = b.map((line) => fitVisible(line, w));
|
|
722
956
|
while (arr.length < height) arr.push(filler);
|
|
723
957
|
return arr;
|
|
724
958
|
});
|
|
@@ -726,8 +960,7 @@ function hstack(blocks, gap = 2, targetWidth) {
|
|
|
726
960
|
for (let row = 0; row < height; row++) {
|
|
727
961
|
let line = padded.map((a) => a[row]).join(" ".repeat(gap));
|
|
728
962
|
if (targetWidth !== void 0) {
|
|
729
|
-
|
|
730
|
-
line += " ".repeat(pad);
|
|
963
|
+
line = fitVisible(line, targetWidth);
|
|
731
964
|
}
|
|
732
965
|
out.push(line);
|
|
733
966
|
}
|
|
@@ -759,7 +992,8 @@ function capacityColor(percent) {
|
|
|
759
992
|
return chalk3.green;
|
|
760
993
|
}
|
|
761
994
|
function formatBytes(bytes) {
|
|
762
|
-
if (bytes <
|
|
995
|
+
if (isNaN(bytes) || bytes < 0) return "0 B";
|
|
996
|
+
if (bytes < 1024) return `${Math.round(bytes)} B`;
|
|
763
997
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
764
998
|
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
765
999
|
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
|
|
@@ -770,6 +1004,12 @@ function celsiusToFahrenheit(c) {
|
|
|
770
1004
|
function formatTemp(c) {
|
|
771
1005
|
return `${c.toFixed(1)}\xB0C / ${celsiusToFahrenheit(c).toFixed(1)}\xB0F`;
|
|
772
1006
|
}
|
|
1007
|
+
function formatTempUnit(c, unit = "c") {
|
|
1008
|
+
if (unit === "f") {
|
|
1009
|
+
return `${(c * (9 / 5) + 32).toFixed(1)}\xB0F`;
|
|
1010
|
+
}
|
|
1011
|
+
return `${c.toFixed(1)}\xB0C`;
|
|
1012
|
+
}
|
|
773
1013
|
function cpuCard(data, contentWidth) {
|
|
774
1014
|
const lines = [
|
|
775
1015
|
statRow("Brand", truncatePlain(data.cpu.brand, Math.max(4, contentWidth - 8))),
|
|
@@ -778,6 +1018,18 @@ function cpuCard(data, contentWidth) {
|
|
|
778
1018
|
gaugeRow("Usage", data.cpu.usage, contentWidth),
|
|
779
1019
|
statRow("Load", data.cpu.loadAvg.map((l) => l.toFixed(2)).join(" "))
|
|
780
1020
|
];
|
|
1021
|
+
if (data.cpu.coreUsage && data.cpu.coreUsage.length) {
|
|
1022
|
+
const slotW = 2;
|
|
1023
|
+
const maxCores = Math.max(1, Math.floor((contentWidth - 10) / (slotW + 1)));
|
|
1024
|
+
const cores = data.cpu.coreUsage.slice(0, maxCores);
|
|
1025
|
+
const bars = cores.map((u) => {
|
|
1026
|
+
const fill = Math.round(Math.min(100, u) / 100 * slotW);
|
|
1027
|
+
const str = "\u2588".repeat(Math.max(0, fill)) + "\u2591".repeat(slotW - fill);
|
|
1028
|
+
const color = u > 90 ? chalk3.red : u > 70 ? chalk3.yellow : chalk3.green;
|
|
1029
|
+
return color(str);
|
|
1030
|
+
});
|
|
1031
|
+
lines.push(statRow("Cores", bars.join(" ")));
|
|
1032
|
+
}
|
|
781
1033
|
if (data.cpu.temperature) lines.push(statRow("Temp", formatTemp(data.cpu.temperature)));
|
|
782
1034
|
return lines;
|
|
783
1035
|
}
|
|
@@ -853,24 +1105,6 @@ function networkCard(data, contentWidth) {
|
|
|
853
1105
|
statRow("TX", `${formatBytes(data.network.txBytes)} / ${data.network.txPackets} pkt`)
|
|
854
1106
|
];
|
|
855
1107
|
}
|
|
856
|
-
function packetCard(data, contentWidth) {
|
|
857
|
-
if (!data.packets) return null;
|
|
858
|
-
const lines = [];
|
|
859
|
-
lines.push(statRow("Total", `${data.packets.totalPackets} pkt`));
|
|
860
|
-
lines.push(statRow("RX", `${data.packets.rxPackets} pkt`));
|
|
861
|
-
lines.push(statRow("TX", `${data.packets.txPackets} pkt`));
|
|
862
|
-
lines.push(statRow("Conns", `${data.packets.connections} TCP`));
|
|
863
|
-
if (data.packets.topProcesses && data.packets.topProcesses.length) {
|
|
864
|
-
lines.push(statRow("Top Proc", truncatePlain(data.packets.topProcesses[0].command, contentWidth - 10)));
|
|
865
|
-
}
|
|
866
|
-
if (data.packets.allProcesses && data.packets.allProcesses.length > 1) {
|
|
867
|
-
lines.push(statRow("Net Procs", `${data.packets.allProcesses.length} processes`));
|
|
868
|
-
}
|
|
869
|
-
if (data.packets.interfaces && data.packets.interfaces.length) {
|
|
870
|
-
lines.push(statRow("Ifaces", `${data.packets.interfaces.length} active`));
|
|
871
|
-
}
|
|
872
|
-
return lines;
|
|
873
|
-
}
|
|
874
1108
|
function tasksCard(data, contentWidth) {
|
|
875
1109
|
if (!data.tasks || !data.tasks.length) return null;
|
|
876
1110
|
const lines = [];
|
|
@@ -885,7 +1119,7 @@ function header(data, width, badges = [], borderAccent = THEMES.default.border)
|
|
|
885
1119
|
const left = `${chalk3.hex("#ff5500").bold(" PYRE ")} ${chalk3.bold(data.header.hostname)} ${chalk3.dim(data.header.os)} ${chalk3.dim("up " + data.header.uptime)}`;
|
|
886
1120
|
const right = `${chalk3.dim(now)} ${badges.join(" ")}`;
|
|
887
1121
|
const gap = Math.max(1, width - visLen(left) - visLen(right));
|
|
888
|
-
const line = left + " ".repeat(gap) + right;
|
|
1122
|
+
const line = fitVisible(left + " ".repeat(gap) + right, width);
|
|
889
1123
|
return [line, borderAccent("\u2500".repeat(width))];
|
|
890
1124
|
}
|
|
891
1125
|
var TAB_DEFS = [
|
|
@@ -925,9 +1159,26 @@ function tabBar(activePanel, width, theme) {
|
|
|
925
1159
|
const keyHint = chalk3.dim(`[${t.key}]`);
|
|
926
1160
|
return `${keyHint}${label}`;
|
|
927
1161
|
});
|
|
928
|
-
const
|
|
929
|
-
const
|
|
930
|
-
|
|
1162
|
+
const separator = chalk3.dim(" \u2502 ");
|
|
1163
|
+
const joined = rendered.join(separator);
|
|
1164
|
+
const indent = " ";
|
|
1165
|
+
const available = width - visLen(indent);
|
|
1166
|
+
let result = joined;
|
|
1167
|
+
if (visLen(joined) > available) {
|
|
1168
|
+
let used = 0;
|
|
1169
|
+
const kept = [];
|
|
1170
|
+
for (let i = 0; i < rendered.length; i++) {
|
|
1171
|
+
const tabW = visLen(rendered[i]) + (kept.length > 0 ? visLen(separator) : 0);
|
|
1172
|
+
if (used + tabW > available && kept.length > 0) break;
|
|
1173
|
+
kept.push(rendered[i]);
|
|
1174
|
+
used += tabW;
|
|
1175
|
+
}
|
|
1176
|
+
if (kept.length < rendered.length) {
|
|
1177
|
+
result = kept.join(separator) + chalk3.dim("\u22EF");
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
const pad = Math.max(0, available - visLen(result));
|
|
1181
|
+
return fitVisible(chalk3.dim(indent) + result + " ".repeat(pad), width);
|
|
931
1182
|
}
|
|
932
1183
|
function sortProcesses(processes, sortBy) {
|
|
933
1184
|
const copy = [...processes];
|
|
@@ -1115,19 +1366,152 @@ function activeDetailLines(data, activePanel, width, opts2 = {}) {
|
|
|
1115
1366
|
return null;
|
|
1116
1367
|
}
|
|
1117
1368
|
}
|
|
1369
|
+
function anomalyPanel(anomalies, width, theme) {
|
|
1370
|
+
const lines = [];
|
|
1371
|
+
for (const a of anomalies) {
|
|
1372
|
+
const color = a.severity === "critical" ? chalk3.red.bold : chalk3.yellow;
|
|
1373
|
+
const direction = a.zScore > 0 ? "\u2191 spike" : "\u2193 drop";
|
|
1374
|
+
const valStr = a.metric.toLowerCase().includes("net") ? `${formatBytes(a.value)}/s` : `${a.value.toFixed(1)}%`;
|
|
1375
|
+
lines.push(color(` \u26A0 ${a.metric}: ${valStr} (${direction}, \u03C3=${a.zScore.toFixed(1)})`));
|
|
1376
|
+
}
|
|
1377
|
+
return lines;
|
|
1378
|
+
}
|
|
1379
|
+
function btopCpuBox(data, width, theme, opts2) {
|
|
1380
|
+
const contentWidth = width - 4;
|
|
1381
|
+
const rightTitle = `${data.header.hostname} \xB7 up ${data.header.uptime} \xB7 ${data.cpu.usage.toFixed(0)}%`;
|
|
1382
|
+
if (contentWidth >= 65) {
|
|
1383
|
+
const leftW = Math.floor(contentWidth * 0.48);
|
|
1384
|
+
const rightW = contentWidth - leftW - 2;
|
|
1385
|
+
const leftLines = [];
|
|
1386
|
+
leftLines.push(fitVisible(theme.cpu("CPU Usage History"), leftW));
|
|
1387
|
+
const cpuHistory = opts2.history?.cpuUsage || [];
|
|
1388
|
+
if (cpuHistory.length > 1) {
|
|
1389
|
+
const graphRows = multiRowSparkline(cpuHistory, 3, { min: 0, max: 100 });
|
|
1390
|
+
for (const gr of graphRows) {
|
|
1391
|
+
leftLines.push(theme.cpu(fitVisible(gr, leftW)));
|
|
1392
|
+
}
|
|
1393
|
+
} else {
|
|
1394
|
+
leftLines.push(theme.cpu(fitVisible(gaugeBar(data.cpu.usage, leftW), leftW)));
|
|
1395
|
+
leftLines.push(" ".repeat(leftW));
|
|
1396
|
+
leftLines.push(" ".repeat(leftW));
|
|
1397
|
+
}
|
|
1398
|
+
const rightLines = [];
|
|
1399
|
+
rightLines.push(fitVisible(chalk3.bold(truncatePlain(data.cpu.brand, rightW)), rightW));
|
|
1400
|
+
const cpuBarW = Math.max(6, rightW - 18);
|
|
1401
|
+
const tempStr = data.cpu.temperature ? formatTempUnit(data.cpu.temperature, opts2.tempUnit) : "";
|
|
1402
|
+
rightLines.push(fitVisible(`CPU ${gaugeBar(data.cpu.usage, cpuBarW)} ${data.cpu.usage.toFixed(0).padStart(3)}% ${tempStr}`, rightW));
|
|
1403
|
+
const loadStr = data.cpu.loadAvg.map((l) => l.toFixed(2)).join(" ");
|
|
1404
|
+
rightLines.push(fitVisible(`${chalk3.dim("Load:")} ${loadStr}`, rightW));
|
|
1405
|
+
if (data.cpu.coreUsage && data.cpu.coreUsage.length) {
|
|
1406
|
+
const coreColW = Math.floor((rightW - 1) / 2);
|
|
1407
|
+
const half = Math.ceil(data.cpu.coreUsage.length / 2);
|
|
1408
|
+
for (let i = 0; i < Math.min(2, half); i++) {
|
|
1409
|
+
const u1 = data.cpu.coreUsage[i];
|
|
1410
|
+
const u2 = data.cpu.coreUsage[i + half];
|
|
1411
|
+
const c1BarW = Math.max(3, coreColW - 8);
|
|
1412
|
+
const c1Str = `C${i} ${gaugeBar(u1, c1BarW)} ${Math.round(u1)}%`;
|
|
1413
|
+
const c2Str = u2 !== void 0 ? `C${i + half} ${gaugeBar(u2, c1BarW)} ${Math.round(u2)}%` : "";
|
|
1414
|
+
rightLines.push(fitVisible(fitVisible(c1Str, coreColW) + " " + fitVisible(c2Str, coreColW), rightW));
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
if (data.gpu) {
|
|
1418
|
+
rightLines.push(fitVisible(`GPU ${gaugeBar(data.gpu.utilization, cpuBarW)} ${data.gpu.utilization}% ${formatBytes(data.gpu.memory)}`, rightW));
|
|
1419
|
+
}
|
|
1420
|
+
const combinedBody = hstack([leftLines, rightLines], 2, contentWidth);
|
|
1421
|
+
return panel("1 cpu", combinedBody, width, theme.cpu, theme.border, combinedBody.length, rightTitle);
|
|
1422
|
+
} else {
|
|
1423
|
+
const lines = [];
|
|
1424
|
+
lines.push(chalk3.bold(truncatePlain(data.cpu.brand, contentWidth)));
|
|
1425
|
+
lines.push(`CPU ${gaugeBar(data.cpu.usage, Math.max(4, contentWidth - 12))} ${data.cpu.usage.toFixed(0)}%`);
|
|
1426
|
+
if (opts2.history?.cpuUsage.length) {
|
|
1427
|
+
lines.push(theme.cpu(sparkline(opts2.history.cpuUsage, { min: 0, max: 100 })));
|
|
1428
|
+
}
|
|
1429
|
+
lines.push(`Load: ${data.cpu.loadAvg.map((l) => l.toFixed(2)).join(" ")}`);
|
|
1430
|
+
return panel("1 cpu", lines, width, theme.cpu, theme.border, lines.length, rightTitle);
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
function btopLeftColumn(data, width, theme, opts2) {
|
|
1434
|
+
const contentWidth = width - 4;
|
|
1435
|
+
const out = [];
|
|
1436
|
+
const memLines = [];
|
|
1437
|
+
memLines.push(`RAM [${gaugeBar(data.memory.usagePercent, Math.max(4, contentWidth - 12))}] ${data.memory.usagePercent}%`);
|
|
1438
|
+
memLines.push(`Used: ${formatBytes(data.memory.used)} / ${formatBytes(data.memory.total)}`);
|
|
1439
|
+
memLines.push(`Free: ${formatBytes(data.memory.free)} \xB7 Cache: ${formatBytes(data.memory.total - data.memory.free - data.memory.used)}`);
|
|
1440
|
+
if (data.memory.swapTotal > 0) {
|
|
1441
|
+
const swapPct = Math.round(data.memory.swapUsed / data.memory.swapTotal * 100);
|
|
1442
|
+
memLines.push(`Swap [${gaugeBar(swapPct, Math.max(4, contentWidth - 12))}] ${swapPct}%`);
|
|
1443
|
+
}
|
|
1444
|
+
out.push(...panel("2 mem", memLines, width, theme.mem, theme.border, memLines.length, `${formatBytes(data.memory.used)}`));
|
|
1445
|
+
if (data.disk.length) {
|
|
1446
|
+
const diskLines = [];
|
|
1447
|
+
for (const d of data.disk.slice(0, 2)) {
|
|
1448
|
+
const capNum = parseInt(d.capacity, 10) || 0;
|
|
1449
|
+
const mountShort = truncatePlain(d.mountpoint, 8);
|
|
1450
|
+
diskLines.push(`${mountShort} ${d.used}/${d.size} [${gaugeBar(capNum, Math.max(3, contentWidth - 18))}] ${d.capacity}`);
|
|
1451
|
+
}
|
|
1452
|
+
out.push(...panel("0 disk io", diskLines, width, theme.disk, theme.border, diskLines.length));
|
|
1453
|
+
}
|
|
1454
|
+
const netLines = [];
|
|
1455
|
+
const rxRate = opts2.history?.netRxRate.length ? opts2.history.netRxRate[opts2.history.netRxRate.length - 1] : 0;
|
|
1456
|
+
const txRate = opts2.history?.netTxRate.length ? opts2.history.netTxRate[opts2.history.netTxRate.length - 1] : 0;
|
|
1457
|
+
netLines.push(`down \u25BC ${formatBytes(rxRate)}/s (${formatBytes(data.network.rxBytes)})`);
|
|
1458
|
+
if (opts2.history?.netRxRate.length) {
|
|
1459
|
+
netLines.push(chalk3.cyan(fitVisible(sparkline(opts2.history.netRxRate, {}), contentWidth)));
|
|
1460
|
+
}
|
|
1461
|
+
netLines.push(`up \u25B2 ${formatBytes(txRate)}/s (${formatBytes(data.network.txBytes)})`);
|
|
1462
|
+
if (opts2.history?.netTxRate.length) {
|
|
1463
|
+
netLines.push(theme.network(fitVisible(sparkline(opts2.history.netTxRate, {}), contentWidth)));
|
|
1464
|
+
}
|
|
1465
|
+
out.push(...panel("7 net", netLines, width, theme.network, theme.border, netLines.length, `${data.network.interface}`));
|
|
1466
|
+
return out;
|
|
1467
|
+
}
|
|
1468
|
+
function btopProcBox(data, width, targetHeight, theme, opts2) {
|
|
1469
|
+
const contentWidth = width - 4;
|
|
1470
|
+
const filtered = opts2.filter ? data.processes.filter((p) => p.command.toLowerCase().includes(opts2.filter.toLowerCase())) : data.processes;
|
|
1471
|
+
const sorted = sortProcesses(filtered, opts2.sortBy ?? "cpu");
|
|
1472
|
+
const headerRowOverhead = 2;
|
|
1473
|
+
const tableHeaderOverhead = 2;
|
|
1474
|
+
const availableRows = Math.max(1, targetHeight - headerRowOverhead - tableHeaderOverhead);
|
|
1475
|
+
const limited = sorted.slice(0, availableRows);
|
|
1476
|
+
const pidW = 6, userW = 7, thrW = 3, memW = 6, cpuW = 6;
|
|
1477
|
+
const fixedW = pidW + userW + thrW + memW + cpuW + 5;
|
|
1478
|
+
const cmdW = Math.max(8, contentWidth - fixedW);
|
|
1479
|
+
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));
|
|
1480
|
+
if (opts2.treeView) {
|
|
1481
|
+
const treeLines = buildProcessTree(limited);
|
|
1482
|
+
const rightTitle2 = opts2.filter ? `filter: "${opts2.filter}"` : `tree \xB7 sort: ${opts2.sortBy || "cpu"}`;
|
|
1483
|
+
return panel("P proc", [head, theme.border("\u2500".repeat(contentWidth)), ...treeLines], width, theme.process, theme.border, targetHeight - 2, rightTitle2);
|
|
1484
|
+
}
|
|
1485
|
+
const rows = limited.map((p) => {
|
|
1486
|
+
const cpuVal = p.cpu.toFixed(1) + "%";
|
|
1487
|
+
const cpuStr = p.cpu > 50 ? chalk3.red.bold(cpuVal.padStart(cpuW)) : p.cpu > 20 ? chalk3.yellow(cpuVal.padStart(cpuW)) : chalk3.green(cpuVal.padStart(cpuW));
|
|
1488
|
+
const memStr = (p.mem.toFixed(1) + "%").padStart(memW);
|
|
1489
|
+
const userStr = truncatePlain(p.user, userW).padEnd(userW);
|
|
1490
|
+
const thrStr = String(p.threads).padStart(thrW);
|
|
1491
|
+
const pidStr = String(p.pid).padEnd(pidW);
|
|
1492
|
+
const cmdStr = truncatePlain(p.command, cmdW).padEnd(cmdW);
|
|
1493
|
+
return `${pidStr} ${cmdStr} ${userStr} ${thrStr} ${memStr} ${cpuStr}`;
|
|
1494
|
+
});
|
|
1495
|
+
const lines = [head, theme.border("\u2500".repeat(contentWidth)), ...rows];
|
|
1496
|
+
if (rows.length === 0) {
|
|
1497
|
+
lines.push(chalk3.dim(`No processes match "${opts2.filter || ""}"`));
|
|
1498
|
+
}
|
|
1499
|
+
const rightTitle = opts2.filter ? `filter: "${opts2.filter}"` : `sort: ${opts2.sortBy || "cpu"}`;
|
|
1500
|
+
return panel("P proc", lines, width, theme.process, theme.border, targetHeight - 2, rightTitle);
|
|
1501
|
+
}
|
|
1118
1502
|
function formatTable(data, opts2 = {}) {
|
|
1119
1503
|
const width = clampWidth(opts2.width);
|
|
1120
1504
|
const activePanel = opts2.activePanel || "grid";
|
|
1121
|
-
const columns = gridColumns(width);
|
|
1122
|
-
const gap = 2;
|
|
1123
|
-
const cardWidth = Math.floor((width - gap * (columns - 1)) / columns);
|
|
1124
|
-
const contentWidth = cardWidth - 4;
|
|
1125
1505
|
const themeName = opts2.theme || "default";
|
|
1126
1506
|
const theme = THEMES[themeName] || THEMES.default;
|
|
1127
|
-
const vis = opts2.visible ?? {};
|
|
1128
1507
|
const out = [];
|
|
1129
1508
|
out.push(...header(data, width, [], theme.border));
|
|
1130
1509
|
out.push("");
|
|
1510
|
+
const anomalies = opts2.anomalies ?? [];
|
|
1511
|
+
if (anomalies.length && activePanel === "grid") {
|
|
1512
|
+
out.push(...anomalyPanel(anomalies, width, theme));
|
|
1513
|
+
out.push("");
|
|
1514
|
+
}
|
|
1131
1515
|
out.push(tabBar(activePanel, width, theme));
|
|
1132
1516
|
out.push("");
|
|
1133
1517
|
if (activePanel !== "grid") {
|
|
@@ -1137,107 +1521,340 @@ function formatTable(data, opts2 = {}) {
|
|
|
1137
1521
|
}
|
|
1138
1522
|
return out.join("\n");
|
|
1139
1523
|
}
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1524
|
+
out.push(...btopCpuBox(data, width, theme, opts2));
|
|
1525
|
+
out.push("");
|
|
1526
|
+
if (width >= 85) {
|
|
1527
|
+
const leftColW = Math.max(32, Math.floor(width * 0.38));
|
|
1528
|
+
const rightColW = width - leftColW - 2;
|
|
1529
|
+
const leftLines = btopLeftColumn(data, leftColW, theme, opts2);
|
|
1530
|
+
const procLines = btopProcBox(data, rightColW, leftLines.length, theme, opts2);
|
|
1531
|
+
out.push(...hstack([leftLines, procLines], 2, width));
|
|
1532
|
+
} else {
|
|
1533
|
+
const leftLines = btopLeftColumn(data, width, theme, opts2);
|
|
1534
|
+
out.push(...leftLines);
|
|
1535
|
+
out.push("");
|
|
1536
|
+
const procLines = btopProcBox(data, width, 12, theme, opts2);
|
|
1537
|
+
out.push(...procLines);
|
|
1150
1538
|
}
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1539
|
+
return out.join("\n");
|
|
1540
|
+
}
|
|
1541
|
+
function clampWidth(width) {
|
|
1542
|
+
if (!width || Number.isNaN(width)) return 80;
|
|
1543
|
+
return Math.max(40, Math.min(width, 240));
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
// src/formatters/output.ts
|
|
1547
|
+
import chalk4 from "chalk";
|
|
1548
|
+
function formatJson(data) {
|
|
1549
|
+
return JSON.stringify(data, null, 2);
|
|
1550
|
+
}
|
|
1551
|
+
function formatHtml(data) {
|
|
1552
|
+
const rows = (items) => items.map((i) => `<tr><td class="label">${escapeHtml(i.label)}</td><td class="value">${escapeHtml(i.value)}</td></tr>`).join("\n");
|
|
1553
|
+
const sections = [];
|
|
1554
|
+
sections.push(`<h2>System</h2>
|
|
1555
|
+
<table>
|
|
1556
|
+
${rows([
|
|
1557
|
+
{ label: "Hostname", value: data.header.hostname },
|
|
1558
|
+
{ label: "OS", value: data.header.os },
|
|
1559
|
+
{ label: "Uptime", value: data.header.uptime },
|
|
1560
|
+
{ label: "Timestamp", value: data.timestamp }
|
|
1561
|
+
]).trim()}
|
|
1562
|
+
</table>`);
|
|
1563
|
+
sections.push(`<h2>CPU</h2>
|
|
1564
|
+
<table>
|
|
1565
|
+
${rows([
|
|
1566
|
+
{ label: "Brand", value: data.cpu.brand },
|
|
1567
|
+
{ label: "Cores", value: `${data.cpu.physicalCores}/${data.cpu.cores}` },
|
|
1568
|
+
{ label: "Frequency", value: `${data.cpu.frequency} MHz` },
|
|
1569
|
+
{ label: "Usage", value: `${data.cpu.usage}%` },
|
|
1570
|
+
{ label: "Load Avg", value: data.cpu.loadAvg.map((l) => l.toFixed(2)).join(" ") },
|
|
1571
|
+
...data.cpu.temperature ? [{ label: "Temperature", value: `${data.cpu.temperature.toFixed(1)}\xB0C` }] : []
|
|
1572
|
+
]).trim()}
|
|
1573
|
+
</table>`);
|
|
1574
|
+
sections.push(`<h2>Memory</h2>
|
|
1575
|
+
<table>
|
|
1576
|
+
${rows([
|
|
1577
|
+
{ label: "Total", value: formatBytes(data.memory.total) },
|
|
1578
|
+
{ label: "Used", value: formatBytes(data.memory.used) },
|
|
1579
|
+
{ label: "Free", value: formatBytes(data.memory.free) },
|
|
1580
|
+
{ label: "Usage", value: `${data.memory.usagePercent}%` },
|
|
1581
|
+
{ label: "Swap", value: `${formatBytes(data.memory.swapUsed)} / ${formatBytes(data.memory.swapTotal)}` }
|
|
1582
|
+
]).trim()}
|
|
1583
|
+
</table>`);
|
|
1584
|
+
if (data.gpu) {
|
|
1585
|
+
sections.push(`<h2>GPU</h2>
|
|
1586
|
+
<table>
|
|
1587
|
+
${rows([
|
|
1588
|
+
{ label: "Model", value: data.gpu.model },
|
|
1589
|
+
{ label: "Memory", value: formatBytes(data.gpu.memory) },
|
|
1590
|
+
{ label: "Utilization", value: `${data.gpu.utilization}%` },
|
|
1591
|
+
...data.gpu.temperature ? [{ label: "Temperature", value: `${data.gpu.temperature.toFixed(1)}\xB0C` }] : [],
|
|
1592
|
+
{ label: "Processes", value: String(data.gpu.processes) }
|
|
1593
|
+
]).trim()}
|
|
1594
|
+
</table>`);
|
|
1154
1595
|
}
|
|
1155
|
-
if (
|
|
1156
|
-
const
|
|
1157
|
-
if (
|
|
1596
|
+
if (data.power) {
|
|
1597
|
+
const powerRows = [];
|
|
1598
|
+
if (data.power.cpuWatts !== void 0) powerRows.push({ label: "CPU", value: `${data.power.cpuWatts.toFixed(2)} W` });
|
|
1599
|
+
if (data.power.gpuWatts !== void 0) powerRows.push({ label: "GPU", value: `${data.power.gpuWatts.toFixed(2)} W` });
|
|
1600
|
+
if (data.power.combinedWatts !== void 0) powerRows.push({ label: "Combined", value: `${data.power.combinedWatts.toFixed(2)} W` });
|
|
1601
|
+
if (powerRows.length) {
|
|
1602
|
+
sections.push(`<h2>Power</h2>
|
|
1603
|
+
<table>
|
|
1604
|
+
${rows(powerRows).trim()}
|
|
1605
|
+
</table>`);
|
|
1606
|
+
}
|
|
1158
1607
|
}
|
|
1159
|
-
if (
|
|
1160
|
-
|
|
1608
|
+
if (data.battery) {
|
|
1609
|
+
const battRows = [
|
|
1610
|
+
{ label: "Level", value: `${data.battery.level}%` },
|
|
1611
|
+
{ label: "State", value: data.battery.state },
|
|
1612
|
+
{ label: "Time Remaining", value: data.battery.timeRemaining },
|
|
1613
|
+
{ label: "Health", value: data.battery.health },
|
|
1614
|
+
{ label: "Power Source", value: data.battery.powerSource }
|
|
1615
|
+
];
|
|
1616
|
+
if (data.battery.condition) battRows.push({ label: "Condition", value: data.battery.condition });
|
|
1617
|
+
if (data.battery.maxCapacityPercent !== void 0) battRows.push({ label: "Max Capacity", value: `${data.battery.maxCapacityPercent}%` });
|
|
1618
|
+
if (data.battery.cycles !== void 0) battRows.push({ label: "Cycles", value: String(data.battery.cycles) });
|
|
1619
|
+
if (data.battery.estimatedTimeToEmpty) battRows.push({ label: "Est. Empty", value: data.battery.estimatedTimeToEmpty });
|
|
1620
|
+
if (data.battery.dischargeRatePerHour !== void 0) battRows.push({ label: "Discharge Rate", value: `${data.battery.dischargeRatePerHour}%/h` });
|
|
1621
|
+
sections.push(`<h2>Battery</h2>
|
|
1622
|
+
<table>
|
|
1623
|
+
${rows(battRows).trim()}
|
|
1624
|
+
</table>`);
|
|
1625
|
+
}
|
|
1626
|
+
sections.push(`<h2>Thermal</h2>
|
|
1627
|
+
<table>
|
|
1628
|
+
${rows([
|
|
1629
|
+
{ label: "State", value: data.thermal.state },
|
|
1630
|
+
{ label: "Pressure", value: ["Nominal", "Fair", "Serious", "Critical"][data.thermal.pressureLevel] || "Unknown" },
|
|
1631
|
+
...data.thermal.temperatures ? Object.entries(data.thermal.temperatures).flatMap(
|
|
1632
|
+
([k, v]) => v !== null && v !== void 0 ? [{ label: k.replace(/_/g, " "), value: `${v.toFixed(1)}\xB0C` }] : []
|
|
1633
|
+
) : []
|
|
1634
|
+
]).trim()}
|
|
1635
|
+
</table>`);
|
|
1636
|
+
sections.push(`<h2>Network</h2>
|
|
1637
|
+
<table>
|
|
1638
|
+
${rows([
|
|
1639
|
+
{ label: "Interface", value: `${data.network.interface} (${data.network.ip})` },
|
|
1640
|
+
{ label: "RX", value: `${formatBytes(data.network.rxBytes)} / ${data.network.rxPackets} pkt` },
|
|
1641
|
+
{ label: "TX", value: `${formatBytes(data.network.txBytes)} / ${data.network.txPackets} pkt` }
|
|
1642
|
+
]).trim()}
|
|
1643
|
+
</table>`);
|
|
1644
|
+
if (data.packets) {
|
|
1645
|
+
sections.push(`<h2>Packets</h2>
|
|
1646
|
+
<table>
|
|
1647
|
+
${rows([
|
|
1648
|
+
{ label: "Total", value: `${data.packets.totalPackets} pkt` },
|
|
1649
|
+
{ label: "RX", value: `${data.packets.rxPackets} pkt` },
|
|
1650
|
+
{ label: "TX", value: `${data.packets.txPackets} pkt` },
|
|
1651
|
+
{ label: "Connections", value: `${data.packets.connections} TCP` }
|
|
1652
|
+
]).trim()}
|
|
1653
|
+
</table>`);
|
|
1654
|
+
}
|
|
1655
|
+
if (data.tasks.length) {
|
|
1656
|
+
const taskRows = data.tasks.slice(0, 20).map((t) => ({
|
|
1657
|
+
label: `${t.pid}`,
|
|
1658
|
+
value: `${t.user.padEnd(10)} ${t.cpu.toFixed(1).padStart(6)}% ${t.mem.toFixed(1).padStart(6)}% ${t.state} ${t.command}`
|
|
1659
|
+
}));
|
|
1660
|
+
sections.push(`<h2>Tasks</h2>
|
|
1661
|
+
<table>
|
|
1662
|
+
${rows(taskRows).trim()}
|
|
1663
|
+
</table>`);
|
|
1664
|
+
}
|
|
1665
|
+
if (data.disk.length) {
|
|
1666
|
+
const diskRows = data.disk.map((d) => ({
|
|
1667
|
+
label: d.mountpoint,
|
|
1668
|
+
value: `${d.used} / ${d.size} (${d.capacity})`
|
|
1669
|
+
}));
|
|
1670
|
+
sections.push(`<h2>Disk</h2>
|
|
1671
|
+
<table>
|
|
1672
|
+
${rows(diskRows).trim()}
|
|
1673
|
+
</table>`);
|
|
1674
|
+
}
|
|
1675
|
+
if (data.processes.length) {
|
|
1676
|
+
const procRows = data.processes.slice(0, 50).map((p) => ({
|
|
1677
|
+
label: String(p.pid),
|
|
1678
|
+
value: `${p.user.padEnd(10)} ${p.cpu.toFixed(1).padStart(6)}% ${p.mem.toFixed(1).padStart(6)}% ${p.state} ${p.command}`
|
|
1679
|
+
}));
|
|
1680
|
+
sections.push(`<h2>Processes</h2>
|
|
1681
|
+
<table>
|
|
1682
|
+
${rows(procRows).trim()}
|
|
1683
|
+
</table>`);
|
|
1684
|
+
}
|
|
1685
|
+
return `<!DOCTYPE html>
|
|
1686
|
+
<html lang="en">
|
|
1687
|
+
<head>
|
|
1688
|
+
<meta charset="UTF-8">
|
|
1689
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1690
|
+
<title>pyre \u2014 ${data.header.hostname}</title>
|
|
1691
|
+
<style>
|
|
1692
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #1e1e2e; color: #cdd6f4; margin: 0; padding: 24px; }
|
|
1693
|
+
h1 { color: #ff6a39; margin-bottom: 4px; }
|
|
1694
|
+
.meta { color: #a6adc8; margin-bottom: 24px; }
|
|
1695
|
+
h2 { color: #89b4fa; margin-top: 28px; border-bottom: 1px solid #313244; padding-bottom: 6px; }
|
|
1696
|
+
table { width: 100%; border-collapse: collapse; margin-bottom: 16px; }
|
|
1697
|
+
td { padding: 6px 10px; border-bottom: 1px solid #313244; vertical-align: top; }
|
|
1698
|
+
td.label { color: #a6adc8; width: 22%; font-weight: 500; }
|
|
1699
|
+
td.value { color: #cdd6f4; }
|
|
1700
|
+
tr:last-child td { border-bottom: none; }
|
|
1701
|
+
</style>
|
|
1702
|
+
</head>
|
|
1703
|
+
<body>
|
|
1704
|
+
<h1>PYRE</h1>
|
|
1705
|
+
<div class="meta">${escapeHtml(data.header.hostname)} \xB7 ${escapeHtml(data.header.os)} \xB7 up ${escapeHtml(data.header.uptime)} \xB7 ${escapeHtml(data.timestamp)}</div>
|
|
1706
|
+
${sections.join("\n ")}
|
|
1707
|
+
</body>
|
|
1708
|
+
</html>`;
|
|
1709
|
+
}
|
|
1710
|
+
function formatMarkdown(data) {
|
|
1711
|
+
const lines = [];
|
|
1712
|
+
lines.push(`# pyre \u2014 ${data.header.hostname}`);
|
|
1713
|
+
lines.push("");
|
|
1714
|
+
lines.push(`**${data.header.os}** \xB7 up ${data.header.uptime} \xB7 ${data.timestamp}`);
|
|
1715
|
+
lines.push("");
|
|
1716
|
+
lines.push("## System");
|
|
1717
|
+
lines.push("");
|
|
1718
|
+
lines.push("| Property | Value |");
|
|
1719
|
+
lines.push("|---|---|");
|
|
1720
|
+
lines.push(`| Hostname | ${mdCell(data.header.hostname)} |`);
|
|
1721
|
+
lines.push(`| OS | ${mdCell(data.header.os)} |`);
|
|
1722
|
+
lines.push(`| Uptime | ${mdCell(data.header.uptime)} |`);
|
|
1723
|
+
lines.push("");
|
|
1724
|
+
lines.push("## CPU");
|
|
1725
|
+
lines.push("");
|
|
1726
|
+
lines.push("| Property | Value |");
|
|
1727
|
+
lines.push("|---|---|");
|
|
1728
|
+
lines.push(`| Brand | ${mdCell(data.cpu.brand)} |`);
|
|
1729
|
+
lines.push(`| Cores | ${mdCell(`${data.cpu.physicalCores}/${data.cpu.cores}`)} |`);
|
|
1730
|
+
lines.push(`| Frequency | ${mdCell(`${data.cpu.frequency} MHz`)} |`);
|
|
1731
|
+
lines.push(`| Usage | ${mdCell(`${data.cpu.usage}%`)} |`);
|
|
1732
|
+
lines.push(`| Load Avg | ${mdCell(data.cpu.loadAvg.map((l) => l.toFixed(2)).join(" "))} |`);
|
|
1733
|
+
if (data.cpu.temperature) lines.push(`| Temperature | ${mdCell(`${data.cpu.temperature.toFixed(1)}\xB0C`)} |`);
|
|
1734
|
+
lines.push("");
|
|
1735
|
+
lines.push("## Memory");
|
|
1736
|
+
lines.push("");
|
|
1737
|
+
lines.push("| Property | Value |");
|
|
1738
|
+
lines.push("|---|---|");
|
|
1739
|
+
lines.push(`| Total | ${mdCell(formatBytes(data.memory.total))} |`);
|
|
1740
|
+
lines.push(`| Used | ${mdCell(formatBytes(data.memory.used))} |`);
|
|
1741
|
+
lines.push(`| Free | ${mdCell(formatBytes(data.memory.free))} |`);
|
|
1742
|
+
lines.push(`| Usage | ${mdCell(`${data.memory.usagePercent}%`)} |`);
|
|
1743
|
+
lines.push(`| Swap | ${mdCell(`${formatBytes(data.memory.swapUsed)} / ${formatBytes(data.memory.swapTotal)}`)} |`);
|
|
1744
|
+
lines.push("");
|
|
1745
|
+
if (data.gpu) {
|
|
1746
|
+
lines.push("## GPU");
|
|
1747
|
+
lines.push("");
|
|
1748
|
+
lines.push("| Property | Value |");
|
|
1749
|
+
lines.push("|---|---|");
|
|
1750
|
+
lines.push(`| Model | ${mdCell(data.gpu.model)} |`);
|
|
1751
|
+
lines.push(`| Memory | ${mdCell(formatBytes(data.gpu.memory))} |`);
|
|
1752
|
+
lines.push(`| Utilization | ${mdCell(`${data.gpu.utilization}%`)} |`);
|
|
1753
|
+
if (data.gpu.temperature) lines.push(`| Temperature | ${mdCell(`${data.gpu.temperature.toFixed(1)}\xB0C`)} |`);
|
|
1754
|
+
lines.push(`| Processes | ${mdCell(String(data.gpu.processes))} |`);
|
|
1755
|
+
lines.push("");
|
|
1161
1756
|
}
|
|
1162
|
-
if (
|
|
1163
|
-
|
|
1757
|
+
if (data.power) {
|
|
1758
|
+
const pw = [];
|
|
1759
|
+
if (data.power.cpuWatts !== void 0) pw.push(`CPU ${data.power.cpuWatts.toFixed(2)} W`);
|
|
1760
|
+
if (data.power.gpuWatts !== void 0) pw.push(`GPU ${data.power.gpuWatts.toFixed(2)} W`);
|
|
1761
|
+
if (data.power.combinedWatts !== void 0) pw.push(`Combined ${data.power.combinedWatts.toFixed(2)} W`);
|
|
1762
|
+
if (pw.length) {
|
|
1763
|
+
lines.push("## Power");
|
|
1764
|
+
lines.push("");
|
|
1765
|
+
lines.push("| Metric | Value |");
|
|
1766
|
+
lines.push("|---|---|");
|
|
1767
|
+
if (data.power.cpuWatts !== void 0) lines.push(`| CPU | ${mdCell(`${data.power.cpuWatts.toFixed(2)} W`)} |`);
|
|
1768
|
+
if (data.power.gpuWatts !== void 0) lines.push(`| GPU | ${mdCell(`${data.power.gpuWatts.toFixed(2)} W`)} |`);
|
|
1769
|
+
if (data.power.combinedWatts !== void 0) lines.push(`| Combined | ${mdCell(`${data.power.combinedWatts.toFixed(2)} W`)} |`);
|
|
1770
|
+
lines.push("");
|
|
1771
|
+
}
|
|
1164
1772
|
}
|
|
1165
|
-
if (
|
|
1166
|
-
|
|
1167
|
-
|
|
1773
|
+
if (data.battery) {
|
|
1774
|
+
lines.push("## Battery");
|
|
1775
|
+
lines.push("");
|
|
1776
|
+
lines.push("| Property | Value |");
|
|
1777
|
+
lines.push("|---|---|");
|
|
1778
|
+
lines.push(`| Level | ${mdCell(`${data.battery.level}%`)} |`);
|
|
1779
|
+
lines.push(`| State | ${mdCell(data.battery.state)} |`);
|
|
1780
|
+
lines.push(`| Time Remaining | ${mdCell(data.battery.timeRemaining)} |`);
|
|
1781
|
+
lines.push(`| Health | ${mdCell(data.battery.health)} |`);
|
|
1782
|
+
lines.push(`| Power Source | ${mdCell(data.battery.powerSource)} |`);
|
|
1783
|
+
if (data.battery.condition) lines.push(`| Condition | ${mdCell(data.battery.condition)} |`);
|
|
1784
|
+
if (data.battery.maxCapacityPercent !== void 0) lines.push(`| Max Capacity | ${mdCell(`${data.battery.maxCapacityPercent}%`)} |`);
|
|
1785
|
+
if (data.battery.cycles !== void 0) lines.push(`| Cycles | ${mdCell(String(data.battery.cycles))} |`);
|
|
1786
|
+
if (data.battery.estimatedTimeToEmpty) lines.push(`| Est. Empty | ${mdCell(data.battery.estimatedTimeToEmpty)} |`);
|
|
1787
|
+
if (data.battery.dischargeRatePerHour !== void 0) lines.push(`| Discharge Rate | ${mdCell(`${data.battery.dischargeRatePerHour}%/h`)} |`);
|
|
1788
|
+
lines.push("");
|
|
1168
1789
|
}
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1790
|
+
lines.push("## Thermal");
|
|
1791
|
+
lines.push("");
|
|
1792
|
+
lines.push("| Property | Value |");
|
|
1793
|
+
lines.push("|---|---|");
|
|
1794
|
+
lines.push(`| State | ${mdCell(data.thermal.state)} |`);
|
|
1795
|
+
lines.push(`| Pressure | ${mdCell(["Nominal", "Fair", "Serious", "Critical"][data.thermal.pressureLevel] || "Unknown")} |`);
|
|
1796
|
+
if (data.thermal.temperatures) {
|
|
1797
|
+
for (const [k, v] of Object.entries(data.thermal.temperatures)) {
|
|
1798
|
+
if (v !== null && v !== void 0) lines.push(`| ${mdCell(k.replace(/_/g, " "))} | ${mdCell(`${v.toFixed(1)}\xB0C`)} |`);
|
|
1799
|
+
}
|
|
1172
1800
|
}
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1801
|
+
lines.push("");
|
|
1802
|
+
lines.push("## Network");
|
|
1803
|
+
lines.push("");
|
|
1804
|
+
lines.push("| Property | Value |");
|
|
1805
|
+
lines.push("|---|---|");
|
|
1806
|
+
lines.push(`| Interface | ${mdCell(`${data.network.interface} (${data.network.ip})`)} |`);
|
|
1807
|
+
lines.push(`| RX | ${mdCell(`${formatBytes(data.network.rxBytes)} / ${data.network.rxPackets} pkt`)} |`);
|
|
1808
|
+
lines.push(`| TX | ${mdCell(`${formatBytes(data.network.txBytes)} / ${data.network.txPackets} pkt`)} |`);
|
|
1809
|
+
lines.push("");
|
|
1810
|
+
if (data.packets) {
|
|
1811
|
+
lines.push("## Packets");
|
|
1812
|
+
lines.push("");
|
|
1813
|
+
lines.push("| Property | Value |");
|
|
1814
|
+
lines.push("|---|---|");
|
|
1815
|
+
lines.push(`| Total | ${mdCell(`${data.packets.totalPackets} pkt`)} |`);
|
|
1816
|
+
lines.push(`| RX | ${mdCell(`${data.packets.rxPackets} pkt`)} |`);
|
|
1817
|
+
lines.push(`| TX | ${mdCell(`${data.packets.txPackets} pkt`)} |`);
|
|
1818
|
+
lines.push(`| Connections | ${mdCell(`${data.packets.connections} TCP`)} |`);
|
|
1819
|
+
lines.push("");
|
|
1179
1820
|
}
|
|
1180
|
-
if (
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1821
|
+
if (data.tasks.length) {
|
|
1822
|
+
lines.push("## Tasks");
|
|
1823
|
+
lines.push("");
|
|
1824
|
+
lines.push("| PID | User | CPU% | MEM% | State | Command |");
|
|
1825
|
+
lines.push("|---|---|---|---|---|---|");
|
|
1826
|
+
for (const t of data.tasks.slice(0, 20)) {
|
|
1827
|
+
lines.push(`| ${t.pid} | ${mdCell(t.user)} | ${t.cpu.toFixed(1)} | ${t.mem.toFixed(1)} | ${t.state} | ${mdCell(t.command)} |`);
|
|
1828
|
+
}
|
|
1829
|
+
lines.push("");
|
|
1184
1830
|
}
|
|
1185
|
-
if (
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
if (treeLines.length) {
|
|
1193
|
-
out.push(...panel(procTitle + " \xB7 tree", treeLines, width, theme.process, theme.border));
|
|
1194
|
-
} else if (opts2.filter) {
|
|
1195
|
-
out.push(...panel(procTitle, [chalk3.dim(`No processes match "${opts2.filter}"`)], width, theme.process, theme.border));
|
|
1196
|
-
}
|
|
1197
|
-
} else if (limited.length) {
|
|
1198
|
-
out.push(...panel(procTitle, processTableLines(limited, width - 4, theme.border), width, theme.process, theme.border));
|
|
1199
|
-
} else if (opts2.filter) {
|
|
1200
|
-
out.push(...panel(procTitle, [chalk3.dim(`No processes match "${opts2.filter}"`)], width, theme.process, theme.border));
|
|
1831
|
+
if (data.disk.length) {
|
|
1832
|
+
lines.push("## Disk");
|
|
1833
|
+
lines.push("");
|
|
1834
|
+
lines.push("| Mountpoint | Used / Size | Capacity |");
|
|
1835
|
+
lines.push("|---|---|---|");
|
|
1836
|
+
for (const d of data.disk) {
|
|
1837
|
+
lines.push(`| ${mdCell(d.mountpoint)} | ${mdCell(`${d.used} / ${d.size}`)} | ${mdCell(d.capacity)} |`);
|
|
1201
1838
|
}
|
|
1839
|
+
lines.push("");
|
|
1202
1840
|
}
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
if (width >= 150) return 3;
|
|
1211
|
-
if (width >= 96) return 2;
|
|
1212
|
-
return 1;
|
|
1213
|
-
}
|
|
1214
|
-
|
|
1215
|
-
// src/formatters/output.ts
|
|
1216
|
-
import chalk4 from "chalk";
|
|
1217
|
-
|
|
1218
|
-
// src/sparkline.ts
|
|
1219
|
-
var BLOCKS = "\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
|
|
1220
|
-
function sparkline(values, opts2 = {}) {
|
|
1221
|
-
if (!values.length) return "";
|
|
1222
|
-
let min = opts2.min ?? Infinity;
|
|
1223
|
-
let max = opts2.max ?? -Infinity;
|
|
1224
|
-
if (opts2.min === void 0 || opts2.max === void 0) {
|
|
1225
|
-
for (const v of values) {
|
|
1226
|
-
if (v < min) min = v;
|
|
1227
|
-
if (v > max) max = v;
|
|
1841
|
+
if (data.processes.length) {
|
|
1842
|
+
lines.push("## Processes");
|
|
1843
|
+
lines.push("");
|
|
1844
|
+
lines.push("| PID | User | CPU% | MEM% | State | Command |");
|
|
1845
|
+
lines.push("|---|---|---|---|---|---|");
|
|
1846
|
+
for (const p of data.processes.slice(0, 50)) {
|
|
1847
|
+
lines.push(`| ${p.pid} | ${mdCell(p.user)} | ${p.cpu.toFixed(1)} | ${p.mem.toFixed(1)} | ${p.state} | ${mdCell(p.command)} |`);
|
|
1228
1848
|
}
|
|
1849
|
+
lines.push("");
|
|
1229
1850
|
}
|
|
1230
|
-
|
|
1231
|
-
return values.map((v) => {
|
|
1232
|
-
const idx = Math.round((v - min) / range * (BLOCKS.length - 1));
|
|
1233
|
-
const clamped = Math.min(BLOCKS.length - 1, Math.max(0, idx));
|
|
1234
|
-
return BLOCKS[clamped];
|
|
1235
|
-
}).join("");
|
|
1851
|
+
return lines.join("\n");
|
|
1236
1852
|
}
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1853
|
+
function escapeHtml(s) {
|
|
1854
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
1855
|
+
}
|
|
1856
|
+
function mdCell(s) {
|
|
1857
|
+
return String(s).replace(/\|/g, "\\|");
|
|
1241
1858
|
}
|
|
1242
1859
|
function formatCsv(data) {
|
|
1243
1860
|
const rows = [
|
|
@@ -1247,6 +1864,9 @@ function formatCsv(data) {
|
|
|
1247
1864
|
["cpu", "loadAvg_1m", String(data.cpu.loadAvg[0])],
|
|
1248
1865
|
["cpu", "loadAvg_5m", String(data.cpu.loadAvg[1])],
|
|
1249
1866
|
["cpu", "loadAvg_15m", String(data.cpu.loadAvg[2])],
|
|
1867
|
+
["cpu", "cores", String(data.cpu.cores)],
|
|
1868
|
+
["cpu", "physicalCores", String(data.cpu.physicalCores)],
|
|
1869
|
+
["cpu", "frequency", String(data.cpu.frequency)],
|
|
1250
1870
|
["memory", "total", String(data.memory.total)],
|
|
1251
1871
|
["memory", "used", String(data.memory.used)],
|
|
1252
1872
|
["memory", "usagePercent", String(data.memory.usagePercent)],
|
|
@@ -1254,11 +1874,7 @@ function formatCsv(data) {
|
|
|
1254
1874
|
["memory", "swapTotal", String(data.memory.swapTotal)],
|
|
1255
1875
|
["thermal", "state", data.thermal.state],
|
|
1256
1876
|
["thermal", "cpuTempC", data.thermal.temperatures?.cpu_die ? String(data.thermal.temperatures.cpu_die) : ""],
|
|
1257
|
-
[
|
|
1258
|
-
"thermal",
|
|
1259
|
-
"cpuTempF",
|
|
1260
|
-
data.thermal.temperatures?.cpu_die != null ? String(celsiusToFahrenheit(data.thermal.temperatures.cpu_die)) : ""
|
|
1261
|
-
],
|
|
1877
|
+
["thermal", "cpuTempF", data.thermal.temperatures?.cpu_die != null ? String(celsiusToFahrenheit(data.thermal.temperatures.cpu_die)) : ""],
|
|
1262
1878
|
["network", "rxBytes", String(data.network.rxBytes)],
|
|
1263
1879
|
["network", "txBytes", String(data.network.txBytes)],
|
|
1264
1880
|
["network", "rxPackets", String(data.network.rxPackets)],
|
|
@@ -1279,6 +1895,9 @@ function formatCsv(data) {
|
|
|
1279
1895
|
["system", "uptime", data.header.uptime],
|
|
1280
1896
|
["system", "timestamp", data.timestamp]
|
|
1281
1897
|
];
|
|
1898
|
+
if (data.cpu.coreUsage && data.cpu.coreUsage.length) {
|
|
1899
|
+
data.cpu.coreUsage.forEach((u, i) => rows.push(["cpu", `coreUsage_${i}`, String(u)]));
|
|
1900
|
+
}
|
|
1282
1901
|
if (data.packets) {
|
|
1283
1902
|
rows.push(["packets", "totalPackets", String(data.packets.totalPackets)]);
|
|
1284
1903
|
rows.push(["packets", "rxPackets", String(data.packets.rxPackets)]);
|
|
@@ -1318,28 +1937,28 @@ function formatGraphs(history, width = 80, themeName = "default", graphMode = "s
|
|
|
1318
1937
|
const theme = THEMES[themeName] || THEMES.default;
|
|
1319
1938
|
const contentWidth = width - 4;
|
|
1320
1939
|
const sampleCount = history.cpuUsage.length;
|
|
1321
|
-
const sparkWidth = Math.max(10, contentWidth -
|
|
1940
|
+
const sparkWidth = Math.max(10, contentWidth - 24);
|
|
1322
1941
|
const lines = [];
|
|
1323
|
-
lines.push(graphRow("CPU %", history.cpuUsage, { min: 0, max: 100 }, (v) => `${v.toFixed(0)}%`, sparkWidth, "collecting data...", graphMode));
|
|
1324
|
-
lines.push(graphRow("Mem %", history.memUsage, { min: 0, max: 100 }, (v) => `${v.toFixed(0)}%`, sparkWidth, "collecting data...", graphMode));
|
|
1942
|
+
lines.push(graphRow("CPU %", history.cpuUsage, { min: 0, max: 100 }, (v) => `${v.toFixed(0)}%`, sparkWidth, "collecting data...", graphMode, contentWidth));
|
|
1943
|
+
lines.push(graphRow("Mem %", history.memUsage, { min: 0, max: 100 }, (v) => `${v.toFixed(0)}%`, sparkWidth, "collecting data...", graphMode, contentWidth));
|
|
1325
1944
|
lines.push(
|
|
1326
|
-
graphRow("Temp", history.temp, {},
|
|
1945
|
+
graphRow("Temp", history.temp, {}, (v) => `${v.toFixed(1)}\xB0C`, sparkWidth, "no sensor access", graphMode, contentWidth)
|
|
1327
1946
|
);
|
|
1328
|
-
lines.push(graphRow("Net RX/s", history.netRxRate, {}, (v) => `${formatBytes(v)}/s`, sparkWidth, "collecting data...", graphMode));
|
|
1329
|
-
lines.push(graphRow("Net TX/s", history.netTxRate, {}, (v) => `${formatBytes(v)}/s`, sparkWidth, "collecting data...", graphMode));
|
|
1947
|
+
lines.push(graphRow("Net RX/s", history.netRxRate, {}, (v) => `${formatBytes(v)}/s`, sparkWidth, "collecting data...", graphMode, contentWidth));
|
|
1948
|
+
lines.push(graphRow("Net TX/s", history.netTxRate, {}, (v) => `${formatBytes(v)}/s`, sparkWidth, "collecting data...", graphMode, contentWidth));
|
|
1330
1949
|
if (history.powerWatts.length) {
|
|
1331
|
-
lines.push(graphRow("Power W", history.powerWatts, {}, (v) => `${v.toFixed(1)} W`, sparkWidth, "collecting data...", graphMode));
|
|
1950
|
+
lines.push(graphRow("Power W", history.powerWatts, {}, (v) => `${v.toFixed(1)} W`, sparkWidth, "collecting data...", graphMode, contentWidth));
|
|
1332
1951
|
}
|
|
1333
1952
|
if (history.rxPacketRate.length) {
|
|
1334
|
-
lines.push(graphRow("RX pkt/s", history.rxPacketRate, {}, (v) => `${v.toFixed(0)} pkt/s`, sparkWidth, "collecting data...", graphMode));
|
|
1953
|
+
lines.push(graphRow("RX pkt/s", history.rxPacketRate, {}, (v) => `${v.toFixed(0)} pkt/s`, sparkWidth, "collecting data...", graphMode, contentWidth));
|
|
1335
1954
|
}
|
|
1336
1955
|
if (history.txPacketRate.length) {
|
|
1337
|
-
lines.push(graphRow("TX pkt/s", history.txPacketRate, {}, (v) => `${v.toFixed(0)} pkt/s`, sparkWidth, "collecting data...", graphMode));
|
|
1956
|
+
lines.push(graphRow("TX pkt/s", history.txPacketRate, {}, (v) => `${v.toFixed(0)} pkt/s`, sparkWidth, "collecting data...", graphMode, contentWidth));
|
|
1338
1957
|
}
|
|
1339
1958
|
const title = `Graphs \xB7 mode:${graphMode} \xB7 last ${sampleCount} sample${sampleCount === 1 ? "" : "s"}`;
|
|
1340
1959
|
return panel(title, lines, width, theme.graphs, theme.border).join("\n");
|
|
1341
1960
|
}
|
|
1342
|
-
function graphRow(label, values, bounds, fmt, sparkWidth = 40, emptyMessage = "collecting data...", mode = "spark") {
|
|
1961
|
+
function graphRow(label, values, bounds, fmt, sparkWidth = 40, emptyMessage = "collecting data...", mode = "spark", contentWidth = 76) {
|
|
1343
1962
|
if (!values.length) {
|
|
1344
1963
|
return `${chalk4.dim(label.padEnd(10))} ${chalk4.dim(emptyMessage)}`;
|
|
1345
1964
|
}
|
|
@@ -1351,7 +1970,8 @@ function graphRow(label, values, bounds, fmt, sparkWidth = 40, emptyMessage = "c
|
|
|
1351
1970
|
const pct = (current - bounds.min) / (bounds.max - bounds.min || 1) * 100;
|
|
1352
1971
|
color = pct > 90 ? chalk4.red : pct > 70 ? chalk4.yellow : chalk4.green;
|
|
1353
1972
|
}
|
|
1354
|
-
|
|
1973
|
+
const raw = `${chalk4.dim(label.padEnd(10))}${color(spark)} ${chalk4.bold(fmt(current))}`;
|
|
1974
|
+
return fitVisible(raw, contentWidth);
|
|
1355
1975
|
}
|
|
1356
1976
|
function barGraph(values, bounds, width) {
|
|
1357
1977
|
let min = bounds.min ?? Infinity;
|
|
@@ -1377,7 +1997,7 @@ function barGraph(values, bounds, width) {
|
|
|
1377
1997
|
import readline from "readline";
|
|
1378
1998
|
|
|
1379
1999
|
// src/history.ts
|
|
1380
|
-
var History = class {
|
|
2000
|
+
var History = class _History {
|
|
1381
2001
|
/** Maximum number of samples retained in the rolling window. */
|
|
1382
2002
|
maxLen;
|
|
1383
2003
|
/** Monotonically increasing version, bumped on every push. */
|
|
@@ -1414,6 +2034,46 @@ var History = class {
|
|
|
1414
2034
|
constructor(maxLen = 40) {
|
|
1415
2035
|
this.maxLen = maxLen;
|
|
1416
2036
|
}
|
|
2037
|
+
/**
|
|
2038
|
+
* Compute the arithmetic mean of a numeric array.
|
|
2039
|
+
* Returns 0 if the array is empty.
|
|
2040
|
+
*/
|
|
2041
|
+
static mean(arr) {
|
|
2042
|
+
if (!arr.length) return 0;
|
|
2043
|
+
return arr.reduce((s, v) => s + v, 0) / arr.length;
|
|
2044
|
+
}
|
|
2045
|
+
/**
|
|
2046
|
+
* Compute the population standard deviation of a numeric array.
|
|
2047
|
+
* Returns 0 if the array has fewer than 2 elements.
|
|
2048
|
+
*/
|
|
2049
|
+
static stdDev(arr) {
|
|
2050
|
+
if (arr.length < 2) return 0;
|
|
2051
|
+
const m = _History.mean(arr);
|
|
2052
|
+
const variance = arr.reduce((s, v) => s + (v - m) ** 2, 0) / arr.length;
|
|
2053
|
+
return Math.sqrt(variance);
|
|
2054
|
+
}
|
|
2055
|
+
/**
|
|
2056
|
+
* Compute the z-score of a value against a reference array.
|
|
2057
|
+
* Returns 0 when the array is empty or has zero std dev.
|
|
2058
|
+
*/
|
|
2059
|
+
static zScore(value, arr) {
|
|
2060
|
+
const m = _History.mean(arr);
|
|
2061
|
+
const sd = _History.stdDev(arr);
|
|
2062
|
+
if (sd === 0) return 0;
|
|
2063
|
+
return (value - m) / sd;
|
|
2064
|
+
}
|
|
2065
|
+
/**
|
|
2066
|
+
* Check whether a value is a statistical outlier against a
|
|
2067
|
+
* reference array.
|
|
2068
|
+
* @param value - The current reading
|
|
2069
|
+
* @param arr - The rolling history window
|
|
2070
|
+
* @param threshold - Z-score threshold (default 2.5)
|
|
2071
|
+
* @returns true if the value deviates from the baseline by more than `threshold` standard deviations
|
|
2072
|
+
*/
|
|
2073
|
+
static isAnomaly(value, arr, threshold = 2.5) {
|
|
2074
|
+
if (arr.length < 5) return false;
|
|
2075
|
+
return Math.abs(_History.zScore(value, arr)) > threshold;
|
|
2076
|
+
}
|
|
1417
2077
|
/**
|
|
1418
2078
|
* Push a new sample into the rolling window.
|
|
1419
2079
|
*
|
|
@@ -1491,20 +2151,31 @@ var History = class {
|
|
|
1491
2151
|
}
|
|
1492
2152
|
};
|
|
1493
2153
|
|
|
1494
|
-
// src/
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
2154
|
+
// src/state/config.ts
|
|
2155
|
+
import fs from "fs";
|
|
2156
|
+
import os2 from "os";
|
|
2157
|
+
import path from "path";
|
|
2158
|
+
var CONFIG_DIR = path.join(os2.homedir(), ".config", "pyre");
|
|
2159
|
+
var CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
|
|
2160
|
+
var DEFAULT_CONFIG = {
|
|
2161
|
+
theme: "default",
|
|
1502
2162
|
interval: 2,
|
|
1503
|
-
showGraphs: true,
|
|
1504
|
-
graphMode: "spark",
|
|
1505
|
-
exportFormat: "json",
|
|
1506
2163
|
exportDir: "./pyre-exports",
|
|
1507
|
-
|
|
2164
|
+
detailed: false,
|
|
2165
|
+
sortMode: "cpu",
|
|
2166
|
+
treeView: false,
|
|
2167
|
+
graphMode: "spark",
|
|
2168
|
+
showGraphs: true,
|
|
2169
|
+
autoLog: false,
|
|
2170
|
+
mouseEnabled: true,
|
|
2171
|
+
cpuAlertPct: 90,
|
|
2172
|
+
tempAlertC: 95,
|
|
2173
|
+
notificationsEnabled: true,
|
|
2174
|
+
p2pPort: 9876,
|
|
2175
|
+
p2pPassword: "pyre",
|
|
2176
|
+
splashEnabled: true,
|
|
2177
|
+
splashColorScheme: "fire",
|
|
2178
|
+
splashAnimation: "classic",
|
|
1508
2179
|
visiblePanels: {
|
|
1509
2180
|
cpu: true,
|
|
1510
2181
|
mem: true,
|
|
@@ -1516,7 +2187,72 @@ var state = {
|
|
|
1516
2187
|
packets: true,
|
|
1517
2188
|
tasks: true,
|
|
1518
2189
|
disk: true,
|
|
1519
|
-
process: true
|
|
2190
|
+
process: true,
|
|
2191
|
+
tree: false
|
|
2192
|
+
}
|
|
2193
|
+
};
|
|
2194
|
+
function readConfig() {
|
|
2195
|
+
try {
|
|
2196
|
+
if (!fs.existsSync(CONFIG_FILE)) return { ...DEFAULT_CONFIG };
|
|
2197
|
+
const raw = fs.readFileSync(CONFIG_FILE, "utf-8");
|
|
2198
|
+
const parsed = JSON.parse(raw);
|
|
2199
|
+
return deepMerge(DEFAULT_CONFIG, parsed);
|
|
2200
|
+
} catch {
|
|
2201
|
+
return { ...DEFAULT_CONFIG };
|
|
2202
|
+
}
|
|
2203
|
+
}
|
|
2204
|
+
function writeConfig(config3) {
|
|
2205
|
+
try {
|
|
2206
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
2207
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
2208
|
+
}
|
|
2209
|
+
const merged = deepMerge(readConfig(), config3);
|
|
2210
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(merged, null, 2) + "\n");
|
|
2211
|
+
} catch {
|
|
2212
|
+
}
|
|
2213
|
+
}
|
|
2214
|
+
function deepMerge(target, source) {
|
|
2215
|
+
const result = { ...target };
|
|
2216
|
+
for (const key of Object.keys(source)) {
|
|
2217
|
+
if (source[key] !== void 0 && source[key] !== null) {
|
|
2218
|
+
if (typeof source[key] === "object" && !Array.isArray(source[key]) && typeof target[key] === "object" && !Array.isArray(target[key])) {
|
|
2219
|
+
result[key] = deepMerge(target[key] || {}, source[key]);
|
|
2220
|
+
} else {
|
|
2221
|
+
result[key] = source[key];
|
|
2222
|
+
}
|
|
2223
|
+
}
|
|
2224
|
+
}
|
|
2225
|
+
return result;
|
|
2226
|
+
}
|
|
2227
|
+
|
|
2228
|
+
// src/live/state.ts
|
|
2229
|
+
var SIGNAL_OPTIONS = ["SIGTERM", "SIGKILL", "SIGINT", "SIGHUP", "SIGSTOP", "SIGCONT"];
|
|
2230
|
+
var config = readConfig();
|
|
2231
|
+
var state = {
|
|
2232
|
+
intervalHandle: null,
|
|
2233
|
+
uiIntervalHandle: null,
|
|
2234
|
+
running: false,
|
|
2235
|
+
paused: false,
|
|
2236
|
+
detailed: config.detailed,
|
|
2237
|
+
interval: config.interval,
|
|
2238
|
+
showGraphs: config.showGraphs,
|
|
2239
|
+
graphMode: config.graphMode,
|
|
2240
|
+
exportFormat: "json",
|
|
2241
|
+
tempUnit: "c",
|
|
2242
|
+
exportDir: config.exportDir,
|
|
2243
|
+
currentTheme: config.theme,
|
|
2244
|
+
visiblePanels: {
|
|
2245
|
+
cpu: config.visiblePanels.cpu,
|
|
2246
|
+
mem: config.visiblePanels.mem,
|
|
2247
|
+
gpu: config.visiblePanels.gpu,
|
|
2248
|
+
power: config.visiblePanels.power,
|
|
2249
|
+
battery: config.visiblePanels.battery,
|
|
2250
|
+
thermal: config.visiblePanels.thermal,
|
|
2251
|
+
network: config.visiblePanels.network,
|
|
2252
|
+
packets: config.visiblePanels.packets,
|
|
2253
|
+
tasks: config.visiblePanels.tasks,
|
|
2254
|
+
disk: config.visiblePanels.disk,
|
|
2255
|
+
process: config.visiblePanels.process
|
|
1520
2256
|
},
|
|
1521
2257
|
logging: false,
|
|
1522
2258
|
logStream: null,
|
|
@@ -1527,18 +2263,23 @@ var state = {
|
|
|
1527
2263
|
keypressHandler: null,
|
|
1528
2264
|
termWidth: process.stdout.columns || 80,
|
|
1529
2265
|
termHeight: process.stdout.rows || 24,
|
|
1530
|
-
sortMode:
|
|
2266
|
+
sortMode: config.sortMode,
|
|
1531
2267
|
processFilter: "",
|
|
1532
2268
|
inputMode: null,
|
|
1533
2269
|
inputBuffer: "",
|
|
1534
2270
|
targetPid: "",
|
|
1535
2271
|
selectedSignal: "SIGTERM",
|
|
1536
|
-
treeView:
|
|
2272
|
+
treeView: config.treeView,
|
|
1537
2273
|
SIGNAL_OPTIONS,
|
|
1538
2274
|
customizerIndex: 0,
|
|
1539
2275
|
CUSTOMIZER_OPTIONS: [
|
|
1540
2276
|
"Theme",
|
|
1541
2277
|
"Graph Mode",
|
|
2278
|
+
"Splash Screen",
|
|
2279
|
+
"Splash Color",
|
|
2280
|
+
"Splash Animation",
|
|
2281
|
+
"Notifications",
|
|
2282
|
+
"Temperature Unit",
|
|
1542
2283
|
"Toggle CPU",
|
|
1543
2284
|
"Toggle Memory",
|
|
1544
2285
|
"Toggle GPU",
|
|
@@ -1550,16 +2291,14 @@ var state = {
|
|
|
1550
2291
|
"Toggle Tasks",
|
|
1551
2292
|
"Toggle Disk",
|
|
1552
2293
|
"Toggle Processes",
|
|
1553
|
-
"Toggle Tree View"
|
|
1554
|
-
"Splash Screen",
|
|
1555
|
-
"Splash Color",
|
|
1556
|
-
"Splash Animation"
|
|
2294
|
+
"Toggle Tree View"
|
|
1557
2295
|
],
|
|
1558
|
-
CPU_ALERT_PCT:
|
|
1559
|
-
TEMP_ALERT_C:
|
|
2296
|
+
CPU_ALERT_PCT: config.cpuAlertPct,
|
|
2297
|
+
TEMP_ALERT_C: config.tempAlertC,
|
|
2298
|
+
notificationsEnabled: config.notificationsEnabled,
|
|
1560
2299
|
alerted: false,
|
|
1561
2300
|
activePanel: "grid",
|
|
1562
|
-
mouseEnabled:
|
|
2301
|
+
mouseEnabled: config.mouseEnabled,
|
|
1563
2302
|
PANEL_TABS: [
|
|
1564
2303
|
{ id: "cpu", label: "CPU", key: "1" },
|
|
1565
2304
|
{ id: "mem", label: "Memory", key: "2" },
|
|
@@ -1578,11 +2317,11 @@ var state = {
|
|
|
1578
2317
|
p2pBind: "",
|
|
1579
2318
|
p2pServer: null,
|
|
1580
2319
|
p2pServerRunning: false,
|
|
1581
|
-
p2pPort:
|
|
1582
|
-
p2pPassword:
|
|
1583
|
-
splashEnabled:
|
|
1584
|
-
splashColorScheme:
|
|
1585
|
-
splashAnimation:
|
|
2320
|
+
p2pPort: config.p2pPort,
|
|
2321
|
+
p2pPassword: config.p2pPassword,
|
|
2322
|
+
splashEnabled: config.splashEnabled,
|
|
2323
|
+
splashColorScheme: config.splashColorScheme,
|
|
2324
|
+
splashAnimation: config.splashAnimation
|
|
1586
2325
|
};
|
|
1587
2326
|
function setStatus(msg, ms = 3e3) {
|
|
1588
2327
|
state.statusMessage = msg;
|
|
@@ -1610,10 +2349,10 @@ function getToggleKey(opt) {
|
|
|
1610
2349
|
}
|
|
1611
2350
|
|
|
1612
2351
|
// src/live/export.ts
|
|
1613
|
-
import
|
|
1614
|
-
import
|
|
2352
|
+
import fs2 from "fs";
|
|
2353
|
+
import path2 from "path";
|
|
1615
2354
|
function ensureDir(dir) {
|
|
1616
|
-
if (!
|
|
2355
|
+
if (!fs2.existsSync(dir)) fs2.mkdirSync(dir, { recursive: true });
|
|
1617
2356
|
}
|
|
1618
2357
|
function timestampForFile() {
|
|
1619
2358
|
return (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
@@ -1625,9 +2364,27 @@ function exportSnapshot() {
|
|
|
1625
2364
|
}
|
|
1626
2365
|
try {
|
|
1627
2366
|
ensureDir(state.exportDir);
|
|
1628
|
-
const
|
|
1629
|
-
const
|
|
1630
|
-
|
|
2367
|
+
const ext = state.exportFormat === "json" ? "json" : state.exportFormat === "csv" ? "csv" : state.exportFormat === "tsv" ? "tsv" : state.exportFormat === "html" ? "html" : "md";
|
|
2368
|
+
const file = path2.join(state.exportDir, `pyre-${timestampForFile()}.${ext}`);
|
|
2369
|
+
let content;
|
|
2370
|
+
switch (state.exportFormat) {
|
|
2371
|
+
case "json":
|
|
2372
|
+
content = formatJson(state.lastData);
|
|
2373
|
+
break;
|
|
2374
|
+
case "csv":
|
|
2375
|
+
content = formatCsv(state.lastData);
|
|
2376
|
+
break;
|
|
2377
|
+
case "tsv":
|
|
2378
|
+
content = formatTsv(state.lastData);
|
|
2379
|
+
break;
|
|
2380
|
+
case "html":
|
|
2381
|
+
content = formatHtml(state.lastData);
|
|
2382
|
+
break;
|
|
2383
|
+
case "md":
|
|
2384
|
+
content = formatMarkdown(state.lastData);
|
|
2385
|
+
break;
|
|
2386
|
+
}
|
|
2387
|
+
fs2.writeFileSync(file, content);
|
|
1631
2388
|
setStatus(`Exported snapshot -> ${file}`);
|
|
1632
2389
|
} catch (err) {
|
|
1633
2390
|
setStatus(`Export failed: ${err.message}`);
|
|
@@ -1636,8 +2393,9 @@ function exportSnapshot() {
|
|
|
1636
2393
|
function startLogging() {
|
|
1637
2394
|
try {
|
|
1638
2395
|
ensureDir(state.exportDir);
|
|
1639
|
-
|
|
1640
|
-
|
|
2396
|
+
rotateLogs();
|
|
2397
|
+
const file = path2.join(state.exportDir, `pyre-log-${timestampForFile()}.csv`);
|
|
2398
|
+
state.logStream = fs2.createWriteStream(file, { flags: "a" });
|
|
1641
2399
|
state.logStream.write("timestamp,cpu_usage,mem_usage_percent,temp_c,net_rx_bytes,net_tx_bytes,net_rx_packets,net_tx_packets,connections,thermal_state\n");
|
|
1642
2400
|
state.logging = true;
|
|
1643
2401
|
setStatus(`Logging every tick -> ${file}`);
|
|
@@ -1645,6 +2403,52 @@ function startLogging() {
|
|
|
1645
2403
|
setStatus(`Logging failed to start: ${err.message}`);
|
|
1646
2404
|
}
|
|
1647
2405
|
}
|
|
2406
|
+
var MAX_LOG_AGE_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
2407
|
+
var MAX_LOG_COUNT = 20;
|
|
2408
|
+
var MAX_LOG_BYTES = 50 * 1024 * 1024;
|
|
2409
|
+
function rotateLogs() {
|
|
2410
|
+
try {
|
|
2411
|
+
const dir = state.exportDir;
|
|
2412
|
+
if (!fs2.existsSync(dir)) return;
|
|
2413
|
+
const files = fs2.readdirSync(dir).filter((f) => f.startsWith("pyre-log-") && f.endsWith(".csv")).map((f) => ({ name: f, path: path2.join(dir, f), mtime: fs2.statSync(path2.join(dir, f)).mtimeMs, size: fs2.statSync(path2.join(dir, f)).size })).sort((a, b) => a.mtime - b.mtime);
|
|
2414
|
+
const now = Date.now();
|
|
2415
|
+
for (const f of files) {
|
|
2416
|
+
if (now - f.mtime > MAX_LOG_AGE_MS) {
|
|
2417
|
+
try {
|
|
2418
|
+
fs2.unlinkSync(f.path);
|
|
2419
|
+
} catch {
|
|
2420
|
+
}
|
|
2421
|
+
}
|
|
2422
|
+
}
|
|
2423
|
+
const remaining = fs2.readdirSync(dir).filter((f) => f.startsWith("pyre-log-") && f.endsWith(".csv"));
|
|
2424
|
+
if (remaining.length > MAX_LOG_COUNT) {
|
|
2425
|
+
const toDelete = remaining.sort().slice(0, remaining.length - MAX_LOG_COUNT);
|
|
2426
|
+
for (const f of toDelete) {
|
|
2427
|
+
try {
|
|
2428
|
+
fs2.unlinkSync(path2.join(dir, f));
|
|
2429
|
+
} catch {
|
|
2430
|
+
}
|
|
2431
|
+
}
|
|
2432
|
+
}
|
|
2433
|
+
let totalSize = 0;
|
|
2434
|
+
for (const f of remaining) {
|
|
2435
|
+
totalSize += fs2.statSync(path2.join(dir, f)).size;
|
|
2436
|
+
}
|
|
2437
|
+
if (totalSize > MAX_LOG_BYTES) {
|
|
2438
|
+
const sorted = remaining.sort();
|
|
2439
|
+
for (const f of sorted) {
|
|
2440
|
+
const sz = fs2.statSync(path2.join(dir, f)).size;
|
|
2441
|
+
try {
|
|
2442
|
+
fs2.unlinkSync(path2.join(dir, f));
|
|
2443
|
+
} catch {
|
|
2444
|
+
}
|
|
2445
|
+
totalSize -= sz;
|
|
2446
|
+
if (totalSize <= MAX_LOG_BYTES * 0.8) break;
|
|
2447
|
+
}
|
|
2448
|
+
}
|
|
2449
|
+
} catch {
|
|
2450
|
+
}
|
|
2451
|
+
}
|
|
1648
2452
|
function stopLogging() {
|
|
1649
2453
|
state.logging = false;
|
|
1650
2454
|
state.logStream?.end();
|
|
@@ -1657,7 +2461,10 @@ function toggleLogging() {
|
|
|
1657
2461
|
}
|
|
1658
2462
|
function writeLogRow(data) {
|
|
1659
2463
|
if (!state.logging || !state.logStream) return;
|
|
1660
|
-
|
|
2464
|
+
let temp = data.cpu.temperature ?? data.thermal.temperatures?.cpu_die ?? "";
|
|
2465
|
+
if (temp !== "" && state.tempUnit === "f") {
|
|
2466
|
+
temp = (temp * 9 / 5 + 32).toFixed(1);
|
|
2467
|
+
}
|
|
1661
2468
|
const rxPackets = data.network.rxPackets ?? 0;
|
|
1662
2469
|
const txPackets = data.network.txPackets ?? 0;
|
|
1663
2470
|
const connections = data.network.connections ?? 0;
|
|
@@ -1668,7 +2475,101 @@ function writeLogRow(data) {
|
|
|
1668
2475
|
}
|
|
1669
2476
|
|
|
1670
2477
|
// src/live/render.ts
|
|
2478
|
+
import { execFile } from "child_process";
|
|
1671
2479
|
import chalk5 from "chalk";
|
|
2480
|
+
|
|
2481
|
+
// src/anomalies.ts
|
|
2482
|
+
var ZSCORE_THRESHOLD = 2.5;
|
|
2483
|
+
var CRITICAL_ZSCORE = 3.5;
|
|
2484
|
+
var MIN_SAMPLES = 5;
|
|
2485
|
+
function detectAnomalies(data, history) {
|
|
2486
|
+
const alerts = [];
|
|
2487
|
+
const cpuZ = History.zScore(data.cpu.usage, history.cpuUsage);
|
|
2488
|
+
if (Math.abs(cpuZ) > ZSCORE_THRESHOLD && history.cpuUsage.length >= MIN_SAMPLES) {
|
|
2489
|
+
alerts.push({
|
|
2490
|
+
metric: "CPU",
|
|
2491
|
+
value: data.cpu.usage,
|
|
2492
|
+
zScore: cpuZ,
|
|
2493
|
+
severity: Math.abs(cpuZ) > CRITICAL_ZSCORE ? "critical" : "warning"
|
|
2494
|
+
});
|
|
2495
|
+
}
|
|
2496
|
+
const memZ = History.zScore(data.memory.usagePercent, history.memUsage);
|
|
2497
|
+
if (Math.abs(memZ) > ZSCORE_THRESHOLD && history.memUsage.length >= MIN_SAMPLES) {
|
|
2498
|
+
alerts.push({
|
|
2499
|
+
metric: "Memory",
|
|
2500
|
+
value: data.memory.usagePercent,
|
|
2501
|
+
zScore: memZ,
|
|
2502
|
+
severity: Math.abs(memZ) > CRITICAL_ZSCORE ? "critical" : "warning"
|
|
2503
|
+
});
|
|
2504
|
+
}
|
|
2505
|
+
if (data.cpu.temperature !== void 0) {
|
|
2506
|
+
const tempZ = History.zScore(data.cpu.temperature, history.temp);
|
|
2507
|
+
if (Math.abs(tempZ) > ZSCORE_THRESHOLD && history.temp.length >= MIN_SAMPLES) {
|
|
2508
|
+
alerts.push({
|
|
2509
|
+
metric: "Temp",
|
|
2510
|
+
value: data.cpu.temperature,
|
|
2511
|
+
zScore: tempZ,
|
|
2512
|
+
severity: Math.abs(tempZ) > CRITICAL_ZSCORE ? "critical" : "warning"
|
|
2513
|
+
});
|
|
2514
|
+
}
|
|
2515
|
+
}
|
|
2516
|
+
const netRxZ = History.zScore(data.network.rxBytes, history.netRxRate);
|
|
2517
|
+
if (Math.abs(netRxZ) > ZSCORE_THRESHOLD && history.netRxRate.length >= MIN_SAMPLES) {
|
|
2518
|
+
alerts.push({
|
|
2519
|
+
metric: "Net RX",
|
|
2520
|
+
value: data.network.rxBytes,
|
|
2521
|
+
zScore: netRxZ,
|
|
2522
|
+
severity: Math.abs(netRxZ) > CRITICAL_ZSCORE ? "critical" : "warning"
|
|
2523
|
+
});
|
|
2524
|
+
}
|
|
2525
|
+
const netTxZ = History.zScore(data.network.txBytes, history.netTxRate);
|
|
2526
|
+
if (Math.abs(netTxZ) > ZSCORE_THRESHOLD && history.netTxRate.length >= MIN_SAMPLES) {
|
|
2527
|
+
alerts.push({
|
|
2528
|
+
metric: "Net TX",
|
|
2529
|
+
value: data.network.txBytes,
|
|
2530
|
+
zScore: netTxZ,
|
|
2531
|
+
severity: Math.abs(netTxZ) > CRITICAL_ZSCORE ? "critical" : "warning"
|
|
2532
|
+
});
|
|
2533
|
+
}
|
|
2534
|
+
if (data.power?.combinedWatts !== void 0) {
|
|
2535
|
+
const powerZ = History.zScore(data.power.combinedWatts, history.powerWatts);
|
|
2536
|
+
if (Math.abs(powerZ) > ZSCORE_THRESHOLD && history.powerWatts.length >= MIN_SAMPLES) {
|
|
2537
|
+
alerts.push({
|
|
2538
|
+
metric: "Power",
|
|
2539
|
+
value: data.power.combinedWatts,
|
|
2540
|
+
zScore: powerZ,
|
|
2541
|
+
severity: Math.abs(powerZ) > CRITICAL_ZSCORE ? "critical" : "warning"
|
|
2542
|
+
});
|
|
2543
|
+
}
|
|
2544
|
+
}
|
|
2545
|
+
if (data.gpu) {
|
|
2546
|
+
const gpuZ = History.zScore(data.gpu.utilization, history.gpuUtil);
|
|
2547
|
+
if (Math.abs(gpuZ) > ZSCORE_THRESHOLD && history.gpuUtil.length >= MIN_SAMPLES) {
|
|
2548
|
+
alerts.push({
|
|
2549
|
+
metric: "GPU",
|
|
2550
|
+
value: data.gpu.utilization,
|
|
2551
|
+
zScore: gpuZ,
|
|
2552
|
+
severity: Math.abs(gpuZ) > CRITICAL_ZSCORE ? "critical" : "warning"
|
|
2553
|
+
});
|
|
2554
|
+
}
|
|
2555
|
+
}
|
|
2556
|
+
return alerts;
|
|
2557
|
+
}
|
|
2558
|
+
|
|
2559
|
+
// src/live/render.ts
|
|
2560
|
+
function formatTempForUnit(c) {
|
|
2561
|
+
if (state.tempUnit === "f") {
|
|
2562
|
+
const f = c * (9 / 5) + 32;
|
|
2563
|
+
return `${f.toFixed(1)}\xB0F`;
|
|
2564
|
+
}
|
|
2565
|
+
return `${c.toFixed(1)}\xB0C`;
|
|
2566
|
+
}
|
|
2567
|
+
function applyTempUnit(text) {
|
|
2568
|
+
if (state.tempUnit === "f") {
|
|
2569
|
+
return text.replace(/([\d.]+)°C \/ ([\d.]+)°F/g, "$2\xB0F");
|
|
2570
|
+
}
|
|
2571
|
+
return text.replace(/([\d.]+)°C \/ ([\d.]+)°F/g, "$1\xB0C");
|
|
2572
|
+
}
|
|
1672
2573
|
var _cachedTable = "";
|
|
1673
2574
|
var _cachedTableData = null;
|
|
1674
2575
|
var _cachedTableParams = "";
|
|
@@ -1688,6 +2589,7 @@ function tableCacheParams() {
|
|
|
1688
2589
|
state.currentTheme,
|
|
1689
2590
|
state.activePanel,
|
|
1690
2591
|
state.treeView ? "1" : "0",
|
|
2592
|
+
state.history.version,
|
|
1691
2593
|
JSON.stringify(state.visiblePanels)
|
|
1692
2594
|
].join("|");
|
|
1693
2595
|
}
|
|
@@ -1706,11 +2608,8 @@ function processRowBudget() {
|
|
|
1706
2608
|
if (state.activePanel === "process") {
|
|
1707
2609
|
return Math.max(10, state.termHeight - 12 - overlayRows);
|
|
1708
2610
|
}
|
|
1709
|
-
const
|
|
1710
|
-
|
|
1711
|
-
const cardRows = Math.ceil(visibleCards / columns);
|
|
1712
|
-
const reserved = 4 + cardRows * 8 + 8 + (state.showGraphs ? 11 : 0) + 5 + overlayRows;
|
|
1713
|
-
return Math.max(5, state.termHeight - reserved);
|
|
2611
|
+
const reserved = 16 + overlayRows;
|
|
2612
|
+
return Math.max(4, state.termHeight - reserved);
|
|
1714
2613
|
}
|
|
1715
2614
|
function p2pPanelLines() {
|
|
1716
2615
|
const events = state.p2pServer ? state.p2pServer.peerEventHistory : state.p2pEvents;
|
|
@@ -1754,6 +2653,11 @@ function renderCustomizerOverlay() {
|
|
|
1754
2653
|
lines.push(`${prefix}${opt}: ${chalk5.bold.green(state.splashColorScheme)} [fire, ocean, forest, purple, monochrome]`);
|
|
1755
2654
|
} else if (opt === "Splash Animation") {
|
|
1756
2655
|
lines.push(`${prefix}${opt}: ${chalk5.bold.green(state.splashAnimation)} [classic, wave, sparks]`);
|
|
2656
|
+
} else if (opt === "Notifications") {
|
|
2657
|
+
const status = state.notificationsEnabled ? chalk5.green("[ON]") : chalk5.red("[OFF]");
|
|
2658
|
+
lines.push(`${prefix}${opt}: ${status}`);
|
|
2659
|
+
} else if (opt === "Temperature Unit") {
|
|
2660
|
+
lines.push(`${prefix}${opt}: ${chalk5.bold.green(state.tempUnit.toUpperCase())} [C, F]`);
|
|
1757
2661
|
} else {
|
|
1758
2662
|
const toggleKey = getToggleKey(opt);
|
|
1759
2663
|
const isVisible = toggleKey ? toggleKey === "tree" ? state.treeView : state.visiblePanels[toggleKey] !== false : true;
|
|
@@ -1786,10 +2690,25 @@ function writeFrame(lines) {
|
|
|
1786
2690
|
}
|
|
1787
2691
|
function render() {
|
|
1788
2692
|
if (!state.lastData) return;
|
|
2693
|
+
const rows = state.termHeight || 24;
|
|
2694
|
+
const cols = state.termWidth || 80;
|
|
2695
|
+
if (cols < 50 || rows < 15) {
|
|
2696
|
+
const msg = chalk5.yellow(`pyre ${rows}x${cols} too small. Resize terminal to >= 50x20.`);
|
|
2697
|
+
const centered = Math.max(0, Math.floor((rows - 2) / 2));
|
|
2698
|
+
const out = [];
|
|
2699
|
+
for (let i = 0; i < centered; i++) out.push("");
|
|
2700
|
+
out.push(msg.padEnd(cols));
|
|
2701
|
+
while (out.length < rows) out.push("");
|
|
2702
|
+
writeFrame(out);
|
|
2703
|
+
return;
|
|
2704
|
+
}
|
|
1789
2705
|
const lines = [];
|
|
1790
2706
|
const theme = THEMES[state.currentTheme] || THEMES.default;
|
|
1791
2707
|
const params = tableCacheParams();
|
|
1792
|
-
const
|
|
2708
|
+
const anomalies = detectAnomalies(state.lastData, state.history);
|
|
2709
|
+
const anomalyCacheKey = anomalies.map((a) => `${a.metric}:${a.zScore.toFixed(2)}`).join(",");
|
|
2710
|
+
const cacheParams = params + "|" + anomalyCacheKey;
|
|
2711
|
+
const tableStr = _tableDataDirty || state.lastData !== _cachedTableData || cacheParams !== _cachedTableParams ? (() => {
|
|
1793
2712
|
const out = formatTable(state.lastData, {
|
|
1794
2713
|
width: state.termWidth,
|
|
1795
2714
|
sortBy: state.sortMode,
|
|
@@ -1798,13 +2717,17 @@ function render() {
|
|
|
1798
2717
|
theme: state.currentTheme,
|
|
1799
2718
|
visible: state.visiblePanels,
|
|
1800
2719
|
treeView: state.treeView,
|
|
1801
|
-
activePanel: state.activePanel
|
|
2720
|
+
activePanel: state.activePanel,
|
|
2721
|
+
anomalies,
|
|
2722
|
+
tempUnit: state.tempUnit,
|
|
2723
|
+
history: state.history,
|
|
2724
|
+
graphMode: state.graphMode
|
|
1802
2725
|
});
|
|
1803
2726
|
_cachedTableData = state.lastData;
|
|
1804
|
-
_cachedTableParams =
|
|
1805
|
-
_cachedTable = out;
|
|
2727
|
+
_cachedTableParams = cacheParams;
|
|
2728
|
+
_cachedTable = applyTempUnit(out);
|
|
1806
2729
|
_tableDataDirty = false;
|
|
1807
|
-
return
|
|
2730
|
+
return _cachedTable;
|
|
1808
2731
|
})() : _cachedTable;
|
|
1809
2732
|
const bodyLines = [];
|
|
1810
2733
|
if (state.activePanel === "p2p") {
|
|
@@ -1818,44 +2741,47 @@ function render() {
|
|
|
1818
2741
|
const out = formatGraphs(state.history, state.termWidth, state.currentTheme, state.graphMode);
|
|
1819
2742
|
_cachedGraphsVersion = state.history.version;
|
|
1820
2743
|
_cachedGraphsParams = graphsParams;
|
|
1821
|
-
_cachedGraphs = out;
|
|
1822
|
-
return
|
|
2744
|
+
_cachedGraphs = applyTempUnit(out);
|
|
2745
|
+
return _cachedGraphs;
|
|
1823
2746
|
})() : _cachedGraphs;
|
|
1824
2747
|
if (state.showGraphs && state.activePanel !== "p2p") {
|
|
1825
2748
|
bodyLines.push("");
|
|
1826
2749
|
bodyLines.push(...graphsStr.split("\n"));
|
|
1827
2750
|
}
|
|
1828
|
-
const
|
|
1829
|
-
|
|
1830
|
-
|
|
2751
|
+
const footerLine1 = footerLine()[0] || "";
|
|
2752
|
+
const footerLine22 = footerLine()[1] || "";
|
|
2753
|
+
const footerLine3 = footerLine()[2] || " ";
|
|
2754
|
+
const footerLines = [footerLine1, footerLine22, footerLine3];
|
|
1831
2755
|
if (state.inputMode === "customizer") {
|
|
1832
|
-
|
|
2756
|
+
bodyLines.push("");
|
|
2757
|
+
bodyLines.push(...renderCustomizerOverlay().split("\n"));
|
|
1833
2758
|
} else if (state.inputMode === "filter") {
|
|
1834
|
-
|
|
2759
|
+
bodyLines.push(chalk5.cyan(` Filter processes: ${state.inputBuffer}_`));
|
|
1835
2760
|
} else if (state.inputMode === "kill") {
|
|
1836
|
-
|
|
2761
|
+
bodyLines.push(chalk5.cyan(` Kill PID: ${state.inputBuffer}_ (enter to confirm, esc to cancel)`));
|
|
1837
2762
|
} else if (state.inputMode === "signal") {
|
|
1838
2763
|
const sigIdx = state.SIGNAL_OPTIONS.indexOf(state.inputBuffer);
|
|
1839
2764
|
const sigList = state.SIGNAL_OPTIONS.map((s, i) => i === sigIdx ? chalk5.yellow(s) : chalk5.dim(s)).join(" ");
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
2765
|
+
bodyLines.push(chalk5.cyan(` Signal: ${state.inputBuffer}_`));
|
|
2766
|
+
bodyLines.push(chalk5.dim(` ${sigList}`));
|
|
2767
|
+
bodyLines.push(chalk5.dim(" \u2191/\u2193 to select, Enter to confirm, Esc to cancel"));
|
|
1843
2768
|
} else if (state.inputMode === "p2p") {
|
|
1844
|
-
|
|
2769
|
+
bodyLines.push(chalk5.cyan(` P2P password (${state.p2pPort}): ${state.inputBuffer}_ (enter to start, esc to cancel)`));
|
|
1845
2770
|
} else if (state.statusMessage) {
|
|
1846
|
-
|
|
1847
|
-
} else {
|
|
1848
|
-
footerLines.push("");
|
|
2771
|
+
bodyLines.push(` ${state.statusMessage}`);
|
|
1849
2772
|
}
|
|
1850
|
-
const
|
|
1851
|
-
|
|
1852
|
-
|
|
2773
|
+
const footerHeight = 3;
|
|
2774
|
+
const targetBodyHeight = Math.max(1, state.termHeight - footerHeight);
|
|
2775
|
+
if (bodyLines.length > targetBodyHeight) {
|
|
2776
|
+
bodyLines.length = targetBodyHeight;
|
|
1853
2777
|
}
|
|
1854
|
-
const frameLines = [...bodyLines
|
|
1855
|
-
|
|
1856
|
-
frameLines.
|
|
2778
|
+
const frameLines = [...bodyLines];
|
|
2779
|
+
while (frameLines.length < targetBodyHeight) {
|
|
2780
|
+
frameLines.push("");
|
|
1857
2781
|
}
|
|
1858
|
-
|
|
2782
|
+
frameLines.push(...footerLines);
|
|
2783
|
+
const fittedLines = frameLines.map((l) => fitVisible(l, cols));
|
|
2784
|
+
writeFrame(fittedLines);
|
|
1859
2785
|
}
|
|
1860
2786
|
function footerLine() {
|
|
1861
2787
|
const row1 = [
|
|
@@ -1867,6 +2793,7 @@ function footerLine() {
|
|
|
1867
2793
|
["b", `graph:${state.graphMode}`],
|
|
1868
2794
|
["t", state.treeView ? "flat" : "tree"],
|
|
1869
2795
|
["d", state.detailed ? "basic" : "detailed"],
|
|
2796
|
+
["T", `temp:${state.tempUnit}`],
|
|
1870
2797
|
["p", state.paused ? "resume" : "pause"]
|
|
1871
2798
|
];
|
|
1872
2799
|
const row2 = [
|
|
@@ -1888,20 +2815,35 @@ function footerLine() {
|
|
|
1888
2815
|
if (state.logging) badges.push(chalk5.red.bold("\u25CF REC"));
|
|
1889
2816
|
if (state.activePanel !== "grid") badges.push(chalk5.cyan.bold(`\u25C9 ${state.activePanel.toUpperCase()}`));
|
|
1890
2817
|
if (state.p2pServerRunning) badges.push(chalk5.green.bold(`P2P ${state.p2pBind || "0.0.0.0"}:${state.p2pPort}`));
|
|
1891
|
-
const badgeStr = badges.join(" ");
|
|
1892
|
-
return
|
|
1893
|
-
|
|
1894
|
-
|
|
2818
|
+
const badgeStr = badges.join(" ") || " ";
|
|
2819
|
+
return [str1, str2, badgeStr];
|
|
2820
|
+
}
|
|
2821
|
+
function sendNotification(title, message) {
|
|
2822
|
+
if (!state.notificationsEnabled) return;
|
|
2823
|
+
execFile("osascript", ["-e", `display notification "${message}" with title "${title}"`], (err) => {
|
|
2824
|
+
if (err) {
|
|
2825
|
+
}
|
|
2826
|
+
});
|
|
1895
2827
|
}
|
|
1896
2828
|
function checkAlerts(data) {
|
|
1897
2829
|
const temp = data.cpu.temperature ?? data.thermal.temperatures?.cpu_die ?? null;
|
|
1898
2830
|
const hot = data.cpu.usage >= state.CPU_ALERT_PCT || temp !== null && temp >= state.TEMP_ALERT_C;
|
|
1899
|
-
|
|
2831
|
+
const anomalies = detectAnomalies(data, state.history);
|
|
2832
|
+
const hasCriticalAnomaly = anomalies.some((a) => a.severity === "critical");
|
|
2833
|
+
const hasWarningAnomaly = anomalies.some((a) => a.severity === "warning");
|
|
2834
|
+
const anomalyTriggered = hasCriticalAnomaly || hasWarningAnomaly;
|
|
2835
|
+
if ((hot || anomalyTriggered) && !state.alerted) {
|
|
1900
2836
|
state.alerted = true;
|
|
1901
2837
|
process.stdout.write("\x07");
|
|
1902
|
-
const
|
|
1903
|
-
|
|
1904
|
-
|
|
2838
|
+
const reasons = [];
|
|
2839
|
+
if (data.cpu.usage >= state.CPU_ALERT_PCT) reasons.push(`CPU at ${data.cpu.usage}%`);
|
|
2840
|
+
if (temp !== null && temp >= state.TEMP_ALERT_C) reasons.push(`Temp at ${formatTempForUnit(temp)}`);
|
|
2841
|
+
for (const a of anomalies) {
|
|
2842
|
+
reasons.push(`${a.metric} anomaly (\u03C3=${a.zScore.toFixed(1)})`);
|
|
2843
|
+
}
|
|
2844
|
+
setStatus(chalk5.red(`\u26A0 Alert: ${reasons.join("; ")}`), 5e3);
|
|
2845
|
+
sendNotification("pyre Alert", reasons.join("; "));
|
|
2846
|
+
} else if (!hot && !anomalyTriggered) {
|
|
1905
2847
|
state.alerted = false;
|
|
1906
2848
|
}
|
|
1907
2849
|
}
|
|
@@ -1909,7 +2851,7 @@ function checkAlerts(data) {
|
|
|
1909
2851
|
// src/p2p/server.ts
|
|
1910
2852
|
import net from "net";
|
|
1911
2853
|
import tls from "tls";
|
|
1912
|
-
import
|
|
2854
|
+
import fs3 from "fs";
|
|
1913
2855
|
import crypto2 from "crypto";
|
|
1914
2856
|
import chalk6 from "chalk";
|
|
1915
2857
|
|
|
@@ -2054,9 +2996,9 @@ var P2PServer = class {
|
|
|
2054
2996
|
async start() {
|
|
2055
2997
|
if (this.options.auditLog) {
|
|
2056
2998
|
try {
|
|
2057
|
-
|
|
2999
|
+
fs3.mkdirSync(this.options.auditLog, { recursive: true });
|
|
2058
3000
|
const logFile = `${this.options.auditLog}/p2p-audit.log`;
|
|
2059
|
-
this.auditStream =
|
|
3001
|
+
this.auditStream = fs3.createWriteStream(logFile, { flags: "a" });
|
|
2060
3002
|
} catch {
|
|
2061
3003
|
this.log(chalk6.yellow("Warning: could not open audit log"));
|
|
2062
3004
|
}
|
|
@@ -2065,11 +3007,11 @@ var P2PServer = class {
|
|
|
2065
3007
|
const useTLS = !!(this.options.tlsCert && this.options.tlsKey);
|
|
2066
3008
|
if (useTLS) {
|
|
2067
3009
|
const tlsOptions = {
|
|
2068
|
-
key:
|
|
2069
|
-
cert:
|
|
3010
|
+
key: fs3.readFileSync(this.options.tlsKey),
|
|
3011
|
+
cert: fs3.readFileSync(this.options.tlsCert)
|
|
2070
3012
|
};
|
|
2071
3013
|
if (this.options.tlsCA) {
|
|
2072
|
-
tlsOptions.ca =
|
|
3014
|
+
tlsOptions.ca = fs3.readFileSync(this.options.tlsCA);
|
|
2073
3015
|
}
|
|
2074
3016
|
this.server = tls.createServer(tlsOptions, (socket) => this.onConnection(socket));
|
|
2075
3017
|
} else {
|
|
@@ -2337,7 +3279,7 @@ async function startP2PServer(options) {
|
|
|
2337
3279
|
// src/p2p/client.ts
|
|
2338
3280
|
import net2 from "net";
|
|
2339
3281
|
import tls2 from "tls";
|
|
2340
|
-
import
|
|
3282
|
+
import fs4 from "fs";
|
|
2341
3283
|
import crypto3 from "crypto";
|
|
2342
3284
|
import chalk7 from "chalk";
|
|
2343
3285
|
var P2PClient = class {
|
|
@@ -2365,9 +3307,9 @@ var P2PClient = class {
|
|
|
2365
3307
|
async connect() {
|
|
2366
3308
|
if (this.options.auditLog) {
|
|
2367
3309
|
try {
|
|
2368
|
-
|
|
3310
|
+
fs4.mkdirSync(this.options.auditLog, { recursive: true });
|
|
2369
3311
|
const logFile = `${this.options.auditLog}/p2p-client-audit.log`;
|
|
2370
|
-
this.auditStream =
|
|
3312
|
+
this.auditStream = fs4.createWriteStream(logFile, { flags: "a" });
|
|
2371
3313
|
} catch {
|
|
2372
3314
|
console.log(chalk7.yellow("Warning: could not open audit log"));
|
|
2373
3315
|
}
|
|
@@ -2382,7 +3324,7 @@ var P2PClient = class {
|
|
|
2382
3324
|
rejectUnauthorized: !this.options.tlsInsecure
|
|
2383
3325
|
};
|
|
2384
3326
|
if (this.options.tlsCA) {
|
|
2385
|
-
tlsOptions.ca =
|
|
3327
|
+
tlsOptions.ca = fs4.readFileSync(this.options.tlsCA);
|
|
2386
3328
|
}
|
|
2387
3329
|
this.socket = tls2.connect(tlsOptions, () => {
|
|
2388
3330
|
this.running = true;
|
|
@@ -2572,6 +3514,29 @@ function deriveHMACKey2(password) {
|
|
|
2572
3514
|
}
|
|
2573
3515
|
|
|
2574
3516
|
// src/live/input.ts
|
|
3517
|
+
function persistConfig() {
|
|
3518
|
+
writeConfig({
|
|
3519
|
+
theme: state.currentTheme,
|
|
3520
|
+
interval: state.interval,
|
|
3521
|
+
exportDir: state.exportDir,
|
|
3522
|
+
detailed: state.detailed,
|
|
3523
|
+
sortMode: state.sortMode,
|
|
3524
|
+
treeView: state.treeView,
|
|
3525
|
+
graphMode: state.graphMode,
|
|
3526
|
+
showGraphs: state.showGraphs,
|
|
3527
|
+
autoLog: state.logging,
|
|
3528
|
+
mouseEnabled: state.mouseEnabled,
|
|
3529
|
+
cpuAlertPct: state.CPU_ALERT_PCT,
|
|
3530
|
+
tempAlertC: state.TEMP_ALERT_C,
|
|
3531
|
+
p2pPort: state.p2pPort,
|
|
3532
|
+
p2pPassword: state.p2pPassword,
|
|
3533
|
+
splashEnabled: state.splashEnabled,
|
|
3534
|
+
splashColorScheme: state.splashColorScheme,
|
|
3535
|
+
splashAnimation: state.splashAnimation,
|
|
3536
|
+
visiblePanels: { ...state.visiblePanels },
|
|
3537
|
+
notificationsEnabled: state.notificationsEnabled
|
|
3538
|
+
});
|
|
3539
|
+
}
|
|
2575
3540
|
function isUpKey(key, str) {
|
|
2576
3541
|
return key.name === "up" || str === "k" || key.sequence === "\x1B[A" || key.sequence === "\x1BOA";
|
|
2577
3542
|
}
|
|
@@ -2617,6 +3582,12 @@ function handleInputModeKey(str, key) {
|
|
|
2617
3582
|
const curIdx = animations.indexOf(state.splashAnimation);
|
|
2618
3583
|
state.splashAnimation = animations[(curIdx + 1) % animations.length];
|
|
2619
3584
|
setStatus(`Splash animation: ${state.splashAnimation}`);
|
|
3585
|
+
} else if (selected === "Notifications") {
|
|
3586
|
+
state.notificationsEnabled = !state.notificationsEnabled;
|
|
3587
|
+
setStatus(`Notifications: ${state.notificationsEnabled ? "on" : "off"}`);
|
|
3588
|
+
} else if (selected === "Temperature Unit") {
|
|
3589
|
+
state.tempUnit = state.tempUnit === "c" ? "f" : "c";
|
|
3590
|
+
setStatus(`Temperature unit: ${state.tempUnit.toUpperCase()}`);
|
|
2620
3591
|
} else {
|
|
2621
3592
|
const toggleKey = getToggleKey(selected);
|
|
2622
3593
|
if (toggleKey) {
|
|
@@ -2631,6 +3602,7 @@ function handleInputModeKey(str, key) {
|
|
|
2631
3602
|
}
|
|
2632
3603
|
invalidateTableCache();
|
|
2633
3604
|
invalidateFrame();
|
|
3605
|
+
persistConfig();
|
|
2634
3606
|
}
|
|
2635
3607
|
render();
|
|
2636
3608
|
return;
|
|
@@ -2843,6 +3815,9 @@ async function startLive(opts2, splashPromise) {
|
|
|
2843
3815
|
if (opts2.theme) state.currentTheme = opts2.theme;
|
|
2844
3816
|
state.interval = opts2.interval;
|
|
2845
3817
|
if (opts2.exportDir) state.exportDir = opts2.exportDir;
|
|
3818
|
+
if (opts2.alertCpu !== void 0) state.CPU_ALERT_PCT = opts2.alertCpu;
|
|
3819
|
+
if (opts2.alertTemp !== void 0) state.TEMP_ALERT_C = opts2.alertTemp;
|
|
3820
|
+
if (opts2.tempUnit) state.tempUnit = opts2.tempUnit;
|
|
2846
3821
|
state.termWidth = process.stdout.columns || 80;
|
|
2847
3822
|
state.termHeight = process.stdout.rows || 24;
|
|
2848
3823
|
state.history.reset();
|
|
@@ -2880,8 +3855,9 @@ async function startLive(opts2, splashPromise) {
|
|
|
2880
3855
|
handleInputModeKey(str, key);
|
|
2881
3856
|
return;
|
|
2882
3857
|
}
|
|
2883
|
-
|
|
2884
|
-
|
|
3858
|
+
const mouse = key.mouse;
|
|
3859
|
+
if (mouse) {
|
|
3860
|
+
handleMouseClick(mouse.y, mouse.x);
|
|
2885
3861
|
return;
|
|
2886
3862
|
}
|
|
2887
3863
|
if (key.sequence && key.sequence.startsWith("\x1B[M")) {
|
|
@@ -2989,6 +3965,12 @@ async function startLive(opts2, splashPromise) {
|
|
|
2989
3965
|
state.detailed = !state.detailed;
|
|
2990
3966
|
setStatus(`Detailed sensor mode: ${state.detailed ? "on" : "off"}`);
|
|
2991
3967
|
break;
|
|
3968
|
+
case "T":
|
|
3969
|
+
state.tempUnit = state.tempUnit === "c" ? "f" : "c";
|
|
3970
|
+
setStatus(`Temperature unit: ${state.tempUnit.toUpperCase()}`);
|
|
3971
|
+
invalidateTableCache();
|
|
3972
|
+
render();
|
|
3973
|
+
break;
|
|
2992
3974
|
case "s":
|
|
2993
3975
|
const sortCycle = ["cpu", "mem", "pid", "user", "command", "state", "threads", "runtime"];
|
|
2994
3976
|
const curIdx = sortCycle.indexOf(state.sortMode);
|
|
@@ -3020,7 +4002,9 @@ async function startLive(opts2, splashPromise) {
|
|
|
3020
4002
|
render();
|
|
3021
4003
|
break;
|
|
3022
4004
|
case "f":
|
|
3023
|
-
|
|
4005
|
+
const fmtCycle = ["json", "csv", "tsv", "html", "md"];
|
|
4006
|
+
const curFmt = fmtCycle.indexOf(state.exportFormat);
|
|
4007
|
+
state.exportFormat = fmtCycle[(curFmt + 1) % fmtCycle.length];
|
|
3024
4008
|
setStatus(`Export format: ${state.exportFormat}`);
|
|
3025
4009
|
render();
|
|
3026
4010
|
break;
|
|
@@ -3089,6 +4073,7 @@ function handleMouseClick(y, x) {
|
|
|
3089
4073
|
}
|
|
3090
4074
|
}
|
|
3091
4075
|
function stopLive() {
|
|
4076
|
+
persistConfig();
|
|
3092
4077
|
if (state.intervalHandle) {
|
|
3093
4078
|
clearInterval(state.intervalHandle);
|
|
3094
4079
|
state.intervalHandle = null;
|
|
@@ -3401,11 +4386,11 @@ function spreadFireSparks(fire, cols, rows) {
|
|
|
3401
4386
|
}
|
|
3402
4387
|
}
|
|
3403
4388
|
}
|
|
3404
|
-
async function showSplash(
|
|
3405
|
-
const enabled =
|
|
4389
|
+
async function showSplash(config3) {
|
|
4390
|
+
const enabled = config3?.enabled ?? true;
|
|
3406
4391
|
if (!enabled) return;
|
|
3407
|
-
const colorScheme =
|
|
3408
|
-
const animation =
|
|
4392
|
+
const colorScheme = config3?.colorScheme ?? "fire";
|
|
4393
|
+
const animation = config3?.animation ?? "classic";
|
|
3409
4394
|
const rgbPalette = getPalette(colorScheme);
|
|
3410
4395
|
const cols = 60;
|
|
3411
4396
|
const rows = 15;
|
|
@@ -3455,13 +4440,14 @@ async function showSplash(config) {
|
|
|
3455
4440
|
}
|
|
3456
4441
|
|
|
3457
4442
|
// src/index.ts
|
|
3458
|
-
var
|
|
4443
|
+
var config2 = readConfig();
|
|
4444
|
+
var pkg = JSON.parse(fs5.readFileSync(new URL("../package.json", import.meta.url), "utf-8"));
|
|
3459
4445
|
var program = new Command();
|
|
3460
|
-
program.name("pyre").version(pkg.version).description("Mac system monitoring CLI: interactive live dashboard, stats, graphs, export, packet monitor, battery predictor").option("-j, --json", "Output as JSON").option("-c, --csv", "Output as CSV").option("-t, --tsv", "Output as TSV").option("--detailed", "Include detailed system info and sensor readings").option("--theme <name>", "Default theme for live mode (default, dracula, cyberpunk, monochrome, nord, gruvbox)",
|
|
4446
|
+
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");
|
|
3461
4447
|
program.parse(process.argv);
|
|
3462
4448
|
var opts = program.opts();
|
|
3463
4449
|
function isExportMode() {
|
|
3464
|
-
return opts.json || opts.csv || opts.tsv;
|
|
4450
|
+
return opts.json || opts.csv || opts.tsv || opts.html || opts.md;
|
|
3465
4451
|
}
|
|
3466
4452
|
function stripAnsi(s) {
|
|
3467
4453
|
return s.replace(/\x1b\[[0-9;]*m/g, "");
|
|
@@ -3470,7 +4456,7 @@ function sanitizeHost2(host) {
|
|
|
3470
4456
|
return host.trim().replace(/%$/, "");
|
|
3471
4457
|
}
|
|
3472
4458
|
function detectLocalIP() {
|
|
3473
|
-
const ifaces =
|
|
4459
|
+
const ifaces = os3.networkInterfaces();
|
|
3474
4460
|
for (const [, addrs] of Object.entries(ifaces)) {
|
|
3475
4461
|
if (!addrs) continue;
|
|
3476
4462
|
for (const addr of addrs) {
|
|
@@ -3504,22 +4490,75 @@ async function runServerCommand() {
|
|
|
3504
4490
|
}
|
|
3505
4491
|
async function main() {
|
|
3506
4492
|
const cmd = program.args[0];
|
|
4493
|
+
if (cmd === "info") {
|
|
4494
|
+
await runInfoCommand();
|
|
4495
|
+
return;
|
|
4496
|
+
}
|
|
4497
|
+
if (cmd === "ssh") {
|
|
4498
|
+
const host = program.args[1];
|
|
4499
|
+
if (!host) {
|
|
4500
|
+
console.log(chalk9.red("Usage: pyre ssh <host>"));
|
|
4501
|
+
process.exit(1);
|
|
4502
|
+
}
|
|
4503
|
+
await runSshCommand(host);
|
|
4504
|
+
return;
|
|
4505
|
+
}
|
|
4506
|
+
if (cmd === "web") {
|
|
4507
|
+
await runWebCommand();
|
|
4508
|
+
return;
|
|
4509
|
+
}
|
|
4510
|
+
if (cmd === "bench") {
|
|
4511
|
+
const benchCmd = program.args.slice(1).join(" ");
|
|
4512
|
+
if (!benchCmd) {
|
|
4513
|
+
console.log(chalk9.red("Usage: pyre bench <command>"));
|
|
4514
|
+
process.exit(1);
|
|
4515
|
+
}
|
|
4516
|
+
await runBenchCommand(benchCmd);
|
|
4517
|
+
return;
|
|
4518
|
+
}
|
|
4519
|
+
if (cmd === "config") {
|
|
4520
|
+
const sub = program.args[1];
|
|
4521
|
+
if (sub === "show") {
|
|
4522
|
+
const cfg = readConfig();
|
|
4523
|
+
console.log(JSON.stringify(cfg, null, 2));
|
|
4524
|
+
} else if (sub === "reset") {
|
|
4525
|
+
fs5.rmSync(CONFIG_FILE, { force: true });
|
|
4526
|
+
console.log(chalk9.green("Config reset to defaults"));
|
|
4527
|
+
} else {
|
|
4528
|
+
console.log("Usage: pyre config <show|reset>");
|
|
4529
|
+
}
|
|
4530
|
+
return;
|
|
4531
|
+
}
|
|
3507
4532
|
if (cmd === "server") {
|
|
3508
4533
|
await runServerCommand();
|
|
3509
4534
|
return;
|
|
3510
4535
|
}
|
|
4536
|
+
const alertCpu = opts.alertCpu ? parseInt(opts.alertCpu, 10) : void 0;
|
|
4537
|
+
const alertTemp = opts.alertTemp ? parseInt(opts.alertTemp, 10) : void 0;
|
|
4538
|
+
const tempUnit = opts.tempUnit === "f" ? "f" : "c";
|
|
3511
4539
|
if (cmd === "live" || !isExportMode() && !opts.once && cmd !== "p2p") {
|
|
4540
|
+
if (!process.stdout.isTTY && cmd !== "live") {
|
|
4541
|
+
await runWebCommand();
|
|
4542
|
+
return;
|
|
4543
|
+
}
|
|
3512
4544
|
if (cmd === "live") {
|
|
3513
4545
|
program.args.shift();
|
|
3514
4546
|
}
|
|
3515
|
-
const splashPromise = showSplash(
|
|
4547
|
+
const splashPromise = showSplash({
|
|
4548
|
+
enabled: config2.splashEnabled,
|
|
4549
|
+
colorScheme: config2.splashColorScheme,
|
|
4550
|
+
animation: config2.splashAnimation
|
|
4551
|
+
});
|
|
3516
4552
|
const interval = parseFloat(opts.interval) || 2;
|
|
3517
4553
|
await startLive({
|
|
3518
4554
|
interval,
|
|
3519
|
-
detailed: opts.detailed,
|
|
3520
|
-
theme: opts.theme,
|
|
3521
|
-
exportDir: opts.exportDir,
|
|
3522
|
-
autoLog: opts.log
|
|
4555
|
+
detailed: opts.detailed ?? config2.detailed,
|
|
4556
|
+
theme: opts.theme || config2.theme,
|
|
4557
|
+
exportDir: opts.exportDir || config2.exportDir,
|
|
4558
|
+
autoLog: opts.log || config2.autoLog,
|
|
4559
|
+
alertCpu,
|
|
4560
|
+
alertTemp,
|
|
4561
|
+
tempUnit
|
|
3523
4562
|
}, splashPromise);
|
|
3524
4563
|
return;
|
|
3525
4564
|
}
|
|
@@ -3541,12 +4580,14 @@ async function main() {
|
|
|
3541
4580
|
const data = await collectAll({ detailed: opts.detailed, processLimit });
|
|
3542
4581
|
let output;
|
|
3543
4582
|
if (opts.json) output = formatJson(data);
|
|
4583
|
+
else if (opts.html) output = formatHtml(data);
|
|
4584
|
+
else if (opts.md) output = formatMarkdown(data);
|
|
3544
4585
|
else if (opts.csv) output = formatCsv(data);
|
|
3545
4586
|
else if (opts.tsv) output = formatTsv(data);
|
|
3546
|
-
else output = formatTable(data, { width: process.stdout.columns || 80, sortBy: opts.sort, treeView: opts.tree, visible: { packets: opts.packets ? true : void 0 } });
|
|
4587
|
+
else output = formatTable(data, { width: process.stdout.columns || 80, sortBy: opts.sort, treeView: opts.tree ?? config2.treeView, visible: { packets: opts.packets ? true : void 0 } });
|
|
3547
4588
|
console.log(output);
|
|
3548
4589
|
if (opts.out) {
|
|
3549
|
-
|
|
4590
|
+
fs5.writeFileSync(opts.out, stripAnsi(output));
|
|
3550
4591
|
console.log(chalk9.dim(`
|
|
3551
4592
|
Saved to ${opts.out}`));
|
|
3552
4593
|
}
|
|
@@ -3728,3 +4769,135 @@ async function runP2PConnect() {
|
|
|
3728
4769
|
process.exit(0);
|
|
3729
4770
|
});
|
|
3730
4771
|
}
|
|
4772
|
+
async function runInfoCommand() {
|
|
4773
|
+
const data = await collectAll({ detailed: true });
|
|
4774
|
+
const lines = [];
|
|
4775
|
+
lines.push(chalk9.bold(" pyre info"));
|
|
4776
|
+
lines.push("");
|
|
4777
|
+
lines.push(` Hostname: ${data.header.hostname}`);
|
|
4778
|
+
lines.push(` OS: ${data.header.os}`);
|
|
4779
|
+
lines.push(` Uptime: ${data.header.uptime}`);
|
|
4780
|
+
lines.push(` CPU: ${data.cpu.brand}`);
|
|
4781
|
+
lines.push(` Cores: ${data.cpu.physicalCores}/${data.cpu.cores} (phys/log)`);
|
|
4782
|
+
lines.push(` Frequency: ${data.cpu.frequency} MHz`);
|
|
4783
|
+
lines.push(` Memory: ${formatBytes(data.memory.total)}`);
|
|
4784
|
+
if (data.gpu) lines.push(` GPU: ${data.gpu.model} (${formatBytes(data.gpu.memory)})`);
|
|
4785
|
+
if (data.battery) {
|
|
4786
|
+
lines.push(` Battery: ${data.battery.level}% (${data.battery.state})`);
|
|
4787
|
+
lines.push(` Health: ${data.battery.health}`);
|
|
4788
|
+
}
|
|
4789
|
+
lines.push(` Thermal: ${data.thermal.state}`);
|
|
4790
|
+
console.log(lines.join("\n"));
|
|
4791
|
+
}
|
|
4792
|
+
async function runSshCommand(host) {
|
|
4793
|
+
const password = opts.p2pPassword || "mysecret";
|
|
4794
|
+
const cmd = `ssh -o LogLevel=QUIET ${host} "pyre --json --once"`;
|
|
4795
|
+
console.log(chalk9.bold(`
|
|
4796
|
+
pyre ssh \u2014 ${host}`));
|
|
4797
|
+
console.log(chalk9.dim(` Running: ${cmd}
|
|
4798
|
+
`));
|
|
4799
|
+
const { execFile: execFile2 } = await import("child_process");
|
|
4800
|
+
const child = execFile2("ssh", ["-o", "LogLevel=QUIET", host, "pyre", "--json", "--once"], (err, stdout, stderr) => {
|
|
4801
|
+
if (err) {
|
|
4802
|
+
console.log(chalk9.red(` SSH failed: ${err.message}`));
|
|
4803
|
+
console.log(chalk9.dim(" Ensure SSH keys are set up or the host is reachable."));
|
|
4804
|
+
process.exit(1);
|
|
4805
|
+
}
|
|
4806
|
+
if (stderr) console.log(chalk9.dim(stderr));
|
|
4807
|
+
try {
|
|
4808
|
+
const remote = JSON.parse(stdout);
|
|
4809
|
+
console.log(formatTable(remote, { width: process.stdout.columns || 80, sortBy: "cpu", treeView: false, visible: { packets: true } }));
|
|
4810
|
+
} catch {
|
|
4811
|
+
console.log(stdout);
|
|
4812
|
+
}
|
|
4813
|
+
process.exit(0);
|
|
4814
|
+
});
|
|
4815
|
+
child.stdin?.end();
|
|
4816
|
+
process.once("SIGINT", () => {
|
|
4817
|
+
child.kill("SIGTERM");
|
|
4818
|
+
process.exit(0);
|
|
4819
|
+
});
|
|
4820
|
+
}
|
|
4821
|
+
async function runWebCommand() {
|
|
4822
|
+
const http = await import("http");
|
|
4823
|
+
const port = parseInt(opts.port || process.env.PORT || "3000", 10) || 3e3;
|
|
4824
|
+
const server = http.createServer(async (req, res) => {
|
|
4825
|
+
const url = req.url || "/";
|
|
4826
|
+
if (url === "/" || url === "/index.html") {
|
|
4827
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
4828
|
+
const indexPath = path3.join(process.cwd(), "index.html");
|
|
4829
|
+
let htmlContent = "";
|
|
4830
|
+
if (fs5.existsSync(indexPath)) {
|
|
4831
|
+
htmlContent = fs5.readFileSync(indexPath, "utf-8");
|
|
4832
|
+
} else {
|
|
4833
|
+
const data = await collectAll({ detailed: true });
|
|
4834
|
+
htmlContent = formatHtml(data);
|
|
4835
|
+
}
|
|
4836
|
+
res.end(htmlContent);
|
|
4837
|
+
} else if (url === "/api" || url === "/api/stats" || url === "/data") {
|
|
4838
|
+
const data = await collectAll({ detailed: true });
|
|
4839
|
+
res.writeHead(200, {
|
|
4840
|
+
"Content-Type": "application/json",
|
|
4841
|
+
"Access-Control-Allow-Origin": "*",
|
|
4842
|
+
"Access-Control-Allow-Methods": "GET, OPTIONS"
|
|
4843
|
+
});
|
|
4844
|
+
res.end(JSON.stringify(data, null, 2));
|
|
4845
|
+
} else {
|
|
4846
|
+
res.writeHead(404);
|
|
4847
|
+
res.end("Not found");
|
|
4848
|
+
}
|
|
4849
|
+
});
|
|
4850
|
+
server.listen(port, "0.0.0.0", () => {
|
|
4851
|
+
console.log(chalk9.bold(`
|
|
4852
|
+
pyre web server running on port ${port}`));
|
|
4853
|
+
console.log(chalk9.dim(` Dashboard: http://0.0.0.0:${port}/`));
|
|
4854
|
+
console.log(chalk9.dim(` API: http://0.0.0.0:${port}/api`));
|
|
4855
|
+
console.log(chalk9.dim(" Press Ctrl+C to stop\n"));
|
|
4856
|
+
});
|
|
4857
|
+
process.once("SIGINT", () => {
|
|
4858
|
+
server.close();
|
|
4859
|
+
process.exit(0);
|
|
4860
|
+
});
|
|
4861
|
+
}
|
|
4862
|
+
async function runBenchCommand(benchCmd) {
|
|
4863
|
+
const { spawn } = await import("child_process");
|
|
4864
|
+
const logDir = opts.exportDir || "./pyre-exports";
|
|
4865
|
+
const file = path3.join(logDir, `pyre-bench-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.csv`);
|
|
4866
|
+
if (!fs5.existsSync(logDir)) fs5.mkdirSync(logDir, { recursive: true });
|
|
4867
|
+
const stream = fs5.createWriteStream(file, { flags: "a" });
|
|
4868
|
+
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");
|
|
4869
|
+
console.log(chalk9.bold(`
|
|
4870
|
+
pyre bench`));
|
|
4871
|
+
console.log(chalk9.dim(` Command: ${benchCmd}`));
|
|
4872
|
+
console.log(chalk9.dim(` Log: ${file}
|
|
4873
|
+
`));
|
|
4874
|
+
const child = spawn("sh", ["-c", benchCmd]);
|
|
4875
|
+
child.stdout.on("data", (d) => process.stdout.write(d));
|
|
4876
|
+
child.stderr.on("data", (d) => process.stdout.write(d));
|
|
4877
|
+
const interval = parseFloat(opts.interval) || 2;
|
|
4878
|
+
const handle = setInterval(async () => {
|
|
4879
|
+
try {
|
|
4880
|
+
const data = await collectAll({ detailed: opts.detailed });
|
|
4881
|
+
const temp = data.cpu.temperature ?? data.thermal.temperatures?.cpu_die ?? "";
|
|
4882
|
+
const rxPackets = data.network.rxPackets ?? 0;
|
|
4883
|
+
const txPackets = data.network.txPackets ?? 0;
|
|
4884
|
+
const connections = data.network.connections ?? 0;
|
|
4885
|
+
stream.write(`${data.timestamp},${data.cpu.usage},${data.memory.usagePercent},${temp},${data.network.rxBytes},${data.network.txBytes},${rxPackets},${txPackets},${connections},${data.thermal.state}
|
|
4886
|
+
`);
|
|
4887
|
+
} catch {
|
|
4888
|
+
}
|
|
4889
|
+
}, interval * 1e3);
|
|
4890
|
+
child.on("close", () => {
|
|
4891
|
+
clearInterval(handle);
|
|
4892
|
+
stream.end();
|
|
4893
|
+
console.log(chalk9.green(`
|
|
4894
|
+
Bench complete. Log saved to ${file}`));
|
|
4895
|
+
process.exit(0);
|
|
4896
|
+
});
|
|
4897
|
+
process.once("SIGINT", () => {
|
|
4898
|
+
clearInterval(handle);
|
|
4899
|
+
child.kill("SIGTERM");
|
|
4900
|
+
stream.end();
|
|
4901
|
+
process.exit(0);
|
|
4902
|
+
});
|
|
4903
|
+
}
|