Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,11 @@ commands:
create-user:
short: "Create a user in the IAM service"
aliases: [adduser]
shortcuts:
- use: quick-user
params:
type: human
role: viewer
example: |
acmectl iam create-user \
--set email=alice@example.com \
Expand All @@ -349,6 +354,9 @@ Bulk pagination defaults fill matching command params that have no spec default.
Explicit `commands.<use>.params.<name>.default` still wins when a command needs a
different value. Parameter `required: true` marks an existing generated flag as
required when an upstream spec is incomplete; it does not create new flags.
Command `shortcuts` add root-level commands that execute the same generated
operation with preset values for existing parameters. Shortcut params may use
the parameter name or flag name; invocation flags can still override the preset.

Run codegen with an overlay directory:

Expand Down
5 changes: 4 additions & 1 deletion examples/petstore/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ Demonstrates the minimal Lathe workflow: OpenAPI 3 spec -> codegen -> working CL
## Path

1. Inspect `cli.yaml`, `specs/sources.yaml`, and the checked-in fixture cache.
2. Run `lathe codegen -cache fixtures`.
2. Run `lathe codegen -cache fixtures -overlay overlays`.
3. Use `cmd/petstore/main.go` to mount `internal/generated`.
4. Run `go mod tidy` and `go build -o bin/petstore ./cmd/petstore`.
5. Verify the generated agent loop with `search`, `commands show`, and `commands schema`.
6. Try the generated-command shortcut: `petstore pet-123` executes `petstore pets get --id 123`.

## Expected output

Expand All @@ -22,6 +23,7 @@ Authentication:
auth Authenticate petstore with a host

Modules:
pet-123 Get a pet by ID
pets Pets operations

Additional Commands:
Expand All @@ -36,4 +38,5 @@ See [CLI Usage](../../docs/cli-usage.md) for the full command sequence. The key

- **`cli.yaml`** — CLI name, description, auth endpoint
- **`specs/sources.yaml`** — upstream specs pinned at immutable tags
- **`overlays/pets.yaml`** — generated-command shortcuts and polish
- **`cmd/<name>/main.go`** — embed `cli.yaml`, call `lathe.NewApp`, then handle `generated.MountModules` errors
6 changes: 6 additions & 0 deletions examples/petstore/overlays/pets.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
commands:
get:
shortcuts:
- use: pet-123
params:
id: "123"
6 changes: 6 additions & 0 deletions internal/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ func NewCommand(m *config.Manifest) *cobra.Command {
return cmd
}

func NewHiddenLoginCommand(m *config.Manifest) *cobra.Command {
cmd := newLogin(m)
cmd.Hidden = true
return cmd
}

func rootString(cmd *cobra.Command, name string) string {
v, _ := cmd.Root().PersistentFlags().GetString(name)
return v
Expand Down
69 changes: 68 additions & 1 deletion internal/codegen/render/render.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,10 @@ func MergeOverlayModule(specs []runtime.CommandSpec, mod overlay.Module) []runti
func cloneCommandSpec(spec runtime.CommandSpec) runtime.CommandSpec {
cloned := spec
cloned.Aliases = append([]string(nil), spec.Aliases...)
cloned.Shortcuts = append([]runtime.CommandShortcut(nil), spec.Shortcuts...)
for i := range cloned.Shortcuts {
cloned.Shortcuts[i].Params = copyStringMap(spec.Shortcuts[i].Params)
}
cloned.Notes = append([]string(nil), spec.Notes...)
cloned.Prerequisites = append([]string(nil), spec.Prerequisites...)
cloned.KnownErrors = append([]runtime.KnownError(nil), spec.KnownErrors...)
Expand Down Expand Up @@ -216,6 +220,12 @@ func applyCommandOverride(spec *runtime.CommandSpec, override overlay.Override)
if len(override.Aliases) > 0 {
spec.Aliases = append(spec.Aliases, override.Aliases...)
}
for _, shortcut := range override.Shortcuts {
spec.Shortcuts = append(spec.Shortcuts, runtime.CommandShortcut{
Use: shortcut.Use,
Params: copyStringMap(shortcut.Params),
})
}
if override.Group != "" {
spec.Group = override.Group
}
Expand Down Expand Up @@ -247,6 +257,37 @@ func applyCommandOverride(spec *runtime.CommandSpec, override overlay.Override)
}
}

