use std::path::Path;
use colored::Colorize;
use crate::{
cli::{confirm::confirm_destructive, global::json_output},
domain::{error::DotLockError, model::DotLockResult},
git::install::install_merge_driver_if_in_git_repo,
storage::{init_project::init_vault_pair, project, secure_fs},
utils::render_table,
};
use crate::cli::args::EnvCommand;
pub fn run(command: EnvCommand) -> DotLockResult<()> {
match command {
EnvCommand::List => list(),
EnvCommand::Add { name } => add(&name),
EnvCommand::Use { name } => use_env(&name),
EnvCommand::Remove { name, yes } => remove(&name, yes),
}
}
fn ensure_base_project_initialized() -> DotLockResult<()> {
if Path::new(&project::vault_file_for(None)).exists() {
return Ok(());
}
Err(DotLockError::ProjectNotInitialized)
}
fn list() -> DotLockResult<()> {
ensure_base_project_initialized()?;
let active = project::active_env_name();
let persisted = project::persisted_default_env();
let envs = project::list_environments();
if json_output() {
let items: Vec<serde_json::Value> = envs
.iter()
.map(|name| {
let env = (name != project::DEFAULT_ENV_NAME).then_some(name.as_str());
serde_json::json!({
"name": name,
"active": *name == active,
"persisted_default": persisted.as_deref() == Some(name.as_str()),
"path": project::vault_file_for(env),
})
})
.collect();
println!("{}", serde_json::Value::Array(items));
return Ok(());
}
let rows: Vec<Vec<String>> = envs
.iter()
.map(|name| {
let env = (name != project::DEFAULT_ENV_NAME).then_some(name.as_str());
vec![
name.clone(),
if *name == active {
"*".to_string()
} else {
String::new()
},
project::vault_file_for(env),
]
})
.collect();
render_table(
&["ENVIRONMENT", "ACTIVE", "VAULT"],
&rows,
&[|s| s.bold(), |s| s.green(), |s| s.dimmed()],
);
Ok(())
}
fn add(name: &str) -> DotLockResult<()> {
ensure_base_project_initialized()?;
project::validate_env_name(name)?;
if name == project::DEFAULT_ENV_NAME {
return Err(DotLockError::EnvironmentAlreadyExists {
name: name.to_string(),
});
}
if Path::new(&project::vault_file_for(Some(name))).exists() {
return Err(DotLockError::EnvironmentAlreadyExists {
name: name.to_string(),
});
}
println!(
"{} creating environment {} (choose its master password)",
"info:".cyan().bold(),
name.bold()
);
init_vault_pair(Some(name))?;
println!(
"{} environment {} created",
"ok:".green().bold(),
name.bold()
);
println!(
" {} {}",
"created".dimmed(),
project::vault_file_for(Some(name)).bold()
);
println!(
" {} {}",
"created".dimmed(),
project::secrets_file_for(Some(name)).bold()
);
println!(
" select it with {} or {}",
format!("dl --env {name} ...").bold(),
format!("dl env use {name}").bold()
);
let _ = install_merge_driver_if_in_git_repo()?;
Ok(())
}
fn remove(name: &str, yes: bool) -> DotLockResult<()> {
ensure_base_project_initialized()?;
project::validate_env_name(name)?;
if name == project::DEFAULT_ENV_NAME {
return Err(DotLockError::Io(
"the default environment cannot be removed; it is the project's root vault \
(remove the whole project by deleting `.lock/`)"
.to_string(),
));
}
if !Path::new(&project::vault_file_for(Some(name))).exists() {
return Err(DotLockError::EnvironmentNotInitialized {
name: name.to_string(),
});
}
println!(
"{} this permanently deletes environment {} — every secret stored in it is lost \
(its vault pair under {} is removed)",
"warn:".yellow().bold(),
name.bold(),
project::env_lock_dir_for(Some(name)).bold()
);
confirm_destructive(&format!("permanently delete environment {name}"), yes)?;
std::fs::remove_dir_all(project::env_lock_dir_for(Some(name))).map_err(DotLockError::from)?;
if project::persisted_default_env().as_deref() == Some(name) {
std::fs::remove_file(project::ENV_SELECTION_FILE).map_err(DotLockError::from)?;
println!(
"{} persisted selection reset to {} (it pointed at the removed environment)",
"info:".cyan().bold(),
project::DEFAULT_ENV_NAME.bold()
);
}
println!(
"{} environment {} removed",
"ok:".green().bold(),
name.bold()
);
Ok(())
}
fn use_env(name: &str) -> DotLockResult<()> {
ensure_base_project_initialized()?;
project::validate_env_name(name)?;
if name != project::DEFAULT_ENV_NAME
&& !Path::new(&project::vault_file_for(Some(name))).exists()
{
return Err(DotLockError::EnvironmentNotInitialized {
name: name.to_string(),
});
}
secure_fs::write_string_atomic(
Path::new(project::ENV_SELECTION_FILE),
&format!("{name}\n"),
0o700,
0o600,
)?;
println!(
"{} default environment set to {} (persisted in {})",
"ok:".green().bold(),
name.bold(),
project::ENV_SELECTION_FILE.dimmed()
);
Ok(())
}