xbp 0.5.1

XBP is a build pack and deployment management tool to deploy, rust, nextjs etc and manage the NGINX configs below it
Documentation
//! Deployment execution module
//!
//! This module handles the actual deployment process including port management,
//! git operations, building, and process management.

use super::deployment_config::DeploymentConfig;
use super::project_detector::{ProjectDetector, ProjectType};
use tokio::process::Command;
use std::time::Instant;

pub struct DeploymentExecutor {
    config: DeploymentConfig,
    debug: bool,
}

impl DeploymentExecutor {
    pub fn new(config: DeploymentConfig, debug: bool) -> Self {
        Self { config, debug }
    }

    /// Execute the full deployment process
    pub async fn deploy(&mut self) -> Result<(), String> {
        println!("\x1b[96m🚀 Starting deployment of {} on port {}\x1b[0m", 
                 self.config.app_name, self.config.port);

        let start_time = Instant::now();

        // Step 1: Detect project type and merge recommendations
        self.detect_and_configure_project().await?;

        // Step 2: Ensure port is available
        self.ensure_port_available().await?;

        // Step 3: Navigate to project directory
        self.validate_project_directory().await?;

        // Step 4: Git operations
        self.git_reset_and_pull().await?;

        // Step 5: Install dependencies
        if let Some(install_cmd) = &self.config.install_command.clone() {
            self.run_install_command(install_cmd).await?;
        }

        // Step 6: Build project
        if let Some(build_cmd) = &self.config.build_command.clone() {
            self.run_build_command(build_cmd).await?;
        }

        // Step 7: Stop existing processes
        self.stop_existing_processes().await?;

        // Step 8: Start new process
        self.start_application().await?;

        // Step 9: Save PM2 configuration
        self.save_pm2_config().await?;

        // Step 10: Save updated xbp.json
        self.config.save_xbp_config(None).await?;

        let elapsed = start_time.elapsed();
        println!("\x1b[92m✅ Successfully deployed {} on port {} in {:.2?}\x1b[0m", 
                 self.config.app_name, self.config.port, elapsed);

        Ok(())
    }

    /// Detect project type and configure deployment accordingly
    async fn detect_and_configure_project(&mut self) -> Result<(), String> {
        if self.debug {
            println!("[DEBUG] Detecting project type in: {}", self.config.app_dir.display());
        }

        let project_type = ProjectDetector::detect_project_type(&self.config.app_dir).await?;
        
        match &project_type {
            ProjectType::NextJs { package_json, has_next_config } => {
                println!("\x1b[94m📦 Detected Next.js project: {} v{}\x1b[0m", 
                         package_json.name, package_json.version);
                if *has_next_config {
                    println!("   ⚙️  Next.js configuration found");
                }
            }
            ProjectType::NodeJs { package_json } => {
                println!("\x1b[94m📦 Detected Node.js project: {} v{}\x1b[0m", 
                         package_json.name, package_json.version);
            }
            ProjectType::Rust { cargo_toml } => {
                println!("\x1b[94m🦀 Detected Rust project: {} v{}\x1b[0m", 
                         cargo_toml.name, cargo_toml.version);
            }
            ProjectType::Unknown => {
                println!("\x1b[93m⚠️  Unknown project type, using default configuration\x1b[0m");
            }
        }

        // Get recommendations and merge with current config
        let recommendations = ProjectDetector::get_deployment_recommendations(&project_type);
        self.config.merge_with_recommendations(&recommendations);

        if self.debug {
            println!("[DEBUG] Build command: {:?}", self.config.build_command);
            println!("[DEBUG] Start command: {:?}", self.config.start_command);
            println!("[DEBUG] Install command: {:?}", self.config.install_command);
        }

        Ok(())
    }

    /// Ensure the target port is available, kill existing processes or find new port
    async fn ensure_port_available(&mut self) -> Result<(), String> {
        println!("\x1b[94m🔍 Checking port {} availability...\x1b[0m", self.config.port);

        // Check if port is in use
        let port_check = Command::new("sh")
            .arg("-c")
            .arg(format!("sudo fuser {}/tcp", self.config.port))
            .output()
            .await;

        match port_check {
            Ok(output) if output.status.success() => {
                // Port is in use, try to kill the process
                println!("\x1b[93m⚠️  Port {} is in use, attempting to kill process...\x1b[0m", 
                         self.config.port);
                
                let kill_output = Command::new("sh")
                    .arg("-c")
                    .arg(format!("sudo fuser -k {}/tcp", self.config.port))
                    .output()
                    .await
                    .map_err(|e| format!("Failed to kill process on port {}: {}", self.config.port, e))?;

                if kill_output.status.success() {
                    println!("\x1b[92m✅ Successfully killed process on port {}\x1b[0m", self.config.port);
                } else {
                    // Failed to kill, find a new port
                    println!("\x1b[93m⚠️  Failed to kill process, searching for available port...\x1b[0m");
                    let new_port = self.find_available_port().await?;
                    self.config.update_port(new_port);
                    println!("\x1b[92m✅ Found available port: {}\x1b[0m", new_port);
                }
            }
            _ => {
                // Port is available
                println!("\x1b[92m✅ Port {} is available\x1b[0m", self.config.port);
            }
        }

        Ok(())
    }

    /// Find an available port in the range 1025-65535
    async fn find_available_port(&self) -> Result<u16, String> {
        for port in 1025..=65535 {
            let check_output = Command::new("sh")
                .arg("-c")
                .arg(format!("sudo fuser {}/tcp", port))
                .output()
                .await;

            if let Ok(output) = check_output {
                if !output.status.success() {
                    return Ok(port);
                }
            }
        }

        Err("No available ports found in range 1025-65535".to_string())
    }

