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