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
//! 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 std::time::Instant;
use tokio::process::Command;

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> {
        let start_time: Instant = Instant::now();

        self.detect_and_configure_project().await?;
        self.ensure_port_available().await?;
        self.validate_project_directory().await?;
        self.git_reset_and_pull().await?;
        if let Some(install_cmd) = &self.config.install_command.clone() {
            self.run_install_command(install_cmd).await?;
        }
        if let Some(build_cmd) = &self.config.build_command.clone() {
            self.run_build_command(build_cmd).await?;
        }
        self.stop_existing_processes().await?;
        self.start_application().await?;
        self.save_pm2_config().await?;
        self.config.save_xbp_config(None).await?;

        let elapsed: std::time::Duration = 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(())
    }
}