coderlib 0.1.0

A Rust library for AI-powered code assistance and agentic system
Documentation
//! Command discovery for finding command files in various directories

use std::path::PathBuf;
use tracing::{debug, info, warn};
use walkdir::WalkDir;

use super::{CustomCommand, CommandError, CommandConfig, CommandParser, utils};

/// Command discovery service
pub struct CommandDiscovery {
    parser: CommandParser,
    config: CommandConfig,
}

impl CommandDiscovery {
    /// Create a new command discovery service
    pub fn new(config: CommandConfig) -> Self {
        Self {
            parser: CommandParser::new(),
            config,
        }
    }
    
    /// Discover all commands from configured directories
    pub fn discover_all_commands(&self) -> Result<Vec<CustomCommand>, CommandError> {
        if !self.config.enabled {
            debug!("Custom commands are disabled");
            return Ok(Vec::new());
        }
        
        let mut commands = Vec::new();
        
        // Discover user commands
        match self.discover_user_commands() {
            Ok(user_commands) => {
                info!("Discovered {} user commands", user_commands.len());
                commands.extend(user_commands);
            }
            Err(e) => {
                warn!("Failed to discover user commands: {}", e);
            }
        }
        
        // Discover project commands
        match self.discover_project_commands() {
            Ok(project_commands) => {
                info!("Discovered {} project commands", project_commands.len());
                commands.extend(project_commands);
            }
            Err(e) => {
                warn!("Failed to discover project commands: {}", e);
            }
        }
        
        info!("Total commands discovered: {}", commands.len());
        Ok(commands)
    }
    
    /// Discover user-level commands
    fn discover_user_commands(&self) -> Result<Vec<CustomCommand>, CommandError> {
        let mut commands = Vec::new();
        
        for dir in &self.config.user_command_dirs {
            if dir.exists() {
                debug!("Scanning user command directory: {}", dir.display());
                match self.discover_commands_in_directory(dir) {
                    Ok(dir_commands) => {
                        debug!("Found {} commands in {}", dir_commands.len(), dir.display());
                        commands.extend(dir_commands);
                    }
                    Err(e) => {
                        warn!("Failed to scan directory {}: {}", dir.display(), e);
                    }
                }
            } else {
                debug!("User command directory does not exist: {}", dir.display());
            }
        }
        
        Ok(commands)
    }
    
    /// Discover project-level commands
    fn discover_project_commands(&self) -> Result<Vec<CustomCommand>, CommandError> {
        let current_dir = std::env::current_dir()
            .map_err(|e| CommandError::FileError(format!("Failed to get current directory: {}", e)))?;
        
        let project_command_dir = current_dir.join(&self.config.project_command_dir);
        
        if project_command_dir.exists() {
            debug!("Scanning project command directory: {}", project_command_dir.display());
            self.discover_commands_in_directory(&project_command_dir)
        } else {
            debug!("Project command directory does not exist: {}", project_command_dir.display());
            Ok(Vec::new())
        }
    }
    
    /// Discover commands in a specific directory
    fn discover_commands_in_directory(&self, dir: &PathBuf) -> Result<Vec<CustomCommand>, CommandError> {
        let mut commands = Vec::new();
        
        let walker = WalkDir::new(dir)
            .max_depth(self.config.max_depth)
            .follow_links(false);
        
        for entry in walker {
            let entry = entry.map_err(|e| CommandError::FileError(format!("Walk error: {}", e)))?;
            let path = entry.path();
            
            if utils::is_command_file(path) {
                debug!("Processing command file: {}", path.display());
                
                match self.parser.parse_command_file(path) {
                    Ok(command) => {
                        debug!("Successfully parsed command: {} ({})", command.name, command.id);
                        commands.push(command);
                    }
                    Err(e) => {
                        warn!("Failed to parse command file {}: {}", path.display(), e);
                    }
                }
            }
        }
        
        Ok(commands)
    }
    
    /// Get the configured user command directories
    pub fn get_user_command_directories(&self) -> &[PathBuf] {
        &self.config.user_command_dirs
    }
    
    /// Get the configured project command directory
    pub fn get_project_command_directory(&self) -> Result<PathBuf, CommandError> {
        let current_dir = std::env::current_dir()
            .map_err(|e| CommandError::FileError(format!("Failed to get current directory: {}", e)))?;
        Ok(current_dir.join(&self.config.project_command_dir))
    }
    
    /// Check if a specific directory contains commands
    pub fn has_commands_in_directory(&self, dir: &PathBuf) -> bool {
        if !dir.exists() {
            return false;
        }
        
        let walker = WalkDir::new(dir)
            .max_depth(self.config.max_depth)
            .follow_links(false);
        
        for entry in walker {
            if let Ok(entry) = entry {
                if utils::is_command_file(entry.path()) {
                    return true;
                }
            }
        }
        
        false
    }
    
