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
This commit is contained in:
hannesrudolph 2025-07-01 09:25:40 -06:00
parent db8c60ceff
commit 82c3e927b8
24 changed files with 138 additions and 90 deletions

23
pnpm-lock.yaml generated
View file

@ -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:

View file

@ -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
*/

View file

@ -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",

View file

@ -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(" ")
}

View file

@ -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",

View file

@ -313,7 +313,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
setClineAsk("command")
setEnableButtons(!isPartial)
setPrimaryButtonText(t("chat:runCommand.title"))
setSecondaryButtonText("Add & Run")
setSecondaryButtonText(t("chat:addAndRun.title"))
setTertiaryButtonText(t("chat:reject.title"))
break
case "command_output":

View file

@ -46,6 +46,10 @@
"title": "Desar",
"tooltip": "Desa els canvis del fitxer"
},
"addAndRun": {
"title": "Add & Run",
"tooltip": "Add command to whitelist and run it"
},
"reject": {
"title": "Rebutjar",
"tooltip": "Rebutja aquesta acció"

View file

@ -46,6 +46,10 @@
"title": "Speichern",
"tooltip": "Dateiänderungen speichern"
},
"addAndRun": {
"title": "Add & Run",
"tooltip": "Add command to whitelist and run it"
},
"reject": {
"title": "Ablehnen",
"tooltip": "Diese Aktion ablehnen"

View file

@ -46,6 +46,10 @@
"tokensUsed": "Tokens used: {{used}} of {{total}}",
"reservedForResponse": "Reserved for model response: {{amount}} tokens"
},
"addAndRun": {
"title": "Add & Run",
"tooltip": "Add command to whitelist and run it"
},
"reject": {
"title": "Reject",
"tooltip": "Reject this action"

View file

@ -46,6 +46,10 @@
"tokensUsed": "Tokens utilizados: {{used}} de {{total}}",
"reservedForResponse": "Reservado para respuesta del modelo: {{amount}} tokens"
},
"addAndRun": {
"title": "Add & Run",
"tooltip": "Add command to whitelist and run it"
},
"reject": {
"title": "Rechazar",
"tooltip": "Rechazar esta acción"

View file

@ -46,6 +46,10 @@
"title": "Enregistrer",
"tooltip": "Sauvegarder les modifications du fichier"
},
"addAndRun": {
"title": "Add & Run",
"tooltip": "Add command to whitelist and run it"
},
"reject": {
"title": "Rejeter",
"tooltip": "Rejeter cette action"

View file

@ -46,6 +46,10 @@
"title": "सहेजें",
"tooltip": "फ़ाइल परिवर्तन सहेजें"
},
"addAndRun": {
"title": "Add & Run",
"tooltip": "Add command to whitelist and run it"
},
"reject": {
"title": "अस्वीकार करें",
"tooltip": "इस क्रिया को अस्वीकार करें"

View file

@ -52,6 +52,10 @@
"tokensUsed": "Token digunakan: {{used}} dari {{total}}",
"reservedForResponse": "Dicadangkan untuk respons model: {{amount}} token"
},
"addAndRun": {
"title": "Add & Run",
"tooltip": "Add command to whitelist and run it"
},
"reject": {
"title": "Tolak",
"tooltip": "Tolak aksi ini"

View file

@ -46,6 +46,10 @@
"title": "Salva",
"tooltip": "Salva le modifiche al file"
},
"addAndRun": {
"title": "Add & Run",
"tooltip": "Add command to whitelist and run it"
},
"reject": {
"title": "Rifiuta",
"tooltip": "Rifiuta questa azione"

View file

@ -46,6 +46,10 @@
"title": "保存",
"tooltip": "ファイル変更を保存"
},
"addAndRun": {
"title": "Add & Run",
"tooltip": "Add command to whitelist and run it"
},
"reject": {
"title": "拒否",
"tooltip": "このアクションを拒否"

View file

@ -46,6 +46,10 @@
"title": "저장",
"tooltip": "파일 변경사항 저장"
},
"addAndRun": {
"title": "Add & Run",
"tooltip": "Add command to whitelist and run it"
},
"reject": {
"title": "거부",
"tooltip": "이 작업 거부"

View file

@ -46,6 +46,10 @@
"tokensUsed": "Gebruikte tokens: {{used}} van {{total}}",
"reservedForResponse": "Gereserveerd voor modelantwoord: {{amount}} tokens"
},
"addAndRun": {
"title": "Add & Run",
"tooltip": "Add command to whitelist and run it"
},
"reject": {
"title": "Weigeren",
"tooltip": "Deze actie weigeren"

View file

@ -46,6 +46,10 @@
"title": "Zapisz",
"tooltip": "Zapisz zmiany w pliku"
},
"addAndRun": {
"title": "Add & Run",
"tooltip": "Add command to whitelist and run it"
},
"reject": {
"title": "Odrzuć",
"tooltip": "Odrzuć tę akcję"

View file

@ -46,6 +46,10 @@
"title": "Salvar",
"tooltip": "Salvar as alterações do arquivo"
},
"addAndRun": {
"title": "Add & Run",
"tooltip": "Add command to whitelist and run it"
},
"reject": {
"title": "Rejeitar",
"tooltip": "Rejeitar esta ação"

View file

@ -46,6 +46,10 @@
"tokensUsed": "Использовано токенов: {{used}} из {{total}}",
"reservedForResponse": "Зарезервировано для ответа модели: {{amount}} токенов"
},
"addAndRun": {
"title": "Add & Run",
"tooltip": "Add command to whitelist and run it"
},
"reject": {
"title": "Отклонить",
"tooltip": "Отклонить это действие"

View file

@ -46,6 +46,10 @@
"title": "Kaydet",
"tooltip": "Dosya değişikliklerini kaydet"
},
"addAndRun": {
"title": "Add & Run",
"tooltip": "Add command to whitelist and run it"
},
"reject": {
"title": "Reddet",
"tooltip": "Bu eylemi reddet"

View file

@ -46,6 +46,10 @@
"title": "Lưu",
"tooltip": "Lưu các thay đổi tệp"
},
"addAndRun": {
"title": "Add & Run",
"tooltip": "Add command to whitelist and run it"
},
"reject": {
"title": "Từ chối",
"tooltip": "Từ chối hành động này"

View file

@ -46,6 +46,10 @@
"title": "保存",
"tooltip": "保存文件更改"
},
"addAndRun": {
"title": "Add & Run",
"tooltip": "Add command to whitelist and run it"
},
"reject": {
"title": "拒绝",
"tooltip": "拒绝此操作"

View file

@ -46,6 +46,10 @@
"title": "儲存",
"tooltip": "儲存檔案變更"
},
"addAndRun": {
"title": "Add & Run",
"tooltip": "Add command to whitelist and run it"
},
"reject": {
"title": "拒絕",
"tooltip": "拒絕此操作"