pyre-cli 7.1.0 → 8.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -17,31 +17,35 @@ Mac system monitoring CLI: temps, CPU, memory, disk, battery, GPU, power draw, l
17
17
  - **Real-time system stats & per-core CPU** — CPU brand, cores, frequency, load, per-core utilization mini-bars, and overall usage
18
18
  - **GPU monitoring** — GPU model, VRAM, utilization, temperature, and process count (detailed mode)
19
19
  - **Power draw monitoring** — CPU watts, GPU watts, and combined power draw via SMC/ioreg
20
- - **Memory monitoring** — usage, swap, and total/available
20
+ - **Memory monitoring & pressure** — usage, swap, wired, compressed, purgeable, swapins/outs, and kernel VM pressure level
21
21
  - **Disk I/O throughput & space** — read/write speed per volume and mounted volume space usage
22
22
  - **Battery & power health** — level, power source, condition, charge cycles, max capacity, health trends, estimated time to empty, discharge rate, power draw in watts
23
23
  - **Packet monitor** — network packet counts, packet rates, active TCP connections, top network processes
24
24
  - **Task list** — running tasks/applications sorted by CPU with PID, user, memory, state, and runtime
25
25
  - **Thermal state** — CPU temperature via `pmset` and `powermetrics` (sudo for detailed readings)
26
26
  - **Network** — RX/TX bytes and per-second rates
27
- - **Top processes & search-as-you-type** — live filtering, tree view, sorting by CPU/memory/PID, and process kill with signal picker
27
+ - **Top processes & search-as-you-type** — live filtering, tree view, sorting by CPU/memory/PID, and process kill with protected PID confirmation guard
28
28
  - **Interactive live dashboard** — full-screen TUI with keyboard-driven controls, mouse support, configurable panel layout, and tab-based panel navigation
29
29
  - **Snapshot export** — JSON, CSV, TSV, HTML, or Markdown output formats
30
- - **Continuous CSV logging** — automatic per-tick data logging to timestamped files with log rotation
30
+ - **Continuous CSV logging & Anomaly Digest** — automatic per-tick logging and statistical spike analysis (`pyre anomalies`)
31
31
  - **Visual themes & custom themes** — six built-in color themes plus user-defined JSON theme support in `~/.config/pyre/themes/`
32
- - **Persistent configuration** — save default themes, intervals, and thresholds in `~/.config/pyre/config.json`
32
+ - **Persistent configuration & Profiles** — save themes and thresholds in `~/.config/pyre/config.json`, plus atomic profile management (`pyre profile`)
33
33
  - **Alert system & webhooks** — configurable threshold alerts with terminal bell, desktop notifications, webhook POST payload execution (`--webhook-url`), and custom shell command execution (`--alert-cmd`)
34
- - **System diagnostic tool (`pyre doctor`)** — check permissions, SMC access, network reachability, and configuration health
34
+ - **System diagnostic tool (`pyre doctor`)** — check permissions, SMC access, Gatekeeper, SIP status, XProtect definitions, and P2P reachability
35
+ - **Security & Ecosystem inspectors** — System Extensions inspector (`pyre extensions`), Homebrew health (`pyre brew`), and Time Machine backup status
36
+ - **Accessible plain-text mode** — `--plain`/`--a11y` mode for screen readers and ANSI-free logging
37
+ - **Grafana Integration** — ready-to-import Grafana dashboard template (`grafana/pyre-dashboard.json`)
35
38
  - **History & Snapshot Diff** — query historical CSV log trends (`pyre history`) and compare snapshot files side-by-side (`pyre diff`)
36
39
  - **Multi-host fleet dashboard** — stream and tile stats from multiple remote Macs (`pyre fleet`)
37
40
  - **Menu Bar & xbar integration** — generate menu bar plugin script (`pyre xbar`) for continuous background status display
38
41
  - **Graph mode toggle** — switch between sparkline and bar graph rendering in the live dashboard
39
42
  - **P2P dashboard panel** — monitor P2P server status and peer events directly from the live dashboard
40
43
  - **Temperature unit toggle** — switch between Celsius and Fahrenheit display in live mode
