Merge pull request #313 from buster-so/staging

Staging
This commit is contained in:
dal 2025-05-12 17:07:37 -07:00 committed by GitHub
commit 0fe185e5b1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 333 additions and 276 deletions

View File

@ -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<String, BusterError> {
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::<u32>() {
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(),
})
}
}

View File

@ -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<PathBuf, BusterError> {
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() {