mod commands;
mod error;
pub mod format;
mod tui;
use clap::{CommandFactory, Parser, Subcommand};
use format::OutputFormat;
use std::io::Read;
pub use error::{CliError, cli_exit_code, core_exit_code, format_cli_error, format_core_error};
#[derive(Parser)]
#[command(name = "envy", version, about, long_about = None)]
pub struct Cli {
#[command(subcommand)]
pub command: Option<Commands>,
#[arg(long, short = 'f', global = true, default_value = "table")]
pub format: OutputFormat,
}
#[derive(Subcommand)]
pub enum Commands {
Init,
Set {
assignment: String,
#[arg(long)]
stdin: bool,
#[arg(short = 'e', long = "env", value_name = "ENV")]
env: Option<String>,
},
Get {
key: String,
#[arg(short = 'e', long = "env", value_name = "ENV")]
env: Option<String>,
},
#[command(visible_alias = "ls")]
List {
#[arg(short = 'e', long = "env", value_name = "ENV")]
env: Option<String>,
},
#[command(visible_aliases = ["remove", "unset"])]
Rm {
key: String,
#[arg(short = 'e', long = "env", value_name = "ENV")]
env: Option<String>,
},
Run {
#[arg(short = 'e', long = "env", value_name = "ENV")]
env: Option<String>,
#[arg(last = true, required = true)]
command: Vec<String>,
},
Migrate {
file: std::path::PathBuf,
#[arg(short = 'e', long = "env", value_name = "ENV")]
env: Option<String>,
},
#[command(visible_alias = "enc")]
Encrypt {
#[arg(short = 'e', long = "env", value_name = "ENV")]
env: Option<String>,
},
#[command(visible_alias = "dec")]
Decrypt,
Export {
#[arg(
short = 'e',
long = "env",
value_name = "ENV",
default_value = "development"
)]
env: String,
},
#[command(visible_alias = "df")]
Diff {
#[arg(
short = 'e',
long = "env",
value_name = "ENV",
default_value = "development"
)]
env: String,
#[arg(long)]
reveal: bool,
},
#[command(visible_alias = "st")]
Status,
Rotate {
#[arg(short = 'e', long = "env", value_name = "ENV")]
env: Option<String>,
},
Scan {
#[arg(short = 'e', long = "env", value_name = "ENV")]
env: Option<String>,
#[arg(long)]
reveal: bool,
},
#[command(visible_alias = "au")]
Audit {
#[arg(short = 'e', long = "env", value_name = "ENV")]
env: Option<String>,
#[arg(long, default_value_t = 50)]
limit: i64,
},
#[command(hide = true)]
Completions {
shell: clap_complete::Shell,
},
Hooks {
#[command(subcommand)]
action: HooksAction,
},
}
#[derive(Subcommand)]
pub enum HooksAction {
Install {
#[arg(long)]
force: bool,
},
}
fn artifact_path(manifest_path: &std::path::Path) -> std::path::PathBuf {
manifest_path.join("envy.enc")
}
pub(super) fn vault_path() -> std::path::PathBuf {
dirs::home_dir()
.unwrap_or_else(|| std::path::PathBuf::from("."))
.join(".envy")
.join("vault.db")
}
pub fn run() -> i32 {
use clap::Parser as _;
let cli = Cli::parse();
if cli.command.is_none() {
use std::io::IsTerminal;
if std::io::stdout().is_terminal() {
return match tui::run() {
Ok(()) => 0,
Err(error) => {
eprintln!("{}", format_cli_error(&error));
cli_exit_code(&error)
}
};
}
let mut command = Cli::command();
return match command.write_long_help(&mut std::io::stderr()) {
Ok(()) => {
eprintln!();
0
}
Err(error) => {
eprintln!("error: cannot write help: {error}");
1
}
};
}
let command = cli.command.as_ref().expect("checked above");
if let Commands::Completions { shell } = command {
clap_complete::generate(*shell, &mut Cli::command(), "envy", &mut std::io::stdout());
return 0;
}
if let Some(vault_dir) = vault_path().parent() {
if let Err(e) = std::fs::create_dir_all(vault_dir) {
eprintln!("error: cannot create vault directory: {e}");
return 4;
}
}
if let Some(Commands::Init) = &cli.command {
return match commands::cmd_init() {
Ok(()) => 0,
Err(e) => {
eprintln!("{}", format_cli_error(&e));
cli_exit_code(&e)
}
};
}
let cwd = match std::env::current_dir() {
Ok(d) => d,
Err(e) => {
eprintln!("error: cannot determine current directory: {e}");
return 1;
}
};
let (manifest, manifest_path) = match crate::core::find_manifest(&cwd) {
Ok(m) => m,
Err(e) => {
eprintln!("{}", format_core_error(&e));
return core_exit_code(&e);
}
};
if let Some(Commands::Hooks { action }) = cli.command {
return match action {
HooksAction::Install { force } => {
match commands::cmd_hooks_install(&manifest_path, force) {
Ok(()) => 0,
Err(e) => {
eprintln!("{}", format_cli_error(&e));
cli_exit_code(&e)
}
}
}
};
}
let master_key = match crate::crypto::get_or_create_master_key() {
Ok(k) => k,
Err(e) => {
eprintln!("error: {e}");
return 4;
}
};
let vp = vault_path();
let vault = match crate::db::Vault::open(&vp, master_key.as_ref()) {
Ok(v) => v,
Err(e) => {
eprintln!("{}", format_cli_error(&CliError::VaultOpen(e.to_string())));
return 4;
}
};
let project_id = crate::db::ProjectId(manifest.project_id.clone());
let project_name = cwd
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("unknown")
.to_owned();
if let Err(e) = vault.ensure_project(&project_id, &project_name) {
eprintln!("error: cannot register project in vault: {e}");
return 4;
}
match cli.command.expect("command validated before vault setup") {
Commands::Init => unreachable!("Init is handled above"),
Commands::Set {
assignment,
stdin,
env,
} => {
let env = env.as_deref().unwrap_or("");
if stdin {
let mut value = String::new();
if let Err(e) = std::io::stdin().read_to_string(&mut value) {
eprintln!("error: cannot read value from stdin: {e}");
return 4;
}
if value.ends_with('\n') {
value.pop();
if value.ends_with('\r') {
value.pop();
}
}
match commands::cmd_set(&vault, &master_key, &project_id, env, &assignment, &value)
{
Ok(()) => 0,
Err(e) => {
eprintln!("{}", format_core_error(&e));
core_exit_code(&e)
}
}
} else {
match assignment.split_once('=') {
None => {
let e = CliError::InvalidAssignment(assignment);
eprintln!("{}", format_cli_error(&e));
cli_exit_code(&e)
}
Some((key, value)) => {
match commands::cmd_set(&vault, &master_key, &project_id, env, key, value) {
Ok(()) => 0,
Err(e) => {
eprintln!("{}", format_core_error(&e));
core_exit_code(&e)
}
}
}
}
}
}
Commands::Get { key, env } => {
let env = env.as_deref().unwrap_or("");
match commands::cmd_get(&vault, &master_key, &project_id, env, &key, cli.format) {
Ok(()) => 0,
Err(e) => {
if cli.format == OutputFormat::Table {
eprintln!("{}", format_cli_error(&e));
}
cli_exit_code(&e)
}
}
}
Commands::List { env } => {
let env = env.as_deref().unwrap_or("");
match commands::cmd_list(&vault, &master_key, &project_id, env, cli.format) {
Ok(()) => 0,
Err(e) => {
eprintln!("{}", format_cli_error(&e));
cli_exit_code(&e)
}
}
}
Commands::Rm { key, env } => {
let env = env.as_deref().unwrap_or("");
match commands::cmd_rm(&vault, &project_id, env, &key) {
Ok(()) => 0,
Err(e) => {
eprintln!("{}", format_core_error(&e));
core_exit_code(&e)
}
}
}
Commands::Run { env, command } => {
let env = env.as_deref().unwrap_or("");
commands::cmd_run(&vault, &master_key, &project_id, env, &command)
}
Commands::Migrate { file, env } => {
let env = env.as_deref().unwrap_or("");
match commands::cmd_migrate(&vault, &master_key, &project_id, env, &file) {
Ok(()) => 0,
Err(e) => {
eprintln!("{}", format_cli_error(&e));
cli_exit_code(&e)
}
}
}
Commands::Encrypt { env } => {
let artifact = artifact_path(&manifest_path);
let env_filter = env.as_deref();
match commands::cmd_encrypt(&vault, &master_key, &project_id, &artifact, env_filter) {
Ok(()) => 0,
Err(e) => {
eprintln!("error: {e}");
cli_exit_code(&e)
}
}
}
Commands::Decrypt => {
let artifact = artifact_path(&manifest_path);
match commands::cmd_decrypt(&vault, &master_key, &project_id, &artifact) {
Ok(()) => 0,
Err(e) => {
eprintln!("error: {e}");
cli_exit_code(&e)
}
}
}
Commands::Export { env } => {
match commands::cmd_export(&vault, &master_key, &project_id, &env, cli.format) {
Ok(()) => 0,
Err(e) => {
eprintln!("{}", format_cli_error(&e));
cli_exit_code(&e)
}
}
}
Commands::Diff { env, reveal } => {
let artifact = artifact_path(&manifest_path);
match commands::cmd_diff(
&vault,
&master_key,
&project_id,
&env,
&artifact,
cli.format,
reveal,
) {
Ok(has_diff) => {
if has_diff {
1
} else {
0
}
}
Err(e) => {
eprintln!("{}", format_cli_error(&e));
cli_exit_code(&e)
}
}
}
Commands::Status => {
let artifact = artifact_path(&manifest_path);
match commands::cmd_status(
&vault,
&project_id,
&artifact,
cli.format,
manifest.rotation_reminder_days,
) {
Ok(()) => 0,
Err(e) => {
eprintln!("{}", format_cli_error(&e));
cli_exit_code(&e)
}
}
}
Commands::Rotate { env } => {
let artifact = artifact_path(&manifest_path);
match commands::cmd_rotate(&vault, &master_key, &project_id, &artifact, env.as_deref())
{
Ok(()) => 0,
Err(e) => {
eprintln!("{}", format_cli_error(&e));
cli_exit_code(&e)
}
}
}
Commands::Scan { env, reveal } => {
match commands::cmd_scan(
&vault,
&master_key,
&project_id,
env.as_deref(),
&manifest_path,
cli.format,
reveal,
) {
Ok(has_leaks) => {
if has_leaks {
1
} else {
0
}
}
Err(e) => {
eprintln!("{}", format_cli_error(&e));
cli_exit_code(&e)
}
}
}
Commands::Audit { env, limit } => {
match commands::cmd_audit(&vault, &project_id, env.as_deref(), limit, cli.format) {
Ok(()) => 0,
Err(e) => {
eprintln!("{}", format_cli_error(&e));
cli_exit_code(&e)
}
}
}
Commands::Hooks { .. } => unreachable!("Hooks is handled above"),
Commands::Completions { .. } => unreachable!("Completions is handled above"),
}
}