secenv 0.0.0

Secure environments.
pub mod args;
pub mod config;
pub mod pgp;
pub mod reference;

use {
    anyhow::{
        Context,
        Result,
    },
    args::ManualFormat,
    std::collections::HashMap,
};

#[tokio::main]
async fn main() -> Result<()> {
    let cmd = crate::args::ClapArgumentLoader::load()?;

    match cmd.command {
        | crate::args::Command::Manual { path, format } => {
            std::fs::create_dir_all(&path)
                .with_context(|| format!("Failed to create directory: {}", path.display()))?;
            match format {
                | ManualFormat::Manpages => {
                    reference::build_manpages(&path)?;
                },
                | ManualFormat::Markdown => {
                    reference::build_markdown(&path)?;
                },
            }
            Ok(())
        },
        | crate::args::Command::Autocomplete { path, shell } => {
            std::fs::create_dir_all(&path)
                .with_context(|| format!("Failed to create directory: {}", path.display()))?;
            reference::build_shell_completion(&path, &shell)?;
            Ok(())
        },
        | crate::args::Command::Unlock { profile, command } => {
            let mut env_vars = HashMap::new();
            
            for (key, value) in profile.vars.iter() {
                match value.get_value() {
                    | Ok(val) => {
                        env_vars.insert(key.clone(), val);
                    },
                    | Err(e) => {
                        eprintln!("Error decrypting {}: {}", key, e);
                        return Err(e);
                    },
                }
            }

            match command {
                Some(cmd_args) if !cmd_args.is_empty() => {
                    execute_command_with_env(&cmd_args, &env_vars, &profile.propagate)
                },
                _ => {
                    for (key, value) in env_vars {
                        println!("{}={}", key, value);
                    }
                    Ok(())
                }
            }
        },
        | crate::args::Command::Init { path, force } => {
            if path.exists() && !force {
                return Err(anyhow::anyhow!(
                    "Config file '{}' already exists. Use --force to overwrite.",
                    path.display()
                ));
            }

            let example_config = generate_example_config();
            
            std::fs::write(&path, example_config)
                .with_context(|| format!("Failed to write config file: {}", path.display()))?;
            
            println!("Created example configuration file: {}", path.display());
            println!("Edit the file to add your own variables and PGP keys.");
            Ok(())
        },
    }
}

/// Execute a command with the specified environment variables
fn execute_command_with_env(
    cmd_args: &[String], 
    env_vars: &HashMap<String, String>, 
    propagate_patterns: &Option<Vec<String>>
) -> Result<()> {
    use std::process::Command;
    use regex::Regex;
    
    if cmd_args.is_empty() {
        return Err(anyhow::anyhow!("No command provided"));
    }

    let program = &cmd_args[0];
    let args = &cmd_args[1..];

    let mut command = Command::new(program);
    command.args(args);

    match propagate_patterns {
        None => {
            for (key, value) in env_vars {
                command.env(key, value);
            }
        }
        Some(patterns) => {
            command.env_clear();
            
            let compiled_patterns: Result<Vec<Regex>, _> = patterns
                .iter()
                .map(|pattern| Regex::new(pattern))
                .collect();
            
            let compiled_patterns = compiled_patterns
                .with_context(|| "Failed to compile regex patterns")?;
            
            for (env_key, env_value) in std::env::vars() {
                for pattern in &compiled_patterns {
                    if pattern.is_match(&env_key) {
                        command.env(&env_key, &env_value);
                        break;
                    }
                }
            }
            
            for (key, value) in env_vars {
                command.env(key, value);
            }
        }
    }

    let status = command.status()
        .with_context(|| format!("Failed to execute command: {}", program))?;

    if !status.success() {
        if let Some(code) = status.code() {
            std::process::exit(code);
        } else {
            std::process::exit(1);
        }
    }

    Ok(())
}

/// Generate an example configuration file content
fn generate_example_config() -> String {
    r#"# secenv configuration file
# 
# This file contains environment variable definitions organized by profiles.
# You can encrypt sensitive values using PGP encryption.

# Define reusable PGP key fingerprints as YAML anchors
.defaultkey: &defaultkey "YOUR-PGP-KEY-FINGERPRINT-HERE"

profiles:
  # Development profile
  dev:
    # propagate: omitted (default) - propagates all existing environment variables
    vars:
      # Literal string value
      APP_NAME: !literal "myapp-dev"
      
      # Reference to an environment variable
      HOME_DIR: !environment "HOME"
      
      # Read content from a file
      CONFIG_JSON_CONTENT: !file "/etc/myapp/config.json"
      
      # PGP encrypted value
      # To create: echo "your-secret" | gpg --encrypt --armor --recipient YOUR-EMAIL
      DATABASE_PASSWORD: !pgp
        key: *defaultkey
        value: |
          -----BEGIN PGP MESSAGE-----
          
          Replace this with your actual PGP encrypted message.
          Use: echo "your-secret" | gpg --encrypt --armor --recipient your@email.com
          Then copy the entire output including BEGIN/END lines.
          
          -----END PGP MESSAGE-----

  # Production profile with selective environment propagation
  production:
    # Only propagate environment variables matching these regex patterns
    propagate:
      - "^PATH$"           # Exact match for PATH
      - "^HOME$"           # Exact match for HOME
      - "^LANG.*"          # Any variable starting with LANG
      - "^LC_.*"           # Any locale variable
    vars:
      APP_NAME: !literal "myapp"
      
      # Use a different key for production
      DATABASE_PASSWORD: !pgp
        key: "PRODUCTION-PGP-KEY-FINGERPRINT"
        value: |
          -----BEGIN PGP MESSAGE-----
          
          Production encrypted value here
          
          -----END PGP MESSAGE-----

  # Staging profile with no environment propagation
  staging:
    propagate: []  # Empty list = clear all environment variables
    vars:
      APP_NAME: !literal "myapp-staging"

# Usage:
# secenv unlock                    # Uses 'dev' profile by default
# secenv unlock --profile production
# secenv unlock --config /path/to/config.yaml --profile staging
"#.to_string()
}