Skip to content

Commit 82c3e92

Browse files
committed
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
1 parent db8c60c commit 82c3e92

File tree

24 files changed

+138
-90
lines changed

24 files changed

+138
-90
lines changed

pnpm-lock.yaml

Lines changed: 11 additions & 12 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/core/tools/executeCommandTool.ts

Lines changed: 1 addition & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -15,85 +15,10 @@ import { ExitCodeDetails, RooTerminalCallbacks, RooTerminalProcess } from "../..
1515
import { TerminalRegistry } from "../../integrations/terminal/TerminalRegistry"
1616
import { Terminal } from "../../integrations/terminal/Terminal"
1717
import { t } from "../../i18n"
18+
import { extractCommandPattern } from "../../shared/extract-command-pattern"
1819

1920
class ShellIntegrationError extends Error {}
2021

21-
/**
22-
* Extract the base command pattern from a full command string.
23-
* For example: "gh pr checkout 1234" -> "gh pr checkout"
24-
*
25-
* @param command The full command string
26-
* @returns The base command pattern suitable for whitelisting
27-
*/
28-
function extractCommandPattern(command: string): string {
29-
if (!command?.trim()) return ""
30-
31-
// Split by whitespace, handling quoted strings
32-
const parts: string[] = []
33-
let current = ""
34-
let inQuotes = false
35-
let quoteChar = ""
36-
37-
for (let i = 0; i < command.length; i++) {
38-
const char = command[i]
39-
const prevChar = i > 0 ? command[i - 1] : ""
40-
41-
if ((char === '"' || char === "'") && prevChar !== "\\") {
42-
if (!inQuotes) {
43-
inQuotes = true
44-
quoteChar = char
45-
} else if (char === quoteChar) {
46-
inQuotes = false
47-
quoteChar = ""
48-
}
49-
current += char
50-
} else if (char === " " && !inQuotes) {
51-
if (current) {
52-
parts.push(current)
53-
current = ""
54-
}
55-
} else {
56-
current += char
57-
}
58-
}
59-
60-
if (current) {
61-
parts.push(current)
62-
}
63-
64-
// Extract pattern parts, stopping at arguments
65-
const patternParts: string[] = []
66-
67-
for (const part of parts) {
68-
// Remove quotes for analysis
69-
const unquoted = part.replace(/^["']|["']$/g, "")
70-
71-
// Stop at common argument patterns:
72-
// - Pure numbers (like PR numbers, PIDs, etc.)
73-
// - Flags starting with - or --
74-
// - File paths or URLs
75-
// - Variable assignments (KEY=VALUE)
76-
// - Operators (&&, ||, |, ;, >, <, etc.)
77-
if (
78-
/^\d+$/.test(unquoted) ||
79-
unquoted.startsWith("-") ||
80-
unquoted.includes("/") ||
81-
unquoted.includes("\\") ||
82-
unquoted.includes("=") ||
83-
unquoted.startsWith("http") ||
84-
unquoted.includes(".") ||
85-
["&&", "||", "|", ";", ">", "<", ">>", "<<", "&"].includes(unquoted)
86-
) {
87-
// Stop collecting pattern parts
88-
break
89-
}
90-
patternParts.push(part)
91-
}
92-
93-
// Return the base command pattern
94-
return patternParts.join(" ")
95-
}
96-
9722
/**
9823
* Adds a command pattern to the whitelist
9924
*/

src/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,7 @@
407407
"sanitize-filename": "^1.6.3",
408408
"say": "^0.16.0",
409409
"serialize-error": "^12.0.0",
410+
"shell-quote": "1.8.3",
410411
"simple-git": "^3.27.0",
411412
"sound-play": "^1.1.0",
412413
"stream-json": "^1.8.0",
@@ -439,6 +440,7 @@
439440
"@types/node-ipc": "^9.2.3",
440441
"@types/proper-lockfile": "^4.1.4",
441442
"@types/ps-tree": "^1.1.6",
443+
"@types/shell-quote": "^1.7.5",
442444
"@types/stream-json": "^1.7.8",
443445
"@types/string-similarity": "^4.0.2",
444446
"@types/tmp": "^0.2.6",
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { parse } from "shell-quote"
2+
3+
type ShellToken = string | { op: string } | { command: string }
4+
5+
/**
6+
* Extract the base command pattern from a full command string.
7+
* For example: "gh pr checkout 1234" -> "gh pr checkout"
8+
*
9+
* Uses shell-quote v1.8.3 for proper shell parsing.
10+
*
11+
* @param command The full command string
12+
* @returns The base command pattern suitable for whitelisting
13+
*/
14+
export function extractCommandPattern(command: string): string {
15+
if (!command?.trim()) return ""
16+
17+
// Parse the command to get tokens
18+
const tokens = parse(command.trim()) as ShellToken[]
19+
const patternParts: string[] = []
20+
21+
for (const token of tokens) {
22+
if (typeof token === "string") {
23+
// Check if this token looks like an argument (number, flag, etc.)
24+
// Common patterns to stop at:
25+
// - Pure numbers (like PR numbers, PIDs, etc.)
26+
// - Flags starting with - or --
27+
// - File paths or URLs
28+
// - Variable assignments (KEY=VALUE)
29+
if (
30+
/^\d+$/.test(token) ||
31+
token.startsWith("-") ||
32+
token.includes("/") ||
33+
token.includes("\\") ||
34+
token.includes("=") ||
35+
token.startsWith("http") ||
36+
token.includes(".")
37+
) {
38+
// Stop collecting pattern parts
39+
break
40+
}
41+
patternParts.push(token)
42+
} else if (typeof token === "object" && "op" in token) {
43+
// Stop at operators
44+
break
45+
}
46+
}
47+
48+
// Return the base command pattern
49+
return patternParts.join(" ")
50+
}

webview-ui/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@
6565
"remark-gfm": "^4.0.1",
6666
"remark-math": "^6.0.0",
6767
"remove-markdown": "^0.6.0",
68-
"shell-quote": "^1.8.2",
68+
"shell-quote": "^1.8.3",
6969
"shiki": "^3.2.1",
7070
"source-map": "^0.7.4",
7171
"styled-components": "^6.1.13",

webview-ui/src/components/chat/ChatView.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -313,7 +313,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
313313
setClineAsk("command")
314314
setEnableButtons(!isPartial)
315315
setPrimaryButtonText(t("chat:runCommand.title"))
316-
setSecondaryButtonText("Add & Run")
316+
setSecondaryButtonText(t("chat:addAndRun.title"))
317317
setTertiaryButtonText(t("chat:reject.title"))
318318
break
319319
case "command_output":

webview-ui/src/i18n/locales/ca/chat.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@
4646
"title": "Desar",
4747
"tooltip": "Desa els canvis del fitxer"
4848
},
49+
"addAndRun": {
50+
"title": "Add & Run",
51+
"tooltip": "Add command to whitelist and run it"
52+
},
4953
"reject": {
5054
"title": "Rebutjar",
5155
"tooltip": "Rebutja aquesta acció"

webview-ui/src/i18n/locales/de/chat.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@
4646
"title": "Speichern",
4747
"tooltip": "Dateiänderungen speichern"
4848
},
49+
"addAndRun": {
50+
"title": "Add & Run",
51+
"tooltip": "Add command to whitelist and run it"
52+
},
4953
"reject": {
5054
"title": "Ablehnen",
5155
"tooltip": "Diese Aktion ablehnen"

webview-ui/src/i18n/locales/en/chat.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@
4646
"tokensUsed": "Tokens used: {{used}} of {{total}}",
4747
"reservedForResponse": "Reserved for model response: {{amount}} tokens"
4848
},
49+
"addAndRun": {
50+
"title": "Add & Run",
51+
"tooltip": "Add command to whitelist and run it"
52+
},
4953
"reject": {
5054
"title": "Reject",
5155
"tooltip": "Reject this action"

webview-ui/src/i18n/locales/es/chat.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@
4646
"tokensUsed": "Tokens utilizados: {{used}} de {{total}}",
4747
"reservedForResponse": "Reservado para respuesta del modelo: {{amount}} tokens"
4848
},
49+
"addAndRun": {
50+
"title": "Add & Run",
51+
"tooltip": "Add command to whitelist and run it"
52+
},
4953
"reject": {
5054
"title": "Rechazar",
5155
"tooltip": "Rechazar esta acción"

0 commit comments

Comments
 (0)