harmont_cli/builtin/
plugin.rs1use anyhow::{Context, Result};
8
9use crate::cli::PluginCommand;
10use crate::plugin::{PluginRegistry, RegistryConfig, paths};
11
12pub async fn run(cmd: PluginCommand) -> Result<()> {
21 match cmd {
22 PluginCommand::List => list().await,
23 PluginCommand::Info { name } => info(&name).await,
24 PluginCommand::Install { source, pin } => install_cmd(&source, pin.as_deref()).await,
25 PluginCommand::Remove { name } => remove(&name).await,
26 }
27}
28
29#[allow(clippy::print_stdout)]
32#[allow(clippy::unused_async)]
35async fn list() -> Result<()> {
36 let reg = PluginRegistry::load(RegistryConfig {
37 auto_discover: true,
38 ..Default::default()
39 })?;
40 if reg.manifests().count() == 0 {
41 println!("No plugins installed.");
42 println!();
43 println!("Plugins live in:");
44 if let Some(p) = paths::user_plugins_dir() {
45 println!(" {}", p.display());
46 }
47 if let Some(p) = paths::project_plugins_dir() {
48 println!(" {}", p.display());
49 }
50 println!();
51 println!("Install one with `hm plugin install <path-or-url>`.");
52 return Ok(());
53 }
54 println!("{:<28} {:>10} capabilities", "name", "version");
55 for m in reg.manifests() {
56 let caps: Vec<String> = m.capabilities.iter().map(capability_summary).collect();
57 println!("{:<28} {:>10} {}", m.name, m.version, caps.join(", "));
58 }
59 Ok(())
60}
61
62#[allow(clippy::print_stdout)]
64#[allow(clippy::unused_async)]
65async fn info(name: &str) -> Result<()> {
66 let reg = PluginRegistry::load(RegistryConfig {
67 auto_discover: true,
68 ..Default::default()
69 })?;
70 let m = reg
71 .manifests()
72 .find(|m| m.name == name)
73 .with_context(|| format!("no plugin named '{name}' is installed"))?;
74 let json = serde_json::to_string_pretty(m)?;
75 println!("{json}");
76 Ok(())
77}
78
79#[allow(clippy::print_stdout)]
81async fn install_cmd(source: &str, pin: Option<&str>) -> Result<()> {
82 let path = crate::plugin::install::install(source, pin).await?;
83 println!("Installed plugin to {}", path.display());
84 Ok(())
85}
86
87#[allow(clippy::print_stdout)]
89#[allow(clippy::unused_async)]
90async fn remove(name: &str) -> Result<()> {
91 let dir = crate::plugin::paths::install_dir().context("no install dir")?;
92 let target = dir.join(format!("{name}.wasm"));
93 if !target.is_file() {
94 anyhow::bail!("no plugin file at {}", target.display());
95 }
96 std::fs::remove_file(&target).context("remove plugin")?;
97 println!("Removed {}", target.display());
98 Ok(())
99}
100
101fn capability_summary(cap: &hm_plugin_protocol::Capability) -> String {
102 use hm_plugin_protocol::Capability::{
103 LifecycleHook, OutputFormatter, StepExecutor, Subcommand,
104 };
105 match cap {
106 Subcommand(s) => format!("subcmd:{}", s.verb),
107 StepExecutor(s) => {
108 if s.default {
109 format!("runner:{}(*)", s.runner)
110 } else {
111 format!("runner:{}", s.runner)
112 }
113 }
114 LifecycleHook(_) => "hook".into(),
115 OutputFormatter(s) => format!("format:{}", s.name),
116 }
117}