flk 0.6.3

A CLI tool for managing flake.nix devShell environments
Documentation
//! # Remove Command Handler
//!
//! Removes packages from the development environment.
//! If the package is version-pinned, also removes the pin entry from `pins.nix`.

use anyhow::{bail, Context, Result};
use colored::Colorize;
use flk::flake::parsers::overlays::remove_pinned_package_with_cleanup;
use std::fs;
use std::path::Path;

use flk::flake::parsers::{packages::parse_packages_section, utils::resolve_profile};

/// Remove a package from the development environment.
///
/// Removes the package from the active profile file. If the package was
/// version-pinned, also cleans up the corresponding entry in `.flk/pins.nix`.
///
/// # Arguments
///
/// * `package` - Package name to remove
/// * `target_profile` - Optional profile override
pub fn run_remove(package: &str, target_profile: Option<String>) -> Result<()> {
    let profile = resolve_profile(target_profile)?;
    let flake_path = Path::new(".flk/profiles/").join(format!("{}.nix", profile));

    if package.trim().is_empty() {
        bail!("Package name cannot be empty!");
    }

    let flake_content = fs::read_to_string(&flake_path).with_context(|| {
        format!(
            "Failed to read profile file at '{}'. Have you run 'flk init'?",
            flake_path.display()
        )
    })?;
    let section = parse_packages_section(&flake_content)?;

    if !section.package_exists(package) {
        bail!(
            "Package '{}' is not present in the packages declaration",
            package
        );
    }

    // Check if package is pinned to a version
    if section
        .entries
        .iter()
        .any(|e| e.name == package && e.version.is_some())
    {
        let pins_path = ".flk/pins.nix";
        let pins_content = fs::read_to_string(pins_path).context("Failed to read pins.nix file")?;

        let updated_pins_content = remove_pinned_package_with_cleanup(&pins_content, package)?;
        fs::write(pins_path, updated_pins_content).context("Failed to write pins.nix")?;
    }

    let updated_content = section.remove_package(&flake_content, package)?;
    fs::write(flake_path, updated_content).context("Failed to write flake.nix")?;

    println!(
        "{} Package '{}' removed successfully!",
        "".green().bold(),
        package
    );

    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::commands::cwd_test_guard;
    use tempfile::TempDir;

    /// Build a minimal `.flk/` layout in `dir` with a `rust` profile that
    /// contains the given package list, and chdir into it for the test.
    fn setup_profile(dir: &TempDir, rust_packages: &str, pins: Option<&str>) {
        let flk = dir.path().join(".flk");
        std::fs::create_dir_all(flk.join("profiles")).unwrap();
        std::fs::write(flk.join("default.nix"), "{ defaultShell = \"rust\"; }\n").unwrap();
        std::fs::write(
            flk.join("profiles").join("rust.nix"),
            format!(
                "{{pkgs, ...}}: {{\n  packages = [\n{}  ];\n}}\n",
                rust_packages
            ),
        )
        .unwrap();
        if let Some(pins_content) = pins {
            std::fs::write(flk.join("pins.nix"), pins_content).unwrap();
        }
        std::env::set_current_dir(dir.path()).unwrap();
    }

    #[test]
    fn rejects_empty_package_name() {
        let _guard = cwd_test_guard();
        let tmp = TempDir::new().unwrap();
        setup_profile(&tmp, "    pkgs.ripgrep\n", None);

        let err = run_remove("   ", Some("rust".into())).unwrap_err();
        assert!(err.to_string().contains("empty"), "got: {err}");
    }

    #[test]
    fn errors_when_package_not_in_profile() {
        let _guard = cwd_test_guard();
        let tmp = TempDir::new().unwrap();
        setup_profile(&tmp, "    pkgs.ripgrep\n", None);

        let err = run_remove("does-not-exist", Some("rust".into())).unwrap_err();
        assert!(err.to_string().contains("not present"), "got: {err}");
    }

    #[test]
    fn removes_unversioned_package_from_profile() {
        let _guard = cwd_test_guard();
        let tmp = TempDir::new().unwrap();
        setup_profile(&tmp, "    pkgs.ripgrep\n    pkgs.fd\n", None);

        run_remove("ripgrep", Some("rust".into())).unwrap();

        let after = std::fs::read_to_string(tmp.path().join(".flk/profiles/rust.nix")).unwrap();
        assert!(
            !after.contains("ripgrep"),
            "ripgrep still present:\n{after}"
        );
        assert!(after.contains("pkgs.fd"));
    }
}