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