Skip to main content

cargo_mate/
anchor.rs

1use anyhow::{Context, Result};
2use chrono::{DateTime, Utc};
3use colored::*;
4use notify::{Event, RecursiveMode, Watcher};
5use serde::{Deserialize, Serialize};
6use sha2::{Sha256, Digest};
7use std::collections::HashMap;
8use std::fs;
9use std::io::Read;
10use std::path::{Path, PathBuf};
11use std::process::Command;
12use std::sync::mpsc::channel;
13use crate::captain::{license, config::ConfigManager};
14#[derive(Debug, Serialize, Deserialize, Clone)]
15pub struct Anchor {
16    pub name: String,
17    pub timestamp: DateTime<Utc>,
18    pub description: String,
19    pub git_commit: Option<String>,
20    pub cargo_lock_hash: String,
21    pub files_snapshot: HashMap<String, FileSnapshot>,
22    pub environment: HashMap<String, String>,
23    pub metadata: AnchorMetadata,
24}
25#[derive(Debug, Serialize, Deserialize, Clone)]
26pub struct FileSnapshot {
27    pub path: PathBuf,
28    pub hash: String,
29    pub size: u64,
30    pub modified: DateTime<Utc>,
31}
32#[derive(Debug, Serialize, Deserialize, Clone)]
33pub struct AnchorMetadata {
34    pub project_name: String,
35    pub rust_version: String,
36    pub dependencies_count: usize,
37    pub total_loc: usize,
38}
39pub struct AnchorManager {
40    anchors_dir: PathBuf,
41    snapshots_dir: PathBuf,
42}
43impl AnchorManager {
44    pub fn new() -> Result<Self> {
45        let shipwreck = dirs::home_dir()
46            .context("Could not find home directory")?
47            .join(".shipwreck");
48        let anchors_dir = shipwreck.join("anchors");
49        let snapshots_dir = shipwreck.join("snapshots");
50        fs::create_dir_all(&anchors_dir)?;
51        fs::create_dir_all(&snapshots_dir)?;
52        Ok(Self { anchors_dir, snapshots_dir })
53    }
54    pub fn save(&self, name: &str, description: &str) -> Result<()> {
55        println!("āš“ Dropping anchor: {}", name.cyan().bold());
56        let config = ConfigManager::new()?;
57        let auto_anchor_git = config.get("version_control.auto_anchor_git")
58            .unwrap_or_else(|| "true".to_string())
59            .parse()
60            .unwrap_or(true);
61        let git_commit = if auto_anchor_git {
62            self.get_current_git_commit().ok()
63        } else {
64            None
65        };
66        let cargo_lock_hash = self.hash_cargo_lock()?;
67        let files_snapshot = self.create_files_snapshot()?;
68        let environment = self.capture_environment();
69        let metadata = self.gather_metadata()?;
70        let anchor = Anchor {
71            name: name.to_string(),
72            timestamp: Utc::now(),
73            description: description.to_string(),
74            git_commit,
75            cargo_lock_hash,
76            files_snapshot: files_snapshot.clone(),
77            environment,
78            metadata,
79        };
80        self.save_anchor(&anchor)?;
81        self.save_file_backups(&anchor)?;
82        println!("āœ… Anchor '{}' saved successfully!", name.green());
83        println!("   šŸ“ {} files backed up", files_snapshot.len());
84        Ok(())
85    }
86    pub fn restore(&self, name: &str) -> Result<()> {
87        println!("āš“ Restoring anchor: {}", name.cyan().bold());
88        let anchor = self.load_anchor(name)?;
89        self.restore_cargo_lock(&anchor)?;
90        let restored_count = self.restore_files(&anchor)?;
91        println!("āœ… Anchor '{}' restored successfully!", name.green());
92        println!("   šŸ“ {} files restored", restored_count);
93        println!("   šŸ• From: {}", anchor.timestamp.format("%Y-%m-%d %H:%M:%S"));
94        Ok(())
95    }
96    pub fn update_file(&self, anchor_name: &str, file_path: &Path) -> Result<()> {
97        let mut anchor = self.load_anchor(anchor_name)?;
98        if let Some(file_key) = anchor
99            .files_snapshot
100            .keys()
101            .find(|&path| path == &file_path.to_string_lossy())
102        {
103            let file_snapshot = self.create_file_snapshot(file_path)?;
104            anchor.files_snapshot.insert(file_key.clone(), file_snapshot);
105            anchor.timestamp = Utc::now();
106            self.save_anchor(&anchor)?;
107            println!("šŸ”„ Updated {} in anchor '{}'", file_path.display(), anchor_name);
108        }
109        Ok(())
110    }
111    pub fn start_auto_update(&self, anchor_name: &str) -> Result<()> {
112        self.start_auto_update_with_options(anchor_name, false)
113    }
114    pub fn start_auto_update_background(&self, anchor_name: &str) -> Result<()> {
115        self.start_auto_update_with_options(anchor_name, true)
116    }
117    pub fn start_auto_update_with_options(
118        &self,
119        anchor_name: &str,
120        background: bool,
121    ) -> Result<()> {
122        let anchor = self.load_anchor(anchor_name)?;
123        if background {
124            println!(
125                "šŸš€ {}", format!("Starting auto-update for anchor: {}", anchor_name)
126                .cyan().bold()
127            );
128            println!("šŸ“ Setting up file monitoring...");
129            let manager = AnchorManager::new()?;
130            let anchor_clone = anchor.clone();
131            let anchor_name_clone = anchor_name.to_string();
132            std::thread::spawn(move || {
133                if let Err(e) = manager
134                    .run_auto_update_loop(&anchor_clone, &anchor_name_clone)
135                {
136                    eprintln!("āŒ Auto-update error for {}: {}", anchor_name_clone, e);
137                }
138            });
139            println!("āœ… {}", "Auto-update STARTED successfully!".green().bold());
140            println!("šŸ”„ Files will be updated automatically when changed");
141            println!("šŸ›‘ Use 'cargo anchor stop {}' to stop monitoring", anchor_name);
142            println!();
143            println!(
144                "šŸ’” {}", format!("Background daemon running for anchor '{}'",
145                anchor_name) .dimmed()
146            );
147            return Ok(());
148        } else {
149            println!(
150                "šŸ“ Monitoring {} files for changes...", anchor.files_snapshot.len()
151            );
152            println!("šŸ’” Press Ctrl+C to stop auto-update");
153            println!();
154        }
155        let (tx, rx) = channel();
156        let mut watcher = notify::recommended_watcher(tx)?;
157        let mut watched_dirs = HashMap::new();
158        for file_path in anchor.files_snapshot.keys() {
159            let path = Path::new(file_path);
160            if let Some(parent) = path.parent() {
161                if !watched_dirs.contains_key(parent) {
162                    watcher.watch(parent, RecursiveMode::NonRecursive)?;
163                    watched_dirs.insert(parent.to_path_buf(), true);
164                }
165            }
166        }
167        println!("šŸ‘€ Watching {} directories", watched_dirs.len());
168        println!("āœ… Auto-update started! Files will be updated automatically.");
169        println!();
170        loop {
171            match rx.recv() {
172                Ok(event) => {
173                    match event {
174                        Ok(Event { paths, kind: _, attrs: _ }) => {
175                            for path in paths {
176                                let path_str = path.to_string_lossy();
177                                if anchor.files_snapshot.contains_key(&path_str.to_string())
178                                {
179                                    if let Err(e) = self.update_file(anchor_name, &path) {
180                                        eprintln!("āŒ Failed to update {}: {}", path.display(), e);
181                                    } else {
182                                        println!(
183                                            "šŸ”„ Updated {} in anchor '{}'", path.display(),
184                                            anchor_name
185                                        );
186                                    }
187                                }
188                            }
189                        }
190                        Err(e) => {
191                            eprintln!("āŒ File watcher error: {}", e);
192                        }
193                    }
194                }
195                Err(e) => {
196                    eprintln!("āŒ Channel receive error: {}", e);
197                    break;
198                }
199            }
200        }
201        Ok(())
202    }
203    pub fn run_auto_update_loop(
204        &self,
205        anchor: &Anchor,
206        anchor_name: &str,
207    ) -> Result<()> {
208        let (tx, rx) = channel();
209        let mut watcher = notify::recommended_watcher(tx)?;
210        let mut watched_dirs = HashMap::new();
211        for file_path in anchor.files_snapshot.keys() {
212            let path = Path::new(file_path);
213            if let Some(parent) = path.parent() {
214                if !watched_dirs.contains_key(parent) {
215                    watcher.watch(parent, RecursiveMode::NonRecursive)?;
216                    watched_dirs.insert(parent.to_path_buf(), true);
217                }
218            }
219        }
220        println!("šŸ”„ Auto-update daemon running for '{}'", anchor_name);
221        loop {
222            match rx.recv() {
223                Ok(event) => {
224                    match event {
225                        Ok(Event { paths, kind: _, attrs: _ }) => {
226                            for path in paths {
227                                let path_str = path.to_string_lossy();
228                                if anchor.files_snapshot.contains_key(&path_str.to_string())
229                                {
230                                    if let Err(e) = self.update_file(anchor_name, &path) {
231                                        eprintln!("āŒ Failed to update {}: {}", path.display(), e);
232                                    } else {
233                                        println!(
234                                            "šŸ”„ [{}] Updated {} in anchor '{}'", chrono::Utc::now()
235                                            .format("%H:%M:%S"), path.display(), anchor_name
236                                        );
237                                    }
238                                }
239                            }
240                        }
241                        Err(e) => {
242                            eprintln!("āŒ File watcher error: {}", e);
243                        }
244                    }
245                }
246                Err(e) => {
247                    eprintln!("āŒ Channel receive error: {}", e);
248                    break;
249                }
250            }
251        }
252        Ok(())
253    }
254    pub fn stop_auto_update(&self, anchor_name: &str) -> Result<()> {
255        println!("šŸ›‘ Stopping auto-update for anchor: {}", anchor_name.cyan().bold());
256        println!(
257            "āš ļø  Note: In this implementation, stopping requires restarting the shell"
258        );
259        println!("šŸ’” Future versions will have proper daemon management");
260        Ok(())
261    }
262    pub fn list(&self) -> Result<Vec<AnchorSummary>> {
263        let mut anchors = Vec::new();
264        for entry in fs::read_dir(&self.anchors_dir)? {
265            let entry = entry?;
266            let path = entry.path();
267            if path.extension() == Some(std::ffi::OsStr::new("json")) {
268                let content = fs::read_to_string(&path)?;
269                let anchor: Anchor = serde_json::from_str(&content)?;
270                anchors
271                    .push(AnchorSummary {
272                        name: anchor.name,
273                        timestamp: anchor.timestamp,
274                        description: anchor.description,
275                        files_count: anchor.files_snapshot.len(),
276                    });
277            }
278        }
279        anchors.sort_by(|a, b| b.timestamp.cmp(&a.timestamp));
280        Ok(anchors)
281    }
282    pub fn show(&self, name: &str) -> Result<()> {
283        let anchor = self.load_anchor(name)?;
284        println!("{}", format!("=== Anchor: {} ===", anchor.name) .blue().bold());
285        println!("šŸ“… Created: {}", anchor.timestamp.format("%Y-%m-%d %H:%M:%S"));
286        println!("šŸ“ Description: {}", anchor.description);
287        if let Some(ref commit) = anchor.git_commit {
288            println!("šŸ”— Git commit: {}", commit.dimmed());
289        }
290        println!("\nšŸ“Š Metadata:");
291        println!("   Project: {}", anchor.metadata.project_name);
292        println!("   Rust version: {}", anchor.metadata.rust_version);
293        println!("   Dependencies: {}", anchor.metadata.dependencies_count);
294        println!("   Lines of code: {}", anchor.metadata.total_loc);
295        println!("\nšŸ“ Files snapshot ({} files):", anchor.files_snapshot.len());
296        let mut files: Vec<_> = anchor.files_snapshot.values().collect();
297        files.sort_by(|a, b| a.path.cmp(&b.path));
298        for (i, file) in files.iter().enumerate().take(10) {
299            println!("   {} {}", if i < 9 { " " } else { "" }, file.path.display());
300        }
301        if anchor.files_snapshot.len() > 10 {
302            println!("   ... and {} more files", anchor.files_snapshot.len() - 10);
303        }
304        Ok(())
305    }
306    pub fn diff(&self, name: &str) -> Result<()> {
307        let anchor = self.load_anchor(name)?;
308        let current_snapshot = self.create_files_snapshot()?;
309        println!("{}", format!("=== Diff from anchor '{}' ===", name) .blue().bold());
310        let mut added = Vec::new();
311        let mut modified = Vec::new();
312        let mut deleted = Vec::new();
313        for (path, current_file) in &current_snapshot {
314            match anchor.files_snapshot.get(path) {
315                Some(anchor_file) => {
316                    if anchor_file.hash != current_file.hash {
317                        modified.push(path.clone());
318                    }
319                }
320                None => added.push(path.clone()),
321            }
322        }
323        for path in anchor.files_snapshot.keys() {
324            if !current_snapshot.contains_key(path) {
325                deleted.push(path.clone());
326            }
327        }
328        if !added.is_empty() {
329            println!("\n✨ Added files:");
330            for path in &added {
331                println!("   + {}", path.green());
332            }
333        }
334        if !modified.is_empty() {
335            println!("\nšŸ“ Modified files:");
336            for path in &modified {
337                println!("   ~ {}", path.yellow());
338            }
339        }
340        if !deleted.is_empty() {
341            println!("\nšŸ—‘ļø  Deleted files:");
342            for path in &deleted {
343                println!("   - {}", path.red());
344            }
345        }
346        if added.is_empty() && modified.is_empty() && deleted.is_empty() {
347            println!("āœ… No changes since anchor '{}'", name);
348        }
349        Ok(())
350    }
351    fn save_anchor(&self, anchor: &Anchor) -> Result<()> {
352        let anchor_file = self.anchors_dir.join(format!("{}.json", anchor.name));
353        let json = serde_json::to_string_pretty(anchor)?;
354        fs::write(&anchor_file, json)?;
355        Ok(())
356    }
357    fn load_anchor(&self, name: &str) -> Result<Anchor> {
358        let anchor_file = self.anchors_dir.join(format!("{}.json", name));
359        if !anchor_file.exists() {
360            return Err(anyhow::anyhow!("Anchor '{}' not found", name));
361        }
362        let content = fs::read_to_string(&anchor_file)?;
363        let anchor: Anchor = serde_json::from_str(&content)?;
364        Ok(anchor)
365    }
366    fn get_current_git_commit(&self) -> Result<String> {
367        let output = Command::new("git")
368            .args(&["rev-parse", "HEAD"])
369            .output()?;
370        if !output.status.success() {
371            return Err(anyhow::anyhow!(
372                "Failed to get current git commit: {}",
373                String::from_utf8_lossy(&output.stderr)
374            ));
375        }
376        Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
377    }
378
379    fn checkout_git_commit(&self, commit: &str) -> Result<()> {
380        let output = Command::new("git").args(&["checkout", commit]).output()?;
381        if !output.status.success() {
382            return Err(
383                anyhow::anyhow!(
384                    "Failed to checkout commit: {}", String::from_utf8_lossy(& output
385                    .stderr)
386                ),
387            );
388        }
389        Ok(())
390    }
391    fn hash_cargo_lock(&self) -> Result<String> {
392        let cargo_lock = Path::new("Cargo.lock");
393        if !cargo_lock.exists() {
394            return Ok("no-cargo-lock".to_string());
395        }
396        let mut file = fs::File::open(cargo_lock)?;
397        let mut hasher = Sha256::new();
398        let mut buffer = [0; 8192];
399        loop {
400            let bytes_read = file.read(&mut buffer)?;
401            if bytes_read == 0 {
402                break;
403            }
404            hasher.update(&buffer[..bytes_read]);
405        }
406        Ok(format!("{:x}", hasher.finalize()))
407    }
408    fn create_files_snapshot(&self) -> Result<HashMap<String, FileSnapshot>> {
409        let mut snapshot = HashMap::new();
410        let patterns = vec![
411            "src/**/*.rs", "tests/**/*.rs", "**/*.toml", "**/*.lock", "**/build.rs",
412            "**/*.rs", ".env", "**/.env*",
413        ];
414        for pattern in patterns {
415            for entry in glob::glob(pattern)? {
416                if let Ok(path) = entry {
417                    if path.is_file() {
418                        let metadata = fs::metadata(&path)?;
419                        let hash = self.hash_file(&path)?;
420                        snapshot
421                            .insert(
422                                path.to_string_lossy().to_string(),
423                                FileSnapshot {
424                                    path: path.clone(),
425                                    hash,
426                                    size: metadata.len(),
427                                    modified: DateTime::from(metadata.modified()?),
428                                },
429                            );
430                    }
431                }
432            }
433        }
434        Ok(snapshot)
435    }
436    fn create_file_snapshot(&self, path: &Path) -> Result<FileSnapshot> {
437        let metadata = fs::metadata(path)?;
438        let hash = self.hash_file(path)?;
439        Ok(FileSnapshot {
440            path: path.to_path_buf(),
441            hash,
442            size: metadata.len(),
443            modified: DateTime::from(metadata.modified()?),
444        })
445    }
446    fn hash_file(&self, path: &Path) -> Result<String> {
447        let mut file = fs::File::open(path)?;
448        let mut hasher = Sha256::new();
449        let mut buffer = [0; 8192];
450        loop {
451            let bytes_read = file.read(&mut buffer)?;
452            if bytes_read == 0 {
453                break;
454            }
455            hasher.update(&buffer[..bytes_read]);
456        }
457        Ok(format!("{:x}", hasher.finalize()))
458    }
459    fn save_file_backups(&self, anchor: &Anchor) -> Result<()> {
460        let backup_dir = self.snapshots_dir.join(&anchor.name);
461        fs::create_dir_all(&backup_dir)?;
462        for (_, file) in &anchor.files_snapshot {
463            if file.path.exists() {
464                let backup_path = backup_dir
465                    .join(file.path.strip_prefix("./").unwrap_or(&file.path));
466                if let Some(parent) = backup_path.parent() {
467                    fs::create_dir_all(parent)?;
468                }
469                fs::copy(&file.path, &backup_path)?;
470            }
471        }
472        Ok(())
473    }
474    fn restore_cargo_lock(&self, anchor: &Anchor) -> Result<()> {
475        let current_hash = self.hash_cargo_lock()?;
476        if current_hash != anchor.cargo_lock_hash
477            && anchor.cargo_lock_hash != "no-cargo-lock"
478        {
479            let backup_dir = self.snapshots_dir.join(&anchor.name);
480            let backup_cargo_lock = backup_dir.join("Cargo.lock");
481            if backup_cargo_lock.exists() {
482                fs::copy(&backup_cargo_lock, "Cargo.lock")?;
483                println!("   šŸ“¦ Cargo.lock restored");
484            }
485        }
486        Ok(())
487    }
488    fn restore_files(&self, anchor: &Anchor) -> Result<usize> {
489        let backup_dir = self.snapshots_dir.join(&anchor.name);
490        let mut restored_count = 0;
491        for (_, file) in &anchor.files_snapshot {
492            let backup_path = backup_dir
493                .join(file.path.strip_prefix("./").unwrap_or(&file.path));
494            if backup_path.exists() {
495                let current_hash = if file.path.exists() {
496                    self.hash_file(&file.path).unwrap_or_default()
497                } else {
498                    String::new()
499                };
500                if current_hash != file.hash {
501                    if let Some(parent) = file.path.parent() {
502                        fs::create_dir_all(parent)?;
503                    }
504                    fs::copy(&backup_path, &file.path)?;
505                    restored_count += 1;
506                }
507            }
508        }
509        Ok(restored_count)
510    }
511    fn capture_environment(&self) -> HashMap<String, String> {
512        let mut env = HashMap::new();
513        for (key, value) in std::env::vars() {
514            if key.starts_with("CARGO_") || key.starts_with("RUST_") {
515                env.insert(key, value);
516            }
517        }
518        env
519    }
520    fn gather_metadata(&self) -> Result<AnchorMetadata> {
521        let cargo_toml = fs::read_to_string("Cargo.toml")?;
522        let manifest: toml::Value = toml::from_str(&cargo_toml)?;
523        let project_name = manifest
524            .get("package")
525            .and_then(|p| p.get("name"))
526            .and_then(|n| n.as_str())
527            .unwrap_or("unknown")
528            .to_string();
529        let rust_version = Command::new("rustc")
530            .arg("--version")
531            .output()
532            .map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
533            .unwrap_or_else(|_| "unknown".to_string());
534        let dependencies_count = manifest
535            .get("dependencies")
536            .and_then(|d| d.as_table())
537            .map(|t| t.len())
538            .unwrap_or(0);
539        let total_loc = self.count_lines_of_code()?;
540        Ok(AnchorMetadata {
541            project_name,
542            rust_version,
543            dependencies_count,
544            total_loc,
545        })
546    }
547    fn count_lines_of_code(&self) -> Result<usize> {
548        let mut total = 0;
549        for entry in glob::glob("src/**/*.rs")? {
550            if let Ok(path) = entry {
551                if path.is_file() {
552                    let content = fs::read_to_string(&path)?;
553                    total += content.lines().count();
554                }
555            }
556        }
557        Ok(total)
558    }
559}
560#[derive(Debug)]
561pub struct AnchorSummary {
562    pub name: String,
563    pub timestamp: DateTime<Utc>,
564    pub description: String,
565    pub files_count: usize,
566}
567impl AnchorSummary {
568    pub fn display(&self) {
569        println!(
570            "āš“ {} - {} ({} files)", self.name.cyan().bold(), self.timestamp
571            .format("%Y-%m-%d %H:%M:%S").to_string().dimmed(), self.files_count
572        );
573        println!("   {}", self.description.dimmed());
574    }
575}
576pub fn check_license_ahoy(command: &str) -> Result<bool> {
577    println!(
578        "šŸ“ā€ā˜ ļø Ahoy there, matey! Let me check yer license for '{}'", command
579        .cyan()
580    );
581    let license_manager = license::LicenseManager::new();
582    match license_manager?.enforce_license(command) {
583        Ok(_) => {
584            println!("āœ… Aye aye, Captain! License be valid. Full speed ahead!");
585            Ok(true)
586        }
587        Err(e) => {
588            if e.to_string().contains("limit") {
589                println!("āš ļø  Blimey! Ye've hit the daily limit, scallywag!");
590                println!("   šŸ’° Walk the plank to Pro: https://cargo.do/checkout");
591            } else if e.to_string().contains("License not found") {
592                println!("āŒ Arr! No license found! Register with 'cm register <key>'");
593            } else {
594                println!(
595                    "āŒ Shiver me timbers! License error: {}", e.to_string().red()
596                );
597            }
598            Ok(false)
599        }
600    }
601}