eh: add missing eh dev command; add --ask for sensitive operations

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: If48f4d337c62dad669af97f9e97c1cb76a6a6964
This commit is contained in:
raf 2026-05-09 20:09:29 +03:00
commit f87bc4158c
Signed by: NotAShelf
GPG key ID: 29D95B64378DB4BF
5 changed files with 101 additions and 12 deletions

View file

@ -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() {

View file

@ -337,6 +337,7 @@ pub fn handle_nix_command(
fixer: &dyn NixFileFixer,
classifier: &dyn NixErrorClassifier,
cfg: &crate::config::CommandConfig,
ask: bool,
) -> Result<i32> {
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,
)
}

View file

@ -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<String>,
},
/// Enter a Nix shell
Shell {
#[arg(short, long, default_value = "false")]
ask: bool,
#[arg(trailing_var_arg = true)]
args: Vec<String>,
},
/// Build a Nix derivation
Build {
#[arg(short, long, default_value = "false")]
ask: bool,
#[arg(trailing_var_arg = true)]
args: Vec<String>,
},
/// Enter a Nix development shell
Develop {
#[arg(short, long, default_value = "false")]
ask: bool,
#[arg(trailing_var_arg = true)]
args: Vec<String>,
},
@ -47,4 +66,10 @@ pub enum Command {
#[arg(trailing_var_arg = true)]
args: Vec<String>,
},
/// Generate shell completions
Completion {
/// Shell to generate completions for
#[arg(value_enum)]
shell: Shell,
},
}

View file

@ -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<i32> {
fn handle_command(command: &str, args: &[String], ask: bool) -> error::Result<i32> {
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<i32> {
&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<i32> {
@ -106,17 +108,28 @@ fn run_app() -> error::Result<i32> {
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()?;

View file

@ -486,6 +486,7 @@ pub fn handle_nix_with_retry(
classifier: &dyn NixErrorClassifier,
interactive: bool,
cfg: &crate::config::CommandConfig,
ask: bool,
) -> Result<i32> {
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)