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: truetoinstall: false - Relocated: Files with changed
installed_atpaths 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:
- Collects all
installed_atpaths from the new lockfile into aHashSet(excluding resources withinstall: falsewhich should not have files) - Iterates through old lockfile resources
- 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 pathsnew_lockfile- The current lockfile state with updated installation pathsproject_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
HashSetof 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
HashSetfor 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 deletedThe 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