use std::path::PathBuf;
use tracing::{debug, info, warn};
use walkdir::WalkDir;
use super::{CustomCommand, CommandError, CommandConfig, CommandParser, utils};
pub struct CommandDiscovery {
parser: CommandParser,
config: CommandConfig,
}
impl CommandDiscovery {
pub fn new(config: CommandConfig) -> Self {
Self {
parser: CommandParser::new(),
config,
}
}
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();
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);
}
}
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)
}
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)
}
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())
}
}
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)
}
pub fn get_user_command_directories(&self) -> &[PathBuf] {
&self.config.user_command_dirs
}
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))
}
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
}
pub fn get_discovery_stats(&self) -> DiscoveryStats {
let mut stats = DiscoveryStats::default();
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;
}
}
}
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
}
}
#[derive(Debug, Default)]
pub struct DiscoveryStats {
pub user_directories: usize,
pub user_directories_with_commands: usize,
pub project_directory_exists: bool,
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_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...
"#);
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);
assert!(!discovery.has_commands_in_directory(&commands_dir));
create_test_command_file(&commands_dir, "test", "# Test\nTest command");
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());
}
}