Custom CLI plugins
If built-in commands are not enough, you can extend jzero with external executables instead of modifying the main binary.
This is useful for team-specific scaffolding, internal release workflows, deployment helpers, or any command that should feel like a native jzero subcommand.
Discovery rules
When jzero receives an unknown command, it searches PATH for matching plugin executables:
jzero hello->jzero-hellojzero foo bar-> first triesjzero-foo-bar, then falls back tojzero-foo- After a plugin is matched, the remaining arguments are passed through to the plugin unchanged
- The current environment variables are also forwarded to the plugin process
A plugin only needs two requirements:
- The file name starts with
jzero- - The file is executable and available in
PATH
Tips
Put plugin-specific flags after the plugin command, for example jzero hello --name codex.
Minimal example
The plugin can be written in Go, shell, or any language that can produce an executable in PATH.
mkdir -p ~/.local/bin
cat > ~/.local/bin/jzero-hello <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
name="${1:-world}"
printf 'hello, %s\n' "$name"
EOF
chmod +x ~/.local/bin/jzero-hello
export PATH="$HOME/.local/bin:$PATH"
jzero hello codex
# hello, codexRead desc metadata in Go plugins
If your plugin is implemented in Go, you can also reuse github.com/jzero-io/jzero/cmd/jzero/pkg/plugin.
This does not replace external plugin discovery. jzero still discovers your binary through the jzero-* naming rule. The extra package is for reading parsed project metadata inside the plugin process.
plugin.New() scans the current working directory and attempts to parse:
desc/apidesc/protodesc/sql
It returns a Metadata value whose Desc field contains:
Desc.Api.SpecMap: parsed API specs keyed by source file pathDesc.Proto.SpecMap: parsed Proto specs keyed by source file pathDesc.Model.SpecMap: parsed SQL table specs keyed by table name
package main
import (
"fmt"
jplugin "github.com/jzero-io/jzero/cmd/jzero/pkg/plugin"
)
func main() {
metadata, err := jplugin.New()
if err != nil {
panic(err)
}
fmt.Printf("api files: %d\n", len(metadata.Desc.Api.SpecMap))
fmt.Printf("proto files: %d\n", len(metadata.Desc.Proto.SpecMap))
fmt.Printf("sql tables: %d\n", len(metadata.Desc.Model.SpecMap))
}Tips
plugin.New() reads from the plugin process's current working directory, so it is typically used when your plugin is executed inside a jzero project root.
Multi-level commands
You can map multiple command levels to a single executable name.
# jzero foo bar baz
# jzero will try jzero-foo-bar first
# if not found, it falls back to jzero-foo
# this usually means subcommands like "bar baz" are handled by jzero-foo itselfThis allows you to organize team commands in a natural way, such as jzero release publish or jzero company bootstrap.
Naming notes
Inside each command segment, jzero normalizes - to _ before lookup.
For example:
jzero my-cmd-> executable namejzero-my_cmd
To keep naming predictable, prefer simple command names or use _ in the plugin executable when your command segment contains -.
Recommended workflow
- Build or place the plugin executable in a directory that is already in
PATH - Follow the
jzero-<command>naming rule - Add help output in the plugin itself, then use
jzero <command> --helpto view usage
Plugins are discovered dynamically, so they are not part of the built-in static command list printed by jzero --help.