pyre-cli 7.0.0 → 8.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md 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,21 @@ 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 anomalies --since 7d # Statistical resource spike digest from past CSV logs
77
+ pyre doctor # System diagnostics (Gatekeeper, SIP, XProtect, powermetrics)
78
+ pyre extensions # System extensions inspector (systemextensionsctl)
79
+ pyre brew # Homebrew health summary & cellar size
80
+ pyre update # Check for pyre-cli updates
81
+ pyre profile save dev # Save active configuration profile
82
+ pyre profile load dev # Load configuration profile
83
+ pyre info # Concise hardware, display & Time Machine overview
84
+ pyre bench "make build" # Benchmark command & estimate kWh energy cost
77
85
  ```
78
86
 
79
87
  ## Options Reference
@@ -87,7 +95,9 @@ These options apply to both static snapshots and `pyre live`, except where noted
87
95
  | `--md` | Output as Markdown |
88
96
  | `-c, --csv` | Output as CSV |
89
97
  | `-t, --tsv` | Output as TSV |
98
+ | `--plain` / `--a11y` | Plain text output mode without ANSI colors or line-drawing characters |
90
99
  | `--detailed` | Include detailed system info and sensor readings |
100
+ | `--since <range>` | Time range for anomaly digest (e.g. `1d`, `7d`, `yesterday`) |
91
101
  | `--theme <name>` | Theme for live mode (`default`, `dracula`, `cyberpunk`, `monochrome`, `nord`, `gruvbox`) |
92
102
  | `--interval <seconds>` | Refresh interval for live mode (default: `2`) |
93
103
  | `--once` | Show a single static snapshot instead of live feed |
@@ -115,7 +125,11 @@ These options apply to both static snapshots and `pyre live`, except where noted
115
125
  | `pyre --json` | JSON output |
116
126
  | `pyre --html` | Self-contained HTML report |
117
127
  | `pyre --md` | Markdown tables |
118
- | `pyre doctor` | Run system diagnostics (permissions, powermetrics access, P2P network check) |
128
+ | `pyre doctor` | Run system diagnostics (permissions, Gatekeeper, SIP, XProtect, P2P network) |
129
+ | `pyre extensions` | System Extensions inspector (`systemextensionsctl list`) |
130
+ | `pyre brew` | Homebrew health panel (installed formulae, outdated count, Cellar disk size, doctor summary) |
131
+ | `pyre update` | Check npm registry for latest `pyre-cli` release |
132
+ | `pyre profile <save\|load\|list> [name]` | Atomic configuration profile management (`~/.config/pyre/profiles/`) |
119
133
  | `pyre config <show\|reset>` | View or reset persistent configuration file (`~/.config/pyre/config.json`) |
120
134
  | `pyre completions <zsh\|bash\|fish>` | Generate shell auto-completion scripts |
121
135
 
@@ -123,7 +137,8 @@ These options apply to both static snapshots and `pyre live`, except where noted
123
137
 
124
138
  | Command | Description |
125
139
  |---|---|
126
- | `pyre info` | Concise hardware summary (CPU, memory, GPU, battery, thermal) |
140
+ | `pyre info` | Concise hardware summary (CPU, memory pressure, GPU, battery, displays, Time Machine) |
141
+ | `pyre anomalies [--since N]` | Compute z-score anomalies and print plain-language resource spike digest |
127
142
  | `pyre history [--days N]` | Graph historical resource trends from CSV logs over past N days |
128
143
  | `pyre diff <file1.json> <file2.json>` | Compare two saved snapshot files side-by-side |
129
144
 
@@ -131,7 +146,7 @@ These options apply to both static snapshots and `pyre live`, except where noted
131
146
 
132
147
  | Command | Description |
133
148
  |---|---|
134
- | `pyre bench <cmd>` | Log CPU, memory, network, and thermal usage while running a command |
149
+ | `pyre bench <cmd>` | Log CPU, memory, network, power draw, and estimate kWh energy cost during command execution |
135
150
  | `pyre xbar` | Generate an xbar / SwiftBar menu bar plugin script for stats display |
136
151
 
137
152
  ### Remote & Multi-host monitoring
@@ -425,11 +440,16 @@ timestamp,cpu_usage,mem_usage_percent,temp_c,net_rx_bytes,net_tx_bytes,net_rx_pa
425
440
  pyre/
426
441
  ├── src/
427
442
  │ ├── index.ts # CLI entry point (commander definitions, main())
443
+ │ ├── anomaliesCmd.ts # Resource spike & anomaly digest generator
444
+ │ ├── brewHealth.ts # Homebrew health & cellar usage inspector
445
+ │ ├── doctor.ts # System diagnostics (permissions, Gatekeeper, SIP, XProtect)
446
+ │ ├── extensions.ts # System Extensions inspector (systemextensionsctl list)
447
+ │ ├── updateCheck.ts # Passive and on-demand npm registry version check
428
448
  │ ├── monitors/ # System metric collection (CPU, memory, disk, battery, thermal, network, packets, power, tasks)
429
449
  │ │ ├── index.ts # Public API: collectAll, collectPower, collectPackets, collectTasks
430
450
  │ │ ├── types.ts # TypeScript interfaces for all metric types
431
451
  │ │ ├── smc.ts # SMC sensor reading with caching
432
- │ │ ├── collectors.ts # Individual metric collectors and orchestrator
452
+ │ │ ├── collectors.ts # Individual metric collectors, displays, Time Machine, and orchestrator
433
453
  │ │ └── run.ts # Shared shell-execution helper
434
454
  │ ├── formatters/ # Output formatting and rendering
435
455
  │ │ ├── index.ts # Public API: formatTable, formatJson, formatCsv, formatTsv, formatGraphs
@@ -440,7 +460,7 @@ pyre/
440
460
  │ ├── live/ # Interactive live dashboard
441
461
  │ │ ├── index.ts # Public API: startLive, stopLive
442
462
  │ │ ├── state.ts # Shared mutable session state
443
- │ │ ├── input.ts # Keypress handling and session lifecycle
463
+ │ │ ├── input.ts # Keypress handling, signal picker, protected PID kill guards
444
464
  │ │ ├── render.ts # Screen rendering and alert checking
445
465
  │ │ ├── export.ts # Snapshot export and CSV logging
446
466
  │ │ └── types.ts # LiveOptions, ExportFormat, InputMode, SortMode
@@ -450,10 +470,13 @@ pyre/
450
470
  │ │ ├── protocol.ts # Length-prefixed JSON message framing
451
471
  │ │ ├── server.ts # TCP server: auth, TLS, rate limiting, IP filtering, audit logging, HMAC signing, data streaming
452
472
  │ │ └── client.ts # TCP client that connects and displays live data
473
+ │ ├── state/ # Config & profile state management
474
+ │ │ └── config.ts # Config file I/O & atomic profile save/load
453
475
  │ ├── history.ts # Rolling history buffer for sparkline data
454
- │ ├── sparkline.ts # ASCII sparkline rendering
476
+ │ ├── sparkline.ts # ASCII sparkline rendering & plain-text mode
455
477
  │ └── utils/ # Shared utility functions
456
478
  ├── dist/ # Built output (esm)
479
+ ├── grafana/ # Grafana dashboard JSON template
457
480
  ├── pyre-exports/ # Default directory for snapshot exports and logs
458
481
  ├── Formula/ # Homebrew formula
459
482
  ├── 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
+ };