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    pub runtime: RuntimeConfig,
8    pub input: Option<IoConfig>,
9    pub result: Option<IoConfig>,
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    /// Which commands this plugin intercepts (e.g., ["git"])
22    pub commands: Vec<String>,
23    /// Optional: limit to specific subcommands
24    pub subcommands: Option<Vec<String>>,
25}
26
27#[derive(Debug, Deserialize)]
28pub struct RuntimeConfig {
29    #[serde(rename = "type")]
30    pub runtime_type: RuntimeType,
31    /// Entrypoint relative to plugin dir
32    pub entry: String,
33    /// Custom command template for type=custom
34    pub command: Option<String>,
35    /// Required system binaries
36    pub requires: Option<RequiresConfig>,
37}
38
39#[derive(Debug, Deserialize, Clone, Copy, PartialEq, Eq)]
40#[serde(rename_all = "lowercase")]
41pub enum RuntimeType {
42    Wasm,
43    Binary,
44    Shell,
45    Python,
46    Node,
47    Deno,
48    Ruby,
49    Lua,
50    Custom,
51}
52
53#[derive(Debug, Deserialize)]
54pub struct RequiresConfig {
55    pub bins: Option<Vec<String>>,
56    pub optional_bins: Option<Vec<String>>,
57}
58
59#[derive(Debug, Deserialize)]
60pub struct IoConfig {
61    pub format: Option<String>,
62}
63
64#[derive(Debug, Deserialize)]
65pub struct HooksConfig {
66    pub on_install: Option<String>,
67    pub on_update: Option<String>,
68    pub on_remove: Option<String>,
69}
70
71#[derive(Debug, Deserialize)]
72pub struct PipelineConfig {
73    pub pre: Option<Vec<String>>,
74    pub post: Option<Vec<String>>,
75}
76
77impl PluginManifest {
78    pub fn parse(content: &str) -> anyhow::Result<Self> {
79        Ok(toml::from_str(content)?)
80    }
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86
87    #[test]
88    fn parse_minimal_manifest() {
89        let toml = r#"
90[plugin]
91name = "git-compact"
92commands = ["git"]
93
94[runtime]
95type = "node"
96entry = "filter.js"
97"#;
98        let manifest = PluginManifest::parse(toml).unwrap();
99        assert_eq!(manifest.plugin.name, "git-compact");
100        assert_eq!(manifest.plugin.commands, vec!["git"]);
101        assert_eq!(manifest.runtime.runtime_type, RuntimeType::Node);
102        assert_eq!(manifest.runtime.entry, "filter.js");
103    }
104
105    #[test]
106    fn parse_full_manifest() {
107        let toml = r#"
108[plugin]
109name = "git-compact"
110version = "1.2.0"
111description = "Compact git output for LLM contexts"
112author = "zdk"
113category = "git"
114commands = ["git"]
115subcommands = ["status", "diff", "log", "show"]
116
117[runtime]
118type = "wasm"
119entry = "filter.wasm"
120
121[runtime.requires]
122bins = ["git"]
123optional_bins = ["delta"]
124
125[hooks]
126on_install = "cargo build --release"
127
128[pipeline]
129pre = ["strip-ansi"]
130post = ["truncate"]
131"#;
132        let manifest = PluginManifest::parse(toml).unwrap();
133        assert_eq!(manifest.plugin.name, "git-compact");
134        assert_eq!(manifest.runtime.runtime_type, RuntimeType::Wasm);
135        assert!(manifest.hooks.is_some());
136        assert!(manifest.pipeline.is_some());
137    }
138}