void-cli 0.0.2

CLI for void — anonymous encrypted source control
//! The `void rm` command implementation.
//!
//! Removes files from the index and optionally from the working tree.

use std::path::Path;

use serde::Serialize;
use void_core::workspace::stage::{remove_paths, RemoveOptions};

use crate::context::{build_void_context, void_err_to_cli};
use crate::output::{run_command, CliError, CliOptions};

/// JSON output structure for the rm command.
#[derive(Debug, Clone, Serialize)]
pub struct RmOutput {
    /// Files that were removed.
    pub removed: Vec<String>,
    /// Total count of files removed.
    pub count: usize,
}

/// Run the rm command.
///
/// # Arguments
///
/// * `cwd` - Current working directory.
/// * `paths` - Paths to remove.
/// * `cached_only` - If true, only remove from index (keep files on disk).
/// * `force` - If true, remove even if file has uncommitted changes.
/// * `recursive` - If true, recursively remove directories.
/// * `opts` - CLI options.
///
/// # Returns
///
/// Returns `Ok(())` on success, or a `CliError` on failure.
pub fn run(
    cwd: &Path,
    paths: Vec<String>,
    cached_only: bool,
    force: bool,
    recursive: bool,
    opts: &CliOptions,
) -> Result<(), CliError> {
    // Filter empty/whitespace paths
    let paths: Vec<String> = paths.into_iter().filter(|p| !p.trim().is_empty()).collect();

    run_command("rm", opts, |ctx| {
        // Validate paths provided (inside run_command for proper JSON error output)
        if paths.is_empty() {
            return Err(CliError::invalid_args(
                "No paths specified. Provide files or directories to remove.",
            ));
        }

        // Get repository context
        let void_ctx = build_void_context(cwd)?;

        ctx.progress("Removing files...");

        // Remove the paths
        let remove_opts = RemoveOptions {
            ctx: void_ctx,
            paths,
            cached_only,
            force,
            recursive,
        };

        let result = remove_paths(remove_opts).map_err(void_err_to_cli)?;

        // Print human-readable summary if not in JSON mode
        if !ctx.use_json() {
            let total_removed = result.removed.len();

            if total_removed == 0 {
                ctx.info("Nothing to remove");
            } else {
                for path in &result.removed {
                    ctx.info(format!("rm '{}'", path));
                }
            }
        }

        let count = result.removed.len();
        Ok(RmOutput {
            removed: result.removed,
            count,
        })
    })
}

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

    #[test]
    fn test_rm_output_serialization() {
        let output = RmOutput {
            removed: vec!["src/main.rs".to_string(), "src/lib.rs".to_string()],
            count: 2,
        };

        let json = serde_json::to_string(&output).unwrap();
        assert!(json.contains("\"removed\""));
        assert!(json.contains("\"count\":2"));
        assert!(json.contains("src/main.rs"));
        assert!(json.contains("src/lib.rs"));
    }

    #[test]
    fn test_rm_output_empty() {
        let output = RmOutput {
            removed: vec![],
            count: 0,
        };

        let json = serde_json::to_string(&output).unwrap();
        assert!(json.contains("\"removed\":[]"));
        assert!(json.contains("\"count\":0"));
    }
}