sshenv 0.0.1-alpha.0

SSH-key-backed encrypted vault for environment variables
Documentation
use anyhow::{Context, Result, bail};
use sshenv_cli_models::{ListArgs, RmProfileArgs, SetArgs, ShowArgs, UnsetArgs};
use zeroize::Zeroizing;

use crate::commands::{Context as CmdContext, load_and_unlock};

pub fn set(ctx: &CmdContext, args: SetArgs) -> Result<()> {
    let value = resolve_value(&args)?;

    let (mut vault, key) = load_and_unlock(&ctx.vault_path)?;

    vault
        .profiles
        .set(&args.profile, &args.var, value.as_str().to_string());
    vault.save(&ctx.vault_path, &key)?;
    eprintln!("Set {}/{}.", args.profile, args.var);
    Ok(())
}

fn resolve_value(args: &SetArgs) -> Result<Zeroizing<String>> {
    if let Some(v) = &args.value {
        return Ok(Zeroizing::new(v.clone()));
    }
    let prompt = format!("Value for {}/{}: ", args.profile, args.var);
    let raw = rpassword::prompt_password(prompt).context("failed to read value from terminal")?;
    if raw.is_empty() {
        bail!("value is empty; aborting");
    }
    Ok(Zeroizing::new(raw))
}

pub fn unset(ctx: &CmdContext, args: UnsetArgs) -> Result<()> {
    let (mut vault, key) = load_and_unlock(&ctx.vault_path)?;

    if !vault.profiles.unset(&args.profile, &args.var) {
        bail!("no such variable: {}/{}", args.profile, args.var);
    }
    vault.save(&ctx.vault_path, &key)?;
    eprintln!("Unset {}/{}.", args.profile, args.var);
    Ok(())
}

pub fn list(ctx: &CmdContext, args: ListArgs) -> Result<()> {
    let (vault, _key) = load_and_unlock(&ctx.vault_path)?;

    match args.profile {
        Some(profile) => {
            let Some(vars) = vault.profiles.get(&profile) else {
                bail!("no such profile: {profile}");
            };
            for name in vars.keys() {
                println!("{name}");
            }
        }
        None => {
            let names = vault.profiles.profile_names();
            let filtered: Vec<&String> = match &args.prefix {
                Some(p) => names.iter().filter(|n| n.starts_with(p.as_str())).collect(),
                None => names.iter().collect(),
            };
            for name in filtered {
                println!("{name}");
            }
        }
    }
    Ok(())
}

pub fn show(ctx: &CmdContext, args: ShowArgs) -> Result<()> {
    let (vault, _key) = load_and_unlock(&ctx.vault_path)?;

    let Some(vars) = vault.profiles.get(&args.profile) else {
        bail!("no such profile: {}", args.profile);
    };

    eprintln!(
        "warning: printing secret values for profile '{}' to stdout.",
        args.profile
    );
    for (k, v) in vars {
        println!("{k}={v}");
    }
    Ok(())
}

pub fn rm(ctx: &CmdContext, args: RmProfileArgs) -> Result<()> {
    let (mut vault, key) = load_and_unlock(&ctx.vault_path)?;

    if !vault.profiles.remove_profile(&args.profile) {
        bail!("no such profile: {}", args.profile);
    }
    vault.save(&ctx.vault_path, &key)?;
    eprintln!("Removed profile {}.", args.profile);
    Ok(())
}