vtcode_core/tools/
command_resolver.rs1use hashbrown::HashMap;
6use std::path::PathBuf;
7use tracing::{debug, warn};
8
9#[derive(Debug, Clone)]
11pub struct CommandResolution {
12 pub command: String,
14
15 pub resolved_path: Option<PathBuf>,
17
18 pub found: bool,
20
21 pub search_paths: Vec<PathBuf>,
23}
24
25pub struct CommandResolver {
27 cache: HashMap<String, CommandResolution>,
29
30 cache_hits: usize,
32
33 cache_misses: usize,
35}
36
37impl CommandResolver {
38 pub fn new() -> Self {
40 Self {
41 cache: HashMap::new(),
42 cache_hits: 0,
43 cache_misses: 0,
44 }
45 }
46
47 pub fn resolve(&mut self, cmd: &str) -> CommandResolution {
59 let base_cmd = cmd.split_whitespace().next().unwrap_or(cmd);
61
62 if let Some(cached) = self.cache.get(base_cmd) {
64 self.cache_hits += 1;
65 debug!(command = base_cmd, cache_hits = self.cache_hits, "Command resolution cache hit");
66 return cached.clone();
67 }
68
69 self.cache_misses += 1;
70
71 let resolution = if let Ok(path) = which::which(base_cmd) {
73 CommandResolution {
74 command: base_cmd.to_string(),
75 resolved_path: Some(path.clone()),
76 found: true,
77 search_paths: Self::get_search_paths(),
78 }
79 } else {
80 warn!(command = base_cmd, "Command not found in PATH");
81 CommandResolution {
82 command: base_cmd.to_string(),
83 resolved_path: None,
84 found: false,
85 search_paths: Self::get_search_paths(),
86 }
87 };
88
89 self.cache.insert(base_cmd.to_string(), resolution.clone());
91 resolution
92 }
93
94 fn get_search_paths() -> Vec<PathBuf> {
96 std::env::var_os("PATH")
97 .map(|paths| std::env::split_paths(&paths).collect())
98 .unwrap_or_default()
99 }
100
101 pub fn clear_cache(&mut self) {
103 self.cache.clear();
104 debug!("Command resolver cache cleared");
105 }
106
107 pub fn cache_stats(&self) -> (usize, usize) {
109 (self.cache_hits, self.cache_misses)
110 }
111}
112
113impl Default for CommandResolver {
114 fn default() -> Self {
115 Self::new()
116 }
117}
118
119#[cfg(test)]
120mod tests {
121 use super::*;
122
123 #[test]
124 fn test_resolve_common_command() {
125 let mut resolver = CommandResolver::new();
126 let ls = resolver.resolve("ls");
127 assert_eq!(ls.command, "ls");
128 assert!(ls.found);
130 }
131
132 #[test]
133 fn test_cache_hits() {
134 let mut resolver = CommandResolver::new();
135 resolver.resolve("ls");
136 resolver.resolve("ls");
137 let (hits, misses) = resolver.cache_stats();
138 assert_eq!(hits, 1);
139 assert_eq!(misses, 1);
140 }
141
142 #[test]
143 fn test_nonexistent_command() {
144 let mut resolver = CommandResolver::new();
145 let fake = resolver.resolve("this_command_definitely_does_not_exist_xyz");
146 assert_eq!(fake.command, "this_command_definitely_does_not_exist_xyz");
147 assert!(!fake.found);
148 }
149
150 #[test]
151 fn test_extract_base_command() {
152 let mut resolver = CommandResolver::new();
153 let resolution = resolver.resolve("cargo fmt --check");
155 assert_eq!(resolution.command, "cargo");
156 }
157}