mirror of
https://github.com/pacnpal/Roo-Code.git
synced 2025-12-21 12:51:17 -05:00
122 lines
3.7 KiB
TypeScript
122 lines
3.7 KiB
TypeScript
import { parse } from "shell-quote"
|
|
|
|
type ShellToken = string | { op: string } | { command: string }
|
|
|
|
/**
|
|
* Split a command string into individual sub-commands by
|
|
* chaining operators (&&, ||, ;, or |).
|
|
*
|
|
* Uses shell-quote to properly handle:
|
|
* - Quoted strings (preserves quotes)
|
|
* - Subshell commands ($(cmd) or `cmd`)
|
|
* - PowerShell redirections (2>&1)
|
|
* - Chain operators (&&, ||, ;, |)
|
|
*/
|
|
export function parseCommand(command: string): string[] {
|
|
if (!command?.trim()) return []
|
|
|
|
// First handle PowerShell redirections by temporarily replacing them
|
|
const redirections: string[] = []
|
|
let processedCommand = command.replace(/\d*>&\d*/g, (match) => {
|
|
redirections.push(match)
|
|
return `__REDIR_${redirections.length - 1}__`
|
|
})
|
|
|
|
// Then handle subshell commands
|
|
const subshells: string[] = []
|
|
processedCommand = processedCommand
|
|
.replace(/\$\((.*?)\)/g, (_, inner) => {
|
|
subshells.push(inner.trim())
|
|
return `__SUBSH_${subshells.length - 1}__`
|
|
})
|
|
.replace(/`(.*?)`/g, (_, inner) => {
|
|
subshells.push(inner.trim())
|
|
return `__SUBSH_${subshells.length - 1}__`
|
|
})
|
|
|
|
// Then handle quoted strings
|
|
const quotes: string[] = []
|
|
processedCommand = processedCommand.replace(/"[^"]*"/g, (match) => {
|
|
quotes.push(match)
|
|
return `__QUOTE_${quotes.length - 1}__`
|
|
})
|
|
|
|
const tokens = parse(processedCommand) as ShellToken[]
|
|
const commands: string[] = []
|
|
let currentCommand: string[] = []
|
|
|
|
for (const token of tokens) {
|
|
if (typeof token === "object" && "op" in token) {
|
|
// Chain operator - split command
|
|
if (["&&", "||", ";", "|"].includes(token.op)) {
|
|
if (currentCommand.length > 0) {
|
|
commands.push(currentCommand.join(" "))
|
|
currentCommand = []
|
|
}
|
|
} else {
|
|
// Other operators (>, &) are part of the command
|
|
currentCommand.push(token.op)
|
|
}
|
|
} else if (typeof token === "string") {
|
|
// Check if it's a subshell placeholder
|
|
const subshellMatch = token.match(/__SUBSH_(\d+)__/)
|
|
if (subshellMatch) {
|
|
if (currentCommand.length > 0) {
|
|
commands.push(currentCommand.join(" "))
|
|
currentCommand = []
|
|
}
|
|
commands.push(subshells[parseInt(subshellMatch[1])])
|
|
} else {
|
|
currentCommand.push(token)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add any remaining command
|
|
if (currentCommand.length > 0) {
|
|
commands.push(currentCommand.join(" "))
|
|
}
|
|
|
|
// Restore quotes and redirections
|
|
return commands.map((cmd) => {
|
|
let result = cmd
|
|
// Restore quotes
|
|
result = result.replace(/__QUOTE_(\d+)__/g, (_, i) => quotes[parseInt(i)])
|
|
// Restore redirections
|
|
result = result.replace(/__REDIR_(\d+)__/g, (_, i) => redirections[parseInt(i)])
|
|
return result
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Check if a single command is allowed based on prefix matching.
|
|
*/
|
|
export function isAllowedSingleCommand(command: string, allowedCommands: string[]): boolean {
|
|
if (!command || !allowedCommands?.length) return false
|
|
const trimmedCommand = command.trim().toLowerCase()
|
|
return allowedCommands.some((prefix) => trimmedCommand.startsWith(prefix.toLowerCase()))
|
|
}
|
|
|
|
/**
|
|
* Check if a command string is allowed based on the allowed command prefixes.
|
|
* This version also blocks subshell attempts by checking for `$(` or `` ` ``.
|
|
*/
|
|
export function validateCommand(command: string, allowedCommands: string[]): boolean {
|
|
if (!command?.trim()) return true
|
|
|
|
// Block subshell execution attempts
|
|
if (command.includes("$(") || command.includes("`")) {
|
|
return false
|
|
}
|
|
|
|
// Parse into sub-commands (split by &&, ||, ;, |)
|
|
const subCommands = parseCommand(command)
|
|
|
|
// Then ensure every sub-command starts with an allowed prefix
|
|
return subCommands.every((cmd) => {
|
|
// Remove simple PowerShell-like redirections (e.g. 2>&1) before checking
|
|
const cmdWithoutRedirection = cmd.replace(/\d*>&\d*/, "").trim()
|
|
return isAllowedSingleCommand(cmdWithoutRedirection, allowedCommands)
|
|
})
|
|
}
|