systemprompt_cli/commands/admin/config/
config_section.rs1use std::path::PathBuf;
15
16use anyhow::{Context, Result};
17use schemars::JsonSchema;
18use serde::{Deserialize, Serialize};
19use systemprompt_config::ProfileBootstrap;
20
21use super::rate_limit_types::ResetChange;
22
23pub const PROVIDERS_FILE_RELATIVE: &str = "ai/providers.yaml";
29pub const GATEWAY_FILE_RELATIVE: &str = "ai/gateway.yaml";
30pub const PROVIDERS_INCLUDE_RELATIVE: &str = "../ai/providers.yaml";
31pub const GATEWAY_INCLUDE_RELATIVE: &str = "../ai/gateway.yaml";
32
33#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
34pub struct ValidateOutput {
35 pub valid: bool,
36 pub errors: Vec<String>,
37 pub warnings: Vec<String>,
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
41pub struct ExportOutput {
42 pub format: String,
43 pub path: String,
44 pub message: String,
45}
46
47#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
48pub struct ImportOutput {
49 pub path: String,
50 pub changes: Vec<ResetChange>,
51 pub message: String,
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
55pub struct DiffOutput {
56 pub source: String,
57 pub differences: Vec<DiffEntry>,
58 pub identical: bool,
59}
60
61#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
62pub struct DiffEntry {
63 pub field: String,
64 pub current: String,
65 pub other: String,
66}
67
68#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
69pub struct ConfigFileInfo {
70 pub path: String,
71 pub section: String,
72 pub exists: bool,
73 pub valid: bool,
74 pub error: Option<String>,
75}
76
77#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
78pub struct ConfigListOutput {
79 pub total: usize,
80 pub valid: usize,
81 pub invalid: usize,
82 pub files: Vec<ConfigFileInfo>,
83}
84
85#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
86pub struct ConfigValidateOutput {
87 pub files: Vec<ConfigFileInfo>,
88 pub all_valid: bool,
89}
90
91#[derive(Debug, Clone, Copy, PartialEq, Eq)]
92pub enum ConfigSection {
93 Ai,
94 Providers,
95 Gateway,
96 Content,
97 Web,
98 Scheduler,
99 Agents,
100 Mcp,
101 Skills,
102 Profile,
103 Services,
104}
105
106impl ConfigSection {
107 pub const fn all() -> &'static [Self] {
108 &[
109 Self::Profile,
110 Self::Services,
111 Self::Ai,
112 Self::Providers,
113 Self::Gateway,
114 Self::Content,
115 Self::Web,
116 Self::Scheduler,
117 Self::Agents,
118 Self::Mcp,
119 Self::Skills,
120 ]
121 }
122
123 pub fn file_path(self) -> Result<PathBuf> {
124 let profile = ProfileBootstrap::get()?;
125 match self {
126 Self::Ai => Ok(PathBuf::from(&profile.paths.services).join("ai/config.yaml")),
127 Self::Providers => {
128 Ok(PathBuf::from(&profile.paths.services).join(PROVIDERS_FILE_RELATIVE))
129 },
130 Self::Gateway => Ok(PathBuf::from(&profile.paths.services).join(GATEWAY_FILE_RELATIVE)),
131 Self::Content => Ok(PathBuf::from(&profile.paths.services).join("content/config.yaml")),
132 Self::Web => Ok(PathBuf::from(&profile.paths.services).join("web/config.yaml")),
133 Self::Scheduler => {
134 Ok(PathBuf::from(&profile.paths.services).join("scheduler/config.yaml"))
135 },
136 Self::Agents => Ok(PathBuf::from(&profile.paths.services).join("agents/config.yaml")),
137 Self::Mcp => Ok(PathBuf::from(&profile.paths.services).join("mcp/config.yaml")),
138 Self::Skills => Ok(PathBuf::from(&profile.paths.services).join("skills/config.yaml")),
139 Self::Profile => Ok(PathBuf::from(ProfileBootstrap::get_path()?)),
140 Self::Services => Ok(PathBuf::from(&profile.paths.services).join("config/config.yaml")),
141 }
142 }
143
144 pub fn all_files(self) -> Result<Vec<PathBuf>> {
145 let profile = ProfileBootstrap::get()?;
146 let services_path = PathBuf::from(&profile.paths.services);
147
148 match self {
149 Self::Profile => Ok(vec![PathBuf::from(ProfileBootstrap::get_path()?)]),
150 Self::Services => Ok(vec![services_path.join("config/config.yaml")]),
151 Self::Ai => Ok(Self::collect_yaml_files(&services_path.join("ai"))?
154 .into_iter()
155 .filter(|p| {
156 p != &services_path.join(PROVIDERS_FILE_RELATIVE)
157 && p != &services_path.join(GATEWAY_FILE_RELATIVE)
158 })
159 .collect()),
160 Self::Providers => Ok(vec![services_path.join(PROVIDERS_FILE_RELATIVE)]),
161 Self::Gateway => Ok(vec![services_path.join(GATEWAY_FILE_RELATIVE)]),
162 Self::Content => Self::collect_yaml_files(&services_path.join("content")),
163 Self::Web => Self::collect_yaml_files(&services_path.join("web")),
164 Self::Scheduler => Self::collect_yaml_files(&services_path.join("scheduler")),
165 Self::Agents => Self::collect_yaml_files(&services_path.join("agents")),
166 Self::Mcp => Self::collect_yaml_files(&services_path.join("mcp")),
167 Self::Skills => Self::collect_yaml_files(&services_path.join("skills")),
168 }
169 }
170
171 fn collect_yaml_files(dir: &PathBuf) -> Result<Vec<PathBuf>> {
172 let mut files = Vec::new();
173 if dir.exists() {
174 Self::collect_yaml_recursive(dir, &mut files)?;
175 }
176 Ok(files)
177 }
178
179 fn collect_yaml_recursive(dir: &PathBuf, files: &mut Vec<PathBuf>) -> Result<()> {
180 if !dir.is_dir() {
181 return Ok(());
182 }
183
184 for entry in std::fs::read_dir(dir)? {
185 let entry = entry?;
186 let path = entry.path();
187
188 if path.is_dir() {
189 Self::collect_yaml_recursive(&path, files)?;
190 } else if let Some(ext) = path.extension()
191 && (ext == "yaml" || ext == "yml")
192 {
193 files.push(path);
194 }
195 }
196 Ok(())
197 }
198}
199
200impl std::fmt::Display for ConfigSection {
201 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
202 match self {
203 Self::Ai => write!(f, "ai"),
204 Self::Providers => write!(f, "providers"),
205 Self::Gateway => write!(f, "gateway"),
206 Self::Content => write!(f, "content"),
207 Self::Web => write!(f, "web"),
208 Self::Scheduler => write!(f, "scheduler"),
209 Self::Agents => write!(f, "agents"),
210 Self::Mcp => write!(f, "mcp"),
211 Self::Skills => write!(f, "skills"),
212 Self::Profile => write!(f, "profile"),
213 Self::Services => write!(f, "services"),
214 }
215 }
216}
217
218impl std::str::FromStr for ConfigSection {
219 type Err = anyhow::Error;
220
221 fn from_str(s: &str) -> Result<Self> {
222 match s.to_lowercase().as_str() {
223 "ai" => Ok(Self::Ai),
224 "providers" => Ok(Self::Providers),
225 "gateway" => Ok(Self::Gateway),
226 "content" => Ok(Self::Content),
227 "web" => Ok(Self::Web),
228 "scheduler" => Ok(Self::Scheduler),
229 "agents" => Ok(Self::Agents),
230 "mcp" => Ok(Self::Mcp),
231 "skills" => Ok(Self::Skills),
232 "profile" => Ok(Self::Profile),
233 "services" => Ok(Self::Services),
234 _ => Err(anyhow::anyhow!("Unknown config section: {}", s)),
235 }
236 }
237}
238
239pub fn read_yaml_file(path: &std::path::Path) -> Result<serde_yaml::Value> {
240 let content = std::fs::read_to_string(path)
243 .with_context(|| format!("Failed to read file: {}", path.display()))?;
244 serde_yaml::from_str(&content)
245 .with_context(|| format!("Failed to parse YAML from: {}", path.display()))
246}
247
248pub fn write_yaml_file(path: &std::path::Path, content: &serde_yaml::Value) -> Result<()> {
249 let yaml_str = serde_yaml::to_string(content).with_context(|| "Failed to serialize YAML")?;
252 std::fs::write(path, yaml_str)
253 .with_context(|| format!("Failed to write file: {}", path.display()))
254}