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(®istry)?;
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(®istry)?;
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(®istry)?;
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(®istry)?;
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(®istry)?;
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}