voirs_cli/commands/
alias.rs1use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use std::fs;
9use std::path::PathBuf;
10use voirs_sdk::Result;
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct Alias {
15 pub name: String,
17 pub command: String,
19 pub description: Option<String>,
21}
22
23pub struct AliasManager {
25 aliases_path: PathBuf,
27}
28
29impl AliasManager {
30 pub fn new() -> Result<Self> {
32 let aliases_path = Self::get_aliases_path()?;
33 Ok(Self { aliases_path })
34 }
35
36 fn get_aliases_path() -> Result<PathBuf> {
38 let config_dir = dirs::config_dir().ok_or_else(|| {
39 voirs_sdk::VoirsError::config_error("Could not find config directory")
40 })?;
41
42 let voirs_dir = config_dir.join("voirs");
43 fs::create_dir_all(&voirs_dir).map_err(|e| voirs_sdk::VoirsError::IoError {
44 path: voirs_dir.clone(),
45 operation: voirs_sdk::error::IoOperation::Write,
46 source: e,
47 })?;
48
49 Ok(voirs_dir.join("aliases.json"))
50 }
51
52 pub fn load(&self) -> Result<HashMap<String, Alias>> {
54 if !self.aliases_path.exists() {
55 return Ok(HashMap::new());
56 }
57
58 let content =
59 fs::read_to_string(&self.aliases_path).map_err(|e| voirs_sdk::VoirsError::IoError {
60 path: self.aliases_path.clone(),
61 operation: voirs_sdk::error::IoOperation::Read,
62 source: e,
63 })?;
64
65 let aliases: HashMap<String, Alias> = serde_json::from_str(&content).unwrap_or_default();
66 Ok(aliases)
67 }
68
69 fn save(&self, aliases: &HashMap<String, Alias>) -> Result<()> {
71 let content = serde_json::to_string_pretty(aliases).map_err(|e| {
72 voirs_sdk::VoirsError::config_error(format!("Failed to serialize aliases: {}", e))
73 })?;
74
75 fs::write(&self.aliases_path, content).map_err(|e| voirs_sdk::VoirsError::IoError {
76 path: self.aliases_path.clone(),
77 operation: voirs_sdk::error::IoOperation::Write,
78 source: e,
79 })?;
80
81 Ok(())
82 }
83
84 pub fn add_alias(
86 &self,
87 name: String,
88 command: String,
89 description: Option<String>,
90 ) -> Result<()> {
91 if !name
93 .chars()
94 .all(|c| c.is_alphanumeric() || c == '-' || c == '_')
95 {
96 return Err(voirs_sdk::VoirsError::config_error(
97 "Alias name must contain only alphanumeric characters, hyphens, and underscores",
98 ));
99 }
100
101 if is_builtin_command(&name) {
103 return Err(voirs_sdk::VoirsError::config_error(format!(
104 "Cannot create alias '{}': this is a built-in command",
105 name
106 )));
107 }
108
109 let mut aliases = self.load()?;
110 aliases.insert(
111 name.clone(),
112 Alias {
113 name,
114 command,
115 description,
116 },
117 );
118 self.save(&aliases)?;
119
120 Ok(())
121 }
122
123 pub fn remove_alias(&self, name: &str) -> Result<bool> {
125 let mut aliases = self.load()?;
126 let removed = aliases.remove(name).is_some();
127 if removed {
128 self.save(&aliases)?;
129 }
130 Ok(removed)
131 }
132
133 pub fn get_alias(&self, name: &str) -> Result<Option<Alias>> {
135 let aliases = self.load()?;
136 Ok(aliases.get(name).cloned())
137 }
138
139 pub fn list_aliases(&self) -> Result<Vec<Alias>> {
141 let aliases = self.load()?;
142 let mut alias_list: Vec<Alias> = aliases.into_values().collect();
143 alias_list.sort_by(|a, b| a.name.cmp(&b.name));
144 Ok(alias_list)
145 }
146
147 pub fn clear(&self) -> Result<()> {
149 if self.aliases_path.exists() {
150 fs::remove_file(&self.aliases_path).map_err(|e| voirs_sdk::VoirsError::IoError {
151 path: self.aliases_path.clone(),
152 operation: voirs_sdk::error::IoOperation::Delete,
153 source: e,
154 })?;
155 }
156 Ok(())
157 }
158}
159
160impl Default for AliasManager {
161 fn default() -> Self {
162 Self::new().expect("Failed to create alias manager")
163 }
164}
165
166fn is_builtin_command(name: &str) -> bool {
168 matches!(
169 name,
170 "synthesize"
171 | "synthesize-file"
172 | "list-voices"
173 | "voice-info"
174 | "download-voice"
175 | "preview-voice"
176 | "compare-voices"
177 | "test"
178 | "interactive"
179 | "batch"
180 | "server"
181 | "train"
182 | "config"
183 | "capabilities"
184 | "history"
185 | "alias"
186 | "export"
187 | "import"
188 | "help"
189 | "version"
190 )
191}
192
193pub async fn run_alias(subcommand: AliasSubcommand) -> Result<()> {
195 let manager = AliasManager::new()?;
196
197 match subcommand {
198 AliasSubcommand::Add {
199 name,
200 command,
201 description,
202 } => {
203 manager.add_alias(name.clone(), command.clone(), description)?;
204 println!("✅ Alias '{}' created", name);
205 println!(" Command: voirs {}", command);
206 Ok(())
207 }
208
209 AliasSubcommand::Remove { name } => {
210 if manager.remove_alias(&name)? {
211 println!("✅ Alias '{}' removed", name);
212 } else {
213 println!("⚠️ Alias '{}' not found", name);
214 }
215 Ok(())
216 }
217
218 AliasSubcommand::List => {
219 let aliases = manager.list_aliases()?;
220 if aliases.is_empty() {
221 println!("No aliases defined yet.");
222 println!();
223 println!("Create an alias with:");
224 println!(" voirs alias add <name> <command>");
225 } else {
226 println!("📋 Defined aliases:");
227 println!();
228 for alias in aliases {
229 println!(" {} → voirs {}", alias.name, alias.command);
230 if let Some(desc) = alias.description {
231 println!(" {}", desc);
232 }
233 }
234 }
235 Ok(())
236 }
237
238 AliasSubcommand::Show { name } => {
239 if let Some(alias) = manager.get_alias(&name)? {
240 println!("Alias: {}", alias.name);
241 println!("Command: voirs {}", alias.command);
242 if let Some(desc) = alias.description {
243 println!("Description: {}", desc);
244 }
245 } else {
246 println!("❌ Alias '{}' not found", name);
247 }
248 Ok(())
249 }
250
251 AliasSubcommand::Clear => {
252 manager.clear()?;
253 println!("✅ All aliases cleared");
254 Ok(())
255 }
256 }
257}
258
259#[derive(Debug, Clone)]
261pub enum AliasSubcommand {
262 Add {
264 name: String,
266 command: String,
268 description: Option<String>,
270 },
271 Remove {
273 name: String,
275 },
276 List,
278 Show {
280 name: String,
282 },
283 Clear,
285}
286
287#[cfg(test)]
288mod tests {
289 use super::*;
290
291 #[test]
292 fn test_alias_creation() {
293 let alias = Alias {
294 name: "quick".to_string(),
295 command: "synthesize --quality medium".to_string(),
296 description: Some("Quick synthesis".to_string()),
297 };
298
299 assert_eq!(alias.name, "quick");
300 assert_eq!(alias.command, "synthesize --quality medium");
301 }
302
303 #[test]
304 fn test_builtin_command_check() {
305 assert!(is_builtin_command("synthesize"));
306 assert!(is_builtin_command("list-voices"));
307 assert!(!is_builtin_command("my-custom-alias"));
308 }
309
310 #[test]
311 fn test_alias_serialization() {
312 let mut aliases = HashMap::new();
313 aliases.insert(
314 "quick".to_string(),
315 Alias {
316 name: "quick".to_string(),
317 command: "synthesize --quality medium".to_string(),
318 description: None,
319 },
320 );
321
322 let json = serde_json::to_string(&aliases).unwrap();
323 let deserialized: HashMap<String, Alias> = serde_json::from_str(&json).unwrap();
324
325 assert_eq!(deserialized.len(), 1);
326 assert!(deserialized.contains_key("quick"));
327 }
328}