1#![allow(dead_code)]
3
4use std::collections::HashMap;
12use std::process;
13
14use super::plugin_operations::{
15 InstallableScope, PluginOperationResult, PluginUpdateResult, disable_all_plugins_op,
16 disable_plugin_op, enable_plugin_op, install_plugin_op, uninstall_plugin_op, update_plugin_op,
17};
18use crate::utils::plugins::loader::parse_plugin_identifier;
19
20pub use super::plugin_operations::{VALID_INSTALLABLE_SCOPES, VALID_UPDATE_SCOPES};
21
22type PluginCliCommand = &'static str; #[derive(Debug, Clone, Default)]
26struct PluginTelemetryFields {
27 plugin_name: Option<String>,
28 marketplace_name: Option<String>,
29 is_managed: bool,
30}
31
32#[derive(Debug, Clone, Default)]
34struct AnalyticsEvent {
35 event_name: String,
36 properties: HashMap<String, serde_json::Value>,
37}
38
39fn classify_plugin_command_error(error: &dyn std::error::Error) -> String {
41 let msg = error.to_string().to_lowercase();
42 if msg.contains("not found") {
43 "not_found".to_string()
44 } else if msg.contains("permission") || msg.contains("blocked") || msg.contains("policy") {
45 "permission_denied".to_string()
46 } else if msg.contains("network") || msg.contains("timeout") || msg.contains("connection") {
47 "network_error".to_string()
48 } else if msg.contains("parse") || msg.contains("invalid") || msg.contains("format") {
49 "parse_error".to_string()
50 } else {
51 "unknown".to_string()
52 }
53}
54
55fn build_plugin_telemetry_fields(
57 name: Option<&str>,
58 marketplace: Option<&str>,
59 managed_plugin_names: &[String],
60) -> PluginTelemetryFields {
61 PluginTelemetryFields {
62 plugin_name: name.map(String::from),
63 marketplace_name: marketplace.map(String::from),
64 is_managed: name
65 .map(|n| managed_plugin_names.iter().any(|m| m == n))
66 .unwrap_or(false),
67 }
68}
69
70fn get_managed_plugin_names() -> Vec<String> {
72 Vec::new()
73}
74
75fn log_event(event: AnalyticsEvent) {
77 log::debug!(
78 "Analytics event: {} {:?}",
79 event.event_name,
80 event.properties
81 );
82}
83
84mod figures {
86 pub const TICK: &str = "\u{2713}"; pub const CROSS: &str = "\u{2717}"; }
89
90fn handle_plugin_command_error(
94 error: &dyn std::error::Error,
95 command: PluginCliCommand,
96 plugin: Option<&str>,
97) -> ! {
98 log::error!("Plugin command error: {}", error);
99
100 let operation = match plugin {
101 Some(p) => format!("{} plugin \"{}\"", command, p),
102 None if command == "disable-all" => "disable all plugins".to_string(),
103 None => format!("{} plugins", command),
104 };
105
106 eprintln!("{} Failed to {}: {}", figures::CROSS, operation, error);
107
108 let telemetry_fields = plugin.map(|p| {
109 let (name, marketplace) = parse_plugin_identifier(p);
110 let telemetry = build_plugin_telemetry_fields(
111 name.as_deref(),
112 marketplace.as_deref(),
113 &get_managed_plugin_names(),
114 );
115 (name, marketplace, telemetry)
116 });
117
118 let mut properties: HashMap<String, serde_json::Value> = HashMap::new();
119 properties.insert(
120 "command".to_string(),
121 serde_json::Value::String(command.to_string()),
122 );
123 properties.insert(
124 "error_category".to_string(),
125 serde_json::Value::String(classify_plugin_command_error(error)),
126 );
127
128 if let Some((name, marketplace, telemetry)) = telemetry_fields {
129 if let Some(n) = name {
130 properties.insert(
131 "_PROTO_plugin_name".to_string(),
132 serde_json::Value::String(n),
133 );
134 }
135 if let Some(m) = marketplace {
136 properties.insert(
137 "_PROTO_marketplace_name".to_string(),
138 serde_json::Value::String(m),
139 );
140 }
141 properties.insert(
142 "is_managed".to_string(),
143 serde_json::Value::Bool(telemetry.is_managed),
144 );
145 }
146
147 log_event(AnalyticsEvent {
148 event_name: "tengu_plugin_command_failed".to_string(),
149 properties,
150 });
151
152 process::exit(1);
153}
154
155pub async fn install_plugin(
161 plugin: &str,
162 scope: InstallableScope,
163) -> Result<PluginOperationResult, Box<dyn std::error::Error>> {
164 println!("Installing plugin \"{}\"...", plugin);
165
166 let result = install_plugin_op(plugin, scope).await;
167
168 if !result.success {
169 return Err(result.message.clone().into());
170 }
171
172 println!("{} {}", figures::TICK, result.message);
173
174 let (name, marketplace) =
176 parse_plugin_identifier(result.plugin_id.as_deref().unwrap_or(plugin));
177 let mut properties: HashMap<String, serde_json::Value> = HashMap::new();
178 if let Some(n) = &name {
179 properties.insert(
180 "_PROTO_plugin_name".to_string(),
181 serde_json::Value::String(n.clone()),
182 );
183 }
184 if let Some(m) = &marketplace {
185 properties.insert(
186 "_PROTO_marketplace_name".to_string(),
187 serde_json::Value::String(m.clone()),
188 );
189 }
190 properties.insert(
191 "scope".to_string(),
192 serde_json::Value::String(result.scope.clone().unwrap_or_else(|| scope.to_string())),
193 );
194 properties.insert(
195 "install_source".to_string(),
196 serde_json::Value::String("cli-explicit".to_string()),
197 );
198
199 let telemetry = build_plugin_telemetry_fields(
200 name.as_deref(),
201 marketplace.as_deref(),
202 &get_managed_plugin_names(),
203 );
204 properties.insert(
205 "is_managed".to_string(),
206 serde_json::Value::Bool(telemetry.is_managed),
207 );
208
209 log_event(AnalyticsEvent {
210 event_name: "tengu_plugin_installed_cli".to_string(),
211 properties,
212 });
213
214 Ok(result)
215}
216
217pub async fn uninstall_plugin(
224 plugin: &str,
225 scope: InstallableScope,
226 keep_data: bool,
227) -> Result<PluginOperationResult, Box<dyn std::error::Error>> {
228 let result = uninstall_plugin_op(plugin, scope, !keep_data).await;
229
230 if !result.success {
231 return Err(result.message.clone().into());
232 }
233
234 println!("{} {}", figures::TICK, result.message);
235
236 let (name, marketplace) =
238 parse_plugin_identifier(result.plugin_id.as_deref().unwrap_or(plugin));
239 let mut properties: HashMap<String, serde_json::Value> = HashMap::new();
240 if let Some(n) = &name {
241 properties.insert(
242 "_PROTO_plugin_name".to_string(),
243 serde_json::Value::String(n.clone()),
244 );
245 }
246 if let Some(m) = &marketplace {
247 properties.insert(
248 "_PROTO_marketplace_name".to_string(),
249 serde_json::Value::String(m.clone()),
250 );
251 }
252 properties.insert(
253 "scope".to_string(),
254 serde_json::Value::String(result.scope.clone().unwrap_or_else(|| scope.to_string())),
255 );
256
257 let telemetry = build_plugin_telemetry_fields(
258 name.as_deref(),
259 marketplace.as_deref(),
260 &get_managed_plugin_names(),
261 );
262 properties.insert(
263 "is_managed".to_string(),
264 serde_json::Value::Bool(telemetry.is_managed),
265 );
266
267 log_event(AnalyticsEvent {
268 event_name: "tengu_plugin_uninstalled_cli".to_string(),
269 properties,
270 });
271
272 Ok(result)
273}
274
275pub async fn enable_plugin(
281 plugin: &str,
282 scope: Option<InstallableScope>,
283) -> Result<PluginOperationResult, Box<dyn std::error::Error>> {
284 let result = enable_plugin_op(plugin, scope).await;
285
286 if !result.success {
287 return Err(result.message.clone().into());
288 }
289
290 println!("{} {}", figures::TICK, result.message);
291
292 let (name, marketplace) =
294 parse_plugin_identifier(result.plugin_id.as_deref().unwrap_or(plugin));
295 let mut properties: HashMap<String, serde_json::Value> = HashMap::new();
296 if let Some(n) = &name {
297 properties.insert(
298 "_PROTO_plugin_name".to_string(),
299 serde_json::Value::String(n.clone()),
300 );
301 }
302 if let Some(m) = &marketplace {
303 properties.insert(
304 "_PROTO_marketplace_name".to_string(),
305 serde_json::Value::String(m.clone()),
306 );
307 }
308 if let Some(ref s) = result.scope {
309 properties.insert("scope".to_string(), serde_json::Value::String(s.clone()));
310 }
311
312 let telemetry = build_plugin_telemetry_fields(
313 name.as_deref(),
314 marketplace.as_deref(),
315 &get_managed_plugin_names(),
316 );
317 properties.insert(
318 "is_managed".to_string(),
319 serde_json::Value::Bool(telemetry.is_managed),
320 );
321
322 log_event(AnalyticsEvent {
323 event_name: "tengu_plugin_enabled_cli".to_string(),
324 properties,
325 });
326
327 Ok(result)
328}
329
330pub async fn disable_plugin(
336 plugin: &str,
337 scope: Option<InstallableScope>,
338) -> Result<PluginOperationResult, Box<dyn std::error::Error>> {
339 let result = disable_plugin_op(plugin, scope).await;
340
341 if !result.success {
342 return Err(result.message.clone().into());
343 }
344
345 println!("{} {}", figures::TICK, result.message);
346
347 let (name, marketplace) =
349 parse_plugin_identifier(result.plugin_id.as_deref().unwrap_or(plugin));
350 let mut properties: HashMap<String, serde_json::Value> = HashMap::new();
351 if let Some(n) = &name {
352 properties.insert(
353 "_PROTO_plugin_name".to_string(),
354 serde_json::Value::String(n.clone()),
355 );
356 }
357 if let Some(m) = &marketplace {
358 properties.insert(
359 "_PROTO_marketplace_name".to_string(),
360 serde_json::Value::String(m.clone()),
361 );
362 }
363 if let Some(ref s) = result.scope {
364 properties.insert("scope".to_string(), serde_json::Value::String(s.clone()));
365 }
366
367 let telemetry = build_plugin_telemetry_fields(
368 name.as_deref(),
369 marketplace.as_deref(),
370 &get_managed_plugin_names(),
371 );
372 properties.insert(
373 "is_managed".to_string(),
374 serde_json::Value::Bool(telemetry.is_managed),
375 );
376
377 log_event(AnalyticsEvent {
378 event_name: "tengu_plugin_disabled_cli".to_string(),
379 properties,
380 });
381
382 Ok(result)
383}
384
385pub async fn disable_all_plugins() -> Result<PluginOperationResult, Box<dyn std::error::Error>> {
387 let result = disable_all_plugins_op().await;
388
389 if !result.success {
390 return Err(result.message.clone().into());
391 }
392
393 println!("{} {}", figures::TICK, result.message);
394
395 log_event(AnalyticsEvent {
396 event_name: "tengu_plugin_disabled_all_cli".to_string(),
397 properties: HashMap::new(),
398 });
399
400 Ok(result)
401}
402
403pub async fn update_plugin_cli(
409 plugin: &str,
410 scope: &str,
411) -> Result<PluginUpdateResult, Box<dyn std::error::Error>> {
412 println!(
413 "Checking for updates for plugin \"{}\" at {} scope...",
414 plugin, scope
415 );
416
417 let result = update_plugin_op(plugin, scope).await;
418
419 if !result.success {
420 return Err(result.message.clone().into());
421 }
422
423 println!("{} {}", figures::TICK, result.message);
424
425 if !result.already_up_to_date.unwrap_or(false) {
426 let (name, marketplace) =
427 parse_plugin_identifier(result.plugin_id.as_deref().unwrap_or(plugin));
428 let mut properties: HashMap<String, serde_json::Value> = HashMap::new();
429 if let Some(n) = &name {
430 properties.insert(
431 "_PROTO_plugin_name".to_string(),
432 serde_json::Value::String(n.clone()),
433 );
434 }
435 if let Some(m) = &marketplace {
436 properties.insert(
437 "_PROTO_marketplace_name".to_string(),
438 serde_json::Value::String(m.clone()),
439 );
440 }
441 properties.insert(
442 "old_version".to_string(),
443 serde_json::Value::String(
444 result
445 .old_version
446 .clone()
447 .unwrap_or_else(|| "unknown".to_string()),
448 ),
449 );
450 properties.insert(
451 "new_version".to_string(),
452 serde_json::Value::String(
453 result
454 .new_version
455 .clone()
456 .unwrap_or_else(|| "unknown".to_string()),
457 ),
458 );
459
460 let telemetry = build_plugin_telemetry_fields(
461 name.as_deref(),
462 marketplace.as_deref(),
463 &get_managed_plugin_names(),
464 );
465 properties.insert(
466 "is_managed".to_string(),
467 serde_json::Value::Bool(telemetry.is_managed),
468 );
469
470 log_event(AnalyticsEvent {
471 event_name: "tengu_plugin_updated_cli".to_string(),
472 properties,
473 });
474 }
475
476 Ok(result)
477}
478
479#[cfg(test)]
480mod tests {
481 use super::*;
482
483 #[test]
484 fn test_classify_plugin_command_error_not_found() {
485 let err = std::io::Error::new(std::io::ErrorKind::NotFound, "plugin not found");
486 assert_eq!(classify_plugin_command_error(&err), "not_found");
487 }
488
489 #[test]
490 fn test_classify_plugin_command_error_permission() {
491 let err = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "permission denied");
492 assert_eq!(classify_plugin_command_error(&err), "permission_denied");
493 }
494
495 #[test]
496 fn test_classify_plugin_command_error_network() {
497 let err = std::io::Error::new(std::io::ErrorKind::TimedOut, "network timeout");
498 assert_eq!(classify_plugin_command_error(&err), "network_error");
499 }
500
501 #[test]
502 fn test_classify_plugin_command_error_unknown() {
503 let err = std::io::Error::new(std::io::ErrorKind::Other, "some other error");
504 assert_eq!(classify_plugin_command_error(&err), "unknown");
505 }
506
507 #[test]
508 fn test_build_plugin_telemetry_fields() {
509 let fields = build_plugin_telemetry_fields(
510 Some("test-plugin"),
511 Some("test-marketplace"),
512 &["managed-plugin".to_string()],
513 );
514 assert_eq!(fields.plugin_name, Some("test-plugin".to_string()));
515 assert_eq!(
516 fields.marketplace_name,
517 Some("test-marketplace".to_string())
518 );
519 assert!(!fields.is_managed);
520 }
521
522 #[test]
523 fn test_build_plugin_telemetry_fields_managed() {
524 let fields = build_plugin_telemetry_fields(
525 Some("managed-plugin"),
526 None,
527 &["managed-plugin".to_string()],
528 );
529 assert!(fields.is_managed);
530 }
531
532 #[test]
533 fn test_figures_constants() {
534 assert_eq!(figures::TICK, "\u{2713}");
535 assert_eq!(figures::CROSS, "\u{2717}");
536 }
537}