Documentation
¶
Overview ¶
Package cli provides argument-dispatch glue for the ana CLI. It defines the Command interface, an IO struct carrying stdio/env/clock dependencies, and a Group helper that dispatches to named child Commands. The package is pure dispatch logic — it has no dependency on transport or config.
Index ¶
- Variables
- func ApplyAncestorFlags(ctx context.Context, fs *flag.FlagSet)
- func DashIfEmpty(s string) string
- func DeclareBool(fs *flag.FlagSet, target *bool, name string, def bool, usage string)
- func DeclareInt(fs *flag.FlagSet, target *int, name string, def int, usage string)
- func DeclareString(fs *flag.FlagSet, target *string, name, def, usage string)
- func Dispatch(ctx context.Context, verbs map[string]Command, args []string, stdio IO) error
- func EnumFlag(target *string, allowed []string) flag.Value
- func ExitCode(err error) int
- func FirstLine(s string) string
- func FlagWasSet(fs *flag.FlagSet, name string) bool
- func IntListFlag(target *[]int, sep string) flag.Value
- func IsHelpArg(s string) bool
- func NewFlagSet(name string) *flag.FlagSet
- func NewTableWriter(w io.Writer) *tabwriter.Writer
- func ParseFlags(fs *flag.FlagSet, args []string) error
- func ReadPassword(r io.Reader) (string, error)
- func ReadToken(r io.Reader, tokenStdin bool) (string, error)
- func RedactToken(token string) string
- func Remarshal(src, dst any) error
- func RenderOutput[T any](w io.Writer, raw map[string]any, jsonFlag bool, typed *T, ...) error
- func RenderTwoCol(w io.Writer, m map[string]any) error
- func RequireFlags(fs *flag.FlagSet, verb string, names ...string) error
- func RequireIntID(verb string, args []string) (int, error)
- func RequireStringID(verb string, args []string) (string, error)
- func RootHelp(w io.Writer, verbs map[string]Command)
- func SinceFlag(target *time.Time, now func() time.Time) flag.Value
- func UsageErrf(format string, a ...any) error
- func WithAncestorFlags(ctx context.Context, reg func(*flag.FlagSet)) context.Context
- func WithGlobal(ctx context.Context, g Global) context.Context
- func WriteJSON(w io.Writer, v any) error
- type Command
- type Flagger
- type Global
- type Group
- type IO
- type Token
Constants ¶
This section is empty.
Variables ¶
var ErrHelp = errors.New("help")
ErrHelp marks an explicit help request (`--help`, `-h`, or `help`). Dispatch returns this after printing help text. ExitCode maps it to 0 because the user asked for help and got it — that is success, not a usage error.
var ErrReported = errors.New("reported")
ErrReported marks errors whose diagnostic text has already been written to stderr by the callee. main()'s fallback stderr print skips these to avoid double-reporting. Wrap with errors.Join(err, ErrReported) after emitting the diagnostic yourself.
var ErrUsage = errors.New("usage")
ErrUsage marks a usage-related failure — missing arg, unknown command, malformed flag. Commands that want the root to exit with code 1 should return an error that wraps ErrUsage via %w.
Functions ¶
func ApplyAncestorFlags ¶ added in v0.3.0
ApplyAncestorFlags runs every registered ancestor registrar on fs in the order they were appended (outermost first). Leaves call this AFTER declaring their own flags so leaf declarations populate fs first and each ancestor registrar can Lookup-guard its own additions — stdlib flag.FlagSet panics on duplicate declarations, and this ordering makes "leaf wins" fall out naturally.
Callers that build ancestor registrars should wrap StringVar/BoolVar in the DeclareString / DeclareBool helpers (or equivalent Lookup guards) so they're safe when the leaf declared the same name.
func DashIfEmpty ¶
DashIfEmpty renders an em-dash placeholder for empty cells so tabwriter keeps table columns aligned. Every verb's list/show table uses this fallback.
func DeclareBool ¶ added in v0.3.0
DeclareBool is the bool counterpart to DeclareString. Same guard against panicking on duplicate names.
func DeclareInt ¶ added in v0.3.0
DeclareInt is the int counterpart to DeclareString. Same guard against panicking on duplicate names — used by Group.Flags closures that want to inherit-declare an integer flag (e.g. the Databricks Group's shared `--port`) without clobbering a leaf that already declared the same name.
func DeclareString ¶ added in v0.3.0
DeclareString is a Lookup-guarded wrapper around fs.StringVar. Ancestor Group.Flags closures should use this (rather than raw StringVar) so a leaf that already declared the same name isn't clobbered by a duplicate declaration — the stdlib flag package panics in that case.
func Dispatch ¶
Dispatch is the root entry point. It parses global flags, stashes them in ctx, then routes to the matching verb. An explicit help token returns ErrHelp (exit 0); an empty verb or parse error returns ErrUsage (exit 1).
func EnumFlag ¶
EnumFlag returns a flag.Value that validates against a fixed allow-list at parse time: unknown values yield a UsageErr before the verb body runs, so downstream code can trust *target. The allowed values show up in the Set error, which fs.Parse wraps as `invalid value "X" for flag -Y: allowed: ...`. Pairs naturally with RequireFlags when the flag is also mandatory.
func ExitCode ¶
ExitCode maps a returned error to the process exit code.
nil -> 0 ErrHelp -> 0 (user asked for help; help was shown) ErrUsage -> 1 (including wrapped) authError -> 3 (including wrapped; IsAuthError must return true) otherwise -> 2
func FirstLine ¶
FirstLine returns the first line of s (without the newline). Exported so verb packages that render streaming/multi-line payloads one-row-per-frame can reuse the same definition the help renderer uses.
func FlagWasSet ¶
FlagWasSet reports whether fs saw name as an explicit argument. Partial- update verbs use it to distinguish "user left this alone" (keep server's current value) from "user passed the zero value on purpose". Uses the stdlib-documented `fs.Visit` idiom, which only traverses flags that were actually set; an unknown name is reported as not-set rather than panicking.
func IntListFlag ¶
IntListFlag returns a flag.Value that parses a separator-delimited list of ints into *target (whitespace around each entry is tolerated). Empty input and non-integer tokens produce a UsageErr; on success *target is guaranteed non-empty.
func NewFlagSet ¶
NewFlagSet returns a *flag.FlagSet configured the way every leaf verb wants it: ContinueOnError so we can wrap parse failures as usage errors, and output silenced (io.Discard) so each command's own Help() is the sole source of usage text. Currently duplicated in every verb package — Phases 1–10 of the shared-cli-kit refactor switch them over to this helper.
func NewTableWriter ¶
NewTableWriter returns a *tabwriter.Writer configured the way every verb's list/show table wants it: no min-width, no tab-stop, two-space padding. Callers must call Flush() (or defer it) once they finish writing rows.
func ParseFlags ¶
ParseFlags parses args into fs, tolerating positional arguments interleaved with flags. Go's stdlib FlagSet.Parse stops at the first non-flag token, which silently drops any flags that follow — so `cmd <id> --flag v` would parse the positional but ignore --flag. This helper iterates: parse, collect a non-flag token, parse the remainder, repeat. A final Parse with a "--" separator then re-seeds fs.Args() with the collected positionals so callers can read them through the normal flag API.
On any underlying Parse failure the error is wrapped with ErrUsage so the root dispatcher maps it to exit code 1.
func ReadPassword ¶
ReadPassword reads a single password line from r and returns the bytes exactly as supplied, stripping only the trailing line terminator (\n or \r\n). Unlike ReadToken, no whitespace trimming is applied: a password may legitimately contain leading or trailing spaces or tabs, and silently mutating it would cause hard-to-diagnose auth failures. The same JWT-sized scanner buffer is used so very long credentials round-trip intact.
func ReadToken ¶
ReadToken consumes stdin and returns a trimmed token. With tokenStdin=true the whole stream is consumed; otherwise a single newline-terminated line is read. Whitespace is trimmed in both modes so common pipe quirks (trailing newline from `echo`) don't poison the saved value. Centralised here because auth login and profile add share this exact behaviour.
func RedactToken ¶
RedactToken returns a user-facing display for a bearer token or API key. Empty tokens render as "(unset)" so operators can see the slot needs a login; any other value shows a fixed mask plus the last four characters so two tokens can be disambiguated at a glance without leaking them. Tokens shorter than four bytes (never expected from a real API key but exercised defensively) still get fully masked — echoing the whole value in the "last 4:" slot would defeat the redaction for short/malformed/test tokens.
func Remarshal ¶
Remarshal round-trips src through JSON into dst, letting commands have one Unary decode into map[string]any and still derive a typed view for table rendering without a second RPC.
func RenderOutput ¶
func RenderOutput[T any]( w io.Writer, raw map[string]any, jsonFlag bool, typed *T, render func(w io.Writer, typed *T) error, ) error
RenderOutput picks the JSON-vs-table branch every `--json` verb repeats. On jsonFlag the raw map is dumped via WriteJSON; otherwise raw is Remarshal'd into typed and handed to render. The remarshal error is wrapped with "decode response" so verbs preserve their historical error string under a `verb: %w` wrap at the call site.
func RenderTwoCol ¶
RenderTwoCol prints top-level scalar fields then any nested map fields (e.g. postgresMetadata) as an indented sub-block. Keys are sorted so the output is stable across runs for snapshot-style tests. Output is byte-identical to the pre-refactor connector/get.go::renderTwoCol.
func RequireFlags ¶
RequireFlags returns a UsageErr listing any flag from names that was not explicitly set on fs. Built on FlagWasSet, so "explicit zero value" (e.g. --port 0) still counts as supplied — callers that care about the value's content validate it themselves. The verb prefix is prepended so the error reads `verb: missing required flags: --a, --b`.
func RequireIntID ¶
RequireIntID extracts an integer positional <id> from args[0]. Missing or empty input returns the same usage error as RequireStringID; a non-numeric input returns a usage error that quotes the underlying strconv error. Behaviour matches the pre-refactor connector.atoiID with the addition of the args-slice indirection.
func RequireStringID ¶
RequireStringID extracts a non-empty positional <id> from args[0]. An empty or whitespace-only first arg (or a missing arg entirely) is rejected with a usage error. The strictest pre-refactor variant (dashboard's strings.TrimSpace check) is used here so all verb packages converge on identical behaviour after migration.
func RootHelp ¶
RootHelp writes a sorted listing of the top-level verbs to w, each followed by the first line of its own Help(), then the canonical Global Flags block.
func SinceFlag ¶
SinceFlag returns a flag.Value that accepts either a non-negative duration (e.g. "1h", "24h") — interpreted as `now() - dur` — or an absolute RFC3339 timestamp, and stores the UTC-normalised result in *target. The injected now lets tests fix the clock so --since assertions are deterministic. Negative durations are rejected rather than silently producing a future timestamp, which would mask operator typos.
func UsageErrf ¶
UsageErrf builds a user-facing error that wraps ErrUsage so the root dispatcher maps it to exit code 1 via errors.Is. Use this anywhere a verb detects a missing/invalid arg or other shape problem the caller could fix by re-running with different inputs.
func WithAncestorFlags ¶ added in v0.3.0
WithAncestorFlags appends reg to the ctx-carried slice of ancestor flag registrars and returns the new context. Groups call this during Run so child commands inherit the Group's declared flags; the slice preserves registration order (outermost ancestor first) so leaf tests can reason about precedence by inserting guards.
Per stdlib context.WithValue contract, ctx must not be nil.
func WithGlobal ¶
WithGlobal returns a child context carrying g. Per stdlib convention ctx must be non-nil; a nil parent panics (mirroring context.WithValue).
Types ¶
type Command ¶
Command is the consumer interface implemented by every verb or subcommand. Run receives the args remaining after its own name has been consumed.
type Flagger ¶ added in v0.3.0
Flagger is an optional opt-in for leaf commands whose help should include a flag enumeration that stacks ancestor-declared flags with the leaf's own. Leaves that implement Flags(fs) get an automatic Flags: block appended to their --help output by dispatchChild; leaves that don't implement it keep the current hand-written Help() as their sole source of usage text.
type Global ¶
Global holds the root-level flags that apply to every verb. Command implementations read it from context via GlobalFrom; ParseGlobal produces it from raw argv.
func GlobalFrom ¶
GlobalFrom extracts the Global from ctx, or a zero value if absent. Per stdlib convention ctx must be non-nil; a nil ctx panics (mirroring context.Value semantics).
func ParseGlobal ¶
ParseGlobal parses the global flags at the front of args and returns the resulting Global along with the remaining args (the verb and its args). Parsing stops at the first positional argument or `--`; subcommand flags are left to the subcommand itself.
func StripGlobals ¶ added in v0.3.0
StripGlobals walks args once and splits it into (Global, rest, err). Unlike ParseGlobal, which relies on stdlib flag.FlagSet.Parse and stops at the first positional, StripGlobals consumes known global flags wherever they appear — before, after, or interleaved with the verb path and leaf flags. Everything it does not recognise is passed through to rest in original order so the leaf's FlagSet still handles unknown-flag errors.
Supported forms per known flag in globalFlagRegistry (single- and double- dash spellings are equivalent, matching stdlib `flag.FlagSet.Parse`):
- `-name` / `--name` (bool) or `-name=value` / `--name=value` / `-name value` / `--name value` (takesValue)
A bare `--` terminator stops global consumption: every remaining token is copied verbatim to rest (including the `--` itself), so leaves can still use `--` to force positional interpretation of an arg that looks like a flag.
Duplicate globals follow stdlib semantics (last wins). Unknown flags are left in rest unchanged so the leaf's own FlagSet reports a precise `flag provided but not defined: --xyz` at its verb name.
type Group ¶
Group is a Command that dispatches its first argument to a named child Command. A Group can itself be registered as a child, enabling nested verbs (e.g. `ana chat send ...`).
Flags, if set, declares group-level flags that every descendant leaf inherits via the ctx-carried registrar stack (see WithAncestorFlags in root.go). The closure runs on a leaf's *flag.FlagSet AFTER the leaf has declared its own flags, so use DeclareString / DeclareBool (or an equivalent Lookup guard) to avoid the stdlib flag-redeclaration panic — the guard lets the leaf override a name when it wants to.
func (*Group) Help ¶
Help renders the group's summary (if set) followed by a sorted, two-column listing of child commands and the first line of each child's own Help(). When Flags is set, a trailing "Flags:" block enumerates the group-level flags so `ana <group> --help` surfaces inheritable flags even when the user hasn't drilled into a leaf.
func (*Group) Run ¶
Run dispatches to a child command. With no args or an explicit help flag it prints Help() to stdout and returns ErrHelp (exit 0). An unknown child name writes to stderr and returns ErrUsage (exit 1).
If Flags is non-nil, Run appends it to the ctx-carried ancestor-flag stack before delegating so every descendant leaf can ApplyAncestorFlags and pick up the group's declared flags.
type IO ¶
type IO struct {
Stdin io.Reader
Stdout io.Writer
Stderr io.Writer
Env func(string) string
Now func() time.Time
}
IO carries the ambient dependencies a Command needs: standard streams, an environment accessor, and a clock. Pass this through to subcommands rather than reaching for package globals so tests can inject fakes.
type Token ¶
type Token string
Token wraps a bearer token so any accidental `%s`/`%v`/`%q` on a logger or error renders the redacted form (same mask RedactToken produces). Code that needs the raw value calls Value() — the single explicit escape hatch.
The underlying kind is string, so the type is JSON-transparent (persists and loads as a plain string), comparisons against string literals still work, and tests can write `cli.Token("abcdefgh")` with no extra plumbing.
func (Token) Format ¶
Format also intercepts `%q`, `%+v`, and `%#v`, which would otherwise bypass String() and dump the raw bytes (fmt special-cases string kinds for %q/%#v specifically). Returning the same redacted form for every verb means no format directive can accidentally leak a token.
func (Token) String ¶
String returns the redacted representation. Triggered by any verb that doesn't override it (`%s`, `%v`, default Sprintln, error chains, etc.).
func (Token) Value ¶
Value returns the raw token string. This is the intended escape hatch for the two legitimate consumers: the transport layer building an Authorization header, and the auth-keys verb that prints a freshly minted key once. Adding new callers is a design smell — prefer passing the Token through.