1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
use std::process::Command;

use clap::CommandFactory;
use which::which;

use crate::{utils, Root};

#[derive(thiserror::Error, Debug)]
pub enum Error {
    #[error("Plugin not provided. Should be `soroban plugin` for a binary `soroban-plugin`")]
    MissingSubcommand,
    #[error(transparent)]
    IO(#[from] std::io::Error),
    #[error(
        r#"error: no such command: `{0}`
        
        {1}View all installed plugins with `soroban --list`"#
    )]
    ExecutableNotFound(String, String),
    #[error(transparent)]
    Which(#[from] which::Error),
    #[error(transparent)]
    Regex(#[from] regex::Error),
}

const SUBCOMMAND_TOLERANCE: f64 = 0.75;
const PLUGIN_TOLERANCE: f64 = 0.75;
const MIN_LENGTH: usize = 4;

/// Tries to run a plugin, if the plugin's name is similar enough to any of the current subcommands return Ok.
/// Otherwise only errors can be returned because this process will exit with the plugin.
pub fn run() -> Result<(), Error> {
    let (name, args) = {
        let mut args = std::env::args().skip(1);
        let name = args.next().ok_or(Error::MissingSubcommand)?;
        (name, args)
    };

    if Root::command().get_subcommands().any(|c| {
        let sc_name = c.get_name();
        sc_name.starts_with(&name)
            || (name.len() >= MIN_LENGTH && strsim::jaro(sc_name, &name) >= SUBCOMMAND_TOLERANCE)
    }) {
        return Ok(());
    }

    let bin = which(format!("soroban-{name}")).map_err(|_| {
        let suggestion = if let Ok(bins) = list() {
            let suggested_name = bins
                .iter()
                .map(|b| (b, strsim::jaro_winkler(&name, b)))
                .filter(|(_, i)| *i > PLUGIN_TOLERANCE)
                .min_by(|a, b| a.1.total_cmp(&b.1))
                .map(|(a, _)| a.to_string())
                .unwrap_or_default();
            if suggested_name.is_empty() {
                suggested_name
            } else {
                format!(
                    r#"Did you mean `{suggested_name}`?
        "#
                )
            }
        } else {
            String::new()
        };
        Error::ExecutableNotFound(name, suggestion)
    })?;
    std::process::exit(
        Command::new(bin)
            .args(args)
            .spawn()?
            .wait()?
            .code()
            .unwrap(),
    );
}

const MAX_HEX_LENGTH: usize = 10;

pub fn list() -> Result<Vec<String>, Error> {
    let re_str = if cfg!(target_os = "windows") {
        r"^soroban-.*.exe$"
    } else {
        r"^soroban-.*"
    };
    let re = regex::Regex::new(re_str)?;
    Ok(which::which_re(re)?
        .filter_map(|b| {
            let s = b.file_name()?.to_str()?;
            Some(s.strip_suffix(".exe").unwrap_or(s).to_string())
        })
        .filter(|s| !(utils::is_hex_string(s) && s.len() > MAX_HEX_LENGTH))
        .map(|s| s.replace("soroban-", ""))
        .collect())
}