diff --git a/unsloth_cli/commands/start.py b/unsloth_cli/commands/start.py index 6da31229f8..707c2b3c90 100644 --- a/unsloth_cli/commands/start.py +++ b/unsloth_cli/commands/start.py @@ -1274,6 +1274,16 @@ def _install_agent(name: str, install_hint: str) -> Optional[str]: return executable +def _wsl_shim_env(command: list, env: dict, unset_env: tuple) -> tuple[dict, tuple]: + wsl_env_bridge = _wsl_bridge_names(env, unset_env) if _wsl_windows_executable(command) else () + if not wsl_env_bridge: + return env, wsl_env_bridge + # Bridge PWD via WSLENV (PWD/p) so the Windows shim finds its project root from the + # live cwd, not a stale inherited Linux PWD. Don't freeze env["PWD"]: a --no-launch + # recipe must translate the live PWD when run, not when generated; _launch overrides it. + return env, (*wsl_env_bridge, "PWD/p") + + def _launch( command: list, env: dict, @@ -1283,9 +1293,11 @@ def _launch( executable = shutil.which(command[0]) or _install_agent(command[0], install_hint) if executable is None: _fail(f"`{command[0]}` not found on PATH. Install it with: {install_hint}") - wsl_env_bridge = _wsl_bridge_names(env, unset_env) if _wsl_windows_executable(command) else () + env, wsl_env_bridge = _wsl_shim_env(command, env, unset_env) child_env = dict(os.environ) if wsl_env_bridge: + # Override stale inherited PWD with the real cwd so the shim resolves the project root. + env = {**env, "PWD": os.getcwd()} child_env["WSLENV"] = _merge_wslenv(child_env.get("WSLENV", ""), wsl_env_bridge) for name in unset_env: child_env[name] = "" @@ -1353,8 +1365,8 @@ def _run( if launch and clear_screen: click.clear() typer.echo(f"Unsloth {base} ยท model {entry['id']}") - wsl_env_bridge = _wsl_bridge_names(env, unset_env) if _wsl_windows_executable(command) else () if not launch: + env, wsl_env_bridge = _wsl_shim_env(command, env, unset_env) _print_env(env, command, unset_env = unset_env, wsl_env_bridge = wsl_env_bridge) return try: diff --git a/unsloth_cli/tests/test_start.py b/unsloth_cli/tests/test_start.py index 98bd9f8157..227918f63e 100644 --- a/unsloth_cli/tests/test_start.py +++ b/unsloth_cli/tests/test_start.py @@ -476,8 +476,10 @@ def test_connect_claude_launch_scrubs_conflicting_auth_env(fake_studio, monkeypa reason = "WSL-from-Linux scenario (calling a Windows agent .exe from inside WSL); " "os.name is 'posix' under WSL, so this path can't run on a native Windows runner.", ) -def test_connect_claude_windows_shim_from_wsl_bridges_env(fake_studio, monkeypatch): +def test_connect_claude_windows_shim_from_wsl_bridges_env(fake_studio, monkeypatch, tmp_path): captured = {} + monkeypatch.chdir(tmp_path) + monkeypatch.setenv("PWD", "/stale/outer/repo") monkeypatch.setenv("WSL_DISTRO_NAME", "Ubuntu") monkeypatch.setenv("ANTHROPIC_API_KEY", "sk-anthropic-stale") monkeypatch.setenv("CLAUDE_CODE_OAUTH_TOKEN", "oauth-stale") @@ -505,6 +507,8 @@ def test_connect_claude_windows_shim_from_wsl_bridges_env(fake_studio, monkeypat assert captured["env"]["ANTHROPIC_AUTH_TOKEN"] == "sk-unsloth-feedfacefeedface" assert captured["env"]["ANTHROPIC_BASE_URL"] == BASE assert captured["env"]["ANTHROPIC_MODEL"] == MODEL["id"] + assert captured["env"]["PWD"] == str(tmp_path) + assert "PWD/p" in captured["env"]["WSLENV"].split(":") for name in ( "ANTHROPIC_AUTH_TOKEN", "ANTHROPIC_BASE_URL", @@ -520,7 +524,11 @@ def test_connect_claude_windows_shim_from_wsl_bridges_env(fake_studio, monkeypat reason = "WSL-from-Linux scenario (calling a Windows agent .exe from inside WSL); " "os.name is 'posix' under WSL, so this path can't run on a native Windows runner.", ) -def test_connect_claude_no_launch_windows_shim_from_wsl_prints_wslenv(fake_studio, monkeypatch): +def test_connect_claude_no_launch_windows_shim_from_wsl_prints_wslenv( + fake_studio, monkeypatch, tmp_path +): + monkeypatch.chdir(tmp_path) + monkeypatch.setenv("PWD", "/stale/outer/repo") monkeypatch.setenv("WSL_DISTRO_NAME", "Ubuntu") monkeypatch.setattr( start.shutil, "which", lambda _: "/mnt/c/Users/samle/AppData/Roaming/npm/claude" @@ -532,6 +540,10 @@ def test_connect_claude_no_launch_windows_shim_from_wsl_prints_wslenv(fake_studi assert "export ANTHROPIC_API_KEY=" in result.output assert "export CLAUDE_CODE_OAUTH_TOKEN=" in result.output assert "export WSLENV=" in result.output + # PWD must NOT be frozen into the recipe (no `export PWD=`): WSLENV PWD/p translates the + # shell's live PWD at run time, so a recipe reused from another dir resolves the project root. + assert "export PWD=" not in result.output + assert "PWD/p" in result.output assert "ANTHROPIC_AUTH_TOKEN" in result.output assert "CLAUDE_CODE_OAUTH_TOKEN" in result.output