fix(mission-control): dedupe whisper binaries and clean WSL package metadata
Prevent duplicate React keys by de-duplicating whisper binary names and improve WSL package reporting so system metadata shows stable, user-friendly versions. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
a7790b7115
commit
e5acd1dc26
@ -2,12 +2,36 @@ import { NextResponse } from 'next/server';
|
|||||||
import { exec, execFile } from 'child_process';
|
import { exec, execFile } from 'child_process';
|
||||||
import { promisify } from 'util';
|
import { promisify } from 'util';
|
||||||
import { readFile } from 'fs/promises';
|
import { readFile } from 'fs/promises';
|
||||||
|
import { readFileSync } from 'fs';
|
||||||
import os from 'os';
|
import os from 'os';
|
||||||
|
|
||||||
const execAsync = promisify(exec);
|
const execAsync = promisify(exec);
|
||||||
const execFileAsync = promisify(execFile);
|
const execFileAsync = promisify(execFile);
|
||||||
|
|
||||||
const IS_MAC = process.platform === 'darwin';
|
const IS_MAC = process.platform === 'darwin';
|
||||||
|
const IS_WSL =
|
||||||
|
process.platform === 'linux' &&
|
||||||
|
(() => {
|
||||||
|
try {
|
||||||
|
return readFileSync('/proc/version', 'utf-8').toLowerCase().includes('microsoft');
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
async function readWindowsCommand(command: string): Promise<string | null> {
|
||||||
|
try {
|
||||||
|
const { stdout } = await execFileAsync(
|
||||||
|
'powershell.exe',
|
||||||
|
['-NoProfile', '-Command', command],
|
||||||
|
{ timeout: 4000 }
|
||||||
|
);
|
||||||
|
const value = stdout.replace(/\r/g, '').trim();
|
||||||
|
return value || null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Cache slow commands with TTL
|
// Cache slow commands with TTL
|
||||||
let staticCache: {
|
let staticCache: {
|
||||||
@ -29,6 +53,12 @@ async function getChipInfo(): Promise<string> {
|
|||||||
);
|
);
|
||||||
return stdout.trim();
|
return stdout.trim();
|
||||||
}
|
}
|
||||||
|
if (IS_WSL) {
|
||||||
|
const winCpu = await readWindowsCommand(
|
||||||
|
'(Get-CimInstance Win32_Processor | Select-Object -First 1 -ExpandProperty Name)'
|
||||||
|
);
|
||||||
|
if (winCpu) return winCpu;
|
||||||
|
}
|
||||||
// Linux / WSL2
|
// Linux / WSL2
|
||||||
const { stdout } = await execAsync(
|
const { stdout } = await execAsync(
|
||||||
"lscpu 2>/dev/null | grep 'Model name' | sed 's/.*:\\s*//' || cat /proc/cpuinfo | grep 'model name' | head -1 | sed 's/.*: //'"
|
"lscpu 2>/dev/null | grep 'Model name' | sed 's/.*:\\s*//' || cat /proc/cpuinfo | grep 'model name' | head -1 | sed 's/.*: //'"
|
||||||
@ -110,17 +140,60 @@ async function getBrewPackages(): Promise<Array<{ name: string; version: string
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Linux / WSL2 — check via version commands (ffmpeg uses -version, others use --version)
|
// Linux / WSL2 — report installed tools in a stable way (avoid noisy shell errors).
|
||||||
for (const pkg of targets) {
|
for (const pkg of targets) {
|
||||||
const bin = pkg === 'whisper-cpp' ? 'whisper-cli' : pkg;
|
|
||||||
const flag = bin === 'ffmpeg' ? '-version' : '--version';
|
|
||||||
try {
|
try {
|
||||||
const { stdout } = await execAsync(`${bin} ${flag} 2>&1 | head -1`, { timeout: 3000 });
|
if (pkg === 'ffmpeg') {
|
||||||
if (stdout.trim()) {
|
const { stdout } = await execAsync('ffmpeg -version 2>/dev/null | head -1', {
|
||||||
results.push({ name: pkg, version: stdout.trim() });
|
timeout: 3000,
|
||||||
|
});
|
||||||
|
if (stdout.trim()) {
|
||||||
|
results.push({ name: pkg, version: stdout.trim() });
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pkg === 'whisper-cpp') {
|
||||||
|
const { stdout } = await execAsync('command -v whisper-cli 2>/dev/null', { timeout: 2000 });
|
||||||
|
const path = stdout.trim();
|
||||||
|
if (path) {
|
||||||
|
results.push({ name: pkg, version: `installed (${path})` });
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ollama on Linux/WSL may be a Windows-hosted daemon; prefer API version if reachable.
|
||||||
|
if (pkg === 'ollama') {
|
||||||
|
const ollamaUrl = process.env.OLLAMA_URL || process.env.OLLAMA_HOST || 'http://localhost:11434';
|
||||||
|
const normalized = ollamaUrl.startsWith('http') ? ollamaUrl : `http://${ollamaUrl}`;
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${normalized.replace(/\/+$/, '')}/api/version`, {
|
||||||
|
cache: 'no-store',
|
||||||
|
signal: AbortSignal.timeout(2500),
|
||||||
|
});
|
||||||
|
if (res.ok) {
|
||||||
|
const data = (await res.json()) as { version?: string };
|
||||||
|
if (data.version) {
|
||||||
|
results.push({ name: pkg, version: data.version });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// fall through to local binary check
|
||||||
|
}
|
||||||
|
|
||||||
|
const { stdout } = await execAsync('command -v ollama 2>/dev/null', { timeout: 2000 });
|
||||||
|
const path = stdout.trim();
|
||||||
|
if (path) {
|
||||||
|
const { stdout: ver } = await execAsync('ollama --version 2>/dev/null | head -1', {
|
||||||
|
timeout: 2000,
|
||||||
|
});
|
||||||
|
const v = ver.trim();
|
||||||
|
results.push({ name: pkg, version: v || `installed (${path})` });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// not installed
|
// skip unavailable tool
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -204,6 +277,17 @@ async function getAccurateMemory(): Promise<{
|
|||||||
const ratio = appMemory / total;
|
const ratio = appMemory / total;
|
||||||
const pressure = ratio > 0.85 ? 'critical' : ratio > 0.7 ? 'warning' : 'normal';
|
const pressure = ratio > 0.85 ? 'critical' : ratio > 0.7 ? 'warning' : 'normal';
|
||||||
|
|
||||||
|
if (IS_WSL) {
|
||||||
|
const winTotalRaw = await readWindowsCommand(
|
||||||
|
'[string](Get-CimInstance Win32_ComputerSystem).TotalPhysicalMemory'
|
||||||
|
);
|
||||||
|
const winTotal = winTotalRaw ? Number.parseInt(winTotalRaw.replace(/\D/g, ''), 10) : 0;
|
||||||
|
if (Number.isFinite(winTotal) && winTotal > total) {
|
||||||
|
// Keep WSL usage metrics, but report host RAM capacity for Mission Control display.
|
||||||
|
return { total: winTotal, appMemory, cached, free, pressure };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return { total, appMemory, cached, free, pressure };
|
return { total, appMemory, cached, free, pressure };
|
||||||
} catch {
|
} catch {
|
||||||
// fall through to generic fallback
|
// fall through to generic fallback
|
||||||
@ -240,7 +324,7 @@ export async function GET() {
|
|||||||
ollamaDiskUsage: ollamaDisk,
|
ollamaDiskUsage: ollamaDisk,
|
||||||
cpuCores: cpuCount,
|
cpuCores: cpuCount,
|
||||||
uptime,
|
uptime,
|
||||||
platform: `${os.type()} ${os.release()}`,
|
platform: IS_WSL ? 'Windows (via WSL2)' : `${os.type()} ${os.release()}`,
|
||||||
arch: os.arch(),
|
arch: os.arch(),
|
||||||
nodeVersion: process.version,
|
nodeVersion: process.version,
|
||||||
brewPackages: staticInfo.brewPackages,
|
brewPackages: staticInfo.brewPackages,
|
||||||
|
|||||||
@ -13,21 +13,23 @@ async function getWhisperBinaries(): Promise<string[]> {
|
|||||||
try {
|
try {
|
||||||
if (IS_MAC) {
|
if (IS_MAC) {
|
||||||
const { stdout } = await execAsync('ls /opt/homebrew/bin/whisper-* 2>/dev/null');
|
const { stdout } = await execAsync('ls /opt/homebrew/bin/whisper-* 2>/dev/null');
|
||||||
return stdout
|
const bins = stdout
|
||||||
.trim()
|
.trim()
|
||||||
.split('\n')
|
.split('\n')
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.map(p => p.split('/').pop() || p);
|
.map(p => p.split('/').pop() || p);
|
||||||
|
return Array.from(new Set(bins));
|
||||||
}
|
}
|
||||||
// Linux / WSL2 — check common locations
|
// Linux / WSL2 — check common locations
|
||||||
const { stdout } = await execAsync(
|
const { stdout } = await execAsync(
|
||||||
'ls /usr/local/bin/whisper-* /usr/bin/whisper-* 2>/dev/null || which whisper-cli 2>/dev/null'
|
'ls /usr/local/bin/whisper-* /usr/bin/whisper-* 2>/dev/null || which whisper-cli 2>/dev/null'
|
||||||
);
|
);
|
||||||
return stdout
|
const bins = stdout
|
||||||
.trim()
|
.trim()
|
||||||
.split('\n')
|
.split('\n')
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.map(p => p.split('/').pop() || p);
|
.map(p => p.split('/').pop() || p);
|
||||||
|
return Array.from(new Set(bins));
|
||||||
} catch {
|
} catch {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@ -70,8 +72,9 @@ async function getWhisperModels(): Promise<{
|
|||||||
|
|
||||||
async function getWhisperVersion(): Promise<string> {
|
async function getWhisperVersion(): Promise<string> {
|
||||||
try {
|
try {
|
||||||
const { stdout } = await execAsync('whisper-cli --version 2>&1 || echo "unknown"');
|
const { stdout } = await execAsync('whisper-cli -h 2>&1 | head -1');
|
||||||
return stdout.trim();
|
const line = stdout.trim();
|
||||||
|
return line || 'installed';
|
||||||
} catch {
|
} catch {
|
||||||
return 'unknown';
|
return 'unknown';
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user