shimexe_core/
manager.rs

1//! High-level shim management API for tool managers like vx, rye, etc.
2
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5use std::fs;
6use std::path::{Path, PathBuf};
7use tracing::info;
8
9use crate::config::{ShimConfig, ShimCore, ShimMetadata, SourceType};
10use crate::downloader::Downloader;
11use crate::error::{Result, ShimError};
12use crate::runner::ShimRunner;
13
14/// High-level shim manager for tool managers
15#[derive(Debug, Clone)]
16pub struct ShimManager {
17    /// Directory where shims are stored
18    pub shim_dir: PathBuf,
19    /// Optional metadata directory for tool manager specific data
20    pub metadata_dir: Option<PathBuf>,
21}
22
23/// Shim information for listing and management
24#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct ShimInfo {
26    pub name: String,
27    pub path: String,
28    pub source_type: SourceType,
29    pub download_url: Option<String>,
30    pub version: Option<String>,
31    pub description: Option<String>,
32    pub tags: Vec<String>,
33    pub is_valid: bool,
34}
35
36/// Builder for creating shims with a fluent API
37#[derive(Debug, Clone)]
38pub struct ShimBuilder {
39    name: String,
40    path: Option<String>,
41    args: Vec<String>,
42    env: HashMap<String, String>,
43    cwd: Option<String>,
44    download_url: Option<String>,
45    source_type: SourceType,
46    metadata: ShimMetadata,
47}
48
49impl ShimBuilder {
50    /// Create a new shim builder
51    pub fn new(name: impl Into<String>) -> Self {
52        Self {
53            name: name.into(),
54            path: None,
55            args: Vec::new(),
56            env: HashMap::new(),
57            cwd: None,
58            download_url: None,
59            source_type: SourceType::File,
60            metadata: ShimMetadata::default(),
61        }
62    }
63
64    /// Set the executable path
65    pub fn path(mut self, path: impl Into<String>) -> Self {
66        self.path = Some(path.into());
67        self
68    }
69
70    /// Add arguments
71    pub fn args(mut self, args: Vec<String>) -> Self {
72        self.args = args;
73        self
74    }
75
76    /// Add a single argument
77    pub fn arg(mut self, arg: impl Into<String>) -> Self {
78        self.args.push(arg.into());
79        self
80    }
81
82    /// Set working directory
83    pub fn cwd(mut self, cwd: impl Into<String>) -> Self {
84        self.cwd = Some(cwd.into());
85        self
86    }
87
88    /// Add environment variable
89    pub fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
90        self.env.insert(key.into(), value.into());
91        self
92    }
93
94    /// Set download URL for HTTP-based shims
95    pub fn download_url(mut self, url: impl Into<String>) -> Self {
96        let url = url.into();
97        self.download_url = Some(url.clone());
98
99        // Auto-detect source type based on URL
100        if url.ends_with(".zip") || url.ends_with(".tar.gz") || url.ends_with(".tgz") {
101            self.source_type = SourceType::Archive;
102        } else if Downloader::is_url(&url) {
103            self.source_type = SourceType::Url;
104        }
105
106        self
107    }
108
109    /// Set source type explicitly
110    pub fn source_type(mut self, source_type: SourceType) -> Self {
111        self.source_type = source_type;
112        self
113    }
114
115    /// Set description
116    pub fn description(mut self, description: impl Into<String>) -> Self {
117        self.metadata.description = Some(description.into());
118        self
119    }
120
121    /// Set version
122    pub fn version(mut self, version: impl Into<String>) -> Self {
123        self.metadata.version = Some(version.into());
124        self
125    }
126
127    /// Set author
128    pub fn author(mut self, author: impl Into<String>) -> Self {
129        self.metadata.author = Some(author.into());
130        self
131    }
132
133    /// Add tags
134    pub fn tags(mut self, tags: Vec<String>) -> Self {
135        self.metadata.tags = tags;
136        self
137    }
138
139    /// Add a single tag
140    pub fn tag(mut self, tag: impl Into<String>) -> Self {
141        self.metadata.tags.push(tag.into());
142        self
143    }
144
145    /// Build the shim configuration
146    pub fn build(self) -> Result<ShimConfig> {
147        let path = self
148            .path
149            .ok_or_else(|| ShimError::Config("Shim path is required".to_string()))?;
150
151        let config = ShimConfig {
152            shim: ShimCore {
153                name: self.name,
154                path,
155                args: self.args,
156                cwd: self.cwd,
157                download_url: self.download_url,
158                source_type: self.source_type,
159                extracted_executables: Vec::new(),
160            },
161            args: Default::default(),
162            env: self.env,
163            metadata: self.metadata,
164            auto_update: None,
165        };
166
167        config.validate()?;
168        Ok(config)
169    }
170}
171
172impl ShimManager {
173    /// Create a new shim manager
174    pub fn new(shim_dir: PathBuf) -> Result<Self> {
175        fs::create_dir_all(&shim_dir)?;
176
177        Ok(Self {
178            shim_dir,
179            metadata_dir: None,
180        })
181    }
182
183    /// Create a new shim manager with custom metadata directory
184    pub fn with_metadata_dir(shim_dir: PathBuf, metadata_dir: PathBuf) -> Result<Self> {
185        fs::create_dir_all(&shim_dir)?;
186        fs::create_dir_all(&metadata_dir)?;
187
188        Ok(Self {
189            shim_dir,
190            metadata_dir: Some(metadata_dir),
191        })
192    }
193
194    /// Create a new shim builder
195    pub fn builder(&self, name: impl Into<String>) -> ShimBuilder {
196        ShimBuilder::new(name)
197    }
198
199    /// Create a shim from configuration
200    pub fn create_shim(&self, config: ShimConfig) -> Result<PathBuf> {
201        let config_path = self
202            .shim_dir
203            .join(format!("{}.shim.toml", config.shim.name));
204        let shim_path = self.get_shim_executable_path(&config.shim.name);
205
206        // Save configuration
207        config.to_file(&config_path)?;
208
209        // Copy shimexe binary as the shim executable
210        self.copy_shimexe_binary(&shim_path)?;
211
212        info!(
213            "Created shim '{}' at {}",
214            config.shim.name,
215            shim_path.display()
216        );
217        Ok(shim_path)
218    }
219
220    /// Create a shim using the builder pattern
221    pub fn create_shim_with_builder<F>(
222        &self,
223        name: impl Into<String>,
224        builder_fn: F,
225    ) -> Result<PathBuf>
226    where
227        F: FnOnce(ShimBuilder) -> ShimBuilder,
228    {
229        let builder = self.builder(name);
230        let config = builder_fn(builder).build()?;
231        self.create_shim(config)
232    }
233
234    /// Remove a shim
235    pub fn remove_shim(&self, name: &str) -> Result<()> {
236        let config_path = self.shim_dir.join(format!("{}.shim.toml", name));
237        let shim_path = self.get_shim_executable_path(name);
238
239        // Remove files
240        if config_path.exists() {
241            fs::remove_file(&config_path)?;
242        }
243        if shim_path.exists() {
244            fs::remove_file(&shim_path)?;
245        }
246
247        // Remove metadata if exists
248        if let Some(ref metadata_dir) = self.metadata_dir {
249            let metadata_path = metadata_dir.join(format!("{}.json", name));
250            if metadata_path.exists() {
251                fs::remove_file(&metadata_path)?;
252            }
253        }
254
255        info!("Removed shim '{}'", name);
256        Ok(())
257    }
258
259    /// List all shims
260    pub fn list_shims(&self) -> Result<Vec<ShimInfo>> {
261        let mut shims = Vec::new();
262
263        if !self.shim_dir.exists() {
264            return Ok(shims);
265        }
266
267        for entry in fs::read_dir(&self.shim_dir)? {
268            let entry = entry?;
269            let path = entry.path();
270
271            if let Some(extension) = path.extension() {
272                if extension == "toml"
273                    && path
274                        .file_stem()
275                        .and_then(|s| s.to_str())
276                        .map(|s| s.ends_with(".shim"))
277                        .unwrap_or(false)
278                {
279                    if let Ok(config) = ShimConfig::from_file(&path) {
280                        let shim_path = self.get_shim_executable_path(&config.shim.name);
281                        let is_valid = shim_path.exists();
282
283                        shims.push(ShimInfo {
284                            name: config.shim.name.clone(),
285                            path: config.shim.path.clone(),
286                            source_type: config.shim.source_type.clone(),
287                            download_url: config.shim.download_url.clone(),
288                            version: config.metadata.version.clone(),
289                            description: config.metadata.description.clone(),
290                            tags: config.metadata.tags.clone(),
291                            is_valid,
292                        });
293                    }
294                }
295            }
296        }
297
298        shims.sort_by(|a, b| a.name.cmp(&b.name));
299        Ok(shims)
300    }
301
302    /// Get shim information by name
303    pub fn get_shim(&self, name: &str) -> Result<Option<ShimInfo>> {
304        let config_path = self.shim_dir.join(format!("{}.shim.toml", name));
305
306        if !config_path.exists() {
307            return Ok(None);
308        }
309
310        let config = ShimConfig::from_file(&config_path)?;
311        let shim_path = self.get_shim_executable_path(name);
312        let is_valid = shim_path.exists();
313
314        Ok(Some(ShimInfo {
315            name: config.shim.name.clone(),
316            path: config.shim.path.clone(),
317            source_type: config.shim.source_type.clone(),
318            download_url: config.shim.download_url.clone(),
319            version: config.metadata.version.clone(),
320            description: config.metadata.description.clone(),
321            tags: config.metadata.tags.clone(),
322            is_valid,
323        }))
324    }
325
326    /// Update an existing shim
327    pub fn update_shim(&self, name: &str, config: ShimConfig) -> Result<PathBuf> {
328        if self.get_shim(name)?.is_none() {
329            return Err(ShimError::Config(format!("Shim '{}' does not exist", name)));
330        }
331
332        // Remove old shim and create new one
333        self.remove_shim(name)?;
334        self.create_shim(config)
335    }
336
337    /// Execute a shim
338    pub fn execute_shim(&self, name: &str, args: &[String]) -> Result<i32> {
339        let config_path = self.shim_dir.join(format!("{}.shim.toml", name));
340
341        if !config_path.exists() {
342            return Err(ShimError::Config(format!("Shim '{}' not found", name)));
343        }
344
345        let runner = ShimRunner::from_file(&config_path)?;
346        runner.execute(args)
347    }
348
349    /// Validate a shim
350    pub fn validate_shim(&self, name: &str) -> Result<bool> {
351        let config_path = self.shim_dir.join(format!("{}.shim.toml", name));
352
353        if !config_path.exists() {
354            return Ok(false);
355        }
356
357        match ShimRunner::from_file(&config_path) {
358            Ok(runner) => Ok(runner.validate().is_ok()),
359            Err(_) => Ok(false),
360        }
361    }
362
363    /// Get the path to the shim executable
364    fn get_shim_executable_path(&self, name: &str) -> PathBuf {
365        if cfg!(windows) {
366            self.shim_dir.join(format!("{}.exe", name))
367        } else {
368            self.shim_dir.join(name)
369        }
370    }
371
372    /// Copy shimexe binary to create the shim executable
373    fn copy_shimexe_binary(&self, dest_path: &Path) -> Result<()> {
374        // Try to find shimexe binary
375        let shimexe_path = which::which("shimexe")
376            .or_else(|_| std::env::current_exe())
377            .map_err(|_| ShimError::Config("Could not find shimexe binary".to_string()))?;
378
379        fs::copy(&shimexe_path, dest_path)?;
380
381        // Make executable on Unix systems
382        #[cfg(unix)]
383        {
384            use std::os::unix::fs::PermissionsExt;
385            let mut perms = fs::metadata(dest_path)?.permissions();
386            perms.set_mode(0o755);
387            fs::set_permissions(dest_path, perms)?;
388        }
389
390        Ok(())
391    }
392}