1use crate::cli::SyncCommands;
8use crate::config::resolve_db_path;
9use crate::error::{Error, Result};
10use crate::storage::SqliteStorage;
11use crate::sync::{project_export_dir, Exporter, Importer, MergeStrategy};
12use std::env;
13use std::path::PathBuf;
14
15pub fn execute(command: &SyncCommands, db_path: Option<&PathBuf>, json: bool) -> Result<()> {
17 match command {
18 SyncCommands::Export { force } => export(*force, db_path, json),
19 SyncCommands::Import { force } => import(*force, db_path, json),
20 SyncCommands::Status => status(db_path, json),
21 }
22}
23
24fn get_project_path() -> Result<String> {
26 env::current_dir()
27 .map_err(|e| Error::Other(format!("Failed to get current directory: {e}")))?
28 .to_str()
29 .map(String::from)
30 .ok_or_else(|| Error::Other("Current directory path is not valid UTF-8".to_string()))
31}
32
33fn export(force: bool, db_path: Option<&PathBuf>, json: bool) -> Result<()> {
34 let db_path =
35 resolve_db_path(db_path.map(|p| p.as_path())).ok_or(Error::NotInitialized)?;
36
37 if !db_path.exists() {
38 return Err(Error::NotInitialized);
39 }
40
41 let project_path = get_project_path()?;
42 let mut storage = SqliteStorage::open(&db_path)?;
43 let output_dir = project_export_dir(&project_path);
44
45 let mut exporter = Exporter::new(&mut storage, project_path.clone());
46
47 match exporter.export(force) {
48 Ok(stats) => {
49 if json {
50 let output = serde_json::json!({
51 "success": true,
52 "project": project_path,
53 "output_dir": output_dir.display().to_string(),
54 "stats": stats,
55 });
56 println!("{}", serde_json::to_string(&output)?);
57 } else if stats.is_empty() {
58 println!("No records exported.");
59 } else {
60 println!("Export complete for: {project_path}");
61 println!();
62 if stats.sessions > 0 {
63 println!(" Sessions: {}", stats.sessions);
64 }
65 if stats.issues > 0 {
66 println!(" Issues: {}", stats.issues);
67 }
68 if stats.context_items > 0 {
69 println!(" Context Items: {}", stats.context_items);
70 }
71 if stats.memories > 0 {
72 println!(" Memories: {}", stats.memories);
73 }
74 if stats.checkpoints > 0 {
75 println!(" Checkpoints: {}", stats.checkpoints);
76 }
77 println!();
78 println!(" Total: {} records", stats.total());
79 println!(" Location: {}", output_dir.display());
80 }
81 Ok(())
82 }
83 Err(crate::sync::SyncError::NothingToExport) => {
84 if json {
85 let output = serde_json::json!({
86 "error": "nothing_to_export",
87 "project": project_path,
88 "message": "No dirty records to export for this project. Use --force to export all records."
89 });
90 println!("{output}");
91 } else {
92 println!("No dirty records to export for: {project_path}");
93 println!("Use --force to export all records regardless of dirty state.");
94 }
95 Ok(())
96 }
97 Err(e) => Err(Error::Other(e.to_string())),
98 }
99}
100
101fn import(force: bool, db_path: Option<&PathBuf>, json: bool) -> Result<()> {
102 let db_path =
103 resolve_db_path(db_path.map(|p| p.as_path())).ok_or(Error::NotInitialized)?;
104
105 if !db_path.exists() {
106 return Err(Error::NotInitialized);
107 }
108
109 let project_path = get_project_path()?;
110 let mut storage = SqliteStorage::open(&db_path)?;
111 let import_dir = project_export_dir(&project_path);
112
113 let strategy = if force {
115 MergeStrategy::PreferExternal
116 } else {
117 MergeStrategy::PreferNewer
118 };
119
120 let mut importer = Importer::new(&mut storage, strategy);
121
122 match importer.import_all(&import_dir) {
123 Ok(stats) => {
124 let total = stats.total_processed();
125 if json {
126 let output = serde_json::json!({
127 "success": true,
128 "project": project_path,
129 "import_dir": import_dir.display().to_string(),
130 "stats": stats,
131 });
132 println!("{}", serde_json::to_string(&output)?);
133 } else if total == 0 {
134 println!("No records to import for: {project_path}");
135 println!("Export files not found in: {}", import_dir.display());
136 } else {
137 println!("Import complete for: {project_path}");
138 println!();
139 print_entity_stats("Sessions", &stats.sessions);
140 print_entity_stats("Issues", &stats.issues);
141 print_entity_stats("Context Items", &stats.context_items);
142 print_entity_stats("Memories", &stats.memories);
143 print_entity_stats("Checkpoints", &stats.checkpoints);
144 println!();
145 println!(
146 "Total: {} created, {} updated, {} skipped",
147 stats.total_created(),
148 stats.total_updated(),
149 total - stats.total_created() - stats.total_updated()
150 );
151 }
152 Ok(())
153 }
154 Err(crate::sync::SyncError::FileNotFound(path)) => {
155 if json {
156 let output = serde_json::json!({
157 "error": "file_not_found",
158 "project": project_path,
159 "path": path
160 });
161 println!("{output}");
162 } else {
163 println!("Import file not found: {path}");
164 println!("Run 'sc sync export' first to create JSONL files.");
165 }
166 Ok(())
167 }
168 Err(e) => Err(Error::Other(e.to_string())),
169 }
170}
171
172fn print_entity_stats(name: &str, stats: &crate::sync::EntityStats) {
173 let total = stats.total();
174 if total > 0 {
175 println!(
176 " {}: {} created, {} updated, {} skipped",
177 name, stats.created, stats.updated, stats.skipped
178 );
179 }
180}
181
182fn status(db_path: Option<&PathBuf>, json: bool) -> Result<()> {
183 let db_path =
184 resolve_db_path(db_path.map(|p| p.as_path())).ok_or(Error::NotInitialized)?;
185
186 if !db_path.exists() {
187 return Err(Error::NotInitialized);
188 }
189
190 let project_path = get_project_path()?;
191 let storage = SqliteStorage::open(&db_path)?;
192 let export_dir = project_export_dir(&project_path);
193
194 let sync_status = crate::sync::get_sync_status(&storage, &export_dir, &project_path)
195 .map_err(|e| Error::Other(e.to_string()))?;
196
197 if json {
198 let output = serde_json::json!({
199 "project": project_path,
200 "export_dir": export_dir.display().to_string(),
201 "status": sync_status,
202 });
203 println!("{}", serde_json::to_string(&output)?);
204 } else {
205 println!("Sync status for: {project_path}");
206 println!("Export directory: {}", export_dir.display());
207 println!();
208 crate::sync::print_status(&sync_status);
209 }
210
211 Ok(())
212}