guth-cli 0.1.1

Headless plugin discovery and dispatch for the Guth file manager.
Documentation
use guth_cli::{
    discover_plugins, load_enabled_plugins, prune_uninstalled_enabled_plugins, run_plugin,
    set_plugin_enabled, PluginManifest,
};
use std::ffi::OsString;
use std::process::ExitCode;

fn main() -> ExitCode {
    match run(std::env::args_os().skip(1).collect()) {
        Ok(code) => ExitCode::from(code),
        Err(error) => {
            eprintln!("guth-cli: {error}");
            ExitCode::FAILURE
        }
    }
}

fn run(arguments: Vec<OsString>) -> Result<u8, String> {
    let Some(command) = arguments.first().and_then(|value| value.to_str()) else {
        print_help();
        return Ok(0);
    };
    match command {
        "plugins" => {
            let plugins = discover_plugins().map_err(|error| error.to_string())?;
            let enabled = load_enabled_plugins().map_err(|error| error.to_string())?;
            print_plugins(&plugins, &enabled);
            Ok(0)
        }
        "sync" => {
            let plugins = discover_plugins().map_err(|error| error.to_string())?;
            let enabled = load_enabled_plugins().map_err(|error| error.to_string())?;
            dispatch_capability(&plugins, &enabled, "sync", &arguments[1..])
        }
        "run" => {
            let plugins = discover_plugins().map_err(|error| error.to_string())?;
            let enabled = load_enabled_plugins().map_err(|error| error.to_string())?;
            let id = arguments
                .get(1)
                .and_then(|value| value.to_str())
                .ok_or_else(|| {
                    "usage: guth-cli run <plugin-uuid> [-- plugin arguments]".to_string()
                })?;
            let plugin = plugins
                .iter()
                .find(|plugin| plugin.id.to_string() == id)
                .ok_or_else(|| format!("plugin {id} is not installed"))?;
            if !enabled.contains(&plugin.id) {
                return Err(format!(
                    "plugin {id} is disabled; enable it in Guth or with `guth-cli enable {id}`"
                ));
            }
            dispatch(plugin, strip_separator(&arguments[2..]))
        }
        "enable" | "disable" => {
            let id = arguments
                .get(1)
                .and_then(|value| value.to_str())
                .ok_or_else(|| format!("usage: guth-cli {command} <plugin-uuid>"))?;
            let id = uuid::Uuid::parse_str(id).map_err(|_| "invalid plugin UUID".to_string())?;
            let plugins = discover_plugins().map_err(|error| error.to_string())?;
            if command == "enable" && !plugins.iter().any(|plugin| plugin.id == id) {
                return Err(format!("plugin {id} is not installed"));
            }
            set_plugin_enabled(id, command == "enable").map_err(|error| error.to_string())?;
            println!("Plugin {id} {command}d.");
            Ok(0)
        }
        "prune" => {
            let enabled = prune_uninstalled_enabled_plugins().map_err(|error| error.to_string())?;
            println!("Retained {} enabled plugins.", enabled.len());
            Ok(0)
        }
        "help" | "--help" | "-h" => {
            print_help();
            Ok(0)
        }
        "version" | "--version" | "-V" => {
            println!("guth-cli {}", env!("CARGO_PKG_VERSION"));
            Ok(0)
        }
        _ => Err(format!("unknown command: {command}")),
    }
}

fn dispatch_capability(
    plugins: &[PluginManifest],
    enabled: &std::collections::BTreeSet<uuid::Uuid>,
    capability: &str,
    arguments: &[OsString],
) -> Result<u8, String> {
    let installed = plugins
        .iter()
        .filter(|plugin| plugin.supports(capability))
        .collect::<Vec<_>>();
    let matching = plugins
        .iter()
        .filter(|plugin| plugin.supports(capability) && enabled.contains(&plugin.id))
        .collect::<Vec<_>>();
    let plugin = match matching.as_slice() {
        [] if !installed.is_empty() => {
            return Err(format!(
                "the installed {capability} plugin is disabled; enable it in Guth or with `guth-cli enable {}`",
                installed[0].id
            ))
        }
        [] => return Err(format!("no {capability} plugin is installed")),
        [plugin] => *plugin,
        _ => {
            return Err(format!(
                "multiple {capability} plugins are installed; use `guth-cli run`"
            ))
        }
    };
    dispatch(plugin, strip_separator(arguments))
}

fn dispatch(plugin: &PluginManifest, arguments: &[OsString]) -> Result<u8, String> {
    let status =
        run_plugin(plugin, arguments.iter().cloned()).map_err(|error| error.to_string())?;
    Ok(status.code().unwrap_or(1).clamp(0, u8::MAX as i32) as u8)
}

fn strip_separator(arguments: &[OsString]) -> &[OsString] {
    if arguments.first().is_some_and(|value| value == "--") {
        &arguments[1..]
    } else {
        arguments
    }
}

fn print_plugins(plugins: &[PluginManifest], enabled: &std::collections::BTreeSet<uuid::Uuid>) {
    if plugins.is_empty() {
        println!("No external Guth plugins installed.");
        return;
    }
    for plugin in plugins {
        println!(
            "{}  {}  {}  {}  {}",
            plugin.id,
            plugin.name,
            plugin.version,
            if enabled.contains(&plugin.id) {
                "enabled"
            } else {
                "disabled"
            },
            plugin.capabilities.join(",")
        );
    }
}

fn print_help() {
    println!(
        "Guth CLI {}\n\nUSAGE:\n    guth-cli plugins\n    guth-cli enable <plugin-uuid>\n    guth-cli disable <plugin-uuid>\n    guth-cli prune\n    guth-cli sync -- <arguments>\n    guth-cli run <plugin-uuid> -- <arguments>\n\nExternal plugin arguments and output are passed through without being persisted.",
        env!("CARGO_PKG_VERSION")
    );
}