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 }
84 }
85
86 println!("\nSync complete:");
87 println!(" Imported: {}", imported);
88 println!(" Updated: {}", updated);
89 println!(" Deleted: {}", deleted);
90 println!(" Up to date: {}", up_to_date);
91 if errors > 0 {
92 println!(" Errors: {}", errors);
93 }
94
95 if errors > 0 {
96 anyhow::bail!("Sync completed with {} errors", errors);
97 }
98
99 Ok(())
100 }
101}
102
103#[cfg(test)]
104mod tests {
105 use super::*;
106 use crate::commands::InitCommand;
107 use std::fs;
108 use tempfile::tempdir;
109
110 #[tokio::test]
111 async fn test_sync_command_no_workspace() {
112 let temp_dir = tempdir().unwrap();
113 let original_dir = std::env::current_dir().ok();
114
115 std::env::set_current_dir(temp_dir.path()).unwrap();
117
118 let cmd = SyncCommand {};
119 let result = cmd.execute().await;
120
121 assert!(result.is_err());
122 assert!(result
123 .unwrap_err()
124 .to_string()
125 .contains("Not in a Metis workspace"));
126
127 if let Some(original) = original_dir {
129 let _ = std::env::set_current_dir(&original);
130 }
131 }
132
133 #[tokio::test]
134 async fn test_sync_command_with_workspace() {
135 let temp_dir = tempdir().unwrap();
136 let original_dir = std::env::current_dir().ok();
137
138 std::env::set_current_dir(temp_dir.path()).unwrap();
140
141 let init_cmd = InitCommand {
143 name: Some("Test Project".to_string()),
144 preset: None,
145 strategies: None,
146 initiatives: None,
147 prefix: None,
148 };
149 init_cmd.execute().await.unwrap();
150
151 let test_strategy = temp_dir.path().join(".metis/strategies/test-strategy.md");
153 fs::create_dir_all(test_strategy.parent().unwrap()).unwrap();
154 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();
155
156 let cmd = SyncCommand {};
158 let result = cmd.execute().await;
159
160 println!("Sync result: {:?}", result);
164
165 assert!(test_strategy.exists());
167
168 if let Some(original) = original_dir {
170 let _ = std::env::set_current_dir(&original);
171 }
172 }
173}