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