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