use std::env;
use std::io::{IsTerminal, Write};
use std::path::{Path, PathBuf};
use std::process::Stdio;
use std::time::Instant;
use chrono::Local;
use colored::Colorize;
use dialoguer::{theme::ColorfulTheme, FuzzySelect, Select};
use serde::{Deserialize, Serialize};
use tokio::process::Command;
use tracing::info;
use crate::cli::ui;
use crate::commands::pm2::pm2_start_in_dir;
use crate::commands::terminal_table::{render_table, TableStyle};
use crate::config::global_xbp_paths;
use crate::logging::{get_prefix, log_debug, log_info, log_success};
use crate::strategies::{
get_all_services, get_service_by_name, resolve_config_paths_for_runtime, ServiceConfig,
XbpConfig,
};
use crate::utils::{
find_xbp_config_upwards, maybe_auto_convert_legacy_xbp_json_to_yaml, parse_config_with_auto_heal,
resolve_env_placeholders, resolve_service_root,
};
pub async fn list_services(_debug: bool) -> Result<(), String> {
let (project_root, config): (PathBuf, XbpConfig) = load_xbp_config_with_root().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!();
println!(
"{}",
format!("Project: {}", config.project_name)
.bright_cyan()
.bold()
);
println!("{}", "Configured services:".bright_blue().bold());
let rows: Vec<Vec<String>> = services
.iter()
.map(|service| {
let url = service.url.as_deref().unwrap_or("-").to_string();
let avail = available_commands_for_service(service).join(" ");
let commands = if avail.trim().is_empty() {
"-".to_string()
} else {
avail
};
vec![
service.name.clone().bright_white().bold().to_string(),
service.target.clone().bright_yellow().to_string(),
service.port.to_string(),
if commands == "-" {
commands.dimmed().to_string()
} else {
commands.bright_green().to_string()
},
url,
]
})
.collect();
print!(
"{}",
render_table(
&["Name", "Target", "Port", "Available Commands", "URL"],
&rows,
TableStyle::Pipe,
"",
)
);
println!("Total: {} service(s)", services.len());
println!();
ui::tip("Run `xbp service` (no args) for interactive picker, or `xbp service <cmd> <name>`.");
let _ = update_services_registry(&services, &project_root, Some(&config.project_name)).await;
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 start_time = Instant::now();
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 {
resolve_service_root(&project_root, service.root_directory.as_deref())
};
let project_name = config.project_name.clone();
let working_dir_str = working_dir.display().to_string();
let _ = log_info(
"service",
&format!("Running '{}' for service '{}'", command, service_name),
Some(&format!("Working directory: {}", working_dir.display())),
)
.await;
let exec_result: Result<(), String> = (async {
let cmd_str = service
.commands
.as_ref()
.and_then(|commands| commands.get(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(
&project_root,
config.environment.as_ref(),
service.environment.as_ref(),
);
pm2_start_in_dir(
&service.name,
&pm2_command,
&working_dir,
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(
&project_root,
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(())
})
.await;
let duration_ms = start_time.elapsed().as_millis() as u64;
let success = exec_result.is_ok();
let exit_code = if success { Some(0) } else { None };
let _ = record_service_run(ServiceRunRecord {
timestamp: Local::now().to_rfc3339(),
project_root: project_root.to_string_lossy().to_string(),
project_name: Some(project_name),
service: service_name.to_string(),
command: command.to_string(),
working_dir: working_dir_str,
success,
duration_ms,
exit_code,
});
exec_result
}
async fn run_command_in_dir(
dir: &PathBuf,
command: &str,
envs: Option<&std::collections::HashMap<String, String>>,
debug: bool,
) -> Result<(), String> {
let (exit_code, combined_output) =
run_shell_command_streaming(dir, command, envs, debug).await?;
if exit_code == 0 {
return Ok(());
}
if !is_pnpm_virtual_store_mismatch(&combined_output) {
return Err(format!("Command failed with exit code: {exit_code}"));
}
let install_dir = find_pnpm_workspace_root(dir).unwrap_or_else(|| dir.to_path_buf());
let _ = log_info(
"service",
"Detected pnpm virtual-store layout mismatch; running `pnpm install` to recreate node_modules",
Some(&format!("Workspace: {}", install_dir.display())),
)
.await;
eprintln!(
"{} pnpm virtual-store mismatch — running `pnpm install` in {} then retrying",
get_prefix(),
install_dir.display()
);
let (install_code, install_output) =
run_shell_command_streaming(&install_dir, "pnpm install", envs, debug).await?;
if install_code != 0 {
return Err(format!(
"Command failed with exit code: {exit_code}. Auto-heal `pnpm install` also failed with exit code: {install_code}.\n{install_output}"
));
}
let _ = log_info(
"service",
"pnpm install finished; retrying original service command",
Some(command),
)
.await;
let (retry_code, _) = run_shell_command_streaming(dir, command, envs, debug).await?;
if retry_code == 0 {
return Ok(());
}
Err(format!(
"Command failed with exit code: {retry_code} after pnpm install auto-heal"
))
}
fn is_pnpm_virtual_store_mismatch(output: &str) -> bool {
let lower = output.to_ascii_lowercase();
lower.contains("err_pnpm_virtual_store_dir_max_length_diff")
|| (lower.contains("virtual-store-dir-max-length")
&& (lower.contains("recreate the modules directory")
|| lower.contains("run \"pnpm install\"")
|| lower.contains("run 'pnpm install'")
|| lower.contains("run `pnpm install`")
|| lower.contains("pnpm install")))
}
fn find_pnpm_workspace_root(start: &Path) -> Option<PathBuf> {
let mut current = if start.is_file() {
start.parent().map(Path::to_path_buf)?
} else {
start.to_path_buf()
};
let mut package_with_lock: Option<PathBuf> = None;
loop {
if current.join("pnpm-workspace.yaml").is_file()
|| current.join("pnpm-workspace.yml").is_file()
{
return Some(current);
}
if package_with_lock.is_none()
&& current.join("package.json").is_file()
&& (current.join("pnpm-lock.yaml").is_file() || current.join("pnpm-lock.yml").is_file())
{
package_with_lock = Some(current.clone());
}
if !current.pop() {
break;
}
}
package_with_lock
}
async fn run_shell_command_streaming(
dir: &Path,
command: &str,
envs: Option<&std::collections::HashMap<String, String>>,
_debug: bool,
) -> Result<(i32, String), 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").arg(command);
}
#[cfg(windows)]
let mut cmd = Command::new("cmd");
#[cfg(windows)]
{
cmd.arg("/C").arg(command);
}
cmd.current_dir(dir);
if let Some(envs) = envs {
cmd.envs(envs);
}
cmd.stdout(Stdio::piped());
cmd.stderr(Stdio::piped());
let mut child = cmd
.spawn()
.map_err(|e| format!("Failed to execute command: {e}"))?;
let stdout = child
.stdout
.take()
.ok_or_else(|| "Failed to capture command stdout".to_string())?;
let stderr = child
.stderr
.take()
.ok_or_else(|| "Failed to capture command stderr".to_string())?;
let stdout_task = tokio::spawn(async move {
use tokio::io::{AsyncBufReadExt, BufReader};
let mut lines = BufReader::new(stdout).lines();
let mut buffer = String::new();
while let Ok(Some(line)) = lines.next_line().await {
println!("{line}");
buffer.push_str(&line);
buffer.push('\n');
}
buffer
});
let stderr_task = tokio::spawn(async move {
use tokio::io::{AsyncBufReadExt, BufReader};
let mut lines = BufReader::new(stderr).lines();
let mut buffer = String::new();
while let Ok(Some(line)) = lines.next_line().await {
eprintln!("{line}");
buffer.push_str(&line);
buffer.push('\n');
}
buffer
});
let status = child
.wait()
.await
.map_err(|e| format!("Failed to wait for command: {e}"))?;
let stdout_text = stdout_task
.await
.map_err(|e| format!("Failed to read stdout: {e}"))?;
let stderr_text = stderr_task
.await
.map_err(|e| format!("Failed to read stderr: {e}"))?;
let mut combined = stdout_text;
combined.push_str(&stderr_text);
Ok((status.code().unwrap_or(-1), combined))
}
fn merge_envs(
project_root: &std::path::Path,
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(resolve_env_placeholders(project_root, &out))
}
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",
"No xbp.yaml/xbp.yml/xbp.json found in current directory or .xbp/",
"Run 'xbp' to select a project or 'xbp setup' to initialize a new project."
)
})?;
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);
}
resolve_config_paths_for_runtime(&mut config, &found.project_root);
if found.kind == "json" {
let _ = maybe_auto_convert_legacy_xbp_json_to_yaml(&found.project_root, &found.config_path);
}
crate::data::athena::persist_project_snapshot(&found.project_root, &config, Some(found.kind))
.await;
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, "");
for command in commands.available_names() {
info!("{} Service {} {}", prefix, command, service_name);
}
}
info!("{}", prefix);
info!("{} Redeploy:", prefix);
info!("{} Redeploy {}", prefix, service_name);
Ok(())
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ServiceRunRecord {
pub timestamp: String,
pub project_root: String,
pub project_name: Option<String>,
pub service: String,
pub command: String,
pub working_dir: String,
pub success: bool,
pub duration_ms: u64,
pub exit_code: Option<i32>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ServicesRegistry {
pub last_updated: String,
pub services: Vec<RegisteredService>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RegisteredService {
pub name: String,
pub project_path: String,
pub project_name: Option<String>,
pub target: String,
pub port: u16,
pub available_commands: Vec<String>,
pub last_seen: String,
}
fn global_service_dir() -> Result<PathBuf, String> {
let paths = global_xbp_paths()?;
let dir = paths.logs_dir.join("service");
std::fs::create_dir_all(&dir)
.map_err(|e| format!("Failed to create service logs dir: {}", e))?;
Ok(dir)
}
pub fn service_runs_path() -> Result<PathBuf, String> {
Ok(global_service_dir()?.join("runs.jsonl"))
}
pub fn service_registry_path() -> Result<PathBuf, String> {
Ok(global_service_dir()?.join("registry.json"))
}
pub fn record_service_run(record: ServiceRunRecord) -> Result<(), String> {
let path = service_runs_path()?;
let mut file = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(&path)
.map_err(|e| format!("Failed to open runs log {}: {}", path.display(), e))?;
let line = serde_json::to_string(&record).map_err(|e| e.to_string())?;
writeln!(file, "{}", line).map_err(|e| e.to_string())?;
Ok(())
}
pub fn load_services_registry() -> Result<ServicesRegistry, String> {
let path = service_registry_path()?;
if !path.exists() {
return Ok(ServicesRegistry::default());
}
let content =
std::fs::read_to_string(&path).map_err(|e| format!("Failed to read registry: {}", e))?;
let reg: ServicesRegistry =
serde_json::from_str(&content).map_err(|e| format!("Failed to parse registry: {}", e))?;
Ok(reg)
}
pub async fn update_services_registry(
services: &[ServiceConfig],
project_root: &Path,
project_name: Option<&str>,
) -> Result<(), String> {
let mut reg = load_services_registry().unwrap_or_default();
let now = Local::now().to_rfc3339();
let root_str = project_root.to_string_lossy().to_string();
for svc in services {
let avail = available_commands_for_service(svc);
let reg_svc = RegisteredService {
name: svc.name.clone(),
project_path: root_str.clone(),
project_name: project_name.map(|s| s.to_string()),
target: svc.target.clone(),
port: svc.port,
available_commands: avail,
last_seen: now.clone(),
};
if let Some(existing) = reg
.services
.iter_mut()
.find(|r| r.project_path == root_str && r.name == svc.name)
{
*existing = reg_svc;
} else {
reg.services.push(reg_svc);
}
}
reg.last_updated = now;
let path = service_registry_path()?;
let json = serde_json::to_string_pretty(®).map_err(|e| e.to_string())?;
std::fs::write(&path, json).map_err(|e| format!("Failed to write registry: {}", e))?;
Ok(())
}
pub fn available_commands_for_service(s: &ServiceConfig) -> Vec<String> {
s.commands
.as_ref()
.map(|commands| commands.available_names())
.unwrap_or_default()
}
pub async fn print_service_preview() -> Result<(), String> {
let (project_root, config): (PathBuf, XbpConfig) = load_xbp_config_with_root().await?;
let services = get_all_services(&config);
println!();
println!(
"{} {}",
"◆".bright_blue().bold(),
format!("xbp service — {}", config.project_name)
.bright_cyan()
.bold()
);
ui::divider(72);
if services.is_empty() {
println!("No services configured in this project.");
return Ok(());
}
for svc in &services {
let avail = available_commands_for_service(svc);
let cmds_disp = if avail.is_empty() {
"no commands".dimmed().to_string()
} else {
avail.join(", ").bright_green().to_string()
};
let root_note = svc
.root_directory
.as_deref()
.map(|r| format!(" root={}", r))
.unwrap_or_default();
println!(
" {} {} {} port {} {} {}",
"•".bright_magenta(),
svc.name.bright_white().bold(),
format!("[{}]", svc.target).bright_yellow(),
svc.port.to_string().bright_cyan(),
cmds_disp,
root_note.dimmed()
);
}
ui::divider(72);
println!(
"{} {}",
"Hint:".bright_yellow().bold(),
"Pick interactively below, or use `xbp service <command> <name>`".dimmed()
);
let _ = update_services_registry(&services, &project_root, Some(&config.project_name)).await;
Ok(())
}
fn is_interactive_terminal() -> bool {
std::io::stdin().is_terminal()
&& std::io::stdout().is_terminal()
&& std::env::var_os("XBP_NON_INTERACTIVE").is_none()
}
pub async fn run_service_interactive(debug: bool) -> Result<(), String> {
if let Err(_e) = print_service_preview().await {
println!();
println!("{}", "Not inside an XBP project directory.".bright_yellow());
println!(
"{} {}",
"Tip:".bright_cyan(),
"cd into a project with xbp.yaml (or .xbp/xbp.yaml) and try again.".dimmed()
);
if let Ok(reg) = load_services_registry() {
if !reg.services.is_empty() {
println!();
println!("{}", "Recently known services (from registry):".dimmed());
for rs in reg.services.iter().take(8) {
let proj = rs.project_name.as_deref().unwrap_or("?");
println!(
" {} {} ({}) cmds: {}",
"•".dimmed(),
rs.name.bright_white(),
proj,
rs.available_commands.join(", ").dimmed()
);
}
println!();
println!(
"{}",
format!(
"Registry: {}",
service_registry_path().unwrap_or_default().display()
)
.dimmed()
);
}
}
return Ok(());
}
if !is_interactive_terminal() {
ui::tip("Non-interactive terminal detected. Use `xbp service <cmd> <name>` explicitly.");
return Ok(());
}
let (_project_root, config) = load_xbp_config_with_root().await?;
let services = get_all_services(&config);
if services.is_empty() {
return Ok(());
}
let service_labels: Vec<String> = services
.iter()
.map(|s| {
let cmds = available_commands_for_service(s);
let cmd_hint = if cmds.is_empty() {
"".to_string()
} else {
format!(" — {}", cmds.join(", "))
};
format!("{} ({}) port {} {}", s.name, s.target, s.port, cmd_hint)
})
.collect();
let service_idx = match FuzzySelect::with_theme(&ColorfulTheme::default())
.with_prompt("Select service to operate on")
.items(&service_labels)
.default(0)
.interact_opt()
{
Ok(Some(i)) => i,
_ => {
println!("{}", "Cancelled.".dimmed());
return Ok(());
}
};
let chosen = &services[service_idx];
let avail_cmds = available_commands_for_service(chosen);
if avail_cmds.is_empty() {
println!(
"Service '{}' has no runnable commands configured.",
chosen.name
);
return Ok(());
}
let cmd_idx = match Select::with_theme(&ColorfulTheme::default())
.with_prompt(format!("Select command for '{}'", chosen.name))
.items(&avail_cmds)
.default(0)
.interact_opt()
{
Ok(Some(i)) => i,
_ => {
println!("{}", "Cancelled.".dimmed());
return Ok(());
}
};
let chosen_cmd = &avail_cmds[cmd_idx];
println!();
println!(
"{} Running {} {} {}",
"▶".bright_green().bold(),
"xbp service".bright_magenta(),
chosen_cmd.bright_cyan().bold(),
chosen.name.bright_white().bold()
);
ui::divider(40);
run_service_command(chosen_cmd, &chosen.name, debug).await
}
pub async fn pick_service_for_command(command: &str, debug: bool) -> Result<(), String> {
let (_project_root, config) = load_xbp_config_with_root().await?;
let all_services = get_all_services(&config);
let candidates: Vec<&ServiceConfig> = all_services
.iter()
.filter(|s| {
available_commands_for_service(s)
.iter()
.any(|c| c == command)
})
.collect();
if candidates.is_empty() {
return Err(format!(
"No services have a '{}' command configured. Check `xbp services`.",
command
));
}
if candidates.len() == 1 {
let only = candidates[0];
println!(
"Only one service supports '{}': {} — running it.",
command, only.name
);
return run_service_command(command, &only.name, debug).await;
}
if !is_interactive_terminal() {
let names: Vec<_> = candidates.iter().map(|s| s.name.as_str()).collect();
return Err(format!(
"Multiple services support '{}'. Specify one of: {}",
command,
names.join(", ")
));
}
let labels: Vec<String> = candidates
.iter()
.map(|s| format!("{} (port {})", s.name, s.port))
.collect();
let idx = FuzzySelect::with_theme(&ColorfulTheme::default())
.with_prompt(format!("Select service for '{}'", command))
.items(&labels)
.default(0)
.interact()
.map_err(|e| format!("Selection failed: {}", e))?;
let chosen = candidates[idx];
run_service_command(command, &chosen.name, debug).await
}
#[cfg(test)]
mod tests {
use super::{find_pnpm_workspace_root, is_pnpm_virtual_store_mismatch};
use crate::commands::terminal_table::{render_table, TableStyle};
use std::fs;
use std::time::{SystemTime, UNIX_EPOCH};
#[test]
fn service_table_renderer_pads_colored_cells() {
let rendered = render_table(
&["Name", "Port"],
&[vec![
"\u{1b}[1;37mathena\u{1b}[0m".to_string(),
"3000".to_string(),
]],
TableStyle::Box,
"",
);
assert!(rendered.contains("athena"));
assert!(rendered.contains("3000"));
assert!(rendered.contains("┌"));
assert!(rendered.contains("│"));
}
#[test]
fn detects_pnpm_virtual_store_max_length_diff() {
let sample = r#"[ERR_PNPM_VIRTUAL_STORE_DIR_MAX_LENGTH_DIFF] This modules directory was created using a different virtual-store-dir-max-length value. Run "pnpm install" to recreate the modules directory."#;
assert!(is_pnpm_virtual_store_mismatch(sample));
assert!(!is_pnpm_virtual_store_mismatch("tsc: error TS2307"));
}
#[test]
fn finds_workspace_root_from_nested_package() {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos();
let root = std::env::temp_dir().join(format!("xbp-pnpm-ws-{nanos}"));
let nested = root.join("apps").join("api");
fs::create_dir_all(&nested).unwrap();
fs::write(root.join("pnpm-workspace.yaml"), "packages:\n - apps/*\n").unwrap();
fs::write(root.join("package.json"), r#"{"name":"root"}"#).unwrap();
fs::write(nested.join("package.json"), r#"{"name":"api"}"#).unwrap();
let found = find_pnpm_workspace_root(&nested).expect("workspace root");
assert_eq!(found, root);
let _ = fs::remove_dir_all(&root);
}
}