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.
Files changed (94) hide show
  1. package/README.md +84 -14
  2. package/dist/alertChannels-TPSQIJ7S.js +8 -0
  3. package/dist/blender-FGH3ASEQ.js +7 -0
  4. package/dist/brewHealth-P2GW5Y5C.js +176 -0
  5. package/dist/buildCmd-CDPZ2ZFA.js +38 -0
  6. package/dist/{checkCmd-EWFNLBWE.js → checkCmd-RKW6QGDR.js} +10 -11
  7. package/dist/{chunk-RVGKEJXT.js → chunk-53O2RGSL.js} +47 -17
  8. package/dist/{chunk-MOAEGMFP.js → chunk-6UHZP4QP.js} +204 -46
  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-VIACN2RU.js +133 -0
  14. package/dist/chunk-Z2YV5HKV.js +87 -0
  15. package/dist/chunk-ZLVIVFSO.js +110 -0
  16. package/dist/{collectors-WD6VKH6M.js → collectors-H43PN4PS.js} +8 -2
  17. package/dist/{completions-VQMHQCXE.js → completions-JKYEJNGR.js} +34 -1
  18. package/dist/{config-4ZLF4HTS.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 +841 -206
  23. package/dist/ioreport.py +72 -0
  24. package/dist/netUsageCmd-WKBTW2YG.js +90 -0
  25. package/dist/{pipeCmd-7THXEGDH.js → pipeCmd-7REDBLBU.js} +5 -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-GVIAHIVV.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/anomalies.ts +114 -60
  44. package/src/brewHealth.ts +157 -40
  45. package/src/buildCmd.ts +48 -0
  46. package/src/checkCmd.ts +5 -5
  47. package/src/completions.ts +35 -0
  48. package/src/doctor.ts +235 -7
  49. package/src/easy_features.test.ts +40 -0
  50. package/src/explainCmd.ts +261 -0
  51. package/src/extensions.ts +71 -7
  52. package/src/formatters/index.ts +1 -1
  53. package/src/formatters/output.ts +100 -13
  54. package/src/formatters/render.ts +274 -38
  55. package/src/formatters/themes.ts +8 -0
  56. package/src/formatters/types.ts +6 -1
  57. package/src/index.ts +298 -56
  58. package/src/live/input.ts +122 -4
  59. package/src/live/render.ts +98 -0
  60. package/src/live/state.ts +70 -52
  61. package/src/live/types.ts +2 -2
  62. package/src/monitors/batteryPredictor.ts +143 -0
  63. package/src/monitors/blender.ts +144 -0
  64. package/src/monitors/buildTracker.ts +121 -0
  65. package/src/monitors/collectors.ts +558 -147
  66. package/src/monitors/dns_cache.ts +48 -0
  67. package/src/monitors/index.ts +3 -2
  68. package/src/monitors/ioreport.py +72 -0
  69. package/src/monitors/platform/linux.ts +756 -0
  70. package/src/monitors/platform/windows.ts +519 -0
  71. package/src/monitors/run.ts +27 -3
  72. package/src/monitors/smc.ts +21 -1
  73. package/src/monitors/types.ts +112 -14
  74. package/src/netUsageCmd.ts +121 -0
  75. package/src/platform.test.ts +99 -0
  76. package/src/plugins/index.ts +143 -0
  77. package/src/profileProcCmd.ts +133 -0
  78. package/src/prometheusCmd.ts +107 -0
  79. package/src/replayCmd.ts +186 -0
  80. package/src/serveCmd.ts +180 -0
  81. package/src/service.ts +125 -7
  82. package/src/smartCmd.ts +205 -0
  83. package/src/state/config.ts +64 -17
  84. package/src/syncCmd.ts +166 -0
  85. package/src/topoCmd.ts +176 -0
  86. package/src/watchdogCmd.ts +198 -0
  87. package/src/xbar.ts +22 -11
  88. package/dist/brewHealth-F2BXF5M6.js +0 -74
  89. package/dist/chunk-FH5WDJBF.js +0 -15
  90. package/dist/chunk-HGRBBPYX.js +0 -937
  91. package/dist/doctor-L6FZYA7F.js +0 -184
  92. package/dist/extensions-DVA5YLGH.js +0 -74
  93. package/dist/service-WTS3NBQU.js +0 -73
  94. /package/dist/{chunk-SG26Y3WF.js → chunk-IM3IQMMT.js} +0 -0
@@ -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