Skip to main content

ai_agent/utils/plugins/
zip_cache.rs

1// Source: ~/claudecode/openclaudecode/src/utils/plugins/zipCache.ts
2#![allow(dead_code)]
3
4use std::path::{Path, PathBuf};
5
6use super::schemas::MarketplaceSource;
7
8/// Check if the plugin zip cache mode is enabled.
9pub 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
15/// Get the path to the zip cache directory.
16pub 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
33/// Get the path to known_marketplaces.json in the zip cache.
34pub 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
44/// Get the path to installed_plugins.json in the zip cache.
45pub 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
55/// Get the marketplaces directory within the zip cache.
56pub 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
66/// Get the plugins directory within the zip cache.
67pub 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
76/// Session plugin cache: a temp directory on local disk.
77static 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
80/// Get or create the session plugin cache directory.
81pub 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
105/// Clean up the session plugin cache directory.
106pub 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
122/// Write data to a file in the zip cache atomically.
123pub 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
150/// Create a ZIP archive from a directory.
151pub 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
155/// Extract a ZIP file to a target directory.
156pub 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
163/// Convert a plugin directory to a ZIP in-place.
164pub 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
174/// Get the relative path for a marketplace JSON file within the zip cache.
175pub 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
189/// Check if a marketplace source type is supported by zip cache mode.
190pub 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}