Compare commits

...

6 commits

Author SHA1 Message Date
63a81bb9d2
nix: expose NixOS module in default flake outputs; cleanup
Some checks failed
Rust / build (push) Has been cancelled
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I7457515ee7a96e11c9301e5b077afb446a6a6964
2026-05-09 20:19:20 +03:00
86d457b389
nix: deprecate x86_64-darwin platform support in flake
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: Id14e6ee7c53adb4e01febe84f357f3026a6a6964
2026-05-09 20:19:19 +03:00
8cf3409fdd
docs: document new features; minor cleanup
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I6025ff8adf399a2064e316380f36fb076a6a6964
2026-05-09 20:12:51 +03:00
f87bc4158c
eh: add missing eh dev command; add --ask for sensitive operations
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: If48f4d337c62dad669af97f9e97c1cb76a6a6964
2026-05-09 20:10:33 +03:00
1174e4496b
build: bump dependencies; version internal crates
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: Ie83171ed923eda33cbb5ea360b8ddc566a6a6964
2026-05-09 20:10:32 +03:00
4e4f80ba1b
nix: bump inputs
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I5376714e52067386f7470d9aa50c1a176a6a6964
2026-05-09 19:59:11 +03:00
14 changed files with 204 additions and 50 deletions

5
Cargo.lock generated
View file

@ -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",

View file

@ -4,7 +4,6 @@ members = [ "eh", "crates/*" ]
resolver = "3"
[workspace.package]
authors = [ "NotAShelf <raf@notashelf.dev>" ]
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

View file

@ -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.
<!--markdownlint-disable MD013-->

View file

@ -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

View file

@ -3,7 +3,6 @@ name = "xtask"
description.workspace = true
version.workspace = true
edition.workspace = true
authors.workspace = true
rust-version.workspace = true
publish = false

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

@ -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

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)

6
flake.lock generated
View file

@ -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": {

View file

@ -5,10 +5,15 @@
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 {
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;

View file

@ -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
'';
};
};