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 playbook_file = self
75                    .playbooks_path
76                    .join(&item.category)
77                    .join(format!("{}.md", &item.domain));
78
79                if playbook_file.exists() {
80                    std::fs::remove_file(&playbook_file)?;
81                    result.items_deleted += 1;
82                    info!("Deleted orphan playbook: {}", item.playbook_id);
83                }
84            }
85        } else {
86            result.items_skipped += diff.added.len();
87        }
88
89        Ok(result)
90    }
91
92    pub async fn sync_to_db(
93        &self,
94        diff: &PlaybooksDiffResult,
95        delete_orphans: bool,
96    ) -> Result<LocalSyncResult> {
97        let ingestion_service = PlaybookIngestionService::new(Arc::clone(&self.db));
98        let mut result = LocalSyncResult {
99            direction: "to_database".to_string(),
100            ..Default::default()
101        };
102
103        let source_id = SourceId::new("playbooks");
104        let report = ingestion_service
105            .ingest_directory(&self.playbooks_path, source_id, true)
106            .await?;
107
108        result.items_synced += report.files_processed;
109
110        for error in report.errors {
111            result.errors.push(error);
112        }
113
114        info!("Ingested {} playbooks", report.files_processed);
115
116        if delete_orphans && !diff.removed.is_empty() {
117            tracing::warn!(
118                count = diff.removed.len(),
119                "Playbook deletion from database not supported, skipping orphan removal"
120            );
121        }
122        result.items_skipped += diff.removed.len();
123
124        Ok(result)
125    }
126}