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(())
}
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)
);
}
Ok(())
}
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)
);
}
Ok(())
}
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");
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));
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(())
}
pub async fn cleanup(debug: bool) -> Result<(), String> {
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();
for line in stdout.lines() {
if line.contains("stopped") || line.contains("errored") {
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?;
}
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(())
}
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(())
}