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 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}