mirror of
https://github.com/rait-09/obsidian-agent-client.git
synced 2026-07-22 06:43:37 +00:00
fix(wsl): apply the launch hybrid to terminal commands too
Terminal commands (tool_call terminals) still used the nested `sh -c "<buildWslShellWrapper(innerCommand)>"` construction, which carries the same fragility that broke agent launch in some environments (e.g. RHEL8) and double-escaped the command line. Run terminal commands the same way as agents now: wsl.exe [--exec] /bin/sh -c '<launcher>' sh '<commandLine>' The command line is delivered as a single positional and run under the user's login shell ($SHELL -l -c "$1"). This keeps full shell parsing (pipes, redirects, subshells, bash-isms under the user's shell, e.g. bash) -- behaviorally identical to the previous `$SHELL -l -c <line>` -- while skipping wsl's default-shell layer (--exec) and removing the doubly-escaped nested string. Also, in WSL mode omit the Windows-side spawn cwd for terminals: the working directory is applied inside the launcher (cd '<wslCwd>'), and a Linux path as the wsl.exe process cwd would make CreateProcess fail. buildWslShellWrapper is retained (now used only by paths.ts for the WSL `which` lookup). Tests updated/added accordingly (43 cases).
This commit is contained in:
parent
850e2e1557
commit
b91184f012
3 changed files with 102 additions and 19 deletions
|
|
@ -110,9 +110,13 @@ export class TerminalManager {
|
|||
cwd: params.cwd,
|
||||
});
|
||||
|
||||
// Spawn the process
|
||||
// Spawn the process.
|
||||
// In WSL mode the working directory is applied inside the launcher
|
||||
// (cd '<wslCwd>'); the wsl.exe process must NOT receive a Linux path as
|
||||
// its Windows cwd (CreateProcess would fail), so omit cwd there.
|
||||
const useWsl = Platform.isWin && this.plugin.settings.windowsWslMode;
|
||||
const spawnOptions: SpawnOptions = {
|
||||
cwd: params.cwd || undefined,
|
||||
cwd: useWsl ? undefined : params.cwd || undefined,
|
||||
env,
|
||||
stdio: ["pipe", "pipe", "pipe"],
|
||||
shell: needsShell,
|
||||
|
|
|
|||
|
|
@ -289,6 +289,10 @@ export function isSameDirectory(pathA: string, pathB: string): boolean {
|
|||
* $SHELL, and falls back to /bin/sh for non-POSIX shells (fish, elvish,
|
||||
* nushell, xonsh).
|
||||
*
|
||||
* NOTE: agent and terminal launches now use buildWslArgvScript /
|
||||
* buildWslTerminalScript (--exec + positional argv) instead. This helper is
|
||||
* currently used only by paths.ts (resolveCommandPathInWsl, the `which` lookup).
|
||||
*
|
||||
* IMPORTANT: wsl.exe pre-expands $VAR references using WSL environment
|
||||
* variables before passing them to the Linux shell. Intermediate variables
|
||||
* (e.g., s=$SHELL; exec $s) will NOT work because wsl.exe expands $s to
|
||||
|
|
@ -344,6 +348,34 @@ export function buildWslArgvScript(): string {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the constant launcher script for terminal commands in WSL.
|
||||
*
|
||||
* Used as: `wsl.exe [-d dist] --exec /bin/sh -c '<this>' sh '<commandLine>'`
|
||||
* where the full shell command line is passed as a single positional ($1),
|
||||
* NEVER baked into this string — so there is no nested-quoting fragility. The
|
||||
* command line is then run under the user's login shell with `-c`, so it is
|
||||
* parsed by the user's actual shell (e.g. bash): pipes, redirects, subshells and
|
||||
* bash-specific syntax are preserved, with ~/.profile sourced. Falls back to
|
||||
* /bin/sh for non-POSIX shells (fish, elvish, nushell, xonsh).
|
||||
*
|
||||
* This mirrors buildWslShellWrapper's effective behavior (`$SHELL -l -c
|
||||
* <commandLine>`) but delivers the command line via an argv positional + `--exec`
|
||||
* instead of a doubly-escaped nested string, which avoids quoting failures seen
|
||||
* in some WSL environments (e.g. RHEL8).
|
||||
*
|
||||
* IMPORTANT (same caveat as buildWslArgvScript): reference `${SHELL:-/bin/sh}`
|
||||
* directly; do not use intermediate variables.
|
||||
*/
|
||||
export function buildWslTerminalScript(): string {
|
||||
return (
|
||||
`case \${SHELL:-/bin/sh} in ` +
|
||||
`*/fish|*/elvish|*/nushell|*/xonsh) exec /bin/sh -l -c "$1";; ` +
|
||||
`*) exec \${SHELL:-/bin/sh} -l -c "$1";; ` +
|
||||
`esac`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap a command to run inside WSL using wsl.exe.
|
||||
* Generates wsl.exe command with proper arguments for executing commands in WSL environment.
|
||||
|
|
@ -416,9 +448,12 @@ export function wrapCommandForWsl(
|
|||
};
|
||||
}
|
||||
|
||||
// Terminal launch: the command may be a shell line (pipes, redirects, &&),
|
||||
// so keep the shell-string wrapper and let the login shell parse it.
|
||||
// Use login shell (-l) to inherit PATH from user's shell profile.
|
||||
// Terminal launch: the command may be a shell line (pipes, redirects, &&,
|
||||
// subshells, bash-isms), so it must be parsed by the user's shell. Build the
|
||||
// same shell command line as before, but deliver it via --exec + a single
|
||||
// positional (no nested escaped string) so it survives WSL quoting, and run
|
||||
// it under the user's login shell ($SHELL -l -c "$1"). Behaviorally identical
|
||||
// to the previous `$SHELL -l -c <innerCommand>`, just delivered robustly.
|
||||
const escapedArgs = args.map(escapeShellArgBash).join(" ");
|
||||
const argsString = escapedArgs.length > 0 ? ` ${escapedArgs}` : "";
|
||||
|
||||
|
|
@ -431,7 +466,14 @@ export function wrapCommandForWsl(
|
|||
}
|
||||
|
||||
const innerCommand = `${pathPrefix}cd ${escapeShellArgBash(wslCwd)} && ${command}${argsString}`;
|
||||
wslArgs.push("sh", "-c", buildWslShellWrapper(innerCommand));
|
||||
wslArgs.push(
|
||||
"--exec",
|
||||
"/bin/sh",
|
||||
"-c",
|
||||
buildWslTerminalScript(),
|
||||
"sh",
|
||||
innerCommand,
|
||||
);
|
||||
|
||||
return {
|
||||
command: "C:\\Windows\\System32\\wsl.exe",
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import {
|
|||
escapeShellArgBash,
|
||||
escapeShellArgWindows,
|
||||
buildWslArgvScript,
|
||||
buildWslTerminalScript,
|
||||
wrapCommandForWsl,
|
||||
prepareShellCommand,
|
||||
buildWslEnv,
|
||||
|
|
@ -180,8 +181,8 @@ describe("wrapCommandForWsl — agent (useArgvExec)", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("wrapCommandForWsl — terminal (shell string)", () => {
|
||||
it("uses sh -c wrapper and lets the shell parse the command (pipes)", () => {
|
||||
describe("wrapCommandForWsl — terminal (shell string via hybrid)", () => {
|
||||
it("uses --exec + login shell, command line as a single positional (pipes preserved)", () => {
|
||||
const { command, args } = wrapCommandForWsl(
|
||||
"ls -la | grep foo",
|
||||
[],
|
||||
|
|
@ -191,14 +192,48 @@ describe("wrapCommandForWsl — terminal (shell string)", () => {
|
|||
false,
|
||||
);
|
||||
expect(command).toBe(WSL_EXE);
|
||||
expect(args).toContain("sh");
|
||||
expect(args).toContain("-c");
|
||||
const wrapper = args[args.indexOf("-c") + 1];
|
||||
// login shell + profile sourcing preserved
|
||||
expect(wrapper).toContain(". ~/.profile");
|
||||
expect(wrapper).toContain(" -l ");
|
||||
// command is left raw so the shell parses the pipe
|
||||
expect(wrapper).toContain("ls -la | grep foo");
|
||||
// header: --exec /bin/sh -c <launcher script>
|
||||
expect(args.slice(0, 3)).toEqual(["--exec", "/bin/sh", "-c"]);
|
||||
// launcher runs the command line under the user's login shell
|
||||
const script = args[3];
|
||||
expect(script).toContain(" -l ");
|
||||
expect(script).toContain("${SHELL:-/bin/sh}");
|
||||
expect(script).toContain('-c "$1"');
|
||||
// positionals: sh <innerCommand> — the command line is ONE intact element
|
||||
expect(args).toHaveLength(6);
|
||||
expect(args[4]).toBe("sh");
|
||||
const innerCommand = args[5];
|
||||
expect(innerCommand).toContain("ls -la | grep foo"); // raw pipe preserved
|
||||
expect(innerCommand).toContain("cd '/mnt/c/vault'");
|
||||
});
|
||||
|
||||
it("includes the PATH export and cd in the command line when additionalPath is set", () => {
|
||||
const { args } = wrapCommandForWsl(
|
||||
"node x.js",
|
||||
[],
|
||||
"C:\\v",
|
||||
undefined,
|
||||
"C:\\node\\bin",
|
||||
false,
|
||||
);
|
||||
const innerCommand = args[args.length - 1];
|
||||
expect(innerCommand).toContain('export PATH="/mnt/c/node/bin:$PATH"');
|
||||
expect(innerCommand).toContain("node x.js");
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildWslTerminalScript", () => {
|
||||
const s = buildWslTerminalScript();
|
||||
it("runs the command line under the user's login shell via -c", () => {
|
||||
expect(s).toContain(" -l ");
|
||||
expect(s).toContain('-c "$1"');
|
||||
expect(s).toContain("${SHELL:-/bin/sh}");
|
||||
});
|
||||
it("falls back to /bin/sh for non-POSIX shells", () => {
|
||||
expect(s).toContain("*/fish");
|
||||
});
|
||||
it("is a pure constant (no baked-in user data)", () => {
|
||||
expect(s).not.toContain("undefined");
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -227,15 +262,17 @@ describe("prepareShellCommand", () => {
|
|||
expect(r.args).toContain("--exec");
|
||||
});
|
||||
|
||||
it("WSL terminal: wsl.exe via sh -c wrapper", () => {
|
||||
it("WSL terminal: wsl.exe via --exec + login shell, command line as positional", () => {
|
||||
Platform.isWin = true;
|
||||
const r = prepareShellCommand("ls | grep x", [], "C:\\vault", {
|
||||
wslMode: true,
|
||||
alwaysEscape: false,
|
||||
});
|
||||
expect(r.command).toBe(WSL_EXE);
|
||||
expect(r.args).toContain("sh");
|
||||
expect(r.args).not.toContain("--exec");
|
||||
expect(r.needsShell).toBe(false);
|
||||
expect(r.args.slice(0, 3)).toEqual(["--exec", "/bin/sh", "-c"]);
|
||||
// the command line (with the pipe) is the last positional, intact
|
||||
expect(r.args[r.args.length - 1]).toContain("ls | grep x");
|
||||
});
|
||||
|
||||
it("macOS: wraps in a login shell (-l -c)", () => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue