use anyhow::{bail, Context, Result};
use colored::Colorize;
use flk::flake::parsers::overlays::add_pinned_package;
use std::{fs, path};
use crate::nix::run_nix_command;
use flk::flake::parsers::{packages::parse_packages_section, utils::resolve_profile};
use flk::utils::visual::with_spinner;
pub fn run_add(
package: &str,
version: Option<String>,
target_profile: Option<String>,
) -> Result<()> {
let profile = resolve_profile(target_profile)?;
let flake_path = path::Path::new(".flk/profiles/").join(format!("{}.nix", profile));
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).with_context(|| {
format!(
"Failed to parse packages section in profile file '{}'",
flake_path.display()
)
})?;
if package.trim().is_empty() {
bail!("Package name cannot be empty");
}
with_spinner("Validating package...", || validate_package_exists(package))?;
let (package_to_add, package_pin) = if let Some(ver) = &version {
println!(
"{} Adding package: {} (pinned to version {})",
"→".blue().bold(),
package.green(),
ver.yellow()
);
let full_pin = with_spinner("Fetching nixpkgs pin for the package...", || {
get_nix_package_pin_full(package, ver)
})?;
(format!("pkgs.\"{}@{}\"", package, ver), Some(full_pin))
} else {
println!("{} Adding package: {}", "→".blue().bold(), package.green());
(format!("pkgs.{}", package), None)
};
if section.package_exists(&package_to_add) {
bail!(
"Package '{}' is already in the packages declaration",
package_to_add
);
}
if let Some(pin) = &package_pin {
println!(
"{} Package will be pinned to nixpkgs: {}",
"→".blue().bold(),
pin.full_ref.yellow()
);
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 = add_pinned_package(
&pins_content,
&pin.hash,
&pin.full_ref,
package,
version
.as_deref()
.expect("version must be Some when package_pin is Some"),
)
.context("Failed to add pinned package to pins.nix")?;
fs::write(pins_path, updated_pins).context("Failed to write updated pins.nix")?;
}
let updated_content = section.add_package(&flake_content, &package_to_add, None);
fs::write(flake_path, updated_content).context("Failed to write flake.nix")?;
println!(
"{} Package '{}' added successfully!",
"✓".green().bold(),
package_to_add
);
Ok(())
}
struct PinInfo {
hash: String,
full_ref: String,
}
fn get_nix_package_pin_full(package: &str, version: &str) -> Result<PinInfo> {
let package_with_version = format!("{}@{}", package, version);
let (stdout, stderr, success) = run_nix_command(&[
"run",
"github:vic/nix-versions",
&package_with_version,
"--",
"--one",
])
.context("Failed to execute nix eval")?;
if !success || stderr.contains("no packages found") {
bail!(
"Package {} does not exist or is marked as insecure. Aborting",
package
);
}
let stdout = stdout
.lines()
.find(|line| line.contains("nixpkgs"))
.context("Failed to find nixpkgs pin in nix search output")?;
let pin = stdout
.split_whitespace()
.nth(2)
.context("Failed to extract nixpkgs pin value")
.map(|s| s.trim().to_string())?;
let pin_hash = pin
.split('/')
.nth(1)
.and_then(|s| s.split('#').next())
.context("Failed to extract nixpkgs pin from output")?
.trim()
.to_string();
let pin_ref = format!("github:NixOS/nixpkgs/{}", pin_hash);
Ok(PinInfo {
hash: pin_hash,
full_ref: pin_ref,
})
}
fn validate_package_exists(package: &str) -> Result<()> {
let (_, stderr, success) = run_nix_command(&["run", "github:vic/nix-versions", package])
.context("Failed to execute nix eval")?;
if !success || stderr.contains("no packages found") {
bail!(
"Package {} does not exist or is marked as insecure. Aborting",
package
);
}
Ok(())
}