coderlib 0.1.0

A Rust library for AI-powered code assistance and agentic system
Documentation
//! Command manager implementation for handling custom commands

use async_trait::async_trait;
use std::collections::HashMap;
use tokio::sync::RwLock;
use tracing::{debug, info};

use super::{
    CustomCommand, CommandExecution, CommandError, CommandManager, CommandConfig,
    CommandDiscovery, CommandParser,
};

/// Implementation of the command manager
pub struct CommandManagerImpl {
    /// Command discovery service
    discovery: CommandDiscovery,
    
    /// Command parser
    parser: CommandParser,
    
    /// Cache of discovered commands
    commands: RwLock<HashMap<String, CustomCommand>>,
    
    /// Configuration
    config: CommandConfig,
}

impl CommandManagerImpl {
    /// Create a new command manager
    pub fn new(config: CommandConfig) -> Self {
        let discovery = CommandDiscovery::new(config.clone());
        let parser = CommandParser::new();
        
        Self {
            discovery,
            parser,
            commands: RwLock::new(HashMap::new()),
            config,
        }
    }
    
    /// Create a command manager with default configuration
    pub fn with_default_config() -> Self {
        Self::new(CommandConfig::default())
    }
    
    /// Get the current configuration
    pub fn config(&self) -> &CommandConfig {
        &self.config
    }
    
    /// Validate command arguments against parameter requirements
    fn validate_arguments(
        &self,
        command: &CustomCommand,
        arguments: &HashMap<String, String>,
    ) -> Result<(), CommandError> {
        for param in &command.parameters {
            if param.required && !arguments.contains_key(&param.name) {
                return Err(CommandError::MissingParameter(param.name.clone()));
            }
        }
        Ok(())
    }
    
    /// Apply default values for missing optional parameters
    fn apply_defaults(
        &self,
        command: &CustomCommand,
        mut arguments: HashMap<String, String>,
    ) -> HashMap<String, String> {
        for param in &command.parameters {
            if !arguments.contains_key(&param.name) {
                if let Some(default) = &param.default_value {
                    arguments.insert(param.name.clone(), default.clone());
                }
            }
        }
        arguments
    }
    
    /// Get discovery statistics
    pub fn get_discovery_stats(&self) -> super::discovery::DiscoveryStats {
        self.discovery.get_discovery_stats()
    }
    
    /// Check if commands are enabled
    pub fn is_enabled(&self) -> bool {
        self.config.enabled
    }
    
    /// Get the number of cached commands
    pub async fn command_count(&self) -> usize {
        let commands = self.commands.read().await;
        commands.len()
    }
    
    /// Clear the command cache
    pub async fn clear_cache(&self) {
        let mut commands = self.commands.write().await;
        commands.clear();
        debug!("Command cache cleared");
    }
    
    /// Get all command IDs
    pub async fn get_command_ids(&self) -> Vec<String> {
        let commands = self.commands.read().await;
        commands.keys().cloned().collect()
    }
    
    /// Check if a command exists
    pub async fn has_command(&self, command_id: &str) -> bool {
        let commands = self.commands.read().await;
        commands.contains_key(command_id)
    }
    
    /// Get commands by type
    pub async fn get_commands_by_type(&self, command_type: super::CommandType) -> Vec<CustomCommand> {
        let commands = self.commands.read().await;
        commands
            .values()
            .filter(|cmd| cmd.command_type == command_type)
            .cloned()
            .collect()
    }
    
    /// Search commands by name or description
    pub async fn search_commands(&self, query: &str) -> Vec<CustomCommand> {
        let commands = self.commands.read().await;
        let query_lower = query.to_lowercase();
        
        commands
            .values()
            .filter(|cmd| {
                cmd.name.to_lowercase().contains(&query_lower) ||
                cmd.description.as_ref()
                    .map_or(false, |desc| desc.to_lowercase().contains(&query_lower))
            })
            .cloned()
            .collect()
    }
}

#[async_trait]
impl CommandManager for CommandManagerImpl {
    async fn discover_commands(&self) -> Result<Vec<CustomCommand>, CommandError> {
        if !self.config.enabled {
            return Ok(Vec::new());
        }
        
        info!("Discovering commands...");
        let discovered = self.discovery.discover_all_commands()?;
        
        // Update cache
        let mut commands = self.commands.write().await;
        commands.clear();
        
        for command in &discovered {
            commands.insert(command.id.clone(), command.clone());
        }
        
        info!("Cached {} commands", commands.len());
        Ok(discovered)
    }
    
    async fn get_command(&self, command_id: &str) -> Result<Option<CustomCommand>, CommandError> {
        let commands = self.commands.read().await;
        Ok(commands.get(command_id).cloned())
    }
    
    async fn execute_command(
        &self,
        command_id: &str,
        arguments: HashMap<String, String>,
    ) -> Result<CommandExecution, CommandError> {
        debug!("Executing command: {} with {} arguments", command_id, arguments.len());
        
        // Get the command
        let command = {
            let commands = self.commands.read().await;
            commands.get(command_id)
                .cloned()
                .ok_or_else(|| CommandError::CommandNotFound(command_id.to_string()))?
        };
        
        // Validate arguments
        self.validate_arguments(&command, &arguments)?;
        
        // Apply default values
        let final_arguments = self.apply_defaults(&command, arguments);
        
        // Substitute parameters in content
        let resolved_content = self.parser.substitute_parameters(&command.content, &final_arguments)?;
        
        debug!("Command executed successfully: {}", command_id);
        
        Ok(CommandExecution {
            command,
            arguments: final_arguments,
            resolved_content,
        })
    }
    