41
- - **Hardware overview** — `pyre info` prints a concise hardware summary
44
+ - **Hardware & Displays overview** — `pyre info` prints hardware, connected displays, and Time Machine status
42
45
  - **Remote SSH monitoring** — `pyre ssh <host>` streams a remote Mac's live dashboard locally
43
46
  - **Web dashboard** — `pyre web` serves an auto-refreshing HTML dashboard with real-time SSE stream on a local port
44
- - **Micro-benchmarking** — `pyre bench <cmd>` logs resource usage during any command
47
+ - **Micro-benchmarking & Energy Cost** — `pyre bench <cmd>` logs resource usage and estimates energy consumption/cost (kWh)
48
+ - **Update checker** — `pyre update` checks npm registry for new pyre-cli releases
45
49
 
46
50
  ## Installation
47
51
 
@@ -63,17 +67,24 @@ curl -fsSL https://raw.githubusercontent.com/somalip/pyre/main/install.sh | bash
63
67
  pyre # Static snapshot, all system stats
64
68
  pyre --detailed # Include sensor / powermetrics detail
65
69
  pyre --json # JSON output (also --csv, --tsv, --html, --md)
66
- pyre --html --out report.html # Self-contained HTML report
70
+ pyre --plain # Plain-text accessible output without ANSI noise
67
71
 
68
72
  pyre live # Interactive live dashboard
69
73
  pyre live --interval 3 --log # Custom refresh interval + auto-logging
70
74
  pyre live --alert-cpu 80 --alert-temp 85 --temp-unit f
71
75
 
