pyre-cli 8.1.0 → 9.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  run
3
- } from "./chunk-GLH7EKAX.js";
3
+ } from "./chunk-FH5WDJBF.js";
4
4
 
5
5
  // src/brewHealth.ts
6
6
  import chalk from "chalk";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  run
3
- } from "./chunk-GLH7EKAX.js";
3
+ } from "./chunk-FH5WDJBF.js";
4
4
 
5
5
  // src/monitors/collectors.ts
6
6
  import chalk from "chalk";
@@ -15,7 +15,8 @@ async function getSmcMetrics() {
15
15
  try {
16
16
  const pm = (await run(
17
17
  "sudo -n powermetrics --samplers smc,cpu_power -n 1 --format text 2>/dev/null",
18
- ""
18
+ "",
19
+ 2e3
19
20
  )).trim();
20
21
  if (pm) {
21
22
  const cpuTemp = pm.match(/CPU die temperature:\s*([\d.]+)/i);
@@ -71,13 +72,9 @@ async function getSmcMetrics() {
71
72
  }
72
73
  }
73
74
  if (result.temps["cpu_die"] === void 0) {
74
- let loadUsage = 0;
75
- try {
76
- const top = await run("top -l 1 -n 0 2>/dev/null", "");
77
- const m = top.match(/CPU usage:\s*[\d.]+\%\s*user,\s*[\d.]+\%\s*sys,\s*([\d.]+)\%\s*idle/);
78
- if (m) loadUsage = Math.max(0, 100 - parseFloat(m[1]));
79
- } catch {
80
- }
75
+ const loadAvg = (await import("os")).loadavg();
76
+ const ncpus = (await import("os")).cpus().length || 1;
77
+ const loadUsage = Math.min(100, loadAvg[0] / ncpus * 100);
81
78
  const estCpu = Math.round((38 + loadUsage * 0.42) * 10) / 10;
82
79
  result.temps["cpu_die"] = estCpu;
83
80
  if (result.temps["gpu_die"] === void 0) {
@@ -98,6 +95,7 @@ function parseSuffix(s) {
98
95
  var SP_TTL_MS = 1e4;
99
96
  var NETSTAT_TTL_MS = 1e3;
100
97
  var prevCpuTimes = null;
98
+ var prevCpuTotal = { total: 0, idle: 0, ts: 0 };
101
99
  var ROUTE_TTL_MS = 5e3;
102
100
  var SYSCTL_TTL_MS = 6e4;
103
101
  var spGpuCache = null;
@@ -239,7 +237,7 @@ async function collectGpu(detailed) {
239
237
  if (spGpuCache && now - spGpuCache.ts < SP_TTL_MS) {
240
238
  spRaw = spGpuCache.raw;
241
239
  } else {
242
- spRaw = (await run("system_profiler SPDisplaysDataType 2>/dev/null")).trim();
240
+ spRaw = (await run("system_profiler SPDisplaysDataType 2>/dev/null", "", 3e3)).trim();
243
241
  spGpuCache = { raw: spRaw, ts: now };
244
242
  }
245
243
  let model = "Unknown";
@@ -376,12 +374,21 @@ async function collectCpu() {
376
374
  }
377
375
  let usage = 0;
378
376
  try {
379
- const top = (await run("top -l 1 -n 0")).trim();
380
- const cpuLine = top.match(/CPU usage:\s*[\d.]+%\s*user,\s*[\d.]+%\s*sys,\s*([\d.]+)%\s*idle/);
381
- if (cpuLine) {
382
- const idle = parseFloat(cpuLine[1]);
383
- usage = Math.round(100 - idle);
377
+ const cpus = os.cpus();
378
+ let totalNow = 0;
379
+ let idleNow = 0;
380
+ for (const cpu of cpus) {
381
+ totalNow += cpu.times.user + cpu.times.nice + cpu.times.sys + cpu.times.idle + cpu.times.irq;
382
+ idleNow += cpu.times.idle;
383
+ }
384
+ if (prevCpuTotal.ts > 0) {
385
+ const totalDelta = totalNow - prevCpuTotal.total;
386
+ const idleDelta = idleNow - prevCpuTotal.idle;
387
+ if (totalDelta > 0) {
388
+ usage = Math.round((totalDelta - idleDelta) / totalDelta * 100);
389
+ }
384
390
  }
391
+ prevCpuTotal = { total: totalNow, idle: idleNow, ts: Date.now() };
385
392
  } catch {
386
393
  }
387
394
  const loadAvgStr = (await run("sysctl -n vm.loadavg", "0.00 0.00 0.00")).trim();
@@ -485,7 +492,7 @@ async function getDiskIoRates() {
485
492
  return { readBytesSec: cachedDiskIo.readBytesSec, writeBytesSec: cachedDiskIo.writeBytesSec };
486
493
  }
487
494
  try {
488
- const raw = (await run("iostat -d -c 2 1 2", "")).trim();
495
+ const raw = (await run("iostat -d -c 2 1 2", "", 3e3)).trim();
489
496
  const blocks = raw.split(/\n\s*\n/).filter(Boolean);
490
497
  const targetBlock = blocks.length > 1 ? blocks[1] : blocks[0];
491
498
  if (targetBlock) {
@@ -547,7 +554,7 @@ async function collectBattery() {
547
554
  if (spBatteryCache && now - spBatteryCache.ts < SP_TTL_MS) {
548
555
  battInfo = spBatteryCache.raw;
549
556
  } else {
550
- battInfo = (await run("system_profiler SPPowerDataType 2>/dev/null")).trim();
557
+ battInfo = (await run("system_profiler SPPowerDataType 2>/dev/null", "", 3e3)).trim();
551
558
  spBatteryCache = { raw: battInfo, ts: now };
552
559
  }
553
560
  const cycleMatch = battInfo.match(/Cycle Count:\s+(\d+)/);
@@ -723,7 +730,7 @@ async function collectPackets() {
723
730
  }
724
731
  let connections = 0;
725
732
  try {
726
- const tcpConns = (await run("netstat -an | grep ESTABLISHED | wc -l", "0")).trim();
733
+ const tcpConns = (await run("netstat -an | grep ESTABLISHED | wc -l", "0", 2e3)).trim();
727
734
  connections = parseInt(tcpConns) || 0;
728
735
  } catch {
729
736
  connections = 0;
@@ -731,7 +738,7 @@ async function collectPackets() {
731
738
  let topProcesses = [];
732
739
  let allProcesses = [];
733
740
  try {
734
- const lsof = (await run("lsof -i -n -P 2>/dev/null | tail -n +2 | awk '{print $1, $2}' | sort | uniq -c | sort -rn", "")).trim();
741
+ const lsof = (await run("lsof -i -n -P 2>/dev/null | tail -n +2 | awk '{print $1, $2}' | sort | uniq -c | sort -rn", "", 2e3)).trim();
735
742
  const lsofLines = lsof.split("\n");
736
743
  topProcesses = lsofLines.map((line) => {
737
744
  const parts = line.trim().split(/\s+/);
@@ -2,7 +2,7 @@
2
2
  import { exec } from "child_process";
3
3
  import { promisify } from "util";
4
4
  var execAsync = promisify(exec);
5
- async function run(cmd, fallback = "", timeout = 5e3) {
5
+ async function run(cmd, fallback = "", timeout = 3e3) {
6
6
  try {
7
7
  return (await execAsync(cmd, { encoding: "utf8", timeout })).stdout.trim();
8
8
  } catch {
@@ -13,8 +13,8 @@ import {
13
13
  collectThermal,
14
14
  getDisplayInfo,
15
15
  getTimeMachineStatus
16
- } from "./chunk-KBKXU2ZV.js";
17
- import "./chunk-GLH7EKAX.js";
16
+ } from "./chunk-EQZCK2VC.js";
17
+ import "./chunk-FH5WDJBF.js";
18
18
  export {
19
19
  collectAll,
20
20
  collectBattery,
@@ -4,7 +4,7 @@ import {
4
4
  } from "./chunk-RVGKEJXT.js";
5
5
  import {
6
6
  run
7
- } from "./chunk-GLH7EKAX.js";
7
+ } from "./chunk-FH5WDJBF.js";
8
8
 
9
9
  // src/doctor.ts
10
10
  import chalk from "chalk";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  run
3
- } from "./chunk-GLH7EKAX.js";
3
+ } from "./chunk-FH5WDJBF.js";
4
4
 
5
5
  // src/extensions.ts
6
6
  import chalk from "chalk";
package/dist/index.js CHANGED
@@ -1,13 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  collectAll
4
- } from "./chunk-KBKXU2ZV.js";
4
+ } from "./chunk-EQZCK2VC.js";
5
5
  import {
6
6
  CONFIG_FILE,
7
7
  readConfig,
8
8
  writeConfig
9
9
  } from "./chunk-RVGKEJXT.js";
10
- import "./chunk-GLH7EKAX.js";
10
+ import "./chunk-FH5WDJBF.js";
11
11
 
12
12
  // src/index.ts
13
13
  import fs8 from "fs";
@@ -1508,24 +1508,24 @@ function getDashboardHtml() {
1508
1508
  <style>
1509
1509
  *,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
1510
1510
  :root{
1511
- --bg: #050508;
1512
- --bg1: #0a0c16;
1513
- --bg2: #111427;
1511
+ --bg: #000000;
1512
+ --bg1: #0a0a0a;
1513
+ --bg2: #141414;
1514
1514
  --bg3: rgba(255,255,255,0.05);
1515
- --border: #2a2d42;
1516
- --border2: #3a3f5a;
1517
- --text: #d4d8f0;
1518
- --text2: #8891b4;
1519
- --text3: #505878;
1520
- --accent: #6c8fff;
1521
- --accent2: #4a6ee8;
1522
- --cyan: #3edcff;
1523
- --green: #3effa0;
1524
- --yellow: #ffca3e;
1525
- --red: #ff4d6a;
1526
- --orange: #ff8c4a;
1527
- --purple: #b06cff;
1528
- --glow: rgba(108,143,255,0.15);
1515
+ --border: #333333;
1516
+ --border2: #444444;
1517
+ --text: #ffffff;
1518
+ --text2: #aaaaaa;
1519
+ --text3: #666666;
1520
+ --accent: #ffffff;
1521
+ --accent2: #dddddd;
1522
+ --cyan: #ffffff;
1523
+ --green: #ffffff;
1524
+ --yellow: #bbbbbb;
1525
+ --red: #dddddd;
1526
+ --orange: #cccccc;
1527
+ --purple: #eeeeee;
1528
+ --glow: rgba(255,255,255,0.15);
1529
1529
  --font: 'Inter', -apple-system, sans-serif;
1530
1530
  --mono: 'JetBrains Mono', ui-monospace, monospace;
1531
1531
  }
@@ -1567,7 +1567,7 @@ body {
1567
1567
  text-transform:uppercase;font-weight:600;
1568
1568
  }
1569
1569
  .tab:hover{color:var(--text2);background:var(--bg3);}
1570
- .tab.active{color:var(--accent);border-bottom:2px solid var(--accent);background:linear-gradient(0deg, rgba(108,143,255,0.1), transparent);}
1570
+ .tab.active{color:var(--accent);border-bottom:2px solid var(--accent);background:linear-gradient(0deg, rgba(255,255,255,0.1), transparent);}
1571
1571
  .tab-key{opacity:0.5;margin-right:6px;}
1572
1572
 
1573
1573
  #main{flex:1;overflow-y:auto;overflow-x:hidden;padding:16px;display:flex;flex-direction:column;}
@@ -1594,7 +1594,7 @@ body {
1594
1594
  transition:border-color 0.2s, box-shadow 0.2s;
1595
1595
  display:flex;flex-direction:column;
1596
1596
  }
1597
- .card:hover{border-color:var(--cyan);box-shadow:0 0 10px rgba(62,220,255,0.05);}
1597
+ .card:hover{border-color:var(--cyan);box-shadow:0 0 10px rgba(255,255,255,0.05);}
1598
1598
  .card-title{
1599
1599
  position:absolute;top:-9px;left:12px;
1600
1600
  background:var(--bg);
@@ -1619,7 +1619,7 @@ body {
1619
1619
  .stat-label{font-size:11px;color:var(--text2);}
1620
1620
  .stat-val{font-family:var(--mono);font-size:12px;color:var(--text);font-weight:500;}
1621
1621
  .big-metric{text-align:center;padding:20px 0;}
1622
- .big-val{font-family:var(--mono);font-size:48px;font-weight:700;color:var(--cyan);text-shadow:0 0 15px rgba(62,220,255,0.3);}
1622
+ .big-val{font-family:var(--mono);font-size:48px;font-weight:700;color:var(--cyan);text-shadow:0 0 15px rgba(255,255,255,0.3);}
1623
1623
  .big-unit{font-family:var(--mono);font-size:14px;color:var(--text2);margin-top:8px;}
1624
1624
 
1625
1625
  /* Gauges */
@@ -1628,9 +1628,9 @@ body {
1628
1628
  .gauge-pct{font-weight:600;}
1629
1629
  .gauge-track{height:20px;background:var(--bg2);border-radius:4px;overflow:hidden;position:relative;border:1px solid var(--border);}
1630
1630
  .gauge-fill{height:100%;transition:width 0.4s ease;display:flex;align-items:center;justify-content:flex-end;padding-right:6px;font-family:var(--mono);font-size:10px;font-weight:700;color:#000;}
1631
- .gauge-fill.ok{background:linear-gradient(90deg,var(--green),#a3ffce);}
1632
- .gauge-fill.warn{background:linear-gradient(90deg,var(--yellow),#ffea99);}
1633
- .gauge-fill.crit{background:linear-gradient(90deg,var(--red),#ff99aa);}
1631
+ .gauge-fill.ok{background:linear-gradient(90deg,var(--text),#999);}
1632
+ .gauge-fill.warn{background:linear-gradient(90deg,var(--text2),#666);}
1633
+ .gauge-fill.crit{background:linear-gradient(90deg,var(--text3),#333);}
1634
1634
 
1635
1635
  /* SVG Rings */
1636
1636
  .ring-container { display:flex; justify-content:space-around; padding:20px 0; background:var(--bg1); border:1px solid var(--border); border-radius:6px; margin-bottom:12px; }
@@ -1654,9 +1654,9 @@ canvas.spark.large { height:200px; }
1654
1654
  .core-col { display:flex; flex-direction:column; gap:4px; align-items:center; }
1655
1655
  .core-bar { width:100%; height:100px; background:var(--bg2); border-radius:4px; display:flex; align-items:flex-end; overflow:hidden; border:1px solid var(--border); }
1656
1656
  .core-fill { width:100%; transition:height 0.3s; }
1657
- .core-fill.ok { background:linear-gradient(0deg,var(--green),#a3ffce); }
1658
- .core-fill.warn { background:linear-gradient(0deg,var(--yellow),#ffea99); }
1659
- .core-fill.crit { background:linear-gradient(0deg,var(--red),#ff99aa); }
1657
+ .core-fill.ok { background:linear-gradient(0deg,var(--text),#999); }
1658
+ .core-fill.warn { background:linear-gradient(0deg,var(--text2),#666); }
1659
+ .core-fill.crit { background:linear-gradient(0deg,var(--text3),#333); }
1660
1660
  .core-lbl { font-family:var(--mono); font-size:9px; color:var(--text2); }
1661
1661
 
1662
1662
  /* Table */
@@ -1689,7 +1689,7 @@ table.proc td { padding:6px 12px; white-space:nowrap; overflow:hidden; text-over
1689
1689
  .chip.warn { border-color:var(--yellow); color:var(--yellow); }
1690
1690
  .chip.crit { border-color:var(--red); color:var(--red); }
1691
1691
 
1692
- .anomaly { display:flex; gap:12px; padding:12px; border:1px solid var(--red); background:rgba(255,77,106,0.1); border-radius:6px; margin-bottom:12px; }
1692
+ .anomaly { display:flex; gap:12px; padding:12px; border:1px solid var(--red); background:rgba(255,255,255,0.1); border-radius:6px; margin-bottom:12px; }
1693
1693
  .anomaly-icon { font-size:20px; }
1694
1694
  .anomaly-title { font-family:var(--mono); font-weight:700; color:var(--red); font-size:12px; }
1695
1695
  .anomaly-desc { font-family:var(--mono); font-size:11px; color:var(--text); margin-top:4px; }
@@ -1753,7 +1753,7 @@ function svgRing(val, max, label, unit='', size=100) {
1753
1753
  const pct = Math.min(100, Math.max(0, (val/max)*100));
1754
1754
  const off = circ*(1 - pct/100);
1755
1755
  const cls = statusCls(pct);
1756
- const col = cls==='crit'?'var(--red)':cls==='warn'?'var(--yellow)':'var(--cyan)';
1756
+ const col = cls==='crit'?'var(--text3)':cls==='warn'?'var(--text2)':'var(--text)';
1757
1757
  return \`<div class="ring-box">
1758
1758
  <div class="ring-title">\${label}</div>
1759
1759
  <div class="svg-ring \${size>150?'large':''}">
@@ -2073,14 +2073,14 @@ function renderAnomalies(d) {
2073
2073
  }
2074
2074
 
2075
2075
  function drawSparks() {
2076
- drawSpark('sp-ov-cpu', history.cpu, '#3edcff', '#4a6ee8');
2077
- drawSpark('sp-ov-rx', history.rxRate, '#3edcff', '#4a6ee8');
2078
- drawSpark('sp-ov-tx', history.txRate, '#b06cff', '#6c8fff');
2079
- drawSpark('sp-cpu-l', history.cpu, '#3edcff', '#4a6ee8');
2080
- drawSpark('sp-mem-l', history.mem, '#b06cff', '#6c8fff');
2081
- drawSpark('sp-therm-l', history.temp, '#ffca3e', '#ff4d6a');
2082
- drawSpark('sp-net-rx-l', history.rxRate, '#3edcff', '#4a6ee8');
2083
- drawSpark('sp-net-tx-l', history.txRate, '#b06cff', '#6c8fff');
2076
+ drawSpark('sp-ov-cpu', history.cpu, '#ffffff', '#999999');
2077
+ drawSpark('sp-ov-rx', history.rxRate, '#ffffff', '#999999');
2078
+ drawSpark('sp-ov-tx', history.txRate, '#bbbbbb', '#666666');
2079
+ drawSpark('sp-cpu-l', history.cpu, '#ffffff', '#999999');
2080
+ drawSpark('sp-mem-l', history.mem, '#bbbbbb', '#666666');
2081
+ drawSpark('sp-therm-l', history.temp, '#dddddd', '#888888');
2082
+ drawSpark('sp-net-rx-l', history.rxRate, '#ffffff', '#999999');
2083
+ drawSpark('sp-net-tx-l', history.txRate, '#bbbbbb', '#666666');
2084
2084
  }
2085
2085
 
2086
2086
  function switchView(v) {
@@ -2697,8 +2697,7 @@ function graphsCacheParams() {
2697
2697
  }
2698
2698
  function processRowBudget() {
2699
2699
  let overlayRows = 0;
2700
- if (state.inputMode === "customizer") overlayRows = state.CUSTOMIZER_OPTIONS.length + 3;
2701
- else if (state.inputMode === "signal") overlayRows = 6;
2700
+ if (state.inputMode === "signal") overlayRows = 6;
2702
2701
  else if (state.inputMode === "filter" || state.inputMode === "kill" || state.inputMode === "p2p") overlayRows = 3;
2703
2702
  if (state.activePanel === "process") {
2704
2703
  return Math.max(10, state.termHeight - 12 - overlayRows);
@@ -2854,14 +2853,20 @@ function render() {
2854
2853
  const footerHeight = 3;
2855
2854
  const targetBodyHeight = Math.max(1, state.termHeight - footerHeight);
2856
2855
  if (state.inputMode === "customizer") {
2857
- const preOverlayCount = bodyLines.length;
2858
- const availableForOverlay = targetBodyHeight - preOverlayCount;
2859
2856
  const totalItems = state.CUSTOMIZER_OPTIONS.length;
2860
2857
  const headerLines = 1;
2861
- const blankLines = availableForOverlay >= 2 ? 1 : 0;
2862
- const maxVisibleItems = Math.max(0, availableForOverlay - headerLines - blankLines);
2858
+ const blankLines = 1;
2859
+ const idealOverlayHeight = totalItems + headerLines + blankLines;
2860
+ const minOverlayHeight = Math.min(10 + headerLines + blankLines, Math.floor(targetBodyHeight / 2));
2861
+ const overlayHeight = Math.min(idealOverlayHeight, Math.max(minOverlayHeight, Math.floor(targetBodyHeight * 0.6)));
2862
+ const maxBodyForTable = Math.max(3, targetBodyHeight - overlayHeight);
2863
+ if (bodyLines.length > maxBodyForTable) {
2864
+ bodyLines.length = maxBodyForTable;
2865
+ }
2866
+ const availableForOverlay = targetBodyHeight - bodyLines.length;
2867
+ const maxVisibleItems = Math.max(1, availableForOverlay - headerLines - blankLines);
2863
2868
  const scrollOffset = totalItems > maxVisibleItems ? Math.max(0, Math.min(state.customizerIndex, totalItems - maxVisibleItems)) : 0;
2864
- if (blankLines > 0) bodyLines.push("");
2869
+ bodyLines.push("");
2865
2870
  bodyLines.push(...renderCustomizerOverlay(scrollOffset, maxVisibleItems).split("\n"));
2866
2871
  } else if (state.inputMode === "filter") {
2867
2872
  bodyLines.push(chalk4.cyan(` Filter processes: ${state.inputBuffer}_`));
@@ -4591,7 +4596,7 @@ async function showSplash(config3) {
4591
4596
  process.stdin.setRawMode(true);
4592
4597
  process.stdin.resume();
4593
4598
  process.stdin.on("data", onEnter);
4594
- setTimeout(onEnter, 5e3);
4599
+ setTimeout(onEnter, 1500);
4595
4600
  });
4596
4601
  }
4597
4602
 
@@ -4772,6 +4777,29 @@ var config2 = readConfig();
4772
4777
  var pkg = JSON.parse(fs8.readFileSync(new URL("../package.json", import.meta.url), "utf-8"));
4773
4778
  var program = new Command();
4774
4779
  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("--since <range>", "Time range for anomalies digest (e.g. 1d, 7d, yesterday)").option("--plain", "Output in plain accessible text without ANSI colors or line-drawing characters").option("--a11y", "Alias for --plain").option("--webhook-url <url>", "URL to POST to on alert").option("--alert-cmd <cmd>", "Command to execute on alert").option("--port <port>", "Port number for web server mode", "3000");
4780
+ program.addHelpText("after", `
4781
+ Commands:
4782
+ live Interactive live dashboard
4783
+ ui Launch the native macOS UI dashboard
4784
+ web Serve an auto-refreshing live web portal
4785
+ ssh <host> Stream live stats from a remote Mac over SSH
4786
+ fleet <host1> [host2]... Multi-host live dashboard monitoring multiple Macs
4787
+ bench <cmd> Log CPU, memory, network, power draw, and estimate kWh cost
4788
+ anomalies Compute z-score anomalies and print plain-language digest
4789
+ doctor Run system diagnostics
4790
+ extensions System Extensions inspector
4791
+ brew Homebrew health panel
4792
+ update Check for pyre-cli updates
4793
+ profile <save|load|list> Atomic configuration profile management
4794
+ config <show|reset> View or reset persistent configuration file
4795
+ history Graph historical resource trends from CSV logs
4796
+ diff <file1> <file2> Compare two saved snapshot files side-by-side
4797
+ info Concise hardware summary
4798
+ completions <shell> Generate shell auto-completion scripts
4799
+ xbar Generate an xbar / SwiftBar menu bar plugin script
4800
+ p2p <server|connect> Start a P2P server or connect to one
4801
+ server Print commands for starting P2P server/client
4802
+ `);
4775
4803
  program.parse(process.argv);
4776
4804
  var opts = program.opts();
4777
4805
  if (opts.plain || opts.a11y) {
@@ -4838,12 +4866,12 @@ async function main() {
4838
4866
  return;
4839
4867
  }
4840
4868
  if (cmd === "extensions") {
4841
- const { printExtensionsReport } = await import("./extensions-QCFSVCL5.js");
4869
+ const { printExtensionsReport } = await import("./extensions-DVA5YLGH.js");
4842
4870
  await printExtensionsReport();
4843
4871
  return;
4844
4872
  }
4845
4873
  if (cmd === "brew") {
4846
- const { printBrewHealthReport } = await import("./brewHealth-6NE37NAU.js");
4874
+ const { printBrewHealthReport } = await import("./brewHealth-F2BXF5M6.js");
4847
4875
  await printBrewHealthReport();
4848
4876
  return;
4849
4877
  }
@@ -4946,7 +4974,7 @@ async function main() {
4946
4974
  return;
4947
4975
  }
4948
4976
  if (cmd === "doctor") {
4949
- const { runDoctor, printDoctorReport } = await import("./doctor-LJQAABZ4.js");
4977
+ const { runDoctor, printDoctorReport } = await import("./doctor-L6FZYA7F.js");
4950
4978
  const checks = await runDoctor();
4951
4979
  printDoctorReport(checks);
4952
4980
  return;
@@ -5211,7 +5239,7 @@ async function runP2PConnect() {
5211
5239
  }
5212
5240
  async function runInfoCommand() {
5213
5241
  const data = await collectAll({ detailed: true });
5214
- const { getDisplayInfo, getTimeMachineStatus } = await import("./collectors-4VYRJVRO.js");
5242
+ const { getDisplayInfo, getTimeMachineStatus } = await import("./collectors-KNEN4YJD.js");
5215
5243
  const displays = await getDisplayInfo();
5216
5244
  const tmStatus = await getTimeMachineStatus();
5217
5245
  const lines = [];
@@ -5401,7 +5429,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
5401
5429
  window.title = "Pyre"
5402
5430
  window.minSize = NSSize(width: 800, height: 600)
5403
5431
  window.appearance = NSAppearance(named: .darkAqua)
5404
- window.backgroundColor = NSColor(red: 0.027, green: 0.031, blue: 0.063, alpha: 1)
5432
+ window.backgroundColor = NSColor(white: 0, alpha: 1)
5405
5433
  window.isReleasedWhenClosed = false
5406
5434
  window.delegate = self
5407
5435
 
@@ -5409,7 +5437,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
5409
5437
  let cfg = WKWebViewConfiguration()
5410
5438
  webView = DraggableWebView(frame: window.contentView!.bounds, configuration: cfg)
5411
5439
  webView.autoresizingMask = [.width, .height]
5412
- webView.setValue(NSColor(red: 0.027, green: 0.031, blue: 0.063, alpha: 1),
5440
+ webView.setValue(NSColor(white: 0, alpha: 1),
5413
5441
  forKey: "backgroundColor")
5414
5442
 
5415
5443
  // Load the dashboard HTML from the local server
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pyre-cli",
3
- "version": "8.1.0",
3
+ "version": "9.0.0",
4
4
  "description": "CLI Mac monitoring tool: temps, system stats, live dashboard, packet monitor, battery predictor, export",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
package/src/index.ts CHANGED
@@ -74,7 +74,7 @@ program
74
74
  .option('--alert-cmd <cmd>', 'Command to execute on alert')
75
75
  .option('--port <port>', 'Port number for web server mode', '3000');
76
76
 
77
- program.addHelpText('after', \`
77
+ program.addHelpText('after', `
78
78
  Commands:
79
79
  live Interactive live dashboard
80
80
  ui Launch the native macOS UI dashboard
@@ -96,7 +96,7 @@ Commands:
96
96
  xbar Generate an xbar / SwiftBar menu bar plugin script
97
97
  p2p <server|connect> Start a P2P server or connect to one
98
98
  server Print commands for starting P2P server/client
99
- \`);
99
+ `);
100
100
 
101
101
  program.parse(process.argv);
102
102
 
@@ -68,8 +68,7 @@ function graphsCacheParams(): string {
68
68
 
69
69
  function processRowBudget(): number {
70
70
  let overlayRows = 0;
71
- if (state.inputMode === 'customizer') overlayRows = state.CUSTOMIZER_OPTIONS.length + 3;
72
- else if (state.inputMode === 'signal') overlayRows = 6;
71
+ if (state.inputMode === 'signal') overlayRows = 6;
73
72
  else if (state.inputMode === 'filter' || state.inputMode === 'kill' || state.inputMode === 'p2p') overlayRows = 3;
74
73
 
75
74
  if (state.activePanel === 'process') {
@@ -265,17 +264,26 @@ function render() {
265
264
  const targetBodyHeight = Math.max(1, state.termHeight - footerHeight);
266
265
 
267
266
  if (state.inputMode === 'customizer') {
268
- const preOverlayCount = bodyLines.length;
269
- const availableForOverlay = targetBodyHeight - preOverlayCount;
270
267
  const totalItems = state.CUSTOMIZER_OPTIONS.length;
271
- const headerLines = 1;
272
- const blankLines = availableForOverlay >= 2 ? 1 : 0;
273
- const maxVisibleItems = Math.max(0, availableForOverlay - headerLines - blankLines);
268
+ const headerLines = 1; // the "UI CUSTOMIZER" header
269
+ const blankLines = 1; // blank separator line
270
+ // Reserve a fixed height for the customizer: show all items if they fit,
271
+ // otherwise guarantee at least 10 visible items (or half the screen).
272
+ const idealOverlayHeight = totalItems + headerLines + blankLines;
273
+ const minOverlayHeight = Math.min(10 + headerLines + blankLines, Math.floor(targetBodyHeight / 2));
274
+ const overlayHeight = Math.min(idealOverlayHeight, Math.max(minOverlayHeight, Math.floor(targetBodyHeight * 0.6)));
275
+ // Truncate table body to make room for the customizer overlay
276
+ const maxBodyForTable = Math.max(3, targetBodyHeight - overlayHeight);
277
+ if (bodyLines.length > maxBodyForTable) {
278
+ bodyLines.length = maxBodyForTable;
279
+ }
280
+ const availableForOverlay = targetBodyHeight - bodyLines.length;
281
+ const maxVisibleItems = Math.max(1, availableForOverlay - headerLines - blankLines);
274
282
  const scrollOffset = totalItems > maxVisibleItems
275
283
  ? Math.max(0, Math.min(state.customizerIndex, totalItems - maxVisibleItems))
276
284
  : 0;
277
285
 
278
- if (blankLines > 0) bodyLines.push('');
286
+ bodyLines.push('');
279
287
  bodyLines.push(...renderCustomizerOverlay(scrollOffset, maxVisibleItems).split('\n'));
280
288
  } else if (state.inputMode === 'filter') {
281
289
  bodyLines.push(chalk.cyan(` Filter processes: ${state.inputBuffer}_`));
@@ -20,6 +20,7 @@ import type { StatsData, CpuData, MemoryData, ThermalData, BatteryData, PowerDat
20
20
  const SP_TTL_MS = 10_000;
21
21
  const NETSTAT_TTL_MS = 1000;
22
22
  let prevCpuTimes: { total: number; idle: number }[] | null = null;
23
+ let prevCpuTotal = { total: 0, idle: 0, ts: 0 };
23
24
  const ROUTE_TTL_MS = 5_000;
24
25
  const SYSCTL_TTL_MS = 60_000;
25
26
 
@@ -236,7 +237,7 @@ export async function collectPower(): Promise<PowerData | null> {
236
237
  if (spGpuCache && now - spGpuCache.ts < SP_TTL_MS) {
237
238
  spRaw = spGpuCache.raw;
238
239
  } else {
239
- spRaw = (await run('system_profiler SPDisplaysDataType 2>/dev/null')).trim();
240
+ spRaw = (await run('system_profiler SPDisplaysDataType 2>/dev/null', '', 3000)).trim();
240
241
  spGpuCache = { raw: spRaw, ts: now };
241
242
  }
242
243
 
@@ -400,18 +401,25 @@ export async function collectCpu(): Promise<CpuData> {
400
401
  }
401
402
  }
402
403
 
404
+ // Use os.cpus() delta instead of the very slow `top -l 1 -n 0` which
405
+ // blocks for ~1-2s waiting for a sampling window. os.cpus() is instant.
403
406
  let usage = 0;
404
407
  try {
405
- const top = (await run('top -l 1 -n 0')).trim();
406
- // Real `top -l 1 -n 0` output is comma-separated and newline-terminated,
407
- // e.g. "CPU usage: 5.26% user, 10.52% sys, 84.21% idle" — there is no
408
- // semicolon anywhere on that line. The old regex required one to
409
- // terminate the match, so it never matched and usage stayed at 0.
410
- const cpuLine = top.match(/CPU usage:\s*[\d.]+%\s*user,\s*[\d.]+%\s*sys,\s*([\d.]+)%\s*idle/);
411
- if (cpuLine) {
412
- const idle = parseFloat(cpuLine[1]);
413
- usage = Math.round(100 - idle);
408
+ const cpus = os.cpus();
409
+ let totalNow = 0;
410
+ let idleNow = 0;
411
+ for (const cpu of cpus) {
412
+ totalNow += cpu.times.user + cpu.times.nice + cpu.times.sys + cpu.times.idle + cpu.times.irq;
413
+ idleNow += cpu.times.idle;
414
+ }
415
+ if (prevCpuTotal.ts > 0) {
416
+ const totalDelta = totalNow - prevCpuTotal.total;
417
+ const idleDelta = idleNow - prevCpuTotal.idle;
418
+ if (totalDelta > 0) {
419
+ usage = Math.round(((totalDelta - idleDelta) / totalDelta) * 100);
420
+ }
414
421
  }
422
+ prevCpuTotal = { total: totalNow, idle: idleNow, ts: Date.now() };
415
423
  } catch {
416
424
  // ignore
417
425
  }
@@ -534,7 +542,7 @@ async function getDiskIoRates(): Promise<{ readBytesSec: number; writeBytesSec:
534
542
  return { readBytesSec: cachedDiskIo.readBytesSec, writeBytesSec: cachedDiskIo.writeBytesSec };
535
543
  }
536
544
  try {
537
- const raw = (await run('iostat -d -c 2 1 2', '')).trim();
545
+ const raw = (await run('iostat -d -c 2 1 2', '', 3000)).trim();
538
546
  const blocks = raw.split(/\n\s*\n/).filter(Boolean);
539
547
  const targetBlock = blocks.length > 1 ? blocks[1] : blocks[0];
540
548
  if (targetBlock) {
@@ -606,7 +614,7 @@ export async function collectBattery(): Promise<BatteryData | null> {
606
614
  if (spBatteryCache && now - spBatteryCache.ts < SP_TTL_MS) {
607
615
  battInfo = spBatteryCache.raw;
608
616
  } else {
609
- battInfo = (await run('system_profiler SPPowerDataType 2>/dev/null')).trim();
617
+ battInfo = (await run('system_profiler SPPowerDataType 2>/dev/null', '', 3000)).trim();
610
618
  spBatteryCache = { raw: battInfo, ts: now };
611
619
  }
612
620
 
@@ -821,7 +829,7 @@ export async function collectPackets(): Promise<PacketData | null> {
821
829
 
822
830
  let connections = 0;
823
831
  try {
824
- const tcpConns = (await run('netstat -an | grep ESTABLISHED | wc -l', '0')).trim();
832
+ const tcpConns = (await run('netstat -an | grep ESTABLISHED | wc -l', '0', 2000)).trim();
825
833
  connections = parseInt(tcpConns) || 0;
826
834
  } catch {
827
835
  connections = 0;
@@ -830,7 +838,7 @@ export async function collectPackets(): Promise<PacketData | null> {
830
838
  let topProcesses: { pid: number; command: string; rxBytes: number; txBytes: number }[] = [];
831
839
  let allProcesses: { pid: number; command: string; rxBytes: number; txBytes: number; rxPackets?: number; txPackets?: number }[] = [];
832
840
  try {
833
- const lsof = (await run('lsof -i -n -P 2>/dev/null | tail -n +2 | awk \'{print $1, $2}\' | sort | uniq -c | sort -rn', '')).trim();
841
+ const lsof = (await run('lsof -i -n -P 2>/dev/null | tail -n +2 | awk \'{print $1, $2}\' | sort | uniq -c | sort -rn', '', 2000)).trim();
834
842
  const lsofLines = lsof.split('\n');
835
843
  topProcesses = lsofLines.map(line => {
836
844
  const parts = line.trim().split(/\s+/);
@@ -11,7 +11,7 @@ import { promisify } from 'node:util';
11
11
 
12
12
  const execAsync = promisify(exec);
13
13
 
14
- async function run(cmd: string, fallback: string = '', timeout = 5000): Promise<string> {
14
+ async function run(cmd: string, fallback: string = '', timeout = 3000): Promise<string> {
15
15
  try {
16
16
  return (await execAsync(cmd, { encoding: 'utf8', timeout })).stdout.trim();
17
17
  } catch {
@@ -41,7 +41,8 @@ export async function getSmcMetrics(): Promise<SmcMetrics> {
41
41
  const pm = (
42
42
  await run(
43
43
  'sudo -n powermetrics --samplers smc,cpu_power -n 1 --format text 2>/dev/null',
44
- ''
44
+ '',
45
+ 2000
45
46
  )
46
47
  ).trim();
47
48
 
@@ -116,14 +117,10 @@ export async function getSmcMetrics(): Promise<SmcMetrics> {
116
117
 
117
118
  // 4. Smart load-based thermal estimation if hardware sensors return nothing
118
119
  if (result.temps['cpu_die'] === undefined) {
119
- let loadUsage = 0;
120
- try {
121
- const top = await run('top -l 1 -n 0 2>/dev/null', '');
122
- const m = top.match(/CPU usage:\s*[\d.]+\%\s*user,\s*[\d.]+\%\s*sys,\s*([\d.]+)\%\s*idle/);
123
- if (m) loadUsage = Math.max(0, 100 - parseFloat(m[1]));
124
- } catch {
125
- // ignore
126
- }
120
+ // Use os.loadavg() (instant) instead of the slow `top -l 1` (~1-2s)
121
+ const loadAvg = (await import('node:os')).loadavg();
122
+ const ncpus = (await import('node:os')).cpus().length || 1;
123
+ const loadUsage = Math.min(100, (loadAvg[0] / ncpus) * 100);
127
124
 
128
125
  const estCpu = Math.round((38 + (loadUsage * 0.42)) * 10) / 10;
129
126
  result.temps['cpu_die'] = estCpu;
package/src/splash.ts CHANGED
@@ -198,6 +198,6 @@ export async function showSplash(config?: SplashConfig): Promise<void> {
198
198
  process.stdin.resume();
199
199
  process.stdin.on('data', onEnter);
200
200
 
201
- setTimeout(onEnter, 5000);
201
+ setTimeout(onEnter, 1500);
202
202
  });
203
203
  }