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