1use anyhow::{Context, Result};
4use colored::Colorize;
5use std::io::{self, IsTerminal, Write};
6use std::path::{Path, PathBuf};
7use tokio::io::{AsyncBufReadExt, BufReader};
8
9use crate::manifest::{Manifest, find_manifest};
10
11pub trait CommandExecutor: Sized {
13 fn execute(self) -> impl std::future::Future<Output = Result<()>> + Send
15 where
16 Self: Send,
17 {
18 async move {
19 let manifest_path = if let Ok(path) = find_manifest() {
20 path
21 } else {
22 match handle_legacy_ccpm_migration().await {
24 Ok(Some(path)) => path,
25 Ok(None) => {
26 return Err(anyhow::anyhow!(
27 "No agpm.toml found in current directory or any parent directory. \
28 Run 'agpm init' to create a new project."
29 ));
30 }
31 Err(e) => return Err(e),
32 }
33 };
34 self.execute_from_path(manifest_path).await
35 }
36 }
37
38 fn execute_from_path(
40 self,
41 manifest_path: PathBuf,
42 ) -> impl std::future::Future<Output = Result<()>> + Send;
43}
44
45#[derive(Debug)]
47pub struct CommandContext {
48 pub manifest: Manifest,
50 pub manifest_path: PathBuf,
52 pub project_dir: PathBuf,
54 pub lockfile_path: PathBuf,
56}
57
58impl CommandContext {
59 pub fn from_manifest_path(manifest_path: impl AsRef<Path>) -> Result<Self> {
61 let manifest_path = manifest_path.as_ref();
62
63 if !manifest_path.exists() {
64 return Err(anyhow::anyhow!("Manifest file {} not found", manifest_path.display()));
65 }
66
67 let project_dir = manifest_path
68 .parent()
69 .ok_or_else(|| anyhow::anyhow!("Invalid manifest path"))?
70 .to_path_buf();
71
72 let manifest = Manifest::load(manifest_path).with_context(|| {
73 format!("Failed to parse manifest file: {}", manifest_path.display())
74 })?;
75
76 let lockfile_path = project_dir.join("agpm.lock");
77
78 Ok(Self {
79 manifest,
80 manifest_path: manifest_path.to_path_buf(),
81 project_dir,
82 lockfile_path,
83 })
84 }
85
86 pub fn load_lockfile(&self) -> Result<Option<crate::lockfile::LockFile>> {
88 if self.lockfile_path.exists() {
89 let lockfile =
90 crate::lockfile::LockFile::load(&self.lockfile_path).with_context(|| {
91 format!("Failed to load lockfile: {}", self.lockfile_path.display())
92 })?;
93 Ok(Some(lockfile))
94 } else {
95 Ok(None)
96 }
97 }
98
99 pub fn save_lockfile(&self, lockfile: &crate::lockfile::LockFile) -> Result<()> {
101 lockfile
102 .save(&self.lockfile_path)
103 .with_context(|| format!("Failed to save lockfile: {}", self.lockfile_path.display()))
104 }
105}
106
107pub async fn handle_legacy_ccpm_migration() -> Result<Option<PathBuf>> {
140 let current_dir = std::env::current_dir()?;
141 let legacy_dir = find_legacy_ccpm_directory(¤t_dir);
142
143 let Some(dir) = legacy_dir else {
144 return Ok(None);
145 };
146
147 if !std::io::stdin().is_terminal() {
149 eprintln!("{}", "Legacy CCPM files detected (non-interactive mode).".yellow());
151 eprintln!(
152 "Run {} to migrate manually.",
153 format!("agpm migrate --path {}", dir.display()).cyan()
154 );
155 return Ok(None);
156 }
157
158 let ccpm_toml = dir.join("ccpm.toml");
160 let ccpm_lock = dir.join("ccpm.lock");
161
162 let mut files = Vec::new();
163 if ccpm_toml.exists() {
164 files.push("ccpm.toml");
165 }
166 if ccpm_lock.exists() {
167 files.push("ccpm.lock");
168 }
169
170 let files_str = files.join(" and ");
171
172 println!("{}", "Legacy CCPM files detected!".yellow().bold());
173 println!("{} {} found in {}", "→".cyan(), files_str, dir.display());
174 println!();
175
176 print!("{} ", "Would you like to migrate to AGPM now? [Y/n]:".green());
178 io::stdout().flush()?;
179
180 let mut reader = BufReader::new(tokio::io::stdin());
182 let mut response = String::new();
183 reader.read_line(&mut response).await?;
184 let response = response.trim().to_lowercase();
185
186 if response.is_empty() || response == "y" || response == "yes" {
187 println!();
188 println!("{}", "🚀 Starting migration...".cyan());
189
190 let migrate_cmd = super::migrate::MigrateCommand::new(Some(dir.clone()), false, false);
192
193 migrate_cmd.execute().await?;
194
195 Ok(Some(dir.join("agpm.toml")))
197 } else {
198 println!();
199 println!("{}", "Migration cancelled.".yellow());
200 println!(
201 "Run {} to migrate manually.",
202 format!("agpm migrate --path {}", dir.display()).cyan()
203 );
204 Ok(None)
205 }
206}
207
208pub fn check_for_legacy_ccpm_files() -> Option<String> {
220 check_for_legacy_ccpm_files_from(std::env::current_dir().ok()?)
221}
222
223fn find_legacy_ccpm_directory(start_dir: &Path) -> Option<PathBuf> {
233 let mut dir = start_dir;
234
235 loop {
236 let ccpm_toml = dir.join("ccpm.toml");
237 let ccpm_lock = dir.join("ccpm.lock");
238
239 if ccpm_toml.exists() || ccpm_lock.exists() {
240 return Some(dir.to_path_buf());
241 }
242
243 dir = dir.parent()?;
244 }
245}
246
247fn check_for_legacy_ccpm_files_from(start_dir: PathBuf) -> Option<String> {
252 let current = start_dir;
253 let mut dir = current.as_path();
254
255 loop {
256 let ccpm_toml = dir.join("ccpm.toml");
257 let ccpm_lock = dir.join("ccpm.lock");
258
259 if ccpm_toml.exists() || ccpm_lock.exists() {
260 let mut files = Vec::new();
261 if ccpm_toml.exists() {
262 files.push("ccpm.toml");
263 }
264 if ccpm_lock.exists() {
265 files.push("ccpm.lock");
266 }
267
268 let files_str = files.join(" and ");
269 let location = if dir == current {
270 "current directory".to_string()
271 } else {
272 format!("parent directory: {}", dir.display())
273 };
274
275 return Some(format!(
276 "{}\n\n{} {} found in {}.\n{}\n {}\n\n{}",
277 "Legacy CCPM files detected!".yellow().bold(),
278 "→".cyan(),
279 files_str,
280 location,
281 "Run the migration command to upgrade:".yellow(),
282 format!("agpm migrate --path {}", dir.display()).cyan().bold(),
283 "Or run 'agpm init' to create a new AGPM project.".dimmed()
284 ));
285 }
286
287 dir = dir.parent()?;
288 }
289}
290
291#[cfg(test)]
292mod tests {
293 use super::*;
294 use tempfile::TempDir;
295
296 #[test]
297 fn test_command_context_from_manifest_path() {
298 let temp_dir = TempDir::new().unwrap();
299 let manifest_path = temp_dir.path().join("agpm.toml");
300
301 std::fs::write(
303 &manifest_path,
304 r#"
305[sources]
306test = "https://github.com/test/repo.git"
307
308[agents]
309"#,
310 )
311 .unwrap();
312
313 let context = CommandContext::from_manifest_path(&manifest_path).unwrap();
314
315 assert_eq!(context.manifest_path, manifest_path);
316 assert_eq!(context.project_dir, temp_dir.path());
317 assert_eq!(context.lockfile_path, temp_dir.path().join("agpm.lock"));
318 assert!(context.manifest.sources.contains_key("test"));
319 }
320
321 #[test]
322 fn test_command_context_missing_manifest() {
323 let result = CommandContext::from_manifest_path("/nonexistent/agpm.toml");
324 assert!(result.is_err());
325 assert!(result.unwrap_err().to_string().contains("not found"));
326 }
327
328 #[test]
329 fn test_command_context_invalid_manifest() {
330 let temp_dir = TempDir::new().unwrap();
331 let manifest_path = temp_dir.path().join("agpm.toml");
332
333 std::fs::write(&manifest_path, "invalid toml {{").unwrap();
335
336 let result = CommandContext::from_manifest_path(&manifest_path);
337 assert!(result.is_err());
338 assert!(result.unwrap_err().to_string().contains("Failed to parse manifest"));
339 }
340
341 #[test]
342 fn test_load_lockfile_exists() {
343 let temp_dir = TempDir::new().unwrap();
344 let manifest_path = temp_dir.path().join("agpm.toml");
345 let lockfile_path = temp_dir.path().join("agpm.lock");
346
347 std::fs::write(&manifest_path, "[sources]\n").unwrap();
349 std::fs::write(
350 &lockfile_path,
351 r#"
352version = 1
353
354[[sources]]
355name = "test"
356url = "https://github.com/test/repo.git"
357commit = "abc123"
358fetched_at = "2024-01-01T00:00:00Z"
359"#,
360 )
361 .unwrap();
362
363 let context = CommandContext::from_manifest_path(&manifest_path).unwrap();
364 let lockfile = context.load_lockfile().unwrap();
365
366 assert!(lockfile.is_some());
367 let lockfile = lockfile.unwrap();
368 assert_eq!(lockfile.sources.len(), 1);
369 assert_eq!(lockfile.sources[0].name, "test");
370 }
371
372 #[test]
373 fn test_load_lockfile_not_exists() {
374 let temp_dir = TempDir::new().unwrap();
375 let manifest_path = temp_dir.path().join("agpm.toml");
376
377 std::fs::write(&manifest_path, "[sources]\n").unwrap();
378
379 let context = CommandContext::from_manifest_path(&manifest_path).unwrap();
380 let lockfile = context.load_lockfile().unwrap();
381
382 assert!(lockfile.is_none());
383 }
384
385 #[test]
386 fn test_save_lockfile() {
387 let temp_dir = TempDir::new().unwrap();
388 let manifest_path = temp_dir.path().join("agpm.toml");
389
390 std::fs::write(&manifest_path, "[sources]\n").unwrap();
391
392 let context = CommandContext::from_manifest_path(&manifest_path).unwrap();
393
394 let lockfile = crate::lockfile::LockFile {
395 version: 1,
396 sources: vec![],
397 agents: vec![],
398 snippets: vec![],
399 commands: vec![],
400 scripts: vec![],
401 hooks: vec![],
402 mcp_servers: vec![],
403 };
404
405 context.save_lockfile(&lockfile).unwrap();
406
407 assert!(context.lockfile_path.exists());
408 let saved_content = std::fs::read_to_string(&context.lockfile_path).unwrap();
409 assert!(saved_content.contains("version = 1"));
410 }
411
412 #[test]
413 fn test_check_for_legacy_ccpm_no_files() {
414 let temp_dir = TempDir::new().unwrap();
415 let result = check_for_legacy_ccpm_files_from(temp_dir.path().to_path_buf());
416 assert!(result.is_none());
417 }
418
419 #[test]
420 fn test_check_for_legacy_ccpm_toml_only() {
421 let temp_dir = TempDir::new().unwrap();
422 std::fs::write(temp_dir.path().join("ccpm.toml"), "[sources]\n").unwrap();
423
424 let result = check_for_legacy_ccpm_files_from(temp_dir.path().to_path_buf());
425 assert!(result.is_some());
426 let msg = result.unwrap();
427 assert!(msg.contains("Legacy CCPM files detected"));
428 assert!(msg.contains("ccpm.toml"));
429 assert!(msg.contains("agpm migrate"));
430 }
431
432 #[test]
433 fn test_check_for_legacy_ccpm_lock_only() {
434 let temp_dir = TempDir::new().unwrap();
435 std::fs::write(temp_dir.path().join("ccpm.lock"), "# lock\n").unwrap();
436
437 let result = check_for_legacy_ccpm_files_from(temp_dir.path().to_path_buf());
438 assert!(result.is_some());
439 let msg = result.unwrap();
440 assert!(msg.contains("ccpm.lock"));
441 }
442
443 #[test]
444 fn test_check_for_legacy_ccpm_both_files() {
445 let temp_dir = TempDir::new().unwrap();
446 std::fs::write(temp_dir.path().join("ccpm.toml"), "[sources]\n").unwrap();
447 std::fs::write(temp_dir.path().join("ccpm.lock"), "# lock\n").unwrap();
448
449 let result = check_for_legacy_ccpm_files_from(temp_dir.path().to_path_buf());
450 assert!(result.is_some());
451 let msg = result.unwrap();
452 assert!(msg.contains("ccpm.toml and ccpm.lock"));
453 }
454
455 #[test]
456 fn test_find_legacy_ccpm_directory_no_files() {
457 let temp_dir = TempDir::new().unwrap();
458 let result = find_legacy_ccpm_directory(temp_dir.path());
459 assert!(result.is_none());
460 }
461
462 #[test]
463 fn test_find_legacy_ccpm_directory_in_current_dir() {
464 let temp_dir = TempDir::new().unwrap();
465 std::fs::write(temp_dir.path().join("ccpm.toml"), "[sources]\n").unwrap();
466
467 let result = find_legacy_ccpm_directory(temp_dir.path());
468 assert!(result.is_some());
469 assert_eq!(result.unwrap(), temp_dir.path());
470 }
471
472 #[test]
473 fn test_find_legacy_ccpm_directory_in_parent() {
474 let temp_dir = TempDir::new().unwrap();
475 let parent = temp_dir.path();
476 let child = parent.join("subdir");
477 std::fs::create_dir(&child).unwrap();
478
479 std::fs::write(parent.join("ccpm.toml"), "[sources]\n").unwrap();
481
482 let result = find_legacy_ccpm_directory(&child);
484 assert!(result.is_some());
485 assert_eq!(result.unwrap(), parent);
486 }
487
488 #[test]
489 fn test_find_legacy_ccpm_directory_finds_lock_file() {
490 let temp_dir = TempDir::new().unwrap();
491 std::fs::write(temp_dir.path().join("ccpm.lock"), "# lock\n").unwrap();
492
493 let result = find_legacy_ccpm_directory(temp_dir.path());
494 assert!(result.is_some());
495 assert_eq!(result.unwrap(), temp_dir.path());
496 }
497
498 #[tokio::test]
499 async fn test_handle_legacy_ccpm_migration_no_files() {
500 let temp_dir = TempDir::new().unwrap();
501 let original_dir = std::env::current_dir().unwrap();
502
503 std::env::set_current_dir(temp_dir.path()).unwrap();
505
506 let result = handle_legacy_ccpm_migration().await;
507
508 std::env::set_current_dir(original_dir).unwrap();
510
511 assert!(result.is_ok());
512 assert!(result.unwrap().is_none());
513 }
514
515 }