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(
87 config: &Config,
88 discourse_name: &str,
89 url: &str,
90 dry_run: bool,
91) -> Result<()> {
92 let discourse = select_discourse(config, Some(discourse_name))?;
93 let target = ssh_target(discourse);
94 let template = std::env::var("DSC_SSH_PLUGIN_INSTALL_CMD")
95 .map_err(|_| {
96 anyhow!(
97 "missing DSC_SSH_PLUGIN_INSTALL_CMD for plugin install; set DSC_SSH_PLUGIN_INSTALL_CMD to your install command"
98 )
99 })?;
100 let command = render_template(&template, &[("url", url), ("name", url)]);
101 if dry_run {
102 println!("[dry-run] would run on {}: {}", target, command);
103 return Ok(());
104 }
105 let output = run_ssh_command(&target, &command)?;
106 println!("Plugin install completed: {}", url);
107 if !output.trim().is_empty() {
108 println!("{}", output.trim());
109 }
110 Ok(())
111}
112
113pub fn plugin_remove(
114 config: &Config,
115 discourse_name: &str,
116 name: &str,
117 dry_run: bool,
118) -> Result<()> {
119 let discourse = select_discourse(config, Some(discourse_name))?;
120 let target = ssh_target(discourse);
121 let template = std::env::var("DSC_SSH_PLUGIN_REMOVE_CMD")
122 .map_err(|_| {
123 anyhow!(
124 "missing DSC_SSH_PLUGIN_REMOVE_CMD for plugin remove; set DSC_SSH_PLUGIN_REMOVE_CMD to your remove command"
125 )
126 })?;
127 let command = render_template(&template, &[("name", name), ("url", name)]);
128 if dry_run {
129 println!("[dry-run] would run on {}: {}", target, command);
130 return Ok(());
131 }
132 let output = run_ssh_command(&target, &command)?;
133 println!("Plugin removal completed: {}", name);
134 if !output.trim().is_empty() {
135 println!("{}", output.trim());
136 }
137 Ok(())
138}
139
140fn ssh_target(discourse: &DiscourseConfig) -> String {
141 discourse
142 .ssh_host
143 .clone()
144 .unwrap_or_else(|| discourse.name.clone())
145}
146
147fn render_template(template: &str, replacements: &[(&str, &str)]) -> String {
148 let mut out = template.to_string();
149 for (key, value) in replacements {
150 out = out.replace(&format!("{{{}}}", key), value);
151 }
152 out
153}