swictation
Version:
Cross-platform voice-to-text dictation for Linux and macOS with GPU acceleration (NVIDIA CUDA/CoreML), Secretary Mode (60+ natural language commands), Context-Aware Meta-Learning, and pure Rust performance. Meta-package that automatically installs platfor
1,372 lines (1,191 loc) • 117 kB
JavaScript
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
const os = require('os');
const https = require('https');
const crypto = require('crypto');
const { checkNvidiaHibernationStatus } = require('./src/nvidia-hibernation-setup');
const { getIpcSocketPath } = require('./src/socket-paths');
// Environment variable support for model test-loading
// By default, model testing runs when GPU is detected
// Set SKIP_MODEL_TEST=1 to disable (useful for CI/headless environments)
const SKIP_MODEL_TEST = process.env.SKIP_MODEL_TEST === '1';
// Colors for console output (basic implementation without chalk dependency)
const colors = {
reset: '\x1b[0m',
green: '\x1b[32m',
yellow: '\x1b[33m',
cyan: '\x1b[36m',
red: '\x1b[31m'
};
function log(color, message) {
console.log(`${colors[color]}${message}${colors.reset}`);
}
function checkPlatform() {
const platform = process.platform;
const arch = process.arch;
// Support both Linux and macOS
if (platform === 'linux') {
// Linux-specific checks
if (arch !== 'x64') {
log('yellow', 'Note: Swictation on Linux currently only supports x64 architecture');
process.exit(0);
}
// Check GLIBC version
try {
const glibcVersion = execSync('ldd --version 2>&1 | head -1', { encoding: 'utf8' });
const versionMatch = glibcVersion.match(/(\d+)\.(\d+)/);
if (versionMatch) {
const major = parseInt(versionMatch[1]);
const minor = parseInt(versionMatch[2]);
if (major < 2 || (major === 2 && minor < 39)) {
log('red', '\n⚠ INCOMPATIBLE GLIBC VERSION');
log('yellow', `Detected GLIBC ${major}.${minor} (need 2.39+)`);
log('yellow', 'Swictation requires Ubuntu 24.04 LTS or newer');
log('yellow', 'Ubuntu 22.04 is NOT supported due to GLIBC 2.35');
log('yellow', '\nSupported distributions:');
log('cyan', ' - Ubuntu 24.04 LTS (Noble Numbat) or newer');
log('cyan', ' - Debian 13+ (Trixie)');
log('cyan', ' - Fedora 39+');
log('yellow', '\nInstallation will continue but binaries may not work.');
}
}
} catch (err) {
log('yellow', 'Warning: Could not check GLIBC version');
}
} else if (platform === 'darwin') {
// macOS-specific checks
if (arch !== 'arm64') {
log('red', '\n⚠ UNSUPPORTED ARCHITECTURE');
log('yellow', `Detected architecture: ${arch}`);
log('yellow', 'Swictation on macOS requires Apple Silicon (M1/M2/M3/M4)');
log('yellow', 'Intel Macs are not supported');
process.exit(1);
}
// Check macOS version (require Sonoma 14.0+ or Sequoia 15.0+)
try {
const osVersion = execSync('sw_vers -productVersion', { encoding: 'utf8' }).trim();
const versionMatch = osVersion.match(/(\d+)\.(\d+)/);
if (versionMatch) {
const major = parseInt(versionMatch[1]);
const minor = parseInt(versionMatch[2]);
if (major < 14) {
log('red', '\n⚠ UNSUPPORTED MACOS VERSION');
log('yellow', `Detected macOS ${osVersion}`);
log('yellow', 'Swictation requires macOS 14.0 (Sonoma) or newer');
log('yellow', '\nSupported versions:');
log('cyan', ' - macOS 14.x (Sonoma)');
log('cyan', ' - macOS 15.x (Sequoia)');
log('yellow', '\nInstallation will continue but may not work correctly.');
} else {
log('green', `✓ macOS ${osVersion} (Apple Silicon)`);
}
}
} catch (err) {
log('yellow', 'Warning: Could not check macOS version');
}
} else {
log('yellow', `Note: Swictation currently only supports Linux and macOS`);
log('yellow', `Detected platform: ${platform}`);
log('yellow', 'Skipping postinstall for unsupported platform');
process.exit(0);
}
}
/**
* Phase 1: Clean up old/conflicting service files from previous installations
* This prevents conflicts between old Python-based services and new Node.js services
*/
/**
* Stop currently running services before upgrade to prevent CUDA state corruption
* This must run BEFORE any file modifications happen
*/
async function stopExistingServices() {
log('cyan', '\n🛑 Stopping currently running services...');
let stopped = false;
try {
// Method 1: Try using swictation CLI if available
try {
execSync('which swictation 2>/dev/null', { stdio: 'ignore' });
execSync('swictation stop 2>/dev/null', { stdio: 'ignore' });
log('green', '✓ Stopped swictation services via CLI');
stopped = true;
// Give services time to fully stop and release CUDA
await new Promise(resolve => setTimeout(resolve, 2000));
} catch (cliErr) {
// swictation CLI not available, try systemctl
try {
execSync('systemctl --user stop swictation-daemon.service 2>/dev/null', { stdio: 'ignore' });
execSync('systemctl --user stop swictation-ui.service 2>/dev/null', { stdio: 'ignore' });
log('green', '✓ Stopped services via systemctl');
stopped = true;
await new Promise(resolve => setTimeout(resolve, 2000));
} catch (systemctlErr) {
log('cyan', 'ℹ No existing services to stop');
}
}
} catch (err) {
log('cyan', 'ℹ No existing services to stop');
}
return stopped;
}
/**
* Clean up old ONNX Runtime libraries from Python pip installations
* These cause version conflicts (1.20.1 vs 1.22.x)
*/
function cleanupOldOnnxRuntime() {
log('cyan', '\n🧹 Checking for old ONNX Runtime libraries...');
try {
const homeDir = os.homedir();
const pythonLibDirs = [
path.join(homeDir, '.local', 'lib', 'python3.13', 'site-packages', 'onnxruntime'),
path.join(homeDir, '.local', 'lib', 'python3.12', 'site-packages', 'onnxruntime'),
path.join(homeDir, '.local', 'lib', 'python3.11', 'site-packages', 'onnxruntime'),
path.join(homeDir, '.local', 'lib', 'python3.10', 'site-packages', 'onnxruntime'),
];
let removedAny = false;
for (const ortDir of pythonLibDirs) {
if (fs.existsSync(ortDir)) {
try {
// Check if it's an old version that conflicts
const capiDir = path.join(ortDir, 'capi');
if (fs.existsSync(capiDir)) {
const ortFiles = fs.readdirSync(capiDir).filter(f => f.includes('libonnxruntime.so'));
if (ortFiles.length > 0 && ortFiles[0].includes('1.20')) {
log('yellow', `⚠️ Found old ONNX Runtime 1.20.x at ${ortDir}`);
log('cyan', ` Removing to prevent version conflicts...`);
execSync(`rm -rf "${ortDir}"`, { stdio: 'ignore' });
log('green', `✓ Removed old ONNX Runtime installation`);
removedAny = true;
}
}
} catch (err) {
// Can't determine version, leave it alone
}
}
}
if (!removedAny) {
log('green', '✓ No conflicting ONNX Runtime installations found');
}
} catch (err) {
log('yellow', `⚠️ Error checking ONNX Runtime installations: ${err.message}`);
}
}
/**
* Remove old npm installations that conflict with new installation
* Handles both system-wide and nvm installations
*/
function cleanupOldNpmInstallations() {
log('cyan', '\n🧹 Checking for old npm installations...');
const oldInstallPaths = [
'/usr/local/lib/node_modules/swictation',
'/usr/local/nodejs/lib/node_modules/swictation',
'/usr/lib/node_modules/swictation',
];
let removedAny = false;
for (const oldPath of oldInstallPaths) {
if (fs.existsSync(oldPath) && oldPath !== __dirname) {
log('yellow', `⚠️ Found old npm installation at ${oldPath}`);
log('cyan', ` Removing to prevent conflicts...`);
try {
execSync(`sudo rm -rf "${oldPath}" 2>/dev/null || rm -rf "${oldPath}"`, { stdio: 'ignore' });
log('green', `✓ Removed old installation`);
removedAny = true;
} catch (err) {
log('yellow', `⚠️ Could not remove ${oldPath}: ${err.message}`);
log('yellow', ` You may need to run: sudo rm -rf "${oldPath}"`);
}
}
}
if (!removedAny) {
log('green', '✓ No conflicting npm installations found');
}
}
async function cleanOldServices() {
log('cyan', '\n🧹 Checking for old service files...');
const oldServiceLocations = [
// Old system-wide service files (from Python version)
'/usr/lib/systemd/user/swictation.service',
'/usr/lib/systemd/system/swictation.service',
// Old user service files that might conflict
path.join(os.homedir(), '.config', 'systemd', 'user', 'swictation.service')
];
let foundOldServices = false;
for (const servicePath of oldServiceLocations) {
if (fs.existsSync(servicePath)) {
foundOldServices = true;
log('yellow', `⚠️ Found old service file: ${servicePath}`);
// Extract service name from path
const serviceName = path.basename(servicePath);
const isSystemService = servicePath.includes('/system/');
try {
// Try to stop the service if it's running
const stopCmd = isSystemService
? `sudo systemctl stop ${serviceName} 2>/dev/null || true`
: `systemctl --user stop ${serviceName} 2>/dev/null || true`;
execSync(stopCmd, { stdio: 'ignore' });
log('cyan', ` ✓ Stopped service: ${serviceName}`);
// Disable the service
const disableCmd = isSystemService
? `sudo systemctl disable ${serviceName} 2>/dev/null || true`
: `systemctl --user disable ${serviceName} 2>/dev/null || true`;
execSync(disableCmd, { stdio: 'ignore' });
log('cyan', ` ✓ Disabled service: ${serviceName}`);
// Remove the service file (requires sudo for system services)
if (isSystemService) {
try {
execSync(`sudo rm -f "${servicePath}"`, { stdio: 'ignore' });
log('green', ` ✓ Removed old service file: ${servicePath}`);
} catch (err) {
log('yellow', ` ⚠️ Could not remove ${servicePath} (may need manual cleanup)`);
}
} else {
try {
fs.unlinkSync(servicePath);
log('green', ` ✓ Removed old service file: ${servicePath}`);
} catch (err) {
log('yellow', ` ⚠️ Could not remove ${servicePath}: ${err.message}`);
}
}
} catch (err) {
log('yellow', ` ⚠️ Error cleaning up ${serviceName}: ${err.message}`);
}
}
}
if (foundOldServices) {
// Reload systemd to pick up changes
try {
execSync('systemctl --user daemon-reload 2>/dev/null', { stdio: 'ignore' });
execSync('sudo systemctl daemon-reload 2>/dev/null || true', { stdio: 'ignore' });
log('green', '✓ Reloaded systemd daemon');
} catch (err) {
log('yellow', '⚠️ Could not reload systemd daemon');
}
} else {
log('green', '✓ No old service files found');
}
return foundOldServices;
}
function ensureBinaryPermissions() {
const binaries = [];
// CLI wrapper in main package
const cliBinary = path.join(__dirname, 'bin', 'swictation');
if (fs.existsSync(cliBinary)) {
binaries.push(cliBinary);
}
// Platform package binaries
try {
const { resolveBinaryPaths } = require('./src/resolve-binary');
const binaryPaths = resolveBinaryPaths();
// Add daemon and UI from platform package
if (fs.existsSync(binaryPaths.daemon)) {
binaries.push(binaryPaths.daemon);
}
if (fs.existsSync(binaryPaths.ui)) {
binaries.push(binaryPaths.ui);
}
} catch (err) {
// Platform package not installed yet - skip platform binaries
log('yellow', ` ⚠️ Platform package binaries not found (will be checked later)`);
}
// Legacy binaries (if they exist from old installations)
const legacyBinaries = [
path.join(__dirname, 'lib', 'native', 'swictation-daemon.bin'),
path.join(__dirname, 'bin', 'swictation-daemon'),
path.join(__dirname, 'bin', 'swictation-ui')
];
for (const binary of legacyBinaries) {
if (fs.existsSync(binary)) {
binaries.push(binary);
}
}
// Set execute permissions
for (const binary of binaries) {
try {
fs.chmodSync(binary, '755');
log('green', `✓ Set execute permissions for ${path.basename(binary)}`);
} catch (err) {
log('yellow', `Warning: Could not set permissions for ${path.basename(binary)}: ${err.message}`);
}
}
}
function createDirectories() {
const dirs = [
path.join(os.homedir(), '.config', 'swictation'),
path.join(os.homedir(), '.local', 'share', 'swictation'),
path.join(os.homedir(), '.local', 'share', 'swictation', 'models'),
path.join(os.homedir(), '.cache', 'swictation')
];
for (const dir of dirs) {
if (!fs.existsSync(dir)) {
try {
fs.mkdirSync(dir, { recursive: true });
log('green', `✓ Created directory: ${dir}`);
} catch (err) {
log('yellow', `Warning: Could not create ${dir}: ${err.message}`);
}
}
}
}
/**
* Detect system package manager
* @returns {object} Package manager info with install command
*/
function detectPackageManager() {
const managers = [
{ cmd: 'apt', name: 'apt', installCmd: 'sudo apt update && sudo apt install -y' },
{ cmd: 'dnf', name: 'dnf', installCmd: 'sudo dnf install -y' },
{ cmd: 'pacman', name: 'pacman', installCmd: 'sudo pacman -S --noconfirm' },
{ cmd: 'zypper', name: 'zypper', installCmd: 'sudo zypper install -y' }
];
for (const manager of managers) {
try {
execSync(`which ${manager.cmd}`, { stdio: 'ignore' });
return manager;
} catch {
// Try next manager
}
}
return null;
}
/**
* Install a package using the system package manager
* @param {string} packageName - Package name to install
* @param {string} displayName - Display name for logging
* @returns {boolean} Success status
*/
function installPackage(packageName, displayName) {
const pkgManager = detectPackageManager();
if (!pkgManager) {
log('yellow', ` ⚠️ No supported package manager found (apt/dnf/pacman/zypper)`);
log('cyan', ` Please install ${displayName} manually`);
return false;
}
log('cyan', ` Installing ${displayName} via ${pkgManager.name}...`);
try {
execSync(`${pkgManager.installCmd} ${packageName}`, { stdio: 'inherit' });
log('green', ` ✓ ${displayName} installed successfully`);
return true;
} catch (err) {
log('yellow', ` ⚠️ Failed to install ${displayName}: ${err.message}`);
log('cyan', ` Install manually: ${pkgManager.installCmd} ${packageName}`);
return false;
}
}
function checkDependencies() {
const optional = [];
const required = [];
// Check for required tools
const tools = [
{ name: 'systemctl', type: 'optional', package: 'systemd' },
{ name: 'nc', type: 'optional', package: 'netcat' },
{ name: 'wtype', type: 'optional', package: 'wtype (for Wayland)' },
{ name: 'xdotool', type: 'optional', package: 'xdotool (for X11)' },
{ name: 'hf', type: 'optional', package: 'huggingface_hub[cli] (pip install huggingface_hub[cli])' }
];
for (const tool of tools) {
try {
execSync(`which ${tool.name}`, { stdio: 'ignore' });
} catch {
if (tool.type === 'required') {
required.push(tool);
} else {
optional.push(tool);
}
}
}
if (required.length > 0) {
log('red', '\n⚠ Required dependencies missing:');
for (const tool of required) {
log('yellow', ` - ${tool.name} (install: ${tool.package})`);
}
log('red', '\nPlease install required dependencies before using Swictation');
process.exit(1);
}
if (optional.length > 0) {
log('yellow', '\n📦 Optional dependencies for full functionality:');
for (const tool of optional) {
log('cyan', ` - ${tool.name} (${tool.package})`);
}
}
}
function detectNvidiaGPU() {
try {
execSync('nvidia-smi', { stdio: 'ignore' });
return true;
} catch {
return false;
}
}
/**
* Detect GPU compute capability (sm_XX architecture)
* Returns { hasGPU: boolean, computeCap: string, smVersion: number }
* @returns {object} GPU compute capability info
*/
function detectGPUComputeCapability() {
const result = {
hasGPU: false,
computeCap: null, // e.g., "5.2", "8.6", "12.0"
smVersion: null, // e.g., 52, 86, 120
gpuName: null
};
if (!detectNvidiaGPU()) {
return result;
}
result.hasGPU = true;
try {
// Get compute capability and GPU name
const output = execSync(
'nvidia-smi --query-gpu=compute_cap,name --format=csv,noheader',
{ encoding: 'utf8' }
).trim();
const [computeCap, gpuName] = output.split(',').map(s => s.trim());
result.computeCap = computeCap;
result.gpuName = gpuName;
// Convert "5.2" -> 52, "8.6" -> 86, "12.0" -> 120
const [major, minor] = computeCap.split('.').map(n => parseInt(n));
result.smVersion = major * 10 + minor;
log('green', `✓ Detected GPU: ${gpuName}`);
log('cyan', ` Compute Capability: ${computeCap} (sm_${result.smVersion})`);
} catch (err) {
log('yellow', `⚠️ Could not detect compute capability: ${err.message}`);
}
return result;
}
/**
* Select appropriate GPU library package variant based on compute capability
* @param {number} smVersion - Compute capability as integer (e.g., 52, 86, 120)
* @returns {object} Package variant info
*/
function selectGPUPackageVariant(smVersion) {
// Architecture mapping based on RELEASE_NOTES.md
if (smVersion >= 50 && smVersion <= 70) {
// sm_50-70: Maxwell, Pascal, Volta (2014-2017)
return {
variant: 'legacy',
architectures: 'sm_50-70',
description: 'Maxwell, Pascal, Volta GPUs (2014-2017)',
examples: 'GTX 750/900/1000, Quadro M/P series, Titan V, V100'
};
} else if (smVersion >= 75 && smVersion <= 86) {
// sm_75-86: Turing, Ampere (2018-2021)
return {
variant: 'modern',
architectures: 'sm_75-86',
description: 'Turing, Ampere GPUs (2018-2021)',
examples: 'GTX 16/RTX 20/30 series, A100, RTX A1000-A6000'
};
} else if (smVersion >= 89 && smVersion <= 121) {
// sm_89-120: Ada Lovelace, Hopper, Blackwell (2022-2024)
return {
variant: 'latest',
architectures: 'sm_89-120',
description: 'Ada Lovelace, Hopper, Blackwell GPUs (2022-2024)',
examples: 'RTX 4090, H100, B100/B200, RTX PRO 6000 Blackwell, RTX 50 series'
};
} else {
// Unsupported architecture
return {
variant: null,
architectures: `sm_${smVersion}`,
description: 'Unsupported GPU architecture',
examples: 'GPU too old (<sm_50) or unknown architecture'
};
}
}
/**
* Detect CUDA and cuDNN library paths dynamically
* Returns an array of directories to include in LD_LIBRARY_PATH
* Now includes ~/.local/share/swictation/gpu-libs as PRIMARY source
*/
/**
* Detect actual npm installation path (handles nvm vs system-wide)
* This is critical for service files to find the correct libraries
*/
function detectActualNpmInstallPath() {
// __dirname is where this script is running from
// For system-wide: /usr/local/lib/node_modules/swictation
// For nvm: /home/user/.nvm/versions/node/vX.Y.Z/lib/node_modules/swictation
// Return the actual installation directory
return __dirname;
}
/**
* Detect where npm global packages are installed
* This helps find the native library path for LD_LIBRARY_PATH
*/
function detectNpmNativeLibPath() {
const installDir = detectActualNpmInstallPath();
return path.join(installDir, 'lib', 'native');
}
function detectCudaLibraryPaths() {
const paths = [];
// PRIORITY 1: User's GPU libs directory (our multi-architecture packages)
const gpuLibsDir = path.join(os.homedir(), '.local', 'share', 'swictation', 'gpu-libs');
if (fs.existsSync(gpuLibsDir)) {
paths.push(gpuLibsDir);
}
// PRIORITY 2: Check common CUDA installation directories (system-wide fallback)
const cudaDirs = [
'/usr/local/cuda/lib64',
'/usr/local/cuda/lib',
'/usr/local/cuda-13/lib64',
'/usr/local/cuda-13/lib',
'/usr/local/cuda-12.9/lib64',
'/usr/local/cuda-12.9/lib',
'/usr/local/cuda-12/lib64',
'/usr/local/cuda-12/lib',
];
// Find directories that contain cuDNN or CUDA runtime
for (const dir of cudaDirs) {
try {
if (fs.existsSync(dir)) {
const files = fs.readdirSync(dir);
// Check for cuDNN or CUDA runtime libraries
if (files.some(f => f.startsWith('libcudnn.so') || f.startsWith('libcudart.so'))) {
if (!paths.includes(dir)) {
paths.push(dir);
}
}
}
} catch (err) {
// Ignore errors from directories we can't read
}
}
return paths;
}
async function downloadFile(url, dest) {
return new Promise((resolve, reject) => {
const file = fs.createWriteStream(dest);
https.get(url, (response) => {
if (response.statusCode === 302 || response.statusCode === 301) {
// Follow redirect
https.get(response.headers.location, (redirectResponse) => {
redirectResponse.pipe(file);
file.on('finish', () => {
file.close();
resolve();
});
}).on('error', (err) => {
fs.unlink(dest, () => {});
reject(err);
});
} else {
response.pipe(file);
file.on('finish', () => {
file.close();
resolve();
});
}
}).on('error', (err) => {
fs.unlink(dest, () => {});
reject(err);
});
});
}
/**
* Verify SHA256 checksum of a downloaded file
* @param {string} filePath - Path to file to verify
* @param {string} expectedChecksum - Expected SHA256 hash (lowercase hex)
* @returns {boolean} - True if checksum matches
*/
function verifyChecksum(filePath, expectedChecksum) {
const fileBuffer = fs.readFileSync(filePath);
const hashSum = crypto.createHash('sha256');
hashSum.update(fileBuffer);
const actualChecksum = hashSum.digest('hex');
return actualChecksum === expectedChecksum.toLowerCase();
}
/**
* Get SHA256 checksum of a file (for debugging/generating checksums)
* @param {string} filePath - Path to file
* @returns {string} - SHA256 hash as lowercase hex
*/
function getFileChecksum(filePath) {
const fileBuffer = fs.readFileSync(filePath);
const hashSum = crypto.createHash('sha256');
hashSum.update(fileBuffer);
return hashSum.digest('hex');
}
// SHA256 checksums for macOS release binaries (SECURITY: verify integrity of downloads)
const MACOS_CHECKSUMS = {
// Daemon v0.7.4 - ARM64 macOS binary
'daemon-0.7.4': 'd992f8c424448eedbc902d81377d1079b7af62798a1e29fe089895838c7ab6b7',
// UI v0.1.0 - ARM64 macOS Tauri app
'ui-0.1.0': '5ce09af9942c5b11683380621fce2937ea1e2beefa3842a8ec6a1a698ce6b319',
};
/**
* Load expected checksums from checksums.txt
* @returns {Map<string, string>} Map of filename -> sha512 hash
*/
function loadChecksums() {
const checksumsPath = path.join(__dirname, 'checksums.txt');
if (!fs.existsSync(checksumsPath)) {
throw new Error('checksums.txt not found - package may be corrupted');
}
const content = fs.readFileSync(checksumsPath, 'utf8');
const checksums = new Map();
for (const line of content.split('\n')) {
// Skip comments and empty lines
if (line.trim().startsWith('#') || line.trim() === '') {
continue;
}
// Parse "hash filename" format
const match = line.match(/^([a-f0-9]{128})\s+(.+)$/);
if (match) {
const [, hash, filename] = match;
checksums.set(filename, hash);
}
}
return checksums;
}
/**
* Calculate SHA-512 checksum of a file
* @param {string} filePath - Path to file
* @returns {Promise<string>} SHA-512 hash in hex format
*/
function calculateChecksum(filePath) {
return new Promise((resolve, reject) => {
const hash = crypto.createHash('sha512');
const stream = fs.createReadStream(filePath);
stream.on('data', (chunk) => {
hash.update(chunk);
});
stream.on('end', () => {
resolve(hash.digest('hex'));
});
stream.on('error', (err) => {
reject(err);
});
});
}
/**
* Verify downloaded file checksum matches expected value
* @param {string} filePath - Path to downloaded file
* @param {string} filename - Original filename for lookup
* @param {Map<string, string>} checksums - Expected checksums map
* @throws {Error} If checksum doesn't match or file is missing from checksums
*/
async function verifyChecksum(filePath, filename, checksums) {
const expectedChecksum = checksums.get(filename);
if (!expectedChecksum) {
throw new Error(`No checksum found for ${filename} - package may be corrupted`);
}
log('cyan', ' Verifying file integrity...');
const actualChecksum = await calculateChecksum(filePath);
if (actualChecksum !== expectedChecksum) {
throw new Error(
`SECURITY: Checksum mismatch for ${filename}!\n` +
` Expected: ${expectedChecksum}\n` +
` Actual: ${actualChecksum}\n` +
` This could indicate a corrupted download or supply chain attack.\n` +
` DO NOT extract this file. Please report this issue.`
);
}
log('green', ` ✓ Checksum verified (SHA-512)`);
}
async function downloadGPULibraries() {
const hasGPU = detectNvidiaGPU();
if (!hasGPU) {
log('cyan', '\nℹ No NVIDIA GPU detected - skipping GPU library download');
log('cyan', ' CPU-only mode will be used');
return;
}
log('green', '\n✓ NVIDIA GPU detected!');
log('cyan', '📦 Detecting GPU architecture and downloading optimized libraries...\n');
// Get platform package lib directory for GPU libraries
const { resolveBinaryPaths } = require('./src/resolve-binary');
let gpuLibsDir;
try {
const binaryPaths = resolveBinaryPaths();
gpuLibsDir = binaryPaths.libDir;
log('cyan', ` GPU libraries will be installed to platform package: ${gpuLibsDir}\n`);
} catch (err) {
log('red', ' ✗ Platform package not found - cannot install GPU libraries');
log('cyan', ' This should not happen as platform package was verified earlier');
throw new Error('Platform package lib directory not found');
}
// Detect GPU compute capability
const gpuInfo = detectGPUComputeCapability();
if (!gpuInfo.smVersion) {
log('yellow', '⚠️ Could not detect GPU compute capability');
log('cyan', ' Skipping GPU library download');
log('cyan', ' You can manually download from:');
log('cyan', ' https://github.com/robertelee78/swictation/releases/tag/gpu-libs-v1.2.0');
return;
}
// Select appropriate package variant
const packageInfo = selectGPUPackageVariant(gpuInfo.smVersion);
if (!packageInfo.variant) {
log('yellow', `⚠️ GPU architecture ${packageInfo.architectures} is not supported`);
log('cyan', ` ${packageInfo.description}`);
log('cyan', ' Supported architectures: sm_50 through sm_121');
log('cyan', ' Your GPU may be too old or require a newer ONNX Runtime build');
return;
}
log('cyan', `📦 Selected Package: ${packageInfo.variant.toUpperCase()}`);
log('cyan', ` Architectures: ${packageInfo.architectures}`);
log('cyan', ` Description: ${packageInfo.description}`);
log('cyan', ` Examples: ${packageInfo.examples}\n`);
// GPU libs v1.1.0: Multi-architecture CUDA support (sm_50-120)
// ONNX Runtime 1.23.2, CUDA 12.9, cuDNN 9.15.1
const GPU_LIBS_VERSION = '1.2.0';
const variant = packageInfo.variant;
const releaseUrl = `https://github.com/robertelee78/swictation/releases/download/gpu-libs-v${GPU_LIBS_VERSION}/cuda-libs-${variant}.tar.gz`;
const tmpDir = path.join(os.tmpdir(), 'swictation-gpu-install');
const tarPath = path.join(tmpDir, `cuda-libs-${variant}.tar.gz`);
try {
// Load checksums for verification
let checksums;
try {
checksums = loadChecksums();
log('green', ' ✓ Loaded integrity checksums');
} catch (err) {
log('red', ` ✗ Failed to load checksums: ${err.message}`);
throw new Error('Cannot proceed without checksums - package integrity cannot be verified');
}
// Create directories
if (!fs.existsSync(tmpDir)) {
fs.mkdirSync(tmpDir, { recursive: true });
}
if (!fs.existsSync(gpuLibsDir)) {
fs.mkdirSync(gpuLibsDir, { recursive: true });
}
// Check if GPU libs are already installed by checking metadata file
const configDir = path.join(os.homedir(), '.config', 'swictation');
const gpuPackageInfoPath = path.join(configDir, 'gpu-package-info.json');
let skipDownload = false;
if (fs.existsSync(gpuPackageInfoPath)) {
try {
const existingMetadata = JSON.parse(fs.readFileSync(gpuPackageInfoPath, 'utf8'));
if (existingMetadata.version === GPU_LIBS_VERSION && existingMetadata.variant === variant) {
skipDownload = true;
log('green', ` ✓ GPU libraries v${GPU_LIBS_VERSION} (${variant}) already installed`);
log('cyan', ` Location: ${gpuLibsDir}`);
log('cyan', ` Installed: ${existingMetadata.installedAt}`);
log('cyan', ` Skipping download to save time and bandwidth`);
}
} catch (err) {
log('yellow', ` Warning: Could not read GPU package metadata: ${err.message}`);
}
}
if (!skipDownload) {
// Download tarball
log('cyan', ` Downloading ${variant} package...`);
log('cyan', ` URL: ${releaseUrl}`);
await downloadFile(releaseUrl, tarPath);
log('green', ` ✓ Downloaded ${variant} package (~1.5GB)`);
// Verify cryptographic checksum before extraction
const filename = `cuda-libs-${variant}.tar.gz`;
try {
await verifyChecksum(tarPath, filename, checksums);
} catch (err) {
// Delete potentially malicious file
fs.unlinkSync(tarPath);
throw err;
}
// Extract tarball to gpu-libs directory
log('cyan', ' Extracting libraries...');
execSync(`tar -xzf "${tarPath}" -C "${tmpDir}"`, { stdio: 'inherit' });
// Move libraries from extracted ${variant}/libs/ to gpu-libs directory
const extractedLibsDir = path.join(tmpDir, variant, 'libs');
if (fs.existsSync(extractedLibsDir)) {
const libFiles = fs.readdirSync(extractedLibsDir);
for (const file of libFiles) {
const srcPath = path.join(extractedLibsDir, file);
const destPath = path.join(gpuLibsDir, file);
fs.copyFileSync(srcPath, destPath);
}
log('green', ` ✓ Extracted ${libFiles.length} libraries to ${gpuLibsDir}`);
} else {
throw new Error(`Expected directory not found: ${extractedLibsDir}`);
}
// Cleanup
fs.unlinkSync(tarPath);
execSync(`rm -rf "${path.join(tmpDir, variant)}"`, { stdio: 'ignore' });
// Save GPU package info for systemd service generation
const packageMetadata = {
variant: packageInfo.variant,
architectures: packageInfo.architectures,
smVersion: gpuInfo.smVersion,
computeCap: gpuInfo.computeCap,
gpuName: gpuInfo.gpuName,
version: GPU_LIBS_VERSION,
libsPath: gpuLibsDir,
installedAt: new Date().toISOString()
};
try {
fs.writeFileSync(gpuPackageInfoPath, JSON.stringify(packageMetadata, null, 2));
log('green', ` ✓ Saved package metadata to ${gpuPackageInfoPath}`);
} catch (err) {
log('yellow', ` ⚠️ Could not save package metadata: ${err.message}`);
}
}
log('green', '\n✅ GPU acceleration enabled!');
log('cyan', ` Architecture: ${packageInfo.architectures}`);
log('cyan', ` Libraries: ${gpuLibsDir}`);
log('cyan', ' Your system will use CUDA for faster transcription\n');
} catch (err) {
log('yellow', `\n⚠️ Failed to download GPU libraries: ${err.message}`);
log('cyan', ' Continuing with CPU-only mode');
log('cyan', ' You can manually download from:');
log('cyan', ` ${releaseUrl}`);
log('cyan', '\n Manual installation:');
log('cyan', ` 1. Download: curl -L -o /tmp/cuda-libs-${variant}.tar.gz ${releaseUrl}`);
log('cyan', ` 2. Extract: tar -xzf /tmp/cuda-libs-${variant}.tar.gz -C /tmp`);
log('cyan', ` 3. Install: cp /tmp/${variant}/libs/*.so ${gpuLibsDir}/`);
}
}
/**
* Download ONNX Runtime CoreML dylib for macOS
* CoreML is Apple's GPU acceleration framework for neural networks
*/
async function downloadONNXRuntimeCoreML() {
log('cyan', '\n📦 Setting up ONNX Runtime CoreML library for macOS...');
// Version info - must match build-macos-release.sh expectations
// NOTE: Using 1.22.0 due to ORT 1.23.x regression with external data + CoreML
// See: https://github.com/microsoft/onnxruntime/issues/26261
// TODO: Upgrade to 1.23.x+ when fix (PR #26263) is released
const ORT_VERSION = '1.22.0'; // ONNX Runtime version with CoreML support
const releaseUrl = `https://github.com/robertelee78/swictation/releases/download/onnx-runtime-macos-v${ORT_VERSION}/libonnxruntime.dylib`;
const tmpDir = path.join(os.tmpdir(), 'swictation-macos-install');
const dylibPath = path.join(tmpDir, 'libonnxruntime.dylib');
// Target directory in npm package
const nativeDir = path.join(__dirname, 'lib', 'native');
const targetDylibPath = path.join(nativeDir, 'libonnxruntime.dylib');
try {
// Check if already downloaded
if (fs.existsSync(targetDylibPath)) {
log('green', ` ✓ ONNX Runtime CoreML dylib already present`);
log('cyan', ` Location: ${targetDylibPath}`);
log('cyan', ` Skipping download`);
return;
}
// Check for pre-signed library from platform package (CI-signed with Developer ID)
// This is critical for macOS hardened runtime compatibility
const platformPkgLib = path.join(__dirname, 'node_modules', '@agidreams', 'darwin-arm64', 'lib', 'libonnxruntime.dylib');
if (fs.existsSync(platformPkgLib)) {
log('green', ` ✓ Found Developer ID signed ONNX Runtime from platform package`);
log('cyan', ` Source: ${platformPkgLib}`);
// Create target directory if needed
if (!fs.existsSync(nativeDir)) {
fs.mkdirSync(nativeDir, { recursive: true });
}
// Copy the pre-signed library
fs.copyFileSync(platformPkgLib, targetDylibPath);
log('green', ` ✓ Copied signed library to ${targetDylibPath}`);
// Verify signature (informational)
try {
const sigInfo = execSync(`codesign -dv "${targetDylibPath}" 2>&1 | head -5`, { encoding: 'utf8' });
if (sigInfo.includes('TeamIdentifier')) {
log('green', ` ✓ Library has valid Developer ID signature`);
}
} catch (err) {
// Signature check is informational only
}
return;
}
// Create directories
if (!fs.existsSync(tmpDir)) {
fs.mkdirSync(tmpDir, { recursive: true });
}
if (!fs.existsSync(nativeDir)) {
fs.mkdirSync(nativeDir, { recursive: true });
}
// Download dylib
log('cyan', ` Downloading CoreML-enabled ONNX Runtime...`);
log('cyan', ` URL: ${releaseUrl}`);
await downloadFile(releaseUrl, dylibPath);
log('green', ` ✓ Downloaded CoreML dylib (~80MB)`);
// Verify it's a valid Mach-O library
try {
const fileOutput = execSync(`file "${dylibPath}"`, { encoding: 'utf8' });
if (!fileOutput.includes('Mach-O') || !fileOutput.includes('arm64')) {
throw new Error(`Invalid Mach-O library: ${fileOutput}`);
}
log('green', ` ✓ Verified Mach-O ARM64 library`);
} catch (err) {
log('red', ` ✗ Library verification failed: ${err.message}`);
throw err;
}
// Copy to npm package native directory
fs.copyFileSync(dylibPath, targetDylibPath);
log('green', ` ✓ Installed to ${targetDylibPath}`);
// Check for CoreML support
try {
const symbols = execSync(`nm -g "${targetDylibPath}" | grep -i coreml | head -5`, { encoding: 'utf8' });
if (symbols) {
log('green', ` ✓ CoreML symbols detected in library`);
}
} catch (err) {
log('yellow', ` ⚠️ Could not verify CoreML symbols (may be normal)`);
}
// Cleanup temp file
try {
fs.unlinkSync(dylibPath);
} catch (err) {
// Cleanup is optional
}
log('green', `✅ CoreML-enabled ONNX Runtime ready for GPU acceleration`);
} catch (err) {
log('red', `\n❌ Failed to download ONNX Runtime CoreML library`);
log('yellow', ` Error: ${err.message}`);
log('cyan', '\n Manual installation:');
log('cyan', ` 1. Download: ${releaseUrl}`);
log('cyan', ` 2. Copy to: ${targetDylibPath}`);
throw err;
}
}
/**
* Download macOS ARM64 daemon binary from GitHub releases
* Required for Apple Silicon Macs - cannot use bundled Linux ELF binaries
*/
/**
* Download macOS ARM64 UI application from GitHub releases
* Required for Apple Silicon Macs - the Tauri-based UI application
*/
function detectOrtLibrary() {
log('cyan', '\n🔍 Detecting ONNX Runtime library path...');
// PRIORITY 1: Check platform package library (new architecture)
// Platform packages (@agidreams/linux-x64, @agidreams/darwin-arm64) include GPU-enabled ORT
try {
const { resolveBinaryPaths, isPlatformPackageInstalled } = require('./src/resolve-binary');
if (isPlatformPackageInstalled()) {
const binaryPaths = resolveBinaryPaths();
const isMacOS = process.platform === 'darwin';
const ortFileName = isMacOS ? 'libonnxruntime.dylib' : 'libonnxruntime.so';
const platformOrtLib = path.join(binaryPaths.libDir, ortFileName);
if (fs.existsSync(platformOrtLib)) {
log('green', `✓ Found ONNX Runtime (platform package): ${platformOrtLib}`);
log('cyan', ` Using GPU-enabled library from ${binaryPaths.packageName}`);
return platformOrtLib;
}
}
} catch (err) {
// Platform package not installed yet - continue to fallbacks
}
// PRIORITY 2: Check legacy bundled library path (deprecated, for backwards compatibility)
const npmOrtLib = path.join(__dirname, 'lib', 'native', 'libonnxruntime.so');
if (fs.existsSync(npmOrtLib)) {
log('green', `✓ Found ONNX Runtime (bundled): ${npmOrtLib}`);
log('cyan', ' Using bundled GPU-enabled library with CUDA provider support');
return npmOrtLib;
}
// PRIORITY 3: Fall back to Python installation (usually CPU-only)
log('yellow', '⚠️ Platform package ORT not found - checking Python fallback...');
try {
// Try to find ONNX Runtime through Python (fallback, usually CPU-only)
const ortPath = execSync(
'python3 -c "import onnxruntime; import os; print(os.path.join(os.path.dirname(onnxruntime.__file__), \'capi\'))"',
{ encoding: 'utf-8' }
).trim();
if (!fs.existsSync(ortPath)) {
log('yellow', '⚠️ Warning: ONNX Runtime capi directory not found at ' + ortPath);
log('yellow', ' Daemon may not work correctly without onnxruntime-gpu');
log('cyan', ' Install with: pip3 install onnxruntime-gpu');
return null;
}
// Find the actual .so file
const ortFiles = fs.readdirSync(ortPath).filter(f => f.startsWith('libonnxruntime.so'));
if (ortFiles.length === 0) {
log('yellow', '⚠️ Warning: Could not find libonnxruntime.so in ' + ortPath);
log('yellow', ' Daemon may not work correctly');
log('cyan', ' Install with: pip3 install onnxruntime-gpu');
return null;
}
// Use the first (or only) .so file found
const ortLibPath = path.join(ortPath, ortFiles[0]);
log('yellow', `⚠️ Using Python ONNX Runtime: ${ortLibPath}`);
log('yellow', ' Note: This may be CPU-only and lack CUDA support');
// Store in a config file for systemd service generation
const configDir = path.join(__dirname, 'config');
const envFilePath = path.join(configDir, 'detected-environment.json');
const envConfig = {
ORT_DYLIB_PATH: ortLibPath,
detected_at: new Date().toISOString(),
onnxruntime_version: execSync('python3 -c "import onnxruntime; print(onnxruntime.__version__)"', { encoding: 'utf-8' }).trim(),
warning: 'Using Python pip installation - may be CPU-only'
};
try {
fs.writeFileSync(envFilePath, JSON.stringify(envConfig, null, 2));
log('green', `✓ Saved environment config to ${envFilePath}`);
} catch (err) {
log('yellow', `Warning: Could not save environment config: ${err.message}`);
}
return ortLibPath;
} catch (err) {
log('yellow', '\n⚠️ Could not detect ONNX Runtime:');
log('yellow', ` ${err.message}`);
log('cyan', '\n📦 Please install onnxruntime-gpu for optimal performance:');
log('cyan', ' pip3 install onnxruntime-gpu');
log('cyan', '\n The daemon will not work correctly without this library!');
return null;
}
}
function generateSystemdService(ortLibPath) {
log('cyan', '\n⚙️ Generating systemd service files...');
try {
// Get platform package binary paths
const { resolveBinaryPaths } = require('./src/resolve-binary');
let binaryPaths;
try {
binaryPaths = resolveBinaryPaths();
log('cyan', ` Using platform package: ${binaryPaths.packageName}`);
log('cyan', ` Daemon binary: ${binaryPaths.daemon}`);
log('cyan', ` Platform lib directory: ${binaryPaths.libDir}`);
} catch (err) {
log('red', ` ✗ Could not resolve platform package binaries: ${err.message}`);
log('yellow', ' Service generation cannot proceed without platform package');
return;
}
const systemdDir = path.join(os.homedir(), '.config', 'systemd', 'user');
// Create systemd directory if it doesn't exist
if (!fs.existsSync(systemdDir)) {
fs.mkdirSync(systemdDir, { recursive: true });
log('green', `✓ Created ${systemdDir}`);
}
// Detect display environment variables (used by both daemon and UI services)
const runtimeDir = process.env.XDG_RUNTIME_DIR || `/run/user/${process.getuid()}`;
let waylandDisplay = null;
let xDisplay = process.env.DISPLAY || null;
try {
const sockets = fs.readdirSync(runtimeDir).filter(f => f.startsWith('wayland-'));
if (sockets.length > 0) {
// Use the first wayland socket (usually wayland-0 or wayland-1)
waylandDisplay = sockets[0];
log('cyan', ` Detected Wayland display: ${waylandDisplay}`);
}
} catch (err) {
// Wayland socket not found, may be X11-only system
}
// 1. Generate daemon service from template
const templatePath = path.join(__dirname, 'templates', 'swictation-daemon.service.template');
if (!fs.existsSync(templatePath)) {
log('yellow', `⚠️ Warning: Template not found at ${templatePath}`);
log('yellow', ' Skipping daemon service generation');
} else {
let template = fs.readFileSync(templatePath, 'utf8');
// Replace placeholders with platform package paths
// __INSTALL_DIR__ in template refers to the bin directory containing daemon binary
// Template has: __INSTALL_DIR__/swictation-daemon
// We need to replace __INSTALL_DIR__ with actual binDir from platform package
template = template.replace(/__INSTALL_DIR__/g, binaryPaths.binDir);
// CRITICAL: Detect GPU variant to determine which ONNX Runtime to use
const configDir = path.join(os.homedir(), '.config', 'swictation');
const gpuPackageInfoPath = path.join(configDir, 'gpu-package-info.json');
let finalOrtLibPath, finalLdLibraryPath;
let variant = 'latest'; // default
// Try to read GPU package info to get variant
if (fs.existsSync(gpuPackageInfoPath)) {
try {
const metadata = JSON.parse(fs.readFileSync(gpuPackageInfoPath, 'utf8'));
variant = metadata.variant || 'latest';
log('cyan', ` Detected GPU package variant: ${variant}`);
} catch (err) {
log('yellow', ` Warning: Could not read GPU package metadata: ${err.message}`);
}
}
// Detect CUDA paths upfront (needed for logging)
const detectedCudaPaths = detectCudaLibraryPaths();
// Check multiple possible ONNX Runtime locations in priority order:
// 1. gpu-libs directory (downloaded GPU libraries)
// 2. Platform package lib directory (bundled with package)
const gpuLibsDir = path.join(os.homedir(), '.local', 'share', 'swictation', 'gpu-libs');
const gpuLibsOrtPath = path.join(gpuLibsDir, 'libonnxruntime.so');
const platformOrtPath = path.join(binaryPaths.libDir, 'libonnxruntime.so');
if (fs.existsSync(gpuLibsOrtPath)) {
// Use gpu-libs ONNX Runtime (downloaded GPU libraries)
finalOrtLibPath = gpuLibsOrtPath;
// LD_LIBRARY_PATH: gpu-libs first (has CUDA providers), then platform lib, then CUDA paths
finalLdLibraryPath = [gpuLibsDir, binaryPaths.libDir, ...detectedCudaPaths].join(':');
log('cyan', ` Using downloaded ONNX Runtime: ${finalOrtLibPath}`);
log('cyan', ` GPU libraries directory: ${gpuLibsDir}`);
} else if (fs.existsSync(platformOrtPath)) {
// Use platform package ONNX Runtime (bundled)
finalOrtLibPath = platformOrtPath;
finalLdLibraryPath = [...detectedCudaPaths, binaryPaths.libDir].join(':');
log('cyan', ` Using platform package ONNX Runtime: ${finalOrtLibPath}`);
log('cyan', ` Platform lib directory: ${binaryPaths.libDir}`);
} else {
// Neither exists - use platform path but warn
log('yellow', ` ⚠️ ONNX Runtime not found in gpu-libs: ${gpuLibsOrtPath}`);
log('yellow', ` ⚠️ ONNX Runtime not found in platform: ${platformOrtPath}`);
finalOrtLibPath = platformOrtPath; // Use platform path as placeholder
finalLdLibraryPath = [...detectedCudaPaths, binaryPaths.libDir].join(':');
log('yellow', ' ⚠️ Service may fail - run GPU library download manually');
}
// CRITICAL: Trim all whitespace and newlines from paths to prevent malformed service file
if (finalOrtLibPath) {
const cleanPath = finalOrtLibPath.trim().replace(/[\r\n]/g, '');
template = template.replace(/__ORT_DYLIB_PATH__/g, cleanPath);
log('cyan', ` ORT_DYLIB_PATH set to: ${cleanPath}`);
} else {
log('yellow', '⚠️ Warning: ORT_DYLIB_PATH not detected');
log('yellow', ' Service file will contain placeholder - you must set it manually');
}
// CRITICAL: Trim and clean LD_LIBRARY_PATH
const cleanLdPath = finalLdLibraryPath.trim().replace(/[\r\n]/g, '');
template = template.replace(/__LD_LIBRARY_PATH__/g, cleanLdPath);
log('cyan', ` LD_LIBRARY_PATH set to: ${cleanLdPath}`);
if (detectedCudaPaths.length > 0) {
log('cyan', ` Detected ${detectedCudaPaths.length} CUDA library path(s):`);
detectedCudaPaths.forEach(p => log('cyan', ` ${p}`));
} else {
log('yellow', ' ⚠️ No CUDA libraries detected (CPU-only mode)');
}
// Add display environment variables before ImportEnvironment
if (waylandDisplay || xDisplay) {
const envVars = [];
if (waylandDisplay) envVars.push(`Environment="WAYLAND_DISPLAY=${waylandDisplay}"`);
if (xDisplay) envVars.push(`Environment="DISPLAY=${xDisplay}"`);
// Insert before ImportEnvironment= line
template = template.replace(
/ImportEnvironment=/,
`${envVars.join('\n')}\n\n# Import full user environment for PulseAudio/PipeWire session\n# This ensures all audio devices are detected properly (4 devices instead of 1)\n# Required for microphone access in user session\nImportEnvironment=`
);
}
// VALIDATION: Check for malformed Environment variables
const validateServiceFile = (content) => {
const lines = content.split('\n');
for (let i = 0; i < lines.length; i++) {
if (lines[i].includes('Environment=')) {
const quoteCnt = (lines[i].match(/"/g) || []).length;
if (quoteCnt % 2 !== 0) {
throw new Error(`Malformed Environment variable at line ${i+1}: ${lines[i]}`);
}
}
}
};
try {
validateServiceFile(template);
log('green', ' ✓ Service file validation passed');
} catch (err) {
log('red', ` ✗ Service file validation failed: ${err.message}`);
throw err;
}
// Write daemon service file
const daemonServicePath = path.join(systemdDir, 'swictation-daemon.service');
fs.writeFileSync(daemonServicePath, template);
log('green', `✓ Generated daemon service: ${daemonServicePath}`);
if (ortLibPath) {
log('cyan', ' Service configured with detected ONNX Runtime path');
} else {
log('yellow', ' ⚠️ Please edit the service file to set ORT_DYLIB_PATH manually');
}
}
// 2. Install UI service - detect environment and use appropriate UI
// Sway/wlroots compositors: Use Python Qt tray (better Wayland support)
// GNOME/KDE/X11: Use Tauri UI binary
// CRITICAL: Environment variables may not be set during npm postinstall
// So we use multiple detection methods that work regardless of environment:
// 1. Check running compositor processes (most reliable)
// 2. Check for compositor sockets
// 3. Fall back to environment variables if available
let isSway = false;
let isHyprland = false;
let isRiver = false;
let detectedCompositor = 'unknown';
// Method 1: Check running processes (works even without env vars)
try {
const pgrepSway = execSync('pgrep -x sway 2>/dev/null || true', { encoding: 'utf8' }).trim();
if (pgrepSway) {
isSway = true;