use super::super::*;
impl CliRunner {
pub(crate) async fn handle_run(&self, action: &RunAction) -> Result<()> {
match action {
RunAction::List => self.run_list().await,
RunAction::View { id } => self.run_view(id).await,
RunAction::Diff { a, b } => self.run_diff(a, b).await,
}
}
async fn run_diff(&self, a: &str, b: &str) -> Result<()> {
let runs_dir = self.repo_path.join(".ccswarm/runs");
let read_timeline = |id: &str| -> Result<Vec<(String, String, Option<u64>)>> {
let p = runs_dir.join(id).join("events.ndjson");
if !p.exists() {
anyhow::bail!("events.ndjson not found for run '{}'", id);
}
let content = std::fs::read_to_string(&p)?;
let items: Vec<(String, String, Option<u64>)> = content
.lines()
.filter(|l| !l.trim().is_empty())
.filter_map(|l| serde_json::from_str::<serde_json::Value>(l).ok())
.map(|v| {
let et = v
.get("event_type")
.and_then(|s| s.as_str())
.unwrap_or("")
.to_string();
let mv = v
.get("stage")
.and_then(|s| s.as_str())
.unwrap_or("")
.to_string();
let ms = v
.get("metadata")
.and_then(|m| m.get("duration_ms"))
.and_then(|d| d.as_u64());
(et, mv, ms)
})
.collect();
Ok(items)
};
let ta = read_timeline(a)?;
let tb = read_timeline(b)?;
println!(
"{} {} vs {}",
"Run diff".bright_cyan().bold(),
a.bright_yellow(),
b.bright_yellow()
);
println!("{}", "=".repeat(70).bright_black());
println!(
" {:<30} {:<12} {:<12} {:<12}",
"event (stage)", "a", "b", "Δ ms"
);
println!("{}", "-".repeat(70).bright_black());
let max_len = ta.len().max(tb.len());
let mut a_total: u64 = 0;
let mut b_total: u64 = 0;
for i in 0..max_len {
let ae = ta.get(i);
let be = tb.get(i);
let (label_a, ms_a) = ae
.map(|(e, m, ms)| (format!("{} ({})", e, m), *ms))
.unwrap_or_else(|| ("—".to_string(), None));
let (label_b, ms_b) = be
.map(|(e, m, ms)| (format!("{} ({})", e, m), *ms))
.unwrap_or_else(|| ("—".to_string(), None));
a_total += ms_a.unwrap_or(0);
b_total += ms_b.unwrap_or(0);
let diff: i64 = ms_b.unwrap_or(0) as i64 - ms_a.unwrap_or(0) as i64;
let same = label_a == label_b;
let shown_label = if same {
label_a.bright_white()
} else {
format!("{} / {}", label_a, label_b).bright_red()
};
println!(
" {:<30} {:<12} {:<12} {:<12}",
shown_label,
ms_a.map(|v| v.to_string()).unwrap_or_else(|| "-".into()),
ms_b.map(|v| v.to_string()).unwrap_or_else(|| "-".into()),
if diff == 0 {
"0".bright_black()
} else if diff > 0 {
format!("+{}", diff).bright_yellow()
} else {
diff.to_string().bright_green()
},
);
}
println!("{}", "-".repeat(70).bright_black());
println!(
" total ms a={} b={} Δ={}",
a_total,
b_total,
(b_total as i64 - a_total as i64)
);
Ok(())
}
async fn run_list(&self) -> Result<()> {
let runs_dir = self.repo_path.join(".ccswarm/runs");
if !runs_dir.exists() {
if self.json_output {
println!(
"{}",
serde_json::to_string_pretty(&serde_json::json!({
"status": "success",
"message": "No pipeline runs found.",
"data": []
}))?
);
} else {
println!("No pipeline runs found.");
}
return Ok(());
}
let mut summaries: Vec<serde_json::Value> = Vec::new();
let mut dir = tokio::fs::read_dir(&runs_dir).await?;
while let Some(entry) = dir.next_entry().await? {
let path = entry.path();
if !path.is_dir() {
continue;
}
let summary_path = path.join("summary.json");
if !summary_path.exists() {
continue;
}
let content = tokio::fs::read_to_string(&summary_path).await?;
if let Ok(v) = serde_json::from_str::<serde_json::Value>(&content) {
summaries.push(v);
}
}
summaries.sort_by(|a, b| {
let ts_a = a.get("started_at").and_then(|v| v.as_str()).unwrap_or("");
let ts_b = b.get("started_at").and_then(|v| v.as_str()).unwrap_or("");
ts_b.cmp(ts_a)
});
if self.json_output {
println!(
"{}",
serde_json::to_string_pretty(&serde_json::json!({
"status": "success",
"data": summaries,
}))?
);
return Ok(());
}
if summaries.is_empty() {
println!("No pipeline runs found.");
return Ok(());
}
println!(
"{:<36} {:<25} {:<10} {:>10} {:>6} {}",
"Run ID".bright_cyan().bold(),
"Date".bright_cyan().bold(),
"Status".bright_cyan().bold(),
"Events".bright_cyan().bold(),
"Tasks".bright_cyan().bold(),
"Agents".bright_cyan().bold(),
);
println!("{}", "-".repeat(110).bright_black());
for summary in &summaries {
let run_id = summary
.get("run_id")
.and_then(|v| v.as_str())
.unwrap_or("?");
let started_at = summary
.get("started_at")
.and_then(|v| v.as_str())
.unwrap_or("?");
let ended_at = summary.get("ended_at").and_then(|v| v.as_str());
let status = if ended_at.is_some() {
"completed"
} else {
"running"
};
let total_events = summary
.get("total_events")
.and_then(|v| v.as_u64())
.unwrap_or(0);
let tasks_completed = summary
.get("tasks_completed")
.and_then(|v| v.as_u64())
.unwrap_or(0);
let agents_used: Vec<&str> = summary
.get("agents_used")
.and_then(|v| v.as_array())
.map(|arr| arr.iter().filter_map(|a| a.as_str()).collect())
.unwrap_or_default();
let status_colored = match status {
"completed" => status.bright_green(),
_ => status.bright_yellow(),
};
println!(
"{:<36} {:<25} {:<10} {:>10} {:>6} {}",
run_id.bright_yellow(),
started_at,
status_colored,
total_events,
tasks_completed,
agents_used.join(", "),
);
}
println!();
println!("Total: {} run(s)", summaries.len());
Ok(())
}
async fn run_view(&self, id: &str) -> Result<()> {
let run_dir = self.repo_path.join(".ccswarm/runs").join(id);
if !run_dir.exists() {
anyhow::bail!("Run '{}' not found in .ccswarm/runs/", id);
}
let summary_path = run_dir.join("summary.json");
let summary: Option<serde_json::Value> = if summary_path.exists() {
let content = tokio::fs::read_to_string(&summary_path).await?;
serde_json::from_str(&content).ok()
} else {
None
};
let events_path = run_dir.join("events.ndjson");
let events: Vec<serde_json::Value> = if events_path.exists() {
let content = tokio::fs::read_to_string(&events_path).await?;
content
.lines()
.filter(|line| !line.trim().is_empty())
.filter_map(|line| serde_json::from_str::<serde_json::Value>(line).ok())
.collect()
} else {
Vec::new()
};
if self.json_output {
println!(
"{}",
serde_json::to_string_pretty(&serde_json::json!({
"status": "success",
"data": {
"summary": summary,
"events": events,
}
}))?
);
return Ok(());
}
println!(
"{} {}",
"Run".bright_cyan().bold(),
id.bright_yellow().bold()
);
println!("{}", "=".repeat(70).bright_black());
if let Some(ref s) = summary {
let started_at = s.get("started_at").and_then(|v| v.as_str()).unwrap_or("?");
let ended_at = s
.get("ended_at")
.and_then(|v| v.as_str())
.unwrap_or("in progress");
let total_events = s.get("total_events").and_then(|v| v.as_u64()).unwrap_or(0);
let tasks_completed = s
.get("tasks_completed")
.and_then(|v| v.as_u64())
.unwrap_or(0);
let tasks_failed = s.get("tasks_failed").and_then(|v| v.as_u64()).unwrap_or(0);
let agents_used: Vec<&str> = s
.get("agents_used")
.and_then(|v| v.as_array())
.map(|arr| arr.iter().filter_map(|a| a.as_str()).collect())
.unwrap_or_default();
println!(" Started: {}", started_at);
println!(" Ended: {}", ended_at);
println!(" Total events: {}", total_events);
println!(
" Tasks completed: {}",
tasks_completed.to_string().bright_green()
);
if tasks_failed > 0 {
println!(
" Tasks failed: {}",
tasks_failed.to_string().bright_red()
);
}
println!(" Agents: {}", agents_used.join(", "));
} else {
println!(" (no summary.json found)");
}
println!();
println!("{}", "Events".bright_cyan().bold());
println!("{}", "-".repeat(70).bright_black());
if events.is_empty() {
println!(" (no events.ndjson found)");
} else {
for event in &events {
let ts = event.get("ts").and_then(|v| v.as_str()).unwrap_or("?");
let level = event
.get("level")
.and_then(|v| v.as_str())
.unwrap_or("info");
let event_type = event
.get("event_type")
.and_then(|v| v.as_str())
.unwrap_or("?");
let message = event.get("message").and_then(|v| v.as_str()).unwrap_or("");
let level_colored = match level {
"error" => format!("{:<5}", level).bright_red(),
"warn" => format!("{:<5}", level).bright_yellow(),
"debug" => format!("{:<5}", level).bright_black(),
_ => format!("{:<5}", level).bright_white(),
};
let type_colored = match event_type {
t if t.starts_with("stage") => t.bright_magenta(),
t if t.starts_with("task") => t.bright_cyan(),
t if t.starts_with("hitl") => t.bright_yellow(),
t if t.starts_with("provider") => t.bright_blue(),
t => t.bright_white(),
};
let ts_short = if ts.len() >= 19 { &ts[..19] } else { ts };
let duration_str = event
.get("metadata")
.and_then(|m| m.get("duration_ms"))
.and_then(|d| d.as_u64())
.map(|ms| format!(" ({}ms)", ms))
.unwrap_or_default();
println!(
" [{}] [{}] [{:<20}] {}{}",
ts_short.bright_black(),
level_colored,
type_colored,
message,
duration_str.bright_black(),
);
}
}
println!();
Ok(())
}
}