zilliz 1.4.2

TUI and CLI tool for managing Zilliz Cloud clusters and Milvus operations
Documentation
use std::fs::OpenOptions;
use std::io::{self, Write};

use anyhow::Result;
use fs2::FileExt;

use crate::cli::args::HistoryCommands;
use crate::cli::formatter;
use crate::config::history::{read_all_records, resolve_history_path, HistoryRecord};
use crate::config::manager::ConfigManager;

pub async fn run(
    config_mgr: &ConfigManager,
    subcmd: Option<HistoryCommands>,
    output_format: &str,
    no_header: bool,
) -> Result<()> {
    match subcmd {
        None | Some(HistoryCommands::List { .. }) => {
            let (limit, all) = match subcmd {
                Some(HistoryCommands::List { limit, all }) => (limit, all),
                _ => (50, false),
            };
            list(config_mgr, limit, all, output_format, no_header)
        }
        Some(HistoryCommands::Search { keyword }) => {
            search(config_mgr, &keyword, output_format, no_header)
        }
        Some(HistoryCommands::Clear { force }) => clear(config_mgr, force),
    }
}

fn records_to_json(records: &[HistoryRecord]) -> serde_json::Value {
    serde_json::to_value(records).unwrap_or(serde_json::Value::Array(vec![]))
}

fn print_records(records: &[HistoryRecord], output_format: &str, no_header: bool) {
    let json = records_to_json(records);

    if records.is_empty() {
        // For structured formats, output an empty collection instead of a human message
        match output_format {
            "json" => {
                println!("[]");
                return;
            }
            "yaml" => {
                println!("[]");
                return;
            }
            "csv" => return,
            "text" => return,
            _ => {
                eprintln!("No history records found.");
                return;
            }
        }
    }

    match output_format {
        "json" => println!("{}", formatter::format_json(&json)),
        "yaml" => println!("{}", formatter::format_yaml(&json)),
        "csv" => {
            println!("{}", formatter::format_csv(&json, no_header));
        }
        "text" => println!("{}", formatter::format_text(&json)),
        _ => {
            // table format
            let columns = &["timestamp", "command", "command_type", "success"];
            if let serde_json::Value::Array(ref arr) = json {
                let refs: Vec<&serde_json::Value> = arr.iter().collect();
                println!(
                    "{}",
                    formatter::format_table_with_opts(&refs, columns, no_header, None)
                );
            }
        }
    }
}

fn list(
    config_mgr: &ConfigManager,
    limit: usize,
    all: bool,
    output_format: &str,
    no_header: bool,
) -> Result<()> {
    let path = resolve_history_path(config_mgr);
    let records = read_all_records(&path);

    // Reverse chronological order (newest first)
    let mut display_records: Vec<HistoryRecord> = records.into_iter().rev().collect();

    if !all {
        display_records.truncate(limit);
    }

    print_records(&display_records, output_format, no_header);
    Ok(())
}

fn search(
    config_mgr: &ConfigManager,
    keyword: &str,
    output_format: &str,
    no_header: bool,
) -> Result<()> {
    let path = resolve_history_path(config_mgr);
    let records = read_all_records(&path);

    let keyword_lower = keyword.to_lowercase();
    let matched: Vec<HistoryRecord> = records
        .into_iter()
        .rev()
        .filter(|r| r.command.to_lowercase().contains(&keyword_lower))
        .collect();

    print_records(&matched, output_format, no_header);
    Ok(())
}

fn clear(config_mgr: &ConfigManager, force: bool) -> Result<()> {
    let path = resolve_history_path(config_mgr);

    if !force {
        eprint!("Are you sure you want to clear all history? [y/N] ");
        io::stderr().flush()?;
        let mut input = String::new();
        io::stdin().read_line(&mut input)?;
        if input.trim().to_lowercase() != "y" {
            eprintln!("Cancelled.");
            return Ok(());
        }
    }

    if path.exists() {
        // Acquire exclusive lock, truncate to zero, then remove -- all while
        // holding the lock to prevent concurrent appends from being lost.
        let file = OpenOptions::new()
            .write(true)
            .truncate(false)
            .open(&path)
            .map_err(|e| anyhow::anyhow!("Failed to open history file: {}", e))?;

        file.lock_exclusive()
            .map_err(|e| anyhow::anyhow!("Failed to lock history file: {}", e))?;

        file.set_len(0)
            .map_err(|e| anyhow::anyhow!("Failed to truncate history file: {}", e))?;

        // Remove the file while still holding the lock. On Unix this is safe
        // because the inode stays alive until the last fd is closed. On Windows
        // the truncate above already cleared the content.
        let remove_result = std::fs::remove_file(&path);

        let _ = file.unlock();

        remove_result.map_err(|e| anyhow::anyhow!("Failed to remove history file: {}", e))?;
    }

    eprintln!("History cleared.");
    Ok(())
}