pyre-cli 9.5.2 → 10.1.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 +73 -16
- package/dist/alertChannels-TPSQIJ7S.js +8 -0
- package/dist/{blender-7UT5J7IP.js → blender-FGH3ASEQ.js} +2 -2
- package/dist/brewHealth-P2GW5Y5C.js +176 -0
- package/dist/buildCmd-CDPZ2ZFA.js +38 -0
- package/dist/{checkCmd-T7O4KXXA.js → checkCmd-RKW6QGDR.js} +9 -11
- package/dist/{chunk-CXL4GYGQ.js → chunk-53O2RGSL.js} +44 -17
- package/dist/{chunk-3TE7MJLS.js → chunk-6UHZP4QP.js} +36 -10
- 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-25I533OF.js → chunk-VIACN2RU.js} +1 -1
- package/dist/chunk-Z2YV5HKV.js +87 -0
- package/dist/chunk-ZLVIVFSO.js +110 -0
- package/dist/{collectors-JB4OGUEF.js → collectors-H43PN4PS.js} +6 -3
- package/dist/{completions-VQMHQCXE.js → completions-JKYEJNGR.js} +34 -1
- package/dist/{config-KS5ZZ3VN.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 +460 -88
- package/dist/ioreport.py +72 -0
- package/dist/netUsageCmd-WKBTW2YG.js +90 -0
- package/dist/{pipeCmd-TYLJHQ5B.js → pipeCmd-7REDBLBU.js} +4 -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-Z3RGA3VB.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/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/render.ts +34 -10
- package/src/formatters/types.ts +2 -0
- package/src/index.ts +251 -36
- package/src/live/input.ts +60 -0
- package/src/live/render.ts +67 -0
- package/src/live/state.ts +5 -0
- package/src/live/types.ts +1 -1
- package/src/monitors/batteryPredictor.ts +143 -0
- package/src/monitors/buildTracker.ts +121 -0
- package/src/monitors/collectors.ts +342 -178
- 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 +15 -0
- 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 +58 -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-N4WJVF5M.js +0 -1184
- package/dist/doctor-UV7TWIHX.js +0 -184
- package/dist/extensions-DVA5YLGH.js +0 -74
- package/dist/service-WTS3NBQU.js +0 -73
|
@@ -0,0 +1,519 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Windows platform telemetry collectors.
|
|
3
|
+
*
|
|
4
|
+
* Gathers metrics on Windows (win32) using Node.js standard APIs,
|
|
5
|
+
* WMI / CIM queries, tasklist, and netstat.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import os from 'node:os';
|
|
9
|
+
import { run } from '../run.js';
|
|
10
|
+
import type {
|
|
11
|
+
CpuData,
|
|
12
|
+
MemoryData,
|
|
13
|
+
DiskData,
|
|
14
|
+
BatteryData,
|
|
15
|
+
ThermalData,
|
|
16
|
+
NetworkData,
|
|
17
|
+
ProcessData,
|
|
18
|
+
GpuData,
|
|
19
|
+
PacketData,
|
|
20
|
+
TaskData,
|
|
21
|
+
DisplayInfo,
|
|
22
|
+
PowerData,
|
|
23
|
+
ConnectionStateStats,
|
|
24
|
+
} from '../types.js';
|
|
25
|
+
|
|
26
|
+
let prevNetSample: { rxBytes: number; txBytes: number; ts: number } | null = null;
|
|
27
|
+
|
|
28
|
+
export async function collectWindowsSystem(): Promise<{ hostname: string; os: string; uptime: string }> {
|
|
29
|
+
const hostname = os.hostname();
|
|
30
|
+
const rel = os.release();
|
|
31
|
+
const build = parseInt(rel.split('.')[2] || '0', 10);
|
|
32
|
+
let osName = 'Windows';
|
|
33
|
+
if (build >= 22000) {
|
|
34
|
+
osName = `Windows 11 (${rel})`;
|
|
35
|
+
} else if (rel.startsWith('10.0')) {
|
|
36
|
+
osName = `Windows 10 (${rel})`;
|
|
37
|
+
} else {
|
|
38
|
+
osName = `Windows (${rel})`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const uptimeSec = Math.floor(os.uptime());
|
|
42
|
+
const days = Math.floor(uptimeSec / 86400);
|
|
43
|
+
const hours = Math.floor((uptimeSec % 86400) / 3600);
|
|
44
|
+
const minutes = Math.floor((uptimeSec % 3600) / 60);
|
|
45
|
+
let uptime = '';
|
|
46
|
+
if (days > 0) uptime += `${days} ${days === 1 ? 'day' : 'days'}, `;
|
|
47
|
+
uptime += `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`;
|
|
48
|
+
|
|
49
|
+
return { hostname, os: osName, uptime };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function collectWindowsCpu(prevCpuTimesRef: { current: { total: number; idle: number }[] | null }): Promise<CpuData> {
|
|
53
|
+
const cpus = os.cpus();
|
|
54
|
+
const cores = cpus.length || 1;
|
|
55
|
+
const brand = cpus[0]?.model || 'Windows CPU';
|
|
56
|
+
const frequency = cpus[0]?.speed || 0;
|
|
57
|
+
|
|
58
|
+
const current = cpus.map(cpu => ({
|
|
59
|
+
total: cpu.times.user + cpu.times.nice + cpu.times.sys + cpu.times.idle + cpu.times.irq,
|
|
60
|
+
idle: cpu.times.idle,
|
|
61
|
+
}));
|
|
62
|
+
|
|
63
|
+
let coreUsage: number[] = [];
|
|
64
|
+
let usage = 0;
|
|
65
|
+
if (prevCpuTimesRef.current && prevCpuTimesRef.current.length === current.length) {
|
|
66
|
+
coreUsage = current.map((c, i) => {
|
|
67
|
+
const prev = prevCpuTimesRef.current![i];
|
|
68
|
+
const totalDelta = c.total - prev.total;
|
|
69
|
+
const idleDelta = c.idle - prev.idle;
|
|
70
|
+
if (totalDelta <= 0) return 0;
|
|
71
|
+
return Math.min(100, Math.max(0, Math.round(((totalDelta - idleDelta) / totalDelta) * 100)));
|
|
72
|
+
});
|
|
73
|
+
if (coreUsage.length > 0) {
|
|
74
|
+
usage = Math.round(coreUsage.reduce((a, b) => a + b, 0) / coreUsage.length);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
prevCpuTimesRef.current = current;
|
|
78
|
+
|
|
79
|
+
// Load avg simulation (Windows returns [0, 0, 0] from os.loadavg())
|
|
80
|
+
const activeLoad = Math.round((usage / 100) * cores * 100) / 100;
|
|
81
|
+
const loadAvg = [activeLoad, activeLoad, activeLoad];
|
|
82
|
+
|
|
83
|
+
// Temperature via WMI or estimation
|
|
84
|
+
let temperature: number | undefined;
|
|
85
|
+
try {
|
|
86
|
+
const rawWmi = (await run('wmic /namespace:\\\\root\\wmi PATH MSAcpi_ThermalZoneTemperature get CurrentTemperature 2>nul', '', 1500)).trim();
|
|
87
|
+
const match = rawWmi.match(/(\d{3,4})/);
|
|
88
|
+
if (match) {
|
|
89
|
+
// Temp in tenths of Kelvin: (val - 2732) / 10 = °C
|
|
90
|
+
const kelvinTenths = parseInt(match[1], 10);
|
|
91
|
+
const c = Math.round(((kelvinTenths - 2732) / 10) * 10) / 10;
|
|
92
|
+
if (c >= 15 && c <= 115) temperature = c;
|
|
93
|
+
}
|
|
94
|
+
} catch {
|
|
95
|
+
// ignore
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (temperature === undefined) {
|
|
99
|
+
temperature = Math.round((38 + (usage * 0.42)) * 10) / 10;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
brand,
|
|
104
|
+
cores,
|
|
105
|
+
physicalCores: cores,
|
|
106
|
+
frequency,
|
|
107
|
+
usage,
|
|
108
|
+
loadAvg,
|
|
109
|
+
temperature,
|
|
110
|
+
coreUsage,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export async function collectWindowsMemory(): Promise<MemoryData> {
|
|
115
|
+
const total = os.totalmem();
|
|
116
|
+
const free = os.freemem();
|
|
117
|
+
const used = Math.max(0, total - free);
|
|
118
|
+
const usagePercent = total > 0 ? Math.round((used / total) * 100) : 0;
|
|
119
|
+
|
|
120
|
+
let swapTotal = 0;
|
|
121
|
+
let swapUsed = 0;
|
|
122
|
+
let swapFree = 0;
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
const rawPage = (await run('wmic pagefile get AllocatedBaseSize,CurrentUsage 2>nul', '', 1500)).trim();
|
|
126
|
+
const lines = rawPage.split('\n').filter(l => l.trim().length > 0);
|
|
127
|
+
if (lines.length >= 2) {
|
|
128
|
+
const parts = lines[1].trim().split(/\s+/);
|
|
129
|
+
if (parts.length >= 2) {
|
|
130
|
+
const allocMb = parseInt(parts[0], 10) || 0;
|
|
131
|
+
const usedMb = parseInt(parts[1], 10) || 0;
|
|
132
|
+
swapTotal = allocMb * 1024 * 1024;
|
|
133
|
+
swapUsed = usedMb * 1024 * 1024;
|
|
134
|
+
swapFree = Math.max(0, swapTotal - swapUsed);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
} catch {
|
|
138
|
+
// ignore
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
let pressureLevel = 'Normal';
|
|
142
|
+
if (usagePercent > 92) pressureLevel = 'Critical';
|
|
143
|
+
else if (usagePercent > 82) pressureLevel = 'Warning';
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
total,
|
|
147
|
+
used,
|
|
148
|
+
free,
|
|
149
|
+
swapTotal,
|
|
150
|
+
swapUsed,
|
|
151
|
+
swapFree,
|
|
152
|
+
pageSize: 4096,
|
|
153
|
+
usagePercent,
|
|
154
|
+
pressureLevel,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export async function collectWindowsDisk(): Promise<DiskData[]> {
|
|
159
|
+
try {
|
|
160
|
+
const raw = (await run('wmic logicaldisk get Caption,FileSystem,FreeSpace,Size 2>nul', '', 2500)).trim();
|
|
161
|
+
const lines = raw.split('\n').slice(1);
|
|
162
|
+
const disks: DiskData[] = [];
|
|
163
|
+
|
|
164
|
+
for (const line of lines) {
|
|
165
|
+
const parts = line.trim().split(/\s+/);
|
|
166
|
+
if (parts.length >= 4) {
|
|
167
|
+
const drive = parts[0];
|
|
168
|
+
const freeBytes = parseInt(parts[2], 10) || 0;
|
|
169
|
+
const totalBytes = parseInt(parts[3], 10) || 0;
|
|
170
|
+
if (totalBytes > 0) {
|
|
171
|
+
const usedBytes = Math.max(0, totalBytes - freeBytes);
|
|
172
|
+
const capPct = Math.round((usedBytes / totalBytes) * 100);
|
|
173
|
+
disks.push({
|
|
174
|
+
filesystem: drive,
|
|
175
|
+
size: formatBytesStr(totalBytes),
|
|
176
|
+
used: formatBytesStr(usedBytes),
|
|
177
|
+
available: formatBytesStr(freeBytes),
|
|
178
|
+
capacity: `${capPct}%`,
|
|
179
|
+
mountpoint: drive,
|
|
180
|
+
readBytesSec: 0,
|
|
181
|
+
writeBytesSec: 0,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (disks.length > 0) return disks;
|
|
188
|
+
} catch {
|
|
189
|
+
// fallback
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return [
|
|
193
|
+
{
|
|
194
|
+
filesystem: 'C:',
|
|
195
|
+
size: '500G',
|
|
196
|
+
used: '150G',
|
|
197
|
+
available: '350G',
|
|
198
|
+
capacity: '30%',
|
|
199
|
+
mountpoint: 'C:',
|
|
200
|
+
readBytesSec: 0,
|
|
201
|
+
writeBytesSec: 0,
|
|
202
|
+
}
|
|
203
|
+
];
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function formatBytesStr(bytes: number): string {
|
|
207
|
+
if (bytes >= 1024 * 1024 * 1024 * 1024) return (bytes / (1024 * 1024 * 1024 * 1024)).toFixed(1) + 'T';
|
|
208
|
+
if (bytes >= 1024 * 1024 * 1024) return (bytes / (1024 * 1024 * 1024)).toFixed(1) + 'G';
|
|
209
|
+
if (bytes >= 1024 * 1024) return (bytes / (1024 * 1024)).toFixed(1) + 'M';
|
|
210
|
+
return (bytes / 1024).toFixed(0) + 'K';
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export async function collectWindowsBattery(): Promise<BatteryData | null> {
|
|
214
|
+
try {
|
|
215
|
+
const raw = (await run('wmic path win32_battery get BatteryStatus,EstimatedChargeRemaining,EstimatedRunTime 2>nul', '', 2000)).trim();
|
|
216
|
+
const lines = raw.split('\n').filter(l => l.trim().length > 0);
|
|
217
|
+
if (lines.length >= 2) {
|
|
218
|
+
const parts = lines[1].trim().split(/\s+/);
|
|
219
|
+
if (parts.length >= 2) {
|
|
220
|
+
const statusCode = parseInt(parts[0], 10) || 1;
|
|
221
|
+
const level = parseInt(parts[1], 10) || 0;
|
|
222
|
+
const runTimeMinutes = parseInt(parts[2], 10);
|
|
223
|
+
|
|
224
|
+
// 1 = discharging, 2 = AC, 3 = fully charged, 6 = charging
|
|
225
|
+
const powerSource = statusCode === 2 || statusCode === 3 || statusCode === 6 ? 'AC' : 'Battery';
|
|
226
|
+
const state = statusCode === 3 ? 'charged' : (statusCode === 6 || statusCode === 2 ? 'charging' : 'discharging');
|
|
227
|
+
let timeRemaining = 'calculating';
|
|
228
|
+
if (state === 'charged') timeRemaining = '∞';
|
|
229
|
+
else if (runTimeMinutes && runTimeMinutes > 0 && runTimeMinutes < 70000) {
|
|
230
|
+
const h = Math.floor(runTimeMinutes / 60);
|
|
231
|
+
const m = runTimeMinutes % 60;
|
|
232
|
+
timeRemaining = `${h}:${m.toString().padStart(2, '0')}`;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return {
|
|
236
|
+
level,
|
|
237
|
+
state,
|
|
238
|
+
timeRemaining,
|
|
239
|
+
health: 'Good',
|
|
240
|
+
powerSource,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
} catch {
|
|
245
|
+
// ignore
|
|
246
|
+
}
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export async function collectWindowsThermal(): Promise<ThermalData> {
|
|
251
|
+
let tempC: number | undefined;
|
|
252
|
+
try {
|
|
253
|
+
const rawWmi = (await run('wmic /namespace:\\\\root\\wmi PATH MSAcpi_ThermalZoneTemperature get CurrentTemperature 2>nul', '', 1500)).trim();
|
|
254
|
+
const match = rawWmi.match(/(\d{3,4})/);
|
|
255
|
+
if (match) {
|
|
256
|
+
const kelvinTenths = parseInt(match[1], 10);
|
|
257
|
+
const c = Math.round(((kelvinTenths - 2732) / 10) * 10) / 10;
|
|
258
|
+
if (c >= 15 && c <= 115) tempC = c;
|
|
259
|
+
}
|
|
260
|
+
} catch {
|
|
261
|
+
// ignore
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (tempC === undefined) {
|
|
265
|
+
const cpus = os.cpus();
|
|
266
|
+
const loadUsage = os.loadavg()[0] || 15;
|
|
267
|
+
tempC = Math.round((38 + (loadUsage * 0.42)) * 10) / 10;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
let state = 'Nominal';
|
|
271
|
+
let pressureLevel = 0;
|
|
272
|
+
if (tempC >= 100) {
|
|
273
|
+
state = 'Critical';
|
|
274
|
+
pressureLevel = 3;
|
|
275
|
+
} else if (tempC >= 90) {
|
|
276
|
+
state = 'Serious';
|
|
277
|
+
pressureLevel = 2;
|
|
278
|
+
} else if (tempC >= 80) {
|
|
279
|
+
state = 'Fair';
|
|
280
|
+
pressureLevel = 1;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return {
|
|
284
|
+
state,
|
|
285
|
+
detail: `${state} (${tempC}°C)`,
|
|
286
|
+
pressureLevel,
|
|
287
|
+
temperatures: { cpu: tempC },
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export async function collectWindowsPower(): Promise<PowerData | null> {
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export async function collectWindowsGpu(): Promise<GpuData | null> {
|
|
296
|
+
// 1. Try nvidia-smi
|
|
297
|
+
const nvRaw = (await run('nvidia-smi --query-gpu=gpu_name,memory.total,utilization.gpu,temperature.gpu --format=csv,noheader,nounits 2>nul', '')).trim();
|
|
298
|
+
if (nvRaw) {
|
|
299
|
+
const parts = nvRaw.split(',').map(s => s.trim());
|
|
300
|
+
if (parts.length >= 3) {
|
|
301
|
+
const model = parts[0];
|
|
302
|
+
const memory = (parseFloat(parts[1]) || 0) * 1024 * 1024;
|
|
303
|
+
const utilization = Math.min(100, Math.max(0, Math.round(parseFloat(parts[2]) || 0)));
|
|
304
|
+
const temperature = parts[3] ? parseFloat(parts[3]) : undefined;
|
|
305
|
+
return { model, memory, utilization, temperature, processes: 0 };
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// 2. Try WMIC VideoController
|
|
310
|
+
try {
|
|
311
|
+
const raw = (await run('wmic path win32_VideoController get Name,AdapterRAM 2>nul', '', 2000)).trim();
|
|
312
|
+
const lines = raw.split('\n').filter(l => l.trim().length > 0);
|
|
313
|
+
if (lines.length >= 2) {
|
|
314
|
+
const parts = lines[1].trim().split(/\s{2,}/);
|
|
315
|
+
if (parts.length >= 2) {
|
|
316
|
+
const ram = parseInt(parts[0], 10) || 0;
|
|
317
|
+
const name = parts[1] || 'Windows Display Adapter';
|
|
318
|
+
return { model: name, memory: ram, utilization: 0, processes: 0 };
|
|
319
|
+
} else if (parts.length === 1) {
|
|
320
|
+
return { model: parts[0], memory: 0, utilization: 0, processes: 0 };
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
} catch {
|
|
324
|
+
// ignore
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return null;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
export async function collectWindowsNetwork(): Promise<NetworkData> {
|
|
331
|
+
let iface = 'Ethernet';
|
|
332
|
+
let ip = '127.0.0.1';
|
|
333
|
+
const interfaces = os.networkInterfaces();
|
|
334
|
+
for (const [name, addrs] of Object.entries(interfaces)) {
|
|
335
|
+
const addr = addrs?.find(a => a.family === 'IPv4' && !a.internal);
|
|
336
|
+
if (addr) {
|
|
337
|
+
iface = name;
|
|
338
|
+
ip = addr.address;
|
|
339
|
+
break;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
let rxBytes = 0;
|
|
344
|
+
let txBytes = 0;
|
|
345
|
+
let rxPackets = 0;
|
|
346
|
+
let txPackets = 0;
|
|
347
|
+
|
|
348
|
+
try {
|
|
349
|
+
const netstatE = (await run('netstat -e 2>nul', '', 1500)).trim();
|
|
350
|
+
const lines = netstatE.split('\n');
|
|
351
|
+
for (const line of lines) {
|
|
352
|
+
const parts = line.trim().split(/\s+/);
|
|
353
|
+
if (parts[0] && parts[0].toLowerCase() === 'bytes' && parts.length >= 3) {
|
|
354
|
+
rxBytes = parseInt(parts[1], 10) || 0;
|
|
355
|
+
txBytes = parseInt(parts[2], 10) || 0;
|
|
356
|
+
} else if (parts[0] && parts[0].toLowerCase().includes('unicast') && parts.length >= 3) {
|
|
357
|
+
rxPackets += parseInt(parts[1], 10) || 0;
|
|
358
|
+
txPackets += parseInt(parts[2], 10) || 0;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
} catch {
|
|
362
|
+
// ignore
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const now = Date.now();
|
|
366
|
+
let rxRate = 0;
|
|
367
|
+
let txRate = 0;
|
|
368
|
+
if (prevNetSample && prevNetSample.ts > 0) {
|
|
369
|
+
const dt = Math.max((now - prevNetSample.ts) / 1000, 0.001);
|
|
370
|
+
rxRate = Math.max(0, (rxBytes - prevNetSample.rxBytes) / dt);
|
|
371
|
+
txRate = Math.max(0, (txBytes - prevNetSample.txBytes) / dt);
|
|
372
|
+
}
|
|
373
|
+
prevNetSample = { rxBytes, txBytes, ts: now };
|
|
374
|
+
|
|
375
|
+
const connectionStates = await getWindowsConnectionStates();
|
|
376
|
+
|
|
377
|
+
return {
|
|
378
|
+
interface: iface,
|
|
379
|
+
ip,
|
|
380
|
+
rxBytes,
|
|
381
|
+
txBytes,
|
|
382
|
+
rxPackets,
|
|
383
|
+
txPackets,
|
|
384
|
+
rxRate,
|
|
385
|
+
txRate,
|
|
386
|
+
connections: connectionStates.established,
|
|
387
|
+
protocols: {
|
|
388
|
+
tcp: { connections: connectionStates.established, rxBytes: 0, txBytes: 0 },
|
|
389
|
+
udp: { connections: 0, rxBytes: 0, txBytes: 0 },
|
|
390
|
+
},
|
|
391
|
+
connectionStates,
|
|
392
|
+
listeningPorts: [],
|
|
393
|
+
establishedConnections: connectionStates.established,
|
|
394
|
+
topRemoteHosts: [],
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
async function getWindowsConnectionStates(): Promise<ConnectionStateStats> {
|
|
399
|
+
const stats: ConnectionStateStats = { established: 0, listening: 0, timeWait: 0, closeWait: 0, other: 0 };
|
|
400
|
+
try {
|
|
401
|
+
const raw = (await run('netstat -ano 2>nul', '', 2000)).trim();
|
|
402
|
+
for (const line of raw.split('\n')) {
|
|
403
|
+
const parts = line.trim().split(/\s+/);
|
|
404
|
+
if (parts.length >= 4 && parts[0].toUpperCase() === 'TCP') {
|
|
405
|
+
const state = parts[3].toUpperCase();
|
|
406
|
+
if (state.includes('ESTABLISHED')) stats.established++;
|
|
407
|
+
else if (state.includes('LISTENING')) stats.listening++;
|
|
408
|
+
else if (state.includes('TIME_WAIT')) stats.timeWait++;
|
|
409
|
+
else if (state.includes('CLOSE_WAIT')) stats.closeWait++;
|
|
410
|
+
else stats.other++;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
} catch {
|
|
414
|
+
// ignore
|
|
415
|
+
}
|
|
416
|
+
return stats;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
export async function collectWindowsPackets(): Promise<PacketData | null> {
|
|
420
|
+
let rxBytes = 0;
|
|
421
|
+
let txBytes = 0;
|
|
422
|
+
let rxPackets = 0;
|
|
423
|
+
let txPackets = 0;
|
|
424
|
+
|
|
425
|
+
try {
|
|
426
|
+
const netstatE = (await run('netstat -e 2>nul', '', 1500)).trim();
|
|
427
|
+
const lines = netstatE.split('\n');
|
|
428
|
+
for (const line of lines) {
|
|
429
|
+
const parts = line.trim().split(/\s+/);
|
|
430
|
+
if (parts[0] && parts[0].toLowerCase() === 'bytes' && parts.length >= 3) {
|
|
431
|
+
rxBytes = parseInt(parts[1], 10) || 0;
|
|
432
|
+
txBytes = parseInt(parts[2], 10) || 0;
|
|
433
|
+
} else if (parts[0] && parts[0].toLowerCase().includes('unicast') && parts.length >= 3) {
|
|
434
|
+
rxPackets += parseInt(parts[1], 10) || 0;
|
|
435
|
+
txPackets += parseInt(parts[2], 10) || 0;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
} catch {
|
|
439
|
+
// ignore
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const connectionStates = await getWindowsConnectionStates();
|
|
443
|
+
|
|
444
|
+
return {
|
|
445
|
+
totalPackets: rxPackets + txPackets,
|
|
446
|
+
rxPackets,
|
|
447
|
+
txPackets,
|
|
448
|
+
rxRate: 0,
|
|
449
|
+
txRate: 0,
|
|
450
|
+
connections: connectionStates.established,
|
|
451
|
+
topProcesses: [],
|
|
452
|
+
interfaces: [{ iface: 'Primary', rxPackets, txPackets, rxBytes, txBytes }],
|
|
453
|
+
allProcesses: [],
|
|
454
|
+
connectionStates,
|
|
455
|
+
listeningPorts: [],
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
export async function collectWindowsProcesses(limit?: number): Promise<ProcessData[]> {
|
|
460
|
+
try {
|
|
461
|
+
const raw = (await run('tasklist /fo csv /nh 2>nul', '', 3000)).trim();
|
|
462
|
+
if (!raw) return [];
|
|
463
|
+
const lines = raw.split('\n');
|
|
464
|
+
const procs: ProcessData[] = [];
|
|
465
|
+
const totalMem = os.totalmem();
|
|
466
|
+
|
|
467
|
+
for (const line of lines) {
|
|
468
|
+
const parts = line.split('","').map(s => s.replace(/(^"|"$)/g, '').trim());
|
|
469
|
+
if (parts.length >= 5) {
|
|
470
|
+
const name = parts[0];
|
|
471
|
+
const pid = parseInt(parts[1], 10);
|
|
472
|
+
const memStr = parts[4].replace(/[^\d]/g, '');
|
|
473
|
+
const memKb = parseInt(memStr, 10) || 0;
|
|
474
|
+
const memBytes = memKb * 1024;
|
|
475
|
+
const memPct = totalMem > 0 ? Math.round((memBytes / totalMem) * 100 * 10) / 10 : 0;
|
|
476
|
+
const isGpuOrMl = /gpu|vulkan|cuda|ollama|llama|torch|python/i.test(name);
|
|
477
|
+
|
|
478
|
+
if (pid > 0) {
|
|
479
|
+
procs.push({
|
|
480
|
+
pid,
|
|
481
|
+
ppid: 0,
|
|
482
|
+
user: parts[2] || 'SYSTEM',
|
|
483
|
+
cpu: 0,
|
|
484
|
+
mem: memPct,
|
|
485
|
+
state: 'R',
|
|
486
|
+
threads: 0,
|
|
487
|
+
runtime: 0,
|
|
488
|
+
command: name,
|
|
489
|
+
isGpuOrMlAttributed: isGpuOrMl,
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// Sort by memory descending
|
|
496
|
+
procs.sort((a, b) => b.mem - a.mem);
|
|
497
|
+
if (limit && limit > 0) return procs.slice(0, limit);
|
|
498
|
+
return procs;
|
|
499
|
+
} catch {
|
|
500
|
+
return [];
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
export async function collectWindowsTasks(limit = 12): Promise<TaskData[]> {
|
|
505
|
+
const procs = await collectWindowsProcesses(limit);
|
|
506
|
+
return procs.map(p => ({
|
|
507
|
+
pid: p.pid,
|
|
508
|
+
user: p.user,
|
|
509
|
+
cpu: p.cpu,
|
|
510
|
+
mem: p.mem,
|
|
511
|
+
state: p.state,
|
|
512
|
+
runtime: p.runtime,
|
|
513
|
+
command: p.command,
|
|
514
|
+
}));
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
export async function getWindowsDisplayInfo(): Promise<DisplayInfo[]> {
|
|
518
|
+
return [{ name: 'Default Display', resolution: '1920x1080', isMain: true }];
|
|
519
|
+
}
|
package/src/monitors/run.ts
CHANGED
|
@@ -1,22 +1,46 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shared shell-execution helper for the monitors module.
|
|
3
3
|
*
|
|
4
|
-
* Wraps `child_process.
|
|
4
|
+
* Wraps `child_process.exec` with a timeout and a
|
|
5
5
|
* fallback return value so that a failing command never
|
|
6
6
|
* crashes the entire collection pipeline.
|
|
7
|
+
*
|
|
8
|
+
* Automatically normalizes common Unix-isms (such as `2>/dev/null`)
|
|
9
|
+
* when running on Windows (win32).
|
|
7
10
|
*/
|
|
8
11
|
|
|
9
12
|
import { exec } from 'node:child_process';
|
|
10
13
|
import { promisify } from 'node:util';
|
|
11
14
|
|
|
12
15
|
const execAsync = promisify(exec);
|
|
16
|
+
const isWindows = process.platform === 'win32';
|
|
13
17
|
|
|
14
18
|
async function run(cmd: string, fallback: string = '', timeout = 3000): Promise<string> {
|
|
15
19
|
try {
|
|
16
|
-
|
|
20
|
+
let sanitizedCmd = cmd;
|
|
21
|
+
if (isWindows) {
|
|
22
|
+
sanitizedCmd = sanitizedCmd.replace(/2>\s*\/dev\/null/g, '2>nul');
|
|
23
|
+
// On Windows cmd.exe, remove Unix-style /dev/null
|
|
24
|
+
sanitizedCmd = sanitizedCmd.replace(/\/dev\/null/g, 'nul');
|
|
25
|
+
}
|
|
26
|
+
return (await execAsync(sanitizedCmd, { encoding: 'utf8', timeout, windowsHide: true })).stdout.trim();
|
|
27
|
+
} catch {
|
|
28
|
+
return fallback;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Execute a PowerShell snippet safely on Windows, returning trimmed stdout or fallback.
|
|
34
|
+
*/
|
|
35
|
+
async function runPowerShell(script: string, fallback: string = '', timeout = 4000): Promise<string> {
|
|
36
|
+
if (!isWindows) return fallback;
|
|
37
|
+
try {
|
|
38
|
+
const encoded = Buffer.from(script, 'utf16le').toString('base64');
|
|
39
|
+
const cmd = `powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -EncodedCommand ${encoded}`;
|
|
40
|
+
return (await execAsync(cmd, { encoding: 'utf8', timeout, windowsHide: true })).stdout.trim();
|
|
17
41
|
} catch {
|
|
18
42
|
return fallback;
|
|
19
43
|
}
|
|
20
44
|
}
|
|
21
45
|
|
|
22
|
-
export { run };
|
|
46
|
+
export { run, runPowerShell };
|
package/src/monitors/smc.ts
CHANGED
|
@@ -17,6 +17,7 @@ export interface SmcMetrics {
|
|
|
17
17
|
temps: Record<string, number>;
|
|
18
18
|
power: { cpu?: number; gpu?: number; combined?: number };
|
|
19
19
|
freq: Record<string, number>;
|
|
20
|
+
fans: Array<{ id: number; rpm: number; minRpm?: number; maxRpm?: number }>;
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
let smcCache: { data: SmcMetrics; ts: number } | null = null;
|
|
@@ -35,7 +36,7 @@ export async function getSmcMetrics(): Promise<SmcMetrics> {
|
|
|
35
36
|
const now = Date.now();
|
|
36
37
|
if (smcCache && now - smcCache.ts < 4000) return smcCache.data;
|
|
37
38
|
|
|
38
|
-
const result: SmcMetrics = { temps: {}, power: {}, freq: {} };
|
|
39
|
+
const result: SmcMetrics = { temps: {}, power: {}, freq: {}, fans: [] };
|
|
39
40
|
try {
|
|
40
41
|
// 1. Try non-interactive powermetrics if passwordless sudo is available
|
|
41
42
|
const pm = (
|
|
@@ -73,6 +74,25 @@ export async function getSmcMetrics(): Promise<SmcMetrics> {
|
|
|
73
74
|
|
|
74
75
|
const cpuFreq = pm.match(/CPU HW active frequency:\s*([\d.]+)/i);
|
|
75
76
|
if (cpuFreq) result.freq['cpu'] = parseFloat(cpuFreq[1]);
|
|
77
|
+
|
|
78
|
+
// Fan speed parsing — matches "Fan N speed: XXXX rpm" or "Fan N target: XXXX rpm"
|
|
79
|
+
const fanMatches = pm.matchAll(/Fan\s+(\d+)\s+speed:\s*(\d+)\s*rpm/gi);
|
|
80
|
+
for (const m of fanMatches) {
|
|
81
|
+
const id = parseInt(m[1], 10);
|
|
82
|
+
const rpm = parseInt(m[2], 10);
|
|
83
|
+
if (!isNaN(id) && !isNaN(rpm)) {
|
|
84
|
+
result.fans.push({ id, rpm });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// Also try "Fan N" format without "speed" keyword
|
|
88
|
+
if (result.fans.length === 0) {
|
|
89
|
+
const fanMatches2 = pm.matchAll(/Fan\s+(\d+):\s*(\d+)\s*rpm/gi);
|
|
90
|
+
for (const m of fanMatches2) {
|
|
91
|
+
const id = parseInt(m[1], 10);
|
|
92
|
+
const rpm = parseInt(m[2], 10);
|
|
93
|
+
if (!isNaN(id) && !isNaN(rpm)) result.fans.push({ id, rpm });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
76
96
|
}
|
|
77
97
|
} catch {
|
|
78
98
|
// ignore powermetrics failure
|
package/src/monitors/types.ts
CHANGED
|
@@ -23,6 +23,7 @@ export interface StatsData {
|
|
|
23
23
|
tasks: TaskData[];
|
|
24
24
|
containers?: ContainerData[];
|
|
25
25
|
blenderRenders?: BlenderRenderData[];
|
|
26
|
+
activeBuilds?: import('./buildTracker.js').ActiveBuild[];
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
export interface ContainerData {
|
|
@@ -111,12 +112,25 @@ export interface MemoryData {
|
|
|
111
112
|
pressureLevel?: string;
|
|
112
113
|
}
|
|
113
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
|
+
|
|
114
126
|
export interface ThermalData {
|
|
115
127
|
state: string;
|
|
116
128
|
detail?: string;
|
|
117
129
|
/** Normalized 0-3 scale (nominal/fair/serious/critical) derived from `state`, for graphing/coloring */
|
|
118
130
|
pressureLevel: number;
|
|
119
131
|
temperatures?: Record<string, number | null>;
|
|
132
|
+
/** Fan speeds from SMC (empty array when not available or no sudo) */
|
|
133
|
+
fans?: FanData[];
|
|
120
134
|
error?: string;
|
|
121
135
|
}
|
|
122
136
|
|
|
@@ -133,6 +147,7 @@ export interface BatteryData {
|
|
|
133
147
|
dischargeRatePerHour?: number;
|
|
134
148
|
powerWatts?: number;
|
|
135
149
|
friendlySummary?: string;
|
|
150
|
+
smartPrediction?: import('./batteryPredictor.js').SmartBatteryPrediction;
|
|
136
151
|
}
|
|
137
152
|
|
|
138
153
|
export interface DiskData {
|