Skip to main content

systemprompt_sync/local/
skills_sync.rs

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