Skip to main content

dsc/commands/
plugin.rs

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}