pyre-cli 10.2.0 → 10.4.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 (88) hide show
  1. package/README.md +75 -18
  2. package/dist/{alertChannels-TPSQIJ7S.js → alertChannels-MNX4XH6J.js} +1 -0
  3. package/dist/{anomaliesCmd-25OFKYPK.js → anomaliesCmd-HHTY5LYL.js} +2 -0
  4. package/dist/benchGitCmd-HIXUC5EK.js +87 -0
  5. package/dist/{benchmarkCmd-A3ETK6VY.js → benchmarkCmd-B7DLP2VC.js} +2 -0
  6. package/dist/{blender-FGH3ASEQ.js → blender-6ZX5SPYC.js} +3 -1
  7. package/dist/{brewHealth-P2GW5Y5C.js → brewHealth-VFW33QB3.js} +1 -0
  8. package/dist/budgetCmd-TGO3RYLN.js +11 -0
  9. package/dist/{buildCmd-CDPZ2ZFA.js → buildCmd-E7KUCFYG.js} +3 -1
  10. package/dist/{checkCmd-WQAHAUWO.js → checkCmd-QLYTP77Y.js} +5 -3
  11. package/dist/chunk-3RG5ZIWI.js +10 -0
  12. package/dist/{chunk-VIACN2RU.js → chunk-FYCBGUTW.js} +41 -19
  13. package/dist/chunk-JH7P6KLN.js +143 -0
  14. package/dist/{chunk-QM5L43EK.js → chunk-LDJB5V4N.js} +5 -1
  15. package/dist/chunk-NMGQVRCA.js +111 -0
  16. package/dist/chunk-OCL7P7N7.js +936 -0
  17. package/dist/{chunk-23YWCI3O.js → chunk-QEH4LR6P.js} +28 -686
  18. package/dist/{chunk-KVUMOA6C.js → chunk-RKPGPVTO.js} +77 -4
  19. package/dist/chunk-UF3XQ3R3.js +3800 -0
  20. package/dist/{collectors-JDN5Y7EV.js → collectors-V7SCIIDH.js} +5 -3
  21. package/dist/{completions-ZVTNE52B.js → completions-FTPXDSR2.js} +44 -3
  22. package/dist/{config-J4YMADEF.js → config-5WW5M63G.js} +2 -1
  23. package/dist/containersCmd-FSMYOBG2.js +84 -0
  24. package/dist/{doctor-LHFW4UJP.js → doctor-O4DIAMHK.js} +3 -2
  25. package/dist/{explainCmd-MF6PIMLW.js → explainCmd-7PBPA7O5.js} +1 -0
  26. package/dist/{extensions-XJGHG7X3.js → extensions-QQEV7U3D.js} +1 -0
  27. package/dist/{firebaseSync-ON3RZ6KX.js → firebaseSync-LRN67UTV.js} +2 -0
  28. package/dist/flightRecorder-GJIAQOXC.js +162 -0
  29. package/dist/index.js +737 -4125
  30. package/dist/inspectCmd-5FXJBJTK.js +137 -0
  31. package/dist/menu-AJFGDLBP.js +1233 -0
  32. package/dist/monitors/platform/windows-ui.ps1 +31 -29
  33. package/dist/{netUsageCmd-67IBZZYN.js → netUsageCmd-JP6YKBPF.js} +7 -5
  34. package/dist/optimizeCmd-WZKLFYZU.js +1110 -0
  35. package/dist/{pipeCmd-2N4JPKYE.js → pipeCmd-CPK6FVKB.js} +5 -3
  36. package/dist/playbookCmd-FRROIEXR.js +145 -0
  37. package/dist/{profileProcCmd-N4H6MCBT.js → profileProcCmd-5Y3733E7.js} +64 -13
  38. package/dist/{prometheus-5BL5PVLC.js → prometheus-CYNUR3QT.js} +1 -0
  39. package/dist/{prometheusCmd-LEUM5WCZ.js → prometheusCmd-G2CSEZRQ.js} +5 -3
  40. package/dist/{replayCmd-Z3PYXY7V.js → replayCmd-ODB5SPOQ.js} +25 -6
  41. package/dist/{run-HOGQDDTA.js → run-UJQYT2TO.js} +1 -0
  42. package/dist/{serveCmd-L7LVJOZN.js → serveCmd-KDPWLVQI.js} +5 -3
  43. package/dist/{service-7GIX3YNC.js → service-LIYYR3UI.js} +1 -0
  44. package/dist/{smartCmd-ODWKSFNG.js → smartCmd-2BJJ37GV.js} +130 -5
  45. package/dist/statuslineCmd-EUKYRR2G.js +75 -0
  46. package/dist/{stressCmd-MFXMFXYX.js → stressCmd-GKHGY26X.js} +2 -0
  47. package/dist/{syncCmd-42IRF633.js → syncCmd-MIBFKX24.js} +16 -5
  48. package/dist/topoCmd-RTXQJ6XA.js +189 -0
  49. package/dist/{updateCheck-52X6LJG5.js → updateCheck-LJHEMG3G.js} +2 -0
  50. package/dist/{watchdogCmd-6GR4UN6B.js → watchdogCmd-XM6OPTKM.js} +41 -6
  51. package/dist/{wizard-JQMQWFAW.js → wizard-PRDOJROQ.js} +2 -1
  52. package/package.json +2 -2
  53. package/src/benchGitCmd.ts +110 -0
  54. package/src/budgetCmd.ts +199 -0
  55. package/src/completions.ts +42 -3
  56. package/src/containersCmd.ts +106 -0
  57. package/src/doctor.ts +86 -6
  58. package/src/easy_features.test.ts +83 -0
  59. package/src/flightRecorder.ts +214 -0
  60. package/src/index.ts +183 -9
  61. package/src/inspectCmd.ts +165 -0
  62. package/src/live/input.ts +70 -3
  63. package/src/live/render.ts +1 -1
  64. package/src/live/state.ts +2 -1
  65. package/src/menu/commands.ts +754 -0
  66. package/src/menu/index.ts +358 -0
  67. package/src/menu/render.ts +272 -0
  68. package/src/menu/types.ts +63 -0
  69. package/src/menu.test.ts +110 -0
  70. package/src/monitors/blender.ts +49 -19
  71. package/src/monitors/buildTracker.ts +72 -54
  72. package/src/monitors/platform/windows-ui.ps1 +31 -29
  73. package/src/monitors/platform/windows.ts +613 -280
  74. package/src/optimizeCmd.ts +1248 -0
  75. package/src/platform.test.ts +135 -0
  76. package/src/playbookCmd.ts +156 -0
  77. package/src/power_features.test.ts +251 -0
  78. package/src/profileProcCmd.ts +68 -21
  79. package/src/replayCmd.ts +15 -6
  80. package/src/smartCmd.ts +149 -10
  81. package/src/state/config.ts +13 -0
  82. package/src/statuslineCmd.ts +87 -0
  83. package/src/syncCmd.ts +12 -0
  84. package/src/topoCmd.ts +83 -50
  85. package/src/watchdogCmd.ts +40 -3
  86. package/src/xbar.ts +195 -17
  87. package/dist/chunk-M73HCQD2.js +0 -95
  88. package/dist/topoCmd-DZ3YS5IL.js +0 -148
