coderlib 0.1.0

A Rust library for AI-powered code assistance and agentic system
Documentation
//! Custom Commands System
//!
//! This module provides support for user-defined command templates with parameter
//! substitution, supporting both user and project-level commands with markdown file parsing.
//!
//! ## Features
//!
//! - **Markdown Command Files**: Commands defined in `.md` files with frontmatter
//! - **Parameter Substitution**: `$PARAMETER` syntax for dynamic content
//! - **User & Project Commands**: Support for both global and project-specific commands
//! - **Auto-Discovery**: Automatic scanning of command directories
//! - **Template Execution**: Integration with CoderLib's AI processing pipeline

use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::{Path, PathBuf};


pub mod parser;
pub mod discovery;
pub mod manager;

pub use parser::CommandParser;
pub use discovery::CommandDiscovery;
pub use manager::CommandManagerImpl;

/// Custom command definition
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CustomCommand {
    /// Unique command identifier (e.g., "user:hello" or "project:deploy")
    pub id: String,
    
    /// Human-readable command name
    pub name: String,
    
    /// Optional command description
    pub description: Option<String>,
    
    /// Command content template with parameters
    pub content: String,
    
    /// List of parameters that can be substituted
    pub parameters: Vec<CommandParameter>,
    
    /// Source file path
    pub source_file: PathBuf,
    
    /// Command type (user or project)
    pub command_type: CommandType,
}

/// Command parameter definition
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CommandParameter {
    /// Parameter name (without $ prefix)
    pub name: String,
    
    /// Optional parameter description
    pub description: Option<String>,
    
    /// Whether this parameter is required
    pub required: bool,
    
    /// Default value if parameter is optional
    pub default_value: Option<String>,
}

/// Type of custom command
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum CommandType {
    /// User-level command (global)
    User,
    
    /// Project-level command (local to current project)
    Project,
}

/// Result of command execution with resolved content
#[derive(Debug, Clone)]
pub struct CommandExecution {
    /// The original command
    pub command: CustomCommand,
    
    /// Arguments provided for execution
    pub arguments: HashMap<String, String>,
    
    /// Content with parameters substituted
    pub resolved_content: String,
}

/// Command-related errors
#[derive(Debug, thiserror::Error)]
pub enum CommandError {
    #[error("File error: {0}")]
    FileError(String),
    
    #[error("Parse error: {0}")]
    ParseError(String),
    
    #[error("Missing required parameter: {0}")]
    MissingParameter(String),
    
    #[error("Command not found: {0}")]
    CommandNotFound(String),
    
    #[error("Invalid parameter value: {0}")]
    InvalidParameter(String),
    
