Implemented the next missing parity slice around IDE path conversion (Windows ↔ WSL).
This commit is contained in:
+1
-1
@@ -720,7 +720,7 @@ Missing major utility categories:
|
|||||||
- [ ] Session management (`utils/sessionStorage.ts`, `utils/sessionState.ts`, `utils/sessionStart.ts`, `utils/sessionRestore.ts`)
|
- [ ] Session management (`utils/sessionStorage.ts`, `utils/sessionState.ts`, `utils/sessionStart.ts`, `utils/sessionRestore.ts`)
|
||||||
- [ ] Plugin/Skill utilities (`utils/plugins/`, `utils/skills/`)
|
- [ ] Plugin/Skill utilities (`utils/plugins/`, `utils/skills/`)
|
||||||
- [ ] Memory/Context (`utils/memory/`, `utils/claudemd.ts`, `utils/contextAnalysis.ts`)
|
- [ ] Memory/Context (`utils/memory/`, `utils/claudemd.ts`, `utils/contextAnalysis.ts`)
|
||||||
- [ ] IDE integration (`utils/ide.ts`, `utils/jetbrains.ts`)
|
- [ ] IDE integration (`utils/ide.ts`, `utils/jetbrains.ts`) — partial: `utils/idePathConversion.ts` ported in `src/ide_path_conversion.py` (`WindowsToWSLConverter`, `checkWSLDistroMatch`)
|
||||||
- [ ] Platform/OS (`utils/platform.ts`, `utils/terminal.ts`, `utils/systemDirectories.ts`) — partial: platform detection (`getPlatform`, `getWslVersion`, `getLinuxDistroInfo`, `detectVcs`) and `getSystemDirectories` ported in `src/platform_info.py`
|
- [ ] Platform/OS (`utils/platform.ts`, `utils/terminal.ts`, `utils/systemDirectories.ts`) — partial: platform detection (`getPlatform`, `getWslVersion`, `getLinuxDistroInfo`, `detectVcs`) and `getSystemDirectories` ported in `src/platform_info.py`
|
||||||
- [ ] Debugging (`utils/debug.ts`, `utils/diagLogs.ts`, `utils/log.ts`, `utils/profilerBase.ts`)
|
- [ ] Debugging (`utils/debug.ts`, `utils/diagLogs.ts`, `utils/log.ts`, `utils/profilerBase.ts`)
|
||||||
- [ ] Telemetry (`utils/telemetry/`)
|
- [ ] Telemetry (`utils/telemetry/`)
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
"""IDE path conversion — Python port of ``utils/idePathConversion.ts``.
|
||||||
|
|
||||||
|
Used when Claude runs under WSL but the IDE (VS Code, JetBrains) is on the
|
||||||
|
host Windows side. Outgoing paths need to be converted to ``\\\\wsl$\\...``
|
||||||
|
form for the IDE; incoming paths from the IDE need to be converted back to
|
||||||
|
``/mnt/c/...`` form for Claude.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
from typing import Protocol
|
||||||
|
|
||||||
|
|
||||||
|
_WSL_UNC_RE = re.compile(r'^\\\\wsl(?:\.localhost|\$)\\([^\\]+)(.*)$')
|
||||||
|
_DRIVE_RE = re.compile(r'^([A-Za-z]):')
|
||||||
|
|
||||||
|
|
||||||
|
class IDEPathConverter(Protocol):
|
||||||
|
"""Bidirectional path mapping between IDE-side and Claude-side paths."""
|
||||||
|
|
||||||
|
def to_local_path(self, ide_path: str) -> str: ...
|
||||||
|
|
||||||
|
def to_ide_path(self, local_path: str) -> str: ...
|
||||||
|
|
||||||
|
|
||||||
|
def _run_wslpath(flag: str, path: str) -> str:
|
||||||
|
"""Invoke ``wslpath`` and return the stripped stdout. Raises on failure."""
|
||||||
|
completed = subprocess.run(
|
||||||
|
['wslpath', flag, path],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
return completed.stdout.strip()
|
||||||
|
|
||||||
|
|
||||||
|
def _manual_windows_to_wsl(windows_path: str) -> str:
|
||||||
|
"""Fallback when ``wslpath`` is unavailable: ``C:\\foo`` → ``/mnt/c/foo``."""
|
||||||
|
converted = windows_path.replace('\\', '/')
|
||||||
|
|
||||||
|
def _replace_drive(match: re.Match[str]) -> str:
|
||||||
|
return f'/mnt/{match.group(1).lower()}'
|
||||||
|
|
||||||
|
return _DRIVE_RE.sub(_replace_drive, converted)
|
||||||
|
|
||||||
|
|
||||||
|
class WindowsToWSLConverter:
|
||||||
|
"""Converter for the Windows IDE + WSL Claude scenario."""
|
||||||
|
|
||||||
|
def __init__(self, wsl_distro_name: str | None) -> None:
|
||||||
|
self.wsl_distro_name = wsl_distro_name
|
||||||
|
|
||||||
|
def to_local_path(self, windows_path: str) -> str:
|
||||||
|
if not windows_path:
|
||||||
|
return windows_path
|
||||||
|
|
||||||
|
if self.wsl_distro_name:
|
||||||
|
unc = _WSL_UNC_RE.match(windows_path)
|
||||||
|
if unc and unc.group(1) != self.wsl_distro_name:
|
||||||
|
# Path belongs to a different distro — wslpath would fail.
|
||||||
|
return windows_path
|
||||||
|
|
||||||
|
try:
|
||||||
|
return _run_wslpath('-u', windows_path)
|
||||||
|
except (subprocess.CalledProcessError, FileNotFoundError, OSError):
|
||||||
|
return _manual_windows_to_wsl(windows_path)
|
||||||
|
|
||||||
|
def to_ide_path(self, wsl_path: str) -> str:
|
||||||
|
if not wsl_path:
|
||||||
|
return wsl_path
|
||||||
|
try:
|
||||||
|
return _run_wslpath('-w', wsl_path)
|
||||||
|
except (subprocess.CalledProcessError, FileNotFoundError, OSError):
|
||||||
|
return wsl_path
|
||||||
|
|
||||||
|
|
||||||
|
def check_wsl_distro_match(windows_path: str, wsl_distro_name: str) -> bool:
|
||||||
|
"""True if ``windows_path`` isn't a WSL UNC path or names this distro."""
|
||||||
|
unc = _WSL_UNC_RE.match(windows_path)
|
||||||
|
if unc:
|
||||||
|
return unc.group(1) == wsl_distro_name
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
'IDEPathConverter',
|
||||||
|
'WindowsToWSLConverter',
|
||||||
|
'check_wsl_distro_match',
|
||||||
|
]
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
"""Tests for ``src/ide_path_conversion.py``."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
import unittest
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
from src.ide_path_conversion import (
|
||||||
|
WindowsToWSLConverter,
|
||||||
|
check_wsl_distro_match,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class CheckWslDistroMatchTest(unittest.TestCase):
|
||||||
|
def test_matches_named_distro(self) -> None:
|
||||||
|
self.assertTrue(
|
||||||
|
check_wsl_distro_match(r'\\wsl$\Ubuntu\home\me', 'Ubuntu'),
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_matches_localhost_form(self) -> None:
|
||||||
|
self.assertTrue(
|
||||||
|
check_wsl_distro_match(
|
||||||
|
r'\\wsl.localhost\Ubuntu\home\me', 'Ubuntu',
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_mismatch(self) -> None:
|
||||||
|
self.assertFalse(
|
||||||
|
check_wsl_distro_match(r'\\wsl$\Debian\home\me', 'Ubuntu'),
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_non_unc_path_returns_true(self) -> None:
|
||||||
|
self.assertTrue(check_wsl_distro_match(r'C:\Users\me', 'Ubuntu'))
|
||||||
|
|
||||||
|
|
||||||
|
class WindowsToWSLConverterToLocalPathTest(unittest.TestCase):
|
||||||
|
def test_empty_path_passthrough(self) -> None:
|
||||||
|
conv = WindowsToWSLConverter('Ubuntu')
|
||||||
|
self.assertEqual(conv.to_local_path(''), '')
|
||||||
|
|
||||||
|
def test_uses_wslpath_when_available(self) -> None:
|
||||||
|
conv = WindowsToWSLConverter(None)
|
||||||
|
with mock.patch(
|
||||||
|
'src.ide_path_conversion.subprocess.run',
|
||||||
|
return_value=subprocess.CompletedProcess(
|
||||||
|
args=[], returncode=0, stdout='/mnt/c/Users/me\n', stderr='',
|
||||||
|
),
|
||||||
|
) as run:
|
||||||
|
self.assertEqual(conv.to_local_path(r'C:\Users\me'), '/mnt/c/Users/me')
|
||||||
|
run.assert_called_once()
|
||||||
|
args = run.call_args[0][0]
|
||||||
|
self.assertEqual(args, ['wslpath', '-u', r'C:\Users\me'])
|
||||||
|
|
||||||
|
def test_falls_back_to_manual_when_wslpath_missing(self) -> None:
|
||||||
|
conv = WindowsToWSLConverter(None)
|
||||||
|
with mock.patch(
|
||||||
|
'src.ide_path_conversion.subprocess.run',
|
||||||
|
side_effect=FileNotFoundError(),
|
||||||
|
):
|
||||||
|
self.assertEqual(
|
||||||
|
conv.to_local_path(r'C:\Users\me'), '/mnt/c/Users/me',
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_falls_back_to_manual_on_called_process_error(self) -> None:
|
||||||
|
conv = WindowsToWSLConverter(None)
|
||||||
|
with mock.patch(
|
||||||
|
'src.ide_path_conversion.subprocess.run',
|
||||||
|
side_effect=subprocess.CalledProcessError(1, 'wslpath'),
|
||||||
|
):
|
||||||
|
self.assertEqual(
|
||||||
|
conv.to_local_path(r'D:\path\to\file.txt'),
|
||||||
|
'/mnt/d/path/to/file.txt',
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_different_distro_path_returned_as_is(self) -> None:
|
||||||
|
conv = WindowsToWSLConverter('Ubuntu')
|
||||||
|
with mock.patch(
|
||||||
|
'src.ide_path_conversion.subprocess.run',
|
||||||
|
) as run:
|
||||||
|
self.assertEqual(
|
||||||
|
conv.to_local_path(r'\\wsl$\Debian\home\me'),
|
||||||
|
r'\\wsl$\Debian\home\me',
|
||||||
|
)
|
||||||
|
run.assert_not_called()
|
||||||
|
|
||||||
|
def test_same_distro_unc_uses_wslpath(self) -> None:
|
||||||
|
conv = WindowsToWSLConverter('Ubuntu')
|
||||||
|
with mock.patch(
|
||||||
|
'src.ide_path_conversion.subprocess.run',
|
||||||
|
return_value=subprocess.CompletedProcess(
|
||||||
|
args=[], returncode=0, stdout='/home/me\n', stderr='',
|
||||||
|
),
|
||||||
|
) as run:
|
||||||
|
self.assertEqual(
|
||||||
|
conv.to_local_path(r'\\wsl$\Ubuntu\home\me'),
|
||||||
|
'/home/me',
|
||||||
|
)
|
||||||
|
run.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
class WindowsToWSLConverterToIdePathTest(unittest.TestCase):
|
||||||
|
def test_empty_passthrough(self) -> None:
|
||||||
|
conv = WindowsToWSLConverter(None)
|
||||||
|
self.assertEqual(conv.to_ide_path(''), '')
|
||||||
|
|
||||||
|
def test_uses_wslpath(self) -> None:
|
||||||
|
conv = WindowsToWSLConverter(None)
|
||||||
|
with mock.patch(
|
||||||
|
'src.ide_path_conversion.subprocess.run',
|
||||||
|
return_value=subprocess.CompletedProcess(
|
||||||
|
args=[], returncode=0, stdout=r'\\wsl$\Ubuntu\home\me' + '\n',
|
||||||
|
stderr='',
|
||||||
|
),
|
||||||
|
) as run:
|
||||||
|
self.assertEqual(
|
||||||
|
conv.to_ide_path('/home/me'), r'\\wsl$\Ubuntu\home\me',
|
||||||
|
)
|
||||||
|
run.assert_called_once()
|
||||||
|
args = run.call_args[0][0]
|
||||||
|
self.assertEqual(args, ['wslpath', '-w', '/home/me'])
|
||||||
|
|
||||||
|
def test_returns_original_on_failure(self) -> None:
|
||||||
|
conv = WindowsToWSLConverter(None)
|
||||||
|
with mock.patch(
|
||||||
|
'src.ide_path_conversion.subprocess.run',
|
||||||
|
side_effect=FileNotFoundError(),
|
||||||
|
):
|
||||||
|
self.assertEqual(conv.to_ide_path('/home/me'), '/home/me')
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user