From 0fd7271ae40904d06074ce64259494058bbe452e Mon Sep 17 00:00:00 2001 From: Pat Date: Sun, 24 May 2026 16:13:11 -0700 Subject: [PATCH] feat(docs): add internal/cmddocs schema emitter for docs.baseten.co Walks the declarative cmd.Root tree and emits a versioned docs.json describing every command, flag, example, output shape, and typed error. Consumed by the docs.baseten.co pipeline, which pins a baseten-cli ref, runs `go run ./internal/cmddocs`, and generates the CLI reference. The emitter lives in internal/cmddocs (package main) so it is not a user-facing baseten subcommand. Release flow and goreleaser are untouched: the docs pipeline is the only caller. --- internal/cmddocs/README.md | 50 +++++++ internal/cmddocs/main.go | 63 +++++++++ internal/cmddocs/schema.go | 76 +++++++++++ internal/cmddocs/schema_test.go | 54 ++++++++ internal/cmddocs/walk.go | 99 ++++++++++++++ internal/cmddocs/walk_test.go | 222 ++++++++++++++++++++++++++++++++ 6 files changed, 564 insertions(+) create mode 100644 internal/cmddocs/README.md create mode 100644 internal/cmddocs/main.go create mode 100644 internal/cmddocs/schema.go create mode 100644 internal/cmddocs/schema_test.go create mode 100644 internal/cmddocs/walk.go create mode 100644 internal/cmddocs/walk_test.go diff --git a/internal/cmddocs/README.md b/internal/cmddocs/README.md new file mode 100644 index 0000000..2fa6131 --- /dev/null +++ b/internal/cmddocs/README.md @@ -0,0 +1,50 @@ +# internal/cmddocs + +Walks the declarative `cmd.Root` tree and emits a versioned JSON description of +every command, flag, example, output shape, and typed error. Consumed by +`docs.baseten.co` to generate the published CLI reference. + +This is a developer tool, not a user-facing `baseten` subcommand. Its only +caller is the `docs.baseten.co` pipeline. + +## Output contract + +The JSON shape is defined by the Go types in `schema.go`. The top-level +`schema_version` field is "1"; bump `SchemaVersion` in `schema.go` whenever an +existing field is removed or its meaning changes (adding a new optional field +does **not** require a bump). + +`group_pri` is emitted verbatim: `0` means the flag set no `group-pri`, and the +consumer applies the framework default (`DefaultFlagGroupPri = 100`). The walker +does not resolve it. + +## Running locally + +```sh +# Write to stdout (default). +go run ./internal/cmddocs --cli-version=dev + +# Write to a file. +go run ./internal/cmddocs --cli-version=v0.1.0 --out=docs.json + +# Reproducible timestamp. +SOURCE_DATE_EPOCH=1700000000 go run ./internal/cmddocs --cli-version=v0.1.0 --out=docs.json +``` + +## Tests + +```sh +go test ./internal/cmddocs/... +``` + +Unit tests exercise the walker over synthetic command trees and assert the +emitted JSON is valid and stable. There is no committed snapshot to maintain: +schema drift surfaces in the `docs.baseten.co` pipeline, which regenerates the +reference against a pinned baseten-cli ref and opens a PR on any change. + +## How `docs.baseten.co` consumes this + +The docs pipeline pins a baseten-cli ref, checks it out, and runs +`go run ./internal/cmddocs --cli-version= --out=docs.json`, then feeds the +result to its MDX generator. Nothing in this repo's release flow runs the +emitter or publishes `docs.json`. diff --git a/internal/cmddocs/main.go b/internal/cmddocs/main.go new file mode 100644 index 0000000..59ea388 --- /dev/null +++ b/internal/cmddocs/main.go @@ -0,0 +1,63 @@ +// Command cmddocs walks the declarative cmd.Root tree and emits a versioned +// JSON description for consumption by docs.baseten.co. It is invoked by the +// docs.baseten.co pipeline against a pinned baseten-cli ref; nothing in this +// repo's release flow runs it. +// +// Usage: +// +// go run ./internal/cmddocs --cli-version=v0.1.0 --out=docs.json +// go run ./internal/cmddocs --cli-version=dev # writes to stdout +// +// Set SOURCE_DATE_EPOCH (Unix seconds) to pin GeneratedAt for reproducible +// output. +package main + +import ( + "encoding/json" + "flag" + "fmt" + "os" + "strconv" + "time" + + cmdpkg "github.com/basetenlabs/baseten-cli/cmd" +) + +func main() { + cliVersion := flag.String("cli-version", "dev", "CLI version string to embed in the output (e.g. v0.1.0).") + outPath := flag.String("out", "-", "Output file path; '-' writes to stdout.") + flag.Parse() + + generatedAt := time.Now().UTC().Format(time.RFC3339) + if epoch := os.Getenv("SOURCE_DATE_EPOCH"); epoch != "" { + secs, err := strconv.ParseInt(epoch, 10, 64) + if err != nil { + fmt.Fprintf(os.Stderr, "invalid SOURCE_DATE_EPOCH %q: %v\n", epoch, err) + os.Exit(2) + } + generatedAt = time.Unix(secs, 0).UTC().Format(time.RFC3339) + } + + schema := Walk(*cliVersion, generatedAt, cmdpkg.Root) + payload, err := json.MarshalIndent(schema, "", " ") + if err != nil { + fmt.Fprintf(os.Stderr, "marshal: %v\n", err) + os.Exit(1) + } + payload = append(payload, '\n') + + w := os.Stdout + if *outPath != "-" { + f, err := os.Create(*outPath) + if err != nil { + fmt.Fprintf(os.Stderr, "create %s: %v\n", *outPath, err) + os.Exit(1) + } + defer f.Close() + w = f + } + if _, err := w.Write(payload); err != nil { + fmt.Fprintf(os.Stderr, "write: %v\n", err) + os.Exit(1) + } +} diff --git a/internal/cmddocs/schema.go b/internal/cmddocs/schema.go new file mode 100644 index 0000000..454bf8e --- /dev/null +++ b/internal/cmddocs/schema.go @@ -0,0 +1,76 @@ +package main + +// This file defines the versioned JSON schema emitted for docs.baseten.co. The +// schema is the contract; bump SchemaVersion on any breaking change and +// coordinate with the consumer. + +// SchemaVersion is the major version of the emitted JSON. Increment when an +// existing field is removed or its semantics change. Adding a new optional +// field does not require a bump. +const SchemaVersion = "1" + +// Schema is the top-level document emitted by the walker. +type Schema struct { + SchemaVersion string `json:"schema_version"` + CLIVersion string `json:"cli_version"` + // GeneratedAt is the UTC RFC3339 timestamp this Schema was emitted at. + GeneratedAt string `json:"generated_at"` + StandardErrors []ErrorEntry `json:"standard_errors"` + Root Command `json:"root"` +} + +// Command is one node in the command tree. Non-leaf commands (with Children) +// have empty Flags/Examples/Output fields. +type Command struct { + Name string `json:"name"` + Path []string `json:"path"` + Summary string `json:"summary"` + Description string `json:"description"` + IsLeaf bool `json:"is_leaf"` + ArgsUsage string `json:"args_usage"` + ExactArgs int `json:"exact_args"` + MaxArgs int `json:"max_args"` + DisableFlagParsing bool `json:"disable_flag_parsing"` + Flags []Flag `json:"flags"` + Examples []Example `json:"examples"` + JQExample *Example `json:"jq_example"` + TextDescription string `json:"text_description"` + JSONDescription string `json:"json_description"` + JSONOutputType string `json:"json_output_type"` + JSONArrayStreamed bool `json:"json_array_streamed"` + Errors []ErrorEntry `json:"errors"` + Children []Command `json:"children"` +} + +// Flag is one CLI flag on a leaf command. +type Flag struct { + Name string `json:"name"` + Short string `json:"short"` + Description string `json:"description"` + Default string `json:"default"` + Enum []string `json:"enum"` + Required bool `json:"required"` + Oneof string `json:"oneof"` + Type string `json:"type"` + FieldName string `json:"field_name"` + Group string `json:"group"` + // GroupPri is the rendering priority for the flag's group (lower renders + // earlier), copied verbatim from the source field's group-pri tag. A value + // of 0 means the field set no group-pri; the consumer applies the framework + // default (DefaultFlagGroupPri = 100). The walker does not resolve it. + GroupPri int `json:"group_pri"` +} + +// Example is one documented invocation of a command. +type Example struct { + Description string `json:"description"` + Command string `json:"command"` +} + +// ErrorEntry is one typed error a command may surface. Standard errors at the +// top level are inherited by every leaf; per-command Errors lists additions. +type ErrorEntry struct { + Name string `json:"name"` + Code int `json:"code"` + Meaning string `json:"meaning"` +} diff --git a/internal/cmddocs/schema_test.go b/internal/cmddocs/schema_test.go new file mode 100644 index 0000000..07730c2 --- /dev/null +++ b/internal/cmddocs/schema_test.go @@ -0,0 +1,54 @@ +package main + +import ( + "encoding/json" + "testing" +) + +func Test_Schema_VersionIsStable(t *testing.T) { + if SchemaVersion != "1" { + t.Fatalf("SchemaVersion = %q, want %q (bump deliberately and update docs.baseten.co consumer)", SchemaVersion, "1") + } +} + +func Test_Schema_MarshalsEmpty(t *testing.T) { + s := Schema{ + SchemaVersion: SchemaVersion, + CLIVersion: "v0.0.0-test", + GeneratedAt: "2026-01-01T00:00:00Z", + } + b, err := json.MarshalIndent(s, "", " ") + if err != nil { + t.Fatalf("marshal: %v", err) + } + got := string(b) + want := `{ + "schema_version": "1", + "cli_version": "v0.0.0-test", + "generated_at": "2026-01-01T00:00:00Z", + "standard_errors": null, + "root": { + "name": "", + "path": null, + "summary": "", + "description": "", + "is_leaf": false, + "args_usage": "", + "exact_args": 0, + "max_args": 0, + "disable_flag_parsing": false, + "flags": null, + "examples": null, + "jq_example": null, + "text_description": "", + "json_description": "", + "json_output_type": "", + "json_array_streamed": false, + "errors": null, + "children": null + } +}` + if got != want { + t.Fatalf("JSON mismatch.\nGot:\n%s\nWant:\n%s", got, want) + } +} diff --git a/internal/cmddocs/walk.go b/internal/cmddocs/walk.go new file mode 100644 index 0000000..70d6a45 --- /dev/null +++ b/internal/cmddocs/walk.go @@ -0,0 +1,99 @@ +package main + +import ( + cmdpkg "github.com/basetenlabs/baseten-cli/cmd" +) + +// WalkCommand converts a declarative cmdpkg.Command into the JSON-emittable +// Command at the given parent path. The returned node's Path is parentPath +// with c.Name appended. +func WalkCommand(parentPath []string, c cmdpkg.Command) Command { + path := append(append([]string{}, parentPath...), c.Name) + out := Command{ + Name: c.Name, + Path: path, + Summary: c.Summary, + Description: c.Description, + IsLeaf: len(c.Children) == 0, + ArgsUsage: c.ArgsUsage, + ExactArgs: c.ExactArgs, + MaxArgs: c.MaxArgs, + DisableFlagParsing: c.DisableFlagParsing, + Flags: flagsFor(c), + } + applyOutput(&out, c.Output) + out.Errors = errorEntries(c.Errors) + for _, child := range c.Children { + out.Children = append(out.Children, WalkCommand(path, child)) + } + return out +} + +func applyOutput(dst *Command, spec cmdpkg.CommandOutputSpec) { + if spec == nil { + return + } + dst.TextDescription = spec.Text() + dst.JSONDescription = spec.JSON() + dst.JSONArrayStreamed = spec.JSONArrayStreamedBool() + if t := spec.JSONOutputType(); t != nil { + dst.JSONOutputType = t.String() + } + for _, ex := range spec.ExampleList() { + dst.Examples = append(dst.Examples, Example{Description: ex.Description, Command: ex.Command}) + } + jq := spec.JQ() + if jq.Command != "" || jq.Description != "" { + dst.JQExample = &Example{Description: jq.Description, Command: jq.Command} + } +} + +func flagsFor(c cmdpkg.Command) []Flag { + raw := c.LoadFlags() + if len(raw) == 0 { + return nil + } + out := make([]Flag, 0, len(raw)) + for _, f := range raw { + out = append(out, Flag{ + Name: f.Name, + Short: f.Short, + Description: f.Desc, + Default: f.Default, + Enum: f.Enum, + Required: f.Required, + Oneof: f.Oneof, + Type: f.Type.String(), + FieldName: f.FieldName, + Group: f.Group, + GroupPri: f.GroupPri, + }) + } + return out +} + +// Walk produces the full Schema for the given root command. cliVersion is +// embedded as-is (e.g. "v0.1.0", "dev"); generatedAt is the RFC3339 timestamp +// to embed. +func Walk(cliVersion, generatedAt string, root cmdpkg.Command) Schema { + return Schema{ + SchemaVersion: SchemaVersion, + CLIVersion: cliVersion, + GeneratedAt: generatedAt, + Root: WalkCommand(nil, root), + StandardErrors: errorEntries(cmdpkg.StandardErrors()), + } +} + +// errorEntries converts the framework's typed error descriptors into the +// JSON-emittable ErrorEntry shape. +func errorEntries(src []cmdpkg.ErrorDesc) []ErrorEntry { + if len(src) == 0 { + return nil + } + out := make([]ErrorEntry, 0, len(src)) + for _, e := range src { + out = append(out, ErrorEntry{Name: e.Name, Code: int(e.Code), Meaning: e.Meaning}) + } + return out +} diff --git a/internal/cmddocs/walk_test.go b/internal/cmddocs/walk_test.go new file mode 100644 index 0000000..2bfa66a --- /dev/null +++ b/internal/cmddocs/walk_test.go @@ -0,0 +1,222 @@ +package main + +import ( + "testing" + + cmdpkg "github.com/basetenlabs/baseten-cli/cmd" +) + +func Test_Walk_LeafBasicFields(t *testing.T) { + leaf := cmdpkg.Command{ + Name: "ping", + Summary: "Ping the server", + Description: "Sends a ping.", + Output: &cmdpkg.CommandOutput[cmdpkg.JSONUndefined]{}, + } + got := WalkCommand([]string{"baseten"}, leaf) + if got.Name != "ping" || got.Summary != "Ping the server" || got.Description != "Sends a ping." { + t.Fatalf("basic fields wrong: %+v", got) + } + wantPath := []string{"baseten", "ping"} + if len(got.Path) != len(wantPath) || got.Path[0] != "baseten" || got.Path[1] != "ping" { + t.Fatalf("path = %v, want %v", got.Path, wantPath) + } + if !got.IsLeaf { + t.Fatalf("IsLeaf = false, want true (no Children)") + } +} + +func Test_Walk_RecursesIntoChildren(t *testing.T) { + tree := cmdpkg.Command{ + Name: "auth", + Summary: "Authentication", + Children: []cmdpkg.Command{ + {Name: "login", Summary: "Log in", Output: &cmdpkg.CommandOutput[cmdpkg.JSONUndefined]{}}, + {Name: "logout", Summary: "Log out", Output: &cmdpkg.CommandOutput[cmdpkg.JSONUndefined]{}}, + }, + } + got := WalkCommand([]string{"baseten"}, tree) + if got.IsLeaf { + t.Fatalf("IsLeaf = true for parent, want false") + } + if len(got.Children) != 2 { + t.Fatalf("Children len = %d, want 2", len(got.Children)) + } + if got.Children[0].Name != "login" || got.Children[1].Name != "logout" { + t.Fatalf("child names = %q, %q; want login, logout", got.Children[0].Name, got.Children[1].Name) + } + wantLoginPath := []string{"baseten", "auth", "login"} + if !sliceEq(got.Children[0].Path, wantLoginPath) { + t.Fatalf("login path = %v, want %v", got.Children[0].Path, wantLoginPath) + } +} + +func sliceEq(a, b []string) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if a[i] != b[i] { + return false + } + } + return true +} + +type pingFlags struct { + cmdpkg.CommandFlags + Count int `flag:"count" short:"n" desc:"Number of pings" default:"3" group:"command"` + Tag string `flag:"tag" desc:"Optional tag" enum:"a,b,c"` + All bool `flag:"all" desc:"Ping all hosts" required:"true"` +} + +func Test_Walk_FlagsExtracted(t *testing.T) { + leaf := cmdpkg.Command{ + Name: "ping", + Flags: pingFlags{}, + Output: &cmdpkg.CommandOutput[cmdpkg.JSONUndefined]{}, + } + got := WalkCommand([]string{"baseten"}, leaf) + + byName := map[string]Flag{} + for _, f := range got.Flags { + byName[f.Name] = f + } + + count, ok := byName["count"] + if !ok { + t.Fatalf("missing flag 'count'; got %v", flagNames(got.Flags)) + } + if count.Short != "n" || count.Description != "Number of pings" || count.Default != "3" || count.Type != "int" { + t.Fatalf("count flag wrong: %+v", count) + } + + tag := byName["tag"] + if len(tag.Enum) != 3 || tag.Enum[0] != "a" || tag.Enum[2] != "c" { + t.Fatalf("tag enum wrong: %v", tag.Enum) + } + + all := byName["all"] + if !all.Required { + t.Fatalf("all should be required") + } + if all.Type != "bool" { + t.Fatalf("all type = %q, want %q", all.Type, "bool") + } + + // Common flags from embedded CommandFlags should be present. + if _, ok := byName["verbose"]; !ok { + t.Fatalf("missing embedded 'verbose' flag") + } +} + +func flagNames(fs []Flag) []string { + out := make([]string, len(fs)) + for i, f := range fs { + out[i] = f.Name + } + return out +} + +type pingResult struct { + Host string `json:"host"` + OK bool `json:"ok"` +} + +func Test_Walk_OutputAndExamples(t *testing.T) { + leaf := cmdpkg.Command{ + Name: "ping", + Output: &cmdpkg.CommandOutput[pingResult]{ + TextDescription: "Prints \"OK\" on success.", + JSONDescription: "Returns {host, ok}.", + Examples: []cmdpkg.CommandExample{ + {Description: "Ping example.com.", Command: "baseten ping example.com"}, + }, + JQExample: cmdpkg.CommandExample{Description: "Just the host.", Command: "baseten ping example.com --jq '.host'"}, + JSONArrayStreamed: false, + }, + } + got := WalkCommand([]string{"baseten"}, leaf) + if got.TextDescription != "Prints \"OK\" on success." { + t.Fatalf("TextDescription wrong: %q", got.TextDescription) + } + if got.JSONDescription != "Returns {host, ok}." { + t.Fatalf("JSONDescription wrong: %q", got.JSONDescription) + } + if got.JSONArrayStreamed { + t.Fatalf("JSONArrayStreamed = true, want false") + } + if len(got.Examples) != 1 || got.Examples[0].Command != "baseten ping example.com" { + t.Fatalf("Examples wrong: %+v", got.Examples) + } + if got.JQExample == nil { + t.Fatalf("JQExample = nil, want set") + } + if got.JQExample.Command != "baseten ping example.com --jq '.host'" { + t.Fatalf("JQExample.Command = %q", got.JQExample.Command) + } + if got.JSONOutputType != "main.pingResult" { + t.Fatalf("JSONOutputType = %q, want %q", got.JSONOutputType, "main.pingResult") + } +} + +func Test_Walk_OutputNilForParent(t *testing.T) { + parent := cmdpkg.Command{ + Name: "auth", + Children: []cmdpkg.Command{{Name: "x", Output: &cmdpkg.CommandOutput[cmdpkg.JSONUndefined]{}}}, + } + got := WalkCommand([]string{"baseten"}, parent) + if got.TextDescription != "" || got.JQExample != nil || got.JSONOutputType != "" { + t.Fatalf("parent should have empty output fields: %+v", got) + } +} + +func Test_Walk_PerCommandErrors(t *testing.T) { + leaf := cmdpkg.Command{ + Name: "fetch", + Output: &cmdpkg.CommandOutput[cmdpkg.JSONUndefined]{}, + Errors: []cmdpkg.ErrorDesc{ + {Name: "ErrRateLimited", Code: 7, Meaning: "Rate limit exceeded"}, + }, + } + got := WalkCommand([]string{"baseten"}, leaf) + if len(got.Errors) != 1 || got.Errors[0].Name != "ErrRateLimited" || got.Errors[0].Code != 7 || got.Errors[0].Meaning != "Rate limit exceeded" { + t.Fatalf("Errors wrong: %+v", got.Errors) + } +} + +func Test_Walk_TopLevelMetadata(t *testing.T) { + root := cmdpkg.Command{ + Name: "baseten", + Summary: "Baseten CLI", + Children: []cmdpkg.Command{ + {Name: "version", Summary: "Print version", Output: &cmdpkg.CommandOutput[cmdpkg.JSONUndefined]{}}, + }, + } + s := Walk("v9.9.9", "2026-01-01T00:00:00Z", root) + if s.SchemaVersion != SchemaVersion { + t.Fatalf("SchemaVersion = %q, want %q", s.SchemaVersion, SchemaVersion) + } + if s.CLIVersion != "v9.9.9" { + t.Fatalf("CLIVersion = %q", s.CLIVersion) + } + if s.GeneratedAt != "2026-01-01T00:00:00Z" { + t.Fatalf("GeneratedAt = %q", s.GeneratedAt) + } + if s.Root.Name != "baseten" || len(s.Root.Children) != 1 { + t.Fatalf("Root wrong: %+v", s.Root) + } + if len(s.StandardErrors) == 0 { + t.Fatalf("StandardErrors is empty; want framework's standard set") + } + // Spot-check one standard error code. + foundAuth := false + for _, e := range s.StandardErrors { + if e.Name == "ErrAuth" && e.Code == 3 { + foundAuth = true + } + } + if !foundAuth { + t.Fatalf("StandardErrors missing ErrAuth (code 3): %+v", s.StandardErrors) + } +}