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
clibuilder12.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.
At a glance
Section titled “At a glance”Output modes: axi and console
Section titled “Output modes: axi and console”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.
toon is the default format
Section titled “toon is the default format”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.
ui.* goes to stderr in axi mode
Section titled “ui.* goes to stderr in axi mode”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'.
Help is an answer
Section titled “Help is an answer”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.
Returned values are printed
Section titled “Returned values are printed”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.
Exit codes 0, 1 and 2
Section titled “Exit codes 0, 1 and 2”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.
zod is replaced by t
Section titled “zod is replaced by t”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.
standard-log is removed
Section titled “standard-log is removed”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.
erroris red andwarnis yellow only on a stream that is a TTY, followingNO_COLORandFORCE_COLOR. In 11.5.0 standard-log-color wrapped clibuilder’s ownerrorandwarntext 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
clibuilderprefix. clibuilder’s own messages, such as those shown with--debug-cli, no longer start withclibuilder. 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.
Plugins move to @clibuilder/plugins
Section titled “Plugins move to @clibuilder/plugins”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
configoption ofcli()is removed..use(config())fromclibuilder/configloads the config for the commands only. It no longer loads the plugins that the config’spluginskey names. - The
keywordsoption ofcli()andthis.keywordsinrun()are removed. Keywords are an option ofplugins({ keywords }), and default to the CLI’s name. - The
pluginscommand group (plugins list,plugins search) is no longer added whenconfigorkeywordsis set.plugins()adds it. Passcommand: falseto opt out. clibuilder12 no longer depends onimport-meta-resolve,find-installed-packagesorsearch-packages.@clibuilder/pluginsowns 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.
@clibuilder/axi is folded into core
Section titled “@clibuilder/axi is folded into core”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.