xbp 0.9.4

XBP is a zero-config build pack that can also interact with proxies, kafka, sockets, synthetic monitors.
Documentation
use tracing::{debug, info};
use tokio::process::Command;
use std::path::PathBuf;
use std::fs;
use colored::Colorize;

async fn check_pm2_installed() -> Result<(), String> {
    let check_result = if cfg!(target_os = "windows") {
        Command::new("powershell")
            .arg("-Command")
            .arg("pm2 --version")
            .output()
            .await
    } else {
        Command::new("pm2")
            .arg("--version")
            .output()
            .await
    };

    match check_result {
        Ok(output) if output.status.success() => Ok(()),
        _ => {
            eprintln!("PM2 is not installed.");
            eprintln!("");
            eprintln!("To install PM2:");
            eprintln!("  npm install -g pm2");
            eprintln!("");
            info!("{}", "PM2 is not installed.".red());
            info!("");
            info!("{}", "To install PM2:".bright_blue());
            info!("  {}", "npm install -g pm2".cyan());
            info!("");
            Err("PM2 not found".to_string())
        }
    }
}

pub async fn list(debug: bool) -> Result<(), String> {
    check_pm2_installed().await?;

    let mut cmd = if cfg!(target_os = "windows") {
        let mut c = Command::new("powershell");
        c.arg("-Command");
        c.arg("pm2 list");
        c
    } else {
        let mut c = Command::new("pm2");
        c.arg("list");
        c
    };
    
    let status = cmd
        .stdout(std::process::Stdio::inherit())
        .stderr(std::process::Stdio::inherit())
        .status()
        .await
        .map_err(|e| format!("failed to run pm2 list: {}", e))?;
    
    if !status.success() {
        return Err("pm2 list failed".to_string());
    }
    
    Ok(())
}

pub async fn logs(project: Option<String>, debug: bool) -> Result<(), String> {
    let mut cmd = Command::new("pm2");
    cmd.arg("logs");
    if let Some(name) = project {
        cmd.arg(name);
    }
    let mut child = cmd
        .stdout(std::process::Stdio::inherit())
        .stderr(std::process::Stdio::inherit())
        .spawn()
        .map_err(|e| format!("failed to spawn pm2 logs: {}", e))?;
    if debug { debug!("spawned pm2 logs"); }
    let status = child.wait().await.map_err(|e| format!("pm2 logs wait failed: {}", e))?;
    if !status.success() { return Err(format!("pm2 logs exited with status: {}", status)); }
    Ok(())
}

/// Stop a PM2 process by name
pub async fn stop(name: &str, debug: bool) -> Result<(), String> {
    let mut cmd = Command::new("pm2");
    cmd.arg("stop").arg(name);
    let output = cmd.output().await.map_err(|e| format!("failed to run pm2 stop: {}", e))?;
    
    if debug {
        debug!(
            "pm2 stop {} status={:?} stdout='{}' stderr='{}'",
            name,
            output.status,
            String::from_utf8_lossy(&output.stdout),
            String::from_utf8_lossy(&output.stderr)
        );
    }
    
    // PM2 stop returns success even if process doesn't exist, so we don't fail here
    Ok(())
}

/// Delete a PM2 process by name
pub async fn delete(name: &str, debug: bool) -> Result<(), String> {
    let mut cmd = Command::new("pm2");
    cmd.arg("delete").arg(name);
    let output = cmd.output().await.map_err(|e| format!("failed to run pm2 delete: {}", e))?;
    
    if debug {
        debug!(
            "pm2 delete {} status={:?} stdout='{}' stderr='{}'",
            name,
            output.status,
            String::from_utf8_lossy(&output.stdout),
            String::from_utf8_lossy(&output.stderr)
        );
    }
    
    // PM2 delete returns success even if process doesn't exist
    Ok(())
}

