Skip to main content

ai_agent/utils/plugins/
installed_plugins_manager.rs

1// Source: ~/claudecode/openclaudecode/src/utils/plugins/installedPluginsManager.ts
2#![allow(dead_code)]
3
4use std::collections::HashMap;
5use std::path::{Path, PathBuf};
6use std::sync::Mutex;
7use std::time::SystemTime;
8
9use serde::{Deserialize, Serialize};
10use tokio::fs;
11
12use super::loader::{get_plugin_cache_path, get_versioned_cache_path};
13use super::plugin_directories::get_plugins_directory;
14use super::plugin_identifier::{parse_plugin_identifier, setting_source_to_scope};
15use super::schemas::{PluginInstallationEntry, PluginScope};
16
17/// Installed plugins file structure (V2 format).
18#[derive(Serialize, Deserialize, Debug, Clone, Default)]
19pub struct InstalledPluginsFileV2 {
20    pub version: u32,
21    pub plugins: HashMap<String, Vec<PluginInstallationEntry>>,
22}
23
24static MIGRATION_COMPLETED: Mutex<bool> = Mutex::new(false);
25static INSTALLED_PLUGINS_CACHE_V2: Mutex<Option<InstalledPluginsFileV2>> = Mutex::new(None);
26static IN_MEMORY_INSTALLED_PLUGINS: Mutex<Option<InstalledPluginsFileV2>> = Mutex::new(None);
27
28/// Get the path to the installed_plugins.json file.
29pub fn get_installed_plugins_file_path() -> PathBuf {
30    PathBuf::from(get_plugins_directory()).join("installed_plugins.json")
31}
32
33/// Clear the installed plugins cache.
34pub fn clear_installed_plugins_cache() {
35    let mut cache = INSTALLED_PLUGINS_CACHE_V2.lock().unwrap();
36    *cache = None;
37    let mut in_memory = IN_MEMORY_INSTALLED_PLUGINS.lock().unwrap();
38    *in_memory = None;
39    log::debug!("Cleared installed plugins cache");
40}
41
42/// Read raw file data from installed_plugins.json.
43fn read_installed_plugins_file_raw()
44-> Result<Option<(u64, serde_json::Value)>, Box<dyn std::error::Error + Send + Sync>> {
45    let file_path = get_installed_plugins_file_path();
46
47    let content = match std::fs::read_to_string(&file_path) {
48        Ok(c) => c,
49        Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
50        Err(e) => return Err(Box::new(e)),
51    };
52
53    let data: serde_json::Value = serde_json::from_str(&content)?;
54    let version = data.get("version").and_then(|v| v.as_u64()).unwrap_or(1);
55    Ok(Some((version, data)))
56}
57
58/// Load installed plugins in V2 format.
59pub fn load_installed_plugins_v2()
60-> Result<InstalledPluginsFileV2, Box<dyn std::error::Error + Send + Sync>> {
61    {
62        let cache = INSTALLED_PLUGINS_CACHE_V2.lock().unwrap();
63        if let Some(ref data) = *cache {
64            return Ok(data.clone());
65        }
66    }
67
68    let file_path = get_installed_plugins_file_path();
69
70    let result = match read_installed_plugins_file_raw() {
71        Ok(Some((2, data))) => {
72            let validated: InstalledPluginsFileV2 = serde_json::from_value(data)?;
73            validated
74        }
75        Ok(Some((1, data))) => migrate_v1_to_v2(&data)?,
76        Ok(Some((_version, _data))) => {
77            log::debug!(
78                "installed_plugins.json has unsupported version, returning empty V2 object"
79            );
80            InstalledPluginsFileV2 {
81                version: 2,
82                plugins: HashMap::new(),
83            }
84        }
85        Ok(None) => {
86            log::debug!("installed_plugins.json doesn't exist, returning empty V2 object");
87            InstalledPluginsFileV2 {
88                version: 2,
89                plugins: HashMap::new(),
90            }
91        }
92        Err(e) => {
93            log::debug!("Failed to read installed_plugins.json: {}", e);
94            InstalledPluginsFileV2 {
95                version: 2,
96                plugins: HashMap::new(),
97            }
98        }
99    };
100
101    {
102        let mut cache = INSTALLED_PLUGINS_CACHE_V2.lock().unwrap();
103        *cache = Some(result.clone());
104    }
105
106    Ok(result)
107}
108
109/// Migrate V1 data to V2 format.
110fn migrate_v1_to_v2(
111    _v1_data: &serde_json::Value,
112) -> Result<InstalledPluginsFileV2, Box<dyn std::error::Error + Send + Sync>> {
113    Ok(InstalledPluginsFileV2 {
114        version: 2,
115        plugins: HashMap::new(),
116    })
117}
118
119/// Save installed plugins in V2 format.
120fn save_installed_plugins_v2(
121    data: &InstalledPluginsFileV2,
122) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
123    let file_path = get_installed_plugins_file_path();
124
125    std::fs::create_dir_all(get_plugins_directory())?;
126
127    let json_content = serde_json::to_string_pretty(data)?;
128    std::fs::write(&file_path, json_content)?;
129
130    {
131        let mut cache = INSTALLED_PLUGINS_CACHE_V2.lock().unwrap();
132        *cache = Some(data.clone());
133    }
134
135    log::debug!(
136        "Saved {} installed plugins to {:?}",
137        data.plugins.len(),
138        file_path
139    );
140    Ok(())
141}
142
143/// Add or update a plugin installation entry.
144pub fn add_plugin_installation(
145    plugin_id: &str,
146    scope: PluginScope,
147    install_path: &str,
148    metadata: &PluginInstallationEntry,
149    project_path: Option<&str>,
150) {
151    let mut data = match load_installed_plugins_from_disk() {
152        Ok(d) => d,
153        Err(e) => {
154            log::error!("Failed to load installed plugins: {}", e);
155            return;
156        }
157    };
158
159    let installations = data.plugins.entry(plugin_id.to_string()).or_default();
160
161    let existing_index = installations
162        .iter()
163        .position(|entry| entry.scope == scope && entry.project_path.as_deref() == project_path);
164
165    let now = SystemTime::now()
166        .duration_since(SystemTime::UNIX_EPOCH)
167        .unwrap_or_default()
168        .as_millis();
169
170    let new_entry = PluginInstallationEntry {
171        scope: scope.clone(),
172        install_path: install_path.to_string(),
173        version: metadata.version.clone(),
174        installed_at: metadata.installed_at.clone(),
175        last_updated: now.to_string(),
176        git_commit_sha: metadata.git_commit_sha.clone(),
177        project_path: project_path.map(|s| s.to_string()),
178    };
179
180    if let Some(idx) = existing_index {
181        installations[idx] = new_entry;
182        log::debug!(
183            "Updated installation for {} at scope {:?}",
184            plugin_id,
185            scope
186        );
187    } else {
188        installations.push(new_entry);
189        log::debug!("Added installation for {} at scope {:?}", plugin_id, scope);
190    }
191
192    if let Err(e) = save_installed_plugins_v2(&data) {
193        log::error!("Failed to save installed plugins: {}", e);
194    }
195}
196
197/// Remove a plugin installation entry from a specific scope.
198pub fn remove_plugin_installation(plugin_id: &str, scope: PluginScope, project_path: Option<&str>) {
199    let mut data = match load_installed_plugins_from_disk() {
200        Ok(d) => d,
201        Err(_) => return,
202    };
203
204    if let Some(installations) = data.plugins.get_mut(plugin_id) {
205        installations.retain(|entry| {
206            !(entry.scope == scope && entry.project_path.as_deref() == project_path)
207        });
208
209        if installations.is_empty() {
210            data.plugins.remove(plugin_id);
211        }
212    }
213
214    let _ = save_installed_plugins_v2(&data);
215    log::debug!(
216        "Removed installation for {} at scope {:?}",
217        plugin_id,
218        scope
219    );
220}
221
222/// Load installed plugins directly from disk, bypassing all caches.
223pub fn load_installed_plugins_from_disk()
224-> Result<InstalledPluginsFileV2, Box<dyn std::error::Error + Send + Sync>> {
225    let file_path = get_installed_plugins_file_path();
226
227    let content = match std::fs::read_to_string(&file_path) {
228        Ok(c) => c,
229        Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
230            return Ok(InstalledPluginsFileV2 {
231                version: 2,
232                plugins: HashMap::new(),
233            });
234        }
235        Err(e) => return Err(e.into()),
236    };
237
238    let data: serde_json::Value = serde_json::from_str(&content)?;
239    let version = data.get("version").and_then(|v| v.as_u64()).unwrap_or(1);
240
241    if version == 2 {
242        let validated: InstalledPluginsFileV2 = serde_json::from_value(data)?;
243        Ok(validated)
244    } else {
245        migrate_v1_to_v2(&data)
246    }
247}
248
249/// Check if a plugin is installed.
250pub fn is_plugin_installed(plugin_id: &str) -> bool {
251    match load_installed_plugins_v2() {
252        Ok(data) => data.plugins.contains_key(plugin_id),
253        Err(_) => false,
254    }
255}
256
257/// Remove all plugin entries belonging to a specific marketplace.
258pub fn remove_all_plugins_for_marketplace(marketplace_name: &str) -> (Vec<String>, Vec<String>) {
259    if marketplace_name.is_empty() {
260        return (Vec::new(), Vec::new());
261    }
262
263    let mut data = match load_installed_plugins_from_disk() {
264        Ok(d) => d,
265        Err(_) => return (Vec::new(), Vec::new()),
266    };
267
268    let suffix = format!("@{}", marketplace_name);
269    let mut orphaned_paths = Vec::new();
270    let mut removed_plugin_ids = Vec::new();
271
272    let plugin_ids: Vec<String> = data.plugins.keys().cloned().collect();
273    for plugin_id in plugin_ids {
274        if !plugin_id.ends_with(&suffix) {
275            continue;
276        }
277
278        if let Some(entries) = data.plugins.remove(&plugin_id) {
279            for entry in entries {
280                orphaned_paths.push(entry.install_path);
281            }
282        }
283        removed_plugin_ids.push(plugin_id.clone());
284        log::debug!(
285            "Removed installed plugin for marketplace removal: {}",
286            plugin_id
287        );
288    }
289
290    if !removed_plugin_ids.is_empty() {
291        let _ = save_installed_plugins_v2(&data);
292    }
293
294    (orphaned_paths, removed_plugin_ids)
295}
296
297/// Get the in-memory installed plugins (session state).
298pub fn get_in_memory_installed_plugins() -> InstalledPluginsFileV2 {
299    let mut in_memory = IN_MEMORY_INSTALLED_PLUGINS.lock().unwrap();
300    if in_memory.is_none() {
301        *in_memory = load_installed_plugins_v2().ok();
302    }
303    in_memory.clone().unwrap_or_default()
304}
305
306/// Initialize the versioned plugins system.
307pub async fn initialize_versioned_plugins() -> Result<(), Box<dyn std::error::Error + Send + Sync>>
308{
309    migrate_to_single_plugin_file();
310
311    if let Err(e) = migrate_from_enabled_plugins().await {
312        log::error!("Failed to migrate from enabled plugins: {}", e);
313    }
314
315    let data = get_in_memory_installed_plugins();
316    log::debug!(
317        "Initialized versioned plugins system with {} plugins",
318        data.plugins.len()
319    );
320    Ok(())
321}
322
323fn migrate_to_single_plugin_file() {
324    let mut completed = MIGRATION_COMPLETED.lock().unwrap();
325    if *completed {
326        return;
327    }
328    *completed = true;
329}
330
331/// Migrate from enabledPlugins in settings to installed_plugins.json.
332pub async fn migrate_from_enabled_plugins() -> Result<(), Box<dyn std::error::Error + Send + Sync>>
333{
334    Ok(())
335}
336
337/// Details about a pending plugin update.
338pub struct PendingUpdateDetails {
339    pub plugin_id: String,
340    pub version: String,
341}
342
343/// Check if there are pending plugin updates.
344pub fn has_pending_updates() -> bool {
345    false
346}
347
348/// Get details about pending plugin updates.
349pub fn get_pending_updates_details() -> Vec<PendingUpdateDetails> {
350    Vec::new()
351}
352
353/// Check if a plugin installation is relevant to the current project.
354pub fn is_installation_relevant_to_current_project(_entry: &PluginInstallationEntry) -> bool {
355    true
356}