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(
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}