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