1#![allow(dead_code)]
3
4use std::collections::HashMap;
10use std::sync::{Arc, RwLock};
11
12#[derive(Debug, Clone)]
14pub enum MarketplaceProgressEvent {
15 Installing { name: String },
16 Installed { name: String },
17 Failed { name: String, error: String },
18}
19
20impl MarketplaceProgressEvent {
21 pub fn event_type(&self) -> &'static str {
22 match self {
23 Self::Installing { .. } => "installing",
24 Self::Installed { .. } => "installed",
25 Self::Failed { .. } => "failed",
26 }
27 }
28
29 pub fn name(&self) -> &str {
30 match self {
31 Self::Installing { name } | Self::Installed { name } | Self::Failed { name, .. } => {
32 name
33 }
34 }
35 }
36}
37
38#[derive(Debug, Clone)]
40pub struct MarketplaceStatus {
41 pub name: String,
42 pub status: MarketplaceStatusKind,
43 pub error: Option<String>,
44}
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48pub enum MarketplaceStatusKind {
49 Pending,
50 Installing,
51 Installed,
52 Failed,
53}
54
55#[derive(Debug, Clone, Default)]
57pub struct PluginInstallationStatus {
58 pub marketplaces: Vec<MarketplaceStatus>,
59 pub plugins: Vec<PluginStatusEntry>,
60}
61
62#[derive(Debug, Clone, Default)]
64pub struct PluginStatusEntry {
65 pub name: String,
66 pub status: String,
67}
68
69#[derive(Debug, Clone, Default)]
71pub struct PluginsState {
72 pub installation_status: PluginInstallationStatus,
73 pub needs_refresh: bool,
74}
75
76#[derive(Debug, Clone, Default)]
78pub struct AppState {
79 pub plugins: PluginsState,
80}
81
82pub type SetAppState = Arc<dyn Fn(&AppState) -> AppState + Send + Sync>;
84
85pub type OnProgressCallback = Box<dyn Fn(MarketplaceProgressEvent) + Send>;
87
88#[derive(Debug, Clone, Default)]
90pub struct ReconcileMarketplacesResult {
91 pub installed: Vec<String>,
92 pub updated: Vec<String>,
93 pub failed: Vec<(String, String)>, pub up_to_date: Vec<String>,
95}
96
97#[derive(Debug, Clone, Default)]
99pub struct MarketplaceDiff {
100 pub missing: Vec<String>,
102 pub source_changed: Vec<SourceChangedEntry>,
104}
105
106#[derive(Debug, Clone)]
108pub struct SourceChangedEntry {
109 pub name: String,
110 pub old_source: String,
111 pub new_source: String,
112}
113
114#[derive(Debug, Clone)]
116pub struct DeclaredMarketplace {
117 pub name: String,
118 pub source: String,
119}
120
121#[derive(Debug, Clone)]
123pub struct InstalledMarketplaceConfig {
124 pub name: String,
125 pub install_location: String,
126 pub source: String,
127}
128
129#[derive(Debug, Clone)]
131pub struct MarketplaceBackgroundInstallMetrics {
132 pub installed_count: usize,
133 pub updated_count: usize,
134 pub failed_count: usize,
135 pub up_to_date_count: usize,
136}
137
138fn update_marketplace_status(
140 set_app_state: &SetAppState,
141 name: &str,
142 status: MarketplaceStatusKind,
143 error: Option<&str>,
144) {
145 let name = name.to_string();
146 let error = error.map(String::from);
147
148 set_app_state(&AppState {
149 plugins: PluginsState {
150 installation_status: PluginInstallationStatus {
151 marketplaces: Vec::new(), plugins: Vec::new(),
153 },
154 needs_refresh: false,
155 },
156 });
157
158 log::debug!(
159 "Marketplace status update: {} -> {:?} (error: {:?})",
160 name,
161 status,
162 error
163 );
164}
165
166fn get_declared_marketplaces() -> Vec<DeclaredMarketplace> {
168 Vec::new()
172}
173
174async fn load_known_marketplaces_config() -> HashMap<String, InstalledMarketplaceConfig> {
176 HashMap::new()
178}
179
180fn diff_marketplaces(
182 declared: &[DeclaredMarketplace],
183 materialized: &HashMap<String, InstalledMarketplaceConfig>,
184) -> MarketplaceDiff {
185 let mut missing = Vec::new();
186 let mut source_changed = Vec::new();
187
188 for declared_mkt in declared {
189 if let Some(installed) = materialized.get(&declared_mkt.name) {
190 if installed.source != declared_mkt.source {
192 source_changed.push(SourceChangedEntry {
193 name: declared_mkt.name.clone(),
194 old_source: installed.source.clone(),
195 new_source: declared_mkt.source.clone(),
196 });
197 }
198 } else {
199 missing.push(declared_mkt.name.clone());
201 }
202 }
203
204 MarketplaceDiff {
205 missing,
206 source_changed,
207 }
208}
209
210async fn reconcile_marketplaces(
215 _on_progress: Option<OnProgressCallback>,
216) -> ReconcileMarketplacesResult {
217 ReconcileMarketplacesResult::default()
223}
224
225fn clear_marketplaces_cache() {
227 log::debug!("Clearing marketplaces cache");
229}
230
231fn clear_plugin_cache(reason: &str) {
233 log::debug!("Clearing plugin cache: {}", reason);
234}
235
236async fn refresh_active_plugins(
238 _set_app_state: &SetAppState,
239) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
240 log::debug!("Refreshing active plugins");
246 Ok(())
247}
248
249fn log_event(event_name: &str, metrics: &MarketplaceBackgroundInstallMetrics) {
251 log::debug!(
252 "Analytics event: {} installed={} updated={} failed={} up_to_date={}",
253 event_name,
254 metrics.installed_count,
255 metrics.updated_count,
256 metrics.failed_count,
257 metrics.up_to_date_count
258 );
259}
260
261fn log_for_diagnostics_no_pii(
263 level: &str,
264 event: &str,
265 metrics: &MarketplaceBackgroundInstallMetrics,
266) {
267 log::debug!(
268 "[{}] {} installed={} updated={} failed={} up_to_date={}",
269 level,
270 event,
271 metrics.installed_count,
272 metrics.updated_count,
273 metrics.failed_count,
274 metrics.up_to_date_count
275 );
276}
277
278fn log_for_debugging(msg: &str) {
280 log::debug!("{}", msg);
281}
282
283fn log_error(error: &dyn std::error::Error) {
285 log::error!("{}", error);
286}
287
288fn plural(count: usize, singular: &str) -> String {
290 if count == 1 {
291 singular.to_string()
292 } else {
293 format!("{}s", singular)
294 }
295}
296
297pub async fn perform_background_plugin_installations(set_app_state: &SetAppState) {
306 log_for_debugging("perform_background_plugin_installations called");
307
308 let declared = get_declared_marketplaces();
310 let materialized = load_known_marketplaces_config().await;
311 let diff = diff_marketplaces(&declared, &materialized);
312
313 let pending_names: Vec<String> = diff
314 .missing
315 .iter()
316 .chain(diff.source_changed.iter().map(|c| &c.name))
317 .cloned()
318 .collect();
319
320 let pending_statuses: Vec<MarketplaceStatus> = pending_names
324 .iter()
325 .map(|name| MarketplaceStatus {
326 name: name.clone(),
327 status: MarketplaceStatusKind::Pending,
328 error: None,
329 })
330 .collect();
331
332 {
333 let new_state = AppState {
334 plugins: PluginsState {
335 installation_status: PluginInstallationStatus {
336 marketplaces: pending_statuses,
337 plugins: Vec::new(),
338 },
339 needs_refresh: false,
340 },
341 };
342 set_app_state(&new_state);
343 }
344
345 if pending_names.is_empty() {
346 return;
347 }
348
349 log_for_debugging(&format!(
350 "Installing {} marketplace(s) in background",
351 pending_names.len()
352 ));
353
354 let result = reconcile_marketplaces(Some(Box::new(move |event| {
355 let on_progress = move |ev: MarketplaceProgressEvent| match ev {
356 MarketplaceProgressEvent::Installing { name } => {
357 log::debug!("Installing marketplace: {}", name);
358 }
359 MarketplaceProgressEvent::Installed { name } => {
360 log::debug!("Installed marketplace: {}", name);
361 }
362 MarketplaceProgressEvent::Failed { name, error } => {
363 log::error!("Failed to install marketplace {}: {}", name, error);
364 }
365 };
366 on_progress(event);
367 })))
368 .await;
369
370 let metrics = MarketplaceBackgroundInstallMetrics {
371 installed_count: result.installed.len(),
372 updated_count: result.updated.len(),
373 failed_count: result.failed.len(),
374 up_to_date_count: result.up_to_date.len(),
375 };
376
377 log_event("tengu_marketplace_background_install", &metrics);
378 log_for_diagnostics_no_pii("info", "tengu_marketplace_background_install", &metrics);
379
380 if !result.installed.is_empty() {
381 clear_marketplaces_cache();
387 log_for_debugging(&format!(
388 "Auto-refreshing plugins after {} new marketplace(s) installed",
389 result.installed.len()
390 ));
391
392 if let Err(refresh_error) = refresh_active_plugins(set_app_state).await {
393 log_error(refresh_error.as_ref());
396 log_for_debugging(&format!(
397 "Auto-refresh failed, falling back to needs_refresh: {}",
398 refresh_error
399 ));
400 clear_plugin_cache("perform_background_plugin_installations: auto-refresh failed");
401
402 let new_state = AppState {
403 plugins: PluginsState {
404 installation_status: PluginInstallationStatus::default(),
405 needs_refresh: true,
406 },
407 };
408 set_app_state(&new_state);
409 }
410 } else if !result.updated.is_empty() {
411 clear_marketplaces_cache();
414 clear_plugin_cache("perform_background_plugin_installations: marketplaces reconciled");
415
416 let new_state = AppState {
417 plugins: PluginsState {
418 installation_status: PluginInstallationStatus::default(),
419 needs_refresh: true,
420 },
421 };
422 set_app_state(&new_state);
423 }
424}
425
426pub struct PluginInstallationManagerBuilder {
428 set_app_state: Option<SetAppState>,
429}
430
431impl PluginInstallationManagerBuilder {
432 pub fn new() -> Self {
433 Self {
434 set_app_state: None,
435 }
436 }
437
438 pub fn with_set_app_state(mut self, set_app_state: SetAppState) -> Self {
439 self.set_app_state = Some(set_app_state);
440 self
441 }
442
443 pub fn build(self) -> PluginInstallationManager {
444 PluginInstallationManager {
445 set_app_state: self.set_app_state,
446 }
447 }
448}
449
450impl Default for PluginInstallationManagerBuilder {
451 fn default() -> Self {
452 Self::new()
453 }
454}
455
456pub struct PluginInstallationManager {
458 set_app_state: Option<SetAppState>,
459}
460
461impl PluginInstallationManager {
462 pub fn new() -> Self {
463 Self {
464 set_app_state: None,
465 }
466 }
467
468 pub fn with_set_app_state(mut self, set_app_state: SetAppState) -> Self {
469 self.set_app_state = Some(set_app_state);
470 self
471 }
472
473 pub async fn perform_installations(&self) {
475 if let Some(ref set_app_state) = self.set_app_state {
476 perform_background_plugin_installations(set_app_state).await;
477 }
478 }
479
480 pub fn get_app_state(&self) -> Option<AppState> {
482 None
484 }
485}
486
487impl Default for PluginInstallationManager {
488 fn default() -> Self {
489 Self::new()
490 }
491}
492
493#[cfg(test)]
494mod tests {
495 use super::*;
496
497 #[test]
498 fn test_marketplace_progress_event_types() {
499 let installing = MarketplaceProgressEvent::Installing {
500 name: "test".to_string(),
501 };
502 assert_eq!(installing.event_type(), "installing");
503 assert_eq!(installing.name(), "test");
504
505 let installed = MarketplaceProgressEvent::Installed {
506 name: "test".to_string(),
507 };
508 assert_eq!(installed.event_type(), "installed");
509
510 let failed = MarketplaceProgressEvent::Failed {
511 name: "test".to_string(),
512 error: "some error".to_string(),
513 };
514 assert_eq!(failed.event_type(), "failed");
515 }
516
517 #[test]
518 fn test_marketplace_status_kind() {
519 assert_eq!(
520 MarketplaceStatusKind::Pending,
521 MarketplaceStatusKind::Pending
522 );
523 assert_eq!(
524 MarketplaceStatusKind::Installing,
525 MarketplaceStatusKind::Installing
526 );
527 assert_eq!(
528 MarketplaceStatusKind::Installed,
529 MarketplaceStatusKind::Installed
530 );
531 assert_eq!(MarketplaceStatusKind::Failed, MarketplaceStatusKind::Failed);
532 }
533
534 #[test]
535 fn test_diff_marketplaces_empty() {
536 let declared: Vec<DeclaredMarketplace> = Vec::new();
537 let materialized: HashMap<String, InstalledMarketplaceConfig> = HashMap::new();
538 let diff = diff_marketplaces(&declared, &materialized);
539 assert!(diff.missing.is_empty());
540 assert!(diff.source_changed.is_empty());
541 }
542
543 #[test]
544 fn test_diff_marketplaces_missing() {
545 let declared = vec![DeclaredMarketplace {
546 name: "test-marketplace".to_string(),
547 source: "https://example.com".to_string(),
548 }];
549 let materialized: HashMap<String, InstalledMarketplaceConfig> = HashMap::new();
550 let diff = diff_marketplaces(&declared, &materialized);
551 assert_eq!(diff.missing, vec!["test-marketplace".to_string()]);
552 assert!(diff.source_changed.is_empty());
553 }
554
555 #[test]
556 fn test_diff_marketplaces_source_changed() {
557 let declared = vec![DeclaredMarketplace {
558 name: "test-marketplace".to_string(),
559 source: "https://new-source.com".to_string(),
560 }];
561 let mut materialized = HashMap::new();
562 materialized.insert(
563 "test-marketplace".to_string(),
564 InstalledMarketplaceConfig {
565 name: "test-marketplace".to_string(),
566 install_location: "/path/to/marketplace".to_string(),
567 source: "https://old-source.com".to_string(),
568 },
569 );
570 let diff = diff_marketplaces(&declared, &materialized);
571 assert!(diff.missing.is_empty());
572 assert_eq!(diff.source_changed.len(), 1);
573 assert_eq!(diff.source_changed[0].name, "test-marketplace");
574 assert_eq!(diff.source_changed[0].old_source, "https://old-source.com");
575 assert_eq!(diff.source_changed[0].new_source, "https://new-source.com");
576 }
577
578 #[test]
579 fn test_plural() {
580 assert_eq!(plural(0, "plugin"), "plugins");
581 assert_eq!(plural(1, "plugin"), "plugin");
582 assert_eq!(plural(2, "plugin"), "plugins");
583 }
584
585 #[test]
586 fn test_reconcile_result_default() {
587 let result = ReconcileMarketplacesResult::default();
588 assert!(result.installed.is_empty());
589 assert!(result.updated.is_empty());
590 assert!(result.failed.is_empty());
591 assert!(result.up_to_date.is_empty());
592 }
593
594 #[test]
595 fn test_plugin_installation_manager_default() {
596 let manager = PluginInstallationManager::default();
597 assert!(manager.set_app_state.is_none());
598 }
599
600 #[test]
601 fn test_plugin_installation_manager_builder() {
602 let manager = PluginInstallationManagerBuilder::new().build();
603 assert!(manager.set_app_state.is_none());
604 }
605
606 #[test]
607 fn test_marketplace_status_default() {
608 let status = MarketplaceStatus {
609 name: "test".to_string(),
610 status: MarketplaceStatusKind::Pending,
611 error: None,
612 };
613 assert_eq!(status.name, "test");
614 assert_eq!(status.status, MarketplaceStatusKind::Pending);
615 assert!(status.error.is_none());
616 }
617}