pyre-cli 10.4.0 → 10.5.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pyre-cli",
3
- "version": "10.4.0",
3
+ "version": "10.5.0",
4
4
  "description": "Cross-platform system monitoring CLI for macOS, Linux, and Windows: temps, system stats, live dashboard, packet monitor, battery predictor, export",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -562,7 +562,15 @@ export async function collectWindowsPower(): Promise<PowerData | null> {
562
562
 
563
563
  // ─── GPU Telemetry Across Intel, AMD, NVIDIA ─────────────────────────────────
564
564
 
565
+ let nvidiaSmiPathCache: { path: string | null; checkedAt: number } | null = null;
566
+ const NVIDIA_SMI_NEGATIVE_TTL_MS = 5 * 60 * 1000;
567
+
565
568
  async function findNvidiaSmiPath(): Promise<string> {
569
+ if (nvidiaSmiPathCache) {
570
+ if (nvidiaSmiPathCache.path) return nvidiaSmiPathCache.path;
571
+ // Not found previously: only re-probe occasionally (e.g. driver installed while running)
572
+ if (Date.now() - nvidiaSmiPathCache.checkedAt < NVIDIA_SMI_NEGATIVE_TTL_MS) return 'nvidia-smi';
573
+ }
566
574
  const commonPaths = [
567
575
  'nvidia-smi',
568
576
  path.join(process.env.ProgramFiles || 'C:\\Program Files', 'NVIDIA Corporation', 'NVSMI', 'nvidia-smi.exe'),
@@ -571,14 +579,19 @@ async function findNvidiaSmiPath(): Promise<string> {
571
579
  for (const p of commonPaths) {
572
580
  try {
573
581
  const test = await run(`"${p}" -L 2>nul`, '');
574
- if (test.includes('GPU 0:')) return p;
582
+ if (test.includes('GPU 0:')) {
583
+ nvidiaSmiPathCache = { path: p, checkedAt: Date.now() };
584
+ return p;
585
+ }
575
586
  } catch {}
576
587
  }
588
+ nvidiaSmiPathCache = { path: null, checkedAt: Date.now() };
577
589
  return 'nvidia-smi';
578
590
  }
579
591
 
580
592
  async function runNvidiaSmiQuery(query: string): Promise<string> {
581
593
  const smiPath = await findNvidiaSmiPath();
594
+ if (nvidiaSmiPathCache && nvidiaSmiPathCache.path === null) return '';
582
595
  return (await run(`"${smiPath}" --query-gpu=${query} --format=csv,noheader,nounits 2>nul`, '')).trim();
583
596
  }
584
597
 
@@ -656,50 +669,60 @@ export function parseWindowsVideoControllers(csvRaw: string): WindowsVideoContro
656
669
  return controllers;
657
670
  }
658
671
 
672
+ function isVirtualGpuName(name: string): boolean {
673
+ return /microsoft basic|remote display|hyper-v|virtual|vmware|virtualbox|parsec|citrix|indirect display|displaylink/i.test(name);
674
+ }
675
+
676
+ function isDiscreteGpuName(name: string): boolean {
677
+ const n = name.toLowerCase();
678
+ if (isVirtualGpuName(n)) return false;
679
+ return (
680
+ /\b(rtx|gtx|geforce|quadro|titan|tesla)\b/.test(n) ||
681
+ /\bnvidia\b/.test(n) ||
682
+ /radeon\s+(rx|pro|vii|r[5-9]|hd)\b/.test(n) ||
683
+ /\bfirepro\b/.test(n) ||
684
+ (/\barc\b/.test(n) && /\b(a\d{3}|b\d{3}|pro)\b/.test(n))
685
+ );
686
+ }
687
+
659
688
  export function prioritizeGpuControllers(controllers: WindowsVideoControllerInfo[]): WindowsVideoControllerInfo[] {
660
- // Sort discrete cards (NVIDIA, AMD Radeon RX, Intel Arc) before integrated adapters (Intel UHD, AMD Radeon(TM) Graphics)
689
+ // Order: discrete (NVIDIA, AMD Radeon RX/Pro, Intel Arc) > integrated (Intel UHD/Iris/Xe, AMD Radeon(TM) Graphics) > virtual/basic adapters
690
+ const rank = (c: WindowsVideoControllerInfo) =>
691
+ isVirtualGpuName(c.name) ? 2 : isDiscreteGpuName(c.name) ? 0 : 1;
661
692
  return [...controllers].sort((a, b) => {
662
- const aName = a.name.toLowerCase();
663
- const bName = b.name.toLowerCase();
664
- const aIsDiscrete = aName.includes('rtx') || aName.includes('gtx') || aName.includes('geforce') ||
665
- aName.includes('radeon rx') || aName.includes('arc');
666
- const bIsDiscrete = bName.includes('rtx') || bName.includes('gtx') || bName.includes('geforce') ||
667
- bName.includes('radeon rx') || bName.includes('arc');
668
- if (aIsDiscrete && !bIsDiscrete) return -1;
669
- if (!aIsDiscrete && bIsDiscrete) return 1;
693
+ const ra = rank(a);
694
+ const rb = rank(b);
695
+ if (ra !== rb) return ra - rb;
670
696
  return b.adapterRam - a.adapterRam;
671
697
  });
672
698
  }
673
699
 
674
- export async function getWindowsGpu64BitVram(): Promise<number> {
700
+ export async function getWindowsGpu64BitVram(adapterName?: string): Promise<number> {
675
701
  // Win32_VideoController clamps AdapterRAM to 4GB (UInt32).
676
- // Read exact 64-bit VRAM from Registry Display Class hardware information:
702
+ // Read exact 64-bit VRAM from the Display Class registry keys. The value is literally named
703
+ // 'HardwareInformation.qwMemorySize' (dot is part of the name), so it must be quoted.
704
+ // Match by DriverDesc so hybrid systems report the VRAM of the selected adapter.
677
705
  try {
706
+ const safeName = (adapterName || '').replace(/'/g, "''");
678
707
  const raw = await runPowerShell(
679
- `Get-ItemProperty -Path 'HKLM:\\SYSTEM\\CurrentControlSet\\Control\\Class\\{4d36e968-e325-11ce-bfc1-08002be10318}\\0*' -ErrorAction SilentlyContinue | Where-Object { $_.HardwareInformation.qwMemorySize -gt 0 } | Select-Object -First 1 -ExpandProperty HardwareInformation.qwMemorySize`,
708
+ `$name = '${safeName}'; $items = Get-ItemProperty -Path 'HKLM:\\SYSTEM\\CurrentControlSet\\Control\\Class\\{4d36e968-e325-11ce-bfc1-08002be10318}\\0*' -ErrorAction SilentlyContinue | Where-Object { $_.'HardwareInformation.qwMemorySize' -gt 0 }; $pick = $items | Where-Object { $name -and $_.DriverDesc -eq $name } | Select-Object -First 1; if (-not $pick) { $pick = $items | Sort-Object -Property 'HardwareInformation.qwMemorySize' -Descending | Select-Object -First 1 }; if ($pick) { [string][uint64]$pick.'HardwareInformation.qwMemorySize' }`,
680
709
  ''
681
710
  );
682
- const bytes = safeParseInt(raw, 0);
683
- if (bytes > 0) return bytes;
684
- } catch {}
685
-
686
- // Fallback to Performance Counters Dedicated Usage + Free
687
- try {
688
- const rawDedicated = await runPowerShell(
689
- `(Get-Counter '\\GPU Adapter Memory(*)\\Dedicated Usage' -ErrorAction SilentlyContinue).CounterSamples | Measure-Object -Property CookedValue -Sum | Select-Object -ExpandProperty Sum`,
690
- ''
691
- );
692
- const bytes = safeParseFloat(rawDedicated);
711
+ const bytes = safeParseFloat(raw.trim());
693
712
  if (bytes > 0) return bytes;
694
713
  } catch {}
695
714
 
696
715
  return 0;
697
716
  }
698
717
 
718
+ /**
719
+ * 3D engine utilization via CIM performance classes. Unlike Get-Counter paths these class/property
720
+ * names are not localized, so this works on non-English Windows. Works for NVIDIA, AMD and Intel (WDDM 2.0+).
721
+ */
699
722
  export async function getWindowsGpu3dUtilization(): Promise<{ utilization: number; processCount: number }> {
700
723
  try {
701
724
  const raw = await runPowerShell(
702
- `$samples = (Get-Counter '\\GPU Engine(*engtype_3D*)\\Utilization Percentage' -ErrorAction SilentlyContinue).CounterSamples; if ($samples) { $activePids = $samples | Where-Object { $_.CookedValue -gt 0 } | ForEach-Object { if ($_.InstanceName -match 'pid_(\\d+)') { $matches[1] } } | Select-Object -Unique; $total = ($samples | Measure-Object -Property CookedValue -Sum).Sum; "$total|$($activePids.Count)" } else { '0|0' }`,
725
+ `$samples = Get-CimInstance Win32_PerfFormattedData_GPUPerformanceCounters_GPUEngine -ErrorAction SilentlyContinue | Where-Object { $_.Name -like '*engtype_3D*' }; if ($samples) { $activePids = $samples | Where-Object { $_.UtilizationPercentage -gt 0 } | ForEach-Object { if ($_.Name -match 'pid_(\\d+)') { $matches[1] } } | Select-Object -Unique; $total = ($samples | Measure-Object -Property UtilizationPercentage -Sum).Sum; "$total|$(@($activePids).Count)" } else { '0|0' }`,
703
726
  '0|0'
704
727
  );
705
728
  const [utilStr, procCountStr] = raw.split('|');
@@ -729,7 +752,7 @@ export async function collectWindowsGpu(): Promise<GpuData | null> {
729
752
  const bestController = prioritized[0];
730
753
 
731
754
  // Read 64-bit VRAM (avoids 4GB uint32 clamp on RX 7900, Arc A770, etc.)
732
- let vramBytes = await getWindowsGpu64BitVram();
755
+ let vramBytes = await getWindowsGpu64BitVram(bestController.name);
733
756
  if (vramBytes === 0 && bestController.adapterRam > 0) {
734
757
  vramBytes = bestController.adapterRam;
735
758
  }
@@ -119,6 +119,17 @@ describe('Intel, AMD & NVIDIA Hardware Telemetry', () => {
119
119
  assert.strictEqual(prioritized[0].name, 'Intel(R) Arc(TM) A770 Graphics');
120
120
  });
121
121
 
122
+ it('should rank hybrid laptop adapters: discrete > integrated > virtual', async () => {
123
+ const { prioritizeGpuControllers } = await import('./monitors/platform/windows.js');
124
+ const mk = (name: string, adapterRam = 0) => ({ name, adapterRam, driverVersion: '', videoProcessor: '', pnpDeviceId: '' });
125
+ const order = (names: string[]) => prioritizeGpuControllers(names.map(n => mk(n))).map(c => c.name);
126
+
127
+ assert.strictEqual(order(['Microsoft Basic Display Adapter', 'Intel(R) Iris(R) Xe Graphics', 'NVIDIA GeForce RTX 4060 Laptop GPU'])[0], 'NVIDIA GeForce RTX 4060 Laptop GPU');
128
+ assert.strictEqual(order(['Intel(R) UHD Graphics', 'Microsoft Remote Display Adapter'])[0], 'Intel(R) UHD Graphics');
129
+ assert.strictEqual(order(['AMD Radeon(TM) Graphics', 'AMD Radeon Pro W7800'])[0], 'AMD Radeon Pro W7800');
130
+ });
131
+
132
+
122
133
  it('should detect CPU vendors and estimate architecture TDP accurately', async () => {
123
134
  const { detectProcessorVendor, estimateCpuTdp } = await import('./monitors/platform/windows.js');
124
135
  // AMD tests