task_graph_mcp/cli/
migrate.rs1use anyhow::{Context, Result};
4use clap::Args;
5use std::fs;
6use std::path::Path;
7
8#[derive(Args, Debug)]
10pub struct MigrateArgs {
11 #[arg(short = 'y', long)]
13 pub yes: bool,
14
15 #[arg(long)]
17 pub dry_run: bool,
18
19 #[arg(long, default_value = ".task-graph")]
21 pub from: String,
22
23 #[arg(long, default_value = "task-graph")]
25 pub to: String,
26}
27
28pub fn run_migrate(args: &MigrateArgs) -> Result<()> {
30 let from = Path::new(&args.from);
31 let to = Path::new(&args.to);
32
33 if !from.exists() {
35 println!("No migration needed: '{}' does not exist.", args.from);
36 return Ok(());
37 }
38
39 if to.exists() {
41 println!(
42 "Target directory '{}' already exists. Cannot migrate.",
43 args.to
44 );
45 println!("Options:");
46 println!(" 1. Remove '{}' and run migrate again", args.to);
47 println!(" 2. Manually merge the directories");
48 println!(" 3. Use --to to specify a different target");
49 return Ok(());
50 }
51
52 println!("Migration plan:");
54 println!(" From: {}", from.display());
55 println!(" To: {}", to.display());
56 println!();
57
58 let entries = list_directory_contents(from)?;
60 if entries.is_empty() {
61 println!(" (empty directory)");
62 } else {
63 for entry in &entries {
64 println!(" {}", entry);
65 }
66 }
67 println!();
68
69 if args.dry_run {
70 println!("Dry run: No changes made.");
71 return Ok(());
72 }
73
74 if !args.yes {
76 println!("This will move '{}' to '{}'.", args.from, args.to);
77 print!("Continue? [y/N] ");
78 use std::io::Write;
79 std::io::stdout().flush()?;
80
81 let mut input = String::new();
82 std::io::stdin().read_line(&mut input)?;
83
84 if !input.trim().eq_ignore_ascii_case("y") {
85 println!("Migration cancelled.");
86 return Ok(());
87 }
88 }
89
90 println!("Migrating...");
92 fs::rename(from, to).context("Failed to rename directory")?;
93
94 println!("Migration complete!");
95 println!();
96 println!("Your configuration has been moved to '{}'.", args.to);
97 println!();
98 println!("If you have any scripts or configurations that reference '.task-graph/',");
99 println!("please update them to use 'task-graph/' instead.");
100
101 Ok(())
102}
103
104fn list_directory_contents(dir: &Path) -> Result<Vec<String>> {
106 let mut entries = Vec::new();
107 list_directory_recursive(dir, dir, &mut entries)?;
108 entries.sort();
109 Ok(entries)
110}
111
112fn list_directory_recursive(base: &Path, dir: &Path, entries: &mut Vec<String>) -> Result<()> {
113 if !dir.is_dir() {
114 return Ok(());
115 }
116
117 for entry in fs::read_dir(dir)? {
118 let entry = entry?;
119 let path = entry.path();
120 let relative = path.strip_prefix(base).unwrap_or(&path);
121
122 if path.is_dir() {
123 entries.push(format!("{}/", relative.display()));
124 list_directory_recursive(base, &path, entries)?;
125 } else {
126 entries.push(relative.display().to_string());
127 }
128 }
129
130 Ok(())
131}
132
133pub fn check_and_warn_deprecated() {
135 let deprecated = Path::new(".task-graph");
136 let new_location = Path::new("task-graph");
137
138 if deprecated.exists() && !new_location.exists() {
139 eprintln!();
140 eprintln!("Warning: Using deprecated directory '.task-graph/'.");
141 eprintln!("Run 'task-graph migrate' to move to 'task-graph/'.");
142 eprintln!();
143 }
144}
145
146#[cfg(test)]
147mod tests {
148 use super::*;
149 use std::path::PathBuf;
150 use tempfile::TempDir;
151
152 fn create_test_structure(temp: &TempDir) -> PathBuf {
153 let base = temp.path().to_path_buf();
154 let deprecated = base.join(".task-graph");
155
156 fs::create_dir_all(deprecated.join("skills")).unwrap();
158 fs::create_dir_all(deprecated.join("media")).unwrap();
159 fs::write(
160 deprecated.join("config.yaml"),
161 "server:\n claim_limit: 10\n",
162 )
163 .unwrap();
164 fs::write(deprecated.join("tasks.db"), "fake-db-content").unwrap();
165 fs::write(deprecated.join("skills/custom.md"), "# Custom Skill").unwrap();
166
167 base
168 }
169
170 #[test]
171 fn test_migrate_dry_run_no_changes() {
172 let temp = TempDir::new().unwrap();
173 let base = create_test_structure(&temp);
174
175 let args = MigrateArgs {
176 yes: false,
177 dry_run: true,
178 from: base.join(".task-graph").to_string_lossy().to_string(),
179 to: base.join("task-graph").to_string_lossy().to_string(),
180 };
181
182 run_migrate(&args).unwrap();
183
184 assert!(base.join(".task-graph").exists());
186 assert!(!base.join("task-graph").exists());
188 }
189
190 #[test]
191 fn test_migrate_moves_directory() {
192 let temp = TempDir::new().unwrap();
193 let base = create_test_structure(&temp);
194
195 let args = MigrateArgs {
196 yes: true, dry_run: false,
198 from: base.join(".task-graph").to_string_lossy().to_string(),
199 to: base.join("task-graph").to_string_lossy().to_string(),
200 };
201
202 run_migrate(&args).unwrap();
203
204 assert!(!base.join(".task-graph").exists());
206 assert!(base.join("task-graph").exists());
208 assert!(base.join("task-graph/config.yaml").exists());
209 assert!(base.join("task-graph/tasks.db").exists());
210 assert!(base.join("task-graph/skills/custom.md").exists());
211
212 let config = fs::read_to_string(base.join("task-graph/config.yaml")).unwrap();
214 assert!(config.contains("claim_limit: 10"));
215 }
216
217 #[test]
218 fn test_migrate_no_source() {
219 let temp = TempDir::new().unwrap();
220 let base = temp.path();
221
222 let args = MigrateArgs {
223 yes: true,
224 dry_run: false,
225 from: base.join(".task-graph").to_string_lossy().to_string(),
226 to: base.join("task-graph").to_string_lossy().to_string(),
227 };
228
229 run_migrate(&args).unwrap();
231 }
232
233 #[test]
234 fn test_migrate_target_exists() {
235 let temp = TempDir::new().unwrap();
236 let base = create_test_structure(&temp);
237
238 fs::create_dir_all(base.join("task-graph")).unwrap();
240
241 let args = MigrateArgs {
242 yes: true,
243 dry_run: false,
244 from: base.join(".task-graph").to_string_lossy().to_string(),
245 to: base.join("task-graph").to_string_lossy().to_string(),
246 };
247
248 run_migrate(&args).unwrap();
250
251 assert!(base.join(".task-graph").exists());
253 assert!(base.join("task-graph").exists());
254 }
255}