mirror of https://github.com/buster-so/buster.git
commit
0fe185e5b1
|
@ -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(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
Loading…
Reference in New Issue