    /// Validate that the project directory exists and is accessible
    async fn validate_project_directory(&self) -> Result<(), String> {
        if !self.config.app_dir.exists() {
            return Err(format!("Project directory does not exist: {}", self.config.app_dir.display()));
        }

        if !self.config.app_dir.is_dir() {
            return Err(format!("Project path is not a directory: {}", self.config.app_dir.display()));
        }

        println!("\x1b[94m📁 Project directory: {}\x1b[0m", self.config.app_dir.display());
        Ok(())
    }

    /// Reset git repository and pull latest changes
    async fn git_reset_and_pull(&self) -> Result<(), String> {
        println!("\x1b[94m🔄 Resetting git repository...\x1b[0m");
        
        let reset_output = Command::new("git")
            .arg("reset")
            .arg("--hard")
            .current_dir(&self.config.app_dir)
            .output()
            .await
            .map_err(|e| format!("Failed to execute git reset: {}", e))?;

        if !reset_output.status.success() {
            return Err(format!("Git reset failed: {}", 
                             String::from_utf8_lossy(&reset_output.stderr)));
        }

        println!("\x1b[94m⬇️  Pulling latest changes...\x1b[0m");
        
        let pull_output = Command::new("git")
            .arg("pull")
            .arg("origin")
            .arg("main")
            .current_dir(&self.config.app_dir)
            .output()
            .await
            .map_err(|e| format!("Failed to execute git pull: {}", e))?;

        if !pull_output.status.success() {
            return Err(format!("Git pull failed: {}", 
                             String::from_utf8_lossy(&pull_output.stderr)));
        }

        println!("\x1b[92m✅ Git operations completed\x1b[0m");
        Ok(())
    }

    /// Run the install command (e.g., pnpm install, cargo fetch)
    async fn run_install_command(&self, install_cmd: &str) -> Result<(), String> {
        println!("\x1b[94m📦 Installing dependencies: {}\x1b[0m", install_cmd);
        
        let install_output = Command::new("sh")
            .arg("-c")
            .arg(install_cmd)
            .current_dir(&self.config.app_dir)
            .output()
            .await
            .map_err(|e| format!("Failed to execute install command: {}", e))?;

        if !install_output.status.success() {
            return Err(format!("Install command failed: {}", 
                             String::from_utf8_lossy(&install_output.stderr)));
        }

        println!("\x1b[92m✅ Dependencies installed successfully\x1b[0m");
        Ok(())
    }

    /// Run the build command
    async fn run_build_command(&self, build_cmd: &str) -> Result<(), String> {
        println!("\x1b[94m🔨 Building project: {}\x1b[0m", build_cmd);
        
        let build_output = Command::new("sh")
            .arg("-c")
            .arg(build_cmd)
            .current_dir(&self.config.app_dir)
            .output()
            .await
            .map_err(|e| format!("Failed to execute build command: {}", e))?;

        if !build_output.status.success() {
            return Err(format!("Build command failed: {}", 
                             String::from_utf8_lossy(&build_output.stderr)));
        }

        println!("\x1b[92m✅ Project built successfully\x1b[0m");
        Ok(())
    }

    /// Stop existing PM2 processes and kill processes on the target port
    async fn stop_existing_processes(&self) -> Result<(), String> {
        println!("\x1b[94m🛑 Stopping existing processes...\x1b[0m");

        // Stop PM2 process
        let pm2_stop = Command::new("pm2")
            .arg("stop")
            .arg(&self.config.app_name)
            .output()
            .await;

        match pm2_stop {
            Ok(output) if output.status.success() => {
                println!("   ✅ Stopped PM2 process: {}", self.config.app_name);
            }
            _ => {
                println!("   ℹ️  No existing PM2 process found");
            }
        }

        // Kill any remaining processes on the port
        let kill_port = Command::new("sh")
            .arg("-c")
            .arg(format!("sudo fuser -k {}/tcp", self.config.port))
            .output()
            .await;

        match kill_port {
            Ok(output) if output.status.success() => {
                println!("   ✅ Killed processes on port {}", self.config.port);
            }
            _ => {
                println!("   ℹ️  No processes found on port {}", self.config.port);
            }
        }

        Ok(())
    }

    /// Start the application using PM2
    async fn start_application(&self) -> Result<(), String> {
        let start_cmd = self.config.start_command.as_ref()
            .ok_or("No start command configured")?;

        println!("\x1b[94m🚀 Starting application: {}\x1b[0m", start_cmd);

        // Construct PM2 start command with port
        let pm2_cmd = format!("{} --port {}", start_cmd, self.config.port);
        
        let start_output = Command::new("pm2")
            .arg("start")
            .arg(&pm2_cmd)
            .arg("--name")
            .arg(&self.config.app_name)
            .arg("--")
            .arg("--port")
            .arg(&self.config.port.to_string())
            .current_dir(&self.config.app_dir)
            .output()
            .await
            .map_err(|e| format!("Failed to start PM2 process: {}", e))?;

        if !start_output.status.success() {
            return Err(format!("Failed to start application: {}", 
                             String::from_utf8_lossy(&start_output.stderr)));
        }

        println!("\x1b[92m✅ Application started successfully\x1b[0m");
        Ok(())
    }

    /// Save PM2 process list
    async fn save_pm2_config(&self) -> Result<(), String> {
        println!("\x1b[94m💾 Saving PM2 configuration...\x1b[0m");
        
        let save_output = Command::new("pm2")
            .arg("save")
            .output()
            .await
            .map_err(|e| format!("Failed to save PM2 config: {}", e))?;

        if !save_output.status.success() {
            return Err(format!("PM2 save failed: {}", 
                             String::from_utf8_lossy(&save_output.stderr)));
        }

        println!("\x1b[92m✅ PM2 configuration saved\x1b[0m");
        Ok(())
    }
}