diff --git a/apps/native/src-tauri/src/evolve/search_packages.rs b/apps/native/src-tauri/src/evolve/search_packages.rs index dce77905..1e4c0b89 100644 --- a/apps/native/src-tauri/src/evolve/search_packages.rs +++ b/apps/native/src-tauri/src/evolve/search_packages.rs @@ -32,6 +32,42 @@ pub struct SearchPackageResult { pub install_via: SearchResultInstallTarget, } +/// Wrapper for `nix registry list` that returns the raw output as a string, or an error if the command fails. +/// Clients can parse it themselves. +fn nix_registry_list(config_dir: &str) -> Result { + let mut cmd = Command::new("nix"); + cmd.args(["registry", "list"]) + .current_dir(config_dir) + .env("PATH", crate::system::nix::get_nix_path()) + .env("NIX_CONFIG", "experimental-features = nix-command flakes"); + + let output = cmd.output()?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + let truncated_stderr = truncate_error(&stderr, 8000); + return Err(anyhow::anyhow!( + "nix registry list failed with status {:?}: {}", + output.status.code(), + truncated_stderr + )); + } + + Ok(String::from_utf8_lossy(&output.stdout).to_string()) +} + +/// Determine prior to searching whether a channel is registered and can be searched, +/// to avoid unnecessary command execution and errors. +/// Check if the output contains the channel in a "flake:channel" format. +fn channel_is_registered(registry_list: &str, channel: &str) -> bool { + // Split each line by whitespace and check if any token equals "flake:". + // Avoids partial matches. + let channel_pattern = format!("flake:{}", channel); + registry_list + .lines() + .any(|line| line.split_whitespace().any(|token| token == channel_pattern)) +} + /// Search a single channel and return a list of SearchPackageResult /// results. fn search_single_channel( @@ -154,11 +190,19 @@ fn collect_from_channels( limit: u64, structured: &mut Vec, ) -> Result { + let registry_list = nix_registry_list(config_dir)?; + for channel in channels { if structured.len() >= limit as usize { return Ok(true); } + if !channel_is_registered(®istry_list, channel) { + // CONSIDER: Whether we need to surface this to the agent somehow. + log::warn!("Channel '{}' is not registered, skipping search for this channel", channel); + continue; + } + let channel_results = search_single_channel(config_dir, query_term, use_regex, channel)?; let complete = process_channel_results(structured, channel_results, limit)?; if complete { @@ -398,4 +442,12 @@ mod tests { assert_eq!(structured.len(), 2, "expected deduplication by attr_path"); } + + #[test] + fn channel_registration_check() { + let registry_list = "global flake:agda github:agda/agd\nglobal flake:nixpkgs github:NixOS/nixpkgs/nixpkgs-unstable\nglobal flake:nix github:NixOS/nix\n"; + assert!(channel_is_registered(registry_list, "nixpkgs")); + assert!(channel_is_registered(registry_list, "nix")); + assert!(!channel_is_registered(registry_list, "unregistered-channel")); + } }