pyre-cli 9.5.2 → 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.
Files changed (86) hide show
  1. package/README.md +73 -16
  2. package/dist/alertChannels-TPSQIJ7S.js +8 -0
  3. package/dist/{blender-7UT5J7IP.js → blender-FGH3ASEQ.js} +2 -2
  4. package/dist/brewHealth-P2GW5Y5C.js +176 -0
  5. package/dist/buildCmd-CDPZ2ZFA.js +38 -0
  6. package/dist/{checkCmd-T7O4KXXA.js → checkCmd-RKW6QGDR.js} +9 -11
  7. package/dist/{chunk-CXL4GYGQ.js → chunk-53O2RGSL.js} +44 -17
  8. package/dist/{chunk-3TE7MJLS.js → chunk-6UHZP4QP.js} +36 -10
  9. package/dist/chunk-GHRDW646.js +2506 -0
  10. package/dist/{prometheus-LFYHF6VO.js → chunk-HT4VQ4OH.js} +1 -0
  11. package/dist/chunk-M73HCQD2.js +95 -0
  12. package/dist/chunk-MZO5QKUR.js +32 -0
  13. package/dist/{chunk-25I533OF.js → chunk-VIACN2RU.js} +1 -1
  14. package/dist/chunk-Z2YV5HKV.js +87 -0
  15. package/dist/chunk-ZLVIVFSO.js +110 -0
  16. package/dist/{collectors-JB4OGUEF.js → collectors-H43PN4PS.js} +6 -3
  17. package/dist/{completions-VQMHQCXE.js → completions-JKYEJNGR.js} +34 -1
  18. package/dist/{config-KS5ZZ3VN.js → config-GYGJXU2Q.js} +9 -1
  19. package/dist/doctor-GYHFTNZQ.js +387 -0
  20. package/dist/explainCmd-MF6PIMLW.js +222 -0
  21. package/dist/extensions-XJGHG7X3.js +134 -0
  22. package/dist/index.js +460 -88
  23. package/dist/ioreport.py +72 -0
  24. package/dist/netUsageCmd-WKBTW2YG.js +90 -0
  25. package/dist/{pipeCmd-TYLJHQ5B.js → pipeCmd-7REDBLBU.js} +4 -3
  26. package/dist/profileProcCmd-N4H6MCBT.js +107 -0
  27. package/dist/prometheus-5BL5PVLC.js +6 -0
  28. package/dist/prometheusCmd-PL64BLN5.js +93 -0
  29. package/dist/replayCmd-Z3PYXY7V.js +164 -0
  30. package/dist/run-HOGQDDTA.js +8 -0
  31. package/dist/serveCmd-2H5VLF3P.js +153 -0
  32. package/dist/service-Q74BBILO.js +181 -0
  33. package/dist/smartCmd-ODWKSFNG.js +166 -0
  34. package/dist/syncCmd-A2W3QB3S.js +126 -0
  35. package/dist/topoCmd-DU2HHZNH.js +148 -0
  36. package/dist/watchdogCmd-UF53JPZB.js +161 -0
  37. package/dist/{wizard-Z3RGA3VB.js → wizard-TD7BGX6O.js} +1 -1
  38. package/install.ps1 +29 -0
  39. package/install.sh +10 -9
  40. package/package.json +7 -3
  41. package/src/ai/models.ts +138 -0
  42. package/src/alertChannels.ts +119 -0
  43. package/src/brewHealth.ts +157 -40
  44. package/src/buildCmd.ts +48 -0
  45. package/src/checkCmd.ts +5 -5
  46. package/src/completions.ts +35 -0
  47. package/src/doctor.ts +235 -7
  48. package/src/easy_features.test.ts +40 -0
  49. package/src/explainCmd.ts +261 -0
  50. package/src/extensions.ts +71 -7
  51. package/src/formatters/render.ts +34 -10
  52. package/src/formatters/types.ts +2 -0
  53. package/src/index.ts +251 -36
  54. package/src/live/input.ts +60 -0
  55. package/src/live/render.ts +67 -0
  56. package/src/live/state.ts +5 -0
  57. package/src/live/types.ts +1 -1
  58. package/src/monitors/batteryPredictor.ts +143 -0
  59. package/src/monitors/buildTracker.ts +121 -0
  60. package/src/monitors/collectors.ts +342 -178
  61. package/src/monitors/ioreport.py +72 -0
  62. package/src/monitors/platform/linux.ts +756 -0
  63. package/src/monitors/platform/windows.ts +519 -0
  64. package/src/monitors/run.ts +27 -3
  65. package/src/monitors/smc.ts +21 -1
  66. package/src/monitors/types.ts +15 -0
  67. package/src/netUsageCmd.ts +121 -0
  68. package/src/platform.test.ts +99 -0
  69. package/src/plugins/index.ts +143 -0
  70. package/src/profileProcCmd.ts +133 -0
  71. package/src/prometheusCmd.ts +107 -0
  72. package/src/replayCmd.ts +186 -0
  73. package/src/serveCmd.ts +180 -0
  74. package/src/service.ts +125 -7
  75. package/src/smartCmd.ts +205 -0
  76. package/src/state/config.ts +58 -17
  77. package/src/syncCmd.ts +166 -0
  78. package/src/topoCmd.ts +176 -0
  79. package/src/watchdogCmd.ts +198 -0
  80. package/src/xbar.ts +22 -11
  81. package/dist/brewHealth-F2BXF5M6.js +0 -74
  82. package/dist/chunk-FH5WDJBF.js +0 -15
  83. package/dist/chunk-N4WJVF5M.js +0 -1184
  84. package/dist/doctor-UV7TWIHX.js +0 -184
  85. package/dist/extensions-DVA5YLGH.js +0 -74
  86. 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
+ }
@@ -1,22 +1,46 @@
1
1
  /**
2
2
  * Shared shell-execution helper for the monitors module.
3
3
  *
4
- * Wraps `child_process.execSync` with a timeout and a
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
- return (await execAsync(cmd, { encoding: 'utf8', timeout })).stdout.trim();
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 };
@@ -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
@@ -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 {