code
How to write .say.cue / .say.yaml — the ordered-map rule pattern, built-in generators (auto-gomplate, auto-cue), CUE basics. Use when setting up code generation rules, linting rules, or CUE-based conf
SKILL.mdplugins/sayt/skills/code
GitHubgenerate / lint — .say.{cue,yaml,nu} Configuration
sayt generate runs code-generation rules. sayt lint runs static checks. Both are driven by configuration in .say.{cue,yaml,yml,json,toml,nu} files merged with sayt's built-in config.cue.
.say.nu files are executed and their output is piped into the CUE evaluator. All other files are cue export-merged directly.
Rule Shape
Rules are defined via an ordered map (rulemap) that flattens to a list. Each rule has a list of cmds, each of which is a nushell command block:
say:
generate:
rulemap:
my-rule:
cmds:
- do: "buf generate" # nushell expression run inside `do { ... }`
use: "./module.nu" # optional: nushell module to import first
workdir: "./gen" # optional
inputs: ["../proto/"] # optional: files the rule depends on
outputs: ["gen/"] # optional: files the rule produces
The Ordered Map Pattern
Keys enable granular composition:
- Add: introduce a new unique key.
- Modify: reference an existing key to merge fields.
- Delete: set an existing key to
null. - Order: optional
priorityfield; stable by name within priority.
say:
generate:
rulemap:
"auto-cue": null # disable a built-in
my-protobuf: # add a custom rule
priority: 10
cmds:
- do: "buf generate"
Built-in Generate Rules
sayt ships three built-in generate rules, all enabled by default:
auto-gomplate
Implemented in generate-gomplate.nu. For every X.tmpl in the current directory, runs cue export X.cue to produce JSON data, then pipes it into gomplate -d data=stdin:///data.json -f X.tmpl, saving the result as X.
Convention:
Dockerfile.tmpl + Dockerfile.cue → Dockerfile
auto-cue
Implemented in generate-cue.nu. For every X.cue whose stem X is an existing file, runs cue export X.cue --out <ext> and saves the result as X.
Convention:
compose.cue (if compose.yaml exists) → compose.yaml
auto-bayt
Implemented in auto-bayt.nu. A nop unless bayt.cue exists in the current directory; otherwise it runs bayt's generator — the sibling checkout's when sayt sits beside bayt, else mise exec -- bayt generate at the project's pin — with docker compose pinned to sayt's compose.toml stub on Unix.
Disable a built-in by nulling its key: rulemap: { "auto-gomplate": null }, { "auto-cue": null } or { "auto-bayt": null }.
Built-in Lint Sugar
sayt's built-in lint verb runs auto-cue.nu, which performs three kinds of checks declared at the top level of say.lint:
copy — files that must be byte-identical
say:
lint:
copy: ["libraries/foo/README.md", "services/bar/README.md"]
# or a list of lists for multiple groups
copies:
- ["a.json", "b.json"]
- ["Dockerfile", "Dockerfile.alt"]
Each group lists 2+ files. The first file is canonical; the others must match byte-for-byte or lint fails with a cp hint.
shared — regex-extracted values that must agree
say:
lint:
shared:
pattern: 'version = "(\d+\.\d+\.\d+)"'
files: ["Cargo.toml", "pyproject.toml", "package.json"]
The regex is extracted from every file. Missing matches fail; differing captured values fail with a side-by-side diff.
vet — cue vet for CUE-generated files
For every X.cue whose stem X is an existing file, runs cue vet X.cue <ext>:X to confirm the generated file still satisfies the CUE schema. This is the static counterpart to auto-cue generate.
All three checks run as the single built-in auto-cue lint rule. Disable with rulemap: { "auto-cue": null }.
Type Annotations for Custom Lint Rules
For rulemap entries, sayt exposes #copy, #shared, and #vet type annotations that let you attach a declarative check to a named rule instead of using the top-level sugar:
package say
say: lint: rulemap: {
"version-pins": #shared & { shared: { pattern: "v[0-9]+\\.[0-9]+\\.[0-9]+", files: [...] } }
}
Adding Custom Rules
Generate — protobuf example
say:
generate:
rulemap:
protobuf:
cmds:
- do: "buf generate ../../libraries/xproto"
outputs: ["gen/"]
Lint — run app linters and config validators
The broader lint verb is where app linters and config validators live. Use mise exec -- for any mise-managed tool invocation:
say:
lint:
rulemap:
app:
cmds:
- do: |
mise exec -- pnpm lint
mise exec -- pnpm exec tsc --noEmit
configs:
cmds:
- do: |
mise exec -- rpk connect lint services/transform/pipelines/*.yaml
mise exec -- caddy validate --config services/proxy/Caddyfile --adapter caddyfile
mise exec -- kubeconform -summary -strict k8s/base/*.yaml
See the sayt-tdd skill for why lint is the right verb for all static checks.
.say.cue — Typed Rules in CUE
package say
say: generate: rulemap: "openapi-types": {
cmds: [{
do: "openapi-typescript api.yaml -o types.ts"
inputs: ["api.yaml"]
outputs: ["types.ts"]
}]
}
CUE unifies rather than overwrites, so you can split rules across multiple .say.cue files and they merge automatically. Defaults use *value | type. Use null to delete.
.say.nu — Dynamic Rules
For configuration that needs runtime logic, write a nushell file that outputs YAML/JSON. Its output is merged with the other .say.* files:
# .say.nu
{
say: generate: rulemap: {
dynamic: {
cmds: [{ do: $"echo (date now | format date '%Y')" }]
}
}
} | to yaml
--force
sayt generate --force sets SAY_GENERATE_ARGS_FORCE=true. Built-in rules honor it via save --force=$env.SAY_GENERATE_ARGS_FORCE. Custom rules that write files should do the same.
Current flags
Run sayt help generate and sayt help lint for current flags.