Why zcli

Comptime over runtime. Conventions over registration.

Zig already has good CLI libraries — the difference is the level they operate at. Here's the philosophy, an honest comparison, and every measured number behind the "batteries-included" claim.

Philosophy

Comptime over runtime. The command tree, the argument parser, and the routing table are all generated at build time from your files and your structs. There's no filesystem scan at startup, no reflection, no registry object built on every run — the binary that ships already knows its own shape.

Conventions over registration. A file in commands/ is a command. A folder is a group. There's no addCommand() call to forget, no builder chain to keep in sync with the file that implements it — the two can't drift apart because they're the same artifact.

Batteries with opt-out. Every package — zinput, zprogress, ztheme, terminal, vterm — works standalone, with no dependency on the framework. Use the whole thing, or take one package and leave the rest.

Stable Zig only. zcli targets Zig 0.16.0 — the current stable release, no nightly required. That's a deliberate constraint: a framework you can't build tomorrow because it tracked a moving target isn't actually batteries-included.

Comparison

zig-clap is an argument parser, and the lightest of the three: describe flags in a comptime help-text DSL, get typed results back, build the rest of the CLI yourself. If flag parsing is all you need, it's a great choice. zli is a batteries-included framework built around a runtime builder: commands are constructed with Command.init(...), wired up with addCommand, flags registered with addFlag and read by name.

zcli moves that work to the filesystem and the compiler. The directory tree is the command tree, discovered at build time with routing generated as ordinary Zig code. And the batteries extend past parsing into the whole terminal experience.

 zig-clapzlizcli
Scopeargument parserCLI frameworkCLI framework
Commands defined bymanual dispatchruntime builderfiles on disk, discovered at build time
Flags arecomptime DSL → typed resultregistered at runtime, read by namestruct fields, checked at compile time
Beyond parsinghelp texthelp, version, spinnershelp, completions, prompts, progress, theming, config files, plugins, testing tools

At the code level

The difference isn't abstract — it shows up in how you read an option value.

zli — runtime lookupzig
const verbose = ctx.flag("verbose", bool);
// string key, looked up at runtime;
// a typo compiles and returns a default
zcli — struct fieldzig
pub const Options = struct {
    verbose: bool = false,
};
// options.verbose — a typo is a
// compile error, not a silent default

Numbers

Every figure below is measured against zcli v0.19.0, built from the showcase and a minimal hello-world command. Expand a row for the exact commands used.

Hello-world binary, ReleaseSmall 215 KB
reproduce
$ zcli init hello --description "hello world"
$ cd hello && zig build -Doptimize=ReleaseSmall
$ ls -la zig-out/bin/hello
Static musl binary, same command, x86_64-linux-musl 195 KB
reproduce
$ zig build -Dtarget=x86_64-linux-musl -Doptimize=ReleaseSmall
$ file zig-out/bin/hello   # statically linked
macOS runtime dependencies libSystem only
reproduce
$ otool -L zig-out/bin/hello
ANSI escape sequences emitted with NO_COLOR=1 0
reproduce
$ NO_COLOR=1 zig-out/bin/hello --help | grep -c $'\x1b'
CI matrix 3 OSes, every commit
reproduce
Linux, macOS, and Windows — unit tests + full e2e suite,
including the interactive tier via a ConPTY backend on Windows.
See .github/workflows/ci.yml

Stability

zcli targets stable Zig — no nightly required. main and the latest release are built and tested against the current stable Zig on Linux, macOS, and Windows in CI on every commit.

zcliZig
main, v0.18.0 and later0.16.0
v0.14.0 – v0.17.00.15.1

Each release is tagged twice: vX.Y.Z is the framework library — the tag for your build.zig.zon — and zcli-vX.Y.Z carries the prebuilt meta-CLI binaries that install.sh downloads. The two ship in lockstep, tags are immutable once cut, and CI enforces the version pin doesn't drift silently between them.

Versioning policy →