From f87bc4158c428823d85e7133a69d62f912ffd52b Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Sat, 9 May 2026 20:09:29 +0300 Subject: [PATCH] eh: add missing `eh dev` command; add `--ask` for sensitive operations Signed-off-by: NotAShelf Change-Id: If48f4d337c62dad669af97f9e97c1cb76a6a6964 --- crates/xtask/src/main.rs | 5 ++++- eh/src/commands/mod.rs | 2 ++ eh/src/lib.rs | 25 +++++++++++++++++++++ eh/src/main.rs | 33 ++++++++++++++++++--------- eh/src/util.rs | 48 +++++++++++++++++++++++++++++++++++++++- 5 files changed, 101 insertions(+), 12 deletions(-) diff --git a/crates/xtask/src/main.rs b/crates/xtask/src/main.rs index 0254d57..94c95a9 100644 --- a/crates/xtask/src/main.rs +++ b/crates/xtask/src/main.rs @@ -45,6 +45,7 @@ enum Binary { Nd, Ni, Nu, + Dev, } impl Binary { @@ -56,6 +57,7 @@ impl Binary { Self::Nd => "nd", Self::Ni => "ni", Self::Nu => "nu", + Self::Dev => "dev", } } } @@ -103,6 +105,7 @@ fn create_multicall_binaries( Binary::Nd, Binary::Ni, Binary::Nu, + Binary::Dev, ]; let bin_path = Path::new(bin_dir); @@ -166,7 +169,7 @@ fn generate_completions( println!("completion file generated: {}", completion_file.display()); // Create symlinks for multicall binaries - let multicall_names = ["nb", "nd", "ni", "nr", "ns", "nu"]; + let multicall_names = ["dev", "nb", "nd", "ni", "nr", "ns", "nu"]; for name in &multicall_names { let symlink_path = output_dir.join(format!("{name}.{shell}")); if symlink_path.exists() { diff --git a/eh/src/commands/mod.rs b/eh/src/commands/mod.rs index eeacf35..36fb4aa 100644 --- a/eh/src/commands/mod.rs +++ b/eh/src/commands/mod.rs @@ -337,6 +337,7 @@ pub fn handle_nix_command( fixer: &dyn NixFileFixer, classifier: &dyn NixErrorClassifier, cfg: &crate::config::CommandConfig, + ask: bool, ) -> Result { let intercept_env = matches!(command, "run" | "shell"); handle_nix_with_retry( @@ -347,6 +348,7 @@ pub fn handle_nix_command( classifier, intercept_env, cfg, + ask, ) } diff --git a/eh/src/lib.rs b/eh/src/lib.rs index 532625a..2d00f4f 100644 --- a/eh/src/lib.rs +++ b/eh/src/lib.rs @@ -6,6 +6,17 @@ pub mod util; pub use clap::{CommandFactory, Parser, Subcommand}; pub use error::{EhError, Result}; +/// Supported shells for completion generation. +#[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum)] +pub enum Shell { + /// Bash shell + Bash, + /// Zsh shell + Zsh, + /// Fish shell + Fish, +} + #[derive(Parser)] #[command(name = "eh")] #[command(about = "Ergonomic Nix helper", long_about = None)] @@ -19,21 +30,29 @@ pub struct Cli { pub enum Command { /// Run a Nix derivation Run { + #[arg(short, long, default_value = "false")] + ask: bool, #[arg(trailing_var_arg = true)] args: Vec, }, /// Enter a Nix shell Shell { + #[arg(short, long, default_value = "false")] + ask: bool, #[arg(trailing_var_arg = true)] args: Vec, }, /// Build a Nix derivation Build { + #[arg(short, long, default_value = "false")] + ask: bool, #[arg(trailing_var_arg = true)] args: Vec, }, /// Enter a Nix development shell Develop { + #[arg(short, long, default_value = "false")] + ask: bool, #[arg(trailing_var_arg = true)] args: Vec, }, @@ -47,4 +66,10 @@ pub enum Command { #[arg(trailing_var_arg = true)] args: Vec, }, + /// Generate shell completions + Completion { + /// Shell to generate completions for + #[arg(value_enum)] + shell: Shell, + }, } diff --git a/eh/src/main.rs b/eh/src/main.rs index 38cf7c7..6b66951 100644 --- a/eh/src/main.rs +++ b/eh/src/main.rs @@ -1,6 +1,7 @@ use std::{env, path::Path}; -use eh::{Cli, Command, CommandFactory, Parser}; +use clap_complete::{generate, Shell}; +use eh::{Cli, Command, CommandFactory, Parser, Shell as EhShell}; use yansi::Paint; mod commands; @@ -26,7 +27,7 @@ fn main() { } } -fn handle_command(command: &str, args: &[String]) -> error::Result { +fn handle_command(command: &str, args: &[String], ask: bool) -> error::Result { let hash_extractor = util::RegexHashExtractor; let fixer = util::DefaultNixFileFixer; let classifier = util::DefaultNixErrorClassifier; @@ -45,6 +46,7 @@ fn handle_command(command: &str, args: &[String]) -> error::Result { &fixer, &classifier, &cmd_cfg, + ask, ) }, _ => unreachable!(), @@ -61,7 +63,7 @@ fn dispatch_multicall( "nr" => "run", "ns" => "shell", "nb" => "build", - "nd" => "develop", + "nd" | "dev" => "develop", "ni" => "info", "nu" => "update", _ => return None, @@ -87,7 +89,7 @@ fn dispatch_multicall( return Some(Ok(0)); } - Some(handle_command(subcommand, &rest)) + Some(handle_command(subcommand, &rest, false)) } fn run_app() -> error::Result { @@ -106,17 +108,28 @@ fn run_app() -> error::Result { let cli = Cli::parse(); match cli.command { - Some(Command::Run { args }) => handle_command("run", &args), + Some(Command::Run { ask, args }) => handle_command("run", &args, ask), - Some(Command::Shell { args }) => handle_command("shell", &args), + Some(Command::Shell { ask, args }) => handle_command("shell", &args, ask), - Some(Command::Build { args }) => handle_command("build", &args), + Some(Command::Build { ask, args }) => handle_command("build", &args, ask), - Some(Command::Develop { args }) => handle_command("develop", &args), + Some(Command::Develop { ask, args }) => handle_command("develop", &args, ask), - Some(Command::Info { args }) => handle_command("info", &args), + Some(Command::Info { args }) => handle_command("info", &args, false), - Some(Command::Update { args }) => handle_command("update", &args), + Some(Command::Update { args }) => handle_command("update", &args, false), + + Some(Command::Completion { shell }) => { + let mut cmd = Cli::command(); + let shell: Shell = match shell { + EhShell::Bash => Shell::Bash, + EhShell::Zsh => Shell::Zsh, + EhShell::Fish => Shell::Fish, + }; + generate(shell, &mut cmd, "eh", &mut std::io::stdout()); + Ok(0) + }, None => { Cli::command().print_help()?; diff --git a/eh/src/util.rs b/eh/src/util.rs index 9076834..80e2710 100644 --- a/eh/src/util.rs +++ b/eh/src/util.rs @@ -486,6 +486,7 @@ pub fn handle_nix_with_retry( classifier: &dyn NixErrorClassifier, interactive: bool, cfg: &crate::config::CommandConfig, + ask: bool, ) -> Result { validate_nix_args(args)?; @@ -501,6 +502,25 @@ pub fn handle_nix_with_retry( reason: reason.to_string(), }); } + + // With --ask, prompt before auto-retry + if ask && std::io::stdin().is_terminal() { + let choices = ["Yes, retry with --impure", "No, cancel"]; + let idx = dialoguer::Select::new() + .with_prompt(format!( + "Package {} requires `--impure` ({}). Retry?", + pkg.bold(), + reason.bold() + )) + .items(&choices) + .default(0) + .interact() + .map_err(|e| EhError::Io(std::io::Error::other(e)))?; + if idx != 0 { + return Err(EhError::ProcessExit { code: 1 }); + } + } + print_retry_msg(pkg, reason, env_var); let mut retry_cmd = NixCommand::new(subcommand) .print_build_logs(true) @@ -540,7 +560,8 @@ pub fn handle_nix_with_retry( if let Some(new_hash) = hash_extractor.extract_hash(&stderr) { let old_hash = hash_extractor.extract_old_hash(&stderr); - // Ask for confirmation before fixing hash (skip in non-interactive mode) + // Ask for confirmation before fixing hash. + // With --ask: prompt always (error if no TTY). Without --ask: prompt only in TTY mode. let should_fix = if std::io::stdin().is_terminal() { dialoguer::Confirm::new() .with_prompt(format!( @@ -550,6 +571,12 @@ pub fn handle_nix_with_retry( .default(true) .interact() .map_err(|e| EhError::Io(std::io::Error::other(e)))? + } else if ask { + return Err(EhError::Io( + std::io::Error::other( + "cannot prompt for hash fix confirmation in non-interactive mode (no TTY)" + ) + )); } else { log_warn!( "{}: hash mismatch detected in non-interactive mode, skipping auto-fix", @@ -611,6 +638,25 @@ pub fn handle_nix_with_retry( reason: reason.to_string(), }); } + + // With --ask, prompt before auto-retry + if ask && std::io::stdin().is_terminal() { + let choices = ["Yes, retry with --impure", "No, cancel"]; + let idx = dialoguer::Select::new() + .with_prompt(format!( + "Package {} requires `--impure` ({}). Retry?", + pkg.bold(), + reason.bold() + )) + .items(&choices) + .default(0) + .interact() + .map_err(|e| EhError::Io(std::io::Error::other(e)))?; + if idx != 0 { + return Err(EhError::ProcessExit { code: 1 }); + } + } + print_retry_msg(pkg, reason, env_var); let mut retry_cmd = NixCommand::new(subcommand) .print_build_logs(true)