use anyhow::Result;
use std::path::PathBuf;
use crate::models::task::TaskStatus;
use crate::storage::Storage;
pub fn run(project_root: Option<PathBuf>, tag: Option<&str>, all_tags: bool) -> Result<()> {
let storage = Storage::new(project_root);
let all_tasks = storage.load_tasks()?;
let phase_tags: Vec<String> = if all_tags {
all_tasks.keys().cloned().collect()
} else if let Some(t) = tag {
if !all_tasks.contains_key(t) {
anyhow::bail!("Phase '{}' not found. Run: scud tags", t);
}
vec![t.to_string()]
} else {
let active = storage.get_active_group()?;
match active {
Some(t) => vec![t],
None => anyhow::bail!("No active task group. Use --tag <phase-tag> or run: scud tags"),
}
};
println!("```mermaid");
println!("flowchart TD");
for tag in &phase_tags {
if let Some(phase) = all_tasks.get(tag) {
if phase_tags.len() > 1 {
println!(" subgraph {}[\"Phase: {}\"]", sanitize_id(tag), tag);
}
for task in &phase.tasks {
let node_id = sanitize_id(&task.id);
let label = escape_label(&task.title);
let style_class = status_to_class(&task.status);
let shape = match task.status {
TaskStatus::Expanded => format!("{}[[\"{}\"]]", node_id, label),
TaskStatus::Done => format!("{}([\"{}\"])", node_id, label),
TaskStatus::Blocked => format!("{}{{\"{}\"}}", node_id, label),
_ => format!("{}[\"{}\"]", node_id, label),
};
println!(" {}", shape);
if !style_class.is_empty() {
println!(" class {} {}", node_id, style_class);
}
}
for task in &phase.tasks {
let task_node = sanitize_id(&task.id);
for dep in &task.dependencies {
let dep_node = sanitize_id(dep);
println!(" {} --> {}", dep_node, task_node);
}
if let Some(ref parent_id) = task.parent_id {
let parent_node = sanitize_id(parent_id);
println!(" {} -.-> {}", parent_node, task_node);
}
}
if phase_tags.len() > 1 {
println!(" end");
}
}
}
println!();
println!(" %% Status styles");
println!(" classDef pending fill:#f9f9f9,stroke:#999,color:#333");
println!(" classDef inprogress fill:#e3f2fd,stroke:#1976d2,color:#1976d2,stroke-width:2px");
println!(" classDef done fill:#e8f5e9,stroke:#4caf50,color:#2e7d32");
println!(" classDef blocked fill:#ffebee,stroke:#f44336,color:#c62828");
println!(" classDef review fill:#fff3e0,stroke:#ff9800,color:#e65100");
println!(" classDef expanded fill:#f3e5f5,stroke:#9c27b0,color:#6a1b9a");
println!(" classDef deferred fill:#eceff1,stroke:#607d8b,color:#455a64");
println!(
" classDef cancelled fill:#fafafa,stroke:#bdbdbd,color:#9e9e9e,stroke-dasharray: 5 5"
);
println!(" classDef failed fill:#ffcdd2,stroke:#b71c1c,color:#b71c1c,stroke-width:3px");
println!("```");
Ok(())
}
fn sanitize_id(id: &str) -> String {
id.replace([':', '.', '-', ' '], "_")
}
fn escape_label(text: &str) -> String {
text.replace('"', "'").replace('\n', " ")
}
fn status_to_class(status: &TaskStatus) -> &'static str {
match status {
TaskStatus::Pending => "pending",
TaskStatus::InProgress => "inprogress",
TaskStatus::Done => "done",
TaskStatus::Blocked => "blocked",
TaskStatus::Review => "review",
TaskStatus::Expanded => "expanded",
TaskStatus::Deferred => "deferred",
TaskStatus::Cancelled => "cancelled",
TaskStatus::Failed => "failed",
}
}