1use std::collections::BTreeMap;
2use std::fmt::{Display, Formatter};
3use std::path::PathBuf;
4use std::process::{Command, Stdio};
5
6use serde::{Deserialize, Serialize};
7use serde_json::Value;
8
9use crate::constants::{BUILTIN_MARKETPLACE, BUNDLED_MARKETPLACE, EXTERNAL_MARKETPLACE};
10use crate::error::PluginError;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
13#[serde(rename_all = "lowercase")]
14pub enum PluginKind {
15 Builtin,
16 Bundled,
17 External,
18}
19
20impl Display for PluginKind {
21 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
22 match self {
23 Self::Builtin => write!(f, "builtin"),
24 Self::Bundled => write!(f, "bundled"),
25 Self::External => write!(f, "external"),
26 }
27 }
28}
29
30impl PluginKind {
31 #[must_use]
32 pub(crate) fn marketplace(self) -> &'static str {
33 match self {
34 Self::Builtin => BUILTIN_MARKETPLACE,
35 Self::Bundled => BUNDLED_MARKETPLACE,
36 Self::External => EXTERNAL_MARKETPLACE,
37 }
38 }
39}
40
41#[derive(Debug, Clone, PartialEq, Eq)]
42pub struct PluginMetadata {
43 pub id: String,
44 pub name: String,
45 pub version: String,
46 pub description: String,
47 pub kind: PluginKind,
48 pub source: String,
49 pub default_enabled: bool,
50 pub root: Option<PathBuf>,
51}
52
53#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
54pub struct PluginHooks {
55 #[serde(rename = "PreToolUse", default)]
56 pub pre_tool_use: Vec<String>,
57 #[serde(rename = "PostToolUse", default)]
58 pub post_tool_use: Vec<String>,
59}
60
61impl PluginHooks {
62 #[must_use]
63 pub fn is_empty(&self) -> bool {
64 self.pre_tool_use.is_empty() && self.post_tool_use.is_empty()
65 }
66
67 #[must_use]
68 pub fn merged_with(&self, other: &Self) -> Self {
69 let mut merged = self.clone();
70 merged
71 .pre_tool_use
72 .extend(other.pre_tool_use.iter().cloned());
73 merged
74 .post_tool_use
75 .extend(other.post_tool_use.iter().cloned());
76 merged
77 }
78}
79
80#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
81pub struct PluginLifecycle {
82 #[serde(rename = "Init", default)]
83 pub init: Vec<String>,
84 #[serde(rename = "Shutdown", default)]
85 pub shutdown: Vec<String>,
86}
87
88impl PluginLifecycle {
89 #[must_use]
90 pub fn is_empty(&self) -> bool {
91 self.init.is_empty() && self.shutdown.is_empty()
92 }
93}
94
95#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
96pub struct PluginManifest {
97 pub name: String,
98 pub version: String,
99 pub description: String,
100 pub permissions: Vec<PluginPermission>,
101 #[serde(rename = "defaultEnabled", default)]
102 pub default_enabled: bool,
103 #[serde(default)]
104 pub hooks: PluginHooks,
105 #[serde(default)]
106 pub lifecycle: PluginLifecycle,
107 #[serde(default)]
108 pub tools: Vec<PluginToolManifest>,
109 #[serde(default)]
110 pub commands: Vec<PluginCommandManifest>,
111}
112
113#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
114#[serde(rename_all = "lowercase")]
115pub enum PluginPermission {
116 Read,
117 Write,
118 Execute,
119}
120
121impl PluginPermission {
122 #[must_use]
123 pub fn as_str(self) -> &'static str {
124 match self {
125 Self::Read => "read",
126 Self::Write => "write",
127 Self::Execute => "execute",
128 }
129 }
130
131 pub(crate) fn parse(value: &str) -> Option<Self> {
132 match value {
133 "read" => Some(Self::Read),
134 "write" => Some(Self::Write),
135 "execute" => Some(Self::Execute),
136 _ => None,
137 }
138 }
139}
140
141impl AsRef<str> for PluginPermission {
142 fn as_ref(&self) -> &str {
143 self.as_str()
144 }
145}
146
147#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
148pub struct PluginToolManifest {
149 pub name: String,
150 pub description: String,
151 #[serde(rename = "inputSchema")]
152 pub input_schema: Value,
153 pub command: String,
154 #[serde(default)]
155 pub args: Vec<String>,
156 pub required_permission: PluginToolPermission,
157}
158
159#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
160#[serde(rename_all = "kebab-case")]
161pub enum PluginToolPermission {
162 ReadOnly,
163 WorkspaceWrite,
164 DangerFullAccess,
165}
166
167impl PluginToolPermission {
168 #[must_use]
169 pub fn as_str(self) -> &'static str {
170 match self {
171 Self::ReadOnly => "read-only",
172 Self::WorkspaceWrite => "workspace-write",
173 Self::DangerFullAccess => "danger-full-access",
174 }
175 }
176
177 pub(crate) fn parse(value: &str) -> Option<Self> {
178 match value {
179 "read-only" => Some(Self::ReadOnly),
180 "workspace-write" => Some(Self::WorkspaceWrite),
181 "danger-full-access" => Some(Self::DangerFullAccess),
182 _ => None,
183 }
184 }
185}
186
187#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
188pub struct PluginToolDefinition {
189 pub name: String,
190 #[serde(default)]
191 pub description: Option<String>,
192 #[serde(rename = "inputSchema")]
193 pub input_schema: Value,
194}
195
196#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
197pub struct PluginCommandManifest {
198 pub name: String,
199 pub description: String,
200 pub command: String,
201}
202
203#[derive(Debug, Clone, PartialEq)]
204pub struct PluginTool {
205 plugin_id: String,
206 plugin_name: String,
207 definition: PluginToolDefinition,
208 pub(crate) command: String,
209 args: Vec<String>,
210 required_permission: PluginToolPermission,
211 root: Option<PathBuf>,
212}
213
214impl PluginTool {
215 #[must_use]
216 pub fn new(
217 plugin_id: impl Into<String>,
218 plugin_name: impl Into<String>,
219 definition: PluginToolDefinition,
220 command: impl Into<String>,
221 args: Vec<String>,
222 required_permission: PluginToolPermission,
223 root: Option<PathBuf>,
224 ) -> Self {
225 Self {
226 plugin_id: plugin_id.into(),
227 plugin_name: plugin_name.into(),
228 definition,
229 command: command.into(),
230 args,
231 required_permission,
232 root,
233 }
234 }
235
236 #[must_use]
237 pub fn plugin_id(&self) -> &str {
238 &self.plugin_id
239 }
240
241 #[must_use]
242 pub fn definition(&self) -> &PluginToolDefinition {
243 &self.definition
244 }
245
246 #[must_use]
247 pub fn required_permission(&self) -> &str {
248 self.required_permission.as_str()
249 }
250
251 pub fn execute(&self, input: &Value) -> Result<String, PluginError> {
252 let input_json = input.to_string();
253 let mut process = if cfg!(windows) && self.command.ends_with(".sh") {
254 let mut p = Command::new("bash");
255 p.arg(&self.command);
256 p.args(&self.args);
257 p
258 } else {
259 let mut p = Command::new(&self.command);
260 p.args(&self.args);
261 p
262 };
263 process
264 .stdin(Stdio::piped())
265 .stdout(Stdio::piped())
266 .stderr(Stdio::piped())
267 .env("CODINEER_PLUGIN_ID", &self.plugin_id)
268 .env("CODINEER_PLUGIN_NAME", &self.plugin_name)
269 .env("CODINEER_TOOL_NAME", &self.definition.name)
270 .env("CODINEER_TOOL_INPUT", &input_json);
271 if let Some(root) = &self.root {
272 process
273 .current_dir(root)
274 .env("CODINEER_PLUGIN_ROOT", root.display().to_string());
275 }
276
277 let mut child = process.spawn()?;
278 if let Some(stdin) = child.stdin.as_mut() {
279 use std::io::Write as _;
280 stdin.write_all(input_json.as_bytes())?;
281 }
282
283 let output = child.wait_with_output()?;
284 if output.status.success() {
285 Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
286 } else {
287 let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
288 Err(PluginError::CommandFailed(format!(
289 "plugin tool `{}` from `{}` failed for `{}`: {}",
290 self.definition.name,
291 self.plugin_id,
292 self.command,
293 if stderr.is_empty() {
294 format!("exit status {}", output.status)
295 } else {
296 stderr
297 }
298 )))
299 }
300 }
301}
302#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
303#[serde(tag = "type", rename_all = "snake_case")]
304pub enum PluginInstallSource {
305 LocalPath { path: PathBuf },
306 GitUrl { url: String },
307}
308
309#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
310pub struct InstalledPluginRecord {
311 #[serde(default = "default_plugin_kind")]
312 pub kind: PluginKind,
313 pub id: String,
314 pub name: String,
315 pub version: String,
316 pub description: String,
317 pub install_path: PathBuf,
318 pub source: PluginInstallSource,
319 pub installed_at_unix_ms: u128,
320 pub updated_at_unix_ms: u128,
321}
322
323#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
324pub struct InstalledPluginRegistry {
325 #[serde(default)]
326 pub plugins: BTreeMap<String, InstalledPluginRecord>,
327}
328
329fn default_plugin_kind() -> PluginKind {
330 PluginKind::External
331}
332
333#[derive(Debug, Clone, PartialEq)]
334pub struct BuiltinPlugin {
335 pub(crate) metadata: PluginMetadata,
336 pub(crate) hooks: PluginHooks,
337 pub(crate) lifecycle: PluginLifecycle,
338 pub(crate) tools: Vec<PluginTool>,
339}
340
341#[derive(Debug, Clone, PartialEq)]
342pub struct BundledPlugin {
343 pub(crate) metadata: PluginMetadata,
344 pub(crate) hooks: PluginHooks,
345 pub(crate) lifecycle: PluginLifecycle,
346 pub(crate) tools: Vec<PluginTool>,
347}
348
349#[derive(Debug, Clone, PartialEq)]
350pub struct ExternalPlugin {
351 pub(crate) metadata: PluginMetadata,
352 pub(crate) hooks: PluginHooks,
353 pub(crate) lifecycle: PluginLifecycle,
354 pub(crate) tools: Vec<PluginTool>,
355}
356
357pub trait Plugin {
358 fn metadata(&self) -> &PluginMetadata;
359 fn hooks(&self) -> &PluginHooks;
360 fn lifecycle(&self) -> &PluginLifecycle;
361 fn tools(&self) -> &[PluginTool];
362 fn validate(&self) -> Result<(), PluginError>;
363 fn initialize(&self) -> Result<(), PluginError>;
364 fn shutdown(&self) -> Result<(), PluginError>;
365}
366
367#[derive(Debug, Clone, PartialEq)]
368pub enum PluginDefinition {
369 Builtin(BuiltinPlugin),
370 Bundled(BundledPlugin),
371 External(ExternalPlugin),
372}
373
374impl Plugin for BuiltinPlugin {
375 fn metadata(&self) -> &PluginMetadata {
376 &self.metadata
377 }
378
379 fn hooks(&self) -> &PluginHooks {
380 &self.hooks
381 }
382
383 fn lifecycle(&self) -> &PluginLifecycle {
384 &self.lifecycle
385 }
386
387 fn tools(&self) -> &[PluginTool] {
388 &self.tools
389 }
390
391 fn validate(&self) -> Result<(), PluginError> {
392 Ok(())
393 }
394
395 fn initialize(&self) -> Result<(), PluginError> {
396 Ok(())
397 }
398
399 fn shutdown(&self) -> Result<(), PluginError> {
400 Ok(())
401 }
402}
403
404impl Plugin for PluginDefinition {
405 fn metadata(&self) -> &PluginMetadata {
406 match self {
407 Self::Builtin(plugin) => plugin.metadata(),
408 Self::Bundled(plugin) => plugin.metadata(),
409 Self::External(plugin) => plugin.metadata(),
410 }
411 }
412
413 fn hooks(&self) -> &PluginHooks {
414 match self {
415 Self::Builtin(plugin) => plugin.hooks(),
416 Self::Bundled(plugin) => plugin.hooks(),
417 Self::External(plugin) => plugin.hooks(),
418 }
419 }
420
421 fn lifecycle(&self) -> &PluginLifecycle {
422 match self {
423 Self::Builtin(plugin) => plugin.lifecycle(),
424 Self::Bundled(plugin) => plugin.lifecycle(),
425 Self::External(plugin) => plugin.lifecycle(),
426 }
427 }
428
429 fn tools(&self) -> &[PluginTool] {
430 match self {
431 Self::Builtin(plugin) => plugin.tools(),
432 Self::Bundled(plugin) => plugin.tools(),
433 Self::External(plugin) => plugin.tools(),
434 }
435 }
436
437 fn validate(&self) -> Result<(), PluginError> {
438 match self {
439 Self::Builtin(plugin) => plugin.validate(),
440 Self::Bundled(plugin) => plugin.validate(),
441 Self::External(plugin) => plugin.validate(),
442 }
443 }
444
445 fn initialize(&self) -> Result<(), PluginError> {
446 match self {
447 Self::Builtin(plugin) => plugin.initialize(),
448 Self::Bundled(plugin) => plugin.initialize(),
449 Self::External(plugin) => plugin.initialize(),
450 }
451 }
452
453 fn shutdown(&self) -> Result<(), PluginError> {
454 match self {
455 Self::Builtin(plugin) => plugin.shutdown(),
456 Self::Bundled(plugin) => plugin.shutdown(),
457 Self::External(plugin) => plugin.shutdown(),
458 }
459 }
460}
461
462#[derive(Debug, Clone, PartialEq)]
463pub struct RegisteredPlugin {
464 definition: PluginDefinition,
465 enabled: bool,
466}
467
468impl RegisteredPlugin {
469 #[must_use]
470 pub fn new(definition: PluginDefinition, enabled: bool) -> Self {
471 Self {
472 definition,
473 enabled,
474 }
475 }
476
477 #[must_use]
478 pub fn metadata(&self) -> &PluginMetadata {
479 self.definition.metadata()
480 }
481
482 #[must_use]
483 pub fn hooks(&self) -> &PluginHooks {
484 self.definition.hooks()
485 }
486
487 #[must_use]
488 pub fn tools(&self) -> &[PluginTool] {
489 self.definition.tools()
490 }
491
492 #[must_use]
493 pub fn is_enabled(&self) -> bool {
494 self.enabled
495 }
496
497 pub fn validate(&self) -> Result<(), PluginError> {
498 self.definition.validate()
499 }
500
501 pub fn initialize(&self) -> Result<(), PluginError> {
502 self.definition.initialize()
503 }
504
505 pub fn shutdown(&self) -> Result<(), PluginError> {
506 self.definition.shutdown()
507 }
508
509 #[must_use]
510 pub fn summary(&self) -> PluginSummary {
511 PluginSummary {
512 metadata: self.metadata().clone(),
513 enabled: self.enabled,
514 }
515 }
516}
517
518#[derive(Debug, Clone, PartialEq, Eq)]
519pub struct PluginSummary {
520 pub metadata: PluginMetadata,
521 pub enabled: bool,
522}
523
524#[derive(Debug, Clone, Default, PartialEq)]
525pub struct PluginRegistry {
526 plugins: Vec<RegisteredPlugin>,
527}
528
529impl PluginRegistry {
530 #[must_use]
531 pub fn new(mut plugins: Vec<RegisteredPlugin>) -> Self {
532 plugins.sort_by(|left, right| left.metadata().id.cmp(&right.metadata().id));
533 Self { plugins }
534 }
535
536 #[must_use]
537 pub fn plugins(&self) -> &[RegisteredPlugin] {
538 &self.plugins
539 }
540
541 #[must_use]
542 pub fn get(&self, plugin_id: &str) -> Option<&RegisteredPlugin> {
543 self.plugins
544 .iter()
545 .find(|plugin| plugin.metadata().id == plugin_id)
546 }
547
548 #[must_use]
549 pub fn contains(&self, plugin_id: &str) -> bool {
550 self.get(plugin_id).is_some()
551 }
552
553 #[must_use]
554 pub fn summaries(&self) -> Vec<PluginSummary> {
555 self.plugins.iter().map(RegisteredPlugin::summary).collect()
556 }
557
558 pub fn aggregated_hooks(&self) -> Result<PluginHooks, PluginError> {
559 self.plugins
560 .iter()
561 .filter(|plugin| plugin.is_enabled())
562 .try_fold(PluginHooks::default(), |acc, plugin| {
563 plugin.validate()?;
564 Ok(acc.merged_with(plugin.hooks()))
565 })
566 }
567
568 pub fn aggregated_tools(&self) -> Result<Vec<PluginTool>, PluginError> {
569 let mut tools = Vec::new();
570 let mut seen_names = BTreeMap::new();
571 for plugin in self.plugins.iter().filter(|plugin| plugin.is_enabled()) {
572 plugin.validate()?;
573 for tool in plugin.tools() {
574 if let Some(existing_plugin) =
575 seen_names.insert(tool.definition().name.clone(), tool.plugin_id().to_string())
576 {
577 return Err(PluginError::InvalidManifest(format!(
578 "plugin tool `{}` is defined by both `{existing_plugin}` and `{}`",
579 tool.definition().name,
580 tool.plugin_id()
581 )));
582 }
583 tools.push(tool.clone());
584 }
585 }
586 Ok(tools)
587 }
588
589 pub fn initialize(&self) -> Result<(), PluginError> {
590 for plugin in self.plugins.iter().filter(|plugin| plugin.is_enabled()) {
591 plugin.validate()?;
592 plugin.initialize()?;
593 }
594 Ok(())
595 }
596
597 pub fn shutdown(&self) -> Result<(), PluginError> {
598 for plugin in self
599 .plugins
600 .iter()
601 .rev()
602 .filter(|plugin| plugin.is_enabled())
603 {
604 plugin.shutdown()?;
605 }
606 Ok(())
607 }
608}
609
610#[derive(Debug, Clone, PartialEq, Eq)]
611pub struct PluginManagerConfig {
612 pub config_home: PathBuf,
613 pub enabled_plugins: BTreeMap<String, bool>,
614 pub external_dirs: Vec<PathBuf>,
615 pub install_root: Option<PathBuf>,
616 pub registry_path: Option<PathBuf>,
617 pub bundled_root: Option<PathBuf>,
618}
619
620impl PluginManagerConfig {
621 #[must_use]
622 pub fn new(config_home: impl Into<PathBuf>) -> Self {
623 Self {
624 config_home: config_home.into(),
625 enabled_plugins: BTreeMap::new(),
626 external_dirs: Vec::new(),
627 install_root: None,
628 registry_path: None,
629 bundled_root: None,
630 }
631 }
632}
633#[derive(Debug, Clone, PartialEq, Eq)]
634pub struct PluginManager {
635 pub(crate) config: PluginManagerConfig,
636}
637
638#[derive(Debug, Clone, PartialEq, Eq)]
639pub struct InstallOutcome {
640 pub plugin_id: String,
641 pub version: String,
642 pub install_path: PathBuf,
643}
644
645#[derive(Debug, Clone, PartialEq, Eq)]
646pub struct UpdateOutcome {
647 pub plugin_id: String,
648 pub old_version: String,
649 pub new_version: String,
650 pub install_path: PathBuf,
651}