diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 310fdcc..01e0ec0 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 }}' || true + ./scripts/link 'github.com/stainless-sdks/agentmail-go@${{ github.ref_name }}' || go mod edit -dropreplace='github.com/stainless-sdks/agentmail-go' - 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 }}' || true + ./scripts/link 'github.com/stainless-sdks/agentmail-go@${{ github.ref_name }}' || go mod edit -dropreplace='github.com/stainless-sdks/agentmail-go' - 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 }}' || true + ./scripts/link 'github.com/stainless-sdks/agentmail-go@${{ github.ref_name }}' || go mod edit -dropreplace='github.com/stainless-sdks/agentmail-go' - name: Bootstrap run: ./scripts/bootstrap diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 7ded2be..ef29836 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.7.7" + ".": "0.7.8" } \ No newline at end of file diff --git a/.stats.yml b/.stats.yml index 5d351ed..4fd92c3 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 63 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/agentmail%2Fagentmail-5b7e1869efdc36823facfecf752c92fc0dd69fcaf6c9316da8e58341260bf894.yml -openapi_spec_hash: f0c65e3e3147439fac4573015d4a8a18 -config_hash: f6f3698d5c44d64d43573d55bd5d9c49 +configured_endpoints: 94 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/agentmail%2Fagentmail-800ca32bd9e05b092bbe726b31890653d74f1d43bb8436a98476b4dc16035e15.yml +openapi_spec_hash: a2cd4922b1e8e7aef452d530efe4658d +config_hash: 1ae33a780b8223537909e04cb1264399 diff --git a/CHANGELOG.md b/CHANGELOG.md index 3538464..8121fb6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,41 @@ # Changelog +## 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) + +### Features + +* allow `-` as value representing stdin to binary-only file parameters in CLIs ([4fb771a](https://github.com/agentmail-to/agentmail-cli/commit/4fb771aa678ef1ecb7d343dae770dbb71e055508)) +* **api:** api update ([89f41fe](https://github.com/agentmail-to/agentmail-cli/commit/89f41fe796ce15b93ec8ef05e2a5cf7e3a5a3033)) +* **api:** api update ([606036f](https://github.com/agentmail-to/agentmail-cli/commit/606036f0823d4f7866e6f14ed8c1f42e233d8755)) +* **api:** api update ([5620db5](https://github.com/agentmail-to/agentmail-cli/commit/5620db56682523df5cc935944c6ebfc59bb79b22)) +* **api:** api update ([7d1c53a](https://github.com/agentmail-to/agentmail-cli/commit/7d1c53a967e324aedabd621a2c015d10c7bbd2dc)) +* **api:** api update ([8a097fc](https://github.com/agentmail-to/agentmail-cli/commit/8a097fcd316707a55cbc9a6b8afe703b957ba289)) +* **api:** manual updates ([e70a1d2](https://github.com/agentmail-to/agentmail-cli/commit/e70a1d2adcc22ed240705c31a29204ccb19c12f9)) +* **api:** manual updates ([35c4f7d](https://github.com/agentmail-to/agentmail-cli/commit/35c4f7d6e95015444332830e0c771bd84c5d3099)) +* **api:** manual updates ([990b138](https://github.com/agentmail-to/agentmail-cli/commit/990b138aa27685e76755b1d15dfb3848495a0559)) +* better error message if scheme forgotten in CLI `*_BASE_URL`/`--base-url` ([57e1a52](https://github.com/agentmail-to/agentmail-cli/commit/57e1a52c1113691d1cbca572aad8596a8f5e41b2)) +* binary-only parameters become CLI flags that take filenames only ([1b23d84](https://github.com/agentmail-to/agentmail-cli/commit/1b23d84fd4b71798b92f1911789d7fcaa1c54967)) + + +### Bug Fixes + +* fall back to main branch if linking fails in CI ([eb15535](https://github.com/agentmail-to/agentmail-cli/commit/eb15535b59629251822755e30056002d531fe641)) +* fix quoting typo ([6d8d3f4](https://github.com/agentmail-to/agentmail-cli/commit/6d8d3f4a18baa6837e93ae0c2972ac917588e2b7)) +* handle empty data set using `--format explore` ([2852394](https://github.com/agentmail-to/agentmail-cli/commit/2852394d9c496a915983a2dbc1869cbc9207df88)) +* use `RawJSON` when iterating items with `--format explore` in the CLI ([13259ac](https://github.com/agentmail-to/agentmail-cli/commit/13259ac6cd741976f91701e2eb688dc745fa6ea5)) +* use RELEASE_PAT to approve and auto-merge release PRs ([dacdd81](https://github.com/agentmail-to/agentmail-cli/commit/dacdd81f4be1104065813f510dda657b516aa844)) + + +### Chores + +* **internal:** codegen related update ([792c2ce](https://github.com/agentmail-to/agentmail-cli/commit/792c2ceb7524362f45055e5edbfee07aa3605ef6)) +* mark all CLI-related tests in Go with `t.Parallel()` ([132e0f5](https://github.com/agentmail-to/agentmail-cli/commit/132e0f5ff3e6c50c0cad628b2b258f708a3ee2cb)) +* modify CLI tests to inject stdout so mutating `os.Stdout` isn't necessary ([f488623](https://github.com/agentmail-to/agentmail-cli/commit/f488623c7a1568221a18d345a3991e130d928720)) +* switch some CLI Go tests from `os.Chdir` to `t.Chdir` ([6f4709d](https://github.com/agentmail-to/agentmail-cli/commit/6f4709d39b216adc01607cc55e9c9bfa6f1c0c95)) +* update SDK settings ([628d088](https://github.com/agentmail-to/agentmail-cli/commit/628d088add60a690cc24e460ad02fa375b82e651)) + ## 0.7.7 (2026-03-31) Full Changelog: [v0.7.6...v0.7.7](https://github.com/agentmail-to/agentmail-cli/compare/v0.7.6...v0.7.7) diff --git a/README.md b/README.md index c8b20a8..5d57b35 100644 --- a/README.md +++ b/README.md @@ -40,15 +40,63 @@ agentmail inboxes:threads list --inbox-id inb_xxx Use `--help` on any command for details. +## Environment variables + +| Environment variable | Required | +| -------------------- | -------- | +| `AGENTMAIL_API_KEY` | yes | + ## Global flags -| Flag | Description | -| --- | --- | -| `--api-key` | API key (or set `AGENTMAIL_API_KEY`) | -| `--format` | Output format: `json`, `yaml`, `pretty`, `raw`, `explore` | -| `--debug` | Enable debug logging | -| `--help` | Show help | -| `--version` | Show version | +- `--api-key` (can also be set with `AGENTMAIL_API_KEY` env var) +- `--help` - Show command line usage +- `--debug` - Enable debug logging (includes HTTP request/response details) +- `--version`, `-v` - Show the CLI version +- `--base-url` - Use a custom API backend URL +- `--format` - Change the output format (`auto`, `explore`, `json`, `jsonl`, `pretty`, `raw`, `yaml`) +- `--format-error` - Change the output format for errors (`auto`, `explore`, `json`, `jsonl`, `pretty`, `raw`, `yaml`) +- `--transform` - Transform the data output using [GJSON syntax](https://github.com/tidwall/gjson/blob/master/SYNTAX.md) +- `--transform-error` - Transform the error output using [GJSON syntax](https://github.com/tidwall/gjson/blob/master/SYNTAX.md) + +### Passing files as arguments + +To pass files to your API, you can use the `@myfile.ext` syntax: + +```bash +agentmail --arg @abe.jpg +``` + +Files can also be passed inside JSON or YAML blobs: + +```bash +agentmail --arg '{image: "@abe.jpg"}' +# Equivalent: +agentmail < --username '\@abe' +``` + +#### Explicit encoding + +For JSON endpoints, the CLI tool does filetype sniffing to determine whether the +file contents should be sent as a string literal (for plain text files) or as a +base64-encoded string literal (for binary files). If you need to explicitly send +the file as either plain text or base64-encoded data, you can use +`@file://myfile.txt` (for string encoding) or `@data://myfile.dat` (for +base64-encoding). Note that absolute paths will begin with `@file://` or +`@data://`, followed by a third `/` (for example, `@file:///tmp/file.txt`). + +```bash +agentmail --arg @data://file.txt +``` ## Documentation diff --git a/cmd/agentmail/main.go b/cmd/agentmail/main.go index cf0c172..a8702f2 100644 --- a/cmd/agentmail/main.go +++ b/cmd/agentmail/main.go @@ -23,6 +23,13 @@ func main() { prepareForAutocomplete(app) } + if baseURL, ok := os.LookupEnv("AGENTMAIL_BASE_URL"); ok { + if err := cmd.ValidateBaseURL(baseURL, "AGENTMAIL_BASE_URL"); err != nil { + fmt.Fprintf(os.Stderr, "%s\n", err.Error()) + os.Exit(1) + } + } + if err := app.Run(context.Background(), os.Args); err != nil { exitCode := 1 diff --git a/go.mod b/go.mod index 00f465d..3183fac 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/agentmail-to/agentmail-cli go 1.25 require ( - github.com/agentmail-to/agentmail-go v0.2.0 + github.com/agentmail-to/agentmail-go v0.6.0 github.com/charmbracelet/bubbles v0.21.0 github.com/charmbracelet/bubbletea v1.3.6 github.com/charmbracelet/lipgloss v1.1.0 diff --git a/go.sum b/go.sum index 82399a6..550ec0a 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -github.com/agentmail-to/agentmail-go v0.2.0 h1:MfFsY6MRMVSuTiT6626Im8vhZz+Khryv6ANcrNA/Izc= -github.com/agentmail-to/agentmail-go v0.2.0/go.mod h1:3NrKbeXLQKRgb9gj2bmCoN9WXDTy9y9yacV070xpvDU= +github.com/agentmail-to/agentmail-go v0.6.0 h1:CCQtBbYE97KASraV2v4IvSuaaJrjKDn97qH1rhMmJt8= +github.com/agentmail-to/agentmail-go v0.6.0/go.mod h1:3NrKbeXLQKRgb9gj2bmCoN9WXDTy9y9yacV070xpvDU= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8= diff --git a/internal/apiform/form_test.go b/internal/apiform/form_test.go index 2cf5bdd..f68cfd1 100644 --- a/internal/apiform/form_test.go +++ b/internal/apiform/form_test.go @@ -85,8 +85,12 @@ var tests = map[string]struct { } func TestEncode(t *testing.T) { + t.Parallel() + for name, test := range tests { t.Run(name, func(t *testing.T) { + t.Parallel() + buf := bytes.NewBuffer(nil) writer := multipart.NewWriter(buf) writer.SetBoundary("xxx") diff --git a/internal/apiquery/query_test.go b/internal/apiquery/query_test.go index 8bee784..3791ec9 100644 --- a/internal/apiquery/query_test.go +++ b/internal/apiquery/query_test.go @@ -6,6 +6,8 @@ import ( ) func TestEncode(t *testing.T) { + t.Parallel() + tests := map[string]struct { val any settings QuerySettings @@ -114,6 +116,8 @@ func TestEncode(t *testing.T) { for name, test := range tests { t.Run(name, func(t *testing.T) { + t.Parallel() + query := map[string]any{"query": test.val} values, err := MarshalWithSettings(query, test.settings) if err != nil { diff --git a/internal/autocomplete/autocomplete_test.go b/internal/autocomplete/autocomplete_test.go index 3e8aa33..2338924 100644 --- a/internal/autocomplete/autocomplete_test.go +++ b/internal/autocomplete/autocomplete_test.go @@ -8,6 +8,8 @@ import ( ) func TestGetCompletions_EmptyArgs(t *testing.T) { + t.Parallel() + root := &cli.Command{ Commands: []*cli.Command{ {Name: "generate", Usage: "Generate SDK"}, @@ -26,6 +28,8 @@ func TestGetCompletions_EmptyArgs(t *testing.T) { } func TestGetCompletions_SubcommandPrefix(t *testing.T) { + t.Parallel() + root := &cli.Command{ Commands: []*cli.Command{ {Name: "generate", Usage: "Generate SDK"}, @@ -43,6 +47,8 @@ func TestGetCompletions_SubcommandPrefix(t *testing.T) { } func TestGetCompletions_HiddenCommand(t *testing.T) { + t.Parallel() + root := &cli.Command{ Commands: []*cli.Command{ {Name: "visible", Usage: "Visible command"}, @@ -57,6 +63,8 @@ func TestGetCompletions_HiddenCommand(t *testing.T) { } func TestGetCompletions_NestedSubcommand(t *testing.T) { + t.Parallel() + root := &cli.Command{ Commands: []*cli.Command{ { @@ -79,6 +87,8 @@ func TestGetCompletions_NestedSubcommand(t *testing.T) { } func TestGetCompletions_FlagCompletion(t *testing.T) { + t.Parallel() + root := &cli.Command{ Commands: []*cli.Command{ { @@ -102,6 +112,8 @@ func TestGetCompletions_FlagCompletion(t *testing.T) { } func TestGetCompletions_ShortFlagCompletion(t *testing.T) { + t.Parallel() + root := &cli.Command{ Commands: []*cli.Command{ { @@ -123,6 +135,8 @@ func TestGetCompletions_ShortFlagCompletion(t *testing.T) { } func TestGetCompletions_FileFlagBehavior(t *testing.T) { + t.Parallel() + root := &cli.Command{ Commands: []*cli.Command{ { @@ -142,6 +156,8 @@ func TestGetCompletions_FileFlagBehavior(t *testing.T) { } func TestGetCompletions_NonBoolFlagValue(t *testing.T) { + t.Parallel() + root := &cli.Command{ Commands: []*cli.Command{ { @@ -161,6 +177,8 @@ func TestGetCompletions_NonBoolFlagValue(t *testing.T) { } func TestGetCompletions_BoolFlagDoesNotBlockCompletion(t *testing.T) { + t.Parallel() + root := &cli.Command{ Commands: []*cli.Command{ { @@ -185,6 +203,8 @@ func TestGetCompletions_BoolFlagDoesNotBlockCompletion(t *testing.T) { } func TestGetCompletions_ColonCommands_NoColonTyped(t *testing.T) { + t.Parallel() + root := &cli.Command{ Commands: []*cli.Command{ {Name: "config:get", Usage: "Get config value"}, @@ -202,6 +222,8 @@ func TestGetCompletions_ColonCommands_NoColonTyped(t *testing.T) { } func TestGetCompletions_ColonCommands_ColonTyped_Bash(t *testing.T) { + t.Parallel() + root := &cli.Command{ Commands: []*cli.Command{ {Name: "config:get", Usage: "Get config value"}, @@ -221,6 +243,8 @@ func TestGetCompletions_ColonCommands_ColonTyped_Bash(t *testing.T) { } func TestGetCompletions_ColonCommands_ColonTyped_Zsh(t *testing.T) { + t.Parallel() + root := &cli.Command{ Commands: []*cli.Command{ {Name: "config:get", Usage: "Get config value"}, @@ -240,6 +264,8 @@ func TestGetCompletions_ColonCommands_ColonTyped_Zsh(t *testing.T) { } func TestGetCompletions_BashStyleColonCompletion(t *testing.T) { + t.Parallel() + root := &cli.Command{ Commands: []*cli.Command{ {Name: "config:get", Usage: "Get config value"}, @@ -257,6 +283,8 @@ func TestGetCompletions_BashStyleColonCompletion(t *testing.T) { } func TestGetCompletions_BashStyleColonCompletion_NoMatch(t *testing.T) { + t.Parallel() + root := &cli.Command{ Commands: []*cli.Command{ {Name: "config:get", Usage: "Get config value"}, @@ -271,6 +299,8 @@ func TestGetCompletions_BashStyleColonCompletion_NoMatch(t *testing.T) { } func TestGetCompletions_ZshStyleColonCompletion(t *testing.T) { + t.Parallel() + root := &cli.Command{ Commands: []*cli.Command{ {Name: "config:get", Usage: "Get config value"}, @@ -287,6 +317,8 @@ func TestGetCompletions_ZshStyleColonCompletion(t *testing.T) { } func TestGetCompletions_MixedColonAndRegularCommands(t *testing.T) { + t.Parallel() + root := &cli.Command{ Commands: []*cli.Command{ {Name: "generate", Usage: "Generate SDK"}, @@ -305,6 +337,8 @@ func TestGetCompletions_MixedColonAndRegularCommands(t *testing.T) { } func TestGetCompletions_FlagWithBoolFlagSkipsValue(t *testing.T) { + t.Parallel() + root := &cli.Command{ Commands: []*cli.Command{ { @@ -329,6 +363,8 @@ func TestGetCompletions_FlagWithBoolFlagSkipsValue(t *testing.T) { } func TestGetCompletions_MultipleFlagsBeforeSubcommand(t *testing.T) { + t.Parallel() + root := &cli.Command{ Commands: []*cli.Command{ { @@ -353,6 +389,8 @@ func TestGetCompletions_MultipleFlagsBeforeSubcommand(t *testing.T) { } func TestGetCompletions_CommandAliases(t *testing.T) { + t.Parallel() + root := &cli.Command{ Commands: []*cli.Command{ {Name: "generate", Aliases: []string{"gen", "g"}, Usage: "Generate SDK"}, @@ -372,6 +410,8 @@ func TestGetCompletions_CommandAliases(t *testing.T) { } func TestGetCompletions_AllFlagsWhenNoPrefix(t *testing.T) { + t.Parallel() + root := &cli.Command{ Commands: []*cli.Command{ { diff --git a/internal/jsonview/explorer.go b/internal/jsonview/explorer.go index 055541e..836bb2c 100644 --- a/internal/jsonview/explorer.go +++ b/internal/jsonview/explorer.go @@ -1,6 +1,7 @@ package jsonview import ( + "bytes" "encoding/json" "errors" "fmt" @@ -309,6 +310,10 @@ func ExploreJSON(title string, json gjson.Result) error { return err } +type hasRawJSON interface { + RawJSON() string +} + // ExploreJSONStream explores JSON data loaded incrementally via an iterator func ExploreJSONStream[T any](title string, it Iterator[T]) error { anyIt := genericToAnyIterator(it) @@ -327,12 +332,12 @@ func ExploreJSONStream[T any](title string, it Iterator[T]) error { return err } - // Convert items to JSON array - jsonBytes, err := json.Marshal(items) + arrayJSONBytes, err := marshalItemsToJSONArray(items) if err != nil { return err } - arrayJSON := gjson.ParseBytes(jsonBytes) + + arrayJSON := gjson.ParseBytes(arrayJSONBytes) view, err := newTableView("", arrayJSON, false) if err != nil { return err @@ -352,6 +357,29 @@ func ExploreJSONStream[T any](title string, it Iterator[T]) error { return err } +func marshalItemsToJSONArray(items []any) ([]byte, error) { + var buf bytes.Buffer + buf.WriteByte('[') + + for i, item := range items { + if i > 0 { + buf.WriteByte(',') + } + if hasRaw, ok := item.(hasRawJSON); ok { + buf.WriteString(hasRaw.RawJSON()) + } else { + jsonData, err := json.Marshal(item) + if err != nil { + return nil, err + } + buf.Write(jsonData) + } + } + + buf.WriteByte(']') + return buf.Bytes(), nil +} + func (v *JSONViewer) current() JSONView { return v.stack[len(v.stack)-1] } func (v *JSONViewer) Init() tea.Cmd { return nil } @@ -406,6 +434,10 @@ func (v *JSONViewer) navigateForward() (tea.Model, tea.Cmd) { return v, nil } + if len(tableView.rowData) < 1 { + return v, nil + } + cursor := tableView.table.Cursor() selected := tableView.rowData[cursor] if !v.canNavigateInto(selected) { diff --git a/internal/jsonview/explorer_test.go b/internal/jsonview/explorer_test.go new file mode 100644 index 0000000..67ee730 --- /dev/null +++ b/internal/jsonview/explorer_test.go @@ -0,0 +1,66 @@ +package jsonview + +import ( + "testing" + + "github.com/charmbracelet/bubbles/help" + "github.com/tidwall/gjson" + + "github.com/stretchr/testify/require" +) + +func TestNavigateForward_EmptyRowData(t *testing.T) { + t.Parallel() + + // An empty JSON array produces a TableView with no rows. + emptyArray := gjson.Parse("[]") + view, err := newTableView("", emptyArray, false) + require.NoError(t, err) + + viewer := &JSONViewer{ + stack: []JSONView{view}, + root: "test", + help: help.New(), + } + + // Should return without panicking despite the empty data set. + model, cmd := viewer.navigateForward() + require.Equal(t, model, viewer, "expected same viewer model returned") + require.Nil(t, cmd) + + // Stack should remain unchanged (no new view pushed). + require.Equal(t, 1, len(viewer.stack), "expected stack length 1, got %d", len(viewer.stack)) +} + +// rawJSONItem implements HasRawJSON, returning pre-built JSON. +type rawJSONItem struct { + raw string +} + +func (r rawJSONItem) RawJSON() string { return r.raw } + +func TestMarshalItemsToJSONArray_WithHasRawJSON(t *testing.T) { + t.Parallel() + + items := []any{ + rawJSONItem{raw: `{"id":1,"name":"alice"}`}, + rawJSONItem{raw: `{"id":2,"name":"bob"}`}, + } + + got, err := marshalItemsToJSONArray(items) + require.NoError(t, err) + require.JSONEq(t, `[{"id":1,"name":"alice"},{"id":2,"name":"bob"}]`, string(got)) +} + +func TestMarshalItemsToJSONArray_WithoutHasRawJSON(t *testing.T) { + t.Parallel() + + items := []any{ + map[string]any{"id": 1, "name": "alice"}, + map[string]any{"id": 2, "name": "bob"}, + } + + got, err := marshalItemsToJSONArray(items) + require.NoError(t, err) + require.JSONEq(t, `[{"id":1,"name":"alice"},{"id":2,"name":"bob"}]`, string(got)) +} diff --git a/internal/requestflag/innerflag_test.go b/internal/requestflag/innerflag_test.go index 3f204c9..133e8b4 100644 --- a/internal/requestflag/innerflag_test.go +++ b/internal/requestflag/innerflag_test.go @@ -8,6 +8,8 @@ import ( ) func TestInnerFlagSet(t *testing.T) { + t.Parallel() + tests := []struct { name string flagType string @@ -27,6 +29,8 @@ func TestInnerFlagSet(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + t.Parallel() + outerFlag := &Flag[map[string]any]{ Name: "test-flag", } @@ -81,6 +85,8 @@ func TestInnerFlagSet(t *testing.T) { } func TestInnerFlagValidator(t *testing.T) { + t.Parallel() + outerFlag := &Flag[map[string]any]{Name: "test-flag"} innerFlag := &InnerFlag[int64]{ @@ -105,6 +111,8 @@ func TestInnerFlagValidator(t *testing.T) { } func TestWithInnerFlags(t *testing.T) { + t.Parallel() + outerFlag := &Flag[map[string]any]{Name: "outer"} innerFlag := &InnerFlag[string]{ Name: "outer.baz", @@ -126,6 +134,8 @@ func TestWithInnerFlags(t *testing.T) { } func TestInnerFlagTypeNames(t *testing.T) { + t.Parallel() + tests := []struct { name string flag cli.DocGenerationFlag @@ -143,6 +153,8 @@ func TestInnerFlagTypeNames(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + t.Parallel() + typeName := tt.flag.TypeName() assert.Equal(t, tt.expected, typeName, "Expected type name %q, got %q", tt.expected, typeName) }) @@ -150,8 +162,12 @@ func TestInnerFlagTypeNames(t *testing.T) { } func TestInnerYamlHandling(t *testing.T) { + t.Parallel() + // Test with map value t.Run("Parse YAML to map", func(t *testing.T) { + t.Parallel() + outerFlag := &Flag[map[string]any]{Name: "outer"} innerFlag := &InnerFlag[map[string]any]{ Name: "outer.baz", @@ -176,6 +192,8 @@ func TestInnerYamlHandling(t *testing.T) { // Test with invalid YAML t.Run("Parse invalid YAML", func(t *testing.T) { + t.Parallel() + outerFlag := &Flag[map[string]any]{Name: "outer"} innerFlag := &InnerFlag[map[string]any]{ Name: "outer.baz", @@ -190,6 +208,8 @@ func TestInnerYamlHandling(t *testing.T) { // Test setting inner flags on a map multiple times t.Run("Set inner flags on map multiple times", func(t *testing.T) { + t.Parallel() + outerFlag := &Flag[map[string]any]{Name: "outer"} // Set first inner flag @@ -219,6 +239,8 @@ func TestInnerYamlHandling(t *testing.T) { // Test setting YAML and then an inner flag t.Run("Set YAML and then inner flag", func(t *testing.T) { + t.Parallel() + outerFlag := &Flag[map[string]any]{Name: "outer"} // First set the outer flag with YAML @@ -246,7 +268,11 @@ func TestInnerYamlHandling(t *testing.T) { } func TestInnerFlagWithSliceType(t *testing.T) { + t.Parallel() + t.Run("Setting inner flags on slice of maps", func(t *testing.T) { + t.Parallel() + outerFlag := &Flag[[]map[string]any]{Name: "outer"} // Set first inner flag (should create first item) @@ -284,6 +310,8 @@ func TestInnerFlagWithSliceType(t *testing.T) { }) t.Run("Appending to existing slice", func(t *testing.T) { + t.Parallel() + // Initialize with existing items outerFlag := &Flag[[]map[string]any]{Name: "outer"} err := outerFlag.Set(outerFlag.Name, `{name: initial}`) diff --git a/internal/requestflag/requestflag.go b/internal/requestflag/requestflag.go index 32c13f5..bdef64f 100644 --- a/internal/requestflag/requestflag.go +++ b/internal/requestflag/requestflag.go @@ -43,6 +43,11 @@ type Flag[ // parameters. Const bool + // FileInput, when true, indicates that the flag value is always treated as a file path. The file is read + // automatically without requiring the "@" prefix. This is used for parameters with `type: string, format: + // binary` in the OpenAPI spec. + FileInput bool + // unexported fields for internal use count int // number of times the flag has been set hasBeenSet bool // whether the flag has been set from env or file @@ -59,6 +64,7 @@ type InRequest interface { GetHeaderPath() string GetBodyPath() string IsBodyRoot() bool + IsFileInput() bool } func (f Flag[T]) GetQueryPath() string { @@ -77,6 +83,10 @@ func (f Flag[T]) IsBodyRoot() bool { return f.BodyRoot } +func (f Flag[T]) IsFileInput() bool { + return f.FileInput +} + // The values that will be sent in different parts of a request. type RequestContents struct { Queries map[string]any diff --git a/internal/requestflag/requestflag_test.go b/internal/requestflag/requestflag_test.go index 9751904..0e86e07 100644 --- a/internal/requestflag/requestflag_test.go +++ b/internal/requestflag/requestflag_test.go @@ -11,6 +11,8 @@ import ( ) func TestDateValueParse(t *testing.T) { + t.Parallel() + tests := []struct { name string input string @@ -56,6 +58,8 @@ func TestDateValueParse(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + t.Parallel() + var d DateValue err := d.Parse(tt.input) @@ -70,6 +74,8 @@ func TestDateValueParse(t *testing.T) { } func TestDateTimeValueParse(t *testing.T) { + t.Parallel() + tests := []struct { name string input string @@ -119,6 +125,8 @@ func TestDateTimeValueParse(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + t.Parallel() + var d DateTimeValue err := d.Parse(tt.input) @@ -136,6 +144,8 @@ func TestDateTimeValueParse(t *testing.T) { } func TestTimeValueParse(t *testing.T) { + t.Parallel() + tests := []struct { name string input string @@ -181,6 +191,8 @@ func TestTimeValueParse(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + t.Parallel() + var tv TimeValue err := tv.Parse(tt.input) @@ -195,7 +207,11 @@ func TestTimeValueParse(t *testing.T) { } func TestRequestParams(t *testing.T) { + t.Parallel() + t.Run("map body type", func(t *testing.T) { + t.Parallel() + // Create a mock command with flags cmd := &cli.Command{ Name: "test", @@ -283,6 +299,8 @@ func TestRequestParams(t *testing.T) { }) t.Run("non-map body type", func(t *testing.T) { + t.Parallel() + // Create a mock command with flags cmd := &cli.Command{ Name: "test", @@ -304,6 +322,8 @@ func TestRequestParams(t *testing.T) { } func TestFlagSet(t *testing.T) { + t.Parallel() + strFlag := &Flag[string]{ Name: "string-flag", Default: "default-string", @@ -327,38 +347,52 @@ func TestFlagSet(t *testing.T) { // Test initialization and setting t.Run("PreParse initialization", func(t *testing.T) { + t.Parallel() + assert.NoError(t, strFlag.PreParse()) assert.True(t, strFlag.applied) assert.Equal(t, "default-string", strFlag.Get()) }) t.Run("Set string flag", func(t *testing.T) { + t.Parallel() + assert.NoError(t, strFlag.Set("string-flag", "new-value")) assert.Equal(t, "new-value", strFlag.Get()) assert.True(t, strFlag.IsSet()) }) t.Run("Set int flag with valid value", func(t *testing.T) { + t.Parallel() + assert.NoError(t, superstitiousIntFlag.Set("int-flag", "100")) assert.Equal(t, int64(100), superstitiousIntFlag.Get()) assert.True(t, superstitiousIntFlag.IsSet()) }) t.Run("Set int flag with invalid value", func(t *testing.T) { + t.Parallel() + assert.Error(t, superstitiousIntFlag.Set("int-flag", "not-an-int")) }) t.Run("Set int flag with validator failing", func(t *testing.T) { + t.Parallel() + assert.Error(t, superstitiousIntFlag.Set("int-flag", "13")) }) t.Run("Set bool flag", func(t *testing.T) { + t.Parallel() + assert.NoError(t, boolFlag.Set("bool-flag", "true")) assert.Equal(t, true, boolFlag.Get()) assert.True(t, boolFlag.IsSet()) }) t.Run("Set slice flag with multiple values", func(t *testing.T) { + t.Parallel() + sliceFlag := &Flag[[]int64]{ Name: "slice-flag", Default: []int64{}, @@ -381,6 +415,8 @@ func TestFlagSet(t *testing.T) { }) t.Run("Set slice flag with a nonempty default", func(t *testing.T) { + t.Parallel() + sliceFlag := &Flag[[]int64]{ Name: "slice-flag", Default: []int64{99, 100}, @@ -400,6 +436,8 @@ func TestFlagSet(t *testing.T) { } func TestParseTimeWithFormats(t *testing.T) { + t.Parallel() + tests := []struct { name string input string @@ -439,6 +477,8 @@ func TestParseTimeWithFormats(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + t.Parallel() + got, err := parseTimeWithFormats(tt.input, tt.formats) if tt.wantErr { @@ -452,8 +492,12 @@ func TestParseTimeWithFormats(t *testing.T) { } func TestYamlHandling(t *testing.T) { + t.Parallel() + // Test with any value t.Run("Parse YAML to any", func(t *testing.T) { + t.Parallel() + cv := &cliValue[any]{} err := cv.Set("name: test\nvalue: 42\n") assert.NoError(t, err) @@ -478,6 +522,8 @@ func TestYamlHandling(t *testing.T) { // Test with array t.Run("Parse YAML array", func(t *testing.T) { + t.Parallel() + cv := &cliValue[any]{} err := cv.Set("- item1\n- item2\n- item3\n") assert.NoError(t, err) @@ -495,6 +541,8 @@ func TestYamlHandling(t *testing.T) { }) t.Run("Parse @file.txt as YAML", func(t *testing.T) { + t.Parallel() + flag := &Flag[any]{ Name: "file-flag", Default: nil, @@ -507,6 +555,8 @@ func TestYamlHandling(t *testing.T) { }) t.Run("Parse @file.txt list as YAML", func(t *testing.T) { + t.Parallel() + flag := &Flag[[]any]{ Name: "file-flag", Default: nil, @@ -520,6 +570,8 @@ func TestYamlHandling(t *testing.T) { }) t.Run("Parse identifiers as YAML", func(t *testing.T) { + t.Parallel() + tests := []string{ "hello", "e4e355fa-b03b-4c57-a73d-25c9733eec79", @@ -555,6 +607,8 @@ func TestYamlHandling(t *testing.T) { // Test with invalid YAML t.Run("Parse invalid YAML", func(t *testing.T) { + t.Parallel() + invalidYaml := `[not closed` cv := &cliValue[any]{} err := cv.Set(invalidYaml) @@ -563,6 +617,8 @@ func TestYamlHandling(t *testing.T) { } func TestFlagTypeNames(t *testing.T) { + t.Parallel() + tests := []struct { name string flag cli.DocGenerationFlag @@ -583,6 +639,8 @@ func TestFlagTypeNames(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + t.Parallel() + typeName := tt.flag.TypeName() assert.Equal(t, tt.expected, typeName, "Expected type name %q, got %q", tt.expected, typeName) }) diff --git a/pkg/cmd/agent.go b/pkg/cmd/agent.go index a1b939d..03ee71b 100644 --- a/pkg/cmd/agent.go +++ b/pkg/cmd/agent.go @@ -17,7 +17,7 @@ import ( var agentSignUp = cli.Command{ Name: "sign-up", - Usage: "Create a new agent organization with an inbox and API key. A 6-digit OTP is sent\nto the human's email for verification.", + Usage: "Create a new agent organization with an inbox and API key. This endpoint is for\nsigning up for the first time. If you've already signed up, you're all set —\njust use your existing API key.", Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ diff --git a/pkg/cmd/apikey.go b/pkg/cmd/apikey.go index cfdecd4..f7f8b3a 100644 --- a/pkg/cmd/apikey.go +++ b/pkg/cmd/apikey.go @@ -17,13 +17,12 @@ import ( var apiKeysCreate = requestflag.WithInnerFlags(cli.Command{ Name: "create", - Usage: "Create API Key", + Usage: "**CLI:**", Suggest: true, Flags: []cli.Flag{ - &requestflag.Flag[string]{ + &requestflag.Flag[any]{ Name: "name", Usage: "Name of api key.", - Required: true, BodyPath: "name", }, &requestflag.Flag[any]{ @@ -216,7 +215,7 @@ var apiKeysCreate = requestflag.WithInnerFlags(cli.Command{ var apiKeysList = cli.Command{ Name: "list", - Usage: "List API Keys", + Usage: "**CLI:**", Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[any]{ @@ -239,6 +238,21 @@ var apiKeysList = cli.Command{ HideHelpCommand: true, } +var apiKeysDelete = cli.Command{ + Name: "delete", + Usage: "**CLI:**", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "api-key-id", + Usage: "ID of api key.", + Required: true, + }, + }, + Action: handleAPIKeysDelete, + HideHelpCommand: true, +} + func handleAPIKeysCreate(ctx context.Context, cmd *cli.Command) error { client := agentmail.NewClient(getDefaultRequestOptions(cmd)...) unusedArgs := cmd.Args().Slice() @@ -306,3 +320,28 @@ func handleAPIKeysList(ctx context.Context, cmd *cli.Command) error { transform := cmd.Root().String("transform") return ShowJSON(os.Stdout, "api-keys list", obj, format, transform) } + +func handleAPIKeysDelete(ctx context.Context, cmd *cli.Command) error { + client := agentmail.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("api-key-id") && len(unusedArgs) > 0 { + cmd.Set("api-key-id", unusedArgs[0]) + unusedArgs = unusedArgs[1:] + } + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + + options, err := flagOptions( + cmd, + apiquery.NestedQueryFormatBrackets, + apiquery.ArrayQueryFormatComma, + EmptyBody, + false, + ) + if err != nil { + return err + } + + return client.APIKeys.Delete(ctx, cmd.Value("api-key-id").(string), options...) +} diff --git a/pkg/cmd/apikey_test.go b/pkg/cmd/apikey_test.go index 69d9ae2..2dd1fa4 100644 --- a/pkg/cmd/apikey_test.go +++ b/pkg/cmd/apikey_test.go @@ -130,3 +130,15 @@ func TestAPIKeysList(t *testing.T) { ) }) } + +func TestAPIKeysDelete(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "api-keys", "delete", + "--api-key-id", "api_key_id", + ) + }) +} diff --git a/pkg/cmd/cmd.go b/pkg/cmd/cmd.go index 96062c5..7a44df1 100644 --- a/pkg/cmd/cmd.go +++ b/pkg/cmd/cmd.go @@ -39,6 +39,9 @@ func init() { Name: "base-url", DefaultText: "url", Usage: "Override the base URL for API requests", + Validator: func(baseURL string) error { + return ValidateBaseURL(baseURL, "--base-url") + }, }, &cli.StringFlag{ Name: "format", @@ -112,6 +115,7 @@ func init() { &inboxesDraftsUpdate, &inboxesDraftsList, &inboxesDraftsDelete, + &inboxesDraftsGetAttachment, &inboxesDraftsSend, }, }, @@ -142,6 +146,27 @@ func init() { &inboxesThreadsGetAttachment, }, }, + { + Name: "inboxes:lists", + Category: "API RESOURCE", + Suggest: true, + Commands: []*cli.Command{ + &inboxesListsCreate, + &inboxesListsRetrieve, + &inboxesListsList, + &inboxesListsDelete, + }, + }, + { + Name: "inboxes:api-keys", + Category: "API RESOURCE", + Suggest: true, + Commands: []*cli.Command{ + &inboxesAPIKeysCreate, + &inboxesAPIKeysList, + &inboxesAPIKeysDelete, + }, + }, { Name: "pods", Category: "API RESOURCE", @@ -159,8 +184,12 @@ func init() { Suggest: true, Commands: []*cli.Command{ &podsDomainsCreate, + &podsDomainsRetrieve, + &podsDomainsUpdate, &podsDomainsList, &podsDomainsDelete, + &podsDomainsGetZoneFile, + &podsDomainsVerify, }, }, { @@ -170,6 +199,7 @@ func init() { Commands: []*cli.Command{ &podsDraftsRetrieve, &podsDraftsList, + &podsDraftsGetAttachment, }, }, { @@ -179,6 +209,7 @@ func init() { Commands: []*cli.Command{ &podsInboxesCreate, &podsInboxesRetrieve, + &podsInboxesUpdate, &podsInboxesList, &podsInboxesDelete, }, @@ -190,9 +221,39 @@ func init() { Commands: []*cli.Command{ &podsThreadsRetrieve, &podsThreadsList, + &podsThreadsDelete, &podsThreadsGetAttachment, }, }, + { + Name: "pods:lists", + Category: "API RESOURCE", + Suggest: true, + Commands: []*cli.Command{ + &podsListsCreate, + &podsListsRetrieve, + &podsListsList, + &podsListsDelete, + }, + }, + { + Name: "pods:api-keys", + Category: "API RESOURCE", + Suggest: true, + Commands: []*cli.Command{ + &podsAPIKeysCreate, + &podsAPIKeysList, + &podsAPIKeysDelete, + }, + }, + { + Name: "pods:metrics", + Category: "API RESOURCE", + Suggest: true, + Commands: []*cli.Command{ + &podsMetricsQuery, + }, + }, { Name: "webhooks", Category: "API RESOURCE", @@ -212,6 +273,7 @@ func init() { Commands: []*cli.Command{ &apiKeysCreate, &apiKeysList, + &apiKeysDelete, }, }, { @@ -221,6 +283,7 @@ func init() { Commands: []*cli.Command{ &domainsCreate, &domainsRetrieve, + &domainsUpdate, &domainsList, &domainsDelete, &domainsGetZoneFile, @@ -234,6 +297,18 @@ func init() { Commands: []*cli.Command{ &draftsRetrieve, &draftsList, + &draftsGetAttachment, + }, + }, + { + Name: "lists", + Category: "API RESOURCE", + Suggest: true, + Commands: []*cli.Command{ + &listsCreate, + &listsRetrieve, + &listsList, + &listsDelete, }, }, { @@ -259,6 +334,7 @@ func init() { Commands: []*cli.Command{ &threadsRetrieve, &threadsList, + &threadsDelete, &threadsRetrieveAttachment, }, }, diff --git a/pkg/cmd/cmdutil.go b/pkg/cmd/cmdutil.go index 468bf09..d9a875c 100644 --- a/pkg/cmd/cmdutil.go +++ b/pkg/cmd/cmdutil.go @@ -29,6 +29,15 @@ import ( var OutputFormats = []string{"auto", "explore", "json", "jsonl", "pretty", "raw", "yaml"} +// ValidateBaseURL checks that a base URL is correctly prefixed with a protocol scheme and produces a better +// error message than the person would see otherwise if it doesn't. +func ValidateBaseURL(value, source string) error { + if value != "" && !strings.HasPrefix(value, "http://") && !strings.HasPrefix(value, "https://") { + return fmt.Errorf("%s %q is missing a scheme (expected http:// or https://)", source, value) + } + return nil +} + func getDefaultRequestOptions(cmd *cli.Command) []option.RequestOption { opts := []option.RequestOption{ option.WithHeader("User-Agent", fmt.Sprintf("Agentmail/CLI %s", Version)), @@ -196,7 +205,10 @@ func streamToStdout(generateOutput func(w *os.File) error) error { return err } -func writeBinaryResponse(response *http.Response, outfile string) (string, error) { +// writeBinaryResponse writes a binary response to stdout or a file. +// +// Takes in a stdout reference so we can test this function without overriding os.Stdout in tests. +func writeBinaryResponse(response *http.Response, stdout io.Writer, outfile string) (string, error) { defer response.Body.Close() body, err := io.ReadAll(response.Body) if err != nil { @@ -204,13 +216,13 @@ func writeBinaryResponse(response *http.Response, outfile string) (string, error } switch outfile { case "-", "/dev/stdout": - _, err := os.Stdout.Write(body) + _, err := stdout.Write(body) return "", err case "": // If output file is unspecified, then print to stdout for plain text or // if stdout is not a terminal: if !isTerminal(os.Stdout) || isUTF8TextFile(body) { - _, err := os.Stdout.Write(body) + _, err := stdout.Write(body) return "", err } @@ -384,7 +396,7 @@ func countTerminalLines(data []byte, terminalWidth int) int { return bytes.Count([]byte(wrap.String(string(data), terminalWidth)), []byte("\n")) } -type HasRawJSON interface { +type hasRawJSON interface { RawJSON() string } @@ -410,7 +422,7 @@ func ShowJSONIterator[T any](stdout *os.File, title string, iter jsonview.Iterat for itemsToDisplay != 0 && iter.Next() { item := iter.Current() var obj gjson.Result - if hasRaw, ok := any(item).(HasRawJSON); ok { + if hasRaw, ok := any(item).(hasRawJSON); ok { obj = gjson.Parse(hasRaw.RawJSON()) } else { jsonData, err := json.Marshal(item) @@ -457,7 +469,7 @@ func ShowJSONIterator[T any](stdout *os.File, title string, iter jsonview.Iterat } item := iter.Current() var obj gjson.Result - if hasRaw, ok := any(item).(HasRawJSON); ok { + if hasRaw, ok := any(item).(hasRawJSON); ok { obj = gjson.Parse(hasRaw.RawJSON()) } else { jsonData, err := json.Marshal(item) diff --git a/pkg/cmd/cmdutil_test.go b/pkg/cmd/cmdutil_test.go index 0a46fd1..8eca397 100644 --- a/pkg/cmd/cmdutil_test.go +++ b/pkg/cmd/cmdutil_test.go @@ -32,7 +32,7 @@ func TestWriteBinaryResponse(t *testing.T) { Body: io.NopCloser(bytes.NewReader(body)), } - msg, err := writeBinaryResponse(resp, outfile) + msg, err := writeBinaryResponse(resp, os.Stdout, outfile) require.NoError(t, err) assert.Contains(t, msg, outfile) @@ -43,34 +43,24 @@ func TestWriteBinaryResponse(t *testing.T) { }) t.Run("write to stdout", func(t *testing.T) { - oldStdout := os.Stdout - r, w, _ := os.Pipe() - os.Stdout = w + t.Parallel() + var buf bytes.Buffer body := []byte("stdout content") resp := &http.Response{ Body: io.NopCloser(bytes.NewReader(body)), } - msg, err := writeBinaryResponse(resp, "-") - - w.Close() - os.Stdout = oldStdout + msg, err := writeBinaryResponse(resp, &buf, "-") require.NoError(t, err) assert.Empty(t, msg) - - var buf bytes.Buffer - _, _ = buf.ReadFrom(r) assert.Equal(t, body, buf.Bytes()) }) } func TestCreateDownloadFile(t *testing.T) { t.Run("creates file with filename from header", func(t *testing.T) { - tmpDir := t.TempDir() - oldWd, _ := os.Getwd() - os.Chdir(tmpDir) - defer os.Chdir(oldWd) + t.Chdir(t.TempDir()) resp := &http.Response{ Header: http.Header{ @@ -96,10 +86,7 @@ func TestCreateDownloadFile(t *testing.T) { }) t.Run("creates temp file when no header", func(t *testing.T) { - tmpDir := t.TempDir() - oldWd, _ := os.Getwd() - os.Chdir(tmpDir) - defer os.Chdir(oldWd) + t.Chdir(t.TempDir()) resp := &http.Response{Header: http.Header{}} file, err := createDownloadFile(resp, []byte("test content")) @@ -109,10 +96,7 @@ func TestCreateDownloadFile(t *testing.T) { }) t.Run("prevents directory traversal", func(t *testing.T) { - tmpDir := t.TempDir() - oldWd, _ := os.Getwd() - os.Chdir(tmpDir) - defer os.Chdir(oldWd) + t.Chdir(t.TempDir()) resp := &http.Response{ Header: http.Header{ @@ -125,3 +109,42 @@ func TestCreateDownloadFile(t *testing.T) { assert.Equal(t, "passwd", filepath.Base(file.Name())) }) } + +func TestValidateBaseURL(t *testing.T) { + t.Parallel() + + t.Run("ValidHTTPS", func(t *testing.T) { + t.Parallel() + + require.NoError(t, ValidateBaseURL("https://api.example.com", "--base-url")) + }) + + t.Run("ValidHTTP", func(t *testing.T) { + t.Parallel() + + require.NoError(t, ValidateBaseURL("http://localhost:8080", "--base-url")) + }) + + t.Run("Empty", func(t *testing.T) { + t.Parallel() + + require.NoError(t, ValidateBaseURL("", "MY_BASE_URL")) + }) + + t.Run("MissingScheme", func(t *testing.T) { + t.Parallel() + + err := ValidateBaseURL("localhost:8080", "MY_BASE_URL") + require.Error(t, err) + assert.Contains(t, err.Error(), "MY_BASE_URL") + assert.Contains(t, err.Error(), "missing a scheme") + }) + + t.Run("HostOnly", func(t *testing.T) { + t.Parallel() + + err := ValidateBaseURL("api.example.com", "--base-url") + require.Error(t, err) + assert.Contains(t, err.Error(), "--base-url") + }) +} diff --git a/pkg/cmd/domain.go b/pkg/cmd/domain.go index 3341305..dd4411c 100644 --- a/pkg/cmd/domain.go +++ b/pkg/cmd/domain.go @@ -17,7 +17,7 @@ import ( var domainsCreate = cli.Command{ Name: "create", - Usage: "Create Domain", + Usage: "**CLI:**", Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ @@ -39,7 +39,7 @@ var domainsCreate = cli.Command{ var domainsRetrieve = cli.Command{ Name: "retrieve", - Usage: "Get Domain", + Usage: "**CLI:**", Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ @@ -52,9 +52,29 @@ var domainsRetrieve = cli.Command{ HideHelpCommand: true, } +var domainsUpdate = cli.Command{ + Name: "update", + Usage: "**CLI:**", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "domain-id", + Usage: "The ID of the domain.", + Required: true, + }, + &requestflag.Flag[any]{ + Name: "feedback-enabled", + Usage: "Bounce and complaint notifications are sent to your inboxes.", + BodyPath: "feedback_enabled", + }, + }, + Action: handleDomainsUpdate, + HideHelpCommand: true, +} + var domainsList = cli.Command{ Name: "list", - Usage: "List Domains", + Usage: "**CLI:**", Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[any]{ @@ -79,7 +99,7 @@ var domainsList = cli.Command{ var domainsDelete = cli.Command{ Name: "delete", - Usage: "Delete Domain", + Usage: "**CLI:**", Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ @@ -94,7 +114,7 @@ var domainsDelete = cli.Command{ var domainsGetZoneFile = cli.Command{ Name: "get-zone-file", - Usage: "Get Zone File", + Usage: "**CLI:**", Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ @@ -109,7 +129,7 @@ var domainsGetZoneFile = cli.Command{ var domainsVerify = cli.Command{ Name: "verify", - Usage: "Verify Domain", + Usage: "**CLI:**", Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ @@ -191,6 +211,48 @@ func handleDomainsRetrieve(ctx context.Context, cmd *cli.Command) error { return ShowJSON(os.Stdout, "domains retrieve", obj, format, transform) } +func handleDomainsUpdate(ctx context.Context, cmd *cli.Command) error { + client := agentmail.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("domain-id") && len(unusedArgs) > 0 { + cmd.Set("domain-id", unusedArgs[0]) + unusedArgs = unusedArgs[1:] + } + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + + params := agentmail.DomainUpdateParams{} + + options, err := flagOptions( + cmd, + apiquery.NestedQueryFormatBrackets, + apiquery.ArrayQueryFormatComma, + ApplicationJSON, + false, + ) + if err != nil { + return err + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Domains.Update( + ctx, + cmd.Value("domain-id").(string), + params, + options..., + ) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + transform := cmd.Root().String("transform") + return ShowJSON(os.Stdout, "domains update", obj, format, transform) +} + func handleDomainsList(ctx context.Context, cmd *cli.Command) error { client := agentmail.NewClient(getDefaultRequestOptions(cmd)...) unusedArgs := cmd.Args().Slice() diff --git a/pkg/cmd/domain_test.go b/pkg/cmd/domain_test.go index 4f055bb..fa01fd9 100644 --- a/pkg/cmd/domain_test.go +++ b/pkg/cmd/domain_test.go @@ -45,6 +45,30 @@ func TestDomainsRetrieve(t *testing.T) { }) } +func TestDomainsUpdate(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "domains", "update", + "--domain-id", "domain_id", + "--feedback-enabled=true", + ) + }) + + t.Run("piping data", func(t *testing.T) { + // Test piping YAML data over stdin + pipeData := []byte("feedback_enabled: true") + mocktest.TestRunMockTestWithPipeAndFlags( + t, pipeData, + "--api-key", "string", + "domains", "update", + "--domain-id", "domain_id", + ) + }) +} + func TestDomainsList(t *testing.T) { t.Skip("Mock server tests are disabled") t.Run("regular flags", func(t *testing.T) { diff --git a/pkg/cmd/draft.go b/pkg/cmd/draft.go index 9fe5f03..2a5da24 100644 --- a/pkg/cmd/draft.go +++ b/pkg/cmd/draft.go @@ -17,7 +17,7 @@ import ( var draftsRetrieve = cli.Command{ Name: "retrieve", - Usage: "Get Draft", + Usage: "**CLI:**", Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ @@ -32,7 +32,7 @@ var draftsRetrieve = cli.Command{ var draftsList = cli.Command{ Name: "list", - Usage: "List Drafts", + Usage: "**CLI:**", Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[any]{ @@ -70,6 +70,26 @@ var draftsList = cli.Command{ HideHelpCommand: true, } +var draftsGetAttachment = cli.Command{ + Name: "get-attachment", + Usage: "**CLI:**", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "draft-id", + Usage: "ID of draft.", + Required: true, + }, + &requestflag.Flag[string]{ + Name: "attachment-id", + Usage: "ID of attachment.", + Required: true, + }, + }, + Action: handleDraftsGetAttachment, + HideHelpCommand: true, +} + func handleDraftsRetrieve(ctx context.Context, cmd *cli.Command) error { client := agentmail.NewClient(getDefaultRequestOptions(cmd)...) unusedArgs := cmd.Args().Slice() @@ -138,3 +158,47 @@ func handleDraftsList(ctx context.Context, cmd *cli.Command) error { transform := cmd.Root().String("transform") return ShowJSON(os.Stdout, "drafts list", obj, format, transform) } + +func handleDraftsGetAttachment(ctx context.Context, cmd *cli.Command) error { + client := agentmail.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("attachment-id") && len(unusedArgs) > 0 { + cmd.Set("attachment-id", unusedArgs[0]) + unusedArgs = unusedArgs[1:] + } + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + + params := agentmail.DraftGetAttachmentParams{ + DraftID: cmd.Value("draft-id").(string), + } + + options, err := flagOptions( + cmd, + apiquery.NestedQueryFormatBrackets, + apiquery.ArrayQueryFormatComma, + EmptyBody, + false, + ) + if err != nil { + return err + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Drafts.GetAttachment( + ctx, + cmd.Value("attachment-id").(string), + params, + options..., + ) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + transform := cmd.Root().String("transform") + return ShowJSON(os.Stdout, "drafts get-attachment", obj, format, transform) +} diff --git a/pkg/cmd/draft_test.go b/pkg/cmd/draft_test.go index 25c9dbd..fee25c7 100644 --- a/pkg/cmd/draft_test.go +++ b/pkg/cmd/draft_test.go @@ -36,3 +36,16 @@ func TestDraftsList(t *testing.T) { ) }) } + +func TestDraftsGetAttachment(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "drafts", "get-attachment", + "--draft-id", "draft_id", + "--attachment-id", "attachment_id", + ) + }) +} diff --git a/pkg/cmd/flagoptions.go b/pkg/cmd/flagoptions.go index 0bd1ff5..0d94ce4 100644 --- a/pkg/cmd/flagoptions.go +++ b/pkg/cmd/flagoptions.go @@ -40,12 +40,48 @@ const ( EmbedIOReader ) -func embedFiles(obj any, embedStyle FileEmbedStyle) (any, error) { +// onceStdinReader wraps an io.Reader that can only be consumed once, used to ensure stdin is read by at most +// one parameter (or only for a body root parameter or only for YAML parameter input). If reason is set, stdin +// is unavailable and read() returns an error explaining why. +type onceStdinReader struct { + stdinReader io.Reader + failureReason string +} + +func (o *onceStdinReader) read() (io.Reader, error) { + if o.failureReason != "" { + return nil, fmt.Errorf("cannot read from stdin: %s", o.failureReason) + } + if o.stdinReader == nil { + return nil, fmt.Errorf("stdin has already been read by another parameter; it can only be read once") + } + r := o.stdinReader + o.stdinReader = nil + return r, nil +} + +func (o *onceStdinReader) readAll() ([]byte, error) { + r, err := o.read() + if err != nil { + return nil, err + } + return io.ReadAll(r) +} + +func isStdinPath(s string) bool { + switch s { + case "-", "/dev/fd/0", "/dev/stdin": + return true + } + return false +} + +func embedFiles(obj any, embedStyle FileEmbedStyle, stdin *onceStdinReader) (any, error) { if obj == nil { return obj, nil } v := reflect.ValueOf(obj) - result, err := embedFilesValue(v, embedStyle) + result, err := embedFilesValue(v, embedStyle, stdin) if err != nil { return nil, err } @@ -53,7 +89,7 @@ func embedFiles(obj any, embedStyle FileEmbedStyle) (any, error) { } // Replace "@file.txt" with the file's contents inside a value -func embedFilesValue(v reflect.Value, embedStyle FileEmbedStyle) (reflect.Value, error) { +func embedFilesValue(v reflect.Value, embedStyle FileEmbedStyle, stdin *onceStdinReader) (reflect.Value, error) { // Unwrap interface values to get the concrete type if v.Kind() == reflect.Interface { if v.IsNil() { @@ -74,7 +110,7 @@ func embedFilesValue(v reflect.Value, embedStyle FileEmbedStyle) (reflect.Value, for iter.Next() { key := iter.Key() val := iter.Value() - newVal, err := embedFilesValue(val, embedStyle) + newVal, err := embedFilesValue(val, embedStyle, stdin) if err != nil { return reflect.Value{}, err } @@ -89,7 +125,7 @@ func embedFilesValue(v reflect.Value, embedStyle FileEmbedStyle) (reflect.Value, // Use `[]any` to allow for types to change when embedding files result := reflect.MakeSlice(reflect.TypeOf([]any{}), v.Len(), v.Len()) for i := 0; i < v.Len(); i++ { - newVal, err := embedFilesValue(v.Index(i), embedStyle) + newVal, err := embedFilesValue(v.Index(i), embedStyle, stdin) if err != nil { return reflect.Value{}, err } @@ -98,6 +134,28 @@ func embedFilesValue(v reflect.Value, embedStyle FileEmbedStyle) (reflect.Value, return result, nil case reflect.String: + // FilePathValue is always treated as a file path without needing the "@" prefix. + // These only appear on binary upload parameters (multipart/octet-stream), which + // always use EmbedIOReader. + if v.Type() == reflect.TypeOf(FilePathValue("")) { + s := v.String() + if s == "" { + return v, nil + } + if isStdinPath(s) { + content, err := stdin.readAll() + if err != nil { + return v, err + } + return reflect.ValueOf(string(content)), nil + } + content, err := os.ReadFile(s) + if err != nil { + return v, err + } + return reflect.ValueOf(string(content)), nil + } + s := v.String() if literal, ok := strings.CutPrefix(s, "\\@"); ok { // Allow for escaped @ signs if you don't want them to be treated as files @@ -108,6 +166,13 @@ func embedFilesValue(v reflect.Value, embedStyle FileEmbedStyle) (reflect.Value, if filename, ok := strings.CutPrefix(s, "@data://"); ok { // The "@data://" prefix is for files you explicitly want to upload // as base64-encoded (even if the file itself is plain text) + if isStdinPath(filename) { + content, err := stdin.readAll() + if err != nil { + return v, err + } + return reflect.ValueOf(base64.StdEncoding.EncodeToString(content)), nil + } content, err := os.ReadFile(filename) if err != nil { return v, err @@ -117,12 +182,29 @@ func embedFilesValue(v reflect.Value, embedStyle FileEmbedStyle) (reflect.Value, // The "@file://" prefix is for files that you explicitly want to // upload as a string literal with backslash escapes (not base64 // encoded) + if isStdinPath(filename) { + content, err := stdin.readAll() + if err != nil { + return v, err + } + return reflect.ValueOf(string(content)), nil + } content, err := os.ReadFile(filename) if err != nil { return v, err } return reflect.ValueOf(string(content)), nil } else if filename, ok := strings.CutPrefix(s, "@"); ok { + if isStdinPath(filename) { + content, err := stdin.readAll() + if err != nil { + return v, err + } + if isUTF8TextFile(content) { + return reflect.ValueOf(string(content)), nil + } + return reflect.ValueOf(base64.StdEncoding.EncodeToString(content)), nil + } content, err := os.ReadFile(filename) if err != nil { // If the string is "@username", it's probably supposed to be a @@ -160,6 +242,14 @@ func embedFilesValue(v reflect.Value, embedStyle FileEmbedStyle) (reflect.Value, expectsFile = strings.Contains(filename, ".") || strings.Contains(filename, "/") } + if isStdinPath(filename) { + r, err := stdin.read() + if err != nil { + return v, err + } + return reflect.ValueOf(io.NopCloser(r)), nil + } + file, err := os.Open(filename) if err != nil { if !expectsFile { @@ -219,6 +309,7 @@ func flagOptions( requestContents := requestflag.ExtractRequestContents(cmd) + stdinConsumedByPipe := false if (bodyType == MultipartFormEncoded || bodyType == ApplicationJSON) && !ignoreStdin && isInputPiped() { pipeData, err := io.ReadAll(os.Stdin) if err != nil { @@ -226,6 +317,7 @@ func flagOptions( } if len(pipeData) > 0 { + stdinConsumedByPipe = true var bodyData any if err := yaml.Unmarshal(pipeData, &bodyData); err != nil { return nil, fmt.Errorf("Failed to parse piped data as YAML/JSON:\n%w", err) @@ -258,24 +350,40 @@ func flagOptions( } } + // For flags marked as FileInput (type: string, format: binary), the value is always + // a file path. Wrap with FilePathValue so embedFiles reads the file automatically + // without requiring the user to type the "@" prefix. This handles both values set + // via explicit CLI flags and values that arrived via piped YAML/JSON data. + wrapFileInputValues(cmd, &requestContents) + + // Determine stdin availability for FileInput params that use "-". + var stdinReader onceStdinReader + if ignoreStdin { + stdinReader = onceStdinReader{failureReason: "stdin is already being used for the request body"} + } else if stdinConsumedByPipe { + stdinReader = onceStdinReader{failureReason: "stdin was already consumed by piped YAML/JSON input"} + } else { + stdinReader = onceStdinReader{stdinReader: os.Stdin} + } + // Embed files passed as "@file.jpg" in the request body, headers, and query: embedStyle := EmbedText if bodyType == ApplicationOctetStream || bodyType == MultipartFormEncoded { embedStyle = EmbedIOReader } - if embedded, err := embedFiles(requestContents.Body, embedStyle); err != nil { + if embedded, err := embedFiles(requestContents.Body, embedStyle, &stdinReader); err != nil { return nil, err } else { requestContents.Body = embedded } - if headersWithFiles, err := embedFiles(requestContents.Headers, EmbedText); err != nil { + if headersWithFiles, err := embedFiles(requestContents.Headers, EmbedText, &stdinReader); err != nil { return nil, err } else { requestContents.Headers = headersWithFiles.(map[string]any) } - if queriesWithFiles, err := embedFiles(requestContents.Queries, EmbedText); err != nil { + if queriesWithFiles, err := embedFiles(requestContents.Queries, EmbedText, &stdinReader); err != nil { return nil, err } else { requestContents.Queries = queriesWithFiles.(map[string]any) @@ -371,3 +479,78 @@ func flagOptions( return options, nil } + +// FilePathValue is a string wrapper that marks a value as a file path whose contents should be read +// and embedded in the request. Unlike a regular string, embedFilesValue always treats a FilePathValue +// as a file path without needing the "@" prefix. +type FilePathValue string + +// wrapFileInputValues replaces string values for FileInput flags (type: string, format: binary) with +// FilePathValue sentinel values. embedFilesValue recognizes FilePathValue and reads the file contents +// directly, so the user doesn't need to type the "@" prefix. This handles both values set via explicit +// CLI flags and values that arrived via piped YAML/JSON data. +func wrapFileInputValues(cmd *cli.Command, contents *requestflag.RequestContents) { + bodyMap, _ := contents.Body.(map[string]any) + + for _, flag := range cmd.Flags { + inReq, ok := flag.(requestflag.InRequest) + if !ok || !inReq.IsFileInput() || inReq.IsBodyRoot() { + continue + } + + // Wrap values set via explicit CLI flags. + if flag.IsSet() { + if wrapped, changed := wrapFileInputValue(flag.Get()); changed { + if bodyPath := inReq.GetBodyPath(); bodyPath != "" { + if bodyMap != nil { + bodyMap[bodyPath] = wrapped + } + } else if queryPath := inReq.GetQueryPath(); queryPath != "" { + contents.Queries[queryPath] = wrapped + } else if headerPath := inReq.GetHeaderPath(); headerPath != "" { + contents.Headers[headerPath] = wrapped + } + } + } + + // Wrap values that arrived via piped YAML/JSON data in the body map. + if bodyPath := inReq.GetBodyPath(); bodyPath != "" && bodyMap != nil { + if value, exists := bodyMap[bodyPath]; exists { + if wrapped, changed := wrapFileInputValue(value); changed { + bodyMap[bodyPath] = wrapped + } + } + } + } +} + +func wrapFileInputValue(value any) (any, bool) { + switch v := value.(type) { + case string: + if v == "" { + return value, false + } + return FilePathValue(v), true + + case []string: + result := make([]any, len(v)) + for i, s := range v { + result[i] = FilePathValue(s) + } + return result, true + + case []any: + result := make([]any, len(v)) + for i, elem := range v { + if s, ok := elem.(string); ok { + result[i] = FilePathValue(s) + } else { + result[i] = elem + } + } + return result, true + + default: + return value, false + } +} diff --git a/pkg/cmd/flagoptions_test.go b/pkg/cmd/flagoptions_test.go index e5dad4b..039b9ff 100644 --- a/pkg/cmd/flagoptions_test.go +++ b/pkg/cmd/flagoptions_test.go @@ -2,8 +2,10 @@ package cmd import ( "encoding/base64" + "io" "os" "path/filepath" + "strings" "testing" "github.com/stretchr/testify/assert" @@ -11,6 +13,8 @@ import ( ) func TestIsUTF8TextFile(t *testing.T) { + t.Parallel() + tests := []struct { content []byte expected bool @@ -32,6 +36,8 @@ func TestIsUTF8TextFile(t *testing.T) { } func TestEmbedFiles(t *testing.T) { + t.Parallel() + // Create temporary directory for test files tmpDir := t.TempDir() @@ -216,7 +222,9 @@ func TestEmbedFiles(t *testing.T) { for _, tt := range tests { t.Run(tt.name+" text", func(t *testing.T) { - got, err := embedFiles(tt.input, EmbedText) + t.Parallel() + + got, err := embedFiles(tt.input, EmbedText, nil) if tt.wantErr { assert.Error(t, err) } else { @@ -226,7 +234,9 @@ func TestEmbedFiles(t *testing.T) { }) t.Run(tt.name+" io.Reader", func(t *testing.T) { - _, err := embedFiles(tt.input, EmbedIOReader) + t.Parallel() + + _, err := embedFiles(tt.input, EmbedIOReader, nil) if tt.wantErr { assert.Error(t, err) } else { @@ -236,9 +246,98 @@ func TestEmbedFiles(t *testing.T) { } } +func TestEmbedFilesStdin(t *testing.T) { + t.Parallel() + + t.Run("FilePathValueDash", func(t *testing.T) { + t.Parallel() + + stdin := &onceStdinReader{stdinReader: strings.NewReader("stdin content")} + + withEmbedded, err := embedFiles(map[string]any{"file": FilePathValue("-")}, EmbedText, stdin) + require.NoError(t, err) + require.Equal(t, map[string]any{"file": "stdin content"}, withEmbedded) + }) + + t.Run("FilePathValueDevStdin", func(t *testing.T) { + t.Parallel() + + stdin := &onceStdinReader{stdinReader: strings.NewReader("stdin content")} + + withEmbedded, err := embedFiles(map[string]any{"file": FilePathValue("/dev/stdin")}, EmbedText, stdin) + require.NoError(t, err) + require.Equal(t, map[string]any{"file": "stdin content"}, withEmbedded) + }) + + t.Run("MultipleFilePathValueDashesError", func(t *testing.T) { + t.Parallel() + + stdin := &onceStdinReader{stdinReader: strings.NewReader("stdin content")} + + _, err := embedFiles(map[string]any{ + "file1": FilePathValue("-"), + "file2": FilePathValue("-"), + }, EmbedText, stdin) + require.Error(t, err) + require.Contains(t, err.Error(), "already been read") + }) + + t.Run("FilePathValueDashUnavailableStdin", func(t *testing.T) { + t.Parallel() + + stdin := &onceStdinReader{failureReason: "stdin is already being used for the request body"} + + _, err := embedFiles(map[string]any{"file": FilePathValue("-")}, EmbedText, stdin) + require.Error(t, err) + require.Contains(t, err.Error(), "cannot read from stdin") + require.Contains(t, err.Error(), "request body") + }) + + t.Run("AtDashEmbedText", func(t *testing.T) { + t.Parallel() + + stdin := &onceStdinReader{stdinReader: strings.NewReader("piped content")} + + withEmbedded, err := embedFiles(map[string]any{"data": "@-"}, EmbedText, stdin) + require.NoError(t, err) + require.Equal(t, map[string]any{"data": "piped content"}, withEmbedded) + }) + + t.Run("AtDashEmbedIOReader", func(t *testing.T) { + t.Parallel() + + stdin := &onceStdinReader{stdinReader: strings.NewReader("piped content")} + + withEmbedded, err := embedFiles(map[string]any{"data": "@-"}, EmbedIOReader, stdin) + require.NoError(t, err) + + withEmbeddedMap := withEmbedded.(map[string]any) + r := withEmbeddedMap["data"].(io.ReadCloser) + + content, err := io.ReadAll(r) + require.NoError(t, err) + require.Equal(t, "piped content", string(content)) + }) + + t.Run("FilePathValueRealFile", func(t *testing.T) { + t.Parallel() + + tmpDir := t.TempDir() + writeTestFile(t, tmpDir, "test.txt", "file content") + + stdin := &onceStdinReader{stdinReader: strings.NewReader("unused stdin")} + + withEmbedded, err := embedFiles(map[string]any{"file": FilePathValue(filepath.Join(tmpDir, "test.txt"))}, EmbedText, stdin) + require.NoError(t, err) + require.Equal(t, map[string]any{"file": "file content"}, withEmbedded) + }) +} + func writeTestFile(t *testing.T, dir, filename, content string) { t.Helper() + path := filepath.Join(dir, filename) + err := os.WriteFile(path, []byte(content), 0644) require.NoError(t, err, "failed to write test file %s", path) } diff --git a/pkg/cmd/inbox.go b/pkg/cmd/inbox.go index 40ac88d..b9473ca 100644 --- a/pkg/cmd/inbox.go +++ b/pkg/cmd/inbox.go @@ -17,7 +17,7 @@ import ( var inboxesCreate = cli.Command{ Name: "create", - Usage: "Create Inbox", + Usage: "**CLI:**", Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[any]{ @@ -47,7 +47,7 @@ var inboxesCreate = cli.Command{ var inboxesRetrieve = cli.Command{ Name: "retrieve", - Usage: "Get Inbox", + Usage: "**CLI:**", Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ @@ -62,7 +62,7 @@ var inboxesRetrieve = cli.Command{ var inboxesUpdate = cli.Command{ Name: "update", - Usage: "Update Inbox", + Usage: "**CLI:**", Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ @@ -83,7 +83,7 @@ var inboxesUpdate = cli.Command{ var inboxesList = cli.Command{ Name: "list", - Usage: "List Inboxes", + Usage: "**CLI:**", Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[any]{ @@ -108,7 +108,7 @@ var inboxesList = cli.Command{ var inboxesDelete = cli.Command{ Name: "delete", - Usage: "Delete Inbox", + Usage: "**CLI:**", Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ @@ -123,7 +123,7 @@ var inboxesDelete = cli.Command{ var inboxesListMetrics = cli.Command{ Name: "list-metrics", - Usage: "Query Metrics", + Usage: "**CLI:**", Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ diff --git a/pkg/cmd/inboxapikey.go b/pkg/cmd/inboxapikey.go new file mode 100644 index 0000000..4005e3e --- /dev/null +++ b/pkg/cmd/inboxapikey.go @@ -0,0 +1,382 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "context" + "fmt" + "os" + + "github.com/agentmail-to/agentmail-cli/internal/apiquery" + "github.com/agentmail-to/agentmail-cli/internal/requestflag" + "github.com/agentmail-to/agentmail-go" + "github.com/agentmail-to/agentmail-go/option" + "github.com/tidwall/gjson" + "github.com/urfave/cli/v3" +) + +var inboxesAPIKeysCreate = requestflag.WithInnerFlags(cli.Command{ + Name: "create", + Usage: "**CLI:**", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "inbox-id", + Usage: "The ID of the inbox.", + Required: true, + }, + &requestflag.Flag[any]{ + Name: "name", + Usage: "Name of api key.", + BodyPath: "name", + }, + &requestflag.Flag[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", + }, + }, + Action: handleInboxesAPIKeysCreate, + HideHelpCommand: true, +}, map[string][]requestflag.HasOuterFlag{ + "permissions": { + &requestflag.InnerFlag[any]{ + Name: "permissions.api-key-create", + Usage: "Create API keys.", + InnerField: "api_key_create", + }, + &requestflag.InnerFlag[any]{ + Name: "permissions.api-key-delete", + Usage: "Delete API keys.", + InnerField: "api_key_delete", + }, + &requestflag.InnerFlag[any]{ + Name: "permissions.api-key-read", + Usage: "Read API keys.", + InnerField: "api_key_read", + }, + &requestflag.InnerFlag[any]{ + Name: "permissions.domain-create", + Usage: "Create domains.", + InnerField: "domain_create", + }, + &requestflag.InnerFlag[any]{ + Name: "permissions.domain-delete", + Usage: "Delete domains.", + InnerField: "domain_delete", + }, + &requestflag.InnerFlag[any]{ + Name: "permissions.domain-read", + Usage: "Read domain details.", + InnerField: "domain_read", + }, + &requestflag.InnerFlag[any]{ + Name: "permissions.domain-update", + Usage: "Update domains.", + InnerField: "domain_update", + }, + &requestflag.InnerFlag[any]{ + Name: "permissions.draft-create", + Usage: "Create drafts.", + InnerField: "draft_create", + }, + &requestflag.InnerFlag[any]{ + Name: "permissions.draft-delete", + Usage: "Delete drafts.", + InnerField: "draft_delete", + }, + &requestflag.InnerFlag[any]{ + Name: "permissions.draft-read", + Usage: "Read drafts.", + InnerField: "draft_read", + }, + &requestflag.InnerFlag[any]{ + Name: "permissions.draft-send", + Usage: "Send drafts.", + InnerField: "draft_send", + }, + &requestflag.InnerFlag[any]{ + Name: "permissions.draft-update", + Usage: "Update drafts.", + InnerField: "draft_update", + }, + &requestflag.InnerFlag[any]{ + Name: "permissions.inbox-create", + Usage: "Create new inboxes.", + InnerField: "inbox_create", + }, + &requestflag.InnerFlag[any]{ + Name: "permissions.inbox-delete", + Usage: "Delete inboxes.", + InnerField: "inbox_delete", + }, + &requestflag.InnerFlag[any]{ + Name: "permissions.inbox-read", + Usage: "Read inbox details.", + InnerField: "inbox_read", + }, + &requestflag.InnerFlag[any]{ + Name: "permissions.inbox-update", + Usage: "Update inbox settings.", + InnerField: "inbox_update", + }, + &requestflag.InnerFlag[any]{ + Name: "permissions.label-blocked-read", + Usage: "Access messages labeled blocked.", + InnerField: "label_blocked_read", + }, + &requestflag.InnerFlag[any]{ + Name: "permissions.label-spam-read", + Usage: "Access messages labeled spam.", + InnerField: "label_spam_read", + }, + &requestflag.InnerFlag[any]{ + Name: "permissions.label-trash-read", + Usage: "Access messages labeled trash.", + InnerField: "label_trash_read", + }, + &requestflag.InnerFlag[any]{ + Name: "permissions.list-entry-create", + Usage: "Create list entries.", + InnerField: "list_entry_create", + }, + &requestflag.InnerFlag[any]{ + Name: "permissions.list-entry-delete", + Usage: "Delete list entries.", + InnerField: "list_entry_delete", + }, + &requestflag.InnerFlag[any]{ + Name: "permissions.list-entry-read", + Usage: "Read list entries.", + InnerField: "list_entry_read", + }, + &requestflag.InnerFlag[any]{ + Name: "permissions.message-read", + Usage: "Read messages.", + InnerField: "message_read", + }, + &requestflag.InnerFlag[any]{ + Name: "permissions.message-send", + Usage: "Send messages.", + InnerField: "message_send", + }, + &requestflag.InnerFlag[any]{ + Name: "permissions.message-update", + Usage: "Update message labels.", + InnerField: "message_update", + }, + &requestflag.InnerFlag[any]{ + Name: "permissions.metrics-read", + Usage: "Read metrics.", + InnerField: "metrics_read", + }, + &requestflag.InnerFlag[any]{ + Name: "permissions.pod-create", + Usage: "Create pods.", + InnerField: "pod_create", + }, + &requestflag.InnerFlag[any]{ + Name: "permissions.pod-delete", + Usage: "Delete pods.", + InnerField: "pod_delete", + }, + &requestflag.InnerFlag[any]{ + Name: "permissions.pod-read", + Usage: "Read pods.", + InnerField: "pod_read", + }, + &requestflag.InnerFlag[any]{ + Name: "permissions.thread-delete", + Usage: "Delete threads.", + InnerField: "thread_delete", + }, + &requestflag.InnerFlag[any]{ + Name: "permissions.thread-read", + Usage: "Read threads.", + InnerField: "thread_read", + }, + &requestflag.InnerFlag[any]{ + Name: "permissions.webhook-create", + Usage: "Create webhooks.", + InnerField: "webhook_create", + }, + &requestflag.InnerFlag[any]{ + Name: "permissions.webhook-delete", + Usage: "Delete webhooks.", + InnerField: "webhook_delete", + }, + &requestflag.InnerFlag[any]{ + Name: "permissions.webhook-read", + Usage: "Read webhook configurations.", + InnerField: "webhook_read", + }, + &requestflag.InnerFlag[any]{ + Name: "permissions.webhook-update", + Usage: "Update webhooks.", + InnerField: "webhook_update", + }, + }, +}) + +var inboxesAPIKeysList = cli.Command{ + Name: "list", + Usage: "**CLI:**", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "inbox-id", + Usage: "The ID of the inbox.", + Required: true, + }, + &requestflag.Flag[any]{ + Name: "limit", + Usage: "Limit of number of items returned.", + QueryPath: "limit", + }, + &requestflag.Flag[any]{ + Name: "page-token", + Usage: "Page token for pagination.", + QueryPath: "page_token", + }, + }, + Action: handleInboxesAPIKeysList, + HideHelpCommand: true, +} + +var inboxesAPIKeysDelete = cli.Command{ + Name: "delete", + Usage: "**CLI:**", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "inbox-id", + Usage: "The ID of the inbox.", + Required: true, + }, + &requestflag.Flag[string]{ + Name: "api-key-id", + Usage: "ID of api key.", + Required: true, + }, + }, + Action: handleInboxesAPIKeysDelete, + HideHelpCommand: true, +} + +func handleInboxesAPIKeysCreate(ctx context.Context, cmd *cli.Command) error { + client := agentmail.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("inbox-id") && len(unusedArgs) > 0 { + cmd.Set("inbox-id", unusedArgs[0]) + unusedArgs = unusedArgs[1:] + } + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + + params := agentmail.InboxAPIKeyNewParams{} + + options, err := flagOptions( + cmd, + apiquery.NestedQueryFormatBrackets, + apiquery.ArrayQueryFormatComma, + ApplicationJSON, + false, + ) + if err != nil { + return err + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Inboxes.APIKeys.New( + ctx, + cmd.Value("inbox-id").(string), + params, + options..., + ) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + transform := cmd.Root().String("transform") + return ShowJSON(os.Stdout, "inboxes:api-keys create", obj, format, transform) +} + +func handleInboxesAPIKeysList(ctx context.Context, cmd *cli.Command) error { + client := agentmail.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("inbox-id") && len(unusedArgs) > 0 { + cmd.Set("inbox-id", unusedArgs[0]) + unusedArgs = unusedArgs[1:] + } + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + + params := agentmail.InboxAPIKeyListParams{} + + options, err := flagOptions( + cmd, + apiquery.NestedQueryFormatBrackets, + apiquery.ArrayQueryFormatComma, + EmptyBody, + false, + ) + if err != nil { + return err + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Inboxes.APIKeys.List( + ctx, + cmd.Value("inbox-id").(string), + params, + options..., + ) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + transform := cmd.Root().String("transform") + return ShowJSON(os.Stdout, "inboxes:api-keys list", obj, format, transform) +} + +func handleInboxesAPIKeysDelete(ctx context.Context, cmd *cli.Command) error { + client := agentmail.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("api-key-id") && len(unusedArgs) > 0 { + cmd.Set("api-key-id", unusedArgs[0]) + unusedArgs = unusedArgs[1:] + } + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + + params := agentmail.InboxAPIKeyDeleteParams{ + InboxID: cmd.Value("inbox-id").(string), + } + + options, err := flagOptions( + cmd, + apiquery.NestedQueryFormatBrackets, + apiquery.ArrayQueryFormatComma, + EmptyBody, + false, + ) + if err != nil { + return err + } + + return client.Inboxes.APIKeys.Delete( + ctx, + cmd.Value("api-key-id").(string), + params, + options..., + ) +} diff --git a/pkg/cmd/inboxapikey_test.go b/pkg/cmd/inboxapikey_test.go new file mode 100644 index 0000000..363128d --- /dev/null +++ b/pkg/cmd/inboxapikey_test.go @@ -0,0 +1,148 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "testing" + + "github.com/agentmail-to/agentmail-cli/internal/mocktest" + "github.com/agentmail-to/agentmail-cli/internal/requestflag" +) + +func TestInboxesAPIKeysCreate(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "inboxes:api-keys", "create", + "--inbox-id", "inbox_id", + "--name", "name", + "--permissions", "{api_key_create: true, api_key_delete: true, api_key_read: true, domain_create: true, domain_delete: true, domain_read: true, domain_update: true, draft_create: true, draft_delete: true, draft_read: true, draft_send: true, draft_update: true, inbox_create: true, inbox_delete: true, inbox_read: true, inbox_update: true, label_blocked_read: true, label_spam_read: true, label_trash_read: true, list_entry_create: true, list_entry_delete: true, list_entry_read: true, message_read: true, message_send: true, message_update: true, metrics_read: true, pod_create: true, pod_delete: true, pod_read: true, thread_delete: true, thread_read: true, webhook_create: true, webhook_delete: true, webhook_read: true, webhook_update: true}", + ) + }) + + t.Run("inner flags", func(t *testing.T) { + // Check that inner flags have been set up correctly + requestflag.CheckInnerFlags(inboxesAPIKeysCreate) + + // Alternative argument passing style using inner flags + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "inboxes:api-keys", "create", + "--inbox-id", "inbox_id", + "--name", "name", + "--permissions.api-key-create=true", + "--permissions.api-key-delete=true", + "--permissions.api-key-read=true", + "--permissions.domain-create=true", + "--permissions.domain-delete=true", + "--permissions.domain-read=true", + "--permissions.domain-update=true", + "--permissions.draft-create=true", + "--permissions.draft-delete=true", + "--permissions.draft-read=true", + "--permissions.draft-send=true", + "--permissions.draft-update=true", + "--permissions.inbox-create=true", + "--permissions.inbox-delete=true", + "--permissions.inbox-read=true", + "--permissions.inbox-update=true", + "--permissions.label-blocked-read=true", + "--permissions.label-spam-read=true", + "--permissions.label-trash-read=true", + "--permissions.list-entry-create=true", + "--permissions.list-entry-delete=true", + "--permissions.list-entry-read=true", + "--permissions.message-read=true", + "--permissions.message-send=true", + "--permissions.message-update=true", + "--permissions.metrics-read=true", + "--permissions.pod-create=true", + "--permissions.pod-delete=true", + "--permissions.pod-read=true", + "--permissions.thread-delete=true", + "--permissions.thread-read=true", + "--permissions.webhook-create=true", + "--permissions.webhook-delete=true", + "--permissions.webhook-read=true", + "--permissions.webhook-update=true", + ) + }) + + t.Run("piping data", func(t *testing.T) { + // Test piping YAML data over stdin + pipeData := []byte("" + + "name: name\n" + + "permissions:\n" + + " api_key_create: true\n" + + " api_key_delete: true\n" + + " api_key_read: true\n" + + " domain_create: true\n" + + " domain_delete: true\n" + + " domain_read: true\n" + + " domain_update: true\n" + + " draft_create: true\n" + + " draft_delete: true\n" + + " draft_read: true\n" + + " draft_send: true\n" + + " draft_update: true\n" + + " inbox_create: true\n" + + " inbox_delete: true\n" + + " inbox_read: true\n" + + " inbox_update: true\n" + + " label_blocked_read: true\n" + + " label_spam_read: true\n" + + " label_trash_read: true\n" + + " list_entry_create: true\n" + + " list_entry_delete: true\n" + + " list_entry_read: true\n" + + " message_read: true\n" + + " message_send: true\n" + + " message_update: true\n" + + " metrics_read: true\n" + + " pod_create: true\n" + + " pod_delete: true\n" + + " pod_read: true\n" + + " thread_delete: true\n" + + " thread_read: true\n" + + " webhook_create: true\n" + + " webhook_delete: true\n" + + " webhook_read: true\n" + + " webhook_update: true\n") + mocktest.TestRunMockTestWithPipeAndFlags( + t, pipeData, + "--api-key", "string", + "inboxes:api-keys", "create", + "--inbox-id", "inbox_id", + ) + }) +} + +func TestInboxesAPIKeysList(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "inboxes:api-keys", "list", + "--inbox-id", "inbox_id", + "--limit", "0", + "--page-token", "page_token", + ) + }) +} + +func TestInboxesAPIKeysDelete(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "inboxes:api-keys", "delete", + "--inbox-id", "inbox_id", + "--api-key-id", "api_key_id", + ) + }) +} diff --git a/pkg/cmd/inboxdraft.go b/pkg/cmd/inboxdraft.go index 02b97ad..c0f607c 100644 --- a/pkg/cmd/inboxdraft.go +++ b/pkg/cmd/inboxdraft.go @@ -17,7 +17,7 @@ import ( var inboxesDraftsCreate = requestflag.WithInnerFlags(cli.Command{ Name: "create", - Usage: "Create Draft", + Usage: "**CLI:**", Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ @@ -125,7 +125,7 @@ var inboxesDraftsCreate = requestflag.WithInnerFlags(cli.Command{ var inboxesDraftsRetrieve = cli.Command{ Name: "retrieve", - Usage: "Get Draft", + Usage: "**CLI:**", Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ @@ -145,7 +145,7 @@ var inboxesDraftsRetrieve = cli.Command{ var inboxesDraftsUpdate = cli.Command{ Name: "update", - Usage: "Update Draft", + Usage: "**CLI:**", Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ @@ -205,7 +205,7 @@ var inboxesDraftsUpdate = cli.Command{ var inboxesDraftsList = cli.Command{ Name: "list", - Usage: "List Drafts", + Usage: "**CLI:**", Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ @@ -250,7 +250,7 @@ var inboxesDraftsList = cli.Command{ var inboxesDraftsDelete = cli.Command{ Name: "delete", - Usage: "Delete Draft", + Usage: "**CLI:**", Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ @@ -268,9 +268,34 @@ var inboxesDraftsDelete = cli.Command{ HideHelpCommand: true, } +var inboxesDraftsGetAttachment = cli.Command{ + Name: "get-attachment", + Usage: "**CLI:**", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "inbox-id", + Usage: "The ID of the inbox.", + Required: true, + }, + &requestflag.Flag[string]{ + Name: "draft-id", + Usage: "ID of draft.", + Required: true, + }, + &requestflag.Flag[string]{ + Name: "attachment-id", + Usage: "ID of attachment.", + Required: true, + }, + }, + Action: handleInboxesDraftsGetAttachment, + HideHelpCommand: true, +} + var inboxesDraftsSend = cli.Command{ Name: "send", - Usage: "Send Draft", + Usage: "**CLI:**", Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ @@ -504,6 +529,51 @@ func handleInboxesDraftsDelete(ctx context.Context, cmd *cli.Command) error { ) } +func handleInboxesDraftsGetAttachment(ctx context.Context, cmd *cli.Command) error { + client := agentmail.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("attachment-id") && len(unusedArgs) > 0 { + cmd.Set("attachment-id", unusedArgs[0]) + unusedArgs = unusedArgs[1:] + } + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + + params := agentmail.InboxDraftGetAttachmentParams{ + InboxID: cmd.Value("inbox-id").(string), + DraftID: cmd.Value("draft-id").(string), + } + + options, err := flagOptions( + cmd, + apiquery.NestedQueryFormatBrackets, + apiquery.ArrayQueryFormatComma, + EmptyBody, + false, + ) + if err != nil { + return err + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Inboxes.Drafts.GetAttachment( + ctx, + cmd.Value("attachment-id").(string), + params, + options..., + ) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + transform := cmd.Root().String("transform") + return ShowJSON(os.Stdout, "inboxes:drafts get-attachment", obj, format, transform) +} + func handleInboxesDraftsSend(ctx context.Context, cmd *cli.Command) error { client := agentmail.NewClient(getDefaultRequestOptions(cmd)...) unusedArgs := cmd.Args().Slice() diff --git a/pkg/cmd/inboxdraft_test.go b/pkg/cmd/inboxdraft_test.go index 44eb258..2254fab 100644 --- a/pkg/cmd/inboxdraft_test.go +++ b/pkg/cmd/inboxdraft_test.go @@ -186,6 +186,20 @@ func TestInboxesDraftsDelete(t *testing.T) { }) } +func TestInboxesDraftsGetAttachment(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "inboxes:drafts", "get-attachment", + "--inbox-id", "inbox_id", + "--draft-id", "draft_id", + "--attachment-id", "attachment_id", + ) + }) +} + func TestInboxesDraftsSend(t *testing.T) { t.Skip("Mock server tests are disabled") t.Run("regular flags", func(t *testing.T) { diff --git a/pkg/cmd/inboxlist.go b/pkg/cmd/inboxlist.go new file mode 100644 index 0000000..95acde8 --- /dev/null +++ b/pkg/cmd/inboxlist.go @@ -0,0 +1,317 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "context" + "fmt" + "os" + + "github.com/agentmail-to/agentmail-cli/internal/apiquery" + "github.com/agentmail-to/agentmail-cli/internal/requestflag" + "github.com/agentmail-to/agentmail-go" + "github.com/agentmail-to/agentmail-go/option" + "github.com/tidwall/gjson" + "github.com/urfave/cli/v3" +) + +var inboxesListsCreate = cli.Command{ + Name: "create", + Usage: "**CLI:**", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "inbox-id", + Usage: "The ID of the inbox.", + Required: true, + }, + &requestflag.Flag[string]{ + Name: "direction", + Usage: "Direction of list entry.", + Required: true, + }, + &requestflag.Flag[string]{ + Name: "type", + Usage: "Type of list entry.", + Required: true, + }, + &requestflag.Flag[string]{ + Name: "entry", + Usage: "Email address or domain to add.", + Required: true, + BodyPath: "entry", + }, + &requestflag.Flag[any]{ + Name: "reason", + Usage: "Reason for adding the entry.", + BodyPath: "reason", + }, + }, + Action: handleInboxesListsCreate, + HideHelpCommand: true, +} + +var inboxesListsRetrieve = cli.Command{ + Name: "retrieve", + Usage: "**CLI:**", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "inbox-id", + Usage: "The ID of the inbox.", + Required: true, + }, + &requestflag.Flag[string]{ + Name: "direction", + Usage: "Direction of list entry.", + Required: true, + }, + &requestflag.Flag[string]{ + Name: "type", + Usage: "Type of list entry.", + Required: true, + }, + &requestflag.Flag[string]{ + Name: "entry", + Required: true, + }, + }, + Action: handleInboxesListsRetrieve, + HideHelpCommand: true, +} + +var inboxesListsList = cli.Command{ + Name: "list", + Usage: "**CLI:**", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "inbox-id", + Usage: "The ID of the inbox.", + Required: true, + }, + &requestflag.Flag[string]{ + Name: "direction", + Usage: "Direction of list entry.", + Required: true, + }, + &requestflag.Flag[string]{ + Name: "type", + Usage: "Type of list entry.", + Required: true, + }, + &requestflag.Flag[any]{ + Name: "limit", + Usage: "Limit of number of items returned.", + QueryPath: "limit", + }, + &requestflag.Flag[any]{ + Name: "page-token", + Usage: "Page token for pagination.", + QueryPath: "page_token", + }, + }, + Action: handleInboxesListsList, + HideHelpCommand: true, +} + +var inboxesListsDelete = cli.Command{ + Name: "delete", + Usage: "**CLI:**", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "inbox-id", + Usage: "The ID of the inbox.", + Required: true, + }, + &requestflag.Flag[string]{ + Name: "direction", + Usage: "Direction of list entry.", + Required: true, + }, + &requestflag.Flag[string]{ + Name: "type", + Usage: "Type of list entry.", + Required: true, + }, + &requestflag.Flag[string]{ + Name: "entry", + Required: true, + }, + }, + Action: handleInboxesListsDelete, + HideHelpCommand: true, +} + +func handleInboxesListsCreate(ctx context.Context, cmd *cli.Command) error { + client := agentmail.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("type") && len(unusedArgs) > 0 { + cmd.Set("type", unusedArgs[0]) + unusedArgs = unusedArgs[1:] + } + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + + params := agentmail.InboxListNewParams{ + InboxID: cmd.Value("inbox-id").(string), + Direction: agentmail.InboxListNewParamsDirection(cmd.Value("direction").(string)), + } + + options, err := flagOptions( + cmd, + apiquery.NestedQueryFormatBrackets, + apiquery.ArrayQueryFormatComma, + ApplicationJSON, + false, + ) + if err != nil { + return err + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Inboxes.Lists.New( + ctx, + agentmail.InboxListNewParamsType(cmd.Value("type").(string)), + params, + options..., + ) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + transform := cmd.Root().String("transform") + return ShowJSON(os.Stdout, "inboxes:lists create", obj, format, transform) +} + +func handleInboxesListsRetrieve(ctx context.Context, cmd *cli.Command) error { + client := agentmail.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("entry") && len(unusedArgs) > 0 { + cmd.Set("entry", unusedArgs[0]) + unusedArgs = unusedArgs[1:] + } + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + + params := agentmail.InboxListGetParams{ + InboxID: cmd.Value("inbox-id").(string), + Direction: agentmail.InboxListGetParamsDirection(cmd.Value("direction").(string)), + Type: agentmail.InboxListGetParamsType(cmd.Value("type").(string)), + } + + options, err := flagOptions( + cmd, + apiquery.NestedQueryFormatBrackets, + apiquery.ArrayQueryFormatComma, + EmptyBody, + false, + ) + if err != nil { + return err + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Inboxes.Lists.Get( + ctx, + cmd.Value("entry").(string), + params, + options..., + ) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + transform := cmd.Root().String("transform") + return ShowJSON(os.Stdout, "inboxes:lists retrieve", obj, format, transform) +} + +func handleInboxesListsList(ctx context.Context, cmd *cli.Command) error { + client := agentmail.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("type") && len(unusedArgs) > 0 { + cmd.Set("type", unusedArgs[0]) + unusedArgs = unusedArgs[1:] + } + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + + params := agentmail.InboxListListParams{ + InboxID: cmd.Value("inbox-id").(string), + Direction: agentmail.InboxListListParamsDirection(cmd.Value("direction").(string)), + } + + options, err := flagOptions( + cmd, + apiquery.NestedQueryFormatBrackets, + apiquery.ArrayQueryFormatComma, + EmptyBody, + false, + ) + if err != nil { + return err + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Inboxes.Lists.List( + ctx, + agentmail.InboxListListParamsType(cmd.Value("type").(string)), + params, + options..., + ) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + transform := cmd.Root().String("transform") + return ShowJSON(os.Stdout, "inboxes:lists list", obj, format, transform) +} + +func handleInboxesListsDelete(ctx context.Context, cmd *cli.Command) error { + client := agentmail.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("entry") && len(unusedArgs) > 0 { + cmd.Set("entry", unusedArgs[0]) + unusedArgs = unusedArgs[1:] + } + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + + params := agentmail.InboxListDeleteParams{ + InboxID: cmd.Value("inbox-id").(string), + Direction: agentmail.InboxListDeleteParamsDirection(cmd.Value("direction").(string)), + Type: agentmail.InboxListDeleteParamsType(cmd.Value("type").(string)), + } + + options, err := flagOptions( + cmd, + apiquery.NestedQueryFormatBrackets, + apiquery.ArrayQueryFormatComma, + EmptyBody, + false, + ) + if err != nil { + return err + } + + return client.Inboxes.Lists.Delete( + ctx, + cmd.Value("entry").(string), + params, + options..., + ) +} diff --git a/pkg/cmd/inboxlist_test.go b/pkg/cmd/inboxlist_test.go new file mode 100644 index 0000000..abd8333 --- /dev/null +++ b/pkg/cmd/inboxlist_test.go @@ -0,0 +1,86 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "testing" + + "github.com/agentmail-to/agentmail-cli/internal/mocktest" +) + +func TestInboxesListsCreate(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "inboxes:lists", "create", + "--inbox-id", "inbox_id", + "--direction", "send", + "--type", "allow", + "--entry", "entry", + "--reason", "reason", + ) + }) + + t.Run("piping data", func(t *testing.T) { + // Test piping YAML data over stdin + pipeData := []byte("" + + "entry: entry\n" + + "reason: reason\n") + mocktest.TestRunMockTestWithPipeAndFlags( + t, pipeData, + "--api-key", "string", + "inboxes:lists", "create", + "--inbox-id", "inbox_id", + "--direction", "send", + "--type", "allow", + ) + }) +} + +func TestInboxesListsRetrieve(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "inboxes:lists", "retrieve", + "--inbox-id", "inbox_id", + "--direction", "send", + "--type", "allow", + "--entry", "entry", + ) + }) +} + +func TestInboxesListsList(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "inboxes:lists", "list", + "--inbox-id", "inbox_id", + "--direction", "send", + "--type", "allow", + "--limit", "0", + "--page-token", "page_token", + ) + }) +} + +func TestInboxesListsDelete(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "inboxes:lists", "delete", + "--inbox-id", "inbox_id", + "--direction", "send", + "--type", "allow", + "--entry", "entry", + ) + }) +} diff --git a/pkg/cmd/inboxmessage.go b/pkg/cmd/inboxmessage.go index 076031f..5af3470 100644 --- a/pkg/cmd/inboxmessage.go +++ b/pkg/cmd/inboxmessage.go @@ -17,7 +17,7 @@ import ( var inboxesMessagesRetrieve = cli.Command{ Name: "retrieve", - Usage: "Get Message", + Usage: "**CLI:**", Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ @@ -37,7 +37,7 @@ var inboxesMessagesRetrieve = cli.Command{ var inboxesMessagesUpdate = cli.Command{ Name: "update", - Usage: "Update Message", + Usage: "**CLI:**", Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ @@ -67,7 +67,7 @@ var inboxesMessagesUpdate = cli.Command{ var inboxesMessagesList = cli.Command{ Name: "list", - Usage: "List Messages", + Usage: "**CLI:**", Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ @@ -127,7 +127,7 @@ var inboxesMessagesList = cli.Command{ var inboxesMessagesForward = requestflag.WithInnerFlags(cli.Command{ Name: "forward", - Usage: "Forward Message", + Usage: "**CLI:**", Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ @@ -226,7 +226,7 @@ var inboxesMessagesForward = requestflag.WithInnerFlags(cli.Command{ var inboxesMessagesGetAttachment = cli.Command{ Name: "get-attachment", - Usage: "Get Attachment", + Usage: "**CLI:**", Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ @@ -251,7 +251,7 @@ var inboxesMessagesGetAttachment = cli.Command{ var inboxesMessagesGetRaw = cli.Command{ Name: "get-raw", - Usage: "Get Raw Message", + Usage: "**CLI:**", Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ @@ -271,7 +271,7 @@ var inboxesMessagesGetRaw = cli.Command{ var inboxesMessagesReply = requestflag.WithInnerFlags(cli.Command{ Name: "reply", - Usage: "Reply To Message", + Usage: "**CLI:**", Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ @@ -370,7 +370,7 @@ var inboxesMessagesReply = requestflag.WithInnerFlags(cli.Command{ var inboxesMessagesReplyAll = requestflag.WithInnerFlags(cli.Command{ Name: "reply-all", - Usage: "Reply All Message", + Usage: "**CLI:**", Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ @@ -452,7 +452,7 @@ var inboxesMessagesReplyAll = requestflag.WithInnerFlags(cli.Command{ var inboxesMessagesSend = requestflag.WithInnerFlags(cli.Command{ Name: "send", - Usage: "Send Message", + Usage: "**CLI:**", Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ diff --git a/pkg/cmd/inboxthread.go b/pkg/cmd/inboxthread.go index f9d68df..2700e55 100644 --- a/pkg/cmd/inboxthread.go +++ b/pkg/cmd/inboxthread.go @@ -17,7 +17,7 @@ import ( var inboxesThreadsRetrieve = cli.Command{ Name: "retrieve", - Usage: "Get Thread", + Usage: "**CLI:**", Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ @@ -37,7 +37,7 @@ var inboxesThreadsRetrieve = cli.Command{ var inboxesThreadsList = cli.Command{ Name: "list", - Usage: "List Threads", + Usage: "**CLI:**", Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ @@ -122,7 +122,7 @@ var inboxesThreadsDelete = cli.Command{ var inboxesThreadsGetAttachment = cli.Command{ Name: "get-attachment", - Usage: "Get Attachment", + Usage: "**CLI:**", Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ diff --git a/pkg/cmd/list.go b/pkg/cmd/list.go new file mode 100644 index 0000000..b9929da --- /dev/null +++ b/pkg/cmd/list.go @@ -0,0 +1,293 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "context" + "fmt" + "os" + + "github.com/agentmail-to/agentmail-cli/internal/apiquery" + "github.com/agentmail-to/agentmail-cli/internal/requestflag" + "github.com/agentmail-to/agentmail-go" + "github.com/agentmail-to/agentmail-go/option" + "github.com/tidwall/gjson" + "github.com/urfave/cli/v3" +) + +var listsCreate = cli.Command{ + Name: "create", + Usage: "**CLI:**", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "direction", + Usage: "Direction of list entry.", + Required: true, + }, + &requestflag.Flag[string]{ + Name: "type", + Usage: "Type of list entry.", + Required: true, + }, + &requestflag.Flag[string]{ + Name: "entry", + Usage: "Email address or domain to add.", + Required: true, + BodyPath: "entry", + }, + &requestflag.Flag[any]{ + Name: "reason", + Usage: "Reason for adding the entry.", + BodyPath: "reason", + }, + }, + Action: handleListsCreate, + HideHelpCommand: true, +} + +var listsRetrieve = cli.Command{ + Name: "retrieve", + Usage: "**CLI:**", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "direction", + Usage: "Direction of list entry.", + Required: true, + }, + &requestflag.Flag[string]{ + Name: "type", + Usage: "Type of list entry.", + Required: true, + }, + &requestflag.Flag[string]{ + Name: "entry", + Required: true, + }, + }, + Action: handleListsRetrieve, + HideHelpCommand: true, +} + +var listsList = cli.Command{ + Name: "list", + Usage: "**CLI:**", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "direction", + Usage: "Direction of list entry.", + Required: true, + }, + &requestflag.Flag[string]{ + Name: "type", + Usage: "Type of list entry.", + Required: true, + }, + &requestflag.Flag[any]{ + Name: "limit", + Usage: "Limit of number of items returned.", + QueryPath: "limit", + }, + &requestflag.Flag[any]{ + Name: "page-token", + Usage: "Page token for pagination.", + QueryPath: "page_token", + }, + }, + Action: handleListsList, + HideHelpCommand: true, +} + +var listsDelete = cli.Command{ + Name: "delete", + Usage: "**CLI:**", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "direction", + Usage: "Direction of list entry.", + Required: true, + }, + &requestflag.Flag[string]{ + Name: "type", + Usage: "Type of list entry.", + Required: true, + }, + &requestflag.Flag[string]{ + Name: "entry", + Required: true, + }, + }, + Action: handleListsDelete, + HideHelpCommand: true, +} + +func handleListsCreate(ctx context.Context, cmd *cli.Command) error { + client := agentmail.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("type") && len(unusedArgs) > 0 { + cmd.Set("type", unusedArgs[0]) + unusedArgs = unusedArgs[1:] + } + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + + params := agentmail.ListNewParams{ + Direction: agentmail.ListNewParamsDirection(cmd.Value("direction").(string)), + } + + options, err := flagOptions( + cmd, + apiquery.NestedQueryFormatBrackets, + apiquery.ArrayQueryFormatComma, + ApplicationJSON, + false, + ) + if err != nil { + return err + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Lists.New( + ctx, + agentmail.ListNewParamsType(cmd.Value("type").(string)), + params, + options..., + ) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + transform := cmd.Root().String("transform") + return ShowJSON(os.Stdout, "lists create", obj, format, transform) +} + +func handleListsRetrieve(ctx context.Context, cmd *cli.Command) error { + client := agentmail.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("entry") && len(unusedArgs) > 0 { + cmd.Set("entry", unusedArgs[0]) + unusedArgs = unusedArgs[1:] + } + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + + params := agentmail.ListGetParams{ + Direction: agentmail.ListGetParamsDirection(cmd.Value("direction").(string)), + Type: agentmail.ListGetParamsType(cmd.Value("type").(string)), + } + + options, err := flagOptions( + cmd, + apiquery.NestedQueryFormatBrackets, + apiquery.ArrayQueryFormatComma, + EmptyBody, + false, + ) + if err != nil { + return err + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Lists.Get( + ctx, + cmd.Value("entry").(string), + params, + options..., + ) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + transform := cmd.Root().String("transform") + return ShowJSON(os.Stdout, "lists retrieve", obj, format, transform) +} + +func handleListsList(ctx context.Context, cmd *cli.Command) error { + client := agentmail.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("type") && len(unusedArgs) > 0 { + cmd.Set("type", unusedArgs[0]) + unusedArgs = unusedArgs[1:] + } + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + + params := agentmail.ListListParams{ + Direction: agentmail.ListListParamsDirection(cmd.Value("direction").(string)), + } + + options, err := flagOptions( + cmd, + apiquery.NestedQueryFormatBrackets, + apiquery.ArrayQueryFormatComma, + EmptyBody, + false, + ) + if err != nil { + return err + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Lists.List( + ctx, + agentmail.ListListParamsType(cmd.Value("type").(string)), + params, + options..., + ) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + transform := cmd.Root().String("transform") + return ShowJSON(os.Stdout, "lists list", obj, format, transform) +} + +func handleListsDelete(ctx context.Context, cmd *cli.Command) error { + client := agentmail.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("entry") && len(unusedArgs) > 0 { + cmd.Set("entry", unusedArgs[0]) + unusedArgs = unusedArgs[1:] + } + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + + params := agentmail.ListDeleteParams{ + Direction: agentmail.ListDeleteParamsDirection(cmd.Value("direction").(string)), + Type: agentmail.ListDeleteParamsType(cmd.Value("type").(string)), + } + + options, err := flagOptions( + cmd, + apiquery.NestedQueryFormatBrackets, + apiquery.ArrayQueryFormatComma, + EmptyBody, + false, + ) + if err != nil { + return err + } + + return client.Lists.Delete( + ctx, + cmd.Value("entry").(string), + params, + options..., + ) +} diff --git a/pkg/cmd/list_test.go b/pkg/cmd/list_test.go new file mode 100644 index 0000000..cf4c667 --- /dev/null +++ b/pkg/cmd/list_test.go @@ -0,0 +1,81 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "testing" + + "github.com/agentmail-to/agentmail-cli/internal/mocktest" +) + +func TestListsCreate(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "lists", "create", + "--direction", "send", + "--type", "allow", + "--entry", "entry", + "--reason", "reason", + ) + }) + + t.Run("piping data", func(t *testing.T) { + // Test piping YAML data over stdin + pipeData := []byte("" + + "entry: entry\n" + + "reason: reason\n") + mocktest.TestRunMockTestWithPipeAndFlags( + t, pipeData, + "--api-key", "string", + "lists", "create", + "--direction", "send", + "--type", "allow", + ) + }) +} + +func TestListsRetrieve(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "lists", "retrieve", + "--direction", "send", + "--type", "allow", + "--entry", "entry", + ) + }) +} + +func TestListsList(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "lists", "list", + "--direction", "send", + "--type", "allow", + "--limit", "0", + "--page-token", "page_token", + ) + }) +} + +func TestListsDelete(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "lists", "delete", + "--direction", "send", + "--type", "allow", + "--entry", "entry", + ) + }) +} diff --git a/pkg/cmd/metric.go b/pkg/cmd/metric.go index 991f65b..7b1768b 100644 --- a/pkg/cmd/metric.go +++ b/pkg/cmd/metric.go @@ -17,7 +17,7 @@ import ( var metricsList = cli.Command{ Name: "list", - Usage: "Query Metrics", + Usage: "**CLI:**", Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[any]{ diff --git a/pkg/cmd/pod.go b/pkg/cmd/pod.go index 91ace67..0f88189 100644 --- a/pkg/cmd/pod.go +++ b/pkg/cmd/pod.go @@ -17,7 +17,7 @@ import ( var podsCreate = cli.Command{ Name: "create", - Usage: "Create Pod", + Usage: "**CLI:**", Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[any]{ @@ -37,7 +37,7 @@ var podsCreate = cli.Command{ var podsRetrieve = cli.Command{ Name: "retrieve", - Usage: "Get Pod", + Usage: "**CLI:**", Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ @@ -52,7 +52,7 @@ var podsRetrieve = cli.Command{ var podsList = cli.Command{ Name: "list", - Usage: "List Pods", + Usage: "**CLI:**", Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[any]{ @@ -77,7 +77,7 @@ var podsList = cli.Command{ var podsDelete = cli.Command{ Name: "delete", - Usage: "Delete Pod", + Usage: "**CLI:**", Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ diff --git a/pkg/cmd/podapikey.go b/pkg/cmd/podapikey.go new file mode 100644 index 0000000..35ea60f --- /dev/null +++ b/pkg/cmd/podapikey.go @@ -0,0 +1,382 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "context" + "fmt" + "os" + + "github.com/agentmail-to/agentmail-cli/internal/apiquery" + "github.com/agentmail-to/agentmail-cli/internal/requestflag" + "github.com/agentmail-to/agentmail-go" + "github.com/agentmail-to/agentmail-go/option" + "github.com/tidwall/gjson" + "github.com/urfave/cli/v3" +) + +var podsAPIKeysCreate = requestflag.WithInnerFlags(cli.Command{ + Name: "create", + Usage: "**CLI:**", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "pod-id", + Usage: "ID of pod.", + Required: true, + }, + &requestflag.Flag[any]{ + Name: "name", + Usage: "Name of api key.", + BodyPath: "name", + }, + &requestflag.Flag[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", + }, + }, + Action: handlePodsAPIKeysCreate, + HideHelpCommand: true, +}, map[string][]requestflag.HasOuterFlag{ + "permissions": { + &requestflag.InnerFlag[any]{ + Name: "permissions.api-key-create", + Usage: "Create API keys.", + InnerField: "api_key_create", + }, + &requestflag.InnerFlag[any]{ + Name: "permissions.api-key-delete", + Usage: "Delete API keys.", + InnerField: "api_key_delete", + }, + &requestflag.InnerFlag[any]{ + Name: "permissions.api-key-read", + Usage: "Read API keys.", + InnerField: "api_key_read", + }, + &requestflag.InnerFlag[any]{ + Name: "permissions.domain-create", + Usage: "Create domains.", + InnerField: "domain_create", + }, + &requestflag.InnerFlag[any]{ + Name: "permissions.domain-delete", + Usage: "Delete domains.", + InnerField: "domain_delete", + }, + &requestflag.InnerFlag[any]{ + Name: "permissions.domain-read", + Usage: "Read domain details.", + InnerField: "domain_read", + }, + &requestflag.InnerFlag[any]{ + Name: "permissions.domain-update", + Usage: "Update domains.", + InnerField: "domain_update", + }, + &requestflag.InnerFlag[any]{ + Name: "permissions.draft-create", + Usage: "Create drafts.", + InnerField: "draft_create", + }, + &requestflag.InnerFlag[any]{ + Name: "permissions.draft-delete", + Usage: "Delete drafts.", + InnerField: "draft_delete", + }, + &requestflag.InnerFlag[any]{ + Name: "permissions.draft-read", + Usage: "Read drafts.", + InnerField: "draft_read", + }, + &requestflag.InnerFlag[any]{ + Name: "permissions.draft-send", + Usage: "Send drafts.", + InnerField: "draft_send", + }, + &requestflag.InnerFlag[any]{ + Name: "permissions.draft-update", + Usage: "Update drafts.", + InnerField: "draft_update", + }, + &requestflag.InnerFlag[any]{ + Name: "permissions.inbox-create", + Usage: "Create new inboxes.", + InnerField: "inbox_create", + }, + &requestflag.InnerFlag[any]{ + Name: "permissions.inbox-delete", + Usage: "Delete inboxes.", + InnerField: "inbox_delete", + }, + &requestflag.InnerFlag[any]{ + Name: "permissions.inbox-read", + Usage: "Read inbox details.", + InnerField: "inbox_read", + }, + &requestflag.InnerFlag[any]{ + Name: "permissions.inbox-update", + Usage: "Update inbox settings.", + InnerField: "inbox_update", + }, + &requestflag.InnerFlag[any]{ + Name: "permissions.label-blocked-read", + Usage: "Access messages labeled blocked.", + InnerField: "label_blocked_read", + }, + &requestflag.InnerFlag[any]{ + Name: "permissions.label-spam-read", + Usage: "Access messages labeled spam.", + InnerField: "label_spam_read", + }, + &requestflag.InnerFlag[any]{ + Name: "permissions.label-trash-read", + Usage: "Access messages labeled trash.", + InnerField: "label_trash_read", + }, + &requestflag.InnerFlag[any]{ + Name: "permissions.list-entry-create", + Usage: "Create list entries.", + InnerField: "list_entry_create", + }, + &requestflag.InnerFlag[any]{ + Name: "permissions.list-entry-delete", + Usage: "Delete list entries.", + InnerField: "list_entry_delete", + }, + &requestflag.InnerFlag[any]{ + Name: "permissions.list-entry-read", + Usage: "Read list entries.", + InnerField: "list_entry_read", + }, + &requestflag.InnerFlag[any]{ + Name: "permissions.message-read", + Usage: "Read messages.", + InnerField: "message_read", + }, + &requestflag.InnerFlag[any]{ + Name: "permissions.message-send", + Usage: "Send messages.", + InnerField: "message_send", + }, + &requestflag.InnerFlag[any]{ + Name: "permissions.message-update", + Usage: "Update message labels.", + InnerField: "message_update", + }, + &requestflag.InnerFlag[any]{ + Name: "permissions.metrics-read", + Usage: "Read metrics.", + InnerField: "metrics_read", + }, + &requestflag.InnerFlag[any]{ + Name: "permissions.pod-create", + Usage: "Create pods.", + InnerField: "pod_create", + }, + &requestflag.InnerFlag[any]{ + Name: "permissions.pod-delete", + Usage: "Delete pods.", + InnerField: "pod_delete", + }, + &requestflag.InnerFlag[any]{ + Name: "permissions.pod-read", + Usage: "Read pods.", + InnerField: "pod_read", + }, + &requestflag.InnerFlag[any]{ + Name: "permissions.thread-delete", + Usage: "Delete threads.", + InnerField: "thread_delete", + }, + &requestflag.InnerFlag[any]{ + Name: "permissions.thread-read", + Usage: "Read threads.", + InnerField: "thread_read", + }, + &requestflag.InnerFlag[any]{ + Name: "permissions.webhook-create", + Usage: "Create webhooks.", + InnerField: "webhook_create", + }, + &requestflag.InnerFlag[any]{ + Name: "permissions.webhook-delete", + Usage: "Delete webhooks.", + InnerField: "webhook_delete", + }, + &requestflag.InnerFlag[any]{ + Name: "permissions.webhook-read", + Usage: "Read webhook configurations.", + InnerField: "webhook_read", + }, + &requestflag.InnerFlag[any]{ + Name: "permissions.webhook-update", + Usage: "Update webhooks.", + InnerField: "webhook_update", + }, + }, +}) + +var podsAPIKeysList = cli.Command{ + Name: "list", + Usage: "**CLI:**", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "pod-id", + Usage: "ID of pod.", + Required: true, + }, + &requestflag.Flag[any]{ + Name: "limit", + Usage: "Limit of number of items returned.", + QueryPath: "limit", + }, + &requestflag.Flag[any]{ + Name: "page-token", + Usage: "Page token for pagination.", + QueryPath: "page_token", + }, + }, + Action: handlePodsAPIKeysList, + HideHelpCommand: true, +} + +var podsAPIKeysDelete = cli.Command{ + Name: "delete", + Usage: "**CLI:**", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "pod-id", + Usage: "ID of pod.", + Required: true, + }, + &requestflag.Flag[string]{ + Name: "api-key-id", + Usage: "ID of api key.", + Required: true, + }, + }, + Action: handlePodsAPIKeysDelete, + HideHelpCommand: true, +} + +func handlePodsAPIKeysCreate(ctx context.Context, cmd *cli.Command) error { + client := agentmail.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("pod-id") && len(unusedArgs) > 0 { + cmd.Set("pod-id", unusedArgs[0]) + unusedArgs = unusedArgs[1:] + } + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + + params := agentmail.PodAPIKeyNewParams{} + + options, err := flagOptions( + cmd, + apiquery.NestedQueryFormatBrackets, + apiquery.ArrayQueryFormatComma, + ApplicationJSON, + false, + ) + if err != nil { + return err + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Pods.APIKeys.New( + ctx, + cmd.Value("pod-id").(string), + params, + options..., + ) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + transform := cmd.Root().String("transform") + return ShowJSON(os.Stdout, "pods:api-keys create", obj, format, transform) +} + +func handlePodsAPIKeysList(ctx context.Context, cmd *cli.Command) error { + client := agentmail.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("pod-id") && len(unusedArgs) > 0 { + cmd.Set("pod-id", unusedArgs[0]) + unusedArgs = unusedArgs[1:] + } + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + + params := agentmail.PodAPIKeyListParams{} + + options, err := flagOptions( + cmd, + apiquery.NestedQueryFormatBrackets, + apiquery.ArrayQueryFormatComma, + EmptyBody, + false, + ) + if err != nil { + return err + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Pods.APIKeys.List( + ctx, + cmd.Value("pod-id").(string), + params, + options..., + ) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + transform := cmd.Root().String("transform") + return ShowJSON(os.Stdout, "pods:api-keys list", obj, format, transform) +} + +func handlePodsAPIKeysDelete(ctx context.Context, cmd *cli.Command) error { + client := agentmail.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("api-key-id") && len(unusedArgs) > 0 { + cmd.Set("api-key-id", unusedArgs[0]) + unusedArgs = unusedArgs[1:] + } + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + + params := agentmail.PodAPIKeyDeleteParams{ + PodID: cmd.Value("pod-id").(string), + } + + options, err := flagOptions( + cmd, + apiquery.NestedQueryFormatBrackets, + apiquery.ArrayQueryFormatComma, + EmptyBody, + false, + ) + if err != nil { + return err + } + + return client.Pods.APIKeys.Delete( + ctx, + cmd.Value("api-key-id").(string), + params, + options..., + ) +} diff --git a/pkg/cmd/podapikey_test.go b/pkg/cmd/podapikey_test.go new file mode 100644 index 0000000..730ea70 --- /dev/null +++ b/pkg/cmd/podapikey_test.go @@ -0,0 +1,148 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "testing" + + "github.com/agentmail-to/agentmail-cli/internal/mocktest" + "github.com/agentmail-to/agentmail-cli/internal/requestflag" +) + +func TestPodsAPIKeysCreate(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "pods:api-keys", "create", + "--pod-id", "pod_id", + "--name", "name", + "--permissions", "{api_key_create: true, api_key_delete: true, api_key_read: true, domain_create: true, domain_delete: true, domain_read: true, domain_update: true, draft_create: true, draft_delete: true, draft_read: true, draft_send: true, draft_update: true, inbox_create: true, inbox_delete: true, inbox_read: true, inbox_update: true, label_blocked_read: true, label_spam_read: true, label_trash_read: true, list_entry_create: true, list_entry_delete: true, list_entry_read: true, message_read: true, message_send: true, message_update: true, metrics_read: true, pod_create: true, pod_delete: true, pod_read: true, thread_delete: true, thread_read: true, webhook_create: true, webhook_delete: true, webhook_read: true, webhook_update: true}", + ) + }) + + t.Run("inner flags", func(t *testing.T) { + // Check that inner flags have been set up correctly + requestflag.CheckInnerFlags(podsAPIKeysCreate) + + // Alternative argument passing style using inner flags + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "pods:api-keys", "create", + "--pod-id", "pod_id", + "--name", "name", + "--permissions.api-key-create=true", + "--permissions.api-key-delete=true", + "--permissions.api-key-read=true", + "--permissions.domain-create=true", + "--permissions.domain-delete=true", + "--permissions.domain-read=true", + "--permissions.domain-update=true", + "--permissions.draft-create=true", + "--permissions.draft-delete=true", + "--permissions.draft-read=true", + "--permissions.draft-send=true", + "--permissions.draft-update=true", + "--permissions.inbox-create=true", + "--permissions.inbox-delete=true", + "--permissions.inbox-read=true", + "--permissions.inbox-update=true", + "--permissions.label-blocked-read=true", + "--permissions.label-spam-read=true", + "--permissions.label-trash-read=true", + "--permissions.list-entry-create=true", + "--permissions.list-entry-delete=true", + "--permissions.list-entry-read=true", + "--permissions.message-read=true", + "--permissions.message-send=true", + "--permissions.message-update=true", + "--permissions.metrics-read=true", + "--permissions.pod-create=true", + "--permissions.pod-delete=true", + "--permissions.pod-read=true", + "--permissions.thread-delete=true", + "--permissions.thread-read=true", + "--permissions.webhook-create=true", + "--permissions.webhook-delete=true", + "--permissions.webhook-read=true", + "--permissions.webhook-update=true", + ) + }) + + t.Run("piping data", func(t *testing.T) { + // Test piping YAML data over stdin + pipeData := []byte("" + + "name: name\n" + + "permissions:\n" + + " api_key_create: true\n" + + " api_key_delete: true\n" + + " api_key_read: true\n" + + " domain_create: true\n" + + " domain_delete: true\n" + + " domain_read: true\n" + + " domain_update: true\n" + + " draft_create: true\n" + + " draft_delete: true\n" + + " draft_read: true\n" + + " draft_send: true\n" + + " draft_update: true\n" + + " inbox_create: true\n" + + " inbox_delete: true\n" + + " inbox_read: true\n" + + " inbox_update: true\n" + + " label_blocked_read: true\n" + + " label_spam_read: true\n" + + " label_trash_read: true\n" + + " list_entry_create: true\n" + + " list_entry_delete: true\n" + + " list_entry_read: true\n" + + " message_read: true\n" + + " message_send: true\n" + + " message_update: true\n" + + " metrics_read: true\n" + + " pod_create: true\n" + + " pod_delete: true\n" + + " pod_read: true\n" + + " thread_delete: true\n" + + " thread_read: true\n" + + " webhook_create: true\n" + + " webhook_delete: true\n" + + " webhook_read: true\n" + + " webhook_update: true\n") + mocktest.TestRunMockTestWithPipeAndFlags( + t, pipeData, + "--api-key", "string", + "pods:api-keys", "create", + "--pod-id", "pod_id", + ) + }) +} + +func TestPodsAPIKeysList(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "pods:api-keys", "list", + "--pod-id", "pod_id", + "--limit", "0", + "--page-token", "page_token", + ) + }) +} + +func TestPodsAPIKeysDelete(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "pods:api-keys", "delete", + "--pod-id", "pod_id", + "--api-key-id", "api_key_id", + ) + }) +} diff --git a/pkg/cmd/poddomain.go b/pkg/cmd/poddomain.go index c4cb094..49720eb 100644 --- a/pkg/cmd/poddomain.go +++ b/pkg/cmd/poddomain.go @@ -17,7 +17,7 @@ import ( var podsDomainsCreate = cli.Command{ Name: "create", - Usage: "Create Domain", + Usage: "**CLI:**", Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ @@ -42,9 +42,54 @@ var podsDomainsCreate = cli.Command{ HideHelpCommand: true, } +var podsDomainsRetrieve = cli.Command{ + Name: "retrieve", + Usage: "**CLI:**", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "pod-id", + Usage: "ID of pod.", + Required: true, + }, + &requestflag.Flag[string]{ + Name: "domain-id", + Usage: "The ID of the domain.", + Required: true, + }, + }, + Action: handlePodsDomainsRetrieve, + HideHelpCommand: true, +} + +var podsDomainsUpdate = cli.Command{ + Name: "update", + Usage: "**CLI:**", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "pod-id", + Usage: "ID of pod.", + Required: true, + }, + &requestflag.Flag[string]{ + Name: "domain-id", + Usage: "The ID of the domain.", + Required: true, + }, + &requestflag.Flag[any]{ + Name: "feedback-enabled", + Usage: "Bounce and complaint notifications are sent to your inboxes.", + BodyPath: "feedback_enabled", + }, + }, + Action: handlePodsDomainsUpdate, + HideHelpCommand: true, +} + var podsDomainsList = cli.Command{ Name: "list", - Usage: "List Domains", + Usage: "**CLI:**", Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ @@ -74,7 +119,7 @@ var podsDomainsList = cli.Command{ var podsDomainsDelete = cli.Command{ Name: "delete", - Usage: "Delete Domain", + Usage: "**CLI:**", Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ @@ -92,6 +137,46 @@ var podsDomainsDelete = cli.Command{ HideHelpCommand: true, } +var podsDomainsGetZoneFile = cli.Command{ + Name: "get-zone-file", + Usage: "**CLI:**", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "pod-id", + Usage: "ID of pod.", + Required: true, + }, + &requestflag.Flag[string]{ + Name: "domain-id", + Usage: "The ID of the domain.", + Required: true, + }, + }, + Action: handlePodsDomainsGetZoneFile, + HideHelpCommand: true, +} + +var podsDomainsVerify = cli.Command{ + Name: "verify", + Usage: "**CLI:**", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "pod-id", + Usage: "ID of pod.", + Required: true, + }, + &requestflag.Flag[string]{ + Name: "domain-id", + Usage: "The ID of the domain.", + Required: true, + }, + }, + Action: handlePodsDomainsVerify, + HideHelpCommand: true, +} + func handlePodsDomainsCreate(ctx context.Context, cmd *cli.Command) error { client := agentmail.NewClient(getDefaultRequestOptions(cmd)...) unusedArgs := cmd.Args().Slice() @@ -134,6 +219,94 @@ func handlePodsDomainsCreate(ctx context.Context, cmd *cli.Command) error { return ShowJSON(os.Stdout, "pods:domains create", obj, format, transform) } +func handlePodsDomainsRetrieve(ctx context.Context, cmd *cli.Command) error { + client := agentmail.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("domain-id") && len(unusedArgs) > 0 { + cmd.Set("domain-id", unusedArgs[0]) + unusedArgs = unusedArgs[1:] + } + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + + params := agentmail.PodDomainGetParams{ + PodID: cmd.Value("pod-id").(string), + } + + options, err := flagOptions( + cmd, + apiquery.NestedQueryFormatBrackets, + apiquery.ArrayQueryFormatComma, + EmptyBody, + false, + ) + if err != nil { + return err + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Pods.Domains.Get( + ctx, + cmd.Value("domain-id").(string), + params, + options..., + ) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + transform := cmd.Root().String("transform") + return ShowJSON(os.Stdout, "pods:domains retrieve", obj, format, transform) +} + +func handlePodsDomainsUpdate(ctx context.Context, cmd *cli.Command) error { + client := agentmail.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("domain-id") && len(unusedArgs) > 0 { + cmd.Set("domain-id", unusedArgs[0]) + unusedArgs = unusedArgs[1:] + } + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + + params := agentmail.PodDomainUpdateParams{ + PodID: cmd.Value("pod-id").(string), + } + + options, err := flagOptions( + cmd, + apiquery.NestedQueryFormatBrackets, + apiquery.ArrayQueryFormatComma, + ApplicationJSON, + false, + ) + if err != nil { + return err + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Pods.Domains.Update( + ctx, + cmd.Value("domain-id").(string), + params, + options..., + ) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + transform := cmd.Root().String("transform") + return ShowJSON(os.Stdout, "pods:domains update", obj, format, transform) +} + func handlePodsDomainsList(ctx context.Context, cmd *cli.Command) error { client := agentmail.NewClient(getDefaultRequestOptions(cmd)...) unusedArgs := cmd.Args().Slice() @@ -209,3 +382,71 @@ func handlePodsDomainsDelete(ctx context.Context, cmd *cli.Command) error { options..., ) } + +func handlePodsDomainsGetZoneFile(ctx context.Context, cmd *cli.Command) error { + client := agentmail.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("domain-id") && len(unusedArgs) > 0 { + cmd.Set("domain-id", unusedArgs[0]) + unusedArgs = unusedArgs[1:] + } + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + + params := agentmail.PodDomainGetZoneFileParams{ + PodID: cmd.Value("pod-id").(string), + } + + options, err := flagOptions( + cmd, + apiquery.NestedQueryFormatBrackets, + apiquery.ArrayQueryFormatComma, + EmptyBody, + false, + ) + if err != nil { + return err + } + + return client.Pods.Domains.GetZoneFile( + ctx, + cmd.Value("domain-id").(string), + params, + options..., + ) +} + +func handlePodsDomainsVerify(ctx context.Context, cmd *cli.Command) error { + client := agentmail.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("domain-id") && len(unusedArgs) > 0 { + cmd.Set("domain-id", unusedArgs[0]) + unusedArgs = unusedArgs[1:] + } + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + + params := agentmail.PodDomainVerifyParams{ + PodID: cmd.Value("pod-id").(string), + } + + options, err := flagOptions( + cmd, + apiquery.NestedQueryFormatBrackets, + apiquery.ArrayQueryFormatComma, + EmptyBody, + false, + ) + if err != nil { + return err + } + + return client.Pods.Domains.Verify( + ctx, + cmd.Value("domain-id").(string), + params, + options..., + ) +} diff --git a/pkg/cmd/poddomain_test.go b/pkg/cmd/poddomain_test.go index fde87f5..e63d5c0 100644 --- a/pkg/cmd/poddomain_test.go +++ b/pkg/cmd/poddomain_test.go @@ -35,6 +35,45 @@ func TestPodsDomainsCreate(t *testing.T) { }) } +func TestPodsDomainsRetrieve(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "pods:domains", "retrieve", + "--pod-id", "pod_id", + "--domain-id", "domain_id", + ) + }) +} + +func TestPodsDomainsUpdate(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "pods:domains", "update", + "--pod-id", "pod_id", + "--domain-id", "domain_id", + "--feedback-enabled=true", + ) + }) + + t.Run("piping data", func(t *testing.T) { + // Test piping YAML data over stdin + pipeData := []byte("feedback_enabled: true") + mocktest.TestRunMockTestWithPipeAndFlags( + t, pipeData, + "--api-key", "string", + "pods:domains", "update", + "--pod-id", "pod_id", + "--domain-id", "domain_id", + ) + }) +} + func TestPodsDomainsList(t *testing.T) { t.Skip("Mock server tests are disabled") t.Run("regular flags", func(t *testing.T) { @@ -62,3 +101,29 @@ func TestPodsDomainsDelete(t *testing.T) { ) }) } + +func TestPodsDomainsGetZoneFile(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "pods:domains", "get-zone-file", + "--pod-id", "pod_id", + "--domain-id", "domain_id", + ) + }) +} + +func TestPodsDomainsVerify(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "pods:domains", "verify", + "--pod-id", "pod_id", + "--domain-id", "domain_id", + ) + }) +} diff --git a/pkg/cmd/poddraft.go b/pkg/cmd/poddraft.go index 5a73990..074e8e4 100644 --- a/pkg/cmd/poddraft.go +++ b/pkg/cmd/poddraft.go @@ -17,7 +17,7 @@ import ( var podsDraftsRetrieve = cli.Command{ Name: "retrieve", - Usage: "Get Draft", + Usage: "**CLI:**", Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ @@ -37,7 +37,7 @@ var podsDraftsRetrieve = cli.Command{ var podsDraftsList = cli.Command{ Name: "list", - Usage: "List Drafts", + Usage: "**CLI:**", Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ @@ -80,6 +80,31 @@ var podsDraftsList = cli.Command{ HideHelpCommand: true, } +var podsDraftsGetAttachment = cli.Command{ + Name: "get-attachment", + Usage: "**CLI:**", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "pod-id", + Usage: "ID of pod.", + Required: true, + }, + &requestflag.Flag[string]{ + Name: "draft-id", + Usage: "ID of draft.", + Required: true, + }, + &requestflag.Flag[string]{ + Name: "attachment-id", + Usage: "ID of attachment.", + Required: true, + }, + }, + Action: handlePodsDraftsGetAttachment, + HideHelpCommand: true, +} + func handlePodsDraftsRetrieve(ctx context.Context, cmd *cli.Command) error { client := agentmail.NewClient(getDefaultRequestOptions(cmd)...) unusedArgs := cmd.Args().Slice() @@ -165,3 +190,48 @@ func handlePodsDraftsList(ctx context.Context, cmd *cli.Command) error { transform := cmd.Root().String("transform") return ShowJSON(os.Stdout, "pods:drafts list", obj, format, transform) } + +func handlePodsDraftsGetAttachment(ctx context.Context, cmd *cli.Command) error { + client := agentmail.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("attachment-id") && len(unusedArgs) > 0 { + cmd.Set("attachment-id", unusedArgs[0]) + unusedArgs = unusedArgs[1:] + } + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + + params := agentmail.PodDraftGetAttachmentParams{ + PodID: cmd.Value("pod-id").(string), + DraftID: cmd.Value("draft-id").(string), + } + + options, err := flagOptions( + cmd, + apiquery.NestedQueryFormatBrackets, + apiquery.ArrayQueryFormatComma, + EmptyBody, + false, + ) + if err != nil { + return err + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Pods.Drafts.GetAttachment( + ctx, + cmd.Value("attachment-id").(string), + params, + options..., + ) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + transform := cmd.Root().String("transform") + return ShowJSON(os.Stdout, "pods:drafts get-attachment", obj, format, transform) +} diff --git a/pkg/cmd/poddraft_test.go b/pkg/cmd/poddraft_test.go index ea6a65a..8bed702 100644 --- a/pkg/cmd/poddraft_test.go +++ b/pkg/cmd/poddraft_test.go @@ -38,3 +38,17 @@ func TestPodsDraftsList(t *testing.T) { ) }) } + +func TestPodsDraftsGetAttachment(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "pods:drafts", "get-attachment", + "--pod-id", "pod_id", + "--draft-id", "draft_id", + "--attachment-id", "attachment_id", + ) + }) +} diff --git a/pkg/cmd/podinbox.go b/pkg/cmd/podinbox.go index a7f945f..1b84e3b 100644 --- a/pkg/cmd/podinbox.go +++ b/pkg/cmd/podinbox.go @@ -17,7 +17,7 @@ import ( var podsInboxesCreate = cli.Command{ Name: "create", - Usage: "Create Inbox", + Usage: "**CLI:**", Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ @@ -52,7 +52,7 @@ var podsInboxesCreate = cli.Command{ var podsInboxesRetrieve = cli.Command{ Name: "retrieve", - Usage: "Get Inbox", + Usage: "**CLI:**", Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ @@ -70,9 +70,35 @@ var podsInboxesRetrieve = cli.Command{ HideHelpCommand: true, } +var podsInboxesUpdate = cli.Command{ + Name: "update", + Usage: "**CLI:**", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "pod-id", + Usage: "ID of pod.", + Required: true, + }, + &requestflag.Flag[string]{ + Name: "inbox-id", + Usage: "The ID of the inbox.", + Required: true, + }, + &requestflag.Flag[string]{ + Name: "display-name", + Usage: "Display name: `Display Name `.", + Required: true, + BodyPath: "display_name", + }, + }, + Action: handlePodsInboxesUpdate, + HideHelpCommand: true, +} + var podsInboxesList = cli.Command{ Name: "list", - Usage: "List Inboxes", + Usage: "**CLI:**", Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ @@ -102,7 +128,7 @@ var podsInboxesList = cli.Command{ var podsInboxesDelete = cli.Command{ Name: "delete", - Usage: "Delete Inbox", + Usage: "**CLI:**", Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ @@ -206,6 +232,50 @@ func handlePodsInboxesRetrieve(ctx context.Context, cmd *cli.Command) error { return ShowJSON(os.Stdout, "pods:inboxes retrieve", obj, format, transform) } +func handlePodsInboxesUpdate(ctx context.Context, cmd *cli.Command) error { + client := agentmail.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("inbox-id") && len(unusedArgs) > 0 { + cmd.Set("inbox-id", unusedArgs[0]) + unusedArgs = unusedArgs[1:] + } + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + + params := agentmail.PodInboxUpdateParams{ + PodID: cmd.Value("pod-id").(string), + } + + options, err := flagOptions( + cmd, + apiquery.NestedQueryFormatBrackets, + apiquery.ArrayQueryFormatComma, + ApplicationJSON, + false, + ) + if err != nil { + return err + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Pods.Inboxes.Update( + ctx, + cmd.Value("inbox-id").(string), + params, + options..., + ) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + transform := cmd.Root().String("transform") + return ShowJSON(os.Stdout, "pods:inboxes update", obj, format, transform) +} + func handlePodsInboxesList(ctx context.Context, cmd *cli.Command) error { client := agentmail.NewClient(getDefaultRequestOptions(cmd)...) unusedArgs := cmd.Args().Slice() diff --git a/pkg/cmd/podinbox_test.go b/pkg/cmd/podinbox_test.go index 53cbb5b..6b58c12 100644 --- a/pkg/cmd/podinbox_test.go +++ b/pkg/cmd/podinbox_test.go @@ -52,6 +52,32 @@ func TestPodsInboxesRetrieve(t *testing.T) { }) } +func TestPodsInboxesUpdate(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "pods:inboxes", "update", + "--pod-id", "pod_id", + "--inbox-id", "inbox_id", + "--display-name", "display_name", + ) + }) + + t.Run("piping data", func(t *testing.T) { + // Test piping YAML data over stdin + pipeData := []byte("display_name: display_name") + mocktest.TestRunMockTestWithPipeAndFlags( + t, pipeData, + "--api-key", "string", + "pods:inboxes", "update", + "--pod-id", "pod_id", + "--inbox-id", "inbox_id", + ) + }) +} + func TestPodsInboxesList(t *testing.T) { t.Skip("Mock server tests are disabled") t.Run("regular flags", func(t *testing.T) { diff --git a/pkg/cmd/podlist.go b/pkg/cmd/podlist.go new file mode 100644 index 0000000..9a93f47 --- /dev/null +++ b/pkg/cmd/podlist.go @@ -0,0 +1,317 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "context" + "fmt" + "os" + + "github.com/agentmail-to/agentmail-cli/internal/apiquery" + "github.com/agentmail-to/agentmail-cli/internal/requestflag" + "github.com/agentmail-to/agentmail-go" + "github.com/agentmail-to/agentmail-go/option" + "github.com/tidwall/gjson" + "github.com/urfave/cli/v3" +) + +var podsListsCreate = cli.Command{ + Name: "create", + Usage: "**CLI:**", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "pod-id", + Usage: "ID of pod.", + Required: true, + }, + &requestflag.Flag[string]{ + Name: "direction", + Usage: "Direction of list entry.", + Required: true, + }, + &requestflag.Flag[string]{ + Name: "type", + Usage: "Type of list entry.", + Required: true, + }, + &requestflag.Flag[string]{ + Name: "entry", + Usage: "Email address or domain to add.", + Required: true, + BodyPath: "entry", + }, + &requestflag.Flag[any]{ + Name: "reason", + Usage: "Reason for adding the entry.", + BodyPath: "reason", + }, + }, + Action: handlePodsListsCreate, + HideHelpCommand: true, +} + +var podsListsRetrieve = cli.Command{ + Name: "retrieve", + Usage: "**CLI:**", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "pod-id", + Usage: "ID of pod.", + Required: true, + }, + &requestflag.Flag[string]{ + Name: "direction", + Usage: "Direction of list entry.", + Required: true, + }, + &requestflag.Flag[string]{ + Name: "type", + Usage: "Type of list entry.", + Required: true, + }, + &requestflag.Flag[string]{ + Name: "entry", + Required: true, + }, + }, + Action: handlePodsListsRetrieve, + HideHelpCommand: true, +} + +var podsListsList = cli.Command{ + Name: "list", + Usage: "**CLI:**", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "pod-id", + Usage: "ID of pod.", + Required: true, + }, + &requestflag.Flag[string]{ + Name: "direction", + Usage: "Direction of list entry.", + Required: true, + }, + &requestflag.Flag[string]{ + Name: "type", + Usage: "Type of list entry.", + Required: true, + }, + &requestflag.Flag[any]{ + Name: "limit", + Usage: "Limit of number of items returned.", + QueryPath: "limit", + }, + &requestflag.Flag[any]{ + Name: "page-token", + Usage: "Page token for pagination.", + QueryPath: "page_token", + }, + }, + Action: handlePodsListsList, + HideHelpCommand: true, +} + +var podsListsDelete = cli.Command{ + Name: "delete", + Usage: "**CLI:**", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "pod-id", + Usage: "ID of pod.", + Required: true, + }, + &requestflag.Flag[string]{ + Name: "direction", + Usage: "Direction of list entry.", + Required: true, + }, + &requestflag.Flag[string]{ + Name: "type", + Usage: "Type of list entry.", + Required: true, + }, + &requestflag.Flag[string]{ + Name: "entry", + Required: true, + }, + }, + Action: handlePodsListsDelete, + HideHelpCommand: true, +} + +func handlePodsListsCreate(ctx context.Context, cmd *cli.Command) error { + client := agentmail.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("type") && len(unusedArgs) > 0 { + cmd.Set("type", unusedArgs[0]) + unusedArgs = unusedArgs[1:] + } + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + + params := agentmail.PodListNewParams{ + PodID: cmd.Value("pod-id").(string), + Direction: agentmail.PodListNewParamsDirection(cmd.Value("direction").(string)), + } + + options, err := flagOptions( + cmd, + apiquery.NestedQueryFormatBrackets, + apiquery.ArrayQueryFormatComma, + ApplicationJSON, + false, + ) + if err != nil { + return err + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Pods.Lists.New( + ctx, + agentmail.PodListNewParamsType(cmd.Value("type").(string)), + params, + options..., + ) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + transform := cmd.Root().String("transform") + return ShowJSON(os.Stdout, "pods:lists create", obj, format, transform) +} + +func handlePodsListsRetrieve(ctx context.Context, cmd *cli.Command) error { + client := agentmail.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("entry") && len(unusedArgs) > 0 { + cmd.Set("entry", unusedArgs[0]) + unusedArgs = unusedArgs[1:] + } + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + + params := agentmail.PodListGetParams{ + PodID: cmd.Value("pod-id").(string), + Direction: agentmail.PodListGetParamsDirection(cmd.Value("direction").(string)), + Type: agentmail.PodListGetParamsType(cmd.Value("type").(string)), + } + + options, err := flagOptions( + cmd, + apiquery.NestedQueryFormatBrackets, + apiquery.ArrayQueryFormatComma, + EmptyBody, + false, + ) + if err != nil { + return err + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Pods.Lists.Get( + ctx, + cmd.Value("entry").(string), + params, + options..., + ) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + transform := cmd.Root().String("transform") + return ShowJSON(os.Stdout, "pods:lists retrieve", obj, format, transform) +} + +func handlePodsListsList(ctx context.Context, cmd *cli.Command) error { + client := agentmail.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("type") && len(unusedArgs) > 0 { + cmd.Set("type", unusedArgs[0]) + unusedArgs = unusedArgs[1:] + } + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + + params := agentmail.PodListListParams{ + PodID: cmd.Value("pod-id").(string), + Direction: agentmail.PodListListParamsDirection(cmd.Value("direction").(string)), + } + + options, err := flagOptions( + cmd, + apiquery.NestedQueryFormatBrackets, + apiquery.ArrayQueryFormatComma, + EmptyBody, + false, + ) + if err != nil { + return err + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Pods.Lists.List( + ctx, + agentmail.PodListListParamsType(cmd.Value("type").(string)), + params, + options..., + ) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + transform := cmd.Root().String("transform") + return ShowJSON(os.Stdout, "pods:lists list", obj, format, transform) +} + +func handlePodsListsDelete(ctx context.Context, cmd *cli.Command) error { + client := agentmail.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("entry") && len(unusedArgs) > 0 { + cmd.Set("entry", unusedArgs[0]) + unusedArgs = unusedArgs[1:] + } + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + + params := agentmail.PodListDeleteParams{ + PodID: cmd.Value("pod-id").(string), + Direction: agentmail.PodListDeleteParamsDirection(cmd.Value("direction").(string)), + Type: agentmail.PodListDeleteParamsType(cmd.Value("type").(string)), + } + + options, err := flagOptions( + cmd, + apiquery.NestedQueryFormatBrackets, + apiquery.ArrayQueryFormatComma, + EmptyBody, + false, + ) + if err != nil { + return err + } + + return client.Pods.Lists.Delete( + ctx, + cmd.Value("entry").(string), + params, + options..., + ) +} diff --git a/pkg/cmd/podlist_test.go b/pkg/cmd/podlist_test.go new file mode 100644 index 0000000..48521a4 --- /dev/null +++ b/pkg/cmd/podlist_test.go @@ -0,0 +1,86 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "testing" + + "github.com/agentmail-to/agentmail-cli/internal/mocktest" +) + +func TestPodsListsCreate(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "pods:lists", "create", + "--pod-id", "pod_id", + "--direction", "send", + "--type", "allow", + "--entry", "entry", + "--reason", "reason", + ) + }) + + t.Run("piping data", func(t *testing.T) { + // Test piping YAML data over stdin + pipeData := []byte("" + + "entry: entry\n" + + "reason: reason\n") + mocktest.TestRunMockTestWithPipeAndFlags( + t, pipeData, + "--api-key", "string", + "pods:lists", "create", + "--pod-id", "pod_id", + "--direction", "send", + "--type", "allow", + ) + }) +} + +func TestPodsListsRetrieve(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "pods:lists", "retrieve", + "--pod-id", "pod_id", + "--direction", "send", + "--type", "allow", + "--entry", "entry", + ) + }) +} + +func TestPodsListsList(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "pods:lists", "list", + "--pod-id", "pod_id", + "--direction", "send", + "--type", "allow", + "--limit", "0", + "--page-token", "page_token", + ) + }) +} + +func TestPodsListsDelete(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "pods:lists", "delete", + "--pod-id", "pod_id", + "--direction", "send", + "--type", "allow", + "--entry", "entry", + ) + }) +} diff --git a/pkg/cmd/podmetric.go b/pkg/cmd/podmetric.go new file mode 100644 index 0000000..88579b1 --- /dev/null +++ b/pkg/cmd/podmetric.go @@ -0,0 +1,103 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "context" + "fmt" + "os" + + "github.com/agentmail-to/agentmail-cli/internal/apiquery" + "github.com/agentmail-to/agentmail-cli/internal/requestflag" + "github.com/agentmail-to/agentmail-go" + "github.com/agentmail-to/agentmail-go/option" + "github.com/tidwall/gjson" + "github.com/urfave/cli/v3" +) + +var podsMetricsQuery = cli.Command{ + Name: "query", + Usage: "**CLI:**", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "pod-id", + Usage: "ID of pod.", + Required: true, + }, + &requestflag.Flag[any]{ + Name: "descending", + Usage: "Sort in descending order.", + QueryPath: "descending", + }, + &requestflag.Flag[any]{ + Name: "end", + Usage: "End timestamp for the query.", + QueryPath: "end", + }, + &requestflag.Flag[any]{ + Name: "event-type", + Usage: "List of metric event types to query.", + QueryPath: "event_types", + }, + &requestflag.Flag[any]{ + Name: "limit", + Usage: "Limit on number of buckets to return.", + QueryPath: "limit", + }, + &requestflag.Flag[any]{ + Name: "period", + Usage: "Period in number of seconds for the query.", + QueryPath: "period", + }, + &requestflag.Flag[any]{ + Name: "start", + Usage: "Start timestamp for the query.", + QueryPath: "start", + }, + }, + Action: handlePodsMetricsQuery, + HideHelpCommand: true, +} + +func handlePodsMetricsQuery(ctx context.Context, cmd *cli.Command) error { + client := agentmail.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("pod-id") && len(unusedArgs) > 0 { + cmd.Set("pod-id", unusedArgs[0]) + unusedArgs = unusedArgs[1:] + } + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + + params := agentmail.PodMetricQueryParams{} + + options, err := flagOptions( + cmd, + apiquery.NestedQueryFormatBrackets, + apiquery.ArrayQueryFormatComma, + EmptyBody, + false, + ) + if err != nil { + return err + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Pods.Metrics.Query( + ctx, + cmd.Value("pod-id").(string), + params, + options..., + ) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + transform := cmd.Root().String("transform") + return ShowJSON(os.Stdout, "pods:metrics query", obj, format, transform) +} diff --git a/pkg/cmd/podmetric_test.go b/pkg/cmd/podmetric_test.go new file mode 100644 index 0000000..408bccc --- /dev/null +++ b/pkg/cmd/podmetric_test.go @@ -0,0 +1,27 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "testing" + + "github.com/agentmail-to/agentmail-cli/internal/mocktest" +) + +func TestPodsMetricsQuery(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "pods:metrics", "query", + "--pod-id", "pod_id", + "--descending=true", + "--end", "'2019-12-27T18:11:19.117Z'", + "--event-type", "[message.sent]", + "--limit", "0", + "--period", "period", + "--start", "'2019-12-27T18:11:19.117Z'", + ) + }) +} diff --git a/pkg/cmd/podthread.go b/pkg/cmd/podthread.go index 7a86318..dfe7918 100644 --- a/pkg/cmd/podthread.go +++ b/pkg/cmd/podthread.go @@ -17,7 +17,7 @@ import ( var podsThreadsRetrieve = cli.Command{ Name: "retrieve", - Usage: "Get Thread", + Usage: "**CLI:**", Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ @@ -37,7 +37,7 @@ var podsThreadsRetrieve = cli.Command{ var podsThreadsList = cli.Command{ Name: "list", - Usage: "List Threads", + Usage: "**CLI:**", Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ @@ -95,9 +95,34 @@ var podsThreadsList = cli.Command{ HideHelpCommand: true, } +var podsThreadsDelete = cli.Command{ + Name: "delete", + Usage: "Moves the thread to trash by adding a trash label to all messages. If the thread\nis already in trash, it will be permanently deleted. Use `permanent=true` to\nforce permanent deletion.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "pod-id", + Usage: "ID of pod.", + Required: true, + }, + &requestflag.Flag[string]{ + Name: "thread-id", + Usage: "ID of thread.", + Required: true, + }, + &requestflag.Flag[any]{ + Name: "permanent", + Usage: "If true, permanently delete the thread instead of moving to trash.", + QueryPath: "permanent", + }, + }, + Action: handlePodsThreadsDelete, + HideHelpCommand: true, +} + var podsThreadsGetAttachment = cli.Command{ Name: "get-attachment", - Usage: "Get Attachment", + Usage: "**CLI:**", Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ @@ -206,6 +231,40 @@ func handlePodsThreadsList(ctx context.Context, cmd *cli.Command) error { return ShowJSON(os.Stdout, "pods:threads list", obj, format, transform) } +func handlePodsThreadsDelete(ctx context.Context, cmd *cli.Command) error { + client := agentmail.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("thread-id") && len(unusedArgs) > 0 { + cmd.Set("thread-id", unusedArgs[0]) + unusedArgs = unusedArgs[1:] + } + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + + params := agentmail.PodThreadDeleteParams{ + PodID: cmd.Value("pod-id").(string), + } + + options, err := flagOptions( + cmd, + apiquery.NestedQueryFormatBrackets, + apiquery.ArrayQueryFormatComma, + EmptyBody, + false, + ) + if err != nil { + return err + } + + return client.Pods.Threads.Delete( + ctx, + cmd.Value("thread-id").(string), + params, + options..., + ) +} + func handlePodsThreadsGetAttachment(ctx context.Context, cmd *cli.Command) error { client := agentmail.NewClient(getDefaultRequestOptions(cmd)...) unusedArgs := cmd.Args().Slice() diff --git a/pkg/cmd/podthread_test.go b/pkg/cmd/podthread_test.go index 311603b..ff48113 100644 --- a/pkg/cmd/podthread_test.go +++ b/pkg/cmd/podthread_test.go @@ -42,6 +42,20 @@ func TestPodsThreadsList(t *testing.T) { }) } +func TestPodsThreadsDelete(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "pods:threads", "delete", + "--pod-id", "pod_id", + "--thread-id", "thread_id", + "--permanent=true", + ) + }) +} + func TestPodsThreadsGetAttachment(t *testing.T) { t.Skip("Mock server tests are disabled") t.Run("regular flags", func(t *testing.T) { diff --git a/pkg/cmd/thread.go b/pkg/cmd/thread.go index 8dc42be..89f7fc4 100644 --- a/pkg/cmd/thread.go +++ b/pkg/cmd/thread.go @@ -17,7 +17,7 @@ import ( var threadsRetrieve = cli.Command{ Name: "retrieve", - Usage: "Get Thread", + Usage: "**CLI:**", Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ @@ -32,7 +32,7 @@ var threadsRetrieve = cli.Command{ var threadsList = cli.Command{ Name: "list", - Usage: "List Threads", + Usage: "**CLI:**", Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[any]{ @@ -85,9 +85,29 @@ var threadsList = cli.Command{ HideHelpCommand: true, } +var threadsDelete = cli.Command{ + Name: "delete", + Usage: "Moves the thread to trash by adding a trash label to all messages. If the thread\nis already in trash, it will be permanently deleted. Use `permanent=true` to\nforce permanent deletion.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "thread-id", + Usage: "ID of thread.", + Required: true, + }, + &requestflag.Flag[any]{ + Name: "permanent", + Usage: "If true, permanently delete the thread instead of moving to trash.", + QueryPath: "permanent", + }, + }, + Action: handleThreadsDelete, + HideHelpCommand: true, +} + var threadsRetrieveAttachment = cli.Command{ Name: "retrieve-attachment", - Usage: "Get Attachment", + Usage: "**CLI:**", Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ @@ -174,6 +194,38 @@ func handleThreadsList(ctx context.Context, cmd *cli.Command) error { return ShowJSON(os.Stdout, "threads list", obj, format, transform) } +func handleThreadsDelete(ctx context.Context, cmd *cli.Command) error { + client := agentmail.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("thread-id") && len(unusedArgs) > 0 { + cmd.Set("thread-id", unusedArgs[0]) + unusedArgs = unusedArgs[1:] + } + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + + params := agentmail.ThreadDeleteParams{} + + options, err := flagOptions( + cmd, + apiquery.NestedQueryFormatBrackets, + apiquery.ArrayQueryFormatComma, + EmptyBody, + false, + ) + if err != nil { + return err + } + + return client.Threads.Delete( + ctx, + cmd.Value("thread-id").(string), + params, + options..., + ) +} + func handleThreadsRetrieveAttachment(ctx context.Context, cmd *cli.Command) error { client := agentmail.NewClient(getDefaultRequestOptions(cmd)...) unusedArgs := cmd.Args().Slice() diff --git a/pkg/cmd/thread_test.go b/pkg/cmd/thread_test.go index 6053f05..8541fa4 100644 --- a/pkg/cmd/thread_test.go +++ b/pkg/cmd/thread_test.go @@ -40,6 +40,19 @@ func TestThreadsList(t *testing.T) { }) } +func TestThreadsDelete(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "threads", "delete", + "--thread-id", "thread_id", + "--permanent=true", + ) + }) +} + func TestThreadsRetrieveAttachment(t *testing.T) { t.Skip("Mock server tests are disabled") t.Run("regular flags", func(t *testing.T) { diff --git a/pkg/cmd/version.go b/pkg/cmd/version.go index cf159c1..d920f91 100644 --- a/pkg/cmd/version.go +++ b/pkg/cmd/version.go @@ -2,4 +2,4 @@ package cmd -const Version = "0.7.7" // x-release-please-version +const Version = "0.7.8" // x-release-please-version diff --git a/pkg/cmd/webhook.go b/pkg/cmd/webhook.go index 678c8cd..a344ef2 100644 --- a/pkg/cmd/webhook.go +++ b/pkg/cmd/webhook.go @@ -17,7 +17,7 @@ import ( var webhooksCreate = cli.Command{ Name: "create", - Usage: "Create Webhook", + Usage: "**CLI:**", Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[[]string]{ @@ -54,7 +54,7 @@ var webhooksCreate = cli.Command{ var webhooksRetrieve = cli.Command{ Name: "retrieve", - Usage: "Get Webhook", + Usage: "**CLI:**", Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ @@ -69,7 +69,7 @@ var webhooksRetrieve = cli.Command{ var webhooksUpdate = cli.Command{ Name: "update", - Usage: "Update Webhook", + Usage: "**CLI:**", Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ @@ -104,7 +104,7 @@ var webhooksUpdate = cli.Command{ var webhooksList = cli.Command{ Name: "list", - Usage: "List Webhooks", + Usage: "**CLI:**", Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[any]{ @@ -129,7 +129,7 @@ var webhooksList = cli.Command{ var webhooksDelete = cli.Command{ Name: "delete", - Usage: "Delete Webhook", + Usage: "**CLI:**", Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{