xbp 10.28.0

XBP is a zero-config build pack that can also interact with proxies, kafka, sockets, synthetic monitors.
Documentation
use crate::strategies::project_detector::{ProjectDetector, ProjectHint};
use colored::Colorize;
use std::path::Path;

use crate::utils::{preferred_pip_command, preferred_python_command};

pub use crate::strategies::project_detector::ProjectHint as ProjectType;

pub fn detect_project_types(path: &Path) -> Vec<ProjectType> {
    ProjectDetector::detect_project_hints(path).unwrap_or_default()
}

pub fn suggest_commands(project_types: &[ProjectType]) -> Vec<String> {
    let mut commands = Vec::new();

    for project_type in project_types {
        match project_type {
            ProjectType::Docker => {
                commands.push("docker build -t <image-name> .".to_string());
                commands.push("docker run -p <port>:<port> <image-name>".to_string());
            }
            ProjectType::DockerCompose => {
                commands.push("docker-compose up -d".to_string());
                commands.push("docker-compose down".to_string());
            }
            ProjectType::Python => {
                let pip = preferred_pip_command();
                let python = preferred_python_command();
                commands.push(format!("{pip} install -r requirements.txt"));
                commands.push(format!("{python} main.py"));
            }
            ProjectType::NodeJs => {
                commands.push("npm install".to_string());
                commands.push("npm start".to_string());
            }
            ProjectType::Rust => {
                commands.push("cargo build --release".to_string());
                commands.push("cargo run".to_string());
            }
            ProjectType::Railway => {
                commands.push("railway up".to_string());
                commands.push("railway logs".to_string());
            }
            ProjectType::Vercel => {
                commands.push("vercel".to_string());
                commands.push("vercel deploy --prod".to_string());
                commands.push("vercel --version".to_string());
            }
            ProjectType::Go => {
                commands.push("go build".to_string());
                commands.push("go run .".to_string());
            }
        }
    }

    commands.dedup();
    commands
}

pub fn display_project_types(project_types: &[ProjectType]) {
    if project_types.is_empty() {
        return;
    }

    let types_str = project_types
        .iter()
        .map(|project_type| {
            format!(
                "{} {}",
                project_type_icon(*project_type),
                project_type_name(*project_type)
            )
        })
        .collect::<Vec<_>>()
        .join(", ");

    tracing::info!("{} {}", "Detected:".bright_blue(), types_str);
}

pub fn display_suggested_commands(project_types: &[ProjectType]) {
    let commands = suggest_commands(project_types);

    if commands.is_empty() {
        return;
    }

    tracing::info!("{}", "Suggested commands:".bright_blue());
    for cmd in commands.iter().take(3) {
        tracing::info!("  {}", cmd.dimmed());
    }
}

fn project_type_name(project_type: ProjectHint) -> &'static str {
    match project_type {
        ProjectType::Docker => "Docker",
        ProjectType::DockerCompose => "Docker Compose",
        ProjectType::Python => "Python",
        ProjectType::NodeJs => "Node.js",
        ProjectType::Rust => "Rust",
        ProjectType::Railway => "Railway",
        ProjectType::Vercel => "Vercel",
        ProjectType::Go => "Go",
    }
}

fn project_type_icon(project_type: ProjectHint) -> &'static str {
    match project_type {
        ProjectType::Docker => "docker",
        ProjectType::DockerCompose => "compose",
        ProjectType::Python => "python",
        ProjectType::NodeJs => "node",
        ProjectType::Rust => "rust",
        ProjectType::Railway => "railway",
        ProjectType::Vercel => "vercel",
        ProjectType::Go => "go",
    }
}

#[cfg(test)]
mod tests {
    use super::{detect_project_types, suggest_commands, ProjectType};
    use std::fs;
    use std::sync::atomic::{AtomicU64, Ordering};

    #[test]
    fn detect_project_types_delegates_to_shared_build_hints() {
        let project_root = temp_dir("xbp-cli-project-hints");
        fs::create_dir_all(&project_root).expect("create temp dir");
        fs::write(project_root.join("go.mod"), "module demo\n").expect("write go mod");
        fs::write(project_root.join("Cargo.toml"), "[package]\nname='demo'\n")
            .expect("write cargo toml");

        let project_types = detect_project_types(&project_root);

        assert!(project_types.contains(&ProjectType::Go));
        assert!(project_types.contains(&ProjectType::Rust));

        let _ = fs::remove_dir_all(project_root);
    }

    #[test]
    fn suggest_commands_keeps_go_cli_guidance() {
        let commands = suggest_commands(&[ProjectType::Go]);

        assert_eq!(
            commands,
            vec!["go build".to_string(), "go run .".to_string()]
        );
    }

    #[test]
    fn suggest_commands_include_vercel_deploy_guidance() {
        let commands = suggest_commands(&[ProjectType::Vercel]);

        assert_eq!(
            commands,
            vec![
                "vercel".to_string(),
                "vercel deploy --prod".to_string(),
                "vercel --version".to_string(),
            ]
        );
    }

    fn temp_dir(prefix: &str) -> std::path::PathBuf {
        static COUNTER: AtomicU64 = AtomicU64::new(0);
        let unique = COUNTER.fetch_add(1, Ordering::Relaxed);
        std::env::temp_dir().join(format!("{prefix}-{unique}"))
    }
}