    #[error("IO error: {0}")]
    IoError(#[from] std::io::Error),
    
    #[error("Regex error: {0}")]
    RegexError(#[from] regex::Error),
}

/// Trait for managing custom commands
#[async_trait]
pub trait CommandManager: Send + Sync {
    /// Discover all available commands
    async fn discover_commands(&self) -> Result<Vec<CustomCommand>, CommandError>;
    
    /// Get a specific command by ID
    async fn get_command(&self, command_id: &str) -> Result<Option<CustomCommand>, CommandError>;
    
    /// Execute a command with given arguments
    async fn execute_command(
        &self,
        command_id: &str,
        arguments: HashMap<String, String>,
    ) -> Result<CommandExecution, CommandError>;
    
    /// List all available commands
    async fn list_commands(&self) -> Result<Vec<CustomCommand>, CommandError>;
    
    /// Reload commands from disk
    async fn reload_commands(&self) -> Result<(), CommandError>;
}

/// Configuration for command system
#[derive(Debug, Clone)]
pub struct CommandConfig {
    /// Enable custom commands
    pub enabled: bool,
    
    /// User command directories
    pub user_command_dirs: Vec<PathBuf>,
    
    /// Project command directory name (relative to project root)
    pub project_command_dir: String,
    
    /// Maximum recursion depth for command discovery
    pub max_depth: usize,
    
    /// Whether to auto-reload commands when files change
    pub auto_reload: bool,
}

impl Default for CommandConfig {
    fn default() -> Self {
        let mut user_dirs = Vec::new();
        
        // Add standard user config directories
        if let Some(config_dir) = dirs::config_dir() {
            user_dirs.push(config_dir.join("coderlib").join("commands"));
        }
        
        if let Some(home_dir) = dirs::home_dir() {
            user_dirs.push(home_dir.join(".coderlib").join("commands"));
        }
        
        Self {
            enabled: true,
            user_command_dirs: user_dirs,
            project_command_dir: ".coderlib/commands".to_string(),
            max_depth: 3,
            auto_reload: true,
        }
    }
}

impl CommandConfig {
    /// Create a new command configuration
    pub fn new() -> Self {
        Self::default()
    }
    
    /// Add a user command directory
    pub fn add_user_dir<P: Into<PathBuf>>(mut self, dir: P) -> Self {
        self.user_command_dirs.push(dir.into());
        self
    }
    
    /// Set the project command directory
    pub fn project_dir<S: Into<String>>(mut self, dir: S) -> Self {
        self.project_command_dir = dir.into();
        self
    }
    
    /// Set maximum discovery depth
    pub fn max_depth(mut self, depth: usize) -> Self {
        self.max_depth = depth;
        self
    }
    
    /// Enable or disable auto-reload
    pub fn auto_reload(mut self, enabled: bool) -> Self {
        self.auto_reload = enabled;
        self
    }
}

/// Utility functions for command system
pub mod utils {
    use super::*;
    
    /// Get the default user command directory
    pub fn default_user_command_dir() -> Option<PathBuf> {
        dirs::config_dir().map(|dir| dir.join("coderlib").join("commands"))
    }
    
    /// Get the project command directory for the current working directory
    pub fn project_command_dir() -> Result<PathBuf, CommandError> {
        let current_dir = std::env::current_dir()
            .map_err(|e| CommandError::FileError(e.to_string()))?;
        Ok(current_dir.join(".coderlib").join("commands"))
    }
    
    /// Check if a path is a valid command file
    pub fn is_command_file(path: &Path) -> bool {
        path.is_file() && 
        path.extension().map_or(false, |ext| ext == "md") &&
        !path.file_name().map_or(true, |name| name.to_string_lossy().starts_with('.'))
    }
    
    /// Generate a command ID from file path and type
    pub fn generate_command_id(file_path: &Path, command_type: CommandType) -> Result<String, CommandError> {
        let file_name = file_path.file_stem()
            .and_then(|s| s.to_str())
            .ok_or_else(|| CommandError::ParseError("Invalid file name".to_string()))?;
        
        let prefix = match command_type {
            CommandType::User => "user",
            CommandType::Project => "project",
        };
        
        Ok(format!("{}:{}", prefix, file_name))
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use tempfile::TempDir;
    
    #[test]
    fn test_command_config_default() {
        let config = CommandConfig::default();
        assert!(config.enabled);
        assert!(!config.user_command_dirs.is_empty());
        assert_eq!(config.project_command_dir, ".coderlib/commands");
        assert_eq!(config.max_depth, 3);
        assert!(config.auto_reload);
    }
    
    #[test]
    fn test_command_config_builder() {
        let config = CommandConfig::new()
            .add_user_dir("/custom/commands")
            .project_dir("custom-commands")
            .max_depth(5)
            .auto_reload(false);
        
        assert!(config.user_command_dirs.iter().any(|p| p == &PathBuf::from("/custom/commands")));
        assert_eq!(config.project_command_dir, "custom-commands");
        assert_eq!(config.max_depth, 5);
        assert!(!config.auto_reload);
    }
    
    #[test]
    fn test_utils_is_command_file() {
        use tempfile::TempDir;
        use std::fs::File;

        let temp_dir = TempDir::new().unwrap();

        // Create test files
        let md_file = temp_dir.path().join("test.md");
        let txt_file = temp_dir.path().join("test.txt");
        let hidden_file = temp_dir.path().join(".hidden.md");

        File::create(&md_file).unwrap();
        File::create(&txt_file).unwrap();
        File::create(&hidden_file).unwrap();

        assert!(utils::is_command_file(&md_file));
        assert!(!utils::is_command_file(&txt_file));
        assert!(!utils::is_command_file(&hidden_file));
    }
    
    #[test]
    fn test_utils_generate_command_id() {
        let path = Path::new("hello-world.md");
        let id = utils::generate_command_id(path, CommandType::User).unwrap();
        assert_eq!(id, "user:hello-world");
        
        let id = utils::generate_command_id(path, CommandType::Project).unwrap();
        assert_eq!(id, "project:hello-world");
    }
}