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