executeCommandTool.ts 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218
  1. import fs from "fs/promises"
  2. import * as path from "path"
  3. import delay from "delay"
  4. import { Cline } from "../Cline"
  5. import { ToolUse, AskApproval, HandleError, PushToolResult, RemoveClosingTag, ToolResponse } from "../../shared/tools"
  6. import { formatResponse } from "../prompts/responses"
  7. import { unescapeHtmlEntities } from "../../utils/text-normalization"
  8. import { telemetryService } from "../../services/telemetry/TelemetryService"
  9. import { ExitCodeDetails, RooTerminalProcess } from "../../integrations/terminal/types"
  10. import { TerminalRegistry } from "../../integrations/terminal/TerminalRegistry"
  11. import { Terminal } from "../../integrations/terminal/Terminal"
  12. export async function executeCommandTool(
  13. cline: Cline,
  14. block: ToolUse,
  15. askApproval: AskApproval,
  16. handleError: HandleError,
  17. pushToolResult: PushToolResult,
  18. removeClosingTag: RemoveClosingTag,
  19. ) {
  20. let command: string | undefined = block.params.command
  21. const customCwd: string | undefined = block.params.cwd
  22. try {
  23. if (block.partial) {
  24. await cline.ask("command", removeClosingTag("command", command), block.partial).catch(() => {})
  25. return
  26. } else {
  27. if (!command) {
  28. cline.consecutiveMistakeCount++
  29. cline.recordToolError("execute_command")
  30. pushToolResult(await cline.sayAndCreateMissingParamError("execute_command", "command"))
  31. return
  32. }
  33. const ignoredFileAttemptedToAccess = cline.rooIgnoreController?.validateCommand(command)
  34. if (ignoredFileAttemptedToAccess) {
  35. await cline.say("rooignore_error", ignoredFileAttemptedToAccess)
  36. pushToolResult(formatResponse.toolError(formatResponse.rooIgnoreError(ignoredFileAttemptedToAccess)))
  37. return
  38. }
  39. cline.consecutiveMistakeCount = 0
  40. command = unescapeHtmlEntities(command) // Unescape HTML entities.
  41. const didApprove = await askApproval("command", command)
  42. if (!didApprove) {
  43. return
  44. }
  45. const [userRejected, result] = await executeCommand(cline, command, customCwd)
  46. if (userRejected) {
  47. cline.didRejectTool = true
  48. }
  49. pushToolResult(result)
  50. return
  51. }
  52. } catch (error) {
  53. await handleError("executing command", error)
  54. return
  55. }
  56. }
  57. export async function executeCommand(
  58. cline: Cline,
  59. command: string,
  60. customCwd?: string,
  61. ): Promise<[boolean, ToolResponse]> {
  62. let workingDir: string
  63. if (!customCwd) {
  64. workingDir = cline.cwd
  65. } else if (path.isAbsolute(customCwd)) {
  66. workingDir = customCwd
  67. } else {
  68. workingDir = path.resolve(cline.cwd, customCwd)
  69. }
  70. try {
  71. await fs.access(workingDir)
  72. } catch (error) {
  73. return [false, `Working directory '${workingDir}' does not exist.`]
  74. }
  75. let message: { text?: string; images?: string[] } | undefined
  76. let runInBackground = false
  77. let completed = false
  78. let result: string = ""
  79. let exitDetails: ExitCodeDetails | undefined
  80. const clineProvider = await cline.providerRef.deref()
  81. const clineProviderState = await clineProvider?.getState()
  82. const { terminalOutputLineLimit = 500, terminalShellIntegrationDisabled = false } = clineProviderState ?? {}
  83. const terminalProvider = terminalShellIntegrationDisabled ? "execa" : "vscode"
  84. const callbacks = {
  85. onLine: async (output: string, process: RooTerminalProcess) => {
  86. const compressed = Terminal.compressTerminalOutput(output, terminalOutputLineLimit)
  87. cline.say("command_output", compressed)
  88. if (runInBackground) {
  89. return
  90. }
  91. try {
  92. const { response, text, images } = await cline.ask("command_output", compressed)
  93. runInBackground = true
  94. if (response === "messageResponse") {
  95. message = { text, images }
  96. process.continue()
  97. }
  98. } catch (_error) {}
  99. },
  100. onCompleted: (output: string | undefined) => {
  101. result = Terminal.compressTerminalOutput(output ?? "", terminalOutputLineLimit)
  102. completed = true
  103. },
  104. onShellExecutionComplete: (details: ExitCodeDetails) => {
  105. exitDetails = details
  106. },
  107. onNoShellIntegration: async (message: string) => {
  108. telemetryService.captureShellIntegrationError(cline.taskId)
  109. await cline.say("shell_integration_warning", message)
  110. },
  111. }
  112. const terminal = await TerminalRegistry.getOrCreateTerminal(workingDir, !!customCwd, cline.taskId, terminalProvider)
  113. if (terminal instanceof Terminal) {
  114. terminal.terminal.show()
  115. // Update the working directory in case the terminal we asked for has
  116. // a different working directory so that the model will know where the
  117. // command actually executed.
  118. workingDir = terminal.getCurrentWorkingDirectory()
  119. }
  120. const process = terminal.runCommand(command, callbacks)
  121. cline.terminalProcess = process
  122. await process
  123. cline.terminalProcess = undefined
  124. // Wait for a short delay to ensure all messages are sent to the webview.
  125. // This delay allows time for non-awaited promises to be created and
  126. // for their associated messages to be sent to the webview, maintaining
  127. // the correct order of messages (although the webview is smart about
  128. // grouping command_output messages despite any gaps anyways).
  129. await delay(50)
  130. if (message) {
  131. const { text, images } = message
  132. await cline.say("user_feedback", text, images)
  133. return [
  134. true,
  135. formatResponse.toolResult(
  136. [
  137. `Command is still running in terminal from '${terminal.getCurrentWorkingDirectory().toPosix()}'.`,
  138. result.length > 0 ? `Here's the output so far:\n${result}\n` : "\n",
  139. `The user provided the following feedback:`,
  140. `<feedback>\n${text}\n</feedback>`,
  141. ].join("\n"),
  142. images,
  143. ),
  144. ]
  145. } else if (completed || exitDetails) {
  146. let exitStatus: string = ""
  147. if (exitDetails !== undefined) {
  148. if (exitDetails.signalName) {
  149. exitStatus = `Process terminated by signal ${exitDetails.signalName}`
  150. if (exitDetails.coreDumpPossible) {
  151. exitStatus += " - core dump possible"
  152. }
  153. } else if (exitDetails.exitCode === undefined) {
  154. result += "<VSCE exit code is undefined: terminal output and command execution status is unknown.>"
  155. exitStatus = `Exit code: <undefined, notify user>`
  156. } else {
  157. if (exitDetails.exitCode !== 0) {
  158. exitStatus += "Command execution was not successful, inspect the cause and adjust as needed.\n"
  159. }
  160. exitStatus += `Exit code: ${exitDetails.exitCode}`
  161. }
  162. } else {
  163. result += "<VSCE exitDetails == undefined: terminal output and command execution status is unknown.>"
  164. exitStatus = `Exit code: <undefined, notify user>`
  165. }
  166. let workingDirInfo = ` within working directory '${workingDir.toPosix()}'`
  167. const newWorkingDir = terminal.getCurrentWorkingDirectory()
  168. if (newWorkingDir !== workingDir) {
  169. 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`
  170. }
  171. return [false, `Command executed in terminal ${workingDirInfo}. ${exitStatus}\nOutput:\n${result}`]
  172. } else {
  173. return [
  174. false,
  175. [
  176. `Command is still running in terminal ${workingDir ? ` from '${workingDir.toPosix()}'` : ""}.`,
  177. result.length > 0 ? `Here's the output so far:\n${result}\n` : "\n",
  178. "You will be updated on the terminal status and new output in the future.",
  179. ].join("\n"),
  180. ]
  181. }
  182. }