cli-base is the part of a command line tool that is not about any one service: the two output
streams, the renderer, the error model and exit codes, the keyring and credentials, config and
paths, clocks, retry, and — behind their own entry points — HTTP, the command registry, completion,
self-update and code generation. What each export does is the table in the
README; this page is about the seams and how a change travels.
The shared modularity standard defines responsibility, dependency direction and review guidelines for module size. The entry-point map below applies it to this package.
| Import | Owns | Depends on |
|---|---|---|
@wirecat/cli-base |
streams, renderer, pretty, errors, exit codes, keyring, credentials, config, paths, logging, retry, time, sanitize, field projection, output buffering, deadlines | cli-table3; @napi-rs/keyring loaded on first use |
/http |
status → error code, Retry-After and rate-limit parsing, bounded UTF-8 JSON response reads, the fetch seam |
the error model and WallClock type |
/commands |
the command tree as data (describeProgram, annotate, flatten), protected shared definitions and explicit overrides (sharedCommand, overrideSharedContract, customizeSharedCommand), the commands --json document and its schema (commandsDocument), and the reference page written from it (commandsPage) |
Commander at runtime for contract guards; valibot |
/completion |
shell completion over that tree | /commands types |
/update |
which installer put the CLI there, the daily notice, running the update | config (the state file), /http's FetchLike |
/codegen |
build time: an OpenAPI document → ApiModel → types, Valibot schemas, manifest, coverage page |
nothing at run time |
/codegen/runtime |
the number helpers generated schemas import | valibot |
/release |
release checks a CLI runs before it publishes: releaseCheck and the checks it runs (changelog shape, document links, versions in step, package contents) |
Node built-ins only |
/skill |
the CLI's SKILL.md for agents: the skill command, the daily hint, the MCP resource |
Commander at runtime, /update's state file, the root's Renderer and Streams types |
/mcp |
local stdio MCP entry setup and a handshake probe | Node child processes; no messenger or MCP SDK dependency |
/standards |
the registries of the shared standard: vocabulary, envelope, error object and reasons, next steps, log record, lint rules | valibot, @valibot/to-json-schema; the root's error codes |
/wcat |
the wcat umbrella: dispatching to wcat-<name> programs, --wcat-describe, the launcher and the shortcuts |
Node built-ins, cross-spawn; the root's errors and failure rendering, loaded only when wcat itself runs |
/wcat/start |
wcatStart, a CLI's first call: describe, dispatch mode, the launcher check |
Node built-ins; the dispatcher only in dispatch mode |
/testing |
captureStreams, memoryKeyring, brokenKeyring, fakeClock, allowNetwork |
the root |
/testing/setup |
a vitest setupFiles entry: per-file sandbox, a keyring that throws, no network |
vitest (optional peer) |
Each is a separate exports entry in package.json, built from
src/<name>/index.ts, except /testing/setup from src/testing/setup.ts.
Everything under src/ ships in dist/, including src/testing/, so a helper only cli-base's own
tests use goes in test/.
Two more things ship that are not imports:
cli-dev, thebinbuilt fromsrc/dev/: the development scripts each CLI used to copy. It reads and writes the repository it is run in and never the network.config/: the tsconfig, Biome and lefthook bases a CLI extends, exported as./tsconfig.base.json,./tsconfig.test.json,./tsconfig.scripts.json,./biomeand./lefthook.yml. cli-base extends them itself, so its own checks exercise them. The reusable CI workflow,.github/workflows/node-ci.yml, is not in the package; a repository calls it from GitHub.
Nothing in the root export is HTTP, so a CLI that only speaks a socket never depends on an HTTP
stack. max-cli's personal account is a WebSocket; its bot commands and tg-cli import
/http. A new helper that reads a status code or a header goes to /http.
The environment is an argument. Clock, sleep, keyring, env, fetch, streams — each is a
parameter, with the real one only as the default. That is what lets a timeout test finish instantly
and makes the keyring unreachable from a test (TESTING.md). The trap is the defaults:
new Credentials({...}) without keyring is the owner's real keychain, and resolvePaths without
env reads process.env.
cli-base logs nothing and prints nothing by itself. It hands a host a Logger interface, a Pino
adapter and a renderer; what is written, and where, is the host's call.
result-fields preserves the existing messaging projection behavior without importing its runner.
It validates parsed and directly supplied paths, traverses nested objects and arrays, and preserves
item/list envelopes, coverage, continuation, missing-data and execution metadata and operation identifiers.
Paths are payload-relative; the items. shorthand addresses each list row. Parent selections retain
an entire subtree, and missing descendants never invent values. Consumers call the same projector
without unwrapping item answers. A host chooses rendering and JSONL framing; projection never selects
a messenger profile.
batch owns bounded JSON input, pure registered-handler preparation and serial correlated execution.
It has no Commander, account, store or shell dependency. Domain handlers reserve conservative read ceilings;
the host supplies those handlers and retains authorization, cancellation, envelope framing and final output limits.
output-buffer holds raw rendered chunks until the host has completed its operation and closed
resources. The byte limit applies after optional complete-value JSON/JSONL projection, including
caller-supplied delimiters. It is a one-use buffer; flush consumes all chunks even on a writer failure.
It does not impose renderer framing or conceal partial output from a failing writer.
deadline uses an injected SleepLike and optional parent signal, never process-wide signal handlers.
It cancels cooperatively and waits for the body to settle on interruption. This preserves resource
lifetimes at the cost of delayed completion when a body ignores cancellation. outcome_unknown
from the body takes precedence over timeout/cancellation; other body failures remain unchanged when
there was no interruption. Hosts own close ordering and provider mutation policy. Dispose the scope
in finally; timers and listeners do not belong to the store or a messenger execution shell.
| Repository | Subpaths used (source and scripts, main, 2026-10-14) |
|---|---|
| cli-core | root, /codegen, /codegen/runtime, /commands, /completion, /mcp, /query, /skill, /standards, /update |
| max-cli | root, /codegen, /codegen/runtime, /commands, /http, /mcp, /query, /release, /skill, /update |
| tg-cli | root, /codegen, /codegen/runtime, /commands, /http, /release, /skill, /testing, /update |
Each consumer's pin is in its package.json; a caret on 0.x does not cross a minor, so every pin is
exact in effect. A release reaches nobody until each consumer bumps its pin.
⚠ cli-core takes cli-base as a peer dependency with the range declared in its package.json, so a CLI installs
one copy that both use. A new minor version of cli-base reaches the CLIs only after cli-core widens its range.
isCliError() checks a Symbol.for brand, so copies from 0.22.0 on recognise each other if two are ever
installed; an older copy has no brand, which is why errorObjectOf also accepts a CliError by name and code.
- A pull request here, with the
CHANGELOG.mdentry under## Unreleasedand, when it is ready to ship,versioninpackage.jsonraised and the heading dated as that version. - After merge,
bin/releaseonmain(RELEASING.md). If npm already has the number, it takes the next free one and renames the changelog heading with it, then startsrelease.yml, which runs every check, publishes through npm's trusted publishing and tagsv<version>. - In each consumer, raise the pin in
package.jsonand run its suite — cli-core first, since its peer range decides which cli-base the CLIs may install.