package/README.md CHANGED
@@ -13,7 +13,7 @@
13
13
 
14
14
  <p align="center">
15
15
  <a href="https://www.npmjs.com/package/pyre-cli"><img src="https://img.shields.io/npm/v/pyre-cli.svg?style=flat" alt="npm version" /></a>
16
- <a href="package.json"><img src="https://img.shields.io/badge/version-10.1.0-blue?style=flat" alt="Version" /></a>
16
+ <a href="package.json"><img src="https://img.shields.io/badge/version-10.2.0-blue?style=flat" alt="Version" /></a>
17
17
  <a href="#requirements"><img src="https://img.shields.io/badge/platforms-macOS%20%7C%20Linux%20%7C%20Windows-lightgrey?style=flat" alt="Platforms" /></a>
18
18
  <a href="#requirements"><img src="https://img.shields.io/badge/node-%3E%3D18.0.0-green?style=flat" alt="Node" /></a>
19
19
  <a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-green?style=flat" alt="License" /></a>
@@ -107,6 +107,7 @@ Launch with `pyre live` for a responsive, full-terminal dashboard featuring:
107
107
  | **Fleet & SSH Aggregation** | Monitor remote nodes over SSH (`pyre ssh <host>`) or aggregate hundreds of servers into an auto-tiled overview (`pyre fleet`). |
108
108
  | **Socket & Packet Forensics** | Track socket connection states (`ESTABLISHED`, `LISTENING`, `TIME_WAIT`), top remote hosts, and per-process network bandwidth. |
109
109
  | **AI Anomaly Explanation** | Explain performance spikes and thermal throttles in plain English using local Ollama models or OpenAI (`pyre explain`). |
110
+ | **macOS System Optimizer** | Reclaim memory and disk space: quit idle/forgotten apps, flush DNS, purge inactive RAM, clean developer caches, empty trash, and audit broken startup agents (`pyre optimize`). |
110
111
  | **Multi-Format Export** | Export instant snapshots to JSON, CSV, TSV, HTML, or Markdown reports, plus an importable Grafana dashboard template. |
111
112
 
112
113
  ---
@@ -144,19 +145,19 @@ pyre web --lan --port 3000
144
145
 
145
146
  ## Comparison
146
147
 
