xbp 10.15.4

XBP is a zero-config build pack that can also interact with proxies, kafka, sockets, synthetic monitors.
Documentation
use crate::cli::error::{CliError, CliResult};
use crate::commands::diag::check_nginx_mismatches;
use crate::commands::run_config;
use crate::commands::service::{is_xbp_project, list_services};
use crate::logging::{log_error, log_warn};
use crate::profile::{find_all_xbp_projects, rank_projects_by_proximity, Profile, ProjectInfo};
use crate::project_detector::ProjectType;
use crate::project_detector::{
    detect_project_types, display_project_types, display_suggested_commands,
};
use colored::Colorize;
use crossterm::{
    cursor::MoveToColumn,
    event::{self, Event, KeyCode},
    style::{Color, ResetColor, SetForegroundColor},
    terminal::{disable_raw_mode, enable_raw_mode, Clear, ClearType},
    ExecutableCommand,
};
use dialoguer::Input;
use std::env;
use std::io::{stdout, Write};

pub async fn handle_no_command(port: Option<u16>, debug: bool) -> CliResult<()> {
    if let Some(port) = port {
        let args = vec!["-p".to_string(), port.to_string()];
        if let Err(e) = crate::commands::ports::run_ports(&args, debug).await {
            let _ = log_error("ports", "Error running ports", Some(&e)).await;
        }
        return Ok(());
    }

    if is_xbp_project().await {
        let current_dir: std::path::PathBuf = env::current_dir().unwrap_or_default();

        if let Ok(mut profile) = Profile::load() {
            if let Some(project_name) = current_dir
                .file_name()
                .and_then(|n| n.to_str())
                .map(|s| s.to_string())
            {
                profile.update_last_project(current_dir.clone(), project_name);
                let _ = profile.save();
            }
        }

        println!("{}", "XBP Project Detected".bright_green());
        if let Err(e) = run_config(debug).await {
            let _ = log_warn(
                "config",
                "Unable to display project config summary",
                Some(&e),
            )
            .await;
        }

        let project_types: Vec<ProjectType> = detect_project_types(&current_dir);
        display_project_types(&project_types);

        if let Ok(mismatches) = check_nginx_mismatches().await {
            if !mismatches.is_empty() {
                let _ = log_warn(
                    "diag",
                    &format!(
                        "Found {} Nginx port mismatch(es). Run 'xbp diag' to fix.",
                        mismatches.len()
                    ),
                    None,
                )
                .await;
            }
        }

        if let Err(e) = list_services(debug).await {
            let _ = log_error("services", "Failed to list services", Some(&e)).await;
        }

        display_suggested_commands(&project_types);

        println!("\n{}", "Available Commands:".bright_blue());
        println!("{} - List all services", "xbp services".bright_magenta());
        println!(
            "{} - Run command for a service",
            "xbp service <cmd> <name>".bright_magenta()
        );
        println!(
            "{} - Redeploy a service",
            "xbp redeploy <name>".bright_magenta()
        );
        println!("{} - Show configuration", "xbp config".bright_magenta());
        println!(
            "{} - Save the PM2 process snapshot",
            "xbp snapshot".bright_magenta()
        );
        println!(
            "{} - Inspect and sync versions",
            "xbp version".bright_magenta()
        );
        println!(
            "{} - Diagnose and fix configuration issues",
            "xbp diag".bright_magenta()
        );
        println!("For more help: {}", "xbp --help".yellow());
    } else {
        handle_project_selection().await?;
    }
    Ok(())
}

pub async fn handle_project_selection() -> CliResult<()> {
    let profile: Profile = Profile::load().unwrap_or_default();

    if let Some(last_path) = &profile.last_project_path {
        if last_path.exists() {
            let project_name: &str = last_path
                .file_name()
                .and_then(|n| n.to_str())
                .unwrap_or("unknown");

            println!("\nLast XBP project:");
            println!("  Name: {}", project_name.purple());
            println!("  Path: {}", last_path.display());

            println!(
                "\n{} {}",
                "📌".bright_blue(),
                "Last XBP project:".bright_blue().bold()
            );
            println!(
                "  {} {}",
                "Name:".dimmed(),
                project_name.bright_cyan().bold()
            );
            println!(
                "  {} {}",
                "Path:".dimmed(),
                last_path.display().to_string().bright_black()
            );

            let response: String = Input::new()
                .with_prompt(format!(
                    "{} Continue with this project? (Y/n/a)",
                    "".bright_blue()
                ))
                .default("Y".to_string())
                .interact_text()
                .unwrap_or_else(|_| "n".to_string());

            match response.to_lowercase().as_str() {
                "y" | "" => {
                    println!(
                        "\n{} {} {}",
                        "".bright_green(),
                        "Switching to:".bright_green().bold(),
                        project_name.bright_cyan()
                    );
                    println!("\n{} {}", "💡".yellow(), "To navigate:".yellow());
                    println!(
                        "  {}",
                        format!("cd {}", last_path.display()).bright_cyan().bold()
                    );
                    return Ok(());
                }
                "a" => {
                    return show_all_projects().await;
                }
                _ => {
                    crate::commands::help::print_help().await;
                    return Ok(());
                }
            }
        }
    }
    println!(
        "\n{} {}",
        "⚠️".yellow(),
        "Currently not in an XBP project".yellow().bold()
    );
    println!(
        "  {} {}",
        "".dimmed(),
        "No xbp.json found in current directory or .xbp/xbp.json".dimmed()
    );
    println!("\n{}", "Options:".bright_blue().bold());
    println!(
        "  {} Run {} to initialize a new project",
        "".bright_blue(),
        "xbp setup".bright_cyan()
    );
    println!(
        "  {} Run {} to see all available XBP projects",
        "".bright_blue(),
        "xbp".bright_cyan()
    );

    let response: String = Input::new()
        .with_prompt(format!(
            "{} Show all XBP projects? (y/N)",
            "".bright_blue()
        ))
        .default("N".to_string())
        .interact_text()
        .unwrap_or_else(|_| "N".to_string());

    if response.to_lowercase() == "y" {
        show_all_projects().await?;
    } else {
        crate::commands::help::print_help().await;
    }

    Ok(())
}

