Skip to content

Latest commit

 

History

History
540 lines (427 loc) · 11.4 KB

File metadata and controls

540 lines (427 loc) · 11.4 KB

Plugin Development Guide

This guide provides a comprehensive overview of developing plugins for the meta CLI platform, including the plugin protocol, best practices, and examples.

Table of Contents


Introduction

Plugins extend the functionality of meta by adding new commands. They are discovered automatically and communicate with meta via a JSON protocol over stdin/stdout.

Important: Only plugin commands and meta exec are supported. Bare commands like meta npm install do not work—there is no automatic fallback to loop. Users must either use a plugin command (meta git status) or explicitly use meta exec -- npm install.

Key benefits:

  • Language agnostic - Write in any language that can read/write JSON
  • Isolated - Plugins run as subprocesses
  • Discoverable - Auto-found from standard locations
  • Command ownership - Plugins fully own their command namespace

Plugin Types

Rust Binary Plugins

Build plugins as Rust crates for performance and native integration:

// Cargo.toml
[package]
name = "meta-docker"
version = "0.1.0"

[[bin]]
name = "meta-docker"

External Executables

Any executable in your PATH or .meta-plugins directory:

#!/bin/bash
# meta-hello plugin

Script Plugins

Shell scripts, Python, Node.js, etc.:

#!/usr/bin/env python3
# meta-python plugin
import json
import sys

Plugin Discovery

Meta discovers plugins from these locations (in order):

  1. .meta-plugins/ in current directory
  2. .meta-plugins/ in parent directories (up to root)
  3. ~/.meta-plugins/ in home directory
  4. System PATH (executables named meta-*)

Naming Convention

Plugins must follow the naming pattern:

  • meta-<name> (e.g., meta-docker, meta-npm)
  • Or meta_<name>_cli for Rust crates (e.g., meta_git_cli)

Meta strips the prefix to determine the command. meta-docker handles meta docker <subcommand>.

Plugin Protocol

Plugins communicate with meta via JSON over stdin/stdout.

Info Request

Meta queries plugin capabilities:

meta-docker --meta-plugin-info

Response:

{
  "name": "docker",
  "version": "0.1.0",
  "description": "Docker operations for meta repositories",
  "commands": ["build", "push", "compose"],
  "help": {
    "build": "Build Docker images across repos",
    "push": "Push images to registry",
    "compose": "Run docker-compose operations"
  }
}

Execution Request

Meta invokes plugin execution:

echo '<request>' | meta-docker --meta-plugin-exec

Request Format:

{
  "command": "build",
  "args": ["--tag", "latest"],
  "projects": [
    {
      "name": "api",
      "path": "./api",
      "tags": ["backend"],
      "repo": "git@github.com:org/api.git"
    }
  ],
  "filters": {
    "tags": ["backend"],
    "include": [],
    "exclude": []
  },
  "options": {
    "parallel": false,
    "dry_run": false,
    "json_output": false
  }
}

Response Format:

{
  "success": true,
  "results": [
    {
      "project": "api",
      "success": true,
      "output": "Successfully built image api:latest",
      "exit_code": 0
    },
    {
      "project": "web",
      "success": false,
      "output": "",
      "error": "Dockerfile not found",
      "exit_code": 1
    }
  ],
  "summary": {
    "total": 2,
    "succeeded": 1,
    "failed": 1
  }
}

Error Response

{
  "success": false,
  "error": "Unknown command: foo",
  "results": []
}

Creating a Plugin

Rust Plugin Example

Cargo.toml:

[package]
name = "meta-docker"
version = "0.1.0"
edition = "2021"

