Skip to main content

voirs_cli/commands/
alias.rs

1//! Command alias management system.
2//!
3//! This module allows users to create shortcuts for frequently used commands,
4//! improving productivity and reducing typing.
5
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use std::fs;
9use std::path::PathBuf;
10use voirs_sdk::Result;
11
12/// Alias definition
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct Alias {
15    /// Alias name
16    pub name: String,
17    /// Command to execute
18    pub command: String,
19    /// Description of what this alias does
20    pub description: Option<String>,
21}
22
23/// Alias manager
24pub struct AliasManager {
25    /// Path to aliases file
26    aliases_path: PathBuf,
27}
28
29impl AliasManager {
30    /// Create a new alias manager
31    pub fn new() -> Result<Self> {
32        let aliases_path = Self::get_aliases_path()?;
33        Ok(Self { aliases_path })
34    }
35
36    /// Get the path to the aliases file
37    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    /// Load aliases from file
53    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    /// Save aliases to file
70    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    /// Add or update an alias
85    pub fn add_alias(
86        &self,
87        name: String,
88        command: String,
89        description: Option<String>,
90    ) -> Result<()> {
91        // Validate alias name (alphanumeric and hyphens only)
92        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        // Prevent shadowing built-in commands
102        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    /// Remove an alias
124    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    /// Get an alias by name
134    pub fn get_alias(&self, name: &str) -> Result<Option<Alias>> {
135        let aliases = self.load()?;
136        Ok(aliases.get(name).cloned())
137    }
138
139    /// List all aliases
140    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    /// Clear all aliases
148    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
166/// Check if a name is a built-in command
167fn 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
193/// Run alias command
194pub 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/// Alias subcommands
260#[derive(Debug, Clone)]
261pub enum AliasSubcommand {
262    /// Add a new alias
263    Add {
264        /// Alias name
265        name: String,
266        /// Command to execute
267        command: String,
268        /// Optional description
269        description: Option<String>,
270    },
271    /// Remove an alias
272    Remove {
273        /// Alias name to remove
274        name: String,
275    },
276    /// List all aliases
277    List,
278    /// Show details of a specific alias
279    Show {
280        /// Alias name to show
281        name: String,
282    },
283    /// Clear all aliases
284    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}