use std::io::{self, BufRead, Write};
use anyhow::{Context, Result};
use colored::Colorize;
use tsafe_cli::cli::ProfileAction;
use tsafe_core::{audit::AuditEntry, events::emit_event, profile};
use crate::helpers::*;
pub(crate) fn cmd_rotate(profile: &str) -> Result<()> {
let mut vault = open_vault(profile)?;
let new_pw = if let Ok(pw) = std::env::var("TSAFE_NEW_MASTER_PASSWORD") {
if pw.is_empty() {
anyhow::bail!("TSAFE_NEW_MASTER_PASSWORD is set but empty — unset it or set a non-empty new password");
}
eprintln!(
"{} Using new master password from TSAFE_NEW_MASTER_PASSWORD (non-interactive).",
"i".blue()
);
pw
} else {
prompt_password_confirmed()?
};
vault.rotate(new_pw.as_bytes()).context("rotation failed")?;
audit(profile)
.append(&AuditEntry::success(profile, "rotate", None))
.ok();
emit_event(profile, "rotate", None);
println!("{} Vault re-encrypted with new password", "✓".green());
offer_os_keychain_for_master_password(
profile,
&new_pw,
MasterPasswordKeychainPrompt::AfterRotate,
)?;
Ok(())
}
pub(crate) fn cmd_profile(active_profile: &str, action: ProfileAction) -> Result<()> {
match action {
ProfileAction::List => {
let profiles = profile::list_profiles()?;
let default = profile::get_default_profile();
if profiles.is_empty() {
println!("{} No profiles found", "i".blue());
} else {
for p in &profiles {
if p == &default {
println!("{} {} {}", "▶".cyan(), p, "(default)".dimmed());
} else {
println!(" {p}");
}
}
}
}
ProfileAction::Delete { name, force } => {
if !force {
eprint!("Delete vault for profile '{name}'? [y/N]: ");
io::stderr().flush().ok();
let mut input = String::new();
io::stdin().lock().read_line(&mut input)?;
if !input.trim().eq_ignore_ascii_case("y") {
println!("Aborted.");
return Ok(());
}
}
let path = profile::vault_path(&name);
if !path.exists() {
anyhow::bail!("no vault found for profile '{name}'");
}
std::fs::remove_file(&path)?;
if profile::get_default_profile() == name {
profile::set_default_profile("default").map_err(|e| anyhow::anyhow!("{e}"))?;
}
println!("{} Deleted vault for profile '{name}'", "✓".green());
}
ProfileAction::SetDefault { name } => {
profile::validate_profile_name(&name).map_err(|e| anyhow::anyhow!("{e}"))?;
if !profile::profile_exists(&name) {
anyhow::bail!(
"no vault found for profile '{name}' — create it with: tsafe --profile {name} init"
);
}
profile::set_default_profile(&name).map_err(|e| anyhow::anyhow!("{e}"))?;
println!("{} Default profile set to '{name}'.", "✓".green());
println!(" Running `tsafe` without -p will now use '{name}'.");
}
ProfileAction::Rename { from, to } => {
profile::validate_profile_name(&from).map_err(|e| anyhow::anyhow!("{e}"))?;
profile::validate_profile_name(&to).map_err(|e| anyhow::anyhow!("{e}"))?;
let src = profile::vault_path(&from);
let dst = profile::vault_path(&to);
anyhow::ensure!(src.exists(), "no vault found for profile '{from}'");
anyhow::ensure!(!dst.exists(), "a vault already exists for profile '{to}'");
let snapshots_migrated = profile::rename_profile_snapshot_history(&from, &to)
.with_context(|| format!("failed to migrate snapshots from '{from}' to '{to}'"))?;
if let Err(err) = std::fs::rename(&src, &dst) {
if snapshots_migrated {
match profile::rename_profile_snapshot_history(&to, &from) {
Ok(_) => {
return Err(anyhow::Error::new(err).context(format!(
"failed to rename vault from '{from}' to '{to}' after moving snapshots; snapshot migration was rolled back"
)));
}
Err(rollback_err) => {
return Err(anyhow::anyhow!(
"failed to rename vault from '{from}' to '{to}' after moving snapshots: {err}; snapshot rollback also failed: {rollback_err}"
));
}
}
}
return Err(anyhow::Error::new(err)
.context(format!("failed to rename vault from '{from}' to '{to}'")));
}
let audit_src = profile::audit_log_path(&from);
let audit_dst = profile::audit_log_path(&to);
if audit_src.exists() {
let _ = std::fs::rename(&audit_src, &audit_dst);
}
if profile::get_default_profile() == from {
profile::set_default_profile(&to).map_err(|e| anyhow::anyhow!("{e}"))?;
}
println!("{} Profile '{from}' renamed to '{to}'.", "✓".green());
if snapshots_migrated {
println!(" {} Snapshot history moved to profile '{to}'.", "i".blue());
}
if active_profile == from {
println!(
" {}: update your shell to use -p {to} or set TSAFE_PROFILE={to}",
"note".yellow()
);
}
}
}
Ok(())
}
pub(crate) fn cmd_unlock(profile: &str) -> Result<()> {
profile::validate_profile_name(profile)?;
let vault_path = profile::vault_path(profile);
let lock_path = vault_path.with_extension("vault.lock");
if lock_path.exists() {
std::fs::remove_file(&lock_path)
.with_context(|| format!("Failed to remove lock file: {}", lock_path.display()))?;
println!(
"{} lock removed: {}",
"✓".green().bold(),
lock_path.display()
);
} else {
println!(
"{} no lock file found for profile '{profile}' — vault is not locked",
"i".blue()
);
}
Ok(())
}