Skip to main content

systemprompt_cli/commands/admin/config/
config_section.rs

1//! Config-section model and YAML helpers shared across the `admin config` tree.
2//!
3//! [`ConfigSection`] enumerates the profile and per-service config locations
4//! and resolves them to filesystem paths, while [`read_yaml_file`] and
5//! [`write_yaml_file`] back the editing commands. The remaining types are the
6//! serializable outputs rendered by the list, validate, and import/export
7//! flows.
8
9use std::path::PathBuf;
10
11use anyhow::{Context, Result};
12use schemars::JsonSchema;
13use serde::{Deserialize, Serialize};
14use systemprompt_config::ProfileBootstrap;
15
16use super::rate_limit_types::ResetChange;
17
18#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
19pub struct ValidateOutput {
20    pub valid: bool,
21    pub errors: Vec<String>,
22    pub warnings: Vec<String>,
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
26pub struct ExportOutput {
27    pub format: String,
28    pub path: String,
29    pub message: String,
30}
31
32#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
33pub struct ImportOutput {
34    pub path: String,
35    pub changes: Vec<ResetChange>,
36    pub message: String,
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
40pub struct DiffOutput {
41    pub source: String,
42    pub differences: Vec<DiffEntry>,
43    pub identical: bool,
44}
45
46#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
47pub struct DiffEntry {
48    pub field: String,
49    pub current: String,
50    pub other: String,
51}
52
53#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
54pub struct ConfigFileInfo {
55    pub path: String,
56    pub section: String,
57    pub exists: bool,
58    pub valid: bool,
59    pub error: Option<String>,
60}
61
62#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
63pub struct ConfigListOutput {
64    pub total: usize,
65    pub valid: usize,
66    pub invalid: usize,
67    pub files: Vec<ConfigFileInfo>,
68}
69
70#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
71pub struct ConfigValidateOutput {
72    pub files: Vec<ConfigFileInfo>,
73    pub all_valid: bool,
74}
75
76#[derive(Debug, Clone, Copy, PartialEq, Eq)]
77pub enum ConfigSection {
78    Ai,
79    Content,
80    Web,
81    Scheduler,
82    Agents,
83    Mcp,
84    Skills,
85    Profile,
86    Services,
87}
88
89impl ConfigSection {
90    pub const fn all() -> &'static [Self] {
91        &[
92            Self::Profile,
93            Self::Services,
94            Self::Ai,
95            Self::Content,
96            Self::Web,
97            Self::Scheduler,
98            Self::Agents,
99            Self::Mcp,
100            Self::Skills,
101        ]
102    }
103
104    pub fn file_path(self) -> Result<PathBuf> {
105        let profile = ProfileBootstrap::get()?;
106        match self {
107            Self::Ai => Ok(PathBuf::from(&profile.paths.services).join("ai/config.yaml")),
108            Self::Content => Ok(PathBuf::from(&profile.paths.services).join("content/config.yaml")),
109            Self::Web => Ok(PathBuf::from(&profile.paths.services).join("web/config.yaml")),
110            Self::Scheduler => {
111                Ok(PathBuf::from(&profile.paths.services).join("scheduler/config.yaml"))
112            },
113            Self::Agents => Ok(PathBuf::from(&profile.paths.services).join("agents/config.yaml")),
114            Self::Mcp => Ok(PathBuf::from(&profile.paths.services).join("mcp/config.yaml")),
115            Self::Skills => Ok(PathBuf::from(&profile.paths.services).join("skills/config.yaml")),
116            Self::Profile => Ok(PathBuf::from(ProfileBootstrap::get_path()?)),
117            Self::Services => Ok(PathBuf::from(&profile.paths.services).join("config/config.yaml")),
118        }
119    }
120
121    pub fn all_files(self) -> Result<Vec<PathBuf>> {
122        let profile = ProfileBootstrap::get()?;
123        let services_path = PathBuf::from(&profile.paths.services);
124
125        match self {
126            Self::Profile => Ok(vec![PathBuf::from(ProfileBootstrap::get_path()?)]),
127            Self::Services => Ok(vec![services_path.join("config/config.yaml")]),
128            Self::Ai => Self::collect_yaml_files(&services_path.join("ai")),
129            Self::Content => Self::collect_yaml_files(&services_path.join("content")),
130            Self::Web => Self::collect_yaml_files(&services_path.join("web")),
131            Self::Scheduler => Self::collect_yaml_files(&services_path.join("scheduler")),
132            Self::Agents => Self::collect_yaml_files(&services_path.join("agents")),
133            Self::Mcp => Self::collect_yaml_files(&services_path.join("mcp")),
134            Self::Skills => Self::collect_yaml_files(&services_path.join("skills")),
135        }
136    }
137
138    fn collect_yaml_files(dir: &PathBuf) -> Result<Vec<PathBuf>> {
139        let mut files = Vec::new();
140        if dir.exists() {
141            Self::collect_yaml_recursive(dir, &mut files)?;
142        }
143        Ok(files)
144    }
145
146    fn collect_yaml_recursive(dir: &PathBuf, files: &mut Vec<PathBuf>) -> Result<()> {
147        if !dir.is_dir() {
148            return Ok(());
149        }
150
151        for entry in std::fs::read_dir(dir)? {
152            let entry = entry?;
153            let path = entry.path();
154
155            if path.is_dir() {
156                Self::collect_yaml_recursive(&path, files)?;
157            } else if let Some(ext) = path.extension() {
158                if ext == "yaml" || ext == "yml" {
159                    files.push(path);
160                }
161            }
162        }
163        Ok(())
164    }
165}
166
167impl std::fmt::Display for ConfigSection {
168    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
169        match self {
170            Self::Ai => write!(f, "ai"),
171            Self::Content => write!(f, "content"),
172            Self::Web => write!(f, "web"),
173            Self::Scheduler => write!(f, "scheduler"),
174            Self::Agents => write!(f, "agents"),
175            Self::Mcp => write!(f, "mcp"),
176            Self::Skills => write!(f, "skills"),
177            Self::Profile => write!(f, "profile"),
178            Self::Services => write!(f, "services"),
179        }
180    }
181}
182
183impl std::str::FromStr for ConfigSection {
184    type Err = anyhow::Error;
185
186    fn from_str(s: &str) -> Result<Self> {
187        match s.to_lowercase().as_str() {
188            "ai" => Ok(Self::Ai),
189            "content" => Ok(Self::Content),
190            "web" => Ok(Self::Web),
191            "scheduler" => Ok(Self::Scheduler),
192            "agents" => Ok(Self::Agents),
193            "mcp" => Ok(Self::Mcp),
194            "skills" => Ok(Self::Skills),
195            "profile" => Ok(Self::Profile),
196            "services" => Ok(Self::Services),
197            _ => Err(anyhow::anyhow!("Unknown config section: {}", s)),
198        }
199    }
200}
201
202pub fn read_yaml_file(path: &std::path::Path) -> Result<serde_yaml::Value> {
203    let content = std::fs::read_to_string(path)
204        .with_context(|| format!("Failed to read file: {}", path.display()))?;
205    serde_yaml::from_str(&content)
206        .with_context(|| format!("Failed to parse YAML from: {}", path.display()))
207}
208
209pub fn write_yaml_file(path: &std::path::Path, content: &serde_yaml::Value) -> Result<()> {
210    let yaml_str = serde_yaml::to_string(content).with_context(|| "Failed to serialize YAML")?;
211    std::fs::write(path, yaml_str)
212        .with_context(|| format!("Failed to write file: {}", path.display()))
213}