Skip to main content

codineer_plugins/
types.rs

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}