72
- pyre server # Print ready-to-paste P2P server/client commands for your LAN
73
- pyre info # Concise hardware overview
74
- pyre ssh build-server # Stream remote Mac stats over SSH
75
- pyre web # Serve HTML dashboard on localhost
76
- pyre bench "make build" # Log resources during a command
76
+ pyre ui # Launch native macOS UI dashboard
77
+ pyre web # Serve auto-refreshing live web portal on localhost
78
+
79
+ pyre anomalies --since 7d # Statistical resource spike digest from past CSV logs
80
+ pyre doctor # System diagnostics (Gatekeeper, SIP, XProtect, powermetrics)
81
+ pyre extensions # System extensions inspector (systemextensionsctl)
82
+ pyre brew # Homebrew health summary & cellar size
83
+ pyre update # Check for pyre-cli updates
84
+ pyre profile save dev # Save active configuration profile
85
+ pyre profile load dev # Load configuration profile
86
+ pyre info # Concise hardware, display & Time Machine overview
87
+ pyre bench "make build" # Benchmark command & estimate kWh energy cost
77
88
  ```
78
89
 
79
90
  ## Options Reference
@@ -87,7 +98,9 @@ These options apply to both static snapshots and `pyre live`, except where noted
87
98
  | `--md` | Output as Markdown |
88
99
  | `-c, --csv` | Output as CSV |
89
100
  | `-t, --tsv` | Output as TSV |
101
+ | `--plain` / `--a11y` | Plain text output mode without ANSI colors or line-drawing characters |
90
102
  | `--detailed` | Include detailed system info and sensor readings |
103
+ | `--since <range>` | Time range for anomaly digest (e.g. `1d`, `7d`, `yesterday`) |
91
104
  | `--theme <name>` | Theme for live mode (`default`, `dracula`, `cyberpunk`, `monochrome`, `nord`, `gruvbox`) |
92
105
  | `--interval <seconds>` | Refresh interval for live mode (default: `2`) |
93
106
  | `--once` | Show a single static snapshot instead of live feed |
@@ -115,7 +128,11 @@ These options apply to both static snapshots and `pyre live`, except where noted
115
128
  | `pyre --json` | JSON output |
116
129
  | `pyre --html` | Self-contained HTML report |
117
130
  | `pyre --md` | Markdown tables |
118
- | `pyre doctor` | Run system diagnostics (permissions, powermetrics access, P2P network check) |
131
+ | `pyre doctor` | Run system diagnostics (permissions, Gatekeeper, SIP, XProtect, P2P network) |
132
+ | `pyre extensions` | System Extensions inspector (`systemextensionsctl list`) |
133
+ | `pyre brew` | Homebrew health panel (installed formulae, outdated count, Cellar disk size, doctor summary) |
134
+ | `pyre update` | Check npm registry for latest `pyre-cli` release |
135
+ | `pyre profile <save\|load\|list> [name]` | Atomic configuration profile management (`~/.config/pyre/profiles/`) |
119
136
  | `pyre config <show\|reset>` | View or reset persistent configuration file (`~/.config/pyre/config.json`) |
120
137
  | `pyre completions <zsh\|bash\|fish>` | Generate shell auto-completion scripts |
121
138
 
@@ -123,7 +140,8 @@ These options apply to both static snapshots and `pyre live`, except where noted
123
140
 
124
141
  | Command | Description |
125
142
  |---|---|
126
- | `pyre info` | Concise hardware summary (CPU, memory, GPU, battery, thermal) |
143
+ | `pyre info` | Concise hardware summary (CPU, memory pressure, GPU, battery, displays, Time Machine) |
144
+ | `pyre anomalies [--since N]` | Compute z-score anomalies and print plain-language resource spike digest |
127
145
  | `pyre history [--days N]` | Graph historical resource trends from CSV logs over past N days |
128
146
  | `pyre diff <file1.json> <file2.json>` | Compare two saved snapshot files side-by-side |
129
147
 
@@ -131,7 +149,7 @@ These options apply to both static snapshots and `pyre live`, except where noted
131
149
 
132
150
  | Command | Description |
133
151
  |---|---|
134
- | `pyre bench <cmd>` | Log CPU, memory, network, and thermal usage while running a command |
152
+ | `pyre bench <cmd>` | Log CPU, memory, network, power draw, and estimate kWh energy cost during command execution |
135
153
  | `pyre xbar` | Generate an xbar / SwiftBar menu bar plugin script for stats display |
136
154
 
137
155
  ### Remote & Multi-host monitoring
@@ -147,11 +165,12 @@ These options apply to both static snapshots and `pyre live`, except where noted
147
165
  |---|---|
148
166
  | `pyre live` | Start the interactive live dashboard |
149
167
 
150
- ### Web dashboard
168
+ ### Web & Native dashboards
151
169
 
152
170
  | Command | Description |
153
171
  |---|---|
154
172
  | `pyre web` | Serve an auto-refreshing live web portal with real-time SSE stats streaming on localhost (`--port <port>`) |
173
+ | `pyre ui` | Launch the native macOS UI dashboard (wraps the web portal in a native window) |
155
174
 
156
175
  ### P2P
157
176
 
@@ -425,11 +444,16 @@ timestamp,cpu_usage,mem_usage_percent,temp_c,net_rx_bytes,net_tx_bytes,net_rx_pa
425
444
  pyre/
426
445
  ├── src/
427
446
  │ ├── index.ts # CLI entry point (commander definitions, main())
447
+ │ ├── anomaliesCmd.ts # Resource spike & anomaly digest generator
448
+ │ ├── brewHealth.ts # Homebrew health & cellar usage inspector
449
+ │ ├── doctor.ts # System diagnostics (permissions, Gatekeeper, SIP, XProtect)
450
+ │ ├── extensions.ts # System Extensions inspector (systemextensionsctl list)
451
+ │ ├── updateCheck.ts # Passive and on-demand npm registry version check
428
452
  │ ├── monitors/ # System metric collection (CPU, memory, disk, battery, thermal, network, packets, power, tasks)
429
453
  │ │ ├── index.ts # Public API: collectAll, collectPower, collectPackets, collectTasks
430
454
  │ │ ├── types.ts # TypeScript interfaces for all metric types
431
455
  │ │ ├── smc.ts # SMC sensor reading with caching
432
- │ │ ├── collectors.ts # Individual metric collectors and orchestrator
456
+ │ │ ├── collectors.ts # Individual metric collectors, displays, Time Machine, and orchestrator
433
457
  │ │ └── run.ts # Shared shell-execution helper
434
458
  │ ├── formatters/ # Output formatting and rendering
435
459
  │ │ ├── index.ts # Public API: formatTable, formatJson, formatCsv, formatTsv, formatGraphs
@@ -440,7 +464,7 @@ pyre/
440
464
  │ ├── live/ # Interactive live dashboard
441
465
  │ │ ├── index.ts # Public API: startLive, stopLive
442
466
  │ │ ├── state.ts # Shared mutable session state
443
- │ │ ├── input.ts # Keypress handling and session lifecycle
467
+ │ │ ├── input.ts # Keypress handling, signal picker, protected PID kill guards
444
468
  │ │ ├── render.ts # Screen rendering and alert checking
445
469
  │ │ ├── export.ts # Snapshot export and CSV logging
446
470
  │ │ └── types.ts # LiveOptions, ExportFormat, InputMode, SortMode
@@ -450,10 +474,13 @@ pyre/
450
474
  │ │ ├── protocol.ts # Length-prefixed JSON message framing
451
475
  │ │ ├── server.ts # TCP server: auth, TLS, rate limiting, IP filtering, audit logging, HMAC signing, data streaming
452
476
  │ │ └── client.ts # TCP client that connects and displays live data
477
+ │ ├── state/ # Config & profile state management
478
+ │ │ └── config.ts # Config file I/O & atomic profile save/load
453
479
  │ ├── history.ts # Rolling history buffer for sparkline data
454
- │ ├── sparkline.ts # ASCII sparkline rendering
480
+ │ ├── sparkline.ts # ASCII sparkline rendering & plain-text mode
455
481
  │ └── utils/ # Shared utility functions
456
482
  ├── dist/ # Built output (esm)
483
+ ├── grafana/ # Grafana dashboard JSON template
457
484
  ├── pyre-exports/ # Default directory for snapshot exports and logs
458
485
  ├── Formula/ # Homebrew formula
459
486
  ├── install.sh # curl installation script
@@ -0,0 +1,117 @@
1
+ // src/anomaliesCmd.ts
2
+ import fs from "fs";
3
+ import path from "path";
4
+ import chalk from "chalk";
5
+ function runAnomaliesDigest(opts = {}) {
6
+ const dir = opts.dir || "./pyre-exports";
7
+ const sinceStr = opts.since || "7d";
8
+ console.log(chalk.bold(`
9
+ pyre anomalies \u2014 Resource Spike & Anomaly Digest
10
+ `));
11
+ if (!fs.existsSync(dir)) {
12
+ console.log(chalk.yellow(` No logs directory found for range '${sinceStr}' at ${dir}.`));
13
+ console.log(chalk.dim(" Enable background logging with `pyre --log` to record history.\n"));
14
+ return;
15
+ }
16
+ const files = fs.readdirSync(dir).filter((f) => f.startsWith("pyre-log-") && f.endsWith(".csv"));
17
+ if (files.length === 0) {
18
+ console.log(chalk.yellow(` No CSV logs found for range '${sinceStr}' in ${dir}.`));
19
+ console.log(chalk.dim(" Enable background logging with `pyre --log` to record history.\n"));
20
+ return;
21
+ }
22
+ let cutoff = Date.now() - 7 * 24 * 60 * 60 * 1e3;
23
+ if (sinceStr === "yesterday" || sinceStr === "1d") {
24
+ cutoff = Date.now() - 24 * 60 * 60 * 1e3;
25
+ } else if (sinceStr.endsWith("d")) {
26
+ const d = parseInt(sinceStr.slice(0, -1), 10);
27
+ if (!isNaN(d)) cutoff = Date.now() - d * 24 * 60 * 60 * 1e3;
28
+ } else if (!isNaN(Date.parse(sinceStr))) {
29
+ cutoff = Date.parse(sinceStr);
30
+ }
31
+ const rows = [];
32
+ for (const file of files.sort()) {
33
+ const fullPath = path.join(dir, file);
34
+ try {
35
+ const content = fs.readFileSync(fullPath, "utf-8");
36
+ const lines = content.split("\n").slice(1);
37
+ for (const line of lines) {
38
+ if (!line.trim()) continue;
39
+ const parts = line.split(",");
40
+ if (parts.length >= 3) {
41
+ const ts = new Date(parts[0]);
42
+ if (!isNaN(ts.getTime()) && ts.getTime() >= cutoff) {
43
+ const cpu = parseFloat(parts[1]);
44
+ const mem = parseFloat(parts[2]);
45
+ const temp = parts.length >= 4 ? parseFloat(parts[3]) : void 0;
46
+ const topProc = parts.length >= 5 ? parts[4] : void 0;
47
+ if (!isNaN(cpu) && !isNaN(mem)) {
48
+ rows.push({ ts, cpu, mem, temp: isNaN(temp) ? void 0 : temp, topProc });
49
+ }
50
+ }
51
+ }
52
+ }
53
+ } catch {
54
+ }
55
+ }
56
+ if (rows.length < 5) {
57
+ console.log(chalk.yellow(` Insufficient data recorded since ${sinceStr} (need at least 5 samples).`));
58
+ console.log(chalk.dim(" Run pyre with `--log` for longer intervals to build history.\n"));
59
+ return;
60
+ }
61
+ const cpuVals = rows.map((r) => r.cpu);
62
+ const memVals = rows.map((r) => r.mem);
63
+ const tempVals = rows.filter((r) => r.temp !== void 0).map((r) => r.temp);
64
+ const calcMeanStd = (arr) => {
65
+ if (arr.length === 0) return { mean: 0, std: 0 };
66
+ const mean = arr.reduce((a, b) => a + b, 0) / arr.length;
67
+ const variance = arr.reduce((a, b) => a + Math.pow(b - mean, 2), 0) / arr.length;
68
+ return { mean, std: Math.sqrt(variance) };
69
+ };
70
+ const cpuStats = calcMeanStd(cpuVals);
71
+ const memStats = calcMeanStd(memVals);
72
+ const tempStats = calcMeanStd(tempVals);
73
+ const anomalies = [];
74
+ for (const r of rows) {
75
+ if (cpuStats.std > 0) {
76
+ const z = (r.cpu - cpuStats.mean) / cpuStats.std;
77
+ if (z >= 2.5) {
78
+ anomalies.push({ ts: r.ts, metric: "CPU", value: r.cpu, baseline: cpuStats.mean, zScore: z, topProc: r.topProc });
79
+ }
80
+ }
81
+ if (memStats.std > 0) {
82
+ const z = (r.mem - memStats.mean) / memStats.std;
83
+ if (z >= 2.5) {
84
+ anomalies.push({ ts: r.ts, metric: "Memory", value: r.mem, baseline: memStats.mean, zScore: z, topProc: r.topProc });
85
+ }
86
+ }
87
+ if (r.temp !== void 0 && tempStats.std > 0) {
88
+ const z = (r.temp - tempStats.mean) / tempStats.std;
89
+ if (z >= 2.5) {
90
+ anomalies.push({ ts: r.ts, metric: "Temperature", value: r.temp, baseline: tempStats.mean, zScore: z, topProc: r.topProc });
91
+ }
92
+ }
93
+ }
94
+ console.log(` Analyzed ${chalk.bold(rows.length)} samples since ${chalk.bold(sinceStr)}.`);
95
+ if (anomalies.length === 0) {
96
+ console.log(chalk.green(" \u2714 No statistical anomalies detected in this time range.\n"));
97
+ return;
98
+ }
99
+ console.log(chalk.yellow(` \u26A0 Found ${anomalies.length} anomaly spike event(s):
100
+ `));
101
+ for (const a of anomalies.slice(0, 15)) {
102
+ const timeFormatted = a.ts.toLocaleString();
103
+ const severityColor = a.zScore >= 3.5 ? chalk.red : chalk.yellow;
104
+ const unit = a.metric === "Temperature" ? "\xB0C" : "%";
105
+ console.log(` \u2022 [${chalk.dim(timeFormatted)}] ${severityColor(a.metric)} spike: ${chalk.bold(a.value.toFixed(1) + unit)} (baseline: ${a.baseline.toFixed(1)}${unit}, +${a.zScore.toFixed(1)}\u03C3)`);
106
+ if (a.topProc) {
107
+ console.log(chalk.dim(` Top Process: ${a.topProc}`));
108
+ }
109
+ }
110
+ if (anomalies.length > 15) {
111
+ console.log(chalk.dim(` ... and ${anomalies.length - 15} more anomaly events.`));
112
+ }
113
+ console.log();
114
+ }
115
+ export {
116
+ runAnomaliesDigest
117
+ };
@@ -0,0 +1,74 @@
1
+ import {
2
+ run
3
+ } from "./chunk-GLH7EKAX.js";
4
+
5
+ // src/brewHealth.ts
6
+ import chalk from "chalk";
7
+ async function getBrewHealth() {
8
+ try {
9
+ const brewPath = await run("which brew 2>&1", "");
10
+ if (!brewPath.trim() || brewPath.includes("not found")) {
11
+ return { installedCount: 0, outdatedCount: 0, cellarSize: "N/A", doctorWarnings: [], brewAvailable: false };
12
+ }
13
+ const outdatedRaw = await run("brew outdated --quiet 2>&1", "");
14
+ const outdatedLines = outdatedRaw.trim().split("\n").filter((l) => l.length > 0 && !l.startsWith("Homebrew") && !l.startsWith("==>"));
15
+ const listRaw = await run("brew list --formula -1 2>&1", "");
16
+ const listLines = listRaw.trim().split("\n").filter((l) => l.length > 0);
17
+ let cellarSize = "N/A";
18
+ try {
19
+ const cellarPath = await run("brew --cellar 2>&1", "");
20
+ if (cellarPath.trim()) {
21
+ const duRaw = await run(`du -sh "${cellarPath.trim()}" 2>&1`, "");
22
+ const duMatch = duRaw.match(/^([0-9\.\,]+[KMGTP]?i?B?)/i);
23
+ if (duMatch) cellarSize = duMatch[1];
24
+ }
25
+ } catch {
26
+ cellarSize = "N/A";
27
+ }
28
+ const doctorRaw = await run("brew doctor 2>&1", "");
29
+ const doctorWarnings = [];
30
+ const docLines = doctorRaw.split("\n");
31
+ for (const line of docLines) {
32
+ if (line.startsWith("Warning:")) {
33
+ doctorWarnings.push(line.replace(/^Warning:\s*/, "").trim());
34
+ }
35
+ }
36
+ return {
37
+ installedCount: listLines.length,
38
+ outdatedCount: outdatedLines.length,
39
+ cellarSize,
40
+ doctorWarnings,
41
+ brewAvailable: true
42
+ };
43
+ } catch (err) {
44
+ return { installedCount: 0, outdatedCount: 0, cellarSize: "N/A", doctorWarnings: [], brewAvailable: false };
45
+ }
46
+ }
47
+ async function printBrewHealthReport() {
48
+ console.log(chalk.bold("\n pyre brew \u2014 Homebrew Health Summary\n"));
49
+ const health = await getBrewHealth();
50
+ if (!health.brewAvailable) {
51
+ console.log(chalk.yellow(" \u26A0 Homebrew is not installed or not available on PATH.\n"));
52
+ return;
53
+ }
54
+ console.log(` \u{1F4E6} Installed Formulae: ${chalk.bold(health.installedCount)}`);
55
+ console.log(` \u23F3 Outdated Packages: ${health.outdatedCount > 0 ? chalk.yellow.bold(health.outdatedCount) : chalk.green("0 (Up to date)")}`);
56
+ console.log(` \u{1F4BE} Cellar Disk Usage: ${chalk.bold(health.cellarSize)}`);
57
+ console.log();
58
+ if (health.doctorWarnings.length === 0) {
59
+ console.log(chalk.green(" \u2714 brew doctor: No warnings detected system-wide."));
60
+ } else {
61
+ console.log(chalk.yellow(` \u26A0 brew doctor warnings (${health.doctorWarnings.length}):`));
62
+ for (const warn of health.doctorWarnings.slice(0, 5)) {
63
+ console.log(chalk.dim(` \u2022 ${warn}`));
64
+ }
65
+ if (health.doctorWarnings.length > 5) {
66
+ console.log(chalk.dim(` ... and ${health.doctorWarnings.length - 5} more.`));
67
+ }
68
+ }
69
+ console.log();
70
+ }
71
+ export {
72
+ getBrewHealth,
73
+ printBrewHealthReport
74
+ };
@@ -0,0 +1,15 @@
1
+ // src/monitors/run.ts
2
+ import { exec } from "child_process";
3
+ import { promisify } from "util";
4
+ var execAsync = promisify(exec);
5
+ async function run(cmd, fallback = "", timeout = 5e3) {
6
+ try {
7
+ return (await execAsync(cmd, { encoding: "utf8", timeout })).stdout.trim();
8
+ } catch {
9
+ return fallback;
10
+ }
11
+ }
12
+
13
+ export {
14
+ run
15
+ };