diff --git a/apps/server/src/providers/cursor-provider.ts b/apps/server/src/providers/cursor-provider.ts index af50af7c..1a0c4c90 100644 --- a/apps/server/src/providers/cursor-provider.ts +++ b/apps/server/src/providers/cursor-provider.ts @@ -27,7 +27,15 @@ import { CURSOR_MODEL_MAP, } from '@automaker/types'; import { createLogger, isAbortError } from '@automaker/utils'; -import { spawnJSONLProcess, type SubprocessOptions } from '@automaker/platform'; +import { + spawnJSONLProcess, + type SubprocessOptions, + isWslAvailable, + findCliInWsl, + createWslCommand, + execInWsl, + windowsToWslPath, +} from '@automaker/platform'; // Create logger for this module const logger = createLogger('CursorProvider'); @@ -69,6 +77,10 @@ export class CursorProvider extends BaseProvider { * The install script creates versioned folders like: * ~/.local/share/cursor-agent/versions/2025.12.17-996666f/cursor-agent * And symlinks to ~/.local/bin/cursor-agent + * + * Windows: + * - cursor-agent CLI only supports Linux/macOS, NOT native Windows + * - On Windows, users must install in WSL and we invoke via wsl.exe */ private static COMMON_PATHS: Record = { linux: [ @@ -79,11 +91,8 @@ export class CursorProvider extends BaseProvider { path.join(os.homedir(), '.local/bin/cursor-agent'), // Primary symlink location '/usr/local/bin/cursor-agent', ], - win32: [ - path.join(os.homedir(), 'AppData/Local/Programs/cursor-agent/cursor-agent.exe'), - path.join(os.homedir(), '.local/bin/cursor-agent.exe'), - 'C:\\Program Files\\cursor-agent\\cursor-agent.exe', - ], + // Windows paths are not used - we check for WSL installation instead + win32: [], }; // Version data directory where cursor-agent stores versions @@ -91,9 +100,14 @@ export class CursorProvider extends BaseProvider { private cliPath: string | null = null; + // WSL execution mode for Windows + private useWsl: boolean = false; + private wslCliPath: string | null = null; + private wslDistribution: string | undefined = undefined; + constructor(config: ProviderConfig = {}) { super(config); - this.cliPath = config.cliPath || this.findCliPath(); + this.findCliPath(); } getName(): string { @@ -102,15 +116,45 @@ export class CursorProvider extends BaseProvider { /** * Find cursor-agent CLI in PATH or common installation locations + * + * On Windows, uses WSL utilities from @automaker/platform since + * cursor-agent CLI only supports Linux/macOS natively. */ - private findCliPath(): string | null { - // Try 'which' / 'where' first + private findCliPath(): void { + const wslLogger = (msg: string) => logger.debug(msg); + + // On Windows, we need to use WSL (cursor-agent has no native Windows build) + if (process.platform === 'win32') { + if (isWslAvailable({ logger: wslLogger })) { + const wslResult = findCliInWsl('cursor-agent', { logger: wslLogger }); + if (wslResult) { + this.useWsl = true; + this.wslCliPath = wslResult.wslPath; + this.wslDistribution = wslResult.distribution; + this.cliPath = 'wsl.exe'; // We'll use wsl.exe to invoke + logger.debug( + `Using cursor-agent via WSL (${wslResult.distribution || 'default'}): ${wslResult.wslPath}` + ); + return; + } + } + logger.debug( + 'cursor-agent CLI not found (WSL not available or cursor-agent not installed in WSL)' + ); + this.cliPath = null; + return; + } + + // Linux/macOS - direct execution + // Try 'which' first try { - const cmd = process.platform === 'win32' ? 'where cursor-agent' : 'which cursor-agent'; - const result = execSync(cmd, { encoding: 'utf8', timeout: 5000 }).trim().split('\n')[0]; + const result = execSync('which cursor-agent', { encoding: 'utf8', timeout: 5000 }) + .trim() + .split('\n')[0]; if (result && fs.existsSync(result)) { logger.debug(`Found cursor-agent in PATH: ${result}`); - return result; + this.cliPath = result; + return; } } catch { // Not in PATH @@ -123,7 +167,8 @@ export class CursorProvider extends BaseProvider { for (const p of platformPaths) { if (fs.existsSync(p)) { logger.debug(`Found cursor-agent at: ${p}`); - return p; + this.cliPath = p; + return; } } @@ -137,11 +182,11 @@ export class CursorProvider extends BaseProvider { .reverse(); // Most recent first for (const version of versions) { - const binaryName = platform === 'win32' ? 'cursor-agent.exe' : 'cursor-agent'; - const versionPath = path.join(CursorProvider.VERSIONS_DIR, version, binaryName); + const versionPath = path.join(CursorProvider.VERSIONS_DIR, version, 'cursor-agent'); if (fs.existsSync(versionPath)) { logger.debug(`Found cursor-agent version ${version} at: ${versionPath}`); - return versionPath; + this.cliPath = versionPath; + return; } } } catch { @@ -150,7 +195,7 @@ export class CursorProvider extends BaseProvider { } logger.debug('cursor-agent CLI not found'); - return null; + this.cliPath = null; } /** @@ -167,6 +212,14 @@ export class CursorProvider extends BaseProvider { if (!this.cliPath) return null; try { + if (this.useWsl && this.wslCliPath) { + // Execute via WSL using utility from @automaker/platform + const result = execInWsl(`${this.wslCliPath} --version`, { + timeout: 5000, + distribution: this.wslDistribution, + }); + return result; + } const result = execSync(`"${this.cliPath}" --version`, { encoding: 'utf8', timeout: 5000, @@ -190,7 +243,43 @@ export class CursorProvider extends BaseProvider { return { authenticated: true, method: 'api_key' }; } - // Check for credentials file (location may vary) + // For WSL mode, check credentials inside WSL + if (this.useWsl && this.wslCliPath) { + const wslOpts = { timeout: 5000, distribution: this.wslDistribution }; + + // Check for credentials file inside WSL (use $HOME for proper expansion) + const wslCredPaths = [ + '$HOME/.cursor/credentials.json', + '$HOME/.config/cursor/credentials.json', + ]; + + for (const credPath of wslCredPaths) { + const content = execInWsl(`sh -c "cat ${credPath} 2>/dev/null || echo ''"`, wslOpts); + if (content && content.trim()) { + try { + const creds = JSON.parse(content); + if (creds.accessToken || creds.token) { + return { authenticated: true, method: 'login', hasCredentialsFile: true }; + } + } catch { + // Invalid credentials file + } + } + } + + // Try running --version to check if CLI works + const versionResult = execInWsl(`${this.wslCliPath} --version`, { + timeout: 10000, + distribution: this.wslDistribution, + }); + if (versionResult) { + return { authenticated: true, method: 'login' }; + } + + return { authenticated: false, method: 'none' }; + } + + // Native mode (Linux/macOS) - check local credentials const credentialPaths = [ path.join(os.homedir(), '.cursor', 'credentials.json'), path.join(os.homedir(), '.config', 'cursor', 'credentials.json'), @@ -238,11 +327,17 @@ export class CursorProvider extends BaseProvider { const version = installed ? await this.getVersion() : undefined; const auth = await this.checkAuth(); + // Determine the display path - for WSL, show the WSL path with distribution + const displayPath = + this.useWsl && this.wslCliPath + ? `(WSL${this.wslDistribution ? `:${this.wslDistribution}` : ''}) ${this.wslCliPath}` + : this.cliPath || undefined; + return { installed, version: version || undefined, - path: this.cliPath || undefined, - method: 'cli', + path: displayPath, + method: this.useWsl ? 'wsl' : 'cli', hasApiKey: !!process.env.CURSOR_API_KEY, authenticated: auth.authenticated, }; @@ -500,11 +595,17 @@ export class CursorProvider extends BaseProvider { */ async *executeQuery(options: ExecuteOptions): AsyncGenerator { if (!this.cliPath) { + // Provide platform-specific installation instructions + const installSuggestion = + process.platform === 'win32' + ? 'cursor-agent requires WSL on Windows. Install WSL, then run in WSL: curl https://cursor.com/install -fsS | bash' + : 'Install with: curl https://cursor.com/install -fsS | bash'; + throw this.createError( CursorErrorCode.NOT_INSTALLED, 'Cursor CLI is not installed', true, - 'Install with: curl https://cursor.com/install -fsS | bash' + installSuggestion ); } @@ -532,8 +633,8 @@ export class CursorProvider extends BaseProvider { throw new Error('Invalid prompt format'); } - // Build CLI arguments - const args: string[] = [ + // Build CLI arguments for cursor-agent + const cliArgs: string[] = [ '-p', // Print mode (non-interactive) '--force', // Allow file modifications '--output-format', @@ -543,13 +644,46 @@ export class CursorProvider extends BaseProvider { // Add model if not auto if (model !== 'auto') { - args.push('--model', model); + cliArgs.push('--model', model); } // Add the prompt - args.push(promptText); + cliArgs.push(promptText); - logger.debug(`Executing: ${this.cliPath} ${args.slice(0, 6).join(' ')}...`); + // Determine command and args based on WSL mode + let command: string; + let args: string[]; + let workingDir: string; + + if (this.useWsl && this.wslCliPath) { + // Build WSL command with --cd flag to change directory inside WSL + const wslCmd = createWslCommand(this.wslCliPath, cliArgs, { + distribution: this.wslDistribution, + }); + command = wslCmd.command; + const wslCwd = windowsToWslPath(cwd); + + // Construct args with --cd flag inserted before the CLI path + // createWslCommand returns: ['-d', distro, cliPath, ...cliArgs] or [cliPath, ...cliArgs] + if (this.wslDistribution) { + // With distribution: wsl.exe -d --cd + args = ['-d', this.wslDistribution, '--cd', wslCwd, this.wslCliPath, ...cliArgs]; + } else { + // Without distribution: wsl.exe --cd + args = ['--cd', wslCwd, this.wslCliPath, ...cliArgs]; + } + + // Keep Windows path for spawn's cwd (spawn runs on Windows) + workingDir = cwd; + logger.debug( + `Executing via WSL (${this.wslDistribution || 'default'}): ${command} ${args.slice(0, 6).join(' ')}...` + ); + } else { + command = this.cliPath; + args = cliArgs; + workingDir = cwd; + logger.debug(`Executing: ${command} ${args.slice(0, 6).join(' ')}...`); + } // Use spawnJSONLProcess from @automaker/platform for JSONL streaming // This handles line buffering, timeouts, and abort signals automatically @@ -562,9 +696,9 @@ export class CursorProvider extends BaseProvider { } const subprocessOptions: SubprocessOptions = { - command: this.cliPath, + command, args, - cwd, + cwd: workingDir, env: filteredEnv, abortController: options.abortController, timeout: 120000, // 2 min timeout for CLI operations (may take longer than default 30s) diff --git a/apps/server/src/providers/types.ts b/apps/server/src/providers/types.ts index 5a594361..5fcdb3e6 100644 --- a/apps/server/src/providers/types.ts +++ b/apps/server/src/providers/types.ts @@ -72,7 +72,15 @@ export interface InstallationStatus { installed: boolean; path?: string; version?: string; - method?: 'cli' | 'npm' | 'brew' | 'sdk'; + /** + * How the provider was installed/detected + * - cli: Direct CLI binary + * - wsl: CLI accessed via Windows Subsystem for Linux + * - npm: Installed via npm + * - brew: Installed via Homebrew + * - sdk: Using SDK library + */ + method?: 'cli' | 'wsl' | 'npm' | 'brew' | 'sdk'; hasApiKey?: boolean; authenticated?: boolean; error?: string; diff --git a/apps/server/tests/unit/lib/model-resolver.test.ts b/apps/server/tests/unit/lib/model-resolver.test.ts index 591cfed1..c2ea6123 100644 --- a/apps/server/tests/unit/lib/model-resolver.test.ts +++ b/apps/server/tests/unit/lib/model-resolver.test.ts @@ -42,7 +42,8 @@ describe('model-resolver.ts', () => { }); it('should treat unknown models as falling back to default', () => { - const models = ['o1', 'o1-mini', 'o3', 'gpt-5.2', 'unknown-model']; + // Note: Don't include valid Cursor model IDs here (e.g., 'gpt-5.2' is in CURSOR_MODEL_MAP) + const models = ['o1', 'o1-mini', 'o3', 'unknown-model', 'fake-model-123']; models.forEach((model) => { const result = resolveModelString(model); // Should fall back to default since these aren't supported diff --git a/apps/ui/src/components/views/settings-view.tsx b/apps/ui/src/components/views/settings-view.tsx index a5d420a8..7b3f3213 100644 --- a/apps/ui/src/components/views/settings-view.tsx +++ b/apps/ui/src/components/views/settings-view.tsx @@ -85,7 +85,7 @@ export function SettingsView() { switch (activeView) { case 'providers': case 'claude': // Backwards compatibility - return ; + return ; case 'ai-enhancement': return ; case 'appearance': diff --git a/libs/platform/src/index.ts b/libs/platform/src/index.ts index eba84101..762e9909 100644 --- a/libs/platform/src/index.ts +++ b/libs/platform/src/index.ts @@ -55,3 +55,18 @@ export { type NodeFinderResult, type NodeFinderOptions, } from './node-finder.js'; + +// WSL (Windows Subsystem for Linux) utilities +export { + isWslAvailable, + clearWslCache, + getDefaultWslDistribution, + getWslDistributions, + findCliInWsl, + execInWsl, + createWslCommand, + windowsToWslPath, + wslToWindowsPath, + type WslCliResult, + type WslOptions, +} from './wsl.js'; diff --git a/libs/platform/src/wsl.ts b/libs/platform/src/wsl.ts new file mode 100644 index 00000000..acf07c78 --- /dev/null +++ b/libs/platform/src/wsl.ts @@ -0,0 +1,389 @@ +/** + * WSL (Windows Subsystem for Linux) utilities + * + * Provides cross-platform support for CLI tools that are only available + * on Linux/macOS. On Windows, these tools can be accessed via WSL. + * + * @example + * ```typescript + * import { isWslAvailable, findCliInWsl, createWslCommand } from '@automaker/platform'; + * + * // Check if WSL is available + * if (process.platform === 'win32' && isWslAvailable()) { + * // Find a CLI tool installed in WSL + * const cliPath = findCliInWsl('cursor-agent'); + * if (cliPath) { + * // Create command/args for spawning via WSL + * const { command, args } = createWslCommand(cliPath, ['--version']); + * // command = 'wsl.exe', args = ['cursor-agent', '--version'] + * } + * } + * ``` + */ + +import { execSync } from 'child_process'; +import * as path from 'path'; + +/** + * Get the full path to wsl.exe + * This is needed because spawn() may not find wsl.exe in PATH + */ +function getWslExePath(): string { + // wsl.exe is in System32 on Windows + const systemRoot = process.env.SystemRoot || process.env.SYSTEMROOT || 'C:\\Windows'; + return path.join(systemRoot, 'System32', 'wsl.exe'); +} + +/** Result of finding a CLI in WSL */ +export interface WslCliResult { + /** Path to the CLI inside WSL (Linux path) */ + wslPath: string; + /** The WSL distribution where it was found (if detected) */ + distribution?: string; +} + +/** Options for WSL operations */ +export interface WslOptions { + /** Specific WSL distribution to use (default: use default distro) */ + distribution?: string; + /** Timeout for WSL commands in milliseconds (default: 10000) */ + timeout?: number; + /** Custom logger function */ + logger?: (message: string) => void; +} + +// Cache WSL availability to avoid repeated checks +let wslAvailableCache: boolean | null = null; + +/** + * Check if WSL is available on the current system + * + * Returns false immediately on non-Windows platforms. + * On Windows, checks if wsl.exe exists and can execute commands. + * + * Results are cached after first check. + */ +export function isWslAvailable(options: WslOptions = {}): boolean { + const { timeout = 5000, logger = () => {} } = options; + + // Only relevant on Windows + if (process.platform !== 'win32') { + return false; + } + + // Return cached result if available + if (wslAvailableCache !== null) { + return wslAvailableCache; + } + + try { + // Try to run a simple command via WSL + execSync('wsl.exe echo ok', { + encoding: 'utf8', + timeout, + stdio: ['pipe', 'pipe', 'pipe'], + windowsHide: true, + }); + wslAvailableCache = true; + logger('WSL is available'); + return true; + } catch { + // Try wsl --status as fallback + try { + execSync('wsl.exe --status', { + encoding: 'utf8', + timeout, + stdio: ['pipe', 'pipe', 'pipe'], + windowsHide: true, + }); + wslAvailableCache = true; + logger('WSL is available (via --status)'); + return true; + } catch { + wslAvailableCache = false; + logger('WSL is not available'); + return false; + } + } +} + +/** + * Clear the WSL availability cache + * Useful for testing or when WSL state may have changed + */ +export function clearWslCache(): void { + wslAvailableCache = null; +} + +/** + * Get the default WSL distribution name + */ +export function getDefaultWslDistribution(options: WslOptions = {}): string | null { + const { timeout = 5000 } = options; + + if (!isWslAvailable(options)) { + return null; + } + + try { + // wsl -l -q returns distributions, first one marked with (Default) + const result = execSync('wsl.exe -l -q', { + encoding: 'utf16le', // WSL list output uses UTF-16LE on Windows + timeout, + stdio: ['pipe', 'pipe', 'pipe'], + windowsHide: true, + }).trim(); + + // First non-empty line is the default + const lines = result.split(/\r?\n/).filter((l) => l.trim()); + return lines[0]?.replace(/\0/g, '').trim() || null; + } catch { + return null; + } +} + +/** + * Get all available WSL distributions + */ +export function getWslDistributions(options: WslOptions = {}): string[] { + const { timeout = 5000, logger = () => {} } = options; + + if (!isWslAvailable(options)) { + return []; + } + + try { + const result = execSync('wsl.exe -l -q', { + encoding: 'utf16le', // WSL list output uses UTF-16LE on Windows + timeout, + stdio: ['pipe', 'pipe', 'pipe'], + windowsHide: true, + }).trim(); + + const distributions = result + .split(/\r?\n/) + .map((l) => l.replace(/\0/g, '').trim()) + .filter((l) => l && !l.includes('docker-desktop')); // Exclude docker-desktop as it's minimal + + logger(`Found WSL distributions: ${distributions.join(', ')}`); + return distributions; + } catch { + return []; + } +} + +/** + * Find a CLI tool installed in WSL + * + * Searches for the CLI using 'which' inside WSL, then checks common paths. + * If no distribution is specified, tries all available distributions (excluding docker-desktop). + * + * @param cliName - Name of the CLI to find (e.g., 'cursor-agent') + * @param options - WSL options + * @returns The Linux path to the CLI and the distribution where found, or null if not found + */ +export function findCliInWsl(cliName: string, options: WslOptions = {}): WslCliResult | null { + const { distribution, timeout = 10000, logger = () => {} } = options; + + if (!isWslAvailable(options)) { + return null; + } + + // Helper to search in a specific distribution + const searchInDistribution = (distro: string | undefined): WslCliResult | null => { + const wslPrefix = distro ? `wsl.exe -d ${distro}` : 'wsl.exe'; + const distroLabel = distro || 'default'; + + // Try 'which' first (works if PATH is set up correctly) + try { + const result = execSync(`${wslPrefix} which ${cliName}`, { + encoding: 'utf8', + timeout, + stdio: ['pipe', 'pipe', 'pipe'], + windowsHide: true, + }).trim(); + + if (result && !result.includes('not found') && result.startsWith('/')) { + logger(`Found ${cliName} in WSL (${distroLabel}) via 'which': ${result}`); + return { wslPath: result, distribution: distro }; + } + } catch { + // Not found via which, continue to path checks + } + + // Check common installation paths using sh -c for better compatibility + // Use $HOME instead of ~ for reliable expansion + const commonPaths = ['$HOME/.local/bin', '/usr/local/bin', '/usr/bin']; + + for (const basePath of commonPaths) { + try { + // Use sh -c to properly expand $HOME and test if executable + const checkCmd = `${wslPrefix} sh -c "test -x ${basePath}/${cliName} && echo ${basePath}/${cliName}"`; + const result = execSync(checkCmd, { + encoding: 'utf8', + timeout, + stdio: ['pipe', 'pipe', 'pipe'], + windowsHide: true, + }).trim(); + + if (result && result.startsWith('/')) { + logger(`Found ${cliName} in WSL (${distroLabel}) at: ${result}`); + return { wslPath: result, distribution: distro }; + } + } catch { + // Path doesn't exist or not executable, continue + } + } + + return null; + }; + + // If a specific distribution is requested, only search there + if (distribution) { + return searchInDistribution(distribution); + } + + // Try available distributions (excluding docker-desktop and similar minimal distros) + const distributions = getWslDistributions(options); + + // Prioritize common user distributions + const priorityDistros = ['Ubuntu', 'Debian', 'openSUSE', 'Fedora', 'Arch']; + const sortedDistros = distributions.sort((a, b) => { + const aIndex = priorityDistros.findIndex((p) => a.toLowerCase().includes(p.toLowerCase())); + const bIndex = priorityDistros.findIndex((p) => b.toLowerCase().includes(p.toLowerCase())); + if (aIndex === -1 && bIndex === -1) return 0; + if (aIndex === -1) return 1; + if (bIndex === -1) return -1; + return aIndex - bIndex; + }); + + logger(`Searching for ${cliName} in WSL distributions: ${sortedDistros.join(', ')}`); + + for (const distro of sortedDistros) { + const result = searchInDistribution(distro); + if (result) { + return result; + } + } + + // Fallback: try default distribution as last resort + const defaultResult = searchInDistribution(undefined); + if (defaultResult) { + return defaultResult; + } + + logger(`${cliName} not found in any WSL distribution`); + return null; +} + +/** + * Execute a command in WSL and return the output + * + * @param command - Command to execute (can include arguments) + * @param options - WSL options + * @returns Command output, or null if failed + */ +export function execInWsl(command: string, options: WslOptions = {}): string | null { + const { distribution, timeout = 30000 } = options; + + if (!isWslAvailable(options)) { + return null; + } + + const wslPrefix = distribution ? `wsl.exe -d ${distribution}` : 'wsl.exe'; + + try { + return execSync(`${wslPrefix} ${command}`, { + encoding: 'utf8', + timeout, + stdio: ['pipe', 'pipe', 'pipe'], + windowsHide: true, + }).trim(); + } catch { + return null; + } +} + +/** + * Create command and arguments for spawning a process via WSL + * + * This is useful for constructing spawn() calls that work through WSL. + * Uses the full path to wsl.exe to ensure spawn() can find it. + * + * @param wslCliPath - The Linux path to the CLI inside WSL + * @param args - Arguments to pass to the CLI + * @param options - WSL options + * @returns Object with command (full path to wsl.exe) and modified args + * + * @example + * ```typescript + * const { command, args } = createWslCommand('/home/user/.local/bin/cursor-agent', ['-p', 'hello']); + * // command = 'C:\\Windows\\System32\\wsl.exe' + * // args = ['/home/user/.local/bin/cursor-agent', '-p', 'hello'] + * + * spawn(command, args, { stdio: ['ignore', 'pipe', 'pipe'] }); + * ``` + */ +export function createWslCommand( + wslCliPath: string, + args: string[], + options: WslOptions = {} +): { command: string; args: string[] } { + const { distribution } = options; + // Use full path to wsl.exe to ensure spawn() can find it + const wslExe = getWslExePath(); + + if (distribution) { + return { + command: wslExe, + args: ['-d', distribution, wslCliPath, ...args], + }; + } + + return { + command: wslExe, + args: [wslCliPath, ...args], + }; +} + +/** + * Convert a Windows path to a WSL path + * + * @param windowsPath - Windows path (e.g., 'C:\\Users\\foo\\project') + * @returns WSL path (e.g., '/mnt/c/Users/foo/project') + */ +export function windowsToWslPath(windowsPath: string): string { + // Handle UNC paths + if (windowsPath.startsWith('\\\\')) { + // UNC paths are not directly supported, return as-is + return windowsPath; + } + + // Extract drive letter and convert + const match = windowsPath.match(/^([A-Za-z]):\\(.*)$/); + if (match) { + const [, drive, rest] = match; + const wslPath = `/mnt/${drive.toLowerCase()}/${rest.replace(/\\/g, '/')}`; + return wslPath; + } + + // Already a Unix-style path or relative path + return windowsPath.replace(/\\/g, '/'); +} + +/** + * Convert a WSL path to a Windows path + * + * @param wslPath - WSL path (e.g., '/mnt/c/Users/foo/project') + * @returns Windows path (e.g., 'C:\\Users\\foo\\project'), or original if not a /mnt/ path + */ +export function wslToWindowsPath(wslPath: string): string { + const match = wslPath.match(/^\/mnt\/([a-z])\/(.*)$/); + if (match) { + const [, drive, rest] = match; + return `${drive.toUpperCase()}:\\${rest.replace(/\//g, '\\')}`; + } + + // Not a /mnt/ path, return as-is + return wslPath; +} diff --git a/libs/types/src/provider.ts b/libs/types/src/provider.ts index 53c92717..6e832b1b 100644 --- a/libs/types/src/provider.ts +++ b/libs/types/src/provider.ts @@ -81,7 +81,15 @@ export interface InstallationStatus { installed: boolean; path?: string; version?: string; - method?: 'cli' | 'npm' | 'brew' | 'sdk'; + /** + * How the provider was installed/detected + * - cli: Direct CLI binary + * - wsl: CLI accessed via Windows Subsystem for Linux + * - npm: Installed via npm + * - brew: Installed via Homebrew + * - sdk: Using SDK library + */ + method?: 'cli' | 'wsl' | 'npm' | 'brew' | 'sdk'; hasApiKey?: boolean; authenticated?: boolean; error?: string;