bijux-cli 0.3.6

Command-line runtime for automation, plugin-driven tools, and interactive workflows with structured output.
Documentation
#![forbid(unsafe_code)]
//! History feature operations.

use std::path::Path;

use anyhow::Result;
use serde_json::{json, Value};

use crate::infrastructure::state_store::{read_history_report, write_history_entries};

pub(crate) const DEFAULT_HISTORY_LIMIT: usize = 20;
pub(crate) const MAX_HISTORY_LIMIT: usize = 10_000;

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct HistoryListOptions {
    pub(crate) limit: usize,
    pub(crate) filter_contains: Option<String>,
    pub(crate) sort_by_timestamp: bool,
}

impl Default for HistoryListOptions {
    fn default() -> Self {
        Self { limit: DEFAULT_HISTORY_LIMIT, filter_contains: None, sort_by_timestamp: false }
    }
}

pub(crate) fn list_history(history_file: &Path, options: &HistoryListOptions) -> Result<Value> {
    let limit = options.limit.clamp(1, MAX_HISTORY_LIMIT);
    let requires_full_scan = options.filter_contains.is_some() || options.sort_by_timestamp;
    let report =
        read_history_report(history_file, if requires_full_scan { usize::MAX } else { limit })?;
    let total_entries = report.total_entries;
    let mut indexed = report.entries.into_iter().enumerate().collect::<Vec<_>>();

    if let Some(needle) = options.filter_contains.as_deref() {
        indexed.retain(|(_, entry)| {
            entry
                .get("command")
                .and_then(Value::as_str)
                .map(|command| command.contains(needle))
                .unwrap_or(false)
        });
    }

    if options.sort_by_timestamp {
        indexed.sort_by(|(left_idx, left), (right_idx, right)| {
            let left_timestamp =
                left.get("timestamp").and_then(Value::as_f64).unwrap_or(f64::NEG_INFINITY);
            let right_timestamp =
                right.get("timestamp").and_then(Value::as_f64).unwrap_or(f64::NEG_INFINITY);
            left_timestamp.total_cmp(&right_timestamp).then_with(|| left_idx.cmp(right_idx))
        });
    }

    if indexed.len() > limit {
        indexed = indexed.split_off(indexed.len() - limit);
    }
    let entries = indexed.into_iter().map(|(_, entry)| entry).collect::<Vec<_>>();

    Ok(json!({
        "entries": entries,
        "summary": {
            "source_format": report.source_format,
            "file_bytes": report.file_bytes,
            "total_entries": total_entries,
            "accepted_entries": total_entries,
            "observed_entries": report.observed_entries,
            "returned_entries": entries.len(),
            "dropped_invalid_entries": report.dropped_invalid_entries,
            "truncated_command_entries": report.truncated_command_entries,
                "limit": limit,
                "filter_applied": options.filter_contains.is_some(),
                "sort_mode": if options.sort_by_timestamp { "timestamp" } else { "preserve" },
        }
    }))
}

pub(crate) fn clear_history(history_file: &Path, force: bool) -> Result<Value> {
    let (removed, dropped_invalid_entries, source_format, read_error) =
        match read_history_report(history_file, usize::MAX) {
            Ok(report) => (
                report.entries.len(),
                report.dropped_invalid_entries,
                Some(report.source_format),
                None,
            ),
            Err(error) if force => (0, 0, None, Some(error.to_string())),
            Err(error) => return Err(error),
        };
    write_history_entries(history_file, &[])?;

    Ok(json!({
        "status": "cleared",
        "removed_entries": removed,
        "dropped_invalid_entries": dropped_invalid_entries,
        "source_format": source_format,
        "force_applied": force,
        "corruption_ignored": read_error.is_some(),
        "read_error": read_error,
        "file": history_file,
    }))
}