pyre-cli 10.1.0 → 10.3.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 (46) hide show
  1. package/README.md +211 -494
  2. package/dist/{checkCmd-RKW6QGDR.js → checkCmd-WQAHAUWO.js} +1 -1
  3. package/dist/{chunk-GHRDW646.js → chunk-23YWCI3O.js} +375 -135
  4. package/dist/{service-Q74BBILO.js → chunk-KFPTD266.js} +43 -3
  5. package/dist/{doctor-GYHFTNZQ.js → chunk-KVUMOA6C.js} +7 -6
  6. package/dist/{chunk-53O2RGSL.js → chunk-QM5L43EK.js} +19 -2
  7. package/dist/{chunk-6UHZP4QP.js → chunk-XTOOEKLM.js} +24 -12
  8. package/dist/{collectors-H43PN4PS.js → collectors-JDN5Y7EV.js} +1 -1
  9. package/dist/{completions-JKYEJNGR.js → completions-ZVTNE52B.js} +15 -11
  10. package/dist/{config-GYGJXU2Q.js → config-J4YMADEF.js} +3 -1
  11. package/dist/doctor-LHFW4UJP.js +10 -0
  12. package/dist/firebaseSync-ON3RZ6KX.js +91 -0
  13. package/dist/index.js +4245 -736
  14. package/dist/monitors/platform/windows-ui.ps1 +42 -0
  15. package/dist/{netUsageCmd-WKBTW2YG.js → netUsageCmd-67IBZZYN.js} +2 -2
  16. package/dist/{pipeCmd-7REDBLBU.js → pipeCmd-2N4JPKYE.js} +1 -1
  17. package/dist/{prometheusCmd-PL64BLN5.js → prometheusCmd-LEUM5WCZ.js} +1 -1
  18. package/dist/serveCmd-L7LVJOZN.js +353 -0
  19. package/dist/service-7GIX3YNC.js +16 -0
  20. package/dist/syncCmd-42IRF633.js +246 -0
  21. package/dist/{topoCmd-DU2HHZNH.js → topoCmd-DZ3YS5IL.js} +1 -1
  22. package/dist/{watchdogCmd-UF53JPZB.js → watchdogCmd-6GR4UN6B.js} +7 -7
  23. package/dist/{wizard-TD7BGX6O.js → wizard-JQMQWFAW.js} +1 -1
  24. package/package.json +4 -3
  25. package/parity.md +2 -0
  26. package/src/completions.ts +15 -11
  27. package/src/easy_features.test.ts +149 -0
  28. package/src/firebaseSync.ts +157 -0
  29. package/src/formatters/html_template.ts +3086 -598
  30. package/src/formatters/render.ts +25 -12
  31. package/src/index.ts +518 -24
  32. package/src/live/input.ts +501 -148
  33. package/src/live/render.ts +163 -18
  34. package/src/live/state.ts +52 -2
  35. package/src/live/types.ts +1 -1
  36. package/src/monitors/collectors.ts +3 -10
  37. package/src/monitors/platform/windows-ui.ps1 +42 -0
  38. package/src/monitors/platform/windows.ts +442 -147
  39. package/src/monitors/types.ts +8 -0
  40. package/src/serveCmd.ts +17 -2
  41. package/src/service.ts +49 -0
  42. package/src/state/config.ts +48 -0
  43. package/src/syncCmd.ts +159 -11
  44. package/src/web/dashboard.ts +205 -0
  45. package/dist/serveCmd-2H5VLF3P.js +0 -153
  46. package/dist/syncCmd-A2W3QB3S.js +0 -126
@@ -2,11 +2,11 @@
2
2
  * Windows platform telemetry collectors.
3
3
  *
4
4
  * Gathers metrics on Windows (win32) using Node.js standard APIs,
5
- * WMI / CIM queries, tasklist, and netstat.
5
+ * PowerShell CIM/WMI, Windows Performance Counters, tasklist, and netstat.
6
6
  */
7
7
 
8
8
  import os from 'node:os';
