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" | "sh" | "zsh" | "fish" | "dash" | "ash" | "busybox"
91    )
92}
93
94pub(super) fn is_sandbox_wrapper_program(program: &str, args: &[String]) -> bool {
95    let name = Path::new(program)
96        .file_name()
97        .and_then(|value| value.to_str())
98        .unwrap_or(program)
99        .to_ascii_lowercase();
100    if name == "sandbox-exec" {
101        return true;
102    }
103
104    args.iter().any(|arg| {
105        matches!(
106            arg.as_str(),
107            "--sandbox-policy" | "--sandbox-policy-cwd" | "--seccomp-profile" | "--resource-limits"
108        )
109    })
110}
111
112// Note: resolve_fallback_shell moved to tools::shell module
113
114/// Resolve program path - if program doesn't exist in PATH, return None to signal shell fallback.
115/// This allows the shell to find programs installed in user-specific directories.
116pub fn is_development_toolchain_command(program: &str) -> bool {
117    let name = Path::new(program)
118        .file_name()
119        .and_then(|value| value.to_str())
120        .unwrap_or(program)
121        .to_ascii_lowercase();
122    matches!(
123        name.as_str(),
124        "cargo"
125            | "rustc"
126            | "rustup"
127            | "rustfmt"
128            | "clippy"
129            | "npm"
130            | "node"
131            | "yarn"
132            | "pnpm"
133            | "bun"
134            | "go"
135            | "python"
136            | "python3"
137            | "pip"
138            | "pip3"
139            | "java"
140            | "javac"
141            | "mvn"
142            | "gradle"
143            | "make"
144            | "cmake"
145            | "gcc"
146            | "g++"
147            | "clang"
148            | "clang++"
149            | "which"
150    )
151}
152
153#[cfg(test)]
154mod tests {
155    use super::*;
156
157    #[test]
158    fn test_is_cargo_command() {
159        assert!(is_cargo_command("cargo"));
160        assert!(is_cargo_command("Cargo"));
161        assert!(is_cargo_command("/usr/bin/cargo"));
162        assert!(is_cargo_command("~/.cargo/bin/cargo"));
163        assert!(!is_cargo_command("rustc"));
164        assert!(!is_cargo_command("npm"));
165        assert!(!is_cargo_command("cargo-watch"));
166    }
167
168    #[test]
169    fn test_is_cargo_command_string() {
170        assert!(is_cargo_command_string("cargo check"));
171        assert!(is_cargo_command_string("cargo test --lib"));
172        assert!(is_cargo_command_string("cargo build --release"));
173        assert!(is_cargo_command_string("  cargo clippy  "));
174        assert!(is_cargo_command_string("cargo"));
175        assert!(!is_cargo_command_string("rustc --version"));
176        assert!(!is_cargo_command_string("npm install"));
177        assert!(!is_cargo_command_string("echo cargo"));
178    }
179
180    #[test]
181    fn test_is_long_running_command() {
182        assert!(is_long_running_command("cargo"));
183        assert!(is_long_running_command("cmake"));
184        assert!(is_long_running_command("composer"));
185        assert!(is_long_running_command("bundle"));
186        assert!(is_long_running_command("poetry"));
187        assert!(!is_long_running_command("rg"));
188    }
189
190    #[test]
191    fn test_is_long_running_command_string() {
192        assert!(is_long_running_command_string("cargo test"));
193        assert!(is_long_running_command_string(" cmake --build ."));
194        assert!(is_long_running_command_string("composer install"));
195        assert!(is_long_running_command_string("bundle exec rake"));
196        assert!(is_long_running_command_string("poetry install"));
197        assert!(!is_long_running_command_string("rg \"fn main\" ."));
198    }
199
200    #[test]
201    fn test_is_sandbox_wrapper_program() {
202        assert!(is_sandbox_wrapper_program("sandbox-exec", &[]));
203        assert!(is_sandbox_wrapper_program(
204            "vtcode-linux-sandbox",
205            &["--sandbox-policy".to_string()]
206        ));
207        assert!(!is_sandbox_wrapper_program(
208            "bash",
209            &["-lc".to_string(), "ls".to_string()]
210        ));
211    }
212}