diff --git a/.github/workflows/api-testing.yml b/.github/workflows/api-testing.yml index 9ee4aff8c..61b668c2f 100644 --- a/.github/workflows/api-testing.yml +++ b/.github/workflows/api-testing.yml @@ -64,9 +64,11 @@ jobs: # Sensitive values from Secrets OPENAI_API_KEY: ${{ secrets.GH_ACTIONS_OPENAI_API_KEY }} RESEND_API_KEY: ${{ secrets.GH_ACTIONS_RESEND_API_KEY }} - COHERE_API_KEY: ${{ secrets.GH_ACTIONS_COHERE_API_KEY }} LLM_API_KEY: ${{ secrets.GH_ACTIONS_LLM_API_KEY }} LLM_BASE_URL: ${{ secrets.GH_ACTIONS_LLM_BASE_URL }} + RERANK_API_KEY: ${{ secrets.GH_ACTIONS_COHERE_API_KEY }} + RERANK_MODEL: rerank-v3.5 + RERANK_BASE_URL: https://api.cohere.com/v2/rerank - name: Stop Supabase # Use the cleanup action uses: ./.github/actions/stop-supabase diff --git a/.github/workflows/cli-testing.yml b/.github/workflows/cli-testing.yml index 12fb2f0cb..7608eabff 100644 --- a/.github/workflows/cli-testing.yml +++ b/.github/workflows/cli-testing.yml @@ -58,7 +58,9 @@ jobs: -e RUST_LOG='debug' \ -e OPENAI_API_KEY='${{ secrets.GH_ACTIONS_OPENAI_API_KEY }}' \ -e RESEND_API_KEY='${{ secrets.GH_ACTIONS_RESEND_API_KEY }}' \ - -e COHERE_API_KEY='${{ secrets.GH_ACTIONS_COHERE_API_KEY }}' \ + -e RERANK_API_KEY='${{ secrets.GH_ACTIONS_COHERE_API_KEY }}' \ + -e RERANK_MODEL='rerank-v3.5' \ + -e RERANK_BASE_URL='https://api.cohere.com/v2/rerank' \ -e LLM_API_KEY='${{ secrets.GH_ACTIONS_LLM_API_KEY }}' \ -e LLM_BASE_URL='${{ secrets.GH_ACTIONS_LLM_BASE_URL }}' \ local-api-test:latest diff --git a/.github/workflows/web-e2e-tests.yml b/.github/workflows/web-e2e-tests.yml index 65444f9fb..fce60484c 100644 --- a/.github/workflows/web-e2e-tests.yml +++ b/.github/workflows/web-e2e-tests.yml @@ -93,7 +93,9 @@ jobs: -e RUST_LOG='debug' \ -e OPENAI_API_KEY='${{ secrets.GH_ACTIONS_OPENAI_API_KEY }}' \ -e RESEND_API_KEY='${{ secrets.GH_ACTIONS_RESEND_API_KEY }}' \ - -e COHERE_API_KEY='${{ secrets.GH_ACTIONS_COHERE_API_KEY }}' \ + -e RERANK_API_KEY='${{ secrets.GH_ACTIONS_COHERE_API_KEY }}' \ + -e RERANK_MODEL='rerank-v3.5' \ + -e RERANK_BASE_URL='https://api.cohere.com/v2/rerank' \ -e LLM_API_KEY='${{ secrets.GH_ACTIONS_LLM_API_KEY }}' \ -e LLM_BASE_URL='${{ secrets.GH_ACTIONS_LLM_BASE_URL }}' \ local-api-test:latest diff --git a/api/makefile b/api/makefile index 7a5136f0f..b29ee50bd 100644 --- a/api/makefile +++ b/api/makefile @@ -1,26 +1,16 @@ dev: - @echo "Checking/Restarting Redis for dev target..." - @if ! docker ps -q -f name=buster-redis-make -f status=running > /dev/null; then \ - echo "Redis container 'buster-redis-make' not running. Restarting it..."; \ - docker stop buster-redis-make || true; \ - docker rm buster-redis-make || true; \ - (cd .. && docker run -d --name buster-redis-make -p 6379:6379 -v buster_redis_data:/data redis && cd api); \ - echo "Redis restarted."; \ - else \ - echo "Redis container 'buster-redis-make' already running."; \ - fi - @echo "Ensuring Supabase is (re)started for dev target..." - supabase stop || true - @echo "Starting Supabase..." - supabase start - @echo "Supabase (re)started." - supabase db reset + docker stop buster-redis-make || true && \ + docker rm buster-redis-make || true && \ + docker run -d --name buster-redis-make -p 6379:6379 -v buster_redis_data:/data redis && \ + supabase db reset || true && \ + supabase start || true && \ + supabase db reset || true && \ export DATABASE_URL=postgres://postgres:postgres@127.0.0.1:54322/postgres && \ - diesel migration run && \ - PGPASSWORD=postgres psql -h 127.0.0.1 -p 54322 -d postgres -U postgres -f libs/database/setup.sql && \ - PGPASSWORD=postgres psql -h 127.0.0.1 -p 54322 -d postgres -U postgres -f libs/database/seed.sql && \ - export RUST_LOG=debug - export CARGO_INCREMENTAL=1 + diesel migration run || true && \ + PGPASSWORD=postgres psql -h 127.0.0.1 -p 54322 -d postgres -U postgres -f libs/database/setup.sql || true && \ + PGPASSWORD=postgres psql -h 127.0.0.1 -p 54322 -d postgres -U postgres -f libs/database/seed.sql || true && \ + export RUST_LOG=debug && \ + export CARGO_INCREMENTAL=1 && \ nice cargo watch -C server -x run update-seed: @@ -39,16 +29,6 @@ stop: supabase stop fast: - @echo "Checking Redis for fast target..." - @if ! docker ps -q -f name=buster-redis-make -f status=running > /dev/null; then \ - echo "Redis container 'buster-redis-make' not running. Starting it..."; \ - docker stop buster-redis-make || true; \ - docker rm buster-redis-make || true; \ - (cd .. && docker run -d --name buster-redis-make -p 6379:6379 -v buster_redis_data:/data redis && cd api); \ - echo "Redis started."; \ - else \ - echo "Redis container 'buster-redis-make' already running."; \ - fi export RUST_LOG=debug && \ export CARGO_INCREMENTAL=1 && \ nice cargo watch -C server -x run \ No newline at end of file diff --git a/cli/cli/src/commands/auth.rs b/cli/cli/src/commands/auth.rs index cbf3d2288..5600a337d 100644 --- a/cli/cli/src/commands/auth.rs +++ b/cli/cli/src/commands/auth.rs @@ -30,8 +30,8 @@ pub enum AuthError { #[derive(Parser, Debug)] #[command(about = "Authenticate with Buster API")] pub struct AuthArgs { - /// The Buster API host UL - #[arg(long, env = "BUSTER_HOST")] + /// The Buster API host URL + #[arg(long, env = "BUSTER_HOST", conflicts_with_all = &["local", "cloud"])] pub host: Option, /// Your Buster API key @@ -45,6 +45,14 @@ pub struct AuthArgs { /// Clear saved credentials #[arg(long)] pub clear: bool, + + /// Use local buster instance (http://localhost:3001) + #[arg(long, help = "Use local buster instance (http://localhost:3001)", conflicts_with_all = &["host", "cloud"])] + pub local: bool, + + /// Use cloud buster instance (https://api.buster.so) + #[arg(long, help = "Use cloud buster instance (https://api.buster.so)", conflicts_with_all = &["host", "local"])] + pub cloud: bool, } // --- Credentials Validation Trait --- @@ -157,57 +165,86 @@ async fn handle_clear_flag(clear: bool) -> Result { /// Loads existing credentials or initializes default ones, handling overrides from args/env. /// Also prompts for overwrite confirmation if necessary. async fn load_and_confirm_credentials(args: &AuthArgs) -> Result> { - let mut buster_creds = match get_buster_credentials().await { - Ok(creds) => creds, - Err(_) => BusterCredentials { + // 1. Load credentials as they are from cache, or use pristine defaults if no cache. + let loaded_creds = match get_buster_credentials().await { + Ok(creds) => creds, // These are the actual stored credentials + Err(_) => BusterCredentials { // These are pristine defaults if nothing is stored url: DEFAULT_HOST.to_string(), api_key: String::new(), }, }; - let existing_creds_present = !buster_creds.url.is_empty() && !buster_creds.api_key.is_empty(); - // Apply args overrides early - host - if let Some(host) = &args.host { - buster_creds.url = host.clone(); - } - // Apply args overrides early - api key - if let Some(api_key) = &args.api_key { - buster_creds.api_key = api_key.clone(); - } + // 2. Determine if meaningful credentials were found in the cache. + // "Meaningful" here means an API key was actually stored. + let meaningful_cached_creds_existed = !loaded_creds.api_key.is_empty(); - let host_provided = args.host.is_some(); - let api_key_provided = args.api_key.is_some(); - let fully_provided_via_args = host_provided && api_key_provided; + // 3. Prepare the credentials that will be used, starting with cached/default values. + // These will be modified by CLI args. + let mut effective_creds = loaded_creds.clone(); - // Prompt for overwrite confirmation only if existing creds are present *and* not fully overridden by args - if existing_creds_present && !fully_provided_via_args { + // 4. Determine if host and API key are being specified by CLI arguments. + // `conflicts_with_all` ensures only one of `args.local`, `args.cloud`, or `args.host` is active. + let host_is_specified_by_cli_args = args.local || args.cloud || args.host.is_some(); + let api_key_is_specified_by_cli_args = args.api_key.is_some(); + + // 5. Decide whether to prompt for overwrite. + // Prompt if meaningful cached credentials existed AND + // the user is NOT providing a full set of overriding credentials (both host and API key) via CLI args. + if meaningful_cached_creds_existed && !(host_is_specified_by_cli_args && api_key_is_specified_by_cli_args) { let confirm = Confirm::new("Existing credentials found. Do you want to overwrite them?") .with_default(false) - .with_help_message("Select 'y' to proceed with entering new credentials, or 'n' to cancel.") + .with_help_message("Select 'y' to proceed with configuring credentials, or 'n' to cancel.") .prompt() .map_err(|e| AuthError::UserInputFailed(e.to_string()))?; if !confirm { println!("Authentication cancelled."); - return Ok(None); // Signal cancellation + return Ok(None); // User cancelled, so don't proceed. } - // If confirmed, proceed with the potentially modified buster_creds + // If user confirms, `effective_creds` (which will have CLI args applied next) will be used. + } + // If no meaningful cached creds, or if user provided full CLI args, no prompt needed. + + // 6. Apply CLI argument overrides to `effective_creds`. + if args.local { + effective_creds.url = "http://localhost:3001".to_string(); + } else if args.cloud { + effective_creds.url = DEFAULT_HOST.to_string(); + } else if let Some(host_arg) = &args.host { + effective_creds.url = host_arg.clone(); + } + // If no host arg was provided by CLI (neither --local, --cloud, nor --host) AND + // the URL in effective_creds is currently empty (e.g., cache was empty and returned BusterCredentials with empty url), + // set it to the default. This ensures URL is not empty before `prompt_for_missing_credentials` + // if no host arg was given and cache didn't provide one. + else if effective_creds.url.is_empty() { // This case handles if get_buster_credentials somehow returned empty URL or initial default was empty. + effective_creds.url = DEFAULT_HOST.to_string(); + } + // If get_buster_credentials returned a specific URL from cache, and no host args are given, it remains that specific URL. + // If get_buster_credentials returned the default URL (or cache was empty -> default), and no host args, it remains default. + + if let Some(api_key_arg) = &args.api_key { + effective_creds.api_key = api_key_arg.clone(); } - Ok(Some(buster_creds)) + Ok(Some(effective_creds)) } /// Prompts the user interactively for missing host and API key information. async fn prompt_for_missing_credentials( creds: &mut BusterCredentials, args: &AuthArgs, - existing_creds_present: bool, // Needed to adjust prompt text + api_key_is_currently_set: bool, // Reflects if API key is set in `creds` before this prompt ) -> Result<()> { let host_provided = args.host.is_some(); let api_key_provided = args.api_key.is_some(); - // Prompt for URL if not provided via args and current URL is default or empty - if !host_provided && (creds.url.is_empty() || creds.url == DEFAULT_HOST) { + // Prompt for URL if: + // - Not using --local. + // - Not using --cloud. + // - Not using --host. + // - AND the current URL in `creds` (after cache/args) is still empty or the default. + if !args.local && !args.cloud && args.host.is_none() && (creds.url.is_empty() || creds.url == DEFAULT_HOST) { let default_url_to_show = if creds.url.is_empty() { DEFAULT_HOST } else { &creds.url }; let url_input = Text::new("Enter the URL of your Buster API") .with_default(default_url_to_show) @@ -231,7 +268,7 @@ async fn prompt_for_missing_credentials( format!("{}...", &creds.api_key[0..std::cmp::min(4, creds.api_key.len())]) }; - let prompt_message = if existing_creds_present { + let prompt_message = if api_key_is_currently_set { // Use the passed flag format!("Enter new API key (current: {obfuscated_api_key}):") } else { format!("Enter your API key (current: {obfuscated_api_key}):") @@ -271,13 +308,27 @@ async fn save_credentials_or_notify( creds: BusterCredentials, no_save: bool, ) -> Result<()> { - if !no_save { - set_buster_credentials(creds).await - .map_err(|e| AuthError::SaveCredentialsFailed(e.to_string()))?; - println!("Credentials saved successfully!"); + let masked_api_key = if creds.api_key.is_empty() { + "[Not Set]".to_string() // Should ideally not happen if validation passed } else { - println!("Authentication successful!"); - println!("Note: Credentials were not saved due to --no-save flag"); + let api_key_len = creds.api_key.len(); + let visible_part_start_index = if api_key_len > 6 { api_key_len - 6 } else { 0 }; + format!("****{}", &creds.api_key[visible_part_start_index..]) + }; + + println!("✅ You've successfully connected to Buster!"); + println!(); // Newline for separation + println!("Connection details:"); + println!(" host: {}", creds.url); + println!(" api_key: {}", masked_api_key); + + if !no_save { + set_buster_credentials(creds) + .await + .map_err(|e| AuthError::SaveCredentialsFailed(e.to_string()))?; + println!("\nCredentials saved successfully!"); + } else { + println!("\nNote: Credentials were not saved due to --no-save flag"); } Ok(()) } @@ -289,24 +340,43 @@ pub async fn auth_with_args(args: AuthArgs) -> Result<()> { return Ok(()); // Exit early if credentials were cleared } - // 2. Load existing credentials or initialize defaults, confirm overwrite if needed - let mut opt_buster_creds = match load_and_confirm_credentials(&args).await? { + // 2. Load existing credentials or initialize defaults, confirm overwrite if needed, and apply args + let mut creds_after_load_and_args = match load_and_confirm_credentials(&args).await? { Some(creds) => creds, None => return Ok(()), // User cancelled overwrite prompt }; - let existing_creds_present = !opt_buster_creds.url.is_empty() && !opt_buster_creds.api_key.is_empty(); + + // Store the host URL that will be used for connection attempts. + // This URL is determined after considering cache, defaults, and CLI args like --local, --cloud, --host. + let attempted_host_url = creds_after_load_and_args.url.clone(); + // This flag is for tailoring the prompt text in `prompt_for_missing_credentials`. + let api_key_is_present_before_interactive_prompt = !creds_after_load_and_args.api_key.is_empty(); // 3. Prompt for missing credentials interactively - prompt_for_missing_credentials(&mut opt_buster_creds, &args, existing_creds_present).await?; - + if let Err(prompt_err) = prompt_for_missing_credentials( + &mut creds_after_load_and_args, + &args, + api_key_is_present_before_interactive_prompt + ).await { + eprintln!("Failed during credential input process."); + eprintln!("Intended host for configuration: {}", attempted_host_url); + return Err(prompt_err); // Propagate the original error (anyhow::Error) + } // 4. Validate the final credentials let validator = RealCredentialsValidator; - validate_credentials(&opt_buster_creds, &validator).await?; + if let Err(validation_err) = validate_credentials(&creds_after_load_and_args, &validator).await { + // validation_err is AuthError. We print its specific message. + eprintln!("Authentication failed: {}", validation_err); + eprintln!("Attempted to connect to host: {}", attempted_host_url); + return Err(validation_err.into()); // Propagate as anyhow::Error + } // 5. Save credentials or notify - save_credentials_or_notify(opt_buster_creds, args.no_save).await?; + // This function prints connection details (including host) on success internally. + // If it errors, it's a save error after successful validation, not a connection/validation error. + save_credentials_or_notify(creds_after_load_and_args, args.no_save).await?; Ok(()) } diff --git a/cli/cli/src/main.rs b/cli/cli/src/main.rs index 45704fe2a..9649d4e2b 100644 --- a/cli/cli/src/main.rs +++ b/cli/cli/src/main.rs @@ -39,6 +39,14 @@ pub enum Commands { /// Clear saved credentials #[arg(long)] clear: bool, + + /// Use local Buster API + #[arg(long)] + local: bool, + + /// Use cloud Buster API + #[arg(long)] + cloud: bool, }, /// Update buster-cli to the latest version Update { @@ -111,12 +119,16 @@ async fn main() { api_key, no_save, clear, + local, + cloud, } => { commands::auth::auth_with_args(AuthArgs { host, api_key, no_save, clear, + local, + cloud, }) .await }