147
- | Feature | Pyre | btop | htop | Glances |
148
- |---|:---:|:---:|:---:|:---:|
149
- | **Interactive TUI & Sparklines** | Yes (6 themes + JSON) | Yes | Limited | Yes |
150
- | **Native macOS Desktop Window** | Yes (Swift Cocoa) | No | No | No |
151
- | **Apple Silicon P/E + GPU + ANE** | Yes (Granular watts) | Partial | Partial | Partial |
152
- | **Encrypted P2P Telemetry** | Yes (TLS + HMAC) | No | No | Partial (Plain) |
153
- | **Real-time Web Dashboard (SSE)** | Yes (Zero-config) | No | No | Yes |
154
- | **Multi-Host Fleet Aggregation** | Yes (SSH / P2P) | No | No | Yes (Web only) |
155
- | **Socket Connection Forensics** | Yes | Partial | No | Partial |
156
- | **Statistical Anomaly Radar** | Yes (Z-score) | No | No | No |
157
- | **Energy & Cost Profiler** | Yes (kWh / Joules) | No | No | No |
158
- | **System Security Doctor** | Yes (SIP / Sensors) | No | No | No |
159
- | **Export Formats** | JSON, CSV, TSV, HTML, MD | No | No | InfluxDB, CSV |
148
+ | Feature | Pyre | btop | macmon | mactop | asitop | htop | Glances |
149
+ |---|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
150
+ | **Interactive TUI & Sparklines** | Yes (6 themes + JSON) | Yes | Yes (Glanceable) | Yes | Yes | Limited | Yes |
151
+ | **Native macOS Desktop Window** | Yes (Swift Cocoa) | No | No | No | No | No | No |
152
+ | **Apple Silicon P/E + GPU + ANE** | Yes (Granular watts) | Partial | Yes (IOReport watts) | Yes (Powermetrics) | Yes (Powermetrics) | Partial | Partial |
153
+ | **Encrypted P2P Telemetry** | Yes (TLS + HMAC) | No | No | No | No | No | Partial (Plain) |
154
+ | **Real-time Web Dashboard (SSE)** | Yes (Zero-config) | No | No | No | No | No | Yes |
155
+ | **Multi-Host Fleet Aggregation** | Yes (SSH / P2P) | No | No | No | No | No | Yes (Web only) |
156
+ | **Socket Connection Forensics** | Yes | Partial | No | No | No | No | Partial |
157
+ | **Statistical Anomaly Radar** | Yes (Z-score) | No | No | No | No | No | No |
158
+ | **Energy & Cost Profiler** | Yes (kWh / Joules) | No | No | No | No | No | No |
159
+ | **System Security Doctor** | Yes (SIP / Sensors) | No | No | No | No | No | No |
160
+ | **Export Formats** | JSON, CSV, TSV, HTML, MD | No | None | None | None | No | InfluxDB, CSV |
160
161
 
161
162
  ---
162
163
 
@@ -213,6 +214,15 @@ pyre brew # Package manager health check & disk cache repo
213
214
  pyre smart # S.M.A.R.T. disk health monitor & drive wear
214
215
  pyre blender # Background Blender 3D render job tracker
215
216
  pyre pipe # Continuous newline-delimited JSON stream
217
+ pyre stress # Synthetic CPU/GPU load generator
218
+ pyre extensions # System Extensions and Driver inspector
219
+ pyre info # Hardware summary including battery health & displays
220
+ pyre explain # Plain-English anomaly explanation (Ollama/OpenAI/builtin)
221
+ pyre diff <file1> <file2> # Compare two saved snapshot files side-by-side
222
+ pyre history # Graph historical resource trends from CSV logs
223
+ pyre replay <file> # Play back a pyre CSV log file in the TUI
224
+ pyre build # Active compiler and build system tracker
225
+ pyre topo # Network topology mapper and pyre peer auto-discovery
216
226
  ```
217
227
 
218
228
  ### Automation, Profiles & Daemon
@@ -220,10 +230,18 @@ pyre pipe # Continuous newline-delimited JSON stream
220
230
  ```bash
221
231
  pyre watchdog # Autonomous daemon monitoring rules & killing rogue procs
222
232
  pyre serve [--port 8080] # Lightweight REST API server exposing JSON endpoints
