This is the canonical operator guide for the lathe generator and generated
CLIs. Architecture belongs in Architecture; serialized
compatibility belongs in Machine contracts.
| Command | Purpose |
|---|---|
lathe init |
Create a CLI-first application repository from a supported starter. |
lathe skill install |
Install Lathe's bundled Agent Skill. |
lathe specsync |
Stage declared API specs and record sync state. |
lathe codegen |
Generate Go command packages and optional Skill output. |
lathe bootstrap |
Run specsync and codegen in sequence. |
lathe version |
Print generator version metadata. |
Use lathe <command> --help for the current flags. Application initialization
has a separate starter contract and maintenance guide.
Download the archive for your platform from the
latest release, unpack it,
and put lathe on PATH.
From this repository:
make build
./bin/lathe versionA generated CLI repository owns:
go.mod: downstream module path used by generated imports.cli.yaml: CLI identity and runtime capability configuration.specs/sources.yaml: declared Git or local API sources.- Optional overlay files.
cmd/<cli-name>/main.go: thin runtime entrypoint.
The normal build path is:
go mod init example.com/acme # skip when go.mod exists
lathe bootstrap
go mod tidy
go build -o bin/acmectl ./cmd/acmectl
bin/acmectl __lathe verify --jsoncli:
name: acmectl
short: Command-line tool for Acme services
command_path: auto
auth:
default_type: apikey
api_key_header: X-Auth-Token
login:
type: oauth_device
start_path: /auth/device/start
token_path: /auth/device/token
refresh_path: /auth/device/refresh
start_request:
client_id: acmectl
device_label: ${device_label}
poll_request:
client_id: acmectl
device_code: ${device_code}
poll_response:
access_token: data.token
contexts:
organization: data.organization_id
validate:
method: GET
path: /api/v1/whoami
assert:
field: data.id
non_empty: true
display:
username_field: data.username
fallback_field: data.emailcli.command_path supports:
auto(default): flatten one safe module; namespace multiple modules.namespaced: always retain the module segment.flat: require one module and fail on root-command conflicts.
auth.default_type is bearer, apikey, basic, or oauth.
oauth requires auth.login.type: oauth_device. Interactive device login
opens the verification URL by default; --no-browser keeps the flow manual.
Expired OAuth bearer credentials refresh before execution when
refresh_path and a refresh token are available. start_request and
poll_request replace the default JSON bodies; values may be literals or the
supported ${hostname}, ${provider}, ${device_label}, and poll-only
${device_code} placeholders. poll_response maps dot-separated response
paths for status, errors, tokens, expiry, user identity, and declared contexts.
Omitted mappings use the standard OAuth field names.
auth.validate proves that saved credentials work. assert.field resolves a
dot-separated JSON path; it and display paths require JSON. non_empty: true
rejects an empty field, or checks the raw response body when no field is set.
With neither an assertion nor display paths, any successful response is
accepted, including non-JSON.
Host resolution order is --hostname, $ACMECTL_HOST, the selected host,
http.default_hostname, then the single logged-in host. With several hosts and
none of the above, the command fails and lists them.
bin/acmectl auth use api.acme.comauth use marks the host selected: true in hosts.yml, as does the first
auth login. When the host is chosen implicitly and more than one is logged
in, the CLI prints current host: <name> on stderr; explicit selection is
silent.
Account-scoped defaults are opt-in:
contexts:
organization:
env: ACMECTL_ORG_ID
local_set: trueAn overlay binds an operation parameter to the context:
commands:
list-projects:
params:
organization_id:
context: organization
switch-organization:
context:
set_on_success:
name: organization
from_param: organization_idResolution order is explicit operation flag, declared environment variable,
then the selected host's stored value. auth context status|unset is generated
when contexts exist; set is added only for entries with local_set: true.
A selector operation persists its declared context only after successful
completion.
skill:
root: skills
include: internal/skill-include
bundle: trueroot: ""disables Skill generation.includemerges repo-local resources into the generated Skill.bundle: trueembeds the Skill and mounts<cli> skill install.
Object-form include supports per-file append, create, replace, and
omit policies. Includes may target SKILL.md, agents/, references/,
scripts/, and assets/. Dotfiles, symlinks, traversal, and paths inside
the generated Skill root are rejected.
When bundling is enabled, codegen pins:
go get github.com/lathe-cli/kitup/go@v0.1.3 github.com/lathe-cli/kitup/go-cobra@v0.1.3Generated CLIs expose --version and -v. Supply Version, Commit, and
Date through lathe.RunOptions or Go -ldflags; they are build metadata,
not manifest data.
Optional GitHub Release self-update configuration:
update:
github:
owner: acme
repo: acmectl
asset: "acmectl_{{ .Version }}_{{ .OS }}_{{ .Arch }}.tar.gz"The release asset must expose a sha256: digest. Archive assets must contain a
binary named after cli.name. Update asks before replacing the executable
unless --yes is passed.
workflow.commands compiles API-only multi-step commands into the generated
binary. Its DSL, failure model, conditional execution, and limits are owned by
Workflow commands.
sources:
users:
repo_url: https://github.com/acme/users-api.git
pinned_tag: v1.4.0
backend: openapi3
openapi3:
files: [openapi.yaml]
expose:
operation_ids: [Users_List, Users_Get, Users_Create]
accounts:
repo_url: https://github.com/acme/accounts-api.git
pinned_tag: v2.1.0
backend: proto
proto:
staging:
- from: api/proto
to: "."
entries: [v1/accounts.proto]
console:
repo_url: https://github.com/acme/graphql-console.git
pinned_tag: v3.0.0
backend: graphql
graphql:
schema: schema/console.graphql
expose:
queries: [listApps, getApp]
mutations: [createApp]
local:
local_path: ../service
backend: openapi3
openapi3:
files: [openapi/service.yaml]| Field | Contract |
|---|---|
repo_url + pinned_tag |
Immutable Git input. Floating branches are rejected. |
local_path |
Explicit working-tree input. Relative paths resolve from sources.yaml; it cannot be mixed with Git fields. |
backend |
swagger, openapi3, proto, or graphql. |
swagger.files |
Swagger 2.0 JSON files. |
openapi3.files |
OpenAPI 3.x JSON or YAML files. |
openapi3.expose.operation_ids |
Optional exact allowlist; every configured ID must match exactly once. |
proto.entries |
Entry files whose annotated RPCs may become commands. Imported dependencies never add commands. |
proto.dependencies |
Explicitly staged immutable Buf, Go module, or Git dependencies. |
graphql.expose |
Required query/mutation allow policy; GraphQL has no implicit expose-all mode. |
Buf dependencies require a module, commit, and digest; Go module dependencies
require a version and checksum; Git dependencies require repo_url and
pinned_tag. Staging never overwrites different content at the same include
path.
GraphQL policy can also define operation grouping, output hints, and selection
depth/pruning. Generated GraphQL commands execute POST /graphql with a baked
{query, variables} envelope. Scalar and enum arguments become typed flags;
input-object leaves become dotted variable flags. Relay-shaped results can use
body-cursor pagination.
Overlays own bounded behavior that upstream specs cannot express cleanly. They are merged during codegen and never read at runtime.
groups:
users:
short: Manage users and access
commands:
get-user:
short: Get one user
aliases: [show-user]
params:
user_id:
argument: id
help: User ID
delete-user:
hidden: trueSupported uses include command/group summaries, aliases, examples, shortcuts,
visibility, parameter help/defaults/required tightening/deprecation, positional
alternatives, active contexts, runtime schema preflights, and stream collection.
Unknown groups and conflicting root names fail codegen. Command overrides apply
only to an exact command/match; unmatched command entries and most unmatched
parameter entries are ignored. An argument entry for an unknown parameter
fails validation.
ignore: true removes a command. hidden: true keeps it out of normal help,
search, and catalog output; --include-hidden can still inspect it.
OpenAPI multipart object fields and Swagger formData parameters become
normal command flags. A field with format: binary accepts a local file path;
the runtime opens it and builds the multipart request. These commands do not use
the JSON body builder's --file, --set, or --set-str flags.
Bind a visible, bodyless, non-streaming GET operation in the same module as a
request-body schema source:
commands:
run-app:
body:
runtime_schema:
operation_id: describeApp
response_path: input_schema
params:
app_id: ${params.app_id}The schema operation uses the same hostname and cannot require stronger auth
than the target. Normal execution fetches the schema and validates the JSON
body before the target request. External $ref loading is disabled.
--dry-run remains network-free and skips this preflight. Static request-body
schemas from the API spec remain discovery metadata; they are not runtime
validators.
commands:
run-job:
output:
streaming:
data: json
event_name_path: event
collect:
require_stop: true
stop_events: [done]
pause_events: [input_required]
error_events: [error]
fields:
- events: [chunk]
from: text
to: output
reduce: concat
live:
events: [chunk]
from: textReducers are first, last, concat, and append. JSON/YAML returns one
collected document, raw output preserves wire events, and --stream prints the
configured live field in the default output mode. A pause is a successful
terminal outcome; a workflow stops before its next step.
lathe bootstrapEquivalent explicit phases:
lathe specsync
lathe codegenUseful overrides:
lathe specsync -source users
lathe specsync -cache .cache
lathe codegen -overlay internal/overlay
lathe codegen -skill-root ""
lathe codegen -skill-include internal/skill-includePrefer cli.yaml over one-off Skill flags when generation must be reproducible.
Wire the generated package:
package main
import (
_ "embed"
"os"
"github.com/lathe-cli/lathe/pkg/lathe"
"example.com/acme/internal/generated"
)
//go:embed cli.yaml
var manifestBytes []byte
func main() {
os.Exit(lathe.Run(lathe.RunOptions{
Manifest: manifestBytes,
Mount: generated.Mount,
}))
}Copy cli.yaml beside main.go, then:
go mod tidy
go build -o bin/acmectl ./cmd/acmectl
bin/acmectl __lathe verify --jsonbin/acmectl __lathe verify --json
bin/acmectl search "create user" --json
bin/acmectl commands show users create --json
bin/acmectl auth status -o json
bin/acmectl users create --set email=alice@example.com --dry-run
bin/acmectl users create --set email=alice@example.com -o jsonRules:
- Treat search results as candidates only.
- Use
commands show <path...> --jsonas the exact operation/workflow contract. - Check auth when
auth.required=true. - Read
mutationanddry_runfromcommands show. Whenmutationis notread, preview with--<dry_run.flag>ifdry_run.modeishttp_preview; if preview is unavailable, obtain explicit user confirmation before execution.unknownis not read. - Prefer
-o jsonfor machine-readable output. - Use
--file,--set, and--set-straccording to the body contract. - For a flag with
input_modes, prefer--<flag>-env,--<flag>-file, or--<flag>-stdinfor sensitive values. - Branch on structured
error.codeand process exit status, not error prose.
Framework commands such as auth, commands, completion, search,
skill, update, and __lathe are documented by --help; generated API
operations and workflows are documented by the runtime catalog.