Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cmd/crates/soroban-test/tests/it/plugin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ fn has_no_path_failure() {
.unwrap_or_else(|_| assert_cmd::Command::new("stellar"))
.arg("hello")
.assert()
.stderr(predicates::str::contains("error: no such command: `hello`"));
.stderr(predicates::str::contains("unrecognized subcommand 'hello'"));
}

fn target_bin() -> PathBuf {
Expand Down
6 changes: 5 additions & 1 deletion cmd/soroban-cli/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,13 +83,16 @@ impl Root {
Self::try_parse().map_err(|e| {
if std::env::args().any(|s| s == "--list") {
let plugins = plugin::list().unwrap_or_default();

if plugins.is_empty() {
println!("No Plugins installed. E.g. soroban-hello");
println!("No Plugins installed. E.g. stellar-hello");
} else {
println!("Installed Plugins:\n {}", plugins.join("\n "));
}

std::process::exit(0);
}

match e.kind() {
ErrorKind::InvalidSubcommand => match plugin::run() {
Ok(()) => Error::Clap(e),
Expand All @@ -107,6 +110,7 @@ impl Root {
{
Self::from_arg_matches_mut(&mut Self::command().get_matches_from(itr))
}

pub async fn run(&mut self) -> Result<(), Error> {
match &mut self.cmd {
Cmd::Completion(completion) => completion.run(),
Expand Down
103 changes: 41 additions & 62 deletions cmd/soroban-cli/src/commands/plugin.rs
Original file line number Diff line number Diff line change
@@ -1,82 +1,33 @@
use std::{path::PathBuf, process::Command};

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

use crate::{utils, Root};
use crate::utils;

#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("Plugin not provided. Should be `stellar plugin` for a binary `stellar-plugin`")]
MissingSubcommand,
#[error(transparent)]
IO(#[from] std::io::Error),
#[error(
r"error: no such command: `{0}`

{1}View all installed plugins with `stellar --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(());
if let Some((plugin_bin, args)) = find_plugin() {
std::process::exit(
Command::new(plugin_bin)
.args(args)
.spawn()?
.wait()?
.code()
.unwrap(),
);
}

let bin = find_bin(&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(),
);
Ok(())
}

const MAX_HEX_LENGTH: usize = 10;
Expand Down Expand Up @@ -107,3 +58,31 @@ pub fn list() -> Result<Vec<String>, Error> {
.map(|s| s.replace("soroban-", "").replace("stellar-", ""))
.collect())
}

fn find_plugin() -> Option<(PathBuf, Vec<String>)> {
let args_vec: Vec<String> = std::env::args().skip(1).collect();
let mut chain: Vec<String> = args_vec
.iter()
.take_while(|arg| !arg.starts_with("--"))
.map(ToString::to_string)
.collect();

while !chain.is_empty() {
let name = chain.join("-");
let bin = find_bin(&name).ok();

if let Some(bin) = &bin {
let index = chain.len();
let args = args_vec[index..]
.iter()
.map(ToString::to_string)
.collect::<Vec<String>>();

return Some((bin.into(), args));
}

chain.pop();
}

None
}