systemprompt_loader/
config_writer.rs1use std::collections::HashMap;
9use std::fs;
10use std::path::{Path, PathBuf};
11
12use systemprompt_models::services::AgentConfig;
13
14use crate::error::{ConfigWriteError, ConfigWriteResult};
15
16#[derive(Debug, Clone, Copy)]
17pub struct ConfigWriter;
18
19#[derive(serde::Serialize, serde::Deserialize)]
20struct AgentFileContent {
21 agents: HashMap<String, AgentConfig>,
22}
23
24impl ConfigWriter {
25 pub fn create_agent(agent: &AgentConfig, services_dir: &Path) -> ConfigWriteResult<PathBuf> {
26 let agents_dir = services_dir.join("agents");
27 fs::create_dir_all(&agents_dir).map_err(|e| ConfigWriteError::Io {
28 path: agents_dir.clone(),
29 source: e,
30 })?;
31
32 let agent_file = agents_dir.join(format!("{}.yaml", agent.name));
33
34 if agent_file.exists() {
35 return Err(ConfigWriteError::AgentFileExists(agent_file));
36 }
37
38 Self::write_agent_file(&agent_file, agent)?;
39
40 Ok(agent_file)
41 }
42
43 pub fn update_agent(
44 name: &str,
45 agent: &AgentConfig,
46 services_dir: &Path,
47 ) -> ConfigWriteResult<()> {
48 let agent_file = Self::find_agent_file(name, services_dir)?
49 .ok_or_else(|| ConfigWriteError::AgentNotFound(name.to_string()))?;
50
51 Self::write_agent_file(&agent_file, agent)
52 }
53
54 pub fn delete_agent(name: &str, services_dir: &Path) -> ConfigWriteResult<()> {
55 let agent_file = Self::find_agent_file(name, services_dir)?
56 .ok_or_else(|| ConfigWriteError::AgentNotFound(name.to_string()))?;
57
58 fs::remove_file(&agent_file).map_err(|e| ConfigWriteError::Io {
59 path: agent_file.clone(),
60 source: e,
61 })?;
62
63 let config_path = services_dir.join("config/config.yaml");
64 let include_path = format!("../agents/{name}.yaml");
65 Self::remove_include(&include_path, &config_path)
66 }
67
68 pub fn find_agent_file(name: &str, services_dir: &Path) -> ConfigWriteResult<Option<PathBuf>> {
69 let agents_dir = services_dir.join("agents");
70
71 if !agents_dir.exists() {
72 return Ok(None);
73 }
74
75 let expected_file = agents_dir.join(format!("{name}.yaml"));
76 if expected_file.exists() && Self::file_contains_agent(&expected_file, name)? {
77 return Ok(Some(expected_file));
78 }
79
80 for entry in fs::read_dir(&agents_dir).map_err(|e| ConfigWriteError::Io {
81 path: agents_dir.clone(),
82 source: e,
83 })? {
84 let path = entry
85 .map_err(|e| ConfigWriteError::Io {
86 path: agents_dir.clone(),
87 source: e,
88 })?
89 .path();
90
91 if path
92 .extension()
93 .is_some_and(|ext| ext == "yaml" || ext == "yml")
94 && Self::file_contains_agent(&path, name)?
95 {
96 return Ok(Some(path));
97 }
98 }
99
100 Ok(None)
101 }
102
103 fn file_contains_agent(path: &Path, agent_name: &str) -> ConfigWriteResult<bool> {
104 let content = fs::read_to_string(path).map_err(|e| ConfigWriteError::Io {
105 path: path.to_path_buf(),
106 source: e,
107 })?;
108
109 let parsed: AgentFileContent = serde_yaml::from_str(&content)?;
110
111 Ok(parsed.agents.contains_key(agent_name))
112 }
113
114 fn write_agent_file(path: &Path, agent: &AgentConfig) -> ConfigWriteResult<()> {
115 let mut agents = HashMap::new();
116 agents.insert(agent.name.clone(), agent.clone());
117
118 let content = AgentFileContent { agents };
119
120 let yaml = serde_yaml::to_string(&content)?;
121
122 let header = format!(
123 "# {} Configuration\n# {}\n\n",
124 agent.card.display_name, agent.card.description
125 );
126
127 fs::write(path, format!("{header}{yaml}")).map_err(|e| ConfigWriteError::Io {
128 path: path.to_path_buf(),
129 source: e,
130 })
131 }
132
133 fn remove_include(include_path: &str, config_path: &Path) -> ConfigWriteResult<()> {
134 let content = fs::read_to_string(config_path).map_err(|e| ConfigWriteError::Io {
135 path: config_path.to_path_buf(),
136 source: e,
137 })?;
138
139 let search_pattern = format!(" - {include_path}");
140 let quoted_pattern = format!(" - \"{include_path}\"");
141
142 let new_lines: Vec<&str> = content
143 .lines()
144 .filter(|line| *line != search_pattern && *line != quoted_pattern)
145 .collect();
146
147 fs::write(config_path, new_lines.join("\n")).map_err(|e| ConfigWriteError::Io {
148 path: config_path.to_path_buf(),
149 source: e,
150 })
151 }
152}