docs: add CLI evolution guidelines
The guidelines here provide a written version of common guidance around our CLI evolution that designers/implementors should consider as they propose/implement new or evolving CLI surfaces. Updates #engdocs Change-Id: Idcbc0900a4fda98bd2b29ac8bbc26dc1cb1be48f Signed-off-by: James Tucker <james@tailscale.com>
This commit is contained in:
committed by
James Tucker
parent
66a51c426f
commit
045c979802
+298
@@ -0,0 +1,298 @@
|
||||
# Tailscale CLI evolution guidelines
|
||||
|
||||
This document offers guidance for evolving the Tailscale core command-line
|
||||
interface(s), principally `tailscale` (and, where relevant, `tailscaled`). It
|
||||
is intentionally lightweight. It describes the *spirit* we want to preserve as
|
||||
the CLI grows, not a rigid specification. As with all style guides, some of it
|
||||
is subjective and exists mainly to codify existing conventions and promote
|
||||
consistency; other parts have stronger reasoning, such as compatibility,
|
||||
accessibility, or long-term maintenance burden.
|
||||
|
||||
When a guideline here conflicts with good judgement in a specific case,
|
||||
prefer good judgement, but raise the conflict so the guidance can be improved.
|
||||
These are defaults and tie-breakers, not laws.
|
||||
|
||||
## Collaboration and design review
|
||||
|
||||
CLI design is a collaborative process, and these guidelines are best applied in
|
||||
conversation rather than in isolation.
|
||||
|
||||
Tailscalars should seek collaboration in the **#cli** channel in Slack for
|
||||
design reviews early, before significant implementation work, while a design is
|
||||
still easy to change.
|
||||
|
||||
Open-source contributors should anticipate feedback on CLI changes and expect to
|
||||
be asked to approach design review collaboratively and thoughtfully. CLI surface
|
||||
is long-lived and hard to change once shipped, so a proposed change may go
|
||||
through more discussion than a typical bug fix. This is not a barrier; it is how
|
||||
we keep the CLI coherent. Engaging openly with that review process is the fastest
|
||||
path to getting a change merged.
|
||||
|
||||
## Influences and non-goals
|
||||
|
||||
Some good concepts and vocabulary here are drawn from the Fuchsia CLI
|
||||
guidelines:
|
||||
|
||||
* https://fuchsia.dev/fuchsia-src/development/api/cli
|
||||
* https://fuchsia.dev/fuchsia-src/development/api/cli_help
|
||||
|
||||
We borrow ideas, not rules. Those documents are more prescriptive than we want
|
||||
to be, and several of their specifics do not apply to us. In particular:
|
||||
|
||||
* We are **not** adopting their `--help` output format. Our help output has its
|
||||
own history and conventions, and we will not switch formats arbitrarily.
|
||||
* We have many **pre-existing** commands and behaviors that predate any of
|
||||
this guidance. We accept them as-is. This document is about how we *grow*
|
||||
from here, not a mandate to retrofit existing surfaces.
|
||||
|
||||
## Guiding principles
|
||||
|
||||
The Tailscale CLI is one of the most visible parts of the product. It is used
|
||||
interactively by humans, embedded in scripts and pipelines, read by screen
|
||||
readers, and depended on for automation. Small decisions compound. We would
|
||||
rather do the harder work of getting a surface right than take the easier
|
||||
short-term path of adding one more toggle.
|
||||
|
||||
### Keep overall configurability low
|
||||
|
||||
We aim to keep the overall configurability of the Tailscale client *low*. It is
|
||||
strongly preferred to identify broadly common solutions to heterogeneous user
|
||||
challenges and apply them holistically, rather than exposing large configuration
|
||||
surfaces.
|
||||
|
||||
Wide configuration surfaces are deceptively expensive: they invite users to
|
||||
spend effort tuning them, they increase the support load, they multiply the
|
||||
combinations we must test and reason about, and they tend to ossify into
|
||||
compatibility obligations. Prefer doing the hard work of getting the default
|
||||
right over the easy "now" work of adding another option.
|
||||
|
||||
This is a default bias, not an absolute prohibition. When a setting is genuinely
|
||||
necessary, add it deliberately and in the right place (see below).
|
||||
|
||||
## Command structure
|
||||
|
||||
### Top-level commands
|
||||
|
||||
The top-level command surface (`tailscale <command>`) is precious. Every
|
||||
addition there is effectively permanent and competes for the user's attention
|
||||
and memory. We want it to grow slowly.
|
||||
|
||||
Some commands earn a place at the top level because they are used so frequently
|
||||
that the brevity is worth it:
|
||||
|
||||
* `tailscale set` and `tailscale get` are fine as independent top-level
|
||||
commands, in large part *because their usage is so frequent*. The cost of an
|
||||
extra level of nesting would be paid constantly.
|
||||
|
||||
### Prefer `tailscale <noun> <verb>` for features
|
||||
|
||||
For new "feature"-oriented designs, we will generally prefer a
|
||||
`tailscale <noun> <verb>` structure (for example `tailscale <feature> status`,
|
||||
`tailscale <feature> enable`). This lets a feature's surface grow over time
|
||||
without growing the top-level CLI surface too quickly. New verbs and options
|
||||
live under the noun, where they are discoverable in context and do not crowd the
|
||||
root.
|
||||
|
||||
This is a guideline, not a cage. Do not force a design into
|
||||
`<noun> <verb>` if there is wide consensus that the resulting structure is
|
||||
awkward. Naturalness and usability win over structural purity.
|
||||
|
||||
### Noun naming
|
||||
|
||||
Prefer **singular** nouns (`tailscale cert`, `tailscale ip`) unless a singular
|
||||
form is strongly unnatural for the concept. Consistency in number makes commands
|
||||
easier to predict and remember.
|
||||
|
||||
### `tailscale up` is closed for extension
|
||||
|
||||
`tailscale up` is now effectively **closed for extension**. New configuration
|
||||
features should prefer to go into `set` rather than `up`.
|
||||
|
||||
The reason is historical and concrete: `up` has the long-standing problem that
|
||||
its flags describe a desired *complete* state, so a later invocation must repeat
|
||||
(or carefully manage) all of the earlier flags or risk resetting them. `set` was
|
||||
introduced precisely to allow incremental, additive configuration changes
|
||||
without that footgun. New knobs belong there.
|
||||
|
||||
### Extend existing surfaces when it fits the spirit
|
||||
|
||||
Existing surfaces should be extended where appropriate, provided the extension
|
||||
does not substantially change the *spirit* of the surface.
|
||||
|
||||
For example, adding new target query types to `tailscale ip` is a good fit where
|
||||
those targets are sufficiently visible to clients: it is the same operation
|
||||
("resolve a thing to an IP") applied to more inputs. That is extension in the
|
||||
spirit of the command, not scope creep.
|
||||
|
||||
### Avoid arbitrary growth in complexity
|
||||
|
||||
Resist the temptation to grow a command to match the full surface of some
|
||||
external tool it resembles. For example, `tailscale ssh` does **not** intend to
|
||||
reimplement the full flag surface of `ssh` over time. Doing so would be an
|
||||
enormous and open-ended maintenance burden, and `ssh` itself is essentially
|
||||
ubiquitously available and already serves that purpose. A Tailscale command
|
||||
should do its job well, not absorb an entire adjacent ecosystem.
|
||||
|
||||
## Debug commands
|
||||
|
||||
Heavily used `debug` subcommands should, over time, graduate into proper
|
||||
features of the main program when they prove broadly useful.
|
||||
|
||||
`debug` subcommands are explicitly **not** subject to our usual compatibility
|
||||
promises. They are best-effort and may change or be removed. Treat them as a
|
||||
staging ground and a power-user/diagnostic surface, not as a stable API. If a
|
||||
debug command becomes load-bearing for users, that is a signal to promote and
|
||||
stabilize it deliberately rather than to quietly grant it stability by neglect.
|
||||
|
||||
## Configuration: where settings live
|
||||
|
||||
### Profile-local preferences are the default home
|
||||
|
||||
Most configuration should be **profile-local** preferences, set and read via
|
||||
`set`/`get`, and modifiable at runtime. This is where the overwhelming majority
|
||||
of user-facing configuration belongs.
|
||||
|
||||
### `tailscaled` flags are a last resort
|
||||
|
||||
Daemon (`tailscaled`) command-line flags should ideally be added **only** when a
|
||||
setting is truly *process-global* and *immutable after launch*. If something can
|
||||
reasonably be a profile-local preference that is changeable via `set`/`get`, it
|
||||
should be, not a daemon flag.
|
||||
|
||||
Daemon flags are sticky and global; they are the wrong place for anything a user
|
||||
might want to change per-profile or at runtime.
|
||||
|
||||
Daemon flags are also, in practice, **almost Linux-only**. Most GUI platforms
|
||||
(for example macOS, Windows, and mobile) make it very difficult for a user to
|
||||
adjust how the tailscale backend (e.g. `tailscaled`) is launched, so a setting
|
||||
that lives only in a daemon flag is effectively unreachable for most of our
|
||||
users. This is another reason to prefer profile-local preferences via
|
||||
`set`/`get`, which work uniformly across platforms.
|
||||
|
||||
### Prefer preferences over environment variables
|
||||
|
||||
For both `tailscale` and `tailscaled` configuration surfaces, prefer
|
||||
preferences (`set`/`get`) over environment variables. Environment variables are
|
||||
hard to discover, hard to deprecate, and tend to leak into permanence.
|
||||
|
||||
### `TS_DEBUG` environment knobs
|
||||
|
||||
Add `TS_DEBUG`-style env knobs freely as development work requires them. These
|
||||
are explicitly **for development only**. They are not intended for end users and
|
||||
should not be documented or encouraged for public use.
|
||||
|
||||
A cautionary note: we already have cases of debug env knobs that became
|
||||
load-bearing for someone and which we may never be able to remove. That is an
|
||||
undesirable outcome. Keeping these clearly internal and undocumented is what
|
||||
preserves our freedom to change or delete them.
|
||||
|
||||
## Output and streams
|
||||
|
||||
### `--json` and stable machine output
|
||||
|
||||
Commands should offer a **stable** JSON format behind a `--json` flag. Once a
|
||||
feature is generally available (GA), that JSON format is maintained in a
|
||||
backward-compatible way as much as possible, in keeping with Tailscale's broad
|
||||
compatibility policy.
|
||||
|
||||
Backward compatibility here means existing fields keep their meaning. **Adding
|
||||
new fields is allowed** and expected; consumers must tolerate unknown fields.
|
||||
|
||||
### Human-readable (non-JSON) output
|
||||
|
||||
Non-JSON command output is the default and should be optimized, first and
|
||||
foremost, for *easy reading by sighted humans*, and, where possible, to *read
|
||||
well with a screen reader*. Concretely:
|
||||
|
||||
* Avoid decorative Unicode output. Prefer plain ASCII.
|
||||
* Convey information in the **text**, not only via color or position. Do not
|
||||
rely on color to communicate meaning (see "Color and decoration" below).
|
||||
* Keep output uncluttered and scannable.
|
||||
|
||||
The `grep`/`awk`-ability of stdout output should be considered and maintained on
|
||||
a **best-effort** basis, but it is **not** a hard compatibility guarantee when
|
||||
readability or usability would suffer substantially as a result.
|
||||
|
||||
In practice this means being conservative with the *shape* of stable output. For
|
||||
example:
|
||||
|
||||
* Adding a **new column** in the middle of already-stable tabular output should
|
||||
only be done after strongly weighing the tradeoffs, and most often should not
|
||||
be done, because it breaks naive positional parsers.
|
||||
* Adding **new in-column behavior** can be done with care. A good example: the
|
||||
idle/connected/DERP/relay status field was extended to describe relays
|
||||
*within the existing column* rather than by adding a new column. That
|
||||
technically introduced new parsable content into the column (so the strictest
|
||||
parsers might notice), but it avoided breaking the common case, and *not*
|
||||
conveying the information would have been too large a usability and
|
||||
readability loss.
|
||||
|
||||
The principle: prefer changes that preserve the common parser and the human
|
||||
reader simultaneously; when forced to choose, protect the human reader, and make
|
||||
machine consumers use `--json`.
|
||||
|
||||
### stdout vs. stderr
|
||||
|
||||
**stderr is the standard *information* stream.** It is not only for errors. It
|
||||
carries all informational, diagnostic, and progress output that is not the
|
||||
direct intended output of the command.
|
||||
|
||||
**stdout** should contain (within reason) the command's *intended* output — the
|
||||
thing the user actually asked for — or nothing. Everything else (status notes,
|
||||
progress, warnings, diagnostics) goes to stderr.
|
||||
|
||||
This separation is what makes the CLI usable in pipelines: a user can pipe
|
||||
stdout into another tool without having diagnostic chatter corrupt the data
|
||||
stream.
|
||||
|
||||
A couple of implications worth calling out:
|
||||
|
||||
* `--help` is *intended* output: when the user asks for help, help text should
|
||||
go to **stdout**.
|
||||
* An *error* about bad flags that happens to print usage as part of a
|
||||
diagnostic message is diagnostic output and should go to **stderr**, not
|
||||
stdout.
|
||||
|
||||
(At the time of writing, our handling of this distinction for help output is
|
||||
an existing bug. New code should follow the correct behavior, and we should
|
||||
fix the existing behavior over time.)
|
||||
|
||||
### TTY-dependent behavior
|
||||
|
||||
Altering behavior based on whether stdout is a TTY is sometimes genuinely
|
||||
helpful (for example, suppressing a spinner when not interactive). Use it
|
||||
sparingly.
|
||||
|
||||
Major changes in behavior or output based on TTY detection are an impediment to
|
||||
use and should not be added arbitrarily. Requiring something like
|
||||
`tailscale foo | less` in order to *discover* a behavior should not become a
|
||||
standard pattern: it is a usability issue that excludes many users, including
|
||||
those using screen readers and automation.
|
||||
|
||||
### Color and decoration
|
||||
|
||||
Support for, and further discussion of, colored or heavily decorated output is
|
||||
**deferred at this time** and should not be added until guidance is clarified.
|
||||
|
||||
The primary concern is accessibility: colored and decorated output is often
|
||||
hostile to screen-reader users, and information encoded only in color is
|
||||
invisible to many people. Until we have clear guidance, keep output plain and
|
||||
ensure all meaning is present in the text itself.
|
||||
|
||||
## Summary checklist
|
||||
|
||||
When adding or changing CLI surface, ask:
|
||||
|
||||
* Tailscalars can raise a thread in #cli
|
||||
* Could this be a profile-local preference via `set`/`get` instead of a new
|
||||
top-level command, a `up` flag, a daemon flag, or an env var? Usually it
|
||||
should be.
|
||||
* Does it fit a `tailscale <noun> <verb>` shape without being awkward?
|
||||
* Is the noun singular (unless that's strongly unnatural)?
|
||||
* Does it provide stable `--json` output, and is the human output plain,
|
||||
screen-reader-friendly, and free of color-only meaning?
|
||||
* Does intended output go to stdout and everything else to stderr?
|
||||
* Are you extending an existing surface in its spirit, rather than growing
|
||||
complexity arbitrarily?
|
||||
* If it's a debug command, is it clearly best-effort and not silently becoming
|
||||
a stable dependency?
|
||||
Reference in New Issue
Block a user