.rotini.conf.yaml
Schema for a Rotini CLI configuration file.
.rotini.conf.yaml — every key
# Every key the conf accepts, in one valid file. `rotini validate` checks it on every test run.
$schema: ./.rotini-schema.conf.json
version: 1.0.0
generate:
schemas:
spec:
file: cmd/deploy/.rotini-schema.spec.json
conf:
file: cmd/deploy/.rotini-schema.conf.json
packages:
- type: main
file: cmd/deploy/main.go
- type: cmd
file: internal/cmd/deploy/zz_rotini.go
package: deploy
header: "// Copyright 2026 Example, Inc."
keep: [store.go]
- type: models
file: internal/models/zz_models.go
package: models
features:
- type: help
enabled: true
embed: true
embed_dir: internal/cmd/deploy/help
template: true
template_dir: internal/cmd/deploy/templates
- type: completion
enabled: true
- type: man
enabled: false
- type: markdown
enabled: false
validate:
fail: collectthe JSON Schema
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Rotini Configuration Schema",
"description": "Schema for a Rotini CLI configuration file.",
"type": "object",
"required": ["version"],
"additionalProperties": false,
"properties": {
"$schema": {
"type": "string",
"description": "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.0.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."
},
"version": {
"type": "string",
"description": "The MINIMUM rotini this conf requires (X.Y.Z) — the feature set it was written against, not an exact pin. Any rotini of the same major at or beyond it accepts the document, so a patch or minor upgrade never forces an edit here. A rotini older than this, or a different major, 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). Mirrors the spec document's `version` key.",
"pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$", "examples": ["1.0.0"],
"default": "0.0.0"
},
"generate": {
"$ref": "#/definitions/GenerateConfig",
"description": "Controls `rotini generate`: package targets and derived features. Omitted entirely → the defaults (merged single-file layout under internal/cmd/<root>, all features off)."
},
"validate": {
"$ref": "#/definitions/ValidateConfig",
"description": "Controls how `rotini validate` and `rotini generate` report problems (collect everything vs. fail fast)."
}
},
"definitions": {
"ValidateConfig": {
"type": "object",
"additionalProperties": false,
"description": "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.",
"properties": {
"fail": {
"type": "string",
"enum": ["fast", "collect"],
"default": "collect",
"description": "fast = stop at and report the first problem; collect = run to completion and report every problem at once (default)."
}
}
},
"GenerateConfig": {
"type": "object",
"additionalProperties": false,
"description": "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').",
"properties": {
"schemas": {
"$ref": "#/definitions/SchemasConfig",
"description": "Opt-in: where to write rotini's own embedded conf- and spec-schema JSON Schemas into this project, so a document's `$schema:` key (what `rotini init` seeds) can point at a local copy instead of a remote URL."
},
"packages": {
"type": "array",
"items": { "$ref": "#/definitions/PackageConfig" },
"description": "Generated code targets, one per `type` (main / cmd / models). 'main' is the binary entrypoint (create-once). 'cmd' is the cli package: the editable per-command handler stubs PLUS the one generated file (the framework glue, the handlers rollup, and — unless 'models' splits them out — the typed input/output structs). 'models' is OPTIONAL: declare it to put the typed structs in their own package, which a handler package sourced via a command's `handler:` can import without an import cycle. The rotini runtime is NOT generated — it is an ordinary library dependency the generated code imports (`go get github.com/go-rotini/rotini`)."
},
"features": {
"type": "array",
"items": { "$ref": "#/definitions/Feature" },
"description": "The derived codegen outputs, one per `type` (help / completion / man / markdown), each an opt-in toggle plus its embed/template sourcing knobs."
}
}
},
"SchemasConfig": {
"type": "object",
"additionalProperties": false,
"description": "Where to write rotini's embedded JSON Schemas into this project. Each entry is opt-in: declare 'conf' and/or 'spec' with a 'file' to have `rotini generate` write that schema there (overwriting it from the embedded bytes each pass). The files are not pruned. Intended target for a document's `$schema:` key.",
"properties": {
"conf": {
"$ref": "#/definitions/SchemaConfig",
"description": "Where to write rotini's conf-schema (the schema for this .rotini.conf file)."
},
"spec": {
"$ref": "#/definitions/SchemaConfig",
"description": "Where to write rotini's spec-schema (the schema for .rotini.spec files)."
}
}
},
"SchemaConfig": {
"type": "object",
"additionalProperties": false,
"required": ["file"],
"description": "A single schema write target: the project-relative path the embedded JSON Schema is written to.",
"properties": {
"file": {
"type": "string",
"pattern": "^[^/\\\\].*\\.json$", "examples": ["schema/spec.json"],
"description": "Module-root-relative path file (no leading slash) ending in '.json' where the embedded JSON Schema is written. Overwritten from the embedded bytes on every `generate`; never pruned. Point a document's `$schema:` key at it for local editor completion and validation."
}
}
},
"PackageConfig": {
"allOf": [
{
"type": "object",
"additionalProperties": false,
"required": ["type"],
"description": "One generated code target, discriminated by 'type'. 'file' is the module-root-relative path ending in '.go' the rotini-controlled code is written to; 'package' is the Go package name at its top; 'keep' lists package-relative paths the pruner must spare.",
"properties": {
"type": {
"type": "string",
"enum": ["main", "cmd", "models"],
"description": "Which generated category this target receives. main = the binary entrypoint (main.go, create-once: never overwritten). cmd = the cli package: it locates the editable per-command handler stubs AND the single generated file written into that directory — the typed input/output structs (Collect[T]/Parse* targets), the framework glue (the command-tree definition, NewProgram, ProgramHandlers, BindMeta), and the handlers rollup (handlers struct, Program, Handlers(), command→handler wiring), all merged into the one 'file'. models = ONLY the typed input/output structs, in their own package. Optional: without it they live in the cmd file. Declare it when a command sources its handler from another package (`handler:`) AND that package needs its own input types — the cmd package imports the handler package, so the handler package cannot import cmd back. 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 emitted."
},
"file": {
"type": "string",
"pattern": "^[^/\\\\].*\\.go$", "examples": ["internal/cmd/app/zz_app.go"],
"description": "Module-root-relative path (no leading slash) ending in '.go' for the rotini-controlled file this category is written to. Its parent directory is the target package directory. 'cmd' defaults to 'internal/cmd/<root-command>/zz_rotini.go' (the cli package's generated file); main has no default and is only written when 'file' is set."
},
"package": {
"type": "string",
"pattern": "^[a-z][a-z0-9_]*$", "examples": ["app"],
"description": "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'."
},
"header": {
"type": "string",
"description": "Text written at the very top of every Go file this target produces — the generated file, the editable handler stubs, and the entrypoint — ABOVE rotini's own 'Code generated by rotini' line. Verbatim and unformatted, so write complete comment lines yourself (each starting with '//' or wrapped in /* */); a build constraint needs its own blank line after it, as Go requires. Intended for a license or copyright header a repository mandates on every .go file, and for a '//go:build' constraint. Re-applied on every generate, and applied to a create-once file only when that file is first written — editing the header later does not rewrite a stub or an entrypoint you already own.",
"examples": [
"// Copyright 2026 Acme, Inc.\n// SPDX-License-Identifier: Apache-2.0"
]
},
"keep": {
"type": "array",
"items": {
"type": "string",
"pattern": "^[^/\\\\].*$", "examples": ["extra.go"]
},
"description": "Package-relative paths (e.g. 'helpers.go') that pruning must never remove.\n\nYou rarely need it: pruning only ever removes files ROTINI WROTE — an orphaned handler stub, identified by the generated marker it carries — and reports each one. A file you wrote is never a candidate, whatever it is named, and neither are test files or the editable per-feature templates. Reach for 'keep' when you have adopted a generated stub as your own AND kept its marker, or to protect a rendered output file under an embed_dir."
}
}
},
{
"$comment": "type:main pins the Go package to 'main'.",
"if": { "required": ["type"], "properties": { "type": { "const": "main" } } },
"then": { "properties": { "package": { "const": "main" } } }
}
]
},
"Feature": {
"type": "object",
"additionalProperties": false,
"required": ["type"],
"description": "One rendered/derived codegen feature, discriminated by 'type' (help/completion/man/markdown): a toggle (enabled) plus two orthogonal sourcing knobs — embed (//go:embed a rendered file in 'embed_dir' vs an inline string literal) and template (seed the editable rendering template into 'template_dir' vs render from the built-in). The two dirs default from the cmd package — embed_dir to '<cmd-package>/renders', template_dir to '<cmd-package>/templates'. In embed mode embed_dir must resolve under the cmd package (//go:embed cannot reach outside it); inline features and template_dir have no such constraint. Output files never collide: help pages are 'help_*.txt', man pages 'man_*.txt', markdown 'markdown_*.md', completion scripts 'completion_<shell>.txt', with pruning scoped to each feature's own files.",
"properties": {
"type": {
"type": "string",
"enum": ["help", "completion", "man", "markdown"],
"description": "Which derived output this entry configures. help/man/markdown are per-command render-or-verbatim doc pages (rendered from the command's structured doc-fields via the editable template, or written verbatim when the command sets that string in the spec), each emitting a '<Prefix>' var + 'Help/Man/Markdown(path ...string) (string, error)' resolver. completion is the exception: per-shell scripts (bash/zsh/fish/powershell) generated from the program name, with no editable template and no verbatim escape, emitting 'Completion<Shell>' vars + a 'Completion(shell string) (string, error)' resolver; the scripts call the binary's hidden '__complete' entry."
},
"enabled": {
"type": "boolean",
"default": false,
"description": "When true, rotini generates this feature's outputs into the cmd package and emits the embed vars + resolver. Opt-in only."
},
"embed": {
"type": "boolean",
"default": false,
"description": "How this feature's generated content is sourced into the cmd package's generated file. true: the rendered content is written to a file under 'embed_dir' and the Go var is backed by a //go:embed directive. false (the default): no output file is written — the Go var is a hardcoded string literal holding the content inline, so the generated .go is self-contained. Var names and the resolver are identical either way."
},
"embed_dir": {
"type": "string",
"pattern": "^[a-zA-Z0-9_][a-zA-Z0-9_./-]*$", "examples": ["internal/cmd/app/renders"],
"description": "Directory (relative to the module root) where this feature's rendered OUTPUT files (help_*.txt / man_*.txt / markdown_*.md / completion_<shell>.txt) are written in embed mode (embed: true) and sourced via //go:embed — so in embed mode it MUST resolve under the cmd package (//go:embed cannot reach outside it). In inline mode (embed: false) no output files are written and this is unused — rotini validation WARNS (non-fatal) if you set it there. Defaults to '<cmd-package>/renders'."
},
"template": {
"type": "boolean",
"default": false,
"description": "Whether the editable rendering template (help.txt.tmpl / man.txt.tmpl / markdown.md.tmpl) is seeded into 'template_dir' for customization. true: the template is seeded when missing and pages render from it. false (the default): no template is seeded and pages render from rotini's built-in default. Has no effect on completion (which has no editable template) — rotini validation WARNS if you set it there. A template you have already edited is never pruned — flipping this to false leaves it in place but inert."
},
"template_dir": {
"type": "string",
"pattern": "^[a-zA-Z0-9_][a-zA-Z0-9_./-]*$", "examples": ["templates/help"],
"description": "Directory (relative to the module root) where this feature's editable rendering template (help.txt.tmpl / man.txt.tmpl / markdown.md.tmpl) is seeded when template: true. Templates are never //go:embed'd, so this is unconstrained — it may resolve anywhere. Unused when no template is seeded (template: false, or completion which has none) — rotini validation WARNS (non-fatal) if you set it there. Defaults to '<cmd-package>/templates'."
}
}
}
}
}Below, every key. rotini validate checks all of them before any code is generated, and
this list is rendered from the schema, so it always matches what the tool accepts.
Document
version
string · required · default 0.0.0
The MINIMUM rotini this conf requires (X.Y.Z) — the feature set it was written against, not an exact pin. Any rotini of the same major at or beyond it accepts the document, so a patch or minor upgrade never forces an edit here. A rotini older than this, or a different major, 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). Mirrors the spec document’s version key.
$schema
string
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.0.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.
generate
Controls rotini generate: package targets and derived features. Omitted entirely → the defaults (merged single-file layout under internal/cmd/
validate
Controls how rotini validate and rotini generate report problems (collect everything vs. fail fast).
GenerateConfig
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’).
features
array of Feature
The derived codegen outputs, one per type (help / completion / man / markdown), each an opt-in toggle plus its embed/template sourcing knobs.
packages
array of PackageConfig
Generated code targets, one per type (main / cmd / models). ‘main’ is the binary entrypoint (create-once). ‘cmd’ is the cli package: the editable per-command handler stubs PLUS the one generated file (the framework glue, the handlers rollup, and — unless ‘models’ splits them out — the typed input/output structs). ‘models’ is OPTIONAL: declare it to put the typed structs in their own package, which a handler package sourced via a command’s handler: can import without an import cycle. The rotini runtime is NOT generated — it is an ordinary library dependency the generated code imports (go get github.com/go-rotini/rotini).
schemas
Opt-in: where to write rotini’s own embedded conf- and spec-schema JSON Schemas into this project, so a document’s $schema: key (what rotini init seeds) can point at a local copy instead of a remote URL.
ValidateConfig
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.
fail
string · one of fast, collect · default collect
fast = stop at and report the first problem; collect = run to completion and report every problem at once (default).
Feature
One rendered/derived codegen feature, discriminated by ’type’ (help/completion/man/markdown): a toggle (enabled) plus two orthogonal sourcing knobs — embed (//go:embed a rendered file in ’embed_dir’ vs an inline string literal) and template (seed the editable rendering template into ’template_dir’ vs render from the built-in). The two dirs default from the cmd package — embed_dir to ‘
type
string · required · one of help, completion, man, markdown
Which derived output this entry configures. help/man/markdown are per-command render-or-verbatim doc pages (rendered from the command’s structured doc-fields via the editable template, or written verbatim when the command sets that string in the spec), each emitting a ‘
embed
boolean · default false
How this feature’s generated content is sourced into the cmd package’s generated file. true: the rendered content is written to a file under ’embed_dir’ and the Go var is backed by a //go:embed directive. false (the default): no output file is written — the Go var is a hardcoded string literal holding the content inline, so the generated .go is self-contained. Var names and the resolver are identical either way.
embed_dir
string
Directory (relative to the module root) where this feature’s rendered OUTPUT files (help_.txt / man_.txt / markdown_*.md / completion_
enabled
boolean · default false
When true, rotini generates this feature’s outputs into the cmd package and emits the embed vars + resolver. Opt-in only.
template
boolean · default false
Whether the editable rendering template (help.txt.tmpl / man.txt.tmpl / markdown.md.tmpl) is seeded into ’template_dir’ for customization. true: the template is seeded when missing and pages render from it. false (the default): no template is seeded and pages render from rotini’s built-in default. Has no effect on completion (which has no editable template) — rotini validation WARNS if you set it there. A template you have already edited is never pruned — flipping this to false leaves it in place but inert.
template_dir
string
Directory (relative to the module root) where this feature’s editable rendering template (help.txt.tmpl / man.txt.tmpl / markdown.md.tmpl) is seeded when template: true. Templates are never //go:embed’d, so this is unconstrained — it may resolve anywhere. Unused when no template is seeded (template: false, or completion which has none) — rotini validation WARNS (non-fatal) if you set it there. Defaults to ‘
PackageConfig
One generated code target, discriminated by ’type’. ‘file’ is the module-root-relative path ending in ‘.go’ the rotini-controlled code is written to; ‘package’ is the Go package name at its top; ‘keep’ lists package-relative paths the pruner must spare.
type
string · required · one of main, cmd, models
Which generated category this target receives. main = the binary entrypoint (main.go, create-once: never overwritten). cmd = the cli package: it locates the editable per-command handler stubs AND the single generated file written into that directory — the typed input/output structs (Collect[T]/Parse* targets), the framework glue (the command-tree definition, NewProgram, ProgramHandlers, BindMeta), and the handlers rollup (handlers struct, Program, Handlers(), command→handler wiring), all merged into the one ‘file’. models = ONLY the typed input/output structs, in their own package. Optional: without it they live in the cmd file. Declare it when a command sources its handler from another package (handler:) AND that package needs its own input types — the cmd package imports the handler package, so the handler package cannot import cmd back. 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 emitted.
file
string
Module-root-relative path (no leading slash) ending in ‘.go’ for the rotini-controlled file this category is written to. Its parent directory is the target package directory. ‘cmd’ defaults to ‘internal/cmd/
header
string
Text written at the very top of every Go file this target produces — the generated file, the editable handler stubs, and the entrypoint — ABOVE rotini’s own ‘Code generated by rotini’ line. Verbatim and unformatted, so write complete comment lines yourself (each starting with ‘//’ or wrapped in /* */); a build constraint needs its own blank line after it, as Go requires. Intended for a license or copyright header a repository mandates on every .go file, and for a ‘//go:build’ constraint. Re-applied on every generate, and applied to a create-once file only when that file is first written — editing the header later does not rewrite a stub or an entrypoint you already own.
keep
array of string
Package-relative paths (e.g. ‘helpers.go’) that pruning must never remove.
You rarely need it: pruning only ever removes files ROTINI WROTE — an orphaned handler stub, identified by the generated marker it carries — and reports each one. A file you wrote is never a candidate, whatever it is named, and neither are test files or the editable per-feature templates. Reach for ‘keep’ when you have adopted a generated stub as your own AND kept its marker, or to protect a rendered output file under an embed_dir.
package
string
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’.
SchemasConfig
Where to write rotini’s embedded JSON Schemas into this project. Each entry is opt-in: declare ‘conf’ and/or ‘spec’ with a ‘file’ to have rotini generate write that schema there (overwriting it from the embedded bytes each pass). The files are not pruned. Intended target for a document’s $schema: key.
conf
Where to write rotini’s conf-schema (the schema for this .rotini.conf file).
spec
Where to write rotini’s spec-schema (the schema for .rotini.spec files).
SchemaConfig
A single schema write target: the project-relative path the embedded JSON Schema is written to.
file
string · required
Module-root-relative path file (no leading slash) ending in ‘.json’ where the embedded JSON Schema is written. Overwritten from the embedded bytes on every generate; never pruned. Point a document’s $schema: key at it for local editor completion and validation.