1pub mod api_client;
2pub mod crate_deploy;
3pub mod database;
4pub mod diff;
5pub mod error;
6pub mod export;
7pub mod files;
8pub mod local;
9pub mod models;
10
11use serde::{Deserialize, Serialize};
12
13pub use api_client::SyncApiClient;
14pub use database::{ContextExport, DatabaseExport, DatabaseSyncService, SkillExport};
15pub use diff::{
16 compute_content_hash, ContentDiffCalculator, PlaybooksDiffCalculator, SkillsDiffCalculator,
17};
18pub use error::{SyncError, SyncResult};
19pub use export::{
20 escape_yaml, export_content_to_file, export_playbook_to_disk, export_skill_to_disk,
21 generate_content_markdown, generate_playbook_markdown, generate_skill_config,
22 generate_skill_markdown,
23};
24pub use files::{FileBundle, FileEntry, FileManifest, FileSyncService};
25pub use local::{ContentDiffEntry, ContentLocalSync, PlaybooksLocalSync, SkillsLocalSync};
26pub use models::{
27 ContentDiffItem, ContentDiffResult, DiffStatus, DiskContent, DiskPlaybook, DiskSkill,
28 LocalSyncDirection, LocalSyncResult, PlaybookDiffItem, PlaybooksDiffResult, SkillDiffItem,
29 SkillsDiffResult,
30};
31
32#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
33pub enum SyncDirection {
34 Push,
35 Pull,
36}
37
38#[derive(Clone, Debug)]
39pub struct SyncConfig {
40 pub direction: SyncDirection,
41 pub dry_run: bool,
42 pub verbose: bool,
43 pub tenant_id: String,
44 pub api_url: String,
45 pub api_token: String,
46 pub services_path: String,
47 pub hostname: Option<String>,
48 pub sync_token: Option<String>,
49 pub local_database_url: Option<String>,
50}
51
52#[derive(Debug)]
53pub struct SyncConfigBuilder {
54 direction: SyncDirection,
55 dry_run: bool,
56 verbose: bool,
57 tenant_id: String,
58 api_url: String,
59 api_token: String,
60 services_path: String,
61 hostname: Option<String>,
62 sync_token: Option<String>,
63 local_database_url: Option<String>,
64}
65
66impl SyncConfigBuilder {
67 pub fn new(
68 tenant_id: impl Into<String>,
69 api_url: impl Into<String>,
70 api_token: impl Into<String>,
71 services_path: impl Into<String>,
72 ) -> Self {
73 Self {
74 direction: SyncDirection::Push,
75 dry_run: false,
76 verbose: false,
77 tenant_id: tenant_id.into(),
78 api_url: api_url.into(),
79 api_token: api_token.into(),
80 services_path: services_path.into(),
81 hostname: None,
82 sync_token: None,
83 local_database_url: None,
84 }
85 }
86
87 pub const fn with_direction(mut self, direction: SyncDirection) -> Self {
88 self.direction = direction;
89 self
90 }
91
92 pub const fn with_dry_run(mut self, dry_run: bool) -> Self {
93 self.dry_run = dry_run;
94 self
95 }
96
97 pub const fn with_verbose(mut self, verbose: bool) -> Self {
98 self.verbose = verbose;
99 self
100 }
101
102 pub fn with_hostname(mut self, hostname: Option<String>) -> Self {
103 self.hostname = hostname;
104 self
105 }
106
107 pub fn with_sync_token(mut self, sync_token: Option<String>) -> Self {
108 self.sync_token = sync_token;
109 self
110 }
111
112 pub fn with_local_database_url(mut self, url: impl Into<String>) -> Self {
113 self.local_database_url = Some(url.into());
114 self
115 }
116
117 pub fn build(self) -> SyncConfig {
118 SyncConfig {
119 direction: self.direction,
120 dry_run: self.dry_run,
121 verbose: self.verbose,
122 tenant_id: self.tenant_id,
123 api_url: self.api_url,
124 api_token: self.api_token,
125 services_path: self.services_path,
126 hostname: self.hostname,
127 sync_token: self.sync_token,
128 local_database_url: self.local_database_url,
129 }
130 }
131}
132
133impl SyncConfig {
134 pub fn builder(
135 tenant_id: impl Into<String>,
136 api_url: impl Into<String>,
137 api_token: impl Into<String>,
138 services_path: impl Into<String>,
139 ) -> SyncConfigBuilder {
140 SyncConfigBuilder::new(tenant_id, api_url, api_token, services_path)
141 }
142}
143
144#[derive(Clone, Debug, Serialize, Deserialize)]
145pub struct SyncOperationResult {
146 pub operation: String,
147 pub success: bool,
148 pub items_synced: usize,
149 pub items_skipped: usize,
150 pub errors: Vec<String>,
151 pub details: Option<serde_json::Value>,
152}
153
154impl SyncOperationResult {
155 pub fn success(operation: &str, items_synced: usize) -> Self {
156 Self {
157 operation: operation.to_string(),
158 success: true,
159 items_synced,
160 items_skipped: 0,
161 errors: vec![],
162 details: None,
163 }
164 }
165
166 pub fn with_details(mut self, details: serde_json::Value) -> Self {
167 self.details = Some(details);
168 self
169 }
170
171 pub fn dry_run(operation: &str, items_skipped: usize, details: serde_json::Value) -> Self {
172 Self {
173 operation: operation.to_string(),
174 success: true,
175 items_synced: 0,
176 items_skipped,
177 errors: vec![],
178 details: Some(details),
179 }
180 }
181}
182
183#[derive(Debug)]
184pub struct SyncService {
185 config: SyncConfig,
186 api_client: SyncApiClient,
187}
188
189impl SyncService {
190 pub fn new(config: SyncConfig) -> Self {
191 let api_client = SyncApiClient::new(&config.api_url, &config.api_token)
192 .with_direct_sync(config.hostname.clone(), config.sync_token.clone());
193 Self { config, api_client }
194 }
195
196 pub async fn sync_files(&self) -> SyncResult<SyncOperationResult> {
197 let service = FileSyncService::new(self.config.clone(), self.api_client.clone());
198 service.sync().await
199 }
200
201 pub async fn sync_database(&self) -> SyncResult<SyncOperationResult> {
202 let local_db_url = self.config.local_database_url.as_ref().ok_or_else(|| {
203 SyncError::MissingConfig("local_database_url not configured".to_string())
204 })?;
205
206 let cloud_db_url = self
207 .api_client
208 .get_database_url(&self.config.tenant_id)
209 .await
210 .map_err(|e| SyncError::ApiError {
211 status: 500,
212 message: format!("Failed to get cloud database URL: {e}"),
213 })?;
214
215 let service = DatabaseSyncService::new(
216 self.config.direction,
217 self.config.dry_run,
218 local_db_url,
219 &cloud_db_url,
220 );
221
222 service.sync().await
223 }
224
225 pub async fn sync_all(&self) -> SyncResult<Vec<SyncOperationResult>> {
226 let mut results = Vec::new();
227
228 let files_result = self.sync_files().await?;
229 results.push(files_result);
230
231 match self.sync_database().await {
232 Ok(db_result) => results.push(db_result),
233 Err(e) => {
234 tracing::warn!(error = %e, "Database sync failed");
235 results.push(SyncOperationResult {
236 operation: "database".to_string(),
237 success: false,
238 items_synced: 0,
239 items_skipped: 0,
240 errors: vec![e.to_string()],
241 details: None,
242 });
243 },
244 }
245
246 Ok(results)
247 }
248}