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