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