xbp 10.15.4

XBP is a zero-config build pack that can also interact with proxies, kafka, sockets, synthetic monitors.
Documentation
//! config command module
//!
//! locates and prints xbp.json from either .xbp/xbp.json or ./xbp.json
//! when debug is enabled prints resolution details and raw json
//! displays configuration in a simple aligned table format
use crate::config::global_xbp_paths;
use crate::logging::{get_prefix, log_info};
use crate::strategies::project_detector::DeploymentRecommendations;
use crate::strategies::{ProjectDetector, ProjectType, XbpConfig};
use crate::utils::{collapse_home_to_env, find_xbp_config_upwards};
use crate::utils::{open_path_with_editor, open_with_default_handler};
use std::env;
use std::fs;
use std::path::PathBuf;
use tracing::{debug, error, info};

/// Execute the `config` command.
///
/// Prints discovered configuration keys and values in a simple aligned table.
/// Returns `Ok(())` even when no config is found to keep UX friendly.
pub async fn run_config(debug: bool) -> Result<(), String> {
    let current_dir: PathBuf =
        env::current_dir().map_err(|e| format!("Failed to get current directory: {}", e))?;
    let found = find_xbp_config_upwards(&current_dir);
    let project_root = found
        .as_ref()
        .map(|f| f.project_root.clone())
        .unwrap_or_else(|| current_dir.clone());

    let xbp_yaml_path_dotfolder: PathBuf = project_root.join(".xbp/xbp.yaml");
    let xbp_yml_path_dotfolder: PathBuf = project_root.join(".xbp/xbp.yml");
    let xbp_json_path_dotfolder: PathBuf = project_root.join(".xbp/xbp.json");

    let xbp_yaml_path_root: PathBuf = project_root.join("xbp.yaml");
    let xbp_yml_path_root: PathBuf = project_root.join("xbp.yml");
    let xbp_json_path_root: PathBuf = project_root.join("xbp.json");

    if debug {
        debug!("Current dir: {}", current_dir.display());
        debug!("Project root: {}", project_root.display());
        debug!("Checking for: {}", xbp_yaml_path_dotfolder.display());
        debug!("Checking for: {}", xbp_yml_path_dotfolder.display());
        debug!("Checking for: {}", xbp_json_path_dotfolder.display());
        debug!("Checking for: {}", xbp_yaml_path_root.display());
        debug!("Checking for: {}", xbp_yml_path_root.display());
        debug!("Checking for: {}", xbp_json_path_root.display());
    }

    if !xbp_yaml_path_dotfolder.exists() && xbp_json_path_dotfolder.exists() {
        if let Ok(contents) = fs::read_to_string(&xbp_json_path_dotfolder) {
            if let Ok(mut cfg) = serde_json::from_str::<XbpConfig>(&contents) {
                cfg.build_dir = collapse_home_to_env(&cfg.build_dir);
                if let Some(services) = &mut cfg.services {
                    for s in services {
                        if let Some(rd) = &s.root_directory {
                            s.root_directory = Some(collapse_home_to_env(rd));
                        }
                    }
                }
                if let Ok(yaml) = serde_yaml::to_string(&cfg) {
                    let _ = fs::write(&xbp_yaml_path_dotfolder, yaml);
                }
            }
        }
    }

    let (mut found_path, mut found_location, mut kind): (Option<PathBuf>, Option<String>, &str) =
        if let Some(f) = &found {
            (
                Some(f.config_path.clone()),
                Some(f.location.clone()),
                f.kind,
            )
        } else if xbp_yaml_path_dotfolder.exists() {
            (
                Some(xbp_yaml_path_dotfolder.clone()),
                Some(".xbp/xbp.yaml".to_string()),
                "yaml",
            )
        } else if xbp_yml_path_dotfolder.exists() {
            (
                Some(xbp_yml_path_dotfolder.clone()),
                Some(".xbp/xbp.yml".to_string()),
                "yaml",
            )
        } else if xbp_json_path_dotfolder.exists() {
            (
                Some(xbp_json_path_dotfolder.clone()),
                Some(".xbp/xbp.json".to_string()),
                "json",
            )
        } else if xbp_yaml_path_root.exists() {
            (
                Some(xbp_yaml_path_root.clone()),
                Some("xbp.yaml".to_string()),
                "yaml",
            )
        } else if xbp_yml_path_root.exists() {
            (
                Some(xbp_yml_path_root.clone()),
                Some("xbp.yml".to_string()),
                "yaml",
            )
        } else if xbp_json_path_root.exists() {
            (
                Some(xbp_json_path_root.clone()),
                Some("xbp.json".to_string()),
                "json",
            )
        } else {
            (None, None, "unknown")
        };

    if found_path.is_none() {
        let detected: Option<ProjectType> = ProjectDetector::detect_project_type(&current_dir)
            .await
            .ok();
        let recommendations: Option<DeploymentRecommendations> = detected
            .as_ref()
            .map(ProjectDetector::get_deployment_recommendations);

        let inferred_name: String = current_dir
            .file_name()
            .and_then(|n| n.to_str())
            .unwrap_or("app")
            .to_string();

        let default_port: u16 = recommendations
            .as_ref()
            .map(|r| r.default_port)
            .unwrap_or(8080);

        let target = detected.as_ref().map(|t| match t {
            ProjectType::NextJs { .. } => "nextjs",
            ProjectType::NodeJs { .. } => "expressjs",
            ProjectType::Rust { .. } => "rust",
            ProjectType::Python { .. } => "python",
            ProjectType::DockerCompose { .. } => "docker-compose",
            ProjectType::Docker { .. } => "docker",
            ProjectType::Railway { .. } => "railway",
            ProjectType::OpenApi { .. } => "openapi",
            ProjectType::Terraform { .. } => "terraform",
            ProjectType::Unknown => "unknown",
        });

        let baseline: XbpConfig = XbpConfig {
            project_name: recommendations
                .as_ref()
                .and_then(|r| r.process_name.clone())
                .unwrap_or(inferred_name),
            version: "0.1.0".to_string(),
            port: default_port,
            build_dir: collapse_home_to_env(current_dir.to_string_lossy().as_ref()),
            app_type: target.map(|s| s.to_string()),
            build_command: recommendations
                .as_ref()
                .and_then(|r| r.build_command.clone()),
            start_command: recommendations
                .as_ref()
                .and_then(|r| r.start_command.clone()),
            install_command: recommendations
                .as_ref()
                .and_then(|r| r.install_command.clone()),
            environment: None,
            services: None,
            systemd_service_name: None,
            kafka_brokers: None,
            kafka_topic: None,
            kafka_public_url: None,
            log_files: None,
            monitor_url: None,
            monitor_method: None,
            monitor_expected_code: None,
            monitor_interval: None,
            database: None,
            target: target.map(|s| s.to_string()),
            branch: Some("main".to_string()),
            crate_name: None,
            npm_script: None,
            port_storybook: None,
            url: None,
            url_storybook: None,
        };

        if let Err(e) = fs::create_dir_all(current_dir.join(".xbp")) {
            let msg: String = format!("Failed to create .xbp directory: {}", e);
            error!("{}", msg);
            eprintln!("{}", msg);
        } else {
            let out_json = current_dir.join(".xbp/xbp.json");
            let out_yaml = current_dir.join(".xbp/xbp.yaml");
            let wrote_yaml = if let Ok(yaml) = serde_yaml::to_string(&baseline) {
                fs::write(&out_yaml, yaml).is_ok()
            } else {
                false
            };
            let wrote_json = if let Ok(content) = serde_json::to_string_pretty(&baseline) {
                fs::write(&out_json, content).is_ok()
            } else {
                false
            };

            if wrote_yaml {
                found_path = Some(out_yaml);
                found_location = Some(".xbp/xbp.yaml".to_string());
                kind = "yaml";
                println!("Generated .xbp/xbp.yaml from detected manifests.");
            } else if wrote_json {
                found_path = Some(out_json);
                found_location = Some(".xbp/xbp.json".to_string());
                kind = "json";
                println!("Generated .xbp/xbp.json from detected manifests.");
            }
        }
    }

    if let (Some(path), Some(location)) = (found_path, found_location) {
        let _ = log_info(
            "config",
            &format!("Found config at: {}", path.display()),
            None,
        )
        .await;
        println!("Found config at: {}", path.display());

        match fs::read_to_string(&path) {
            Ok(contents) => {
                if debug {
                    debug!("config contents: {}", contents);
                }
                let data = if kind == "yaml" {
                    serde_yaml::from_str::<serde_yaml::Value>(&contents)
                        .ok()
                        .and_then(|v| serde_json::to_value(v).ok())
                } else {
                    serde_json::from_str::<serde_json::Value>(&contents).ok()
                };

                if let Some(json_data) = data {
                    let prefix = get_prefix();
                    println!("\nConfiguration:");
                    println!("{}", "".repeat(50));
                    for (key, value) in json_data.as_object().unwrap() {
                        let value_str: String = value.to_string().replace("\"", "");
                        println!("{:<15} |   {}", key, value_str);
                        info!("{}{:<15} |   {}", prefix, key, value_str);
                    }
                    println!("{}", "".repeat(50));
                } else {
                    let msg = format!("Failed to parse {} contents.", location);
                    error!("{}", msg);
                    eprintln!("{}", msg);
                }
            }
            Err(e) => {
                let msg = format!("Failed to read {}: {}", location, e);
                error!("{}", msg);
                eprintln!("{}", msg);
            }
        }
    } else {
        let msg =
            "No .xbp/xbp.yaml|.yml|.json or xbp.yaml|.yml|.json found in the current directory.";
        error!("{}", msg);
        eprintln!("{}", msg);
    }

    Ok(())
}

pub async fn open_global_config(no_open: bool) -> Result<(), String> {
    let paths = global_xbp_paths()?;

    println!("Global XBP directory: {}", paths.root_dir.display());
    println!("Config file: {}", paths.config_file.display());
    println!("SSH directory: {}", paths.ssh_dir.display());
    println!("Cache directory: {}", paths.cache_dir.display());
    println!("Logs directory: {}", paths.logs_dir.display());

    if no_open {
        return Ok(());
    }

    let _ = open_with_default_handler(&paths.root_dir.display().to_string());
    let _ = open_path_with_editor(&paths.config_file);

    Ok(())
}