use crate::error::{CliError, CliResult};
use ricecoder_commands::{CommandDefinition, CommandRegistry, ConfigManager};
use ricecoder_storage::PathResolver;
use std::fs;
use std::path::{Path, PathBuf};
pub struct CustomCommandsStorage {
global_path: PathBuf,
project_path: Option<PathBuf>,
}
impl CustomCommandsStorage {
pub fn new() -> CliResult<Self> {
let global_path =
PathResolver::resolve_global_path().map_err(|e| CliError::Internal(e.to_string()))?;
let project_path = if PathResolver::resolve_project_path().exists() {
Some(PathResolver::resolve_project_path())
} else {
None
};
Ok(Self {
global_path,
project_path,
})
}
fn commands_dir(&self, use_project: bool) -> PathBuf {
if use_project {
if let Some(project_path) = &self.project_path {
return project_path.join("commands");
}
}
self.global_path.join("commands")
}
pub fn load_all(&self) -> CliResult<CommandRegistry> {
let mut registry = CommandRegistry::new();
let global_commands_dir = self.commands_dir(false);
if global_commands_dir.exists() {
self.load_from_directory(&global_commands_dir, &mut registry)?;
}
if let Some(project_path) = &self.project_path {
let project_commands_dir = project_path.join("commands");
if project_commands_dir.exists() {
self.load_from_directory(&project_commands_dir, &mut registry)?;
}
}
Ok(registry)
}
fn load_from_directory(&self, dir: &Path, registry: &mut CommandRegistry) -> CliResult<()> {
if !dir.is_dir() {
return Ok(());
}
for entry in fs::read_dir(dir).map_err(CliError::Io)? {
let entry = entry.map_err(CliError::Io)?;
let path = entry.path();
if path.is_file() {
let file_name = path.file_name().unwrap().to_string_lossy();
if file_name.ends_with(".json")
|| file_name.ends_with(".yaml")
|| file_name.ends_with(".yml")
{
match ConfigManager::load_from_file(&path) {
Ok(loaded_registry) => {
for cmd in loaded_registry.list_all() {
let _ = registry.register(cmd);
}
}
Err(e) => {
eprintln!(
"Warning: Failed to load commands from {}: {}",
path.display(),
e
);
}
}
}
}
}
Ok(())
}
pub fn save_command(&self, cmd: &CommandDefinition) -> CliResult<PathBuf> {
let use_project = self.project_path.is_some();
let target_dir = self.commands_dir(use_project);
fs::create_dir_all(&target_dir).map_err(CliError::Io)?;
let file_name = format!("{}.json", cmd.id);
let file_path = target_dir.join(&file_name);
let config = serde_json::json!({
"commands": [cmd]
});
let json_str =
serde_json::to_string_pretty(&config).map_err(|e| CliError::Internal(e.to_string()))?;
fs::write(&file_path, json_str).map_err(CliError::Io)?;
Ok(file_path)
}
pub fn delete_command(&self, command_id: &str) -> CliResult<()> {
if let Some(project_path) = &self.project_path {
let project_commands_dir = project_path.join("commands");
let file_path = project_commands_dir.join(format!("{}.json", command_id));
if file_path.exists() {
fs::remove_file(&file_path).map_err(CliError::Io)?;
return Ok(());
}
}
let global_commands_dir = self.commands_dir(false);
let file_path = global_commands_dir.join(format!("{}.json", command_id));
if file_path.exists() {
fs::remove_file(&file_path).map_err(CliError::Io)?;
return Ok(());
}
Err(CliError::InvalidArgument {
message: format!("Command '{}' not found in storage", command_id),
})
}
pub fn global_path(&self) -> &PathBuf {
&self.global_path
}
pub fn project_path(&self) -> Option<&PathBuf> {
self.project_path.as_ref()
}
}
impl Default for CustomCommandsStorage {
fn default() -> Self {
Self::new().unwrap_or_else(|_| {
Self {
global_path: PathBuf::from("."),
project_path: None,
}
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn test_storage_creation() {
let storage = CustomCommandsStorage::new();
assert!(storage.is_ok());
}
#[test]
fn test_load_empty_storage() {
let temp_dir = TempDir::new().unwrap();
let storage = CustomCommandsStorage {
global_path: temp_dir.path().to_path_buf(),
project_path: None,
};
let registry = storage.load_all().unwrap();
assert_eq!(registry.list_all().len(), 0);
}
#[test]
fn test_save_and_load_command() {
let temp_dir = TempDir::new().unwrap();
let storage = CustomCommandsStorage {
global_path: temp_dir.path().to_path_buf(),
project_path: None,
};
let cmd = CommandDefinition::new("test-cmd", "Test Command", "echo hello")
.with_description("A test command");
let saved_path = storage.save_command(&cmd).unwrap();
assert!(saved_path.exists());
let registry = storage.load_all().unwrap();
let commands = registry.list_all();
assert_eq!(commands.len(), 1);
assert_eq!(commands[0].id, "test-cmd");
}
#[test]
fn test_delete_command() {
let temp_dir = TempDir::new().unwrap();
let storage = CustomCommandsStorage {
global_path: temp_dir.path().to_path_buf(),
project_path: None,
};
let cmd = CommandDefinition::new("test-cmd", "Test Command", "echo hello");
storage.save_command(&cmd).unwrap();
let registry = storage.load_all().unwrap();
assert_eq!(registry.list_all().len(), 1);
storage.delete_command("test-cmd").unwrap();
let registry = storage.load_all().unwrap();
assert_eq!(registry.list_all().len(), 0);
}
#[test]
fn test_delete_nonexistent_command() {
let temp_dir = TempDir::new().unwrap();
let storage = CustomCommandsStorage {
global_path: temp_dir.path().to_path_buf(),
project_path: None,
};
let result = storage.delete_command("nonexistent");
assert!(result.is_err());
}
}