import fs from "fs/promises" import * as path from "path" import delay from "delay" import { Cline } from "../Cline" import { ToolUse, AskApproval, HandleError, PushToolResult, RemoveClosingTag, ToolResponse } from "../../shared/tools" import { formatResponse } from "../prompts/responses" import { unescapeHtmlEntities } from "../../utils/text-normalization" import { ExitCodeDetails, TerminalProcess } from "../../integrations/terminal/TerminalProcess" import { Terminal } from "../../integrations/terminal/Terminal" import { TerminalRegistry } from "../../integrations/terminal/TerminalRegistry" import { telemetryService } from "../../services/telemetry/TelemetryService" export async function executeCommandTool( cline: Cline, block: ToolUse, askApproval: AskApproval, handleError: HandleError, pushToolResult: PushToolResult, removeClosingTag: RemoveClosingTag, ) { let command: string | undefined = block.params.command const customCwd: string | undefined = block.params.cwd try { if (block.partial) { await cline.ask("command", removeClosingTag("command", command), block.partial).catch(() => {}) return } else { if (!command) { cline.consecutiveMistakeCount++ cline.recordToolError("execute_command") pushToolResult(await cline.sayAndCreateMissingParamError("execute_command", "command")) return } const ignoredFileAttemptedToAccess = cline.rooIgnoreController?.validateCommand(command) if (ignoredFileAttemptedToAccess) { await cline.say("rooignore_error", ignoredFileAttemptedToAccess) pushToolResult(formatResponse.toolError(formatResponse.rooIgnoreError(ignoredFileAttemptedToAccess))) return } cline.consecutiveMistakeCount = 0 command = unescapeHtmlEntities(command) // Unescape HTML entities. const didApprove = await askApproval("command", command) if (!didApprove) { return } const [userRejected, result] = await executeCommand(cline, command, customCwd) if (userRejected) { cline.didRejectTool = true } pushToolResult(result) return } } catch (error) { await handleError("executing command", error) return } } export async function executeCommand( cline: Cline, command: string, customCwd?: string, ): Promise<[boolean, ToolResponse]> { let workingDir: string if (!customCwd) { workingDir = cline.cwd } else if (path.isAbsolute(customCwd)) { workingDir = customCwd } else { workingDir = path.resolve(cline.cwd, customCwd) } // Check if directory exists try { await fs.access(workingDir) } catch (error) { return [false, `Working directory '${workingDir}' does not exist.`] } const terminalInfo = await TerminalRegistry.getOrCreateTerminal(workingDir, !!customCwd, cline.taskId) // Update the working directory in case the terminal we asked for has // a different working directory so that the model will know where the // command actually executed: workingDir = terminalInfo.getCurrentWorkingDirectory() const workingDirInfo = workingDir ? ` from '${workingDir.toPosix()}'` : "" terminalInfo.terminal.show() // weird visual bug when creating new terminals (even manually) where there's an empty space at the top. let userFeedback: { text?: string; images?: string[] } | undefined let didContinue = false let completed = false let result: string = "" let exitDetails: ExitCodeDetails | undefined const { terminalOutputLineLimit = 500 } = (await cline.providerRef.deref()?.getState()) ?? {} const sendCommandOutput = async (line: string, terminalProcess: TerminalProcess): Promise => { try { const { response, text, images } = await cline.ask("command_output", line) if (response === "yesButtonClicked") { // proceed while running } else { userFeedback = { text, images } } didContinue = true terminalProcess.continue() // continue past the await } catch { // This can only happen if this ask promise was ignored, so ignore this error } } const process = terminalInfo.runCommand(command, { onLine: (line, process) => { if (!didContinue) { sendCommandOutput(Terminal.compressTerminalOutput(line, terminalOutputLineLimit), process) } else { cline.say("command_output", Terminal.compressTerminalOutput(line, terminalOutputLineLimit)) } }, onCompleted: (output) => { result = output ?? "" completed = true }, onShellExecutionComplete: (details) => { exitDetails = details }, onNoShellIntegration: async (message) => { telemetryService.captureShellIntegrationError(cline.taskId) await cline.say("shell_integration_warning", message) }, }) await process // Wait for a short delay to ensure all messages are sent to the webview // This delay allows time for non-awaited promises to be created and // for their associated messages to be sent to the webview, maintaining // the correct order of messages (although the webview is smart about // grouping command_output messages despite any gaps anyways) await delay(50) result = Terminal.compressTerminalOutput(result, terminalOutputLineLimit) // keep in case we need it to troubleshoot user issues, but this should be removed in the future // if everything looks good: console.debug( "[execute_command status]", JSON.stringify( { completed, userFeedback, hasResult: result.length > 0, exitDetails, terminalId: terminalInfo.id, workingDir: workingDirInfo, isTerminalBusy: terminalInfo.busy, }, null, 2, ), ) if (userFeedback) { await cline.say("user_feedback", userFeedback.text, userFeedback.images) return [ true, formatResponse.toolResult( `Command is still running in terminal ${terminalInfo.id}${workingDirInfo}.${ result.length > 0 ? `\nHere's the output so far:\n${result}` : "" }\n\nThe user provided the following feedback:\n\n${userFeedback.text}\n`, userFeedback.images, ), ] } else if (completed) { let exitStatus: string = "" if (exitDetails !== undefined) { if (exitDetails.signal) { exitStatus = `Process terminated by signal ${exitDetails.signal} (${exitDetails.signalName})` if (exitDetails.coreDumpPossible) { exitStatus += " - core dump possible" } } else if (exitDetails.exitCode === undefined) { result += "" exitStatus = `Exit code: ` } else { if (exitDetails.exitCode !== 0) { exitStatus += "Command execution was not successful, inspect the cause and adjust as needed.\n" } exitStatus += `Exit code: ${exitDetails.exitCode}` } } else { result += "" exitStatus = `Exit code: ` } let workingDirInfo: string = workingDir ? ` within working directory '${workingDir.toPosix()}'` : "" const newWorkingDir = terminalInfo.getCurrentWorkingDirectory() if (newWorkingDir !== workingDir) { workingDirInfo += `\nNOTICE: Your command changed the working directory for this terminal to '${newWorkingDir.toPosix()}' so you MUST adjust future commands accordingly because they will be executed in this directory` } const outputInfo = `\nOutput:\n${result}` return [false, `Command executed in terminal ${terminalInfo.id}${workingDirInfo}. ${exitStatus}${outputInfo}`] } else { return [ false, `Command is still running in terminal ${terminalInfo.id}${workingDirInfo}.${ result.length > 0 ? `\nHere's the output so far:\n${result}` : "" }\n\nYou will be updated on the terminal status and new output in the future.`, ] } }