1use enact_config::enact_home;
10use serde::{Deserialize, Serialize};
11use std::path::{Path, PathBuf};
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct PluginManifest {
16 pub name: String,
17 #[serde(default = "default_version")]
18 pub version: String,
19 #[serde(default)]
20 pub author: Option<String>,
21 #[serde(default)]
22 pub description: Option<String>,
23}
24
25fn default_version() -> String {
26 "0.1.0".to_string()
27}
28
29#[derive(Debug, Clone)]
31pub struct Plugin {
32 pub manifest: PluginManifest,
33 pub root: PathBuf,
34}
35
36impl Plugin {
37 pub fn command_name(&self, base: &str) -> String {
39 format!("{}:{}", self.manifest.name, base)
40 }
41
42 pub fn skill_name(&self, base: &str) -> String {
44 format!("{}:{}", self.manifest.name, base)
45 }
46
47 pub fn commands_dir(&self) -> PathBuf {
49 self.root.join("commands")
50 }
51
52 pub fn skills_dir(&self) -> PathBuf {
54 self.root.join("skills")
55 }
56
57 pub fn hooks_dir(&self) -> PathBuf {
59 self.root.join("hooks")
60 }
61}
62
63pub fn load_plugins(project_dir: Option<&Path>) -> Vec<Plugin> {
66 let home = enact_home();
67 let mut plugins = load_plugins_from_dir(&home.join("plugins"));
68 if let Some(proj) = project_dir {
69 let project_plugins = load_plugins_from_dir(&proj.join(".enact").join("plugins"));
70 for p in project_plugins {
71 if !plugins.iter().any(|e| e.manifest.name == p.manifest.name) {
72 plugins.push(p);
73 }
74 }
75 }
76 plugins
77}
78
79fn load_plugins_from_dir(dir: &Path) -> Vec<Plugin> {
80 let mut out = Vec::new();
81 if !dir.is_dir() {
82 return out;
83 }
84 let entries = match std::fs::read_dir(dir) {
85 Ok(e) => e,
86 Err(_) => return out,
87 };
88 for entry in entries.flatten() {
89 let path = entry.path();
90 if path.is_dir() {
91 let manifest_path = path.join(".enact-plugin").join("plugin.json");
92 if manifest_path.exists() {
93 if let Ok(manifest) = load_manifest(&manifest_path) {
94 out.push(Plugin {
95 manifest,
96 root: path,
97 });
98 }
99 }
100 }
101 }
102 out
103}
104
105fn load_manifest(path: &Path) -> anyhow::Result<PluginManifest> {
106 let content = std::fs::read_to_string(path)?;
107 let manifest: PluginManifest = serde_json::from_str(&content)?;
108 Ok(manifest)
109}
110
111#[cfg(test)]
112mod tests {
113 use super::*;
114
115 #[test]
116 fn plugin_command_name() {
117 let p = Plugin {
118 manifest: PluginManifest {
119 name: "my-plugin".to_string(),
120 version: "1.0".to_string(),
121 author: None,
122 description: None,
123 },
124 root: PathBuf::from("/tmp/my-plugin"),
125 };
126 assert_eq!(p.command_name("hello"), "my-plugin:hello");
127 }
128}