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::CommandResult;
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(
35 args: &ValidateArgs,
36 _config: &CliConfig,
37) -> Result<CommandResult<ConfigValidateOutput>> {
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(CommandResult::table(output).with_title(title))
108}
109
110fn print_profile_schema() -> Result<CommandResult<ConfigValidateOutput>> {
111 let schema = schemars::schema_for!(Profile);
112 let json = serde_json::to_string_pretty(&schema)
113 .map_err(|e| anyhow!("failed to serialize Profile JSON schema: {e}"))?;
114 CliService::output(&json);
115
116 let output = ConfigValidateOutput {
117 files: Vec::new(),
118 all_valid: true,
119 };
120 Ok(CommandResult::table(output).with_skip_render())
121}
122
123fn validate_profile_file(path: &std::path::Path) -> Result<CommandResult<ConfigValidateOutput>> {
124 let content = std::fs::read_to_string(path)
125 .map_err(|e| anyhow!("failed to read profile {}: {e}", path.display()))?;
126
127 match Profile::from_yaml(&content, path) {
128 Ok(profile) => {
129 let output = ConfigValidateOutput {
130 files: vec![ConfigFileInfo {
131 path: path.display().to_string(),
132 section: "profile".to_owned(),
133 exists: true,
134 valid: true,
135 error: None,
136 }],
137 all_valid: true,
138 };
139 let title = format!("Profile '{}' is valid", profile.name);
140 Ok(CommandResult::table(output).with_title(title))
141 },
142 Err(e) => Err(anyhow!(
143 "invalid profile {}: {e}\nThe error above names the offending field or value — fix it \
144 and re-run.",
145 path.display()
146 )),
147 }
148}
149
150fn is_yaml_file(path: &std::path::Path) -> bool {
151 matches!(
152 path.extension().and_then(|e| e.to_str()),
153 Some("yaml" | "yml")
154 )
155}
156
157fn validate_file(path: &std::path::Path, _strict: bool) -> Result<()> {
158 let _content = read_yaml_file(path)?;
159 Ok(())
160}
161
162fn detect_section(path: &std::path::Path) -> String {
163 let path_str = path.display().to_string();
164
165 if path_str.contains("/ai/") {
166 "ai".to_owned()
167 } else if path_str.contains("/content/") {
168 "content".to_owned()
169 } else if path_str.contains("/web/") {
170 "web".to_owned()
171 } else if path_str.contains("/scheduler/") {
172 "scheduler".to_owned()
173 } else if path_str.contains("/agents/") {
174 "agents".to_owned()
175 } else if path_str.contains("/mcp/") {
176 "mcp".to_owned()
177 } else if path_str.contains("/skills/") {
178 "skills".to_owned()
179 } else if path_str.contains("profile.yaml") {
180 "profile".to_owned()
181 } else if path_str.contains("/config/config.yaml") {
182 "services".to_owned()
183 } else {
184 "unknown".to_owned()
185 }
186}