From 07196903b87f536d05c233abe3e27c23659bcd3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=81=B5=E8=B6=8A=E7=BE=BD=E6=AF=9B?= <97326386+Icather@users.noreply.github.com> Date: Mon, 22 Jun 2026 12:09:34 +0800 Subject: [PATCH 1/2] fix(tools): convert Windows native CWD to MSYS format for bash cd When hermes is started from PowerShell or cmd.exe, os.getcwd() returns a Windows native path (C:\Users\...) which gets stored as the session working directory. Both init_session() and _wrap_command() embed this path into bash scripts via cd, but Git Bash cannot parse Windows drive-letter paths. Add _windows_to_msys_path() to BaseEnvironment: - C:\Users\x -> /c/Users/x (Git Bash compatible) - No-op on non-Windows or already-Msys paths Applied in init_session() and _wrap_command(). --- tools/environments/base.py | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/tools/environments/base.py b/tools/environments/base.py index 251bb18f14258..8914d0ab0f929 100644 --- a/tools/environments/base.py +++ b/tools/environments/base.py @@ -10,9 +10,11 @@ import json import logging import os +import re import select import shlex import subprocess +import sys import threading import time import uuid @@ -359,7 +361,8 @@ def init_session(self): # Restore configured cwd after login shell profile scripts, which may # change the working directory (e.g. bashrc `cd ~`). Without this, # pwd -P captures the profile's directory, not terminal.cwd. - _quoted_cwd = shlex.quote(self.cwd) + _cwd_for_bash = self._windows_to_msys_path(self.cwd) + _quoted_cwd = shlex.quote(_cwd_for_bash) # Quote the snapshot / cwd-file paths so Git Bash on Windows handles # ``C:/Users/...``-shaped paths without glob-splitting the colon or # tripping on drive letters. On POSIX this is a no-op (no colons / @@ -403,6 +406,26 @@ def init_session(self): # Command wrapping # ------------------------------------------------------------------ + @staticmethod + def _windows_to_msys_path(cwd: str) -> str: + """Convert a Windows native path to Git Bash / MSYS form for ``cd``. + + ``C:\\Users\\x`` → ``/c/Users/x``. No-op on non-Windows hosts or + paths that are already in MSYS format. + + ``_msys_to_windows_path`` (in ``local.py``) handles the reverse + translation for ``os.path.isdir`` / ``subprocess.Popen(cwd=...)``. + This helper closes the gap for the bash-script side. + """ + if sys.platform != "win32" or not cwd: + return cwd + m = re.match(r'^([a-zA-Z]):[\\/](.*)$', cwd) + if not m: + return cwd + drive = m.group(1).lower() + rest = m.group(2).replace('\\', '/') + return f"/{drive}/{rest}" + @staticmethod def _quote_cwd_for_cd(cwd: str) -> str: """Quote a ``cd`` target while preserving ``~`` expansion.""" @@ -441,7 +464,8 @@ def _wrap_command(self, command: str, cwd: str) -> str: # Preserve bare ``~`` expansion, but rewrite ``~/...`` through # ``$HOME`` so suffixes with spaces remain a single shell word. - quoted_cwd = self._quote_cwd_for_cd(cwd) + _cwd_for_bash = self._windows_to_msys_path(cwd) + quoted_cwd = self._quote_cwd_for_cd(_cwd_for_bash) # ``--`` keeps hyphen-prefixed directory names from being parsed as options. parts.append(f"builtin cd -- {quoted_cwd} || exit 126") From ab49f25f1c1f084e0dbb3a731ac078da1d60e0f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=81=B5=E8=B6=8A=E7=BE=BD=E6=AF=9B?= <97326386+Icather@users.noreply.github.com> Date: Mon, 22 Jun 2026 18:53:01 +0800 Subject: [PATCH 2/2] fix(tools): add double-dash to init_session cd, add unit tests for _windows_to_msys_path Copilot review follow-up: init_session bootstrap used builtin cd without double-dash; a cwd starting with dash could be parsed as an option. _wrap_command already guards. Added 6 unit tests for _windows_to_msys_path(). --- tests/tools/test_base_environment.py | 42 ++++++++++++++++++++++++++++ tools/environments/base.py | 2 +- 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/tests/tools/test_base_environment.py b/tests/tools/test_base_environment.py index 88fa6a7ea0f0c..13e50ffedc709 100644 --- a/tests/tools/test_base_environment.py +++ b/tests/tools/test_base_environment.py @@ -4,6 +4,7 @@ init_session() failure handling, and the CWD marker contract. """ +import sys from unittest.mock import MagicMock from tools.environments.base import BaseEnvironment @@ -194,3 +195,44 @@ def test_unique_per_instance(self): env1 = _TestableEnv() env2 = _TestableEnv() assert env1._cwd_marker != env2._cwd_marker + + +class TestWindowsToMsysPath: + def test_noop_on_non_windows(self, monkeypatch): + monkeypatch.setattr(sys, "platform", "linux") + assert ( + BaseEnvironment._windows_to_msys_path(r"C:\Users\x") + == r"C:\Users\x" + ) + + def test_converts_drive_path_on_windows(self, monkeypatch): + monkeypatch.setattr(sys, "platform", "win32") + assert ( + BaseEnvironment._windows_to_msys_path(r"C:\Users\NVIDIA") + == "/c/Users/NVIDIA" + ) + + def test_converts_forward_slash_variant(self, monkeypatch): + monkeypatch.setattr(sys, "platform", "win32") + assert ( + BaseEnvironment._windows_to_msys_path("D:/Projects/foo") + == "/d/Projects/foo" + ) + + def test_noop_on_already_msys_path(self, monkeypatch): + monkeypatch.setattr(sys, "platform", "win32") + assert ( + BaseEnvironment._windows_to_msys_path("/c/Users/NVIDIA") + == "/c/Users/NVIDIA" + ) + + def test_noop_on_empty_string(self, monkeypatch): + monkeypatch.setattr(sys, "platform", "win32") + assert BaseEnvironment._windows_to_msys_path("") == "" + + def test_preserves_trailing_slash(self, monkeypatch): + monkeypatch.setattr(sys, "platform", "win32") + assert ( + BaseEnvironment._windows_to_msys_path("C:\\Users\\NVIDIA\\") + == "/c/Users/NVIDIA/" + ) diff --git a/tools/environments/base.py b/tools/environments/base.py index 8914d0ab0f929..69c601c1115d0 100644 --- a/tools/environments/base.py +++ b/tools/environments/base.py @@ -379,7 +379,7 @@ def init_session(self): f"echo 'shopt -s expand_aliases' >> {_quoted_snap}\n" f"echo 'set +e' >> {_quoted_snap}\n" f"echo 'set +u' >> {_quoted_snap}\n" - f"builtin cd {_quoted_cwd} 2>/dev/null || true\n" + f"builtin cd -- {_quoted_cwd} 2>/dev/null || true\n" f"pwd -P > {_quoted_cwd_file} 2>/dev/null || true\n" f"printf '\\n{self._cwd_marker}%s{self._cwd_marker}\\n' \"$(pwd -P)\"\n" )