systemprompt_sync/local/
playbooks_sync.rs1use 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}