mcp_sync/commands/
diff.rs1use crate::{all_targets, read_canon_auto, SyncOptions};
4use anyhow::Result;
5use similar::{ChangeTag, TextDiff};
6use std::fs;
7use std::path::Path;
8use tracing::info;
9
10const RED: &str = "\x1b[31m";
12const GREEN: &str = "\x1b[32m";
13const CYAN: &str = "\x1b[36m";
14const RESET: &str = "\x1b[0m";
15const BOLD: &str = "\x1b[1m";
16
17fn print_diff(old: &str, new: &str, label: &str) {
19 let diff = TextDiff::from_lines(old, new);
20
21 let mut has_changes = false;
22 for change in diff.iter_all_changes() {
23 if change.tag() != ChangeTag::Equal {
24 has_changes = true;
25 break;
26 }
27 }
28
29 if !has_changes {
30 println!("{}{}{} - no changes", CYAN, label, RESET);
31 return;
32 }
33
34 println!("\n{}{}{}:", BOLD, label, RESET);
35
36 for change in diff.iter_all_changes() {
37 let (sign, color) = match change.tag() {
38 ChangeTag::Delete => ("-", RED),
39 ChangeTag::Insert => ("+", GREEN),
40 ChangeTag::Equal => (" ", ""),
41 };
42 print!("{}{}{}{}", color, sign, change, RESET);
43 }
44}
45
46fn generate_target_content(
48 target: &dyn crate::Target,
49 canon: &crate::Canon,
50) -> Result<String> {
51 use tempfile::TempDir;
52
53 let temp = TempDir::new()?;
54 let temp_path = temp.path().join("temp_config");
55
56 let opts = SyncOptions::new(false, false, false);
57 target.sync(&temp_path, canon, &opts)?;
58
59 if temp_path.exists() {
60 Ok(fs::read_to_string(&temp_path)?)
61 } else {
62 Ok(String::new())
63 }
64}
65
66pub fn run(canon_path: &str, scope: &str, project_root: &Path) -> Result<()> {
68 let canon = read_canon_auto(canon_path)?;
69
70 info!("Comparing {} servers against targets", canon.servers.len());
71
72 let targets = all_targets();
73 let do_global = scope == "global" || scope == "both";
74 let do_project = scope == "project" || scope == "both";
75
76 for target in &targets {
77 if do_global
78 && let Ok(global_path) = target.global_path() {
79 let current = if global_path.exists() {
80 fs::read_to_string(&global_path).unwrap_or_default()
81 } else {
82 String::new()
83 };
84
85 let expected = generate_target_content(target.as_ref(), &canon)
86 .unwrap_or_default();
87
88 print_diff(
89 ¤t,
90 &expected,
91 &format!("{} (global: {})", target.name(), global_path.display()),
92 );
93 }
94
95 if do_project {
96 let project_path = target.project_path(project_root);
97 let current = if project_path.exists() {
98 fs::read_to_string(&project_path).unwrap_or_default()
99 } else {
100 String::new()
101 };
102
103 let expected = generate_target_content(target.as_ref(), &canon)
104 .unwrap_or_default();
105
106 print_diff(
107 ¤t,
108 &expected,
109 &format!("{} (project: {})", target.name(), project_path.display()),
110 );
111 }
112 }
113
114 Ok(())
115}