Skip to main content

systemprompt_sync/
lib.rs

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}