1use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5
6pub type PluginAuthor = serde_json::Value;
8
9pub type PluginManifest = serde_json::Value;
11
12pub type CommandMetadata = serde_json::Value;
14
15pub struct BuiltinPluginDefinition {
17 pub name: String,
19 pub description: String,
21 pub version: Option<String>,
23 pub skills: Option<Vec<serde_json::Value>>,
25 pub hooks: Option<serde_json::Value>,
27 pub mcp_servers: Option<HashMap<String, serde_json::Value>>,
29 pub is_available: Option<Box<dyn Fn() -> bool + Send + Sync>>,
31 pub default_enabled: Option<bool>,
33}
34
35#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct PluginRepository {
38 pub url: String,
39 pub branch: String,
40 #[serde(skip_serializing_if = "Option::is_none")]
41 #[serde(rename = "lastUpdated")]
42 pub last_updated: Option<String>,
43 #[serde(skip_serializing_if = "Option::is_none")]
44 #[serde(rename = "commitSha")]
45 pub commit_sha: Option<String>,
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct PluginConfig {
51 pub repositories: HashMap<String, PluginRepository>,
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct LoadedPlugin {
57 pub name: String,
58 pub manifest: PluginManifest,
59 pub path: String,
60 pub source: String,
61 pub repository: String,
63 #[serde(skip_serializing_if = "Option::is_none")]
64 pub enabled: Option<bool>,
65 #[serde(skip_serializing_if = "Option::is_none")]
67 #[serde(rename = "isBuiltin")]
68 pub is_builtin: Option<bool>,
69 #[serde(skip_serializing_if = "Option::is_none")]
71 pub sha: Option<String>,
72 #[serde(skip_serializing_if = "Option::is_none")]
73 #[serde(rename = "commandsPath")]
74 pub commands_path: Option<String>,
75 #[serde(skip_serializing_if = "Option::is_none")]
76 #[serde(rename = "commandsPaths")]
77 pub commands_paths: Option<Vec<String>>,
78 #[serde(skip_serializing_if = "Option::is_none")]
79 #[serde(rename = "commandsMetadata")]
80 pub commands_metadata: Option<HashMap<String, CommandMetadata>>,
81 #[serde(skip_serializing_if = "Option::is_none")]
82 #[serde(rename = "agentsPath")]
83 pub agents_path: Option<String>,
84 #[serde(skip_serializing_if = "Option::is_none")]
85 #[serde(rename = "agentsPaths")]
86 pub agents_paths: Option<Vec<String>>,
87 #[serde(skip_serializing_if = "Option::is_none")]
88 #[serde(rename = "skillsPath")]
89 pub skills_path: Option<String>,
90 #[serde(skip_serializing_if = "Option::is_none")]
91 #[serde(rename = "skillsPaths")]
92 pub skills_paths: Option<Vec<String>>,
93 #[serde(skip_serializing_if = "Option::is_none")]
94 #[serde(rename = "outputStylesPath")]
95 pub output_styles_path: Option<String>,
96 #[serde(skip_serializing_if = "Option::is_none")]
97 #[serde(rename = "outputStylesPaths")]
98 pub output_styles_paths: Option<Vec<String>>,
99 #[serde(skip_serializing_if = "Option::is_none")]
100 #[serde(rename = "hooksConfig")]
101 pub hooks_config: Option<serde_json::Value>,
102 #[serde(skip_serializing_if = "Option::is_none")]
103 #[serde(rename = "mcpServers")]
104 pub mcp_servers: Option<HashMap<String, serde_json::Value>>,
105 #[serde(skip_serializing_if = "Option::is_none")]
106 #[serde(rename = "lspServers")]
107 pub lsp_servers: Option<HashMap<String, serde_json::Value>>,
108 #[serde(skip_serializing_if = "Option::is_none")]
109 pub settings: Option<HashMap<String, serde_json::Value>>,
110}
111
112#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
114#[serde(rename_all = "kebab-case")]
115pub enum PluginComponent {
116 Commands,
117 Agents,
118 Skills,
119 Hooks,
120 OutputStyles,
121}
122
123#[derive(Debug, Clone, Serialize, Deserialize)]
125#[serde(tag = "type")]
126pub enum PluginError {
127 #[serde(rename = "path-not-found")]
128 PathNotFound {
129 source: String,
130 #[serde(skip_serializing_if = "Option::is_none")]
131 plugin: Option<String>,
132 path: String,
133 component: PluginComponent,
134 },
135 #[serde(rename = "git-auth-failed")]
136 GitAuthFailed {
137 source: String,
138 #[serde(skip_serializing_if = "Option::is_none")]
139 plugin: Option<String>,
140 #[serde(rename = "gitUrl")]
141 git_url: String,
142 #[serde(rename = "authType")]
143 auth_type: GitAuthType,
144 },
145 #[serde(rename = "git-timeout")]
146 GitTimeout {
147 source: String,
148 #[serde(skip_serializing_if = "Option::is_none")]
149 plugin: Option<String>,
150 #[serde(rename = "gitUrl")]
151 git_url: String,
152 operation: GitOperation,
153 },
154 #[serde(rename = "network-error")]
155 NetworkError {
156 source: String,
157 #[serde(skip_serializing_if = "Option::is_none")]
158 plugin: Option<String>,
159 url: String,
160 #[serde(skip_serializing_if = "Option::is_none")]
161 details: Option<String>,
162 },
163 #[serde(rename = "manifest-parse-error")]
164 ManifestParseError {
165 source: String,
166 #[serde(skip_serializing_if = "Option::is_none")]
167 plugin: Option<String>,
168 #[serde(rename = "manifestPath")]
169 manifest_path: String,
170 #[serde(rename = "parseError")]
171 parse_error: String,
172 },
173 #[serde(rename = "manifest-validation-error")]
174 ManifestValidationError {
175 source: String,
176 #[serde(skip_serializing_if = "Option::is_none")]
177 plugin: Option<String>,
178 #[serde(rename = "manifestPath")]
179 manifest_path: String,
180 #[serde(rename = "validationErrors")]
181 validation_errors: Vec<String>,
182 },
183 #[serde(rename = "plugin-not-found")]
184 PluginNotFound {
185 source: String,
186 #[serde(rename = "pluginId")]
187 plugin_id: String,
188 marketplace: String,
189 },
190 #[serde(rename = "marketplace-not-found")]
191 MarketplaceNotFound {
192 source: String,
193 marketplace: String,
194 #[serde(rename = "availableMarketplaces")]
195 available_marketplaces: Vec<String>,
196 },
197 #[serde(rename = "marketplace-load-failed")]
198 MarketplaceLoadFailed {
199 source: String,
200 marketplace: String,
201 reason: String,
202 },
203 #[serde(rename = "mcp-config-invalid")]
204 McpConfigInvalid {
205 source: String,
206 plugin: String,
207 #[serde(rename = "serverName")]
208 server_name: String,
209 #[serde(rename = "validationError")]
210 validation_error: String,
211 },
212 #[serde(rename = "mcp-server-suppressed-duplicate")]
213 McpServerSuppressedDuplicate {
214 source: String,
215 plugin: String,
216 #[serde(rename = "serverName")]
217 server_name: String,
218 #[serde(rename = "duplicateOf")]
219 duplicate_of: String,
220 },
221 #[serde(rename = "lsp-config-invalid")]
222 LspConfigInvalid {
223 source: String,
224 plugin: String,
225 #[serde(rename = "serverName")]
226 server_name: String,
227 #[serde(rename = "validationError")]
228 validation_error: String,
229 },
230 #[serde(rename = "hook-load-failed")]
231 HookLoadFailed {
232 source: String,
233 plugin: String,
234 #[serde(rename = "hookPath")]
235 hook_path: String,
236 reason: String,
237 },
238 #[serde(rename = "component-load-failed")]
239 ComponentLoadFailed {
240 source: String,
241 plugin: String,
242 component: PluginComponent,
243 path: String,
244 reason: String,
245 },
246 #[serde(rename = "mcpb-download-failed")]
247 McpbDownloadFailed {
248 source: String,
249 plugin: String,
250 url: String,
251 reason: String,
252 },
253 #[serde(rename = "mcpb-extract-failed")]
254 McpbExtractFailed {
255 source: String,
256 plugin: String,
257 #[serde(rename = "mcpbPath")]
258 mcpb_path: String,
259 reason: String,
260 },
261 #[serde(rename = "mcpb-invalid-manifest")]
262 McpbInvalidManifest {
263 source: String,
264 plugin: String,
265 #[serde(rename = "mcpbPath")]
266 mcpb_path: String,
267 #[serde(rename = "validationError")]
268 validation_error: String,
269 },
270 #[serde(rename = "lsp-server-start-failed")]
271 LspServerStartFailed {
272 source: String,
273 plugin: String,
274 #[serde(rename = "serverName")]
275 server_name: String,
276 reason: String,
277 },
278 #[serde(rename = "lsp-server-crashed")]
279 LspServerCrashed {
280 source: String,
281 plugin: String,
282 #[serde(rename = "serverName")]
283 server_name: String,
284 #[serde(rename = "exitCode")]
285 exit_code: Option<i32>,
286 #[serde(skip_serializing_if = "Option::is_none")]
287 signal: Option<String>,
288 },
289 #[serde(rename = "lsp-request-timeout")]
290 LspRequestTimeout {
291 source: String,
292 plugin: String,
293 #[serde(rename = "serverName")]
294 server_name: String,
295 method: String,
296 #[serde(rename = "timeoutMs")]
297 timeout_ms: u64,
298 },
299 #[serde(rename = "lsp-request-failed")]
300 LspRequestFailed {
301 source: String,
302 plugin: String,
303 #[serde(rename = "serverName")]
304 server_name: String,
305 method: String,
306 error: String,
307 },
308 #[serde(rename = "marketplace-blocked-by-policy")]
309 MarketplaceBlockedByPolicy {
310 source: String,
311 #[serde(skip_serializing_if = "Option::is_none")]
312 plugin: Option<String>,
313 marketplace: String,
314 #[serde(skip_serializing_if = "Option::is_none")]
315 #[serde(rename = "blockedByBlocklist")]
316 blocked_by_blocklist: Option<bool>,
317 #[serde(rename = "allowedSources")]
318 allowed_sources: Vec<String>,
319 },
320 #[serde(rename = "dependency-unsatisfied")]
321 DependencyUnsatisfied {
322 source: String,
323 plugin: String,
324 dependency: String,
325 reason: DependencyReason,
326 },
327 #[serde(rename = "plugin-cache-miss")]
328 PluginCacheMiss {
329 source: String,
330 plugin: String,
331 #[serde(rename = "installPath")]
332 install_path: String,
333 },
334 #[serde(rename = "generic-error")]
335 GenericError {
336 source: String,
337 #[serde(skip_serializing_if = "Option::is_none")]
338 plugin: Option<String>,
339 error: String,
340 },
341}
342
343#[derive(Debug, Clone, Serialize, Deserialize)]
345#[serde(rename_all = "lowercase")]
346pub enum GitAuthType {
347 Ssh,
348 Https,
349}
350
351#[derive(Debug, Clone, Serialize, Deserialize)]
353#[serde(rename_all = "lowercase")]
354pub enum GitOperation {
355 Clone,
356 Pull,
357}
358
359#[derive(Debug, Clone, Serialize, Deserialize)]
361#[serde(rename_all = "kebab-case")]
362pub enum DependencyReason {
363 NotEnabled,
364 NotFound,
365}
366
367#[derive(Debug, Clone, Serialize, Deserialize)]
369pub struct PluginLoadResult {
370 pub enabled: Vec<LoadedPlugin>,
371 pub disabled: Vec<LoadedPlugin>,
372 pub errors: Vec<PluginError>,
373}
374
375pub fn get_plugin_error_message(error: &PluginError) -> String {
377 match error {
378 PluginError::GenericError { error: msg, .. } => msg.clone(),
379 PluginError::PathNotFound {
380 path, component, ..
381 } => {
382 format!("Path not found: {} ({:?})", path, component)
383 }
384 PluginError::GitAuthFailed {
385 auth_type, git_url, ..
386 } => {
387 format!("Git authentication failed ({:?}): {}", auth_type, git_url)
388 }
389 PluginError::GitTimeout {
390 operation, git_url, ..
391 } => {
392 format!("Git {:?} timeout: {}", operation, git_url)
393 }
394 PluginError::NetworkError { url, details, .. } => {
395 if let Some(d) = details {
396 format!("Network error: {} - {}", url, d)
397 } else {
398 format!("Network error: {}", url)
399 }
400 }
401 PluginError::ManifestParseError { parse_error, .. } => {
402 format!("Manifest parse error: {}", parse_error)
403 }
404 PluginError::ManifestValidationError {
405 validation_errors, ..
406 } => {
407 format!(
408 "Manifest validation failed: {}",
409 validation_errors.join(", ")
410 )
411 }
412 PluginError::PluginNotFound {
413 plugin_id,
414 marketplace,
415 ..
416 } => {
417 format!(
418 "Plugin {} not found in marketplace {}",
419 plugin_id, marketplace
420 )
421 }
422 PluginError::MarketplaceNotFound { marketplace, .. } => {
423 format!("Marketplace {} not found", marketplace)
424 }
425 PluginError::MarketplaceLoadFailed {
426 marketplace,
427 reason,
428 ..
429 } => {
430 format!("Marketplace {} failed to load: {}", marketplace, reason)
431 }
432 PluginError::McpConfigInvalid {
433 server_name,
434 validation_error,
435 ..
436 } => {
437 format!("MCP server {} invalid: {}", server_name, validation_error)
438 }
439 PluginError::McpServerSuppressedDuplicate {
440 server_name,
441 duplicate_of,
442 ..
443 } => {
444 let dup = if duplicate_of.starts_with("plugin:") {
445 format!(
446 "server provided by plugin \"{}\"",
447 duplicate_of.split(':').nth(1).unwrap_or("?")
448 )
449 } else {
450 format!("already-configured \"{}\"", duplicate_of)
451 };
452 format!(
453 "MCP server \"{}\" skipped — same command/URL as {}",
454 server_name, dup
455 )
456 }
457 PluginError::HookLoadFailed { reason, .. } => {
458 format!("Hook load failed: {}", reason)
459 }
460 PluginError::ComponentLoadFailed {
461 component,
462 path,
463 reason,
464 ..
465 } => {
466 format!("{:?} load failed from {}: {}", component, path, reason)
467 }
468 PluginError::McpbDownloadFailed { url, reason, .. } => {
469 format!("Failed to download MCPB from {}: {}", url, reason)
470 }
471 PluginError::McpbExtractFailed {
472 mcpb_path, reason, ..
473 } => {
474 format!("Failed to extract MCPB {}: {}", mcpb_path, reason)
475 }
476 PluginError::McpbInvalidManifest {
477 mcpb_path,
478 validation_error,
479 ..
480 } => {
481 format!(
482 "MCPB manifest invalid at {}: {}",
483 mcpb_path, validation_error
484 )
485 }
486 PluginError::LspConfigInvalid {
487 plugin,
488 server_name,
489 validation_error,
490 ..
491 } => {
492 format!(
493 "Plugin \"{}\" has invalid LSP server config for \"{}\": {}",
494 plugin, server_name, validation_error
495 )
496 }
497 PluginError::LspServerStartFailed {
498 plugin,
499 server_name,
500 reason,
501 ..
502 } => {
503 format!(
504 "Plugin \"{}\" failed to start LSP server \"{}\": {}",
505 plugin, server_name, reason
506 )
507 }
508 PluginError::LspServerCrashed {
509 plugin,
510 server_name,
511 exit_code,
512 signal,
513 ..
514 } => {
515 if let Some(sig) = signal {
516 format!(
517 "Plugin \"{}\" LSP server \"{}\" crashed with signal {}",
518 plugin, server_name, sig
519 )
520 } else {
521 format!(
522 "Plugin \"{}\" LSP server \"{}\" crashed with exit code {}",
523 plugin,
524 server_name,
525 exit_code
526 .map(|c| c.to_string())
527 .unwrap_or_else(|| "unknown".to_string())
528 )
529 }
530 }
531 PluginError::LspRequestTimeout {
532 plugin,
533 server_name,
534 method,
535 timeout_ms,
536 ..
537 } => {
538 format!(
539 "Plugin \"{}\" LSP server \"{}\" timed out on {} request after {}ms",
540 plugin, server_name, method, timeout_ms
541 )
542 }
543 PluginError::LspRequestFailed {
544 plugin,
545 server_name,
546 method,
547 error,
548 ..
549 } => {
550 format!(
551 "Plugin \"{}\" LSP server \"{}\" {} request failed: {}",
552 plugin, server_name, method, error
553 )
554 }
555 PluginError::MarketplaceBlockedByPolicy {
556 marketplace,
557 blocked_by_blocklist,
558 ..
559 } => {
560 if blocked_by_blocklist == &Some(true) {
561 format!(
562 "Marketplace '{}' is blocked by enterprise policy",
563 marketplace
564 )
565 } else {
566 format!(
567 "Marketplace '{}' is not in the allowed marketplace list",
568 marketplace
569 )
570 }
571 }
572 PluginError::DependencyUnsatisfied {
573 dependency, reason, ..
574 } => {
575 let hint = match reason {
576 DependencyReason::NotEnabled => "disabled — enable it or remove the dependency",
577 DependencyReason::NotFound => "not found in any configured marketplace",
578 };
579 format!("Dependency \"{}\" is {}", dependency, hint)
580 }
581 PluginError::PluginCacheMiss {
582 plugin,
583 install_path,
584 ..
585 } => {
586 format!(
587 "Plugin \"{}\" not cached at {} — run /plugins to refresh",
588 plugin, install_path
589 )
590 }
591 }
592}