diff --git a/packages/cli/src/ui/hooks/shellCommandProcessor.ts b/packages/cli/src/ui/hooks/shellCommandProcessor.ts index ae8367d6..0ec627ca 100644 --- a/packages/cli/src/ui/hooks/shellCommandProcessor.ts +++ b/packages/cli/src/ui/hooks/shellCommandProcessor.ts @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { exec as defaultExec } from 'child_process'; +import { spawn } from 'child_process'; import type { exec as ExecType } from 'child_process'; import { useCallback } from 'react'; import { Config } from '@gemini-code/server'; @@ -24,7 +24,7 @@ export const useShellCommandProcessor = ( onExec: (command: Promise) => void, onDebugMessage: (message: string) => void, config: Config, - executeCommand: typeof ExecType = defaultExec, // Injectable for testing + executeCommand?: typeof ExecType, // injectable for testing ) => { /** * Checks if the query is a shell command, executes it, and adds results to history. @@ -36,9 +36,8 @@ export const useShellCommandProcessor = ( return false; } - let commandToExecute = rawQuery.trim().trimStart(); - // wrap command to write pwd to temporary file + let commandToExecute = rawQuery.trim(); const pwdFileName = `shell_pwd_${crypto.randomBytes(6).toString('hex')}.tmp`; const pwdFilePath = path.join(os.tmpdir(), pwdFileName); if (!commandToExecute.endsWith('&')) commandToExecute += ';'; @@ -68,25 +67,79 @@ export const useShellCommandProcessor = ( }; const execPromise = new Promise((resolve) => { - executeCommand( - commandToExecute, - execOptions, - (error, stdout, stderr) => { - if (error) { - addItemToHistory( - { type: 'error', text: error.message }, - userMessageTimestamp, - ); - } else { - let output = ''; - if (stdout) output += stdout; - if (stderr) output += (output ? '\n' : '') + stderr; // Include stderr as info + if (executeCommand) { + executeCommand( + commandToExecute, + execOptions, + (error, stdout, stderr) => { + if (error) { + addItemToHistory( + { + type: 'error', + // remove wrapper from user's command in error message + text: error.message.replace(commandToExecute, rawQuery), + }, + userMessageTimestamp, + ); + } else { + let output = ''; + if (stdout) output += stdout; + if (stderr) output += (output ? '\n' : '') + stderr; // Include stderr as info + addItemToHistory( + { + type: 'info', + text: output || '(Command produced no output)', + }, + userMessageTimestamp, + ); + } + if (fs.existsSync(pwdFilePath)) { + const pwd = fs.readFileSync(pwdFilePath, 'utf8').trim(); + if (pwd !== targetDir) { + addItemToHistory( + { + type: 'info', + text: `WARNING: shell mode is stateless; \`cd ${pwd}\` will not apply to next command`, + }, + userMessageTimestamp, + ); + } + fs.unlinkSync(pwdFilePath); + } + resolve(); + }, + ); + } else { + const child = spawn('bash', ['-c', commandToExecute], { + cwd: targetDir, + stdio: ['ignore', 'pipe', 'pipe'], + }); + let output = ''; + child.stdout.on('data', (data) => { + output += data; + }); + child.stderr.on('data', (data) => { + output += data; + }); + let error: Error | null = null; + child.on('error', (err: Error) => { + error = err; + }); + child.on('close', (code, signal) => { + output = output.trim() || '(Command produced no output)'; + if (error) { + const text = `${error.message.replace(commandToExecute, rawQuery)}\n${output}`; + addItemToHistory({ type: 'error', text }, userMessageTimestamp); + } else if (code !== 0) { + const text = `Command exited with code ${code}\n${output}`; + addItemToHistory({ type: 'error', text }, userMessageTimestamp); + } else if (signal) { + const text = `Command terminated with signal ${signal}\n${output}`; + addItemToHistory({ type: 'error', text }, userMessageTimestamp); + } else { addItemToHistory( - { - type: 'info', - text: output || '(Command produced no output)', - }, + { type: 'info', text: output }, userMessageTimestamp, ); } @@ -104,8 +157,8 @@ export const useShellCommandProcessor = ( fs.unlinkSync(pwdFilePath); } resolve(); - }, - ); + }); + } }); try { diff --git a/packages/server/src/tools/shell.ts b/packages/server/src/tools/shell.ts index e130fb04..6cf49a7b 100644 --- a/packages/server/src/tools/shell.ts +++ b/packages/server/src/tools/shell.ts @@ -178,6 +178,8 @@ export class ShellTool extends BaseTool { let error: Error | null = null; shell.on('error', (err: Error) => { error = err; + // remove wrapper from user's command in error message + error.message = error.message.replace(command, params.command); }); let code: number | null = null;