cleanup_removed_artifacts

Function cleanup_removed_artifacts 

Source
pub async fn cleanup_removed_artifacts(
    old_lockfile: &LockFile,
    new_lockfile: &LockFile,
    project_dir: &Path,
) -> Result<Vec<String>>
Expand description

Removes artifacts that are no longer needed based on lockfile comparison.

This function performs automatic cleanup of obsolete resource files by comparing the old and new lockfiles. It identifies and removes artifacts that have been:

  • Removed from manifest: Dependencies deleted from agpm.toml
  • Changed to content-only: Dependencies that changed from install: true to install: false
  • Relocated: Files with changed installed_at paths due to:
    • Relative path preservation (v0.3.18+)
    • Custom target changes
    • Dependency name changes
  • Replaced: Resources that moved due to source or version changes

After removing files, it also cleans up any empty parent directories to prevent directory accumulation over time.

§Cleanup Strategy

The function uses a set-based difference algorithm:

  1. Collects all installed_at paths from the new lockfile into a HashSet (excluding resources with install: false which should not have files)
  2. Iterates through old lockfile resources
  3. For each old path not in the new set:
    • Removes the file if it exists
    • Recursively cleans empty parent directories
    • Records the path for reporting

§Arguments

  • old_lockfile - The previous lockfile state containing old installation paths
  • new_lockfile - The current lockfile state with updated installation paths
  • project_dir - The project root directory (usually contains .claude/)

§Returns

Returns Ok(Vec<String>) containing the list of installed_at paths that were successfully removed. An empty vector indicates no artifacts needed cleanup.

§Errors

Returns an error if:

  • File removal fails due to permissions or locks
  • Directory cleanup encounters unexpected I/O errors
  • File system operations fail for other reasons

§Examples

§Basic Cleanup After Update

use agpm_cli::installer::cleanup_removed_artifacts;
use agpm_cli::lockfile::LockFile;
use std::path::Path;

let old_lockfile = LockFile::load(Path::new("agpm.lock"))?;
let new_lockfile = LockFile::new(); // After resolution
let project_dir = Path::new(".");

let removed = cleanup_removed_artifacts(&old_lockfile, &new_lockfile, project_dir).await?;
if !removed.is_empty() {
    println!("Cleaned up {} artifact(s)", removed.len());
    for path in removed {
        println!("  - Removed: {}", path);
    }
}

§Cleanup After Path Migration

When relative path preservation changes installation paths:

Old lockfile (v0.3.17):
  installed_at: ".claude/agents/helper.md"

New lockfile (v0.3.18+):
  installed_at: ".claude/agents/ai/helper.md"  # Preserved subdirectory

Cleanup removes: .claude/agents/helper.md

§Cleanup After Dependency Removal

// Old lockfile had 3 agents
let mut old_lockfile = LockFile::new();
old_lockfile.agents = vec![
    // ... 3 agents including one at .claude/agents/removed.md
];

// New lockfile only has 2 agents (one was removed from manifest)
let mut new_lockfile = LockFile::new();
new_lockfile.agents = vec![
    // ... 2 agents, removed.md is gone
];

let removed = cleanup_removed_artifacts(&old_lockfile, &new_lockfile, Path::new(".")).await?;
assert!(removed.contains(&".claude/agents/removed.md".to_string()));

§Integration with Install Command

This function is automatically called during agpm install when both old and new lockfiles exist:

// In src/cli/install.rs
if !self.frozen && !self.regenerate && lockfile_path.exists() {
    if let Ok(old_lockfile) = LockFile::load(&lockfile_path) {
        detect_tag_movement(&old_lockfile, &lockfile, self.quiet);

        // Automatic cleanup of removed or moved artifacts
        if let Ok(removed) = cleanup_removed_artifacts(
            &old_lockfile,
            &lockfile,
            actual_project_dir,
        ).await && !removed.is_empty() && !self.quiet {
            println!("🗑️  Cleaned up {} moved or removed artifact(s)", removed.len());
        }
    }
}

§Performance

  • Time Complexity: O(n + m) where n = old resources, m = new resources
  • Space Complexity: O(m) for the HashSet of new paths
  • I/O Operations: One file removal per obsolete artifact
  • Directory Cleanup: Walks up parent directories once per removed file

The function is highly efficient as it:

  • Uses HashSet for O(1) path lookups
  • Only performs I/O for files that actually exist
  • Cleans directories recursively but stops at first non-empty directory

§Safety

  • Only removes files explicitly tracked in the old lockfile
  • Never removes files outside the project directory
  • Stops directory cleanup at .claude/ boundary
  • Handles concurrent file access gracefully (ENOENT is not an error)

§Use Cases

§Relative Path Migration (v0.3.18+)

When upgrading to v0.3.18+, resource paths change to preserve directory structure:

Before: .claude/agents/helper.md  (flat)
After:  .claude/agents/ai/helper.md  (nested)

This function removes the old flat file automatically.

§Dependency Reorganization

When reorganizing dependencies with custom targets:

# Before
[agents]
helper = { source = "community", path = "agents/helper.md" }

# After (with custom target)
[agents]
helper = { source = "community", path = "agents/helper.md", target = "tools" }

Old file at .claude/agents/helper.md is removed, new file at .claude/agents/tools/helper.md is installed.

§Manifest Cleanup

Simply removing dependencies from agpm.toml triggers automatic cleanup:

# Remove unwanted dependency
[agents]
# old-agent = { ... }  # Commented out or deleted

The next agpm install removes the old agent file automatically.

§Version History

  • v0.3.18: Introduced to handle relative path preservation and custom target changes
  • Works in conjunction with cleanup_empty_dirs() for comprehensive cleanup