Skip to main content

ai_agent/utils/plugins/
validate_plugin.rs

1// Source: ~/claudecode/openclaudecode/src/utils/plugins/validatePlugin.ts
2#![allow(dead_code)]
3
4use std::path::{Path, PathBuf};
5
6use super::schemas::{PluginManifest, PluginMarketplaceEntry};
7use super::types::PluginMarketplace;
8
9pub struct ValidationResult {
10    pub success: bool,
11    pub errors: Vec<ValidationError>,
12    pub warnings: Vec<ValidationWarning>,
13    pub file_path: String,
14    pub file_type: String,
15}
16
17#[derive(Debug)]
18pub struct ValidationError {
19    pub path: String,
20    pub message: String,
21}
22
23#[derive(Debug)]
24pub struct ValidationWarning {
25    pub path: String,
26    pub message: String,
27}
28
29/// Validate a plugin manifest file (plugin.json).
30pub async fn validate_plugin_manifest(file_path: &str) -> ValidationResult {
31    let absolute_path = match std::fs::canonicalize(file_path) {
32        Ok(p) => p,
33        Err(e) => {
34            return ValidationResult {
35                success: false,
36                errors: vec![ValidationError {
37                    path: "file".to_string(),
38                    message: format!("File not found: {}", e),
39                }],
40                warnings: Vec::new(),
41                file_path: file_path.to_string(),
42                file_type: "plugin".to_string(),
43            };
44        }
45    };
46
47    let content = match tokio::fs::read_to_string(&absolute_path).await {
48        Ok(c) => c,
49        Err(e) => {
50            return ValidationResult {
51                success: false,
52                errors: vec![ValidationError {
53                    path: "file".to_string(),
54                    message: format!("Failed to read file: {}", e),
55                }],
56                warnings: Vec::new(),
57                file_path: absolute_path.to_string_lossy().to_string(),
58                file_type: "plugin".to_string(),
59            };
60        }
61    };
62
63    let parsed: serde_json::Value = match serde_json::from_str(&content) {
64        Ok(v) => v,
65        Err(e) => {
66            return ValidationResult {
67                success: false,
68                errors: vec![ValidationError {
69                    path: "json".to_string(),
70                    message: format!("Invalid JSON syntax: {}", e),
71                }],
72                warnings: Vec::new(),
73                file_path: absolute_path.to_string_lossy().to_string(),
74                file_type: "plugin".to_string(),
75            };
76        }
77    };
78
79    let manifest: PluginManifest = match serde_json::from_value(parsed) {
80        Ok(m) => m,
81        Err(e) => {
82            return ValidationResult {
83                success: false,
84                errors: e
85                    .to_string()
86                    .lines()
87                    .map(|line| ValidationError {
88                        path: "manifest".to_string(),
89                        message: line.to_string(),
90                    })
91                    .collect(),
92                warnings: Vec::new(),
93                file_path: absolute_path.to_string_lossy().to_string(),
94                file_type: "plugin".to_string(),
95            };
96        }
97    };
98
99    let mut warnings = Vec::new();
100
101    // Warn if name isn't kebab-case
102    if !regex::Regex::new(r"^[a-z0-9]+(-[a-z0-9]+)*$")
103        .unwrap()
104        .is_match(&manifest.name)
105    {
106        warnings.push(ValidationWarning {
107            path: "name".to_string(),
108            message: format!("Plugin name \"{}\" is not kebab-case.", manifest.name),
109        });
110    }
111
112    // Warn if no version specified
113    if manifest.version.is_none() {
114        warnings.push(ValidationWarning {
115            path: "version".to_string(),
116            message: "No version specified.".to_string(),
117        });
118    }
119
120    // Warn if no description
121    if manifest.description.is_none() {
122        warnings.push(ValidationWarning {
123            path: "description".to_string(),
124            message: "No description provided.".to_string(),
125        });
126    }
127
128    ValidationResult {
129        success: true,
130        errors: Vec::new(),
131        warnings,
132        file_path: absolute_path.to_string_lossy().to_string(),
133        file_type: "plugin".to_string(),
134    }
135}
136
137/// Validate a marketplace manifest file (marketplace.json).
138pub async fn validate_marketplace_manifest(file_path: &str) -> ValidationResult {
139    let absolute_path = match std::fs::canonicalize(file_path) {
140        Ok(p) => p,
141        Err(e) => {
142            return ValidationResult {
143                success: false,
144                errors: vec![ValidationError {
145                    path: "file".to_string(),
146                    message: format!("File not found: {}", e),
147                }],
148                warnings: Vec::new(),
149                file_path: file_path.to_string(),
150                file_type: "marketplace".to_string(),
151            };
152        }
153    };
154
155    let content = match tokio::fs::read_to_string(&absolute_path).await {
156        Ok(c) => c,
157        Err(e) => {
158            return ValidationResult {
159                success: false,
160                errors: vec![ValidationError {
161                    path: "file".to_string(),
162                    message: format!("Failed to read file: {}", e),
163                }],
164                warnings: Vec::new(),
165                file_path: absolute_path.to_string_lossy().to_string(),
166                file_type: "marketplace".to_string(),
167            };
168        }
169    };
170
171    let marketplace: PluginMarketplace = match serde_json::from_str(&content) {
172        Ok(m) => m,
173        Err(e) => {
174            return ValidationResult {
175                success: false,
176                errors: vec![ValidationError {
177                    path: "json".to_string(),
178                    message: format!("Invalid JSON syntax: {}", e),
179                }],
180                warnings: Vec::new(),
181                file_path: absolute_path.to_string_lossy().to_string(),
182                file_type: "marketplace".to_string(),
183            };
184        }
185    };
186
187    let mut warnings = Vec::new();
188
189    // Warn if no plugins
190    if marketplace.plugins.is_empty() {
191        warnings.push(ValidationWarning {
192            path: "plugins".to_string(),
193            message: "Marketplace has no plugins defined".to_string(),
194        });
195    }
196
197    ValidationResult {
198        success: true,
199        errors: Vec::new(),
200        warnings,
201        file_path: absolute_path.to_string_lossy().to_string(),
202        file_type: "marketplace".to_string(),
203    }
204}