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!(
66 command = base_cmd,
67 cache_hits = self.cache_hits,
68 "Command resolution cache hit"
69 );
70 return cached.clone();
71 }
72
73 self.cache_misses += 1;
74
75 let resolution = if let Ok(path) = which::which(base_cmd) {
77 CommandResolution {
78 command: base_cmd.to_string(),
79 resolved_path: Some(path.clone()),
80 found: true,
81 search_paths: Self::get_search_paths(),
82 }
83 } else {
84 warn!(command = base_cmd, "Command not found in PATH");
85 CommandResolution {
86 command: base_cmd.to_string(),
87 resolved_path: None,
88 found: false,
89 search_paths: Self::get_search_paths(),
90 }
91 };
92
93 self.cache.insert(base_cmd.to_string(), resolution.clone());
95 resolution
96 }
97
98 fn get_search_paths() -> Vec<PathBuf> {
100 std::env::var_os("PATH")
101 .map(|paths| std::env::split_paths(&paths).collect())
102 .unwrap_or_default()
103 }
104
105 pub fn clear_cache(&mut self) {
107 self.cache.clear();
108 debug!("Command resolver cache cleared");
109 }
110
111 pub fn cache_stats(&self) -> (usize, usize) {
113 (self.cache_hits, self.cache_misses)
114 }
115}
116
117impl Default for CommandResolver {
118 fn default() -> Self {
119 Self::new()
120 }
121}
122
123#[cfg(test)]
124mod tests {
125 use super::*;
126
127 #[test]
128 fn test_resolve_common_command() {
129 let mut resolver = CommandResolver::new();
130 let ls = resolver.resolve("ls");
131 assert_eq!(ls.command, "ls");
132 assert!(ls.found);
134 }
135
136 #[test]
137 fn test_cache_hits() {
138 let mut resolver = CommandResolver::new();
139 resolver.resolve("ls");
140 resolver.resolve("ls");
141 let (hits, misses) = resolver.cache_stats();
142 assert_eq!(hits, 1);
143 assert_eq!(misses, 1);
144 }
145
146 #[test]
147 fn test_nonexistent_command() {
148 let mut resolver = CommandResolver::new();
149 let fake = resolver.resolve("this_command_definitely_does_not_exist_xyz");
150 assert_eq!(fake.command, "this_command_definitely_does_not_exist_xyz");
151 assert!(!fake.found);
152 }
153
154 #[test]
155 fn test_extract_base_command() {
156 let mut resolver = CommandResolver::new();
157 let resolution = resolver.resolve("cargo fmt --check");
159 assert_eq!(resolution.command, "cargo");
160 }
161}