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