stynx_code_plugins/application/
installer.rs1use 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}