use crate::config::{get_biovault_home, is_syftbox_env, Config};
use crate::Result;
use dialoguer::{theme::ColorfulTheme, Confirm, Input, Select};
use std::env;
use std::fs;
use std::io::IsTerminal;
use std::path::PathBuf;
use tracing::info;
pub async fn execute(email: Option<&str>, quiet: bool) -> Result<()> {
let default_biovault_dir = get_biovault_home()?;
let config_file = default_biovault_dir.join("config.yaml");
let is_existing_installation = config_file.exists();
let biovault_dir = if is_existing_installation {
default_biovault_dir
} else if env::var("BIOVAULT_HOME").is_ok() {
default_biovault_dir
} else if env::var("SYFTBOX_DATA_DIR").is_ok() {
default_biovault_dir
} else if !quiet && std::io::stdin().is_terminal() {
prompt_for_location()?
} else {
default_biovault_dir
};
let email = if let Some(email) = email {
email.to_string()
} else if let Ok(syftbox_email) = env::var("SYFTBOX_EMAIL") {
if quiet {
syftbox_email
} else if std::io::stdin().is_terminal() {
println!("Detected SyftBox environment email: {}", syftbox_email);
let use_syftbox = Confirm::with_theme(&ColorfulTheme::default())
.with_prompt("Use this email for BioVault?")
.default(true)
.interact()
.map_err(|e| anyhow::anyhow!("Prompt error: {}", e))?;
if use_syftbox {
syftbox_email
} else {
Input::with_theme(&ColorfulTheme::default())
.with_prompt("Enter email address")
.interact_text()
.map_err(|e| anyhow::anyhow!("Prompt error: {}", e))?
}
} else {
syftbox_email
}
} else if std::io::stdin().is_terminal() && !quiet {
Input::with_theme(&ColorfulTheme::default())
.with_prompt("Enter email address")
.interact_text()
.map_err(|e| anyhow::anyhow!("Prompt error: {}", e))?
} else {
return Err(anyhow::anyhow!(
"email is required. Provide as 'bv init <email>' or set SYFTBOX_EMAIL"
)
.into());
};
crate::config::set_persisted_biovault_home(&biovault_dir);
if !biovault_dir.exists() {
fs::create_dir_all(&biovault_dir)?;
info!("Created directory: {:?}", biovault_dir);
}
let config_file = biovault_dir.join("config.yaml");
if config_file.exists() {
println!(
"Configuration file already exists at: {}",
config_file.display()
);
println!("Skipping initialization.");
} else {
if is_syftbox_env() {
println!("✓ Running in SyftBox virtualenv");
if let Ok(data_dir) = env::var("SYFTBOX_DATA_DIR") {
println!(" SyftBox data directory: {}", data_dir);
}
println!(" BioVault home: {}", biovault_dir.display());
}
let syftbox_config = if let Ok(config_path) = env::var("SYFTBOX_CONFIG_PATH") {
if std::path::Path::new(&config_path).exists() {
println!("✓ Detected SyftBox config at: {}", config_path);
Some(config_path)
} else {
None
}
} else {
let home_dir = dirs::home_dir()
.ok_or_else(|| anyhow::anyhow!("Could not determine home directory"))?;
let default_syftbox = home_dir.join(".syftbox").join("config.json");
if default_syftbox.exists() {
Some(default_syftbox.to_string_lossy().to_string())
} else {
None
}
};
let config = Config {
email: email.to_string(),
syftbox_config: syftbox_config.clone(),
version: Some("0.1.27".to_string()),
binary_paths: None,
syftbox_credentials: None,
};
config.save(&config_file)?;
println!("✓ BioVault initialized successfully!");
println!(" Configuration saved to: {}", config_file.display());
println!(" Email: {}", email);
if let Some(ref syftbox_cfg) = syftbox_config {
println!(" SyftBox config: {}", syftbox_cfg);
}
let env_dir = biovault_dir.join("env");
if !env_dir.exists() {
fs::create_dir_all(&env_dir)?;
info!("Created environment directory: {:?}", env_dir);
}
let default_dir = env_dir.join("default");
if !default_dir.exists() {
fs::create_dir_all(&default_dir)?;
info!("Created default template directory: {:?}", default_dir);
}
let template_nf_content = include_str!("../../templates/default/template.nf");
let template_nf_path = default_dir.join("template.nf");
fs::write(&template_nf_path, template_nf_content)?;
info!("Created template.nf at: {:?}", template_nf_path);
let nextflow_config_content = include_str!("../../templates/default/nextflow.config");
let nextflow_config_path = default_dir.join("nextflow.config");
fs::write(&nextflow_config_path, nextflow_config_content)?;
info!("Created nextflow.config at: {:?}", nextflow_config_path);
let snp_dir = env_dir.join("snp");
if !snp_dir.exists() {
fs::create_dir_all(&snp_dir)?;
info!("Created SNP template directory: {:?}", snp_dir);
}
let snp_template_nf_content = include_str!("../../templates/snp/template.nf");
let snp_template_nf_path = snp_dir.join("template.nf");
fs::write(&snp_template_nf_path, snp_template_nf_content)?;
info!("Created SNP template.nf at: {:?}", snp_template_nf_path);
let snp_nextflow_config_content = include_str!("../../templates/snp/nextflow.config");
let snp_nextflow_config_path = snp_dir.join("nextflow.config");
fs::write(&snp_nextflow_config_path, snp_nextflow_config_content)?;
info!(
"Created SNP nextflow.config at: {:?}",
snp_nextflow_config_path
);
let sheet_dir = env_dir.join("sheet");
if !sheet_dir.exists() {
fs::create_dir_all(&sheet_dir)?;
info!("Created sheet template directory: {:?}", sheet_dir);
}
let sheet_template_nf_content = include_str!("../../templates/sheet/template.nf");
let sheet_template_nf_path = sheet_dir.join("template.nf");
fs::write(&sheet_template_nf_path, sheet_template_nf_content)?;
info!("Created sheet template.nf at: {:?}", sheet_template_nf_path);
let sheet_nextflow_config_content = include_str!("../../templates/sheet/nextflow.config");
let sheet_nextflow_config_path = sheet_dir.join("nextflow.config");
fs::write(&sheet_nextflow_config_path, sheet_nextflow_config_content)?;
info!(
"Created sheet nextflow.config at: {:?}",
sheet_nextflow_config_path
);
let dynamic_dir = env_dir.join("dynamic-nextflow");
if !dynamic_dir.exists() {
fs::create_dir_all(&dynamic_dir)?;
info!("Created dynamic template directory: {:?}", dynamic_dir);
}
let dynamic_template_nf_content = include_str!("../../templates/dynamic/template.nf");
let dynamic_template_nf_path = dynamic_dir.join("template.nf");
fs::write(&dynamic_template_nf_path, dynamic_template_nf_content)?;
info!(
"Created dynamic template.nf at: {:?}",
dynamic_template_nf_path
);
let dynamic_nextflow_config_content = "process.executor = 'local'\n";
let dynamic_nextflow_config_path = dynamic_dir.join("nextflow.config");
fs::write(
&dynamic_nextflow_config_path,
dynamic_nextflow_config_content,
)?;
info!(
"Created dynamic nextflow.config at: {:?}",
dynamic_nextflow_config_path
);
println!("✓ Nextflow templates installed:");
println!(" - Default templates: {}", default_dir.display());
println!(" - SNP templates: {}", snp_dir.display());
println!(" - Sheet templates: {}", sheet_dir.display());
println!(" - Dynamic templates: {}", dynamic_dir.display());
match config.get_syftbox_data_dir() {
Ok(data_dir) => {
let app = crate::syftbox::SyftBoxApp::new(&data_dir, &config.email, "biovault")?;
let _ = app.register_endpoint("/message")?;
println!(
"✓ SyftBox RPC initialized for messaging at: {}",
app.rpc_dir.display()
);
}
Err(e) => {
println!(
"⚠️ Skipped SyftBox RPC init (no data dir): {}\n Hint: set SYFTBOX_DATA_DIR or configure ~/.syftbox/config.json and re-run 'bv init'",
e
);
}
}
let projects_dir = biovault_dir.join("projects");
if !projects_dir.exists() {
fs::create_dir_all(&projects_dir)?;
info!("Created projects directory: {:?}", projects_dir);
println!("✓ Created projects directory: {}", projects_dir.display());
}
let runs_dir = biovault_dir.join("runs");
if !runs_dir.exists() {
fs::create_dir_all(&runs_dir)?;
info!("Created runs directory: {:?}", runs_dir);
println!("✓ Created runs directory: {}", runs_dir.display());
}
let cache_dir = biovault_dir.join("data").join("cache");
if !cache_dir.exists() {
fs::create_dir_all(&cache_dir)?;
info!("Created cache directory: {:?}", cache_dir);
println!("✓ Created cache directory: {}", cache_dir.display());
}
}
Ok(())
}
fn prompt_for_location() -> Result<PathBuf> {
println!("\n📁 Where would you like to store BioVault data?\n");
let home_dir =
dirs::home_dir().ok_or_else(|| anyhow::anyhow!("Could not determine home directory"))?;
let desktop_dir = dirs::desktop_dir().unwrap_or_else(|| home_dir.join("Desktop"));
let choices = vec![
format!(
"Desktop/BioVault (recommended for desktop use) [{}]",
desktop_dir.join("BioVault").display()
),
format!(
"Hidden in home directory (for CLI/server use) [{}]",
home_dir.join(".biovault").display()
),
"Custom location".to_string(),
];
let selection = Select::with_theme(&ColorfulTheme::default())
.with_prompt("Choose installation location")
.items(&choices)
.default(0)
.interact()
.map_err(|e| anyhow::anyhow!("Selection error: {}", e))?;
let biovault_dir = match selection {
0 => desktop_dir.join("BioVault"),
1 => home_dir.join(".biovault"),
2 => {
let custom_path: String = Input::with_theme(&ColorfulTheme::default())
.with_prompt("Enter custom path")
.interact_text()
.map_err(|e| anyhow::anyhow!("Input error: {}", e))?;
if let Some(path) = custom_path.strip_prefix("~/") {
home_dir.join(path)
} else if custom_path == "~" {
home_dir.clone()
} else {
PathBuf::from(custom_path)
}
}
_ => desktop_dir.join("BioVault"),
};
println!("\n✓ Selected location: {}\n", biovault_dir.display());
Ok(biovault_dir)
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[tokio::test]
async fn test_execute_with_email_arg() {
let temp_dir = TempDir::new().unwrap();
crate::config::set_test_biovault_home(temp_dir.path().join(".biovault"));
let data_dir = temp_dir.path().join("syftbox");
crate::config::set_test_syftbox_data_dir(&data_dir);
let result = execute(Some("test@example.com"), true).await;
assert!(result.is_ok());
let config = Config::load().unwrap();
assert_eq!(config.email, "test@example.com");
crate::config::clear_test_syftbox_data_dir();
crate::config::clear_test_biovault_home();
}
#[tokio::test]
async fn test_execute_with_syftbox_email_env() {
let temp_dir = TempDir::new().unwrap();
crate::config::set_test_biovault_home(temp_dir.path().join(".biovault"));
let data_dir = temp_dir.path().join("syftbox");
crate::config::set_test_syftbox_data_dir(&data_dir);
env::set_var("SYFTBOX_EMAIL", "syftbox@example.com");
let result = execute(None, true).await;
env::remove_var("SYFTBOX_EMAIL");
crate::config::clear_test_syftbox_data_dir();
crate::config::clear_test_biovault_home();
let _ = result;
}
#[tokio::test]
#[serial_test::serial]
async fn test_execute_overwrite_existing_config() {
let temp_dir = TempDir::new().unwrap();
crate::config::set_test_biovault_home(temp_dir.path().join(".biovault"));
let data_dir = temp_dir.path().join("syftbox");
crate::config::set_test_syftbox_data_dir(&data_dir);
let initial_config = Config {
email: "old@example.com".to_string(),
syftbox_config: None,
version: None,
binary_paths: None,
syftbox_credentials: None,
};
let config_path = temp_dir.path().join(".biovault").join("config.yaml");
fs::create_dir_all(config_path.parent().unwrap()).unwrap();
initial_config.save(&config_path).unwrap();
let _ = execute(Some("new@example.com"), true).await;
crate::config::clear_test_syftbox_data_dir();
crate::config::clear_test_biovault_home();
}
#[tokio::test]
async fn test_execute_creates_biovault_dir() {
let temp_dir = TempDir::new().unwrap();
let biovault_path = temp_dir.path().join(".biovault");
crate::config::set_test_biovault_home(biovault_path.clone());
let data_dir = temp_dir.path().join("syftbox");
crate::config::set_test_syftbox_data_dir(&data_dir);
assert!(!biovault_path.exists());
let result = execute(Some("test@example.com"), true).await;
assert!(result.is_ok());
assert!(biovault_path.exists());
crate::config::clear_test_syftbox_data_dir();
crate::config::clear_test_biovault_home();
}
#[tokio::test]
async fn test_execute_no_email_non_tty() {
let temp_dir = TempDir::new().unwrap();
crate::config::set_test_biovault_home(temp_dir.path().join(".biovault"));
env::remove_var("SYFTBOX_EMAIL");
crate::config::clear_test_biovault_home();
}
}