soroban_cli/commands/
plugin.rs1use std::{path::PathBuf, process::Command};
2
3use clap::CommandFactory;
4use which::which;
5
6use crate::{utils, Root};
7
8#[derive(thiserror::Error, Debug)]
9pub enum Error {
10 #[error("Plugin not provided. Should be `stellar plugin` for a binary `stellar-plugin`")]
11 MissingSubcommand,
12 #[error(transparent)]
13 IO(#[from] std::io::Error),
14 #[error(
15 r#"error: no such command: `{0}`
16
17 {1}View all installed plugins with `stellar --list`"#
18 )]
19 ExecutableNotFound(String, String),
20 #[error(transparent)]
21 Which(#[from] which::Error),
22 #[error(transparent)]
23 Regex(#[from] regex::Error),
24}
25
26const SUBCOMMAND_TOLERANCE: f64 = 0.75;
27const PLUGIN_TOLERANCE: f64 = 0.75;
28const MIN_LENGTH: usize = 4;
29
30pub fn run() -> Result<(), Error> {
33 let (name, args) = {
34 let mut args = std::env::args().skip(1);
35 let name = args.next().ok_or(Error::MissingSubcommand)?;
36 (name, args)
37 };
38
39 if Root::command().get_subcommands().any(|c| {
40 let sc_name = c.get_name();
41 sc_name.starts_with(&name)
42 || (name.len() >= MIN_LENGTH && strsim::jaro(sc_name, &name) >= SUBCOMMAND_TOLERANCE)
43 }) {
44 return Ok(());
45 }
46
47 let bin = find_bin(&name).map_err(|_| {
48 let suggestion = if let Ok(bins) = list() {
49 let suggested_name = bins
50 .iter()
51 .map(|b| (b, strsim::jaro_winkler(&name, b)))
52 .filter(|(_, i)| *i > PLUGIN_TOLERANCE)
53 .min_by(|a, b| a.1.total_cmp(&b.1))
54 .map(|(a, _)| a.to_string())
55 .unwrap_or_default();
56
57 if suggested_name.is_empty() {
58 suggested_name
59 } else {
60 format!(
61 r#"Did you mean `{suggested_name}`?
62 "#
63 )
64 }
65 } else {
66 String::new()
67 };
68
69 Error::ExecutableNotFound(name, suggestion)
70 })?;
71
72 std::process::exit(
73 Command::new(bin)
74 .args(args)
75 .spawn()?
76 .wait()?
77 .code()
78 .unwrap(),
79 );
80}
81
82const MAX_HEX_LENGTH: usize = 10;
83
84fn find_bin(name: &str) -> Result<PathBuf, which::Error> {
85 if let Ok(path) = which(format!("stellar-{name}")) {
86 Ok(path)
87 } else {
88 which(format!("soroban-{name}"))
89 }
90}
91
92pub fn list() -> Result<Vec<String>, Error> {
93 let re_str = if cfg!(target_os = "windows") {
94 r"^(soroban|stellar)-.*.exe$"
95 } else {
96 r"^(soroban|stellar)-.*"
97 };
98
99 let re = regex::Regex::new(re_str)?;
100
101 Ok(which::which_re(re)?
102 .filter_map(|b| {
103 let s = b.file_name()?.to_str()?;
104 Some(s.strip_suffix(".exe").unwrap_or(s).to_string())
105 })
106 .filter(|s| !(utils::is_hex_string(s) && s.len() > MAX_HEX_LENGTH))
107 .map(|s| s.replace("soroban-", "").replace("stellar-", ""))
108 .collect())
109}