flk 0.6.3

A CLI tool for managing flake.nix devShell environments
Documentation
//! # Nix Command Interface
//!
//! Low-level interface for executing Nix commands from the CLI.
//!
//! This module provides functions for running Nix commands and checking
//! Nix availability, abstracting the subprocess management details.
//!
//! ## Testing seam
//!
//! Production callers reach `nix` via [`run_nix_command`], which can be
//! redirected to a fake in tests using [`with_nix_runner`]. The override is
//! thread-local so parallel test runs stay isolated.

use anyhow::{Context, Result};
use std::cell::RefCell;
use std::process::Command;

type Runner = dyn Fn(&[&str]) -> Result<(String, String, bool)>;

thread_local! {
    static OVERRIDE: RefCell<Option<Box<Runner>>> = const { RefCell::new(None) };
}

/// Execute a Nix command with the given arguments.
///
/// Returns a tuple of (stdout, stderr, success) where success indicates
/// whether the command exited with status code 0.
///
/// In tests, if a runner has been installed via [`with_nix_runner`], the call
/// is dispatched to that fake instead of spawning a real `nix` process.
pub fn run_nix_command(args: &[&str]) -> Result<(String, String, bool)> {
    let mocked = OVERRIDE.with(|cell| cell.borrow().as_ref().map(|f| f(args)));
    if let Some(out) = mocked {
        return out;
    }

    let output = Command::new("nix")
        .args(args)
        .output()
        .context("Failed to execute nix command")?;

    let stdout = String::from_utf8(output.stdout)?;
    let stderr = String::from_utf8(output.stderr)?;
    let success = output.status.success();

    Ok((stdout, stderr, success))
}

/// Check if Nix is available on the system.
///
/// Returns `true` if the `nix --version` command executes successfully,
/// indicating that Nix is installed and accessible in PATH. When a runner
/// override is active (tests), reports `true` without spawning a process.
pub fn check_nix_available() -> bool {
    if OVERRIDE.with(|cell| cell.borrow().is_some()) {
        return true;
    }
    Command::new("nix").arg("--version").output().is_ok()
}

/// Install a fake nix runner for the duration of `body`. Test-only.
///
/// The runner receives the args that would have been passed to `nix` and must
/// return the same `(stdout, stderr, success)` tuple `run_nix_command` does.
#[cfg(test)]
pub fn with_nix_runner<F, R>(runner: F, body: impl FnOnce() -> R) -> R
where
    F: Fn(&[&str]) -> Result<(String, String, bool)> + 'static,
{
    OVERRIDE.with(|c| *c.borrow_mut() = Some(Box::new(runner)));
    let result = body();
    OVERRIDE.with(|c| *c.borrow_mut() = None);
    result
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn runner_override_intercepts_calls() {
        let result = with_nix_runner(
            |args| {
                assert_eq!(args, &["search", "nixpkgs", "ripgrep"]);
                Ok(("hit\n".into(), String::new(), true))
            },
            || run_nix_command(&["search", "nixpkgs", "ripgrep"]).unwrap(),
        );
        assert_eq!(result, ("hit\n".to_string(), String::new(), true));
    }

    #[test]
    fn runner_override_makes_nix_appear_available() {
        with_nix_runner(
            |_| Ok((String::new(), String::new(), true)),
            || assert!(check_nix_available()),
        );
    }

    #[test]
    fn runner_override_clears_after_body() {
        with_nix_runner(|_| Ok((String::new(), String::new(), true)), || {});
        // No runner installed afterwards: the inner state should be None again.
        OVERRIDE.with(|c| assert!(c.borrow().is_none()));
    }
}