Skip to main content

codineer_plugins/
resolve.rs

1use std::path::{Path, PathBuf};
2
3use crate::error::PluginError;
4use crate::lifecycle::run_lifecycle_commands;
5use crate::types::{
6    BundledPlugin, ExternalPlugin, Plugin, PluginHooks, PluginLifecycle, PluginMetadata,
7    PluginTool, PluginToolDefinition, PluginToolManifest,
8};
9
10pub(crate) fn resolve_hooks(root: &Path, hooks: &PluginHooks) -> PluginHooks {
11    PluginHooks {
12        pre_tool_use: hooks
13            .pre_tool_use
14            .iter()
15            .map(|entry| resolve_hook_entry(root, entry))
16            .collect(),
17        post_tool_use: hooks
18            .post_tool_use
19            .iter()
20            .map(|entry| resolve_hook_entry(root, entry))
21            .collect(),
22    }
23}
24
25pub(crate) fn resolve_lifecycle(root: &Path, lifecycle: &PluginLifecycle) -> PluginLifecycle {
26    PluginLifecycle {
27        init: lifecycle
28            .init
29            .iter()
30            .map(|entry| resolve_hook_entry(root, entry))
31            .collect(),
32        shutdown: lifecycle
33            .shutdown
34            .iter()
35            .map(|entry| resolve_hook_entry(root, entry))
36            .collect(),
37    }
38}
39
40pub(crate) fn resolve_tools(
41    root: &Path,
42    plugin_id: &str,
43    plugin_name: &str,
44    tools: &[PluginToolManifest],
45) -> Vec<PluginTool> {
46    tools
47        .iter()
48        .map(|tool| {
49            PluginTool::new(
50                plugin_id,
51                plugin_name,
52                PluginToolDefinition {
53                    name: tool.name.clone(),
54                    description: Some(tool.description.clone()),
55                    input_schema: tool.input_schema.clone(),
56                },
57                resolve_hook_entry(root, &tool.command),
58                tool.args.clone(),
59                tool.required_permission,
60                Some(root.to_path_buf()),
61            )
62        })
63        .collect()
64}
65
66fn validate_hook_paths(root: Option<&Path>, hooks: &PluginHooks) -> Result<(), PluginError> {
67    let Some(root) = root else {
68        return Ok(());
69    };
70    for entry in hooks.pre_tool_use.iter().chain(hooks.post_tool_use.iter()) {
71        validate_command_path(root, entry, "hook")?;
72    }
73    Ok(())
74}
75
76fn validate_lifecycle_paths(
77    root: Option<&Path>,
78    lifecycle: &PluginLifecycle,
79) -> Result<(), PluginError> {
80    let Some(root) = root else {
81        return Ok(());
82    };
83    for entry in lifecycle.init.iter().chain(lifecycle.shutdown.iter()) {
84        validate_command_path(root, entry, "lifecycle command")?;
85    }
86    Ok(())
87}
88
89fn validate_tool_paths(root: Option<&Path>, tools: &[PluginTool]) -> Result<(), PluginError> {
90    let Some(root) = root else {
91        return Ok(());
92    };
93    for tool in tools {
94        validate_command_path(root, &tool.command, "tool")?;
95    }
96    Ok(())
97}
98
99fn validate_command_path(root: &Path, entry: &str, kind: &str) -> Result<(), PluginError> {
100    if is_literal_command(entry) {
101        return Ok(());
102    }
103    let path = if Path::new(entry).is_absolute() {
104        PathBuf::from(entry)
105    } else {
106        root.join(entry)
107    };
108    if !path.exists() {
109        return Err(PluginError::InvalidManifest(format!(
110            "{kind} path `{}` does not exist",
111            path.display()
112        )));
113    }
114    Ok(())
115}
116
117fn resolve_hook_entry(root: &Path, entry: &str) -> String {
118    if is_literal_command(entry) {
119        entry.to_string()
120    } else {
121        root.join(entry).display().to_string()
122    }
123}
124
125use crate::constants::is_literal_command;
126
127impl Plugin for BundledPlugin {
128    fn metadata(&self) -> &PluginMetadata {
129        &self.metadata
130    }
131
132    fn hooks(&self) -> &PluginHooks {
133        &self.hooks
134    }
135
136    fn lifecycle(&self) -> &PluginLifecycle {
137        &self.lifecycle
138    }
139
140    fn tools(&self) -> &[PluginTool] {
141        &self.tools
142    }
143
144    fn validate(&self) -> Result<(), PluginError> {
145        validate_hook_paths(self.metadata.root.as_deref(), &self.hooks)?;
146        validate_lifecycle_paths(self.metadata.root.as_deref(), &self.lifecycle)?;
147        validate_tool_paths(self.metadata.root.as_deref(), &self.tools)
148    }
149
150    fn initialize(&self) -> Result<(), PluginError> {
151        run_lifecycle_commands(
152            self.metadata(),
153            self.lifecycle(),
154            "init",
155            &self.lifecycle.init,
156        )
157    }
158
159    fn shutdown(&self) -> Result<(), PluginError> {
160        run_lifecycle_commands(
161            self.metadata(),
162            self.lifecycle(),
163            "shutdown",
164            &self.lifecycle.shutdown,
165        )
166    }
167}
168
169impl Plugin for ExternalPlugin {
170    fn metadata(&self) -> &PluginMetadata {
171        &self.metadata
172    }
173
174    fn hooks(&self) -> &PluginHooks {
175        &self.hooks
176    }
177
178    fn lifecycle(&self) -> &PluginLifecycle {
179        &self.lifecycle
180    }
181
182    fn tools(&self) -> &[PluginTool] {
183        &self.tools
184    }
185
186    fn validate(&self) -> Result<(), PluginError> {
187        validate_hook_paths(self.metadata.root.as_deref(), &self.hooks)?;
188        validate_lifecycle_paths(self.metadata.root.as_deref(), &self.lifecycle)?;
189        validate_tool_paths(self.metadata.root.as_deref(), &self.tools)
190    }
191
192    fn initialize(&self) -> Result<(), PluginError> {
193        run_lifecycle_commands(
194            self.metadata(),
195            self.lifecycle(),
196            "init",
197            &self.lifecycle.init,
198        )
199    }
200
201    fn shutdown(&self) -> Result<(), PluginError> {
202        run_lifecycle_commands(
203            self.metadata(),
204            self.lifecycle(),
205            "shutdown",
206            &self.lifecycle.shutdown,
207        )
208    }
209}