Skip to main content

systemprompt_loader/
config_writer.rs

1use anyhow::{anyhow, Context, Result};
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        let config_path = services_dir.join("config/config.yaml");
38        let include_path = format!("../agents/{}.yaml", agent.name);
39        Self::add_include(&include_path, &config_path)?;
40
41        Ok(agent_file)
42    }
43
44    pub fn update_agent(name: &str, agent: &AgentConfig, services_dir: &Path) -> Result<()> {
45        let agent_file = Self::find_agent_file(name, services_dir)?
46            .ok_or_else(|| anyhow!("Agent '{}' not found in any configuration file", name))?;
47
48        Self::write_agent_file(&agent_file, agent)
49    }
50
51    pub fn delete_agent(name: &str, services_dir: &Path) -> Result<()> {
52        let agent_file = Self::find_agent_file(name, services_dir)?
53            .ok_or_else(|| anyhow!("Agent '{}' not found in any configuration file", name))?;
54
55        fs::remove_file(&agent_file)
56            .with_context(|| format!("Failed to delete agent file: {}", agent_file.display()))?;
57
58        let config_path = services_dir.join("config/config.yaml");
59        let include_path = format!("../agents/{}.yaml", name);
60        Self::remove_include(&include_path, &config_path)
61    }
62
63    pub fn find_agent_file(name: &str, services_dir: &Path) -> Result<Option<PathBuf>> {
64        let agents_dir = services_dir.join("agents");
65
66        if !agents_dir.exists() {
67            return Ok(None);
68        }
69
70        let expected_file = agents_dir.join(format!("{}.yaml", name));
71        if expected_file.exists() && Self::file_contains_agent(&expected_file, name)? {
72            return Ok(Some(expected_file));
73        }
74
75        for entry in fs::read_dir(&agents_dir)
76            .with_context(|| format!("Failed to read agents directory: {}", agents_dir.display()))?
77        {
78            let path = entry?.path();
79
80            if path
81                .extension()
82                .is_some_and(|ext| ext == "yaml" || ext == "yml")
83                && Self::file_contains_agent(&path, name)?
84            {
85                return Ok(Some(path));
86            }
87        }
88
89        Ok(None)
90    }
91
92    fn file_contains_agent(path: &Path, agent_name: &str) -> Result<bool> {
93        let content = fs::read_to_string(path)
94            .with_context(|| format!("Failed to read file: {}", path.display()))?;
95
96        let parsed: AgentFileContent = serde_yaml::from_str(&content)
97            .with_context(|| format!("Failed to parse YAML file: {}", path.display()))?;
98
99        Ok(parsed.agents.contains_key(agent_name))
100    }
101
102    fn write_agent_file(path: &Path, agent: &AgentConfig) -> Result<()> {
103        let mut agents = HashMap::new();
104        agents.insert(agent.name.clone(), agent.clone());
105
106        let content = AgentFileContent { agents };
107
108        let yaml = serde_yaml::to_string(&content).context("Failed to serialize agent to YAML")?;
109
110        let header = format!(
111            "# {} Configuration\n# {}\n\n",
112            agent.card.display_name, agent.card.description
113        );
114
115        fs::write(path, format!("{}{}", header, yaml))
116            .with_context(|| format!("Failed to write agent file: {}", path.display()))
117    }
118
119    pub fn add_include(include_path: &str, config_path: &Path) -> Result<()> {
120        let content = fs::read_to_string(config_path)
121            .with_context(|| format!("Failed to read config file: {}", config_path.display()))?;
122
123        if content.contains(include_path) {
124            return Ok(());
125        }
126
127        let lines: Vec<&str> = content.lines().collect();
128
129        for (i, line) in lines.iter().enumerate() {
130            if line.starts_with("includes:") {
131                let insert_pos = lines
132                    .iter()
133                    .enumerate()
134                    .skip(i + 1)
135                    .take_while(|(_, l)| l.starts_with("  - ") || l.trim().is_empty())
136                    .filter(|(_, l)| l.starts_with("  - "))
137                    .last()
138                    .map_or(i + 1, |(idx, _)| idx + 1);
139
140                let new_line = format!("  - {}", include_path);
141                let new_lines: Vec<&str> = lines[..insert_pos]
142                    .iter()
143                    .copied()
144                    .chain(std::iter::once(new_line.as_str()))
145                    .chain(lines[insert_pos..].iter().copied())
146                    .collect();
147
148                return fs::write(config_path, new_lines.join("\n")).with_context(|| {
149                    format!("Failed to write config file: {}", config_path.display())
150                });
151            }
152        }
153
154        fs::write(
155            config_path,
156            format!("includes:\n  - {}\n\n{}", include_path, content),
157        )
158        .with_context(|| format!("Failed to write config file: {}", config_path.display()))
159    }
160
161    fn remove_include(include_path: &str, config_path: &Path) -> Result<()> {
162        let content = fs::read_to_string(config_path)
163            .with_context(|| format!("Failed to read config file: {}", config_path.display()))?;
164
165        let search_pattern = format!("  - {}", include_path);
166        let quoted_pattern = format!("  - \"{}\"", include_path);
167
168        let new_lines: Vec<&str> = content
169            .lines()
170            .filter(|line| *line != search_pattern && *line != quoted_pattern)
171            .collect();
172
173        fs::write(config_path, new_lines.join("\n"))
174            .with_context(|| format!("Failed to write config file: {}", config_path.display()))
175    }
176}