eenv 0.1.1

Encrypted Env Manager: encrypts .env files, manages examples, and enforces safety via git hooks.
Documentation
use std::process::Command as Proc;
use std::{io, path::Path, path::PathBuf};

pub fn pre_commit(repo_root: &Path, write: bool) -> io::Result<()> {
    let staged = staged_files(repo_root)?;
    let mut offenders = Vec::new();
    for p in &staged {
        if let Some(name) = p.file_name().and_then(|s| s.to_str()) {
            if name.starts_with(".env") && !name.ends_with(".example") && !name.ends_with(".enc") {
                offenders.push(p.clone());
            }
        }
    }
    if !offenders.is_empty() {
        eprintln!("[pre-commit] ❌ refusing to commit raw .env files:");
        for p in offenders {
            eprintln!("  - {}", p.display());
        }
        eprintln!("Hint: encrypt them to .env*.enc or add them to .gitignore.");
        return Err(io::Error::new(io::ErrorKind::Other, "raw .env staged"));
    }

    let (files, _t_find) = crate::util::time_result("find_env_files_recursive", || {
        crate::envscan::find_env_files_recursive(repo_root)
    })?;
    let ((real, _examples, _encs), _t_split) =
        crate::util::time_ok("split_env_files", || crate::envscan::split_env_files(files));

    if write && !real.is_empty() {
        let skeletons = crate::examples::extract_env_skeletons(&real)?;
        if let Ok(actions) = crate::examples::ensure_env_examples_from_skeletons(&skeletons) {
            let mut to_add = Vec::new();
            for (_src, dst, action) in actions {
                match action {
                    crate::examples::ExampleAction::Created
                    | crate::examples::ExampleAction::Overwritten => to_add.push(dst),
                    crate::examples::ExampleAction::SourceIsExample => {}
                }
            }
            if !to_add.is_empty() {
                git_add(repo_root, &to_add)?;
            }
        }
    }

    if write && !real.is_empty() {
        match crate::gitignore::fix_gitignore_from_found(repo_root, &real) {
            Ok(report) => {
                if report.changed {
                    git_add(repo_root, &[report.path])?;
                }
            }
            Err(e) => eprintln!("[pre-commit] gitignore fix error: {e}"),
        }
    }

    if write && !real.is_empty() {
        match crate::config::ensure_eenv_config(repo_root) {
            Ok(crate::config::ConfigStatus::Created) => {
                eprintln!("[config] created eenv.config.json")
            }
            Ok(crate::config::ConfigStatus::FixedMissingKey) => {
                eprintln!("[config] injected key into eenv.config.json")
            }
            Ok(crate::config::ConfigStatus::RewrittenFromInvalid { backup }) => eprintln!(
                "[config] repaired eenv.config.json (backup: {})",
                backup.display()
            ),
            Ok(crate::config::ConfigStatus::Valid) => {}
            Err(e) => eprintln!("[config] error: {e}"),
        }

        let produced = crate::crypto::encrypt_envs_to_enc(repo_root, &real)?;
        if !produced.is_empty() {
            git_add(repo_root, &produced)?;
        }
    }

    Ok(())
}

fn staged_files(repo_root: &Path) -> io::Result<Vec<PathBuf>> {
    let out = Proc::new("git")
        .arg("-C")
        .arg(repo_root)
        .arg("diff")
        .arg("--name-only")
        .arg("--cached")
        .arg("-z")
        .output()?;
    if !out.status.success() {
        return Err(io::Error::new(io::ErrorKind::Other, "git diff failed"));
    }
    let mut files = Vec::new();
    for name in out.stdout.split(|b| *b == 0u8) {
        if name.is_empty() {
            continue;
        }
        let s = String::from_utf8_lossy(name);
        files.push(repo_root.join(s.as_ref()));
    }
    Ok(files)
}

fn git_add(repo_root: &Path, paths: &[PathBuf]) -> io::Result<()> {
    if paths.is_empty() {
        return Ok(());
    }
    let mut cmd = Proc::new("git");
    cmd.arg("-C").arg(repo_root).arg("add").arg("--");
    for p in paths {
        cmd.arg(p);
    }
    let status = cmd.status()?;
    if !status.success() {
        return Err(io::Error::new(io::ErrorKind::Other, "git add failed"));
    }
    Ok(())
}