systemprompt_sync/local/
skills_sync.rs1use 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}