1use anyhow::Result;
9use clap::Subcommand;
10use colored::Colorize;
11use serde::Serialize;
12
13use crate::output::OutputFormat;
14use crate::plugins::{PluginConfig, PluginEntry, PluginManager};
15#[derive(Debug, Subcommand)]
18pub enum PluginCommands {
19 List,
21
22 Enable {
24 name: String,
26 },
27
28 Disable {
30 name: String,
32 },
33
34 Trust {
36 name: String,
38 },
39
40 Verify {
42 name: String,
44 },
45
46 #[command(subcommand)]
48 Alias(AliasCommands),
49}
50
51#[derive(Debug, Subcommand)]
52pub enum AliasCommands {
53 List,
55
56 Add {
58 name: String,
60 command: String,
62 },
63
64 Remove {
66 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 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 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}