flk 0.6.3

A CLI tool for managing flake.nix devShell environments
Documentation
//! # Export Command Handler
//!
//! Export flake configurations to Docker, Podman, or JSON formats.

use std::{path::Path, process::Command};

use anyhow::{Context, Ok, Result};
use clap::ValueEnum;
use std::fs::File;

use crate::nix::run_nix_command;
use flk::flake::parsers::{flake::parse_flake, utils::resolve_profile};
use flk::utils::visual::with_spinner;

/// Export format options.
#[derive(Debug, Clone, ValueEnum)]
#[value(rename_all = "lowercase")]
pub enum ExportType {
    /// Export as a Docker image
    Docker,
    /// Export as a Podman image
    Podman,
    /// Export configuration as JSON
    Json,
}

/// Export the flake configuration to the specified format.
///
/// Supports Docker image, Podman image, and JSON export. Docker and Podman
/// exports build a Nix-based container image and load it into the respective
/// runtime. JSON export serializes the parsed flake configuration to `flake.json`.
///
/// # Arguments
///
/// * `export_type` - Target format (Docker, Podman, or JSON)
/// * `target_profile` - Optional profile override
pub fn run_export(export_type: &ExportType, target_profile: Option<String>) -> Result<()> {
    let profile = resolve_profile(target_profile)?;
    match export_type {
        ExportType::Docker => {
            println!("Exporting flake.nix to Docker image...");
            let (_, _, success) = with_spinner("<export-docker>", || {
                run_nix_command(&[
                    "build",
                    &format!(".#docker-{}", profile.as_str()),
                    "--out-link",
                    ".flk/result",
                    "--impure",
                ])
                .context("Failed to build Docker image from flake.nix")
            })?;
            println!("Docker image created successfully ✅");
            let file = File::open(".flk/result").context("Failed to open .flk/result")?;

            let output = with_spinner("<load-image>", || {
                Command::new("docker")
                    .args(["load"])
                    .stdin(file)
                    .output()
                    .context("Failed to load Docker image")
            })?;
            println!(
                "Docker image export {}",
                if success {
                    "succeeded ✅"
                } else {
                    "failed ❌"
                }
            );
            println!("{}", String::from_utf8_lossy(&output.stdout));
        }
        ExportType::Podman => {
            println!("Exporting flake.nix to Podman image...");
            let (_, _, success) = with_spinner("<export-podman>", || {
                run_nix_command(&[
                    "build",
                    &format!(".#podman-{}", profile.as_str()),
                    "--out-link",
                    ".flk/result",
                    "--impure",
                ])
                .context("Failed to build Podman image from flake.nix")
            })?;
            println!("Podman image created successfully ✅");
            let file = File::open(".flk/result").context("Failed to open .flk/result")?;

            let output = with_spinner("<load-image>", || {
                Command::new("podman")
                    .args(["load"])
                    .stdin(file)
                    .output()
                    .context("Failed to load Podman image")
            })?;
            println!(
                "Podman image export {}",
                if success {
                    "succeeded ✅"
                } else {
                    "failed ❌"
                }
            );
            println!("{}", String::from_utf8_lossy(&output.stdout));
        }
        ExportType::Json => {
            let flake_path = Path::new("flake.nix");
            let flake_content = parse_flake(flake_path.to_str().unwrap())?;

            // Serialize the flake content to JSON file
            let json_output = serde_json::to_string_pretty(&flake_content)
                .context("Failed to serialize flake content to JSON")?;
            std::fs::write("flake.json", json_output).context("Failed to write flake.json file")?;
            println!("Flake export to JSON succeeded ✅");
        }
    }
    Ok(())
}

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

    fn setup_flake(dir: &TempDir) {
        std::fs::write(
            dir.path().join("flake.nix"),
            r#"{
  description = "test";
  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
    flake-utils.url = "github:numtide/flake-utils";
  };
}
"#,
        )
        .unwrap();
        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"),
            r#"{pkgs, ...}: {
  packages = [
    pkgs.ripgrep
  ];

  envVars = {};

  commands = [
  ];

  shellHook = '''';
}
"#,
        )
        .unwrap();
        std::env::set_current_dir(dir.path()).unwrap();
    }

    #[test]
    fn json_export_writes_flake_json_in_cwd() {
        let _guard = cwd_test_guard();
        let tmp = TempDir::new().unwrap();
        setup_flake(&tmp);

        run_export(&ExportType::Json, Some("rust".into()))
            .unwrap_or_else(|e| panic!("export failed: {e:?}"));

        let json = std::fs::read_to_string(tmp.path().join("flake.json")).unwrap();
        // Round-trip enough to know it's structured output, not an empty file.
        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
        assert!(parsed.is_object(), "expected JSON object, got {parsed}");
    }

    #[test]
    fn json_export_fails_when_flake_missing() {
        let _guard = cwd_test_guard();
        let tmp = TempDir::new().unwrap();
        // Only set up .flk/, deliberately omit flake.nix.
        let flk = tmp.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"),
            r#"{pkgs, ...}: {
  packages = [
    pkgs.ripgrep
  ];

  envVars = {};

  commands = [
  ];

  shellHook = '''';
}
"#,
        )
        .unwrap();
        std::env::set_current_dir(tmp.path()).unwrap();

        let err = run_export(&ExportType::Json, Some("rust".into())).unwrap_err();
        let msg = format!("{err:?}");
        assert!(
            msg.contains("flake.nix") || msg.to_lowercase().contains("no such file"),
            "unexpected error: {msg}"
        );
    }
}