use std::path::Path;
use crate::adapters::{
HooksAdapter, RulesAdapter, SkillsAdapter, ToolAdapter, all_adapters, all_hooks_adapters,
all_rules_adapters, all_skills_adapters, find_adapter, find_hooks_adapter, find_rules_adapter,
find_skills_adapter,
};
use crate::config::{HooksConfig, McpConfig};
use crate::error::LorumError;
trait AdapterName {
fn adapter_name(&self) -> &str;
}
impl AdapterName for dyn ToolAdapter {
fn adapter_name(&self) -> &str {
ToolAdapter::name(self)
}
}
impl AdapterName for dyn RulesAdapter {
fn adapter_name(&self) -> &str {
RulesAdapter::name(self)
}
}
impl AdapterName for dyn HooksAdapter {
fn adapter_name(&self) -> &str {
HooksAdapter::name(self)
}
}
impl AdapterName for dyn SkillsAdapter {
fn adapter_name(&self) -> &str {
SkillsAdapter::name(self)
}
}
fn dry_run_specified<A: AdapterName + ?Sized + 'static, T, R>(
tool_names: &[String],
find_fn: impl Fn(&str) -> Option<&'static A>,
read_fn: impl Fn(&A) -> Result<T, LorumError>,
ok_fn: impl Fn(String, T) -> R,
err_fn: impl Fn(String, LorumError) -> R,
) -> Vec<R> {
let mut results = Vec::new();
for name in tool_names {
match find_fn(name) {
Some(adapter) => {
let adapter_name = adapter.adapter_name().to_string();
match read_fn(adapter) {
Ok(current) => results.push(ok_fn(adapter_name, current)),
Err(e) => results.push(err_fn(adapter_name, e)),
}
}
None => {
let err = LorumError::AdapterNotFound { name: name.clone() };
results.push(err_fn(name.clone(), err));
}
}
}
results
}
pub trait SyncResultItem {
fn tool(&self) -> &str;
fn success(&self) -> bool;
fn error(&self) -> Option<&str>;
}
impl SyncResultItem for SyncResult {
fn tool(&self) -> &str {
&self.tool
}
fn success(&self) -> bool {
self.success
}
fn error(&self) -> Option<&str> {
self.error.as_deref()
}
}
impl SyncResultItem for RulesSyncResult {
fn tool(&self) -> &str {
&self.tool
}
fn success(&self) -> bool {
self.success
}
fn error(&self) -> Option<&str> {
self.error.as_deref()
}
}
impl SyncResultItem for HooksSyncResult {
fn tool(&self) -> &str {
&self.tool
}
fn success(&self) -> bool {
self.success
}
fn error(&self) -> Option<&str> {
self.error.as_deref()
}
}
impl SyncResultItem for SkillsSyncResult {
fn tool(&self) -> &str {
&self.tool
}
fn success(&self) -> bool {
self.success
}
fn error(&self) -> Option<&str> {
self.error.as_deref()
}
}
pub fn summarize_sync_results<T: SyncResultItem>(results: &[T]) -> String {
let total = results.len();
let ok = results.iter().filter(|r| r.success()).count();
let failed = total - ok;
if failed == 0 {
format!("All {total} tools synced successfully.")
} else {
let names: Vec<_> = results
.iter()
.filter(|r| !r.success())
.map(|r| r.tool())
.collect();
format!("{ok}/{total} tools synced. Failed: {}", names.join(", "))
}
}
#[derive(Debug)]
pub struct SyncResult {
pub tool: String,
pub success: bool,
pub servers_synced: usize,
pub error: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ConfigDiff {
pub added: Vec<String>,
pub removed: Vec<String>,
pub modified: Vec<String>,
pub unchanged: Vec<String>,
}
impl ConfigDiff {
pub fn is_empty(&self) -> bool {
self.added.is_empty() && self.removed.is_empty() && self.modified.is_empty()
}
pub fn change_count(&self) -> usize {
self.added.len() + self.removed.len() + self.modified.len()
}
}
#[derive(Debug)]
pub struct DryRunResult {
pub tool: String,
pub success: bool,
pub diff: Option<ConfigDiff>,
pub error: Option<String>,
}
pub fn sync_all(mcp_config: &McpConfig) -> Vec<SyncResult> {
let mut results = Vec::new();
for adapter in all_adapters() {
let result = sync_tool(adapter.as_ref(), mcp_config);
results.push(result);
}
results
}
pub fn sync_tools(mcp_config: &McpConfig, tool_names: &[String]) -> Vec<SyncResult> {
let mut results = Vec::new();
for name in tool_names {
match find_adapter(name) {
Some(adapter) => results.push(sync_tool(adapter, mcp_config)),
None => {
let err = LorumError::AdapterNotFound { name: name.clone() };
results.push(SyncResult {
tool: name.clone(),
success: false,
servers_synced: 0,
error: Some(err.to_string()),
})
}
}
}
results
}
fn sync_tool(adapter: &dyn ToolAdapter, mcp_config: &McpConfig) -> SyncResult {
let name = adapter.name().to_string();
for path in adapter.config_paths() {
if path.exists() {
if let Err(e) = crate::backup::create_backup(&name, &path) {
return SyncResult {
tool: name,
success: false,
servers_synced: 0,
error: Some(format!("backup failed: {e}")),
};
}
}
}
match adapter.write_mcp(mcp_config) {
Ok(()) => SyncResult {
tool: name,
success: true,
servers_synced: mcp_config.servers.len(),
error: None,
},
Err(e) => SyncResult {
tool: name,
success: false,
servers_synced: 0,
error: Some(e.to_string()),
},
}
}
pub fn dry_run_all(mcp_config: &McpConfig) -> Vec<DryRunResult> {
let mut results = Vec::new();
for adapter in all_adapters() {
let name = adapter.name().to_string();
match adapter.read_mcp() {
Ok(current) => results.push(DryRunResult {
tool: name,
success: true,
diff: Some(compute_diff(¤t, mcp_config)),
error: None,
}),
Err(e) => results.push(DryRunResult {
tool: name,
success: false,
diff: None,
error: Some(e.to_string()),
}),
}
}
results
}
pub fn dry_run_tools(mcp_config: &McpConfig, tool_names: &[String]) -> Vec<DryRunResult> {
dry_run_specified(
tool_names,
find_adapter,
|a| a.read_mcp(),
|tool, current| DryRunResult {
tool,
success: true,
diff: Some(compute_diff(¤t, mcp_config)),
error: None,
},
|tool, e| DryRunResult {
tool,
success: false,
diff: None,
error: Some(e.to_string()),
},
)
}
pub fn compute_diff(current: &McpConfig, target: &McpConfig) -> ConfigDiff {
let mut added = Vec::new();
let mut removed = Vec::new();
let mut modified = Vec::new();
let mut unchanged = Vec::new();
for name in current.servers.keys() {
if !target.servers.contains_key(name) {
removed.push(name.clone());
} else if current.servers.get(name) != target.servers.get(name) {
modified.push(name.clone());
} else {
unchanged.push(name.clone());
}
}
for name in target.servers.keys() {
if !current.servers.contains_key(name) {
added.push(name.clone());
}
}
ConfigDiff {
added,
removed,
modified,
unchanged,
}
}
#[derive(Debug)]
pub struct RulesSyncResult {
pub tool: String,
pub success: bool,
pub error: Option<String>,
}
#[derive(Debug)]
pub struct RulesDryRunResult {
pub tool: String,
pub success: bool,
pub needs_update: bool,
pub error: Option<String>,
}
pub fn sync_rules_all(project_root: &Path, content: &str) -> Vec<RulesSyncResult> {
let mut results = Vec::new();
for adapter in all_rules_adapters() {
let result = sync_rules_adapter(adapter.as_ref(), project_root, content);
results.push(result);
}
results
}
pub fn sync_rules_tools(
project_root: &Path,
content: &str,
tool_names: &[String],
) -> Vec<RulesSyncResult> {
let mut results = Vec::new();
for name in tool_names {
match find_rules_adapter(name) {
Some(adapter) => {
let result = sync_rules_adapter(adapter, project_root, content);
results.push(result);
}
None => {
let err = LorumError::AdapterNotFound { name: name.clone() };
results.push(RulesSyncResult {
tool: name.clone(),
success: false,
error: Some(err.to_string()),
});
}
}
}
results
}
fn sync_rules_adapter(
adapter: &dyn RulesAdapter,
project_root: &Path,
content: &str,
) -> RulesSyncResult {
let name = adapter.name().to_string();
let path = adapter.rules_path(project_root);
if path.exists() {
if let Err(e) = crate::backup::create_backup(&name, &path) {
return RulesSyncResult {
tool: name,
success: false,
error: Some(format!("backup failed: {e}")),
};
}
}
match adapter.write_rules(project_root, content) {
Ok(()) => RulesSyncResult {
tool: name,
success: true,
error: None,
},
Err(e) => RulesSyncResult {
tool: name,
success: false,
error: Some(e.to_string()),
},
}
}
pub fn dry_run_rules_all(project_root: &Path, content: &str) -> Vec<RulesDryRunResult> {
let mut results = Vec::new();
for adapter in all_rules_adapters() {
let name = adapter.name().to_string();
match adapter.read_rules(project_root) {
Ok(current) => {
let needs_update = current.as_deref() != Some(content);
results.push(RulesDryRunResult {
tool: name,
success: true,
needs_update,
error: None,
});
}
Err(e) => results.push(RulesDryRunResult {
tool: name,
success: false,
needs_update: false,
error: Some(e.to_string()),
}),
}
}
results
}
pub fn dry_run_rules_tools(
project_root: &Path,
content: &str,
tool_names: &[String],
) -> Vec<RulesDryRunResult> {
dry_run_specified(
tool_names,
find_rules_adapter,
|a| a.read_rules(project_root),
|tool, current| {
let needs_update = current.as_deref() != Some(content);
RulesDryRunResult {
tool,
success: true,
needs_update,
error: None,
}
},
|tool, e| RulesDryRunResult {
tool,
success: false,
needs_update: false,
error: Some(e.to_string()),
},
)
}
#[derive(Debug)]
pub struct HooksSyncResult {
pub tool: String,
pub success: bool,
pub error: Option<String>,
}
#[derive(Debug)]
pub struct HooksDryRunResult {
pub tool: String,
pub success: bool,
pub needs_update: bool,
pub error: Option<String>,
}
pub fn sync_hooks_all(hooks_config: &HooksConfig) -> Vec<HooksSyncResult> {
let mut results = Vec::new();
for adapter in all_hooks_adapters() {
let result = sync_hooks_adapter(adapter.as_ref(), hooks_config);
results.push(result);
}
results
}
pub fn sync_hooks_tools(hooks_config: &HooksConfig, tool_names: &[String]) -> Vec<HooksSyncResult> {
let mut results = Vec::new();
for name in tool_names {
match find_hooks_adapter(name) {
Some(adapter) => {
let result = sync_hooks_adapter(adapter, hooks_config);
results.push(result);
}
None => {
let err = LorumError::AdapterNotFound { name: name.clone() };
results.push(HooksSyncResult {
tool: name.clone(),
success: false,
error: Some(err.to_string()),
});
}
}
}
results
}
fn sync_hooks_adapter(adapter: &dyn HooksAdapter, hooks_config: &HooksConfig) -> HooksSyncResult {
let name = adapter.name().to_string();
for path in adapter.config_paths() {
if path.exists() {
if let Err(e) = crate::backup::create_backup(&name, &path) {
return HooksSyncResult {
tool: name,
success: false,
error: Some(format!("backup failed: {e}")),
};
}
}
}
match adapter.write_hooks(hooks_config) {
Ok(()) => HooksSyncResult {
tool: name,
success: true,
error: None,
},
Err(e) => HooksSyncResult {
tool: name,
success: false,
error: Some(e.to_string()),
},
}
}
pub fn dry_run_hooks_all(hooks_config: &HooksConfig) -> Vec<HooksDryRunResult> {
let mut results = Vec::new();
for adapter in all_hooks_adapters() {
let name = adapter.name().to_string();
match adapter.read_hooks() {
Ok(current) => {
let needs_update = current != *hooks_config;
results.push(HooksDryRunResult {
tool: name,
success: true,
needs_update,
error: None,
});
}
Err(e) => results.push(HooksDryRunResult {
tool: name,
success: false,
needs_update: false,
error: Some(e.to_string()),
}),
}
}
results
}
pub fn dry_run_hooks_tools(
hooks_config: &HooksConfig,
tool_names: &[String],
) -> Vec<HooksDryRunResult> {
dry_run_specified(
tool_names,
find_hooks_adapter,
|a| a.read_hooks(),
|tool, current| HooksDryRunResult {
tool,
success: true,
needs_update: current != *hooks_config,
error: None,
},
|tool, e| HooksDryRunResult {
tool,
success: false,
needs_update: false,
error: Some(e.to_string()),
},
)
}
#[derive(Debug)]
pub struct SkillsSyncResult {
pub tool: String,
pub success: bool,
pub skills_synced: usize,
pub error: Option<String>,
}
#[derive(Debug)]
pub struct SkillsDryRunResult {
pub tool: String,
pub success: bool,
pub skills_to_update: usize,
pub skills_up_to_date: usize,
pub skills_to_remove: usize,
pub error: Option<String>,
}
pub fn sync_skills_all(skills_dir: &std::path::Path) -> Vec<SkillsSyncResult> {
let mut results = Vec::new();
for adapter in all_skills_adapters() {
let result = sync_skills_adapter(adapter.as_ref(), skills_dir);
results.push(result);
}
results
}
pub fn sync_skills_tools(
skills_dir: &std::path::Path,
tool_names: &[String],
) -> Vec<SkillsSyncResult> {
let mut results = Vec::new();
for name in tool_names {
match find_skills_adapter(name) {
Some(adapter) => {
let result = sync_skills_adapter(adapter, skills_dir);
results.push(result);
}
None => {
let err = LorumError::AdapterNotFound { name: name.clone() };
results.push(SkillsSyncResult {
tool: name.clone(),
success: false,
skills_synced: 0,
error: Some(err.to_string()),
});
}
}
}
results
}
fn sync_skills_adapter(
adapter: &dyn SkillsAdapter,
skills_dir: &std::path::Path,
) -> SkillsSyncResult {
let name = adapter.name().to_string();
let source_skills = match crate::skills::scan_skills_dir(skills_dir) {
Ok(s) => s,
Err(e) => {
return SkillsSyncResult {
tool: name,
success: false,
skills_synced: 0,
error: Some(e.to_string()),
};
}
};
let mut synced = 0usize;
for skill in &source_skills {
let skill_name = &skill.manifest.name;
if let Some(base) = adapter.skills_base_dir() {
let target = base.join(skill_name);
if target.exists() {
let ts = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
let mut backup = base.join(format!("{skill_name}.backup-{ts}"));
let mut counter = 1u32;
while backup.exists() {
backup = base.join(format!("{skill_name}.backup-{ts}-{counter}"));
counter += 1;
}
if let Err(e) = std::fs::rename(&target, &backup) {
return SkillsSyncResult {
tool: name.clone(),
success: false,
skills_synced: 0,
error: Some(format!("backup failed for skill {skill_name}: {e}")),
};
}
}
}
match adapter.write_skill(skill_name, &skill.dir_path) {
Ok(()) => synced += 1,
Err(e) => {
return SkillsSyncResult {
tool: name.clone(),
success: false,
skills_synced: synced,
error: Some(format!("failed to sync skill {skill_name} to {name}: {e}")),
};
}
}
}
let total = source_skills.len();
let success = synced == total;
let error = if success {
None
} else {
Some(format!("only {synced}/{total} skills synced"))
};
SkillsSyncResult {
tool: name,
success,
skills_synced: synced,
error,
}
}
pub fn dry_run_skills_all(skills_dir: &std::path::Path) -> Vec<SkillsDryRunResult> {
let mut results = Vec::new();
for adapter in all_skills_adapters() {
let result = dry_run_skills_adapter(adapter.as_ref(), skills_dir);
results.push(result);
}
results
}
pub fn dry_run_skills_tools(
skills_dir: &std::path::Path,
tool_names: &[String],
) -> Vec<SkillsDryRunResult> {
dry_run_specified(
tool_names,
find_skills_adapter,
|a| Ok::<_, LorumError>(dry_run_skills_adapter(a, skills_dir)),
|_tool, result| result,
|tool, e| SkillsDryRunResult {
tool,
success: false,
skills_to_update: 0,
skills_up_to_date: 0,
skills_to_remove: 0,
error: Some(e.to_string()),
},
)
}
fn dry_run_skills_adapter(
adapter: &dyn SkillsAdapter,
skills_dir: &std::path::Path,
) -> SkillsDryRunResult {
let name = adapter.name().to_string();
let source_skills = match crate::skills::scan_skills_dir(skills_dir) {
Ok(s) => s,
Err(e) => {
return SkillsDryRunResult {
tool: name,
success: false,
skills_to_update: 0,
skills_up_to_date: 0,
skills_to_remove: 0,
error: Some(e.to_string()),
};
}
};
let target_skills = match adapter.read_skills() {
Ok(s) => s,
Err(e) => {
return SkillsDryRunResult {
tool: name,
success: false,
skills_to_update: 0,
skills_up_to_date: 0,
skills_to_remove: 0,
error: Some(e.to_string()),
};
}
};
let mut to_update = 0usize;
let mut up_to_date = 0usize;
for source in &source_skills {
let source_name = &source.manifest.name;
let target = target_skills
.iter()
.find(|t| t.manifest.name == *source_name);
match target {
Some(t) => {
if t.content != source.content {
to_update += 1;
} else {
up_to_date += 1;
}
}
None => {
to_update += 1;
}
}
}
let to_remove = target_skills
.iter()
.filter(|t| {
!source_skills
.iter()
.any(|s| s.manifest.name == t.manifest.name)
})
.count();
SkillsDryRunResult {
tool: name,
success: true,
skills_to_update: to_update,
skills_up_to_date: up_to_date,
skills_to_remove: to_remove,
error: None,
}
}
#[cfg(test)]
mod tests;