diff --git a/Cargo.lock b/Cargo.lock index e1a76d404b..3a3dc49bd3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -465,17 +465,6 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" -[[package]] -name = "atty" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" -dependencies = [ - "hermit-abi 0.1.19", - "libc", - "winapi", -] - [[package]] name = "auditable-serde" version = "0.8.0" @@ -1561,61 +1550,32 @@ dependencies = [ [[package]] name = "clap" -version = "3.2.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123" -dependencies = [ - "atty", - "bitflags 1.3.2", - "clap_derive 3.2.25", - "clap_lex 0.2.4", - "indexmap 1.9.3", - "once_cell", - "strsim 0.10.0", - "termcolor", - "textwrap", -] - -[[package]] -name = "clap" -version = "4.5.20" +version = "4.5.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97f376d85a664d5837dbae44bf546e6477a679ff6610010f17276f686d867e8" +checksum = "1fc0e74a703892159f5ae7d3aac52c8e6c392f5ae5f359c70b5881d60aaac318" dependencies = [ "clap_builder", - "clap_derive 4.5.18", + "clap_derive", ] [[package]] name = "clap_builder" -version = "4.5.20" +version = "4.5.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19bc80abd44e4bed93ca373a0704ccbd1b710dc5749406201bb018272808dc54" +checksum = "b3e7f4214277f3c7aa526a59dd3fbe306a370daee1f8b7b8c987069cd8e888a8" dependencies = [ "anstream", "anstyle", - "clap_lex 0.7.2", + "clap_lex", "strsim 0.11.1", + "terminal_size", ] [[package]] name = "clap_derive" -version = "3.2.25" +version = "4.5.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae6371b8bdc8b7d3959e9cf7b22d4435ef3e79e138688421ec654acf8c81b008" -dependencies = [ - "heck 0.4.1", - "proc-macro-error", - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "clap_derive" -version = "4.5.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" +checksum = "14cb31bb0a7d536caef2639baa7fad459e15c3144efefa6dbd1c84562c4739f6" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -1625,18 +1585,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.2.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" -dependencies = [ - "os_str_bytes", -] - -[[package]] -name = "clap_lex" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" +checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" [[package]] name = "clearscreen" @@ -3905,15 +3856,6 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" -[[package]] -name = "hermit-abi" -version = "0.1.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" -dependencies = [ - "libc", -] - [[package]] name = "hermit-abi" version = "0.3.9" @@ -4931,7 +4873,7 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cc0bda45ed5b3a2904262c1bb91e526127aa70e7ef3758aba2ef93cf896b9b58" dependencies = [ - "clap 4.5.20", + "clap", "escape8259", "termcolor", "threadpool", @@ -6048,12 +5990,6 @@ dependencies = [ "pin-project-lite", ] -[[package]] -name = "os_str_bytes" -version = "6.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2355d85b9a3786f481747ced0e0ff2ba35213a1f9bd406ed906554d7af805a1" - [[package]] name = "outref" version = "0.5.1" @@ -6494,30 +6430,6 @@ dependencies = [ "toml_edit", ] -[[package]] -name = "proc-macro-error" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" -dependencies = [ - "proc-macro-error-attr", - "proc-macro2", - "quote", - "syn 1.0.109", - "version_check", -] - -[[package]] -name = "proc-macro-error-attr" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" -dependencies = [ - "proc-macro2", - "quote", - "version_check", -] - [[package]] name = "proc-macro-error-attr2" version = "2.0.0" @@ -8254,7 +8166,8 @@ dependencies = [ "async-trait", "bytes", "cargo-target-dep", - "clap 3.2.25", + "clap", + "clap_lex", "clearscreen 4.0.1", "comfy-table", "command-group", @@ -8326,6 +8239,7 @@ name = "spin-common" version = "3.5.0-pre0" dependencies = [ "anyhow", + "clap", "dirs 6.0.0", "sha2", "tempfile", @@ -9031,7 +8945,7 @@ name = "spin-runtime-factors" version = "3.5.0-pre0" dependencies = [ "anyhow", - "clap 3.2.25", + "clap", "spin-common", "spin-factor-key-value", "spin-factor-llm", @@ -9157,7 +9071,7 @@ name = "spin-trigger" version = "3.5.0-pre0" dependencies = [ "anyhow", - "clap 3.2.25", + "clap", "ctrlc", "futures", "sanitize-filename", @@ -9184,7 +9098,7 @@ name = "spin-trigger-http" version = "3.5.0-pre0" dependencies = [ "anyhow", - "clap 3.2.25", + "clap", "futures", "http 1.1.0", "http-body-util", @@ -9609,6 +9523,16 @@ dependencies = [ "termcolor", ] +[[package]] +name = "terminal_size" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45c6481c4829e4cc63825e62c49186a34538b7b2750b73b266581ffb612fb5ed" +dependencies = [ + "rustix 1.0.5", + "windows-sys 0.59.0", +] + [[package]] name = "terminfo" version = "0.8.0" @@ -9684,12 +9608,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "textwrap" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9" - [[package]] name = "thiserror" version = "1.0.69" @@ -10596,7 +10514,7 @@ dependencies = [ "async-recursion", "async-trait", "bytes", - "clap 4.5.20", + "clap", "dialoguer", "dirs 5.0.1", "futures-util", diff --git a/Cargo.toml b/Cargo.toml index fd502eaee7..2026e7cd28 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,8 +18,8 @@ rust-version = "1.86" anyhow = { workspace = true } async-trait = { workspace = true } bytes = { workspace = true } -# 'deprecated' enables deprecation warnings -clap = { workspace = true, features = ["deprecated", "derive", "env"] } +clap = { workspace = true, features = ["derive", "env", "string", "wrap_help"] } +clap_lex = "0.7.5" clearscreen = "4" comfy-table = "7" command-group = "2" @@ -125,7 +125,7 @@ async-trait = "0.1" base64 = "0.22" bytes = "1" chrono = "0.4" -clap = "3.2" +clap = "4.5.45" conformance-tests = { git = "https://github.com/fermyon/conformance-tests", rev = "61f2799f92b5d85f342cc07e3f5dec5cd0a7bc9c" } ctrlc = { version = "3.4", features = ["termination"] } dialoguer = "0.11" diff --git a/crates/common/Cargo.toml b/crates/common/Cargo.toml index cda3d03c17..60bded4542 100644 --- a/crates/common/Cargo.toml +++ b/crates/common/Cargo.toml @@ -6,6 +6,7 @@ edition = { workspace = true } [dependencies] anyhow = { workspace = true } +clap = { workspace = true } dirs = { workspace = true } sha2 = { workspace = true } tempfile = { workspace = true } diff --git a/crates/common/src/cli.rs b/crates/common/src/cli.rs new file mode 100644 index 0000000000..711e05ba53 --- /dev/null +++ b/crates/common/src/cli.rs @@ -0,0 +1,10 @@ +//! Common CLI code and constants + +use clap::builder::{styling::AnsiColor, Styles}; + +/// Clap [`Styles`] for Spin CLI and plugins. +pub const CLAP_STYLES: Styles = Styles::styled() + .header(AnsiColor::Yellow.on_default()) + .usage(AnsiColor::Green.on_default()) + .literal(AnsiColor::Green.on_default()) + .placeholder(AnsiColor::Green.on_default()); diff --git a/crates/common/src/lib.rs b/crates/common/src/lib.rs index f747d1544f..2925b10142 100644 --- a/crates/common/src/lib.rs +++ b/crates/common/src/lib.rs @@ -9,6 +9,7 @@ // - Code should have at least 2 dependents pub mod arg_parser; +pub mod cli; pub mod data_dir; pub mod paths; pub mod sha256; diff --git a/crates/expressions/src/template.rs b/crates/expressions/src/template.rs index 03bdd3aa24..48d572a24e 100644 --- a/crates/expressions/src/template.rs +++ b/crates/expressions/src/template.rs @@ -98,10 +98,7 @@ mod tests { let template = Template::new(tmpl).unwrap(); assert!( template.parts().eq(&expected), - "{:?} -> {:?} != {:?}", - tmpl, - template, - expected, + "{tmpl:?} -> {template:?} != {expected:?}", ); } } diff --git a/crates/trigger-http/src/lib.rs b/crates/trigger-http/src/lib.rs index 67a69d28ee..f7a693bca9 100644 --- a/crates/trigger-http/src/lib.rs +++ b/crates/trigger-http/src/lib.rs @@ -44,11 +44,11 @@ pub struct CliArgs { pub address: SocketAddr, /// The path to the certificate to use for https, if this is not set, normal http will be used. The cert should be in PEM format - #[clap(long, env = "SPIN_TLS_CERT", requires = "tls-key")] + #[clap(long, env = "SPIN_TLS_CERT", requires = "tls_key")] pub tls_cert: Option, /// The path to the certificate key to use for https, if this is not set, normal http will be used. The key should be in PKCS#8 format - #[clap(long, env = "SPIN_TLS_KEY", requires = "tls-cert")] + #[clap(long, env = "SPIN_TLS_KEY", requires = "tls_cert")] pub tls_key: Option, #[clap(long = "find-free-port")] diff --git a/crates/trigger/Cargo.toml b/crates/trigger/Cargo.toml index 44db6ff910..66fc35f8a8 100644 --- a/crates/trigger/Cargo.toml +++ b/crates/trigger/Cargo.toml @@ -16,7 +16,7 @@ unsafe-aot-compilation = [] [dependencies] anyhow = { workspace = true } -clap = { workspace = true, features = ["derive", "env"] } +clap = { workspace = true, features = ["derive", "env", "wrap_help"] } ctrlc = { workspace = true } futures = { workspace = true } sanitize-filename = "0.5" diff --git a/crates/trigger/src/cli.rs b/crates/trigger/src/cli.rs index 2f41ba1b72..4e2ac8dc58 100644 --- a/crates/trigger/src/cli.rs +++ b/crates/trigger/src/cli.rs @@ -9,7 +9,7 @@ use std::path::PathBuf; use std::{future::Future, sync::Arc}; use anyhow::{Context, Result}; -use clap::{Args, IntoApp, Parser}; +use clap::{Args, CommandFactory, Parser}; use spin_app::App; use spin_common::sloth; use spin_common::ui::quoted_path; @@ -41,6 +41,7 @@ pub const SPIN_WORKING_DIR: &str = "SPIN_WORKING_DIR"; /// A command that runs a TriggerExecutor. #[derive(Parser, Debug)] #[clap( + styles = spin_common::cli::CLAP_STYLES, override_usage = "spin [COMMAND] [OPTIONS]", next_help_heading = help_heading::() )] @@ -68,7 +69,6 @@ pub struct FactorsTriggerCommand, B: RuntimeFactorsBuilde long = "disable-cache", env = DISABLE_WASMTIME_CACHE, conflicts_with = WASMTIME_CACHE_FILE, - takes_value = false, )] pub disable_cache: bool, @@ -287,9 +287,9 @@ fn warn_if_wasm_build_slothful() -> sloth::SlothGuard { fn help_heading, F: RuntimeFactors>() -> Option<&'static str> { if T::TYPE == >::TYPE { - Some("TRIGGER OPTIONS") + Some("Trigger Options") } else { - let heading = format!("{} TRIGGER OPTIONS", T::TYPE.to_uppercase()); + let heading = format!("{} Trigger Options", T::TYPE.to_uppercase()); let as_str = Box::new(heading).leak(); Some(as_str) } diff --git a/src/bin/spin.rs b/src/bin/spin.rs index 5e8ba37ef0..ec8dc8aefe 100644 --- a/src/bin/spin.rs +++ b/src/bin/spin.rs @@ -1,30 +1,8 @@ -use anyhow::{Context, Error}; -use clap::{CommandFactory, FromArgMatches, Parser, Subcommand}; -use lazy_static::lazy_static; -use spin_cli::commands::external::predefined_externals; -use spin_cli::commands::maintenance::MaintenanceCommands; -use spin_cli::commands::{ - build::BuildCommand, - cloud::{DeployCommand, LoginCommand}, - doctor::DoctorCommand, - external::execute_external_subcommand, - new::{AddCommand, NewCommand}, - plugins::PluginCommands, - registry::RegistryCommands, - templates::TemplateCommands, - up::UpCommand, - watch::WatchCommand, -}; -use spin_cli::{build_info::*, subprocess::ExitStatusError}; -use spin_runtime_factors::FactorsBuilder; -use spin_trigger::cli::help::HelpArgsOnlyTrigger; -use spin_trigger::cli::FactorsTriggerCommand; -use spin_trigger_http::HttpTrigger; -use spin_trigger_redis::RedisTrigger; +use spin_cli::subprocess::ExitStatusError; #[tokio::main] async fn main() { - if let Err(err) = _main().await { + if let Err(err) = spin_cli::run().await { let code = match err.downcast_ref::() { // If we encounter an `ExitStatusError` it means a subprocess has already // exited unsuccessfully and thus already printed error messages. No need @@ -42,44 +20,6 @@ async fn main() { } } -async fn _main() -> anyhow::Result<()> { - spin_telemetry::init(VERSION.to_string()).context("Failed to initialize telemetry")?; - - let plugin_help_entries = plugin_help_entries(); - - let mut cmd = SpinApp::command(); - for plugin in &plugin_help_entries { - let subcmd = clap::Command::new(plugin.display_text()) - .about(plugin.about.as_str()) - .allow_hyphen_values(true) - .disable_help_flag(true) - .arg( - clap::Arg::new("command") - .allow_hyphen_values(true) - .multiple_values(true), - ); - cmd = cmd.subcommand(subcmd); - } - - if !plugin_help_entries.is_empty() { - cmd = cmd.after_help("* implemented via plugin"); - } - - let matches = cmd.clone().get_matches(); - - if let Some((subcmd, _)) = matches.subcommand() { - if plugin_help_entries.iter().any(|e| e.name == subcmd) { - let command = std::env::args().skip(1).collect(); - return execute_external_subcommand(command, cmd).await; - } - } - - SpinApp::from_arg_matches(&matches)? - .run(cmd) - .await - .inspect_err(|err| tracing::debug!(?err)) -} - fn print_error_chain(err: anyhow::Error) { if let Some(cause) = err.source() { let is_multiple = cause.source().is_some(); @@ -93,136 +33,3 @@ fn print_error_chain(err: anyhow::Error) { } } } - -lazy_static! { - pub static ref VERSION: String = build_info(); -} - -/// Helper for passing VERSION to structopt. -fn version() -> &'static str { - &VERSION -} - -/// The Spin CLI -#[derive(Parser)] -#[clap( - name = "spin", - version = version() -)] -enum SpinApp { - #[clap(subcommand, alias = "template")] - Templates(TemplateCommands), - #[clap(alias = "n")] - New(NewCommand), - #[clap(alias = "a")] - Add(AddCommand), - #[clap(alias = "u")] - Up(UpCommand), - // acts as a cross-level subcommand shortcut -> `spin cloud deploy` - #[clap(alias = "d")] - Deploy(DeployCommand), - // acts as a cross-level subcommand shortcut -> `spin cloud login` - Login(LoginCommand), - #[clap(subcommand, alias = "oci")] - Registry(RegistryCommands), - #[clap(alias = "b")] - Build(BuildCommand), - #[clap(subcommand, alias = "plugin")] - Plugins(PluginCommands), - #[clap(subcommand, hide = true)] - Trigger(TriggerCommands), - #[clap(external_subcommand)] - External(Vec), - #[clap(alias = "w")] - Watch(WatchCommand), - Doctor(DoctorCommand), - #[clap(subcommand, hide = true)] - Maintenance(MaintenanceCommands), -} - -#[derive(Subcommand)] -enum TriggerCommands { - Http(FactorsTriggerCommand), - Redis(FactorsTriggerCommand), - #[clap(name = spin_cli::HELP_ARGS_ONLY_TRIGGER_TYPE, hide = true)] - HelpArgsOnly(FactorsTriggerCommand), -} - -impl SpinApp { - /// The main entry point to Spin. - pub async fn run(self, app: clap::Command<'_>) -> Result<(), Error> { - match self { - Self::Templates(cmd) => cmd.run().await, - Self::Up(cmd) => cmd.run().await, - Self::New(cmd) => cmd.run().await, - Self::Add(cmd) => cmd.run().await, - Self::Deploy(cmd) => cmd.run(SpinApp::command()).await, - Self::Login(cmd) => cmd.run(SpinApp::command()).await, - Self::Registry(cmd) => cmd.run().await, - Self::Build(cmd) => cmd.run().await, - Self::Trigger(TriggerCommands::Http(cmd)) => cmd.run().await, - Self::Trigger(TriggerCommands::Redis(cmd)) => cmd.run().await, - Self::Trigger(TriggerCommands::HelpArgsOnly(cmd)) => cmd.run().await, - Self::Plugins(cmd) => cmd.run().await, - Self::External(cmd) => execute_external_subcommand(cmd, app).await, - Self::Watch(cmd) => cmd.run().await, - Self::Doctor(cmd) => cmd.run().await, - Self::Maintenance(cmd) => cmd.run(SpinApp::command()).await, - } - } -} - -/// Returns build information, similar to: 0.1.0 (2be4034 2022-03-31). -fn build_info() -> String { - format!("{SPIN_VERSION} ({SPIN_COMMIT_SHA} {SPIN_COMMIT_DATE})") -} - -struct PluginHelpEntry { - name: String, - about: String, -} - -impl PluginHelpEntry { - fn from_plugin(plugin: &spin_plugins::manifest::PluginManifest) -> Option { - if hide_plugin_in_help(plugin) { - None - } else { - Some(Self { - name: plugin.name(), - about: plugin.description().unwrap_or_default().to_owned(), - }) - } - } - - fn display_text(&self) -> String { - format!("{}*", self.name) - } -} - -fn plugin_help_entries() -> Vec { - let mut entries = installed_plugin_help_entries(); - for (name, about) in predefined_externals() { - if !entries.iter().any(|e| e.name == name) { - entries.push(PluginHelpEntry { name, about }); - } - } - entries -} - -fn installed_plugin_help_entries() -> Vec { - let Ok(manager) = spin_plugins::manager::PluginManager::try_default() else { - return vec![]; - }; - let Ok(manifests) = manager.store().installed_manifests() else { - return vec![]; - }; - - manifests - .iter() - .filter_map(PluginHelpEntry::from_plugin) - .collect() -} - -fn hide_plugin_in_help(plugin: &spin_plugins::manifest::PluginManifest) -> bool { - plugin.name().starts_with("trigger-") -} diff --git a/src/commands/build.rs b/src/commands/build.rs index 35dd3e286a..73689066be 100644 --- a/src/commands/build.rs +++ b/src/commands/build.rs @@ -32,11 +32,7 @@ pub struct BuildCommand { /// By default, if the application manifest specifies one or more deployment targets, Spin /// checks that all components are compatible with those deployment targets. Specify /// this option to bypass those target checks. - #[clap( - long = "skip-target-checks", - alias = "skip-target-check", - takes_value = false - )] + #[clap(long = "skip-target-checks", alias = "skip-target-check")] skip_target_checks: bool, /// Run the application after building. @@ -62,15 +58,7 @@ impl BuildCommand { .await?; if self.up { - let mut cmd = UpCommand::parse_from( - std::iter::once(OsString::from(format!( - "{} up", - std::env::args().next().unwrap() - ))) - .chain(self.up_args), - ); - cmd.file_source = Some(manifest_file); - cmd.run().await + UpCommand::run_as_flag(manifest_file, self.up_args).await } else { Ok(()) } diff --git a/src/commands/cloud.rs b/src/commands/cloud.rs index 570eb327ff..a5abfb7ac4 100644 --- a/src/commands/cloud.rs +++ b/src/commands/cloud.rs @@ -35,24 +35,24 @@ const DEFAULT_DEPLOY_PLUGIN: &str = "cloud"; const DEPLOY_PLUGIN_ENV: &str = "SPIN_DEPLOY_PLUGIN"; impl DeployCommand { - pub async fn run(self, cmd: clap::Command<'_>) -> Result<()> { + pub async fn run(self) -> Result<()> { const SUBCMD: &str = "deploy"; let deploy_plugin = deployment_plugin(SUBCMD)?; let mut subcmd = vec![deploy_plugin, SUBCMD.to_string()]; subcmd.append(&mut self.args.clone()); - execute_external_subcommand(subcmd, cmd).await + execute_external_subcommand(subcmd).await } } impl LoginCommand { - pub async fn run(self, cmd: clap::Command<'_>) -> Result<()> { + pub async fn run(self) -> Result<()> { const SUBCMD: &str = "login"; let deploy_plugin = deployment_plugin(SUBCMD)?; - let mut subcmd = vec![deploy_plugin, SUBCMD.to_string()]; - subcmd.append(&mut self.args.clone()); - execute_external_subcommand(subcmd, cmd).await + let mut args = vec![deploy_plugin, SUBCMD.to_string()]; + args.append(&mut self.args.clone()); + execute_external_subcommand(args).await } } diff --git a/src/commands/external.rs b/src/commands/external.rs index 40823e2ffe..d98a8ba45d 100644 --- a/src/commands/external.rs +++ b/src/commands/external.rs @@ -2,6 +2,7 @@ use crate::build_info::*; use crate::commands::plugins::{update, Install}; use crate::opts::PLUGIN_OVERRIDE_COMPATIBILITY_CHECK_FLAG; use anyhow::{anyhow, Result}; +use clap::CommandFactory; use spin_common::ui::quoted_path; use spin_plugins::{ badger::BadgerChecker, error::Error as PluginError, manifest::warn_unsupported_version, @@ -52,19 +53,11 @@ pub fn predefined_externals() -> Vec<(String, String)> { /// Executes a Spin plugin as a subprocess, expecting the first argument to /// indicate the plugin to execute. Passes all subsequent arguments on to the /// subprocess. -pub async fn execute_external_subcommand( - subcmd: Vec, - cmd: clap::Command<'_>, -) -> anyhow::Result<()> { - let (plugin_name, args, override_compatibility_check) = parse_subcommand(subcmd)?; +pub async fn execute_external_subcommand(args: Vec) -> anyhow::Result<()> { + let (plugin_name, args, override_compatibility_check) = parse_subcommand(args)?; let plugin_store = PluginStore::try_default()?; - let plugin_version = ensure_plugin_available( - &plugin_name, - &plugin_store, - cmd, - override_compatibility_check, - ) - .await?; + let plugin_version = + ensure_plugin_available(&plugin_name, &plugin_store, override_compatibility_check).await?; let binary = plugin_store.installed_binary_path(&plugin_name); if !binary.exists() { @@ -115,7 +108,6 @@ fn set_kill_on_ctrl_c(child: &tokio::process::Child) { async fn ensure_plugin_available( plugin_name: &str, plugin_store: &PluginStore, - cmd: clap::Command<'_>, override_compatibility_check: bool, ) -> anyhow::Result> { let plugin_version = match plugin_store.read_plugin_manifest(plugin_name) { @@ -129,9 +121,7 @@ async fn ensure_plugin_available( } Some(manifest.version().to_owned()) } - Err(PluginError::NotFound(e)) => { - consider_install(plugin_name, plugin_store, cmd, &e).await? - } + Err(PluginError::NotFound(e)) => consider_install(plugin_name, plugin_store, &e).await?, Err(e) => return Err(e.into()), }; Ok(plugin_version) @@ -140,7 +130,6 @@ async fn ensure_plugin_available( async fn consider_install( plugin_name: &str, plugin_store: &PluginStore, - cmd: clap::Command<'_>, e: &spin_plugins::error::NotFoundError, ) -> anyhow::Result> { if predefined_externals() @@ -176,7 +165,7 @@ async fn consider_install( tracing::debug!("Tried to resolve {plugin_name} to plugin, got {e}"); terminal::error!("'{plugin_name}' is not a known Spin command. See spin --help.\n"); - print_similar_commands(cmd, plugin_name); + print_similar_commands(plugin_name); process::exit(2); } @@ -280,8 +269,8 @@ async fn report_badger_result(badger: tokio::task::JoinHandle) { } } -fn print_similar_commands(cmd: clap::Command, plugin_name: &str) { - let similar = similar_commands(cmd, plugin_name); +fn print_similar_commands(plugin_name: &str) { + let similar = similar_commands(plugin_name); match similar.len() { 0 => (), 1 => eprintln!("The most similar command is:"), @@ -295,8 +284,9 @@ fn print_similar_commands(cmd: clap::Command, plugin_name: &str) { } } -fn similar_commands(cmd: clap::Command, target: &str) -> Vec { - cmd.get_subcommands() +fn similar_commands(target: &str) -> Vec { + crate::SpinApp::command() + .get_subcommands() .filter_map(|sc| { let actual_name = undecorate(sc.get_name()); if levenshtein::levenshtein(&actual_name, target) <= 2 { diff --git a/src/commands/maintenance.rs b/src/commands/maintenance.rs index 0fbde7e922..f0fae2b886 100644 --- a/src/commands/maintenance.rs +++ b/src/commands/maintenance.rs @@ -1,6 +1,6 @@ use std::path::PathBuf; -use clap::{Parser, Subcommand}; +use clap::{CommandFactory, Parser, Subcommand}; /// Commands for Spin maintenance tasks. #[derive(Subcommand, Debug)] @@ -12,9 +12,9 @@ pub enum MaintenanceCommands { } impl MaintenanceCommands { - pub async fn run(&self, app: clap::Command<'_>) -> anyhow::Result<()> { + pub async fn run(&self) -> anyhow::Result<()> { match self { - MaintenanceCommands::GenerateReference(cmd) => cmd.run(app).await, + MaintenanceCommands::GenerateReference(cmd) => cmd.run().await, MaintenanceCommands::GenerateManifestSchema(cmd) => cmd.run().await, } } @@ -28,8 +28,8 @@ pub struct GenerateReference { } impl GenerateReference { - pub async fn run(&self, app: clap::Command<'_>) -> anyhow::Result<()> { - let markdown = crate::clap_markdown::help_markdown_command(&app); + pub async fn run(&self) -> anyhow::Result<()> { + let markdown = crate::clap_markdown::help_markdown_command(&crate::SpinApp::command()); write(&self.output, &markdown)?; Ok(()) } diff --git a/src/commands/new.rs b/src/commands/new.rs index a849744d67..c4f4b951d2 100644 --- a/src/commands/new.rs +++ b/src/commands/new.rs @@ -33,7 +33,7 @@ pub struct TemplateNewCommandCore { pub template_id: Option, /// Filter templates to select by tags. - #[clap(long = "tag", conflicts_with = "template-id")] + #[clap(long = "tag", conflicts_with = "template_id")] pub tags: Vec, /// The directory in which to create the new application or component. @@ -42,7 +42,7 @@ pub struct TemplateNewCommandCore { pub output_path: Option, /// Create the new application or component in the current directory. - #[clap(long = "init", takes_value = false, group = "location")] + #[clap(long, group = "location")] pub init: bool, /// Parameter values to be passed to the template (in name=value format). @@ -52,25 +52,21 @@ pub struct TemplateNewCommandCore { /// A TOML file which contains parameter values in name = "value" format. /// Parameters passed as CLI option overwrite parameters specified in the /// file. - #[clap(long = "values-file")] + #[clap(long)] pub values_file: Option, /// An optional argument that allows to skip prompts for the manifest file /// by accepting the defaults if available on the template - #[clap(short = 'a', long = "accept-defaults", takes_value = false)] + #[clap(short = 'a', long)] pub accept_defaults: bool, /// An optional argument that allows to skip creating .gitignore - #[clap(long = "no-vcs", takes_value = false)] + #[clap(long)] pub no_vcs: bool, /// If the output directory already contains files, generate the new files into /// it without confirming, overwriting any existing files with the same names. - #[clap( - long = "allow-overwrite", - alias = "allow-overwrites", - takes_value = false - )] + #[clap(long, alias = "allow-overwrites")] pub allow_overwrite: bool, } @@ -251,7 +247,7 @@ impl TemplateNewCommandCore { } } -#[derive(Debug)] +#[derive(Clone, Debug)] pub struct ParameterValue { pub name: String, pub value: String, diff --git a/src/commands/plugins.rs b/src/commands/plugins.rs index d960862d48..3d15c520e5 100644 --- a/src/commands/plugins.rs +++ b/src/commands/plugins.rs @@ -92,11 +92,11 @@ pub struct Install { pub remote_manifest_src: Option, /// Skips prompt to accept the installation of the plugin. - #[clap(short = 'y', long = "yes", takes_value = false)] + #[clap(short = 'y', long = "yes")] pub yes_to_all: bool, /// Overrides a failed compatibility check of the plugin with the current version of Spin. - #[clap(long = PLUGIN_OVERRIDE_COMPATIBILITY_CHECK_FLAG, takes_value = false)] + #[clap(long = PLUGIN_OVERRIDE_COMPATIBILITY_CHECK_FLAG)] pub override_compatibility_check: bool, /// Provide the value for the authorization header to be able to install a plugin from a private repository. @@ -189,7 +189,6 @@ pub struct Upgrade { conflicts_with = PLUGIN_NAME_OPT, conflicts_with = PLUGIN_REMOTE_PLUGIN_MANIFEST_OPT, conflicts_with = PLUGIN_LOCAL_PLUGIN_MANIFEST_OPT, - takes_value = false, )] pub all: bool, @@ -212,22 +211,22 @@ pub struct Upgrade { pub remote_manifest_src: Option, /// Skips prompt to accept the installation of the plugin[s]. - #[clap(short = 'y', long = "yes", takes_value = false)] + #[clap(short = 'y', long = "yes")] pub yes_to_all: bool, /// Provide the value for the authorization header to be able to install a plugin from a private repository. /// (e.g) --auth-header-value "Bearer " - #[clap(long = "auth-header-value", requires = PLUGIN_REMOTE_PLUGIN_MANIFEST_OPT)] + #[clap(long, requires = PLUGIN_REMOTE_PLUGIN_MANIFEST_OPT)] pub auth_header_value: Option, /// Overrides a failed compatibility check of the plugin with the current version of Spin. - #[clap(long = PLUGIN_OVERRIDE_COMPATIBILITY_CHECK_FLAG, takes_value = false)] + #[clap(long)] pub override_compatibility_check: bool, /// Specific version of a plugin to be install from the centralized plugins /// repository. #[clap( - long = "version", + long, short = 'v', conflicts_with = PLUGIN_REMOTE_PLUGIN_MANIFEST_OPT, conflicts_with = PLUGIN_LOCAL_PLUGIN_MANIFEST_OPT, @@ -237,7 +236,7 @@ pub struct Upgrade { pub version: Option, /// Allow downgrading a plugin's version. - #[clap(short = 'd', long = "downgrade", takes_value = false)] + #[clap(long, short = 'd')] pub downgrade: bool, } @@ -603,23 +602,23 @@ fn latest_and_rest( #[derive(Parser, Debug)] pub struct List { /// List only installed plugins. - #[clap(long = "installed", takes_value = false, group = "which")] + #[clap(long, group = "which")] pub installed: bool, /// List all versions of plugins. This is the default behaviour. - #[clap(long = "all", takes_value = false, group = "which")] + #[clap(long, group = "which")] pub all: bool, /// List latest and installed versions of plugins. - #[clap(long = "summary", takes_value = false, group = "which")] + #[clap(long, group = "which")] pub summary: bool, /// Filter the list to plugins containing this string. - #[clap(long = "filter")] + #[clap(long)] pub filter: Option, /// The format in which to list the templates. - #[clap(value_enum, long = "format", default_value = "plain")] + #[clap(value_enum, default_value = "plain")] pub format: ListFormat, } diff --git a/src/commands/registry.rs b/src/commands/registry.rs index 34eb8aad93..0d066b56ee 100644 --- a/src/commands/registry.rs +++ b/src/commands/registry.rs @@ -44,8 +44,7 @@ pub struct Push { #[clap( name = INSECURE_OPT, short = 'k', - long = "insecure", - takes_value = false, + long, )] pub insecure: bool, @@ -55,11 +54,11 @@ pub struct Push { /// different Spin runtime hosts. Turning composition off can optimise /// bandwidth for shared dependencies, but makes the pushed image incompatible /// with hosts that cannot carry out composition themselves. - #[clap(long = "compose", default_value_t = true)] + #[clap(long, default_value_t = true)] pub compose: bool, - /// Specifies to perform `spin build` (with the default options) before pushing the application. - #[clap(long, takes_value = false, env = ALWAYS_BUILD_ENV)] + /// Specifies to perform `spin build` before pushing the application. + #[clap(long, env = ALWAYS_BUILD_ENV)] pub build: bool, /// Reference in the registry of the Spin application. @@ -127,8 +126,7 @@ pub struct Pull { #[clap( name = INSECURE_OPT, short = 'k', - long = "insecure", - takes_value = false, + long, )] pub insecure: bool, @@ -158,19 +156,15 @@ impl Pull { #[derive(Parser, Debug)] pub struct Login { /// Username for the registry - #[clap(long = "username", short = 'u')] + #[clap(long, short = 'u')] pub username: Option, /// Password for the registry - #[clap(long = "password", short = 'p')] + #[clap(long, short = 'p')] pub password: Option, /// Take the password from stdin - #[clap( - long = "password-stdin", - takes_value = false, - conflicts_with = "password" - )] + #[clap(long, conflicts_with = "password")] pub password_stdin: bool, /// OCI registry server (e.g. ghcr.io) diff --git a/src/commands/templates.rs b/src/commands/templates.rs index 6d936c7383..1179ba06b8 100644 --- a/src/commands/templates.rs +++ b/src/commands/templates.rs @@ -479,11 +479,11 @@ pub struct List { pub tags: Vec, /// The format in which to list the templates. - #[clap(value_enum, long = "format", default_value = "table", hide = true)] + #[clap(value_enum, long, default_value = "table", hide = true)] pub format: ListFormat, /// Whether to show additional template details in the list. - #[clap(long = "verbose", takes_value = false)] + #[clap(long)] pub verbose: bool, } diff --git a/src/commands/up.rs b/src/commands/up.rs index 47c1763d5c..ecf3cdfd65 100644 --- a/src/commands/up.rs +++ b/src/commands/up.rs @@ -1,4 +1,5 @@ mod app_source; +mod parsing; use std::{ collections::{HashMap, HashSet}, @@ -37,13 +38,48 @@ const MULTI_TRIGGER_START_OFFSET: tokio::time::Duration = tokio::time::Duration: const MULTI_TRIGGER_LET_ALL_START: tokio::time::Duration = tokio::time::Duration::from_millis(500); /// Start the Fermyon runtime. +// NOTE: Most of the messy clap parsing details are in the child parsing module. +pub struct UpCommand(UpCommandInner); + +impl UpCommand { + pub async fn run(self) -> Result<()> { + // For displaying help, first print `spin up`'s own usage text, then + // attempt to load an app and print trigger-type-specific usage. + let help = self.0.help; + if help { + UpCommandInner::command() + .name("spin-up") + .bin_name("spin up") + .styles(spin_common::cli::CLAP_STYLES) + .print_help()?; + println!(); + } + self.0.run().await.or_else(|err| { + if help { + tracing::warn!("Error resolving trigger-specific help: {err:?}"); + Ok(()) + } else { + Err(err) + } + }) + } + + /// Runs the command as called by e.g. `spin build --up` + pub async fn run_as_flag( + manifest_file: PathBuf, + up_args: Vec, + ) -> std::result::Result<(), anyhow::Error> { + let args = [OsString::from("spin up")].into_iter().chain(up_args); + let mut cmd = UpCommand::parse_from(args); + cmd.0.file_source = Some(manifest_file); + cmd.0.run().await + } +} + +/// Argument parser for UpCommand. #[derive(Parser, Debug, Default)] -#[clap( - about = "Start the Spin application", - allow_hyphen_values = true, - disable_help_flag = true -)] -pub struct UpCommand { +#[clap(about = "Start the Spin application", disable_help_flag = true)] +struct UpCommandInner { #[clap(short = 'h', long = "help")] pub help: bool, @@ -83,8 +119,7 @@ pub struct UpCommand { #[clap( name = INSECURE_OPT, short = 'k', - long = "insecure", - takes_value = false, + long, )] pub insecure: bool, @@ -105,13 +140,13 @@ pub struct UpCommand { /// /// This allows you to update the assets on the host filesystem such that the updates are visible to the guest /// without a restart. This cannot be used with registry apps or apps which use file patterns and/or exclusions. - #[clap(long, takes_value = false)] + #[clap(long)] pub direct_mounts: bool, /// For local apps, specifies to perform `spin build` (with the default options) before running the application. /// /// This is ignored on remote applications, as they are already built. - #[clap(long, takes_value = false, env = ALWAYS_BUILD_ENV)] + #[clap(long, env = ALWAYS_BUILD_ENV)] pub build: bool, /// [Experimental] Component ID to run. This can be specified multiple times. The default is all components. @@ -119,33 +154,12 @@ pub struct UpCommand { pub components: Vec, /// All other args, to be passed through to the trigger - #[clap(hide = true)] + #[clap(skip)] pub trigger_args: Vec, } -impl UpCommand { - pub async fn run(self) -> Result<()> { - // For displaying help, first print `spin up`'s own usage text, then - // attempt to load an app and print trigger-type-specific usage. - let help = self.help; - if help { - Self::command() - .name("spin-up") - .bin_name("spin up") - .print_help()?; - println!(); - } - self.run_inner().await.or_else(|err| { - if help { - tracing::warn!("Error resolving trigger-specific help: {err:?}"); - Ok(()) - } else { - Err(err) - } - }) - } - - async fn run_inner(self) -> Result<()> { +impl UpCommandInner { + async fn run(self) -> Result<()> { let app_source = self.app_source(); if app_source == AppSource::None { @@ -696,7 +710,7 @@ mod test { fn can_infer_files() { let file = repo_path("examples/http-rust/spin.toml"); - let source = UpCommand { + let source = UpCommandInner { app_source: Some(file.clone()), ..Default::default() } @@ -709,7 +723,7 @@ mod test { fn can_infer_directories() { let dir = repo_path("examples/http-rust"); - let source = UpCommand { + let source = UpCommandInner { app_source: Some(dir.clone()), ..Default::default() } @@ -725,7 +739,7 @@ mod test { fn reject_nonexistent_files() { let file = repo_path("src/commands/biscuits.toml"); - let source = UpCommand { + let source = UpCommandInner { app_source: Some(file), ..Default::default() } @@ -738,7 +752,7 @@ mod test { fn reject_nonexistent_files_relative_path() { let file = "zoink/honk/biscuits.toml".to_owned(); // NOBODY CREATE THIS OKAY - let source = UpCommand { + let source = UpCommandInner { app_source: Some(file), ..Default::default() } @@ -751,7 +765,7 @@ mod test { fn reject_unsuitable_directories() { let dir = repo_path("src/commands"); - let source = UpCommand { + let source = UpCommandInner { app_source: Some(dir), ..Default::default() } @@ -764,7 +778,7 @@ mod test { fn can_infer_oci_registry_reference() { let reference = "ghcr.io/fermyon/noodles:v1".to_owned(); - let source = UpCommand { + let source = UpCommandInner { app_source: Some(reference.clone()), ..Default::default() } @@ -778,7 +792,7 @@ mod test { // Testing that the magic docker heuristic doesn't misfire here. let reference = "docker.io/fermyon/noodles".to_owned(); - let source = UpCommand { + let source = UpCommandInner { app_source: Some(reference.clone()), ..Default::default() } @@ -791,7 +805,7 @@ mod test { fn can_reject_complete_gibberish() { let garbage = repo_path("ftp://🤡***🤡 HELLO MR CLOWN?!"); - let source = UpCommand { + let source = UpCommandInner { app_source: Some(garbage), ..Default::default() } @@ -850,7 +864,7 @@ mod test { #[test] fn group_no_args_is_empty() { let cmd = UpCommand::try_parse_from(["up"]).unwrap(); - let groups = cmd.group_trigger_args(); + let groups = cmd.0.group_trigger_args(); assert!(groups.is_empty()); } @@ -858,7 +872,7 @@ mod test { fn can_group_valueful_flags() { let cmd = UpCommand::try_parse_from(["up", "--listen", "127.0.0.1:39453", "-L", "/fie"]).unwrap(); - let groups = cmd.group_trigger_args(); + let groups = cmd.0.group_trigger_args(); assert_eq!(2, groups.len()); assert_eq!(2, groups[0].len()); assert_eq!("--listen", groups[0][0]); @@ -880,7 +894,7 @@ mod test { "/fie", ]) .unwrap(); - let groups = cmd.group_trigger_args(); + let groups = cmd.0.group_trigger_args(); assert_eq!(4, groups.len()); assert_eq!(2, groups[0].len()); assert_eq!("--listen", groups[0][0]); @@ -905,7 +919,7 @@ mod test { "/fie", ]) .unwrap(); - let groups = cmd.group_trigger_args(); + let groups = cmd.0.group_trigger_args(); assert_eq!(3, groups.len()); assert_eq!(2, groups[0].len()); assert_eq!("--listen", groups[0][0]); diff --git a/src/commands/up/parsing.rs b/src/commands/up/parsing.rs new file mode 100644 index 0000000000..0009cb5902 --- /dev/null +++ b/src/commands/up/parsing.rs @@ -0,0 +1,142 @@ +//! This code works around incompatibilities between Clap 4 and `spin up`'s +//! messy trigger argument handling. +//! +//! It preprocesses the argument strings, splitting off those arguments +//! recognized by `spin up` itself by introspecting the arguments generated by +//! `UpCommandInner`'s `#[derive(Parser)]`. + +use std::{ + collections::HashMap, + ffi::{OsStr, OsString}, +}; + +use clap::{Args, Command, CommandFactory, Parser}; + +use super::{UpCommand, UpCommandInner}; + +impl Args for UpCommand { + fn augment_args(cmd: clap::Command) -> clap::Command { + // The FromArgMatches impl below depends on some restrictions on + // UpCommandInner, which we assert here to prevent bugs + for arg in UpCommandInner::command().get_arguments() { + assert!( + !arg.is_positional(), + "UpCommandInner cannot use positional arg {arg}" + ); + let num_args = arg.get_num_args().unwrap_or_default(); + assert!( + num_args.min_values() == num_args.max_values(), + "UpCommandInner cannot use args with variable number of values" + ); + } + + // Grab all arguments for later parsing + cmd.disable_help_flag(true).arg( + clap::Arg::new("args") + .action(clap::ArgAction::Append) + .allow_hyphen_values(true), + ) + } + + fn augment_args_for_update(_: clap::Command) -> clap::Command { + unimplemented!("UpCommand cannot be flattened") + } +} + +impl clap::FromArgMatches for UpCommand { + fn from_arg_matches(matches: &clap::ArgMatches) -> std::result::Result { + let cmd = UpCommandInner::command(); + + // Build our own maps of flags -> arguments, because clap doesn't like to share + let mut long_flags = HashMap::new(); + let mut short_flags = HashMap::new(); + for arg in cmd.get_arguments() { + for long in arg + .get_long() + .into_iter() + .chain(arg.get_all_aliases().unwrap_or_default()) + { + long_flags.insert(long, arg); + } + for short in arg + .get_short() + .into_iter() + .chain(arg.get_all_short_aliases().unwrap_or_default()) + { + short_flags.insert(short, arg); + } + } + + // Use clap's own lexer against it + let raw_args = clap_lex::RawArgs::new(matches.get_raw("args").into_iter().flatten()); + let mut cursor = raw_args.cursor(); + + // Split the arguments into those recognized by UpInnerCommand and all + // others. As a general strategy if we see something unexpected at this + // stage we let it through with the expectation that later parsing will + // catch it and produce a nice clap error. + let mut up_args = vec!["spin up".into()]; + let mut trigger_args = vec![]; + while let Some(parsed) = raw_args.next(&mut cursor) { + // --long flags + if let Some((Ok(long), eq_value)) = parsed.to_long() { + if let Some(arg) = long_flags.get(long) { + up_args.push(parsed.to_value_os().to_os_string()); + // We asserted earlier that there are a fixed number of values + let mut need_args = arg.get_num_args().map_or(0, |na| na.min_values()); + // For --key=value args `value` needs to be counted + if need_args > 0 && eq_value.is_some() { + need_args -= 1; + } + // Grab any required value(s) + for _ in 0..need_args { + up_args.extend(raw_args.next_os(&mut cursor).map(ToOwned::to_owned)); + } + } else { + trigger_args.push(parsed.to_value_os().into()); + } + // -short flags; note that clap allows "stacked" flags, i.e. `-abc` == `-a -b -c` + } else if let Some(shorts) = parsed.to_short() { + for short_res in shorts { + match short_res { + Ok(short) if short_flags.contains_key(&short) => { + up_args.push(format!("-{short}").into()); + } + Ok(short) => { + trigger_args.push(format!("-{short}").into()); + } + Err(invalid) => { + trigger_args.push(OsString::from_iter([OsStr::new("-"), invalid])); + } + } + } + } else { + // We asserted that UpCommandInner has no positional args + trigger_args.push(parsed.to_value_os().into()); + } + } + + let mut inner = UpCommandInner::try_parse_from(up_args)?; + inner.trigger_args = trigger_args; + Ok(Self(inner)) + } + + fn update_from_arg_matches( + &mut self, + _: &clap::ArgMatches, + ) -> std::result::Result<(), clap::Error> { + unimplemented!("UpCommand cannot be flattened") + } +} + +impl Parser for UpCommand {} + +impl CommandFactory for UpCommand { + fn command() -> clap::Command { + Self::augment_args(Command::default()) + } + + fn command_for_update() -> clap::Command { + unimplemented!("UpCommand cannot be flattened") + } +} diff --git a/src/lib.rs b/src/lib.rs index 6e5eccd12a..c7bc5ee90a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,4 +10,192 @@ pub mod subprocess; #[allow(clippy::all, dead_code)] mod clap_markdown; +use anyhow::{Context, Error}; +use clap::{ArgAction, CommandFactory, FromArgMatches, Parser, Subcommand}; +use commands::external::predefined_externals; +use commands::maintenance::MaintenanceCommands; +use commands::{ + build::BuildCommand, + cloud::{DeployCommand, LoginCommand}, + doctor::DoctorCommand, + external::execute_external_subcommand, + new::{AddCommand, NewCommand}, + plugins::PluginCommands, + registry::RegistryCommands, + templates::TemplateCommands, + up::UpCommand, + watch::WatchCommand, +}; +use spin_runtime_factors::FactorsBuilder; +use spin_trigger::cli::help::HelpArgsOnlyTrigger; +use spin_trigger::cli::FactorsTriggerCommand; +use spin_trigger_http::HttpTrigger; +use spin_trigger_redis::RedisTrigger; + pub use opts::HELP_ARGS_ONLY_TRIGGER_TYPE; + +pub async fn run() -> anyhow::Result<()> { + let version = build_info(); + spin_telemetry::init(version.clone()).context("Failed to initialize telemetry")?; + + let plugin_help_entries = plugin_help_entries(); + + let mut cmd = SpinApp::command().version(version); + for plugin in &plugin_help_entries { + let subcmd = clap::Command::new(plugin.display_text()) + .about(&plugin.about) + .allow_hyphen_values(true) + .disable_help_flag(true) + .arg( + clap::Arg::new("command") + .allow_hyphen_values(true) + .action(ArgAction::Append), + ); + cmd = cmd.subcommand(subcmd); + } + + if !plugin_help_entries.is_empty() { + cmd = cmd.after_help("* implemented via plugin"); + } + + let matches = cmd.get_matches(); + + if let Some((subcmd, _)) = matches.subcommand() { + if plugin_help_entries.iter().any(|e| e.name == subcmd) { + let args = std::env::args().skip(1).collect(); + return execute_external_subcommand(args).await; + } + } + + SpinApp::from_arg_matches(&matches)? + .run() + .await + .inspect_err(|err| tracing::debug!(?err)) +} + +/// The Spin CLI +#[derive(Parser)] +#[clap( + name = "spin", + styles = spin_common::cli::CLAP_STYLES, + // Sort subcommands + next_display_order = None, +)] +enum SpinApp { + #[clap(subcommand, alias = "template")] + Templates(TemplateCommands), + #[clap(alias = "n")] + New(NewCommand), + #[clap(alias = "a")] + Add(AddCommand), + #[clap(alias = "u")] + Up(UpCommand), + // acts as a cross-level subcommand shortcut -> `spin cloud deploy` + #[clap(alias = "d")] + Deploy(DeployCommand), + // acts as a cross-level subcommand shortcut -> `spin cloud login` + Login(LoginCommand), + #[clap(subcommand, alias = "oci")] + Registry(RegistryCommands), + #[clap(alias = "b")] + Build(BuildCommand), + #[clap(subcommand, alias = "plugin")] + Plugins(PluginCommands), + #[clap(subcommand, hide = true)] + Trigger(TriggerCommands), + #[clap(external_subcommand)] + External(Vec), + #[clap(alias = "w")] + Watch(WatchCommand), + Doctor(DoctorCommand), + #[clap(subcommand, hide = true)] + Maintenance(MaintenanceCommands), +} + +#[derive(Subcommand)] +enum TriggerCommands { + Http(FactorsTriggerCommand), + Redis(FactorsTriggerCommand), + #[clap(name = crate::HELP_ARGS_ONLY_TRIGGER_TYPE, hide = true)] + HelpArgsOnly(FactorsTriggerCommand), +} + +impl SpinApp { + /// The main entry point to Spin. + pub async fn run(self) -> Result<(), Error> { + match self { + Self::Templates(cmd) => cmd.run().await, + Self::Up(cmd) => cmd.run().await, + Self::New(cmd) => cmd.run().await, + Self::Add(cmd) => cmd.run().await, + Self::Deploy(cmd) => cmd.run().await, + Self::Login(cmd) => cmd.run().await, + Self::Registry(cmd) => cmd.run().await, + Self::Build(cmd) => cmd.run().await, + Self::Trigger(TriggerCommands::Http(cmd)) => cmd.run().await, + Self::Trigger(TriggerCommands::Redis(cmd)) => cmd.run().await, + Self::Trigger(TriggerCommands::HelpArgsOnly(cmd)) => cmd.run().await, + Self::Plugins(cmd) => cmd.run().await, + Self::External(args) => execute_external_subcommand(args).await, + Self::Watch(cmd) => cmd.run().await, + Self::Doctor(cmd) => cmd.run().await, + Self::Maintenance(cmd) => cmd.run().await, + } + } +} + +/// Returns build information, similar to: 0.1.0 (2be4034 2022-03-31). +fn build_info() -> String { + use build_info::*; + format!("{SPIN_VERSION} ({SPIN_COMMIT_SHA} {SPIN_COMMIT_DATE})") +} + +struct PluginHelpEntry { + name: String, + about: String, +} + +impl PluginHelpEntry { + fn from_plugin(plugin: &spin_plugins::manifest::PluginManifest) -> Option { + if hide_plugin_in_help(plugin) { + None + } else { + Some(Self { + name: plugin.name(), + about: plugin.description().unwrap_or_default().to_owned(), + }) + } + } + + fn display_text(&self) -> String { + format!("{}*", self.name) + } +} + +fn plugin_help_entries() -> Vec { + let mut entries = installed_plugin_help_entries(); + for (name, about) in predefined_externals() { + if !entries.iter().any(|e| e.name == name) { + entries.push(PluginHelpEntry { name, about }); + } + } + entries +} + +fn installed_plugin_help_entries() -> Vec { + let Ok(manager) = spin_plugins::manager::PluginManager::try_default() else { + return vec![]; + }; + let Ok(manifests) = manager.store().installed_manifests() else { + return vec![]; + }; + + manifests + .iter() + .filter_map(PluginHelpEntry::from_plugin) + .collect() +} + +fn hide_plugin_in_help(plugin: &spin_plugins::manifest::PluginManifest) -> bool { + plugin.name().starts_with("trigger-") +}