/// Start a PM2 process with the given command and name
pub async fn start(
    name: &str,
    command: &str,
    log_dir: Option<&PathBuf>,
    debug: bool,
) -> Result<(), String> {
    let mut cmd = Command::new("pm2");
    cmd.arg("start");
    
    // Set up log redirection if log_dir is provided
    if let Some(log_path) = log_dir {
        let stdout_log = log_path.join(format!("{}-stdout.log", name));
        let stderr_log = log_path.join(format!("{}-stderr.log", name));
        
        // Ensure log directory exists
        if let Some(parent) = log_path.parent() {
            fs::create_dir_all(parent)
                .map_err(|e| format!("Failed to create log directory: {}", e))?;
        }
        
        cmd.arg("--name").arg(name);
        cmd.arg("--log").arg(stdout_log.to_string_lossy().as_ref());
        cmd.arg("--error").arg(stderr_log.to_string_lossy().as_ref());
        cmd.arg("--").arg(command);
    } else {
        cmd.arg("--name").arg(name);
        cmd.arg("--").arg(command);
    }
    
    let output = cmd.output().await.map_err(|e| format!("failed to run pm2 start: {}", e))?;
    
    if debug {
        debug!(
            "pm2 start {} status={:?} stdout='{}' stderr='{}'",
            name,
            output.status,
            String::from_utf8_lossy(&output.stdout),
            String::from_utf8_lossy(&output.stderr)
        );
    }
    
    if !output.status.success() {
        return Err(format!(
            "pm2 start failed: {}",
            String::from_utf8_lossy(&output.stderr)
        ));
    }
    
    Ok(())
}

/// Clean up stopped and errored PM2 processes
pub async fn cleanup(debug: bool) -> Result<(), String> {
    // Get list of PM2 processes
    let mut cmd = Command::new("pm2");
    cmd.arg("list").arg("--no-color");
    let output = cmd.output().await.map_err(|e| format!("failed to run pm2 list: {}", e))?;
    
    if !output.status.success() {
        return Err(format!("Failed to list PM2 processes: {}", String::from_utf8_lossy(&output.stderr)));
    }
    
    let stdout = String::from_utf8_lossy(&output.stdout);
    let mut processes_to_delete = Vec::new();
    
    // Parse PM2 list output to find stopped/errored processes
    for line in stdout.lines() {
        if line.contains("stopped") || line.contains("errored") {
            // PM2 list format: "│ 0  │ app-name │ stopped │ ..."
            let parts: Vec<&str> = line.split('').collect();
            if parts.len() >= 3 {
                let name = parts[2].trim();
                if !name.is_empty() && name != "name" {
                    processes_to_delete.push(name.to_string());
                }
            }
        }
    }
    
    for process_name in processes_to_delete {
        if debug {
            debug!("Deleting stopped/errored process: {}", process_name);
        }
        delete(&process_name, debug).await?;
    }
    
    // Save PM2 process list
    let mut save_cmd = Command::new("pm2");
    save_cmd.arg("save");
    let save_output = save_cmd.output().await.map_err(|e| format!("failed to run pm2 save: {}", e))?;
    
    if !save_output.status.success() {
        return Err(format!("Failed to save PM2 process list: {}", String::from_utf8_lossy(&save_output.stderr)));
    }
    
    Ok(())
}

/// Save PM2 process list
pub async fn save(debug: bool) -> Result<(), String> {
    let mut cmd = Command::new("pm2");
    cmd.arg("save");
    let output = cmd.output().await.map_err(|e| format!("failed to run pm2 save: {}", e))?;
    
    if debug {
        debug!(
            "pm2 save status={:?} stdout='{}' stderr='{}'",
            output.status,
            String::from_utf8_lossy(&output.stdout),
            String::from_utf8_lossy(&output.stderr)
        );
    }
    
    if !output.status.success() {
        return Err(format!("Failed to save PM2 process list: {}", String::from_utf8_lossy(&output.stderr)));
    }
    
    Ok(())
}