Skip to main content

systemprompt_sync/local/
playbooks_sync.rs

1use crate::diff::PlaybooksDiffCalculator;
2use crate::export::export_playbook_to_disk;
3use crate::models::{LocalSyncResult, PlaybooksDiffResult};
4use anyhow::Result;
5use std::path::PathBuf;
6use std::sync::Arc;
7use systemprompt_agent::repository::content::PlaybookRepository;
8use systemprompt_agent::services::PlaybookIngestionService;
9use systemprompt_database::DatabaseProvider;
10use systemprompt_identifiers::{PlaybookId, SourceId};
11use tracing::info;
12
13#[derive(Debug)]
14pub struct PlaybooksLocalSync {
15    db: Arc<dyn DatabaseProvider>,
16    playbooks_path: PathBuf,
17}
18
19impl PlaybooksLocalSync {
20    pub fn new(db: Arc<dyn DatabaseProvider>, playbooks_path: PathBuf) -> Self {
21        Self { db, playbooks_path }
22    }
23
24    pub async fn calculate_diff(&self) -> Result<PlaybooksDiffResult> {
25        let calculator = PlaybooksDiffCalculator::new(Arc::clone(&self.db));
26        calculator.calculate_diff(&self.playbooks_path).await
27    }
28
29    pub async fn sync_to_disk(
30        &self,
31        diff: &PlaybooksDiffResult,
32        delete_orphans: bool,
33    ) -> Result<LocalSyncResult> {
34        let playbook_repo = PlaybookRepository::new(Arc::clone(&self.db));
35        let mut result = LocalSyncResult {
36            direction: "to_disk".to_string(),
37            ..Default::default()
38        };
39
40        for item in &diff.modified {
41            let playbook_id = PlaybookId::new(&item.playbook_id);
42            match playbook_repo.get_by_playbook_id(&playbook_id).await? {
43                Some(playbook) => {
44                    export_playbook_to_disk(&playbook, &self.playbooks_path)?;
45                    result.items_synced += 1;
46                    info!("Exported modified playbook: {}", item.playbook_id);
47                },
48                None => {
49                    result
50                        .errors
51                        .push(format!("Playbook not found in DB: {}", item.playbook_id));
52                },
53            }
54        }
55
56        for item in &diff.removed {
57            let playbook_id = PlaybookId::new(&item.playbook_id);
58            match playbook_repo.get_by_playbook_id(&playbook_id).await? {
59                Some(playbook) => {
60                    export_playbook_to_disk(&playbook, &self.playbooks_path)?;
61                    result.items_synced += 1;
62                    info!("Created playbook on disk: {}", item.playbook_id);
63                },
64                None => {
65                    result
66                        .errors
67                        .push(format!("Playbook not found in DB: {}", item.playbook_id));
68                },
69            }
70        }
71
72        if delete_orphans {
73            for item in &diff.added {
74                let domain_parts: Vec<&str> = item.domain.split('/').collect();
75                let mut file_dir = self.playbooks_path.join(&item.category);
76
77                for part in domain_parts
78                    .iter()
79                    .take(domain_parts.len().saturating_sub(1))
80                {
81                    file_dir = file_dir.join(part);
82                }
83
84                let filename = domain_parts.last().unwrap_or(&"");
85                let playbook_file = file_dir.join(format!("{}.md", filename));
86
87                if playbook_file.exists() {
88                    std::fs::remove_file(&playbook_file)?;
89
90                    let mut current = playbook_file.parent();
91                    while let Some(dir) = current {
92                        if dir == self.playbooks_path {
93                            break;
94                        }
95                        if let Ok(entries) = std::fs::read_dir(dir) {
96                            if entries.count() == 0 {
97                                let _ = std::fs::remove_dir(dir);
98                            } else {
99                                break;
100                            }
101                        }
102                        current = dir.parent();
103                    }
104
105                    result.items_deleted += 1;
106                    info!("Deleted orphan playbook: {}", item.playbook_id);
107                }
108            }
109        } else {
110            result.items_skipped += diff.added.len();
111        }
112
113        Ok(result)
114    }
115
116    pub async fn sync_to_db(
117        &self,
118        diff: &PlaybooksDiffResult,
119        delete_orphans: bool,
120    ) -> Result<LocalSyncResult> {
121        let ingestion_service = PlaybookIngestionService::new(Arc::clone(&self.db));
122        let mut result = LocalSyncResult {
123            direction: "to_database".to_string(),
124            ..Default::default()
125        };
126
127        let source_id = SourceId::new("playbooks");
128        let report = ingestion_service
129            .ingest_directory(&self.playbooks_path, source_id, true)
130            .await?;
131
132        result.items_synced += report.files_processed;
133
134        for error in report.errors {
135            result.errors.push(error);
136        }
137
138        info!("Ingested {} playbooks", report.files_processed);
139
140        if delete_orphans && !diff.removed.is_empty() {
141            tracing::warn!(
142                count = diff.removed.len(),
143                "Playbook deletion from database not supported, skipping orphan removal"
144            );
145        }
146        result.items_skipped += diff.removed.len();
147
148        Ok(result)
149    }
150}