From e1dfb4a8448b39d4b2ceeed28df0804bed739d47 Mon Sep 17 00:00:00 2001 From: tanzhenxin Date: Fri, 13 Mar 2026 15:21:10 +0800 Subject: [PATCH] fix(shell): only use string args for cmd.exe on Windows PowerShell handles array args correctly via CommandLineToArgvW, and the string form breaks quoted paths ending in backslash (e.g., "C:\Temp\") because \" is treated as an escaped quote. This refines the previous fix to only apply the workaround to cmd.exe. Co-authored-by: Qwen-Coder --- .../services/shellExecutionService.test.ts | 23 +++++++++++++++++ .../src/services/shellExecutionService.ts | 25 +++++++++++-------- 2 files changed, 38 insertions(+), 10 deletions(-) diff --git a/packages/core/src/services/shellExecutionService.test.ts b/packages/core/src/services/shellExecutionService.test.ts index 777407cfb..92f3e4c4f 100644 --- a/packages/core/src/services/shellExecutionService.test.ts +++ b/packages/core/src/services/shellExecutionService.test.ts @@ -441,6 +441,29 @@ describe('ShellExecutionService', () => { }); }); + it('should use PowerShell on Windows with array args', async () => { + mockPlatform.mockReturnValue('win32'); + mockGetShellConfiguration.mockReturnValue({ + executable: 'powershell.exe', + argsPrefix: ['-NoProfile', '-Command'], + shell: 'powershell', + }); + await simulateExecution('Test-Path "C:\\Temp\\"', (pty) => + pty.onExit.mock.calls[0][0]({ exitCode: 0, signal: null }), + ); + + expect(mockPtySpawn).toHaveBeenCalledWith( + 'powershell.exe', + ['-NoProfile', '-Command', 'Test-Path "C:\\Temp\\"'], + expect.any(Object), + ); + mockGetShellConfiguration.mockReturnValue({ + executable: 'bash', + argsPrefix: ['-c'], + shell: 'bash', + }); + }); + it('should use bash on Linux', async () => { mockPlatform.mockReturnValue('linux'); await simulateExecution('ls "foo bar"', (pty) => diff --git a/packages/core/src/services/shellExecutionService.ts b/packages/core/src/services/shellExecutionService.ts index 068d439ec..0843fa1f7 100644 --- a/packages/core/src/services/shellExecutionService.ts +++ b/packages/core/src/services/shellExecutionService.ts @@ -418,16 +418,21 @@ export class ShellExecutionService { try { const cols = shellExecutionConfig.terminalWidth ?? 80; const rows = shellExecutionConfig.terminalHeight ?? 30; - const { executable, argsPrefix } = getShellConfiguration(); - // On Windows, pass args as a single string instead of an array. - // node-pty's argsToCommandLine re-quotes array elements that contain - // spaces, which mangles user-provided quoted arguments (e.g., - // `type "hello world"` becomes `"type \"hello world\""`). - // When args is a string, node-pty concatenates it verbatim. - const isWin = os.platform() === 'win32'; - const args: string[] | string = isWin - ? [...argsPrefix, commandToExecute].join(' ') - : [...argsPrefix, commandToExecute]; + const { executable, argsPrefix, shell } = getShellConfiguration(); + // On Windows with cmd.exe, pass args as a single string instead of + // an array. node-pty's argsToCommandLine re-quotes array elements + // that contain spaces, which mangles user-provided quoted arguments + // for cmd.exe (e.g., `type "hello world"` becomes + // `"type \"hello world\""`). + // + // For PowerShell, keep the array form: argsToCommandLine escapes for + // CommandLineToArgvW round-tripping, which .NET correctly parses. + // The string form breaks quoted paths ending in \ (e.g., "C:\Temp\") + // because CommandLineToArgvW treats \" as an escaped quote. + const args: string[] | string = + os.platform() === 'win32' && shell === 'cmd' + ? [...argsPrefix, commandToExecute].join(' ') + : [...argsPrefix, commandToExecute]; const ptyProcess = ptyInfo.module.spawn(executable, args, { cwd,