Skip to main content

envvault/cli/commands/
init.rs

1//! `envvault init` — create a new vault, optionally importing .env secrets.
2
3use std::fs;
4use std::io::{BufRead, BufReader};
5use std::path::Path;
6
7use dialoguer::Confirm;
8
9use crate::cli::env_parser::parse_env_line;
10use crate::cli::output;
11use crate::cli::{load_keyfile, prompt_new_password, Cli};
12use crate::config::Settings;
13use crate::errors::{EnvVaultError, Result};
14use crate::vault::VaultStore;
15
16/// Execute the `init` command.
17pub fn execute(cli: &Cli) -> Result<()> {
18    let cwd = std::env::current_dir()?;
19    let vault_dir = cwd.join(&cli.vault_dir);
20    let env = &cli.env;
21    let vault_path = vault_dir.join(format!("{env}.vault"));
22
23    // 1. Create the vault directory if it doesn't exist.
24    if !vault_dir.exists() {
25        fs::create_dir_all(&vault_dir)?;
26        let dir_display = vault_dir.display();
27        output::info(&format!("Created vault directory: {dir_display}"));
28    }
29
30    // 2. Check if a vault already exists for this environment.
31    if vault_path.exists() {
32        output::tip("Use `envvault set` to add secrets to the existing vault.");
33        return Err(EnvVaultError::VaultAlreadyExists(vault_path));
34    }
35
36    // 3. Prompt for a new password (with confirmation).
37    let password = prompt_new_password()?;
38
39    // 4. Load optional keyfile and settings, then create the vault file.
40    let keyfile = load_keyfile(cli)?;
41    let settings = Settings::load(&cwd)?;
42    let mut store = VaultStore::create(
43        &vault_path,
44        password.as_bytes(),
45        &cli.env,
46        Some(&settings.argon2_params()),
47        keyfile.as_deref(),
48    )?;
49    if keyfile.is_some() {
50        output::info("Vault created with keyfile — you must pass --keyfile on every command.");
51    }
52    output::success(&format!(
53        "Vault created for '{}' environment at {}",
54        cli.env,
55        vault_path.display()
56    ));
57
58    // 5. Auto-detect .env file and offer to import it.
59    let env_file = cwd.join(".env");
60    if env_file.exists() {
61        let should_import = Confirm::new()
62            .with_prompt("Found .env file. Import secrets from it?")
63            .default(true)
64            .interact()
65            .map_err(|e| {
66                EnvVaultError::CommandFailed(format!("failed to read confirmation: {e}"))
67            })?;
68
69        if should_import {
70            let count = import_env_file(&env_file, &mut store)?;
71            store.save()?;
72            output::success(&format!("Imported {count} secrets from .env"));
73        }
74    }
75
76    // 6. Patch .gitignore to exclude the vault directory.
77    crate::cli::gitignore::patch_gitignore(&cwd, &format!("{}/", cli.vault_dir));
78
79    // 7. Install pre-commit git hook to catch accidental secret leaks.
80    match crate::git::install_hook(&cwd) {
81        Ok(crate::git::InstallResult::Installed) => {
82            output::info("Installed pre-commit hook to detect secret leaks.");
83        }
84        Ok(crate::git::InstallResult::ExistingHookFound) => {
85            output::warning("A pre-commit hook already exists — EnvVault hook was not installed.");
86        }
87        Ok(
88            crate::git::InstallResult::AlreadyInstalled | crate::git::InstallResult::NotAGitRepo,
89        )
90        | Err(_) => {} // Non-fatal, skip silently.
91    }
92
93    // 8. Audit log.
94    crate::audit::log_audit(cli, "init", None, Some("vault created"));
95
96    // 9. Show helpful tips.
97    output::tip("Run `envvault set <KEY>` to add a secret.");
98    output::tip("Run `envvault list` to see all secrets.");
99    output::tip("Run `envvault run -- <command>` to inject secrets into a command.");
100
101    Ok(())
102}
103
104/// Parse a .env file and import each KEY=VALUE pair into the vault.
105/// Returns the number of secrets imported.
106///
107/// Handles the `export` prefix that some .env files use:
108///   export DATABASE_URL=postgres://...
109fn import_env_file(path: &Path, store: &mut VaultStore) -> Result<usize> {
110    let file = fs::File::open(path)?;
111    let reader = BufReader::new(file);
112    let mut count = 0;
113
114    for line in reader.lines() {
115        let line = line?;
116
117        if let Some((key, value)) = parse_env_line(&line) {
118            store.set_secret(key, value)?;
119            count += 1;
120        }
121    }
122
123    Ok(count)
124}