pyre-cli 9.5.1 → 10.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +84 -14
- package/dist/alertChannels-TPSQIJ7S.js +8 -0
- package/dist/blender-FGH3ASEQ.js +7 -0
- package/dist/brewHealth-P2GW5Y5C.js +176 -0
- package/dist/buildCmd-CDPZ2ZFA.js +38 -0
- package/dist/{checkCmd-EWFNLBWE.js → checkCmd-RKW6QGDR.js} +10 -11
- package/dist/{chunk-RVGKEJXT.js → chunk-53O2RGSL.js} +47 -17
- package/dist/{chunk-MOAEGMFP.js → chunk-6UHZP4QP.js} +204 -46
- package/dist/chunk-GHRDW646.js +2506 -0
- package/dist/{prometheus-LFYHF6VO.js → chunk-HT4VQ4OH.js} +1 -0
- package/dist/chunk-M73HCQD2.js +95 -0
- package/dist/chunk-MZO5QKUR.js +32 -0
- package/dist/chunk-VIACN2RU.js +133 -0
- package/dist/chunk-Z2YV5HKV.js +87 -0
- package/dist/chunk-ZLVIVFSO.js +110 -0
- package/dist/{collectors-WD6VKH6M.js → collectors-H43PN4PS.js} +8 -2
- package/dist/{completions-VQMHQCXE.js → completions-JKYEJNGR.js} +34 -1
- package/dist/{config-4ZLF4HTS.js → config-GYGJXU2Q.js} +9 -1
- package/dist/doctor-GYHFTNZQ.js +387 -0
- package/dist/explainCmd-MF6PIMLW.js +222 -0
- package/dist/extensions-XJGHG7X3.js +134 -0
- package/dist/index.js +841 -206
- package/dist/ioreport.py +72 -0
- package/dist/netUsageCmd-WKBTW2YG.js +90 -0
- package/dist/{pipeCmd-7THXEGDH.js → pipeCmd-7REDBLBU.js} +5 -3
- package/dist/profileProcCmd-N4H6MCBT.js +107 -0
- package/dist/prometheus-5BL5PVLC.js +6 -0
- package/dist/prometheusCmd-PL64BLN5.js +93 -0
- package/dist/replayCmd-Z3PYXY7V.js +164 -0
- package/dist/run-HOGQDDTA.js +8 -0
- package/dist/serveCmd-2H5VLF3P.js +153 -0
- package/dist/service-Q74BBILO.js +181 -0
- package/dist/smartCmd-ODWKSFNG.js +166 -0
- package/dist/syncCmd-A2W3QB3S.js +126 -0
- package/dist/topoCmd-DU2HHZNH.js +148 -0
- package/dist/watchdogCmd-UF53JPZB.js +161 -0
- package/dist/{wizard-GVIAHIVV.js → wizard-TD7BGX6O.js} +1 -1
- package/install.ps1 +29 -0
- package/install.sh +10 -9
- package/package.json +7 -3
- package/src/ai/models.ts +138 -0
- package/src/alertChannels.ts +119 -0
- package/src/anomalies.ts +114 -60
- package/src/brewHealth.ts +157 -40
- package/src/buildCmd.ts +48 -0
- package/src/checkCmd.ts +5 -5
- package/src/completions.ts +35 -0
- package/src/doctor.ts +235 -7
- package/src/easy_features.test.ts +40 -0
- package/src/explainCmd.ts +261 -0
- package/src/extensions.ts +71 -7
- package/src/formatters/index.ts +1 -1
- package/src/formatters/output.ts +100 -13
- package/src/formatters/render.ts +274 -38
- package/src/formatters/themes.ts +8 -0
- package/src/formatters/types.ts +6 -1
- package/src/index.ts +298 -56
- package/src/live/input.ts +122 -4
- package/src/live/render.ts +98 -0
- package/src/live/state.ts +70 -52
- package/src/live/types.ts +2 -2
- package/src/monitors/batteryPredictor.ts +143 -0
- package/src/monitors/blender.ts +144 -0
- package/src/monitors/buildTracker.ts +121 -0
- package/src/monitors/collectors.ts +558 -147
- package/src/monitors/dns_cache.ts +48 -0
- package/src/monitors/index.ts +3 -2
- package/src/monitors/ioreport.py +72 -0
- package/src/monitors/platform/linux.ts +756 -0
- package/src/monitors/platform/windows.ts +519 -0
- package/src/monitors/run.ts +27 -3
- package/src/monitors/smc.ts +21 -1
- package/src/monitors/types.ts +112 -14
- package/src/netUsageCmd.ts +121 -0
- package/src/platform.test.ts +99 -0
- package/src/plugins/index.ts +143 -0
- package/src/profileProcCmd.ts +133 -0
- package/src/prometheusCmd.ts +107 -0
- package/src/replayCmd.ts +186 -0
- package/src/serveCmd.ts +180 -0
- package/src/service.ts +125 -7
- package/src/smartCmd.ts +205 -0
- package/src/state/config.ts +64 -17
- package/src/syncCmd.ts +166 -0
- package/src/topoCmd.ts +176 -0
- package/src/watchdogCmd.ts +198 -0
- package/src/xbar.ts +22 -11
- package/dist/brewHealth-F2BXF5M6.js +0 -74
- package/dist/chunk-FH5WDJBF.js +0 -15
- package/dist/chunk-HGRBBPYX.js +0 -937
- package/dist/doctor-L6FZYA7F.js +0 -184
- package/dist/extensions-DVA5YLGH.js +0 -74
- package/dist/service-WTS3NBQU.js +0 -73
- /package/dist/{chunk-SG26Y3WF.js → chunk-IM3IQMMT.js} +0 -0
package/src/monitors/types.ts
CHANGED
|
@@ -21,6 +21,20 @@ export interface StatsData {
|
|
|
21
21
|
timestamp: string;
|
|
22
22
|
packets: PacketData | null;
|
|
23
23
|
tasks: TaskData[];
|
|
24
|
+
containers?: ContainerData[];
|
|
25
|
+
blenderRenders?: BlenderRenderData[];
|
|
26
|
+
activeBuilds?: import('./buildTracker.js').ActiveBuild[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface ContainerData {
|
|
30
|
+
id: string;
|
|
31
|
+
name: string;
|
|
32
|
+
cpuPercent: number;
|
|
33
|
+
memUsage: string;
|
|
34
|
+
memPercent: number;
|
|
35
|
+
netIO: string;
|
|
36
|
+
blockIO: string;
|
|
37
|
+
pids: number;
|
|
24
38
|
}
|
|
25
39
|
|
|
26
40
|
export interface GpuData {
|
|
@@ -38,18 +52,20 @@ export interface GpuData {
|
|
|
38
52
|
combinedWatts?: number;
|
|
39
53
|
}
|
|
40
54
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
55
|
+
export interface PacketData {
|
|
56
|
+
totalPackets: number;
|
|
57
|
+
rxPackets: number;
|
|
58
|
+
txPackets: number;
|
|
59
|
+
rxRate: number;
|
|
60
|
+
txRate: number;
|
|
61
|
+
connections: number;
|
|
62
|
+
topProcesses: NetworkProcess[];
|
|
63
|
+
allProcesses: NetworkProcess[];
|
|
64
|
+
interfaces: { iface: string; rxPackets: number; txPackets: number; rxBytes: number; txBytes: number }[];
|
|
65
|
+
protocolStats?: ProtocolStats;
|
|
66
|
+
connectionStates?: ConnectionStateStats;
|
|
67
|
+
listeningPorts?: number[];
|
|
68
|
+
}
|
|
53
69
|
|
|
54
70
|
export interface TaskData {
|
|
55
71
|
pid: number;
|
|
@@ -96,12 +112,25 @@ export interface MemoryData {
|
|
|
96
112
|
pressureLevel?: string;
|
|
97
113
|
}
|
|
98
114
|
|
|
115
|
+
export interface FanData {
|
|
116
|
+
/** Fan index (0-based) */
|
|
117
|
+
id: number;
|
|
118
|
+
/** Current fan speed in RPM */
|
|
119
|
+
rpm: number;
|
|
120
|
+
/** Minimum fan speed in RPM (from SMC), when available */
|
|
121
|
+
minRpm?: number;
|
|
122
|
+
/** Maximum fan speed in RPM (from SMC), when available */
|
|
123
|
+
maxRpm?: number;
|
|
124
|
+
}
|
|
125
|
+
|
|
99
126
|
export interface ThermalData {
|
|
100
127
|
state: string;
|
|
101
128
|
detail?: string;
|
|
102
129
|
/** Normalized 0-3 scale (nominal/fair/serious/critical) derived from `state`, for graphing/coloring */
|
|
103
130
|
pressureLevel: number;
|
|
104
131
|
temperatures?: Record<string, number | null>;
|
|
132
|
+
/** Fan speeds from SMC (empty array when not available or no sudo) */
|
|
133
|
+
fans?: FanData[];
|
|
105
134
|
error?: string;
|
|
106
135
|
}
|
|
107
136
|
|
|
@@ -118,6 +147,7 @@ export interface BatteryData {
|
|
|
118
147
|
dischargeRatePerHour?: number;
|
|
119
148
|
powerWatts?: number;
|
|
120
149
|
friendlySummary?: string;
|
|
150
|
+
smartPrediction?: import('./batteryPredictor.js').SmartBatteryPrediction;
|
|
121
151
|
}
|
|
122
152
|
|
|
123
153
|
export interface DiskData {
|
|
@@ -138,10 +168,60 @@ export interface NetworkData {
|
|
|
138
168
|
txBytes: number;
|
|
139
169
|
rxPackets: number;
|
|
140
170
|
txPackets: number;
|
|
171
|
+
rxRate: number;
|
|
172
|
+
txRate: number;
|
|
141
173
|
rxPacketsPerSec?: number;
|
|
142
174
|
txPacketsPerSec?: number;
|
|
143
175
|
connections?: number;
|
|
144
176
|
topProcesses?: NetworkProcess[];
|
|
177
|
+
protocols?: ProtocolStats;
|
|
178
|
+
connectionStates?: ConnectionStateStats;
|
|
179
|
+
listeningPorts?: number[];
|
|
180
|
+
establishedConnections?: number;
|
|
181
|
+
topRemoteHosts?: RemoteHostInfo[];
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export interface ProtocolStats {
|
|
185
|
+
tcp: { connections: number; rxBytes: number; txBytes: number };
|
|
186
|
+
udp: { connections: number; rxBytes: number; txBytes: number };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export interface ConnectionStateStats {
|
|
190
|
+
established: number;
|
|
191
|
+
listening: number;
|
|
192
|
+
timeWait: number;
|
|
193
|
+
closeWait: number;
|
|
194
|
+
other: number;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export interface RemoteHostInfo {
|
|
198
|
+
host: string;
|
|
199
|
+
ip: string;
|
|
200
|
+
country?: string;
|
|
201
|
+
city?: string;
|
|
202
|
+
asn?: string;
|
|
203
|
+
bytesTransferred: number;
|
|
204
|
+
connectionCount: number;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export interface DnsQuery {
|
|
208
|
+
timestamp: number;
|
|
209
|
+
domain: string;
|
|
210
|
+
type: string;
|
|
211
|
+
process: string;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export interface NetworkConnection {
|
|
215
|
+
protocol: string;
|
|
216
|
+
local: string;
|
|
217
|
+
remote: string;
|
|
218
|
+
state: string;
|
|
219
|
+
rxBytes: number;
|
|
220
|
+
txBytes: number;
|
|
221
|
+
remoteHost?: string;
|
|
222
|
+
country?: string;
|
|
223
|
+
city?: string;
|
|
224
|
+
asn?: string;
|
|
145
225
|
}
|
|
146
226
|
|
|
147
227
|
export interface NetworkProcess {
|
|
@@ -149,8 +229,11 @@ export interface NetworkProcess {
|
|
|
149
229
|
command: string;
|
|
150
230
|
rxBytes: number;
|
|
151
231
|
txBytes: number;
|
|
152
|
-
|
|
153
|
-
|
|
232
|
+
rxRate?: number;
|
|
233
|
+
txRate?: number;
|
|
234
|
+
rxPackets?: number;
|
|
235
|
+
txPackets?: number;
|
|
236
|
+
connections?: NetworkConnection[];
|
|
154
237
|
}
|
|
155
238
|
|
|
156
239
|
export interface ProcessData {
|
|
@@ -164,4 +247,19 @@ export interface ProcessData {
|
|
|
164
247
|
threads: number;
|
|
165
248
|
runtime: number;
|
|
166
249
|
isGpuOrMlAttributed?: boolean;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export interface BlenderRenderData {
|
|
253
|
+
pid: number;
|
|
254
|
+
blendFile: string;
|
|
255
|
+
renderEngine: string;
|
|
256
|
+
frameStart: number;
|
|
257
|
+
frameEnd: number;
|
|
258
|
+
currentFrame: number;
|
|
259
|
+
totalFrames: number;
|
|
260
|
+
elapsedSec: number;
|
|
261
|
+
completionPercent: number;
|
|
262
|
+
status: 'rendering' | 'finalizing' | 'loading' | 'idle' | 'unknown';
|
|
263
|
+
outputPath: string;
|
|
264
|
+
sampleCount?: number;
|
|
167
265
|
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pyre netusage — Per-application network usage tracker.
|
|
3
|
+
*
|
|
4
|
+
* Uses macOS socket accounting and nettop parsing from collectPackets
|
|
5
|
+
* to produce detailed real-time and cumulative network metrics per process.
|
|
6
|
+
*/
|
|
7
|
+
import chalk from 'chalk';
|
|
8
|
+
import { collectPackets } from './monitors/collectors.js';
|
|
9
|
+
import { formatBytes } from './formatters/render.js';
|
|
10
|
+
|
|
11
|
+
export interface NetUsageOptions {
|
|
12
|
+
top?: number;
|
|
13
|
+
sort?: 'rx' | 'tx' | 'total';
|
|
14
|
+
json?: boolean;
|
|
15
|
+
interval?: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function runNetUsageCommand(opts: NetUsageOptions = {}): Promise<void> {
|
|
19
|
+
const topCount = opts.top || 15;
|
|
20
|
+
const sortKey = opts.sort || 'total';
|
|
21
|
+
|
|
22
|
+
const packetData = await collectPackets();
|
|
23
|
+
if (!packetData || !packetData.allProcesses || packetData.allProcesses.length === 0) {
|
|
24
|
+
if (opts.json) {
|
|
25
|
+
console.log(JSON.stringify({ processes: [], totalRx: 0, totalTx: 0 }));
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
console.log(chalk.yellow('\n ⚠ No active per-application network sockets detected.'));
|
|
29
|
+
console.log(chalk.dim(' Ensure network activity is present or run with elevated permissions if required.\n'));
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Aggregate by process command / name
|
|
34
|
+
interface AggregatedProc {
|
|
35
|
+
pid: number;
|
|
36
|
+
command: string;
|
|
37
|
+
rxBytes: number;
|
|
38
|
+
txBytes: number;
|
|
39
|
+
totalBytes: number;
|
|
40
|
+
connections: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const appMap = new Map<string, AggregatedProc>();
|
|
44
|
+
|
|
45
|
+
for (const proc of packetData.allProcesses) {
|
|
46
|
+
const key = proc.command || `pid-${proc.pid}`;
|
|
47
|
+
const rx = proc.rxBytes || 0;
|
|
48
|
+
const tx = proc.txBytes || 0;
|
|
49
|
+
const conns = proc.connections ? proc.connections.length : 0;
|
|
50
|
+
|
|
51
|
+
const existing = appMap.get(key);
|
|
52
|
+
if (existing) {
|
|
53
|
+
existing.rxBytes += rx;
|
|
54
|
+
existing.txBytes += tx;
|
|
55
|
+
existing.totalBytes += (rx + tx);
|
|
56
|
+
existing.connections += conns;
|
|
57
|
+
} else {
|
|
58
|
+
appMap.set(key, {
|
|
59
|
+
pid: proc.pid,
|
|
60
|
+
command: key,
|
|
61
|
+
rxBytes: rx,
|
|
62
|
+
txBytes: tx,
|
|
63
|
+
totalBytes: rx + tx,
|
|
64
|
+
connections: conns,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const sorted = Array.from(appMap.values()).sort((a, b) => {
|
|
70
|
+
if (sortKey === 'rx') return b.rxBytes - a.rxBytes;
|
|
71
|
+
if (sortKey === 'tx') return b.txBytes - a.txBytes;
|
|
72
|
+
return b.totalBytes - a.totalBytes;
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const displayList = sorted.slice(0, topCount);
|
|
76
|
+
|
|
77
|
+
if (opts.json) {
|
|
78
|
+
console.log(JSON.stringify({
|
|
79
|
+
totalRx: packetData.rxPackets,
|
|
80
|
+
totalTx: packetData.txPackets,
|
|
81
|
+
activeConnections: packetData.connections,
|
|
82
|
+
processes: displayList,
|
|
83
|
+
}, null, 2));
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
console.log(chalk.bold('\n 🔥 pyre netusage — Per-Application Network Traffic\n'));
|
|
88
|
+
|
|
89
|
+
const pidW = 8;
|
|
90
|
+
const appW = 28;
|
|
91
|
+
const rxW = 14;
|
|
92
|
+
const txW = 14;
|
|
93
|
+
const totalW = 14;
|
|
94
|
+
const connsW = 8;
|
|
95
|
+
|
|
96
|
+
const header =
|
|
97
|
+
chalk.bold.hex('#f8f8f2').bgHex('#44475a')(' PID ') +
|
|
98
|
+
chalk.bold.hex('#f8f8f2').bgHex('#44475a')(' APPLICATION ') +
|
|
99
|
+
chalk.bold.hex('#f8f8f2').bgHex('#44475a')(' DOWN (RX) ') +
|
|
100
|
+
chalk.bold.hex('#f8f8f2').bgHex('#44475a')(' UP (TX) ') +
|
|
101
|
+
chalk.bold.hex('#f8f8f2').bgHex('#44475a')(' TOTAL ') +
|
|
102
|
+
chalk.bold.hex('#f8f8f2').bgHex('#44475a')(' CONNS ');
|
|
103
|
+
|
|
104
|
+
console.log(' ' + header);
|
|
105
|
+
console.log(' ' + chalk.dim('─'.repeat(pidW + appW + rxW + txW + totalW + connsW)));
|
|
106
|
+
|
|
107
|
+
for (const p of displayList) {
|
|
108
|
+
const pidStr = String(p.pid).padEnd(pidW);
|
|
109
|
+
const appStr = (p.command.length > appW - 2 ? p.command.slice(0, appW - 3) + '…' : p.command).padEnd(appW);
|
|
110
|
+
const rxStr = chalk.green(formatBytes(p.rxBytes).padStart(rxW - 2)) + ' ';
|
|
111
|
+
const txStr = chalk.cyan(formatBytes(p.txBytes).padStart(txW - 2)) + ' ';
|
|
112
|
+
const totStr = chalk.bold(formatBytes(p.totalBytes).padStart(totalW - 2)) + ' ';
|
|
113
|
+
const connsStr = String(p.connections).padStart(connsW - 2);
|
|
114
|
+
|
|
115
|
+
console.log(` ${pidStr}${appStr}${rxStr}${txStr}${totStr}${connsStr}`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
console.log();
|
|
119
|
+
console.log(chalk.dim(` Total active socket connections: ${chalk.bold(String(packetData.connections))} | Showing top ${displayList.length} apps`));
|
|
120
|
+
console.log();
|
|
121
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
|
|
4
|
+
describe('Linux Telemetry Parsers', () => {
|
|
5
|
+
it('should parse Linux memory info structure correctly', async () => {
|
|
6
|
+
const { collectLinuxMemory } = await import('./monitors/platform/linux.js');
|
|
7
|
+
const mem = await collectLinuxMemory();
|
|
8
|
+
assert.ok(typeof mem.total === 'number');
|
|
9
|
+
assert.ok(typeof mem.used === 'number');
|
|
10
|
+
assert.ok(typeof mem.free === 'number');
|
|
11
|
+
assert.ok(typeof mem.usagePercent === 'number');
|
|
12
|
+
assert.ok(mem.pageSize === 4096);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('should collect Linux system info', async () => {
|
|
16
|
+
const { collectLinuxSystem } = await import('./monitors/platform/linux.js');
|
|
17
|
+
const sys = await collectLinuxSystem();
|
|
18
|
+
assert.ok(typeof sys.hostname === 'string');
|
|
19
|
+
assert.ok(typeof sys.os === 'string');
|
|
20
|
+
assert.ok(typeof sys.uptime === 'string');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('should collect Linux disk structure', async () => {
|
|
24
|
+
const { collectLinuxDisk } = await import('./monitors/platform/linux.js');
|
|
25
|
+
const disks = await collectLinuxDisk();
|
|
26
|
+
assert.ok(Array.isArray(disks));
|
|
27
|
+
assert.ok(disks.length > 0);
|
|
28
|
+
assert.ok(typeof disks[0].filesystem === 'string');
|
|
29
|
+
assert.ok(typeof disks[0].mountpoint === 'string');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should collect Linux thermal structure gracefully', async () => {
|
|
33
|
+
const { collectLinuxThermal } = await import('./monitors/platform/linux.js');
|
|
34
|
+
const thermal = await collectLinuxThermal();
|
|
35
|
+
assert.ok(typeof thermal.state === 'string');
|
|
36
|
+
assert.ok(typeof thermal.pressureLevel === 'number');
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe('Windows Telemetry Parsers', () => {
|
|
41
|
+
it('should collect Windows system info with Windows OS mapping', async () => {
|
|
42
|
+
const { collectWindowsSystem } = await import('./monitors/platform/windows.js');
|
|
43
|
+
const sys = await collectWindowsSystem();
|
|
44
|
+
assert.ok(typeof sys.hostname === 'string');
|
|
45
|
+
assert.ok(sys.os.startsWith('Windows'));
|
|
46
|
+
assert.ok(typeof sys.uptime === 'string');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should collect Windows memory structure', async () => {
|
|
50
|
+
const { collectWindowsMemory } = await import('./monitors/platform/windows.js');
|
|
51
|
+
const mem = await collectWindowsMemory();
|
|
52
|
+
assert.ok(typeof mem.total === 'number');
|
|
53
|
+
assert.ok(typeof mem.used === 'number');
|
|
54
|
+
assert.ok(typeof mem.free === 'number');
|
|
55
|
+
assert.ok(typeof mem.usagePercent === 'number');
|
|
56
|
+
assert.ok(mem.pageSize === 4096);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should collect Windows CPU structure with simulated load average', async () => {
|
|
60
|
+
const { collectWindowsCpu } = await import('./monitors/platform/windows.js');
|
|
61
|
+
const cpuRef = { current: null };
|
|
62
|
+
const cpu = await collectWindowsCpu(cpuRef);
|
|
63
|
+
assert.ok(typeof cpu.brand === 'string');
|
|
64
|
+
assert.ok(cpu.cores >= 1);
|
|
65
|
+
assert.ok(Array.isArray(cpu.loadAvg));
|
|
66
|
+
assert.strictEqual(cpu.loadAvg.length, 3);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should return Windows disk fallback structure', async () => {
|
|
70
|
+
const { collectWindowsDisk } = await import('./monitors/platform/windows.js');
|
|
71
|
+
const disks = await collectWindowsDisk();
|
|
72
|
+
assert.ok(Array.isArray(disks));
|
|
73
|
+
assert.ok(disks.length > 0);
|
|
74
|
+
assert.ok(disks[0].mountpoint.includes(':'));
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('should collect Windows thermal structure with pressure level', async () => {
|
|
78
|
+
const { collectWindowsThermal } = await import('./monitors/platform/windows.js');
|
|
79
|
+
const thermal = await collectWindowsThermal();
|
|
80
|
+
assert.ok(typeof thermal.state === 'string');
|
|
81
|
+
assert.ok([0, 1, 2, 3].includes(thermal.pressureLevel));
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe('Cross-Platform Service & Package Management', () => {
|
|
86
|
+
it('should report package manager health without crashing', async () => {
|
|
87
|
+
const { getBrewHealth } = await import('./brewHealth.js');
|
|
88
|
+
const health = await getBrewHealth();
|
|
89
|
+
assert.ok(typeof health.managerName === 'string');
|
|
90
|
+
assert.ok(typeof health.brewAvailable === 'boolean');
|
|
91
|
+
assert.ok(typeof health.installedCount === 'number');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('should query system extensions / drivers without crashing', async () => {
|
|
95
|
+
const { getSystemExtensions } = await import('./extensions.js');
|
|
96
|
+
const extensions = await getSystemExtensions();
|
|
97
|
+
assert.ok(Array.isArray(extensions));
|
|
98
|
+
});
|
|
99
|
+
});
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pyre Plugin System.
|
|
3
|
+
*
|
|
4
|
+
* Discovers and executes custom user-authored metric collectors from
|
|
5
|
+
* ~/.config/pyre/plugins/*.js (or .mjs).
|
|
6
|
+
*
|
|
7
|
+
* Plugin contract:
|
|
8
|
+
* export default {
|
|
9
|
+
* name: 'my-metric',
|
|
10
|
+
* title: 'My Custom Metric',
|
|
11
|
+
* intervalSec: 5, // optional refresh interval
|
|
12
|
+
* collect: async () => {
|
|
13
|
+
* return {
|
|
14
|
+
* title: 'Custom Title', // optional override
|
|
15
|
+
* lines: ['Metric 1: 42', 'Status: OK']
|
|
16
|
+
* };
|
|
17
|
+
* }
|
|
18
|
+
* };
|
|
19
|
+
*/
|
|
20
|
+
import fs from 'node:fs';
|
|
21
|
+
import path from 'node:path';
|
|
22
|
+
import { pathToFileURL } from 'node:url';
|
|
23
|
+
import { getConfigDir } from '../state/config.js';
|
|
24
|
+
|
|
25
|
+
export interface PyrePluginOutput {
|
|
26
|
+
title?: string;
|
|
27
|
+
lines: string[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface PyrePlugin {
|
|
31
|
+
name: string;
|
|
32
|
+
title?: string;
|
|
33
|
+
intervalSec?: number;
|
|
34
|
+
collect: () => Promise<PyrePluginOutput | string[] | Record<string, any>>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface LoadedPlugin {
|
|
38
|
+
plugin: PyrePlugin;
|
|
39
|
+
lastRun: number;
|
|
40
|
+
lastOutput: PyrePluginOutput;
|
|
41
|
+
error?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const loadedPlugins: Map<string, LoadedPlugin> = new Map();
|
|
45
|
+
|
|
46
|
+
export function getPluginsDir(): string {
|
|
47
|
+
return path.join(getConfigDir(), 'plugins');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function loadPlugins(): Promise<Map<string, LoadedPlugin>> {
|
|
51
|
+
const pluginsDir = getPluginsDir();
|
|
52
|
+
if (!fs.existsSync(pluginsDir)) {
|
|
53
|
+
try {
|
|
54
|
+
fs.mkdirSync(pluginsDir, { recursive: true });
|
|
55
|
+
} catch {
|
|
56
|
+
return loadedPlugins;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
let files: string[] = [];
|
|
61
|
+
try {
|
|
62
|
+
files = fs.readdirSync(pluginsDir).filter(f => f.endsWith('.js') || f.endsWith('.mjs'));
|
|
63
|
+
} catch {
|
|
64
|
+
return loadedPlugins;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
for (const file of files) {
|
|
68
|
+
const filePath = path.join(pluginsDir, file);
|
|
69
|
+
try {
|
|
70
|
+
const fileUrl = pathToFileURL(filePath).href;
|
|
71
|
+
const mod = await import(`${fileUrl}?t=${Date.now()}`);
|
|
72
|
+
const plugin: PyrePlugin = mod.default || mod;
|
|
73
|
+
if (plugin && typeof plugin.collect === 'function') {
|
|
74
|
+
const name = plugin.name || path.basename(file, path.extname(file));
|
|
75
|
+
if (!loadedPlugins.has(name)) {
|
|
76
|
+
loadedPlugins.set(name, {
|
|
77
|
+
plugin: { ...plugin, name },
|
|
78
|
+
lastRun: 0,
|
|
79
|
+
lastOutput: { title: plugin.title || name, lines: ['Collecting...'] },
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
} catch (err: any) {
|
|
84
|
+
const name = path.basename(file, path.extname(file));
|
|
85
|
+
loadedPlugins.set(name, {
|
|
86
|
+
plugin: { name, collect: async () => ({ lines: [`Error loading plugin: ${err.message}`] }) },
|
|
87
|
+
lastRun: Date.now(),
|
|
88
|
+
lastOutput: { title: name, lines: [`Failed to load: ${err.message}`] },
|
|
89
|
+
error: err.message,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return loadedPlugins;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export async function executePlugins(): Promise<LoadedPlugin[]> {
|
|
98
|
+
const now = Date.now();
|
|
99
|
+
const results: LoadedPlugin[] = [];
|
|
100
|
+
|
|
101
|
+
for (const [, item] of loadedPlugins.entries()) {
|
|
102
|
+
const intervalMs = (item.plugin.intervalSec || 5) * 1000;
|
|
103
|
+
if (now - item.lastRun >= intervalMs) {
|
|
104
|
+
try {
|
|
105
|
+
const raw = await Promise.race([
|
|
106
|
+
item.plugin.collect(),
|
|
107
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout (5s)')), 5000)),
|
|
108
|
+
]);
|
|
109
|
+
|
|
110
|
+
let output: PyrePluginOutput;
|
|
111
|
+
if (Array.isArray(raw)) {
|
|
112
|
+
output = { title: item.plugin.title || item.plugin.name, lines: raw.map(String) };
|
|
113
|
+
} else if (raw && typeof raw === 'object' && Array.isArray((raw as any).lines)) {
|
|
114
|
+
output = {
|
|
115
|
+
title: (raw as any).title || item.plugin.title || item.plugin.name,
|
|
116
|
+
lines: (raw as any).lines.map(String),
|
|
117
|
+
};
|
|
118
|
+
} else if (raw && typeof raw === 'object') {
|
|
119
|
+
output = {
|
|
120
|
+
title: item.plugin.title || item.plugin.name,
|
|
121
|
+
lines: Object.entries(raw).map(([k, v]) => `${k}: ${v}`),
|
|
122
|
+
};
|
|
123
|
+
} else {
|
|
124
|
+
output = { title: item.plugin.title || item.plugin.name, lines: [String(raw)] };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
item.lastOutput = output;
|
|
128
|
+
item.lastRun = now;
|
|
129
|
+
item.error = undefined;
|
|
130
|
+
} catch (err: any) {
|
|
131
|
+
item.lastOutput = {
|
|
132
|
+
title: item.plugin.title || item.plugin.name,
|
|
133
|
+
lines: [`Execution error: ${err.message}`],
|
|
134
|
+
};
|
|
135
|
+
item.error = err.message;
|
|
136
|
+
item.lastRun = now;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
results.push(item);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return results;
|
|
143
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pyre profile-proc — Process CPU Profiler.
|
|
3
|
+
*
|
|
4
|
+
* Samples a process for a given duration (default 5 seconds) using macOS `sample`
|
|
5
|
+
* or fallback thread analysis, then presents an annotated call-tree and
|
|
6
|
+
* hot frames summary.
|
|
7
|
+
*/
|
|
8
|
+
import { execFile } from 'node:child_process';
|
|
9
|
+
import { promisify } from 'node:util';
|
|
10
|
+
import chalk from 'chalk';
|
|
11
|
+
|
|
12
|
+
const execFileAsync = promisify(execFile);
|
|
13
|
+
|
|
14
|
+
export interface ProfileProcOptions {
|
|
15
|
+
pid: number;
|
|
16
|
+
durationSec?: number;
|
|
17
|
+
raw?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function runProfileProcCommand(opts: ProfileProcOptions): Promise<void> {
|
|
21
|
+
const { pid, durationSec = 5, raw } = opts;
|
|
22
|
+
|
|
23
|
+
if (isNaN(pid) || pid <= 0) {
|
|
24
|
+
console.error(chalk.red('\n ✖ Error: Valid PID is required. Usage: pyre profile-proc <pid> [--duration <sec>]\n'));
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// 1. Verify process exists
|
|
29
|
+
try {
|
|
30
|
+
process.kill(pid, 0);
|
|
31
|
+
} catch {
|
|
32
|
+
console.error(chalk.red(`\n ✖ Error: Process with PID ${pid} is not running or accessible.\n`));
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
console.log(chalk.bold(`\n 🔥 pyre profile-proc — Profiling PID ${pid} for ${durationSec}s...\n`));
|
|
37
|
+
|
|
38
|
+
let sampleOutput = '';
|
|
39
|
+
let usedFallback = false;
|
|
40
|
+
|
|
41
|
+
// Try macOS native `sample` command (available on macOS)
|
|
42
|
+
if (process.platform === 'darwin') {
|
|
43
|
+
try {
|
|
44
|
+
const { stdout } = await execFileAsync('sample', [String(pid), String(durationSec), '-file', '/dev/stdout'], {
|
|
45
|
+
timeout: (durationSec + 5) * 1000,
|
|
46
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
47
|
+
});
|
|
48
|
+
sampleOutput = stdout;
|
|
49
|
+
} catch (err: any) {
|
|
50
|
+
// Sample might fail if SIP restricts it on system processes or timeout
|
|
51
|
+
sampleOutput = err.stdout || '';
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Fallback: If sample produced no usable output or running on Linux, sample via thread / proc stats
|
|
56
|
+
if (!sampleOutput || sampleOutput.length < 100) {
|
|
57
|
+
usedFallback = true;
|
|
58
|
+
try {
|
|
59
|
+
const { stdout } = await execFileAsync('ps', ['-M', '-p', String(pid), '-o', 'pid,thread,pcpu,pmem,state,time,command']);
|
|
60
|
+
sampleOutput = stdout;
|
|
61
|
+
} catch {
|
|
62
|
+
sampleOutput = `Process PID ${pid} active. Deep stack sampling requires elevated permissions or user-owned process.`;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (raw) {
|
|
67
|
+
console.log(sampleOutput);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Parse and display clean call-tree analysis
|
|
72
|
+
if (!usedFallback) {
|
|
73
|
+
// Extract Call graph section
|
|
74
|
+
const lines = sampleOutput.split('\n');
|
|
75
|
+
const headerLines: string[] = [];
|
|
76
|
+
const stackLines: string[] = [];
|
|
77
|
+
let inCallGraph = false;
|
|
78
|
+
|
|
79
|
+
for (const line of lines) {
|
|
80
|
+
if (line.includes('Analysis of sampling') || line.includes('Process:') || line.includes('Path:') || line.includes('Identifier:')) {
|
|
81
|
+
headerLines.push(line.trim());
|
|
82
|
+
}
|
|
83
|
+
if (line.includes('Call graph:') || line.includes('+ Line Coverage')) {
|
|
84
|
+
inCallGraph = true;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (inCallGraph) {
|
|
88
|
+
if (line.includes('Total number in stack') || line.includes('Binary Images:')) {
|
|
89
|
+
inCallGraph = false;
|
|
90
|
+
} else if (line.trim().length > 0 && stackLines.length < 35) {
|
|
91
|
+
stackLines.push(line);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (headerLines.length > 0) {
|
|
97
|
+
console.log(chalk.dim(' Process Info:'));
|
|
98
|
+
for (const h of headerLines.slice(0, 4)) {
|
|
99
|
+
console.log(` ${chalk.cyan(h)}`);
|
|
100
|
+
}
|
|
101
|
+
console.log();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
console.log(chalk.bold(' Top Call Stacks & Hot Functions:'));
|
|
105
|
+
console.log(chalk.dim(' ' + '─'.repeat(70)));
|
|
106
|
+
|
|
107
|
+
if (stackLines.length === 0) {
|
|
108
|
+
for (const l of lines.slice(0, 20)) {
|
|
109
|
+
if (l.trim()) console.log(` ${l}`);
|
|
110
|
+
}
|
|
111
|
+
} else {
|
|
112
|
+
for (const l of stackLines) {
|
|
113
|
+
if (l.includes('???')) {
|
|
114
|
+
console.log(chalk.dim(` ${l}`));
|
|
115
|
+
} else if (l.includes('+')) {
|
|
116
|
+
console.log(chalk.yellow(` ${l}`));
|
|
117
|
+
} else {
|
|
118
|
+
console.log(` ${l}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
} else {
|
|
123
|
+
console.log(chalk.yellow(' Note: Kernel/SIP restriction prevented deep call-graph unwind.'));
|
|
124
|
+
console.log(chalk.dim(' Showing process thread snapshot:\n'));
|
|
125
|
+
for (const l of sampleOutput.split('\n')) {
|
|
126
|
+
if (l.trim()) console.log(` ${l}`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
console.log();
|
|
131
|
+
console.log(chalk.green(` ✔ Sampling complete for PID ${pid} (${durationSec}s profile duration).`));
|
|
132
|
+
console.log();
|
|
133
|
+
}
|