use std::env;
use std::path::PathBuf;
use std::process::Stdio;
use tokio::process::Command;
use tracing::info;
use crate::commands::pm2::pm2_start;
use crate::logging::{get_prefix, log_debug, log_info, log_success};
use crate::strategies::{get_all_services, get_service_by_name, ServiceConfig, XbpConfig};
use crate::utils::{
collapse_home_to_env, expand_home_in_string, find_xbp_config_upwards,
parse_config_with_auto_heal,
};
pub async fn list_services(_debug: bool) -> Result<(), String> {
let config: XbpConfig = load_xbp_config().await?;
let services: Vec<ServiceConfig> = get_all_services(&config);
if services.is_empty() {
let _ = log_info("services", "No services configured.", None).await;
println!("No services configured.");
return Ok(());
}
println!("Services:");
println!("{:-<80}", "");
println!(
"{:<20} | {:<12} | {:<6} | {:<15} | {:<20}",
"Name", "Target", "Port", "Branch", "URL"
);
println!("{:-<80}", "");
for service in &services {
let url = service.url.as_deref().unwrap_or("-");
println!(
"{:<20} | {:<12} | {:<6} | {:<15} | {:<20}",
service.name, service.target, service.port, service.branch, url
);
}
println!("{:-<80}", "");
println!("Total: {} service(s)", services.len());
Ok(())
}
pub async fn get_service_config(name: &str) -> Result<ServiceConfig, String> {
let config = load_xbp_config().await?;
get_service_by_name(&config, name)
}
pub async fn run_service_command(
command: &str,
service_name: &str,
debug: bool,
) -> Result<(), String> {
let (project_root, config): (PathBuf, XbpConfig) = load_xbp_config_with_root().await?;
let service: ServiceConfig = get_service_by_name(&config, service_name)?;
let working_dir = if service.force_run_from_root.unwrap_or(false) {
project_root.clone()
} else if let Some(root_dir) = &service.root_directory {
let expanded = expand_home_in_string(root_dir);
let candidate = PathBuf::from(expanded);
if candidate.is_absolute() {
candidate
} else {
project_root.clone().join(root_dir)
}
} else {
project_root.clone()
};
let _ = log_info(
"service",
&format!("Running '{}' for service '{}'", command, service_name),
Some(&format!("Working directory: {}", working_dir.display())),
)
.await;
let cmd_str: Option<&String> = match command {
"pre" => service.commands.as_ref().and_then(|c| c.pre.as_ref()),
"install" => service.commands.as_ref().and_then(|c| c.install.as_ref()),
"build" => service.commands.as_ref().and_then(|c| c.build.as_ref()),
"start" => service.commands.as_ref().and_then(|c| c.start.as_ref()),
"dev" => service.commands.as_ref().and_then(|c| c.dev.as_ref()),
_ => {
return Err(format!(
"Unknown command: {}. Valid commands: pre, install, build, start, dev",
command
))
}
};
let cmd_str = cmd_str.ok_or_else(|| {
format!(
"Command '{}' not configured for service '{}'",
command, service_name
)
})?;
if command == "build" && cmd_str.is_empty() {
let _ = log_info("service", "Build command is empty, skipping", None).await;
return Ok(());
}
if command == "start" && service.start_wrapper.as_deref() == Some("pm2") {
let log_dir = project_root.join(".xbp").join("logs").join(&service.name);
std::fs::create_dir_all(&log_dir)
.map_err(|e| format!("Failed to create log directory: {}", e))?;
let pm2_command: String = format!("{} --port {}", cmd_str, service.port);
let _ = log_info(
"service",
&format!("Starting service '{}' with PM2", service_name),
Some(&pm2_command),
)
.await;
let envs = merge_envs(config.environment.as_ref(), service.environment.as_ref());
pm2_start(
&service.name,
&pm2_command,
Some(&log_dir),
envs.as_ref(),
debug,
)
.await?;
let _ = log_success(
"service",
&format!("Service '{}' started successfully", service_name),
None,
)
.await;
return Ok(());
}
let envs = merge_envs(config.environment.as_ref(), service.environment.as_ref());
if command != "pre" {
if let Some(pre_cmd) = service.commands.as_ref().and_then(|c| c.pre.as_ref()) {
if !pre_cmd.is_empty() {
let _ = log_info("service", "Running pre-command", Some(pre_cmd)).await;
run_command_in_dir(&working_dir, pre_cmd, envs.as_ref(), debug).await?;
}
}
}
run_command_in_dir(&working_dir, cmd_str, envs.as_ref(), debug).await?;
let _ = log_success(
"service",
&format!(
"Command '{}' completed for service '{}'",
command, service_name
),
None,
)
.await;
Ok(())
}
async fn run_command_in_dir(
dir: &PathBuf,
command: &str,
envs: Option<&std::collections::HashMap<String, String>>,
_debug: bool,
) -> Result<(), String> {
let _ = log_debug(
"service",
&format!("Executing: {} in {}", command, dir.display()),
None,
)
.await;
#[cfg(unix)]
let mut cmd = Command::new("sh");
#[cfg(unix)]
cmd.arg("-c");
#[cfg(windows)]
let mut cmd = Command::new("cmd");
#[cfg(windows)]
cmd.arg("/C");
cmd.arg(command);
cmd.current_dir(dir);
if let Some(envs) = envs {
cmd.envs(envs);
}
cmd.stdout(Stdio::inherit());
cmd.stderr(Stdio::inherit());
let status = cmd
.status()
.await
.map_err(|e| format!("Failed to execute command: {}", e))?;
if !status.success() {
return Err(format!(
"Command failed with exit code: {}",
status.code().unwrap_or(-1)
));
}
Ok(())
}
fn merge_envs(
global: Option<&std::collections::HashMap<String, String>>,
service: Option<&std::collections::HashMap<String, String>>,
) -> Option<std::collections::HashMap<String, String>> {
if global.is_none() && service.is_none() {
return None;
}
let mut out = std::collections::HashMap::new();
if let Some(g) = global {
out.extend(g.clone());
}
if let Some(s) = service {
out.extend(s.clone());
}
Some(out)
}
fn maybe_write_yaml_codemod(project_root: &PathBuf, cfg: &XbpConfig) {
let yaml_path = project_root.join(".xbp").join("xbp.yaml");
if yaml_path.exists() {
return;
}
let mut yaml_cfg = cfg.clone();
yaml_cfg.build_dir = collapse_home_to_env(&yaml_cfg.build_dir);
if let Some(services) = &mut yaml_cfg.services {
for s in services {
if let Some(rd) = &s.root_directory {
s.root_directory = Some(collapse_home_to_env(rd));
}
}
}
if let Ok(yaml) = serde_yaml::to_string(&yaml_cfg) {
let _ = std::fs::create_dir_all(project_root.join(".xbp"));
let _ = std::fs::write(&yaml_path, yaml);
}
}
pub async fn load_xbp_config_with_root() -> Result<(PathBuf, XbpConfig), String> {
let current_dir: PathBuf =
env::current_dir().map_err(|e| format!("Failed to get current directory: {}", e))?;
let found = find_xbp_config_upwards(¤t_dir).ok_or_else(|| {
format!(
"{}\n\n{}\n{}",
"Currently not in an XBP project".to_string(),
"No xbp.yaml/xbp.yml/xbp.json found in current directory or .xbp/".to_string(),
"Run 'xbp' to select a project or 'xbp setup' to initialize a new project.".to_string()
)
})?;
let content: String = std::fs::read_to_string(&found.config_path)
.map_err(|e| format!("Failed to read config file: {}", e))?;
let (mut config, healed_content): (XbpConfig, Option<String>) =
parse_config_with_auto_heal(&content, found.kind).map_err(|e| {
if found.kind == "yaml" {
format!("Failed to parse YAML config: {}", e)
} else {
format!("Failed to parse JSON config: {}", e)
}
})?;
if let Some(healed_content) = healed_content {
let _ = std::fs::write(&found.config_path, healed_content);
}
config.build_dir = expand_home_in_string(&config.build_dir);
if let Some(services) = &mut config.services {
for s in services {
if let Some(rd) = &s.root_directory {
s.root_directory = Some(expand_home_in_string(rd));
}
}
}
if found.kind == "json"
&& found.config_path.parent().and_then(|p| p.file_name())
== Some(std::ffi::OsStr::new(".xbp"))
{
maybe_write_yaml_codemod(&found.project_root, &config);
}
Ok((found.project_root, config))
}
pub async fn load_xbp_config() -> Result<XbpConfig, String> {
Ok(load_xbp_config_with_root().await?.1)
}
pub async fn is_xbp_project() -> bool {
let current_dir = match env::current_dir() {
Ok(dir) => dir,
Err(_) => return false,
};
find_xbp_config_upwards(¤t_dir).is_some()
}
pub async fn show_service_help(service_name: &str) -> Result<(), String> {
let service = get_service_config(service_name).await?;
let prefix = get_prefix();
info!("{}", prefix);
info!("{} Service: {}", prefix, service.name);
info!("{} {:-<60}", prefix, "");
info!("{} Target: {}", prefix, service.target);
info!("{} Port: {}", prefix, service.port);
info!("{} Branch: {}", prefix, service.branch);
if let Some(url) = &service.url {
info!(" {}URL: {}", prefix, url);
}
if let Some(root_dir) = &service.root_directory {
info!(" {}Root Directory: {}", prefix, root_dir);
}
info!(
"{} Force Run From Root: {}",
prefix,
service.force_run_from_root.unwrap_or(false)
);
if let Some(commands) = &service.commands {
info!("{}", prefix);
info!("{} Available Commands:", prefix);
info!("{} {:-<60}", prefix, "");
if commands.pre.is_some() {
info!("{} Service pre {}", prefix, service_name);
}
if commands.install.is_some() {
info!("{} Service install {}", prefix, service_name);
}
if commands.build.is_some() {
info!("{} Service build {}", prefix, service_name);
}
if commands.start.is_some() {
info!("{} Service start {}", prefix, service_name);
}
if commands.dev.is_some() {
info!("{} Service dev {}", prefix, service_name);
}
}
info!("{}", prefix);
info!("{} Redeploy:", prefix);
info!("{} Redeploy {}", prefix, service_name);
Ok(())
}