Skip to content

Roadmap to v12

clibuilder 12 is a breaking release. It makes agent-facing output the default and removes most of the runtime dependencies. Two accepted RFCs set its design:

  • axi-native: output modes, formats, errors and exit codes.
  • slim-dependencies: replacing zod and standard-log, and moving plugin support out of core.

This page summarizes the decisions a CLI author needs. The RFCs are the authority. Where a detail here differs from them, they win.

To upgrade a CLI, follow Upgrading from clibuilder 11 to 12. It goes through each change in order, with the fix and the codemod.

Each item has one of these labels:

  • Available: published on npm. You can install it today.
  • Shipped in 12.0.0: part of clibuilder 12.0.0, published on npm.
  • Planned: decided, not built yet. The design can still change in its pull request.

clibuilder 12.0.0 is published, together with @clibuilder/plugins 0.2.0, @clibuilder/axi 0.3.0 and @clibuilder/codemod 0.1.0.

Change Status Version
Exit codes 0, 1 and 2 Available clibuilder 11
@clibuilder/axi output conventions Available @clibuilder/axi 0.2.1
Bare --version answered before loading Available clibuilder 11.5.0
cli().use() extension hook Available clibuilder 11.5.0
@clibuilder/plugins Available @clibuilder/plugins 0.2.0
t option descriptors in args-minus Available args-minus 0.2.0
t re-exported from clibuilder, parser moved onto args-minus Available clibuilder 11.5.0
cli({ streams }) and cli({ output }), with axi the default and console the permanent opt-out Shipped 12.0.0
Structured CliError, rendered once on stdout by default Shipped 12.0.0
Global --format and this.format on for every CLI, toon the default Shipped 12.0.0
Ranked, coded usage errors on stdout by default Shipped 12.0.0
Command examples and structured --help; --help and --version as answers by default Shipped 12.0.0
ui.* on stderr by default Shipped 12.0.0
Returned values printed by default Shipped 12.0.0
ui.showHelp and ui.showVersion removed from ui Planned a later major; deprecated in 12
ref() and an opt-in next[N]{run,why} in answers (#696) Planned the next 12.x minor
A config write API (#488) Planned not in 12
zod removed; t is the only option type, config any Standard Schema, js-yaml an optional peer Shipped 12.0.0
standard-log removed; the built-in UI is the only one Shipped 12.0.0
Plugin loading removed from core Shipped 12.0.0
clibuilder/axi subpath with the value helpers; @clibuilder/axi 0.3.0 keeps deprecated copies Shipped 12.0.0
testCommand only from clibuilder/testing; the root export removed Shipped 12.0.0
ESM-only; require() works through require(esm) on Node.js 20.19+ Shipped 12.0.0
@clibuilder/axi deprecated on npm Shipped @clibuilder/axi 0.3.0 is the last release
Headless clibuilder/axi: export encode, errorOutput, printAnswer and CliError, and a runScript() runner, so the subpath works without cli() Shipped 12.0.0
Upgrade guide and @clibuilder/codemod Shipped 12.0.0, @clibuilder/codemod 0.1.0

Shipped in 12.0.0. cli({ output: 'axi' | 'console' }) and cli({ streams }). cli({ output }) picks one of two modes. axi is the default. Both modes are permanent, and neither is deprecated. See Streams and output, Errors with output: 'axi' and Usage errors with output: 'axi'.

output: 'axi' (the 12 default) output: 'console'
Value returned from run() stdout, encoded not printed
Thrown CliError stdout, encoded once message and help on stderr
Usage error stdout, coded, exit 2 prose and full help on stderr, exit 2
--help, --version stdout stdout
ui.info, ui.debug stderr stdout
ui.warn, ui.error stderr stderr

The console column is how clibuilder 11 behaves. To keep that behavior in 12, set output: 'console'. Its output is the 11.5.0 output byte for byte, except that the help lists the global --format. Source: axi-native section 3.10.

Shipped in 12.0.0. The global --format, this.format, and the startup error for a command’s own format option are in every CLI, in both output modes. See The global --format option.

Every command accepts a global --format <toon|json|text>, and run() reads the resolved value from this.format. The default is toon. Change it with cli({ format: 'text' }). The format does not change between a terminal and a pipe. A command that declares its own format option is a startup error in v12: delete it and read this.format. -f is not reserved for --format. A bad --format value is a usage error with exit code 2. Source: axi-native section 3.4.

Shipped in 12.0.0. In axi mode, the default, every ui.* call is a diagnostic on stderr, including ui.info. Answers go to stdout. If a script reads what your command prints with ui.info, return the value from run() instead, or use output: 'console'.

Shipped in 12.0.0 with output: 'axi', the default: --help is an answer on stdout that follows --format, text help under text and the structured help under toon and json. --version is plain text in every format. A command may declare examples, shown at the end of its help. ui.showHelp and ui.showVersion are deprecated; from run() they answer like --help and --version. See --help and --version with output: 'axi'.

A usage error no longer prints the full help in axi mode. showHelp and showVersion stay on ui in 12, deprecated, and are removed in a later major. Source: axi-native section 3.7.

Shipped in 12.0.0. The result channel is in the default axi mode (see The result channel). In axi mode, a value other than undefined returned from run() is printed to stdout in the resolved format. parse() still resolves to it, so tests and programmatic callers see no change. Return undefined to print nothing. result(value, { help, render }) controls the printed form, document(content) prints content verbatim, and empty(message) reports an empty result with exit code 0. The codemod lists every return <expr> in a run() for review. Source: axi-native section 3.5.

Available in clibuilder 11: exitCodes is { success: 0, error: 1, usage: 2 }. See Failing.

Shipped in 12.0.0: core keeps exactly these three. CliError.exitCode accepts any integer from 1 to 255, and 0 is rejected. Neither core nor @clibuilder/axi defines named codes above 2. The exit code says success, failure or usage error. The code string on the error says which failure. The rule is written up in Codes above 2 belong to your cli. Source: axi-native section 3.6.

Available in args-minus 0.2.0: the t descriptors, with constraints as options, .optional(), .check(), t.custom(), t.json() and parseArgsAsync().

Available in clibuilder 11.5.0: clibuilder re-exports t, and an argument or option declared with t is converted and checked by args-minus. This is not a breaking change: each declaration is parsed by its own type, so a command can mix z and t, and z declarations behave as before in 11.5.0. See t.

Shipped in 12.0.0: zod is removed, with the z re-export, and type-plus and tersify with it. t is the only way to declare an option or argument type. config takes any Standard Schema, so you can keep zod there as your own dependency. js-yaml becomes an optional peer dependency, loaded only when a YAML config file is read. A clean install of clibuilder drops from 41 packages (19 MB) to 3 (1.2 MB). A codemod rewrites the supported zod calls to t, and the upgrade guide lists the behaviour that changes with it. A conversion that reads files, resolves paths, calls the network or depends on other options moves into run(); see what belongs in the parser. Source: slim-dependencies section 3.4.

Shipped in 12.0.0. clibuilder no longer depends on standard-log and standard-log-color. Every CLI writes through the built-in UI to cli({ streams: { stdout, stderr } }), which defaults to process.stdout and process.stderr. --verbose, --silent and --debug-cli work as before, and testCommand() still returns messages, one line per message.

With output: 'console', info and debug still go to stdout, and warn and error to stderr. What changes from clibuilder 11.5.0:

  • Colour only on a terminal. error is red and warn is yellow only on a stream that is a TTY, following NO_COLOR and FORCE_COLOR. In 11.5.0 standard-log-color wrapped clibuilder’s own error and warn text in colour codes even when stderr was a pipe or a file. To migrate: if a script or a test matched those escape codes, match the plain text.
  • No clibuilder prefix. clibuilder’s own messages, such as those shown with --debug-cli, no longer start with clibuilder. To migrate: match the message without the prefix.

No code change is needed otherwise. If you want standard-log, pino or another logger, wrap it in a function your commands call. Source: slim-dependencies section 4.

Available: @clibuilder/plugins 0.1.0 for clibuilder 11 and the use() hook in clibuilder 11.5.0. A v11 CLI can switch with .use(plugins()).

Shipped in 12.0.0: clibuilder 12 has no plugin support in core. This is a breaking change:

  • The config option of cli() is removed. .use(config()) from clibuilder/config loads the config for the commands only. It no longer loads the plugins that the config’s plugins key names.
  • The keywords option of cli() and this.keywords in run() are removed. Keywords are an option of plugins({ keywords }), and default to the CLI’s name.
  • The plugins command group (plugins list, plugins search) is no longer added when config or keywords is set. plugins() adds it. Pass command: false to opt out.
  • clibuilder 12 no longer depends on import-meta-resolve, find-installed-packages or search-packages. @clibuilder/plugins owns them.

To migrate, add @clibuilder/plugins and one call to .use():

cli({ name: 'my-cli', version: '1.0.0', config: true, keywords: ['my-cli-plugin'] })
cli({ name: 'my-cli', version: '1.0.0' })
.use(config())
.use(plugins({ keywords: ['my-cli-plugin'] }))

Import plugins with import { plugins } from '@clibuilder/plugins', and config with import { config } from 'clibuilder/config'. Add config() before plugins().

If the config has a non-empty plugins list and no extension reads the config, the CLI warns once: “the config lists plugins, but this cli loads none. Plugin loading moved out of clibuilder 12 into @clibuilder/plugins: add .use(plugins()) to the cli.”

What stays in core: cli().use() and ExtensionContext, the registry (defineKey, defineCollectionKey and this.registry), and mockPluginContext in clibuilder/testing.

A plugin’s activate(context) is unchanged, and a plugin built for 11 loads unchanged. import type { PluginActivationContext } from 'clibuilder' still compiles in 12 but is deprecated. Import it from @clibuilder/plugins. clibuilder 13 removes it.

@clibuilder/plugins 0.2.0 requires clibuilder ^12 as a peer. With clibuilder 11, keep using @clibuilder/plugins 0.1.0. The 0.1.0 behavior where 11 skips its own loading when plugins() is used goes away with core’s loading.

Downstream: repobuddy/buddy relies on cli({ config: true }) loading plugins and needs .use(config()).use(plugins()).

Source: slim-dependencies section 9.

Available for clibuilder 11: @clibuilder/axi 0.2.1.

Shipped in 12.0.0: the truncation, count and home helpers are in the clibuilder/axi subpath, and the output, error, usage-error and format parts are built into core. @clibuilder/axi 0.3.0 is the last release and is deprecated on npm. It needs clibuilder 12, keeps deprecated copies of the helpers with the same behavior, and marks every export @deprecated with its core replacement. fullOption is declared with t instead of zod. Keep using 0.2.1 with clibuilder 11. Source: axi-native section 4.1.