systemprompt_loader/
config_writer.rs1use anyhow::{Context, Result, anyhow};
2use std::collections::HashMap;
3use std::fs;
4use std::path::{Path, PathBuf};
5
6use systemprompt_models::services::AgentConfig;
7
8#[derive(Debug, Clone, Copy)]
9pub struct ConfigWriter;
10
11#[derive(serde::Serialize, serde::Deserialize)]
12struct AgentFileContent {
13 agents: HashMap<String, AgentConfig>,
14}
15
16impl ConfigWriter {
17 pub fn create_agent(agent: &AgentConfig, services_dir: &Path) -> Result<PathBuf> {
18 let agents_dir = services_dir.join("agents");
19 fs::create_dir_all(&agents_dir).with_context(|| {
20 format!(
21 "Failed to create agents directory: {}",
22 agents_dir.display()
23 )
24 })?;
25
26 let agent_file = agents_dir.join(format!("{}.yaml", agent.name));
27
28 if agent_file.exists() {
29 return Err(anyhow!(
30 "Agent file already exists: {}. Use 'agents edit' to modify.",
31 agent_file.display()
32 ));
33 }
34
35 Self::write_agent_file(&agent_file, agent)?;
36
37 Ok(agent_file)
38 }
39
40 pub fn update_agent(name: &str, agent: &AgentConfig, services_dir: &Path) -> Result<()> {
41 let agent_file = Self::find_agent_file(name, services_dir)?
42 .ok_or_else(|| anyhow!("Agent '{}' not found in any configuration file", name))?;
43
44 Self::write_agent_file(&agent_file, agent)
45 }
46
47 pub fn delete_agent(name: &str, services_dir: &Path) -> Result<()> {
48 let agent_file = Self::find_agent_file(name, services_dir)?
49 .ok_or_else(|| anyhow!("Agent '{}' not found in any configuration file", name))?;
50
51 fs::remove_file(&agent_file)
52 .with_context(|| format!("Failed to delete agent file: {}", agent_file.display()))?;
53
54 let config_path = services_dir.join("config/config.yaml");
55 let include_path = format!("../agents/{}.yaml", name);
56 Self::remove_include(&include_path, &config_path)
57 }
58
59 pub fn find_agent_file(name: &str, services_dir: &Path) -> Result<Option<PathBuf>> {
60 let agents_dir = services_dir.join("agents");
61
62 if !agents_dir.exists() {
63 return Ok(None);
64 }
65
66 let expected_file = agents_dir.join(format!("{}.yaml", name));
67 if expected_file.exists() && Self::file_contains_agent(&expected_file, name)? {
68 return Ok(Some(expected_file));
69 }
70
71 for entry in fs::read_dir(&agents_dir)
72 .with_context(|| format!("Failed to read agents directory: {}", agents_dir.display()))?
73 {
74 let path = entry?.path();
75
76 if path
77 .extension()
78 .is_some_and(|ext| ext == "yaml" || ext == "yml")
79 && Self::file_contains_agent(&path, name)?
80 {
81 return Ok(Some(path));
82 }
83 }
84
85 Ok(None)
86 }
87
88 fn file_contains_agent(path: &Path, agent_name: &str) -> Result<bool> {
89 let content = fs::read_to_string(path)
90 .with_context(|| format!("Failed to read file: {}", path.display()))?;
91
92 let parsed: AgentFileContent = serde_yaml::from_str(&content)
93 .with_context(|| format!("Failed to parse YAML file: {}", path.display()))?;
94
95 Ok(parsed.agents.contains_key(agent_name))
96 }
97
98 fn write_agent_file(path: &Path, agent: &AgentConfig) -> Result<()> {
99 let mut agents = HashMap::new();
100 agents.insert(agent.name.clone(), agent.clone());
101
102 let content = AgentFileContent { agents };
103
104 let yaml = serde_yaml::to_string(&content).context("Failed to serialize agent to YAML")?;
105
106 let header = format!(
107 "# {} Configuration\n# {}\n\n",
108 agent.card.display_name, agent.card.description
109 );
110
111 fs::write(path, format!("{}{}", header, yaml))
112 .with_context(|| format!("Failed to write agent file: {}", path.display()))
113 }
114
115 fn remove_include(include_path: &str, config_path: &Path) -> Result<()> {
116 let content = fs::read_to_string(config_path)
117 .with_context(|| format!("Failed to read config file: {}", config_path.display()))?;
118
119 let search_pattern = format!(" - {}", include_path);
120 let quoted_pattern = format!(" - \"{}\"", include_path);
121
122 let new_lines: Vec<&str> = content
123 .lines()
124 .filter(|line| *line != search_pattern && *line != quoted_pattern)
125 .collect();
126
127 fs::write(config_path, new_lines.join("\n"))
128 .with_context(|| format!("Failed to write config file: {}", config_path.display()))
129 }
130}