pocket_cli/cards/
mod.rs

1//! Card architecture for Pocket CLI
2//!
3//! This module provides a card system for extending Pocket CLI functionality.
4//! Cards can add new commands, modify existing behavior, or provide additional features.
5
6pub mod backup;
7
8use std::collections::HashMap;
9use std::path::Path;
10use anyhow::{Result, Context};
11use serde::{Serialize, Deserialize};
12
13/// Trait that all cards must implement
14pub trait Card: Send + Sync {
15    /// Returns the name of the card
16    fn name(&self) -> &str;
17    
18    /// Returns the version of the card
19    fn version(&self) -> &str;
20    
21    /// Returns a description of the card
22    fn description(&self) -> &str;
23    
24    /// Initializes the card with the given configuration
25    fn initialize(&mut self, config: &CardConfig) -> Result<()>;
26    
27    /// Executes a command provided by the card
28    fn execute(&self, command: &str, args: &[String]) -> Result<()>;
29    
30    /// Returns a list of commands provided by the card
31    fn commands(&self) -> Vec<CardCommand>;
32    
33    /// Cleans up any resources used by the card
34    fn cleanup(&mut self) -> Result<()>;
35}
36
37/// Configuration for a card
38#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct CardConfig {
40    /// The name of the card
41    pub name: String,
42    
43    /// Whether the card is enabled
44    pub enabled: bool,
45    
46    /// Additional configuration options for the card
47    #[serde(default)]
48    pub options: HashMap<String, serde_json::Value>,
49}
50
51/// A command provided by a card
52#[derive(Debug, Clone)]
53pub struct CardCommand {
54    /// The name of the command
55    pub name: String,
56    
57    /// A description of the command
58    pub description: String,
59    
60    /// The usage pattern for the command
61    pub usage: String,
62}
63
64/// Manager for loading and running cards
65pub struct CardManager {
66    /// The loaded cards
67    cards: Vec<Box<dyn Card>>,
68    
69    /// Configuration for each card
70    configs: HashMap<String, CardConfig>,
71    
72    /// The directory where cards are stored
73    card_dir: std::path::PathBuf,
74}
75
76impl CardManager {
77    /// Creates a new card manager with the given card directory
78    pub fn new(card_dir: impl AsRef<Path>) -> Self {
79        Self {
80            cards: Vec::new(),
81            configs: HashMap::new(),
82            card_dir: card_dir.as_ref().to_path_buf(),
83        }
84    }
85    
86    /// Loads all cards from the card directory
87    pub fn load_cards(&mut self) -> Result<()> {
88        // Ensure the card directory exists
89        if !self.card_dir.exists() {
90            std::fs::create_dir_all(&self.card_dir)
91                .context("Failed to create card directory")?;
92            return Ok(());
93        }
94        
95        // Load card configurations
96        self.load_configs()?;
97        
98        // Register built-in cards
99        self.register_builtin_cards()?;
100        
101        // Load dynamic cards from wallet directory
102        let wallet_dir = self.card_dir.parent().unwrap_or(&self.card_dir).join("wallet");
103        if wallet_dir.exists() {
104            for entry in std::fs::read_dir(wallet_dir)? {
105                let entry = entry?;
106                let path = entry.path();
107                
108                if path.is_dir() {
109                    let card_name = path.file_name()
110                        .and_then(|name| name.to_str())
111                        .ok_or_else(|| anyhow::anyhow!("Invalid card directory name"))?;
112                    
113                    // Check if this card is already registered in the configuration
114                    if !self.configs.contains_key(card_name) {
115                        // Register the card configuration
116                        self.register_card_config(card_name, "local")?;
117                    }
118                    
119                    // Check if the card has been built
120                    let target_dir = path.join("target").join("release");
121                    let lib_name = format!("libpocket_card_{}.dylib", card_name.replace("-", "_"));
122                    let lib_path = target_dir.join(lib_name);
123                    
124                    if lib_path.exists() {
125                        // TODO: In a real implementation, we would load the dynamic library here
126                        // For now, just print a message
127                        println!("Found built card at {}", lib_path.display());
128                        
129                        // Check if the card is already loaded
130                        if !self.cards.iter().any(|p| p.name() == card_name) {
131                            // For now, we'll just add a placeholder card
132                            // In a real implementation, we would load the dynamic library
133                            // and create a proper card instance
134                            
135                            // For testing purposes, let's create a placeholder card
136                            let card = Box::new(PlaceholderCard::new(card_name.to_string()));
137                            self.register_card(card)?;
138                        }
139                    }
140                }
141            }
142        }
143        
144        Ok(())
145    }
146    
147    /// Loads card configurations from the card directory
148    fn load_configs(&mut self) -> Result<()> {
149        let config_path = self.card_dir.join("cards.json");
150        
151        if !config_path.exists() {
152            // Create a default configuration if none exists
153            let default_configs: HashMap<String, CardConfig> = HashMap::new();
154            let json = serde_json::to_string_pretty(&default_configs)?;
155            std::fs::write(&config_path, json)?;
156            return Ok(());
157        }
158        
159        // Read and parse the configuration file
160        let json = std::fs::read_to_string(&config_path)?;
161        self.configs = serde_json::from_str(&json)?;
162        
163        Ok(())
164    }
165    
166    /// Saves card configurations to the card directory
167    pub fn save_configs(&self) -> Result<()> {
168        let config_path = self.card_dir.join("cards.json");
169        let json = serde_json::to_string_pretty(&self.configs)?;
170        std::fs::write(&config_path, json)?;
171        Ok(())
172    }
173    
174    /// Registers built-in cards
175    fn register_builtin_cards(&mut self) -> Result<()> {
176        // Register the backup card
177        use crate::cards::backup::BackupCard;
178        
179        // Create the data directory path (parent of card_dir)
180        let data_dir = self.card_dir.parent().unwrap_or(&self.card_dir).to_path_buf();
181        
182        // Register the backup card
183        self.register_card(Box::new(BackupCard::new(data_dir)))?;
184        
185        Ok(())
186    }
187    
188    /// Registers a card with the manager
189    pub fn register_card(&mut self, mut card: Box<dyn Card>) -> Result<()> {
190        let name = card.name().to_string();
191        
192        // Get or create a configuration for the card
193        let config = self.configs.entry(name.clone()).or_insert_with(|| {
194            CardConfig {
195                name: name.clone(),
196                enabled: true,
197                options: HashMap::new(),
198            }
199        });
200        
201        // Initialize the card with its configuration
202        card.initialize(config)?;
203        
204        // Add the card to the list
205        self.cards.push(card);
206        
207        Ok(())
208    }
209    
210    /// Returns a list of all loaded cards
211    pub fn list_cards(&self) -> Vec<(&str, &str, bool)> {
212        self.cards.iter()
213            .map(|p| {
214                let name = p.name();
215                let version = p.version();
216                let enabled = self.configs.get(name)
217                    .map(|c| c.enabled)
218                    .unwrap_or(false);
219                (name, version, enabled)
220            })
221            .collect()
222    }
223    
224    /// Enables a card by name
225    pub fn enable_card(&mut self, name: &str) -> Result<()> {
226        if let Some(config) = self.configs.get_mut(name) {
227            config.enabled = true;
228            self.save_configs()?;
229            Ok(())
230        } else {
231            anyhow::bail!("Card '{}' not found", name)
232        }
233    }
234    
235    /// Disables a card by name
236    pub fn disable_card(&mut self, name: &str) -> Result<()> {
237        if let Some(config) = self.configs.get_mut(name) {
238            config.enabled = false;
239            self.save_configs()?;
240            Ok(())
241        } else {
242            anyhow::bail!("Card '{}' not found", name)
243        }
244    }
245    
246    /// Executes a command on a card
247    pub fn execute_command(&self, card_name: &str, command: &str, args: &[String]) -> Result<()> {
248        // Find the card
249        let card = self.cards.iter().find(|p| p.name() == card_name);
250        
251        if let Some(card) = card {
252            // Check if the card is enabled
253            let enabled = self.configs.get(card_name)
254                .map(|c| c.enabled)
255                .unwrap_or(false);
256            
257            if !enabled {
258                return Err(anyhow::anyhow!("Card '{}' is disabled", card_name));
259            }
260            
261            // Execute the command
262            card.execute(command, args)
263        } else {
264            // Check if the card exists in the configuration but is not loaded
265            if self.configs.contains_key(card_name) {
266                // For now, just return an error indicating the card is not loaded
267                // In a real implementation, we would attempt to load the card dynamically
268                return Err(anyhow::anyhow!("Card '{}' is registered but not loaded. Try rebuilding the card with: pocket cards build {}", card_name, card_name));
269            }
270            
271            Err(anyhow::anyhow!("Card '{}' not found", card_name))
272        }
273    }
274    
275    /// Returns a list of all commands provided by all enabled cards
276    pub fn list_commands(&self) -> Vec<(String, Vec<CardCommand>)> {
277        self.cards.iter()
278            .filter(|p| {
279                self.configs.get(p.name())
280                    .map(|c| c.enabled)
281                    .unwrap_or(false)
282            })
283            .map(|p| (p.name().to_string(), p.commands()))
284            .collect()
285    }
286    
287    /// Cleans up all cards
288    pub fn cleanup(&mut self) -> Result<()> {
289        for card in &mut self.cards {
290            card.cleanup()?;
291        }
292        Ok(())
293    }
294    
295    /// Checks if a card with the given name exists
296    pub fn card_exists(&self, name: &str) -> bool {
297        // Check if the card is in the loaded cards
298        if self.cards.iter().any(|p| p.name() == name) {
299            return true;
300        }
301        
302        // Check if the card is in the configuration
303        self.configs.contains_key(name)
304    }
305    
306    /// Registers a card configuration without loading the card
307    pub fn register_card_config(&mut self, name: &str, url: &str) -> Result<()> {
308        // Create a new configuration for the card
309        let config = CardConfig {
310            name: name.to_string(),
311            enabled: true,
312            options: {
313                let mut options = HashMap::new();
314                options.insert("url".to_string(), serde_json::Value::String(url.to_string()));
315                options
316            },
317        };
318        
319        // Add the configuration
320        self.configs.insert(name.to_string(), config);
321        
322        // Save the configurations
323        self.save_configs()?;
324        
325        Ok(())
326    }
327    
328    /// Removes a card configuration
329    pub fn remove_card_config(&mut self, name: &str) -> Result<()> {
330        // Remove the card from the configuration
331        self.configs.remove(name);
332        
333        // Save the configurations
334        self.save_configs()?;
335        
336        Ok(())
337    }
338}
339
340impl Drop for CardManager {
341    fn drop(&mut self) {
342        // Attempt to clean up cards when the manager is dropped
343        let _ = self.cleanup();
344    }
345}
346
347// A placeholder card for testing
348struct PlaceholderCard {
349    name: String,
350    version: String,
351    description: String,
352}
353
354impl PlaceholderCard {
355    fn new(name: String) -> Self {
356        Self {
357            name,
358            version: "0.1.0".to_string(),
359            description: "A placeholder card".to_string(),
360        }
361    }
362}
363
364impl Card for PlaceholderCard {
365    fn name(&self) -> &str {
366        &self.name
367    }
368    
369    fn version(&self) -> &str {
370        &self.version
371    }
372    
373    fn description(&self) -> &str {
374        &self.description
375    }
376    
377    fn initialize(&mut self, _config: &CardConfig) -> Result<()> {
378        Ok(())
379    }
380    
381    fn execute(&self, command: &str, args: &[String]) -> Result<()> {
382        println!("Executing command {} with args {:?} on placeholder card {}", command, args, self.name);
383        Ok(())
384    }
385    
386    fn commands(&self) -> Vec<CardCommand> {
387        vec![
388            CardCommand {
389                name: "hello".to_string(),
390                description: "A simple hello command".to_string(),
391                usage: format!("pocket cards execute {} hello [args...]", self.name),
392            },
393        ]
394    }
395    
396    fn cleanup(&mut self) -> Result<()> {
397        Ok(())
398    }
399}