1use crate::{Result, VxEnvironment, VxError};
7use anyhow::Context;
8use std::fs;
9use std::path::{Path, PathBuf};
10use vx_shim::ShimManager;
11
12pub struct VxShimManager {
14 shim_manager: ShimManager,
16 shim_dir: PathBuf,
18 shim_executable: PathBuf,
20 #[allow(dead_code)]
22 environment: VxEnvironment,
23}
24
25impl VxShimManager {
26 pub fn new(environment: VxEnvironment) -> Result<Self> {
28 let shim_dir = environment.shim_dir()?;
29
30 fs::create_dir_all(&shim_dir)
32 .with_context(|| format!("Failed to create shim directory: {}", shim_dir.display()))?;
33
34 let shim_manager = ShimManager::new(&shim_dir);
35
36 let shim_executable = Self::find_shim_executable(&environment)?;
38
39 Ok(Self {
40 shim_manager,
41 shim_dir,
42 shim_executable,
43 environment,
44 })
45 }
46
47 pub fn create_tool_shim(
49 &self,
50 tool_name: &str,
51 tool_path: &Path,
52 version: &str,
53 args: Option<&str>,
54 ) -> Result<PathBuf> {
55 let shim_config_path = self
57 .shim_manager
58 .create_shim(tool_name, tool_path, args)
59 .with_context(|| format!("Failed to create shim config for {}", tool_name))?;
60
61 let shim_executable_path = self.create_shim_executable(tool_name)?;
63
64 self.store_shim_metadata(tool_name, version, &shim_config_path, &shim_executable_path)?;
66
67 Ok(shim_executable_path)
68 }
69
70 pub fn switch_tool_version(
72 &self,
73 tool_name: &str,
74 new_version: &str,
75 tool_path: &Path,
76 ) -> Result<()> {
77 let shim_executable_path = self.shim_dir.join(self.get_executable_name(tool_name));
79
80 if !shim_executable_path.exists() {
81 self.create_tool_shim(tool_name, tool_path, new_version, None)?;
83 } else {
84 self.update_shim_config(tool_name, tool_path, None)?;
86 self.store_shim_metadata(
87 tool_name,
88 new_version,
89 &self.get_shim_config_path(tool_name),
90 &shim_executable_path,
91 )?;
92 }
93
94 Ok(())
95 }
96
97 pub fn list_shims(&self) -> Result<Vec<String>> {
99 Ok(self
100 .shim_manager
101 .list_shims()
102 .context("Failed to list shims")?)
103 }
104
105 pub fn remove_shim(&self, tool_name: &str) -> Result<()> {
107 self.shim_manager
109 .remove_shim(tool_name)
110 .with_context(|| format!("Failed to remove shim config for {}", tool_name))?;
111
112 let shim_executable_path = self.shim_dir.join(self.get_executable_name(tool_name));
114 if shim_executable_path.exists() {
115 fs::remove_file(&shim_executable_path).with_context(|| {
116 format!(
117 "Failed to remove shim executable: {}",
118 shim_executable_path.display()
119 )
120 })?;
121 }
122
123 self.remove_shim_metadata(tool_name)?;
125
126 Ok(())
127 }
128
129 pub fn get_shim_version(&self, tool_name: &str) -> Result<Option<String>> {
131 let metadata = self.load_shim_metadata(tool_name)?;
132 Ok(metadata.and_then(|m| m.version))
133 }
134
135 pub fn shim_dir(&self) -> &Path {
137 &self.shim_dir
138 }
139
140 fn create_shim_executable(&self, tool_name: &str) -> Result<PathBuf> {
142 let shim_executable_path = self.shim_dir.join(self.get_executable_name(tool_name));
143
144 fs::copy(&self.shim_executable, &shim_executable_path).with_context(|| {
146 format!(
147 "Failed to copy shim executable from {} to {}",
148 self.shim_executable.display(),
149 shim_executable_path.display()
150 )
151 })?;
152
153 #[cfg(unix)]
155 {
156 use std::os::unix::fs::PermissionsExt;
157 let mut perms = fs::metadata(&shim_executable_path)?.permissions();
158 perms.set_mode(0o755);
159 fs::set_permissions(&shim_executable_path, perms)?;
160 }
161
162 Ok(shim_executable_path)
163 }
164
165 fn update_shim_config(
167 &self,
168 tool_name: &str,
169 tool_path: &Path,
170 args: Option<&str>,
171 ) -> Result<()> {
172 let _ = self.shim_manager.remove_shim(tool_name);
174 self.shim_manager
175 .create_shim(tool_name, tool_path, args)
176 .with_context(|| format!("Failed to update shim config for {}", tool_name))?;
177 Ok(())
178 }
179
180 fn get_executable_name(&self, tool_name: &str) -> String {
182 if cfg!(windows) {
183 format!("{}.exe", tool_name)
184 } else {
185 tool_name.to_string()
186 }
187 }
188
189 fn get_shim_config_path(&self, tool_name: &str) -> PathBuf {
191 self.shim_dir.join(format!("{}.shim", tool_name))
192 }
193
194 fn find_shim_executable(environment: &VxEnvironment) -> Result<PathBuf> {
196 if let Ok(current_exe) = std::env::current_exe() {
198 if let Some(parent) = current_exe.parent() {
199 let shim_path = parent.join(if cfg!(windows) {
200 "vx-shim.exe"
201 } else {
202 "vx-shim"
203 });
204 if shim_path.exists() {
205 return Ok(shim_path);
206 }
207 }
208 }
209
210 let bin_dir = environment.bin_dir()?;
212 let shim_path = bin_dir.join(if cfg!(windows) {
213 "vx-shim.exe"
214 } else {
215 "vx-shim"
216 });
217 if shim_path.exists() {
218 return Ok(shim_path);
219 }
220
221 if let Ok(shim_path) = which::which("vx-shim") {
223 return Ok(shim_path);
224 }
225
226 Err(VxError::ShimNotFound(
227 "vx-shim executable not found. Please ensure vx-shim is installed and available."
228 .to_string(),
229 ))
230 }
231
232 fn store_shim_metadata(
234 &self,
235 tool_name: &str,
236 version: &str,
237 config_path: &Path,
238 executable_path: &Path,
239 ) -> Result<()> {
240 let metadata = ShimMetadata {
241 tool_name: tool_name.to_string(),
242 version: Some(version.to_string()),
243 config_path: config_path.to_path_buf(),
244 executable_path: executable_path.to_path_buf(),
245 created_at: chrono::Utc::now(),
246 updated_at: chrono::Utc::now(),
247 };
248
249 let metadata_path = self.get_metadata_path(tool_name);
250 let content = toml::to_string(&metadata).context("Failed to serialize shim metadata")?;
251
252 fs::write(&metadata_path, content).with_context(|| {
253 format!("Failed to write shim metadata: {}", metadata_path.display())
254 })?;
255
256 Ok(())
257 }
258
259 fn load_shim_metadata(&self, tool_name: &str) -> Result<Option<ShimMetadata>> {
261 let metadata_path = self.get_metadata_path(tool_name);
262
263 if !metadata_path.exists() {
264 return Ok(None);
265 }
266
267 let content = fs::read_to_string(&metadata_path).with_context(|| {
268 format!("Failed to read shim metadata: {}", metadata_path.display())
269 })?;
270
271 let metadata: ShimMetadata =
272 toml::from_str(&content).context("Failed to parse shim metadata")?;
273
274 Ok(Some(metadata))
275 }
276
277 fn remove_shim_metadata(&self, tool_name: &str) -> Result<()> {
279 let metadata_path = self.get_metadata_path(tool_name);
280
281 if metadata_path.exists() {
282 fs::remove_file(&metadata_path).with_context(|| {
283 format!(
284 "Failed to remove shim metadata: {}",
285 metadata_path.display()
286 )
287 })?;
288 }
289
290 Ok(())
291 }
292
293 fn get_metadata_path(&self, tool_name: &str) -> PathBuf {
295 self.shim_dir.join(format!("{}.meta", tool_name))
296 }
297}
298
299#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
301struct ShimMetadata {
302 tool_name: String,
303 version: Option<String>,
304 config_path: PathBuf,
305 executable_path: PathBuf,
306 created_at: chrono::DateTime<chrono::Utc>,
307 updated_at: chrono::DateTime<chrono::Utc>,
308}
309
310#[cfg(test)]
311mod tests {
312 use super::*;
313 use tempfile::TempDir;
314
315 #[test]
316 fn test_shim_manager_creation() {
317 let temp_dir = TempDir::new().unwrap();
318 let env = VxEnvironment::new_with_base_dir(temp_dir.path()).unwrap();
319
320 let result = VxShimManager::new(env);
322 println!("Shim manager creation result: {:?}", result.is_ok());
324 }
325
326 #[test]
327 fn test_executable_name() {
328 let temp_dir = TempDir::new().unwrap();
329 let env = VxEnvironment::new_with_base_dir(temp_dir.path()).unwrap();
330
331 let shim_dir = temp_dir.path().join("shims");
333 fs::create_dir_all(&shim_dir).unwrap();
334
335 let manager = VxShimManager {
336 shim_manager: ShimManager::new(&shim_dir),
337 shim_dir: shim_dir.clone(),
338 shim_executable: shim_dir.join("vx-shim"),
339 environment: env,
340 };
341
342 if cfg!(windows) {
343 assert_eq!(manager.get_executable_name("node"), "node.exe");
344 } else {
345 assert_eq!(manager.get_executable_name("node"), "node");
346 }
347 }
348}