systemprompt_sync/local/
skills_sync.rs1use crate::diff::SkillsDiffCalculator;
2use crate::export::export_skill_to_disk;
3use crate::models::{LocalSyncResult, SkillsDiffResult};
4use anyhow::Result;
5use std::path::PathBuf;
6use std::sync::Arc;
7use systemprompt_agent::repository::content::SkillRepository;
8use systemprompt_agent::services::SkillIngestionService;
9use systemprompt_database::DatabaseProvider;
10use systemprompt_identifiers::{SkillId, SourceId};
11use tracing::info;
12
13#[derive(Debug)]
14pub struct SkillsLocalSync {
15 db: Arc<dyn DatabaseProvider>,
16 skills_path: PathBuf,
17}
18
19impl SkillsLocalSync {
20 pub fn new(db: Arc<dyn DatabaseProvider>, skills_path: PathBuf) -> Self {
21 Self { db, skills_path }
22 }
23
24 pub async fn calculate_diff(&self) -> Result<SkillsDiffResult> {
25 let calculator = SkillsDiffCalculator::new(Arc::clone(&self.db));
26 calculator.calculate_diff(&self.skills_path).await
27 }
28
29 pub async fn sync_to_disk(
30 &self,
31 diff: &SkillsDiffResult,
32 delete_orphans: bool,
33 ) -> Result<LocalSyncResult> {
34 let skill_repo = SkillRepository::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 skill_id = SkillId::new(&item.skill_id);
42 match skill_repo.get_by_skill_id(&skill_id).await? {
43 Some(skill) => {
44 export_skill_to_disk(&skill, &self.skills_path)?;
45 result.items_synced += 1;
46 info!("Exported modified skill: {}", item.skill_id);
47 },
48 None => {
49 result
50 .errors
51 .push(format!("Skill not found in DB: {}", item.skill_id));
52 },
53 }
54 }
55
56 for item in &diff.removed {
57 let skill_id = SkillId::new(&item.skill_id);
58 match skill_repo.get_by_skill_id(&skill_id).await? {
59 Some(skill) => {
60 export_skill_to_disk(&skill, &self.skills_path)?;
61 result.items_synced += 1;
62 info!("Created skill on disk: {}", item.skill_id);
63 },
64 None => {
65 result
66 .errors
67 .push(format!("Skill not found in DB: {}", item.skill_id));
68 },
69 }
70 }
71
72 if delete_orphans {
73 for item in &diff.added {
74 let skill_dir_name = item.skill_id.replace('_', "-");
75 let skill_dir = self.skills_path.join(&skill_dir_name);
76
77 if skill_dir.exists() {
78 std::fs::remove_dir_all(&skill_dir)?;
79 result.items_deleted += 1;
80 info!("Deleted orphan skill: {}", item.skill_id);
81 }
82 }
83 } else {
84 result.items_skipped += diff.added.len();
85 }
86
87 Ok(result)
88 }
89
90 pub async fn sync_to_db(
91 &self,
92 diff: &SkillsDiffResult,
93 delete_orphans: bool,
94 ) -> Result<LocalSyncResult> {
95 let ingestion_service = SkillIngestionService::new(Arc::clone(&self.db));
96 let mut result = LocalSyncResult {
97 direction: "to_database".to_string(),
98 ..Default::default()
99 };
100
101 let source_id = SourceId::new("skills");
102 let report = ingestion_service
103 .ingest_directory(&self.skills_path, source_id, true)
104 .await?;
105
106 result.items_synced += report.files_processed;
107
108 for error in report.errors {
109 result.errors.push(error);
110 }
111
112 info!("Ingested {} skills", report.files_processed);
113
114 if delete_orphans && !diff.removed.is_empty() {
115 tracing::warn!(
116 count = diff.removed.len(),
117 "Skill deletion from database not supported, skipping orphan removal"
118 );
119 }
120 result.items_skipped += diff.removed.len();
121
122 Ok(result)
123 }
124}