TL;DR
- Favor subcommands with consistent verbs/nouns, predictable flags, and excellent
--help. - Default to safe actions, support
--yes/--force, and prompt on destructive ops. - Output human‑readable by default; offer
--json(and--quiet) for scripts. No colors/progress when not TTY. - Respect config precedence: flags > env > config file > defaults. Provide completions for bash/zsh/fish/powershell.
- Return clear errors with suggestions (did‑you‑mean), proper exit codes, and actionable next steps.
- Ship a single static binary (or simple
pipx/npm i -g), supply checksums/signatures, and self‑update with--version/updatebuilt‑ins.
1) Command model (predictability first)
Structure: tool <noun> <verb> or tool <verb> <noun> — pick one and stick to it.
Examples:
# noun-first
tool project create
tool auth login
tool user list
# or verb-first
tool create project
tool login
tool list users
Rules of thumb
- Subcommand names are short, verbs are imperative.
- Flags are kebab‑case, long form preferred (
--access-token). Provide short aliases only for frequent flags (-o,-q,-v). - Every subcommand supports:
--help,--json,--quiet,--verbose/-v,--no-color.
2) Help that actually helps
$ tool project create --help
Create a new project.
USAGE
tool project create [NAME] [flags]
EXAMPLES
# create interactively
tool project create
# non-interactive, specific region and plan
tool project create myapp --region eu-west-1 --plan hobby --yes
# output machine-readable JSON
tool project create myapp --json
FLAGS
-r, --region string Region slug (e.g., us-east-1)
--plan string Plan tier (hobby|pro|enterprise) (default "hobby")
--tags stringArray Add tags (repeat: --tags a --tags b)
-y, --yes Skip confirmation prompts
-q, --quiet Suppress human output (errors only)
-o, --output string Output format (table|json) (default "table")
--no-color Disable colors
--config string Path to config file (default: ~/.tool/config.yaml)
- Put Examples right under USAGE.
- Group flags by category and show defaults.
- For
--json, document the schema (fields, types, stability).
3) Output modes (humans vs machines)
- Default (human): aligned table, subtle colors, concise messages.
--json/-o json: deterministic field order, no ANSI, no prose, one object/line for streams.--quiet: only errors to stderr; success prints IDs only (or nothing).- Non‑TTY: auto‑disable colors/spinners; keep progress on stderr.
Go (Cobra)
type GlobalFlags struct {
JSON bool; Quiet bool; NoColor bool; Output string
}
var gf GlobalFlags
func init() {
rootCmd.PersistentFlags().BoolVar(&gf.JSON, "json", false, "Output JSON")
rootCmd.PersistentFlags().BoolVarP(&gf.Quiet, "quiet", "q", false, "Quiet mode")
rootCmd.PersistentFlags().BoolVar(&gf.NoColor, "no-color", false, "Disable colors")
rootCmd.PersistentFlags().StringVarP(&gf.Output, "output", "o", "table", "Output format (table|json)")
}
Node (commander)
program
.option("--json", "output JSON")
.option("-q, --quiet", "quiet mode")
.option("--no-color", "disable colors");
Python (Typer)
import typer, sys, json, os
app = typer.Typer(add_completion=False)
def is_tty() -> bool:
return sys.stdout.isatty()
@app.callback()
def common(json_: bool = typer.Option(False, "--json"), quiet: bool = typer.Option(False, "--quiet"),
no_color: bool = typer.Option(False, "--no-color")):
if not is_tty(): os.environ["NO_COLOR"] = "1"
4) Config & precedence
Sources (highest → lowest): flags → env → config file → defaults.
- Config file:
~/.tool/config.yaml(or TOML/JSON). Support profiles:[default],[staging]. - Env vars:
TOOL_REGION,TOOL_TOKEN. Document them. - Support
--configand--profile. Merge with clear precedence, and print effective config ontool config inspect.
Example (YAML)
profile: default
profiles:
default:
region: us-east-1
staging:
region: eu-west-1
5) Prompts, safety, and interactivity
- Default to safe: require
--yesfor destructive ops; show diff/preview on changes (--dry-run). - Prompts must timeout or respect
--no-inputfor CI. - Always support stdin/stdout: use
-to read/write files (tool secrets set --from-file -).
Node prompt (enquirer)
import { Confirm } from "enquirer";
if (!argv.yes) {
const ok = await new Confirm({ name: "proceed", message: "Delete ALL resources?" }).run();
if (!ok) process.exit(0);
}
6) Colors, TTY, and progress
- Detect TTY: disable colors/spinners when piped; respect NO_COLOR.
- Send progress to stderr; results to stdout.
- Use spinners for < a few seconds; progress bars for longer tasks;
--no-progressflag to disable.
isTTY := term.IsTerminal(int(os.Stdout.Fd()))
if !isTTY || gf.NoColor || gf.JSON { color.NoColor = true }
7) Errors & exit codes
- Errors are one line + hint. For
--json, return a machine‑readable error object to stderr, keep exit code ≠ 0.
Suggested codes
0success1runtime/unknown error2usage/validation error3not found / missing resource4unauthorized/forbidden5conflict / precondition failed130terminated by Ctrl‑C
Did‑you‑mean (subcommands/flags)
// Using levenshtein distance to suggest "projects" when user typed "projectz"
8) JSON that doesn’t hurt
- Stable field names, documented schema; ISO 8601 timestamps; lowercase_with_underscores or camelCase, pick one.
- Support
--jq <expr>or--query(JMESPath‑like) for client‑side filtering. - Stream lists as NDJSON (one JSON per line) for large outputs:
--jsonl.
{"id":"p_123","name":"myapp","region":"us-east-1","created_at":"2025-10-05T11:22:33Z"}
9) Auth & credentials
tool auth loginopens a browser for OAuth device flow; fallback to--tokenfor CI.- Store tokens in OS keychain when possible; else in
~/.tool/credentials.jsonwith0600. - Support
tool whoami,tool auth logout,tool auth refresh.
10) Completions & man pages
- Generate shell completions and ship an
install-completionscommand. - Provide rich descriptions per flag/subcommand so completion menus are useful.
- Include a man page or
--help-manthat prints a long help.
Cobra
rootCmd.CompletionOptions.DisableDefaultCmd = false
rootCmd.PersistentFlags().Bool("install-completions", false, "Install shell completions")
11) Distribution & updates
- Prefer a single static binary (macOS, Linux, Windows, arm64/amd64). Provide Homebrew, Scoop, winget, apt/rpm, or
npx/pipx. - Publish checksums and sign releases.
- Provide
tool update(checks GitHub Releases) andtool --versionwith commit/date.
12) Testing & UX checks
- Golden tests for command outputs; run with
NO_COLOR=1. - Contract tests for
--jsonschema. - Smoke tests in CI on macOS/Linux/Windows shells.
- Dogfood with record‑replay tests to minimize flakiness.
13) Snippets you can paste
Go (Cobra): root with JSON/quiet & non‑TTY guard
var rootCmd = &cobra.Command{
Use: "tool",
Short: "Tool does useful things",
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
if !term.IsTerminal(int(os.Stdout.Fd())) { color.NoColor = true }
if gf.JSON { color.NoColor = true; gf.Quiet = true }
return nil
},
}
Node (commander): consistent errors & exit codes
program.exitOverride((err) => {
if (err.code === "commander.unknownOption") {
console.error("Unknown option. Try --help");
process.exit(2);
}
console.error(err.message);
process.exit(1);
});
Python (Typer): --json toggle
@app.command()
def projects_list(json_: bool = typer.Option(False, "--json")):
projects = [{"id":"p1","name":"demo"}]
if json_: print(json.dumps({"items": projects})); return
for p in projects: print(f"{p['id']} {p['name']}")
Pitfalls & fast fixes
| Pitfall | Why it hurts | Fix |
|---|---|---|
| Chatty output in scripts | Breaks automation | Add --json/--quiet, disable colors when not TTY |
| Confusing flags | Support tickets | Consistent naming, aliases, and great help |
| Destructive defaults | Data loss | Default to safe; --yes required |
| Unstable JSON | CI failures | Versioned schema; contract tests |
| No completions | Slower users | Ship completions with descriptions |
| Mixed stdout/stderr | Piping pain | Results stdout, progress/errors stderr |
| Inconsistent exit codes | Poor scripting ergonomics | Map errors → codes; document them |
Quick checklist
- [ ] Consistent subcommands + flags with excellent help/Examples.
- [ ] Dual output: human (table) & machine (
--json,--jsonl). - [ ] TTY‑aware colors/progress; results on stdout, errors on stderr.
- [ ] Clear config precedence and
config inspect. - [ ] Safe defaults (
--yes,--dry-run), prompts respect--no-input. - [ ] Proper exit codes, did‑you‑mean suggestions.
- [ ] Shell completions, man pages, and self‑update.
- [ ] Cross‑platform installers and signed releases.
- [ ] Golden tests; contract tests for JSON.
One‑minute adoption plan
- Add
--json,--quiet,--no-color, and TTY detection. - Rewrite
--helpwith USAGE/EXAMPLES/FLAGS and show defaults. - Standardize config precedence (flags > env > file > defaults) and add
tool config inspect. - Wire safe prompts (
--yes,--dry-run) and send progress to stderr. - Generate completions and document install + update flows.