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 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: LocalSyncDirection::ToDisk,
36            ..Default::default()
37        };
38
39        for item in &diff.modified {
40            match skill_repo.get_by_skill_id(&item.skill_id).await? {
41                Some(skill) => {
42                    export_skill_to_disk(&skill, &self.skills_path)?;
43                    result.items_synced += 1;
44                    info!("Exported modified skill: {}", item.skill_id);
45                },
46                None => {
47                    result
48                        .errors
49                        .push(format!("Skill not found in DB: {}", item.skill_id));
50                },
51            }
52        }
53
54        for item in &diff.removed {
55            match skill_repo.get_by_skill_id(&item.skill_id).await? {
56                Some(skill) => {
57                    export_skill_to_disk(&skill, &self.skills_path)?;
58                    result.items_synced += 1;
59                    info!("Created skill on disk: {}", item.skill_id);
60                },
61                None => {
62                    result
63                        .errors
64                        .push(format!("Skill not found in DB: {}", item.skill_id));
65                },
66            }
67        }
68
69        if delete_orphans {
70            for item in &diff.added {
71                let skill_dir_name = item.skill_id.as_str().replace('_', "-");
72                let skill_dir = self.skills_path.join(&skill_dir_name);
73
74                if skill_dir.exists() {
75                    std::fs::remove_dir_all(&skill_dir)?;
76                    result.items_deleted += 1;
77                    info!("Deleted orphan skill: {}", item.skill_id);
78                }
79            }
80        } else {
81            result.items_skipped += diff.added.len();
82        }
83
84        Ok(result)
85    }
86
87    pub async fn sync_to_db(
88        &self,
89        diff: &SkillsDiffResult,
90        delete_orphans: bool,
91    ) -> Result<LocalSyncResult> {
92        let ingestion_service = SkillIngestionService::new(&self.db)?;
93        let mut result = LocalSyncResult {
94            direction: LocalSyncDirection::ToDatabase,
95            ..Default::default()
96        };
97
98        let source_id = SourceId::new("skills");
99        let report = ingestion_service
100            .ingest_directory(&self.skills_path, source_id, true)
101            .await?;
102
103        result.items_synced += report.files_processed;
104
105        for error in report.errors {
106            result.errors.push(error);
107        }
108
109        info!("Ingested {} skills", report.files_processed);
110
111        if delete_orphans && !diff.removed.is_empty() {
112            tracing::warn!(
113                count = diff.removed.len(),
114                "Skill deletion from database not supported, skipping orphan removal"
115            );
116        }
117        result.items_skipped += diff.removed.len();
118
119        Ok(result)
120    }
121}