pyre-cli 10.3.0 → 10.5.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 +41 -0
- package/dist/{alertChannels-TPSQIJ7S.js → alertChannels-MNX4XH6J.js} +1 -0
- package/dist/{anomaliesCmd-25OFKYPK.js → anomaliesCmd-HHTY5LYL.js} +2 -0
- package/dist/benchGitCmd-HIXUC5EK.js +87 -0
- package/dist/{benchmarkCmd-A3ETK6VY.js → benchmarkCmd-B7DLP2VC.js} +2 -0
- package/dist/{blender-FGH3ASEQ.js → blender-6ZX5SPYC.js} +3 -1
- package/dist/{brewHealth-P2GW5Y5C.js → brewHealth-VFW33QB3.js} +1 -0
- package/dist/budgetCmd-TGO3RYLN.js +11 -0
- package/dist/{buildCmd-CDPZ2ZFA.js → buildCmd-E7KUCFYG.js} +3 -1
- package/dist/{checkCmd-WQAHAUWO.js → checkCmd-QLYTP77Y.js} +5 -3
- package/dist/chunk-3RG5ZIWI.js +10 -0
- package/dist/{chunk-VIACN2RU.js → chunk-FYCBGUTW.js} +41 -19
- package/dist/chunk-JH7P6KLN.js +143 -0
- package/dist/{chunk-QM5L43EK.js → chunk-LDJB5V4N.js} +5 -1
- package/dist/chunk-NMGQVRCA.js +111 -0
- package/dist/chunk-OCL7P7N7.js +936 -0
- package/dist/{chunk-23YWCI3O.js → chunk-QEH4LR6P.js} +28 -686
- package/dist/{chunk-KVUMOA6C.js → chunk-RKPGPVTO.js} +77 -4
- package/dist/chunk-UF3XQ3R3.js +3800 -0
- package/dist/{collectors-JDN5Y7EV.js → collectors-V7SCIIDH.js} +5 -3
- package/dist/{completions-ZVTNE52B.js → completions-FTPXDSR2.js} +44 -3
- package/dist/{config-J4YMADEF.js → config-5WW5M63G.js} +2 -1
- package/dist/containersCmd-FSMYOBG2.js +84 -0
- package/dist/{doctor-LHFW4UJP.js → doctor-O4DIAMHK.js} +3 -2
- package/dist/{explainCmd-MF6PIMLW.js → explainCmd-7PBPA7O5.js} +1 -0
- package/dist/{extensions-XJGHG7X3.js → extensions-QQEV7U3D.js} +1 -0
- package/dist/{firebaseSync-ON3RZ6KX.js → firebaseSync-LRN67UTV.js} +2 -0
- package/dist/flightRecorder-GJIAQOXC.js +162 -0
- package/dist/index.js +736 -4125
- package/dist/inspectCmd-5FXJBJTK.js +137 -0
- package/dist/menu-AJFGDLBP.js +1233 -0
- package/dist/monitors/platform/windows-ui.ps1 +31 -29
- package/dist/{netUsageCmd-67IBZZYN.js → netUsageCmd-JP6YKBPF.js} +7 -5
- package/dist/optimizeCmd-WZKLFYZU.js +1110 -0
- package/dist/{pipeCmd-2N4JPKYE.js → pipeCmd-CPK6FVKB.js} +5 -3
- package/dist/playbookCmd-FRROIEXR.js +145 -0
- package/dist/{profileProcCmd-N4H6MCBT.js → profileProcCmd-5Y3733E7.js} +64 -13
- package/dist/{prometheus-5BL5PVLC.js → prometheus-CYNUR3QT.js} +1 -0
- package/dist/{prometheusCmd-LEUM5WCZ.js → prometheusCmd-G2CSEZRQ.js} +5 -3
- package/dist/{replayCmd-Z3PYXY7V.js → replayCmd-ODB5SPOQ.js} +25 -6
- package/dist/{run-HOGQDDTA.js → run-UJQYT2TO.js} +1 -0
- package/dist/{serveCmd-L7LVJOZN.js → serveCmd-KDPWLVQI.js} +5 -3
- package/dist/{service-7GIX3YNC.js → service-LIYYR3UI.js} +1 -0
- package/dist/{smartCmd-ODWKSFNG.js → smartCmd-2BJJ37GV.js} +130 -5
- package/dist/statuslineCmd-EUKYRR2G.js +75 -0
- package/dist/{stressCmd-MFXMFXYX.js → stressCmd-GKHGY26X.js} +2 -0
- package/dist/{syncCmd-42IRF633.js → syncCmd-MIBFKX24.js} +16 -5
- package/dist/topoCmd-RTXQJ6XA.js +189 -0
- package/dist/{updateCheck-52X6LJG5.js → updateCheck-LJHEMG3G.js} +2 -0
- package/dist/{watchdogCmd-6GR4UN6B.js → watchdogCmd-XM6OPTKM.js} +41 -6
- package/dist/{wizard-JQMQWFAW.js → wizard-PRDOJROQ.js} +2 -1
- package/package.json +2 -2
- package/src/benchGitCmd.ts +110 -0
- package/src/budgetCmd.ts +199 -0
- package/src/completions.ts +42 -3
- package/src/containersCmd.ts +106 -0
- package/src/doctor.ts +86 -6
- package/src/easy_features.test.ts +83 -0
- package/src/flightRecorder.ts +214 -0
- package/src/index.ts +181 -8
- package/src/inspectCmd.ts +165 -0
- package/src/live/input.ts +70 -3
- package/src/live/render.ts +1 -1
- package/src/live/state.ts +2 -1
- package/src/menu/commands.ts +754 -0
- package/src/menu/index.ts +358 -0
- package/src/menu/render.ts +272 -0
- package/src/menu/types.ts +63 -0
- package/src/menu.test.ts +110 -0
- package/src/monitors/blender.ts +49 -19
- package/src/monitors/buildTracker.ts +72 -54
- package/src/monitors/platform/windows-ui.ps1 +31 -29
- package/src/monitors/platform/windows.ts +636 -280
- package/src/optimizeCmd.ts +1248 -0
- package/src/platform.test.ts +146 -0
- package/src/playbookCmd.ts +156 -0
- package/src/power_features.test.ts +251 -0
- package/src/profileProcCmd.ts +68 -21
- package/src/replayCmd.ts +15 -6
- package/src/smartCmd.ts +149 -10
- package/src/state/config.ts +13 -0
- package/src/statuslineCmd.ts +87 -0
- package/src/syncCmd.ts +12 -0
- package/src/topoCmd.ts +83 -50
- package/src/watchdogCmd.ts +40 -3
- package/src/xbar.ts +195 -17
- package/dist/chunk-M73HCQD2.js +0 -95
- package/dist/topoCmd-DZ3YS5IL.js +0 -148
|
@@ -3,7 +3,8 @@ import {
|
|
|
3
3
|
getProfilesDir,
|
|
4
4
|
readConfig,
|
|
5
5
|
writeConfig
|
|
6
|
-
} from "./chunk-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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,8 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
collectAll
|
|
3
|
-
} from "./chunk-
|
|
4
|
-
import "./chunk-
|
|
5
|
-
import "./chunk-
|
|
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-
|
|
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.
|
|
125
|
-
|
|
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,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pyre-cli",
|
|
3
|
-
"version": "10.
|
|
3
|
+
"version": "10.5.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
|
+
}
|
package/src/budgetCmd.ts
ADDED
|
@@ -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
|
+
}
|