Skip to main content

systemprompt_sync/local/
skills_sync.rs

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