[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
clap = { version = "4.0", features = ["derive"] }

src/main.rs:

use serde::{Deserialize, Serialize};
use std::io::{self, Read};

#[derive(Deserialize)]
struct PluginRequest {
    command: String,
    args: Vec<String>,
    projects: Vec<Project>,
    #[serde(default)]
    options: Options,
}

#[derive(Deserialize)]
struct Project {
    name: String,
    path: String,
    tags: Option<Vec<String>>,
}

#[derive(Deserialize, Default)]
struct Options {
    parallel: bool,
    dry_run: bool,
}

#[derive(Serialize)]
struct PluginInfo {
    name: &'static str,
    version: &'static str,
    description: &'static str,
    commands: Vec<&'static str>,
}

#[derive(Serialize)]
struct PluginResponse {
    success: bool,
    results: Vec<ProjectResult>,
    #[serde(skip_serializing_if = "Option::is_none")]
    error: Option<String>,
}

#[derive(Serialize)]
struct ProjectResult {
    project: String,
    success: bool,
    output: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    error: Option<String>,
}

fn main() {
    let args: Vec<String> = std::env::args().collect();

    if args.contains(&"--meta-plugin-info".to_string()) {
        let info = PluginInfo {
            name: "docker",
            version: "0.1.0",
            description: "Docker operations for meta repos",
            commands: vec!["build", "push"],
        };
        println!("{}", serde_json::to_string(&info).unwrap());
        return;
    }

    if args.contains(&"--meta-plugin-exec".to_string()) {
        let mut input = String::new();
        io::stdin().read_to_string(&mut input).unwrap();

        let request: PluginRequest = serde_json::from_str(&input).unwrap();
        let response = execute(request);

        println!("{}", serde_json::to_string(&response).unwrap());
        return;
    }

    // Fallback: show help
    eprintln!("meta-docker: Docker plugin for meta");
    eprintln!("Commands: build, push");
}

fn execute(request: PluginRequest) -> PluginResponse {
    let results: Vec<ProjectResult> = request.projects.iter().map(|p| {
        // Your plugin logic here
        ProjectResult {
            project: p.name.clone(),
            success: true,
            output: format!("Processed {}", p.name),
            error: None,
        }
    }).collect();

    let success = results.iter().all(|r| r.success);

    PluginResponse {
        success,
        results,
        error: None,
    }
}

Shell Script Example

meta-hello:

#!/bin/bash

if [[ "$1" == "--meta-plugin-info" ]]; then
    cat <<EOF
{
  "name": "hello",
  "version": "0.1.0",
  "description": "A friendly greeting plugin",
  "commands": ["world", "there"]
}
EOF
    exit 0
fi

if [[ "$1" == "--meta-plugin-exec" ]]; then
    # Read request from stdin
    REQUEST=$(cat)
    COMMAND=$(echo "$REQUEST" | jq -r '.command')

    # Process and respond
    cat <<EOF
{
  "success": true,
  "results": [
    {"project": ".", "success": true, "output": "Hello, $COMMAND!"}
  ]
}
EOF
    exit 0
fi

echo "meta-hello: Greeting plugin"
echo "Commands: world, there"

Python Example

meta-py:

#!/usr/bin/env python3
import json
import sys

def get_info():
    return {
        "name": "py",
        "version": "0.1.0",
        "description": "Python operations plugin",
        "commands": ["lint", "format"]
    }

def execute(request):
    results = []
    for project in request.get("projects", []):
        results.append({
            "project": project["name"],
            "success": True,
            "output": f"Processed {project['name']}"
        })
    return {
        "success": all(r["success"] for r in results),
        "results": results
    }

if __name__ == "__main__":
    if "--meta-plugin-info" in sys.argv:
        print(json.dumps(get_info()))
    elif "--meta-plugin-exec" in sys.argv:
        request = json.loads(sys.stdin.read())
        response = execute(request)
        print(json.dumps(response))
    else:
        print("meta-py: Python plugin for meta")

Plugin Help System

Plugins can provide structured help via the --meta-plugin-info response:

{
  "name": "docker",
  "version": "0.1.0",
  "description": "Docker operations for meta repositories",
  "commands": ["build", "push", "compose"],
  "help": {
    "build": "Build Docker images\n\nUsage: meta docker build [OPTIONS]\n\nOptions:\n  --tag TAG  Image tag",
    "push": "Push images to registry",
    "compose": "Run docker-compose commands"
  }
}

When users run meta docker --help, meta displays this information.

Best Practices

1. Follow the Protocol

Always implement both --meta-plugin-info and --meta-plugin-exec flags.

2. Handle Errors Gracefully

PluginResponse {
    success: false,
    results: vec![],
    error: Some("Failed to connect to Docker daemon".to_string()),
}

3. Respect Filters

Use the provided filters to only operate on requested projects:

let filtered_projects: Vec<_> = request.projects
    .iter()
    .filter(|p| {
        if let Some(tags) = &p.tags {
            request.filters.tags.iter().any(|t| tags.contains(t))
        } else {
            request.filters.tags.is_empty()
        }
    })
    .collect();

4. Support Dry Run

When options.dry_run is true, show what would happen without executing:

if request.options.dry_run {
    return ProjectResult {
        project: p.name.clone(),
        success: true,
        output: format!("[DRY RUN] Would build {}", p.name),
        error: None,
    };
}

5. Provide JSON Output

When options.json_output is true, ensure structured output.

6. Cross-Platform Compatibility

Avoid OS-specific code when possible. Use standard paths and commands.

Testing Plugins

Manual Testing

# Test info response
./meta-docker --meta-plugin-info | jq .

# Test execution
echo '{"command":"build","args":[],"projects":[{"name":"test","path":"./test"}],"options":{}}' \
  | ./meta-docker --meta-plugin-exec | jq .

Integration Testing

# Place plugin in discovery path
cp meta-docker ~/.meta-plugins/

# Test via meta
meta docker build --dry-run

Automated Tests

#[test]
fn test_plugin_info() {
    let output = Command::new("./target/debug/meta-docker")
        .arg("--meta-plugin-info")
        .output()
        .unwrap();

    let info: PluginInfo = serde_json::from_slice(&output.stdout).unwrap();
    assert_eq!(info.name, "docker");
}

Publishing Plugins

Via Registry

# Search for plugins
meta plugin search docker

# Install from registry
meta plugin install meta-docker

Manual Distribution

  1. Build for target platforms
  2. Distribute binaries or packages
  3. Users place in ~/.meta-plugins/ or PATH

Troubleshooting

Plugin Not Detected

  • Ensure executable permissions: chmod +x meta-plugin
  • Check naming: must start with meta-
  • Verify location: .meta-plugins/, ~/.meta-plugins/, or PATH
  • Use META_DEBUG=1 for debug output

Protocol Errors

  • Validate JSON format with jq
  • Check for proper stdout/stderr separation
  • Ensure newline after JSON output

Permission Issues

  • Check file permissions
  • Verify PATH includes plugin directory

See Also