1use clap::Subcommand;
4use std::fs;
5use std::path::PathBuf;
6
7use nika_engine::error::NikaError;
8use nika_engine::Event;
9
10#[derive(Subcommand)]
11pub enum TraceAction {
12 List {
14 #[arg(short, long)]
16 limit: Option<usize>,
17 },
18
19 Show {
21 id: String,
23 },
24
25 Export {
27 id: String,
29 #[arg(short, long, default_value = "json")]
31 format: String,
32 #[arg(short, long)]
34 output: Option<PathBuf>,
35 },
36
37 Clean {
39 #[arg(short, long, default_value = "10")]
41 keep: usize,
42 },
43}
44
45pub fn handle_trace_command(action: TraceAction) -> Result<(), NikaError> {
46 match action {
47 TraceAction::List { limit } => {
48 let traces = nika_engine::list_traces()?;
49 let traces = match limit {
50 Some(n) => traces.into_iter().take(n).collect::<Vec<_>>(),
51 None => traces,
52 };
53
54 println!("Found {} traces:\n", traces.len());
55 println!("{:<30} {:>10} {:>20}", "GENERATION ID", "SIZE", "CREATED");
56 println!("{}", "-".repeat(62));
57
58 for trace in traces {
59 let size = if trace.size_bytes > 1024 * 1024 {
60 format!("{:.1}MB", trace.size_bytes as f64 / 1024.0 / 1024.0)
61 } else if trace.size_bytes > 1024 {
62 format!("{:.1}KB", trace.size_bytes as f64 / 1024.0)
63 } else {
64 format!("{}B", trace.size_bytes)
65 };
66
67 let created = trace
68 .created
69 .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
70 .map(|d| {
71 chrono::DateTime::from_timestamp(d.as_secs() as i64, 0).map_or_else(
72 || "unknown".to_string(),
73 |dt| dt.format("%Y-%m-%d %H:%M").to_string(),
74 )
75 })
76 .unwrap_or_else(|| "unknown".to_string());
77
78 println!("{:<30} {:>10} {:>20}", trace.generation_id, size, created);
79 }
80 Ok(())
81 }
82
83 TraceAction::Show { id } => {
84 let traces = nika_engine::list_traces()?;
85 let trace = traces
86 .iter()
87 .find(|t| t.generation_id.contains(&id))
88 .ok_or_else(|| NikaError::ValidationError {
89 reason: format!("No trace matching '{id}'"),
90 })?;
91
92 let content = fs::read_to_string(&trace.path)?;
93 let events: Vec<Event> = content
94 .lines()
95 .filter_map(|line| serde_json::from_str(line).ok())
96 .collect();
97
98 println!("Trace: {}", trace.generation_id);
99 println!("Events: {}", events.len());
100 println!("Size: {} bytes\n", trace.size_bytes);
101
102 for event in events {
103 println!("[{:>6}ms] {:?}", event.timestamp_ms, event.kind);
104 }
105 Ok(())
106 }
107
108 TraceAction::Export { id, format, output } => {
109 let traces = nika_engine::list_traces()?;
110 let trace = traces
111 .iter()
112 .find(|t| t.generation_id.contains(&id))
113 .ok_or_else(|| NikaError::ValidationError {
114 reason: format!("No trace matching '{id}'"),
115 })?;
116
117 let content = fs::read_to_string(&trace.path)?;
118 let events: Vec<Event> = content
119 .lines()
120 .filter_map(|line| serde_json::from_str(line).ok())
121 .collect();
122
123 let exported = match format.as_str() {
124 "json" => serde_json::to_string_pretty(&events)?,
125 "yaml" => nika_engine::serde_yaml::to_string(&events).map_err(|e| {
126 NikaError::SerializationError {
127 details: e.to_string(),
128 }
129 })?,
130 other => {
131 return Err(NikaError::ValidationError {
132 reason: format!("Unknown format: {other}. Use 'json' or 'yaml'"),
133 })
134 }
135 };
136
137 match output {
138 Some(path) => {
139 fs::write(&path, &exported)?;
140 println!("Exported {} events to {}", events.len(), path.display());
141 }
142 None => println!("{exported}"),
143 }
144 Ok(())
145 }
146
147 TraceAction::Clean { keep } => {
148 let traces = nika_engine::list_traces()?;
149 let to_delete: Vec<_> = traces.into_iter().skip(keep).collect();
150 let count = to_delete.len();
151
152 for trace in to_delete {
153 fs::remove_file(&trace.path)?;
154 }
155
156 println!("Deleted {count} old traces, kept {keep}");
157 Ok(())
158 }
159 }
160}