From 8836eacb950dd9f8fc78f0e43bac54f69900f7a1 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Fri, 20 Mar 2026 15:11:10 +0300 Subject: [PATCH] eh: add info command; bump deps Signed-off-by: NotAShelf Change-Id: I85faac1cc3a48ed2622c1160ab954d8f6a6a6964 --- Cargo.lock | 296 +++++++++++++++++++++++++++++++++++---- Cargo.toml | 2 + crates/xtask/src/main.rs | 14 +- eh/Cargo.toml | 2 + eh/src/commands/info.rs | 290 ++++++++++++++++++++++++++++++++++++++ eh/src/commands/mod.rs | 1 + eh/src/lib.rs | 5 + eh/src/main.rs | 5 + eh/src/util.rs | 93 ++++++++++-- 9 files changed, 663 insertions(+), 45 deletions(-) create mode 100644 eh/src/commands/info.rs diff --git a/Cargo.lock b/Cargo.lock index 56d355d..30e8551 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13,15 +13,21 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.13" +version = "1.0.14" 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]] name = "bitflags" -version = "2.10.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" [[package]] name = "cfg-if" @@ -78,13 +84,12 @@ checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] name = "console" -version = "0.16.2" +version = "0.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03e45a4a8926227e4197636ba97a9fc9b00477e9f4bd711395687c5f0734bec4" +checksum = "d64e8af5551369d19cf50138de61f1c42074ab970f74e99be916646777f8fc87" dependencies = [ "encode_unicode", "libc", - "once_cell", "unicode-width", "windows-sys", ] @@ -107,8 +112,10 @@ dependencies = [ "dialoguer", "eh-log", "regex", + "serde", "serde_json", "tempfile", + "textwrap", "thiserror", "walkdir", "yansi", @@ -127,6 +134,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + [[package]] name = "errno" version = "0.3.14" @@ -144,17 +157,39 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] -name = "getrandom" -version = "0.3.4" +name = "foldhash" +version = "0.1.5" 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 = [ "cfg-if", "libc", "r-efi", "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]] name = "heck" version = "0.5.0" @@ -162,16 +197,40 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] -name = "itoa" -version = "1.0.17" +name = "id-arena" +version = "2.3.0" 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]] name = "libc" -version = "0.2.183" +version = "0.2.184" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" +checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af" [[package]] name = "linux-raw-sys" @@ -180,16 +239,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] -name = "memchr" -version = "2.7.6" +name = "log" +version = "0.4.29" 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]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" 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]] name = "proc-macro2" @@ -211,9 +286,9 @@ dependencies = [ [[package]] name = "r-efi" -version = "5.3.0" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] name = "regex" @@ -229,9 +304,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", @@ -240,9 +315,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.8" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "rustix" @@ -266,6 +341,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + [[package]] name = "serde" version = "1.0.228" @@ -273,6 +354,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ "serde_core", + "serde_derive", ] [[package]] @@ -314,6 +396,12 @@ version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77" +[[package]] +name = "smawk" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" + [[package]] name = "syn" version = "2.0.117" @@ -338,6 +426,17 @@ dependencies = [ "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]] name = "thiserror" version = "2.0.18" @@ -360,9 +459,15 @@ dependencies = [ [[package]] name = "unicode-ident" -version = "1.0.22" +version = "1.0.24" 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]] name = "unicode-width" @@ -370,6 +475,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[package]] name = "walkdir" version = "2.5.0" @@ -389,6 +500,49 @@ dependencies = [ "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]] name = "winapi-util" version = "0.1.11" @@ -418,6 +572,88 @@ name = "wit-bindgen" version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "xtask" @@ -436,6 +672,6 @@ checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" [[package]] name = "zmij" -version = "1.0.17" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02aae0f83f69aafc94776e879363e9771d7ecbffe2c7fbb6c14c5e00dfe88439" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml index f4f3cf7..816d198 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,8 +17,10 @@ clap = { default-features = false, features = [ "std", "help", "derive" clap_complete = "4.6.0" dialoguer = { default-features = false, version = "0.12.0" } regex = "1.12.3" +serde = { features = [ "derive" ], version = "1.0.149" } serde_json = "1.0.149" tempfile = "3.27.0" +textwrap = "0.16.2" thiserror = "2.0.18" walkdir = "2.5.0" yansi = "1.0.1" diff --git a/crates/xtask/src/main.rs b/crates/xtask/src/main.rs index 4141510..0254d57 100644 --- a/crates/xtask/src/main.rs +++ b/crates/xtask/src/main.rs @@ -43,6 +43,7 @@ enum Binary { Ns, Nb, Nd, + Ni, Nu, } @@ -53,6 +54,7 @@ impl Binary { Self::Ns => "ns", Self::Nb => "nb", Self::Nd => "nd", + Self::Ni => "ni", Self::Nu => "nu", } } @@ -94,8 +96,14 @@ fn create_multicall_binaries( ); } - let multicall_binaries = - [Binary::Nr, Binary::Ns, Binary::Nb, Binary::Nd, Binary::Nu]; + let multicall_binaries = [ + Binary::Nr, + Binary::Ns, + Binary::Nb, + Binary::Nd, + Binary::Ni, + Binary::Nu, + ]; let bin_path = Path::new(bin_dir); for binary in multicall_binaries { @@ -158,7 +166,7 @@ fn generate_completions( println!("completion file generated: {}", completion_file.display()); // 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 { let symlink_path = output_dir.join(format!("{name}.{shell}")); if symlink_path.exists() { diff --git a/eh/Cargo.toml b/eh/Cargo.toml index 92dd751..98c219b 100644 --- a/eh/Cargo.toml +++ b/eh/Cargo.toml @@ -15,8 +15,10 @@ 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 walkdir.workspace = true yansi.workspace = true diff --git a/eh/src/commands/info.rs b/eh/src/commands/info.rs new file mode 100644 index 0000000..03b487a --- /dev/null +++ b/eh/src/commands/info.rs @@ -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, + description: Option, + long_description: Option, + license: Option, + homepage: Option, + platforms: Option>, + broken: Option, + insecure: Option, + #[serde(rename = "unfree")] + unfree: Option, +} + +#[derive(Debug, Deserialize)] +struct PackageOutputs { + #[serde(flatten)] + outputs: HashMap, +} + +pub fn handle_info(args: &[String]) -> Result { + // 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 = 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 = 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()); + } +} diff --git a/eh/src/commands/mod.rs b/eh/src/commands/mod.rs index 6cc417d..843b886 100644 --- a/eh/src/commands/mod.rs +++ b/eh/src/commands/mod.rs @@ -16,6 +16,7 @@ use crate::{ }, }; +pub mod info; pub mod update; const DEFAULT_BUFFER_SIZE: usize = 4096; diff --git a/eh/src/lib.rs b/eh/src/lib.rs index 6cb4f31..d0ddfe6 100644 --- a/eh/src/lib.rs +++ b/eh/src/lib.rs @@ -36,6 +36,11 @@ pub enum Command { #[arg(trailing_var_arg = true)] args: Vec, }, + /// Show package information + Info { + #[arg(trailing_var_arg = true)] + args: Vec, + }, /// Update flake inputs interactively Update { #[arg(trailing_var_arg = true)] diff --git a/eh/src/main.rs b/eh/src/main.rs index e350510..a9133ac 100644 --- a/eh/src/main.rs +++ b/eh/src/main.rs @@ -31,6 +31,8 @@ fn handle_command(command: &str, args: &[String]) -> error::Result { let classifier = util::DefaultNixErrorClassifier; match command { + "info" => commands::info::handle_info(args), + "update" => commands::update::handle_update(args), "run" | "shell" | "build" | "develop" => { commands::handle_nix_command( @@ -56,6 +58,7 @@ fn dispatch_multicall( "ns" => "shell", "nb" => "build", "nd" => "develop", + "ni" => "info", "nu" => "update", _ => return None, }; @@ -107,6 +110,8 @@ fn run_app() -> error::Result { Some(Command::Develop { args }) => handle_command("develop", &args), + Some(Command::Info { args }) => handle_command("info", &args), + Some(Command::Update { args }) => handle_command("update", &args), None => { diff --git a/eh/src/util.rs b/eh/src/util.rs index 229e303..fce099b 100644 --- a/eh/src/util.rs +++ b/eh/src/util.rs @@ -1,5 +1,5 @@ use std::{ - io::{BufWriter, Write}, + io::{BufWriter, IsTerminal, Write}, path::{Path, PathBuf}, 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 = + LazyLock::new(|| Regex::new(r#"Did you mean (?:one of )?(.+?)\?"#).unwrap()); + /// Trait for extracting store paths and hashes from nix output. pub trait HashExtractor { /// 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. /// 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('#') { - 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 { 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) { let old_hash = hash_extractor.extract_old_hash(&stderr); - // Ask for confirmation before fixing hash - let should_fix = dialoguer::Confirm::new() - .with_prompt(format!( - "Hash mismatch detected for {}. Update hash in local .nix files?", + // Ask for confirmation before fixing hash (skip in non-interactive mode) + let should_fix = if std::io::stdin().is_terminal() { + dialoguer::Confirm::new() + .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() - )) - .default(true) - .interact() - .map_err(|e| EhError::Io(std::io::Error::other(e)))?; + ); + false + }; 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 }); } @@ -592,6 +618,9 @@ pub fn handle_nix_with_retry( .write_all(&output.stderr) .map_err(EhError::Io)?; + // Print contextual suggestions for common errors + print_error_suggestions(&output.stderr); + match output.status.code() { Some(code) => Err(EhError::ProcessExit { code }), // 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 { + 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::>() + .join(", "); + log_info!("Did you mean: {}?", formatted); + } + } +} + #[cfg(test)] mod tests { use std::io::Write;