9
- import { run } from '../run.js';
9
+ import { run, runPowerShell } from '../run.js';
10
10
  import type {
11
11
  CpuData,
12
12
  MemoryData,
@@ -25,6 +25,50 @@ import type {
25
25
 
26
26
  let prevNetSample: { rxBytes: number; txBytes: number; ts: number } | null = null;
27
27
 
28
+ function safeParseFloat(s: string | undefined): number {
29
+ if (!s) return 0;
30
+ const n = parseFloat(s);
31
+ return Number.isFinite(n) ? n : 0;
32
+ }
33
+
34
+ function safeParseInt(s: string | undefined, def = 0): number {
35
+ if (!s) return def;
36
+ const n = parseInt(s, 10);
37
+ return Number.isFinite(n) ? n : def;
38
+ }
39
+
40
+ function parsePipeOutput(raw: string): string[][] {
41
+ return raw
42
+ .split('\n')
43
+ .map(l => l.trim())
44
+ .filter(l => l.length > 0)
45
+ .map(l => l.split('|').map(s => s.trim()));
46
+ }
47
+
48
+ async function getCimProperty(className: string, property: string): Promise<string> {
49
+ const raw = await runPowerShell(
50
+ `(Get-CimInstance -ClassName ${className} -ErrorAction SilentlyContinue | Select-Object -ExpandProperty ${property}) -join '|'`,
51
+ ''
52
+ );
53
+ return raw.split('|')[0] || '';
54
+ }
55
+
56
+ async function getCimRow(className: string): Promise<Record<string, string>> {
57
+ const raw = await runPowerShell(
58
+ `Get-CimInstance -ClassName ${className} -ErrorAction SilentlyContinue | Select-Object -Property * -ExcludeProperty CIM* | ConvertTo-Csv -NoTypeInformation`,
59
+ ''
60
+ );
61
+ const lines = raw.split('\n').filter(l => l.trim().length > 0);
62
+ if (lines.length < 2) return {};
63
+ const headers = lines[0].split(',').map(h => h.trim().replace(/^"|"$/g, ''));
64
+ const values = lines[1].split(',').map(v => v.trim().replace(/^"|"$/g, ''));
65
+ const row: Record<string, string> = {};
66
+ headers.forEach((h, i) => {
67
+ row[h] = values[i] || '';
68
+ });
69
+ return row;
70
+ }
71
+
28
72
  export async function collectWindowsSystem(): Promise<{ hostname: string; os: string; uptime: string }> {
29
73
  const hostname = os.hostname();
30
74
  const rel = os.release();
@@ -52,8 +96,19 @@ export async function collectWindowsSystem(): Promise<{ hostname: string; os: st
52
96
  export async function collectWindowsCpu(prevCpuTimesRef: { current: { total: number; idle: number }[] | null }): Promise<CpuData> {
53
97
  const cpus = os.cpus();
54
98
  const cores = cpus.length || 1;
55
- const brand = cpus[0]?.model || 'Windows CPU';
56
- const frequency = cpus[0]?.speed || 0;
99
+
100
+ let brand = cpus[0]?.model || 'Windows CPU';
101
+ let physicalCores = cores;
102
+ let frequency = cpus[0]?.speed || 0;
103
+
104
+ try {
105
+ const cpuRow = await getCimRow('Win32_Processor');
106
+ if (cpuRow.Name) brand = cpuRow.Name.trim();
107
+ if (cpuRow.NumberOfCores) physicalCores = safeParseInt(cpuRow.NumberOfCores, cores);
108
+ if (cpuRow.MaxClockSpeed) frequency = safeParseInt(cpuRow.MaxClockSpeed, frequency);
109
+ } catch {
110
+ // ignore
111
+ }
57
112
 
58
113
  const current = cpus.map(cpu => ({
59
114
  total: cpu.times.user + cpu.times.nice + cpu.times.sys + cpu.times.idle + cpu.times.irq,
@@ -62,9 +117,10 @@ export async function collectWindowsCpu(prevCpuTimesRef: { current: { total: num
62
117
 
63
118
  let coreUsage: number[] = [];
64
119
  let usage = 0;
65
- if (prevCpuTimesRef.current && prevCpuTimesRef.current.length === current.length) {
120
+ const prevCpuTimes = prevCpuTimesRef.current;
121
+ if (prevCpuTimes && prevCpuTimes.length === current.length) {
66
122
  coreUsage = current.map((c, i) => {
67
- const prev = prevCpuTimesRef.current![i];
123
+ const prev = prevCpuTimes[i];
68
124
  const totalDelta = c.total - prev.total;
69
125
  const idleDelta = c.idle - prev.idle;
70
126
  if (totalDelta <= 0) return 0;
@@ -76,33 +132,42 @@ export async function collectWindowsCpu(prevCpuTimesRef: { current: { total: num
76
132
  }
77
133
  prevCpuTimesRef.current = current;
78
134
 
79
- // Load avg simulation (Windows returns [0, 0, 0] from os.loadavg())
80
135
  const activeLoad = Math.round((usage / 100) * cores * 100) / 100;
81
136
  const loadAvg = [activeLoad, activeLoad, activeLoad];
82
137
 
83
- // Temperature via WMI or estimation
84
138
  let temperature: number | undefined;
139
+
85
140
  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;
141
+ const raw = await runPowerShell(
142
+ `(Get-Counter '\Thermal Zone Information(*)\*' -ErrorAction SilentlyContinue).CounterSamples | Where-Object { $_.CookedValue -gt 200 -and $_.CookedValue -lt 400 } | Select-Object -First 1 -ExpandProperty CookedValue`,
143
+ ''
144
+ ).catch(() => '');
145
+ const tempK = parseFloat(raw);
146
+ if (tempK > 200 && tempK < 400) {
147
+ temperature = Math.round((tempK - 273.15) * 10) / 10;
93
148
  }
94
149
  } catch {
95
150
  // ignore
96
151
  }
97
152
 
98
153
  if (temperature === undefined) {
99
- temperature = Math.round((38 + (usage * 0.42)) * 10) / 10;
154
+ try {
155
+ const rawWmi = await run('wmic /namespace:\\\\root\\wmi PATH MSAcpi_ThermalZoneTemperature get CurrentTemperature 2>nul', '', 1500);
156
+ const match = rawWmi.match(/(\d{3,4})/);
157
+ if (match) {
158
+ const kelvinTenths = parseInt(match[1], 10);
159
+ const c = Math.round(((kelvinTenths - 2732) / 10) * 10) / 10;
160
+ if (c >= 15 && c <= 115) temperature = c;
161
+ }
162
+ } catch {
163
+ // ignore
164
+ }
100
165
  }
101
166
 
102
167
  return {
103
168
  brand,
104
169
  cores,
105
- physicalCores: cores,
170
+ physicalCores,
106
171
  frequency,
107
172
  usage,
108
173
  loadAvg,
@@ -122,18 +187,19 @@ export async function collectWindowsMemory(): Promise<MemoryData> {
122
187
  let swapFree = 0;
123
188
 
124
189
  try {
125
- const rawPage = (await run('wmic pagefile get AllocatedBaseSize,CurrentUsage 2>nul', '', 1500)).trim();
190
+ const rawPage = await runPowerShell(
191
+ `Get-CimInstance Win32_PageFileUsage | ForEach-Object { "$($_.AllocatedBaseSize)|$($_.CurrentUsage)" }`,
192
+ ''
193
+ );
126
194
  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
- }
195
+ for (const line of lines) {
196
+ const [allocStr, usedStr] = line.split('|');
197
+ const allocMb = safeParseInt(allocStr);
198
+ const usedMb = safeParseInt(usedStr);
199
+ swapTotal += allocMb * 1024 * 1024;
200
+ swapUsed += usedMb * 1024 * 1024;
136
201
  }
202
+ swapFree = Math.max(0, swapTotal - swapUsed);
137
203
  } catch {
138
204
  // ignore
139
205
  }
@@ -157,16 +223,18 @@ export async function collectWindowsMemory(): Promise<MemoryData> {
157
223
 
158
224
  export async function collectWindowsDisk(): Promise<DiskData[]> {
159
225
  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);
226
+ const raw = await runPowerShell(
227
+ `Get-CimInstance Win32_LogicalDisk -Filter "DriveType=3" | ForEach-Object { "$($_.DeviceID)|$($_.FileSystem)|$($_.Size)|$($_.FreeSpace)" }`,
228
+ ''
229
+ );
162
230
  const disks: DiskData[] = [];
163
231
 
164
- for (const line of lines) {
165
- const parts = line.trim().split(/\s+/);
232
+ for (const line of raw.split('\n')) {
233
+ const parts = line.trim().split('|');
166
234
  if (parts.length >= 4) {
167
235
  const drive = parts[0];
168
- const freeBytes = parseInt(parts[2], 10) || 0;
169
- const totalBytes = parseInt(parts[3], 10) || 0;
236
+ const totalBytes = safeParseInt(parts[2]);
237
+ const freeBytes = safeParseInt(parts[3]);
170
238
  if (totalBytes > 0) {
171
239
  const usedBytes = Math.max(0, totalBytes - freeBytes);
172
240
  const capPct = Math.round((usedBytes / totalBytes) * 100);
@@ -199,7 +267,7 @@ export async function collectWindowsDisk(): Promise<DiskData[]> {
199
267
  mountpoint: 'C:',
200
268
  readBytesSec: 0,
201
269
  writeBytesSec: 0,
202
- }
270
+ },
203
271
  ];
204
272
  }
205
273
 
@@ -212,117 +280,283 @@ function formatBytesStr(bytes: number): string {
212
280
 
213
281
  export async function collectWindowsBattery(): Promise<BatteryData | null> {
214
282
  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
- }
283
+ const raw = await runPowerShell(
284
+ `$bat = Get-CimInstance Win32_Battery -ErrorAction SilentlyContinue; if ($bat) { "$($bat.EstimatedChargeRemaining)|$($bat.BatteryStatus)|$($bat.EstimatedRunTime)|$($bat.DischargeRate)" } else { '' }`,
285
+ ''
286
+ );
287
+ if (!raw) return null;
288
+
289
+ const parts = raw.split('|');
290
+ const level = safeParseInt(parts[0], 0);
291
+ const statusCode = safeParseInt(parts[1], 1);
292
+ const runTimeMinutes = safeParseInt(parts[2], 0);
293
+ const dischargeRate = safeParseInt(parts[3], 0);
294
+
295
+ const powerSource = statusCode === 2 || statusCode === 3 || statusCode === 6 ? 'AC' : 'Battery';
296
+ const state = statusCode === 3 ? 'charged' : statusCode === 6 || statusCode === 2 ? 'charging' : 'discharging';
297
+ let timeRemaining = 'calculating';
298
+ if (state === 'charged') timeRemaining = '∞';
299
+ else if (runTimeMinutes && runTimeMinutes > 0 && runTimeMinutes < 70000) {
300
+ const h = Math.floor(runTimeMinutes / 60);
301
+ const m = runTimeMinutes % 60;
302
+ timeRemaining = `${h}:${m.toString().padStart(2, '0')}`;
243
303
  }
304
+
305
+ const powerWatts = dischargeRate > 0 ? Math.round((dischargeRate / 1000) * 100) / 100 : undefined;
306
+
307
+ return {
308
+ level,
309
+ state,
310
+ timeRemaining,
311
+ health: 'Good',
312
+ powerSource,
313
+ powerWatts,
314
+ };
244
315
  } catch {
245
- // ignore
316
+ return null;
246
317
  }
247
- return null;
248
318
  }
249
319
 
250
320
  export async function collectWindowsThermal(): Promise<ThermalData> {
321
+ const temperatures: Record<string, number | null> = {};
322
+
251
323
  let tempC: number | undefined;
324
+
252
325
  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;
326
+ const raw = await runPowerShell(
327
+ `(Get-Counter '\Thermal Zone Information(*)\*' -ErrorAction SilentlyContinue).CounterSamples | Where-Object { $_.CookedValue -gt 200 -and $_.CookedValue -lt 400 } | Select-Object -First 1 -ExpandProperty CookedValue`,
328
+ ''
329
+ ).catch(() => '');
330
+ const tempK = parseFloat(raw);
331
+ if (tempK > 200 && tempK < 400) {
332
+ tempC = Math.round((tempK - 273.15) * 10) / 10;
333
+ temperatures['cpu'] = tempC;
259
334
  }
260
335
  } catch {
261
336
  // ignore
262
337
  }
263
338
 
264
339
  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;
340
+ try {
341
+ const rawWmi = await run('wmic /namespace:\\\\root\\wmi PATH MSAcpi_ThermalZoneTemperature get CurrentTemperature 2>nul', '', 1500);
342
+ const match = rawWmi.match(/(\d{3,4})/);
343
+ if (match) {
344
+ const kelvinTenths = parseInt(match[1], 10);
345
+ const c = Math.round(((kelvinTenths - 2732) / 10) * 10) / 10;
346
+ if (c >= 15 && c <= 115) {
347
+ tempC = c;
348
+ temperatures['cpu'] = c;
349
+ }
350
+ }
351
+ } catch {
352
+ // ignore
353
+ }
354
+ }
355
+
356
+ let gpuTemp: number | undefined;
357
+ try {
358
+ const gpuRaw = await runPowerShell(
359
+ `(Get-Counter '\GPU(*)\*' -ErrorAction SilentlyContinue).CounterSamples | Where-Object { $_.CounterName -match 'Temperature' -and $_.CookedValue -gt 0 } | Select-Object -First 1 -ExpandProperty CookedValue`,
360
+ ''
361
+ ).catch(() => '');
362
+ gpuTemp = parseFloat(gpuRaw);
363
+ if (gpuTemp > 0 && gpuTemp < 150) {
364
+ temperatures['gpu'] = Math.round(gpuTemp * 10) / 10;
365
+ }
366
+ } catch {
367
+ // ignore
268
368
  }
269
369
 
370
+ const cpuTemp = tempC ?? gpuTemp;
270
371
  let state = 'Nominal';
271
372
  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;
373
+ if (cpuTemp !== undefined) {
374
+ if (cpuTemp >= 100) {
375
+ state = 'Critical';
376
+ pressureLevel = 3;
377
+ } else if (cpuTemp >= 90) {
378
+ state = 'Serious';
379
+ pressureLevel = 2;
380
+ } else if (cpuTemp >= 80) {
381
+ state = 'Fair';
382
+ pressureLevel = 1;
383
+ }
281
384
  }
282
385
 
283
386
  return {
284
387
  state,
285
- detail: `${state} (${tempC}°C)`,
388
+ detail: cpuTemp !== undefined ? `${state} (${cpuTemp}°C)` : state,
286
389
  pressureLevel,
287
- temperatures: { cpu: tempC },
390
+ temperatures: Object.keys(temperatures).length ? temperatures : undefined,
288
391
  };
289
392
  }
290
393
 
291
394
  export async function collectWindowsPower(): Promise<PowerData | null> {
395
+ const power: PowerData = {};
396
+
397
+ try {
398
+ const batRaw = await runPowerShell(
399
+ `$bat = Get-CimInstance Win32_Battery -ErrorAction SilentlyContinue; if ($bat) { "$($bat.DischargeRate)|$($bat.BatteryStatus)" } else { '|0' }`,
400
+ ''
401
+ );
402
+ const [dischargeStr, statusStr] = batRaw.split('|');
403
+ const dischargeRate = safeParseInt(dischargeStr);
404
+ const statusCode = safeParseInt(statusStr, 1);
405
+
406
+ if (dischargeRate > 0 && (statusCode === 1 || statusCode === 2)) {
407
+ power.combinedWatts = Math.round((dischargeRate / 1000) * 100) / 100;
408
+ }
409
+ } catch {
410
+ // ignore
411
+ }
412
+
413
+ try {
414
+ const cpu = os.cpus();
415
+ const usage = cpu.length > 0 ? Math.round(cpu.reduce((a, c) => a + (c.times.user + c.times.nice + c.times.sys + c.times.irq - c.times.idle) / (c.times.user + c.times.nice + c.times.sys + c.times.idle + c.times.irq), 0) / cpu.length * 100) : 0;
416
+ const tdpWatts = 65;
417
+ if (usage > 0) {
418
+ power.cpuWatts = Math.round((usage / 100) * tdpWatts * 100) / 100;
419
+ }
420
+ } catch {
421
+ // ignore
422
+ }
423
+
424
+ try {
425
+ const nvRaw = await run('nvidia-smi --query-gpu=power.draw,utilization.gpu --format=csv,noheader,nounits 2>nul', '').catch(() => '');
426
+ const nvLines = nvRaw.split('\n').filter(l => l.trim());
427
+ for (const line of nvLines) {
428
+ const parts = line.trim().split(',').map(s => s.trim());
429
+ const pwr = parseFloat(parts[0]);
430
+ if (pwr > 0) {
431
+ power.gpuWatts = Math.round(pwr * 100) / 100;
432
+ if (!power.combinedWatts) power.combinedWatts = power.gpuWatts;
433
+ else power.combinedWatts = Math.round((power.combinedWatts + power.gpuWatts) * 100) / 100;
434
+ break;
435
+ }
436
+ }
437
+ } catch {
438
+ // ignore
439
+ }
440
+
441
+ if (power.combinedWatts || power.cpuWatts || power.gpuWatts) {
442
+ if (!power.combinedWatts) {
443
+ power.combinedWatts = Math.round(((power.cpuWatts || 0) + (power.gpuWatts || 0)) * 100) / 100;
444
+ }
445
+ return power;
446
+ }
447
+
292
448
  return null;
293
449
  }
294
450
 
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 };
451
+ async function getNvidiaGpu(): Promise<GpuData | null> {
452
+ try {
453
+ const nvRaw = await run('nvidia-smi --query-gpu=gpu_name,memory.total,utilization.gpu,temperature.gpu,power.draw --format=csv,noheader,nounits 2>nul', '');
454
+ const nvLines = nvRaw.split('\n').filter(l => l.trim());
455
+ if (nvLines.length > 0) {
456
+ const parts = nvLines[0].trim().split(',').map(s => s.trim());
457
+ if (parts.length >= 3) {
458
+ const model = parts[0];
459
+ const memory = (parseFloat(parts[1]) || 0) * 1024 * 1024;
460
+ const utilization = Math.min(100, Math.max(0, Math.round(parseFloat(parts[2]) || 0)));
461
+ const temperature = parts[3] ? parseFloat(parts[3]) : undefined;
462
+ const powerDraw = parts[4] ? parseFloat(parts[4]) : undefined;
463
+ return {
464
+ model,
465
+ memory,
466
+ utilization,
467
+ temperature: Number.isFinite(temperature as number) ? temperature : undefined,
468
+ processes: 0,
469
+ powerDraw: Number.isFinite(powerDraw as number) ? powerDraw : undefined,
470
+ };
471
+ }
306
472
  }
473
+ } catch {
474
+ // ignore
307
475
  }
476
+ return null;
477
+ }
308
478
 
309
- // 2. Try WMIC VideoController
479
+ async function getWmiGpu(): Promise<GpuData | null> {
310
480
  try {
311
- const raw = (await run('wmic path win32_VideoController get Name,AdapterRAM 2>nul', '', 2000)).trim();
481
+ const raw = await runPowerShell(
482
+ `Get-CimInstance Win32_VideoController | Select-Object Name, AdapterRAM, DriverVersion, VideoProcessor | ConvertTo-Csv -NoTypeInformation`,
483
+ ''
484
+ );
312
485
  const lines = raw.split('\n').filter(l => l.trim().length > 0);
313
486
  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 };
487
+ const headers = lines[0].split(',').map(h => h.trim().replace(/^"|"$/g, ''));
488
+ const values = lines[1].split(',').map(v => v.trim().replace(/^"|"$/g, ''));
489
+ const row: Record<string, string> = {};
490
+ headers.forEach((h, i) => { row[h] = values[i] || ''; });
491
+
492
+ const name = row.Name || row.VideoProcessor || 'Windows Display Adapter';
493
+ const ram = safeParseInt(row.AdapterRAM);
494
+ return {
495
+ model: name,
496
+ memory: ram,
497
+ utilization: 0,
498
+ processes: 0,
499
+ };
500
+ }
501
+ } catch {
502
+ // ignore
503
+ }
504
+ return null;
505
+ }
506
+
507
+ async function getPerfCounterGpu(): Promise<GpuData | null> {
508
+ try {
509
+ const raw = await runPowerShell(
510
+ `$engines = Get-Counter '\GPU Engine(*)\*' -ErrorAction SilentlyContinue; if ($engines) { $engines.CounterSamples | Where-Object { $_.CounterName -match 'Utilization Percentage|GPU Time' -and $_.CookedValue -gt 0 } | ForEach-Object { "$($_.InstanceName)|$($_.CounterName)|$($_.CookedValue)" } }`,
511
+ ''
512
+ ).catch(() => '');
513
+ if (!raw) return null;
514
+
515
+ const lines = raw.split('\n').filter(l => l.trim());
516
+ if (lines.length === 0) return null;
517
+
518
+ const gpuGroups = new Map<string, { name: string; values: number[] }>();
519
+ for (const line of lines) {
520
+ const [instanceName, counterName, valueStr] = line.split('|');
521
+ const baseName = instanceName.replace(/\s*\(\d+\)\s*$/, '').trim();
522
+ if (!gpuGroups.has(baseName)) {
523
+ gpuGroups.set(baseName, { name: baseName, values: [] });
524
+ }
525
+ gpuGroups.get(baseName)!.values.push(parseFloat(valueStr) || 0);
526
+ }
527
+
528
+ let bestGpu: { name: string; util: number } | null = null;
529
+ for (const group of gpuGroups.values()) {
530
+ const total = group.values.reduce((a, b) => a + b, 0);
531
+ const util = Math.min(100, Math.round(total));
532
+ if (!bestGpu || util > bestGpu.util) {
533
+ bestGpu = { name: group.name, util };
321
534
  }
322
535
  }
536
+
537
+ if (bestGpu) {
538
+ return {
539
+ model: bestGpu.name,
540
+ memory: 0,
541
+ utilization: bestGpu.util,
542
+ processes: 0,
543
+ };
544
+ }
323
545
  } catch {
324
546
  // ignore
325
547
  }
548
+ return null;
549
+ }
550
+
551
+ export async function collectWindowsGpu(): Promise<GpuData | null> {
552
+ const nv = await getNvidiaGpu();
553
+ if (nv) return nv;
554
+
555
+ const perf = await getPerfCounterGpu();
556
+ if (perf) return perf;
557
+
558
+ const wmi = await getWmiGpu();
559
+ if (wmi) return wmi;
326
560
 
327
561
  return null;
328
562
  }
@@ -346,7 +580,17 @@ export async function collectWindowsNetwork(): Promise<NetworkData> {
346
580
  let txPackets = 0;
347
581
 
348
582
  try {
349
- const netstatE = (await run('netstat -e 2>nul', '', 1500)).trim();
583
+ const raw = await runPowerShell(
584
+ `Get-NetIPConfiguration -AddressFamily IPv4 -ErrorAction SilentlyContinue | Where-Object { $_.NetAdapter.Status -eq 'Up' } | Select-Object -First 1 -ExpandProperty NetAdapter.Name`,
585
+ ''
586
+ );
587
+ if (raw) iface = raw;
588
+ } catch {
589
+ // ignore
590
+ }
591
+
592
+ try {
593
+ const netstatE = await run('netstat -e 2>nul', '', 1500);
350
594
  const lines = netstatE.split('\n');
351
595
  for (const line of lines) {
352
596
  const parts = line.trim().split(/\s+/);
@@ -398,18 +642,29 @@ export async function collectWindowsNetwork(): Promise<NetworkData> {
398
642
  async function getWindowsConnectionStates(): Promise<ConnectionStateStats> {
399
643
  const stats: ConnectionStateStats = { established: 0, listening: 0, timeWait: 0, closeWait: 0, other: 0 };
400
644
  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
- }
645
+ const raw = await runPowerShell(
646
+ `Get-NetTCPConnection -State Established -ErrorAction SilentlyContinue | Measure-Object | Select-Object -ExpandProperty Count`,
647
+ '0'
648
+ );
649
+ stats.established = safeParseInt(raw, 0);
650
+
651
+ const listenRaw = await runPowerShell(
652
+ `Get-NetTCPConnection -State Listen -ErrorAction SilentlyContinue | Measure-Object | Select-Object -ExpandProperty Count`,
653
+ '0'
654
+ );
655
+ stats.listening = safeParseInt(listenRaw, 0);
656
+
657
+ const twRaw = await runPowerShell(
658
+ `Get-NetTCPConnection -State TimeWait -ErrorAction SilentlyContinue | Measure-Object | Select-Object -ExpandProperty Count`,
659
+ '0'
660
+ );
661
+ stats.timeWait = safeParseInt(twRaw, 0);
662
+
663
+ const cwRaw = await runPowerShell(
664
+ `Get-NetTCPConnection -State CloseWait -ErrorAction SilentlyContinue | Measure-Object | Select-Object -ExpandProperty Count`,
665
+ '0'
666
+ );
667
+ stats.closeWait = safeParseInt(cwRaw, 0);
413
668
  } catch {
414
669
  // ignore
415
670
  }
@@ -423,7 +678,7 @@ export async function collectWindowsPackets(): Promise<PacketData | null> {
423
678
  let txPackets = 0;
424
679
 
425
680
  try {
426
- const netstatE = (await run('netstat -e 2>nul', '', 1500)).trim();
681
+ const netstatE = await run('netstat -e 2>nul', '', 1500);
427
682
  const lines = netstatE.split('\n');
428
683
  for (const line of lines) {
429
684
  const parts = line.trim().split(/\s+/);
@@ -456,44 +711,65 @@ export async function collectWindowsPackets(): Promise<PacketData | null> {
456
711
  };
457
712
  }
458
713
 
714
+ const winProcCache = new Map<number, { user: number; kernel: number; ts: number }>();
715
+
459
716
  export async function collectWindowsProcesses(limit?: number): Promise<ProcessData[]> {
460
717
  try {
461
- const raw = (await run('tasklist /fo csv /nh 2>nul', '', 3000)).trim();
462
- if (!raw) return [];
463
- const lines = raw.split('\n');
718
+ const raw = await runPowerShell(
719
+ `Get-CimInstance Win32_Process | Select-Object Name, ProcessId, ThreadCount, WorkingSetSize, UserModeTime, KernelModeTime | ConvertTo-Csv -NoTypeInformation`,
720
+ ''
721
+ );
722
+ const lines = raw.split('\n').filter(l => l.trim().length > 0 && !l.startsWith('"Name"'));
723
+ const now = Date.now();
464
724
  const procs: ProcessData[] = [];
465
725
  const totalMem = os.totalmem();
466
726
 
467
727
  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
- });
728
+ const parts = line.split(',').map(s => s.trim().replace(/^"|"$/g, ''));
729
+ if (parts.length < 6) continue;
730
+ const name = parts[0];
731
+ const pid = safeParseInt(parts[1], 0);
732
+ const threads = safeParseInt(parts[2], 0);
733
+ const memBytes = safeParseInt(parts[3], 0);
734
+ const userTime = safeParseInt(parts[4], 0);
735
+ const kernelTime = safeParseInt(parts[5], 0);
736
+
737
+ if (!pid || pid <= 0) continue;
738
+
739
+ const totalTime = userTime + kernelTime;
740
+ let cpu = 0;
741
+
742
+ const prev = winProcCache.get(pid);
743
+ if (prev) {
744
+ const dt = now - prev.ts;
745
+ if (dt > 0) {
746
+ const timeDelta = totalTime - (prev.user + prev.kernel);
747
+ if (timeDelta > 0) {
748
+ const cpuSeconds = timeDelta / 10_000_000;
749
+ cpu = Math.min(100, Math.round((cpuSeconds / dt) * 10000) / 100);
750
+ }
491
751
  }
492
752
  }
753
+ winProcCache.set(pid, { user: userTime, kernel: kernelTime, ts: now });
754
+
755
+ const memPct = totalMem > 0 ? Math.round((memBytes / totalMem) * 1000) / 10 : 0;
756
+ const isGpuOrMl = /gpu|vulkan|cuda|ollama|llama|torch|python|nvidia|amd|radeon|intel.*graphics/i.test(name);
757
+
758
+ procs.push({
759
+ pid,
760
+ ppid: 0,
761
+ user: 'SYSTEM',
762
+ cpu,
763
+ mem: memPct,
764
+ state: 'R',
765
+ threads,
766
+ runtime: 0,
767
+ command: name,
768
+ isGpuOrMlAttributed: isGpuOrMl,
769
+ });
493
770
  }
494
771
 
495
- // Sort by memory descending
496
- procs.sort((a, b) => b.mem - a.mem);
772
+ procs.sort((a, b) => b.cpu - a.cpu || b.mem - a.mem);
497
773
  if (limit && limit > 0) return procs.slice(0, limit);
498
774
  return procs;
499
775
  } catch {
@@ -515,5 +791,24 @@ export async function collectWindowsTasks(limit = 12): Promise<TaskData[]> {
515
791
  }
516
792
 
517
793
  export async function getWindowsDisplayInfo(): Promise<DisplayInfo[]> {
518
- return [{ name: 'Default Display', resolution: '1920x1080', isMain: true }];
794
+ try {
795
+ const raw = await runPowerShell(
796
+ `Get-CimInstance Win32_VideoController | ForEach-Object { "$($_.Name)|$($_.VideoModeDescription)|$($_.VideoProcessor)" }`,
797
+ ''
798
+ );
799
+ const displays: DisplayInfo[] = [];
800
+ for (const line of raw.split('\n')) {
801
+ const parts = line.trim().split('|');
802
+ if (parts[0]) {
803
+ displays.push({
804
+ name: parts[0],
805
+ resolution: parts[1] || 'Unknown',
806
+ isMain: displays.length === 0,
807
+ });
808
+ }
809
+ }
810
+ return displays.length > 0 ? displays : [{ name: 'Default Display', resolution: '1920x1080', isMain: true }];
811
+ } catch {
812
+ return [{ name: 'Default Display', resolution: '1920x1080', isMain: true }];
813
+ }
519
814
  }