pub async fn show_all_projects() -> CliResult<()> {
    println!(
        "\n{} {}",
        "🔍".bright_blue(),
        "Searching for XBP projects...".bright_blue().bold()
    );

    let projects: Vec<ProjectInfo> = find_all_xbp_projects();

    if projects.is_empty() {
        println!("\nNo XBP projects found.");
        println!("Run 'xbp setup' to initialize a new project.");
        println!(
            "\n{} {}",
            "⚠️".yellow(),
            "No XBP projects found.".yellow().bold()
        );
        println!(
            "  {} Run {} to initialize a new project.",
            "".dimmed(),
            "xbp setup".bright_cyan()
        );
        return Ok(());
    }

    let current_dir: std::path::PathBuf = env::current_dir().unwrap_or_default();
    let ranked_projects: Vec<ProjectInfo> =
        rank_projects_by_proximity(projects, current_dir.clone());

    println!("\nXBP Projects ({} found):", ranked_projects.len());
    println!(
        "\n{} {} {} found",
        "📦".bright_green(),
        "XBP Projects".bright_green().bold(),
        format!("({})", ranked_projects.len()).bright_black()
    );
    println!("{}", "".repeat(60).bright_black());

    let items: Vec<String> = ranked_projects
        .iter()
        .enumerate()
        .map(|(i, p)| {
            let is_current = p.path == current_dir;
            let number = format!("{:2}", i + 1).bright_black();
            let name = if is_current {
                format!("{} {}", p.name, "".yellow())
            } else {
                p.name.clone()
            };

            let path_display: String = if p.path.starts_with(dirs::home_dir().unwrap_or_default()) {
                p.path
                    .strip_prefix(dirs::home_dir().unwrap_or_default())
                    .map(|p| format!("~/{}", p.display()))
                    .unwrap_or_else(|_| p.path.display().to_string())
            } else {
                p.path.display().to_string()
            };

            let line = format!(
                "{} {} {}",
                number,
                name.bright_cyan().bold(),
                format!("({})", path_display).bright_black()
            );
            println!("{}", line);
            line
        })
        .collect();

    if items.is_empty() {
        return Ok(());
    }

    println!("\nUse ← and → to select a project, Enter to confirm, Esc to cancel.\n");
    interactive_project_selector(&ranked_projects)?;

    Ok(())
}

fn interactive_project_selector(projects: &[ProjectInfo]) -> CliResult<()> {
    if projects.is_empty() {
        return Ok(());
    }

    let mut index: usize = 0;
    let mut out: std::io::Stdout = stdout();

    enable_raw_mode().map_err(|e| CliError::from(e.to_string()))?;

    loop {
        out.execute(Clear(ClearType::CurrentLine))
            .map_err(|e| CliError::from(e.to_string()))?;
        out.execute(MoveToColumn(0))
            .map_err(|e| CliError::from(e.to_string()))?;

        for (i, project) in projects.iter().enumerate() {
            if i == index {
                out.execute(SetForegroundColor(Color::Green))
                    .map_err(|e| CliError::from(e.to_string()))?;
                write!(out, " [{}] ", project.name).map_err(|e| CliError::from(e.to_string()))?;
                out.execute(ResetColor)
                    .map_err(|e| CliError::from(e.to_string()))?;
            } else {
                write!(out, " {} ", project.name).map_err(|e| CliError::from(e.to_string()))?;
            }
        }

        out.flush().map_err(|e| CliError::from(e.to_string()))?;

        if let Event::Key(key) = event::read().map_err(|e| CliError::from(e.to_string()))? {
            match key.code {
                KeyCode::Left => {
                    if index == 0 {
                        index = projects.len().saturating_sub(1);
                    } else {
                        index -= 1;
                    }
                }
                KeyCode::Right => {
                    index = (index + 1) % projects.len();
                }
                KeyCode::Enter => {
                    disable_raw_mode().ok();
                    let selected: &ProjectInfo = &projects[index];
                    println!("\nSelected: {}", selected.name);
                    println!("Path: {}", selected.path.display());
                    println!("To navigate: cd {}", selected.path.display());

                    println!(
                        "\n{} {} {}",
                        "".bright_green(),
                        "Selected:".bright_green().bold(),
                        selected.name.bright_cyan()
                    );
                    println!(
                        "  {} {}",
                        "Path:".dimmed(),
                        selected.path.display().to_string().bright_white()
                    );
                    println!(
                        "\n{} {}",
                        "💡".yellow(),
                        "To navigate to this project:".yellow()
                    );
                    println!(
                        "  {}",
                        format!("cd {}", selected.path.display())
                            .bright_cyan()
                            .bold()
                    );

                    return Ok(());
                }
                KeyCode::Esc => {
                    disable_raw_mode().ok();
                    println!();
                    return Ok(());
                }
                _ => {}
            }
        }
    }
}