    /// Get statistics about command discovery
    pub fn get_discovery_stats(&self) -> DiscoveryStats {
        let mut stats = DiscoveryStats::default();
        
        // Count user command directories
        for dir in &self.config.user_command_dirs {
            if dir.exists() {
                stats.user_directories += 1;
                if self.has_commands_in_directory(dir) {
                    stats.user_directories_with_commands += 1;
                }
            }
        }
        
        // Check project command directory
        if let Ok(project_dir) = self.get_project_command_directory() {
            if project_dir.exists() {
                stats.project_directory_exists = true;
                if self.has_commands_in_directory(&project_dir) {
                    stats.project_directory_has_commands = true;
                }
            }
        }
        
        stats
    }
}

/// Statistics about command discovery
#[derive(Debug, Default)]
pub struct DiscoveryStats {
    /// Number of user command directories that exist
    pub user_directories: usize,
    
    /// Number of user directories that contain commands
    pub user_directories_with_commands: usize,
    
    /// Whether the project command directory exists
    pub project_directory_exists: bool,
    
    /// Whether the project directory contains commands
    pub project_directory_has_commands: bool,
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::fs;
    use tempfile::TempDir;
    
    fn create_test_command_file(dir: &std::path::Path, name: &str, content: &str) -> std::path::PathBuf {
        let file_path = dir.join(format!("{}.md", name));
        fs::write(&file_path, content).unwrap();
        file_path
    }
    
    #[test]
    fn test_discover_commands_in_directory() {
        let temp_dir = TempDir::new().unwrap();
        let commands_dir = temp_dir.path().join("commands");
        fs::create_dir_all(&commands_dir).unwrap();
        
        // Create test command files
        create_test_command_file(&commands_dir, "hello", r#"# Hello Command
Say hello to someone.

Hello $NAME!
"#);
        
        create_test_command_file(&commands_dir, "deploy", r#"# Deploy Command
Deploy the application.

Deploying $APP to $ENVIRONMENT...
"#);
        
        // Create a non-command file (should be ignored)
        fs::write(commands_dir.join("readme.txt"), "This is not a command").unwrap();
        
        let config = CommandConfig::new();
        let discovery = CommandDiscovery::new(config);
        
        let commands = discovery.discover_commands_in_directory(&commands_dir).unwrap();
        
        assert_eq!(commands.len(), 2);
        assert!(commands.iter().any(|c| c.name == "Hello Command"));
        assert!(commands.iter().any(|c| c.name == "Deploy Command"));
    }
    
    #[test]
    fn test_has_commands_in_directory() {
        let temp_dir = TempDir::new().unwrap();
        let commands_dir = temp_dir.path().join("commands");
        fs::create_dir_all(&commands_dir).unwrap();
        
        let config = CommandConfig::new();
        let discovery = CommandDiscovery::new(config);
        
        // Initially no commands
        assert!(!discovery.has_commands_in_directory(&commands_dir));
        
        // Add a command file
        create_test_command_file(&commands_dir, "test", "# Test\nTest command");
        
        // Now should have commands
        assert!(discovery.has_commands_in_directory(&commands_dir));
    }
    
    #[test]
    fn test_discovery_stats() {
        let temp_dir = TempDir::new().unwrap();
        let user_dir = temp_dir.path().join("user");
        fs::create_dir_all(&user_dir).unwrap();
        
        create_test_command_file(&user_dir, "user-cmd", "# User Command\nUser command");
        
        let config = CommandConfig::new()
            .add_user_dir(&user_dir);
        
        let discovery = CommandDiscovery::new(config);
        let stats = discovery.get_discovery_stats();
        
        assert!(stats.user_directories > 0);
        assert!(stats.user_directories_with_commands > 0);
    }
    
    #[test]
    fn test_discover_all_commands() {
        let temp_dir = TempDir::new().unwrap();
        let user_dir = temp_dir.path().join("user");
        fs::create_dir_all(&user_dir).unwrap();
        
        create_test_command_file(&user_dir, "global", r#"# Global Command
A global user command.

Global action for $TARGET
"#);
        
        let config = CommandConfig::new()
            .add_user_dir(&user_dir);
        
        let discovery = CommandDiscovery::new(config);
        let commands = discovery.discover_all_commands().unwrap();
        
        assert!(!commands.is_empty());
        assert!(commands.iter().any(|c| c.name == "Global Command"));
    }
    
    #[test]
    fn test_disabled_discovery() {
        let config = CommandConfig::new().auto_reload(false);
        let mut config = config;
        config.enabled = false;
        
        let discovery = CommandDiscovery::new(config);
        let commands = discovery.discover_all_commands().unwrap();
        
        assert!(commands.is_empty());
    }
}