diff --git a/Cargo.toml b/Cargo.toml index 08e71555..2f78bf12 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -58,6 +58,7 @@ testresult = "0.3.0" [features] default = [] postgres = ["warg-server/postgres"] +not-cli = ["warg-client/not-cli"] [workspace] members = ["crates/server"] diff --git a/crates/client/Cargo.toml b/crates/client/Cargo.toml index 86d7e10d..9ebb2a30 100644 --- a/crates/client/Cargo.toml +++ b/crates/client/Cargo.toml @@ -10,8 +10,9 @@ homepage = { workspace = true } repository = { workspace = true} [features] -default = [] +default = ["dialoguer"] native-tls-vendored = ["reqwest/native-tls-vendored"] +not-cli = [] [dependencies] warg-crypto = { workspace = true } @@ -24,7 +25,7 @@ clap = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } tokio = { workspace = true } -dialoguer = { workspace = true } +dialoguer = { workspace = true, optional = true } tokio-util = { workspace = true } tempfile = { workspace = true } reqwest = { workspace = true } diff --git a/crates/client/src/config.rs b/crates/client/src/config.rs index 890df10d..b29d2838 100644 --- a/crates/client/src/config.rs +++ b/crates/client/src/config.rs @@ -122,6 +122,10 @@ pub struct Config { /// Auto accept registry hint or ask the user to confirm #[serde(default)] pub auto_accept_federation_hints: bool, + + /// Disable dialoguer prompts. + #[serde(default)] + pub disable_dialoguer: bool, } impl Config { @@ -202,6 +206,7 @@ impl Config { keyring_auth: self.keyring_auth, ignore_federation_hints: self.ignore_federation_hints, auto_accept_federation_hints: self.auto_accept_federation_hints, + disable_dialoguer: self.disable_dialoguer, }; serde_json::to_writer_pretty( diff --git a/crates/client/src/lib.rs b/crates/client/src/lib.rs index 372990b2..4f00dcb2 100644 --- a/crates/client/src/lib.rs +++ b/crates/client/src/lib.rs @@ -1,10 +1,16 @@ //! A client library for Warg component registries. #![deny(missing_docs)] -use crate::storage::{PackageInfo, PublishEntry}; +use crate::storage::PackageInfo; + +#[cfg(not(feature = "not-cli"))] +use crate::storage::PublishEntry; + use anyhow::{anyhow, Context, Result}; -use dialoguer::theme::ColorfulTheme; -use dialoguer::Confirm; + +#[cfg(not(feature = "not-cli"))] +use dialoguer::{theme::ColorfulTheme, Confirm}; + use indexmap::IndexMap; use reqwest::{Body, IntoUrl}; use secrecy::Secret; @@ -63,21 +69,27 @@ where content: C, namespace_map: N, api: api::Client, + #[cfg(not(feature = "not-cli"))] ignore_federation_hints: bool, + #[cfg(not(feature = "not-cli"))] auto_accept_federation_hints: bool, + #[cfg(not(feature = "not-cli"))] + disable_dialoguer: bool, } impl Client { /// Creates a new client for the given URL, registry storage, and /// content storage. + #[allow(clippy::too_many_arguments)] pub fn new( url: impl IntoUrl, registry: R, content: C, namespace_map: N, auth_token: Option>, - ignore_federation_hints: bool, - auto_accept_federation_hints: bool, + #[cfg(not(feature = "not-cli"))] ignore_federation_hints: bool, + #[cfg(not(feature = "not-cli"))] auto_accept_federation_hints: bool, + #[cfg(not(feature = "not-cli"))] disable_dialoguer: bool, ) -> ClientResult { let api = api::Client::new(url, auth_token)?; Ok(Self { @@ -85,8 +97,12 @@ impl Client Client = None; let (package, record) = loop { let mut info = publish_info.clone(); + + #[cfg(not(feature = "not-cli"))] let mut initializing = info.initializing(); + #[cfg(feature = "not-cli")] + let initializing = info.initializing(); + let package = match self.fetch_package(&info.name).await { Ok(package) => { if initializing { @@ -356,37 +379,42 @@ impl Client { - if !initializing { - let prompt = if has_auth_token { - format!( - "Package `{package_name}` was not found. + if !initializing && cfg!(feature = "not-cli") { + return Err(ClientError::MustInitializePackage { + name, + has_auth_token, + }); + } else if !initializing { + #[cfg(not(feature = "not-cli"))] + { + if self.disable_dialoguer { + return Err(ClientError::MustInitializePackage { + name, + has_auth_token, + }); + } + + if accepted_prompt_to_initialize + || Confirm::with_theme(&ColorfulTheme::default()) + .with_prompt(format!( + "Package `{package_name}` was not found. If it exists, you may not have access. Attempt to create `{package_name}` and publish the release y/N\n", - package_name = &info.name, - ) - } else { - format!( - "Package `{package_name}` was not found. -If it exists, you may not have access without logging in. Try: `warg login` -Attempt to create `{package_name}` and publish the release y/N\n", - package_name = &info.name, - ) - }; - if accepted_prompt_to_initialize - || Confirm::with_theme(&ColorfulTheme::default()) - .with_prompt(prompt) - .default(false) - .interact() - .unwrap() - { - info.entries.insert(0, PublishEntry::Init); - initializing = true; - accepted_prompt_to_initialize = true; - } else { - return Err(ClientError::MustInitializePackage { - name, - has_auth_token, - }); + package_name = &info.name, + )) + .default(false) + .interact() + .unwrap() + { + info.entries.insert(0, PublishEntry::Init); + initializing = true; + accepted_prompt_to_initialize = true; + } else { + return Err(ClientError::MustInitializePackage { + name, + has_auth_token, + }); + } } } PackageInfo::new(info.name.clone()) @@ -700,7 +728,13 @@ Attempt to create `{package_name}` and publish the release y/N\n", } // federated packages in other registries - let mut federated_packages: IndexMap, Vec<&mut PackageInfo>> = + #[cfg(not(feature = "not-cli"))] + let mut federated_packages: IndexMap< + Option, + Vec<&mut PackageInfo>, + > = IndexMap::with_capacity(packages.len()); + #[cfg(feature = "not-cli")] + let federated_packages: IndexMap, Vec<&mut PackageInfo>> = IndexMap::with_capacity(packages.len()); // loop and fetch logs @@ -745,10 +779,44 @@ Attempt to create `{package_name}` and publish the release y/N\n", Err(ClientError::Api(err)) } } + + #[cfg(feature = "not-cli")] + api::ClientError::LogNotFoundWithHint(log_id, hint) => { + let name = packages.get(log_id).unwrap().name.clone(); + + match hint.to_str().ok().map(|s| s.split_once('=')) { + Some(Some((namespace, registry))) if packages.contains_key(log_id) => { + Err(ClientError::PackageDoesNotExistWithHintHeader { + name, + has_auth_token, + hint_namespace: namespace.to_string(), + hint_registry: registry.to_string(), + }) + } + _ => Err(ClientError::PackageDoesNotExist { + name, + has_auth_token, + }), + } + } + + #[cfg(not(feature = "not-cli"))] api::ClientError::LogNotFoundWithHint(log_id, hint) => { match hint.to_str().ok().map(|s| s.split_once('=')) { Some(Some((namespace, registry))) - if !self.ignore_federation_hints + if self.disable_dialoguer && packages.contains_key(log_id) => + { + let name = packages.get(log_id).unwrap().name.clone(); + Err(ClientError::PackageDoesNotExistWithHintHeader { + name, + has_auth_token, + hint_namespace: namespace.to_string(), + hint_registry: registry.to_string(), + }) + } + Some(Some((namespace, registry))) + if !self.disable_dialoguer + && !self.ignore_federation_hints && packages.contains_key(log_id) => { let package_name = &packages.get(log_id).unwrap().name; @@ -1191,8 +1259,12 @@ impl FileSystemClient { content, namespace_map, auth_token, + #[cfg(not(feature = "not-cli"))] config.ignore_federation_hints, + #[cfg(not(feature = "not-cli"))] config.auto_accept_federation_hints, + #[cfg(not(feature = "not-cli"))] + config.disable_dialoguer, )?)) } @@ -1219,8 +1291,12 @@ impl FileSystemClient { FileSystemContentStorage::lock(content_dir)?, FileSystemNamespaceMapStorage::new(namespace_map_path), auth_token, + #[cfg(not(feature = "not-cli"))] config.ignore_federation_hints, + #[cfg(not(feature = "not-cli"))] config.auto_accept_federation_hints, + #[cfg(not(feature = "not-cli"))] + config.disable_dialoguer, ) } } @@ -1315,6 +1391,19 @@ pub enum ClientError { has_auth_token: bool, }, + /// The package does not exist with hint header. + #[error("package `{name}` does not exist but the registry suggests checking registry `{hint_registry}` for packages in namespace `{hint_namespace}`")] + PackageDoesNotExistWithHintHeader { + /// The missing package. + name: PackageName, + /// Client has authentication credentials. + has_auth_token: bool, + /// The hint namespace. + hint_namespace: String, + /// The hint registry. + hint_registry: String, + }, + /// The package version does not exist. #[error("version `{version}` of package `{name}` does not exist")] PackageVersionDoesNotExist { diff --git a/src/bin/warg.rs b/src/bin/warg.rs index 53c7bccd..7c79e79f 100644 --- a/src/bin/warg.rs +++ b/src/bin/warg.rs @@ -92,6 +92,19 @@ pub async fn describe_client_error(e: &ClientError) -> Result<()> { eprintln!("You may be required to login. Try: `warg login`"); } } + ClientError::PackageDoesNotExistWithHintHeader { + name, + has_auth_token, + hint_namespace, + hint_registry, + } => { + eprintln!( + "Package `{name}` was not found or you do not have access. +The registry suggests using registry `{hint_registry}` for packages in namespace `{hint_namespace}`."); + if !has_auth_token { + eprintln!("You may be required to login. Try: `warg login`"); + } + } ClientError::PackageVersionDoesNotExist { name, version } => { eprintln!("Package `{name}` version `{version}` was not found.") } diff --git a/src/commands/config.rs b/src/commands/config.rs index 3ed8a26c..9fe87cb0 100644 --- a/src/commands/config.rs +++ b/src/commands/config.rs @@ -80,6 +80,7 @@ impl ConfigCommand { keyring_auth: false, ignore_federation_hints: self.ignore_federation_hints, auto_accept_federation_hints: self.auto_accept_federation_hints, + disable_dialoguer: false, }; config.write_to_file(&path)?; diff --git a/tests/support/mod.rs b/tests/support/mod.rs index 97d476a2..00ad270a 100644 --- a/tests/support/mod.rs +++ b/tests/support/mod.rs @@ -169,6 +169,7 @@ pub async fn spawn_server( keyring_auth: false, ignore_federation_hints: false, auto_accept_federation_hints: false, + disable_dialoguer: true, }; Ok((instance, config))