diff --git a/src/filesystem/__tests__/path-utils.test.ts b/src/filesystem/__tests__/path-utils.test.ts index 8768de20cf..49590a75a0 100644 --- a/src/filesystem/__tests__/path-utils.test.ts +++ b/src/filesystem/__tests__/path-utils.test.ts @@ -10,14 +10,25 @@ describe('Path Utilities', () => { .toBe('/home/user/some path'); }); - it('converts WSL paths to Windows format', () => { + it('never converts WSL paths (they work correctly in WSL with Node.js fs)', () => { + // WSL paths should NEVER be converted, regardless of platform + // They are valid Linux paths that work with Node.js fs operations inside WSL expect(convertToWindowsPath('/mnt/c/NS/MyKindleContent')) - .toBe('C:\\NS\\MyKindleContent'); + .toBe('/mnt/c/NS/MyKindleContent'); + expect(convertToWindowsPath('/mnt/d/Documents')) + .toBe('/mnt/d/Documents'); }); - it('converts Unix-style Windows paths to Windows format', () => { - expect(convertToWindowsPath('/c/NS/MyKindleContent')) - .toBe('C:\\NS\\MyKindleContent'); + it('converts Unix-style Windows paths only on Windows platform', () => { + // On Windows, /c/ style paths should be converted + if (process.platform === 'win32') { + expect(convertToWindowsPath('/c/NS/MyKindleContent')) + .toBe('C:\\NS\\MyKindleContent'); + } else { + // On Linux, leave them unchanged + expect(convertToWindowsPath('/c/NS/MyKindleContent')) + .toBe('/c/NS/MyKindleContent'); + } }); it('leaves Windows paths unchanged but ensures backslashes', () => { @@ -34,11 +45,20 @@ describe('Path Utilities', () => { .toBe('C:\\Program Files\\Some App'); }); - it('handles uppercase and lowercase drive letters', () => { + it('handles drive letter paths based on platform', () => { + // WSL paths should never be converted expect(convertToWindowsPath('/mnt/d/some/path')) - .toBe('D:\\some\\path'); - expect(convertToWindowsPath('/d/some/path')) - .toBe('D:\\some\\path'); + .toBe('/mnt/d/some/path'); + + if (process.platform === 'win32') { + // On Windows, Unix-style paths like /d/ should be converted + expect(convertToWindowsPath('/d/some/path')) + .toBe('D:\\some\\path'); + } else { + // On Linux, /d/ is just a regular Unix path + expect(convertToWindowsPath('/d/some/path')) + .toBe('/d/some/path'); + } }); }); @@ -67,21 +87,33 @@ describe('Path Utilities', () => { .toBe('C:\\NS\\MyKindleContent'); }); - it('handles WSL paths', () => { + it('always preserves WSL paths (they work correctly in WSL)', () => { + // WSL paths should ALWAYS be preserved, regardless of platform + // This is the fix for issue #2795 expect(normalizePath('/mnt/c/NS/MyKindleContent')) - .toBe('C:\\NS\\MyKindleContent'); + .toBe('/mnt/c/NS/MyKindleContent'); + expect(normalizePath('/mnt/d/Documents')) + .toBe('/mnt/d/Documents'); }); it('handles Unix-style Windows paths', () => { - expect(normalizePath('/c/NS/MyKindleContent')) - .toBe('C:\\NS\\MyKindleContent'); + // On Windows, /c/ paths should be converted + if (process.platform === 'win32') { + expect(normalizePath('/c/NS/MyKindleContent')) + .toBe('C:\\NS\\MyKindleContent'); + } else if (process.platform === 'linux') { + // On Linux, /c/ is just a regular Unix path + expect(normalizePath('/c/NS/MyKindleContent')) + .toBe('/c/NS/MyKindleContent'); + } }); it('handles paths with spaces and mixed slashes', () => { expect(normalizePath('C:/NS/My Kindle Content')) .toBe('C:\\NS\\My Kindle Content'); + // WSL paths should always be preserved expect(normalizePath('/mnt/c/NS/My Kindle Content')) - .toBe('C:\\NS\\My Kindle Content'); + .toBe('/mnt/c/NS/My Kindle Content'); expect(normalizePath('C:\\Program Files (x86)\\App Name')) .toBe('C:\\Program Files (x86)\\App Name'); expect(normalizePath('"C:\\Program Files\\App Name"')) @@ -91,10 +123,19 @@ describe('Path Utilities', () => { }); it('preserves spaces in all path formats', () => { + // WSL paths should always be preserved expect(normalizePath('/mnt/c/Program Files/App Name')) - .toBe('C:\\Program Files\\App Name'); - expect(normalizePath('/c/Program Files/App Name')) - .toBe('C:\\Program Files\\App Name'); + .toBe('/mnt/c/Program Files/App Name'); + + if (process.platform === 'win32') { + // On Windows, Unix-style paths like /c/ should be converted + expect(normalizePath('/c/Program Files/App Name')) + .toBe('C:\\Program Files\\App Name'); + } else { + // On Linux, /c/ is just a regular Unix path + expect(normalizePath('/c/Program Files/App Name')) + .toBe('/c/Program Files/App Name'); + } expect(normalizePath('C:/Program Files/App Name')) .toBe('C:\\Program Files\\App Name'); }); @@ -105,15 +146,16 @@ describe('Path Utilities', () => { .toBe('C:\\NS\\Sub&Folder'); expect(normalizePath('C:/NS/Sub&Folder')) .toBe('C:\\NS\\Sub&Folder'); + // WSL paths should always be preserved expect(normalizePath('/mnt/c/NS/Sub&Folder')) - .toBe('C:\\NS\\Sub&Folder'); - + .toBe('/mnt/c/NS/Sub&Folder'); + // Test tilde in path (short names in Windows) expect(normalizePath('C:\\NS\\MYKIND~1')) .toBe('C:\\NS\\MYKIND~1'); expect(normalizePath('/Users/NEMANS~1/FOLDER~2/SUBFO~1/Public/P12PST~1')) .toBe('/Users/NEMANS~1/FOLDER~2/SUBFO~1/Public/P12PST~1'); - + // Test other special characters expect(normalizePath('C:\\Path with #hash')) .toBe('C:\\Path with #hash'); @@ -128,10 +170,19 @@ describe('Path Utilities', () => { it('capitalizes lowercase drive letters for Windows paths', () => { expect(normalizePath('c:/windows/system32')) .toBe('C:\\windows\\system32'); - expect(normalizePath('/mnt/d/my/folder')) // WSL path with lowercase drive - .toBe('D:\\my\\folder'); - expect(normalizePath('/e/another/folder')) // Unix-style Windows path with lowercase drive - .toBe('E:\\another\\folder'); + // WSL paths should always be preserved + expect(normalizePath('/mnt/d/my/folder')) + .toBe('/mnt/d/my/folder'); + + if (process.platform === 'win32') { + // On Windows, Unix-style paths should be converted and capitalized + expect(normalizePath('/e/another/folder')) + .toBe('E:\\another\\folder'); + } else { + // On Linux, /e/ is just a regular Unix path + expect(normalizePath('/e/another/folder')) + .toBe('/e/another/folder'); + } }); it('handles UNC paths correctly', () => { @@ -172,4 +223,132 @@ describe('Path Utilities', () => { expect(expandHome('C:/test')).toBe('C:/test'); }); }); + + describe('WSL path handling (issue #2795 fix)', () => { + // Save original platform + const originalPlatform = process.platform; + + afterEach(() => { + // Restore platform after each test + Object.defineProperty(process, 'platform', { + value: originalPlatform, + writable: true, + configurable: true + }); + }); + + it('should NEVER convert WSL paths - they work correctly in WSL with Node.js fs', () => { + // The key insight: When running `wsl npx ...`, Node.js runs INSIDE WSL (process.platform === 'linux') + // and /mnt/c/ paths work correctly with Node.js fs operations in that environment. + // Converting them to C:\ format breaks fs operations because Windows paths don't work inside WSL. + + // Mock Linux platform (inside WSL) + Object.defineProperty(process, 'platform', { + value: 'linux', + writable: true, + configurable: true + }); + + // WSL paths should NOT be converted, even inside WSL + expect(normalizePath('/mnt/c/Users/username/folder')) + .toBe('/mnt/c/Users/username/folder'); + + expect(normalizePath('/mnt/d/Documents/project')) + .toBe('/mnt/d/Documents/project'); + }); + + it('should also preserve WSL paths when running on Windows', () => { + // Mock Windows platform + Object.defineProperty(process, 'platform', { + value: 'win32', + writable: true, + configurable: true + }); + + // WSL paths should still be preserved (though they wouldn't be accessible from Windows Node.js) + expect(normalizePath('/mnt/c/Users/username/folder')) + .toBe('/mnt/c/Users/username/folder'); + + expect(normalizePath('/mnt/d/Documents/project')) + .toBe('/mnt/d/Documents/project'); + }); + + it('should convert Unix-style Windows paths (/c/) only when running on Windows (win32)', () => { + // Mock process.platform to be 'win32' (Windows) + Object.defineProperty(process, 'platform', { + value: 'win32', + writable: true, + configurable: true + }); + + // Unix-style Windows paths like /c/ should be converted on Windows + expect(normalizePath('/c/Users/username/folder')) + .toBe('C:\\Users\\username\\folder'); + + expect(normalizePath('/d/Documents/project')) + .toBe('D:\\Documents\\project'); + }); + + it('should NOT convert Unix-style paths (/c/) when running inside WSL (linux)', () => { + // Mock process.platform to be 'linux' (WSL/Linux) + Object.defineProperty(process, 'platform', { + value: 'linux', + writable: true, + configurable: true + }); + + // When on Linux, /c/ is just a regular Unix directory, not a drive letter + expect(normalizePath('/c/some/path')) + .toBe('/c/some/path'); + + expect(normalizePath('/d/another/path')) + .toBe('/d/another/path'); + }); + + it('should preserve regular Unix paths on all platforms', () => { + // Test on Linux + Object.defineProperty(process, 'platform', { + value: 'linux', + writable: true, + configurable: true + }); + + expect(normalizePath('/home/user/documents')) + .toBe('/home/user/documents'); + + expect(normalizePath('/var/log/app')) + .toBe('/var/log/app'); + + // Test on Windows (though these paths wouldn't work on Windows) + Object.defineProperty(process, 'platform', { + value: 'win32', + writable: true, + configurable: true + }); + + expect(normalizePath('/home/user/documents')) + .toBe('/home/user/documents'); + + expect(normalizePath('/var/log/app')) + .toBe('/var/log/app'); + }); + + it('reproduces exact scenario from issue #2795', () => { + // Simulate running inside WSL: wsl npx @modelcontextprotocol/server-filesystem /mnt/c/Users/username/folder + Object.defineProperty(process, 'platform', { + value: 'linux', + writable: true, + configurable: true + }); + + // This is the exact path from the issue + const inputPath = '/mnt/c/Users/username/folder'; + const result = normalizePath(inputPath); + + // Should NOT convert to C:\Users\username\folder + expect(result).toBe('/mnt/c/Users/username/folder'); + expect(result).not.toContain('C:'); + expect(result).not.toContain('\\'); + }); + }); }); diff --git a/src/filesystem/path-utils.ts b/src/filesystem/path-utils.ts index aca7f061fe..c897b9d2c7 100644 --- a/src/filesystem/path-utils.ts +++ b/src/filesystem/path-utils.ts @@ -8,14 +8,15 @@ import os from 'os'; */ export function convertToWindowsPath(p: string): string { // Handle WSL paths (/mnt/c/...) + // NEVER convert WSL paths - they are valid Linux paths that work with Node.js fs operations in WSL + // Converting them to Windows format (C:\...) breaks fs operations inside WSL if (p.startsWith('/mnt/')) { - const driveLetter = p.charAt(5).toUpperCase(); - const pathPart = p.slice(6).replace(/\//g, '\\'); - return `${driveLetter}:${pathPart}`; + return p; // Leave WSL paths unchanged } - + // Handle Unix-style Windows paths (/c/...) - if (p.match(/^\/[a-zA-Z]\//)) { + // Only convert when running on Windows + if (p.match(/^\/[a-zA-Z]\//) && process.platform === 'win32') { const driveLetter = p.charAt(1).toUpperCase(); const pathPart = p.slice(2).replace(/\//g, '\\'); return `${driveLetter}:${pathPart}`; @@ -38,21 +39,29 @@ export function convertToWindowsPath(p: string): string { export function normalizePath(p: string): string { // Remove any surrounding quotes and whitespace p = p.trim().replace(/^["']|["']$/g, ''); - - // Check if this is a Unix path (starts with / but not a Windows or WSL path) - const isUnixPath = p.startsWith('/') && - !p.match(/^\/mnt\/[a-z]\//i) && - !p.match(/^\/[a-zA-Z]\//); - + + // Check if this is a Unix path that should not be converted + // WSL paths (/mnt/) should ALWAYS be preserved as they work correctly in WSL with Node.js fs + // Regular Unix paths should also be preserved + const isUnixPath = p.startsWith('/') && ( + // Always preserve WSL paths (/mnt/c/, /mnt/d/, etc.) + p.match(/^\/mnt\/[a-z]\//i) || + // On non-Windows platforms, treat all absolute paths as Unix paths + (process.platform !== 'win32') || + // On Windows, preserve Unix paths that aren't Unix-style Windows paths (/c/, /d/, etc.) + (process.platform === 'win32' && !p.match(/^\/[a-zA-Z]\//)) + ); + if (isUnixPath) { // For Unix paths, just normalize without converting to Windows format // Replace double slashes with single slashes and remove trailing slashes return p.replace(/\/+/g, '/').replace(/\/+$/, ''); } - - // Convert WSL or Unix-style Windows paths to Windows format + + // Convert Unix-style Windows paths (/c/, /d/) to Windows format if on Windows + // This function will now leave /mnt/ paths unchanged p = convertToWindowsPath(p); - + // Handle double backslashes, preserving leading UNC \\ if (p.startsWith('\\\\')) { // For UNC paths, first normalize any excessive leading backslashes to exactly \\ @@ -67,15 +76,15 @@ export function normalizePath(p: string): string { // For non-UNC paths, normalize all double backslashes p = p.replace(/\\\\/g, '\\'); } - + // Use Node's path normalization, which handles . and .. segments let normalized = path.normalize(p); - + // Fix UNC paths after normalization (path.normalize can remove a leading backslash) if (p.startsWith('\\\\') && !normalized.startsWith('\\\\')) { normalized = '\\' + normalized; } - + // Handle Windows paths: convert slashes and ensure drive letter is capitalized if (normalized.match(/^[a-zA-Z]:/)) { let result = normalized.replace(/\//g, '\\'); @@ -85,10 +94,15 @@ export function normalizePath(p: string): string { } return result; } - - // For all other paths (including relative paths), convert forward slashes to backslashes - // This ensures relative paths like "some/relative/path" become "some\\relative\\path" - return normalized.replace(/\//g, '\\'); + + // On Windows, convert forward slashes to backslashes for relative paths + // On Linux/Unix, preserve forward slashes + if (process.platform === 'win32') { + return normalized.replace(/\//g, '\\'); + } + + // On non-Windows platforms, keep the normalized path as-is + return normalized; } /**