lowfat_plugin/
manifest.rs1use serde::Deserialize;
2use std::path::Path;
3
4#[derive(Debug, Deserialize)]
6pub struct PluginManifest {
7 pub plugin: PluginMeta,
8 #[serde(default)]
9 pub runtime: RuntimeConfig,
10 pub hooks: Option<HooksConfig>,
11 pub pipeline: Option<PipelineConfig>,
12}
13
14#[derive(Debug, Deserialize)]
15pub struct PluginMeta {
16 pub name: String,
17 pub version: Option<String>,
18 pub description: Option<String>,
19 pub author: Option<String>,
20 pub category: Option<String>,
21 pub commands: Vec<String>,
23 pub subcommands: Option<Vec<String>>,
25}
26
27#[derive(Debug, Default, Deserialize)]
28pub struct RuntimeConfig {
29 #[serde(default)]
33 pub entry: Option<String>,
34 #[serde(default)]
37 pub requires: std::collections::BTreeMap<String, String>,
38}
39
40impl RuntimeConfig {
41 pub fn resolve_entry(&self, base_dir: &Path) -> String {
47 if let Some(entry) = &self.entry {
48 return entry.clone();
49 }
50 if base_dir.join("filter.lf").is_file() {
51 "filter.lf".to_string()
52 } else {
53 "filter.sh".to_string()
54 }
55 }
56}
57
58#[derive(Debug, Deserialize)]
59pub struct HooksConfig {
60 pub on_install: Option<String>,
61 pub on_update: Option<String>,
62 pub on_remove: Option<String>,
63}
64
65#[derive(Debug, Deserialize)]
66pub struct PipelineConfig {
67 pub pre: Option<Vec<String>>,
68 pub post: Option<Vec<String>>,
69}
70
71impl PluginManifest {
72 pub fn parse(content: &str) -> anyhow::Result<Self> {
73 Ok(toml::from_str(content)?)
74 }
75}
76
77#[cfg(test)]
78mod tests {
79 use super::*;
80
81 #[test]
82 fn parse_minimal_manifest() {
83 let toml = r#"
84[plugin]
85name = "git-compact"
86commands = ["git"]
87
88[runtime]
89entry = "filter.sh"
90"#;
91 let manifest = PluginManifest::parse(toml).unwrap();
92 assert_eq!(manifest.plugin.name, "git-compact");
93 assert_eq!(manifest.plugin.commands, vec!["git"]);
94 assert_eq!(manifest.runtime.entry.as_deref(), Some("filter.sh"));
95 }
96
97 #[test]
98 fn parse_minimal_manifest_no_runtime() {
99 let toml = r#"
100[plugin]
101name = "git-compact"
102commands = ["git"]
103"#;
104 let manifest = PluginManifest::parse(toml).unwrap();
105 assert_eq!(manifest.plugin.name, "git-compact");
106 assert!(manifest.runtime.entry.is_none());
108 }
109
110 #[test]
111 fn resolve_entry_auto_detects() {
112 let dir = std::env::temp_dir()
113 .join(format!("lowfat-resolve-{}", std::process::id()));
114 let _ = std::fs::remove_dir_all(&dir);
115 std::fs::create_dir_all(&dir).unwrap();
116
117 let rt = RuntimeConfig::default();
118 assert_eq!(rt.resolve_entry(&dir), "filter.sh");
120
121 std::fs::write(dir.join("filter.lf"), "*:\n head 30\n").unwrap();
123 assert_eq!(rt.resolve_entry(&dir), "filter.lf");
124
125 let rt_explicit = RuntimeConfig {
127 entry: Some("custom.sh".to_string()),
128 ..Default::default()
129 };
130 assert_eq!(rt_explicit.resolve_entry(&dir), "custom.sh");
131
132 let _ = std::fs::remove_dir_all(&dir);
133 }
134
135 #[test]
136 fn parse_full_manifest() {
137 let toml = r#"
138[plugin]
139name = "git-compact"
140version = "1.2.0"
141description = "Compact git output for LLM contexts"
142author = "zdk"
143category = "git"
144commands = ["git"]
145subcommands = ["status", "diff", "log", "show"]
146
147[runtime]
148entry = "filter.sh"
149
150[hooks]
151on_install = "chmod +x filter.sh"
152
153[pipeline]
154pre = ["strip-ansi"]
155post = ["truncate"]
156"#;
157 let manifest = PluginManifest::parse(toml).unwrap();
158 assert_eq!(manifest.plugin.name, "git-compact");
159 assert!(manifest.hooks.is_some());
160 assert!(manifest.pipeline.is_some());
161 }
162}