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