1use 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#[derive(Debug, Clone)]
16pub struct ShimManager {
17 pub shim_dir: PathBuf,
19 pub metadata_dir: Option<PathBuf>,
21}
22
23#[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#[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 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 pub fn path(mut self, path: impl Into<String>) -> Self {
66 self.path = Some(path.into());
67 self
68 }
69
70 pub fn args(mut self, args: Vec<String>) -> Self {
72 self.args = args;
73 self
74 }
75
76 pub fn arg(mut self, arg: impl Into<String>) -> Self {
78 self.args.push(arg.into());
79 self
80 }
81
82 pub fn cwd(mut self, cwd: impl Into<String>) -> Self {
84 self.cwd = Some(cwd.into());
85 self
86 }
87
88 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 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 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 pub fn source_type(mut self, source_type: SourceType) -> Self {
111 self.source_type = source_type;
112 self
113 }
114
115 pub fn description(mut self, description: impl Into<String>) -> Self {
117 self.metadata.description = Some(description.into());
118 self
119 }
120
121 pub fn version(mut self, version: impl Into<String>) -> Self {
123 self.metadata.version = Some(version.into());
124 self
125 }
126
127 pub fn author(mut self, author: impl Into<String>) -> Self {
129 self.metadata.author = Some(author.into());
130 self
131 }
132
133 pub fn tags(mut self, tags: Vec<String>) -> Self {
135 self.metadata.tags = tags;
136 self
137 }
138
139 pub fn tag(mut self, tag: impl Into<String>) -> Self {
141 self.metadata.tags.push(tag.into());
142 self
143 }
144
145 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 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 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 pub fn builder(&self, name: impl Into<String>) -> ShimBuilder {
196 ShimBuilder::new(name)
197 }
198
199 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 config.to_file(&config_path)?;
208
209 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 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 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 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 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 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 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 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 self.remove_shim(name)?;
334 self.create_shim(config)
335 }
336
337 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 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 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 fn copy_shimexe_binary(&self, dest_path: &Path) -> Result<()> {
374 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 #[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}