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