ricecoder_cli/commands/
custom_storage.rs1use crate::error::{CliError, CliResult};
5use ricecoder_commands::{CommandDefinition, CommandRegistry, ConfigManager};
6use ricecoder_storage::PathResolver;
7use std::fs;
8use std::path::{Path, PathBuf};
9
10pub struct CustomCommandsStorage {
12 global_path: PathBuf,
13 project_path: Option<PathBuf>,
14}
15
16impl CustomCommandsStorage {
17 pub fn new() -> CliResult<Self> {
19 let global_path =
20 PathResolver::resolve_global_path().map_err(|e| CliError::Internal(e.to_string()))?;
21
22 let project_path = if PathResolver::resolve_project_path().exists() {
23 Some(PathResolver::resolve_project_path())
24 } else {
25 None
26 };
27
28 Ok(Self {
29 global_path,
30 project_path,
31 })
32 }
33
34 fn commands_dir(&self, use_project: bool) -> PathBuf {
36 if use_project {
37 if let Some(project_path) = &self.project_path {
38 return project_path.join("commands");
39 }
40 }
41 self.global_path.join("commands")
42 }
43
44 pub fn load_all(&self) -> CliResult<CommandRegistry> {
46 let mut registry = CommandRegistry::new();
47
48 let global_commands_dir = self.commands_dir(false);
50 if global_commands_dir.exists() {
51 self.load_from_directory(&global_commands_dir, &mut registry)?;
52 }
53
54 if let Some(project_path) = &self.project_path {
56 let project_commands_dir = project_path.join("commands");
57 if project_commands_dir.exists() {
58 self.load_from_directory(&project_commands_dir, &mut registry)?;
59 }
60 }
61
62 Ok(registry)
63 }
64
65 fn load_from_directory(&self, dir: &Path, registry: &mut CommandRegistry) -> CliResult<()> {
67 if !dir.is_dir() {
68 return Ok(());
69 }
70
71 for entry in fs::read_dir(dir).map_err(CliError::Io)? {
72 let entry = entry.map_err(CliError::Io)?;
73 let path = entry.path();
74
75 if path.is_file() {
76 let file_name = path.file_name().unwrap().to_string_lossy();
77
78 if file_name.ends_with(".json")
80 || file_name.ends_with(".yaml")
81 || file_name.ends_with(".yml")
82 {
83 match ConfigManager::load_from_file(&path) {
84 Ok(loaded_registry) => {
85 for cmd in loaded_registry.list_all() {
87 let _ = registry.register(cmd);
89 }
90 }
91 Err(e) => {
92 eprintln!(
94 "Warning: Failed to load commands from {}: {}",
95 path.display(),
96 e
97 );
98 }
99 }
100 }
101 }
102 }
103
104 Ok(())
105 }
106
107 pub fn save_command(&self, cmd: &CommandDefinition) -> CliResult<PathBuf> {
109 let use_project = self.project_path.is_some();
111 let target_dir = self.commands_dir(use_project);
112
113 fs::create_dir_all(&target_dir).map_err(CliError::Io)?;
115
116 let file_name = format!("{}.json", cmd.id);
118 let file_path = target_dir.join(&file_name);
119
120 let config = serde_json::json!({
122 "commands": [cmd]
123 });
124
125 let json_str =
127 serde_json::to_string_pretty(&config).map_err(|e| CliError::Internal(e.to_string()))?;
128
129 fs::write(&file_path, json_str).map_err(CliError::Io)?;
131
132 Ok(file_path)
133 }
134
135 pub fn delete_command(&self, command_id: &str) -> CliResult<()> {
137 if let Some(project_path) = &self.project_path {
139 let project_commands_dir = project_path.join("commands");
140 let file_path = project_commands_dir.join(format!("{}.json", command_id));
141 if file_path.exists() {
142 fs::remove_file(&file_path).map_err(CliError::Io)?;
143 return Ok(());
144 }
145 }
146
147 let global_commands_dir = self.commands_dir(false);
149 let file_path = global_commands_dir.join(format!("{}.json", command_id));
150 if file_path.exists() {
151 fs::remove_file(&file_path).map_err(CliError::Io)?;
152 return Ok(());
153 }
154
155 Err(CliError::InvalidArgument {
156 message: format!("Command '{}' not found in storage", command_id),
157 })
158 }
159
160 pub fn global_path(&self) -> &PathBuf {
162 &self.global_path
163 }
164
165 pub fn project_path(&self) -> Option<&PathBuf> {
167 self.project_path.as_ref()
168 }
169}
170
171impl Default for CustomCommandsStorage {
172 fn default() -> Self {
173 Self::new().unwrap_or_else(|_| {
174 Self {
176 global_path: PathBuf::from("."),
177 project_path: None,
178 }
179 })
180 }
181}
182
183#[cfg(test)]
184mod tests {
185 use super::*;
186 use tempfile::TempDir;
187
188 #[test]
189 fn test_storage_creation() {
190 let storage = CustomCommandsStorage::new();
191 assert!(storage.is_ok());
192 }
193
194 #[test]
195 fn test_load_empty_storage() {
196 let temp_dir = TempDir::new().unwrap();
197 let storage = CustomCommandsStorage {
198 global_path: temp_dir.path().to_path_buf(),
199 project_path: None,
200 };
201
202 let registry = storage.load_all().unwrap();
203 assert_eq!(registry.list_all().len(), 0);
204 }
205
206 #[test]
207 fn test_save_and_load_command() {
208 let temp_dir = TempDir::new().unwrap();
209 let storage = CustomCommandsStorage {
210 global_path: temp_dir.path().to_path_buf(),
211 project_path: None,
212 };
213
214 let cmd = CommandDefinition::new("test-cmd", "Test Command", "echo hello")
216 .with_description("A test command");
217
218 let saved_path = storage.save_command(&cmd).unwrap();
220 assert!(saved_path.exists());
221
222 let registry = storage.load_all().unwrap();
224 let commands = registry.list_all();
225 assert_eq!(commands.len(), 1);
226 assert_eq!(commands[0].id, "test-cmd");
227 }
228
229 #[test]
230 fn test_delete_command() {
231 let temp_dir = TempDir::new().unwrap();
232 let storage = CustomCommandsStorage {
233 global_path: temp_dir.path().to_path_buf(),
234 project_path: None,
235 };
236
237 let cmd = CommandDefinition::new("test-cmd", "Test Command", "echo hello");
239 storage.save_command(&cmd).unwrap();
240
241 let registry = storage.load_all().unwrap();
243 assert_eq!(registry.list_all().len(), 1);
244
245 storage.delete_command("test-cmd").unwrap();
247
248 let registry = storage.load_all().unwrap();
250 assert_eq!(registry.list_all().len(), 0);
251 }
252
253 #[test]
254 fn test_delete_nonexistent_command() {
255 let temp_dir = TempDir::new().unwrap();
256 let storage = CustomCommandsStorage {
257 global_path: temp_dir.path().to_path_buf(),
258 project_path: None,
259 };
260
261 let result = storage.delete_command("nonexistent");
262 assert!(result.is_err());
263 }
264}