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