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.
- package/README.md +75 -18
- 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 +737 -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 +183 -9
- 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 +613 -280
- package/src/optimizeCmd.ts +1248 -0
- package/src/platform.test.ts +135 -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
package/src/platform.test.ts
CHANGED
|
@@ -82,6 +82,141 @@ describe('Windows Telemetry Parsers', () => {
|
|
|
82
82
|
});
|
|
83
83
|
});
|
|
84
84
|
|
|
85
|
+
describe('Intel, AMD & NVIDIA Hardware Telemetry', () => {
|
|
86
|
+
it('should parse NVIDIA GPU telemetry from nvidia-smi correctly', async () => {
|
|
87
|
+
const { parseNvidiaSmiOutput } = await import('./monitors/platform/windows.js');
|
|
88
|
+
const sampleCsv = `NVIDIA GeForce RTX 4090, 24564, 42, 58, 215.4\n`;
|
|
89
|
+
const gpu = parseNvidiaSmiOutput(sampleCsv);
|
|
90
|
+
assert.ok(gpu !== null);
|
|
91
|
+
assert.strictEqual(gpu.model, 'NVIDIA GeForce RTX 4090');
|
|
92
|
+
assert.strictEqual(gpu.memory, 24564 * 1024 * 1024);
|
|
93
|
+
assert.strictEqual(gpu.utilization, 42);
|
|
94
|
+
assert.strictEqual(gpu.temperature, 58);
|
|
95
|
+
assert.strictEqual(gpu.powerDraw, 215.4);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('should parse and prioritize AMD and Intel GPUs over integrated adapters', async () => {
|
|
99
|
+
const { parseWindowsVideoControllers, prioritizeGpuControllers } = await import('./monitors/platform/windows.js');
|
|
100
|
+
const sampleCsv = `"Name","AdapterRAM","DriverVersion","VideoProcessor","PNPDeviceID"
|
|
101
|
+
"AMD Radeon(TM) Graphics","536870912","31.0.12.0","AMD Radeon Graphics Processor","PCI\\VEN_1002&DEV_164E"
|
|
102
|
+
"AMD Radeon RX 7900 XTX","4294967295","31.0.24.0","Navi 31","PCI\\VEN_1002&DEV_744C"`;
|
|
103
|
+
|
|
104
|
+
const controllers = parseWindowsVideoControllers(sampleCsv);
|
|
105
|
+
assert.strictEqual(controllers.length, 2);
|
|
106
|
+
|
|
107
|
+
const prioritized = prioritizeGpuControllers(controllers);
|
|
108
|
+
assert.strictEqual(prioritized[0].name, 'AMD Radeon RX 7900 XTX');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('should parse Intel Arc discrete GPU correctly', async () => {
|
|
112
|
+
const { parseWindowsVideoControllers, prioritizeGpuControllers } = await import('./monitors/platform/windows.js');
|
|
113
|
+
const sampleCsv = `"Name","AdapterRAM","DriverVersion","VideoProcessor","PNPDeviceID"
|
|
114
|
+
"Intel(R) UHD Graphics 770","1073741824","31.0.101.4000","Intel(R) UHD Graphics Family","PCI\\VEN_8086&DEV_4680"
|
|
115
|
+
"Intel(R) Arc(TM) A770 Graphics","4294967295","31.0.101.5000","Intel(R) Arc(TM) A-Series Graphics","PCI\\VEN_8086&DEV_56A0"`;
|
|
116
|
+
|
|
117
|
+
const controllers = parseWindowsVideoControllers(sampleCsv);
|
|
118
|
+
const prioritized = prioritizeGpuControllers(controllers);
|
|
119
|
+
assert.strictEqual(prioritized[0].name, 'Intel(R) Arc(TM) A770 Graphics');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('should detect CPU vendors and estimate architecture TDP accurately', async () => {
|
|
123
|
+
const { detectProcessorVendor, estimateCpuTdp } = await import('./monitors/platform/windows.js');
|
|
124
|
+
// AMD tests
|
|
125
|
+
assert.strictEqual(detectProcessorVendor('AMD Ryzen 9 7950X 16-Core Processor'), 'amd');
|
|
126
|
+
assert.strictEqual(estimateCpuTdp('AMD Ryzen 9 7950X'), 170);
|
|
127
|
+
assert.strictEqual(estimateCpuTdp('AMD Ryzen 7 7800X3D'), 105);
|
|
128
|
+
assert.strictEqual(estimateCpuTdp('AMD Ryzen 5 5600'), 65);
|
|
129
|
+
|
|
130
|
+
// Intel tests
|
|
131
|
+
assert.strictEqual(detectProcessorVendor('13th Gen Intel(R) Core(TM) i9-13900K'), 'intel');
|
|
132
|
+
assert.strictEqual(estimateCpuTdp('Intel(R) Core(TM) i9-13900K'), 125);
|
|
133
|
+
assert.strictEqual(estimateCpuTdp('Intel(R) Core(TM) Ultra 7 155H'), 45);
|
|
134
|
+
assert.strictEqual(estimateCpuTdp('Intel(R) Core(TM) i5-12400'), 65);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('should parse Windows processes with ParentProcessId (ppid) for tree view', async () => {
|
|
138
|
+
const { parseWindowsProcessCsv } = await import('./monitors/platform/windows.js');
|
|
139
|
+
const sampleCsv = `"Name","ProcessId","ParentProcessId","ThreadCount","WorkingSetSize","UserModeTime","KernelModeTime","CommandLine"
|
|
140
|
+
"explorer.exe","1024","512","48","157286400","50000000","25000000","C:\\Windows\\explorer.exe"
|
|
141
|
+
"chrome.exe","2048","1024","32","314572800","100000000","50000000","chrome.exe"
|
|
142
|
+
"python.exe","4096","1024","8","524288000","150000000","75000000","python.exe -m torch"`;
|
|
143
|
+
|
|
144
|
+
const cache = new Map();
|
|
145
|
+
const procs = parseWindowsProcessCsv(sampleCsv, 32 * 1024 * 1024 * 1024, cache, Date.now());
|
|
146
|
+
|
|
147
|
+
assert.strictEqual(procs.length, 3);
|
|
148
|
+
const explorer = procs.find(p => p.pid === 1024);
|
|
149
|
+
const chrome = procs.find(p => p.pid === 2048);
|
|
150
|
+
const python = procs.find(p => p.pid === 4096);
|
|
151
|
+
|
|
152
|
+
assert.ok(explorer);
|
|
153
|
+
assert.strictEqual(explorer?.ppid, 512);
|
|
154
|
+
|
|
155
|
+
assert.ok(chrome);
|
|
156
|
+
assert.strictEqual(chrome?.ppid, 1024); // Parent is explorer! Process tree works!
|
|
157
|
+
|
|
158
|
+
assert.ok(python);
|
|
159
|
+
assert.strictEqual(python?.ppid, 1024);
|
|
160
|
+
assert.strictEqual(python?.isGpuOrMlAttributed, true);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('should parse Windows TCP sockets and build per-app network usage', async () => {
|
|
164
|
+
const { parseWindowsTcpConnections, buildWindowsNetworkProcesses } = await import('./monitors/platform/windows.js');
|
|
165
|
+
const sampleConns = `"LocalPort","RemoteAddress","RemotePort","State","OwningProcess"
|
|
166
|
+
"51234","142.250.190.46","443","Established","2048"
|
|
167
|
+
"51235","142.250.190.46","443","Established","2048"
|
|
168
|
+
"8080","0.0.0.0","0","Listen","4096"`;
|
|
169
|
+
|
|
170
|
+
const conns = parseWindowsTcpConnections(sampleConns);
|
|
171
|
+
assert.strictEqual(conns.length, 3);
|
|
172
|
+
|
|
173
|
+
const procMap = new Map([[2048, 'chrome.exe'], [4096, 'node.exe']]);
|
|
174
|
+
const netProcs = buildWindowsNetworkProcesses(conns, procMap);
|
|
175
|
+
|
|
176
|
+
assert.strictEqual(netProcs.length, 2);
|
|
177
|
+
assert.strictEqual(netProcs[0].command, 'chrome.exe');
|
|
178
|
+
assert.strictEqual(netProcs[0].connections.length, 2);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('should parse Windows physical disk health and reliability', async () => {
|
|
182
|
+
const { parseWindowsPhysicalDiskCsv, parseWindowsReliabilityCsv } = await import('./smartCmd.js');
|
|
183
|
+
const sampleDiskCsv = `"DeviceId","FriendlyName","SerialNumber","MediaType","HealthStatus","Size"
|
|
184
|
+
"0","Samsung SSD 980 PRO 2TB","S5GXNX0T123456","SSD","Healthy","2000398934016"`;
|
|
185
|
+
|
|
186
|
+
const disks = parseWindowsPhysicalDiskCsv(sampleDiskCsv);
|
|
187
|
+
assert.strictEqual(disks.length, 1);
|
|
188
|
+
assert.strictEqual(disks[0].model, 'Samsung SSD 980 PRO 2TB');
|
|
189
|
+
assert.strictEqual(disks[0].health, 'PASSED');
|
|
190
|
+
assert.strictEqual(disks[0].protocol, 'SSD');
|
|
191
|
+
assert.strictEqual(disks[0].capacity, '2.0 TB');
|
|
192
|
+
|
|
193
|
+
const sampleRelCsv = `"Temperature","Wear","PowerOnHours","ReadErrorsTotal","WriteErrorsTotal"
|
|
194
|
+
"42","3","4500","0","0"`;
|
|
195
|
+
const rel = parseWindowsReliabilityCsv(sampleRelCsv);
|
|
196
|
+
assert.strictEqual(rel.tempC, 42);
|
|
197
|
+
assert.strictEqual(rel.percentUsed, 3);
|
|
198
|
+
assert.strictEqual(rel.powerOnHours, 4500);
|
|
199
|
+
assert.strictEqual(rel.reallocatedSectors, 0);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('should parse Windows ARP table for topology discovery', async () => {
|
|
203
|
+
const { parseWindowsArpOutput } = await import('./topoCmd.js');
|
|
204
|
+
const sampleArp = `
|
|
205
|
+
Interface: 192.168.1.150 --- 0x3
|
|
206
|
+
Internet Address Physical Address Type
|
|
207
|
+
192.168.1.1 00-11-22-33-44-55 dynamic
|
|
208
|
+
192.168.1.200 aa-bb-cc-dd-ee-ff dynamic
|
|
209
|
+
`;
|
|
210
|
+
const peers = parseWindowsArpOutput(sampleArp, '192.168.1.1');
|
|
211
|
+
assert.strictEqual(peers.length, 2);
|
|
212
|
+
assert.strictEqual(peers[0].ip, '192.168.1.1');
|
|
213
|
+
assert.strictEqual(peers[0].mac, '00:11:22:33:44:55');
|
|
214
|
+
assert.strictEqual(peers[0].isGateway, true);
|
|
215
|
+
assert.strictEqual(peers[1].ip, '192.168.1.200');
|
|
216
|
+
assert.strictEqual(peers[1].isGateway, false);
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
|
|
85
220
|
describe('Cross-Platform Service & Package Management', () => {
|
|
86
221
|
it('should report package manager health without crashing', async () => {
|
|
87
222
|
const { getBrewHealth } = await import('./brewHealth.js');
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pyre playbook — Multi-Step Macro Recipes & Playbooks.
|
|
3
|
+
*
|
|
4
|
+
* Runs sequenced Pyre diagnostic, optimization, and benchmark workflows
|
|
5
|
+
* with progress reporting and consolidated summary logs.
|
|
6
|
+
*/
|
|
7
|
+
import fs from 'node:fs';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import { spawnSync } from 'node:child_process';
|
|
10
|
+
import chalk from 'chalk';
|
|
11
|
+
import { getConfigDir } from './state/config.js';
|
|
12
|
+
|
|
13
|
+
export interface PlaybookStep {
|
|
14
|
+
name: string;
|
|
15
|
+
command: string; // e.g. "check --detailed"
|
|
16
|
+
description?: string;
|
|
17
|
+
allowFailure?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface Playbook {
|
|
21
|
+
id: string;
|
|
22
|
+
name: string;
|
|
23
|
+
description: string;
|
|
24
|
+
category: 'audit' | 'bench' | 'maintenance' | 'triage';
|
|
25
|
+
steps: PlaybookStep[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const BUILTIN_PLAYBOOKS: Playbook[] = [
|
|
29
|
+
{
|
|
30
|
+
id: 'audit',
|
|
31
|
+
name: 'Full System Health Audit',
|
|
32
|
+
description: 'Run health check, diagnostic doctor, drive S.M.A.R.T., and package manager audit',
|
|
33
|
+
category: 'audit',
|
|
34
|
+
steps: [
|
|
35
|
+
{ name: 'System Health Check', command: 'check' },
|
|
36
|
+
{ name: 'Doctor Diagnostics', command: 'doctor' },
|
|
37
|
+
{ name: 'Drive S.M.A.R.T. Health', command: 'smart', allowFailure: true },
|
|
38
|
+
{ name: 'Package Manager Health', command: 'brew', allowFailure: true },
|
|
39
|
+
],
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
id: 'clean',
|
|
43
|
+
name: 'Complete Mac Maintenance & Cleanup',
|
|
44
|
+
description: 'Quit long-idle background apps, purge inactive memory, clean caches, flush DNS',
|
|
45
|
+
category: 'maintenance',
|
|
46
|
+
steps: [
|
|
47
|
+
{ name: 'Quit Idle Apps', command: 'optimize apps --idle 2h -y' },
|
|
48
|
+
{ name: 'Purge Inactive Memory', command: 'optimize memory -y' },
|
|
49
|
+
{ name: 'Clean Application Caches', command: 'optimize caches --clean -y' },
|
|
50
|
+
{ name: 'Flush DNS Resolver Cache', command: 'optimize dns -y' },
|
|
51
|
+
],
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
id: 'triage',
|
|
55
|
+
name: 'Performance Spike & Incident Triage',
|
|
56
|
+
description: 'Detect historical anomalies, inspect active network usage, and audit drivers',
|
|
57
|
+
category: 'triage',
|
|
58
|
+
steps: [
|
|
59
|
+
{ name: 'Z-Score Anomaly Digest', command: 'anomalies --since 1d' },
|
|
60
|
+
{ name: 'Per-Application Network Usage', command: 'netusage --top 10' },
|
|
61
|
+
{ name: 'System Extensions Audit', command: 'extensions', allowFailure: true },
|
|
62
|
+
],
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
id: 'bench',
|
|
66
|
+
name: 'Quick Compute & Storage Baseline',
|
|
67
|
+
description: 'Execute CPU Pi benchmark and query physical disk health',
|
|
68
|
+
category: 'bench',
|
|
69
|
+
steps: [
|
|
70
|
+
{ name: 'CPU Pi Benchmark', command: 'benchmark' },
|
|
71
|
+
{ name: 'Physical Disk Health', command: 'smart', allowFailure: true },
|
|
72
|
+
],
|
|
73
|
+
},
|
|
74
|
+
];
|
|
75
|
+
|
|
76
|
+
export function getCustomPlaybooks(): Playbook[] {
|
|
77
|
+
const filePath = path.join(getConfigDir(), 'playbooks.json');
|
|
78
|
+
if (!fs.existsSync(filePath)) return [];
|
|
79
|
+
try {
|
|
80
|
+
const raw = fs.readFileSync(filePath, 'utf-8');
|
|
81
|
+
const parsed = JSON.parse(raw);
|
|
82
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
83
|
+
} catch {
|
|
84
|
+
return [];
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function getAllPlaybooks(): Playbook[] {
|
|
89
|
+
return [...BUILTIN_PLAYBOOKS, ...getCustomPlaybooks()];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export async function runPlaybook(playbookId: string): Promise<boolean> {
|
|
93
|
+
const all = getAllPlaybooks();
|
|
94
|
+
const playbook = all.find((p) => p.id === playbookId || p.name.toLowerCase() === playbookId.toLowerCase());
|
|
95
|
+
|
|
96
|
+
if (!playbook) {
|
|
97
|
+
console.error(chalk.red(`\n ✖ Unknown playbook: '${playbookId}'`));
|
|
98
|
+
console.log(chalk.dim(` Available playbooks: ${all.map((p) => p.id).join(', ')}\n`));
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
console.log(chalk.bold(`\n 🔥 pyre playbook: ${chalk.hex('#ff6a39')(playbook.name)}`));
|
|
103
|
+
console.log(chalk.dim(` ${playbook.description}`));
|
|
104
|
+
console.log(chalk.dim(` Steps (${playbook.steps.length}):`));
|
|
105
|
+
playbook.steps.forEach((step, idx) => {
|
|
106
|
+
console.log(chalk.dim(` ${idx + 1}. ${step.name} (pyre ${step.command})`));
|
|
107
|
+
});
|
|
108
|
+
console.log(chalk.dim(' ─────────────────────────────────────────────────────────────\n'));
|
|
109
|
+
|
|
110
|
+
const scriptPath = process.argv[1];
|
|
111
|
+
const startTime = Date.now();
|
|
112
|
+
let stepIndex = 1;
|
|
113
|
+
let allSuccess = true;
|
|
114
|
+
|
|
115
|
+
for (const step of playbook.steps) {
|
|
116
|
+
console.log(chalk.bold.cyan(`\n▶ [${stepIndex}/${playbook.steps.length}] ${step.name} ...`));
|
|
117
|
+
console.log(chalk.dim(` $ pyre ${step.command}\n`));
|
|
118
|
+
|
|
119
|
+
const tokens = step.command.trim().split(/\s+/).filter(Boolean);
|
|
120
|
+
const result = spawnSync(process.argv[0], [scriptPath, ...tokens], {
|
|
121
|
+
stdio: 'inherit',
|
|
122
|
+
env: process.env,
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
if (result.status !== 0 && !step.allowFailure) {
|
|
126
|
+
console.log(chalk.red(`\n ✖ Step '${step.name}' failed with exit code ${result.status}`));
|
|
127
|
+
allSuccess = false;
|
|
128
|
+
break;
|
|
129
|
+
} else if (result.status !== 0 && step.allowFailure) {
|
|
130
|
+
console.log(chalk.yellow(`\n ⚠ Step '${step.name}' failed but marked as optional — continuing...`));
|
|
131
|
+
} else {
|
|
132
|
+
console.log(chalk.green(`\n ✔ Step '${step.name}' completed.`));
|
|
133
|
+
}
|
|
134
|
+
stepIndex++;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const durationSec = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
138
|
+
console.log(chalk.dim('\n ─────────────────────────────────────────────────────────────'));
|
|
139
|
+
if (allSuccess) {
|
|
140
|
+
console.log(chalk.bold.green(` ✔ Playbook '${playbook.name}' completed successfully in ${durationSec}s\n`));
|
|
141
|
+
} else {
|
|
142
|
+
console.log(chalk.bold.red(` ✖ Playbook '${playbook.name}' aborted after ${durationSec}s\n`));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return allSuccess;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function listPlaybooks(): void {
|
|
149
|
+
const all = getAllPlaybooks();
|
|
150
|
+
console.log(chalk.bold('\n 🔥 pyre Playbooks & Macro Recipes:\n'));
|
|
151
|
+
for (const p of all) {
|
|
152
|
+
console.log(` • ${chalk.bold.hex('#ff6a39')(p.id.padEnd(12))} ${p.name}`);
|
|
153
|
+
console.log(chalk.dim(` ${p.description}`));
|
|
154
|
+
console.log(chalk.dim(` Steps: ${p.steps.map((s) => s.command).join(' ➔ ')}\n`));
|
|
155
|
+
}
|
|
156
|
+
}
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { execSync } from 'node:child_process';
|
|
7
|
+
import { FlightRecorder, FlightFrame } from './flightRecorder.js';
|
|
8
|
+
import { BUILTIN_PLAYBOOKS } from './playbookCmd.js';
|
|
9
|
+
import { parseBytesString, formatBytes, runWithBudget } from './budgetCmd.js';
|
|
10
|
+
import { formatStatusline, StatuslineMetrics } from './statuslineCmd.js';
|
|
11
|
+
import { detectContainerEngine, collectContainerStats } from './containersCmd.js';
|
|
12
|
+
import { inspectProcess } from './inspectCmd.js';
|
|
13
|
+
import { generateXbarPlugin } from './xbar.js';
|
|
14
|
+
|
|
15
|
+
describe('Power-User Features', () => {
|
|
16
|
+
describe('In-Memory Flight Recorder (Black Box)', () => {
|
|
17
|
+
it('should push telemetry frames and respect max capacity', () => {
|
|
18
|
+
const recorder = new FlightRecorder(3);
|
|
19
|
+
assert.strictEqual(recorder.size(), 0);
|
|
20
|
+
|
|
21
|
+
const makeFrame = (cpuUsage: number, memUsage: number): FlightFrame => ({
|
|
22
|
+
timestamp: new Date().toISOString(),
|
|
23
|
+
cpu: { usage: cpuUsage, loadAvg: [1, 1, 1], temp: 45 },
|
|
24
|
+
memory: { usagePercent: memUsage, usedBytes: 1024 * 1024 * 500, totalBytes: 1024 * 1024 * 1000 },
|
|
25
|
+
network: { rxPackets: 100, txPackets: 80 },
|
|
26
|
+
topProcesses: [
|
|
27
|
+
{ pid: 101, name: 'node', cpu: cpuUsage, mem: memUsage },
|
|
28
|
+
{ pid: 102, name: 'ffmpeg', cpu: 12.5, mem: 5.0 },
|
|
29
|
+
],
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
recorder.push(makeFrame(20, 30));
|
|
33
|
+
recorder.push(makeFrame(50, 60));
|
|
34
|
+
recorder.push(makeFrame(85, 75));
|
|
35
|
+
assert.strictEqual(recorder.size(), 3);
|
|
36
|
+
|
|
37
|
+
// Pushing 4th item should evict first frame (usage 20)
|
|
38
|
+
recorder.push(makeFrame(95, 80));
|
|
39
|
+
assert.strictEqual(recorder.size(), 3);
|
|
40
|
+
|
|
41
|
+
const frames = recorder.getFrames();
|
|
42
|
+
assert.strictEqual(frames[0].cpu.usage, 50);
|
|
43
|
+
assert.strictEqual(frames[2].cpu.usage, 95);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('should generate structured forensic report with peak metrics', () => {
|
|
47
|
+
const recorder = new FlightRecorder(10);
|
|
48
|
+
recorder.push({
|
|
49
|
+
timestamp: '2026-10-12T10:00:00.000Z',
|
|
50
|
+
cpu: { usage: 40, loadAvg: [1.2], temp: 50 },
|
|
51
|
+
memory: { usagePercent: 55, usedBytes: 4000, totalBytes: 8000 },
|
|
52
|
+
network: { rxPackets: 10, txPackets: 5 },
|
|
53
|
+
topProcesses: [{ pid: 100, name: 'worker1', cpu: 35, mem: 20 }],
|
|
54
|
+
});
|
|
55
|
+
recorder.push({
|
|
56
|
+
timestamp: '2026-10-12T10:00:05.000Z',
|
|
57
|
+
cpu: { usage: 98, loadAvg: [3.5], temp: 88 },
|
|
58
|
+
memory: { usagePercent: 82, usedBytes: 7000, totalBytes: 8000 },
|
|
59
|
+
network: { rxPackets: 50, txPackets: 40 },
|
|
60
|
+
topProcesses: [
|
|
61
|
+
{ pid: 200, name: 'runaway_app', cpu: 92, mem: 65 },
|
|
62
|
+
{ pid: 100, name: 'worker1', cpu: 5, mem: 15 },
|
|
63
|
+
],
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const report = recorder.generateReport();
|
|
67
|
+
assert.strictEqual(report.frameCount, 2);
|
|
68
|
+
assert.strictEqual(report.timeSpanSec, 5);
|
|
69
|
+
assert.strictEqual(report.summary.peakCpuUsage, 98);
|
|
70
|
+
assert.strictEqual(report.summary.peakMemUsage, 82);
|
|
71
|
+
assert.strictEqual(report.summary.peakTempC, 88);
|
|
72
|
+
assert.strictEqual(report.summary.highestCpuProcess?.name, 'runaway_app');
|
|
73
|
+
assert.strictEqual(report.summary.highestCpuProcess?.pid, 200);
|
|
74
|
+
assert.strictEqual(report.summary.highestMemProcess?.name, 'runaway_app');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('should dump forensic report to file', () => {
|
|
78
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pyre-flight-test-'));
|
|
79
|
+
const recorder = new FlightRecorder(5);
|
|
80
|
+
recorder.push({
|
|
81
|
+
timestamp: new Date().toISOString(),
|
|
82
|
+
cpu: { usage: 10, loadAvg: [0.5] },
|
|
83
|
+
memory: { usagePercent: 20, usedBytes: 200, totalBytes: 1000 },
|
|
84
|
+
network: { rxPackets: 1, txPackets: 1 },
|
|
85
|
+
topProcesses: [{ pid: 1, name: 'init', cpu: 1, mem: 1 }],
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const dumpPath = recorder.dump(tmpDir);
|
|
89
|
+
assert.ok(fs.existsSync(dumpPath));
|
|
90
|
+
const parsed = JSON.parse(fs.readFileSync(dumpPath, 'utf-8'));
|
|
91
|
+
assert.strictEqual(parsed.frameCount, 1);
|
|
92
|
+
assert.strictEqual(parsed.summary.peakCpuUsage, 10);
|
|
93
|
+
|
|
94
|
+
// Cleanup
|
|
95
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe('Multi-Step Playbooks & Recipes', () => {
|
|
100
|
+
it('should provide built-in playbooks for audit, clean, triage, and bench', () => {
|
|
101
|
+
assert.ok(BUILTIN_PLAYBOOKS.length >= 4);
|
|
102
|
+
const ids = BUILTIN_PLAYBOOKS.map((p) => p.id);
|
|
103
|
+
assert.ok(ids.includes('audit'));
|
|
104
|
+
assert.ok(ids.includes('clean'));
|
|
105
|
+
assert.ok(ids.includes('triage'));
|
|
106
|
+
assert.ok(ids.includes('bench'));
|
|
107
|
+
|
|
108
|
+
for (const pb of BUILTIN_PLAYBOOKS) {
|
|
109
|
+
assert.ok(pb.name.length > 0);
|
|
110
|
+
assert.ok(pb.description.length > 0);
|
|
111
|
+
assert.ok(Array.isArray(pb.steps));
|
|
112
|
+
assert.ok(pb.steps.length > 0);
|
|
113
|
+
for (const s of pb.steps) {
|
|
114
|
+
assert.ok(s.name.length > 0);
|
|
115
|
+
assert.ok(s.command.length > 0);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
describe('Performance Budgeting Runner (pyre run)', () => {
|
|
122
|
+
it('should parse human-readable byte strings accurately', () => {
|
|
123
|
+
assert.strictEqual(parseBytesString('500MB'), 500 * 1024 * 1024);
|
|
124
|
+
assert.strictEqual(parseBytesString('500m'), 500 * 1024 * 1024);
|
|
125
|
+
assert.strictEqual(parseBytesString('2GB'), 2 * 1024 * 1024 * 1024);
|
|
126
|
+
assert.strictEqual(parseBytesString('64KB'), 64 * 1024);
|
|
127
|
+
assert.strictEqual(parseBytesString('1024'), 1024);
|
|
128
|
+
assert.strictEqual(parseBytesString('invalid'), 0);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('should format bytes cleanly', () => {
|
|
132
|
+
assert.strictEqual(formatBytes(1024 * 1024 * 1024 * 1.5), '1.50 GB');
|
|
133
|
+
assert.strictEqual(formatBytes(1024 * 1024 * 250), '250.00 MB');
|
|
134
|
+
assert.strictEqual(formatBytes(1024 * 45), '45.0 KB');
|
|
135
|
+
assert.strictEqual(formatBytes(512), '512 B');
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('should execute command and track duration and exit code', async () => {
|
|
139
|
+
const res = await runWithBudget([process.execPath, '-e', 'process.exit(0)'], {});
|
|
140
|
+
assert.strictEqual(res.exitCode, 0);
|
|
141
|
+
assert.ok(res.durationSec >= 0);
|
|
142
|
+
assert.strictEqual(res.violations.length, 0);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('should detect budget violations when thresholds are exceeded', async () => {
|
|
146
|
+
// Artificial violation: 0.0001 seconds time budget for a 50ms command
|
|
147
|
+
const res = await runWithBudget(
|
|
148
|
+
[process.execPath, '-e', 'setTimeout(() => {}, 50)'],
|
|
149
|
+
{ maxTimeSec: 0.0001 }
|
|
150
|
+
);
|
|
151
|
+
assert.ok(res.violations.length > 0);
|
|
152
|
+
assert.ok(res.violations.some((v) => v.includes('Runtime exceeded budget')));
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
describe('Headless Terminal Statusline Emitter', () => {
|
|
157
|
+
const mockMetrics: StatuslineMetrics = {
|
|
158
|
+
cpuUsage: 14.2,
|
|
159
|
+
memUsage: 62.8,
|
|
160
|
+
tempC: 48.0,
|
|
161
|
+
batteryPct: 95,
|
|
162
|
+
batteryCharging: true,
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
it('should format statusline for JSON', () => {
|
|
166
|
+
const json = formatStatusline(mockMetrics, 'json');
|
|
167
|
+
const parsed = JSON.parse(json);
|
|
168
|
+
assert.strictEqual(parsed.cpu, 14.2);
|
|
169
|
+
assert.strictEqual(parsed.mem, 62.8);
|
|
170
|
+
assert.strictEqual(parsed.temp, 48.0);
|
|
171
|
+
assert.strictEqual(parsed.battery, 95);
|
|
172
|
+
assert.strictEqual(parsed.charging, true);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('should format statusline for tmux with ANSI/tmux color styling', () => {
|
|
176
|
+
const tmux = formatStatusline(mockMetrics, 'tmux');
|
|
177
|
+
assert.ok(tmux.includes('#[fg=cyan]CPU: 14%'));
|
|
178
|
+
assert.ok(tmux.includes('#[fg=magenta]RAM: 63%'));
|
|
179
|
+
assert.ok(tmux.includes('#[fg=yellow]48°C'));
|
|
180
|
+
assert.ok(tmux.includes('⚡95%'));
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('should format statusline for starship and plain text', () => {
|
|
184
|
+
const plain = formatStatusline(mockMetrics, 'plain');
|
|
185
|
+
assert.strictEqual(plain, '🔥 14% | 63% | 48°C | ⚡95%');
|
|
186
|
+
|
|
187
|
+
const starship = formatStatusline(mockMetrics, 'starship');
|
|
188
|
+
assert.ok(starship.includes('14%'));
|
|
189
|
+
assert.ok(starship.includes('63%'));
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('should format statusline for pango markup', () => {
|
|
193
|
+
const pango = formatStatusline(mockMetrics, 'pango');
|
|
194
|
+
assert.ok(pango.includes('<span color='));
|
|
195
|
+
assert.ok(pango.includes('CPU: 14%'));
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
describe('Container & Micro-VM Telemetry', () => {
|
|
200
|
+
it('should detect container engine or return null safely', () => {
|
|
201
|
+
const engine = detectContainerEngine();
|
|
202
|
+
assert.ok(engine === 'docker' || engine === 'podman' || engine === null);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('should collect container stats safely without throwing', () => {
|
|
206
|
+
const stats = collectContainerStats();
|
|
207
|
+
assert.ok(Array.isArray(stats));
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
describe('Deep-Dive Process Inspector', () => {
|
|
212
|
+
it('should inspect current node process metadata', () => {
|
|
213
|
+
const details = inspectProcess(process.pid);
|
|
214
|
+
assert.strictEqual(details.pid, process.pid);
|
|
215
|
+
assert.ok(details.name.length > 0);
|
|
216
|
+
assert.ok(details.threads >= 1);
|
|
217
|
+
assert.ok(details.memRssBytes > 0);
|
|
218
|
+
assert.ok(Array.isArray(details.openFiles));
|
|
219
|
+
assert.ok(Array.isArray(details.sockets));
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('should throw an informative error when inspecting non-existent PID', () => {
|
|
223
|
+
assert.throws(() => {
|
|
224
|
+
inspectProcess(999999999);
|
|
225
|
+
}, /not found or permission denied/);
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
describe('Temperature Unit Support', () => {
|
|
230
|
+
it('should format temperatures with custom units without reference errors', () => {
|
|
231
|
+
const outC = execSync(`node dist/index.js --once --temp-unit c`, { encoding: 'utf-8' });
|
|
232
|
+
assert.ok(outC.includes('PYRE'));
|
|
233
|
+
const outF = execSync(`node dist/index.js --once --temp-unit f`, { encoding: 'utf-8' });
|
|
234
|
+
assert.ok(outF.includes('PYRE'));
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
describe('Menu Bar Plugin Generator (pyre xbar)', () => {
|
|
239
|
+
it('should generate xbar plugin script into a specified directory', async () => {
|
|
240
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pyre-xbar-test-'));
|
|
241
|
+
const outPath = await generateXbarPlugin({ dir: tmpDir, install: true });
|
|
242
|
+
assert.ok(fs.existsSync(outPath));
|
|
243
|
+
const content = fs.readFileSync(outPath, 'utf-8');
|
|
244
|
+
assert.ok(content.includes('# <xbar.title>Pyre Monitor</xbar.title>'));
|
|
245
|
+
assert.ok(content.includes('--once --json'));
|
|
246
|
+
|
|
247
|
+
// Clean up
|
|
248
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
});
|