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 +42 -19
- package/dist/anomaliesCmd-TO52YY2B.js +117 -0
- package/dist/brewHealth-6NE37NAU.js +74 -0
- package/dist/chunk-GLH7EKAX.js +15 -0
- package/dist/chunk-KBKXU2ZV.js +859 -0
- package/dist/chunk-RVGKEJXT.js +121 -0
- package/dist/collectors-4VYRJVRO.js +33 -0
- package/dist/completions-VQMHQCXE.js +122 -0
- package/dist/config-4ZLF4HTS.js +22 -0
- package/dist/doctor-LJQAABZ4.js +184 -0
- package/dist/extensions-QCFSVCL5.js +74 -0
- package/dist/index.js +1281 -1193
- package/dist/updateCheck-52X6LJG5.js +84 -0
- package/package.json +1 -1
- package/parity.md +16 -9
- package/src/anomaliesCmd.ts +151 -0
- package/src/brewHealth.ts +88 -0
- package/src/doctor.ts +77 -1
- package/src/easy_features.test.ts +43 -0
- package/src/extensions.ts +83 -0
- package/src/formatters/html_template.ts +655 -0
- package/src/formatters/index.ts +1 -0
- package/src/formatters/render.ts +2 -2
- package/src/index.ts +280 -26
- package/src/live/input.ts +14 -2
- package/src/monitors/collectors.ts +110 -5
- package/src/monitors/types.ts +6 -0
- package/src/sparkline.ts +63 -2
- package/src/state/config.ts +36 -2
- package/src/updateCheck.ts +94 -0
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
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
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 --
|
|
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
|
|
73
|
-
pyre
|
|
74
|
-
pyre
|
|
75
|
-
pyre
|
|
76
|
-
pyre
|
|
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,
|
|
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,
|
|
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
|
|
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
|
|
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
|
+
};
|