llm-checker 3.5.4 → 3.5.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -40,7 +40,7 @@ Choosing the right LLM for your hardware is complex. With thousands of model var
40
40
  |:---:|---|---|
41
41
  | **200+** | Dynamic Model Pool | Uses full scraped Ollama catalog/variants when available (with curated fallback) |
42
42
  | **4D** | Scoring Engine | Quality, Speed, Fit, Context — weighted by use case |
43
- | **Multi-GPU** | Hardware Detection | Apple Silicon, NVIDIA CUDA, AMD ROCm, Intel Arc, CPU |
43
+ | **Multi-GPU** | Hardware Detection | Apple Silicon, NVIDIA CUDA, AMD ROCm, Intel Arc, CPU, integrated/dedicated inventory visibility |
44
44
  | **Calibrated** | Memory Estimation | Bytes-per-parameter formula validated against real Ollama sizes |
45
45
  | **Zero** | Native Dependencies | Pure JavaScript — works on any Node.js 16+ system |
46
46
  | **Optional** | SQLite Search | Install `sql.js` to unlock `sync`, `search`, and `smart-recommend` |
@@ -82,6 +82,13 @@ npm install -g llm-checker
82
82
  npx llm-checker hw-detect
83
83
  ```
84
84
 
85
+ **Termux (Android):**
86
+ ```bash
87
+ pkg update
88
+ pkg install ollama
89
+ npm install -g llm-checker
90
+ ```
91
+
85
92
  **Requirements:**
86
93
  - Node.js 16+ (any version: 16, 18, 20, 22, 24)
87
94
  - [Ollama](https://ollama.ai) installed for running models
@@ -508,6 +515,16 @@ Metal:
508
515
  Memory Bandwidth: 273GB/s
509
516
  ```
510
517
 
518
+ On hybrid or integrated-only systems, `hw-detect` now also surfaces GPU topology explicitly:
519
+
520
+ ```
521
+ Dedicated GPUs: NVIDIA GeForce RTX 4060
522
+ Integrated GPUs: Intel Iris Xe Graphics
523
+ Assist path: Integrated/shared-memory GPU detected, runtime remains CPU
524
+ ```
525
+
526
+ This makes integrated GPUs visible even when the selected runtime backend is still CPU.
527
+
511
528
  ### `recommend` — Category Recommendations
512
529
 
