|
@@ -2,7 +2,7 @@ use std::collections::BTreeMap;
|
|
|
use std::fmt::{Display, Formatter};
|
|
use std::fmt::{Display, Formatter};
|
|
|
use std::fs;
|
|
use std::fs;
|
|
|
use std::path::{Path, PathBuf};
|
|
use std::path::{Path, PathBuf};
|
|
|
-use std::process::Command;
|
|
|
|
|
|
|
+use std::process::{Command, Stdio};
|
|
|
use std::time::{SystemTime, UNIX_EPOCH};
|
|
use std::time::{SystemTime, UNIX_EPOCH};
|
|
|
|
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
use serde::{Deserialize, Serialize};
|
|
@@ -13,7 +13,9 @@ const BUILTIN_MARKETPLACE: &str = "builtin";
|
|
|
const BUNDLED_MARKETPLACE: &str = "bundled";
|
|
const BUNDLED_MARKETPLACE: &str = "bundled";
|
|
|
const SETTINGS_FILE_NAME: &str = "settings.json";
|
|
const SETTINGS_FILE_NAME: &str = "settings.json";
|
|
|
const REGISTRY_FILE_NAME: &str = "installed.json";
|
|
const REGISTRY_FILE_NAME: &str = "installed.json";
|
|
|
|
|
+const MANIFEST_FILE_NAME: &str = "plugin.json";
|
|
|
const MANIFEST_RELATIVE_PATH: &str = ".claude-plugin/plugin.json";
|
|
const MANIFEST_RELATIVE_PATH: &str = ".claude-plugin/plugin.json";
|
|
|
|
|
+const PACKAGE_MANIFEST_RELATIVE_PATH: &str = MANIFEST_RELATIVE_PATH;
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
#[serde(rename_all = "lowercase")]
|
|
#[serde(rename_all = "lowercase")]
|
|
@@ -87,17 +89,150 @@ impl PluginLifecycle {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
|
|
|
|
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
|
pub struct PluginManifest {
|
|
pub struct PluginManifest {
|
|
|
pub name: String,
|
|
pub name: String,
|
|
|
pub version: String,
|
|
pub version: String,
|
|
|
pub description: String,
|
|
pub description: String,
|
|
|
|
|
+ #[serde(default)]
|
|
|
|
|
+ pub permissions: Vec<String>,
|
|
|
#[serde(rename = "defaultEnabled", default)]
|
|
#[serde(rename = "defaultEnabled", default)]
|
|
|
pub default_enabled: bool,
|
|
pub default_enabled: bool,
|
|
|
#[serde(default)]
|
|
#[serde(default)]
|
|
|
pub hooks: PluginHooks,
|
|
pub hooks: PluginHooks,
|
|
|
#[serde(default)]
|
|
#[serde(default)]
|
|
|
pub lifecycle: PluginLifecycle,
|
|
pub lifecycle: PluginLifecycle,
|
|
|
|
|
+ #[serde(default)]
|
|
|
|
|
+ pub tools: Vec<PluginToolManifest>,
|
|
|
|
|
+ #[serde(default)]
|
|
|
|
|
+ pub commands: Vec<PluginCommandManifest>,
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
|
|
|
+pub struct PluginToolManifest {
|
|
|
|
|
+ pub name: String,
|
|
|
|
|
+ pub description: String,
|
|
|
|
|
+ #[serde(rename = "inputSchema")]
|
|
|
|
|
+ pub input_schema: Value,
|
|
|
|
|
+ pub command: String,
|
|
|
|
|
+ #[serde(default)]
|
|
|
|
|
+ pub args: Vec<String>,
|
|
|
|
|
+ #[serde(rename = "requiredPermission", default = "default_tool_permission")]
|
|
|
|
|
+ pub required_permission: String,
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
|
|
|
+pub struct PluginToolDefinition {
|
|
|
|
|
+ pub name: String,
|
|
|
|
|
+ #[serde(default)]
|
|
|
|
|
+ pub description: Option<String>,
|
|
|
|
|
+ #[serde(rename = "inputSchema")]
|
|
|
|
|
+ pub input_schema: Value,
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
|
|
+pub struct PluginCommandManifest {
|
|
|
|
|
+ pub name: String,
|
|
|
|
|
+ pub description: String,
|
|
|
|
|
+ pub command: String,
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+type PluginPackageManifest = PluginManifest;
|
|
|
|
|
+
|
|
|
|
|
+#[derive(Debug, Clone, PartialEq)]
|
|
|
|
|
+pub struct PluginTool {
|
|
|
|
|
+ plugin_id: String,
|
|
|
|
|
+ plugin_name: String,
|
|
|
|
|
+ definition: PluginToolDefinition,
|
|
|
|
|
+ command: String,
|
|
|
|
|
+ args: Vec<String>,
|
|
|
|
|
+ required_permission: String,
|
|
|
|
|
+ root: Option<PathBuf>,
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+impl PluginTool {
|
|
|
|
|
+ #[must_use]
|
|
|
|
|
+ pub fn new(
|
|
|
|
|
+ plugin_id: impl Into<String>,
|
|
|
|
|
+ plugin_name: impl Into<String>,
|
|
|
|
|
+ definition: PluginToolDefinition,
|
|
|
|
|
+ command: impl Into<String>,
|
|
|
|
|
+ args: Vec<String>,
|
|
|
|
|
+ required_permission: impl Into<String>,
|
|
|
|
|
+ root: Option<PathBuf>,
|
|
|
|
|
+ ) -> Self {
|
|
|
|
|
+ Self {
|
|
|
|
|
+ plugin_id: plugin_id.into(),
|
|
|
|
|
+ plugin_name: plugin_name.into(),
|
|
|
|
|
+ definition,
|
|
|
|
|
+ command: command.into(),
|
|
|
|
|
+ args,
|
|
|
|
|
+ required_permission: required_permission.into(),
|
|
|
|
|
+ root,
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ #[must_use]
|
|
|
|
|
+ pub fn plugin_id(&self) -> &str {
|
|
|
|
|
+ &self.plugin_id
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ #[must_use]
|
|
|
|
|
+ pub fn definition(&self) -> &PluginToolDefinition {
|
|
|
|
|
+ &self.definition
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ #[must_use]
|
|
|
|
|
+ pub fn required_permission(&self) -> &str {
|
|
|
|
|
+ &self.required_permission
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ pub fn execute(&self, input: &Value) -> Result<String, PluginError> {
|
|
|
|
|
+ let input_json = input.to_string();
|
|
|
|
|
+ let mut process = Command::new(&self.command);
|
|
|
|
|
+ process
|
|
|
|
|
+ .args(&self.args)
|
|
|
|
|
+ .stdin(Stdio::piped())
|
|
|
|
|
+ .stdout(Stdio::piped())
|
|
|
|
|
+ .stderr(Stdio::piped())
|
|
|
|
|
+ .env("CLAWD_PLUGIN_ID", &self.plugin_id)
|
|
|
|
|
+ .env("CLAWD_PLUGIN_NAME", &self.plugin_name)
|
|
|
|
|
+ .env("CLAWD_TOOL_NAME", &self.definition.name)
|
|
|
|
|
+ .env("CLAWD_TOOL_INPUT", &input_json);
|
|
|
|
|
+ if let Some(root) = &self.root {
|
|
|
|
|
+ process
|
|
|
|
|
+ .current_dir(root)
|
|
|
|
|
+ .env("CLAWD_PLUGIN_ROOT", root.display().to_string());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ let mut child = process.spawn()?;
|
|
|
|
|
+ if let Some(stdin) = child.stdin.as_mut() {
|
|
|
|
|
+ use std::io::Write as _;
|
|
|
|
|
+ stdin.write_all(input_json.as_bytes())?;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ let output = child.wait_with_output()?;
|
|
|
|
|
+ if output.status.success() {
|
|
|
|
|
+ Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
|
|
|
|
|
+ } else {
|
|
|
|
|
+ let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
|
|
|
|
|
+ Err(PluginError::CommandFailed(format!(
|
|
|
|
|
+ "plugin tool `{}` from `{}` failed for `{}`: {}",
|
|
|
|
|
+ self.definition.name,
|
|
|
|
|
+ self.plugin_id,
|
|
|
|
|
+ self.command,
|
|
|
|
|
+ if stderr.is_empty() {
|
|
|
|
|
+ format!("exit status {}", output.status)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ stderr
|
|
|
|
|
+ }
|
|
|
|
|
+ )))
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+fn default_tool_permission() -> String {
|
|
|
|
|
+ "danger-full-access".to_string()
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
@@ -125,37 +260,41 @@ pub struct InstalledPluginRegistry {
|
|
|
pub plugins: BTreeMap<String, InstalledPluginRecord>,
|
|
pub plugins: BTreeMap<String, InstalledPluginRecord>,
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-#[derive(Debug, Clone, PartialEq, Eq)]
|
|
|
|
|
|
|
+#[derive(Debug, Clone, PartialEq)]
|
|
|
pub struct BuiltinPlugin {
|
|
pub struct BuiltinPlugin {
|
|
|
metadata: PluginMetadata,
|
|
metadata: PluginMetadata,
|
|
|
hooks: PluginHooks,
|
|
hooks: PluginHooks,
|
|
|
lifecycle: PluginLifecycle,
|
|
lifecycle: PluginLifecycle,
|
|
|
|
|
+ tools: Vec<PluginTool>,
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-#[derive(Debug, Clone, PartialEq, Eq)]
|
|
|
|
|
|
|
+#[derive(Debug, Clone, PartialEq)]
|
|
|
pub struct BundledPlugin {
|
|
pub struct BundledPlugin {
|
|
|
metadata: PluginMetadata,
|
|
metadata: PluginMetadata,
|
|
|
hooks: PluginHooks,
|
|
hooks: PluginHooks,
|
|
|
lifecycle: PluginLifecycle,
|
|
lifecycle: PluginLifecycle,
|
|
|
|
|
+ tools: Vec<PluginTool>,
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-#[derive(Debug, Clone, PartialEq, Eq)]
|
|
|
|
|
|
|
+#[derive(Debug, Clone, PartialEq)]
|
|
|
pub struct ExternalPlugin {
|
|
pub struct ExternalPlugin {
|
|
|
metadata: PluginMetadata,
|
|
metadata: PluginMetadata,
|
|
|
hooks: PluginHooks,
|
|
hooks: PluginHooks,
|
|
|
lifecycle: PluginLifecycle,
|
|
lifecycle: PluginLifecycle,
|
|
|
|
|
+ tools: Vec<PluginTool>,
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
pub trait Plugin {
|
|
pub trait Plugin {
|
|
|
fn metadata(&self) -> &PluginMetadata;
|
|
fn metadata(&self) -> &PluginMetadata;
|
|
|
fn hooks(&self) -> &PluginHooks;
|
|
fn hooks(&self) -> &PluginHooks;
|
|
|
fn lifecycle(&self) -> &PluginLifecycle;
|
|
fn lifecycle(&self) -> &PluginLifecycle;
|
|
|
|
|
+ fn tools(&self) -> &[PluginTool];
|
|
|
fn validate(&self) -> Result<(), PluginError>;
|
|
fn validate(&self) -> Result<(), PluginError>;
|
|
|
fn initialize(&self) -> Result<(), PluginError>;
|
|
fn initialize(&self) -> Result<(), PluginError>;
|
|
|
fn shutdown(&self) -> Result<(), PluginError>;
|
|
fn shutdown(&self) -> Result<(), PluginError>;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-#[derive(Debug, Clone, PartialEq, Eq)]
|
|
|
|
|
|
|
+#[derive(Debug, Clone, PartialEq)]
|
|
|
pub enum PluginDefinition {
|
|
pub enum PluginDefinition {
|
|
|
Builtin(BuiltinPlugin),
|
|
Builtin(BuiltinPlugin),
|
|
|
Bundled(BundledPlugin),
|
|
Bundled(BundledPlugin),
|
|
@@ -175,6 +314,10 @@ impl Plugin for BuiltinPlugin {
|
|
|
&self.lifecycle
|
|
&self.lifecycle
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ fn tools(&self) -> &[PluginTool] {
|
|
|
|
|
+ &self.tools
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
fn validate(&self) -> Result<(), PluginError> {
|
|
fn validate(&self) -> Result<(), PluginError> {
|
|
|
Ok(())
|
|
Ok(())
|
|
|
}
|
|
}
|
|
@@ -201,13 +344,23 @@ impl Plugin for BundledPlugin {
|
|
|
&self.lifecycle
|
|
&self.lifecycle
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ fn tools(&self) -> &[PluginTool] {
|
|
|
|
|
+ &self.tools
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
fn validate(&self) -> Result<(), PluginError> {
|
|
fn validate(&self) -> Result<(), PluginError> {
|
|
|
validate_hook_paths(self.metadata.root.as_deref(), &self.hooks)?;
|
|
validate_hook_paths(self.metadata.root.as_deref(), &self.hooks)?;
|
|
|
- validate_lifecycle_paths(self.metadata.root.as_deref(), &self.lifecycle)
|
|
|
|
|
|
|
+ validate_lifecycle_paths(self.metadata.root.as_deref(), &self.lifecycle)?;
|
|
|
|
|
+ validate_tool_paths(self.metadata.root.as_deref(), &self.tools)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
fn initialize(&self) -> Result<(), PluginError> {
|
|
fn initialize(&self) -> Result<(), PluginError> {
|
|
|
- run_lifecycle_commands(self.metadata(), self.lifecycle(), "init", &self.lifecycle.init)
|
|
|
|
|
|
|
+ run_lifecycle_commands(
|
|
|
|
|
+ self.metadata(),
|
|
|
|
|
+ self.lifecycle(),
|
|
|
|
|
+ "init",
|
|
|
|
|
+ &self.lifecycle.init,
|
|
|
|
|
+ )
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
fn shutdown(&self) -> Result<(), PluginError> {
|
|
fn shutdown(&self) -> Result<(), PluginError> {
|
|
@@ -233,13 +386,23 @@ impl Plugin for ExternalPlugin {
|
|
|
&self.lifecycle
|
|
&self.lifecycle
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ fn tools(&self) -> &[PluginTool] {
|
|
|
|
|
+ &self.tools
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
fn validate(&self) -> Result<(), PluginError> {
|
|
fn validate(&self) -> Result<(), PluginError> {
|
|
|
validate_hook_paths(self.metadata.root.as_deref(), &self.hooks)?;
|
|
validate_hook_paths(self.metadata.root.as_deref(), &self.hooks)?;
|
|
|
- validate_lifecycle_paths(self.metadata.root.as_deref(), &self.lifecycle)
|
|
|
|
|
|
|
+ validate_lifecycle_paths(self.metadata.root.as_deref(), &self.lifecycle)?;
|
|
|
|
|
+ validate_tool_paths(self.metadata.root.as_deref(), &self.tools)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
fn initialize(&self) -> Result<(), PluginError> {
|
|
fn initialize(&self) -> Result<(), PluginError> {
|
|
|
- run_lifecycle_commands(self.metadata(), self.lifecycle(), "init", &self.lifecycle.init)
|
|
|
|
|
|
|
+ run_lifecycle_commands(
|
|
|
|
|
+ self.metadata(),
|
|
|
|
|
+ self.lifecycle(),
|
|
|
|
|
+ "init",
|
|
|
|
|
+ &self.lifecycle.init,
|
|
|
|
|
+ )
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
fn shutdown(&self) -> Result<(), PluginError> {
|
|
fn shutdown(&self) -> Result<(), PluginError> {
|
|
@@ -277,6 +440,14 @@ impl Plugin for PluginDefinition {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ fn tools(&self) -> &[PluginTool] {
|
|
|
|
|
+ match self {
|
|
|
|
|
+ Self::Builtin(plugin) => plugin.tools(),
|
|
|
|
|
+ Self::Bundled(plugin) => plugin.tools(),
|
|
|
|
|
+ Self::External(plugin) => plugin.tools(),
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
fn validate(&self) -> Result<(), PluginError> {
|
|
fn validate(&self) -> Result<(), PluginError> {
|
|
|
match self {
|
|
match self {
|
|
|
Self::Builtin(plugin) => plugin.validate(),
|
|
Self::Builtin(plugin) => plugin.validate(),
|
|
@@ -302,7 +473,7 @@ impl Plugin for PluginDefinition {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-#[derive(Debug, Clone, PartialEq, Eq)]
|
|
|
|
|
|
|
+#[derive(Debug, Clone, PartialEq)]
|
|
|
pub struct RegisteredPlugin {
|
|
pub struct RegisteredPlugin {
|
|
|
definition: PluginDefinition,
|
|
definition: PluginDefinition,
|
|
|
enabled: bool,
|
|
enabled: bool,
|
|
@@ -327,6 +498,11 @@ impl RegisteredPlugin {
|
|
|
self.definition.hooks()
|
|
self.definition.hooks()
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ #[must_use]
|
|
|
|
|
+ pub fn tools(&self) -> &[PluginTool] {
|
|
|
|
|
+ self.definition.tools()
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
#[must_use]
|
|
#[must_use]
|
|
|
pub fn is_enabled(&self) -> bool {
|
|
pub fn is_enabled(&self) -> bool {
|
|
|
self.enabled
|
|
self.enabled
|
|
@@ -359,7 +535,7 @@ pub struct PluginSummary {
|
|
|
pub enabled: bool,
|
|
pub enabled: bool,
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
|
|
|
|
|
|
+#[derive(Debug, Clone, Default, PartialEq)]
|
|
|
pub struct PluginRegistry {
|
|
pub struct PluginRegistry {
|
|
|
plugins: Vec<RegisteredPlugin>,
|
|
plugins: Vec<RegisteredPlugin>,
|
|
|
}
|
|
}
|
|
@@ -403,6 +579,27 @@ impl PluginRegistry {
|
|
|
})
|
|
})
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ pub fn aggregated_tools(&self) -> Result<Vec<PluginTool>, PluginError> {
|
|
|
|
|
+ let mut tools = Vec::new();
|
|
|
|
|
+ let mut seen_names = BTreeMap::new();
|
|
|
|
|
+ for plugin in self.plugins.iter().filter(|plugin| plugin.is_enabled()) {
|
|
|
|
|
+ plugin.validate()?;
|
|
|
|
|
+ for tool in plugin.tools() {
|
|
|
|
|
+ if let Some(existing_plugin) =
|
|
|
|
|
+ seen_names.insert(tool.definition().name.clone(), tool.plugin_id().to_string())
|
|
|
|
|
+ {
|
|
|
|
|
+ return Err(PluginError::InvalidManifest(format!(
|
|
|
|
|
+ "plugin tool `{}` is defined by both `{existing_plugin}` and `{}`",
|
|
|
|
|
+ tool.definition().name,
|
|
|
|
|
+ tool.plugin_id()
|
|
|
|
|
+ )));
|
|
|
|
|
+ }
|
|
|
|
|
+ tools.push(tool.clone());
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ Ok(tools)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
pub fn initialize(&self) -> Result<(), PluginError> {
|
|
pub fn initialize(&self) -> Result<(), PluginError> {
|
|
|
for plugin in self.plugins.iter().filter(|plugin| plugin.is_enabled()) {
|
|
for plugin in self.plugins.iter().filter(|plugin| plugin.is_enabled()) {
|
|
|
plugin.validate()?;
|
|
plugin.validate()?;
|
|
@@ -412,7 +609,12 @@ impl PluginRegistry {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
pub fn shutdown(&self) -> Result<(), PluginError> {
|
|
pub fn shutdown(&self) -> Result<(), PluginError> {
|
|
|
- for plugin in self.plugins.iter().rev().filter(|plugin| plugin.is_enabled()) {
|
|
|
|
|
|
|
+ for plugin in self
|
|
|
|
|
+ .plugins
|
|
|
|
|
+ .iter()
|
|
|
|
|
+ .rev()
|
|
|
|
|
+ .filter(|plugin| plugin.is_enabled())
|
|
|
|
|
+ {
|
|
|
plugin.shutdown()?;
|
|
plugin.shutdown()?;
|
|
|
}
|
|
}
|
|
|
Ok(())
|
|
Ok(())
|
|
@@ -561,7 +763,7 @@ impl PluginManager {
|
|
|
|
|
|
|
|
pub fn validate_plugin_source(&self, source: &str) -> Result<PluginManifest, PluginError> {
|
|
pub fn validate_plugin_source(&self, source: &str) -> Result<PluginManifest, PluginError> {
|
|
|
let path = resolve_local_source(source)?;
|
|
let path = resolve_local_source(source)?;
|
|
|
- load_validated_manifest_from_root(&path)
|
|
|
|
|
|
|
+ load_plugin_from_directory(&path)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
pub fn install(&mut self, source: &str) -> Result<InstallOutcome, PluginError> {
|
|
pub fn install(&mut self, source: &str) -> Result<InstallOutcome, PluginError> {
|
|
@@ -569,7 +771,7 @@ impl PluginManager {
|
|
|
let temp_root = self.install_root().join(".tmp");
|
|
let temp_root = self.install_root().join(".tmp");
|
|
|
let staged_source = materialize_source(&install_source, &temp_root)?;
|
|
let staged_source = materialize_source(&install_source, &temp_root)?;
|
|
|
let cleanup_source = matches!(install_source, PluginInstallSource::GitUrl { .. });
|
|
let cleanup_source = matches!(install_source, PluginInstallSource::GitUrl { .. });
|
|
|
- let manifest = load_validated_manifest_from_root(&staged_source)?;
|
|
|
|
|
|
|
+ let manifest = load_validated_package_manifest_from_root(&staged_source)?;
|
|
|
|
|
|
|
|
let plugin_id = plugin_id(&manifest.name, EXTERNAL_MARKETPLACE);
|
|
let plugin_id = plugin_id(&manifest.name, EXTERNAL_MARKETPLACE);
|
|
|
let install_path = self.install_root().join(sanitize_plugin_id(&plugin_id));
|
|
let install_path = self.install_root().join(sanitize_plugin_id(&plugin_id));
|
|
@@ -647,7 +849,7 @@ impl PluginManager {
|
|
|
let temp_root = self.install_root().join(".tmp");
|
|
let temp_root = self.install_root().join(".tmp");
|
|
|
let staged_source = materialize_source(&record.source, &temp_root)?;
|
|
let staged_source = materialize_source(&record.source, &temp_root)?;
|
|
|
let cleanup_source = matches!(record.source, PluginInstallSource::GitUrl { .. });
|
|
let cleanup_source = matches!(record.source, PluginInstallSource::GitUrl { .. });
|
|
|
- let manifest = load_validated_manifest_from_root(&staged_source)?;
|
|
|
|
|
|
|
+ let manifest = load_validated_package_manifest_from_root(&staged_source)?;
|
|
|
|
|
|
|
|
if record.install_path.exists() {
|
|
if record.install_path.exists() {
|
|
|
fs::remove_dir_all(&record.install_path)?;
|
|
fs::remove_dir_all(&record.install_path)?;
|
|
@@ -806,6 +1008,7 @@ pub fn builtin_plugins() -> Vec<PluginDefinition> {
|
|
|
},
|
|
},
|
|
|
hooks: PluginHooks::default(),
|
|
hooks: PluginHooks::default(),
|
|
|
lifecycle: PluginLifecycle::default(),
|
|
lifecycle: PluginLifecycle::default(),
|
|
|
|
|
+ tools: Vec::new(),
|
|
|
})]
|
|
})]
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -815,7 +1018,7 @@ fn load_plugin_definition(
|
|
|
source: String,
|
|
source: String,
|
|
|
marketplace: &str,
|
|
marketplace: &str,
|
|
|
) -> Result<PluginDefinition, PluginError> {
|
|
) -> Result<PluginDefinition, PluginError> {
|
|
|
- let manifest = load_validated_manifest_from_root(root)?;
|
|
|
|
|
|
|
+ let manifest = load_validated_package_manifest_from_root(root)?;
|
|
|
let metadata = PluginMetadata {
|
|
let metadata = PluginMetadata {
|
|
|
id: plugin_id(&manifest.name, marketplace),
|
|
id: plugin_id(&manifest.name, marketplace),
|
|
|
name: manifest.name,
|
|
name: manifest.name,
|
|
@@ -828,34 +1031,46 @@ fn load_plugin_definition(
|
|
|
};
|
|
};
|
|
|
let hooks = resolve_hooks(root, &manifest.hooks);
|
|
let hooks = resolve_hooks(root, &manifest.hooks);
|
|
|
let lifecycle = resolve_lifecycle(root, &manifest.lifecycle);
|
|
let lifecycle = resolve_lifecycle(root, &manifest.lifecycle);
|
|
|
|
|
+ let tools = resolve_tools(root, &metadata.id, &metadata.name, &manifest.tools);
|
|
|
Ok(match kind {
|
|
Ok(match kind {
|
|
|
PluginKind::Builtin => PluginDefinition::Builtin(BuiltinPlugin {
|
|
PluginKind::Builtin => PluginDefinition::Builtin(BuiltinPlugin {
|
|
|
metadata,
|
|
metadata,
|
|
|
hooks,
|
|
hooks,
|
|
|
lifecycle,
|
|
lifecycle,
|
|
|
|
|
+ tools,
|
|
|
}),
|
|
}),
|
|
|
PluginKind::Bundled => PluginDefinition::Bundled(BundledPlugin {
|
|
PluginKind::Bundled => PluginDefinition::Bundled(BundledPlugin {
|
|
|
metadata,
|
|
metadata,
|
|
|
hooks,
|
|
hooks,
|
|
|
lifecycle,
|
|
lifecycle,
|
|
|
|
|
+ tools,
|
|
|
}),
|
|
}),
|
|
|
PluginKind::External => PluginDefinition::External(ExternalPlugin {
|
|
PluginKind::External => PluginDefinition::External(ExternalPlugin {
|
|
|
metadata,
|
|
metadata,
|
|
|
hooks,
|
|
hooks,
|
|
|
lifecycle,
|
|
lifecycle,
|
|
|
|
|
+ tools,
|
|
|
}),
|
|
}),
|
|
|
})
|
|
})
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-fn load_validated_manifest_from_root(root: &Path) -> Result<PluginManifest, PluginError> {
|
|
|
|
|
- let manifest = load_manifest_from_root(root)?;
|
|
|
|
|
- validate_manifest(&manifest)?;
|
|
|
|
|
|
|
+pub fn load_plugin_from_directory(root: &Path) -> Result<PluginManifest, PluginError> {
|
|
|
|
|
+ let manifest = load_manifest_from_directory(root)?;
|
|
|
|
|
+ validate_plugin_manifest(root, &manifest)?;
|
|
|
|
|
+ Ok(manifest)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+fn load_validated_package_manifest_from_root(
|
|
|
|
|
+ root: &Path,
|
|
|
|
|
+) -> Result<PluginPackageManifest, PluginError> {
|
|
|
|
|
+ let manifest = load_package_manifest_from_root(root)?;
|
|
|
|
|
+ validate_package_manifest(root, &manifest)?;
|
|
|
validate_hook_paths(Some(root), &manifest.hooks)?;
|
|
validate_hook_paths(Some(root), &manifest.hooks)?;
|
|
|
validate_lifecycle_paths(Some(root), &manifest.lifecycle)?;
|
|
validate_lifecycle_paths(Some(root), &manifest.lifecycle)?;
|
|
|
Ok(manifest)
|
|
Ok(manifest)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-fn validate_manifest(manifest: &PluginManifest) -> Result<(), PluginError> {
|
|
|
|
|
|
|
+fn validate_plugin_manifest(root: &Path, manifest: &PluginManifest) -> Result<(), PluginError> {
|
|
|
if manifest.name.trim().is_empty() {
|
|
if manifest.name.trim().is_empty() {
|
|
|
return Err(PluginError::InvalidManifest(
|
|
return Err(PluginError::InvalidManifest(
|
|
|
"plugin manifest name cannot be empty".to_string(),
|
|
"plugin manifest name cannot be empty".to_string(),
|
|
@@ -871,10 +1086,45 @@ fn validate_manifest(manifest: &PluginManifest) -> Result<(), PluginError> {
|
|
|
"plugin manifest description cannot be empty".to_string(),
|
|
"plugin manifest description cannot be empty".to_string(),
|
|
|
));
|
|
));
|
|
|
}
|
|
}
|
|
|
|
|
+ validate_named_strings(&manifest.permissions, "permission")?;
|
|
|
|
|
+ validate_hook_paths(Some(root), &manifest.hooks)?;
|
|
|
|
|
+ validate_named_commands(root, &manifest.tools, "tool")?;
|
|
|
|
|
+ validate_named_commands(root, &manifest.commands, "command")?;
|
|
|
Ok(())
|
|
Ok(())
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-fn load_manifest_from_root(root: &Path) -> Result<PluginManifest, PluginError> {
|
|
|
|
|
|
|
+fn validate_package_manifest(root: &Path, manifest: &PluginPackageManifest) -> Result<(), PluginError> {
|
|
|
|
|
+ if manifest.name.trim().is_empty() {
|
|
|
|
|
+ return Err(PluginError::InvalidManifest(
|
|
|
|
|
+ "plugin manifest name cannot be empty".to_string(),
|
|
|
|
|
+ ));
|
|
|
|
|
+ }
|
|
|
|
|
+ if manifest.version.trim().is_empty() {
|
|
|
|
|
+ return Err(PluginError::InvalidManifest(
|
|
|
|
|
+ "plugin manifest version cannot be empty".to_string(),
|
|
|
|
|
+ ));
|
|
|
|
|
+ }
|
|
|
|
|
+ if manifest.description.trim().is_empty() {
|
|
|
|
|
+ return Err(PluginError::InvalidManifest(
|
|
|
|
|
+ "plugin manifest description cannot be empty".to_string(),
|
|
|
|
|
+ ));
|
|
|
|
|
+ }
|
|
|
|
|
+ validate_named_commands(root, &manifest.tools, "tool")?;
|
|
|
|
|
+ Ok(())
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+fn load_manifest_from_directory(root: &Path) -> Result<PluginManifest, PluginError> {
|
|
|
|
|
+ let manifest_path = plugin_manifest_path(root)?;
|
|
|
|
|
+ let contents = fs::read_to_string(&manifest_path).map_err(|error| {
|
|
|
|
|
+ PluginError::NotFound(format!(
|
|
|
|
|
+ "plugin manifest not found at {}: {error}",
|
|
|
|
|
+ manifest_path.display()
|
|
|
|
|
+ ))
|
|
|
|
|
+ })?;
|
|
|
|
|
+ Ok(serde_json::from_str(&contents)?)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+fn load_package_manifest_from_root(root: &Path) -> Result<PluginPackageManifest, PluginError> {
|
|
|
let manifest_path = root.join(MANIFEST_RELATIVE_PATH);
|
|
let manifest_path = root.join(MANIFEST_RELATIVE_PATH);
|
|
|
let contents = fs::read_to_string(&manifest_path).map_err(|error| {
|
|
let contents = fs::read_to_string(&manifest_path).map_err(|error| {
|
|
|
PluginError::NotFound(format!(
|
|
PluginError::NotFound(format!(
|
|
@@ -885,6 +1135,109 @@ fn load_manifest_from_root(root: &Path) -> Result<PluginManifest, PluginError> {
|
|
|
Ok(serde_json::from_str(&contents)?)
|
|
Ok(serde_json::from_str(&contents)?)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+fn plugin_manifest_path(root: &Path) -> Result<PathBuf, PluginError> {
|
|
|
|
|
+ let direct_path = root.join(MANIFEST_FILE_NAME);
|
|
|
|
|
+ if direct_path.exists() {
|
|
|
|
|
+ return Ok(direct_path);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ let packaged_path = root.join(MANIFEST_RELATIVE_PATH);
|
|
|
|
|
+ if packaged_path.exists() {
|
|
|
|
|
+ return Ok(packaged_path);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ Err(PluginError::NotFound(format!(
|
|
|
|
|
+ "plugin manifest not found at {} or {}",
|
|
|
|
|
+ direct_path.display(),
|
|
|
|
|
+ packaged_path.display()
|
|
|
|
|
+ )))
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+fn validate_named_strings(entries: &[String], kind: &str) -> Result<(), PluginError> {
|
|
|
|
|
+ let mut seen = BTreeMap::<&str, ()>::new();
|
|
|
|
|
+ for entry in entries {
|
|
|
|
|
+ let trimmed = entry.trim();
|
|
|
|
|
+ if trimmed.is_empty() {
|
|
|
|
|
+ return Err(PluginError::InvalidManifest(format!(
|
|
|
|
|
+ "plugin manifest {kind} cannot be empty"
|
|
|
|
|
+ )));
|
|
|
|
|
+ }
|
|
|
|
|
+ if seen.insert(trimmed, ()).is_some() {
|
|
|
|
|
+ return Err(PluginError::InvalidManifest(format!(
|
|
|
|
|
+ "plugin manifest {kind} `{trimmed}` is duplicated"
|
|
|
|
|
+ )));
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ Ok(())
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+fn validate_named_commands(
|
|
|
|
|
+ root: &Path,
|
|
|
|
|
+ entries: &[impl NamedCommand],
|
|
|
|
|
+ kind: &str,
|
|
|
|
|
+) -> Result<(), PluginError> {
|
|
|
|
|
+ let mut seen = BTreeMap::<&str, ()>::new();
|
|
|
|
|
+ for entry in entries {
|
|
|
|
|
+ let name = entry.name().trim();
|
|
|
|
|
+ if name.is_empty() {
|
|
|
|
|
+ return Err(PluginError::InvalidManifest(format!(
|
|
|
|
|
+ "plugin {kind} name cannot be empty"
|
|
|
|
|
+ )));
|
|
|
|
|
+ }
|
|
|
|
|
+ if seen.insert(name, ()).is_some() {
|
|
|
|
|
+ return Err(PluginError::InvalidManifest(format!(
|
|
|
|
|
+ "plugin {kind} `{name}` is duplicated"
|
|
|
|
|
+ )));
|
|
|
|
|
+ }
|
|
|
|
|
+ if entry.description().trim().is_empty() {
|
|
|
|
|
+ return Err(PluginError::InvalidManifest(format!(
|
|
|
|
|
+ "plugin {kind} `{name}` description cannot be empty"
|
|
|
|
|
+ )));
|
|
|
|
|
+ }
|
|
|
|
|
+ if entry.command().trim().is_empty() {
|
|
|
|
|
+ return Err(PluginError::InvalidManifest(format!(
|
|
|
|
|
+ "plugin {kind} `{name}` command cannot be empty"
|
|
|
|
|
+ )));
|
|
|
|
|
+ }
|
|
|
|
|
+ validate_command_path(root, entry.command(), kind)?;
|
|
|
|
|
+ }
|
|
|
|
|
+ Ok(())
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+trait NamedCommand {
|
|
|
|
|
+ fn name(&self) -> &str;
|
|
|
|
|
+ fn description(&self) -> &str;
|
|
|
|
|
+ fn command(&self) -> &str;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+impl NamedCommand for PluginToolManifest {
|
|
|
|
|
+ fn name(&self) -> &str {
|
|
|
|
|
+ &self.name
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ fn description(&self) -> &str {
|
|
|
|
|
+ &self.description
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ fn command(&self) -> &str {
|
|
|
|
|
+ &self.command
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+impl NamedCommand for PluginCommandManifest {
|
|
|
|
|
+ fn name(&self) -> &str {
|
|
|
|
|
+ &self.name
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ fn description(&self) -> &str {
|
|
|
|
|
+ &self.description
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ fn command(&self) -> &str {
|
|
|
|
|
+ &self.command
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
fn resolve_hooks(root: &Path, hooks: &PluginHooks) -> PluginHooks {
|
|
fn resolve_hooks(root: &Path, hooks: &PluginHooks) -> PluginHooks {
|
|
|
PluginHooks {
|
|
PluginHooks {
|
|
|
pre_tool_use: hooks
|
|
pre_tool_use: hooks
|
|
@@ -915,6 +1268,32 @@ fn resolve_lifecycle(root: &Path, lifecycle: &PluginLifecycle) -> PluginLifecycl
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+fn resolve_tools(
|
|
|
|
|
+ root: &Path,
|
|
|
|
|
+ plugin_id: &str,
|
|
|
|
|
+ plugin_name: &str,
|
|
|
|
|
+ tools: &[PluginToolManifest],
|
|
|
|
|
+) -> Vec<PluginTool> {
|
|
|
|
|
+ tools
|
|
|
|
|
+ .iter()
|
|
|
|
|
+ .map(|tool| {
|
|
|
|
|
+ PluginTool::new(
|
|
|
|
|
+ plugin_id,
|
|
|
|
|
+ plugin_name,
|
|
|
|
|
+ PluginToolDefinition {
|
|
|
|
|
+ name: tool.name.clone(),
|
|
|
|
|
+ description: Some(tool.description.clone()),
|
|
|
|
|
+ input_schema: tool.input_schema.clone(),
|
|
|
|
|
+ },
|
|
|
|
|
+ resolve_hook_entry(root, &tool.command),
|
|
|
|
|
+ tool.args.clone(),
|
|
|
|
|
+ tool.required_permission.clone(),
|
|
|
|
|
+ Some(root.to_path_buf()),
|
|
|
|
|
+ )
|
|
|
|
|
+ })
|
|
|
|
|
+ .collect()
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
fn validate_hook_paths(root: Option<&Path>, hooks: &PluginHooks) -> Result<(), PluginError> {
|
|
fn validate_hook_paths(root: Option<&Path>, hooks: &PluginHooks) -> Result<(), PluginError> {
|
|
|
let Some(root) = root else {
|
|
let Some(root) = root else {
|
|
|
return Ok(());
|
|
return Ok(());
|
|
@@ -938,6 +1317,16 @@ fn validate_lifecycle_paths(
|
|
|
Ok(())
|
|
Ok(())
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+fn validate_tool_paths(root: Option<&Path>, tools: &[PluginTool]) -> Result<(), PluginError> {
|
|
|
|
|
+ let Some(root) = root else {
|
|
|
|
|
+ return Ok(());
|
|
|
|
|
+ };
|
|
|
|
|
+ for tool in tools {
|
|
|
|
|
+ validate_command_path(root, &tool.command, "tool")?;
|
|
|
|
|
+ }
|
|
|
|
|
+ Ok(())
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
fn validate_command_path(root: &Path, entry: &str, kind: &str) -> Result<(), PluginError> {
|
|
fn validate_command_path(root: &Path, entry: &str, kind: &str) -> Result<(), PluginError> {
|
|
|
if is_literal_command(entry) {
|
|
if is_literal_command(entry) {
|
|
|
return Ok(());
|
|
return Ok(());
|
|
@@ -965,7 +1354,7 @@ fn resolve_hook_entry(root: &Path, entry: &str) -> String {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
fn is_literal_command(entry: &str) -> bool {
|
|
fn is_literal_command(entry: &str) -> bool {
|
|
|
- !entry.starts_with("./") && !entry.starts_with("../")
|
|
|
|
|
|
|
+ !entry.starts_with("./") && !entry.starts_with("../") && !Path::new(entry).is_absolute()
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
fn run_lifecycle_commands(
|
|
fn run_lifecycle_commands(
|
|
@@ -979,17 +1368,29 @@ fn run_lifecycle_commands(
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
for command in commands {
|
|
for command in commands {
|
|
|
- let output = if Path::new(command).exists() {
|
|
|
|
|
|
|
+ let mut process = if Path::new(command).exists() {
|
|
|
if cfg!(windows) {
|
|
if cfg!(windows) {
|
|
|
- Command::new("cmd").arg("/C").arg(command).output()?
|
|
|
|
|
|
|
+ let mut process = Command::new("cmd");
|
|
|
|
|
+ process.arg("/C").arg(command);
|
|
|
|
|
+ process
|
|
|
} else {
|
|
} else {
|
|
|
- Command::new("sh").arg(command).output()?
|
|
|
|
|
|
|
+ let mut process = Command::new("sh");
|
|
|
|
|
+ process.arg(command);
|
|
|
|
|
+ process
|
|
|
}
|
|
}
|
|
|
} else if cfg!(windows) {
|
|
} else if cfg!(windows) {
|
|
|
- Command::new("cmd").arg("/C").arg(command).output()?
|
|
|
|
|
|
|
+ let mut process = Command::new("cmd");
|
|
|
|
|
+ process.arg("/C").arg(command);
|
|
|
|
|
+ process
|
|
|
} else {
|
|
} else {
|
|
|
- Command::new("sh").arg("-lc").arg(command).output()?
|
|
|
|
|
|
|
+ let mut process = Command::new("sh");
|
|
|
|
|
+ process.arg("-lc").arg(command);
|
|
|
|
|
+ process
|
|
|
};
|
|
};
|
|
|
|
|
+ if let Some(root) = &metadata.root {
|
|
|
|
|
+ process.current_dir(root);
|
|
|
|
|
+ }
|
|
|
|
|
+ let output = process.output()?;
|
|
|
|
|
|
|
|
if !output.status.success() {
|
|
if !output.status.success() {
|
|
|
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
|
|
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
|
|
@@ -1206,12 +1607,12 @@ mod tests {
|
|
|
let log_path = root.join("lifecycle.log");
|
|
let log_path = root.join("lifecycle.log");
|
|
|
fs::write(
|
|
fs::write(
|
|
|
root.join("lifecycle").join("init.sh"),
|
|
root.join("lifecycle").join("init.sh"),
|
|
|
- "#!/bin/sh\nprintf 'init\\n' >> \"$(dirname \"$0\")/../lifecycle.log\"\n",
|
|
|
|
|
|
|
+ "#!/bin/sh\nprintf 'init\\n' >> lifecycle.log\n",
|
|
|
)
|
|
)
|
|
|
.expect("write init hook");
|
|
.expect("write init hook");
|
|
|
fs::write(
|
|
fs::write(
|
|
|
root.join("lifecycle").join("shutdown.sh"),
|
|
root.join("lifecycle").join("shutdown.sh"),
|
|
|
- "#!/bin/sh\nprintf 'shutdown\\n' >> \"$(dirname \"$0\")/../lifecycle.log\"\n",
|
|
|
|
|
|
|
+ "#!/bin/sh\nprintf 'shutdown\\n' >> lifecycle.log\n",
|
|
|
)
|
|
)
|
|
|
.expect("write shutdown hook");
|
|
.expect("write shutdown hook");
|
|
|
fs::write(
|
|
fs::write(
|
|
@@ -1232,6 +1633,7 @@ mod tests {
|
|
|
description: "desc".to_string(),
|
|
description: "desc".to_string(),
|
|
|
default_enabled: false,
|
|
default_enabled: false,
|
|
|
hooks: PluginHooks::default(),
|
|
hooks: PluginHooks::default(),
|
|
|
|
|
+ lifecycle: PluginLifecycle::default(),
|
|
|
})
|
|
})
|
|
|
.expect_err("empty name should fail");
|
|
.expect_err("empty name should fail");
|
|
|
assert!(error.to_string().contains("name cannot be empty"));
|
|
assert!(error.to_string().contains("name cannot be empty"));
|
|
@@ -1364,12 +1766,13 @@ mod tests {
|
|
|
fn plugin_registry_runs_initialize_and_shutdown_for_enabled_plugins() {
|
|
fn plugin_registry_runs_initialize_and_shutdown_for_enabled_plugins() {
|
|
|
let config_home = temp_dir("lifecycle-home");
|
|
let config_home = temp_dir("lifecycle-home");
|
|
|
let source_root = temp_dir("lifecycle-source");
|
|
let source_root = temp_dir("lifecycle-source");
|
|
|
- let log_path = write_lifecycle_plugin(&source_root, "lifecycle-demo", "1.0.0");
|
|
|
|
|
|
|
+ let _ = write_lifecycle_plugin(&source_root, "lifecycle-demo", "1.0.0");
|
|
|
|
|
|
|
|
let mut manager = PluginManager::new(PluginManagerConfig::new(&config_home));
|
|
let mut manager = PluginManager::new(PluginManagerConfig::new(&config_home));
|
|
|
- manager
|
|
|
|
|
|
|
+ let install = manager
|
|
|
.install(source_root.to_str().expect("utf8 path"))
|
|
.install(source_root.to_str().expect("utf8 path"))
|
|
|
.expect("install should succeed");
|
|
.expect("install should succeed");
|
|
|
|
|
+ let log_path = install.install_path.join("lifecycle.log");
|
|
|
|
|
|
|
|
let registry = manager.plugin_registry().expect("registry should build");
|
|
let registry = manager.plugin_registry().expect("registry should build");
|
|
|
registry.initialize().expect("init should succeed");
|
|
registry.initialize().expect("init should succeed");
|