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
2 changes: 1 addition & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ Overlays apply at codegen-time. The runtime has no overlay types. This matrix sh
| `Short` | `summary` / first comment | override | overlay > spec |
| `Long` | `description` / comment block | override | overlay > spec |
| `Aliases` | — | append | overlay-only |
| `Example` | — | set | overlay-only |
| `Example`, `Examples` | — | set | overlay-only |
| `Notes`, `Prerequisites`, `KnownErrors` | — | set | overlay-only |
| `Method`, `PathTpl`, `HasBody` | spec | locked | spec-only |
| `OperationID` | `operationId` | — | spec-only |
Expand Down
1 change: 1 addition & 0 deletions docs/cli-usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,7 @@ Rules:

- Treat `search` output as candidates only.
- Inspect exact command details with `commands show` before execution.
- Use `examples` from command detail when overlays provide runnable command metadata.
- Run `auth status --hostname <host>` before authenticated commands.
- Prefer `-o json` for agent-readable output.
- Use `--file`, `--set`, or `--set-str` according to the command detail body
Expand Down
11 changes: 11 additions & 0 deletions examples/overlay/example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,17 @@ commands: {}
# --email alice@example.com \
# --role viewer \
# --workspace default
# examples:
# - summary: "Create a user from a JSON body file"
# command: "myctl iam create-user --file user.json -o json"
# body_shape:
# input:
# email: "alice@example.com"
# role: "viewer"
# output_hints:
# id_path: "data.createUser.id"
# follow_up_commands:
# - "myctl iam get-user --id <id> -o json"
# group: "Identity"
# hidden: false
# params:
Expand Down
65 changes: 61 additions & 4 deletions internal/codegen/render/render.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package render

import (
"encoding/json"
"fmt"
"go/format"
"os"
Expand Down Expand Up @@ -91,6 +92,14 @@ func RewriteCommandExamples(cli, module string, specs []runtime.CommandSpec, fla
if next.Example != "" {
next.Example = commandExample(next.Example, cli, module, next, flat)
}
for i := range next.Examples {
if next.Examples[i].Command != "" {
next.Examples[i].Command = commandExample(next.Examples[i].Command, cli, module, next, flat)
}
for j := range next.Examples[i].FollowUpCommands {
next.Examples[i].FollowUpCommands[j] = commandExample(next.Examples[i].FollowUpCommands[j], cli, module, next, flat)
}
}
rewritten = append(rewritten, next)
}
return rewritten
Expand Down Expand Up @@ -192,6 +201,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.Examples = append([]runtime.CommandExample(nil), spec.Examples...)
for i := range cloned.Examples {
cloned.Examples[i].FollowUpCommands = append([]string(nil), spec.Examples[i].FollowUpCommands...)
}
cloned.Shortcuts = append([]runtime.CommandShortcut(nil), spec.Shortcuts...)
for i := range cloned.Shortcuts {
cloned.Shortcuts[i].Params = copyStringMap(spec.Shortcuts[i].Params)
Expand Down Expand Up @@ -246,6 +259,12 @@ func applyCommandOverride(spec *runtime.CommandSpec, override overlay.Override)
if override.Example != "" {
spec.Example = override.Example
}
if len(override.Examples) > 0 {
spec.Examples = make([]runtime.CommandExample, 0, len(override.Examples))
for _, example := range override.Examples {
spec.Examples = append(spec.Examples, runtimeCommandExample(example))
}
}
if len(override.Notes) > 0 {
spec.Notes = append([]string(nil), override.Notes...)
}
Expand Down Expand Up @@ -298,6 +317,26 @@ func applyCommandOverride(spec *runtime.CommandSpec, override overlay.Override)
}
}

func runtimeCommandExample(example overlay.Example) runtime.CommandExample {
var bodyShape json.RawMessage
if len(example.BodyShape) > 0 {
bodyShape, _ = json.Marshal(example.BodyShape)
}
out := runtime.CommandExample{
Summary: example.Summary,
Command: example.Command,
BodyShape: bodyShape,
FollowUpCommands: append([]string(nil), example.FollowUpCommands...),
}
if example.OutputHints.IDPath != "" || example.OutputHints.ListPath != "" {
out.OutputHints = &runtime.ExampleOutputHints{
IDPath: example.OutputHints.IDPath,
ListPath: example.OutputHints.ListPath,
}
}
return out
}

