pyre-cli 5.1.0 → 6.1.0

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