pyre-cli 6.0.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";
@@ -472,9 +472,41 @@ async function collectMemory() {
472
472
  usagePercent
473
473
  };
474
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
+ }
475
504
  async function collectDisk() {
476
- const raw = (await run("df -h")).trim();
477
- 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);
478
510
  return lines.map((line) => {
479
511
  const parts = line.split(/\s+/);
480
512
  return {
@@ -483,7 +515,9 @@ async function collectDisk() {
483
515
  used: parts[2],
484
516
  available: parts[3],
485
517
  capacity: parts[4],
486
- mountpoint: parts[5] || parts[6] || "/"
518
+ mountpoint: parts[5] || parts[6] || "/",
519
+ readBytesSec: ioRates.readBytesSec,
520
+ writeBytesSec: ioRates.writeBytesSec
487
521
  };
488
522
  }).filter((d) => d.filesystem.includes("/") || d.size.includes("G") || d.size.includes("T"));
489
523
  }
@@ -791,7 +825,10 @@ function multiRowSparkline(values, height, opts2 = {}) {
791
825
 
792
826
  // src/formatters/themes.ts
793
827
  import chalk2 from "chalk";
794
- var THEMES = {
828
+ import fs from "fs";
829
+ import os2 from "os";
830
+ import path from "path";
831
+ var BUILTIN_THEMES = {
795
832
  default: {
796
833
  border: chalk2.hex("#4b5563"),
797
834
  cpu: chalk2.hex("#22d3ee"),
@@ -871,6 +908,42 @@ var THEMES = {
871
908
  process: chalk2.hex("#ebdbb2")
872
909
  }
873
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();
874
947
 
875
948
  // src/formatters/render.ts
876
949
  function visLen(s) {
@@ -1405,7 +1478,7 @@ function btopCpuBox(data, width, theme, opts2) {
1405
1478
  if (data.cpu.coreUsage && data.cpu.coreUsage.length) {
1406
1479
  const coreColW = Math.floor((rightW - 1) / 2);
1407
1480
  const half = Math.ceil(data.cpu.coreUsage.length / 2);
1408
- for (let i = 0; i < Math.min(2, half); i++) {
1481
+ for (let i = 0; i < half; i++) {
1409
1482
  const u1 = data.cpu.coreUsage[i];
1410
1483
  const u2 = data.cpu.coreUsage[i + half];
1411
1484
  const c1BarW = Math.max(3, coreColW - 8);
@@ -1447,9 +1520,18 @@ function btopLeftColumn(data, width, theme, opts2) {
1447
1520
  for (const d of data.disk.slice(0, 2)) {
1448
1521
  const capNum = parseInt(d.capacity, 10) || 0;
1449
1522
  const mountShort = truncatePlain(d.mountpoint, 8);
1450
- diskLines.push(`${mountShort} ${d.used}/${d.size} [${gaugeBar(capNum, Math.max(3, contentWidth - 18))}] ${d.capacity}`);
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}`);
1451
1530
  }
1452
- out.push(...panel("0 disk io", diskLines, width, theme.disk, theme.border, diskLines.length));
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));
1453
1535
  }
1454
1536
  const netLines = [];
1455
1537
  const rxRate = opts2.history?.netRxRate.length ? opts2.history.netRxRate[opts2.history.netRxRate.length - 1] : 0;
@@ -2152,11 +2234,11 @@ var History = class _History {
2152
2234
  };
2153
2235
 
2154
2236
  // src/state/config.ts
2155
- import fs from "fs";
2156
- import os2 from "os";
2157
- import path from "path";
2158
- var CONFIG_DIR = path.join(os2.homedir(), ".config", "pyre");
2159
- var CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
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");
2160
2242
  var DEFAULT_CONFIG = {
2161
2243
  theme: "default",
2162
2244
  interval: 2,
@@ -2170,6 +2252,9 @@ var DEFAULT_CONFIG = {
2170
2252
  mouseEnabled: true,
2171
2253
  cpuAlertPct: 90,
2172
2254
  tempAlertC: 95,
2255
+ watchdogProcess: "",
2256
+ watchdogCpu: 80,
2257
+ watchdogMem: 80,
2173
2258
  notificationsEnabled: true,
2174
2259
  p2pPort: 9876,
2175
2260
  p2pPassword: "pyre",
@@ -2193,8 +2278,8 @@ var DEFAULT_CONFIG = {
2193
2278
  };
2194
2279
  function readConfig() {
2195
2280
  try {
2196
- if (!fs.existsSync(CONFIG_FILE)) return { ...DEFAULT_CONFIG };
2197
- 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");
2198
2283
  const parsed = JSON.parse(raw);
2199
2284
  return deepMerge(DEFAULT_CONFIG, parsed);
2200
2285
  } catch {
@@ -2203,11 +2288,11 @@ function readConfig() {
2203
2288
  }
2204
2289
  function writeConfig(config3) {
2205
2290
  try {
2206
- if (!fs.existsSync(CONFIG_DIR)) {
2207
- fs.mkdirSync(CONFIG_DIR, { recursive: true });
2291
+ if (!fs2.existsSync(CONFIG_DIR)) {
2292
+ fs2.mkdirSync(CONFIG_DIR, { recursive: true });
2208
2293
  }
2209
2294
  const merged = deepMerge(readConfig(), config3);
2210
- fs.writeFileSync(CONFIG_FILE, JSON.stringify(merged, null, 2) + "\n");
2295
+ fs2.writeFileSync(CONFIG_FILE, JSON.stringify(merged, null, 2) + "\n");
2211
2296
  } catch {
2212
2297
  }
2213
2298
  }
@@ -2295,6 +2380,9 @@ var state = {
2295
2380
  ],
2296
2381
  CPU_ALERT_PCT: config.cpuAlertPct,
2297
2382
  TEMP_ALERT_C: config.tempAlertC,
2383
+ watchdogProcess: config.watchdogProcess || "",
2384
+ watchdogCpu: config.watchdogCpu || 80,
2385
+ watchdogMem: config.watchdogMem || 80,
2298
2386
  notificationsEnabled: config.notificationsEnabled,
2299
2387
  alerted: false,
2300
2388
  activePanel: "grid",
@@ -2349,10 +2437,10 @@ function getToggleKey(opt) {
2349
2437
  }
2350
2438
 
2351
2439
  // src/live/export.ts
2352
- import fs2 from "fs";
2353
- import path2 from "path";
2440
+ import fs3 from "fs";
2441
+ import path3 from "path";
2354
2442
  function ensureDir(dir) {
2355
- if (!fs2.existsSync(dir)) fs2.mkdirSync(dir, { recursive: true });
2443
+ if (!fs3.existsSync(dir)) fs3.mkdirSync(dir, { recursive: true });
2356
2444
  }
2357
2445
  function timestampForFile() {
2358
2446
  return (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
@@ -2365,7 +2453,7 @@ function exportSnapshot() {
2365
2453
  try {
2366
2454
  ensureDir(state.exportDir);
2367
2455
  const ext = state.exportFormat === "json" ? "json" : state.exportFormat === "csv" ? "csv" : state.exportFormat === "tsv" ? "tsv" : state.exportFormat === "html" ? "html" : "md";
2368
- const file = path2.join(state.exportDir, `pyre-${timestampForFile()}.${ext}`);
2456
+ const file = path3.join(state.exportDir, `pyre-${timestampForFile()}.${ext}`);
2369
2457
  let content;
2370
2458
  switch (state.exportFormat) {
2371
2459
  case "json":
@@ -2384,7 +2472,7 @@ function exportSnapshot() {
2384
2472
  content = formatMarkdown(state.lastData);
2385
2473
  break;
2386
2474
  }
2387
- fs2.writeFileSync(file, content);
2475
+ fs3.writeFileSync(file, content);
2388
2476
  setStatus(`Exported snapshot -> ${file}`);
2389
2477
  } catch (err) {
2390
2478
  setStatus(`Export failed: ${err.message}`);
@@ -2394,8 +2482,8 @@ function startLogging() {
2394
2482
  try {
2395
2483
  ensureDir(state.exportDir);
2396
2484
  rotateLogs();
2397
- const file = path2.join(state.exportDir, `pyre-log-${timestampForFile()}.csv`);
2398
- 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" });
2399
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");
2400
2488
  state.logging = true;
2401
2489
  setStatus(`Logging every tick -> ${file}`);
@@ -2409,37 +2497,37 @@ var MAX_LOG_BYTES = 50 * 1024 * 1024;
2409
2497
  function rotateLogs() {
2410
2498
  try {
2411
2499
  const dir = state.exportDir;
2412
- if (!fs2.existsSync(dir)) return;
2413
- const files = fs2.readdirSync(dir).filter((f) => f.startsWith("pyre-log-") && f.endsWith(".csv")).map((f) => ({ name: f, path: path2.join(dir, f), mtime: fs2.statSync(path2.join(dir, f)).mtimeMs, size: fs2.statSync(path2.join(dir, f)).size })).sort((a, b) => a.mtime - b.mtime);
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);
2414
2502
  const now = Date.now();
2415
2503
  for (const f of files) {
2416
2504
  if (now - f.mtime > MAX_LOG_AGE_MS) {
2417
2505
  try {
2418
- fs2.unlinkSync(f.path);
2506
+ fs3.unlinkSync(f.path);
2419
2507
  } catch {
2420
2508
  }
2421
2509
  }
2422
2510
  }
2423
- 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"));
2424
2512
  if (remaining.length > MAX_LOG_COUNT) {
2425
2513
  const toDelete = remaining.sort().slice(0, remaining.length - MAX_LOG_COUNT);
2426
2514
  for (const f of toDelete) {
2427
2515
  try {
2428
- fs2.unlinkSync(path2.join(dir, f));
2516
+ fs3.unlinkSync(path3.join(dir, f));
2429
2517
  } catch {
2430
2518
  }
2431
2519
  }
2432
2520
  }
2433
2521
  let totalSize = 0;
2434
2522
  for (const f of remaining) {
2435
- totalSize += fs2.statSync(path2.join(dir, f)).size;
2523
+ totalSize += fs3.statSync(path3.join(dir, f)).size;
2436
2524
  }
2437
2525
  if (totalSize > MAX_LOG_BYTES) {
2438
2526
  const sorted = remaining.sort();
2439
2527
  for (const f of sorted) {
2440
- const sz = fs2.statSync(path2.join(dir, f)).size;
2528
+ const sz = fs3.statSync(path3.join(dir, f)).size;
2441
2529
  try {
2442
- fs2.unlinkSync(path2.join(dir, f));
2530
+ fs3.unlinkSync(path3.join(dir, f));
2443
2531
  } catch {
2444
2532
  }
2445
2533
  totalSize -= sz;
@@ -2634,13 +2722,15 @@ function p2pPanelLines() {
2634
2722
  const title = `P2P \xB7 ${state.p2pServerRunning ? "ON" : "OFF"} \xB7 ${events.length} events`;
2635
2723
  return { title, body };
2636
2724
  }
2637
- function renderCustomizerOverlay() {
2725
+ function renderCustomizerOverlay(scrollOffset, maxVisibleItems) {
2638
2726
  const themesList = Object.keys(THEMES);
2639
- const lines = [
2640
- chalk5.bgCyan.black.bold(" UI CUSTOMIZER (Use \u2191/\u2193 or J/K to navigate, Enter/Space to toggle, Esc to exit) ")
2641
- ];
2642
- state.CUSTOMIZER_OPTIONS.forEach((opt, idx) => {
2643
- 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;
2644
2734
  const prefix = isSelected ? chalk5.yellow("\u25B6 ") : " ";
2645
2735
  if (opt === "Theme") {
2646
2736
  lines.push(`${prefix}${opt}: ${chalk5.bold.green(state.currentTheme)} [${themesList.join(", ")}]`);
@@ -2664,7 +2754,7 @@ function renderCustomizerOverlay() {
2664
2754
  const status = isVisible ? chalk5.green("[VISIBLE]") : chalk5.red("[HIDDEN]");
2665
2755
  lines.push(`${prefix}${opt}: ${status}`);
2666
2756
  }
2667
- });
2757
+ }
2668
2758
  return lines.map((l) => ` ${l}`).join("\n");
2669
2759
  }
2670
2760
  var _prevFrameLines = [];
@@ -2752,9 +2842,18 @@ function render() {
2752
2842
  const footerLine22 = footerLine()[1] || "";
2753
2843
  const footerLine3 = footerLine()[2] || " ";
2754
2844
  const footerLines = [footerLine1, footerLine22, footerLine3];
2845
+ const footerHeight = 3;
2846
+ const targetBodyHeight = Math.max(1, state.termHeight - footerHeight);
2755
2847
  if (state.inputMode === "customizer") {
2756
- bodyLines.push("");
2757
- bodyLines.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"));
2758
2857
  } else if (state.inputMode === "filter") {
2759
2858
  bodyLines.push(chalk5.cyan(` Filter processes: ${state.inputBuffer}_`));
2760
2859
  } else if (state.inputMode === "kill") {
@@ -2770,8 +2869,6 @@ function render() {
2770
2869
  } else if (state.statusMessage) {
2771
2870
  bodyLines.push(` ${state.statusMessage}`);
2772
2871
  }
2773
- const footerHeight = 3;
2774
- const targetBodyHeight = Math.max(1, state.termHeight - footerHeight);
2775
2872
  if (bodyLines.length > targetBodyHeight) {
2776
2873
  bodyLines.length = targetBodyHeight;
2777
2874
  }
@@ -2831,19 +2928,30 @@ function checkAlerts(data) {
2831
2928
  const anomalies = detectAnomalies(data, state.history);
2832
2929
  const hasCriticalAnomaly = anomalies.some((a) => a.severity === "critical");
2833
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
+ }
2834
2941
  const anomalyTriggered = hasCriticalAnomaly || hasWarningAnomaly;
2835
- if ((hot || anomalyTriggered) && !state.alerted) {
2942
+ if ((hot || anomalyTriggered || watchdogTriggered) && !state.alerted) {
2836
2943
  state.alerted = true;
2837
2944
  process.stdout.write("\x07");
2838
2945
  const reasons = [];
2839
2946
  if (data.cpu.usage >= state.CPU_ALERT_PCT) reasons.push(`CPU at ${data.cpu.usage}%`);
2840
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`);
2841
2949
  for (const a of anomalies) {
2842
2950
  reasons.push(`${a.metric} anomaly (\u03C3=${a.zScore.toFixed(1)})`);
2843
2951
  }
2844
2952
  setStatus(chalk5.red(`\u26A0 Alert: ${reasons.join("; ")}`), 5e3);
2845
2953
  sendNotification("pyre Alert", reasons.join("; "));
2846
- } else if (!hot && !anomalyTriggered) {
2954
+ } else if (!hot && !anomalyTriggered && !watchdogTriggered) {
2847
2955
  state.alerted = false;
2848
2956
  }
2849
2957
  }
@@ -2851,7 +2959,7 @@ function checkAlerts(data) {
2851
2959
  // src/p2p/server.ts
2852
2960
  import net from "net";
2853
2961
  import tls from "tls";
2854
- import fs3 from "fs";
2962
+ import fs4 from "fs";
2855
2963
  import crypto2 from "crypto";
2856
2964
  import chalk6 from "chalk";
2857
2965
 
@@ -2996,9 +3104,9 @@ var P2PServer = class {
2996
3104
  async start() {
2997
3105
  if (this.options.auditLog) {
2998
3106
  try {
2999
- fs3.mkdirSync(this.options.auditLog, { recursive: true });
3107
+ fs4.mkdirSync(this.options.auditLog, { recursive: true });
3000
3108
  const logFile = `${this.options.auditLog}/p2p-audit.log`;
3001
- this.auditStream = fs3.createWriteStream(logFile, { flags: "a" });
3109
+ this.auditStream = fs4.createWriteStream(logFile, { flags: "a" });
3002
3110
  } catch {
3003
3111
  this.log(chalk6.yellow("Warning: could not open audit log"));
3004
3112
  }
@@ -3007,11 +3115,11 @@ var P2PServer = class {
3007
3115
  const useTLS = !!(this.options.tlsCert && this.options.tlsKey);
3008
3116
  if (useTLS) {
3009
3117
  const tlsOptions = {
3010
- key: fs3.readFileSync(this.options.tlsKey),
3011
- cert: fs3.readFileSync(this.options.tlsCert)
3118
+ key: fs4.readFileSync(this.options.tlsKey),
3119
+ cert: fs4.readFileSync(this.options.tlsCert)
3012
3120
  };
3013
3121
  if (this.options.tlsCA) {
3014
- tlsOptions.ca = fs3.readFileSync(this.options.tlsCA);
3122
+ tlsOptions.ca = fs4.readFileSync(this.options.tlsCA);
3015
3123
  }
3016
3124
  this.server = tls.createServer(tlsOptions, (socket) => this.onConnection(socket));
3017
3125
  } else {
@@ -3279,7 +3387,7 @@ async function startP2PServer(options) {
3279
3387
  // src/p2p/client.ts
3280
3388
  import net2 from "net";
3281
3389
  import tls2 from "tls";
3282
- import fs4 from "fs";
3390
+ import fs5 from "fs";
3283
3391
  import crypto3 from "crypto";
3284
3392
  import chalk7 from "chalk";
3285
3393
  var P2PClient = class {
@@ -3307,9 +3415,9 @@ var P2PClient = class {
3307
3415
  async connect() {
3308
3416
  if (this.options.auditLog) {
3309
3417
  try {
3310
- fs4.mkdirSync(this.options.auditLog, { recursive: true });
3418
+ fs5.mkdirSync(this.options.auditLog, { recursive: true });
3311
3419
  const logFile = `${this.options.auditLog}/p2p-client-audit.log`;
3312
- this.auditStream = fs4.createWriteStream(logFile, { flags: "a" });
3420
+ this.auditStream = fs5.createWriteStream(logFile, { flags: "a" });
3313
3421
  } catch {
3314
3422
  console.log(chalk7.yellow("Warning: could not open audit log"));
3315
3423
  }
@@ -3324,7 +3432,7 @@ var P2PClient = class {
3324
3432
  rejectUnauthorized: !this.options.tlsInsecure
3325
3433
  };
3326
3434
  if (this.options.tlsCA) {
3327
- tlsOptions.ca = fs4.readFileSync(this.options.tlsCA);
3435
+ tlsOptions.ca = fs5.readFileSync(this.options.tlsCA);
3328
3436
  }
3329
3437
  this.socket = tls2.connect(tlsOptions, () => {
3330
3438
  this.running = true;
@@ -3678,11 +3786,19 @@ function handleInputModeKey(str, key) {
3678
3786
  }
3679
3787
  if (key.name === "backspace") {
3680
3788
  state.inputBuffer = state.inputBuffer.slice(0, -1);
3789
+ if (state.inputMode === "filter") {
3790
+ state.processFilter = state.inputBuffer;
3791
+ invalidateTableCache();
3792
+ }
3681
3793
  render();
3682
3794
  return;
3683
3795
  }
3684
3796
  if (str && str.length === 1 && !key.ctrl && !key.meta) {
3685
3797
  state.inputBuffer += str;
3798
+ if (state.inputMode === "filter") {
3799
+ state.processFilter = state.inputBuffer;
3800
+ invalidateTableCache();
3801
+ }
3686
3802
  render();
3687
3803
  }
3688
3804
  }
@@ -4439,11 +4555,106 @@ async function showSplash(config3) {
4439
4555
  });
4440
4556
  }
4441
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
+
4442
4653
  // src/index.ts
4443
4654
  var config2 = readConfig();
4444
- 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"));
4445
4656
  var program = new Command();
4446
- program.name("pyre").version(pkg.version).description("Mac system monitoring CLI: interactive live dashboard, stats, graphs, export, packet monitor, battery predictor").option("-j, --json", "Output as JSON").option("--html", "Output as HTML").option("--md", "Output as Markdown").option("-c, --csv", "Output as CSV").option("-t, --tsv", "Output as TSV").option("--detailed", "Include detailed system info and sensor readings").option("--theme <name>", "Default theme for live mode (default, dracula, cyberpunk, monochrome, nord, gruvbox)", config2.theme).option("--interval <seconds>", "Refresh interval for live mode", String(config2.interval)).option("--once", "Show a single static snapshot instead of live feed").option("--out <file>", "Also write the snapshot output to a file (--once/--json/--csv/--tsv modes)").option("--export-dir <dir>", "Directory used for live-mode snapshot exports and logs", config2.exportDir).option("--log", "Start continuous CSV logging immediately when live mode starts").option("--tree", "Show process tree view instead of flat list").option("--sort <key>", "Sort processes by: cpu, mem, pid, user, command, state, threads, runtime", config2.sortMode).option("--packets", "Include packet monitor panel in output").option("--limit <n>", "Max number of processes to include in --once/--json/--csv/--tsv snapshots (0 = all)", "10").option("--alert-cpu <pct>", "CPU usage alert threshold (default: 90)", String(config2.cpuAlertPct)).option("--alert-temp <c>", "CPU temperature alert threshold in Celsius (default: 95)", String(config2.tempAlertC)).option("--temp-unit <unit>", "Temperature display unit: c or f (default: c)", "c").option("--p2p-host <host>", "P2P host address (server: bind address, client: server address)").option("--p2p-port <port>", "P2P port number", "9876").option("--p2p-password <password>", "Password for P2P authentication").option("--p2p-tls", "Enable TLS encryption for P2P connections").option("--p2p-cert <file>", "TLS certificate file (PEM) for P2P server").option("--p2p-key <file>", "TLS private key file (PEM) for P2P server").option("--p2p-ca <file>", "TLS CA certificate file (PEM) for P2P client").option("--p2p-insecure", "Skip TLS certificate verification (client only)").option("--p2p-rate-limit <n>", "Max auth attempts per IP per minute (default: 5)", "5").option("--p2p-allow <ips>", "Comma-separated list of allowed IPs (empty = all)").option("--p2p-deny <ips>", "Comma-separated list of denied IPs").option("--p2p-audit-log <dir>", "Directory for P2P audit logs").option("--p2p-hmac-key <key>", "HMAC key for message signing (default: derived from password)").option("--port <port>", "Port number for web server mode", "3000");
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");
4447
4658
  program.parse(process.argv);
4448
4659
  var opts = program.opts();
4449
4660
  function isExportMode() {
@@ -4456,7 +4667,7 @@ function sanitizeHost2(host) {
4456
4667
  return host.trim().replace(/%$/, "");
4457
4668
  }
4458
4669
  function detectLocalIP() {
4459
- const ifaces = os3.networkInterfaces();
4670
+ const ifaces = os4.networkInterfaces();
4460
4671
  for (const [, addrs] of Object.entries(ifaces)) {
4461
4672
  if (!addrs) continue;
4462
4673
  for (const addr of addrs) {
@@ -4470,34 +4681,46 @@ function detectLocalIP() {
4470
4681
  async function runServerCommand() {
4471
4682
  const ip = detectLocalIP();
4472
4683
  if (!ip) {
4473
- 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."));
4474
4685
  process.exit(1);
4475
4686
  }
4476
4687
  const port = 9876;
4477
4688
  const password = "mysecret";
4478
- console.log(chalk9.bold(`
4689
+ console.log(chalk11.bold(`
4479
4690
  pyre server`));
4480
- console.log(chalk9.dim(` Detected IP: ${ip}`));
4481
- console.log(chalk9.dim(` Port: ${port}`));
4482
- 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}`));
4483
4694
  console.log();
4484
- console.log(chalk9.bold("Run this on THIS machine (server):"));
4485
- 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}`));
4486
4697
  console.log();
4487
- console.log(chalk9.bold("Run this on the OTHER machine (client):"));
4488
- 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}`));
4489
4700
  console.log();
4490
4701
  }
4491
4702
  async function main() {
4492
4703
  const cmd = program.args[0];
4493
- if (cmd === "info") {
4494
- 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);
4495
4718
  return;
4496
4719
  }
4497
4720
  if (cmd === "ssh") {
4498
4721
  const host = program.args[1];
4499
4722
  if (!host) {
4500
- console.log(chalk9.red("Usage: pyre ssh <host>"));
4723
+ console.log(chalk11.red("Usage: pyre ssh <host>"));
4501
4724
  process.exit(1);
4502
4725
  }
4503
4726
  await runSshCommand(host);
@@ -4510,7 +4733,7 @@ async function main() {
4510
4733
  if (cmd === "bench") {
4511
4734
  const benchCmd = program.args.slice(1).join(" ");
4512
4735
  if (!benchCmd) {
4513
- console.log(chalk9.red("Usage: pyre bench <command>"));
4736
+ console.log(chalk11.red("Usage: pyre bench <command>"));
4514
4737
  process.exit(1);
4515
4738
  }
4516
4739
  await runBenchCommand(benchCmd);
@@ -4522,8 +4745,8 @@ async function main() {
4522
4745
  const cfg = readConfig();
4523
4746
  console.log(JSON.stringify(cfg, null, 2));
4524
4747
  } else if (sub === "reset") {
4525
- fs5.rmSync(CONFIG_FILE, { force: true });
4526
- console.log(chalk9.green("Config reset to defaults"));
4748
+ fs8.rmSync(CONFIG_FILE, { force: true });
4749
+ console.log(chalk11.green("Config reset to defaults"));
4527
4750
  } else {
4528
4751
  console.log("Usage: pyre config <show|reset>");
4529
4752
  }
@@ -4571,7 +4794,7 @@ async function main() {
4571
4794
  await runP2PConnect();
4572
4795
  return;
4573
4796
  } else {
4574
- console.log(chalk9.red("Usage: pyre p2p <server|connect> [options]"));
4797
+ console.log(chalk11.red("Usage: pyre p2p <server|connect> [options]"));
4575
4798
  process.exit(1);
4576
4799
  }
4577
4800
  }
@@ -4587,13 +4810,13 @@ async function main() {
4587
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 } });
4588
4811
  console.log(output);
4589
4812
  if (opts.out) {
4590
- fs5.writeFileSync(opts.out, stripAnsi(output));
4591
- console.log(chalk9.dim(`
4813
+ fs8.writeFileSync(opts.out, stripAnsi(output));
4814
+ console.log(chalk11.dim(`
4592
4815
  Saved to ${opts.out}`));
4593
4816
  }
4594
4817
  }
4595
4818
  main().catch((err) => {
4596
- console.error(chalk9.red(`Error: ${err.message}`));
4819
+ console.error(chalk11.red(`Error: ${err.message}`));
4597
4820
  process.exit(1);
4598
4821
  });
4599
4822
  async function runP2PServer() {
@@ -4609,15 +4832,15 @@ async function runP2PServer() {
4609
4832
  const allowList = opts.p2pAllow ? opts.p2pAllow.split(",").map((s) => s.trim()).filter(Boolean) : [];
4610
4833
  const denyList = opts.p2pDeny ? opts.p2pDeny.split(",").map((s) => s.trim()).filter(Boolean) : [];
4611
4834
  if (!password) {
4612
- 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>"));
4613
4836
  process.exit(1);
4614
4837
  }
4615
4838
  if (tlsCert && !tlsKey) {
4616
- 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"));
4617
4840
  process.exit(1);
4618
4841
  }
4619
4842
  if (tlsKey && !tlsCert) {
4620
- 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"));
4621
4844
  process.exit(1);
4622
4845
  }
4623
4846
  const server = new P2PServer({
@@ -4637,18 +4860,18 @@ async function runP2PServer() {
4637
4860
  hmacKey: opts.p2pHmacKey
4638
4861
  });
4639
4862
  const tlsEnabled = !!(tlsCert && tlsKey);
4640
- console.log(chalk9.bold(`
4863
+ console.log(chalk11.bold(`
4641
4864
  pyre P2P Server`));
4642
- console.log(chalk9.dim(` Host: ${host}`));
4643
- console.log(chalk9.dim(` Port: ${port}`));
4644
- console.log(chalk9.dim(` Protocol: ${tlsEnabled ? "TLS" : "TCP"}`));
4645
- console.log(chalk9.dim(` Password: ${password}`));
4646
- console.log(chalk9.dim(` Interval: ${interval}s`));
4647
- console.log(chalk9.dim(` Detailed: ${detailed ? "on" : "off"}`));
4648
- console.log(chalk9.dim(` Rate limit: ${rateLimit} attempts/min`));
4649
- if (allowList.length > 0) console.log(chalk9.dim(` Allowed IPs: ${allowList.join(", ")}`));
4650
- if (denyList.length > 0) console.log(chalk9.dim(` Denied IPs: ${denyList.join(", ")}`));
4651
- 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...
4652
4875
  `));
4653
4876
  await server.start();
4654
4877
  const rl = readline2.createInterface({ input: process.stdin });
@@ -4660,7 +4883,7 @@ async function runP2PServer() {
4660
4883
  process.exit(0);
4661
4884
  } else if (trimmed === "status") {
4662
4885
  const info = server.info;
4663
- console.log(chalk9.dim(` Peers connected: ${info.peersConnected}`));
4886
+ console.log(chalk11.dim(` Peers connected: ${info.peersConnected}`));
4664
4887
  }
4665
4888
  });
4666
4889
  process.once("SIGINT", () => {
@@ -4693,7 +4916,7 @@ async function runP2PConnect() {
4693
4916
  const tlsCA = opts.p2pCA;
4694
4917
  const tlsInsecure = !!opts.p2pInsecure;
4695
4918
  if (!password) {
4696
- 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>"));
4697
4920
  process.exit(1);
4698
4921
  }
4699
4922
  const client = new P2PClient({
@@ -4707,12 +4930,12 @@ async function runP2PConnect() {
4707
4930
  hmacKey: opts.p2pHmacKey
4708
4931
  });
4709
4932
  client.setOnStatus((msg) => {
4710
- process.stdout.write(`\r${chalk9.dim(msg)}${" ".repeat(40)}\r`);
4933
+ process.stdout.write(`\r${chalk11.dim(msg)}${" ".repeat(40)}\r`);
4711
4934
  });
4712
4935
  client.setOnData((data) => {
4713
4936
  const lines = [];
4714
- lines.push(chalk9.bold(` pyre P2P Client \u2014 ${data.header.hostname}`));
4715
- 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}`));
4716
4939
  lines.push("");
4717
4940
  lines.push(...formatTable(data, {
4718
4941
  width: process.stdout.columns || 80,
@@ -4729,11 +4952,11 @@ async function runP2PConnect() {
4729
4952
  writeFrame2(lines);
4730
4953
  });
4731
4954
  const protocolLabel = useTLS ? "TLS" : "TCP";
4732
- console.log(chalk9.bold(`
4955
+ console.log(chalk11.bold(`
4733
4956
  pyre P2P Client`));
4734
- console.log(chalk9.dim(` Protocol: ${protocolLabel}`));
4735
- console.log(chalk9.dim(` Connecting to ${host}:${port}...`));
4736
- 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
4737
4960
  `));
4738
4961
  process.stdout.write("\x1B[?25l");
4739
4962
  try {
@@ -4741,23 +4964,23 @@ async function runP2PConnect() {
4741
4964
  } catch (err) {
4742
4965
  const code = err.code;
4743
4966
  const msg = err.message || String(err);
4744
- console.log(chalk9.red(`Connection failed: ${msg}`));
4967
+ console.log(chalk11.red(`Connection failed: ${msg}`));
4745
4968
  if (code === "ECONNREFUSED") {
4746
- console.log(chalk9.yellow(" Hints:"));
4747
- console.log(chalk9.dim(" 1. Start the server: pyre p2p server --p2p-host 0.0.0.0 --p2p-port " + port + " --p2p-password <password>"));
4748
- console.log(chalk9.dim(" 2. Check the server IP with: hostname -I"));
4749
- 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"));
4750
4973
  } else if (code === "ETIMEDOUT") {
4751
- console.log(chalk9.yellow(" Hints:"));
4752
- console.log(chalk9.dim(" 1. Verify the server IP is correct"));
4753
- console.log(chalk9.dim(" 2. Check that both machines are on the same network/subnet"));
4754
- 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"));
4755
4978
  } else if (code === "EHOSTDOWN" || code === "EHOSTUNREACH") {
4756
- console.log(chalk9.yellow(" Hints:"));
4757
- console.log(chalk9.dim(" 1. The server at " + host + " is not responding"));
4758
- console.log(chalk9.dim(" 2. Start the server first: pyre p2p server --p2p-host 0.0.0.0 --p2p-port " + port + " --p2p-password <password>"));
4759
- console.log(chalk9.dim(" 3. Check connectivity: ping " + host));
4760
- 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)"));
4761
4984
  }
4762
4985
  process.stdout.write("\x1B[?25h");
4763
4986
  process.exit(1);
@@ -4769,41 +4992,21 @@ async function runP2PConnect() {
4769
4992
  process.exit(0);
4770
4993
  });
4771
4994
  }
4772
- async function runInfoCommand() {
4773
- const data = await collectAll({ detailed: true });
4774
- const lines = [];
4775
- lines.push(chalk9.bold(" pyre info"));
4776
- lines.push("");
4777
- lines.push(` Hostname: ${data.header.hostname}`);
4778
- lines.push(` OS: ${data.header.os}`);
4779
- lines.push(` Uptime: ${data.header.uptime}`);
4780
- lines.push(` CPU: ${data.cpu.brand}`);
4781
- lines.push(` Cores: ${data.cpu.physicalCores}/${data.cpu.cores} (phys/log)`);
4782
- lines.push(` Frequency: ${data.cpu.frequency} MHz`);
4783
- lines.push(` Memory: ${formatBytes(data.memory.total)}`);
4784
- if (data.gpu) lines.push(` GPU: ${data.gpu.model} (${formatBytes(data.gpu.memory)})`);
4785
- if (data.battery) {
4786
- lines.push(` Battery: ${data.battery.level}% (${data.battery.state})`);
4787
- lines.push(` Health: ${data.battery.health}`);
4788
- }
4789
- lines.push(` Thermal: ${data.thermal.state}`);
4790
- console.log(lines.join("\n"));
4791
- }
4792
4995
  async function runSshCommand(host) {
4793
4996
  const password = opts.p2pPassword || "mysecret";
4794
4997
  const cmd = `ssh -o LogLevel=QUIET ${host} "pyre --json --once"`;
4795
- console.log(chalk9.bold(`
4998
+ console.log(chalk11.bold(`
4796
4999
  pyre ssh \u2014 ${host}`));
4797
- console.log(chalk9.dim(` Running: ${cmd}
5000
+ console.log(chalk11.dim(` Running: ${cmd}
4798
5001
  `));
4799
5002
  const { execFile: execFile2 } = await import("child_process");
4800
5003
  const child = execFile2("ssh", ["-o", "LogLevel=QUIET", host, "pyre", "--json", "--once"], (err, stdout, stderr) => {
4801
5004
  if (err) {
4802
- console.log(chalk9.red(` SSH failed: ${err.message}`));
4803
- console.log(chalk9.dim(" Ensure SSH keys are set up or the host is reachable."));
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."));
4804
5007
  process.exit(1);
4805
5008
  }
4806
- if (stderr) console.log(chalk9.dim(stderr));
5009
+ if (stderr) console.log(chalk11.dim(stderr));
4807
5010
  try {
4808
5011
  const remote = JSON.parse(stdout);
4809
5012
  console.log(formatTable(remote, { width: process.stdout.columns || 80, sortBy: "cpu", treeView: false, visible: { packets: true } }));
@@ -4821,14 +5024,66 @@ async function runSshCommand(host) {
4821
5024
  async function runWebCommand() {
4822
5025
  const http = await import("http");
4823
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";
4824
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
+ }
4825
5052
  const url = req.url || "/";
5053
+ if (url === "/health" || url === "/api/health") {
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
+ }
4826
5081
  if (url === "/" || url === "/index.html") {
4827
5082
  res.writeHead(200, { "Content-Type": "text/html" });
4828
- const indexPath = path3.join(process.cwd(), "index.html");
5083
+ const indexPath = path5.join(process.cwd(), "index.html");
4829
5084
  let htmlContent = "";
4830
- if (fs5.existsSync(indexPath)) {
4831
- htmlContent = fs5.readFileSync(indexPath, "utf-8");
5085
+ if (fs8.existsSync(indexPath)) {
5086
+ htmlContent = fs8.readFileSync(indexPath, "utf-8");
4832
5087
  } else {
4833
5088
  const data = await collectAll({ detailed: true });
4834
5089
  htmlContent = formatHtml(data);
@@ -4848,11 +5103,15 @@ async function runWebCommand() {
4848
5103
  }
4849
5104
  });
4850
5105
  server.listen(port, "0.0.0.0", () => {
4851
- console.log(chalk9.bold(`
5106
+ console.log(chalk11.bold(`
4852
5107
  pyre web server running on port ${port}`));
4853
- console.log(chalk9.dim(` Dashboard: http://0.0.0.0:${port}/`));
4854
- console.log(chalk9.dim(` API: http://0.0.0.0:${port}/api`));
4855
- console.log(chalk9.dim(" Press Ctrl+C to stop\n"));
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"));
4856
5115
  });
4857
5116
  process.once("SIGINT", () => {
4858
5117
  server.close();
@@ -4862,14 +5121,14 @@ async function runWebCommand() {
4862
5121
  async function runBenchCommand(benchCmd) {
4863
5122
  const { spawn } = await import("child_process");
4864
5123
  const logDir = opts.exportDir || "./pyre-exports";
4865
- const file = path3.join(logDir, `pyre-bench-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.csv`);
4866
- if (!fs5.existsSync(logDir)) fs5.mkdirSync(logDir, { recursive: true });
4867
- const stream = fs5.createWriteStream(file, { flags: "a" });
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" });
4868
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");
4869
- console.log(chalk9.bold(`
5128
+ console.log(chalk11.bold(`
4870
5129
  pyre bench`));
4871
- console.log(chalk9.dim(` Command: ${benchCmd}`));
4872
- console.log(chalk9.dim(` Log: ${file}
5130
+ console.log(chalk11.dim(` Command: ${benchCmd}`));
5131
+ console.log(chalk11.dim(` Log: ${file}
4873
5132
  `));
4874
5133
  const child = spawn("sh", ["-c", benchCmd]);
4875
5134
  child.stdout.on("data", (d) => process.stdout.write(d));
@@ -4890,7 +5149,7 @@ async function runBenchCommand(benchCmd) {
4890
5149
  child.on("close", () => {
4891
5150
  clearInterval(handle);
4892
5151
  stream.end();
4893
- console.log(chalk9.green(`
5152
+ console.log(chalk11.green(`
4894
5153
  Bench complete. Log saved to ${file}`));
4895
5154
  process.exit(0);
4896
5155
  });