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