tsafe-cli 1.0.27

Secrets runtime for developers — inject credentials into processes via exec, never into shell history or .env files
//! `tsafe browser-profile` — map domains to vault profiles for the extension.

use anyhow::Result;
use colored::Colorize;
use crate::cli::BrowserProfileAction;
use tsafe_core::profile;

fn browser_profiles_path() -> std::path::PathBuf {
    profile::vault_dir().join("browser-profiles.json")
}

fn load_browser_profiles() -> Result<serde_json::Map<String, serde_json::Value>> {
    let path = browser_profiles_path();
    if !path.exists() {
        return Ok(serde_json::Map::new());
    }
    let data = std::fs::read_to_string(&path)?;
    Ok(serde_json::from_str(&data)?)
}

fn save_browser_profiles(map: &serde_json::Map<String, serde_json::Value>) -> Result<()> {
    let path = browser_profiles_path();
    if let Some(parent) = path.parent() {
        std::fs::create_dir_all(parent)?;
    }
    let tmp = path.with_extension("tmp");
    std::fs::write(&tmp, serde_json::to_string_pretty(map)?)?;
    std::fs::rename(&tmp, &path)?;
    Ok(())
}

fn empty_browser_profile_list_message() -> String {
    format!(
        "{} No mappings. Add one: tsafe browser-profile add <domain>",
        "i".blue()
    )
}

pub(crate) fn cmd_browser_profile(
    active_profile: &str,
    action: BrowserProfileAction,
) -> Result<()> {
    match action {
        BrowserProfileAction::Add {
            domain,
            profile: vault_profile,
        } => {
            let target = vault_profile.as_deref().unwrap_or(active_profile);
            let mut map = load_browser_profiles()?;
            map.insert(
                domain.clone(),
                serde_json::Value::String(target.to_string()),
            );
            save_browser_profiles(&map)?;
            println!("{} {} → profile '{target}'", "".green(), domain);
        }
        BrowserProfileAction::List => {
            let map = load_browser_profiles()?;
            if map.is_empty() {
                println!("{}", empty_browser_profile_list_message());
            } else {
                println!("{:<45} Profile", "Domain");
                println!("{}", "-".repeat(60));
                let mut entries: Vec<_> = map.iter().collect();
                entries.sort_by_key(|(d, _)| d.as_str());
                for (domain, prof) in entries {
                    println!("{:<45} {}", domain, prof.as_str().unwrap_or("?"));
                }
            }
        }
        BrowserProfileAction::Remove { domain } => {
            let mut map = load_browser_profiles()?;
            if map.remove(&domain).is_some() {
                save_browser_profiles(&map)?;
                println!("{} Removed mapping for '{domain}'", "".green());
            } else {
                anyhow::bail!("no mapping found for '{domain}'");
            }
        }
    }
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn empty_list_message_stays_command_local() {
        let message = empty_browser_profile_list_message();

        assert!(message.contains("No mappings."));
        assert!(message.contains("tsafe browser-profile add <domain>"));
        assert!(!message.contains("extension"));
    }
}