Commands & structure
Commands are .zig files in your commands directory; the file path becomes the command path. There is no routing table and no registration call — the build scans the directory, validates every command at compile time, and generates direct dispatch code. Zero runtime reflection.
init.zig→myapp initusers/create.zig→myapp users createusers/index.zig→ describes theusersgroup (noexecute= shows subcommands)index.zigat the top level → the root itself — a CLI with no subcommands at all is a single-command CLI
Groups nest as deep as you need (gh/add/workflow/release.zig → myapp gh add workflow release). Files starting with _ are skipped — use them for helpers shared by sibling commands.
Anatomy of a command
Every command has up to four exports. Args and Options are required whenever execute is present (empty is spelled struct {}); meta carries descriptions and per-field metadata.
const Context = @import("command_registry").Context;
pub const meta = .{
.description = "Deploy your application",
.examples = &.{ "deploy api --env staging" },
};
pub const Args = struct {
service: []const u8,
};
pub const Options = struct {
env: []const u8 = "dev",
};
pub fn execute(args: Args, options: Options, context: *Context) !void {
try context.stdout().print("Deploying {s} to {s}\n", .{ args.service, options.env });
}
The contract is checked at compile time: a command with execute but no Args, an Options that isn’t a struct, or a meta entry naming a field that doesn’t exist all fail the build with an error that names the file and the fix.
Group landing pages
A directory is a group. Add an index.zig to give it a description — and optionally a landing command:
// src/commands/users/index.zig — pure group: describes it, lists subcommands
pub const meta = .{
.description = "Manage users",
};
Without an execute, running myapp users shows the group’s help and subcommands. Give index.zig an execute (plus Args/Options) and the group itself becomes runnable.
A runnable landing command can even take positionals: when its Args declares at least one positional, a word that matches no subcommand routes to the landing command as a value — myapp users 123 runs users/index.zig with 123. Declaring root positionals trades away “did you mean?” typo suggestions for that group (the framework can no longer tell a typo from a value — real subcommand names always win, and -- forces a colliding word to be a value); a landing command with no positionals keeps full suggestions.
Single-command CLIs
The root of your commands directory is a group like any other — so a top-level index.zig with an execute and no sibling commands is a single-command CLI, the rg/fd/jq shape:
src/commands/
└── index.zig → myapp <args> [options]
The root index is a full command: typed Args and Options, meta, help (usage leads with myapp [OPTIONS] <ARGS>), completions, zcli tree, and unit-test wiring all work exactly as they do for subcommands. Bare invocation, option-first argv (myapp --loud World), and bare positionals (myapp World) all route to it by the same longest-match-then-fallback rule described above. Scaffold this shape with zcli init --template single.
Aliases and hidden commands
Two more meta fields shape how a command is reached:
pub const meta = .{
.description = "Remove a resource",
.aliases = &.{ "delete", "del" }, // alternate names for the same command
.hidden = true, // runs normally, but not listed in help
};
Aliases route like the real name; hidden commands are for internal or deprecated entry points you don’t want advertised.
Typos get suggestions
A mistyped command gets a “did you mean?” computed by edit distance, followed by the available commands — wired up by the zcli_not_found plugin, on by default in scaffolded projects:
$ myapp usrs list
Error: Unknown command 'usrs'
Did you mean 'users'?
Change structure with the scaffolder
The file layout is your CLI’s structure, so structure changes are file operations — and the meta-CLI does them mechanically, keeping the struct fields, meta entries, and argument order in sync:
zcli add command users/create # new command file (wizard or flags)
zcli add arg users/create name # splice a positional into Args
zcli mv users/create people/new # move/rename, tidying empty groups
zcli rm command people/new # remove the file
zcli tree --show-options # read the current shape, no build needed
zcli add/rm/mv edit command files via an AST splice engine that preserves your hand-written execute body. Full reference: the zcli CLI.
Next
- Args & options — typed parsing, required options, arrays, env vars
- The context — what your
executegets to work with