pub struct Cache { /* private fields */ }Expand description
Git repository cache for efficient resource management
The Cache struct provides the primary interface for managing Git repository
caching in AGPM. It handles repository cloning, updating, version management,
and resource file copying operations.
§Thread Safety
While the Cache struct itself is not thread-safe (not Send + Sync),
multiple instances can safely operate on the same cache directory through
the file-based locking mechanism provided by CacheLock.
§Platform Compatibility
The cache automatically handles platform-specific differences:
- Path separators: Uses
std::pathfor cross-platform compatibility - Cache location: Follows platform conventions for app data storage
- File locking: Uses
fs4crate for cross-platform file locking - Directory creation: Handles permissions and long paths on Windows
§Examples
Create a cache with default platform-specific location:
use agpm_cli::cache::Cache;
let cache = Cache::new()?;
println!("Cache location: {}", cache.get_cache_location().display());Create a cache with custom location (useful for testing):
use agpm_cli::cache::Cache;
use std::path::PathBuf;
let custom_dir = PathBuf::from("/tmp/test-cache");
let cache = Cache::with_dir(custom_dir)?;Implementations§
Source§impl Cache
impl Cache
Sourcepub fn new() -> Result<Self>
pub fn new() -> Result<Self>
Creates a new Cache instance using the default platform-specific cache directory.
The cache directory is determined based on the current platform:
- Linux/macOS:
~/.agpm/cache/ - Windows:
%LOCALAPPDATA%\agpm\cache\
§Environment Variable Override
The cache location can be overridden by setting the AGPM_CACHE_DIR
environment variable. This is particularly useful for:
- Testing with isolated cache directories
- CI/CD environments with specific cache locations
- Custom deployment scenarios
§Errors
Returns an error if:
- Unable to determine the home/local data directory
- The resolved path is invalid or inaccessible
§Examples
use agpm_cli::cache::Cache;
let cache = Cache::new()?;
println!("Using cache at: {}", cache.get_cache_location().display());Sourcepub fn with_dir(cache_dir: PathBuf) -> Result<Self>
pub fn with_dir(cache_dir: PathBuf) -> Result<Self>
Creates a new Cache instance using a custom cache directory.
This constructor allows you to specify exactly where the cache should be stored, overriding platform defaults. The directory will be created if it doesn’t exist when cache operations are performed.
§Use Cases
- Testing: Use temporary directories for isolated test environments
- Development: Use project-local cache directories
- Deployment: Use specific paths in containerized environments
- Multi-user systems: Use user-specific cache locations
§Parameters
cache_dir- The absolute path where cache data should be stored
§Examples
use agpm_cli::cache::Cache;
use std::path::PathBuf;
// Use a project-local cache
let project_cache = Cache::with_dir(PathBuf::from("./cache"))?;
// Use a system-wide cache
let system_cache = Cache::with_dir(PathBuf::from("/var/cache/agpm"))?;
// Use a temporary cache for testing
let temp_cache = Cache::with_dir(std::env::temp_dir().join("agpm-test"))?;Sourcepub async fn ensure_cache_dir(&self) -> Result<()>
pub async fn ensure_cache_dir(&self) -> Result<()>
Ensures the cache directory exists, creating it if necessary.
This method creates the cache directory and all necessary parent directories if they don’t already exist. It’s safe to call multiple times - it will not error if the directory already exists.
§Platform Considerations
- Windows: Handles long path names (>260 characters) correctly
- Unix: Respects umask settings for directory permissions
- All platforms: Creates intermediate directories as needed
§Errors
Returns an error if:
- Insufficient permissions to create the directory
- Disk space is exhausted
- Path contains invalid characters for the platform
- A file exists at the target path (not a directory)
§Examples
use agpm_cli::cache::Cache;
let cache = Cache::new()?;
// Ensure cache directory exists before operations
cache.ensure_cache_dir().await?;
// Safe to call multiple times
cache.ensure_cache_dir().await?; // No errorSourcepub fn cache_dir(&self) -> &Path
pub fn cache_dir(&self) -> &Path
Returns the path to the cache directory.
This is useful for operations that need direct access to the cache directory, such as lock file cleanup or cache size calculations.
§Example
use agpm_cli::cache::Cache;
let cache = Cache::new()?;
let cache_dir = cache.cache_dir();
println!("Cache directory: {}", cache_dir.display());Sourcepub fn get_worktree_path(&self, url: &str, sha: &str) -> Result<PathBuf>
pub fn get_worktree_path(&self, url: &str, sha: &str) -> Result<PathBuf>
Get the worktree path for a specific URL and commit SHA.
This method constructs the expected worktree directory path based on the cache’s
naming scheme. It does NOT check if the worktree exists or create it - use
get_or_create_worktree_for_sha for that.
§Arguments
url- Git repository URLsha- Full commit SHA (will be shortened to first 8 characters)
§Returns
Path to the worktree directory (may not exist yet)
§Example
use agpm_cli::cache::Cache;
let cache = Cache::new()?;
let path = cache.get_worktree_path(
"https://github.com/owner/repo.git",
"abc1234567890def"
)?;
println!("Worktree path: {}", path.display());Sourcepub async fn get_or_clone_source(
&self,
name: &str,
url: &str,
version: Option<&str>,
) -> Result<PathBuf>
pub async fn get_or_clone_source( &self, name: &str, url: &str, version: Option<&str>, ) -> Result<PathBuf>
Gets or clones a source repository, ensuring it’s available in the cache.
This is the primary method for source repository management. It handles both initial cloning of new repositories and updating existing cached repositories. The operation is atomic and thread-safe through file-based locking.
§Operation Flow
- Lock acquisition: Acquires exclusive lock for the source name
- Directory check: Determines if repository already exists in cache
- Clone or update: Either clones new repository or fetches updates
- Version checkout: Switches to requested version if specified
- Path return: Returns path to cached repository
§Concurrency Behavior
- Same source: Concurrent calls with the same
namewill block - Different sources: Concurrent calls with different
namerun in parallel - Process safety: Safe across multiple AGPM processes
§Version Handling
The version parameter accepts various Git reference types:
- Tags:
"v1.0.0","release-2023"(most common for releases) - Branches:
"main","develop","feature/new-agents" - Commits:
"abc123def"(full or short SHA hashes) - None: Uses repository’s default branch (typically
mainormaster)
§Parameters
name- Unique source identifier (used for cache directory and locking)url- Git repository URL (HTTPS, SSH, or local paths)version- Optional version constraint (tag, branch, or commit)
§Returns
Returns the PathBuf to the cached repository directory, which contains
the full Git repository structure and can be used for resource file access.
§Errors
Returns an error if:
- Network issues: Unable to clone or fetch from remote repository
- Authentication: Invalid credentials for private repositories
- Version issues: Specified version doesn’t exist in repository
- Lock timeout: Unable to acquire exclusive lock (rare)
- File system: Permission or disk space issues
- Git errors: Repository corruption or invalid Git operations
§Performance Notes
- First call: Performs full repository clone (slower)
- Subsequent calls: Only fetches updates (faster)
- Version switching: Uses Git checkout (very fast)
- Parallel sources: Multiple sources processed concurrently
§Examples
Clone a public repository with specific version:
use agpm_cli::cache::Cache;
let cache = Cache::new()?;
let repo_path = cache.get_or_clone_source(
"community",
"https://github.com/example/agpm-community.git",
Some("v1.2.0")
).await?;
println!("Repository cached at: {}", repo_path.display());Use latest version from default branch:
use agpm_cli::cache::Cache;
let cache = Cache::new()?;
let repo_path = cache.get_or_clone_source(
"dev-tools",
"https://github.com/myorg/dev-tools.git",
None // Use default branch
).await?;Work with development branch:
use agpm_cli::cache::Cache;
let cache = Cache::new()?;
let repo_path = cache.get_or_clone_source(
"experimental",
"https://github.com/myorg/experimental.git",
Some("develop")
).await?;Sourcepub async fn cleanup_worktree(&self, worktree_path: &Path) -> Result<()>
pub async fn cleanup_worktree(&self, worktree_path: &Path) -> Result<()>
Clean up a worktree after use (fast version).
This just removes the worktree directory without calling git.
Git will clean up its internal references when git worktree prune is called.
§Parameters
worktree_path- The path to the worktree to clean up
Sourcepub async fn cleanup_all_worktrees(&self) -> Result<()>
pub async fn cleanup_all_worktrees(&self) -> Result<()>
Clean up all worktrees in the cache.
This is useful for cleaning up after batch operations or on cache clear.
Sourcepub async fn get_or_create_worktree_for_sha(
&self,
name: &str,
url: &str,
sha: &str,
context: Option<&str>,
) -> Result<PathBuf>
pub async fn get_or_create_worktree_for_sha( &self, name: &str, url: &str, sha: &str, context: Option<&str>, ) -> Result<PathBuf>
Get or create a worktree for a specific commit SHA.
This method is the cornerstone of AGPM’s optimized dependency resolution. By using commit SHAs as the primary key for worktrees, we ensure:
- Maximum worktree reuse (same SHA = same worktree)
- Deterministic installations (SHA uniquely identifies content)
- Reduced disk usage (no duplicate worktrees for same commit)
§SHA-Based Caching Strategy
Unlike version-based worktrees that create separate directories for “v1.0.0” and “release-1.0” even if they point to the same commit, SHA-based worktrees ensure a single worktree per unique commit.
§Parameters
name- Source name from manifesturl- Git repository URLsha- Full 40-character commit SHA (must be pre-resolved)context- Optional context for logging
§Returns
Path to the worktree containing the exact commit specified by SHA.
§Example
let cache = Cache::new()?;
// First resolve version to SHA
let sha = "abc1234567890def1234567890abcdef12345678";
// Get worktree for that specific commit
let worktree = cache.get_or_create_worktree_for_sha(
"community",
"https://github.com/example/repo.git",
sha,
Some("my-agent")
).await?;Sourcepub async fn copy_resource(
&self,
source_dir: &Path,
source_path: &str,
target_path: &Path,
) -> Result<()>
pub async fn copy_resource( &self, source_dir: &Path, source_path: &str, target_path: &Path, ) -> Result<()>
Copies a resource file from cached repository to project directory.
This method performs the core resource installation operation by copying files from the cached Git repository to the project’s local directory. It provides a simple interface for resource installation without output.
§Copy Strategy
The method uses a copy-based approach rather than symlinks for:
- Cross-platform compatibility: Works identically on all platforms
- Git integration: Real files can be tracked and committed
- Editor support: No symlink confusion in IDEs and editors
- User flexibility: Local files can be modified if needed
§Path Resolution
- Source path: Relative to the repository root directory
- Target path: Absolute path where file should be installed
- Directory creation: Parent directories created automatically
- Path normalization: Handles platform-specific path separators
§Parameters
source_dir- Path to the cached repository directorysource_path- Relative path to the resource file within the repositorytarget_path- Absolute path where the resource should be installed
§Errors
Returns an error if:
- Source file doesn’t exist in the repository
- Target directory cannot be created (permissions)
- File copy operation fails (disk space, permissions)
- Source path attempts directory traversal (security)
§Examples
Copy a single resource file:
use agpm_cli::cache::Cache;
use std::path::PathBuf;
let cache = Cache::new()?;
// Get cached repository
let repo_path = cache.get_or_clone_source(
"community",
"https://github.com/example/repo.git",
Some("v1.0.0")
).await?;
// Copy resource to project
cache.copy_resource(
&repo_path,
"agents/helper.md", // Source: agents/helper.md in repository
&PathBuf::from("./my-agents/helper.md") // Target: project location
).await?;Copy nested resource:
use agpm_cli::cache::Cache;
use std::path::PathBuf;
let cache = Cache::new()?;
let repo_path = PathBuf::from("/cache/community");
cache.copy_resource(
&repo_path,
"tools/generators/api-client.md", // Nested source path
&PathBuf::from("./tools/api-client.md") // Flattened target
).await?;Sourcepub async fn copy_resource_with_output(
&self,
source_dir: &Path,
source_path: &str,
target_path: &Path,
show_output: bool,
) -> Result<()>
pub async fn copy_resource_with_output( &self, source_dir: &Path, source_path: &str, target_path: &Path, show_output: bool, ) -> Result<()>
Copies a resource file with optional installation output messages.
This is the full-featured resource copying method that provides control over whether installation progress is displayed to the user. It handles all the details of safe file copying including directory creation, error handling, and atomic operations.
§Operation Details
- Source validation: Verifies the source file exists in repository
- Directory creation: Creates target parent directories if needed
- Atomic copy: Performs file copy operation safely
- Progress output: Optionally displays installation confirmation
§File Safety
- Overwrite protection: Will overwrite existing files without warning
- Atomic operations: Uses system copy operations for atomicity
- Permission preservation: Maintains reasonable file permissions
- Path validation: Prevents directory traversal attacks
§Output Control
When show_output is true, displays user-friendly installation messages:
✅ Installed ./agents/helper.md
✅ Installed ./snippets/docker-compose.md§Parameters
source_dir- Path to the cached repository directorysource_path- Relative path to resource file within repositorytarget_path- Absolute path where resource should be installedshow_output- Whether to display installation progress messages
§Errors
Returns specific error types for different failure modes:
AgpmError::ResourceFileNotFound: Source file doesn’t exist- File system errors: Permission, disk space, invalid paths
- Directory creation errors: Parent directory creation failures
§Examples
Silent installation (for batch operations):
use agpm_cli::cache::Cache;
use std::path::PathBuf;
let cache = Cache::new()?;
let repo_path = PathBuf::from("/cache/community");
cache.copy_resource_with_output(
&repo_path,
"agents/helper.md",
&PathBuf::from("./agents/helper.md"),
false // No output
).await?;Interactive installation (with progress):
use agpm_cli::cache::Cache;
use std::path::PathBuf;
let cache = Cache::new()?;
let repo_path = PathBuf::from("/cache/community");
cache.copy_resource_with_output(
&repo_path,
"snippets/deployment.md",
&PathBuf::from("./snippets/deployment.md"),
true // Show "✅ Installed" message
).await?;Sourcepub async fn clean_unused(&self, active_sources: &[String]) -> Result<usize>
pub async fn clean_unused(&self, active_sources: &[String]) -> Result<usize>
Removes unused cached repositories to reclaim disk space.
This method performs selective cache cleanup by removing repositories that are no longer referenced by any active source configurations. It’s a safe operation that preserves repositories currently in use.
§Cleanup Strategy
- Directory scanning: Enumerates all cached repository directories
- Active comparison: Checks each directory against active sources list
- Safe removal: Removes only unused directories, preserving files
- Progress reporting: Displays removal progress for user feedback
§Safety Guarantees
- Active protection: Never removes repositories listed in active sources
- Directory-only: Only removes directories, preserves any loose files
- Atomic removal: Each directory is removed completely or not at all
- Lock awareness: Respects file locks but doesn’t acquire them
§Performance Considerations
- I/O intensive: Scans entire cache directory structure
- Disk space recovery: Can free significant space for large repositories
- Network savings: Removed repositories will need re-cloning if used again
- Concurrent safe: Can run while other cache operations are in progress
§Parameters
active_sources- List of source names that should be preserved in cache
§Returns
Returns the number of repository directories that were successfully removed.
§Errors
Returns an error if:
- Cache directory cannot be read (permissions)
- Unable to remove a directory (file locks, permissions)
- File system errors during directory traversal
§Output Messages
Displays progress messages for each removed repository:
🗑️ Removing unused cache: old-project
🗑️ Removing unused cache: deprecated-tools§Examples
Clean cache based on current manifest sources:
use agpm_cli::cache::Cache;
let cache = Cache::new()?;
// Active sources from current agpm.toml
let active_sources = vec![
"community".to_string(),
"work-tools".to_string(),
"personal".to_string(),
];
let removed = cache.clean_unused(&active_sources).await?;
println!("Cleaned {} unused repositories", removed);Clean all cached repositories:
use agpm_cli::cache::Cache;
let cache = Cache::new()?;
// Empty active list removes everything
let removed = cache.clean_unused(&[]).await?;
println!("Removed all {} cached repositories", removed);Sourcepub async fn get_cache_size(&self) -> Result<u64>
pub async fn get_cache_size(&self) -> Result<u64>
Calculates the total size of the cache directory in bytes.
This method recursively calculates the disk space used by all cached repositories and supporting files. It’s useful for cache size monitoring, cleanup decisions, and storage management.
§Calculation Method
- Recursive traversal: Includes all subdirectories and files
- Actual file sizes: Reports real disk usage, not allocated blocks
- All file types: Includes Git objects, working files, and lock files
- Cross-platform: Consistent behavior across different file systems
§Performance Notes
- I/O intensive: May be slow for very large caches
- File system dependent: Performance varies by underlying storage
- Concurrent safe: Can run during other cache operations
- Memory efficient: Streams directory traversal without loading all paths
§Returns
Returns the total size in bytes. For a non-existent cache directory,
returns 0 without error.
§Errors
Returns an error if:
- Permission denied reading cache directory or subdirectories
- File system errors during directory traversal
- Symbolic link cycles (rare, but possible)
§Examples
Check current cache size:
use agpm_cli::cache::Cache;
let cache = Cache::new()?;
let size_bytes = cache.get_cache_size().await?;
let size_mb = size_bytes / 1024 / 1024;
println!("Cache size: {} MB ({} bytes)", size_mb, size_bytes);Display human-readable sizes:
use agpm_cli::cache::Cache;
let cache = Cache::new()?;
let size_bytes = cache.get_cache_size().await?;
let (size, unit) = match size_bytes {
s if s < 1024 => (s, "B"),
s if s < 1024 * 1024 => (s / 1024, "KB"),
s if s < 1024 * 1024 * 1024 => (s / 1024 / 1024, "MB"),
s => (s / 1024 / 1024 / 1024, "GB"),
};
println!("Cache size: {}{}", size, unit);Sourcepub fn get_cache_location(&self) -> &Path
pub fn get_cache_location(&self) -> &Path
Returns the path to the cache directory.
This method provides access to the cache directory path for inspection, logging, or integration with other tools. The path represents where all cached repositories and supporting files are stored.
§Return Value
Returns a reference to the Path representing the cache directory.
The path may or may not exist on the file system - use ensure_cache_dir
to create it if needed.
§Thread Safety
This method is safe to call from multiple threads as it only returns
a reference to the immutable path stored in the Cache instance.
§Examples
Display cache location:
use agpm_cli::cache::Cache;
let cache = Cache::new()?;
println!("Cache stored at: {}", cache.get_cache_location().display());Check if cache exists:
use agpm_cli::cache::Cache;
let cache = Cache::new()?;
let location = cache.get_cache_location();
if location.exists() {
println!("Cache directory exists at: {}", location.display());
} else {
println!("Cache directory not yet created: {}", location.display());
}Sourcepub async fn clear_all(&self) -> Result<()>
pub async fn clear_all(&self) -> Result<()>
Completely removes the entire cache directory and all its contents.
This is a destructive operation that removes all cached repositories, lock files, and any other cache-related data. Use with caution as this will require re-cloning all repositories on the next operation.
§Operation Details
- Complete removal: Deletes the entire cache directory tree
- Recursive deletion: Removes all subdirectories and files
- Lock files: Also removes .locks directory and all lock files
- Atomic operation: Either succeeds completely or leaves cache intact
§Recovery Impact
After calling this method:
- All repositories must be re-cloned on next use
- Network bandwidth will be required for repository downloads
- Disk space is immediately reclaimed
- Cache directory will be recreated automatically on next operation
§Safety Considerations
- No confirmation: This method doesn’t ask for confirmation
- Irreversible: Cannot undo the deletion operation
- Concurrent operations: May interfere with running cache operations
- Lock respect: Doesn’t wait for locks, may fail if repositories are in use
§Errors
Returns an error if:
- Permission denied for cache directory or contents
- Files are locked by other processes
- File system errors during deletion
- Cache directory is in use by another process
§Output Messages
Displays confirmation message on successful completion:
🗑️ Cleared all cache§Examples
Clear cache for fresh start:
use agpm_cli::cache::Cache;
let cache = Cache::new()?;
// Check size before clearing
let size_before = cache.get_cache_size().await?;
println!("Cache size before: {} bytes", size_before);
// Clear everything
cache.clear_all().await?;
// Verify cache is empty
let size_after = cache.get_cache_size().await?;
println!("Cache size after: {} bytes", size_after); // Should be 0Clear cache with error handling:
use agpm_cli::cache::Cache;
let cache = Cache::new()?;
match cache.clear_all().await {
Ok(()) => println!("Cache cleared successfully"),
Err(e) => {
eprintln!("Failed to clear cache: {}", e);
eprintln!("Some files may be in use by other processes");
}
}