|
| 1 | +--- |
| 2 | +title: Custom CLI plugins |
| 3 | +icon: /icons/arcticons-game-plugins.svg |
| 4 | +star: true |
| 5 | +order: 0.15 |
| 6 | +--- |
| 7 | + |
| 8 | +If built-in commands are not enough, you can extend `jzero` with external executables instead of modifying the main binary. |
| 9 | + |
| 10 | +This is useful for team-specific scaffolding, internal release workflows, deployment helpers, or any command that should feel like a native `jzero` subcommand. |
| 11 | + |
| 12 | +## Discovery rules |
| 13 | + |
| 14 | +When `jzero` receives an unknown command, it searches `PATH` for matching plugin executables: |
| 15 | + |
| 16 | +* `jzero hello` -> `jzero-hello` |
| 17 | +* `jzero foo bar` -> first tries `jzero-foo-bar`, then falls back to `jzero-foo` |
| 18 | +* After a plugin is matched, the remaining arguments are passed through to the plugin unchanged |
| 19 | +* The current environment variables are also forwarded to the plugin process |
| 20 | + |
| 21 | +A plugin only needs two requirements: |
| 22 | + |
| 23 | +* The file name starts with `jzero-` |
| 24 | +* The file is executable and available in `PATH` |
| 25 | + |
| 26 | +:::tip |
| 27 | +Put plugin-specific flags after the plugin command, for example `jzero hello --name codex`. |
| 28 | +::: |
| 29 | + |
| 30 | +## Minimal example |
| 31 | + |
| 32 | +The plugin can be written in Go, shell, or any language that can produce an executable in `PATH`. |
| 33 | + |
| 34 | +```bash |
| 35 | +mkdir -p ~/.local/bin |
| 36 | + |
| 37 | +cat > ~/.local/bin/jzero-hello <<'EOF' |
| 38 | +#!/usr/bin/env bash |
| 39 | +set -euo pipefail |
| 40 | +
|
| 41 | +name="${1:-world}" |
| 42 | +printf 'hello, %s\n' "$name" |
| 43 | +EOF |
| 44 | + |
| 45 | +chmod +x ~/.local/bin/jzero-hello |
| 46 | +export PATH="$HOME/.local/bin:$PATH" |
| 47 | + |
| 48 | +jzero hello codex |
| 49 | +# hello, codex |
| 50 | +``` |
| 51 | + |
| 52 | +## Read `desc` metadata in Go plugins |
| 53 | + |
| 54 | +If your plugin is implemented in Go, you can also reuse `github.com/jzero-io/jzero/cmd/jzero/pkg/plugin`. |
| 55 | + |
| 56 | +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. |
| 57 | + |
| 58 | +`plugin.New()` scans the current working directory and attempts to parse: |
| 59 | + |
| 60 | +* `desc/api` |
| 61 | +* `desc/proto` |
| 62 | +* `desc/sql` |
| 63 | + |
| 64 | +It returns a `Metadata` value whose `Desc` field contains: |
| 65 | + |
| 66 | +* `Desc.Api.SpecMap`: parsed API specs keyed by source file path |
| 67 | +* `Desc.Proto.SpecMap`: parsed Proto specs keyed by source file path |
| 68 | +* `Desc.Model.SpecMap`: parsed SQL table specs keyed by table name |
| 69 | + |
| 70 | +```go |
| 71 | +package main |
| 72 | + |
| 73 | +import ( |
| 74 | + "fmt" |
| 75 | + |
| 76 | + jplugin "github.com/jzero-io/jzero/cmd/jzero/pkg/plugin" |
| 77 | +) |
| 78 | + |
| 79 | +func main() { |
| 80 | + metadata, err := jplugin.New() |
| 81 | + if err != nil { |
| 82 | + panic(err) |
| 83 | + } |
| 84 | + |
| 85 | + fmt.Printf("api files: %d\n", len(metadata.Desc.Api.SpecMap)) |
| 86 | + fmt.Printf("proto files: %d\n", len(metadata.Desc.Proto.SpecMap)) |
| 87 | + fmt.Printf("sql tables: %d\n", len(metadata.Desc.Model.SpecMap)) |
| 88 | +} |
| 89 | +``` |
| 90 | + |
| 91 | +:::tip |
| 92 | +`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. |
| 93 | +::: |
| 94 | + |
| 95 | +## Multi-level commands |
| 96 | + |
| 97 | +You can map multiple command levels to a single executable name. |
| 98 | + |
| 99 | +```bash |
| 100 | +# jzero foo bar baz |
| 101 | +# jzero will try jzero-foo-bar first |
| 102 | +# if not found, it falls back to jzero-foo |
| 103 | +# this usually means subcommands like "bar baz" are handled by jzero-foo itself |
| 104 | +``` |
| 105 | + |
| 106 | +This allows you to organize team commands in a natural way, such as `jzero release publish` or `jzero company bootstrap`. |
| 107 | + |
| 108 | +## Naming notes |
| 109 | + |
| 110 | +Inside each command segment, `jzero` normalizes `-` to `_` before lookup. |
| 111 | + |
| 112 | +For example: |
| 113 | + |
| 114 | +* `jzero my-cmd` -> executable name `jzero-my_cmd` |
| 115 | + |
| 116 | +To keep naming predictable, prefer simple command names or use `_` in the plugin executable when your command segment contains `-`. |
| 117 | + |
| 118 | +## Recommended workflow |
| 119 | + |
| 120 | +1. Build or place the plugin executable in a directory that is already in `PATH` |
| 121 | +2. Follow the `jzero-<command>` naming rule |
| 122 | +3. Add help output in the plugin itself, then use `jzero <command> --help` to view usage |
| 123 | + |
| 124 | +Plugins are discovered dynamically, so they are not part of the built-in static command list printed by `jzero --help`. |
0 commit comments