Skip to main content

codineer_plugins/
manager.rs

1use std::collections::BTreeSet;
2use std::fs;
3use std::path::PathBuf;
4
5use serde_json::Value;
6
7use crate::constants::{
8    BUNDLED_MARKETPLACE, EXTERNAL_MARKETPLACE, REGISTRY_FILE_NAME, SETTINGS_FILE_NAME,
9};
10use crate::definition::{builtin_plugins, load_plugin_definition};
11use crate::error::PluginError;
12use crate::install::{
13    copy_dir_all, describe_install_source, discover_plugin_dirs, ensure_object, materialize_source,
14    parse_install_source, plugin_id, resolve_local_source, sanitize_plugin_id, unix_time_ms,
15    update_settings_json,
16};
17use crate::manifest::{load_plugin_from_directory, plugin_manifest_path};
18use crate::types::{
19    InstallOutcome, InstalledPluginRecord, InstalledPluginRegistry, Plugin, PluginDefinition,
20    PluginHooks, PluginInstallSource, PluginKind, PluginManager, PluginManagerConfig,
21    PluginManifest, PluginMetadata, PluginRegistry, PluginSummary, PluginTool, RegisteredPlugin,
22    UpdateOutcome,
23};
24
25impl PluginManager {
26    #[must_use]
27    pub fn new(config: PluginManagerConfig) -> Self {
28        Self { config }
29    }
30
31    #[must_use]
32    pub fn bundled_root() -> PathBuf {
33        PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("bundled")
34    }
35
36    #[must_use]
37    pub fn install_root(&self) -> PathBuf {
38        self.config
39            .install_root
40            .clone()
41            .unwrap_or_else(|| self.config.config_home.join("plugins").join("installed"))
42    }
43
44    #[must_use]
45    pub fn registry_path(&self) -> PathBuf {
46        self.config.registry_path.clone().unwrap_or_else(|| {
47            self.config
48                .config_home
49                .join("plugins")
50                .join(REGISTRY_FILE_NAME)
51        })
52    }
53
54    #[must_use]
55    pub fn settings_path(&self) -> PathBuf {
56        self.config.config_home.join(SETTINGS_FILE_NAME)
57    }
58
59    pub fn plugin_registry(&self) -> Result<PluginRegistry, PluginError> {
60        Ok(PluginRegistry::new(
61            self.discover_plugins()?
62                .into_iter()
63                .map(|plugin| {
64                    let enabled = self.is_enabled(plugin.metadata());
65                    RegisteredPlugin::new(plugin, enabled)
66                })
67                .collect(),
68        ))
69    }
70
71    pub fn list_plugins(&self) -> Result<Vec<PluginSummary>, PluginError> {
72        Ok(self.plugin_registry()?.summaries())
73    }
74
75    pub fn list_installed_plugins(&self) -> Result<Vec<PluginSummary>, PluginError> {
76        Ok(self.installed_plugin_registry()?.summaries())
77    }
78
79    pub fn discover_plugins(&self) -> Result<Vec<PluginDefinition>, PluginError> {
80        self.sync_bundled_plugins()?;
81        let mut plugins = builtin_plugins();
82        plugins.extend(self.discover_installed_plugins()?);
83        plugins.extend(self.discover_external_directory_plugins(&plugins)?);
84        Ok(plugins)
85    }
86
87    pub fn aggregated_hooks(&self) -> Result<PluginHooks, PluginError> {
88        self.plugin_registry()?.aggregated_hooks()
89    }
90
91    pub fn aggregated_tools(&self) -> Result<Vec<PluginTool>, PluginError> {
92        self.plugin_registry()?.aggregated_tools()
93    }
94
95    pub fn validate_plugin_source(&self, source: &str) -> Result<PluginManifest, PluginError> {
96        let path = resolve_local_source(source)?;
97        load_plugin_from_directory(&path)
98    }
99
100    pub fn install(&mut self, source: &str) -> Result<InstallOutcome, PluginError> {
101        let install_source = parse_install_source(source)?;
102        let temp_root = self.install_root().join(".tmp");
103        let staged_source = materialize_source(&install_source, &temp_root)?;
104        let cleanup_source = matches!(install_source, PluginInstallSource::GitUrl { .. });
105        let manifest = load_plugin_from_directory(&staged_source)?;
106
107        let plugin_id = plugin_id(&manifest.name, EXTERNAL_MARKETPLACE);
108        let install_path = self.install_root().join(sanitize_plugin_id(&plugin_id));
109        if install_path.exists() {
110            fs::remove_dir_all(&install_path)?;
111        }
112        copy_dir_all(&staged_source, &install_path)?;
113        if cleanup_source {
114            let _ = fs::remove_dir_all(&staged_source);
115        }
116
117        let now = unix_time_ms();
118        let record = InstalledPluginRecord {
119            kind: PluginKind::External,
120            id: plugin_id.clone(),
121            name: manifest.name,
122            version: manifest.version.clone(),
123            description: manifest.description,
124            install_path: install_path.clone(),
125            source: install_source,
126            installed_at_unix_ms: now,
127            updated_at_unix_ms: now,
128        };
129
130        let mut registry = self.load_registry()?;
131        registry.plugins.insert(plugin_id.clone(), record);
132        self.store_registry(&registry)?;
133        self.write_enabled_state(&plugin_id, Some(true))?;
134        self.config.enabled_plugins.insert(plugin_id.clone(), true);
135
136        Ok(InstallOutcome {
137            plugin_id,
138            version: manifest.version,
139            install_path,
140        })
141    }
142
143    pub fn enable(&mut self, plugin_id: &str) -> Result<(), PluginError> {
144        self.ensure_known_plugin(plugin_id)?;
145        self.write_enabled_state(plugin_id, Some(true))?;
146        self.config
147            .enabled_plugins
148            .insert(plugin_id.to_string(), true);
149        Ok(())
150    }
151
152    pub fn disable(&mut self, plugin_id: &str) -> Result<(), PluginError> {
153        self.ensure_known_plugin(plugin_id)?;
154        self.write_enabled_state(plugin_id, Some(false))?;
155        self.config
156            .enabled_plugins
157            .insert(plugin_id.to_string(), false);
158        Ok(())
159    }
160
161    pub fn uninstall(&mut self, plugin_id: &str) -> Result<(), PluginError> {
162        let mut registry = self.load_registry()?;
163        let record = registry.plugins.remove(plugin_id).ok_or_else(|| {
164            PluginError::NotFound(format!("plugin `{plugin_id}` is not installed"))
165        })?;
166        if record.kind == PluginKind::Bundled {
167            registry.plugins.insert(plugin_id.to_string(), record);
168            return Err(PluginError::CommandFailed(format!(
169                "plugin `{plugin_id}` is bundled and managed automatically; disable it instead"
170            )));
171        }
172        if record.install_path.exists() {
173            fs::remove_dir_all(&record.install_path)?;
174        }
175        self.store_registry(&registry)?;
176        self.write_enabled_state(plugin_id, None)?;
177        self.config.enabled_plugins.remove(plugin_id);
178        Ok(())
179    }
180
181    pub fn update(&mut self, plugin_id: &str) -> Result<UpdateOutcome, PluginError> {
182        let mut registry = self.load_registry()?;
183        let record = registry.plugins.get(plugin_id).cloned().ok_or_else(|| {
184            PluginError::NotFound(format!("plugin `{plugin_id}` is not installed"))
185        })?;
186
187        let temp_root = self.install_root().join(".tmp");
188        let staged_source = materialize_source(&record.source, &temp_root)?;
189        let cleanup_source = matches!(record.source, PluginInstallSource::GitUrl { .. });
190        let manifest = load_plugin_from_directory(&staged_source)?;
191
192        if record.install_path.exists() {
193            fs::remove_dir_all(&record.install_path)?;
194        }
195        copy_dir_all(&staged_source, &record.install_path)?;
196        if cleanup_source {
197            let _ = fs::remove_dir_all(&staged_source);
198        }
199
200        let updated_record = InstalledPluginRecord {
201            version: manifest.version.clone(),
202            description: manifest.description,
203            updated_at_unix_ms: unix_time_ms(),
204            ..record.clone()
205        };
206        registry
207            .plugins
208            .insert(plugin_id.to_string(), updated_record);
209        self.store_registry(&registry)?;
210
211        Ok(UpdateOutcome {
212            plugin_id: plugin_id.to_string(),
213            old_version: record.version,
214            new_version: manifest.version,
215            install_path: record.install_path,
216        })
217    }
218
219    fn discover_installed_plugins(&self) -> Result<Vec<PluginDefinition>, PluginError> {
220        let mut registry = self.load_registry()?;
221        let mut plugins = Vec::new();
222        let mut seen_ids = BTreeSet::<String>::new();
223        let mut seen_paths = BTreeSet::<PathBuf>::new();
224        let mut stale_registry_ids = Vec::new();
225
226        for install_path in discover_plugin_dirs(&self.install_root())? {
227            let matched_record = registry
228                .plugins
229                .values()
230                .find(|record| record.install_path == install_path);
231            let kind = matched_record.map_or(PluginKind::External, |record| record.kind);
232            let source = matched_record.map_or_else(
233                || install_path.display().to_string(),
234                |record| describe_install_source(&record.source),
235            );
236            let plugin = load_plugin_definition(&install_path, kind, source, kind.marketplace())?;
237            if seen_ids.insert(plugin.metadata().id.clone()) {
238                seen_paths.insert(install_path);
239                plugins.push(plugin);
240            }
241        }
242
243        for record in registry.plugins.values() {
244            if seen_paths.contains(&record.install_path) {
245                continue;
246            }
247            if !record.install_path.exists() || plugin_manifest_path(&record.install_path).is_err()
248            {
249                stale_registry_ids.push(record.id.clone());
250                continue;
251            }
252            let plugin = load_plugin_definition(
253                &record.install_path,
254                record.kind,
255                describe_install_source(&record.source),
256                record.kind.marketplace(),
257            )?;
258            if seen_ids.insert(plugin.metadata().id.clone()) {
259                seen_paths.insert(record.install_path.clone());
260                plugins.push(plugin);
261            }
262        }
263
264        if !stale_registry_ids.is_empty() {
265            for plugin_id in stale_registry_ids {
266                registry.plugins.remove(&plugin_id);
267            }
268            self.store_registry(&registry)?;
269        }
270
271        Ok(plugins)
272    }
273
274    fn discover_external_directory_plugins(
275        &self,
276        existing_plugins: &[PluginDefinition],
277    ) -> Result<Vec<PluginDefinition>, PluginError> {
278        let mut plugins = Vec::new();
279
280        for directory in &self.config.external_dirs {
281            for root in discover_plugin_dirs(directory)? {
282                let plugin = load_plugin_definition(
283                    &root,
284                    PluginKind::External,
285                    root.display().to_string(),
286                    EXTERNAL_MARKETPLACE,
287                )?;
288                if existing_plugins
289                    .iter()
290                    .chain(plugins.iter())
291                    .all(|existing| existing.metadata().id != plugin.metadata().id)
292                {
293                    plugins.push(plugin);
294                }
295            }
296        }
297
298        Ok(plugins)
299    }
300
301    fn installed_plugin_registry(&self) -> Result<PluginRegistry, PluginError> {
302        self.sync_bundled_plugins()?;
303        Ok(PluginRegistry::new(
304            self.discover_installed_plugins()?
305                .into_iter()
306                .map(|plugin| {
307                    let enabled = self.is_enabled(plugin.metadata());
308                    RegisteredPlugin::new(plugin, enabled)
309                })
310                .collect(),
311        ))
312    }
313
314    fn sync_bundled_plugins(&self) -> Result<(), PluginError> {
315        let bundled_root = self
316            .config
317            .bundled_root
318            .clone()
319            .unwrap_or_else(Self::bundled_root);
320        let bundled_plugins = discover_plugin_dirs(&bundled_root)?;
321        let mut registry = self.load_registry()?;
322        let mut changed = false;
323        let install_root = self.install_root();
324        let mut active_bundled_ids = BTreeSet::new();
325
326        for source_root in bundled_plugins {
327            let manifest = load_plugin_from_directory(&source_root)?;
328            let plugin_id = plugin_id(&manifest.name, BUNDLED_MARKETPLACE);
329            active_bundled_ids.insert(plugin_id.clone());
330            let install_path = install_root.join(sanitize_plugin_id(&plugin_id));
331            let now = unix_time_ms();
332            let existing_record = registry.plugins.get(&plugin_id);
333            let installed_copy_is_valid =
334                install_path.exists() && load_plugin_from_directory(&install_path).is_ok();
335            let needs_sync = existing_record.is_none_or(|record| {
336                record.kind != PluginKind::Bundled
337                    || record.version != manifest.version
338                    || record.name != manifest.name
339                    || record.description != manifest.description
340                    || record.install_path != install_path
341                    || !record.install_path.exists()
342                    || !installed_copy_is_valid
343            });
344
345            if !needs_sync {
346                continue;
347            }
348
349            if install_path.exists() {
350                fs::remove_dir_all(&install_path)?;
351            }
352            copy_dir_all(&source_root, &install_path)?;
353
354            let installed_at_unix_ms =
355                existing_record.map_or(now, |record| record.installed_at_unix_ms);
356            registry.plugins.insert(
357                plugin_id.clone(),
358                InstalledPluginRecord {
359                    kind: PluginKind::Bundled,
360                    id: plugin_id,
361                    name: manifest.name,
362                    version: manifest.version,
363                    description: manifest.description,
364                    install_path,
365                    source: PluginInstallSource::LocalPath { path: source_root },
366                    installed_at_unix_ms,
367                    updated_at_unix_ms: now,
368                },
369            );
370            changed = true;
371        }
372
373        let stale_bundled_ids = registry
374            .plugins
375            .iter()
376            .filter_map(|(plugin_id, record)| {
377                (record.kind == PluginKind::Bundled && !active_bundled_ids.contains(plugin_id))
378                    .then_some(plugin_id.clone())
379            })
380            .collect::<Vec<_>>();
381
382        for plugin_id in stale_bundled_ids {
383            if let Some(record) = registry.plugins.remove(&plugin_id) {
384                if record.install_path.exists() {
385                    fs::remove_dir_all(&record.install_path)?;
386                }
387                changed = true;
388            }
389        }
390
391        if changed {
392            self.store_registry(&registry)?;
393        }
394
395        Ok(())
396    }
397
398    fn is_enabled(&self, metadata: &PluginMetadata) -> bool {
399        self.config
400            .enabled_plugins
401            .get(&metadata.id)
402            .copied()
403            .unwrap_or(match metadata.kind {
404                PluginKind::External => false,
405                PluginKind::Builtin | PluginKind::Bundled => metadata.default_enabled,
406            })
407    }
408
409    fn ensure_known_plugin(&self, plugin_id: &str) -> Result<(), PluginError> {
410        if self.plugin_registry()?.contains(plugin_id) {
411            Ok(())
412        } else {
413            Err(PluginError::NotFound(format!(
414                "plugin `{plugin_id}` is not installed or discoverable"
415            )))
416        }
417    }
418
419    pub(crate) fn load_registry(&self) -> Result<InstalledPluginRegistry, PluginError> {
420        let path = self.registry_path();
421        match fs::read_to_string(&path) {
422            Ok(contents) if contents.trim().is_empty() => Ok(InstalledPluginRegistry::default()),
423            Ok(contents) => Ok(serde_json::from_str(&contents)?),
424            Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
425                Ok(InstalledPluginRegistry::default())
426            }
427            Err(error) => Err(PluginError::Io(error)),
428        }
429    }
430
431    pub(crate) fn store_registry(
432        &self,
433        registry: &InstalledPluginRegistry,
434    ) -> Result<(), PluginError> {
435        let path = self.registry_path();
436        if let Some(parent) = path.parent() {
437            fs::create_dir_all(parent)?;
438        }
439        fs::write(path, serde_json::to_string_pretty(registry)?)?;
440        Ok(())
441    }
442
443    pub(crate) fn write_enabled_state(
444        &self,
445        plugin_id: &str,
446        enabled: Option<bool>,
447    ) -> Result<(), PluginError> {
448        update_settings_json(&self.settings_path(), |root| {
449            let enabled_plugins = ensure_object(root, "enabledPlugins");
450            match enabled {
451                Some(value) => {
452                    enabled_plugins.insert(plugin_id.to_string(), Value::Bool(value));
453                }
454                None => {
455                    enabled_plugins.remove(plugin_id);
456                }
457            }
458        })
459    }
460}