command_vault/exec/
mod.rs

1use std::io::{self, Write};
2use std::process::Command as ProcessCommand;
3use std::env;
4use std::path::{Path, PathBuf};
5use anyhow::Result;
6use crossterm::terminal;
7use dialoguer::{theme::ColorfulTheme, Input};
8use crate::db::models::Command;
9use crate::shell::hooks::detect_current_shell;
10
11pub struct ExecutionContext {
12    pub command: String,
13    pub directory: String,
14    pub test_mode: bool,
15    pub debug_mode: bool,
16}
17
18pub fn wrap_command(command: &str, test_mode: bool) -> String {
19    if test_mode {
20        command.to_string()
21    } else {
22        // For interactive mode, handle shell initialization
23        let shell_type = detect_current_shell();
24        let clean_command = command.trim_matches('"').to_string();
25            
26        match shell_type.as_str() {
27            "zsh" => format!(
28                r#"setopt no_global_rcs; if [ -f ~/.zshrc ]; then ZDOTDIR=~ source ~/.zshrc; fi; {}"#,
29                clean_command
30            ),
31            "fish" => format!(
32                r#"if test -f ~/.config/fish/config.fish; source ~/.config/fish/config.fish 2>/dev/null; end; {}"#,
33                clean_command
34            ),
35            "bash" | _ => format!(
36                r#"if [ -f ~/.bashrc ]; then . ~/.bashrc >/dev/null 2>&1; fi; if [ -f ~/.bash_profile ]; then . ~/.bash_profile >/dev/null 2>&1; fi; {}"#,
37                clean_command
38            ),
39        }
40    }
41}
42
43fn is_path_traversal_attempt(command: &str, working_dir: &Path) -> bool {
44    // Check if the command contains path traversal attempts
45    if command.contains("..") {
46        // Get the absolute path of the working directory
47        if let Ok(working_dir) = working_dir.canonicalize() {
48            // Try to resolve any path in the command relative to working_dir
49            let potential_path = working_dir.join(command);
50            if let Ok(resolved_path) = potential_path.canonicalize() {
51                // Check if the resolved path is outside the working directory
52                return !resolved_path.starts_with(working_dir);
53            }
54        }
55        // If we can't resolve the paths, assume it's a traversal attempt
56        return true;
57    }
58    false
59}
60
61pub fn execute_shell_command(ctx: &ExecutionContext) -> Result<()> {
62    // Get the current shell
63    let shell = if cfg!(windows) {
64        String::from("cmd.exe")
65    } else {
66        env::var("SHELL").unwrap_or_else(|_| String::from("/bin/sh"))
67    };
68
69    // Wrap the command for shell execution
70    let wrapped_command = wrap_command(&ctx.command, ctx.test_mode);
71
72    // Check for directory traversal attempts
73    if is_path_traversal_attempt(&wrapped_command, Path::new(&ctx.directory)) {
74        return Err(anyhow::anyhow!("Directory traversal attempt detected"));
75    }
76
77    // Create command with the appropriate shell
78    let mut command = ProcessCommand::new(&shell);
79    
80    // In test mode, use simple shell execution
81    if ctx.test_mode {
82        command.args(&["-c", &wrapped_command]);
83    } else {
84        // Use -i for all shells in interactive mode to ensure proper initialization
85        command.args(&["-i", "-c", &wrapped_command]);
86    }
87    
88    // Set working directory
89    command.current_dir(&ctx.directory);
90
91    if ctx.debug_mode {
92        println!("Full command: {:?}", command);
93    }
94
95    // Disable raw mode only in interactive mode
96    if !ctx.test_mode {
97        let _ = terminal::disable_raw_mode();
98        // Reset cursor position
99        let mut stdout = io::stdout();
100        let _ = crossterm::execute!(
101            stdout,
102            crossterm::cursor::MoveTo(0, crossterm::cursor::position()?.1)
103        );
104        println!(); // Add a newline before command output
105    }
106
107    // Execute the command and capture output
108    let output = command.output()?;
109
110    // Handle command output
111    if !output.status.success() {
112        let stderr = String::from_utf8_lossy(&output.stderr);
113        return Err(anyhow::anyhow!(
114            "Command failed with status: {}. stderr: {}",
115            output.status,
116            stderr
117        ));
118    }
119
120    // Print stdout
121    if !output.stdout.is_empty() {
122        let stdout_str = String::from_utf8_lossy(&output.stdout);
123        print!("{}", stdout_str);
124    }
125
126    // Print stderr
127    if !output.stderr.is_empty() {
128        let stderr_str = String::from_utf8_lossy(&output.stderr);
129        eprint!("{}", stderr_str);
130    }
131
132    Ok(())
133}
134
135pub fn execute_command(command: &Command) -> Result<()> {
136    let test_mode = std::env::var("COMMAND_VAULT_TEST").is_ok();
137    let debug_mode = std::env::var("COMMAND_VAULT_DEBUG").is_ok();
138    let mut final_command = command.command.clone();
139
140    // If command has parameters, prompt for values first
141    if !command.parameters.is_empty() {
142        for param in &command.parameters {
143            println!("Parameter: {}", param.name);
144            println!();
145
146            let value = if test_mode {
147                let value = std::env::var("COMMAND_VAULT_TEST_INPUT")
148                    .unwrap_or_else(|_| "test_value".to_string());
149                println!("Enter value: {}", value);
150                println!();
151                value
152            } else {
153                let input: String = Input::with_theme(&ColorfulTheme::default())
154                    .with_prompt("Enter value")
155                    .allow_empty(true)
156                    .interact_text()?;
157                println!();
158                
159                if input.contains(' ') {
160                    format!("'{}'", input.replace("'", "'\\''"))
161                } else {
162                    input
163                }
164            };
165
166            final_command = final_command.replace(&format!("@{}", param.name), &value);
167        }
168    }
169
170    let ctx = ExecutionContext {
171        command: final_command,
172        directory: command.directory.clone(),
173        test_mode,
174        debug_mode,
175    };
176
177    // Print command details only once
178    println!("─────────────────────────────────────────────");
179    println!();
180    println!("Command to execute: {}", ctx.command);
181    println!("Working directory: {}", ctx.directory);
182    println!();
183
184    execute_shell_command(&ctx)
185}