pyre-cli 9.5.2 → 10.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 +73 -16
- package/dist/alertChannels-TPSQIJ7S.js +8 -0
- package/dist/{blender-7UT5J7IP.js → blender-FGH3ASEQ.js} +2 -2
- package/dist/brewHealth-P2GW5Y5C.js +176 -0
- package/dist/buildCmd-CDPZ2ZFA.js +38 -0
- package/dist/{checkCmd-T7O4KXXA.js → checkCmd-RKW6QGDR.js} +9 -11
- package/dist/{chunk-CXL4GYGQ.js → chunk-53O2RGSL.js} +44 -17
- package/dist/{chunk-3TE7MJLS.js → chunk-6UHZP4QP.js} +36 -10
- package/dist/chunk-GHRDW646.js +2506 -0
- package/dist/{prometheus-LFYHF6VO.js → chunk-HT4VQ4OH.js} +1 -0
- package/dist/chunk-M73HCQD2.js +95 -0
- package/dist/chunk-MZO5QKUR.js +32 -0
- package/dist/{chunk-25I533OF.js → chunk-VIACN2RU.js} +1 -1
- package/dist/chunk-Z2YV5HKV.js +87 -0
- package/dist/chunk-ZLVIVFSO.js +110 -0
- package/dist/{collectors-JB4OGUEF.js → collectors-H43PN4PS.js} +6 -3
- package/dist/{completions-VQMHQCXE.js → completions-JKYEJNGR.js} +34 -1
- package/dist/{config-KS5ZZ3VN.js → config-GYGJXU2Q.js} +9 -1
- package/dist/doctor-GYHFTNZQ.js +387 -0
- package/dist/explainCmd-MF6PIMLW.js +222 -0
- package/dist/extensions-XJGHG7X3.js +134 -0
- package/dist/index.js +460 -88
- package/dist/ioreport.py +72 -0
- package/dist/netUsageCmd-WKBTW2YG.js +90 -0
- package/dist/{pipeCmd-TYLJHQ5B.js → pipeCmd-7REDBLBU.js} +4 -3
- package/dist/profileProcCmd-N4H6MCBT.js +107 -0
- package/dist/prometheus-5BL5PVLC.js +6 -0
- package/dist/prometheusCmd-PL64BLN5.js +93 -0
- package/dist/replayCmd-Z3PYXY7V.js +164 -0
- package/dist/run-HOGQDDTA.js +8 -0
- package/dist/serveCmd-2H5VLF3P.js +153 -0
- package/dist/service-Q74BBILO.js +181 -0
- package/dist/smartCmd-ODWKSFNG.js +166 -0
- package/dist/syncCmd-A2W3QB3S.js +126 -0
- package/dist/topoCmd-DU2HHZNH.js +148 -0
- package/dist/watchdogCmd-UF53JPZB.js +161 -0
- package/dist/{wizard-Z3RGA3VB.js → wizard-TD7BGX6O.js} +1 -1
- package/install.ps1 +29 -0
- package/install.sh +10 -9
- package/package.json +7 -3
- package/src/ai/models.ts +138 -0
- package/src/alertChannels.ts +119 -0
- package/src/brewHealth.ts +157 -40
- package/src/buildCmd.ts +48 -0
- package/src/checkCmd.ts +5 -5
- package/src/completions.ts +35 -0
- package/src/doctor.ts +235 -7
- package/src/easy_features.test.ts +40 -0
- package/src/explainCmd.ts +261 -0
- package/src/extensions.ts +71 -7
- package/src/formatters/render.ts +34 -10
- package/src/formatters/types.ts +2 -0
- package/src/index.ts +251 -36
- package/src/live/input.ts +60 -0
- package/src/live/render.ts +67 -0
- package/src/live/state.ts +5 -0
- package/src/live/types.ts +1 -1
- package/src/monitors/batteryPredictor.ts +143 -0
- package/src/monitors/buildTracker.ts +121 -0
- package/src/monitors/collectors.ts +342 -178
- package/src/monitors/ioreport.py +72 -0
- package/src/monitors/platform/linux.ts +756 -0
- package/src/monitors/platform/windows.ts +519 -0
- package/src/monitors/run.ts +27 -3
- package/src/monitors/smc.ts +21 -1
- package/src/monitors/types.ts +15 -0
- package/src/netUsageCmd.ts +121 -0
- package/src/platform.test.ts +99 -0
- package/src/plugins/index.ts +143 -0
- package/src/profileProcCmd.ts +133 -0
- package/src/prometheusCmd.ts +107 -0
- package/src/replayCmd.ts +186 -0
- package/src/serveCmd.ts +180 -0
- package/src/service.ts +125 -7
- package/src/smartCmd.ts +205 -0
- package/src/state/config.ts +58 -17
- package/src/syncCmd.ts +166 -0
- package/src/topoCmd.ts +176 -0
- package/src/watchdogCmd.ts +198 -0
- package/src/xbar.ts +22 -11
- package/dist/brewHealth-F2BXF5M6.js +0 -74
- package/dist/chunk-FH5WDJBF.js +0 -15
- package/dist/chunk-N4WJVF5M.js +0 -1184
- package/dist/doctor-UV7TWIHX.js +0 -184
- package/dist/extensions-DVA5YLGH.js +0 -74
- package/dist/service-WTS3NBQU.js +0 -73
package/README.md
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
# pyre
|
|
2
2
|
|
|
3
|
-
|
|
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
|

|
|
6
|
-

|
|
7
|
+

|
|
8
|
+

|
|
7
9
|

|
|
8
10
|

|
|
9
11
|
|
|
10
|
-
> **Note:** things may not work as intended if your firewall settings do not allow inbound connections for
|
|
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
|
-
- **
|
|
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
|
-
|
|
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 #
|
|
464
|
-
│ ├── doctor.ts # System diagnostics (
|
|
465
|
-
│ ├── extensions.ts #
|
|
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 #
|
|
472
|
-
│ │
|
|
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,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-
|
|
8
|
-
import "./chunk-
|
|
9
|
-
import "./chunk-
|
|
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.
|
|
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
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
if (
|
|
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
|
-
|
|
54
|
-
|
|
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
|
-
|
|
64
|
-
|
|
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(
|
|
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
|
-
|
|
77
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
94
|
-
|
|
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
|
|
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.
|
|
451
|
-
lines.push(statRow("
|
|
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
|
|
787
|
-
|
|
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": {
|