lowfat-plugin 0.1.0

Plugin manifest, discovery, registry for lowfat
Documentation
use serde::Deserialize;

/// Parsed `lowfat.toml` (or `init.toml`) plugin manifest.
#[derive(Debug, Deserialize)]
pub struct PluginManifest {
    pub plugin: PluginMeta,
    pub runtime: RuntimeConfig,
    pub input: Option<IoConfig>,
    pub result: Option<IoConfig>,
    pub hooks: Option<HooksConfig>,
    pub pipeline: Option<PipelineConfig>,
}

#[derive(Debug, Deserialize)]
pub struct PluginMeta {
    pub name: String,
    pub version: Option<String>,
    pub description: Option<String>,
    pub author: Option<String>,
    pub category: Option<String>,
    /// Which commands this plugin intercepts (e.g., ["git"])
    pub commands: Vec<String>,
    /// Optional: limit to specific subcommands
    pub subcommands: Option<Vec<String>>,
}

#[derive(Debug, Deserialize)]
pub struct RuntimeConfig {
    #[serde(rename = "type")]
    pub runtime_type: RuntimeType,
    /// Entrypoint relative to plugin dir
    pub entry: String,
    /// Custom command template for type=custom
    pub command: Option<String>,
    /// Required system binaries
    pub requires: Option<RequiresConfig>,
}

#[derive(Debug, Deserialize, Clone, Copy, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum RuntimeType {
    Wasm,
    Binary,
    Shell,
    Python,
    Node,
    Deno,
    Ruby,
    Lua,
    Custom,
}

#[derive(Debug, Deserialize)]
pub struct RequiresConfig {
    pub bins: Option<Vec<String>>,
    pub optional_bins: Option<Vec<String>>,
}

#[derive(Debug, Deserialize)]
pub struct IoConfig {
    pub format: Option<String>,
}

#[derive(Debug, Deserialize)]
pub struct HooksConfig {
    pub on_install: Option<String>,
    pub on_update: Option<String>,
    pub on_remove: Option<String>,
}

#[derive(Debug, Deserialize)]
pub struct PipelineConfig {
    pub pre: Option<Vec<String>>,
    pub post: Option<Vec<String>>,
}

impl PluginManifest {
    pub fn parse(content: &str) -> anyhow::Result<Self> {
        Ok(toml::from_str(content)?)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn parse_minimal_manifest() {
        let toml = r#"
[plugin]
name = "git-compact"
commands = ["git"]

[runtime]
type = "node"
entry = "filter.js"
"#;
        let manifest = PluginManifest::parse(toml).unwrap();
        assert_eq!(manifest.plugin.name, "git-compact");
        assert_eq!(manifest.plugin.commands, vec!["git"]);
        assert_eq!(manifest.runtime.runtime_type, RuntimeType::Node);
        assert_eq!(manifest.runtime.entry, "filter.js");
    }

    #[test]
    fn parse_full_manifest() {
        let toml = r#"
[plugin]
name = "git-compact"
version = "1.2.0"
description = "Compact git output for LLM contexts"
author = "zdk"
category = "git"
commands = ["git"]
subcommands = ["status", "diff", "log", "show"]

[runtime]
type = "wasm"
entry = "filter.wasm"

[runtime.requires]
bins = ["git"]
optional_bins = ["delta"]

[hooks]
on_install = "cargo build --release"

[pipeline]
pre = ["strip-ansi"]
post = ["truncate"]
"#;
        let manifest = PluginManifest::parse(toml).unwrap();
        assert_eq!(manifest.plugin.name, "git-compact");
        assert_eq!(manifest.runtime.runtime_type, RuntimeType::Wasm);
        assert!(manifest.hooks.is_some());
        assert!(manifest.pipeline.is_some());
    }
}