pyre-cli 10.3.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 +41 -0
  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 +736 -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 +181 -8
  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
@@ -3,7 +3,8 @@ import {
3
3
  getProfilesDir,
4
4
  readConfig,
5
5
  writeConfig
6
- } from "./chunk-QM5L43EK.js";
6
+ } from "./chunk-LDJB5V4N.js";
7
+ import "./chunk-3RG5ZIWI.js";
7
8
 
8
9
  // src/syncCmd.ts
9
10
  import fs from "fs";
@@ -13,6 +14,16 @@ import crypto from "crypto";
13
14
  import chalk from "chalk";
14
15
  function getDefaultSyncDir() {
15
16
  if (process.env.PYRE_SYNC_DIR) return process.env.PYRE_SYNC_DIR;
17
+ if (process.platform === "win32") {
18
+ const oneDriveEnv = process.env.OneDrive || process.env.OneDriveConsumer;
19
+ if (oneDriveEnv && fs.existsSync(oneDriveEnv)) {
20
+ return path.join(oneDriveEnv, "PyreSync");
21
+ }
22
+ const userOneDrive = path.join(os.homedir(), "OneDrive");
23
+ if (fs.existsSync(userOneDrive)) {
24
+ return path.join(userOneDrive, "PyreSync");
25
+ }
26
+ }
16
27
  const icloudPath = path.join(os.homedir(), "Library", "Mobile Documents", "com~apple~CloudDocs", "PyreSync");
17
28
  if (fs.existsSync(path.dirname(icloudPath))) {
18
29
  return icloudPath;
@@ -71,7 +82,7 @@ async function runFirebaseSubcommand(action) {
71
82
  console.log(chalk.dim(' Run "pyre sync firebase enable" to activate.\n'));
72
83
  return;
73
84
  }
74
- const { firebaseStatus } = await import("./firebaseSync-ON3RZ6KX.js");
85
+ const { firebaseStatus } = await import("./firebaseSync-LRN67UTV.js");
75
86
  const hosts = await firebaseStatus();
76
87
  if (hosts.length === 0) {
77
88
  console.log(chalk.dim(' No remote hosts found in Firestore. Run "pyre sync push" to upload.\n'));
@@ -102,7 +113,7 @@ async function runSyncCommand(subcommand = "status", opts = {}) {
102
113
  console.log(` Backend: ${chalk.cyan("Firebase Firestore")} (${chalk.dim("dearborn-internal-tool")})`);
103
114
  const hostId = config.firebaseHostId || void 0;
104
115
  if (subcommand === "status") {
105
- const { firebaseStatus } = await import("./firebaseSync-ON3RZ6KX.js");
116
+ const { firebaseStatus } = await import("./firebaseSync-LRN67UTV.js");
106
117
  const hosts = await firebaseStatus();
107
118
  if (hosts.length === 0) {
108
119
  console.log(chalk.dim(' No remote hosts found. Run "pyre sync push" to upload your config.\n'));
@@ -119,7 +130,7 @@ async function runSyncCommand(subcommand = "status", opts = {}) {
119
130
  return;
120
131
  }
121
132
  if (subcommand === "push") {
122
- const { firebasePush } = await import("./firebaseSync-ON3RZ6KX.js");
133
+ const { firebasePush } = await import("./firebaseSync-LRN67UTV.js");
123
134
  const profiles = loadLocalProfiles();
124
135
  const result = await firebasePush(config, profiles, hostId);
125
136
  const profileCount = Object.keys(profiles).length;
@@ -132,7 +143,7 @@ async function runSyncCommand(subcommand = "status", opts = {}) {
132
143
  if (subcommand === "pull") {
133
144
  const targetHost = process.argv[process.argv.indexOf("pull") + 1];
134
145
  const resolvedTarget = targetHost && !targetHost.startsWith("-") ? targetHost : void 0;
135
- const { firebasePull } = await import("./firebaseSync-ON3RZ6KX.js");
146
+ const { firebasePull } = await import("./firebaseSync-LRN67UTV.js");
136
147
  const result = await firebasePull(resolvedTarget);
137
148
  if (!result) {
138
149
  const hint = resolvedTarget ? `host "${resolvedTarget}"` : "any host";
@@ -0,0 +1,189 @@
1
+ import {
2
+ collectNetwork
3
+ } from "./chunk-QEH4LR6P.js";
4
+ import "./chunk-NMGQVRCA.js";
5
+ import "./chunk-FYCBGUTW.js";
6
+ import "./chunk-OCL7P7N7.js";
7
+ import {
8
+ run,
9
+ runPowerShell
10
+ } from "./chunk-MZO5QKUR.js";
11
+ import "./chunk-3RG5ZIWI.js";
12
+
13
+ // src/topoCmd.ts
14
+ import net from "net";
15
+ import os from "os";
16
+ import chalk from "chalk";
17
+ async function probePort(ip, port, timeoutMs = 400) {
18
+ return new Promise((resolve) => {
19
+ const socket = new net.Socket();
20
+ socket.setTimeout(timeoutMs);
21
+ socket.once("connect", () => {
22
+ socket.destroy();
23
+ resolve(true);
24
+ });
25
+ socket.once("timeout", () => {
26
+ socket.destroy();
27
+ resolve(false);
28
+ });
29
+ socket.once("error", () => {
30
+ socket.destroy();
31
+ resolve(false);
32
+ });
33
+ socket.connect(port, ip);
34
+ });
35
+ }
36
+ async function getDefaultGateway() {
37
+ if (process.platform === "win32") {
38
+ try {
39
+ const raw = await runPowerShell(
40
+ `(Get-NetRoute -DestinationPrefix '0.0.0.0/0' -ErrorAction SilentlyContinue | Select-Object -First 1 -ExpandProperty NextHop)`,
41
+ ""
42
+ );
43
+ if (raw.trim() && raw.trim() !== "0.0.0.0") return raw.trim();
44
+ } catch {
45
+ }
46
+ try {
47
+ const rawCim = await runPowerShell(
48
+ `(Get-CimInstance Win32_NetworkAdapterConfiguration -Filter "IPEnabled = TRUE" -ErrorAction SilentlyContinue | Select-Object -First 1 -ExpandProperty DefaultIPGateway) -join ''`,
49
+ ""
50
+ );
51
+ if (rawCim.trim()) return rawCim.trim();
52
+ } catch {
53
+ }
54
+ }
55
+ try {
56
+ const raw = await run("route -n get default 2>/dev/null | grep gateway | awk '{print $2}'", "");
57
+ if (raw.trim()) return raw.trim();
58
+ } catch {
59
+ }
60
+ try {
61
+ const raw = await run("netstat -nr 2>/dev/null | grep default | awk '{print $2}' | head -n1", "");
62
+ if (raw.trim()) return raw.trim();
63
+ } catch {
64
+ }
65
+ return "192.168.1.1";
66
+ }
67
+ function parseWindowsArpOutput(raw, gatewayIp) {
68
+ const peers = [];
69
+ let currentIface = "Ethernet";
70
+ for (const line of raw.split("\n")) {
71
+ const trimmed = line.trim();
72
+ const ifaceMatch = trimmed.match(/Interface:\s*([\d.]+)/i);
73
+ if (ifaceMatch) {
74
+ currentIface = ifaceMatch[1];
75
+ continue;
76
+ }
77
+ const match = trimmed.match(/^([\d.]+)\s+([0-9a-fA-F-]+)\s+(\w+)/);
78
+ if (match) {
79
+ const ip = match[1];
80
+ const mac = match[2].replace(/-/g, ":");
81
+ if (ip === "255.255.255.255" || ip.startsWith("224.") || mac.includes("ff:ff:ff")) continue;
82
+ peers.push({
83
+ ip,
84
+ mac,
85
+ interface: currentIface,
86
+ isGateway: ip === gatewayIp
87
+ });
88
+ }
89
+ }
90
+ return peers;
91
+ }
92
+ async function getArpNeighbors(gatewayIp) {
93
+ const peers = [];
94
+ try {
95
+ if (process.platform === "win32") {
96
+ const raw2 = await run("arp -a 2>nul", "");
97
+ return parseWindowsArpOutput(raw2, gatewayIp);
98
+ }
99
+ const raw = await run("arp -a -n 2>/dev/null", "");
100
+ const lines = raw.split("\n");
101
+ for (const line of lines) {
102
+ const match = line.match(/\(([\d.]+)\)\s+at\s+([0-9a-fA-F:]+)\s+on\s+(\S+)/);
103
+ if (match) {
104
+ const ip = match[1];
105
+ const mac = match[2];
106
+ const iface = match[3];
107
+ if (ip === "255.255.255.255" || ip.startsWith("224.") || mac.includes("incomplete")) continue;
108
+ peers.push({
109
+ ip,
110
+ mac,
111
+ interface: iface,
112
+ isGateway: ip === gatewayIp
113
+ });
114
+ }
115
+ }
116
+ } catch {
117
+ }
118
+ return peers;
119
+ }
120
+ async function runTopoCommand(opts = {}) {
121
+ const localNet = await collectNetwork();
122
+ const gatewayIp = await getDefaultGateway();
123
+ const lanPeers = await getArpNeighbors(gatewayIp);
124
+ const hostname = os.hostname();
125
+ if (opts.scan || lanPeers.length <= 15) {
126
+ await Promise.all(
127
+ lanPeers.map(async (peer) => {
128
+ const [hasP2P, hasRest] = await Promise.all([
129
+ probePort(peer.ip, 9876, 250),
130
+ probePort(peer.ip, 8080, 250)
131
+ ]);
132
+ peer.pyreP2P = hasP2P;
133
+ peer.pyreRest = hasRest;
134
+ })
135
+ );
136
+ }
137
+ if (opts.json) {
138
+ console.log(
139
+ JSON.stringify(
140
+ {
141
+ local: {
142
+ hostname,
143
+ ip: localNet.ip,
144
+ interface: localNet.interface,
145
+ gateway: gatewayIp
146
+ },
147
+ lanPeers
148
+ },
149
+ null,
150
+ 2
151
+ )
152
+ );
153
+ return;
154
+ }
155
+ console.log(chalk.bold("\n \u{1F5FA} pyre topo \u2014 Network Topology Map\n"));
156
+ console.log(chalk.bold(" Local Node:"));
157
+ console.log(` Hostname: ${chalk.cyan(hostname)}`);
158
+ console.log(` IP: ${chalk.green(localNet.ip)}`);
159
+ console.log(` Interface: ${localNet.interface}`);
160
+ console.log(` Gateway: ${chalk.yellow(gatewayIp)}`);
161
+ console.log();
162
+ console.log(chalk.bold(` LAN Neighbors & Nodes (${lanPeers.length} discovered via ARP):`));
163
+ console.log(chalk.dim(" " + "\u2500".repeat(74)));
164
+ if (lanPeers.length === 0) {
165
+ console.log(chalk.dim(" No active ARP neighbors found in cache.\n"));
166
+ return;
167
+ }
168
+ for (const peer of lanPeers) {
169
+ const isGw = peer.isGateway ? chalk.yellow(" [Default Gateway]") : "";
170
+ const pyreBadge = peer.pyreP2P ? chalk.green.bold(" \u{1F525} PYRE PEER") : "";
171
+ const restBadge = peer.pyreRest ? chalk.blue(" (REST API)") : "";
172
+ console.log(` \u2022 ${chalk.bold(peer.ip.padEnd(16))} ${chalk.dim(peer.mac.padEnd(20))} [${peer.interface}]${isGw}${pyreBadge}${restBadge}`);
173
+ }
174
+ console.log();
175
+ const pyrePeers = lanPeers.filter((p) => p.pyreP2P);
176
+ if (pyrePeers.length > 0) {
177
+ console.log(chalk.green(` \u2714 Found ${pyrePeers.length} active Pyre node(s) on your LAN!`));
178
+ console.log(chalk.dim(` Connect using: pyre p2p connect --p2p-host ${pyrePeers[0].ip}
179
+ `));
180
+ } else {
181
+ console.log(chalk.dim(" Tip: Pass --scan to sweep and test pyre ports on discovered LAN neighbors.\n"));
182
+ }
183
+ }
184
+ export {
185
+ getArpNeighbors,
186
+ getDefaultGateway,
187
+ parseWindowsArpOutput,
188
+ runTopoCommand
189
+ };
@@ -1,3 +1,5 @@
1
+ import "./chunk-3RG5ZIWI.js";
2
+
1
3
  // src/updateCheck.ts
2
4
  import fs from "fs";
3
5
  import path from "path";
@@ -1,8 +1,9 @@
1
1
  import {
2
2
  collectAll
3
- } from "./chunk-23YWCI3O.js";
4
- import "./chunk-M73HCQD2.js";
5
- import "./chunk-VIACN2RU.js";
3
+ } from "./chunk-QEH4LR6P.js";
4
+ import "./chunk-NMGQVRCA.js";
5
+ import "./chunk-FYCBGUTW.js";
6
+ import "./chunk-OCL7P7N7.js";
6
7
  import {
7
8
  sendAlert
8
9
  } from "./chunk-Z2YV5HKV.js";
@@ -10,11 +11,13 @@ import "./chunk-MZO5QKUR.js";
10
11
  import {
11
12
  getConfigDir,
12
13
  readConfig
13
- } from "./chunk-QM5L43EK.js";
14
+ } from "./chunk-LDJB5V4N.js";
15
+ import "./chunk-3RG5ZIWI.js";
14
16
 
15
17
  // src/watchdogCmd.ts
16
18
  import fs from "fs";
17
19
  import path from "path";
20
+ import { execSync } from "child_process";
18
21
  import chalk from "chalk";
19
22
  var DEFAULT_WATCHDOG_RULES = [
20
23
  {
@@ -121,12 +124,44 @@ async function runWatchdogCommand(opts = {}) {
121
124
  }
122
125
  if (rule.action === "kill" && targetPid) {
123
126
  try {
124
- process.kill(targetPid, "SIGTERM");
125
- console.log(chalk.green(` \u2714 Killed PID ${targetPid} with SIGTERM`));
127
+ if (process.platform === "win32") {
128
+ execSync(`taskkill /F /PID ${targetPid} /T`, { stdio: "pipe" });
129
+ console.log(chalk.green(` \u2714 Killed PID ${targetPid} with taskkill /F /T`));
130
+ } else {
131
+ process.kill(targetPid, "SIGTERM");
132
+ console.log(chalk.green(` \u2714 Killed PID ${targetPid} with SIGTERM`));
133
+ }
126
134
  } catch (err) {
127
135
  console.log(chalk.red(` \u2716 Failed to kill PID ${targetPid}: ${err.message}`));
128
136
  }
129
137
  }
138
+ if (rule.action === "renice" && targetPid) {
139
+ try {
140
+ execSync(`renice +10 -p ${targetPid}`, { stdio: "pipe" });
141
+ console.log(chalk.green(` \u2714 Deprioritized runaway PID ${targetPid} to nice +10`));
142
+ } catch (err) {
143
+ console.log(chalk.red(` \u2716 Failed to renice PID ${targetPid}: ${err.message}`));
144
+ }
145
+ }
146
+ if (rule.action === "command" && rule.command) {
147
+ try {
148
+ console.log(chalk.cyan(` \u25B6 Executing remediation command: ${rule.command}`));
149
+ execSync(rule.command, { stdio: "pipe" });
150
+ console.log(chalk.green(` \u2714 Remediation command executed successfully.`));
151
+ } catch (err) {
152
+ console.log(chalk.red(` \u2716 Remediation command failed: ${err.message}`));
153
+ }
154
+ }
155
+ if (rule.dumpFlight) {
156
+ try {
157
+ const { captureCurrentFlightFrame, globalFlightRecorder } = await import("./flightRecorder-GJIAQOXC.js");
158
+ const frame = await captureCurrentFlightFrame();
159
+ globalFlightRecorder.push(frame);
160
+ const path2 = globalFlightRecorder.dump();
161
+ console.log(chalk.green(` \u2714 Forensic flight recording dumped: ${path2}`));
162
+ } catch {
163
+ }
164
+ }
130
165
  if (rule.action === "alert") {
131
166
  await sendAlert({
132
167
  metric: rule.name,
@@ -1,7 +1,8 @@
1
1
  import {
2
2
  CONFIG_FILE,
3
3
  writeConfig
4
- } from "./chunk-QM5L43EK.js";
4
+ } from "./chunk-LDJB5V4N.js";
5
+ import "./chunk-3RG5ZIWI.js";
5
6
 
6
7
  // src/wizard.ts
7
8
  import readline from "readline";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pyre-cli",
3
- "version": "10.3.0",
3
+ "version": "10.4.0",
4
4
  "description": "Cross-platform system monitoring CLI for macOS, Linux, and Windows: temps, system stats, live dashboard, packet monitor, battery predictor, export",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -21,7 +21,7 @@
21
21
  "build": "tsup src/index.ts --format esm --clean",
22
22
  "start": "node dist/index.js",
23
23
  "dev": "tsx src/index.ts",
24
- "test": "node dist/index.js --help && tsx --test src/easy_features.test.ts src/platform.test.ts"
24
+ "test": "node dist/index.js --help && tsx --test src/easy_features.test.ts src/platform.test.ts src/menu.test.ts src/power_features.test.ts"
25
25
  },
26
26
  "engines": {
27
27
  "node": ">=18.0.0"
@@ -0,0 +1,110 @@
1
+ /**
2
+ * pyre bench-git — Git Branch Performance Differ.
3
+ *
4
+ * Automatically benchmarks a test or build command across two git branches
5
+ * or commits, computing delta percentages for execution duration, peak RAM,
6
+ * and CPU utilization.
7
+ */
8
+ import { execSync } from 'node:child_process';
9
+ import chalk from 'chalk';
10
+ import { runWithBudget, formatBytes } from './budgetCmd.js';
11
+
12
+ export interface BranchMetric {
13
+ branch: string;
14
+ durationSec: number;
15
+ peakMemoryBytes: number;
16
+ peakCpuPct: number;
17
+ exitCode: number;
18
+ }
19
+
20
+ export async function runBenchGitCommand(branchA: string, branchB: string, commandStr: string): Promise<void> {
21
+ // 1. Verify git repo and clean status
22
+ try {
23
+ const status = execSync('git status --porcelain', { encoding: 'utf-8' }).trim();
24
+ if (status) {
25
+ console.error(chalk.red('\n ✖ Working directory contains uncommitted changes.'));
26
+ console.log(chalk.yellow(' Please commit or stash your changes before running `pyre bench-git`.\n'));
27
+ return;
28
+ }
29
+ } catch (err: any) {
30
+ console.error(chalk.red(`\n ✖ Not a git repository: ${err.message}`));
31
+ return;
32
+ }
33
+
34
+ const startingBranch = execSync('git rev-parse --abbrev-ref HEAD', { encoding: 'utf-8' }).trim();
35
+ console.log(chalk.bold(`\n 🔥 pyre bench-git — Cross-Branch Performance Differ\n`));
36
+ console.log(chalk.dim(` Command: ${commandStr}`));
37
+ console.log(chalk.dim(` Baseline Branch: ${branchA}`));
38
+ console.log(chalk.dim(` Target Branch: ${branchB}`));
39
+ console.log(chalk.dim(` Current Branch: ${startingBranch}\n`));
40
+
41
+ const tokens = commandStr.split(/\s+/).filter(Boolean);
42
+
43
+ try {
44
+ // Benchmark Branch A
45
+ console.log(chalk.bold.cyan(`▶ [1/2] Checking out '${branchA}' and benchmarking...`));
46
+ execSync(`git checkout ${branchA}`, { stdio: 'pipe' });
47
+ const resA = await runWithBudget(tokens, {});
48
+
49
+ // Benchmark Branch B
50
+ console.log(chalk.bold.cyan(`\n▶ [2/2] Checking out '${branchB}' and benchmarking...`));
51
+ execSync(`git checkout ${branchB}`, { stdio: 'pipe' });
52
+ const resB = await runWithBudget(tokens, {});
53
+
54
+ // Restore original branch
55
+ execSync(`git checkout ${startingBranch}`, { stdio: 'pipe' });
56
+ console.log(chalk.dim(`\n Restored workspace to '${startingBranch}'.`));
57
+
58
+ // Print side-by-side comparison
59
+ printGitComparisonTable(branchA, branchB, resA, resB);
60
+ } catch (err: any) {
61
+ // Ensure we attempt to restore branch
62
+ try {
63
+ execSync(`git checkout ${startingBranch}`, { stdio: 'pipe' });
64
+ } catch {}
65
+ console.error(chalk.red(`\n ✖ Benchmark failed: ${err.message}`));
66
+ }
67
+ }
68
+
69
+ function printGitComparisonTable(branchA: string, branchB: string, resA: any, resB: any) {
70
+ const diffPct = (valA: number, valB: number, invertGood = false) => {
71
+ if (valA === 0) return '0%';
72
+ const delta = ((valB - valA) / valA) * 100;
73
+ const sign = delta > 0 ? '+' : '';
74
+ const formatted = `${sign}${delta.toFixed(1)}%`;
75
+ const isGood = invertGood ? delta > 0 : delta < 0;
76
+ const color = delta === 0 ? chalk.dim : isGood ? chalk.green : chalk.red;
77
+ return color(formatted);
78
+ };
79
+
80
+ console.log(chalk.bold('\n ─────────────────────────────────────────────────────────────'));
81
+ console.log(chalk.bold(' 🔥 Cross-Branch Performance Comparison Results'));
82
+ console.log(chalk.dim(' ─────────────────────────────────────────────────────────────'));
83
+
84
+ const colBranchA = branchA.padEnd(16);
85
+ const colBranchB = branchB.padEnd(16);
86
+ console.log(` ${'Metric'.padEnd(18)} ${colBranchA} ${colBranchB} Delta`);
87
+ console.log(chalk.dim(' ─'.repeat(30)));
88
+
89
+ // Duration
90
+ const durA = `${resA.durationSec.toFixed(2)}s`.padEnd(16);
91
+ const durB = `${resB.durationSec.toFixed(2)}s`.padEnd(16);
92
+ console.log(` ${'Duration'.padEnd(18)} ${durA} ${durB} ${diffPct(resA.durationSec, resB.durationSec)}`);
93
+
94
+ // Peak RAM
95
+ const ramA = formatBytes(resA.peakMemoryBytes).padEnd(16);
96
+ const ramB = formatBytes(resB.peakMemoryBytes).padEnd(16);
97
+ console.log(` ${'Peak Memory'.padEnd(18)} ${ramA} ${ramB} ${diffPct(resA.peakMemoryBytes, resB.peakMemoryBytes)}`);
98
+
99
+ // Peak CPU
100
+ const cpuA = `${resA.peakCpuPct.toFixed(1)}%`.padEnd(16);
101
+ const cpuB = `${resB.peakCpuPct.toFixed(1)}%`.padEnd(16);
102
+ console.log(` ${'Peak CPU'.padEnd(18)} ${cpuA} ${cpuB} ${diffPct(resA.peakCpuPct, resB.peakCpuPct)}`);
103
+
104
+ // Exit code
105
+ const codeA = `${resA.exitCode}`.padEnd(16);
106
+ const codeB = `${resB.exitCode}`.padEnd(16);
107
+ console.log(` ${'Exit Code'.padEnd(18)} ${codeA} ${codeB}`);
108
+
109
+ console.log(chalk.dim(' ─────────────────────────────────────────────────────────────\n'));
110
+ }
@@ -0,0 +1,199 @@
1
+ /**
2
+ * pyre run — Performance Budgeting Runner.
3
+ *
4
+ * Spawns an arbitrary command and tracks its execution lifecycle,
5
+ * measuring runtime duration, peak RSS memory, and CPU utilization.
6
+ * Enforces budgets (--budget-cpu, --budget-mem, --budget-time), exiting
7
+ * with code 124 if thresholds are exceeded (ideal for CI/CD gates).
8
+ */
9
+ import fs from 'node:fs';
10
+ import { spawn } from 'node:child_process';
11
+ import chalk from 'chalk';
12
+
13
+ export interface BudgetLimits {
14
+ maxCpuPct?: number;
15
+ maxMemoryBytes?: number;
16
+ maxTimeSec?: number;
17
+ }
18
+
19
+ export interface BudgetRunResult {
20
+ command: string;
21
+ exitCode: number;
22
+ durationSec: number;
23
+ peakCpuPct: number;
24
+ avgCpuPct: number;
25
+ peakMemoryBytes: number;
26
+ violations: string[];
27
+ }
28
+
29
+ export function parseBytesString(s: string): number {
30
+ const trimmed = s.trim().toUpperCase();
31
+ const num = parseFloat(trimmed);
32
+ if (isNaN(num)) return 0;
33
+ if (trimmed.endsWith('G') || trimmed.endsWith('GB')) return num * 1024 * 1024 * 1024;
34
+ if (trimmed.endsWith('M') || trimmed.endsWith('MB')) return num * 1024 * 1024;
35
+ if (trimmed.endsWith('K') || trimmed.endsWith('KB')) return num * 1024;
36
+ return num;
37
+ }
38
+
39
+ export function formatBytes(b: number): string {
40
+ if (b >= 1024 * 1024 * 1024) return `${(b / (1024 * 1024 * 1024)).toFixed(2)} GB`;
41
+ if (b >= 1024 * 1024) return `${(b / (1024 * 1024)).toFixed(2)} MB`;
42
+ if (b >= 1024) return `${(b / 1024).toFixed(1)} KB`;
43
+ return `${b} B`;
44
+ }
45
+
46
+ export async function runWithBudget(
47
+ commandArgs: string[],
48
+ budgets: BudgetLimits,
49
+ opts?: { markdown?: boolean; outFile?: string }
50
+ ): Promise<BudgetRunResult> {
51
+ if (commandArgs.length === 0) {
52
+ throw new Error('No command specified. Usage: pyre run [budget flags] -- <command>');
53
+ }
54
+
55
+ const cmdStr = commandArgs.join(' ');
56
+ const [bin, ...args] = commandArgs;
57
+
58
+ const startTime = Date.now();
59
+ const cpuSamples: number[] = [];
60
+ let peakMemBytes = 0;
61
+ let peakCpu = 0;
62
+
63
+ const child = spawn(bin, args, {
64
+ stdio: 'inherit',
65
+ env: process.env,
66
+ });
67
+
68
+ const pid = child.pid;
69
+ let sampleInterval: NodeJS.Timeout | null = null;
70
+
71
+ if (pid) {
72
+ sampleInterval = setInterval(() => {
73
+ try {
74
+ // Sample RSS memory using ps
75
+ const { execSync } = require('node:child_process');
76
+ const output = execSync(`ps -o %cpu,rss -p ${pid} 2>/dev/null`, { encoding: 'utf-8' });
77
+ const lines = output.trim().split('\n');
78
+ if (lines.length > 1) {
79
+ const parts = lines[1].trim().split(/\s+/);
80
+ const cpu = parseFloat(parts[0]) || 0;
81
+ const rssKb = parseInt(parts[1], 10) || 0;
82
+ const memBytes = rssKb * 1024;
83
+
84
+ cpuSamples.push(cpu);
85
+ if (cpu > peakCpu) peakCpu = cpu;
86
+ if (memBytes > peakMemBytes) peakMemBytes = memBytes;
87
+ }
88
+ } catch {
89
+ // Child exited
90
+ }
91
+ }, 100);
92
+ }
93
+
94
+ const exitCode = await new Promise<number>((resolve) => {
95
+ child.on('close', (code) => {
96
+ if (sampleInterval) clearInterval(sampleInterval);
97
+ resolve(code ?? 0);
98
+ });
99
+ child.on('error', () => {
100
+ if (sampleInterval) clearInterval(sampleInterval);
101
+ resolve(1);
102
+ });
103
+ });
104
+
105
+ const durationSec = (Date.now() - startTime) / 1000;
106
+ const avgCpu = cpuSamples.length > 0 ? cpuSamples.reduce((a, b) => a + b, 0) / cpuSamples.length : peakCpu;
107
+
108
+ // Evaluate budget limits
109
+ const violations: string[] = [];
110
+ if (budgets.maxTimeSec !== undefined && durationSec > budgets.maxTimeSec) {
111
+ violations.push(`Runtime exceeded budget: ${durationSec.toFixed(2)}s > ${budgets.maxTimeSec}s limit`);
112
+ }
113
+ if (budgets.maxMemoryBytes !== undefined && peakMemBytes > budgets.maxMemoryBytes) {
114
+ violations.push(`Memory exceeded budget: ${formatBytes(peakMemBytes)} > ${formatBytes(budgets.maxMemoryBytes)} limit`);
115
+ }
116
+ if (budgets.maxCpuPct !== undefined && peakCpu > budgets.maxCpuPct) {
117
+ violations.push(`CPU exceeded budget: ${peakCpu.toFixed(1)}% > ${budgets.maxCpuPct}% limit`);
118
+ }
119
+
120
+ const result: BudgetRunResult = {
121
+ command: cmdStr,
122
+ exitCode,
123
+ durationSec,
124
+ peakCpuPct: peakCpu,
125
+ avgCpuPct: avgCpu,
126
+ peakMemoryBytes: peakMemBytes,
127
+ violations,
128
+ };
129
+
130
+ printBudgetReport(result, budgets, opts);
131
+ return result;
132
+ }
133
+
134
+ function printBudgetReport(result: BudgetRunResult, budgets: BudgetLimits, opts?: { markdown?: boolean; outFile?: string }) {
135
+ const isViolated = result.violations.length > 0;
136
+ const isFailed = result.exitCode !== 0 || isViolated;
137
+
138
+ console.log(chalk.bold('\n ─────────────────────────────────────────────────────────────'));
139
+ console.log(chalk.bold(` 🔥 pyre run — Performance Budget Report`));
140
+ console.log(chalk.dim(` Command: ${result.command}`));
141
+ console.log(chalk.dim(' ─────────────────────────────────────────────────────────────'));
142
+
143
+ // Table rows
144
+ const statusTime = budgets.maxTimeSec === undefined
145
+ ? chalk.dim('unconstrained')
146
+ : result.durationSec <= budgets.maxTimeSec
147
+ ? chalk.green(`✔ Pass (<= ${budgets.maxTimeSec}s)`)
148
+ : chalk.red(`✖ FAIL (> ${budgets.maxTimeSec}s)`);
149
+
150
+ const statusMem = budgets.maxMemoryBytes === undefined
151
+ ? chalk.dim('unconstrained')
152
+ : result.peakMemoryBytes <= budgets.maxMemoryBytes
153
+ ? chalk.green(`✔ Pass (<= ${formatBytes(budgets.maxMemoryBytes)})`)
154
+ : chalk.red(`✖ FAIL (> ${formatBytes(budgets.maxMemoryBytes)})`);
155
+
156
+ const statusCpu = budgets.maxCpuPct === undefined
157
+ ? chalk.dim('unconstrained')
158
+ : result.peakCpuPct <= budgets.maxCpuPct
159
+ ? chalk.green(`✔ Pass (<= ${budgets.maxCpuPct}%)`)
160
+ : chalk.red(`✖ FAIL (> ${budgets.maxCpuPct}%)`);
161
+
162
+ console.log(` Duration: ${result.durationSec.toFixed(2)}s`.padEnd(28) + statusTime);
163
+ console.log(` Peak Memory: ${formatBytes(result.peakMemoryBytes)}`.padEnd(28) + statusMem);
164
+ console.log(` Peak CPU: ${result.peakCpuPct.toFixed(1)}% (avg ${result.avgCpuPct.toFixed(1)}%)`.padEnd(28) + statusCpu);
165
+ console.log(` Exit Code: ${result.exitCode === 0 ? chalk.green('0') : chalk.red(result.exitCode)}`);
166
+
167
+ if (isViolated) {
168
+ console.log(chalk.bold.red('\n ✖ Performance Budget Violations:'));
169
+ result.violations.forEach((v) => console.log(chalk.red(` • ${v}`)));
170
+ }
171
+
172
+ console.log(chalk.dim(' ─────────────────────────────────────────────────────────────\n'));
173
+
174
+ if (opts?.outFile || opts?.markdown) {
175
+ const md = generateMarkdownBudgetReport(result, budgets);
176
+ if (opts.outFile) {
177
+ fs.writeFileSync(opts.outFile, md, 'utf-8');
178
+ console.log(chalk.dim(` Saved report to ${opts.outFile}\n`));
179
+ }
180
+ }
181
+ }
182
+
183
+ function generateMarkdownBudgetReport(result: BudgetRunResult, budgets: BudgetLimits): string {
184
+ return [
185
+ `# Pyre Performance Budget Report`,
186
+ ``,
187
+ `**Command**: \`${result.command}\` `,
188
+ `**Status**: ${result.violations.length === 0 && result.exitCode === 0 ? '✅ PASSED' : '❌ FAILED'} `,
189
+ `**Exit Code**: \`${result.exitCode}\` `,
190
+ ``,
191
+ `| Metric | Measured | Limit | Status |`,
192
+ `| :--- | :--- | :--- | :--- |`,
193
+ `| Duration | ${result.durationSec.toFixed(2)}s | ${budgets.maxTimeSec ? budgets.maxTimeSec + 's' : 'N/A'} | ${budgets.maxTimeSec && result.durationSec > budgets.maxTimeSec ? '❌ FAIL' : '✅ PASS'} |`,
194
+ `| Peak Memory | ${formatBytes(result.peakMemoryBytes)} | ${budgets.maxMemoryBytes ? formatBytes(budgets.maxMemoryBytes) : 'N/A'} | ${budgets.maxMemoryBytes && result.peakMemoryBytes > budgets.maxMemoryBytes ? '❌ FAIL' : '✅ PASS'} |`,
195
+ `| Peak CPU | ${result.peakCpuPct.toFixed(1)}% | ${budgets.maxCpuPct ? budgets.maxCpuPct + '%' : 'N/A'} | ${budgets.maxCpuPct && result.peakCpuPct > budgets.maxCpuPct ? '❌ FAIL' : '✅ PASS'} |`,
196
+ ``,
197
+ ...(result.violations.length > 0 ? [`### Violations`, ...result.violations.map((v) => `- ${v}`)] : []),
198
+ ].join('\n');
199
+ }