func ValidateShortcuts(moduleNames []string, specs []runtime.CommandSpec, flat bool) error {
rootNames := make([]string, 0, len(reservedRootCommands)+len(moduleNames)+len(specs))
for name := range reservedRootCommands {
Expand Down Expand Up @@ -491,10 +530,28 @@ var Specs = []runtime.CommandSpec{
{{- if $op.Long}}
Long: {{printf "%q" $op.Long}},
{{- end}}
{{- if $op.Example}}
Example: {{printf "%q" $op.Example}},
{{- end}}
{{- if $op.Notes}}
{{- if $op.Example}}
Example: {{printf "%q" $op.Example}},
{{- end}}
{{- if $op.Examples}}
Examples: []runtime.CommandExample{
{{- range $example := $op.Examples}}
{
{{- if $example.Summary}}Summary: {{printf "%q" $example.Summary}},{{end}}
{{- if $example.Command}}Command: {{printf "%q" $example.Command}},{{end}}
{{- if $example.BodyShape}}BodyShape: []byte({{printf "%q" $example.BodyShape}}),{{end}}
{{- if $example.OutputHints}}OutputHints: &runtime.ExampleOutputHints{
{{- if $example.OutputHints.IDPath}}IDPath: {{printf "%q" $example.OutputHints.IDPath}},{{end}}
{{- if $example.OutputHints.ListPath}}ListPath: {{printf "%q" $example.OutputHints.ListPath}},{{end}}
},{{end}}
{{- if $example.FollowUpCommands}}FollowUpCommands: []string{
{{- range $example.FollowUpCommands}}{{printf "%q" .}},{{end}}
},{{end}}
},
{{- end}}
},
{{- end}}
{{- if $op.Notes}}
Notes: []string{
{{- range $op.Notes}}{{printf "%q" .}},{{end}}
},
Expand Down
36 changes: 32 additions & 4 deletions internal/codegen/render/render_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,19 @@ func TestRenderModule_AppliesOverlay(t *testing.T) {
}
overrides := map[string]overlay.Override{
"install-addon": {
Aliases: []string{"addon-install"},
Short: "OVERLAY SHORT",
Long: "OVERLAY LONG DESC",
Example: "myctl demo install-addon --name foo",
Aliases: []string{"addon-install"},
Short: "OVERLAY SHORT",
Long: "OVERLAY LONG DESC",
Example: "myctl demo install-addon --name foo",
Examples: []overlay.Example{{
Summary: "Install from JSON",
Command: "myctl demo install-addon --file addon.json -o json",
BodyShape: map[string]any{
"input": map[string]any{"name": "foo"},
},
OutputHints: overlay.ExampleOutputHints{IDPath: "data.installAddon.id"},
FollowUpCommands: []string{"myctl demo get-addon --id <id> -o json"},
}},
Notes: []string{"Use the canonical addon ID."},
Prerequisites: []string{"List clusters before installing."},
KnownErrors: []overlay.KnownError{{Status: 400, Cause: "missing addon name"}},
Expand All @@ -73,6 +82,12 @@ func TestRenderModule_AppliesOverlay(t *testing.T) {
`"OVERLAY SHORT"`,
`"OVERLAY LONG DESC"`,
`"myctl demo install-addon --name foo"`,
`Examples: []runtime.CommandExample{`,
`Summary: "Install from JSON"`,
`Command: "myctl demo install-addon --file addon.json -o json"`,
`BodyShape: []byte("{\"input\":{\"name\":\"foo\"}}")`,
`OutputHints: &runtime.ExampleOutputHints{IDPath: "data.installAddon.id"}`,
`FollowUpCommands: []string{"myctl demo get-addon --id <id> -o json"}`,
`"addon-install"`,
`Notes:`,
`"Use the canonical addon ID."`,
Expand Down Expand Up @@ -524,15 +539,28 @@ func TestRewriteCommandExamples_NormalizesMultiWordGroupPaths(t *testing.T) {
Group: "Payment API",
Use: "list-payments",
Example: "acmectl billing payment api list-payments -o json",
Examples: []runtime.CommandExample{{
Command: "acmectl billing payment api list-payments --file payment.json -o json",
FollowUpCommands: []string{"acmectl billing payment api get-payment --id <id> -o json"},
}},
}}

got := RewriteCommandExamples("acmectl", "billing", specs, true)
if got[0].Example != "acmectl payment list-payments -o json" {
t.Fatalf("flat example = %q", got[0].Example)
}
if got[0].Examples[0].Command != "acmectl payment list-payments --file payment.json -o json" {
t.Fatalf("flat structured example = %q", got[0].Examples[0].Command)
}
if got[0].Examples[0].FollowUpCommands[0] != "acmectl billing payment api get-payment --id <id> -o json" {
t.Fatalf("flat follow-up = %q", got[0].Examples[0].FollowUpCommands[0])
}

got = RewriteCommandExamples("acmectl", "billing", specs, false)
if got[0].Example != "acmectl billing payment list-payments -o json" {
t.Fatalf("namespaced example = %q", got[0].Example)
}
if got[0].Examples[0].Command != "acmectl billing payment list-payments --file payment.json -o json" {
t.Fatalf("namespaced structured example = %q", got[0].Examples[0].Command)
}
}
50 changes: 47 additions & 3 deletions internal/codegen/render/skill.go
Original file line number Diff line number Diff line change
Expand Up @@ -548,6 +548,7 @@ func renderCatalogReference(manifest *config.Manifest) string {
b.WriteString("- `flags`: CLI flags, parameter location, type, required state, defaults, enum values, format, and help.\n")
b.WriteString("- `body`: request body requirement and media type.\n")
b.WriteString("- `auth`: whether auth is required and which scopes are declared.\n")
b.WriteString("- `examples`: runnable examples with optional body shape, output hints, and follow-up commands.\n")
b.WriteString("- `output`: list path, default columns, response media type, pagination, and streaming hints.\n")
b.WriteString("- `notes`, `prerequisites`, and `known_errors`: overlay-provided operation context that is not inferred from the API spec.\n\n")
b.WriteString("## Command Detail\n\n")
Expand Down Expand Up @@ -638,9 +639,7 @@ func renderModuleReference(manifest *config.Manifest, mod SkillModule, flat bool
fmt.Fprintf(&b, "- Output: %s\n", out)
}
writeOperationContext(&b, spec)
if spec.Example != "" {
writeExample(&b, commandExample(spec.Example, cli, module, spec, flat))
}
writeExamples(&b, cli, module, spec, flat)
b.WriteString("\n")
}
}
Expand Down Expand Up @@ -747,6 +746,51 @@ func writeExample(b *strings.Builder, example string) {
fmt.Fprintf(b, "- Example: `%s`\n", strings.ReplaceAll(oneLine(text), "`", "'"))
}

func writeExamples(b *strings.Builder, cli, module string, spec runtime.CommandSpec, flat bool) {
examples := spec.Examples
if len(examples) == 0 && spec.Example != "" {
examples = []runtime.CommandExample{{Command: spec.Example}}
}
if len(examples) == 0 {
return
}
if len(examples) == 1 {
example := examples[0]
if example.Summary == "" && example.Command != "" && len(example.BodyShape) == 0 && example.OutputHints == nil && len(example.FollowUpCommands) == 0 {
writeExample(b, commandExample(example.Command, cli, module, spec, flat))
return
}
}
b.WriteString("- Examples:\n")
for _, example := range examples {
summary := oneLine(example.Summary)
if summary == "" {
summary = "Example"
}
fmt.Fprintf(b, " - %s\n", summary)
if example.Command != "" {
fmt.Fprintf(b, " Command: `%s`\n", strings.ReplaceAll(oneLine(commandExample(example.Command, cli, module, spec, flat)), "`", "'"))
}
if len(example.BodyShape) > 0 {
fmt.Fprintf(b, " Body shape: `%s`\n", strings.ReplaceAll(oneLine(string(example.BodyShape)), "`", "'"))
}
if example.OutputHints != nil {
if example.OutputHints.IDPath != "" {
fmt.Fprintf(b, " Output ID path: `%s`\n", example.OutputHints.IDPath)
}
if example.OutputHints.ListPath != "" {
fmt.Fprintf(b, " Output list path: `%s`\n", example.OutputHints.ListPath)
}
}
if len(example.FollowUpCommands) > 0 {
b.WriteString(" Follow-up commands:\n")
for _, command := range example.FollowUpCommands {
fmt.Fprintf(b, " - `%s`\n", strings.ReplaceAll(oneLine(commandExample(command, cli, module, spec, flat)), "`", "'"))
}
}
}
}

func markdownFence(text string) string {
fence := "```"
for strings.Contains(text, fence) {
Expand Down
19 changes: 16 additions & 3 deletions internal/codegen/render/skill_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -175,13 +175,28 @@ func TestRenderModuleReference_FormatsExamples(t *testing.T) {
" --start $START --end $END -o json\n" +
"jq '.items[]'",
},
{
Group: "Users",
Use: "create-user",
Short: "Create user",
Method: "POST",
PathTpl: "/users",
Examples: []runtime.CommandExample{{
Summary: "Create from JSON",
Command: "acmectl users users create-user --file user.json -o json",
BodyShape: []byte(`{"input":{"name":"..."}}`),
OutputHints: &runtime.ExampleOutputHints{IDPath: "data.createUser.id"},
FollowUpCommands: []string{"acmectl users users get-user --id <id> -o json"},
}},
},
},
}

got := renderModuleReference(manifest, module, false)
for _, want := range []string{
"- Example: `acmectl users users get-user --id 123`",
"- Example:\n\n```\nEND=$(date +%s); START=$((END - 3600))\nacmectl users users query-logs \\\n --start $START --end $END -o json\njq '.items[]'\n```",
"- Examples:\n - Create from JSON\n Command: `acmectl users users create-user --file user.json -o json`\n Body shape: `{\"input\":{\"name\":\"...\"}}`\n Output ID path: `data.createUser.id`\n Follow-up commands:\n - `acmectl users users get-user --id <id> -o json`",
} {
if !strings.Contains(got, want) {
t.Fatalf("module reference missing %q\nfull output:\n%s", want, got)
Expand All @@ -192,14 +207,12 @@ func TestRenderModuleReference_FormatsExamples(t *testing.T) {
for _, want := range []string{
"- Example: `acmectl users get-user --id 123`",
"acmectl users query-logs \\\n --start $START --end $END -o json",
"Command: `acmectl users create-user --file user.json -o json`",
} {
if !strings.Contains(flat, want) {
t.Fatalf("flat module reference missing %q\nfull output:\n%s", want, flat)
}
}
if strings.Contains(flat, "acmectl users users") {
t.Fatalf("flat module reference kept stale namespaced command:\n%s", flat)
}
}

func TestRenderModuleReference_NormalizesMultiWordGroupPaths(t *testing.T) {
Expand Down
14 changes: 14 additions & 0 deletions internal/overlay/overlay.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ type Override struct {
Short string `yaml:"short"`
Long string `yaml:"long"`
Example string `yaml:"example"`
Examples []Example `yaml:"examples"`
Group string `yaml:"group"`
Hidden *bool `yaml:"hidden"`
Ignore bool `yaml:"ignore"`
Expand All @@ -25,6 +26,19 @@ type Override struct {
KnownErrors []KnownError `yaml:"known_errors"`
}

type Example struct {
Summary string `yaml:"summary"`
Command string `yaml:"command"`
BodyShape map[string]any `yaml:"body_shape"`
OutputHints ExampleOutputHints `yaml:"output_hints"`
FollowUpCommands []string `yaml:"follow_up_commands"`
}

type ExampleOutputHints struct {
IDPath string `yaml:"id_path"`
ListPath string `yaml:"list_path"`
}

type Shortcut struct {
Use string `yaml:"use"`
Params map[string]string `yaml:"params"`
Expand Down
23 changes: 23 additions & 0 deletions internal/overlay/overlay_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,17 @@ func TestLoadDir_ParsesMultipleModules(t *testing.T) {
short: "Create a user"
long: "Long description for create-user."
example: "myctl iam create-user --email a@b.c"
examples:
- summary: "Create a user from JSON"
command: "myctl iam create-user --file user.json -o json"
body_shape:
input:
email: "alice@example.com"
output_hints:
id_path: "data.createUser.id"
list_path: "data.users"
follow_up_commands:
- "myctl iam get-user --id <id> -o json"
`)
writeFile(t, filepath.Join(dir, "billing.yaml"), `commands:
list-invoices:
Expand All @@ -56,6 +67,18 @@ func TestLoadDir_ParsesMultipleModules(t *testing.T) {
if u.Short != "Create a user" || u.Long == "" || u.Example == "" {
t.Errorf("iam create-user override incomplete: %+v", u)
}
if len(u.Examples) != 1 || u.Examples[0].Summary != "Create a user from JSON" {
t.Fatalf("examples = %#v", u.Examples)
}
if u.Examples[0].OutputHints.IDPath != "data.createUser.id" || u.Examples[0].OutputHints.ListPath != "data.users" {
t.Errorf("example output hints = %#v", u.Examples[0].OutputHints)
}
if input, ok := u.Examples[0].BodyShape["input"].(map[string]any); !ok || input["email"] != "alice@example.com" {
t.Errorf("example body shape = %#v", u.Examples[0].BodyShape)
}
if len(u.Examples[0].FollowUpCommands) != 1 || u.Examples[0].FollowUpCommands[0] != "myctl iam get-user --id <id> -o json" {
t.Errorf("follow-up commands = %#v", u.Examples[0].FollowUpCommands)
}
if len(u.Aliases) != 2 || u.Aliases[0] != "adduser" || u.Aliases[1] != "new-user" {
t.Errorf("iam create-user aliases: %v", u.Aliases)
}
Expand Down
Loading