From 5deb0f855d20fdadc9e52c6b159fe7bc37aa2400 Mon Sep 17 00:00:00 2001 From: samzong Date: Sun, 16 Aug 2026 04:47:57 -0400 Subject: [PATCH] build: prepare for lathe v0.6.0 Signed-off-by: samzong --- Makefile | 3 +-- go.mod | 11 +++++--- go.sum | 35 ++++++++++++++++++++----- internal/generated/app/app_gen.go | 11 ++++---- internal/generated/modules_gen.go | 7 ++++- skills/appctl/SKILL.md | 4 +++ skills/appctl/references/catalog.md | 12 ++++++--- skills/appctl/references/modules/app.md | 2 +- tests/test_cli.py | 9 +++---- 9 files changed, 65 insertions(+), 29 deletions(-) diff --git a/Makefile b/Makefile index bfb42f0..fb1c795 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ cli-sync: cp cli.yaml cmd/appctl/cli.yaml - go run github.com/lathe-cli/lathe/cmd/lathe@v0.4.4 bootstrap + go run github.com/lathe-cli/lathe/cmd/lathe@v0.5.3-0.20260816043253-e33580932a42 bootstrap go mod tidy cli-build: @@ -14,4 +14,3 @@ test: cli-build check: cli-sync test uv run --locked python -m compileall -q src tests go vet ./... - diff --git a/go.mod b/go.mod index b4f9342..cf14562 100644 --- a/go.mod +++ b/go.mod @@ -1,16 +1,19 @@ module example.com/cli-first-python-app -go 1.25.7 +go 1.25.13 require ( - github.com/lathe-cli/lathe v0.4.4 + github.com/lathe-cli/lathe v0.5.3-0.20260816043253-e33580932a42 github.com/spf13/cobra v1.10.2 ) require ( + github.com/gofrs/flock v0.13.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/santhosh-tekuri/jsonschema/v6 v6.0.3 // indirect github.com/spf13/pflag v1.0.9 // indirect - golang.org/x/sys v0.46.0 // indirect - golang.org/x/term v0.44.0 // indirect + golang.org/x/sys v0.47.0 // indirect + golang.org/x/term v0.45.0 // indirect + golang.org/x/text v0.32.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 3b0743f..869ef0b 100644 --- a/go.sum +++ b/go.sum @@ -1,19 +1,40 @@ github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= +github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw= +github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/lathe-cli/lathe v0.4.4 h1:5TNnLpLccH7YAxt3n1Lv5GOrf4i+U264BGvb3v2Haws= -github.com/lathe-cli/lathe v0.4.4/go.mod h1:R7LnEOMjrGHDutF3l2VyY5yc7AAoKLsFSj4JeSuLJRc= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lathe-cli/lathe v0.5.3-0.20260816043253-e33580932a42 h1:eknDAb1CeWtRsntyTzmf9Dnbr3zjo1tNvGnoyVUkAfQ= +github.com/lathe-cli/lathe v0.5.3-0.20260816043253-e33580932a42/go.mod h1:/b5JLEkz9hQqKDXEnNsY/U4IGDJqxRlVD7Tw7iIyOSA= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.16.0 h1:O9DK+vNMDVGLr2BeZqmpLeMjiMNkuXfcqntWbZV6S5g= +github.com/rogpeppe/go-internal v1.16.0/go.mod h1:DrUVZyrJU+txYW5/1kwtXQSMFio52ZOxX7yM1VHvnxs= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.3 h1:1EYB5IzjZawrrnELUi78f9fPu57HuXjmddZPjrls/28= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.3/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= -golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc= -golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0= +golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/generated/app/app_gen.go b/internal/generated/app/app_gen.go index a64d62d..6cb4cce 100644 --- a/internal/generated/app/app_gen.go +++ b/internal/generated/app/app_gen.go @@ -8,14 +8,13 @@ import ( "github.com/lathe-cli/lathe/pkg/runtime" ) -const generatedSchemaVersion = 8 +const generatedSchemaVersion = 11 func Mount(root *cobra.Command) error { if err := runtime.AssertSchema(generatedSchemaVersion); err != nil { return err } - runtime.Build(root, "app", Specs) - return nil + return runtime.Build(root, "app", Specs) } func MountFlat(root *cobra.Command) error { @@ -50,6 +49,7 @@ var Specs = []runtime.CommandSpec{ MediaType: "application/json", Schema: &runtime.SchemaSpec{Type: "object", Properties: map[string]*runtime.SchemaSpec{"title": &runtime.SchemaSpec{Type: "string"}}, Required: []string{"title"}}, }, + Output: runtime.OutputHints{ResponseMediaType: "application/json"}, Security: &runtime.SecurityHint{Public: true}, }, { @@ -87,9 +87,8 @@ var Specs = []runtime.CommandSpec{ Method: "GET", PathTpl: "/tasks", DefaultHostname: "http://127.0.0.1:3000", - Output: runtime.OutputHints{DefaultColumns: []string{"id", "completed", "title"}, ResponseMediaType: "application/json", - }, - Security: &runtime.SecurityHint{Public: true}, + Output: runtime.OutputHints{DefaultColumns: []string{"id", "completed", "title"}, ResponseMediaType: "application/json"}, + Security: &runtime.SecurityHint{Public: true}, }, { Group: "Tasks", diff --git a/internal/generated/modules_gen.go b/internal/generated/modules_gen.go index 5a7816d..664e93e 100644 --- a/internal/generated/modules_gen.go +++ b/internal/generated/modules_gen.go @@ -7,7 +7,12 @@ import ( "github.com/spf13/cobra" ) -// MountModules mounts every module declared in sources.yaml under root. +// Mount mounts every generated command and capability declared by codegen. +func Mount(root *cobra.Command) error { + return MountModules(root) +} + +// MountModules mounts every module and generated capability under root. // The import list above is the single source of truth for which modules // are compiled into this binary. main.go wires this call after // app.NewApp() so the framework package never imports downstream code. diff --git a/skills/appctl/SKILL.md b/skills/appctl/SKILL.md index aa5ba7d..efd21f7 100644 --- a/skills/appctl/SKILL.md +++ b/skills/appctl/SKILL.md @@ -38,5 +38,9 @@ Use this skill when a user asks you to operate `appctl`, inspect its API command - Do not guess flags or request body shape from command names. - Do not execute directly from search results; confirm with `commands show` first. - Prefer `-o json` for machine-readable command output unless the user asks for human-readable output. +- With `-o json` or `-o yaml`, branch on `error.code` and process exit status; `error.message` and `error.hint` are safe human guidance, and `error.http.status` is the only optional HTTP context. +- A configured stream pause is successful (`exit 0`); inspect the collected output field mapped from the pause event instead of treating it as an error. +- For collected streams, choose one mode: `-o json` for one stable document, `--stream` in the default output mode when catalog `output.streaming.policy.live` is present, or `-o raw` for wire events. - Use `--file`, `--set`, or `--set-str` for JSON request bodies according to `commands show` body requirements. +- When `body.runtime_schema` is present, normal execution fetches and validates against that schema before the target request; `--dry-run` stays network-free and skips this preflight. - For sensitive flags, prefer safe modes from `flags[].input_modes`: `---env`, `---file`, or `---stdin`. diff --git a/skills/appctl/references/catalog.md b/skills/appctl/references/catalog.md index 8269664..0ea8f77 100644 --- a/skills/appctl/references/catalog.md +++ b/skills/appctl/references/catalog.md @@ -17,10 +17,12 @@ Key fields: - `http`: HTTP method and path template. - `http.default_hostname`: optional source-level host selected after explicit `--hostname` and `$APPCTL_HOST`; when present it is used before the single-host fallback from `hosts.yml`. - `flags`: CLI flags, parameter location, type, required state, defaults, enum values, format, input modes, and help. -- `body`: request body requirement and media type. +- `body`: request body requirement, media type, and optional `runtime_schema` preflight source, including its active-context prerequisites. - `auth`: whether auth is required and which scopes are declared. - `examples`: runnable examples with optional body shape, output hints, and follow-up commands. -- `output`: list path, default columns, response media type, pagination, and streaming hints. +- `output`: list path, default columns, response media type, pagination, and streaming hints; a streaming policy describes collection, terminal outcomes, and optional live projection. +- `flags[].context`: an optional account-scoped default with explicit flag, declared environment, then stored-value precedence. +- `sets_context`: a successful operation persists its declared parameter as the selected host's active context. - `notes`, `prerequisites`, and `known_errors`: overlay-provided operation context that is not inferred from the API spec. ## Command Detail @@ -48,9 +50,13 @@ When a flag entry has `input_modes`, prefer safe modes over putting secrets dire - `--set key.path=value`: build JSON with type inference for booleans, null, integers, and floats. - `--set-str key.path=value`: build JSON while forcing the value to remain a string. +If `body.runtime_schema` is present, normal execution fetches that JSON Schema and validates the body before the target request. `--dry-run` does not fetch it. + ## Output -Use `-o json` for machine-readable command output. Other supported formats are `table`, `yaml`, and `raw`. +Use `-o json` for machine-readable command output. Other supported formats are `table`, `yaml`, and `raw`. For collected streams, choose one mode: JSON or YAML for one document, `--stream` in the default output mode when `output.streaming.policy.live` is present, or raw for wire events. + +On a non-zero exit with JSON or YAML output, read `error.code`, `error.message`, and `error.hint`; optional `error.http` contains only `status`. A configured pause exits zero and is represented by the field mapping in its stream collection policy. ## Auth diff --git a/skills/appctl/references/modules/app.md b/skills/appctl/references/modules/app.md index 2cf52e5..544bfaa 100644 --- a/skills/appctl/references/modules/app.md +++ b/skills/appctl/references/modules/app.md @@ -28,6 +28,7 @@ - Auth: public - Body: required; media type `application/json` - Flags: none +- Output: response media `application/json` ### `appctl tasks delete` @@ -66,4 +67,3 @@ - Flags: - `--id` (path, required): id - Output: response media `application/json` - diff --git a/tests/test_cli.py b/tests/test_cli.py index a8a4df8..b55b4d6 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -40,7 +40,7 @@ def cli_failure(self, *args): text=True, ) self.assertNotEqual(result.returncode, 0) - return json.loads(result.stderr)["error"]["message"] + return json.loads(result.stderr)["error"] def test_generated_cli_is_the_application_acceptance_surface(self): self.assertEqual(self.cli("health", "get"), {"status": "ok"}) @@ -54,11 +54,11 @@ def test_generated_cli_is_the_application_acceptance_surface(self): self.assertEqual(self.cli("tasks", "list"), []) def test_generated_cli_surfaces_api_errors(self): - self.assertIn("HTTP 400", self.cli_failure("tasks", "create", "--set-str", "title=")) - self.assertIn("HTTP 404", self.cli_failure("tasks", "get", "--id", "missing")) + self.assertEqual(self.cli_failure("tasks", "create", "--set-str", "title=")["http"]["status"], 400) + self.assertEqual(self.cli_failure("tasks", "get", "--id", "missing")["http"]["status"], 404) task = self.cli("tasks", "create", "--set", "title=Keep the contract honest") error = self.cli_failure("tasks", "update", "--id", task["id"], "--file", "test/empty.json") - self.assertIn("title or completed is required", error) + self.assertEqual(error["http"]["status"], 400) def test_http_boundary_rejects_unsupported_input(self): status, headers, _ = self.request("PUT", "/tasks") @@ -88,4 +88,3 @@ def request(self, method, path, content_type=None, body=None): if __name__ == "__main__": unittest.main() -