From 5c563f40ecdb70d2951495408336c715290a8360 Mon Sep 17 00:00:00 2001 From: matt Date: Sat, 14 Feb 2026 14:05:35 +0900 Subject: [PATCH] feat(wsl): enhance WSL integration with improved path resolution and error handling - Refactored the `toWslUncPath` function to accept a prefix for better flexibility. - Introduced `getWslExecutableCandidates` to dynamically find WSL executables. - Implemented `runWsl` to handle WSL command execution with error management. - Updated `listWslDistros` to include a fallback mechanism for inconsistent `-q` behavior. - Enhanced `resolveWslHome` and `handleFindWslClaudeRoots` to improve home path resolution and avoid duplicates in candidate paths. --- src/main/ipc/config.ts | 129 ++++++++++++++++++++++++++++++++--------- 1 file changed, 102 insertions(+), 27 deletions(-) diff --git a/src/main/ipc/config.ts b/src/main/ipc/config.ts index d4cba3cc..78da5bd9 100644 --- a/src/main/ipc/config.ts +++ b/src/main/ipc/config.ts @@ -744,25 +744,77 @@ function normalizeWslHomePath(home: string): string | null { return normalized; } -function toWslUncPath(distro: string, posixPath: string): string { +function toWslUncPath(prefix: '\\\\wsl.localhost' | '\\\\wsl$', distro: string, posixPath: string): string { const uncSuffix = posixPath.replace(/\//g, '\\'); - return `\\\\wsl.localhost\\${distro}${uncSuffix}`; + return `${prefix}\\${distro}${uncSuffix}`; +} + +function getWslExecutableCandidates(): string[] { + const candidates = new Set(); + + const windir = process.env.WINDIR; + if (windir) { + candidates.add(path.join(windir, 'System32', 'wsl.exe')); + candidates.add(path.join(windir, 'Sysnative', 'wsl.exe')); + } + + candidates.add('wsl.exe'); + return Array.from(candidates); +} + +async function runWsl(args: string[], timeout = 5000): Promise<{ stdout: string; stderr: string }> { + const candidates = getWslExecutableCandidates(); + let lastError: unknown = null; + + for (const executable of candidates) { + try { + const result = await execFileAsync(executable, args, { timeout }); + return { + stdout: result.stdout ?? '', + stderr: result.stderr ?? '', + }; + } catch (error) { + lastError = error; + } + } + + throw lastError instanceof Error ? lastError : new Error('Unable to execute wsl.exe'); } async function listWslDistros(): Promise { - const { stdout } = await execFileAsync('wsl.exe', ['-l', '-q'], { timeout: 4000 }); - return stdout - .split(/\r?\n/) - .map((line) => line.trim()) - .filter((line) => line.length > 0); + try { + const { stdout } = await runWsl(['-l', '-q'], 4000); + return stdout + .split(/\r?\n/) + .map((line) => line.trim()) + .filter((line) => line.length > 0); + } catch { + // Fallback for environments where -q behavior is inconsistent. + const { stdout } = await runWsl(['-l'], 4000); + return stdout + .split(/\r?\n/) + .map((line) => line.trim()) + .filter((line) => line.length > 0) + .filter((line) => !line.toLowerCase().startsWith('windows subsystem for linux')) + .filter((line) => !line.toLowerCase().includes('default version')) + .map(stripDefaultSuffix); + } +} + +function stripDefaultSuffix(input: string): string { + const suffix = '(default)'; + if (!input.toLowerCase().endsWith(suffix)) { + return input; + } + + return input.slice(0, input.length - suffix.length).trimEnd(); } async function resolveWslHome(distro: string): Promise { try { - const { stdout } = await execFileAsync( - 'wsl.exe', + const { stdout } = await runWsl( ['-d', distro, '--', 'sh', '-lc', 'printf %s "$HOME"'], - { timeout: 4000 } + 5000 ); return normalizeWslHomePath(stdout); } catch { @@ -787,29 +839,52 @@ async function handleFindWslClaudeRoots( } const candidates: WslClaudeRootCandidate[] = []; + const seen = new Set(); for (const distro of distros) { const homePath = await resolveWslHome(distro); - if (!homePath) { - continue; + const homeCandidates = new Set(); + if (homePath) { + homeCandidates.add(homePath); } + if (process.env.USERNAME) { + homeCandidates.add(`/home/${process.env.USERNAME}`); + } + homeCandidates.add('/root'); - const claudePosixPath = path.posix.join(homePath, '.claude'); - const claudeUncPath = toWslUncPath(distro, claudePosixPath); - const projectsPath = path.join(claudeUncPath, 'projects'); - - const hasProjectsDir = (() => { - try { - return fs.existsSync(projectsPath) && fs.statSync(projectsPath).isDirectory(); - } catch { - return false; + for (const homeCandidate of homeCandidates) { + const normalizedHome = normalizeWslHomePath(homeCandidate); + if (!normalizedHome) { + continue; } - })(); + const claudePosixPath = path.posix.join(normalizedHome, '.claude'); - candidates.push({ - distro, - path: claudeUncPath, - hasProjectsDir, - }); + const uncPaths = [ + toWslUncPath('\\\\wsl.localhost', distro, claudePosixPath), + toWslUncPath('\\\\wsl$', distro, claudePosixPath), + ]; + + for (const claudeUncPath of uncPaths) { + if (seen.has(claudeUncPath.toLowerCase())) { + continue; + } + seen.add(claudeUncPath.toLowerCase()); + + const projectsPath = path.join(claudeUncPath, 'projects'); + const hasProjectsDir = (() => { + try { + return fs.existsSync(projectsPath) && fs.statSync(projectsPath).isDirectory(); + } catch { + return false; + } + })(); + + candidates.push({ + distro, + path: claudeUncPath, + hasProjectsDir, + }); + } + } } return { success: true, data: candidates };