1use anyhow::{Context, Result};
28use colored::Colorize;
29use std::io::{self, IsTerminal, Write};
30use std::path::{Path, PathBuf};
31use tokio::io::{AsyncBufReadExt, BufReader};
32
33use crate::manifest::{Manifest, find_manifest};
34
35pub trait CommandExecutor: Sized {
37 fn execute(self) -> impl std::future::Future<Output = Result<()>> + Send
39 where
40 Self: Send,
41 {
42 async move {
43 let manifest_path = if let Ok(path) = find_manifest() {
44 path
45 } else {
46 match handle_legacy_ccpm_migration().await {
48 Ok(Some(path)) => path,
49 Ok(None) => {
50 return Err(anyhow::anyhow!(
51 "No agpm.toml found in current directory or any parent directory. \
52 Run 'agpm init' to create a new project."
53 ));
54 }
55 Err(e) => return Err(e),
56 }
57 };
58 self.execute_from_path(manifest_path).await
59 }
60 }
61
62 fn execute_from_path(
64 self,
65 manifest_path: PathBuf,
66 ) -> impl std::future::Future<Output = Result<()>> + Send;
67}
68
69#[derive(Debug)]
71pub struct CommandContext {
72 pub manifest: Manifest,
74 pub manifest_path: PathBuf,
76 pub project_dir: PathBuf,
78 pub lockfile_path: PathBuf,
80}
81
82impl CommandContext {
83 pub fn new(manifest: Manifest, project_dir: PathBuf) -> Result<Self> {
85 let lockfile_path = project_dir.join("agpm.lock");
86 Ok(Self {
87 manifest,
88 manifest_path: project_dir.join("agpm.toml"),
89 project_dir,
90 lockfile_path,
91 })
92 }
93
94 pub fn from_manifest_path(manifest_path: impl AsRef<Path>) -> Result<Self> {
99 let manifest_path = manifest_path.as_ref();
100
101 if !manifest_path.exists() {
102 return Err(anyhow::anyhow!("Manifest file {} not found", manifest_path.display()));
103 }
104
105 let project_dir = manifest_path
106 .parent()
107 .ok_or_else(|| anyhow::anyhow!("Invalid manifest path"))?
108 .to_path_buf();
109
110 let manifest = Manifest::load(manifest_path).with_context(|| {
111 format!("Failed to parse manifest file: {}", manifest_path.display())
112 })?;
113
114 let lockfile_path = project_dir.join("agpm.lock");
115
116 Ok(Self {
117 manifest,
118 manifest_path: manifest_path.to_path_buf(),
119 project_dir,
120 lockfile_path,
121 })
122 }
123
124 pub fn load_lockfile(&self) -> Result<Option<crate::lockfile::LockFile>> {
129 if self.lockfile_path.exists() {
130 let lockfile =
131 crate::lockfile::LockFile::load(&self.lockfile_path).with_context(|| {
132 format!("Failed to load lockfile: {}", self.lockfile_path.display())
133 })?;
134 Ok(Some(lockfile))
135 } else {
136 Ok(None)
137 }
138 }
139
140 pub fn load_lockfile_with_regeneration(
183 &self,
184 can_regenerate: bool,
185 operation_name: &str,
186 ) -> Result<Option<crate::lockfile::LockFile>> {
187 if !self.lockfile_path.exists() {
189 return Ok(None);
190 }
191
192 match crate::lockfile::LockFile::load(&self.lockfile_path) {
194 Ok(lockfile) => Ok(Some(lockfile)),
195 Err(e) => {
196 let error_msg = e.to_string();
198 let can_auto_recover = can_regenerate
199 && (error_msg.contains("Invalid TOML syntax")
200 || error_msg.contains("Lockfile version")
201 || error_msg.contains("missing field")
202 || error_msg.contains("invalid type")
203 || error_msg.contains("expected"));
204
205 if !can_auto_recover {
206 return Err(e);
208 }
209
210 let backup_path = self.lockfile_path.with_extension("lock.invalid");
212
213 let regenerate_message = format!(
215 "The lockfile appears to be invalid or corrupted.\n\n\
216 Error: {}\n\n\
217 Note: The lockfile format is not yet stable as this is beta software.\n\n\
218 The invalid lockfile will be backed up to: {}",
219 error_msg,
220 backup_path.display()
221 );
222
223 if io::stdin().is_terminal() {
225 println!("{}", regenerate_message);
227 print!("Would you like to regenerate the lockfile automatically? [Y/n] ");
228 io::stdout().flush().unwrap();
229
230 let mut input = String::new();
231 match io::stdin().read_line(&mut input) {
232 Ok(_) => {
233 let response = input.trim().to_lowercase();
234 if response.is_empty() || response == "y" || response == "yes" {
235 self.backup_and_regenerate_lockfile(&backup_path, operation_name)?;
237 Ok(None) } else {
239 Err(crate::core::AgpmError::InvalidLockfileError {
241 file: self.lockfile_path.display().to_string(),
242 reason: format!(
243 "{} (User declined automatic regeneration)",
244 error_msg
245 ),
246 can_regenerate: true,
247 }
248 .into())
249 }
250 }
251 Err(_) => {
252 Err(self.create_non_interactive_error(&error_msg, operation_name))
254 }
255 }
256 } else {
257 Err(self.create_non_interactive_error(&error_msg, operation_name))
259 }
260 }
261 }
262 }
263
264 fn backup_and_regenerate_lockfile(
266 &self,
267 backup_path: &Path,
268 operation_name: &str,
269 ) -> Result<()> {
270 if let Err(e) = std::fs::copy(&self.lockfile_path, backup_path) {
272 eprintln!("Warning: Failed to backup invalid lockfile: {}", e);
273 } else {
274 println!("✓ Backed up invalid lockfile to: {}", backup_path.display());
275 }
276
277 if let Err(e) = std::fs::remove_file(&self.lockfile_path) {
279 return Err(anyhow::anyhow!("Failed to remove invalid lockfile: {}", e));
280 }
281
282 println!("✓ Removed invalid lockfile");
283 println!("Note: Run 'agpm install' to regenerate the lockfile");
284
285 if operation_name != "install" {
287 println!("Alternatively, run 'agpm {} --regenerate' if available", operation_name);
288 }
289
290 Ok(())
291 }
292
293 fn create_non_interactive_error(
295 &self,
296 error_msg: &str,
297 _operation_name: &str,
298 ) -> anyhow::Error {
299 let backup_path = self.lockfile_path.with_extension("lock.invalid");
300
301 crate::core::AgpmError::InvalidLockfileError {
302 file: self.lockfile_path.display().to_string(),
303 reason: format!(
304 "{}\n\n\
305 To fix this issue:\n\
306 1. Backup the invalid lockfile: cp agpm.lock {}\n\
307 2. Remove the invalid lockfile: rm agpm.lock\n\
308 3. Regenerate it: agpm install\n\n\
309 Note: The lockfile format is not yet stable as this is beta software.",
310 error_msg,
311 backup_path.display()
312 ),
313 can_regenerate: true,
314 }
315 .into()
316 }
317
318 pub fn save_lockfile(&self, lockfile: &crate::lockfile::LockFile) -> Result<()> {
323 lockfile
324 .save(&self.lockfile_path)
325 .with_context(|| format!("Failed to save lockfile: {}", self.lockfile_path.display()))
326 }
327}
328
329pub async fn handle_legacy_ccpm_migration() -> Result<Option<PathBuf>> {
368 let current_dir = std::env::current_dir()?;
369 let legacy_dir = find_legacy_ccpm_directory(¤t_dir);
370
371 let Some(dir) = legacy_dir else {
372 return Ok(None);
373 };
374
375 if !std::io::stdin().is_terminal() {
377 eprintln!("{}", "Legacy CCPM files detected (non-interactive mode).".yellow());
379 eprintln!(
380 "Run {} to migrate manually.",
381 format!("agpm migrate --path {}", dir.display()).cyan()
382 );
383 return Ok(None);
384 }
385
386 let ccpm_toml = dir.join("ccpm.toml");
388 let ccpm_lock = dir.join("ccpm.lock");
389
390 let mut files = Vec::new();
391 if ccpm_toml.exists() {
392 files.push("ccpm.toml");
393 }
394 if ccpm_lock.exists() {
395 files.push("ccpm.lock");
396 }
397
398 let files_str = files.join(" and ");
399
400 println!("{}", "Legacy CCPM files detected!".yellow().bold());
401 println!("{} {} found in {}", "→".cyan(), files_str, dir.display());
402 println!();
403
404 print!("{} ", "Would you like to migrate to AGPM now? [Y/n]:".green());
406 io::stdout().flush()?;
407
408 let mut reader = BufReader::new(tokio::io::stdin());
410 let mut response = String::new();
411 reader.read_line(&mut response).await?;
412 let response = response.trim().to_lowercase();
413
414 if response.is_empty() || response == "y" || response == "yes" {
415 println!();
416 println!("{}", "🚀 Starting migration...".cyan());
417
418 let migrate_cmd = super::migrate::MigrateCommand::new(Some(dir.clone()), false, false);
420
421 migrate_cmd.execute().await?;
422
423 Ok(Some(dir.join("agpm.toml")))
425 } else {
426 println!();
427 println!("{}", "Migration cancelled.".yellow());
428 println!(
429 "Run {} to migrate manually.",
430 format!("agpm migrate --path {}", dir.display()).cyan()
431 );
432 Ok(None)
433 }
434}
435
436#[must_use]
448pub fn check_for_legacy_ccpm_files() -> Option<String> {
449 check_for_legacy_ccpm_files_from(std::env::current_dir().ok()?)
450}
451
452fn find_legacy_ccpm_directory(start_dir: &Path) -> Option<PathBuf> {
462 let mut dir = start_dir;
463
464 loop {
465 let ccpm_toml = dir.join("ccpm.toml");
466 let ccpm_lock = dir.join("ccpm.lock");
467
468 if ccpm_toml.exists() || ccpm_lock.exists() {
469 return Some(dir.to_path_buf());
470 }
471
472 dir = dir.parent()?;
473 }
474}
475
476fn check_for_legacy_ccpm_files_from(start_dir: PathBuf) -> Option<String> {
481 let current = start_dir;
482 let mut dir = current.as_path();
483
484 loop {
485 let ccpm_toml = dir.join("ccpm.toml");
486 let ccpm_lock = dir.join("ccpm.lock");
487
488 if ccpm_toml.exists() || ccpm_lock.exists() {
489 let mut files = Vec::new();
490 if ccpm_toml.exists() {
491 files.push("ccpm.toml");
492 }
493 if ccpm_lock.exists() {
494 files.push("ccpm.lock");
495 }
496
497 let files_str = files.join(" and ");
498 let location = if dir == current {
499 "current directory".to_string()
500 } else {
501 format!("parent directory: {}", dir.display())
502 };
503
504 return Some(format!(
505 "{}\n\n{} {} found in {}.\n{}\n {}\n\n{}",
506 "Legacy CCPM files detected!".yellow().bold(),
507 "→".cyan(),
508 files_str,
509 location,
510 "Run the migration command to upgrade:".yellow(),
511 format!("agpm migrate --path {}", dir.display()).cyan().bold(),
512 "Or run 'agpm init' to create a new AGPM project.".dimmed()
513 ));
514 }
515
516 dir = dir.parent()?;
517 }
518}
519
520#[derive(Debug, Clone, Copy, PartialEq, Eq)]
538pub enum OperationMode {
539 Install,
541 Update,
543}
544
545pub fn display_dry_run_results(
600 new_lockfile: &crate::lockfile::LockFile,
601 existing_lockfile: Option<&crate::lockfile::LockFile>,
602 quiet: bool,
603) -> Result<()> {
604 let (new_resources, updated_resources, unchanged_count) =
606 categorize_resource_changes(new_lockfile, existing_lockfile);
607
608 let has_changes = !new_resources.is_empty() || !updated_resources.is_empty();
610 display_dry_run_output(&new_resources, &updated_resources, unchanged_count, quiet);
611
612 if has_changes {
614 Err(anyhow::anyhow!("Dry-run detected changes (exit 1)"))
615 } else {
616 Ok(())
617 }
618}
619
620#[derive(Debug, Clone)]
622struct NewResource {
623 resource_type: String,
624 name: String,
625 version: String,
626}
627
628#[derive(Debug, Clone)]
630struct UpdatedResource {
631 resource_type: String,
632 name: String,
633 old_version: String,
634 new_version: String,
635}
636
637fn categorize_resource_changes(
642 new_lockfile: &crate::lockfile::LockFile,
643 existing_lockfile: Option<&crate::lockfile::LockFile>,
644) -> (Vec<NewResource>, Vec<UpdatedResource>, usize) {
645 use crate::core::resource_iterator::ResourceIterator;
646
647 let mut new_resources = Vec::new();
648 let mut updated_resources = Vec::new();
649 let mut unchanged_count = 0;
650
651 if let Some(existing) = existing_lockfile {
653 ResourceIterator::for_each_resource(new_lockfile, |resource_type, new_entry| {
654 if let Some((_, old_entry)) = ResourceIterator::find_resource_by_name_and_source(
656 existing,
657 &new_entry.name,
658 new_entry.source.as_deref(),
659 ) {
660 if old_entry.resolved_commit == new_entry.resolved_commit {
662 unchanged_count += 1;
663 } else {
664 let old_version =
665 old_entry.version.clone().unwrap_or_else(|| "latest".to_string());
666 let new_version =
667 new_entry.version.clone().unwrap_or_else(|| "latest".to_string());
668 updated_resources.push(UpdatedResource {
669 resource_type: resource_type.to_string(),
670 name: new_entry.name.clone(),
671 old_version,
672 new_version,
673 });
674 }
675 } else {
676 new_resources.push(NewResource {
678 resource_type: resource_type.to_string(),
679 name: new_entry.name.clone(),
680 version: new_entry.version.clone().unwrap_or_else(|| "latest".to_string()),
681 });
682 }
683 });
684 } else {
685 ResourceIterator::for_each_resource(new_lockfile, |resource_type, new_entry| {
687 new_resources.push(NewResource {
688 resource_type: resource_type.to_string(),
689 name: new_entry.name.clone(),
690 version: new_entry.version.clone().unwrap_or_else(|| "latest".to_string()),
691 });
692 });
693 }
694
695 (new_resources, updated_resources, unchanged_count)
696}
697
698fn display_dry_run_output(
703 new_resources: &[NewResource],
704 updated_resources: &[UpdatedResource],
705 unchanged_count: usize,
706 quiet: bool,
707) {
708 if quiet {
709 return;
710 }
711
712 let has_changes = !new_resources.is_empty() || !updated_resources.is_empty();
713
714 if has_changes {
715 println!("{}", "Dry run - the following changes would be made:".yellow());
716 println!();
717
718 if !new_resources.is_empty() {
719 println!("{}", "New resources:".green().bold());
720 for resource in new_resources {
721 println!(
722 " {} {} ({})",
723 "+".green(),
724 resource.name.cyan(),
725 format!("{} {}", resource.resource_type, resource.version).dimmed()
726 );
727 }
728 println!();
729 }
730
731 if !updated_resources.is_empty() {
732 println!("{}", "Updated resources:".yellow().bold());
733 for resource in updated_resources {
734 print!(
735 " {} {} {} → ",
736 "~".yellow(),
737 resource.name.cyan(),
738 resource.old_version.yellow()
739 );
740 println!("{} ({})", resource.new_version.green(), resource.resource_type.dimmed());
741 }
742 println!();
743 }
744
745 if unchanged_count > 0 {
746 println!("{}", format!("{unchanged_count} unchanged resources").dimmed());
747 }
748
749 println!();
750 println!(
751 "{}",
752 format!(
753 "Total: {} new, {} updated, {} unchanged",
754 new_resources.len(),
755 updated_resources.len(),
756 unchanged_count
757 )
758 .bold()
759 );
760 println!();
761 println!("{}", "No files were modified (dry-run mode)".yellow());
762 } else {
763 println!("✓ {}", "No changes would be made".green());
764 }
765}
766
767pub fn display_no_changes(mode: OperationMode, quiet: bool) {
786 if quiet {
787 return;
788 }
789
790 match mode {
791 OperationMode::Install => println!("No dependencies to install"),
792 OperationMode::Update => println!("All dependencies are up to date!"),
793 }
794}
795
796#[cfg(test)]
797mod tests {
798 use super::*;
799 use tempfile::TempDir;
800
801 #[test]
802 fn test_command_context_from_manifest_path() {
803 let temp_dir = TempDir::new().unwrap();
804 let manifest_path = temp_dir.path().join("agpm.toml");
805
806 std::fs::write(
808 &manifest_path,
809 r#"
810[sources]
811test = "https://github.com/test/repo.git"
812
813[agents]
814"#,
815 )
816 .unwrap();
817
818 let context = CommandContext::from_manifest_path(&manifest_path).unwrap();
819
820 assert_eq!(context.manifest_path, manifest_path);
821 assert_eq!(context.project_dir, temp_dir.path());
822 assert_eq!(context.lockfile_path, temp_dir.path().join("agpm.lock"));
823 assert!(context.manifest.sources.contains_key("test"));
824 }
825
826 #[test]
827 fn test_command_context_missing_manifest() {
828 let result = CommandContext::from_manifest_path("/nonexistent/agpm.toml");
829 assert!(result.is_err());
830 assert!(result.unwrap_err().to_string().contains("not found"));
831 }
832
833 #[test]
834 fn test_command_context_invalid_manifest() {
835 let temp_dir = TempDir::new().unwrap();
836 let manifest_path = temp_dir.path().join("agpm.toml");
837
838 std::fs::write(&manifest_path, "invalid toml {{").unwrap();
840
841 let result = CommandContext::from_manifest_path(&manifest_path);
842 assert!(result.is_err());
843 assert!(result.unwrap_err().to_string().contains("Failed to parse manifest"));
844 }
845
846 #[test]
847 fn test_load_lockfile_exists() {
848 let temp_dir = TempDir::new().unwrap();
849 let manifest_path = temp_dir.path().join("agpm.toml");
850 let lockfile_path = temp_dir.path().join("agpm.lock");
851
852 std::fs::write(&manifest_path, "[sources]\n").unwrap();
854 std::fs::write(
855 &lockfile_path,
856 r#"
857version = 1
858
859[[sources]]
860name = "test"
861url = "https://github.com/test/repo.git"
862commit = "abc123"
863fetched_at = "2024-01-01T00:00:00Z"
864"#,
865 )
866 .unwrap();
867
868 let context = CommandContext::from_manifest_path(&manifest_path).unwrap();
869 let lockfile = context.load_lockfile().unwrap();
870
871 assert!(lockfile.is_some());
872 let lockfile = lockfile.unwrap();
873 assert_eq!(lockfile.sources.len(), 1);
874 assert_eq!(lockfile.sources[0].name, "test");
875 }
876
877 #[test]
878 fn test_load_lockfile_not_exists() {
879 let temp_dir = TempDir::new().unwrap();
880 let manifest_path = temp_dir.path().join("agpm.toml");
881
882 std::fs::write(&manifest_path, "[sources]\n").unwrap();
883
884 let context = CommandContext::from_manifest_path(&manifest_path).unwrap();
885 let lockfile = context.load_lockfile().unwrap();
886
887 assert!(lockfile.is_none());
888 }
889
890 #[test]
891 fn test_save_lockfile() {
892 let temp_dir = TempDir::new().unwrap();
893 let manifest_path = temp_dir.path().join("agpm.toml");
894
895 std::fs::write(&manifest_path, "[sources]\n").unwrap();
896
897 let context = CommandContext::from_manifest_path(&manifest_path).unwrap();
898
899 let lockfile = crate::lockfile::LockFile {
900 version: 1,
901 sources: vec![],
902 agents: vec![],
903 snippets: vec![],
904 commands: vec![],
905 scripts: vec![],
906 hooks: vec![],
907 mcp_servers: vec![],
908 };
909
910 context.save_lockfile(&lockfile).unwrap();
911
912 assert!(context.lockfile_path.exists());
913 let saved_content = std::fs::read_to_string(&context.lockfile_path).unwrap();
914 assert!(saved_content.contains("version = 1"));
915 }
916
917 #[test]
918 fn test_check_for_legacy_ccpm_no_files() {
919 let temp_dir = TempDir::new().unwrap();
920 let result = check_for_legacy_ccpm_files_from(temp_dir.path().to_path_buf());
921 assert!(result.is_none());
922 }
923
924 #[test]
925 fn test_check_for_legacy_ccpm_toml_only() {
926 let temp_dir = TempDir::new().unwrap();
927 std::fs::write(temp_dir.path().join("ccpm.toml"), "[sources]\n").unwrap();
928
929 let result = check_for_legacy_ccpm_files_from(temp_dir.path().to_path_buf());
930 assert!(result.is_some());
931 let msg = result.unwrap();
932 assert!(msg.contains("Legacy CCPM files detected"));
933 assert!(msg.contains("ccpm.toml"));
934 assert!(msg.contains("agpm migrate"));
935 }
936
937 #[test]
938 fn test_check_for_legacy_ccpm_lock_only() {
939 let temp_dir = TempDir::new().unwrap();
940 std::fs::write(temp_dir.path().join("ccpm.lock"), "# lock\n").unwrap();
941
942 let result = check_for_legacy_ccpm_files_from(temp_dir.path().to_path_buf());
943 assert!(result.is_some());
944 let msg = result.unwrap();
945 assert!(msg.contains("ccpm.lock"));
946 }
947
948 #[test]
949 fn test_check_for_legacy_ccpm_both_files() {
950 let temp_dir = TempDir::new().unwrap();
951 std::fs::write(temp_dir.path().join("ccpm.toml"), "[sources]\n").unwrap();
952 std::fs::write(temp_dir.path().join("ccpm.lock"), "# lock\n").unwrap();
953
954 let result = check_for_legacy_ccpm_files_from(temp_dir.path().to_path_buf());
955 assert!(result.is_some());
956 let msg = result.unwrap();
957 assert!(msg.contains("ccpm.toml and ccpm.lock"));
958 }
959
960 #[test]
961 fn test_find_legacy_ccpm_directory_no_files() {
962 let temp_dir = TempDir::new().unwrap();
963 let result = find_legacy_ccpm_directory(temp_dir.path());
964 assert!(result.is_none());
965 }
966
967 #[test]
968 fn test_find_legacy_ccpm_directory_in_current_dir() {
969 let temp_dir = TempDir::new().unwrap();
970 std::fs::write(temp_dir.path().join("ccpm.toml"), "[sources]\n").unwrap();
971
972 let result = find_legacy_ccpm_directory(temp_dir.path());
973 assert!(result.is_some());
974 assert_eq!(result.unwrap(), temp_dir.path());
975 }
976
977 #[test]
978 fn test_find_legacy_ccpm_directory_in_parent() {
979 let temp_dir = TempDir::new().unwrap();
980 let parent = temp_dir.path();
981 let child = parent.join("subdir");
982 std::fs::create_dir(&child).unwrap();
983
984 std::fs::write(parent.join("ccpm.toml"), "[sources]\n").unwrap();
986
987 let result = find_legacy_ccpm_directory(&child);
989 assert!(result.is_some());
990 assert_eq!(result.unwrap(), parent);
991 }
992
993 #[test]
994 fn test_find_legacy_ccpm_directory_finds_lock_file() {
995 let temp_dir = TempDir::new().unwrap();
996 std::fs::write(temp_dir.path().join("ccpm.lock"), "# lock\n").unwrap();
997
998 let result = find_legacy_ccpm_directory(temp_dir.path());
999 assert!(result.is_some());
1000 assert_eq!(result.unwrap(), temp_dir.path());
1001 }
1002
1003 #[tokio::test]
1004 async fn test_handle_legacy_ccpm_migration_no_files() {
1005 let temp_dir = TempDir::new().unwrap();
1006 let original_dir = std::env::current_dir().unwrap();
1007
1008 std::env::set_current_dir(temp_dir.path()).unwrap();
1010
1011 let result = handle_legacy_ccpm_migration().await;
1012
1013 std::env::set_current_dir(original_dir).unwrap();
1015
1016 assert!(result.is_ok());
1017 assert!(result.unwrap().is_none());
1018 }
1019
1020 #[cfg(test)]
1021 mod lockfile_regeneration_tests {
1022 use super::*;
1023 use crate::manifest::Manifest;
1024 use tempfile::TempDir;
1025
1026 #[test]
1027 fn test_load_lockfile_with_regeneration_valid_lockfile() {
1028 let temp_dir = TempDir::new().unwrap();
1029 let project_dir = temp_dir.path();
1030 let manifest_path = project_dir.join("agpm.toml");
1031 let lockfile_path = project_dir.join("agpm.lock");
1032
1033 let manifest_content = r#"[sources]
1035example = "https://github.com/example/repo.git"
1036
1037[agents]
1038test = { source = "example", path = "test.md", version = "v1.0.0" }
1039"#;
1040 std::fs::write(&manifest_path, manifest_content).unwrap();
1041
1042 let lockfile_content = r#"version = 1
1044
1045[[sources]]
1046name = "example"
1047url = "https://github.com/example/repo.git"
1048commit = "abc123def456789012345678901234567890abcd"
1049fetched_at = "2024-01-01T00:00:00Z"
1050
1051[[agents]]
1052name = "test"
1053source = "example"
1054path = "test.md"
1055version = "v1.0.0"
1056resolved_commit = "abc123def456789012345678901234567890abcd"
1057checksum = "sha256:examplechecksum"
1058installed_at = ".claude/agents/test.md"
1059"#;
1060 std::fs::write(&lockfile_path, lockfile_content).unwrap();
1061
1062 let manifest = Manifest::load(&manifest_path).unwrap();
1064 let ctx = CommandContext::new(manifest, project_dir.to_path_buf()).unwrap();
1065
1066 let result = ctx.load_lockfile_with_regeneration(true, "test").unwrap();
1067 assert!(result.is_some());
1068 }
1069
1070 #[test]
1071 fn test_load_lockfile_with_regeneration_invalid_toml() {
1072 let temp_dir = TempDir::new().unwrap();
1073 let project_dir = temp_dir.path();
1074 let manifest_path = project_dir.join("agpm.toml");
1075 let lockfile_path = project_dir.join("agpm.lock");
1076
1077 let manifest_content = r#"[sources]
1079example = "https://github.com/example/repo.git"
1080"#;
1081 std::fs::write(&manifest_path, manifest_content).unwrap();
1082
1083 std::fs::write(&lockfile_path, "invalid toml [[[").unwrap();
1085
1086 let manifest = Manifest::load(&manifest_path).unwrap();
1088 let ctx = CommandContext::new(manifest, project_dir.to_path_buf()).unwrap();
1089
1090 let result = ctx.load_lockfile_with_regeneration(true, "test");
1092 assert!(result.is_err());
1093
1094 let error_msg = result.unwrap_err().to_string();
1095 assert!(error_msg.contains("Invalid or corrupted lockfile detected"));
1096 assert!(error_msg.contains("beta software"));
1097 assert!(error_msg.contains("cp agpm.lock"));
1098 }
1099
1100 #[test]
1101 fn test_load_lockfile_with_regeneration_missing_lockfile() {
1102 let temp_dir = TempDir::new().unwrap();
1103 let project_dir = temp_dir.path();
1104 let manifest_path = project_dir.join("agpm.toml");
1105
1106 let manifest_content = r#"[sources]
1108example = "https://github.com/example/repo.git"
1109"#;
1110 std::fs::write(&manifest_path, manifest_content).unwrap();
1111
1112 let manifest = Manifest::load(&manifest_path).unwrap();
1114 let ctx = CommandContext::new(manifest, project_dir.to_path_buf()).unwrap();
1115
1116 let result = ctx.load_lockfile_with_regeneration(true, "test").unwrap();
1117 assert!(result.is_none()); }
1119
1120 #[test]
1121 fn test_load_lockfile_with_regeneration_version_incompatibility() {
1122 let temp_dir = TempDir::new().unwrap();
1123 let project_dir = temp_dir.path();
1124 let manifest_path = project_dir.join("agpm.toml");
1125 let lockfile_path = project_dir.join("agpm.lock");
1126
1127 let manifest_content = r#"[sources]
1129example = "https://github.com/example/repo.git"
1130"#;
1131 std::fs::write(&manifest_path, manifest_content).unwrap();
1132
1133 let lockfile_content = r#"version = 999
1135
1136[[sources]]
1137name = "example"
1138url = "https://github.com/example/repo.git"
1139commit = "abc123def456789012345678901234567890abcd"
1140fetched_at = "2024-01-01T00:00:00Z"
1141"#;
1142 std::fs::write(&lockfile_path, lockfile_content).unwrap();
1143
1144 let manifest = Manifest::load(&manifest_path).unwrap();
1146 let ctx = CommandContext::new(manifest, project_dir.to_path_buf()).unwrap();
1147
1148 let result = ctx.load_lockfile_with_regeneration(true, "test");
1149 assert!(result.is_err());
1150
1151 let error_msg = result.unwrap_err().to_string();
1152 assert!(error_msg.contains("version") || error_msg.contains("newer"));
1153 }
1154
1155 #[test]
1156 fn test_load_lockfile_with_regeneration_cannot_regenerate() {
1157 let temp_dir = TempDir::new().unwrap();
1158 let project_dir = temp_dir.path();
1159 let manifest_path = project_dir.join("agpm.toml");
1160 let lockfile_path = project_dir.join("agpm.lock");
1161
1162 let manifest_content = r#"[sources]
1164example = "https://github.com/example/repo.git"
1165"#;
1166 std::fs::write(&manifest_path, manifest_content).unwrap();
1167
1168 std::fs::write(&lockfile_path, "invalid toml [[[").unwrap();
1170
1171 let manifest = Manifest::load(&manifest_path).unwrap();
1173 let ctx = CommandContext::new(manifest, project_dir.to_path_buf()).unwrap();
1174
1175 let result = ctx.load_lockfile_with_regeneration(false, "test");
1176 assert!(result.is_err());
1177
1178 let error_msg = result.unwrap_err().to_string();
1180 assert!(!error_msg.contains("Invalid or corrupted lockfile detected"));
1181 assert!(
1182 error_msg.contains("Failed to load lockfile")
1183 || error_msg.contains("Invalid TOML syntax")
1184 );
1185 }
1186
1187 #[test]
1188 fn test_backup_and_regenerate_lockfile() {
1189 let temp_dir = TempDir::new().unwrap();
1190 let project_dir = temp_dir.path();
1191 let manifest_path = project_dir.join("agpm.toml");
1192 let lockfile_path = project_dir.join("agpm.lock");
1193
1194 let manifest_content = r#"[sources]
1196example = "https://github.com/example/repo.git"
1197"#;
1198 std::fs::write(&manifest_path, manifest_content).unwrap();
1199
1200 std::fs::write(&lockfile_path, "invalid content").unwrap();
1202
1203 let manifest = Manifest::load(&manifest_path).unwrap();
1205 let ctx = CommandContext::new(manifest, project_dir.to_path_buf()).unwrap();
1206
1207 let backup_path = lockfile_path.with_extension("lock.invalid");
1208
1209 ctx.backup_and_regenerate_lockfile(&backup_path, "test").unwrap();
1211
1212 assert!(backup_path.exists());
1214 assert_eq!(std::fs::read_to_string(&backup_path).unwrap(), "invalid content");
1215
1216 assert!(!lockfile_path.exists());
1218 }
1219
1220 #[test]
1221 fn test_create_non_interactive_error() {
1222 let temp_dir = TempDir::new().unwrap();
1223 let project_dir = temp_dir.path();
1224 let manifest_path = project_dir.join("agpm.toml");
1225
1226 let manifest_content = r#"[sources]
1228example = "https://github.com/example/repo.git"
1229"#;
1230 std::fs::write(&manifest_path, manifest_content).unwrap();
1231
1232 let manifest = Manifest::load(&manifest_path).unwrap();
1234 let ctx = CommandContext::new(manifest, project_dir.to_path_buf()).unwrap();
1235
1236 let error = ctx.create_non_interactive_error("Invalid TOML syntax", "test");
1237 let error_msg = error.to_string();
1238
1239 assert!(error_msg.contains("Invalid TOML syntax"));
1240 assert!(error_msg.contains("beta software"));
1241 assert!(error_msg.contains("cp agpm.lock"));
1242 assert!(error_msg.contains("rm agpm.lock"));
1243 assert!(error_msg.contains("agpm install"));
1244 }
1245 }
1246
1247 }