1use anyhow::{Context, Result};
4use colored::Colorize;
5use std::collections::HashMap;
6use std::fs;
7use std::path::PathBuf;
8
9use crate::formats::{parse_scg, serialize_scg, Format};
10use crate::models::Phase;
11use crate::storage::Storage;
12
13pub fn run(
14 project_root: Option<PathBuf>,
15 from_format: &str,
16 to_format: &str,
17 backup: bool,
18) -> Result<()> {
19 let from = Format::from_extension(from_format)
20 .ok_or_else(|| anyhow::anyhow!("Unknown format: {}", from_format))?;
21 let to = Format::from_extension(to_format)
22 .ok_or_else(|| anyhow::anyhow!("Unknown format: {}", to_format))?;
23
24 if from == to {
25 println!("{}", "Source and target formats are the same".yellow());
26 return Ok(());
27 }
28
29 if from == Format::Scg && to == Format::Json {
32 anyhow::bail!(
33 "SCG to JSON conversion is not supported.\n\
34 The SCUD CLI requires tasks.scg for storage.\n\
35 Use 'scud show' or 'scud list' to view tasks."
36 );
37 }
38
39 let storage = Storage::new(project_root);
40 let taskmaster_dir = storage.scud_dir();
41 let tasks_dir = taskmaster_dir.join("tasks");
42
43 let source_file = tasks_dir.join(format!("tasks.{}", from.extension()));
45 let target_file = tasks_dir.join(format!("tasks.{}", to.extension()));
46
47 if !source_file.exists() {
48 anyhow::bail!(
49 "Source file not found: {}\nExpected format: {}",
50 source_file.display(),
51 from_format
52 );
53 }
54
55 println!(
56 "{} {} -> {}",
57 "Converting".blue(),
58 source_file.display(),
59 target_file.display()
60 );
61
62 let content = fs::read_to_string(&source_file)
64 .with_context(|| format!("Failed to read {}", source_file.display()))?;
65
66 let phases: HashMap<String, Phase> = match from {
68 Format::Json => serde_json::from_str(&content).with_context(|| "Failed to parse JSON")?,
69 Format::Scg => {
70 parse_multi_phase_scg(&content)?
72 }
73 };
74
75 println!(" {} phase(s) found", phases.len());
76 for (tag, phase) in &phases {
77 println!(" {} {} tasks", tag.cyan(), phase.tasks.len());
78 }
79
80 let output = match to {
82 Format::Json => {
83 serde_json::to_string_pretty(&phases).with_context(|| "Failed to serialize to JSON")?
84 }
85 Format::Scg => {
86 let mut out = String::new();
87 let mut sorted_tags: Vec<_> = phases.keys().collect();
88 sorted_tags.sort();
89
90 for (i, tag) in sorted_tags.iter().enumerate() {
91 if i > 0 {
92 out.push_str("\n---\n\n");
93 }
94 let phase = phases.get(*tag).unwrap();
95 out.push_str(&serialize_scg(phase));
96 }
97 out
98 }
99 };
100
101 if backup && source_file.exists() {
103 let backup_file = tasks_dir.join(format!("tasks.{}.backup", from.extension()));
104 fs::copy(&source_file, &backup_file)
105 .with_context(|| format!("Failed to create backup at {}", backup_file.display()))?;
106 println!(
107 " {} Backup created: {}",
108 "✓".green(),
109 backup_file.display()
110 );
111 }
112
113 fs::write(&target_file, &output)
115 .with_context(|| format!("Failed to write {}", target_file.display()))?;
116
117 if source_file != target_file {
119 fs::remove_file(&source_file)
120 .with_context(|| format!("Failed to remove old file {}", source_file.display()))?;
121 }
122
123 println!();
124 println!("{}", "Conversion complete!".green().bold());
125 println!();
126 println!("{}", "Verify with:".blue());
127 println!(" scud list");
128 println!(" scud stats");
129
130 Ok(())
131}
132
133fn parse_multi_phase_scg(content: &str) -> Result<HashMap<String, Phase>> {
134 let mut phases = HashMap::new();
135
136 if content.trim().is_empty() {
138 return Ok(phases);
139 }
140
141 let sections: Vec<&str> = content.split("\n---\n").collect();
143
144 for section in sections {
145 let section = section.trim();
146 if section.is_empty() {
147 continue;
148 }
149
150 let phase = parse_scg(section).with_context(|| "Failed to parse SCG section")?;
151
152 phases.insert(phase.name.clone(), phase);
153 }
154
155 Ok(phases)
156}