pyre-cli 9.5.2 → 10.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.
Files changed (86) hide show
  1. package/README.md +73 -16
  2. package/dist/alertChannels-TPSQIJ7S.js +8 -0
  3. package/dist/{blender-7UT5J7IP.js → blender-FGH3ASEQ.js} +2 -2
  4. package/dist/brewHealth-P2GW5Y5C.js +176 -0
  5. package/dist/buildCmd-CDPZ2ZFA.js +38 -0
  6. package/dist/{checkCmd-T7O4KXXA.js → checkCmd-RKW6QGDR.js} +9 -11
  7. package/dist/{chunk-CXL4GYGQ.js → chunk-53O2RGSL.js} +44 -17
  8. package/dist/{chunk-3TE7MJLS.js → chunk-6UHZP4QP.js} +36 -10
  9. package/dist/chunk-GHRDW646.js +2506 -0
  10. package/dist/{prometheus-LFYHF6VO.js → chunk-HT4VQ4OH.js} +1 -0
  11. package/dist/chunk-M73HCQD2.js +95 -0
  12. package/dist/chunk-MZO5QKUR.js +32 -0
  13. package/dist/{chunk-25I533OF.js → chunk-VIACN2RU.js} +1 -1
  14. package/dist/chunk-Z2YV5HKV.js +87 -0
  15. package/dist/chunk-ZLVIVFSO.js +110 -0
  16. package/dist/{collectors-JB4OGUEF.js → collectors-H43PN4PS.js} +6 -3
  17. package/dist/{completions-VQMHQCXE.js → completions-JKYEJNGR.js} +34 -1
  18. package/dist/{config-KS5ZZ3VN.js → config-GYGJXU2Q.js} +9 -1
  19. package/dist/doctor-GYHFTNZQ.js +387 -0
  20. package/dist/explainCmd-MF6PIMLW.js +222 -0
  21. package/dist/extensions-XJGHG7X3.js +134 -0
  22. package/dist/index.js +460 -88
  23. package/dist/ioreport.py +72 -0
  24. package/dist/netUsageCmd-WKBTW2YG.js +90 -0
  25. package/dist/{pipeCmd-TYLJHQ5B.js → pipeCmd-7REDBLBU.js} +4 -3
  26. package/dist/profileProcCmd-N4H6MCBT.js +107 -0
  27. package/dist/prometheus-5BL5PVLC.js +6 -0
  28. package/dist/prometheusCmd-PL64BLN5.js +93 -0
  29. package/dist/replayCmd-Z3PYXY7V.js +164 -0
  30. package/dist/run-HOGQDDTA.js +8 -0
  31. package/dist/serveCmd-2H5VLF3P.js +153 -0
  32. package/dist/service-Q74BBILO.js +181 -0
  33. package/dist/smartCmd-ODWKSFNG.js +166 -0
  34. package/dist/syncCmd-A2W3QB3S.js +126 -0
  35. package/dist/topoCmd-DU2HHZNH.js +148 -0
  36. package/dist/watchdogCmd-UF53JPZB.js +161 -0
  37. package/dist/{wizard-Z3RGA3VB.js → wizard-TD7BGX6O.js} +1 -1
  38. package/install.ps1 +29 -0
  39. package/install.sh +10 -9
  40. package/package.json +7 -3
  41. package/src/ai/models.ts +138 -0
  42. package/src/alertChannels.ts +119 -0
  43. package/src/brewHealth.ts +157 -40
  44. package/src/buildCmd.ts +48 -0
  45. package/src/checkCmd.ts +5 -5
  46. package/src/completions.ts +35 -0
  47. package/src/doctor.ts +235 -7
  48. package/src/easy_features.test.ts +40 -0
  49. package/src/explainCmd.ts +261 -0
  50. package/src/extensions.ts +71 -7
  51. package/src/formatters/render.ts +34 -10
  52. package/src/formatters/types.ts +2 -0
  53. package/src/index.ts +251 -36
  54. package/src/live/input.ts +60 -0
  55. package/src/live/render.ts +67 -0
  56. package/src/live/state.ts +5 -0
  57. package/src/live/types.ts +1 -1
  58. package/src/monitors/batteryPredictor.ts +143 -0
  59. package/src/monitors/buildTracker.ts +121 -0
  60. package/src/monitors/collectors.ts +342 -178
  61. package/src/monitors/ioreport.py +72 -0
  62. package/src/monitors/platform/linux.ts +756 -0
  63. package/src/monitors/platform/windows.ts +519 -0
  64. package/src/monitors/run.ts +27 -3
  65. package/src/monitors/smc.ts +21 -1
  66. package/src/monitors/types.ts +15 -0
  67. package/src/netUsageCmd.ts +121 -0
  68. package/src/platform.test.ts +99 -0
  69. package/src/plugins/index.ts +143 -0
  70. package/src/profileProcCmd.ts +133 -0
  71. package/src/prometheusCmd.ts +107 -0
  72. package/src/replayCmd.ts +186 -0
  73. package/src/serveCmd.ts +180 -0
  74. package/src/service.ts +125 -7
  75. package/src/smartCmd.ts +205 -0
  76. package/src/state/config.ts +58 -17
  77. package/src/syncCmd.ts +166 -0
  78. package/src/topoCmd.ts +176 -0
  79. package/src/watchdogCmd.ts +198 -0
  80. package/src/xbar.ts +22 -11
  81. package/dist/brewHealth-F2BXF5M6.js +0 -74
  82. package/dist/chunk-FH5WDJBF.js +0 -15
  83. package/dist/chunk-N4WJVF5M.js +0 -1184
  84. package/dist/doctor-UV7TWIHX.js +0 -184
  85. package/dist/extensions-DVA5YLGH.js +0 -74
  86. package/dist/service-WTS3NBQU.js +0 -73
