pub mod commands;
use clap::Parser;
use commands::{Cli, Commands};
use crate::commands::curl;
use crate::commands::diag::check_nginx_mismatches;
use crate::commands::pm2_save;
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, ProjectInfo};
use crate::project_detector::{
detect_project_types, display_project_types, display_suggested_commands,
};
use colored::Colorize;
use crossterm::{
cursor::MoveToColumn,
event::{self, Event, KeyCode},
style::{Color, ResetColor, SetForegroundColor},
terminal::{disable_raw_mode, enable_raw_mode, Clear, ClearType},
ExecutableCommand,
};
use dialoguer::Input;
use std::env;
use std::io::{stdout, Write};
use tracing::info;
#[cfg(feature = "monitoring")]
use xbp_monitoring::{list_items, run_all_once, serve, RunKind};
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,
#[cfg(feature = "monitoring")]
Some(Commands::Monitoring(cmd)) => handle_monitoring(cmd).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);
}
if let Err(e) = pm2_save(debug).await {
let _ = log_error("start", "pm2 save failed", Some(&e)).await;
return Err(e);
}
if let Err(e) = pm2_list(debug).await {
let _ = log_error("start", "pm2 list failed", 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(())
}
#[cfg(feature = "monitoring")]
async fn handle_monitoring(cmd: commands::MonitoringCmd) -> Result<(), String> {
match cmd.command {
commands::MonitoringSubCommand::Serve { file } => {
if let Err(err) = serve(&file).await {
return Err(format!("Failed to start monitoring service: {}", err));
}
}
commands::MonitoringSubCommand::RunOnce {
file,
probes_only,
stories_only,
} => {
let results = run_all_once(&file, probes_only, stories_only)
.await
.map_err(|e| e.to_string())?;
if results.is_empty() {
info!("No probes or stories configured");
} else {
for result in results {
let label = match result.kind {
RunKind::Probe => "probe",
RunKind::Story => "story",
};
if result.success {
info!("{} {} ✓", label, result.name);
} else {
let code = result
.status_code
.map(|c| c.to_string())
.unwrap_or_else(|| "-".into());
let err = result.error.unwrap_or_else(|| "Unknown error".into());
info!("{} {} ✗ (code {}, {})", label, result.name, code, err);
}
}
}
}
commands::MonitoringSubCommand::List { file } => {
let (probes, stories) = list_items(&file).await.map_err(|e| e.to_string())?;
info!(
"Probes: {}",
if probes.is_empty() {
"none".into()
} else {
probes.join(", ")
}
);
info!(
"Stories: {}",
if stories.is_empty() {
"none".into()
} else {
stories.join(", ")
}
);
}
}
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;
println!("\nSearching for XBP projects...");
info!(
"\n{} {}",
"🔍".bright_blue(),
"Searching for XBP projects...".bright_blue().bold()
);
let projects = find_all_xbp_projects();
if projects.is_empty() {
println!("\nNo XBP projects found.");
println!("Run 'xbp setup' to initialize a new project.");
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());
println!("\nXBP Projects ({} found):", ranked_projects.len());
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()
};
let line = format!(
"{} {} {}",
number,
name.bright_cyan().bold(),
format!("({})", path_display).bright_black()
);
println!("{}", line);
line
})
.collect();
if items.is_empty() {
return Ok(());
}
println!("\nUse ← and → to select a project, Enter to confirm, Esc to cancel.\n");
interactive_project_selector(&ranked_projects)?;
Ok(())
}
fn interactive_project_selector(projects: &[ProjectInfo]) -> Result<(), String> {
if projects.is_empty() {
return Ok(());
}
let mut index: usize = 0;
let mut out = stdout();
enable_raw_mode().map_err(|e| e.to_string())?;
loop {
out.execute(Clear(ClearType::CurrentLine))
.map_err(|e| e.to_string())?;
out.execute(MoveToColumn(0)).map_err(|e| e.to_string())?;
for (i, project) in projects.iter().enumerate() {
if i == index {
out.execute(SetForegroundColor(Color::Green))
.map_err(|e| e.to_string())?;
write!(out, " [{}] ", project.name).map_err(|e| e.to_string())?;
out.execute(ResetColor).map_err(|e| e.to_string())?;
} else {
write!(out, " {} ", project.name).map_err(|e| e.to_string())?;
}
}
out.flush().map_err(|e| e.to_string())?;
match event::read().map_err(|e| e.to_string())? {
Event::Key(key) => match key.code {
KeyCode::Left => {
if index == 0 {
index = projects.len().saturating_sub(1);
} else {
index -= 1;
}
}
KeyCode::Right => {
index = (index + 1) % projects.len();
}
KeyCode::Enter => {
disable_raw_mode().ok();
println!();
let selected = &projects[index];
println!("\nSelected: {}", selected.name);
println!("Path: {}", selected.path.display());
println!("To navigate: cd {}", selected.path.display());
info!(
"\n{} {} {}",
"✓".bright_green(),
"Selected:".bright_green().bold(),
selected.name.bright_cyan()
);
info!(
" {} {}",
"Path:".dimmed(),
selected.path.display().to_string().bright_white()
);
info!(
"\n{} {}",
"💡".yellow(),
"To navigate to this project:".yellow()
);
info!(
" {}",
format!("cd {}", selected.path.display())
.bright_cyan()
.bold()
);
return Ok(());
}
KeyCode::Esc => {
disable_raw_mode().ok();
println!();
return Ok(());
}
_ => {}
},
_ => {}
}
}
}