This guide provides a comprehensive overview of developing plugins for the meta CLI platform, including the plugin protocol, best practices, and examples.
- Introduction
- Plugin Types
- Plugin Discovery
- Plugin Protocol
- Creating a Plugin
- Plugin Help System
- Best Practices
- Testing Plugins
- Publishing Plugins
- Troubleshooting
- See Also
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
Build plugins as Rust crates for performance and native integration:
// Cargo.toml
[package]
name = "meta-docker"
version = "0.1.0"
[[bin]]
name = "meta-docker"Any executable in your PATH or .meta-plugins directory:
#!/bin/bash
# meta-hello pluginShell scripts, Python, Node.js, etc.:
#!/usr/bin/env python3
# meta-python plugin
import json
import sysMeta discovers plugins from these locations (in order):
.meta-plugins/in current directory.meta-plugins/in parent directories (up to root)~/.meta-plugins/in home directory- System PATH (executables named
meta-*)
Plugins must follow the naming pattern:
meta-<name>(e.g.,meta-docker,meta-npm)- Or
meta_<name>_clifor Rust crates (e.g.,meta_git_cli)
Meta strips the prefix to determine the command. meta-docker handles meta docker <subcommand>.
Plugins communicate with meta via JSON over stdin/stdout.
Meta queries plugin capabilities:
meta-docker --meta-plugin-infoResponse:
{
"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"
}
}Meta invokes plugin execution:
echo '<request>' | meta-docker --meta-plugin-execRequest 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
}
}{
"success": false,
"error": "Unknown command: foo",
"results": []
}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,
}
}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"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")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.
Always implement both --meta-plugin-info and --meta-plugin-exec flags.
PluginResponse {
success: false,
results: vec![],
error: Some("Failed to connect to Docker daemon".to_string()),
}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();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,
};
}When options.json_output is true, ensure structured output.
Avoid OS-specific code when possible. Use standard paths and commands.
# 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 .# Place plugin in discovery path
cp meta-docker ~/.meta-plugins/
# Test via meta
meta docker build --dry-run#[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");
}# Search for plugins
meta plugin search docker
# Install from registry
meta plugin install meta-docker- Build for target platforms
- Distribute binaries or packages
- Users place in
~/.meta-plugins/or PATH
- Ensure executable permissions:
chmod +x meta-plugin - Check naming: must start with
meta- - Verify location:
.meta-plugins/,~/.meta-plugins/, or PATH - Use
META_DEBUG=1for debug output
- Validate JSON format with
jq - Check for proper stdout/stderr separation
- Ensure newline after JSON output
- Check file permissions
- Verify PATH includes plugin directory