package/README.md CHANGED
@@ -1,13 +1,15 @@
1
1
  # pyre
2
2
 
3
- Mac system monitoring CLI: temps, CPU, memory, disk, battery, GPU, power draw, live dashboard, packet monitor, process management, export, alerts, P2P live data streaming, web dashboard, SSH monitoring, micro-benchmarking, and Blender render tracking.
3
+ Cross-platform system monitoring CLI for **macOS**, **Linux**, and **Windows**: temps, CPU, memory, disk, battery, GPU, power draw, live dashboard, packet monitor, process management, export, alerts, P2P live data streaming, web dashboard, SSH monitoring, micro-benchmarking, and background service management.
4
4
 
5
5
  ![Version](https://img.shields.io/badge/version-5.0.0-blue)
6
- ![macOS](https://img.shields.io/badge/macos-14%2B-lightgrey)
6
+ ![macOS](https://img.shields.io/badge/macOS-14%2B-lightgrey)
7
+ ![Linux](https://img.shields.io/badge/Linux-x86__64%20%7C%20arm64-orange)
8
+ ![Windows](https://img.shields.io/badge/Windows-10%20%7C%2011%20%7C%20Server-blue)
7
9
  ![Node](https://img.shields.io/badge/node-18%2B-green)
8
10
  ![License](https://img.shields.io/badge/license-MIT-green)
9
11
 
10
- > **Note:** things may not work as intended if your firewall settings do not allow inbound connections for this program.
12
+ > **Note:** things may not work as intended if your firewall settings do not allow inbound connections for P2P streaming or Web dashboard mode.
11
13
  >
12
14
  > You can skip init with Enter, but you may encounter some UI bugs. This is being worked on.
13
15
 
@@ -51,18 +53,24 @@ Mac system monitoring CLI: temps, CPU, memory, disk, battery, GPU, power draw, l
51
53
 
52
54
  ## Installation
53
55
 
54
- ### npm
56
+ ### npm (macOS, Linux, Windows)
55
57
 
56
58
  ```bash
57
59
  npm install -g pyre-cli
58
60
  ```
59
61
 
60
- ### curl
62
+ ### curl (macOS & Linux)
61
63
 
62
64
  ```bash
63
65
  curl -fsSL https://raw.githubusercontent.com/somalip/pyre/main/install.sh | bash
64
66
  ```
65
67
 
68
+ ### PowerShell (Windows)
69
+
70
+ ```powershell
71
+ irm https://raw.githubusercontent.com/somalip/pyre/main/install.ps1 | iex
72
+ ```
73
+
66
74
  ## Quick Start
67
75
 
68
76
  ```bash
@@ -441,18 +449,17 @@ timestamp,cpu_usage,mem_usage_percent,temp_c,net_rx_bytes,net_tx_bytes,net_rx_pa
441
449
 
442
450
  ## Requirements
443
451
 
444
- - **macOS 14+** (Sonoma or later)
445
452
  - **Node.js 18+**
446
- - **Optional:** `sudo` for `powermetrics` detailed temperatures in `--detailed` mode
453
+ - **macOS:** macOS 14+ (Sonoma or later recommended). Optional `sudo` for `powermetrics` detailed thermal/power readings.
454
+ - **Linux:** Any modern Linux distribution (Ubuntu/Debian, Fedora/RHEL, Arch) with kernel 4.x+; reads `/proc` and `/sys`. Optional `smartmontools` for disk health.
455
+ - **Windows:** Windows 10, Windows 11, or Windows Server 2016+ with PowerShell 5.1+.
447
456
 
448
- To enable password‑less access for `powermetrics` (required for accurate sensor readings), run:
457
+ On macOS, to enable password‑less access for `powermetrics` (optional, for detailed sensor readings), run:
449
458
 
450
459
  ```bash
451
460
  echo "$(whoami) ALL=(root) NOPASSWD: /usr/bin/powermetrics" | sudo tee /etc/sudoers.d/pyre-powermetrics
452
461
  ```
453
462
 
454
- This adds a single sudoers entry granting `pyre` the ability to execute `powermetrics` without a password prompt. Remove it later with `sudo rm /etc/sudoers.d/pyre-powermetrics`.
455
-
456
463
  ## Project Structure
457
464
 
458
465
  ```
@@ -460,16 +467,20 @@ pyre/
460
467
  ├── src/
461
468
  │ ├── index.ts # CLI entry point (commander definitions, main())
462
469
  │ ├── anomaliesCmd.ts # Resource spike & anomaly digest generator
463
- │ ├── brewHealth.ts # Homebrew health & cellar usage inspector
464
- │ ├── doctor.ts # System diagnostics (permissions, Gatekeeper, SIP, XProtect)
465
- │ ├── extensions.ts # System Extensions inspector (systemextensionsctl list)
470
+ │ ├── brewHealth.ts # Package manager health inspector (Brew, APT, Pacman, DNF, Winget, Scoop)
471
+ │ ├── doctor.ts # System diagnostics (macOS SIP/Gatekeeper, Linux /proc/sys, Windows WMI)
472
+ │ ├── extensions.ts # Extensions/modules inspector (macOS extensions, Linux lsmod, Windows driverquery)
473
+ │ ├── service.ts # Background service daemon manager (launchd, systemd, schtasks)
466
474
  │ ├── updateCheck.ts # Passive and on-demand npm registry version check
467
475
  │ ├── monitors/ # System metric collection (CPU, memory, disk, battery, thermal, network, packets, power, tasks)
468
476
  │ │ ├── index.ts # Public API: collectAll, collectPower, collectPackets, collectTasks
469
477
  │ │ ├── types.ts # TypeScript interfaces for all metric types
470
- │ │ ├── smc.ts # SMC sensor reading with caching
471
- │ │ ├── collectors.ts # Individual metric collectors, displays, Time Machine, and orchestrator
472
- │ │ └── run.ts # Shared shell-execution helper
478
+ │ │ ├── smc.ts # SMC sensor reading with caching (macOS)
479
+ │ │ ├── collectors.ts # Metric collection orchestrator & platform dispatch
480
+ │ │ ├── platform/ # OS-specific native metric collectors
481
+ │ │ │ ├── linux.ts # Linux /proc, /sys, df, ps, and netstat collectors
482
+ │ │ │ └── windows.ts # Windows os, WMI/PowerShell, netstat, and tasklist collectors
483
+ │ │ └── run.ts # Shared shell & PowerShell execution helpers
473
484
  │ ├── formatters/ # Output formatting and rendering
474
485
  │ │ ├── index.ts # Public API: formatTable, formatJson, formatCsv, formatTsv, formatGraphs
475
486
  │ │ ├── output.ts # JSON/CSV/TSV serialisation and sparkline graphs
@@ -523,6 +534,52 @@ npm install
523
534
  | `npm run dev` | Run directly via `tsx` (no build step) |
524
535
  | `npm test` | Verify the built binary responds to `--help` |
525
536
 
537
+ ## Roadmap
538
+
539
+ Proposed features for upcoming versions, organized by effort and impact.
540
+
541
+ ### Tier 1 — High Impact, Achievable
542
+
543
+ | Feature | Description |
544
+ |---|---|
545
+ | **AI Anomaly Explanation** (`pyre explain`) | Translate z-score spikes into plain-English diagnoses using a local (Ollama) or API-based LLM |
546
+ | **S.M.A.R.T. Disk Health** (`pyre smart`) | Display SSD/HDD wear level, reallocated sectors, power-on hours, and remaining lifespan via `smartctl` |
547
+ | **Docker / Container Panel** | Show running Docker/OrbStack containers with CPU%, memory, network I/O, and status — toggle with `C` in the live dashboard |
548
+ | **Fan Speed Monitoring** | Display current RPM for all system fans via SMC readings in the Thermal panel |
549
+ | **Log Replay Mode** (`pyre replay`) | Load a historical CSV log and play it back inside the interactive TUI with scrubbing controls |
550
+ | **Multi-Channel Alert Integrations** | Native Slack, Discord, Pushover, and ntfy.sh alert targets with pre-configured payload templates (extends `--webhook-url`) |
551
+ | **Prometheus Exporter** | Expose a `/metrics` endpoint in Prometheus text format — `src/prometheus.ts` is already stubbed |
552
+
553
+ ### Tier 2 — Strong Differentiation
554
+
555
+ | Feature | Description |
556
+ |---|---|
557
+ | **Plugin System** | User-authored metric collectors as JS scripts in `~/.config/pyre/plugins/` rendered as custom dashboard panels |
558
+ | **Per-App Network Usage** (`pyre netusage`) | Real-time and cumulative network I/O broken down per application, backed by `nettop` (parser already in repo) |
559
+ | **Configurable Dashboard Grid** | Define custom panel layout, order, and sizing in `~/.config/pyre/config.json` |
560
+ | **Smart Battery Predictor** | Use accumulated power-drain history from CSV logs to produce a personalized, heuristic battery time-remaining estimate |
561
+ | **REST API Mode** (`pyre serve`) | Expose all metrics as structured JSON REST endpoints (`GET /api/cpu`, `/api/all`, etc.) with optional API key auth |
562
+ | **Process CPU Profiling** (`pyre profile-proc <pid>`) | Capture a 5-second `sample`/`spindump` for a misbehaving process and render an annotated call-tree summary |
563
+ | **Watchdog Rules Engine** (`pyre watchdog`) | Persistent daemon that monitors metrics and executes configurable rules — e.g. "if CPU > 90% for 5 min → kill `photoanalysisd` → notify Slack" |
564
+ | **Build System Tracker** (`pyre build`) | First-class tracking for `xcodebuild`, `cargo`, `make`, `bazel` — extends the existing Blender render tracker pattern |
565
+
566
+ ### Tier 3 — Big Bets
567
+
568
+ | Feature | Description |
569
+ |---|---|
570
+ | **Network Topology Mapper** (`pyre topo`) | Visualize LAN peers, active connections, and routing hops as an ASCII diagram; auto-detects pyre-capable peers |
571
+ | **Cloud Sync** (`pyre sync`) | Sync config profiles, alert thresholds, and anomaly history across multiple Macs via iCloud Drive or a custom S3/WebDAV backend |
572
+
573
+ ### Quality of Life
574
+
575
+ | Feature | Description |
576
+ |---|---|
577
+ | **Gaming / Performance Mode Profile** | `pyre live --profile gaming` — GPU + Thermal + Power panels only, 1 s interval, bar graph mode, minimal overlay |
578
+ | **iOS Shortcut Integration** | Documented iOS Shortcut that runs `pyre check --plain` over SSH and displays a summary notification |
579
+ | **Setup Wizard Polish** (`pyre setup`) | Guided first-run experience for `powermetrics` sudo, alert thresholds, theme selection, and webhook setup |
580
+
581
+ ---
582
+
526
583
  ## License
527
584
 
528
585
  MIT
@@ -0,0 +1,8 @@
1
+ import {
2
+ sendAlert,
3
+ sendTestAlert
4
+ } from "./chunk-Z2YV5HKV.js";
5
+ export {
6
+ sendAlert,
7
+ sendTestAlert
8
+ };
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  collectBlenderRenders
3
- } from "./chunk-25I533OF.js";
4
- import "./chunk-FH5WDJBF.js";
3
+ } from "./chunk-VIACN2RU.js";
4
+ import "./chunk-MZO5QKUR.js";
5
5
  export {
6
6
  collectBlenderRenders
7
7
  };
@@ -0,0 +1,176 @@
1
+ import {
2
+ run
3
+ } from "./chunk-MZO5QKUR.js";
4
+
5
+ // src/brewHealth.ts
6
+ import chalk from "chalk";
7
+ async function getBrewHealth() {
8
+ const isWindows = process.platform === "win32";
9
+ try {
10
+ const brewPath = await run(isWindows ? "where brew 2>nul" : "which brew 2>&1", "");
11
+ if (brewPath.trim() && !brewPath.includes("not found") && !brewPath.includes("Could not find")) {
12
+ const outdatedRaw = await run("brew outdated --quiet 2>&1", "");
13
+ const outdatedLines = outdatedRaw.trim().split("\n").filter((l) => l.length > 0 && !l.startsWith("Homebrew") && !l.startsWith("==>"));
14
+ const listRaw = await run("brew list --formula -1 2>&1", "");
15
+ const listLines = listRaw.trim().split("\n").filter((l) => l.length > 0);
16
+ let cellarSize = "N/A";
17
+ try {
18
+ const cellarPath = await run("brew --cellar 2>&1", "");
19
+ if (cellarPath.trim()) {
20
+ const duRaw = await run(`du -sh "${cellarPath.trim()}" 2>&1`, "");
21
+ const duMatch = duRaw.match(/^([0-9.,]+[KMGTP]?i?B?)/i);
22
+ if (duMatch) cellarSize = duMatch[1];
23
+ }
24
+ } catch {
25
+ cellarSize = "N/A";
26
+ }
27
+ const doctorRaw = await run("brew doctor 2>&1", "");
28
+ const doctorWarnings = [];
29
+ const docLines = doctorRaw.split("\n");
30
+ for (const line of docLines) {
31
+ if (line.startsWith("Warning:")) {
32
+ doctorWarnings.push(line.replace(/^Warning:\s*/, "").trim());
33
+ }
34
+ }
35
+ return {
36
+ installedCount: listLines.length,
37
+ outdatedCount: outdatedLines.length,
38
+ cellarSize,
39
+ doctorWarnings,
40
+ brewAvailable: true,
41
+ managerName: "Homebrew"
42
+ };
43
+ }
44
+ } catch {
45
+ }
46
+ if (process.platform === "linux") {
47
+ try {
48
+ const whichApt = (await run("which apt-get 2>/dev/null", "")).trim();
49
+ if (whichApt) {
50
+ const listRaw = (await run("dpkg-query -l 2>/dev/null", "")).trim();
51
+ const installed = listRaw ? listRaw.split("\n").filter((l) => l.startsWith("ii")).length : 0;
52
+ const upgradeRaw = (await run("apt list --upgradable 2>/dev/null", "")).trim();
53
+ const outdated = upgradeRaw ? upgradeRaw.split("\n").filter((l) => l.includes("/") && !l.startsWith("Listing")).length : 0;
54
+ return {
55
+ installedCount: installed,
56
+ outdatedCount: outdated,
57
+ cellarSize: "/var/cache/apt",
58
+ doctorWarnings: [],
59
+ brewAvailable: true,
60
+ managerName: "APT (Debian/Ubuntu)"
61
+ };
62
+ }
63
+ } catch {
64
+ }
65
+ try {
66
+ const whichPacman = (await run("which pacman 2>/dev/null", "")).trim();
67
+ if (whichPacman) {
68
+ const listRaw = (await run("pacman -Q 2>/dev/null", "")).trim();
69
+ const installed = listRaw ? listRaw.split("\n").filter(Boolean).length : 0;
70
+ const upgradeRaw = (await run("checkupdates 2>/dev/null || pacman -Qu 2>/dev/null", "")).trim();
71
+ const outdated = upgradeRaw ? upgradeRaw.split("\n").filter(Boolean).length : 0;
72
+ return {
73
+ installedCount: installed,
74
+ outdatedCount: outdated,
75
+ cellarSize: "/var/cache/pacman",
76
+ doctorWarnings: [],
77
+ brewAvailable: true,
78
+ managerName: "Pacman (Arch)"
79
+ };
80
+ }
81
+ } catch {
82
+ }
83
+ try {
84
+ const whichDnf = (await run("which dnf 2>/dev/null", "")).trim();
85
+ if (whichDnf) {
86
+ const listRaw = (await run("dnf list installed 2>/dev/null", "")).trim();
87
+ const installed = listRaw ? listRaw.split("\n").filter(Boolean).length - 1 : 0;
88
+ return {
89
+ installedCount: Math.max(0, installed),
90
+ outdatedCount: 0,
91
+ cellarSize: "/var/cache/dnf",
92
+ doctorWarnings: [],
93
+ brewAvailable: true,
94
+ managerName: "DNF (Fedora/RHEL)"
95
+ };
96
+ }
97
+ } catch {
98
+ }
99
+ }
100
+ if (isWindows) {
101
+ try {
102
+ const whichWinget = (await run("where winget 2>nul", "")).trim();
103
+ if (whichWinget) {
104
+ const listRaw = (await run("winget list 2>nul", "", 4e3)).trim();
105
+ const installed = listRaw ? listRaw.split("\n").filter(Boolean).length - 2 : 0;
106
+ return {
107
+ installedCount: Math.max(0, installed),
108
+ outdatedCount: 0,
109
+ cellarSize: "N/A",
110
+ doctorWarnings: [],
111
+ brewAvailable: true,
112
+ managerName: "Windows Package Manager (winget)"
113
+ };
114
+ }
115
+ } catch {
116
+ }
117
+ try {
118
+ const whichScoop = (await run("where scoop 2>nul", "")).trim();
119
+ if (whichScoop) {
120
+ const listRaw = (await run("scoop list 2>nul", "")).trim();
121
+ const installed = listRaw ? listRaw.split("\n").filter(Boolean).length - 4 : 0;
122
+ return {
123
+ installedCount: Math.max(0, installed),
124
+ outdatedCount: 0,
125
+ cellarSize: "N/A",
126
+ doctorWarnings: [],
127
+ brewAvailable: true,
128
+ managerName: "Scoop"
129
+ };
130
+ }
131
+ } catch {
132
+ }
133
+ }
134
+ return {
135
+ installedCount: 0,
136
+ outdatedCount: 0,
137
+ cellarSize: "N/A",
138
+ doctorWarnings: [],
139
+ brewAvailable: false,
140
+ managerName: "None"
141
+ };
142
+ }
143
+ async function printBrewHealthReport() {
144
+ const health = await getBrewHealth();
145
+ const label = health.brewAvailable ? health.managerName : "Package Manager";
146
+ console.log(chalk.bold(`
147
+ pyre brew \u2014 ${label} Health Summary
148
+ `));
149
+ if (!health.brewAvailable) {
150
+ console.log(chalk.yellow(" \u26A0 No supported package manager detected on PATH.\n"));
151
+ return;
152
+ }
153
+ console.log(` \u{1F4E6} Package Manager: ${chalk.cyan.bold(health.managerName)}`);
154
+ console.log(` \u{1F4E6} Installed Packages: ${chalk.bold(health.installedCount)}`);
155
+ console.log(` \u23F3 Outdated Packages: ${health.outdatedCount > 0 ? chalk.yellow.bold(health.outdatedCount) : chalk.green("0 (Up to date)")}`);
156
+ if (health.cellarSize !== "N/A") {
157
+ console.log(` \u{1F4BE} Cache / Cellar: ${chalk.bold(health.cellarSize)}`);
158
+ }
159
+ console.log();
160
+ if (health.doctorWarnings.length === 0) {
161
+ console.log(chalk.green(` \u2714 ${health.managerName}: Ready and healthy.`));
162
+ } else {
163
+ console.log(chalk.yellow(` \u26A0 Diagnostics warnings (${health.doctorWarnings.length}):`));
164
+ for (const warn of health.doctorWarnings.slice(0, 5)) {
165
+ console.log(chalk.dim(` \u2022 ${warn}`));
166
+ }
167
+ if (health.doctorWarnings.length > 5) {
168
+ console.log(chalk.dim(` ... and ${health.doctorWarnings.length - 5} more.`));
169
+ }
170
+ }
171
+ console.log();
172
+ }
173
+ export {
174
+ getBrewHealth,
175
+ printBrewHealthReport
176
+ };
@@ -0,0 +1,38 @@
1
+ import {
2
+ collectBuilds
3
+ } from "./chunk-M73HCQD2.js";
4
+ import "./chunk-MZO5QKUR.js";
5
+
6
+ // src/buildCmd.ts
7
+ import chalk from "chalk";
8
+ async function runBuildCommand(opts = {}) {
9
+ const builds = await collectBuilds();
10
+ if (opts.json) {
11
+ console.log(JSON.stringify({ activeBuilds: builds }, null, 2));
12
+ return;
13
+ }
14
+ console.log(chalk.bold("\n \u{1F525} pyre build \u2014 Active Build System Tracker\n"));
15
+ if (builds.length === 0) {
16
+ console.log(chalk.dim(" No active build or compilation processes detected."));
17
+ console.log(chalk.dim(" (Supports: xcodebuild, cargo, swift build, make, ninja, bazel, go build)\n"));
18
+ return;
19
+ }
20
+ for (const b of builds) {
21
+ const elapsedMins = Math.floor(b.elapsedSec / 60);
22
+ const elapsedSecs = b.elapsedSec % 60;
23
+ const timeStr = elapsedMins > 0 ? `${elapsedMins}m ${elapsedSecs}s` : `${elapsedSecs}s`;
24
+ console.log(` ${chalk.bgCyan.black.bold(` ${b.tool.toUpperCase()} `)} ${chalk.bold(`PID ${b.rootPid}`)}`);
25
+ console.log(` Command: ${chalk.white(b.command)}`);
26
+ if (b.target) {
27
+ console.log(` Target: ${chalk.cyan(b.target)}`);
28
+ }
29
+ console.log(` Compiler Workers: ${chalk.green.bold(String(b.compilerProcesses))} active thread/processes`);
30
+ console.log(` Combined CPU: ${chalk.yellow.bold(b.totalCpu.toFixed(1) + "%")}`);
31
+ console.log(` Combined RAM: ${chalk.magenta(b.totalMem.toFixed(1) + "%")}`);
32
+ console.log(` Elapsed Time: ${chalk.dim(timeStr)}`);
33
+ console.log();
34
+ }
35
+ }
36
+ export {
37
+ runBuildCommand
38
+ };
@@ -1,12 +1,10 @@
1
- import {
2
- formatBytes
3
- } from "./chunk-3TE7MJLS.js";
4
1
  import "./chunk-IM3IQMMT.js";
5
2
  import {
6
3
  collectAll
7
- } from "./chunk-N4WJVF5M.js";
8
- import "./chunk-25I533OF.js";
9
- import "./chunk-FH5WDJBF.js";
4
+ } from "./chunk-GHRDW646.js";
5
+ import "./chunk-M73HCQD2.js";
6
+ import "./chunk-VIACN2RU.js";
7
+ import "./chunk-MZO5QKUR.js";
10
8
 
11
9
  // src/checkCmd.ts
12
10
  import chalk from "chalk";
@@ -15,7 +13,7 @@ async function runCheckCommand(opts = {}) {
15
13
  const cpuUsage = data.cpu.usage;
16
14
  const tempC = data.cpu.temperature ?? data.thermal.temperatures?.cpu_die;
17
15
  const memUsage = data.memory.usagePercent;
18
- const disk = data.disk.mainVolume;
16
+ const disk = data.disk && data.disk.length > 0 ? data.disk[0] : null;
19
17
  const sentences = [];
20
18
  let isWarning = false;
21
19
  const tempStr = tempC !== void 0 ? `${Math.round(tempC)}\xB0C` : null;
@@ -53,10 +51,10 @@ async function runCheckCommand(opts = {}) {
53
51
  sentences.push(`Memory at ${Math.round(memUsage)}% used.`);
54
52
  }
55
53
  if (disk) {
56
- const freeBytes = disk.free;
57
- const freeStr = formatBytes(freeBytes);
58
- sentences.push(`${freeStr} free on ${disk.mount}.`);
59
- if (disk.usePercent >= 90) {
54
+ const freeStr = disk.available;
55
+ sentences.push(`${freeStr} free on ${disk.mountpoint}.`);
56
+ const cap = parseInt(disk.capacity, 10);
57
+ if (!isNaN(cap) && cap >= 90) {
60
58
  isWarning = true;
61
59
  }
62
60
  }
@@ -2,8 +2,6 @@
2
2
  import fs from "fs";
3
3
  import os from "os";
4
4
  import path from "path";
5
- var CONFIG_DIR = path.join(os.homedir(), ".config", "pyre");
6
- var CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
7
5
  var DEFAULT_CONFIG = {
8
6
  theme: "default",
9
7
  interval: 2,
@@ -29,6 +27,14 @@ var DEFAULT_CONFIG = {
29
27
  splashAnimation: "classic",
30
28
  webhookUrl: "",
31
29
  alertCmd: "",
30
+ slackAlertUrl: "",
31
+ discordAlertUrl: "",
32
+ pushoverToken: "",
33
+ pushoverUser: "",
34
+ ntfyUrl: "",
35
+ aiBackend: "builtin",
36
+ aiModel: "expert-rules-v1",
37
+ aiApiKey: "",
32
38
  dockerModeConfirmed: false,
33
39
  visiblePanels: {
34
40
  cpu: true,
@@ -48,10 +54,23 @@ var DEFAULT_CONFIG = {
48
54
  },
49
55
  panelLayout: ["mem", "disk", "net"]
50
56
  };
57
+ function getConfigDir() {
58
+ return process.env.PYRE_CONFIG_DIR || path.join(os.homedir(), ".config", "pyre");
59
+ }
60
+ function getConfigFile() {
61
+ return path.join(getConfigDir(), "config.json");
62
+ }
63
+ function getProfilesDir() {
64
+ return path.join(getConfigDir(), "profiles");
65
+ }
66
+ var CONFIG_DIR = getConfigDir();
67
+ var CONFIG_FILE = getConfigFile();
68
+ var PROFILES_DIR = getProfilesDir();
51
69
  function readConfig() {
52
70
  try {
53
- if (!fs.existsSync(CONFIG_FILE)) return { ...DEFAULT_CONFIG };
54
- const raw = fs.readFileSync(CONFIG_FILE, "utf-8");
71
+ const file = getConfigFile();
72
+ if (!fs.existsSync(file)) return { ...DEFAULT_CONFIG };
73
+ const raw = fs.readFileSync(file, "utf-8");
55
74
  const parsed = JSON.parse(raw);
56
75
  return deepMerge(DEFAULT_CONFIG, parsed);
57
76
  } catch {
@@ -60,28 +79,31 @@ function readConfig() {
60
79
  }
61
80
  function writeConfig(config) {
62
81
  try {
63
- if (!fs.existsSync(CONFIG_DIR)) {
64
- fs.mkdirSync(CONFIG_DIR, { recursive: true });
82
+ const dir = getConfigDir();
83
+ const file = getConfigFile();
84
+ if (!fs.existsSync(dir)) {
85
+ fs.mkdirSync(dir, { recursive: true });
65
86
  }
66
87
  const merged = deepMerge(readConfig(), config);
67
- fs.writeFileSync(CONFIG_FILE, JSON.stringify(merged, null, 2) + "\n");
88
+ fs.writeFileSync(file, JSON.stringify(merged, null, 2) + "\n");
68
89
  } catch {
69
90
  }
70
91
  }
71
- var PROFILES_DIR = path.join(CONFIG_DIR, "profiles");
72
92
  function saveProfile(name) {
73
93
  if (!name || name.includes("/") || name.includes("\\")) {
74
94
  throw new Error("Invalid profile name.");
75
95
  }
76
- if (!fs.existsSync(PROFILES_DIR)) {
77
- fs.mkdirSync(PROFILES_DIR, { recursive: true });
96
+ const profilesDir = getProfilesDir();
97
+ if (!fs.existsSync(profilesDir)) {
98
+ fs.mkdirSync(profilesDir, { recursive: true });
78
99
  }
79
100
  const currentConfig = readConfig();
80
- const profileFile = path.join(PROFILES_DIR, `${name}.json`);
101
+ const profileFile = path.join(profilesDir, `${name}.json`);
81
102
  fs.writeFileSync(profileFile, JSON.stringify(currentConfig, null, 2) + "\n");
82
103
  }
83
104
  function loadProfile(name) {
84
- const profileFile = path.join(PROFILES_DIR, `${name}.json`);
105
+ const profilesDir = getProfilesDir();
106
+ const profileFile = path.join(profilesDir, `${name}.json`);
85
107
  if (!fs.existsSync(profileFile)) {
86
108
  throw new Error(`Profile '${name}' does not exist.`);
87
109
  }
@@ -90,8 +112,9 @@ function loadProfile(name) {
90
112
  writeConfig(parsed);
91
113
  }
92
114
  function listProfiles() {
93
- if (!fs.existsSync(PROFILES_DIR)) return [];
94
- const files = fs.readdirSync(PROFILES_DIR);
115
+ const profilesDir = getProfilesDir();
116
+ if (!fs.existsSync(profilesDir)) return [];
117
+ const files = fs.readdirSync(profilesDir);
95
118
  return files.filter((f) => f.endsWith(".json")).map((f) => f.replace(/\.json$/, ""));
96
119
  }
97
120
  function deepMerge(target, source) {
@@ -108,15 +131,19 @@ function deepMerge(target, source) {
108
131
  return result;
109
132
  }
110
133
  function getConfigPath() {
111
- return CONFIG_FILE;
134
+ return getConfigFile();
112
135
  }
113
136
 
114
137
  export {
115
- CONFIG_FILE,
116
138
  DEFAULT_CONFIG,
139
+ getConfigDir,
140
+ getConfigFile,
141
+ getProfilesDir,
142
+ CONFIG_DIR,
143
+ CONFIG_FILE,
144
+ PROFILES_DIR,
117
145
  readConfig,
118
146
  writeConfig,
119
- PROFILES_DIR,
120
147
  saveProfile,
121
148
  loadProfile,
122
149
  listProfiles,
@@ -1,3 +1,7 @@
1
+ import {
2
+ explainAnomaliesLocally
3
+ } from "./chunk-ZLVIVFSO.js";
4
+
1
5
  // src/formatters/render.ts
2
6
  import chalk2 from "chalk";
3
7
 
@@ -447,8 +451,17 @@ function batteryCard(data, contentWidth) {
447
451
  if (data.battery.estimatedTimeToEmpty && data.battery.state !== "charged") {
448
452
  lines.push(statRow("Est. Empty", data.battery.estimatedTimeToEmpty));
449
453
  }
450
- if (data.battery.dischargeRatePerHour !== void 0 && data.battery.state !== "charged") {
451
- lines.push(statRow("Disch. Rate", `${data.battery.dischargeRatePerHour}%/h`));
454
+ if (data.battery.powerWatts !== void 0 && data.battery.powerWatts > 0 && data.battery.state !== "charged") {
455
+ lines.push(statRow("Power Draw", `${data.battery.powerWatts.toFixed(2)} W`));
456
+ }
457
+ if (data.battery.smartPrediction) {
458
+ const sp = data.battery.smartPrediction;
459
+ lines.push("");
460
+ lines.push(chalk2.bold("SMART RUNTIME FORECAST:"));
461
+ lines.push(statRow("Light Load", `${chalk2.green(sp.scenarios.idle.timeRemaining)} (~${sp.scenarios.idle.ratePerHour}%/h)`));
462
+ lines.push(statRow("Current Load", `${chalk2.yellow(sp.scenarios.current.timeRemaining)} (~${sp.scenarios.current.ratePerHour}%/h)`));
463
+ lines.push(statRow("Heavy Load", `${chalk2.red(sp.scenarios.heavy.timeRemaining)} (~${sp.scenarios.heavy.ratePerHour}%/h)`));
464
+ lines.push(chalk2.dim(`Advice: ${sp.recommendation}`));
452
465
  }
453
466
  return lines;
454
467
  }
@@ -783,13 +796,8 @@ function activeDetailLines(data, activePanel, width, opts = {}) {
783
796
  }
784
797
  case "power": {
785
798
  const power = powerCard(data);
786
- if (!power && !data.battery) return ["No power data available"];
787
- const lines = [];
788
- if (power) lines.push(...power);
789
- if (data.battery?.powerWatts !== void 0 && data.battery.powerWatts > 0) {
790
- lines.push(statRow("Battery", `${data.battery.powerWatts.toFixed(2)} W`));
791
- }
792
- return lines.length ? lines : ["No power data available"];
799
+ if (!power) return ["No power data available"];
800
+ return power;
793
801
  }
794
802
  case "battery":
795
803
  return data.battery ? batteryCard(data, contentWidth) : ["No battery data available"];
@@ -885,13 +893,31 @@ function activeDetailLines(data, activePanel, width, opts = {}) {
885
893
  if (!opts.anomalyHistory || opts.anomalyHistory.length === 0) return ["No anomalies detected yet."];
886
894
  const lines = [];
887
895
  const sorted = [...opts.anomalyHistory].sort((a, b) => (b.timestamp?.getTime() || 0) - (a.timestamp?.getTime() || 0));
888
- for (const a of sorted) {
896
+ for (const a of sorted.slice(0, 10)) {
889
897
  const color = a.severity === "critical" ? chalk2.red.bold : chalk2.yellow;
890
898
  const direction = a.zScore > 0 ? "\u2191 spike" : "\u2193 drop";
891
899
  const valStr = a.metric.toLowerCase().includes("net") ? `${formatBytes(a.value)}/s` : `${a.value.toFixed(1)}%`;
892
900
  const timeStr = a.timestamp ? a.timestamp.toLocaleTimeString() : "";
893
901
  lines.push(color(` ${timeStr.padEnd(10)} \u26A0 ${a.metric.padEnd(8)}: ${valStr.padEnd(12)} (${direction}, \u03C3=${a.zScore.toFixed(1)})`));
894
902
  }
903
+ lines.push("");
904
+ const modelId = opts.aiModel || "expert-rules-v1";
905
+ const aiAnalysis = explainAnomaliesLocally(sorted, modelId);
906
+ for (const al of aiAnalysis.split("\n")) {
907
+ if (al.startsWith("Diagnosis Engine:")) {
908
+ lines.push(chalk2.bold.magenta(al));
909
+ } else if (al.startsWith("Tip:")) {
910
+ lines.push(chalk2.cyan.bold(al));
911
+ } else if (al.startsWith(" \u2022 Recommendation:")) {
912
+ lines.push(chalk2.green(al));
913
+ } else if (al.startsWith(" \u2022 Cause:")) {
914
+ lines.push(chalk2.yellow(al));
915
+ } else if (al.startsWith(" \u2022 Verdict:")) {
916
+ lines.push(chalk2.dim(al));
917
+ } else {
918
+ lines.push(chalk2.dim(al));
919
+ }
920
+ }
895
921
  return lines;
896
922
  }
897
923
  case "containers": {