Documentation
¶
Overview ¶
Package codegen turns a CLI definition (a .rotini.spec and .rotini.conf) into a Go program. It is a small compiler, and the package is laid out as its pipeline.
Pipeline ¶
The Processor holds rotini's own spec and conf JSON Schemas and runs the author's files through four stages:
reconcile → validate → lint → generate
Each stage in turn:
- reconcile reads and decodes the spec and conf into the in-memory model.
- validate checks the model against rotini's schemas, and its declared version against this binary.
- lint applies the rules a JSON Schema cannot express.
- generate resolves the model into a program and writes it.
Processor.Generate and Processor.Validate read as the whole process.
File layout ¶
Each file belongs to one stage, named by its prefix:
processor* the Processor and the run/watch loop schema* rotini's JSON Schemas, the generated model types, reading and walking them reconcile* reading and decoding the spec and conf, with source positions validate* schema and version validation, and the problem type lint* lint rules for the spec and conf, composition, suggestions generate* the generate stage initialize.go `rotini initialize`: scaffold a new CLI, then generate it
Generate ¶
A spec and conf resolve into a [program]: the command tree, the output layout and the target module. program.generate runs the emit steps in order:
emit schemas → emit contract → emit models file → emit cmd file → emit feature outputs → emit handler stubs → emit entrypoint → prune orphans → audit handler hooks
The last step reads the author's handler code instead of writing: it reports methods whose names nearly match a lifecycle hook, which the generated `var _ rotini.Handler` assertion cannot catch, and handlers that acquire another command's inputs type. The runtime is an imported library and is never emitted.
Index ¶
- func DryRunEnv(specPath, confPath string) string
- func ResolvePaths(specPath, confPath string) (spec, conf string, err error)
- type ArgumentInput
- type BaseSchema
- type Command
- type Conf
- type ConfigInput
- type ConfigurationFile
- type ConfigurationFileDiscover
- type ContractConfig
- type EnvInput
- type ExitStatusEntry
- type Feature
- type FlagDependency
- type FlagGroup
- type FlagInput
- type GenerateConfig
- type GenerateDryRunFn
- type GenerateFn
- type HandlerSource
- type HelpHeadings
- type InitializeDryRunFn
- type InitializeFn
- type Initialized
- type InputSchema
- type InputSchemaComplete
- type Inputs
- type OutputSchemasConfig
- type PackageConfig
- type Planned
- type PluginDiscovery
- type PluginSpec
- type Processor
- func (p *Processor) Generate(specPath, confPath string, watch bool, ...) error
- func (p *Processor) GenerateDryRun(specPath, confPath string, onNotices func(notices []error)) (Planned, error)
- func (p *Processor) Initialize(name, format string, force bool) (Initialized, error)
- func (p *Processor) InitializeDryRun(name, format string, force bool) (Initialized, error)
- func (p *Processor) Validate(specPath, confPath string, watch bool, failMode string, ...) error
- type Schema
- type SchemaConfig
- type SchemasConfig
- type Spec
- type StdinSpec
- type ValidateConfig
- type ValidateFn
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func DryRunEnv ¶ added in v1.3.0
DryRunEnv returns the environment variable the conf beside specPath (or at confPath) names in generate.dry_run_env, or "" when it names none. A conf that can't be read names none; generate then reports the problem itself.
func ResolvePaths ¶
ResolvePaths resolves the spec and conf paths the pipeline would read. An empty specPath discovers the first .rotini.spec.* in the working directory; an empty confPath discovers the first .rotini.conf.* beside the spec, and conf is "" when there is none. Given paths are returned unchanged and unchecked; discovered paths are made relative for display.
Types ¶
type ArgumentInput ¶
type ArgumentInput struct {
// Deprecation message. The argument is annotated as deprecated in generated help, and supplying it is reported at run time by rotini.Deprecations with this message (a data feed for the handler; rotini itself prints nothing).
Deprecated string `json:"deprecated,omitempty"`
// When true, the argument is omitted from generated help (it still parses on the command line).
Hidden bool `json:"hidden,omitempty"`
// Logical name for the argument
Name string `json:"name"`
// Type definition and input-level metadata (type, required, default, enum, nullable, constraints)
Schema *InputSchema `json:"schema,omitempty"`
// Short one-liner shown next to this argument in the Arguments section of generated help.
Summary string `json:"summary,omitempty"`
}
type BaseSchema ¶
type BaseSchema struct {
// Reference to a named schema in the root command's "schemas" map, by its name (`$ref: DB`) or as a JSON pointer (`$ref: '#/schemas/DB'`). The two mean the same; the pointer is what JSON Schema tooling reads. Resolved when you generate. A reference to a named scalar schema brings that schema's enum, pattern (and pattern_message), lengths and bounds with it, wherever the input leaves them unset.
//
// On a flag, a named object schema makes the flag object-valued: its generated field is the schema's struct, and it takes any of:
//
// - JSON — `--db '{"host":"h","port":5}'`
// - key=value pairs — `--db host=h,port=5`; dotted keys nest (`pool.max=9`), a repeated key appends to a list field, and quotes keep a comma (`host="a,b"`)
// - a JSON or YAML file, with `from: [file]` — `--db @db.yaml`
// - one field per flag — `--db.host=h --db.port=5`
//
// Occurrences merge in the order they appear, a later key winning. Declared as `type: array, items: {$ref: …}`, the flag is a list of objects and each occurrence is one element (per-field flags do not apply). A flag's environment fallback takes the same spellings (DB='{"host":"h"}'), and its configuration-file fallback is the object as a mapping (or a list of mappings).
//
// Every spelling is validated against the named schema, with the same validator a stdin payload of that shape meets, and errors name the flag and the key (`--db: port: value must be >= 1`, `unknown key "bogus"`). A `default` is written as a mapping (a list of mappings for a list) and is checked against the schema by `rotini validate`; a supplied value replaces it rather than merging. Keys that shape a scalar value (enum, separator, ignore_case, implicit_value, negatable, dotted_keys, bounds, pattern) do not apply: an object's rules live in its schema. Arguments cannot be object-valued.
Ref string `json:"$ref,omitempty"`
// Allowed values. The check applies to the final value, wherever it came from, so a flag value supplied by an environment variable or a config file is checked too. At least one member: an empty list would mean the same as no enum.
Enum []string `json:"enum,omitempty"`
// Exclusive upper bound: the value must be strictly less. Same applicability rules as 'maximum'.
ExclusiveMaximum any `json:"exclusiveMaximum,omitempty"`
// Exclusive lower bound: the value must be strictly greater. Same applicability rules as 'minimum' (numbers, durations and sizes; per-element for arrays; rejected elsewhere). exclusiveMinimum: 0 expresses "positive" exactly.
ExclusiveMinimum any `json:"exclusiveMinimum,omitempty"`
// Optional Go import path backing 'type'. Set it when 'type' references a stdlib or third-party package whose name rotini does not already know (e.g. 'github.com/google/uuid' for uuid.UUID). Omit (or leave empty) for builtins and rotini's own type names (string, int, duration, url, ip, bytesize, …) — codegen treats omitted/empty as 'no import'. The aliased form 'alias path' renames the import to avoid a clash (e.g. 'urlx github.com/me/url'). Codegen dedupes identical entries across the spec.
Import string `json:"import,omitempty"`
// Element schema for an array type: 'array' + items int generates []int, and items $ref a slice of the named type; with no items, elements are strings. Per-value constraints (enum, pattern, minimum/maximum, exclusiveMinimum/exclusiveMaximum, multipleOf, minLength/maxLength) apply to every element and may be written either here, JSON Schema style, or on the list itself. The two spellings mean the same thing, and declaring the same constraint in both places with different values is an error. minItems/maxItems belong on the list (they count elements) and are rejected here on flags, arguments, env and config. The TextUnmarshaler rule also applies to each element (see 'type').
Items *Schema `json:"items,omitempty"`
// Maximum number of values for a repeatable (array or map) input — rejected on scalar types.
MaxItems int `json:"maxItems,omitempty"`
// Maximum string length in runes (string types only; for []string, each element) — rejected on non-string types.
MaxLength int `json:"maxLength,omitempty"`
// Maximum allowed value (inclusive). Same applicability rules as 'minimum' (numbers, durations and sizes, each in its own spelling; per-element for arrays; rejected elsewhere); maximum: 0 is a real, enforced bound.
Maximum any `json:"maximum,omitempty"`
// Minimum number of values for a repeatable (array or map) input — rejected on scalar types. A list flag or variadic argument that is left out has zero values, so minItems also applies to it unless it has a default (add required for the message to say the input is missing). An env or config list is checked only when it is set.
MinItems int `json:"minItems,omitempty"`
// Minimum string length in runes (string types only; for []string, each element) — rejected on non-string types.
MinLength int `json:"minLength,omitempty"`
// Minimum allowed value (inclusive). Number types (the int, uint and float types and the integer and number aliases) take a number; a duration takes a duration (`minimum: 1s`) and a bytesize a size (`minimum: 1Mi`, or a number of bytes), read by the same parser as the value and printed back that way in errors (`must be >= 1s`). Rejected on any other type, where it would have no effect. For a repeatable input the bound applies to each element. minimum: 0 is a real, enforced bound.
Minimum any `json:"minimum,omitempty"`
// The value must be an integer multiple of this (JSON Schema semantics: the division yields an integer). Numbers, durations and sizes, in their own spelling (`multipleOf: 1s`); per-element for arrays; must be strictly positive.
MultipleOf any `json:"multipleOf,omitempty"`
// Generate the field as a pointer (*T): nil means the input was not provided, distinguishable from its zero value. Defaults/values coerce through the pointer.
Nullable bool `json:"nullable,omitempty"`
// Regular expression the value must match (string types only; for a list, each element). As in JSON Schema, the pattern matches anywhere in the value unless anchored: use ^…$ for a full match. A failure shows the user the regex itself unless `pattern_message:` says it in words.
Pattern string `json:"pattern,omitempty"`
// With 'pattern' only: what the user is told when a value does not match, in place of the regex — which is written for the program, not the person typing. Phrase it to follow the input's name: `pattern_message: must be json, yaml, wide, name or custom-columns=<spec>` reports `-o must be json, yaml, wide, name or custom-columns=<spec> (got "bogus")`, where the default is `-o must match ^(json|yaml|…)$ (got "bogus")`. Applies wherever the pattern is checked: an input on every channel it reads, each element of a list, a named schema an input refers to, and a property of an object-valued flag or a stdin payload.
PatternMessage string `json:"pattern_message,omitempty"`
// Property schemas for an object shape. Used by document shapes (output, stdin payloads, named schemas) — and, on a map-typed FLAG, the declared property names feed shell completion's key vocabulary (dotted paths when dotted_keys is set, offered up to the '=').
Properties map[string]Schema `json:"properties,omitempty"`
// The type used to parse and store the value. Go type names (bool, int, float64, []string, duration, map) and JSON Schema names (boolean, integer, number, array, object) are equivalent. A bool accepts true/false, yes/no, on/off, y/n, t/f and 1/0, in any case, so `--cache=off` and CACHE=yes both work.
//
// Value types parse a kind of value and generate the matching Go field:
//
// - 'duration' — time.Duration; Go units plus 'd' days and 'w' weeks (7d, 2w3d)
// - 'time' / 'datetime' — time.Time, RFC 3339 (2026-09-29T14:00:00Z)
// - 'date' — time.Time, a calendar date (2026-09-29, that day's UTC midnight). All three time types take `layout:` for another format, Unix timestamps included
// - 'url' — *url.URL; needs a scheme and host
// - 'email' — mail.Address; 'Name <a@b.c>' or a bare address
// - 'timezone' — *time.Location; an IANA name such as Europe/Berlin
// - 'mac' (net.HardwareAddr), 'ip' (netip.Addr), 'cidr' (netip.Prefix), 'hostport' (netip.AddrPort)
// - 'bytesize' — rotini.ByteSize; 512Mi, 10MB, 1.5GiB (an 'i' makes the unit binary)
// - 'hexbytes' — rotini.HexBytes; optional 0x
// - 'base64bytes' — rotini.Base64Bytes; standard or URL-safe, padded or not
// - 'existingfile' / 'existingdir' — a plain string field, checked at parse time to exist and be that kind of thing, so a bad path is a usage error naming the flag the user typed. The check is existence and kind only: expanding '~', cleaning, following symlinks and creating a missing file are the handler's policy
//
// A value type works inside Go spellings too ('[]bytesize', 'map[string]duration'), and help shows the name as written ('--limit bytesize'), not the Go type. A lowercase name that is neither a Go builtin nor one of these is rejected as a typo, with a suggestion.
//
// Shapes that change how a flag is written:
//
// - 'count' (flags only) — a presence counter: the flag takes no value, each occurrence increments the generated int field (-vvv → 3, clustering included), an inline value (--verbose=3) is a parse error, and every value-shaped key (default, enum, constraints, from, key, …) is rejected
// - '[]…' / 'array' — repeatable (--tag a --tag b → a slice); 'items' declares the element type ('array' + items int → []int), defaulting to string
// - a map ('map[string]string', or 'map'/'object' → map[string]any) — repeatable too, taking 'key=value' pairs (--label k=v --label a=b; split on the first '='; the value coerced to the element type)
//
// Any other Go type (time.Time, uuid.UUID, your own): set 'import' to the backing package path. It must implement encoding.TextUnmarshaler — that method is its parser and validator, applied to each element of a list too. Rotini refuses a type without it at parse time with a loud error, never a silently zeroed field; validate cannot check it, since that would mean type-checking foreign packages.
//
// A NAMED OBJECT schema is different: a flag whose schema is `$ref: '#/schemas/DB'` (or `type: array, items: {$ref: '#/schemas/DB'}` for a list) takes a structured value — see `$ref`.
Type string `json:"type,omitempty"`
}
Shared fields for Schema and InputSchema. JSON Schema Draft 7 cannot combine allOf inheritance with additionalProperties: false, so an unknown key inside a schema block is rejected by `rotini validate` itself rather than by the schema — with the same positioned "unknown key" message as anywhere else in the spec. A JSON Schema keyword rotini does not implement (uniqueItems, format, title, …) is rejected too, since it would otherwise do nothing. `description` is accepted on object schemas and their properties, where it becomes a Go doc comment.
type Command ¶
type Command struct {
// The spec file whose root command is mounted here as this sub-command. Not valid on the root command. Two forms are accepted:
//
// - A relative path, resolved against this spec file's directory (`$ref: ../db/.rotini.spec.yaml`).
// - `mod://<module>@<version>/<path>`, a spec inside another Go module (`$ref: mod://github.com/acme/db@v1.4.0/cmd/db/.rotini.spec.yaml`). The version is required, and <path> is the spec file's path inside that module. The module is read from the module cache with `go mod download`, so it is verified against go.sum; add it to go.mod first (`go get github.com/acme/db@v1.4.0`). A relative $ref inside that spec resolves within the same module and cannot leave it.
//
// Git and https URLs are not accepted. Either way, the mounted command uses the handlers of the composed spec's own generated package unless `handler:` names another.
//
// The composed spec is the base, and the parent can adjust it where it is mounted:
//
// - Identity and presentation keys declared next to the $ref (name, aliases, summary, description, usage, header, footer, examples, headings, help, man, markdown, exit_status, see_also, group, hidden, deprecated, deprecated_identifiers, filename, plugin_path) replace the child's, for that one mounted command only. The child's own sub-commands keep theirs, so a parent can tailor the child for its tree without forking it.
// - A 'commands:' list next to the $ref is added to the child's own sub-commands: its inline entries get their own handler files, and its $ref entries are mounted as further children.
// - `handler:` on a $ref command points it at a different handler package.
// - Keys the handler depends on (flags, arguments, env, config, config_files, stdin, flag_groups, flag_dependencies, output, plugins, plugin_discovery, passthrough) cannot be changed here: the mounted command runs the child's handler, built against the child's own inputs and output, so validation rejects them. Declare them in the child spec.
//
// The child's own plugins, plugin_discovery and passthrough travel with it. `rotini validate` and `generate` on the parent also check every spec composed by a relative path as its own document, reporting problems at their position in that file. A spec composed with mod:// is not checked that way: it belongs to its own module, which validates it.
Ref string `json:"$ref,omitempty"`
// Additional names that invoke this command. Command aliases affect dispatch routing; use identifiers on flags for flag aliases. Sub-commands only: the root command is reached by invoking the binary (argv[0] is not a routing token), so rotini validation rejects aliases there.
Aliases []string `json:"aliases,omitempty"`
// Positional argument inputs for this command
Arguments []ArgumentInput `json:"arguments,omitempty"`
// Sub-commands of this command, declared inline or composed with $ref. On a $ref command these are added to the composed child's own sub-commands (see '$ref'); a name or alias that collides across the combined list is an error.
Commands []Command `json:"commands,omitempty"`
// Config-value inputs for this command, bound by key from an in-scope config_files source (declared on this command or any ancestor — see config_files).
Config []ConfigInput `json:"config,omitempty"`
// Configuration files this command reads values from, each at a fixed 'path' or found with 'discover'. Files cascade: a command can read from the files declared on it and on every command above it, so a 'config' input here or on any sub-command may pin itself (schema 'file:') to a file declared here or on any ancestor. Only the files along the invoked command's path are loaded; files declared on other branches are never read. File names must be unique along a path (a collision is an error), and declaring the same physical file at two levels is a warning. When two files in scope define the same key, the one declared nearest the invoked command wins, and within one command's list the first declared wins, so list the more specific file (a project's) before the more general one (the user's). In a composed CLI, a `$ref`'d child's files travel with its commands, so `parent child cmd` reads what `child cmd` reads without the parent declaring anything.
ConfigFiles []ConfigurationFile `json:"config_files,omitempty"`
// Deprecation message. The command is annotated as deprecated in its parent's generated Commands list, and invoking it is reported at run time by rotini.Deprecations with this message (a data feed for the handler; rotini itself prints nothing). With `deprecated_identifiers`, only those aliases report; without, every name does.
Deprecated string `json:"deprecated,omitempty"`
// Aliases of this command that are deprecated (a subset of 'aliases'). When the command is invoked via one of these, rotini's Deprecations surfaces it for the handler to act on; invoking via the name or a non-listed alias is unaffected. Sub-commands only, like 'aliases' — rejected on the root by rotini validation.
DeprecatedIdentifiers []string `json:"deprecated_identifiers,omitempty"`
// Long description block shown atop this command's generated help page. Ignored when 'help' (verbatim) is set.
Description string `json:"description,omitempty"`
// Root only: the name the generated help, man and markdown pages show for the program, in place of the root's 'name'. For a plugin, which a host runs as `<host>-<name>` but the user types as `<host> <name>`: with name: kubectl-ctx and display_name: "kubectl ctx", every derived usage line reads `kubectl ctx use <name> [flags]`, the man page's SYNOPSIS `kubectl ctx use …` and the markdown title `# kubectl ctx use`. It may contain spaces. It changes presentation only: 'name' still matches the binary and names the generated page files and man pages (kubectl-ctx-use.1), completion scripts still register for 'name' (the shell completes the binary), and routing, handler names and Context.CommandPath() are unaffected. Text the author writes verbatim (usage, footer, examples, a verbatim help/man/markdown page) is not rewritten. A composed child's display_name is ignored; the composing parent's root decides.
DisplayName string `json:"display_name,omitempty"`
// Environment-variable inputs for this command
Env []EnvInput `json:"env,omitempty"`
// Root only: a prefix for every environment-variable name rotini derives. Derived names are the UPPER_SNAKE forms of plain env inputs without 'variable:' (input 'home' → ACME_HOME), of nested env families without 'variable:' (the family's base name), and of flags' environment fallbacks (key 'server.port' → ACME_SERVER_PORT). A name set explicitly with 'variable:' is used exactly as written and is never prefixed. With a prefix declared, an unprefixed name no longer binds: input 'home' reads ACME_HOME, not HOME. Write it in UPPER_SNAKE with no trailing underscore (rotini adds the '_'). The derived name is written into the generated field's `env:` tag when you generate, so the name is fixed in the code. Help lists env: inputs by name under its Environment section. Help also shows each flag's environment fallback, and its configuration key when the command reads configuration files, on a line under the flag.
//
// In a composed CLI, a `$ref`'d child's env_prefix travels with its commands: a parent that declares none adopts the child's, a parent that declares one wins, and two children with different prefixes are rejected.
EnvPrefix string `json:"env_prefix,omitempty"`
// Example command-line invocations, rendered one per line. Ignored when 'help' is set.
Examples []string `json:"examples,omitempty"`
// Exit codes this command documents, rendered as an EXIT STATUS section in the man and markdown pages. The runtime does not check it, and sets no exit code of its own except two. A recorded error or a recovered panic exits 1 when no handler set a code, and the default signal handling exits 128+n on signal n (130 for Ctrl-C). So a command that documents `2: invalid input` here and only calls rtx.RecordError will exit 1. Set the code in the handler with rtx.Exit or rtx.HaltWithCode to make the program agree with this section. `rotini generate` warns when a command's handler sets an exit code (a literal or constant passed to rtx.HaltWithCode or rtx.Exit) that this list doesn't include, and `rotini validate` reports a code listed twice and warns about a code above 128, which a signal exit also uses. Code 0 needs no entry, and a command that prints its help when called without a sub-command exits 1, so list 1 for it. Ignored when 'man' (verbatim) is set.
ExitStatus []ExitStatusEntry `json:"exit_status,omitempty"`
// Override the name of this command's generated handler-stub .go file (in the cli package). Defaults to a name derived from the command path ('<root>_<path>.go', every '-' written '_': config_get_contexts.go), reserved-name-escaped so a command named 'test'/'<GOOS>'/'<GOARCH>' does not collide with Go's filename rules. Must end in '.go', must not itself be a name Go reads specially ('_test.go', '_<GOOS>.go', '_<GOARCH>.go'), and must be unique among the commands generated into the same package. Renaming it orphans (and prunes) the previous stub file — move your handler code first.
Filename string `json:"filename,omitempty"`
// Conditional cross-flag requirements validated at parse time: when one flag is set, others become required (e.g. when --tls is set, --cert and --key are required).
FlagDependencies []FlagDependency `json:"flag_dependencies,omitempty"`
// Cross-flag presence rules validated at parse time (e.g. mutually exclusive output formats, a required-together credential pair).
FlagGroups []FlagGroup `json:"flag_groups,omitempty"`
// Flag inputs for this command
Flags []FlagInput `json:"flags,omitempty"`
Footer string `json:"footer,omitempty"`
// Group label for organizing this command under a heading in its parent's generated Commands list. Commands sharing a group are bucketed together; groups appear in the order their first member is declared. Ungrouped commands fall under the default Commands heading. Presentation-only.
Group string `json:"group,omitempty"`
// Use a handler from another Go package for this command instead of a generated handler file. Valid on any sub-command, not the root.
//
// On a '$ref' command it replaces the default: a composed local or mod:// spec normally uses the handlers of its own generated package, and this points the command at a different package. On an inline command, the command's tree and typed inputs are still generated here, but its handler comes from the package and no handler file is written. It applies to this command only: an inline sub-command without its own 'handler:' still gets a generated handler file.
//
// The package must export a constructor '<convention>() rotini.Handler' for each command it serves (the usual five-hook handler; hooks it does not implement default to no-ops), and the generated code calls 'pkg.<Convention>()'. The compiler enforces this, since rotini cannot type-check another package.
Handler *HandlerSource `json:"handler,omitempty"`
// Text rendered above the description block. Ignored when 'help' is set.
Header string `json:"header,omitempty"`
// Section heading overrides for the generated page; sane defaults fill any unset heading. Ignored when 'help' is set.
Headings *HelpHeadings `json:"headings,omitempty"`
// Exact, verbatim help page for this command. When set, rotini writes it byte-for-byte (no rendering; terminal styling kept) and ignores the structured help fields (description/usage/header/footer/examples/headings); 'summary' is still used in the parent's Commands list. When unset, rotini generates the page from the structured fields.
Help string `json:"help,omitempty"`
// When true, the command is omitted from its parent's generated Commands list (it still dispatches on the command line).
Hidden bool `json:"hidden,omitempty"`
// Exact, verbatim man page for this command, written in roff, the markup the man program reads (the man feature's per-command escape, mirroring 'help'). When set, rotini writes it as given — byte-for-byte except that ANSI styling is removed, since a man page carries none — and ignores the structured doc-fields for the man page; when unset, the page is rendered as roff from those fields through the man template. Either way the page is named after the command path joined with '-' and lowercased, with the man section as its extension (deploy-status.1).
Man string `json:"man,omitempty"`
// Exact, verbatim markdown reference page for this command (the markdown feature's per-command escape, mirroring 'help'/'man'). When set, rotini writes it as given — byte-for-byte except that ANSI styling is removed; when unset, the page is rendered from the structured doc-fields through the markdown template.
Markdown string `json:"markdown,omitempty"`
// Command name used in routing. As the root command (the document itself) this is the binary name and must be set — the root cannot use '$ref'.
Name string `json:"name,omitempty"`
// The shape of what this command writes to stdout when it succeeds, as a schema. Rotini generates a typed '<Prefix>Output' Go type (when the shape is a '$ref' to a document-level schema, a new named type defined on that schema's type, such as 'type <Prefix>Output Task', so convert a value with <Prefix>Output(v)), documents the shape in an OUTPUT section of the help, man and markdown pages, and describes it in the output schema files and the contract document. It describes the shape only: it adds no flag and wires no format. How the output is written, and in which format, is the handler's own code — rtx.WriteOutput is an optional helper that writes json, yaml or toml, hands any other format to a renderer, and checks the value is this type. A command that writes a stream of items declares the shape of one item.
Output *Schema `json:"output,omitempty"`
// When true, every token after this command's own name binds as a raw positional — no flag parsing, no unknown-flag errors, no '--' needed (the wrapper-CLI case: `mytool exec ls -la` forwards '-la' verbatim, and a literal '--' passes through too). Tokens BEFORE the command (ancestor flags) parse normally. A passthrough command declares no flags, no sub-commands, no declared plugins or discovery, and its last argument must be a variadic '[]string' — the receiver of the raw tokens (validation enforces all of this). Shell completion offers nothing past the boundary, falling back to file completion.
Passthrough bool `json:"passthrough,omitempty"`
// Auto-expose external '<prefix>*' executables as plugin sub-commands of this command (kubectl/git/gh plugin discovery), in addition to any declared plugins. Presence enables discovery.
PluginDiscovery *PluginDiscovery `json:"plugin_discovery,omitempty"`
// Extra directory to search for this command's plugin binaries, in addition to the host binary's own directory and PATH. Relative to the working directory at run time; a leading ~ and $VAR references are expanded when the program runs, and a directory that does not exist yet is simply empty. On a `$ref` node it overrides the composed child's own. It applies to BOTH kinds of plugin: the 'plugins' this spec declares and anything 'plugin_discovery' finds — they are the same binaries in the same place, so they are configured once here rather than per-mechanism. Without it, a declared plugin could only ever be installed next to the host binary or on PATH, which is the git/kubectl convention and not always the right one for a vendored or bundled plugin. Search order is fixed and the same for both: next to the host binary, then this directory, then PATH — so a plugin shipped beside the binary always wins over one found here, and a failure names the locations it actually searched.
PluginPath string `json:"plugin_path,omitempty"`
// Declared plugins: separate executables dispatched as first-class sub-commands of this command.
Plugins []PluginSpec `json:"plugins,omitempty"`
// Document-level (root only): reusable named schema definitions. Referenced elsewhere by name, `$ref: <Name>`, or as a pointer, `$ref: "#/schemas/<Name>"`. Each may carry a `description`, which becomes the generated type's doc comment. Names must be PascalCase Go-exportable identifiers — each becomes a generated Go type in the cmd package (or in the models package, when the conf declares one), which other packages may import.
Schemas map[string]Schema `json:"schemas,omitempty"`
// Cross-references rendered as a SEE ALSO section in the man page (e.g. related commands or man pages like 'rotini-generate(1)', or URLs). Ignored when 'man' (verbatim) is set.
SeeAlso []string `json:"see_also,omitempty"`
// Declares expected stdin format and schema for this command
Stdin *StdinSpec `json:"stdin,omitempty"`
// Short one-liner describing this command. It is shown next to the command in its parent's generated Commands list (so it applies even when a verbatim 'help' string is set), and it is also the NAME line of the man page, the opening line of the markdown page, and the lead of the command's own help page when no 'description' is set.
Summary string `json:"summary,omitempty"`
// Not supported on a local command and rejected by rotini validation: a timeout is a plugin-only, host-side bound on a dispatched binary, so it has no effect on local execution. Set it on a plugins[] entry's 'timeout' instead. (Recognized here only so validation can give that targeted error rather than a generic 'unknown property'.)
Timeout string `json:"timeout,omitempty"`
// Usage-line override. When omitted, rotini derives one from the command's shape. Ignored when 'help' is set.
Usage string `json:"usage,omitempty"`
}
A command node in the CLI command tree — the root command (under the document's 'command' key) and every sub-command share this recursive shape. Declared inline (with 'name') or composed from another spec file (with '$ref'). The root must use 'name' (not '$ref'). The two root-command-level keys (env_prefix, schemas) are accepted on this shape but are valid only on the root command — rotini validation rejects them on a sub-command.
type Conf ¶
type Conf struct {
// Optional URI identifying the rotini conf schema, for editor tooling only: rotini never fetches it, and the version check reads the `version` key below. Any URI is accepted: a released schema (https://raw.githubusercontent.com/go-rotini/rotini/refs/tags/v1.3.0/schema-conf.json — note the 'v', matching the git tag), a path written into your project by `generate.schemas.conf.file`, or a fork's own URL.
Schema string `json:"$schema,omitempty"`
// Controls `rotini generate`: the generated packages and features. When omitted entirely, the defaults apply: one generated file under internal/cmd/<root>, and every feature off.
Generate *GenerateConfig `json:"generate,omitempty"`
// Controls how `rotini validate` and `rotini generate` report problems (collect everything vs. fail fast).
Validate *ValidateConfig `json:"validate,omitempty"`
// The minimum rotini version this conf requires (X.Y.Z): the feature set it was written against, not an exact pin. Any rotini of the same major version at or beyond it accepts the document, so a patch or minor upgrade never requires an edit here. A rotini older than this, or a different major version, is an error. The check is skipped for a development build of rotini, which reports no release version (0.0.0, or none at all). Works the same as the spec's `version` key.
Version string `json:"version"`
}
The conf controls what `rotini generate` writes and where: the Go packages, the documentation and completion features, the JSON Schemas and the contract document. It also sets how `rotini validate` reports problems.
type ConfigInput ¶
type ConfigInput struct {
// Deprecation message; the input is annotated as deprecated in generated help. (Run-time deprecation reporting covers what argv carries — commands, flags and arguments.)
Deprecated string `json:"deprecated,omitempty"`
// When true, the input is omitted from generated help (it is still bound).
Hidden bool `json:"hidden,omitempty"`
// Logical name for this config value
Name string `json:"name"`
// Type definition and input-level metadata (required, default, file, key)
Schema *InputSchema `json:"schema,omitempty"`
// Short one-liner shown next to this input in the generated Environment/Configuration help section.
Summary string `json:"summary,omitempty"`
}
type ConfigurationFile ¶
type ConfigurationFile struct {
// Locate this file at run time instead of a fixed 'path'. Exactly one of 'path' or 'discover' must be set.
Discover *ConfigurationFileDiscover `json:"discover,omitempty"`
// The file's format. When omitted, the format is inferred from the file extension; declare it when the extension is missing or misleading. 'jsonc' is JSON with comments and trailing commas. 'dotenv' reads KEY=value lines whose keys are used as written: a config input reading one declares `key: API_ENDPOINT`, not a dotted path.
Format string `json:"format,omitempty"`
// Logical name for the config file (e.g. 'app-config'). It anchors per-input pins (schema 'file:') and config_source claims, so it must be unique within its chain (this command and its ancestors) — a name collision in scope is an error.
Name string `json:"name"`
// File path (supports ~ for home dir). Exactly one of 'path' or 'discover' must be set.
Path string `json:"path,omitempty"`
// Optional load-time validation: the loaded document is validated against this schema at bind time, before any value is read from it — a non-conforming file is a loud error naming the file and the violation (the same gate the stdin channel applies to its payload). The file that actually resolved — fixed path, discovered, or config_source-supplied — is the file validated; an absent file passes vacuously (absence is the per-input required's concern). Document-level named schemas resolve via "$ref": "#/schemas/<Name>". No typed struct is generated from this — typed access to config values is the config: inputs channel.
Schema *Schema `json:"schema,omitempty"`
}
type ConfigurationFileDiscover ¶
type ConfigurationFileDiscover struct {
// The application directory under the XDG config root: the '<app>' in $XDG_CONFIG_HOME/<app>. Required by the 'xdg' strategy and rejected by 'walk-up', which has no such directory. `rotini validate` enforces both, so its message can say which strategy needs it and what it is for.
App string `json:"app,omitempty"`
// The file name to look for in each searched directory (e.g. '.acme.toml', 'config.yaml').
File string `json:"file"`
// 'walk-up': search from the working directory upward, one parent at a time, until a directory containing 'file' is found or the root is reached. Use it for project-local config. On Windows the search stops at the drive root.
//
// 'xdg': search $XDG_CONFIG_HOME/<app>, defaulting to ~/.config/<app>, on every platform, Windows and macOS included. Rotini does not substitute %APPDATA% or ~/Library/Application Support, so a CLI documented as reading ~/.config/<app> reads the same path everywhere, and a dotfiles repository works unchanged across machines. For the platform's native location on each OS, declare a fixed 'path' instead.
Strategy string `json:"strategy"`
}
A run-time location strategy for a configuration file, instead of a fixed 'path'. The first directory (in the strategy's order) containing 'file' wins; a file found nowhere is simply absent, the same as a missing fixed path. Among one command's config_files the first declared still wins, wherever each was found.
type ContractConfig ¶ added in v1.2.0
type ContractConfig struct {
// Module-root-relative path (no leading slash) ending in '.json' the contract document is written to. Rewritten on every `generate`.
File string `json:"file"`
}
Optional: write the contract document, one JSON file describing the whole CLI for scripts, tools and AI agents. Every visible command is listed with its arguments, flags (including inherited cascading flags), environment variables, configuration keys and stdin; a `parameters` JSON Schema combining its arguments and flags; its output shape where one is declared; and its exit statuses. The format is rotini's own, described by schema-contract.json in the rotini repository, and the shape of the error line rotini.StructuredReporter writes to stderr is included under `errors`.
type EnvInput ¶
type EnvInput struct {
// Deprecation message; the input is annotated as deprecated in generated help. (Run-time deprecation reporting covers what argv carries — commands, flags and arguments.)
Deprecated string `json:"deprecated,omitempty"`
// When true, the input is omitted from generated help (it is still bound).
Hidden bool `json:"hidden,omitempty"`
// Logical name for this env var input
Name string `json:"name"`
// Type definition and input-level metadata (required, default, variable)
Schema *InputSchema `json:"schema,omitempty"`
// Short one-liner shown next to this input in the generated Environment/Configuration help section.
Summary string `json:"summary,omitempty"`
}
type ExitStatusEntry ¶
type ExitStatusEntry struct {
// The exit status code being documented (0-255 — the range a process can actually return).
Code int `json:"code"`
// The shape stdout still carries when the command exits with this code, for an outcome that is not plain success but prints data anyway (`3: some tasks failed; stdout lists what succeeded`). Documented in the EXIT STATUS section and described in the output schema files and the contract document.
Output *Schema `json:"output,omitempty"`
// What this exit code means.
Summary string `json:"summary,omitempty"`
}
type Feature ¶
type Feature struct {
// How this feature's content is stored in the cmd package. true: the rendered content is written to a file under 'embed_dir' and loaded with a //go:embed directive. false (the default): no file is written, and the content is a string literal in the generated code, so the generated file is self-contained. The variable names and the lookup function are the same either way.
Embed bool `json:"embed,omitempty"`
// Directory (relative to the module root) where this feature's rendered files (help_*.txt, <page-name>.<section>, markdown_*.md, completion_<shell>.txt) are written when embed is true, and loaded with //go:embed, so it must be inside the cmd package (//go:embed cannot reach outside it). With embed false no files are written and this is unused; rotini validation warns if you set it then. Defaults to '<cmd-package>/renders'.
EmbedDir string `json:"embed_dir,omitempty"`
// When true, rotini generates this feature's outputs into the cmd package, with their variables and lookup function. Off by default.
Enabled bool `json:"enabled,omitempty"`
// completion only: turns on completion messages, lines the shell shows while a value is being completed and there is nothing to offer. 'declared' shows the inputs' `complete.message` lines from the spec. 'all' also shows a line derived from the summary of every other flag and argument that has one, such as `--replicas <int>: how many instances`. Either way a completer can add its own with rtx.AddCompletionMessage, which take the place of the static line. Omitted, there are no messages. zsh and bash 4.4 or later show them; fish, PowerShell and older bash skip them, and the plugin hosts kubectl, Docker and Flux show them their own way. Setting it on any other feature is an error.
Messages string `json:"messages,omitempty"`
// completion only: the name of an environment variable your users can set to 0, false or off (any case) to hide completion messages; unset or any other value leaves them on. It is listed in the root man page's ENVIRONMENT section, the contract document and the completion scripts' header. Program.WithCompletionMessages replaces this check with a rule of your own. Requires `messages`.
MessagesEnv string `json:"messages_env,omitempty"`
// man only: the man page section the pages are generated for, a single digit 1-9 (default 1, user commands; 8 is administration tools and daemons). It is the section in each page's header, the extension of each page file (taskr-add.8), and the section in cross-references between pages, and the generated ManSection constant holds it. One value for the whole program. Setting it on any other feature is an error.
Section int `json:"section,omitempty"`
// Whether the editable template (help.txt.tmpl, man.txt.tmpl or markdown.md.tmpl) is seeded into 'template_dir' for you to customize. true: the template is written when missing, and pages render from it. false (the default): no template is written, and pages render from rotini's built-in one. Has no effect on completion, which has no template; rotini validation warns if you set it there. A template you have edited is never removed: setting this back to false leaves it in place, unused.
Template bool `json:"template,omitempty"`
// Directory (relative to the module root) where this feature's editable template (help.txt.tmpl, man.txt.tmpl or markdown.md.tmpl) is written when template is true. Templates are not embedded, so it may be anywhere. Unused when no template is seeded (template false, or completion, which has none); rotini validation warns if you set it then. Defaults to '<cmd-package>/templates'.
TemplateDir string `json:"template_dir,omitempty"`
// Which output this entry configures. help, man and markdown are per-command pages, rendered from the command's documentation fields in the spec through the template, or written verbatim when the command sets that page in the spec. Each generates a variable per page and a 'Help', 'Man' or 'Markdown(path ...string) (string, error)' function that returns the page for a command path. completion is different: one script per shell (bash, zsh, fish, powershell), generated from the program name, with no editable template and no verbatim form. It generates a 'Completion<Shell>' variable per shell and a 'Completion(shell string) (string, error)' function; the scripts call the program's hidden '__complete' command.
Type string `json:"type"`
}
One generated feature, chosen by 'type' (help, completion, man, markdown): an on/off switch ('enabled') plus two independent options. 'embed' chooses how the content is stored: a rendered file in 'embed_dir' loaded with //go:embed, or a string literal in the generated code. 'template' chooses how it is rendered: from an editable template seeded into 'template_dir', or from rotini's built-in one. The directories default to '<cmd-package>/renders' and '<cmd-package>/templates'. With embed on, embed_dir must be inside the cmd package, since //go:embed cannot reach outside it; template_dir may be anywhere. Output files never collide: help pages are 'help_*.txt', man pages '<page-name>.<section>' (taskr-add.1), markdown pages 'markdown_*.md' and completion scripts 'completion_<shell>.txt', and each feature removes only its own files.
type FlagDependency ¶
type FlagDependency struct {
// Flags that must also be set when 'when' is set.
Requires []string `json:"requires"`
// The flag whose presence triggers the requirement.
When string `json:"when"`
}
A conditional requirement: when the 'when' flag is explicitly set on the command line, every flag in 'requires' must also be set. Both reference flag logical names; 'set' means explicitly provided (a default or env/config fallback does not count).
type FlagGroup ¶
type FlagGroup struct {
// The logical flag names the rule covers (at least two). Each must be a flag declared on the same command — validation rejects unknown names. "Set" means explicitly set on argv: defaults and env/config fallbacks neither trip nor satisfy a group.
Flags []string `json:"flags"`
// mutually_exclusive: at most one set. required_together: all or none. one_of: exactly one. at_least_one: one or more.
Kind string `json:"kind"`
}
A constraint on which of this command's flags may (or must) be set together. 'flags' references flag logical names; 'set' means explicitly provided on the command line (a default or env/config fallback does not count).
type FlagInput ¶
type FlagInput struct {
// When true, this flag is advertised in the generated help of every descendant command (under the 'Global Flags' section), not only on its own command. Display-only: at run time a flag may be written anywhere after the name of the command that declares it — after its sub-commands' names too — regardless of this setting, but never before that name (a flag written before a sub-command's name belongs to an ancestor, which is how two commands may declare the same flag). cascading controls whether descendants document it.
Cascading bool `json:"cascading,omitempty"`
// Deprecation message. The flag is annotated as deprecated in generated help, and using it is reported at run time by rotini.Deprecations with this message (a data feed for the handler; rotini itself prints nothing). With `deprecated_identifiers`, only those spellings report — the others are the ones to move to (`identifiers: [--db, --database]`, `deprecated_identifiers: [--database]`, `deprecated: use --db`); without, the whole flag is deprecated and every spelling reports.
Deprecated string `json:"deprecated,omitempty"`
// CLI tokens for this input that are deprecated — a subset of its identifiers (flags) or aliases (commands). When one of these is used on the command line, rotini's Deprecations surfaces it as a data point for the handler to act on (warn, emit telemetry, etc.); the framework itself does nothing. Tokens not listed here are unaffected. Pair it with `deprecated:` to give the report a message; `deprecated:` alone deprecates every spelling
DeprecatedIdentifiers []string `json:"deprecated_identifiers,omitempty"`
// Group label that puts this flag under its own heading in generated help, the way a command's 'group' does in the Commands list: flags sharing a group appear together, groups appear in the order their first member is declared, and ungrouped flags fall under the default Flags heading.
//
// Presentation only: parsing, precedence and the generated field are unchanged. Use it on a command with many flags, so its help page is easy to scan. Not to be confused with 'flag_groups', which validates combinations of flags.
Group string `json:"group,omitempty"`
// When true, the flag is omitted from generated help (it still parses on the command line).
Hidden bool `json:"hidden,omitempty"`
// CLI flag identifiers (e.g., '--force', '-f'). When absent, '--<name>' is derived from the flag's name, with '_' written as '-' ('dry_run' → --dry-run).
Identifiers []string `json:"identifiers,omitempty"`
// Logical name for the flag
Name string `json:"name"`
// Type definition and input-level metadata (type, required, default, enum, nullable, constraints)
Schema *InputSchema `json:"schema,omitempty"`
// When true, setting this flag on the command line waives every declared requirement of the invoked command chain: required inputs, enums, bounds, patterns, flag groups and flag dependencies are not checked, and rtx.Inputs succeeds. Use it for flags that replace the command's normal run, such as --help, --version or --print-schema. Errors in reading the command line (an unknown flag or command, a value of the wrong type, too many arguments) are still reported. rotini takes no action of its own: your handler checks the flag and decides what to do. Only the command line sets it, never an environment variable, a configuration file or a default. Must be a bool, and can't be required, negatable, given a default of true, read from the environment or a configuration file (key, variable), or listed in a flag group or flag dependency.
ShortCircuit bool `json:"short_circuit,omitempty"`
// Short one-liner shown next to this flag in the Flags section of generated help.
Summary string `json:"summary,omitempty"`
}
type GenerateConfig ¶
type GenerateConfig struct {
// Optional: where to write the contract document, a JSON description of every command's inputs, output and exit statuses.
Contract *ContractConfig `json:"contract,omitempty"`
// The name of an environment variable that makes `rotini generate` do a dry run: when it is set to 1, true, yes or on (any case), generate writes nothing, lists what it would change and exits 2 if anything would, or 0 if nothing would. Set it to CI to dry-run in most CI systems, which set CI=true, so `go generate ./...` checks every CLI in the module. --dry-run and --no-dry-run on the command line take precedence. Unset, the environment never changes what generate does. It doesn't apply to `rotini init`, which has no conf to read before it runs.
DryRunEnv string `json:"dry_run_env,omitempty"`
// The generated documentation and completion outputs, one per `type` (help, completion, man, markdown). Each is off unless enabled, with options for how its content is stored and rendered.
Features []Feature `json:"features,omitempty"`
// The generated code targets, one per `type` (main, cmd, models). 'main' is the program's entrypoint, created once. 'cmd' is the CLI package: the handler files you edit, plus the one generated file (the command tree, the handler wiring and, unless 'models' moves them out, the typed input and output structs). 'models' is optional: declare it to put the typed structs in their own package, which a handler package used through a command's `handler:` can import without an import cycle. The rotini runtime is not generated: it is an ordinary dependency the generated code imports (`go get github.com/go-rotini/rotini`).
Packages []PackageConfig `json:"packages,omitempty"`
// Optional: where to write rotini's conf and spec JSON Schemas into this project, so a document's `$schema:` key (which `rotini init` seeds) can point at a local copy instead of a URL.
Schemas *SchemasConfig `json:"schemas,omitempty"`
}
Controls `rotini generate`: where rotini's JSON Schemas are written ('schemas'), where the generated code is written ('packages'), and which derived doc/completion outputs are emitted ('features').
type GenerateDryRunFn ¶ added in v1.3.0
type GenerateDryRunFn = func(specPath, confPath string, onNotices func(notices []error)) (Planned, error)
GenerateDryRunFn is the signature of Processor.GenerateDryRun.
type GenerateFn ¶
type GenerateFn = func(specPath, confPath string, watch bool, onGenerate func(result string, err error), onNotices func(notices []error)) error
GenerateFn is the signature of Processor.Generate. Command handlers fetch it as an injectable service so tests can substitute a double.
type HandlerSource ¶
type HandlerSource struct {
// Function-name prefix the package exports per command: codegen delegates this command to '<alias>.<convention>()' and each sub-command to '<alias>.<convention><SubPath>()', each returning a rotini.Handler. PascalCase Go-exportable identifier.
Convention string `json:"convention"`
// Go import path of the handler package, in the same 'alias path' form an input type's 'import' uses (e.g. 'deployhandlers github.com/acme/clis/deploy/handlers'); identical imports are merged. A bare path takes its alias from the last path segment. The generated code calls '<alias>.<convention>()'.
Import string `json:"import"`
}
Where a command's handlers come from when they are not a generated stub: a Go package (handler delegation). The package's typed inputs live with it; this spec contributes only the command tree.
type HelpHeadings ¶
type HelpHeadings struct {
// Heading rendered above the arguments section of the generated help page. Rendered verbatim — include any trailing ':' you want. Default: "Arguments:".
Arguments string `json:"arguments,omitempty"`
// Heading rendered above the cascading-flags section on descendant commands' generated help pages. Rendered verbatim — include any trailing ':' you want. Default: "Global Flags:".
Cascading string `json:"cascading,omitempty"`
// Heading rendered above the commands section of the generated help page. Rendered verbatim — include any trailing ':' you want. Default: "Commands:".
Commands string `json:"commands,omitempty"`
// Heading rendered above the configuration section of the generated help page. Rendered verbatim — include any trailing ':' you want. Default: "Configuration:".
Configuration string `json:"configuration,omitempty"`
// Heading rendered above the environment section of the generated help page. Rendered verbatim — include any trailing ':' you want. Default: "Environment:".
Environment string `json:"environment,omitempty"`
// Heading rendered above the examples section of the generated help page. Rendered verbatim — include any trailing ':' you want. Default: "Examples:".
Examples string `json:"examples,omitempty"`
// Heading rendered above the flags section of the generated help page. Rendered verbatim — include any trailing ':' you want. Default: "Flags:".
Flags string `json:"flags,omitempty"`
// Heading rendered above the Output section, which describes what the command writes when it declares `output:`. Rendered verbatim — include any trailing ':' you want. Default: "Output:".
Output string `json:"output,omitempty"`
// Heading rendered above the usage section of the generated help page. Rendered verbatim — include any trailing ':' you want. Default: "Usage:".
Usage string `json:"usage,omitempty"`
}
Section heading overrides for generated help pages.
type InitializeDryRunFn ¶ added in v1.3.0
type InitializeDryRunFn = InitializeFn
InitializeDryRunFn is the signature of Processor.InitializeDryRun.
type InitializeFn ¶
type InitializeFn = func(name, format string, force bool) (Initialized, error)
InitializeFn is the signature of Processor.Initialize. The companion CLI injects it as a dependency so tests can substitute a double (see GenerateFn).
type Initialized ¶
type Initialized struct {
Spec, Conf, Result string
// Changes is set by a dry run: each file init would create or replace, one per line.
Changes []string
}
Initialized reports what an init wrote: the spec and conf paths (relative to the working directory where possible) and a "[15:04:05] 12.3ms" timing line.
type InputSchema ¶
type InputSchema struct {
BaseSchema
// Shell-completion hint for this input's value: for the common case of a file or directory, between a fixed `enum` and a completer written in Go (FlagValueCompleter), plus an optional message to show when there is nothing to offer. Declare 'kind', 'message' or both.
//
// Flags and arguments only. Each generated completion script turns the hint into that shell's own path completion. A completer written in Go still wins when it answers; the hint is the fallback.
Complete *InputSchemaComplete `json:"complete,omitempty"`
// Flag and env inputs only: names a config_files entry whose file path this input supplies, so a user can point the program at a config file (`--config ./other.yaml`). The command line and environment are read first, then the file they name is opened. The path comes from the flag if set on the command line, then the env input's variable, then the flag's default, then the entry's own path or discover. A path supplied this way must exist: unlike a declared path, a missing file is then an error, because the user asked for it. The input's type must be string. At most one flag and one env input may name the same entry.
ConfigSource string `json:"config_source,omitempty"`
// The value an input takes when nothing supplies one. A scalar for a single value; a list for a repeatable input, each element added as one occurrence (as if the flag were repeated); a mapping for a map input, added as key=value pairs; and for an object-valued flag a mapping of the named schema's keys (a list of mappings for a list of objects). Write it the way a user would type the value (a duration as 30s, a date as 2026-09-29 or in the input's `layout:`). `rotini validate` checks it against the input's type, enum, bounds and schema, so a default that could never be accepted fails there rather than on every run that uses it. A supplied value replaces the default; the two never merge. `default_text:` changes only how help shows it.
Default any `json:"default,omitempty"`
// How help, man and markdown pages show the default, in place of the value itself. Use it for a default that reads badly as written (`default_text: 'the number of CPUs'`, `default_text: '$HOME/.cache/app'`), or one the handler computes when the input is unset, so there is no literal to show. Display only: the value the input takes is still `default`, or nothing.
DefaultText string `json:"default_text,omitempty"`
// Map-typed flags only, and only with 'any' values ('map'/'object' → map[string]any). When true, a '.'-separated key in a key=value pair assigns into nested maps, helm-style: --set image.tag=v2 → map[image][tag]=v2. Opt-in because '.' is a legal character in plain map keys — without it, --label a.b=c stores the literal key 'a.b'. Each assignment overwrites whatever is at its path (creating intermediate maps as needed), so later pairs win and --set a=1 --set a.b=2 leaves a nested map under 'a'. A value is read as its JSON spelling would be — true/false, null and JSON numbers are booleans, null and numbers; anything else is text — so --set replicas=3 stores the number 3, as a config file's replicas: 3 does. Declare 'properties' on the flag's schema to give shell completion the known key paths (offered up to the '=').
DottedKeys bool `json:"dotted_keys,omitempty"`
// Config inputs only: reads this input from one named config_files entry only, never from the other files, so a key present in another file does not satisfy it (or its 'required'). Omit it to read through the declared order, where the first file with the key wins.
File string `json:"file,omitempty"`
// Flag inputs only: where this flag's value may come from, besides the text on the command line.
//
// - 'file' — a value starting with '@' is replaced by the named file's contents (`--token @/run/secret`; pair with `secret: true` for a token file)
// - 'stdin' — a value of exactly '-' is replaced by what is piped on stdin (`-f -`); empty stdin is then a usage error, and a command cannot combine a from:stdin flag with a declared stdin: input, since stdin can be read only once
// - 'value' — always allowed; listing it is documentation only. Any value that does not match an enabled marker stays literal
//
// Text read from a file or stdin has one trailing line ending removed (leading and interior whitespace is kept), then goes through the normal type, enum and constraint checks: the flag's value is that text. On an object-valued flag the text is decoded as the object (JSON, or YAML when it spans lines); a structured payload for the whole command belongs in the command's stdin: input.
//
// Without 'from', '@' and '-' are ordinary characters. Defaults and environment and config fallbacks are always literal: the markers apply only to values given on the command line.
From []string `json:"from,omitempty"`
// With 'enum' only: match a value against the enum without regard to case, so `--mode FAST` is accepted against [fast, slow]. The value is stored in its declared spelling (fast), so a handler compares against one form. Applies wherever the input reads a value: the command line, a flag's environment and config fallbacks, and env and config inputs.
IgnoreCase bool `json:"ignore_case,omitempty"`
// Flags only: the value a flag takes when it is given without one, which makes its value optional (the `--color[=when]` shape). With `implicit_value: always`, a bare `--color` means always, `--color=never` sets never, and a flag left out takes its `default` as usual. Because the value is optional it must be attached: in `--color never`, `never` is the next argument, not the flag's value (a short flag attaches too: `-cnever`, `-c=never`). Help shows the flag as `--color[=<type>]` with `(implicit: always)`. For a scalar flag that is not a bool (a bool already works this way, with true), and the value must satisfy the flag's type, enum and constraints.
ImplicitValue any `json:"implicit_value,omitempty"`
// Dotted key path the value is read from (config inputs and flag config-fallbacks; e.g. 'server.port'). Segments of letters/digits/_/-, joined by dots; rotini resolves it through the configuration files (and SNAKE_UPPER of it names a flag's env fallback variable). Help shows a flag's key under the flag when the command reads configuration files.
Key string `json:"key,omitempty"`
// Time inputs only (time, datetime, date, time.Time, and lists of them): how the value is written. A Go reference-time layout (the reference time Mon Jan 2 15:04:05 MST 2006 written the way yours is: `2006-01-02`, `02/01/2006`, `Jan 2 2006 15:04`), or `unix` (seconds since the epoch, fractions allowed) or `unixmilli` (milliseconds). A layout with no zone parses as UTC. Without it, `date` reads `2006-01-02` (that day's UTC midnight) and `time`/`datetime` read RFC 3339 (`2026-09-29T14:00:00Z`). Applies wherever the input reads a value, and a `default` must parse under it.
Layout string `json:"layout,omitempty"`
// Bool flags only: also accept a `--no-<name>` form for every long identifier, which sets the flag false. `--color` with negatable declares `--no-color` too.
//
// Use it to turn something off for one run when a default, a config file or an environment variable already turned it on, which a plain bool flag cannot do. Short identifiers get no negated form.
//
// A declared identifier always wins over a derived negated one, so a flag you declare as `--no-cache` keeps that name. The negated form takes no value: `--no-color=true` is a parse error. The generated field is the same single bool either way, and help shows it as `--[no-]color`.
Negatable bool `json:"negatable,omitempty"`
// Env inputs only, map-typed ('map'/'object' → map[string]any): the separator that collects a family of environment variables into this one nested input. The variable prefix is 'variable:' when set, else the UPPER_SNAKE form of the input's name. With name: http, variable: ACME_HTTP, nesting: "__", ACME_HTTP__TIMEOUT=30 and ACME_HTTP__RETRY__MAX=9 bind as http = {timeout: "30", retry: {max: "9"}} (segments lowercased; values are strings). 'required: true' is an error when no matching variables exist. 'default:' is rejected: set defaults in code or in a config file instead.
Nesting string `json:"nesting,omitempty"`
// Display name for this input's value in generated help, man pages and usage lines: `--file <PATH>` instead of the type, `<PATH>` instead of the argument's name. Presentation only: parsing, completion and the generated field are unchanged. Conventionally UPPERCASE or <angle-bracketed>.
Placeholder string `json:"placeholder,omitempty"`
// When true, the input must be provided (or stdin must not be empty for stdin inputs). Note: this is a boolean — unlike the string-array 'required' on Schema.
Required bool `json:"required,omitempty"`
// When true, this input's value is treated as a secret: redacted in provenance/error output by the default input reader. It does not prompt: a handler that wants to ask for the value interactively reads it without echo itself (golang.org/x/term's ReadPassword, for one); for non-interactive supply, pair secret with from: [file] (token file) or an env input.
Secret bool `json:"secret,omitempty"`
// List and map flags, and a variadic argument: split each value on this character, so `--tags a,b,c` is three tags and `--label a=1,b=2` two entries. Splitting is CSV-style: an item in double quotes keeps the separator (`--tags '"a,b",c'`), leading spaces are trimmed, and an empty value (`--tags ""`) is an empty list. Repeating the flag still appends, so `--tags a,b --tags c` is three tags. Items are split before validation, so enum, item constraints and minItems/maxItems see each one. A flag's environment-variable fallback splits the same way (TAGS=a,b); a configuration file's list binds item by item whether or not a separator is declared. Without a separator, each occurrence is one value, used as is. Not valid on env and config inputs: an env input's list is always split on commas (TAGS=a,b), and a configuration file writes a list as a list.
Separator string `json:"separator,omitempty"`
// The exact environment variable this input reads, instead of the name rotini would derive. It may be a list, first preferred: `variable: [GH_TOKEN, GITHUB_TOKEN]` reads the first one that is set, for a value other tools already know under more than one name. Help lists every name. For a flag, help shows them under the flag, in lookup order. A nested env input (`nesting:`) takes one name, since it is the prefix of a family of variables. Valid on env inputs and on flags (as a flag's environment fallback); rejected on arguments, config inputs and stdin, which have no environment variable.
//
// A variable named here is never given the `env_prefix`: it is already exact, and prefixing it would silently make it a different variable.
//
// On a flag it also turns on the fallback chain (command line, then environment, then config file, then default), keyed by the flag's own name unless `key:` names one. So `--token` with `variable: GITHUB_TOKEN` reads that variable directly, with no need for a config `key:` whose UPPER_SNAKE form happens to match. Because the flag now has a key, a configuration file that defines that key supplies it too; declare `key:` to control what that key is.
Variable any `json:"variable,omitempty"`
}
Extended schema for input definitions (flags, arguments, env vars, config values, stdin). Inherits all BaseSchema fields and adds input-level metadata. The 'required' field here is a boolean indicating whether this input must be provided — unlike Schema where 'required' is a string array of property names.
type InputSchemaComplete ¶
type InputSchemaComplete struct {
// Narrows kind 'file' to these suffixes, written without a leading dot ('yaml', 'json'). Omitted, every file is offered. Rejected on the other kinds, which have no extensions to filter.
Extensions []string `json:"extensions,omitempty"`
// 'file': complete file paths (narrowed by 'extensions'). 'directory': complete directories only. 'none': complete nothing, which is not the same as declaring no hint. With no hint the shell applies its own default, and for bash and zsh that is file completion; 'none' turns it off, so for an opaque value (a container id, an API resource name) the shell does not offer the files in the current directory.
Kind string `json:"kind,omitempty"`
// A line the shell shows while this input's value is being completed and there is nothing to offer, such as `a service name from deploy.yaml`. One line. It shows only when the conf's completion feature sets `messages`, and in zsh and bash 4.4 or later; other shells skip it. A message a completer adds with rtx.AddCompletionMessage takes its place.
Message string `json:"message,omitempty"`
}
Shell-completion hint for this input's value: for the common case of a file or directory, between a fixed `enum` and a completer written in Go (FlagValueCompleter), plus an optional message to show when there is nothing to offer. Declare 'kind', 'message' or both.
Flags and arguments only. Each generated completion script turns the hint into that shell's own path completion. A completer written in Go still wins when it answers; the hint is the fallback.
type Inputs ¶
type Inputs struct {
Flags []FlagInput
Arguments []ArgumentInput
ConfigFiles []ConfigurationFile
Config []ConfigInput
Env []EnvInput
Stdin *StdinSpec
FlagGroups []FlagGroup
FlagDependencies []FlagDependency
}
Inputs bundles the input channels the spec schema flattens onto a command, for helpers that treat a command's inputs as a unit.
type OutputSchemasConfig ¶ added in v1.2.0
type OutputSchemasConfig struct {
// Module-root-relative directory (no leading slash) the output schemas are written to.
Dir string `json:"dir"`
}
A directory of JSON Schemas, one per declared output: '<page-name>.output.json' for a command's `output:` (taskr-list.output.json) and '<page-name>.exit-<code>.output.json' for an `exit_status` entry's `output:`. A schema is standard JSON Schema (draft-07): rotini's type names are written as JSON Schema types, and the spec's named schemas it references are included as definitions. Hidden commands get none. Rewritten on every `generate`; a '*.output.json' file in the directory that no output produces any more is removed.
type PackageConfig ¶
type PackageConfig struct {
// Module-root-relative path (no leading slash) ending in '.go' for the file rotini writes for this target. Its parent directory is the package directory. 'cmd' defaults to 'internal/cmd/<root-command>/zz_rotini.go'; 'main' has no default and is written only when 'file' is set.
File string `json:"file,omitempty"`
// Text written at the very top of every Go file this target produces (the generated file, the handler files and the entrypoint), above rotini's own 'Code generated by rotini' line. It is written as is, so write complete comment lines yourself (each starting with '//' or wrapped in /* */); a build constraint needs a blank line after it, as Go requires. Use it for a license or copyright header your repository requires on every .go file, or for a '//go:build' constraint. It is applied to the generated file on every run, and to a file created once (a handler file or the entrypoint) only when that file is first written, so editing the header later does not rewrite a file you already own.
Header string `json:"header,omitempty"`
// Package-relative paths (e.g. 'helpers.go') that rotini must never remove.
//
// You rarely need it: rotini only ever removes files it wrote that no longer match the spec, such as a handler file whose command has left the spec (identified by the generated marker it carries), and it reports each one. A handler file goes in two steps: the next generate disables it with `//go:build ignore`, and the one after deletes it. A file you wrote is never removed, whatever it is named, and neither are test files or the editable feature templates. Use 'keep' when you have kept a handler file whose command is gone and left its marker in place, or to protect a rendered output file under an embed_dir.
Keep []string `json:"keep,omitempty"`
// Go package name written at the top of 'file'. Defaults to the file's parent-directory name (sanitized to a valid Go identifier). For type 'main' it must be 'main'. Targets that resolve to the same 'file' must declare the same 'package'.
Package string `json:"package,omitempty"`
// Which code this target receives.
//
// - main: the program's entrypoint (main.go), created once and never overwritten.
// - cmd: the CLI package. Its directory holds the handler files you edit and the one generated file, which holds the typed input and output structs (what rtx.Inputs[T] and the per-source input methods fill), the command tree with NewProgram, ProgramHandlers and InputSettings, and the handler wiring (Program and Handlers()).
// - models: only the typed input and output structs, in their own package. Optional: without it they live in the cmd file. Declare it when a command uses a handler from another package (`handler:`) and that package needs the input types: the cmd package imports the handler package, so the handler package cannot import cmd back, and with models both import it instead. The cmd package re-exports every model as a type alias, so handler code inside cmd is unaffected either way.
//
// There is no 'runtime' target: the rotini runtime is imported from github.com/go-rotini/rotini, not generated.
Type string `json:"type"`
}
type Planned ¶ added in v1.3.0
Planned reports a dry run: the timing line, and each change the run would make, one per line ("2. create internal/cmd/app/app_add.go (311 bytes, mode 0644)"). No changes means the files on disk are already what the spec generates.
type PluginDiscovery ¶ added in v1.2.0
type PluginDiscovery struct {
// When true, discovered plugins still dispatch but are omitted from completion listings.
Hidden bool `json:"hidden,omitempty"`
// Executable-name prefix to discover. Default: the host binary name followed by '-' (e.g. 'acme-').
Prefix string `json:"prefix,omitempty"`
}
Auto-expose external '<prefix>*' executables as plugin sub-commands (kubectl/git/gh plugin style), alongside any declared plugins. Presence enables discovery; a discovered name that collides with a declared command or plugin is skipped.
type PluginSpec ¶ added in v1.2.0
type PluginSpec struct {
// Additional names that invoke this plugin.
Aliases []string `json:"aliases,omitempty"`
// Name of the declared plugin. The dispatched binary is named <program>-<name>, and is searched for next to the host binary, then in the command's plugin_path, then on PATH. Inside a $ref-composed subtree <program> is the composed spec's own name, so one installed plugin serves both that spec's own binary and a parent that composes it.
Name string `json:"name"`
// Short one-liner shown next to this plugin in its parent's generated Commands list.
Summary string `json:"summary,omitempty"`
// Host-side timeout for running the plugin. Uses Go duration format (e.g. "10s", "1m30s"). Empty or omitted means no timeout.
Timeout string `json:"timeout,omitempty"`
}
type Processor ¶
type Processor struct {
// contains filtered or unexported fields
}
Processor drives rotini's pipeline for Generate, Validate and Initialize:
reconcile (read + decode) → validate (version + schema) → lint (rotini rules) → generate
It holds only immutable state (the binary version and compiled schemas); per-pass values flow between stages, so one Processor can drive repeated watch-mode passes.
func NewProcessor ¶
NewProcessor returns a Processor for the given binary version. It panics if the embedded schemas fail to compile, which is a rotini build defect rather than a user error.
func (*Processor) Generate ¶
func (p *Processor) Generate(specPath, confPath string, watch bool, onGenerate func(result string, err error), onNotices func(notices []error)) error
Generate reconciles, validates and emits the program, once or, with watch, on every spec or conf change until interrupted. onGenerate (optional) receives a "[HH:MM:SS] <took>" summary and each pass's error; without watch the pass's error is also returned. onNotices (optional) receives each pass's non-fatal findings: validation warnings, pruned files, and hook-audit warnings about handler files.
func (*Processor) GenerateDryRun ¶ added in v1.3.0
func (p *Processor) GenerateDryRun(specPath, confPath string, onNotices func(notices []error)) (Planned, error)
GenerateDryRun reconciles, validates and plans the program exactly as Processor.Generate does, and writes nothing. onNotices (optional) receives validation and hook-audit warnings; a file the run would prune is a change, not a notice.
func (*Processor) Initialize ¶
func (p *Processor) Initialize(name, format string, force bool) (Initialized, error)
Initialize scaffolds a new CLI named name: it writes the seed spec and conf in format, then validates and generates a ready-to-build program. force replaces an existing seed spec and conf. It never deletes files; stale handlers are pruned by the next generate.
func (*Processor) InitializeDryRun ¶ added in v1.3.0
func (p *Processor) InitializeDryRun(name, format string, force bool) (Initialized, error)
InitializeDryRun plans everything Processor.Initialize would write, and writes nothing. The result's Changes lists each file it would create or replace.
func (*Processor) Validate ¶
func (p *Processor) Validate(specPath, confPath string, watch bool, failMode string, onValidate func(result string, err error), onWarnings func(warnings []error)) error
Validate reconciles, validates and lints both documents, once or, with watch, on every change. failMode overrides the conf's validate.fail ("fast" or "collect"; "" uses the conf). onValidate (optional) receives a summary and each pass's error; onWarnings (optional) receives each pass's non-fatal warnings.
type Schema ¶
type Schema struct {
BaseSchema
// What this shape or property is, in a sentence. Rotini writes it as the Go doc comment on the generated type or struct field (a named schema, an output or stdin shape, and each of their properties), so the code a handler reads explains itself. Documentation only: it changes no validation and no field. Accepted on object schemas and their properties; an input's own schema uses `summary:` on the input instead.
Description string `json:"description,omitempty"`
// Required property names for object schemas (standard JSON Schema semantics).
Required []string `json:"required,omitempty"`
}
JSON Schema-inspired type definition used for output/response and object property schemas. The 'required' field is a string array of required property names (JSON Schema object semantics). For input schemas where 'required' means 'must be provided', use InputSchema instead.
type SchemaConfig ¶
type SchemaConfig struct {
// Module-root-relative path (no leading slash) ending in '.json' where the JSON Schema is written. Overwritten on every `generate`, and never removed. Point a document's `$schema:` key at it for completion and validation in your editor.
File string `json:"file"`
}
A single schema write target: the project-relative path the embedded JSON Schema is written to.
type SchemasConfig ¶
type SchemasConfig struct {
// Where to write rotini's conf-schema (the schema for this .rotini.conf file).
Conf *SchemaConfig `json:"conf,omitempty"`
// Where to write one JSON Schema per command output declared in the spec, so scripts and other tools can validate what a command writes.
Output *OutputSchemasConfig `json:"output,omitempty"`
// Where to write rotini's spec-schema (the schema for .rotini.spec files).
Spec *SchemaConfig `json:"spec,omitempty"`
}
Where to write rotini's JSON Schemas into this project. Each entry is optional: declare 'conf' and/or 'spec' with a 'file' to have `rotini generate` write that schema there, overwriting it on every run. The files are never removed. Point a document's `$schema:` key at them.
type Spec ¶
type Spec struct {
// Optional URI identifying the rotini spec schema, for editor tooling only: rotini never fetches it, and the version check reads the top-level `version` key, not this. Any URI is accepted: a released schema (https://raw.githubusercontent.com/go-rotini/rotini/refs/tags/v1.3.0/schema-spec.json — note the 'v', matching the git tag), a path written into your project by the conf's `generate.schemas.spec.file`, or a fork's own URL. A relative path is resolved by your editor, not by rotini. `rotini init` seeds this key (`$schema: ./.rotini-schema.spec.json`), and it works in every format; YAML editors also accept a `# yaml-language-server: $schema=<path>` comment in its place.
Schema string `json:"$schema,omitempty"`
// The CLI's root command (the binary itself): its name, doc-fields, inputs (flags/arguments/env/config/config_files/stdin) and sub-commands. The root must use 'name' (not '$ref'). The root-command-level keys `env_prefix` and `schemas` live here.
Command Command `json:"command"`
// The minimum rotini version this spec requires (X.Y.Z): the feature set it was written against, not an exact pin. Any rotini of the same major version at or beyond it accepts the document, so a patch or minor upgrade never requires an edit here. Two cases are errors: a rotini older than this, which may not know keys the spec uses, and a different major version. The check is skipped for a development build of rotini, which reports no release version (0.0.0, or none at all). This key, not the optional `$schema` URL, is what the check reads.
Version string `json:"version"`
}
The spec describes your CLI: its commands, their inputs and what they write. The document holds a top-level `version` and one root `command` (the program itself), and every sub-command below it has the same shape. `env_prefix` and `schemas` are valid only on the root command, and `$schema` is an optional key for editor tooling.
type StdinSpec ¶
type StdinSpec struct {
// How the piped stdin payload is read.
//
// The four document formats (json, yaml, jsonc, toml) decode it into the generated <Prefix>Stdin struct, validated against the declared schema. The default is json.
//
// The two raw formats are for commands whose stdin is not a document, such as text filters: 'text' binds the whole payload as a single string, and 'lines' binds it as []string split on newlines (a trailing newline adds no empty element). The schema's type must match ('string' for text, '[]string' or 'array' for lines), and neither generates a <Prefix>Stdin struct, because there is nothing to shape. Declaring stdin this way, rather than reading rtx.Stdin directly, puts it in the command's help page and completion.
Format string `json:"format,omitempty"`
// Type definition for stdin content. Set required: true in schema to error when stdin is empty.
Schema *InputSchema `json:"schema,omitempty"`
}
type ValidateConfig ¶
type ValidateConfig struct {
// fast = stop at and report the first problem; collect = run to completion and report every problem at once (default).
Fail string `json:"fail,omitempty"`
}
Controls how `rotini validate`, and the validation `rotini generate` runs first, report problems. Strictness is fixed (validation is always strict); only the failure-reporting mode is configurable. `rotini validate --fail` overrides this.
type ValidateFn ¶
type ValidateFn = func(specPath, confPath string, watch bool, failMode string, onValidate func(result string, err error), onWarnings func(warnings []error)) error
ValidateFn is the signature of Processor.Validate. The companion CLI injects it as a dependency so tests can substitute a double (see GenerateFn).
Source Files
¶
- doc.go
- generate.go
- generate_completion.go
- generate_contract.go
- generate_features.go
- generate_gosource.go
- generate_hookaudit.go
- generate_inputs.go
- generate_layout.go
- generate_literals.go
- generate_messages.go
- generate_naming.go
- generate_output.go
- generate_prune.go
- generate_ref.go
- generate_renderer.go
- generate_roff.go
- generate_schemagen.go
- generate_tree.go
- generate_writer.go
- initialize.go
- lint_compose.go
- lint_conf.go
- lint_cross.go
- lint_spec.go
- lint_suggest.go
- lint_values.go
- processor.go
- processor_watch.go
- reconcile.go
- reconcile_position.go
- reconcile_reader.go
- schema.go
- schema_accessors.go
- schema_conf.go
- schema_spec.go
- schema_traverse.go
- validate.go
- validate_problem.go