diff --git a/crates/goose-cli/src/commands/configure.rs b/crates/goose-cli/src/commands/configure.rs index 365e2e3f23d0..b6b5b03f200d 100644 --- a/crates/goose-cli/src/commands/configure.rs +++ b/crates/goose-cli/src/commands/configure.rs @@ -33,219 +33,225 @@ pub async fn handle_configure() -> anyhow::Result<()> { let config = Config::global(); if !config.exists() { - // First time setup flow - println!(); - println!( - "{}", - style("Welcome to goose! Let's get you set up with a provider.").dim() - ); - println!( - "{}", - style(" you can rerun this command later to update your configuration").dim() - ); - println!(); - cliclack::intro(style(" goose-configure ").on_cyan().black())?; - - // Check if user wants to use OpenRouter login or manual configuration - let setup_method = cliclack::select("How would you like to set up your provider?") - .item( - "openrouter", - "OpenRouter Login (Recommended)", - "Sign in with OpenRouter to automatically configure models", - ) - .item( - "tetrate", - "Tetrate Agent Router Service Login", - "Sign in with Tetrate Agent Router Service to automatically configure models", - ) - .item( - "manual", - "Manual Configuration", - "Choose a provider and enter credentials manually", - ) - .interact()?; + handle_first_time_setup(config).await + } else { + handle_existing_config().await + } +} - match setup_method { - "openrouter" => { - match handle_openrouter_auth().await { - Ok(_) => { - // OpenRouter auth already handles everything including enabling developer extension - } - Err(e) => { - let _ = config.clear(); - println!( - "\n {} OpenRouter authentication failed: {} \n Please try again or use manual configuration", - style("Error").red().italic(), - e, - ); - } - } - } - "tetrate" => { - match handle_tetrate_auth().await { - Ok(_) => { - // Tetrate auth already handles everything including enabling developer extension - } - Err(e) => { - let _ = config.clear(); - println!( - "\n {} Tetrate Agent Router Service authentication failed: {} \n Please try again or use manual configuration", - style("Error").red().italic(), - e, - ); - } - } +async fn handle_first_time_setup(config: &Config) -> anyhow::Result<()> { + println!(); + println!( + "{}", + style("Welcome to goose! Let's get you set up with a provider.").dim() + ); + println!( + "{}", + style(" you can rerun this command later to update your configuration").dim() + ); + println!(); + cliclack::intro(style(" goose-configure ").on_cyan().black())?; + + let setup_method = cliclack::select("How would you like to set up your provider?") + .item( + "openrouter", + "OpenRouter Login (Recommended)", + "Sign in with OpenRouter to automatically configure models", + ) + .item( + "tetrate", + "Tetrate Agent Router Service Login", + "Sign in with Tetrate Agent Router Service to automatically configure models", + ) + .item( + "manual", + "Manual Configuration", + "Choose a provider and enter credentials manually", + ) + .interact()?; + + match setup_method { + "openrouter" => { + if let Err(e) = handle_openrouter_auth().await { + let _ = config.clear(); + println!( + "\n {} OpenRouter authentication failed: {} \n Please try again or use manual configuration", + style("Error").red().italic(), + e, + ); } - "manual" => { - match configure_provider_dialog().await { - Ok(true) => { - println!( - "\n {}: Run '{}' again to adjust your config or add extensions", - style("Tip").green().italic(), - style("goose configure").cyan() - ); - // Since we are setting up for the first time, we'll also enable the developer system - // This operation is best-effort and errors are ignored - set_extension(ExtensionEntry { - enabled: true, - config: ExtensionConfig::default(), - }); - } - Ok(false) => { - let _ = config.clear(); - println!( - "\n {}: We did not save your config, inspect your credentials\n and run '{}' again to ensure goose can connect", - style("Warning").yellow().italic(), - style("goose configure").cyan() - ); - } - Err(e) => { - let _ = config.clear(); - - match e.downcast_ref::() { - Some(ConfigError::NotFound(key)) => { - println!( - "\n {} Required configuration key '{}' not found \n Please provide this value and run '{}' again", - style("Error").red().italic(), - key, - style("goose configure").cyan() - ); - } - Some(ConfigError::KeyringError(msg)) => { - #[cfg(target_os = "macos")] - println!( - "\n {} Failed to access secure storage (keyring): {} \n Please check your system keychain and run '{}' again. \n If your system is unable to use the keyring, please try setting secret key(s) via environment variables.", - style("Error").red().italic(), - msg, - style("goose configure").cyan() - ); - - #[cfg(target_os = "windows")] - println!( - "\n {} Failed to access Windows Credential Manager: {} \n Please check Windows Credential Manager and run '{}' again. \n If your system is unable to use the Credential Manager, please try setting secret key(s) via environment variables.", - style("Error").red().italic(), - msg, - style("goose configure").cyan() - ); - - #[cfg(not(any(target_os = "macos", target_os = "windows")))] - println!( - "\n {} Failed to access secure storage: {} \n Please check your system's secure storage and run '{}' again. \n If your system is unable to use secure storage, please try setting secret key(s) via environment variables.", - style("Error").red().italic(), - msg, - style("goose configure").cyan() - ); - } - Some(ConfigError::DeserializeError(msg)) => { - println!( - "\n {} Invalid configuration value: {} \n Please check your input and run '{}' again", - style("Error").red().italic(), - msg, - style("goose configure").cyan() - ); - } - Some(ConfigError::FileError(e)) => { - println!( - "\n {} Failed to access config file: {} \n Please check file permissions and run '{}' again", - style("Error").red().italic(), - e, - style("goose configure").cyan() - ); - } - Some(ConfigError::DirectoryError(msg)) => { - println!( - "\n {} Failed to access config directory: {} \n Please check directory permissions and run '{}' again", - style("Error").red().italic(), - msg, - style("goose configure").cyan() - ); - } - // handle all other nonspecific errors - _ => { - println!( - "\n {} {} \n We did not save your config, inspect your credentials\n and run '{}' again to ensure goose can connect", - style("Error").red().italic(), - e, - style("goose configure").cyan() - ); - } - } - } - } + } + "tetrate" => { + if let Err(e) = handle_tetrate_auth().await { + let _ = config.clear(); + println!( + "\n {} Tetrate Agent Router Service authentication failed: {} \n Please try again or use manual configuration", + style("Error").red().italic(), + e, + ); } - _ => unreachable!(), } - Ok(()) - } else { - let config_dir = Paths::config_dir().display().to_string(); + "manual" => handle_manual_provider_setup(config).await, + _ => unreachable!(), + } + Ok(()) +} - println!(); - println!( - "{}", - style("This will update your existing config files").dim() - ); - println!( - "{} {}", - style(" if you prefer, you can edit them directly at").dim(), - config_dir - ); - println!(); - - cliclack::intro(style(" goose-configure ").on_cyan().black())?; - let action = cliclack::select("What would you like to configure?") - .item( - "providers", - "Configure Providers", - "Change provider or update credentials", - ) - .item( - "custom_providers", - "Custom Providers", - "Add custom provider with compatible API", - ) - .item("add", "Add Extension", "Connect to a new extension") - .item( - "toggle", - "Toggle Extensions", - "Enable or disable connected extensions", - ) - .item("remove", "Remove Extension", "Remove an extension") - .item( - "settings", - "goose settings", - "Set the goose mode, Tool Output, Tool Permissions, Experiment, goose recipe github repo and more", - ) - .interact()?; +async fn handle_manual_provider_setup(config: &Config) { + match configure_provider_dialog().await { + Ok(true) => { + println!( + "\n {}: Run '{}' again to adjust your config or add extensions", + style("Tip").green().italic(), + style("goose configure").cyan() + ); + set_extension(ExtensionEntry { + enabled: true, + config: ExtensionConfig::default(), + }); + } + Ok(false) => { + let _ = config.clear(); + println!( + "\n {}: We did not save your config, inspect your credentials\n and run '{}' again to ensure goose can connect", + style("Warning").yellow().italic(), + style("goose configure").cyan() + ); + } + Err(e) => { + let _ = config.clear(); + print_manual_config_error(&e); + } + } +} - match action { - "toggle" => toggle_extensions_dialog(), - "add" => configure_extensions_dialog(), - "remove" => remove_extension_dialog(), - "settings" => configure_settings_dialog().await, - "providers" => configure_provider_dialog().await.map(|_| ()), - "custom_providers" => configure_custom_provider_dialog(), - _ => unreachable!(), +fn print_manual_config_error(e: &anyhow::Error) { + match e.downcast_ref::() { + Some(ConfigError::NotFound(key)) => { + println!( + "\n {} Required configuration key '{}' not found \n Please provide this value and run '{}' again", + style("Error").red().italic(), + key, + style("goose configure").cyan() + ); + } + Some(ConfigError::KeyringError(msg)) => { + print_keyring_error(msg); + } + Some(ConfigError::DeserializeError(msg)) => { + println!( + "\n {} Invalid configuration value: {} \n Please check your input and run '{}' again", + style("Error").red().italic(), + msg, + style("goose configure").cyan() + ); + } + Some(ConfigError::FileError(err)) => { + println!( + "\n {} Failed to access config file: {} \n Please check file permissions and run '{}' again", + style("Error").red().italic(), + err, + style("goose configure").cyan() + ); + } + Some(ConfigError::DirectoryError(msg)) => { + println!( + "\n {} Failed to access config directory: {} \n Please check directory permissions and run '{}' again", + style("Error").red().italic(), + msg, + style("goose configure").cyan() + ); } + _ => { + println!( + "\n {} {} \n We did not save your config, inspect your credentials\n and run '{}' again to ensure goose can connect", + style("Error").red().italic(), + e, + style("goose configure").cyan() + ); + } + } +} + +#[cfg(target_os = "macos")] +fn print_keyring_error(msg: &str) { + println!( + "\n {} Failed to access secure storage (keyring): {} \n Please check your system keychain and run '{}' again. \n If your system is unable to use the keyring, please try setting secret key(s) via environment variables.", + style("Error").red().italic(), + msg, + style("goose configure").cyan() + ); +} + +#[cfg(target_os = "windows")] +fn print_keyring_error(msg: &str) { + println!( + "\n {} Failed to access Windows Credential Manager: {} \n Please check Windows Credential Manager and run '{}' again. \n If your system is unable to use the Credential Manager, please try setting secret key(s) via environment variables.", + style("Error").red().italic(), + msg, + style("goose configure").cyan() + ); +} + +#[cfg(not(any(target_os = "macos", target_os = "windows")))] +fn print_keyring_error(msg: &str) { + println!( + "\n {} Failed to access secure storage: {} \n Please check your system's secure storage and run '{}' again. \n If your system is unable to use secure storage, please try setting secret key(s) via environment variables.", + style("Error").red().italic(), + msg, + style("goose configure").cyan() + ); +} + +async fn handle_existing_config() -> anyhow::Result<()> { + let config_dir = Paths::config_dir().display().to_string(); + + println!(); + println!( + "{}", + style("This will update your existing config files").dim() + ); + println!( + "{} {}", + style(" if you prefer, you can edit them directly at").dim(), + config_dir + ); + println!(); + + cliclack::intro(style(" goose-configure ").on_cyan().black())?; + let action = cliclack::select("What would you like to configure?") + .item( + "providers", + "Configure Providers", + "Change provider or update credentials", + ) + .item( + "custom_providers", + "Custom Providers", + "Add custom provider with compatible API", + ) + .item("add", "Add Extension", "Connect to a new extension") + .item( + "toggle", + "Toggle Extensions", + "Enable or disable connected extensions", + ) + .item("remove", "Remove Extension", "Remove an extension") + .item( + "settings", + "goose settings", + "Set the goose mode, Tool Output, Tool Permissions, Experiment, goose recipe github repo and more", + ) + .interact()?; + + match action { + "toggle" => toggle_extensions_dialog(), + "add" => configure_extensions_dialog(), + "remove" => remove_extension_dialog(), + "settings" => configure_settings_dialog().await, + "providers" => configure_provider_dialog().await.map(|_| ()), + "custom_providers" => configure_custom_provider_dialog(), + _ => unreachable!(), } }