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
use std::process::Command;
use which::which;

#[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#"no such command: `{0}`
        
        {1}View all installed plugins with `soroban --list`"#
    )]
    ExecutableNotFound(String, String),
    #[error(transparent)]
    Which(#[from] which::Error),
}

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)
    };
    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 > 0.5f64)
                .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).unwrap();
    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| !(is_hex_string(s) && s.len() > MAX_HEX_LENGTH))
        .map(|s| s.replace("soroban-", ""))
        .collect())
}

fn is_hex_string(s: &str) -> bool {
    s.chars().all(|s| s.is_ascii_hexdigit())
}