xbp 0.7.0

XBP is a build pack and deployment management tool to deploy, rust, nextjs etc and manage the NGINX configs below it
Documentation
use crate::cli::commands::{DiagCmd, NginxSubCommand};
use crate::commands::load_xbp_config;
use crate::commands::nginx::run_nginx;
use crate::logging::{log_error, log_info, log_success, log_warn};
use crate::strategies::get_all_services;
use anyhow::Result;
use regex::Regex;
use std::fs;
use std::io::{self, Write};
use std::path::PathBuf;

pub async fn run_diag(cmd: DiagCmd, debug: bool) -> Result<()> {
    if cmd.nginx {
        check_nginx_interactive(debug).await?;
    } else {
        // Run all checks
        check_nginx_interactive(debug).await?;
    }
    Ok(())
}

/// Check Nginx status and return a list of mismatches: (service_name/domain, expected_port, actual_port)
pub async fn check_nginx_mismatches() -> Result<Vec<(String, u16, u16)>> {
    let config = match load_xbp_config().await {
        Ok(c) => c,
        Err(_) => return Ok(vec![]), // Not in a project or invalid config
    };

    let services = get_all_services(&config);
    let mut mismatches = Vec::new();

    let sites_enabled = PathBuf::from("/etc/nginx/sites-enabled");
    if !sites_enabled.exists() {
        return Ok(vec![]);
    }

    let proxy_pass_regex = Regex::new(r"proxy_pass\s+http://127\.0\.0\.1:(\d+);").unwrap();
    // server_name_regex removed as unused

    for service in services {
        let expected_port = service.port;
        // Try to find config by service name or url (if it's a domain)
        let mut potential_names = vec![service.name.clone()];
        if let Some(url) = &service.url {
            let domain = url.trim_start_matches("http://").trim_start_matches("https://").split('/').next().unwrap_or(url);
            potential_names.push(domain.to_string());
        }

        for name in potential_names {
            let config_path = sites_enabled.join(&name);
            if config_path.exists() {
                if let Ok(content) = fs::read_to_string(&config_path) {
                     if let Some(cap) = proxy_pass_regex.captures(&content) {
                        if let Ok(port) = cap[1].parse::<u16>() {
                            if port != expected_port {
                                mismatches.push((name.clone(), expected_port, port));
                                break; // Found the config, stop checking other names
                            }
                        }
                     }
                }
            }
        }
    }

    Ok(mismatches)
}

async fn check_nginx_interactive(debug: bool) -> Result<()> {
    let _ = log_info("diag", "Checking Nginx configuration...", None).await;
    
    let mismatches = check_nginx_mismatches().await?;

    if mismatches.is_empty() {
        let _ = log_success("diag", "Nginx configuration looks correct.", None).await;
        return Ok(());
    }

    for (name, expected, actual) in mismatches {
        let _ = log_warn(
            "diag", 
            &format!("Port mismatch for {}: Nginx uses {}, xbp.json uses {}", name, actual, expected), 
            None
        ).await;

        print!("Do you want to update Nginx to use port {}? [y/N]: ", expected);
        io::stdout().flush()?;
        
        let mut input = String::new();
        io::stdin().read_line(&mut input)?;
        
        if input.trim().to_lowercase() == "y" {
            // Call nginx update command
            let cmd = NginxSubCommand::Update {
                domain: name.clone(),
                port: expected,
            };
            if let Err(e) = run_nginx(cmd, debug).await {
                let _ = log_error("diag", &format!("Failed to update Nginx for {}", name), Some(&e.to_string())).await;
            }
        }
    }

    Ok(())
}