Skip to main content

vtcode_core/tools/pty/
command_utils.rs

1use std::path::Path;
2
3/// Check if a command uses cargo, which requires exclusive file lock access
4pub fn is_cargo_command(program: &str) -> bool {
5    let name = Path::new(program)
6        .file_name()
7        .and_then(|value| value.to_str())
8        .unwrap_or(program)
9        .to_ascii_lowercase();
10    name == "cargo"
11}
12
13/// Check if a command string (potentially passed via shell -c) is a cargo command
14pub fn is_cargo_command_string(command: &str) -> bool {
15    let trimmed = command.trim();
16    trimmed.starts_with("cargo ") || trimmed == "cargo"
17}
18
19/// Commands that use lock files and need serialization.
20/// These commands access lock files (Cargo.lock, package-lock.json, etc.)
21/// and can cause "blocking waiting for file lock" errors if run concurrently.
22const LOCKFILE_COMMANDS: &[&str] = &[
23    "cargo",    // Cargo.lock
24    "npm",      // package-lock.json
25    "pnpm",     // pnpm-lock.yaml
26    "yarn",     // yarn.lock
27    "bun",      // bun.lockb
28    "go",       // go.sum
29    "gradle",   // gradle.lockfile
30    "mvn",      // uses local repo locks
31    "pip",      // pip can have concurrent install issues
32    "pip3",     // pip3 same as pip
33    "poetry",   // poetry.lock
34    "composer", // composer.lock (PHP)
35    "bundler",  // Gemfile.lock (Ruby)
36    "bundle",   // Gemfile.lock (Ruby)
37];
38
39fn normalize_program_name(program: &str) -> String {
40    Path::new(program)
41        .file_name()
42        .and_then(|value| value.to_str())
43        .unwrap_or(program)
44        .to_ascii_lowercase()
45}
46
47/// Check if a command requires serialization due to lock file access.
48/// These commands should not be run concurrently within the same workspace
49/// to prevent lock file contention errors.
50pub fn is_lockfile_command(program: &str) -> bool {
51    let name = normalize_program_name(program);
52    LOCKFILE_COMMANDS.contains(&name.as_str())
53}
54
55/// Check if a command should be serialized because it is long-running.
56pub fn is_long_running_command(program: &str) -> bool {
57    let name = normalize_program_name(program);
58    is_development_toolchain_command(&name) || is_lockfile_command(&name)
59}
60
61/// Check if a command string (potentially passed via shell -c) is a lockfile command
62pub fn is_lockfile_command_string(command: &str) -> bool {
63    let trimmed = command.trim();
64    LOCKFILE_COMMANDS
65        .iter()
66        .any(|&cmd| trimmed.starts_with(&format!("{cmd} ")) || trimmed == cmd)
67}
68
69/// Check if a command string (potentially passed via shell -c) is long-running
70pub fn is_long_running_command_string(command: &str) -> bool {
71    let trimmed = command.trim();
72    if trimmed.is_empty() {
73        return false;
74    }
75    if is_lockfile_command_string(trimmed) {
76        return true;
77    }
78    let first = trimmed.split_whitespace().next().unwrap_or_default();
79    is_development_toolchain_command(first)
80}
81
82pub(super) fn is_shell_program(program: &str) -> bool {
83    let name = Path::new(program)
84        .file_name()
85        .and_then(|value| value.to_str())
86        .unwrap_or(program)
87        .to_ascii_lowercase();
88    matches!(
89        name.as_str(),
90        "bash"
91            | "sh"
92            | "zsh"
93            | "fish"
94            | "dash"
95            | "ash"
96            | "busybox"
97            | "ksh"
98            | "ksh93"
99            | "mksh"
100            | "tcsh"
101            | "csh"
102            | "posh"
103            | "yash"
104            | "oksh"
105            | "loksh"
106            | "oil"
107            | "osh"
108            | "nu"
109            | "nushell"
110            | "elvish"
111            | "pwsh"
112    )
113}
114
115pub(super) fn is_sandbox_wrapper_program(program: &str, args: &[String]) -> bool {
116    let name = Path::new(program)
117        .file_name()
118        .and_then(|value| value.to_str())
119        .unwrap_or(program)
120        .to_ascii_lowercase();
121    if name == "sandbox-exec" {
122        return true;
123    }
124
125    args.iter().any(|arg| {
126        matches!(
127            arg.as_str(),
128            "--sandbox-policy" | "--sandbox-policy-cwd" | "--seccomp-profile" | "--resource-limits"
129        )
130    })
131}
132
133// Note: resolve_fallback_shell moved to tools::shell module
134
135/// Resolve program path - if program doesn't exist in PATH, return None to signal shell fallback.
136/// This allows the shell to find programs installed in user-specific directories.
137pub fn is_development_toolchain_command(program: &str) -> bool {
138    let name = Path::new(program)
139        .file_name()
140        .and_then(|value| value.to_str())
141        .unwrap_or(program)
142        .to_ascii_lowercase();
143    matches!(
144        name.as_str(),
145        "cargo"
146            | "rustc"
147            | "rustup"
148            | "rustfmt"
149            | "clippy"
150            | "npm"
151            | "node"
152            | "yarn"
153            | "pnpm"
154            | "bun"
155            | "go"
156            | "python"
157            | "python3"
158            | "pip"
159            | "pip3"
160            | "java"
161            | "javac"
162            | "mvn"
163            | "gradle"
164            | "make"
165            | "cmake"
166            | "gcc"
167            | "g++"
168            | "clang"
169            | "clang++"
170            | "which"
171    )
172}
173
174#[cfg(test)]
175mod tests {
176    use super::*;
177
178    #[test]
179    fn test_is_cargo_command() {
180        assert!(is_cargo_command("cargo"));
181        assert!(is_cargo_command("Cargo"));
182        assert!(is_cargo_command("/usr/bin/cargo"));
183        assert!(is_cargo_command("~/.cargo/bin/cargo"));
184        assert!(!is_cargo_command("rustc"));
185        assert!(!is_cargo_command("npm"));
186        assert!(!is_cargo_command("cargo-watch"));
187    }
188
189    #[test]
190    fn test_is_cargo_command_string() {
191        assert!(is_cargo_command_string("cargo check"));
192        assert!(is_cargo_command_string("cargo test --lib"));
193        assert!(is_cargo_command_string("cargo build --release"));
194        assert!(is_cargo_command_string("  cargo clippy  "));
195        assert!(is_cargo_command_string("cargo"));
196        assert!(!is_cargo_command_string("rustc --version"));
197        assert!(!is_cargo_command_string("npm install"));
198        assert!(!is_cargo_command_string("echo cargo"));
199    }
200
201    #[test]
202    fn test_is_long_running_command() {
203        assert!(is_long_running_command("cargo"));
204        assert!(is_long_running_command("cmake"));
205        assert!(is_long_running_command("composer"));
206        assert!(is_long_running_command("bundle"));
207        assert!(is_long_running_command("poetry"));
208        assert!(!is_long_running_command("rg"));
209    }
210
211    #[test]
212    fn test_is_long_running_command_string() {
213        assert!(is_long_running_command_string("cargo test"));
214        assert!(is_long_running_command_string(" cmake --build ."));
215        assert!(is_long_running_command_string("composer install"));
216        assert!(is_long_running_command_string("bundle exec rake"));
217        assert!(is_long_running_command_string("poetry install"));
218        assert!(!is_long_running_command_string("rg \"fn main\" ."));
219    }
220
221    #[test]
222    fn test_is_sandbox_wrapper_program() {
223        assert!(is_sandbox_wrapper_program("sandbox-exec", &[]));
224        assert!(is_sandbox_wrapper_program("vtcode-linux-sandbox", &["--sandbox-policy".to_string()]));
225        assert!(!is_sandbox_wrapper_program("bash", &["-lc".to_string(), "ls".to_string()]));
226    }
227}