packlet 0.2.0

A high-performance tool that bundles local code dependencies into a single markdown file by following import statements from an entry point
Documentation
use anyhow::Result;
use async_trait::async_trait;
use lru::LruCache;
use std::num::NonZeroUsize;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use tokio::fs;
use tokio::sync::Mutex;

/// Find the git root directory by searching for .git folder
pub async fn find_git_root(from: &Path) -> Option<PathBuf> {
    let mut current = if from.is_dir() {
        from.to_path_buf()
    } else {
        from.parent()?.to_path_buf()
    };

    loop {
        let git_dir = current.join(".git");
        if git_dir.exists() {
            return Some(current);
        }

        current = current.parent()?.to_path_buf();
    }
}

#[async_trait]
pub trait FileSystemProvider: Send + Sync {
    async fn read_file(&self, path: &Path) -> Result<String>;
    async fn exists(&self, path: &Path) -> bool;
    async fn is_directory(&self, path: &Path) -> bool;
    async fn canonicalize(&self, path: &Path) -> Result<PathBuf>;
}

pub struct LocalFileSystem;

#[async_trait]
impl FileSystemProvider for LocalFileSystem {
    async fn read_file(&self, path: &Path) -> Result<String> {
        Ok(fs::read_to_string(path).await?)
    }

    async fn exists(&self, path: &Path) -> bool {
        fs::try_exists(path).await.unwrap_or(false)
    }

    async fn is_directory(&self, path: &Path) -> bool {
        if let Ok(metadata) = fs::metadata(path).await {
            metadata.is_dir()
        } else {
            false
        }
    }

    async fn canonicalize(&self, path: &Path) -> Result<PathBuf> {
        Ok(tokio::fs::canonicalize(path).await?)
    }
}

pub struct CachedFileSystem {
    inner: Box<dyn FileSystemProvider>,
    cache: Arc<Mutex<LruCache<PathBuf, String>>>,
    canonical_cache: Arc<Mutex<LruCache<PathBuf, PathBuf>>>,
}

impl CachedFileSystem {
    pub fn new(inner: Box<dyn FileSystemProvider>) -> Self {
        Self {
            inner,
            cache: Arc::new(Mutex::new(LruCache::new(NonZeroUsize::new(512).unwrap()))),
            // Canonical cache - larger since it's just paths, not file contents
            canonical_cache: Arc::new(Mutex::new(LruCache::new(NonZeroUsize::new(2048).unwrap()))),
        }
    }
}

#[async_trait]
impl FileSystemProvider for CachedFileSystem {
    async fn read_file(&self, path: &Path) -> Result<String> {
        let mut cache = self.cache.lock().await;
        if let Some(content) = cache.get(path) {
            return Ok(content.clone());
        }
        let content = self.inner.read_file(path).await?;
        cache.put(path.to_path_buf(), content.clone());
        Ok(content)
    }

    async fn exists(&self, path: &Path) -> bool {
        self.inner.exists(path).await
    }

    async fn is_directory(&self, path: &Path) -> bool {
        self.inner.is_directory(path).await
    }

    async fn canonicalize(&self, path: &Path) -> Result<PathBuf> {
        let mut cache = self.canonical_cache.lock().await;

        if let Some(canonical) = cache.get(path) {
            return Ok(canonical.clone());
        }

        drop(cache); // Release lock before async call
        let canonical = self.inner.canonicalize(path).await?;

        let mut cache = self.canonical_cache.lock().await;
        cache.put(path.to_path_buf(), canonical.clone());

        Ok(canonical)
    }
}