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(())
},
}
}
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(())
}
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()
}