use crate::config::global_xbp_paths;
use crate::logging::{get_prefix, log_info};
use crate::strategies::project_detector::DeploymentRecommendations;
use crate::strategies::{ProjectDetector, ProjectType, XbpConfig};
use crate::utils::{collapse_home_to_env, find_xbp_config_upwards};
use crate::utils::{open_path_with_editor, open_with_default_handler};
use std::env;
use std::fs;
use std::path::PathBuf;
use tracing::{debug, error, info};
pub async fn run_config(debug: bool) -> Result<(), 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);
let project_root = found
.as_ref()
.map(|f| f.project_root.clone())
.unwrap_or_else(|| current_dir.clone());
let xbp_yaml_path_dotfolder: PathBuf = project_root.join(".xbp/xbp.yaml");
let xbp_yml_path_dotfolder: PathBuf = project_root.join(".xbp/xbp.yml");
let xbp_json_path_dotfolder: PathBuf = project_root.join(".xbp/xbp.json");
let xbp_yaml_path_root: PathBuf = project_root.join("xbp.yaml");
let xbp_yml_path_root: PathBuf = project_root.join("xbp.yml");
let xbp_json_path_root: PathBuf = project_root.join("xbp.json");
if debug {
debug!("Current dir: {}", current_dir.display());
debug!("Project root: {}", project_root.display());
debug!("Checking for: {}", xbp_yaml_path_dotfolder.display());
debug!("Checking for: {}", xbp_yml_path_dotfolder.display());
debug!("Checking for: {}", xbp_json_path_dotfolder.display());
debug!("Checking for: {}", xbp_yaml_path_root.display());
debug!("Checking for: {}", xbp_yml_path_root.display());
debug!("Checking for: {}", xbp_json_path_root.display());
}
if !xbp_yaml_path_dotfolder.exists() && xbp_json_path_dotfolder.exists() {
if let Ok(contents) = fs::read_to_string(&xbp_json_path_dotfolder) {
if let Ok(mut cfg) = serde_json::from_str::<XbpConfig>(&contents) {
cfg.build_dir = collapse_home_to_env(&cfg.build_dir);
if let Some(services) = &mut 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(&cfg) {
let _ = fs::write(&xbp_yaml_path_dotfolder, yaml);
}
}
}
}
let (mut found_path, mut found_location, mut kind): (Option<PathBuf>, Option<String>, &str) =
if let Some(f) = &found {
(
Some(f.config_path.clone()),
Some(f.location.clone()),
f.kind,
)
} else if xbp_yaml_path_dotfolder.exists() {
(
Some(xbp_yaml_path_dotfolder.clone()),
Some(".xbp/xbp.yaml".to_string()),
"yaml",
)
} else if xbp_yml_path_dotfolder.exists() {
(
Some(xbp_yml_path_dotfolder.clone()),
Some(".xbp/xbp.yml".to_string()),
"yaml",
)
} else if xbp_json_path_dotfolder.exists() {
(
Some(xbp_json_path_dotfolder.clone()),
Some(".xbp/xbp.json".to_string()),
"json",
)
} else if xbp_yaml_path_root.exists() {
(
Some(xbp_yaml_path_root.clone()),
Some("xbp.yaml".to_string()),
"yaml",
)
} else if xbp_yml_path_root.exists() {
(
Some(xbp_yml_path_root.clone()),
Some("xbp.yml".to_string()),
"yaml",
)
} else if xbp_json_path_root.exists() {
(
Some(xbp_json_path_root.clone()),
Some("xbp.json".to_string()),
"json",
)
} else {
(None, None, "unknown")
};
if found_path.is_none() {
let detected: Option<ProjectType> = ProjectDetector::detect_project_type(¤t_dir)
.await
.ok();
let recommendations: Option<DeploymentRecommendations> = detected
.as_ref()
.map(ProjectDetector::get_deployment_recommendations);
let inferred_name: String = current_dir
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("app")
.to_string();
let default_port: u16 = recommendations
.as_ref()
.map(|r| r.default_port)
.unwrap_or(8080);
let target = detected.as_ref().map(|t| match t {
ProjectType::NextJs { .. } => "nextjs",
ProjectType::NodeJs { .. } => "expressjs",
ProjectType::Rust { .. } => "rust",
ProjectType::Python { .. } => "python",
ProjectType::DockerCompose { .. } => "docker-compose",
ProjectType::Docker { .. } => "docker",
ProjectType::Railway { .. } => "railway",
ProjectType::OpenApi { .. } => "openapi",
ProjectType::Terraform { .. } => "terraform",
ProjectType::Unknown => "unknown",
});
let baseline: XbpConfig = XbpConfig {
project_name: recommendations
.as_ref()
.and_then(|r| r.process_name.clone())
.unwrap_or(inferred_name),
version: "0.1.0".to_string(),
port: default_port,
build_dir: collapse_home_to_env(current_dir.to_string_lossy().as_ref()),
app_type: target.map(|s| s.to_string()),
build_command: recommendations
.as_ref()
.and_then(|r| r.build_command.clone()),
start_command: recommendations
.as_ref()
.and_then(|r| r.start_command.clone()),
install_command: recommendations
.as_ref()
.and_then(|r| r.install_command.clone()),
environment: None,
services: None,
systemd_service_name: None,
kafka_brokers: None,
kafka_topic: None,
kafka_public_url: None,
log_files: None,
monitor_url: None,
monitor_method: None,
monitor_expected_code: None,
monitor_interval: None,
database: None,
target: target.map(|s| s.to_string()),
branch: Some("main".to_string()),
crate_name: None,
npm_script: None,
port_storybook: None,
url: None,
url_storybook: None,
};
if let Err(e) = fs::create_dir_all(current_dir.join(".xbp")) {
let msg: String = format!("Failed to create .xbp directory: {}", e);
error!("{}", msg);
eprintln!("{}", msg);
} else {
let out_json = current_dir.join(".xbp/xbp.json");
let out_yaml = current_dir.join(".xbp/xbp.yaml");
let wrote_yaml = if let Ok(yaml) = serde_yaml::to_string(&baseline) {
fs::write(&out_yaml, yaml).is_ok()
} else {
false
};
let wrote_json = if let Ok(content) = serde_json::to_string_pretty(&baseline) {
fs::write(&out_json, content).is_ok()
} else {
false
};
if wrote_yaml {
found_path = Some(out_yaml);
found_location = Some(".xbp/xbp.yaml".to_string());
kind = "yaml";
println!("Generated .xbp/xbp.yaml from detected manifests.");
} else if wrote_json {
found_path = Some(out_json);
found_location = Some(".xbp/xbp.json".to_string());
kind = "json";
println!("Generated .xbp/xbp.json from detected manifests.");
}
}
}
if let (Some(path), Some(location)) = (found_path, found_location) {
let _ = log_info(
"config",
&format!("Found config at: {}", path.display()),
None,
)
.await;
println!("Found config at: {}", path.display());
match fs::read_to_string(&path) {
Ok(contents) => {
if debug {
debug!("config contents: {}", contents);
}
let data = if kind == "yaml" {
serde_yaml::from_str::<serde_yaml::Value>(&contents)
.ok()
.and_then(|v| serde_json::to_value(v).ok())
} else {
serde_json::from_str::<serde_json::Value>(&contents).ok()
};
if let Some(json_data) = data {
let prefix = get_prefix();
println!("\nConfiguration:");
println!("{}", "─".repeat(50));
for (key, value) in json_data.as_object().unwrap() {
let value_str: String = value.to_string().replace("\"", "");
println!("{:<15} | {}", key, value_str);
info!("{}{:<15} | {}", prefix, key, value_str);
}
println!("{}", "─".repeat(50));
} else {
let msg = format!("Failed to parse {} contents.", location);
error!("{}", msg);
eprintln!("{}", msg);
}
}
Err(e) => {
let msg = format!("Failed to read {}: {}", location, e);
error!("{}", msg);
eprintln!("{}", msg);
}
}
} else {
let msg =
"No .xbp/xbp.yaml|.yml|.json or xbp.yaml|.yml|.json found in the current directory.";
error!("{}", msg);
eprintln!("{}", msg);
}
Ok(())
}
pub async fn open_global_config(no_open: bool) -> Result<(), String> {
let paths = global_xbp_paths()?;
println!("Global XBP directory: {}", paths.root_dir.display());
println!("Config file: {}", paths.config_file.display());
println!("SSH directory: {}", paths.ssh_dir.display());
println!("Cache directory: {}", paths.cache_dir.display());
println!("Logs directory: {}", paths.logs_dir.display());
if no_open {
return Ok(());
}
let _ = open_with_default_handler(&paths.root_dir.display().to_string());
let _ = open_path_with_editor(&paths.config_file);
Ok(())
}