use chrono::{NaiveDate, TimeZone, Utc};
use colored::Colorize;
use crate::adapters::audit::json_audit_logger::JsonAuditLogger;
use crate::cli::output;
use crate::config::app_config::AppConfig;
use crate::core::errors::{Result, VaulticError};
use crate::core::models::audit_entry::{AuditAction, AuditEntry};
use crate::core::traits::audit::AuditLogger;
pub fn execute(author: Option<&str>, since: Option<&str>, last: Option<usize>) -> Result<()> {
let vaultic_dir = crate::cli::context::vaultic_dir();
if !vaultic_dir.exists() {
return Err(VaulticError::InvalidConfig {
detail: "Vaultic not initialized. Run 'vaultic init' first.".into(),
});
}
let config = AppConfig::load(vaultic_dir)?;
let audit_section = config.audit.as_ref();
let logger = JsonAuditLogger::from_config(vaultic_dir, audit_section);
let since_dt = since.map(parse_since).transpose()?;
let entries = logger.query(author, since_dt)?;
if entries.is_empty() {
output::header("vaultic log");
output::warning("No audit entries found");
if author.is_some() || since.is_some() {
println!(" Try removing filters to see all entries.");
}
return Ok(());
}
let display: Vec<&AuditEntry> = match last {
Some(n) => entries
.iter()
.rev()
.take(n)
.collect::<Vec<_>>()
.into_iter()
.rev()
.collect(),
None => entries.iter().collect(),
};
output::header(&format!("vaultic log ({} entries)", display.len()));
println!();
for entry in &display {
print_entry(entry);
}
Ok(())
}
fn parse_since(s: &str) -> Result<chrono::DateTime<Utc>> {
NaiveDate::parse_from_str(s, "%Y-%m-%d")
.map_err(|_| VaulticError::InvalidConfig {
detail: format!(
"Invalid date format: '{s}'. Expected ISO 8601 (YYYY-MM-DD), e.g. 2026-01-15"
),
})
.map(|d| Utc.from_utc_datetime(&d.and_hms_opt(0, 0, 0).expect("midnight is always valid")))
}
fn print_entry(entry: &AuditEntry) {
let date = entry.timestamp.format("%Y-%m-%d %H:%M:%S");
let author = truncate_author(&entry.author, 10);
let action = format_action(&entry.action);
let files = if entry.files.is_empty() {
"—".dimmed().to_string()
} else {
entry.files.join(", ")
};
let detail = entry.detail.as_deref().unwrap_or("").dimmed().to_string();
let sep = "│".dimmed();
println!(
" {} {sep} {:<10} {sep} {:<10} {sep} {} {}",
date.to_string().dimmed(),
author,
action,
files,
detail,
);
}
fn truncate_author(author: &str, max_len: usize) -> String {
let char_count = author.chars().count();
if char_count <= max_len {
author.to_string()
} else {
author
.chars()
.take(max_len.saturating_sub(1))
.collect::<String>()
+ "…"
}
}
fn format_action(action: &AuditAction) -> String {
match action {
AuditAction::Init => "init".cyan().to_string(),
AuditAction::Encrypt => "encrypt".green().to_string(),
AuditAction::Decrypt => "decrypt".blue().to_string(),
AuditAction::KeyAdd => "key add".green().to_string(),
AuditAction::KeyRemove => "key rm".red().to_string(),
AuditAction::Check => "check".yellow().to_string(),
AuditAction::Diff => "diff".yellow().to_string(),
AuditAction::Resolve => "resolve".blue().to_string(),
AuditAction::HookInstall => "hook +".green().to_string(),
AuditAction::HookUninstall => "hook -".red().to_string(),
}
}