use std::collections::HashMap;
use std::io::{self, BufRead, IsTerminal, Write};
use anyhow::{Context, Result};
use colored::Colorize;
use tsafe_cli::cli::ExportFormat;
use tsafe_core::{
audit::AuditEntry, env as tsenv, errors::SafeError, events::emit_event, profile, vault::Vault,
};
use crate::helpers::*;
pub(crate) fn cmd_init(profile: &str, profile_explicit: bool) -> Result<()> {
let resolved: String = if !profile_explicit {
if io::stdin().is_terminal() {
eprint!("Vault name [{profile}]: ");
io::stderr().flush().ok();
let mut input = String::new();
io::stdin().lock().read_line(&mut input)?;
let trimmed = input.trim().to_owned();
if trimmed.is_empty() {
profile.to_owned()
} else {
trimmed
}
} else {
profile.to_owned()
}
} else {
profile.to_owned()
};
let profile = resolved.as_str();
profile::validate_profile_name(profile)?;
if profile::profile_exists(profile) {
anyhow::bail!(
"vault already exists for profile '{profile}'\n\
\n To use a different profile name: tsafe --profile <name> init\
\n To see existing profiles: tsafe profile list"
);
}
let password = prompt_password_confirmed()?;
let v = Vault::create(&profile::vault_path(profile), password.as_bytes())
.context("failed to create vault")?;
audit(profile)
.append(&AuditEntry::success(profile, "init", None))
.ok();
emit_event(profile, "init", None);
println!("{} Vault ready: {}", "✓".green(), v.path().display());
backup_new_profile_password_if_configured(profile, &password)?;
onboarding_biometric_unlock(profile, &password)?;
onboarding_import_dotenv(profile, &password)?;
Ok(())
}
pub(crate) fn cmd_set(
profile: &str,
key: &str,
value: Option<String>,
tags: Vec<String>,
overwrite: bool,
) -> Result<()> {
let val = match value {
Some(v) => {
eprintln!(
"{} Secret value passed as a command-line argument — \
it may appear in shell history and process listings. \
Omit the value to read it securely from stdin.",
"warn:".yellow().bold()
);
v
}
None => {
if io::stdin().is_terminal() {
use inquire::{Password, PasswordDisplayMode};
Password::new(&format!("Value for '{key}'"))
.with_display_mode(PasswordDisplayMode::Masked)
.without_confirmation()
.prompt()
.with_context(|| format!("failed to read secret value for '{key}'"))?
} else {
eprint!("Value for '{key}' (piped / non-interactive): ");
io::stderr().flush().ok();
let mut line = String::new();
io::stdin().lock().read_line(&mut line)?;
line.trim_end_matches(['\n', '\r']).to_string()
}
}
};
let tag_map: HashMap<String, String> = tags
.iter()
.filter_map(|t| {
let mut p = t.splitn(2, '=');
Some((p.next()?.to_string(), p.next()?.to_string()))
})
.collect();
let mut vault = open_vault(profile)?;
if vault.list().contains(&key) && !overwrite {
if io::stdin().is_terminal() {
eprint!(
"{} '{}' already exists. Overwrite? [y/N]: ",
"warn:".yellow().bold(),
key
);
io::stderr().flush().ok();
let mut resp = String::new();
io::stdin().lock().read_line(&mut resp)?;
if !resp.trim().eq_ignore_ascii_case("y") {
println!("Aborted.");
return Ok(());
}
} else {
anyhow::bail!("key '{key}' already exists — use --overwrite to replace");
}
}
vault
.set(key, &val, tag_map)
.context("failed to set secret")?;
audit(profile)
.append(&AuditEntry::success(profile, "set", Some(key)))
.ok();
emit_event(profile, "set", Some(key));
println!("{} Set '{key}'", "✓".green());
Ok(())
}
pub(crate) fn cmd_get(profile: &str, key: &str, copy: bool, version: Option<usize>) -> Result<()> {
let vault = open_vault(profile)?;
let value = if let Some(v) = version {
vault.get_version(key, v).map_err(|e| match &e {
SafeError::SecretNotFound { .. } => anyhow::anyhow!(
"secret '{key}' not found\n\
\n See all keys: tsafe list\
\n Add a new secret: tsafe set {key} <value>"
),
other => anyhow::anyhow!("{other}"),
})?
} else {
let raw = vault.get(key).map_err(|e| match &e {
SafeError::SecretNotFound { .. } => anyhow::anyhow!(
"secret '{key}' not found\n\
\n See all keys: tsafe list\
\n Add a new secret: tsafe set {key} <value>"
),
other => anyhow::anyhow!("{other}"),
})?;
if let Some(real_key) = raw.strip_prefix("@alias:") {
vault
.get(real_key)
.with_context(|| format!("alias target '{real_key}' not found"))?
} else {
raw
}
};
if let Some(entry) = vault.file().secrets.get(key) {
warn_if_expired(key, &entry.tags);
}
audit(profile)
.append(&AuditEntry::success(profile, "get", Some(key)))
.ok();
if copy {
set_clipboard(&value)?;
println!(
"{} '{}' copied to clipboard — clears in 30 s",
"✓".green(),
key
);
} else {
print!("{}", &*value);
}
Ok(())
}
pub(crate) fn cmd_delete(profile: &str, key: &str) -> Result<()> {
let mut vault = open_vault(profile)?;
vault.delete(key).map_err(|e| match &e {
SafeError::SecretNotFound { .. } => anyhow::anyhow!(
"secret '{key}' not found\n\
\n See all keys: tsafe list"
),
other => anyhow::anyhow!("{other}"),
})?;
audit(profile)
.append(&AuditEntry::success(profile, "delete", Some(key)))
.ok();
emit_event(profile, "delete", Some(key));
println!("{} Deleted '{key}'", "✓".green());
Ok(())
}
pub(crate) fn cmd_list(profile: &str, tag_filters: &[String], ns: Option<&str>) -> Result<()> {
let vault = open_vault(profile)?;
let parsed = parse_tag_filters(tag_filters);
let ns_prefix = ns.map(|n| format!("{n}/"));
let keys: Vec<String> = vault
.list()
.into_iter()
.filter(|k| {
if let Some(ref pfx) = ns_prefix {
if !k.starts_with(pfx.as_str()) {
return false;
}
}
parsed.iter().all(|(fk, fv)| {
vault
.file()
.secrets
.get(*k)
.map(|e| e.tags.get(*fk).map(|v| v == fv).unwrap_or(false))
.unwrap_or(false)
})
})
.map(|k| match &ns_prefix {
Some(pfx) => k.strip_prefix(pfx.as_str()).unwrap_or(k).to_string(),
None => k.to_string(),
})
.collect();
if keys.is_empty() {
let ctx = match (ns, parsed.is_empty()) {
(Some(n), true) => format!(" in namespace '{n}' (profile '{profile}')"),
(Some(n), false) => {
format!(" matching filter in namespace '{n}' (profile '{profile}')")
}
(None, true) => format!(" in profile '{profile}'"),
(None, false) => format!(" matching tag filter in profile '{profile}'"),
};
println!("{} No secrets{ctx}", "i".blue());
} else {
keys.iter().for_each(|k| println!("{k}"));
}
Ok(())
}
pub(crate) fn cmd_export(
profile: &str,
format: ExportFormat,
filter: Vec<String>,
tag_filters: Vec<String>,
ns: Option<&str>,
) -> Result<()> {
let vault = open_vault(profile)?;
let ns_prefix = ns.map(|n| format!("{n}/"));
let mut all = vault.export_all().context("failed to decrypt secrets")?;
if let Some(ref pfx) = ns_prefix {
all.retain(|k, _| k.starts_with(pfx.as_str()));
}
if !filter.is_empty() {
all.retain(|k, _| {
filter
.iter()
.any(|f| k == f || k.ends_with(&format!("/{f}")))
});
}
let parsed = parse_tag_filters(&tag_filters);
if !parsed.is_empty() {
all.retain(|k, _| {
parsed.iter().all(|(fk, fv)| {
vault
.file()
.secrets
.get(k)
.map(|e| e.tags.get(*fk).map(|v| v == fv).unwrap_or(false))
.unwrap_or(false)
})
});
}
if let Some(ref pfx) = ns_prefix {
all = all
.into_iter()
.map(|(k, v)| (k.strip_prefix(pfx.as_str()).unwrap_or(&k).to_string(), v))
.collect();
}
audit(profile)
.append(&AuditEntry::success(profile, "export", None))
.ok();
let out = match format {
ExportFormat::Env => tsenv::format_env(&all),
ExportFormat::Dotenv => tsenv::format_dotenv(&all),
ExportFormat::Powershell => tsenv::format_powershell(&all),
ExportFormat::Json => tsenv::format_json(&all)?,
ExportFormat::GithubActions => tsenv::format_github_actions(&all),
ExportFormat::Yaml => tsenv::format_yaml(&all)?,
ExportFormat::DockerEnv => tsenv::format_docker_env(&all),
ExportFormat::Toml => {
let pairs: Vec<(String, String)> = {
let mut v: Vec<(String, String)> = all.into_iter().collect();
v.sort_by(|(a, _), (b, _)| a.cmp(b));
v
};
tsenv::format_toml(&pairs)
}
};
println!("{out}");
Ok(())
}