1use crate::api::DiscourseClient;
2use crate::cli::ListFormat;
3use crate::commands::common::{ensure_api_credentials, select_discourse};
4use crate::commands::update::run_ssh_command;
5use crate::config::{Config, DiscourseConfig};
6use anyhow::{Result, anyhow};
7use serde::Serialize;
8
9#[derive(Debug, Serialize)]
10struct PluginListEntry {
11 name: String,
12 version: String,
13 status: String,
14}
15
16pub fn plugin_list(
17 config: &Config,
18 discourse_name: &str,
19 format: ListFormat,
20 verbose: bool,
21) -> Result<()> {
22 let discourse = select_discourse(config, Some(discourse_name))?;
23 ensure_api_credentials(discourse)?;
24 let client = DiscourseClient::new(discourse)?;
25 let response = client.list_plugins()?;
26 let plugins = response
27 .get("plugins")
28 .and_then(|v| v.as_array())
29 .cloned()
30 .unwrap_or_default();
31 let entries: Vec<PluginListEntry> = plugins
32 .into_iter()
33 .map(|plugin| {
34 let name = plugin
35 .get("name")
36 .and_then(|v| v.as_str())
37 .unwrap_or("unknown")
38 .to_string();
39 let version = plugin
40 .get("version")
41 .and_then(|v| v.as_str())
42 .unwrap_or("unknown")
43 .to_string();
44 let status = plugin
45 .get("enabled")
46 .and_then(|v| v.as_bool())
47 .or_else(|| plugin.get("active").and_then(|v| v.as_bool()))
48 .map(|value| {
49 if value {
50 "enabled".to_string()
51 } else {
52 "disabled".to_string()
53 }
54 })
55 .unwrap_or_else(|| "unknown".to_string());
56 PluginListEntry {
57 name,
58 version,
59 status,
60 }
61 })
62 .collect();
63
64 match format {
65 ListFormat::Text => {
66 if entries.is_empty() && !verbose {
67 println!("No plugins found.");
68 return Ok(());
69 }
70 for plugin in entries {
71 println!("{} - {} - {}", plugin.name, plugin.version, plugin.status);
72 }
73 }
74 ListFormat::Json => {
75 let raw = serde_json::to_string_pretty(&entries)?;
76 println!("{}", raw);
77 }
78 ListFormat::Yaml => {
79 let raw = serde_yaml::to_string(&entries)?;
80 println!("{}", raw);
81 }
82 }
83 Ok(())
84}
85
86pub fn plugin_install(config: &Config, discourse_name: &str, url: &str) -> Result<()> {
87 let discourse = select_discourse(config, Some(discourse_name))?;
88 let target = ssh_target(discourse);
89 let template = std::env::var("DSC_SSH_PLUGIN_INSTALL_CMD")
90 .map_err(|_| {
91 anyhow!(
92 "missing DSC_SSH_PLUGIN_INSTALL_CMD for plugin install; set DSC_SSH_PLUGIN_INSTALL_CMD to your install command"
93 )
94 })?;
95 let command = render_template(&template, &[("url", url), ("name", url)]);
96 let output = run_ssh_command(&target, &command)?;
97 println!("Plugin install completed: {}", url);
98 if !output.trim().is_empty() {
99 println!("{}", output.trim());
100 }
101 Ok(())
102}
103
104pub fn plugin_remove(config: &Config, discourse_name: &str, name: &str) -> Result<()> {
105 let discourse = select_discourse(config, Some(discourse_name))?;
106 let target = ssh_target(discourse);
107 let template = std::env::var("DSC_SSH_PLUGIN_REMOVE_CMD")
108 .map_err(|_| {
109 anyhow!(
110 "missing DSC_SSH_PLUGIN_REMOVE_CMD for plugin remove; set DSC_SSH_PLUGIN_REMOVE_CMD to your remove command"
111 )
112 })?;
113 let command = render_template(&template, &[("name", name), ("url", name)]);
114 let output = run_ssh_command(&target, &command)?;
115 println!("Plugin removal completed: {}", name);
116 if !output.trim().is_empty() {
117 println!("{}", output.trim());
118 }
119 Ok(())
120}
121
122fn ssh_target(discourse: &DiscourseConfig) -> String {
123 discourse
124 .ssh_host
125 .clone()
126 .unwrap_or_else(|| discourse.name.clone())
127}
128
129fn render_template(template: &str, replacements: &[(&str, &str)]) -> String {
130 let mut out = template.to_string();
131 for (key, value) in replacements {
132 out = out.replace(&format!("{{{}}}", key), value);
133 }
134 out
135}