223
- pyre profile save <name> # Save active configuration profile
224
- pyre profile load <name> # Restore saved profile
225
- pyre config show # Display current settings (~/.config/pyre/config.json)
226
- pyre completions <zsh|bash> # Generate shell auto-completions
233
+ pyre profile <save|load|list> # Atomic configuration profile management
234
+ pyre config <show|reset> # View or reset persistent configuration file
235
+ pyre completions <shell> # Generate shell auto-completions (zsh, bash, fish, powershell)
236
+ pyre prometheus # Start a Prometheus metrics exporter on localhost
237
+ pyre p2p <server|connect> # Start a P2P server or connect to one
238
+ pyre server # Print one-line helper commands for P2P pairing
239
+ pyre alert test # Send a test notification to all configured alert channels
240
+ pyre profile-proc <pid> # CPU call-tree profiler and hot function sampler
241
+ pyre sync <push|pull|status> # Synchronize config profiles across machines
242
+ pyre xbar # Generate an xbar / SwiftBar menu bar plugin script
243
+ pyre optimize [subcommand] # macOS optimizer: quit idle apps, memory, caches, trash, startup, dns
244
+ pyre update # Check for pyre-cli updates
227
245
  ```
228
246
 
229
247
  ---
@@ -248,6 +266,45 @@ Security and networking capabilities:
248
266
 
249
267
  ---
250
268
 
269
+ ## macOS System Optimizer (`pyre optimize`)
270
+
271
+ Active memory, disk, and application lifecycle optimization for macOS:
272
+
273
+ ```bash
274
+ # 1. System health & optimization overview report (safe, dry-run by default)
275
+ pyre optimize
276
+
277
+ # 2. Find and gracefully quit apps open but unused recently
278
+ pyre optimize apps # Interactive prompt with memory & CPU summary
279
+ pyre optimize apps --idle 30m --keep 3 # Custom idle duration & recent apps to keep
280
+ pyre optimize apps --dry-run # Preview without quitting
281
+
282
+ # 3. Background app activity tracker (records precise frontmost app usage)
283
+ pyre optimize track # Run tracker in current terminal
284
+ pyre optimize track --install # Install lightweight background launchd agent
285
+ pyre optimize track --auto-quit 2h # Daemon auto-quits idle apps & sends desktop alert
286
+ pyre optimize track --uninstall # Remove background tracker
287
+
288
+ # 4. Memory pressure & inactive purge
289
+ pyre optimize memory # Shows pressure, swap, compressed RAM, top consumers
290
+ pyre optimize memory -y # Flushes inactive file cache via `sudo purge`
291
+
292
+ # 5. Developer & system caches
293
+ pyre optimize caches # Measure Xcode DerivedData, Homebrew, npm/yarn/pnpm
294
+ pyre optimize caches --clean # Clean selected caches (safely skips running apps)
295
+
296
+ # 6. Trash & startup maintenance
297
+ pyre optimize trash --empty # Measure and empty macOS Trash
298
+ pyre optimize startup # Audit Login Items & LaunchAgents, flag broken leftovers
299
+ pyre optimize dns # Flush macOS DNS cache via dscacheutil & mDNSResponder
300
+
301
+ # 7. Protect whitelist
302
+ pyre optimize protect "Spotify" # Whitelist app so it is never automatically quit
303
+ pyre optimize unprotect "Spotify" # Remove app from custom whitelist
304
+ ```
305
+
306
+ ---
307
+
251
308
  ## Requirements
252
309
 
253
310
  | Platform | Minimum Requirement | Notes |
@@ -2,6 +2,7 @@ import {
2
2
  sendAlert,
3
3
  sendTestAlert
4
4
  } from "./chunk-Z2YV5HKV.js";
5
+ import "./chunk-3RG5ZIWI.js";
5
6
  export {
6
7
  sendAlert,
7
8
  sendTestAlert
@@ -1,3 +1,5 @@
1
+ import "./chunk-3RG5ZIWI.js";
2
+
1
3
  // src/anomaliesCmd.ts
2
4
  import fs from "fs";
3
5
  import path from "path";
@@ -0,0 +1,87 @@
1
+ import {
2
+ formatBytes,
3
+ runWithBudget
4
+ } from "./chunk-JH7P6KLN.js";
5
+ import "./chunk-3RG5ZIWI.js";
6
+
7
+ // src/benchGitCmd.ts
8
+ import { execSync } from "child_process";
9
+ import chalk from "chalk";
10
+ async function runBenchGitCommand(branchA, branchB, commandStr) {
11
+ try {
12
+ const status = execSync("git status --porcelain", { encoding: "utf-8" }).trim();
13
+ if (status) {
14
+ console.error(chalk.red("\n \u2716 Working directory contains uncommitted changes."));
15
+ console.log(chalk.yellow(" Please commit or stash your changes before running `pyre bench-git`.\n"));
16
+ return;
17
+ }
18
+ } catch (err) {
19
+ console.error(chalk.red(`
20
+ \u2716 Not a git repository: ${err.message}`));
21
+ return;
22
+ }
23
+ const startingBranch = execSync("git rev-parse --abbrev-ref HEAD", { encoding: "utf-8" }).trim();
24
+ console.log(chalk.bold(`
25
+ \u{1F525} pyre bench-git \u2014 Cross-Branch Performance Differ
26
+ `));
27
+ console.log(chalk.dim(` Command: ${commandStr}`));
28
+ console.log(chalk.dim(` Baseline Branch: ${branchA}`));
29
+ console.log(chalk.dim(` Target Branch: ${branchB}`));
30
+ console.log(chalk.dim(` Current Branch: ${startingBranch}
31
+ `));
32
+ const tokens = commandStr.split(/\s+/).filter(Boolean);
33
+ try {
34
+ console.log(chalk.bold.cyan(`\u25B6 [1/2] Checking out '${branchA}' and benchmarking...`));
35
+ execSync(`git checkout ${branchA}`, { stdio: "pipe" });
36
+ const resA = await runWithBudget(tokens, {});
37
+ console.log(chalk.bold.cyan(`
38
+ \u25B6 [2/2] Checking out '${branchB}' and benchmarking...`));
39
+ execSync(`git checkout ${branchB}`, { stdio: "pipe" });
40
+ const resB = await runWithBudget(tokens, {});
41
+ execSync(`git checkout ${startingBranch}`, { stdio: "pipe" });
42
+ console.log(chalk.dim(`
43
+ Restored workspace to '${startingBranch}'.`));
44
+ printGitComparisonTable(branchA, branchB, resA, resB);
45
+ } catch (err) {
46
+ try {
47
+ execSync(`git checkout ${startingBranch}`, { stdio: "pipe" });
48
+ } catch {
49
+ }
50
+ console.error(chalk.red(`
51
+ \u2716 Benchmark failed: ${err.message}`));
52
+ }
53
+ }
54
+ function printGitComparisonTable(branchA, branchB, resA, resB) {
55
+ const diffPct = (valA, valB, invertGood = false) => {
56
+ if (valA === 0) return "0%";
57
+ const delta = (valB - valA) / valA * 100;
58
+ const sign = delta > 0 ? "+" : "";
59
+ const formatted = `${sign}${delta.toFixed(1)}%`;
60
+ const isGood = invertGood ? delta > 0 : delta < 0;
61
+ const color = delta === 0 ? chalk.dim : isGood ? chalk.green : chalk.red;
62
+ return color(formatted);
63
+ };
64
+ console.log(chalk.bold("\n \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
65
+ console.log(chalk.bold(" \u{1F525} Cross-Branch Performance Comparison Results"));
66
+ console.log(chalk.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
67
+ const colBranchA = branchA.padEnd(16);
68
+ const colBranchB = branchB.padEnd(16);
69
+ console.log(` ${"Metric".padEnd(18)} ${colBranchA} ${colBranchB} Delta`);
70
+ console.log(chalk.dim(" \u2500".repeat(30)));
71
+ const durA = `${resA.durationSec.toFixed(2)}s`.padEnd(16);
72
+ const durB = `${resB.durationSec.toFixed(2)}s`.padEnd(16);
73
+ console.log(` ${"Duration".padEnd(18)} ${durA} ${durB} ${diffPct(resA.durationSec, resB.durationSec)}`);
74
+ const ramA = formatBytes(resA.peakMemoryBytes).padEnd(16);
75
+ const ramB = formatBytes(resB.peakMemoryBytes).padEnd(16);
76
+ console.log(` ${"Peak Memory".padEnd(18)} ${ramA} ${ramB} ${diffPct(resA.peakMemoryBytes, resB.peakMemoryBytes)}`);
77
+ const cpuA = `${resA.peakCpuPct.toFixed(1)}%`.padEnd(16);
78
+ const cpuB = `${resB.peakCpuPct.toFixed(1)}%`.padEnd(16);
79
+ console.log(` ${"Peak CPU".padEnd(18)} ${cpuA} ${cpuB} ${diffPct(resA.peakCpuPct, resB.peakCpuPct)}`);
80
+ const codeA = `${resA.exitCode}`.padEnd(16);
81
+ const codeB = `${resB.exitCode}`.padEnd(16);
82
+ console.log(` ${"Exit Code".padEnd(18)} ${codeA} ${codeB}`);
83
+ console.log(chalk.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n"));
84
+ }
85
+ export {
86
+ runBenchGitCommand
87
+ };
@@ -1,3 +1,5 @@
1
+ import "./chunk-3RG5ZIWI.js";
2
+
1
3
  // src/benchmarkCmd.ts
2
4
  import chalk from "chalk";
3
5
  async function runBenchmarkCommand() {
@@ -1,7 +1,9 @@
1
1
  import {
2
2
  collectBlenderRenders
3
- } from "./chunk-VIACN2RU.js";
3
+ } from "./chunk-FYCBGUTW.js";
4
+ import "./chunk-OCL7P7N7.js";
4
5
  import "./chunk-MZO5QKUR.js";
6
+ import "./chunk-3RG5ZIWI.js";
5
7
  export {
6
8
  collectBlenderRenders
7
9
  };
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  run
3
3
  } from "./chunk-MZO5QKUR.js";
4
+ import "./chunk-3RG5ZIWI.js";
4
5
 
5
6
  // src/brewHealth.ts
6
7
  import chalk from "chalk";
@@ -0,0 +1,11 @@
1
+ import {
2
+ formatBytes,
3
+ parseBytesString,
4
+ runWithBudget
5
+ } from "./chunk-JH7P6KLN.js";
6
+ import "./chunk-3RG5ZIWI.js";
7
+ export {
8
+ formatBytes,
9
+ parseBytesString,
10
+ runWithBudget
11
+ };
@@ -1,7 +1,9 @@
1
1
  import {
2
2
  collectBuilds
3
- } from "./chunk-M73HCQD2.js";
3
+ } from "./chunk-NMGQVRCA.js";
4
+ import "./chunk-OCL7P7N7.js";
4
5
  import "./chunk-MZO5QKUR.js";
6
+ import "./chunk-3RG5ZIWI.js";
5
7
 
6
8
  // src/buildCmd.ts
7
9
  import chalk from "chalk";
@@ -1,10 +1,12 @@
1
1
  import "./chunk-IM3IQMMT.js";
2
2
  import {
3
3
  collectAll
4
- } from "./chunk-23YWCI3O.js";
5
- import "./chunk-M73HCQD2.js";
6
- import "./chunk-VIACN2RU.js";
4
+ } from "./chunk-QEH4LR6P.js";
5
+ import "./chunk-NMGQVRCA.js";
6
+ import "./chunk-FYCBGUTW.js";
7
+ import "./chunk-OCL7P7N7.js";
7
8
  import "./chunk-MZO5QKUR.js";
9
+ import "./chunk-3RG5ZIWI.js";
8
10
 
9
11
  // src/checkCmd.ts
10
12
  import chalk from "chalk";
@@ -0,0 +1,10 @@
1
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
2
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
3
+ }) : x)(function(x) {
4
+ if (typeof require !== "undefined") return require.apply(this, arguments);
5
+ throw Error('Dynamic require of "' + x + '" is not supported');
6
+ });
7
+
8
+ export {
9
+ __require
10
+ };
@@ -1,14 +1,20 @@
1
+ import {
2
+ collectWindowsProcesses
3
+ } from "./chunk-OCL7P7N7.js";
1
4
  import {
2
5
  run
3
6
  } from "./chunk-MZO5QKUR.js";
4
7
 
5
8
  // src/monitors/blender.ts
6
- var RENDER_LOG_DIR = "/tmp/blender_render_logs";
9
+ import fs from "fs";
10
+ import os from "os";
11
+ import path from "path";
12
+ var RENDER_LOG_DIR = path.join(os.tmpdir(), "blender_render_logs");
7
13
  function blendFileFromArgs(args) {
8
14
  const nonFlag = args.filter((a) => !a.startsWith("-"));
9
15
  for (const a of nonFlag) {
10
16
  if (a.endsWith(".blend") || a.includes(".blend")) {
11
- return a.split("/").pop() || a;
17
+ return a.split(/[/\\]/).pop() || a;
12
18
  }
13
19
  }
14
20
  return "unknown.blend";
@@ -24,15 +30,17 @@ function outputPathFromArgs(args) {
24
30
  const oIdx = args.indexOf("-o");
25
31
  if (oIdx !== -1 && args[oIdx + 1]) {
26
32
  let p = args[oIdx + 1];
27
- if (p.endsWith("/")) p = p.slice(0, -1);
28
- return p.split("/").slice(0, -1).join("/") || "/tmp";
33
+ if (p.endsWith("/") || p.endsWith("\\")) p = p.slice(0, -1);
34
+ const parts = p.split(/[/\\]/);
35
+ return parts.slice(0, -1).join("/") || os.tmpdir();
29
36
  }
30
37
  const fFlag = args.find((a) => a.startsWith("-o="));
31
38
  if (fFlag) {
32
39
  const p = fFlag.slice(3);
33
- return p.endsWith("/") ? p.slice(0, -1).split("/").slice(0, -1).join("/") || "/tmp" : p.split("/").slice(0, -1).join("/") || "/tmp";
40
+ const parts = p.split(/[/\\]/);
41
+ return parts.slice(0, -1).join("/") || os.tmpdir();
34
42
  }
35
- return "/tmp";
43
+ return os.tmpdir();
36
44
  }
37
45
  function frameRangeFromArgs(args) {
38
46
  const fIdx = args.indexOf("-f");
@@ -54,11 +62,12 @@ function frameRangeFromArgs(args) {
54
62
  async function parseRenderLog(blendName) {
55
63
  const baseName = blendName.replace(/\.blend$/i, "");
56
64
  try {
57
- const entries = (await run(`ls -t "${RENDER_LOG_DIR}" 2>/dev/null | grep "^${baseName}_" | head -5`, "")).trim().split("\n").filter(Boolean);
65
+ if (!fs.existsSync(RENDER_LOG_DIR)) return null;
66
+ const entries = fs.readdirSync(RENDER_LOG_DIR).filter((f) => f.startsWith(`${baseName}_`)).map((f) => ({ name: f, time: fs.statSync(path.join(RENDER_LOG_DIR, f)).mtimeMs })).sort((a, b) => b.time - a.time).slice(0, 5);
58
67
  for (const entry of entries) {
59
- const logPath = `${RENDER_LOG_DIR}/${entry}`;
68
+ const logPath = path.join(RENDER_LOG_DIR, entry.name);
60
69
  try {
61
- const content = await run(`cat "${logPath}" 2>/dev/null`, "", 500);
70
+ const content = fs.readFileSync(logPath, "utf-8");
62
71
  if (!content) continue;
63
72
  const lines = content.split("\n");
64
73
  const rendered = lines.filter((l) => /Fra:(\d+)/i.test(l)).length;
@@ -80,18 +89,31 @@ async function parseRenderLog(blendName) {
80
89
  async function collectBlenderRenders() {
81
90
  const renders = [];
82
91
  try {
83
- const psRaw = await run("ps -eo pid,pcpu,pmem,state,command 2>/dev/null | grep -iE '[b]lender'", "", 2e3);
84
- if (!psRaw) return renders;
85
- const lines = psRaw.split("\n");
86
- for (const line of lines) {
87
- const parts = line.trim().match(/\s*(\d+)\s+([\d.]+)\s+([\d.]+)\s+(\S+)\s+(.+)/);
88
- if (!parts) continue;
89
- const pid = parseInt(parts[1], 10);
90
- const cpuPct = parseFloat(parts[2]);
91
- const command = parts[5];
92
+ const procList = [];
93
+ if (process.platform === "win32") {
94
+ const procs = await collectWindowsProcesses();
95
+ for (const p of procs) {
96
+ if (/blender/i.test(p.command)) {
97
+ procList.push({ pid: p.pid, cpuPct: p.cpu, command: p.command });
98
+ }
99
+ }
100
+ } else {
101
+ const psRaw = await run("ps -eo pid,pcpu,pmem,state,command 2>/dev/null | grep -iE '[b]lender'", "", 2e3);
102
+ if (psRaw) {
103
+ const lines = psRaw.split("\n");
104
+ for (const line of lines) {
105
+ const parts = line.trim().match(/\s*(\d+)\s+([\d.]+)\s+([\d.]+)\s+(\S+)\s+(.+)/);
106
+ if (parts) {
107
+ procList.push({ pid: parseInt(parts[1], 10), cpuPct: parseFloat(parts[2]), command: parts[5] });
108
+ }
109
+ }
110
+ }
111
+ }
112
+ for (const p of procList) {
113
+ const { pid, cpuPct, command } = p;
92
114
  const args = command.split(/\s+/);
93
115
  const isRender = /-b|--background|-f\s/.test(command) || /render/i.test(command);
94
- if (!isRender && !command.includes("blender")) continue;
116
+ if (!isRender && !command.toLowerCase().includes("blender")) continue;
95
117
  const blendFile = blendFileFromArgs(args);
96
118
  const engine = engineFromArgs(args);
97
119
  const outputPath = outputPathFromArgs(args);
@@ -0,0 +1,143 @@
1
+ import {
2
+ __require
3
+ } from "./chunk-3RG5ZIWI.js";
4
+
5
+ // src/budgetCmd.ts
6
+ import fs from "fs";
7
+ import { spawn } from "child_process";
8
+ import chalk from "chalk";
9
+ function parseBytesString(s) {
10
+ const trimmed = s.trim().toUpperCase();
11
+ const num = parseFloat(trimmed);
12
+ if (isNaN(num)) return 0;
13
+ if (trimmed.endsWith("G") || trimmed.endsWith("GB")) return num * 1024 * 1024 * 1024;
14
+ if (trimmed.endsWith("M") || trimmed.endsWith("MB")) return num * 1024 * 1024;
15
+ if (trimmed.endsWith("K") || trimmed.endsWith("KB")) return num * 1024;
16
+ return num;
17
+ }
18
+ function formatBytes(b) {
19
+ if (b >= 1024 * 1024 * 1024) return `${(b / (1024 * 1024 * 1024)).toFixed(2)} GB`;
20
+ if (b >= 1024 * 1024) return `${(b / (1024 * 1024)).toFixed(2)} MB`;
21
+ if (b >= 1024) return `${(b / 1024).toFixed(1)} KB`;
22
+ return `${b} B`;
23
+ }
24
+ async function runWithBudget(commandArgs, budgets, opts) {
25
+ if (commandArgs.length === 0) {
26
+ throw new Error("No command specified. Usage: pyre run [budget flags] -- <command>");
27
+ }
28
+ const cmdStr = commandArgs.join(" ");
29
+ const [bin, ...args] = commandArgs;
30
+ const startTime = Date.now();
31
+ const cpuSamples = [];
32
+ let peakMemBytes = 0;
33
+ let peakCpu = 0;
34
+ const child = spawn(bin, args, {
35
+ stdio: "inherit",
36
+ env: process.env
37
+ });
38
+ const pid = child.pid;
39
+ let sampleInterval = null;
40
+ if (pid) {
41
+ sampleInterval = setInterval(() => {
42
+ try {
43
+ const { execSync } = __require("child_process");
44
+ const output = execSync(`ps -o %cpu,rss -p ${pid} 2>/dev/null`, { encoding: "utf-8" });
45
+ const lines = output.trim().split("\n");
46
+ if (lines.length > 1) {
47
+ const parts = lines[1].trim().split(/\s+/);
48
+ const cpu = parseFloat(parts[0]) || 0;
49
+ const rssKb = parseInt(parts[1], 10) || 0;
50
+ const memBytes = rssKb * 1024;
51
+ cpuSamples.push(cpu);
52
+ if (cpu > peakCpu) peakCpu = cpu;
53
+ if (memBytes > peakMemBytes) peakMemBytes = memBytes;
54
+ }
55
+ } catch {
56
+ }
57
+ }, 100);
58
+ }
59
+ const exitCode = await new Promise((resolve) => {
60
+ child.on("close", (code) => {
61
+ if (sampleInterval) clearInterval(sampleInterval);
62
+ resolve(code ?? 0);
63
+ });
64
+ child.on("error", () => {
65
+ if (sampleInterval) clearInterval(sampleInterval);
66
+ resolve(1);
67
+ });
68
+ });
69
+ const durationSec = (Date.now() - startTime) / 1e3;
70
+ const avgCpu = cpuSamples.length > 0 ? cpuSamples.reduce((a, b) => a + b, 0) / cpuSamples.length : peakCpu;
71
+ const violations = [];
72
+ if (budgets.maxTimeSec !== void 0 && durationSec > budgets.maxTimeSec) {
73
+ violations.push(`Runtime exceeded budget: ${durationSec.toFixed(2)}s > ${budgets.maxTimeSec}s limit`);
74
+ }
75
+ if (budgets.maxMemoryBytes !== void 0 && peakMemBytes > budgets.maxMemoryBytes) {
76
+ violations.push(`Memory exceeded budget: ${formatBytes(peakMemBytes)} > ${formatBytes(budgets.maxMemoryBytes)} limit`);
77
+ }
78
+ if (budgets.maxCpuPct !== void 0 && peakCpu > budgets.maxCpuPct) {
79
+ violations.push(`CPU exceeded budget: ${peakCpu.toFixed(1)}% > ${budgets.maxCpuPct}% limit`);
80
+ }
81
+ const result = {
82
+ command: cmdStr,
83
+ exitCode,
84
+ durationSec,
85
+ peakCpuPct: peakCpu,
86
+ avgCpuPct: avgCpu,
87
+ peakMemoryBytes: peakMemBytes,
88
+ violations
89
+ };
90
+ printBudgetReport(result, budgets, opts);
91
+ return result;
92
+ }
93
+ function printBudgetReport(result, budgets, opts) {
94
+ const isViolated = result.violations.length > 0;
95
+ const isFailed = result.exitCode !== 0 || isViolated;
96
+ console.log(chalk.bold("\n \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
97
+ console.log(chalk.bold(` \u{1F525} pyre run \u2014 Performance Budget Report`));
98
+ console.log(chalk.dim(` Command: ${result.command}`));
99
+ console.log(chalk.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
100
+ const statusTime = budgets.maxTimeSec === void 0 ? chalk.dim("unconstrained") : result.durationSec <= budgets.maxTimeSec ? chalk.green(`\u2714 Pass (<= ${budgets.maxTimeSec}s)`) : chalk.red(`\u2716 FAIL (> ${budgets.maxTimeSec}s)`);
101
+ const statusMem = budgets.maxMemoryBytes === void 0 ? chalk.dim("unconstrained") : result.peakMemoryBytes <= budgets.maxMemoryBytes ? chalk.green(`\u2714 Pass (<= ${formatBytes(budgets.maxMemoryBytes)})`) : chalk.red(`\u2716 FAIL (> ${formatBytes(budgets.maxMemoryBytes)})`);
102
+ const statusCpu = budgets.maxCpuPct === void 0 ? chalk.dim("unconstrained") : result.peakCpuPct <= budgets.maxCpuPct ? chalk.green(`\u2714 Pass (<= ${budgets.maxCpuPct}%)`) : chalk.red(`\u2716 FAIL (> ${budgets.maxCpuPct}%)`);
103
+ console.log(` Duration: ${result.durationSec.toFixed(2)}s`.padEnd(28) + statusTime);
104
+ console.log(` Peak Memory: ${formatBytes(result.peakMemoryBytes)}`.padEnd(28) + statusMem);
105
+ console.log(` Peak CPU: ${result.peakCpuPct.toFixed(1)}% (avg ${result.avgCpuPct.toFixed(1)}%)`.padEnd(28) + statusCpu);
106
+ console.log(` Exit Code: ${result.exitCode === 0 ? chalk.green("0") : chalk.red(result.exitCode)}`);
107
+ if (isViolated) {
108
+ console.log(chalk.bold.red("\n \u2716 Performance Budget Violations:"));
109
+ result.violations.forEach((v) => console.log(chalk.red(` \u2022 ${v}`)));
110
+ }
111
+ console.log(chalk.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n"));
112
+ if (opts?.outFile || opts?.markdown) {
113
+ const md = generateMarkdownBudgetReport(result, budgets);
114
+ if (opts.outFile) {
115
+ fs.writeFileSync(opts.outFile, md, "utf-8");
116
+ console.log(chalk.dim(` Saved report to ${opts.outFile}
117
+ `));
118
+ }
119
+ }
120
+ }
121
+ function generateMarkdownBudgetReport(result, budgets) {
122
+ return [
123
+ `# Pyre Performance Budget Report`,
124
+ ``,
125
+ `**Command**: \`${result.command}\` `,
126
+ `**Status**: ${result.violations.length === 0 && result.exitCode === 0 ? "\u2705 PASSED" : "\u274C FAILED"} `,
127
+ `**Exit Code**: \`${result.exitCode}\` `,
128
+ ``,
129
+ `| Metric | Measured | Limit | Status |`,
130
+ `| :--- | :--- | :--- | :--- |`,
131
+ `| Duration | ${result.durationSec.toFixed(2)}s | ${budgets.maxTimeSec ? budgets.maxTimeSec + "s" : "N/A"} | ${budgets.maxTimeSec && result.durationSec > budgets.maxTimeSec ? "\u274C FAIL" : "\u2705 PASS"} |`,
132
+ `| Peak Memory | ${formatBytes(result.peakMemoryBytes)} | ${budgets.maxMemoryBytes ? formatBytes(budgets.maxMemoryBytes) : "N/A"} | ${budgets.maxMemoryBytes && result.peakMemoryBytes > budgets.maxMemoryBytes ? "\u274C FAIL" : "\u2705 PASS"} |`,
133
+ `| Peak CPU | ${result.peakCpuPct.toFixed(1)}% | ${budgets.maxCpuPct ? budgets.maxCpuPct + "%" : "N/A"} | ${budgets.maxCpuPct && result.peakCpuPct > budgets.maxCpuPct ? "\u274C FAIL" : "\u2705 PASS"} |`,
134
+ ``,
135
+ ...result.violations.length > 0 ? [`### Violations`, ...result.violations.map((v) => `- ${v}`)] : []
136
+ ].join("\n");
137
+ }
138
+
139
+ export {
140
+ parseBytesString,
141
+ formatBytes,
142
+ runWithBudget
143
+ };
@@ -64,7 +64,11 @@ var DEFAULT_CONFIG = {
64
64
  historyDuration: 60,
65
65
  tableDensity: "standard",
66
66
  defaultFilter: "all",
67
- defaultSort: "cpu"
67
+ defaultSort: "cpu",
68
+ optimizeProtectedApps: [],
69
+ optimizeIdleThreshold: "2h",
70
+ optimizeKeepRecent: 5,
71
+ defaultAction: "live"
68
72
  };
69
73
  function getConfigDir() {
70
74
  return process.env.PYRE_CONFIG_DIR || path.join(os.homedir(), ".config", "pyre");