Skip to main content

lowfat_plugin/
manifest.rs

1use serde::Deserialize;
2
3/// Parsed `lowfat.toml` (or `init.toml`) plugin manifest.
4#[derive(Debug, Deserialize)]
5pub struct PluginManifest {
6    pub plugin: PluginMeta,
7    #[serde(default)]
8    pub runtime: RuntimeConfig,
9    pub hooks: Option<HooksConfig>,
10    pub pipeline: Option<PipelineConfig>,
11}
12
13#[derive(Debug, Deserialize)]
14pub struct PluginMeta {
15    pub name: String,
16    pub version: Option<String>,
17    pub description: Option<String>,
18    pub author: Option<String>,
19    pub category: Option<String>,
20    /// Which commands this plugin intercepts (e.g., ["git"])
21    pub commands: Vec<String>,
22    /// Optional: limit to specific subcommands
23    pub subcommands: Option<Vec<String>>,
24}
25
26#[derive(Debug, Deserialize)]
27pub struct RuntimeConfig {
28    /// Entrypoint relative to plugin dir (default: "filter.sh")
29    #[serde(default = "default_entry")]
30    pub entry: String,
31}
32
33fn default_entry() -> String {
34    "filter.sh".to_string()
35}
36
37impl Default for RuntimeConfig {
38    fn default() -> Self {
39        Self { entry: default_entry() }
40    }
41}
42
43#[derive(Debug, Deserialize)]
44pub struct HooksConfig {
45    pub on_install: Option<String>,
46    pub on_update: Option<String>,
47    pub on_remove: Option<String>,
48}
49
50#[derive(Debug, Deserialize)]
51pub struct PipelineConfig {
52    pub pre: Option<Vec<String>>,
53    pub post: Option<Vec<String>>,
54}
55
56impl PluginManifest {
57    pub fn parse(content: &str) -> anyhow::Result<Self> {
58        Ok(toml::from_str(content)?)
59    }
60}
61
62#[cfg(test)]
63mod tests {
64    use super::*;
65
66    #[test]
67    fn parse_minimal_manifest() {
68        let toml = r#"
69[plugin]
70name = "git-compact"
71commands = ["git"]
72
73[runtime]
74entry = "filter.sh"
75"#;
76        let manifest = PluginManifest::parse(toml).unwrap();
77        assert_eq!(manifest.plugin.name, "git-compact");
78        assert_eq!(manifest.plugin.commands, vec!["git"]);
79        assert_eq!(manifest.runtime.entry, "filter.sh");
80    }
81
82    #[test]
83    fn parse_minimal_manifest_no_runtime() {
84        let toml = r#"
85[plugin]
86name = "git-compact"
87commands = ["git"]
88"#;
89        let manifest = PluginManifest::parse(toml).unwrap();
90        assert_eq!(manifest.plugin.name, "git-compact");
91        assert_eq!(manifest.runtime.entry, "filter.sh");
92    }
93
94    #[test]
95    fn parse_full_manifest() {
96        let toml = r#"
97[plugin]
98name = "git-compact"
99version = "1.2.0"
100description = "Compact git output for LLM contexts"
101author = "zdk"
102category = "git"
103commands = ["git"]
104subcommands = ["status", "diff", "log", "show"]
105
106[runtime]
107entry = "filter.sh"
108
109[hooks]
110on_install = "chmod +x filter.sh"
111
112[pipeline]
113pre = ["strip-ansi"]
114post = ["truncate"]
115"#;
116        let manifest = PluginManifest::parse(toml).unwrap();
117        assert_eq!(manifest.plugin.name, "git-compact");
118        assert!(manifest.hooks.is_some());
119        assert!(manifest.pipeline.is_some());
120    }
121}