metis_docs_cli/commands/
sync.rs1use crate::workspace;
2use anyhow::Result;
3use clap::Args;
4use metis_core::{Application, Database};
5
6#[derive(Args)]
7pub struct SyncCommand {}
8
9impl SyncCommand {
10 pub async fn execute(&self) -> Result<()> {
11 let (workspace_exists, metis_dir) = workspace::has_metis_vault();
13 if !workspace_exists {
14 anyhow::bail!("Not in a Metis workspace. Run 'metis init' to create one.");
15 }
16
17 let metis_dir = metis_dir.unwrap();
18 let workspace_root = &metis_dir;
19
20 println!("Syncing workspace: {}", workspace_root.display());
21
22 let db_path = metis_dir.join("metis.db");
24 let database = Database::new(db_path.to_str().unwrap())
25 .map_err(|e| anyhow::anyhow!("Failed to initialize database: {}", e))?;
26 let app = Application::new(database);
27
28 let sync_results = app.sync_directory(workspace_root).await?;
30
31 let mut imported = 0;
33 let mut updated = 0;
34 let mut deleted = 0;
35 let mut up_to_date = 0;
36 let mut errors = 0;
37
38 for result in &sync_results {
39 match result {
40 metis_core::application::services::synchronization::SyncResult::Imported {
41 filepath,
42 } => {
43 println!("✓ Imported: {}", filepath);
44 imported += 1;
45 }
46 metis_core::application::services::synchronization::SyncResult::Updated {
47 filepath,
48 } => {
49 println!("✓ Updated: {}", filepath);
50 updated += 1;
51 }
52 metis_core::application::services::synchronization::SyncResult::Deleted {
53 filepath,
54 } => {
55 println!("✓ Deleted: {}", filepath);
56 deleted += 1;
57 }
58 metis_core::application::services::synchronization::SyncResult::UpToDate {
59 filepath,
60 } => {
61 println!("• Up to date: {}", filepath);
62 up_to_date += 1;
63 }
64 metis_core::application::services::synchronization::SyncResult::NotFound {
65 filepath,
66 } => {
67 println!("? Not found: {}", filepath);
68 }
69 metis_core::application::services::synchronization::SyncResult::Error {
70 filepath,
71 error,
72 } => {
73 println!("✗ Error syncing {}: {}", filepath, error);
74 errors += 1;
75 }
76 metis_core::application::services::synchronization::SyncResult::Moved {
77 from,
78 to,
79 } => {
80 println!("↻ Moved: {} → {}", from, to);
81 updated += 1;
82 }
83 metis_core::application::services::synchronization::SyncResult::Renumbered {
84 filepath,
85 old_short_code,
86 new_short_code,
87 } => {
88 println!(
89 "⚠ Renumbered: {} ({} → {})",
90 filepath, old_short_code, new_short_code
91 );
92 updated += 1;
93 }
94 }
95 }
96
97 println!("\nSync complete:");
98 println!(" Imported: {}", imported);
99 println!(" Updated: {}", updated);
100 println!(" Deleted: {}", deleted);
101 println!(" Up to date: {}", up_to_date);
102 if errors > 0 {
103 println!(" Errors: {}", errors);
104 }
105
106 if errors > 0 {
107 anyhow::bail!("Sync completed with {} errors", errors);
108 }
109
110 Ok(())
111 }
112}
113
114#[cfg(test)]
115mod tests {
116 use super::*;
117 use crate::commands::InitCommand;
118 use std::fs;
119 use tempfile::tempdir;
120
121 #[tokio::test]
122 async fn test_sync_command_no_workspace() {
123 let temp_dir = tempdir().unwrap();
124 let original_dir = std::env::current_dir().ok();
125
126 std::env::set_current_dir(temp_dir.path()).unwrap();
128
129 let cmd = SyncCommand {};
130 let result = cmd.execute().await;
131
132 assert!(result.is_err());
133 assert!(result
134 .unwrap_err()
135 .to_string()
136 .contains("Not in a Metis workspace"));
137
138 if let Some(original) = original_dir {
140 let _ = std::env::set_current_dir(&original);
141 }
142 }
143
144 #[tokio::test]
145 async fn test_sync_command_with_workspace() {
146 let temp_dir = tempdir().unwrap();
147 let original_dir = std::env::current_dir().ok();
148
149 std::env::set_current_dir(temp_dir.path()).unwrap();
151
152 let init_cmd = InitCommand {
154 name: Some("Test Project".to_string()),
155 preset: None,
156 strategies: None,
157 initiatives: None,
158 prefix: None,
159 };
160 init_cmd.execute().await.unwrap();
161
162 let test_strategy = temp_dir.path().join(".metis/strategies/test-strategy.md");
164 fs::create_dir_all(test_strategy.parent().unwrap()).unwrap();
165 fs::write(&test_strategy, "---\nid: test-strategy\nlevel: strategy\ntitle: \"Test Strategy\"\ncreated_at: 2025-01-01T00:00:00Z\nupdated_at: 2025-01-01T00:00:00Z\nparent: test-vision\nblocked_by: []\narchived: false\ntags:\n - \"#strategy\"\n - \"#phase/shaping\"\nexit_criteria_met: false\nsuccess_metrics: []\nrisk_level: medium\nstakeholders: []\n---\n\n# Test Strategy\n").unwrap();
166
167 let cmd = SyncCommand {};
169 let result = cmd.execute().await;
170
171 println!("Sync result: {:?}", result);
175
176 assert!(test_strategy.exists());
178
179 if let Some(original) = original_dir {
181 let _ = std::env::set_current_dir(&original);
182 }
183 }
184}