vlt 0.1.0

Fast offline-first CLI for managing .env files across environments
use anyhow::{Context, Result};

use crate::models::config::VltConfig;
use crate::models::env_file::EnvFile;
use crate::models::rules::VltRules;
use crate::utils::output::{self, Icon};
use crate::utils::project;

pub fn run() -> Result<()> {
    let root = std::env::current_dir().context("failed to read current directory")?;
    project::ensure_initialized(&root)?;
    let config = VltConfig::load_or_default(&root.join(".vlt/config.toml"))?;
    let rules = VltRules::load_or_default(&root.join(".vlt/env.rules"))?;
    let available_envs = discover_envs(&root)?;

    match &config.active_env {
        Some(active) => output::print_line(Icon::Info, format!("Active environment: {active}")),
        None => output::print_line(Icon::Warning, "Active environment: none"),
    }

    if available_envs.is_empty() {
        output::print_line(Icon::Warning, "No environments found.");
    } else {
        output::print_line(
            Icon::Success,
            format!("Available environments: {}", available_envs.join(", ")),
        );
    }

    if let Some(active) = &config.active_env {
        let active_env_path = root.join(format!(".vlt/env.{active}"));
        if !active_env_path.exists() {
            output::print_line(
                Icon::Error,
                format!("Active environment file is missing for `{active}`."),
            );
        } else {
            let active_env = EnvFile::load(&active_env_path)?;
            let missing_values = rules.missing_values(&active_env.values);

            if missing_values.is_empty() {
                output::print_line(Icon::Success, "All required keys have values.");
            } else {
                output::print_line(
                    Icon::Warning,
                    format!("Missing values: {}", missing_values.join(", ")),
                );
            }
        }
    }

    let dot_env_path = root.join(".env");
    if dot_env_path.exists() {
        let dot_env = EnvFile::load(&dot_env_path)?;
        let drift = rules.unknown_keys(&dot_env.values);
        if drift.is_empty() {
            output::print_line(Icon::Success, "No drift detected in `.env`.");
        } else {
            output::print_line(Icon::Warning, format!("Drift in `.env`: {}", drift.join(", ")));
        }
    } else {
        output::print_line(Icon::Info, "`.env` has not been generated yet.");
    }

    Ok(())
}

fn discover_envs(root: &std::path::Path) -> Result<Vec<String>> {
    let mut envs = Vec::new();
    let vlt_dir = root.join(".vlt");

    if !vlt_dir.exists() {
        return Ok(envs);
    }

    for entry in std::fs::read_dir(vlt_dir).context("failed to read .vlt directory")? {
        let entry = entry.context("failed to read .vlt directory entry")?;
        if let Some(name) = entry.file_name().to_str()
            && let Some(env_name) = name.strip_prefix("env.")
            && env_name != "rules"
        {
            envs.push(env_name.to_owned());
        }
    }

    envs.sort();
    Ok(envs)
}