use crate::cli::error::{CliError, CliResult};
use crate::commands::diag::check_nginx_mismatches;
use crate::commands::run_config;
use crate::commands::service::{is_xbp_project, list_services};
use crate::logging::{log_error, log_warn};
use crate::profile::{find_all_xbp_projects, rank_projects_by_proximity, Profile, ProjectInfo};
use crate::project_detector::ProjectType;
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};
pub async fn handle_no_command(port: Option<u16>, debug: bool) -> CliResult<()> {
if let Some(port) = port {
let args = vec!["-p".to_string(), port.to_string()];
if let Err(e) = crate::commands::ports::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: std::path::PathBuf = 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".bright_green());
if let Err(e) = run_config(debug).await {
let _ = log_warn(
"config",
"Unable to display project config summary",
Some(&e),
)
.await;
}
let project_types: Vec<ProjectType> = detect_project_types(¤t_dir);
display_project_types(&project_types);
if let Ok(mismatches) = check_nginx_mismatches().await {
if !mismatches.is_empty() {
let _ = log_warn(
"diag",
&format!(
"Found {} Nginx port mismatch(es). Run 'xbp diag' to fix.",
mismatches.len()
),
None,
)
.await;
}
}
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!("\n{}", "Available Commands:".bright_blue());
println!("{} - List all services", "xbp services".bright_magenta());
println!(
"{} - Run command for a service",
"xbp service <cmd> <name>".bright_magenta()
);
println!(
"{} - Redeploy a service",
"xbp redeploy <name>".bright_magenta()
);
println!("{} - Show configuration", "xbp config".bright_magenta());
println!(
"{} - Save the PM2 process snapshot",
"xbp snapshot".bright_magenta()
);
println!(
"{} - Inspect and sync versions",
"xbp version".bright_magenta()
);
println!(
"{} - Diagnose and fix configuration issues",
"xbp diag".bright_magenta()
);
println!("For more help: {}", "xbp --help".yellow());
} else {
handle_project_selection().await?;
}
Ok(())
}
pub async fn handle_project_selection() -> CliResult<()> {
let profile: Profile = Profile::load().unwrap_or_default();
if let Some(last_path) = &profile.last_project_path {
if last_path.exists() {
let project_name: &str = last_path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("unknown");
println!("\nLast XBP project:");
println!(" Name: {}", project_name.purple());
println!(" Path: {}", last_path.display());
println!(
"\n{} {}",
"📌".bright_blue(),
"Last XBP project:".bright_blue().bold()
);
println!(
" {} {}",
"Name:".dimmed(),
project_name.bright_cyan().bold()
);
println!(
" {} {}",
"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" | "" => {
println!(
"\n{} {} {}",
"✓".bright_green(),
"Switching to:".bright_green().bold(),
project_name.bright_cyan()
);
println!("\n{} {}", "💡".yellow(), "To navigate:".yellow());
println!(
" {}",
format!("cd {}", last_path.display()).bright_cyan().bold()
);
return Ok(());
}
"a" => {
return show_all_projects().await;
}
_ => {
crate::commands::help::print_help().await;
return Ok(());
}
}
}
}
println!(
"\n{} {}",
"⚠️".yellow(),
"Currently not in an XBP project".yellow().bold()
);
println!(
" {} {}",
"→".dimmed(),
"No xbp.json found in current directory or .xbp/xbp.json".dimmed()
);
println!("\n{}", "Options:".bright_blue().bold());
println!(
" {} Run {} to initialize a new project",
"•".bright_blue(),
"xbp setup".bright_cyan()
);
println!(
" {} 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 {
crate::commands::help::print_help().await;
}
Ok(())
}
pub async fn show_all_projects() -> CliResult<()> {
println!(
"\n{} {}",
"🔍".bright_blue(),
"Searching for XBP projects...".bright_blue().bold()
);
let projects: Vec<ProjectInfo> = find_all_xbp_projects();
if projects.is_empty() {
println!("\nNo XBP projects found.");
println!("Run 'xbp setup' to initialize a new project.");
println!(
"\n{} {}",
"⚠️".yellow(),
"No XBP projects found.".yellow().bold()
);
println!(
" {} Run {} to initialize a new project.",
"→".dimmed(),
"xbp setup".bright_cyan()
);
return Ok(());
}
let current_dir: std::path::PathBuf = env::current_dir().unwrap_or_default();
let ranked_projects: Vec<ProjectInfo> =
rank_projects_by_proximity(projects, current_dir.clone());
println!("\nXBP Projects ({} found):", ranked_projects.len());
println!(
"\n{} {} {} found",
"📦".bright_green(),
"XBP Projects".bright_green().bold(),
format!("({})", ranked_projects.len()).bright_black()
);
println!("{}", "─".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: String = 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]) -> CliResult<()> {
if projects.is_empty() {
return Ok(());
}
let mut index: usize = 0;
let mut out: std::io::Stdout = stdout();
enable_raw_mode().map_err(|e| CliError::from(e.to_string()))?;
loop {
out.execute(Clear(ClearType::CurrentLine))
.map_err(|e| CliError::from(e.to_string()))?;
out.execute(MoveToColumn(0))
.map_err(|e| CliError::from(e.to_string()))?;
for (i, project) in projects.iter().enumerate() {
if i == index {
out.execute(SetForegroundColor(Color::Green))
.map_err(|e| CliError::from(e.to_string()))?;
write!(out, " [{}] ", project.name).map_err(|e| CliError::from(e.to_string()))?;
out.execute(ResetColor)
.map_err(|e| CliError::from(e.to_string()))?;
} else {
write!(out, " {} ", project.name).map_err(|e| CliError::from(e.to_string()))?;
}
}
out.flush().map_err(|e| CliError::from(e.to_string()))?;
if let Event::Key(key) = event::read().map_err(|e| CliError::from(e.to_string()))? {
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();
let selected: &ProjectInfo = &projects[index];
println!("\nSelected: {}", selected.name);
println!("Path: {}", selected.path.display());
println!("To navigate: cd {}", selected.path.display());
println!(
"\n{} {} {}",
"✓".bright_green(),
"Selected:".bright_green().bold(),
selected.name.bright_cyan()
);
println!(
" {} {}",
"Path:".dimmed(),
selected.path.display().to_string().bright_white()
);
println!(
"\n{} {}",
"💡".yellow(),
"To navigate to this project:".yellow()
);
println!(
" {}",
format!("cd {}", selected.path.display())
.bright_cyan()
.bold()
);
return Ok(());
}
KeyCode::Esc => {
disable_raw_mode().ok();
println!();
return Ok(());
}
_ => {}
}
}
}
}