From 4e4f80ba1bfacf0c38167e9e0755ae8cf2546aae Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Thu, 23 Apr 2026 17:47:41 +0300 Subject: [PATCH 1/6] nix: bump inputs Signed-off-by: NotAShelf Change-Id: I5376714e52067386f7470d9aa50c1a176a6a6964 --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index dfdfdf9..4fe7997 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1776548001, - "narHash": "sha256-ZSK0NL4a1BwVbbTBoSnWgbJy9HeZFXLYQizjb2DPF24=", + "lastModified": 1777954456, + "narHash": "sha256-hGdgeU2Nk87RAuZyYjyDjFL6LK7dAZN5RE9+hrDTkDU=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "b12141ef619e0a9c1c84dc8c684040326f27cdcc", + "rev": "549bd84d6279f9852cae6225e372cc67fb91a4c1", "type": "github" }, "original": { From 1174e4496b79b93cf47dda548efdc1535e9180dc Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Sat, 9 May 2026 19:59:08 +0300 Subject: [PATCH 2/6] build: bump dependencies; version internal crates Signed-off-by: NotAShelf Change-Id: Ie83171ed923eda33cbb5ea360b8ddc566a6a6964 --- Cargo.lock | 5 +++-- Cargo.toml | 11 +++++------ crates/eh-log/Cargo.toml | 2 +- crates/xtask/Cargo.toml | 1 - eh/Cargo.toml | 26 +++++++++++++------------- 5 files changed, 22 insertions(+), 23 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index dd5c19e..e55a104 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -57,9 +57,9 @@ dependencies = [ [[package]] name = "clap_complete" -version = "4.6.2" +version = "4.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff7a1dccbdd8b078c2bdebff47e404615151534d5043da397ec50286816f9cb" +checksum = "660c0520455b1013b9bcb0393d5f643d7e4454fb69c915b8d6d2aa0e9a45acc3" dependencies = [ "clap", ] @@ -109,6 +109,7 @@ name = "eh" version = "0.2.0" dependencies = [ "clap", + "clap_complete", "dialoguer", "eh-log", "regex", diff --git a/Cargo.toml b/Cargo.toml index 931f336..43502a8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,6 @@ members = [ "eh", "crates/*" ] resolver = "3" [workspace.package] -authors = [ "NotAShelf " ] description = "Ergonomic Nix CLI helper" edition = "2024" license = "MPL-2.0" @@ -13,11 +12,11 @@ rust-version = "1.94.0" version = "0.2.0" [workspace.dependencies] -clap = { default-features = false, features = [ "std", "help", "derive" ], version = "4.6.0" } -clap_complete = "4.6.0" +clap = { default-features = false, features = [ "std", "help", "derive" ], version = "4.6.1" } +clap_complete = "4.6.3" dialoguer = { default-features = false, version = "0.12.0" } regex = "1.12.3" -serde = { features = [ "derive" ], version = "1.0.149" } +serde = { features = [ "derive" ], version = "1.0.228" } serde_json = "1.0.149" tempfile = "3.27.0" textwrap = "0.16.2" @@ -26,8 +25,8 @@ toml = { default-features = false, features = [ "parse", "serde" ], ver walkdir = "2.5.0" yansi = "1.0.1" -eh = { path = "./eh" } -eh-log = { path = "./crates/eh-log" } +eh = { path = "./eh", version = "0.2.0" } +eh-log = { path = "./crates/eh-log", version = "0.2.0" } [profile.release] codegen-units = 1 diff --git a/crates/eh-log/Cargo.toml b/crates/eh-log/Cargo.toml index e3ef1f4..17b2a02 100644 --- a/crates/eh-log/Cargo.toml +++ b/crates/eh-log/Cargo.toml @@ -3,8 +3,8 @@ name = "eh-log" description = "Styled logging for eh" version.workspace = true edition.workspace = true -authors.workspace = true rust-version.workspace = true +publish = false [dependencies] yansi.workspace = true diff --git a/crates/xtask/Cargo.toml b/crates/xtask/Cargo.toml index 4343ed0..4a83c82 100644 --- a/crates/xtask/Cargo.toml +++ b/crates/xtask/Cargo.toml @@ -3,7 +3,6 @@ name = "xtask" description.workspace = true version.workspace = true edition.workspace = true -authors.workspace = true rust-version.workspace = true publish = false diff --git a/eh/Cargo.toml b/eh/Cargo.toml index f3f4e51..2223eaa 100644 --- a/eh/Cargo.toml +++ b/eh/Cargo.toml @@ -3,7 +3,6 @@ name = "eh" description.workspace = true version.workspace = true edition.workspace = true -authors.workspace = true rust-version.workspace = true [lib] @@ -11,15 +10,16 @@ crate-type = [ "lib" ] name = "eh" [dependencies] -clap.workspace = true -dialoguer.workspace = true -eh-log.workspace = true -regex.workspace = true -serde.workspace = true -serde_json.workspace = true -tempfile.workspace = true -textwrap.workspace = true -thiserror.workspace = true -toml.workspace = true -walkdir.workspace = true -yansi.workspace = true +clap.workspace = true +clap_complete.workspace = true +dialoguer.workspace = true +eh-log.workspace = true +regex.workspace = true +serde.workspace = true +serde_json.workspace = true +tempfile.workspace = true +textwrap.workspace = true +thiserror.workspace = true +toml.workspace = true +walkdir.workspace = true +yansi.workspace = true From f87bc4158c428823d85e7133a69d62f912ffd52b Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Sat, 9 May 2026 20:09:29 +0300 Subject: [PATCH 3/6] 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) From 8cf3409fdd193f1908cd5be5a4be861a64998c47 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Sat, 9 May 2026 20:10:26 +0300 Subject: [PATCH 4/6] docs: document new features; minor cleanup Signed-off-by: NotAShelf Change-Id: I6025ff8adf399a2064e316380f36fb076a6a6964 --- README.md | 54 ++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 48 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 1c4a320..9e1b2fc 100644 --- a/README.md +++ b/README.md @@ -37,15 +37,31 @@ When a hash mismatch is detected in the underlying `nix build`, `eh` can automatically update the old, broken hash with a new and correct one _directly in the source file_. +### Interactive `--ask` Flag + +Pass `-a` / `--ask` to any command (`run`, `shell`, `build`, `develop`) to +enable interactive confirmation prompts. Before retrying with `--impure` or +auto-fixing a hash mismatch, `eh` will ask for confirmation: + +```bash +eh build nixpkgs#steam --ask +# ? Package 'steam' has an unfree license. Retry with --impure? (Y/n) +# ? Hash mismatch detected in pkgs/hello/default.nix. Fix it? (Y/n) +``` + +In non-interactive mode (no TTY), `--ask` raises an error rather than silently +skipping. Without `--ask`, prompts only appear in TTY mode. + ## Shell Aliases By default, you may run the `eh` binary akin to Nix with a nicer interface. The supported Nix commands, i.e., nix `build`, `shell`, `run` and `develop` become `eh build`, `eh shell`, `eh run` and `eh develop`. However, it is possible to -symlink the `eh` binary to `nb`, `ns`, `nr`, and `nd` to invoke a specific -feature. For example, `nb` will act as `eh build` and `nr` will be `eh run`. +symlink the `eh` binary to `nb`, `ns`, `nr`, `nd`, `dev`, `ni`, and `nu` to +invoke a specific feature. For example, `nb` will act as `eh build`, `nr` will +be `eh run`, and `dev` is an alias for `nd`. -One special example is `eh update`, which is aliases to `nu`, that handles +One special example is `eh update`, which is aliased to `nu`, that handles interactive Nix flake updates. It is special in the sense that the usage is entirely different from its Nix counterpart, where you get to _interactively_ pick which inputs to update. @@ -56,9 +72,34 @@ After enabling shell aliases via the NixOS module or Home Manager, you can use: ns nixpkgs#hello # equivalent to: nix shell nixpkgs#hello nr nixpkgs#cowsay "Hello!" # nix run nixpkgs#cowsay nb .#myPackage # nix build .#myPackage +nd .#myPackage # nix develop .#myPackage +dev .#myPackage # nix develop .#myPackage (alias for nd) +ni nixpkgs#hello # nix eval nixpkgs#hello.meta nu # nix flake update ``` +### Shell Completions + +Generate completions for bash, zsh, or fish: + +```bash +eh completion bash +eh completion zsh +eh completion fish +``` + +### Shell Integration Scripts + +Sourceable shell integration scripts are available in the Nix package output +under `$out/nix/integrations/`: + +| File | Description | +| ----------- | ----------------------------------- | +| `bash.sh` | Aliases and completion setup (bash) | +| `zsh.sh` | Aliases and completion setup (zsh) | +| `fish.fish` | Aliases and completion setup (fish) | +| `direnvrc` | Direnv integration for flakes | + ## Configuration `eh` reads configuration from the first `.eh.toml` found by walking up from the @@ -77,9 +118,10 @@ impure = true impure = false ``` -When `impure` is absent (the default), auto-retry with `--impure` is -**automatic** — `eh` will add `--impure` and the appropriate `NIXPKGS_ALLOW_*` -variable whenever it detects an unfree, insecure, or broken package. +> [!TIP] +> When `impure` is absent (the default), auto-retry with `--impure` is +> **automatic**. `eh` will add `--impure` and the appropriate `NIXPKGS_ALLOW_*` +> variable whenever it detects an unfree, insecure, or broken package. From 86d457b389703ce0965bc61727030601d273e277 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Sat, 9 May 2026 20:12:47 +0300 Subject: [PATCH 5/6] nix: deprecate x86_64-darwin platform support in flake Signed-off-by: NotAShelf Change-Id: Id14e6ee7c53adb4e01febe84f357f3026a6a6964 --- flake.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flake.nix b/flake.nix index c0b6769..d6b751f 100644 --- a/flake.nix +++ b/flake.nix @@ -5,7 +5,7 @@ self, nixpkgs, }: let - systems = ["x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin"]; + systems = ["x86_64-linux" "aarch64-linux" "aarch64-darwin"]; forEachSystem = nixpkgs.lib.genAttrs systems; pkgsForEach = nixpkgs.legacyPackages; in { From 63a81bb9d20d4579f9a1a5254b2dee6bd8017ff7 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Sat, 9 May 2026 20:16:33 +0300 Subject: [PATCH 6/6] nix: expose NixOS module in default flake outputs; cleanup Signed-off-by: NotAShelf Change-Id: I7457515ee7a96e11c9301e5b077afb446a6a6964 --- flake.nix | 5 +++++ nix/modules/nixos.nix | 29 ++++++++++++++++++++++++----- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/flake.nix b/flake.nix index d6b751f..fe24cda 100644 --- a/flake.nix +++ b/flake.nix @@ -9,6 +9,11 @@ forEachSystem = nixpkgs.lib.genAttrs systems; pkgsForEach = nixpkgs.legacyPackages; in { + nixosModules = { + eh = import ./nix/modules/nixos.nix self; + default = self.nixosModules.eh; + }; + packages = forEachSystem (system: { eh = pkgsForEach.${system}.callPackage ./nix/package.nix {}; default = self.packages.${system}.eh; diff --git a/nix/modules/nixos.nix b/nix/modules/nixos.nix index 074ecdb..a91c710 100644 --- a/nix/modules/nixos.nix +++ b/nix/modules/nixos.nix @@ -5,39 +5,58 @@ self: { ... }: let inherit (lib.modules) mkIf; - inherit (lib.options) mkEnableOption mkPackageOption; + inherit (lib.options) mkEnableOption mkPackageOption literalExpression; inherit (lib.strings) optionalString; cfg = config.programs.eh; in { options.programs.eh = { enable = mkEnableOption "eh - Ergonomic Nix CLI helper"; - package = mkPackageOption self.packages.${pkgs.hostPlatform.system} ["eh"] {}; + package = mkPackageOption self.packages.${pkgs.hostPlatform.system} ["eh"] { + pkgsText = literalExpression "self.packages.$${pkgs.hostPlatform.system}"; + }; hooks = { bash.enable = mkEnableOption "Bash shell hook for EH" // {default = config.programs.bash.enable;}; zsh.enable = mkEnableOption "ZSH shell hook for EH" // {default = config.programs.zsh.enable;}; + fish.enable = mkEnableOption "Fish shell hook for EH" // {default = config.programs.fish.enable;}; }; }; config = mkIf cfg.enable { environment.systemPackages = [cfg.package]; - programs = { bash.interactiveShellInit = optionalString cfg.hooks.bash.enable '' - # Aliases added by EH + # EH multicall aliases alias nr='eh run' alias ns='eh shell' alias nb='eh build' + alias nd='eh develop' + alias ni='eh info' alias nu='eh update' + # End of EH aliases ''; zsh.interactiveShellInit = optionalString cfg.hooks.zsh.enable '' - # Aliases added by EH + # EH multicall aliases alias nr='eh run' alias ns='eh shell' alias nb='eh build' + alias nd='eh develop' + alias ni='eh info' alias nu='eh update' + # End of EH aliases + ''; + + fish.interactiveShellInit = optionalString cfg.hooks.fish.enable '' + # EH multicall aliases + alias nr='eh run' + alias ns='eh shell' + alias nb='eh build' + alias nd='eh develop' + alias ni='eh info' + alias nu='eh update' + # End of EH aliases ''; }; };