func ValidateShortcuts(moduleNames []string, specs []runtime.CommandSpec, flat bool) error {
rootNames := make([]string, 0, len(reservedRootCommands)+len(moduleNames)+len(specs))
for name := range reservedRootCommands {
rootNames = append(rootNames, name)
}
if flat {
seen := map[string]bool{}
for _, spec := range specs {
name := rootCommandName(spec.Group)
if !seen[name] {
rootNames = append(rootNames, name)
seen[name] = true
}
}
return runtime.ValidateShortcuts(specs, rootNames)
}
rootNames = append(rootNames, moduleNames...)
return runtime.ValidateShortcuts(specs, rootNames)
}

func copyStringMap(in map[string]string) map[string]string {
if len(in) == 0 {
return nil
}
out := make(map[string]string, len(in))
for key, value := range in {
out[key] = value
}
return out
}

func renderModuleSpecs(name, cliName string, specs []runtime.CommandSpec) error {
var buf strings.Builder
ctx := moduleCtx{
Expand Down Expand Up @@ -308,6 +349,24 @@ func schemaLiteral(s *runtime.SchemaSpec) string {
return b.String()
}

func stringMapLiteral(values map[string]string) string {
if len(values) == 0 {
return "nil"
}
keys := make([]string, 0, len(values))
for key := range values {
keys = append(keys, key)
}
sort.Strings(keys)
var b strings.Builder
b.WriteString("map[string]string{")
for _, key := range keys {
fmt.Fprintf(&b, "%q: %q,", key, values[key])
}
b.WriteByte('}')
return b.String()
}

func writeSchemaLiteral(b *strings.Builder, s *runtime.SchemaSpec) {
if s == nil {
b.WriteString("nil")
Expand Down Expand Up @@ -343,7 +402,8 @@ func writeSchemaLiteral(b *strings.Builder, s *runtime.SchemaSpec) {
}

var moduleTmpl = template.Must(template.New("gen").Funcs(template.FuncMap{
"schemaLiteral": schemaLiteral,
"schemaLiteral": schemaLiteral,
"stringMapLiteral": stringMapLiteral,
}).Parse(`// Code generated by lathe codegen. DO NOT EDIT.

package {{.Module}}
Expand Down Expand Up @@ -379,6 +439,13 @@ var Specs = []runtime.CommandSpec{
{{- if $op.Aliases}}
Aliases: []string{ {{- range $op.Aliases}}{{printf "%q" .}}, {{end -}} },
{{- end}}
{{- if $op.Shortcuts}}
Shortcuts: []runtime.CommandShortcut{
{{- range $shortcut := $op.Shortcuts}}
{Use: {{printf "%q" $shortcut.Use}}{{if $shortcut.Params}}, Params: {{stringMapLiteral $shortcut.Params}}{{end}}},
{{- end}}
},
{{- end}}
Short: {{printf "%q" $op.Short}},
{{- if $op.Long}}
Long: {{printf "%q" $op.Long}},
Expand Down
42 changes: 42 additions & 0 deletions internal/codegen/render/skill.go
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,7 @@ func renderCatalogReference(manifest *config.Manifest) string {
fmt.Fprintf(&b, "Run `%s commands --json` to inspect the generated command catalog. Use `--include-hidden` only when hidden commands are relevant.\n\n", cli)
b.WriteString("Key fields:\n\n")
b.WriteString("- `path`: command path to pass to `commands show` or execute after the CLI name.\n")
b.WriteString("- `shortcuts`: root-level commands that execute the same operation with preset flag values.\n")
b.WriteString("- `http`: HTTP method and path template.\n")
fmt.Fprintf(&b, "- `http.default_hostname`: optional source-level host selected after explicit `--hostname` and `$%s`; when present it is used before the single-host fallback from `hosts.yml`.\n", manifest.CLI.HostEnv)
b.WriteString("- `flags`: CLI flags, parameter location, type, required state, defaults, enum values, format, and help.\n")
Expand Down Expand Up @@ -362,6 +363,7 @@ func renderModuleReference(manifest *config.Manifest, mod SkillModule, flat bool
fmt.Fprintf(&b, "- HTTP: `%s %s`\n", spec.Method, spec.PathTpl)
fmt.Fprintf(&b, "- Auth: %s\n", authSummary(spec.Security))
fmt.Fprintf(&b, "- Body: %s\n", bodySummary(spec.RequestBody))
writeShortcuts(&b, cli, spec)
if len(spec.Params) == 0 {
b.WriteString("- Flags: none\n")
} else {
Expand Down Expand Up @@ -404,6 +406,46 @@ func renderModuleReference(manifest *config.Manifest, mod SkillModule, flat bool
return b.String()
}

func writeShortcuts(b *strings.Builder, cli string, spec runtime.CommandSpec) {
if len(spec.Shortcuts) == 0 {
return
}
b.WriteString("- Shortcuts:\n")
for _, shortcut := range spec.Shortcuts {
preset := shortcutPreset(spec, shortcut)
if preset == "" {
fmt.Fprintf(b, " - `%s %s`\n", cli, shortcut.Use)
continue
}
fmt.Fprintf(b, " - `%s %s` preset %s\n", cli, shortcut.Use, preset)
}
}

func shortcutPreset(spec runtime.CommandSpec, shortcut runtime.CommandShortcut) string {
if len(shortcut.Params) == 0 {
return ""
}
flags := make(map[string]string, len(spec.Params)*2)
for _, param := range spec.Params {
flags[param.Name] = param.Flag
flags[param.Flag] = param.Flag
}
keys := make([]string, 0, len(shortcut.Params))
for key := range shortcut.Params {
keys = append(keys, key)
}
sort.Strings(keys)
parts := make([]string, 0, len(keys))
for _, key := range keys {
flag := flags[key]
if flag == "" {
flag = key
}
parts = append(parts, fmt.Sprintf("`--%s=%s`", flag, shortcut.Params[key]))
}
return strings.Join(parts, ", ")
}

func commandExample(example, cli, module string, spec runtime.CommandSpec, flat bool) string {
newPath := strings.Join(commandPath(cli, module, spec, true), " ")
oldPaths := []string{strings.Join(legacyCommandPath(cli, module, spec, flat), " ")}
Expand Down
25 changes: 20 additions & 5 deletions internal/lathecmd/lathecmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -164,9 +164,18 @@ func runCodegen(sourcesPath string, manifestPath string, cacheRoot string, overl
}

ordered := cfg.Ordered()
moduleNames := make([]string, 0, len(ordered))
for _, src := range ordered {
name := src.Name
if src.DisplayName != "" {
name = src.DisplayName
}
moduleNames = append(moduleNames, name)
}
var mounts []render.ModuleMount
var skillModules []render.SkillModule
for _, src := range ordered {
var shortcutRootNames []string
for i, src := range ordered {
syncDir := filepath.Join(syncRoot, src.Name)
if err := specsync.VerifyState(syncDir, src); err != nil {
return err
Expand All @@ -188,14 +197,20 @@ func runCodegen(sourcesPath string, manifestPath string, cacheRoot string, overl
specs[i].DefaultHostname = *src.DefaultHostname
}
}
cliName := src.Name
if src.DisplayName != "" {
cliName = src.DisplayName
}
cliName := moduleNames[i]
flat, err := render.ResolveFlatCommandPath(manifest.CLI.CommandPath, len(ordered), specs)
if err != nil {
return err
}
validateRootNames := append(append([]string(nil), moduleNames...), shortcutRootNames...)
if err := render.ValidateShortcuts(validateRootNames, specs, flat); err != nil {
return err
}
for _, spec := range specs {
for _, shortcut := range spec.Shortcuts {
shortcutRootNames = append(shortcutRootNames, shortcut.Use)
}
}
specs = render.RewriteCommandExamples(manifest.CLI.Name, cliName, specs, flat)
if err := render.RenderModule(src.Name, cliName, specs, nil); err != nil {
return err
Expand Down
6 changes: 6 additions & 0 deletions internal/overlay/overlay.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (

type Override struct {
Aliases []string `yaml:"aliases"`
Shortcuts []Shortcut `yaml:"shortcuts"`
Short string `yaml:"short"`
Long string `yaml:"long"`
Example string `yaml:"example"`
Expand All @@ -23,6 +24,11 @@ type Override struct {
KnownErrors []KnownError `yaml:"known_errors"`
}

type Shortcut struct {
Use string `yaml:"use"`
Params map[string]string `yaml:"params"`
}

type ParamOverride struct {
Flag string `yaml:"flag"`
Help string `yaml:"help"`
Expand Down
1 change: 1 addition & 0 deletions pkg/lathe/lathe.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ func NewApp(m *config.Manifest) *cobra.Command {
authCmd := auth.NewCommand(m)
authCmd.GroupID = authGroupID
cmd.AddCommand(authCmd)
cmd.AddCommand(auth.NewHiddenLoginCommand(m))
cmd.AddCommand(commandsCmd(m))
cmd.AddCommand(searchCmd(m))
if m.Update.GitHub != nil {
Expand Down
Loading