use clap::{CommandFactory, Parser, Subcommand};
use clap_complete::Shell;
use zorath_env::config::Config;
use zorath_env::commands;
#[derive(Parser, Debug)]
#[command(name="zenv", version, about="Validate .env files with a schema and generate docs.")]
struct Cli {
#[command(subcommand)]
command: Command,
#[arg(long, global = true)]
config: Option<String>,
#[arg(long, global = true, conflicts_with = "quiet")]
verbose: bool,
#[arg(long, global = true, conflicts_with = "verbose")]
quiet: bool,
#[arg(long, global = true)]
no_color: bool,
}
#[derive(Subcommand, Debug)]
enum Command {
#[command(after_help = "\
Examples:
zenv check Validate using defaults
zenv check --schema custom.json Use custom schema
zenv check --detect-secrets Include secret detection
zenv check --watch Watch for file changes
zenv check --env .env.local Validate specific env file
zenv check --format json JSON output for CI/CD
zenv check --allow-missing-env Schema-only validation (no .env required)
Security options for remote schemas:
zenv check --schema https://... --verify-hash abc123...
zenv check --schema https://... --ca-cert /path/to/ca.pem
Config file:
Create .zenvrc in your project root to set defaults:
{\"schema\": \"env.schema.json\", \"detect_secrets\": true}")]
Check {
#[arg(long)]
env: Option<String>,
#[arg(long)]
schema: Option<String>,
#[arg(long)]
allow_missing_env: Option<bool>,
#[arg(long, num_args = 0..=1, default_missing_value = "true")]
detect_secrets: Option<bool>,
#[arg(long)]
no_cache: Option<bool>,
#[arg(long, default_value_t = false)]
watch: bool,
#[arg(long)]
format: Option<String>,
#[arg(long)]
verify_hash: Option<String>,
#[arg(long)]
ca_cert: Option<String>,
},
#[command(after_help = "\
Examples:
zenv docs Generate markdown docs
zenv docs --format json Generate JSON output
zenv docs --schema https://... Use remote schema
zenv docs --schema https://... --verify-hash abc123...")]
Docs {
#[arg(long)]
schema: Option<String>,
#[arg(short = 'f', long)]
format: Option<String>,
#[arg(long)]
no_cache: Option<bool>,
#[arg(long)]
verify_hash: Option<String>,
#[arg(long)]
ca_cert: Option<String>,
},
#[command(after_help = "\
Examples:
zenv init Create schema from .env.example
zenv init --example .env Use .env as source
zenv init --preset nextjs Use Next.js preset
zenv init --preset rails Use Rails preset
zenv init --list-presets Show available presets
Available presets:
nextjs, rails, django, fastapi, express, laravel")]
Init {
#[arg(long, default_value = ".env.example")]
example: String,
#[arg(long)]
schema: Option<String>,
#[arg(long)]
preset: Option<String>,
#[arg(long, default_value_t = false)]
list_presets: bool,
},
#[command(after_help = "\
Examples:
zenv version Show current version
zenv version --check-update Check for newer version")]
Version {
#[arg(long, default_value_t = false)]
check_update: bool,
},
#[command(after_help = "\
Examples:
zenv completions bash Generate bash completions
zenv completions zsh Generate zsh completions
zenv completions fish Generate fish completions
zenv completions powershell Generate PowerShell completions
Installation:
bash: zenv completions bash >> ~/.bashrc
zsh: zenv completions zsh >> ~/.zshrc
fish: zenv completions fish > ~/.config/fish/completions/zenv.fish")]
Completions {
#[arg(value_enum)]
shell: Shell,
},
#[command(after_help = "\
Examples:
zenv example Print to stdout
zenv example --output .env.example Write to file
zenv example --include-defaults Include schema defaults
zenv example --schema https://... --verify-hash abc123...")]
Example {
#[arg(long)]
schema: Option<String>,
#[arg(short = 'o', long)]
output: Option<String>,
#[arg(long, default_value_t = false)]
include_defaults: bool,
#[arg(long)]
no_cache: Option<bool>,
#[arg(long)]
verify_hash: Option<String>,
#[arg(long)]
ca_cert: Option<String>,
},
#[command(after_help = "\
Examples:
zenv diff .env.local .env.prod Compare two env files
zenv diff .env.dev .env --schema s.json With schema validation
zenv diff .env.a .env.b --format json JSON output for CI
Remote schema with integrity verification:
zenv diff .env.a .env.b --schema https://... --verify-hash abc123...")]
Diff {
env_a: String,
env_b: String,
#[arg(long)]
schema: Option<String>,
#[arg(long)]
format: Option<String>,
#[arg(long)]
no_cache: Option<bool>,
#[arg(long)]
verify_hash: Option<String>,
#[arg(long)]
ca_cert: Option<String>,
},
#[command(after_help = "\
Examples:
zenv fix --dry-run Preview fixes without changing files
zenv fix Apply fixes (creates .env.backup)
zenv fix --remove-unknown Also remove keys not in schema
Remote schema with integrity verification:
zenv fix --schema https://... --verify-hash abc123...
What it fixes:
- Adds missing required variables (with schema defaults)
- Removes unknown keys (with --remove-unknown)
What it reports but doesn't fix:
- Invalid types (can't guess correct value)
- Invalid enum values (needs human choice)")]
Fix {
#[arg(long)]
env: Option<String>,
#[arg(long)]
schema: Option<String>,
#[arg(long, default_value_t = false)]
remove_unknown: bool,
#[arg(long, default_value_t = false)]
dry_run: bool,
#[arg(long)]
no_cache: Option<bool>,
#[arg(long)]
verify_hash: Option<String>,
#[arg(long)]
ca_cert: Option<String>,
},
#[command(after_help = "\
Examples:
zenv scan Scan current directory
zenv scan --path ./src Scan specific directory
zenv scan --show-unused Show vars in schema but not in code
zenv scan --show-paths Show file:line for all found vars
zenv scan --format json JSON output for CI
Supported languages:
JavaScript/TypeScript, Python, Go, Rust, PHP, Ruby, Java, C#, Kotlin")]
Scan {
#[arg(long, default_value = ".")]
path: String,
#[arg(long)]
schema: Option<String>,
#[arg(long, default_value_t = false)]
show_unused: bool,
#[arg(long, default_value_t = false)]
show_paths: bool,
#[arg(long)]
format: Option<String>,
#[arg(long)]
no_cache: Option<bool>,
#[arg(long)]
verify_hash: Option<String>,
#[arg(long)]
ca_cert: Option<String>,
},
#[command(after_help = "\
Examples:
zenv cache list List cached schemas
zenv cache stats Show cache statistics
zenv cache clear Clear all cached schemas
zenv cache clear https://... Clear specific URL
zenv cache path Show cache directory")]
Cache {
#[command(subcommand)]
action: CacheAction,
},
#[command(after_help = "\
Examples:
zenv export --format shell Shell script (export FOO=\"bar\")
zenv export --format docker Dockerfile (ENV FOO=bar)
zenv export --format k8s Kubernetes ConfigMap YAML
zenv export --format json JSON object
zenv export --format systemd systemd Environment directives
zenv export --format dotenv Standard .env format
zenv export --format github-secrets GitHub CLI commands (gh secret set)
zenv export --env .env.local Export specific env file
zenv export --schema s.json Only export vars defined in schema
zenv export -f shell -o setup.sh Write to file
Aliases:
shell: bash, sh
docker: dockerfile
k8s: kubernetes, configmap
systemd: service
dotenv: env
github-secrets: gh-secrets, github")]
Export {
#[arg(long)]
env: Option<String>,
#[arg(short = 'f', long)]
format: Option<String>,
#[arg(long)]
schema: Option<String>,
#[arg(short = 'o', long)]
output: Option<String>,
#[arg(long)]
no_cache: Option<bool>,
#[arg(long)]
verify_hash: Option<String>,
#[arg(long)]
ca_cert: Option<String>,
},
#[command(after_help = "\
Examples:
zenv doctor Run full health check
zenv doctor --schema custom.json Use custom schema path
zenv doctor --env .env.local Use specific env file
zenv doctor --schema https://... --verify-hash abc123...
zenv doctor --schema https://... --ca-cert /path/to/ca.pem
Checks:
- Schema file exists and is valid
- .env file exists and parses correctly
- Config file (.zenvrc) is valid JSON
- Remote schema cache is accessible
- Validation passes (if schema and env exist)
Each check shows:
[OK] - No issues
[WARN] - Non-critical issue
[ERROR] - Critical issue that needs attention")]
Doctor {
#[arg(long)]
env: Option<String>,
#[arg(long)]
schema: Option<String>,
#[arg(long)]
no_cache: Option<bool>,
#[arg(long)]
verify_hash: Option<String>,
#[arg(long)]
ca_cert: Option<String>,
},
#[command(after_help = "\
Examples:
zenv template github Output GitHub Actions workflow
zenv template gitlab -o .gitlab-ci.yml Write GitLab CI config to file
zenv template circleci Output CircleCI config
zenv template --list List available templates
zenv template github --use-binary Use binary download (faster CI)
Aliases:
github: gh, github-actions
gitlab: gl, gitlab-ci
circleci: circle")]
Template {
#[arg(default_value = "github")]
name: String,
#[arg(short = 'o', long)]
output: Option<String>,
#[arg(long)]
list: bool,
#[arg(long)]
use_binary: bool,
},
}
#[derive(Subcommand, Debug)]
enum CacheAction {
List,
Clear {
url: Option<String>,
},
Path,
Stats,
}
fn main() {
let cli = Cli::parse();
if cli.quiet {
std::env::set_var("ZENV_QUIET", "1");
}
if cli.verbose {
std::env::set_var("ZENV_VERBOSE", "1");
}
let config = Config::load_from(cli.config.as_deref()).unwrap_or_default();
if cli.no_color || config.no_color_or(false) {
std::env::set_var("NO_COLOR", "1");
}
let result = match cli.command {
Command::Check { env, schema, allow_missing_env, detect_secrets, no_cache, watch, format, verify_hash, ca_cert } => {
let env = env.unwrap_or_else(|| config.env_or(".env"));
let schema = schema.unwrap_or_else(|| config.schema_or("env.schema.json"));
let allow_missing_env = allow_missing_env.unwrap_or_else(|| config.allow_missing_env_or(false));
let detect_secrets = detect_secrets.unwrap_or_else(|| config.detect_secrets_or(false));
let no_cache = no_cache.unwrap_or_else(|| config.no_cache_or(false));
let verify_hash = verify_hash.or_else(|| config.verify_hash());
let ca_cert = ca_cert.or_else(|| config.ca_cert());
let format = format.unwrap_or_else(|| config.format_or("text"));
commands::check::run(&env, &schema, allow_missing_env, detect_secrets, no_cache, watch, &format, verify_hash.as_deref(), ca_cert.as_deref())
}
Command::Docs { schema, format, no_cache, verify_hash, ca_cert } => {
let schema = schema.unwrap_or_else(|| config.schema_or("env.schema.json"));
let no_cache = no_cache.unwrap_or_else(|| config.no_cache_or(false));
let verify_hash = verify_hash.or_else(|| config.verify_hash());
let ca_cert = ca_cert.or_else(|| config.ca_cert());
let format = format.unwrap_or_else(|| config.format_or("markdown"));
commands::docs::run(&schema, &format, no_cache, verify_hash.as_deref(), ca_cert.as_deref())
}
Command::Init { example, schema, preset, list_presets } => {
let schema = schema.unwrap_or_else(|| config.schema_or("env.schema.json"));
commands::init::run_with_options(&example, &schema, preset.as_deref(), list_presets)
}
Command::Version { check_update } => commands::version::run(check_update),
Command::Completions { shell } => commands::completions::run(shell, &mut Cli::command()),
Command::Example { schema, output, include_defaults, no_cache, verify_hash, ca_cert } => {
let schema = schema.unwrap_or_else(|| config.schema_or("env.schema.json"));
let no_cache = no_cache.unwrap_or_else(|| config.no_cache_or(false));
let verify_hash = verify_hash.or_else(|| config.verify_hash());
let ca_cert = ca_cert.or_else(|| config.ca_cert());
commands::example::run(&schema, output.as_deref(), include_defaults, no_cache, verify_hash.as_deref(), ca_cert.as_deref())
}
Command::Diff { env_a, env_b, schema, format, no_cache, verify_hash, ca_cert } => {
let no_cache = no_cache.unwrap_or_else(|| config.no_cache_or(false));
let verify_hash = verify_hash.or_else(|| config.verify_hash());
let ca_cert = ca_cert.or_else(|| config.ca_cert());
let format = format.unwrap_or_else(|| config.format_or("text"));
commands::diff::run(&env_a, &env_b, schema.as_deref(), &format, no_cache, verify_hash.as_deref(), ca_cert.as_deref())
}
Command::Fix { env, schema, remove_unknown, dry_run, no_cache, verify_hash, ca_cert } => {
let env = env.unwrap_or_else(|| config.env_or(".env"));
let schema = schema.unwrap_or_else(|| config.schema_or("env.schema.json"));
let no_cache = no_cache.unwrap_or_else(|| config.no_cache_or(false));
let verify_hash = verify_hash.or_else(|| config.verify_hash());
let ca_cert = ca_cert.or_else(|| config.ca_cert());
commands::fix::run(&env, &schema, remove_unknown, dry_run, no_cache, verify_hash.as_deref(), ca_cert.as_deref())
}
Command::Scan { path, schema, show_unused, show_paths, format, no_cache, verify_hash, ca_cert } => {
let schema = schema.unwrap_or_else(|| config.schema_or("env.schema.json"));
let no_cache = no_cache.unwrap_or_else(|| config.no_cache_or(false));
let verify_hash = verify_hash.or_else(|| config.verify_hash());
let ca_cert = ca_cert.or_else(|| config.ca_cert());
let format = format.unwrap_or_else(|| config.format_or("text"));
commands::scan::run(&path, &schema, show_unused, show_paths, &format, no_cache, verify_hash.as_deref(), ca_cert.as_deref())
}
Command::Cache { action } => match action {
CacheAction::List => commands::cache::run_list(),
CacheAction::Clear { url } => commands::cache::run_clear(url.as_deref()),
CacheAction::Path => commands::cache::run_path(),
CacheAction::Stats => commands::cache::run_stats(),
},
Command::Export { env, format, schema, output, no_cache, verify_hash, ca_cert } => {
let env = env.unwrap_or_else(|| config.env_or(".env"));
let no_cache = no_cache.unwrap_or_else(|| config.no_cache_or(false));
let verify_hash = verify_hash.or_else(|| config.verify_hash());
let ca_cert = ca_cert.or_else(|| config.ca_cert());
let format = format.unwrap_or_else(|| config.format_or("shell"));
commands::export::run(&env, schema.as_deref(), &format, output.as_deref(), no_cache, verify_hash.as_deref(), ca_cert.as_deref())
}
Command::Doctor { env, schema, no_cache, verify_hash, ca_cert } => {
let env = env.unwrap_or_else(|| config.env_or(".env"));
let schema = schema.unwrap_or_else(|| config.schema_or("env.schema.json"));
let no_cache = no_cache.unwrap_or_else(|| config.no_cache_or(false));
let verify_hash = verify_hash.or_else(|| config.verify_hash());
let ca_cert = ca_cert.or_else(|| config.ca_cert());
commands::doctor::run(&env, &schema, no_cache, verify_hash.as_deref(), ca_cert.as_deref())
}
Command::Template { name, output, list, use_binary } => {
commands::template::run(&name, output.as_deref(), list, use_binary)
}
};
if let Err(e) = result {
eprintln!("zenv error: {e}");
std::process::exit(e.exit_code());
}
}