Skip to main content

envvault/cli/commands/
env_list.rs

1//! `envvault env list` — list all vault environments.
2
3use std::fs;
4
5use comfy_table::{ContentArrangement, Table};
6use console::style;
7
8use crate::cli::output;
9use crate::cli::Cli;
10use crate::errors::Result;
11
12/// Execute `envvault env list`.
13pub fn execute(cli: &Cli) -> Result<()> {
14    let cwd = std::env::current_dir()?;
15    let vault_dir = cwd.join(&cli.vault_dir);
16
17    if !vault_dir.exists() {
18        output::info("No vault directory found.");
19        output::tip("Run `envvault init` to create a vault.");
20        return Ok(());
21    }
22
23    let mut envs = list_environments(&vault_dir)?;
24    envs.sort_by(|a, b| a.name.cmp(&b.name));
25
26    if envs.is_empty() {
27        output::info("No environments found.");
28        output::tip("Run `envvault init` to create your first vault.");
29        return Ok(());
30    }
31
32    let mut table = Table::new();
33    table.set_content_arrangement(ContentArrangement::Dynamic);
34    table.set_header(vec!["Environment", "Size", "Active"]);
35
36    for env in &envs {
37        let active = if env.name == cli.env {
38            style("*").green().bold().to_string()
39        } else {
40            String::new()
41        };
42
43        table.add_row(vec![env.name.clone(), format_size(env.size), active]);
44    }
45
46    output::info(&format!("{} environment(s) found:", envs.len()));
47    println!("{table}");
48
49    Ok(())
50}
51
52/// Information about a vault environment.
53pub struct EnvInfo {
54    pub name: String,
55    pub size: u64,
56}
57
58/// Scan a vault directory for `*.vault` files.
59pub fn list_environments(vault_dir: &std::path::Path) -> Result<Vec<EnvInfo>> {
60    let mut envs = Vec::new();
61
62    let entries = fs::read_dir(vault_dir)?;
63    for entry in entries {
64        let entry = entry?;
65        let path = entry.path();
66
67        if let Some(ext) = path.extension() {
68            if ext == "vault" {
69                if let Some(stem) = path.file_stem() {
70                    let name = stem.to_string_lossy().to_string();
71                    let size = entry.metadata().map(|m| m.len()).unwrap_or(0);
72                    envs.push(EnvInfo { name, size });
73                }
74            }
75        }
76    }
77
78    Ok(envs)
79}
80
81/// Format file size in human-readable form.
82#[allow(clippy::cast_precision_loss)] // File sizes are well within f64 precision range
83fn format_size(bytes: u64) -> String {
84    if bytes < 1024 {
85        format!("{bytes} B")
86    } else if bytes < 1024 * 1024 {
87        format!("{:.1} KB", bytes as f64 / 1024.0)
88    } else {
89        format!("{:.1} MB", bytes as f64 / (1024.0 * 1024.0))
90    }
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96
97    #[test]
98    fn format_size_bytes() {
99        assert_eq!(format_size(512), "512 B");
100    }
101
102    #[test]
103    fn format_size_kilobytes() {
104        assert_eq!(format_size(2048), "2.0 KB");
105    }
106
107    #[test]
108    fn format_size_megabytes() {
109        assert_eq!(format_size(2 * 1024 * 1024), "2.0 MB");
110    }
111
112    #[test]
113    fn list_environments_from_dir() {
114        let dir = tempfile::TempDir::new().unwrap();
115        // Create some .vault files.
116        std::fs::write(dir.path().join("dev.vault"), b"test").unwrap();
117        std::fs::write(dir.path().join("staging.vault"), b"test data").unwrap();
118        std::fs::write(dir.path().join("not-a-vault.txt"), b"nope").unwrap();
119
120        let envs = list_environments(dir.path()).unwrap();
121        assert_eq!(envs.len(), 2);
122
123        let names: Vec<&str> = envs.iter().map(|e| e.name.as_str()).collect();
124        assert!(names.contains(&"dev"));
125        assert!(names.contains(&"staging"));
126    }
127}