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
303#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
304#[serde(tag = "type", rename_all = "snake_case")]
305pub enum PluginInstallSource {
306 LocalPath { path: PathBuf },
307 GitUrl { url: String },
308 Embedded,
309}
310
311#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
312pub struct InstalledPluginRecord {
313 #[serde(default = "default_plugin_kind")]
314 pub kind: PluginKind,
315 pub id: String,
316 pub name: String,
317 pub version: String,
318 pub description: String,
319 pub install_path: PathBuf,
320 pub source: PluginInstallSource,
321 pub installed_at_unix_ms: u128,
322 pub updated_at_unix_ms: u128,
323}
324
325#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
326pub struct InstalledPluginRegistry {
327 #[serde(default)]
328 pub plugins: BTreeMap<String, InstalledPluginRecord>,
329}
330
331fn default_plugin_kind() -> PluginKind {
332 PluginKind::External
333}
334
335#[derive(Debug, Clone, PartialEq)]
336pub struct BuiltinPlugin {
337 pub(crate) metadata: PluginMetadata,
338 pub(crate) hooks: PluginHooks,
339 pub(crate) lifecycle: PluginLifecycle,
340 pub(crate) tools: Vec<PluginTool>,
341}
342
343#[derive(Debug, Clone, PartialEq)]
344pub struct BundledPlugin {
345 pub(crate) metadata: PluginMetadata,
346 pub(crate) hooks: PluginHooks,
347 pub(crate) lifecycle: PluginLifecycle,
348 pub(crate) tools: Vec<PluginTool>,
349}
350
351#[derive(Debug, Clone, PartialEq)]
352pub struct ExternalPlugin {
353 pub(crate) metadata: PluginMetadata,
354 pub(crate) hooks: PluginHooks,
355 pub(crate) lifecycle: PluginLifecycle,
356 pub(crate) tools: Vec<PluginTool>,
357}
358
359pub trait Plugin {
360 fn metadata(&self) -> &PluginMetadata;
361 fn hooks(&self) -> &PluginHooks;
362 fn lifecycle(&self) -> &PluginLifecycle;
363 fn tools(&self) -> &[PluginTool];
364 fn validate(&self) -> Result<(), PluginError>;
365 fn initialize(&self) -> Result<(), PluginError>;
366 fn shutdown(&self) -> Result<(), PluginError>;
367}
368
369#[derive(Debug, Clone, PartialEq)]
370pub enum PluginDefinition {
371 Builtin(BuiltinPlugin),
372 Bundled(BundledPlugin),
373 External(ExternalPlugin),
374}
375
376impl Plugin for BuiltinPlugin {
377 fn metadata(&self) -> &PluginMetadata {
378 &self.metadata
379 }
380
381 fn hooks(&self) -> &PluginHooks {
382 &self.hooks
383 }
384
385 fn lifecycle(&self) -> &PluginLifecycle {
386 &self.lifecycle
387 }
388
389 fn tools(&self) -> &[PluginTool] {
390 &self.tools
391 }
392
393 fn validate(&self) -> Result<(), PluginError> {
394 Ok(())
395 }
396
397 fn initialize(&self) -> Result<(), PluginError> {
398 Ok(())
399 }
400
401 fn shutdown(&self) -> Result<(), PluginError> {
402 Ok(())
403 }
404}
405
406impl Plugin for PluginDefinition {
407 fn metadata(&self) -> &PluginMetadata {
408 match self {
409 Self::Builtin(plugin) => plugin.metadata(),
410 Self::Bundled(plugin) => plugin.metadata(),
411 Self::External(plugin) => plugin.metadata(),
412 }
413 }
414
415 fn hooks(&self) -> &PluginHooks {
416 match self {
417 Self::Builtin(plugin) => plugin.hooks(),
418 Self::Bundled(plugin) => plugin.hooks(),
419 Self::External(plugin) => plugin.hooks(),
420 }
421 }
422
423 fn lifecycle(&self) -> &PluginLifecycle {
424 match self {
425 Self::Builtin(plugin) => plugin.lifecycle(),
426 Self::Bundled(plugin) => plugin.lifecycle(),
427 Self::External(plugin) => plugin.lifecycle(),
428 }
429 }
430
431 fn tools(&self) -> &[PluginTool] {
432 match self {
433 Self::Builtin(plugin) => plugin.tools(),
434 Self::Bundled(plugin) => plugin.tools(),
435 Self::External(plugin) => plugin.tools(),
436 }
437 }
438
439 fn validate(&self) -> Result<(), PluginError> {
440 match self {
441 Self::Builtin(plugin) => plugin.validate(),
442 Self::Bundled(plugin) => plugin.validate(),
443 Self::External(plugin) => plugin.validate(),
444 }
445 }
446
447 fn initialize(&self) -> Result<(), PluginError> {
448 match self {
449 Self::Builtin(plugin) => plugin.initialize(),
450 Self::Bundled(plugin) => plugin.initialize(),
451 Self::External(plugin) => plugin.initialize(),
452 }
453 }
454
455 fn shutdown(&self) -> Result<(), PluginError> {
456 match self {
457 Self::Builtin(plugin) => plugin.shutdown(),
458 Self::Bundled(plugin) => plugin.shutdown(),
459 Self::External(plugin) => plugin.shutdown(),
460 }
461 }
462}
463
464#[derive(Debug, Clone, PartialEq)]
465pub struct RegisteredPlugin {
466 definition: PluginDefinition,
467 enabled: bool,
468}
469
470impl RegisteredPlugin {
471 #[must_use]
472 pub fn new(definition: PluginDefinition, enabled: bool) -> Self {
473 Self {
474 definition,
475 enabled,
476 }
477 }
478
479 #[must_use]
480 pub fn metadata(&self) -> &PluginMetadata {
481 self.definition.metadata()
482 }
483
484 #[must_use]
485 pub fn hooks(&self) -> &PluginHooks {
486 self.definition.hooks()
487 }
488
489 #[must_use]
490 pub fn tools(&self) -> &[PluginTool] {
491 self.definition.tools()
492 }
493
494 #[must_use]
495 pub fn is_enabled(&self) -> bool {
496 self.enabled
497 }
498
499 pub fn validate(&self) -> Result<(), PluginError> {
500 self.definition.validate()
501 }
502
503 pub fn initialize(&self) -> Result<(), PluginError> {
504 self.definition.initialize()
505 }
506
507 pub fn shutdown(&self) -> Result<(), PluginError> {
508 self.definition.shutdown()
509 }
510
511 #[must_use]
512 pub fn summary(&self) -> PluginSummary {
513 PluginSummary {
514 metadata: self.metadata().clone(),
515 enabled: self.enabled,
516 }
517 }
518}
519
520#[derive(Debug, Clone, PartialEq, Eq)]
521pub struct PluginSummary {
522 pub metadata: PluginMetadata,
523 pub enabled: bool,
524}
525
526#[derive(Debug, Clone, Default, PartialEq)]
527pub struct PluginRegistry {
528 plugins: Vec<RegisteredPlugin>,
529}
530
531impl PluginRegistry {
532 #[must_use]
533 pub fn new(mut plugins: Vec<RegisteredPlugin>) -> Self {
534 plugins.sort_by(|left, right| left.metadata().id.cmp(&right.metadata().id));
535 Self { plugins }
536 }
537
538 #[must_use]
539 pub fn plugins(&self) -> &[RegisteredPlugin] {
540 &self.plugins
541 }
542
543 #[must_use]
544 pub fn get(&self, plugin_id: &str) -> Option<&RegisteredPlugin> {
545 self.plugins
546 .iter()
547 .find(|plugin| plugin.metadata().id == plugin_id)
548 }
549
550 #[must_use]
551 pub fn contains(&self, plugin_id: &str) -> bool {
552 self.get(plugin_id).is_some()
553 }
554
555 #[must_use]
556 pub fn summaries(&self) -> Vec<PluginSummary> {
557 self.plugins.iter().map(RegisteredPlugin::summary).collect()
558 }
559
560 pub fn aggregated_hooks(&self) -> Result<PluginHooks, PluginError> {
561 self.plugins
562 .iter()
563 .filter(|plugin| plugin.is_enabled())
564 .try_fold(PluginHooks::default(), |acc, plugin| {
565 plugin.validate()?;
566 Ok(acc.merged_with(plugin.hooks()))
567 })
568 }
569
570 pub fn aggregated_tools(&self) -> Result<Vec<PluginTool>, PluginError> {
571 let mut tools = Vec::new();
572 let mut seen_names = BTreeMap::new();
573 for plugin in self.plugins.iter().filter(|plugin| plugin.is_enabled()) {
574 plugin.validate()?;
575 for tool in plugin.tools() {
576 if let Some(existing_plugin) =
577 seen_names.insert(tool.definition().name.clone(), tool.plugin_id().to_string())
578 {
579 return Err(PluginError::InvalidManifest(format!(
580 "plugin tool `{}` is defined by both `{existing_plugin}` and `{}`",
581 tool.definition().name,
582 tool.plugin_id()
583 )));
584 }
585 tools.push(tool.clone());
586 }
587 }
588 Ok(tools)
589 }
590
591 pub fn initialize(&self) -> Result<(), PluginError> {
592 for plugin in self.plugins.iter().filter(|plugin| plugin.is_enabled()) {
593 plugin.validate()?;
594 plugin.initialize()?;
595 }
596 Ok(())
597 }
598
599 pub fn shutdown(&self) -> Result<(), PluginError> {
600 for plugin in self
601 .plugins
602 .iter()
603 .rev()
604 .filter(|plugin| plugin.is_enabled())
605 {
606 plugin.shutdown()?;
607 }
608 Ok(())
609 }
610}
611
612#[derive(Debug, Clone, PartialEq, Eq)]
613pub struct PluginManagerConfig {
614 pub config_home: PathBuf,
615 pub enabled_plugins: BTreeMap<String, bool>,
616 pub external_dirs: Vec<PathBuf>,
617 pub install_root: Option<PathBuf>,
618 pub registry_path: Option<PathBuf>,
619 pub bundled_root: Option<PathBuf>,
620}
621
622impl PluginManagerConfig {
623 #[must_use]
624 pub fn new(config_home: impl Into<PathBuf>) -> Self {
625 Self {
626 config_home: config_home.into(),
627 enabled_plugins: BTreeMap::new(),
628 external_dirs: Vec::new(),
629 install_root: None,
630 registry_path: None,
631 bundled_root: None,
632 }
633 }
634}
635#[derive(Debug, Clone, PartialEq, Eq)]
636pub struct PluginManager {
637 pub(crate) config: PluginManagerConfig,
638}
639
640#[derive(Debug, Clone, PartialEq, Eq)]
641pub struct InstallOutcome {
642 pub plugin_id: String,
643 pub version: String,
644 pub install_path: PathBuf,
645}
646
647#[derive(Debug, Clone, PartialEq, Eq)]
648pub struct UpdateOutcome {
649 pub plugin_id: String,
650 pub old_version: String,
651 pub new_version: String,
652 pub install_path: PathBuf,
653}