xbp 0.6.1

XBP is a build pack and deployment management tool to deploy, rust, nextjs etc and manage the NGINX configs below it
Documentation
//! pm2 process management module
//!
//! provides functions for managing pm2 processes
//! includes list stop delete start cleanup and save operations
//! supports log redirection to xbp/logs directories

use tokio::process::Command;
use std::path::PathBuf;
use std::fs;

pub async fn pm2_list(debug: bool) -> Result<(), String> {
    let mut cmd = Command::new("pm2");
    cmd.arg("list");
    let output = cmd.output().await.map_err(|e| format!("failed to run pm2 list: {}", e))?;
    if debug {
        println!(
            "[DEBUG] pm2 list status={:?} stdout='{}' stderr='{}'",
            output.status,
            String::from_utf8_lossy(&output.stdout),
            String::from_utf8_lossy(&output.stderr)
        );
    }
    if !output.status.success() {
        return Err(String::from_utf8_lossy(&output.stderr).to_string());
    }
    print!("{}", String::from_utf8_lossy(&output.stdout));
    Ok(())
}

pub async fn pm2_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 { println!("[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 pm2_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 {
        println!(
            "[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 pm2_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 {
        println!(
            "[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 pm2_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 {
        println!(
            "[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 pm2_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());
                }
            }
        }
    }
    
    // Delete stopped/errored processes
    for process_name in processes_to_delete {
        if debug {
            println!("[DEBUG] Deleting stopped/errored process: {}", process_name);
        }
        pm2_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 pm2_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 {
        println!(
            "[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(())
}