Skip to main content

systemprompt_cli/commands/admin/config/
config_section.rs

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