vx_core/
shim_integration.rs

1//! Shim integration for vx tool manager
2//!
3//! This module provides integration between vx-core and vx-shim,
4//! enabling seamless tool version switching through shim technology.
5
6use crate::{Result, VxEnvironment, VxError};
7use anyhow::Context;
8use std::fs;
9use std::path::{Path, PathBuf};
10use vx_shim::ShimManager;
11
12/// Manages shim creation and tool version switching for vx
13pub struct VxShimManager {
14    /// The shim manager instance
15    shim_manager: ShimManager,
16    /// Path to the shim directory
17    shim_dir: PathBuf,
18    /// Path to the vx-shim executable
19    shim_executable: PathBuf,
20    /// Environment instance for configuration
21    #[allow(dead_code)]
22    environment: VxEnvironment,
23}
24
25impl VxShimManager {
26    /// Create a new VxShimManager
27    pub fn new(environment: VxEnvironment) -> Result<Self> {
28        let shim_dir = environment.shim_dir()?;
29
30        // Ensure shim directory exists
31        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        // Find or build vx-shim executable
37        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    /// Create a shim for a tool version
48    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        // Create shim configuration file
56        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        // Create the actual shim executable
62        let shim_executable_path = self.create_shim_executable(tool_name)?;
63
64        // Store metadata about this shim
65        self.store_shim_metadata(tool_name, version, &shim_config_path, &shim_executable_path)?;
66
67        Ok(shim_executable_path)
68    }
69
70    /// Switch tool version by updating shim configuration
71    pub fn switch_tool_version(
72        &self,
73        tool_name: &str,
74        new_version: &str,
75        tool_path: &Path,
76    ) -> Result<()> {
77        // Check if shim already exists
78        let shim_executable_path = self.shim_dir.join(self.get_executable_name(tool_name));
79
80        if !shim_executable_path.exists() {
81            // Create new shim if it doesn't exist
82            self.create_tool_shim(tool_name, tool_path, new_version, None)?;
83        } else {
84            // Update existing shim configuration
85            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    /// List all managed shims
98    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    /// Remove a shim
106    pub fn remove_shim(&self, tool_name: &str) -> Result<()> {
107        // Remove shim configuration
108        self.shim_manager
109            .remove_shim(tool_name)
110            .with_context(|| format!("Failed to remove shim config for {}", tool_name))?;
111
112        // Remove shim executable
113        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        // Remove metadata
124        self.remove_shim_metadata(tool_name)?;
125
126        Ok(())
127    }
128
129    /// Get the current version for a tool shim
130    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    /// Get shim directory path
136    pub fn shim_dir(&self) -> &Path {
137        &self.shim_dir
138    }
139
140    /// Create the actual shim executable by copying vx-shim
141    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        // Copy vx-shim executable to create the tool shim
145        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        // Make executable on Unix systems
154        #[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    /// Update shim configuration
166    fn update_shim_config(
167        &self,
168        tool_name: &str,
169        tool_path: &Path,
170        args: Option<&str>,
171    ) -> Result<()> {
172        // Remove existing shim config and create new one
173        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    /// Get platform-specific executable name
181    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    /// Get shim config file path
190    fn get_shim_config_path(&self, tool_name: &str) -> PathBuf {
191        self.shim_dir.join(format!("{}.shim", tool_name))
192    }
193
194    /// Find the vx-shim executable
195    fn find_shim_executable(environment: &VxEnvironment) -> Result<PathBuf> {
196        // First, try to find vx-shim in the same directory as vx
197        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        // Try to find in vx bin directory
211        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        // Try to find in PATH
222        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    /// Store metadata about a shim
233    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    /// Load metadata for a shim
260    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    /// Remove metadata for a shim
278    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    /// Get metadata file path for a tool
294    fn get_metadata_path(&self, tool_name: &str) -> PathBuf {
295        self.shim_dir.join(format!("{}.meta", tool_name))
296    }
297}
298
299/// Metadata stored for each shim
300#[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        // This test might fail if vx-shim is not available, which is expected
321        let result = VxShimManager::new(env);
322        // We don't assert success here because vx-shim might not be available in test environment
323        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        // Create a mock shim manager for testing
332        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}