    async fn list_commands(&self) -> Result<Vec<CustomCommand>, CommandError> {
        let commands = self.commands.read().await;
        Ok(commands.values().cloned().collect())
    }
    
    async fn reload_commands(&self) -> Result<(), CommandError> {
        info!("Reloading commands...");
        self.discover_commands().await?;
        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::commands::CommandType;
    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
    }
    
    #[tokio::test]
    async fn test_command_manager_basic() {
        let temp_dir = TempDir::new().unwrap();
        let commands_dir = temp_dir.path().join("commands");
        fs::create_dir_all(&commands_dir).unwrap();
        
        create_test_command_file(&commands_dir, "hello", r#"# Hello Command
Say hello to someone.

Hello $NAME!
"#);
        
        let config = CommandConfig::new()
            .add_user_dir(&commands_dir);
        
        let manager = CommandManagerImpl::new(config);
        
        // Discover commands
        let commands = manager.discover_commands().await.unwrap();
        assert_eq!(commands.len(), 1);
        assert_eq!(commands[0].name, "Hello Command");
        
        // Test command count
        assert_eq!(manager.command_count().await, 1);
        
        // Test has_command
        assert!(manager.has_command("user:hello").await);
        assert!(!manager.has_command("user:nonexistent").await);
    }
    
    #[tokio::test]
    async fn test_execute_command() {
        let temp_dir = TempDir::new().unwrap();
        let commands_dir = temp_dir.path().join("commands");
        fs::create_dir_all(&commands_dir).unwrap();
        
        create_test_command_file(&commands_dir, "greet", r#"# Greet Command
Greet someone with a custom message.

Hello $NAME, welcome to $PROJECT!
"#);
        
        let config = CommandConfig::new()
            .add_user_dir(&commands_dir);
        
        let manager = CommandManagerImpl::new(config);
        manager.discover_commands().await.unwrap();
        
        // Execute command with arguments
        let mut args = HashMap::new();
        args.insert("NAME".to_string(), "Alice".to_string());
        args.insert("PROJECT".to_string(), "MyApp".to_string());
        
        let execution = manager.execute_command("user:greet", args).await.unwrap();
        
        assert_eq!(execution.resolved_content, "Hello Alice, welcome to MyApp!");
        assert_eq!(execution.arguments.get("NAME").unwrap(), "Alice");
        assert_eq!(execution.arguments.get("PROJECT").unwrap(), "MyApp");
    }
    
    #[tokio::test]
    async fn test_execute_command_missing_parameter() {
        let temp_dir = TempDir::new().unwrap();
        let commands_dir = temp_dir.path().join("commands");
        fs::create_dir_all(&commands_dir).unwrap();
        
        create_test_command_file(&commands_dir, "greet", r#"# Greet Command
Greet someone.

Hello $NAME!
"#);
        
        let config = CommandConfig::new()
            .add_user_dir(&commands_dir);
        
        let manager = CommandManagerImpl::new(config);
        manager.discover_commands().await.unwrap();
        
        // Execute command without required parameter
        let args = HashMap::new();
        let result = manager.execute_command("user:greet", args).await;
        
        assert!(result.is_err());
        assert!(matches!(result.unwrap_err(), CommandError::MissingParameter(_)));
    }
    
    #[tokio::test]
    async fn test_search_commands() {
        let temp_dir = TempDir::new().unwrap();
        let commands_dir = temp_dir.path().join("commands");
        fs::create_dir_all(&commands_dir).unwrap();
        
        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...
"#);
        
        let config = CommandConfig::new()
            .add_user_dir(&commands_dir);
        
        let manager = CommandManagerImpl::new(config);
        manager.discover_commands().await.unwrap();
        
        // Search by name
        let results = manager.search_commands("hello").await;
        assert_eq!(results.len(), 1);
        assert_eq!(results[0].name, "Hello Command");
        
        // Search by description
        let results = manager.search_commands("deploy").await;
        assert_eq!(results.len(), 1);
        assert_eq!(results[0].name, "Deploy Command");
        
        // Search with no matches
        let results = manager.search_commands("nonexistent").await;
        assert!(results.is_empty());
    }
    
    #[tokio::test]
    async fn test_get_commands_by_type() {
        let temp_dir = TempDir::new().unwrap();
        let commands_dir = temp_dir.path().join("commands");
        fs::create_dir_all(&commands_dir).unwrap();
        
        create_test_command_file(&commands_dir, "user-cmd", "# User Command\nUser command");
        
        let config = CommandConfig::new()
            .add_user_dir(&commands_dir);
        
        let manager = CommandManagerImpl::new(config);
        manager.discover_commands().await.unwrap();
        
        let user_commands = manager.get_commands_by_type(CommandType::User).await;
        assert!(!user_commands.is_empty());

        let project_commands = manager.get_commands_by_type(CommandType::Project).await;
        assert!(project_commands.is_empty());
    }
}