systemprompt_cli/commands/core/plugins/
validate.rs1use anyhow::{Context, Result, anyhow};
11use clap::Args;
12use std::path::Path;
13
14use crate::CliConfig;
15use crate::shared::CommandOutput;
16
17use super::types::{PluginValidateAllOutput, PluginValidateOutput};
18
19#[derive(Debug, Clone, Args)]
20pub struct ValidateArgs {
21 #[arg(help = "Plugin ID to validate (validates all if omitted)")]
22 pub id: Option<String>,
23}
24
25pub(super) fn execute(args: ValidateArgs, _config: &CliConfig) -> Result<CommandOutput> {
26 let profile = systemprompt_config::ProfileBootstrap::get().context("Failed to get profile")?;
27 let plugins_path = std::path::PathBuf::from(profile.paths.plugins());
28 let skills_path = std::path::PathBuf::from(profile.paths.skills());
29
30 let plugin_ids = match args.id {
31 Some(id) => {
32 let plugin_dir = plugins_path.join(&id);
33 if !plugin_dir.exists() {
34 return Err(anyhow!("Plugin '{}' not found", id));
35 }
36 vec![id]
37 },
38 None => collect_plugin_ids(&plugins_path)?,
39 };
40
41 let mut results = Vec::new();
42
43 for plugin_id in &plugin_ids {
44 let result = validate_plugin(plugin_id, &plugins_path, &skills_path);
45 results.push(result);
46 }
47
48 let output = PluginValidateAllOutput { results };
49
50 Ok(CommandOutput::table_of(
51 vec!["plugin_id", "valid", "errors", "warnings"],
52 &output.results,
53 )
54 .with_title("Plugin Validation Results"))
55}
56
57pub fn collect_plugin_ids(plugins_path: &Path) -> Result<Vec<String>> {
58 if !plugins_path.exists() {
59 return Ok(Vec::new());
60 }
61
62 let mut ids = Vec::new();
63 for entry in std::fs::read_dir(plugins_path)? {
64 let entry = entry?;
65 if entry.path().is_dir()
66 && entry.path().join("config.yaml").exists()
67 && let Some(name) = entry.file_name().to_str()
68 {
69 ids.push(name.to_owned());
70 }
71 }
72 ids.sort();
73 Ok(ids)
74}
75
76pub fn validate_plugin(
77 plugin_id: &str,
78 plugins_path: &Path,
79 skills_path: &Path,
80) -> PluginValidateOutput {
81 let mut errors = Vec::new();
82 let mut warnings = Vec::new();
83
84 let config_path = plugins_path.join(plugin_id).join("config.yaml");
85 let content = match std::fs::read_to_string(&config_path) {
86 Ok(c) => c,
87 Err(e) => {
88 errors.push(format!("Failed to read config.yaml: {}", e));
89 return PluginValidateOutput {
90 plugin_id: systemprompt_identifiers::PluginId::new(plugin_id),
91 valid: false,
92 errors,
93 warnings,
94 };
95 },
96 };
97
98 let plugin_file: systemprompt_models::PluginConfigFile = match serde_yaml::from_str(&content) {
99 Ok(p) => p,
100 Err(e) => {
101 errors.push(format!("Failed to parse config.yaml: {}", e));
102 return PluginValidateOutput {
103 plugin_id: systemprompt_identifiers::PluginId::new(plugin_id),
104 valid: false,
105 errors,
106 warnings,
107 };
108 },
109 };
110
111 let plugin = &plugin_file.plugin;
112
113 if let Err(e) = plugin.validate(plugin_id) {
114 errors.push(format!("{}", e));
115 }
116
117 if plugin.id != plugin_id {
118 warnings.push(format!(
119 "Plugin id '{}' does not match directory name '{}'",
120 plugin.id, plugin_id
121 ));
122 }
123
124 validate_skill_refs(plugin, skills_path, &mut errors, &mut warnings);
125 validate_scripts(plugin, plugins_path, plugin_id, &mut errors);
126
127 PluginValidateOutput {
128 plugin_id: systemprompt_identifiers::PluginId::new(plugin_id),
129 valid: errors.is_empty(),
130 errors,
131 warnings,
132 }
133}
134
135fn validate_skill_refs(
136 plugin: &systemprompt_models::PluginConfig,
137 skills_path: &Path,
138 errors: &mut Vec<String>,
139 warnings: &mut Vec<String>,
140) {
141 if plugin.skills.source == systemprompt_models::ComponentSource::Explicit {
142 for skill_id in &plugin.skills.include {
143 let skill_dir = skills_path.join(skill_id);
144 if !skill_dir.exists() {
145 errors.push(format!("Referenced skill '{}' not found", skill_id));
146 }
147 }
148 }
149
150 if !skills_path.exists()
151 && plugin.skills.source == systemprompt_models::ComponentSource::Instance
152 {
153 warnings.push("Skills directory does not exist".to_owned());
154 }
155}
156
157fn validate_scripts(
158 plugin: &systemprompt_models::PluginConfig,
159 plugins_path: &Path,
160 plugin_id: &str,
161 errors: &mut Vec<String>,
162) {
163 for script in &plugin.scripts {
164 let script_path = plugins_path.join(plugin_id).join(&script.source);
165 if !script_path.exists() {
166 errors.push(format!(
167 "Script '{}' not found at {}",
168 script.name,
169 script_path.display()
170 ));
171 }
172 }
173}