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