From 2c2c264fe87376ed70f5571496bd39010af1ed78 Mon Sep 17 00:00:00 2001 From: dal Date: Mon, 12 May 2025 18:06:45 -0600 Subject: [PATCH] changes to buster start --- cli/cli/src/commands/config_utils.rs | 347 ++++++++++++++------------- cli/cli/src/commands/run.rs | 262 +++++++++++--------- 2 files changed, 333 insertions(+), 276 deletions(-) diff --git a/cli/cli/src/commands/config_utils.rs b/cli/cli/src/commands/config_utils.rs index 1581b9a26..9150ab67c 100644 --- a/cli/cli/src/commands/config_utils.rs +++ b/cli/cli/src/commands/config_utils.rs @@ -5,6 +5,8 @@ use serde_yaml; use std::fs; use std::io::{self, Write}; use std::path::{Path, PathBuf}; +use colored::*; +use inquire::{Confirm, Password, Select, Text, PasswordDisplayMode, validator::Validation}; // Moved from run.rs pub fn prompt_for_input( @@ -493,60 +495,77 @@ pub fn prompt_and_manage_openai_api_key( app_base_dir: &Path, force_prompt: bool, ) -> Result { - let cache_file = ".openai_api_key"; - let mut current_key = get_cached_value(app_base_dir, cache_file)?; + // --- Add BUSTER ASCII Art Header --- + // Always print the main header + println!("\n{}", r" +██████╗░██╗░░░██╗███████╗████████╗███████╗██████╗░ +██╔══██╗██║░░░██║██╔════╝╚══██╔══╝██╔════╝██╔══██╗ +██████╦╝██║░░░██║███████╗░░░██║░░░█████╗░░██████╔╝ +██╔══██╗██║░░░██║╚════██║░░░██║░░░██╔══╝░░██╔══██╗ +██████╦╝╚██████╔╝███████║░░░██║░░░███████╗██║░░██║ +╚═════╝░░╚═════╝░╚══════╝░░░╚═╝░░░╚══════╝╚═╝░░╚═╝ + ".cyan().bold()); - if force_prompt || current_key.is_none() { - if current_key.is_some() { - let key_display = current_key.as_ref().map_or("", |k| { - if k.len() > 4 { - &k[k.len() - 4..] - } else { - "****" - } - }); - let update_choice = prompt_for_input( - &format!("Current OpenAI API key ends with ...{}. Update? (y/n)", key_display), - Some("n"), - false, - )? - .to_lowercase(); - if update_choice != "y" { - return Ok(current_key.unwrap()); - } + let cache_file = ".openai_api_key"; + let current_key_opt = get_cached_value(app_base_dir, cache_file)?; + let default_api_base = "https://api.openai.com/v1"; + + // Decide if prompting is necessary: Force flag OR key is missing + let needs_prompt = force_prompt || current_key_opt.is_none(); + + if needs_prompt { + // Only print sub-header when actually prompting + println!("{}", "--- OpenAI API Key ---".green()); + + // If forcing prompt and key exists, mention it + if force_prompt && current_key_opt.is_some() { + let key_display = current_key_opt.as_ref().map_or("****", |k| { + if k.len() > 4 { &k[k.len() - 4..] } else { "****" } + }); + println!("{} Current key ends with ...{}. You chose to force update.", "ℹ️".yellow(), key_display); } - let new_key = prompt_for_input("Enter your OpenAI API Key:", None, true)?; - let api_base_choice = prompt_for_input( - "Use custom API base URL? (y/n):", - Some("n"), - false, - )? - .to_lowercase(); - let api_base = if api_base_choice == "y" { - Some( - prompt_for_input( - "Enter the API base URL:", - Some("https://api.openai.com/v1"), - false, - )?, - ) - } else { - Some("https://api.openai.com/v1".to_string()) - }; + // Use inquire::Password for masked input + let new_key = inquire::Password::new("Enter your OpenAI API Key:") + .with_display_mode(inquire::PasswordDisplayMode::Masked) + .with_validator(|input: &str| { + if input.trim().is_empty() { + Ok(inquire::validator::Validation::Invalid("API Key cannot be empty".into())) + } else { + Ok(inquire::validator::Validation::Valid) + } + }) + .without_confirmation() // Don\'t ask to confirm password + .prompt() + .map_err(|e| BusterError::CommandError(format!("Failed to prompt for API key: {}", e)))?; // Update LiteLLM config first (borrows new_key) - update_litellm_yaml(app_base_dir, &new_key, api_base.as_deref())?; + match update_litellm_yaml(app_base_dir, &new_key, Some(default_api_base)) { + Ok(config_path) => { + println!("{} {}", "✅".green(), format!("LiteLLM configuration updated successfully at {}", config_path.display()).green()); + } + Err(e) => { + eprintln!("{}", format!("⚠️ Failed to update LiteLLM config: {}. Proceeding to cache key.", e).yellow()); + } + } - // Cache the key after successful update + // Cache the new key cache_value(app_base_dir, cache_file, &new_key)?; - current_key = Some(new_key); - println!("LiteLLM configuration file updated successfully."); - } + println!("{} {}", "✅".green(), "OpenAI API Key cached.".green()); + Ok(new_key) - current_key.ok_or_else(|| { - BusterError::CommandError("OpenAI API Key setup failed.".to_string()) - }) + } else { + // Key exists and force_prompt is false, use existing key + let existing_key = current_key_opt.unwrap(); + // Still ensure LiteLLM config reflects the existing key + if let Err(e) = update_litellm_yaml(app_base_dir, &existing_key, Some(default_api_base)) { + println!("{}", format!("⚠️ Warning: Failed to verify/update LiteLLM config for existing key: {}", e).yellow()); + } else { + // Optionally print a quieter message confirming usage + // println!("{}", "✅ Using cached OpenAI API key.".dimmed()); + } + Ok(existing_key) + } } pub struct RerankerConfig { @@ -565,140 +584,142 @@ pub fn prompt_and_manage_reranker_settings( let model_cache = ".reranker_model"; let url_cache = ".reranker_base_url"; - let mut current_provider = get_cached_value(app_base_dir, provider_cache)?; - let mut current_key = get_cached_value(app_base_dir, key_cache)?; - let mut current_model = get_cached_value(app_base_dir, model_cache)?; - let mut current_url = get_cached_value(app_base_dir, url_cache)?; + let current_provider = get_cached_value(app_base_dir, provider_cache)?; + let current_key = get_cached_value(app_base_dir, key_cache)?; + let current_model = get_cached_value(app_base_dir, model_cache)?; + let current_url = get_cached_value(app_base_dir, url_cache)?; - let mut needs_update = force_prompt; - if !needs_update - && (current_provider.is_none() - || current_key.is_none() - || current_model.is_none() - || current_url.is_none()) - { - needs_update = true; // If any part is missing, force update flow for initial setup - } + // Check if *all* required settings are cached + let all_settings_cached = current_provider.is_some() + && current_key.is_some() + && current_model.is_some() + && current_url.is_some(); - if needs_update { - if !force_prompt && current_provider.is_some() && current_model.is_some() { - // Already prompted if force_prompt is true - let update_choice = prompt_for_input( - &format!( - "Current Reranker: {} (Model: {}). Update settings? (y/n)", - current_provider.as_ref().unwrap_or(&"N/A".to_string()), - current_model.as_ref().unwrap_or(&"N/A".to_string()) - ), - Some("n"), - false, - )? - .to_lowercase(); - if update_choice != "y" - && current_provider.is_some() - && current_key.is_some() - && current_model.is_some() - && current_url.is_some() - { - return Ok(RerankerConfig { - provider: current_provider.unwrap(), - api_key: current_key.unwrap(), - model: current_model.unwrap(), - base_url: current_url.unwrap(), - }); - } - } else if force_prompt && current_provider.is_some() && current_model.is_some() { - let update_choice = prompt_for_input( - &format!( - "Current Reranker: {} (Model: {}). Update settings? (y/n)", - current_provider.as_ref().unwrap_or(&"N/A".to_string()), - current_model.as_ref().unwrap_or(&"N/A".to_string()) - ), - Some("n"), - false, - )? - .to_lowercase(); - if update_choice != "y" - && current_provider.is_some() - && current_key.is_some() - && current_model.is_some() - && current_url.is_some() - { - return Ok(RerankerConfig { - provider: current_provider.unwrap(), - api_key: current_key.unwrap(), - model: current_model.unwrap(), - base_url: current_url.unwrap(), - }); + // Decide if prompting is necessary: Force flag OR not all settings are cached + let needs_prompt = force_prompt || !all_settings_cached; + + if needs_prompt { + println!("\n{}", "--- Reranker Setup ---".bold().green()); + + if force_prompt && all_settings_cached { + println!("{} Current Reranker: {} (Model: {}). You chose to force update.", + "ℹ️".yellow(), + current_provider.as_ref().unwrap().cyan(), + current_model.as_ref().unwrap().cyan() + ); + } else if !all_settings_cached { + println!("{}", "Some reranker settings are missing. Please configure.".yellow()); + } + + // Define provider options for Select + #[derive(Debug, Clone)] + enum ProviderOption { + Cohere, Mixedbread, Jina, None + } + impl std::fmt::Display for ProviderOption { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ProviderOption::Cohere => write!(f, "Cohere"), + ProviderOption::Mixedbread => write!(f, "Mixedbread"), + ProviderOption::Jina => write!(f, "Jina"), + ProviderOption::None => write!(f, "None (Skip reranker setup)"), + } } } - println!("--- Reranker Setup ---"); - println!("Choose your reranker provider:"); - println!("1: Cohere"); - println!("2: Mixedbread"); - println!("3: Jina"); - let provider_choice = loop { - match prompt_for_input("Enter choice (1-3):", Some("1"), false)?.parse::() { - Ok(choice @ 1..=3) => break choice, - _ => println!("Invalid choice. Please enter a number between 1 and 3."), - } + let options = vec![ + ProviderOption::Cohere, + ProviderOption::Mixedbread, + ProviderOption::Jina, + ProviderOption::None, + ]; + + // Use inquire::Select + let selected_provider_opt = Select::new("Choose your reranker provider:", options) + // Start selection intelligently based on current state + .with_starting_cursor(if all_settings_cached { 3 } else { 0 }) // Start at None if configured, else Cohere + .prompt() + .map_err(|e| BusterError::CommandError(format!("Failed to select provider: {}", e)))?; + + if matches!(selected_provider_opt, ProviderOption::None) { + println!("{}", "ℹ️ Skipping reranker setup.".yellow()); + // Clear existing cached values if skipping + let _ = fs::remove_file(app_base_dir.join(provider_cache)); + let _ = fs::remove_file(app_base_dir.join(key_cache)); + let _ = fs::remove_file(app_base_dir.join(model_cache)); + let _ = fs::remove_file(app_base_dir.join(url_cache)); + // Return an error specifically indicating skip, handled in run.rs + return Err(BusterError::CommandError("Reranker setup skipped by user.".to_string())); + } + + let (new_provider, default_model, default_url) = match selected_provider_opt { + ProviderOption::Cohere => ( + "Cohere", + "rerank-v3.5", + "https://api.cohere.com/v2/rerank", + ), + ProviderOption::Mixedbread => ( + "Mixedbread", + "mixedbread-ai/mxbai-rerank-xsmall-v1", + "https://api.mixedbread.ai/v1/reranking", + ), + ProviderOption::Jina => ( + "Jina", + "jina-reranker-v1-base-en", + "https://api.jina.ai/v1/rerank", + ), + ProviderOption::None => unreachable!(), // Handled above }; - let (new_provider, default_model, default_url) = match provider_choice { - 1 => ( - "Cohere", - "rerank-english-v3.0", - "https://api.cohere.com/v1/rerank", - ), // user asked for v3.5 but official docs say v3.0 for rerank model - 2 => ( - "Mixedbread", - "mixedbread-ai/mxbai-rerank-xsmall-v1", - "https://api.mixedbread.ai/v1/reranking", - ), - 3 => ( - "Jina", - "jina-reranker-v1-base-en", - "https://api.jina.ai/v1/rerank", - ), - _ => unreachable!(), - }; + // Use inquire::Password for the API key + let new_key_val = Password::new(&format!("Enter your {} API Key:", new_provider)) + .with_display_mode(PasswordDisplayMode::Masked) + .with_validator(|input: &str| { + if input.trim().is_empty() { Ok(Validation::Invalid("API Key cannot be empty".into())) } + else { Ok(Validation::Valid) } + }) + .without_confirmation() + .prompt() + .map_err(|e| BusterError::CommandError(format!("Failed to prompt for API key: {}", e)))?; - let new_key_val = - prompt_for_input(&format!("Enter your {} API Key:", new_provider), None, true)?; - let new_model_val = prompt_for_input( - &format!("Enter {} model name:", new_provider), - Some(default_model), - false, - )?; - let new_url_val = prompt_for_input( - &format!("Enter {} rerank base URL:", new_provider), - Some(default_url), - false, - )?; + // Use inquire::Text for model and URL, with defaults + let new_model_val = Text::new(&format!("Enter {} model name:", new_provider)) + .with_default(default_model) + .with_help_message("Press Enter to use the default.") + .prompt() + .map_err(|e| BusterError::CommandError(format!("Failed to prompt for model name: {}", e)))?; + + let new_url_val = Text::new(&format!("Enter {} rerank base URL:", new_provider)) + .with_default(default_url) + .with_help_message("Press Enter to use the default.") + .prompt() + .map_err(|e| BusterError::CommandError(format!("Failed to prompt for base URL: {}", e)))?; cache_value(app_base_dir, provider_cache, new_provider)?; cache_value(app_base_dir, key_cache, &new_key_val)?; cache_value(app_base_dir, model_cache, &new_model_val)?; cache_value(app_base_dir, url_cache, &new_url_val)?; - current_provider = Some(new_provider.to_string()); - current_key = Some(new_key_val); - current_model = Some(new_model_val); - current_url = Some(new_url_val); - } + println!("{} Reranker settings updated successfully for {}. +", "✅".green(), new_provider.cyan()); - if let (Some(prov), Some(key), Some(model), Some(url)) = - (current_provider, current_key, current_model, current_url) - { + // Construct the result from the newly prompted values Ok(RerankerConfig { - provider: prov, - api_key: key, - model, - base_url: url, + provider: new_provider.to_string(), + api_key: new_key_val, + model: new_model_val, + base_url: new_url_val, }) + } else { - Err(BusterError::CommandError( - "Reranker configuration setup failed. Some values are missing.".to_string(), - )) + // All settings cached and force_prompt is false, use existing + // Optionally print a quieter message + // println!("{}", "✅ Using cached Reranker settings.".dimmed()); + Ok(RerankerConfig { + provider: current_provider.unwrap(), + api_key: current_key.unwrap(), + model: current_model.unwrap(), + base_url: current_url.unwrap(), + }) } } diff --git a/cli/cli/src/commands/run.rs b/cli/cli/src/commands/run.rs index 284148c93..bc72be323 100644 --- a/cli/cli/src/commands/run.rs +++ b/cli/cli/src/commands/run.rs @@ -5,9 +5,10 @@ use indicatif::{ProgressBar, ProgressStyle}; use rust_embed::RustEmbed; use std::fs; use std::io::{self, Write}; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::process::Command; use std::time::Duration; +use colored::*; #[derive(RustEmbed)] #[folder = "../../"] @@ -27,134 +28,163 @@ async fn setup_persistent_app_environment() -> Result { BusterError::CommandError(format!("Failed to get app base directory: {}", e)) })?; - fs::create_dir_all(&app_base_dir).map_err(|e| { - BusterError::CommandError(format!( - "Failed to create persistent app directory at {}: {}", - app_base_dir.display(), - e - )) - })?; + // --- Check if core config files exist --- + let main_dot_env_path = app_base_dir.join(".env"); + let docker_compose_path = app_base_dir.join("docker-compose.yml"); + let supabase_dot_env_path = app_base_dir.join("supabase/.env"); + let litellm_config_path = app_base_dir.join("litellm_config/config.yaml"); - for filename_cow in StaticAssets::iter() { - let filename = filename_cow.as_ref(); - let asset = StaticAssets::get(filename).ok_or_else(|| { - BusterError::CommandError(format!("Failed to get embedded asset: {}", filename)) + let initial_setup_needed = !main_dot_env_path.exists() + || !docker_compose_path.exists() + || !supabase_dot_env_path.exists() + || !litellm_config_path.exists(); + // --- End Check --- + + if initial_setup_needed { + println!("Performing initial setup for Buster environment in {}", app_base_dir.display()); + + // --- Begin Initial Setup Block (Asset Extraction, Directory Creation) --- + fs::create_dir_all(&app_base_dir).map_err(|e| { + BusterError::CommandError(format!( + "Failed to create persistent app directory at {}: {}", + app_base_dir.display(), + e + )) })?; - let target_file_path = app_base_dir.join(filename); - if let Some(parent) = target_file_path.parent() { - fs::create_dir_all(parent).map_err(|e| { + for filename_cow in StaticAssets::iter() { + let filename = filename_cow.as_ref(); + let asset = StaticAssets::get(filename).ok_or_else(|| { + BusterError::CommandError(format!("Failed to get embedded asset: {}", filename)) + })?; + let target_file_path = app_base_dir.join(filename); + + if let Some(parent) = target_file_path.parent() { + fs::create_dir_all(parent).map_err(|e| { + BusterError::CommandError(format!( + "Failed to create directory {}: {}", + parent.display(), + e + )) + })?; + } + + fs::write(&target_file_path, asset.data).map_err(|e| { BusterError::CommandError(format!( - "Failed to create directory {}: {}", - parent.display(), + "Failed to write embedded file {} to {}: {}", + filename, + target_file_path.display(), e )) })?; } - fs::write(&target_file_path, asset.data).map_err(|e| { + let supabase_volumes_functions_path = app_base_dir.join("supabase/volumes/functions"); + fs::create_dir_all(supabase_volumes_functions_path).map_err(|e| { BusterError::CommandError(format!( - "Failed to write embedded file {} to {}: {}", - filename, - target_file_path.display(), + "Failed to create supabase/volumes/functions in persistent app dir: {}", e )) })?; - } - let supabase_volumes_functions_path = app_base_dir.join("supabase/volumes/functions"); - fs::create_dir_all(supabase_volumes_functions_path).map_err(|e| { - BusterError::CommandError(format!( - "Failed to create supabase/volumes/functions in persistent app dir: {}", - e - )) - })?; + // Initialize .env from .env.example (root) + let example_env_src_path = app_base_dir.join(".env.example"); + if example_env_src_path.exists() { + fs::copy(&example_env_src_path, &main_dot_env_path).map_err(|e| { + BusterError::CommandError(format!( + "Failed to initialize root .env ({}) from {}: {}", + main_dot_env_path.display(), + example_env_src_path.display(), + e + )) + })?; + } else { + return Err(BusterError::CommandError(format!( + "Critical setup error: Root {} not found after asset extraction. Cannot initialize main .env file.", + example_env_src_path.display() + ))); + } - // Initialize .env from .env.example (the root one), which should have been extracted by StaticAssets loop - let example_env_src_path = app_base_dir.join(".env.example"); - let main_dot_env_target_path = app_base_dir.join(".env"); // This is the root .env + // Initialize supabase/.env from supabase/.env.example + let supabase_example_env_src_path = app_base_dir.join("supabase/.env.example"); + if supabase_example_env_src_path.exists() { + fs::copy(&supabase_example_env_src_path, &supabase_dot_env_path).map_err(|e| { + BusterError::CommandError(format!( + "Failed to initialize supabase/.env ({}) from {}: {}", + supabase_dot_env_path.display(), + supabase_example_env_src_path.display(), + e + )) + })?; + } else { + return Err(BusterError::CommandError(format!( + "Critical setup error: Supabase {} not found after asset extraction. Cannot initialize supabase/.env file.", + supabase_example_env_src_path.display() + ))); + } + // --- End Initial Setup Block --- - if example_env_src_path.exists() { - fs::copy(&example_env_src_path, &main_dot_env_target_path).map_err(|e| { - BusterError::CommandError(format!( - "Failed to initialize root .env ({}) from {}: {}", - main_dot_env_target_path.display(), - example_env_src_path.display(), - e - )) - })?; } else { - // This case should ideally not be hit if .env.example is correctly embedded and extracted. - return Err(BusterError::CommandError(format!( - "Critical setup error: Root {} not found after asset extraction. Cannot initialize main .env file.", - example_env_src_path.display() - ))); + println!("Core configuration files found. Skipping initial asset extraction."); } - // Initialize supabase/.env from supabase/.env.example - let supabase_example_env_src_path = app_base_dir.join("supabase/.env.example"); - let supabase_dot_env_target_path = app_base_dir.join("supabase/.env"); - - if supabase_example_env_src_path.exists() { - fs::copy(&supabase_example_env_src_path, &supabase_dot_env_target_path).map_err(|e| { - BusterError::CommandError(format!( - "Failed to initialize supabase/.env ({}) from {}: {}", - supabase_dot_env_target_path.display(), - supabase_example_env_src_path.display(), - e - )) - })?; - } else { - return Err(BusterError::CommandError(format!( - "Critical setup error: Supabase {} not found after asset extraction. Cannot initialize supabase/.env file.", - supabase_example_env_src_path.display() - ))); - } - - // --- BEGIN API Key and Reranker Setup using config_utils --- - println!("--- Buster Configuration Setup ---"); - + // --- Configuration Checks/Updates (Always Run) --- + // NOTE: These functions handle their own caching and prompting logic internally. + // They also print their own headers/messages. let llm_api_key = config_utils::prompt_and_manage_openai_api_key(&app_base_dir, false)?; - let reranker_config = config_utils::prompt_and_manage_reranker_settings(&app_base_dir, false)?; + let reranker_config_result = config_utils::prompt_and_manage_reranker_settings(&app_base_dir, false); - // Create/update LiteLLM YAML config - let litellm_config_path = config_utils::update_litellm_yaml( + // Handle potential skip of reranker setup + let (rerank_api_key_opt, rerank_model_opt, rerank_base_url_opt) = match reranker_config_result { + Ok(config) => (Some(config.api_key), Some(config.model), Some(config.base_url)), + Err(BusterError::CommandError(msg)) if msg.contains("skipped by user") => { + println!("Reranker setup was skipped. Ensuring related .env variables are not present."); + (None, None, None) + }, + Err(e) => return Err(e), // Propagate other errors + }; + + // Update LiteLLM YAML config - use the confirmed LLM key, handle base URL appropriately + // We pass None for api_base here because prompt_and_manage_openai_api_key defaults to OpenAI and doesn't handle custom base URLs yet. + // update_env_file will handle LLM_BASE_URL if needed. + let updated_litellm_config_path = config_utils::update_litellm_yaml( &app_base_dir, &llm_api_key, - Some("https://api.openai.com/v1"), + None, // Let .env handle LLM_BASE_URL if set )?; - let litellm_config_path_str = litellm_config_path.to_string_lossy(); - - // Update .env file (this is the root .env) + let litellm_config_path_str = updated_litellm_config_path.to_string_lossy(); + + // Update root .env file with potentially new/confirmed keys and paths config_utils::update_env_file( - &main_dot_env_target_path, // Ensure this targets the root .env + &main_dot_env_path, Some(&llm_api_key), - Some(&reranker_config.api_key), - Some(&reranker_config.model), - Some(&reranker_config.base_url), - None, // Not prompting for LLM_BASE_URL in this flow yet, example has it. - Some(&litellm_config_path_str), // Add LiteLLM config path to env + rerank_api_key_opt.as_deref(), // Use Option::as_deref + rerank_model_opt.as_deref(), + rerank_base_url_opt.as_deref(), + None, // LLM_BASE_URL not explicitly managed here, relies on .env default or user manual edit + Some(&litellm_config_path_str), ) .map_err(|e| { BusterError::CommandError(format!( "Failed to ensure .env file configurations in {}: {}", - main_dot_env_target_path.display(), // Use root .env path here + main_dot_env_path.display(), e )) })?; - println!("--- Configuration Setup Complete ---"); - // --- END API Key and Reranker Setup using config_utils --- + println!("--- Configuration Setup/Check Complete ---"); + // --- END Configuration Checks/Updates --- Ok(app_base_dir) } async fn run_docker_compose_command( + app_base_dir: &Path, args: &[&str], operation_name: &str, no_track: bool, ) -> Result<(), BusterError> { - let persistent_app_dir = setup_persistent_app_environment().await?; + let persistent_app_dir = app_base_dir; // --- BEGIN Telemetry Update --- if operation_name == "Starting" && no_track { @@ -318,6 +348,10 @@ async fn run_docker_compose_command( "Buster services {} successfully.", operation_name.to_lowercase() )); + // Add port information specifically after starting + if operation_name == "Starting" { + println!("\n{}", format!("✅ Buster is now available at: {}", "http://localhost:3000".cyan()).bold()); + } Ok(()) } else { let err_msg = format!( @@ -338,12 +372,19 @@ async fn run_docker_compose_command( } pub async fn start(no_track: bool) -> Result<(), BusterError> { - run_docker_compose_command(&["up", "-d"], "Starting", no_track).await + // First, run the setup/check which includes printing headers/prompts if needed + let app_base_dir = setup_persistent_app_environment().await?; + // Then, run the docker command in that directory + run_docker_compose_command(&app_base_dir, &["up", "-d"], "Starting", no_track).await } pub async fn stop() -> Result<(), BusterError> { - // Pass false for no_track as it's irrelevant for 'stop' - run_docker_compose_command(&["down"], "Stopping", false).await + // Get the app dir path directly, skipping setup/checks + let app_base_dir = config_utils::get_app_base_dir().map_err(|e| { + BusterError::CommandError(format!("Failed to get app base directory: {}", e)) + })?; + // Run the docker command in that directory + run_docker_compose_command(&app_base_dir, &["down"], "Stopping", false).await } pub async fn reset() -> Result<(), BusterError> { @@ -368,12 +409,13 @@ pub async fn reset() -> Result<(), BusterError> { return Ok(()); } + // Get app_base_dir directly at the start let app_base_dir = config_utils::get_app_base_dir().map_err(|e| { BusterError::CommandError(format!("Failed to get app base directory: {}", e)) })?; println!("Target application directory for reset: {}", app_base_dir.display()); - // Backup credentials if they exist + // Backup credentials if they exist (uses app_base_dir) let credentials_path = app_base_dir.join("credentials.yml"); let credentials_backup = fs::read(&credentials_path).ok(); if credentials_backup.is_some() { @@ -382,20 +424,21 @@ pub async fn reset() -> Result<(), BusterError> { println!("No credentials.yml found at {} to preserve.", credentials_path.display()); } - // Ensure app_base_dir exists and essential files for Docker commands are present - // These files will be wiped later with the rest of app_base_dir. + // Ensure app_base_dir exists and temporary docker-compose.yml for commands fs::create_dir_all(&app_base_dir).map_err(|e| BusterError::CommandError(format!("Failed to create app base directory {}: {}", app_base_dir.display(), e)))?; - let dc_filename = "docker-compose.yml"; - let dc_asset = StaticAssets::get(dc_filename) - .ok_or_else(|| BusterError::CommandError(format!("Failed to get embedded asset: {}", dc_filename)))?; - fs::write(app_base_dir.join(dc_filename), dc_asset.data.as_ref()).map_err(|e| BusterError::CommandError(format!("Failed to write temporary {}: {}", dc_filename, e)))?; - - // docker-compose.yml references supabase/.env, so ensure it exists (can be empty) + // Ensure docker-compose.yml exists for the down command, even if basic + if !app_base_dir.join(dc_filename).exists() { + let dc_asset = StaticAssets::get(dc_filename) + .ok_or_else(|| BusterError::CommandError(format!("Failed to get embedded asset: {}", dc_filename)))?; + fs::write(app_base_dir.join(dc_filename), dc_asset.data.as_ref()).map_err(|e| BusterError::CommandError(format!("Failed to write temporary {}: {}", dc_filename, e)))?; + } + // Ensure supabase/.env exists for docker-compose down (can be empty) let supabase_dir = app_base_dir.join("supabase"); fs::create_dir_all(&supabase_dir).map_err(|e| BusterError::CommandError(format!("Failed to create supabase directory in app base dir: {}", e)))?; - fs::write(supabase_dir.join(".env"), "").map_err(|e| BusterError::CommandError(format!("Failed to write temporary supabase/.env: {}",e)))?; - + if !supabase_dir.join(".env").exists() { + fs::write(supabase_dir.join(".env"), "").map_err(|e| BusterError::CommandError(format!("Failed to write temporary supabase/.env: {}",e)))?; + } let pb = ProgressBar::new_spinner(); pb.enable_steady_tick(Duration::from_millis(120)); @@ -406,12 +449,11 @@ pub async fn reset() -> Result<(), BusterError> { .expect("Failed to set progress bar style"), ); - // Step 1: Stop services + // Step 1: Stop services (Execute docker compose down directly) pb.set_message("Resetting Buster services (1/3): Stopping services..."); - let mut down_cmd = Command::new("docker"); down_cmd - .current_dir(&app_base_dir) // Use the prepared app_base_dir + .current_dir(&app_base_dir) // Use the obtained app_base_dir .arg("compose") .arg("-p") .arg("buster") @@ -424,12 +466,7 @@ pub async fn reset() -> Result<(), BusterError> { })?; if !down_output.status.success() { let err_msg = format!( - "docker compose down failed (status: {}). Logs: -Working directory: {} -Stdout: -{} -Stderr: -{}", + "docker compose down failed (status: {}). Logs:\nWorking directory: {}\nStdout:\n{}\nStderr:\n{}", down_output.status, app_base_dir.display(), String::from_utf8_lossy(&down_output.stdout), @@ -440,12 +477,11 @@ Stderr: pb.println("Services stopped successfully."); } - - // Step 2: Identify and Remove service images + // Step 2: Identify and Remove service images (uses app_base_dir) pb.set_message("Resetting Buster services (2/3): Removing service images..."); let mut config_images_cmd = Command::new("docker"); config_images_cmd - .current_dir(&app_base_dir) // Use the prepared app_base_dir + .current_dir(&app_base_dir) // Use the obtained app_base_dir .arg("compose") .arg("-p") .arg("buster") @@ -525,7 +561,7 @@ Stderr: } pb.println("Service image removal process complete."); - // Step 3: Wipe app_base_dir and restore credentials + // Step 3: Wipe app_base_dir and restore credentials (uses app_base_dir) pb.set_message(format!("Resetting Buster services (3/3): Wiping {} and restoring credentials...", app_base_dir.display())); if app_base_dir.exists() {