use std::collections::HashSet;
use std::time::Duration;
pub const DEFAULT_COMMAND_TIMEOUT: u64 = 30;
pub const MAX_OUTPUT_SIZE: usize = 1024 * 1024;
static INTERACTIVE_COMMANDS: &[&str] = &[
"vim",
"vi",
"nano",
"emacs",
"code",
"subl",
"python",
"python3",
"node",
"nodejs",
"ruby",
"irb",
"scala",
"ghci",
"mysql",
"psql",
"sqlite3",
"redis-cli",
"mongo",
"less",
"more",
"view",
"top",
"htop",
"watch",
"tail -f",
"git rebase -i",
"git add -i",
"git commit", ];
static LONG_RUNNING_COMMANDS: &[&str] = &[
"make",
"cargo build",
"npm install",
"pip install",
"yarn install",
"gcc",
"g++",
"clang",
"rustc",
"javac",
"apt-get",
"yum",
"brew install",
"pacman",
"wget",
"curl",
"rsync",
"scp",
"tar",
"zip",
"unzip",
"gzip",
];
static BACKGROUND_COMMANDS: &[&str] = &[
"python -m http.server",
"node server",
"rails server",
"cargo run",
"nohup",
"screen",
"tmux",
"systemctl start",
"service start",
];
#[derive(Debug, Clone)]
pub struct CommandSafety {
interactive: HashSet<String>,
long_running: HashSet<String>,
background: HashSet<String>,
}
impl Default for CommandSafety {
fn default() -> Self {
Self::new()
}
}
impl CommandSafety {
pub fn new() -> Self {
let interactive = INTERACTIVE_COMMANDS.iter().map(|s| (*s).to_string()).collect();
let long_running = LONG_RUNNING_COMMANDS.iter().map(|s| (*s).to_string()).collect();
let background = BACKGROUND_COMMANDS.iter().map(|s| (*s).to_string()).collect();
Self { interactive, long_running, background }
}
pub fn is_interactive(&self, command: &str) -> bool {
let normalized = Self::normalize_command(command);
if self.interactive.contains(&normalized) {
return true;
}
for interactive_cmd in &self.interactive {
if normalized.starts_with(interactive_cmd) {
let rest = &normalized[interactive_cmd.len()..];
if rest.is_empty() || rest.starts_with(' ') || rest.starts_with('\t') {
if interactive_cmd == "git commit"
&& (normalized.contains("-m") || normalized.contains("--message"))
{
return false;
}
if interactive_cmd == "python" || interactive_cmd == "python3" {
let parts: Vec<&str> = normalized.split_whitespace().collect();
if parts.len() > 1 && !parts[1].starts_with('-') {
return false;
}
}
return true;
}
}
}
Self::check_special_interactive_cases(&normalized)
}
pub fn is_long_running(&self, command: &str) -> bool {
let normalized = Self::normalize_command(command);
for long_cmd in &self.long_running {
if normalized.starts_with(long_cmd) {
let rest = &normalized[long_cmd.len()..];
if rest.is_empty() || rest.starts_with(' ') || rest.starts_with('\t') {
return true;
}
}
}
false
}
pub fn is_background_command(&self, command: &str) -> bool {
let normalized = Self::normalize_command(command);
if normalized.contains(" &") || normalized.ends_with('&') {
return true;
}
for bg_cmd in &self.background {
if normalized.starts_with(bg_cmd) {
let rest = &normalized[bg_cmd.len()..];
if rest.is_empty() || rest.starts_with(' ') || rest.starts_with('\t') {
return true;
}
}
}
false
}
pub fn get_timeout(&self, command: &str) -> Duration {
if self.is_long_running(command) {
Duration::from_secs(300) } else if self.is_background_command(command) {
Duration::from_secs(60) } else {
Duration::from_secs(DEFAULT_COMMAND_TIMEOUT) }
}
pub fn get_warnings(&self, command: &str) -> Vec<String> {
let mut warnings = Vec::new();
if self.is_interactive(command) {
warnings.push(format!(
"Command '{command}' appears to be interactive and may hang waiting for input"
));
warnings.push("Consider using non-interactive flags or alternatives".to_string());
}
if self.is_long_running(command) {
warnings.push(format!("Command '{command}' may take a long time to complete"));
warnings.push("Consider using status_check to monitor progress".to_string());
}
if self.is_background_command(command) {
warnings.push(format!("Command '{command}' may spawn background processes"));
warnings.push("Use explicit process management if needed".to_string());
}
warnings
}
fn normalize_command(command: &str) -> String {
command.trim().to_lowercase()
}
fn check_special_interactive_cases(command: &str) -> bool {
if command.starts_with("git commit")
&& !command.contains("-m")
&& !command.contains("--message")
{
return true;
}
if command.starts_with("docker run")
&& !command.contains("-d")
&& !command.contains("--detach")
{
return true;
}
if command == "ssh" || (command.starts_with("ssh ") && !command.contains(" -- ")) {
return true;
}
if command.starts_with("ftp ") || command.starts_with("sftp ") {
return true;
}
false
}
}
#[derive(Debug, Clone)]
pub struct CommandContext {
pub command: String,
pub timeout: Duration,
pub max_output_size: usize,
pub is_interactive: bool,
pub is_long_running: bool,
pub is_background: bool,
pub warnings: Vec<String>,
}
impl CommandContext {
pub fn new(command: &str) -> Self {
let safety = CommandSafety::new();
let timeout = safety.get_timeout(command);
let is_interactive = safety.is_interactive(command);
let is_long_running = safety.is_long_running(command);
let is_background = safety.is_background_command(command);
let warnings = safety.get_warnings(command);
Self {
command: command.to_string(),
timeout,
max_output_size: MAX_OUTPUT_SIZE,
is_interactive,
is_long_running,
is_background,
warnings,
}
}
pub fn should_allow_execution(&self) -> Result<(), crate::errors::WinxError> {
if self.is_interactive {
return Err(crate::errors::WinxError::InteractiveCommandDetected {
command: self.command.clone(),
});
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_interactive_detection() {
let safety = CommandSafety::new();
assert!(safety.is_interactive("vim file.txt"));
assert!(safety.is_interactive("python"));
assert!(safety.is_interactive("git commit"));
assert!(safety.is_interactive("mysql -u root"));
assert!(!safety.is_interactive("ls -la"));
assert!(!safety.is_interactive("git commit -m 'message'"));
assert!(!safety.is_interactive("python script.py"));
assert!(!safety.is_interactive("cat file.txt"));
}
#[test]
fn test_long_running_detection() {
let safety = CommandSafety::new();
assert!(safety.is_long_running("cargo build"));
assert!(safety.is_long_running("npm install"));
assert!(safety.is_long_running("make all"));
assert!(!safety.is_long_running("ls"));
assert!(!safety.is_long_running("echo hello"));
}
#[test]
fn test_background_detection() {
let safety = CommandSafety::new();
assert!(safety.is_background_command("python -m http.server &"));
assert!(safety.is_background_command("nohup long_process"));
assert!(safety.is_background_command("screen -S session"));
assert!(!safety.is_background_command("ls"));
assert!(!safety.is_background_command("python script.py"));
}
#[test]
fn test_timeout_calculation() {
let safety = CommandSafety::new();
assert_eq!(safety.get_timeout("cargo build"), Duration::from_secs(300));
assert_eq!(safety.get_timeout("nohup process &"), Duration::from_secs(60));
assert_eq!(safety.get_timeout("ls"), Duration::from_secs(30));
}
#[test]
fn test_command_context() {
let ctx = CommandContext::new("vim file.txt");
assert!(ctx.is_interactive);
assert!(!ctx.warnings.is_empty());
assert!(ctx.should_allow_execution().is_err());
let ctx2 = CommandContext::new("ls -la");
assert!(!ctx2.is_interactive);
assert!(ctx2.should_allow_execution().is_ok());
}
}