xbp 0.6.0

XBP is a build pack and deployment management tool to deploy, rust, nextjs etc and manage the NGINX configs below it
Documentation
//! service command module
//!
//! handles service related commands list build install start dev
//! manages service configuration loading and command execution
//! supports root directory and force run from root logic
//! wraps start commands with pm2 when configured

use std::env;
use std::path::PathBuf;
use std::process::Stdio;
use tokio::process::Command;

use crate::strategies::{XbpConfig, ServiceConfig, get_all_services, get_service_by_name};
use crate::commands::pm2::pm2_start;
use crate::logging::{log_info, log_success, log_debug};

/// List all services from the current xbp.json config
pub async fn list_services(_debug: bool) -> Result<(), String> {
    let config = load_xbp_config().await?;
    let services = get_all_services(&config);
    
    if services.is_empty() {
        println!("No services configured.");
        return Ok(());
    }
    
    println!("\nServices:");
    println!("{:-<80}", "");
    println!("{:<20} | {:<12} | {:<6} | {:<15} | {:<20}", "Name", "Target", "Port", "Branch", "URL");
    println!("{:-<80}", "");
    
    for service in &services {
        let url = service.url.as_deref().unwrap_or("-");
        println!(
            "{:<20} | {:<12} | {:<6} | {:<15} | {:<20}",
            service.name, service.target, service.port, service.branch, url
        );
    }
    
    println!("{:-<80}", "");
    println!("\nTotal: {} service(s)", services.len());
    
    Ok(())
}

/// Get service by name from config
pub async fn get_service_config(name: &str) -> Result<ServiceConfig, String> {
    let config = load_xbp_config().await?;
    get_service_by_name(&config, name)
}

/// Run a service command (build, install, start, dev)
pub async fn run_service_command(
    command: &str,
    service_name: &str,
    debug: bool,
) -> Result<(), String> {
    let config = load_xbp_config().await?;
    let service = get_service_by_name(&config, service_name)?;
    
    // Determine working directory
    let project_root = env::current_dir()
        .map_err(|e| format!("Failed to get current directory: {}", e))?;
    
    let working_dir = if service.force_run_from_root.unwrap_or(false) {
        // If force_run_from_root is true, use project root
        project_root.clone()
    } else if let Some(root_dir) = &service.root_directory {
        // Use root_directory if specified
        if root_dir.starts_with('/') {
            // Absolute path
            PathBuf::from(root_dir)
        } else {
            // Relative to project root
            project_root.clone().join(root_dir)
        }
    } else {
        // Default to project root
        project_root.clone()
    };
    
    let _ = log_info(
        "service",
        &format!("Running '{}' for service '{}'", command, service_name),
        Some(&format!("Working directory: {}", working_dir.display())),
    )
    .await;
    
    // Get the command to run
    let cmd_str = match command {
        "pre" => service.commands.as_ref().and_then(|c| c.pre.as_ref()),
        "install" => service.commands.as_ref().and_then(|c| c.install.as_ref()),
        "build" => service.commands.as_ref().and_then(|c| c.build.as_ref()),
        "start" => service.commands.as_ref().and_then(|c| c.start.as_ref()),
        "dev" => service.commands.as_ref().and_then(|c| c.dev.as_ref()),
        _ => return Err(format!("Unknown command: {}. Valid commands: pre, install, build, start, dev", command)),
    };
    
    let cmd_str = cmd_str.ok_or_else(|| format!("Command '{}' not configured for service '{}'", command, service_name))?;
    
    // Handle empty build command
    if command == "build" && cmd_str.is_empty() {
        let _ = log_info("service", "Build command is empty, skipping", None).await;
        return Ok(());
    }
    
    // For start command, wrap with PM2 if start_wrapper is pm2
    if command == "start" && service.start_wrapper.as_deref() == Some("pm2") {
        // Create log directory
        let log_dir = project_root.join(".xbp").join("logs").join(&service.name);
        std::fs::create_dir_all(&log_dir)
            .map_err(|e| format!("Failed to create log directory: {}", e))?;
        
        // Build PM2 start command with port argument
        let pm2_command = format!("{} --port {}", cmd_str, service.port);
        
        let _ = log_info(
            "service",
            &format!("Starting service '{}' with PM2", service_name),
            Some(&pm2_command),
        )
        .await;
        
        pm2_start(&service.name, &pm2_command, Some(&log_dir), debug).await?;
        
        let _ = log_success(
            "service",
            &format!("Service '{}' started successfully", service_name),
            None,
        )
        .await;
        
        return Ok(());
    }
    
    // Run pre-command if it exists and we're not running pre itself
    if command != "pre" {
        if let Some(pre_cmd) = service.commands.as_ref().and_then(|c| c.pre.as_ref()) {
            if !pre_cmd.is_empty() {
                let _ = log_info("service", "Running pre-command", Some(pre_cmd)).await;
                run_command_in_dir(&working_dir, pre_cmd, debug).await?;
            }
        }
    }
    
    // Execute the command
    run_command_in_dir(&working_dir, cmd_str, debug).await?;
    
    let _ = log_success(
        "service",
        &format!("Command '{}' completed for service '{}'", command, service_name),
        None,
    )
    .await;
    
    Ok(())
}

