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 systemprompt_agent::repository::content::SkillRepository;
7use systemprompt_agent::services::SkillIngestionService;
8use systemprompt_database::DbPool;
9use systemprompt_identifiers::{SkillId, SourceId};
10use tracing::info;
11
12#[derive(Debug)]
13pub struct SkillsLocalSync {
14    db: DbPool,
15    skills_path: PathBuf,
16}
17
18impl SkillsLocalSync {
19    pub const fn new(db: DbPool, skills_path: PathBuf) -> Self {
20        Self { db, skills_path }
21    }
22
23    pub async fn calculate_diff(&self) -> Result<SkillsDiffResult> {
24        let calculator = SkillsDiffCalculator::new(&self.db)?;
25        calculator.calculate_diff(&self.skills_path).await
26    }
27
28    pub async fn sync_to_disk(
29        &self,
30        diff: &SkillsDiffResult,
31        delete_orphans: bool,
32    ) -> Result<LocalSyncResult> {
33        let skill_repo = SkillRepository::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 skill_id = SkillId::new(&item.skill_id);
41            match skill_repo.get_by_skill_id(&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            let skill_id = SkillId::new(&item.skill_id);
57            match skill_repo.get_by_skill_id(&skill_id).await? {
58                Some(skill) => {
59                    export_skill_to_disk(&skill, &self.skills_path)?;
60                    result.items_synced += 1;
61                    info!("Created skill on disk: {}", item.skill_id);
62                },
63                None => {
64                    result
65                        .errors
66                        .push(format!("Skill not found in DB: {}", item.skill_id));
67                },
68            }
69        }
70
71        if delete_orphans {
72            for item in &diff.added {
73                let skill_dir_name = item.skill_id.replace('_', "-");
74                let skill_dir = self.skills_path.join(&skill_dir_name);
75
76                if skill_dir.exists() {
77                    std::fs::remove_dir_all(&skill_dir)?;
78                    result.items_deleted += 1;
79                    info!("Deleted orphan skill: {}", item.skill_id);
80                }
81            }
82        } else {
83            result.items_skipped += diff.added.len();
84        }
85
86        Ok(result)
87    }
88
89    pub async fn sync_to_db(
90        &self,
91        diff: &SkillsDiffResult,
92        delete_orphans: bool,
93    ) -> Result<LocalSyncResult> {
94        let ingestion_service = SkillIngestionService::new(&self.db)?;
95        let mut result = LocalSyncResult {
96            direction: "to_database".to_string(),
97            ..Default::default()
98        };
99
100        let source_id = SourceId::new("skills");
101        let report = ingestion_service
102            .ingest_directory(&self.skills_path, 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}