513
530
  ```bash
@@ -16,6 +16,7 @@ function getLLMChecker() {
16
16
  const { getLogger } = require('../src/utils/logger');
17
17
  const fs = require('fs');
18
18
  const path = require('path');
19
+ const { normalizePlatform, isTermuxEnvironment } = require('../src/utils/platform');
19
20
  const {
20
21
  SUPPORTED_RUNTIMES,
21
22
  normalizeRuntime,
@@ -513,8 +514,23 @@ if (!program.commands.some((cmd) => cmd.name() === 'help')) {
513
514
 
514
515
  // Ollama installation helper
515
516
  function getOllamaInstallInstructions() {
516
- const platform = os.platform();
517
+ const rawPlatform = os.platform();
518
+ const platform = normalizePlatform(rawPlatform);
517
519
  const arch = os.arch();
520
+
521
+ if (isTermuxEnvironment(rawPlatform, process.env)) {
522
+ return {
523
+ name: `Termux (Android${arch ? ` ${arch}` : ''})`,
524
+ downloadUrl: 'https://github.com/termux/termux-packages',
525
+ instructions: [
526
+ '1. Update Termux packages: pkg update',
527
+ '2. Install Ollama from the Termux repository: pkg install ollama',
528
+ '3. Start Ollama in the current shell: ollama serve',
529
+ '4. In a new Termux session, test with: ollama run llama3.2:1b'
530
+ ],
531
+ alternativeInstall: 'pkg install ollama'
532
+ };
533
+ }
518
534
 
519
535
  const instructions = {
520
536
  'darwin': {
@@ -546,8 +562,8 @@ function getOllamaInstallInstructions() {
546
562
  '1. Review official installation options:',
547
563
  ' https://github.com/ollama/ollama/blob/main/docs/linux.md',
548
564
  '2. Prefer a package manager (apt/dnf/pacman) when available',
549
- '3. Start service after install:',
550
- ' sudo systemctl start ollama',
565
+ '3. Start Ollama after install:',
566
+ ' ollama serve',
551
567
  '4. Test with: ollama run llama2:7b'
552
568
  ],
553
569
  alternativeInstall: 'Manual install: https://github.com/ollama/ollama/blob/main/docs/linux.md'
@@ -856,6 +872,13 @@ function calculateModelCompatibilityScore(model, hardware) {
856
872
  return Math.max(0, Math.min(100, Math.round(score)));
857
873
  }
858
874
 
875
+ function formatGpuInventoryList(models = []) {
876
+ if (!Array.isArray(models) || models.length === 0) return 'None';
877
+ return models
878
+ .map(({ name, count }) => (count > 1 ? `${count}x ${name}` : name))
879
+ .join(' + ');
880
+ }
881
+
859
882
  // Helper function to get hardware tier for display
860
883
  function getHardwareTierForDisplay(hardware) {
861
884
  const ram = hardware.memory.total;
@@ -863,9 +886,15 @@ function getHardwareTierForDisplay(hardware) {
863
886
  const gpuModel = hardware.gpu?.model || '';
864
887
  const vramGB = hardware.gpu?.vram || 0;
865
888
 
866
- // Check if it's integrated GPU (should cap tier)
867
- const isIntegratedGPU = /iris.*xe|iris.*graphics|uhd.*graphics|vega.*integrated|radeon.*graphics|intel.*integrated|integrated/i.test(gpuModel);
868
- const hasDedicatedGPU = vramGB > 0 && !isIntegratedGPU;
889
+ const integratedGpuInventory = Array.isArray(hardware.summary?.integratedGpuModels)
890
+ ? hardware.summary.integratedGpuModels.map(({ name }) => name).join(' ')
891
+ : '';
892
+ const isIntegratedGPU = typeof hardware.summary?.hasIntegratedGPU === 'boolean'
893
+ ? hardware.summary.hasIntegratedGPU
894
+ : /iris.*xe|iris.*graphics|uhd.*graphics|vega.*integrated|radeon.*graphics|intel.*integrated|integrated/i.test(`${gpuModel} ${integratedGpuInventory}`);
895
+ const hasDedicatedGPU = typeof hardware.summary?.hasDedicatedGPU === 'boolean'
896
+ ? hardware.summary.hasDedicatedGPU
897
+ : (vramGB > 0 && !isIntegratedGPU);
869
898
  const isAppleSilicon = process.platform === 'darwin' && (gpuModel.toLowerCase().includes('apple') || gpuModel.toLowerCase().includes('m1') || gpuModel.toLowerCase().includes('m2') || gpuModel.toLowerCase().includes('m3') || gpuModel.toLowerCase().includes('m4'));
870
899
 
871
900
  // Base tier calculation
@@ -932,6 +961,8 @@ function displaySystemInfo(hardware, analysis) {
932
961
  const cpuColor = hardware.cpu.cores >= 8 ? chalk.green : hardware.cpu.cores >= 4 ? chalk.yellow : chalk.red;
933
962
  const ramColor = hardware.memory.total >= 32 ? chalk.green : hardware.memory.total >= 16 ? chalk.yellow : chalk.red;
934
963
  const gpuColor = hardware.gpu.dedicated ? chalk.green : chalk.hex('#FFA500');
964
+ const integratedList = formatGpuInventoryList(hardware.gpu.integratedGpuModels || hardware.summary?.integratedGpuModels);
965
+ const dedicatedList = formatGpuInventoryList(hardware.gpu.dedicatedGpuModels || hardware.summary?.dedicatedGpuModels);
935
966
 
936
967
  const lines = [
937
968
  `${chalk.cyan('CPU:')} ${cpuColor(hardware.cpu.brand)} ${chalk.gray(`(${hardware.cpu.cores} cores, ${hardware.cpu.speed}GHz)`)}`,
@@ -939,6 +970,8 @@ function displaySystemInfo(hardware, analysis) {
939
970
  `${chalk.cyan('RAM:')} ${ramColor(hardware.memory.total + 'GB')}`,
940
971
  `${chalk.cyan('GPU:')} ${gpuColor(hardware.gpu.model || 'Not detected')}`,
941
972
  `${chalk.cyan('VRAM:')} ${hardware.gpu.vram === 0 && hardware.gpu.model && hardware.gpu.model.toLowerCase().includes('apple') ? 'Unified Memory' : `${hardware.gpu.vram || 'N/A'}GB`}${hardware.gpu.dedicated ? chalk.green(' (Dedicated)') : chalk.hex('#FFA500')(' (Integrated)')}`,
973
+ `${chalk.cyan('Dedicated GPUs:')} ${chalk.green(dedicatedList)}`,
974
+ `${chalk.cyan('Integrated GPUs:')} ${chalk.hex('#FFA500')(integratedList)}`,
942
975
  ];
943
976
 
944
977
  const tier = analysis.summary.hardwareTier?.replace('_', ' ').toUpperCase() || 'UNKNOWN';
@@ -5026,6 +5059,11 @@ program
5026
5059
  console.log(` Tier: ${chalk.cyan(detector.getHardwareTier().replace('_', ' ').toUpperCase())}`);
5027
5060
  console.log(` Max model size: ${chalk.green(detector.getMaxModelSize() + 'GB')}`);
5028
5061
  console.log(` Best backend: ${chalk.cyan(hardware.summary.bestBackend)}`);
5062
+ console.log(` Dedicated GPUs: ${chalk.green(formatGpuInventoryList(hardware.summary.dedicatedGpuModels))}`);
5063
+ console.log(` Integrated GPUs: ${chalk.hex('#FFA500')(formatGpuInventoryList(hardware.summary.integratedGpuModels))}`);
5064
+ if (hardware.summary.hasIntegratedGPU && hardware.summary.bestBackend === 'cpu') {
5065
+ console.log(` Assist path: ${chalk.yellow('Integrated/shared-memory GPU detected, runtime remains CPU')}`);
5066
+ }
5029
5067
 
5030
5068
  // CPU
5031
5069
  if (hardware.cpu) {
@@ -187,7 +187,7 @@ const ALLOWED_CLI_COMMANDS = new Set([
187
187
 
188
188
  const server = new McpServer({
189
189
  name: "llm-checker",
190
- version: "3.4.0",
190
+ version: "3.5.7",
191
191
  });
192
192
 
193
193
  // ============================================================================
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "llm-checker",
3
- "version": "3.5.4",
3
+ "version": "3.5.7",
4
4
  "description": "Intelligent CLI tool with AI-powered model selection that analyzes your hardware and recommends optimal LLM models for your system",
5
5
  "bin": {
6
6
  "llm-checker": "bin/cli.js",
@@ -107,6 +107,7 @@
107
107
  "npm": ">=8.0.0"
108
108
  },
109
109
  "os": [
110
+ "android",
110
111
  "darwin",
111
112
  "linux",
112
113
  "win32"
@@ -9,6 +9,7 @@
9
9
  */
10
10
 
11
11
  const { MULTI_OBJECTIVE_WEIGHTS } = require('../models/scoring-config');
12
+ const { normalizePlatform } = require('../utils/platform');
12
13
 
13
14
  class MultiObjectiveSelector {
14
15
  constructor() {
@@ -346,12 +347,22 @@ class MultiObjectiveSelector {
346
347
  const unified = isAppleSilicon;
347
348
 
348
349
  // Detect PC platform (Windows/Linux) to match main algorithm
349
- const isPC = !isAppleSilicon && (process.platform === 'win32' || process.platform === 'linux');
350
+ const integratedGpuInventory = Array.isArray(hardware.summary?.integratedGpuModels)
351
+ ? hardware.summary.integratedGpuModels.map(({ name }) => name).join(' ')
352
+ : '';
353
+ const hasIntegratedGPU = typeof hardware.summary?.hasIntegratedGPU === 'boolean'
354
+ ? hardware.summary.hasIntegratedGPU
355
+ : /iris xe|uhd.*graphics|vega.*integrated|radeon.*graphics/i.test(`${gpuModel} ${integratedGpuInventory}`);
356
+ const hasDedicatedGPU = typeof hardware.summary?.hasDedicatedGPU === 'boolean'
357
+ ? hardware.summary.hasDedicatedGPU
358
+ : Boolean(hardware.gpu?.dedicated || (vramGB > 0 && !hasIntegratedGPU));
359
+ const platform = normalizePlatform(hardware?.os?.platform || process.platform);
360
+ const isPC = !isAppleSilicon && (platform === 'win32' || platform === 'linux');
350
361
 
351
362
  // 1) Effective memory for model weights (45%) - Apple Silicon & PC optimized
352
363
  let effMem;
353
364
 
354
- if (vramGB > 0 && !unified) {
365
+ if (hasDedicatedGPU && vramGB > 0 && !unified) {
355
366
  // Dedicated GPU path (Windows/Linux with discrete GPU)
356
367
  if (isPC) {
357
368
  // PC-specific GPU memory calculation with offload support
@@ -446,9 +457,9 @@ class MultiObjectiveSelector {
446
457
  tier = bumpTier(tier, +1); // High-end GPU boost
447
458
  } else if (!vramGB && !unified) {
448
459
  tier = bumpTier(tier, -1); // CPU-only penalty (moderate)
449
- } else if (/iris xe|uhd.*graphics|vega.*integrated|radeon.*graphics/i.test(gpuModel)) {
460
+ } else if (hasIntegratedGPU && !hasDedicatedGPU) {
450
461
  tier = bumpTier(tier, -1); // iGPU penalty
451
- } else if (vramGB > 0 && vramGB < 6) {
462
+ } else if (hasDedicatedGPU && vramGB > 0 && vramGB < 6) {
452
463
  tier = bumpTier(tier, -1); // Low VRAM penalty
453
464
  }
454
465
 
@@ -658,7 +669,10 @@ class MultiObjectiveSelector {
658
669
  const cpuModel = hardware.cpu?.brand || hardware.cpu?.model || '';
659
670
  const gpuModel = hardware.gpu?.model || '';
660
671
  const cpu = cpuModel.toLowerCase();
661
- const gpu = gpuModel.toLowerCase();
672
+ const integratedGpuInventory = Array.isArray(hardware.summary?.integratedGpuModels)
673
+ ? hardware.summary.integratedGpuModels.map(({ name }) => name).join(' ')
674
+ : '';
675
+ const gpu = `${gpuModel} ${integratedGpuInventory}`.toLowerCase();
662
676
  const cores = hardware.cpu?.physicalCores || hardware.cpu?.cores || 1;
663
677
 
664
678
  let specs = {
@@ -738,13 +752,19 @@ class MultiObjectiveSelector {
738
752
  const cores = hardware.cpu?.physicalCores || hardware.cpu?.cores || 1;
739
753
  const baseSpeed = hardware.cpu?.speed || 2.0;
740
754
  const vramGB = hardware.gpu?.vram || 0;
755
+ const hasIntegratedGPU = typeof hardware.summary?.hasIntegratedGPU === 'boolean'
756
+ ? hardware.summary.hasIntegratedGPU
757
+ : false;
758
+ const hasDedicatedGPU = typeof hardware.summary?.hasDedicatedGPU === 'boolean'
759
+ ? hardware.summary.hasDedicatedGPU
760
+ : Boolean(hardware.gpu?.dedicated || (vramGB > 0 && !hasIntegratedGPU));
741
761
 
742
762
  // Use improved CPU estimation function for more realistic and varying speeds
743
763
  const hasAVX512 = cpuModel.toLowerCase().includes('intel') &&
744
764
  (cpuModel.includes('13th') || cpuModel.includes('14th') || cpuModel.includes('12th'));
745
765
 
746
766
  // GPU-based calculation (dedicated GPU only)
747
- if (vramGB > 0 && !gpuModel.toLowerCase().includes('iris') && !gpuModel.toLowerCase().includes('integrated')) {
767
+ if (hasDedicatedGPU && vramGB > 0) {
748
768
  let gpuTPS = 20; // Conservative GPU baseline
749
769
  if (gpuModel.toLowerCase().includes('gb10') ||
750
770
  gpuModel.toLowerCase().includes('grace blackwell') ||
@@ -780,7 +800,7 @@ class MultiObjectiveSelector {
780
800
  threads: cores,
781
801
  paramsB: params,
782
802
  avx512: hasAVX512,
783
- isIrisXe: gpuModel.toLowerCase().includes('iris xe')
803
+ isIrisXe: hasIntegratedGPU && gpuModel.toLowerCase().includes('iris xe')
784
804
  });
785
805
  }
786
806
  }
@@ -4,9 +4,10 @@
4
4
  * Focuses on AVX2, AVX512, AMX, and other SIMD extensions
5
5
  */
6
6
 
7
- const { execSync } = require('child_process');
7
+ const childProcess = require('child_process');
8
8
  const os = require('os');
9
9
  const fs = require('fs');
10
+ const { normalizePlatform } = require('../../utils/platform');
10
11
 
11
12
  class CPUDetector {
12
13
  constructor() {
@@ -87,10 +88,12 @@ class CPUDetector {
87
88
  * Get physical core count
88
89
  */
89
90
  getPhysicalCores() {
91
+ const platform = normalizePlatform();
92
+
90
93
  try {
91
- if (process.platform === 'darwin') {
92
- return parseInt(execSync('sysctl -n hw.physicalcpu', { encoding: 'utf8', timeout: 5000 }).trim());
93
- } else if (process.platform === 'linux') {
94
+ if (platform === 'darwin') {
95
+ return parseInt(childProcess.execSync('sysctl -n hw.physicalcpu', { encoding: 'utf8', timeout: 5000 }).trim());
96
+ } else if (platform === 'linux') {
94
97
  const cpuInfo = fs.readFileSync('/proc/cpuinfo', 'utf8');
95
98
  const coreIds = new Set();
96
99
  const matches = cpuInfo.matchAll(/core id\s*:\s*(\d+)/g);
@@ -98,7 +101,7 @@ class CPUDetector {
98
101
  coreIds.add(match[1]);
99
102
  }
100
103
  return coreIds.size || os.cpus().length;
101
- } else if (process.platform === 'win32') {
104
+ } else if (platform === 'win32') {
102
105
  const physicalCores = this.getWindowsPhysicalCoreCount();
103
106
  return physicalCores || os.cpus().length;
104
107
  }
@@ -112,18 +115,20 @@ class CPUDetector {
112
115
  * Get maximum CPU frequency
113
116
  */
114
117
  getMaxFrequency() {
118
+ const platform = normalizePlatform();
119
+
115
120
  try {
116
- if (process.platform === 'darwin') {
121
+ if (platform === 'darwin') {
117
122
  // macOS doesn't expose max frequency easily
118
123
  const cpus = os.cpus();
119
124
  return cpus.length > 0 ? cpus[0].speed : 0;
120
- } else if (process.platform === 'linux') {
125
+ } else if (platform === 'linux') {
121
126
  const maxFreq = fs.readFileSync(
122
127
  '/sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_max_freq',
123
128
  'utf8'
124
129
  );
125
130
  return Math.round(parseInt(maxFreq) / 1000); // kHz to MHz
126
- } else if (process.platform === 'win32') {
131
+ } else if (platform === 'win32') {
127
132
  const maxClock = this.getWindowsMaxClockSpeed();
128
133
  return maxClock || (os.cpus()[0]?.speed || 0);
129
134
  }
@@ -137,7 +142,37 @@ class CPUDetector {
137
142
  * Execute shell command with consistent options.
138
143
  */
139
144
  runCommand(command) {
140
- return execSync(command, { encoding: 'utf8', timeout: 5000 });
145
+ const baseOptions = {
146
+ encoding: 'utf8',
147
+ timeout: 5000
148
+ };
149
+
150
+ if (normalizePlatform() === 'win32') {
151
+ const result = childProcess.spawnSync(command, {
152
+ ...baseOptions,
153
+ shell: true,
154
+ stdio: ['ignore', 'pipe', 'pipe'],
155
+ windowsHide: true
156
+ });
157
+
158
+ if (result.error) {
159
+ throw result.error;
160
+ }
161
+
162
+ if (result.status !== 0) {
163
+ const stderr = String(result.stderr || '').trim();
164
+ const stdout = String(result.stdout || '').trim();
165
+ const error = new Error(stderr || stdout || `Command failed: ${command}`);
166
+ error.status = result.status;
167
+ error.stdout = result.stdout;
168
+ error.stderr = result.stderr;
169
+ throw error;
170
+ }
171
+
172
+ return result.stdout;
173
+ }
174
+
175
+ return childProcess.execSync(command, baseOptions);
141
176
  }
142
177
 
143
178
  /**
@@ -206,13 +241,15 @@ class CPUDetector {
206
241
  l3: 0
207
242
  };
208
243
 
244
+ const platform = normalizePlatform();
245
+
209
246
  try {
210
- if (process.platform === 'darwin') {
211
- cache.l1d = parseInt(execSync('sysctl -n hw.l1dcachesize', { encoding: 'utf8', timeout: 5000 })) / 1024 || 0;
212
- cache.l1i = parseInt(execSync('sysctl -n hw.l1icachesize', { encoding: 'utf8', timeout: 5000 })) / 1024 || 0;
213
- cache.l2 = parseInt(execSync('sysctl -n hw.l2cachesize', { encoding: 'utf8', timeout: 5000 })) / 1024 / 1024 || 0;
214
- cache.l3 = parseInt(execSync('sysctl -n hw.l3cachesize', { encoding: 'utf8', timeout: 5000 })) / 1024 / 1024 || 0;
215
- } else if (process.platform === 'linux') {
247
+ if (platform === 'darwin') {
248
+ cache.l1d = parseInt(childProcess.execSync('sysctl -n hw.l1dcachesize', { encoding: 'utf8', timeout: 5000 })) / 1024 || 0;
249
+ cache.l1i = parseInt(childProcess.execSync('sysctl -n hw.l1icachesize', { encoding: 'utf8', timeout: 5000 })) / 1024 || 0;
250
+ cache.l2 = parseInt(childProcess.execSync('sysctl -n hw.l2cachesize', { encoding: 'utf8', timeout: 5000 })) / 1024 / 1024 || 0;
251
+ cache.l3 = parseInt(childProcess.execSync('sysctl -n hw.l3cachesize', { encoding: 'utf8', timeout: 5000 })) / 1024 / 1024 || 0;
252
+ } else if (platform === 'linux') {
216
253
  // Parse from /sys/devices/system/cpu/cpu0/cache/
217
254
  const cachePath = '/sys/devices/system/cpu/cpu0/cache';
218
255
  if (fs.existsSync(cachePath)) {
@@ -268,8 +305,10 @@ class CPUDetector {
268
305
  bestSimd: 'none'
269
306
  };
270
307
 
308
+ const platform = normalizePlatform();
309
+
271
310
  try {
272
- if (process.platform === 'darwin') {
311
+ if (platform === 'darwin') {
273
312
  // For Apple Silicon (ARM64), use ARM features
274
313
  if (process.arch === 'arm64') {
275
314
  caps.neon = true; // All Apple Silicon has NEON
@@ -279,12 +318,12 @@ class CPUDetector {
279
318
  } else {
280
319
  // Intel Mac - check via sysctl
281
320
  try {
282
- const features = execSync('sysctl -n machdep.cpu.features', {
321
+ const features = childProcess.execSync('sysctl -n machdep.cpu.features', {
283
322
  encoding: 'utf8',
284
323
  timeout: 5000
285
324
  }).toLowerCase();
286
325
 
287
- const leafFeatures = execSync('sysctl -n machdep.cpu.leaf7_features', {
326
+ const leafFeatures = childProcess.execSync('sysctl -n machdep.cpu.leaf7_features', {
288
327
  encoding: 'utf8',
289
328
  timeout: 5000
290
329
  }).toLowerCase();
@@ -308,7 +347,7 @@ class CPUDetector {
308
347
  }
309
348
  }
310
349
 
311
- } else if (process.platform === 'linux') {
350
+ } else if (platform === 'linux') {
312
351
  // Linux - check /proc/cpuinfo
313
352
  const cpuInfo = fs.readFileSync('/proc/cpuinfo', 'utf8').toLowerCase();
314
353
  const flags = cpuInfo.match(/flags\s*:\s*(.+)/)?.[1] || '';
@@ -334,7 +373,7 @@ class CPUDetector {
334
373
  caps.dotprod = flags.includes('asimddp');
335
374
  }
336
375
 
337
- } else if (process.platform === 'win32') {
376
+ } else if (platform === 'win32') {
338
377
  // Windows - use WMIC or assume based on CPU model
339
378
  const cpuName = os.cpus()[0]?.model?.toLowerCase() || '';
340
379
 
@@ -15,6 +15,10 @@ class CUDADetector {
15
15
  this.detectionMode = null;
16
16
  }
17
17
 
18
+ execCommand(command, options = {}) {
19
+ return execSync(command, options);
20
+ }
21
+
18
22
  /**
19
23
  * Check if CUDA is available
20
24
  */
@@ -43,7 +47,7 @@ class CUDADetector {
43
47
 
44
48
  hasNvidiaSMI() {
45
49
  try {
46
- execSync('nvidia-smi --version', {
50
+ this.execCommand('nvidia-smi --version', {
47
51
  encoding: 'utf8',
48
52
  timeout: 5000,
49
53
  stdio: ['pipe', 'pipe', 'pipe']
@@ -142,7 +146,7 @@ class CUDADetector {
142
146
  }
143
147
 
144
148
  try {
145
- execSync('nvcc --version', {
149
+ this.execCommand('nvcc --version', {
146
150
  encoding: 'utf8',
147
151
  timeout: 5000,
148
152
  stdio: ['pipe', 'pipe', 'pipe']
@@ -197,17 +201,18 @@ class CUDADetector {
197
201
 
198
202
  try {
199
203
  // Get driver and CUDA version
200
- const versionInfo = execSync('nvidia-smi --query-gpu=driver_version --format=csv,noheader,nounits', {
204
+ const versionInfo = this.execCommand('nvidia-smi --query-gpu=driver_version --format=csv,noheader,nounits', {
201
205
  encoding: 'utf8',
202
206
  timeout: 5000
203
207
  }).trim().split('\n')[0];
204
208
  result.driver = versionInfo;
205
209
 
206
- // Get CUDA version from nvidia-smi header
207
- const header = execSync('nvidia-smi | head -n 3', {
210
+ // Parse the nvidia-smi banner in JS so Windows does not require shell-only tools like `head`.
211
+ const banner = this.execCommand('nvidia-smi', {
208
212
  encoding: 'utf8',
209
213
  timeout: 5000
210
214
  });
215
+ const header = banner.split('\n').slice(0, 3).join('\n');
211
216
  const cudaMatch = header.match(/CUDA Version:\s*([\d.]+)/);
212
217
  if (cudaMatch) {
213
218
  result.cuda = cudaMatch[1];
@@ -237,7 +242,7 @@ class CUDADetector {
237
242
  'clocks.max.sm'
238
243
  ].join(',');
239
244
 
240
- const gpuData = execSync(
245
+ const gpuData = this.execCommand(
241
246
  `nvidia-smi --query-gpu=${query} --format=csv,noheader,nounits`,
242
247
  { encoding: 'utf8', timeout: 10000 }
243
248
  ).trim();
@@ -286,7 +291,7 @@ class CUDADetector {
286
291
  } catch (e) {
287
292
  // Fallback to simpler query
288
293
  try {
289
- const simpleQuery = execSync(
294
+ const simpleQuery = this.execCommand(
290
295
  'nvidia-smi --query-gpu=name,memory.total --format=csv,noheader,nounits',
291
296
  { encoding: 'utf8', timeout: 5000 }
292
297
  ).trim();
@@ -408,7 +413,7 @@ class CUDADetector {
408
413
  }
409
414
 
410
415
  try {
411
- const nvccVersion = execSync('nvcc --version', {
416
+ const nvccVersion = this.execCommand('nvcc --version', {
412
417
  encoding: 'utf8',
413
418
  timeout: 5000,
414
419
  stdio: ['pipe', 'pipe', 'pipe']