Documentation
¶
Overview ¶
Package rotini is the runtime for rotini-built command-line programs. A CLI is declared in a spec file, generated into Go with the rotini tool, and run on this runtime, which does nothing the spec did not declare; everything beyond dispatch is an explicit, opt-in service.
One module serves two roles at one version:
- Tool: `go get -tool github.com/go-rotini/rotini/cmd/rotini@latest` installs the codegen binary (`go tool rotini init` / `generate` / `validate`), which compiles a spec into a per-CLI framework file plus one editable handler file per command.
- Library: `go get github.com/go-rotini/rotini` provides this package, which generated code imports and handlers are written against.
The companion CLI under cmd/rotini is built with rotini; docs/assets/examples/ holds a spec and a conf that use every key.
Declare ¶
A CLI is a .rotini.spec.yaml (or json, jsonc, toml) document declaring the command tree and every input channel — argv flags and positional arguments, the value sentinels (from: file's @path, from: stdin's -), environment variables and flags' env fallbacks, configuration files, a typed stdin payload, declared defaults — plus documentation, shell completion and plugin dispatch.
`rotini validate` checks the spec: the JSON Schema rejects what it can express and lint rules reject the rest, each problem reported at file:line:col. Every schema-accepted key has a consumer.
Generate ¶
`rotini generate` compiles the spec into a framework file — the Definition literal, typed per-command input structs, embedded help, man and markdown pages, and completion scripts — plus one handler stub per command, created once and then owned by the program. `rotini init` scaffolds a working CLI: a spec declaring -h/--help, -v/--version and the help and version commands, a conf with the help feature on and the other three off, an entrypoint, and a stub per command wired to the generated pages and services. All of it may be edited or deleted.
Composition ¶
A command can be composed at any node, so a CLI is assembled from specs as its tree is assembled from commands. Five modes determine where a command's spec and handler code come from:
- Standalone — the command's own spec node and generated stub. The default.
- Inline with handler passthrough — the command's own spec node, with typed inputs generated locally and handler code from a package named by handler: { import: …, convention: … }. Codegen emits the delegating call and seeds no stub. It does not cascade: an inline sub-command without its own handler gets a stub.
- Local composition — a "$ref" to a sibling spec in the same module. The child's tree merges in, the parent winning on an overlapping key, and each composed command delegates to the child's generated package.
- Module composition — a "$ref" to mod://<module>@<version>/<path>, resolved through the Go module cache and pinned by go.sum, delegating to that module's generated package.
- Declared plugin — a sibling binary <program>-<name>, dispatched at run time rather than composed at codegen; a dispatch failure is a *PluginError. Plugin discovery dispatches an unmatched token to <prefix><token> the same way.
A composed child is generated on its own, so its typed inputs start at its own root and do not include a parent's cascading flags. The parent passes those as a Dependency: the child's package declares it, and the parent, which imports the child, collects its own inputs in CascadingPreRun and sets it. Collecting there validates only the parent's own inputs, so a descendant's --help and required inputs are unaffected:
// package child
var Kubeconfig = rotini.NewDependency[string]("child.kubeconfig")
// package parent, in its CascadingPreRun
in, err := rtx.Inputs[ParentInputs]()
if err != nil { rtx.HaltWith(err); return }
rtx.SetDependency(child.Kubeconfig, in.Parent.Flags.Kubeconfig)
`rotini validate` follows refs, validates each locally composed spec as its own document and checks the assembled tree for collisions, so a duplicate name, a cycle, a missing ref or an error inside a child is caught before codegen. Generation is hermetic: a local ref reads the filesystem, a mod:// ref reads the module cache, and a git:: or raw https:// ref is refused.
The runtime ¶
The generated entrypoint builds a Program with NewProgram and calls Program.Execute, which resolves the invoked command from argv, runs its Handler hooks, and exits. Each invocation carries a Context: the argv, the resolved chain, the program's streams and its dependencies. A handler stops with Context.HaltWith, Context.Halt, Context.HaltWithCode or Context.Exit; recorded errors, recovered panics and detected faults are reported once, after teardown, by the outcome reporter.
The runtime resolves only which command was invoked. Flags and arguments are parsed and validated when a handler calls Context.Inputs (or the per-channel Context.ArgvInputs, Context.EnvInputs, Context.FileInputs and Context.StdinInputs), which the generated stubs do first. A handler that never calls it reads the raw Context.Argv and gets no validation.
The runtime's only built-in behaviors are a default SIGINT/SIGTERM trap (see Program.WithoutSignalHandling and Program.WithSignals), the hidden __complete entry the generated shell scripts call, and os.Exit as the default exit action (see Program.WithExit).
Input values ¶
Input types are declared in the spec and parsed the same way on every channel an input reads: argv, a flag's environment and configuration fallbacks, and env and config inputs.
- Scalars parse into the generated field's type: string; bool (also boolean) as true/false, yes/no, on/off, y/n, t/f or 1/0 in any case; the integers int, int8, int16, int32, int64 and rune (also integer); the unsigned integers uint, uint8, uint16, uint32, uint64 and byte; the floats float32 and float64 (also number); and any, which holds the raw text. complex64, complex128 and uintptr have no parser and are refused.
- Value types: duration with Go's units plus d and w (7d, 2w3d); time and datetime (RFC 3339) and date (2026-09-29), each taking `layout:` for another format; url, email, timezone, mac, ip, cidr and hostport; bytesize (ByteSize: 512Mi, 10MB), hexbytes (HexBytes) and base64bytes (Base64Bytes).
- Path checks: existingfile and existingdir are strings checked at parse time to exist and to be that kind of entry.
- Shapes: count (flags only) counts occurrences (-vvv is 3); a list ([]T, or array with `items:`) and a map (map[string]T, or map and object) repeat.
- Any other type parses through its own encoding.TextUnmarshaler, with `import:` naming its package.
- A list or map flag repeats (--tag a --tag b, --label k=v); with `separator:` one value also splits (--tag a,b), CSV-style, before validation sees the items.
- `implicit_value:` makes a flag's value optional: bare --color takes it, --color=never sets one, and the next word is never consumed.
- An `enum` matches exactly, or regardless of case with `ignore_case:`, binding the declared spelling.
- A flag whose schema is a named object ($ref: '#/schemas/DB') takes a structured value: JSON (--db '{"host":"h","port":5}'), key=value pairs (--db host=h,port=5, dotted keys nesting, quotes keeping a comma), a JSON or YAML file with `from: [file]` (--db @db.yaml), or one field per flag (--db.host=h). Occurrences merge in order, a later key winning; a list of objects takes one element per occurrence. Every form is validated against the named schema. Where the schema does not type a value (inside a free-form map, or in a `dotted_keys:` map), key=value text is read as JSON would read it: true, false, null and JSON numbers are typed, anything else is text, so `-p spec.replicas=5` and `-p '{"spec":{"replicas":5}}'` store the same number.
Slices at the boundary ¶
- A slice rotini returns is a copy. Context.CommandChain and every Outcome channel return their own, so modifying one does not affect the run. Context.Argv is the exception: it is the live argv, for a handler's own parser.
- A slice passed in is retained, not copied. Program.WithArgs, Program.WithSignals and the slices inside an InputSettings are held by reference, so modifying the caller's slice afterwards changes the program.
Outcomes ¶
A handler does not print its outcome; it records it, and one reporter (Program.WithReporter) receives all five channels as an Outcome once the lifecycle finishes:
- Context.RecordInfo — neutral informational output.
- Context.RecordSuccess — what succeeded.
- Context.RecordWarning — non-fatal: a deprecation, a fallback. Never changes the code.
- Context.RecordError — the end user's failures: a bad input, a domain error.
- Recovered panics and rotini-detected faults, captured by the runtime; there is no record call for them.
Recording does not stop the run. The reporter is called only when some channel is non-empty, so a run that records nothing exits silently with code 0 unless a handler set one.
A handler stops in one of four ways:
- Context.HaltWith records an error and stops forward progress, setting no code. It is the usual way for a hook to fail.
- Context.Halt stops forward progress without recording anything or setting a code.
- Context.HaltWithCode stops and sets a code, when the code carries meaning: a filter reporting "no match" as 1, a wrapper passing through a child's status.
- Context.Exit stops and sets a code, skipping pending teardown.
A hook that records a failure without stopping lets the next hook run, often repeating the same failure.
The default reporter prints infos, warnings, errors, panics, then successes (infos and successes to stdout, the rest to stderr), then applies the exit floor: a recorded error or fault exits 1 unless a handler already set a non-zero code. A custom reporter owns the exit code entirely.
Every failure class is errors.Is-able against the ErrUsage or ErrInternal sentinel, so CategoryOf classifies it (except a plugin timeout, which is CategoryNone), and errors.As-able to a typed value with structured fields. rotini's own messages expose no recon, decode or OS internals and no secret values:
- *ParseError — the argv channel. ParseError.Kind identifies the failure; Token and Candidates are what a Suggestor turns into "did you mean".
- *InputError — the env, config, stdin and flag-fallback channels, carrying the channel, the input and a message, with the recon cause reachable via errors.As.
- *PluginError — a plugin dispatch, recorded as an error. A missing discovered plugin is a usage error; a missing declared plugin, or a plugin that cannot start, is internal; a timeout is neither.
- *DependencyError and *PanicError arrive as panics, as does a *WiringError from the program's wiring. Context.Inputs returns one *WiringError as an error instead: config inputs requested on a program built without an InputSettings.
rotini prints no "did you mean" suggestions and no help on error; a program that wants either writes its own reporter.
Sharing dependencies between handlers ¶
A value every handler needs (a store, a client, a logger) is a dependency, named by a typed Dependency handle so its name and type cannot drift apart:
// declared once, beside the thing it names
var Store = rotini.NewDependency[*store.Store]("tasks.store")
// main.go — the value's type is checked where it is supplied
cmd.Program.WithDependency(tasks.Store, store.New()).Execute()
// or several at once ([WithDependency] and [Program.With])
cmd.Program.
With(
rotini.WithDependency(tasks.Store, store.New()),
rotini.WithDependency(tasks.Client, client.New()),
).
WithVersion(version).
Execute()
// any handler
s := rtx.MustGetDependency(tasks.Store)
Context.GetDependency reports a miss instead of routing it to the reporter, and Context.SetDependency sets one for the rest of this run only. Context.Command is the command whose hook is running, Context.CommandPath its canonical path ("tasks add"), and Context.CommandChain the full chain with the tokens the user typed.
Opt-in services ¶
Everything else is a function or type a handler calls when it needs it, with nothing to register. Dependencies (Program.WithDependency, Context.GetDependency, Context.MustGetDependency) hold only the program's own values.
rotini's own seams are typed options, not dependencies: Program.WithInputSettings, Program.WithInputReader, Program.WithParser, Program.WithVersion and Program.WithHelp set them; Context.Parser, Context.Version and Context.Help read them. A dependency name or type can therefore never shadow one.
Context.Inputs reads every declared channel, reconciled and validated, into the command's generated inputs type:
inputs, err := rtx.Inputs[DeployInputs]()
Context.InputsWithReport adds the provenance InputReport. Both use the InputSettings the generated NewProgram supplies via Program.WithInputSettings.
Parser parses and validates the argv channel alone (GNU/POSIX grammar, typed coercion, enum and constraint checks), failing with a *ParseError. InputReader is the engine behind Context.Inputs, for callers who hold the settings explicitly. Neither needs to be supplied; Program.WithParser replaces only the parser Context.Parser returns.
Deprecations reports the deprecated aliases and identifiers this invocation used.
Context.WriteOutput writes a command's output in the format the handler passes: json, yaml and toml by rotini, any other by the handler's renderer. Context.WriteOutputItem writes one item of a stream. Context.CheckOutput, Program.WithOutputChecks and DecodeOutput check values against the declared shape, and StructuredReporter reports a run's outcome as JSON lines on stderr when the program's rule marks the run structured.
The per-channel methods (Context.ArgvInputs, Context.EnvInputs, Context.FileInputs, Context.StdinInputs, Context.DefaultInputs, merged by MergeInputs or MergeInputsWithReport) read channels one at a time, for programs with custom precedence.
Suggestor turns a *ParseError's rejected token and candidates into "did you mean" suggestions; Suggestor.For does it in one call.
Program.WithResolver and Program.WithLifecycle replace the resolve and run phases. FlagValueCompleter and ArgValueCompleter supply dynamic completion, and Program.Complete answers it in a host's CompletionFormat: PluginCompletion for a plugin host such as kubectl completing a rotini plugin. Program.WithCompletion makes __complete answer in that format, for hosts that call it (Docker, Flux).
Batteries ¶
rotini does nothing on import, starts no background goroutine and touches no terminal. It ships no styler, table, spinner, prompt, pager, terminal probe or process runner; use golang.org/x/term, os/exec and similar libraries. The one text helper it keeps is needed by its own generated pages:
- StripANSI removes ANSI escape sequences, making a styled string safe for a man page, a markdown page or a completion description.
Program shapes ¶
An interactive loop, a daemon or a server answering a peer runs the same program by calling Program.Run repeatedly. Each dispatch gets a fresh Context, so nothing carries over between invocations, while dependencies registered up front reach all of them. Run is safe for concurrent use once configuration is complete; the handlers value and the program's streams remain shared, so a concurrent host synchronizes those. See Program.Run.
rotini ships no loop. A host calls Program.RunContext once per line or request; supplying the context leaves signal handling to the host.
What rotini does not ship ¶
Some functionality lives in sibling modules, imported directly:
- watching files — go-rotini/fs, fs.NewWatcher
- a single-instance lock — go-rotini/fs, fs.PIDLock
- caching in a long-running program — go-rotini/memcache
Styling, tables, spinners, prompts, forms and paging are left to the program and to libraries built for them. rotini turns a spec into a parsed, validated, dispatched invocation and hands the handler a Context; what the handler prints, and how, is the program's.
Example (Unopinionated) ¶
Example_unopinionated runs a program without the input helpers: WithArgs supplies argv, WithExit captures the code, and WithoutSignalHandling installs no signal trap.
def := Definition{
Name: "greet", Handler: "Main",
Flags: []FlagDef{{Name: "name", Identifiers: []string{"--name"}, Type: "string"}},
}
os.Setenv("GREETING", "hi")
defer os.Unsetenv("GREETING")
code := -1
NewProgram(def, unopinionatedApp{}).
WithArgs([]string{"--name", "ada"}).
WithStdout(os.Stdout).
WithoutSignalHandling().
WithExit(func(c int) { code = c }).
Execute()
fmt.Println("exit:", code)
Output: hi, ada! (command "greet") exit: 0
Index ¶
- Variables
- func AsCommand(i int, hook func(context.Context, *Context)) func(context.Context, *Context)
- func DecodeOutput[T any](p *Program, data []byte, format string) (T, error)
- func ExitCause(code int) error
- func InternalError(err error) error
- func MergeInputs[T any](layers ...InputLayer[T]) T
- func PluginCompletion(w io.Writer, result CompletionResult) error
- func Ptr[T any](v T) *T
- func StripANSI(text string) string
- func UsageError(err error) error
- type ArgDef
- type ArgValueCompleter
- type Base64Bytes
- type ByteSize
- type Category
- type Command
- type CommandDef
- type Completion
- type CompletionCandidate
- type CompletionFormat
- type CompletionResult
- type ConfigFile
- type Constraints
- type Context
- func (rtx *Context) ArgvInputs[T any]() (InputLayer[T], error)
- func (rtx *Context) CheckOutput(v any) error
- func (rtx *Context) Command() Command
- func (rtx *Context) CommandChain() []Command
- func (rtx *Context) CommandPath() string
- func (rtx *Context) DefaultInputs[T any]() (InputLayer[T], error)
- func (rtx *Context) EnvInputs[T any]() (InputLayer[T], error)
- func (rtx *Context) Exit(code int)
- func (rtx *Context) Failed() bool
- func (rtx *Context) FileInputs[T any]() (InputLayer[T], error)
- func (rtx *Context) GetDependency[T any](dep Dependency[T]) (T, bool)
- func (rtx *Context) Halt()
- func (rtx *Context) HaltWith(err error)
- func (rtx *Context) HaltWithCode(code int)
- func (rtx *Context) Help() string
- func (rtx *Context) Inputs[T any]() (T, error)
- func (rtx *Context) InputsWithReport[T any]() (T, InputReport, error)
- func (rtx *Context) MustGetDependency[T any](dep Dependency[T]) T
- func (rtx *Context) Parser() *Parser
- func (rtx *Context) RecordError(err error)
- func (rtx *Context) RecordInfo(msg string)
- func (rtx *Context) RecordSuccess(msg string)
- func (rtx *Context) RecordWarning(warn error)
- func (rtx *Context) SetDependency[T any](dep Dependency[T], value T)
- func (rtx *Context) SetDependencyIfAbsent[T any](dep Dependency[T], value T)
- func (rtx *Context) StdinInputs[T any]() (InputLayer[T], error)
- func (rtx *Context) Version() string
- func (rtx *Context) WithHelp(help HelpFunc) *Context
- func (rtx *Context) WithInputReader(fn func(InputSettings) *InputReader) *Context
- func (rtx *Context) WithInputSettings(meta InputSettings) *Context
- func (rtx *Context) WithParser(parser *Parser) *Context
- func (rtx *Context) WithVersion(version string) *Context
- func (rtx *Context) WriteOutput[T any](v T, format string, render func(io.Writer, string, T) error) error
- func (rtx *Context) WriteOutputItem[T any](item T, format string, render func(io.Writer, string, T) error) error
- type Definition
- type Dependency
- type DependencyError
- type Deprecation
- type DiscoverDef
- type DiscoveredPlugin
- type FieldPath
- type FlagDef
- type FlagDependency
- type FlagGroup
- type FlagGroupKind
- type FlagValueCompleter
- type Handler
- type HelpFunc
- type HexBytes
- type InputError
- type InputLayer
- type InputReader
- type InputReport
- type InputSettings
- type InputSource
- type Lifecycle
- type LifecycleStep
- type NoCascadingPostRun
- type NoCascadingPreRun
- type NoHooks
- type NoPostRun
- type NoPreRun
- type Option
- type Outcome
- type OutputDef
- type Page
- type PanicError
- type ParseError
- type ParseKind
- type Parser
- type PathFromDef
- type PluginDef
- type PluginDiscoveryDef
- type PluginDispatch
- type PluginError
- type PluginErrorKind
- type Presence
- type Program
- func (p *Program) Complete(words []string, format CompletionFormat) (int, error)
- func (p *Program) Execute() error
- func (p *Program) Run(argv []string) (int, error)
- func (p *Program) RunContext(ctx context.Context, argv []string) (int, error)
- func (p *Program) With(opts ...Option) *Program
- func (p *Program) WithArgs(args []string) *Program
- func (p *Program) WithCompletion(format CompletionFormat) *Program
- func (p *Program) WithContext(ctx context.Context) *Program
- func (p *Program) WithDependency[T any](dep Dependency[T], value T) *Program
- func (p *Program) WithExit(fn func(int)) *Program
- func (p *Program) WithHelp(help HelpFunc) *Program
- func (p *Program) WithInputReader(fn func(InputSettings) *InputReader) *Program
- func (p *Program) WithInputSettings(meta InputSettings) *Program
- func (p *Program) WithLifecycle(fn Lifecycle) *Program
- func (p *Program) WithOutputChecks() *Program
- func (p *Program) WithPanicRecover(enabled bool) *Program
- func (p *Program) WithParser(parser *Parser) *Program
- func (p *Program) WithReporter(fn Reporter) *Program
- func (p *Program) WithResolver(fn Resolver) *Program
- func (p *Program) WithSignals(sigs ...os.Signal) *Program
- func (p *Program) WithStderr(w io.Writer) *Program
- func (p *Program) WithStdin(r io.Reader) *Program
- func (p *Program) WithStdout(w io.Writer) *Program
- func (p *Program) WithTeardownOnPanic(enabled bool) *Program
- func (p *Program) WithVersion(version string) *Program
- func (p *Program) WithoutSignalHandling() *Program
- type Reporter
- type Resolution
- type Resolver
- type Suggestor
- func (s *Suggestor) Closest(input string, candidates []string) (suggestion string, ok bool)
- func (s *Suggestor) For(err error) []string
- func (s *Suggestor) Suggest(input string, candidates []string) []string
- func (s *Suggestor) WithMaxResults(n int) *Suggestor
- func (s *Suggestor) WithMinScore(score float64) *Suggestor
- type WiringError
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var ( ErrUsage = errors.New("usage error") ErrInternal = errors.New("internal error") )
ErrUsage and ErrInternal are the sentinels the categories match on, so a reporter may branch either way:
if errors.Is(err, rotini.ErrUsage) { /* usage */ }
switch rotini.CategoryOf(err) { case rotini.CategoryUsage: /* usage */ }
Prefer the UsageError and InternalError constructors to wrapping these with fmt.Errorf: they tag the category without prepending the sentinel's text to the message.
var ErrDependencyNotFound = errors.New("rotini: dependency not found")
ErrDependencyNotFound is the sentinel for a dependency that is not registered, or is registered as a different type. Context.MustGetDependency panics with a *DependencyError wrapping it, which the runtime recovers and routes to the reporter.
Functions ¶
func AsCommand ¶ added in v1.2.0
AsCommand labels a hook with the chain index of the command it belongs to, so that Context.Command and Context.Inputs inside it refer to that command. The previous label is restored when the hook returns. DefaultLifecycle wraps every hook it plans. An unlabeled hook reports the invoked command.
A nil hook returns nil, which the engine skips.
func DecodeOutput ¶ added in v1.2.0
DecodeOutput decodes a command's output, as captured from its stdout, into T, checking it against the command's declared schema. T is the generated <Prefix>Output type, which names the command; for output written item by item with Context.WriteOutputItem, T is a slice of it and every item is decoded. format is json, yaml or toml. It is for tests:
var stdout bytes.Buffer
p := cmd.NewProgram(cmd.Handlers()).WithStdout(&stdout)
p.Run([]string{"list", "-o", "json"})
list, err := rotini.DecodeOutput[cmd.TaskrListOutput](p, stdout.Bytes(), "json")
func ExitCause ¶ added in v1.2.0
ExitCause returns a context-cancellation cause that sets the exit code of the run the cancellation halts:
ctx, cancel := context.WithCancelCause(parent) prog.WithContext(ctx) cancel(rotini.ExitCause(3)) // exits 3
A cancellation without an ExitCause also halts the run, and the exit code is resolved as usual. Cancellation never preempts a running hook, and teardown always runs. See Program.WithContext.
func InternalError ¶
InternalError tags err as a CategoryInternal error (a bug or misconfiguration) without altering its message. It returns nil when err is nil.
func MergeInputs ¶ added in v1.2.0
func MergeInputs[T any](layers ...InputLayer[T]) T
MergeInputs merges layers into one inputs value. Slice order is precedence, low → high: a field set by a later layer wins, a field no layer set stays the zero value, and an unset layer field never clobbers a lower layer's. Overlaying an empty layer is the identity.
func PluginCompletion ¶ added in v1.2.0
func PluginCompletion(w io.Writer, result CompletionResult) error
PluginCompletion is the CompletionFormat the plugin hosts kubectl, Docker and Flux read: one candidate per line ("value\tdescription" allowed), then a final ":<directive>" line, a number telling the shell what to do next. kubectl reads it from kubectl_complete-<plugin>, and the Docker and Flux CLIs from the plugin's own __complete (see Program.WithCompletion).
The hint maps onto the directives: kind none to 4, offer no file names; kind directory to 16, directory names only; kind file with extensions to 8, file names with those extensions, which the format carries as the candidates; and kind file or no hint at all to 0, whose fallback is file completion. The hosts read the candidates of the filtering directives as their arguments, so a file or directory hint applies only when there are no candidates. The directive line is always written, since the hosts read the last line as the directive unconditionally. The format belongs to the hosts and is covered by rotini's compatibility promise.
func Ptr ¶
func Ptr[T any](v T) *T
Ptr returns a pointer to v, for the presence-carrying Constraints bounds: Constraints{Minimum: rotini.Ptr(0.0)} declares an enforced >= 0. Prefer the built-in new(v); go fix inlines Ptr to it.
func StripANSI ¶ added in v1.2.0
StripANSI removes every ANSI escape sequence from text, SGR styling and OSC alike, leaving the characters a terminal would display. rotini applies it to man pages, markdown pages and completion descriptions, whose consumers would print escapes literally.
func UsageError ¶
UsageError tags err as bad input the end-user can correct, without altering its message: the result reads exactly like err but matches ErrUsage, and errors.Is/As still reach err. It returns nil when err is nil.
if id == "" {
return rotini.UsageError(fmt.Errorf("a widget id is required"))
}
Types ¶
type ArgDef ¶
type ArgDef struct {
Name string
Type string
Required bool
Variadic bool
Default string
Enum []string
// IgnoreCase matches a value against Enum without regard to case and binds the declared
// spelling; see [FlagDef.IgnoreCase].
IgnoreCase bool
// Separator splits each value of a variadic argument into several; see [FlagDef.Separator].
Separator string
// Layout is how a time argument's value is written; see [FlagDef.Layout].
Layout string
// Deprecated is the argument's deprecation message: supplying it reports a [Deprecation]
// carrying it.
Deprecated string
// Complete is the declarative shell-completion hint for this argument's value.
Complete Completion
Secret bool // when true, the value is redacted in usage/validation error output
Hidden bool // omitted from completion candidates (it still parses); help omission happens at codegen
Constraints
}
ArgDef describes a single positional argument of a command. Variadic is true for a trailing slice argument that absorbs the remaining positionals.
type ArgValueCompleter ¶
ArgValueCompleter is the positional-argument counterpart of FlagValueCompleter: completion calls CompleteArgValue on the invoked command's handler with the argument's logical name and the word being typed. The same contract applies: a nil return falls back to the static enum, and a candidate may carry a "value\tdescription" suffix.
type Base64Bytes ¶
type Base64Bytes []byte
Base64Bytes is binary data written as base64. Standard and URL-safe alphabets are both accepted, padded or not. A spec declares one with `type: base64bytes`.
func (Base64Bytes) MarshalText ¶
func (b Base64Bytes) MarshalText() ([]byte, error)
MarshalText implements encoding.TextMarshaler with the standard, padded alphabet.
func (Base64Bytes) String ¶
func (b Base64Bytes) String() string
String renders the bytes as standard, padded base64.
func (*Base64Bytes) UnmarshalText ¶
func (b *Base64Bytes) UnmarshalText(text []byte) error
UnmarshalText implements encoding.TextUnmarshaler.
type ByteSize ¶
type ByteSize int64
ByteSize is a count of bytes that parses human sizes: `512Mi`, `10MB`, `1.5GiB`, `4096`.
Suffixes follow the SI and IEC standards; the `i` makes a unit binary:
B 1 K KB M MB … 1000, 1000², … (decimal) Ki KiB Mi MiB … 1024, 1024², … (binary)
Letters are case-insensitive and fractions are allowed (`1.5Gi`); the result is rounded to a whole number of bytes. A bare `m` is decimal (megabytes), unlike some tools that read it as binary.
A spec declares one with `type: bytesize`.
func (ByteSize) MarshalText ¶
MarshalText implements encoding.TextMarshaler, rendering the size as ByteSize.String does.
func (ByteSize) String ¶
String renders the size in the largest binary unit it is an exact multiple of — `512Mi`, `2Gi` — and in plain bytes otherwise, so it always parses back to the same value.
func (*ByteSize) UnmarshalText ¶
UnmarshalText implements encoding.TextUnmarshaler.
type Category ¶
type Category int
Category classifies an error by whose fault it is, so a reporter can choose the exit code and message style from one call to CategoryOf. rotini tags its own errors (a missing dependency is CategoryInternal, a parse failure CategoryUsage); user code tags domain errors with UsageError or InternalError.
A category is a label, not an exit code. The default reporter exits 1 for any recorded error or fault; a program that wants distinct codes maps categories in its own reporter.
The constants are ordered by increasing severity (none < usage < internal), so a reporter can keep the worst of several with a plain comparison. The ordering is part of the contract; the numeric values are not.
const ( // CategoryNone is an unclassified error that rotini cannot attribute. CategoryNone Category = iota // CategoryUsage is bad input from the end-user: an unknown flag, a missing required // argument, a value that fails validation. The user fixes it by changing the command. CategoryUsage // CategoryInternal is a bug or misconfiguration in the program, such as a missing // dependency or a wiring mistake. Only the author can fix it. CategoryInternal )
func CategoryOf ¶
CategoryOf returns the Category an error carries, or CategoryNone when it matches neither sentinel:
cmd.Program.WithReporter(func(ctx context.Context, rtx *rotini.Context, out rotini.Outcome) {
worst := rotini.CategoryNone
for _, err := range out.Errors {
fmt.Fprintln(rtx.Stderr, err)
if c := rotini.CategoryOf(err); c > worst {
worst = c
}
}
switch worst {
case rotini.CategoryInternal:
rtx.Exit(70)
case rotini.CategoryUsage:
rtx.Exit(2)
}
})
Inside a reporter the lifecycle has already settled, so Context.HaltWithCode is a no-op and Context.Exit is the only way to set a code.
CategoryOf classifies a single error and tests ErrUsage first, so an error carrying both sentinels (such as the errors.Join that Program.Run returns for a run that recorded both) reports CategoryUsage. To classify a whole run, walk out.Errors and keep the most severe category, as above.
Example ¶
Errors are tagged with a category at the source and mapped to exit codes in one switch, typically inside a reporter. Here usage errors map to 2.
classify := func(err error) int {
switch CategoryOf(err) {
case CategoryUsage:
return 2
case CategoryInternal:
return 70
default:
return 1
}
}
fmt.Println(classify(UsageError(errors.New("unknown flag"))))
fmt.Println(classify(InternalError(errors.New("wiring mismatch"))))
fmt.Println(classify(errors.New("untagged")))
Output: 2 70 1
type Command ¶ added in v1.2.0
type Command struct {
Name string
Handler string // ProgramHandlers method for this command; see [CommandDef.Handler]
Matched string // the argv token that resolved this command (name or an alias); "" for the root
DeprecatedIdentifiers []string // aliases of this command that are deprecated
Deprecated string // the command's deprecation message, when it is deprecated as a whole
Flags []FlagDef
Arguments []ArgDef
FlagGroups []FlagGroup
FlagDependencies []FlagDependency
Commands []CommandDef // sub-commands; empty for a leaf
Plugins []PluginDef // co-located plugin binaries dispatched as sub-commands
PluginDiscovery *PluginDiscoveryDef // plugin auto-discovery (nil = off)
// PluginPath is an extra directory searched for this command's declared and discovered
// plugin binaries, with a leading ~ and $VAR references already expanded. Empty means only
// the host binary's directory and PATH are searched.
PluginPath string
Passthrough bool // every token after this command is a raw positional (no flag parsing)
Output *OutputDef // what the command writes to stdout (nil = not declared); see [Context.WriteOutput]
// Invoked reports whether this is the command the user invoked: the last command in the
// chain. Exactly one entry of [Context.CommandChain] has it set; in a cascading hook,
// rtx.Command().Invoked distinguishes the invoked command from its ancestors.
Invoked bool
}
Command is one node on the invoked command path, root → leaf, as resolved for this invocation and exposed via Context.CommandChain. For a statically composed child, the chain is its full path under the parent.
Fields copy the matching Definition (root) or CommandDef fields; an empty slice means the command declares none of that kind.
func (Command) DiscoveredPlugins ¶ added in v1.2.0
func (cmd Command) DiscoveredPlugins() []DiscoveredPlugin
DiscoveredPlugins returns each plugin discovered for cmd — an executable "<prefix>foo" found next to the binary, in the plugin path, or on PATH — deduped and sorted by name, with any name colliding with a declared sub-command, declared plugin or alias removed. It returns nil when cmd has no discovery or discovery is hidden. rotini renders nothing; a handler lists the result itself:
chain := rtx.CommandChain()
for _, p := range chain[len(chain)-1].DiscoveredPlugins() {
fmt.Fprintf(out, " %s\t%s\n", p.Name, p.Path)
}
It reads the filesystem on every call and is best-effort: an unreadable directory contributes nothing. Command.PluginDiscoveryErrors reports failures of the configured plugin path.
func (Command) PluginBinary ¶ added in v1.2.0
PluginBinary reports the executable the named plugin of cmd would run, and whether it resolves. It searches where dispatch searches, in the same order: next to the host binary, then the command's plugin path, then PATH. Use it, not exec.LookPath, to check which plugins are installed.
name may be a declared plugin's name or alias, or a discovered plugin's token. It returns "", false when cmd declares no such plugin and has no discovery, or when the binary is not found. It reads the filesystem on every call.
func (Command) PluginDiscoveryErrors ¶ added in v1.2.0
PluginDiscoveryErrors returns the problems encountered scanning cmd's configured plugin path — typically that it is unreadable or not a directory — and nil when there is no discovery, no plugin path, discovery is hidden, or the path scanned cleanly. A path that does not exist is not a problem. The directory of the binary and the $PATH entries are not reported. rotini prints no warning itself, since that would corrupt completion output. Each error carries the path and cause, so errors.Is(err, fs.ErrPermission) classifies it.
type CommandDef ¶
type CommandDef struct {
Name string
Aliases []string
Summary string // one-line description (completion candidates carry it as "name\tsummary")
Handler string // ProgramHandlers method, e.g. "RotiniGenerate"
Hidden bool // omitted from completion candidates (it still dispatches); help omission happens at codegen
DeprecatedIdentifiers []string // aliases (subset of Aliases) that [Deprecations] reports when used to invoke
// Deprecated is the command's deprecation message: invoking it by any name reports a
// [Deprecation] carrying it. Empty means the command is not deprecated as a whole.
Deprecated string
Flags []FlagDef
Arguments []ArgDef
FlagGroups []FlagGroup // cross-flag presence rules validated at parse time
FlagDependencies []FlagDependency // conditional cross-flag requirements validated at parse time
Commands []CommandDef
Plugins []PluginDef // plugin binaries dispatched as sub-commands of this command
PluginDiscovery *PluginDiscoveryDef // plugin auto-discovery on this command (nil = off)
PluginPath string // extra directory searched for BOTH this command's declared plugins and its discovered plugins
Passthrough bool // every token after this command is a raw positional (no flag parsing)
Output *OutputDef // what the command writes to stdout (nil = not declared)
}
CommandDef describes one command node within a Definition. Handler is the ProgramHandlers method name the runtime invokes to obtain this command's Handler.
type Completion ¶
type Completion struct {
// Kind is "file", "directory", or "none". Empty means no hint: the shell applies its
// own default, which for bash and zsh is file completion. "none" suppresses that
// default, for opaque values such as resource IDs.
Kind string
// Extensions narrows Kind "file" to these suffixes, written without a dot
// ("yaml", "json"). Empty offers every file.
Extensions []string
}
Completion is a declarative hint about what an input's value is, for the shell to complete. It reaches the shell as a directive on the last line of the hidden __complete output, and each generated script translates it into that shell's own path completion. A dynamic completer (FlagValueCompleter, ArgValueCompleter) wins when it answers; the hint is the fallback.
type CompletionCandidate ¶ added in v1.1.1
CompletionCandidate is one offered value and its optional one-line description.
type CompletionFormat ¶ added in v1.1.1
type CompletionFormat func(w io.Writer, result CompletionResult) error
CompletionFormat writes a CompletionResult to w in one completion protocol, the wire shape a completing host expects. rotini computes the answer; the format only encodes it. PluginCompletion is the built-in for the plugin hosts kubectl, Docker and Flux.
A format is called once per request and must write only the answer, since the host parses all of it.
type CompletionResult ¶ added in v1.1.1
type CompletionResult struct {
// Candidates are the offered values, in order — sub-commands, flags, enum values, or what a
// handler's [FlagValueCompleter] or [ArgValueCompleter] returned — already filtered by the
// typed prefix, with hidden inputs left out.
Candidates []CompletionCandidate
// Hint is the spec's declared `complete:` hint for the input being completed, or the zero
// value when it declares none. A completer that answers still wins over it: the hint is the
// fallback for when Candidates is empty, except kind "none", which also means "never offer
// files" when there are candidates.
Hint Completion
}
CompletionResult is one completion answer, before any wire format: what a program offers for the word being completed. It is what a CompletionFormat renders.
type ConfigFile ¶
type ConfigFile struct {
Name string // logical name
// Scope is the command path this source is declared on. Sources cascade: one is in
// scope for the invoked chain when its Scope is one of the chain's commands. "" is
// unscoped, in scope for every command; generated descriptors always set it.
Scope string
Path string // fixed file path (may contain ~)
Format string // "json" | "yaml" | "toml"; "" lets the input reader infer from the extension
Discover *DiscoverDef // run-time location strategy, instead of a fixed Path
PathFrom *PathFromDef // runtime inputs that supply/override the path (spec config_source)
// Schema is the self-contained JSON Schema the input reader validates the loaded document
// against at bind time; "" is none. The file that actually resolved is the one
// validated, and an absent optional file passes vacuously.
Schema string
}
ConfigFile is one configuration-file source the input reader reads (reconciled by recon). Exactly one of Path and Discover locates the file (the spec enforces this).
type Constraints ¶
type Constraints struct {
Minimum *float64 // inclusive numeric lower bound; nil = unset
Maximum *float64 // inclusive numeric upper bound; nil = unset
ExclusiveMinimum *float64 // strict numeric lower bound (value must be >); nil = unset
ExclusiveMaximum *float64 // strict numeric upper bound (value must be <); nil = unset
MultipleOf *float64 // the value must be an integer multiple (strictly positive); nil = unset
MinLength int // minimum string length in runes; 0 = unset
MaxLength int // maximum string length in runes; 0 = unset
MinItems int // minimum item count (repeatable flag / variadic argument); 0 = unset
MaxItems int // maximum item count; 0 = unset
Pattern string // regular expression the value must contain (string types); "" = unset
PatternMessage string // what a Pattern failure tells the user, in place of the regex; "" = show the regex
}
Constraints carries the validation bounds a spec may declare on a flag or argument. The parser enforces them after reconciliation, so a value supplied via env or config is checked too. The numeric bounds are presence-carrying pointers — nil is unset, so `minimum: 0` is a real, enforced bound. The length and count bounds keep the zero-sentinel convention: a 0 minimum is vacuous and a 0 maximum is not expressible.
type Context ¶
type Context struct {
// Stdin, Stdout and Stderr are the program's streams ([Program.WithStdin] and siblings).
// Handlers use them instead of os.Std* so tests can substitute streams. They are set before
// dispatch and never nil.
//
// Assigning one is not supported: the default reporter writes to the Program's streams, so
// reassigning rtx.Stdout redirects only the handler's own writes. Redirect a run with
// [Program.WithStdout] and its siblings.
Stdin io.Reader
Stdout io.Writer
Stderr io.Writer
// Argv is the raw argument vector for this invocation, command names and flags included,
// for a handler that runs its own parser. It is the live slice, not a copy: mutating it
// changes what later parses see. A command's declared positionals are the Arguments field
// of its generated inputs.
Argv []string
// contains filtered or unexported fields
}
Context is rotini's per-invocation context: the program's dependencies and streams, the raw argument vector (Context.Argv) and the resolved command chain (Context.CommandChain). One is built per Program.Run and passed to every hook of that run, so hooks share its dependencies, records and exit state, and nothing carries over between runs.
The surface groups into eight jobs:
- invocation — the Context.Argv, Context.Stdin, Context.Stdout and Context.Stderr fields
- command — Context.Command is the command whose hook is running (its Invoked field reports whether the user ran it), Context.CommandPath names it, and Context.CommandChain lists every command from the root to the invoked one
- inputs — Context.Inputs returns the command's validated inputs, Context.InputsWithReport adds where each value came from, and the per-channel Context.ArgvInputs, Context.EnvInputs, Context.FileInputs, Context.StdinInputs and Context.DefaultInputs read one channel each
- output — Context.WriteOutput writes the command's declared output to stdout, Context.WriteOutputItem one item of a stream, and Context.CheckOutput checks a value against the declared shape without writing it
- dependencies — Context.GetDependency and Context.MustGetDependency read one; Context.SetDependency and Context.SetDependencyIfAbsent set one for this run
- records — Context.RecordInfo, Context.RecordSuccess, Context.RecordWarning, Context.RecordError, and Context.Failed
- stopping — Context.HaltWith to fail, Context.Halt to stop, Context.HaltWithCode to stop with a code, Context.Exit to stop and skip pending teardown
- rotini's own seams — Context.Version, Context.Help and Context.Parser read them; Context.WithVersion, Context.WithHelp, Context.WithParser, Context.WithInputSettings and Context.WithInputReader set them on a standalone Context
Nothing is parsed or validated until a handler calls an inputs method; a handler with its own parser reads Context.Argv instead.
A Context is safe for concurrent use by the goroutines a hook starts. Context.Command reflects the step running when it is called (see its doc). A Context must not be copied.
Methods do not check for a nil receiver, with these exceptions: the With setters do nothing and return nil, the inputs methods return an error, and Deprecations returns none.
func NewContextFor ¶
func NewContextFor(def Definition, argv []string) *Context
NewContextFor builds a Context with argv resolved against def, as the runtime does before dispatch, using the os streams and no seams. It serves tests of the Parser, the Context.Inputs family, or a single hook:
def := rotini.Definition{Name: "app", Handler: "App", Commands: []rotini.CommandDef{ … }}
rtx := rotini.NewContextFor(def, []string{"build", "x.yaml"})
var in appInputs
err := rtx.Parser().Parse(rtx, &in)
To test a generated program end to end, build it with the generated NewProgram and run it with Program.WithExit and captured streams.
A plugin token resolves to the chain that precedes it; no plugin is executed.
func (*Context) ArgvInputs ¶ added in v1.2.0
func (rtx *Context) ArgvInputs[T any]() (InputLayer[T], error)
ArgvInputs parses the command line only — flags and positionals across the resolved chain, exactly as supplied: no defaults, no fallback, and no required or enum validation. Validate the overlaid result with InputReport.Validate, so a required flag satisfied by another layer passes. Parse failures are *ParseError values.
func (*Context) CheckOutput ¶ added in v1.2.0
CheckOutput checks v against the invoked command's declared output schema, returning an internal error that names each field that does not match:
taskr list: output does not match its contract: output.tasks[2].status: value is not in enum
It writes nothing. Program.WithOutputChecks makes every WriteOutput call check.
func (*Context) Command ¶
Command returns the command whose hook is running.
In PreRun, Run and PostRun that is the command the user invoked. A cascading hook runs for every command in the chain, and there it is the command the hook belongs to:
$ mig db status hook Command() Command().Invoked ──── ───────── ───────────────── mig's CascadingPreRun mig false db's CascadingPreRun db false the leaf's PreRun / Run / PostRun status true db's CascadingPostRun db false mig's CascadingPostRun mig false
Invoked distinguishes the command the user ran from its ancestors:
func (*songsHandler) CascadingPreRun(ctx context.Context, rtx *rotini.Context) {
if rtx.Command().Invoked {
// `musak songs`: this command is the invocation.
return
}
// `musak songs list`: a sub-command is running.
}
Context.Inputs, Context.CommandPath and Context.Help all describe the command Command returns. Outside a lifecycle step (a Context from NewContextFor, or in the reporter), Command returns the invoked command.
Goroutines ¶
Command reports the step running when it is called, not the hook that started the caller. A goroutine that outlives its hook may therefore see a later command, and inputs it reads anchor there too. Capture what it needs before starting it:
cmd := rtx.Command()
go func() { log.Println(cmd.Name) }()
func (*Context) CommandChain ¶ added in v1.2.0
CommandChain returns every command of this invocation from the root (index 0) to the invoked command (the last entry, the only one with Invoked set).
The slice is a copy; changing it does not affect the run. The copy is shallow: each command's Flags, Arguments and Commands are the Definition's own slices, shared by every run, and must be treated as read-only.
func (*Context) CommandPath ¶
CommandPath returns the path of Context.Command, space-joined from the root: "tasks add" for a sub-command, "tasks" for the root. In the root's cascading hook during `tasks add` it is "tasks".
The names are canonical, not aliases; each command's Matched field in Context.CommandChain holds the token the user typed.
func (*Context) DefaultInputs ¶ added in v1.2.0
func (rtx *Context) DefaultInputs[T any]() (InputLayer[T], error)
DefaultInputs synthesizes the spec's declared defaults as an explicit layer, conventionally the lowest. Flag and argument defaults come from the resolved chain; env and config defaults from their recon tags.
func (*Context) EnvInputs ¶ added in v1.2.0
func (rtx *Context) EnvInputs[T any]() (InputLayer[T], error)
EnvInputs acquires the environment channel: every Env input, by its explicit variable or the SNAKE_UPPER projection of its key, plus the env fallback of any flag that declares a recon key. A missing required input or a constraint violation is this call's error.
func (*Context) Exit ¶
Exit sets the exit code and stops the lifecycle immediately, skipping every pending teardown hook, as os.Exit skips deferred calls. Prefer Context.HaltWithCode for an orderly stop. It records no error. A panic recovered before Exit still reaches the reporter.
During the lifecycle the first non-zero code wins; inside the reporter, Exit overrides any code already set.
func (*Context) Failed ¶
Failed reports whether this run has so far recorded an error or captured a fault. A teardown hook uses it to decide between committing and rolling back:
func (*migrateHandlers) CascadingPostRun(ctx context.Context, rtx *rotini.Context) {
if rtx.Failed() {
tx.Rollback()
return
}
tx.Commit()
}
The errors themselves reach only the reporter, through Outcome. Once true, Failed stays true for the rest of the run.
func (*Context) FileInputs ¶ added in v1.2.0
func (rtx *Context) FileInputs[T any]() (InputLayer[T], error)
FileInputs acquires the configuration-files channel: every Config input from the InputSettings sources, declared order being precedence, plus the config fallback of any flag that declares a recon key. Required and constraint failures are this call's errors.
func (*Context) GetDependency ¶ added in v1.2.0
func (rtx *Context) GetDependency[T any](dep Dependency[T]) (T, bool)
GetDependency returns the value registered under dep and whether one is registered as type T. It never panics.
client, ok := rtx.GetDependency(tasks.Client)
func (*Context) Halt ¶
func (rtx *Context) Halt()
Halt stops the lifecycle's forward progress without setting an exit code or recording anything. Teardown is unaffected: every PostRun and CascadingPostRun whose setup hook began still runs, in reverse. Under the default plan:
Hook Effect of Halt ──── ────────────── CascadingPreRun no further setup, no PreRun, no Run PreRun Run is skipped Run none: Run is the last forward step PostRun none: teardown runs to completion CascadingPostRun none
Use Halt to stop when nothing failed:
if !inputs.Force && !confirmed {
rtx.Halt()
return
}
Use Context.HaltWith to fail, Context.HaltWithCode to stop with a specific code, and Context.Exit to stop without teardown. Halt is a no-op inside the reporter.
func (*Context) HaltWith ¶
HaltWith records err and stops the lifecycle's forward progress: Context.RecordError and Context.Halt in one call. It sets no exit code; the reporter decides it.
if err := store.Save(task); err != nil {
rtx.HaltWith(err)
return
}
It is the standard way for any hook to fail. Where Halt has no effect (Run and the teardown hooks), HaltWith only records. A nil err records nothing and still halts. To record and continue, use RecordError alone.
Inside the reporter it has no effect: the halt is a no-op and the record is dropped.
func (*Context) HaltWithCode ¶
HaltWithCode sets the exit code and stops the lifecycle's forward progress, for a code that carries meaning: a filter reporting "no match" as 1, a wrapper passing through a child's status. It records no error.
Teardown is unaffected, as with Context.Halt. The first non-zero code wins, so a later HaltWithCode or Context.Exit does not replace an earlier code.
Halt() stop HaltWith(err) stop and record err HaltWithCode(n) stop and set exit code n Exit(n) stop, set exit code n, and skip pending teardown
It is a no-op inside the reporter; the reporter sets the code with Context.Exit.
func (*Context) Help ¶
Help returns the help page of Context.Command from Program.WithHelp, as a generated --help prints it, or "" when there is none. A cascading hook gets its own command's page. The page is the running program's, so a composed command shows its full path and inherited flags.
func (*Context) Inputs ¶ added in v1.2.0
Inputs acquires every declared channel (argv, environment, configuration files, the stdin payload, defaults), reconciles them in the standard precedence defaults < files < env < argv, validates the result, and returns it:
inputs, err := rtx.Inputs[MycliDeployInputs]()
Stdin is outside that order because it never competes: it fills only the leaf command's payload field, which no other channel writes.
T must be the inputs type generated for the command whose hook is running (Context.Command), in any hook. Its last field describes that command and the preceding fields its ancestors, so the struct is anchored on the running command regardless of how deep the invocation went. A type that cannot be anchored there is an error, not a silent zero value.
Inputs delegates to InputReader.Read; errors are *ParseError and *InputError values. On error the returned T is partially filled and must not be used. Inputs stops at the first argv error, before the environment and configuration channels are read; use Context.InputsWithReport for a merged value and per-field provenance alongside the error.
func (*Context) InputsWithReport ¶ added in v1.2.0
func (rtx *Context) InputsWithReport[T any]() (T, InputReport, error)
InputsWithReport is Context.Inputs with provenance: the same reconciled, validated inputs plus an InputReport giving each field's Winner and History. It acquires each channel separately and overlays the layers in the standard precedence. An acquisition error returns the zero T; a validation error returns the merged inputs and report alongside it, so the caller can see which layer supplied the offending value.
func (*Context) MustGetDependency ¶ added in v1.2.0
func (rtx *Context) MustGetDependency[T any](dep Dependency[T]) T
MustGetDependency returns the value registered under dep, or panics with a *DependencyError when it is missing or not a T. The runtime recovers the panic during dispatch and routes it to the reporter.
s := rtx.MustGetDependency(tasks.Store)
Example ¶
A typed handle names a dependency once, and every read is typed. GetDependency reports absence; MustGetDependency panics, and in a hook that panic reaches the reporter as a *PanicError after teardown.
type apiClient struct{ baseURL string }
api := NewDependency[*apiClient]("api")
rtx := NewContextFor(Definition{Name: "app", Handler: "App"}, nil)
rtx.SetDependency(api, &apiClient{baseURL: "https://api.example"})
client := rtx.MustGetDependency(api)
fmt.Println(client.baseURL)
if _, ok := rtx.GetDependency(NewDependency[*apiClient]("other")); !ok {
fmt.Println("nothing registered as \"other\"")
}
Output: https://api.example nothing registered as "other"
func (*Context) Parser ¶
Parser returns the Parser set by Program.WithParser, or a new default parser. It never returns nil.
func (*Context) RecordError ¶
RecordError records err as one of this run's errors. It neither prints nor stops the lifecycle, so a handler can record several errors before stopping, or leave the decision to a later hook that checks Context.Failed. A nil err is ignored.
for _, path := range inputs.Check.Arguments.Paths {
if err := validate(path); err != nil {
rtx.RecordError(err)
}
}
To record and stop in one call, use Context.HaltWith.
func (*Context) RecordInfo ¶
RecordInfo records msg as an informational message for the reporter. Like every record method it neither prints nor stops the lifecycle. An empty msg is ignored.
func (*Context) RecordSuccess ¶
RecordSuccess records msg as a success message for the reporter. An empty msg is ignored. The reporter runs after the lifecycle, so records appear after anything a handler wrote directly to Context.Stdout.
func (*Context) RecordWarning ¶
RecordWarning records warn as a non-fatal warning: a deprecation, a fallback, a skipped item. It is an error so a reporter can inspect it with errors.As or CategoryOf; it never affects the exit code. A nil warn is ignored.
func (*Context) SetDependency ¶ added in v1.2.0
func (rtx *Context) SetDependency[T any](dep Dependency[T], value T)
SetDependency registers value under dep for this run, replacing any existing value, so hooks that run later see it. Use it for per-run values such as a client built from the inputs; program-wide values belong in Program.WithDependency. It is safe for concurrent use.
func (*Context) SetDependencyIfAbsent ¶ added in v1.2.0
func (rtx *Context) SetDependencyIfAbsent[T any](dep Dependency[T], value T)
SetDependencyIfAbsent registers value under dep for this run only if nothing is registered there yet, atomically. A handler can register its real implementation this way while a test that registered a double under the same name keeps the double.
rtx.SetDependencyIfAbsent(Clock, time.Now) now := rtx.MustGetDependency(Clock)
func (*Context) StdinInputs ¶ added in v1.2.0
func (rtx *Context) StdinInputs[T any]() (InputLayer[T], error)
StdinInputs acquires the stdin channel: the leaf command's typed payload, decoded per its declared format, schema-validated, and honoring required.
func (*Context) Version ¶
Version returns the version set by Program.WithVersion, or "" if none was set.
func (*Context) WithHelp ¶
WithHelp sets where Context.Help finds pages. See Program.WithHelp. It is for a Context built with NewContextFor; during a run, the change lasts for the rest of that run. A nil help is ignored.
func (*Context) WithInputReader ¶ added in v1.2.0
func (rtx *Context) WithInputReader(fn func(InputSettings) *InputReader) *Context
WithInputReader replaces the input reader Context.Inputs uses. See Program.WithInputReader. It is for a Context built with NewContextFor; during a run, the change lasts for the rest of that run. A nil fn is ignored.
func (*Context) WithInputSettings ¶ added in v1.2.0
func (rtx *Context) WithInputSettings(meta InputSettings) *Context
WithInputSettings supplies the generated descriptor Context.Inputs reads from. See Program.WithInputSettings. It is for a Context built with NewContextFor; during a run, the change lasts for the rest of that run.
func (*Context) WithParser ¶
WithParser sets the parser Context.Parser returns. See Program.WithParser. It is for a Context built with NewContextFor; during a run, the change lasts for the rest of that run. A nil parser is ignored.
func (*Context) WithVersion ¶
WithVersion sets what Context.Version reports. See Program.WithVersion. It is for a Context built with NewContextFor; during a run, the change lasts for the rest of that run.
func (*Context) WriteOutput ¶ added in v1.2.0
func (rtx *Context) WriteOutput[T any](v T, format string, render func(io.Writer, string, T) error) error
WriteOutput writes v, the invoked command's output, to rtx.Stdout in format: indented json, yaml or toml by rotini, any other format by render, which may be nil when the handler only ever passes those three. An empty format means json.
It returns an internal error when v is not the type the command declares as its output, when no renderer is passed for a format rotini does not write, or, with Program.WithOutputChecks, when v does not match the declared shape. Nothing is written then. A renderer's error is returned unwrapped. A command that declares no output may still use it; nothing is checked.
func (*Context) WriteOutputItem ¶ added in v1.2.0
func (rtx *Context) WriteOutputItem[T any](item T, format string, render func(io.Writer, string, T) error) error
WriteOutputItem writes one item of a stream to rtx.Stdout, for a command that writes its output item by item as each is ready: compact json, one value per line; a yaml document starting "---"; any other format by render. toml cannot be streamed. Its checks are WriteOutput's, with the item checked against the declared shape, which for such a command is one item.
type Definition ¶
type Definition struct {
Name string
Handler string // ProgramHandlers method for the root command, e.g. "Rotini"
Flags []FlagDef
Arguments []ArgDef
FlagGroups []FlagGroup // cross-flag presence rules validated at parse time
FlagDependencies []FlagDependency // conditional cross-flag requirements validated at parse time
Commands []CommandDef
Plugins []PluginDef // co-located plugin binaries dispatched as sub-commands of the root
PluginDiscovery *PluginDiscoveryDef // plugin auto-discovery on the root command (nil = off)
PluginPath string // extra directory searched for BOTH declared and discovered plugins
Passthrough bool // every token after the program name is a raw positional (no flag parsing)
Output *OutputDef // what the root command writes to stdout (nil = not declared)
}
Definition is the compiled command tree of a generated rotini program. Codegen emits it as a Go literal; the runtime parses argv, dispatches and completes against it. Help pages are rendered at codegen and supplied through Program.WithHelp. The Definition types are data only, with no behavior.
type Dependency ¶ added in v1.2.0
type Dependency[T any] struct { // contains filtered or unexported fields }
Dependency is a typed handle for one of the program's dependencies, pairing the name it is stored under with the type it is stored as, so a handler needs neither a string key nor a type assertion:
// declared once, beside the thing it names
var Store = rotini.NewDependency[*store.Store]("taskr.store")
// main.go: the value's type is checked at compile time here
cmd.Program.WithDependency(tasks.Store, store.New()).Execute()
// any handler
s := rtx.MustGetDependency(tasks.Store)
Registration happens at run time, so a missing registration is not a compile error; it is reported when a handler requests the dependency.
Dependencies have two scopes:
- program-wide: Program.WithDependency or the WithDependency option, seeded into every run
- one run: Context.SetDependency or Context.SetDependencyIfAbsent from a hook, visible to the hooks that run after it
A handle whose name is known only at run time can be built as rotini.NewDependency[any](name).
The zero Dependency is named "" and is shared by every zero Dependency of any type; build handles with NewDependency.
func NewDependency ¶ added in v1.2.0
func NewDependency[T any](name string) Dependency[T]
NewDependency returns a typed handle for a dependency stored under name. The name must be unique within a program; an empty name is accepted and refers to the same entry as the zero Dependency.
func (Dependency[T]) Name ¶ added in v1.2.0
func (d Dependency[T]) Name() string
Name returns the name the dependency is stored under.
func (Dependency[T]) String ¶ added in v1.2.0
func (d Dependency[T]) String() string
String implements fmt.Stringer, returning the dependency's name.
type DependencyError ¶ added in v1.2.0
type DependencyError struct {
Name string // the name the dependency was requested under
// contains filtered or unexported fields
}
DependencyError reports a dependency that was requested but not registered, or registered as a different type. It unwraps to ErrDependencyNotFound; recover the name with errors.As.
func (*DependencyError) Error ¶ added in v1.2.0
func (e *DependencyError) Error() string
Error returns a one-line message stating whether the dependency is missing or registered as a different type.
func (*DependencyError) Unwrap ¶ added in v1.2.0
func (e *DependencyError) Unwrap() []error
Unwrap returns ErrDependencyNotFound and ErrInternal, so CategoryOf classifies the error as CategoryInternal.
type Deprecation ¶
type Deprecation struct {
Kind string // "flag", "argument" or "command"
Name string // the input's logical name (the flag/argument/command name)
Identifier string // the token actually used on argv (e.g. "--conf", "build"); an argument's <name>
Message string // the spec's `deprecated:` message, when the input is deprecated as a whole
}
Deprecation is a deprecated token found in this invocation's argv: the identifier used, the kind of input, its logical name, and the spec's `deprecated:` message. It implements error so it can be recorded or printed directly.
func Deprecations ¶
func Deprecations(rtx *Context) []Deprecation
Deprecations returns each deprecated token this invocation used: a command invoked via a deprecated alias or deprecated as a whole, a flag set via a deprecated identifier, or a deprecated argument given a value. rotini prints nothing; the handler decides what to do:
for _, d := range rotini.Deprecations(rtx) {
rtx.RecordWarning(fmt.Errorf("%w — use %q instead", d, d.Name))
}
It needs no Parser: it reads the resolved chain and argv from rtx, and never reads a file or stdin. It returns nil for a nil rtx. When argv does not parse, only command deprecations are reported.
func (Deprecation) Error ¶
func (d Deprecation) Error() string
Error renders the deprecation notice as a single line, with the author's message when there is one: `flag "--conf" is deprecated: use --config`.
type DiscoverDef ¶
type DiscoverDef struct {
Strategy string // "walk-up" (working directory up to the filesystem root) | "xdg" ($XDG_CONFIG_HOME/<app>, default ~/.config/<app>)
File string // the file name looked for in each searched directory
App string // the application directory under the XDG config root (xdg only)
}
DiscoverDef locates a configuration file at run time. The strategy orders the directories searched for File; the first containing it wins, and a file found nowhere is absent.
type DiscoveredPlugin ¶
type DiscoveredPlugin struct {
// Name is the token a user types — "foo" for an executable "<prefix>foo".
Name string
// Path is the executable dispatch would run for Name right now: the first "<prefix>foo"
// in search order (next to the binary, then the plugin path, then PATH), so a copy
// shadowed by an earlier one is not the one listed.
Path string
}
DiscoveredPlugin is one plugin discovery found: the token it is invoked by and the binary that token runs.
type FieldPath ¶
type FieldPath string
FieldPath identifies one leaf field of a generated inputs struct by its dot-joined Go field path, e.g. "RotiniGenerate.Flags.ConfFilePath".
type FlagDef ¶
type FlagDef struct {
Name string
Identifiers []string // CLI forms, e.g. {"--loud", "-l"}
Summary string // one-line description (completion candidates carry it as "identifier\tsummary")
Type string // resolved Go type, e.g. "bool", "string", "[]string", "int", "time.Duration"
Required bool
Default string
// Defaults is the multi-value default for a REPEATABLE input (a `[]…` or map type):
// each element is seeded as one occurrence, exactly as if the user had repeated the
// flag. It is used only when Default is empty, and only when the input is unset from
// every channel — a default never merges with a supplied value.
Defaults []string
Enum []string
// IgnoreCase matches a value against Enum without regard to case (`--mode FAST` against
// fast/slow) and binds the declared spelling, so a handler compares against one form.
IgnoreCase bool
// Separator splits each value of a list or map flag into several (`--tags a,b` is two
// tags), CSV-style: a quoted item keeps the separator (`--tags '"a,b",c'`). Empty means
// one value per occurrence.
Separator string
// ImplicitValue is the value a flag takes when given without one (`--color` means
// "always"), making its value optional: a value must then be attached (`--color=never`),
// since the next argument is never consumed. Empty means the flag requires a value.
ImplicitValue string
// Layout is how a time input's value is written: a Go reference-time layout
// ("2006-01-02", "Jan 2 2006 15:04"), or "unix" / "unixmilli" for a timestamp. Empty means
// RFC 3339. `type: date` gets "2006-01-02".
Layout string
// ObjectSchema is the JSON Schema of an object-valued flag's value — set when the spec's
// schema is a named object (`$ref: '#/schemas/DB'`), or a list of them. The flag then
// takes JSON, key=value pairs, a YAML @file, or one field per flag (--db.host=…); see
// "Input values" in the package documentation.
ObjectSchema string
Secret bool // when true, the value is redacted in usage/validation error output
Hidden bool // omitted from completion candidates (it still parses); help omission happens at codegen
DeprecatedIdentifiers []string // identifiers (subset of Identifiers) that [Deprecations] reports when used
// Deprecated is the flag's deprecation message: setting it by any identifier reports a
// [Deprecation] carrying it. Empty means the flag is not deprecated as a whole.
Deprecated string
// Negatable adds a "--no-<x>" form for every long identifier of a bool flag, which sets
// it false, overriding a true default, config value or environment variable.
Negatable bool
DottedKeys bool // map flag whose key=value keys are '.'-separated paths into nested maps (spec dotted_keys)
KeyPaths []string // a map flag's declared key paths (from its schema's properties), completed up to the '='
From []string // extra acquisition modes (spec from:): "file" resolves @path values, "stdin" resolves a bare "-"
// Complete is the declarative shell-completion hint for this flag's value (spec
// complete:). The zero value means no hint.
Complete Completion
Constraints
}
FlagDef describes a single flag of a command. Name is the logical name and matches the `rotini:"<name>"` tag on the corresponding generated input field.
type FlagDependency ¶
type FlagDependency struct {
When string // the flag whose presence triggers the requirement
Requires []string // flags that must also be set when When is set
}
FlagDependency is a conditional cross-flag requirement: when the When flag is set on argv, every flag in Requires must be too. "Set" follows the same convention as FlagGroup.
type FlagGroup ¶
type FlagGroup struct {
Kind FlagGroupKind
Flags []string // logical flag names that make up the group
}
FlagGroup constrains which of a command's flags may, or must, appear together. Flags are referenced by their logical Name, and "set" means explicitly provided on argv — a default or fallback does not count. The Parser enforces it; a violation is a usage error.
type FlagGroupKind ¶
type FlagGroupKind string
FlagGroupKind names a cross-flag presence rule. The value is the spec's `kind`.
const ( // FlagGroupMutuallyExclusive: at most one of the group's flags may be set. FlagGroupMutuallyExclusive FlagGroupKind = "mutually_exclusive" // FlagGroupRequiredTogether: set all of the group's flags, or none. FlagGroupRequiredTogether FlagGroupKind = "required_together" // FlagGroupOneOf: exactly one of the group's flags must be set. FlagGroupOneOf FlagGroupKind = "one_of" // FlagGroupAtLeastOne: at least one of the group's flags must be set. FlagGroupAtLeastOne FlagGroupKind = "at_least_one" )
type FlagValueCompleter ¶
type FlagValueCompleter interface {
CompleteFlagValue(rtx *Context, flag, partial string) []string
}
FlagValueCompleter is an optional interface a command's handler may implement to supply dynamic completion candidates for one of its flags' values. Completion resolves the handler of the command that declares the flag and, if it implements this interface, calls CompleteFlagValue with the flag's logical name and the word being typed. A nil return falls back to the flag's static enum; a non-nil return, empty included, is authoritative.
rtx carries the resolved chain, the completion words in Context.Argv, and the dependencies registered with Program.WithDependency.
The line is half-typed, so Context.Inputs would fail validation. To read what has been typed so far, merge the lenient per-channel layers, which bind without validating. Flags of an ancestor are read with that ancestor's inputs type, running as that ancestor:
var in AppInputs
rotini.AsCommand(0, func(_ context.Context, rtx *rotini.Context) {
env, _ := rtx.EnvInputs[AppInputs]()
argv, _ := rtx.ArgvInputs[AppInputs]()
in = rotini.MergeInputs(env, argv) // argv wins, as it would at run time
})(context.Background(), rtx)
A panic in a completer is not recovered. It may run on every keystroke, so it must be read-only and fast.
A candidate may carry a one-line description after a tab, "value\tdescription": zsh, fish and powershell render it beside the value, and bash strips it.
type Handler ¶ added in v1.2.0
type Handler interface {
CascadingPreRun(ctx context.Context, rtx *Context)
PreRun(ctx context.Context, rtx *Context)
Run(ctx context.Context, rtx *Context)
PostRun(ctx context.Context, rtx *Context)
CascadingPostRun(ctx context.Context, rtx *Context)
}
Handler is the lifecycle interface every command's handler implements. The runtime invokes the hooks in order, sharing one Context across the chain. Embed the No* types below to declare only the hooks a command uses.
Instance lifetime ¶
Per run, the runtime asks the handler set given to NewProgram (the generated ProgramHandlers) once for each command in the chain, and that value serves the command's hooks for the run:
- A field carries state between one command's own hooks. The leaf's PreRun, Run and PostRun share one value, as do a command's CascadingPreRun and CascadingPostRun.
- A field cannot cross commands. State that travels down the chain is a dependency.
Where the state flows decides the tool:
state flows… use ──────────── ─── between one command's own hooks a field on the handler between different commands in the chain rtx.SetDependency(dep, v) across every run of the program p.WithDependency(dep, v)
Hand-written handler sets ¶
Generated wiring methods return a new handler per call, so fields are per-run state. A hand-written wiring method that returns a shared value makes that handler's fields shared across runs, and a data race under concurrent Program.Run. Handlers that keep state in fields must be returned fresh on every call.
type HelpFunc ¶
HelpFunc returns the help page of the command named by path (canonical names below the root; none for the root), or an error when there is no such command. Codegen generates a Help function of this type.
type HexBytes ¶
type HexBytes []byte
HexBytes is binary data written as hexadecimal, with or without a `0x` prefix: `deadbeef`, `0xDEADBEEF`. A spec declares one with `type: hexbytes`.
func (HexBytes) MarshalText ¶
MarshalText implements encoding.TextMarshaler as lowercase hex without a prefix.
func (*HexBytes) UnmarshalText ¶
UnmarshalText implements encoding.TextUnmarshaler.
type InputError ¶ added in v1.2.0
type InputError struct {
Channel string // one of "env", "config", "stdin", "flag"
Input string // the offending input key/path, when a single one is known (else "")
Msg string // a clean, non-leaky, rotini-owned message
Cause error // the underlying recon/decode/OS error, reachable via errors.As (may be nil)
// contains filtered or unexported fields
}
InputError reports a failure acquiring or decoding one of a command's non-argv input channels: environment variables, configuration files, a typed stdin payload, or a flag's env/config fallback. It is the counterpart of the argv channel's *ParseError.
Error names the channel, the input, and what went wrong. The underlying recon, decode or OS Cause is reachable via errors.As but kept out of the message, and the value of an input marked secret is redacted.
A bad value, a missing required input, or a malformed document the user supplied is CategoryUsage; a registry build, schema compile, IO read, or codegen mismatch is CategoryInternal.
var be *rotini.InputError
if errors.As(err, &be) {
fmt.Fprintf(os.Stderr, "bad %s input %q: %s\n", be.Channel, be.Input, be.Error())
}
func (*InputError) Error ¶ added in v1.2.0
func (e *InputError) Error() string
Error returns Msg: the channel, the input, and what went wrong. A secret input's value is redacted.
func (*InputError) Unwrap ¶ added in v1.2.0
func (e *InputError) Unwrap() []error
Unwrap exposes the Cause and the category sentinel, so errors.Is/As reach both the original recon error and ErrUsage/ErrInternal.
type InputLayer ¶ added in v1.2.0
type InputLayer[T any] struct { Name string Values T Set Presence // contains filtered or unexported fields }
InputLayer is one input channel's view of the inputs type T: the values it supplied (all other fields are zero) and which fields those are. Layers from rotini's channel parsers also carry unexported data that InputReport.Validate uses; a hand-built InputLayer participates in overlay and provenance but contributes nothing to validation. A nil or empty Set means the layer supplied nothing: overlaying it leaves every field as it was.
type InputReader ¶ added in v1.2.0
type InputReader struct {
// contains filtered or unexported fields
}
InputReader is the default multi-source input reader. It fills a command's typed inputs from argv (via a default Parser) and from the non-argv channels — environment variables, configuration files, and the leaf command's typed stdin payload — reconciled and decoded by recon. It is the engine behind Context.Inputs; Context.ArgvInputs and its siblings expose the channels individually.
reader := rotini.NewInputReader(settings)
var in WidgetCreateInputs
if err := reader.Read(rtx, &in); err != nil { /* handler owns it */ }
Env values fill the generated <Prefix>Env struct and config-file values <Prefix>Config. The two channels use independent registries, so an env var never leaks into a config field or the reverse. Flags may additionally fall back to env and config.
func NewInputReader ¶ added in v1.2.0
func NewInputReader(meta InputSettings) *InputReader
NewInputReader returns the default input reader, configured from the generated InputSettings.
func (*InputReader) Read ¶ added in v1.2.0
func (b *InputReader) Read(rtx *Context, out any) error
Read fills out, a non-nil pointer to the generated inputs struct, from every channel. Flag and argument validation runs once over the reconciled values, so a required flag is satisfiable from env or config and an env- or config-supplied value is still enum-checked.
It returns the first error: a *ParseError from the argv channel or an *InputError from the others, with the recon cause reachable via errors.As, or a *WiringError when a command declares config inputs but the program has no InputSettings.
Example ¶
InputReader.Read reconciles every declared channel in one call; here a flag unset in argv is read from its environment fallback.
def := Definition{
Name: "app", Handler: "App",
Flags: []FlagDef{
// The recon key gives the flag fallbacks: $SERVER_PORT, then any
// declared config files, then the default.
{Name: "port", Identifiers: []string{"--port"}, Type: "int", Default: "8080"},
},
}
var inputs struct {
App struct {
Flags struct {
Port int `rotini:"port" recon:"server.port"`
}
Arguments struct{}
}
}
os.Setenv("SERVER_PORT", "9090")
defer os.Unsetenv("SERVER_PORT")
rtx := NewContextFor(def, nil) // --port absent from argv
if err := NewInputReader(InputSettings{}).Read(rtx, &inputs); err != nil {
fmt.Println("bind:", err)
return
}
fmt.Println("port:", inputs.App.Flags.Port)
Output: port: 9090
type InputReport ¶ added in v1.2.0
type InputReport struct {
// contains filtered or unexported fields
}
InputReport is the merged provenance of one overlay: which layer won each field, every layer that set it (low → high), and validation over the merged values. The zero InputReport reports no fields, and its Validate returns nil.
func MergeInputsWithReport ¶ added in v1.2.0
func MergeInputsWithReport[T any](layers ...InputLayer[T]) (T, InputReport)
MergeInputsWithReport is MergeInputs plus the merged InputReport: which layer won each field, the full per-field history, and Validate over the merged result.
Example ¶
ExampleOverlayInputsP shows the merge and provenance contract with two hand-built layers: order is precedence, and the Report names the winner.
type inputs struct {
App struct {
Flags struct {
Color string `rotini:"color"`
}
}
}
var defaults, env inputs
defaults.App.Flags.Color = "blue"
env.App.Flags.Color = "teal"
merged, report := MergeInputsWithReport(
InputLayer[inputs]{Name: "defaults", Values: defaults, Set: Presence{"App.Flags.Color": {Layer: "defaults", Raw: "blue"}}},
InputLayer[inputs]{Name: "env", Values: env, Set: Presence{"App.Flags.Color": {Layer: "env", Raw: "teal"}}},
)
win, _ := report.Winner("App.Flags.Color")
fmt.Println(merged.App.Flags.Color, "from", win.Layer)
Output: teal from env
func (InputReport) Fields ¶ added in v1.2.0
func (r InputReport) Fields() []FieldPath
Fields returns every field any layer set, sorted.
func (InputReport) History ¶ added in v1.2.0
func (r InputReport) History(path FieldPath) []InputSource
History returns every layer that set path, low → high precedence; the last element is the winner.
func (InputReport) Validate ¶ added in v1.2.0
func (r InputReport) Validate() error
Validate runs the same declarative checks Parser.Parse applies — required, enum, constraints, flag groups and dependencies — with "explicitly set" meaning set by the argv layer. Run it after the overlay so a required flag satisfied by any layer passes.
It checks what the layers supplied, not the merged struct field by field:
- Presence rules fire on absence: a required input no layer supplied is an error.
- Value rules fire only on a supplied value: an unsupplied field's zero value is not checked against its enum or bounds.
A merge that omits Context.DefaultInputs can therefore yield an enum-constrained flag as "" without error. Hand-built layers contribute values but nothing to validate.
func (InputReport) Winner ¶ added in v1.2.0
func (r InputReport) Winner(path FieldPath) (InputSource, bool)
Winner returns the provenance of the layer that supplied path's final value.
type InputSettings ¶ added in v1.2.0
type InputSettings struct {
ConfigFiles []ConfigFile // per-command config_files sources, each tagged with its Scope; the input reader scopes them to the invoked chain (cascade, nearest-wins)
// EnvPrefix scopes every derived env-var name under "<EnvPrefix>_". Explicit variable
// names are exempt, and with a prefix set the unprefixed names no longer bind.
EnvPrefix string
// Sources are custom recon sources — a secrets manager, a remote config service —
// joined into the config precedence after the declared config_files, so explicit
// files beat ambient services. Codegen never emits one; the program appends its own.
// A per-input `file:` pin stays a config_files anchor and cannot name a custom
// source, and a source name colliding with a declared file is rejected.
Sources []recon.Source
// StdinSchemas maps a command's stdin payload type name ("<Prefix>Stdin") to a
// self-contained JSON Schema the input reader validates the decoded payload against.
StdinSchemas map[string]string
}
InputSettings is the generated descriptor the InputReader consumes to fill the non-argv input channels. It carries the document-level concerns the dispatch-time Definition omits.
type InputSource ¶ added in v1.2.0
type InputSource struct {
Layer string // the supplying layer's name: "defaults", "files", "env", "argv", "stdin", or custom
Raw string // the supplied text, a list's values joined with ", " whichever layer supplied it ("" when non-textual, e.g. a decoded stdin document); "[redacted]" for secrets
}
InputSource records which layer supplied a field's value and the raw text it supplied. Raw is pre-redacted for inputs the spec marks secret.
type Lifecycle ¶
type Lifecycle func(chain []Command, handlers []Handler) []LifecycleStep
Lifecycle is the run phase's planner: given the resolved chain and each command's Handler, index-aligned, it returns the ordered steps the engine executes. Handlers are resolved before it is called. See DefaultLifecycle.
A plan built from scratch should wrap each hook in AsCommand so Context.Command and Context.Inputs anchor on the hook's own command; an unwrapped hook reports the invoked command, which is wrong for a cascading hook.
type LifecycleStep ¶
type LifecycleStep struct {
Name string // diagnostic label, e.g. "cascading:app", "prerun:build", "run:build"
Do func(ctx context.Context, rtx *Context) // the forward hook
Undo func(ctx context.Context, rtx *Context) // the paired teardown; nil for none
}
LifecycleStep pairs one forward hook with its teardown; a plan is a slice of them.
Either half may be nil. A nil Undo is a step with no teardown, like the default plan's Run step; a nil Do is a teardown-only step. A step counts as begun when the engine reaches it, so a teardown-only step still unwinds.
func DefaultLifecycle ¶
func DefaultLifecycle(chain []Command, handlers []Handler) []LifecycleStep
DefaultLifecycle is rotini's run-phase plan, exported for a custom Lifecycle to wrap: one CascadingPreRun/CascadingPostRun pair per command, root → leaf, then the leaf's PreRun/PostRun pair, then the leaf's Run with no teardown. Every hook is wrapped in AsCommand.
type NoCascadingPostRun ¶ added in v1.2.0
type NoCascadingPostRun struct{}
NoCascadingPostRun is an embeddable no-op Handler.CascadingPostRun. NoHooks embeds all four no-ops at once.
func (NoCascadingPostRun) CascadingPostRun ¶ added in v1.2.0
func (NoCascadingPostRun) CascadingPostRun(ctx context.Context, rtx *Context)
CascadingPostRun does nothing.
type NoCascadingPreRun ¶ added in v1.2.0
type NoCascadingPreRun struct{}
NoCascadingPreRun is an embeddable no-op Handler.CascadingPreRun. NoHooks embeds all four no-ops at once.
func (NoCascadingPreRun) CascadingPreRun ¶ added in v1.2.0
func (NoCascadingPreRun) CascadingPreRun(ctx context.Context, rtx *Context)
CascadingPreRun does nothing.
type NoHooks ¶ added in v1.2.0
type NoHooks struct {
NoCascadingPreRun
NoPreRun
NoPostRun
NoCascadingPostRun
}
NoHooks embeds the four no-op hooks, for a hand-written handler:
type handlers struct{ rotini.NoHooks }
func (*handlers) Run(ctx context.Context, rtx *rotini.Context) { … }
A method declared on the outer type takes precedence over the promoted no-op.
NoHooks does not supply Run, so a handler with a missing or misspelled Run fails the `var _ rotini.Handler` assertion at compile time. Generated stubs embed the four types individually.
type NoPostRun ¶ added in v1.2.0
type NoPostRun struct{}
NoPostRun is an embeddable no-op Handler.PostRun. NoHooks embeds all four no-ops at once.
type NoPreRun ¶ added in v1.2.0
type NoPreRun struct{}
NoPreRun is an embeddable no-op Handler.PreRun. NoHooks embeds all four no-ops at once.
type Option ¶
type Option func(*Program)
Option is one configuration step as a value, applied by Program.With. WithDependency returns one; a program can define its own:
func devDefaults() rotini.Option {
return func(p *rotini.Program) { p.WithStdout(os.Stderr).WithoutSignalHandling() }
}
func WithDependency ¶ added in v1.2.0
func WithDependency[T any](dep Dependency[T], value T) Option
WithDependency returns an Option that registers value under dep for the whole program; it is the composable form of Program.WithDependency, for use with Program.With:
cmd.Program. With( rotini.WithDependency(tasks.Store, store), rotini.WithDependency(tasks.Client, client), ). WithVersion(version). Execute()
type Outcome ¶
type Outcome struct {
// Infos are [Context.RecordInfo] messages: neutral output, no bearing on the exit code.
Infos []string
// Successes are [Context.RecordSuccess] messages.
Successes []string
// Warnings are [Context.RecordWarning] values: non-fatal, never raising the exit code.
Warnings []error
// Errors are [Context.RecordError] values.
Errors []error
// Panics are recovered panics and rotini-detected faults, captured by the runtime; there
// is no record call for them.
Panics []*PanicError
}
Outcome is everything a run recorded, handed to the Reporter. Each slice is in recording order and is a copy. Context exposes no other way to read the records.
type OutputDef ¶ added in v1.2.0
type OutputDef struct {
// Type is the generated <Prefix>Output type: the whole output, or one item when the command
// writes a stream of them.
Type reflect.Type
// Schema is the shape as a self-contained JSON Schema, which [Program.WithOutputChecks],
// [Context.CheckOutput] and [DecodeOutput] check values against. "" when there is none.
Schema string
}
OutputDef is what a command declares it writes to stdout when it succeeds: the spec's `output:`, recorded by `rotini generate`. A nil OutputDef means the command declares none.
type Page ¶ added in v1.2.0
type Page struct {
// Name is the command path joined with "-" and lowercased ("taskr-add"): the name `man`
// looks the page up by, and the man page's file name without its extension.
Name string
// Path is the command path below the root, ["add"]; nil for the root command.
Path []string
// Content is the page itself: roff for a man page, markdown for a markdown page.
Content string
}
Page is one generated documentation page for one command: a man page or a markdown reference page. With the man feature on, codegen emits a ManPages function returning every command's man page; with the markdown feature on, MarkdownPages. Both list the visible commands in tree order, root first.
for _, p := range cmd.ManPages() {
path := filepath.Join(dir, p.Name+"."+cmd.ManSection)
if err := os.WriteFile(path, []byte(p.Content), 0o644); err != nil {
return err
}
}
type PanicError ¶
PanicError carries a panic recovered from a lifecycle hook to the reporter: Value is the value passed to panic, Stack the goroutine stack captured at recovery. Error renders Value alone.
It also carries faults rotini detects without a panic, such as a *WiringError or a resolver failure, with the error as Value and a nil Stack.
func (*PanicError) Error ¶
func (e *PanicError) Error() string
Error renders the panic value without the stack.
func (*PanicError) Unwrap ¶
func (e *PanicError) Unwrap() []error
Unwrap returns the panic value when it is an error, followed by ErrInternal, so a panic is CategoryInternal unless its error value classifies otherwise (CategoryOf tests ErrUsage first).
type ParseError ¶
type ParseError struct {
Kind ParseKind // what went wrong, for branching without matching Msg (zero = ParseKindUnspecified)
Msg string // the human-readable failure, opinion-free
Command string // the command in whose scope parsing failed ("" when not command-scoped)
Flag string // the flag involved, by the identifier or label used ("" when not flag-related)
Token string // the offending argv token or value ("" when none)
Candidates []string // the vocabulary Token failed against — sibling commands, declared flags, enum members (nil when none applies)
}
ParseError is a parse-time failure caused by bad input. Its message carries no suggestions or usage text; the structured fields let a handler compose its own response: switch on Kind, pass Token and Candidates to a Suggestor, or render help for Command. It unwraps to ErrUsage (ErrInternal for ParseKindInternal), so CategoryOf can classify it. The default reporter exits 1; a program wanting exit code 2 for usage errors maps it in its own reporter (see Category).
var pe *rotini.ParseError
if errors.As(err, &pe) {
switch pe.Kind {
case rotini.ParseKindUnknownFlag, rotini.ParseKindUnknownCommand:
// pe.Token + pe.Candidates feed a Suggestor's "did you mean"
case rotini.ParseKindMissingRequired:
// prompt, or point at the help for pe.Command
}
}
func (*ParseError) Error ¶
func (e *ParseError) Error() string
Error renders the parse failure as a single, user-facing line.
func (*ParseError) Unwrap ¶
func (e *ParseError) Unwrap() error
Unwrap returns the category sentinel: ErrInternal for ParseKindInternal and ErrUsage for every other kind.
type ParseKind ¶
type ParseKind int
ParseKind classifies a *ParseError so a reporter can branch on the failure without matching the message. Every kind is the end-user's to fix except ParseKindInternal, a misuse of the parser API by the author.
const ( // ParseKindUnspecified is the zero value: a [*ParseError] whose construction // site did not classify it (a hand-built error that sets no Kind). ParseKindUnspecified ParseKind = iota ParseKindUnknownFlag // an argv token looked like a flag no command on the chain declares ParseKindUnknownCommand // a stray positional on a branch-only command (a mistyped sub-command) ParseKindNeedsValue // a value-taking flag was given no value ParseKindInvalidValue // a value could not be coerced/resolved (bad type, unreadable @file, malformed map, value on a no-value flag) ParseKindEnumViolation // a value was not one of a declared enum's members ParseKindConstraintViolation // a declared bound or flag-group/dependency rule was violated ParseKindMissingRequired // a required flag or argument was absent ParseKindNoArguments // a positional was given to a command that accepts none ParseKindTooManyArguments // more positionals than the command's declared (non-variadic) arity ParseKindInternal // a parser API misuse: nil parser/context, a bad out argument, or an inputs type that does not describe the running command )
The parse failure kinds.
type Parser ¶
type Parser struct{}
Parser is rotini's argv parser: it parses and validates the command line against what the resolved chain declares (GNU/POSIX grammar, typed coercion, enum and constraint checks), failing with a *ParseError.
Parsing is opt-in: a CLI that wants raw argv reads Context.Argv instead. Supply a parser with Program.WithParser; a handler reads it with Context.Parser:
parser := rtx.Parser() var in MycliInputs err := parser.Parse(rtx, &in)
func (*Parser) Parse ¶
Parse binds the running command's arguments into out — a non-nil pointer to the generated inputs struct — from the resolved chain and raw argv on rtx, in the json.Unmarshal style:
var in MycliInputs
if err := parser.Parse(rtx, &in); err != nil { /* handler owns it */ }
It applies declared defaults, fills out by reflection from the `rotini:"…"` struct tags, then validates. Built-ins and any encoding.TextUnmarshaler are coerced, and a trailing []string absorbs the remaining positionals. Parse does not consult env or config fallbacks; Context.Inputs does.
It returns a *ParseError when out is not a non-nil pointer, a flag is unknown or missing its value, a value cannot be coerced, a required input is absent, a value falls outside a declared enum, or any declared constraint, flag group or flag dependency is violated.
Unlike Context.Inputs and the per-channel layer methods, Parse does not check that out describes the running command: it binds what fits and leaves the rest zeroed, so one struct can span a whole tree. A handler reading its own inputs should use Context.Inputs.
Example ¶
Parser.Parse fills the generated input struct from argv alone, with typed coercion, defaults, and enum and constraint checks, failing with a *ParseError.
def := Definition{
Name: "app", Handler: "App",
Commands: []CommandDef{{
Name: "deploy", Handler: "AppDeploy",
Arguments: []ArgDef{{Name: "service", Type: "string", Required: true}},
Flags: []FlagDef{
{Name: "env", Identifiers: []string{"--env"}, Type: "string", Default: "dev", Enum: []string{"dev", "prod"}},
{Name: "loud", Identifiers: []string{"--loud", "-l"}, Type: "count"},
},
}},
}
// The shape codegen emits: one field per command on the resolved path.
var inputs struct {
App struct {
Flags struct{}
Arguments struct{}
}
Deploy struct {
Flags struct {
Env string `rotini:"env"`
Loud int `rotini:"loud"`
}
Arguments struct {
Service string `rotini:"service"`
}
}
}
rtx := NewContextFor(def, []string{"deploy", "api", "--env", "prod", "-ll"})
if err := NewParser().Parse(rtx, &inputs); err != nil {
fmt.Println("usage:", err)
return
}
fmt.Printf("%s → %s (verbosity %d)\n",
inputs.Deploy.Arguments.Service, inputs.Deploy.Flags.Env, inputs.Deploy.Flags.Loud)
Output: api → prod (verbosity 2)
type PathFromDef ¶
type PathFromDef struct {
Flag string // logical flag name searched across the resolved chain
Env string // environment variable read directly; comma-separated names: the first one set wins
}
PathFromDef names the runtime inputs that supply a ConfigFile's path — the declarative two-phase parse, where argv and env are read first and the file channel then opens whatever they pointed at. Precedence: the flag set on argv, then the env variable, then the flag's default, then the entry's own path or discover. A path supplied this way must exist.
type PluginDef ¶ added in v1.2.0
type PluginDef struct {
Name string
Aliases []string
Summary string // one-line description (completion candidates carry it as "name\tsummary")
Binary string // expected executable name, e.g. "kubectl-ctx"
Timeout time.Duration // 0 means no timeout
}
PluginDef describes a co-located sub-command, kubectl/git plugin style: invoking it execs the sibling Binary with the remaining arguments passed through.
type PluginDiscoveryDef ¶ added in v1.2.0
type PluginDiscoveryDef struct {
Prefix string // executable-name prefix, e.g. "acme-"
Hidden bool // dispatch discovered plugins but omit them from completion listings
}
PluginDiscoveryDef enables plugin discovery on a command: an unmatched token execs the sibling binary Prefix+<token>, and `<Prefix>*` executables are offered as completion candidates unless Hidden. A nil pointer means discovery is off for that command.
type PluginDispatch ¶ added in v1.2.0
type PluginDispatch struct {
Def PluginDef
Args []string
Dir string
// Discovered marks a plugin-discovery dispatch rather than a declared plugin, which
// decides the error category when the binary cannot be resolved.
Discovered bool
}
PluginDispatch is a resolved plugin invocation, declared or discovered: Def.Binary run with Args. Dir is the command's plugin path, searched after the host binary's own directory and before PATH; empty means none.
type PluginError ¶ added in v1.2.0
type PluginError struct {
Name string // the declared plugin name (or discovery token)
Binary string // the plugin binary that was sought or spawned
Kind PluginErrorKind // what went wrong
Timeout time.Duration // the elapsed deadline, for Kind == PluginTimeout (else 0)
Cause error // the underlying OS/exec error, reachable via errors.As (may be nil)
Msg string // the human-readable failure
// contains filtered or unexported fields
}
PluginError reports a failure by rotini to carry out a plugin dispatch. The plugin's own non-zero exit is not a PluginError; its exit code passes through unchanged.
var re *rotini.PluginError
if errors.As(err, &re) && re.Kind == rotini.PluginTimeout {
fmt.Fprintf(os.Stderr, "%s timed out after %s\n", re.Name, re.Timeout)
}
A missing binary is CategoryUsage when discovered (a mistyped token) and CategoryInternal when declared (an install problem); a spawn failure is CategoryInternal; a timeout is CategoryNone.
func (*PluginError) Error ¶ added in v1.2.0
func (e *PluginError) Error() string
Error renders the plugin-dispatch failure as a single, user-facing line.
func (*PluginError) Unwrap ¶ added in v1.2.0
func (e *PluginError) Unwrap() []error
Unwrap exposes the Cause (when present) and the category sentinel (ErrUsage/ErrInternal) so errors.Is/As reach both; a timeout adds no sentinel, so CategoryOf reports CategoryNone.
type PluginErrorKind ¶ added in v1.2.0
type PluginErrorKind int
PluginErrorKind classifies a plugin-dispatch failure: the plugin binary could not be located, it exceeded its declared timeout, or it could not be spawned.
const ( // PluginNotFound: no binary was found next to the executable, in the // plugin path, or on PATH. PluginNotFound PluginErrorKind = iota // PluginTimeout: the plugin ran past its declared timeout and was killed. PluginTimeout // PluginStartFailed: the binary was found but could not be started (a fork, // exec or pipe failure). The plugin's own non-zero exit is not an error kind. PluginStartFailed )
func (PluginErrorKind) String ¶ added in v1.2.0
func (k PluginErrorKind) String() string
String renders the kind as a short, stable label.
type Presence ¶
type Presence map[FieldPath]InputSource
Presence maps each field a layer supplied to its provenance. Overlay copies only these fields, so a layer's absent fields never overwrite a lower layer's values.
type Program ¶
type Program struct {
// contains filtered or unexported fields
}
Program is a rotini CLI ready to run: the compiled command tree (Definition), the handlers that implement it, and the seams around them. The generated entrypoint builds one with NewProgram and calls Program.Execute. Every With method returns the receiver, so calls chain.
The surface groups into eight jobs:
- run — Program.Execute exits, Program.Run returns the code, Program.RunContext scopes one invocation, Program.Complete answers a completion request in a CompletionFormat
- streams — Program.WithStdin, Program.WithStdout, Program.WithStderr
- process — Program.WithExit, Program.WithArgs, Program.WithContext, Program.WithSignals, Program.WithoutSignalHandling, Program.WithCompletion
- failure — Program.WithTeardownOnPanic, Program.WithPanicRecover, Program.WithReporter
- output — Program.WithOutputChecks checks every output written with Context.WriteOutput against the command's declared contract
- the program's dependencies — Program.WithDependency, or Program.With with the WithDependency option to register several at once
- rotini's own seams — Program.WithVersion, Program.WithParser, Program.WithInputReader, and the two the generated code sets, Program.WithInputSettings and Program.WithHelp
- phase replacement — Program.WithResolver, Program.WithLifecycle
A setting that rotini reads (the runtime or generated code) is a typed option on the Program; a setting only the program's own code reads is a dependency. Typed options cannot be shadowed by a dependency name, and a wrong type is a compile error.
A Program is reusable: Program.Run dispatches one invocation and returns, giving each call a fresh Context.
The With methods are not synchronized: configure before the first run. Applied between sequential runs, they take effect on the next one.
Methods do not check for a nil receiver. The zero value is not usable; start from NewProgram.
func NewProgram ¶
func NewProgram(def Definition, handlers any) *Program
NewProgram wires a generated command tree and its aggregate handler set to the runtime. Dispatch calls the method of handlers named by each Command.Handler in def to obtain that command's Handler. A nil handlers value is accepted here; a run that reaches dispatch then fails with a *WiringError.
func (*Program) Complete ¶ added in v1.1.1
func (p *Program) Complete(words []string, format CompletionFormat) (int, error)
Complete answers one shell-completion request in the given format and returns the exit code, the completion counterpart of Program.Run. It serves hosts that do not call the hidden __complete entry; kubectl, for example, runs a separate kubectl_complete-<plugin> with only the plugin's words:
if strings.Contains(filepath.Base(os.Args[0]), "_complete-") {
code, _ := cmd.Program.Complete(os.Args[1:], rotini.PluginCompletion)
os.Exit(code)
}
words are the words after the program's own name; the last is the word being completed, empty when the cursor starts a new one, and no words at all completes a new first word. The answer is what __complete computes, written to the program's stdout by format. A nil format uses rotini's own format, the __complete default, which is private to rotini's generated scripts.
The exit code is 0, or 1 when the format fails to write, with its error.
func (*Program) Execute ¶
Execute runs the arguments set by Program.WithArgs (default os.Args[1:]) through Program.Run and passes the resulting code to the exit action (os.Exit by default; see Program.WithExit).
The returned error joins every Context.RecordError value and every captured fault with errors.Join, so errors.Is and errors.As reach each one. It is returned only when the exit action returns; under os.Exit the process ends first. The reporter has already reported the outcome by then; the error lets an embedding host act on the failure.
Signals ¶
With no Program.WithContext, rotini traps os.Interrupt and syscall.SIGTERM. The first signal halts the lifecycle as Context.HaltWithCode does (forward progress stops, every begun teardown hook runs) with exit code 128+signum. A second signal calls the exit action with 130 immediately. See Program.WithoutSignalHandling and Program.WithSignals.
func (*Program) Run ¶
Run dispatches one invocation of argv and returns its exit code and error (see Program.Execute). It resolves the invoked command, executes a declared plugin if one was selected, and otherwise runs the lifecycle; inputs are parsed only when a handler calls Context.Inputs. Run never ends the process.
Each call gets a fresh Context. Dependencies registered with Program.WithDependency are seeded into every run; one set with Context.SetDependency stays local to its run.
With no supplied context, Run installs and removes the signal trap on every call (about 30µs). A host dispatching in a loop uses Program.RunContext or Program.WithoutSignalHandling.
Concurrency ¶
Run is safe for concurrent use once configuration is complete. Each run's records, exit state and run-local dependencies are its own. Shared, and synchronized by the host:
- The handlers value given to NewProgram, whose methods are called from each run's goroutine.
- The program's streams.
With no supplied context, each concurrent run installs its own trap and all of them observe a signal. A concurrent host passes its own context (Program.RunContext) or disables the trap with Program.WithoutSignalHandling.
func (*Program) RunContext ¶
RunContext is Program.Run under ctx, for this invocation only; the program is not modified. As with Program.WithContext, supplying a context leaves signal handling to the caller unless Program.WithSignals was set. A nil ctx returns exit code 1 and an ErrInternal error without running.
func (*Program) With ¶
With applies each Option in order and returns the program:
cmd.Program. With( rotini.WithDependency(tasks.Store, store), rotini.WithDependency(tasks.Client, client), ). WithVersion(version). Execute()
A later Option registering the same dependency replaces an earlier one. A nil Option is skipped.
func (*Program) WithArgs ¶
WithArgs sets the argument vector Program.Execute runs (defaults to os.Args[1:]).
Only Execute reads it. Program.Run and Program.RunContext use the argv they are passed, so `p.WithArgs(x).Run(nil)` runs with no arguments.
A nil args is ignored; pass []string{} to run with none.
func (*Program) WithCompletion ¶ added in v1.1.1
func (p *Program) WithCompletion(format CompletionFormat) *Program
WithCompletion sets the CompletionFormat the hidden __complete entry answers in, in place of rotini's own. It serves a plugin whose host completes it by calling the plugin's __complete and reading the host's format, as the Docker CLI (`docker-<name> __complete <name> …`) and the Flux CLI (`flux-<name> __complete …`) do with the format PluginCompletion writes:
cmd.Program.WithCompletion(rotini.PluginCompletion).Execute()
rotini's generated completion scripts read rotini's own format, so a standalone CLI leaves this unset. A host that runs a separately named completer without a __complete word, such as kubectl's kubectl_complete-<name>, is served by calling Program.Complete from main.
A nil format restores rotini's own.
func (*Program) WithContext ¶
WithContext sets the base context threaded to every lifecycle hook, the reporter, and any plugin exec, so a caller can cancel or time-bound the whole run. A nil context is ignored.
Cancellation is cooperative: it does not preempt a running hook, but once the context is canceled no further forward hook starts, and the teardown of every begun setup hook runs in reverse. ExitCause attaches an exit code; without one the code is resolved as usual.
Supplying a context disables rotini's signal trap by default, leaving signals to the caller (typically via signal.NotifyContext). Program.WithSignals re-enables the trap on top of a supplied context; Program.WithoutSignalHandling disables it without one.
func (*Program) WithDependency ¶ added in v1.2.0
func (p *Program) WithDependency[T any](dep Dependency[T], value T) *Program
WithDependency registers value under dep for the whole program, so every run sees it, and returns p for chaining:
cmd.Program. WithDependency(tasks.Store, store). WithVersion(version). Execute()
A later registration under the same name replaces an earlier one.
Go infers T from both arguments, so for a handle declared over an interface the value must already have that interface type. Convert the value, or name the type:
var Store = rotini.NewDependency[store.Store]("taskr.store") // an interface
p.WithDependency(Store, store.Store(store.NewMem()))
p.WithDependency[store.Store](Store, store.NewMem())
The dependency namespace belongs to the application. rotini's own seams (input reader, parser, version, help) are typed Program options, so no dependency name can shadow them.
func (*Program) WithExit ¶
WithExit overrides what Program.Execute does with the resolved exit code (default os.Exit). The same function receives the forced exit code when a second trapped signal arrives. A nil function is ignored.
When fn returns, Execute returns the run's error to its caller; under os.Exit it never does. This makes an end-to-end test or an embedding host possible:
code := -1
err := cmd.Program.
WithArgs(argv).WithStdout(&out).WithStderr(&errs).
WithExit(func(c int) { code = c }).
Execute()
func (*Program) WithHelp ¶
WithHelp sets where Context.Help finds a command's help page. The generated NewProgram passes its own Help function. A nil help is ignored.
Help is a program-level seam so that a command composed from another spec prints the page of the program it runs in, with that program's full command path and inherited flags.
func (*Program) WithInputReader ¶ added in v1.2.0
func (p *Program) WithInputReader(fn func(InputSettings) *InputReader) *Program
WithInputReader replaces the input reader that Context.Inputs and the per-channel methods use. fn receives the program's InputSettings, so a replacement starts from the generated descriptor and keeps the declared configuration sources:
p.WithInputReader(func(meta rotini.InputSettings) *rotini.InputReader {
meta.Sources = append(meta.Sources, mySource)
return rotini.NewInputReader(meta)
})
A nil fn is ignored.
func (*Program) WithInputSettings ¶ added in v1.2.0
func (p *Program) WithInputSettings(meta InputSettings) *Program
WithInputSettings supplies the generated input descriptor: the configuration sources, the env prefix and the stdin schemas Context.Inputs reads from. The generated NewProgram calls it; a hand-built program calls it to enable those channels.
func (*Program) WithLifecycle ¶
WithLifecycle overrides the run phase's plan: which hooks run, in what pairing and order (see Lifecycle and DefaultLifecycle). Halting, the reverse teardown unwind, panic handling and exit-code resolution are unchanged. A nil lifecycle is ignored.
Example ¶
A lifecycle that wraps DefaultLifecycle and swaps the cascading pairs, so CascadingPostRun unwinds root→leaf. Halting, unwind and panic handling are unchanged.
NewProgram(exampleDef(), exHandlers{}).
WithArgs([]string{"status"}).
WithExit(func(int) {}).
WithLifecycle(func(chain []Command, hs []Handler) []LifecycleStep {
steps := DefaultLifecycle(chain, hs)
for i, j := 0, len(hs)-1; i < j; i, j = i+1, j-1 {
steps[i].Undo, steps[j].Undo = steps[j].Undo, steps[i].Undo
}
return steps
}).
Execute()
Output: app.CascadingPreRun status.CascadingPreRun status.PreRun status.Run status.PostRun app.CascadingPostRun status.CascadingPostRun
func (*Program) WithOutputChecks ¶ added in v1.2.0
WithOutputChecks makes every Context.WriteOutput and Context.WriteOutputItem call check its value against the command's declared output schema before writing it. A value that does not match is an internal error naming each field at fault, and nothing is written. It is off by default; enable it in tests or debug builds:
p := cmd.NewProgram(cmd.Handlers()).WithOutputChecks()
Only output written through WriteOutput and WriteOutputItem is checked. To check captured stdout, use DecodeOutput.
func (*Program) WithPanicRecover ¶
WithPanicRecover controls where a hook panic goes. The default, true, recovers it and routes it to the reporter as a *PanicError. False re-raises it to the caller, for an embedding host's own recover, a crash reporter, or debugging.
Combined with Program.WithTeardownOnPanic:
- recover=true: the panic reaches the reporter; teardown runs per WithTeardownOnPanic.
- recover=false, teardown=true: teardown runs, then the panic is re-raised (its stack starts at the re-raise, not the original site).
- recover=false, teardown=false: the hook runs unguarded, so the panic propagates immediately with its original stack and no teardown.
Only panics on the hook goroutine are recovered; a panic in a goroutine a handler started crashes the process.
func (*Program) WithParser ¶
WithParser replaces the Parser that Context.Parser returns. Context.Inputs and the InputReader always use the default parser, so this changes only what a handler gets from Context.Parser. A nil parser is ignored.
func (*Program) WithReporter ¶ added in v1.2.0
WithReporter sets the program's outcome reporter. See Reporter.
The default prints infos, warnings, errors, panics, then successes (infos and successes to stdout, the rest to stderr), and applies an exit floor: a recorded error or panic exits 1 unless a handler already set a non-zero code. A custom reporter owns the exit code entirely.
A nil fn restores the default reporter.
func (*Program) WithResolver ¶
WithResolver overrides the resolve phase: argv to invocation target, plus the argv the parsers later see. A resolver that rewrites tokens should rewrite argv and delegate to DefaultResolver, so routing and parsing agree. A resolver error is reported as a fault (CategoryInternal unless the error carries a category) and fails the run.
Completion walks the Definition, so an alias known only to the resolver is dispatchable but not completed. A nil resolver is ignored.
Example ¶
A resolver that adds a routing alias: "st" rewrites to "status" and delegates to DefaultResolver, so routing and parsing agree on the rewritten argv. A resolver alias is not completed; declare aliases in the spec for that.
NewProgram(exampleDef(), exHandlers{}).
WithArgs([]string{"st"}).
WithExit(func(int) {}).
WithResolver(func(def Definition, argv []string) (Resolution, error) {
if len(argv) > 0 && argv[0] == "st" {
argv = append([]string{"status"}, argv[1:]...)
}
return DefaultResolver(def, argv)
}).
Execute()
Output: app.CascadingPreRun status.CascadingPreRun status.PreRun status.Run status.PostRun status.CascadingPostRun app.CascadingPostRun
func (*Program) WithSignals ¶
WithSignals enables rotini's signal trap for the given signals, whether or not a context was supplied. With a supplied context the trap cancels a derived child, never the caller's context. The first signal halts the run (teardown runs, exit 128+signum); a second exits immediately with 130 through the exit action. An empty list is ignored; use Program.WithoutSignalHandling to disable the trap.
func (*Program) WithStderr ¶
WithStderr overrides the program's standard error (default os.Stderr), where the default reporter writes warnings, errors and panics. A nil writer is ignored.
func (*Program) WithStdin ¶
WithStdin overrides the program's standard input (default os.Stdin): the reader exposed as Context.Stdin and decoded for stdin inputs. A nil reader is ignored.
func (*Program) WithStdout ¶
WithStdout overrides the program's standard output (default os.Stdout), where the runtime writes completion candidates and the default reporter writes infos and successes. A nil writer is ignored.
func (*Program) WithTeardownOnPanic ¶
WithTeardownOnPanic controls whether teardown runs when a hook panics. The default, true, halts forward progress and still runs the teardown of every begun setup hook, as deferred calls run during a panic. False skips the remaining teardown, as Context.Exit does.
Where the panic goes is controlled separately by Program.WithPanicRecover.
func (*Program) WithVersion ¶
WithVersion sets the program's version string, read with Context.Version and printed by the generated version command and --version flag.
var version = "0.0.0" // go build -ldflags "-X main.version=1.2.3" cmd.Program.WithVersion(version).Execute()
func (*Program) WithoutSignalHandling ¶
WithoutSignalHandling disables rotini's signal trap; of it and Program.WithSignals, the later call wins. rotini still owns the run context but calls no signal.Notify, so the program's own handling is the only one (signal.Notify registrations are additive).
rotini exposes no cancel in this mode, so the program's signal handler cannot halt the run gracefully; for that, use Program.WithContext with signal.NotifyContext.
type Reporter ¶ added in v1.2.0
Reporter is the program's outcome reporter. The runtime calls it once per run, after the lifecycle and its teardown finish, when the Outcome is not empty.
The reporter decides what to print and where, and sets the final exit code: Context.Exit inside it overrides the code the lifecycle set, and Context.HaltWithCode is a no-op.
The error Program.Run returns is built before the reporter is called, so modifying the Outcome does not change it. Records made inside the reporter are dropped.
A panic inside a reporter is not recovered. A reporter that can fail handles its own failure, for example by writing to rtx.Stderr and setting a code with Context.Exit.
func StructuredReporter ¶ added in v1.2.0
StructuredReporter returns a Reporter for programs whose output scripts read. When structured reports true for the run, it writes each thing the run recorded to stderr as one JSON object per line, and leaves stdout alone, so a partial result there is never interleaved with an error:
{"error":{"message":"missing required input: <title>","category":"usage","kind":"missing-required","command":"taskr add","exit_code":1}}
{"warning":{"message":"the cache is stale","command":"taskr list"}}
Infos and successes are written the same way, as {"info":{…}} and {"success":{…}}, rather than to stdout. A field is present only when rotini knows it: kind, flag and token come from a *ParseError, kind from a *PluginError too. Each line's shape is described by schema-error.json in the rotini repository, and the contract document includes it.
When structured reports false, or is nil, it reports as the default reporter does. Either way the exit code follows the default reporter's rule.
structured is the program's own rule, typically whether its format flag asks for json. The run may have failed in parsing, so the rule should read Context.Argv, not validated inputs:
cmd.Program.WithReporter(rotini.StructuredReporter(func(rtx *rotini.Context) bool {
return slices.Contains(rtx.Argv, "--json")
})).Execute()
type Resolution ¶
type Resolution struct {
// Chain is the resolved command path the run phase dispatches (when
// Plugin is nil). It must be non-empty — the root command is always there.
Chain []Command
// Plugin, when non-nil, short-circuits local dispatch: the runtime execs this binary
// instead, stdio passed through and context honored.
Plugin *PluginDispatch
// Argv is the vector the run phase exposes as [Context.Argv]. A resolver that rewrites
// tokens returns the rewritten vector here so parsing agrees with its routing; nil keeps
// the original argv.
Argv []string
}
Resolution is the outcome of the resolve phase: the invoked command path (root → leaf), or a plugin dispatch that replaces local execution.
func DefaultResolver ¶
func DefaultResolver(def Definition, argv []string) (Resolution, error)
DefaultResolver is rotini's resolve phase, exported for a custom Resolver to wrap. It descends sub-commands by name or alias, skips flags and their values, stops at the first positional, and diverts to a plugin dispatch for declared and discovered plugins. It does not validate input and never returns an error.
type Resolver ¶
type Resolver func(def Definition, argv []string) (Resolution, error)
Resolver is the resolve phase: it matches argv against the Definition to decide what this invocation targets. An error is reported as a fault and fails the run. See DefaultResolver.
The Definition is passed by value, but its slices are the program's own and shared by every run. A resolver must treat it as read-only; writing through it changes every later run of the Program.
type Suggestor ¶
type Suggestor struct {
// contains filtered or unexported fields
}
Suggestor ranks a possibly-mistyped token against a list of candidates by optimal string alignment distance. It prints nothing and holds no state beyond its configuration. rotini never suggests on its own; a program opts in by calling a Suggestor, typically from its reporter with Suggestor.For:
var suggestor = rotini.NewSuggestor()
func reporter(_ context.Context, rtx *rotini.Context, out rotini.Outcome) {
for _, err := range out.Errors {
fmt.Fprintf(rtx.Stderr, "Error: %s\n", err)
if hits := suggestor.For(err); len(hits) > 0 {
fmt.Fprintf(rtx.Stderr, "Did you mean %q?\n", hits[0])
}
}
}
Matching is always case-insensitive. A configured Suggestor is safe for concurrent use; finish configuring it before sharing it across goroutines.
func NewSuggestor ¶
func NewSuggestor() *Suggestor
NewSuggestor returns a Suggestor with a minimum score of 0.75 and at most 3 results. The With methods return the receiver, so they chain. The zero value is not usable.
func (*Suggestor) Closest ¶
Closest returns the single best suggestion for input, or ok=false when input exactly matches a candidate or nothing clears the minimum score.
func (*Suggestor) For ¶
For returns the suggestions for the token a *ParseError rejected, ranked against its Candidates, nearest first. It returns nil for an error that is not a *ParseError, one carrying no Token or no Candidates, and one whose token is near nothing.
func (*Suggestor) Suggest ¶
Suggest ranks candidates by nearness to input, nearest first, keeping those at or above the minimum score and at most the configured number of results.
An input that matches a candidate byte-for-byte yields nil; a case-only difference ("--VERBOSE" for "--verbose") is treated as a typo.
Ties prefer the candidate sharing the longer common prefix with input, then break lexicographically, so the result is deterministic. Empty candidates are skipped and duplicates collapse to the first occurrence.
func (*Suggestor) WithMaxResults ¶
WithMaxResults caps how many suggestions Suggestor.Suggest and Suggestor.For return (default 3). Zero or less means no cap.
func (*Suggestor) WithMinScore ¶
WithMinScore sets the similarity a candidate must reach to be offered, in [0,1]. A value outside that range is ignored. Below about 0.7 the ranker starts offering unrelated words; above 0.75 it no longer matches one-edit typos of four-character commands.
type WiringError ¶
type WiringError struct {
Command string // the command whose handler wiring is broken
Handler string // the handler method name the Definition referenced
Msg string // the human-readable failure
}
WiringError reports that the generated Definition and the handler set are out of sync — a resolved command names a handler method that does not exist, or whose return value does not implement Handler. It is always CategoryInternal.
Command and Handler are empty when NewProgram was given a nil handlers value.
func (*WiringError) Error ¶
func (e *WiringError) Error() string
Error renders the mismatch as a single line.
func (*WiringError) Unwrap ¶
func (e *WiringError) Unwrap() error
Unwrap reports ErrInternal: a wiring mismatch is the author's bug, never the user's.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
cmd
|
|
|
rotini
command
|
|
|
internal
|
|
|
cmd/rotini
Code generated by rotini; DO NOT EDIT.
|
Code generated by rotini; DO NOT EDIT. |
|
codegen
Package codegen turns a CLI definition (a .rotini.spec and .rotini.conf) into a Go program.
|
Package codegen turns a CLI definition (a .rotini.spec and .rotini.conf) into a Go program. |