Skip to main content

harmont_cli/
dispatcher.rs

1//! Subcommand-plugin dispatcher.
2//!
3//! Routes `hm <unknown-verb> <args...>` to the registered plugin
4//! whose manifest's `SubcommandSpec.verb` matches the first argv
5//! token. The plugin parses its own argv internally; the host
6//! forwards the raw args.
7
8#![allow(
9    clippy::print_stderr,
10    reason = "this is a top-level dispatch site; ExitInfo.message is user-facing output to stderr"
11)]
12
13use std::collections::BTreeMap;
14
15use anyhow::{Context, Result};
16use hm_plugin_protocol::{ExitInfo, SubcommandInput};
17
18use crate::error::HmError;
19use crate::plugin::{PluginRegistry, RegistryConfig};
20
21/// Entry point: invoke a plugin-provided subcommand. `argv` is the
22/// captured `external_subcommand` args INCLUDING the verb itself (clap's
23/// convention). Returns the process exit code.
24///
25/// # Errors
26/// Returns an error if no plugin claims the verb, the plugin fails to
27/// load, or the plugin panics during dispatch. Non-zero `ExitInfo.exit_code`
28/// is surfaced as `Ok(i32)`, not as `Err`.
29pub async fn run(argv: Vec<String>) -> Result<i32> {
30    let verb = argv
31        .first()
32        .cloned()
33        .ok_or_else(|| anyhow::anyhow!("dispatcher called with empty argv (clap bug)"))?;
34
35    let registry = PluginRegistry::load(RegistryConfig {
36        auto_discover: true,
37        extra_paths: vec![],
38        embedded: vec![
39            (
40                "harmont-docker",
41                crate::plugin::embedded::DOCKER_PLUGIN_WASM,
42            ),
43            (
44                "harmont-output-human",
45                crate::plugin::embedded::OUTPUT_HUMAN_PLUGIN_WASM,
46            ),
47            (
48                "harmont-output-json",
49                crate::plugin::embedded::OUTPUT_JSON_PLUGIN_WASM,
50            ),
51            ("harmont-cloud", crate::plugin::embedded::CLOUD_PLUGIN_WASM),
52        ],
53        pool_sizes: BTreeMap::new(),
54    })
55    .context("load plugin registry")?;
56
57    let idx = registry
58        .subcommand_index
59        .get(&verb)
60        .copied()
61        .ok_or_else(|| HmError::UnknownVerb {
62            verb: verb.clone(),
63            available: registry.subcommand_index.keys().cloned().collect(),
64        })?;
65
66    let plugin = registry
67        .get(idx)
68        .context("plugin moved away during dispatch")?;
69
70    let env: BTreeMap<String, String> = std::env::vars()
71        .filter(|(k, _)| k.starts_with("HARMONT_"))
72        .collect();
73
74    let input = SubcommandInput {
75        verb_path: argv.clone(),
76        args: serde_json::Value::Null, // plugin parses raw argv itself
77        env,
78    };
79
80    let info: ExitInfo = plugin
81        .call_capability("hm_subcommand_run", &input)
82        .await
83        .with_context(|| format!("invoke plugin for verb '{verb}'"))?;
84
85    if let Some(msg) = info.message {
86        eprintln!("{msg}");
87    }
88    Ok(info.exit_code)
89}