/// Run a shell command in a specific directory
async fn run_command_in_dir(dir: &PathBuf, command: &str, _debug: bool) -> Result<(), String> {
    let _ = log_debug(
        "service",
        &format!("Executing: {} in {}", command, dir.display()),
        None,
    )
    .await;
    
    // Use shell to execute the command (supports complex commands with pipes, etc.)
    #[cfg(unix)]
    let mut cmd = Command::new("sh");
    #[cfg(unix)]
    cmd.arg("-c");
    
    #[cfg(windows)]
    let mut cmd = Command::new("cmd");
    #[cfg(windows)]
    cmd.arg("/C");
    
    cmd.arg(command);
    cmd.current_dir(dir);
    cmd.stdout(Stdio::inherit());
    cmd.stderr(Stdio::inherit());
    
    let status = cmd
        .status()
        .await
        .map_err(|e| format!("Failed to execute command: {}", e))?;
    
    if !status.success() {
        return Err(format!(
            "Command failed with exit code: {}",
            status.code().unwrap_or(-1)
        ));
    }
    
    Ok(())
}

/// Load xbp.json config from current directory
pub async fn load_xbp_config() -> Result<XbpConfig, String> {
    let current_dir = env::current_dir()
        .map_err(|e| format!("Failed to get current directory: {}", e))?;
    
    // Try .xbp/xbp.json first, then xbp.json
    let config_path = current_dir
        .join(".xbp")
        .join("xbp.json");
    
    let config_path = if config_path.exists() {
        config_path
    } else {
        let alt_path = current_dir.join("xbp.json");
        if alt_path.exists() {
            alt_path
        } else {
            return Err("No xbp.json found in current directory or .xbp/xbp.json".to_string());
        }
    };
    
    let content = std::fs::read_to_string(&config_path)
        .map_err(|e| format!("Failed to read config file: {}", e))?;
    
    let config: XbpConfig = serde_json::from_str(&content)
        .map_err(|e| format!("Failed to parse config file: {}", e))?;
    
    Ok(config)
}

/// Check if we're in an xbp project
pub async fn is_xbp_project() -> bool {
    let current_dir = match env::current_dir() {
        Ok(dir) => dir,
        Err(_) => return false,
    };
    
    current_dir.join(".xbp").join("xbp.json").exists()
        || current_dir.join("xbp.json").exists()
}

/// Show help for a specific service
pub async fn show_service_help(service_name: &str) -> Result<(), String> {
    let service = get_service_config(service_name).await?;
    
    println!("\nService: {}", service.name);
    println!("{:-<60}", "");
    println!("Target: {}", service.target);
    println!("Port: {}", service.port);
    println!("Branch: {}", service.branch);
    if let Some(url) = &service.url {
        println!("URL: {}", url);
    }
    if let Some(root_dir) = &service.root_directory {
        println!("Root Directory: {}", root_dir);
    }
    println!("Force Run From Root: {}", service.force_run_from_root.unwrap_or(false));
    
    if let Some(commands) = &service.commands {
        println!("\nAvailable Commands:");
        println!("{:-<60}", "");
        if commands.pre.is_some() {
            println!("  xbp service pre {}", service_name);
        }
        if commands.install.is_some() {
            println!("  xbp service install {}", service_name);
        }
        if commands.build.is_some() {
            println!("  xbp service build {}", service_name);
        }
        if commands.start.is_some() {
            println!("  xbp service start {}", service_name);
        }
        if commands.dev.is_some() {
            println!("  xbp service dev {}", service_name);
        }
    }
    
    println!("\nRedeploy:");
    println!("  xbp redeploy {}", service_name);
    
    Ok(())
}