1#![allow(dead_code)]
3
4use std::collections::HashMap;
5
6use super::marketplace_manager::{
7 DeclaredMarketplace, add_marketplace_source, get_declared_marketplaces,
8 load_known_marketplaces_config,
9};
10use super::schemas::{MarketplaceSource, is_local_marketplace_source};
11
12pub struct MarketplaceDiff {
14 pub missing: Vec<String>,
15 pub source_changed: Vec<SourceChangedEntry>,
16 pub up_to_date: Vec<String>,
17}
18
19pub struct SourceChangedEntry {
20 pub name: String,
21 pub declared_source: MarketplaceSource,
22 pub materialized_source: MarketplaceSource,
23}
24
25pub fn diff_marketplaces(
27 declared: &HashMap<String, DeclaredMarketplace>,
28 materialized: &HashMap<String, super::types::KnownMarketplace>,
29 _project_root: Option<&str>,
30) -> MarketplaceDiff {
31 let mut missing = Vec::new();
32 let mut source_changed = Vec::new();
33 let mut up_to_date = Vec::new();
34
35 for (name, intent) in declared {
36 let state = materialized.get(name);
37
38 match state {
39 None => missing.push(name.clone()),
40 Some(state_entry) => {
41 if intent.source_is_fallback.unwrap_or(false) {
42 up_to_date.push(name.clone());
43 } else if sources_equal_serialized(&intent.source, &state_entry.source) {
44 up_to_date.push(name.clone());
45 } else {
46 let materialized_source =
47 plugin_source_to_marketplace_source(&state_entry.source);
48 source_changed.push(SourceChangedEntry {
49 name: name.clone(),
50 declared_source: intent.source.clone(),
51 materialized_source,
52 });
53 }
54 }
55 }
56 }
57
58 MarketplaceDiff {
59 missing,
60 source_changed,
61 up_to_date,
62 }
63}
64
65fn sources_equal_serialized(a: &MarketplaceSource, b: &super::types::PluginSource) -> bool {
67 let a_json = serde_json::to_value(a).unwrap_or_default();
68 let b_json = serde_json::to_value(b).unwrap_or_default();
69 a_json == b_json
70}
71
72fn plugin_source_to_marketplace_source(source: &super::types::PluginSource) -> MarketplaceSource {
74 match source {
75 super::types::PluginSource::Relative(path) => MarketplaceSource::Directory {
76 path: path.clone(),
77 },
78 super::types::PluginSource::Github {
79 repo,
80 ref_,
81 path,
82 ..
83 } => MarketplaceSource::Github {
84 repo: repo.clone(),
85 ref_: ref_.clone(),
86 path: path.clone(),
87 },
88 super::types::PluginSource::Git { url, ref_, .. } => MarketplaceSource::Git {
89 url: url.clone(),
90 ref_: ref_.clone(),
91 path: None,
92 },
93 super::types::PluginSource::GitSubdir {
94 repo, subdir, ref_, ..
95 } => MarketplaceSource::GitSubdir {
96 url: repo.clone(),
97 path: subdir.clone(),
98 ref_: ref_.clone(),
99 },
100 super::types::PluginSource::Url { url, .. } => MarketplaceSource::Url {
101 url: url.clone(),
102 },
103 super::types::PluginSource::Npm { package, .. } => MarketplaceSource::Url {
104 url: format!("npm:{}", package),
105 },
106 super::types::PluginSource::Pip { package, .. } => MarketplaceSource::Url {
107 url: format!("pip:{}", package),
108 },
109 super::types::PluginSource::Settings { .. } => MarketplaceSource::Settings {
110 name: String::new(),
111 plugins: Vec::new(),
112 },
113 }
114}
115
116fn _sources_equal(a: &MarketplaceSource, b: &MarketplaceSource) -> bool {
117 a == b
118}
119
120fn _normalize_source(source: &MarketplaceSource, _project_root: Option<&str>) -> MarketplaceSource {
121 source.clone()
122}
123
124fn _find_canonical_git_root(base: &str) -> Option<String> {
125 Some(base.to_string())
126}
127
128pub enum ReconcileProgressEvent {
130 Installing {
131 name: String,
132 action: String,
133 index: usize,
134 total: usize,
135 },
136 Installed {
137 name: String,
138 already_materialized: bool,
139 },
140 Failed {
141 name: String,
142 error: String,
143 },
144}
145
146pub struct ReconcileOptions {
148 pub skip: Option<Box<dyn Fn(&str, &MarketplaceSource) -> bool>>,
149 pub on_progress: Option<Box<dyn Fn(ReconcileProgressEvent)>>,
150}
151
152pub struct ReconcileResult {
154 pub installed: Vec<String>,
155 pub updated: Vec<String>,
156 pub failed: Vec<(String, String)>,
157 pub up_to_date: Vec<String>,
158 pub skipped: Vec<String>,
159}
160
161pub async fn reconcile_marketplaces(
163 opts: Option<ReconcileOptions>,
164) -> Result<ReconcileResult, Box<dyn std::error::Error + Send + Sync>> {
165 let declared = get_declared_marketplaces();
166 if declared.is_empty() {
167 return Ok(ReconcileResult {
168 installed: Vec::new(),
169 updated: Vec::new(),
170 failed: Vec::new(),
171 up_to_date: Vec::new(),
172 skipped: Vec::new(),
173 });
174 }
175
176 let materialized = match load_known_marketplaces_config().await {
177 Ok(m) => m,
178 Err(e) => {
179 log::error!("Failed to load known marketplaces config: {}", e);
180 HashMap::new()
181 }
182 };
183
184 let diff = diff_marketplaces(&declared, &materialized, None);
185
186 let mut work = Vec::new();
187
188 for name in &diff.missing {
189 if let Some(intent) = declared.get(name) {
190 work.push((name.clone(), intent.source.clone(), "install".to_string()));
191 }
192 }
193
194 for entry in diff.source_changed {
195 work.push((
196 entry.name.clone(),
197 entry.declared_source,
198 "update".to_string(),
199 ));
200 }
201
202 let mut skipped = Vec::new();
203 let mut to_process = Vec::new();
204
205 for (name, source, action) in work {
206 if let Some(skip_fn) = opts.as_ref().and_then(|o| o.skip.as_ref()) {
207 if skip_fn(&name, &source) {
208 skipped.push(name);
209 continue;
210 }
211 }
212
213 if action == "update" && is_local_marketplace_source(&source) {
214 skipped.push(name);
215 continue;
216 }
217
218 to_process.push((name, source, action));
219 }
220
221 if to_process.is_empty() {
222 return Ok(ReconcileResult {
223 installed: Vec::new(),
224 updated: Vec::new(),
225 failed: Vec::new(),
226 up_to_date: diff.up_to_date,
227 skipped,
228 });
229 }
230
231 let mut installed = Vec::new();
232 let mut updated = Vec::new();
233 let mut failed = Vec::new();
234
235 for (i, (name, source, action)) in to_process.iter().enumerate() {
236 if let Some(on_progress) = opts.as_ref().and_then(|o| o.on_progress.as_ref()) {
237 on_progress(ReconcileProgressEvent::Installing {
238 name: name.clone(),
239 action: action.clone(),
240 index: i + 1,
241 total: to_process.len(),
242 });
243 }
244
245 match add_marketplace_source(source).await {
246 Ok(result) => {
247 if action == "install" {
248 installed.push(name.clone());
249 } else {
250 updated.push(name.clone());
251 }
252
253 if let Some(on_progress) = opts.as_ref().and_then(|o| o.on_progress.as_ref()) {
254 on_progress(ReconcileProgressEvent::Installed {
255 name: name.clone(),
256 already_materialized: result.already_materialized,
257 });
258 }
259 }
260 Err(e) => {
261 let error = e.to_string();
262 failed.push((name.clone(), error.clone()));
263
264 if let Some(on_progress) = opts.as_ref().and_then(|o| o.on_progress.as_ref()) {
265 on_progress(ReconcileProgressEvent::Failed {
266 name: name.clone(),
267 error,
268 });
269 }
270
271 log::error!("Failed to reconcile marketplace {}: {}", name, e);
272 }
273 }
274 }
275
276 Ok(ReconcileResult {
277 installed,
278 updated,
279 failed,
280 up_to_date: diff.up_to_date,
281 skipped,
282 })
283}
284
285async fn _path_exists(source: &MarketplaceSource) -> bool {
286 if let MarketplaceSource::Directory { path } | MarketplaceSource::File { path } = source {
287 tokio::fs::metadata(path).await.is_ok()
288 } else {
289 true
290 }
291}