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