use crate::tools::command_args::{is_readonly_command_string, raw_command_text};
use serde_json::Value;
const READONLY_UNIFIED_EXEC_COMMANDS: &[&str] = &[
"rg", "ls", "cat", "diff", "find", "wc", "grep", "egrep", "fgrep", "head", "tail", "sort", "uniq", "awk", "sed",
"cut", "tr", "ast-grep", "sg",
];
pub fn is_readonly_base_command(command: &str) -> bool {
READONLY_UNIFIED_EXEC_COMMANDS.contains(&command)
}
pub fn is_readonly_command_session_command(args: &Value) -> bool {
let Ok(Some(parts)) = crate::tools::command_args::command_words(args) else {
return false;
};
if parts.iter().any(|part| part == "--dry-run") {
return true;
}
let Some(command) = parts.first().map(String::as_str) else {
return false;
};
if is_readonly_base_command(command) {
if !is_readonly_command_string(args) {
return false;
}
return is_readonly_pipeline_segments(args);
}
match command {
"git" => matches!(parts.get(1).map(String::as_str), Some("status")),
"cargo" => matches!(parts.get(1).map(String::as_str), Some("check" | "test")),
"npm" | "pnpm" | "yarn" => match parts.get(1).map(String::as_str) {
Some("test") => true,
Some("run") => matches!(parts.get(2).map(String::as_str), Some("test")),
_ => false,
},
_ => false,
}
}
pub fn is_readonly_pipeline_segments(args: &Value) -> bool {
let Some(raw) = raw_command_text(args) else {
return false;
};
let segments: Vec<&str> = raw.split('|').map(str::trim).collect();
if segments.len() <= 1 {
return true;
}
for segment in segments {
if segment.is_empty() {
return false;
}
let first_command = segment
.split_whitespace()
.find(|token| !token.starts_with('-') && !token.contains('='))
.map(|token| token.to_ascii_lowercase());
let Some(first_command) = first_command else {
return false;
};
if !is_readonly_base_command(&first_command) {
return false;
}
}
true
}