eh: add info command; bump deps

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I85faac1cc3a48ed2622c1160ab954d8f6a6a6964
This commit is contained in:
raf 2026-03-20 15:11:10 +03:00
commit 8836eacb95
Signed by: NotAShelf
GPG key ID: 29D95B64378DB4BF
9 changed files with 660 additions and 42 deletions

296
Cargo.lock generated
View file

@ -13,15 +13,21 @@ dependencies = [
[[package]] [[package]]
name = "anstyle" name = "anstyle"
version = "1.0.13" version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
[[package]]
name = "anyhow"
version = "1.0.102"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
[[package]] [[package]]
name = "bitflags" name = "bitflags"
version = "2.10.0" version = "2.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
[[package]] [[package]]
name = "cfg-if" name = "cfg-if"
@ -78,13 +84,12 @@ checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
[[package]] [[package]]
name = "console" name = "console"
version = "0.16.2" version = "0.16.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03e45a4a8926227e4197636ba97a9fc9b00477e9f4bd711395687c5f0734bec4" checksum = "d64e8af5551369d19cf50138de61f1c42074ab970f74e99be916646777f8fc87"
dependencies = [ dependencies = [
"encode_unicode", "encode_unicode",
"libc", "libc",
"once_cell",
"unicode-width", "unicode-width",
"windows-sys", "windows-sys",
] ]
@ -107,8 +112,10 @@ dependencies = [
"dialoguer", "dialoguer",
"eh-log", "eh-log",
"regex", "regex",
"serde",
"serde_json", "serde_json",
"tempfile", "tempfile",
"textwrap",
"thiserror", "thiserror",
"walkdir", "walkdir",
"yansi", "yansi",
@ -127,6 +134,12 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0"
[[package]]
name = "equivalent"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]] [[package]]
name = "errno" name = "errno"
version = "0.3.14" version = "0.3.14"
@ -144,17 +157,39 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
[[package]] [[package]]
name = "getrandom" name = "foldhash"
version = "0.3.4" version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
[[package]]
name = "getrandom"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"libc", "libc",
"r-efi", "r-efi",
"wasip2", "wasip2",
"wasip3",
] ]
[[package]]
name = "hashbrown"
version = "0.15.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
dependencies = [
"foldhash",
]
[[package]]
name = "hashbrown"
version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
[[package]] [[package]]
name = "heck" name = "heck"
version = "0.5.0" version = "0.5.0"
@ -162,16 +197,40 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]] [[package]]
name = "itoa" name = "id-arena"
version = "1.0.17" version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
[[package]]
name = "indexmap"
version = "2.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017"
dependencies = [
"equivalent",
"hashbrown 0.16.1",
"serde",
"serde_core",
]
[[package]]
name = "itoa"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
name = "leb128fmt"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.183" version = "0.2.184"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af"
[[package]] [[package]]
name = "linux-raw-sys" name = "linux-raw-sys"
@ -180,16 +239,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
[[package]] [[package]]
name = "memchr" name = "log"
version = "2.7.6" version = "0.4.29"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
[[package]]
name = "memchr"
version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
[[package]] [[package]]
name = "once_cell" name = "once_cell"
version = "1.21.3" version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]]
name = "prettyplease"
version = "0.2.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
dependencies = [
"proc-macro2",
"syn",
]
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
@ -211,9 +286,9 @@ dependencies = [
[[package]] [[package]]
name = "r-efi" name = "r-efi"
version = "5.3.0" version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
[[package]] [[package]]
name = "regex" name = "regex"
@ -229,9 +304,9 @@ dependencies = [
[[package]] [[package]]
name = "regex-automata" name = "regex-automata"
version = "0.4.13" version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
dependencies = [ dependencies = [
"aho-corasick", "aho-corasick",
"memchr", "memchr",
@ -240,9 +315,9 @@ dependencies = [
[[package]] [[package]]
name = "regex-syntax" name = "regex-syntax"
version = "0.8.8" version = "0.8.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
[[package]] [[package]]
name = "rustix" name = "rustix"
@ -266,6 +341,12 @@ dependencies = [
"winapi-util", "winapi-util",
] ]
[[package]]
name = "semver"
version = "1.0.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.228" version = "1.0.228"
@ -273,6 +354,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
dependencies = [ dependencies = [
"serde_core", "serde_core",
"serde_derive",
] ]
[[package]] [[package]]
@ -314,6 +396,12 @@ version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77" checksum = "dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77"
[[package]]
name = "smawk"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c"
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.117" version = "2.0.117"
@ -338,6 +426,17 @@ dependencies = [
"windows-sys", "windows-sys",
] ]
[[package]]
name = "textwrap"
version = "0.16.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057"
dependencies = [
"smawk",
"unicode-linebreak",
"unicode-width",
]
[[package]] [[package]]
name = "thiserror" name = "thiserror"
version = "2.0.18" version = "2.0.18"
@ -360,9 +459,15 @@ dependencies = [
[[package]] [[package]]
name = "unicode-ident" name = "unicode-ident"
version = "1.0.22" version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "unicode-linebreak"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f"
[[package]] [[package]]
name = "unicode-width" name = "unicode-width"
@ -370,6 +475,12 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
[[package]]
name = "unicode-xid"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
[[package]] [[package]]
name = "walkdir" name = "walkdir"
version = "2.5.0" version = "2.5.0"
@ -389,6 +500,49 @@ dependencies = [
"wit-bindgen", "wit-bindgen",
] ]
[[package]]
name = "wasip3"
version = "0.4.0+wasi-0.3.0-rc-2026-01-06"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"
dependencies = [
"wit-bindgen",
]
[[package]]
name = "wasm-encoder"
version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319"
dependencies = [
"leb128fmt",
"wasmparser",
]
[[package]]
name = "wasm-metadata"
version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909"
dependencies = [
"anyhow",
"indexmap",
"wasm-encoder",
"wasmparser",
]
[[package]]
name = "wasmparser"
version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
dependencies = [
"bitflags",
"hashbrown 0.15.5",
"indexmap",
"semver",
]
[[package]] [[package]]
name = "winapi-util" name = "winapi-util"
version = "0.1.11" version = "0.1.11"
@ -418,6 +572,88 @@ name = "wit-bindgen"
version = "0.51.0" version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
dependencies = [
"wit-bindgen-rust-macro",
]
[[package]]
name = "wit-bindgen-core"
version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc"
dependencies = [
"anyhow",
"heck",
"wit-parser",
]
[[package]]
name = "wit-bindgen-rust"
version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21"
dependencies = [
"anyhow",
"heck",
"indexmap",
"prettyplease",
"syn",
"wasm-metadata",
"wit-bindgen-core",
"wit-component",
]
[[package]]
name = "wit-bindgen-rust-macro"
version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a"
dependencies = [
"anyhow",
"prettyplease",
"proc-macro2",
"quote",
"syn",
"wit-bindgen-core",
"wit-bindgen-rust",
]
[[package]]
name = "wit-component"
version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
dependencies = [
"anyhow",
"bitflags",
"indexmap",
"log",
"serde",
"serde_derive",
"serde_json",
"wasm-encoder",
"wasm-metadata",
"wasmparser",
"wit-parser",
]
[[package]]
name = "wit-parser"
version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736"
dependencies = [
"anyhow",
"id-arena",
"indexmap",
"log",
"semver",
"serde",
"serde_derive",
"serde_json",
"unicode-xid",
"wasmparser",
]
[[package]] [[package]]
name = "xtask" name = "xtask"
@ -436,6 +672,6 @@ checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
[[package]] [[package]]
name = "zmij" name = "zmij"
version = "1.0.17" version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02aae0f83f69aafc94776e879363e9771d7ecbffe2c7fbb6c14c5e00dfe88439" checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"

View file

@ -17,8 +17,10 @@ clap = { default-features = false, features = [ "std", "help", "derive"
clap_complete = "4.6.0" clap_complete = "4.6.0"
dialoguer = { default-features = false, version = "0.12.0" } dialoguer = { default-features = false, version = "0.12.0" }
regex = "1.12.3" regex = "1.12.3"
serde = { features = [ "derive" ], version = "1.0.149" }
serde_json = "1.0.149" serde_json = "1.0.149"
tempfile = "3.27.0" tempfile = "3.27.0"
textwrap = "0.16.2"
thiserror = "2.0.18" thiserror = "2.0.18"
walkdir = "2.5.0" walkdir = "2.5.0"
yansi = "1.0.1" yansi = "1.0.1"

View file

@ -43,6 +43,7 @@ enum Binary {
Ns, Ns,
Nb, Nb,
Nd, Nd,
Ni,
Nu, Nu,
} }
@ -53,6 +54,7 @@ impl Binary {
Self::Ns => "ns", Self::Ns => "ns",
Self::Nb => "nb", Self::Nb => "nb",
Self::Nd => "nd", Self::Nd => "nd",
Self::Ni => "ni",
Self::Nu => "nu", Self::Nu => "nu",
} }
} }
@ -94,8 +96,14 @@ fn create_multicall_binaries(
); );
} }
let multicall_binaries = let multicall_binaries = [
[Binary::Nr, Binary::Ns, Binary::Nb, Binary::Nd, Binary::Nu]; Binary::Nr,
Binary::Ns,
Binary::Nb,
Binary::Nd,
Binary::Ni,
Binary::Nu,
];
let bin_path = Path::new(bin_dir); let bin_path = Path::new(bin_dir);
for binary in multicall_binaries { for binary in multicall_binaries {
@ -158,7 +166,7 @@ fn generate_completions(
println!("completion file generated: {}", completion_file.display()); println!("completion file generated: {}", completion_file.display());
// Create symlinks for multicall binaries // Create symlinks for multicall binaries
let multicall_names = ["nb", "nd", "nr", "ns", "nu"]; let multicall_names = ["nb", "nd", "ni", "nr", "ns", "nu"];
for name in &multicall_names { for name in &multicall_names {
let symlink_path = output_dir.join(format!("{name}.{shell}")); let symlink_path = output_dir.join(format!("{name}.{shell}"));
if symlink_path.exists() { if symlink_path.exists() {

View file

@ -15,8 +15,10 @@ clap.workspace = true
dialoguer.workspace = true dialoguer.workspace = true
eh-log.workspace = true eh-log.workspace = true
regex.workspace = true regex.workspace = true
serde.workspace = true
serde_json.workspace = true serde_json.workspace = true
tempfile.workspace = true tempfile.workspace = true
textwrap.workspace = true
thiserror.workspace = true thiserror.workspace = true
walkdir.workspace = true walkdir.workspace = true
yansi.workspace = true yansi.workspace = true

290
eh/src/commands/info.rs Normal file
View file

@ -0,0 +1,290 @@
use std::collections::HashMap;
use eh_log::{log_error, log_info};
use serde::Deserialize;
use yansi::Paint;
use crate::{
commands::NixCommand,
error::{EhError, Result},
util::{make_eval_expr, print_error_suggestions},
};
#[derive(Debug, Deserialize)]
struct PackageMeta {
name: String,
version: Option<String>,
description: Option<String>,
long_description: Option<String>,
license: Option<serde_json::Value>,
homepage: Option<String>,
platforms: Option<Vec<String>>,
broken: Option<bool>,
insecure: Option<bool>,
#[serde(rename = "unfree")]
unfree: Option<bool>,
}
#[derive(Debug, Deserialize)]
struct PackageOutputs {
#[serde(flatten)]
outputs: HashMap<String, serde_json::Value>,
}
pub fn handle_info(args: &[String]) -> Result<i32> {
// Get the package argument (skip flags)
let pkg = args
.iter()
.find(|arg| !arg.starts_with('-'))
.cloned()
.unwrap_or_else(|| ".".to_string());
let eval_arg = make_eval_expr(&pkg);
let pkg_name: String = if eval_arg.contains("#") {
eval_arg
.split("#")
.last()
.unwrap_or(&eval_arg)
.trim_end_matches(".meta")
.to_string()
} else {
eval_arg.trim_end_matches(".meta").to_string()
};
// Handle .# case - show "default" as the package name
let pkg_name = if pkg_name.is_empty() {
"default".to_string()
} else {
pkg_name
};
log_info!("Fetching info for {}", pkg_name.bold());
// Fetch metadata
let meta_cmd = NixCommand::new("eval")
.arg("--json")
.arg(&eval_arg)
.print_build_logs(false);
let meta_output = meta_cmd.output()?;
if !meta_output.status.success() {
log_error!("Failed to fetch package info");
print_error_suggestions(&meta_output.stderr);
return Err(EhError::NixCommandFailed {
command: "eval".to_string(),
});
}
let meta: PackageMeta =
serde_json::from_slice(&meta_output.stdout).map_err(|e| {
EhError::Io(std::io::Error::other(format!(
"Failed to parse package metadata: {}",
e
)))
})?;
// Fetch outputs
let outputs_expr = eval_arg
.strip_suffix(".meta")
.unwrap_or(&eval_arg)
.to_string();
let outputs_cmd = NixCommand::new("eval")
.arg("--json")
.arg(format!("{}.outputs", outputs_expr))
.print_build_logs(false);
let outputs_output = outputs_cmd.output()?;
let outputs: Option<PackageOutputs> = if outputs_output.status.success() {
serde_json::from_slice(&outputs_output.stdout).ok()
} else {
None
};
// Print formatted info
print_package_info(&meta, outputs.as_ref(), &pkg);
Ok(0)
}
fn print_package_info(
meta: &PackageMeta,
outputs: Option<&PackageOutputs>,
pkg_ref: &str,
) {
println!();
// Header
println!(" {} {}", "Package:".bold(), meta.name);
if let Some(ref version) = meta.version {
println!(" {} {}", "Version:".bold(), version);
}
if let Some(ref desc) = meta.description {
println!(" {} {}", "Description:".bold(), desc);
}
// Show long description if available and different from short description
if let Some(ref long_desc) = meta.long_description {
let should_show = meta
.description
.as_ref()
.map(|d| d != long_desc)
.unwrap_or(true);
if should_show {
println!();
// Wrap long description to 70 chars for readability
let wrapped = textwrap::fill(long_desc, 70);
for line in wrapped.lines() {
println!(" {}", line);
}
}
}
// License
if let Some(ref license) = meta.license {
let license_str = match license {
serde_json::Value::String(s) => s.clone(),
serde_json::Value::Object(obj) => {
obj
.get("spdxId")
.and_then(|v| v.as_str())
.or_else(|| obj.get("shortName").and_then(|v| v.as_str()))
.unwrap_or("Unknown")
.to_string()
},
serde_json::Value::Array(licenses) => {
// Handle multiple licenses (e.g., neovim has Apache-2.0 AND Vim)
let license_names: Vec<String> = licenses
.iter()
.filter_map(|lic| {
match lic {
serde_json::Value::Object(obj) => {
obj
.get("spdxId")
.and_then(|v| v.as_str())
.or_else(|| obj.get("shortName").and_then(|v| v.as_str()))
.map(|s| s.to_string())
},
serde_json::Value::String(s) => Some(s.clone()),
_ => None,
}
})
.collect();
if license_names.is_empty() {
"Unknown".to_string()
} else {
license_names.join(", ")
}
},
_ => "Unknown".to_string(),
};
println!(" {} {}", "License:".bold(), license_str);
}
// Homepage
if let Some(ref homepage) = meta.homepage {
println!(" {} {}", "Homepage:".bold(), homepage);
}
// Meta section
println!();
println!(" {}", "Meta:".bold());
// Status indicators
let mut status_parts = Vec::new();
if meta.broken == Some(true) {
status_parts.push("Broken".red().to_string());
}
if meta.insecure == Some(true) {
status_parts.push("Insecure".red().to_string());
}
if meta.unfree == Some(true) {
status_parts.push("Unfree".yellow().to_string());
}
if status_parts.is_empty() {
println!(" {} {}", "Status:".bold(), "✓ Available".green());
} else {
println!(" {} {}", "Status:".bold(), status_parts.join(", "));
}
// Platforms
if let Some(ref platforms) = meta.platforms {
let platform_list: Vec<_> = platforms.iter().take(4).cloned().collect();
let platform_str = if platforms.len() > 4 {
format!(
"{} + {} more",
platform_list.join(", "),
platforms.len() - 4
)
} else {
platform_list.join(", ")
};
println!(" {} {}", "Platforms:".bold(), platform_str);
}
// Outputs section
if let Some(outputs) = outputs {
println!();
println!(" {}", "Outputs:".bold());
let output_names: Vec<_> = outputs.outputs.keys().cloned().collect();
for name in output_names {
let marker = if name == "out" { " (default)" } else { "" };
println!("{}{}", name, marker.dim());
}
}
// Usage section
println!();
println!(" {}", "Usage:".bold());
println!(
" {} {} {}",
"eh run".dim(),
pkg_ref,
"# Run the package".dim()
);
println!(
" {} {} {}",
"eh shell".dim(),
pkg_ref,
"# Enter shell with package".dim()
);
println!();
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_package_meta_deserialization() {
let json = r#"{
"name": "hello",
"version": "2.12.1",
"description": "A greeting program",
"license": "GPL-3.0",
"homepage": "https://example.com",
"platforms": ["x86_64-linux"],
"broken": false,
"insecure": false,
"unfree": false
}"#;
let meta: PackageMeta = serde_json::from_str(json).unwrap();
assert_eq!(meta.name, "hello");
assert_eq!(meta.version, Some("2.12.1".to_string()));
}
#[test]
fn test_license_object_parsing() {
let json = r#"{
"name": "test",
"license": {"spdxId": "MIT", "fullName": "MIT License"}
}"#;
let meta: PackageMeta = serde_json::from_str(json).unwrap();
assert!(meta.license.is_some());
}
}

View file

@ -16,6 +16,7 @@ use crate::{
}, },
}; };
pub mod info;
pub mod update; pub mod update;
const DEFAULT_BUFFER_SIZE: usize = 4096; const DEFAULT_BUFFER_SIZE: usize = 4096;

View file

@ -36,6 +36,11 @@ pub enum Command {
#[arg(trailing_var_arg = true)] #[arg(trailing_var_arg = true)]
args: Vec<String>, args: Vec<String>,
}, },
/// Show package information
Info {
#[arg(trailing_var_arg = true)]
args: Vec<String>,
},
/// Update flake inputs interactively /// Update flake inputs interactively
Update { Update {
#[arg(trailing_var_arg = true)] #[arg(trailing_var_arg = true)]

View file

@ -31,6 +31,8 @@ fn handle_command(command: &str, args: &[String]) -> error::Result<i32> {
let classifier = util::DefaultNixErrorClassifier; let classifier = util::DefaultNixErrorClassifier;
match command { match command {
"info" => commands::info::handle_info(args),
"update" => commands::update::handle_update(args), "update" => commands::update::handle_update(args),
"run" | "shell" | "build" | "develop" => { "run" | "shell" | "build" | "develop" => {
commands::handle_nix_command( commands::handle_nix_command(
@ -56,6 +58,7 @@ fn dispatch_multicall(
"ns" => "shell", "ns" => "shell",
"nb" => "build", "nb" => "build",
"nd" => "develop", "nd" => "develop",
"ni" => "info",
"nu" => "update", "nu" => "update",
_ => return None, _ => return None,
}; };
@ -107,6 +110,8 @@ fn run_app() -> error::Result<i32> {
Some(Command::Develop { args }) => handle_command("develop", &args), Some(Command::Develop { args }) => handle_command("develop", &args),
Some(Command::Info { args }) => handle_command("info", &args),
Some(Command::Update { args }) => handle_command("update", &args), Some(Command::Update { args }) => handle_command("update", &args),
None => { None => {

View file

@ -1,5 +1,5 @@
use std::{ use std::{
io::{BufWriter, Write}, io::{BufWriter, IsTerminal, Write},
path::{Path, PathBuf}, path::{Path, PathBuf},
sync::LazyLock, sync::LazyLock,
}; };
@ -42,6 +42,13 @@ static HASH_FIX_PATTERNS: LazyLock<[Regex; 3]> = LazyLock::new(|| {
] ]
}); });
/// Regex to extract suggestions from Nix's "Did you mean" error line.
/// Matches patterns like:
/// - "Did you mean one of hello, world, or foo?"
/// - "Did you mean lib.hello?"
static DID_YOU_MEAN_PATTERN: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r#"Did you mean (?:one of )?(.+?)\?"#).unwrap());
/// Trait for extracting store paths and hashes from nix output. /// Trait for extracting store paths and hashes from nix output.
pub trait HashExtractor { pub trait HashExtractor {
/// Extract the new store path/hash from nix output. /// Extract the new store path/hash from nix output.
@ -311,9 +318,20 @@ fn is_hash_mismatch_error(stderr: &str) -> bool {
/// Construct the eval expression for a given argument. /// Construct the eval expression for a given argument.
/// Handles both plain package names and flake references. /// Handles both plain package names and flake references.
fn make_eval_expr(eval_arg: &str) -> String { pub fn make_eval_expr(eval_arg: &str) -> String {
// Handle . (current directory) as .# (default package of current flake)
// Nix treats `nix build .` and `nix build .#` as equivalent
let eval_arg = if eval_arg == "." { ".#" } else { eval_arg };
if eval_arg.contains('#') { if eval_arg.contains('#') {
format!("{eval_arg}.meta") // Handle .# (current flake default package) case
// .# needs to become .#default for meta evaluation to work
// because .#.meta evaluates 'meta' on the flake itself, not the package
if eval_arg.ends_with('#') {
format!("{eval_arg}default.meta")
} else {
format!("{eval_arg}.meta")
}
} else { } else {
format!("nixpkgs#{eval_arg}.meta") format!("nixpkgs#{eval_arg}.meta")
} }
@ -512,18 +530,26 @@ pub fn handle_nix_with_retry(
if let Some(new_hash) = hash_extractor.extract_hash(&stderr) { if let Some(new_hash) = hash_extractor.extract_hash(&stderr) {
let old_hash = hash_extractor.extract_old_hash(&stderr); let old_hash = hash_extractor.extract_old_hash(&stderr);
// Ask for confirmation before fixing hash // Ask for confirmation before fixing hash (skip in non-interactive mode)
let should_fix = dialoguer::Confirm::new() let should_fix = if std::io::stdin().is_terminal() {
.with_prompt(format!( dialoguer::Confirm::new()
"Hash mismatch detected for {}. Update hash in local .nix files?", .with_prompt(format!(
"Hash mismatch detected for {}. Update hash in local .nix files?",
pkg.bold()
))
.default(true)
.interact()
.map_err(|e| EhError::Io(std::io::Error::other(e)))?
} else {
log_warn!(
"{}: hash mismatch detected in non-interactive mode, skipping auto-fix",
pkg.bold() pkg.bold()
)) );
.default(true) false
.interact() };
.map_err(|e| EhError::Io(std::io::Error::other(e)))?;
if !should_fix { if !should_fix {
log_warn!("{}: hash fix cancelled by user", pkg.bold()); log_warn!("{}: hash fix cancelled", pkg.bold());
return Err(EhError::ProcessExit { code: 1 }); return Err(EhError::ProcessExit { code: 1 });
} }
@ -592,6 +618,9 @@ pub fn handle_nix_with_retry(
.write_all(&output.stderr) .write_all(&output.stderr)
.map_err(EhError::Io)?; .map_err(EhError::Io)?;
// Print contextual suggestions for common errors
print_error_suggestions(&output.stderr);
match output.status.code() { match output.status.code() {
Some(code) => Err(EhError::ProcessExit { code }), Some(code) => Err(EhError::ProcessExit { code }),
// No exit code means the process was killed by a signal // No exit code means the process was killed by a signal
@ -611,6 +640,46 @@ impl NixErrorClassifier for DefaultNixErrorClassifier {
} }
} }
/// Parse suggestions from Nix's "Did you mean" error line.
/// Input: "Did you mean one of neovim, hevi, navi, neo or neo4j?"
/// Output: vec!["neovim", "hevi", "navi", "neo", "neo4j"]
fn parse_nix_suggestions(did_you_mean_line: &str) -> Vec<String> {
DID_YOU_MEAN_PATTERN
.captures(did_you_mean_line)
.and_then(|caps| caps.get(1))
.map(|m| m.as_str())
.map(|suggestions| {
suggestions
.split(", ")
.flat_map(|part| part.split(" or "))
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect()
})
.unwrap_or_default()
}
/// Print contextual error suggestions when a command fails.
/// Parses Nix's own "Did you mean" suggestions from stderr and presents them
/// nicely to the user.
pub fn print_error_suggestions(stderr: &[u8]) {
let stderr_str = String::from_utf8_lossy(stderr);
// Look for Nix's "Did you mean" line in the error output
if let Some(line) = stderr_str.lines().find(|l| l.contains("Did you mean")) {
let suggestions = parse_nix_suggestions(line);
if !suggestions.is_empty() {
let formatted = suggestions
.iter()
.map(|s| s.bold().to_string())
.collect::<Vec<_>>()
.join(", ");
log_info!("Did you mean: {}?", formatted);
}
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::io::Write; use std::io::Write;