- Published on
Auto Means Ask the Terminal: Making One CLI Serve Humans and Pipes
- Authors

- Name
- Mark Beacom
- @mbeacom

A command-line tool that both a person and a pipeline use has a problem it can't design away. There's one stdout, and the two audiences want incompatible things from it.
A person wants color, alignment, truncation, and a summary. A pipeline wants bytes that don't change when the terminal width does. Most tools resolve this by picking a side and making the other audience suffer, or by adding a --porcelain flag that half the users never discover.
adrkit's adr graph had picked a side. It emitted DOT, always, because a graph of decision relationships is genuinely useful as machine input. Which meant that a human running it in a terminal to answer "what supersedes what?" got a wall of Graphviz source.
Two releases this month fixed that without breaking a single pipeline. Here's the reasoning, because the general pattern applies well beyond this tool.
- The default is a question, not a format
- Two more views
- Making mistakes cheap
- Completions, and why they're only stdout
- The name I couldn't have
- One more distribution surface
- What generalizes
The default is a question, not a format
adr graph's default format is now auto, and auto means: ask the terminal.
Attached to a TTY, you get a compact status and relationship view meant for reading. Captured, piped, or redirected, you get the same deterministic DOT the command has always produced. An explicit --format terminal, dot, json, or mermaid always wins over the detection.
That last clause is the one that makes it safe. Auto-detection is a convenience for the interactive case and never the thing a script depends on. A script that says what it wants gets what it asked for, on every runner, in every container, regardless of whether something upstream happens to allocate a pseudo-terminal.
Detect for the human, obey for the machine. Auto-detection should only ever change output for a caller who did not state a preference, and stating a preference must be absolute.
The DOT output did get better while this happened, but carefully: it now carries visible status labels and the project's palette, while preserving node ids, status attributes, relationship directions, and edge labels. Anything a consumer was already parsing still parses. The JSON format keeps its historical ordering rather than being quietly "improved" into a different byte sequence.
One genuine behavior change came with it. A graph built from a partially invalid corpus is still emitted completely from the valid records, but the error findings now go to stderr and the command exits 1. Previously you could get a graph that silently omitted things and a success code, which is the same failure class as the CI bug I wrote about separately: output that looks healthy because the broken path and the working path are indistinguishable.
Two more views
--format mermaid emits deterministic, GitHub-compatible flowchart source, which matters because GitHub renders Mermaid inline in markdown. A decision graph you can paste into a pull request description is a different artifact from one you have to run Graphviz over.
The other addition is scope control. A corpus of thirty-four records renders as an unreadable hairball no matter how good the renderer is, so --focus <id> keeps one decision and its direct neighborhood, and a repeatable --kind <supersedes|relatesTo|conflictsWith> filters edges by relationship type in every format.
The filter behind both of those, and the Mermaid renderer, are exported from @adrkit/core as pure functions, so a consumer can narrow a graph and render it without shelling out to the CLI.
The terminal renderer also truncates multilingual titles by grapheme-safe display width rather than by character count. That's a small thing that's wrong in a surprising number of tools: a title containing an emoji, a combining mark, or any East Asian text truncates to the wrong visual width if you count code points, and the column alignment falls apart.
Making mistakes cheap
The other half of this work is what happens when you get it wrong, which for a CLI is most of the time you're learning it.
Three layers, all added in v0.10:
Command typos get a suggestion. Mistyping a subcommand now proposes the one you probably meant, rather than dumping global help and letting you find it.
Mistyped long options get a suggestion too. This is the one I wanted most. Nearly every CLI helps with commands and then says "unknown option --foramt" and stops, which is strange, because that's a smaller and easier search space.
Closed value sets get near-miss hints. new --status, graph --format, migrate --from, and queue --format all accept a fixed set of values, so a wrong value can say what the right ones are and which one you were close to.
The constraint on all three is stated explicitly in the release: none of it changes the existing exit codes or the split between stdout and stderr. That's the discipline that makes error-message work safe to do. A friendlier message that moves an error from stderr to stdout, or that turns a 2 into a 1, has broken every script that was handling it correctly.
Help itself is now task-oriented: it names and describes each command, shows common workflows, links to the full reference, and no longer exposes an internal citation that meant nothing to a reader. Running adr with no arguments shows help and exits successfully, rather than treating "you didn't tell me what to do" as a failure.
Completions, and why they're only stdout
adr completion prints shell scripts for Bash, Zsh, and Fish. It writes to stdout only, covers the top-level commands plus each command's documented options, and does not install anything.
That restraint is deliberate. A completion command that writes into your shell configuration is a tool that mutates your machine as a side effect of asking it a question. Printing to stdout means the user decides where it goes, eval works, redirecting to a file works, and inspecting it before trusting it works.
Color follows the same posture. --color auto|always|never controls styling, NO_COLOR is respected, and piped or redirected plain-text output stays free of ANSI escapes. JSON and completion output are never styled at all.
The name I couldn't have
A digression, but a useful one if you're ever naming a package.
The plan was to publish an unscoped adrkit on npm as a thin forwarder, so that npx adrkit couldn't resolve to whatever someone else might publish under that name. It was built, tested, and packed. Publishing it during the release failed:
403 Forbidden - PUT https://registry.npmjs.org/adrkit
Package name too similar to existing package pdfkit
npm's similarity check compares names after stripping punctuation, and adrkit is apparently too close to pdfkit. The same rule blocks adr-kit and adr_kit, which both normalize to adrkit exactly.
The instructive part is what the pre-check had actually proven. npm view adrkit returned a 404, and a 404 means the name is unused. Publishable is a strictly stronger property, and nothing in the registry's read API tells you whether you have it. The only test for publishable is publishing.
So the zero-install npx adrkit form is permanently unavailable, and npx @adrkit/cli remains the zero-install path.
What did ship is a binary alias. @adrkit/cli now installs adrkit alongside adr, because the bare name adr on npm belongs to an unrelated package that's been there since 2017. That creates two distinct ambiguities: a bare npx adr fetches the other package when adrkit's binary isn't linked into node_modules/.bin, and a project holding both dependencies has them competing for the same .bin entry. The name adrkit collides with nothing. adr is unchanged and still primary; this is purely additive.
And the limit is worth stating, because it's the kind of thing that gets rounded up into a safety claim it doesn't support: the alias covers the installed binary only. npx adrkit is not made safe by it. That still resolves against the registry and would run whatever anyone publishes there, without prompting, because npm assumes --yes on a non-TTY. npx --no isn't a sufficient answer either, since it declines to install a missing package but will still run one already sitting in the npx cache.
One more distribution surface
v0.11 also ships a source-built OCI image at ghcr.io/mbeacom/adrkit, multi-architecture, running as a non-root user on Node 24, with immutable version tags plus a registry provenance attestation. One dispatcher fronts the CLI, the MCP server, and both CI entry points, and the documented MCP examples use a read-only root with the repository mounted in.
That's mostly interesting as a statement about the trust posture: a governance tool that reads your decision corpus should be trivially runnable without a global install and without write access to anything.
What generalizes
Very little of the above is about architecture decision records. The parts I'd carry to any command-line tool:
Auto-detect for humans and obey explicit flags absolutely. The flag is the contract. Detection is a courtesy extended only to callers who didn't use it.
Improve error messages without moving exit codes or streams. A better message that changes either one is a breaking change wearing a friendly face.
Help with flag typos, not just command typos. Smaller search space, more common mistake, and almost nobody does it.
Print, don't install. A command that modifies the user's environment to answer a question has exceeded its brief.
Availability is not permission. Whether it's a package name, a subdomain, or a database identifier, the read API tells you a thing is unused. It does not tell you that you can have it.
- Site: adrkit.dev: quickstart, command reference
- Source: github.com/mbeacom/adrkit, Apache-2.0, schema additionally CC0
Sources. Format, completion, color, and error-recovery behavior is quoted from the project's CHANGELOG for v0.10.0 and v0.11.0, and the graph presentation decision is recorded as ADR-0033. The npm rejection message and the adr / adrkit binary reasoning are recorded under v0.8.0. Checked against the published v0.11.0 package on 26 August 2026.
