tsafe-cli 1.0.21

tsafe CLI — local secret and credential manager (replaces .env files)
Documentation
//! Template rendering and stdin-redaction command handlers.
//!
//! Implements `tsafe template` and `tsafe redact` — substituting vault secrets
//! into `{{KEY}}` placeholders in a file, and masking secret values on stdin.

use std::io::{self, BufRead};

use anyhow::{Context, Result};
use colored::Colorize;

use crate::helpers::*;

pub(crate) fn cmd_template(
    profile: &str,
    file: &str,
    output: Option<&str>,
    ignore_missing: bool,
) -> Result<()> {
    let vault = open_vault(profile)?;
    let all = vault.export_all()?;
    let template = std::fs::read_to_string(file)
        .with_context(|| format!("could not read template file: {file}"))?;

    let mut result = template.clone();
    // Find all {{KEY}} placeholders.
    let re_pattern = "{{";
    let mut pos = 0;
    let mut missing = Vec::new();
    while let Some(start) = result[pos..].find(re_pattern) {
        let abs_start = pos + start;
        if let Some(end) = result[abs_start + 2..].find("}}") {
            let abs_end = abs_start + 2 + end;
            let key = result[abs_start + 2..abs_end].trim();
            if let Some(value) = all.get(key) {
                let placeholder = &result[abs_start..abs_end + 2];
                result = result.replacen(placeholder, value, 1);
                // Don't advance pos — the replacement may be shorter.
            } else {
                if !ignore_missing {
                    missing.push(key.to_string());
                }
                pos = abs_end + 2;
            }
        } else {
            break;
        }
    }

    if !missing.is_empty() {
        anyhow::bail!(
            "template has unknown placeholders: {}\nUse --ignore-missing to skip them.",
            missing.join(", ")
        );
    }

    if let Some(out_path) = output {
        let tmp = format!("{out_path}.tmp");
        std::fs::write(&tmp, &result)?;
        std::fs::rename(&tmp, out_path)?;
        println!("{} Written to {}", "".green(), out_path);
    } else {
        print!("{result}");
    }
    Ok(())
}

pub(crate) fn cmd_redact(profile: &str) -> Result<()> {
    let vault = open_vault(profile)?;
    let all = vault.export_all()?;
    // Collect values sorted by length descending so longer values are redacted first.
    let mut values: Vec<&str> = all.values().map(String::as_str).collect();
    values.sort_by_key(|b| std::cmp::Reverse(b.len()));
    // Filter out very short values (<=2 chars) to avoid false positives.
    let values: Vec<&str> = values.into_iter().filter(|v| v.len() > 2).collect();

    drop(vault);

    let stdin = io::stdin();
    for line in stdin.lock().lines() {
        let mut line = line?;
        for val in &values {
            if line.contains(val) {
                line = line.replace(val, "[REDACTED]");
            }
        }
        println!("{line}");
    }
    Ok(())
}