Cache

Struct Cache 

Source
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::path for cross-platform compatibility
  • Cache location: Follows platform conventions for app data storage
  • File locking: Uses fs4 crate 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

Source

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());
Source

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"))?;
Source

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 error
Source

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());
Source

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 URL
  • sha - 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());
Source

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
  1. Lock acquisition: Acquires exclusive lock for the source name
  2. Directory check: Determines if repository already exists in cache
  3. Clone or update: Either clones new repository or fetches updates
  4. Version checkout: Switches to requested version if specified
  5. Path return: Returns path to cached repository
§Concurrency Behavior
  • Same source: Concurrent calls with the same name will block
  • Different sources: Concurrent calls with different name run 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 main or master)
§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?;
Source

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
Source

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.

Source

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 manifest
  • url - Git repository URL
  • sha - 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?;
Source

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 directory
  • source_path - Relative path to the resource file within the repository
  • target_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?;
Source

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
  1. Source validation: Verifies the source file exists in repository
  2. Directory creation: Creates target parent directories if needed
  3. Atomic copy: Performs file copy operation safely
  4. 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 directory
  • source_path - Relative path to resource file within repository
  • target_path - Absolute path where resource should be installed
  • show_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?;
Source

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
  1. Directory scanning: Enumerates all cached repository directories
  2. Active comparison: Checks each directory against active sources list
  3. Safe removal: Removes only unused directories, preserving files
  4. 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);
Source

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);
Source

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());
}
Source

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 0

Clear 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");
    }
}

Trait Implementations§

Source§

impl Clone for Cache

Source§

fn clone(&self) -> Self

Returns a duplicate of the value. Read more
1.0.0 · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more

Auto Trait Implementations§

§

impl Freeze for Cache

§

impl !RefUnwindSafe for Cache

§

impl Send for Cache

§

impl Sync for Cache

§

impl Unpin for Cache

§

impl !UnwindSafe for Cache

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self>

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self>

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> Pointable for T

Source§

const ALIGN: usize

The alignment of pointer.
Source§

type Init = T

The type for initializers.
Source§

unsafe fn init(init: <T as Pointable>::Init) -> usize

Initializes a with the given initializer. Read more
Source§

unsafe fn deref<'a>(ptr: usize) -> &'a T

Dereferences the given pointer. Read more
Source§

unsafe fn deref_mut<'a>(ptr: usize) -> &'a mut T

Mutably dereferences the given pointer. Read more
Source§

unsafe fn drop(ptr: usize)

Drops the object pointed to by the given pointer. Read more
Source§

impl<T> PolicyExt for T
where T: ?Sized,

Source§

fn and<P, B, E>(self, other: P) -> And<T, P>
where T: Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow only if self and other return Action::Follow. Read more
Source§

fn or<P, B, E>(self, other: P) -> Or<T, P>
where T: Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow if either self or other returns Action::Follow. Read more
Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<V, T> VZip<V> for T
where V: MultiLane<T>,

Source§

fn vzip(self) -> V

Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self>
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self>

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

impl<T> ErasedDestructor for T
where T: 'static,