Skip to main content

stynx_code_plugins/application/
installer.rs

1use std::path::Path;
2
3use crate::application::loader::{PluginLoader, SubprocessPluginLoader};
4use crate::domain::plugin::{PluginId, PluginInfo};
5use stynx_code_errors::{AppError, AppResult};
6
7pub struct PluginInstaller;
8
9impl PluginInstaller {
10    pub fn new() -> Self {
11        Self
12    }
13
14    pub async fn install_from_path(&self, src: &Path, dest: &Path) -> AppResult<PluginInfo> {
15        if !src.exists() {
16            return Err(AppError::BadRequest(format!(
17                "Source plugin path does not exist: {}",
18                src.display()
19            )));
20        }
21
22        tokio::fs::create_dir_all(dest).await.map_err(|e| {
23            AppError::Internal(anyhow::anyhow!(
24                "Failed to create plugin destination {}: {e}",
25                dest.display()
26            ))
27        })?;
28
29        copy_dir_all(src, dest).await?;
30
31        let loader = SubprocessPluginLoader;
32        loader.load(dest).await
33    }
34
35    pub async fn uninstall(&self, id: &PluginId) -> AppResult<()> {
36        let plugins_dir = plugins_base_dir()?;
37        let plugin_path = plugins_dir.join(id.as_str());
38
39        if !plugin_path.exists() {
40            return Err(AppError::BadRequest(format!(
41                "Plugin '{}' is not installed",
42                id
43            )));
44        }
45
46        tokio::fs::remove_dir_all(&plugin_path).await.map_err(|e| {
47            AppError::Internal(anyhow::anyhow!(
48                "Failed to remove plugin directory {}: {e}",
49                plugin_path.display()
50            ))
51        })?;
52
53        Ok(())
54    }
55}
56
57impl Default for PluginInstaller {
58    fn default() -> Self {
59        Self::new()
60    }
61}
62
63fn plugins_base_dir() -> AppResult<std::path::PathBuf> {
64    let home = dirs_home()?;
65    Ok(home.join(".claude").join("plugins"))
66}
67
68fn dirs_home() -> AppResult<std::path::PathBuf> {
69    stynx_code_config::home_dir()
70        .ok_or_else(|| AppError::Internal(anyhow::anyhow!("cannot determine home directory")))
71}
72
73async fn copy_dir_all(src: &Path, dst: &Path) -> AppResult<()> {
74    let mut entries = tokio::fs::read_dir(src).await.map_err(|e| {
75        AppError::Internal(anyhow::anyhow!("Failed to read dir {}: {e}", src.display()))
76    })?;
77
78    while let Some(entry) = entries.next_entry().await.map_err(|e| {
79        AppError::Internal(anyhow::anyhow!("Failed to read dir entry: {e}"))
80    })? {
81        let src_path = entry.path();
82        let dst_path = dst.join(entry.file_name());
83        let file_type = entry.file_type().await.map_err(|e| {
84            AppError::Internal(anyhow::anyhow!("Failed to get file type: {e}"))
85        })?;
86
87        if file_type.is_dir() {
88            tokio::fs::create_dir_all(&dst_path).await.map_err(|e| {
89                AppError::Internal(anyhow::anyhow!(
90                    "Failed to create dir {}: {e}",
91                    dst_path.display()
92                ))
93            })?;
94            Box::pin(copy_dir_all(&src_path, &dst_path)).await?;
95        } else {
96            tokio::fs::copy(&src_path, &dst_path).await.map_err(|e| {
97                AppError::Internal(anyhow::anyhow!(
98                    "Failed to copy {} -> {}: {e}",
99                    src_path.display(),
100                    dst_path.display()
101                ))
102            })?;
103        }
104    }
105
106    Ok(())
107}