xbp 0.9.3

XBP is a zero-config build pack that can also interact with proxies, kafka, sockets, synthetic monitors.
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 tracing::info;

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

/// 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() {
        let _ = log_info("services", "No services configured.", None).await;
        return Ok(());
    }

    let prefix = get_prefix();
    info!("{}", prefix);
    info!("{} Services:", prefix);
    info!("{} {:-<80}", prefix, "");
    info!(
        "{} {:<20} | {:<12} | {:<6} | {:<15} | {:<20}",
        prefix, "Name", "Target", "Port", "Branch", "URL"
    );
    info!(" {}{:-<80}", prefix, "");

    for service in &services {
        let url = service.url.as_deref().unwrap_or("-");
        info!(
            "{}{:<20} | {:<12} | {:<6} | {:<15} | {:<20}",
            prefix, service.name, service.target, service.port, service.branch, url
        );
    }

    info!("{} {:-<80}", prefix, "");
    info!("{}", prefix);
    info!("{} Total: {} service(s)", prefix, 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: XbpConfig = load_xbp_config().await?;
    let service: ServiceConfig = 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: Option<&String> = 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: String = 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: PathBuf =
        env::current_dir().map_err(|e| format!("Failed to get current directory: {}", e))?;

    // Try .xbp/xbp.json first, then xbp.json
    let config_path: PathBuf = current_dir.join(".xbp").join("xbp.json");

    let config_path: PathBuf = if config_path.exists() {
        config_path
    } else {
        let alt_path = current_dir.join("xbp.json");
        if alt_path.exists() {
            alt_path
        } else {
            return Err(format!(
                "{}\n\n{}\n{}",
                "Currently not in an XBP project".to_string(),
                "No xbp.json found in current directory or .xbp/xbp.json".to_string(),
                "Run 'xbp' to select a project or 'xbp setup' to initialize a new project.".to_string()
            ));
        }
    };

    let content: String = 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?;
    let prefix = get_prefix();

    info!("{}", prefix);
    info!("{} Service: {}", prefix, service.name);
    info!("{} {:-<60}", prefix, "");
    info!("{} Target: {}", prefix, service.target);
    info!("{} Port: {}", prefix, service.port);
    info!("{} Branch: {}", prefix, service.branch);
    if let Some(url) = &service.url {
        info!(" {}URL: {}", prefix, url);
    }
    if let Some(root_dir) = &service.root_directory {
        info!(" {}Root Directory: {}", prefix, root_dir);
    }
    info!(
        "{} Force Run From Root: {}",
        prefix,
        service.force_run_from_root.unwrap_or(false)
    );

    if let Some(commands) = &service.commands {
        info!("{}", prefix);
        info!("{} Available Commands:", prefix);
        info!("{} {:-<60}", prefix, "");
        if commands.pre.is_some() {
            info!("{} Service pre {}", prefix, service_name);
        }
        if commands.install.is_some() {
            info!("{} Service install {}", prefix, service_name);
        }
        if commands.build.is_some() {
            info!("{} Service build {}", prefix, service_name);
        }
        if commands.start.is_some() {
            info!("{} Service start {}", prefix, service_name);
        }
        if commands.dev.is_some() {
            info!("{} Service dev {}", prefix, service_name);
        }
    }

    info!("{}", prefix);
    info!("{} Redeploy:", prefix);
    info!("{} Redeploy {}", prefix, service_name);

    Ok(())
}