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