1pub mod backup;
7pub mod snippet;
8pub mod core;
9pub mod blend;
10
11use std::collections::HashMap;
12use std::path::{Path, PathBuf};
13use std::fs::{self, File};
14use std::io::{Read, Write};
15use serde::{Deserialize, Serialize};
16use anyhow::{Result, Context, anyhow, bail};
17use dirs;
18
19pub trait Card: Send + Sync {
21 fn name(&self) -> &str;
23
24 fn version(&self) -> &str;
26
27 fn description(&self) -> &str;
29
30 fn initialize(&mut self, config: &CardConfig) -> Result<()>;
32
33 fn execute(&self, command: &str, args: &[String]) -> Result<()>;
35
36 fn commands(&self) -> Vec<CardCommand>;
38
39 fn cleanup(&mut self) -> Result<()>;
41}
42
43#[derive(Debug, Clone, Serialize, Deserialize)]
45pub struct CardConfig {
46 pub name: String,
48
49 pub enabled: bool,
51
52 #[serde(default)]
54 pub options: HashMap<String, serde_json::Value>,
55}
56
57#[derive(Debug, Clone)]
59pub struct CardCommand {
60 pub name: String,
62
63 pub description: String,
65
66 pub usage: String,
68}
69
70pub struct CardManager {
72 cards: HashMap<String, Box<dyn Card>>,
74
75 configs: HashMap<String, CardConfig>,
77
78 card_dir: std::path::PathBuf,
80
81 builtin_card_names: Vec<String>,
83}
84
85impl CardManager {
86 pub fn new(card_dir: impl AsRef<Path>) -> Self {
88 Self {
89 cards: HashMap::new(),
90 configs: HashMap::new(),
91 card_dir: card_dir.as_ref().to_path_buf(),
92 builtin_card_names: vec![
93 "backup".to_string(),
94 "snippet".to_string(),
95 "core".to_string(),
96 "blend".to_string(),
97 ],
98 }
99 }
100
101 pub fn load_cards(&mut self) -> Result<()> {
103 self.register_builtin_cards()?;
105
106 self.load_configs()?;
108
109 self.load_external_cards()?;
111
112 Ok(())
113 }
114
115 fn load_configs(&mut self) -> Result<()> {
117 let config_path = self.card_dir.join("cards.json");
118
119 for card_name in &self.builtin_card_names {
121 if !self.configs.contains_key(card_name) {
122 self.configs.insert(card_name.clone(), CardConfig {
123 name: card_name.clone(),
124 enabled: true, options: HashMap::new(),
126 });
127 } else {
128 let config = self.configs.get_mut(card_name).unwrap();
130 config.enabled = true;
131 }
132 }
133
134 if !config_path.exists() {
135 let json = serde_json::to_string_pretty(&self.configs)?;
137 std::fs::write(&config_path, json)?;
138 return Ok(());
139 }
140
141 let json = std::fs::read_to_string(&config_path)?;
143 match serde_json::from_str::<HashMap<String, CardConfig>>(&json) {
144 Ok(external_configs) => {
145 for (name, config) in external_configs {
147 if self.is_builtin_card(&name) {
149 if let Some(builtin_config) = self.configs.get_mut(&name) {
150 builtin_config.options = config.options;
151 builtin_config.enabled = true;
153 }
154 } else {
155 self.configs.insert(name, config);
157 }
158 }
159 },
160 Err(e) => {
161 log::error!("Failed to parse card configs: {}. Using defaults for built-in cards.", e);
163 let json = serde_json::to_string_pretty(&self.configs)?;
167 std::fs::write(&config_path, json)?;
168 }
169 }
170
171 Ok(())
172 }
173
174 fn is_builtin_card(&self, name: &str) -> bool {
176 self.builtin_card_names.contains(&name.to_string())
177 }
178
179 pub fn save_configs(&self) -> Result<()> {
181 let config_path = self.card_dir.join("cards.json");
182 let json = serde_json::to_string_pretty(&self.configs)?;
183 std::fs::write(&config_path, json)?;
184 Ok(())
185 }
186
187 fn register_builtin_cards(&mut self) -> Result<()> {
189 let data_dir = self.card_dir.parent().unwrap_or(&self.card_dir).to_path_buf();
191
192 use crate::cards::backup::BackupCard;
194 let backup_card = BackupCard::new(data_dir.clone());
195 let backup_name = backup_card.name().to_string();
196 self.cards.insert(backup_name.clone(), Box::new(backup_card) as Box<dyn Card>);
197
198 use crate::cards::snippet::SnippetCard;
200 let snippet_card = SnippetCard::new(data_dir.clone());
201 let snippet_name = snippet_card.name().to_string();
202 self.cards.insert(snippet_name.clone(), Box::new(snippet_card) as Box<dyn Card>);
203
204 use crate::cards::core::CoreCard;
206 let core_card = CoreCard::new(data_dir.clone());
207 let core_name = core_card.name().to_string();
208 self.cards.insert(core_name.clone(), Box::new(core_card) as Box<dyn Card>);
209
210 use crate::cards::blend::BlendCard;
212 let blend_card = BlendCard::new(data_dir);
213 let blend_name = blend_card.name().to_string();
214 self.cards.insert(blend_name.clone(), Box::new(blend_card) as Box<dyn Card>);
215
216 self.ensure_card_enabled(&backup_name)?;
218 self.ensure_card_enabled(&snippet_name)?;
219 self.ensure_card_enabled(&core_name)?;
220 self.ensure_card_enabled(&blend_name)?;
221
222 Ok(())
223 }
224
225 fn ensure_card_enabled(&mut self, name: &str) -> Result<()> {
227 let is_builtin = self.builtin_card_names.contains(&name.to_string());
229 let is_test_card = name == "test-card3"; if !self.configs.contains_key(name) {
232 let config = CardConfig {
234 name: name.to_string(),
235 enabled: true,
236 options: HashMap::new(),
237 };
238 self.configs.insert(name.to_string(), config);
239 self.save_configs()?;
240 } else if let Some(config) = self.configs.get_mut(name) {
241 if !config.enabled {
243 if is_builtin || is_test_card {
245 config.enabled = true;
246 self.save_configs()?;
247 }
248 }
249 }
250 Ok(())
251 }
252
253 fn register_card(&mut self, card: Box<dyn Card>) -> Result<()> {
255 let name = card.name().to_string();
256
257 if self.cards.contains_key(&name) {
259 return Err(anyhow!("Card already registered: {}", name));
260 }
261
262 if !self.configs.contains_key(&name) {
264 self.register_card_config(&name, "local")?;
266 }
267
268 self.cards.insert(name, card);
270
271 Ok(())
272 }
273
274 pub fn list_cards(&self) -> Vec<(String, String, bool)> {
276 self.cards.iter()
277 .map(|(name, card)| {
278 let version = card.version().to_string();
279 let enabled = self.configs.get(name)
280 .map(|c| c.enabled)
281 .unwrap_or(false);
282
283 (name.clone(), version, enabled)
284 })
285 .collect()
286 }
287
288 pub fn enable_card(&mut self, name: &str) -> Result<()> {
290 if let Some(config) = self.configs.get_mut(name) {
291 config.enabled = true;
292 self.save_configs()?;
293 Ok(())
294 } else {
295 anyhow::bail!("Card '{}' not found", name)
296 }
297 }
298
299 pub fn disable_card(&mut self, name: &str) -> Result<()> {
301 if self.is_builtin_card(name) {
303 return Err(anyhow!("Cannot disable built-in card '{}'", name));
304 }
305
306 if let Some(config) = self.configs.get_mut(name) {
307 config.enabled = false;
308 self.save_configs()?;
309 Ok(())
310 } else {
311 anyhow::bail!("Card '{}' not found", name)
312 }
313 }
314
315 pub fn execute_command(&self, card_name: &str, command: &str, args: &[String]) -> Result<()> {
317 let card = self.cards.get(card_name);
319
320 if let Some(card) = card {
321 let enabled = self.configs.get(card_name)
323 .map(|c| c.enabled)
324 .unwrap_or(false);
325
326 if !enabled {
327 return Err(anyhow::anyhow!("Card '{}' is disabled", card_name));
328 }
329
330 card.execute(command, args)
332 } else {
333 if self.configs.contains_key(card_name) {
335 return Err(anyhow::anyhow!("Card '{}' is registered but not loaded. Try rebuilding the card with: pocket cards build {}", card_name, card_name));
338 }
339
340 Err(anyhow::anyhow!("Card '{}' not found", card_name))
341 }
342 }
343
344 pub fn list_commands(&self) -> Vec<(String, Vec<CardCommand>)> {
346 let mut result = Vec::new();
347
348 for (name, card) in &self.cards {
349 let commands = card.commands();
350 if !commands.is_empty() {
351 result.push((name.clone(), commands));
352 }
353 }
354
355 result
356 }
357
358 pub fn get_card_commands(&self, name: &str) -> Result<Vec<CardCommand>> {
360 if let Some(card) = self.cards.get(name) {
361 Ok(card.commands())
362 } else {
363 Err(anyhow!("Card not found: {}", name))
364 }
365 }
366
367 pub fn cleanup(&mut self) -> Result<()> {
369 for card in self.cards.values_mut() {
370 card.cleanup()?;
371 }
372 Ok(())
373 }
374
375 pub fn card_exists(&self, name: &str) -> bool {
377 self.cards.contains_key(name)
378 }
379
380 pub fn register_card_config(&mut self, name: &str, url: &str) -> Result<()> {
382 let config = CardConfig {
384 name: name.to_string(),
385 enabled: true,
386 options: {
387 let mut options = HashMap::new();
388 options.insert("url".to_string(), serde_json::Value::String(url.to_string()));
389 options
390 },
391 };
392
393 self.configs.insert(name.to_string(), config);
395
396 self.save_configs()?;
398
399 Ok(())
400 }
401
402 pub fn remove_card_config(&mut self, name: &str) -> Result<()> {
404 if self.is_builtin_card(name) {
406 return Err(anyhow!("Cannot remove built-in card '{}'", name));
407 }
408
409 self.configs.remove(name);
411
412 self.save_configs()?;
414
415 Ok(())
416 }
417
418 fn load_external_cards(&mut self) -> Result<()> {
420 let wallet_dir = self.card_dir.parent().unwrap_or(&self.card_dir).join("wallet");
422
423 if !wallet_dir.exists() {
425 return Ok(());
426 }
427
428 for entry in fs::read_dir(&wallet_dir)? {
430 let entry = entry?;
431 let path = entry.path();
432
433 if !path.is_dir() {
435 continue;
436 }
437
438 let card_name = match path.file_name().and_then(|name| name.to_str()) {
440 Some(name) => name.to_string(),
441 None => continue, };
443
444 if self.cards.contains_key(&card_name) {
446 continue;
447 }
448
449 #[cfg(target_os = "macos")]
451 let lib_filename = format!("libpocket_card_{}.dylib", card_name.replace('-', "_"));
452
453 #[cfg(target_os = "linux")]
454 let lib_filename = format!("libpocket_card_{}.so", card_name.replace('-', "_"));
455
456 #[cfg(target_os = "windows")]
457 let lib_filename = format!("pocket_card_{}.dll", card_name.replace('-', "_"));
458
459 let release_dir = path.join("target").join("release");
461 let release_lib_path = release_dir.join(&lib_filename);
462
463 let debug_dir = path.join("target").join("debug");
465 let debug_lib_path = debug_dir.join(&lib_filename);
466
467 let lib_path = if release_lib_path.exists() {
469 release_lib_path
470 } else if debug_lib_path.exists() {
471 debug_lib_path
472 } else {
473 let debug_deps_lib_path = debug_dir.join("deps").join(&lib_filename);
475 if debug_deps_lib_path.exists() {
476 debug_deps_lib_path
477 } else {
478 log::debug!("Card {} library not found in release or debug directories", card_name);
479 continue;
480 }
481 };
482
483 let result = self.load_dynamic_card(&card_name, &lib_path);
485 match result {
486 Ok(_) => {
487 log::info!("Successfully loaded card: {}", card_name);
488
489 self.ensure_card_enabled(&card_name)?;
491 },
492 Err(e) => {
493 log::error!("Failed to load card {}: {}", card_name, e);
494 }
495 }
496 }
497
498 Ok(())
499 }
500
501 fn load_dynamic_card(&mut self, name: &str, lib_path: &Path) -> Result<()> {
503 use libloading::{Library, Symbol};
504
505 type CreateCardFunc = unsafe fn() -> Box<dyn Card>;
507
508 unsafe {
509 let lib = Library::new(lib_path)
511 .map_err(|e| anyhow!("Failed to load dynamic library: {}", e))?;
512
513 let create_card: Symbol<CreateCardFunc> = lib.get(b"create_card")
515 .map_err(|e| anyhow!("Failed to find create_card function: {}", e))?;
516
517 let card = create_card();
519
520 if card.name() != name {
522 return Err(anyhow!(
523 "Card name mismatch: expected '{}', got '{}'",
524 name, card.name()
525 ));
526 }
527
528 self.cards.insert(name.to_string(), card);
530
531 std::mem::forget(lib);
534 }
535
536 Ok(())
537 }
538
539 pub fn create_card(&self, name: &str, description: &str) -> Result<()> {
541 let home_dir = dirs::home_dir().ok_or_else(|| anyhow!("Could not determine home directory"))?;
543 let wallet_dir = home_dir.join(".pocket").join("wallet");
544
545 if !wallet_dir.exists() {
547 fs::create_dir_all(&wallet_dir)?;
548 }
549
550 let card_dir = wallet_dir.join(name);
552 if card_dir.exists() {
553 bail!("Card '{}' already exists at {}", name, card_dir.display());
554 }
555
556 fs::create_dir(&card_dir)?;
558 fs::create_dir(card_dir.join("src"))?;
559
560 let current_dir = std::env::current_dir()?;
562 let pocket_cli_path = format!("\"{}\"", current_dir.display());
563
564 let cargo_toml = format!(
566 r#"[package]
567name = "pocket-card-{}"
568version = "0.1.0"
569edition = "2021"
570description = "{}"
571authors = [""]
572license = "MIT"
573
574[lib]
575name = "pocket_card_{}"
576crate-type = ["cdylib"]
577
578[dependencies]
579anyhow = "1.0"
580serde = {{ version = "1.0", features = ["derive"] }}
581serde_json = "1.0"
582pocket-cli = {{ path = {} }}
583"#,
584 name, description, name.replace("-", "_"), pocket_cli_path
585 );
586
587 fs::write(card_dir.join("Cargo.toml"), cargo_toml)?;
588
589 let card_toml = format!(
591 r#"[card]
592name = "{}"
593version = "0.1.0"
594description = "{}"
595author = ""
596enabled = true
597
598[commands]
599hello = "A simple hello command"
600"#,
601 name, description
602 );
603
604 fs::write(card_dir.join("card.toml"), card_toml)?;
605
606 let readme = format!(
608 r#"# {}
609
610{}
611
612## Usage
613
614```
615pocket cards run {} hello [name]
616```
617
618## Commands
619
620- `hello`: A simple hello command
621"#,
622 name, description, name
623 );
624
625 fs::write(card_dir.join("README.md"), readme)?;
626
627 let struct_name = format!("{}Card", name.replace("-", "_"));
629 let lib_rs = format!(
630 r#"use anyhow::{{Result, bail}};
631use serde::{{Serialize, Deserialize}};
632use std::collections::HashMap;
633
634// Struct to hold card configuration options
635#[derive(Debug, Clone, Serialize, Deserialize, Default)]
636pub struct CardConfig {{
637 // Add any card-specific configuration options here
638 pub some_option: Option<String>,
639}}
640
641// The main card struct
642pub struct {} {{
643 name: String,
644 version: String,
645 description: String,
646 config: CardConfig,
647}}
648
649// The Card trait implementation
650impl pocket_cli::cards::Card for {} {{
651 fn name(&self) -> &str {{
652 &self.name
653 }}
654
655 fn version(&self) -> &str {{
656 &self.version
657 }}
658
659 fn description(&self) -> &str {{
660 &self.description
661 }}
662
663 fn initialize(&mut self, config: &pocket_cli::cards::CardConfig) -> Result<()> {{
664 // Load card-specific configuration
665 if let Some(card_config) = config.options.get("config") {{
666 if let Ok(config) = serde_json::from_value::<CardConfig>(card_config.clone()) {{
667 self.config = config;
668 }}
669 }}
670
671 Ok(())
672 }}
673
674 fn execute(&self, command: &str, args: &[String]) -> Result<()> {{
675 match command {{
676 "hello" => {{
677 let name = args.get(0).map(|s| s.as_str()).unwrap_or("World");
678 println!("Hello, {{}}!", name);
679 Ok(())
680 }},
681 _ => bail!("Unknown command: {{}}", command),
682 }}
683 }}
684
685 fn commands(&self) -> Vec<pocket_cli::cards::CardCommand> {{
686 vec![
687 pocket_cli::cards::CardCommand {{
688 name: "hello".to_string(),
689 description: "A simple hello command".to_string(),
690 usage: format!("pocket cards run {} hello [name]"),
691 }},
692 ]
693 }}
694
695 fn cleanup(&mut self) -> Result<()> {{
696 // Cleanup any resources used by the card
697 Ok(())
698 }}
699}}
700
701// This function is required for dynamic loading
702#[no_mangle]
703pub extern "C" fn create_card() -> Box<dyn pocket_cli::cards::Card> {{
704 Box::new({} {{
705 name: "{}".to_string(),
706 version: "0.1.0".to_string(),
707 description: "{}".to_string(),
708 config: CardConfig::default(),
709 }})
710}}
711"#,
712 struct_name, struct_name, name, struct_name, name, description
713 );
714
715 fs::write(card_dir.join("src").join("lib.rs"), lib_rs)?;
716
717 log::info!("Created new card '{}' in {}", name, card_dir.display());
721 log::info!("To register the card: pocket cards add {} local", name);
722 log::info!("To build the card: pocket cards build {}", name);
723
724 Ok(())
725 }
726
727 pub fn build_card(&self, name: &str, release: bool) -> Result<()> {
729 let wallet_dir = self.card_dir.parent().unwrap_or(&self.card_dir).join("wallet");
731
732 let card_dir = wallet_dir.join(name);
734 if !card_dir.exists() {
735 return Err(anyhow!("Card '{}' not found", name));
736 }
737
738 let mut command = std::process::Command::new("cargo");
740 command.current_dir(&card_dir);
741 command.arg("build");
742
743 if release {
744 command.arg("--release");
745 }
746
747 log::info!("Building card '{}' (release={})", name, release);
748
749 let output = command.output()
751 .map_err(|e| anyhow!("Failed to run cargo build: {}", e))?;
752
753 if !output.status.success() {
754 let stderr = String::from_utf8_lossy(&output.stderr);
755 return Err(anyhow!("Failed to build card: {}", stderr));
756 }
757
758 log::info!("Successfully built card '{}'", name);
759
760 Ok(())
761 }
762}
763
764impl Drop for CardManager {
765 fn drop(&mut self) {
766 let _ = self.cleanup();
768 }
769}
770
771struct PlaceholderCard {
773 name: String,
774 version: String,
775 description: String,
776}
777
778impl PlaceholderCard {
779 fn new(name: String) -> Self {
780 Self {
781 name,
782 version: "0.1.0".to_string(),
783 description: "A placeholder card".to_string(),
784 }
785 }
786}
787
788impl Card for PlaceholderCard {
789 fn name(&self) -> &str {
790 &self.name
791 }
792
793 fn version(&self) -> &str {
794 &self.version
795 }
796
797 fn description(&self) -> &str {
798 &self.description
799 }
800
801 fn initialize(&mut self, _config: &CardConfig) -> Result<()> {
802 Ok(())
803 }
804
805 fn execute(&self, command: &str, args: &[String]) -> Result<()> {
806 println!("Executing command {} with args {:?} on placeholder card {}", command, args, self.name);
807 Ok(())
808 }
809
810 fn commands(&self) -> Vec<CardCommand> {
811 vec![
812 CardCommand {
813 name: "hello".to_string(),
814 description: "A simple hello command".to_string(),
815 usage: format!("pocket cards execute {} hello [args...]", self.name),
816 },
817 ]
818 }
819
820 fn cleanup(&mut self) -> Result<()> {
821 Ok(())
822 }
823}