1use std::path::{Path, PathBuf};
2use std::process::Command;
3
4use serde::{Deserialize, Serialize};
5use serde_json::Value;
6
7use crate::manifest::{
8 PluginError, PluginHooks, PluginLifecycle, PluginManifest, PluginToolPermission,
9};
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
12pub enum PluginKind {
13 Builtin,
14 Bundled,
15 External,
16}
17
18impl PluginKind {
19 pub fn marketplace(self) -> &'static str {
20 match self {
21 Self::Builtin => "builtin",
22 Self::Bundled => "bundled",
23 Self::External => "external",
24 }
25 }
26}
27
28impl std::fmt::Display for PluginKind {
29 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
30 match self {
31 Self::Builtin => write!(f, "builtin"),
32 Self::Bundled => write!(f, "bundled"),
33 Self::External => write!(f, "external"),
34 }
35 }
36}
37
38#[derive(Debug, Clone)]
39pub struct PluginMetadata {
40 pub id: String,
41 pub name: String,
42 pub version: String,
43 pub description: String,
44 pub kind: PluginKind,
45 pub source: String,
46 pub default_enabled: bool,
47 pub root: Option<PathBuf>,
48}
49
50#[derive(Debug, Clone)]
51pub struct PluginTool {
52 pub plugin_id: String,
53 pub plugin_name: String,
54 pub name: String,
55 pub description: String,
56 pub input_schema: Value,
57 pub command: String,
58 pub args: Vec<String>,
59 pub required_permission: PluginToolPermission,
60 pub root: Option<PathBuf>,
61}
62
63impl PluginTool {
64 pub fn execute(&self, input: &Value) -> Result<String, PluginError> {
65 let input_json = input.to_string();
66 let mut cmd = Command::new(&self.command);
67 cmd.args(&self.args)
68 .stdin(std::process::Stdio::piped())
69 .stdout(std::process::Stdio::piped())
70 .stderr(std::process::Stdio::piped())
71 .env("PLEIADES_PLUGIN_ID", &self.plugin_id)
72 .env("PLEIADES_PLUGIN_NAME", &self.plugin_name)
73 .env("PLEIADES_TOOL_NAME", &self.name)
74 .env("PLEIADES_TOOL_INPUT", &input_json);
75
76 if let Some(root) = &self.root {
77 cmd.current_dir(root)
78 .env("PLEIADES_PLUGIN_ROOT", root.display().to_string());
79 }
80
81 let mut child = cmd.spawn()?;
82 if let Some(mut stdin) = child.stdin.take() {
83 use std::io::Write;
84 let _ = stdin.write_all(input_json.as_bytes());
85 }
86
87 let output = child.wait_with_output()?;
88 if output.status.success() {
89 Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
90 } else {
91 let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
92 Err(PluginError::CommandFailed(format!(
93 "plugin tool `{}` from `{}` failed: {}",
94 self.name,
95 self.plugin_id,
96 if stderr.is_empty() {
97 format!("exit status {}", output.status)
98 } else {
99 stderr
100 },
101 )))
102 }
103 }
104}
105
106pub trait Plugin: std::fmt::Debug {
107 fn metadata(&self) -> &PluginMetadata;
108 fn hooks(&self) -> &PluginHooks;
109 fn lifecycle(&self) -> &PluginLifecycle;
110 fn tools(&self) -> &[PluginTool];
111}
112
113#[derive(Debug, Clone)]
114pub struct BuiltinPlugin {
115 pub metadata: PluginMetadata,
116 pub hooks: PluginHooks,
117 pub lifecycle: PluginLifecycle,
118 pub tools: Vec<PluginTool>,
119}
120
121impl Plugin for BuiltinPlugin {
122 fn metadata(&self) -> &PluginMetadata {
123 &self.metadata
124 }
125 fn hooks(&self) -> &PluginHooks {
126 &self.hooks
127 }
128 fn lifecycle(&self) -> &PluginLifecycle {
129 &self.lifecycle
130 }
131 fn tools(&self) -> &[PluginTool] {
132 &self.tools
133 }
134}
135
136#[derive(Debug, Clone)]
137pub struct BundledPlugin {
138 pub metadata: PluginMetadata,
139 pub hooks: PluginHooks,
140 pub lifecycle: PluginLifecycle,
141 pub tools: Vec<PluginTool>,
142}
143
144impl Plugin for BundledPlugin {
145 fn metadata(&self) -> &PluginMetadata {
146 &self.metadata
147 }
148 fn hooks(&self) -> &PluginHooks {
149 &self.hooks
150 }
151 fn lifecycle(&self) -> &PluginLifecycle {
152 &self.lifecycle
153 }
154 fn tools(&self) -> &[PluginTool] {
155 &self.tools
156 }
157}
158
159#[derive(Debug, Clone)]
160pub struct ExternalPlugin {
161 pub metadata: PluginMetadata,
162 pub hooks: PluginHooks,
163 pub lifecycle: PluginLifecycle,
164 pub tools: Vec<PluginTool>,
165}
166
167impl Plugin for ExternalPlugin {
168 fn metadata(&self) -> &PluginMetadata {
169 &self.metadata
170 }
171 fn hooks(&self) -> &PluginHooks {
172 &self.hooks
173 }
174 fn lifecycle(&self) -> &PluginLifecycle {
175 &self.lifecycle
176 }
177 fn tools(&self) -> &[PluginTool] {
178 &self.tools
179 }
180}
181
182#[derive(Debug, Clone)]
183pub enum PluginDefinition {
184 Builtin(BuiltinPlugin),
185 Bundled(BundledPlugin),
186 External(ExternalPlugin),
187}
188
189impl Plugin for PluginDefinition {
190 fn metadata(&self) -> &PluginMetadata {
191 match self {
192 Self::Builtin(p) => p.metadata(),
193 Self::Bundled(p) => p.metadata(),
194 Self::External(p) => p.metadata(),
195 }
196 }
197
198 fn hooks(&self) -> &PluginHooks {
199 match self {
200 Self::Builtin(p) => p.hooks(),
201 Self::Bundled(p) => p.hooks(),
202 Self::External(p) => p.hooks(),
203 }
204 }
205
206 fn lifecycle(&self) -> &PluginLifecycle {
207 match self {
208 Self::Builtin(p) => p.lifecycle(),
209 Self::Bundled(p) => p.lifecycle(),
210 Self::External(p) => p.lifecycle(),
211 }
212 }
213
214 fn tools(&self) -> &[PluginTool] {
215 match self {
216 Self::Builtin(p) => p.tools(),
217 Self::Bundled(p) => p.tools(),
218 Self::External(p) => p.tools(),
219 }
220 }
221}
222
223impl PluginDefinition {
224 pub fn load_from_directory(
225 root: &Path,
226 kind: PluginKind,
227 source: String,
228 marketplace: &str,
229 ) -> Result<Self, PluginError> {
230 let manifest = PluginManifest::load_from_directory(root)?;
231 let plugin_id = format!("{}-{}", manifest.name, marketplace);
232 let metadata = PluginMetadata {
233 id: plugin_id,
234 name: manifest.name,
235 version: manifest.version,
236 description: manifest.description,
237 kind,
238 source,
239 default_enabled: manifest.default_enabled,
240 root: Some(root.to_path_buf()),
241 };
242 let hooks = manifest.hooks;
243 let lifecycle = manifest.lifecycle;
244 let tools = manifest
245 .tools
246 .into_iter()
247 .map(|t| PluginTool {
248 plugin_id: metadata.id.clone(),
249 plugin_name: metadata.name.clone(),
250 name: t.name,
251 description: t.description,
252 input_schema: t.input_schema,
253 command: resolve_path(root, &t.command),
254 args: t.args,
255 required_permission: t.required_permission,
256 root: Some(root.to_path_buf()),
257 })
258 .collect();
259
260 Ok(match kind {
261 PluginKind::Builtin => Self::Builtin(BuiltinPlugin {
262 metadata,
263 hooks,
264 lifecycle,
265 tools,
266 }),
267 PluginKind::Bundled => Self::Bundled(BundledPlugin {
268 metadata,
269 hooks,
270 lifecycle,
271 tools,
272 }),
273 PluginKind::External => Self::External(ExternalPlugin {
274 metadata,
275 hooks,
276 lifecycle,
277 tools,
278 }),
279 })
280 }
281}
282
283fn resolve_path(root: &Path, path: &str) -> String {
284 if Path::new(path).is_absolute() {
285 path.to_string()
286 } else {
287 root.join(path).to_string_lossy().to_string()
288 }
289}