diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 01e0ec0..310fdcc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,7 +34,7 @@ jobs: - name: Link staging branch if: github.repository == 'stainless-sdks/agentmail-cli' run: | - ./scripts/link 'github.com/stainless-sdks/agentmail-go@${{ github.ref_name }}' || go mod edit -dropreplace='github.com/stainless-sdks/agentmail-go' + ./scripts/link 'github.com/stainless-sdks/agentmail-go@${{ github.ref_name }}' || true - name: Bootstrap run: ./scripts/bootstrap @@ -60,7 +60,7 @@ jobs: - name: Link staging branch if: github.repository == 'stainless-sdks/agentmail-cli' run: | - ./scripts/link 'github.com/stainless-sdks/agentmail-go@${{ github.ref_name }}' || go mod edit -dropreplace='github.com/stainless-sdks/agentmail-go' + ./scripts/link 'github.com/stainless-sdks/agentmail-go@${{ github.ref_name }}' || true - name: Bootstrap run: ./scripts/bootstrap @@ -107,7 +107,7 @@ jobs: - name: Link staging branch if: github.repository == 'stainless-sdks/agentmail-cli' run: | - ./scripts/link 'github.com/stainless-sdks/agentmail-go@${{ github.ref_name }}' || go mod edit -dropreplace='github.com/stainless-sdks/agentmail-go' + ./scripts/link 'github.com/stainless-sdks/agentmail-go@${{ github.ref_name }}' || true - name: Bootstrap run: ./scripts/bootstrap diff --git a/.release-please-manifest.json b/.release-please-manifest.json index ef29836..6290e0c 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.7.8" + ".": "0.7.9" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 8121fb6..afd2312 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,21 @@ # Changelog +## 0.7.9 (2026-04-10) + +Full Changelog: [v0.7.8...v0.7.9](https://github.com/agentmail-to/agentmail-cli/compare/v0.7.8...v0.7.9) + +### Bug Fixes + +* **cli:** fix incompatible Go types for flag generated as array of maps ([18a9ddb](https://github.com/agentmail-to/agentmail-cli/commit/18a9ddb0f6a51f90aea0d205e26bd55a479d79ec)) +* decouple npm publish from goreleaser so homebrew failures don't block npm ([3373dbc](https://github.com/agentmail-to/agentmail-cli/commit/3373dbc5b00b491ae265908e2e9d3b9fbe17d0a8)) +* fix for failing to drop invalid module replace in link script ([d1c7780](https://github.com/agentmail-to/agentmail-cli/commit/d1c7780f5f80b69200626a96cb832bcd3a2c455a)) + + +### Chores + +* **cli:** additional test cases for `ShowJSONIterator` ([e96692a](https://github.com/agentmail-to/agentmail-cli/commit/e96692a5a3d9051faea76bfaf25889243f70ab0d)) +* **cli:** let `--format raw` be used in conjunction with `--transform` ([cfdc42c](https://github.com/agentmail-to/agentmail-cli/commit/cfdc42cafa845ee617f35166aeeae6ad6a381c2a)) + ## 0.7.8 (2026-04-08) Full Changelog: [v0.7.7...v0.7.8](https://github.com/agentmail-to/agentmail-cli/compare/v0.7.7...v0.7.8) diff --git a/pkg/cmd/apikey.go b/pkg/cmd/apikey.go index f7f8b3a..c93f3c8 100644 --- a/pkg/cmd/apikey.go +++ b/pkg/cmd/apikey.go @@ -25,7 +25,7 @@ var apiKeysCreate = requestflag.WithInnerFlags(cli.Command{ Usage: "Name of api key.", BodyPath: "name", }, - &requestflag.Flag[any]{ + &requestflag.Flag[map[string]any]{ Name: "permissions", Usage: "Granular permissions for the API key. When ommitted all permissions are granted. Otherwise, only permissions set to true are granted.", BodyPath: "permissions", diff --git a/pkg/cmd/cmdutil.go b/pkg/cmd/cmdutil.go index d9a875c..75c8b90 100644 --- a/pkg/cmd/cmdutil.go +++ b/pkg/cmd/cmdutil.go @@ -324,7 +324,7 @@ func shouldUseColors(w io.Writer) bool { } func formatJSON(expectedOutput *os.File, title string, res gjson.Result, format string, transform string) ([]byte, error) { - if format != "raw" && transform != "" { + if transform != "" { transformed := res.Get(transform) if transformed.Exists() { res = transformed @@ -368,7 +368,7 @@ func formatJSON(expectedOutput *os.File, title string, res gjson.Result, format // Display JSON to the user in various different formats func ShowJSON(out *os.File, title string, res gjson.Result, format string, transform string) error { - if format != "raw" && transform != "" { + if transform != "" { transformed := res.Get(transform) if transformed.Exists() { res = transformed diff --git a/pkg/cmd/cmdutil_test.go b/pkg/cmd/cmdutil_test.go index 8eca397..66e3b07 100644 --- a/pkg/cmd/cmdutil_test.go +++ b/pkg/cmd/cmdutil_test.go @@ -10,6 +10,9 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" + + "github.com/agentmail-to/agentmail-cli/internal/jsonview" ) func TestStreamOutput(t *testing.T) { @@ -148,3 +151,120 @@ func TestValidateBaseURL(t *testing.T) { assert.Contains(t, err.Error(), "--base-url") }) } + +func TestFormatJSON(t *testing.T) { + t.Parallel() + + t.Run("RawWithTransform", func(t *testing.T) { + t.Parallel() + + res := gjson.Parse(`{"id":"abc123","name":"test"}`) + formatted, err := formatJSON(os.Stdout, "test", res, "raw", "id") + require.NoError(t, err) + require.Equal(t, `"abc123"`+"\n", string(formatted)) + }) + + t.Run("RawWithoutTransform", func(t *testing.T) { + t.Parallel() + + res := gjson.Parse(`{"id":"abc123","name":"test"}`) + formatted, err := formatJSON(os.Stdout, "test", res, "raw", "") + require.NoError(t, err) + require.Equal(t, `{"id":"abc123","name":"test"}`+"\n", string(formatted)) + }) + + t.Run("RawWithNestedTransform", func(t *testing.T) { + t.Parallel() + + res := gjson.Parse(`{"data":{"items":[1,2,3]}}`) + formatted, err := formatJSON(os.Stdout, "test", res, "raw", "data.items") + require.NoError(t, err) + require.Equal(t, "[1,2,3]\n", string(formatted)) + }) + + t.Run("RawWithNonexistentTransform", func(t *testing.T) { + t.Parallel() + + res := gjson.Parse(`{"id":"abc123"}`) + formatted, err := formatJSON(os.Stdout, "test", res, "raw", "missing") + require.NoError(t, err) + // Transform path doesn't exist, so original result is returned + require.Equal(t, `{"id":"abc123"}`+"\n", string(formatted)) + }) +} + +func TestShowJSONIterator(t *testing.T) { + t.Parallel() + + t.Run("RawMultipleItems", func(t *testing.T) { + t.Parallel() + + iter := &sliceIterator[map[string]any]{items: []map[string]any{ + {"id": "abc", "name": "first"}, + {"id": "def", "name": "second"}, + }} + captured := captureShowJSONIterator(t, iter, "raw", "", -1) + assert.Equal(t, `{"id":"abc","name":"first"}`+"\n"+`{"id":"def","name":"second"}`+"\n", captured) + }) + + t.Run("RawWithTransform", func(t *testing.T) { + t.Parallel() + + iter := &sliceIterator[map[string]any]{items: []map[string]any{ + {"id": "abc", "name": "first"}, + {"id": "def", "name": "second"}, + }} + captured := captureShowJSONIterator(t, iter, "raw", "id", -1) + assert.Equal(t, `"abc"`+"\n"+`"def"`+"\n", captured) + }) + + t.Run("LimitItems", func(t *testing.T) { + t.Parallel() + + iter := &sliceIterator[map[string]any]{items: []map[string]any{ + {"id": "abc"}, + {"id": "def"}, + {"id": "ghi"}, + }} + captured := captureShowJSONIterator(t, iter, "raw", "", 2) + assert.Equal(t, `{"id":"abc"}`+"\n"+`{"id":"def"}`+"\n", captured) + }) +} + +// sliceIterator is a simple iterator over a slice for testing. +type sliceIterator[T any] struct { + index int + items []T +} + +func (it *sliceIterator[T]) Next() bool { + it.index++ + return it.index <= len(it.items) +} + +func (it *sliceIterator[T]) Current() T { + return it.items[it.index-1] +} + +func (it *sliceIterator[T]) Err() error { + return nil +} + +var _ jsonview.Iterator[any] = (*sliceIterator[any])(nil) + +// captureShowJSONIterator runs ShowJSONIterator and captures the output written to a file. +func captureShowJSONIterator[T any](t *testing.T, iter jsonview.Iterator[T], format, transform string, itemsToDisplay int64) string { + t.Helper() + + r, w, err := os.Pipe() + require.NoError(t, err) + defer r.Close() + + err = ShowJSONIterator(w, "test", iter, format, transform, itemsToDisplay) + w.Close() + require.NoError(t, err) + + var buf bytes.Buffer + _, _ = buf.ReadFrom(r) + return buf.String() +} diff --git a/pkg/cmd/inboxapikey.go b/pkg/cmd/inboxapikey.go index 4005e3e..eb44d6d 100644 --- a/pkg/cmd/inboxapikey.go +++ b/pkg/cmd/inboxapikey.go @@ -30,7 +30,7 @@ var inboxesAPIKeysCreate = requestflag.WithInnerFlags(cli.Command{ Usage: "Name of api key.", BodyPath: "name", }, - &requestflag.Flag[any]{ + &requestflag.Flag[map[string]any]{ Name: "permissions", Usage: "Granular permissions for the API key. When ommitted all permissions are granted. Otherwise, only permissions set to true are granted.", BodyPath: "permissions", diff --git a/pkg/cmd/inboxmessage.go b/pkg/cmd/inboxmessage.go index 5af3470..90ca5a2 100644 --- a/pkg/cmd/inboxmessage.go +++ b/pkg/cmd/inboxmessage.go @@ -153,7 +153,7 @@ var inboxesMessagesForward = requestflag.WithInnerFlags(cli.Command{ Name: "cc", BodyPath: "cc", }, - &requestflag.Flag[any]{ + &requestflag.Flag[map[string]any]{ Name: "headers", Usage: "Headers to include in message.", BodyPath: "headers", @@ -297,7 +297,7 @@ var inboxesMessagesReply = requestflag.WithInnerFlags(cli.Command{ Name: "cc", BodyPath: "cc", }, - &requestflag.Flag[any]{ + &requestflag.Flag[map[string]any]{ Name: "headers", Usage: "Headers to include in message.", BodyPath: "headers", @@ -388,7 +388,7 @@ var inboxesMessagesReplyAll = requestflag.WithInnerFlags(cli.Command{ Usage: "Attachments to include in message.", BodyPath: "attachments", }, - &requestflag.Flag[any]{ + &requestflag.Flag[map[string]any]{ Name: "headers", Usage: "Headers to include in message.", BodyPath: "headers", @@ -473,7 +473,7 @@ var inboxesMessagesSend = requestflag.WithInnerFlags(cli.Command{ Name: "cc", BodyPath: "cc", }, - &requestflag.Flag[any]{ + &requestflag.Flag[map[string]any]{ Name: "headers", Usage: "Headers to include in message.", BodyPath: "headers", diff --git a/pkg/cmd/podapikey.go b/pkg/cmd/podapikey.go index 35ea60f..3b47191 100644 --- a/pkg/cmd/podapikey.go +++ b/pkg/cmd/podapikey.go @@ -30,7 +30,7 @@ var podsAPIKeysCreate = requestflag.WithInnerFlags(cli.Command{ Usage: "Name of api key.", BodyPath: "name", }, - &requestflag.Flag[any]{ + &requestflag.Flag[map[string]any]{ Name: "permissions", Usage: "Granular permissions for the API key. When ommitted all permissions are granted. Otherwise, only permissions set to true are granted.", BodyPath: "permissions", diff --git a/pkg/cmd/version.go b/pkg/cmd/version.go index d920f91..0174c7b 100644 --- a/pkg/cmd/version.go +++ b/pkg/cmd/version.go @@ -2,4 +2,4 @@ package cmd -const Version = "0.7.8" // x-release-please-version +const Version = "0.7.9" // x-release-please-version diff --git a/scripts/link b/scripts/link index e6dac01..eb04683 100755 --- a/scripts/link +++ b/scripts/link @@ -9,5 +9,9 @@ export GOPRIVATE="${GOPRIVATE:+$GOPRIVATE,}github.com/agentmail-to/agentmail-go, REPLACEMENT="${1:-"../agentmail-go"}" echo "==> Replacing Go SDK with $REPLACEMENT" -go mod edit -replace github.com/agentmail-to/agentmail-go="$REPLACEMENT" -go mod tidy -e +if [[ -d "$REPLACEMENT" ]] || go list -m "$REPLACEMENT" >/dev/null; then + go mod edit -replace github.com/agentmail-to/agentmail-go="$REPLACEMENT" + go mod tidy -e +else + echo "Skipping Go SDK replacement (branch may not exist on Go SDK)" +fi