feat(wsl): improve WSL path handling and output decoding
- Refactored `toWslUncPath` to remove the prefix parameter for consistency. - Added `looksLikeUtf16Le` and `decodeWslOutput` functions to enhance output decoding from WSL commands. - Updated `listWslDistros` to utilize a command array for better command execution handling. - Simplified home path resolution in `handleFindWslClaudeRoots` to avoid duplicates and improve clarity.
This commit is contained in:
parent
5c563f40ec
commit
0a987b8aea
1 changed files with 124 additions and 63 deletions
|
|
@ -744,9 +744,9 @@ function normalizeWslHomePath(home: string): string | null {
|
||||||
return normalized;
|
return normalized;
|
||||||
}
|
}
|
||||||
|
|
||||||
function toWslUncPath(prefix: '\\\\wsl.localhost' | '\\\\wsl$', distro: string, posixPath: string): string {
|
function toWslUncPath(distro: string, posixPath: string): string {
|
||||||
const uncSuffix = posixPath.replace(/\//g, '\\');
|
const uncSuffix = posixPath.replace(/\//g, '\\');
|
||||||
return `${prefix}\\${distro}${uncSuffix}`;
|
return `\\\\wsl.localhost\\${distro}${uncSuffix}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getWslExecutableCandidates(): string[] {
|
function getWslExecutableCandidates(): string[] {
|
||||||
|
|
@ -762,16 +762,54 @@ function getWslExecutableCandidates(): string[] {
|
||||||
return Array.from(candidates);
|
return Array.from(candidates);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function looksLikeUtf16Le(buffer: Buffer): boolean {
|
||||||
|
const sampleSize = Math.min(buffer.length, 512);
|
||||||
|
if (sampleSize < 2) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let pairs = 0;
|
||||||
|
let nullsAtOddIndex = 0;
|
||||||
|
for (let i = 0; i + 1 < sampleSize; i += 2) {
|
||||||
|
pairs += 1;
|
||||||
|
if (buffer[i + 1] === 0) {
|
||||||
|
nullsAtOddIndex += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return pairs > 0 && nullsAtOddIndex / pairs >= 0.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
function decodeWslOutput(output: string | Buffer | undefined): string {
|
||||||
|
if (typeof output === 'string') {
|
||||||
|
return output.replace(/\0/g, '');
|
||||||
|
}
|
||||||
|
if (!output || output.length === 0) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasUtf16LeBom = output.length >= 2 && output[0] === 0xff && output[1] === 0xfe;
|
||||||
|
const decoded = hasUtf16LeBom || looksLikeUtf16Le(output)
|
||||||
|
? output.toString('utf16le')
|
||||||
|
: output.toString('utf8');
|
||||||
|
return decoded.replace(/\0/g, '');
|
||||||
|
}
|
||||||
|
|
||||||
async function runWsl(args: string[], timeout = 5000): Promise<{ stdout: string; stderr: string }> {
|
async function runWsl(args: string[], timeout = 5000): Promise<{ stdout: string; stderr: string }> {
|
||||||
const candidates = getWslExecutableCandidates();
|
const candidates = getWslExecutableCandidates();
|
||||||
let lastError: unknown = null;
|
let lastError: unknown = null;
|
||||||
|
|
||||||
for (const executable of candidates) {
|
for (const executable of candidates) {
|
||||||
try {
|
try {
|
||||||
const result = await execFileAsync(executable, args, { timeout });
|
const result = await execFileAsync(executable, args, {
|
||||||
|
timeout,
|
||||||
|
windowsHide: true,
|
||||||
|
maxBuffer: 1024 * 1024,
|
||||||
|
encoding: 'buffer',
|
||||||
|
});
|
||||||
return {
|
return {
|
||||||
stdout: result.stdout ?? '',
|
stdout: decodeWslOutput(result.stdout),
|
||||||
stderr: result.stderr ?? '',
|
stderr: decodeWslOutput(result.stderr),
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
lastError = error;
|
lastError = error;
|
||||||
|
|
@ -781,24 +819,59 @@ async function runWsl(args: string[], timeout = 5000): Promise<{ stdout: string;
|
||||||
throw lastError instanceof Error ? lastError : new Error('Unable to execute wsl.exe');
|
throw lastError instanceof Error ? lastError : new Error('Unable to execute wsl.exe');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function listWslDistros(): Promise<string[]> {
|
function parseWslDistros(stdout: string): string[] {
|
||||||
try {
|
const distros: string[] = [];
|
||||||
const { stdout } = await runWsl(['-l', '-q'], 4000);
|
const seen = new Set<string>();
|
||||||
return stdout
|
const lines = stdout.split(/\r?\n/);
|
||||||
.split(/\r?\n/)
|
|
||||||
.map((line) => line.trim())
|
for (const rawLine of lines) {
|
||||||
.filter((line) => line.length > 0);
|
let line = rawLine.replace(/\0/g, '').trim();
|
||||||
} catch {
|
if (!line) {
|
||||||
// Fallback for environments where -q behavior is inconsistent.
|
continue;
|
||||||
const { stdout } = await runWsl(['-l'], 4000);
|
}
|
||||||
return stdout
|
|
||||||
.split(/\r?\n/)
|
line = line.replace(/^\*\s*/, '').trim();
|
||||||
.map((line) => line.trim())
|
line = stripDefaultSuffix(line);
|
||||||
.filter((line) => line.length > 0)
|
|
||||||
.filter((line) => !line.toLowerCase().startsWith('windows subsystem for linux'))
|
const lower = line.toLowerCase();
|
||||||
.filter((line) => !line.toLowerCase().includes('default version'))
|
if (
|
||||||
.map(stripDefaultSuffix);
|
lower.startsWith('windows subsystem for linux') ||
|
||||||
|
lower.includes('default version') ||
|
||||||
|
lower.startsWith('the following is a list')
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const key = line.toLowerCase();
|
||||||
|
if (!seen.has(key)) {
|
||||||
|
seen.add(key);
|
||||||
|
distros.push(line);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return distros;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function listWslDistros(): Promise<string[]> {
|
||||||
|
const commands: string[][] = [
|
||||||
|
['--list', '--quiet'],
|
||||||
|
['-l', '-q'],
|
||||||
|
['-l'],
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const command of commands) {
|
||||||
|
try {
|
||||||
|
const { stdout } = await runWsl(command, 4000);
|
||||||
|
const parsed = parseWslDistros(stdout);
|
||||||
|
if (parsed.length > 0) {
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Try the next command variant.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
function stripDefaultSuffix(input: string): string {
|
function stripDefaultSuffix(input: string): string {
|
||||||
|
|
@ -841,50 +914,38 @@ async function handleFindWslClaudeRoots(
|
||||||
const candidates: WslClaudeRootCandidate[] = [];
|
const candidates: WslClaudeRootCandidate[] = [];
|
||||||
const seen = new Set<string>();
|
const seen = new Set<string>();
|
||||||
for (const distro of distros) {
|
for (const distro of distros) {
|
||||||
const homePath = await resolveWslHome(distro);
|
const resolvedHomePath = await resolveWslHome(distro);
|
||||||
const homeCandidates = new Set<string>();
|
const fallbackHomePath = process.env.USERNAME ? `/home/${process.env.USERNAME}` : null;
|
||||||
if (homePath) {
|
const normalizedHome =
|
||||||
homeCandidates.add(homePath);
|
normalizeWslHomePath(resolvedHomePath ?? '') ??
|
||||||
}
|
(fallbackHomePath ? normalizeWslHomePath(fallbackHomePath) : null);
|
||||||
if (process.env.USERNAME) {
|
|
||||||
homeCandidates.add(`/home/${process.env.USERNAME}`);
|
|
||||||
}
|
|
||||||
homeCandidates.add('/root');
|
|
||||||
|
|
||||||
for (const homeCandidate of homeCandidates) {
|
if (!normalizedHome) {
|
||||||
const normalizedHome = normalizeWslHomePath(homeCandidate);
|
continue;
|
||||||
if (!normalizedHome) {
|
}
|
||||||
continue;
|
|
||||||
|
const claudePosixPath = path.posix.join(normalizedHome, '.claude');
|
||||||
|
const claudeUncPath = toWslUncPath(distro, claudePosixPath);
|
||||||
|
const key = claudeUncPath.toLowerCase();
|
||||||
|
if (seen.has(key)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
seen.add(key);
|
||||||
|
|
||||||
|
const projectsPath = path.join(claudeUncPath, 'projects');
|
||||||
|
const hasProjectsDir = (() => {
|
||||||
|
try {
|
||||||
|
return fs.existsSync(projectsPath) && fs.statSync(projectsPath).isDirectory();
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
const claudePosixPath = path.posix.join(normalizedHome, '.claude');
|
})();
|
||||||
|
|
||||||
const uncPaths = [
|
candidates.push({
|
||||||
toWslUncPath('\\\\wsl.localhost', distro, claudePosixPath),
|
distro,
|
||||||
toWslUncPath('\\\\wsl$', distro, claudePosixPath),
|
path: claudeUncPath,
|
||||||
];
|
hasProjectsDir,
|
||||||
|
});
|
||||||
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 };
|
return { success: true, data: candidates };
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue