1use std::path::PathBuf;
3use std::process::Command;
4
5pub fn find_tool_binary(tool_name: &str) -> anyhow::Result<String> {
7 let output = Command::new("which").arg(tool_name).output()?;
8
9 if !output.status.success() {
10 anyhow::bail!("Tool '{}' not found in PATH", tool_name);
11 }
12
13 let path = String::from_utf8(output.stdout)?.trim().to_string();
14
15 Ok(path)
16}
17
18pub fn get_tool_help(tool_name: &str) -> anyhow::Result<String> {
20 let output = Command::new(tool_name).arg("--help").output()?;
21
22 let mut help = String::from_utf8(output.stdout)?;
25
26 if help.is_empty() {
27 help = String::from_utf8(output.stderr)?;
28 }
29
30 if help.is_empty() {
31 anyhow::bail!("Failed to get help output from '{}'", tool_name);
32 }
33
34 Ok(help)
35}
36
37pub fn get_tool_config_dir(tool_name: &str) -> anyhow::Result<PathBuf> {
39 let home = std::env::var("HOME")?;
40 let config_dir = PathBuf::from(home)
41 .join(".casting")
42 .join("tools")
43 .join(tool_name);
44 Ok(config_dir)
45}
46
47pub fn generate_aliases(tool_name: &str) -> Vec<String> {
53 let mut aliases = Vec::new();
54
55 if !tool_name.contains(' ') {
57 return aliases;
58 }
59
60 let words: Vec<&str> = tool_name.split_whitespace().collect();
61
62 let snake_case: String = words
64 .iter()
65 .map(|w| w.to_lowercase())
66 .collect::<Vec<_>>()
67 .join("_");
68 aliases.push(snake_case);
69
70 let initials: String = words
74 .iter()
75 .flat_map(|w| {
76 let is_acronym = w.len() > 1 && w.chars().all(|c| c.is_uppercase());
77 if is_acronym {
78 w.to_lowercase().chars().collect::<Vec<_>>()
80 } else {
81 w.chars()
83 .next()
84 .map(|c| c.to_lowercase().collect::<Vec<_>>())
85 .unwrap_or_default()
86 }
87 })
88 .collect();
89
90 if initials.len() >= 2 && !aliases.contains(&initials) {
92 aliases.push(initials);
93 }
94
95 aliases
96}
97
98pub fn get_all_tool_identifiers() -> anyhow::Result<std::collections::HashSet<String>> {
100 use crate::tool_config::{setup_config_migrator, ToolConfig};
101 use std::fs;
102
103 let mut identifiers = std::collections::HashSet::new();
104
105 let home = std::env::var("HOME")?;
106 let tools_dir = PathBuf::from(home).join(".casting").join("tools");
107
108 if !tools_dir.exists() {
109 return Ok(identifiers);
110 }
111
112 let migrator = setup_config_migrator()?;
113
114 for entry in fs::read_dir(&tools_dir)? {
115 let entry = entry?;
116 let path = entry.path();
117
118 if path.is_dir() {
119 let config_path = path.join("config.json");
120 if config_path.exists() {
121 let config_str = fs::read_to_string(&config_path)?;
122 if let Ok(config) =
123 migrator.load_with_fallback::<ToolConfig>("tool_config", &config_str)
124 {
125 identifiers.insert(config.tool_name.to_lowercase());
127 for alias in config.aliases {
129 identifiers.insert(alias.to_lowercase());
130 }
131 }
132 }
133 }
134 }
135
136 Ok(identifiers)
137}
138
139pub fn generate_unique_aliases(tool_name: &str) -> anyhow::Result<Vec<String>> {
141 let candidates = generate_aliases(tool_name);
142 let existing = get_all_tool_identifiers()?;
143
144 let unique: Vec<String> = candidates
145 .into_iter()
146 .filter(|alias| !existing.contains(&alias.to_lowercase()))
147 .collect();
148
149 Ok(unique)
150}
151
152pub fn resolve_tool_name(identifier: &str) -> anyhow::Result<Option<String>> {
154 use crate::tool_config::{setup_config_migrator, ToolConfig};
155 use std::fs;
156
157 let home = std::env::var("HOME")?;
158 let tools_dir = PathBuf::from(home).join(".casting").join("tools");
159
160 if !tools_dir.exists() {
161 return Ok(None);
162 }
163
164 let direct_path = tools_dir.join(identifier);
166 if direct_path.exists() && direct_path.is_dir() {
167 return Ok(Some(identifier.to_string()));
168 }
169
170 let migrator = setup_config_migrator()?;
172 let identifier_lower = identifier.to_lowercase();
173
174 for entry in fs::read_dir(&tools_dir)? {
175 let entry = entry?;
176 let path = entry.path();
177
178 if path.is_dir() {
179 let config_path = path.join("config.json");
180 if config_path.exists() {
181 let config_str = fs::read_to_string(&config_path)?;
182 if let Ok(config) =
183 migrator.load_with_fallback::<ToolConfig>("tool_config", &config_str)
184 {
185 for alias in &config.aliases {
187 if alias.to_lowercase() == identifier_lower {
188 return Ok(Some(config.tool_name));
189 }
190 }
191 }
192 }
193 }
194 }
195
196 Ok(None)
197}
198
199#[cfg(test)]
200mod tests {
201 use super::*;
202
203 #[test]
204 fn test_generate_aliases_multi_word() {
205 let aliases = generate_aliases("UX Designer");
207 assert_eq!(aliases.len(), 2);
208 assert_eq!(aliases[0], "ux_designer");
209 assert_eq!(aliases[1], "uxd");
210 }
211
212 #[test]
213 fn test_generate_aliases_three_words() {
214 let aliases = generate_aliases("Senior Software Engineer");
216 assert_eq!(aliases.len(), 2);
217 assert_eq!(aliases[0], "senior_software_engineer");
218 assert_eq!(aliases[1], "sse");
219 }
220
221 #[test]
222 fn test_generate_aliases_single_word_returns_empty() {
223 let aliases = generate_aliases("git");
225 assert!(aliases.is_empty());
226 }
227
228 #[test]
229 fn test_generate_aliases_acronym_preserved() {
230 let aliases = generate_aliases("API Gateway");
233 assert_eq!(aliases[0], "api_gateway");
234 assert_eq!(aliases[1], "apig");
235 }
236
237 #[test]
238 fn test_generate_aliases_handles_extra_whitespace() {
239 let aliases = generate_aliases("UX Designer");
241 assert_eq!(aliases.len(), 2);
242 assert_eq!(aliases[0], "ux_designer");
243 assert_eq!(aliases[1], "uxd");
244 }
245
246 #[test]
247 fn test_generate_aliases_multiple_acronyms() {
248 let aliases = generate_aliases("AI ML Expert");
251 assert_eq!(aliases.len(), 2);
252 assert_eq!(aliases[0], "ai_ml_expert");
253 assert_eq!(aliases[1], "aimle");
254 }
255
256 #[test]
257 fn test_generate_aliases_long_name() {
258 let aliases = generate_aliases("Chief Technology Officer");
260 assert_eq!(aliases.len(), 2);
261 assert_eq!(aliases[0], "chief_technology_officer");
262 assert_eq!(aliases[1], "cto");
263 }
264}