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
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}