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.zigmyapp init
  • users/create.zigmyapp users create
  • users/index.zig → describes the users group (no execute = shows subcommands)
  • index.zig at 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.zigmyapp 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