diff --git a/.github/actions/setup-go/action.yml b/.github/actions/setup-go/action.yml index d847169..56e8dbe 100644 --- a/.github/actions/setup-go/action.yml +++ b/.github/actions/setup-go/action.yml @@ -7,7 +7,7 @@ inputs: runs: using: composite steps: - - uses: stainless-api/retrieve-github-access-token@v1 + - uses: stainless-api/retrieve-github-access-token@1f03f929b746c5b03dcdafa2bebbb18ca5672e1a # v1.0.0 if: github.repository == 'stainless-sdks/zavudev-cli' id: get_token with: @@ -20,7 +20,7 @@ runs: run: git config --global url."https://x-access-token:${{ steps.get_token.outputs.github_access_token }}@github.com/stainless-sdks/zavudev-go".insteadOf "https://github.com/stainless-sdks/zavudev-go" - name: Setup go - uses: actions/setup-go@v5 + uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0 with: go-version-file: ./go.mod diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e7c6b36..060b69f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,11 +21,11 @@ jobs: lint: timeout-minutes: 10 name: lint - runs-on: ${{ github.repository == 'stainless-sdks/zavudev-cli' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} + runs-on: ${{ startsWith(github.repository, 'stainless-sdks/') && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} if: (github.event_name == 'push' || github.event.pull_request.head.repo.fork) && (github.event_name != 'push' || github.event.head_commit.message != 'codegen metadata') steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: ./.github/actions/setup-go with: @@ -48,10 +48,10 @@ jobs: permissions: contents: read id-token: write - runs-on: ${{ github.repository == 'stainless-sdks/zavudev-cli' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} + runs-on: ${{ startsWith(github.repository, 'stainless-sdks/') && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} if: (github.event_name == 'push' || github.event.pull_request.head.repo.fork) && (github.event_name != 'push' || github.event.head_commit.message != 'codegen metadata') steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: ./.github/actions/setup-go with: @@ -66,7 +66,7 @@ jobs: run: ./scripts/bootstrap - name: Run goreleaser - uses: goreleaser/goreleaser-action@v6.1.0 + uses: goreleaser/goreleaser-action@9ed2f89a662bf1735a48bc8557fd212fa902bebf # v6.1.0 with: version: latest args: release --snapshot --clean --skip=publish @@ -78,7 +78,7 @@ jobs: github.repository == 'stainless-sdks/zavudev-cli' && !startsWith(github.ref, 'refs/heads/stl/') id: github-oidc - uses: actions/github-script@v8 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 with: script: core.setOutput('github_token', await core.getIDToken()); @@ -95,10 +95,10 @@ jobs: test: timeout-minutes: 10 name: test - runs-on: ${{ github.repository == 'stainless-sdks/zavudev-cli' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} + runs-on: ${{ startsWith(github.repository, 'stainless-sdks/') && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} if: github.event_name == 'push' || github.event.pull_request.head.repo.fork steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: ./.github/actions/setup-go with: diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index 49983f5..40c07b2 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -10,20 +10,21 @@ on: push: tags: - "v*" + workflow_dispatch: {} jobs: goreleaser: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 - name: Set up Go - uses: actions/setup-go@v5 + uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0 with: go-version-file: "go.mod" - name: Run GoReleaser - uses: goreleaser/goreleaser-action@v6.1.0 + uses: goreleaser/goreleaser-action@9ed2f89a662bf1735a48bc8557fd212fa902bebf # v6.1.0 with: version: latest args: release --clean diff --git a/.github/workflows/release-doctor.yml b/.github/workflows/release-doctor.yml index 09a93d7..0ea3b8f 100644 --- a/.github/workflows/release-doctor.yml +++ b/.github/workflows/release-doctor.yml @@ -12,7 +12,7 @@ jobs: if: github.repository == 'zavudev/cli' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || startsWith(github.head_ref, 'release-please') || github.head_ref == 'next') steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Check release environment run: | diff --git a/.release-please-manifest.json b/.release-please-manifest.json index da59f99..2aca35a 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.4.0" + ".": "0.5.0" } \ No newline at end of file diff --git a/.stats.yml b/.stats.yml index aad1a14..ddb1fe8 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 77 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/zavu%2Fzavudev-d264cd6c8b053a313afd2c81d2a85d5c0053889ec50e81acd8c4f0db93b9e0d3.yml -openapi_spec_hash: 721c392cd496ecce25354e9f8220638e -config_hash: 866b537cf536cf8cb91b44e437fa812d +configured_endpoints: 183 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/zavu/zavudev-5d75bbdcac3a0b0a8b798c0b4ef0dacfbc64ad4a20605489dc46dda7c8d1d8d8.yml +openapi_spec_hash: bb242cfd8cc43354412164c2dceae89c +config_hash: 261e1b852ca7f8364e4a283a3edd350a diff --git a/CHANGELOG.md b/CHANGELOG.md index a289dc5..f7b1aa8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,49 @@ # Changelog +## 0.5.0 (2026-09-08) + +Full Changelog: [v0.4.0...v0.5.0](https://github.com/zavudev/cli/compare/v0.4.0...v0.5.0) + +### Features + +* **api:** api update ([8f3ded7](https://github.com/zavudev/cli/commit/8f3ded7ce92478656e1bf174d4eb7eb8fe0758ae)) +* **api:** api update ([9890852](https://github.com/zavudev/cli/commit/9890852702362db1dc22caddb327cdda28aecd89)) +* **api:** api update ([036bc77](https://github.com/zavudev/cli/commit/036bc772adbb059b148c9737cfa43e50cea5b734)) +* **api:** api update ([4879896](https://github.com/zavudev/cli/commit/48798969f5e101f3ff5b033046012351dc96d10d)) +* **api:** api update ([246f57f](https://github.com/zavudev/cli/commit/246f57f9419a5a7d4e386534724c4907a3160d99)) +* **api:** api update ([e114f18](https://github.com/zavudev/cli/commit/e114f18311f9362822f1410c7ac98e1c11ab7ac0)) +* **api:** api update ([d9200b0](https://github.com/zavudev/cli/commit/d9200b09fa86ca1c86f2ae140a2b2f22554840a9)) +* **api:** api update ([0a0f73f](https://github.com/zavudev/cli/commit/0a0f73f9f8a329e1370c2787c27904752e6ef801)) +* **api:** api update ([771f45e](https://github.com/zavudev/cli/commit/771f45e52871e2e8499b5477b9f19c04e4167a1d)) +* **api:** api update ([ace0081](https://github.com/zavudev/cli/commit/ace00816df72353a38c8d9427988c61b21631528)) +* **api:** api update ([00f5deb](https://github.com/zavudev/cli/commit/00f5deb659ce9dca56f617f547d31ef461bb0f7f)) +* **api:** api update ([6126b7e](https://github.com/zavudev/cli/commit/6126b7ed48909667c2f3b2979332602b7b5b154c)) +* **api:** api update ([bcb63ef](https://github.com/zavudev/cli/commit/bcb63ef6b1b4e3f5fad06d07650c34f8bd3450d3)) +* **api:** manual updates ([9a299ae](https://github.com/zavudev/cli/commit/9a299ae7b231a95ed973e60d1f66dcaeec50422d)) +* **api:** manual updates ([c40c4cb](https://github.com/zavudev/cli/commit/c40c4cb409948540bb7feef81b0ae11de056e7f5)) +* **cli:** add `--raw-output`/`-r` option to print raw (non-JSON) strings ([d764186](https://github.com/zavudev/cli/commit/d76418669cafbae10aedd4c7e7d42f3fa356cf20)) +* **cli:** alias parameters in data with `x-stainless-cli-data-alias` ([3711e06](https://github.com/zavudev/cli/commit/3711e0654e91a0345effe9b901f8b8635fbe0c62)) +* **cli:** send filename and content type when reading input from files ([82faa3c](https://github.com/zavudev/cli/commit/82faa3cc225b7e931602af05d49a1482d42d0fcd)) +* support passing path and query params over stdin ([b8f994d](https://github.com/zavudev/cli/commit/b8f994dc81af29fb1526cbe06cfc7704e886b573)) + + +### Bug Fixes + +* **cli:** correctly load zsh autocompletion ([a29fdc5](https://github.com/zavudev/cli/commit/a29fdc57fec69b50adf10f5ca9d76da5267c60cc)) +* flags for nullable body scalar fields are strictly typed ([f71454c](https://github.com/zavudev/cli/commit/f71454c4db5b528adceda8850a41857a80da27d7)) + + +### Chores + +* add documentation for ./scripts/link ([df7e71b](https://github.com/zavudev/cli/commit/df7e71b731758877a22074668862d865d71603eb)) +* **ci:** support manually triggering release workflow ([0c7441d](https://github.com/zavudev/cli/commit/0c7441d9ef8c2da629a88ba8fc750319ca4e6225)) +* **cli:** fall back to JSON when using default "explore" with non-TTY ([b9b7c60](https://github.com/zavudev/cli/commit/b9b7c6049f69268590f059883babc559076e5945)) +* **cli:** switch long lists of positional args over to param structs ([c97dd87](https://github.com/zavudev/cli/commit/c97dd8703f6ca089d3c29307eab81e23bec7c1d4)) +* **cli:** use `ShowJSONOpts` as argument to `formatJSON` instead of many positionals ([c0c6221](https://github.com/zavudev/cli/commit/c0c62210622f72686086063f315649eb8bc35ec4)) +* configure new SDK language ([f703781](https://github.com/zavudev/cli/commit/f7037818453dadaac1a379e1fce5d78179c93ef1)) +* **internal:** more robust bootstrap script ([873650f](https://github.com/zavudev/cli/commit/873650f94169cc8ee552d5dab9e426b6b1007fa8)) +* redact api-key headers in debug logs ([4a93079](https://github.com/zavudev/cli/commit/4a93079e546fcc9437520a3d9d72a1c8fc49a5e1)) + ## 0.4.0 (2026-04-12) Full Changelog: [v0.3.0...v0.4.0](https://github.com/zavudev/cli/compare/v0.3.0...v0.4.0) diff --git a/README.md b/README.md index 5afe42f..2e0ae2f 100644 --- a/README.md +++ b/README.md @@ -119,3 +119,23 @@ base64-encoding). Note that absolute paths will begin with `@file://` or ```bash zavudev --arg @data://file.txt ``` + +## Linking different Go SDK versions + +You can link the CLI against a different version of the Zavudev Go SDK +for development purposes using the `./scripts/link` script. + +To link to a specific version from a repository (version can be a branch, +git tag, or commit hash): + +```bash +./scripts/link github.com/org/repo@version +``` + +To link to a local copy of the SDK: + +```bash +./scripts/link ../path/to/zavudev-go +``` + +If you run the link script without any arguments, it will default to `../zavudev-go`. diff --git a/cmd/zavudev/main.go b/cmd/zavudev/main.go index 0c097f4..d7974c5 100644 --- a/cmd/zavudev/main.go +++ b/cmd/zavudev/main.go @@ -43,7 +43,12 @@ func main() { fmt.Fprintf(os.Stderr, "%s %q: %d %s\n", apierr.Request.Method, apierr.Request.URL, apierr.Response.StatusCode, http.StatusText(apierr.Response.StatusCode)) format := app.String("format-error") json := gjson.Parse(apierr.RawJSON()) - show_err := cmd.ShowJSON(os.Stdout, "Error", json, format, app.String("transform-error")) + show_err := cmd.ShowJSON(json, cmd.ShowJSONOpts{ + ExplicitFormat: app.IsSet("format-error"), + Format: format, + Title: "Error", + Transform: app.String("transform-error"), + }) if show_err != nil { // Just print the original error: fmt.Fprintf(os.Stderr, "%s\n", err.Error()) diff --git a/go.mod b/go.mod index a0b6e1e..f661d68 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,7 @@ require ( github.com/tidwall/pretty v1.2.1 github.com/urfave/cli-docs/v3 v3.0.0-alpha6 github.com/urfave/cli/v3 v3.3.2 - github.com/zavudev/sdk-go v0.3.0 + github.com/zavudev/sdk-go v0.4.0 golang.org/x/sys v0.38.0 ) diff --git a/go.sum b/go.sum index 2bc85b4..067c6b1 100644 --- a/go.sum +++ b/go.sum @@ -71,8 +71,8 @@ github.com/urfave/cli/v3 v3.3.2 h1:BYFVnhhZ8RqT38DxEYVFPPmGFTEf7tJwySTXsVRrS/o= github.com/urfave/cli/v3 v3.3.2/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= -github.com/zavudev/sdk-go v0.3.0 h1:lIPS6cTM0bQMbGG5+CjL3wV80+r6iLlWLnnv4BYRNXs= -github.com/zavudev/sdk-go v0.3.0/go.mod h1:ncdVbaf7JHHiUcpJu2JE3c1xion5lkWSAUCn5CnX3bo= +github.com/zavudev/sdk-go v0.4.0 h1:OpPmvyDZVLF8y7Gc6tHB3JHhqRPy8aZMzVB6cOaeSPE= +github.com/zavudev/sdk-go v0.4.0/go.mod h1:ncdVbaf7JHHiUcpJu2JE3c1xion5lkWSAUCn5CnX3bo= golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E= golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= diff --git a/internal/autocomplete/shellscripts/zsh_autocomplete.zsh b/internal/autocomplete/shellscripts/zsh_autocomplete.zsh index 4d4bdcd..d937171 100644 --- a/internal/autocomplete/shellscripts/zsh_autocomplete.zsh +++ b/internal/autocomplete/shellscripts/zsh_autocomplete.zsh @@ -1,5 +1,4 @@ -#!/bin/zsh -compdef ____APPNAME___zsh_autocomplete __APPNAME__ +#compdef __APPNAME__ ____APPNAME___zsh_autocomplete() { @@ -44,3 +43,14 @@ ____APPNAME___zsh_autocomplete() { ;; esac } + +# When installed in fpath (e.g., via Homebrew's zsh_completion stanza), this file +# is autoloaded as the function ___APPNAME__ and its body becomes that function's +# body. Detect that case via funcstack and dispatch to the completion function. +# When sourced (e.g., `source <(__APPNAME__ @completion zsh)`), register the +# function with compdef instead. +if [[ "${funcstack[1]}" = "___APPNAME__" ]]; then + ____APPNAME___zsh_autocomplete "$@" +else + compdef ____APPNAME___zsh_autocomplete __APPNAME__ +fi diff --git a/internal/debugmiddleware/debug_middleware.go b/internal/debugmiddleware/debug_middleware.go index f07b93b..647f1de 100644 --- a/internal/debugmiddleware/debug_middleware.go +++ b/internal/debugmiddleware/debug_middleware.go @@ -21,7 +21,12 @@ const redactedPlaceholder = "" // Headers known to contain sensitive information like an API key. Note that this exclude `Authorization`, // which is handled specially in `redactRequest` below. -var sensitiveHeaders = []string{} +var sensitiveHeaders = []string{ + "api-key", + "x-api-key", + "cookie", + "set-cookie", +} // RequestLogger is a middleware that logs HTTP requests and responses. type RequestLogger struct { diff --git a/internal/requestflag/innerflag.go b/internal/requestflag/innerflag.go index 102624f..528915f 100644 --- a/internal/requestflag/innerflag.go +++ b/internal/requestflag/innerflag.go @@ -14,7 +14,8 @@ import ( type InnerFlag[ T []any | []map[string]any | []DateTimeValue | []DateValue | []TimeValue | []string | []float64 | []int64 | []bool | any | map[string]any | DateTimeValue | DateValue | TimeValue | - string | float64 | int64 | bool, + string | float64 | int64 | bool | + *string | *float64 | *int64 | *bool | *DateTimeValue | *DateValue | *TimeValue, ] struct { Name string // name of the flag DefaultText string // default text of the flag for usage purposes @@ -22,14 +23,35 @@ type InnerFlag[ Aliases []string // aliases that are allowed for this flag Validator func(T) error // custom function to validate this flag value - OuterFlag cli.Flag // The flag on which this inner flag will set values - InnerField string // The inner field which this flag will set + OuterFlag cli.Flag // The flag on which this inner flag will set values + InnerField string // The inner field which this flag will set + DataAliases []string // alternate names recognized in YAML values passed as the outer flag + + // OuterIsArrayOfObjects tells an untyped outer flag (Flag[any], used for nullable + // complex schemas) to seed its underlying value as []map[string]any rather than + // map[string]any before SetInnerField runs. The hint is ignored for typed outer + // flags whose zero value already carries a dispatchable reflect.Kind. + OuterIsArrayOfObjects bool +} + +// GetDataAliases returns the aliases recognized when parsing inner field keys from piped or flag YAML. +func (f *InnerFlag[T]) GetDataAliases() []string { + return f.DataAliases +} + +// GetInnerField returns the API field name that this inner flag sets on its outer flag's value. +// For example, the flag --parent.foo targeting a parameter whose OpenAPI property name is "foo" +// would return "foo". This is distinct from the flag's CLI name and from any DataAliases entries. +func (f *InnerFlag[T]) GetInnerField() string { + return f.InnerField } type HasOuterFlag interface { cli.Flag SetOuterFlag(cli.Flag) GetOuterFlag() cli.Flag + GetInnerField() string + GetDataAliases() []string } func (f *InnerFlag[T]) SetOuterFlag(flag cli.Flag) { @@ -61,6 +83,10 @@ func (f *InnerFlag[T]) Set(name string, rawVal string) error { } } + if seeder, ok := f.OuterFlag.(InnerFieldSeeder); ok { + seeder.SeedInnerCollection(f.OuterIsArrayOfObjects) + } + if settableInnerField, ok := f.OuterFlag.(SettableInnerField); ok { settableInnerField.SetInnerField(f.InnerField, parsedValue) } else { @@ -121,6 +147,9 @@ func (f *InnerFlag[T]) TypeName() string { if ty == nil { return "" } + if ty.Kind() == reflect.Pointer { + ty = ty.Elem() + } // Get base type name with special handling for built-in types getTypeName := func(t reflect.Type) string { diff --git a/internal/requestflag/requestflag.go b/internal/requestflag/requestflag.go index bdef64f..77c4f1f 100644 --- a/internal/requestflag/requestflag.go +++ b/internal/requestflag/requestflag.go @@ -1,6 +1,7 @@ package requestflag import ( + "encoding/json" "fmt" "reflect" "strconv" @@ -12,13 +13,38 @@ import ( "github.com/urfave/cli/v3" ) +// formatForFlagSet converts a Go value parsed from YAML/JSON stdin data into a string +// that flag.Set (and thus parseCLIArg) can parse correctly for each flag type. +// Strings are returned as-is (parseCLIArg[string] assigns the raw value directly, so +// JSON-quoting must be avoided). Scalars use %v. Complex types (maps, slices) are +// JSON-encoded, which the yaml.Unmarshal default branch in parseCLIArg can parse. +func formatForFlagSet(val any) (string, error) { + switch v := val.(type) { + case string: + return v, nil + case bool, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64: + return fmt.Sprintf("%v", val), nil + default: + b, err := json.Marshal(val) + if err != nil { + return "", fmt.Errorf("cannot format value %T for flag.Set: %w", val, err) + } + return string(b), nil + } +} + // Flag [T] is a generic flag base which can be used to implement the most // common interfaces used by urfave/cli. Additionally, it allows specifying // where in an HTTP request the flag values should be placed (e.g. query, body, etc.). +// +// Pointer-to-primitive type parameters (e.g. *string) are used for flags whose underlying +// schema is nullable. They give flags a tri-state: unset (excluded from the request), +// set to the literal "null" (nil pointer → JSON null), or set to a value (*v → JSON value). type Flag[ T []any | []map[string]any | []DateTimeValue | []DateValue | []TimeValue | []string | []float64 | []int64 | []bool | any | map[string]any | DateTimeValue | DateValue | TimeValue | - string | float64 | int64 | bool, + string | float64 | int64 | bool | + *string | *float64 | *int64 | *bool | *DateTimeValue | *DateValue | *TimeValue, ] struct { Name string // name of the flag Category string // category of the flag, if any @@ -36,6 +62,7 @@ type Flag[ HeaderPath string // location in the request header to put this flag's value BodyPath string // location in the request body to put this flag's value BodyRoot bool // if true, then use this value as the entire request body + PathParam string // name of the URL path parameter this flag's value maps to // Const, when true, marks this flag as a constant. The flag's Default value is used as the fixed value // and always included in the request (IsSet returns true). The user can still see and override the flag, @@ -48,6 +75,10 @@ type Flag[ // binary` in the OpenAPI spec. FileInput bool + // DataAliases is a list of alternate names for this parameter recognized when parsing piped YAML/JSON + // input. Values keyed by any alias are translated to the canonical API name before being sent. + DataAliases []string + // 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 @@ -63,8 +94,10 @@ type InRequest interface { GetQueryPath() string GetHeaderPath() string GetBodyPath() string + GetPathParam() string IsBodyRoot() bool IsFileInput() bool + GetDataAliases() []string } func (f Flag[T]) GetQueryPath() string { @@ -79,6 +112,10 @@ func (f Flag[T]) GetBodyPath() string { return f.BodyPath } +func (f Flag[T]) GetPathParam() string { + return f.PathParam +} + func (f Flag[T]) IsBodyRoot() bool { return f.BodyRoot } @@ -87,6 +124,10 @@ func (f Flag[T]) IsFileInput() bool { return f.FileInput } +func (f Flag[T]) GetDataAliases() []string { + return f.DataAliases +} + // The values that will be sent in different parts of a request. type RequestContents struct { Queries map[string]any @@ -94,7 +135,91 @@ type RequestContents struct { Body any } -// Extract query parameters, headers, and body values from command flags. +// ApplyStdinDataToFlags sets flag values from a parsed stdin data map for flags that have not already been +// set via the command line. This allows piped YAML/JSON data to satisfy path, query, and header parameters. +// Body parameters are excluded: they are already handled by the maps.Copy merge in flagOptions. +// For each unset flag, if the parsed data map contains a key matching the flag's QueryPath, HeaderPath, or +// PathParam (or any of its DataAliases), the flag is set to that value via flag.Set. +// +// Inner flags (those with an outer flag) are also handled: if the outer flag's body path key exists in the +// data map and contains a nested map with a key matching the inner flag's field (or aliases), the inner +// flag is set from that nested value. +func ApplyStdinDataToFlags(cmd *cli.Command, data map[string]any) error { + for _, flag := range cmd.Flags { + if flag.IsSet() { + continue + } + + // Handle inner flags: look for their value nested under the outer flag's body path. + if inner, ok := flag.(HasOuterFlag); ok { + outer, outerOk := inner.GetOuterFlag().(InRequest) + if !outerOk || outer.GetBodyPath() == "" { + continue + } + nested, ok := data[outer.GetBodyPath()].(map[string]any) + if !ok { + continue + } + innerField := inner.GetInnerField() + val, found := nested[innerField] + if !found { + for _, alias := range inner.GetDataAliases() { + if alias != "" && alias != innerField { + if v, ok := nested[alias]; ok { + val, found = v, true + break + } + } + } + } + if !found { + continue + } + setVal, err := formatForFlagSet(val) + if err != nil { + return fmt.Errorf("cannot format piped value for flag %q: %w", flag.Names()[0], err) + } + if err := flag.Set(flag.Names()[0], setVal); err != nil { + return fmt.Errorf("cannot set flag %q from piped data: %w", flag.Names()[0], err) + } + continue + } + + inReq, ok := flag.(InRequest) + if !ok { + continue + } + + // Try each request location in turn, checking the canonical path key and all aliases. + // Body params are excluded: they are already handled by the maps.Copy merge in flagOptions. + for _, path := range []string{inReq.GetQueryPath(), inReq.GetHeaderPath(), inReq.GetPathParam()} { + if path == "" { + continue + } + var val any + var found bool + for _, key := range append([]string{path}, inReq.GetDataAliases()...) { + if v, ok := data[key]; ok { + val, found = v, true + break + } + } + if !found { + continue + } + setVal, err := formatForFlagSet(val) + if err != nil { + return fmt.Errorf("cannot format piped value for flag %q: %w", flag.Names()[0], err) + } + if err := flag.Set(flag.Names()[0], setVal); err != nil { + return fmt.Errorf("cannot set flag %q from piped data: %w", flag.Names()[0], err) + } + break + } + } + return nil +} + func ExtractRequestContents(cmd *cli.Command) RequestContents { bodyMap := make(map[string]any) res := RequestContents{ @@ -277,7 +402,7 @@ func (f *Flag[T]) IsRequired() bool { } // Intentionally don't use `f.Required`, because request flags may be passed // over stdin as well as by flag. - if f.BodyPath != "" || f.BodyRoot { + if f.BodyPath != "" || f.BodyRoot || f.PathParam != "" || f.QueryPath != "" || f.HeaderPath != "" { return false } return f.Required @@ -332,6 +457,11 @@ func (f *Flag[T]) TypeName() string { if ty == nil { return "" } + // Deref pointer-typed flags so --help surfaces the pointee kind (e.g. "string"), not + // Go's pointer syntax. + if ty.Kind() == reflect.Pointer { + ty = ty.Elem() + } // Get base type name with special handling for built-in types getTypeName := func(t reflect.Type) string { @@ -387,6 +517,8 @@ func (f *Flag[T]) IsMultiValueFlag() bool { } func (f *Flag[T]) IsBoolFlag() bool { + // Flag[*bool] is deliberately not treated as a bool flag — the pointer form needs an + // explicit value (`--foo true`, `--foo null`) to disambiguate the tri-state. _, isBool := any(f.Default).(bool) return isBool } @@ -410,7 +542,8 @@ func (f Flag[T]) IsLocal() bool { type cliValue[ T []any | []map[string]any | []DateTimeValue | []DateValue | []TimeValue | []string | []float64 | []int64 | []bool | any | map[string]any | DateTimeValue | DateValue | TimeValue | string | - float64 | int64 | bool, + float64 | int64 | bool | + *string | *float64 | *int64 | *bool | *DateTimeValue | *DateValue | *TimeValue, ] struct { value T } @@ -420,12 +553,27 @@ type cliValue[ func parseCLIArg[ T []any | []map[string]any | []DateTimeValue | []DateValue | []TimeValue | []string | []float64 | []int64 | []bool | any | map[string]any | DateTimeValue | DateValue | TimeValue | string | - float64 | int64 | bool, + float64 | int64 | bool | + *string | *float64 | *int64 | *bool | *DateTimeValue | *DateValue | *TimeValue, ](value string) (T, error) { var parsedValue any var err error var empty T + + if value == "null" { + switch any(empty).(type) { + // Pointer-to-primitive: explicit nil gives the tri-state its "null" state + // (unset / null / value). Without this, numeric flags would fail to parse + // "null" and string flags would accept the literal word as a raw value. + case *string, *int64, *float64, *bool, *DateValue, *DateTimeValue, *TimeValue: + return empty, nil + // Maps marshal nil as JSON null natively; short-circuit avoids a YAML round-trip. + case map[string]any: + return empty, nil + } + } + switch any(empty).(type) { case string: parsedValue = value @@ -456,6 +604,48 @@ func parseCLIArg[ parsedValue = t } + // Pointer-to-primitive flags reach here only when `value != "null"`; we parse the + // pointee type and return its address so JSON marshaling emits the underlying value. + case *string: + v := value + parsedValue = &v + case *int64: + var v int64 + v, err = strconv.ParseInt(value, 0, 64) + if err == nil { + parsedValue = &v + } + case *float64: + var v float64 + v, err = strconv.ParseFloat(value, 64) + if err == nil { + parsedValue = &v + } + case *bool: + var v bool + v, err = strconv.ParseBool(value) + if err == nil { + parsedValue = &v + } + case *DateTimeValue: + var dt DateTimeValue + err = (&dt).Parse(value) + if err == nil { + parsedValue = &dt + } + case *DateValue: + var d DateValue + err = (&d).Parse(value) + if err == nil { + parsedValue = &d + } + case *TimeValue: + var t TimeValue + err = (&t).Parse(value) + if err == nil { + parsedValue = &t + } + default: if strings.HasPrefix(value, "@") { // File literals like @file.txt should work here @@ -492,6 +682,13 @@ func parseCLIArg[ } +// Ptr returns a pointer to its argument. It is used to initialize `Default` on pointer-typed +// Flag values, since Go does not allow taking the address of a composite literal's element +// or of an untyped constant. +func Ptr[T any](v T) *T { + return &v +} + // Assuming this string failed to parse as valid YAML, this function will // return true for strings that can reasonably be interpreted as a string literal, // like identifiers (`foo_bar`), UUIDs (`945b2f0c-8e89-487a-b02c-f851c69ea459`), @@ -585,6 +782,15 @@ func (c *cliValue[T]) String() string { // For basic types, use standard string representation return fmt.Sprintf("%v", v) + case *string, *int64, *float64, *bool, *DateTimeValue, *DateValue, *TimeValue: + // Pointer-to-primitive: nil renders as "null" (the CLI literal that produces it); + // non-nil derefs to the pointee's standard representation. + rv := reflect.ValueOf(v) + if rv.IsNil() { + return "null" + } + return fmt.Sprintf("%v", rv.Elem().Interface()) + default: // For complex types, convert to YAML yamlBytes, err := yaml.MarshalWithOptions(c.value, yaml.Flow(true)) @@ -696,6 +902,15 @@ type SettableInnerField interface { SetInnerField(string, any) } +// InnerFieldSeeder lets an InnerFlag prepare its outer flag's underlying value +// before dispatching SetInnerField. This is only meaningful for Flag[any] — +// the codegen output for nullable complex schemas — whose untyped-nil zero +// value would otherwise have no reflect.Kind for the inner-field switch to +// dispatch on. +type InnerFieldSeeder interface { + SeedInnerCollection(isArrayOfObjects bool) +} + func (f *Flag[T]) SetInnerField(field string, val any) { if f.value == nil { f.value = &cliValue[T]{} @@ -709,6 +924,33 @@ func (f *Flag[T]) SetInnerField(field string, val any) { } } +// SeedInnerCollection initializes a Flag[any]'s underlying value as an empty +// map[string]any or []map[string]any so subsequent SetInnerField calls have a +// dispatchable reflect.Kind. For typed Flag[T] this is a no-op: the type +// assertion fails and the existing reflect.Kind on the typed-nil zero value +// already routes correctly. +func (f *Flag[T]) SeedInnerCollection(isArrayOfObjects bool) { + if f.value == nil { + f.value = &cliValue[T]{} + } + cv, ok := f.value.(*cliValue[T]) + if !ok { + return + } + if reflect.ValueOf(cv.value).Kind() != reflect.Invalid { + return + } + if isArrayOfObjects { + if seed, ok := any([]map[string]any{}).(T); ok { + cv.value = seed + } + return + } + if seed, ok := any(map[string]any{}).(T); ok { + cv.value = seed + } +} + func (c *cliValue[T]) SetInnerField(field string, val any) { flagVal := c.value flagValReflect := reflect.ValueOf(flagVal) diff --git a/internal/requestflag/requestflag_test.go b/internal/requestflag/requestflag_test.go index 0e86e07..779bd57 100644 --- a/internal/requestflag/requestflag_test.go +++ b/internal/requestflag/requestflag_test.go @@ -1,6 +1,7 @@ package requestflag import ( + "encoding/json" "fmt" "testing" "time" @@ -616,6 +617,178 @@ func TestYamlHandling(t *testing.T) { }) } +// TestNullLiteralHandling pins how each Flag[T] type handles the literal value "null" +// when passed via the CLI. Pointer-typed flags serialize nil as JSON null, which is how +// nullable body fields (`anyOf: [T, null]` / `{nullable: true}`) let users clear a field +// via `--foo null`. Non-pointer primitive flags treat "null" as a raw value — these are +// non-nullable schemas where explicit null has no API semantics anyway. +func TestNullLiteralHandling(t *testing.T) { + t.Parallel() + + assertJSONBody := func(t *testing.T, value any, expected string) { + t.Helper() + body, err := json.Marshal(map[string]any{"foo": value}) + assert.NoError(t, err) + assert.JSONEq(t, expected, string(body)) + } + + t.Run("Flag[any] null sends JSON null", func(t *testing.T) { + t.Parallel() + cv := &cliValue[any]{} + assert.NoError(t, cv.Set("null")) + assertJSONBody(t, cv.Get(), `{"foo":null}`) + }) + + t.Run("Flag[string] null is the raw string \"null\"", func(t *testing.T) { + t.Parallel() + cv := &cliValue[string]{} + assert.NoError(t, cv.Set("null")) + assertJSONBody(t, cv.Get(), `{"foo":"null"}`) + }) + + t.Run("Flag[int64] null errors", func(t *testing.T) { + t.Parallel() + cv := &cliValue[int64]{} + assert.Error(t, cv.Set("null")) + }) + + t.Run("Flag[*string] null sends JSON null", func(t *testing.T) { + t.Parallel() + cv := &cliValue[*string]{} + assert.NoError(t, cv.Set("null")) + assertJSONBody(t, cv.Get(), `{"foo":null}`) + }) + + t.Run("Flag[*string] value sends the string", func(t *testing.T) { + t.Parallel() + cv := &cliValue[*string]{} + assert.NoError(t, cv.Set("1.1")) + assertJSONBody(t, cv.Get(), `{"foo":"1.1"}`) + }) + + t.Run("Flag[*int64] null sends JSON null", func(t *testing.T) { + t.Parallel() + cv := &cliValue[*int64]{} + assert.NoError(t, cv.Set("null")) + assertJSONBody(t, cv.Get(), `{"foo":null}`) + }) + + t.Run("Flag[*int64] value sends the integer", func(t *testing.T) { + t.Parallel() + cv := &cliValue[*int64]{} + assert.NoError(t, cv.Set("42")) + assertJSONBody(t, cv.Get(), `{"foo":42}`) + }) + + t.Run("Flag[*int64] invalid value errors", func(t *testing.T) { + t.Parallel() + cv := &cliValue[*int64]{} + assert.Error(t, cv.Set("not-an-int")) + }) + + t.Run("Flag[*bool] null sends JSON null", func(t *testing.T) { + t.Parallel() + cv := &cliValue[*bool]{} + assert.NoError(t, cv.Set("null")) + assertJSONBody(t, cv.Get(), `{"foo":null}`) + }) + + t.Run("Flag[*bool] value sends the boolean", func(t *testing.T) { + t.Parallel() + cv := &cliValue[*bool]{} + assert.NoError(t, cv.Set("true")) + assertJSONBody(t, cv.Get(), `{"foo":true}`) + }) + + t.Run("Flag[*float64] null sends JSON null", func(t *testing.T) { + t.Parallel() + cv := &cliValue[*float64]{} + assert.NoError(t, cv.Set("null")) + assertJSONBody(t, cv.Get(), `{"foo":null}`) + }) + + t.Run("Flag[*float64] value sends the float", func(t *testing.T) { + t.Parallel() + cv := &cliValue[*float64]{} + assert.NoError(t, cv.Set("1.5")) + assertJSONBody(t, cv.Get(), `{"foo":1.5}`) + }) + + t.Run("Flag[*float64] invalid value errors", func(t *testing.T) { + t.Parallel() + cv := &cliValue[*float64]{} + assert.Error(t, cv.Set("not-a-float")) + }) + + t.Run("Flag[*DateValue] null sends JSON null", func(t *testing.T) { + t.Parallel() + cv := &cliValue[*DateValue]{} + assert.NoError(t, cv.Set("null")) + assertJSONBody(t, cv.Get(), `{"foo":null}`) + }) + + t.Run("Flag[*DateValue] value sends the date", func(t *testing.T) { + t.Parallel() + cv := &cliValue[*DateValue]{} + assert.NoError(t, cv.Set("2023-05-15")) + assertJSONBody(t, cv.Get(), `{"foo":"2023-05-15"}`) + }) + + t.Run("Flag[*DateValue] invalid value errors", func(t *testing.T) { + t.Parallel() + cv := &cliValue[*DateValue]{} + assert.Error(t, cv.Set("not-a-date")) + }) + + t.Run("Flag[*DateTimeValue] null sends JSON null", func(t *testing.T) { + t.Parallel() + cv := &cliValue[*DateTimeValue]{} + assert.NoError(t, cv.Set("null")) + assertJSONBody(t, cv.Get(), `{"foo":null}`) + }) + + t.Run("Flag[*DateTimeValue] value sends the datetime", func(t *testing.T) { + t.Parallel() + cv := &cliValue[*DateTimeValue]{} + assert.NoError(t, cv.Set("2023-05-15T14:30:45Z")) + assertJSONBody(t, cv.Get(), `{"foo":"2023-05-15T14:30:45Z"}`) + }) + + t.Run("Flag[*DateTimeValue] invalid value errors", func(t *testing.T) { + t.Parallel() + cv := &cliValue[*DateTimeValue]{} + assert.Error(t, cv.Set("not-a-datetime")) + }) + + t.Run("Flag[*TimeValue] null sends JSON null", func(t *testing.T) { + t.Parallel() + cv := &cliValue[*TimeValue]{} + assert.NoError(t, cv.Set("null")) + assertJSONBody(t, cv.Get(), `{"foo":null}`) + }) + + t.Run("Flag[*TimeValue] value sends the time", func(t *testing.T) { + t.Parallel() + cv := &cliValue[*TimeValue]{} + assert.NoError(t, cv.Set("14:30:45")) + assertJSONBody(t, cv.Get(), `{"foo":"14:30:45"}`) + }) + + t.Run("Flag[*TimeValue] invalid value errors", func(t *testing.T) { + t.Parallel() + cv := &cliValue[*TimeValue]{} + assert.Error(t, cv.Set("not-a-time")) + }) + + // Nullable maps don't need pointer wrapping — a nil map already marshals as JSON null. + t.Run("Flag[map[string]any] null sends JSON null", func(t *testing.T) { + t.Parallel() + cv := &cliValue[map[string]any]{} + assert.NoError(t, cv.Set("null")) + assertJSONBody(t, cv.Get(), `{"foo":null}`) + }) +} + func TestFlagTypeNames(t *testing.T) { t.Parallel() @@ -646,3 +819,409 @@ func TestFlagTypeNames(t *testing.T) { }) } } + +// TestInnerFlagDispatchOnUntypedFlag pins inner-flag behavior for `Flag[any]`, +// which is the codegen output for nullable complex schemas (`anyOf: [T, null]` +// or `{nullable: true}`). The untyped-nil zero value carries no reflect.Kind, +// so SetInnerField has nowhere to dispatch the assignment — without explicit +// help the inner-field value silently drops. +func TestInnerFlagDispatchOnUntypedFlag(t *testing.T) { + t.Parallel() + + t.Run("nullable array of objects appends element from inner flag", func(t *testing.T) { + t.Parallel() + outer := &Flag[any]{Name: "mcp-server"} + assert.NoError(t, outer.PreParse()) + + nameFlag := &InnerFlag[string]{ + Name: "mcp-server.name", InnerField: "name", + OuterFlag: outer, OuterIsArrayOfObjects: true, + } + assert.NoError(t, nameFlag.Set("mcp-server.name", "first")) + + body, err := json.Marshal(map[string]any{"foo": outer.Get()}) + assert.NoError(t, err) + assert.JSONEq(t, `{"foo":[{"name":"first"}]}`, string(body)) + }) + + t.Run("nullable object sets field from inner flag", func(t *testing.T) { + t.Parallel() + outer := &Flag[any]{Name: "metadata"} + assert.NoError(t, outer.PreParse()) + + keyFlag := &InnerFlag[string]{ + Name: "metadata.key", InnerField: "key", OuterFlag: outer, + } + assert.NoError(t, keyFlag.Set("metadata.key", "value")) + + body, err := json.Marshal(map[string]any{"foo": outer.Get()}) + assert.NoError(t, err) + assert.JSONEq(t, `{"foo":{"key":"value"}}`, string(body)) + }) + + t.Run("multiple inner flags merge into the trailing element", func(t *testing.T) { + t.Parallel() + outer := &Flag[any]{Name: "mcp-server"} + assert.NoError(t, outer.PreParse()) + + nameFlag := &InnerFlag[string]{ + Name: "mcp-server.name", InnerField: "name", + OuterFlag: outer, OuterIsArrayOfObjects: true, + } + urlFlag := &InnerFlag[string]{ + Name: "mcp-server.url", InnerField: "url", + OuterFlag: outer, OuterIsArrayOfObjects: true, + } + assert.NoError(t, nameFlag.Set("mcp-server.name", "first")) + assert.NoError(t, urlFlag.Set("mcp-server.url", "https://example.com")) + + body, err := json.Marshal(map[string]any{"foo": outer.Get()}) + assert.NoError(t, err) + assert.JSONEq(t, `{"foo":[{"name":"first","url":"https://example.com"}]}`, string(body)) + }) +} + +func TestApplyStdinDataToFlags(t *testing.T) { + t.Parallel() + + t.Run("sets query path flag from piped data", func(t *testing.T) { + t.Parallel() + + flag := &Flag[string]{ + Name: "account-id", + QueryPath: "account_id", + } + assert.NoError(t, flag.PreParse()) + + data := map[string]any{"account_id": "acct_123"} + cmd := &cli.Command{Flags: []cli.Flag{flag}} + assert.NoError(t, ApplyStdinDataToFlags(cmd, data)) + + assert.True(t, flag.IsSet()) + assert.Equal(t, "acct_123", flag.Get()) + }) + + t.Run("sets header path flag from piped data", func(t *testing.T) { + t.Parallel() + + flag := &Flag[string]{ + Name: "idempotency-key", + HeaderPath: "Idempotency-Key", + } + assert.NoError(t, flag.PreParse()) + + data := map[string]any{"Idempotency-Key": "key-xyz"} + cmd := &cli.Command{Flags: []cli.Flag{flag}} + assert.NoError(t, ApplyStdinDataToFlags(cmd, data)) + + assert.True(t, flag.IsSet()) + assert.Equal(t, "key-xyz", flag.Get()) + }) + + t.Run("does not set body path flag from piped data", func(t *testing.T) { + t.Parallel() + + // Body params are handled by the maps.Copy merge in flagOptions, not by ApplyStdinDataToFlags. + flag := &Flag[string]{ + Name: "message", + BodyPath: "message", + } + assert.NoError(t, flag.PreParse()) + + data := map[string]any{"message": "hello world"} + cmd := &cli.Command{Flags: []cli.Flag{flag}} + assert.NoError(t, ApplyStdinDataToFlags(cmd, data)) + + assert.False(t, flag.IsSet()) + }) + + t.Run("does not override flag already set via CLI", func(t *testing.T) { + t.Parallel() + + flag := &Flag[string]{ + Name: "account-id", + QueryPath: "account_id", + } + assert.NoError(t, flag.PreParse()) + assert.NoError(t, flag.Set("account-id", "explicit_value")) + + data := map[string]any{"account_id": "piped_value"} + cmd := &cli.Command{Flags: []cli.Flag{flag}} + assert.NoError(t, ApplyStdinDataToFlags(cmd, data)) + + // The explicitly-set value should win. + assert.Equal(t, "explicit_value", flag.Get()) + }) + + t.Run("sets integer query flag from piped data", func(t *testing.T) { + t.Parallel() + + flag := &Flag[int64]{ + Name: "page-size", + QueryPath: "page_size", + } + assert.NoError(t, flag.PreParse()) + + data := map[string]any{"page_size": int64(50)} + cmd := &cli.Command{Flags: []cli.Flag{flag}} + assert.NoError(t, ApplyStdinDataToFlags(cmd, data)) + + assert.True(t, flag.IsSet()) + assert.Equal(t, int64(50), flag.Get()) + }) + + t.Run("sets boolean query flag from piped data", func(t *testing.T) { + t.Parallel() + + flag := &Flag[bool]{ + Name: "include-deleted", + QueryPath: "include_deleted", + } + assert.NoError(t, flag.PreParse()) + + data := map[string]any{"include_deleted": true} + cmd := &cli.Command{Flags: []cli.Flag{flag}} + assert.NoError(t, ApplyStdinDataToFlags(cmd, data)) + + assert.True(t, flag.IsSet()) + assert.Equal(t, true, flag.Get()) + }) + + t.Run("resolves query path flag via data alias", func(t *testing.T) { + t.Parallel() + + flag := &Flag[string]{ + Name: "account-id", + QueryPath: "account_id", + DataAliases: []string{"accountId", "account"}, + } + assert.NoError(t, flag.PreParse()) + + // Use one of the aliases as the key in piped data. + data := map[string]any{"accountId": "acct_alias"} + cmd := &cli.Command{Flags: []cli.Flag{flag}} + assert.NoError(t, ApplyStdinDataToFlags(cmd, data)) + + assert.True(t, flag.IsSet()) + assert.Equal(t, "acct_alias", flag.Get()) + }) + + t.Run("does not set body path flag via data alias", func(t *testing.T) { + t.Parallel() + + // Body params are handled by the maps.Copy merge in flagOptions, not by ApplyStdinDataToFlags. + flag := &Flag[string]{ + Name: "user-name", + BodyPath: "user_name", + DataAliases: []string{"userName", "username"}, + } + assert.NoError(t, flag.PreParse()) + + data := map[string]any{"userName": "alice"} + cmd := &cli.Command{Flags: []cli.Flag{flag}} + assert.NoError(t, ApplyStdinDataToFlags(cmd, data)) + + assert.False(t, flag.IsSet()) + }) + + t.Run("ignores flags with no matching key in piped data", func(t *testing.T) { + t.Parallel() + + flag := &Flag[string]{ + Name: "account-id", + QueryPath: "account_id", + } + assert.NoError(t, flag.PreParse()) + + data := map[string]any{"other_key": "value"} + cmd := &cli.Command{Flags: []cli.Flag{flag}} + assert.NoError(t, ApplyStdinDataToFlags(cmd, data)) + + assert.False(t, flag.IsSet()) + }) + + t.Run("ignores flags with no path set", func(t *testing.T) { + t.Parallel() + + flag := &Flag[string]{ + Name: "some-flag", + // No QueryPath, HeaderPath, or BodyPath + } + assert.NoError(t, flag.PreParse()) + + data := map[string]any{"some-flag": "value"} + cmd := &cli.Command{Flags: []cli.Flag{flag}} + assert.NoError(t, ApplyStdinDataToFlags(cmd, data)) + + assert.False(t, flag.IsSet()) + }) + + t.Run("handles multiple flags from piped data", func(t *testing.T) { + t.Parallel() + + accountFlag := &Flag[string]{ + Name: "account-id", + QueryPath: "account_id", + } + limitFlag := &Flag[int64]{ + Name: "limit", + QueryPath: "limit", + } + assert.NoError(t, accountFlag.PreParse()) + assert.NoError(t, limitFlag.PreParse()) + + data := map[string]any{ + "account_id": "acct_abc", + "limit": int64(25), + } + cmd := &cli.Command{Flags: []cli.Flag{accountFlag, limitFlag}} + assert.NoError(t, ApplyStdinDataToFlags(cmd, data)) + + assert.True(t, accountFlag.IsSet()) + assert.Equal(t, "acct_abc", accountFlag.Get()) + assert.True(t, limitFlag.IsSet()) + assert.Equal(t, int64(25), limitFlag.Get()) + }) + + t.Run("sets inner flag from nested piped data under outer body path", func(t *testing.T) { + t.Parallel() + + outer := &Flag[map[string]any]{ + Name: "address", + BodyPath: "address", + } + assert.NoError(t, outer.PreParse()) + + cityInner := &InnerFlag[string]{ + Name: "address.city", + InnerField: "city", + OuterFlag: outer, + } + + data := map[string]any{ + "address": map[string]any{"city": "San Francisco"}, + } + cmd := &cli.Command{Flags: []cli.Flag{outer, cityInner}} + assert.NoError(t, ApplyStdinDataToFlags(cmd, data)) + + // InnerFlag.IsSet() is always false by design; verify the value was written + // into the outer flag's underlying map instead. + outerVal, ok := outer.Get().(map[string]any) + assert.True(t, ok, "expected outer flag value to be map[string]any, got %T", outer.Get()) + assert.Equal(t, "San Francisco", outerVal["city"]) + }) + + t.Run("sets inner flag via data alias in nested piped data", func(t *testing.T) { + t.Parallel() + + outer := &Flag[map[string]any]{ + Name: "address", + BodyPath: "address", + } + assert.NoError(t, outer.PreParse()) + + cityInner := &InnerFlag[string]{ + Name: "address.city", + InnerField: "city", + DataAliases: []string{"cityName"}, + OuterFlag: outer, + } + + // Use the alias in piped data. + data := map[string]any{ + "address": map[string]any{"cityName": "Portland"}, + } + cmd := &cli.Command{Flags: []cli.Flag{outer, cityInner}} + assert.NoError(t, ApplyStdinDataToFlags(cmd, data)) + + // InnerFlag.IsSet() is always false by design; verify the value was written + // into the outer flag's underlying map instead. + outerVal, ok := outer.Get().(map[string]any) + assert.True(t, ok, "expected outer flag value to be map[string]any, got %T", outer.Get()) + assert.Equal(t, "Portland", outerVal["city"]) + }) + + t.Run("does not set inner flag when outer flag has no body path", func(t *testing.T) { + t.Parallel() + + outer := &Flag[map[string]any]{ + Name: "options", + // No BodyPath set + } + assert.NoError(t, outer.PreParse()) + + inner := &InnerFlag[string]{ + Name: "options.key", + InnerField: "key", + OuterFlag: outer, + } + + data := map[string]any{ + "options": map[string]any{"key": "value"}, + } + cmd := &cli.Command{Flags: []cli.Flag{outer, inner}} + assert.NoError(t, ApplyStdinDataToFlags(cmd, data)) + + assert.False(t, inner.IsSet()) + }) + + t.Run("does not set inner flag when piped data has no nested map for outer path", func(t *testing.T) { + t.Parallel() + + outer := &Flag[map[string]any]{ + Name: "address", + BodyPath: "address", + } + assert.NoError(t, outer.PreParse()) + + inner := &InnerFlag[string]{ + Name: "address.city", + InnerField: "city", + OuterFlag: outer, + } + + // The outer body path key is missing from the piped data. + data := map[string]any{"other": "value"} + cmd := &cli.Command{Flags: []cli.Flag{outer, inner}} + assert.NoError(t, ApplyStdinDataToFlags(cmd, data)) + + assert.False(t, inner.IsSet()) + }) + + t.Run("canonical path key takes precedence over alias when both are present", func(t *testing.T) { + t.Parallel() + + flag := &Flag[string]{ + Name: "account-id", + QueryPath: "account_id", + DataAliases: []string{"accountId"}, + } + assert.NoError(t, flag.PreParse()) + + // Both canonical and alias present — canonical should win because it's checked first. + data := map[string]any{ + "account_id": "canonical_value", + "accountId": "alias_value", + } + cmd := &cli.Command{Flags: []cli.Flag{flag}} + assert.NoError(t, ApplyStdinDataToFlags(cmd, data)) + + assert.True(t, flag.IsSet()) + assert.Equal(t, "canonical_value", flag.Get()) + }) + + t.Run("empty data map does not set any flags", func(t *testing.T) { + t.Parallel() + + flag := &Flag[string]{ + Name: "account-id", + QueryPath: "account_id", + } + assert.NoError(t, flag.PreParse()) + + cmd := &cli.Command{Flags: []cli.Flag{flag}} + assert.NoError(t, ApplyStdinDataToFlags(cmd, map[string]any{})) + + assert.False(t, flag.IsSet()) + }) +} diff --git a/pkg/cmd/address.go b/pkg/cmd/address.go index d95a181..b21556a 100644 --- a/pkg/cmd/address.go +++ b/pkg/cmd/address.go @@ -5,7 +5,6 @@ package cmd import ( "context" "fmt" - "os" "github.com/tidwall/gjson" "github.com/urfave/cli/v3" @@ -71,8 +70,9 @@ var addressesRetrieve = cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "address-id", - Required: true, + Name: "address-id", + Required: true, + PathParam: "addressId", }, }, Action: handleAddressesRetrieve, @@ -108,8 +108,9 @@ var addressesDelete = cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "address-id", - Required: true, + Name: "address-id", + Required: true, + PathParam: "addressId", }, }, Action: handleAddressesDelete, @@ -124,8 +125,6 @@ func handleAddressesCreate(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := zavudev.AddressNewParams{} - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -137,6 +136,8 @@ func handleAddressesCreate(ctx context.Context, cmd *cli.Command) error { return err } + params := zavudev.AddressNewParams{} + var res []byte options = append(options, option.WithResponseBodyInto(&res)) _, err = client.Addresses.New(ctx, params, options...) @@ -146,8 +147,15 @@ func handleAddressesCreate(ctx context.Context, cmd *cli.Command) error { obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "addresses create", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "addresses create", + Transform: transform, + }) } func handleAddressesRetrieve(ctx context.Context, cmd *cli.Command) error { @@ -181,8 +189,15 @@ func handleAddressesRetrieve(ctx context.Context, cmd *cli.Command) error { obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "addresses retrieve", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "addresses retrieve", + Transform: transform, + }) } func handleAddressesList(ctx context.Context, cmd *cli.Command) error { @@ -193,8 +208,6 @@ func handleAddressesList(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := zavudev.AddressListParams{} - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -206,7 +219,10 @@ func handleAddressesList(ctx context.Context, cmd *cli.Command) error { return err } + params := zavudev.AddressListParams{} + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") if format == "raw" { var res []byte @@ -216,14 +232,26 @@ func handleAddressesList(ctx context.Context, cmd *cli.Command) error { return err } obj := gjson.ParseBytes(res) - return ShowJSON(os.Stdout, "addresses list", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "addresses list", + Transform: transform, + }) } else { iter := client.Addresses.ListAutoPaging(ctx, params, options...) maxItems := int64(-1) if cmd.IsSet("max-items") { maxItems = cmd.Value("max-items").(int64) } - return ShowJSONIterator(os.Stdout, "addresses list", iter, format, transform, maxItems) + return ShowJSONIterator(iter, maxItems, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "addresses list", + Transform: transform, + }) } } diff --git a/pkg/cmd/agent.go b/pkg/cmd/agent.go new file mode 100644 index 0000000..d803baf --- /dev/null +++ b/pkg/cmd/agent.go @@ -0,0 +1,744 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "context" + "fmt" + + "github.com/tidwall/gjson" + "github.com/urfave/cli/v3" + "github.com/zavudev/cli/internal/apiquery" + "github.com/zavudev/cli/internal/requestflag" + "github.com/zavudev/sdk-go" + "github.com/zavudev/sdk-go/option" +) + +var agentsCreate = requestflag.WithInnerFlags(cli.Command{ + Name: "create", + Usage: "Create an agent without a sender. It is created disabled; connect a sender and\nenable it when you are ready for it to answer.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "model", + Required: true, + BodyPath: "model", + }, + &requestflag.Flag[string]{ + Name: "name", + Required: true, + BodyPath: "name", + }, + &requestflag.Flag[string]{ + Name: "provider", + Usage: "LLM provider for the AI agent.", + Required: true, + BodyPath: "provider", + }, + &requestflag.Flag[string]{ + Name: "system-prompt", + Required: true, + BodyPath: "systemPrompt", + }, + &requestflag.Flag[int64]{ + Name: "context-window-messages", + Default: 10, + BodyPath: "contextWindowMessages", + }, + &requestflag.Flag[bool]{ + Name: "include-contact-metadata", + Default: true, + BodyPath: "includeContactMetadata", + }, + &requestflag.Flag[int64]{ + Name: "max-tokens", + BodyPath: "maxTokens", + }, + &requestflag.Flag[float64]{ + Name: "temperature", + BodyPath: "temperature", + }, + &requestflag.Flag[[]string]{ + Name: "trigger-on-channel", + Default: []string{"*"}, + BodyPath: "triggerOnChannels", + }, + &requestflag.Flag[[]string]{ + Name: "trigger-on-message-type", + Default: []string{"text"}, + BodyPath: "triggerOnMessageTypes", + }, + &requestflag.Flag[map[string]any]{ + Name: "voice", + Usage: "Voice Agent configuration on a sender's AI agent. Controls how the agent behaves on inbound and outbound phone calls through Zavu's managed voice pipeline (speech recognition, the agent's LLM, and speech synthesis, with real-time interruption handling). Requires the Voice Agents feature to be enabled for your team.", + BodyPath: "voice", + }, + }, + Action: handleAgentsCreate, + HideHelpCommand: true, +}, map[string][]requestflag.HasOuterFlag{ + "voice": { + &requestflag.InnerFlag[bool]{ + Name: "voice.enabled", + Usage: "Whether the agent handles voice calls. When false, the sender's number is not answered by the voice agent and outbound calls are rejected.", + InnerField: "enabled", + }, + &requestflag.InnerFlag[string]{ + Name: "voice.greeting", + Usage: "Opening line the agent speaks when the call connects. If omitted, the agent waits for the caller to speak first.", + InnerField: "greeting", + }, + &requestflag.InnerFlag[map[string]any]{ + Name: "voice.greetings", + Usage: "Greeting per language, keyed by language code. Used when the caller's language differs from the one `greeting` is written in.", + InnerField: "greetings", + }, + &requestflag.InnerFlag[bool]{ + Name: "voice.interruptible", + Usage: "Whether the caller can interrupt the agent while it is speaking (barge-in). When true, the agent stops talking as soon as the caller starts.", + InnerField: "interruptible", + }, + &requestflag.InnerFlag[string]{ + Name: "voice.language", + Usage: "BCP-47 language code used for both speech recognition and speech synthesis (e.g. `en`, `es`, `pt-BR`). Auto-detected from the recipient when omitted.", + InnerField: "language", + }, + &requestflag.InnerFlag[int64]{ + Name: "voice.max-call-duration-minutes", + Usage: "Hard limit on call length in minutes. The call ends automatically when reached.", + InnerField: "maxCallDurationMinutes", + }, + &requestflag.InnerFlag[int64]{ + Name: "voice.max-idle-seconds", + Usage: "How long the agent waits during silence before ending the call.", + InnerField: "maxIdleSeconds", + }, + &requestflag.InnerFlag[string]{ + Name: "voice.model", + Usage: "Model that runs the conversation, co-located in the voice network for lowest latency. Independent of the model used for text messaging. Derived from the agent's text model when omitted.", + InnerField: "model", + }, + &requestflag.InnerFlag[bool]{ + Name: "voice.record-calls", + Usage: "Whether the call audio is recorded.", + InnerField: "recordCalls", + }, + &requestflag.InnerFlag[string]{ + Name: "voice.stt-model", + Usage: "Speech-recognition model. Uses the default when omitted.", + InnerField: "sttModel", + }, + &requestflag.InnerFlag[string]{ + Name: "voice.stt-provider", + Usage: "Speech-recognition provider. Uses the default when omitted.", + InnerField: "sttProvider", + }, + &requestflag.InnerFlag[string]{ + Name: "voice.transfer-phone-number", + Usage: "E.164 phone number the agent can transfer the call to. When set, the agent is given a transfer tool it can use to hand the call to a human.", + InnerField: "transferPhoneNumber", + }, + &requestflag.InnerFlag[string]{ + Name: "voice.tts-provider", + Usage: "Speech-synthesis provider. Uses the default when omitted.", + InnerField: "ttsProvider", + }, + &requestflag.InnerFlag[string]{ + Name: "voice.tts-voice-id", + Usage: "Identifier of the synthesized voice that speaks. Choose from the voices available in the dashboard. Uses a neutral default when omitted.", + InnerField: "ttsVoiceId", + }, + &requestflag.InnerFlag[string]{ + Name: "voice.voicemail-action", + Usage: "What the agent does when an answering machine or voicemail is detected on an outbound call.", + InnerField: "voicemailAction", + }, + &requestflag.InnerFlag[string]{ + Name: "voice.voicemail-message", + Usage: "Message spoken when `voicemailAction` is `leave_message`. Falls back to `greeting` when omitted.", + InnerField: "voicemailMessage", + }, + &requestflag.InnerFlag[float64]{ + Name: "voice.voice-speed", + Usage: "Speech rate. 1.0 is natural. Only honoured by voices that support rate control; ignored by the others.", + InnerField: "voiceSpeed", + }, + }, +}) + +var agentsRetrieve = cli.Command{ + Name: "retrieve", + Usage: "Get an agent", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "agent-id", + Required: true, + PathParam: "agentId", + }, + }, + Action: handleAgentsRetrieve, + HideHelpCommand: true, +} + +var agentsUpdate = requestflag.WithInnerFlags(cli.Command{ + Name: "update", + Usage: "Update an agent", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "agent-id", + Required: true, + PathParam: "agentId", + }, + &requestflag.Flag[string]{ + Name: "api-key", + BodyPath: "apiKey", + }, + &requestflag.Flag[int64]{ + Name: "context-window-messages", + BodyPath: "contextWindowMessages", + }, + &requestflag.Flag[bool]{ + Name: "enabled", + BodyPath: "enabled", + }, + &requestflag.Flag[bool]{ + Name: "include-contact-metadata", + BodyPath: "includeContactMetadata", + }, + &requestflag.Flag[*int64]{ + Name: "max-tokens", + BodyPath: "maxTokens", + }, + &requestflag.Flag[string]{ + Name: "model", + BodyPath: "model", + }, + &requestflag.Flag[string]{ + Name: "name", + BodyPath: "name", + }, + &requestflag.Flag[string]{ + Name: "provider", + Usage: "LLM provider for the AI agent.", + BodyPath: "provider", + }, + &requestflag.Flag[string]{ + Name: "system-prompt", + BodyPath: "systemPrompt", + }, + &requestflag.Flag[*float64]{ + Name: "temperature", + BodyPath: "temperature", + }, + &requestflag.Flag[[]string]{ + Name: "trigger-on-channel", + BodyPath: "triggerOnChannels", + }, + &requestflag.Flag[[]string]{ + Name: "trigger-on-message-type", + BodyPath: "triggerOnMessageTypes", + }, + &requestflag.Flag[map[string]any]{ + Name: "voice", + Usage: "Voice Agent configuration. Patch this object to enable voice, change the greeting, or adjust call limits. Requires the Voice Agents feature to be enabled for your team.", + BodyPath: "voice", + }, + }, + Action: handleAgentsUpdate, + HideHelpCommand: true, +}, map[string][]requestflag.HasOuterFlag{ + "voice": { + &requestflag.InnerFlag[bool]{ + Name: "voice.enabled", + Usage: "Whether the agent handles voice calls. When false, the sender's number is not answered by the voice agent and outbound calls are rejected.", + InnerField: "enabled", + }, + &requestflag.InnerFlag[string]{ + Name: "voice.greeting", + Usage: "Opening line the agent speaks when the call connects. If omitted, the agent waits for the caller to speak first.", + InnerField: "greeting", + }, + &requestflag.InnerFlag[map[string]any]{ + Name: "voice.greetings", + Usage: "Greeting per language, keyed by language code. Used when the caller's language differs from the one `greeting` is written in.", + InnerField: "greetings", + }, + &requestflag.InnerFlag[bool]{ + Name: "voice.interruptible", + Usage: "Whether the caller can interrupt the agent while it is speaking (barge-in). When true, the agent stops talking as soon as the caller starts.", + InnerField: "interruptible", + }, + &requestflag.InnerFlag[string]{ + Name: "voice.language", + Usage: "BCP-47 language code used for both speech recognition and speech synthesis (e.g. `en`, `es`, `pt-BR`). Auto-detected from the recipient when omitted.", + InnerField: "language", + }, + &requestflag.InnerFlag[int64]{ + Name: "voice.max-call-duration-minutes", + Usage: "Hard limit on call length in minutes. The call ends automatically when reached.", + InnerField: "maxCallDurationMinutes", + }, + &requestflag.InnerFlag[int64]{ + Name: "voice.max-idle-seconds", + Usage: "How long the agent waits during silence before ending the call.", + InnerField: "maxIdleSeconds", + }, + &requestflag.InnerFlag[string]{ + Name: "voice.model", + Usage: "Model that runs the conversation, co-located in the voice network for lowest latency. Independent of the model used for text messaging. Derived from the agent's text model when omitted.", + InnerField: "model", + }, + &requestflag.InnerFlag[bool]{ + Name: "voice.record-calls", + Usage: "Whether the call audio is recorded.", + InnerField: "recordCalls", + }, + &requestflag.InnerFlag[string]{ + Name: "voice.stt-model", + Usage: "Speech-recognition model. Uses the default when omitted.", + InnerField: "sttModel", + }, + &requestflag.InnerFlag[string]{ + Name: "voice.stt-provider", + Usage: "Speech-recognition provider. Uses the default when omitted.", + InnerField: "sttProvider", + }, + &requestflag.InnerFlag[string]{ + Name: "voice.transfer-phone-number", + Usage: "E.164 phone number the agent can transfer the call to. When set, the agent is given a transfer tool it can use to hand the call to a human.", + InnerField: "transferPhoneNumber", + }, + &requestflag.InnerFlag[string]{ + Name: "voice.tts-provider", + Usage: "Speech-synthesis provider. Uses the default when omitted.", + InnerField: "ttsProvider", + }, + &requestflag.InnerFlag[string]{ + Name: "voice.tts-voice-id", + Usage: "Identifier of the synthesized voice that speaks. Choose from the voices available in the dashboard. Uses a neutral default when omitted.", + InnerField: "ttsVoiceId", + }, + &requestflag.InnerFlag[string]{ + Name: "voice.voicemail-action", + Usage: "What the agent does when an answering machine or voicemail is detected on an outbound call.", + InnerField: "voicemailAction", + }, + &requestflag.InnerFlag[string]{ + Name: "voice.voicemail-message", + Usage: "Message spoken when `voicemailAction` is `leave_message`. Falls back to `greeting` when omitted.", + InnerField: "voicemailMessage", + }, + &requestflag.InnerFlag[float64]{ + Name: "voice.voice-speed", + Usage: "Speech rate. 1.0 is natural. Only honoured by voices that support rate control; ignored by the others.", + InnerField: "voiceSpeed", + }, + }, +}) + +var agentsList = cli.Command{ + Name: "list", + Usage: "Every agent in the project, newest first — including agents that are not\nconnected to any sender yet, which the sender-scoped routes cannot reach. Each\nitem carries `senderIds`, the senders the agent answers on.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "cursor", + QueryPath: "cursor", + }, + &requestflag.Flag[int64]{ + Name: "limit", + Default: 50, + QueryPath: "limit", + }, + &requestflag.Flag[int64]{ + Name: "max-items", + Usage: "The maximum number of items to return (use -1 for unlimited).", + }, + }, + Action: handleAgentsList, + HideHelpCommand: true, +} + +var agentsDelete = cli.Command{ + Name: "delete", + Usage: "Delete an agent", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "agent-id", + Required: true, + PathParam: "agentId", + }, + }, + Action: handleAgentsDelete, + HideHelpCommand: true, +} + +var agentsListVoices = cli.Command{ + Name: "list-voices", + Usage: "The voices an agent can speak with, for `voice.ttsVoiceId`. Filter by `language`\nto get the ones that speak it; a voice can still be used with `language: auto`,\nwhere the agent follows the caller and keeps the chosen voice.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "language", + Usage: "BCP-47 tag (`en`, `es`, `pt-BR`). Omit, or pass `auto`, for every voice.", + QueryPath: "language", + }, + }, + Action: handleAgentsListVoices, + HideHelpCommand: true, +} + +var agentsTest = requestflag.WithInnerFlags(cli.Command{ + Name: "test", + Usage: "Run the agent's prompt, model and knowledge base against a message and return\nthe reply instead of delivering it. Writes nothing and charges nothing, so it is\nsafe to call repeatedly while iterating on a prompt.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "agent-id", + Required: true, + PathParam: "agentId", + }, + &requestflag.Flag[string]{ + Name: "message", + Usage: "What to say to the agent.", + Required: true, + BodyPath: "message", + }, + &requestflag.Flag[bool]{ + Name: "execute-tools", + Usage: "Run the tools the agent calls instead of reporting the choice and stopping.\n\nOff by default because a tool handler talks to the outside world: a rehearsal that charges a card is not a rehearsal. Turn it on to exercise the loop that actually matters — the model picks a tool, the handler answers, the model replies with the result — without sending a message to anyone. What ran comes back in `executedToolCalls`.", + Default: false, + BodyPath: "executeTools", + }, + &requestflag.Flag[[]map[string]any]{ + Name: "history", + Usage: "Prior turns, oldest first, to exercise multi-turn behaviour without persisting a thread. Trimmed to the agent's context window.", + BodyPath: "history", + }, + &requestflag.Flag[bool]{ + Name: "use-knowledge-base", + Usage: "Set false to skip retrieval and isolate prompt behaviour from the knowledge base.", + Default: true, + BodyPath: "useKnowledgeBase", + }, + }, + Action: handleAgentsTest, + HideHelpCommand: true, +}, map[string][]requestflag.HasOuterFlag{ + "history": { + &requestflag.InnerFlag[string]{ + Name: "history.content", + InnerField: "content", + }, + &requestflag.InnerFlag[string]{ + Name: "history.role", + Usage: `Allowed values: "user", "assistant".`, + InnerField: "role", + }, + }, +}) + +func handleAgentsCreate(ctx context.Context, cmd *cli.Command) error { + client := zavudev.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + + options, err := flagOptions( + cmd, + apiquery.NestedQueryFormatBrackets, + apiquery.ArrayQueryFormatComma, + ApplicationJSON, + false, + ) + if err != nil { + return err + } + + params := zavudev.AgentNewParams{} + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Agents.New(ctx, params, options...) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") + transform := cmd.Root().String("transform") + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "agents create", + Transform: transform, + }) +} + +func handleAgentsRetrieve(ctx context.Context, cmd *cli.Command) error { + client := zavudev.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("agent-id") && len(unusedArgs) > 0 { + cmd.Set("agent-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 + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Agents.Get(ctx, cmd.Value("agent-id").(string), options...) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") + transform := cmd.Root().String("transform") + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "agents retrieve", + Transform: transform, + }) +} + +func handleAgentsUpdate(ctx context.Context, cmd *cli.Command) error { + client := zavudev.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("agent-id") && len(unusedArgs) > 0 { + cmd.Set("agent-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, + ApplicationJSON, + false, + ) + if err != nil { + return err + } + + params := zavudev.AgentUpdateParams{} + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Agents.Update( + ctx, + cmd.Value("agent-id").(string), + params, + options..., + ) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") + transform := cmd.Root().String("transform") + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "agents update", + Transform: transform, + }) +} + +func handleAgentsList(ctx context.Context, cmd *cli.Command) error { + client := zavudev.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + + 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 + } + + params := zavudev.AgentListParams{} + + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") + transform := cmd.Root().String("transform") + if format == "raw" { + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Agents.List(ctx, params, options...) + if err != nil { + return err + } + obj := gjson.ParseBytes(res) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "agents list", + Transform: transform, + }) + } else { + iter := client.Agents.ListAutoPaging(ctx, params, options...) + maxItems := int64(-1) + if cmd.IsSet("max-items") { + maxItems = cmd.Value("max-items").(int64) + } + return ShowJSONIterator(iter, maxItems, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "agents list", + Transform: transform, + }) + } +} + +func handleAgentsDelete(ctx context.Context, cmd *cli.Command) error { + client := zavudev.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("agent-id") && len(unusedArgs) > 0 { + cmd.Set("agent-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.Agents.Delete(ctx, cmd.Value("agent-id").(string), options...) +} + +func handleAgentsListVoices(ctx context.Context, cmd *cli.Command) error { + client := zavudev.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + + 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 + } + + params := zavudev.AgentListVoicesParams{} + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Agents.ListVoices(ctx, params, options...) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") + transform := cmd.Root().String("transform") + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "agents list-voices", + Transform: transform, + }) +} + +func handleAgentsTest(ctx context.Context, cmd *cli.Command) error { + client := zavudev.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("agent-id") && len(unusedArgs) > 0 { + cmd.Set("agent-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, + ApplicationJSON, + false, + ) + if err != nil { + return err + } + + params := zavudev.AgentTestParams{} + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Agents.Test( + ctx, + cmd.Value("agent-id").(string), + params, + options..., + ) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") + transform := cmd.Root().String("transform") + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "agents test", + Transform: transform, + }) +} diff --git a/pkg/cmd/agent_test.go b/pkg/cmd/agent_test.go new file mode 100644 index 0000000..391608e --- /dev/null +++ b/pkg/cmd/agent_test.go @@ -0,0 +1,324 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "testing" + + "github.com/zavudev/cli/internal/mocktest" + "github.com/zavudev/cli/internal/requestflag" +) + +func TestAgentsCreate(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "agents", "create", + "--model", "model", + "--name", "name", + "--provider", "openai", + "--system-prompt", "systemPrompt", + "--context-window-messages", "1", + "--include-contact-metadata=true", + "--max-tokens", "1", + "--temperature", "0", + "--trigger-on-channel", "string", + "--trigger-on-message-type", "string", + "--voice", "{enabled: true, greeting: 'Hi, thanks for calling Acme. How can I help you today?', greetings: {es: 'Hola, soy Atlas. Preguntame lo que quieras.'}, interruptible: true, language: en, maxCallDurationMinutes: 1, maxIdleSeconds: 5, model: openai/gpt-4o, recordCalls: true, sttModel: sttModel, sttProvider: sttProvider, transferPhoneNumber: '+14155551234', ttsProvider: ttsProvider, ttsVoiceId: aria, voicemailAction: hangup, voicemailMessage: voicemailMessage, voiceSpeed: 0.5}", + ) + }) + + t.Run("inner flags", func(t *testing.T) { + // Check that inner flags have been set up correctly + requestflag.CheckInnerFlags(agentsCreate) + + // Alternative argument passing style using inner flags + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "agents", "create", + "--model", "model", + "--name", "name", + "--provider", "openai", + "--system-prompt", "systemPrompt", + "--context-window-messages", "1", + "--include-contact-metadata=true", + "--max-tokens", "1", + "--temperature", "0", + "--trigger-on-channel", "string", + "--trigger-on-message-type", "string", + "--voice.enabled=true", + "--voice.greeting", "Hi, thanks for calling Acme. How can I help you today?", + "--voice.greetings", "{es: 'Hola, soy Atlas. Preguntame lo que quieras.'}", + "--voice.interruptible=true", + "--voice.language", "en", + "--voice.max-call-duration-minutes", "1", + "--voice.max-idle-seconds", "5", + "--voice.model", "openai/gpt-4o", + "--voice.record-calls=true", + "--voice.stt-model", "sttModel", + "--voice.stt-provider", "sttProvider", + "--voice.transfer-phone-number", "+14155551234", + "--voice.tts-provider", "ttsProvider", + "--voice.tts-voice-id", "aria", + "--voice.voicemail-action", "hangup", + "--voice.voicemail-message", "voicemailMessage", + "--voice.voice-speed", "0.5", + ) + }) + + t.Run("piping data", func(t *testing.T) { + // Test piping YAML data over stdin + pipeData := []byte("" + + "model: model\n" + + "name: name\n" + + "provider: openai\n" + + "systemPrompt: systemPrompt\n" + + "contextWindowMessages: 1\n" + + "includeContactMetadata: true\n" + + "maxTokens: 1\n" + + "temperature: 0\n" + + "triggerOnChannels:\n" + + " - string\n" + + "triggerOnMessageTypes:\n" + + " - string\n" + + "voice:\n" + + " enabled: true\n" + + " greeting: Hi, thanks for calling Acme. How can I help you today?\n" + + " greetings:\n" + + " es: Hola, soy Atlas. Preguntame lo que quieras.\n" + + " interruptible: true\n" + + " language: en\n" + + " maxCallDurationMinutes: 1\n" + + " maxIdleSeconds: 5\n" + + " model: openai/gpt-4o\n" + + " recordCalls: true\n" + + " sttModel: sttModel\n" + + " sttProvider: sttProvider\n" + + " transferPhoneNumber: '+14155551234'\n" + + " ttsProvider: ttsProvider\n" + + " ttsVoiceId: aria\n" + + " voicemailAction: hangup\n" + + " voicemailMessage: voicemailMessage\n" + + " voiceSpeed: 0.5\n") + mocktest.TestRunMockTestWithPipeAndFlags( + t, pipeData, + "--api-key", "string", + "agents", "create", + ) + }) +} + +func TestAgentsRetrieve(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "agents", "retrieve", + "--agent-id", "agentId", + ) + }) +} + +func TestAgentsUpdate(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "agents", "update", + "--agent-id", "agentId", + "--api-key", "apiKey", + "--context-window-messages", "1", + "--enabled=true", + "--include-contact-metadata=true", + "--max-tokens", "1", + "--model", "model", + "--name", "name", + "--provider", "openai", + "--system-prompt", "systemPrompt", + "--temperature", "0", + "--trigger-on-channel", "string", + "--trigger-on-message-type", "string", + "--voice", "{enabled: true, greeting: 'Hi, thanks for calling Acme. How can I help you today?', greetings: {es: 'Hola, soy Atlas. Preguntame lo que quieras.'}, interruptible: true, language: en, maxCallDurationMinutes: 1, maxIdleSeconds: 5, model: openai/gpt-4o, recordCalls: true, sttModel: sttModel, sttProvider: sttProvider, transferPhoneNumber: '+14155551234', ttsProvider: ttsProvider, ttsVoiceId: aria, voicemailAction: hangup, voicemailMessage: voicemailMessage, voiceSpeed: 0.5}", + ) + }) + + t.Run("inner flags", func(t *testing.T) { + // Check that inner flags have been set up correctly + requestflag.CheckInnerFlags(agentsUpdate) + + // Alternative argument passing style using inner flags + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "agents", "update", + "--agent-id", "agentId", + "--api-key", "apiKey", + "--context-window-messages", "1", + "--enabled=true", + "--include-contact-metadata=true", + "--max-tokens", "1", + "--model", "model", + "--name", "name", + "--provider", "openai", + "--system-prompt", "systemPrompt", + "--temperature", "0", + "--trigger-on-channel", "string", + "--trigger-on-message-type", "string", + "--voice.enabled=true", + "--voice.greeting", "Hi, thanks for calling Acme. How can I help you today?", + "--voice.greetings", "{es: 'Hola, soy Atlas. Preguntame lo que quieras.'}", + "--voice.interruptible=true", + "--voice.language", "en", + "--voice.max-call-duration-minutes", "1", + "--voice.max-idle-seconds", "5", + "--voice.model", "openai/gpt-4o", + "--voice.record-calls=true", + "--voice.stt-model", "sttModel", + "--voice.stt-provider", "sttProvider", + "--voice.transfer-phone-number", "+14155551234", + "--voice.tts-provider", "ttsProvider", + "--voice.tts-voice-id", "aria", + "--voice.voicemail-action", "hangup", + "--voice.voicemail-message", "voicemailMessage", + "--voice.voice-speed", "0.5", + ) + }) + + t.Run("piping data", func(t *testing.T) { + // Test piping YAML data over stdin + pipeData := []byte("" + + "apiKey: apiKey\n" + + "contextWindowMessages: 1\n" + + "enabled: true\n" + + "includeContactMetadata: true\n" + + "maxTokens: 1\n" + + "model: model\n" + + "name: name\n" + + "provider: openai\n" + + "systemPrompt: systemPrompt\n" + + "temperature: 0\n" + + "triggerOnChannels:\n" + + " - string\n" + + "triggerOnMessageTypes:\n" + + " - string\n" + + "voice:\n" + + " enabled: true\n" + + " greeting: Hi, thanks for calling Acme. How can I help you today?\n" + + " greetings:\n" + + " es: Hola, soy Atlas. Preguntame lo que quieras.\n" + + " interruptible: true\n" + + " language: en\n" + + " maxCallDurationMinutes: 1\n" + + " maxIdleSeconds: 5\n" + + " model: openai/gpt-4o\n" + + " recordCalls: true\n" + + " sttModel: sttModel\n" + + " sttProvider: sttProvider\n" + + " transferPhoneNumber: '+14155551234'\n" + + " ttsProvider: ttsProvider\n" + + " ttsVoiceId: aria\n" + + " voicemailAction: hangup\n" + + " voicemailMessage: voicemailMessage\n" + + " voiceSpeed: 0.5\n") + mocktest.TestRunMockTestWithPipeAndFlags( + t, pipeData, + "--api-key", "string", + "agents", "update", + "--agent-id", "agentId", + ) + }) +} + +func TestAgentsList(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "agents", "list", + "--max-items", "10", + "--cursor", "cursor", + "--limit", "100", + ) + }) +} + +func TestAgentsDelete(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "agents", "delete", + "--agent-id", "agentId", + ) + }) +} + +func TestAgentsListVoices(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "agents", "list-voices", + "--language", "es", + ) + }) +} + +func TestAgentsTest(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "agents", "test", + "--agent-id", "agentId", + "--message", "Where is order ORD-12345?", + "--execute-tools=true", + "--history", "{content: content, role: user}", + "--use-knowledge-base=true", + ) + }) + + t.Run("inner flags", func(t *testing.T) { + // Check that inner flags have been set up correctly + requestflag.CheckInnerFlags(agentsTest) + + // Alternative argument passing style using inner flags + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "agents", "test", + "--agent-id", "agentId", + "--message", "Where is order ORD-12345?", + "--execute-tools=true", + "--history.content", "content", + "--history.role", "user", + "--use-knowledge-base=true", + ) + }) + + t.Run("piping data", func(t *testing.T) { + // Test piping YAML data over stdin + pipeData := []byte("" + + "message: Where is order ORD-12345?\n" + + "executeTools: true\n" + + "history:\n" + + " - content: content\n" + + " role: user\n" + + "useKnowledgeBase: true\n") + mocktest.TestRunMockTestWithPipeAndFlags( + t, pipeData, + "--api-key", "string", + "agents", "test", + "--agent-id", "agentId", + ) + }) +} diff --git a/pkg/cmd/agentsender.go b/pkg/cmd/agentsender.go new file mode 100644 index 0000000..7ee9735 --- /dev/null +++ b/pkg/cmd/agentsender.go @@ -0,0 +1,139 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "context" + "fmt" + + "github.com/tidwall/gjson" + "github.com/urfave/cli/v3" + "github.com/zavudev/cli/internal/apiquery" + "github.com/zavudev/cli/internal/requestflag" + "github.com/zavudev/sdk-go" + "github.com/zavudev/sdk-go/option" +) + +var agentsSendersConnect = cli.Command{ + Name: "connect", + Usage: "Make the agent answer on this sender. An agent can serve several senders; a\nsender answers with at most one agent, so connecting one that is already in use\nreturns `400` naming the agent that holds it.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "agent-id", + Required: true, + PathParam: "agentId", + }, + &requestflag.Flag[string]{ + Name: "sender-id", + Usage: "Sender to connect.", + Required: true, + BodyPath: "senderId", + }, + }, + Action: handleAgentsSendersConnect, + HideHelpCommand: true, +} + +var agentsSendersDisconnect = cli.Command{ + Name: "disconnect", + Usage: "Stop the agent answering on this sender. The agent's primary sender is part of\nthe agent itself and cannot be disconnected here.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "agent-id", + Required: true, + PathParam: "agentId", + }, + &requestflag.Flag[string]{ + Name: "sender-id", + Required: true, + PathParam: "senderId", + }, + }, + Action: handleAgentsSendersDisconnect, + HideHelpCommand: true, +} + +func handleAgentsSendersConnect(ctx context.Context, cmd *cli.Command) error { + client := zavudev.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("agent-id") && len(unusedArgs) > 0 { + cmd.Set("agent-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, + ApplicationJSON, + false, + ) + if err != nil { + return err + } + + params := zavudev.AgentSenderConnectParams{} + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Agents.Senders.Connect( + ctx, + cmd.Value("agent-id").(string), + params, + options..., + ) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") + transform := cmd.Root().String("transform") + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "agents:senders connect", + Transform: transform, + }) +} + +func handleAgentsSendersDisconnect(ctx context.Context, cmd *cli.Command) error { + client := zavudev.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("sender-id") && len(unusedArgs) > 0 { + cmd.Set("sender-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 + } + + params := zavudev.AgentSenderDisconnectParams{ + AgentID: cmd.Value("agent-id").(string), + } + + return client.Agents.Senders.Disconnect( + ctx, + cmd.Value("sender-id").(string), + params, + options..., + ) +} diff --git a/pkg/cmd/agentsender_test.go b/pkg/cmd/agentsender_test.go new file mode 100644 index 0000000..d2fea18 --- /dev/null +++ b/pkg/cmd/agentsender_test.go @@ -0,0 +1,46 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "testing" + + "github.com/zavudev/cli/internal/mocktest" +) + +func TestAgentsSendersConnect(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "agents:senders", "connect", + "--agent-id", "agentId", + "--sender-id", "senderId", + ) + }) + + t.Run("piping data", func(t *testing.T) { + // Test piping YAML data over stdin + pipeData := []byte("senderId: senderId") + mocktest.TestRunMockTestWithPipeAndFlags( + t, pipeData, + "--api-key", "string", + "agents:senders", "connect", + "--agent-id", "agentId", + ) + }) +} + +func TestAgentsSendersDisconnect(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "agents:senders", "disconnect", + "--agent-id", "agentId", + "--sender-id", "senderId", + ) + }) +} diff --git a/pkg/cmd/agenttemplate.go b/pkg/cmd/agenttemplate.go new file mode 100644 index 0000000..ab4bd26 --- /dev/null +++ b/pkg/cmd/agenttemplate.go @@ -0,0 +1,120 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "context" + "fmt" + + "github.com/tidwall/gjson" + "github.com/urfave/cli/v3" + "github.com/zavudev/cli/internal/apiquery" + "github.com/zavudev/cli/internal/requestflag" + "github.com/zavudev/sdk-go" + "github.com/zavudev/sdk-go/option" +) + +var agentTemplatesRetrieve = cli.Command{ + Name: "retrieve", + Usage: "Fetch a single factory agent fully rendered: the function files to scaffold (an\n`index.ts` that declares the agent with `defineAgent` and its skills with\n`defineTool`) plus the secrets it needs. This is what\n`npx zavudev agents pull ` writes to disk before `npx zavudev deploy`.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "template-id", + Required: true, + PathParam: "templateId", + }, + }, + Action: handleAgentTemplatesRetrieve, + HideHelpCommand: true, +} + +var agentTemplatesList = cli.Command{ + Name: "list", + Usage: "List the factory agents available to scaffold with `npx zavudev agents pull`.\nEach entry is a ready-made voice or text agent (system prompt, skills, and — for\nvoice agents — a co-located voice config).", + Suggest: true, + Flags: []cli.Flag{}, + Action: handleAgentTemplatesList, + HideHelpCommand: true, +} + +func handleAgentTemplatesRetrieve(ctx context.Context, cmd *cli.Command) error { + client := zavudev.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("template-id") && len(unusedArgs) > 0 { + cmd.Set("template-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 + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.AgentTemplates.Get(ctx, cmd.Value("template-id").(string), options...) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") + transform := cmd.Root().String("transform") + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "agent-templates retrieve", + Transform: transform, + }) +} + +func handleAgentTemplatesList(ctx context.Context, cmd *cli.Command) error { + client := zavudev.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + + 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 + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.AgentTemplates.List(ctx, options...) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") + transform := cmd.Root().String("transform") + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "agent-templates list", + Transform: transform, + }) +} diff --git a/pkg/cmd/agenttemplate_test.go b/pkg/cmd/agenttemplate_test.go new file mode 100644 index 0000000..5dff614 --- /dev/null +++ b/pkg/cmd/agenttemplate_test.go @@ -0,0 +1,32 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "testing" + + "github.com/zavudev/cli/internal/mocktest" +) + +func TestAgentTemplatesRetrieve(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "agent-templates", "retrieve", + "--template-id", "fermi", + ) + }) +} + +func TestAgentTemplatesList(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "agent-templates", "list", + ) + }) +} diff --git a/pkg/cmd/balance.go b/pkg/cmd/balance.go new file mode 100644 index 0000000..97a6480 --- /dev/null +++ b/pkg/cmd/balance.go @@ -0,0 +1,62 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "context" + "fmt" + + "github.com/tidwall/gjson" + "github.com/urfave/cli/v3" + "github.com/zavudev/cli/internal/apiquery" + "github.com/zavudev/sdk-go" + "github.com/zavudev/sdk-go/option" +) + +var balanceRetrieve = cli.Command{ + Name: "retrieve", + Usage: "Get balance for the API key's team. If the API key belongs to a sub-account,\nalso includes the sub-account's total spending and credit limit.", + Suggest: true, + Flags: []cli.Flag{}, + Action: handleBalanceRetrieve, + HideHelpCommand: true, +} + +func handleBalanceRetrieve(ctx context.Context, cmd *cli.Command) error { + client := zavudev.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + + 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 + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Balance.Get(ctx, options...) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") + transform := cmd.Root().String("transform") + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "balance retrieve", + Transform: transform, + }) +} diff --git a/pkg/cmd/balance_test.go b/pkg/cmd/balance_test.go new file mode 100644 index 0000000..12ef8a7 --- /dev/null +++ b/pkg/cmd/balance_test.go @@ -0,0 +1,20 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "testing" + + "github.com/zavudev/cli/internal/mocktest" +) + +func TestBalanceRetrieve(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "balance", "retrieve", + ) + }) +} diff --git a/pkg/cmd/broadcast.go b/pkg/cmd/broadcast.go index 78e04b1..4cd3c82 100644 --- a/pkg/cmd/broadcast.go +++ b/pkg/cmd/broadcast.go @@ -5,7 +5,6 @@ package cmd import ( "context" "fmt" - "os" "github.com/tidwall/gjson" "github.com/urfave/cli/v3" @@ -101,6 +100,16 @@ var broadcastsCreate = requestflag.WithInnerFlags(cli.Command{ Usage: "MIME type of the media.", InnerField: "mimeType", }, + &requestflag.InnerFlag[map[string]any]{ + Name: "content.template-button-variables", + Usage: "Default button variables for dynamic URL/OTP buttons. Keys are the button index (0, 1, 2). Per-contact values override these.", + InnerField: "templateButtonVariables", + }, + &requestflag.InnerFlag[map[string]any]{ + Name: "content.template-header-variables", + Usage: "Default value for a text-header variable, keyed by `1` (can be overridden per contact). If omitted, Zavu resolves the header from `templateVariables` by the header placeholder's name.", + InnerField: "templateHeaderVariables", + }, &requestflag.InnerFlag[string]{ Name: "content.template-id", Usage: "Template ID for template messages.", @@ -108,7 +117,7 @@ var broadcastsCreate = requestflag.WithInnerFlags(cli.Command{ }, &requestflag.InnerFlag[map[string]any]{ Name: "content.template-variables", - Usage: "Default template variables (can be overridden per contact).", + Usage: "Default body variables (can be overridden per contact). Key them to match the template body: by position (`1`, `2`, ...) for positional templates, or by name (e.g. `customer_name`) for named templates. Zavu detects the template's format and sends the correct payload to Meta. Do not mix positional and named keys.", InnerField: "templateVariables", }, }, @@ -120,8 +129,9 @@ var broadcastsRetrieve = cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "broadcast-id", - Required: true, + Name: "broadcast-id", + Required: true, + PathParam: "broadcastId", }, }, Action: handleBroadcastsRetrieve, @@ -134,8 +144,9 @@ var broadcastsUpdate = requestflag.WithInnerFlags(cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "broadcast-id", - Required: true, + Name: "broadcast-id", + Required: true, + PathParam: "broadcastId", }, &requestflag.Flag[map[string]any]{ Name: "content", @@ -187,6 +198,16 @@ var broadcastsUpdate = requestflag.WithInnerFlags(cli.Command{ Usage: "MIME type of the media.", InnerField: "mimeType", }, + &requestflag.InnerFlag[map[string]any]{ + Name: "content.template-button-variables", + Usage: "Default button variables for dynamic URL/OTP buttons. Keys are the button index (0, 1, 2). Per-contact values override these.", + InnerField: "templateButtonVariables", + }, + &requestflag.InnerFlag[map[string]any]{ + Name: "content.template-header-variables", + Usage: "Default value for a text-header variable, keyed by `1` (can be overridden per contact). If omitted, Zavu resolves the header from `templateVariables` by the header placeholder's name.", + InnerField: "templateHeaderVariables", + }, &requestflag.InnerFlag[string]{ Name: "content.template-id", Usage: "Template ID for template messages.", @@ -194,7 +215,7 @@ var broadcastsUpdate = requestflag.WithInnerFlags(cli.Command{ }, &requestflag.InnerFlag[map[string]any]{ Name: "content.template-variables", - Usage: "Default template variables (can be overridden per contact).", + Usage: "Default body variables (can be overridden per contact). Key them to match the template body: by position (`1`, `2`, ...) for positional templates, or by name (e.g. `customer_name`) for named templates. Zavu detects the template's format and sends the correct payload to Meta. Do not mix positional and named keys.", InnerField: "templateVariables", }, }, @@ -234,8 +255,9 @@ var broadcastsDelete = cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "broadcast-id", - Required: true, + Name: "broadcast-id", + Required: true, + PathParam: "broadcastId", }, }, Action: handleBroadcastsDelete, @@ -248,22 +270,39 @@ var broadcastsCancel = cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "broadcast-id", - Required: true, + Name: "broadcast-id", + Required: true, + PathParam: "broadcastId", }, }, Action: handleBroadcastsCancel, HideHelpCommand: true, } +var broadcastsEscalateReview = cli.Command{ + Name: "escalate-review", + Usage: "Request manual review by the Zavu team for a rejected broadcast. Use this after\nautomated review rejection if you believe the content is legitimate.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "broadcast-id", + Required: true, + PathParam: "broadcastId", + }, + }, + Action: handleBroadcastsEscalateReview, + HideHelpCommand: true, +} + var broadcastsProgress = cli.Command{ Name: "progress", Usage: "Get real-time progress of a broadcast including delivery counts and estimated\ncompletion time.", Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "broadcast-id", - Required: true, + Name: "broadcast-id", + Required: true, + PathParam: "broadcastId", }, }, Action: handleBroadcastsProgress, @@ -276,8 +315,9 @@ var broadcastsReschedule = cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "broadcast-id", - Required: true, + Name: "broadcast-id", + Required: true, + PathParam: "broadcastId", }, &requestflag.Flag[any]{ Name: "scheduled-at", @@ -290,14 +330,30 @@ var broadcastsReschedule = cli.Command{ HideHelpCommand: true, } +var broadcastsRetryReview = cli.Command{ + Name: "retry-review", + Usage: "Resubmit a rejected broadcast for AI review after editing content. Maximum 3\nreview attempts allowed per broadcast.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "broadcast-id", + Required: true, + PathParam: "broadcastId", + }, + }, + Action: handleBroadcastsRetryReview, + HideHelpCommand: true, +} + var broadcastsSend = cli.Command{ Name: "send", - Usage: "Start sending the broadcast immediately or schedule for later. Broadcasts go\nthrough automated AI content review before sending. If the review passes, the\nbroadcast proceeds. If rejected, use PATCH to edit content, then call POST\n/retry-review. Reserves the estimated cost from your balance.", + Usage: "Start sending the broadcast immediately or schedule for later.", Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "broadcast-id", - Required: true, + Name: "broadcast-id", + Required: true, + PathParam: "broadcastId", }, &requestflag.Flag[any]{ Name: "scheduled-at", @@ -317,8 +373,6 @@ func handleBroadcastsCreate(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := zavudev.BroadcastNewParams{} - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -330,6 +384,8 @@ func handleBroadcastsCreate(ctx context.Context, cmd *cli.Command) error { return err } + params := zavudev.BroadcastNewParams{} + var res []byte options = append(options, option.WithResponseBodyInto(&res)) _, err = client.Broadcasts.New(ctx, params, options...) @@ -339,8 +395,15 @@ func handleBroadcastsCreate(ctx context.Context, cmd *cli.Command) error { obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "broadcasts create", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "broadcasts create", + Transform: transform, + }) } func handleBroadcastsRetrieve(ctx context.Context, cmd *cli.Command) error { @@ -374,8 +437,15 @@ func handleBroadcastsRetrieve(ctx context.Context, cmd *cli.Command) error { obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "broadcasts retrieve", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "broadcasts retrieve", + Transform: transform, + }) } func handleBroadcastsUpdate(ctx context.Context, cmd *cli.Command) error { @@ -389,8 +459,6 @@ func handleBroadcastsUpdate(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := zavudev.BroadcastUpdateParams{} - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -402,6 +470,8 @@ func handleBroadcastsUpdate(ctx context.Context, cmd *cli.Command) error { return err } + params := zavudev.BroadcastUpdateParams{} + var res []byte options = append(options, option.WithResponseBodyInto(&res)) _, err = client.Broadcasts.Update( @@ -416,8 +486,15 @@ func handleBroadcastsUpdate(ctx context.Context, cmd *cli.Command) error { obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "broadcasts update", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "broadcasts update", + Transform: transform, + }) } func handleBroadcastsList(ctx context.Context, cmd *cli.Command) error { @@ -428,8 +505,6 @@ func handleBroadcastsList(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := zavudev.BroadcastListParams{} - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -441,7 +516,10 @@ func handleBroadcastsList(ctx context.Context, cmd *cli.Command) error { return err } + params := zavudev.BroadcastListParams{} + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") if format == "raw" { var res []byte @@ -451,14 +529,26 @@ func handleBroadcastsList(ctx context.Context, cmd *cli.Command) error { return err } obj := gjson.ParseBytes(res) - return ShowJSON(os.Stdout, "broadcasts list", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "broadcasts list", + Transform: transform, + }) } else { iter := client.Broadcasts.ListAutoPaging(ctx, params, options...) maxItems := int64(-1) if cmd.IsSet("max-items") { maxItems = cmd.Value("max-items").(int64) } - return ShowJSONIterator(os.Stdout, "broadcasts list", iter, format, transform, maxItems) + return ShowJSONIterator(iter, maxItems, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "broadcasts list", + Transform: transform, + }) } } @@ -518,8 +608,57 @@ func handleBroadcastsCancel(ctx context.Context, cmd *cli.Command) error { obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "broadcasts cancel", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "broadcasts cancel", + Transform: transform, + }) +} + +func handleBroadcastsEscalateReview(ctx context.Context, cmd *cli.Command) error { + client := zavudev.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("broadcast-id") && len(unusedArgs) > 0 { + cmd.Set("broadcast-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 + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Broadcasts.EscalateReview(ctx, cmd.Value("broadcast-id").(string), options...) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") + transform := cmd.Root().String("transform") + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "broadcasts escalate-review", + Transform: transform, + }) } func handleBroadcastsProgress(ctx context.Context, cmd *cli.Command) error { @@ -553,8 +692,15 @@ func handleBroadcastsProgress(ctx context.Context, cmd *cli.Command) error { obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "broadcasts progress", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "broadcasts progress", + Transform: transform, + }) } func handleBroadcastsReschedule(ctx context.Context, cmd *cli.Command) error { @@ -568,8 +714,6 @@ func handleBroadcastsReschedule(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := zavudev.BroadcastRescheduleParams{} - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -581,6 +725,8 @@ func handleBroadcastsReschedule(ctx context.Context, cmd *cli.Command) error { return err } + params := zavudev.BroadcastRescheduleParams{} + var res []byte options = append(options, option.WithResponseBodyInto(&res)) _, err = client.Broadcasts.Reschedule( @@ -595,11 +741,18 @@ func handleBroadcastsReschedule(ctx context.Context, cmd *cli.Command) error { obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "broadcasts reschedule", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "broadcasts reschedule", + Transform: transform, + }) } -func handleBroadcastsSend(ctx context.Context, cmd *cli.Command) error { +func handleBroadcastsRetryReview(ctx context.Context, cmd *cli.Command) error { client := zavudev.NewClient(getDefaultRequestOptions(cmd)...) unusedArgs := cmd.Args().Slice() if !cmd.IsSet("broadcast-id") && len(unusedArgs) > 0 { @@ -610,7 +763,47 @@ func handleBroadcastsSend(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := zavudev.BroadcastSendParams{} + 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.Broadcasts.RetryReview(ctx, cmd.Value("broadcast-id").(string), options...) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") + transform := cmd.Root().String("transform") + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "broadcasts retry-review", + Transform: transform, + }) +} + +func handleBroadcastsSend(ctx context.Context, cmd *cli.Command) error { + client := zavudev.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("broadcast-id") && len(unusedArgs) > 0 { + cmd.Set("broadcast-id", unusedArgs[0]) + unusedArgs = unusedArgs[1:] + } + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } options, err := flagOptions( cmd, @@ -623,6 +816,8 @@ func handleBroadcastsSend(ctx context.Context, cmd *cli.Command) error { return err } + params := zavudev.BroadcastSendParams{} + var res []byte options = append(options, option.WithResponseBodyInto(&res)) _, err = client.Broadcasts.Send( @@ -637,6 +832,13 @@ func handleBroadcastsSend(ctx context.Context, cmd *cli.Command) error { obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "broadcasts send", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "broadcasts send", + Transform: transform, + }) } diff --git a/pkg/cmd/broadcast_test.go b/pkg/cmd/broadcast_test.go index 8a9c39b..bc05c62 100644 --- a/pkg/cmd/broadcast_test.go +++ b/pkg/cmd/broadcast_test.go @@ -18,7 +18,7 @@ func TestBroadcastsCreate(t *testing.T) { "broadcasts", "create", "--channel", "sms", "--name", "Black Friday Sale", - "--content", "{filename: filename, mediaId: mediaId, mediaUrl: mediaUrl, mimeType: mimeType, templateId: templateId, templateVariables: {foo: string}}", + "--content", "{filename: filename, mediaId: mediaId, mediaUrl: mediaUrl, mimeType: mimeType, templateButtonVariables: {foo: string}, templateHeaderVariables: {foo: string}, templateId: templateId, templateVariables: {foo: string}}", "--email-html-body", "emailHtmlBody", "--email-subject", "emailSubject", "--idempotency-key", "idempotencyKey", @@ -45,6 +45,8 @@ func TestBroadcastsCreate(t *testing.T) { "--content.media-id", "mediaId", "--content.media-url", "mediaUrl", "--content.mime-type", "mimeType", + "--content.template-button-variables", "{foo: string}", + "--content.template-header-variables", "{foo: string}", "--content.template-id", "templateId", "--content.template-variables", "{foo: string}", "--email-html-body", "emailHtmlBody", @@ -68,6 +70,10 @@ func TestBroadcastsCreate(t *testing.T) { " mediaId: mediaId\n" + " mediaUrl: mediaUrl\n" + " mimeType: mimeType\n" + + " templateButtonVariables:\n" + + " foo: string\n" + + " templateHeaderVariables:\n" + + " foo: string\n" + " templateId: templateId\n" + " templateVariables:\n" + " foo: string\n" + @@ -108,7 +114,7 @@ func TestBroadcastsUpdate(t *testing.T) { "--api-key", "string", "broadcasts", "update", "--broadcast-id", "broadcastId", - "--content", "{filename: filename, mediaId: mediaId, mediaUrl: mediaUrl, mimeType: mimeType, templateId: templateId, templateVariables: {foo: string}}", + "--content", "{filename: filename, mediaId: mediaId, mediaUrl: mediaUrl, mimeType: mimeType, templateButtonVariables: {foo: string}, templateHeaderVariables: {foo: string}, templateId: templateId, templateVariables: {foo: string}}", "--email-html-body", "emailHtmlBody", "--email-subject", "emailSubject", "--metadata", "{foo: string}", @@ -131,6 +137,8 @@ func TestBroadcastsUpdate(t *testing.T) { "--content.media-id", "mediaId", "--content.media-url", "mediaUrl", "--content.mime-type", "mimeType", + "--content.template-button-variables", "{foo: string}", + "--content.template-header-variables", "{foo: string}", "--content.template-id", "templateId", "--content.template-variables", "{foo: string}", "--email-html-body", "emailHtmlBody", @@ -149,6 +157,10 @@ func TestBroadcastsUpdate(t *testing.T) { " mediaId: mediaId\n" + " mediaUrl: mediaUrl\n" + " mimeType: mimeType\n" + + " templateButtonVariables:\n" + + " foo: string\n" + + " templateHeaderVariables:\n" + + " foo: string\n" + " templateId: templateId\n" + " templateVariables:\n" + " foo: string\n" + @@ -206,6 +218,18 @@ func TestBroadcastsCancel(t *testing.T) { }) } +func TestBroadcastsEscalateReview(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "broadcasts", "escalate-review", + "--broadcast-id", "broadcastId", + ) + }) +} + func TestBroadcastsProgress(t *testing.T) { t.Skip("Mock server tests are disabled") t.Run("regular flags", func(t *testing.T) { @@ -242,6 +266,18 @@ func TestBroadcastsReschedule(t *testing.T) { }) } +func TestBroadcastsRetryReview(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "broadcasts", "retry-review", + "--broadcast-id", "broadcastId", + ) + }) +} + func TestBroadcastsSend(t *testing.T) { t.Skip("Mock server tests are disabled") t.Run("regular flags", func(t *testing.T) { diff --git a/pkg/cmd/broadcastcontact.go b/pkg/cmd/broadcastcontact.go index 07520a4..04ae086 100644 --- a/pkg/cmd/broadcastcontact.go +++ b/pkg/cmd/broadcastcontact.go @@ -5,7 +5,6 @@ package cmd import ( "context" "fmt" - "os" "github.com/tidwall/gjson" "github.com/urfave/cli/v3" @@ -21,8 +20,9 @@ var broadcastsContactsList = cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "broadcast-id", - Required: true, + Name: "broadcast-id", + Required: true, + PathParam: "broadcastId", }, &requestflag.Flag[string]{ Name: "cursor", @@ -53,8 +53,9 @@ var broadcastsContactsAdd = requestflag.WithInnerFlags(cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "broadcast-id", - Required: true, + Name: "broadcast-id", + Required: true, + PathParam: "broadcastId", }, &requestflag.Flag[[]map[string]any]{ Name: "contact", @@ -72,9 +73,19 @@ var broadcastsContactsAdd = requestflag.WithInnerFlags(cli.Command{ Usage: "Phone number (E.164) or email address.", InnerField: "recipient", }, + &requestflag.InnerFlag[map[string]any]{ + Name: "contact.template-button-variables", + Usage: "Per-contact button variables for dynamic URL/OTP buttons. Keys are the button index (0, 1, 2).", + InnerField: "templateButtonVariables", + }, + &requestflag.InnerFlag[map[string]any]{ + Name: "contact.template-header-variables", + Usage: "Per-contact value for a text-header variable, keyed by `1`. If omitted, Zavu resolves the header from `templateVariables` by the header placeholder's name.", + InnerField: "templateHeaderVariables", + }, &requestflag.InnerFlag[map[string]any]{ Name: "contact.template-variables", - Usage: "Per-contact template variables to personalize the message.", + Usage: "Per-contact body variables. Key them to match the template body: by position (`1`, `2`, ...) for positional templates, or by name (e.g. `customer_name`) for named templates. Zavu detects the template's format and sends the correct payload to Meta. Do not mix positional and named keys.", InnerField: "templateVariables", }, }, @@ -86,12 +97,14 @@ var broadcastsContactsRemove = cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "broadcast-id", - Required: true, + Name: "broadcast-id", + Required: true, + PathParam: "broadcastId", }, &requestflag.Flag[string]{ - Name: "contact-id", - Required: true, + Name: "contact-id", + Required: true, + PathParam: "contactId", }, }, Action: handleBroadcastsContactsRemove, @@ -109,8 +122,6 @@ func handleBroadcastsContactsList(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := zavudev.BroadcastContactListParams{} - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -122,7 +133,10 @@ func handleBroadcastsContactsList(ctx context.Context, cmd *cli.Command) error { return err } + params := zavudev.BroadcastContactListParams{} + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") if format == "raw" { var res []byte @@ -137,7 +151,13 @@ func handleBroadcastsContactsList(ctx context.Context, cmd *cli.Command) error { return err } obj := gjson.ParseBytes(res) - return ShowJSON(os.Stdout, "broadcasts:contacts list", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "broadcasts:contacts list", + Transform: transform, + }) } else { iter := client.Broadcasts.Contacts.ListAutoPaging( ctx, @@ -149,7 +169,13 @@ func handleBroadcastsContactsList(ctx context.Context, cmd *cli.Command) error { if cmd.IsSet("max-items") { maxItems = cmd.Value("max-items").(int64) } - return ShowJSONIterator(os.Stdout, "broadcasts:contacts list", iter, format, transform, maxItems) + return ShowJSONIterator(iter, maxItems, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "broadcasts:contacts list", + Transform: transform, + }) } } @@ -164,8 +190,6 @@ func handleBroadcastsContactsAdd(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := zavudev.BroadcastContactAddParams{} - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -177,6 +201,8 @@ func handleBroadcastsContactsAdd(ctx context.Context, cmd *cli.Command) error { return err } + params := zavudev.BroadcastContactAddParams{} + var res []byte options = append(options, option.WithResponseBodyInto(&res)) _, err = client.Broadcasts.Contacts.Add( @@ -191,8 +217,15 @@ func handleBroadcastsContactsAdd(ctx context.Context, cmd *cli.Command) error { obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "broadcasts:contacts add", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "broadcasts:contacts add", + Transform: transform, + }) } func handleBroadcastsContactsRemove(ctx context.Context, cmd *cli.Command) error { @@ -206,10 +239,6 @@ func handleBroadcastsContactsRemove(ctx context.Context, cmd *cli.Command) error return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := zavudev.BroadcastContactRemoveParams{ - BroadcastID: cmd.Value("broadcast-id").(string), - } - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -221,6 +250,10 @@ func handleBroadcastsContactsRemove(ctx context.Context, cmd *cli.Command) error return err } + params := zavudev.BroadcastContactRemoveParams{ + BroadcastID: cmd.Value("broadcast-id").(string), + } + return client.Broadcasts.Contacts.Remove( ctx, cmd.Value("contact-id").(string), diff --git a/pkg/cmd/broadcastcontact_test.go b/pkg/cmd/broadcastcontact_test.go index 488f14a..90846ad 100644 --- a/pkg/cmd/broadcastcontact_test.go +++ b/pkg/cmd/broadcastcontact_test.go @@ -33,8 +33,8 @@ func TestBroadcastsContactsAdd(t *testing.T) { "--api-key", "string", "broadcasts:contacts", "add", "--broadcast-id", "broadcastId", - "--contact", "{recipient: '+14155551234', templateVariables: {name: John, order_id: ORD-001}}", - "--contact", "{recipient: '+14155555678', templateVariables: {name: Jane, order_id: ORD-002}}", + "--contact", "{recipient: '+14155551234', templateButtonVariables: {'0': abc-report-token}, templateHeaderVariables: {'1': Jorge y Laura}, templateVariables: {name: John, order_id: ORD-001}}", + "--contact", "{recipient: '+14155555678', templateButtonVariables: {'0': abc-report-token}, templateHeaderVariables: {'1': Jorge y Laura}, templateVariables: {name: Jane, order_id: ORD-002}}", ) }) @@ -49,8 +49,12 @@ func TestBroadcastsContactsAdd(t *testing.T) { "broadcasts:contacts", "add", "--broadcast-id", "broadcastId", "--contact.recipient", "+14155551234", + "--contact.template-button-variables", "{'0': abc-report-token}", + "--contact.template-header-variables", "{'1': Jorge y Laura}", "--contact.template-variables", "{name: John, order_id: ORD-001}", "--contact.recipient", "+14155555678", + "--contact.template-button-variables", "{'0': abc-report-token}", + "--contact.template-header-variables", "{'1': Jorge y Laura}", "--contact.template-variables", "{name: Jane, order_id: ORD-002}", ) }) @@ -60,10 +64,18 @@ func TestBroadcastsContactsAdd(t *testing.T) { pipeData := []byte("" + "contacts:\n" + " - recipient: '+14155551234'\n" + + " templateButtonVariables:\n" + + " '0': abc-report-token\n" + + " templateHeaderVariables:\n" + + " '1': Jorge y Laura\n" + " templateVariables:\n" + " name: John\n" + " order_id: ORD-001\n" + " - recipient: '+14155555678'\n" + + " templateButtonVariables:\n" + + " '0': abc-report-token\n" + + " templateHeaderVariables:\n" + + " '1': Jorge y Laura\n" + " templateVariables:\n" + " name: Jane\n" + " order_id: ORD-002\n") diff --git a/pkg/cmd/call.go b/pkg/cmd/call.go new file mode 100644 index 0000000..d2650fa --- /dev/null +++ b/pkg/cmd/call.go @@ -0,0 +1,299 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "context" + "fmt" + + "github.com/tidwall/gjson" + "github.com/urfave/cli/v3" + "github.com/zavudev/cli/internal/apiquery" + "github.com/zavudev/cli/internal/requestflag" + "github.com/zavudev/sdk-go" + "github.com/zavudev/sdk-go/option" +) + +var callsCreate = cli.Command{ + Name: "create", + Usage: "Place an outbound voice call answered by the voice agent configured on the\nsender. Zavu dials the recipient and runs the conversation through its managed\nvoice pipeline (speech recognition, the agent's LLM, and speech synthesis, with\nreal-time interruption handling).", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "to", + Usage: "Recipient phone number in E.164 format.", + Required: true, + BodyPath: "to", + }, + &requestflag.Flag[string]{ + Name: "greeting", + Usage: "Overrides the agent's configured greeting for this call only.", + BodyPath: "greeting", + }, + &requestflag.Flag[string]{ + Name: "language", + Usage: "Language the agent speaks on this call only, as a BCP-47 tag (`en`, `es`, `es-ES`, `pt-BR`), or `auto` to detect the caller's language and follow it. Overrides the agent's configured language for speech recognition, the agent's replies, and the synthesized voice. If the agent uses a custom voice you supplied, that voice is kept and only the language changes. When omitted, the agent's configured language is used.", + BodyPath: "language", + }, + &requestflag.Flag[int64]{ + Name: "max-duration-minutes", + Usage: "Overrides the agent's maximum call duration for this call only.", + BodyPath: "maxDurationMinutes", + }, + &requestflag.Flag[map[string]any]{ + Name: "metadata", + Usage: "Arbitrary metadata to associate with the call. Returned on the call object and included in voice webhooks.", + BodyPath: "metadata", + }, + &requestflag.Flag[string]{ + Name: "sender-id", + Usage: "Sender profile that places the call. Uses the project's default sender if omitted. The sender's agent must have voice enabled.", + BodyPath: "senderId", + }, + }, + Action: handleCallsCreate, + HideHelpCommand: true, +} + +var callsRetrieve = cli.Command{ + Name: "retrieve", + Usage: "Retrieve a single voice call, including its full transcript once the\nconversation has produced turns.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "call-id", + Required: true, + PathParam: "callId", + }, + }, + Action: handleCallsRetrieve, + HideHelpCommand: true, +} + +var callsList = cli.Command{ + Name: "list", + Usage: "List voice calls for this project, most recent first. Transcripts are omitted\nfrom the list; fetch a single call to get its transcript.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "cursor", + QueryPath: "cursor", + }, + &requestflag.Flag[string]{ + Name: "direction", + Usage: "Whether the call was placed by Zavu (outbound) or received from a caller (inbound).", + QueryPath: "direction", + }, + &requestflag.Flag[int64]{ + Name: "limit", + Default: 50, + QueryPath: "limit", + }, + &requestflag.Flag[string]{ + Name: "status", + Usage: "Lifecycle status of a voice call.\n- `queued`: outbound call created, not yet dialing.\n- `ringing`: dialing (outbound) or received and ringing (inbound).\n- `in_progress`: answered, the agent is connected.\n- `completed`: ended after a conversation.\n- `failed`: could not be completed.\n- `busy`: the line was busy.\n- `no_answer`: rang but was not answered.\n- `canceled`: canceled before it was answered.", + QueryPath: "status", + }, + &requestflag.Flag[int64]{ + Name: "max-items", + Usage: "The maximum number of items to return (use -1 for unlimited).", + }, + }, + Action: handleCallsList, + HideHelpCommand: true, +} + +var callsHangup = cli.Command{ + Name: "hangup", + Usage: "End an active voice call. The call must still be ringing or in progress. Not\navailable with test-mode API keys.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "call-id", + Required: true, + PathParam: "callId", + }, + }, + Action: handleCallsHangup, + HideHelpCommand: true, +} + +func handleCallsCreate(ctx context.Context, cmd *cli.Command) error { + client := zavudev.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + + options, err := flagOptions( + cmd, + apiquery.NestedQueryFormatBrackets, + apiquery.ArrayQueryFormatComma, + ApplicationJSON, + false, + ) + if err != nil { + return err + } + + params := zavudev.CallNewParams{} + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Calls.New(ctx, params, options...) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") + transform := cmd.Root().String("transform") + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "calls create", + Transform: transform, + }) +} + +func handleCallsRetrieve(ctx context.Context, cmd *cli.Command) error { + client := zavudev.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("call-id") && len(unusedArgs) > 0 { + cmd.Set("call-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 + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Calls.Get(ctx, cmd.Value("call-id").(string), options...) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") + transform := cmd.Root().String("transform") + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "calls retrieve", + Transform: transform, + }) +} + +func handleCallsList(ctx context.Context, cmd *cli.Command) error { + client := zavudev.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + + 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 + } + + params := zavudev.CallListParams{} + + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") + transform := cmd.Root().String("transform") + if format == "raw" { + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Calls.List(ctx, params, options...) + if err != nil { + return err + } + obj := gjson.ParseBytes(res) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "calls list", + Transform: transform, + }) + } else { + iter := client.Calls.ListAutoPaging(ctx, params, options...) + maxItems := int64(-1) + if cmd.IsSet("max-items") { + maxItems = cmd.Value("max-items").(int64) + } + return ShowJSONIterator(iter, maxItems, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "calls list", + Transform: transform, + }) + } +} + +func handleCallsHangup(ctx context.Context, cmd *cli.Command) error { + client := zavudev.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("call-id") && len(unusedArgs) > 0 { + cmd.Set("call-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 + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Calls.Hangup(ctx, cmd.Value("call-id").(string), options...) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") + transform := cmd.Root().String("transform") + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "calls hangup", + Transform: transform, + }) +} diff --git a/pkg/cmd/call_test.go b/pkg/cmd/call_test.go new file mode 100644 index 0000000..531443d --- /dev/null +++ b/pkg/cmd/call_test.go @@ -0,0 +1,83 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "testing" + + "github.com/zavudev/cli/internal/mocktest" +) + +func TestCallsCreate(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "calls", "create", + "--to", "+56912345678", + "--greeting", "greeting", + "--language", "es-ES", + "--max-duration-minutes", "1", + "--metadata", "{foo: string}", + "--sender-id", "sender_12345", + ) + }) + + t.Run("piping data", func(t *testing.T) { + // Test piping YAML data over stdin + pipeData := []byte("" + + "to: '+56912345678'\n" + + "greeting: greeting\n" + + "language: es-ES\n" + + "maxDurationMinutes: 1\n" + + "metadata:\n" + + " foo: string\n" + + "senderId: sender_12345\n") + mocktest.TestRunMockTestWithPipeAndFlags( + t, pipeData, + "--api-key", "string", + "calls", "create", + ) + }) +} + +func TestCallsRetrieve(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "calls", "retrieve", + "--call-id", "callId", + ) + }) +} + +func TestCallsList(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "calls", "list", + "--max-items", "10", + "--cursor", "cursor", + "--direction", "inbound", + "--limit", "100", + "--status", "queued", + ) + }) +} + +func TestCallsHangup(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "calls", "hangup", + "--call-id", "callId", + ) + }) +} diff --git a/pkg/cmd/cmd.go b/pkg/cmd/cmd.go index 69f46c9..75183ac 100644 --- a/pkg/cmd/cmd.go +++ b/pkg/cmd/cmd.go @@ -73,6 +73,11 @@ func init() { Name: "transform-error", Usage: "The GJSON transformation for errors.", }, + &cli.BoolFlag{ + Name: "raw-output", + Aliases: []string{"r"}, + Usage: "If the result is a string, print it without JSON quotes. This can be useful for making output transforms talk to non-JSON-based systems.", + }, &requestflag.Flag[string]{ Name: "api-key", Sources: cli.EnvVars("ZAVUDEV_API_KEY"), @@ -86,8 +91,10 @@ func init() { Commands: []*cli.Command{ &messagesRetrieve, &messagesList, + &messagesListAttachments, &messagesReact, &messagesSend, + &messagesShowTyping, }, }, { @@ -100,6 +107,7 @@ func init() { &templatesList, &templatesDelete, &templatesSubmit, + &templatesSync, }, }, { @@ -135,6 +143,7 @@ func init() { Category: "API RESOURCE", Suggest: true, Commands: []*cli.Command{ + &sendersAgentExecutionsRetrieve, &sendersAgentExecutionsList, }, }, @@ -161,9 +170,18 @@ func init() { &sendersAgentToolsUpdate, &sendersAgentToolsList, &sendersAgentToolsDelete, + &sendersAgentToolsListTestRuns, &sendersAgentToolsTest, }, }, + { + Name: "senders:agent:tools:webhook", + Category: "API RESOURCE", + Suggest: true, + Commands: []*cli.Command{ + &sendersAgentToolsWebhookRotateSecret, + }, + }, { Name: "senders:agent:knowledge-bases", Category: "API RESOURCE", @@ -184,6 +202,27 @@ func init() { &sendersAgentKnowledgeBasesDocumentsCreate, &sendersAgentKnowledgeBasesDocumentsList, &sendersAgentKnowledgeBasesDocumentsDelete, + &sendersAgentKnowledgeBasesDocumentsRetrieveDocument, + &sendersAgentKnowledgeBasesDocumentsUpdateDocument, + }, + }, + { + Name: "senders:whatsapp-sync", + Category: "API RESOURCE", + Suggest: true, + Commands: []*cli.Command{ + &sendersWhatsappSyncRetrieve, + &sendersWhatsappSyncStartContactsSync, + &sendersWhatsappSyncStartHistorySync, + }, + }, + { + Name: "senders:telegram", + Category: "API RESOURCE", + Suggest: true, + Commands: []*cli.Command{ + &sendersTelegramConnect, + &sendersTelegramDisconnect, }, }, { @@ -191,12 +230,26 @@ func init() { Category: "API RESOURCE", Suggest: true, Commands: []*cli.Command{ + &contactsCreate, &contactsRetrieve, &contactsUpdate, &contactsList, + &contactsDelete, + &contactsMerge, &contactsRetrieveByPhone, }, }, + { + Name: "contacts:channels", + Category: "API RESOURCE", + Suggest: true, + Commands: []*cli.Command{ + &contactsChannelsUpdate, + &contactsChannelsAdd, + &contactsChannelsRemove, + &contactsChannelsSetPrimary, + }, + }, { Name: "broadcasts", Category: "API RESOURCE", @@ -208,8 +261,10 @@ func init() { &broadcastsList, &broadcastsDelete, &broadcastsCancel, + &broadcastsEscalateReview, &broadcastsProgress, &broadcastsReschedule, + &broadcastsRetryReview, &broadcastsSend, }, }, @@ -228,6 +283,7 @@ func init() { Category: "API RESOURCE", Suggest: true, Commands: []*cli.Command{ + &introspectValidateEmail, &introspectValidatePhone, }, }, @@ -268,6 +324,222 @@ func init() { ®ulatoryDocumentsUploadURL, }, }, + { + Name: "invitations", + Category: "API RESOURCE", + Suggest: true, + Commands: []*cli.Command{ + &invitationsCreate, + &invitationsRetrieve, + &invitationsList, + &invitationsCancel, + }, + }, + { + Name: "urls", + Category: "API RESOURCE", + Suggest: true, + Commands: []*cli.Command{ + &urlsEscalate, + &urlsListVerified, + &urlsRetrieveDetails, + &urlsSubmitForVerification, + }, + }, + { + Name: "balance", + Category: "API RESOURCE", + Suggest: true, + Commands: []*cli.Command{ + &balanceRetrieve, + }, + }, + { + Name: "sub-accounts", + Category: "API RESOURCE", + Suggest: true, + Commands: []*cli.Command{ + &subAccountsCreate, + &subAccountsRetrieve, + &subAccountsUpdate, + &subAccountsList, + &subAccountsDeactivate, + &subAccountsGetBalance, + }, + }, + { + Name: "sub-accounts:api-keys", + Category: "API RESOURCE", + Suggest: true, + Commands: []*cli.Command{ + &subAccountsAPIKeysCreate, + &subAccountsAPIKeysList, + &subAccountsAPIKeysRevoke, + }, + }, + { + Name: "number-10dlc:brands", + Category: "API RESOURCE", + Suggest: true, + Commands: []*cli.Command{ + &number10dlcBrandsCreate, + &number10dlcBrandsRetrieve, + &number10dlcBrandsUpdate, + &number10dlcBrandsList, + &number10dlcBrandsDelete, + &number10dlcBrandsListUseCases, + &number10dlcBrandsSubmit, + &number10dlcBrandsSyncStatus, + }, + }, + { + Name: "number-10dlc:campaigns", + Category: "API RESOURCE", + Suggest: true, + Commands: []*cli.Command{ + &number10dlcCampaignsCreate, + &number10dlcCampaignsRetrieve, + &number10dlcCampaignsUpdate, + &number10dlcCampaignsList, + &number10dlcCampaignsDelete, + &number10dlcCampaignsSubmit, + &number10dlcCampaignsSyncStatus, + }, + }, + { + Name: "number-10dlc:campaigns:phone-numbers", + Category: "API RESOURCE", + Suggest: true, + Commands: []*cli.Command{ + &number10dlcCampaignsPhoneNumbersList, + &number10dlcCampaignsPhoneNumbersAssign, + &number10dlcCampaignsPhoneNumbersUnassign, + }, + }, + { + Name: "me", + Category: "API RESOURCE", + Suggest: true, + Commands: []*cli.Command{ + &meRetrieve, + }, + }, + { + Name: "functions", + Category: "API RESOURCE", + Suggest: true, + Commands: []*cli.Command{ + &functionsCreate, + &functionsRetrieve, + &functionsUpdate, + &functionsDelete, + &functionsDeploy, + &functionsGetDeployment, + &functionsListDeployments, + &functionsListEventTypes, + &functionsRollbackDeployment, + &functionsTailLogs, + }, + }, + { + Name: "functions:secrets", + Category: "API RESOURCE", + Suggest: true, + Commands: []*cli.Command{ + &functionsSecretsList, + &functionsSecretsSet, + &functionsSecretsUnset, + }, + }, + { + Name: "functions:triggers", + Category: "API RESOURCE", + Suggest: true, + Commands: []*cli.Command{ + &functionsTriggersCreate, + &functionsTriggersUpdate, + &functionsTriggersList, + &functionsTriggersDelete, + }, + }, + { + Name: "functions:git-link", + Category: "API RESOURCE", + Suggest: true, + Commands: []*cli.Command{ + &functionsGitLinkRetrieve, + &functionsGitLinkUpdate, + &functionsGitLinkDeployNow, + &functionsGitLinkLink, + &functionsGitLinkUnlink, + }, + }, + { + Name: "conversations", + Category: "API RESOURCE", + Suggest: true, + Commands: []*cli.Command{ + &conversationsRetrieve, + &conversationsList, + &conversationsListMessages, + &conversationsMarkAsRead, + }, + }, + { + Name: "calls", + Category: "API RESOURCE", + Suggest: true, + Commands: []*cli.Command{ + &callsCreate, + &callsRetrieve, + &callsList, + &callsHangup, + }, + }, + { + Name: "agent-templates", + Category: "API RESOURCE", + Suggest: true, + Commands: []*cli.Command{ + &agentTemplatesRetrieve, + &agentTemplatesList, + }, + }, + { + Name: "email-domains", + Category: "API RESOURCE", + Suggest: true, + Commands: []*cli.Command{ + &emailDomainsCreate, + &emailDomainsRetrieve, + &emailDomainsList, + &emailDomainsDelete, + &emailDomainsVerify, + }, + }, + { + Name: "agents", + Category: "API RESOURCE", + Suggest: true, + Commands: []*cli.Command{ + &agentsCreate, + &agentsRetrieve, + &agentsUpdate, + &agentsList, + &agentsDelete, + &agentsListVoices, + &agentsTest, + }, + }, + { + Name: "agents:senders", + Category: "API RESOURCE", + Suggest: true, + Commands: []*cli.Command{ + &agentsSendersConnect, + &agentsSendersDisconnect, + }, + }, { Name: "@manpages", Usage: "Generate documentation for 'man'", diff --git a/pkg/cmd/cmdutil.go b/pkg/cmd/cmdutil.go index 8b3da62..251d3a1 100644 --- a/pkg/cmd/cmdutil.go +++ b/pkg/cmd/cmdutil.go @@ -311,21 +311,29 @@ func shouldUseColors(w io.Writer) bool { return isTerminal(w) } -func formatJSON(expectedOutput *os.File, title string, res gjson.Result, format string, transform string) ([]byte, error) { - if transform != "" { - transformed := res.Get(transform) +func formatJSON(res gjson.Result, opts ShowJSONOpts) ([]byte, error) { + if opts.Transform != "" { + transformed := res.Get(opts.Transform) if transformed.Exists() { res = transformed } } - switch strings.ToLower(format) { + // Modeled after `jq -r` (`--raw-output`): if the result is a string, print it without JSON quotes so that + // it's easier to pipe into other programs. + if opts.RawOutput && res.Type == gjson.String { + return []byte(res.Str + "\n"), nil + } + switch strings.ToLower(opts.Format) { case "auto": - return formatJSON(expectedOutput, title, res, "json", "") + autoOpts := opts + autoOpts.Format = "json" + autoOpts.Transform = "" + return formatJSON(res, autoOpts) case "pretty": - return []byte(jsonview.RenderJSON(title, res) + "\n"), nil + return []byte(jsonview.RenderJSON(opts.Title, res) + "\n"), nil case "json": prettyJSON := pretty.Pretty([]byte(res.Raw)) - if shouldUseColors(expectedOutput) { + if shouldUseColors(opts.Stdout) { return pretty.Color(prettyJSON, pretty.TerminalStyle), nil } else { return prettyJSON, nil @@ -333,7 +341,7 @@ func formatJSON(expectedOutput *os.File, title string, res gjson.Result, format case "jsonl": // @ugly is gjson syntax for "no whitespace", so it fits on one line oneLineJSON := res.Get("@ugly").Raw - if shouldUseColors(expectedOutput) { + if shouldUseColors(opts.Stdout) { bytes := append(pretty.Color([]byte(oneLineJSON), pretty.TerminalStyle), '\n') return bytes, nil } else { @@ -347,34 +355,67 @@ func formatJSON(expectedOutput *os.File, title string, res gjson.Result, format if err := json2yaml.Convert(&yaml, input); err != nil { return nil, err } - _, err := expectedOutput.Write([]byte(yaml.String())) + _, err := opts.Stdout.Write([]byte(yaml.String())) return nil, err default: - return nil, fmt.Errorf("Invalid format: %s, valid formats are: %s", format, strings.Join(OutputFormats, ", ")) + return nil, fmt.Errorf("Invalid format: %s, valid formats are: %s", opts.Format, strings.Join(OutputFormats, ", ")) } } -// Display JSON to the user in various different formats -func ShowJSON(out *os.File, title string, res gjson.Result, format string, transform string) error { - if transform != "" { - transformed := res.Get(transform) - if transformed.Exists() { - res = transformed - } +const warningExploreNotSupported = "Warning: Output format 'explore' not supported for non-terminal output; falling back to 'json'\n" + +// ShowJSONOpts configures how JSON output is displayed. +type ShowJSONOpts struct { + ExplicitFormat bool // true if the user explicitly passed --format + Format string // output format (auto, explore, json, jsonl, pretty, raw, yaml) + RawOutput bool // like jq -r: print strings without JSON quotes + Stderr io.Writer // stderr for warnings; injectable for testing; defaults to os.Stderr + Stdout *os.File // stdout (or pager); injectable for testing; defaults to os.Stdout + Title string // display title + Transform string // GJSON path to extract before displaying +} + +func (o *ShowJSONOpts) setDefaults() { + if o.Stderr == nil { + o.Stderr = os.Stderr + } + if o.Stdout == nil { + o.Stdout = os.Stdout } +} + +// ShowJSON displays a single JSON result to the user. +func ShowJSON(res gjson.Result, opts ShowJSONOpts) error { + opts.setDefaults() - switch strings.ToLower(format) { + switch strings.ToLower(opts.Format) { case "auto": - return ShowJSON(out, title, res, "json", "") + autoOpts := opts + autoOpts.Format = "json" + return ShowJSON(res, autoOpts) case "explore": - return jsonview.ExploreJSON(title, res) + if !isTerminal(opts.Stdout) { + if opts.ExplicitFormat { + fmt.Fprint(opts.Stderr, warningExploreNotSupported) + } + jsonOpts := opts + jsonOpts.Format = "json" + return ShowJSON(res, jsonOpts) + } + if opts.Transform != "" { + transformed := res.Get(opts.Transform) + if transformed.Exists() { + res = transformed + } + } + return jsonview.ExploreJSON(opts.Title, res) default: - bytes, err := formatJSON(out, title, res, format, transform) + bytes, err := formatJSON(res, opts) if err != nil { return err } - _, err = out.Write(bytes) + _, err = opts.Stdout.Write(bytes) return err } } @@ -388,12 +429,18 @@ type hasRawJSON interface { RawJSON() string } -// For an iterator over different value types, display its values to the user in -// different formats. -// -1 is used to signal no limit of items to display -func ShowJSONIterator[T any](stdout *os.File, title string, iter jsonview.Iterator[T], format string, transform string, itemsToDisplay int64) error { - if format == "explore" { - return jsonview.ExploreJSONStream(title, iter) +// ShowJSONIterator displays an iterator of values to the user. Use itemsToDisplay = -1 for no limit. +func ShowJSONIterator[T any](iter jsonview.Iterator[T], itemsToDisplay int64, opts ShowJSONOpts) error { + opts.setDefaults() + + if opts.Format == "explore" { + if isTerminal(opts.Stdout) { + return jsonview.ExploreJSONStream(opts.Title, iter) + } + if opts.ExplicitFormat { + fmt.Fprint(opts.Stderr, warningExploreNotSupported) + } + opts.Format = "json" } terminalWidth, terminalHeight, err := term.GetSize(os.Stdout.Fd()) @@ -419,7 +466,7 @@ func ShowJSONIterator[T any](stdout *os.File, title string, iter jsonview.Iterat } obj = gjson.ParseBytes(jsonData) } - json, err := formatJSON(stdout, title, obj, format, transform) + json, err := formatJSON(obj, opts) if err != nil { return err } @@ -436,7 +483,7 @@ func ShowJSONIterator[T any](stdout *os.File, title string, iter jsonview.Iterat } if !usePager { - _, err := stdout.Write(output) + _, err := opts.Stdout.Write(output) if err != nil { return err } @@ -444,13 +491,15 @@ func ShowJSONIterator[T any](stdout *os.File, title string, iter jsonview.Iterat return iter.Err() } - return streamOutput(title, func(pager *os.File) error { - // Write the output we used during the initial terminal size computation + return streamOutput(opts.Title, func(pager *os.File) error { _, err := pager.Write(output) if err != nil { return err } + pagerOpts := opts + pagerOpts.Stdout = pager + for iter.Next() { if itemsToDisplay == 0 { break @@ -466,7 +515,7 @@ func ShowJSONIterator[T any](stdout *os.File, title string, iter jsonview.Iterat } obj = gjson.ParseBytes(jsonData) } - if err := ShowJSON(pager, title, obj, format, transform); err != nil { + if err := ShowJSON(obj, pagerOpts); err != nil { return err } itemsToDisplay -= 1 diff --git a/pkg/cmd/cmdutil_test.go b/pkg/cmd/cmdutil_test.go index 782496a..518985b 100644 --- a/pkg/cmd/cmdutil_test.go +++ b/pkg/cmd/cmdutil_test.go @@ -159,7 +159,7 @@ func TestFormatJSON(t *testing.T) { t.Parallel() res := gjson.Parse(`{"id":"abc123","name":"test"}`) - formatted, err := formatJSON(os.Stdout, "test", res, "raw", "id") + formatted, err := formatJSON(res, ShowJSONOpts{Format: "raw", Stdout: os.Stdout, Transform: "id"}) require.NoError(t, err) require.Equal(t, `"abc123"`+"\n", string(formatted)) }) @@ -168,7 +168,7 @@ func TestFormatJSON(t *testing.T) { t.Parallel() res := gjson.Parse(`{"id":"abc123","name":"test"}`) - formatted, err := formatJSON(os.Stdout, "test", res, "raw", "") + formatted, err := formatJSON(res, ShowJSONOpts{Format: "raw", Stdout: os.Stdout}) require.NoError(t, err) require.Equal(t, `{"id":"abc123","name":"test"}`+"\n", string(formatted)) }) @@ -177,7 +177,7 @@ func TestFormatJSON(t *testing.T) { t.Parallel() res := gjson.Parse(`{"data":{"items":[1,2,3]}}`) - formatted, err := formatJSON(os.Stdout, "test", res, "raw", "data.items") + formatted, err := formatJSON(res, ShowJSONOpts{Format: "raw", Stdout: os.Stdout, Transform: "data.items"}) require.NoError(t, err) require.Equal(t, "[1,2,3]\n", string(formatted)) }) @@ -186,11 +186,40 @@ func TestFormatJSON(t *testing.T) { t.Parallel() res := gjson.Parse(`{"id":"abc123"}`) - formatted, err := formatJSON(os.Stdout, "test", res, "raw", "missing") + formatted, err := formatJSON(res, ShowJSONOpts{Format: "raw", Stdout: os.Stdout, Transform: "missing"}) require.NoError(t, err) // Transform path doesn't exist, so original result is returned require.Equal(t, `{"id":"abc123"}`+"\n", string(formatted)) }) + + t.Run("RawOutputString", func(t *testing.T) { + t.Parallel() + + res := gjson.Parse(`{"id":"abc123","name":"test"}`) + formatted, err := formatJSON(res, ShowJSONOpts{Format: "json", Stdout: os.Stdout, Transform: "id", RawOutput: true}) + require.NoError(t, err) + require.Equal(t, "abc123\n", string(formatted)) + }) + + t.Run("RawOutputNonString", func(t *testing.T) { + t.Parallel() + + // --raw-output has no effect on non-string values + res := gjson.Parse(`{"count":42}`) + formatted, err := formatJSON(res, ShowJSONOpts{Format: "raw", Stdout: os.Stdout, Transform: "count", RawOutput: true}) + require.NoError(t, err) + require.Equal(t, "42\n", string(formatted)) + }) + + t.Run("RawOutputObject", func(t *testing.T) { + t.Parallel() + + // --raw-output has no effect on objects + res := gjson.Parse(`{"nested":{"a":1}}`) + formatted, err := formatJSON(res, ShowJSONOpts{Format: "raw", Stdout: os.Stdout, Transform: "nested", RawOutput: true}) + require.NoError(t, err) + require.Equal(t, `{"a":1}`+"\n", string(formatted)) + }) } func TestShowJSONIterator(t *testing.T) { @@ -231,6 +260,89 @@ func TestShowJSONIterator(t *testing.T) { }) } +func TestExploreFallback(t *testing.T) { + t.Parallel() + + t.Run("ShowJSONFallsBackToJsonOnNonTTY", func(t *testing.T) { + t.Parallel() + + // os.Pipe() produces a *os.File that isn't a terminal, so explore should fall back. + r, w, err := os.Pipe() + require.NoError(t, err) + defer r.Close() + + var stderr bytes.Buffer + res := gjson.Parse(`{"id":"abc"}`) + err = ShowJSON(res, ShowJSONOpts{ + Format: "explore", + Stderr: &stderr, + Stdout: w, + Title: "test", + }) + w.Close() + require.NoError(t, err) + + var buf bytes.Buffer + _, _ = buf.ReadFrom(r) + assert.Contains(t, buf.String(), `"id"`) + assert.Contains(t, buf.String(), `"abc"`) + }) + + t.Run("ShowJSONIteratorFallsBackToJsonOnNonTTY", func(t *testing.T) { + t.Parallel() + + iter := &sliceIterator[map[string]any]{items: []map[string]any{ + {"id": "abc"}, + }} + captured := captureShowJSONIterator(t, iter, "explore", "", -1) + assert.Contains(t, captured, `"id"`) + assert.Contains(t, captured, `"abc"`) + }) + + t.Run("ShowJSONWarnsWhenExplicitFormatOnNonTTY", func(t *testing.T) { + t.Parallel() + + r, w, err := os.Pipe() + require.NoError(t, err) + defer r.Close() + + var stderr bytes.Buffer + res := gjson.Parse(`{"id":"abc"}`) + err = ShowJSON(res, ShowJSONOpts{ + ExplicitFormat: true, + Format: "explore", + Stderr: &stderr, + Stdout: w, + Title: "test", + }) + w.Close() + require.NoError(t, err) + + assert.Equal(t, warningExploreNotSupported, stderr.String()) + }) + + t.Run("ShowJSONSilentWhenDefaultFormatOnNonTTY", func(t *testing.T) { + t.Parallel() + + r, w, err := os.Pipe() + require.NoError(t, err) + defer r.Close() + + var stderr bytes.Buffer + res := gjson.Parse(`{"id":"abc"}`) + err = ShowJSON(res, ShowJSONOpts{ + Format: "explore", + Stderr: &stderr, + Stdout: w, + Title: "test", + }) + w.Close() + require.NoError(t, err) + + assert.Empty(t, stderr.String(), "no warning expected when format was not explicit") + }) +} + // sliceIterator is a simple iterator over a slice for testing. type sliceIterator[T any] struct { index int @@ -260,7 +372,13 @@ func captureShowJSONIterator[T any](t *testing.T, iter jsonview.Iterator[T], for require.NoError(t, err) defer r.Close() - err = ShowJSONIterator(w, "test", iter, format, transform, itemsToDisplay) + err = ShowJSONIterator(iter, itemsToDisplay, ShowJSONOpts{ + Format: format, + Stderr: io.Discard, + Stdout: w, + Title: "test", + Transform: transform, + }) w.Close() require.NoError(t, err) diff --git a/pkg/cmd/contact.go b/pkg/cmd/contact.go index 9cd8f98..1f1fa1d 100644 --- a/pkg/cmd/contact.go +++ b/pkg/cmd/contact.go @@ -5,7 +5,6 @@ package cmd import ( "context" "fmt" - "os" "github.com/tidwall/gjson" "github.com/urfave/cli/v3" @@ -15,14 +14,69 @@ import ( "github.com/zavudev/sdk-go/option" ) +var contactsCreate = requestflag.WithInnerFlags(cli.Command{ + Name: "create", + Usage: "Create a new contact with one or more communication channels.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[[]map[string]any]{ + Name: "channel", + Usage: "Communication channels for the contact.", + Required: true, + BodyPath: "channels", + }, + &requestflag.Flag[string]{ + Name: "display-name", + Usage: "Display name for the contact.", + BodyPath: "displayName", + }, + &requestflag.Flag[map[string]any]{ + Name: "metadata", + Usage: "Arbitrary metadata to associate with the contact.", + BodyPath: "metadata", + }, + }, + Action: handleContactsCreate, + HideHelpCommand: true, +}, map[string][]requestflag.HasOuterFlag{ + "channel": { + &requestflag.InnerFlag[string]{ + Name: "channel.channel", + Usage: "Channel type.", + InnerField: "channel", + }, + &requestflag.InnerFlag[string]{ + Name: "channel.identifier", + Usage: "Channel identifier (phone number in E.164 format or email address).", + InnerField: "identifier", + }, + &requestflag.InnerFlag[string]{ + Name: "channel.country-code", + Usage: "ISO country code for phone numbers.", + InnerField: "countryCode", + }, + &requestflag.InnerFlag[bool]{ + Name: "channel.is-primary", + Usage: "Whether this should be the primary channel for its type.", + InnerField: "isPrimary", + }, + &requestflag.InnerFlag[string]{ + Name: "channel.label", + Usage: "Optional label for the channel.", + InnerField: "label", + }, + }, +}) + var contactsRetrieve = cli.Command{ Name: "retrieve", Usage: "Get contact", Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "contact-id", - Required: true, + Name: "contact-id", + Required: true, + PathParam: "contactId", }, }, Action: handleContactsRetrieve, @@ -35,14 +89,20 @@ var contactsUpdate = cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "contact-id", - Required: true, + Name: "contact-id", + Required: true, + PathParam: "contactId", }, - &requestflag.Flag[any]{ + &requestflag.Flag[*string]{ Name: "default-channel", Usage: "Preferred channel for this contact. Set to null to clear.", BodyPath: "defaultChannel", }, + &requestflag.Flag[*string]{ + Name: "display-name", + Usage: "Human-readable name for this contact. Set to null to clear it and fall back to the contact's identifier. Contacts created automatically from an inbound message have no display name until you set one.", + BodyPath: "displayName", + }, &requestflag.Flag[map[string]any]{ Name: "metadata", BodyPath: "metadata", @@ -59,6 +119,7 @@ var contactsList = cli.Command{ Flags: []cli.Flag{ &requestflag.Flag[string]{ Name: "cursor", + Usage: "Opaque cursor from a previous response's `nextCursor`. Do not construct it.", QueryPath: "cursor", }, &requestflag.Flag[int64]{ @@ -68,8 +129,19 @@ var contactsList = cli.Command{ }, &requestflag.Flag[string]{ Name: "phone-number", + Usage: "Exact match on the contact's primary phone number, in E.164.", QueryPath: "phoneNumber", }, + &requestflag.Flag[string]{ + Name: "search", + Usage: "Free-text match over the contact's name (`displayName` and the WhatsApp profile name), phone numbers and email addresses. Case- and accent-insensitive. A phone number matches on a trailing fragment too, so `5551234` finds `+14155551234`.\n\nContacts created automatically from an inbound message have no `displayName` — they are matched by their identifier until you set one with `PATCH /v1/contacts/{contactId}`.\n\nResults come back in relevance order rather than newest-first. `cursor` is opaque in both modes; pass back exactly what the previous response returned, and start a new pagination run when the search term changes.", + QueryPath: "search", + }, + &requestflag.Flag[[]string]{ + Name: "tag", + Usage: "Tag name. Repeatable: `?tag=vip&tag=chile` returns contacts carrying **every** tag given, not any of them — the same rule the dashboard filter applies.\n\nTags are matched by name, case-insensitively. An unknown tag returns 400 rather than being ignored, because a typo that silently matched every contact would be a worse answer than an error.", + QueryPath: "tag", + }, &requestflag.Flag[int64]{ Name: "max-items", Usage: "The maximum number of items to return (use -1 for unlimited).", @@ -79,20 +151,98 @@ var contactsList = cli.Command{ HideHelpCommand: true, } +var contactsDelete = cli.Command{ + Name: "delete", + Usage: "Permanently delete a contact and its communication channels. Implements\nright-to-erasure obligations under GDPR Art. 17, Ley 19.628 (Chile) Art. 12,\nCCPA § 1798.105, and LGPD Art. 18.VI. The contact, its channels, and any\nassociated agent flow sessions and conversation threads are removed. Past\nmessage records and broadcast delivery logs are retained for billing/audit but\nno longer reference the deleted contact.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "contact-id", + Required: true, + PathParam: "contactId", + }, + }, + Action: handleContactsDelete, + HideHelpCommand: true, +} + +var contactsMerge = cli.Command{ + Name: "merge", + Usage: "Merge a source contact into this contact. All channels from the source contact\nwill be moved to the target contact, and the source contact will be marked as\nmerged.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "contact-id", + Required: true, + PathParam: "contactId", + }, + &requestflag.Flag[string]{ + Name: "source-contact-id", + Usage: "ID of the contact to merge into the target contact. The source contact will be marked as merged.", + Required: true, + BodyPath: "sourceContactId", + }, + }, + Action: handleContactsMerge, + HideHelpCommand: true, +} + var contactsRetrieveByPhone = cli.Command{ Name: "retrieve-by-phone", Usage: "Get contact by phone number", Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "phone-number", - Required: true, + Name: "phone-number", + Required: true, + PathParam: "phoneNumber", }, }, Action: handleContactsRetrieveByPhone, HideHelpCommand: true, } +func handleContactsCreate(ctx context.Context, cmd *cli.Command) error { + client := zavudev.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + + options, err := flagOptions( + cmd, + apiquery.NestedQueryFormatBrackets, + apiquery.ArrayQueryFormatComma, + ApplicationJSON, + false, + ) + if err != nil { + return err + } + + params := zavudev.ContactNewParams{} + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Contacts.New(ctx, params, options...) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") + transform := cmd.Root().String("transform") + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "contacts create", + Transform: transform, + }) +} + func handleContactsRetrieve(ctx context.Context, cmd *cli.Command) error { client := zavudev.NewClient(getDefaultRequestOptions(cmd)...) unusedArgs := cmd.Args().Slice() @@ -124,8 +274,15 @@ func handleContactsRetrieve(ctx context.Context, cmd *cli.Command) error { obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "contacts retrieve", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "contacts retrieve", + Transform: transform, + }) } func handleContactsUpdate(ctx context.Context, cmd *cli.Command) error { @@ -139,8 +296,6 @@ func handleContactsUpdate(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := zavudev.ContactUpdateParams{} - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -152,6 +307,8 @@ func handleContactsUpdate(ctx context.Context, cmd *cli.Command) error { return err } + params := zavudev.ContactUpdateParams{} + var res []byte options = append(options, option.WithResponseBodyInto(&res)) _, err = client.Contacts.Update( @@ -166,8 +323,15 @@ func handleContactsUpdate(ctx context.Context, cmd *cli.Command) error { obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "contacts update", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "contacts update", + Transform: transform, + }) } func handleContactsList(ctx context.Context, cmd *cli.Command) error { @@ -178,8 +342,6 @@ func handleContactsList(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := zavudev.ContactListParams{} - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -191,7 +353,10 @@ func handleContactsList(ctx context.Context, cmd *cli.Command) error { return err } + params := zavudev.ContactListParams{} + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") if format == "raw" { var res []byte @@ -201,15 +366,101 @@ func handleContactsList(ctx context.Context, cmd *cli.Command) error { return err } obj := gjson.ParseBytes(res) - return ShowJSON(os.Stdout, "contacts list", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "contacts list", + Transform: transform, + }) } else { iter := client.Contacts.ListAutoPaging(ctx, params, options...) maxItems := int64(-1) if cmd.IsSet("max-items") { maxItems = cmd.Value("max-items").(int64) } - return ShowJSONIterator(os.Stdout, "contacts list", iter, format, transform, maxItems) + return ShowJSONIterator(iter, maxItems, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "contacts list", + Transform: transform, + }) + } +} + +func handleContactsDelete(ctx context.Context, cmd *cli.Command) error { + client := zavudev.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("contact-id") && len(unusedArgs) > 0 { + cmd.Set("contact-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.Contacts.Delete(ctx, cmd.Value("contact-id").(string), options...) +} + +func handleContactsMerge(ctx context.Context, cmd *cli.Command) error { + client := zavudev.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("contact-id") && len(unusedArgs) > 0 { + cmd.Set("contact-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, + ApplicationJSON, + false, + ) + if err != nil { + return err + } + + params := zavudev.ContactMergeParams{} + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Contacts.Merge( + ctx, + cmd.Value("contact-id").(string), + params, + options..., + ) + if err != nil { + return err } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") + transform := cmd.Root().String("transform") + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "contacts merge", + Transform: transform, + }) } func handleContactsRetrieveByPhone(ctx context.Context, cmd *cli.Command) error { @@ -243,6 +494,13 @@ func handleContactsRetrieveByPhone(ctx context.Context, cmd *cli.Command) error obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "contacts retrieve-by-phone", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "contacts retrieve-by-phone", + Transform: transform, + }) } diff --git a/pkg/cmd/contact_test.go b/pkg/cmd/contact_test.go index b11eb87..fb63cd9 100644 --- a/pkg/cmd/contact_test.go +++ b/pkg/cmd/contact_test.go @@ -6,8 +6,61 @@ import ( "testing" "github.com/zavudev/cli/internal/mocktest" + "github.com/zavudev/cli/internal/requestflag" ) +func TestContactsCreate(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "contacts", "create", + "--channel", "{channel: sms, identifier: '+14155551234', countryCode: US, isPrimary: true, label: work}", + "--display-name", "John Doe", + "--metadata", "{foo: string}", + ) + }) + + t.Run("inner flags", func(t *testing.T) { + // Check that inner flags have been set up correctly + requestflag.CheckInnerFlags(contactsCreate) + + // Alternative argument passing style using inner flags + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "contacts", "create", + "--channel.channel", "sms", + "--channel.identifier", "+14155551234", + "--channel.country-code", "US", + "--channel.is-primary=true", + "--channel.label", "work", + "--display-name", "John Doe", + "--metadata", "{foo: string}", + ) + }) + + t.Run("piping data", func(t *testing.T) { + // Test piping YAML data over stdin + pipeData := []byte("" + + "channels:\n" + + " - channel: sms\n" + + " identifier: '+14155551234'\n" + + " countryCode: US\n" + + " isPrimary: true\n" + + " label: work\n" + + "displayName: John Doe\n" + + "metadata:\n" + + " foo: string\n") + mocktest.TestRunMockTestWithPipeAndFlags( + t, pipeData, + "--api-key", "string", + "contacts", "create", + ) + }) +} + func TestContactsRetrieve(t *testing.T) { t.Skip("Mock server tests are disabled") t.Run("regular flags", func(t *testing.T) { @@ -29,6 +82,7 @@ func TestContactsUpdate(t *testing.T) { "contacts", "update", "--contact-id", "contactId", "--default-channel", "sms", + "--display-name", "John Doe", "--metadata", "{foo: string}", ) }) @@ -37,6 +91,7 @@ func TestContactsUpdate(t *testing.T) { // Test piping YAML data over stdin pipeData := []byte("" + "defaultChannel: sms\n" + + "displayName: John Doe\n" + "metadata:\n" + " foo: string\n") mocktest.TestRunMockTestWithPipeAndFlags( @@ -59,6 +114,44 @@ func TestContactsList(t *testing.T) { "--cursor", "cursor", "--limit", "100", "--phone-number", "phoneNumber", + "--search", "search", + "--tag", "string", + ) + }) +} + +func TestContactsDelete(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "contacts", "delete", + "--contact-id", "contactId", + ) + }) +} + +func TestContactsMerge(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "contacts", "merge", + "--contact-id", "contactId", + "--source-contact-id", "jx7xyz789", + ) + }) + + t.Run("piping data", func(t *testing.T) { + // Test piping YAML data over stdin + pipeData := []byte("sourceContactId: jx7xyz789") + mocktest.TestRunMockTestWithPipeAndFlags( + t, pipeData, + "--api-key", "string", + "contacts", "merge", + "--contact-id", "contactId", ) }) } diff --git a/pkg/cmd/contactchannel.go b/pkg/cmd/contactchannel.go new file mode 100644 index 0000000..73ee1c8 --- /dev/null +++ b/pkg/cmd/contactchannel.go @@ -0,0 +1,317 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "context" + "fmt" + + "github.com/tidwall/gjson" + "github.com/urfave/cli/v3" + "github.com/zavudev/cli/internal/apiquery" + "github.com/zavudev/cli/internal/requestflag" + "github.com/zavudev/sdk-go" + "github.com/zavudev/sdk-go/option" +) + +var contactsChannelsUpdate = cli.Command{ + Name: "update", + Usage: "Update a contact's channel properties.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "contact-id", + Required: true, + PathParam: "contactId", + }, + &requestflag.Flag[string]{ + Name: "channel-id", + Required: true, + PathParam: "channelId", + }, + &requestflag.Flag[*string]{ + Name: "label", + Usage: "Optional label for the channel. Set to null to clear.", + BodyPath: "label", + }, + &requestflag.Flag[map[string]any]{ + Name: "metadata", + BodyPath: "metadata", + }, + &requestflag.Flag[bool]{ + Name: "verified", + Usage: "Whether the channel is verified.", + BodyPath: "verified", + }, + }, + Action: handleContactsChannelsUpdate, + HideHelpCommand: true, +} + +var contactsChannelsAdd = cli.Command{ + Name: "add", + Usage: "Add a new communication channel to an existing contact.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "contact-id", + Required: true, + PathParam: "contactId", + }, + &requestflag.Flag[string]{ + Name: "channel", + Usage: "Channel type.", + Required: true, + BodyPath: "channel", + }, + &requestflag.Flag[string]{ + Name: "identifier", + Usage: "Channel identifier (phone number in E.164 format or email address).", + Required: true, + BodyPath: "identifier", + }, + &requestflag.Flag[string]{ + Name: "country-code", + Usage: "ISO country code for phone numbers.", + BodyPath: "countryCode", + }, + &requestflag.Flag[bool]{ + Name: "is-primary", + Usage: "Whether this should be the primary channel for its type.", + Default: false, + BodyPath: "isPrimary", + }, + &requestflag.Flag[string]{ + Name: "label", + Usage: "Optional label for the channel.", + BodyPath: "label", + }, + }, + Action: handleContactsChannelsAdd, + HideHelpCommand: true, +} + +var contactsChannelsRemove = cli.Command{ + Name: "remove", + Usage: "Remove a communication channel from a contact. Cannot remove the last channel.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "contact-id", + Required: true, + PathParam: "contactId", + }, + &requestflag.Flag[string]{ + Name: "channel-id", + Required: true, + PathParam: "channelId", + }, + }, + Action: handleContactsChannelsRemove, + HideHelpCommand: true, +} + +var contactsChannelsSetPrimary = cli.Command{ + Name: "set-primary", + Usage: "Set a channel as the primary channel for its type.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "contact-id", + Required: true, + PathParam: "contactId", + }, + &requestflag.Flag[string]{ + Name: "channel-id", + Required: true, + PathParam: "channelId", + }, + }, + Action: handleContactsChannelsSetPrimary, + HideHelpCommand: true, +} + +func handleContactsChannelsUpdate(ctx context.Context, cmd *cli.Command) error { + client := zavudev.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("channel-id") && len(unusedArgs) > 0 { + cmd.Set("channel-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, + ApplicationJSON, + false, + ) + if err != nil { + return err + } + + params := zavudev.ContactChannelUpdateParams{ + ContactID: cmd.Value("contact-id").(string), + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Contacts.Channels.Update( + ctx, + cmd.Value("channel-id").(string), + params, + options..., + ) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") + transform := cmd.Root().String("transform") + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "contacts:channels update", + Transform: transform, + }) +} + +func handleContactsChannelsAdd(ctx context.Context, cmd *cli.Command) error { + client := zavudev.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("contact-id") && len(unusedArgs) > 0 { + cmd.Set("contact-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, + ApplicationJSON, + false, + ) + if err != nil { + return err + } + + params := zavudev.ContactChannelAddParams{} + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Contacts.Channels.Add( + ctx, + cmd.Value("contact-id").(string), + params, + options..., + ) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") + transform := cmd.Root().String("transform") + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "contacts:channels add", + Transform: transform, + }) +} + +func handleContactsChannelsRemove(ctx context.Context, cmd *cli.Command) error { + client := zavudev.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("channel-id") && len(unusedArgs) > 0 { + cmd.Set("channel-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 + } + + params := zavudev.ContactChannelRemoveParams{ + ContactID: cmd.Value("contact-id").(string), + } + + return client.Contacts.Channels.Remove( + ctx, + cmd.Value("channel-id").(string), + params, + options..., + ) +} + +func handleContactsChannelsSetPrimary(ctx context.Context, cmd *cli.Command) error { + client := zavudev.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("channel-id") && len(unusedArgs) > 0 { + cmd.Set("channel-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 + } + + params := zavudev.ContactChannelSetPrimaryParams{ + ContactID: cmd.Value("contact-id").(string), + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Contacts.Channels.SetPrimary( + ctx, + cmd.Value("channel-id").(string), + params, + options..., + ) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") + transform := cmd.Root().String("transform") + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "contacts:channels set-primary", + Transform: transform, + }) +} diff --git a/pkg/cmd/contactchannel_test.go b/pkg/cmd/contactchannel_test.go new file mode 100644 index 0000000..6ebf814 --- /dev/null +++ b/pkg/cmd/contactchannel_test.go @@ -0,0 +1,100 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "testing" + + "github.com/zavudev/cli/internal/mocktest" +) + +func TestContactsChannelsUpdate(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "contacts:channels", "update", + "--contact-id", "contactId", + "--channel-id", "channelId", + "--label", "label", + "--metadata", "{foo: string}", + "--verified=true", + ) + }) + + t.Run("piping data", func(t *testing.T) { + // Test piping YAML data over stdin + pipeData := []byte("" + + "label: label\n" + + "metadata:\n" + + " foo: string\n" + + "verified: true\n") + mocktest.TestRunMockTestWithPipeAndFlags( + t, pipeData, + "--api-key", "string", + "contacts:channels", "update", + "--contact-id", "contactId", + "--channel-id", "channelId", + ) + }) +} + +func TestContactsChannelsAdd(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "contacts:channels", "add", + "--contact-id", "contactId", + "--channel", "email", + "--identifier", "john.work@company.com", + "--country-code", "US", + "--is-primary=true", + "--label", "work", + ) + }) + + t.Run("piping data", func(t *testing.T) { + // Test piping YAML data over stdin + pipeData := []byte("" + + "channel: email\n" + + "identifier: john.work@company.com\n" + + "countryCode: US\n" + + "isPrimary: true\n" + + "label: work\n") + mocktest.TestRunMockTestWithPipeAndFlags( + t, pipeData, + "--api-key", "string", + "contacts:channels", "add", + "--contact-id", "contactId", + ) + }) +} + +func TestContactsChannelsRemove(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "contacts:channels", "remove", + "--contact-id", "contactId", + "--channel-id", "channelId", + ) + }) +} + +func TestContactsChannelsSetPrimary(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "contacts:channels", "set-primary", + "--contact-id", "contactId", + "--channel-id", "channelId", + ) + }) +} diff --git a/pkg/cmd/conversation.go b/pkg/cmd/conversation.go new file mode 100644 index 0000000..822b40e --- /dev/null +++ b/pkg/cmd/conversation.go @@ -0,0 +1,320 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "context" + "fmt" + + "github.com/tidwall/gjson" + "github.com/urfave/cli/v3" + "github.com/zavudev/cli/internal/apiquery" + "github.com/zavudev/cli/internal/requestflag" + "github.com/zavudev/sdk-go" + "github.com/zavudev/sdk-go/option" +) + +var conversationsRetrieve = cli.Command{ + Name: "retrieve", + Usage: "Get conversation", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "conversation-id", + Required: true, + PathParam: "conversationId", + }, + }, + Action: handleConversationsRetrieve, + HideHelpCommand: true, +} + +var conversationsList = cli.Command{ + Name: "list", + Usage: "List inbox threads, most recently active first. A conversation groups every\nmessage with one contact across channels, which is what you need to build an\ninbox: `GET /v1/messages` returns a flat log with no thread to hang it on.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "channel", + Usage: "Keep only threads that have carried this channel.", + QueryPath: "channel", + }, + &requestflag.Flag[string]{ + Name: "cursor", + Usage: "Opaque cursor from a previous response's `nextCursor`. Do not construct it.", + QueryPath: "cursor", + }, + &requestflag.Flag[int64]{ + Name: "limit", + Default: 50, + QueryPath: "limit", + }, + &requestflag.Flag[string]{ + Name: "search", + Usage: "Search threads by identity: phone number (any format — `+1 (555) 123-4567` and `15551234567` both match), email address (full or local part), WhatsApp group subject, WhatsApp username, or BSUID. Matching is by whole word, with prefix matching on the last term, so `mar` finds `maria@example.com` and `+1555` finds `+15551234567`; a fragment from the middle or end of a number (`4567`) does not match.\n\nIt does **not** search message bodies — only who the thread is with.\n\nResults come back ranked by relevance rather than by recency, so the usual \"most recently active first\" ordering does not apply while `q` is set. `senderId` and `channel` still narrow the results, and `cursor` paginates them as usual. An empty or whitespace-only `q` returns no items rather than the full list.", + QueryPath: "search", + }, + &requestflag.Flag[string]{ + Name: "sender-id", + Usage: "Keep only threads last handled by this sender.", + QueryPath: "senderId", + }, + &requestflag.Flag[int64]{ + Name: "max-items", + Usage: "The maximum number of items to return (use -1 for unlimited).", + }, + }, + Action: handleConversationsList, + HideHelpCommand: true, +} + +var conversationsListMessages = cli.Command{ + Name: "list-messages", + Usage: "Messages in this thread, newest first, across every channel it has carried.\nReply with `POST /v1/messages`, passing the conversation's `senderId` as the\n`Zavu-Sender` header so the answer leaves from the number the contact already\nknows.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "conversation-id", + Required: true, + PathParam: "conversationId", + }, + &requestflag.Flag[string]{ + Name: "cursor", + Usage: "Opaque cursor from a previous response's `nextCursor`.", + QueryPath: "cursor", + }, + &requestflag.Flag[int64]{ + Name: "limit", + Default: 50, + QueryPath: "limit", + }, + &requestflag.Flag[int64]{ + Name: "max-items", + Usage: "The maximum number of items to return (use -1 for unlimited).", + }, + }, + Action: handleConversationsListMessages, + HideHelpCommand: true, +} + +var conversationsMarkAsRead = cli.Command{ + Name: "mark-as-read", + Usage: "Reset the thread's `unreadCount` to zero. Marks the thread read in your own\ninbox only: it does not send a read receipt to the contact.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "conversation-id", + Required: true, + PathParam: "conversationId", + }, + }, + Action: handleConversationsMarkAsRead, + HideHelpCommand: true, +} + +func handleConversationsRetrieve(ctx context.Context, cmd *cli.Command) error { + client := zavudev.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("conversation-id") && len(unusedArgs) > 0 { + cmd.Set("conversation-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 + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Conversations.Get(ctx, cmd.Value("conversation-id").(string), options...) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") + transform := cmd.Root().String("transform") + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "conversations retrieve", + Transform: transform, + }) +} + +func handleConversationsList(ctx context.Context, cmd *cli.Command) error { + client := zavudev.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + + 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 + } + + params := zavudev.ConversationListParams{} + + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") + transform := cmd.Root().String("transform") + if format == "raw" { + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Conversations.List(ctx, params, options...) + if err != nil { + return err + } + obj := gjson.ParseBytes(res) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "conversations list", + Transform: transform, + }) + } else { + iter := client.Conversations.ListAutoPaging(ctx, params, options...) + maxItems := int64(-1) + if cmd.IsSet("max-items") { + maxItems = cmd.Value("max-items").(int64) + } + return ShowJSONIterator(iter, maxItems, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "conversations list", + Transform: transform, + }) + } +} + +func handleConversationsListMessages(ctx context.Context, cmd *cli.Command) error { + client := zavudev.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("conversation-id") && len(unusedArgs) > 0 { + cmd.Set("conversation-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 + } + + params := zavudev.ConversationListMessagesParams{} + + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") + transform := cmd.Root().String("transform") + if format == "raw" { + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Conversations.ListMessages( + ctx, + cmd.Value("conversation-id").(string), + params, + options..., + ) + if err != nil { + return err + } + obj := gjson.ParseBytes(res) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "conversations list-messages", + Transform: transform, + }) + } else { + iter := client.Conversations.ListMessagesAutoPaging( + ctx, + cmd.Value("conversation-id").(string), + params, + options..., + ) + maxItems := int64(-1) + if cmd.IsSet("max-items") { + maxItems = cmd.Value("max-items").(int64) + } + return ShowJSONIterator(iter, maxItems, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "conversations list-messages", + Transform: transform, + }) + } +} + +func handleConversationsMarkAsRead(ctx context.Context, cmd *cli.Command) error { + client := zavudev.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("conversation-id") && len(unusedArgs) > 0 { + cmd.Set("conversation-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 + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Conversations.MarkAsRead(ctx, cmd.Value("conversation-id").(string), options...) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") + transform := cmd.Root().String("transform") + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "conversations mark-as-read", + Transform: transform, + }) +} diff --git a/pkg/cmd/conversation_test.go b/pkg/cmd/conversation_test.go new file mode 100644 index 0000000..949809f --- /dev/null +++ b/pkg/cmd/conversation_test.go @@ -0,0 +1,65 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "testing" + + "github.com/zavudev/cli/internal/mocktest" +) + +func TestConversationsRetrieve(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "conversations", "retrieve", + "--conversation-id", "conversationId", + ) + }) +} + +func TestConversationsList(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "conversations", "list", + "--max-items", "10", + "--channel", "sms", + "--cursor", "cursor", + "--limit", "100", + "--search", "+56912345678", + "--sender-id", "senderId", + ) + }) +} + +func TestConversationsListMessages(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "conversations", "list-messages", + "--max-items", "10", + "--conversation-id", "conversationId", + "--cursor", "cursor", + "--limit", "100", + ) + }) +} + +func TestConversationsMarkAsRead(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "conversations", "mark-as-read", + "--conversation-id", "conversationId", + ) + }) +} diff --git a/pkg/cmd/emaildomain.go b/pkg/cmd/emaildomain.go new file mode 100644 index 0000000..578c923 --- /dev/null +++ b/pkg/cmd/emaildomain.go @@ -0,0 +1,274 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "context" + "fmt" + + "github.com/tidwall/gjson" + "github.com/urfave/cli/v3" + "github.com/zavudev/cli/internal/apiquery" + "github.com/zavudev/cli/internal/requestflag" + "github.com/zavudev/sdk-go" + "github.com/zavudev/sdk-go/option" +) + +var emailDomainsCreate = cli.Command{ + Name: "create", + Usage: "Add a domain to send email from. Returns the DNS records to publish (DKIM CNAMEs\nare required; SPF, DMARC, and MAIL FROM are recommended). Publish them at your\nDNS provider, then verify.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "domain", + Usage: "Bare domain, e.g. example.com.", + Required: true, + BodyPath: "domain", + }, + }, + Action: handleEmailDomainsCreate, + HideHelpCommand: true, +} + +var emailDomainsRetrieve = cli.Command{ + Name: "retrieve", + Usage: "Fetch a domain with its DNS records and current status.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "domain-id", + Required: true, + PathParam: "domainId", + }, + }, + Action: handleEmailDomainsRetrieve, + HideHelpCommand: true, +} + +var emailDomainsList = cli.Command{ + Name: "list", + Usage: "List email domains", + Suggest: true, + Flags: []cli.Flag{}, + Action: handleEmailDomainsList, + HideHelpCommand: true, +} + +var emailDomainsDelete = cli.Command{ + Name: "delete", + Usage: "Remove an email domain", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "domain-id", + Required: true, + PathParam: "domainId", + }, + }, + Action: handleEmailDomainsDelete, + HideHelpCommand: true, +} + +var emailDomainsVerify = cli.Command{ + Name: "verify", + Usage: "Re-check the domain's published DNS records and refresh its status.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "domain-id", + Required: true, + PathParam: "domainId", + }, + }, + Action: handleEmailDomainsVerify, + HideHelpCommand: true, +} + +func handleEmailDomainsCreate(ctx context.Context, cmd *cli.Command) error { + client := zavudev.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + + options, err := flagOptions( + cmd, + apiquery.NestedQueryFormatBrackets, + apiquery.ArrayQueryFormatComma, + ApplicationJSON, + false, + ) + if err != nil { + return err + } + + params := zavudev.EmailDomainNewParams{} + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.EmailDomains.New(ctx, params, options...) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") + transform := cmd.Root().String("transform") + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "email-domains create", + Transform: transform, + }) +} + +func handleEmailDomainsRetrieve(ctx context.Context, cmd *cli.Command) error { + client := zavudev.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) + } + + 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.EmailDomains.Get(ctx, cmd.Value("domain-id").(string), options...) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") + transform := cmd.Root().String("transform") + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "email-domains retrieve", + Transform: transform, + }) +} + +func handleEmailDomainsList(ctx context.Context, cmd *cli.Command) error { + client := zavudev.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + + 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 + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.EmailDomains.List(ctx, options...) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") + transform := cmd.Root().String("transform") + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "email-domains list", + Transform: transform, + }) +} + +func handleEmailDomainsDelete(ctx context.Context, cmd *cli.Command) error { + client := zavudev.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) + } + + options, err := flagOptions( + cmd, + apiquery.NestedQueryFormatBrackets, + apiquery.ArrayQueryFormatComma, + EmptyBody, + false, + ) + if err != nil { + return err + } + + return client.EmailDomains.Delete(ctx, cmd.Value("domain-id").(string), options...) +} + +func handleEmailDomainsVerify(ctx context.Context, cmd *cli.Command) error { + client := zavudev.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) + } + + 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.EmailDomains.Verify(ctx, cmd.Value("domain-id").(string), options...) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") + transform := cmd.Root().String("transform") + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "email-domains verify", + Transform: transform, + }) +} diff --git a/pkg/cmd/emaildomain_test.go b/pkg/cmd/emaildomain_test.go new file mode 100644 index 0000000..fccee9a --- /dev/null +++ b/pkg/cmd/emaildomain_test.go @@ -0,0 +1,78 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "testing" + + "github.com/zavudev/cli/internal/mocktest" +) + +func TestEmailDomainsCreate(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "email-domains", "create", + "--domain", "example.com", + ) + }) + + t.Run("piping data", func(t *testing.T) { + // Test piping YAML data over stdin + pipeData := []byte("domain: example.com") + mocktest.TestRunMockTestWithPipeAndFlags( + t, pipeData, + "--api-key", "string", + "email-domains", "create", + ) + }) +} + +func TestEmailDomainsRetrieve(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "email-domains", "retrieve", + "--domain-id", "domainId", + ) + }) +} + +func TestEmailDomainsList(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "email-domains", "list", + ) + }) +} + +func TestEmailDomainsDelete(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "email-domains", "delete", + "--domain-id", "domainId", + ) + }) +} + +func TestEmailDomainsVerify(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "email-domains", "verify", + "--domain-id", "domainId", + ) + }) +} diff --git a/pkg/cmd/flagoptions.go b/pkg/cmd/flagoptions.go index 3c0f276..8ed654c 100644 --- a/pkg/cmd/flagoptions.go +++ b/pkg/cmd/flagoptions.go @@ -7,9 +7,11 @@ import ( "fmt" "io" "maps" + "mime" "mime/multipart" "net/http" "os" + "path/filepath" "reflect" "strings" "unicode/utf8" @@ -36,7 +38,14 @@ const ( type FileEmbedStyle int const ( + // EmbedText reads referenced files fully into memory and substitutes the file's contents back into the + // value as a string. Binary files are base64-encoded. Used for JSON request bodies and for headers and + // query parameters, where the file contents need to be serialized inline. EmbedText FileEmbedStyle = iota + + // EmbedIOReader replaces file references with an io.Reader that streams the file's contents. Used for + // `multipart/form-data` and `application/octet-stream` request bodies, where files are uploaded as binary + // parts rather than embedded into a text value. EmbedIOReader ) @@ -142,6 +151,20 @@ func embedFilesValue(v reflect.Value, embedStyle FileEmbedStyle, stdin *onceStdi if s == "" { return v, nil } + if embedStyle == EmbedIOReader { + if isStdinPath(s) { + r, err := stdin.read() + if err != nil { + return v, err + } + return reflect.ValueOf(io.NopCloser(r)), nil + } + upload, err := openFileUpload(s) + if err != nil { + return v, err + } + return reflect.ValueOf(upload), nil + } if isStdinPath(s) { content, err := stdin.readAll() if err != nil { @@ -250,7 +273,7 @@ func embedFilesValue(v reflect.Value, embedStyle FileEmbedStyle, stdin *onceStdi return reflect.ValueOf(io.NopCloser(r)), nil } - file, err := os.Open(filename) + upload, err := openFileUpload(filename) if err != nil { if !expectsFile { // For strings that start with "@" and don't look like a filename, return the string @@ -258,7 +281,7 @@ func embedFilesValue(v reflect.Value, embedStyle FileEmbedStyle, stdin *onceStdi } return v, err } - return reflect.ValueOf(file), nil + return reflect.ValueOf(upload), nil } } return v, nil @@ -309,8 +332,14 @@ func flagOptions( requestContents := requestflag.ExtractRequestContents(cmd) + // Translate inner-field aliases in YAML values that came from flags (e.g. + // `--parent '{"alias": val}'` resolving to the canonical inner field). + if bodyMap, ok := requestContents.Body.(map[string]any); ok { + applyDataAliases(cmd, bodyMap) + } + stdinConsumedByPipe := false - if (bodyType == MultipartFormEncoded || bodyType == ApplicationJSON) && !ignoreStdin && isInputPiped() { + if bodyType != ApplicationOctetStream && !ignoreStdin && isInputPiped() { pipeData, err := io.ReadAll(os.Stdin) if err != nil { return nil, err @@ -323,16 +352,46 @@ func flagOptions( return nil, fmt.Errorf("Failed to parse piped data as YAML/JSON:\n%w", err) } if bodyMap, ok := bodyData.(map[string]any); ok { - if flagMap, ok := requestContents.Body.(map[string]any); ok { - maps.Copy(bodyMap, flagMap) - requestContents.Body = bodyMap + applyDataAliases(cmd, bodyMap) + // Apply any matching keys from the piped data to path, query, and header flags + // that have not already been set via the command line. + if err := requestflag.ApplyStdinDataToFlags(cmd, bodyMap); err != nil { + return nil, err + } + // Re-extract request contents now that flags may have been updated. + requestContents = requestflag.ExtractRequestContents(cmd) + // Remove keys that were consumed as query, header, or path params so they + // don't also leak into the request body via the maps.Copy merge below. + // We delete both the canonical key and any aliases since the user may have + // piped data using an alias name rather than the canonical API name. + for _, flag := range cmd.Flags { + inReq, ok := flag.(requestflag.InRequest) + if !ok || !flag.IsSet() { + continue + } + if inReq.GetQueryPath() != "" || inReq.GetHeaderPath() != "" || inReq.GetPathParam() != "" { + delete(bodyMap, inReq.GetQueryPath()) + delete(bodyMap, inReq.GetHeaderPath()) + delete(bodyMap, inReq.GetPathParam()) + for _, alias := range inReq.GetDataAliases() { + delete(bodyMap, alias) + } + } + } + if bodyType != EmptyBody { + if flagMap, ok := requestContents.Body.(map[string]any); ok { + maps.Copy(bodyMap, flagMap) + requestContents.Body = bodyMap + } else { + bodyData = requestContents.Body + } + } + } else if bodyType != EmptyBody { + if flagMap, ok := requestContents.Body.(map[string]any); ok && len(flagMap) > 0 { + return nil, fmt.Errorf("Cannot merge flags with a body that is not a map: %v", bodyData) } else { - bodyData = requestContents.Body + requestContents.Body = bodyData } - } else if flagMap, ok := requestContents.Body.(map[string]any); ok && len(flagMap) > 0 { - return nil, fmt.Errorf("Cannot merge flags with a body that is not a map: %v", bodyData) - } else { - requestContents.Body = bodyData } } } @@ -340,7 +399,6 @@ func flagOptions( if missingFlags := requestflag.GetMissingRequiredFlags(cmd, requestContents.Body); len(missingFlags) > 0 { if len(missingFlags) == 1 { return nil, fmt.Errorf("Required flag %q not set\nRun '%s --help' for usage information", missingFlags[0].Names()[0], cmd.FullName()) - } else { names := []string{} for _, flag := range missingFlags { @@ -485,6 +543,84 @@ func flagOptions( // as a file path without needing the "@" prefix. type FilePathValue string +// fileUpload wraps an io.Reader with filename and content-type metadata for +// use as a multipart form part. The apiform encoder detects the Filename and +// ContentType methods and uses them to populate the Content-Disposition +// filename and the Content-Type header on the part. +type fileUpload struct { + io.Reader // apiform checks for reader and reads its contents during encode + filename string + contentType string +} + +func (f fileUpload) Filename() string { return f.filename } +func (f fileUpload) ContentType() string { return f.contentType } +func (f fileUpload) Close() error { + if c, ok := f.Reader.(io.Closer); ok { + return c.Close() + } + return nil +} + +// openFileUpload opens the file at path and returns a fileUpload whose filename +// is the path's basename and whose content type is derived from the file +// extension (falling back to application/octet-stream when unknown). +func openFileUpload(path string) (fileUpload, error) { + file, err := os.Open(path) + if err != nil { + return fileUpload{}, err + } + contentType := mime.TypeByExtension(filepath.Ext(path)) + if contentType == "" { + contentType = "application/octet-stream" + } + return fileUpload{ + Reader: file, + filename: filepath.Base(path), + contentType: contentType, + }, nil +} + +// applyDataAliases rewrites keys in a body map based on flag `DataAliases` metadata. For top-level flags, +// `{alias: value}` becomes `{canonical: value}`. For inner flags (those registered under an outer flag +// via WithInnerFlags), the alias translation is also applied to the nested map under the outer flag's +// body path, so values like `--parent '{"alias": val}'` resolve to the canonical inner field name. +func applyDataAliases(cmd *cli.Command, bodyMap map[string]any) { + for _, flag := range cmd.Flags { + // Inner flags: rewrite aliases inside the nested map under the outer flag's body path. + if inner, ok := flag.(requestflag.HasOuterFlag); ok { + outer, outerOk := inner.GetOuterFlag().(requestflag.InRequest) + if !outerOk { + continue + } + if nested, ok := bodyMap[outer.GetBodyPath()].(map[string]any); ok && inner.GetInnerField() != "" { + rewriteAliases(nested, inner.GetInnerField(), inner.GetDataAliases()) + } + continue + } + // Top-level flags: rewrite aliases in the body map. + if inReq, ok := flag.(requestflag.InRequest); ok && inReq.GetBodyPath() != "" { + rewriteAliases(bodyMap, inReq.GetBodyPath(), inReq.GetDataAliases()) + } + } +} + +// rewriteAliases replaces each alias key in m with the canonical key, preserving the value. The +// "canonical" key is the name the API itself expects (the OpenAPI property/field name) — e.g. for +// a top-level flag, the parameter's BodyPath; for an inner flag, the inner field name. Aliases are +// the user-facing alternate names declared via x-stainless-cli-data-alias. +func rewriteAliases(m map[string]any, canonical string, aliases []string) { + for _, alias := range aliases { + if alias == "" || alias == canonical { + continue + } + if val, exists := m[alias]; exists { + m[canonical] = val + delete(m, alias) + } + } +} + // 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 diff --git a/pkg/cmd/flagoptions_test.go b/pkg/cmd/flagoptions_test.go index 039b9ff..00734ca 100644 --- a/pkg/cmd/flagoptions_test.go +++ b/pkg/cmd/flagoptions_test.go @@ -8,7 +8,6 @@ import ( "strings" "testing" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -31,7 +30,7 @@ func TestIsUTF8TextFile(t *testing.T) { } for _, tt := range tests { - assert.Equal(t, tt.expected, isUTF8TextFile(tt.content)) + require.Equal(t, tt.expected, isUTF8TextFile(tt.content)) } } @@ -226,10 +225,10 @@ func TestEmbedFiles(t *testing.T) { got, err := embedFiles(tt.input, EmbedText, nil) if tt.wantErr { - assert.Error(t, err) + require.Error(t, err) } else { require.NoError(t, err) - assert.Equal(t, tt.want, got) + require.Equal(t, tt.want, got) } }) @@ -238,7 +237,7 @@ func TestEmbedFiles(t *testing.T) { _, err := embedFiles(tt.input, EmbedIOReader, nil) if tt.wantErr { - assert.Error(t, err) + require.Error(t, err) } else { require.NoError(t, err) } @@ -333,6 +332,56 @@ func TestEmbedFilesStdin(t *testing.T) { }) } +// TestEmbedFilesUploadMetadata verifies that EmbedIOReader mode wraps file readers with filename and +// content-type metadata so the multipart encoder populates `Content-Disposition` and `Content-Type` headers. +func TestEmbedFilesUploadMetadata(t *testing.T) { + t.Parallel() + + tmpDir := t.TempDir() + writeTestFile(t, tmpDir, "hello.txt", "hi") + writeTestFile(t, tmpDir, "page.html", "") + writeTestFile(t, tmpDir, "blob.bin", "\x00\x01") + + cases := []struct { + basename string + wantContentType string + }{ + {"hello.txt", "text/plain; charset=utf-8"}, + {"page.html", "text/html; charset=utf-8"}, + {"blob.bin", "application/octet-stream"}, + } + + for _, tc := range cases { + t.Run("AtPrefix_"+tc.basename, func(t *testing.T) { + t.Parallel() + + path := filepath.Join(tmpDir, tc.basename) + withEmbedded, err := embedFiles(map[string]any{"file": "@" + path}, EmbedIOReader, nil) + require.NoError(t, err) + + upload, ok := withEmbedded.(map[string]any)["file"].(fileUpload) + require.True(t, ok, "expected fileUpload, got %T", withEmbedded.(map[string]any)["file"]) + require.Equal(t, tc.basename, upload.Filename()) + require.Equal(t, upload.ContentType(), tc.wantContentType) + require.NoError(t, upload.Close()) + }) + + t.Run("FilePathValue_"+tc.basename, func(t *testing.T) { + t.Parallel() + + path := filepath.Join(tmpDir, tc.basename) + withEmbedded, err := embedFiles(map[string]any{"file": FilePathValue(path)}, EmbedIOReader, nil) + require.NoError(t, err) + + upload, ok := withEmbedded.(map[string]any)["file"].(fileUpload) + require.True(t, ok, "expected fileUpload, got %T", withEmbedded.(map[string]any)["file"]) + require.Equal(t, tc.basename, upload.Filename()) + require.Equal(t, upload.ContentType(), tc.wantContentType) + require.NoError(t, upload.Close()) + }) + } +} + func writeTestFile(t *testing.T, dir, filename, content string) { t.Helper() diff --git a/pkg/cmd/function.go b/pkg/cmd/function.go new file mode 100644 index 0000000..87fb74f --- /dev/null +++ b/pkg/cmd/function.go @@ -0,0 +1,745 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "context" + "fmt" + + "github.com/tidwall/gjson" + "github.com/urfave/cli/v3" + "github.com/zavudev/cli/internal/apiquery" + "github.com/zavudev/cli/internal/requestflag" + "github.com/zavudev/sdk-go" + "github.com/zavudev/sdk-go/option" +) + +var functionsCreate = cli.Command{ + Name: "create", + Usage: "Create a new Zavu Function. The function starts in `draft` status. A dedicated\nAPI key is auto-provisioned and injected as the `ZAVU_API_KEY` secret so the\nfunction can call back into the Zavu API without manual setup.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "name", + Required: true, + BodyPath: "name", + }, + &requestflag.Flag[string]{ + Name: "slug", + Usage: "URL-safe identifier (lowercase, digits, hyphens). Must be unique per project.", + Required: true, + BodyPath: "slug", + }, + &requestflag.Flag[map[string]any]{ + Name: "dependencies", + Usage: "npm dependencies. Keys are package names, values are semver ranges.", + BodyPath: "dependencies", + }, + &requestflag.Flag[string]{ + Name: "description", + BodyPath: "description", + }, + &requestflag.Flag[string]{ + Name: "entrypoint", + Usage: "Which file in `files` is the entry point. Defaults to `index.ts`.", + Default: "index.ts", + BodyPath: "entrypoint", + }, + &requestflag.Flag[map[string]any]{ + Name: "files", + Usage: "The project's source files, keyed by path relative to the project root (e.g. `index.ts`, `lib/orders.ts`). Imports between them are resolved when the function is built, so a function can be split across as many files as it needs.\n\nPaths must be relative and use forward slashes; `..`, `node_modules/` and `package.json` are rejected. npm packages are not uploaded here — declare them under `dependencies` and Zavu installs them. Limits: 200 files and 900,000 bytes for the whole tree.", + BodyPath: "files", + }, + &requestflag.Flag[bool]{ + Name: "http-enabled", + Usage: "Whether to expose a public HTTPS URL for this function.", + Default: false, + BodyPath: "httpEnabled", + }, + &requestflag.Flag[int64]{ + Name: "memory-mb", + Usage: "Allowed values: 128, 256, 512, 1024.", + Default: 256, + BodyPath: "memoryMb", + }, + &requestflag.Flag[string]{ + Name: "runtime", + Usage: "Runtime the function is deployed on.", + BodyPath: "runtime", + }, + &requestflag.Flag[string]{ + Name: "source-code", + Usage: "Shortcut for a single-file function: exactly equivalent to sending `files` with one entry named after `entrypoint` (`index.ts` by default). Fully supported — use whichever fits. If both are sent, `files` wins.", + BodyPath: "sourceCode", + }, + &requestflag.Flag[int64]{ + Name: "timeout-sec", + Usage: "Per-invocation timeout in seconds. Event and cron invocations are asynchronous, so a long timeout only bounds cost; a tool called during a live conversation holds up the reply, and a function exposed over HTTP is additionally bounded by the platform's HTTP response limit.", + Default: 30, + BodyPath: "timeoutSec", + }, + }, + Action: handleFunctionsCreate, + HideHelpCommand: true, +} + +var functionsRetrieve = cli.Command{ + Name: "retrieve", + Usage: "Get function", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "function-id", + Required: true, + PathParam: "functionId", + }, + }, + Action: handleFunctionsRetrieve, + HideHelpCommand: true, +} + +var functionsUpdate = cli.Command{ + Name: "update", + Usage: "Update an existing function. `sourceCode` / `dependencies` edit the draft\nwithout triggering a build — they go live on the next\n`POST /v1/functions/{functionId}/deploy`. `httpEnabled` is applied to the\ndeployed function immediately, so turning the public endpoint on or off does not\nrequire a redeploy.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "function-id", + Required: true, + PathParam: "functionId", + }, + &requestflag.Flag[map[string]any]{ + Name: "dependencies", + Usage: "New dependency map (replaces existing dependencies).", + BodyPath: "dependencies", + }, + &requestflag.Flag[string]{ + Name: "entrypoint", + Usage: "Which file in `files` is the entry point. Defaults to `index.ts`.", + Default: "index.ts", + BodyPath: "entrypoint", + }, + &requestflag.Flag[map[string]any]{ + Name: "files", + Usage: "The project's source files, keyed by path relative to the project root (e.g. `index.ts`, `lib/orders.ts`). Imports between them are resolved when the function is built, so a function can be split across as many files as it needs.\n\nPaths must be relative and use forward slashes; `..`, `node_modules/` and `package.json` are rejected. npm packages are not uploaded here — declare them under `dependencies` and Zavu installs them. Limits: 200 files and 900,000 bytes for the whole tree.", + BodyPath: "files", + }, + &requestflag.Flag[bool]{ + Name: "http-enabled", + Usage: "Expose the function on its public HTTPS URL, or take it down. Applies to the already-deployed function without redeploying; the URL is returned as `publicUrl`.", + BodyPath: "httpEnabled", + }, + &requestflag.Flag[string]{ + Name: "source-code", + Usage: "Shortcut for a single-file function: exactly equivalent to sending `files` with one entry named after `entrypoint` (`index.ts` by default). Fully supported — use whichever fits. If both are sent, `files` wins.", + BodyPath: "sourceCode", + }, + }, + Action: handleFunctionsUpdate, + HideHelpCommand: true, +} + +var functionsDelete = cli.Command{ + Name: "delete", + Usage: "Permanently delete a function and cascade: triggers, secrets, deployment\nhistory, managed agents+tools, and revoke the auto-provisioned API key. The AWS\nLambda + log group are torn down asynchronously.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "function-id", + Required: true, + PathParam: "functionId", + }, + }, + Action: handleFunctionsDelete, + HideHelpCommand: true, +} + +var functionsDeploy = cli.Command{ + Name: "deploy", + Usage: "Publish the function. If `sourceCode` or `dependencies` are provided in the\nbody, they replace the current draft before deployment. Returns immediately with\na deployment ID — poll `GET /v1/functions/deployments/{deploymentId}` until\nstatus is `active` or `failed`.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "function-id", + Required: true, + PathParam: "functionId", + }, + &requestflag.Flag[map[string]any]{ + Name: "dependencies", + Usage: "New dependency map (replaces existing dependencies).", + BodyPath: "dependencies", + }, + &requestflag.Flag[string]{ + Name: "entrypoint", + Usage: "Which file in `files` is the entry point. Defaults to `index.ts`.", + Default: "index.ts", + BodyPath: "entrypoint", + }, + &requestflag.Flag[map[string]any]{ + Name: "files", + Usage: "The project's source files, keyed by path relative to the project root (e.g. `index.ts`, `lib/orders.ts`). Imports between them are resolved when the function is built, so a function can be split across as many files as it needs.\n\nPaths must be relative and use forward slashes; `..`, `node_modules/` and `package.json` are rejected. npm packages are not uploaded here — declare them under `dependencies` and Zavu installs them. Limits: 200 files and 900,000 bytes for the whole tree.", + BodyPath: "files", + }, + &requestflag.Flag[string]{ + Name: "source-code", + Usage: "Shortcut for a single-file function: exactly equivalent to sending `files` with one entry named after `entrypoint` (`index.ts` by default). Fully supported — use whichever fits. If both are sent, `files` wins.", + BodyPath: "sourceCode", + }, + }, + Action: handleFunctionsDeploy, + HideHelpCommand: true, +} + +var functionsGetDeployment = cli.Command{ + Name: "get-deployment", + Usage: "Fetch a deployment to poll its status during a deploy.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "deployment-id", + Required: true, + PathParam: "deploymentId", + }, + }, + Action: handleFunctionsGetDeployment, + HideHelpCommand: true, +} + +var functionsListDeployments = cli.Command{ + Name: "list-deployments", + Usage: "List a function's deployment history, newest first. Source code is omitted;\nfetch a single deployment via GET /v1/functions/deployments/{deploymentId} for\nfull details.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "function-id", + Required: true, + PathParam: "functionId", + }, + &requestflag.Flag[int64]{ + Name: "limit", + Default: 20, + QueryPath: "limit", + }, + }, + Action: handleFunctionsListDeployments, + HideHelpCommand: true, +} + +var functionsListEventTypes = cli.Command{ + Name: "list-event-types", + Usage: "List the event types a function trigger can subscribe to. Includes the special\ntype `cron`, which fires on a schedule (see POST\n/v1/functions/{functionId}/triggers) rather than on a messaging event.", + Suggest: true, + Flags: []cli.Flag{}, + Action: handleFunctionsListEventTypes, + HideHelpCommand: true, +} + +var functionsRollbackDeployment = cli.Command{ + Name: "rollback-deployment", + Usage: "Re-deploy a previous version by copying its source, dependencies, and runtime\npin onto the function's draft, then deploying. Returns immediately with a\ndeployment ID — poll GET /v1/functions/deployments/{deploymentId} until status\nis active or failed. Secrets are not rolled back.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "function-id", + Required: true, + PathParam: "functionId", + }, + &requestflag.Flag[string]{ + Name: "deployment-id", + Usage: "ID of the deployment to roll back to.", + Required: true, + BodyPath: "deploymentId", + }, + }, + Action: handleFunctionsRollbackDeployment, + HideHelpCommand: true, +} + +var functionsTailLogs = cli.Command{ + Name: "tail-logs", + Usage: "Fetch invocation logs for a function. Logs are paginated via `nextToken`. Pass\n`startTime` / `endTime` (Unix epoch milliseconds) to bound the window, or\n`filterPattern` to filter messages.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "function-id", + Required: true, + PathParam: "functionId", + }, + &requestflag.Flag[int64]{ + Name: "end-time", + Usage: "End of the log window in Unix epoch milliseconds.", + QueryPath: "endTime", + }, + &requestflag.Flag[string]{ + Name: "filter-pattern", + QueryPath: "filterPattern", + }, + &requestflag.Flag[int64]{ + Name: "limit", + Default: 100, + QueryPath: "limit", + }, + &requestflag.Flag[string]{ + Name: "next-token", + QueryPath: "nextToken", + }, + &requestflag.Flag[int64]{ + Name: "start-time", + Usage: "Start of the log window in Unix epoch milliseconds.", + QueryPath: "startTime", + }, + }, + Action: handleFunctionsTailLogs, + HideHelpCommand: true, +} + +func handleFunctionsCreate(ctx context.Context, cmd *cli.Command) error { + client := zavudev.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + + options, err := flagOptions( + cmd, + apiquery.NestedQueryFormatBrackets, + apiquery.ArrayQueryFormatComma, + ApplicationJSON, + false, + ) + if err != nil { + return err + } + + params := zavudev.FunctionNewParams{} + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Functions.New(ctx, params, options...) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") + transform := cmd.Root().String("transform") + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "functions create", + Transform: transform, + }) +} + +func handleFunctionsRetrieve(ctx context.Context, cmd *cli.Command) error { + client := zavudev.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("function-id") && len(unusedArgs) > 0 { + cmd.Set("function-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 + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Functions.Get(ctx, cmd.Value("function-id").(string), options...) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") + transform := cmd.Root().String("transform") + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "functions retrieve", + Transform: transform, + }) +} + +func handleFunctionsUpdate(ctx context.Context, cmd *cli.Command) error { + client := zavudev.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("function-id") && len(unusedArgs) > 0 { + cmd.Set("function-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, + ApplicationJSON, + false, + ) + if err != nil { + return err + } + + params := zavudev.FunctionUpdateParams{} + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Functions.Update( + ctx, + cmd.Value("function-id").(string), + params, + options..., + ) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") + transform := cmd.Root().String("transform") + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "functions update", + Transform: transform, + }) +} + +func handleFunctionsDelete(ctx context.Context, cmd *cli.Command) error { + client := zavudev.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("function-id") && len(unusedArgs) > 0 { + cmd.Set("function-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 + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Functions.Delete(ctx, cmd.Value("function-id").(string), options...) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") + transform := cmd.Root().String("transform") + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "functions delete", + Transform: transform, + }) +} + +func handleFunctionsDeploy(ctx context.Context, cmd *cli.Command) error { + client := zavudev.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("function-id") && len(unusedArgs) > 0 { + cmd.Set("function-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, + ApplicationJSON, + false, + ) + if err != nil { + return err + } + + params := zavudev.FunctionDeployParams{} + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Functions.Deploy( + ctx, + cmd.Value("function-id").(string), + params, + options..., + ) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") + transform := cmd.Root().String("transform") + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "functions deploy", + Transform: transform, + }) +} + +func handleFunctionsGetDeployment(ctx context.Context, cmd *cli.Command) error { + client := zavudev.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("deployment-id") && len(unusedArgs) > 0 { + cmd.Set("deployment-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 + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Functions.GetDeployment(ctx, cmd.Value("deployment-id").(string), options...) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") + transform := cmd.Root().String("transform") + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "functions get-deployment", + Transform: transform, + }) +} + +func handleFunctionsListDeployments(ctx context.Context, cmd *cli.Command) error { + client := zavudev.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("function-id") && len(unusedArgs) > 0 { + cmd.Set("function-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 + } + + params := zavudev.FunctionListDeploymentsParams{} + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Functions.ListDeployments( + ctx, + cmd.Value("function-id").(string), + params, + options..., + ) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") + transform := cmd.Root().String("transform") + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "functions list-deployments", + Transform: transform, + }) +} + +func handleFunctionsListEventTypes(ctx context.Context, cmd *cli.Command) error { + client := zavudev.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + + 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 + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Functions.ListEventTypes(ctx, options...) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") + transform := cmd.Root().String("transform") + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "functions list-event-types", + Transform: transform, + }) +} + +func handleFunctionsRollbackDeployment(ctx context.Context, cmd *cli.Command) error { + client := zavudev.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("function-id") && len(unusedArgs) > 0 { + cmd.Set("function-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, + ApplicationJSON, + false, + ) + if err != nil { + return err + } + + params := zavudev.FunctionRollbackDeploymentParams{} + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Functions.RollbackDeployment( + ctx, + cmd.Value("function-id").(string), + params, + options..., + ) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") + transform := cmd.Root().String("transform") + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "functions rollback-deployment", + Transform: transform, + }) +} + +func handleFunctionsTailLogs(ctx context.Context, cmd *cli.Command) error { + client := zavudev.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("function-id") && len(unusedArgs) > 0 { + cmd.Set("function-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 + } + + params := zavudev.FunctionTailLogsParams{} + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Functions.TailLogs( + ctx, + cmd.Value("function-id").(string), + params, + options..., + ) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") + transform := cmd.Root().String("transform") + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "functions tail-logs", + Transform: transform, + }) +} diff --git a/pkg/cmd/function_test.go b/pkg/cmd/function_test.go new file mode 100644 index 0000000..bd8e9c3 --- /dev/null +++ b/pkg/cmd/function_test.go @@ -0,0 +1,255 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "testing" + + "github.com/zavudev/cli/internal/mocktest" +) + +func TestFunctionsCreate(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "functions", "create", + "--name", "Order Bot", + "--slug", "order-bot", + "--dependencies", "{openai: ^4.20.0}", + "--description", "Replies to order status questions on WhatsApp.", + "--entrypoint", "index.ts", + "--files", `{index.ts: "import { formatOrder } from './lib/orders';\n\nexport default async function handler(event) {\n return { statusCode: 200, body: formatOrder(event) };\n}\n", lib/orders.ts: "export function formatOrder(event) {\n return JSON.stringify(event);\n}\n"}`, + "--http-enabled=true", + "--memory-mb", "128", + "--runtime", "nodejs24", + "--source-code", "import { defineFunction } from '@zavudev/functions';\n\nexport default defineFunction(async (event, ctx) => {\n ctx.log('received', event.type);\n});\n", + "--timeout-sec", "1", + ) + }) + + t.Run("piping data", func(t *testing.T) { + // Test piping YAML data over stdin + pipeData := []byte("" + + "name: Order Bot\n" + + "slug: order-bot\n" + + "dependencies:\n" + + " openai: ^4.20.0\n" + + "description: Replies to order status questions on WhatsApp.\n" + + "entrypoint: index.ts\n" + + "files:\n" + + " index.ts: |\n" + + " import { formatOrder } from './lib/orders';\n" + + "\n" + + " export default async function handler(event) {\n" + + " return { statusCode: 200, body: formatOrder(event) };\n" + + " }\n" + + " lib/orders.ts: |\n" + + " export function formatOrder(event) {\n" + + " return JSON.stringify(event);\n" + + " }\n" + + "httpEnabled: true\n" + + "memoryMb: 128\n" + + "runtime: nodejs24\n" + + "sourceCode: |\n" + + " import { defineFunction } from '@zavudev/functions';\n" + + "\n" + + " export default defineFunction(async (event, ctx) => {\n" + + " ctx.log('received', event.type);\n" + + " });\n" + + "timeoutSec: 1\n") + mocktest.TestRunMockTestWithPipeAndFlags( + t, pipeData, + "--api-key", "string", + "functions", "create", + ) + }) +} + +func TestFunctionsRetrieve(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "functions", "retrieve", + "--function-id", "functionId", + ) + }) +} + +func TestFunctionsUpdate(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "functions", "update", + "--function-id", "functionId", + "--dependencies", "{foo: string}", + "--entrypoint", "index.ts", + "--files", `{index.ts: "import { formatOrder } from './lib/orders';\n\nexport default async function handler(event) {\n return { statusCode: 200, body: formatOrder(event) };\n}\n", lib/orders.ts: "export function formatOrder(event) {\n return JSON.stringify(event);\n}\n"}`, + "--http-enabled=true", + "--source-code", "sourceCode", + ) + }) + + t.Run("piping data", func(t *testing.T) { + // Test piping YAML data over stdin + pipeData := []byte("" + + "dependencies:\n" + + " foo: string\n" + + "entrypoint: index.ts\n" + + "files:\n" + + " index.ts: |\n" + + " import { formatOrder } from './lib/orders';\n" + + "\n" + + " export default async function handler(event) {\n" + + " return { statusCode: 200, body: formatOrder(event) };\n" + + " }\n" + + " lib/orders.ts: |\n" + + " export function formatOrder(event) {\n" + + " return JSON.stringify(event);\n" + + " }\n" + + "httpEnabled: true\n" + + "sourceCode: sourceCode\n") + mocktest.TestRunMockTestWithPipeAndFlags( + t, pipeData, + "--api-key", "string", + "functions", "update", + "--function-id", "functionId", + ) + }) +} + +func TestFunctionsDelete(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "functions", "delete", + "--function-id", "functionId", + ) + }) +} + +func TestFunctionsDeploy(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "functions", "deploy", + "--function-id", "functionId", + "--dependencies", "{foo: string}", + "--entrypoint", "index.ts", + "--files", `{index.ts: "import { formatOrder } from './lib/orders';\n\nexport default async function handler(event) {\n return { statusCode: 200, body: formatOrder(event) };\n}\n", lib/orders.ts: "export function formatOrder(event) {\n return JSON.stringify(event);\n}\n"}`, + "--source-code", "sourceCode", + ) + }) + + t.Run("piping data", func(t *testing.T) { + // Test piping YAML data over stdin + pipeData := []byte("" + + "dependencies:\n" + + " foo: string\n" + + "entrypoint: index.ts\n" + + "files:\n" + + " index.ts: |\n" + + " import { formatOrder } from './lib/orders';\n" + + "\n" + + " export default async function handler(event) {\n" + + " return { statusCode: 200, body: formatOrder(event) };\n" + + " }\n" + + " lib/orders.ts: |\n" + + " export function formatOrder(event) {\n" + + " return JSON.stringify(event);\n" + + " }\n" + + "sourceCode: sourceCode\n") + mocktest.TestRunMockTestWithPipeAndFlags( + t, pipeData, + "--api-key", "string", + "functions", "deploy", + "--function-id", "functionId", + ) + }) +} + +func TestFunctionsGetDeployment(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "functions", "get-deployment", + "--deployment-id", "deploymentId", + ) + }) +} + +func TestFunctionsListDeployments(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "functions", "list-deployments", + "--function-id", "functionId", + "--limit", "100", + ) + }) +} + +func TestFunctionsListEventTypes(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "functions", "list-event-types", + ) + }) +} + +func TestFunctionsRollbackDeployment(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "functions", "rollback-deployment", + "--function-id", "functionId", + "--deployment-id", "fnd_abc123", + ) + }) + + t.Run("piping data", func(t *testing.T) { + // Test piping YAML data over stdin + pipeData := []byte("deploymentId: fnd_abc123") + mocktest.TestRunMockTestWithPipeAndFlags( + t, pipeData, + "--api-key", "string", + "functions", "rollback-deployment", + "--function-id", "functionId", + ) + }) +} + +func TestFunctionsTailLogs(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "functions", "tail-logs", + "--function-id", "functionId", + "--end-time", "0", + "--filter-pattern", "filterPattern", + "--limit", "1", + "--next-token", "nextToken", + "--start-time", "0", + ) + }) +} diff --git a/pkg/cmd/functiongitlink.go b/pkg/cmd/functiongitlink.go new file mode 100644 index 0000000..fbc959a --- /dev/null +++ b/pkg/cmd/functiongitlink.go @@ -0,0 +1,334 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "context" + "fmt" + + "github.com/tidwall/gjson" + "github.com/urfave/cli/v3" + "github.com/zavudev/cli/internal/apiquery" + "github.com/zavudev/cli/internal/requestflag" + "github.com/zavudev/sdk-go" + "github.com/zavudev/sdk-go/option" +) + +var functionsGitLinkRetrieve = cli.Command{ + Name: "retrieve", + Usage: "The link and its last deploy. Never returns the webhook secret.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "function-id", + Required: true, + PathParam: "functionId", + }, + }, + Action: handleFunctionsGitLinkRetrieve, + HideHelpCommand: true, +} + +var functionsGitLinkUpdate = cli.Command{ + Name: "update", + Usage: "Change the branch, the root directory, or whether pushes deploy. Pass at least\none field. `rootDir: null` clears the subdirectory.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "function-id", + Required: true, + PathParam: "functionId", + }, + &requestflag.Flag[bool]{ + Name: "auto-deploy", + BodyPath: "autoDeploy", + }, + &requestflag.Flag[string]{ + Name: "branch", + BodyPath: "branch", + }, + &requestflag.Flag[*string]{ + Name: "root-dir", + BodyPath: "rootDir", + }, + }, + Action: handleFunctionsGitLinkUpdate, + HideHelpCommand: true, +} + +var functionsGitLinkDeployNow = cli.Command{ + Name: "deploy-now", + Usage: "Fetch the linked branch and deploy it without waiting for a push. Returns\nimmediately; follow the outcome with `GET /v1/functions/{functionId}/git-link`,\nwhose `lastStatus` and `lastError` describe the run.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "function-id", + Required: true, + PathParam: "functionId", + }, + }, + Action: handleFunctionsGitLinkDeployNow, + HideHelpCommand: true, +} + +var functionsGitLinkLink = cli.Command{ + Name: "link", + Usage: "Bind a repository to this function so every push to `branch` deploys it. A\nfunction holds at most one link; linking again returns 400.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "function-id", + Required: true, + PathParam: "functionId", + }, + &requestflag.Flag[string]{ + Name: "owner", + Required: true, + BodyPath: "owner", + }, + &requestflag.Flag[string]{ + Name: "repo", + Required: true, + BodyPath: "repo", + }, + &requestflag.Flag[bool]{ + Name: "auto-deploy", + Default: true, + BodyPath: "autoDeploy", + }, + &requestflag.Flag[string]{ + Name: "branch", + Default: "main", + BodyPath: "branch", + }, + &requestflag.Flag[string]{ + Name: "root-dir", + Usage: "Subdirectory holding the project, for monorepos.", + BodyPath: "rootDir", + }, + }, + Action: handleFunctionsGitLinkLink, + HideHelpCommand: true, +} + +var functionsGitLinkUnlink = cli.Command{ + Name: "unlink", + Usage: "Remove the link. The function and its deployments stay. A manual webhook left in\nthe repository stops being accepted, so remove it there too.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "function-id", + Required: true, + PathParam: "functionId", + }, + }, + Action: handleFunctionsGitLinkUnlink, + HideHelpCommand: true, +} + +func handleFunctionsGitLinkRetrieve(ctx context.Context, cmd *cli.Command) error { + client := zavudev.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("function-id") && len(unusedArgs) > 0 { + cmd.Set("function-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 + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Functions.GitLink.Get(ctx, cmd.Value("function-id").(string), options...) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") + transform := cmd.Root().String("transform") + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "functions:git-link retrieve", + Transform: transform, + }) +} + +func handleFunctionsGitLinkUpdate(ctx context.Context, cmd *cli.Command) error { + client := zavudev.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("function-id") && len(unusedArgs) > 0 { + cmd.Set("function-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, + ApplicationJSON, + false, + ) + if err != nil { + return err + } + + params := zavudev.FunctionGitLinkUpdateParams{} + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Functions.GitLink.Update( + ctx, + cmd.Value("function-id").(string), + params, + options..., + ) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") + transform := cmd.Root().String("transform") + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "functions:git-link update", + Transform: transform, + }) +} + +func handleFunctionsGitLinkDeployNow(ctx context.Context, cmd *cli.Command) error { + client := zavudev.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("function-id") && len(unusedArgs) > 0 { + cmd.Set("function-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 + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Functions.GitLink.DeployNow(ctx, cmd.Value("function-id").(string), options...) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") + transform := cmd.Root().String("transform") + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "functions:git-link deploy-now", + Transform: transform, + }) +} + +func handleFunctionsGitLinkLink(ctx context.Context, cmd *cli.Command) error { + client := zavudev.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("function-id") && len(unusedArgs) > 0 { + cmd.Set("function-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, + ApplicationJSON, + false, + ) + if err != nil { + return err + } + + params := zavudev.FunctionGitLinkLinkParams{} + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Functions.GitLink.Link( + ctx, + cmd.Value("function-id").(string), + params, + options..., + ) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") + transform := cmd.Root().String("transform") + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "functions:git-link link", + Transform: transform, + }) +} + +func handleFunctionsGitLinkUnlink(ctx context.Context, cmd *cli.Command) error { + client := zavudev.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("function-id") && len(unusedArgs) > 0 { + cmd.Set("function-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.Functions.GitLink.Unlink(ctx, cmd.Value("function-id").(string), options...) +} diff --git a/pkg/cmd/functiongitlink_test.go b/pkg/cmd/functiongitlink_test.go new file mode 100644 index 0000000..aca8265 --- /dev/null +++ b/pkg/cmd/functiongitlink_test.go @@ -0,0 +1,107 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "testing" + + "github.com/zavudev/cli/internal/mocktest" +) + +func TestFunctionsGitLinkRetrieve(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "functions:git-link", "retrieve", + "--function-id", "functionId", + ) + }) +} + +func TestFunctionsGitLinkUpdate(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "functions:git-link", "update", + "--function-id", "functionId", + "--auto-deploy=false", + "--branch", "branch", + "--root-dir", "rootDir", + ) + }) + + t.Run("piping data", func(t *testing.T) { + // Test piping YAML data over stdin + pipeData := []byte("" + + "autoDeploy: false\n" + + "branch: branch\n" + + "rootDir: rootDir\n") + mocktest.TestRunMockTestWithPipeAndFlags( + t, pipeData, + "--api-key", "string", + "functions:git-link", "update", + "--function-id", "functionId", + ) + }) +} + +func TestFunctionsGitLinkDeployNow(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "functions:git-link", "deploy-now", + "--function-id", "functionId", + ) + }) +} + +func TestFunctionsGitLinkLink(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "functions:git-link", "link", + "--function-id", "functionId", + "--owner", "acme", + "--repo", "order-bot", + "--auto-deploy=true", + "--branch", "main", + "--root-dir", "apps/bot", + ) + }) + + t.Run("piping data", func(t *testing.T) { + // Test piping YAML data over stdin + pipeData := []byte("" + + "owner: acme\n" + + "repo: order-bot\n" + + "autoDeploy: true\n" + + "branch: main\n" + + "rootDir: apps/bot\n") + mocktest.TestRunMockTestWithPipeAndFlags( + t, pipeData, + "--api-key", "string", + "functions:git-link", "link", + "--function-id", "functionId", + ) + }) +} + +func TestFunctionsGitLinkUnlink(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "functions:git-link", "unlink", + "--function-id", "functionId", + ) + }) +} diff --git a/pkg/cmd/functionsecret.go b/pkg/cmd/functionsecret.go new file mode 100644 index 0000000..d5af2a3 --- /dev/null +++ b/pkg/cmd/functionsecret.go @@ -0,0 +1,202 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "context" + "fmt" + + "github.com/tidwall/gjson" + "github.com/urfave/cli/v3" + "github.com/zavudev/cli/internal/apiquery" + "github.com/zavudev/cli/internal/requestflag" + "github.com/zavudev/sdk-go" + "github.com/zavudev/sdk-go/option" +) + +var functionsSecretsList = cli.Command{ + Name: "list", + Usage: "Lists every secret key set on the function. Plaintext is NEVER returned — only\nthe last 4 characters of each value, for visual confirmation.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "function-id", + Required: true, + PathParam: "functionId", + }, + }, + Action: handleFunctionsSecretsList, + HideHelpCommand: true, +} + +var functionsSecretsSet = cli.Command{ + Name: "set", + Usage: "Create or update a secret on a function. Marks the function out-of-sync; the\nnext `POST /deploy` re-publishes the Lambda with the new env. Keys must match\n`[A-Z_][A-Z0-9_]*` (uppercase env-var style) and cannot start with reserved\nprefixes (AWS*, LAMBDA*, etc).", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "function-id", + Required: true, + PathParam: "functionId", + }, + &requestflag.Flag[string]{ + Name: "key", + Required: true, + PathParam: "key", + }, + &requestflag.Flag[string]{ + Name: "value", + Required: true, + BodyPath: "value", + }, + }, + Action: handleFunctionsSecretsSet, + HideHelpCommand: true, +} + +var functionsSecretsUnset = cli.Command{ + Name: "unset", + Usage: "Remove a secret from a function. Doesn't take effect on the running Lambda until\nthe next deploy.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "function-id", + Required: true, + PathParam: "functionId", + }, + &requestflag.Flag[string]{ + Name: "key", + Required: true, + PathParam: "key", + }, + }, + Action: handleFunctionsSecretsUnset, + HideHelpCommand: true, +} + +func handleFunctionsSecretsList(ctx context.Context, cmd *cli.Command) error { + client := zavudev.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("function-id") && len(unusedArgs) > 0 { + cmd.Set("function-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 + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Functions.Secrets.List(ctx, cmd.Value("function-id").(string), options...) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") + transform := cmd.Root().String("transform") + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "functions:secrets list", + Transform: transform, + }) +} + +func handleFunctionsSecretsSet(ctx context.Context, cmd *cli.Command) error { + client := zavudev.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("key") && len(unusedArgs) > 0 { + cmd.Set("key", 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, + ApplicationJSON, + false, + ) + if err != nil { + return err + } + + params := zavudev.FunctionSecretSetParams{ + FunctionID: cmd.Value("function-id").(string), + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Functions.Secrets.Set( + ctx, + cmd.Value("key").(string), + params, + options..., + ) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") + transform := cmd.Root().String("transform") + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "functions:secrets set", + Transform: transform, + }) +} + +func handleFunctionsSecretsUnset(ctx context.Context, cmd *cli.Command) error { + client := zavudev.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("key") && len(unusedArgs) > 0 { + cmd.Set("key", 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 + } + + params := zavudev.FunctionSecretUnsetParams{ + FunctionID: cmd.Value("function-id").(string), + } + + return client.Functions.Secrets.Unset( + ctx, + cmd.Value("key").(string), + params, + options..., + ) +} diff --git a/pkg/cmd/functionsecret_test.go b/pkg/cmd/functionsecret_test.go new file mode 100644 index 0000000..9040f62 --- /dev/null +++ b/pkg/cmd/functionsecret_test.go @@ -0,0 +1,60 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "testing" + + "github.com/zavudev/cli/internal/mocktest" +) + +func TestFunctionsSecretsList(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "functions:secrets", "list", + "--function-id", "functionId", + ) + }) +} + +func TestFunctionsSecretsSet(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "functions:secrets", "set", + "--function-id", "functionId", + "--key", "key", + "--value", "value", + ) + }) + + t.Run("piping data", func(t *testing.T) { + // Test piping YAML data over stdin + pipeData := []byte("value: value") + mocktest.TestRunMockTestWithPipeAndFlags( + t, pipeData, + "--api-key", "string", + "functions:secrets", "set", + "--function-id", "functionId", + "--key", "key", + ) + }) +} + +func TestFunctionsSecretsUnset(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "functions:secrets", "unset", + "--function-id", "functionId", + "--key", "key", + ) + }) +} diff --git a/pkg/cmd/functiontrigger.go b/pkg/cmd/functiontrigger.go new file mode 100644 index 0000000..4794571 --- /dev/null +++ b/pkg/cmd/functiontrigger.go @@ -0,0 +1,262 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "context" + "fmt" + + "github.com/tidwall/gjson" + "github.com/urfave/cli/v3" + "github.com/zavudev/cli/internal/apiquery" + "github.com/zavudev/cli/internal/requestflag" + "github.com/zavudev/sdk-go" + "github.com/zavudev/sdk-go/option" +) + +var functionsTriggersCreate = cli.Command{ + Name: "create", + Usage: "Subscribe a function to one or more event types, optionally scoped to specific\nsenders. Provide eventTypes and senderIds (use null in senderIds for all\nsenders); a trigger is created for each event type and sender combination.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "function-id", + Required: true, + PathParam: "functionId", + }, + &requestflag.Flag[[]string]{ + Name: "event-type", + Usage: "Event types to subscribe to.", + Required: true, + BodyPath: "eventTypes", + }, + &requestflag.Flag[[]string]{ + Name: "sender-id", + Usage: "Senders to scope the triggers to. Use null for all senders.", + Required: true, + BodyPath: "senderIds", + }, + &requestflag.Flag[string]{ + Name: "cron", + Usage: "Required when eventTypes includes `cron`: a 5-field cron expression (minute hour day-of-month month day-of-week), evaluated in UTC.", + BodyPath: "cron", + }, + }, + Action: handleFunctionsTriggersCreate, + HideHelpCommand: true, +} + +var functionsTriggersUpdate = cli.Command{ + Name: "update", + Usage: "Enable or disable a trigger", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "trigger-id", + Required: true, + PathParam: "triggerId", + }, + &requestflag.Flag[bool]{ + Name: "active", + Required: true, + BodyPath: "active", + }, + }, + Action: handleFunctionsTriggersUpdate, + HideHelpCommand: true, +} + +var functionsTriggersList = cli.Command{ + Name: "list", + Usage: "List function triggers", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "function-id", + Required: true, + PathParam: "functionId", + }, + }, + Action: handleFunctionsTriggersList, + HideHelpCommand: true, +} + +var functionsTriggersDelete = cli.Command{ + Name: "delete", + Usage: "Delete a trigger", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "trigger-id", + Required: true, + PathParam: "triggerId", + }, + }, + Action: handleFunctionsTriggersDelete, + HideHelpCommand: true, +} + +func handleFunctionsTriggersCreate(ctx context.Context, cmd *cli.Command) error { + client := zavudev.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("function-id") && len(unusedArgs) > 0 { + cmd.Set("function-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, + ApplicationJSON, + false, + ) + if err != nil { + return err + } + + params := zavudev.FunctionTriggerNewParams{} + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Functions.Triggers.New( + ctx, + cmd.Value("function-id").(string), + params, + options..., + ) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") + transform := cmd.Root().String("transform") + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "functions:triggers create", + Transform: transform, + }) +} + +func handleFunctionsTriggersUpdate(ctx context.Context, cmd *cli.Command) error { + client := zavudev.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("trigger-id") && len(unusedArgs) > 0 { + cmd.Set("trigger-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, + ApplicationJSON, + false, + ) + if err != nil { + return err + } + + params := zavudev.FunctionTriggerUpdateParams{} + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Functions.Triggers.Update( + ctx, + cmd.Value("trigger-id").(string), + params, + options..., + ) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") + transform := cmd.Root().String("transform") + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "functions:triggers update", + Transform: transform, + }) +} + +func handleFunctionsTriggersList(ctx context.Context, cmd *cli.Command) error { + client := zavudev.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("function-id") && len(unusedArgs) > 0 { + cmd.Set("function-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 + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Functions.Triggers.List(ctx, cmd.Value("function-id").(string), options...) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") + transform := cmd.Root().String("transform") + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "functions:triggers list", + Transform: transform, + }) +} + +func handleFunctionsTriggersDelete(ctx context.Context, cmd *cli.Command) error { + client := zavudev.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("trigger-id") && len(unusedArgs) > 0 { + cmd.Set("trigger-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.Functions.Triggers.Delete(ctx, cmd.Value("trigger-id").(string), options...) +} diff --git a/pkg/cmd/functiontrigger_test.go b/pkg/cmd/functiontrigger_test.go new file mode 100644 index 0000000..7bccddd --- /dev/null +++ b/pkg/cmd/functiontrigger_test.go @@ -0,0 +1,88 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "testing" + + "github.com/zavudev/cli/internal/mocktest" +) + +func TestFunctionsTriggersCreate(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "functions:triggers", "create", + "--function-id", "functionId", + "--event-type", "message.inbound", + "--sender-id", "null", + "--cron", "0 9 * * 1-5", + ) + }) + + t.Run("piping data", func(t *testing.T) { + // Test piping YAML data over stdin + pipeData := []byte("" + + "eventTypes:\n" + + " - message.inbound\n" + + "senderIds:\n" + + " - null\n" + + "cron: 0 9 * * 1-5\n") + mocktest.TestRunMockTestWithPipeAndFlags( + t, pipeData, + "--api-key", "string", + "functions:triggers", "create", + "--function-id", "functionId", + ) + }) +} + +func TestFunctionsTriggersUpdate(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "functions:triggers", "update", + "--trigger-id", "triggerId", + "--active=true", + ) + }) + + t.Run("piping data", func(t *testing.T) { + // Test piping YAML data over stdin + pipeData := []byte("active: true") + mocktest.TestRunMockTestWithPipeAndFlags( + t, pipeData, + "--api-key", "string", + "functions:triggers", "update", + "--trigger-id", "triggerId", + ) + }) +} + +func TestFunctionsTriggersList(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "functions:triggers", "list", + "--function-id", "functionId", + ) + }) +} + +func TestFunctionsTriggersDelete(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "functions:triggers", "delete", + "--trigger-id", "triggerId", + ) + }) +} diff --git a/pkg/cmd/introspect.go b/pkg/cmd/introspect.go index e7d3cea..752663b 100644 --- a/pkg/cmd/introspect.go +++ b/pkg/cmd/introspect.go @@ -5,7 +5,6 @@ package cmd import ( "context" "fmt" - "os" "github.com/tidwall/gjson" "github.com/urfave/cli/v3" @@ -15,6 +14,26 @@ import ( "github.com/zavudev/sdk-go/option" ) +var introspectValidateEmail = cli.Command{ + Name: "validate-email", + Usage: "Heuristic email validation to run before sending: catches invalid syntax, dead\ndomains (no MX/A records), disposable inboxes, role-based addresses (info@,\ncontacto@, sales@), and addresses already on your project's suppression list.\nUse it to clean a list before a broadcast and keep your bounce rate low.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "email", + Usage: "Single email address to validate.", + BodyPath: "email", + }, + &requestflag.Flag[[]string]{ + Name: "email", + Usage: "Batch of email addresses to validate (max 100).", + BodyPath: "emails", + }, + }, + Action: handleIntrospectValidateEmail, + HideHelpCommand: true, +} + var introspectValidatePhone = cli.Command{ Name: "validate-phone", Usage: "Validate a phone number and check if a WhatsApp conversation window is open.", @@ -30,7 +49,7 @@ var introspectValidatePhone = cli.Command{ HideHelpCommand: true, } -func handleIntrospectValidatePhone(ctx context.Context, cmd *cli.Command) error { +func handleIntrospectValidateEmail(ctx context.Context, cmd *cli.Command) error { client := zavudev.NewClient(getDefaultRequestOptions(cmd)...) unusedArgs := cmd.Args().Slice() @@ -38,7 +57,46 @@ func handleIntrospectValidatePhone(ctx context.Context, cmd *cli.Command) error return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := zavudev.IntrospectValidatePhoneParams{} + options, err := flagOptions( + cmd, + apiquery.NestedQueryFormatBrackets, + apiquery.ArrayQueryFormatComma, + ApplicationJSON, + false, + ) + if err != nil { + return err + } + + params := zavudev.IntrospectValidateEmailParams{} + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Introspect.ValidateEmail(ctx, params, options...) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") + transform := cmd.Root().String("transform") + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "introspect validate-email", + Transform: transform, + }) +} + +func handleIntrospectValidatePhone(ctx context.Context, cmd *cli.Command) error { + client := zavudev.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } options, err := flagOptions( cmd, @@ -51,6 +109,8 @@ func handleIntrospectValidatePhone(ctx context.Context, cmd *cli.Command) error return err } + params := zavudev.IntrospectValidatePhoneParams{} + var res []byte options = append(options, option.WithResponseBodyInto(&res)) _, err = client.Introspect.ValidatePhone(ctx, params, options...) @@ -60,6 +120,13 @@ func handleIntrospectValidatePhone(ctx context.Context, cmd *cli.Command) error obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "introspect validate-phone", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "introspect validate-phone", + Transform: transform, + }) } diff --git a/pkg/cmd/introspect_test.go b/pkg/cmd/introspect_test.go index 99bc1fa..fc56fb6 100644 --- a/pkg/cmd/introspect_test.go +++ b/pkg/cmd/introspect_test.go @@ -8,6 +8,34 @@ import ( "github.com/zavudev/cli/internal/mocktest" ) +func TestIntrospectValidateEmail(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "introspect", "validate-email", + "--email", "maria@example.com", + "--email", "maria@example.com", + "--email", "info@deaddomain.example", + ) + }) + + t.Run("piping data", func(t *testing.T) { + // Test piping YAML data over stdin + pipeData := []byte("" + + "email: maria@example.com\n" + + "emails:\n" + + " - maria@example.com\n" + + " - info@deaddomain.example\n") + mocktest.TestRunMockTestWithPipeAndFlags( + t, pipeData, + "--api-key", "string", + "introspect", "validate-email", + ) + }) +} + func TestIntrospectValidatePhone(t *testing.T) { t.Skip("Mock server tests are disabled") t.Run("regular flags", func(t *testing.T) { diff --git a/pkg/cmd/invitation.go b/pkg/cmd/invitation.go new file mode 100644 index 0000000..c983459 --- /dev/null +++ b/pkg/cmd/invitation.go @@ -0,0 +1,300 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "context" + "fmt" + + "github.com/tidwall/gjson" + "github.com/urfave/cli/v3" + "github.com/zavudev/cli/internal/apiquery" + "github.com/zavudev/cli/internal/requestflag" + "github.com/zavudev/sdk-go" + "github.com/zavudev/sdk-go/option" +) + +var invitationsCreate = cli.Command{ + Name: "create", + Usage: "Create a partner invitation link for a client to connect a Meta channel. The\nclient opens the returned `url` and authorizes with Meta; the resulting sender\nis created in your project when they finish, and the invitation transitions to\n`completed`.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[[]string]{ + Name: "allowed-phone-country", + Usage: "ISO country codes for allowed phone numbers. Only valid when `connectionType` is `whatsapp_waba` — sending it with `messenger` returns 400.", + BodyPath: "allowedPhoneCountries", + }, + &requestflag.Flag[string]{ + Name: "client-email", + Usage: "Email of the client being invited.", + BodyPath: "clientEmail", + }, + &requestflag.Flag[string]{ + Name: "client-name", + Usage: "Name of the client being invited.", + BodyPath: "clientName", + }, + &requestflag.Flag[string]{ + Name: "client-phone", + Usage: "Phone number of the client in E.164 format.", + BodyPath: "clientPhone", + }, + &requestflag.Flag[string]{ + Name: "connection-type", + Usage: "Which Meta channel the client connects, and how.\n- `whatsapp_waba` (default): Meta's embedded signup links an official WhatsApp Business Account. Accepts `phoneNumberId` and `allowedPhoneCountries`.\n- `messenger`: the client authorizes with Facebook and picks a Facebook Page they administer. The Page's Messenger inbox — including Marketplace chats — is routed to Zavu. They must be an admin of at least one Page. A Page can only be connected to one Zavu project at a time: if the client picks a Page that another project already connected, the newer connection wins and the older one is disconnected.\n\nOne invitation connects one channel. To onboard a client on several channels, create one invitation per channel; each completes into its own sender.", + Default: "whatsapp_waba", + BodyPath: "connectionType", + }, + &requestflag.Flag[int64]{ + Name: "expires-in-days", + Usage: "Number of days until the invitation expires.", + Default: 7, + BodyPath: "expiresInDays", + }, + &requestflag.Flag[string]{ + Name: "phone-number-id", + Usage: "ID of a Zavu phone number to pre-assign for WhatsApp registration. If provided, the client will use this number instead of their own. Only valid when `connectionType` is `whatsapp_waba` — sending it with `messenger` returns 400, since a Facebook Page has no phone number.", + BodyPath: "phoneNumberId", + }, + }, + Action: handleInvitationsCreate, + HideHelpCommand: true, +} + +var invitationsRetrieve = cli.Command{ + Name: "retrieve", + Usage: "Get invitation", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "invitation-id", + Required: true, + PathParam: "invitationId", + }, + }, + Action: handleInvitationsRetrieve, + HideHelpCommand: true, +} + +var invitationsList = cli.Command{ + Name: "list", + Usage: "List partner invitations for this project.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "cursor", + QueryPath: "cursor", + }, + &requestflag.Flag[int64]{ + Name: "limit", + Default: 50, + QueryPath: "limit", + }, + &requestflag.Flag[string]{ + Name: "status", + Usage: "Current status of the partner invitation.\n\n`failed` means the client started the connection and it did not finish (they cancelled Meta's dialog, denied a permission, or abandoned the tab). A failed invitation is still usable: the same link can be retried, and it moves back to `in_progress` when the client tries again.", + QueryPath: "status", + }, + &requestflag.Flag[int64]{ + Name: "max-items", + Usage: "The maximum number of items to return (use -1 for unlimited).", + }, + }, + Action: handleInvitationsList, + HideHelpCommand: true, +} + +var invitationsCancel = cli.Command{ + Name: "cancel", + Usage: "Cancel an active invitation. The client will no longer be able to use the\ninvitation link.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "invitation-id", + Required: true, + PathParam: "invitationId", + }, + }, + Action: handleInvitationsCancel, + HideHelpCommand: true, +} + +func handleInvitationsCreate(ctx context.Context, cmd *cli.Command) error { + client := zavudev.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + + options, err := flagOptions( + cmd, + apiquery.NestedQueryFormatBrackets, + apiquery.ArrayQueryFormatComma, + ApplicationJSON, + false, + ) + if err != nil { + return err + } + + params := zavudev.InvitationNewParams{} + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Invitations.New(ctx, params, options...) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") + transform := cmd.Root().String("transform") + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "invitations create", + Transform: transform, + }) +} + +func handleInvitationsRetrieve(ctx context.Context, cmd *cli.Command) error { + client := zavudev.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("invitation-id") && len(unusedArgs) > 0 { + cmd.Set("invitation-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 + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Invitations.Get(ctx, cmd.Value("invitation-id").(string), options...) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") + transform := cmd.Root().String("transform") + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "invitations retrieve", + Transform: transform, + }) +} + +func handleInvitationsList(ctx context.Context, cmd *cli.Command) error { + client := zavudev.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + + 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 + } + + params := zavudev.InvitationListParams{} + + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") + transform := cmd.Root().String("transform") + if format == "raw" { + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Invitations.List(ctx, params, options...) + if err != nil { + return err + } + obj := gjson.ParseBytes(res) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "invitations list", + Transform: transform, + }) + } else { + iter := client.Invitations.ListAutoPaging(ctx, params, options...) + maxItems := int64(-1) + if cmd.IsSet("max-items") { + maxItems = cmd.Value("max-items").(int64) + } + return ShowJSONIterator(iter, maxItems, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "invitations list", + Transform: transform, + }) + } +} + +func handleInvitationsCancel(ctx context.Context, cmd *cli.Command) error { + client := zavudev.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("invitation-id") && len(unusedArgs) > 0 { + cmd.Set("invitation-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 + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Invitations.Cancel(ctx, cmd.Value("invitation-id").(string), options...) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") + transform := cmd.Root().String("transform") + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "invitations cancel", + Transform: transform, + }) +} diff --git a/pkg/cmd/invitation_test.go b/pkg/cmd/invitation_test.go new file mode 100644 index 0000000..99140bb --- /dev/null +++ b/pkg/cmd/invitation_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/zavudev/cli/internal/mocktest" +) + +func TestInvitationsCreate(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "invitations", "create", + "--allowed-phone-country", "US", + "--allowed-phone-country", "MX", + "--client-email", "contact@acme.com", + "--client-name", "Acme Corp", + "--client-phone", "+14155551234", + "--connection-type", "whatsapp_waba", + "--expires-in-days", "1", + "--phone-number-id", "pn_abc123", + ) + }) + + t.Run("piping data", func(t *testing.T) { + // Test piping YAML data over stdin + pipeData := []byte("" + + "allowedPhoneCountries:\n" + + " - US\n" + + " - MX\n" + + "clientEmail: contact@acme.com\n" + + "clientName: Acme Corp\n" + + "clientPhone: '+14155551234'\n" + + "connectionType: whatsapp_waba\n" + + "expiresInDays: 1\n" + + "phoneNumberId: pn_abc123\n") + mocktest.TestRunMockTestWithPipeAndFlags( + t, pipeData, + "--api-key", "string", + "invitations", "create", + ) + }) +} + +func TestInvitationsRetrieve(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "invitations", "retrieve", + "--invitation-id", "invitationId", + ) + }) +} + +func TestInvitationsList(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "invitations", "list", + "--max-items", "10", + "--cursor", "cursor", + "--limit", "100", + "--status", "pending", + ) + }) +} + +func TestInvitationsCancel(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "invitations", "cancel", + "--invitation-id", "invitationId", + ) + }) +} diff --git a/pkg/cmd/me.go b/pkg/cmd/me.go new file mode 100644 index 0000000..02d3892 --- /dev/null +++ b/pkg/cmd/me.go @@ -0,0 +1,62 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "context" + "fmt" + + "github.com/tidwall/gjson" + "github.com/urfave/cli/v3" + "github.com/zavudev/cli/internal/apiquery" + "github.com/zavudev/sdk-go" + "github.com/zavudev/sdk-go/option" +) + +var meRetrieve = cli.Command{ + Name: "retrieve", + Usage: "Returns the project, team, and API key metadata bound to the current Bearer\ntoken. Used by CLIs and SDKs to confirm which project they will operate on.", + Suggest: true, + Flags: []cli.Flag{}, + Action: handleMeRetrieve, + HideHelpCommand: true, +} + +func handleMeRetrieve(ctx context.Context, cmd *cli.Command) error { + client := zavudev.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + + 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 + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Me.Get(ctx, options...) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") + transform := cmd.Root().String("transform") + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "me retrieve", + Transform: transform, + }) +} diff --git a/pkg/cmd/me_test.go b/pkg/cmd/me_test.go new file mode 100644 index 0000000..51b532a --- /dev/null +++ b/pkg/cmd/me_test.go @@ -0,0 +1,20 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "testing" + + "github.com/zavudev/cli/internal/mocktest" +) + +func TestMeRetrieve(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "me", "retrieve", + ) + }) +} diff --git a/pkg/cmd/message.go b/pkg/cmd/message.go index 25fe35b..1f0d6b8 100644 --- a/pkg/cmd/message.go +++ b/pkg/cmd/message.go @@ -5,7 +5,6 @@ package cmd import ( "context" "fmt" - "os" "github.com/tidwall/gjson" "github.com/urfave/cli/v3" @@ -21,8 +20,9 @@ var messagesRetrieve = cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "message-id", - Required: true, + Name: "message-id", + Required: true, + PathParam: "messageId", }, }, Action: handleMessagesRetrieve, @@ -36,7 +36,7 @@ var messagesList = cli.Command{ Flags: []cli.Flag{ &requestflag.Flag[string]{ Name: "channel", - Usage: "Delivery channel. Use 'auto' for intelligent routing.", + Usage: "Filter by delivery channel.", QueryPath: "channel", }, &requestflag.Flag[string]{ @@ -50,7 +50,7 @@ var messagesList = cli.Command{ }, &requestflag.Flag[string]{ Name: "status", - Usage: `Allowed values: "queued", "sending", "sent", "delivered", "read", "failed", "received", "pending_url_verification".`, + Usage: "Filter by status. Not all stored statuses are filterable.", QueryPath: "status", }, &requestflag.Flag[string]{ @@ -66,14 +66,30 @@ var messagesList = cli.Command{ HideHelpCommand: true, } +var messagesListAttachments = cli.Command{ + Name: "list-attachments", + Usage: "List the stored file attachments for an email message and get a short-lived\nsigned `downloadUrl` for each. Works for both inbound emails (received via\n`message.inbound`) and outbound emails you sent with attachments. Messages\nwithout stored attachments (including SMS, WhatsApp, and other channels) return\nan empty list. Each `downloadUrl` is generated fresh per request and expires —\nfetch the file promptly and do not cache the URL.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "message-id", + Required: true, + PathParam: "messageId", + }, + }, + Action: handleMessagesListAttachments, + HideHelpCommand: true, +} + var messagesReact = cli.Command{ Name: "react", Usage: "Send an emoji reaction to an existing WhatsApp message. Reactions are only\nsupported for WhatsApp messages.", Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "message-id", - Required: true, + Name: "message-id", + Required: true, + PathParam: "messageId", }, &requestflag.Flag[string]{ Name: "emoji", @@ -97,7 +113,7 @@ var messagesSend = requestflag.WithInnerFlags(cli.Command{ Flags: []cli.Flag{ &requestflag.Flag[string]{ Name: "to", - Usage: "Recipient phone number in E.164 format, email address, or numeric chat ID (for Telegram/Instagram).", + Usage: "Recipient phone number in E.164 format, email address, WhatsApp business-scoped user ID (BSUID, e.g. `US.13491208655302741918`), or numeric chat ID (for Telegram/Instagram/Messenger). A BSUID is routed to WhatsApp and sent via the `recipient` field; use it to message a contact who adopted a username and whose phone number is hidden.", Required: true, BodyPath: "to", }, @@ -134,7 +150,7 @@ var messagesSend = requestflag.WithInnerFlags(cli.Command{ }, &requestflag.Flag[string]{ Name: "message-type", - Usage: "Type of message. Non-text types are supported by WhatsApp and Telegram (varies by type).", + Usage: "Type of message. Non-text types are supported by WhatsApp and Telegram (varies by type).\n\n`location_request` asks the recipient to share their location and is WhatsApp-only. It takes no `content` object — the prompt goes in `text` (max 1024 characters) and the button label is fixed by WhatsApp. The recipient's answer arrives as an inbound `location` message whose `content.replyToMessageId` is the ID of the request.\n\n`request_contact_info` asks the recipient to share their phone number and is WhatsApp-only. Like `location_request` it takes no `content` object — the prompt goes in `text` (max 1024 characters) and WhatsApp renders a fixed **Share Contact Info** button. The answer arrives as an inbound `contact` message. Use it to recover the phone number of a contact who adopted a WhatsApp username and is only known by their business-scoped user ID (BSUID); when they share it, Zavu automatically links the phone number to that contact.", BodyPath: "messageType", }, &requestflag.Flag[map[string]any]{ @@ -208,6 +224,31 @@ var messagesSend = requestflag.WithInnerFlags(cli.Command{ Usage: "Contact cards for contact messages.", InnerField: "contacts", }, + &requestflag.InnerFlag[string]{ + Name: "content.cta-display-text", + Usage: "Button label for cta_url messages.", + InnerField: "ctaDisplayText", + }, + &requestflag.InnerFlag[string]{ + Name: "content.cta-header-media-url", + Usage: "Public HTTPS URL of the header media when ctaHeaderType is 'image', 'video', or 'document'. WhatsApp fetches this URL — it must be publicly reachable and return the declared content type.", + InnerField: "ctaHeaderMediaUrl", + }, + &requestflag.InnerFlag[string]{ + Name: "content.cta-header-text", + Usage: "Header text when ctaHeaderType is 'text'.", + InnerField: "ctaHeaderText", + }, + &requestflag.InnerFlag[string]{ + Name: "content.cta-header-type", + Usage: "Optional header type for cta_url messages.", + InnerField: "ctaHeaderType", + }, + &requestflag.InnerFlag[string]{ + Name: "content.cta-url", + Usage: "Destination URL opened in the device's default browser when the button is tapped. Used with messageType=cta_url. WhatsApp requires HTTPS in production.", + InnerField: "ctaUrl", + }, &requestflag.InnerFlag[string]{ Name: "content.emoji", Usage: "Emoji for reaction messages.", @@ -218,6 +259,11 @@ var messagesSend = requestflag.WithInnerFlags(cli.Command{ Usage: "Filename for documents.", InnerField: "filename", }, + &requestflag.InnerFlag[string]{ + Name: "content.footer-text", + Usage: "Optional footer text for cta_url messages.", + InnerField: "footerText", + }, &requestflag.InnerFlag[float64]{ Name: "content.latitude", Usage: "Latitude for location messages.", @@ -263,11 +309,51 @@ var messagesSend = requestflag.WithInnerFlags(cli.Command{ Usage: "Message ID to react to.", InnerField: "reactToMessageId", }, + &requestflag.InnerFlag[map[string]any]{ + Name: "content.referral", + Usage: "Click-to-WhatsApp (CTWA) ad attribution: where an inbound conversation came from.\n\nWhatsApp only. Present on the **first inbound message** of a conversation opened from a Meta ad or post, and on no message after it — so store it when it arrives rather than expecting it again. Organic conversations never carry it.\n\nField names are camelCased to match the rest of this API; Meta sends them as snake_case (`ctwa_clid`, `source_id`, ...). Fields that do not apply are omitted: a `post` source has no click id, and an image ad has no `videoUrl`.", + InnerField: "referral", + }, + &requestflag.InnerFlag[string]{ + Name: "content.reply-to-from", + Usage: "Sender of the quoted message (phone number in E.164 format).", + InnerField: "replyToFrom", + }, + &requestflag.InnerFlag[string]{ + Name: "content.reply-to-message-id", + Usage: "Zavu message ID of the quoted message this message replies to. Present on inbound messages that quote an earlier message. Omitted when the quoted message is not found in Zavu (e.g. an old or unknown message) — use replyToProviderMessageId in that case.", + InnerField: "replyToMessageId", + }, + &requestflag.InnerFlag[string]{ + Name: "content.reply-to-message-type", + Usage: "Type of the quoted message (text, image, video, etc.).", + InnerField: "replyToMessageType", + }, + &requestflag.InnerFlag[string]{ + Name: "content.reply-to-provider-message-id", + Usage: "Provider message ID (WhatsApp WAMID) of the quoted message. Present whenever an inbound message is a reply, even if the quoted message is not stored in Zavu.", + InnerField: "replyToProviderMessageId", + }, + &requestflag.InnerFlag[string]{ + Name: "content.reply-to-text", + Usage: "Truncated snippet of the quoted message's text, for display. Empty when the quoted message has no text (e.g. media).", + InnerField: "replyToText", + }, &requestflag.InnerFlag[[]map[string]any]{ Name: "content.sections", Usage: "Sections for list messages.", InnerField: "sections", }, + &requestflag.InnerFlag[map[string]any]{ + Name: "content.template-button-variables", + Usage: "Variables for dynamic button placeholders (URL buttons and OTP buttons). Keys are the button index (0, 1, 2) in the template's `buttons` array — not the placeholder name. Values substitute the `{{1}}` placeholder inside that button's URL.\n\n**WhatsApp constraints:**\n- URL buttons only accept `{{1}}` — positional, numeric, no whitespace, no name. Named placeholders like `{{token}}` are stored as literal URL text by Meta and cannot be substituted.\n- At most one placeholder per URL button.\n- A template may have at most three buttons.\n- Static URL buttons (no placeholder) and `quick_reply` buttons are not included here.", + InnerField: "templateButtonVariables", + }, + &requestflag.InnerFlag[map[string]any]{ + Name: "content.template-header-variables", + Usage: "Value for a text-header variable, keyed by `1` (WhatsApp text headers allow at most one variable). Optional override. If omitted, Zavu resolves the header from `templateVariables` using the header placeholder's name (e.g. `novios`). Static text headers need no value.", + InnerField: "templateHeaderVariables", + }, &requestflag.InnerFlag[string]{ Name: "content.template-id", Usage: "Template ID for template messages.", @@ -275,12 +361,31 @@ var messagesSend = requestflag.WithInnerFlags(cli.Command{ }, &requestflag.InnerFlag[map[string]any]{ Name: "content.template-variables", - Usage: "Variables for template rendering. Keys are variable positions (1, 2, 3...).", + Usage: "Variables for body placeholders. Key them to match the template body: by position (`1`, `2`, ...) for positional templates, or by name (e.g. `customer_name`) for named templates. Zavu detects the template's format and sends the correct payload to Meta. Named keys also resolve a named text-header variable. Do not mix positional and named keys in the same request.", InnerField: "templateVariables", }, }, }) +var messagesShowTyping = cli.Command{ + Name: "show-typing", + Usage: "Mark an inbound WhatsApp message as read and display a typing indicator to the\nuser while you prepare a response. The indicator is automatically dismissed when\nyou send a reply, or after 25 seconds — whichever comes first. Only valid for\ninbound WhatsApp messages. Use this when a reply will take more than a couple of\nseconds (LLM agent, tool call, lookup) to improve the recipient's experience.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "message-id", + Required: true, + PathParam: "messageId", + }, + &requestflag.Flag[string]{ + Name: "zavu-sender", + HeaderPath: "Zavu-Sender", + }, + }, + Action: handleMessagesShowTyping, + HideHelpCommand: true, +} + func handleMessagesRetrieve(ctx context.Context, cmd *cli.Command) error { client := zavudev.NewClient(getDefaultRequestOptions(cmd)...) unusedArgs := cmd.Args().Slice() @@ -312,8 +417,15 @@ func handleMessagesRetrieve(ctx context.Context, cmd *cli.Command) error { obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "messages retrieve", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "messages retrieve", + Transform: transform, + }) } func handleMessagesList(ctx context.Context, cmd *cli.Command) error { @@ -324,8 +436,6 @@ func handleMessagesList(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := zavudev.MessageListParams{} - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -337,7 +447,10 @@ func handleMessagesList(ctx context.Context, cmd *cli.Command) error { return err } + params := zavudev.MessageListParams{} + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") if format == "raw" { var res []byte @@ -347,18 +460,30 @@ func handleMessagesList(ctx context.Context, cmd *cli.Command) error { return err } obj := gjson.ParseBytes(res) - return ShowJSON(os.Stdout, "messages list", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "messages list", + Transform: transform, + }) } else { iter := client.Messages.ListAutoPaging(ctx, params, options...) maxItems := int64(-1) if cmd.IsSet("max-items") { maxItems = cmd.Value("max-items").(int64) } - return ShowJSONIterator(os.Stdout, "messages list", iter, format, transform, maxItems) + return ShowJSONIterator(iter, maxItems, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "messages list", + Transform: transform, + }) } } -func handleMessagesReact(ctx context.Context, cmd *cli.Command) error { +func handleMessagesListAttachments(ctx context.Context, cmd *cli.Command) error { client := zavudev.NewClient(getDefaultRequestOptions(cmd)...) unusedArgs := cmd.Args().Slice() if !cmd.IsSet("message-id") && len(unusedArgs) > 0 { @@ -369,7 +494,47 @@ func handleMessagesReact(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := zavudev.MessageReactParams{} + 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.Messages.ListAttachments(ctx, cmd.Value("message-id").(string), options...) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") + transform := cmd.Root().String("transform") + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "messages list-attachments", + Transform: transform, + }) +} + +func handleMessagesReact(ctx context.Context, cmd *cli.Command) error { + client := zavudev.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("message-id") && len(unusedArgs) > 0 { + cmd.Set("message-id", unusedArgs[0]) + unusedArgs = unusedArgs[1:] + } + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } options, err := flagOptions( cmd, @@ -382,6 +547,8 @@ func handleMessagesReact(ctx context.Context, cmd *cli.Command) error { return err } + params := zavudev.MessageReactParams{} + var res []byte options = append(options, option.WithResponseBodyInto(&res)) _, err = client.Messages.React( @@ -396,8 +563,15 @@ func handleMessagesReact(ctx context.Context, cmd *cli.Command) error { obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "messages react", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "messages react", + Transform: transform, + }) } func handleMessagesSend(ctx context.Context, cmd *cli.Command) error { @@ -408,8 +582,6 @@ func handleMessagesSend(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := zavudev.MessageSendParams{} - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -421,6 +593,8 @@ func handleMessagesSend(ctx context.Context, cmd *cli.Command) error { return err } + params := zavudev.MessageSendParams{} + var res []byte options = append(options, option.WithResponseBodyInto(&res)) _, err = client.Messages.Send(ctx, params, options...) @@ -430,6 +604,62 @@ func handleMessagesSend(ctx context.Context, cmd *cli.Command) error { obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") + transform := cmd.Root().String("transform") + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "messages send", + Transform: transform, + }) +} + +func handleMessagesShowTyping(ctx context.Context, cmd *cli.Command) error { + client := zavudev.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("message-id") && len(unusedArgs) > 0 { + cmd.Set("message-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 + } + + params := zavudev.MessageShowTypingParams{} + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Messages.ShowTyping( + ctx, + cmd.Value("message-id").(string), + params, + options..., + ) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "messages send", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "messages show-typing", + Transform: transform, + }) } diff --git a/pkg/cmd/message_test.go b/pkg/cmd/message_test.go index cb21547..6fe513e 100644 --- a/pkg/cmd/message_test.go +++ b/pkg/cmd/message_test.go @@ -29,7 +29,7 @@ func TestMessagesList(t *testing.T) { "--api-key", "string", "messages", "list", "--max-items", "10", - "--channel", "auto", + "--channel", "sms", "--cursor", "cursor", "--limit", "100", "--status", "queued", @@ -38,6 +38,18 @@ func TestMessagesList(t *testing.T) { }) } +func TestMessagesListAttachments(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "messages", "list-attachments", + "--message-id", "messageId", + ) + }) +} + func TestMessagesReact(t *testing.T) { t.Skip("Mock server tests are disabled") t.Run("regular flags", func(t *testing.T) { @@ -74,7 +86,7 @@ func TestMessagesSend(t *testing.T) { "--to", "+56912345678", "--attachment", "{filename: invoice.pdf, content: content, content_id: logo, content_type: application/pdf, path: https://example.com}", "--channel", "auto", - "--content", "{buttons: [{id: id, title: title}], contacts: [{name: name, phones: [string]}], emoji: emoji, filename: invoice.pdf, latitude: 0, listButton: listButton, locationAddress: locationAddress, locationName: locationName, longitude: 0, mediaId: mediaId, mediaUrl: https://example.com/image.jpg, mimeType: image/jpeg, reactToMessageId: reactToMessageId, sections: [{rows: [{id: id, title: title, description: description}], title: title}], templateId: templateId, templateVariables: {'1': John, '2': ORD-12345}}", + "--content", "{buttons: [{id: id, title: title}], contacts: [{name: name, phones: [string]}], ctaDisplayText: See Dates, ctaHeaderMediaUrl: https://example.com, ctaHeaderText: ctaHeaderText, ctaHeaderType: text, ctaUrl: https://example.com/schedule, emoji: emoji, filename: invoice.pdf, footerText: Dates subject to change., latitude: 0, listButton: listButton, locationAddress: locationAddress, locationName: locationName, longitude: 0, mediaId: mediaId, mediaUrl: https://example.com/image.jpg, mimeType: image/jpeg, reactToMessageId: reactToMessageId, referral: {body: body, ctwaClid: ARIzZm9vYmFyY3R3YWNsaWQ, headline: headline, imageUrl: https://example.com, mediaType: image, sourceId: '120210000000000000', sourceType: ad, sourceUrl: https://example.com, thumbnailUrl: https://example.com, videoUrl: https://example.com}, replyToFrom: replyToFrom, replyToMessageId: replyToMessageId, replyToMessageType: replyToMessageType, replyToProviderMessageId: replyToProviderMessageId, replyToText: replyToText, sections: [{rows: [{id: id, title: title, description: description}], title: title}], templateButtonVariables: {'0': abc-report-token}, templateHeaderVariables: {'1': Jorge y Laura}, templateId: templateId, templateVariables: {'1': John, '2': ORD-12345}}", "--fallback-enabled=true", "--html-body", "htmlBody", "--idempotency-key", "msg_01HZY4ZP7VQY2J3BRW7Z6G0QGE", @@ -106,8 +118,14 @@ func TestMessagesSend(t *testing.T) { "--channel", "auto", "--content.buttons", "[{id: id, title: title}]", "--content.contacts", "[{name: name, phones: [string]}]", + "--content.cta-display-text", "See Dates", + "--content.cta-header-media-url", "https://example.com", + "--content.cta-header-text", "ctaHeaderText", + "--content.cta-header-type", "text", + "--content.cta-url", "https://example.com/schedule", "--content.emoji", "emoji", "--content.filename", "invoice.pdf", + "--content.footer-text", "Dates subject to change.", "--content.latitude", "0", "--content.list-button", "listButton", "--content.location-address", "locationAddress", @@ -117,7 +135,15 @@ func TestMessagesSend(t *testing.T) { "--content.media-url", "https://example.com/image.jpg", "--content.mime-type", "image/jpeg", "--content.react-to-message-id", "reactToMessageId", + "--content.referral", "{body: body, ctwaClid: ARIzZm9vYmFyY3R3YWNsaWQ, headline: headline, imageUrl: https://example.com, mediaType: image, sourceId: '120210000000000000', sourceType: ad, sourceUrl: https://example.com, thumbnailUrl: https://example.com, videoUrl: https://example.com}", + "--content.reply-to-from", "replyToFrom", + "--content.reply-to-message-id", "replyToMessageId", + "--content.reply-to-message-type", "replyToMessageType", + "--content.reply-to-provider-message-id", "replyToProviderMessageId", + "--content.reply-to-text", "replyToText", "--content.sections", "[{rows: [{id: id, title: title, description: description}], title: title}]", + "--content.template-button-variables", "{'0': abc-report-token}", + "--content.template-header-variables", "{'1': Jorge y Laura}", "--content.template-id", "templateId", "--content.template-variables", "{'1': John, '2': ORD-12345}", "--fallback-enabled=true", @@ -152,8 +178,14 @@ func TestMessagesSend(t *testing.T) { " - name: name\n" + " phones:\n" + " - string\n" + + " ctaDisplayText: See Dates\n" + + " ctaHeaderMediaUrl: https://example.com\n" + + " ctaHeaderText: ctaHeaderText\n" + + " ctaHeaderType: text\n" + + " ctaUrl: https://example.com/schedule\n" + " emoji: emoji\n" + " filename: invoice.pdf\n" + + " footerText: Dates subject to change.\n" + " latitude: 0\n" + " listButton: listButton\n" + " locationAddress: locationAddress\n" + @@ -163,12 +195,32 @@ func TestMessagesSend(t *testing.T) { " mediaUrl: https://example.com/image.jpg\n" + " mimeType: image/jpeg\n" + " reactToMessageId: reactToMessageId\n" + + " referral:\n" + + " body: body\n" + + " ctwaClid: ARIzZm9vYmFyY3R3YWNsaWQ\n" + + " headline: headline\n" + + " imageUrl: https://example.com\n" + + " mediaType: image\n" + + " sourceId: '120210000000000000'\n" + + " sourceType: ad\n" + + " sourceUrl: https://example.com\n" + + " thumbnailUrl: https://example.com\n" + + " videoUrl: https://example.com\n" + + " replyToFrom: replyToFrom\n" + + " replyToMessageId: replyToMessageId\n" + + " replyToMessageType: replyToMessageType\n" + + " replyToProviderMessageId: replyToProviderMessageId\n" + + " replyToText: replyToText\n" + " sections:\n" + " - rows:\n" + " - id: id\n" + " title: title\n" + " description: description\n" + " title: title\n" + + " templateButtonVariables:\n" + + " '0': abc-report-token\n" + + " templateHeaderVariables:\n" + + " '1': Jorge y Laura\n" + " templateId: templateId\n" + " templateVariables:\n" + " '1': John\n" + @@ -191,3 +243,16 @@ func TestMessagesSend(t *testing.T) { ) }) } + +func TestMessagesShowTyping(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "messages", "show-typing", + "--message-id", "messageId", + "--zavu-sender", "sender_12345", + ) + }) +} diff --git a/pkg/cmd/number10dlcbrand.go b/pkg/cmd/number10dlcbrand.go new file mode 100644 index 0000000..e521dd8 --- /dev/null +++ b/pkg/cmd/number10dlcbrand.go @@ -0,0 +1,621 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "context" + "fmt" + + "github.com/tidwall/gjson" + "github.com/urfave/cli/v3" + "github.com/zavudev/cli/internal/apiquery" + "github.com/zavudev/cli/internal/requestflag" + "github.com/zavudev/sdk-go" + "github.com/zavudev/sdk-go/option" +) + +var number10dlcBrandsCreate = cli.Command{ + Name: "create", + Usage: "Create a 10DLC brand registration. The brand starts in draft status. Submit it\nfor review using the submit endpoint.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "city", + Required: true, + BodyPath: "city", + }, + &requestflag.Flag[string]{ + Name: "country", + Usage: "Two-letter ISO country code.", + Required: true, + BodyPath: "country", + }, + &requestflag.Flag[string]{ + Name: "display-name", + Usage: "Display name of the brand.", + Required: true, + BodyPath: "displayName", + }, + &requestflag.Flag[string]{ + Name: "email", + Required: true, + BodyPath: "email", + }, + &requestflag.Flag[string]{ + Name: "entity-type", + Usage: "Business entity type for 10DLC brand registration.", + Required: true, + BodyPath: "entityType", + }, + &requestflag.Flag[string]{ + Name: "phone", + Usage: "Contact phone in E.164 format.", + Required: true, + BodyPath: "phone", + }, + &requestflag.Flag[string]{ + Name: "postal-code", + Required: true, + BodyPath: "postalCode", + }, + &requestflag.Flag[string]{ + Name: "state", + Required: true, + BodyPath: "state", + }, + &requestflag.Flag[string]{ + Name: "street", + Required: true, + BodyPath: "street", + }, + &requestflag.Flag[string]{ + Name: "vertical", + Usage: "Industry vertical.", + Required: true, + BodyPath: "vertical", + }, + &requestflag.Flag[string]{ + Name: "company-name", + Usage: "Legal company name.", + BodyPath: "companyName", + }, + &requestflag.Flag[string]{ + Name: "ein", + Usage: "Employer Identification Number (format: XX-XXXXXXX).", + BodyPath: "ein", + }, + &requestflag.Flag[string]{ + Name: "first-name", + BodyPath: "firstName", + }, + &requestflag.Flag[string]{ + Name: "last-name", + BodyPath: "lastName", + }, + &requestflag.Flag[string]{ + Name: "stock-exchange", + BodyPath: "stockExchange", + }, + &requestflag.Flag[string]{ + Name: "stock-symbol", + BodyPath: "stockSymbol", + }, + &requestflag.Flag[string]{ + Name: "website", + BodyPath: "website", + }, + }, + Action: handleNumber10dlcBrandsCreate, + HideHelpCommand: true, +} + +var number10dlcBrandsRetrieve = cli.Command{ + Name: "retrieve", + Usage: "Get 10DLC brand", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "brand-id", + Required: true, + PathParam: "brandId", + }, + }, + Action: handleNumber10dlcBrandsRetrieve, + HideHelpCommand: true, +} + +var number10dlcBrandsUpdate = cli.Command{ + Name: "update", + Usage: "Update a 10DLC brand in draft status. Cannot update after submission.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "brand-id", + Required: true, + PathParam: "brandId", + }, + &requestflag.Flag[string]{ + Name: "city", + BodyPath: "city", + }, + &requestflag.Flag[string]{ + Name: "company-name", + BodyPath: "companyName", + }, + &requestflag.Flag[string]{ + Name: "country", + BodyPath: "country", + }, + &requestflag.Flag[string]{ + Name: "display-name", + BodyPath: "displayName", + }, + &requestflag.Flag[string]{ + Name: "ein", + BodyPath: "ein", + }, + &requestflag.Flag[string]{ + Name: "email", + BodyPath: "email", + }, + &requestflag.Flag[string]{ + Name: "entity-type", + Usage: "Business entity type for 10DLC brand registration.", + BodyPath: "entityType", + }, + &requestflag.Flag[string]{ + Name: "first-name", + BodyPath: "firstName", + }, + &requestflag.Flag[string]{ + Name: "last-name", + BodyPath: "lastName", + }, + &requestflag.Flag[string]{ + Name: "phone", + BodyPath: "phone", + }, + &requestflag.Flag[string]{ + Name: "postal-code", + BodyPath: "postalCode", + }, + &requestflag.Flag[string]{ + Name: "state", + BodyPath: "state", + }, + &requestflag.Flag[string]{ + Name: "stock-exchange", + BodyPath: "stockExchange", + }, + &requestflag.Flag[string]{ + Name: "stock-symbol", + BodyPath: "stockSymbol", + }, + &requestflag.Flag[string]{ + Name: "street", + BodyPath: "street", + }, + &requestflag.Flag[string]{ + Name: "vertical", + BodyPath: "vertical", + }, + &requestflag.Flag[string]{ + Name: "website", + BodyPath: "website", + }, + }, + Action: handleNumber10dlcBrandsUpdate, + HideHelpCommand: true, +} + +var number10dlcBrandsList = cli.Command{ + Name: "list", + Usage: "List 10DLC brand registrations for this project.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "cursor", + QueryPath: "cursor", + }, + &requestflag.Flag[int64]{ + Name: "limit", + Default: 50, + QueryPath: "limit", + }, + &requestflag.Flag[int64]{ + Name: "max-items", + Usage: "The maximum number of items to return (use -1 for unlimited).", + }, + }, + Action: handleNumber10dlcBrandsList, + HideHelpCommand: true, +} + +var number10dlcBrandsDelete = cli.Command{ + Name: "delete", + Usage: "Delete 10DLC brand", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "brand-id", + Required: true, + PathParam: "brandId", + }, + }, + Action: handleNumber10dlcBrandsDelete, + HideHelpCommand: true, +} + +var number10dlcBrandsListUseCases = cli.Command{ + Name: "list-use-cases", + Usage: "List available use cases for 10DLC campaign registration.", + Suggest: true, + Flags: []cli.Flag{}, + Action: handleNumber10dlcBrandsListUseCases, + HideHelpCommand: true, +} + +var number10dlcBrandsSubmit = cli.Command{ + Name: "submit", + Usage: "Submit a draft brand to The Campaign Registry (TCR) for vetting. The brand must\nbe in draft status, and the team must have an approved Business Verification\n(KYB) — carriers register a brand against a vetted legal entity, so submitting\nwithout one returns `403` with code `kyb_required`.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "brand-id", + Required: true, + PathParam: "brandId", + }, + }, + Action: handleNumber10dlcBrandsSubmit, + HideHelpCommand: true, +} + +var number10dlcBrandsSyncStatus = cli.Command{ + Name: "sync-status", + Usage: "Sync the brand status with the registration provider. Use this to check for\napproval updates after submission.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "brand-id", + Required: true, + PathParam: "brandId", + }, + }, + Action: handleNumber10dlcBrandsSyncStatus, + HideHelpCommand: true, +} + +func handleNumber10dlcBrandsCreate(ctx context.Context, cmd *cli.Command) error { + client := zavudev.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + + options, err := flagOptions( + cmd, + apiquery.NestedQueryFormatBrackets, + apiquery.ArrayQueryFormatComma, + ApplicationJSON, + false, + ) + if err != nil { + return err + } + + params := zavudev.Number10dlcBrandNewParams{} + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Number10dlc.Brands.New(ctx, params, options...) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") + transform := cmd.Root().String("transform") + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "number-10dlc:brands create", + Transform: transform, + }) +} + +func handleNumber10dlcBrandsRetrieve(ctx context.Context, cmd *cli.Command) error { + client := zavudev.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("brand-id") && len(unusedArgs) > 0 { + cmd.Set("brand-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 + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Number10dlc.Brands.Get(ctx, cmd.Value("brand-id").(string), options...) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") + transform := cmd.Root().String("transform") + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "number-10dlc:brands retrieve", + Transform: transform, + }) +} + +func handleNumber10dlcBrandsUpdate(ctx context.Context, cmd *cli.Command) error { + client := zavudev.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("brand-id") && len(unusedArgs) > 0 { + cmd.Set("brand-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, + ApplicationJSON, + false, + ) + if err != nil { + return err + } + + params := zavudev.Number10dlcBrandUpdateParams{} + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Number10dlc.Brands.Update( + ctx, + cmd.Value("brand-id").(string), + params, + options..., + ) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") + transform := cmd.Root().String("transform") + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "number-10dlc:brands update", + Transform: transform, + }) +} + +func handleNumber10dlcBrandsList(ctx context.Context, cmd *cli.Command) error { + client := zavudev.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + + 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 + } + + params := zavudev.Number10dlcBrandListParams{} + + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") + transform := cmd.Root().String("transform") + if format == "raw" { + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Number10dlc.Brands.List(ctx, params, options...) + if err != nil { + return err + } + obj := gjson.ParseBytes(res) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "number-10dlc:brands list", + Transform: transform, + }) + } else { + iter := client.Number10dlc.Brands.ListAutoPaging(ctx, params, options...) + maxItems := int64(-1) + if cmd.IsSet("max-items") { + maxItems = cmd.Value("max-items").(int64) + } + return ShowJSONIterator(iter, maxItems, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "number-10dlc:brands list", + Transform: transform, + }) + } +} + +func handleNumber10dlcBrandsDelete(ctx context.Context, cmd *cli.Command) error { + client := zavudev.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("brand-id") && len(unusedArgs) > 0 { + cmd.Set("brand-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.Number10dlc.Brands.Delete(ctx, cmd.Value("brand-id").(string), options...) +} + +func handleNumber10dlcBrandsListUseCases(ctx context.Context, cmd *cli.Command) error { + client := zavudev.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + + 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 + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Number10dlc.Brands.ListUseCases(ctx, options...) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") + transform := cmd.Root().String("transform") + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "number-10dlc:brands list-use-cases", + Transform: transform, + }) +} + +func handleNumber10dlcBrandsSubmit(ctx context.Context, cmd *cli.Command) error { + client := zavudev.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("brand-id") && len(unusedArgs) > 0 { + cmd.Set("brand-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 + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Number10dlc.Brands.Submit(ctx, cmd.Value("brand-id").(string), options...) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") + transform := cmd.Root().String("transform") + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "number-10dlc:brands submit", + Transform: transform, + }) +} + +func handleNumber10dlcBrandsSyncStatus(ctx context.Context, cmd *cli.Command) error { + client := zavudev.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("brand-id") && len(unusedArgs) > 0 { + cmd.Set("brand-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 + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Number10dlc.Brands.SyncStatus(ctx, cmd.Value("brand-id").(string), options...) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") + transform := cmd.Root().String("transform") + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "number-10dlc:brands sync-status", + Transform: transform, + }) +} diff --git a/pkg/cmd/number10dlcbrand_test.go b/pkg/cmd/number10dlcbrand_test.go new file mode 100644 index 0000000..48c5f53 --- /dev/null +++ b/pkg/cmd/number10dlcbrand_test.go @@ -0,0 +1,194 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "testing" + + "github.com/zavudev/cli/internal/mocktest" +) + +func TestNumber10dlcBrandsCreate(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "number-10dlc:brands", "create", + "--city", "San Francisco", + "--country", "US", + "--display-name", "Acme Corp", + "--email", "compliance@acme.com", + "--entity-type", "PRIVATE_PROFIT", + "--phone", "+14155551234", + "--postal-code", "94102", + "--state", "CA", + "--street", "123 Main St", + "--vertical", "Technology", + "--company-name", "Acme Corporation", + "--ein", "12-3456789", + "--first-name", "firstName", + "--last-name", "lastName", + "--stock-exchange", "stockExchange", + "--stock-symbol", "stockSymbol", + "--website", "https://acme.com", + ) + }) + + t.Run("piping data", func(t *testing.T) { + // Test piping YAML data over stdin + pipeData := []byte("" + + "city: San Francisco\n" + + "country: US\n" + + "displayName: Acme Corp\n" + + "email: compliance@acme.com\n" + + "entityType: PRIVATE_PROFIT\n" + + "phone: '+14155551234'\n" + + "postalCode: '94102'\n" + + "state: CA\n" + + "street: 123 Main St\n" + + "vertical: Technology\n" + + "companyName: Acme Corporation\n" + + "ein: 12-3456789\n" + + "firstName: firstName\n" + + "lastName: lastName\n" + + "stockExchange: stockExchange\n" + + "stockSymbol: stockSymbol\n" + + "website: https://acme.com\n") + mocktest.TestRunMockTestWithPipeAndFlags( + t, pipeData, + "--api-key", "string", + "number-10dlc:brands", "create", + ) + }) +} + +func TestNumber10dlcBrandsRetrieve(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "number-10dlc:brands", "retrieve", + "--brand-id", "brandId", + ) + }) +} + +func TestNumber10dlcBrandsUpdate(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "number-10dlc:brands", "update", + "--brand-id", "brandId", + "--city", "city", + "--company-name", "companyName", + "--country", "xx", + "--display-name", "displayName", + "--ein", "ein", + "--email", "dev@stainless.com", + "--entity-type", "PRIVATE_PROFIT", + "--first-name", "firstName", + "--last-name", "lastName", + "--phone", "phone", + "--postal-code", "postalCode", + "--state", "state", + "--stock-exchange", "stockExchange", + "--stock-symbol", "stockSymbol", + "--street", "street", + "--vertical", "vertical", + "--website", "https://example.com", + ) + }) + + t.Run("piping data", func(t *testing.T) { + // Test piping YAML data over stdin + pipeData := []byte("" + + "city: city\n" + + "companyName: companyName\n" + + "country: xx\n" + + "displayName: displayName\n" + + "ein: ein\n" + + "email: dev@stainless.com\n" + + "entityType: PRIVATE_PROFIT\n" + + "firstName: firstName\n" + + "lastName: lastName\n" + + "phone: phone\n" + + "postalCode: postalCode\n" + + "state: state\n" + + "stockExchange: stockExchange\n" + + "stockSymbol: stockSymbol\n" + + "street: street\n" + + "vertical: vertical\n" + + "website: https://example.com\n") + mocktest.TestRunMockTestWithPipeAndFlags( + t, pipeData, + "--api-key", "string", + "number-10dlc:brands", "update", + "--brand-id", "brandId", + ) + }) +} + +func TestNumber10dlcBrandsList(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "number-10dlc:brands", "list", + "--max-items", "10", + "--cursor", "cursor", + "--limit", "100", + ) + }) +} + +func TestNumber10dlcBrandsDelete(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "number-10dlc:brands", "delete", + "--brand-id", "brandId", + ) + }) +} + +func TestNumber10dlcBrandsListUseCases(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "number-10dlc:brands", "list-use-cases", + ) + }) +} + +func TestNumber10dlcBrandsSubmit(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "number-10dlc:brands", "submit", + "--brand-id", "brandId", + ) + }) +} + +func TestNumber10dlcBrandsSyncStatus(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "number-10dlc:brands", "sync-status", + "--brand-id", "brandId", + ) + }) +} diff --git a/pkg/cmd/number10dlccampaign.go b/pkg/cmd/number10dlccampaign.go new file mode 100644 index 0000000..2318997 --- /dev/null +++ b/pkg/cmd/number10dlccampaign.go @@ -0,0 +1,544 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "context" + "fmt" + + "github.com/tidwall/gjson" + "github.com/urfave/cli/v3" + "github.com/zavudev/cli/internal/apiquery" + "github.com/zavudev/cli/internal/requestflag" + "github.com/zavudev/sdk-go" + "github.com/zavudev/sdk-go/option" +) + +var number10dlcCampaignsCreate = cli.Command{ + Name: "create", + Usage: "Create a 10DLC campaign under an existing brand. The campaign starts in draft\nstatus. Submit it for carrier review using the submit endpoint.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[bool]{ + Name: "affiliate-marketing", + Required: true, + BodyPath: "affiliateMarketing", + }, + &requestflag.Flag[bool]{ + Name: "age-gated", + Required: true, + BodyPath: "ageGated", + }, + &requestflag.Flag[string]{ + Name: "brand-id", + Usage: "ID of the brand to create this campaign under.", + Required: true, + BodyPath: "brandId", + }, + &requestflag.Flag[string]{ + Name: "description", + Required: true, + BodyPath: "description", + }, + &requestflag.Flag[bool]{ + Name: "direct-lending", + Required: true, + BodyPath: "directLending", + }, + &requestflag.Flag[bool]{ + Name: "embedded-link", + Required: true, + BodyPath: "embeddedLink", + }, + &requestflag.Flag[bool]{ + Name: "embedded-phone", + Required: true, + BodyPath: "embeddedPhone", + }, + &requestflag.Flag[string]{ + Name: "name", + Required: true, + BodyPath: "name", + }, + &requestflag.Flag[bool]{ + Name: "number-pooling", + Required: true, + BodyPath: "numberPooling", + }, + &requestflag.Flag[[]string]{ + Name: "sample-message", + Required: true, + BodyPath: "sampleMessages", + }, + &requestflag.Flag[bool]{ + Name: "subscriber-help", + Required: true, + BodyPath: "subscriberHelp", + }, + &requestflag.Flag[bool]{ + Name: "subscriber-opt-in", + Required: true, + BodyPath: "subscriberOptIn", + }, + &requestflag.Flag[bool]{ + Name: "subscriber-opt-out", + Required: true, + BodyPath: "subscriberOptOut", + }, + &requestflag.Flag[string]{ + Name: "use-case", + Usage: "Campaign use case (e.g., ACCOUNT_NOTIFICATION, MARKETING, 2FA).", + Required: true, + BodyPath: "useCase", + }, + &requestflag.Flag[string]{ + Name: "help-message", + BodyPath: "helpMessage", + }, + &requestflag.Flag[string]{ + Name: "message-flow", + BodyPath: "messageFlow", + }, + &requestflag.Flag[[]string]{ + Name: "opt-in-keyword", + BodyPath: "optInKeywords", + }, + &requestflag.Flag[[]string]{ + Name: "opt-out-keyword", + BodyPath: "optOutKeywords", + }, + &requestflag.Flag[[]string]{ + Name: "sub-use-case", + BodyPath: "subUseCases", + }, + }, + Action: handleNumber10dlcCampaignsCreate, + HideHelpCommand: true, +} + +var number10dlcCampaignsRetrieve = cli.Command{ + Name: "retrieve", + Usage: "Get 10DLC campaign", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "campaign-id", + Required: true, + PathParam: "campaignId", + }, + }, + Action: handleNumber10dlcCampaignsRetrieve, + HideHelpCommand: true, +} + +var number10dlcCampaignsUpdate = cli.Command{ + Name: "update", + Usage: "Update a 10DLC campaign in draft status. Cannot update after submission.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "campaign-id", + Required: true, + PathParam: "campaignId", + }, + &requestflag.Flag[string]{ + Name: "description", + BodyPath: "description", + }, + &requestflag.Flag[string]{ + Name: "help-message", + BodyPath: "helpMessage", + }, + &requestflag.Flag[string]{ + Name: "message-flow", + BodyPath: "messageFlow", + }, + &requestflag.Flag[string]{ + Name: "name", + BodyPath: "name", + }, + &requestflag.Flag[[]string]{ + Name: "opt-in-keyword", + BodyPath: "optInKeywords", + }, + &requestflag.Flag[[]string]{ + Name: "opt-out-keyword", + BodyPath: "optOutKeywords", + }, + &requestflag.Flag[[]string]{ + Name: "sample-message", + BodyPath: "sampleMessages", + }, + }, + Action: handleNumber10dlcCampaignsUpdate, + HideHelpCommand: true, +} + +var number10dlcCampaignsList = cli.Command{ + Name: "list", + Usage: "List 10DLC campaign registrations for this project.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "brand-id", + Usage: "Filter campaigns by brand ID.", + QueryPath: "brandId", + }, + &requestflag.Flag[string]{ + Name: "cursor", + QueryPath: "cursor", + }, + &requestflag.Flag[int64]{ + Name: "limit", + Default: 50, + QueryPath: "limit", + }, + &requestflag.Flag[int64]{ + Name: "max-items", + Usage: "The maximum number of items to return (use -1 for unlimited).", + }, + }, + Action: handleNumber10dlcCampaignsList, + HideHelpCommand: true, +} + +var number10dlcCampaignsDelete = cli.Command{ + Name: "delete", + Usage: "Delete 10DLC campaign", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "campaign-id", + Required: true, + PathParam: "campaignId", + }, + }, + Action: handleNumber10dlcCampaignsDelete, + HideHelpCommand: true, +} + +var number10dlcCampaignsSubmit = cli.Command{ + Name: "submit", + Usage: "The team must have an approved Business Verification (KYB): submitting without\none returns `403` with code `kyb_required`. Submit a draft campaign for carrier\nreview. The campaign must be in draft status and its brand must be verified.\nTCR's one-time registration fee is charged from your balance at submission ($15\nfor standard use cases, $2 for LOW_VOLUME), passed through at cost and refunded\nif the carrier rejects it. Once approved, the campaign's monthly TCR fee ($10\nstandard, $2 LOW_VOLUME) is charged from your balance while the campaign is\nactive — see registrationCostCents and monthlyFeeCents on the campaign object.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "campaign-id", + Required: true, + PathParam: "campaignId", + }, + }, + Action: handleNumber10dlcCampaignsSubmit, + HideHelpCommand: true, +} + +var number10dlcCampaignsSyncStatus = cli.Command{ + Name: "sync-status", + Usage: "Sync the campaign status with the registration provider. Use this to check for\napproval updates after submission.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "campaign-id", + Required: true, + PathParam: "campaignId", + }, + }, + Action: handleNumber10dlcCampaignsSyncStatus, + HideHelpCommand: true, +} + +func handleNumber10dlcCampaignsCreate(ctx context.Context, cmd *cli.Command) error { + client := zavudev.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + + options, err := flagOptions( + cmd, + apiquery.NestedQueryFormatBrackets, + apiquery.ArrayQueryFormatComma, + ApplicationJSON, + false, + ) + if err != nil { + return err + } + + params := zavudev.Number10dlcCampaignNewParams{} + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Number10dlc.Campaigns.New(ctx, params, options...) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") + transform := cmd.Root().String("transform") + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "number-10dlc:campaigns create", + Transform: transform, + }) +} + +func handleNumber10dlcCampaignsRetrieve(ctx context.Context, cmd *cli.Command) error { + client := zavudev.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("campaign-id") && len(unusedArgs) > 0 { + cmd.Set("campaign-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 + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Number10dlc.Campaigns.Get(ctx, cmd.Value("campaign-id").(string), options...) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") + transform := cmd.Root().String("transform") + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "number-10dlc:campaigns retrieve", + Transform: transform, + }) +} + +func handleNumber10dlcCampaignsUpdate(ctx context.Context, cmd *cli.Command) error { + client := zavudev.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("campaign-id") && len(unusedArgs) > 0 { + cmd.Set("campaign-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, + ApplicationJSON, + false, + ) + if err != nil { + return err + } + + params := zavudev.Number10dlcCampaignUpdateParams{} + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Number10dlc.Campaigns.Update( + ctx, + cmd.Value("campaign-id").(string), + params, + options..., + ) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") + transform := cmd.Root().String("transform") + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "number-10dlc:campaigns update", + Transform: transform, + }) +} + +func handleNumber10dlcCampaignsList(ctx context.Context, cmd *cli.Command) error { + client := zavudev.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + + 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 + } + + params := zavudev.Number10dlcCampaignListParams{} + + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") + transform := cmd.Root().String("transform") + if format == "raw" { + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Number10dlc.Campaigns.List(ctx, params, options...) + if err != nil { + return err + } + obj := gjson.ParseBytes(res) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "number-10dlc:campaigns list", + Transform: transform, + }) + } else { + iter := client.Number10dlc.Campaigns.ListAutoPaging(ctx, params, options...) + maxItems := int64(-1) + if cmd.IsSet("max-items") { + maxItems = cmd.Value("max-items").(int64) + } + return ShowJSONIterator(iter, maxItems, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "number-10dlc:campaigns list", + Transform: transform, + }) + } +} + +func handleNumber10dlcCampaignsDelete(ctx context.Context, cmd *cli.Command) error { + client := zavudev.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("campaign-id") && len(unusedArgs) > 0 { + cmd.Set("campaign-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.Number10dlc.Campaigns.Delete(ctx, cmd.Value("campaign-id").(string), options...) +} + +func handleNumber10dlcCampaignsSubmit(ctx context.Context, cmd *cli.Command) error { + client := zavudev.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("campaign-id") && len(unusedArgs) > 0 { + cmd.Set("campaign-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 + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Number10dlc.Campaigns.Submit(ctx, cmd.Value("campaign-id").(string), options...) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") + transform := cmd.Root().String("transform") + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "number-10dlc:campaigns submit", + Transform: transform, + }) +} + +func handleNumber10dlcCampaignsSyncStatus(ctx context.Context, cmd *cli.Command) error { + client := zavudev.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("campaign-id") && len(unusedArgs) > 0 { + cmd.Set("campaign-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 + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Number10dlc.Campaigns.SyncStatus(ctx, cmd.Value("campaign-id").(string), options...) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") + transform := cmd.Root().String("transform") + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "number-10dlc:campaigns sync-status", + Transform: transform, + }) +} diff --git a/pkg/cmd/number10dlccampaign_test.go b/pkg/cmd/number10dlccampaign_test.go new file mode 100644 index 0000000..39fd758 --- /dev/null +++ b/pkg/cmd/number10dlccampaign_test.go @@ -0,0 +1,179 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "testing" + + "github.com/zavudev/cli/internal/mocktest" +) + +func TestNumber10dlcCampaignsCreate(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "number-10dlc:campaigns", "create", + "--affiliate-marketing=false", + "--age-gated=false", + "--brand-id", "brand_abc123", + "--description", "Send order status updates and shipping notifications to customers who opted in.", + "--direct-lending=false", + "--embedded-link=true", + "--embedded-phone=false", + "--name", "Order Notifications", + "--number-pooling=false", + "--sample-message", "Hi {{name}}, your order #{{order_id}} has shipped! Track it at {{url}}", + "--sample-message", "Your order #{{order_id}} has been delivered. Thank you for your purchase!", + "--subscriber-help=true", + "--subscriber-opt-in=true", + "--subscriber-opt-out=true", + "--use-case", "ACCOUNT_NOTIFICATION", + "--help-message", "helpMessage", + "--message-flow", "messageFlow", + "--opt-in-keyword", "string", + "--opt-out-keyword", "string", + "--sub-use-case", "string", + ) + }) + + t.Run("piping data", func(t *testing.T) { + // Test piping YAML data over stdin + pipeData := []byte("" + + "affiliateMarketing: false\n" + + "ageGated: false\n" + + "brandId: brand_abc123\n" + + "description: >-\n" + + " Send order status updates and shipping notifications to customers who opted\n" + + " in.\n" + + "directLending: false\n" + + "embeddedLink: true\n" + + "embeddedPhone: false\n" + + "name: Order Notifications\n" + + "numberPooling: false\n" + + "sampleMessages:\n" + + " - 'Hi {{name}}, your order #{{order_id}} has shipped! Track it at {{url}}'\n" + + " - 'Your order #{{order_id}} has been delivered. Thank you for your purchase!'\n" + + "subscriberHelp: true\n" + + "subscriberOptIn: true\n" + + "subscriberOptOut: true\n" + + "useCase: ACCOUNT_NOTIFICATION\n" + + "helpMessage: helpMessage\n" + + "messageFlow: messageFlow\n" + + "optInKeywords:\n" + + " - string\n" + + "optOutKeywords:\n" + + " - string\n" + + "subUseCases:\n" + + " - string\n") + mocktest.TestRunMockTestWithPipeAndFlags( + t, pipeData, + "--api-key", "string", + "number-10dlc:campaigns", "create", + ) + }) +} + +func TestNumber10dlcCampaignsRetrieve(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "number-10dlc:campaigns", "retrieve", + "--campaign-id", "campaignId", + ) + }) +} + +func TestNumber10dlcCampaignsUpdate(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "number-10dlc:campaigns", "update", + "--campaign-id", "campaignId", + "--description", "description", + "--help-message", "helpMessage", + "--message-flow", "messageFlow", + "--name", "name", + "--opt-in-keyword", "string", + "--opt-out-keyword", "string", + "--sample-message", "string", + ) + }) + + t.Run("piping data", func(t *testing.T) { + // Test piping YAML data over stdin + pipeData := []byte("" + + "description: description\n" + + "helpMessage: helpMessage\n" + + "messageFlow: messageFlow\n" + + "name: name\n" + + "optInKeywords:\n" + + " - string\n" + + "optOutKeywords:\n" + + " - string\n" + + "sampleMessages:\n" + + " - string\n") + mocktest.TestRunMockTestWithPipeAndFlags( + t, pipeData, + "--api-key", "string", + "number-10dlc:campaigns", "update", + "--campaign-id", "campaignId", + ) + }) +} + +func TestNumber10dlcCampaignsList(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "number-10dlc:campaigns", "list", + "--max-items", "10", + "--brand-id", "brandId", + "--cursor", "cursor", + "--limit", "100", + ) + }) +} + +func TestNumber10dlcCampaignsDelete(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "number-10dlc:campaigns", "delete", + "--campaign-id", "campaignId", + ) + }) +} + +func TestNumber10dlcCampaignsSubmit(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "number-10dlc:campaigns", "submit", + "--campaign-id", "campaignId", + ) + }) +} + +func TestNumber10dlcCampaignsSyncStatus(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "number-10dlc:campaigns", "sync-status", + "--campaign-id", "campaignId", + ) + }) +} diff --git a/pkg/cmd/number10dlccampaignphonenumber.go b/pkg/cmd/number10dlccampaignphonenumber.go new file mode 100644 index 0000000..b70daeb --- /dev/null +++ b/pkg/cmd/number10dlccampaignphonenumber.go @@ -0,0 +1,196 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "context" + "fmt" + + "github.com/tidwall/gjson" + "github.com/urfave/cli/v3" + "github.com/zavudev/cli/internal/apiquery" + "github.com/zavudev/cli/internal/requestflag" + "github.com/zavudev/sdk-go" + "github.com/zavudev/sdk-go/option" +) + +var number10dlcCampaignsPhoneNumbersList = cli.Command{ + Name: "list", + Usage: "List phone numbers assigned to a 10DLC campaign.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "campaign-id", + Required: true, + PathParam: "campaignId", + }, + }, + Action: handleNumber10dlcCampaignsPhoneNumbersList, + HideHelpCommand: true, +} + +var number10dlcCampaignsPhoneNumbersAssign = cli.Command{ + Name: "assign", + Usage: "Assign a US phone number to an approved 10DLC campaign. The campaign must be in\napproved status.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "campaign-id", + Required: true, + PathParam: "campaignId", + }, + &requestflag.Flag[string]{ + Name: "phone-number-id", + Usage: "ID of the phone number to assign.", + Required: true, + BodyPath: "phoneNumberId", + }, + }, + Action: handleNumber10dlcCampaignsPhoneNumbersAssign, + HideHelpCommand: true, +} + +var number10dlcCampaignsPhoneNumbersUnassign = cli.Command{ + Name: "unassign", + Usage: "Remove a phone number assignment from a 10DLC campaign.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "campaign-id", + Required: true, + PathParam: "campaignId", + }, + &requestflag.Flag[string]{ + Name: "assignment-id", + Required: true, + PathParam: "assignmentId", + }, + }, + Action: handleNumber10dlcCampaignsPhoneNumbersUnassign, + HideHelpCommand: true, +} + +func handleNumber10dlcCampaignsPhoneNumbersList(ctx context.Context, cmd *cli.Command) error { + client := zavudev.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("campaign-id") && len(unusedArgs) > 0 { + cmd.Set("campaign-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 + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Number10dlc.Campaigns.PhoneNumbers.List(ctx, cmd.Value("campaign-id").(string), options...) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") + transform := cmd.Root().String("transform") + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "number-10dlc:campaigns:phone-numbers list", + Transform: transform, + }) +} + +func handleNumber10dlcCampaignsPhoneNumbersAssign(ctx context.Context, cmd *cli.Command) error { + client := zavudev.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("campaign-id") && len(unusedArgs) > 0 { + cmd.Set("campaign-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, + ApplicationJSON, + false, + ) + if err != nil { + return err + } + + params := zavudev.Number10dlcCampaignPhoneNumberAssignParams{} + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Number10dlc.Campaigns.PhoneNumbers.Assign( + ctx, + cmd.Value("campaign-id").(string), + params, + options..., + ) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") + transform := cmd.Root().String("transform") + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "number-10dlc:campaigns:phone-numbers assign", + Transform: transform, + }) +} + +func handleNumber10dlcCampaignsPhoneNumbersUnassign(ctx context.Context, cmd *cli.Command) error { + client := zavudev.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("assignment-id") && len(unusedArgs) > 0 { + cmd.Set("assignment-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 + } + + params := zavudev.Number10dlcCampaignPhoneNumberUnassignParams{ + CampaignID: cmd.Value("campaign-id").(string), + } + + return client.Number10dlc.Campaigns.PhoneNumbers.Unassign( + ctx, + cmd.Value("assignment-id").(string), + params, + options..., + ) +} diff --git a/pkg/cmd/number10dlccampaignphonenumber_test.go b/pkg/cmd/number10dlccampaignphonenumber_test.go new file mode 100644 index 0000000..f447958 --- /dev/null +++ b/pkg/cmd/number10dlccampaignphonenumber_test.go @@ -0,0 +1,58 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "testing" + + "github.com/zavudev/cli/internal/mocktest" +) + +func TestNumber10dlcCampaignsPhoneNumbersList(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "number-10dlc:campaigns:phone-numbers", "list", + "--campaign-id", "campaignId", + ) + }) +} + +func TestNumber10dlcCampaignsPhoneNumbersAssign(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "number-10dlc:campaigns:phone-numbers", "assign", + "--campaign-id", "campaignId", + "--phone-number-id", "pn_abc123", + ) + }) + + t.Run("piping data", func(t *testing.T) { + // Test piping YAML data over stdin + pipeData := []byte("phoneNumberId: pn_abc123") + mocktest.TestRunMockTestWithPipeAndFlags( + t, pipeData, + "--api-key", "string", + "number-10dlc:campaigns:phone-numbers", "assign", + "--campaign-id", "campaignId", + ) + }) +} + +func TestNumber10dlcCampaignsPhoneNumbersUnassign(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "number-10dlc:campaigns:phone-numbers", "unassign", + "--campaign-id", "campaignId", + "--assignment-id", "assignmentId", + ) + }) +} diff --git a/pkg/cmd/phonenumber.go b/pkg/cmd/phonenumber.go index 5f7fcdf..453c6da 100644 --- a/pkg/cmd/phonenumber.go +++ b/pkg/cmd/phonenumber.go @@ -5,7 +5,6 @@ package cmd import ( "context" "fmt" - "os" "github.com/tidwall/gjson" "github.com/urfave/cli/v3" @@ -21,8 +20,9 @@ var phoneNumbersRetrieve = cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "phone-number-id", - Required: true, + Name: "phone-number-id", + Required: true, + PathParam: "phoneNumberId", }, }, Action: handlePhoneNumbersRetrieve, @@ -35,15 +35,16 @@ var phoneNumbersUpdate = cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "phone-number-id", - Required: true, + Name: "phone-number-id", + Required: true, + PathParam: "phoneNumberId", }, - &requestflag.Flag[any]{ + &requestflag.Flag[*string]{ Name: "name", Usage: "Custom name for the phone number. Set to null to clear.", BodyPath: "name", }, - &requestflag.Flag[any]{ + &requestflag.Flag[*string]{ Name: "sender-id", Usage: "Sender ID to assign the phone number to. Set to null to unassign.", BodyPath: "senderId", @@ -84,7 +85,7 @@ var phoneNumbersList = cli.Command{ var phoneNumbersPurchase = cli.Command{ Name: "purchase", - Usage: "Purchase an available phone number. The first US phone number is free for each\nteam.", + Usage: "Purchase an available phone number. Requires a paid plan: the Free plan cannot\npurchase phone numbers and receives `402` with code `paid_plan_required`. Paid\nplans include one US number at no charge. The included number is one per account\nand is granted once: claiming it spends the benefit for good, so releasing that\nnumber does not make another one free, and numbers the account already bought do\nnot consume it.", Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ @@ -109,8 +110,9 @@ var phoneNumbersRelease = cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "phone-number-id", - Required: true, + Name: "phone-number-id", + Required: true, + PathParam: "phoneNumberId", }, }, Action: handlePhoneNumbersRelease, @@ -130,7 +132,7 @@ var phoneNumbersRequirements = cli.Command{ }, &requestflag.Flag[string]{ Name: "type", - Usage: `Allowed values: "local", "mobile", "tollFree".`, + Usage: "Type of phone number. `mobile` is stocked in countries where no geographic (`local`) or non-geographic (`national`) inventory exists, and in several markets it is the only type that can receive SMS.", QueryPath: "type", }, }, @@ -149,6 +151,11 @@ var phoneNumbersSearchAvailable = cli.Command{ Required: true, QueryPath: "countryCode", }, + &requestflag.Flag[string]{ + Name: "capabilities", + Usage: "Comma-separated capabilities the number must have: `sms`, `voice`, `mms`. Numbers missing any of them are dropped.", + QueryPath: "capabilities", + }, &requestflag.Flag[string]{ Name: "contains", Usage: "Search for numbers containing this string.", @@ -162,7 +169,7 @@ var phoneNumbersSearchAvailable = cli.Command{ }, &requestflag.Flag[string]{ Name: "type", - Usage: `Allowed values: "local", "mobile", "tollFree".`, + Usage: "Type of phone number. `mobile` is stocked in countries where no geographic (`local`) or non-geographic (`national`) inventory exists, and in several markets it is the only type that can receive SMS.", QueryPath: "type", }, }, @@ -201,8 +208,15 @@ func handlePhoneNumbersRetrieve(ctx context.Context, cmd *cli.Command) error { obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "phone-numbers retrieve", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "phone-numbers retrieve", + Transform: transform, + }) } func handlePhoneNumbersUpdate(ctx context.Context, cmd *cli.Command) error { @@ -216,8 +230,6 @@ func handlePhoneNumbersUpdate(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := zavudev.PhoneNumberUpdateParams{} - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -229,6 +241,8 @@ func handlePhoneNumbersUpdate(ctx context.Context, cmd *cli.Command) error { return err } + params := zavudev.PhoneNumberUpdateParams{} + var res []byte options = append(options, option.WithResponseBodyInto(&res)) _, err = client.PhoneNumbers.Update( @@ -243,8 +257,15 @@ func handlePhoneNumbersUpdate(ctx context.Context, cmd *cli.Command) error { obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "phone-numbers update", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "phone-numbers update", + Transform: transform, + }) } func handlePhoneNumbersList(ctx context.Context, cmd *cli.Command) error { @@ -255,8 +276,6 @@ func handlePhoneNumbersList(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := zavudev.PhoneNumberListParams{} - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -268,7 +287,10 @@ func handlePhoneNumbersList(ctx context.Context, cmd *cli.Command) error { return err } + params := zavudev.PhoneNumberListParams{} + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") if format == "raw" { var res []byte @@ -278,14 +300,26 @@ func handlePhoneNumbersList(ctx context.Context, cmd *cli.Command) error { return err } obj := gjson.ParseBytes(res) - return ShowJSON(os.Stdout, "phone-numbers list", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "phone-numbers list", + Transform: transform, + }) } else { iter := client.PhoneNumbers.ListAutoPaging(ctx, params, options...) maxItems := int64(-1) if cmd.IsSet("max-items") { maxItems = cmd.Value("max-items").(int64) } - return ShowJSONIterator(os.Stdout, "phone-numbers list", iter, format, transform, maxItems) + return ShowJSONIterator(iter, maxItems, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "phone-numbers list", + Transform: transform, + }) } } @@ -297,8 +331,6 @@ func handlePhoneNumbersPurchase(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := zavudev.PhoneNumberPurchaseParams{} - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -310,6 +342,8 @@ func handlePhoneNumbersPurchase(ctx context.Context, cmd *cli.Command) error { return err } + params := zavudev.PhoneNumberPurchaseParams{} + var res []byte options = append(options, option.WithResponseBodyInto(&res)) _, err = client.PhoneNumbers.Purchase(ctx, params, options...) @@ -319,8 +353,15 @@ func handlePhoneNumbersPurchase(ctx context.Context, cmd *cli.Command) error { obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "phone-numbers purchase", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "phone-numbers purchase", + Transform: transform, + }) } func handlePhoneNumbersRelease(ctx context.Context, cmd *cli.Command) error { @@ -356,8 +397,6 @@ func handlePhoneNumbersRequirements(ctx context.Context, cmd *cli.Command) error return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := zavudev.PhoneNumberRequirementsParams{} - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -369,6 +408,8 @@ func handlePhoneNumbersRequirements(ctx context.Context, cmd *cli.Command) error return err } + params := zavudev.PhoneNumberRequirementsParams{} + var res []byte options = append(options, option.WithResponseBodyInto(&res)) _, err = client.PhoneNumbers.Requirements(ctx, params, options...) @@ -378,8 +419,15 @@ func handlePhoneNumbersRequirements(ctx context.Context, cmd *cli.Command) error obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "phone-numbers requirements", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "phone-numbers requirements", + Transform: transform, + }) } func handlePhoneNumbersSearchAvailable(ctx context.Context, cmd *cli.Command) error { @@ -390,8 +438,6 @@ func handlePhoneNumbersSearchAvailable(ctx context.Context, cmd *cli.Command) er return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := zavudev.PhoneNumberSearchAvailableParams{} - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -403,6 +449,8 @@ func handlePhoneNumbersSearchAvailable(ctx context.Context, cmd *cli.Command) er return err } + params := zavudev.PhoneNumberSearchAvailableParams{} + var res []byte options = append(options, option.WithResponseBodyInto(&res)) _, err = client.PhoneNumbers.SearchAvailable(ctx, params, options...) @@ -412,6 +460,13 @@ func handlePhoneNumbersSearchAvailable(ctx context.Context, cmd *cli.Command) er obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "phone-numbers search-available", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "phone-numbers search-available", + Transform: transform, + }) } diff --git a/pkg/cmd/phonenumber_test.go b/pkg/cmd/phonenumber_test.go index cf0ed8e..9fdcb84 100644 --- a/pkg/cmd/phonenumber_test.go +++ b/pkg/cmd/phonenumber_test.go @@ -120,6 +120,7 @@ func TestPhoneNumbersSearchAvailable(t *testing.T) { "--api-key", "string", "phone-numbers", "search-available", "--country-code", "xx", + "--capabilities", "voice,sms", "--contains", "contains", "--limit", "50", "--type", "local", diff --git a/pkg/cmd/regulatorydocument.go b/pkg/cmd/regulatorydocument.go index 12d2daf..3b31a0f 100644 --- a/pkg/cmd/regulatorydocument.go +++ b/pkg/cmd/regulatorydocument.go @@ -5,7 +5,6 @@ package cmd import ( "context" "fmt" - "os" "github.com/tidwall/gjson" "github.com/urfave/cli/v3" @@ -58,8 +57,9 @@ var regulatoryDocumentsRetrieve = cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "document-id", - Required: true, + Name: "document-id", + Required: true, + PathParam: "documentId", }, }, Action: handleRegulatoryDocumentsRetrieve, @@ -95,8 +95,9 @@ var regulatoryDocumentsDelete = cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "document-id", - Required: true, + Name: "document-id", + Required: true, + PathParam: "documentId", }, }, Action: handleRegulatoryDocumentsDelete, @@ -120,8 +121,6 @@ func handleRegulatoryDocumentsCreate(ctx context.Context, cmd *cli.Command) erro return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := zavudev.RegulatoryDocumentNewParams{} - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -133,6 +132,8 @@ func handleRegulatoryDocumentsCreate(ctx context.Context, cmd *cli.Command) erro return err } + params := zavudev.RegulatoryDocumentNewParams{} + var res []byte options = append(options, option.WithResponseBodyInto(&res)) _, err = client.RegulatoryDocuments.New(ctx, params, options...) @@ -142,8 +143,15 @@ func handleRegulatoryDocumentsCreate(ctx context.Context, cmd *cli.Command) erro obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "regulatory-documents create", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "regulatory-documents create", + Transform: transform, + }) } func handleRegulatoryDocumentsRetrieve(ctx context.Context, cmd *cli.Command) error { @@ -177,8 +185,15 @@ func handleRegulatoryDocumentsRetrieve(ctx context.Context, cmd *cli.Command) er obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "regulatory-documents retrieve", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "regulatory-documents retrieve", + Transform: transform, + }) } func handleRegulatoryDocumentsList(ctx context.Context, cmd *cli.Command) error { @@ -189,8 +204,6 @@ func handleRegulatoryDocumentsList(ctx context.Context, cmd *cli.Command) error return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := zavudev.RegulatoryDocumentListParams{} - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -202,7 +215,10 @@ func handleRegulatoryDocumentsList(ctx context.Context, cmd *cli.Command) error return err } + params := zavudev.RegulatoryDocumentListParams{} + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") if format == "raw" { var res []byte @@ -212,14 +228,26 @@ func handleRegulatoryDocumentsList(ctx context.Context, cmd *cli.Command) error return err } obj := gjson.ParseBytes(res) - return ShowJSON(os.Stdout, "regulatory-documents list", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "regulatory-documents list", + Transform: transform, + }) } else { iter := client.RegulatoryDocuments.ListAutoPaging(ctx, params, options...) maxItems := int64(-1) if cmd.IsSet("max-items") { maxItems = cmd.Value("max-items").(int64) } - return ShowJSONIterator(os.Stdout, "regulatory-documents list", iter, format, transform, maxItems) + return ShowJSONIterator(iter, maxItems, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "regulatory-documents list", + Transform: transform, + }) } } @@ -276,6 +304,13 @@ func handleRegulatoryDocumentsUploadURL(ctx context.Context, cmd *cli.Command) e obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "regulatory-documents upload-url", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "regulatory-documents upload-url", + Transform: transform, + }) } diff --git a/pkg/cmd/sender.go b/pkg/cmd/sender.go index 1150c7d..d42f630 100644 --- a/pkg/cmd/sender.go +++ b/pkg/cmd/sender.go @@ -5,7 +5,6 @@ package cmd import ( "context" "fmt" - "os" "github.com/tidwall/gjson" "github.com/urfave/cli/v3" @@ -25,9 +24,41 @@ var sendersCreate = cli.Command{ Required: true, BodyPath: "name", }, + &requestflag.Flag[string]{ + Name: "email-address", + Usage: "From-address for the email channel (e.g. noreply@yourdomain.com). The address's domain must be a verified email domain in your project. Setting this attaches the email channel to the sender.", + BodyPath: "emailAddress", + }, + &requestflag.Flag[string]{ + Name: "email-domain-id", + Usage: "ID of the verified email domain to attach. Optional — resolved from `emailAddress`'s domain when omitted.", + BodyPath: "emailDomainId", + }, + &requestflag.Flag[string]{ + Name: "email-from-name", + Usage: "Display name shown in the recipient's inbox for the email channel.", + BodyPath: "emailFromName", + }, + &requestflag.Flag[bool]{ + Name: "email-receiving-enabled", + Usage: "Enable inbound email receiving on this sender. Requires a verified MX record on the domain; ignored otherwise.", + BodyPath: "emailReceivingEnabled", + }, + &requestflag.Flag[bool]{ + Name: "enable-sms-oneway", + Usage: "Enable the one-way SMS channel (`sms_oneway`). Needs nothing else — no phone number, no credential — so it is the fastest way to get a sender that can send. Recipients cannot reply. Confirm with `sms_oneway` in the `channels` array on the response.", + Default: false, + BodyPath: "enableSmsOneway", + }, + &requestflag.Flag[bool]{ + Name: "enable-voice", + Usage: "Let this sender place and answer phone calls. Requires `phoneNumber`; enabling it without one returns 400. Check the `channels` array on the response to confirm `voice` is on.", + Default: false, + BodyPath: "enableVoice", + }, &requestflag.Flag[string]{ Name: "phone-number", - Required: true, + Usage: "Phone number in E.164 format, and it must be a number your project already owns (see `GET /v1/phone-numbers`). The number is routed to the sender as part of this call, which is what turns the SMS channel on. Passing a number the project does not own, or one already attached to another sender, returns 400 rather than creating a sender that cannot send. Omit for an email-only sender.", BodyPath: "phoneNumber", }, &requestflag.Flag[bool]{ @@ -40,6 +71,11 @@ var sendersCreate = cli.Command{ Usage: "Events to subscribe to.", BodyPath: "webhookEvents", }, + &requestflag.Flag[string]{ + Name: "webhook-signature-version", + Usage: "Which `X-Zavu-Signature` scheme this receiver is sent.\n\n- `v1`: `v1=HMAC_SHA256(secret, body)`. The scheme used before this was configurable. Existing webhooks stay on it until you move them.\n- `v2`: `v2=HMAC_SHA256(secret, \"{t}.{body}\")`. The current scheme, and the default for new senders. It signs the timestamp together with the body.\n- `v1+v2`: both signatures, sharing one `t`. The migration setting: a receiver reading either one works, so you can deploy and confirm your new verifier before switching over.\n\nMoving from `v1` straight to `v2` returns `400`. Set `v1+v2` first. See https://docs.zavu.dev/guides/receiving-messages/signature-migration", + BodyPath: "webhookSignatureVersion", + }, &requestflag.Flag[string]{ Name: "webhook-url", Usage: "HTTPS URL for webhook events.", @@ -56,8 +92,9 @@ var sendersRetrieve = cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "sender-id", - Required: true, + Name: "sender-id", + Required: true, + PathParam: "senderId", }, }, Action: handleSendersRetrieve, @@ -70,14 +107,45 @@ var sendersUpdate = cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "sender-id", - Required: true, + Name: "sender-id", + Required: true, + PathParam: "senderId", + }, + &requestflag.Flag[string]{ + Name: "email-address", + Usage: "Attach or change the sender's email from-address (e.g. noreply@yourdomain.com). The domain must be a verified email domain in your project.", + BodyPath: "emailAddress", + }, + &requestflag.Flag[bool]{ + Name: "email-catch-all-enabled", + Usage: "Enable or disable domain catch-all. When enabled (with emailReceivingEnabled true), this sender receives email for any address at its domain. Ignored (treated as false) if receiving is not enabled.", + BodyPath: "emailCatchAllEnabled", + }, + &requestflag.Flag[string]{ + Name: "email-domain-id", + Usage: "ID of the verified email domain to attach. Optional — resolved from `emailAddress`'s domain when omitted.", + BodyPath: "emailDomainId", + }, + &requestflag.Flag[string]{ + Name: "email-from-name", + Usage: "Display name shown in the recipient's inbox for the email channel.", + BodyPath: "emailFromName", }, &requestflag.Flag[bool]{ Name: "email-receiving-enabled", Usage: "Enable or disable inbound email receiving for this sender.", BodyPath: "emailReceivingEnabled", }, + &requestflag.Flag[bool]{ + Name: "enable-sms-oneway", + Usage: "Turn the one-way SMS channel on or off. Enabling needs nothing else and takes effect immediately; disabling removes the channel from the sender. Confirm with the `channels` array on the response.", + BodyPath: "enableSmsOneway", + }, + &requestflag.Flag[bool]{ + Name: "enable-voice", + Usage: "Turn the voice channel on or off. The sender must already have a phone number provisioned for calls; enabling it otherwise returns 400 instead of storing a flag that changes nothing. Confirm with the `channels` array on the response.", + BodyPath: "enableVoice", + }, &requestflag.Flag[string]{ Name: "name", BodyPath: "name", @@ -96,7 +164,12 @@ var sendersUpdate = cli.Command{ Usage: "Events to subscribe to.", BodyPath: "webhookEvents", }, - &requestflag.Flag[any]{ + &requestflag.Flag[string]{ + Name: "webhook-signature-version", + Usage: "Which `X-Zavu-Signature` scheme this receiver is sent.\n\n- `v1`: `v1=HMAC_SHA256(secret, body)`. The scheme used before this was configurable. Existing webhooks stay on it until you move them.\n- `v2`: `v2=HMAC_SHA256(secret, \"{t}.{body}\")`. The current scheme, and the default for new senders. It signs the timestamp together with the body.\n- `v1+v2`: both signatures, sharing one `t`. The migration setting: a receiver reading either one works, so you can deploy and confirm your new verifier before switching over.\n\nMoving from `v1` straight to `v2` returns `400`. Set `v1+v2` first. See https://docs.zavu.dev/guides/receiving-messages/signature-migration", + BodyPath: "webhookSignatureVersion", + }, + &requestflag.Flag[*string]{ Name: "webhook-url", Usage: "HTTPS URL for webhook events. Set to null to remove webhook.", BodyPath: "webhookUrl", @@ -135,8 +208,9 @@ var sendersDelete = cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "sender-id", - Required: true, + Name: "sender-id", + Required: true, + PathParam: "senderId", }, }, Action: handleSendersDelete, @@ -149,8 +223,9 @@ var sendersGetProfile = cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "sender-id", - Required: true, + Name: "sender-id", + Required: true, + PathParam: "senderId", }, }, Action: handleSendersGetProfile, @@ -163,8 +238,9 @@ var sendersRegenerateWebhookSecret = cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "sender-id", - Required: true, + Name: "sender-id", + Required: true, + PathParam: "senderId", }, }, Action: handleSendersRegenerateWebhookSecret, @@ -177,8 +253,9 @@ var sendersUpdateProfile = cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "sender-id", - Required: true, + Name: "sender-id", + Required: true, + PathParam: "senderId", }, &requestflag.Flag[string]{ Name: "about", @@ -221,8 +298,9 @@ var sendersUploadProfilePicture = cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "sender-id", - Required: true, + Name: "sender-id", + Required: true, + PathParam: "senderId", }, &requestflag.Flag[string]{ Name: "image-url", @@ -249,8 +327,6 @@ func handleSendersCreate(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := zavudev.SenderNewParams{} - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -262,6 +338,8 @@ func handleSendersCreate(ctx context.Context, cmd *cli.Command) error { return err } + params := zavudev.SenderNewParams{} + var res []byte options = append(options, option.WithResponseBodyInto(&res)) _, err = client.Senders.New(ctx, params, options...) @@ -271,8 +349,15 @@ func handleSendersCreate(ctx context.Context, cmd *cli.Command) error { obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "senders create", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "senders create", + Transform: transform, + }) } func handleSendersRetrieve(ctx context.Context, cmd *cli.Command) error { @@ -306,8 +391,15 @@ func handleSendersRetrieve(ctx context.Context, cmd *cli.Command) error { obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "senders retrieve", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "senders retrieve", + Transform: transform, + }) } func handleSendersUpdate(ctx context.Context, cmd *cli.Command) error { @@ -321,8 +413,6 @@ func handleSendersUpdate(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := zavudev.SenderUpdateParams{} - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -334,6 +424,8 @@ func handleSendersUpdate(ctx context.Context, cmd *cli.Command) error { return err } + params := zavudev.SenderUpdateParams{} + var res []byte options = append(options, option.WithResponseBodyInto(&res)) _, err = client.Senders.Update( @@ -348,8 +440,15 @@ func handleSendersUpdate(ctx context.Context, cmd *cli.Command) error { obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "senders update", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "senders update", + Transform: transform, + }) } func handleSendersList(ctx context.Context, cmd *cli.Command) error { @@ -360,8 +459,6 @@ func handleSendersList(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := zavudev.SenderListParams{} - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -373,7 +470,10 @@ func handleSendersList(ctx context.Context, cmd *cli.Command) error { return err } + params := zavudev.SenderListParams{} + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") if format == "raw" { var res []byte @@ -383,14 +483,26 @@ func handleSendersList(ctx context.Context, cmd *cli.Command) error { return err } obj := gjson.ParseBytes(res) - return ShowJSON(os.Stdout, "senders list", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "senders list", + Transform: transform, + }) } else { iter := client.Senders.ListAutoPaging(ctx, params, options...) maxItems := int64(-1) if cmd.IsSet("max-items") { maxItems = cmd.Value("max-items").(int64) } - return ShowJSONIterator(os.Stdout, "senders list", iter, format, transform, maxItems) + return ShowJSONIterator(iter, maxItems, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "senders list", + Transform: transform, + }) } } @@ -450,8 +562,15 @@ func handleSendersGetProfile(ctx context.Context, cmd *cli.Command) error { obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "senders get-profile", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "senders get-profile", + Transform: transform, + }) } func handleSendersRegenerateWebhookSecret(ctx context.Context, cmd *cli.Command) error { @@ -485,8 +604,15 @@ func handleSendersRegenerateWebhookSecret(ctx context.Context, cmd *cli.Command) obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "senders regenerate-webhook-secret", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "senders regenerate-webhook-secret", + Transform: transform, + }) } func handleSendersUpdateProfile(ctx context.Context, cmd *cli.Command) error { @@ -500,8 +626,6 @@ func handleSendersUpdateProfile(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := zavudev.SenderUpdateProfileParams{} - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -513,6 +637,8 @@ func handleSendersUpdateProfile(ctx context.Context, cmd *cli.Command) error { return err } + params := zavudev.SenderUpdateProfileParams{} + var res []byte options = append(options, option.WithResponseBodyInto(&res)) _, err = client.Senders.UpdateProfile( @@ -527,8 +653,15 @@ func handleSendersUpdateProfile(ctx context.Context, cmd *cli.Command) error { obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "senders update-profile", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "senders update-profile", + Transform: transform, + }) } func handleSendersUploadProfilePicture(ctx context.Context, cmd *cli.Command) error { @@ -542,8 +675,6 @@ func handleSendersUploadProfilePicture(ctx context.Context, cmd *cli.Command) er return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := zavudev.SenderUploadProfilePictureParams{} - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -555,6 +686,8 @@ func handleSendersUploadProfilePicture(ctx context.Context, cmd *cli.Command) er return err } + params := zavudev.SenderUploadProfilePictureParams{} + var res []byte options = append(options, option.WithResponseBodyInto(&res)) _, err = client.Senders.UploadProfilePicture( @@ -569,6 +702,13 @@ func handleSendersUploadProfilePicture(ctx context.Context, cmd *cli.Command) er obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "senders upload-profile-picture", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "senders upload-profile-picture", + Transform: transform, + }) } diff --git a/pkg/cmd/sender_test.go b/pkg/cmd/sender_test.go index b3b796f..68a7f18 100644 --- a/pkg/cmd/sender_test.go +++ b/pkg/cmd/sender_test.go @@ -16,9 +16,16 @@ func TestSendersCreate(t *testing.T) { "--api-key", "string", "senders", "create", "--name", "name", + "--email-address", "noreply@yourdomain.com", + "--email-domain-id", "emailDomainId", + "--email-from-name", "emailFromName", + "--email-receiving-enabled=true", + "--enable-sms-oneway=true", + "--enable-voice=true", "--phone-number", "phoneNumber", "--set-as-default=true", "--webhook-event", "message.queued", + "--webhook-signature-version", "v2", "--webhook-url", "https://example.com", ) }) @@ -27,10 +34,17 @@ func TestSendersCreate(t *testing.T) { // Test piping YAML data over stdin pipeData := []byte("" + "name: name\n" + + "emailAddress: noreply@yourdomain.com\n" + + "emailDomainId: emailDomainId\n" + + "emailFromName: emailFromName\n" + + "emailReceivingEnabled: true\n" + + "enableSmsOneway: true\n" + + "enableVoice: true\n" + "phoneNumber: phoneNumber\n" + "setAsDefault: true\n" + "webhookEvents:\n" + " - message.queued\n" + + "webhookSignatureVersion: v2\n" + "webhookUrl: https://example.com\n") mocktest.TestRunMockTestWithPipeAndFlags( t, pipeData, @@ -60,11 +74,18 @@ func TestSendersUpdate(t *testing.T) { "--api-key", "string", "senders", "update", "--sender-id", "senderId", + "--email-address", "noreply@yourdomain.com", + "--email-catch-all-enabled=true", + "--email-domain-id", "emailDomainId", + "--email-from-name", "emailFromName", "--email-receiving-enabled=true", + "--enable-sms-oneway=true", + "--enable-voice=true", "--name", "name", "--set-as-default=true", "--webhook-active=true", "--webhook-event", "message.queued", + "--webhook-signature-version", "v2", "--webhook-url", "https://example.com", ) }) @@ -72,12 +93,19 @@ func TestSendersUpdate(t *testing.T) { t.Run("piping data", func(t *testing.T) { // Test piping YAML data over stdin pipeData := []byte("" + + "emailAddress: noreply@yourdomain.com\n" + + "emailCatchAllEnabled: true\n" + + "emailDomainId: emailDomainId\n" + + "emailFromName: emailFromName\n" + "emailReceivingEnabled: true\n" + + "enableSmsOneway: true\n" + + "enableVoice: true\n" + "name: name\n" + "setAsDefault: true\n" + "webhookActive: true\n" + "webhookEvents:\n" + " - message.queued\n" + + "webhookSignatureVersion: v2\n" + "webhookUrl: https://example.com\n") mocktest.TestRunMockTestWithPipeAndFlags( t, pipeData, diff --git a/pkg/cmd/senderagent.go b/pkg/cmd/senderagent.go index 9dc135c..d69f5c3 100644 --- a/pkg/cmd/senderagent.go +++ b/pkg/cmd/senderagent.go @@ -5,7 +5,6 @@ package cmd import ( "context" "fmt" - "os" "github.com/tidwall/gjson" "github.com/urfave/cli/v3" @@ -15,14 +14,15 @@ import ( "github.com/zavudev/sdk-go/option" ) -var sendersAgentCreate = cli.Command{ +var sendersAgentCreate = requestflag.WithInnerFlags(cli.Command{ Name: "create", Usage: "Create an AI agent for a sender. Each sender can have at most one agent.", Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "sender-id", - Required: true, + Name: "sender-id", + Required: true, + PathParam: "senderId", }, &requestflag.Flag[string]{ Name: "model", @@ -78,10 +78,103 @@ var sendersAgentCreate = cli.Command{ Default: []string{"text"}, BodyPath: "triggerOnMessageTypes", }, + &requestflag.Flag[map[string]any]{ + Name: "voice", + Usage: "Voice Agent configuration. Enable this to let the agent answer and place phone calls with Zavu's managed voice pipeline. Requires the Voice Agents feature to be enabled for your team.", + BodyPath: "voice", + }, }, Action: handleSendersAgentCreate, HideHelpCommand: true, -} +}, map[string][]requestflag.HasOuterFlag{ + "voice": { + &requestflag.InnerFlag[bool]{ + Name: "voice.enabled", + Usage: "Whether the agent handles voice calls. When false, the sender's number is not answered by the voice agent and outbound calls are rejected.", + InnerField: "enabled", + }, + &requestflag.InnerFlag[string]{ + Name: "voice.greeting", + Usage: "Opening line the agent speaks when the call connects. If omitted, the agent waits for the caller to speak first.", + InnerField: "greeting", + }, + &requestflag.InnerFlag[map[string]any]{ + Name: "voice.greetings", + Usage: "Greeting per language, keyed by language code. Used when the caller's language differs from the one `greeting` is written in.", + InnerField: "greetings", + }, + &requestflag.InnerFlag[bool]{ + Name: "voice.interruptible", + Usage: "Whether the caller can interrupt the agent while it is speaking (barge-in). When true, the agent stops talking as soon as the caller starts.", + InnerField: "interruptible", + }, + &requestflag.InnerFlag[string]{ + Name: "voice.language", + Usage: "BCP-47 language code used for both speech recognition and speech synthesis (e.g. `en`, `es`, `pt-BR`). Auto-detected from the recipient when omitted.", + InnerField: "language", + }, + &requestflag.InnerFlag[int64]{ + Name: "voice.max-call-duration-minutes", + Usage: "Hard limit on call length in minutes. The call ends automatically when reached.", + InnerField: "maxCallDurationMinutes", + }, + &requestflag.InnerFlag[int64]{ + Name: "voice.max-idle-seconds", + Usage: "How long the agent waits during silence before ending the call.", + InnerField: "maxIdleSeconds", + }, + &requestflag.InnerFlag[string]{ + Name: "voice.model", + Usage: "Model that runs the conversation, co-located in the voice network for lowest latency. Independent of the model used for text messaging. Derived from the agent's text model when omitted.", + InnerField: "model", + }, + &requestflag.InnerFlag[bool]{ + Name: "voice.record-calls", + Usage: "Whether the call audio is recorded.", + InnerField: "recordCalls", + }, + &requestflag.InnerFlag[string]{ + Name: "voice.stt-model", + Usage: "Speech-recognition model. Uses the default when omitted.", + InnerField: "sttModel", + }, + &requestflag.InnerFlag[string]{ + Name: "voice.stt-provider", + Usage: "Speech-recognition provider. Uses the default when omitted.", + InnerField: "sttProvider", + }, + &requestflag.InnerFlag[string]{ + Name: "voice.transfer-phone-number", + Usage: "E.164 phone number the agent can transfer the call to. When set, the agent is given a transfer tool it can use to hand the call to a human.", + InnerField: "transferPhoneNumber", + }, + &requestflag.InnerFlag[string]{ + Name: "voice.tts-provider", + Usage: "Speech-synthesis provider. Uses the default when omitted.", + InnerField: "ttsProvider", + }, + &requestflag.InnerFlag[string]{ + Name: "voice.tts-voice-id", + Usage: "Identifier of the synthesized voice that speaks. Choose from the voices available in the dashboard. Uses a neutral default when omitted.", + InnerField: "ttsVoiceId", + }, + &requestflag.InnerFlag[string]{ + Name: "voice.voicemail-action", + Usage: "What the agent does when an answering machine or voicemail is detected on an outbound call.", + InnerField: "voicemailAction", + }, + &requestflag.InnerFlag[string]{ + Name: "voice.voicemail-message", + Usage: "Message spoken when `voicemailAction` is `leave_message`. Falls back to `greeting` when omitted.", + InnerField: "voicemailMessage", + }, + &requestflag.InnerFlag[float64]{ + Name: "voice.voice-speed", + Usage: "Speech rate. 1.0 is natural. Only honoured by voices that support rate control; ignored by the others.", + InnerField: "voiceSpeed", + }, + }, +}) var sendersAgentRetrieve = cli.Command{ Name: "retrieve", @@ -89,22 +182,24 @@ var sendersAgentRetrieve = cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "sender-id", - Required: true, + Name: "sender-id", + Required: true, + PathParam: "senderId", }, }, Action: handleSendersAgentRetrieve, HideHelpCommand: true, } -var sendersAgentUpdate = cli.Command{ +var sendersAgentUpdate = requestflag.WithInnerFlags(cli.Command{ Name: "update", Usage: "Update an AI agent's configuration.", Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "sender-id", - Required: true, + Name: "sender-id", + Required: true, + PathParam: "senderId", }, &requestflag.Flag[string]{ Name: "api-key", @@ -122,7 +217,7 @@ var sendersAgentUpdate = cli.Command{ Name: "include-contact-metadata", BodyPath: "includeContactMetadata", }, - &requestflag.Flag[any]{ + &requestflag.Flag[*int64]{ Name: "max-tokens", BodyPath: "maxTokens", }, @@ -143,7 +238,7 @@ var sendersAgentUpdate = cli.Command{ Name: "system-prompt", BodyPath: "systemPrompt", }, - &requestflag.Flag[any]{ + &requestflag.Flag[*float64]{ Name: "temperature", BodyPath: "temperature", }, @@ -155,10 +250,103 @@ var sendersAgentUpdate = cli.Command{ Name: "trigger-on-message-type", BodyPath: "triggerOnMessageTypes", }, + &requestflag.Flag[map[string]any]{ + Name: "voice", + Usage: "Voice Agent configuration. Patch this object to enable voice, change the greeting, or adjust call limits. Requires the Voice Agents feature to be enabled for your team.", + BodyPath: "voice", + }, }, Action: handleSendersAgentUpdate, HideHelpCommand: true, -} +}, map[string][]requestflag.HasOuterFlag{ + "voice": { + &requestflag.InnerFlag[bool]{ + Name: "voice.enabled", + Usage: "Whether the agent handles voice calls. When false, the sender's number is not answered by the voice agent and outbound calls are rejected.", + InnerField: "enabled", + }, + &requestflag.InnerFlag[string]{ + Name: "voice.greeting", + Usage: "Opening line the agent speaks when the call connects. If omitted, the agent waits for the caller to speak first.", + InnerField: "greeting", + }, + &requestflag.InnerFlag[map[string]any]{ + Name: "voice.greetings", + Usage: "Greeting per language, keyed by language code. Used when the caller's language differs from the one `greeting` is written in.", + InnerField: "greetings", + }, + &requestflag.InnerFlag[bool]{ + Name: "voice.interruptible", + Usage: "Whether the caller can interrupt the agent while it is speaking (barge-in). When true, the agent stops talking as soon as the caller starts.", + InnerField: "interruptible", + }, + &requestflag.InnerFlag[string]{ + Name: "voice.language", + Usage: "BCP-47 language code used for both speech recognition and speech synthesis (e.g. `en`, `es`, `pt-BR`). Auto-detected from the recipient when omitted.", + InnerField: "language", + }, + &requestflag.InnerFlag[int64]{ + Name: "voice.max-call-duration-minutes", + Usage: "Hard limit on call length in minutes. The call ends automatically when reached.", + InnerField: "maxCallDurationMinutes", + }, + &requestflag.InnerFlag[int64]{ + Name: "voice.max-idle-seconds", + Usage: "How long the agent waits during silence before ending the call.", + InnerField: "maxIdleSeconds", + }, + &requestflag.InnerFlag[string]{ + Name: "voice.model", + Usage: "Model that runs the conversation, co-located in the voice network for lowest latency. Independent of the model used for text messaging. Derived from the agent's text model when omitted.", + InnerField: "model", + }, + &requestflag.InnerFlag[bool]{ + Name: "voice.record-calls", + Usage: "Whether the call audio is recorded.", + InnerField: "recordCalls", + }, + &requestflag.InnerFlag[string]{ + Name: "voice.stt-model", + Usage: "Speech-recognition model. Uses the default when omitted.", + InnerField: "sttModel", + }, + &requestflag.InnerFlag[string]{ + Name: "voice.stt-provider", + Usage: "Speech-recognition provider. Uses the default when omitted.", + InnerField: "sttProvider", + }, + &requestflag.InnerFlag[string]{ + Name: "voice.transfer-phone-number", + Usage: "E.164 phone number the agent can transfer the call to. When set, the agent is given a transfer tool it can use to hand the call to a human.", + InnerField: "transferPhoneNumber", + }, + &requestflag.InnerFlag[string]{ + Name: "voice.tts-provider", + Usage: "Speech-synthesis provider. Uses the default when omitted.", + InnerField: "ttsProvider", + }, + &requestflag.InnerFlag[string]{ + Name: "voice.tts-voice-id", + Usage: "Identifier of the synthesized voice that speaks. Choose from the voices available in the dashboard. Uses a neutral default when omitted.", + InnerField: "ttsVoiceId", + }, + &requestflag.InnerFlag[string]{ + Name: "voice.voicemail-action", + Usage: "What the agent does when an answering machine or voicemail is detected on an outbound call.", + InnerField: "voicemailAction", + }, + &requestflag.InnerFlag[string]{ + Name: "voice.voicemail-message", + Usage: "Message spoken when `voicemailAction` is `leave_message`. Falls back to `greeting` when omitted.", + InnerField: "voicemailMessage", + }, + &requestflag.InnerFlag[float64]{ + Name: "voice.voice-speed", + Usage: "Speech rate. 1.0 is natural. Only honoured by voices that support rate control; ignored by the others.", + InnerField: "voiceSpeed", + }, + }, +}) var sendersAgentDelete = cli.Command{ Name: "delete", @@ -166,8 +354,9 @@ var sendersAgentDelete = cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "sender-id", - Required: true, + Name: "sender-id", + Required: true, + PathParam: "senderId", }, }, Action: handleSendersAgentDelete, @@ -180,8 +369,9 @@ var sendersAgentStats = cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "sender-id", - Required: true, + Name: "sender-id", + Required: true, + PathParam: "senderId", }, }, Action: handleSendersAgentStats, @@ -199,8 +389,6 @@ func handleSendersAgentCreate(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := zavudev.SenderAgentNewParams{} - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -212,6 +400,8 @@ func handleSendersAgentCreate(ctx context.Context, cmd *cli.Command) error { return err } + params := zavudev.SenderAgentNewParams{} + var res []byte options = append(options, option.WithResponseBodyInto(&res)) _, err = client.Senders.Agent.New( @@ -226,8 +416,15 @@ func handleSendersAgentCreate(ctx context.Context, cmd *cli.Command) error { obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "senders:agent create", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "senders:agent create", + Transform: transform, + }) } func handleSendersAgentRetrieve(ctx context.Context, cmd *cli.Command) error { @@ -261,8 +458,15 @@ func handleSendersAgentRetrieve(ctx context.Context, cmd *cli.Command) error { obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "senders:agent retrieve", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "senders:agent retrieve", + Transform: transform, + }) } func handleSendersAgentUpdate(ctx context.Context, cmd *cli.Command) error { @@ -276,8 +480,6 @@ func handleSendersAgentUpdate(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := zavudev.SenderAgentUpdateParams{} - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -289,6 +491,8 @@ func handleSendersAgentUpdate(ctx context.Context, cmd *cli.Command) error { return err } + params := zavudev.SenderAgentUpdateParams{} + var res []byte options = append(options, option.WithResponseBodyInto(&res)) _, err = client.Senders.Agent.Update( @@ -303,8 +507,15 @@ func handleSendersAgentUpdate(ctx context.Context, cmd *cli.Command) error { obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "senders:agent update", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "senders:agent update", + Transform: transform, + }) } func handleSendersAgentDelete(ctx context.Context, cmd *cli.Command) error { @@ -363,6 +574,13 @@ func handleSendersAgentStats(ctx context.Context, cmd *cli.Command) error { obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "senders:agent stats", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "senders:agent stats", + Transform: transform, + }) } diff --git a/pkg/cmd/senderagent_test.go b/pkg/cmd/senderagent_test.go index 8425f0e..5e77159 100644 --- a/pkg/cmd/senderagent_test.go +++ b/pkg/cmd/senderagent_test.go @@ -6,6 +6,7 @@ import ( "testing" "github.com/zavudev/cli/internal/mocktest" + "github.com/zavudev/cli/internal/requestflag" ) func TestSendersAgentCreate(t *testing.T) { @@ -27,6 +28,48 @@ func TestSendersAgentCreate(t *testing.T) { "--temperature", "0", "--trigger-on-channel", "string", "--trigger-on-message-type", "string", + "--voice", "{enabled: true, greeting: 'Hi, thanks for calling Acme. How can I help you today?', greetings: {es: 'Hola, soy Atlas. Preguntame lo que quieras.'}, interruptible: true, language: en, maxCallDurationMinutes: 1, maxIdleSeconds: 5, model: openai/gpt-4o, recordCalls: true, sttModel: sttModel, sttProvider: sttProvider, transferPhoneNumber: '+14155551234', ttsProvider: ttsProvider, ttsVoiceId: aria, voicemailAction: hangup, voicemailMessage: voicemailMessage, voiceSpeed: 0.5}", + ) + }) + + t.Run("inner flags", func(t *testing.T) { + // Check that inner flags have been set up correctly + requestflag.CheckInnerFlags(sendersAgentCreate) + + // Alternative argument passing style using inner flags + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "senders:agent", "create", + "--sender-id", "senderId", + "--model", "gpt-4o-mini", + "--name", "Customer Support", + "--provider", "openai", + "--system-prompt", "You are a helpful customer support agent. Be friendly and concise.", + "--api-key", "sk-...", + "--context-window-messages", "1", + "--include-contact-metadata=true", + "--max-tokens", "1", + "--temperature", "0", + "--trigger-on-channel", "string", + "--trigger-on-message-type", "string", + "--voice.enabled=true", + "--voice.greeting", "Hi, thanks for calling Acme. How can I help you today?", + "--voice.greetings", "{es: 'Hola, soy Atlas. Preguntame lo que quieras.'}", + "--voice.interruptible=true", + "--voice.language", "en", + "--voice.max-call-duration-minutes", "1", + "--voice.max-idle-seconds", "5", + "--voice.model", "openai/gpt-4o", + "--voice.record-calls=true", + "--voice.stt-model", "sttModel", + "--voice.stt-provider", "sttProvider", + "--voice.transfer-phone-number", "+14155551234", + "--voice.tts-provider", "ttsProvider", + "--voice.tts-voice-id", "aria", + "--voice.voicemail-action", "hangup", + "--voice.voicemail-message", "voicemailMessage", + "--voice.voice-speed", "0.5", ) }) @@ -45,7 +88,26 @@ func TestSendersAgentCreate(t *testing.T) { "triggerOnChannels:\n" + " - string\n" + "triggerOnMessageTypes:\n" + - " - string\n") + " - string\n" + + "voice:\n" + + " enabled: true\n" + + " greeting: Hi, thanks for calling Acme. How can I help you today?\n" + + " greetings:\n" + + " es: Hola, soy Atlas. Preguntame lo que quieras.\n" + + " interruptible: true\n" + + " language: en\n" + + " maxCallDurationMinutes: 1\n" + + " maxIdleSeconds: 5\n" + + " model: openai/gpt-4o\n" + + " recordCalls: true\n" + + " sttModel: sttModel\n" + + " sttProvider: sttProvider\n" + + " transferPhoneNumber: '+14155551234'\n" + + " ttsProvider: ttsProvider\n" + + " ttsVoiceId: aria\n" + + " voicemailAction: hangup\n" + + " voicemailMessage: voicemailMessage\n" + + " voiceSpeed: 0.5\n") mocktest.TestRunMockTestWithPipeAndFlags( t, pipeData, "--api-key", "string", @@ -87,6 +149,49 @@ func TestSendersAgentUpdate(t *testing.T) { "--temperature", "0", "--trigger-on-channel", "string", "--trigger-on-message-type", "string", + "--voice", "{enabled: true, greeting: 'Hi, thanks for calling Acme. How can I help you today?', greetings: {es: 'Hola, soy Atlas. Preguntame lo que quieras.'}, interruptible: true, language: en, maxCallDurationMinutes: 1, maxIdleSeconds: 5, model: openai/gpt-4o, recordCalls: true, sttModel: sttModel, sttProvider: sttProvider, transferPhoneNumber: '+14155551234', ttsProvider: ttsProvider, ttsVoiceId: aria, voicemailAction: hangup, voicemailMessage: voicemailMessage, voiceSpeed: 0.5}", + ) + }) + + t.Run("inner flags", func(t *testing.T) { + // Check that inner flags have been set up correctly + requestflag.CheckInnerFlags(sendersAgentUpdate) + + // Alternative argument passing style using inner flags + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "senders:agent", "update", + "--sender-id", "senderId", + "--api-key", "apiKey", + "--context-window-messages", "1", + "--enabled=true", + "--include-contact-metadata=true", + "--max-tokens", "1", + "--model", "model", + "--name", "name", + "--provider", "openai", + "--system-prompt", "systemPrompt", + "--temperature", "0", + "--trigger-on-channel", "string", + "--trigger-on-message-type", "string", + "--voice.enabled=true", + "--voice.greeting", "Hi, thanks for calling Acme. How can I help you today?", + "--voice.greetings", "{es: 'Hola, soy Atlas. Preguntame lo que quieras.'}", + "--voice.interruptible=true", + "--voice.language", "en", + "--voice.max-call-duration-minutes", "1", + "--voice.max-idle-seconds", "5", + "--voice.model", "openai/gpt-4o", + "--voice.record-calls=true", + "--voice.stt-model", "sttModel", + "--voice.stt-provider", "sttProvider", + "--voice.transfer-phone-number", "+14155551234", + "--voice.tts-provider", "ttsProvider", + "--voice.tts-voice-id", "aria", + "--voice.voicemail-action", "hangup", + "--voice.voicemail-message", "voicemailMessage", + "--voice.voice-speed", "0.5", ) }) @@ -106,7 +211,26 @@ func TestSendersAgentUpdate(t *testing.T) { "triggerOnChannels:\n" + " - string\n" + "triggerOnMessageTypes:\n" + - " - string\n") + " - string\n" + + "voice:\n" + + " enabled: true\n" + + " greeting: Hi, thanks for calling Acme. How can I help you today?\n" + + " greetings:\n" + + " es: Hola, soy Atlas. Preguntame lo que quieras.\n" + + " interruptible: true\n" + + " language: en\n" + + " maxCallDurationMinutes: 1\n" + + " maxIdleSeconds: 5\n" + + " model: openai/gpt-4o\n" + + " recordCalls: true\n" + + " sttModel: sttModel\n" + + " sttProvider: sttProvider\n" + + " transferPhoneNumber: '+14155551234'\n" + + " ttsProvider: ttsProvider\n" + + " ttsVoiceId: aria\n" + + " voicemailAction: hangup\n" + + " voicemailMessage: voicemailMessage\n" + + " voiceSpeed: 0.5\n") mocktest.TestRunMockTestWithPipeAndFlags( t, pipeData, "--api-key", "string", diff --git a/pkg/cmd/senderagentexecution.go b/pkg/cmd/senderagentexecution.go index fb35434..695e892 100644 --- a/pkg/cmd/senderagentexecution.go +++ b/pkg/cmd/senderagentexecution.go @@ -5,7 +5,6 @@ package cmd import ( "context" "fmt" - "os" "github.com/tidwall/gjson" "github.com/urfave/cli/v3" @@ -15,14 +14,35 @@ import ( "github.com/zavudev/sdk-go/option" ) +var sendersAgentExecutionsRetrieve = cli.Command{ + Name: "retrieve", + Usage: "Fetch full details for one execution — including `errorMessage`, `errorCode`,\nand `responseText`. Use this to debug failures surfaced by the list endpoint.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "sender-id", + Required: true, + PathParam: "senderId", + }, + &requestflag.Flag[string]{ + Name: "execution-id", + Required: true, + PathParam: "executionId", + }, + }, + Action: handleSendersAgentExecutionsRetrieve, + HideHelpCommand: true, +} + var sendersAgentExecutionsList = cli.Command{ Name: "list", Usage: "List recent agent executions with pagination.", Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "sender-id", - Required: true, + Name: "sender-id", + Required: true, + PathParam: "senderId", }, &requestflag.Flag[string]{ Name: "cursor", @@ -47,6 +67,57 @@ var sendersAgentExecutionsList = cli.Command{ HideHelpCommand: true, } +func handleSendersAgentExecutionsRetrieve(ctx context.Context, cmd *cli.Command) error { + client := zavudev.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("execution-id") && len(unusedArgs) > 0 { + cmd.Set("execution-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 + } + + params := zavudev.SenderAgentExecutionGetParams{ + SenderID: cmd.Value("sender-id").(string), + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Senders.Agent.Executions.Get( + ctx, + cmd.Value("execution-id").(string), + params, + options..., + ) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") + transform := cmd.Root().String("transform") + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "senders:agent:executions retrieve", + Transform: transform, + }) +} + func handleSendersAgentExecutionsList(ctx context.Context, cmd *cli.Command) error { client := zavudev.NewClient(getDefaultRequestOptions(cmd)...) unusedArgs := cmd.Args().Slice() @@ -58,8 +129,6 @@ func handleSendersAgentExecutionsList(ctx context.Context, cmd *cli.Command) err return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := zavudev.SenderAgentExecutionListParams{} - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -71,7 +140,10 @@ func handleSendersAgentExecutionsList(ctx context.Context, cmd *cli.Command) err return err } + params := zavudev.SenderAgentExecutionListParams{} + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") if format == "raw" { var res []byte @@ -86,7 +158,13 @@ func handleSendersAgentExecutionsList(ctx context.Context, cmd *cli.Command) err return err } obj := gjson.ParseBytes(res) - return ShowJSON(os.Stdout, "senders:agent:executions list", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "senders:agent:executions list", + Transform: transform, + }) } else { iter := client.Senders.Agent.Executions.ListAutoPaging( ctx, @@ -98,6 +176,12 @@ func handleSendersAgentExecutionsList(ctx context.Context, cmd *cli.Command) err if cmd.IsSet("max-items") { maxItems = cmd.Value("max-items").(int64) } - return ShowJSONIterator(os.Stdout, "senders:agent:executions list", iter, format, transform, maxItems) + return ShowJSONIterator(iter, maxItems, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "senders:agent:executions list", + Transform: transform, + }) } } diff --git a/pkg/cmd/senderagentexecution_test.go b/pkg/cmd/senderagentexecution_test.go index a4c6811..15d7276 100644 --- a/pkg/cmd/senderagentexecution_test.go +++ b/pkg/cmd/senderagentexecution_test.go @@ -8,6 +8,19 @@ import ( "github.com/zavudev/cli/internal/mocktest" ) +func TestSendersAgentExecutionsRetrieve(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "senders:agent:executions", "retrieve", + "--sender-id", "senderId", + "--execution-id", "executionId", + ) + }) +} + func TestSendersAgentExecutionsList(t *testing.T) { t.Skip("Mock server tests are disabled") t.Run("regular flags", func(t *testing.T) { diff --git a/pkg/cmd/senderagentflow.go b/pkg/cmd/senderagentflow.go index 0560010..682c11a 100644 --- a/pkg/cmd/senderagentflow.go +++ b/pkg/cmd/senderagentflow.go @@ -5,7 +5,6 @@ package cmd import ( "context" "fmt" - "os" "github.com/tidwall/gjson" "github.com/urfave/cli/v3" @@ -21,8 +20,9 @@ var sendersAgentFlowsCreate = requestflag.WithInnerFlags(cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "sender-id", - Required: true, + Name: "sender-id", + Required: true, + PathParam: "senderId", }, &requestflag.Flag[string]{ Name: "name", @@ -73,7 +73,7 @@ var sendersAgentFlowsCreate = requestflag.WithInnerFlags(cli.Command{ Usage: "Type of flow step.", InnerField: "type", }, - &requestflag.InnerFlag[any]{ + &requestflag.InnerFlag[*string]{ Name: "step.next-step-id", Usage: "ID of the next step to execute.", InnerField: "nextStepId", @@ -104,12 +104,14 @@ var sendersAgentFlowsRetrieve = cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "sender-id", - Required: true, + Name: "sender-id", + Required: true, + PathParam: "senderId", }, &requestflag.Flag[string]{ - Name: "flow-id", - Required: true, + Name: "flow-id", + Required: true, + PathParam: "flowId", }, }, Action: handleSendersAgentFlowsRetrieve, @@ -122,12 +124,14 @@ var sendersAgentFlowsUpdate = requestflag.WithInnerFlags(cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "sender-id", - Required: true, + Name: "sender-id", + Required: true, + PathParam: "senderId", }, &requestflag.Flag[string]{ - Name: "flow-id", - Required: true, + Name: "flow-id", + Required: true, + PathParam: "flowId", }, &requestflag.Flag[string]{ Name: "description", @@ -173,7 +177,7 @@ var sendersAgentFlowsUpdate = requestflag.WithInnerFlags(cli.Command{ Usage: "Type of flow step.", InnerField: "type", }, - &requestflag.InnerFlag[any]{ + &requestflag.InnerFlag[*string]{ Name: "step.next-step-id", Usage: "ID of the next step to execute.", InnerField: "nextStepId", @@ -204,8 +208,9 @@ var sendersAgentFlowsList = cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "sender-id", - Required: true, + Name: "sender-id", + Required: true, + PathParam: "senderId", }, &requestflag.Flag[string]{ Name: "cursor", @@ -235,12 +240,14 @@ var sendersAgentFlowsDelete = cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "sender-id", - Required: true, + Name: "sender-id", + Required: true, + PathParam: "senderId", }, &requestflag.Flag[string]{ - Name: "flow-id", - Required: true, + Name: "flow-id", + Required: true, + PathParam: "flowId", }, }, Action: handleSendersAgentFlowsDelete, @@ -253,12 +260,14 @@ var sendersAgentFlowsDuplicate = cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "sender-id", - Required: true, + Name: "sender-id", + Required: true, + PathParam: "senderId", }, &requestflag.Flag[string]{ - Name: "flow-id", - Required: true, + Name: "flow-id", + Required: true, + PathParam: "flowId", }, &requestflag.Flag[string]{ Name: "new-name", @@ -281,8 +290,6 @@ func handleSendersAgentFlowsCreate(ctx context.Context, cmd *cli.Command) error return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := zavudev.SenderAgentFlowNewParams{} - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -294,6 +301,8 @@ func handleSendersAgentFlowsCreate(ctx context.Context, cmd *cli.Command) error return err } + params := zavudev.SenderAgentFlowNewParams{} + var res []byte options = append(options, option.WithResponseBodyInto(&res)) _, err = client.Senders.Agent.Flows.New( @@ -308,8 +317,15 @@ func handleSendersAgentFlowsCreate(ctx context.Context, cmd *cli.Command) error obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "senders:agent:flows create", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "senders:agent:flows create", + Transform: transform, + }) } func handleSendersAgentFlowsRetrieve(ctx context.Context, cmd *cli.Command) error { @@ -323,10 +339,6 @@ func handleSendersAgentFlowsRetrieve(ctx context.Context, cmd *cli.Command) erro return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := zavudev.SenderAgentFlowGetParams{ - SenderID: cmd.Value("sender-id").(string), - } - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -338,6 +350,10 @@ func handleSendersAgentFlowsRetrieve(ctx context.Context, cmd *cli.Command) erro return err } + params := zavudev.SenderAgentFlowGetParams{ + SenderID: cmd.Value("sender-id").(string), + } + var res []byte options = append(options, option.WithResponseBodyInto(&res)) _, err = client.Senders.Agent.Flows.Get( @@ -352,8 +368,15 @@ func handleSendersAgentFlowsRetrieve(ctx context.Context, cmd *cli.Command) erro obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "senders:agent:flows retrieve", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "senders:agent:flows retrieve", + Transform: transform, + }) } func handleSendersAgentFlowsUpdate(ctx context.Context, cmd *cli.Command) error { @@ -367,10 +390,6 @@ func handleSendersAgentFlowsUpdate(ctx context.Context, cmd *cli.Command) error return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := zavudev.SenderAgentFlowUpdateParams{ - SenderID: cmd.Value("sender-id").(string), - } - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -382,6 +401,10 @@ func handleSendersAgentFlowsUpdate(ctx context.Context, cmd *cli.Command) error return err } + params := zavudev.SenderAgentFlowUpdateParams{ + SenderID: cmd.Value("sender-id").(string), + } + var res []byte options = append(options, option.WithResponseBodyInto(&res)) _, err = client.Senders.Agent.Flows.Update( @@ -396,8 +419,15 @@ func handleSendersAgentFlowsUpdate(ctx context.Context, cmd *cli.Command) error obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "senders:agent:flows update", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "senders:agent:flows update", + Transform: transform, + }) } func handleSendersAgentFlowsList(ctx context.Context, cmd *cli.Command) error { @@ -411,8 +441,6 @@ func handleSendersAgentFlowsList(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := zavudev.SenderAgentFlowListParams{} - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -424,7 +452,10 @@ func handleSendersAgentFlowsList(ctx context.Context, cmd *cli.Command) error { return err } + params := zavudev.SenderAgentFlowListParams{} + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") if format == "raw" { var res []byte @@ -439,7 +470,13 @@ func handleSendersAgentFlowsList(ctx context.Context, cmd *cli.Command) error { return err } obj := gjson.ParseBytes(res) - return ShowJSON(os.Stdout, "senders:agent:flows list", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "senders:agent:flows list", + Transform: transform, + }) } else { iter := client.Senders.Agent.Flows.ListAutoPaging( ctx, @@ -451,7 +488,13 @@ func handleSendersAgentFlowsList(ctx context.Context, cmd *cli.Command) error { if cmd.IsSet("max-items") { maxItems = cmd.Value("max-items").(int64) } - return ShowJSONIterator(os.Stdout, "senders:agent:flows list", iter, format, transform, maxItems) + return ShowJSONIterator(iter, maxItems, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "senders:agent:flows list", + Transform: transform, + }) } } @@ -466,10 +509,6 @@ func handleSendersAgentFlowsDelete(ctx context.Context, cmd *cli.Command) error return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := zavudev.SenderAgentFlowDeleteParams{ - SenderID: cmd.Value("sender-id").(string), - } - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -481,6 +520,10 @@ func handleSendersAgentFlowsDelete(ctx context.Context, cmd *cli.Command) error return err } + params := zavudev.SenderAgentFlowDeleteParams{ + SenderID: cmd.Value("sender-id").(string), + } + return client.Senders.Agent.Flows.Delete( ctx, cmd.Value("flow-id").(string), @@ -500,10 +543,6 @@ func handleSendersAgentFlowsDuplicate(ctx context.Context, cmd *cli.Command) err return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := zavudev.SenderAgentFlowDuplicateParams{ - SenderID: cmd.Value("sender-id").(string), - } - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -515,6 +554,10 @@ func handleSendersAgentFlowsDuplicate(ctx context.Context, cmd *cli.Command) err return err } + params := zavudev.SenderAgentFlowDuplicateParams{ + SenderID: cmd.Value("sender-id").(string), + } + var res []byte options = append(options, option.WithResponseBodyInto(&res)) _, err = client.Senders.Agent.Flows.Duplicate( @@ -529,6 +572,13 @@ func handleSendersAgentFlowsDuplicate(ctx context.Context, cmd *cli.Command) err obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "senders:agent:flows duplicate", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "senders:agent:flows duplicate", + Transform: transform, + }) } diff --git a/pkg/cmd/senderagentknowledgebase.go b/pkg/cmd/senderagentknowledgebase.go index 2a2902f..fa8f1d3 100644 --- a/pkg/cmd/senderagentknowledgebase.go +++ b/pkg/cmd/senderagentknowledgebase.go @@ -5,7 +5,6 @@ package cmd import ( "context" "fmt" - "os" "github.com/tidwall/gjson" "github.com/urfave/cli/v3" @@ -21,8 +20,9 @@ var sendersAgentKnowledgeBasesCreate = cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "sender-id", - Required: true, + Name: "sender-id", + Required: true, + PathParam: "senderId", }, &requestflag.Flag[string]{ Name: "name", @@ -44,12 +44,14 @@ var sendersAgentKnowledgeBasesRetrieve = cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "sender-id", - Required: true, + Name: "sender-id", + Required: true, + PathParam: "senderId", }, &requestflag.Flag[string]{ - Name: "kb-id", - Required: true, + Name: "kb-id", + Required: true, + PathParam: "kbId", }, }, Action: handleSendersAgentKnowledgeBasesRetrieve, @@ -62,14 +64,16 @@ var sendersAgentKnowledgeBasesUpdate = cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "sender-id", - Required: true, + Name: "sender-id", + Required: true, + PathParam: "senderId", }, &requestflag.Flag[string]{ - Name: "kb-id", - Required: true, + Name: "kb-id", + Required: true, + PathParam: "kbId", }, - &requestflag.Flag[any]{ + &requestflag.Flag[*string]{ Name: "description", BodyPath: "description", }, @@ -88,8 +92,9 @@ var sendersAgentKnowledgeBasesList = cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "sender-id", - Required: true, + Name: "sender-id", + Required: true, + PathParam: "senderId", }, &requestflag.Flag[string]{ Name: "cursor", @@ -115,12 +120,14 @@ var sendersAgentKnowledgeBasesDelete = cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "sender-id", - Required: true, + Name: "sender-id", + Required: true, + PathParam: "senderId", }, &requestflag.Flag[string]{ - Name: "kb-id", - Required: true, + Name: "kb-id", + Required: true, + PathParam: "kbId", }, }, Action: handleSendersAgentKnowledgeBasesDelete, @@ -138,8 +145,6 @@ func handleSendersAgentKnowledgeBasesCreate(ctx context.Context, cmd *cli.Comman return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := zavudev.SenderAgentKnowledgeBaseNewParams{} - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -151,6 +156,8 @@ func handleSendersAgentKnowledgeBasesCreate(ctx context.Context, cmd *cli.Comman return err } + params := zavudev.SenderAgentKnowledgeBaseNewParams{} + var res []byte options = append(options, option.WithResponseBodyInto(&res)) _, err = client.Senders.Agent.KnowledgeBases.New( @@ -165,8 +172,15 @@ func handleSendersAgentKnowledgeBasesCreate(ctx context.Context, cmd *cli.Comman obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "senders:agent:knowledge-bases create", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "senders:agent:knowledge-bases create", + Transform: transform, + }) } func handleSendersAgentKnowledgeBasesRetrieve(ctx context.Context, cmd *cli.Command) error { @@ -180,10 +194,6 @@ func handleSendersAgentKnowledgeBasesRetrieve(ctx context.Context, cmd *cli.Comm return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := zavudev.SenderAgentKnowledgeBaseGetParams{ - SenderID: cmd.Value("sender-id").(string), - } - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -195,6 +205,10 @@ func handleSendersAgentKnowledgeBasesRetrieve(ctx context.Context, cmd *cli.Comm return err } + params := zavudev.SenderAgentKnowledgeBaseGetParams{ + SenderID: cmd.Value("sender-id").(string), + } + var res []byte options = append(options, option.WithResponseBodyInto(&res)) _, err = client.Senders.Agent.KnowledgeBases.Get( @@ -209,8 +223,15 @@ func handleSendersAgentKnowledgeBasesRetrieve(ctx context.Context, cmd *cli.Comm obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "senders:agent:knowledge-bases retrieve", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "senders:agent:knowledge-bases retrieve", + Transform: transform, + }) } func handleSendersAgentKnowledgeBasesUpdate(ctx context.Context, cmd *cli.Command) error { @@ -224,10 +245,6 @@ func handleSendersAgentKnowledgeBasesUpdate(ctx context.Context, cmd *cli.Comman return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := zavudev.SenderAgentKnowledgeBaseUpdateParams{ - SenderID: cmd.Value("sender-id").(string), - } - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -239,6 +256,10 @@ func handleSendersAgentKnowledgeBasesUpdate(ctx context.Context, cmd *cli.Comman return err } + params := zavudev.SenderAgentKnowledgeBaseUpdateParams{ + SenderID: cmd.Value("sender-id").(string), + } + var res []byte options = append(options, option.WithResponseBodyInto(&res)) _, err = client.Senders.Agent.KnowledgeBases.Update( @@ -253,8 +274,15 @@ func handleSendersAgentKnowledgeBasesUpdate(ctx context.Context, cmd *cli.Comman obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "senders:agent:knowledge-bases update", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "senders:agent:knowledge-bases update", + Transform: transform, + }) } func handleSendersAgentKnowledgeBasesList(ctx context.Context, cmd *cli.Command) error { @@ -268,8 +296,6 @@ func handleSendersAgentKnowledgeBasesList(ctx context.Context, cmd *cli.Command) return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := zavudev.SenderAgentKnowledgeBaseListParams{} - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -281,7 +307,10 @@ func handleSendersAgentKnowledgeBasesList(ctx context.Context, cmd *cli.Command) return err } + params := zavudev.SenderAgentKnowledgeBaseListParams{} + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") if format == "raw" { var res []byte @@ -296,7 +325,13 @@ func handleSendersAgentKnowledgeBasesList(ctx context.Context, cmd *cli.Command) return err } obj := gjson.ParseBytes(res) - return ShowJSON(os.Stdout, "senders:agent:knowledge-bases list", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "senders:agent:knowledge-bases list", + Transform: transform, + }) } else { iter := client.Senders.Agent.KnowledgeBases.ListAutoPaging( ctx, @@ -308,7 +343,13 @@ func handleSendersAgentKnowledgeBasesList(ctx context.Context, cmd *cli.Command) if cmd.IsSet("max-items") { maxItems = cmd.Value("max-items").(int64) } - return ShowJSONIterator(os.Stdout, "senders:agent:knowledge-bases list", iter, format, transform, maxItems) + return ShowJSONIterator(iter, maxItems, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "senders:agent:knowledge-bases list", + Transform: transform, + }) } } @@ -323,10 +364,6 @@ func handleSendersAgentKnowledgeBasesDelete(ctx context.Context, cmd *cli.Comman return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := zavudev.SenderAgentKnowledgeBaseDeleteParams{ - SenderID: cmd.Value("sender-id").(string), - } - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -338,6 +375,10 @@ func handleSendersAgentKnowledgeBasesDelete(ctx context.Context, cmd *cli.Comman return err } + params := zavudev.SenderAgentKnowledgeBaseDeleteParams{ + SenderID: cmd.Value("sender-id").(string), + } + return client.Senders.Agent.KnowledgeBases.Delete( ctx, cmd.Value("kb-id").(string), diff --git a/pkg/cmd/senderagentknowledgebasedocument.go b/pkg/cmd/senderagentknowledgebasedocument.go index 873079d..060b88c 100644 --- a/pkg/cmd/senderagentknowledgebasedocument.go +++ b/pkg/cmd/senderagentknowledgebasedocument.go @@ -5,7 +5,6 @@ package cmd import ( "context" "fmt" - "os" "github.com/tidwall/gjson" "github.com/urfave/cli/v3" @@ -21,12 +20,14 @@ var sendersAgentKnowledgeBasesDocumentsCreate = cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "sender-id", - Required: true, + Name: "sender-id", + Required: true, + PathParam: "senderId", }, &requestflag.Flag[string]{ - Name: "kb-id", - Required: true, + Name: "kb-id", + Required: true, + PathParam: "kbId", }, &requestflag.Flag[string]{ Name: "content", @@ -49,12 +50,14 @@ var sendersAgentKnowledgeBasesDocumentsList = cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "sender-id", - Required: true, + Name: "sender-id", + Required: true, + PathParam: "senderId", }, &requestflag.Flag[string]{ - Name: "kb-id", - Required: true, + Name: "kb-id", + Required: true, + PathParam: "kbId", }, &requestflag.Flag[string]{ Name: "cursor", @@ -80,22 +83,83 @@ var sendersAgentKnowledgeBasesDocumentsDelete = cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "sender-id", - Required: true, + Name: "sender-id", + Required: true, + PathParam: "senderId", }, &requestflag.Flag[string]{ - Name: "kb-id", - Required: true, + Name: "kb-id", + Required: true, + PathParam: "kbId", }, &requestflag.Flag[string]{ - Name: "doc-id", - Required: true, + Name: "doc-id", + Required: true, + PathParam: "docId", }, }, Action: handleSendersAgentKnowledgeBasesDocumentsDelete, HideHelpCommand: true, } +var sendersAgentKnowledgeBasesDocumentsRetrieveDocument = cli.Command{ + Name: "retrieve-document", + Usage: "Get a single document from a knowledge base.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "sender-id", + Required: true, + PathParam: "senderId", + }, + &requestflag.Flag[string]{ + Name: "kb-id", + Required: true, + PathParam: "kbId", + }, + &requestflag.Flag[string]{ + Name: "doc-id", + Required: true, + PathParam: "docId", + }, + }, + Action: handleSendersAgentKnowledgeBasesDocumentsRetrieveDocument, + HideHelpCommand: true, +} + +var sendersAgentKnowledgeBasesDocumentsUpdateDocument = cli.Command{ + Name: "update-document", + Usage: "Update a document's title or content. Updating content reprocesses the document\nfor RAG.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "sender-id", + Required: true, + PathParam: "senderId", + }, + &requestflag.Flag[string]{ + Name: "kb-id", + Required: true, + PathParam: "kbId", + }, + &requestflag.Flag[string]{ + Name: "doc-id", + Required: true, + PathParam: "docId", + }, + &requestflag.Flag[string]{ + Name: "content", + BodyPath: "content", + }, + &requestflag.Flag[string]{ + Name: "title", + BodyPath: "title", + }, + }, + Action: handleSendersAgentKnowledgeBasesDocumentsUpdateDocument, + HideHelpCommand: true, +} + func handleSendersAgentKnowledgeBasesDocumentsCreate(ctx context.Context, cmd *cli.Command) error { client := zavudev.NewClient(getDefaultRequestOptions(cmd)...) unusedArgs := cmd.Args().Slice() @@ -107,10 +171,6 @@ func handleSendersAgentKnowledgeBasesDocumentsCreate(ctx context.Context, cmd *c return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := zavudev.SenderAgentKnowledgeBaseDocumentNewParams{ - SenderID: cmd.Value("sender-id").(string), - } - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -122,6 +182,10 @@ func handleSendersAgentKnowledgeBasesDocumentsCreate(ctx context.Context, cmd *c return err } + params := zavudev.SenderAgentKnowledgeBaseDocumentNewParams{ + SenderID: cmd.Value("sender-id").(string), + } + var res []byte options = append(options, option.WithResponseBodyInto(&res)) _, err = client.Senders.Agent.KnowledgeBases.Documents.New( @@ -136,8 +200,15 @@ func handleSendersAgentKnowledgeBasesDocumentsCreate(ctx context.Context, cmd *c obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "senders:agent:knowledge-bases:documents create", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "senders:agent:knowledge-bases:documents create", + Transform: transform, + }) } func handleSendersAgentKnowledgeBasesDocumentsList(ctx context.Context, cmd *cli.Command) error { @@ -151,10 +222,6 @@ func handleSendersAgentKnowledgeBasesDocumentsList(ctx context.Context, cmd *cli return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := zavudev.SenderAgentKnowledgeBaseDocumentListParams{ - SenderID: cmd.Value("sender-id").(string), - } - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -166,7 +233,12 @@ func handleSendersAgentKnowledgeBasesDocumentsList(ctx context.Context, cmd *cli return err } + params := zavudev.SenderAgentKnowledgeBaseDocumentListParams{ + SenderID: cmd.Value("sender-id").(string), + } + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") if format == "raw" { var res []byte @@ -181,7 +253,13 @@ func handleSendersAgentKnowledgeBasesDocumentsList(ctx context.Context, cmd *cli return err } obj := gjson.ParseBytes(res) - return ShowJSON(os.Stdout, "senders:agent:knowledge-bases:documents list", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "senders:agent:knowledge-bases:documents list", + Transform: transform, + }) } else { iter := client.Senders.Agent.KnowledgeBases.Documents.ListAutoPaging( ctx, @@ -193,7 +271,13 @@ func handleSendersAgentKnowledgeBasesDocumentsList(ctx context.Context, cmd *cli if cmd.IsSet("max-items") { maxItems = cmd.Value("max-items").(int64) } - return ShowJSONIterator(os.Stdout, "senders:agent:knowledge-bases:documents list", iter, format, transform, maxItems) + return ShowJSONIterator(iter, maxItems, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "senders:agent:knowledge-bases:documents list", + Transform: transform, + }) } } @@ -208,11 +292,41 @@ func handleSendersAgentKnowledgeBasesDocumentsDelete(ctx context.Context, cmd *c return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } + options, err := flagOptions( + cmd, + apiquery.NestedQueryFormatBrackets, + apiquery.ArrayQueryFormatComma, + EmptyBody, + false, + ) + if err != nil { + return err + } + params := zavudev.SenderAgentKnowledgeBaseDocumentDeleteParams{ SenderID: cmd.Value("sender-id").(string), KBID: cmd.Value("kb-id").(string), } + return client.Senders.Agent.KnowledgeBases.Documents.Delete( + ctx, + cmd.Value("doc-id").(string), + params, + options..., + ) +} + +func handleSendersAgentKnowledgeBasesDocumentsRetrieveDocument(ctx context.Context, cmd *cli.Command) error { + client := zavudev.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("doc-id") && len(unusedArgs) > 0 { + cmd.Set("doc-id", unusedArgs[0]) + unusedArgs = unusedArgs[1:] + } + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -224,10 +338,84 @@ func handleSendersAgentKnowledgeBasesDocumentsDelete(ctx context.Context, cmd *c return err } - return client.Senders.Agent.KnowledgeBases.Documents.Delete( + params := zavudev.SenderAgentKnowledgeBaseDocumentGetDocumentParams{ + SenderID: cmd.Value("sender-id").(string), + KBID: cmd.Value("kb-id").(string), + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Senders.Agent.KnowledgeBases.Documents.GetDocument( ctx, cmd.Value("doc-id").(string), params, options..., ) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") + transform := cmd.Root().String("transform") + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "senders:agent:knowledge-bases:documents retrieve-document", + Transform: transform, + }) +} + +func handleSendersAgentKnowledgeBasesDocumentsUpdateDocument(ctx context.Context, cmd *cli.Command) error { + client := zavudev.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("doc-id") && len(unusedArgs) > 0 { + cmd.Set("doc-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, + ApplicationJSON, + false, + ) + if err != nil { + return err + } + + params := zavudev.SenderAgentKnowledgeBaseDocumentUpdateDocumentParams{ + SenderID: cmd.Value("sender-id").(string), + KBID: cmd.Value("kb-id").(string), + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Senders.Agent.KnowledgeBases.Documents.UpdateDocument( + ctx, + cmd.Value("doc-id").(string), + params, + options..., + ) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") + transform := cmd.Root().String("transform") + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "senders:agent:knowledge-bases:documents update-document", + Transform: transform, + }) } diff --git a/pkg/cmd/senderagentknowledgebasedocument_test.go b/pkg/cmd/senderagentknowledgebasedocument_test.go index 629e0f2..ee9403d 100644 --- a/pkg/cmd/senderagentknowledgebasedocument_test.go +++ b/pkg/cmd/senderagentknowledgebasedocument_test.go @@ -66,3 +66,48 @@ func TestSendersAgentKnowledgeBasesDocumentsDelete(t *testing.T) { ) }) } + +func TestSendersAgentKnowledgeBasesDocumentsRetrieveDocument(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "senders:agent:knowledge-bases:documents", "retrieve-document", + "--sender-id", "senderId", + "--kb-id", "kbId", + "--doc-id", "docId", + ) + }) +} + +func TestSendersAgentKnowledgeBasesDocumentsUpdateDocument(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "senders:agent:knowledge-bases:documents", "update-document", + "--sender-id", "senderId", + "--kb-id", "kbId", + "--doc-id", "docId", + "--content", "content", + "--title", "title", + ) + }) + + t.Run("piping data", func(t *testing.T) { + // Test piping YAML data over stdin + pipeData := []byte("" + + "content: content\n" + + "title: title\n") + mocktest.TestRunMockTestWithPipeAndFlags( + t, pipeData, + "--api-key", "string", + "senders:agent:knowledge-bases:documents", "update-document", + "--sender-id", "senderId", + "--kb-id", "kbId", + "--doc-id", "docId", + ) + }) +} diff --git a/pkg/cmd/senderagenttool.go b/pkg/cmd/senderagenttool.go index 60b970c..e6c8d94 100644 --- a/pkg/cmd/senderagenttool.go +++ b/pkg/cmd/senderagenttool.go @@ -5,7 +5,6 @@ package cmd import ( "context" "fmt" - "os" "github.com/tidwall/gjson" "github.com/urfave/cli/v3" @@ -21,8 +20,9 @@ var sendersAgentToolsCreate = requestflag.WithInnerFlags(cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "sender-id", - Required: true, + Name: "sender-id", + Required: true, + PathParam: "senderId", }, &requestflag.Flag[string]{ Name: "description", @@ -52,7 +52,7 @@ var sendersAgentToolsCreate = requestflag.WithInnerFlags(cli.Command{ }, &requestflag.Flag[string]{ Name: "webhook-secret", - Usage: "Optional secret for webhook signature verification.", + Usage: "Signing secret for the webhook. Optional: Zavu generates one when omitted and returns it on this response only. Supply your own if you already have a secret you want reused.", BodyPath: "webhookSecret", }, }, @@ -82,12 +82,14 @@ var sendersAgentToolsRetrieve = cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "sender-id", - Required: true, + Name: "sender-id", + Required: true, + PathParam: "senderId", }, &requestflag.Flag[string]{ - Name: "tool-id", - Required: true, + Name: "tool-id", + Required: true, + PathParam: "toolId", }, }, Action: handleSendersAgentToolsRetrieve, @@ -100,12 +102,14 @@ var sendersAgentToolsUpdate = requestflag.WithInnerFlags(cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "sender-id", - Required: true, + Name: "sender-id", + Required: true, + PathParam: "senderId", }, &requestflag.Flag[string]{ - Name: "tool-id", - Required: true, + Name: "tool-id", + Required: true, + PathParam: "toolId", }, &requestflag.Flag[string]{ Name: "description", @@ -123,7 +127,7 @@ var sendersAgentToolsUpdate = requestflag.WithInnerFlags(cli.Command{ Name: "parameters", BodyPath: "parameters", }, - &requestflag.Flag[any]{ + &requestflag.Flag[*string]{ Name: "webhook-secret", BodyPath: "webhookSecret", }, @@ -158,8 +162,9 @@ var sendersAgentToolsList = cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "sender-id", - Required: true, + Name: "sender-id", + Required: true, + PathParam: "senderId", }, &requestflag.Flag[string]{ Name: "cursor", @@ -189,30 +194,59 @@ var sendersAgentToolsDelete = cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "sender-id", - Required: true, + Name: "sender-id", + Required: true, + PathParam: "senderId", }, &requestflag.Flag[string]{ - Name: "tool-id", - Required: true, + Name: "tool-id", + Required: true, + PathParam: "toolId", }, }, Action: handleSendersAgentToolsDelete, HideHelpCommand: true, } +var sendersAgentToolsListTestRuns = cli.Command{ + Name: "list-test-runs", + Usage: "Recent runs of this tool triggered from the test endpoint, newest first. Covers\nmanual tests only: a tool called by an agent during a real conversation is not\nrecorded here.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "sender-id", + Required: true, + PathParam: "senderId", + }, + &requestflag.Flag[string]{ + Name: "tool-id", + Required: true, + PathParam: "toolId", + }, + &requestflag.Flag[int64]{ + Name: "limit", + Default: 20, + QueryPath: "limit", + }, + }, + Action: handleSendersAgentToolsListTestRuns, + HideHelpCommand: true, +} + var sendersAgentToolsTest = cli.Command{ Name: "test", - Usage: "Test a tool by triggering its webhook with test parameters.", + Usage: "Run a tool with the parameters you supply and return what it answered.", Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "sender-id", - Required: true, + Name: "sender-id", + Required: true, + PathParam: "senderId", }, &requestflag.Flag[string]{ - Name: "tool-id", - Required: true, + Name: "tool-id", + Required: true, + PathParam: "toolId", }, &requestflag.Flag[map[string]any]{ Name: "test-params", @@ -236,8 +270,6 @@ func handleSendersAgentToolsCreate(ctx context.Context, cmd *cli.Command) error return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := zavudev.SenderAgentToolNewParams{} - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -249,6 +281,8 @@ func handleSendersAgentToolsCreate(ctx context.Context, cmd *cli.Command) error return err } + params := zavudev.SenderAgentToolNewParams{} + var res []byte options = append(options, option.WithResponseBodyInto(&res)) _, err = client.Senders.Agent.Tools.New( @@ -263,8 +297,15 @@ func handleSendersAgentToolsCreate(ctx context.Context, cmd *cli.Command) error obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "senders:agent:tools create", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "senders:agent:tools create", + Transform: transform, + }) } func handleSendersAgentToolsRetrieve(ctx context.Context, cmd *cli.Command) error { @@ -278,10 +319,6 @@ func handleSendersAgentToolsRetrieve(ctx context.Context, cmd *cli.Command) erro return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := zavudev.SenderAgentToolGetParams{ - SenderID: cmd.Value("sender-id").(string), - } - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -293,6 +330,10 @@ func handleSendersAgentToolsRetrieve(ctx context.Context, cmd *cli.Command) erro return err } + params := zavudev.SenderAgentToolGetParams{ + SenderID: cmd.Value("sender-id").(string), + } + var res []byte options = append(options, option.WithResponseBodyInto(&res)) _, err = client.Senders.Agent.Tools.Get( @@ -307,8 +348,15 @@ func handleSendersAgentToolsRetrieve(ctx context.Context, cmd *cli.Command) erro obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "senders:agent:tools retrieve", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "senders:agent:tools retrieve", + Transform: transform, + }) } func handleSendersAgentToolsUpdate(ctx context.Context, cmd *cli.Command) error { @@ -322,10 +370,6 @@ func handleSendersAgentToolsUpdate(ctx context.Context, cmd *cli.Command) error return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := zavudev.SenderAgentToolUpdateParams{ - SenderID: cmd.Value("sender-id").(string), - } - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -337,6 +381,10 @@ func handleSendersAgentToolsUpdate(ctx context.Context, cmd *cli.Command) error return err } + params := zavudev.SenderAgentToolUpdateParams{ + SenderID: cmd.Value("sender-id").(string), + } + var res []byte options = append(options, option.WithResponseBodyInto(&res)) _, err = client.Senders.Agent.Tools.Update( @@ -351,8 +399,15 @@ func handleSendersAgentToolsUpdate(ctx context.Context, cmd *cli.Command) error obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "senders:agent:tools update", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "senders:agent:tools update", + Transform: transform, + }) } func handleSendersAgentToolsList(ctx context.Context, cmd *cli.Command) error { @@ -366,8 +421,6 @@ func handleSendersAgentToolsList(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := zavudev.SenderAgentToolListParams{} - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -379,7 +432,10 @@ func handleSendersAgentToolsList(ctx context.Context, cmd *cli.Command) error { return err } + params := zavudev.SenderAgentToolListParams{} + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") if format == "raw" { var res []byte @@ -394,7 +450,13 @@ func handleSendersAgentToolsList(ctx context.Context, cmd *cli.Command) error { return err } obj := gjson.ParseBytes(res) - return ShowJSON(os.Stdout, "senders:agent:tools list", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "senders:agent:tools list", + Transform: transform, + }) } else { iter := client.Senders.Agent.Tools.ListAutoPaging( ctx, @@ -406,7 +468,13 @@ func handleSendersAgentToolsList(ctx context.Context, cmd *cli.Command) error { if cmd.IsSet("max-items") { maxItems = cmd.Value("max-items").(int64) } - return ShowJSONIterator(os.Stdout, "senders:agent:tools list", iter, format, transform, maxItems) + return ShowJSONIterator(iter, maxItems, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "senders:agent:tools list", + Transform: transform, + }) } } @@ -421,10 +489,40 @@ func handleSendersAgentToolsDelete(ctx context.Context, cmd *cli.Command) error return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } + options, err := flagOptions( + cmd, + apiquery.NestedQueryFormatBrackets, + apiquery.ArrayQueryFormatComma, + EmptyBody, + false, + ) + if err != nil { + return err + } + params := zavudev.SenderAgentToolDeleteParams{ SenderID: cmd.Value("sender-id").(string), } + return client.Senders.Agent.Tools.Delete( + ctx, + cmd.Value("tool-id").(string), + params, + options..., + ) +} + +func handleSendersAgentToolsListTestRuns(ctx context.Context, cmd *cli.Command) error { + client := zavudev.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("tool-id") && len(unusedArgs) > 0 { + cmd.Set("tool-id", unusedArgs[0]) + unusedArgs = unusedArgs[1:] + } + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -436,12 +534,33 @@ func handleSendersAgentToolsDelete(ctx context.Context, cmd *cli.Command) error return err } - return client.Senders.Agent.Tools.Delete( + params := zavudev.SenderAgentToolListTestRunsParams{ + SenderID: cmd.Value("sender-id").(string), + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Senders.Agent.Tools.ListTestRuns( ctx, cmd.Value("tool-id").(string), params, options..., ) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") + transform := cmd.Root().String("transform") + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "senders:agent:tools list-test-runs", + Transform: transform, + }) } func handleSendersAgentToolsTest(ctx context.Context, cmd *cli.Command) error { @@ -455,10 +574,6 @@ func handleSendersAgentToolsTest(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := zavudev.SenderAgentToolTestParams{ - SenderID: cmd.Value("sender-id").(string), - } - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -470,6 +585,10 @@ func handleSendersAgentToolsTest(ctx context.Context, cmd *cli.Command) error { return err } + params := zavudev.SenderAgentToolTestParams{ + SenderID: cmd.Value("sender-id").(string), + } + var res []byte options = append(options, option.WithResponseBodyInto(&res)) _, err = client.Senders.Agent.Tools.Test( @@ -484,6 +603,13 @@ func handleSendersAgentToolsTest(ctx context.Context, cmd *cli.Command) error { obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "senders:agent:tools test", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "senders:agent:tools test", + Transform: transform, + }) } diff --git a/pkg/cmd/senderagenttool_test.go b/pkg/cmd/senderagenttool_test.go index 0bc643f..a621896 100644 --- a/pkg/cmd/senderagenttool_test.go +++ b/pkg/cmd/senderagenttool_test.go @@ -180,6 +180,20 @@ func TestSendersAgentToolsDelete(t *testing.T) { }) } +func TestSendersAgentToolsListTestRuns(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "senders:agent:tools", "list-test-runs", + "--sender-id", "senderId", + "--tool-id", "toolId", + "--limit", "100", + ) + }) +} + func TestSendersAgentToolsTest(t *testing.T) { t.Skip("Mock server tests are disabled") t.Run("regular flags", func(t *testing.T) { diff --git a/pkg/cmd/senderagenttoolwebhook.go b/pkg/cmd/senderagenttoolwebhook.go new file mode 100644 index 0000000..737dd9f --- /dev/null +++ b/pkg/cmd/senderagenttoolwebhook.go @@ -0,0 +1,86 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "context" + "fmt" + + "github.com/tidwall/gjson" + "github.com/urfave/cli/v3" + "github.com/zavudev/cli/internal/apiquery" + "github.com/zavudev/cli/internal/requestflag" + "github.com/zavudev/sdk-go" + "github.com/zavudev/sdk-go/option" +) + +var sendersAgentToolsWebhookRotateSecret = cli.Command{ + Name: "rotate-secret", + Usage: "Generate a new signing secret for this tool. The previous one stops working on\nthe next call, with no overlap, so update your endpoint first. The tool keeps\nits id, so flows that reference it by name are unaffected.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "sender-id", + Required: true, + PathParam: "senderId", + }, + &requestflag.Flag[string]{ + Name: "tool-id", + Required: true, + PathParam: "toolId", + }, + }, + Action: handleSendersAgentToolsWebhookRotateSecret, + HideHelpCommand: true, +} + +func handleSendersAgentToolsWebhookRotateSecret(ctx context.Context, cmd *cli.Command) error { + client := zavudev.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("tool-id") && len(unusedArgs) > 0 { + cmd.Set("tool-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 + } + + params := zavudev.SenderAgentToolWebhookRotateSecretParams{ + SenderID: cmd.Value("sender-id").(string), + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Senders.Agent.Tools.Webhook.RotateSecret( + ctx, + cmd.Value("tool-id").(string), + params, + options..., + ) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") + transform := cmd.Root().String("transform") + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "senders:agent:tools:webhook rotate-secret", + Transform: transform, + }) +} diff --git a/pkg/cmd/senderagenttoolwebhook_test.go b/pkg/cmd/senderagenttoolwebhook_test.go new file mode 100644 index 0000000..e62e6b7 --- /dev/null +++ b/pkg/cmd/senderagenttoolwebhook_test.go @@ -0,0 +1,22 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "testing" + + "github.com/zavudev/cli/internal/mocktest" +) + +func TestSendersAgentToolsWebhookRotateSecret(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "senders:agent:tools:webhook", "rotate-secret", + "--sender-id", "senderId", + "--tool-id", "toolId", + ) + }) +} diff --git a/pkg/cmd/sendertelegram.go b/pkg/cmd/sendertelegram.go new file mode 100644 index 0000000..c219efe --- /dev/null +++ b/pkg/cmd/sendertelegram.go @@ -0,0 +1,125 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "context" + "fmt" + + "github.com/tidwall/gjson" + "github.com/urfave/cli/v3" + "github.com/zavudev/cli/internal/apiquery" + "github.com/zavudev/cli/internal/requestflag" + "github.com/zavudev/sdk-go" + "github.com/zavudev/sdk-go/option" +) + +var sendersTelegramConnect = cli.Command{ + Name: "connect", + Usage: "Connect a Telegram bot to a sender. Provide the bot token from @BotFather; Zavu\nvalidates it, registers the webhook, and routes the sender's Telegram messages\nthrough it.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "sender-id", + Required: true, + PathParam: "senderId", + }, + &requestflag.Flag[string]{ + Name: "bot-token", + Usage: "Bot token from @BotFather.", + Required: true, + BodyPath: "botToken", + }, + }, + Action: handleSendersTelegramConnect, + HideHelpCommand: true, +} + +var sendersTelegramDisconnect = cli.Command{ + Name: "disconnect", + Usage: "Disconnect Telegram from a sender and remove the webhook.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "sender-id", + Required: true, + PathParam: "senderId", + }, + }, + Action: handleSendersTelegramDisconnect, + HideHelpCommand: true, +} + +func handleSendersTelegramConnect(ctx context.Context, cmd *cli.Command) error { + client := zavudev.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("sender-id") && len(unusedArgs) > 0 { + cmd.Set("sender-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, + ApplicationJSON, + false, + ) + if err != nil { + return err + } + + params := zavudev.SenderTelegramConnectParams{} + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Senders.Telegram.Connect( + ctx, + cmd.Value("sender-id").(string), + params, + options..., + ) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") + transform := cmd.Root().String("transform") + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "senders:telegram connect", + Transform: transform, + }) +} + +func handleSendersTelegramDisconnect(ctx context.Context, cmd *cli.Command) error { + client := zavudev.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("sender-id") && len(unusedArgs) > 0 { + cmd.Set("sender-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.Senders.Telegram.Disconnect(ctx, cmd.Value("sender-id").(string), options...) +} diff --git a/pkg/cmd/sendertelegram_test.go b/pkg/cmd/sendertelegram_test.go new file mode 100644 index 0000000..2d07970 --- /dev/null +++ b/pkg/cmd/sendertelegram_test.go @@ -0,0 +1,45 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "testing" + + "github.com/zavudev/cli/internal/mocktest" +) + +func TestSendersTelegramConnect(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "senders:telegram", "connect", + "--sender-id", "senderId", + "--bot-token", "botToken", + ) + }) + + t.Run("piping data", func(t *testing.T) { + // Test piping YAML data over stdin + pipeData := []byte("botToken: botToken") + mocktest.TestRunMockTestWithPipeAndFlags( + t, pipeData, + "--api-key", "string", + "senders:telegram", "connect", + "--sender-id", "senderId", + ) + }) +} + +func TestSendersTelegramDisconnect(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "senders:telegram", "disconnect", + "--sender-id", "senderId", + ) + }) +} diff --git a/pkg/cmd/senderwhatsappsync.go b/pkg/cmd/senderwhatsappsync.go new file mode 100644 index 0000000..287ccdf --- /dev/null +++ b/pkg/cmd/senderwhatsappsync.go @@ -0,0 +1,186 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "context" + "fmt" + + "github.com/tidwall/gjson" + "github.com/urfave/cli/v3" + "github.com/zavudev/cli/internal/apiquery" + "github.com/zavudev/cli/internal/requestflag" + "github.com/zavudev/sdk-go" + "github.com/zavudev/sdk-go/option" +) + +var sendersWhatsappSyncRetrieve = cli.Command{ + Name: "retrieve", + Usage: "Get the current sync status for a sender's WhatsApp coexistence account. Only\navailable for senders connected in coexistence mode (WhatsApp Business App +\nCloud API).", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "sender-id", + Required: true, + PathParam: "senderId", + }, + }, + Action: handleSendersWhatsappSyncRetrieve, + HideHelpCommand: true, +} + +var sendersWhatsappSyncStartContactsSync = cli.Command{ + Name: "start-contacts-sync", + Usage: "Initiate contact names sync from the WhatsApp Business App. This imports contact\nnames stored in the app to Zavu. Only available for coexistence accounts with\nactive status.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "sender-id", + Required: true, + PathParam: "senderId", + }, + }, + Action: handleSendersWhatsappSyncStartContactsSync, + HideHelpCommand: true, +} + +var sendersWhatsappSyncStartHistorySync = cli.Command{ + Name: "start-history-sync", + Usage: "Initiate message history sync from the WhatsApp Business App. This sends a\nrequest to the account owner to approve sharing their conversation history. Only\navailable for coexistence accounts with active status.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "sender-id", + Required: true, + PathParam: "senderId", + }, + }, + Action: handleSendersWhatsappSyncStartHistorySync, + HideHelpCommand: true, +} + +func handleSendersWhatsappSyncRetrieve(ctx context.Context, cmd *cli.Command) error { + client := zavudev.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("sender-id") && len(unusedArgs) > 0 { + cmd.Set("sender-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 + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Senders.WhatsappSync.Get(ctx, cmd.Value("sender-id").(string), options...) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") + transform := cmd.Root().String("transform") + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "senders:whatsapp-sync retrieve", + Transform: transform, + }) +} + +func handleSendersWhatsappSyncStartContactsSync(ctx context.Context, cmd *cli.Command) error { + client := zavudev.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("sender-id") && len(unusedArgs) > 0 { + cmd.Set("sender-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 + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Senders.WhatsappSync.StartContactsSync(ctx, cmd.Value("sender-id").(string), options...) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") + transform := cmd.Root().String("transform") + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "senders:whatsapp-sync start-contacts-sync", + Transform: transform, + }) +} + +func handleSendersWhatsappSyncStartHistorySync(ctx context.Context, cmd *cli.Command) error { + client := zavudev.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("sender-id") && len(unusedArgs) > 0 { + cmd.Set("sender-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 + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Senders.WhatsappSync.StartHistorySync(ctx, cmd.Value("sender-id").(string), options...) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") + transform := cmd.Root().String("transform") + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "senders:whatsapp-sync start-history-sync", + Transform: transform, + }) +} diff --git a/pkg/cmd/senderwhatsappsync_test.go b/pkg/cmd/senderwhatsappsync_test.go new file mode 100644 index 0000000..7f7edb1 --- /dev/null +++ b/pkg/cmd/senderwhatsappsync_test.go @@ -0,0 +1,45 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "testing" + + "github.com/zavudev/cli/internal/mocktest" +) + +func TestSendersWhatsappSyncRetrieve(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "senders:whatsapp-sync", "retrieve", + "--sender-id", "senderId", + ) + }) +} + +func TestSendersWhatsappSyncStartContactsSync(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "senders:whatsapp-sync", "start-contacts-sync", + "--sender-id", "senderId", + ) + }) +} + +func TestSendersWhatsappSyncStartHistorySync(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "senders:whatsapp-sync", "start-history-sync", + "--sender-id", "senderId", + ) + }) +} diff --git a/pkg/cmd/subaccount.go b/pkg/cmd/subaccount.go new file mode 100644 index 0000000..fe52978 --- /dev/null +++ b/pkg/cmd/subaccount.go @@ -0,0 +1,420 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "context" + "fmt" + + "github.com/tidwall/gjson" + "github.com/urfave/cli/v3" + "github.com/zavudev/cli/internal/apiquery" + "github.com/zavudev/cli/internal/requestflag" + "github.com/zavudev/sdk-go" + "github.com/zavudev/sdk-go/option" +) + +var subAccountsCreate = cli.Command{ + Name: "create", + Usage: "Create a new sub-account (project) with its own API key. All charges are billed\nto the parent team's balance. Use creditLimit to set a spending cap. The\nsub-account's API key is returned only in the creation response. Requires a\nparent project API key; sub-account API keys receive HTTP 403.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "name", + Usage: "Name of the sub-account.", + Required: true, + BodyPath: "name", + }, + &requestflag.Flag[int64]{ + Name: "credit-limit", + Usage: "Spending cap in cents. When reached, messages from this sub-account will be blocked. Omit or set to 0 for no limit.", + BodyPath: "creditLimit", + }, + &requestflag.Flag[string]{ + Name: "external-id", + Usage: "External reference ID for your own tracking.", + BodyPath: "externalId", + }, + &requestflag.Flag[map[string]any]{ + Name: "metadata", + BodyPath: "metadata", + }, + }, + Action: handleSubAccountsCreate, + HideHelpCommand: true, +} + +var subAccountsRetrieve = cli.Command{ + Name: "retrieve", + Usage: "Get sub-account. Requires a parent project API key; sub-account API keys receive\nHTTP 403.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "id", + Required: true, + PathParam: "id", + }, + }, + Action: handleSubAccountsRetrieve, + HideHelpCommand: true, +} + +var subAccountsUpdate = cli.Command{ + Name: "update", + Usage: "Update sub-account. Requires a parent project API key; sub-account API keys\nreceive HTTP 403.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "id", + Required: true, + PathParam: "id", + }, + &requestflag.Flag[*int64]{ + Name: "credit-limit", + BodyPath: "creditLimit", + }, + &requestflag.Flag[string]{ + Name: "external-id", + BodyPath: "externalId", + }, + &requestflag.Flag[map[string]any]{ + Name: "metadata", + BodyPath: "metadata", + }, + &requestflag.Flag[string]{ + Name: "name", + BodyPath: "name", + }, + &requestflag.Flag[string]{ + Name: "status", + Usage: `Allowed values: "active", "inactive".`, + BodyPath: "status", + }, + }, + Action: handleSubAccountsUpdate, + HideHelpCommand: true, +} + +var subAccountsList = cli.Command{ + Name: "list", + Usage: "List sub-accounts for this team. Requires a parent project API key; sub-account\nAPI keys receive HTTP 403.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "cursor", + QueryPath: "cursor", + }, + &requestflag.Flag[int64]{ + Name: "limit", + Default: 50, + QueryPath: "limit", + }, + &requestflag.Flag[int64]{ + Name: "max-items", + Usage: "The maximum number of items to return (use -1 for unlimited).", + }, + }, + Action: handleSubAccountsList, + HideHelpCommand: true, +} + +var subAccountsDeactivate = cli.Command{ + Name: "deactivate", + Usage: "Deactivate a sub-account. Remaining balance is returned to the parent team and\nall API keys are revoked. Requires a parent project API key; sub-account API\nkeys receive HTTP 403.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "id", + Required: true, + PathParam: "id", + }, + }, + Action: handleSubAccountsDeactivate, + HideHelpCommand: true, +} + +var subAccountsGetBalance = cli.Command{ + Name: "get-balance", + Usage: "Get spending information for a sub-account. Returns the parent team's balance,\nthe sub-account's total spending, and its credit limit (spending cap). Requires\na parent project API key; sub-account API keys receive HTTP 403.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "id", + Required: true, + PathParam: "id", + }, + }, + Action: handleSubAccountsGetBalance, + HideHelpCommand: true, +} + +func handleSubAccountsCreate(ctx context.Context, cmd *cli.Command) error { + client := zavudev.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + + options, err := flagOptions( + cmd, + apiquery.NestedQueryFormatBrackets, + apiquery.ArrayQueryFormatComma, + ApplicationJSON, + false, + ) + if err != nil { + return err + } + + params := zavudev.SubAccountNewParams{} + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.SubAccounts.New(ctx, params, options...) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") + transform := cmd.Root().String("transform") + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "sub-accounts create", + Transform: transform, + }) +} + +func handleSubAccountsRetrieve(ctx context.Context, cmd *cli.Command) error { + client := zavudev.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("id") && len(unusedArgs) > 0 { + cmd.Set("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 + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.SubAccounts.Get(ctx, cmd.Value("id").(string), options...) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") + transform := cmd.Root().String("transform") + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "sub-accounts retrieve", + Transform: transform, + }) +} + +func handleSubAccountsUpdate(ctx context.Context, cmd *cli.Command) error { + client := zavudev.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("id") && len(unusedArgs) > 0 { + cmd.Set("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, + ApplicationJSON, + false, + ) + if err != nil { + return err + } + + params := zavudev.SubAccountUpdateParams{} + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.SubAccounts.Update( + ctx, + cmd.Value("id").(string), + params, + options..., + ) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") + transform := cmd.Root().String("transform") + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "sub-accounts update", + Transform: transform, + }) +} + +func handleSubAccountsList(ctx context.Context, cmd *cli.Command) error { + client := zavudev.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + + 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 + } + + params := zavudev.SubAccountListParams{} + + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") + transform := cmd.Root().String("transform") + if format == "raw" { + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.SubAccounts.List(ctx, params, options...) + if err != nil { + return err + } + obj := gjson.ParseBytes(res) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "sub-accounts list", + Transform: transform, + }) + } else { + iter := client.SubAccounts.ListAutoPaging(ctx, params, options...) + maxItems := int64(-1) + if cmd.IsSet("max-items") { + maxItems = cmd.Value("max-items").(int64) + } + return ShowJSONIterator(iter, maxItems, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "sub-accounts list", + Transform: transform, + }) + } +} + +func handleSubAccountsDeactivate(ctx context.Context, cmd *cli.Command) error { + client := zavudev.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("id") && len(unusedArgs) > 0 { + cmd.Set("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 + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.SubAccounts.Deactivate(ctx, cmd.Value("id").(string), options...) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") + transform := cmd.Root().String("transform") + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "sub-accounts deactivate", + Transform: transform, + }) +} + +func handleSubAccountsGetBalance(ctx context.Context, cmd *cli.Command) error { + client := zavudev.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("id") && len(unusedArgs) > 0 { + cmd.Set("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 + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.SubAccounts.GetBalance(ctx, cmd.Value("id").(string), options...) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") + transform := cmd.Root().String("transform") + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "sub-accounts get-balance", + Transform: transform, + }) +} diff --git a/pkg/cmd/subaccount_test.go b/pkg/cmd/subaccount_test.go new file mode 100644 index 0000000..97de900 --- /dev/null +++ b/pkg/cmd/subaccount_test.go @@ -0,0 +1,123 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "testing" + + "github.com/zavudev/cli/internal/mocktest" +) + +func TestSubAccountsCreate(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "sub-accounts", "create", + "--name", "Client ABC", + "--credit-limit", "0", + "--external-id", "externalId", + "--metadata", "{foo: bar}", + ) + }) + + t.Run("piping data", func(t *testing.T) { + // Test piping YAML data over stdin + pipeData := []byte("" + + "name: Client ABC\n" + + "creditLimit: 0\n" + + "externalId: externalId\n" + + "metadata:\n" + + " foo: bar\n") + mocktest.TestRunMockTestWithPipeAndFlags( + t, pipeData, + "--api-key", "string", + "sub-accounts", "create", + ) + }) +} + +func TestSubAccountsRetrieve(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "sub-accounts", "retrieve", + "--id", "id", + ) + }) +} + +func TestSubAccountsUpdate(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "sub-accounts", "update", + "--id", "id", + "--credit-limit", "0", + "--external-id", "externalId", + "--metadata", "{foo: bar}", + "--name", "name", + "--status", "active", + ) + }) + + t.Run("piping data", func(t *testing.T) { + // Test piping YAML data over stdin + pipeData := []byte("" + + "creditLimit: 0\n" + + "externalId: externalId\n" + + "metadata:\n" + + " foo: bar\n" + + "name: name\n" + + "status: active\n") + mocktest.TestRunMockTestWithPipeAndFlags( + t, pipeData, + "--api-key", "string", + "sub-accounts", "update", + "--id", "id", + ) + }) +} + +func TestSubAccountsList(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "sub-accounts", "list", + "--max-items", "10", + "--cursor", "cursor", + "--limit", "100", + ) + }) +} + +func TestSubAccountsDeactivate(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "sub-accounts", "deactivate", + "--id", "id", + ) + }) +} + +func TestSubAccountsGetBalance(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "sub-accounts", "get-balance", + "--id", "id", + ) + }) +} diff --git a/pkg/cmd/subaccountapikey.go b/pkg/cmd/subaccountapikey.go new file mode 100644 index 0000000..abe4d7b --- /dev/null +++ b/pkg/cmd/subaccountapikey.go @@ -0,0 +1,205 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "context" + "fmt" + + "github.com/tidwall/gjson" + "github.com/urfave/cli/v3" + "github.com/zavudev/cli/internal/apiquery" + "github.com/zavudev/cli/internal/requestflag" + "github.com/zavudev/sdk-go" + "github.com/zavudev/sdk-go/option" +) + +var subAccountsAPIKeysCreate = cli.Command{ + Name: "create", + Usage: "Create sub-account API key. Requires a parent project API key; sub-account API\nkeys receive HTTP 403.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "id", + Required: true, + PathParam: "id", + }, + &requestflag.Flag[string]{ + Name: "name", + Required: true, + BodyPath: "name", + }, + &requestflag.Flag[string]{ + Name: "environment", + Usage: `Allowed values: "live", "test".`, + Default: "live", + BodyPath: "environment", + }, + &requestflag.Flag[[]string]{ + Name: "permission", + BodyPath: "permissions", + }, + }, + Action: handleSubAccountsAPIKeysCreate, + HideHelpCommand: true, +} + +var subAccountsAPIKeysList = cli.Command{ + Name: "list", + Usage: "List sub-account API keys. Requires a parent project API key; sub-account API\nkeys receive HTTP 403.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "id", + Required: true, + PathParam: "id", + }, + }, + Action: handleSubAccountsAPIKeysList, + HideHelpCommand: true, +} + +var subAccountsAPIKeysRevoke = cli.Command{ + Name: "revoke", + Usage: "Revoke sub-account API key. Requires a parent project API key; sub-account API\nkeys receive HTTP 403.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "id", + Required: true, + PathParam: "id", + }, + &requestflag.Flag[string]{ + Name: "key-id", + Required: true, + PathParam: "keyId", + }, + }, + Action: handleSubAccountsAPIKeysRevoke, + HideHelpCommand: true, +} + +func handleSubAccountsAPIKeysCreate(ctx context.Context, cmd *cli.Command) error { + client := zavudev.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("id") && len(unusedArgs) > 0 { + cmd.Set("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, + ApplicationJSON, + false, + ) + if err != nil { + return err + } + + params := zavudev.SubAccountAPIKeyNewParams{} + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.SubAccounts.APIKeys.New( + ctx, + cmd.Value("id").(string), + params, + options..., + ) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") + transform := cmd.Root().String("transform") + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "sub-accounts:api-keys create", + Transform: transform, + }) +} + +func handleSubAccountsAPIKeysList(ctx context.Context, cmd *cli.Command) error { + client := zavudev.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("id") && len(unusedArgs) > 0 { + cmd.Set("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 + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.SubAccounts.APIKeys.List(ctx, cmd.Value("id").(string), options...) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") + transform := cmd.Root().String("transform") + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "sub-accounts:api-keys list", + Transform: transform, + }) +} + +func handleSubAccountsAPIKeysRevoke(ctx context.Context, cmd *cli.Command) error { + client := zavudev.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("key-id") && len(unusedArgs) > 0 { + cmd.Set("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 + } + + params := zavudev.SubAccountAPIKeyRevokeParams{ + ID: cmd.Value("id").(string), + } + + return client.SubAccounts.APIKeys.Revoke( + ctx, + cmd.Value("key-id").(string), + params, + options..., + ) +} diff --git a/pkg/cmd/subaccountapikey_test.go b/pkg/cmd/subaccountapikey_test.go new file mode 100644 index 0000000..f0f37f1 --- /dev/null +++ b/pkg/cmd/subaccountapikey_test.go @@ -0,0 +1,64 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "testing" + + "github.com/zavudev/cli/internal/mocktest" +) + +func TestSubAccountsAPIKeysCreate(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "sub-accounts:api-keys", "create", + "--id", "id", + "--name", "Production Key", + "--environment", "live", + "--permission", "string", + ) + }) + + t.Run("piping data", func(t *testing.T) { + // Test piping YAML data over stdin + pipeData := []byte("" + + "name: Production Key\n" + + "environment: live\n" + + "permissions:\n" + + " - string\n") + mocktest.TestRunMockTestWithPipeAndFlags( + t, pipeData, + "--api-key", "string", + "sub-accounts:api-keys", "create", + "--id", "id", + ) + }) +} + +func TestSubAccountsAPIKeysList(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "sub-accounts:api-keys", "list", + "--id", "id", + ) + }) +} + +func TestSubAccountsAPIKeysRevoke(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "sub-accounts:api-keys", "revoke", + "--id", "id", + "--key-id", "keyId", + ) + }) +} diff --git a/pkg/cmd/template.go b/pkg/cmd/template.go index 02b9b20..88eac51 100644 --- a/pkg/cmd/template.go +++ b/pkg/cmd/template.go @@ -5,7 +5,6 @@ package cmd import ( "context" "fmt" - "os" "github.com/tidwall/gjson" "github.com/urfave/cli/v3" @@ -52,6 +51,21 @@ var templatesCreate = requestflag.WithInnerFlags(cli.Command{ Usage: "Code expiration time in minutes. Only for AUTHENTICATION templates.", BodyPath: "codeExpirationMinutes", }, + &requestflag.Flag[string]{ + Name: "footer", + Usage: "Footer text for the template.", + BodyPath: "footer", + }, + &requestflag.Flag[string]{ + Name: "header-content", + Usage: "Header content (text string or media URL).", + BodyPath: "headerContent", + }, + &requestflag.Flag[string]{ + Name: "header-type", + Usage: "Type of header for the template.", + BodyPath: "headerType", + }, &requestflag.Flag[string]{ Name: "instagram-body", Usage: "Channel-specific body for Instagram. Falls back to `body` if not set.", @@ -81,15 +95,16 @@ var templatesCreate = requestflag.WithInnerFlags(cli.Command{ HideHelpCommand: true, }, map[string][]requestflag.HasOuterFlag{ "button": { - &requestflag.InnerFlag[string]{ - Name: "button.text", - InnerField: "text", - }, &requestflag.InnerFlag[string]{ Name: "button.type", - Usage: `Allowed values: "quick_reply", "url", "phone", "otp".`, + Usage: "`request_contact_info` renders a fixed **Share Contact Info** button that asks the recipient to share their phone number — useful when a contact adopted a WhatsApp username and you only know their BSUID. It takes no other fields.", InnerField: "type", }, + &requestflag.InnerFlag[string]{ + Name: "button.example", + Usage: "Sample value Meta uses to review templates with a dynamic URL button. Substituted into `{{1}}` of the URL when the template is submitted to Meta. Only meaningful when `url` contains `{{1}}`; ignored for static URLs.", + InnerField: "example", + }, &requestflag.InnerFlag[string]{ Name: "button.otp-type", Usage: "Required when type is 'otp'. COPY_CODE shows copy button, ONE_TAP enables Android autofill.", @@ -109,8 +124,14 @@ var templatesCreate = requestflag.WithInnerFlags(cli.Command{ Usage: "Android app signature hash. Required for ONE_TAP buttons.", InnerField: "signatureHash", }, + &requestflag.InnerFlag[string]{ + Name: "button.text", + Usage: "Button label. Required for every type except `request_contact_info`, whose label is fixed by WhatsApp.", + InnerField: "text", + }, &requestflag.InnerFlag[string]{ Name: "button.url", + Usage: "Button destination. Use `{{1}}` exactly once for a dynamic URL (e.g. `https://example.com/orders/{{1}}`); WhatsApp only accepts the strict `{{1}}` form. Static URLs must not contain any `{{...}}` placeholder.", InnerField: "url", }, }, @@ -122,8 +143,9 @@ var templatesRetrieve = cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "template-id", - Required: true, + Name: "template-id", + Required: true, + PathParam: "templateId", }, }, Action: handleTemplatesRetrieve, @@ -159,8 +181,9 @@ var templatesDelete = cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "template-id", - Required: true, + Name: "template-id", + Required: true, + PathParam: "templateId", }, }, Action: handleTemplatesDelete, @@ -173,8 +196,9 @@ var templatesSubmit = cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "template-id", - Required: true, + Name: "template-id", + Required: true, + PathParam: "templateId", }, &requestflag.Flag[string]{ Name: "sender-id", @@ -192,6 +216,21 @@ var templatesSubmit = cli.Command{ HideHelpCommand: true, } +var templatesSync = cli.Command{ + Name: "sync", + Usage: "Reconcile this project's templates against WhatsApp. Two things happen per\nconnected WhatsApp Business Account: templates that exist on Meta but not in\nZavu are imported (or linked to an existing template with the same name), and\nthe approval status of the templates Zavu already knows about is refreshed from\nMeta.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "sender-id", + Usage: "Sync only the WhatsApp Business Account attached to this sender. If omitted, every WhatsApp sender in the project is synced.", + BodyPath: "senderId", + }, + }, + Action: handleTemplatesSync, + HideHelpCommand: true, +} + func handleTemplatesCreate(ctx context.Context, cmd *cli.Command) error { client := zavudev.NewClient(getDefaultRequestOptions(cmd)...) unusedArgs := cmd.Args().Slice() @@ -200,8 +239,6 @@ func handleTemplatesCreate(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := zavudev.TemplateNewParams{} - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -213,6 +250,8 @@ func handleTemplatesCreate(ctx context.Context, cmd *cli.Command) error { return err } + params := zavudev.TemplateNewParams{} + var res []byte options = append(options, option.WithResponseBodyInto(&res)) _, err = client.Templates.New(ctx, params, options...) @@ -222,8 +261,15 @@ func handleTemplatesCreate(ctx context.Context, cmd *cli.Command) error { obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "templates create", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "templates create", + Transform: transform, + }) } func handleTemplatesRetrieve(ctx context.Context, cmd *cli.Command) error { @@ -257,8 +303,15 @@ func handleTemplatesRetrieve(ctx context.Context, cmd *cli.Command) error { obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "templates retrieve", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "templates retrieve", + Transform: transform, + }) } func handleTemplatesList(ctx context.Context, cmd *cli.Command) error { @@ -269,8 +322,6 @@ func handleTemplatesList(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := zavudev.TemplateListParams{} - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -282,7 +333,10 @@ func handleTemplatesList(ctx context.Context, cmd *cli.Command) error { return err } + params := zavudev.TemplateListParams{} + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") if format == "raw" { var res []byte @@ -292,14 +346,26 @@ func handleTemplatesList(ctx context.Context, cmd *cli.Command) error { return err } obj := gjson.ParseBytes(res) - return ShowJSON(os.Stdout, "templates list", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "templates list", + Transform: transform, + }) } else { iter := client.Templates.ListAutoPaging(ctx, params, options...) maxItems := int64(-1) if cmd.IsSet("max-items") { maxItems = cmd.Value("max-items").(int64) } - return ShowJSONIterator(os.Stdout, "templates list", iter, format, transform, maxItems) + return ShowJSONIterator(iter, maxItems, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "templates list", + Transform: transform, + }) } } @@ -339,8 +405,6 @@ func handleTemplatesSubmit(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := zavudev.TemplateSubmitParams{} - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -352,6 +416,8 @@ func handleTemplatesSubmit(ctx context.Context, cmd *cli.Command) error { return err } + params := zavudev.TemplateSubmitParams{} + var res []byte options = append(options, option.WithResponseBodyInto(&res)) _, err = client.Templates.Submit( @@ -366,6 +432,54 @@ func handleTemplatesSubmit(ctx context.Context, cmd *cli.Command) error { obj := gjson.ParseBytes(res) format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") + transform := cmd.Root().String("transform") + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "templates submit", + Transform: transform, + }) +} + +func handleTemplatesSync(ctx context.Context, cmd *cli.Command) error { + client := zavudev.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + + options, err := flagOptions( + cmd, + apiquery.NestedQueryFormatBrackets, + apiquery.ArrayQueryFormatComma, + ApplicationJSON, + false, + ) + if err != nil { + return err + } + + params := zavudev.TemplateSyncParams{} + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Templates.Sync(ctx, params, options...) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, "templates submit", obj, format, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "templates sync", + Transform: transform, + }) } diff --git a/pkg/cmd/template_test.go b/pkg/cmd/template_test.go index a3c5661..cbba832 100644 --- a/pkg/cmd/template_test.go +++ b/pkg/cmd/template_test.go @@ -20,8 +20,11 @@ func TestTemplatesCreate(t *testing.T) { "--language", "en", "--name", "order_confirmation", "--add-security-recommendation=true", - "--button", "{text: text, type: quick_reply, otpType: COPY_CODE, packageName: packageName, phoneNumber: phoneNumber, signatureHash: signatureHash, url: https://example.com}", + "--button", "{type: quick_reply, example: ORD-12345, otpType: COPY_CODE, packageName: packageName, phoneNumber: phoneNumber, signatureHash: signatureHash, text: text, url: https://example.com}", "--code-expiration-minutes", "1", + "--footer", "footer", + "--header-content", "headerContent", + "--header-type", "text", "--instagram-body", "instagramBody", "--sms-body", "smsBody", "--telegram-body", "telegramBody", @@ -44,14 +47,18 @@ func TestTemplatesCreate(t *testing.T) { "--language", "en", "--name", "order_confirmation", "--add-security-recommendation=true", - "--button.text", "text", "--button.type", "quick_reply", + "--button.example", "ORD-12345", "--button.otp-type", "COPY_CODE", "--button.package-name", "packageName", "--button.phone-number", "phoneNumber", "--button.signature-hash", "signatureHash", + "--button.text", "text", "--button.url", "https://example.com", "--code-expiration-minutes", "1", + "--footer", "footer", + "--header-content", "headerContent", + "--header-type", "text", "--instagram-body", "instagramBody", "--sms-body", "smsBody", "--telegram-body", "telegramBody", @@ -69,14 +76,18 @@ func TestTemplatesCreate(t *testing.T) { "name: order_confirmation\n" + "addSecurityRecommendation: true\n" + "buttons:\n" + - " - text: text\n" + - " type: quick_reply\n" + + " - type: quick_reply\n" + + " example: ORD-12345\n" + " otpType: COPY_CODE\n" + " packageName: packageName\n" + " phoneNumber: phoneNumber\n" + " signatureHash: signatureHash\n" + + " text: text\n" + " url: https://example.com\n" + "codeExpirationMinutes: 1\n" + + "footer: footer\n" + + "headerContent: headerContent\n" + + "headerType: text\n" + "instagramBody: instagramBody\n" + "smsBody: smsBody\n" + "telegramBody: telegramBody\n" + @@ -156,3 +167,25 @@ func TestTemplatesSubmit(t *testing.T) { ) }) } + +func TestTemplatesSync(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "templates", "sync", + "--sender-id", "sender_12345", + ) + }) + + t.Run("piping data", func(t *testing.T) { + // Test piping YAML data over stdin + pipeData := []byte("senderId: sender_12345") + mocktest.TestRunMockTestWithPipeAndFlags( + t, pipeData, + "--api-key", "string", + "templates", "sync", + ) + }) +} diff --git a/pkg/cmd/url.go b/pkg/cmd/url.go new file mode 100644 index 0000000..2d4aa70 --- /dev/null +++ b/pkg/cmd/url.go @@ -0,0 +1,282 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "context" + "fmt" + + "github.com/tidwall/gjson" + "github.com/urfave/cli/v3" + "github.com/zavudev/cli/internal/apiquery" + "github.com/zavudev/cli/internal/requestflag" + "github.com/zavudev/sdk-go" + "github.com/zavudev/sdk-go/option" +) + +var urlsEscalate = cli.Command{ + Name: "escalate", + Usage: "Request manual review of a rejected URL. Only URLs in 'rejected' status can be\nescalated; the status then moves to 'escalated'.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "url-id", + Required: true, + PathParam: "urlId", + }, + &requestflag.Flag[string]{ + Name: "reason", + Usage: "Why the URL should be reviewed manually.", + Required: true, + BodyPath: "reason", + }, + }, + Action: handleURLsEscalate, + HideHelpCommand: true, +} + +var urlsListVerified = cli.Command{ + Name: "list-verified", + Usage: "List URLs that have been verified for this project.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "cursor", + QueryPath: "cursor", + }, + &requestflag.Flag[int64]{ + Name: "limit", + Default: 50, + QueryPath: "limit", + }, + &requestflag.Flag[string]{ + Name: "status", + Usage: "Filter by verification status.", + QueryPath: "status", + }, + &requestflag.Flag[int64]{ + Name: "max-items", + Usage: "The maximum number of items to return (use -1 for unlimited).", + }, + }, + Action: handleURLsListVerified, + HideHelpCommand: true, +} + +var urlsRetrieveDetails = cli.Command{ + Name: "retrieve-details", + Usage: "Get details of a specific verified URL.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "url-id", + Required: true, + PathParam: "urlId", + }, + }, + Action: handleURLsRetrieveDetails, + HideHelpCommand: true, +} + +var urlsSubmitForVerification = cli.Command{ + Name: "submit-for-verification", + Usage: "Submit a URL for verification. URLs are automatically checked against Google Web\nRisk API. Safe URLs are auto-approved, malicious URLs are blocked. URL\nshorteners (bit.ly, t.co, etc.) are always blocked.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "url", + Usage: "The URL to submit for verification.", + Required: true, + BodyPath: "url", + }, + }, + Action: handleURLsSubmitForVerification, + HideHelpCommand: true, +} + +func handleURLsEscalate(ctx context.Context, cmd *cli.Command) error { + client := zavudev.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("url-id") && len(unusedArgs) > 0 { + cmd.Set("url-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, + ApplicationJSON, + false, + ) + if err != nil { + return err + } + + params := zavudev.URLEscalateParams{} + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.URLs.Escalate( + ctx, + cmd.Value("url-id").(string), + params, + options..., + ) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") + transform := cmd.Root().String("transform") + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "urls escalate", + Transform: transform, + }) +} + +func handleURLsListVerified(ctx context.Context, cmd *cli.Command) error { + client := zavudev.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + + 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 + } + + params := zavudev.URLListVerifiedParams{} + + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") + transform := cmd.Root().String("transform") + if format == "raw" { + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.URLs.ListVerified(ctx, params, options...) + if err != nil { + return err + } + obj := gjson.ParseBytes(res) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "urls list-verified", + Transform: transform, + }) + } else { + iter := client.URLs.ListVerifiedAutoPaging(ctx, params, options...) + maxItems := int64(-1) + if cmd.IsSet("max-items") { + maxItems = cmd.Value("max-items").(int64) + } + return ShowJSONIterator(iter, maxItems, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "urls list-verified", + Transform: transform, + }) + } +} + +func handleURLsRetrieveDetails(ctx context.Context, cmd *cli.Command) error { + client := zavudev.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("url-id") && len(unusedArgs) > 0 { + cmd.Set("url-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 + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.URLs.GetDetails(ctx, cmd.Value("url-id").(string), options...) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") + transform := cmd.Root().String("transform") + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "urls retrieve-details", + Transform: transform, + }) +} + +func handleURLsSubmitForVerification(ctx context.Context, cmd *cli.Command) error { + client := zavudev.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + + options, err := flagOptions( + cmd, + apiquery.NestedQueryFormatBrackets, + apiquery.ArrayQueryFormatComma, + ApplicationJSON, + false, + ) + if err != nil { + return err + } + + params := zavudev.URLSubmitForVerificationParams{} + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.URLs.SubmitForVerification(ctx, params, options...) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") + transform := cmd.Root().String("transform") + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "urls submit-for-verification", + Transform: transform, + }) +} diff --git a/pkg/cmd/url_test.go b/pkg/cmd/url_test.go new file mode 100644 index 0000000..1ad64f3 --- /dev/null +++ b/pkg/cmd/url_test.go @@ -0,0 +1,82 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "testing" + + "github.com/zavudev/cli/internal/mocktest" +) + +func TestURLsEscalate(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "urls", "escalate", + "--url-id", "urlId", + "--reason", "This is our official landing page and was rejected in error.", + ) + }) + + t.Run("piping data", func(t *testing.T) { + // Test piping YAML data over stdin + pipeData := []byte("reason: This is our official landing page and was rejected in error.") + mocktest.TestRunMockTestWithPipeAndFlags( + t, pipeData, + "--api-key", "string", + "urls", "escalate", + "--url-id", "urlId", + ) + }) +} + +func TestURLsListVerified(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "urls", "list-verified", + "--max-items", "10", + "--cursor", "cursor", + "--limit", "100", + "--status", "pending", + ) + }) +} + +func TestURLsRetrieveDetails(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "urls", "retrieve-details", + "--url-id", "urlId", + ) + }) +} + +func TestURLsSubmitForVerification(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "urls", "submit-for-verification", + "--url", "https://example.com/page", + ) + }) + + t.Run("piping data", func(t *testing.T) { + // Test piping YAML data over stdin + pipeData := []byte("url: https://example.com/page") + mocktest.TestRunMockTestWithPipeAndFlags( + t, pipeData, + "--api-key", "string", + "urls", "submit-for-verification", + ) + }) +} diff --git a/pkg/cmd/version.go b/pkg/cmd/version.go index b113456..86845a8 100644 --- a/pkg/cmd/version.go +++ b/pkg/cmd/version.go @@ -2,4 +2,4 @@ package cmd -const Version = "0.4.0" // x-release-please-version +const Version = "0.5.0" // x-release-please-version diff --git a/scripts/bootstrap b/scripts/bootstrap index 9ebb7d3..bbc786d 100755 --- a/scripts/bootstrap +++ b/scripts/bootstrap @@ -4,7 +4,7 @@ set -e cd "$(dirname "$0")/.." -if [ -f "Brewfile" ] && [ "$(uname -s)" = "Darwin" ] && [ "$SKIP_BREW" != "1" ] && [ -t 0 ]; then +if [ -f "Brewfile" ] && [ "$(uname -s)" = "Darwin" ] && [ "${SKIP_BREW:-}" != "1" ] && [ -t 0 ]; then brew bundle check >/dev/null 2>&1 || { echo -n "==> Install Homebrew dependencies? (y/N): " read -r response