Skip to main content

raps_cli/commands/
plugin.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2024-2025 Dmytro Yemelianov
3
4//! Plugin Management Commands
5//!
6//! Commands for managing RAPS plugins, hooks, and aliases.
7
8use anyhow::Result;
9use clap::Subcommand;
10use colored::Colorize;
11use serde::Serialize;
12
13use crate::output::OutputFormat;
14use crate::plugins::{PluginConfig, PluginEntry, PluginManager};
15// use raps_kernel::output::OutputFormat;
16
17#[derive(Debug, Subcommand)]
18pub enum PluginCommands {
19    /// List discovered and configured plugins
20    List,
21
22    /// Enable a plugin
23    Enable {
24        /// Plugin name
25        name: String,
26    },
27
28    /// Disable a plugin
29    Disable {
30        /// Plugin name
31        name: String,
32    },
33
34    /// Trust a plugin by recording its current SHA-256 hash
35    Trust {
36        /// Plugin name
37        name: String,
38    },
39
40    /// Verify a plugin's integrity (hash and optional signature)
41    Verify {
42        /// Plugin name
43        name: String,
44    },
45
46    /// Manage command aliases
47    #[command(subcommand)]
48    Alias(AliasCommands),
49}
50
51#[derive(Debug, Subcommand)]
52pub enum AliasCommands {
53    /// List all configured aliases
54    List,
55
56    /// Add a new alias
57    Add {
58        /// Alias name
59        name: String,
60        /// Command to alias (e.g., "object upload --resume")
61        command: String,
62    },
63
64    /// Remove an alias
65    Remove {
66        /// Alias name
67        name: String,
68    },
69}
70
71impl PluginCommands {
72    pub fn execute(self, output_format: OutputFormat) -> Result<()> {
73        match self {
74            PluginCommands::List => list_plugins(output_format),
75            PluginCommands::Enable { name } => enable_plugin(&name, output_format),
76            PluginCommands::Disable { name } => disable_plugin(&name, output_format),
77            PluginCommands::Trust { name } => trust_plugin(&name, output_format),
78            PluginCommands::Verify { name } => verify_plugin(&name, output_format),
79            PluginCommands::Alias(cmd) => cmd.execute(output_format),
80        }
81    }
82}
83
84impl AliasCommands {
85    pub fn execute(self, output_format: OutputFormat) -> Result<()> {
86        match self {
87            AliasCommands::List => list_aliases(output_format),
88            AliasCommands::Add { name, command } => add_alias(&name, &command, output_format),
89            AliasCommands::Remove { name } => remove_alias(&name, output_format),
90        }
91    }
92}
93
94#[derive(Serialize)]
95struct PluginOutput {
96    name: String,
97    path: String,
98    enabled: bool,
99    description: Option<String>,
100}
101
102fn list_plugins(output_format: OutputFormat) -> Result<()> {
103    let manager = PluginManager::default();
104    let plugins = manager.list_plugins();
105
106    let outputs: Vec<PluginOutput> = plugins
107        .iter()
108        .map(|p| PluginOutput {
109            name: p.name.clone(),
110            path: p.path.to_string_lossy().to_string(),
111            enabled: p.enabled,
112            description: None,
113        })
114        .collect();
115
116    match output_format {
117        OutputFormat::Table => {
118            if outputs.is_empty() {
119                println!("{}", "No plugins discovered.".yellow());
120                println!("\n{}", "To create a plugin:".dimmed());
121                println!(
122                    "  1. Create an executable named {} in your PATH",
123                    "raps-<name>".cyan()
124                );
125                println!("  2. Run {} to see it listed", "raps plugin list".cyan());
126            } else {
127                println!("\n{}", "Discovered Plugins:".bold());
128                println!("{}", "─".repeat(80));
129                println!(
130                    "  {:<20} {:<45} {}",
131                    "Name".bold(),
132                    "Path".bold(),
133                    "Status".bold()
134                );
135                println!("{}", "─".repeat(80));
136
137                for plugin in &outputs {
138                    let status = if plugin.enabled {
139                        "✓ enabled".green().to_string()
140                    } else {
141                        "✗ disabled".red().to_string()
142                    };
143                    println!(
144                        "  {:<20} {:<45} {}",
145                        plugin.name.cyan(),
146                        truncate_str(&plugin.path, 45),
147                        status
148                    );
149                }
150
151                println!("{}", "─".repeat(80));
152                println!("{} {} plugin(s) found", "→".cyan(), outputs.len());
153            }
154        }
155        _ => {
156            output_format.write(&outputs)?;
157        }
158    }
159
160    Ok(())
161}
162
163fn enable_plugin(name: &str, output_format: OutputFormat) -> Result<()> {
164    let mut config = PluginConfig::load()?;
165
166    // Update or create the plugin entry
167    if let Some(entry) = config.plugins.get_mut(name) {
168        entry.enabled = true;
169    } else {
170        config.plugins.insert(
171            name.to_string(),
172            PluginEntry {
173                enabled: true,
174                path: None,
175                description: None,
176                sha256: None,
177                public_key: None,
178                signature: None,
179                trusted: false,
180            },
181        );
182    }
183
184    config.save()?;
185
186    match output_format {
187        OutputFormat::Table => {
188            println!("{} Plugin '{}' enabled", "✓".green().bold(), name.cyan());
189        }
190        _ => {
191            output_format.write(&serde_json::json!({
192                "plugin": name,
193                "enabled": true
194            }))?;
195        }
196    }
197
198    Ok(())
199}
200
201fn disable_plugin(name: &str, output_format: OutputFormat) -> Result<()> {
202    let mut config = PluginConfig::load()?;
203
204    // Update or create the plugin entry
205    if let Some(entry) = config.plugins.get_mut(name) {
206        entry.enabled = false;
207    } else {
208        config.plugins.insert(
209            name.to_string(),
210            PluginEntry {
211                enabled: false,
212                path: None,
213                description: None,
214                sha256: None,
215                public_key: None,
216                signature: None,
217                trusted: false,
218            },
219        );
220    }
221
222    config.save()?;
223
224    match output_format {
225        OutputFormat::Table => {
226            println!("{} Plugin '{}' disabled", "✓".green().bold(), name.cyan());
227        }
228        _ => {
229            output_format.write(&serde_json::json!({
230                "plugin": name,
231                "enabled": false
232            }))?;
233        }
234    }
235
236    Ok(())
237}
238
239#[derive(Serialize)]
240struct AliasOutput {
241    name: String,
242    command: String,
243}
244
245fn list_aliases(output_format: OutputFormat) -> Result<()> {
246    let config = PluginConfig::load()?;
247
248    let outputs: Vec<AliasOutput> = config
249        .aliases
250        .iter()
251        .map(|(name, cmd)| AliasOutput {
252            name: name.clone(),
253            command: cmd.clone(),
254        })
255        .collect();
256
257    match output_format {
258        OutputFormat::Table => {
259            if outputs.is_empty() {
260                println!("{}", "No aliases configured.".yellow());
261                println!("\n{}", "To add an alias:".dimmed());
262                println!("  {}", "raps plugin alias add <name> \"<command>\"".cyan());
263                println!("\n{}", "Example:".dimmed());
264                println!(
265                    "  {}",
266                    "raps plugin alias add up \"object upload --resume\"".cyan()
267                );
268            } else {
269                println!("\n{}", "Configured Aliases:".bold());
270                println!("{}", "─".repeat(70));
271                println!("  {:<15} {}", "Alias".bold(), "Command".bold());
272                println!("{}", "─".repeat(70));
273
274                for alias in &outputs {
275                    println!("  {:<15} {}", alias.name.cyan(), alias.command);
276                }
277
278                println!("{}", "─".repeat(70));
279                println!("{} {} alias(es) configured", "→".cyan(), outputs.len());
280            }
281        }
282        _ => {
283            output_format.write(&outputs)?;
284        }
285    }
286
287    Ok(())
288}
289
290fn add_alias(name: &str, command: &str, output_format: OutputFormat) -> Result<()> {
291    let mut config = PluginConfig::load()?;
292    config.aliases.insert(name.to_string(), command.to_string());
293    config.save()?;
294
295    match output_format {
296        OutputFormat::Table => {
297            println!("{} Alias '{}' added", "✓".green().bold(), name.cyan());
298            println!(
299                "  {} {} → {}",
300                "Usage:".dimmed(),
301                format!("raps {}", name).cyan(),
302                command
303            );
304        }
305        _ => {
306            output_format.write(&serde_json::json!({
307                "alias": name,
308                "command": command
309            }))?;
310        }
311    }
312
313    Ok(())
314}
315
316fn remove_alias(name: &str, output_format: OutputFormat) -> Result<()> {
317    let mut config = PluginConfig::load()?;
318
319    if config.aliases.remove(name).is_some() {
320        config.save()?;
321
322        match output_format {
323            OutputFormat::Table => {
324                println!("{} Alias '{}' removed", "✓".green().bold(), name.cyan());
325            }
326            _ => {
327                output_format.write(&serde_json::json!({
328                    "alias": name,
329                    "removed": true
330                }))?;
331            }
332        }
333    } else {
334        match output_format {
335            OutputFormat::Table => {
336                println!("{} Alias '{}' not found", "!".yellow().bold(), name);
337            }
338            _ => {
339                output_format.write(&serde_json::json!({
340                    "alias": name,
341                    "error": "not found"
342                }))?;
343            }
344        }
345    }
346
347    Ok(())
348}
349
350fn trust_plugin(name: &str, output_format: OutputFormat) -> Result<()> {
351    let manager = PluginManager::default();
352    let hash = manager.trust_plugin(name)?;
353
354    match output_format {
355        OutputFormat::Table => {
356            println!(
357                "{} Plugin '{}' trusted with SHA-256: {}",
358                "✓".green().bold(),
359                name.cyan(),
360                &hash[..16]
361            );
362        }
363        _ => {
364            output_format.write(&serde_json::json!({
365                "plugin": name,
366                "trusted": true,
367                "sha256": hash
368            }))?;
369        }
370    }
371
372    Ok(())
373}
374
375fn verify_plugin(name: &str, output_format: OutputFormat) -> Result<()> {
376    let manager = PluginManager::default();
377    let result = manager.verify_plugin(name)?;
378
379    match output_format {
380        OutputFormat::Table => {
381            println!("\n{} {}", "Plugin:".bold(), result.name.cyan());
382            println!("  {:<16} {}", "Path:".dimmed(), result.path.display());
383            println!("  {:<16} {}", "SHA-256:".dimmed(), result.current_hash);
384
385            if let Some(ref recorded) = result.recorded_hash {
386                let status = if result.hash_match {
387                    "match".green().to_string()
388                } else {
389                    "MISMATCH".red().bold().to_string()
390                };
391                println!("  {:<16} {} ({})", "Recorded:".dimmed(), recorded, status);
392            } else {
393                println!(
394                    "  {:<16} {}",
395                    "Recorded:".dimmed(),
396                    "none (not yet trusted)".yellow()
397                );
398            }
399
400            if result.has_signature {
401                let sig_status = if result.signature_valid {
402                    "valid".green().bold().to_string()
403                } else {
404                    "INVALID".red().bold().to_string()
405                };
406                println!("  {:<16} {}", "Signature:".dimmed(), sig_status);
407            }
408
409            let trust_status = if result.has_signature && result.signature_valid {
410                "signed + verified".green().to_string()
411            } else if result.hash_match && result.trusted {
412                "TOFU hash verified".green().to_string()
413            } else if !result.hash_match && result.recorded_hash.is_some() {
414                "UNTRUSTED — hash changed".red().bold().to_string()
415            } else {
416                "not yet trusted".yellow().to_string()
417            };
418            println!("  {:<16} {}", "Trust:".dimmed(), trust_status);
419        }
420        _ => {
421            output_format.write(&serde_json::json!({
422                "plugin": result.name,
423                "path": result.path.to_string_lossy(),
424                "sha256": result.current_hash,
425                "recorded_sha256": result.recorded_hash,
426                "hash_match": result.hash_match,
427                "has_signature": result.has_signature,
428                "signature_valid": result.signature_valid,
429                "trusted": result.trusted,
430            }))?;
431        }
432    }
433
434    Ok(())
435}
436
437fn truncate_str(s: &str, max_len: usize) -> String {
438    if s.len() <= max_len {
439        s.to_string()
440    } else {
441        format!("{}...", &s[..max_len - 3])
442    }
443}