From 82c3e927b8d600e58953de820ab53c2d2be56948 Mon Sep 17 00:00:00 2001 From: hannesrudolph Date: Tue, 1 Jul 2025 09:25:40 -0600 Subject: [PATCH] fix: Update shell-quote to v1.8.3 and use it for command pattern extraction - Updated shell-quote from v1.8.2 to v1.8.3 in webview-ui - Added shell-quote v1.8.3 to backend dependencies - Updated shared extract-command-pattern module to use shell-quote for proper parsing - Added @types/shell-quote for TypeScript support - Ensures consistent command pattern extraction across frontend and backend --- pnpm-lock.yaml | 23 +++--- src/core/tools/executeCommandTool.ts | 77 +-------------------- src/package.json | 2 + src/shared/extract-command-pattern.ts | 50 +++++++++++++ webview-ui/package.json | 2 +- webview-ui/src/components/chat/ChatView.tsx | 2 +- webview-ui/src/i18n/locales/ca/chat.json | 4 ++ webview-ui/src/i18n/locales/de/chat.json | 4 ++ webview-ui/src/i18n/locales/en/chat.json | 4 ++ webview-ui/src/i18n/locales/es/chat.json | 4 ++ webview-ui/src/i18n/locales/fr/chat.json | 4 ++ webview-ui/src/i18n/locales/hi/chat.json | 4 ++ webview-ui/src/i18n/locales/id/chat.json | 4 ++ webview-ui/src/i18n/locales/it/chat.json | 4 ++ webview-ui/src/i18n/locales/ja/chat.json | 4 ++ webview-ui/src/i18n/locales/ko/chat.json | 4 ++ webview-ui/src/i18n/locales/nl/chat.json | 4 ++ webview-ui/src/i18n/locales/pl/chat.json | 4 ++ webview-ui/src/i18n/locales/pt-BR/chat.json | 4 ++ webview-ui/src/i18n/locales/ru/chat.json | 4 ++ webview-ui/src/i18n/locales/tr/chat.json | 4 ++ webview-ui/src/i18n/locales/vi/chat.json | 4 ++ webview-ui/src/i18n/locales/zh-CN/chat.json | 4 ++ webview-ui/src/i18n/locales/zh-TW/chat.json | 4 ++ 24 files changed, 138 insertions(+), 90 deletions(-) create mode 100644 src/shared/extract-command-pattern.ts diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5030055fea..b6569d4113 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -723,6 +723,9 @@ importers: serialize-error: specifier: ^12.0.0 version: 12.0.0 + shell-quote: + specifier: 1.8.3 + version: 1.8.3 simple-git: specifier: ^3.27.0 version: 3.27.0 @@ -814,6 +817,9 @@ importers: '@types/ps-tree': specifier: ^1.1.6 version: 1.1.6 + '@types/shell-quote': + specifier: ^1.7.5 + version: 1.7.5 '@types/stream-json': specifier: ^1.7.8 version: 1.7.8 @@ -1028,8 +1034,8 @@ importers: specifier: ^0.6.0 version: 0.6.2 shell-quote: - specifier: ^1.8.2 - version: 1.8.2 + specifier: ^1.8.3 + version: 1.8.3 shiki: specifier: ^3.2.1 version: 3.4.1 @@ -8478,10 +8484,6 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} - shell-quote@1.8.2: - resolution: {integrity: sha512-AzqKpGKjrj7EM6rKVQEPpB288oCfnrEIuyoT9cyF4nmGa7V8Zk6f7RRqYisX8X9m+Q7bd632aZW4ky7EhbQztA==} - engines: {node: '>= 0.4'} - shell-quote@1.8.3: resolution: {integrity: sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==} engines: {node: '>= 0.4'} @@ -13295,7 +13297,7 @@ snapshots: sirv: 3.0.1 tinyglobby: 0.2.14 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/debug@4.1.12)(@types/node@22.15.29)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0) + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@20.17.50)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0) '@vitest/utils@3.2.4': dependencies: @@ -17293,7 +17295,7 @@ snapshots: minimatch: 10.0.1 pidtree: 0.6.0 read-package-json-fast: 4.0.0 - shell-quote: 1.8.2 + shell-quote: 1.8.3 which: 5.0.0 npm-run-path@4.0.1: @@ -18522,10 +18524,7 @@ snapshots: shebang-regex@3.0.0: {} - shell-quote@1.8.2: {} - - shell-quote@1.8.3: - optional: true + shell-quote@1.8.3: {} shiki@3.4.1: dependencies: diff --git a/src/core/tools/executeCommandTool.ts b/src/core/tools/executeCommandTool.ts index 830dbb8018..80a66834d2 100644 --- a/src/core/tools/executeCommandTool.ts +++ b/src/core/tools/executeCommandTool.ts @@ -15,85 +15,10 @@ import { ExitCodeDetails, RooTerminalCallbacks, RooTerminalProcess } from "../.. import { TerminalRegistry } from "../../integrations/terminal/TerminalRegistry" import { Terminal } from "../../integrations/terminal/Terminal" import { t } from "../../i18n" +import { extractCommandPattern } from "../../shared/extract-command-pattern" class ShellIntegrationError extends Error {} -/** - * Extract the base command pattern from a full command string. - * For example: "gh pr checkout 1234" -> "gh pr checkout" - * - * @param command The full command string - * @returns The base command pattern suitable for whitelisting - */ -function extractCommandPattern(command: string): string { - if (!command?.trim()) return "" - - // Split by whitespace, handling quoted strings - const parts: string[] = [] - let current = "" - let inQuotes = false - let quoteChar = "" - - for (let i = 0; i < command.length; i++) { - const char = command[i] - const prevChar = i > 0 ? command[i - 1] : "" - - if ((char === '"' || char === "'") && prevChar !== "\\") { - if (!inQuotes) { - inQuotes = true - quoteChar = char - } else if (char === quoteChar) { - inQuotes = false - quoteChar = "" - } - current += char - } else if (char === " " && !inQuotes) { - if (current) { - parts.push(current) - current = "" - } - } else { - current += char - } - } - - if (current) { - parts.push(current) - } - - // Extract pattern parts, stopping at arguments - const patternParts: string[] = [] - - for (const part of parts) { - // Remove quotes for analysis - const unquoted = part.replace(/^["']|["']$/g, "") - - // Stop at common argument patterns: - // - Pure numbers (like PR numbers, PIDs, etc.) - // - Flags starting with - or -- - // - File paths or URLs - // - Variable assignments (KEY=VALUE) - // - Operators (&&, ||, |, ;, >, <, etc.) - if ( - /^\d+$/.test(unquoted) || - unquoted.startsWith("-") || - unquoted.includes("/") || - unquoted.includes("\\") || - unquoted.includes("=") || - unquoted.startsWith("http") || - unquoted.includes(".") || - ["&&", "||", "|", ";", ">", "<", ">>", "<<", "&"].includes(unquoted) - ) { - // Stop collecting pattern parts - break - } - patternParts.push(part) - } - - // Return the base command pattern - return patternParts.join(" ") -} - /** * Adds a command pattern to the whitelist */ diff --git a/src/package.json b/src/package.json index 6412b4b858..79263d8b75 100644 --- a/src/package.json +++ b/src/package.json @@ -407,6 +407,7 @@ "sanitize-filename": "^1.6.3", "say": "^0.16.0", "serialize-error": "^12.0.0", + "shell-quote": "1.8.3", "simple-git": "^3.27.0", "sound-play": "^1.1.0", "stream-json": "^1.8.0", @@ -439,6 +440,7 @@ "@types/node-ipc": "^9.2.3", "@types/proper-lockfile": "^4.1.4", "@types/ps-tree": "^1.1.6", + "@types/shell-quote": "^1.7.5", "@types/stream-json": "^1.7.8", "@types/string-similarity": "^4.0.2", "@types/tmp": "^0.2.6", diff --git a/src/shared/extract-command-pattern.ts b/src/shared/extract-command-pattern.ts new file mode 100644 index 0000000000..6bf16d1d3a --- /dev/null +++ b/src/shared/extract-command-pattern.ts @@ -0,0 +1,50 @@ +import { parse } from "shell-quote" + +type ShellToken = string | { op: string } | { command: string } + +/** + * Extract the base command pattern from a full command string. + * For example: "gh pr checkout 1234" -> "gh pr checkout" + * + * Uses shell-quote v1.8.3 for proper shell parsing. + * + * @param command The full command string + * @returns The base command pattern suitable for whitelisting + */ +export function extractCommandPattern(command: string): string { + if (!command?.trim()) return "" + + // Parse the command to get tokens + const tokens = parse(command.trim()) as ShellToken[] + const patternParts: string[] = [] + + for (const token of tokens) { + if (typeof token === "string") { + // Check if this token looks like an argument (number, flag, etc.) + // Common patterns to stop at: + // - Pure numbers (like PR numbers, PIDs, etc.) + // - Flags starting with - or -- + // - File paths or URLs + // - Variable assignments (KEY=VALUE) + if ( + /^\d+$/.test(token) || + token.startsWith("-") || + token.includes("/") || + token.includes("\\") || + token.includes("=") || + token.startsWith("http") || + token.includes(".") + ) { + // Stop collecting pattern parts + break + } + patternParts.push(token) + } else if (typeof token === "object" && "op" in token) { + // Stop at operators + break + } + } + + // Return the base command pattern + return patternParts.join(" ") +} diff --git a/webview-ui/package.json b/webview-ui/package.json index 4c6edc7a2b..86dd21525d 100644 --- a/webview-ui/package.json +++ b/webview-ui/package.json @@ -65,7 +65,7 @@ "remark-gfm": "^4.0.1", "remark-math": "^6.0.0", "remove-markdown": "^0.6.0", - "shell-quote": "^1.8.2", + "shell-quote": "^1.8.3", "shiki": "^3.2.1", "source-map": "^0.7.4", "styled-components": "^6.1.13", diff --git a/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx index 5ddce925ca..dc8c3c7a9f 100644 --- a/webview-ui/src/components/chat/ChatView.tsx +++ b/webview-ui/src/components/chat/ChatView.tsx @@ -313,7 +313,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction