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