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 }
}
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(())
}
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");
}
}
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(())
}
async fn ensure_port_available(&mut self) -> Result<(), String> {
println!(
"\x1b[94m🔍 Checking port {} availability...\x1b[0m",
self.config.port
);
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() => {
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 {
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);
}
}
_ => {
println!("\x1b[92m✅ Port {} is available\x1b[0m", self.config.port);
}
}
Ok(())
}
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())
}
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(())
}
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(())
}
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(())
}
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(())
}
async fn stop_existing_processes(&self) -> Result<(), String> {
println!("\x1b[94m🛑 Stopping existing processes...\x1b[0m");
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");
}
}
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(())
}
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);
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(())
}
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(())
}
}