pub mod commands;
use clap::Parser;
use commands::{Cli, Commands};
use crate::commands::curl;
use crate::commands::diag::check_nginx_mismatches;
use crate::commands::redeploy_v2::run_redeploy_v2;
use crate::commands::{install_package, run_config, run_ports, run_redeploy, run_setup};
use crate::commands::{
is_xbp_project, list_services, print_help, run_redeploy_service, run_service_command,
show_service_help,
};
use crate::commands::{pm2_list, pm2_logs, pm2_start_wrapper};
use crate::commands::{run_diag, run_nginx};
use crate::logging::{get_log_directory, init_logger, log_error, log_info, log_success, log_warn};
use crate::profile::{find_all_xbp_projects, rank_projects_by_proximity, Profile};
use crate::project_detector::{detect_project_types, display_project_types, display_suggested_commands};
use colored::Colorize;
use dialoguer::{Input, Select};
use std::env;
use tracing::info;
pub async fn run() -> Result<(), String> {
let cli = Cli::parse();
let debug = cli.debug;
if let Err(e) = init_logger(debug).await {
let _ = log_error(
"system",
"Failed to initialize logger",
Some(&e.to_string()),
)
.await;
}
if cli.logs {
return handle_logs_flag().await;
}
if cli.list && cli.command.is_none() {
if let Err(e) = pm2_list(debug).await {
let _ = log_error("pm2", "pm2 list failed", Some(&e)).await;
}
return Ok(());
}
match cli.command {
Some(Commands::Ports(cmd)) => handle_ports(cmd, cli.port, debug).await,
Some(Commands::Setup) => handle_setup(debug).await,
Some(Commands::Redeploy { service_name }) => handle_redeploy(service_name, debug).await,
Some(Commands::RedeployV2(cmd)) => handle_redeploy_v2(cmd, debug).await,
Some(Commands::Config) => handle_config(debug).await,
Some(Commands::Install { package }) => handle_install(package, debug).await,
Some(Commands::Logs(cmd)) => handle_logs(cmd, debug).await,
Some(Commands::List) => handle_list(debug).await,
Some(Commands::Curl { url }) => handle_curl(url, debug).await,
Some(Commands::Services) => handle_services(debug).await,
Some(Commands::Service {
command,
service_name,
}) => handle_service(command, service_name, debug).await,
Some(Commands::Nginx(cmd)) => handle_nginx(cmd.command, debug).await,
Some(Commands::Diag(cmd)) => handle_diag(cmd, debug).await,
Some(Commands::Monitor(cmd)) => handle_monitor(cmd, debug).await,
Some(Commands::Tail(cmd)) => handle_tail(cmd, debug).await,
Some(Commands::Start { args }) => handle_start(args, debug).await,
None => handle_no_command(cli.port, debug).await,
}
}
async fn handle_start(args: Vec<String>, debug: bool) -> Result<(), String> {
if let Err(e) = pm2_start_wrapper(args, debug).await {
let _ = log_error("start", "Failed to start process", Some(&e)).await;
return Err(e);
}
Ok(())
}
async fn handle_logs_flag() -> Result<(), String> {
let log_dir = get_log_directory().await?;
info!("{} {}", "Logs directory:".bright_blue(), log_dir.display().to_string().cyan());
if cfg!(target_os = "windows") {
info!("\n{}", "Opening in Explorer...".dimmed());
let _ = std::process::Command::new("explorer")
.arg(log_dir)
.spawn();
} else {
info!("\n{}", "To view logs:".bright_blue());
info!(" cd {}", log_dir.display().to_string().cyan());
info!(" tail -f xbp-*.log");
}
Ok(())
}
async fn handle_ports(
cmd: commands::PortsCmd,
global_port: Option<u16>,
debug: bool,
) -> Result<(), String> {
let mut args: Vec<String> = Vec::new();
let port = global_port.or(cmd.port);
if let Some(p) = port {
args.push("-p".to_string());
args.push(p.to_string());
}
if cmd.kill {
args.push("--kill".to_string());
args.push("-k".to_string());
}
if cmd.nginx {
args.push("-n".to_string());
args.push("--nginx".to_string());
}
if let Err(e) = run_ports(&args, debug).await {
let _ = log_error("ports", "Error running ports", Some(&e)).await;
}
Ok(())
}
async fn handle_setup(debug: bool) -> Result<(), String> {
if let Err(e) = run_setup(debug).await {
let _ = log_error("setup", "Setup failed", Some(&e)).await;
}
Ok(())
}
async fn handle_redeploy(service_name: Option<String>, debug: bool) -> Result<(), String> {
if let Some(name) = service_name {
if let Err(e) = run_redeploy_service(&name, debug).await {
let _ = log_error("redeploy", "Service redeploy failed", Some(&e)).await;
}
} else {
if let Err(e) = run_redeploy().await {
let _ = log_error("redeploy", "Redeploy failed", Some(&e)).await;
}
}
Ok(())
}
async fn handle_redeploy_v2(cmd: commands::RedeployV2Cmd, debug: bool) -> Result<(), String> {
let _ = log_info("redeploy_v2", "Starting remote redeploy process", None).await;
match run_redeploy_v2(cmd.password, cmd.username, cmd.host, cmd.project_dir, debug).await {
Ok(()) => Ok(()),
Err(e) => {
let _ = log_error("redeploy_v2", "Remote redeploy failed", Some(&e)).await;
Err(e)
}
}
}
async fn handle_config(debug: bool) -> Result<(), String> {
let _ = run_config(debug).await;
Ok(())
}
async fn handle_install(package: String, debug: bool) -> Result<(), String> {
if package.is_empty() || package == "--help" || package == "help" {
return install_package("", debug).await;
}
let install_msg = format!("Installing package: {}", package);
let _ = log_info("install", &install_msg, None).await;
match install_package(&package, debug).await {
Ok(()) => {
let success_msg = format!("Successfully installed: {}", package);
let _ = log_success("install", &success_msg, None).await;
Ok(())
}
Err(e) => {
Err(e)
}
}
}
async fn handle_logs(cmd: commands::LogsCmd, debug: bool) -> Result<(), String> {
if let Err(e) = pm2_logs(cmd.project, debug).await {
let _ = log_error("pm2", "pm2 logs failed", Some(&e)).await;
}
Ok(())
}
async fn handle_list(debug: bool) -> Result<(), String> {
if let Err(e) = pm2_list(debug).await {
let _ = log_error("pm2", "pm2 list failed", Some(&e)).await;
}
Ok(())
}
async fn handle_curl(url: Option<String>, debug: bool) -> Result<(), String> {
let url = url.unwrap_or_else(|| "https://example.com/api".to_string());
if let Err(e) = curl::run_curl(&url, debug).await {
let _ = log_error("curl", "Curl command failed", Some(&e)).await;
}
Ok(())
}
async fn handle_services(debug: bool) -> Result<(), String> {
if let Err(e) = list_services(debug).await {
let _ = log_error("services", "Failed to list services", Some(&e)).await;
}
Ok(())
}
async fn handle_service(
command: Option<String>,
service_name: Option<String>,
debug: bool,
) -> Result<(), String> {
if let Some(cmd) = command {
if cmd == "--help" || cmd == "help" {
if let Some(name) = service_name {
if let Err(e) = show_service_help(&name).await {
let _ = log_error("service", "Failed to show service help", Some(&e)).await;
}
} else {
info!("Usage: xbp service <command> <service-name>");
info!("Commands: build, install, start, dev");
info!("Example: xbp service build zeus");
info!("For help on a specific service: xbp service --help <service-name>");
}
} else {
if let Some(name) = service_name {
if let Err(e) = run_service_command(&cmd, &name, debug).await {
let _ = log_error(
"service",
&format!("Service command '{}' failed", cmd),
Some(&e),
)
.await;
}
} else {
let _ = log_error("service", "Service name required", None).await;
}
}
} else {
info!("Usage: xbp service <command> <service-name>");
info!("Commands: build, install, start, dev");
info!("Example: xbp service build zeus");
}
Ok(())
}
async fn handle_nginx(cmd: commands::NginxSubCommand, debug: bool) -> Result<(), String> {
if let Err(e) = run_nginx(cmd, debug).await {
let _ = log_error("nginx", "Nginx command failed", Some(&e.to_string())).await;
}
Ok(())
}
async fn handle_diag(cmd: commands::DiagCmd, debug: bool) -> Result<(), String> {
if let Err(e) = run_diag(cmd, debug).await {
let _ = log_error("diag", "Diag command failed", Some(&e.to_string())).await;
}
Ok(())
}
async fn handle_monitor(cmd: commands::MonitorCmd, _debug: bool) -> Result<(), String> {
use crate::commands::{run_single_check, start_monitor_daemon};
match cmd.command {
Some(commands::MonitorSubCommand::Check) => {
if let Err(e) = run_single_check().await {
let _ = log_error("monitor", "Monitor check failed", Some(&e.to_string())).await;
}
}
Some(commands::MonitorSubCommand::Start) => {
if let Err(e) = start_monitor_daemon().await {
let _ = log_error("monitor", "Monitor daemon failed", Some(&e.to_string())).await;
}
}
None => {
if let Err(e) = run_single_check().await {
let _ = log_error("monitor", "Monitor check failed", Some(&e.to_string())).await;
}
}
}
Ok(())
}
async fn handle_tail(cmd: commands::TailCmd, _debug: bool) -> Result<(), String> {
use crate::commands::{kafka_logs::LogConfig, start_log_shipping, tail_kafka_topic};
if cmd.kafka {
match LogConfig::from_xbp_config().await {
Ok(Some(config)) => {
if let Err(e) = tail_kafka_topic(&config).await {
let _ =
log_error("tail", "Failed to tail Kafka topic", Some(&e.to_string())).await;
}
}
Ok(None) => {
let _ = log_error("tail", "No log configuration found in xbp.json", None).await;
}
Err(e) => {
let _ =
log_error("tail", "Failed to load configuration", Some(&e.to_string())).await;
}
}
} else if cmd.ship {
if let Err(e) = start_log_shipping().await {
let _ = log_error("tail", "Failed to ship logs", Some(&e.to_string())).await;
}
} else {
if let Err(e) = start_log_shipping().await {
let _ = log_error("tail", "Failed to tail logs", Some(&e.to_string())).await;
}
}
Ok(())
}
async fn handle_no_command(port: Option<u16>, debug: bool) -> Result<(), String> {
if let Some(port) = port {
let args = vec!["-p".to_string(), port.to_string()];
if let Err(e) = run_ports(&args, debug).await {
let _ = log_error("ports", "Error running ports", Some(&e)).await;
}
return Ok(());
}
if is_xbp_project().await {
let current_dir = env::current_dir().unwrap_or_default();
if let Ok(mut profile) = Profile::load() {
if let Some(project_name) = current_dir
.file_name()
.and_then(|n| n.to_str())
.map(|s| s.to_string())
{
profile.update_last_project(current_dir.clone(), project_name);
let _ = profile.save();
}
}
println!("XBP Project Detected");
info!("{}", "XBP Project Detected".bright_green());
let project_types = detect_project_types(¤t_dir);
display_project_types(&project_types);
match check_nginx_mismatches().await {
Ok(mismatches) => {
if !mismatches.is_empty() {
let _ = log_warn(
"diag",
&format!(
"Found {} Nginx port mismatch(es). Run 'xbp diag' to fix.",
mismatches.len()
),
None,
)
.await;
}
}
Err(_) => {}
}
if let Err(e) = list_services(debug).await {
let _ = log_error("services", "Failed to list services", Some(&e)).await;
}
display_suggested_commands(&project_types);
println!("\nAvailable Commands:");
println!(" xbp services - List all services");
println!(" xbp service <cmd> <name> - Run command for a service");
println!(" xbp redeploy <name> - Redeploy a service");
println!(" xbp config - Show configuration");
println!(" xbp diag - Diagnose and fix configuration issues");
println!("For more help: xbp --help");
info!("\n{}", "Available Commands:".bright_blue());
info!(" {} - List all services", "xbp services".cyan());
info!(" {} - Run command for a service", "xbp service <cmd> <name>".cyan());
info!(" {} - Redeploy a service", "xbp redeploy <name>".cyan());
info!(" {} - Show configuration", "xbp config".cyan());
info!(" {} - Diagnose and fix configuration issues", "xbp diag".cyan());
info!("For more help: {}", "xbp --help".yellow());
} else {
handle_project_selection().await?;
}
Ok(())
}
async fn handle_project_selection() -> Result<(), String> {
use colored::Colorize;
let profile = Profile::load().unwrap_or_default();
if let Some(last_path) = &profile.last_project_path {
if last_path.exists() {
let project_name = last_path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("unknown");
println!("\n📌 Last XBP project:");
println!(" Name: {}", project_name);
println!(" Path: {}", last_path.display());
info!("\n{} {}", "📌".bright_blue(), "Last XBP project:".bright_blue().bold());
info!(" {} {}", "Name:".dimmed(), project_name.bright_cyan().bold());
info!(" {} {}", "Path:".dimmed(), last_path.display().to_string().bright_black());
let response: String = Input::new()
.with_prompt(format!("{} Continue with this project? (Y/n/a)", "→".bright_blue()))
.default("Y".to_string())
.interact_text()
.unwrap_or_else(|_| "n".to_string());
match response.to_lowercase().as_str() {
"y" | "" => {
info!("\n{} {} {}",
"✓".bright_green(),
"Switching to:".bright_green().bold(),
project_name.bright_cyan()
);
info!("\n{} {}", "💡".yellow(), "To navigate:".yellow());
info!(" {}", format!("cd {}", last_path.display()).bright_cyan().bold());
return Ok(());
}
"a" => {
return show_all_projects().await;
}
_ => {
print_help().await;
return Ok(());
}
}
}
}
println!("\n⚠️ Currently not in an XBP project");
println!(" → No xbp.json found in current directory or .xbp/xbp.json");
println!("\n💡 Options:");
println!(" • Run 'xbp setup' to initialize a new project");
println!(" • Run 'xbp' to see all available XBP projects");
info!("\n{} {}", "⚠️".yellow(), "Currently not in an XBP project".yellow().bold());
info!(" {} {}", "→".dimmed(), "No xbp.json found in current directory or .xbp/xbp.json".dimmed());
info!("\n{} {}", "💡".bright_blue(), "Options:".bright_blue().bold());
info!(" {} Run {} to initialize a new project", "•".bright_blue(), "xbp setup".bright_cyan());
info!(" {} Run {} to see all available XBP projects", "•".bright_blue(), "xbp".bright_cyan());
let response: String = Input::new()
.with_prompt(format!("{} Show all XBP projects? (y/N)", "→".bright_blue()))
.default("N".to_string())
.interact_text()
.unwrap_or_else(|_| "N".to_string());
if response.to_lowercase() == "y" {
show_all_projects().await?;
} else {
print_help().await;
}
Ok(())
}
async fn show_all_projects() -> Result<(), String> {
use colored::Colorize;
info!("\n{} {}", "🔍".bright_blue(), "Searching for XBP projects...".bright_blue().bold());
let projects = find_all_xbp_projects();
if projects.is_empty() {
info!("\n{} {}", "⚠️".yellow(), "No XBP projects found.".yellow().bold());
info!(" {} {}", "→".dimmed(), format!("Run {} to initialize a new project.", "xbp setup".bright_cyan()));
return Ok(());
}
let current_dir = env::current_dir().unwrap_or_default();
let ranked_projects = rank_projects_by_proximity(projects, current_dir.clone());
info!("\n{} {} {} {}",
"📦".bright_green(),
"XBP Projects".bright_green().bold(),
format!("({})", ranked_projects.len()).bright_black(),
if ranked_projects.len() > 1 { "found" } else { "found" }
);
info!("{}", "─".repeat(60).bright_black());
let items: Vec<String> = ranked_projects
.iter()
.enumerate()
.map(|(i, p)| {
let is_current = p.path == current_dir;
let number = format!("{:2}", i + 1).bright_black();
let name = if is_current {
format!("{} {}", p.name, "★".yellow())
} else {
p.name.clone()
};
let path_display = if p.path.starts_with(dirs::home_dir().unwrap_or_default()) {
p.path.strip_prefix(dirs::home_dir().unwrap_or_default())
.map(|p| format!("~/{}", p.display()))
.unwrap_or_else(|_| p.path.display().to_string())
} else {
p.path.display().to_string()
};
format!("{} {} {}",
number,
name.bright_cyan().bold(),
format!("({})", path_display).bright_black()
)
})
.collect();
if items.is_empty() {
return Ok(());
}
let selection = Select::new()
.with_prompt(format!("{} Select a project", "→".bright_blue()))
.items(&items)
.default(0)
.interact_opt()
.unwrap_or(None);
if let Some(index) = selection {
let selected_project = &ranked_projects[index];
info!("\n{} {} {}",
"✓".bright_green(),
"Selected:".bright_green().bold(),
selected_project.name.bright_cyan()
);
info!(" {} {}", "Path:".dimmed(), selected_project.path.display().to_string().bright_white());
info!("\n{} {}", "💡".yellow(), "To navigate to this project:".yellow());
info!(" {}", format!("cd {}", selected_project.path.display()).bright_cyan().bold());
}
Ok(())
}