ai_agent/utils/plugins/
zip_cache.rs1#![allow(dead_code)]
3
4use std::path::{Path, PathBuf};
5
6use super::schemas::MarketplaceSource;
7
8pub fn is_plugin_zip_cache_enabled() -> bool {
10 std::env::var("CLAUDE_CODE_PLUGIN_USE_ZIP_CACHE")
11 .map(|v| v == "1" || v.to_lowercase() == "true")
12 .unwrap_or(false)
13}
14
15pub fn get_plugin_zip_cache_path() -> Option<String> {
17 if !is_plugin_zip_cache_enabled() {
18 return None;
19 }
20 std::env::var("CLAUDE_CODE_PLUGIN_CACHE_DIR")
21 .ok()
22 .map(|dir| {
23 if dir.starts_with("~/") {
24 dirs::home_dir()
25 .map(|h| format!("{}{}", h.display(), &dir[1..]))
26 .unwrap_or(dir)
27 } else {
28 dir
29 }
30 })
31}
32
33pub fn get_zip_cache_known_marketplaces_path()
35-> Result<String, Box<dyn std::error::Error + Send + Sync>> {
36 let cache_path =
37 get_plugin_zip_cache_path().ok_or_else(|| "Plugin zip cache is not enabled".to_string())?;
38 Ok(PathBuf::from(cache_path)
39 .join("known_marketplaces.json")
40 .to_string_lossy()
41 .to_string())
42}
43
44pub fn get_zip_cache_installed_plugins_path()
46-> Result<String, Box<dyn std::error::Error + Send + Sync>> {
47 let cache_path =
48 get_plugin_zip_cache_path().ok_or_else(|| "Plugin zip cache is not enabled".to_string())?;
49 Ok(PathBuf::from(cache_path)
50 .join("installed_plugins.json")
51 .to_string_lossy()
52 .to_string())
53}
54
55pub fn get_zip_cache_marketplaces_dir() -> Result<String, Box<dyn std::error::Error + Send + Sync>>
57{
58 let cache_path =
59 get_plugin_zip_cache_path().ok_or_else(|| "Plugin zip cache is not enabled".to_string())?;
60 Ok(PathBuf::from(cache_path)
61 .join("marketplaces")
62 .to_string_lossy()
63 .to_string())
64}
65
66pub fn get_zip_cache_plugins_dir() -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
68 let cache_path =
69 get_plugin_zip_cache_path().ok_or_else(|| "Plugin zip cache is not enabled".to_string())?;
70 Ok(PathBuf::from(cache_path)
71 .join("plugins")
72 .to_string_lossy()
73 .to_string())
74}
75
76static SESSION_PLUGIN_CACHE_PATH: once_cell::sync::Lazy<std::sync::Mutex<Option<String>>> =
78 once_cell::sync::Lazy::new(|| std::sync::Mutex::new(None));
79
80pub async fn get_session_plugin_cache_path()
82-> Result<String, Box<dyn std::error::Error + Send + Sync>> {
83 {
84 let guard = SESSION_PLUGIN_CACHE_PATH.lock().unwrap();
85 if let Some(ref path) = *guard {
86 return Ok(path.clone());
87 }
88 }
89
90 let suffix = hex::encode(rand::random::<[u8; 8]>());
91 let dir = PathBuf::from(std::env::temp_dir()).join(format!("claude-plugin-session-{}", suffix));
92
93 tokio::fs::create_dir_all(&dir).await?;
94
95 let path_str = dir.to_string_lossy().to_string();
96 {
97 let mut guard = SESSION_PLUGIN_CACHE_PATH.lock().unwrap();
98 *guard = Some(path_str.clone());
99 }
100
101 log::debug!("Created session plugin cache at {}", path_str);
102 Ok(path_str)
103}
104
105pub async fn cleanup_session_plugin_cache() -> Result<(), Box<dyn std::error::Error + Send + Sync>>
107{
108 let path = {
109 let mut guard = SESSION_PLUGIN_CACHE_PATH.lock().unwrap();
110 guard.take()
111 };
112 if let Some(path) = path {
113 if let Err(e) = tokio::fs::remove_dir_all(&path).await {
114 log::debug!("Failed to clean up session plugin cache at {}: {}", path, e);
115 } else {
116 log::debug!("Cleaned up session plugin cache at {}", path);
117 }
118 }
119 Ok(())
120}
121
122pub async fn atomic_write_to_zip_cache(
124 target_path: &str,
125 data: &[u8],
126) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
127 let dir = Path::new(target_path)
128 .parent()
129 .ok_or_else(|| "Invalid target path".to_string())?;
130 tokio::fs::create_dir_all(dir).await?;
131
132 let file_name = Path::new(target_path)
133 .file_name()
134 .map(|n| n.to_string_lossy())
135 .unwrap_or_default();
136
137 let tmp_name = format!(
138 ".{}.tmp.{}",
139 file_name,
140 hex::encode(rand::random::<[u8; 4]>())
141 );
142 let tmp_path = dir.join(&tmp_name);
143
144 tokio::fs::write(&tmp_path, data).await?;
145 tokio::fs::rename(&tmp_path, target_path).await?;
146
147 Ok(())
148}
149
150pub async fn create_zip_from_directory(_source_dir: &Path) -> Result<Vec<u8>, String> {
152 Err("create_zip_from_directory not implemented - add `zip` crate".to_string())
153}
154
155pub async fn extract_zip_to_directory(
157 _zip_path: &str,
158 _target_dir: &str,
159) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
160 Err("extract_zip_to_directory not implemented - add `zip` crate".into())
161}
162
163pub async fn convert_directory_to_zip_in_place(
165 dir_path: &str,
166 zip_path: &str,
167) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
168 let zip_data = create_zip_from_directory(Path::new(dir_path)).await?;
169 atomic_write_to_zip_cache(zip_path, &zip_data).await?;
170 let _ = tokio::fs::remove_dir_all(dir_path).await;
171 Ok(())
172}
173
174pub fn get_marketplace_json_relative_path(marketplace_name: &str) -> String {
176 let sanitized = marketplace_name
177 .chars()
178 .map(|c| {
179 if c.is_alphanumeric() || c == '-' || c == '_' {
180 c
181 } else {
182 '-'
183 }
184 })
185 .collect::<String>();
186 format!("marketplaces/{}.json", sanitized)
187}
188
189pub fn is_marketplace_source_supported_by_zip_cache(source: &MarketplaceSource) -> bool {
191 matches!(
192 source,
193 MarketplaceSource::Github { .. }
194 | MarketplaceSource::Git { .. }
195 | MarketplaceSource::Url { .. }
196 | MarketplaceSource::Settings { .. }
197 )
198}