systemprompt_cli/commands/admin/config/
validate.rs1use anyhow::{Result, anyhow};
13use clap::Args;
14
15use super::types::{ConfigFileInfo, ConfigSection, ConfigValidateOutput, read_yaml_file};
16use crate::CliConfig;
17use crate::shared::CommandOutput;
18use systemprompt_logging::CliService;
19use systemprompt_models::profile::Profile;
20
21#[derive(Debug, Clone, Args)]
22pub struct ValidateArgs {
23 #[arg(value_name = "PATH_OR_SECTION")]
24 pub target: Option<String>,
25
26 #[arg(long)]
27 pub strict: bool,
28
29 #[arg(
30 long,
31 help = "Print the generated JSON schema for the Profile config type instead of validating \
32 any file"
33 )]
34 pub schema: bool,
35}
36
37pub fn execute(args: &ValidateArgs, _config: &CliConfig) -> Result<(CommandOutput, bool)> {
38 if args.schema {
39 return print_profile_schema();
40 }
41
42 if let Some(target) = &args.target {
43 let path = std::path::PathBuf::from(target);
44 if path.exists() && is_yaml_file(&path) && target.parse::<ConfigSection>().is_err() {
45 return validate_profile_file(&path);
46 }
47 }
48
49 let files_to_validate = if let Some(target) = &args.target {
50 if let Ok(section) = target.parse::<ConfigSection>() {
51 section.all_files()?
52 } else {
53 vec![std::path::PathBuf::from(target)]
54 }
55 } else {
56 let mut all_files = Vec::new();
57 for section in ConfigSection::all() {
58 if let Ok(files) = section.all_files() {
59 all_files.extend(files);
60 }
61 }
62 all_files
63 };
64
65 let mut results = Vec::new();
66 let mut all_valid = true;
67
68 for file_path in files_to_validate {
69 let section = detect_section(&file_path);
70 let exists = file_path.exists();
71
72 let (valid, error) = if exists {
73 match validate_file(&file_path, args.strict) {
74 Ok(()) => (true, None),
75 Err(e) => {
76 all_valid = false;
77 (false, Some(e.to_string()))
78 },
79 }
80 } else {
81 all_valid = false;
82 (false, Some("File not found".to_owned()))
83 };
84
85 results.push(ConfigFileInfo {
86 path: file_path.display().to_string(),
87 section,
88 exists,
89 valid,
90 error,
91 });
92 }
93
94 let output = ConfigValidateOutput {
95 files: results,
96 all_valid,
97 };
98
99 let title = if all_valid {
100 "Validation Passed"
101 } else {
102 "Validation Failed"
103 };
104
105 Ok((
106 CommandOutput::table_of(
107 vec!["path", "section", "exists", "valid", "error"],
108 &output.files,
109 )
110 .with_title(title),
111 all_valid,
112 ))
113}
114
115fn print_profile_schema() -> Result<(CommandOutput, bool)> {
116 let schema = schemars::schema_for!(Profile);
117 let json = serde_json::to_string_pretty(&schema)
118 .map_err(|e| anyhow!("failed to serialize Profile JSON schema: {e}"))?;
119 CliService::output(&json);
120
121 let output = ConfigValidateOutput {
122 files: Vec::new(),
123 all_valid: true,
124 };
125 Ok((
126 CommandOutput::table_of(
127 vec!["path", "section", "exists", "valid", "error"],
128 &output.files,
129 )
130 .with_skip_render(),
131 true,
132 ))
133}
134
135fn validate_profile_file(path: &std::path::Path) -> Result<(CommandOutput, bool)> {
136 let content = std::fs::read_to_string(path)
137 .map_err(|e| anyhow!("failed to read profile {}: {e}", path.display()))?;
138
139 match Profile::from_yaml(&content, path) {
140 Ok(profile) => {
141 let output = ConfigValidateOutput {
142 files: vec![ConfigFileInfo {
143 path: path.display().to_string(),
144 section: "profile".to_owned(),
145 exists: true,
146 valid: true,
147 error: None,
148 }],
149 all_valid: true,
150 };
151 let title = format!("Profile '{}' is valid", profile.name);
152 Ok((
153 CommandOutput::table_of(
154 vec!["path", "section", "exists", "valid", "error"],
155 &output.files,
156 )
157 .with_title(title),
158 true,
159 ))
160 },
161 Err(e) => Err(anyhow!(
162 "invalid profile {}: {e}\nThe error above names the offending field or value — fix it \
163 and re-run.",
164 path.display()
165 )),
166 }
167}
168
169fn is_yaml_file(path: &std::path::Path) -> bool {
170 matches!(
171 path.extension().and_then(|e| e.to_str()),
172 Some("yaml" | "yml")
173 )
174}
175
176fn validate_file(path: &std::path::Path, _strict: bool) -> Result<()> {
177 let _content = read_yaml_file(path)?;
178 Ok(())
179}
180
181fn detect_section(path: &std::path::Path) -> String {
182 let path_str = path.display().to_string();
183
184 if path_str.contains("/ai/") {
185 "ai".to_owned()
186 } else if path_str.contains("/content/") {
187 "content".to_owned()
188 } else if path_str.contains("/web/") {
189 "web".to_owned()
190 } else if path_str.contains("/scheduler/") {
191 "scheduler".to_owned()
192 } else if path_str.contains("/agents/") {
193 "agents".to_owned()
194 } else if path_str.contains("/mcp/") {
195 "mcp".to_owned()
196 } else if path_str.contains("/skills/") {
197 "skills".to_owned()
198 } else if path_str.contains("profile.yaml") {
199 "profile".to_owned()
200 } else if path_str.contains("/config/config.yaml") {
201 "services".to_owned()
202 } else {
203 "unknown".to_owned()
204 }
205}