From d18d972e88c1a03890fcbe3ba2f0dbffb06f5e48 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 25 Mar 2026 02:05:13 +0000 Subject: [PATCH 01/54] chore(ci): skip lint on metadata-only changes Note that we still want to run tests, as these depend on the metadata. --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 27426133..940785a8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,7 +22,7 @@ jobs: timeout-minutes: 10 name: lint runs-on: ${{ github.repository == 'stainless-sdks/beeper-desktop-api-cli' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} - if: github.event_name == 'push' || github.event.pull_request.head.repo.fork + 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 @@ -49,7 +49,7 @@ jobs: contents: read id-token: write runs-on: ${{ github.repository == 'stainless-sdks/beeper-desktop-api-cli' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} - if: github.event_name == 'push' || github.event.pull_request.head.repo.fork + 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 From c7c519cf80adffc7cebbd3ec220227baf48672d7 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 25 Mar 2026 02:06:05 +0000 Subject: [PATCH 02/54] chore(tests): bump steady to v0.19.7 --- scripts/mock | 6 +++--- scripts/test | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/mock b/scripts/mock index 0f82c957..3732f8e6 100755 --- a/scripts/mock +++ b/scripts/mock @@ -22,9 +22,9 @@ echo "==> Starting mock server with URL ${URL}" # Run steady mock on the given spec if [ "$1" == "--daemon" ]; then # Pre-install the package so the download doesn't eat into the startup timeout - npm exec --package=@stdy/cli@0.19.6 -- steady --version + npm exec --package=@stdy/cli@0.19.7 -- steady --version - npm exec --package=@stdy/cli@0.19.6 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=repeat --validator-query-array-format=repeat --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL" &> .stdy.log & + npm exec --package=@stdy/cli@0.19.7 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=repeat --validator-query-array-format=repeat --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL" &> .stdy.log & # Wait for server to come online via health endpoint (max 30s) echo -n "Waiting for server" @@ -48,5 +48,5 @@ if [ "$1" == "--daemon" ]; then echo else - npm exec --package=@stdy/cli@0.19.6 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=repeat --validator-query-array-format=repeat --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL" + npm exec --package=@stdy/cli@0.19.7 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=repeat --validator-query-array-format=repeat --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL" fi diff --git a/scripts/test b/scripts/test index 9f448cef..0a858e69 100755 --- a/scripts/test +++ b/scripts/test @@ -46,7 +46,7 @@ elif ! steady_is_running ; then echo -e "To run the server, pass in the path or url of your OpenAPI" echo -e "spec to the steady command:" echo - echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.19.6 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-form-array-format=repeat --validator-query-array-format=repeat --validator-form-object-format=brackets --validator-query-object-format=brackets${NC}" + echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.19.7 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-form-array-format=repeat --validator-query-array-format=repeat --validator-form-object-format=brackets --validator-query-object-format=brackets${NC}" echo exit 1 From 03e1c530c7c6e0988d6f9f95a4f115dc178d7463 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 25 Mar 2026 02:25:05 +0000 Subject: [PATCH 03/54] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index 2b39be67..60bb4539 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 23 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-5a8ac7b545c48dc892e5c680303e305254921554dabee848e40a808659dbcf1e.yml openapi_spec_hash: 0103975601aac1445d3a4ef418c5d17a -config_hash: ca148af6be59ec54295b2c5f852a38d1 +config_hash: e342a96262eaf44c54e8bbb93cc8d7a7 From 8ccee5cbcc06978fef3c96a220d18eaae98bd49a Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 25 Mar 2026 02:25:23 +0000 Subject: [PATCH 04/54] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index 60bb4539..16d5bba2 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 23 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-5a8ac7b545c48dc892e5c680303e305254921554dabee848e40a808659dbcf1e.yml openapi_spec_hash: 0103975601aac1445d3a4ef418c5d17a -config_hash: e342a96262eaf44c54e8bbb93cc8d7a7 +config_hash: f99f904573839260bdb6d428bad17613 From 42310f51c4a4a7de7562f5d79b87bdf6cc33fb8f Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 25 Mar 2026 02:25:34 +0000 Subject: [PATCH 05/54] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index 16d5bba2..2c479249 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 23 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-5a8ac7b545c48dc892e5c680303e305254921554dabee848e40a808659dbcf1e.yml openapi_spec_hash: 0103975601aac1445d3a4ef418c5d17a -config_hash: f99f904573839260bdb6d428bad17613 +config_hash: 7d85c0b454fc78a59db6474c5c4d73c6 From c332ed822bc6e94cb521620032bdcf941d4b7593 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 27 Mar 2026 02:07:21 +0000 Subject: [PATCH 06/54] feat: set CLI flag constant values automatically where `x-stainless-const` is set --- internal/requestflag/requestflag.go | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/internal/requestflag/requestflag.go b/internal/requestflag/requestflag.go index 21a8a690..32c13f53 100644 --- a/internal/requestflag/requestflag.go +++ b/internal/requestflag/requestflag.go @@ -37,6 +37,12 @@ type Flag[ 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 + // 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, + // but isn't required to provide it. This is used for single-value enums and `x-stainless-const` + // parameters. + Const bool + // unexported fields for internal use count int // number of times the flag has been set hasBeenSet bool // whether the flag has been set from env or file @@ -229,7 +235,7 @@ func (f *Flag[T]) String() string { } func (f *Flag[T]) IsSet() bool { - return f.hasBeenSet + return f.hasBeenSet || f.Const } func (f *Flag[T]) Names() []string { @@ -255,6 +261,10 @@ func (f *Flag[T]) SetCategory(c string) { var _ cli.RequiredFlag = (*Flag[any])(nil) // Type assertion to ensure interface compliance func (f *Flag[T]) IsRequired() bool { + // Const flags are always auto-set, so never required from the user. + if f.Const { + return false + } // Intentionally don't use `f.Required`, because request flags may be passed // over stdin as well as by flag. if f.BodyPath != "" || f.BodyRoot { @@ -268,6 +278,10 @@ type RequiredFlagOrStdin interface { } func (f *Flag[T]) IsRequiredAsFlagOrStdin() bool { + // Const flags are always auto-set, so never required from the user. + if f.Const { + return false + } return f.Required } From 8b749cf09b595e66687d4030cee0791aff7e14c4 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 27 Mar 2026 02:08:12 +0000 Subject: [PATCH 07/54] chore: omit full usage information when missing required CLI parameters --- pkg/cmd/flagoptions.go | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/pkg/cmd/flagoptions.go b/pkg/cmd/flagoptions.go index 4a39cd01..f74e11e6 100644 --- a/pkg/cmd/flagoptions.go +++ b/pkg/cmd/flagoptions.go @@ -246,17 +246,15 @@ func flagOptions( } if missingFlags := requestflag.GetMissingRequiredFlags(cmd, requestContents.Body); len(missingFlags) > 0 { - var buf bytes.Buffer - cli.HelpPrinter(&buf, cli.SubcommandHelpTemplate, cmd) - usage := buf.String() if len(missingFlags) == 1 { - return nil, fmt.Errorf("%sRequired flag %q not set", usage, missingFlags[0].Names()[0]) + 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 { names = append(names, flag.Names()[0]) } - return nil, fmt.Errorf("%sRequired flags %q not set", usage, strings.Join(names, ", ")) + return nil, fmt.Errorf("Required flags %q not set\nRun '%s --help' for usage information", strings.Join(names, ", "), cmd.FullName()) } } From 180bf353b36de3cfee9e9497dca4558622654e18 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 27 Mar 2026 02:09:39 +0000 Subject: [PATCH 08/54] chore(internal): update multipart form array serialization --- scripts/mock | 4 ++-- scripts/test | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/mock b/scripts/mock index 3732f8e6..58e46285 100755 --- a/scripts/mock +++ b/scripts/mock @@ -24,7 +24,7 @@ if [ "$1" == "--daemon" ]; then # Pre-install the package so the download doesn't eat into the startup timeout npm exec --package=@stdy/cli@0.19.7 -- steady --version - npm exec --package=@stdy/cli@0.19.7 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=repeat --validator-query-array-format=repeat --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL" &> .stdy.log & + npm exec --package=@stdy/cli@0.19.7 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=repeat --validator-form-array-format=repeat --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL" &> .stdy.log & # Wait for server to come online via health endpoint (max 30s) echo -n "Waiting for server" @@ -48,5 +48,5 @@ if [ "$1" == "--daemon" ]; then echo else - npm exec --package=@stdy/cli@0.19.7 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=repeat --validator-query-array-format=repeat --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL" + npm exec --package=@stdy/cli@0.19.7 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=repeat --validator-form-array-format=repeat --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL" fi diff --git a/scripts/test b/scripts/test index 0a858e69..79ceebee 100755 --- a/scripts/test +++ b/scripts/test @@ -46,7 +46,7 @@ elif ! steady_is_running ; then echo -e "To run the server, pass in the path or url of your OpenAPI" echo -e "spec to the steady command:" echo - echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.19.7 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-form-array-format=repeat --validator-query-array-format=repeat --validator-form-object-format=brackets --validator-query-object-format=brackets${NC}" + echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.19.7 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-query-array-format=repeat --validator-form-array-format=repeat --validator-query-object-format=brackets --validator-form-object-format=brackets${NC}" echo exit 1 From 16566c5d80a77de14f33e1895ec72cfa89da99c7 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 28 Mar 2026 02:21:33 +0000 Subject: [PATCH 09/54] fix: fix for off-by-one error in pagination logic --- pkg/cmd/cmdutil.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/pkg/cmd/cmdutil.go b/pkg/cmd/cmdutil.go index 9b241776..cb8183c4 100644 --- a/pkg/cmd/cmdutil.go +++ b/pkg/cmd/cmdutil.go @@ -378,6 +378,7 @@ type HasRawJSON interface { // 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) @@ -393,10 +394,8 @@ func ShowJSONIterator[T any](stdout *os.File, title string, iter jsonview.Iterat usePager := false output := []byte{} numberOfNewlines := 0 - for iter.Next() { - if itemsToDisplay == 0 { - break - } + // -1 is used to signal no limit of items to display + for itemsToDisplay != 0 && iter.Next() { item := iter.Current() var obj gjson.Result if hasRaw, ok := any(item).(HasRawJSON); ok { From 533501d2960fc565d5891d5f610a7feef458d0b2 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 1 Apr 2026 02:18:01 +0000 Subject: [PATCH 10/54] chore(tests): bump steady to v0.20.1 --- scripts/mock | 6 +++--- scripts/test | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/mock b/scripts/mock index 58e46285..5ea72a2e 100755 --- a/scripts/mock +++ b/scripts/mock @@ -22,9 +22,9 @@ echo "==> Starting mock server with URL ${URL}" # Run steady mock on the given spec if [ "$1" == "--daemon" ]; then # Pre-install the package so the download doesn't eat into the startup timeout - npm exec --package=@stdy/cli@0.19.7 -- steady --version + npm exec --package=@stdy/cli@0.20.1 -- steady --version - npm exec --package=@stdy/cli@0.19.7 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=repeat --validator-form-array-format=repeat --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL" &> .stdy.log & + npm exec --package=@stdy/cli@0.20.1 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=repeat --validator-form-array-format=repeat --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL" &> .stdy.log & # Wait for server to come online via health endpoint (max 30s) echo -n "Waiting for server" @@ -48,5 +48,5 @@ if [ "$1" == "--daemon" ]; then echo else - npm exec --package=@stdy/cli@0.19.7 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=repeat --validator-form-array-format=repeat --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL" + npm exec --package=@stdy/cli@0.20.1 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=repeat --validator-form-array-format=repeat --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL" fi diff --git a/scripts/test b/scripts/test index 79ceebee..17f4857f 100755 --- a/scripts/test +++ b/scripts/test @@ -46,7 +46,7 @@ elif ! steady_is_running ; then echo -e "To run the server, pass in the path or url of your OpenAPI" echo -e "spec to the steady command:" echo - echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.19.7 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-query-array-format=repeat --validator-form-array-format=repeat --validator-query-object-format=brackets --validator-form-object-format=brackets${NC}" + echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.20.1 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-query-array-format=repeat --validator-form-array-format=repeat --validator-query-object-format=brackets --validator-form-object-format=brackets${NC}" echo exit 1 From 3a7b085b2130d21ee0a2b434dbaa3efada1e1bb3 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 1 Apr 2026 02:22:27 +0000 Subject: [PATCH 11/54] chore(tests): bump steady to v0.20.2 --- scripts/mock | 6 +++--- scripts/test | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/mock b/scripts/mock index 5ea72a2e..7c58865f 100755 --- a/scripts/mock +++ b/scripts/mock @@ -22,9 +22,9 @@ echo "==> Starting mock server with URL ${URL}" # Run steady mock on the given spec if [ "$1" == "--daemon" ]; then # Pre-install the package so the download doesn't eat into the startup timeout - npm exec --package=@stdy/cli@0.20.1 -- steady --version + npm exec --package=@stdy/cli@0.20.2 -- steady --version - npm exec --package=@stdy/cli@0.20.1 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=repeat --validator-form-array-format=repeat --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL" &> .stdy.log & + npm exec --package=@stdy/cli@0.20.2 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=repeat --validator-form-array-format=repeat --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL" &> .stdy.log & # Wait for server to come online via health endpoint (max 30s) echo -n "Waiting for server" @@ -48,5 +48,5 @@ if [ "$1" == "--daemon" ]; then echo else - npm exec --package=@stdy/cli@0.20.1 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=repeat --validator-form-array-format=repeat --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL" + npm exec --package=@stdy/cli@0.20.2 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=repeat --validator-form-array-format=repeat --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL" fi diff --git a/scripts/test b/scripts/test index 17f4857f..7ba4b3fc 100755 --- a/scripts/test +++ b/scripts/test @@ -46,7 +46,7 @@ elif ! steady_is_running ; then echo -e "To run the server, pass in the path or url of your OpenAPI" echo -e "spec to the steady command:" echo - echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.20.1 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-query-array-format=repeat --validator-form-array-format=repeat --validator-query-object-format=brackets --validator-form-object-format=brackets${NC}" + echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.20.2 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-query-array-format=repeat --validator-form-array-format=repeat --validator-query-object-format=brackets --validator-form-object-format=brackets${NC}" echo exit 1 From 6df00248f562bbbd98e7cbccf8ddd82b35a40ae3 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 2 Apr 2026 02:09:40 +0000 Subject: [PATCH 12/54] fix: handle empty data set using `--format explore` --- internal/jsonview/explorer.go | 4 ++++ internal/jsonview/explorer_test.go | 37 ++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 internal/jsonview/explorer_test.go diff --git a/internal/jsonview/explorer.go b/internal/jsonview/explorer.go index 055541e7..ea900bcc 100644 --- a/internal/jsonview/explorer.go +++ b/internal/jsonview/explorer.go @@ -406,6 +406,10 @@ func (v *JSONViewer) navigateForward() (tea.Model, tea.Cmd) { return v, nil } + if len(tableView.rowData) < 1 { + return v, nil + } + cursor := tableView.table.Cursor() selected := tableView.rowData[cursor] if !v.canNavigateInto(selected) { diff --git a/internal/jsonview/explorer_test.go b/internal/jsonview/explorer_test.go new file mode 100644 index 00000000..3f0e7516 --- /dev/null +++ b/internal/jsonview/explorer_test.go @@ -0,0 +1,37 @@ +package jsonview + +import ( + "testing" + + "github.com/charmbracelet/bubbles/help" + "github.com/tidwall/gjson" +) + +func TestNavigateForward_EmptyRowData(t *testing.T) { + // An empty JSON array produces a TableView with no rows. + emptyArray := gjson.Parse("[]") + view, err := newTableView("", emptyArray, false) + if err != nil { + t.Fatalf("newTableView: %v", err) + } + + viewer := &JSONViewer{ + stack: []JSONView{view}, + root: "test", + help: help.New(), + } + + // Should return without panicking despite the empty data set. + model, cmd := viewer.navigateForward() + if model != viewer { + t.Error("expected same viewer model returned") + } + if cmd != nil { + t.Error("expected nil cmd") + } + + // Stack should remain unchanged (no new view pushed). + if len(viewer.stack) != 1 { + t.Errorf("expected stack length 1, got %d", len(viewer.stack)) + } +} From 4af189dc0e5add5c654204e0e90c91e199de479a Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 2 Apr 2026 02:09:55 +0000 Subject: [PATCH 13/54] fix: use `RawJSON` when iterating items with `--format explore` in the CLI --- internal/jsonview/explorer.go | 34 ++++++++++++++++++++-- internal/jsonview/explorer_test.go | 45 ++++++++++++++++++++++-------- pkg/cmd/cmdutil.go | 6 ++-- 3 files changed, 68 insertions(+), 17 deletions(-) diff --git a/internal/jsonview/explorer.go b/internal/jsonview/explorer.go index ea900bcc..836bb2c7 100644 --- a/internal/jsonview/explorer.go +++ b/internal/jsonview/explorer.go @@ -1,6 +1,7 @@ package jsonview import ( + "bytes" "encoding/json" "errors" "fmt" @@ -309,6 +310,10 @@ func ExploreJSON(title string, json gjson.Result) error { return err } +type hasRawJSON interface { + RawJSON() string +} + // ExploreJSONStream explores JSON data loaded incrementally via an iterator func ExploreJSONStream[T any](title string, it Iterator[T]) error { anyIt := genericToAnyIterator(it) @@ -327,12 +332,12 @@ func ExploreJSONStream[T any](title string, it Iterator[T]) error { return err } - // Convert items to JSON array - jsonBytes, err := json.Marshal(items) + arrayJSONBytes, err := marshalItemsToJSONArray(items) if err != nil { return err } - arrayJSON := gjson.ParseBytes(jsonBytes) + + arrayJSON := gjson.ParseBytes(arrayJSONBytes) view, err := newTableView("", arrayJSON, false) if err != nil { return err @@ -352,6 +357,29 @@ func ExploreJSONStream[T any](title string, it Iterator[T]) error { return err } +func marshalItemsToJSONArray(items []any) ([]byte, error) { + var buf bytes.Buffer + buf.WriteByte('[') + + for i, item := range items { + if i > 0 { + buf.WriteByte(',') + } + if hasRaw, ok := item.(hasRawJSON); ok { + buf.WriteString(hasRaw.RawJSON()) + } else { + jsonData, err := json.Marshal(item) + if err != nil { + return nil, err + } + buf.Write(jsonData) + } + } + + buf.WriteByte(']') + return buf.Bytes(), nil +} + func (v *JSONViewer) current() JSONView { return v.stack[len(v.stack)-1] } func (v *JSONViewer) Init() tea.Cmd { return nil } diff --git a/internal/jsonview/explorer_test.go b/internal/jsonview/explorer_test.go index 3f0e7516..c5592542 100644 --- a/internal/jsonview/explorer_test.go +++ b/internal/jsonview/explorer_test.go @@ -5,15 +5,15 @@ import ( "github.com/charmbracelet/bubbles/help" "github.com/tidwall/gjson" + + "github.com/stretchr/testify/require" ) func TestNavigateForward_EmptyRowData(t *testing.T) { // An empty JSON array produces a TableView with no rows. emptyArray := gjson.Parse("[]") view, err := newTableView("", emptyArray, false) - if err != nil { - t.Fatalf("newTableView: %v", err) - } + require.NoError(t, err) viewer := &JSONViewer{ stack: []JSONView{view}, @@ -23,15 +23,38 @@ func TestNavigateForward_EmptyRowData(t *testing.T) { // Should return without panicking despite the empty data set. model, cmd := viewer.navigateForward() - if model != viewer { - t.Error("expected same viewer model returned") - } - if cmd != nil { - t.Error("expected nil cmd") - } + require.Equal(t, model, viewer, "expected same viewer model returned") + require.Nil(t, cmd) // Stack should remain unchanged (no new view pushed). - if len(viewer.stack) != 1 { - t.Errorf("expected stack length 1, got %d", len(viewer.stack)) + require.Equal(t, 1, len(viewer.stack), "expected stack length 1, got %d", len(viewer.stack)) +} + +// rawJSONItem implements HasRawJSON, returning pre-built JSON. +type rawJSONItem struct { + raw string +} + +func (r rawJSONItem) RawJSON() string { return r.raw } + +func TestMarshalItemsToJSONArray_WithHasRawJSON(t *testing.T) { + items := []any{ + rawJSONItem{raw: `{"id":1,"name":"alice"}`}, + rawJSONItem{raw: `{"id":2,"name":"bob"}`}, } + + got, err := marshalItemsToJSONArray(items) + require.NoError(t, err) + require.JSONEq(t, `[{"id":1,"name":"alice"},{"id":2,"name":"bob"}]`, string(got)) +} + +func TestMarshalItemsToJSONArray_WithoutHasRawJSON(t *testing.T) { + items := []any{ + map[string]any{"id": 1, "name": "alice"}, + map[string]any{"id": 2, "name": "bob"}, + } + + got, err := marshalItemsToJSONArray(items) + require.NoError(t, err) + require.JSONEq(t, `[{"id":1,"name":"alice"},{"id":2,"name":"bob"}]`, string(got)) } diff --git a/pkg/cmd/cmdutil.go b/pkg/cmd/cmdutil.go index cb8183c4..82655ad4 100644 --- a/pkg/cmd/cmdutil.go +++ b/pkg/cmd/cmdutil.go @@ -372,7 +372,7 @@ func countTerminalLines(data []byte, terminalWidth int) int { return bytes.Count([]byte(wrap.String(string(data), terminalWidth)), []byte("\n")) } -type HasRawJSON interface { +type hasRawJSON interface { RawJSON() string } @@ -398,7 +398,7 @@ func ShowJSONIterator[T any](stdout *os.File, title string, iter jsonview.Iterat for itemsToDisplay != 0 && iter.Next() { item := iter.Current() var obj gjson.Result - if hasRaw, ok := any(item).(HasRawJSON); ok { + if hasRaw, ok := any(item).(hasRawJSON); ok { obj = gjson.Parse(hasRaw.RawJSON()) } else { jsonData, err := json.Marshal(item) @@ -445,7 +445,7 @@ func ShowJSONIterator[T any](stdout *os.File, title string, iter jsonview.Iterat } item := iter.Current() var obj gjson.Result - if hasRaw, ok := any(item).(HasRawJSON); ok { + if hasRaw, ok := any(item).(hasRawJSON); ok { obj = gjson.Parse(hasRaw.RawJSON()) } else { jsonData, err := json.Marshal(item) From ed64986c234a83e6c875a1202a7c217df9efa206 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 3 Apr 2026 02:10:33 +0000 Subject: [PATCH 14/54] feat: binary-only parameters become CLI flags that take filenames only --- internal/requestflag/requestflag.go | 10 +++ pkg/cmd/asset.go | 9 +-- pkg/cmd/asset_test.go | 10 ++- pkg/cmd/flagoptions.go | 96 +++++++++++++++++++++++++++++ 4 files changed, 118 insertions(+), 7 deletions(-) diff --git a/internal/requestflag/requestflag.go b/internal/requestflag/requestflag.go index 32c13f53..bdef64f2 100644 --- a/internal/requestflag/requestflag.go +++ b/internal/requestflag/requestflag.go @@ -43,6 +43,11 @@ type Flag[ // parameters. Const bool + // FileInput, when true, indicates that the flag value is always treated as a file path. The file is read + // automatically without requiring the "@" prefix. This is used for parameters with `type: string, format: + // binary` in the OpenAPI spec. + FileInput bool + // unexported fields for internal use count int // number of times the flag has been set hasBeenSet bool // whether the flag has been set from env or file @@ -59,6 +64,7 @@ type InRequest interface { GetHeaderPath() string GetBodyPath() string IsBodyRoot() bool + IsFileInput() bool } func (f Flag[T]) GetQueryPath() string { @@ -77,6 +83,10 @@ func (f Flag[T]) IsBodyRoot() bool { return f.BodyRoot } +func (f Flag[T]) IsFileInput() bool { + return f.FileInput +} + // The values that will be sent in different parts of a request. type RequestContents struct { Queries map[string]any diff --git a/pkg/cmd/asset.go b/pkg/cmd/asset.go index d7fc7790..65023e3d 100644 --- a/pkg/cmd/asset.go +++ b/pkg/cmd/asset.go @@ -53,10 +53,11 @@ var assetsUpload = cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "file", - Usage: "The file to upload (max 500 MB).", - Required: true, - BodyPath: "file", + Name: "file", + Usage: "The file to upload (max 500 MB).", + Required: true, + BodyPath: "file", + FileInput: true, }, &requestflag.Flag[string]{ Name: "file-name", diff --git a/pkg/cmd/asset_test.go b/pkg/cmd/asset_test.go index 2ce574c3..696660a8 100644 --- a/pkg/cmd/asset_test.go +++ b/pkg/cmd/asset_test.go @@ -3,6 +3,7 @@ package cmd import ( + "strings" "testing" "github.com/beeper/desktop-api-cli/internal/mocktest" @@ -46,18 +47,21 @@ func TestAssetsUpload(t *testing.T) { t, "--access-token", "string", "assets", "upload", - "--file", "Example data", + "--file", mocktest.TestFile(t, "Example data"), "--file-name", "fileName", "--mime-type", "mimeType", ) }) t.Run("piping data", func(t *testing.T) { + testFile := mocktest.TestFile(t, "Example data") // Test piping YAML data over stdin - pipeData := []byte("" + + pipeDataStr := "" + "file: Example data\n" + "fileName: fileName\n" + - "mimeType: mimeType\n") + "mimeType: mimeType\n" + pipeDataStr = strings.ReplaceAll(pipeDataStr, "Example data", testFile) + pipeData := []byte(pipeDataStr) mocktest.TestRunMockTestWithPipeAndFlags( t, pipeData, "--access-token", "string", diff --git a/pkg/cmd/flagoptions.go b/pkg/cmd/flagoptions.go index f74e11e6..24ed79a5 100644 --- a/pkg/cmd/flagoptions.go +++ b/pkg/cmd/flagoptions.go @@ -98,6 +98,21 @@ func embedFilesValue(v reflect.Value, embedStyle FileEmbedStyle) (reflect.Value, return result, nil case reflect.String: + // FilePathValue is always treated as a file path without needing the "@" prefix. + // These only appear on binary upload parameters (multipart/octet-stream), which + // always use EmbedIOReader. + if v.Type() == reflect.TypeOf(FilePathValue("")) { + s := v.String() + if s == "" { + return v, nil + } + content, err := os.ReadFile(s) + if err != nil { + return v, err + } + return reflect.ValueOf(string(content)), nil + } + s := v.String() if literal, ok := strings.CutPrefix(s, "\\@"); ok { // Allow for escaped @ signs if you don't want them to be treated as files @@ -258,6 +273,12 @@ func flagOptions( } } + // For flags marked as FileInput (type: string, format: binary), the value is always + // a file path. Wrap with FilePathValue so embedFiles reads the file automatically + // without requiring the user to type the "@" prefix. This handles both values set + // via explicit CLI flags and values that arrived via piped YAML/JSON data. + wrapFileInputValues(cmd, &requestContents) + // Embed files passed as "@file.jpg" in the request body, headers, and query: embedStyle := EmbedText if bodyType == ApplicationOctetStream || bodyType == MultipartFormEncoded { @@ -371,3 +392,78 @@ func flagOptions( return options, nil } + +// FilePathValue is a string wrapper that marks a value as a file path whose contents should be read +// and embedded in the request. Unlike a regular string, embedFilesValue always treats a FilePathValue +// as a file path without needing the "@" prefix. +type FilePathValue string + +// wrapFileInputValues replaces string values for FileInput flags (type: string, format: binary) with +// FilePathValue sentinel values. embedFilesValue recognizes FilePathValue and reads the file contents +// directly, so the user doesn't need to type the "@" prefix. This handles both values set via explicit +// CLI flags and values that arrived via piped YAML/JSON data. +func wrapFileInputValues(cmd *cli.Command, contents *requestflag.RequestContents) { + bodyMap, _ := contents.Body.(map[string]any) + + for _, flag := range cmd.Flags { + inReq, ok := flag.(requestflag.InRequest) + if !ok || !inReq.IsFileInput() || inReq.IsBodyRoot() { + continue + } + + // Wrap values set via explicit CLI flags. + if flag.IsSet() { + if wrapped, changed := wrapFileInputValue(flag.Get()); changed { + if bodyPath := inReq.GetBodyPath(); bodyPath != "" { + if bodyMap != nil { + bodyMap[bodyPath] = wrapped + } + } else if queryPath := inReq.GetQueryPath(); queryPath != "" { + contents.Queries[queryPath] = wrapped + } else if headerPath := inReq.GetHeaderPath(); headerPath != "" { + contents.Headers[headerPath] = wrapped + } + } + } + + // Wrap values that arrived via piped YAML/JSON data in the body map. + if bodyPath := inReq.GetBodyPath(); bodyPath != "" && bodyMap != nil { + if value, exists := bodyMap[bodyPath]; exists { + if wrapped, changed := wrapFileInputValue(value); changed { + bodyMap[bodyPath] = wrapped + } + } + } + } +} + +func wrapFileInputValue(value any) (any, bool) { + switch v := value.(type) { + case string: + if v == "" { + return value, false + } + return FilePathValue(v), true + + case []string: + result := make([]any, len(v)) + for i, s := range v { + result[i] = FilePathValue(s) + } + return result, true + + case []any: + result := make([]any, len(v)) + for i, elem := range v { + if s, ok := elem.(string); ok { + result[i] = FilePathValue(s) + } else { + result[i] = elem + } + } + return result, true + + default: + return value, false + } +} From 8d976d441c86ddfaa67c8fad50a156f9374e0d16 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 3 Apr 2026 02:11:17 +0000 Subject: [PATCH 15/54] feat: better error message if scheme forgotten in CLI `*_BASE_URL`/`--base-url` --- cmd/beeper-desktop-cli/main.go | 7 ++++++ pkg/cmd/cmd.go | 3 +++ pkg/cmd/cmdutil.go | 9 ++++++++ pkg/cmd/cmdutil_test.go | 39 ++++++++++++++++++++++++++++++++++ 4 files changed, 58 insertions(+) diff --git a/cmd/beeper-desktop-cli/main.go b/cmd/beeper-desktop-cli/main.go index e27225a3..8ac76ade 100644 --- a/cmd/beeper-desktop-cli/main.go +++ b/cmd/beeper-desktop-cli/main.go @@ -23,6 +23,13 @@ func main() { prepareForAutocomplete(app) } + if baseURL, ok := os.LookupEnv("BEEPER_DESKTOP_BASE_URL"); ok { + if err := cmd.ValidateBaseURL(baseURL, "BEEPER_DESKTOP_BASE_URL"); err != nil { + fmt.Fprintf(os.Stderr, "%s\n", err.Error()) + os.Exit(1) + } + } + if err := app.Run(context.Background(), os.Args); err != nil { exitCode := 1 diff --git a/pkg/cmd/cmd.go b/pkg/cmd/cmd.go index 3351e7f6..4c91ac7a 100644 --- a/pkg/cmd/cmd.go +++ b/pkg/cmd/cmd.go @@ -39,6 +39,9 @@ func init() { Name: "base-url", DefaultText: "url", Usage: "Override the base URL for API requests", + Validator: func(baseURL string) error { + return ValidateBaseURL(baseURL, "--base-url") + }, }, &cli.StringFlag{ Name: "format", diff --git a/pkg/cmd/cmdutil.go b/pkg/cmd/cmdutil.go index 82655ad4..06531f39 100644 --- a/pkg/cmd/cmdutil.go +++ b/pkg/cmd/cmdutil.go @@ -29,6 +29,15 @@ import ( var OutputFormats = []string{"auto", "explore", "json", "jsonl", "pretty", "raw", "yaml"} +// ValidateBaseURL checks that a base URL is correctly prefixed with a protocol scheme and produces a better +// error message than the person would see otherwise if it doesn't. +func ValidateBaseURL(value, source string) error { + if value != "" && !strings.HasPrefix(value, "http://") && !strings.HasPrefix(value, "https://") { + return fmt.Errorf("%s %q is missing a scheme (expected http:// or https://)", source, value) + } + return nil +} + func getDefaultRequestOptions(cmd *cli.Command) []option.RequestOption { opts := []option.RequestOption{ option.WithHeader("User-Agent", fmt.Sprintf("BeeperDesktop/CLI %s", Version)), diff --git a/pkg/cmd/cmdutil_test.go b/pkg/cmd/cmdutil_test.go index 0a46fd14..84874083 100644 --- a/pkg/cmd/cmdutil_test.go +++ b/pkg/cmd/cmdutil_test.go @@ -125,3 +125,42 @@ func TestCreateDownloadFile(t *testing.T) { assert.Equal(t, "passwd", filepath.Base(file.Name())) }) } + +func TestValidateBaseURL(t *testing.T) { + t.Parallel() + + t.Run("ValidHTTPS", func(t *testing.T) { + t.Parallel() + + require.NoError(t, ValidateBaseURL("https://api.example.com", "--base-url")) + }) + + t.Run("ValidHTTP", func(t *testing.T) { + t.Parallel() + + require.NoError(t, ValidateBaseURL("http://localhost:8080", "--base-url")) + }) + + t.Run("Empty", func(t *testing.T) { + t.Parallel() + + require.NoError(t, ValidateBaseURL("", "MY_BASE_URL")) + }) + + t.Run("MissingScheme", func(t *testing.T) { + t.Parallel() + + err := ValidateBaseURL("localhost:8080", "MY_BASE_URL") + require.Error(t, err) + assert.Contains(t, err.Error(), "MY_BASE_URL") + assert.Contains(t, err.Error(), "missing a scheme") + }) + + t.Run("HostOnly", func(t *testing.T) { + t.Parallel() + + err := ValidateBaseURL("api.example.com", "--base-url") + require.Error(t, err) + assert.Contains(t, err.Error(), "--base-url") + }) +} From eacfea0f881332d854999d73092a4786d0b3ae33 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 3 Apr 2026 02:11:44 +0000 Subject: [PATCH 16/54] feat: allow `-` as value representing stdin to binary-only file parameters in CLIs --- pkg/cmd/flagoptions.go | 103 +++++++++++++++++++++++++++++++++--- pkg/cmd/flagoptions_test.go | 91 ++++++++++++++++++++++++++++++- 2 files changed, 184 insertions(+), 10 deletions(-) diff --git a/pkg/cmd/flagoptions.go b/pkg/cmd/flagoptions.go index 24ed79a5..198b295e 100644 --- a/pkg/cmd/flagoptions.go +++ b/pkg/cmd/flagoptions.go @@ -40,12 +40,48 @@ const ( EmbedIOReader ) -func embedFiles(obj any, embedStyle FileEmbedStyle) (any, error) { +// onceStdinReader wraps an io.Reader that can only be consumed once, used to ensure stdin is read by at most +// one parameter (or only for a body root parameter or only for YAML parameter input). If reason is set, stdin +// is unavailable and read() returns an error explaining why. +type onceStdinReader struct { + stdinReader io.Reader + failureReason string +} + +func (o *onceStdinReader) read() (io.Reader, error) { + if o.failureReason != "" { + return nil, fmt.Errorf("cannot read from stdin: %s", o.failureReason) + } + if o.stdinReader == nil { + return nil, fmt.Errorf("stdin has already been read by another parameter; it can only be read once") + } + r := o.stdinReader + o.stdinReader = nil + return r, nil +} + +func (o *onceStdinReader) readAll() ([]byte, error) { + r, err := o.read() + if err != nil { + return nil, err + } + return io.ReadAll(r) +} + +func isStdinPath(s string) bool { + switch s { + case "-", "/dev/fd/0", "/dev/stdin": + return true + } + return false +} + +func embedFiles(obj any, embedStyle FileEmbedStyle, stdin *onceStdinReader) (any, error) { if obj == nil { return obj, nil } v := reflect.ValueOf(obj) - result, err := embedFilesValue(v, embedStyle) + result, err := embedFilesValue(v, embedStyle, stdin) if err != nil { return nil, err } @@ -53,7 +89,7 @@ func embedFiles(obj any, embedStyle FileEmbedStyle) (any, error) { } // Replace "@file.txt" with the file's contents inside a value -func embedFilesValue(v reflect.Value, embedStyle FileEmbedStyle) (reflect.Value, error) { +func embedFilesValue(v reflect.Value, embedStyle FileEmbedStyle, stdin *onceStdinReader) (reflect.Value, error) { // Unwrap interface values to get the concrete type if v.Kind() == reflect.Interface { if v.IsNil() { @@ -74,7 +110,7 @@ func embedFilesValue(v reflect.Value, embedStyle FileEmbedStyle) (reflect.Value, for iter.Next() { key := iter.Key() val := iter.Value() - newVal, err := embedFilesValue(val, embedStyle) + newVal, err := embedFilesValue(val, embedStyle, stdin) if err != nil { return reflect.Value{}, err } @@ -89,7 +125,7 @@ func embedFilesValue(v reflect.Value, embedStyle FileEmbedStyle) (reflect.Value, // Use `[]any` to allow for types to change when embedding files result := reflect.MakeSlice(reflect.TypeOf([]any{}), v.Len(), v.Len()) for i := 0; i < v.Len(); i++ { - newVal, err := embedFilesValue(v.Index(i), embedStyle) + newVal, err := embedFilesValue(v.Index(i), embedStyle, stdin) if err != nil { return reflect.Value{}, err } @@ -106,6 +142,13 @@ func embedFilesValue(v reflect.Value, embedStyle FileEmbedStyle) (reflect.Value, if s == "" { return v, nil } + if isStdinPath(s) { + content, err := stdin.readAll() + if err != nil { + return v, err + } + return reflect.ValueOf(string(content)), nil + } content, err := os.ReadFile(s) if err != nil { return v, err @@ -123,6 +166,13 @@ func embedFilesValue(v reflect.Value, embedStyle FileEmbedStyle) (reflect.Value, if filename, ok := strings.CutPrefix(s, "@data://"); ok { // The "@data://" prefix is for files you explicitly want to upload // as base64-encoded (even if the file itself is plain text) + if isStdinPath(filename) { + content, err := stdin.readAll() + if err != nil { + return v, err + } + return reflect.ValueOf(base64.StdEncoding.EncodeToString(content)), nil + } content, err := os.ReadFile(filename) if err != nil { return v, err @@ -132,12 +182,29 @@ func embedFilesValue(v reflect.Value, embedStyle FileEmbedStyle) (reflect.Value, // The "@file://" prefix is for files that you explicitly want to // upload as a string literal with backslash escapes (not base64 // encoded) + if isStdinPath(filename) { + content, err := stdin.readAll() + if err != nil { + return v, err + } + return reflect.ValueOf(string(content)), nil + } content, err := os.ReadFile(filename) if err != nil { return v, err } return reflect.ValueOf(string(content)), nil } else if filename, ok := strings.CutPrefix(s, "@"); ok { + if isStdinPath(filename) { + content, err := stdin.readAll() + if err != nil { + return v, err + } + if isUTF8TextFile(content) { + return reflect.ValueOf(string(content)), nil + } + return reflect.ValueOf(base64.StdEncoding.EncodeToString(content)), nil + } content, err := os.ReadFile(filename) if err != nil { // If the string is "@username", it's probably supposed to be a @@ -175,6 +242,14 @@ func embedFilesValue(v reflect.Value, embedStyle FileEmbedStyle) (reflect.Value, expectsFile = strings.Contains(filename, ".") || strings.Contains(filename, "/") } + if isStdinPath(filename) { + r, err := stdin.read() + if err != nil { + return v, err + } + return reflect.ValueOf(io.NopCloser(r)), nil + } + file, err := os.Open(filename) if err != nil { if !expectsFile { @@ -234,6 +309,7 @@ func flagOptions( requestContents := requestflag.ExtractRequestContents(cmd) + stdinConsumedByPipe := false if (bodyType == MultipartFormEncoded || bodyType == ApplicationJSON) && !ignoreStdin && isInputPiped() { pipeData, err := io.ReadAll(os.Stdin) if err != nil { @@ -241,6 +317,7 @@ func flagOptions( } if len(pipeData) > 0 { + stdinConsumedByPipe = true var bodyData any if err := yaml.Unmarshal(pipeData, &bodyData); err != nil { return nil, fmt.Errorf("Failed to parse piped data as YAML/JSON:\n%w", err) @@ -279,24 +356,34 @@ func flagOptions( // via explicit CLI flags and values that arrived via piped YAML/JSON data. wrapFileInputValues(cmd, &requestContents) + // Determine stdin availability for FileInput params that use "-". + var stdinReader onceStdinReader + if ignoreStdin { + stdinReader = onceStdinReader{failureReason: "stdin is already being used for the request body"} + } else if stdinConsumedByPipe { + stdinReader = onceStdinReader{failureReason: "stdin was already consumed by piped YAML/JSON input"} + } else { + stdinReader = onceStdinReader{stdinReader: os.Stdin} + } + // Embed files passed as "@file.jpg" in the request body, headers, and query: embedStyle := EmbedText if bodyType == ApplicationOctetStream || bodyType == MultipartFormEncoded { embedStyle = EmbedIOReader } - if embedded, err := embedFiles(requestContents.Body, embedStyle); err != nil { + if embedded, err := embedFiles(requestContents.Body, embedStyle, &stdinReader); err != nil { return nil, err } else { requestContents.Body = embedded } - if headersWithFiles, err := embedFiles(requestContents.Headers, EmbedText); err != nil { + if headersWithFiles, err := embedFiles(requestContents.Headers, EmbedText, &stdinReader); err != nil { return nil, err } else { requestContents.Headers = headersWithFiles.(map[string]any) } - if queriesWithFiles, err := embedFiles(requestContents.Queries, EmbedText); err != nil { + if queriesWithFiles, err := embedFiles(requestContents.Queries, EmbedText, &stdinReader); err != nil { return nil, err } else { requestContents.Queries = queriesWithFiles.(map[string]any) diff --git a/pkg/cmd/flagoptions_test.go b/pkg/cmd/flagoptions_test.go index e5dad4be..9a7fe3b6 100644 --- a/pkg/cmd/flagoptions_test.go +++ b/pkg/cmd/flagoptions_test.go @@ -2,8 +2,10 @@ package cmd import ( "encoding/base64" + "io" "os" "path/filepath" + "strings" "testing" "github.com/stretchr/testify/assert" @@ -11,6 +13,7 @@ import ( ) func TestIsUTF8TextFile(t *testing.T) { + tests := []struct { content []byte expected bool @@ -32,6 +35,7 @@ func TestIsUTF8TextFile(t *testing.T) { } func TestEmbedFiles(t *testing.T) { + // Create temporary directory for test files tmpDir := t.TempDir() @@ -216,7 +220,8 @@ func TestEmbedFiles(t *testing.T) { for _, tt := range tests { t.Run(tt.name+" text", func(t *testing.T) { - got, err := embedFiles(tt.input, EmbedText) + + got, err := embedFiles(tt.input, EmbedText, nil) if tt.wantErr { assert.Error(t, err) } else { @@ -226,7 +231,8 @@ func TestEmbedFiles(t *testing.T) { }) t.Run(tt.name+" io.Reader", func(t *testing.T) { - _, err := embedFiles(tt.input, EmbedIOReader) + + _, err := embedFiles(tt.input, EmbedIOReader, nil) if tt.wantErr { assert.Error(t, err) } else { @@ -236,9 +242,90 @@ func TestEmbedFiles(t *testing.T) { } } +func TestEmbedFilesStdin(t *testing.T) { + + t.Run("FilePathValueDash", func(t *testing.T) { + + stdin := &onceStdinReader{stdinReader: strings.NewReader("stdin content")} + + withEmbedded, err := embedFiles(map[string]any{"file": FilePathValue("-")}, EmbedText, stdin) + require.NoError(t, err) + require.Equal(t, map[string]any{"file": "stdin content"}, withEmbedded) + }) + + t.Run("FilePathValueDevStdin", func(t *testing.T) { + + stdin := &onceStdinReader{stdinReader: strings.NewReader("stdin content")} + + withEmbedded, err := embedFiles(map[string]any{"file": FilePathValue("/dev/stdin")}, EmbedText, stdin) + require.NoError(t, err) + require.Equal(t, map[string]any{"file": "stdin content"}, withEmbedded) + }) + + t.Run("MultipleFilePathValueDashesError", func(t *testing.T) { + + stdin := &onceStdinReader{stdinReader: strings.NewReader("stdin content")} + + _, err := embedFiles(map[string]any{ + "file1": FilePathValue("-"), + "file2": FilePathValue("-"), + }, EmbedText, stdin) + require.Error(t, err) + require.Contains(t, err.Error(), "already been read") + }) + + t.Run("FilePathValueDashUnavailableStdin", func(t *testing.T) { + + stdin := &onceStdinReader{failureReason: "stdin is already being used for the request body"} + + _, err := embedFiles(map[string]any{"file": FilePathValue("-")}, EmbedText, stdin) + require.Error(t, err) + require.Contains(t, err.Error(), "cannot read from stdin") + require.Contains(t, err.Error(), "request body") + }) + + t.Run("AtDashEmbedText", func(t *testing.T) { + + stdin := &onceStdinReader{stdinReader: strings.NewReader("piped content")} + + withEmbedded, err := embedFiles(map[string]any{"data": "@-"}, EmbedText, stdin) + require.NoError(t, err) + require.Equal(t, map[string]any{"data": "piped content"}, withEmbedded) + }) + + t.Run("AtDashEmbedIOReader", func(t *testing.T) { + + stdin := &onceStdinReader{stdinReader: strings.NewReader("piped content")} + + withEmbedded, err := embedFiles(map[string]any{"data": "@-"}, EmbedIOReader, stdin) + require.NoError(t, err) + + withEmbeddedMap := withEmbedded.(map[string]any) + r := withEmbeddedMap["data"].(io.ReadCloser) + + content, err := io.ReadAll(r) + require.NoError(t, err) + require.Equal(t, "piped content", string(content)) + }) + + t.Run("FilePathValueRealFile", func(t *testing.T) { + + tmpDir := t.TempDir() + writeTestFile(t, tmpDir, "test.txt", "file content") + + stdin := &onceStdinReader{stdinReader: strings.NewReader("unused stdin")} + + withEmbedded, err := embedFiles(map[string]any{"file": FilePathValue(filepath.Join(tmpDir, "test.txt"))}, EmbedText, stdin) + require.NoError(t, err) + require.Equal(t, map[string]any{"file": "file content"}, withEmbedded) + }) +} + func writeTestFile(t *testing.T, dir, filename, content string) { t.Helper() + path := filepath.Join(dir, filename) + err := os.WriteFile(path, []byte(content), 0644) require.NoError(t, err, "failed to write test file %s", path) } From c0de07ef35b6d776781b0347a102c3855445f0cb Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 3 Apr 2026 02:11:59 +0000 Subject: [PATCH 17/54] chore: switch some CLI Go tests from `os.Chdir` to `t.Chdir` --- pkg/cmd/cmdutil_test.go | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/pkg/cmd/cmdutil_test.go b/pkg/cmd/cmdutil_test.go index 84874083..550c9954 100644 --- a/pkg/cmd/cmdutil_test.go +++ b/pkg/cmd/cmdutil_test.go @@ -67,10 +67,7 @@ func TestWriteBinaryResponse(t *testing.T) { func TestCreateDownloadFile(t *testing.T) { t.Run("creates file with filename from header", func(t *testing.T) { - tmpDir := t.TempDir() - oldWd, _ := os.Getwd() - os.Chdir(tmpDir) - defer os.Chdir(oldWd) + t.Chdir(t.TempDir()) resp := &http.Response{ Header: http.Header{ @@ -96,10 +93,7 @@ func TestCreateDownloadFile(t *testing.T) { }) t.Run("creates temp file when no header", func(t *testing.T) { - tmpDir := t.TempDir() - oldWd, _ := os.Getwd() - os.Chdir(tmpDir) - defer os.Chdir(oldWd) + t.Chdir(t.TempDir()) resp := &http.Response{Header: http.Header{}} file, err := createDownloadFile(resp, []byte("test content")) @@ -109,10 +103,7 @@ func TestCreateDownloadFile(t *testing.T) { }) t.Run("prevents directory traversal", func(t *testing.T) { - tmpDir := t.TempDir() - oldWd, _ := os.Getwd() - os.Chdir(tmpDir) - defer os.Chdir(oldWd) + t.Chdir(t.TempDir()) resp := &http.Response{ Header: http.Header{ From 59678e17b692460f3fdc03ca82ba6270d13ed460 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 3 Apr 2026 02:12:15 +0000 Subject: [PATCH 18/54] chore: mark all CLI-related tests in Go with `t.Parallel()` --- internal/apiform/form_test.go | 4 ++ internal/apiquery/query_test.go | 4 ++ internal/autocomplete/autocomplete_test.go | 40 +++++++++++++++ internal/jsonview/explorer_test.go | 6 +++ internal/requestflag/innerflag_test.go | 28 +++++++++++ internal/requestflag/requestflag_test.go | 58 ++++++++++++++++++++++ pkg/cmd/flagoptions_test.go | 12 +++++ 7 files changed, 152 insertions(+) diff --git a/internal/apiform/form_test.go b/internal/apiform/form_test.go index 2cf5bdd2..f68cfd1c 100644 --- a/internal/apiform/form_test.go +++ b/internal/apiform/form_test.go @@ -85,8 +85,12 @@ var tests = map[string]struct { } func TestEncode(t *testing.T) { + t.Parallel() + for name, test := range tests { t.Run(name, func(t *testing.T) { + t.Parallel() + buf := bytes.NewBuffer(nil) writer := multipart.NewWriter(buf) writer.SetBoundary("xxx") diff --git a/internal/apiquery/query_test.go b/internal/apiquery/query_test.go index 8bee784a..3791ec97 100644 --- a/internal/apiquery/query_test.go +++ b/internal/apiquery/query_test.go @@ -6,6 +6,8 @@ import ( ) func TestEncode(t *testing.T) { + t.Parallel() + tests := map[string]struct { val any settings QuerySettings @@ -114,6 +116,8 @@ func TestEncode(t *testing.T) { for name, test := range tests { t.Run(name, func(t *testing.T) { + t.Parallel() + query := map[string]any{"query": test.val} values, err := MarshalWithSettings(query, test.settings) if err != nil { diff --git a/internal/autocomplete/autocomplete_test.go b/internal/autocomplete/autocomplete_test.go index 3e8aa335..23389245 100644 --- a/internal/autocomplete/autocomplete_test.go +++ b/internal/autocomplete/autocomplete_test.go @@ -8,6 +8,8 @@ import ( ) func TestGetCompletions_EmptyArgs(t *testing.T) { + t.Parallel() + root := &cli.Command{ Commands: []*cli.Command{ {Name: "generate", Usage: "Generate SDK"}, @@ -26,6 +28,8 @@ func TestGetCompletions_EmptyArgs(t *testing.T) { } func TestGetCompletions_SubcommandPrefix(t *testing.T) { + t.Parallel() + root := &cli.Command{ Commands: []*cli.Command{ {Name: "generate", Usage: "Generate SDK"}, @@ -43,6 +47,8 @@ func TestGetCompletions_SubcommandPrefix(t *testing.T) { } func TestGetCompletions_HiddenCommand(t *testing.T) { + t.Parallel() + root := &cli.Command{ Commands: []*cli.Command{ {Name: "visible", Usage: "Visible command"}, @@ -57,6 +63,8 @@ func TestGetCompletions_HiddenCommand(t *testing.T) { } func TestGetCompletions_NestedSubcommand(t *testing.T) { + t.Parallel() + root := &cli.Command{ Commands: []*cli.Command{ { @@ -79,6 +87,8 @@ func TestGetCompletions_NestedSubcommand(t *testing.T) { } func TestGetCompletions_FlagCompletion(t *testing.T) { + t.Parallel() + root := &cli.Command{ Commands: []*cli.Command{ { @@ -102,6 +112,8 @@ func TestGetCompletions_FlagCompletion(t *testing.T) { } func TestGetCompletions_ShortFlagCompletion(t *testing.T) { + t.Parallel() + root := &cli.Command{ Commands: []*cli.Command{ { @@ -123,6 +135,8 @@ func TestGetCompletions_ShortFlagCompletion(t *testing.T) { } func TestGetCompletions_FileFlagBehavior(t *testing.T) { + t.Parallel() + root := &cli.Command{ Commands: []*cli.Command{ { @@ -142,6 +156,8 @@ func TestGetCompletions_FileFlagBehavior(t *testing.T) { } func TestGetCompletions_NonBoolFlagValue(t *testing.T) { + t.Parallel() + root := &cli.Command{ Commands: []*cli.Command{ { @@ -161,6 +177,8 @@ func TestGetCompletions_NonBoolFlagValue(t *testing.T) { } func TestGetCompletions_BoolFlagDoesNotBlockCompletion(t *testing.T) { + t.Parallel() + root := &cli.Command{ Commands: []*cli.Command{ { @@ -185,6 +203,8 @@ func TestGetCompletions_BoolFlagDoesNotBlockCompletion(t *testing.T) { } func TestGetCompletions_ColonCommands_NoColonTyped(t *testing.T) { + t.Parallel() + root := &cli.Command{ Commands: []*cli.Command{ {Name: "config:get", Usage: "Get config value"}, @@ -202,6 +222,8 @@ func TestGetCompletions_ColonCommands_NoColonTyped(t *testing.T) { } func TestGetCompletions_ColonCommands_ColonTyped_Bash(t *testing.T) { + t.Parallel() + root := &cli.Command{ Commands: []*cli.Command{ {Name: "config:get", Usage: "Get config value"}, @@ -221,6 +243,8 @@ func TestGetCompletions_ColonCommands_ColonTyped_Bash(t *testing.T) { } func TestGetCompletions_ColonCommands_ColonTyped_Zsh(t *testing.T) { + t.Parallel() + root := &cli.Command{ Commands: []*cli.Command{ {Name: "config:get", Usage: "Get config value"}, @@ -240,6 +264,8 @@ func TestGetCompletions_ColonCommands_ColonTyped_Zsh(t *testing.T) { } func TestGetCompletions_BashStyleColonCompletion(t *testing.T) { + t.Parallel() + root := &cli.Command{ Commands: []*cli.Command{ {Name: "config:get", Usage: "Get config value"}, @@ -257,6 +283,8 @@ func TestGetCompletions_BashStyleColonCompletion(t *testing.T) { } func TestGetCompletions_BashStyleColonCompletion_NoMatch(t *testing.T) { + t.Parallel() + root := &cli.Command{ Commands: []*cli.Command{ {Name: "config:get", Usage: "Get config value"}, @@ -271,6 +299,8 @@ func TestGetCompletions_BashStyleColonCompletion_NoMatch(t *testing.T) { } func TestGetCompletions_ZshStyleColonCompletion(t *testing.T) { + t.Parallel() + root := &cli.Command{ Commands: []*cli.Command{ {Name: "config:get", Usage: "Get config value"}, @@ -287,6 +317,8 @@ func TestGetCompletions_ZshStyleColonCompletion(t *testing.T) { } func TestGetCompletions_MixedColonAndRegularCommands(t *testing.T) { + t.Parallel() + root := &cli.Command{ Commands: []*cli.Command{ {Name: "generate", Usage: "Generate SDK"}, @@ -305,6 +337,8 @@ func TestGetCompletions_MixedColonAndRegularCommands(t *testing.T) { } func TestGetCompletions_FlagWithBoolFlagSkipsValue(t *testing.T) { + t.Parallel() + root := &cli.Command{ Commands: []*cli.Command{ { @@ -329,6 +363,8 @@ func TestGetCompletions_FlagWithBoolFlagSkipsValue(t *testing.T) { } func TestGetCompletions_MultipleFlagsBeforeSubcommand(t *testing.T) { + t.Parallel() + root := &cli.Command{ Commands: []*cli.Command{ { @@ -353,6 +389,8 @@ func TestGetCompletions_MultipleFlagsBeforeSubcommand(t *testing.T) { } func TestGetCompletions_CommandAliases(t *testing.T) { + t.Parallel() + root := &cli.Command{ Commands: []*cli.Command{ {Name: "generate", Aliases: []string{"gen", "g"}, Usage: "Generate SDK"}, @@ -372,6 +410,8 @@ func TestGetCompletions_CommandAliases(t *testing.T) { } func TestGetCompletions_AllFlagsWhenNoPrefix(t *testing.T) { + t.Parallel() + root := &cli.Command{ Commands: []*cli.Command{ { diff --git a/internal/jsonview/explorer_test.go b/internal/jsonview/explorer_test.go index c5592542..67ee730a 100644 --- a/internal/jsonview/explorer_test.go +++ b/internal/jsonview/explorer_test.go @@ -10,6 +10,8 @@ import ( ) func TestNavigateForward_EmptyRowData(t *testing.T) { + t.Parallel() + // An empty JSON array produces a TableView with no rows. emptyArray := gjson.Parse("[]") view, err := newTableView("", emptyArray, false) @@ -38,6 +40,8 @@ type rawJSONItem struct { func (r rawJSONItem) RawJSON() string { return r.raw } func TestMarshalItemsToJSONArray_WithHasRawJSON(t *testing.T) { + t.Parallel() + items := []any{ rawJSONItem{raw: `{"id":1,"name":"alice"}`}, rawJSONItem{raw: `{"id":2,"name":"bob"}`}, @@ -49,6 +53,8 @@ func TestMarshalItemsToJSONArray_WithHasRawJSON(t *testing.T) { } func TestMarshalItemsToJSONArray_WithoutHasRawJSON(t *testing.T) { + t.Parallel() + items := []any{ map[string]any{"id": 1, "name": "alice"}, map[string]any{"id": 2, "name": "bob"}, diff --git a/internal/requestflag/innerflag_test.go b/internal/requestflag/innerflag_test.go index 3f204c97..133e8b44 100644 --- a/internal/requestflag/innerflag_test.go +++ b/internal/requestflag/innerflag_test.go @@ -8,6 +8,8 @@ import ( ) func TestInnerFlagSet(t *testing.T) { + t.Parallel() + tests := []struct { name string flagType string @@ -27,6 +29,8 @@ func TestInnerFlagSet(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + t.Parallel() + outerFlag := &Flag[map[string]any]{ Name: "test-flag", } @@ -81,6 +85,8 @@ func TestInnerFlagSet(t *testing.T) { } func TestInnerFlagValidator(t *testing.T) { + t.Parallel() + outerFlag := &Flag[map[string]any]{Name: "test-flag"} innerFlag := &InnerFlag[int64]{ @@ -105,6 +111,8 @@ func TestInnerFlagValidator(t *testing.T) { } func TestWithInnerFlags(t *testing.T) { + t.Parallel() + outerFlag := &Flag[map[string]any]{Name: "outer"} innerFlag := &InnerFlag[string]{ Name: "outer.baz", @@ -126,6 +134,8 @@ func TestWithInnerFlags(t *testing.T) { } func TestInnerFlagTypeNames(t *testing.T) { + t.Parallel() + tests := []struct { name string flag cli.DocGenerationFlag @@ -143,6 +153,8 @@ func TestInnerFlagTypeNames(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + t.Parallel() + typeName := tt.flag.TypeName() assert.Equal(t, tt.expected, typeName, "Expected type name %q, got %q", tt.expected, typeName) }) @@ -150,8 +162,12 @@ func TestInnerFlagTypeNames(t *testing.T) { } func TestInnerYamlHandling(t *testing.T) { + t.Parallel() + // Test with map value t.Run("Parse YAML to map", func(t *testing.T) { + t.Parallel() + outerFlag := &Flag[map[string]any]{Name: "outer"} innerFlag := &InnerFlag[map[string]any]{ Name: "outer.baz", @@ -176,6 +192,8 @@ func TestInnerYamlHandling(t *testing.T) { // Test with invalid YAML t.Run("Parse invalid YAML", func(t *testing.T) { + t.Parallel() + outerFlag := &Flag[map[string]any]{Name: "outer"} innerFlag := &InnerFlag[map[string]any]{ Name: "outer.baz", @@ -190,6 +208,8 @@ func TestInnerYamlHandling(t *testing.T) { // Test setting inner flags on a map multiple times t.Run("Set inner flags on map multiple times", func(t *testing.T) { + t.Parallel() + outerFlag := &Flag[map[string]any]{Name: "outer"} // Set first inner flag @@ -219,6 +239,8 @@ func TestInnerYamlHandling(t *testing.T) { // Test setting YAML and then an inner flag t.Run("Set YAML and then inner flag", func(t *testing.T) { + t.Parallel() + outerFlag := &Flag[map[string]any]{Name: "outer"} // First set the outer flag with YAML @@ -246,7 +268,11 @@ func TestInnerYamlHandling(t *testing.T) { } func TestInnerFlagWithSliceType(t *testing.T) { + t.Parallel() + t.Run("Setting inner flags on slice of maps", func(t *testing.T) { + t.Parallel() + outerFlag := &Flag[[]map[string]any]{Name: "outer"} // Set first inner flag (should create first item) @@ -284,6 +310,8 @@ func TestInnerFlagWithSliceType(t *testing.T) { }) t.Run("Appending to existing slice", func(t *testing.T) { + t.Parallel() + // Initialize with existing items outerFlag := &Flag[[]map[string]any]{Name: "outer"} err := outerFlag.Set(outerFlag.Name, `{name: initial}`) diff --git a/internal/requestflag/requestflag_test.go b/internal/requestflag/requestflag_test.go index 97519048..0e86e074 100644 --- a/internal/requestflag/requestflag_test.go +++ b/internal/requestflag/requestflag_test.go @@ -11,6 +11,8 @@ import ( ) func TestDateValueParse(t *testing.T) { + t.Parallel() + tests := []struct { name string input string @@ -56,6 +58,8 @@ func TestDateValueParse(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + t.Parallel() + var d DateValue err := d.Parse(tt.input) @@ -70,6 +74,8 @@ func TestDateValueParse(t *testing.T) { } func TestDateTimeValueParse(t *testing.T) { + t.Parallel() + tests := []struct { name string input string @@ -119,6 +125,8 @@ func TestDateTimeValueParse(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + t.Parallel() + var d DateTimeValue err := d.Parse(tt.input) @@ -136,6 +144,8 @@ func TestDateTimeValueParse(t *testing.T) { } func TestTimeValueParse(t *testing.T) { + t.Parallel() + tests := []struct { name string input string @@ -181,6 +191,8 @@ func TestTimeValueParse(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + t.Parallel() + var tv TimeValue err := tv.Parse(tt.input) @@ -195,7 +207,11 @@ func TestTimeValueParse(t *testing.T) { } func TestRequestParams(t *testing.T) { + t.Parallel() + t.Run("map body type", func(t *testing.T) { + t.Parallel() + // Create a mock command with flags cmd := &cli.Command{ Name: "test", @@ -283,6 +299,8 @@ func TestRequestParams(t *testing.T) { }) t.Run("non-map body type", func(t *testing.T) { + t.Parallel() + // Create a mock command with flags cmd := &cli.Command{ Name: "test", @@ -304,6 +322,8 @@ func TestRequestParams(t *testing.T) { } func TestFlagSet(t *testing.T) { + t.Parallel() + strFlag := &Flag[string]{ Name: "string-flag", Default: "default-string", @@ -327,38 +347,52 @@ func TestFlagSet(t *testing.T) { // Test initialization and setting t.Run("PreParse initialization", func(t *testing.T) { + t.Parallel() + assert.NoError(t, strFlag.PreParse()) assert.True(t, strFlag.applied) assert.Equal(t, "default-string", strFlag.Get()) }) t.Run("Set string flag", func(t *testing.T) { + t.Parallel() + assert.NoError(t, strFlag.Set("string-flag", "new-value")) assert.Equal(t, "new-value", strFlag.Get()) assert.True(t, strFlag.IsSet()) }) t.Run("Set int flag with valid value", func(t *testing.T) { + t.Parallel() + assert.NoError(t, superstitiousIntFlag.Set("int-flag", "100")) assert.Equal(t, int64(100), superstitiousIntFlag.Get()) assert.True(t, superstitiousIntFlag.IsSet()) }) t.Run("Set int flag with invalid value", func(t *testing.T) { + t.Parallel() + assert.Error(t, superstitiousIntFlag.Set("int-flag", "not-an-int")) }) t.Run("Set int flag with validator failing", func(t *testing.T) { + t.Parallel() + assert.Error(t, superstitiousIntFlag.Set("int-flag", "13")) }) t.Run("Set bool flag", func(t *testing.T) { + t.Parallel() + assert.NoError(t, boolFlag.Set("bool-flag", "true")) assert.Equal(t, true, boolFlag.Get()) assert.True(t, boolFlag.IsSet()) }) t.Run("Set slice flag with multiple values", func(t *testing.T) { + t.Parallel() + sliceFlag := &Flag[[]int64]{ Name: "slice-flag", Default: []int64{}, @@ -381,6 +415,8 @@ func TestFlagSet(t *testing.T) { }) t.Run("Set slice flag with a nonempty default", func(t *testing.T) { + t.Parallel() + sliceFlag := &Flag[[]int64]{ Name: "slice-flag", Default: []int64{99, 100}, @@ -400,6 +436,8 @@ func TestFlagSet(t *testing.T) { } func TestParseTimeWithFormats(t *testing.T) { + t.Parallel() + tests := []struct { name string input string @@ -439,6 +477,8 @@ func TestParseTimeWithFormats(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + t.Parallel() + got, err := parseTimeWithFormats(tt.input, tt.formats) if tt.wantErr { @@ -452,8 +492,12 @@ func TestParseTimeWithFormats(t *testing.T) { } func TestYamlHandling(t *testing.T) { + t.Parallel() + // Test with any value t.Run("Parse YAML to any", func(t *testing.T) { + t.Parallel() + cv := &cliValue[any]{} err := cv.Set("name: test\nvalue: 42\n") assert.NoError(t, err) @@ -478,6 +522,8 @@ func TestYamlHandling(t *testing.T) { // Test with array t.Run("Parse YAML array", func(t *testing.T) { + t.Parallel() + cv := &cliValue[any]{} err := cv.Set("- item1\n- item2\n- item3\n") assert.NoError(t, err) @@ -495,6 +541,8 @@ func TestYamlHandling(t *testing.T) { }) t.Run("Parse @file.txt as YAML", func(t *testing.T) { + t.Parallel() + flag := &Flag[any]{ Name: "file-flag", Default: nil, @@ -507,6 +555,8 @@ func TestYamlHandling(t *testing.T) { }) t.Run("Parse @file.txt list as YAML", func(t *testing.T) { + t.Parallel() + flag := &Flag[[]any]{ Name: "file-flag", Default: nil, @@ -520,6 +570,8 @@ func TestYamlHandling(t *testing.T) { }) t.Run("Parse identifiers as YAML", func(t *testing.T) { + t.Parallel() + tests := []string{ "hello", "e4e355fa-b03b-4c57-a73d-25c9733eec79", @@ -555,6 +607,8 @@ func TestYamlHandling(t *testing.T) { // Test with invalid YAML t.Run("Parse invalid YAML", func(t *testing.T) { + t.Parallel() + invalidYaml := `[not closed` cv := &cliValue[any]{} err := cv.Set(invalidYaml) @@ -563,6 +617,8 @@ func TestYamlHandling(t *testing.T) { } func TestFlagTypeNames(t *testing.T) { + t.Parallel() + tests := []struct { name string flag cli.DocGenerationFlag @@ -583,6 +639,8 @@ func TestFlagTypeNames(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + t.Parallel() + typeName := tt.flag.TypeName() assert.Equal(t, tt.expected, typeName, "Expected type name %q, got %q", tt.expected, typeName) }) diff --git a/pkg/cmd/flagoptions_test.go b/pkg/cmd/flagoptions_test.go index 9a7fe3b6..039b9ff9 100644 --- a/pkg/cmd/flagoptions_test.go +++ b/pkg/cmd/flagoptions_test.go @@ -13,6 +13,7 @@ import ( ) func TestIsUTF8TextFile(t *testing.T) { + t.Parallel() tests := []struct { content []byte @@ -35,6 +36,7 @@ func TestIsUTF8TextFile(t *testing.T) { } func TestEmbedFiles(t *testing.T) { + t.Parallel() // Create temporary directory for test files tmpDir := t.TempDir() @@ -220,6 +222,7 @@ func TestEmbedFiles(t *testing.T) { for _, tt := range tests { t.Run(tt.name+" text", func(t *testing.T) { + t.Parallel() got, err := embedFiles(tt.input, EmbedText, nil) if tt.wantErr { @@ -231,6 +234,7 @@ func TestEmbedFiles(t *testing.T) { }) t.Run(tt.name+" io.Reader", func(t *testing.T) { + t.Parallel() _, err := embedFiles(tt.input, EmbedIOReader, nil) if tt.wantErr { @@ -243,8 +247,10 @@ func TestEmbedFiles(t *testing.T) { } func TestEmbedFilesStdin(t *testing.T) { + t.Parallel() t.Run("FilePathValueDash", func(t *testing.T) { + t.Parallel() stdin := &onceStdinReader{stdinReader: strings.NewReader("stdin content")} @@ -254,6 +260,7 @@ func TestEmbedFilesStdin(t *testing.T) { }) t.Run("FilePathValueDevStdin", func(t *testing.T) { + t.Parallel() stdin := &onceStdinReader{stdinReader: strings.NewReader("stdin content")} @@ -263,6 +270,7 @@ func TestEmbedFilesStdin(t *testing.T) { }) t.Run("MultipleFilePathValueDashesError", func(t *testing.T) { + t.Parallel() stdin := &onceStdinReader{stdinReader: strings.NewReader("stdin content")} @@ -275,6 +283,7 @@ func TestEmbedFilesStdin(t *testing.T) { }) t.Run("FilePathValueDashUnavailableStdin", func(t *testing.T) { + t.Parallel() stdin := &onceStdinReader{failureReason: "stdin is already being used for the request body"} @@ -285,6 +294,7 @@ func TestEmbedFilesStdin(t *testing.T) { }) t.Run("AtDashEmbedText", func(t *testing.T) { + t.Parallel() stdin := &onceStdinReader{stdinReader: strings.NewReader("piped content")} @@ -294,6 +304,7 @@ func TestEmbedFilesStdin(t *testing.T) { }) t.Run("AtDashEmbedIOReader", func(t *testing.T) { + t.Parallel() stdin := &onceStdinReader{stdinReader: strings.NewReader("piped content")} @@ -309,6 +320,7 @@ func TestEmbedFilesStdin(t *testing.T) { }) t.Run("FilePathValueRealFile", func(t *testing.T) { + t.Parallel() tmpDir := t.TempDir() writeTestFile(t, tmpDir, "test.txt", "file content") From 9e6fe8f0d1cba9d5115fba0b82f24a5938b3e7bc Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 4 Apr 2026 02:08:31 +0000 Subject: [PATCH 19/54] chore: modify CLI tests to inject stdout so mutating `os.Stdout` isn't necessary --- pkg/cmd/cmdutil.go | 9 ++++++--- pkg/cmd/cmdutil_test.go | 15 ++++----------- 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/pkg/cmd/cmdutil.go b/pkg/cmd/cmdutil.go index 06531f39..fad3c3c7 100644 --- a/pkg/cmd/cmdutil.go +++ b/pkg/cmd/cmdutil.go @@ -193,7 +193,10 @@ func streamToStdout(generateOutput func(w *os.File) error) error { return err } -func writeBinaryResponse(response *http.Response, outfile string) (string, error) { +// writeBinaryResponse writes a binary response to stdout or a file. +// +// Takes in a stdout reference so we can test this function without overriding os.Stdout in tests. +func writeBinaryResponse(response *http.Response, stdout io.Writer, outfile string) (string, error) { defer response.Body.Close() body, err := io.ReadAll(response.Body) if err != nil { @@ -201,13 +204,13 @@ func writeBinaryResponse(response *http.Response, outfile string) (string, error } switch outfile { case "-", "/dev/stdout": - _, err := os.Stdout.Write(body) + _, err := stdout.Write(body) return "", err case "": // If output file is unspecified, then print to stdout for plain text or // if stdout is not a terminal: if !isTerminal(os.Stdout) || isUTF8TextFile(body) { - _, err := os.Stdout.Write(body) + _, err := stdout.Write(body) return "", err } diff --git a/pkg/cmd/cmdutil_test.go b/pkg/cmd/cmdutil_test.go index 550c9954..8eca3979 100644 --- a/pkg/cmd/cmdutil_test.go +++ b/pkg/cmd/cmdutil_test.go @@ -32,7 +32,7 @@ func TestWriteBinaryResponse(t *testing.T) { Body: io.NopCloser(bytes.NewReader(body)), } - msg, err := writeBinaryResponse(resp, outfile) + msg, err := writeBinaryResponse(resp, os.Stdout, outfile) require.NoError(t, err) assert.Contains(t, msg, outfile) @@ -43,24 +43,17 @@ func TestWriteBinaryResponse(t *testing.T) { }) t.Run("write to stdout", func(t *testing.T) { - oldStdout := os.Stdout - r, w, _ := os.Pipe() - os.Stdout = w + t.Parallel() + var buf bytes.Buffer body := []byte("stdout content") resp := &http.Response{ Body: io.NopCloser(bytes.NewReader(body)), } - msg, err := writeBinaryResponse(resp, "-") - - w.Close() - os.Stdout = oldStdout + msg, err := writeBinaryResponse(resp, &buf, "-") require.NoError(t, err) assert.Empty(t, msg) - - var buf bytes.Buffer - _, _ = buf.ReadFrom(r) assert.Equal(t, body, buf.Bytes()) }) } From 6960f674e30ae4b66c8c727accf4b140df4aa257 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 7 Apr 2026 02:18:10 +0000 Subject: [PATCH 20/54] fix: fall back to main branch if linking fails in CI --- .github/workflows/ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 940785a8..82ce2502 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,7 +34,7 @@ jobs: - name: Link staging branch if: github.repository == 'stainless-sdks/beeper-desktop-api-cli' run: | - ./scripts/link 'github.com/stainless-sdks/beeper-desktop-api-go@${{ github.ref_name }}' || true + ./scripts/link 'github.com/stainless-sdks/beeper-desktop-api-go@${{ github.ref_name }}' || go mod edit -dropreplace='github.com/stainless-sdks/beeper-desktop-api-go - name: Bootstrap run: ./scripts/bootstrap @@ -60,7 +60,7 @@ jobs: - name: Link staging branch if: github.repository == 'stainless-sdks/beeper-desktop-api-cli' run: | - ./scripts/link 'github.com/stainless-sdks/beeper-desktop-api-go@${{ github.ref_name }}' || true + ./scripts/link 'github.com/stainless-sdks/beeper-desktop-api-go@${{ github.ref_name }}' || go mod edit -dropreplace='github.com/stainless-sdks/beeper-desktop-api-go - name: Bootstrap run: ./scripts/bootstrap @@ -107,7 +107,7 @@ jobs: - name: Link staging branch if: github.repository == 'stainless-sdks/beeper-desktop-api-cli' run: | - ./scripts/link 'github.com/stainless-sdks/beeper-desktop-api-go@${{ github.ref_name }}' || true + ./scripts/link 'github.com/stainless-sdks/beeper-desktop-api-go@${{ github.ref_name }}' || go mod edit -dropreplace='github.com/stainless-sdks/beeper-desktop-api-go - name: Bootstrap run: ./scripts/bootstrap From 81a96d9880aee422f73677cdbe67134af37f6814 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 7 Apr 2026 02:19:52 +0000 Subject: [PATCH 21/54] fix: fix quoting typo --- .github/workflows/ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 82ce2502..a726d4d3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,7 +34,7 @@ jobs: - name: Link staging branch if: github.repository == 'stainless-sdks/beeper-desktop-api-cli' run: | - ./scripts/link 'github.com/stainless-sdks/beeper-desktop-api-go@${{ github.ref_name }}' || go mod edit -dropreplace='github.com/stainless-sdks/beeper-desktop-api-go + ./scripts/link 'github.com/stainless-sdks/beeper-desktop-api-go@${{ github.ref_name }}' || go mod edit -dropreplace='github.com/stainless-sdks/beeper-desktop-api-go' - name: Bootstrap run: ./scripts/bootstrap @@ -60,7 +60,7 @@ jobs: - name: Link staging branch if: github.repository == 'stainless-sdks/beeper-desktop-api-cli' run: | - ./scripts/link 'github.com/stainless-sdks/beeper-desktop-api-go@${{ github.ref_name }}' || go mod edit -dropreplace='github.com/stainless-sdks/beeper-desktop-api-go + ./scripts/link 'github.com/stainless-sdks/beeper-desktop-api-go@${{ github.ref_name }}' || go mod edit -dropreplace='github.com/stainless-sdks/beeper-desktop-api-go' - name: Bootstrap run: ./scripts/bootstrap @@ -107,7 +107,7 @@ jobs: - name: Link staging branch if: github.repository == 'stainless-sdks/beeper-desktop-api-cli' run: | - ./scripts/link 'github.com/stainless-sdks/beeper-desktop-api-go@${{ github.ref_name }}' || go mod edit -dropreplace='github.com/stainless-sdks/beeper-desktop-api-go + ./scripts/link 'github.com/stainless-sdks/beeper-desktop-api-go@${{ github.ref_name }}' || go mod edit -dropreplace='github.com/stainless-sdks/beeper-desktop-api-go' - name: Bootstrap run: ./scripts/bootstrap From c37cb3e7e49185c7e7e398fb70a254ea8daf2456 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 9 Apr 2026 02:35:23 +0000 Subject: [PATCH 22/54] chore(cli): let `--format raw` be used in conjunction with `--transform` --- pkg/cmd/cmdutil.go | 4 ++-- pkg/cmd/cmdutil_test.go | 42 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/pkg/cmd/cmdutil.go b/pkg/cmd/cmdutil.go index fad3c3c7..3435a90a 100644 --- a/pkg/cmd/cmdutil.go +++ b/pkg/cmd/cmdutil.go @@ -312,7 +312,7 @@ func shouldUseColors(w io.Writer) bool { } func formatJSON(expectedOutput *os.File, title string, res gjson.Result, format string, transform string) ([]byte, error) { - if format != "raw" && transform != "" { + if transform != "" { transformed := res.Get(transform) if transformed.Exists() { res = transformed @@ -356,7 +356,7 @@ func formatJSON(expectedOutput *os.File, title string, res gjson.Result, format // Display JSON to the user in various different formats func ShowJSON(out *os.File, title string, res gjson.Result, format string, transform string) error { - if format != "raw" && transform != "" { + if transform != "" { transformed := res.Get(transform) if transformed.Exists() { res = transformed diff --git a/pkg/cmd/cmdutil_test.go b/pkg/cmd/cmdutil_test.go index 8eca3979..5178057e 100644 --- a/pkg/cmd/cmdutil_test.go +++ b/pkg/cmd/cmdutil_test.go @@ -10,6 +10,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" ) func TestStreamOutput(t *testing.T) { @@ -148,3 +149,44 @@ func TestValidateBaseURL(t *testing.T) { assert.Contains(t, err.Error(), "--base-url") }) } + +func TestFormatJSON(t *testing.T) { + t.Parallel() + + t.Run("RawWithTransform", func(t *testing.T) { + t.Parallel() + + res := gjson.Parse(`{"id":"abc123","name":"test"}`) + formatted, err := formatJSON(os.Stdout, "test", res, "raw", "id") + require.NoError(t, err) + require.Equal(t, `"abc123"`+"\n", string(formatted)) + }) + + t.Run("RawWithoutTransform", func(t *testing.T) { + t.Parallel() + + res := gjson.Parse(`{"id":"abc123","name":"test"}`) + formatted, err := formatJSON(os.Stdout, "test", res, "raw", "") + require.NoError(t, err) + require.Equal(t, `{"id":"abc123","name":"test"}`+"\n", string(formatted)) + }) + + t.Run("RawWithNestedTransform", func(t *testing.T) { + t.Parallel() + + res := gjson.Parse(`{"data":{"items":[1,2,3]}}`) + formatted, err := formatJSON(os.Stdout, "test", res, "raw", "data.items") + require.NoError(t, err) + require.Equal(t, "[1,2,3]\n", string(formatted)) + }) + + t.Run("RawWithNonexistentTransform", func(t *testing.T) { + t.Parallel() + + res := gjson.Parse(`{"id":"abc123"}`) + formatted, err := formatJSON(os.Stdout, "test", res, "raw", "missing") + require.NoError(t, err) + // Transform path doesn't exist, so original result is returned + require.Equal(t, `{"id":"abc123"}`+"\n", string(formatted)) + }) +} From 79f9f751f5fd415d9a34893e47b8409aa82dcb7c Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 10 Apr 2026 02:39:30 +0000 Subject: [PATCH 23/54] chore(cli): additional test cases for `ShowJSONIterator` --- pkg/cmd/cmdutil_test.go | 78 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/pkg/cmd/cmdutil_test.go b/pkg/cmd/cmdutil_test.go index 5178057e..024fbe64 100644 --- a/pkg/cmd/cmdutil_test.go +++ b/pkg/cmd/cmdutil_test.go @@ -11,6 +11,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/tidwall/gjson" + + "github.com/beeper/desktop-api-cli/internal/jsonview" ) func TestStreamOutput(t *testing.T) { @@ -190,3 +192,79 @@ func TestFormatJSON(t *testing.T) { require.Equal(t, `{"id":"abc123"}`+"\n", string(formatted)) }) } + +func TestShowJSONIterator(t *testing.T) { + t.Parallel() + + t.Run("RawMultipleItems", func(t *testing.T) { + t.Parallel() + + iter := &sliceIterator[map[string]any]{items: []map[string]any{ + {"id": "abc", "name": "first"}, + {"id": "def", "name": "second"}, + }} + captured := captureShowJSONIterator(t, iter, "raw", "", -1) + assert.Equal(t, `{"id":"abc","name":"first"}`+"\n"+`{"id":"def","name":"second"}`+"\n", captured) + }) + + t.Run("RawWithTransform", func(t *testing.T) { + t.Parallel() + + iter := &sliceIterator[map[string]any]{items: []map[string]any{ + {"id": "abc", "name": "first"}, + {"id": "def", "name": "second"}, + }} + captured := captureShowJSONIterator(t, iter, "raw", "id", -1) + assert.Equal(t, `"abc"`+"\n"+`"def"`+"\n", captured) + }) + + t.Run("LimitItems", func(t *testing.T) { + t.Parallel() + + iter := &sliceIterator[map[string]any]{items: []map[string]any{ + {"id": "abc"}, + {"id": "def"}, + {"id": "ghi"}, + }} + captured := captureShowJSONIterator(t, iter, "raw", "", 2) + assert.Equal(t, `{"id":"abc"}`+"\n"+`{"id":"def"}`+"\n", captured) + }) +} + +// sliceIterator is a simple iterator over a slice for testing. +type sliceIterator[T any] struct { + index int + items []T +} + +func (it *sliceIterator[T]) Next() bool { + it.index++ + return it.index <= len(it.items) +} + +func (it *sliceIterator[T]) Current() T { + return it.items[it.index-1] +} + +func (it *sliceIterator[T]) Err() error { + return nil +} + +var _ jsonview.Iterator[any] = (*sliceIterator[any])(nil) + +// captureShowJSONIterator runs ShowJSONIterator and captures the output written to a file. +func captureShowJSONIterator[T any](t *testing.T, iter jsonview.Iterator[T], format, transform string, itemsToDisplay int64) string { + t.Helper() + + r, w, err := os.Pipe() + require.NoError(t, err) + defer r.Close() + + err = ShowJSONIterator(w, "test", iter, format, transform, itemsToDisplay) + w.Close() + require.NoError(t, err) + + var buf bytes.Buffer + _, _ = buf.ReadFrom(r) + return buf.String() +} From 5480c0e637e57e45ef1780847a2ec1638ef8550d Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 10 Apr 2026 02:40:54 +0000 Subject: [PATCH 24/54] fix: fix for failing to drop invalid module replace in link script --- .github/workflows/ci.yml | 6 +++--- scripts/link | 8 ++++++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a726d4d3..940785a8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,7 +34,7 @@ jobs: - name: Link staging branch if: github.repository == 'stainless-sdks/beeper-desktop-api-cli' run: | - ./scripts/link 'github.com/stainless-sdks/beeper-desktop-api-go@${{ github.ref_name }}' || go mod edit -dropreplace='github.com/stainless-sdks/beeper-desktop-api-go' + ./scripts/link 'github.com/stainless-sdks/beeper-desktop-api-go@${{ github.ref_name }}' || true - name: Bootstrap run: ./scripts/bootstrap @@ -60,7 +60,7 @@ jobs: - name: Link staging branch if: github.repository == 'stainless-sdks/beeper-desktop-api-cli' run: | - ./scripts/link 'github.com/stainless-sdks/beeper-desktop-api-go@${{ github.ref_name }}' || go mod edit -dropreplace='github.com/stainless-sdks/beeper-desktop-api-go' + ./scripts/link 'github.com/stainless-sdks/beeper-desktop-api-go@${{ github.ref_name }}' || true - name: Bootstrap run: ./scripts/bootstrap @@ -107,7 +107,7 @@ jobs: - name: Link staging branch if: github.repository == 'stainless-sdks/beeper-desktop-api-cli' run: | - ./scripts/link 'github.com/stainless-sdks/beeper-desktop-api-go@${{ github.ref_name }}' || go mod edit -dropreplace='github.com/stainless-sdks/beeper-desktop-api-go' + ./scripts/link 'github.com/stainless-sdks/beeper-desktop-api-go@${{ github.ref_name }}' || true - name: Bootstrap run: ./scripts/bootstrap diff --git a/scripts/link b/scripts/link index 2da9715f..332584c6 100755 --- a/scripts/link +++ b/scripts/link @@ -9,5 +9,9 @@ export GOPRIVATE="${GOPRIVATE:+$GOPRIVATE,}github.com/beeper/desktop-api-go,gith REPLACEMENT="${1:-"../beeperdesktop-go"}" echo "==> Replacing Go SDK with $REPLACEMENT" -go mod edit -replace github.com/beeper/desktop-api-go="$REPLACEMENT" -go mod tidy -e +if [[ -d "$REPLACEMENT" ]] || go list -m "$REPLACEMENT" >/dev/null; then + go mod edit -replace github.com/beeper/desktop-api-go="$REPLACEMENT" + go mod tidy -e +else + echo "Skipping Go SDK replacement (branch may not exist on Go SDK)" +fi From ccd116df91e25275b2f63f53bedf9939aae8a64a Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sun, 12 Apr 2026 18:31:54 +0000 Subject: [PATCH 25/54] feat(api): add network, bridge fields to accounts --- .stats.yml | 6 ++-- README.md | 3 +- pkg/cmd/chat.go | 74 +++----------------------------------------- pkg/cmd/chat_test.go | 46 +++------------------------ 4 files changed, 13 insertions(+), 116 deletions(-) diff --git a/.stats.yml b/.stats.yml index 2c479249..229f6b5f 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 23 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-5a8ac7b545c48dc892e5c680303e305254921554dabee848e40a808659dbcf1e.yml -openapi_spec_hash: 0103975601aac1445d3a4ef418c5d17a -config_hash: 7d85c0b454fc78a59db6474c5c4d73c6 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-611aa7641fbca8cf31d626bf86f9efd3c2b92778e897ebbb25c6ea44185ed1ed.yml +openapi_spec_hash: d6c0a1776048dab04f6c5625c9893c9c +config_hash: 39ed0717b5f415499aaace2468346e1a diff --git a/README.md b/README.md index 09e97fbd..d1e4df93 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,6 @@ The official CLI for the [Beeper Desktop REST API](https://developers.beeper.com/desktop-api/). -It is generated with [Stainless](https://www.stainless.com/). - ## Installation @@ -55,6 +53,7 @@ beeper-desktop-cli [resource] [flags...] ```sh beeper-desktop-cli chats search \ + --access-token 'My Access Token' \ --include-muted \ --limit 3 \ --type single diff --git a/pkg/cmd/chat.go b/pkg/cmd/chat.go index 41a766b1..afa01f8c 100644 --- a/pkg/cmd/chat.go +++ b/pkg/cmd/chat.go @@ -15,85 +15,19 @@ import ( "github.com/urfave/cli/v3" ) -var chatsCreate = requestflag.WithInnerFlags(cli.Command{ +var chatsCreate = cli.Command{ Name: "create", Usage: "Create a single/group chat (mode='create') or start a direct chat from merged\nuser data (mode='start').", Suggest: true, Flags: []cli.Flag{ - &requestflag.Flag[string]{ - Name: "account-id", - Usage: "Account to create or start the chat on.", - Required: true, - BodyPath: "accountID", - }, - &requestflag.Flag[bool]{ - Name: "allow-invite", - Usage: "Whether invite-based DM creation is allowed when required by the platform. Used for mode='start'.", - Default: true, - BodyPath: "allowInvite", - }, - &requestflag.Flag[string]{ - Name: "message-text", - Usage: "Optional first message content if the platform requires it to create the chat.", - BodyPath: "messageText", - }, - &requestflag.Flag[string]{ - Name: "mode", - Usage: "Operation mode. Defaults to 'create' when omitted.", - BodyPath: "mode", - }, - &requestflag.Flag[[]string]{ - Name: "participant-id", - Usage: "Required when mode='create'. User IDs to include in the new chat.", - BodyPath: "participantIDs", - }, - &requestflag.Flag[string]{ - Name: "title", - Usage: "Optional title for group chats when mode='create'; ignored for single chats on most platforms.", - BodyPath: "title", - }, - &requestflag.Flag[string]{ - Name: "type", - Usage: "Required when mode='create'. 'single' requires exactly one participantID; 'group' supports multiple participants and optional title.", - BodyPath: "type", - }, &requestflag.Flag[map[string]any]{ - Name: "user", - Usage: "Required when mode='start'. Merged user-like contact payload used to resolve the best identifier.", - BodyPath: "user", + Name: "params", + BodyRoot: true, }, }, Action: handleChatsCreate, HideHelpCommand: true, -}, map[string][]requestflag.HasOuterFlag{ - "user": { - &requestflag.InnerFlag[string]{ - Name: "user.id", - Usage: "Known user ID when available.", - InnerField: "id", - }, - &requestflag.InnerFlag[string]{ - Name: "user.email", - Usage: "Email candidate.", - InnerField: "email", - }, - &requestflag.InnerFlag[string]{ - Name: "user.full-name", - Usage: "Display name hint used for ranking only.", - InnerField: "fullName", - }, - &requestflag.InnerFlag[string]{ - Name: "user.phone-number", - Usage: "Phone number candidate (E.164 preferred).", - InnerField: "phoneNumber", - }, - &requestflag.InnerFlag[string]{ - Name: "user.username", - Usage: "Username/handle candidate.", - InnerField: "username", - }, - }, -}) +} var chatsRetrieve = cli.Command{ Name: "retrieve", diff --git a/pkg/cmd/chat_test.go b/pkg/cmd/chat_test.go index ff041f79..ea462e1d 100644 --- a/pkg/cmd/chat_test.go +++ b/pkg/cmd/chat_test.go @@ -6,7 +6,6 @@ import ( "testing" "github.com/beeper/desktop-api-cli/internal/mocktest" - "github.com/beeper/desktop-api-cli/internal/requestflag" ) func TestChatsCreate(t *testing.T) { @@ -15,38 +14,7 @@ func TestChatsCreate(t *testing.T) { t, "--access-token", "string", "chats", "create", - "--account-id", "accountID", - "--allow-invite=true", - "--message-text", "messageText", - "--mode", "create", - "--participant-id", "string", - "--title", "title", - "--type", "single", - "--user", "{id: id, email: email, fullName: fullName, phoneNumber: phoneNumber, username: username}", - ) - }) - - t.Run("inner flags", func(t *testing.T) { - // Check that inner flags have been set up correctly - requestflag.CheckInnerFlags(chatsCreate) - - // Alternative argument passing style using inner flags - mocktest.TestRunMockTestWithFlags( - t, - "--access-token", "string", - "chats", "create", - "--account-id", "accountID", - "--allow-invite=true", - "--message-text", "messageText", - "--mode", "create", - "--participant-id", "string", - "--title", "title", - "--type", "single", - "--user.id", "id", - "--user.email", "email", - "--user.full-name", "fullName", - "--user.phone-number", "phoneNumber", - "--user.username", "username", + "--params", "{accountID: accountID, mode: start, user: {id: id, email: email, fullName: fullName, phoneNumber: phoneNumber, username: username}, allowInvite: true, messageText: messageText}", ) }) @@ -54,19 +22,15 @@ func TestChatsCreate(t *testing.T) { // Test piping YAML data over stdin pipeData := []byte("" + "accountID: accountID\n" + - "allowInvite: true\n" + - "messageText: messageText\n" + - "mode: create\n" + - "participantIDs:\n" + - " - string\n" + - "title: title\n" + - "type: single\n" + + "mode: start\n" + "user:\n" + " id: id\n" + " email: email\n" + " fullName: fullName\n" + " phoneNumber: phoneNumber\n" + - " username: username\n") + " username: username\n" + + "allowInvite: true\n" + + "messageText: messageText\n") mocktest.TestRunMockTestWithPipeAndFlags( t, pipeData, "--access-token", "string", From b57a69441818f20bf3d1bbf949a30b65c55aa728 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 14 Apr 2026 02:16:37 +0000 Subject: [PATCH 26/54] chore: add documentation for ./scripts/link --- README.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/README.md b/README.md index d1e4df93..de0b813e 100644 --- a/README.md +++ b/README.md @@ -118,3 +118,23 @@ base64-encoding). Note that absolute paths will begin with `@file://` or ```bash beeper-desktop-cli --arg @data://file.txt ``` + +## Linking different Go SDK versions + +You can link the CLI against a different version of the Beeper Desktop 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/beeperdesktopapi-go +``` + +If you run the link script without any arguments, it will default to `../beeperdesktopapi-go`. From de64b1c7b2620be74c563e4da9ae4ff6a486555e Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 15 Apr 2026 02:15:22 +0000 Subject: [PATCH 27/54] chore(cli): fall back to JSON when using default "explore" with non-TTY --- cmd/beeper-desktop-cli/main.go | 2 +- pkg/cmd/account.go | 3 +- pkg/cmd/accountcontact.go | 8 ++-- pkg/cmd/asset.go | 9 +++-- pkg/cmd/beeperdesktopapi.go | 6 ++- pkg/cmd/chat.go | 16 +++++--- pkg/cmd/chatmessagereaction.go | 6 ++- pkg/cmd/cmdutil.go | 29 +++++++++++--- pkg/cmd/cmdutil_test.go | 70 +++++++++++++++++++++++++++++++++- pkg/cmd/info.go | 3 +- pkg/cmd/message.go | 16 +++++--- 11 files changed, 136 insertions(+), 32 deletions(-) diff --git a/cmd/beeper-desktop-cli/main.go b/cmd/beeper-desktop-cli/main.go index 8ac76ade..a8ca8c2e 100644 --- a/cmd/beeper-desktop-cli/main.go +++ b/cmd/beeper-desktop-cli/main.go @@ -43,7 +43,7 @@ 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(os.Stdout, os.Stderr, "Error", json, format, app.IsSet("format-error"), app.String("transform-error")) if show_err != nil { // Just print the original error: fmt.Fprintf(os.Stderr, "%s\n", err.Error()) diff --git a/pkg/cmd/account.go b/pkg/cmd/account.go index 6ee17743..e779c12d 100644 --- a/pkg/cmd/account.go +++ b/pkg/cmd/account.go @@ -51,6 +51,7 @@ func handleAccountsList(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, "accounts list", obj, format, transform) + return ShowJSON(os.Stdout, os.Stderr, "accounts list", obj, format, explicitFormat, transform) } diff --git a/pkg/cmd/accountcontact.go b/pkg/cmd/accountcontact.go index 76cc8625..6ab3c043 100644 --- a/pkg/cmd/accountcontact.go +++ b/pkg/cmd/accountcontact.go @@ -101,6 +101,7 @@ func handleAccountsContactsList(ctx context.Context, cmd *cli.Command) error { } format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") if format == "raw" { var res []byte @@ -115,7 +116,7 @@ func handleAccountsContactsList(ctx context.Context, cmd *cli.Command) error { return err } obj := gjson.ParseBytes(res) - return ShowJSON(os.Stdout, "accounts:contacts list", obj, format, transform) + return ShowJSON(os.Stdout, os.Stderr, "accounts:contacts list", obj, format, explicitFormat, transform) } else { iter := client.Accounts.Contacts.ListAutoPaging( ctx, @@ -127,7 +128,7 @@ func handleAccountsContactsList(ctx context.Context, cmd *cli.Command) error { if cmd.IsSet("max-items") { maxItems = cmd.Value("max-items").(int64) } - return ShowJSONIterator(os.Stdout, "accounts:contacts list", iter, format, transform, maxItems) + return ShowJSONIterator(os.Stdout, os.Stderr, "accounts:contacts list", iter, format, explicitFormat, transform, maxItems) } } @@ -169,6 +170,7 @@ func handleAccountsContactsSearch(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, "accounts:contacts search", obj, format, transform) + return ShowJSON(os.Stdout, os.Stderr, "accounts:contacts search", obj, format, explicitFormat, transform) } diff --git a/pkg/cmd/asset.go b/pkg/cmd/asset.go index 65023e3d..160f3177 100644 --- a/pkg/cmd/asset.go +++ b/pkg/cmd/asset.go @@ -130,8 +130,9 @@ func handleAssetsDownload(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, "assets download", obj, format, transform) + return ShowJSON(os.Stdout, os.Stderr, "assets download", obj, format, explicitFormat, transform) } func handleAssetsServe(ctx context.Context, cmd *cli.Command) error { @@ -188,8 +189,9 @@ func handleAssetsUpload(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, "assets upload", obj, format, transform) + return ShowJSON(os.Stdout, os.Stderr, "assets upload", obj, format, explicitFormat, transform) } func handleAssetsUploadBase64(ctx context.Context, cmd *cli.Command) error { @@ -222,6 +224,7 @@ func handleAssetsUploadBase64(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, "assets upload-base64", obj, format, transform) + return ShowJSON(os.Stdout, os.Stderr, "assets upload-base64", obj, format, explicitFormat, transform) } diff --git a/pkg/cmd/beeperdesktopapi.go b/pkg/cmd/beeperdesktopapi.go index bbef6b7d..e55d9f29 100644 --- a/pkg/cmd/beeperdesktopapi.go +++ b/pkg/cmd/beeperdesktopapi.go @@ -91,8 +91,9 @@ func handleFocus(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, "focus", obj, format, transform) + return ShowJSON(os.Stdout, os.Stderr, "focus", obj, format, explicitFormat, transform) } func handleSearch(ctx context.Context, cmd *cli.Command) error { @@ -125,6 +126,7 @@ func handleSearch(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, "search", obj, format, transform) + return ShowJSON(os.Stdout, os.Stderr, "search", obj, format, explicitFormat, transform) } diff --git a/pkg/cmd/chat.go b/pkg/cmd/chat.go index afa01f8c..730ecfd5 100644 --- a/pkg/cmd/chat.go +++ b/pkg/cmd/chat.go @@ -208,8 +208,9 @@ func handleChatsCreate(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, "chats create", obj, format, transform) + return ShowJSON(os.Stdout, os.Stderr, "chats create", obj, format, explicitFormat, transform) } func handleChatsRetrieve(ctx context.Context, cmd *cli.Command) error { @@ -250,8 +251,9 @@ func handleChatsRetrieve(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, "chats retrieve", obj, format, transform) + return ShowJSON(os.Stdout, os.Stderr, "chats retrieve", obj, format, explicitFormat, transform) } func handleChatsList(ctx context.Context, cmd *cli.Command) error { @@ -276,6 +278,7 @@ func handleChatsList(ctx context.Context, cmd *cli.Command) error { } format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") if format == "raw" { var res []byte @@ -285,14 +288,14 @@ func handleChatsList(ctx context.Context, cmd *cli.Command) error { return err } obj := gjson.ParseBytes(res) - return ShowJSON(os.Stdout, "chats list", obj, format, transform) + return ShowJSON(os.Stdout, os.Stderr, "chats list", obj, format, explicitFormat, transform) } else { iter := client.Chats.ListAutoPaging(ctx, params, options...) maxItems := int64(-1) if cmd.IsSet("max-items") { maxItems = cmd.Value("max-items").(int64) } - return ShowJSONIterator(os.Stdout, "chats list", iter, format, transform, maxItems) + return ShowJSONIterator(os.Stdout, os.Stderr, "chats list", iter, format, explicitFormat, transform, maxItems) } } @@ -350,6 +353,7 @@ func handleChatsSearch(ctx context.Context, cmd *cli.Command) error { } format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") if format == "raw" { var res []byte @@ -359,13 +363,13 @@ func handleChatsSearch(ctx context.Context, cmd *cli.Command) error { return err } obj := gjson.ParseBytes(res) - return ShowJSON(os.Stdout, "chats search", obj, format, transform) + return ShowJSON(os.Stdout, os.Stderr, "chats search", obj, format, explicitFormat, transform) } else { iter := client.Chats.SearchAutoPaging(ctx, params, options...) maxItems := int64(-1) if cmd.IsSet("max-items") { maxItems = cmd.Value("max-items").(int64) } - return ShowJSONIterator(os.Stdout, "chats search", iter, format, transform, maxItems) + return ShowJSONIterator(os.Stdout, os.Stderr, "chats search", iter, format, explicitFormat, transform, maxItems) } } diff --git a/pkg/cmd/chatmessagereaction.go b/pkg/cmd/chatmessagereaction.go index f72f1894..ee0615f0 100644 --- a/pkg/cmd/chatmessagereaction.go +++ b/pkg/cmd/chatmessagereaction.go @@ -110,8 +110,9 @@ func handleChatsMessagesReactionsDelete(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, "chats:messages:reactions delete", obj, format, transform) + return ShowJSON(os.Stdout, os.Stderr, "chats:messages:reactions delete", obj, format, explicitFormat, transform) } func handleChatsMessagesReactionsAdd(ctx context.Context, cmd *cli.Command) error { @@ -154,6 +155,7 @@ func handleChatsMessagesReactionsAdd(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, "chats:messages:reactions add", obj, format, transform) + return ShowJSON(os.Stdout, os.Stderr, "chats:messages:reactions add", obj, format, explicitFormat, transform) } diff --git a/pkg/cmd/cmdutil.go b/pkg/cmd/cmdutil.go index 3435a90a..5163bc13 100644 --- a/pkg/cmd/cmdutil.go +++ b/pkg/cmd/cmdutil.go @@ -354,8 +354,13 @@ func formatJSON(expectedOutput *os.File, title string, res gjson.Result, format } } -// Display JSON to the user in various different formats -func ShowJSON(out *os.File, title string, res gjson.Result, format string, transform string) error { +const warningExploreNotSupported = "Warning: Output format 'explore' not supported for non-terminal output; falling back to 'json'\n" + +// Display JSON to the user in various different formats. The explicitFormat parameter indicates +// whether the format was explicitly set by the user (via --format), which controls whether we +// silently fall back to json when explore is requested on non-terminal output. Warnings are +// written to stderr. +func ShowJSON(out *os.File, stderr io.Writer, title string, res gjson.Result, format string, explicitFormat bool, transform string) error { if transform != "" { transformed := res.Get(transform) if transformed.Exists() { @@ -365,8 +370,14 @@ func ShowJSON(out *os.File, title string, res gjson.Result, format string, trans switch strings.ToLower(format) { case "auto": - return ShowJSON(out, title, res, "json", "") + return ShowJSON(out, stderr, title, res, "json", explicitFormat, "") case "explore": + if !isTerminal(out) { + if explicitFormat { + fmt.Fprint(stderr, warningExploreNotSupported) + } + return ShowJSON(out, stderr, title, res, "json", explicitFormat, transform) + } return jsonview.ExploreJSON(title, res) default: bytes, err := formatJSON(out, title, res, format, transform) @@ -391,9 +402,15 @@ type hasRawJSON interface { // 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 { +func ShowJSONIterator[T any](stdout *os.File, stderr io.Writer, title string, iter jsonview.Iterator[T], format string, explicitFormat bool, transform string, itemsToDisplay int64) error { if format == "explore" { - return jsonview.ExploreJSONStream(title, iter) + if isTerminal(stdout) { + return jsonview.ExploreJSONStream(title, iter) + } + if explicitFormat { + fmt.Fprint(stderr, warningExploreNotSupported) + } + format = "json" } terminalWidth, terminalHeight, err := term.GetSize(os.Stdout.Fd()) @@ -466,7 +483,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(pager, stderr, title, obj, format, explicitFormat, transform); err != nil { return err } itemsToDisplay -= 1 diff --git a/pkg/cmd/cmdutil_test.go b/pkg/cmd/cmdutil_test.go index 024fbe64..aaf0b1b6 100644 --- a/pkg/cmd/cmdutil_test.go +++ b/pkg/cmd/cmdutil_test.go @@ -231,6 +231,73 @@ 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(w, &stderr, "test", res, "explore", false, "") + 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(w, &stderr, "test", res, "explore", true, "") + 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(w, &stderr, "test", res, "explore", false, "") + 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 +327,8 @@ 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) + var stderr bytes.Buffer + err = ShowJSONIterator(w, &stderr, "test", iter, format, false, transform, itemsToDisplay) w.Close() require.NoError(t, err) diff --git a/pkg/cmd/info.go b/pkg/cmd/info.go index 2b46d494..2272e2b0 100644 --- a/pkg/cmd/info.go +++ b/pkg/cmd/info.go @@ -51,6 +51,7 @@ func handleInfoRetrieve(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, "info retrieve", obj, format, transform) + return ShowJSON(os.Stdout, os.Stderr, "info retrieve", obj, format, explicitFormat, transform) } diff --git a/pkg/cmd/message.go b/pkg/cmd/message.go index b65cef7b..0e0a0974 100644 --- a/pkg/cmd/message.go +++ b/pkg/cmd/message.go @@ -254,8 +254,9 @@ func handleMessagesUpdate(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 update", obj, format, transform) + return ShowJSON(os.Stdout, os.Stderr, "messages update", obj, format, explicitFormat, transform) } func handleMessagesList(ctx context.Context, cmd *cli.Command) error { @@ -283,6 +284,7 @@ func handleMessagesList(ctx context.Context, cmd *cli.Command) error { } format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") if format == "raw" { var res []byte @@ -297,7 +299,7 @@ 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(os.Stdout, os.Stderr, "messages list", obj, format, explicitFormat, transform) } else { iter := client.Messages.ListAutoPaging( ctx, @@ -309,7 +311,7 @@ func handleMessagesList(ctx context.Context, cmd *cli.Command) error { if cmd.IsSet("max-items") { maxItems = cmd.Value("max-items").(int64) } - return ShowJSONIterator(os.Stdout, "messages list", iter, format, transform, maxItems) + return ShowJSONIterator(os.Stdout, os.Stderr, "messages list", iter, format, explicitFormat, transform, maxItems) } } @@ -335,6 +337,7 @@ func handleMessagesSearch(ctx context.Context, cmd *cli.Command) error { } format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") if format == "raw" { var res []byte @@ -344,14 +347,14 @@ func handleMessagesSearch(ctx context.Context, cmd *cli.Command) error { return err } obj := gjson.ParseBytes(res) - return ShowJSON(os.Stdout, "messages search", obj, format, transform) + return ShowJSON(os.Stdout, os.Stderr, "messages search", obj, format, explicitFormat, transform) } else { iter := client.Messages.SearchAutoPaging(ctx, params, options...) maxItems := int64(-1) if cmd.IsSet("max-items") { maxItems = cmd.Value("max-items").(int64) } - return ShowJSONIterator(os.Stdout, "messages search", iter, format, transform, maxItems) + return ShowJSONIterator(os.Stdout, os.Stderr, "messages search", iter, format, explicitFormat, transform, maxItems) } } @@ -393,6 +396,7 @@ 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(os.Stdout, "messages send", obj, format, transform) + return ShowJSON(os.Stdout, os.Stderr, "messages send", obj, format, explicitFormat, transform) } From 301f6f2894cae408e91bb5834d50d6c16b78c28f Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 15 Apr 2026 02:16:39 +0000 Subject: [PATCH 28/54] feat(cli): alias parameters in data with `x-stainless-cli-data-alias` --- internal/requestflag/innerflag.go | 19 ++++++++++-- internal/requestflag/requestflag.go | 9 ++++++ pkg/cmd/flagoptions.go | 47 +++++++++++++++++++++++++++++ 3 files changed, 73 insertions(+), 2 deletions(-) diff --git a/internal/requestflag/innerflag.go b/internal/requestflag/innerflag.go index 102624fd..eeeb8bc3 100644 --- a/internal/requestflag/innerflag.go +++ b/internal/requestflag/innerflag.go @@ -22,14 +22,29 @@ 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 +} + +// 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) { diff --git a/internal/requestflag/requestflag.go b/internal/requestflag/requestflag.go index bdef64f2..bfaf064d 100644 --- a/internal/requestflag/requestflag.go +++ b/internal/requestflag/requestflag.go @@ -48,6 +48,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 @@ -65,6 +69,7 @@ type InRequest interface { GetBodyPath() string IsBodyRoot() bool IsFileInput() bool + GetDataAliases() []string } func (f Flag[T]) GetQueryPath() string { @@ -87,6 +92,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 diff --git a/pkg/cmd/flagoptions.go b/pkg/cmd/flagoptions.go index 198b295e..4cc19658 100644 --- a/pkg/cmd/flagoptions.go +++ b/pkg/cmd/flagoptions.go @@ -309,6 +309,12 @@ 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() { pipeData, err := io.ReadAll(os.Stdin) @@ -323,6 +329,7 @@ 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 { + applyDataAliases(cmd, bodyMap) if flagMap, ok := requestContents.Body.(map[string]any); ok { maps.Copy(bodyMap, flagMap) requestContents.Body = bodyMap @@ -485,6 +492,46 @@ func flagOptions( // as a file path without needing the "@" prefix. type FilePathValue string +// 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 From d4e5630718dc3170dac5f9efe0acfd0be2f1a390 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 16 Apr 2026 02:11:59 +0000 Subject: [PATCH 29/54] chore(cli): switch long lists of positional args over to param structs --- cmd/beeper-desktop-cli/main.go | 7 ++- pkg/cmd/account.go | 8 +++- pkg/cmd/accountcontact.go | 22 +++++++-- pkg/cmd/asset.go | 22 +++++++-- pkg/cmd/beeperdesktopapi.go | 15 ++++-- pkg/cmd/chat.go | 43 ++++++++++++++--- pkg/cmd/chatmessagereaction.go | 15 ++++-- pkg/cmd/cmdutil.go | 84 ++++++++++++++++++++++------------ pkg/cmd/cmdutil_test.go | 31 +++++++++++-- pkg/cmd/info.go | 8 +++- pkg/cmd/message.go | 43 ++++++++++++++--- 11 files changed, 231 insertions(+), 67 deletions(-) diff --git a/cmd/beeper-desktop-cli/main.go b/cmd/beeper-desktop-cli/main.go index a8ca8c2e..c98a9264 100644 --- a/cmd/beeper-desktop-cli/main.go +++ b/cmd/beeper-desktop-cli/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, os.Stderr, "Error", json, format, app.IsSet("format-error"), 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/pkg/cmd/account.go b/pkg/cmd/account.go index e779c12d..c2809b0b 100644 --- a/pkg/cmd/account.go +++ b/pkg/cmd/account.go @@ -5,7 +5,6 @@ package cmd import ( "context" "fmt" - "os" "github.com/beeper/desktop-api-cli/internal/apiquery" "github.com/beeper/desktop-api-go" @@ -53,5 +52,10 @@ func handleAccountsList(ctx context.Context, cmd *cli.Command) error { format := cmd.Root().String("format") explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, os.Stderr, "accounts list", obj, format, explicitFormat, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + Title: "accounts list", + Transform: transform, + }) } diff --git a/pkg/cmd/accountcontact.go b/pkg/cmd/accountcontact.go index 6ab3c043..c8ec6e35 100644 --- a/pkg/cmd/accountcontact.go +++ b/pkg/cmd/accountcontact.go @@ -5,7 +5,6 @@ package cmd import ( "context" "fmt" - "os" "github.com/beeper/desktop-api-cli/internal/apiquery" "github.com/beeper/desktop-api-cli/internal/requestflag" @@ -116,7 +115,12 @@ func handleAccountsContactsList(ctx context.Context, cmd *cli.Command) error { return err } obj := gjson.ParseBytes(res) - return ShowJSON(os.Stdout, os.Stderr, "accounts:contacts list", obj, format, explicitFormat, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + Title: "accounts:contacts list", + Transform: transform, + }) } else { iter := client.Accounts.Contacts.ListAutoPaging( ctx, @@ -128,7 +132,12 @@ func handleAccountsContactsList(ctx context.Context, cmd *cli.Command) error { if cmd.IsSet("max-items") { maxItems = cmd.Value("max-items").(int64) } - return ShowJSONIterator(os.Stdout, os.Stderr, "accounts:contacts list", iter, format, explicitFormat, transform, maxItems) + return ShowJSONIterator(iter, maxItems, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + Title: "accounts:contacts list", + Transform: transform, + }) } } @@ -172,5 +181,10 @@ func handleAccountsContactsSearch(ctx context.Context, cmd *cli.Command) error { format := cmd.Root().String("format") explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, os.Stderr, "accounts:contacts search", obj, format, explicitFormat, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + Title: "accounts:contacts search", + Transform: transform, + }) } diff --git a/pkg/cmd/asset.go b/pkg/cmd/asset.go index 160f3177..227a235b 100644 --- a/pkg/cmd/asset.go +++ b/pkg/cmd/asset.go @@ -5,7 +5,6 @@ package cmd import ( "context" "fmt" - "os" "github.com/beeper/desktop-api-cli/internal/apiquery" "github.com/beeper/desktop-api-cli/internal/requestflag" @@ -132,7 +131,12 @@ func handleAssetsDownload(ctx context.Context, cmd *cli.Command) error { format := cmd.Root().String("format") explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, os.Stderr, "assets download", obj, format, explicitFormat, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + Title: "assets download", + Transform: transform, + }) } func handleAssetsServe(ctx context.Context, cmd *cli.Command) error { @@ -191,7 +195,12 @@ func handleAssetsUpload(ctx context.Context, cmd *cli.Command) error { format := cmd.Root().String("format") explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, os.Stderr, "assets upload", obj, format, explicitFormat, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + Title: "assets upload", + Transform: transform, + }) } func handleAssetsUploadBase64(ctx context.Context, cmd *cli.Command) error { @@ -226,5 +235,10 @@ func handleAssetsUploadBase64(ctx context.Context, cmd *cli.Command) error { format := cmd.Root().String("format") explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, os.Stderr, "assets upload-base64", obj, format, explicitFormat, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + Title: "assets upload-base64", + Transform: transform, + }) } diff --git a/pkg/cmd/beeperdesktopapi.go b/pkg/cmd/beeperdesktopapi.go index e55d9f29..a948c5a8 100644 --- a/pkg/cmd/beeperdesktopapi.go +++ b/pkg/cmd/beeperdesktopapi.go @@ -5,7 +5,6 @@ package cmd import ( "context" "fmt" - "os" "github.com/beeper/desktop-api-cli/internal/apiquery" "github.com/beeper/desktop-api-cli/internal/requestflag" @@ -93,7 +92,12 @@ func handleFocus(ctx context.Context, cmd *cli.Command) error { format := cmd.Root().String("format") explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, os.Stderr, "focus", obj, format, explicitFormat, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + Title: "focus", + Transform: transform, + }) } func handleSearch(ctx context.Context, cmd *cli.Command) error { @@ -128,5 +132,10 @@ func handleSearch(ctx context.Context, cmd *cli.Command) error { format := cmd.Root().String("format") explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, os.Stderr, "search", obj, format, explicitFormat, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + Title: "search", + Transform: transform, + }) } diff --git a/pkg/cmd/chat.go b/pkg/cmd/chat.go index 730ecfd5..60cb728c 100644 --- a/pkg/cmd/chat.go +++ b/pkg/cmd/chat.go @@ -5,7 +5,6 @@ package cmd import ( "context" "fmt" - "os" "github.com/beeper/desktop-api-cli/internal/apiquery" "github.com/beeper/desktop-api-cli/internal/requestflag" @@ -210,7 +209,12 @@ func handleChatsCreate(ctx context.Context, cmd *cli.Command) error { format := cmd.Root().String("format") explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, os.Stderr, "chats create", obj, format, explicitFormat, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + Title: "chats create", + Transform: transform, + }) } func handleChatsRetrieve(ctx context.Context, cmd *cli.Command) error { @@ -253,7 +257,12 @@ func handleChatsRetrieve(ctx context.Context, cmd *cli.Command) error { format := cmd.Root().String("format") explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, os.Stderr, "chats retrieve", obj, format, explicitFormat, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + Title: "chats retrieve", + Transform: transform, + }) } func handleChatsList(ctx context.Context, cmd *cli.Command) error { @@ -288,14 +297,24 @@ func handleChatsList(ctx context.Context, cmd *cli.Command) error { return err } obj := gjson.ParseBytes(res) - return ShowJSON(os.Stdout, os.Stderr, "chats list", obj, format, explicitFormat, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + Title: "chats list", + Transform: transform, + }) } else { iter := client.Chats.ListAutoPaging(ctx, params, options...) maxItems := int64(-1) if cmd.IsSet("max-items") { maxItems = cmd.Value("max-items").(int64) } - return ShowJSONIterator(os.Stdout, os.Stderr, "chats list", iter, format, explicitFormat, transform, maxItems) + return ShowJSONIterator(iter, maxItems, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + Title: "chats list", + Transform: transform, + }) } } @@ -363,13 +382,23 @@ func handleChatsSearch(ctx context.Context, cmd *cli.Command) error { return err } obj := gjson.ParseBytes(res) - return ShowJSON(os.Stdout, os.Stderr, "chats search", obj, format, explicitFormat, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + Title: "chats search", + Transform: transform, + }) } else { iter := client.Chats.SearchAutoPaging(ctx, params, options...) maxItems := int64(-1) if cmd.IsSet("max-items") { maxItems = cmd.Value("max-items").(int64) } - return ShowJSONIterator(os.Stdout, os.Stderr, "chats search", iter, format, explicitFormat, transform, maxItems) + return ShowJSONIterator(iter, maxItems, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + Title: "chats search", + Transform: transform, + }) } } diff --git a/pkg/cmd/chatmessagereaction.go b/pkg/cmd/chatmessagereaction.go index ee0615f0..da64dc4a 100644 --- a/pkg/cmd/chatmessagereaction.go +++ b/pkg/cmd/chatmessagereaction.go @@ -5,7 +5,6 @@ package cmd import ( "context" "fmt" - "os" "github.com/beeper/desktop-api-cli/internal/apiquery" "github.com/beeper/desktop-api-cli/internal/requestflag" @@ -112,7 +111,12 @@ func handleChatsMessagesReactionsDelete(ctx context.Context, cmd *cli.Command) e format := cmd.Root().String("format") explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, os.Stderr, "chats:messages:reactions delete", obj, format, explicitFormat, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + Title: "chats:messages:reactions delete", + Transform: transform, + }) } func handleChatsMessagesReactionsAdd(ctx context.Context, cmd *cli.Command) error { @@ -157,5 +161,10 @@ func handleChatsMessagesReactionsAdd(ctx context.Context, cmd *cli.Command) erro format := cmd.Root().String("format") explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, os.Stderr, "chats:messages:reactions add", obj, format, explicitFormat, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + Title: "chats:messages:reactions add", + Transform: transform, + }) } diff --git a/pkg/cmd/cmdutil.go b/pkg/cmd/cmdutil.go index 5163bc13..8054c002 100644 --- a/pkg/cmd/cmdutil.go +++ b/pkg/cmd/cmdutil.go @@ -356,36 +356,58 @@ func formatJSON(expectedOutput *os.File, title string, res gjson.Result, format const warningExploreNotSupported = "Warning: Output format 'explore' not supported for non-terminal output; falling back to 'json'\n" -// Display JSON to the user in various different formats. The explicitFormat parameter indicates -// whether the format was explicitly set by the user (via --format), which controls whether we -// silently fall back to json when explore is requested on non-terminal output. Warnings are -// written to stderr. -func ShowJSON(out *os.File, stderr io.Writer, title string, res gjson.Result, format string, explicitFormat bool, transform string) error { - if transform != "" { - transformed := res.Get(transform) +// 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) + 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() + + if opts.Transform != "" { + transformed := res.Get(opts.Transform) if transformed.Exists() { res = transformed } } - switch strings.ToLower(format) { + switch strings.ToLower(opts.Format) { case "auto": - return ShowJSON(out, stderr, title, res, "json", explicitFormat, "") + jsonOpts := opts + jsonOpts.Format = "json" + return ShowJSON(res, jsonOpts) case "explore": - if !isTerminal(out) { - if explicitFormat { - fmt.Fprint(stderr, warningExploreNotSupported) + if !isTerminal(opts.Stdout) { + if opts.ExplicitFormat { + fmt.Fprint(opts.Stderr, warningExploreNotSupported) } - return ShowJSON(out, stderr, title, res, "json", explicitFormat, transform) + jsonOpts := opts + jsonOpts.Format = "json" + return ShowJSON(res, jsonOpts) } - return jsonview.ExploreJSON(title, res) + return jsonview.ExploreJSON(opts.Title, res) default: - bytes, err := formatJSON(out, title, res, format, transform) + bytes, err := formatJSON(opts.Stdout, opts.Title, res, opts.Format, opts.Transform) if err != nil { return err } - _, err = out.Write(bytes) + _, err = opts.Stdout.Write(bytes) return err } } @@ -399,16 +421,17 @@ 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, stderr io.Writer, title string, iter jsonview.Iterator[T], format string, explicitFormat bool, transform string, itemsToDisplay int64) error { +// 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() + + format := opts.Format if format == "explore" { - if isTerminal(stdout) { - return jsonview.ExploreJSONStream(title, iter) + if isTerminal(opts.Stdout) { + return jsonview.ExploreJSONStream(opts.Title, iter) } - if explicitFormat { - fmt.Fprint(stderr, warningExploreNotSupported) + if opts.ExplicitFormat { + fmt.Fprint(opts.Stderr, warningExploreNotSupported) } format = "json" } @@ -436,7 +459,7 @@ func ShowJSONIterator[T any](stdout *os.File, stderr io.Writer, title string, it } obj = gjson.ParseBytes(jsonData) } - json, err := formatJSON(stdout, title, obj, format, transform) + json, err := formatJSON(opts.Stdout, opts.Title, obj, format, opts.Transform) if err != nil { return err } @@ -453,7 +476,7 @@ func ShowJSONIterator[T any](stdout *os.File, stderr io.Writer, title string, it } if !usePager { - _, err := stdout.Write(output) + _, err := opts.Stdout.Write(output) if err != nil { return err } @@ -461,13 +484,16 @@ func ShowJSONIterator[T any](stdout *os.File, stderr io.Writer, title string, it 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.Format = format + pagerOpts.Stdout = pager + for iter.Next() { if itemsToDisplay == 0 { break @@ -483,7 +509,7 @@ func ShowJSONIterator[T any](stdout *os.File, stderr io.Writer, title string, it } obj = gjson.ParseBytes(jsonData) } - if err := ShowJSON(pager, stderr, title, obj, format, explicitFormat, 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 aaf0b1b6..b318a0eb 100644 --- a/pkg/cmd/cmdutil_test.go +++ b/pkg/cmd/cmdutil_test.go @@ -244,7 +244,12 @@ func TestExploreFallback(t *testing.T) { var stderr bytes.Buffer res := gjson.Parse(`{"id":"abc"}`) - err = ShowJSON(w, &stderr, "test", res, "explore", false, "") + err = ShowJSON(res, ShowJSONOpts{ + Format: "explore", + Stderr: &stderr, + Stdout: w, + Title: "test", + }) w.Close() require.NoError(t, err) @@ -274,7 +279,13 @@ func TestExploreFallback(t *testing.T) { var stderr bytes.Buffer res := gjson.Parse(`{"id":"abc"}`) - err = ShowJSON(w, &stderr, "test", res, "explore", true, "") + err = ShowJSON(res, ShowJSONOpts{ + ExplicitFormat: true, + Format: "explore", + Stderr: &stderr, + Stdout: w, + Title: "test", + }) w.Close() require.NoError(t, err) @@ -290,7 +301,12 @@ func TestExploreFallback(t *testing.T) { var stderr bytes.Buffer res := gjson.Parse(`{"id":"abc"}`) - err = ShowJSON(w, &stderr, "test", res, "explore", false, "") + err = ShowJSON(res, ShowJSONOpts{ + Format: "explore", + Stderr: &stderr, + Stdout: w, + Title: "test", + }) w.Close() require.NoError(t, err) @@ -327,8 +343,13 @@ func captureShowJSONIterator[T any](t *testing.T, iter jsonview.Iterator[T], for require.NoError(t, err) defer r.Close() - var stderr bytes.Buffer - err = ShowJSONIterator(w, &stderr, "test", iter, format, false, 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/info.go b/pkg/cmd/info.go index 2272e2b0..bade3e1b 100644 --- a/pkg/cmd/info.go +++ b/pkg/cmd/info.go @@ -5,7 +5,6 @@ package cmd import ( "context" "fmt" - "os" "github.com/beeper/desktop-api-cli/internal/apiquery" "github.com/beeper/desktop-api-go" @@ -53,5 +52,10 @@ func handleInfoRetrieve(ctx context.Context, cmd *cli.Command) error { format := cmd.Root().String("format") explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, os.Stderr, "info retrieve", obj, format, explicitFormat, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + Title: "info retrieve", + Transform: transform, + }) } diff --git a/pkg/cmd/message.go b/pkg/cmd/message.go index 0e0a0974..79d78272 100644 --- a/pkg/cmd/message.go +++ b/pkg/cmd/message.go @@ -5,7 +5,6 @@ package cmd import ( "context" "fmt" - "os" "github.com/beeper/desktop-api-cli/internal/apiquery" "github.com/beeper/desktop-api-cli/internal/requestflag" @@ -256,7 +255,12 @@ func handleMessagesUpdate(ctx context.Context, cmd *cli.Command) error { format := cmd.Root().String("format") explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, os.Stderr, "messages update", obj, format, explicitFormat, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + Title: "messages update", + Transform: transform, + }) } func handleMessagesList(ctx context.Context, cmd *cli.Command) error { @@ -299,7 +303,12 @@ func handleMessagesList(ctx context.Context, cmd *cli.Command) error { return err } obj := gjson.ParseBytes(res) - return ShowJSON(os.Stdout, os.Stderr, "messages list", obj, format, explicitFormat, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + Title: "messages list", + Transform: transform, + }) } else { iter := client.Messages.ListAutoPaging( ctx, @@ -311,7 +320,12 @@ func handleMessagesList(ctx context.Context, cmd *cli.Command) error { if cmd.IsSet("max-items") { maxItems = cmd.Value("max-items").(int64) } - return ShowJSONIterator(os.Stdout, os.Stderr, "messages list", iter, format, explicitFormat, transform, maxItems) + return ShowJSONIterator(iter, maxItems, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + Title: "messages list", + Transform: transform, + }) } } @@ -347,14 +361,24 @@ func handleMessagesSearch(ctx context.Context, cmd *cli.Command) error { return err } obj := gjson.ParseBytes(res) - return ShowJSON(os.Stdout, os.Stderr, "messages search", obj, format, explicitFormat, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + Title: "messages search", + Transform: transform, + }) } else { iter := client.Messages.SearchAutoPaging(ctx, params, options...) maxItems := int64(-1) if cmd.IsSet("max-items") { maxItems = cmd.Value("max-items").(int64) } - return ShowJSONIterator(os.Stdout, os.Stderr, "messages search", iter, format, explicitFormat, transform, maxItems) + return ShowJSONIterator(iter, maxItems, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + Title: "messages search", + Transform: transform, + }) } } @@ -398,5 +422,10 @@ func handleMessagesSend(ctx context.Context, cmd *cli.Command) error { format := cmd.Root().String("format") explicitFormat := cmd.Root().IsSet("format") transform := cmd.Root().String("transform") - return ShowJSON(os.Stdout, os.Stderr, "messages send", obj, format, explicitFormat, transform) + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + Title: "messages send", + Transform: transform, + }) } From 5a018e83f5f9347b262b10176639fcc24ec5f370 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 17 Apr 2026 02:29:22 +0000 Subject: [PATCH 30/54] chore(ci): support manually triggering release workflow --- .github/workflows/publish-release.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index 4e556fb7..b2076972 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -10,6 +10,7 @@ on: push: tags: - "v*" + workflow_dispatch: {} jobs: goreleaser: runs-on: ubuntu-latest From b1ab49bb30c6d4132fb9827cff19f73d83e7af5c Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 17 Apr 2026 02:30:44 +0000 Subject: [PATCH 31/54] feat(cli): send filename and content type when reading input from files --- pkg/cmd/flagoptions.go | 65 +++++++++++++++++++++++++++++++++++-- pkg/cmd/flagoptions_test.go | 59 ++++++++++++++++++++++++++++++--- 2 files changed, 117 insertions(+), 7 deletions(-) diff --git a/pkg/cmd/flagoptions.go b/pkg/cmd/flagoptions.go index 4cc19658..aa566b3a 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 @@ -492,6 +515,44 @@ 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 diff --git a/pkg/cmd/flagoptions_test.go b/pkg/cmd/flagoptions_test.go index 039b9ff9..00734cae 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() From 963cb2275ff0b6900c09dbbd0cc45c37e745d561 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 17 Apr 2026 02:32:45 +0000 Subject: [PATCH 32/54] feat(cli): add `--raw-output`/`-r` option to print raw (non-JSON) strings --- pkg/cmd/account.go | 1 + pkg/cmd/accountcontact.go | 3 +++ pkg/cmd/asset.go | 3 +++ pkg/cmd/beeperdesktopapi.go | 2 ++ pkg/cmd/chat.go | 6 ++++++ pkg/cmd/chatmessagereaction.go | 2 ++ pkg/cmd/cmd.go | 5 +++++ pkg/cmd/cmdutil.go | 14 +++++++++---- pkg/cmd/cmdutil_test.go | 37 ++++++++++++++++++++++++++++++---- pkg/cmd/info.go | 1 + pkg/cmd/message.go | 6 ++++++ 11 files changed, 72 insertions(+), 8 deletions(-) diff --git a/pkg/cmd/account.go b/pkg/cmd/account.go index c2809b0b..c47af67a 100644 --- a/pkg/cmd/account.go +++ b/pkg/cmd/account.go @@ -55,6 +55,7 @@ func handleAccountsList(ctx context.Context, cmd *cli.Command) error { return ShowJSON(obj, ShowJSONOpts{ ExplicitFormat: explicitFormat, Format: format, + RawOutput: cmd.Root().Bool("raw-output"), Title: "accounts list", Transform: transform, }) diff --git a/pkg/cmd/accountcontact.go b/pkg/cmd/accountcontact.go index c8ec6e35..82bc9516 100644 --- a/pkg/cmd/accountcontact.go +++ b/pkg/cmd/accountcontact.go @@ -118,6 +118,7 @@ func handleAccountsContactsList(ctx context.Context, cmd *cli.Command) error { return ShowJSON(obj, ShowJSONOpts{ ExplicitFormat: explicitFormat, Format: format, + RawOutput: cmd.Root().Bool("raw-output"), Title: "accounts:contacts list", Transform: transform, }) @@ -135,6 +136,7 @@ func handleAccountsContactsList(ctx context.Context, cmd *cli.Command) error { return ShowJSONIterator(iter, maxItems, ShowJSONOpts{ ExplicitFormat: explicitFormat, Format: format, + RawOutput: cmd.Root().Bool("raw-output"), Title: "accounts:contacts list", Transform: transform, }) @@ -184,6 +186,7 @@ func handleAccountsContactsSearch(ctx context.Context, cmd *cli.Command) error { return ShowJSON(obj, ShowJSONOpts{ ExplicitFormat: explicitFormat, Format: format, + RawOutput: cmd.Root().Bool("raw-output"), Title: "accounts:contacts search", Transform: transform, }) diff --git a/pkg/cmd/asset.go b/pkg/cmd/asset.go index 227a235b..2e564895 100644 --- a/pkg/cmd/asset.go +++ b/pkg/cmd/asset.go @@ -134,6 +134,7 @@ func handleAssetsDownload(ctx context.Context, cmd *cli.Command) error { return ShowJSON(obj, ShowJSONOpts{ ExplicitFormat: explicitFormat, Format: format, + RawOutput: cmd.Root().Bool("raw-output"), Title: "assets download", Transform: transform, }) @@ -198,6 +199,7 @@ func handleAssetsUpload(ctx context.Context, cmd *cli.Command) error { return ShowJSON(obj, ShowJSONOpts{ ExplicitFormat: explicitFormat, Format: format, + RawOutput: cmd.Root().Bool("raw-output"), Title: "assets upload", Transform: transform, }) @@ -238,6 +240,7 @@ func handleAssetsUploadBase64(ctx context.Context, cmd *cli.Command) error { return ShowJSON(obj, ShowJSONOpts{ ExplicitFormat: explicitFormat, Format: format, + RawOutput: cmd.Root().Bool("raw-output"), Title: "assets upload-base64", Transform: transform, }) diff --git a/pkg/cmd/beeperdesktopapi.go b/pkg/cmd/beeperdesktopapi.go index a948c5a8..0d2da452 100644 --- a/pkg/cmd/beeperdesktopapi.go +++ b/pkg/cmd/beeperdesktopapi.go @@ -95,6 +95,7 @@ func handleFocus(ctx context.Context, cmd *cli.Command) error { return ShowJSON(obj, ShowJSONOpts{ ExplicitFormat: explicitFormat, Format: format, + RawOutput: cmd.Root().Bool("raw-output"), Title: "focus", Transform: transform, }) @@ -135,6 +136,7 @@ func handleSearch(ctx context.Context, cmd *cli.Command) error { return ShowJSON(obj, ShowJSONOpts{ ExplicitFormat: explicitFormat, Format: format, + RawOutput: cmd.Root().Bool("raw-output"), Title: "search", Transform: transform, }) diff --git a/pkg/cmd/chat.go b/pkg/cmd/chat.go index 60cb728c..cbb512a8 100644 --- a/pkg/cmd/chat.go +++ b/pkg/cmd/chat.go @@ -212,6 +212,7 @@ func handleChatsCreate(ctx context.Context, cmd *cli.Command) error { return ShowJSON(obj, ShowJSONOpts{ ExplicitFormat: explicitFormat, Format: format, + RawOutput: cmd.Root().Bool("raw-output"), Title: "chats create", Transform: transform, }) @@ -260,6 +261,7 @@ func handleChatsRetrieve(ctx context.Context, cmd *cli.Command) error { return ShowJSON(obj, ShowJSONOpts{ ExplicitFormat: explicitFormat, Format: format, + RawOutput: cmd.Root().Bool("raw-output"), Title: "chats retrieve", Transform: transform, }) @@ -300,6 +302,7 @@ func handleChatsList(ctx context.Context, cmd *cli.Command) error { return ShowJSON(obj, ShowJSONOpts{ ExplicitFormat: explicitFormat, Format: format, + RawOutput: cmd.Root().Bool("raw-output"), Title: "chats list", Transform: transform, }) @@ -312,6 +315,7 @@ func handleChatsList(ctx context.Context, cmd *cli.Command) error { return ShowJSONIterator(iter, maxItems, ShowJSONOpts{ ExplicitFormat: explicitFormat, Format: format, + RawOutput: cmd.Root().Bool("raw-output"), Title: "chats list", Transform: transform, }) @@ -385,6 +389,7 @@ func handleChatsSearch(ctx context.Context, cmd *cli.Command) error { return ShowJSON(obj, ShowJSONOpts{ ExplicitFormat: explicitFormat, Format: format, + RawOutput: cmd.Root().Bool("raw-output"), Title: "chats search", Transform: transform, }) @@ -397,6 +402,7 @@ func handleChatsSearch(ctx context.Context, cmd *cli.Command) error { return ShowJSONIterator(iter, maxItems, ShowJSONOpts{ ExplicitFormat: explicitFormat, Format: format, + RawOutput: cmd.Root().Bool("raw-output"), Title: "chats search", Transform: transform, }) diff --git a/pkg/cmd/chatmessagereaction.go b/pkg/cmd/chatmessagereaction.go index da64dc4a..ed245df3 100644 --- a/pkg/cmd/chatmessagereaction.go +++ b/pkg/cmd/chatmessagereaction.go @@ -114,6 +114,7 @@ func handleChatsMessagesReactionsDelete(ctx context.Context, cmd *cli.Command) e return ShowJSON(obj, ShowJSONOpts{ ExplicitFormat: explicitFormat, Format: format, + RawOutput: cmd.Root().Bool("raw-output"), Title: "chats:messages:reactions delete", Transform: transform, }) @@ -164,6 +165,7 @@ func handleChatsMessagesReactionsAdd(ctx context.Context, cmd *cli.Command) erro return ShowJSON(obj, ShowJSONOpts{ ExplicitFormat: explicitFormat, Format: format, + RawOutput: cmd.Root().Bool("raw-output"), Title: "chats:messages:reactions add", Transform: transform, }) diff --git a/pkg/cmd/cmd.go b/pkg/cmd/cmd.go index 4c91ac7a..66f0c298 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: "access-token", Usage: "Bearer access token obtained via OAuth2 PKCE flow or created in-app. Required for all API operations.", diff --git a/pkg/cmd/cmdutil.go b/pkg/cmd/cmdutil.go index 8054c002..6dd1aae4 100644 --- a/pkg/cmd/cmdutil.go +++ b/pkg/cmd/cmdutil.go @@ -311,16 +311,21 @@ 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) { +func formatJSON(expectedOutput *os.File, title string, res gjson.Result, format string, transform string, rawOutput bool) ([]byte, error) { if transform != "" { transformed := res.Get(transform) if transformed.Exists() { res = transformed } } + // 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 rawOutput && res.Type == gjson.String { + return []byte(res.Str + "\n"), nil + } switch strings.ToLower(format) { case "auto": - return formatJSON(expectedOutput, title, res, "json", "") + return formatJSON(expectedOutput, title, res, "json", "", rawOutput) case "pretty": return []byte(jsonview.RenderJSON(title, res) + "\n"), nil case "json": @@ -360,6 +365,7 @@ const warningExploreNotSupported = "Warning: Output format 'explore' not support 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 @@ -402,7 +408,7 @@ func ShowJSON(res gjson.Result, opts ShowJSONOpts) error { } return jsonview.ExploreJSON(opts.Title, res) default: - bytes, err := formatJSON(opts.Stdout, opts.Title, res, opts.Format, opts.Transform) + bytes, err := formatJSON(opts.Stdout, opts.Title, res, opts.Format, opts.Transform, opts.RawOutput) if err != nil { return err } @@ -459,7 +465,7 @@ func ShowJSONIterator[T any](iter jsonview.Iterator[T], itemsToDisplay int64, op } obj = gjson.ParseBytes(jsonData) } - json, err := formatJSON(opts.Stdout, opts.Title, obj, format, opts.Transform) + json, err := formatJSON(opts.Stdout, opts.Title, obj, format, opts.Transform, opts.RawOutput) if err != nil { return err } diff --git a/pkg/cmd/cmdutil_test.go b/pkg/cmd/cmdutil_test.go index b318a0eb..f8e10ef6 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(os.Stdout, "test", res, "raw", "id", false) 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(os.Stdout, "test", res, "raw", "", false) 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(os.Stdout, "test", res, "raw", "data.items", false) 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(os.Stdout, "test", res, "raw", "missing", false) 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(os.Stdout, "test", res, "json", "id", 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(os.Stdout, "test", res, "raw", "count", 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(os.Stdout, "test", res, "raw", "nested", true) + require.NoError(t, err) + require.Equal(t, `{"a":1}`+"\n", string(formatted)) + }) } func TestShowJSONIterator(t *testing.T) { diff --git a/pkg/cmd/info.go b/pkg/cmd/info.go index bade3e1b..bb9301ea 100644 --- a/pkg/cmd/info.go +++ b/pkg/cmd/info.go @@ -55,6 +55,7 @@ func handleInfoRetrieve(ctx context.Context, cmd *cli.Command) error { return ShowJSON(obj, ShowJSONOpts{ ExplicitFormat: explicitFormat, Format: format, + RawOutput: cmd.Root().Bool("raw-output"), Title: "info retrieve", Transform: transform, }) diff --git a/pkg/cmd/message.go b/pkg/cmd/message.go index 79d78272..873cbfc6 100644 --- a/pkg/cmd/message.go +++ b/pkg/cmd/message.go @@ -258,6 +258,7 @@ func handleMessagesUpdate(ctx context.Context, cmd *cli.Command) error { return ShowJSON(obj, ShowJSONOpts{ ExplicitFormat: explicitFormat, Format: format, + RawOutput: cmd.Root().Bool("raw-output"), Title: "messages update", Transform: transform, }) @@ -306,6 +307,7 @@ func handleMessagesList(ctx context.Context, cmd *cli.Command) error { return ShowJSON(obj, ShowJSONOpts{ ExplicitFormat: explicitFormat, Format: format, + RawOutput: cmd.Root().Bool("raw-output"), Title: "messages list", Transform: transform, }) @@ -323,6 +325,7 @@ func handleMessagesList(ctx context.Context, cmd *cli.Command) error { return ShowJSONIterator(iter, maxItems, ShowJSONOpts{ ExplicitFormat: explicitFormat, Format: format, + RawOutput: cmd.Root().Bool("raw-output"), Title: "messages list", Transform: transform, }) @@ -364,6 +367,7 @@ func handleMessagesSearch(ctx context.Context, cmd *cli.Command) error { return ShowJSON(obj, ShowJSONOpts{ ExplicitFormat: explicitFormat, Format: format, + RawOutput: cmd.Root().Bool("raw-output"), Title: "messages search", Transform: transform, }) @@ -376,6 +380,7 @@ func handleMessagesSearch(ctx context.Context, cmd *cli.Command) error { return ShowJSONIterator(iter, maxItems, ShowJSONOpts{ ExplicitFormat: explicitFormat, Format: format, + RawOutput: cmd.Root().Bool("raw-output"), Title: "messages search", Transform: transform, }) @@ -425,6 +430,7 @@ func handleMessagesSend(ctx context.Context, cmd *cli.Command) error { return ShowJSON(obj, ShowJSONOpts{ ExplicitFormat: explicitFormat, Format: format, + RawOutput: cmd.Root().Bool("raw-output"), Title: "messages send", Transform: transform, }) From 384a7f434bd6d40b626b6e635f005853f66ff917 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 18 Apr 2026 02:31:35 +0000 Subject: [PATCH 33/54] chore(cli): use `ShowJSONOpts` as argument to `formatJSON` instead of many positionals --- pkg/cmd/cmdutil.go | 54 ++++++++++++++++++++--------------------- pkg/cmd/cmdutil_test.go | 14 +++++------ 2 files changed, 34 insertions(+), 34 deletions(-) diff --git a/pkg/cmd/cmdutil.go b/pkg/cmd/cmdutil.go index 6dd1aae4..c98055ae 100644 --- a/pkg/cmd/cmdutil.go +++ b/pkg/cmd/cmdutil.go @@ -311,26 +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, rawOutput bool) ([]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 } } // 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 rawOutput && res.Type == gjson.String { + if opts.RawOutput && res.Type == gjson.String { return []byte(res.Str + "\n"), nil } - switch strings.ToLower(format) { + switch strings.ToLower(opts.Format) { case "auto": - return formatJSON(expectedOutput, title, res, "json", "", rawOutput) + 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 @@ -338,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 { @@ -352,10 +355,10 @@ 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, ", ")) } } @@ -385,18 +388,11 @@ func (o *ShowJSONOpts) setDefaults() { func ShowJSON(res gjson.Result, opts ShowJSONOpts) error { opts.setDefaults() - if opts.Transform != "" { - transformed := res.Get(opts.Transform) - if transformed.Exists() { - res = transformed - } - } - switch strings.ToLower(opts.Format) { case "auto": - jsonOpts := opts - jsonOpts.Format = "json" - return ShowJSON(res, jsonOpts) + autoOpts := opts + autoOpts.Format = "json" + return ShowJSON(res, autoOpts) case "explore": if !isTerminal(opts.Stdout) { if opts.ExplicitFormat { @@ -406,9 +402,15 @@ func ShowJSON(res gjson.Result, opts ShowJSONOpts) error { 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(opts.Stdout, opts.Title, res, opts.Format, opts.Transform, opts.RawOutput) + bytes, err := formatJSON(res, opts) if err != nil { return err } @@ -431,15 +433,14 @@ type hasRawJSON interface { func ShowJSONIterator[T any](iter jsonview.Iterator[T], itemsToDisplay int64, opts ShowJSONOpts) error { opts.setDefaults() - format := opts.Format - if format == "explore" { + if opts.Format == "explore" { if isTerminal(opts.Stdout) { return jsonview.ExploreJSONStream(opts.Title, iter) } if opts.ExplicitFormat { fmt.Fprint(opts.Stderr, warningExploreNotSupported) } - format = "json" + opts.Format = "json" } terminalWidth, terminalHeight, err := term.GetSize(os.Stdout.Fd()) @@ -465,7 +466,7 @@ func ShowJSONIterator[T any](iter jsonview.Iterator[T], itemsToDisplay int64, op } obj = gjson.ParseBytes(jsonData) } - json, err := formatJSON(opts.Stdout, opts.Title, obj, format, opts.Transform, opts.RawOutput) + json, err := formatJSON(obj, opts) if err != nil { return err } @@ -497,7 +498,6 @@ func ShowJSONIterator[T any](iter jsonview.Iterator[T], itemsToDisplay int64, op } pagerOpts := opts - pagerOpts.Format = format pagerOpts.Stdout = pager for iter.Next() { diff --git a/pkg/cmd/cmdutil_test.go b/pkg/cmd/cmdutil_test.go index f8e10ef6..2c500d70 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", false) + 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", "", false) + 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", false) + 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,7 +186,7 @@ func TestFormatJSON(t *testing.T) { t.Parallel() res := gjson.Parse(`{"id":"abc123"}`) - formatted, err := formatJSON(os.Stdout, "test", res, "raw", "missing", false) + 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)) @@ -196,7 +196,7 @@ func TestFormatJSON(t *testing.T) { t.Parallel() res := gjson.Parse(`{"id":"abc123","name":"test"}`) - formatted, err := formatJSON(os.Stdout, "test", res, "json", "id", true) + 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)) }) @@ -206,7 +206,7 @@ func TestFormatJSON(t *testing.T) { // --raw-output has no effect on non-string values res := gjson.Parse(`{"count":42}`) - formatted, err := formatJSON(os.Stdout, "test", res, "raw", "count", true) + 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)) }) @@ -216,7 +216,7 @@ func TestFormatJSON(t *testing.T) { // --raw-output has no effect on objects res := gjson.Parse(`{"nested":{"a":1}}`) - formatted, err := formatJSON(os.Stdout, "test", res, "raw", "nested", true) + 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)) }) From 216464542c88aca1c22ebf0e9af3ac0eb6854a38 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 18 Apr 2026 02:33:09 +0000 Subject: [PATCH 34/54] chore(tests): bump steady to v0.22.1 --- scripts/mock | 6 +++--- scripts/test | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/mock b/scripts/mock index 7c58865f..9c7c4399 100755 --- a/scripts/mock +++ b/scripts/mock @@ -22,9 +22,9 @@ echo "==> Starting mock server with URL ${URL}" # Run steady mock on the given spec if [ "$1" == "--daemon" ]; then # Pre-install the package so the download doesn't eat into the startup timeout - npm exec --package=@stdy/cli@0.20.2 -- steady --version + npm exec --package=@stdy/cli@0.22.1 -- steady --version - npm exec --package=@stdy/cli@0.20.2 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=repeat --validator-form-array-format=repeat --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL" &> .stdy.log & + npm exec --package=@stdy/cli@0.22.1 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=repeat --validator-form-array-format=repeat --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL" &> .stdy.log & # Wait for server to come online via health endpoint (max 30s) echo -n "Waiting for server" @@ -48,5 +48,5 @@ if [ "$1" == "--daemon" ]; then echo else - npm exec --package=@stdy/cli@0.20.2 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=repeat --validator-form-array-format=repeat --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL" + npm exec --package=@stdy/cli@0.22.1 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=repeat --validator-form-array-format=repeat --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL" fi diff --git a/scripts/test b/scripts/test index 7ba4b3fc..e2baecad 100755 --- a/scripts/test +++ b/scripts/test @@ -46,7 +46,7 @@ elif ! steady_is_running ; then echo -e "To run the server, pass in the path or url of your OpenAPI" echo -e "spec to the steady command:" echo - echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.20.2 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-query-array-format=repeat --validator-form-array-format=repeat --validator-query-object-format=brackets --validator-form-object-format=brackets${NC}" + echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.22.1 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-query-array-format=repeat --validator-form-array-format=repeat --validator-query-object-format=brackets --validator-form-object-format=brackets${NC}" echo exit 1 From 888af53697b79c93159f9fce5e60cca4ea1e5a04 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 23 Apr 2026 02:11:53 +0000 Subject: [PATCH 35/54] chore(internal): more robust bootstrap script --- scripts/bootstrap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/bootstrap b/scripts/bootstrap index 9ebb7d3b..bbc786d1 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 From 9cc3bc3470b42af42366690da4a7264542c4fd78 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 29 Apr 2026 19:50:53 +0000 Subject: [PATCH 36/54] Update Desktop API Stainless config and OpenAPI spec --- .stats.yml | 4 +- cmd/beeper-desktop-cli/main.go | 4 +- pkg/cmd/account.go | 5 +- pkg/cmd/accountcontact.go | 10 +++- pkg/cmd/asset.go | 16 +++++- pkg/cmd/asset_test.go | 1 + pkg/cmd/beeperdesktopapi.go | 5 +- pkg/cmd/chat.go | 93 ++++++++++++++++++++++++++++++---- pkg/cmd/chat_test.go | 44 ++++++++++++++-- pkg/cmd/chatmessagereaction.go | 2 +- pkg/cmd/cmd.go | 4 +- pkg/cmd/info.go | 5 +- pkg/cmd/message.go | 12 +++-- 13 files changed, 176 insertions(+), 29 deletions(-) diff --git a/.stats.yml b/.stats.yml index 229f6b5f..e925f68f 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 23 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-611aa7641fbca8cf31d626bf86f9efd3c2b92778e897ebbb25c6ea44185ed1ed.yml -openapi_spec_hash: d6c0a1776048dab04f6c5625c9893c9c -config_hash: 39ed0717b5f415499aaace2468346e1a +openapi_spec_hash: 4840f003552e8b48eb8e689b59a819ef +config_hash: 05ebdec072113f63395372504da98192 diff --git a/cmd/beeper-desktop-cli/main.go b/cmd/beeper-desktop-cli/main.go index c98a9264..8640e9d6 100644 --- a/cmd/beeper-desktop-cli/main.go +++ b/cmd/beeper-desktop-cli/main.go @@ -23,8 +23,8 @@ func main() { prepareForAutocomplete(app) } - if baseURL, ok := os.LookupEnv("BEEPER_DESKTOP_BASE_URL"); ok { - if err := cmd.ValidateBaseURL(baseURL, "BEEPER_DESKTOP_BASE_URL"); err != nil { + if baseURL, ok := os.LookupEnv("BEEPER_BASE_URL"); ok { + if err := cmd.ValidateBaseURL(baseURL, "BEEPER_BASE_URL"); err != nil { fmt.Fprintf(os.Stderr, "%s\n", err.Error()) os.Exit(1) } diff --git a/pkg/cmd/account.go b/pkg/cmd/account.go index c47af67a..9cf40bb3 100644 --- a/pkg/cmd/account.go +++ b/pkg/cmd/account.go @@ -49,8 +49,11 @@ func handleAccountsList(ctx context.Context, cmd *cli.Command) error { } obj := gjson.ParseBytes(res) - format := cmd.Root().String("format") + format := "json" explicitFormat := cmd.Root().IsSet("format") + if explicitFormat { + format = cmd.Root().String("format") + } transform := cmd.Root().String("transform") return ShowJSON(obj, ShowJSONOpts{ ExplicitFormat: explicitFormat, diff --git a/pkg/cmd/accountcontact.go b/pkg/cmd/accountcontact.go index 82bc9516..cd491b07 100644 --- a/pkg/cmd/accountcontact.go +++ b/pkg/cmd/accountcontact.go @@ -99,8 +99,11 @@ func handleAccountsContactsList(ctx context.Context, cmd *cli.Command) error { return err } - format := cmd.Root().String("format") + format := "json" explicitFormat := cmd.Root().IsSet("format") + if explicitFormat { + format = cmd.Root().String("format") + } transform := cmd.Root().String("transform") if format == "raw" { var res []byte @@ -180,8 +183,11 @@ func handleAccountsContactsSearch(ctx context.Context, cmd *cli.Command) error { } obj := gjson.ParseBytes(res) - format := cmd.Root().String("format") + format := "json" explicitFormat := cmd.Root().IsSet("format") + if explicitFormat { + format = cmd.Root().String("format") + } transform := cmd.Root().String("transform") return ShowJSON(obj, ShowJSONOpts{ ExplicitFormat: explicitFormat, diff --git a/pkg/cmd/asset.go b/pkg/cmd/asset.go index 2e564895..6e620062 100644 --- a/pkg/cmd/asset.go +++ b/pkg/cmd/asset.go @@ -5,6 +5,7 @@ package cmd import ( "context" "fmt" + "os" "github.com/beeper/desktop-api-cli/internal/apiquery" "github.com/beeper/desktop-api-cli/internal/requestflag" @@ -41,6 +42,11 @@ var assetsServe = cli.Command{ Required: true, QueryPath: "url", }, + &requestflag.Flag[string]{ + Name: "output", + Aliases: []string{"o"}, + Usage: "The file where the response contents will be stored. Use the value '-' to force output to stdout.", + }, }, Action: handleAssetsServe, HideHelpCommand: true, @@ -161,7 +167,15 @@ func handleAssetsServe(ctx context.Context, cmd *cli.Command) error { return err } - return client.Assets.Serve(ctx, params, options...) + response, err := client.Assets.Serve(ctx, params, options...) + if err != nil { + return err + } + message, err := writeBinaryResponse(response, os.Stdout, cmd.String("output")) + if message != "" { + fmt.Println(message) + } + return err } func handleAssetsUpload(ctx context.Context, cmd *cli.Command) error { diff --git a/pkg/cmd/asset_test.go b/pkg/cmd/asset_test.go index 696660a8..4375d2e7 100644 --- a/pkg/cmd/asset_test.go +++ b/pkg/cmd/asset_test.go @@ -37,6 +37,7 @@ func TestAssetsServe(t *testing.T) { "--access-token", "string", "assets", "serve", "--url", "x", + "--output", "/dev/null", ) }) } diff --git a/pkg/cmd/beeperdesktopapi.go b/pkg/cmd/beeperdesktopapi.go index 0d2da452..5bc483ea 100644 --- a/pkg/cmd/beeperdesktopapi.go +++ b/pkg/cmd/beeperdesktopapi.go @@ -130,8 +130,11 @@ func handleSearch(ctx context.Context, cmd *cli.Command) error { } obj := gjson.ParseBytes(res) - format := cmd.Root().String("format") + format := "json" explicitFormat := cmd.Root().IsSet("format") + if explicitFormat { + format = cmd.Root().String("format") + } transform := cmd.Root().String("transform") return ShowJSON(obj, ShowJSONOpts{ ExplicitFormat: explicitFormat, diff --git a/pkg/cmd/chat.go b/pkg/cmd/chat.go index cbb512a8..166458eb 100644 --- a/pkg/cmd/chat.go +++ b/pkg/cmd/chat.go @@ -14,19 +14,85 @@ import ( "github.com/urfave/cli/v3" ) -var chatsCreate = cli.Command{ +var chatsCreate = requestflag.WithInnerFlags(cli.Command{ Name: "create", - Usage: "Create a single/group chat (mode='create') or start a direct chat from merged\nuser data (mode='start').", + Usage: "Create a direct or group chat with mode=\"create\", or use mode=\"start\" to resolve\na contact and open a direct chat.", Suggest: true, Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "account-id", + Usage: "Account to create or start the chat on.", + Required: true, + BodyPath: "accountID", + }, + &requestflag.Flag[bool]{ + Name: "allow-invite", + Usage: "Only used for mode='start'. Whether invite-based DM creation is allowed when required by the platform.", + Default: true, + BodyPath: "allowInvite", + }, + &requestflag.Flag[string]{ + Name: "message-text", + Usage: "Optional first message content if the platform requires it to create the chat.", + BodyPath: "messageText", + }, + &requestflag.Flag[string]{ + Name: "mode", + Usage: "Operation mode. Use 'start' to resolve a user/contact and start a direct chat. Omit or set 'create' to create a chat directly.", + BodyPath: "mode", + }, + &requestflag.Flag[[]string]{ + Name: "participant-id", + Usage: "Required for create mode. Provide exactly one user ID for 'single' chats and one or more for 'group' chats.", + BodyPath: "participantIDs", + }, + &requestflag.Flag[string]{ + Name: "title", + Usage: "Optional title for group chats; ignored for single chats on most networks.", + BodyPath: "title", + }, + &requestflag.Flag[string]{ + Name: "type", + Usage: "Required for create mode. 'single' creates a direct message chat; 'group' creates a group chat.", + BodyPath: "type", + }, &requestflag.Flag[map[string]any]{ - Name: "params", - BodyRoot: true, + Name: "user", + Usage: "Required for mode='start'. Merged user-like contact payload used to resolve the best identifier.", + BodyPath: "user", }, }, Action: handleChatsCreate, HideHelpCommand: true, -} +}, map[string][]requestflag.HasOuterFlag{ + "user": { + &requestflag.InnerFlag[string]{ + Name: "user.id", + Usage: "Known user ID when available.", + InnerField: "id", + }, + &requestflag.InnerFlag[string]{ + Name: "user.email", + Usage: "Email candidate.", + InnerField: "email", + }, + &requestflag.InnerFlag[string]{ + Name: "user.full-name", + Usage: "Display name hint used for ranking only.", + InnerField: "fullName", + }, + &requestflag.InnerFlag[string]{ + Name: "user.phone-number", + Usage: "Phone number candidate (E.164 preferred).", + InnerField: "phoneNumber", + }, + &requestflag.InnerFlag[string]{ + Name: "user.username", + Usage: "Username/handle candidate.", + InnerField: "username", + }, + }, +}) var chatsRetrieve = cli.Command{ Name: "retrieve", @@ -101,7 +167,7 @@ var chatsArchive = cli.Command{ var chatsSearch = cli.Command{ Name: "search", - Usage: "Search chats by title/network or participants using Beeper Desktop's renderer\nalgorithm.", + Usage: "Search chats by title, network, or participant names.", Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[[]string]{ @@ -255,8 +321,11 @@ func handleChatsRetrieve(ctx context.Context, cmd *cli.Command) error { } obj := gjson.ParseBytes(res) - format := cmd.Root().String("format") + format := "json" explicitFormat := cmd.Root().IsSet("format") + if explicitFormat { + format = cmd.Root().String("format") + } transform := cmd.Root().String("transform") return ShowJSON(obj, ShowJSONOpts{ ExplicitFormat: explicitFormat, @@ -288,8 +357,11 @@ func handleChatsList(ctx context.Context, cmd *cli.Command) error { return err } - format := cmd.Root().String("format") + format := "json" explicitFormat := cmd.Root().IsSet("format") + if explicitFormat { + format = cmd.Root().String("format") + } transform := cmd.Root().String("transform") if format == "raw" { var res []byte @@ -375,8 +447,11 @@ func handleChatsSearch(ctx context.Context, cmd *cli.Command) error { return err } - format := cmd.Root().String("format") + format := "json" explicitFormat := cmd.Root().IsSet("format") + if explicitFormat { + format = cmd.Root().String("format") + } transform := cmd.Root().String("transform") if format == "raw" { var res []byte diff --git a/pkg/cmd/chat_test.go b/pkg/cmd/chat_test.go index ea462e1d..d7966cd4 100644 --- a/pkg/cmd/chat_test.go +++ b/pkg/cmd/chat_test.go @@ -6,6 +6,7 @@ import ( "testing" "github.com/beeper/desktop-api-cli/internal/mocktest" + "github.com/beeper/desktop-api-cli/internal/requestflag" ) func TestChatsCreate(t *testing.T) { @@ -14,7 +15,38 @@ func TestChatsCreate(t *testing.T) { t, "--access-token", "string", "chats", "create", - "--params", "{accountID: accountID, mode: start, user: {id: id, email: email, fullName: fullName, phoneNumber: phoneNumber, username: username}, allowInvite: true, messageText: messageText}", + "--account-id", "accountID", + "--allow-invite=true", + "--message-text", "messageText", + "--mode", "start", + "--participant-id", "string", + "--title", "title", + "--type", "single", + "--user", "{id: id, email: email, fullName: fullName, phoneNumber: phoneNumber, username: username}", + ) + }) + + t.Run("inner flags", func(t *testing.T) { + // Check that inner flags have been set up correctly + requestflag.CheckInnerFlags(chatsCreate) + + // Alternative argument passing style using inner flags + mocktest.TestRunMockTestWithFlags( + t, + "--access-token", "string", + "chats", "create", + "--account-id", "accountID", + "--allow-invite=true", + "--message-text", "messageText", + "--mode", "start", + "--participant-id", "string", + "--title", "title", + "--type", "single", + "--user.id", "id", + "--user.email", "email", + "--user.full-name", "fullName", + "--user.phone-number", "phoneNumber", + "--user.username", "username", ) }) @@ -22,15 +54,19 @@ func TestChatsCreate(t *testing.T) { // Test piping YAML data over stdin pipeData := []byte("" + "accountID: accountID\n" + + "allowInvite: true\n" + + "messageText: messageText\n" + "mode: start\n" + + "participantIDs:\n" + + " - string\n" + + "title: title\n" + + "type: single\n" + "user:\n" + " id: id\n" + " email: email\n" + " fullName: fullName\n" + " phoneNumber: phoneNumber\n" + - " username: username\n" + - "allowInvite: true\n" + - "messageText: messageText\n") + " username: username\n") mocktest.TestRunMockTestWithPipeAndFlags( t, pipeData, "--access-token", "string", diff --git a/pkg/cmd/chatmessagereaction.go b/pkg/cmd/chatmessagereaction.go index ed245df3..35cacf3f 100644 --- a/pkg/cmd/chatmessagereaction.go +++ b/pkg/cmd/chatmessagereaction.go @@ -16,7 +16,7 @@ import ( var chatsMessagesReactionsDelete = cli.Command{ Name: "delete", - Usage: "Remove the authenticated user's reaction from an existing message.", + Usage: "Remove the reaction added by the authenticated user from an existing message.", Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ diff --git a/pkg/cmd/cmd.go b/pkg/cmd/cmd.go index 66f0c298..81a042c1 100644 --- a/pkg/cmd/cmd.go +++ b/pkg/cmd/cmd.go @@ -46,7 +46,7 @@ func init() { &cli.StringFlag{ Name: "format", Usage: "The format for displaying response data (one of: " + strings.Join(OutputFormats, ", ") + ")", - Value: "pretty", + Value: "json", Validator: func(format string) error { if !slices.Contains(OutputFormats, strings.ToLower(format)) { return fmt.Errorf("format must be one of: %s", strings.Join(OutputFormats, ", ")) @@ -57,7 +57,7 @@ func init() { &cli.StringFlag{ Name: "format-error", Usage: "The format for displaying error data (one of: " + strings.Join(OutputFormats, ", ") + ")", - Value: "pretty", + Value: "json", Validator: func(format string) error { if !slices.Contains(OutputFormats, strings.ToLower(format)) { return fmt.Errorf("format must be one of: %s", strings.Join(OutputFormats, ", ")) diff --git a/pkg/cmd/info.go b/pkg/cmd/info.go index bb9301ea..57fdd222 100644 --- a/pkg/cmd/info.go +++ b/pkg/cmd/info.go @@ -49,8 +49,11 @@ func handleInfoRetrieve(ctx context.Context, cmd *cli.Command) error { } obj := gjson.ParseBytes(res) - format := cmd.Root().String("format") + format := "json" explicitFormat := cmd.Root().IsSet("format") + if explicitFormat { + format = cmd.Root().String("format") + } transform := cmd.Root().String("transform") return ShowJSON(obj, ShowJSONOpts{ ExplicitFormat: explicitFormat, diff --git a/pkg/cmd/message.go b/pkg/cmd/message.go index 873cbfc6..c1a22eb7 100644 --- a/pkg/cmd/message.go +++ b/pkg/cmd/message.go @@ -70,7 +70,7 @@ var messagesList = cli.Command{ var messagesSearch = cli.Command{ Name: "search", - Usage: "Search messages across chats using Beeper's message index", + Usage: "Search messages across chats.", Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[[]string]{ @@ -288,8 +288,11 @@ func handleMessagesList(ctx context.Context, cmd *cli.Command) error { return err } - format := cmd.Root().String("format") + format := "json" explicitFormat := cmd.Root().IsSet("format") + if explicitFormat { + format = cmd.Root().String("format") + } transform := cmd.Root().String("transform") if format == "raw" { var res []byte @@ -353,8 +356,11 @@ func handleMessagesSearch(ctx context.Context, cmd *cli.Command) error { return err } - format := cmd.Root().String("format") + format := "json" explicitFormat := cmd.Root().IsSet("format") + if explicitFormat { + format = cmd.Root().String("format") + } transform := cmd.Root().String("transform") if format == "raw" { var res []byte From 24ceca2b5cbf662fd9fe190036d6f29d6bf7a8d7 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 29 Apr 2026 19:53:32 +0000 Subject: [PATCH 37/54] Preserve asset serve SDK compatibility --- .stats.yml | 2 +- pkg/cmd/asset.go | 16 +--------------- pkg/cmd/asset_test.go | 1 - 3 files changed, 2 insertions(+), 17 deletions(-) diff --git a/.stats.yml b/.stats.yml index e925f68f..1d3cc367 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 23 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-611aa7641fbca8cf31d626bf86f9efd3c2b92778e897ebbb25c6ea44185ed1ed.yml -openapi_spec_hash: 4840f003552e8b48eb8e689b59a819ef +openapi_spec_hash: 8dff13848934c5b20f3804236e8286d3 config_hash: 05ebdec072113f63395372504da98192 diff --git a/pkg/cmd/asset.go b/pkg/cmd/asset.go index 6e620062..2e564895 100644 --- a/pkg/cmd/asset.go +++ b/pkg/cmd/asset.go @@ -5,7 +5,6 @@ package cmd import ( "context" "fmt" - "os" "github.com/beeper/desktop-api-cli/internal/apiquery" "github.com/beeper/desktop-api-cli/internal/requestflag" @@ -42,11 +41,6 @@ var assetsServe = cli.Command{ Required: true, QueryPath: "url", }, - &requestflag.Flag[string]{ - Name: "output", - Aliases: []string{"o"}, - Usage: "The file where the response contents will be stored. Use the value '-' to force output to stdout.", - }, }, Action: handleAssetsServe, HideHelpCommand: true, @@ -167,15 +161,7 @@ func handleAssetsServe(ctx context.Context, cmd *cli.Command) error { return err } - response, err := client.Assets.Serve(ctx, params, options...) - if err != nil { - return err - } - message, err := writeBinaryResponse(response, os.Stdout, cmd.String("output")) - if message != "" { - fmt.Println(message) - } - return err + return client.Assets.Serve(ctx, params, options...) } func handleAssetsUpload(ctx context.Context, cmd *cli.Command) error { diff --git a/pkg/cmd/asset_test.go b/pkg/cmd/asset_test.go index 4375d2e7..696660a8 100644 --- a/pkg/cmd/asset_test.go +++ b/pkg/cmd/asset_test.go @@ -37,7 +37,6 @@ func TestAssetsServe(t *testing.T) { "--access-token", "string", "assets", "serve", "--url", "x", - "--output", "/dev/null", ) }) } From ce78a27ad6a8ef45ff2bbeea7b56d965f03e4c04 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 29 Apr 2026 19:54:37 +0000 Subject: [PATCH 38/54] Document asset serve stream response --- .stats.yml | 2 +- pkg/cmd/asset.go | 16 +++++++++++++++- pkg/cmd/asset_test.go | 1 + 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 1d3cc367..e925f68f 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 23 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-611aa7641fbca8cf31d626bf86f9efd3c2b92778e897ebbb25c6ea44185ed1ed.yml -openapi_spec_hash: 8dff13848934c5b20f3804236e8286d3 +openapi_spec_hash: 4840f003552e8b48eb8e689b59a819ef config_hash: 05ebdec072113f63395372504da98192 diff --git a/pkg/cmd/asset.go b/pkg/cmd/asset.go index 2e564895..6e620062 100644 --- a/pkg/cmd/asset.go +++ b/pkg/cmd/asset.go @@ -5,6 +5,7 @@ package cmd import ( "context" "fmt" + "os" "github.com/beeper/desktop-api-cli/internal/apiquery" "github.com/beeper/desktop-api-cli/internal/requestflag" @@ -41,6 +42,11 @@ var assetsServe = cli.Command{ Required: true, QueryPath: "url", }, + &requestflag.Flag[string]{ + Name: "output", + Aliases: []string{"o"}, + Usage: "The file where the response contents will be stored. Use the value '-' to force output to stdout.", + }, }, Action: handleAssetsServe, HideHelpCommand: true, @@ -161,7 +167,15 @@ func handleAssetsServe(ctx context.Context, cmd *cli.Command) error { return err } - return client.Assets.Serve(ctx, params, options...) + response, err := client.Assets.Serve(ctx, params, options...) + if err != nil { + return err + } + message, err := writeBinaryResponse(response, os.Stdout, cmd.String("output")) + if message != "" { + fmt.Println(message) + } + return err } func handleAssetsUpload(ctx context.Context, cmd *cli.Command) error { diff --git a/pkg/cmd/asset_test.go b/pkg/cmd/asset_test.go index 696660a8..4375d2e7 100644 --- a/pkg/cmd/asset_test.go +++ b/pkg/cmd/asset_test.go @@ -37,6 +37,7 @@ func TestAssetsServe(t *testing.T) { "--access-token", "string", "assets", "serve", "--url", "x", + "--output", "/dev/null", ) }) } From 2b1da99d8aa0d518a3a1e90126467b4eabff4a03 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 30 Apr 2026 07:49:49 +0000 Subject: [PATCH 39/54] fix(cli): correctly load zsh autocompletion --- .../autocomplete/shellscripts/zsh_autocomplete.zsh | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/internal/autocomplete/shellscripts/zsh_autocomplete.zsh b/internal/autocomplete/shellscripts/zsh_autocomplete.zsh index 4d4bdcd4..d9371717 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 From 55190af84fadce3f17bf08a89471632232412c49 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 30 Apr 2026 07:53:01 +0000 Subject: [PATCH 40/54] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index e925f68f..a2edbe52 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 23 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-611aa7641fbca8cf31d626bf86f9efd3c2b92778e897ebbb25c6ea44185ed1ed.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper/beeper-desktop-api-fdefa92da44ff91a34438591d022c182a83d9daf9a28b452556e325c6ef7b3f0.yml openapi_spec_hash: 4840f003552e8b48eb8e689b59a819ef config_hash: 05ebdec072113f63395372504da98192 From 865b85180334a84053ebbc19c718f17c29c9d9d7 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 30 Apr 2026 07:54:21 +0000 Subject: [PATCH 41/54] fix: flags for nullable body scalar fields are strictly typed --- internal/requestflag/innerflag.go | 16 +- internal/requestflag/requestflag.go | 128 ++++++++++++- internal/requestflag/requestflag_test.go | 234 +++++++++++++++++++++++ pkg/cmd/chat.go | 10 +- pkg/cmd/message.go | 8 +- 5 files changed, 383 insertions(+), 13 deletions(-) diff --git a/internal/requestflag/innerflag.go b/internal/requestflag/innerflag.go index eeeb8bc3..528915f6 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 @@ -25,6 +26,12 @@ type InnerFlag[ 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. @@ -76,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 { @@ -136,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 bfaf064d..54c25098 100644 --- a/internal/requestflag/requestflag.go +++ b/internal/requestflag/requestflag.go @@ -15,10 +15,15 @@ import ( // 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 @@ -341,6 +346,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 { @@ -396,6 +406,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 } @@ -419,7 +431,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 } @@ -429,12 +442,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 @@ -465,6 +493,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 @@ -501,6 +571,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`), @@ -594,6 +671,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)) @@ -705,6 +791,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]{} @@ -718,6 +813,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 0e86e074..06ffb726 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,64 @@ 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)) + }) +} diff --git a/pkg/cmd/chat.go b/pkg/cmd/chat.go index 166458eb..a85812f4 100644 --- a/pkg/cmd/chat.go +++ b/pkg/cmd/chat.go @@ -104,10 +104,10 @@ var chatsRetrieve = cli.Command{ Usage: "Unique identifier of the chat.", Required: true, }, - &requestflag.Flag[any]{ + &requestflag.Flag[*int64]{ Name: "max-participant-count", Usage: "Maximum number of participants to return. Use -1 for all; otherwise 0–500. Defaults to all (-1).", - Default: -1, + Default: requestflag.Ptr[int64](-1), QueryPath: "maxParticipantCount", }, }, @@ -190,10 +190,10 @@ var chatsSearch = cli.Command{ Usage: `Filter by inbox type: "primary" (non-archived, non-low-priority), "low-priority", or "archive". If not specified, shows all chats.`, QueryPath: "inbox", }, - &requestflag.Flag[any]{ + &requestflag.Flag[*bool]{ Name: "include-muted", Usage: "Include chats marked as Muted by the user, which are usually less important. Default: true. Set to false if the user wants a more refined search.", - Default: true, + Default: requestflag.Ptr[bool](true), QueryPath: "includeMuted", }, &requestflag.Flag[any]{ @@ -229,7 +229,7 @@ var chatsSearch = cli.Command{ Default: "any", QueryPath: "type", }, - &requestflag.Flag[any]{ + &requestflag.Flag[*bool]{ Name: "unread-only", Usage: "Set to true to only retrieve chats that have unread messages", QueryPath: "unreadOnly", diff --git a/pkg/cmd/message.go b/pkg/cmd/message.go index c1a22eb7..815bb102 100644 --- a/pkg/cmd/message.go +++ b/pkg/cmd/message.go @@ -108,16 +108,16 @@ var messagesSearch = cli.Command{ Usage: "Pagination direction used with 'cursor': 'before' fetches older results, 'after' fetches newer results. Defaults to 'before' when only 'cursor' is provided.", QueryPath: "direction", }, - &requestflag.Flag[any]{ + &requestflag.Flag[*bool]{ Name: "exclude-low-priority", Usage: "Exclude messages marked Low Priority by the user. Default: true. Set to false to include all.", - Default: true, + Default: requestflag.Ptr[bool](true), QueryPath: "excludeLowPriority", }, - &requestflag.Flag[any]{ + &requestflag.Flag[*bool]{ Name: "include-muted", Usage: "Include messages in chats marked as Muted by the user, which are usually less important. Default: true. Set to false if the user wants a more refined search.", - Default: true, + Default: requestflag.Ptr[bool](true), QueryPath: "includeMuted", }, &requestflag.Flag[int64]{ From 5fe5a8d8aa02222e08a8407728d8063d08d9fdb9 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 1 May 2026 03:37:58 +0000 Subject: [PATCH 42/54] feat: support passing path and query params over stdin --- internal/requestflag/requestflag.go | 115 +++++++- internal/requestflag/requestflag_test.go | 345 +++++++++++++++++++++++ pkg/cmd/accountcontact.go | 22 +- pkg/cmd/asset.go | 16 +- pkg/cmd/beeperdesktopapi.go | 8 +- pkg/cmd/chat.go | 34 +-- pkg/cmd/chatmessagereaction.go | 40 +-- pkg/cmd/chatreminder.go | 18 +- pkg/cmd/flagoptions.go | 48 +++- pkg/cmd/message.go | 46 +-- 10 files changed, 595 insertions(+), 97 deletions(-) diff --git a/internal/requestflag/requestflag.go b/internal/requestflag/requestflag.go index 54c25098..77c4f1f3 100644 --- a/internal/requestflag/requestflag.go +++ b/internal/requestflag/requestflag.go @@ -1,6 +1,7 @@ package requestflag import ( + "encoding/json" "fmt" "reflect" "strconv" @@ -12,6 +13,26 @@ 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.). @@ -41,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, @@ -72,6 +94,7 @@ type InRequest interface { GetQueryPath() string GetHeaderPath() string GetBodyPath() string + GetPathParam() string IsBodyRoot() bool IsFileInput() bool GetDataAliases() []string @@ -89,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 } @@ -108,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{ @@ -291,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 diff --git a/internal/requestflag/requestflag_test.go b/internal/requestflag/requestflag_test.go index 06ffb726..779bd57f 100644 --- a/internal/requestflag/requestflag_test.go +++ b/internal/requestflag/requestflag_test.go @@ -880,3 +880,348 @@ func TestInnerFlagDispatchOnUntypedFlag(t *testing.T) { 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/accountcontact.go b/pkg/cmd/accountcontact.go index cd491b07..e6265816 100644 --- a/pkg/cmd/accountcontact.go +++ b/pkg/cmd/accountcontact.go @@ -20,9 +20,10 @@ var accountsContactsList = cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "account-id", - Usage: "Account ID this resource belongs to.", - Required: true, + Name: "account-id", + Usage: "Account ID this resource belongs to.", + Required: true, + PathParam: "accountID", }, &requestflag.Flag[string]{ Name: "cursor", @@ -60,9 +61,10 @@ var accountsContactsSearch = cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "account-id", - Usage: "Account ID this resource belongs to.", - Required: true, + Name: "account-id", + Usage: "Account ID this resource belongs to.", + Required: true, + PathParam: "accountID", }, &requestflag.Flag[string]{ Name: "query", @@ -86,8 +88,6 @@ func handleAccountsContactsList(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := beeperdesktopapi.AccountContactListParams{} - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -99,6 +99,8 @@ func handleAccountsContactsList(ctx context.Context, cmd *cli.Command) error { return err } + params := beeperdesktopapi.AccountContactListParams{} + format := "json" explicitFormat := cmd.Root().IsSet("format") if explicitFormat { @@ -157,8 +159,6 @@ func handleAccountsContactsSearch(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := beeperdesktopapi.AccountContactSearchParams{} - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -170,6 +170,8 @@ func handleAccountsContactsSearch(ctx context.Context, cmd *cli.Command) error { return err } + params := beeperdesktopapi.AccountContactSearchParams{} + var res []byte options = append(options, option.WithResponseBodyInto(&res)) _, err = client.Accounts.Contacts.Search( diff --git a/pkg/cmd/asset.go b/pkg/cmd/asset.go index 6e620062..1b5126c4 100644 --- a/pkg/cmd/asset.go +++ b/pkg/cmd/asset.go @@ -113,8 +113,6 @@ func handleAssetsDownload(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := beeperdesktopapi.AssetDownloadParams{} - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -126,6 +124,8 @@ func handleAssetsDownload(ctx context.Context, cmd *cli.Command) error { return err } + params := beeperdesktopapi.AssetDownloadParams{} + var res []byte options = append(options, option.WithResponseBodyInto(&res)) _, err = client.Assets.Download(ctx, params, options...) @@ -154,8 +154,6 @@ func handleAssetsServe(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := beeperdesktopapi.AssetServeParams{} - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -167,6 +165,8 @@ func handleAssetsServe(ctx context.Context, cmd *cli.Command) error { return err } + params := beeperdesktopapi.AssetServeParams{} + response, err := client.Assets.Serve(ctx, params, options...) if err != nil { return err @@ -186,8 +186,6 @@ func handleAssetsUpload(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := beeperdesktopapi.AssetUploadParams{} - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -199,6 +197,8 @@ func handleAssetsUpload(ctx context.Context, cmd *cli.Command) error { return err } + params := beeperdesktopapi.AssetUploadParams{} + var res []byte options = append(options, option.WithResponseBodyInto(&res)) _, err = client.Assets.Upload(ctx, params, options...) @@ -227,8 +227,6 @@ func handleAssetsUploadBase64(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := beeperdesktopapi.AssetUploadBase64Params{} - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -240,6 +238,8 @@ func handleAssetsUploadBase64(ctx context.Context, cmd *cli.Command) error { return err } + params := beeperdesktopapi.AssetUploadBase64Params{} + var res []byte options = append(options, option.WithResponseBodyInto(&res)) _, err = client.Assets.UploadBase64(ctx, params, options...) diff --git a/pkg/cmd/beeperdesktopapi.go b/pkg/cmd/beeperdesktopapi.go index 5bc483ea..ee5f5ca2 100644 --- a/pkg/cmd/beeperdesktopapi.go +++ b/pkg/cmd/beeperdesktopapi.go @@ -68,8 +68,6 @@ func handleFocus(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := beeperdesktopapi.FocusParams{} - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -81,6 +79,8 @@ func handleFocus(ctx context.Context, cmd *cli.Command) error { return err } + params := beeperdesktopapi.FocusParams{} + var res []byte options = append(options, option.WithResponseBodyInto(&res)) _, err = client.Focus(ctx, params, options...) @@ -109,8 +109,6 @@ func handleSearch(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := beeperdesktopapi.SearchParams{} - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -122,6 +120,8 @@ func handleSearch(ctx context.Context, cmd *cli.Command) error { return err } + params := beeperdesktopapi.SearchParams{} + var res []byte options = append(options, option.WithResponseBodyInto(&res)) _, err = client.Search(ctx, params, options...) diff --git a/pkg/cmd/chat.go b/pkg/cmd/chat.go index a85812f4..e88c6564 100644 --- a/pkg/cmd/chat.go +++ b/pkg/cmd/chat.go @@ -100,9 +100,10 @@ var chatsRetrieve = cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "chat-id", - Usage: "Unique identifier of the chat.", - Required: true, + Name: "chat-id", + Usage: "Unique identifier of the chat.", + Required: true, + PathParam: "chatID", }, &requestflag.Flag[*int64]{ Name: "max-participant-count", @@ -150,9 +151,10 @@ var chatsArchive = cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "chat-id", - Usage: "Unique identifier of the chat.", - Required: true, + Name: "chat-id", + Usage: "Unique identifier of the chat.", + Required: true, + PathParam: "chatID", }, &requestflag.Flag[bool]{ Name: "archived", @@ -251,8 +253,6 @@ func handleChatsCreate(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := beeperdesktopapi.ChatNewParams{} - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -264,6 +264,8 @@ func handleChatsCreate(ctx context.Context, cmd *cli.Command) error { return err } + params := beeperdesktopapi.ChatNewParams{} + var res []byte options = append(options, option.WithResponseBodyInto(&res)) _, err = client.Chats.New(ctx, params, options...) @@ -295,8 +297,6 @@ func handleChatsRetrieve(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := beeperdesktopapi.ChatGetParams{} - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -308,6 +308,8 @@ func handleChatsRetrieve(ctx context.Context, cmd *cli.Command) error { return err } + params := beeperdesktopapi.ChatGetParams{} + var res []byte options = append(options, option.WithResponseBodyInto(&res)) _, err = client.Chats.Get( @@ -344,8 +346,6 @@ func handleChatsList(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := beeperdesktopapi.ChatListParams{} - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -357,6 +357,8 @@ func handleChatsList(ctx context.Context, cmd *cli.Command) error { return err } + params := beeperdesktopapi.ChatListParams{} + format := "json" explicitFormat := cmd.Root().IsSet("format") if explicitFormat { @@ -405,8 +407,6 @@ func handleChatsArchive(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := beeperdesktopapi.ChatArchiveParams{} - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -418,6 +418,8 @@ func handleChatsArchive(ctx context.Context, cmd *cli.Command) error { return err } + params := beeperdesktopapi.ChatArchiveParams{} + return client.Chats.Archive( ctx, cmd.Value("chat-id").(string), @@ -434,8 +436,6 @@ func handleChatsSearch(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := beeperdesktopapi.ChatSearchParams{} - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -447,6 +447,8 @@ func handleChatsSearch(ctx context.Context, cmd *cli.Command) error { return err } + params := beeperdesktopapi.ChatSearchParams{} + format := "json" explicitFormat := cmd.Root().IsSet("format") if explicitFormat { diff --git a/pkg/cmd/chatmessagereaction.go b/pkg/cmd/chatmessagereaction.go index 35cacf3f..f96f9155 100644 --- a/pkg/cmd/chatmessagereaction.go +++ b/pkg/cmd/chatmessagereaction.go @@ -20,13 +20,15 @@ var chatsMessagesReactionsDelete = cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "chat-id", - Usage: "Unique identifier of the chat.", - Required: true, + Name: "chat-id", + Usage: "Unique identifier of the chat.", + Required: true, + PathParam: "chatID", }, &requestflag.Flag[string]{ - Name: "message-id", - Required: true, + Name: "message-id", + Required: true, + PathParam: "messageID", }, &requestflag.Flag[string]{ Name: "reaction-key", @@ -45,13 +47,15 @@ var chatsMessagesReactionsAdd = cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "chat-id", - Usage: "Unique identifier of the chat.", - Required: true, + Name: "chat-id", + Usage: "Unique identifier of the chat.", + Required: true, + PathParam: "chatID", }, &requestflag.Flag[string]{ - Name: "message-id", - Required: true, + Name: "message-id", + Required: true, + PathParam: "messageID", }, &requestflag.Flag[string]{ Name: "reaction-key", @@ -80,10 +84,6 @@ func handleChatsMessagesReactionsDelete(ctx context.Context, cmd *cli.Command) e return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := beeperdesktopapi.ChatMessageReactionDeleteParams{ - ChatID: cmd.Value("chat-id").(string), - } - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -95,6 +95,10 @@ func handleChatsMessagesReactionsDelete(ctx context.Context, cmd *cli.Command) e return err } + params := beeperdesktopapi.ChatMessageReactionDeleteParams{ + ChatID: cmd.Value("chat-id").(string), + } + var res []byte options = append(options, option.WithResponseBodyInto(&res)) _, err = client.Chats.Messages.Reactions.Delete( @@ -131,10 +135,6 @@ func handleChatsMessagesReactionsAdd(ctx context.Context, cmd *cli.Command) erro return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := beeperdesktopapi.ChatMessageReactionAddParams{ - ChatID: cmd.Value("chat-id").(string), - } - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -146,6 +146,10 @@ func handleChatsMessagesReactionsAdd(ctx context.Context, cmd *cli.Command) erro return err } + params := beeperdesktopapi.ChatMessageReactionAddParams{ + ChatID: cmd.Value("chat-id").(string), + } + var res []byte options = append(options, option.WithResponseBodyInto(&res)) _, err = client.Chats.Messages.Reactions.Add( diff --git a/pkg/cmd/chatreminder.go b/pkg/cmd/chatreminder.go index 5f288e1c..acccdaf9 100644 --- a/pkg/cmd/chatreminder.go +++ b/pkg/cmd/chatreminder.go @@ -18,9 +18,10 @@ var chatsRemindersCreate = requestflag.WithInnerFlags(cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "chat-id", - Usage: "Unique identifier of the chat.", - Required: true, + Name: "chat-id", + Usage: "Unique identifier of the chat.", + Required: true, + PathParam: "chatID", }, &requestflag.Flag[map[string]any]{ Name: "reminder", @@ -52,9 +53,10 @@ var chatsRemindersDelete = cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "chat-id", - Usage: "Unique identifier of the chat.", - Required: true, + Name: "chat-id", + Usage: "Unique identifier of the chat.", + Required: true, + PathParam: "chatID", }, }, Action: handleChatsRemindersDelete, @@ -72,8 +74,6 @@ func handleChatsRemindersCreate(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := beeperdesktopapi.ChatReminderNewParams{} - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -85,6 +85,8 @@ func handleChatsRemindersCreate(ctx context.Context, cmd *cli.Command) error { return err } + params := beeperdesktopapi.ChatReminderNewParams{} + return client.Chats.Reminders.New( ctx, cmd.Value("chat-id").(string), diff --git a/pkg/cmd/flagoptions.go b/pkg/cmd/flagoptions.go index aa566b3a..9452fd02 100644 --- a/pkg/cmd/flagoptions.go +++ b/pkg/cmd/flagoptions.go @@ -339,7 +339,7 @@ func flagOptions( } 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 @@ -353,16 +353,45 @@ func flagOptions( } if bodyMap, ok := bodyData.(map[string]any); ok { applyDataAliases(cmd, bodyMap) - if flagMap, ok := requestContents.Body.(map[string]any); ok { - maps.Copy(bodyMap, flagMap) - requestContents.Body = 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 } } } @@ -370,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 { diff --git a/pkg/cmd/message.go b/pkg/cmd/message.go index 815bb102..ff1413f7 100644 --- a/pkg/cmd/message.go +++ b/pkg/cmd/message.go @@ -20,13 +20,15 @@ var messagesUpdate = cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "chat-id", - Usage: "Unique identifier of the chat.", - Required: true, + Name: "chat-id", + Usage: "Unique identifier of the chat.", + Required: true, + PathParam: "chatID", }, &requestflag.Flag[string]{ - Name: "message-id", - Required: true, + Name: "message-id", + Required: true, + PathParam: "messageID", }, &requestflag.Flag[string]{ Name: "text", @@ -45,9 +47,10 @@ var messagesList = cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "chat-id", - Usage: "Unique identifier of the chat.", - Required: true, + Name: "chat-id", + Usage: "Unique identifier of the chat.", + Required: true, + PathParam: "chatID", }, &requestflag.Flag[string]{ Name: "cursor", @@ -156,9 +159,10 @@ var messagesSend = requestflag.WithInnerFlags(cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "chat-id", - Usage: "Unique identifier of the chat.", - Required: true, + Name: "chat-id", + Usage: "Unique identifier of the chat.", + Required: true, + PathParam: "chatID", }, &requestflag.Flag[map[string]any]{ Name: "attachment", @@ -224,10 +228,6 @@ func handleMessagesUpdate(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := beeperdesktopapi.MessageUpdateParams{ - ChatID: cmd.Value("chat-id").(string), - } - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -239,6 +239,10 @@ func handleMessagesUpdate(ctx context.Context, cmd *cli.Command) error { return err } + params := beeperdesktopapi.MessageUpdateParams{ + ChatID: cmd.Value("chat-id").(string), + } + var res []byte options = append(options, option.WithResponseBodyInto(&res)) _, err = client.Messages.Update( @@ -275,8 +279,6 @@ func handleMessagesList(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := beeperdesktopapi.MessageListParams{} - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -288,6 +290,8 @@ func handleMessagesList(ctx context.Context, cmd *cli.Command) error { return err } + params := beeperdesktopapi.MessageListParams{} + format := "json" explicitFormat := cmd.Root().IsSet("format") if explicitFormat { @@ -343,8 +347,6 @@ func handleMessagesSearch(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := beeperdesktopapi.MessageSearchParams{} - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -356,6 +358,8 @@ func handleMessagesSearch(ctx context.Context, cmd *cli.Command) error { return err } + params := beeperdesktopapi.MessageSearchParams{} + format := "json" explicitFormat := cmd.Root().IsSet("format") if explicitFormat { @@ -404,8 +408,6 @@ func handleMessagesSend(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } - params := beeperdesktopapi.MessageSendParams{} - options, err := flagOptions( cmd, apiquery.NestedQueryFormatBrackets, @@ -417,6 +419,8 @@ func handleMessagesSend(ctx context.Context, cmd *cli.Command) error { return err } + params := beeperdesktopapi.MessageSendParams{} + var res []byte options = append(options, option.WithResponseBodyInto(&res)) _, err = client.Messages.Send( From 36b4a2f8acddfd6f1da0ee61f18438bdd26c4d8c Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 1 May 2026 03:38:56 +0000 Subject: [PATCH 43/54] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index a2edbe52..ec755712 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 23 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper/beeper-desktop-api-fdefa92da44ff91a34438591d022c182a83d9daf9a28b452556e325c6ef7b3f0.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper/beeper-desktop-api-356444646dafe352d3ef7c2e01aedf030197a5519b41cf2c3fd8be2571456b43.yml openapi_spec_hash: 4840f003552e8b48eb8e689b59a819ef config_hash: 05ebdec072113f63395372504da98192 From bf4b43c832c3b56adda72018ffd91a7dc0660966 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 4 May 2026 18:07:10 +0000 Subject: [PATCH 44/54] feat(api): api update --- .stats.yml | 8 +- pkg/cmd/chat.go | 172 +++++++++++++++++++++++++++++-------------- pkg/cmd/chat_test.go | 93 +++++++++++++---------- pkg/cmd/cmd.go | 1 + 4 files changed, 177 insertions(+), 97 deletions(-) diff --git a/.stats.yml b/.stats.yml index ec755712..75ad7955 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 23 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper/beeper-desktop-api-356444646dafe352d3ef7c2e01aedf030197a5519b41cf2c3fd8be2571456b43.yml -openapi_spec_hash: 4840f003552e8b48eb8e689b59a819ef -config_hash: 05ebdec072113f63395372504da98192 +configured_endpoints: 24 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper/beeper-desktop-api-8b89ffbfeb39b4186328fefd81f7dab1d28c786012201feb8035c7f920c4fbae.yml +openapi_spec_hash: de40e013fcc83fa44d5c51ddecb543c0 +config_hash: 08b781db5f1857ed601d1b2f4b6b45d9 diff --git a/pkg/cmd/chat.go b/pkg/cmd/chat.go index e88c6564..99523ed6 100644 --- a/pkg/cmd/chat.go +++ b/pkg/cmd/chat.go @@ -14,9 +14,9 @@ import ( "github.com/urfave/cli/v3" ) -var chatsCreate = requestflag.WithInnerFlags(cli.Command{ +var chatsCreate = cli.Command{ Name: "create", - Usage: "Create a direct or group chat with mode=\"create\", or use mode=\"start\" to resolve\na contact and open a direct chat.", + Usage: "Create a direct or group chat from participant IDs.", Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ @@ -25,74 +25,32 @@ var chatsCreate = requestflag.WithInnerFlags(cli.Command{ Required: true, BodyPath: "accountID", }, - &requestflag.Flag[bool]{ - Name: "allow-invite", - Usage: "Only used for mode='start'. Whether invite-based DM creation is allowed when required by the platform.", - Default: true, - BodyPath: "allowInvite", + &requestflag.Flag[[]string]{ + Name: "participant-id", + Usage: "User IDs to include in the new chat.", + Required: true, + BodyPath: "participantIDs", + }, + &requestflag.Flag[string]{ + Name: "type", + Usage: "'single' requires exactly one participantID; 'group' supports multiple participants and optional title.", + Required: true, + BodyPath: "type", }, &requestflag.Flag[string]{ Name: "message-text", Usage: "Optional first message content if the platform requires it to create the chat.", BodyPath: "messageText", }, - &requestflag.Flag[string]{ - Name: "mode", - Usage: "Operation mode. Use 'start' to resolve a user/contact and start a direct chat. Omit or set 'create' to create a chat directly.", - BodyPath: "mode", - }, - &requestflag.Flag[[]string]{ - Name: "participant-id", - Usage: "Required for create mode. Provide exactly one user ID for 'single' chats and one or more for 'group' chats.", - BodyPath: "participantIDs", - }, &requestflag.Flag[string]{ Name: "title", Usage: "Optional title for group chats; ignored for single chats on most networks.", BodyPath: "title", }, - &requestflag.Flag[string]{ - Name: "type", - Usage: "Required for create mode. 'single' creates a direct message chat; 'group' creates a group chat.", - BodyPath: "type", - }, - &requestflag.Flag[map[string]any]{ - Name: "user", - Usage: "Required for mode='start'. Merged user-like contact payload used to resolve the best identifier.", - BodyPath: "user", - }, }, Action: handleChatsCreate, HideHelpCommand: true, -}, map[string][]requestflag.HasOuterFlag{ - "user": { - &requestflag.InnerFlag[string]{ - Name: "user.id", - Usage: "Known user ID when available.", - InnerField: "id", - }, - &requestflag.InnerFlag[string]{ - Name: "user.email", - Usage: "Email candidate.", - InnerField: "email", - }, - &requestflag.InnerFlag[string]{ - Name: "user.full-name", - Usage: "Display name hint used for ranking only.", - InnerField: "fullName", - }, - &requestflag.InnerFlag[string]{ - Name: "user.phone-number", - Usage: "Phone number candidate (E.164 preferred).", - InnerField: "phoneNumber", - }, - &requestflag.InnerFlag[string]{ - Name: "user.username", - Usage: "Username/handle candidate.", - InnerField: "username", - }, - }, -}) +} var chatsRetrieve = cli.Command{ Name: "retrieve", @@ -245,6 +203,67 @@ var chatsSearch = cli.Command{ HideHelpCommand: true, } +var chatsStart = requestflag.WithInnerFlags(cli.Command{ + Name: "start", + Usage: "Resolve a user/contact and open a direct chat. Reuses an existing direct chat\nwhen one is found. Available in Beeper Desktop v4.2.799+.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "account-id", + Usage: "Account to create or start the chat on.", + Required: true, + BodyPath: "accountID", + }, + &requestflag.Flag[map[string]any]{ + Name: "user", + Usage: "Merged user-like contact payload used to resolve the best identifier.", + Required: true, + BodyPath: "user", + }, + &requestflag.Flag[bool]{ + Name: "allow-invite", + Usage: "Whether invite-based DM creation is allowed when required by the platform.", + Default: true, + BodyPath: "allowInvite", + }, + &requestflag.Flag[string]{ + Name: "message-text", + Usage: "Optional first message content if the platform requires it to create the chat.", + BodyPath: "messageText", + }, + }, + Action: handleChatsStart, + HideHelpCommand: true, +}, map[string][]requestflag.HasOuterFlag{ + "user": { + &requestflag.InnerFlag[string]{ + Name: "user.id", + Usage: "Known user ID when available.", + InnerField: "id", + }, + &requestflag.InnerFlag[string]{ + Name: "user.email", + Usage: "Email candidate.", + InnerField: "email", + }, + &requestflag.InnerFlag[string]{ + Name: "user.full-name", + Usage: "Display name hint used for ranking only.", + InnerField: "fullName", + }, + &requestflag.InnerFlag[string]{ + Name: "user.phone-number", + Usage: "Phone number candidate (E.164 preferred).", + InnerField: "phoneNumber", + }, + &requestflag.InnerFlag[string]{ + Name: "user.username", + Usage: "Username/handle candidate.", + InnerField: "username", + }, + }, +}) + func handleChatsCreate(ctx context.Context, cmd *cli.Command) error { client := beeperdesktopapi.NewClient(getDefaultRequestOptions(cmd)...) unusedArgs := cmd.Args().Slice() @@ -485,3 +504,44 @@ func handleChatsSearch(ctx context.Context, cmd *cli.Command) error { }) } } + +func handleChatsStart(ctx context.Context, cmd *cli.Command) error { + client := beeperdesktopapi.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.ArrayQueryFormatRepeat, + ApplicationJSON, + false, + ) + if err != nil { + return err + } + + params := beeperdesktopapi.ChatStartParams{} + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Chats.Start(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: "chats start", + Transform: transform, + }) +} diff --git a/pkg/cmd/chat_test.go b/pkg/cmd/chat_test.go index d7966cd4..4e79c016 100644 --- a/pkg/cmd/chat_test.go +++ b/pkg/cmd/chat_test.go @@ -16,37 +16,10 @@ func TestChatsCreate(t *testing.T) { "--access-token", "string", "chats", "create", "--account-id", "accountID", - "--allow-invite=true", - "--message-text", "messageText", - "--mode", "start", "--participant-id", "string", - "--title", "title", "--type", "single", - "--user", "{id: id, email: email, fullName: fullName, phoneNumber: phoneNumber, username: username}", - ) - }) - - t.Run("inner flags", func(t *testing.T) { - // Check that inner flags have been set up correctly - requestflag.CheckInnerFlags(chatsCreate) - - // Alternative argument passing style using inner flags - mocktest.TestRunMockTestWithFlags( - t, - "--access-token", "string", - "chats", "create", - "--account-id", "accountID", - "--allow-invite=true", "--message-text", "messageText", - "--mode", "start", - "--participant-id", "string", "--title", "title", - "--type", "single", - "--user.id", "id", - "--user.email", "email", - "--user.full-name", "fullName", - "--user.phone-number", "phoneNumber", - "--user.username", "username", ) }) @@ -54,19 +27,11 @@ func TestChatsCreate(t *testing.T) { // Test piping YAML data over stdin pipeData := []byte("" + "accountID: accountID\n" + - "allowInvite: true\n" + - "messageText: messageText\n" + - "mode: start\n" + "participantIDs:\n" + " - string\n" + - "title: title\n" + "type: single\n" + - "user:\n" + - " id: id\n" + - " email: email\n" + - " fullName: fullName\n" + - " phoneNumber: phoneNumber\n" + - " username: username\n") + "messageText: messageText\n" + + "title: title\n") mocktest.TestRunMockTestWithPipeAndFlags( t, pipeData, "--access-token", "string", @@ -148,3 +113,57 @@ func TestChatsSearch(t *testing.T) { ) }) } + +func TestChatsStart(t *testing.T) { + t.Skip("Stainless mock tests currently load the project-published OpenAPI spec URL, which may not include newly-added local-only endpoints during build checks.") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--access-token", "string", + "chats", "start", + "--account-id", "accountID", + "--user", "{id: id, email: email, fullName: fullName, phoneNumber: phoneNumber, username: username}", + "--allow-invite=true", + "--message-text", "messageText", + ) + }) + + t.Run("inner flags", func(t *testing.T) { + // Check that inner flags have been set up correctly + requestflag.CheckInnerFlags(chatsStart) + + // Alternative argument passing style using inner flags + mocktest.TestRunMockTestWithFlags( + t, + "--access-token", "string", + "chats", "start", + "--account-id", "accountID", + "--user.id", "id", + "--user.email", "email", + "--user.full-name", "fullName", + "--user.phone-number", "phoneNumber", + "--user.username", "username", + "--allow-invite=true", + "--message-text", "messageText", + ) + }) + + t.Run("piping data", func(t *testing.T) { + // Test piping YAML data over stdin + pipeData := []byte("" + + "accountID: accountID\n" + + "user:\n" + + " id: id\n" + + " email: email\n" + + " fullName: fullName\n" + + " phoneNumber: phoneNumber\n" + + " username: username\n" + + "allowInvite: true\n" + + "messageText: messageText\n") + mocktest.TestRunMockTestWithPipeAndFlags( + t, pipeData, + "--access-token", "string", + "chats", "start", + ) + }) +} diff --git a/pkg/cmd/cmd.go b/pkg/cmd/cmd.go index 81a042c1..12982357 100644 --- a/pkg/cmd/cmd.go +++ b/pkg/cmd/cmd.go @@ -114,6 +114,7 @@ func init() { &chatsList, &chatsArchive, &chatsSearch, + &chatsStart, }, }, { From 3920764851961421776d64cf694dee1f999afa3e Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 4 May 2026 18:51:31 +0000 Subject: [PATCH 45/54] feat(api): api update --- .stats.yml | 4 ++-- pkg/cmd/asset.go | 17 +++++++++-------- pkg/cmd/asset_test.go | 18 +++++++----------- 3 files changed, 18 insertions(+), 21 deletions(-) diff --git a/.stats.yml b/.stats.yml index 75ad7955..8180b73e 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 24 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper/beeper-desktop-api-8b89ffbfeb39b4186328fefd81f7dab1d28c786012201feb8035c7f920c4fbae.yml -openapi_spec_hash: de40e013fcc83fa44d5c51ddecb543c0 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper/beeper-desktop-api-7a6e57b0b4f45713b9a6e87421b1e6d867e83151b2536cde07165c6b7e1ffc87.yml +openapi_spec_hash: 68a4fd30bd7eb32aea004dc2af15e9ac config_hash: 08b781db5f1857ed601d1b2f4b6b45d9 diff --git a/pkg/cmd/asset.go b/pkg/cmd/asset.go index 1b5126c4..9ccd4b6c 100644 --- a/pkg/cmd/asset.go +++ b/pkg/cmd/asset.go @@ -58,20 +58,21 @@ var assetsUpload = cli.Command{ Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "file", - Usage: "The file to upload (max 500 MB).", - Required: true, - BodyPath: "file", - FileInput: true, + Name: "content", + Usage: "Base64-encoded file content (max ~500MB decoded)", + Required: true, + BodyPath: "content", }, &requestflag.Flag[string]{ Name: "file-name", - Usage: "Original filename. Defaults to the uploaded file name if omitted", + Usage: "Original filename. Required for the JSON form of /v1/assets/upload.", + Required: true, BodyPath: "fileName", }, &requestflag.Flag[string]{ Name: "mime-type", - Usage: "MIME type. Auto-detected from magic bytes if omitted", + Usage: "MIME type. Required for the JSON form of /v1/assets/upload.", + Required: true, BodyPath: "mimeType", }, }, @@ -190,7 +191,7 @@ func handleAssetsUpload(ctx context.Context, cmd *cli.Command) error { cmd, apiquery.NestedQueryFormatBrackets, apiquery.ArrayQueryFormatRepeat, - MultipartFormEncoded, + ApplicationJSON, false, ) if err != nil { diff --git a/pkg/cmd/asset_test.go b/pkg/cmd/asset_test.go index 4375d2e7..869a671f 100644 --- a/pkg/cmd/asset_test.go +++ b/pkg/cmd/asset_test.go @@ -3,7 +3,6 @@ package cmd import ( - "strings" "testing" "github.com/beeper/desktop-api-cli/internal/mocktest" @@ -48,21 +47,18 @@ func TestAssetsUpload(t *testing.T) { t, "--access-token", "string", "assets", "upload", - "--file", mocktest.TestFile(t, "Example data"), - "--file-name", "fileName", - "--mime-type", "mimeType", + "--content", "x", + "--file-name", "x", + "--mime-type", "x", ) }) t.Run("piping data", func(t *testing.T) { - testFile := mocktest.TestFile(t, "Example data") // Test piping YAML data over stdin - pipeDataStr := "" + - "file: Example data\n" + - "fileName: fileName\n" + - "mimeType: mimeType\n" - pipeDataStr = strings.ReplaceAll(pipeDataStr, "Example data", testFile) - pipeData := []byte(pipeDataStr) + pipeData := []byte("" + + "content: x\n" + + "fileName: x\n" + + "mimeType: x\n") mocktest.TestRunMockTestWithPipeAndFlags( t, pipeData, "--access-token", "string", From 3c797176b7ced4499d6bf3ff71f7d3a94967e026 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 6 May 2026 21:17:49 +0000 Subject: [PATCH 46/54] feat(api): api update --- .stats.yml | 8 +- README.md | 3 + pkg/cmd/account.go | 2 +- pkg/cmd/asset.go | 27 ++- pkg/cmd/asset_test.go | 18 +- pkg/cmd/beeperdesktopapi.go | 6 +- pkg/cmd/chat.go | 340 +++++++++++++++++++++++++++- pkg/cmd/chat_test.go | 141 +++++++++++- pkg/cmd/chatmessagereaction.go | 21 +- pkg/cmd/chatmessagereaction_test.go | 6 +- pkg/cmd/chatreminder.go | 12 +- pkg/cmd/chatreminder_test.go | 6 +- pkg/cmd/cmd.go | 6 + pkg/cmd/info.go | 2 +- pkg/cmd/message.go | 149 +++++++++++- pkg/cmd/message_test.go | 38 +++- 16 files changed, 713 insertions(+), 72 deletions(-) diff --git a/.stats.yml b/.stats.yml index 8180b73e..2dd3fee6 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 24 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper/beeper-desktop-api-7a6e57b0b4f45713b9a6e87421b1e6d867e83151b2536cde07165c6b7e1ffc87.yml -openapi_spec_hash: 68a4fd30bd7eb32aea004dc2af15e9ac -config_hash: 08b781db5f1857ed601d1b2f4b6b45d9 +configured_endpoints: 30 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper/beeper-desktop-api-c08c14bb754b4cb0e02b21fabb680469368286be339dec0aaa8c69d04a1f021a.yml +openapi_spec_hash: a10246aaf7cdc33b682fc245bd5f893b +config_hash: 72f9d43b9b51a5da912e9f3730e53ae2 diff --git a/README.md b/README.md index de0b813e..6bca0848 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,9 @@ beeper-desktop-cli [resource] [flags...] ```sh beeper-desktop-cli chats search \ --access-token 'My Access Token' \ + --account-id matrix \ + --account-id discordgo \ + --account-id local-whatsapp_ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc \ --include-muted \ --limit 3 \ --type single diff --git a/pkg/cmd/account.go b/pkg/cmd/account.go index 9cf40bb3..382b5fcc 100644 --- a/pkg/cmd/account.go +++ b/pkg/cmd/account.go @@ -15,7 +15,7 @@ import ( var accountsList = cli.Command{ Name: "list", - Usage: "Lists chat accounts across networks (WhatsApp, Telegram, Twitter/X, etc.)\nactively connected to this Beeper Desktop instance", + Usage: "List Chat Accounts connected to this Beeper Desktop instance, including bridge\nmetadata and network identity.", Suggest: true, Flags: []cli.Flag{}, Action: handleAccountsList, diff --git a/pkg/cmd/asset.go b/pkg/cmd/asset.go index 9ccd4b6c..959b2fb9 100644 --- a/pkg/cmd/asset.go +++ b/pkg/cmd/asset.go @@ -17,12 +17,12 @@ import ( var assetsDownload = cli.Command{ Name: "download", - Usage: "Download a Matrix asset using its mxc:// or localmxc:// URL to the device\nrunning Beeper Desktop and return the local file URL.", + Usage: "Download a Matrix file using its mxc:// or localmxc:// URL to the device running\nBeeper Desktop and return the local file URL.", Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ Name: "url", - Usage: "Matrix content URL (mxc:// or localmxc://) for the asset to download.", + Usage: "Matrix content URL (mxc:// or localmxc://) for the file to download.", Required: true, BodyPath: "url", }, @@ -38,7 +38,7 @@ var assetsServe = cli.Command{ Flags: []cli.Flag{ &requestflag.Flag[string]{ Name: "url", - Usage: "Asset URL to serve. Accepts mxc://, localmxc://, or file:// URLs.", + Usage: "File URL to serve. Accepts mxc://, localmxc://, or file:// URLs.", Required: true, QueryPath: "url", }, @@ -54,25 +54,24 @@ var assetsServe = cli.Command{ var assetsUpload = cli.Command{ Name: "upload", - Usage: "Upload a file to a temporary location using multipart/form-data. Returns an\nuploadID that can be referenced when sending messages with attachments.", + Usage: "Upload a file to a temporary location using multipart/form-data. Returns an\nuploadID that can be referenced when sending a message or materializing a draft\nattachment.", Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "content", - Usage: "Base64-encoded file content (max ~500MB decoded)", - Required: true, - BodyPath: "content", + Name: "file", + Usage: "The file to upload (max 500 MB).", + Required: true, + BodyPath: "file", + FileInput: true, }, &requestflag.Flag[string]{ Name: "file-name", - Usage: "Original filename. Required for the JSON form of /v1/assets/upload.", - Required: true, + Usage: "Original filename. Defaults to the uploaded file name if omitted", BodyPath: "fileName", }, &requestflag.Flag[string]{ Name: "mime-type", - Usage: "MIME type. Required for the JSON form of /v1/assets/upload.", - Required: true, + Usage: "MIME type. Auto-detected from magic bytes if omitted", BodyPath: "mimeType", }, }, @@ -82,7 +81,7 @@ var assetsUpload = cli.Command{ var assetsUploadBase64 = cli.Command{ Name: "upload-base64", - Usage: "Upload a file using a JSON body with base64-encoded content. Returns an uploadID\nthat can be referenced when sending messages with attachments. Alternative to\nthe multipart upload endpoint.", + Usage: "Upload a file using a JSON body with base64-encoded content. Returns an uploadID\nthat can be referenced when sending a message or materializing a draft\nattachment. Alternative to the multipart upload endpoint.", Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ @@ -191,7 +190,7 @@ func handleAssetsUpload(ctx context.Context, cmd *cli.Command) error { cmd, apiquery.NestedQueryFormatBrackets, apiquery.ArrayQueryFormatRepeat, - ApplicationJSON, + MultipartFormEncoded, false, ) if err != nil { diff --git a/pkg/cmd/asset_test.go b/pkg/cmd/asset_test.go index 869a671f..4375d2e7 100644 --- a/pkg/cmd/asset_test.go +++ b/pkg/cmd/asset_test.go @@ -3,6 +3,7 @@ package cmd import ( + "strings" "testing" "github.com/beeper/desktop-api-cli/internal/mocktest" @@ -47,18 +48,21 @@ func TestAssetsUpload(t *testing.T) { t, "--access-token", "string", "assets", "upload", - "--content", "x", - "--file-name", "x", - "--mime-type", "x", + "--file", mocktest.TestFile(t, "Example data"), + "--file-name", "fileName", + "--mime-type", "mimeType", ) }) t.Run("piping data", func(t *testing.T) { + testFile := mocktest.TestFile(t, "Example data") // Test piping YAML data over stdin - pipeData := []byte("" + - "content: x\n" + - "fileName: x\n" + - "mimeType: x\n") + pipeDataStr := "" + + "file: Example data\n" + + "fileName: fileName\n" + + "mimeType: mimeType\n" + pipeDataStr = strings.ReplaceAll(pipeDataStr, "Example data", testFile) + pipeData := []byte(pipeDataStr) mocktest.TestRunMockTestWithPipeAndFlags( t, pipeData, "--access-token", "string", diff --git a/pkg/cmd/beeperdesktopapi.go b/pkg/cmd/beeperdesktopapi.go index ee5f5ca2..c5757106 100644 --- a/pkg/cmd/beeperdesktopapi.go +++ b/pkg/cmd/beeperdesktopapi.go @@ -16,7 +16,7 @@ import ( var focus = cli.Command{ Name: "focus", - Usage: "Focus Beeper Desktop and optionally navigate to a specific chat, message, or\npre-fill draft text and attachment.", + Usage: "Focus Beeper Desktop and optionally navigate to a specific chat, message, or\npre-fill plain text and an image path.", Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ @@ -26,12 +26,12 @@ var focus = cli.Command{ }, &requestflag.Flag[string]{ Name: "draft-attachment-path", - Usage: "Optional draft attachment path to populate in the message input field.", + Usage: "Optional image path to populate in the message input field.", BodyPath: "draftAttachmentPath", }, &requestflag.Flag[string]{ Name: "draft-text", - Usage: "Optional draft text to populate in the message input field.", + Usage: "Optional plain text to populate in the message input field.", BodyPath: "draftText", }, &requestflag.Flag[string]{ diff --git a/pkg/cmd/chat.go b/pkg/cmd/chat.go index 99523ed6..7b73175b 100644 --- a/pkg/cmd/chat.go +++ b/pkg/cmd/chat.go @@ -16,7 +16,7 @@ import ( var chatsCreate = cli.Command{ Name: "create", - Usage: "Create a direct or group chat from participant IDs.", + Usage: "Create a direct or group chat from participant IDs. Returns the created chat.", Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ @@ -59,14 +59,14 @@ var chatsRetrieve = cli.Command{ Flags: []cli.Flag{ &requestflag.Flag[string]{ Name: "chat-id", - Usage: "Unique identifier of the chat.", + Usage: "Chat ID. Input routes also accept the local chat ID from this Beeper Desktop installation when available.", Required: true, PathParam: "chatID", }, &requestflag.Flag[*int64]{ Name: "max-participant-count", - Usage: "Maximum number of participants to return. Use -1 for all; otherwise 0–500. Defaults to all (-1).", - Default: requestflag.Ptr[int64](-1), + Usage: "Maximum number of participants to return. Use -1 for all; otherwise 0-500. Defaults to 100. List and search endpoints return up to 20 participants per chat.", + Default: requestflag.Ptr[int64](100), QueryPath: "maxParticipantCount", }, }, @@ -74,6 +74,80 @@ var chatsRetrieve = cli.Command{ HideHelpCommand: true, } +var chatsUpdate = requestflag.WithInnerFlags(cli.Command{ + Name: "update", + Usage: "Update supported chat fields. Non-empty draft objects are accepted only when the\ncurrent draft is empty. Send draft=null to clear the draft before setting new\ndraft text or attachments.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "chat-id", + Usage: "Chat ID. Input routes also accept the local chat ID from this Beeper Desktop installation when available.", + Required: true, + PathParam: "chatID", + }, + &requestflag.Flag[*string]{ + Name: "description", + Usage: "Group chat description/topic. Support depends on the chat account and chat permissions.", + BodyPath: "description", + }, + &requestflag.Flag[map[string]any]{ + Name: "draft", + Usage: "Draft object to set or clear. Non-empty drafts are only accepted when the current draft is empty. Send draft=null to clear text and attachments together before setting a new draft.", + BodyPath: "draft", + }, + &requestflag.Flag[*string]{ + Name: "img-url", + Usage: "Local filesystem path to a group chat avatar image. Support depends on the chat account and chat permissions.", + BodyPath: "imgURL", + }, + &requestflag.Flag[bool]{ + Name: "is-archived", + Usage: "Archive or unarchive the chat.", + BodyPath: "isArchived", + }, + &requestflag.Flag[bool]{ + Name: "is-low-priority", + Usage: "Mark or unmark the chat as low priority when supported by the account.", + BodyPath: "isLowPriority", + }, + &requestflag.Flag[bool]{ + Name: "is-muted", + Usage: "Mute or unmute the chat.", + BodyPath: "isMuted", + }, + &requestflag.Flag[bool]{ + Name: "is-pinned", + Usage: "Pin or unpin the chat when supported by the account.", + BodyPath: "isPinned", + }, + &requestflag.Flag[*int64]{ + Name: "message-expiry-seconds", + Usage: "Disappearing-message timer in seconds, or null to clear when supported.", + BodyPath: "messageExpirySeconds", + }, + &requestflag.Flag[*string]{ + Name: "title", + Usage: "Custom chat title. Support depends on the chat account and chat permissions.", + BodyPath: "title", + }, + }, + Action: handleChatsUpdate, + HideHelpCommand: true, +}, map[string][]requestflag.HasOuterFlag{ + "draft": { + &requestflag.InnerFlag[string]{ + Name: "draft.text", + Usage: "Draft text. Plain text and Markdown are converted to Matrix HTML with the same rules used by send and edit.", + InnerField: "text", + }, + &requestflag.InnerFlag[map[string]any]{ + Name: "draft.attachments", + Usage: "Draft attachments keyed by attachment ID. Each attachment must reference an uploadID returned by the upload file endpoint.", + InnerField: "attachments", + }, + }, +}) + var chatsList = cli.Command{ Name: "list", Usage: "List all chats sorted by last activity (most recent first). Combines all\naccounts into a single paginated list.", @@ -110,7 +184,7 @@ var chatsArchive = cli.Command{ Flags: []cli.Flag{ &requestflag.Flag[string]{ Name: "chat-id", - Usage: "Unique identifier of the chat.", + Usage: "Chat ID. Input routes also accept the local chat ID from this Beeper Desktop installation when available.", Required: true, PathParam: "chatID", }, @@ -125,6 +199,64 @@ var chatsArchive = cli.Command{ HideHelpCommand: true, } +var chatsMarkRead = cli.Command{ + Name: "mark-read", + Usage: "Mark a chat as read, optionally through a specific message ID.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "chat-id", + Usage: "Chat ID. Input routes also accept the local chat ID from this Beeper Desktop installation when available.", + Required: true, + PathParam: "chatID", + }, + &requestflag.Flag[string]{ + Name: "message-id", + Usage: "Optional message ID to mark read through.", + BodyPath: "messageID", + }, + }, + Action: handleChatsMarkRead, + HideHelpCommand: true, +} + +var chatsMarkUnread = cli.Command{ + Name: "mark-unread", + Usage: "Mark a chat as unread, optionally from a specific message ID.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "chat-id", + Usage: "Chat ID. Input routes also accept the local chat ID from this Beeper Desktop installation when available.", + Required: true, + PathParam: "chatID", + }, + &requestflag.Flag[string]{ + Name: "message-id", + Usage: "Optional message ID to mark unread from.", + BodyPath: "messageID", + }, + }, + Action: handleChatsMarkUnread, + HideHelpCommand: true, +} + +var chatsNotifyAnyway = cli.Command{ + Name: "notify-anyway", + Usage: "Force a delivery notification when supported by the underlying network.\nCurrently intended for iMessage on macOS; unsupported networks return an error.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "chat-id", + Usage: "Chat ID. Input routes also accept the local chat ID from this Beeper Desktop installation when available.", + Required: true, + PathParam: "chatID", + }, + }, + Action: handleChatsNotifyAnyway, + HideHelpCommand: true, +} + var chatsSearch = cli.Command{ Name: "search", Usage: "Search chats by title, network, or participant names.", @@ -205,7 +337,7 @@ var chatsSearch = cli.Command{ var chatsStart = requestflag.WithInnerFlags(cli.Command{ Name: "start", - Usage: "Resolve a user/contact and open a direct chat. Reuses an existing direct chat\nwhen one is found. Available in Beeper Desktop v4.2.799+.", + Usage: "Resolve a user/contact and open a direct chat. Reuses and returns an existing\ndirect chat when one is found. Available in Beeper Desktop v4.2.808+.", Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ @@ -357,6 +489,55 @@ func handleChatsRetrieve(ctx context.Context, cmd *cli.Command) error { }) } +func handleChatsUpdate(ctx context.Context, cmd *cli.Command) error { + client := beeperdesktopapi.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("chat-id") && len(unusedArgs) > 0 { + cmd.Set("chat-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.ArrayQueryFormatRepeat, + ApplicationJSON, + false, + ) + if err != nil { + return err + } + + params := beeperdesktopapi.ChatUpdateParams{} + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Chats.Update( + ctx, + cmd.Value("chat-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: "chats update", + Transform: transform, + }) +} + func handleChatsList(ctx context.Context, cmd *cli.Command) error { client := beeperdesktopapi.NewClient(getDefaultRequestOptions(cmd)...) unusedArgs := cmd.Args().Slice() @@ -447,6 +628,153 @@ func handleChatsArchive(ctx context.Context, cmd *cli.Command) error { ) } +func handleChatsMarkRead(ctx context.Context, cmd *cli.Command) error { + client := beeperdesktopapi.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("chat-id") && len(unusedArgs) > 0 { + cmd.Set("chat-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.ArrayQueryFormatRepeat, + ApplicationJSON, + false, + ) + if err != nil { + return err + } + + params := beeperdesktopapi.ChatMarkReadParams{} + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Chats.MarkRead( + ctx, + cmd.Value("chat-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: "chats mark-read", + Transform: transform, + }) +} + +func handleChatsMarkUnread(ctx context.Context, cmd *cli.Command) error { + client := beeperdesktopapi.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("chat-id") && len(unusedArgs) > 0 { + cmd.Set("chat-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.ArrayQueryFormatRepeat, + ApplicationJSON, + false, + ) + if err != nil { + return err + } + + params := beeperdesktopapi.ChatMarkUnreadParams{} + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Chats.MarkUnread( + ctx, + cmd.Value("chat-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: "chats mark-unread", + Transform: transform, + }) +} + +func handleChatsNotifyAnyway(ctx context.Context, cmd *cli.Command) error { + client := beeperdesktopapi.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("chat-id") && len(unusedArgs) > 0 { + cmd.Set("chat-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.ArrayQueryFormatRepeat, + EmptyBody, + false, + ) + if err != nil { + return err + } + + params := beeperdesktopapi.ChatNotifyAnywayParams{} + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Chats.NotifyAnyway( + ctx, + cmd.Value("chat-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: "chats notify-anyway", + Transform: transform, + }) +} + func handleChatsSearch(ctx context.Context, cmd *cli.Command) error { client := beeperdesktopapi.NewClient(getDefaultRequestOptions(cmd)...) unusedArgs := cmd.Args().Slice() diff --git a/pkg/cmd/chat_test.go b/pkg/cmd/chat_test.go index 4e79c016..01c710b6 100644 --- a/pkg/cmd/chat_test.go +++ b/pkg/cmd/chat_test.go @@ -47,7 +47,82 @@ func TestChatsRetrieve(t *testing.T) { "--access-token", "string", "chats", "retrieve", "--chat-id", "!NCdzlIaMjZUmvmvyHU:beeper.com", - "--max-participant-count", "50", + "--max-participant-count", "100", + ) + }) +} + +func TestChatsUpdate(t *testing.T) { + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--access-token", "string", + "chats", "update", + "--chat-id", "!NCdzlIaMjZUmvmvyHU:beeper.com", + "--description", "description", + "--draft", "{text: text, attachments: {foo: {uploadID: uploadID, id: id, duration: 0, fileName: fileName, mimeType: mimeType, size: {height: 0, width: 0}, type: image}}}", + "--img-url", "imgURL", + "--is-archived=true", + "--is-low-priority=true", + "--is-muted=true", + "--is-pinned=true", + "--message-expiry-seconds", "0", + "--title", "title", + ) + }) + + t.Run("inner flags", func(t *testing.T) { + // Check that inner flags have been set up correctly + requestflag.CheckInnerFlags(chatsUpdate) + + // Alternative argument passing style using inner flags + mocktest.TestRunMockTestWithFlags( + t, + "--access-token", "string", + "chats", "update", + "--chat-id", "!NCdzlIaMjZUmvmvyHU:beeper.com", + "--description", "description", + "--draft.text", "text", + "--draft.attachments", "{foo: {uploadID: uploadID, id: id, duration: 0, fileName: fileName, mimeType: mimeType, size: {height: 0, width: 0}, type: image}}", + "--img-url", "imgURL", + "--is-archived=true", + "--is-low-priority=true", + "--is-muted=true", + "--is-pinned=true", + "--message-expiry-seconds", "0", + "--title", "title", + ) + }) + + t.Run("piping data", func(t *testing.T) { + // Test piping YAML data over stdin + pipeData := []byte("" + + "description: description\n" + + "draft:\n" + + " text: text\n" + + " attachments:\n" + + " foo:\n" + + " uploadID: uploadID\n" + + " id: id\n" + + " duration: 0\n" + + " fileName: fileName\n" + + " mimeType: mimeType\n" + + " size:\n" + + " height: 0\n" + + " width: 0\n" + + " type: image\n" + + "imgURL: imgURL\n" + + "isArchived: true\n" + + "isLowPriority: true\n" + + "isMuted: true\n" + + "isPinned: true\n" + + "messageExpirySeconds: 0\n" + + "title: title\n") + mocktest.TestRunMockTestWithPipeAndFlags( + t, pipeData, + "--access-token", "string", + "chats", "update", + "--chat-id", "!NCdzlIaMjZUmvmvyHU:beeper.com", ) }) } @@ -59,8 +134,9 @@ func TestChatsList(t *testing.T) { "--access-token", "string", "chats", "list", "--max-items", "10", + "--account-id", "matrix", + "--account-id", "discordgo", "--account-id", "local-whatsapp_ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc", - "--account-id", "local-instagram_ba_eRfQMmnSNy_p7Ih7HL7RduRpKFU", "--cursor", "1725489123456|c29tZUltc2dQYWdl", "--direction", "before", ) @@ -90,6 +166,63 @@ func TestChatsArchive(t *testing.T) { }) } +func TestChatsMarkRead(t *testing.T) { + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--access-token", "string", + "chats", "mark-read", + "--chat-id", "!NCdzlIaMjZUmvmvyHU:beeper.com", + "--message-id", "1343993", + ) + }) + + t.Run("piping data", func(t *testing.T) { + // Test piping YAML data over stdin + pipeData := []byte("messageID: '1343993'") + mocktest.TestRunMockTestWithPipeAndFlags( + t, pipeData, + "--access-token", "string", + "chats", "mark-read", + "--chat-id", "!NCdzlIaMjZUmvmvyHU:beeper.com", + ) + }) +} + +func TestChatsMarkUnread(t *testing.T) { + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--access-token", "string", + "chats", "mark-unread", + "--chat-id", "!NCdzlIaMjZUmvmvyHU:beeper.com", + "--message-id", "1343993", + ) + }) + + t.Run("piping data", func(t *testing.T) { + // Test piping YAML data over stdin + pipeData := []byte("messageID: '1343993'") + mocktest.TestRunMockTestWithPipeAndFlags( + t, pipeData, + "--access-token", "string", + "chats", "mark-unread", + "--chat-id", "!NCdzlIaMjZUmvmvyHU:beeper.com", + ) + }) +} + +func TestChatsNotifyAnyway(t *testing.T) { + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--access-token", "string", + "chats", "notify-anyway", + "--chat-id", "!NCdzlIaMjZUmvmvyHU:beeper.com", + ) + }) +} + func TestChatsSearch(t *testing.T) { t.Run("regular flags", func(t *testing.T) { mocktest.TestRunMockTestWithFlags( @@ -97,8 +230,9 @@ func TestChatsSearch(t *testing.T) { "--access-token", "string", "chats", "search", "--max-items", "10", + "--account-id", "matrix", + "--account-id", "discordgo", "--account-id", "local-whatsapp_ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc", - "--account-id", "local-telegram_ba_QFrb5lrLPhO3OT5MFBeTWv0x4BI", "--cursor", "1725489123456|c29tZUltc2dQYWdl", "--direction", "before", "--inbox", "primary", @@ -115,7 +249,6 @@ func TestChatsSearch(t *testing.T) { } func TestChatsStart(t *testing.T) { - t.Skip("Stainless mock tests currently load the project-published OpenAPI spec URL, which may not include newly-added local-only endpoints during build checks.") t.Run("regular flags", func(t *testing.T) { mocktest.TestRunMockTestWithFlags( t, diff --git a/pkg/cmd/chatmessagereaction.go b/pkg/cmd/chatmessagereaction.go index f96f9155..681f963f 100644 --- a/pkg/cmd/chatmessagereaction.go +++ b/pkg/cmd/chatmessagereaction.go @@ -21,20 +21,21 @@ var chatsMessagesReactionsDelete = cli.Command{ Flags: []cli.Flag{ &requestflag.Flag[string]{ Name: "chat-id", - Usage: "Unique identifier of the chat.", + Usage: "Chat ID. Input routes also accept the local chat ID from this Beeper Desktop installation when available.", Required: true, PathParam: "chatID", }, &requestflag.Flag[string]{ Name: "message-id", + Usage: "Message ID.", Required: true, PathParam: "messageID", }, &requestflag.Flag[string]{ Name: "reaction-key", - Usage: "Reaction key to remove", + Usage: "Reaction key to remove (emoji, shortcode, or custom emoji key)", Required: true, - QueryPath: "reactionKey", + PathParam: "reactionKey", }, }, Action: handleChatsMessagesReactionsDelete, @@ -48,12 +49,13 @@ var chatsMessagesReactionsAdd = cli.Command{ Flags: []cli.Flag{ &requestflag.Flag[string]{ Name: "chat-id", - Usage: "Unique identifier of the chat.", + Usage: "Chat ID. Input routes also accept the local chat ID from this Beeper Desktop installation when available.", Required: true, PathParam: "chatID", }, &requestflag.Flag[string]{ Name: "message-id", + Usage: "Message ID.", Required: true, PathParam: "messageID", }, @@ -65,7 +67,7 @@ var chatsMessagesReactionsAdd = cli.Command{ }, &requestflag.Flag[string]{ Name: "transaction-id", - Usage: "Optional transaction ID for deduplication and local echo tracking", + Usage: "Optional transaction ID for deduplication and send tracking", BodyPath: "transactionID", }, }, @@ -76,8 +78,8 @@ var chatsMessagesReactionsAdd = cli.Command{ func handleChatsMessagesReactionsDelete(ctx context.Context, cmd *cli.Command) error { client := beeperdesktopapi.NewClient(getDefaultRequestOptions(cmd)...) unusedArgs := cmd.Args().Slice() - if !cmd.IsSet("message-id") && len(unusedArgs) > 0 { - cmd.Set("message-id", unusedArgs[0]) + if !cmd.IsSet("reaction-key") && len(unusedArgs) > 0 { + cmd.Set("reaction-key", unusedArgs[0]) unusedArgs = unusedArgs[1:] } if len(unusedArgs) > 0 { @@ -96,14 +98,15 @@ func handleChatsMessagesReactionsDelete(ctx context.Context, cmd *cli.Command) e } params := beeperdesktopapi.ChatMessageReactionDeleteParams{ - ChatID: cmd.Value("chat-id").(string), + ChatID: cmd.Value("chat-id").(string), + MessageID: cmd.Value("message-id").(string), } var res []byte options = append(options, option.WithResponseBodyInto(&res)) _, err = client.Chats.Messages.Reactions.Delete( ctx, - cmd.Value("message-id").(string), + cmd.Value("reaction-key").(string), params, options..., ) diff --git a/pkg/cmd/chatmessagereaction_test.go b/pkg/cmd/chatmessagereaction_test.go index 74168cda..16a01574 100644 --- a/pkg/cmd/chatmessagereaction_test.go +++ b/pkg/cmd/chatmessagereaction_test.go @@ -15,7 +15,7 @@ func TestChatsMessagesReactionsDelete(t *testing.T) { "--access-token", "string", "chats:messages:reactions", "delete", "--chat-id", "!NCdzlIaMjZUmvmvyHU:beeper.com", - "--message-id", "messageID", + "--message-id", "1343993", "--reaction-key", "x", ) }) @@ -28,7 +28,7 @@ func TestChatsMessagesReactionsAdd(t *testing.T) { "--access-token", "string", "chats:messages:reactions", "add", "--chat-id", "!NCdzlIaMjZUmvmvyHU:beeper.com", - "--message-id", "messageID", + "--message-id", "1343993", "--reaction-key", "x", "--transaction-id", "transactionID", ) @@ -44,7 +44,7 @@ func TestChatsMessagesReactionsAdd(t *testing.T) { "--access-token", "string", "chats:messages:reactions", "add", "--chat-id", "!NCdzlIaMjZUmvmvyHU:beeper.com", - "--message-id", "messageID", + "--message-id", "1343993", ) }) } diff --git a/pkg/cmd/chatreminder.go b/pkg/cmd/chatreminder.go index acccdaf9..8afb7242 100644 --- a/pkg/cmd/chatreminder.go +++ b/pkg/cmd/chatreminder.go @@ -19,7 +19,7 @@ var chatsRemindersCreate = requestflag.WithInnerFlags(cli.Command{ Flags: []cli.Flag{ &requestflag.Flag[string]{ Name: "chat-id", - Usage: "Unique identifier of the chat.", + Usage: "Chat ID. Input routes also accept the local chat ID from this Beeper Desktop installation when available.", Required: true, PathParam: "chatID", }, @@ -34,10 +34,10 @@ var chatsRemindersCreate = requestflag.WithInnerFlags(cli.Command{ HideHelpCommand: true, }, map[string][]requestflag.HasOuterFlag{ "reminder": { - &requestflag.InnerFlag[float64]{ - Name: "reminder.remind-at-ms", - Usage: "Unix timestamp in milliseconds when reminder should trigger", - InnerField: "remindAtMs", + &requestflag.InnerFlag[any]{ + Name: "reminder.remind-at", + Usage: "Timestamp when the reminder should trigger.", + InnerField: "remindAt", }, &requestflag.InnerFlag[bool]{ Name: "reminder.dismiss-on-incoming-message", @@ -54,7 +54,7 @@ var chatsRemindersDelete = cli.Command{ Flags: []cli.Flag{ &requestflag.Flag[string]{ Name: "chat-id", - Usage: "Unique identifier of the chat.", + Usage: "Chat ID. Input routes also accept the local chat ID from this Beeper Desktop installation when available.", Required: true, PathParam: "chatID", }, diff --git a/pkg/cmd/chatreminder_test.go b/pkg/cmd/chatreminder_test.go index 84f08fba..9ef55ab6 100644 --- a/pkg/cmd/chatreminder_test.go +++ b/pkg/cmd/chatreminder_test.go @@ -16,7 +16,7 @@ func TestChatsRemindersCreate(t *testing.T) { "--access-token", "string", "chats:reminders", "create", "--chat-id", "!NCdzlIaMjZUmvmvyHU:beeper.com", - "--reminder", "{remindAtMs: 0, dismissOnIncomingMessage: true}", + "--reminder", "{remindAt: '2025-08-31T23:30:12.520Z', dismissOnIncomingMessage: true}", ) }) @@ -30,7 +30,7 @@ func TestChatsRemindersCreate(t *testing.T) { "--access-token", "string", "chats:reminders", "create", "--chat-id", "!NCdzlIaMjZUmvmvyHU:beeper.com", - "--reminder.remind-at-ms", "0", + "--reminder.remind-at", "2025-08-31T23:30:12.520Z", "--reminder.dismiss-on-incoming-message=true", ) }) @@ -39,7 +39,7 @@ func TestChatsRemindersCreate(t *testing.T) { // Test piping YAML data over stdin pipeData := []byte("" + "reminder:\n" + - " remindAtMs: 0\n" + + " remindAt: '2025-08-31T23:30:12.520Z'\n" + " dismissOnIncomingMessage: true\n") mocktest.TestRunMockTestWithPipeAndFlags( t, pipeData, diff --git a/pkg/cmd/cmd.go b/pkg/cmd/cmd.go index 12982357..1304eaa3 100644 --- a/pkg/cmd/cmd.go +++ b/pkg/cmd/cmd.go @@ -111,8 +111,12 @@ func init() { Commands: []*cli.Command{ &chatsCreate, &chatsRetrieve, + &chatsUpdate, &chatsList, &chatsArchive, + &chatsMarkRead, + &chatsMarkUnread, + &chatsNotifyAnyway, &chatsSearch, &chatsStart, }, @@ -140,8 +144,10 @@ func init() { Category: "API RESOURCE", Suggest: true, Commands: []*cli.Command{ + &messagesRetrieve, &messagesUpdate, &messagesList, + &messagesDelete, &messagesSearch, &messagesSend, }, diff --git a/pkg/cmd/info.go b/pkg/cmd/info.go index 57fdd222..afa35912 100644 --- a/pkg/cmd/info.go +++ b/pkg/cmd/info.go @@ -15,7 +15,7 @@ import ( var infoRetrieve = cli.Command{ Name: "retrieve", - Usage: "Returns app, platform, server, and endpoint discovery metadata for this Beeper\nDesktop instance.", + Usage: "Returns app, platform, server, endpoint discovery, OAuth, and WebSocket metadata\nfor this Beeper Desktop instance.", Suggest: true, Flags: []cli.Flag{}, Action: handleInfoRetrieve, diff --git a/pkg/cmd/message.go b/pkg/cmd/message.go index ff1413f7..57d143cb 100644 --- a/pkg/cmd/message.go +++ b/pkg/cmd/message.go @@ -14,6 +14,28 @@ import ( "github.com/urfave/cli/v3" ) +var messagesRetrieve = cli.Command{ + Name: "retrieve", + Usage: "Retrieve a message by final message ID, pendingMessageID, or Matrix event ID.\nChat ID may be a Beeper chat ID or local chat ID.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "chat-id", + Usage: "Chat ID. Input routes also accept the local chat ID from this Beeper Desktop installation when available.", + Required: true, + PathParam: "chatID", + }, + &requestflag.Flag[string]{ + Name: "message-id", + Usage: "Message ID.", + Required: true, + PathParam: "messageID", + }, + }, + Action: handleMessagesRetrieve, + HideHelpCommand: true, +} + var messagesUpdate = cli.Command{ Name: "update", Usage: "Edit the text content of an existing message. Messages with attachments cannot\nbe edited.", @@ -21,12 +43,13 @@ var messagesUpdate = cli.Command{ Flags: []cli.Flag{ &requestflag.Flag[string]{ Name: "chat-id", - Usage: "Unique identifier of the chat.", + Usage: "Chat ID. Input routes also accept the local chat ID from this Beeper Desktop installation when available.", Required: true, PathParam: "chatID", }, &requestflag.Flag[string]{ Name: "message-id", + Usage: "Message ID.", Required: true, PathParam: "messageID", }, @@ -48,7 +71,7 @@ var messagesList = cli.Command{ Flags: []cli.Flag{ &requestflag.Flag[string]{ Name: "chat-id", - Usage: "Unique identifier of the chat.", + Usage: "Chat ID. Input routes also accept the local chat ID from this Beeper Desktop installation when available.", Required: true, PathParam: "chatID", }, @@ -71,6 +94,34 @@ var messagesList = cli.Command{ HideHelpCommand: true, } +var messagesDelete = cli.Command{ + Name: "delete", + Usage: "Delete a message by final message ID. Pending message IDs are not accepted\nbecause messages cannot be deleted while sending.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "chat-id", + Usage: "Chat ID. Input routes also accept the local chat ID from this Beeper Desktop installation when available.", + Required: true, + PathParam: "chatID", + }, + &requestflag.Flag[string]{ + Name: "message-id", + Usage: "Message ID.", + Required: true, + PathParam: "messageID", + }, + &requestflag.Flag[*bool]{ + Name: "for-everyone", + Usage: "True to request deletion for everyone when the network supports it; false to delete only for the authenticated user when supported.", + Default: requestflag.Ptr[bool](true), + QueryPath: "forEveryone", + }, + }, + Action: handleMessagesDelete, + HideHelpCommand: true, +} + var messagesSearch = cli.Command{ Name: "search", Usage: "Search messages across chats.", @@ -160,7 +211,7 @@ var messagesSend = requestflag.WithInnerFlags(cli.Command{ Flags: []cli.Flag{ &requestflag.Flag[string]{ Name: "chat-id", - Usage: "Unique identifier of the chat.", + Usage: "Chat ID. Input routes also accept the local chat ID from this Beeper Desktop installation when available.", Required: true, PathParam: "chatID", }, @@ -176,7 +227,7 @@ var messagesSend = requestflag.WithInnerFlags(cli.Command{ }, &requestflag.Flag[string]{ Name: "text", - Usage: "Text content of the message you want to send. You may use markdown.", + Usage: "Draft text. Plain text and Markdown are converted to Matrix HTML with the same rules used by send and edit.", BodyPath: "text", }, }, @@ -211,12 +262,66 @@ var messagesSend = requestflag.WithInnerFlags(cli.Command{ }, &requestflag.InnerFlag[string]{ Name: "attachment.type", - Usage: "Special attachment type (gif, voiceNote, sticker). If omitted, auto-detected from mimeType", + Usage: "Attachment type hint (image, video, audio, file, gif, voice-note, sticker). If omitted, auto-detected from mimeType", InnerField: "type", }, }, }) +func handleMessagesRetrieve(ctx context.Context, cmd *cli.Command) error { + client := beeperdesktopapi.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.ArrayQueryFormatRepeat, + EmptyBody, + false, + ) + if err != nil { + return err + } + + params := beeperdesktopapi.MessageGetParams{ + ChatID: cmd.Value("chat-id").(string), + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Messages.Get( + ctx, + cmd.Value("message-id").(string), + params, + options..., + ) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := "json" + explicitFormat := cmd.Root().IsSet("format") + if explicitFormat { + format = cmd.Root().String("format") + } + transform := cmd.Root().String("transform") + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "messages retrieve", + Transform: transform, + }) +} + func handleMessagesUpdate(ctx context.Context, cmd *cli.Command) error { client := beeperdesktopapi.NewClient(getDefaultRequestOptions(cmd)...) unusedArgs := cmd.Args().Slice() @@ -339,6 +444,40 @@ func handleMessagesList(ctx context.Context, cmd *cli.Command) error { } } +func handleMessagesDelete(ctx context.Context, cmd *cli.Command) error { + client := beeperdesktopapi.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.ArrayQueryFormatRepeat, + EmptyBody, + false, + ) + if err != nil { + return err + } + + params := beeperdesktopapi.MessageDeleteParams{ + ChatID: cmd.Value("chat-id").(string), + } + + return client.Messages.Delete( + ctx, + cmd.Value("message-id").(string), + params, + options..., + ) +} + func handleMessagesSearch(ctx context.Context, cmd *cli.Command) error { client := beeperdesktopapi.NewClient(getDefaultRequestOptions(cmd)...) unusedArgs := cmd.Args().Slice() diff --git a/pkg/cmd/message_test.go b/pkg/cmd/message_test.go index 11fe9a29..913046b1 100644 --- a/pkg/cmd/message_test.go +++ b/pkg/cmd/message_test.go @@ -9,6 +9,18 @@ import ( "github.com/beeper/desktop-api-cli/internal/requestflag" ) +func TestMessagesRetrieve(t *testing.T) { + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--access-token", "string", + "messages", "retrieve", + "--chat-id", "!NCdzlIaMjZUmvmvyHU:beeper.com", + "--message-id", "1343993", + ) + }) +} + func TestMessagesUpdate(t *testing.T) { t.Run("regular flags", func(t *testing.T) { mocktest.TestRunMockTestWithFlags( @@ -16,7 +28,7 @@ func TestMessagesUpdate(t *testing.T) { "--access-token", "string", "messages", "update", "--chat-id", "!NCdzlIaMjZUmvmvyHU:beeper.com", - "--message-id", "messageID", + "--message-id", "1343993", "--text", "x", ) }) @@ -29,7 +41,7 @@ func TestMessagesUpdate(t *testing.T) { "--access-token", "string", "messages", "update", "--chat-id", "!NCdzlIaMjZUmvmvyHU:beeper.com", - "--message-id", "messageID", + "--message-id", "1343993", ) }) } @@ -48,6 +60,19 @@ func TestMessagesList(t *testing.T) { }) } +func TestMessagesDelete(t *testing.T) { + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--access-token", "string", + "messages", "delete", + "--chat-id", "!NCdzlIaMjZUmvmvyHU:beeper.com", + "--message-id", "1343993", + "--for-everyone=true", + ) + }) +} + func TestMessagesSearch(t *testing.T) { t.Run("regular flags", func(t *testing.T) { mocktest.TestRunMockTestWithFlags( @@ -55,8 +80,9 @@ func TestMessagesSearch(t *testing.T) { "--access-token", "string", "messages", "search", "--max-items", "10", + "--account-id", "matrix", + "--account-id", "discordgo", "--account-id", "local-whatsapp_ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc", - "--account-id", "local-instagram_ba_eRfQMmnSNy_p7Ih7HL7RduRpKFU", "--chat-id", "!NCdzlIaMjZUmvmvyHU:beeper.com", "--chat-id", "1231073", "--chat-type", "group", @@ -81,7 +107,7 @@ func TestMessagesSend(t *testing.T) { "--access-token", "string", "messages", "send", "--chat-id", "!NCdzlIaMjZUmvmvyHU:beeper.com", - "--attachment", "{uploadID: uploadID, duration: 0, fileName: fileName, mimeType: mimeType, size: {height: 0, width: 0}, type: gif}", + "--attachment", "{uploadID: uploadID, duration: 0, fileName: fileName, mimeType: mimeType, size: {height: 0, width: 0}, type: image}", "--reply-to-message-id", "replyToMessageID", "--text", "text", ) @@ -102,7 +128,7 @@ func TestMessagesSend(t *testing.T) { "--attachment.file-name", "fileName", "--attachment.mime-type", "mimeType", "--attachment.size", "{height: 0, width: 0}", - "--attachment.type", "gif", + "--attachment.type", "image", "--reply-to-message-id", "replyToMessageID", "--text", "text", ) @@ -119,7 +145,7 @@ func TestMessagesSend(t *testing.T) { " size:\n" + " height: 0\n" + " width: 0\n" + - " type: gif\n" + + " type: image\n" + "replyToMessageID: replyToMessageID\n" + "text: text\n") mocktest.TestRunMockTestWithPipeAndFlags( From ec112ec91ee336d835177ab87406e72ad585966e Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 7 May 2026 03:18:24 +0000 Subject: [PATCH 47/54] chore(internal): codegen related update --- .github/workflows/ci.yml | 8 ++++---- cmd/beeper-desktop-cli/main.go | 2 +- go.mod | 2 +- go.sum | 4 ++-- pkg/cmd/account.go | 4 ++-- pkg/cmd/accountcontact.go | 4 ++-- pkg/cmd/asset.go | 4 ++-- pkg/cmd/beeperdesktopapi.go | 4 ++-- pkg/cmd/chat.go | 4 ++-- pkg/cmd/chatmessagereaction.go | 4 ++-- pkg/cmd/chatreminder.go | 2 +- pkg/cmd/cmdutil.go | 2 +- pkg/cmd/flagoptions.go | 2 +- pkg/cmd/info.go | 4 ++-- pkg/cmd/message.go | 4 ++-- scripts/build | 2 +- scripts/link | 4 ++-- scripts/lint | 2 +- scripts/run | 2 +- scripts/test | 2 +- scripts/unlink | 2 +- 21 files changed, 34 insertions(+), 34 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 940785a8..a9d361d1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,7 +15,7 @@ on: - 'stl-preview-base/**' env: - GOPRIVATE: github.com/beeper/desktop-api-go,github.com/stainless-sdks/beeper-desktop-api-go + GOPRIVATE: github.com/beeper/desktop-api-go/v5,github.com/stainless-sdks/beeper-desktop-api-go/v5 jobs: lint: @@ -34,7 +34,7 @@ jobs: - name: Link staging branch if: github.repository == 'stainless-sdks/beeper-desktop-api-cli' run: | - ./scripts/link 'github.com/stainless-sdks/beeper-desktop-api-go@${{ github.ref_name }}' || true + ./scripts/link 'github.com/stainless-sdks/beeper-desktop-api-go/v5@${{ github.ref_name }}' || true - name: Bootstrap run: ./scripts/bootstrap @@ -60,7 +60,7 @@ jobs: - name: Link staging branch if: github.repository == 'stainless-sdks/beeper-desktop-api-cli' run: | - ./scripts/link 'github.com/stainless-sdks/beeper-desktop-api-go@${{ github.ref_name }}' || true + ./scripts/link 'github.com/stainless-sdks/beeper-desktop-api-go/v5@${{ github.ref_name }}' || true - name: Bootstrap run: ./scripts/bootstrap @@ -107,7 +107,7 @@ jobs: - name: Link staging branch if: github.repository == 'stainless-sdks/beeper-desktop-api-cli' run: | - ./scripts/link 'github.com/stainless-sdks/beeper-desktop-api-go@${{ github.ref_name }}' || true + ./scripts/link 'github.com/stainless-sdks/beeper-desktop-api-go/v5@${{ github.ref_name }}' || true - name: Bootstrap run: ./scripts/bootstrap diff --git a/cmd/beeper-desktop-cli/main.go b/cmd/beeper-desktop-cli/main.go index 8640e9d6..189bd39f 100644 --- a/cmd/beeper-desktop-cli/main.go +++ b/cmd/beeper-desktop-cli/main.go @@ -11,7 +11,7 @@ import ( "slices" "github.com/beeper/desktop-api-cli/pkg/cmd" - "github.com/beeper/desktop-api-go" + "github.com/beeper/desktop-api-go/v5" "github.com/tidwall/gjson" "github.com/urfave/cli/v3" ) diff --git a/go.mod b/go.mod index 4d423435..f23b9d79 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/beeper/desktop-api-cli go 1.25 require ( - github.com/beeper/desktop-api-go v0.5.0 + github.com/beeper/desktop-api-go/v5 v5.0.0 github.com/charmbracelet/bubbles v0.21.0 github.com/charmbracelet/bubbletea v1.3.6 github.com/charmbracelet/lipgloss v1.1.0 diff --git a/go.sum b/go.sum index c6fed8fd..57a38afa 100644 --- a/go.sum +++ b/go.sum @@ -2,8 +2,8 @@ github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiE github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8= github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= -github.com/beeper/desktop-api-go v0.5.0 h1:0Myrz8eop5dC3/QseUrbYVIyWkHPGLyU47/lffw/kT4= -github.com/beeper/desktop-api-go v0.5.0/go.mod h1:y9Mk83OdQWo6ldLTcPyaUPrwjkmvy/3QkhHqZLhU/mA= +github.com/beeper/desktop-api-go/v5 v5.0.0 h1:kKL/HNGnGS9K3fjM3+f7TTmiA5KeWO3ZusG42AXRAeo= +github.com/beeper/desktop-api-go/v5 v5.0.0/go.mod h1:WUEtzxs0a2AhNYPlLy2Ip3SEXV9w9h1o3M4zz/qXfi0= github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs= github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg= github.com/charmbracelet/bubbletea v1.3.6 h1:VkHIxPJQeDt0aFJIsVxw8BQdh/F/L2KKZGsK6et5taU= diff --git a/pkg/cmd/account.go b/pkg/cmd/account.go index 382b5fcc..284208c4 100644 --- a/pkg/cmd/account.go +++ b/pkg/cmd/account.go @@ -7,8 +7,8 @@ import ( "fmt" "github.com/beeper/desktop-api-cli/internal/apiquery" - "github.com/beeper/desktop-api-go" - "github.com/beeper/desktop-api-go/option" + "github.com/beeper/desktop-api-go/v5" + "github.com/beeper/desktop-api-go/v5/option" "github.com/tidwall/gjson" "github.com/urfave/cli/v3" ) diff --git a/pkg/cmd/accountcontact.go b/pkg/cmd/accountcontact.go index e6265816..40e36b64 100644 --- a/pkg/cmd/accountcontact.go +++ b/pkg/cmd/accountcontact.go @@ -8,8 +8,8 @@ import ( "github.com/beeper/desktop-api-cli/internal/apiquery" "github.com/beeper/desktop-api-cli/internal/requestflag" - "github.com/beeper/desktop-api-go" - "github.com/beeper/desktop-api-go/option" + "github.com/beeper/desktop-api-go/v5" + "github.com/beeper/desktop-api-go/v5/option" "github.com/tidwall/gjson" "github.com/urfave/cli/v3" ) diff --git a/pkg/cmd/asset.go b/pkg/cmd/asset.go index 959b2fb9..2222a2c7 100644 --- a/pkg/cmd/asset.go +++ b/pkg/cmd/asset.go @@ -9,8 +9,8 @@ import ( "github.com/beeper/desktop-api-cli/internal/apiquery" "github.com/beeper/desktop-api-cli/internal/requestflag" - "github.com/beeper/desktop-api-go" - "github.com/beeper/desktop-api-go/option" + "github.com/beeper/desktop-api-go/v5" + "github.com/beeper/desktop-api-go/v5/option" "github.com/tidwall/gjson" "github.com/urfave/cli/v3" ) diff --git a/pkg/cmd/beeperdesktopapi.go b/pkg/cmd/beeperdesktopapi.go index c5757106..88d2583f 100644 --- a/pkg/cmd/beeperdesktopapi.go +++ b/pkg/cmd/beeperdesktopapi.go @@ -8,8 +8,8 @@ import ( "github.com/beeper/desktop-api-cli/internal/apiquery" "github.com/beeper/desktop-api-cli/internal/requestflag" - "github.com/beeper/desktop-api-go" - "github.com/beeper/desktop-api-go/option" + "github.com/beeper/desktop-api-go/v5" + "github.com/beeper/desktop-api-go/v5/option" "github.com/tidwall/gjson" "github.com/urfave/cli/v3" ) diff --git a/pkg/cmd/chat.go b/pkg/cmd/chat.go index 7b73175b..4b22dc00 100644 --- a/pkg/cmd/chat.go +++ b/pkg/cmd/chat.go @@ -8,8 +8,8 @@ import ( "github.com/beeper/desktop-api-cli/internal/apiquery" "github.com/beeper/desktop-api-cli/internal/requestflag" - "github.com/beeper/desktop-api-go" - "github.com/beeper/desktop-api-go/option" + "github.com/beeper/desktop-api-go/v5" + "github.com/beeper/desktop-api-go/v5/option" "github.com/tidwall/gjson" "github.com/urfave/cli/v3" ) diff --git a/pkg/cmd/chatmessagereaction.go b/pkg/cmd/chatmessagereaction.go index 681f963f..e1976b90 100644 --- a/pkg/cmd/chatmessagereaction.go +++ b/pkg/cmd/chatmessagereaction.go @@ -8,8 +8,8 @@ import ( "github.com/beeper/desktop-api-cli/internal/apiquery" "github.com/beeper/desktop-api-cli/internal/requestflag" - "github.com/beeper/desktop-api-go" - "github.com/beeper/desktop-api-go/option" + "github.com/beeper/desktop-api-go/v5" + "github.com/beeper/desktop-api-go/v5/option" "github.com/tidwall/gjson" "github.com/urfave/cli/v3" ) diff --git a/pkg/cmd/chatreminder.go b/pkg/cmd/chatreminder.go index 8afb7242..535ee8aa 100644 --- a/pkg/cmd/chatreminder.go +++ b/pkg/cmd/chatreminder.go @@ -8,7 +8,7 @@ import ( "github.com/beeper/desktop-api-cli/internal/apiquery" "github.com/beeper/desktop-api-cli/internal/requestflag" - "github.com/beeper/desktop-api-go" + "github.com/beeper/desktop-api-go/v5" "github.com/urfave/cli/v3" ) diff --git a/pkg/cmd/cmdutil.go b/pkg/cmd/cmdutil.go index c98055ae..f36172c0 100644 --- a/pkg/cmd/cmdutil.go +++ b/pkg/cmd/cmdutil.go @@ -17,7 +17,7 @@ import ( "syscall" "github.com/beeper/desktop-api-cli/internal/jsonview" - "github.com/beeper/desktop-api-go/option" + "github.com/beeper/desktop-api-go/v5/option" "github.com/charmbracelet/x/term" "github.com/itchyny/json2yaml" diff --git a/pkg/cmd/flagoptions.go b/pkg/cmd/flagoptions.go index 9452fd02..2d9c0a52 100644 --- a/pkg/cmd/flagoptions.go +++ b/pkg/cmd/flagoptions.go @@ -20,7 +20,7 @@ import ( "github.com/beeper/desktop-api-cli/internal/apiquery" "github.com/beeper/desktop-api-cli/internal/debugmiddleware" "github.com/beeper/desktop-api-cli/internal/requestflag" - "github.com/beeper/desktop-api-go/option" + "github.com/beeper/desktop-api-go/v5/option" "github.com/goccy/go-yaml" "github.com/urfave/cli/v3" diff --git a/pkg/cmd/info.go b/pkg/cmd/info.go index afa35912..fa4ea4de 100644 --- a/pkg/cmd/info.go +++ b/pkg/cmd/info.go @@ -7,8 +7,8 @@ import ( "fmt" "github.com/beeper/desktop-api-cli/internal/apiquery" - "github.com/beeper/desktop-api-go" - "github.com/beeper/desktop-api-go/option" + "github.com/beeper/desktop-api-go/v5" + "github.com/beeper/desktop-api-go/v5/option" "github.com/tidwall/gjson" "github.com/urfave/cli/v3" ) diff --git a/pkg/cmd/message.go b/pkg/cmd/message.go index 57d143cb..3b9b74e2 100644 --- a/pkg/cmd/message.go +++ b/pkg/cmd/message.go @@ -8,8 +8,8 @@ import ( "github.com/beeper/desktop-api-cli/internal/apiquery" "github.com/beeper/desktop-api-cli/internal/requestflag" - "github.com/beeper/desktop-api-go" - "github.com/beeper/desktop-api-go/option" + "github.com/beeper/desktop-api-go/v5" + "github.com/beeper/desktop-api-go/v5/option" "github.com/tidwall/gjson" "github.com/urfave/cli/v3" ) diff --git a/scripts/build b/scripts/build index 7eb0308a..d2675777 100755 --- a/scripts/build +++ b/scripts/build @@ -5,7 +5,7 @@ set -euo pipefail cd "$(dirname "$0")/.." # Mark the necessary Go modules as private to avoid Go's proxy -export GOPRIVATE="${GOPRIVATE:+$GOPRIVATE,}github.com/beeper/desktop-api-go,github.com/stainless-sdks/beeper-desktop-api-go" +export GOPRIVATE="${GOPRIVATE:+$GOPRIVATE,}github.com/beeper/desktop-api-go/v5,github.com/stainless-sdks/beeper-desktop-api-go/v5" echo "==> Building beeper-desktop-cli" go build ./cmd/beeper-desktop-cli diff --git a/scripts/link b/scripts/link index 332584c6..60c6042f 100755 --- a/scripts/link +++ b/scripts/link @@ -5,12 +5,12 @@ set -euo pipefail cd "$(dirname "$0")/.." # Mark the necessary Go modules as private to avoid Go's proxy -export GOPRIVATE="${GOPRIVATE:+$GOPRIVATE,}github.com/beeper/desktop-api-go,github.com/stainless-sdks/beeper-desktop-api-go" +export GOPRIVATE="${GOPRIVATE:+$GOPRIVATE,}github.com/beeper/desktop-api-go/v5,github.com/stainless-sdks/beeper-desktop-api-go/v5" REPLACEMENT="${1:-"../beeperdesktop-go"}" echo "==> Replacing Go SDK with $REPLACEMENT" if [[ -d "$REPLACEMENT" ]] || go list -m "$REPLACEMENT" >/dev/null; then - go mod edit -replace github.com/beeper/desktop-api-go="$REPLACEMENT" + go mod edit -replace github.com/beeper/desktop-api-go/v5="$REPLACEMENT" go mod tidy -e else echo "Skipping Go SDK replacement (branch may not exist on Go SDK)" diff --git a/scripts/lint b/scripts/lint index bfcb7b67..8a18b1dc 100755 --- a/scripts/lint +++ b/scripts/lint @@ -5,7 +5,7 @@ set -euo pipefail cd "$(dirname "$0")/.." # Mark the necessary Go modules as private to avoid Go's proxy -export GOPRIVATE="${GOPRIVATE:+$GOPRIVATE,}github.com/beeper/desktop-api-go,github.com/stainless-sdks/beeper-desktop-api-go" +export GOPRIVATE="${GOPRIVATE:+$GOPRIVATE,}github.com/beeper/desktop-api-go/v5,github.com/stainless-sdks/beeper-desktop-api-go/v5" echo "==> Running Go build" go build ./... diff --git a/scripts/run b/scripts/run index 5cfec5f7..cf548690 100755 --- a/scripts/run +++ b/scripts/run @@ -5,6 +5,6 @@ set -euo pipefail cd "$(dirname "$0")/.." # Mark the necessary Go modules as private to avoid Go's proxy -export GOPRIVATE="${GOPRIVATE:+$GOPRIVATE,}github.com/beeper/desktop-api-go,github.com/stainless-sdks/beeper-desktop-api-go" +export GOPRIVATE="${GOPRIVATE:+$GOPRIVATE,}github.com/beeper/desktop-api-go/v5,github.com/stainless-sdks/beeper-desktop-api-go/v5" go run ./cmd/beeper-desktop-cli "$@" diff --git a/scripts/test b/scripts/test index e2baecad..95e0bdee 100755 --- a/scripts/test +++ b/scripts/test @@ -5,7 +5,7 @@ set -euo pipefail cd "$(dirname "$0")/.." # Mark the necessary Go modules as private to avoid Go's proxy -export GOPRIVATE="${GOPRIVATE:+$GOPRIVATE,}github.com/beeper/desktop-api-go,github.com/stainless-sdks/beeper-desktop-api-go" +export GOPRIVATE="${GOPRIVATE:+$GOPRIVATE,}github.com/beeper/desktop-api-go/v5,github.com/stainless-sdks/beeper-desktop-api-go/v5" RED='\033[0;31m' GREEN='\033[0;32m' diff --git a/scripts/unlink b/scripts/unlink index dfb62ae3..9ba3798d 100755 --- a/scripts/unlink +++ b/scripts/unlink @@ -5,4 +5,4 @@ set -e cd "$(dirname "$0")/.." echo "==> Unlinking with local directory" -go mod edit -dropreplace github.com/beeper/desktop-api-go +go mod edit -dropreplace github.com/beeper/desktop-api-go/v5 From 3b786e24f0f0b206fc0817fa1fc80fa3b084346a Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 8 May 2026 03:42:53 +0000 Subject: [PATCH 48/54] chore: redact api-key headers in debug logs --- internal/debugmiddleware/debug_middleware.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/internal/debugmiddleware/debug_middleware.go b/internal/debugmiddleware/debug_middleware.go index f07b93be..647f1ded 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 { From 7d88c2d5073569f59934a3b1d261e4c404531e0f Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 13 May 2026 02:40:49 +0000 Subject: [PATCH 49/54] ci: pin GitHub Actions to commit SHAs Pin all GitHub Actions referenced in generated workflows (both first-party `actions/*` and third-party) to immutable commit SHAs. Updating pinned actions is now a deliberate codegen-side bump rather than implicit on every workflow run. --- .github/actions/setup-go/action.yml | 4 ++-- .github/workflows/ci.yml | 10 +++++----- .github/workflows/publish-release.yml | 6 +++--- .github/workflows/release-doctor.yml | 2 +- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/actions/setup-go/action.yml b/.github/actions/setup-go/action.yml index 480f40f2..4210aa62 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/beeper-desktop-api-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/beeper-desktop-api-go".insteadOf "https://github.com/stainless-sdks/beeper-desktop-api-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 a9d361d1..54838a51 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,7 +25,7 @@ jobs: 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: @@ -51,7 +51,7 @@ jobs: runs-on: ${{ github.repository == 'stainless-sdks/beeper-desktop-api-cli' && '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/beeper-desktop-api-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()); @@ -98,7 +98,7 @@ jobs: runs-on: ${{ github.repository == 'stainless-sdks/beeper-desktop-api-cli' && '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 b2076972..058282ae 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -16,15 +16,15 @@ jobs: 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 8f8d88b5..bd60cc62 100644 --- a/.github/workflows/release-doctor.yml +++ b/.github/workflows/release-doctor.yml @@ -12,7 +12,7 @@ jobs: if: github.repository == 'beeper/desktop-api-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: | From 387f5ad6a8e720dc43dfef4a63d52c7cb171003a Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 16 May 2026 19:57:02 +0000 Subject: [PATCH 50/54] Update Desktop API SDKs --- .stats.yml | 8 +- pkg/cmd/app.go | 65 +++ pkg/cmd/app_test.go | 19 + pkg/cmd/appe2eerecoverycode.go | 120 +++++ pkg/cmd/appe2eerecoverycode_test.go | 40 ++ pkg/cmd/appe2eerecoverycodereset.go | 128 +++++ pkg/cmd/appe2eerecoverycodereset_test.go | 51 ++ pkg/cmd/appe2eeverification.go | 204 ++++++++ pkg/cmd/appe2eeverification_test.go | 67 +++ pkg/cmd/appe2eeverificationqr.go | 130 +++++ pkg/cmd/appe2eeverificationqr_test.go | 41 ++ pkg/cmd/appe2eeverificationsa.go | 131 +++++ pkg/cmd/appe2eeverificationsa_test.go | 31 ++ pkg/cmd/applogin.go | 264 ++++++++++ pkg/cmd/applogin_test.go | 95 ++++ pkg/cmd/bridge.go | 65 +++ pkg/cmd/bridge_test.go | 19 + pkg/cmd/cmd.go | 175 +++++++ pkg/cmd/matrixbridgeauth.go | 584 +++++++++++++++++++++++ pkg/cmd/matrixbridgeauth_test.go | 134 ++++++ pkg/cmd/matrixbridgecapability.go | 75 +++ pkg/cmd/matrixbridgecapability_test.go | 20 + pkg/cmd/matrixbridgecontact.go | 87 ++++ pkg/cmd/matrixbridgecontact_test.go | 21 + pkg/cmd/matrixbridgeroom.go | 240 ++++++++++ pkg/cmd/matrixbridgeroom_test.go | 98 ++++ pkg/cmd/matrixbridgeuser.go | 168 +++++++ pkg/cmd/matrixbridgeuser_test.go | 47 ++ pkg/cmd/matrixroom.go | 337 +++++++++++++ pkg/cmd/matrixroom_test.go | 168 +++++++ pkg/cmd/matrixroomaccountdata.go | 177 +++++++ pkg/cmd/matrixroomaccountdata_test.go | 49 ++ pkg/cmd/matrixroomevent.go | 89 ++++ pkg/cmd/matrixroomevent_test.go | 21 + pkg/cmd/matrixroomstate.go | 160 +++++++ pkg/cmd/matrixroomstate_test.go | 34 ++ pkg/cmd/matrixuser.go | 75 +++ pkg/cmd/matrixuser_test.go | 20 + pkg/cmd/matrixuseraccountdata.go | 165 +++++++ pkg/cmd/matrixuseraccountdata_test.go | 46 ++ 40 files changed, 4464 insertions(+), 4 deletions(-) create mode 100644 pkg/cmd/app.go create mode 100644 pkg/cmd/app_test.go create mode 100644 pkg/cmd/appe2eerecoverycode.go create mode 100644 pkg/cmd/appe2eerecoverycode_test.go create mode 100644 pkg/cmd/appe2eerecoverycodereset.go create mode 100644 pkg/cmd/appe2eerecoverycodereset_test.go create mode 100644 pkg/cmd/appe2eeverification.go create mode 100644 pkg/cmd/appe2eeverification_test.go create mode 100644 pkg/cmd/appe2eeverificationqr.go create mode 100644 pkg/cmd/appe2eeverificationqr_test.go create mode 100644 pkg/cmd/appe2eeverificationsa.go create mode 100644 pkg/cmd/appe2eeverificationsa_test.go create mode 100644 pkg/cmd/applogin.go create mode 100644 pkg/cmd/applogin_test.go create mode 100644 pkg/cmd/bridge.go create mode 100644 pkg/cmd/bridge_test.go create mode 100644 pkg/cmd/matrixbridgeauth.go create mode 100644 pkg/cmd/matrixbridgeauth_test.go create mode 100644 pkg/cmd/matrixbridgecapability.go create mode 100644 pkg/cmd/matrixbridgecapability_test.go create mode 100644 pkg/cmd/matrixbridgecontact.go create mode 100644 pkg/cmd/matrixbridgecontact_test.go create mode 100644 pkg/cmd/matrixbridgeroom.go create mode 100644 pkg/cmd/matrixbridgeroom_test.go create mode 100644 pkg/cmd/matrixbridgeuser.go create mode 100644 pkg/cmd/matrixbridgeuser_test.go create mode 100644 pkg/cmd/matrixroom.go create mode 100644 pkg/cmd/matrixroom_test.go create mode 100644 pkg/cmd/matrixroomaccountdata.go create mode 100644 pkg/cmd/matrixroomaccountdata_test.go create mode 100644 pkg/cmd/matrixroomevent.go create mode 100644 pkg/cmd/matrixroomevent_test.go create mode 100644 pkg/cmd/matrixroomstate.go create mode 100644 pkg/cmd/matrixroomstate_test.go create mode 100644 pkg/cmd/matrixuser.go create mode 100644 pkg/cmd/matrixuser_test.go create mode 100644 pkg/cmd/matrixuseraccountdata.go create mode 100644 pkg/cmd/matrixuseraccountdata_test.go diff --git a/.stats.yml b/.stats.yml index 2dd3fee6..b63d20d3 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 30 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper/beeper-desktop-api-c08c14bb754b4cb0e02b21fabb680469368286be339dec0aaa8c69d04a1f021a.yml -openapi_spec_hash: a10246aaf7cdc33b682fc245bd5f893b -config_hash: 72f9d43b9b51a5da912e9f3730e53ae2 +configured_endpoints: 72 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper/beeper-desktop-api-de1370e6a3183044fa135a886d2ee8f779d5e86228cdbd503d553b4c13cc7cbe.yml +openapi_spec_hash: 30b435d7585d8b6951610e7147369779 +config_hash: 683b13ea6fb6aa9d6b1b8814cca24f1c diff --git a/pkg/cmd/app.go b/pkg/cmd/app.go new file mode 100644 index 00000000..aa20f30c --- /dev/null +++ b/pkg/cmd/app.go @@ -0,0 +1,65 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "context" + "fmt" + + "github.com/beeper/desktop-api-cli/internal/apiquery" + "github.com/beeper/desktop-api-go/v5" + "github.com/beeper/desktop-api-go/v5/option" + "github.com/tidwall/gjson" + "github.com/urfave/cli/v3" +) + +var appStatus = cli.Command{ + Name: "status", + Usage: "Return the current Beeper Desktop sign-in and encrypted messaging setup state.\nThis endpoint is public before sign-in so apps can discover that login is\nneeded; after sign-in, pass a read token.", + Suggest: true, + Flags: []cli.Flag{}, + Action: handleAppStatus, + HideHelpCommand: true, +} + +func handleAppStatus(ctx context.Context, cmd *cli.Command) error { + client := beeperdesktopapi.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.ArrayQueryFormatRepeat, + EmptyBody, + false, + ) + if err != nil { + return err + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.App.Status(ctx, options...) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := "json" + explicitFormat := cmd.Root().IsSet("format") + if explicitFormat { + format = cmd.Root().String("format") + } + transform := cmd.Root().String("transform") + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "app status", + Transform: transform, + }) +} diff --git a/pkg/cmd/app_test.go b/pkg/cmd/app_test.go new file mode 100644 index 00000000..214d3086 --- /dev/null +++ b/pkg/cmd/app_test.go @@ -0,0 +1,19 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "testing" + + "github.com/beeper/desktop-api-cli/internal/mocktest" +) + +func TestAppStatus(t *testing.T) { + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--access-token", "string", + "app", "status", + ) + }) +} diff --git a/pkg/cmd/appe2eerecoverycode.go b/pkg/cmd/appe2eerecoverycode.go new file mode 100644 index 00000000..6279a233 --- /dev/null +++ b/pkg/cmd/appe2eerecoverycode.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/beeper/desktop-api-cli/internal/apiquery" + "github.com/beeper/desktop-api-cli/internal/requestflag" + "github.com/beeper/desktop-api-go/v5" + "github.com/beeper/desktop-api-go/v5/option" + "github.com/tidwall/gjson" + "github.com/urfave/cli/v3" +) + +var appE2eeRecoveryCodeMarkBackedUp = cli.Command{ + Name: "mark-backed-up", + Usage: "Record that the user saved their recovery key.", + Suggest: true, + Flags: []cli.Flag{}, + Action: handleAppE2eeRecoveryCodeMarkBackedUp, + HideHelpCommand: true, +} + +var appE2eeRecoveryCodeVerify = cli.Command{ + Name: "verify", + Usage: "Unlock encrypted messages with the user recovery key.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "recovery-code", + Usage: "Recovery key saved by the user.", + Required: true, + BodyPath: "recoveryCode", + }, + }, + Action: handleAppE2eeRecoveryCodeVerify, + HideHelpCommand: true, +} + +func handleAppE2eeRecoveryCodeMarkBackedUp(ctx context.Context, cmd *cli.Command) error { + client := beeperdesktopapi.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.ArrayQueryFormatRepeat, + EmptyBody, + false, + ) + if err != nil { + return err + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.App.E2ee.RecoveryCode.MarkBackedUp(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: "app:e2ee:recovery-code mark-backed-up", + Transform: transform, + }) +} + +func handleAppE2eeRecoveryCodeVerify(ctx context.Context, cmd *cli.Command) error { + client := beeperdesktopapi.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.ArrayQueryFormatRepeat, + ApplicationJSON, + false, + ) + if err != nil { + return err + } + + params := beeperdesktopapi.AppE2eeRecoveryCodeVerifyParams{} + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.App.E2ee.RecoveryCode.Verify(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: "app:e2ee:recovery-code verify", + Transform: transform, + }) +} diff --git a/pkg/cmd/appe2eerecoverycode_test.go b/pkg/cmd/appe2eerecoverycode_test.go new file mode 100644 index 00000000..c460860a --- /dev/null +++ b/pkg/cmd/appe2eerecoverycode_test.go @@ -0,0 +1,40 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "testing" + + "github.com/beeper/desktop-api-cli/internal/mocktest" +) + +func TestAppE2eeRecoveryCodeMarkBackedUp(t *testing.T) { + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--access-token", "string", + "app:e2ee:recovery-code", "mark-backed-up", + ) + }) +} + +func TestAppE2eeRecoveryCodeVerify(t *testing.T) { + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--access-token", "string", + "app:e2ee:recovery-code", "verify", + "--recovery-code", "x", + ) + }) + + t.Run("piping data", func(t *testing.T) { + // Test piping YAML data over stdin + pipeData := []byte("recoveryCode: x") + mocktest.TestRunMockTestWithPipeAndFlags( + t, pipeData, + "--access-token", "string", + "app:e2ee:recovery-code", "verify", + ) + }) +} diff --git a/pkg/cmd/appe2eerecoverycodereset.go b/pkg/cmd/appe2eerecoverycodereset.go new file mode 100644 index 00000000..1226ae86 --- /dev/null +++ b/pkg/cmd/appe2eerecoverycodereset.go @@ -0,0 +1,128 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "context" + "fmt" + + "github.com/beeper/desktop-api-cli/internal/apiquery" + "github.com/beeper/desktop-api-cli/internal/requestflag" + "github.com/beeper/desktop-api-go/v5" + "github.com/beeper/desktop-api-go/v5/option" + "github.com/tidwall/gjson" + "github.com/urfave/cli/v3" +) + +var appE2eeRecoveryCodeResetCreate = cli.Command{ + Name: "create", + Usage: "Create a new recovery key when the user cannot use the existing one.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "recovery-code", + Usage: "Existing recovery key, if the user has it.", + BodyPath: "recoveryCode", + }, + }, + Action: handleAppE2eeRecoveryCodeResetCreate, + HideHelpCommand: true, +} + +var appE2eeRecoveryCodeResetConfirm = cli.Command{ + Name: "confirm", + Usage: "Confirm that the new recovery key should be used for this account.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "recovery-code", + Usage: "New recovery key returned by the reset step.", + Required: true, + BodyPath: "recoveryCode", + }, + }, + Action: handleAppE2eeRecoveryCodeResetConfirm, + HideHelpCommand: true, +} + +func handleAppE2eeRecoveryCodeResetCreate(ctx context.Context, cmd *cli.Command) error { + client := beeperdesktopapi.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.ArrayQueryFormatRepeat, + ApplicationJSON, + false, + ) + if err != nil { + return err + } + + params := beeperdesktopapi.AppE2eeRecoveryCodeResetNewParams{} + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.App.E2ee.RecoveryCode.Reset.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: "app:e2ee:recovery-code:reset create", + Transform: transform, + }) +} + +func handleAppE2eeRecoveryCodeResetConfirm(ctx context.Context, cmd *cli.Command) error { + client := beeperdesktopapi.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.ArrayQueryFormatRepeat, + ApplicationJSON, + false, + ) + if err != nil { + return err + } + + params := beeperdesktopapi.AppE2eeRecoveryCodeResetConfirmParams{} + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.App.E2ee.RecoveryCode.Reset.Confirm(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: "app:e2ee:recovery-code:reset confirm", + Transform: transform, + }) +} diff --git a/pkg/cmd/appe2eerecoverycodereset_test.go b/pkg/cmd/appe2eerecoverycodereset_test.go new file mode 100644 index 00000000..75f8543e --- /dev/null +++ b/pkg/cmd/appe2eerecoverycodereset_test.go @@ -0,0 +1,51 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "testing" + + "github.com/beeper/desktop-api-cli/internal/mocktest" +) + +func TestAppE2eeRecoveryCodeResetCreate(t *testing.T) { + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--access-token", "string", + "app:e2ee:recovery-code:reset", "create", + "--recovery-code", "recoveryCode", + ) + }) + + t.Run("piping data", func(t *testing.T) { + // Test piping YAML data over stdin + pipeData := []byte("recoveryCode: recoveryCode") + mocktest.TestRunMockTestWithPipeAndFlags( + t, pipeData, + "--access-token", "string", + "app:e2ee:recovery-code:reset", "create", + ) + }) +} + +func TestAppE2eeRecoveryCodeResetConfirm(t *testing.T) { + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--access-token", "string", + "app:e2ee:recovery-code:reset", "confirm", + "--recovery-code", "x", + ) + }) + + t.Run("piping data", func(t *testing.T) { + // Test piping YAML data over stdin + pipeData := []byte("recoveryCode: x") + mocktest.TestRunMockTestWithPipeAndFlags( + t, pipeData, + "--access-token", "string", + "app:e2ee:recovery-code:reset", "confirm", + ) + }) +} diff --git a/pkg/cmd/appe2eeverification.go b/pkg/cmd/appe2eeverification.go new file mode 100644 index 00000000..4caecba2 --- /dev/null +++ b/pkg/cmd/appe2eeverification.go @@ -0,0 +1,204 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "context" + "fmt" + + "github.com/beeper/desktop-api-cli/internal/apiquery" + "github.com/beeper/desktop-api-cli/internal/requestflag" + "github.com/beeper/desktop-api-go/v5" + "github.com/beeper/desktop-api-go/v5/option" + "github.com/tidwall/gjson" + "github.com/urfave/cli/v3" +) + +var appE2eeVerificationCreate = cli.Command{ + Name: "create", + Usage: "Start verifying this device from another signed-in device.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "user-id", + Usage: "User ID to verify. Defaults to the signed-in user.", + BodyPath: "userID", + }, + }, + Action: handleAppE2eeVerificationCreate, + HideHelpCommand: true, +} + +var appE2eeVerificationAccept = cli.Command{ + Name: "accept", + Usage: "Accept an incoming device verification request.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "verification-id", + Usage: "Verification ID.", + Required: true, + PathParam: "verificationID", + }, + }, + Action: handleAppE2eeVerificationAccept, + HideHelpCommand: true, +} + +var appE2eeVerificationCancel = cli.Command{ + Name: "cancel", + Usage: "Cancel an active device verification request.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "verification-id", + Usage: "Verification ID.", + Required: true, + PathParam: "verificationID", + }, + &requestflag.Flag[string]{ + Name: "code", + Usage: "Optional cancellation code.", + BodyPath: "code", + }, + &requestflag.Flag[string]{ + Name: "reason", + Usage: "Optional user-facing cancellation reason.", + BodyPath: "reason", + }, + }, + Action: handleAppE2eeVerificationCancel, + HideHelpCommand: true, +} + +func handleAppE2eeVerificationCreate(ctx context.Context, cmd *cli.Command) error { + client := beeperdesktopapi.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.ArrayQueryFormatRepeat, + ApplicationJSON, + false, + ) + if err != nil { + return err + } + + params := beeperdesktopapi.AppE2eeVerificationNewParams{} + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.App.E2ee.Verification.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: "app:e2ee:verification create", + Transform: transform, + }) +} + +func handleAppE2eeVerificationAccept(ctx context.Context, cmd *cli.Command) error { + client := beeperdesktopapi.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("verification-id") && len(unusedArgs) > 0 { + cmd.Set("verification-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.ArrayQueryFormatRepeat, + EmptyBody, + false, + ) + if err != nil { + return err + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.App.E2ee.Verification.Accept(ctx, cmd.Value("verification-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: "app:e2ee:verification accept", + Transform: transform, + }) +} + +func handleAppE2eeVerificationCancel(ctx context.Context, cmd *cli.Command) error { + client := beeperdesktopapi.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("verification-id") && len(unusedArgs) > 0 { + cmd.Set("verification-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.ArrayQueryFormatRepeat, + ApplicationJSON, + false, + ) + if err != nil { + return err + } + + params := beeperdesktopapi.AppE2eeVerificationCancelParams{} + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.App.E2ee.Verification.Cancel( + ctx, + cmd.Value("verification-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: "app:e2ee:verification cancel", + Transform: transform, + }) +} diff --git a/pkg/cmd/appe2eeverification_test.go b/pkg/cmd/appe2eeverification_test.go new file mode 100644 index 00000000..57130ce2 --- /dev/null +++ b/pkg/cmd/appe2eeverification_test.go @@ -0,0 +1,67 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "testing" + + "github.com/beeper/desktop-api-cli/internal/mocktest" +) + +func TestAppE2eeVerificationCreate(t *testing.T) { + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--access-token", "string", + "app:e2ee:verification", "create", + "--user-id", "userID", + ) + }) + + t.Run("piping data", func(t *testing.T) { + // Test piping YAML data over stdin + pipeData := []byte("userID: userID") + mocktest.TestRunMockTestWithPipeAndFlags( + t, pipeData, + "--access-token", "string", + "app:e2ee:verification", "create", + ) + }) +} + +func TestAppE2eeVerificationAccept(t *testing.T) { + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--access-token", "string", + "app:e2ee:verification", "accept", + "--verification-id", "x", + ) + }) +} + +func TestAppE2eeVerificationCancel(t *testing.T) { + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--access-token", "string", + "app:e2ee:verification", "cancel", + "--verification-id", "x", + "--code", "code", + "--reason", "reason", + ) + }) + + t.Run("piping data", func(t *testing.T) { + // Test piping YAML data over stdin + pipeData := []byte("" + + "code: code\n" + + "reason: reason\n") + mocktest.TestRunMockTestWithPipeAndFlags( + t, pipeData, + "--access-token", "string", + "app:e2ee:verification", "cancel", + "--verification-id", "x", + ) + }) +} diff --git a/pkg/cmd/appe2eeverificationqr.go b/pkg/cmd/appe2eeverificationqr.go new file mode 100644 index 00000000..331aa1b8 --- /dev/null +++ b/pkg/cmd/appe2eeverificationqr.go @@ -0,0 +1,130 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "context" + "fmt" + + "github.com/beeper/desktop-api-cli/internal/apiquery" + "github.com/beeper/desktop-api-cli/internal/requestflag" + "github.com/beeper/desktop-api-go/v5" + "github.com/beeper/desktop-api-go/v5/option" + "github.com/tidwall/gjson" + "github.com/urfave/cli/v3" +) + +var appE2eeVerificationQrConfirmScanned = cli.Command{ + Name: "confirm-scanned", + Usage: "Confirm that another device scanned this device QR code.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "verification-id", + Usage: "Verification ID.", + Required: true, + PathParam: "verificationID", + }, + }, + Action: handleAppE2eeVerificationQrConfirmScanned, + HideHelpCommand: true, +} + +var appE2eeVerificationQrScan = cli.Command{ + Name: "scan", + Usage: "Submit the QR code scanned from another signed-in device.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "data", + Usage: "QR code payload scanned from the other device.", + Required: true, + BodyPath: "data", + }, + }, + Action: handleAppE2eeVerificationQrScan, + HideHelpCommand: true, +} + +func handleAppE2eeVerificationQrConfirmScanned(ctx context.Context, cmd *cli.Command) error { + client := beeperdesktopapi.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("verification-id") && len(unusedArgs) > 0 { + cmd.Set("verification-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.ArrayQueryFormatRepeat, + EmptyBody, + false, + ) + if err != nil { + return err + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.App.E2ee.Verification.Qr.ConfirmScanned(ctx, cmd.Value("verification-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: "app:e2ee:verification:qr confirm-scanned", + Transform: transform, + }) +} + +func handleAppE2eeVerificationQrScan(ctx context.Context, cmd *cli.Command) error { + client := beeperdesktopapi.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.ArrayQueryFormatRepeat, + ApplicationJSON, + false, + ) + if err != nil { + return err + } + + params := beeperdesktopapi.AppE2eeVerificationQrScanParams{} + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.App.E2ee.Verification.Qr.Scan(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: "app:e2ee:verification:qr scan", + Transform: transform, + }) +} diff --git a/pkg/cmd/appe2eeverificationqr_test.go b/pkg/cmd/appe2eeverificationqr_test.go new file mode 100644 index 00000000..635420dd --- /dev/null +++ b/pkg/cmd/appe2eeverificationqr_test.go @@ -0,0 +1,41 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "testing" + + "github.com/beeper/desktop-api-cli/internal/mocktest" +) + +func TestAppE2eeVerificationQrConfirmScanned(t *testing.T) { + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--access-token", "string", + "app:e2ee:verification:qr", "confirm-scanned", + "--verification-id", "x", + ) + }) +} + +func TestAppE2eeVerificationQrScan(t *testing.T) { + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--access-token", "string", + "app:e2ee:verification:qr", "scan", + "--data", "x", + ) + }) + + t.Run("piping data", func(t *testing.T) { + // Test piping YAML data over stdin + pipeData := []byte("data: x") + mocktest.TestRunMockTestWithPipeAndFlags( + t, pipeData, + "--access-token", "string", + "app:e2ee:verification:qr", "scan", + ) + }) +} diff --git a/pkg/cmd/appe2eeverificationsa.go b/pkg/cmd/appe2eeverificationsa.go new file mode 100644 index 00000000..afbda966 --- /dev/null +++ b/pkg/cmd/appe2eeverificationsa.go @@ -0,0 +1,131 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "context" + "fmt" + + "github.com/beeper/desktop-api-cli/internal/apiquery" + "github.com/beeper/desktop-api-cli/internal/requestflag" + "github.com/beeper/desktop-api-go/v5" + "github.com/beeper/desktop-api-go/v5/option" + "github.com/tidwall/gjson" + "github.com/urfave/cli/v3" +) + +var appE2eeVerificationSasConfirm = cli.Command{ + Name: "confirm", + Usage: "Confirm that the emoji or number sequence matches on both devices.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "verification-id", + Usage: "Verification ID.", + Required: true, + PathParam: "verificationID", + }, + }, + Action: handleAppE2eeVerificationSasConfirm, + HideHelpCommand: true, +} + +var appE2eeVerificationSasStart = cli.Command{ + Name: "start", + Usage: "Start emoji comparison for device verification.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "verification-id", + Usage: "Verification ID.", + Required: true, + PathParam: "verificationID", + }, + }, + Action: handleAppE2eeVerificationSasStart, + HideHelpCommand: true, +} + +func handleAppE2eeVerificationSasConfirm(ctx context.Context, cmd *cli.Command) error { + client := beeperdesktopapi.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("verification-id") && len(unusedArgs) > 0 { + cmd.Set("verification-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.ArrayQueryFormatRepeat, + EmptyBody, + false, + ) + if err != nil { + return err + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.App.E2ee.Verification.Sas.Confirm(ctx, cmd.Value("verification-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: "app:e2ee:verification:sas confirm", + Transform: transform, + }) +} + +func handleAppE2eeVerificationSasStart(ctx context.Context, cmd *cli.Command) error { + client := beeperdesktopapi.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("verification-id") && len(unusedArgs) > 0 { + cmd.Set("verification-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.ArrayQueryFormatRepeat, + EmptyBody, + false, + ) + if err != nil { + return err + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.App.E2ee.Verification.Sas.Start(ctx, cmd.Value("verification-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: "app:e2ee:verification:sas start", + Transform: transform, + }) +} diff --git a/pkg/cmd/appe2eeverificationsa_test.go b/pkg/cmd/appe2eeverificationsa_test.go new file mode 100644 index 00000000..526a6539 --- /dev/null +++ b/pkg/cmd/appe2eeverificationsa_test.go @@ -0,0 +1,31 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "testing" + + "github.com/beeper/desktop-api-cli/internal/mocktest" +) + +func TestAppE2eeVerificationSasConfirm(t *testing.T) { + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--access-token", "string", + "app:e2ee:verification:sas", "confirm", + "--verification-id", "x", + ) + }) +} + +func TestAppE2eeVerificationSasStart(t *testing.T) { + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--access-token", "string", + "app:e2ee:verification:sas", "start", + "--verification-id", "x", + ) + }) +} diff --git a/pkg/cmd/applogin.go b/pkg/cmd/applogin.go new file mode 100644 index 00000000..2c83b7f3 --- /dev/null +++ b/pkg/cmd/applogin.go @@ -0,0 +1,264 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "context" + "fmt" + + "github.com/beeper/desktop-api-cli/internal/apiquery" + "github.com/beeper/desktop-api-cli/internal/requestflag" + "github.com/beeper/desktop-api-go/v5" + "github.com/beeper/desktop-api-go/v5/option" + "github.com/tidwall/gjson" + "github.com/urfave/cli/v3" +) + +var appLoginEmail = cli.Command{ + Name: "email", + Usage: "Send a sign-in code to the user email address.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "email", + Usage: "Email address to send the sign-in code to.", + Required: true, + BodyPath: "email", + }, + &requestflag.Flag[string]{ + Name: "request", + Usage: "Login request ID returned by the start step.", + Required: true, + BodyPath: "request", + }, + }, + Action: handleAppLoginEmail, + HideHelpCommand: true, +} + +var appLoginRegister = cli.Command{ + Name: "register", + Usage: "Create a Beeper account after the user chooses a username and accepts the Terms\nof Use.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[bool]{ + Name: "accept-terms", + Usage: "Confirms that the user accepted the Terms of Use and acknowledged the Privacy Policy.", + Required: true, + BodyPath: "acceptTerms", + }, + &requestflag.Flag[string]{ + Name: "lead-token", + Usage: "Registration token returned by Beeper.", + Required: true, + BodyPath: "leadToken", + }, + &requestflag.Flag[string]{ + Name: "request", + Usage: "Login request ID returned by the start step.", + Required: true, + BodyPath: "request", + }, + &requestflag.Flag[string]{ + Name: "username", + Usage: "Username selected by the user.", + Required: true, + BodyPath: "username", + }, + }, + Action: handleAppLoginRegister, + HideHelpCommand: true, +} + +var appLoginResponse = cli.Command{ + Name: "response", + Usage: "Finish sign-in with the code sent to the user email address. If the user needs a\nnew account, the response includes account creation copy and username\nsuggestions.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "request", + Usage: "Login request ID returned by the start step.", + Required: true, + BodyPath: "request", + }, + &requestflag.Flag[string]{ + Name: "response", + Usage: "Sign-in code from the user email.", + Required: true, + BodyPath: "response", + }, + }, + Action: handleAppLoginResponse, + HideHelpCommand: true, +} + +var appLoginStart = cli.Command{ + Name: "start", + Usage: "Start a first-party Beeper Desktop sign-in session.", + Suggest: true, + Flags: []cli.Flag{}, + Action: handleAppLoginStart, + HideHelpCommand: true, +} + +func handleAppLoginEmail(ctx context.Context, cmd *cli.Command) error { + client := beeperdesktopapi.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.ArrayQueryFormatRepeat, + ApplicationJSON, + false, + ) + if err != nil { + return err + } + + params := beeperdesktopapi.AppLoginEmailParams{} + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.App.Login.Email(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: "app:login email", + Transform: transform, + }) +} + +func handleAppLoginRegister(ctx context.Context, cmd *cli.Command) error { + client := beeperdesktopapi.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.ArrayQueryFormatRepeat, + ApplicationJSON, + false, + ) + if err != nil { + return err + } + + params := beeperdesktopapi.AppLoginRegisterParams{} + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.App.Login.Register(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: "app:login register", + Transform: transform, + }) +} + +func handleAppLoginResponse(ctx context.Context, cmd *cli.Command) error { + client := beeperdesktopapi.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.ArrayQueryFormatRepeat, + ApplicationJSON, + false, + ) + if err != nil { + return err + } + + params := beeperdesktopapi.AppLoginResponseParams{} + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.App.Login.Response(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: "app:login response", + Transform: transform, + }) +} + +func handleAppLoginStart(ctx context.Context, cmd *cli.Command) error { + client := beeperdesktopapi.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.ArrayQueryFormatRepeat, + EmptyBody, + false, + ) + if err != nil { + return err + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.App.Login.Start(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: "app:login start", + Transform: transform, + }) +} diff --git a/pkg/cmd/applogin_test.go b/pkg/cmd/applogin_test.go new file mode 100644 index 00000000..b4f866a0 --- /dev/null +++ b/pkg/cmd/applogin_test.go @@ -0,0 +1,95 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "testing" + + "github.com/beeper/desktop-api-cli/internal/mocktest" +) + +func TestAppLoginEmail(t *testing.T) { + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--access-token", "string", + "app:login", "email", + "--email", "dev@stainless.com", + "--request", "request", + ) + }) + + t.Run("piping data", func(t *testing.T) { + // Test piping YAML data over stdin + pipeData := []byte("" + + "email: dev@stainless.com\n" + + "request: request\n") + mocktest.TestRunMockTestWithPipeAndFlags( + t, pipeData, + "--access-token", "string", + "app:login", "email", + ) + }) +} + +func TestAppLoginRegister(t *testing.T) { + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--access-token", "string", + "app:login", "register", + "--accept-terms=true", + "--lead-token", "leadToken", + "--request", "request", + "--username", "x", + ) + }) + + t.Run("piping data", func(t *testing.T) { + // Test piping YAML data over stdin + pipeData := []byte("" + + "acceptTerms: true\n" + + "leadToken: leadToken\n" + + "request: request\n" + + "username: x\n") + mocktest.TestRunMockTestWithPipeAndFlags( + t, pipeData, + "--access-token", "string", + "app:login", "register", + ) + }) +} + +func TestAppLoginResponse(t *testing.T) { + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--access-token", "string", + "app:login", "response", + "--request", "request", + "--response", "response", + ) + }) + + t.Run("piping data", func(t *testing.T) { + // Test piping YAML data over stdin + pipeData := []byte("" + + "request: request\n" + + "response: response\n") + mocktest.TestRunMockTestWithPipeAndFlags( + t, pipeData, + "--access-token", "string", + "app:login", "response", + ) + }) +} + +func TestAppLoginStart(t *testing.T) { + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--access-token", "string", + "app:login", "start", + ) + }) +} diff --git a/pkg/cmd/bridge.go b/pkg/cmd/bridge.go new file mode 100644 index 00000000..a33054e6 --- /dev/null +++ b/pkg/cmd/bridge.go @@ -0,0 +1,65 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "context" + "fmt" + + "github.com/beeper/desktop-api-cli/internal/apiquery" + "github.com/beeper/desktop-api-go/v5" + "github.com/beeper/desktop-api-go/v5/option" + "github.com/tidwall/gjson" + "github.com/urfave/cli/v3" +) + +var bridgesList = cli.Command{ + Name: "list", + Usage: "List bridge-backed account types that can be shown in add-account flows, grouped\nwith connected accounts that use the same Account schema as GET /v1/accounts.", + Suggest: true, + Flags: []cli.Flag{}, + Action: handleBridgesList, + HideHelpCommand: true, +} + +func handleBridgesList(ctx context.Context, cmd *cli.Command) error { + client := beeperdesktopapi.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.ArrayQueryFormatRepeat, + EmptyBody, + false, + ) + if err != nil { + return err + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Bridges.List(ctx, options...) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := "json" + explicitFormat := cmd.Root().IsSet("format") + if explicitFormat { + format = cmd.Root().String("format") + } + transform := cmd.Root().String("transform") + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "bridges list", + Transform: transform, + }) +} diff --git a/pkg/cmd/bridge_test.go b/pkg/cmd/bridge_test.go new file mode 100644 index 00000000..25bdeb87 --- /dev/null +++ b/pkg/cmd/bridge_test.go @@ -0,0 +1,19 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "testing" + + "github.com/beeper/desktop-api-cli/internal/mocktest" +) + +func TestBridgesList(t *testing.T) { + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--access-token", "string", + "bridges", "list", + ) + }) +} diff --git a/pkg/cmd/cmd.go b/pkg/cmd/cmd.go index 1304eaa3..089dd61c 100644 --- a/pkg/cmd/cmd.go +++ b/pkg/cmd/cmd.go @@ -87,6 +87,71 @@ func init() { Commands: []*cli.Command{ &focus, &search, + { + Name: "app", + Category: "API RESOURCE", + Suggest: true, + Commands: []*cli.Command{ + &appStatus, + }, + }, + { + Name: "app:login", + Category: "API RESOURCE", + Suggest: true, + Commands: []*cli.Command{ + &appLoginEmail, + &appLoginRegister, + &appLoginResponse, + &appLoginStart, + }, + }, + { + Name: "app:e2ee:recovery-code", + Category: "API RESOURCE", + Suggest: true, + Commands: []*cli.Command{ + &appE2eeRecoveryCodeMarkBackedUp, + &appE2eeRecoveryCodeVerify, + }, + }, + { + Name: "app:e2ee:recovery-code:reset", + Category: "API RESOURCE", + Suggest: true, + Commands: []*cli.Command{ + &appE2eeRecoveryCodeResetCreate, + &appE2eeRecoveryCodeResetConfirm, + }, + }, + { + Name: "app:e2ee:verification", + Category: "API RESOURCE", + Suggest: true, + Commands: []*cli.Command{ + &appE2eeVerificationCreate, + &appE2eeVerificationAccept, + &appE2eeVerificationCancel, + }, + }, + { + Name: "app:e2ee:verification:qr", + Category: "API RESOURCE", + Suggest: true, + Commands: []*cli.Command{ + &appE2eeVerificationQrConfirmScanned, + &appE2eeVerificationQrScan, + }, + }, + { + Name: "app:e2ee:verification:sas", + Category: "API RESOURCE", + Suggest: true, + Commands: []*cli.Command{ + &appE2eeVerificationSasConfirm, + &appE2eeVerificationSasStart, + }, + }, { Name: "accounts", Category: "API RESOURCE", @@ -104,6 +169,116 @@ func init() { &accountsContactsSearch, }, }, + { + Name: "bridges", + Category: "API RESOURCE", + Suggest: true, + Commands: []*cli.Command{ + &bridgesList, + }, + }, + { + Name: "matrix:users", + Category: "API RESOURCE", + Suggest: true, + Commands: []*cli.Command{ + &matrixUsersRetrieveProfile, + }, + }, + { + Name: "matrix:users:account-data", + Category: "API RESOURCE", + Suggest: true, + Commands: []*cli.Command{ + &matrixUsersAccountDataRetrieve, + &matrixUsersAccountDataUpdate, + }, + }, + { + Name: "matrix:rooms", + Category: "API RESOURCE", + Suggest: true, + Commands: []*cli.Command{ + &matrixRoomsCreate, + &matrixRoomsJoin, + &matrixRoomsLeave, + }, + }, + { + Name: "matrix:rooms:account-data", + Category: "API RESOURCE", + Suggest: true, + Commands: []*cli.Command{ + &matrixRoomsAccountDataRetrieve, + &matrixRoomsAccountDataUpdate, + }, + }, + { + Name: "matrix:rooms:state", + Category: "API RESOURCE", + Suggest: true, + Commands: []*cli.Command{ + &matrixRoomsStateRetrieve, + &matrixRoomsStateList, + }, + }, + { + Name: "matrix:rooms:events", + Category: "API RESOURCE", + Suggest: true, + Commands: []*cli.Command{ + &matrixRoomsEventsRetrieve, + }, + }, + { + Name: "matrix:bridges:auth", + Category: "API RESOURCE", + Suggest: true, + Commands: []*cli.Command{ + &matrixBridgesAuthListFlows, + &matrixBridgesAuthListLogins, + &matrixBridgesAuthLogout, + &matrixBridgesAuthStartLogin, + &matrixBridgesAuthSubmitCookies, + &matrixBridgesAuthSubmitUserInput, + &matrixBridgesAuthWaitForStep, + &matrixBridgesAuthWhoami, + }, + }, + { + Name: "matrix:bridges:contacts", + Category: "API RESOURCE", + Suggest: true, + Commands: []*cli.Command{ + &matrixBridgesContactsList, + }, + }, + { + Name: "matrix:bridges:users", + Category: "API RESOURCE", + Suggest: true, + Commands: []*cli.Command{ + &matrixBridgesUsersResolve, + &matrixBridgesUsersSearch, + }, + }, + { + Name: "matrix:bridges:rooms", + Category: "API RESOURCE", + Suggest: true, + Commands: []*cli.Command{ + &matrixBridgesRoomsCreateDm, + &matrixBridgesRoomsCreateGroup, + }, + }, + { + Name: "matrix:bridges:capabilities", + Category: "API RESOURCE", + Suggest: true, + Commands: []*cli.Command{ + &matrixBridgesCapabilitiesRetrieve, + }, + }, { Name: "chats", Category: "API RESOURCE", diff --git a/pkg/cmd/matrixbridgeauth.go b/pkg/cmd/matrixbridgeauth.go new file mode 100644 index 00000000..ea8deb55 --- /dev/null +++ b/pkg/cmd/matrixbridgeauth.go @@ -0,0 +1,584 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "context" + "fmt" + + "github.com/beeper/desktop-api-cli/internal/apiquery" + "github.com/beeper/desktop-api-cli/internal/requestflag" + "github.com/beeper/desktop-api-go/v5" + "github.com/beeper/desktop-api-go/v5/option" + "github.com/tidwall/gjson" + "github.com/urfave/cli/v3" +) + +var matrixBridgesAuthListFlows = cli.Command{ + Name: "list-flows", + Usage: "Get the available login flows.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "bridge-id", + Required: true, + PathParam: "bridgeID", + }, + }, + Action: handleMatrixBridgesAuthListFlows, + HideHelpCommand: true, +} + +var matrixBridgesAuthListLogins = cli.Command{ + Name: "list-logins", + Usage: "Get the login IDs of the current user.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "bridge-id", + Required: true, + PathParam: "bridgeID", + }, + }, + Action: handleMatrixBridgesAuthListLogins, + HideHelpCommand: true, +} + +var matrixBridgesAuthLogout = cli.Command{ + Name: "logout", + Usage: "Log out of an existing login.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "bridge-id", + Required: true, + PathParam: "bridgeID", + }, + &requestflag.Flag[string]{ + Name: "login-id", + Usage: "The unique ID of a login. Defined by the network connector.", + Required: true, + PathParam: "loginID", + }, + }, + Action: handleMatrixBridgesAuthLogout, + HideHelpCommand: true, +} + +var matrixBridgesAuthStartLogin = cli.Command{ + Name: "start-login", + Usage: "This endpoint starts a new login process, which is used to log into the bridge.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "bridge-id", + Required: true, + PathParam: "bridgeID", + }, + &requestflag.Flag[string]{ + Name: "flow-id", + Required: true, + PathParam: "flowID", + }, + &requestflag.Flag[string]{ + Name: "login-id", + Usage: "An existing login ID to re-login as. If this is specified and the user logs into a different account, the provided ID will be logged out.", + QueryPath: "login_id", + }, + }, + Action: handleMatrixBridgesAuthStartLogin, + HideHelpCommand: true, +} + +var matrixBridgesAuthSubmitCookies = cli.Command{ + Name: "submit-cookies", + Usage: "Submit extracted cookies in a login process.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "bridge-id", + Required: true, + PathParam: "bridgeID", + }, + &requestflag.Flag[string]{ + Name: "login-process-id", + Required: true, + PathParam: "loginProcessID", + }, + &requestflag.Flag[string]{ + Name: "step-id", + Required: true, + PathParam: "stepID", + }, + &requestflag.Flag[map[string]any]{ + Name: "body", + Required: true, + BodyRoot: true, + }, + }, + Action: handleMatrixBridgesAuthSubmitCookies, + HideHelpCommand: true, +} + +var matrixBridgesAuthSubmitUserInput = cli.Command{ + Name: "submit-user-input", + Usage: "Submit user input in a login process.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "bridge-id", + Required: true, + PathParam: "bridgeID", + }, + &requestflag.Flag[string]{ + Name: "login-process-id", + Required: true, + PathParam: "loginProcessID", + }, + &requestflag.Flag[string]{ + Name: "step-id", + Required: true, + PathParam: "stepID", + }, + &requestflag.Flag[map[string]any]{ + Name: "body", + Required: true, + BodyRoot: true, + }, + }, + Action: handleMatrixBridgesAuthSubmitUserInput, + HideHelpCommand: true, +} + +var matrixBridgesAuthWaitForStep = cli.Command{ + Name: "wait-for-step", + Usage: "Wait for the next step after displaying data to the user.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "bridge-id", + Required: true, + PathParam: "bridgeID", + }, + &requestflag.Flag[string]{ + Name: "login-process-id", + Required: true, + PathParam: "loginProcessID", + }, + &requestflag.Flag[string]{ + Name: "step-id", + Required: true, + PathParam: "stepID", + }, + }, + Action: handleMatrixBridgesAuthWaitForStep, + HideHelpCommand: true, +} + +var matrixBridgesAuthWhoami = cli.Command{ + Name: "whoami", + Usage: "Get all info that is useful for presenting this bridge in a manager interface.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "bridge-id", + Required: true, + PathParam: "bridgeID", + }, + }, + Action: handleMatrixBridgesAuthWhoami, + HideHelpCommand: true, +} + +func handleMatrixBridgesAuthListFlows(ctx context.Context, cmd *cli.Command) error { + client := beeperdesktopapi.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("bridge-id") && len(unusedArgs) > 0 { + cmd.Set("bridge-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.ArrayQueryFormatRepeat, + EmptyBody, + false, + ) + if err != nil { + return err + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Matrix.Bridges.Auth.ListFlows(ctx, cmd.Value("bridge-id").(string), options...) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := "json" + explicitFormat := cmd.Root().IsSet("format") + if explicitFormat { + format = cmd.Root().String("format") + } + transform := cmd.Root().String("transform") + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "matrix:bridges:auth list-flows", + Transform: transform, + }) +} + +func handleMatrixBridgesAuthListLogins(ctx context.Context, cmd *cli.Command) error { + client := beeperdesktopapi.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("bridge-id") && len(unusedArgs) > 0 { + cmd.Set("bridge-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.ArrayQueryFormatRepeat, + EmptyBody, + false, + ) + if err != nil { + return err + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Matrix.Bridges.Auth.ListLogins(ctx, cmd.Value("bridge-id").(string), options...) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := "json" + explicitFormat := cmd.Root().IsSet("format") + if explicitFormat { + format = cmd.Root().String("format") + } + transform := cmd.Root().String("transform") + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "matrix:bridges:auth list-logins", + Transform: transform, + }) +} + +func handleMatrixBridgesAuthLogout(ctx context.Context, cmd *cli.Command) error { + client := beeperdesktopapi.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("login-id") && len(unusedArgs) > 0 { + cmd.Set("login-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.ArrayQueryFormatRepeat, + EmptyBody, + false, + ) + if err != nil { + return err + } + + params := beeperdesktopapi.MatrixBridgeAuthLogoutParams{ + BridgeID: cmd.Value("bridge-id").(string), + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Matrix.Bridges.Auth.Logout( + ctx, + cmd.Value("login-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: "matrix:bridges:auth logout", + Transform: transform, + }) +} + +func handleMatrixBridgesAuthStartLogin(ctx context.Context, cmd *cli.Command) error { + client := beeperdesktopapi.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("flow-id") && len(unusedArgs) > 0 { + cmd.Set("flow-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.ArrayQueryFormatRepeat, + EmptyBody, + false, + ) + if err != nil { + return err + } + + params := beeperdesktopapi.MatrixBridgeAuthStartLoginParams{ + BridgeID: cmd.Value("bridge-id").(string), + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Matrix.Bridges.Auth.StartLogin( + ctx, + cmd.Value("flow-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: "matrix:bridges:auth start-login", + Transform: transform, + }) +} + +func handleMatrixBridgesAuthSubmitCookies(ctx context.Context, cmd *cli.Command) error { + client := beeperdesktopapi.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("step-id") && len(unusedArgs) > 0 { + cmd.Set("step-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.ArrayQueryFormatRepeat, + ApplicationJSON, + false, + ) + if err != nil { + return err + } + + params := beeperdesktopapi.MatrixBridgeAuthSubmitCookiesParams{ + BridgeID: cmd.Value("bridge-id").(string), + LoginProcessID: cmd.Value("login-process-id").(string), + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Matrix.Bridges.Auth.SubmitCookies( + ctx, + cmd.Value("step-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: "matrix:bridges:auth submit-cookies", + Transform: transform, + }) +} + +func handleMatrixBridgesAuthSubmitUserInput(ctx context.Context, cmd *cli.Command) error { + client := beeperdesktopapi.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("step-id") && len(unusedArgs) > 0 { + cmd.Set("step-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.ArrayQueryFormatRepeat, + ApplicationJSON, + false, + ) + if err != nil { + return err + } + + params := beeperdesktopapi.MatrixBridgeAuthSubmitUserInputParams{ + BridgeID: cmd.Value("bridge-id").(string), + LoginProcessID: cmd.Value("login-process-id").(string), + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Matrix.Bridges.Auth.SubmitUserInput( + ctx, + cmd.Value("step-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: "matrix:bridges:auth submit-user-input", + Transform: transform, + }) +} + +func handleMatrixBridgesAuthWaitForStep(ctx context.Context, cmd *cli.Command) error { + client := beeperdesktopapi.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("step-id") && len(unusedArgs) > 0 { + cmd.Set("step-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.ArrayQueryFormatRepeat, + EmptyBody, + false, + ) + if err != nil { + return err + } + + params := beeperdesktopapi.MatrixBridgeAuthWaitForStepParams{ + BridgeID: cmd.Value("bridge-id").(string), + LoginProcessID: cmd.Value("login-process-id").(string), + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Matrix.Bridges.Auth.WaitForStep( + ctx, + cmd.Value("step-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: "matrix:bridges:auth wait-for-step", + Transform: transform, + }) +} + +func handleMatrixBridgesAuthWhoami(ctx context.Context, cmd *cli.Command) error { + client := beeperdesktopapi.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("bridge-id") && len(unusedArgs) > 0 { + cmd.Set("bridge-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.ArrayQueryFormatRepeat, + EmptyBody, + false, + ) + if err != nil { + return err + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Matrix.Bridges.Auth.Whoami(ctx, cmd.Value("bridge-id").(string), options...) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := "json" + explicitFormat := cmd.Root().IsSet("format") + if explicitFormat { + format = cmd.Root().String("format") + } + transform := cmd.Root().String("transform") + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "matrix:bridges:auth whoami", + Transform: transform, + }) +} diff --git a/pkg/cmd/matrixbridgeauth_test.go b/pkg/cmd/matrixbridgeauth_test.go new file mode 100644 index 00000000..1776edae --- /dev/null +++ b/pkg/cmd/matrixbridgeauth_test.go @@ -0,0 +1,134 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "testing" + + "github.com/beeper/desktop-api-cli/internal/mocktest" +) + +func TestMatrixBridgesAuthListFlows(t *testing.T) { + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--access-token", "string", + "matrix:bridges:auth", "list-flows", + "--bridge-id", "bridgeID", + ) + }) +} + +func TestMatrixBridgesAuthListLogins(t *testing.T) { + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--access-token", "string", + "matrix:bridges:auth", "list-logins", + "--bridge-id", "bridgeID", + ) + }) +} + +func TestMatrixBridgesAuthLogout(t *testing.T) { + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--access-token", "string", + "matrix:bridges:auth", "logout", + "--bridge-id", "bridgeID", + "--login-id", "bcc68892-b180-414f-9516-b4aadf7d0496", + ) + }) +} + +func TestMatrixBridgesAuthStartLogin(t *testing.T) { + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--access-token", "string", + "matrix:bridges:auth", "start-login", + "--bridge-id", "bridgeID", + "--flow-id", "qr", + "--login-id", "bcc68892-b180-414f-9516-b4aadf7d0496", + ) + }) +} + +func TestMatrixBridgesAuthSubmitCookies(t *testing.T) { + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--access-token", "string", + "matrix:bridges:auth", "submit-cookies", + "--bridge-id", "bridgeID", + "--login-process-id", "loginProcessID", + "--step-id", "stepID", + "--body", "{foo: string}", + ) + }) + + t.Run("piping data", func(t *testing.T) { + // Test piping YAML data over stdin + pipeData := []byte("foo: string") + mocktest.TestRunMockTestWithPipeAndFlags( + t, pipeData, + "--access-token", "string", + "matrix:bridges:auth", "submit-cookies", + "--bridge-id", "bridgeID", + "--login-process-id", "loginProcessID", + "--step-id", "stepID", + ) + }) +} + +func TestMatrixBridgesAuthSubmitUserInput(t *testing.T) { + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--access-token", "string", + "matrix:bridges:auth", "submit-user-input", + "--bridge-id", "bridgeID", + "--login-process-id", "loginProcessID", + "--step-id", "stepID", + "--body", "{foo: string}", + ) + }) + + t.Run("piping data", func(t *testing.T) { + // Test piping YAML data over stdin + pipeData := []byte("foo: string") + mocktest.TestRunMockTestWithPipeAndFlags( + t, pipeData, + "--access-token", "string", + "matrix:bridges:auth", "submit-user-input", + "--bridge-id", "bridgeID", + "--login-process-id", "loginProcessID", + "--step-id", "stepID", + ) + }) +} + +func TestMatrixBridgesAuthWaitForStep(t *testing.T) { + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--access-token", "string", + "matrix:bridges:auth", "wait-for-step", + "--bridge-id", "bridgeID", + "--login-process-id", "loginProcessID", + "--step-id", "stepID", + ) + }) +} + +func TestMatrixBridgesAuthWhoami(t *testing.T) { + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--access-token", "string", + "matrix:bridges:auth", "whoami", + "--bridge-id", "bridgeID", + ) + }) +} diff --git a/pkg/cmd/matrixbridgecapability.go b/pkg/cmd/matrixbridgecapability.go new file mode 100644 index 00000000..0accdfed --- /dev/null +++ b/pkg/cmd/matrixbridgecapability.go @@ -0,0 +1,75 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "context" + "fmt" + + "github.com/beeper/desktop-api-cli/internal/apiquery" + "github.com/beeper/desktop-api-cli/internal/requestflag" + "github.com/beeper/desktop-api-go/v5" + "github.com/beeper/desktop-api-go/v5/option" + "github.com/tidwall/gjson" + "github.com/urfave/cli/v3" +) + +var matrixBridgesCapabilitiesRetrieve = cli.Command{ + Name: "retrieve", + Usage: "Get bridge capabilities", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "bridge-id", + Required: true, + PathParam: "bridgeID", + }, + }, + Action: handleMatrixBridgesCapabilitiesRetrieve, + HideHelpCommand: true, +} + +func handleMatrixBridgesCapabilitiesRetrieve(ctx context.Context, cmd *cli.Command) error { + client := beeperdesktopapi.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("bridge-id") && len(unusedArgs) > 0 { + cmd.Set("bridge-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.ArrayQueryFormatRepeat, + EmptyBody, + false, + ) + if err != nil { + return err + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Matrix.Bridges.Capabilities.Get(ctx, cmd.Value("bridge-id").(string), options...) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := "json" + explicitFormat := cmd.Root().IsSet("format") + if explicitFormat { + format = cmd.Root().String("format") + } + transform := cmd.Root().String("transform") + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "matrix:bridges:capabilities retrieve", + Transform: transform, + }) +} diff --git a/pkg/cmd/matrixbridgecapability_test.go b/pkg/cmd/matrixbridgecapability_test.go new file mode 100644 index 00000000..d90cd364 --- /dev/null +++ b/pkg/cmd/matrixbridgecapability_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/beeper/desktop-api-cli/internal/mocktest" +) + +func TestMatrixBridgesCapabilitiesRetrieve(t *testing.T) { + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--access-token", "string", + "matrix:bridges:capabilities", "retrieve", + "--bridge-id", "bridgeID", + ) + }) +} diff --git a/pkg/cmd/matrixbridgecontact.go b/pkg/cmd/matrixbridgecontact.go new file mode 100644 index 00000000..55c3eb74 --- /dev/null +++ b/pkg/cmd/matrixbridgecontact.go @@ -0,0 +1,87 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "context" + "fmt" + + "github.com/beeper/desktop-api-cli/internal/apiquery" + "github.com/beeper/desktop-api-cli/internal/requestflag" + "github.com/beeper/desktop-api-go/v5" + "github.com/beeper/desktop-api-go/v5/option" + "github.com/tidwall/gjson" + "github.com/urfave/cli/v3" +) + +var matrixBridgesContactsList = cli.Command{ + Name: "list", + Usage: "Get a list of contacts.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "bridge-id", + Required: true, + PathParam: "bridgeID", + }, + &requestflag.Flag[string]{ + Name: "login-id", + Usage: "An optional explicit login ID to do the action through.", + QueryPath: "login_id", + }, + }, + Action: handleMatrixBridgesContactsList, + HideHelpCommand: true, +} + +func handleMatrixBridgesContactsList(ctx context.Context, cmd *cli.Command) error { + client := beeperdesktopapi.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("bridge-id") && len(unusedArgs) > 0 { + cmd.Set("bridge-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.ArrayQueryFormatRepeat, + EmptyBody, + false, + ) + if err != nil { + return err + } + + params := beeperdesktopapi.MatrixBridgeContactListParams{} + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Matrix.Bridges.Contacts.List( + ctx, + cmd.Value("bridge-id").(string), + params, + options..., + ) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := "json" + explicitFormat := cmd.Root().IsSet("format") + if explicitFormat { + format = cmd.Root().String("format") + } + transform := cmd.Root().String("transform") + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "matrix:bridges:contacts list", + Transform: transform, + }) +} diff --git a/pkg/cmd/matrixbridgecontact_test.go b/pkg/cmd/matrixbridgecontact_test.go new file mode 100644 index 00000000..df14d107 --- /dev/null +++ b/pkg/cmd/matrixbridgecontact_test.go @@ -0,0 +1,21 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "testing" + + "github.com/beeper/desktop-api-cli/internal/mocktest" +) + +func TestMatrixBridgesContactsList(t *testing.T) { + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--access-token", "string", + "matrix:bridges:contacts", "list", + "--bridge-id", "bridgeID", + "--login-id", "bcc68892-b180-414f-9516-b4aadf7d0496", + ) + }) +} diff --git a/pkg/cmd/matrixbridgeroom.go b/pkg/cmd/matrixbridgeroom.go new file mode 100644 index 00000000..20d611df --- /dev/null +++ b/pkg/cmd/matrixbridgeroom.go @@ -0,0 +1,240 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "context" + "fmt" + + "github.com/beeper/desktop-api-cli/internal/apiquery" + "github.com/beeper/desktop-api-cli/internal/requestflag" + "github.com/beeper/desktop-api-go/v5" + "github.com/beeper/desktop-api-go/v5/option" + "github.com/tidwall/gjson" + "github.com/urfave/cli/v3" +) + +var matrixBridgesRoomsCreateDm = cli.Command{ + Name: "create-dm", + Usage: "Create a direct chat with a user on the remote network.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "bridge-id", + Required: true, + PathParam: "bridgeID", + }, + &requestflag.Flag[string]{ + Name: "identifier", + Required: true, + PathParam: "identifier", + }, + &requestflag.Flag[string]{ + Name: "login-id", + Usage: "An optional explicit login ID to do the action through.", + QueryPath: "login_id", + }, + }, + Action: handleMatrixBridgesRoomsCreateDm, + HideHelpCommand: true, +} + +var matrixBridgesRoomsCreateGroup = requestflag.WithInnerFlags(cli.Command{ + Name: "create-group", + Usage: "Create a group chat on the remote network.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "bridge-id", + Required: true, + PathParam: "bridgeID", + }, + &requestflag.Flag[string]{ + Name: "group-type", + Required: true, + PathParam: "groupType", + }, + &requestflag.Flag[string]{ + Name: "login-id", + Usage: "An optional explicit login ID to do the action through.", + QueryPath: "login_id", + }, + &requestflag.Flag[map[string]any]{ + Name: "avatar", + Usage: "The `m.room.avatar` event content for the room.", + BodyPath: "avatar", + }, + &requestflag.Flag[map[string]any]{ + Name: "disappear", + Usage: "The `com.beeper.disappearing_timer` event content for the room.", + BodyPath: "disappear", + }, + &requestflag.Flag[map[string]any]{ + Name: "name", + Usage: "The `m.room.name` event content for the room.", + BodyPath: "name", + }, + &requestflag.Flag[any]{ + Name: "parent", + BodyPath: "parent", + }, + &requestflag.Flag[[]string]{ + Name: "participant", + Usage: "The users to add to the group initially.", + BodyPath: "participants", + }, + &requestflag.Flag[string]{ + Name: "room-id", + Usage: "An existing Matrix room ID to bridge to.\nThe other parameters must be already in sync with the room state when using this parameter.\n", + BodyPath: "room_id", + }, + &requestflag.Flag[map[string]any]{ + Name: "topic", + Usage: "The `m.room.topic` event content for the room.", + BodyPath: "topic", + }, + &requestflag.Flag[string]{ + Name: "type", + Usage: "The type of group to create.", + BodyPath: "type", + }, + &requestflag.Flag[string]{ + Name: "username", + Usage: "The public username for the created group.", + BodyPath: "username", + }, + }, + Action: handleMatrixBridgesRoomsCreateGroup, + HideHelpCommand: true, +}, map[string][]requestflag.HasOuterFlag{ + "avatar": { + &requestflag.InnerFlag[string]{ + Name: "avatar.url", + InnerField: "url", + }, + }, + "disappear": { + &requestflag.InnerFlag[float64]{ + Name: "disappear.timer", + InnerField: "timer", + }, + &requestflag.InnerFlag[string]{ + Name: "disappear.type", + InnerField: "type", + }, + }, + "name": { + &requestflag.InnerFlag[string]{ + Name: "name.name", + InnerField: "name", + }, + }, + "topic": { + &requestflag.InnerFlag[string]{ + Name: "topic.topic", + InnerField: "topic", + }, + }, +}) + +func handleMatrixBridgesRoomsCreateDm(ctx context.Context, cmd *cli.Command) error { + client := beeperdesktopapi.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("identifier") && len(unusedArgs) > 0 { + cmd.Set("identifier", unusedArgs[0]) + unusedArgs = unusedArgs[1:] + } + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + + options, err := flagOptions( + cmd, + apiquery.NestedQueryFormatBrackets, + apiquery.ArrayQueryFormatRepeat, + EmptyBody, + false, + ) + if err != nil { + return err + } + + params := beeperdesktopapi.MatrixBridgeRoomNewDmParams{ + BridgeID: cmd.Value("bridge-id").(string), + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Matrix.Bridges.Rooms.NewDm( + ctx, + cmd.Value("identifier").(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: "matrix:bridges:rooms create-dm", + Transform: transform, + }) +} + +func handleMatrixBridgesRoomsCreateGroup(ctx context.Context, cmd *cli.Command) error { + client := beeperdesktopapi.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("group-type") && len(unusedArgs) > 0 { + cmd.Set("group-type", unusedArgs[0]) + unusedArgs = unusedArgs[1:] + } + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + + options, err := flagOptions( + cmd, + apiquery.NestedQueryFormatBrackets, + apiquery.ArrayQueryFormatRepeat, + ApplicationJSON, + false, + ) + if err != nil { + return err + } + + params := beeperdesktopapi.MatrixBridgeRoomNewGroupParams{ + BridgeID: cmd.Value("bridge-id").(string), + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Matrix.Bridges.Rooms.NewGroup( + ctx, + cmd.Value("group-type").(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: "matrix:bridges:rooms create-group", + Transform: transform, + }) +} diff --git a/pkg/cmd/matrixbridgeroom_test.go b/pkg/cmd/matrixbridgeroom_test.go new file mode 100644 index 00000000..4e4c59e9 --- /dev/null +++ b/pkg/cmd/matrixbridgeroom_test.go @@ -0,0 +1,98 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "testing" + + "github.com/beeper/desktop-api-cli/internal/mocktest" + "github.com/beeper/desktop-api-cli/internal/requestflag" +) + +func TestMatrixBridgesRoomsCreateDm(t *testing.T) { + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--access-token", "string", + "matrix:bridges:rooms", "create-dm", + "--bridge-id", "bridgeID", + "--identifier", "identifier", + "--login-id", "bcc68892-b180-414f-9516-b4aadf7d0496", + ) + }) +} + +func TestMatrixBridgesRoomsCreateGroup(t *testing.T) { + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--access-token", "string", + "matrix:bridges:rooms", "create-group", + "--bridge-id", "bridgeID", + "--group-type", "groupType", + "--login-id", "bcc68892-b180-414f-9516-b4aadf7d0496", + "--avatar", "{url: url}", + "--disappear", "{timer: 0, type: type}", + "--name", "{name: name}", + "--parent", "{}", + "--participant", "string", + "--room-id", "room_id", + "--topic", "{topic: topic}", + "--type", "channel", + "--username", "username", + ) + }) + + t.Run("inner flags", func(t *testing.T) { + // Check that inner flags have been set up correctly + requestflag.CheckInnerFlags(matrixBridgesRoomsCreateGroup) + + // Alternative argument passing style using inner flags + mocktest.TestRunMockTestWithFlags( + t, + "--access-token", "string", + "matrix:bridges:rooms", "create-group", + "--bridge-id", "bridgeID", + "--group-type", "groupType", + "--login-id", "bcc68892-b180-414f-9516-b4aadf7d0496", + "--avatar.url", "url", + "--disappear.timer", "0", + "--disappear.type", "type", + "--name.name", "name", + "--parent", "{}", + "--participant", "string", + "--room-id", "room_id", + "--topic.topic", "topic", + "--type", "channel", + "--username", "username", + ) + }) + + t.Run("piping data", func(t *testing.T) { + // Test piping YAML data over stdin + pipeData := []byte("" + + "avatar:\n" + + " url: url\n" + + "disappear:\n" + + " timer: 0\n" + + " type: type\n" + + "name:\n" + + " name: name\n" + + "parent: {}\n" + + "participants:\n" + + " - string\n" + + "room_id: room_id\n" + + "topic:\n" + + " topic: topic\n" + + "type: channel\n" + + "username: username\n") + mocktest.TestRunMockTestWithPipeAndFlags( + t, pipeData, + "--access-token", "string", + "matrix:bridges:rooms", "create-group", + "--bridge-id", "bridgeID", + "--group-type", "groupType", + "--login-id", "bcc68892-b180-414f-9516-b4aadf7d0496", + ) + }) +} diff --git a/pkg/cmd/matrixbridgeuser.go b/pkg/cmd/matrixbridgeuser.go new file mode 100644 index 00000000..4596e5cd --- /dev/null +++ b/pkg/cmd/matrixbridgeuser.go @@ -0,0 +1,168 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "context" + "fmt" + + "github.com/beeper/desktop-api-cli/internal/apiquery" + "github.com/beeper/desktop-api-cli/internal/requestflag" + "github.com/beeper/desktop-api-go/v5" + "github.com/beeper/desktop-api-go/v5/option" + "github.com/tidwall/gjson" + "github.com/urfave/cli/v3" +) + +var matrixBridgesUsersResolve = cli.Command{ + Name: "resolve", + Usage: "Resolve an identifier to a user on the remote network.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "bridge-id", + Required: true, + PathParam: "bridgeID", + }, + &requestflag.Flag[string]{ + Name: "identifier", + Required: true, + PathParam: "identifier", + }, + &requestflag.Flag[string]{ + Name: "login-id", + Usage: "An optional explicit login ID to do the action through.", + QueryPath: "login_id", + }, + }, + Action: handleMatrixBridgesUsersResolve, + HideHelpCommand: true, +} + +var matrixBridgesUsersSearch = cli.Command{ + Name: "search", + Usage: "Search for users on the remote network", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "bridge-id", + Required: true, + PathParam: "bridgeID", + }, + &requestflag.Flag[string]{ + Name: "login-id", + Usage: "An optional explicit login ID to do the action through.", + QueryPath: "login_id", + }, + &requestflag.Flag[string]{ + Name: "query", + Usage: "The search query to send to the remote network", + BodyPath: "query", + }, + }, + Action: handleMatrixBridgesUsersSearch, + HideHelpCommand: true, +} + +func handleMatrixBridgesUsersResolve(ctx context.Context, cmd *cli.Command) error { + client := beeperdesktopapi.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("identifier") && len(unusedArgs) > 0 { + cmd.Set("identifier", unusedArgs[0]) + unusedArgs = unusedArgs[1:] + } + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + + options, err := flagOptions( + cmd, + apiquery.NestedQueryFormatBrackets, + apiquery.ArrayQueryFormatRepeat, + EmptyBody, + false, + ) + if err != nil { + return err + } + + params := beeperdesktopapi.MatrixBridgeUserResolveParams{ + BridgeID: cmd.Value("bridge-id").(string), + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Matrix.Bridges.Users.Resolve( + ctx, + cmd.Value("identifier").(string), + params, + options..., + ) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := "json" + explicitFormat := cmd.Root().IsSet("format") + if explicitFormat { + format = cmd.Root().String("format") + } + transform := cmd.Root().String("transform") + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "matrix:bridges:users resolve", + Transform: transform, + }) +} + +func handleMatrixBridgesUsersSearch(ctx context.Context, cmd *cli.Command) error { + client := beeperdesktopapi.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("bridge-id") && len(unusedArgs) > 0 { + cmd.Set("bridge-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.ArrayQueryFormatRepeat, + ApplicationJSON, + false, + ) + if err != nil { + return err + } + + params := beeperdesktopapi.MatrixBridgeUserSearchParams{} + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Matrix.Bridges.Users.Search( + ctx, + cmd.Value("bridge-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: "matrix:bridges:users search", + Transform: transform, + }) +} diff --git a/pkg/cmd/matrixbridgeuser_test.go b/pkg/cmd/matrixbridgeuser_test.go new file mode 100644 index 00000000..c3f11aa1 --- /dev/null +++ b/pkg/cmd/matrixbridgeuser_test.go @@ -0,0 +1,47 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "testing" + + "github.com/beeper/desktop-api-cli/internal/mocktest" +) + +func TestMatrixBridgesUsersResolve(t *testing.T) { + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--access-token", "string", + "matrix:bridges:users", "resolve", + "--bridge-id", "bridgeID", + "--identifier", "identifier", + "--login-id", "bcc68892-b180-414f-9516-b4aadf7d0496", + ) + }) +} + +func TestMatrixBridgesUsersSearch(t *testing.T) { + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--access-token", "string", + "matrix:bridges:users", "search", + "--bridge-id", "bridgeID", + "--login-id", "bcc68892-b180-414f-9516-b4aadf7d0496", + "--query", "query", + ) + }) + + t.Run("piping data", func(t *testing.T) { + // Test piping YAML data over stdin + pipeData := []byte("query: query") + mocktest.TestRunMockTestWithPipeAndFlags( + t, pipeData, + "--access-token", "string", + "matrix:bridges:users", "search", + "--bridge-id", "bridgeID", + "--login-id", "bcc68892-b180-414f-9516-b4aadf7d0496", + ) + }) +} diff --git a/pkg/cmd/matrixroom.go b/pkg/cmd/matrixroom.go new file mode 100644 index 00000000..f870eaae --- /dev/null +++ b/pkg/cmd/matrixroom.go @@ -0,0 +1,337 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "context" + "fmt" + + "github.com/beeper/desktop-api-cli/internal/apiquery" + "github.com/beeper/desktop-api-cli/internal/requestflag" + "github.com/beeper/desktop-api-go/v5" + "github.com/beeper/desktop-api-go/v5/option" + "github.com/tidwall/gjson" + "github.com/urfave/cli/v3" +) + +var matrixRoomsCreate = requestflag.WithInnerFlags(cli.Command{ + Name: "create", + Usage: "Create a new room with various configuration options.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[any]{ + Name: "creation-content", + Usage: "Extra keys, such as `m.federate`, to be added to the content\nof the [`m.room.create`](https://spec.matrix.org/v1.18/client-server-api/#mroomcreate) event.\n\nThe server will overwrite the following\nkeys: `creator`, `room_version`. Future versions of the specification\nmay allow the server to overwrite other keys.\n\nWhen using the `trusted_private_chat` preset, the server SHOULD combine\n`additional_creators` specified here and the `invite` array into the\neventual `m.room.create` event's `additional_creators`, deduplicating\nbetween the two parameters.", + BodyPath: "creation_content", + }, + &requestflag.Flag[[]map[string]any]{ + Name: "initial-state", + Usage: "A list of state events to set in the new room. This allows\nthe user to override the default state events set in the new\nroom. The expected format of the state events are an object\nwith type, state_key and content keys set.\n\nTakes precedence over events set by `preset`, but gets\noverridden by `name` and `topic` keys.", + BodyPath: "initial_state", + }, + &requestflag.Flag[[]string]{ + Name: "invite", + Usage: "A list of user IDs to invite to the room. This will tell the\nserver to invite everyone in the list to the newly created room.", + BodyPath: "invite", + }, + &requestflag.Flag[[]map[string]any]{ + Name: "invite-3pid", + Usage: "A list of objects representing third-party IDs to invite into\nthe room.", + BodyPath: "invite_3pid", + }, + &requestflag.Flag[bool]{ + Name: "is-direct", + Usage: "This flag makes the server set the `is_direct` flag on the\n`m.room.member` events sent to the users in `invite` and\n`invite_3pid`. See [Direct Messaging](https://spec.matrix.org/v1.18/client-server-api/#direct-messaging) for more information.", + BodyPath: "is_direct", + }, + &requestflag.Flag[string]{ + Name: "name", + Usage: "If this is included, an [`m.room.name`](https://spec.matrix.org/v1.18/client-server-api/#mroomname) event\nwill be sent into the room to indicate the name for the room.\nThis overwrites any [`m.room.name`](https://spec.matrix.org/v1.18/client-server-api/#mroomname)\nevent in `initial_state`.", + BodyPath: "name", + }, + &requestflag.Flag[any]{ + Name: "power-level-content-override", + Usage: "The power level content to override in the default power level\nevent. This object is applied on top of the generated\n[`m.room.power_levels`](https://spec.matrix.org/v1.18/client-server-api/#mroompower_levels)\nevent content prior to it being sent to the room. Defaults to\noverriding nothing.", + BodyPath: "power_level_content_override", + }, + &requestflag.Flag[string]{ + Name: "preset", + Usage: "Convenience parameter for setting various default state events\nbased on a preset.\n\nIf unspecified, the server should use the `visibility` to determine\nwhich preset to use. A visibility of `public` equates to a preset of\n`public_chat` and `private` visibility equates to a preset of\n`private_chat`.", + BodyPath: "preset", + }, + &requestflag.Flag[string]{ + Name: "room-alias-name", + Usage: "The desired room alias **local part**. If this is included, a\nroom alias will be created and mapped to the newly created\nroom. The alias will belong on the *same* homeserver which\ncreated the room. For example, if this was set to \"foo\" and\nsent to the homeserver \"example.com\" the complete room alias\nwould be `#foo:example.com`.\n\nThe complete room alias will become the canonical alias for\nthe room and an `m.room.canonical_alias` event will be sent\ninto the room.", + BodyPath: "room_alias_name", + }, + &requestflag.Flag[string]{ + Name: "room-version", + Usage: "The room version to set for the room. If not provided, the homeserver is\nto use its configured default. If provided, the homeserver will return a\n400 error with the errcode `M_UNSUPPORTED_ROOM_VERSION` if it does not\nsupport the room version.", + BodyPath: "room_version", + }, + &requestflag.Flag[string]{ + Name: "topic", + Usage: "If this is included, an [`m.room.topic`](https://spec.matrix.org/v1.18/client-server-api/#mroomtopic)\nevent with a `text/plain` mimetype will be sent into the room\nto indicate the topic for the room. This overwrites any\n[`m.room.topic`](https://spec.matrix.org/v1.18/client-server-api/#mroomtopic) event in `initial_state`.", + BodyPath: "topic", + }, + &requestflag.Flag[string]{ + Name: "visibility", + Usage: "The room's visibility in the server's\n[published room directory](https://spec.matrix.org/v1.18/client-server-api#published-room-directory).\nDefaults to `private`.", + BodyPath: "visibility", + }, + }, + Action: handleMatrixRoomsCreate, + HideHelpCommand: true, +}, map[string][]requestflag.HasOuterFlag{ + "initial-state": { + &requestflag.InnerFlag[any]{ + Name: "initial-state.content", + Usage: "The content of the event.", + InnerField: "content", + }, + &requestflag.InnerFlag[string]{ + Name: "initial-state.type", + Usage: "The type of event to send.", + InnerField: "type", + }, + &requestflag.InnerFlag[string]{ + Name: "initial-state.state-key", + Usage: "The state_key of the state event. Defaults to an empty string.", + InnerField: "state_key", + }, + }, + "invite-3pid": { + &requestflag.InnerFlag[string]{ + Name: "invite-3pid.address", + Usage: "The invitee's third-party identifier.", + InnerField: "address", + }, + &requestflag.InnerFlag[string]{ + Name: "invite-3pid.id-access-token", + Usage: "An access token previously registered with the identity server. Servers\ncan treat this as optional to distinguish between r0.5-compatible clients\nand this specification version.", + InnerField: "id_access_token", + }, + &requestflag.InnerFlag[string]{ + Name: "invite-3pid.id-server", + Usage: "The hostname+port of the identity server which should be used for third-party identifier lookups.", + InnerField: "id_server", + }, + &requestflag.InnerFlag[string]{ + Name: "invite-3pid.medium", + Usage: "The kind of address being passed in the address field, for example `email`\n(see [the list of recognised values](https://spec.matrix.org/v1.18/appendices/#3pid-types)).", + InnerField: "medium", + }, + }, +}) + +var matrixRoomsJoin = requestflag.WithInnerFlags(cli.Command{ + Name: "join", + Usage: "_Note that this API takes either a room ID or alias, unlike_\n`/rooms/{roomId}/join`.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "room-id-or-alias", + Required: true, + PathParam: "roomIdOrAlias", + }, + &requestflag.Flag[[]string]{ + Name: "via", + Usage: "The servers to attempt to join the room through. One of the servers\nmust be participating in the room.", + QueryPath: "via", + }, + &requestflag.Flag[string]{ + Name: "reason", + Usage: "Optional reason to be included as the `reason` on the subsequent\nmembership event.", + BodyPath: "reason", + }, + &requestflag.Flag[map[string]any]{ + Name: "third-party-signed", + Usage: "A signature of an `m.third_party_invite` token to prove that this user\nowns a third-party identity which has been invited to the room.", + BodyPath: "third_party_signed", + }, + }, + Action: handleMatrixRoomsJoin, + HideHelpCommand: true, +}, map[string][]requestflag.HasOuterFlag{ + "third-party-signed": { + &requestflag.InnerFlag[string]{ + Name: "third-party-signed.token", + Usage: "The state key of the m.third_party_invite event.", + InnerField: "token", + }, + &requestflag.InnerFlag[string]{ + Name: "third-party-signed.mxid", + Usage: "The Matrix ID of the invitee.", + InnerField: "mxid", + }, + &requestflag.InnerFlag[string]{ + Name: "third-party-signed.sender", + Usage: "The Matrix ID of the user who issued the invite.", + InnerField: "sender", + }, + &requestflag.InnerFlag[map[string]any]{ + Name: "third-party-signed.signatures", + Usage: "A signatures object containing a signature of the entire signed object.", + InnerField: "signatures", + }, + }, +}) + +var matrixRoomsLeave = cli.Command{ + Name: "leave", + Usage: "This API stops a user participating in a particular room.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "room-id", + Required: true, + PathParam: "roomId", + }, + &requestflag.Flag[string]{ + Name: "reason", + Usage: "Optional reason to be included as the `reason` on the subsequent\nmembership event.", + BodyPath: "reason", + }, + }, + Action: handleMatrixRoomsLeave, + HideHelpCommand: true, +} + +func handleMatrixRoomsCreate(ctx context.Context, cmd *cli.Command) error { + client := beeperdesktopapi.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.ArrayQueryFormatRepeat, + ApplicationJSON, + false, + ) + if err != nil { + return err + } + + params := beeperdesktopapi.MatrixRoomNewParams{} + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Matrix.Rooms.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: "matrix:rooms create", + Transform: transform, + }) +} + +func handleMatrixRoomsJoin(ctx context.Context, cmd *cli.Command) error { + client := beeperdesktopapi.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("room-id-or-alias") && len(unusedArgs) > 0 { + cmd.Set("room-id-or-alias", unusedArgs[0]) + unusedArgs = unusedArgs[1:] + } + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + + options, err := flagOptions( + cmd, + apiquery.NestedQueryFormatBrackets, + apiquery.ArrayQueryFormatRepeat, + ApplicationJSON, + false, + ) + if err != nil { + return err + } + + params := beeperdesktopapi.MatrixRoomJoinParams{} + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Matrix.Rooms.Join( + ctx, + cmd.Value("room-id-or-alias").(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: "matrix:rooms join", + Transform: transform, + }) +} + +func handleMatrixRoomsLeave(ctx context.Context, cmd *cli.Command) error { + client := beeperdesktopapi.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("room-id") && len(unusedArgs) > 0 { + cmd.Set("room-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.ArrayQueryFormatRepeat, + ApplicationJSON, + false, + ) + if err != nil { + return err + } + + params := beeperdesktopapi.MatrixRoomLeaveParams{} + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Matrix.Rooms.Leave( + ctx, + cmd.Value("room-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: "matrix:rooms leave", + Transform: transform, + }) +} diff --git a/pkg/cmd/matrixroom_test.go b/pkg/cmd/matrixroom_test.go new file mode 100644 index 00000000..241ec53a --- /dev/null +++ b/pkg/cmd/matrixroom_test.go @@ -0,0 +1,168 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "testing" + + "github.com/beeper/desktop-api-cli/internal/mocktest" + "github.com/beeper/desktop-api-cli/internal/requestflag" +) + +func TestMatrixRoomsCreate(t *testing.T) { + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--access-token", "string", + "matrix:rooms", "create", + "--creation-content", "{m.federate: false}", + "--initial-state", "{content: {}, type: type, state_key: state_key}", + "--invite", "string", + "--invite-3pid", "{address: cheeky@monkey.com, id_access_token: abc123_OpaqueString, id_server: matrix.org, medium: email}", + "--is-direct=true", + "--name", "The Grand Duke Pub", + "--power-level-content-override", "{}", + "--preset", "public_chat", + "--room-alias-name", "thepub", + "--room-version", "1", + "--topic", "All about happy hour", + "--visibility", "public", + ) + }) + + t.Run("inner flags", func(t *testing.T) { + // Check that inner flags have been set up correctly + requestflag.CheckInnerFlags(matrixRoomsCreate) + + // Alternative argument passing style using inner flags + mocktest.TestRunMockTestWithFlags( + t, + "--access-token", "string", + "matrix:rooms", "create", + "--creation-content", "{m.federate: false}", + "--initial-state.content", "{}", + "--initial-state.type", "type", + "--initial-state.state-key", "state_key", + "--invite", "string", + "--invite-3pid.address", "cheeky@monkey.com", + "--invite-3pid.id-access-token", "abc123_OpaqueString", + "--invite-3pid.id-server", "matrix.org", + "--invite-3pid.medium", "email", + "--is-direct=true", + "--name", "The Grand Duke Pub", + "--power-level-content-override", "{}", + "--preset", "public_chat", + "--room-alias-name", "thepub", + "--room-version", "1", + "--topic", "All about happy hour", + "--visibility", "public", + ) + }) + + t.Run("piping data", func(t *testing.T) { + // Test piping YAML data over stdin + pipeData := []byte("" + + "creation_content:\n" + + " m.federate: false\n" + + "initial_state:\n" + + " - content: {}\n" + + " type: type\n" + + " state_key: state_key\n" + + "invite:\n" + + " - string\n" + + "invite_3pid:\n" + + " - address: cheeky@monkey.com\n" + + " id_access_token: abc123_OpaqueString\n" + + " id_server: matrix.org\n" + + " medium: email\n" + + "is_direct: true\n" + + "name: The Grand Duke Pub\n" + + "power_level_content_override: {}\n" + + "preset: public_chat\n" + + "room_alias_name: thepub\n" + + "room_version: '1'\n" + + "topic: All about happy hour\n" + + "visibility: public\n") + mocktest.TestRunMockTestWithPipeAndFlags( + t, pipeData, + "--access-token", "string", + "matrix:rooms", "create", + ) + }) +} + +func TestMatrixRoomsJoin(t *testing.T) { + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--access-token", "string", + "matrix:rooms", "join", + "--room-id-or-alias", "!monkeys:matrix.org", + "--via", "string", + "--reason", "Looking for support", + "--third-party-signed", "{token: random8nonce, mxid: bob, sender: alice, signatures: {example.org: {ed25519:0: some9signature}}}", + ) + }) + + t.Run("inner flags", func(t *testing.T) { + // Check that inner flags have been set up correctly + requestflag.CheckInnerFlags(matrixRoomsJoin) + + // Alternative argument passing style using inner flags + mocktest.TestRunMockTestWithFlags( + t, + "--access-token", "string", + "matrix:rooms", "join", + "--room-id-or-alias", "!monkeys:matrix.org", + "--via", "string", + "--reason", "Looking for support", + "--third-party-signed.token", "random8nonce", + "--third-party-signed.mxid", "bob", + "--third-party-signed.sender", "alice", + "--third-party-signed.signatures", "{example.org: {ed25519:0: some9signature}}", + ) + }) + + t.Run("piping data", func(t *testing.T) { + // Test piping YAML data over stdin + pipeData := []byte("" + + "reason: Looking for support\n" + + "third_party_signed:\n" + + " token: random8nonce\n" + + " mxid: bob\n" + + " sender: alice\n" + + " signatures:\n" + + " example.org:\n" + + " ed25519:0: some9signature\n") + mocktest.TestRunMockTestWithPipeAndFlags( + t, pipeData, + "--access-token", "string", + "matrix:rooms", "join", + "--room-id-or-alias", "!monkeys:matrix.org", + "--via", "string", + ) + }) +} + +func TestMatrixRoomsLeave(t *testing.T) { + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--access-token", "string", + "matrix:rooms", "leave", + "--room-id", "!nkl290a:matrix.org", + "--reason", "Saying farewell - thanks for the support!", + ) + }) + + t.Run("piping data", func(t *testing.T) { + // Test piping YAML data over stdin + pipeData := []byte("reason: Saying farewell - thanks for the support!") + mocktest.TestRunMockTestWithPipeAndFlags( + t, pipeData, + "--access-token", "string", + "matrix:rooms", "leave", + "--room-id", "!nkl290a:matrix.org", + ) + }) +} diff --git a/pkg/cmd/matrixroomaccountdata.go b/pkg/cmd/matrixroomaccountdata.go new file mode 100644 index 00000000..0d786806 --- /dev/null +++ b/pkg/cmd/matrixroomaccountdata.go @@ -0,0 +1,177 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "context" + "fmt" + + "github.com/beeper/desktop-api-cli/internal/apiquery" + "github.com/beeper/desktop-api-cli/internal/requestflag" + "github.com/beeper/desktop-api-go/v5" + "github.com/beeper/desktop-api-go/v5/option" + "github.com/tidwall/gjson" + "github.com/urfave/cli/v3" +) + +var matrixRoomsAccountDataRetrieve = cli.Command{ + Name: "retrieve", + Usage: "Get some account data for the client on a given room. This config is only\nvisible to the user that set the account data.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "user-id", + Required: true, + PathParam: "userId", + }, + &requestflag.Flag[string]{ + Name: "room-id", + Required: true, + PathParam: "roomId", + }, + &requestflag.Flag[string]{ + Name: "type", + Required: true, + PathParam: "type", + }, + }, + Action: handleMatrixRoomsAccountDataRetrieve, + HideHelpCommand: true, +} + +var matrixRoomsAccountDataUpdate = cli.Command{ + Name: "update", + Usage: "Set some account data for the client on a given room. This config is only\nvisible to the user that set the account data. The config will be delivered to\nclients in the per-room entries via\n[/sync](https://spec.matrix.org/v1.18/client-server-api/#get_matrixclientv3sync).", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "user-id", + Required: true, + PathParam: "userId", + }, + &requestflag.Flag[string]{ + Name: "room-id", + Required: true, + PathParam: "roomId", + }, + &requestflag.Flag[string]{ + Name: "type", + Required: true, + PathParam: "type", + }, + &requestflag.Flag[any]{ + Name: "body", + Required: true, + BodyRoot: true, + }, + }, + Action: handleMatrixRoomsAccountDataUpdate, + HideHelpCommand: true, +} + +func handleMatrixRoomsAccountDataRetrieve(ctx context.Context, cmd *cli.Command) error { + client := beeperdesktopapi.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("type") && len(unusedArgs) > 0 { + cmd.Set("type", unusedArgs[0]) + unusedArgs = unusedArgs[1:] + } + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + + options, err := flagOptions( + cmd, + apiquery.NestedQueryFormatBrackets, + apiquery.ArrayQueryFormatRepeat, + EmptyBody, + false, + ) + if err != nil { + return err + } + + params := beeperdesktopapi.MatrixRoomAccountDataGetParams{ + UserID: cmd.Value("user-id").(string), + RoomID: cmd.Value("room-id").(string), + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Matrix.Rooms.AccountData.Get( + ctx, + cmd.Value("type").(string), + params, + options..., + ) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := "json" + explicitFormat := cmd.Root().IsSet("format") + if explicitFormat { + format = cmd.Root().String("format") + } + transform := cmd.Root().String("transform") + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "matrix:rooms:account-data retrieve", + Transform: transform, + }) +} + +func handleMatrixRoomsAccountDataUpdate(ctx context.Context, cmd *cli.Command) error { + client := beeperdesktopapi.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("type") && len(unusedArgs) > 0 { + cmd.Set("type", unusedArgs[0]) + unusedArgs = unusedArgs[1:] + } + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + + options, err := flagOptions( + cmd, + apiquery.NestedQueryFormatBrackets, + apiquery.ArrayQueryFormatRepeat, + ApplicationJSON, + false, + ) + if err != nil { + return err + } + + params := beeperdesktopapi.MatrixRoomAccountDataUpdateParams{ + UserID: cmd.Value("user-id").(string), + RoomID: cmd.Value("room-id").(string), + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Matrix.Rooms.AccountData.Update( + ctx, + cmd.Value("type").(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: "matrix:rooms:account-data update", + Transform: transform, + }) +} diff --git a/pkg/cmd/matrixroomaccountdata_test.go b/pkg/cmd/matrixroomaccountdata_test.go new file mode 100644 index 00000000..08d8c650 --- /dev/null +++ b/pkg/cmd/matrixroomaccountdata_test.go @@ -0,0 +1,49 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "testing" + + "github.com/beeper/desktop-api-cli/internal/mocktest" +) + +func TestMatrixRoomsAccountDataRetrieve(t *testing.T) { + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--access-token", "string", + "matrix:rooms:account-data", "retrieve", + "--user-id", "@alice:example.com", + "--room-id", "!726s6s6q:example.com", + "--type", "org.example.custom.room.config", + ) + }) +} + +func TestMatrixRoomsAccountDataUpdate(t *testing.T) { + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--access-token", "string", + "matrix:rooms:account-data", "update", + "--user-id", "@alice:example.com", + "--room-id", "!726s6s6q:example.com", + "--type", "org.example.custom.room.config", + "--body", "{custom_account_data_key: custom_account_data_value}", + ) + }) + + t.Run("piping data", func(t *testing.T) { + // Test piping YAML data over stdin + pipeData := []byte("custom_account_data_key: custom_account_data_value") + mocktest.TestRunMockTestWithPipeAndFlags( + t, pipeData, + "--access-token", "string", + "matrix:rooms:account-data", "update", + "--user-id", "@alice:example.com", + "--room-id", "!726s6s6q:example.com", + "--type", "org.example.custom.room.config", + ) + }) +} diff --git a/pkg/cmd/matrixroomevent.go b/pkg/cmd/matrixroomevent.go new file mode 100644 index 00000000..072ae10a --- /dev/null +++ b/pkg/cmd/matrixroomevent.go @@ -0,0 +1,89 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "context" + "fmt" + + "github.com/beeper/desktop-api-cli/internal/apiquery" + "github.com/beeper/desktop-api-cli/internal/requestflag" + "github.com/beeper/desktop-api-go/v5" + "github.com/beeper/desktop-api-go/v5/option" + "github.com/tidwall/gjson" + "github.com/urfave/cli/v3" +) + +var matrixRoomsEventsRetrieve = cli.Command{ + Name: "retrieve", + Usage: "Get a single event based on `roomId/eventId`. You must have permission to\nretrieve this event e.g. by being a member in the room for this event.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "room-id", + Required: true, + PathParam: "roomId", + }, + &requestflag.Flag[string]{ + Name: "event-id", + Required: true, + PathParam: "eventId", + }, + }, + Action: handleMatrixRoomsEventsRetrieve, + HideHelpCommand: true, +} + +func handleMatrixRoomsEventsRetrieve(ctx context.Context, cmd *cli.Command) error { + client := beeperdesktopapi.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("event-id") && len(unusedArgs) > 0 { + cmd.Set("event-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.ArrayQueryFormatRepeat, + EmptyBody, + false, + ) + if err != nil { + return err + } + + params := beeperdesktopapi.MatrixRoomEventGetParams{ + RoomID: cmd.Value("room-id").(string), + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Matrix.Rooms.Events.Get( + ctx, + cmd.Value("event-id").(string), + params, + options..., + ) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := "json" + explicitFormat := cmd.Root().IsSet("format") + if explicitFormat { + format = cmd.Root().String("format") + } + transform := cmd.Root().String("transform") + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "matrix:rooms:events retrieve", + Transform: transform, + }) +} diff --git a/pkg/cmd/matrixroomevent_test.go b/pkg/cmd/matrixroomevent_test.go new file mode 100644 index 00000000..31d44839 --- /dev/null +++ b/pkg/cmd/matrixroomevent_test.go @@ -0,0 +1,21 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "testing" + + "github.com/beeper/desktop-api-cli/internal/mocktest" +) + +func TestMatrixRoomsEventsRetrieve(t *testing.T) { + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--access-token", "string", + "matrix:rooms:events", "retrieve", + "--room-id", "!636q39766251:matrix.org", + "--event-id", "$asfDuShaf7Gafaw:matrix.org", + ) + }) +} diff --git a/pkg/cmd/matrixroomstate.go b/pkg/cmd/matrixroomstate.go new file mode 100644 index 00000000..f09b1002 --- /dev/null +++ b/pkg/cmd/matrixroomstate.go @@ -0,0 +1,160 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "context" + "fmt" + + "github.com/beeper/desktop-api-cli/internal/apiquery" + "github.com/beeper/desktop-api-cli/internal/requestflag" + "github.com/beeper/desktop-api-go/v5" + "github.com/beeper/desktop-api-go/v5/option" + "github.com/tidwall/gjson" + "github.com/urfave/cli/v3" +) + +var matrixRoomsStateRetrieve = cli.Command{ + Name: "retrieve", + Usage: "Looks up the contents of a state event in a room. If the user is joined to the\nroom then the state is taken from the current state of the room. If the user has\nleft the room then the state is taken from the state of the room when they left.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "room-id", + Required: true, + PathParam: "roomId", + }, + &requestflag.Flag[string]{ + Name: "event-type", + Required: true, + PathParam: "eventType", + }, + &requestflag.Flag[string]{ + Name: "state-key", + Required: true, + PathParam: "stateKey", + }, + &requestflag.Flag[string]{ + Name: "format", + Usage: "The format to use for the returned data. `content` (the default) will\nreturn only the content of the state event. `event` will return the entire\nevent in the usual format suitable for clients, including fields like event\nID, sender and timestamp.", + QueryPath: "format", + }, + }, + Action: handleMatrixRoomsStateRetrieve, + HideHelpCommand: true, +} + +var matrixRoomsStateList = cli.Command{ + Name: "list", + Usage: "Get the state events for the current state of a room.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "room-id", + Required: true, + PathParam: "roomId", + }, + }, + Action: handleMatrixRoomsStateList, + HideHelpCommand: true, +} + +func handleMatrixRoomsStateRetrieve(ctx context.Context, cmd *cli.Command) error { + client := beeperdesktopapi.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("state-key") && len(unusedArgs) > 0 { + cmd.Set("state-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.ArrayQueryFormatRepeat, + EmptyBody, + false, + ) + if err != nil { + return err + } + + params := beeperdesktopapi.MatrixRoomStateGetParams{ + RoomID: cmd.Value("room-id").(string), + EventType: cmd.Value("event-type").(string), + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Matrix.Rooms.State.Get( + ctx, + cmd.Value("state-key").(string), + params, + options..., + ) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := "json" + explicitFormat := cmd.Root().IsSet("format") + if explicitFormat { + format = cmd.Root().String("format") + } + transform := cmd.Root().String("transform") + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "matrix:rooms:state retrieve", + Transform: transform, + }) +} + +func handleMatrixRoomsStateList(ctx context.Context, cmd *cli.Command) error { + client := beeperdesktopapi.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("room-id") && len(unusedArgs) > 0 { + cmd.Set("room-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.ArrayQueryFormatRepeat, + EmptyBody, + false, + ) + if err != nil { + return err + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Matrix.Rooms.State.List(ctx, cmd.Value("room-id").(string), options...) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := "json" + explicitFormat := cmd.Root().IsSet("format") + if explicitFormat { + format = cmd.Root().String("format") + } + transform := cmd.Root().String("transform") + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "matrix:rooms:state list", + Transform: transform, + }) +} diff --git a/pkg/cmd/matrixroomstate_test.go b/pkg/cmd/matrixroomstate_test.go new file mode 100644 index 00000000..8ff3c71e --- /dev/null +++ b/pkg/cmd/matrixroomstate_test.go @@ -0,0 +1,34 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "testing" + + "github.com/beeper/desktop-api-cli/internal/mocktest" +) + +func TestMatrixRoomsStateRetrieve(t *testing.T) { + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--access-token", "string", + "matrix:rooms:state", "retrieve", + "--room-id", "!636q39766251:example.com", + "--event-type", "m.room.name", + "--state-key", "state_key", + "--format", "content", + ) + }) +} + +func TestMatrixRoomsStateList(t *testing.T) { + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--access-token", "string", + "matrix:rooms:state", "list", + "--room-id", "!636q39766251:example.com", + ) + }) +} diff --git a/pkg/cmd/matrixuser.go b/pkg/cmd/matrixuser.go new file mode 100644 index 00000000..50d1fb04 --- /dev/null +++ b/pkg/cmd/matrixuser.go @@ -0,0 +1,75 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "context" + "fmt" + + "github.com/beeper/desktop-api-cli/internal/apiquery" + "github.com/beeper/desktop-api-cli/internal/requestflag" + "github.com/beeper/desktop-api-go/v5" + "github.com/beeper/desktop-api-go/v5/option" + "github.com/tidwall/gjson" + "github.com/urfave/cli/v3" +) + +var matrixUsersRetrieveProfile = cli.Command{ + Name: "retrieve-profile", + Usage: "Get the complete profile for a user.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "user-id", + Required: true, + PathParam: "userId", + }, + }, + Action: handleMatrixUsersRetrieveProfile, + HideHelpCommand: true, +} + +func handleMatrixUsersRetrieveProfile(ctx context.Context, cmd *cli.Command) error { + client := beeperdesktopapi.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("user-id") && len(unusedArgs) > 0 { + cmd.Set("user-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.ArrayQueryFormatRepeat, + EmptyBody, + false, + ) + if err != nil { + return err + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Matrix.Users.GetProfile(ctx, cmd.Value("user-id").(string), options...) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := "json" + explicitFormat := cmd.Root().IsSet("format") + if explicitFormat { + format = cmd.Root().String("format") + } + transform := cmd.Root().String("transform") + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "matrix:users retrieve-profile", + Transform: transform, + }) +} diff --git a/pkg/cmd/matrixuser_test.go b/pkg/cmd/matrixuser_test.go new file mode 100644 index 00000000..6c48b75b --- /dev/null +++ b/pkg/cmd/matrixuser_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/beeper/desktop-api-cli/internal/mocktest" +) + +func TestMatrixUsersRetrieveProfile(t *testing.T) { + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--access-token", "string", + "matrix:users", "retrieve-profile", + "--user-id", "@alice:example.com", + ) + }) +} diff --git a/pkg/cmd/matrixuseraccountdata.go b/pkg/cmd/matrixuseraccountdata.go new file mode 100644 index 00000000..dd0347b9 --- /dev/null +++ b/pkg/cmd/matrixuseraccountdata.go @@ -0,0 +1,165 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "context" + "fmt" + + "github.com/beeper/desktop-api-cli/internal/apiquery" + "github.com/beeper/desktop-api-cli/internal/requestflag" + "github.com/beeper/desktop-api-go/v5" + "github.com/beeper/desktop-api-go/v5/option" + "github.com/tidwall/gjson" + "github.com/urfave/cli/v3" +) + +var matrixUsersAccountDataRetrieve = cli.Command{ + Name: "retrieve", + Usage: "Get some account data for the client. This config is only visible to the user\nthat set the account data.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "user-id", + Required: true, + PathParam: "userId", + }, + &requestflag.Flag[string]{ + Name: "type", + Required: true, + PathParam: "type", + }, + }, + Action: handleMatrixUsersAccountDataRetrieve, + HideHelpCommand: true, +} + +var matrixUsersAccountDataUpdate = cli.Command{ + Name: "update", + Usage: "Set some account data for the client. This config is only visible to the user\nthat set the account data. The config will be available to clients through the\ntop-level `account_data` field in the homeserver response to\n[/sync](https://spec.matrix.org/v1.18/client-server-api/#get_matrixclientv3sync).", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "user-id", + Required: true, + PathParam: "userId", + }, + &requestflag.Flag[string]{ + Name: "type", + Required: true, + PathParam: "type", + }, + &requestflag.Flag[any]{ + Name: "body", + Required: true, + BodyRoot: true, + }, + }, + Action: handleMatrixUsersAccountDataUpdate, + HideHelpCommand: true, +} + +func handleMatrixUsersAccountDataRetrieve(ctx context.Context, cmd *cli.Command) error { + client := beeperdesktopapi.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("type") && len(unusedArgs) > 0 { + cmd.Set("type", unusedArgs[0]) + unusedArgs = unusedArgs[1:] + } + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + + options, err := flagOptions( + cmd, + apiquery.NestedQueryFormatBrackets, + apiquery.ArrayQueryFormatRepeat, + EmptyBody, + false, + ) + if err != nil { + return err + } + + params := beeperdesktopapi.MatrixUserAccountDataGetParams{ + UserID: cmd.Value("user-id").(string), + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Matrix.Users.AccountData.Get( + ctx, + cmd.Value("type").(string), + params, + options..., + ) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := "json" + explicitFormat := cmd.Root().IsSet("format") + if explicitFormat { + format = cmd.Root().String("format") + } + transform := cmd.Root().String("transform") + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "matrix:users:account-data retrieve", + Transform: transform, + }) +} + +func handleMatrixUsersAccountDataUpdate(ctx context.Context, cmd *cli.Command) error { + client := beeperdesktopapi.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("type") && len(unusedArgs) > 0 { + cmd.Set("type", unusedArgs[0]) + unusedArgs = unusedArgs[1:] + } + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + + options, err := flagOptions( + cmd, + apiquery.NestedQueryFormatBrackets, + apiquery.ArrayQueryFormatRepeat, + ApplicationJSON, + false, + ) + if err != nil { + return err + } + + params := beeperdesktopapi.MatrixUserAccountDataUpdateParams{ + UserID: cmd.Value("user-id").(string), + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Matrix.Users.AccountData.Update( + ctx, + cmd.Value("type").(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: "matrix:users:account-data update", + Transform: transform, + }) +} diff --git a/pkg/cmd/matrixuseraccountdata_test.go b/pkg/cmd/matrixuseraccountdata_test.go new file mode 100644 index 00000000..af509366 --- /dev/null +++ b/pkg/cmd/matrixuseraccountdata_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/beeper/desktop-api-cli/internal/mocktest" +) + +func TestMatrixUsersAccountDataRetrieve(t *testing.T) { + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--access-token", "string", + "matrix:users:account-data", "retrieve", + "--user-id", "@alice:example.com", + "--type", "org.example.custom.config", + ) + }) +} + +func TestMatrixUsersAccountDataUpdate(t *testing.T) { + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--access-token", "string", + "matrix:users:account-data", "update", + "--user-id", "@alice:example.com", + "--type", "org.example.custom.config", + "--body", "{custom_account_data_key: custom_config_value}", + ) + }) + + t.Run("piping data", func(t *testing.T) { + // Test piping YAML data over stdin + pipeData := []byte("custom_account_data_key: custom_config_value") + mocktest.TestRunMockTestWithPipeAndFlags( + t, pipeData, + "--access-token", "string", + "matrix:users:account-data", "update", + "--user-id", "@alice:example.com", + "--type", "org.example.custom.config", + ) + }) +} From b23ec6ef972e12e2b27e95bfeec2741566a64182 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 16 May 2026 20:21:44 +0000 Subject: [PATCH 51/54] Update Desktop API Stainless config --- .stats.yml | 4 ++-- README.md | 8 ++++---- pkg/cmd/cmd.go | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.stats.yml b/.stats.yml index b63d20d3..62239e78 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 72 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper/beeper-desktop-api-de1370e6a3183044fa135a886d2ee8f779d5e86228cdbd503d553b4c13cc7cbe.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper/beeper-desktop-api-8ba2755730c4180ec88f92a300948445d7917898abfc912ca3fa6adc766a7520.yml openapi_spec_hash: 30b435d7585d8b6951610e7147369779 -config_hash: 683b13ea6fb6aa9d6b1b8814cca24f1c +config_hash: a53888715ed00d433e5a5dafab9f7b9f diff --git a/README.md b/README.md index 6bca0848..be90c673 100644 --- a/README.md +++ b/README.md @@ -66,13 +66,13 @@ For details about specific commands, use the `--help` flag. ### Environment variables -| Environment variable | Description | Required | -| --------------------- | ----------------------------------------------------------------------------------------------------- | -------- | -| `BEEPER_ACCESS_TOKEN` | Bearer access token obtained via OAuth2 PKCE flow or created in-app. Required for all API operations. | yes | +| Environment variable | Description | Required | Default value | +| --------------------- | --------------------------------------------------------------------------------------------------------------- | -------- | ------------- | +| `BEEPER_ACCESS_TOKEN` | Bearer access token obtained via OAuth2 PKCE flow or created in-app. Required for authenticated API operations. | no | `null` | ### Global flags -- `--access-token` - Bearer access token obtained via OAuth2 PKCE flow or created in-app. Required for all API operations. (can also be set with `BEEPER_ACCESS_TOKEN` env var) +- `--access-token` - Bearer access token obtained via OAuth2 PKCE flow or created in-app. Required for authenticated API operations. (can also be set with `BEEPER_ACCESS_TOKEN` env var) - `--help` - Show command line usage - `--debug` - Enable debug logging (includes HTTP request/response details) - `--version`, `-v` - Show the CLI version diff --git a/pkg/cmd/cmd.go b/pkg/cmd/cmd.go index 089dd61c..e45969cc 100644 --- a/pkg/cmd/cmd.go +++ b/pkg/cmd/cmd.go @@ -80,7 +80,7 @@ func init() { }, &requestflag.Flag[string]{ Name: "access-token", - Usage: "Bearer access token obtained via OAuth2 PKCE flow or created in-app. Required for all API operations.", + Usage: "Bearer access token obtained via OAuth2 PKCE flow or created in-app. Required for authenticated API operations.", Sources: cli.EnvVars("BEEPER_ACCESS_TOKEN"), }, }, From dcd13484ebd39b36889b7ab284b78c2f4c619fd4 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sun, 17 May 2026 16:32:29 +0000 Subject: [PATCH 52/54] feat(api): add accounts/bridges endpoints, login-flows/sessions, remove app/matrix APIs --- .github/workflows/publish-release.yml | 8 +- .gitignore | 2 +- .goreleaser.yml | 46 +- .stats.yml | 8 +- README.md | 30 +- .../main.go | 0 internal/mocktest/mocktest.go | 4 +- pkg/cmd/account.go | 66 +- pkg/cmd/account_test.go | 11 + pkg/cmd/accountcontact.go | 14 +- pkg/cmd/app.go | 65 -- pkg/cmd/appe2eerecoverycode.go | 120 ---- pkg/cmd/appe2eerecoverycode_test.go | 40 -- pkg/cmd/appe2eerecoverycodereset.go | 128 ---- pkg/cmd/appe2eerecoverycodereset_test.go | 51 -- pkg/cmd/appe2eeverificationqr.go | 130 ---- pkg/cmd/appe2eeverificationqr_test.go | 41 -- pkg/cmd/appe2eeverificationsa.go | 131 ---- pkg/cmd/appe2eeverificationsa_test.go | 31 - pkg/cmd/applogin.go | 264 -------- pkg/cmd/applogin_test.go | 95 --- pkg/cmd/asset.go | 8 +- pkg/cmd/beeperdesktopapi.go | 13 +- pkg/cmd/bridge.go | 124 +++- pkg/cmd/bridge_test.go | 22 + ...bridgecapability.go => bridgeloginflow.go} | 20 +- .../{app_test.go => bridgeloginflow_test.go} | 5 +- ...everification.go => bridgeloginsession.go} | 127 ++-- ...ion_test.go => bridgeloginsession_test.go} | 46 +- pkg/cmd/bridgeloginsessionstep.go | 116 ++++ pkg/cmd/bridgeloginsessionstep_test.go | 44 ++ pkg/cmd/chat.go | 49 +- pkg/cmd/chatmessagereaction.go | 4 +- pkg/cmd/chatreminder.go | 8 +- pkg/cmd/cmd.go | 172 +----- pkg/cmd/info.go | 7 +- pkg/cmd/matrixbridgeauth.go | 584 ------------------ pkg/cmd/matrixbridgeauth_test.go | 134 ---- pkg/cmd/matrixbridgecapability_test.go | 20 - pkg/cmd/matrixbridgecontact.go | 87 --- pkg/cmd/matrixbridgecontact_test.go | 21 - pkg/cmd/matrixbridgeroom.go | 240 ------- pkg/cmd/matrixbridgeroom_test.go | 98 --- pkg/cmd/matrixbridgeuser.go | 168 ----- pkg/cmd/matrixbridgeuser_test.go | 47 -- pkg/cmd/matrixroom.go | 337 ---------- pkg/cmd/matrixroom_test.go | 168 ----- pkg/cmd/matrixroomaccountdata.go | 177 ------ pkg/cmd/matrixroomaccountdata_test.go | 49 -- pkg/cmd/matrixroomevent.go | 89 --- pkg/cmd/matrixroomevent_test.go | 21 - pkg/cmd/matrixroomstate.go | 160 ----- pkg/cmd/matrixroomstate_test.go | 34 - pkg/cmd/matrixuser.go | 75 --- pkg/cmd/matrixuser_test.go | 20 - pkg/cmd/matrixuseraccountdata.go | 165 ----- pkg/cmd/matrixuseraccountdata_test.go | 46 -- pkg/cmd/message.go | 31 +- scripts/build | 4 +- scripts/run | 2 +- scripts/utils/upload-artifact.sh | 2 +- 61 files changed, 588 insertions(+), 4241 deletions(-) rename cmd/{beeper-desktop-cli => beeper-desktop}/main.go (100%) delete mode 100644 pkg/cmd/app.go delete mode 100644 pkg/cmd/appe2eerecoverycode.go delete mode 100644 pkg/cmd/appe2eerecoverycode_test.go delete mode 100644 pkg/cmd/appe2eerecoverycodereset.go delete mode 100644 pkg/cmd/appe2eerecoverycodereset_test.go delete mode 100644 pkg/cmd/appe2eeverificationqr.go delete mode 100644 pkg/cmd/appe2eeverificationqr_test.go delete mode 100644 pkg/cmd/appe2eeverificationsa.go delete mode 100644 pkg/cmd/appe2eeverificationsa_test.go delete mode 100644 pkg/cmd/applogin.go delete mode 100644 pkg/cmd/applogin_test.go rename pkg/cmd/{matrixbridgecapability.go => bridgeloginflow.go} (74%) rename pkg/cmd/{app_test.go => bridgeloginflow_test.go} (72%) rename pkg/cmd/{appe2eeverification.go => bridgeloginsession.go} (52%) rename pkg/cmd/{appe2eeverification_test.go => bridgeloginsession_test.go} (50%) create mode 100644 pkg/cmd/bridgeloginsessionstep.go create mode 100644 pkg/cmd/bridgeloginsessionstep_test.go delete mode 100644 pkg/cmd/matrixbridgeauth.go delete mode 100644 pkg/cmd/matrixbridgeauth_test.go delete mode 100644 pkg/cmd/matrixbridgecapability_test.go delete mode 100644 pkg/cmd/matrixbridgecontact.go delete mode 100644 pkg/cmd/matrixbridgecontact_test.go delete mode 100644 pkg/cmd/matrixbridgeroom.go delete mode 100644 pkg/cmd/matrixbridgeroom_test.go delete mode 100644 pkg/cmd/matrixbridgeuser.go delete mode 100644 pkg/cmd/matrixbridgeuser_test.go delete mode 100644 pkg/cmd/matrixroom.go delete mode 100644 pkg/cmd/matrixroom_test.go delete mode 100644 pkg/cmd/matrixroomaccountdata.go delete mode 100644 pkg/cmd/matrixroomaccountdata_test.go delete mode 100644 pkg/cmd/matrixroomevent.go delete mode 100644 pkg/cmd/matrixroomevent_test.go delete mode 100644 pkg/cmd/matrixroomstate.go delete mode 100644 pkg/cmd/matrixroomstate_test.go delete mode 100644 pkg/cmd/matrixuser.go delete mode 100644 pkg/cmd/matrixuser_test.go delete mode 100644 pkg/cmd/matrixuseraccountdata.go delete mode 100644 pkg/cmd/matrixuseraccountdata_test.go diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index 058282ae..6b026e01 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -29,10 +29,4 @@ jobs: version: latest args: release --clean env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - HOMEBREW_TAP_GITHUB_TOKEN: ${{ secrets.HOMEBREW_TAP_GITHUB_TOKEN }} - MACOS_SIGN_P12: ${{ secrets.MACOS_SIGN_P12 }} - MACOS_SIGN_PASSWORD: ${{ secrets.MACOS_SIGN_PASSWORD }} - MACOS_NOTARY_ISSUER_ID: ${{ secrets.MACOS_NOTARY_ISSUER_ID }} - MACOS_NOTARY_KEY_ID: ${{ secrets.MACOS_NOTARY_KEY_ID }} - MACOS_NOTARY_KEY: ${{ secrets.MACOS_NOTARY_KEY }} \ No newline at end of file + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 268ede01..a9020e84 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ .prism.log .stdy.log dist/ -/beeper-desktop-cli +/beeper-desktop *.exe diff --git a/.goreleaser.yml b/.goreleaser.yml index b4184080..9ff2b450 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -1,20 +1,20 @@ -project_name: beeper-desktop-cli +project_name: beeper-desktop version: 2 before: hooks: - mkdir -p completions - - sh -c "go run ./cmd/beeper-desktop-cli/main.go @completion bash > completions/beeper-desktop-cli.bash" - - sh -c "go run ./cmd/beeper-desktop-cli/main.go @completion zsh > completions/beeper-desktop-cli.zsh" - - sh -c "go run ./cmd/beeper-desktop-cli/main.go @completion fish > completions/beeper-desktop-cli.fish" - - sh -c "go run ./cmd/beeper-desktop-cli/main.go @manpages -o man" + - sh -c "go run ./cmd/beeper-desktop/main.go @completion bash > completions/beeper-desktop.bash" + - sh -c "go run ./cmd/beeper-desktop/main.go @completion zsh > completions/beeper-desktop.zsh" + - sh -c "go run ./cmd/beeper-desktop/main.go @completion fish > completions/beeper-desktop.fish" + - sh -c "go run ./cmd/beeper-desktop/main.go @manpages -o man" builds: - id: macos goos: [darwin] goarch: [amd64, arm64] binary: '{{ .ProjectName }}' - main: ./cmd/beeper-desktop-cli/main.go + main: ./cmd/beeper-desktop/main.go mod_timestamp: '{{ .CommitTimestamp }}' ldflags: - '-s -w -X main.version={{.Version}} -X main.commit={{.Commit}}' @@ -25,7 +25,7 @@ builds: env: - CGO_ENABLED=0 binary: '{{ .ProjectName }}' - main: ./cmd/beeper-desktop-cli/main.go + main: ./cmd/beeper-desktop/main.go mod_timestamp: '{{ .CommitTimestamp }}' ldflags: - '-s -w -X main.version={{.Version}} -X main.commit={{.Commit}}' @@ -34,7 +34,7 @@ builds: goos: [windows] goarch: ['386', amd64, arm64] binary: '{{ .ProjectName }}' - main: ./cmd/beeper-desktop-cli/main.go + main: ./cmd/beeper-desktop/main.go mod_timestamp: '{{ .CommitTimestamp }}' ldflags: - '-s -w -X main.version={{.Version}} -X main.commit={{.Commit}}' @@ -78,33 +78,3 @@ nfpms: contents: - src: man/man1/*.1.gz dst: /usr/share/man/man1/ -homebrew_casks: - - name: beeper-desktop-cli - repository: - owner: beeper - name: homebrew-tap - token: "{{ .Env.HOMEBREW_TAP_GITHUB_TOKEN }}" - homepage: https://developers.beeper.com/desktop-api/ - description: CLI for Beeper Desktop API - license: MIT - binary: "beeper-desktop-cli" - completions: - bash: "completions/beeper-desktop-cli.bash" - zsh: "completions/beeper-desktop-cli.zsh" - fish: "completions/beeper-desktop-cli.fish" - manpages: - - man/man1/beeper-desktop-cli.1.gz - -notarize: - macos: - - enabled: '{{ isEnvSet "MACOS_SIGN_P12" }}' - ids: [macos] - - sign: - certificate: "{{.Env.MACOS_SIGN_P12}}" - password: "{{.Env.MACOS_SIGN_PASSWORD}}" - - notarize: - issuer_id: "{{.Env.MACOS_NOTARY_ISSUER_ID}}" - key_id: "{{.Env.MACOS_NOTARY_KEY_ID}}" - key: "{{.Env.MACOS_NOTARY_KEY}}" diff --git a/.stats.yml b/.stats.yml index 62239e78..f8d62378 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 72 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper/beeper-desktop-api-8ba2755730c4180ec88f92a300948445d7917898abfc912ca3fa6adc766a7520.yml -openapi_spec_hash: 30b435d7585d8b6951610e7147369779 -config_hash: a53888715ed00d433e5a5dafab9f7b9f +configured_endpoints: 39 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper/beeper-desktop-api-87df69f641d994f09669f77093988df0b13da380d36076964d4a2563e9ce202e.yml +openapi_spec_hash: 9de80d05f7562b7ecd07c466f0fdf58b +config_hash: 2ebcc80e2cbd2342e132f4474ec24212 diff --git a/README.md b/README.md index be90c673..4f516d08 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Beeper Desktop CLI +# CLI for Beeper Desktop The official CLI for the [Beeper Desktop REST API](https://developers.beeper.com/desktop-api/). @@ -6,18 +6,12 @@ The official CLI for the [Beeper Desktop REST API](https://developers.beeper.com ## Installation -### Installing with Homebrew - -```sh -brew install beeper/tap/beeper-desktop-cli -``` - ### Installing with Go To test or install the CLI locally, you need [Go](https://go.dev/doc/install) version 1.22 or later installed. ```sh -go install 'github.com/beeper/desktop-api-cli/cmd/beeper-desktop-cli@latest' +go install 'github.com/beeper/desktop-api-cli/cmd/beeper-desktop@latest' ``` Once you have run `go install`, the binary is placed in your Go bin directory: @@ -48,11 +42,11 @@ After cloning the git repository for this project, you can use the The CLI follows a resource-based command structure: ```sh -beeper-desktop-cli [resource] [flags...] +beeper-desktop [resource] [flags...] ``` ```sh -beeper-desktop-cli chats search \ +beeper-desktop chats search \ --access-token 'My Access Token' \ --account-id matrix \ --account-id discordgo \ @@ -66,9 +60,9 @@ For details about specific commands, use the `--help` flag. ### Environment variables -| Environment variable | Description | Required | Default value | -| --------------------- | --------------------------------------------------------------------------------------------------------------- | -------- | ------------- | -| `BEEPER_ACCESS_TOKEN` | Bearer access token obtained via OAuth2 PKCE flow or created in-app. Required for authenticated API operations. | no | `null` | +| Environment variable | Description | Required | +| --------------------- | --------------------------------------------------------------------------------------------------------------- | -------- | +| `BEEPER_ACCESS_TOKEN` | Bearer access token obtained via OAuth2 PKCE flow or created in-app. Required for authenticated API operations. | yes | ### Global flags @@ -87,15 +81,15 @@ For details about specific commands, use the `--help` flag. To pass files to your API, you can use the `@myfile.ext` syntax: ```bash -beeper-desktop-cli --arg @abe.jpg +beeper-desktop --arg @abe.jpg ``` Files can also be passed inside JSON or YAML blobs: ```bash -beeper-desktop-cli --arg '{image: "@abe.jpg"}' +beeper-desktop --arg '{image: "@abe.jpg"}' # Equivalent: -beeper-desktop-cli < < --username '\@abe' +beeper-desktop --username '\@abe' ``` #### Explicit encoding @@ -119,7 +113,7 @@ base64-encoding). Note that absolute paths will begin with `@file://` or `@data://`, followed by a third `/` (for example, `@file:///tmp/file.txt`). ```bash -beeper-desktop-cli --arg @data://file.txt +beeper-desktop --arg @data://file.txt ``` ## Linking different Go SDK versions diff --git a/cmd/beeper-desktop-cli/main.go b/cmd/beeper-desktop/main.go similarity index 100% rename from cmd/beeper-desktop-cli/main.go rename to cmd/beeper-desktop/main.go diff --git a/internal/mocktest/mocktest.go b/internal/mocktest/mocktest.go index e51ceb52..950ca59f 100644 --- a/internal/mocktest/mocktest.go +++ b/internal/mocktest/mocktest.go @@ -79,11 +79,11 @@ func TestRunMockTestWithPipeAndFlags(t *testing.T, pipeData []byte, args ...stri _, filename, _, ok := runtime.Caller(0) require.True(t, ok, "Could not get current file path") dirPath := filepath.Dir(filename) - project := filepath.Join(dirPath, "..", "..", "cmd", "beeper-desktop-cli") + project := filepath.Join(dirPath, "..", "..", "cmd", "beeper-desktop") args = append([]string{"run", project, "--base-url", mockServerURL.String()}, args...) - t.Logf("Testing command: go run ./cmd/beeper-desktop-cli %s", strings.Join(args[2:], " ")) + t.Logf("Testing command: go run ./cmd/beeper-desktop %s", strings.Join(args[2:], " ")) cmd := exec.Command("go", args...) cmd.Stdin = bytes.NewReader(pipeData) diff --git a/pkg/cmd/account.go b/pkg/cmd/account.go index 284208c4..99974ede 100644 --- a/pkg/cmd/account.go +++ b/pkg/cmd/account.go @@ -7,21 +7,80 @@ import ( "fmt" "github.com/beeper/desktop-api-cli/internal/apiquery" + "github.com/beeper/desktop-api-cli/internal/requestflag" "github.com/beeper/desktop-api-go/v5" "github.com/beeper/desktop-api-go/v5/option" "github.com/tidwall/gjson" "github.com/urfave/cli/v3" ) +var accountsRetrieve = cli.Command{ + Name: "retrieve", + Usage: "Get one chat account connected to this Beeper Client API server.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "account-id", + Usage: "Account ID this resource belongs to.", + Required: true, + PathParam: "accountID", + }, + }, + Action: handleAccountsRetrieve, + HideHelpCommand: true, +} + var accountsList = cli.Command{ Name: "list", - Usage: "List Chat Accounts connected to this Beeper Desktop instance, including bridge\nmetadata and network identity.", + Usage: "List chat accounts connected to this Beeper Client API server, including bridge,\nnetwork, user identity, and connection status.", Suggest: true, Flags: []cli.Flag{}, Action: handleAccountsList, HideHelpCommand: true, } +func handleAccountsRetrieve(ctx context.Context, cmd *cli.Command) error { + client := beeperdesktopapi.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("account-id") && len(unusedArgs) > 0 { + cmd.Set("account-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.ArrayQueryFormatRepeat, + EmptyBody, + false, + ) + if err != nil { + return err + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Accounts.Get(ctx, cmd.Value("account-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: "accounts retrieve", + Transform: transform, + }) +} + func handleAccountsList(ctx context.Context, cmd *cli.Command) error { client := beeperdesktopapi.NewClient(getDefaultRequestOptions(cmd)...) unusedArgs := cmd.Args().Slice() @@ -49,11 +108,8 @@ func handleAccountsList(ctx context.Context, cmd *cli.Command) error { } obj := gjson.ParseBytes(res) - format := "json" + format := cmd.Root().String("format") explicitFormat := cmd.Root().IsSet("format") - if explicitFormat { - format = cmd.Root().String("format") - } transform := cmd.Root().String("transform") return ShowJSON(obj, ShowJSONOpts{ ExplicitFormat: explicitFormat, diff --git a/pkg/cmd/account_test.go b/pkg/cmd/account_test.go index cc8844fc..e2564c62 100644 --- a/pkg/cmd/account_test.go +++ b/pkg/cmd/account_test.go @@ -8,6 +8,17 @@ import ( "github.com/beeper/desktop-api-cli/internal/mocktest" ) +func TestAccountsRetrieve(t *testing.T) { + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--access-token", "string", + "accounts", "retrieve", + "--account-id", "accountID", + ) + }) +} + func TestAccountsList(t *testing.T) { t.Run("regular flags", func(t *testing.T) { mocktest.TestRunMockTestWithFlags( diff --git a/pkg/cmd/accountcontact.go b/pkg/cmd/accountcontact.go index 40e36b64..cb6fbe1f 100644 --- a/pkg/cmd/accountcontact.go +++ b/pkg/cmd/accountcontact.go @@ -43,7 +43,7 @@ var accountsContactsList = cli.Command{ }, &requestflag.Flag[string]{ Name: "query", - Usage: "Optional search query for blended contact lookup.", + Usage: "Optional search query for contact lookup.", QueryPath: "query", }, &requestflag.Flag[int64]{ @@ -68,7 +68,7 @@ var accountsContactsSearch = cli.Command{ }, &requestflag.Flag[string]{ Name: "query", - Usage: "Text to search users by. Network-specific behavior.", + Usage: "Text to search contacts by. Matching behavior depends on the network.", Required: true, QueryPath: "query", }, @@ -101,11 +101,8 @@ func handleAccountsContactsList(ctx context.Context, cmd *cli.Command) error { params := beeperdesktopapi.AccountContactListParams{} - format := "json" + format := cmd.Root().String("format") explicitFormat := cmd.Root().IsSet("format") - if explicitFormat { - format = cmd.Root().String("format") - } transform := cmd.Root().String("transform") if format == "raw" { var res []byte @@ -185,11 +182,8 @@ func handleAccountsContactsSearch(ctx context.Context, cmd *cli.Command) error { } obj := gjson.ParseBytes(res) - format := "json" + format := cmd.Root().String("format") explicitFormat := cmd.Root().IsSet("format") - if explicitFormat { - format = cmd.Root().String("format") - } transform := cmd.Root().String("transform") return ShowJSON(obj, ShowJSONOpts{ ExplicitFormat: explicitFormat, diff --git a/pkg/cmd/app.go b/pkg/cmd/app.go deleted file mode 100644 index aa20f30c..00000000 --- a/pkg/cmd/app.go +++ /dev/null @@ -1,65 +0,0 @@ -// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -package cmd - -import ( - "context" - "fmt" - - "github.com/beeper/desktop-api-cli/internal/apiquery" - "github.com/beeper/desktop-api-go/v5" - "github.com/beeper/desktop-api-go/v5/option" - "github.com/tidwall/gjson" - "github.com/urfave/cli/v3" -) - -var appStatus = cli.Command{ - Name: "status", - Usage: "Return the current Beeper Desktop sign-in and encrypted messaging setup state.\nThis endpoint is public before sign-in so apps can discover that login is\nneeded; after sign-in, pass a read token.", - Suggest: true, - Flags: []cli.Flag{}, - Action: handleAppStatus, - HideHelpCommand: true, -} - -func handleAppStatus(ctx context.Context, cmd *cli.Command) error { - client := beeperdesktopapi.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.ArrayQueryFormatRepeat, - EmptyBody, - false, - ) - if err != nil { - return err - } - - var res []byte - options = append(options, option.WithResponseBodyInto(&res)) - _, err = client.App.Status(ctx, options...) - if err != nil { - return err - } - - obj := gjson.ParseBytes(res) - format := "json" - explicitFormat := cmd.Root().IsSet("format") - if explicitFormat { - format = cmd.Root().String("format") - } - transform := cmd.Root().String("transform") - return ShowJSON(obj, ShowJSONOpts{ - ExplicitFormat: explicitFormat, - Format: format, - RawOutput: cmd.Root().Bool("raw-output"), - Title: "app status", - Transform: transform, - }) -} diff --git a/pkg/cmd/appe2eerecoverycode.go b/pkg/cmd/appe2eerecoverycode.go deleted file mode 100644 index 6279a233..00000000 --- a/pkg/cmd/appe2eerecoverycode.go +++ /dev/null @@ -1,120 +0,0 @@ -// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -package cmd - -import ( - "context" - "fmt" - - "github.com/beeper/desktop-api-cli/internal/apiquery" - "github.com/beeper/desktop-api-cli/internal/requestflag" - "github.com/beeper/desktop-api-go/v5" - "github.com/beeper/desktop-api-go/v5/option" - "github.com/tidwall/gjson" - "github.com/urfave/cli/v3" -) - -var appE2eeRecoveryCodeMarkBackedUp = cli.Command{ - Name: "mark-backed-up", - Usage: "Record that the user saved their recovery key.", - Suggest: true, - Flags: []cli.Flag{}, - Action: handleAppE2eeRecoveryCodeMarkBackedUp, - HideHelpCommand: true, -} - -var appE2eeRecoveryCodeVerify = cli.Command{ - Name: "verify", - Usage: "Unlock encrypted messages with the user recovery key.", - Suggest: true, - Flags: []cli.Flag{ - &requestflag.Flag[string]{ - Name: "recovery-code", - Usage: "Recovery key saved by the user.", - Required: true, - BodyPath: "recoveryCode", - }, - }, - Action: handleAppE2eeRecoveryCodeVerify, - HideHelpCommand: true, -} - -func handleAppE2eeRecoveryCodeMarkBackedUp(ctx context.Context, cmd *cli.Command) error { - client := beeperdesktopapi.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.ArrayQueryFormatRepeat, - EmptyBody, - false, - ) - if err != nil { - return err - } - - var res []byte - options = append(options, option.WithResponseBodyInto(&res)) - _, err = client.App.E2ee.RecoveryCode.MarkBackedUp(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: "app:e2ee:recovery-code mark-backed-up", - Transform: transform, - }) -} - -func handleAppE2eeRecoveryCodeVerify(ctx context.Context, cmd *cli.Command) error { - client := beeperdesktopapi.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.ArrayQueryFormatRepeat, - ApplicationJSON, - false, - ) - if err != nil { - return err - } - - params := beeperdesktopapi.AppE2eeRecoveryCodeVerifyParams{} - - var res []byte - options = append(options, option.WithResponseBodyInto(&res)) - _, err = client.App.E2ee.RecoveryCode.Verify(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: "app:e2ee:recovery-code verify", - Transform: transform, - }) -} diff --git a/pkg/cmd/appe2eerecoverycode_test.go b/pkg/cmd/appe2eerecoverycode_test.go deleted file mode 100644 index c460860a..00000000 --- a/pkg/cmd/appe2eerecoverycode_test.go +++ /dev/null @@ -1,40 +0,0 @@ -// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -package cmd - -import ( - "testing" - - "github.com/beeper/desktop-api-cli/internal/mocktest" -) - -func TestAppE2eeRecoveryCodeMarkBackedUp(t *testing.T) { - t.Run("regular flags", func(t *testing.T) { - mocktest.TestRunMockTestWithFlags( - t, - "--access-token", "string", - "app:e2ee:recovery-code", "mark-backed-up", - ) - }) -} - -func TestAppE2eeRecoveryCodeVerify(t *testing.T) { - t.Run("regular flags", func(t *testing.T) { - mocktest.TestRunMockTestWithFlags( - t, - "--access-token", "string", - "app:e2ee:recovery-code", "verify", - "--recovery-code", "x", - ) - }) - - t.Run("piping data", func(t *testing.T) { - // Test piping YAML data over stdin - pipeData := []byte("recoveryCode: x") - mocktest.TestRunMockTestWithPipeAndFlags( - t, pipeData, - "--access-token", "string", - "app:e2ee:recovery-code", "verify", - ) - }) -} diff --git a/pkg/cmd/appe2eerecoverycodereset.go b/pkg/cmd/appe2eerecoverycodereset.go deleted file mode 100644 index 1226ae86..00000000 --- a/pkg/cmd/appe2eerecoverycodereset.go +++ /dev/null @@ -1,128 +0,0 @@ -// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -package cmd - -import ( - "context" - "fmt" - - "github.com/beeper/desktop-api-cli/internal/apiquery" - "github.com/beeper/desktop-api-cli/internal/requestflag" - "github.com/beeper/desktop-api-go/v5" - "github.com/beeper/desktop-api-go/v5/option" - "github.com/tidwall/gjson" - "github.com/urfave/cli/v3" -) - -var appE2eeRecoveryCodeResetCreate = cli.Command{ - Name: "create", - Usage: "Create a new recovery key when the user cannot use the existing one.", - Suggest: true, - Flags: []cli.Flag{ - &requestflag.Flag[string]{ - Name: "recovery-code", - Usage: "Existing recovery key, if the user has it.", - BodyPath: "recoveryCode", - }, - }, - Action: handleAppE2eeRecoveryCodeResetCreate, - HideHelpCommand: true, -} - -var appE2eeRecoveryCodeResetConfirm = cli.Command{ - Name: "confirm", - Usage: "Confirm that the new recovery key should be used for this account.", - Suggest: true, - Flags: []cli.Flag{ - &requestflag.Flag[string]{ - Name: "recovery-code", - Usage: "New recovery key returned by the reset step.", - Required: true, - BodyPath: "recoveryCode", - }, - }, - Action: handleAppE2eeRecoveryCodeResetConfirm, - HideHelpCommand: true, -} - -func handleAppE2eeRecoveryCodeResetCreate(ctx context.Context, cmd *cli.Command) error { - client := beeperdesktopapi.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.ArrayQueryFormatRepeat, - ApplicationJSON, - false, - ) - if err != nil { - return err - } - - params := beeperdesktopapi.AppE2eeRecoveryCodeResetNewParams{} - - var res []byte - options = append(options, option.WithResponseBodyInto(&res)) - _, err = client.App.E2ee.RecoveryCode.Reset.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: "app:e2ee:recovery-code:reset create", - Transform: transform, - }) -} - -func handleAppE2eeRecoveryCodeResetConfirm(ctx context.Context, cmd *cli.Command) error { - client := beeperdesktopapi.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.ArrayQueryFormatRepeat, - ApplicationJSON, - false, - ) - if err != nil { - return err - } - - params := beeperdesktopapi.AppE2eeRecoveryCodeResetConfirmParams{} - - var res []byte - options = append(options, option.WithResponseBodyInto(&res)) - _, err = client.App.E2ee.RecoveryCode.Reset.Confirm(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: "app:e2ee:recovery-code:reset confirm", - Transform: transform, - }) -} diff --git a/pkg/cmd/appe2eerecoverycodereset_test.go b/pkg/cmd/appe2eerecoverycodereset_test.go deleted file mode 100644 index 75f8543e..00000000 --- a/pkg/cmd/appe2eerecoverycodereset_test.go +++ /dev/null @@ -1,51 +0,0 @@ -// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -package cmd - -import ( - "testing" - - "github.com/beeper/desktop-api-cli/internal/mocktest" -) - -func TestAppE2eeRecoveryCodeResetCreate(t *testing.T) { - t.Run("regular flags", func(t *testing.T) { - mocktest.TestRunMockTestWithFlags( - t, - "--access-token", "string", - "app:e2ee:recovery-code:reset", "create", - "--recovery-code", "recoveryCode", - ) - }) - - t.Run("piping data", func(t *testing.T) { - // Test piping YAML data over stdin - pipeData := []byte("recoveryCode: recoveryCode") - mocktest.TestRunMockTestWithPipeAndFlags( - t, pipeData, - "--access-token", "string", - "app:e2ee:recovery-code:reset", "create", - ) - }) -} - -func TestAppE2eeRecoveryCodeResetConfirm(t *testing.T) { - t.Run("regular flags", func(t *testing.T) { - mocktest.TestRunMockTestWithFlags( - t, - "--access-token", "string", - "app:e2ee:recovery-code:reset", "confirm", - "--recovery-code", "x", - ) - }) - - t.Run("piping data", func(t *testing.T) { - // Test piping YAML data over stdin - pipeData := []byte("recoveryCode: x") - mocktest.TestRunMockTestWithPipeAndFlags( - t, pipeData, - "--access-token", "string", - "app:e2ee:recovery-code:reset", "confirm", - ) - }) -} diff --git a/pkg/cmd/appe2eeverificationqr.go b/pkg/cmd/appe2eeverificationqr.go deleted file mode 100644 index 331aa1b8..00000000 --- a/pkg/cmd/appe2eeverificationqr.go +++ /dev/null @@ -1,130 +0,0 @@ -// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -package cmd - -import ( - "context" - "fmt" - - "github.com/beeper/desktop-api-cli/internal/apiquery" - "github.com/beeper/desktop-api-cli/internal/requestflag" - "github.com/beeper/desktop-api-go/v5" - "github.com/beeper/desktop-api-go/v5/option" - "github.com/tidwall/gjson" - "github.com/urfave/cli/v3" -) - -var appE2eeVerificationQrConfirmScanned = cli.Command{ - Name: "confirm-scanned", - Usage: "Confirm that another device scanned this device QR code.", - Suggest: true, - Flags: []cli.Flag{ - &requestflag.Flag[string]{ - Name: "verification-id", - Usage: "Verification ID.", - Required: true, - PathParam: "verificationID", - }, - }, - Action: handleAppE2eeVerificationQrConfirmScanned, - HideHelpCommand: true, -} - -var appE2eeVerificationQrScan = cli.Command{ - Name: "scan", - Usage: "Submit the QR code scanned from another signed-in device.", - Suggest: true, - Flags: []cli.Flag{ - &requestflag.Flag[string]{ - Name: "data", - Usage: "QR code payload scanned from the other device.", - Required: true, - BodyPath: "data", - }, - }, - Action: handleAppE2eeVerificationQrScan, - HideHelpCommand: true, -} - -func handleAppE2eeVerificationQrConfirmScanned(ctx context.Context, cmd *cli.Command) error { - client := beeperdesktopapi.NewClient(getDefaultRequestOptions(cmd)...) - unusedArgs := cmd.Args().Slice() - if !cmd.IsSet("verification-id") && len(unusedArgs) > 0 { - cmd.Set("verification-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.ArrayQueryFormatRepeat, - EmptyBody, - false, - ) - if err != nil { - return err - } - - var res []byte - options = append(options, option.WithResponseBodyInto(&res)) - _, err = client.App.E2ee.Verification.Qr.ConfirmScanned(ctx, cmd.Value("verification-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: "app:e2ee:verification:qr confirm-scanned", - Transform: transform, - }) -} - -func handleAppE2eeVerificationQrScan(ctx context.Context, cmd *cli.Command) error { - client := beeperdesktopapi.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.ArrayQueryFormatRepeat, - ApplicationJSON, - false, - ) - if err != nil { - return err - } - - params := beeperdesktopapi.AppE2eeVerificationQrScanParams{} - - var res []byte - options = append(options, option.WithResponseBodyInto(&res)) - _, err = client.App.E2ee.Verification.Qr.Scan(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: "app:e2ee:verification:qr scan", - Transform: transform, - }) -} diff --git a/pkg/cmd/appe2eeverificationqr_test.go b/pkg/cmd/appe2eeverificationqr_test.go deleted file mode 100644 index 635420dd..00000000 --- a/pkg/cmd/appe2eeverificationqr_test.go +++ /dev/null @@ -1,41 +0,0 @@ -// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -package cmd - -import ( - "testing" - - "github.com/beeper/desktop-api-cli/internal/mocktest" -) - -func TestAppE2eeVerificationQrConfirmScanned(t *testing.T) { - t.Run("regular flags", func(t *testing.T) { - mocktest.TestRunMockTestWithFlags( - t, - "--access-token", "string", - "app:e2ee:verification:qr", "confirm-scanned", - "--verification-id", "x", - ) - }) -} - -func TestAppE2eeVerificationQrScan(t *testing.T) { - t.Run("regular flags", func(t *testing.T) { - mocktest.TestRunMockTestWithFlags( - t, - "--access-token", "string", - "app:e2ee:verification:qr", "scan", - "--data", "x", - ) - }) - - t.Run("piping data", func(t *testing.T) { - // Test piping YAML data over stdin - pipeData := []byte("data: x") - mocktest.TestRunMockTestWithPipeAndFlags( - t, pipeData, - "--access-token", "string", - "app:e2ee:verification:qr", "scan", - ) - }) -} diff --git a/pkg/cmd/appe2eeverificationsa.go b/pkg/cmd/appe2eeverificationsa.go deleted file mode 100644 index afbda966..00000000 --- a/pkg/cmd/appe2eeverificationsa.go +++ /dev/null @@ -1,131 +0,0 @@ -// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -package cmd - -import ( - "context" - "fmt" - - "github.com/beeper/desktop-api-cli/internal/apiquery" - "github.com/beeper/desktop-api-cli/internal/requestflag" - "github.com/beeper/desktop-api-go/v5" - "github.com/beeper/desktop-api-go/v5/option" - "github.com/tidwall/gjson" - "github.com/urfave/cli/v3" -) - -var appE2eeVerificationSasConfirm = cli.Command{ - Name: "confirm", - Usage: "Confirm that the emoji or number sequence matches on both devices.", - Suggest: true, - Flags: []cli.Flag{ - &requestflag.Flag[string]{ - Name: "verification-id", - Usage: "Verification ID.", - Required: true, - PathParam: "verificationID", - }, - }, - Action: handleAppE2eeVerificationSasConfirm, - HideHelpCommand: true, -} - -var appE2eeVerificationSasStart = cli.Command{ - Name: "start", - Usage: "Start emoji comparison for device verification.", - Suggest: true, - Flags: []cli.Flag{ - &requestflag.Flag[string]{ - Name: "verification-id", - Usage: "Verification ID.", - Required: true, - PathParam: "verificationID", - }, - }, - Action: handleAppE2eeVerificationSasStart, - HideHelpCommand: true, -} - -func handleAppE2eeVerificationSasConfirm(ctx context.Context, cmd *cli.Command) error { - client := beeperdesktopapi.NewClient(getDefaultRequestOptions(cmd)...) - unusedArgs := cmd.Args().Slice() - if !cmd.IsSet("verification-id") && len(unusedArgs) > 0 { - cmd.Set("verification-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.ArrayQueryFormatRepeat, - EmptyBody, - false, - ) - if err != nil { - return err - } - - var res []byte - options = append(options, option.WithResponseBodyInto(&res)) - _, err = client.App.E2ee.Verification.Sas.Confirm(ctx, cmd.Value("verification-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: "app:e2ee:verification:sas confirm", - Transform: transform, - }) -} - -func handleAppE2eeVerificationSasStart(ctx context.Context, cmd *cli.Command) error { - client := beeperdesktopapi.NewClient(getDefaultRequestOptions(cmd)...) - unusedArgs := cmd.Args().Slice() - if !cmd.IsSet("verification-id") && len(unusedArgs) > 0 { - cmd.Set("verification-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.ArrayQueryFormatRepeat, - EmptyBody, - false, - ) - if err != nil { - return err - } - - var res []byte - options = append(options, option.WithResponseBodyInto(&res)) - _, err = client.App.E2ee.Verification.Sas.Start(ctx, cmd.Value("verification-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: "app:e2ee:verification:sas start", - Transform: transform, - }) -} diff --git a/pkg/cmd/appe2eeverificationsa_test.go b/pkg/cmd/appe2eeverificationsa_test.go deleted file mode 100644 index 526a6539..00000000 --- a/pkg/cmd/appe2eeverificationsa_test.go +++ /dev/null @@ -1,31 +0,0 @@ -// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -package cmd - -import ( - "testing" - - "github.com/beeper/desktop-api-cli/internal/mocktest" -) - -func TestAppE2eeVerificationSasConfirm(t *testing.T) { - t.Run("regular flags", func(t *testing.T) { - mocktest.TestRunMockTestWithFlags( - t, - "--access-token", "string", - "app:e2ee:verification:sas", "confirm", - "--verification-id", "x", - ) - }) -} - -func TestAppE2eeVerificationSasStart(t *testing.T) { - t.Run("regular flags", func(t *testing.T) { - mocktest.TestRunMockTestWithFlags( - t, - "--access-token", "string", - "app:e2ee:verification:sas", "start", - "--verification-id", "x", - ) - }) -} diff --git a/pkg/cmd/applogin.go b/pkg/cmd/applogin.go deleted file mode 100644 index 2c83b7f3..00000000 --- a/pkg/cmd/applogin.go +++ /dev/null @@ -1,264 +0,0 @@ -// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -package cmd - -import ( - "context" - "fmt" - - "github.com/beeper/desktop-api-cli/internal/apiquery" - "github.com/beeper/desktop-api-cli/internal/requestflag" - "github.com/beeper/desktop-api-go/v5" - "github.com/beeper/desktop-api-go/v5/option" - "github.com/tidwall/gjson" - "github.com/urfave/cli/v3" -) - -var appLoginEmail = cli.Command{ - Name: "email", - Usage: "Send a sign-in code to the user email address.", - Suggest: true, - Flags: []cli.Flag{ - &requestflag.Flag[string]{ - Name: "email", - Usage: "Email address to send the sign-in code to.", - Required: true, - BodyPath: "email", - }, - &requestflag.Flag[string]{ - Name: "request", - Usage: "Login request ID returned by the start step.", - Required: true, - BodyPath: "request", - }, - }, - Action: handleAppLoginEmail, - HideHelpCommand: true, -} - -var appLoginRegister = cli.Command{ - Name: "register", - Usage: "Create a Beeper account after the user chooses a username and accepts the Terms\nof Use.", - Suggest: true, - Flags: []cli.Flag{ - &requestflag.Flag[bool]{ - Name: "accept-terms", - Usage: "Confirms that the user accepted the Terms of Use and acknowledged the Privacy Policy.", - Required: true, - BodyPath: "acceptTerms", - }, - &requestflag.Flag[string]{ - Name: "lead-token", - Usage: "Registration token returned by Beeper.", - Required: true, - BodyPath: "leadToken", - }, - &requestflag.Flag[string]{ - Name: "request", - Usage: "Login request ID returned by the start step.", - Required: true, - BodyPath: "request", - }, - &requestflag.Flag[string]{ - Name: "username", - Usage: "Username selected by the user.", - Required: true, - BodyPath: "username", - }, - }, - Action: handleAppLoginRegister, - HideHelpCommand: true, -} - -var appLoginResponse = cli.Command{ - Name: "response", - Usage: "Finish sign-in with the code sent to the user email address. If the user needs a\nnew account, the response includes account creation copy and username\nsuggestions.", - Suggest: true, - Flags: []cli.Flag{ - &requestflag.Flag[string]{ - Name: "request", - Usage: "Login request ID returned by the start step.", - Required: true, - BodyPath: "request", - }, - &requestflag.Flag[string]{ - Name: "response", - Usage: "Sign-in code from the user email.", - Required: true, - BodyPath: "response", - }, - }, - Action: handleAppLoginResponse, - HideHelpCommand: true, -} - -var appLoginStart = cli.Command{ - Name: "start", - Usage: "Start a first-party Beeper Desktop sign-in session.", - Suggest: true, - Flags: []cli.Flag{}, - Action: handleAppLoginStart, - HideHelpCommand: true, -} - -func handleAppLoginEmail(ctx context.Context, cmd *cli.Command) error { - client := beeperdesktopapi.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.ArrayQueryFormatRepeat, - ApplicationJSON, - false, - ) - if err != nil { - return err - } - - params := beeperdesktopapi.AppLoginEmailParams{} - - var res []byte - options = append(options, option.WithResponseBodyInto(&res)) - _, err = client.App.Login.Email(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: "app:login email", - Transform: transform, - }) -} - -func handleAppLoginRegister(ctx context.Context, cmd *cli.Command) error { - client := beeperdesktopapi.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.ArrayQueryFormatRepeat, - ApplicationJSON, - false, - ) - if err != nil { - return err - } - - params := beeperdesktopapi.AppLoginRegisterParams{} - - var res []byte - options = append(options, option.WithResponseBodyInto(&res)) - _, err = client.App.Login.Register(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: "app:login register", - Transform: transform, - }) -} - -func handleAppLoginResponse(ctx context.Context, cmd *cli.Command) error { - client := beeperdesktopapi.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.ArrayQueryFormatRepeat, - ApplicationJSON, - false, - ) - if err != nil { - return err - } - - params := beeperdesktopapi.AppLoginResponseParams{} - - var res []byte - options = append(options, option.WithResponseBodyInto(&res)) - _, err = client.App.Login.Response(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: "app:login response", - Transform: transform, - }) -} - -func handleAppLoginStart(ctx context.Context, cmd *cli.Command) error { - client := beeperdesktopapi.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.ArrayQueryFormatRepeat, - EmptyBody, - false, - ) - if err != nil { - return err - } - - var res []byte - options = append(options, option.WithResponseBodyInto(&res)) - _, err = client.App.Login.Start(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: "app:login start", - Transform: transform, - }) -} diff --git a/pkg/cmd/applogin_test.go b/pkg/cmd/applogin_test.go deleted file mode 100644 index b4f866a0..00000000 --- a/pkg/cmd/applogin_test.go +++ /dev/null @@ -1,95 +0,0 @@ -// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -package cmd - -import ( - "testing" - - "github.com/beeper/desktop-api-cli/internal/mocktest" -) - -func TestAppLoginEmail(t *testing.T) { - t.Run("regular flags", func(t *testing.T) { - mocktest.TestRunMockTestWithFlags( - t, - "--access-token", "string", - "app:login", "email", - "--email", "dev@stainless.com", - "--request", "request", - ) - }) - - t.Run("piping data", func(t *testing.T) { - // Test piping YAML data over stdin - pipeData := []byte("" + - "email: dev@stainless.com\n" + - "request: request\n") - mocktest.TestRunMockTestWithPipeAndFlags( - t, pipeData, - "--access-token", "string", - "app:login", "email", - ) - }) -} - -func TestAppLoginRegister(t *testing.T) { - t.Run("regular flags", func(t *testing.T) { - mocktest.TestRunMockTestWithFlags( - t, - "--access-token", "string", - "app:login", "register", - "--accept-terms=true", - "--lead-token", "leadToken", - "--request", "request", - "--username", "x", - ) - }) - - t.Run("piping data", func(t *testing.T) { - // Test piping YAML data over stdin - pipeData := []byte("" + - "acceptTerms: true\n" + - "leadToken: leadToken\n" + - "request: request\n" + - "username: x\n") - mocktest.TestRunMockTestWithPipeAndFlags( - t, pipeData, - "--access-token", "string", - "app:login", "register", - ) - }) -} - -func TestAppLoginResponse(t *testing.T) { - t.Run("regular flags", func(t *testing.T) { - mocktest.TestRunMockTestWithFlags( - t, - "--access-token", "string", - "app:login", "response", - "--request", "request", - "--response", "response", - ) - }) - - t.Run("piping data", func(t *testing.T) { - // Test piping YAML data over stdin - pipeData := []byte("" + - "request: request\n" + - "response: response\n") - mocktest.TestRunMockTestWithPipeAndFlags( - t, pipeData, - "--access-token", "string", - "app:login", "response", - ) - }) -} - -func TestAppLoginStart(t *testing.T) { - t.Run("regular flags", func(t *testing.T) { - mocktest.TestRunMockTestWithFlags( - t, - "--access-token", "string", - "app:login", "start", - ) - }) -} diff --git a/pkg/cmd/asset.go b/pkg/cmd/asset.go index 2222a2c7..1ba30051 100644 --- a/pkg/cmd/asset.go +++ b/pkg/cmd/asset.go @@ -17,12 +17,12 @@ import ( var assetsDownload = cli.Command{ Name: "download", - Usage: "Download a Matrix file using its mxc:// or localmxc:// URL to the device running\nBeeper Desktop and return the local file URL.", + Usage: "Download a file from an mxc:// or localmxc:// URL to the device running the\nBeeper Client API and return the local file URL.", Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ Name: "url", - Usage: "Matrix content URL (mxc:// or localmxc://) for the file to download.", + Usage: "Beeper media URL (mxc:// or localmxc://) for the file to download.", Required: true, BodyPath: "url", }, @@ -54,7 +54,7 @@ var assetsServe = cli.Command{ var assetsUpload = cli.Command{ Name: "upload", - Usage: "Upload a file to a temporary location using multipart/form-data. Returns an\nuploadID that can be referenced when sending a message or materializing a draft\nattachment.", + Usage: "Upload a file to a temporary location using multipart/form-data. Returns an\nuploadID that can be referenced when sending a message or creating a draft\nattachment.", Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ @@ -81,7 +81,7 @@ var assetsUpload = cli.Command{ var assetsUploadBase64 = cli.Command{ Name: "upload-base64", - Usage: "Upload a file using a JSON body with base64-encoded content. Returns an uploadID\nthat can be referenced when sending a message or materializing a draft\nattachment. Alternative to the multipart upload endpoint.", + Usage: "Upload a file using a JSON body with base64-encoded content. Returns an uploadID\nthat can be referenced when sending a message or creating a draft attachment.\nAlternative to the multipart upload endpoint.", Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ diff --git a/pkg/cmd/beeperdesktopapi.go b/pkg/cmd/beeperdesktopapi.go index 88d2583f..aff44f20 100644 --- a/pkg/cmd/beeperdesktopapi.go +++ b/pkg/cmd/beeperdesktopapi.go @@ -16,7 +16,7 @@ import ( var focus = cli.Command{ Name: "focus", - Usage: "Focus Beeper Desktop and optionally navigate to a specific chat, message, or\npre-fill plain text and an image path.", + Usage: "Focus Beeper Desktop and optionally open a specific chat, jump to a message, or\npre-fill text and an image.", Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ @@ -26,7 +26,7 @@ var focus = cli.Command{ }, &requestflag.Flag[string]{ Name: "draft-attachment-path", - Usage: "Optional image path to populate in the message input field.", + Usage: "Optional local image path to populate in the message input field.", BodyPath: "draftAttachmentPath", }, &requestflag.Flag[string]{ @@ -46,12 +46,12 @@ var focus = cli.Command{ var search = cli.Command{ Name: "search", - Usage: "Returns matching chats, participant name matches in groups, and the first page\nof messages in one call. Paginate messages via search-messages. Paginate chats\nvia search-chats.", + Usage: "Return matching chats, participant matches in group chats, and the first page of\nmessage results in one call. Use the dedicated chat and message search endpoints\nfor pagination.", Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ Name: "query", - Usage: "User-typed search text. Literal word matching (non-semantic).", + Usage: "User-typed search text. Uses literal word matching.", Required: true, QueryPath: "query", }, @@ -130,11 +130,8 @@ func handleSearch(ctx context.Context, cmd *cli.Command) error { } obj := gjson.ParseBytes(res) - format := "json" + format := cmd.Root().String("format") explicitFormat := cmd.Root().IsSet("format") - if explicitFormat { - format = cmd.Root().String("format") - } transform := cmd.Root().String("transform") return ShowJSON(obj, ShowJSONOpts{ ExplicitFormat: explicitFormat, diff --git a/pkg/cmd/bridge.go b/pkg/cmd/bridge.go index a33054e6..22ce8f15 100644 --- a/pkg/cmd/bridge.go +++ b/pkg/cmd/bridge.go @@ -7,21 +7,96 @@ import ( "fmt" "github.com/beeper/desktop-api-cli/internal/apiquery" + "github.com/beeper/desktop-api-cli/internal/requestflag" "github.com/beeper/desktop-api-go/v5" "github.com/beeper/desktop-api-go/v5/option" "github.com/tidwall/gjson" "github.com/urfave/cli/v3" ) +var bridgesRetrieve = cli.Command{ + Name: "retrieve", + Usage: "Get one bridge, including the chat accounts connected through it.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "bridge-id", + Usage: "Bridge ID.", + Required: true, + PathParam: "bridgeID", + }, + }, + Action: handleBridgesRetrieve, + HideHelpCommand: true, +} + var bridgesList = cli.Command{ Name: "list", - Usage: "List bridge-backed account types that can be shown in add-account flows, grouped\nwith connected accounts that use the same Account schema as GET /v1/accounts.", + Usage: "List available bridges. A bridge is a chat-network connector that can connect or\nreconnect chat accounts. Connected accounts use the same Account schema as GET\n/v1/accounts.", Suggest: true, Flags: []cli.Flag{}, Action: handleBridgesList, HideHelpCommand: true, } +var bridgesRetrieveCapabilities = cli.Command{ + Name: "retrieve-capabilities", + Usage: "Get advanced network capabilities for a bridge. This endpoint is intended for\nclients that build custom connect or chat-creation flows.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "bridge-id", + Usage: "Bridge ID.", + Required: true, + PathParam: "bridgeID", + }, + }, + Action: handleBridgesRetrieveCapabilities, + HideHelpCommand: true, +} + +func handleBridgesRetrieve(ctx context.Context, cmd *cli.Command) error { + client := beeperdesktopapi.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("bridge-id") && len(unusedArgs) > 0 { + cmd.Set("bridge-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.ArrayQueryFormatRepeat, + EmptyBody, + false, + ) + if err != nil { + return err + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Bridges.Get(ctx, cmd.Value("bridge-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: "bridges retrieve", + Transform: transform, + }) +} + func handleBridgesList(ctx context.Context, cmd *cli.Command) error { client := beeperdesktopapi.NewClient(getDefaultRequestOptions(cmd)...) unusedArgs := cmd.Args().Slice() @@ -49,11 +124,8 @@ func handleBridgesList(ctx context.Context, cmd *cli.Command) error { } obj := gjson.ParseBytes(res) - format := "json" + format := cmd.Root().String("format") explicitFormat := cmd.Root().IsSet("format") - if explicitFormat { - format = cmd.Root().String("format") - } transform := cmd.Root().String("transform") return ShowJSON(obj, ShowJSONOpts{ ExplicitFormat: explicitFormat, @@ -63,3 +135,45 @@ func handleBridgesList(ctx context.Context, cmd *cli.Command) error { Transform: transform, }) } + +func handleBridgesRetrieveCapabilities(ctx context.Context, cmd *cli.Command) error { + client := beeperdesktopapi.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("bridge-id") && len(unusedArgs) > 0 { + cmd.Set("bridge-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.ArrayQueryFormatRepeat, + EmptyBody, + false, + ) + if err != nil { + return err + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Bridges.GetCapabilities(ctx, cmd.Value("bridge-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: "bridges retrieve-capabilities", + Transform: transform, + }) +} diff --git a/pkg/cmd/bridge_test.go b/pkg/cmd/bridge_test.go index 25bdeb87..3e4c1fb1 100644 --- a/pkg/cmd/bridge_test.go +++ b/pkg/cmd/bridge_test.go @@ -8,6 +8,17 @@ import ( "github.com/beeper/desktop-api-cli/internal/mocktest" ) +func TestBridgesRetrieve(t *testing.T) { + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--access-token", "string", + "bridges", "retrieve", + "--bridge-id", "local-whatsapp", + ) + }) +} + func TestBridgesList(t *testing.T) { t.Run("regular flags", func(t *testing.T) { mocktest.TestRunMockTestWithFlags( @@ -17,3 +28,14 @@ func TestBridgesList(t *testing.T) { ) }) } + +func TestBridgesRetrieveCapabilities(t *testing.T) { + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--access-token", "string", + "bridges", "retrieve-capabilities", + "--bridge-id", "local-whatsapp", + ) + }) +} diff --git a/pkg/cmd/matrixbridgecapability.go b/pkg/cmd/bridgeloginflow.go similarity index 74% rename from pkg/cmd/matrixbridgecapability.go rename to pkg/cmd/bridgeloginflow.go index 0accdfed..0674dc36 100644 --- a/pkg/cmd/matrixbridgecapability.go +++ b/pkg/cmd/bridgeloginflow.go @@ -14,22 +14,23 @@ import ( "github.com/urfave/cli/v3" ) -var matrixBridgesCapabilitiesRetrieve = cli.Command{ - Name: "retrieve", - Usage: "Get bridge capabilities", +var bridgesLoginFlowsList = cli.Command{ + Name: "list", + Usage: "List connect and reconnect flow options for a bridge. Use a flowID when creating\na bridge login session.", Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ Name: "bridge-id", + Usage: "Bridge ID.", Required: true, PathParam: "bridgeID", }, }, - Action: handleMatrixBridgesCapabilitiesRetrieve, + Action: handleBridgesLoginFlowsList, HideHelpCommand: true, } -func handleMatrixBridgesCapabilitiesRetrieve(ctx context.Context, cmd *cli.Command) error { +func handleBridgesLoginFlowsList(ctx context.Context, cmd *cli.Command) error { client := beeperdesktopapi.NewClient(getDefaultRequestOptions(cmd)...) unusedArgs := cmd.Args().Slice() if !cmd.IsSet("bridge-id") && len(unusedArgs) > 0 { @@ -53,23 +54,20 @@ func handleMatrixBridgesCapabilitiesRetrieve(ctx context.Context, cmd *cli.Comma var res []byte options = append(options, option.WithResponseBodyInto(&res)) - _, err = client.Matrix.Bridges.Capabilities.Get(ctx, cmd.Value("bridge-id").(string), options...) + _, err = client.Bridges.LoginFlows.List(ctx, cmd.Value("bridge-id").(string), options...) if err != nil { return err } obj := gjson.ParseBytes(res) - format := "json" + format := cmd.Root().String("format") explicitFormat := cmd.Root().IsSet("format") - if explicitFormat { - format = cmd.Root().String("format") - } transform := cmd.Root().String("transform") return ShowJSON(obj, ShowJSONOpts{ ExplicitFormat: explicitFormat, Format: format, RawOutput: cmd.Root().Bool("raw-output"), - Title: "matrix:bridges:capabilities retrieve", + Title: "bridges:login-flows list", Transform: transform, }) } diff --git a/pkg/cmd/app_test.go b/pkg/cmd/bridgeloginflow_test.go similarity index 72% rename from pkg/cmd/app_test.go rename to pkg/cmd/bridgeloginflow_test.go index 214d3086..43e640d2 100644 --- a/pkg/cmd/app_test.go +++ b/pkg/cmd/bridgeloginflow_test.go @@ -8,12 +8,13 @@ import ( "github.com/beeper/desktop-api-cli/internal/mocktest" ) -func TestAppStatus(t *testing.T) { +func TestBridgesLoginFlowsList(t *testing.T) { t.Run("regular flags", func(t *testing.T) { mocktest.TestRunMockTestWithFlags( t, "--access-token", "string", - "app", "status", + "bridges:login-flows", "list", + "--bridge-id", "local-whatsapp", ) }) } diff --git a/pkg/cmd/appe2eeverification.go b/pkg/cmd/bridgeloginsession.go similarity index 52% rename from pkg/cmd/appe2eeverification.go rename to pkg/cmd/bridgeloginsession.go index 4caecba2..c5e4f12f 100644 --- a/pkg/cmd/appe2eeverification.go +++ b/pkg/cmd/bridgeloginsession.go @@ -14,67 +14,88 @@ import ( "github.com/urfave/cli/v3" ) -var appE2eeVerificationCreate = cli.Command{ +var bridgesLoginSessionsCreate = cli.Command{ Name: "create", - Usage: "Start verifying this device from another signed-in device.", + Usage: "Start a temporary bridge login session to connect a new chat account or\nreconnect an existing bridge login. Omit loginID and accountID to connect a new\naccount.", Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "user-id", - Usage: "User ID to verify. Defaults to the signed-in user.", - BodyPath: "userID", + Name: "bridge-id", + Usage: "Bridge ID.", + Required: true, + PathParam: "bridgeID", + }, + &requestflag.Flag[string]{ + Name: "account-id", + Usage: "Existing chat account ID to reconnect. Omit to connect a new account.", + BodyPath: "accountID", + }, + &requestflag.Flag[string]{ + Name: "flow-id", + Usage: "Optional flow ID returned by the list login flows endpoint. If omitted, Beeper chooses the default flow.", + BodyPath: "flowID", + }, + &requestflag.Flag[string]{ + Name: "login-id", + Usage: "Existing bridge login ID to reconnect. Omit to connect a new account.", + BodyPath: "loginID", }, }, - Action: handleAppE2eeVerificationCreate, + Action: handleBridgesLoginSessionsCreate, HideHelpCommand: true, } -var appE2eeVerificationAccept = cli.Command{ - Name: "accept", - Usage: "Accept an incoming device verification request.", +var bridgesLoginSessionsRetrieve = cli.Command{ + Name: "retrieve", + Usage: "Get the current state of a temporary bridge login session.", Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "verification-id", - Usage: "Verification ID.", + Name: "bridge-id", + Usage: "Bridge ID.", Required: true, - PathParam: "verificationID", + PathParam: "bridgeID", + }, + &requestflag.Flag[string]{ + Name: "login-session-id", + Usage: "Temporary bridge login session ID.", + Required: true, + PathParam: "loginSessionID", }, }, - Action: handleAppE2eeVerificationAccept, + Action: handleBridgesLoginSessionsRetrieve, HideHelpCommand: true, } -var appE2eeVerificationCancel = cli.Command{ +var bridgesLoginSessionsCancel = cli.Command{ Name: "cancel", - Usage: "Cancel an active device verification request.", + Usage: "Cancel a temporary bridge login session.", Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ - Name: "verification-id", - Usage: "Verification ID.", + Name: "bridge-id", + Usage: "Bridge ID.", Required: true, - PathParam: "verificationID", - }, - &requestflag.Flag[string]{ - Name: "code", - Usage: "Optional cancellation code.", - BodyPath: "code", + PathParam: "bridgeID", }, &requestflag.Flag[string]{ - Name: "reason", - Usage: "Optional user-facing cancellation reason.", - BodyPath: "reason", + Name: "login-session-id", + Usage: "Temporary bridge login session ID.", + Required: true, + PathParam: "loginSessionID", }, }, - Action: handleAppE2eeVerificationCancel, + Action: handleBridgesLoginSessionsCancel, HideHelpCommand: true, } -func handleAppE2eeVerificationCreate(ctx context.Context, cmd *cli.Command) error { +func handleBridgesLoginSessionsCreate(ctx context.Context, cmd *cli.Command) error { client := beeperdesktopapi.NewClient(getDefaultRequestOptions(cmd)...) unusedArgs := cmd.Args().Slice() - + if !cmd.IsSet("bridge-id") && len(unusedArgs) > 0 { + cmd.Set("bridge-id", unusedArgs[0]) + unusedArgs = unusedArgs[1:] + } if len(unusedArgs) > 0 { return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) } @@ -90,11 +111,16 @@ func handleAppE2eeVerificationCreate(ctx context.Context, cmd *cli.Command) erro return err } - params := beeperdesktopapi.AppE2eeVerificationNewParams{} + params := beeperdesktopapi.BridgeLoginSessionNewParams{} var res []byte options = append(options, option.WithResponseBodyInto(&res)) - _, err = client.App.E2ee.Verification.New(ctx, params, options...) + _, err = client.Bridges.LoginSessions.New( + ctx, + cmd.Value("bridge-id").(string), + params, + options..., + ) if err != nil { return err } @@ -107,16 +133,16 @@ func handleAppE2eeVerificationCreate(ctx context.Context, cmd *cli.Command) erro ExplicitFormat: explicitFormat, Format: format, RawOutput: cmd.Root().Bool("raw-output"), - Title: "app:e2ee:verification create", + Title: "bridges:login-sessions create", Transform: transform, }) } -func handleAppE2eeVerificationAccept(ctx context.Context, cmd *cli.Command) error { +func handleBridgesLoginSessionsRetrieve(ctx context.Context, cmd *cli.Command) error { client := beeperdesktopapi.NewClient(getDefaultRequestOptions(cmd)...) unusedArgs := cmd.Args().Slice() - if !cmd.IsSet("verification-id") && len(unusedArgs) > 0 { - cmd.Set("verification-id", unusedArgs[0]) + if !cmd.IsSet("login-session-id") && len(unusedArgs) > 0 { + cmd.Set("login-session-id", unusedArgs[0]) unusedArgs = unusedArgs[1:] } if len(unusedArgs) > 0 { @@ -134,9 +160,18 @@ func handleAppE2eeVerificationAccept(ctx context.Context, cmd *cli.Command) erro return err } + params := beeperdesktopapi.BridgeLoginSessionGetParams{ + BridgeID: cmd.Value("bridge-id").(string), + } + var res []byte options = append(options, option.WithResponseBodyInto(&res)) - _, err = client.App.E2ee.Verification.Accept(ctx, cmd.Value("verification-id").(string), options...) + _, err = client.Bridges.LoginSessions.Get( + ctx, + cmd.Value("login-session-id").(string), + params, + options..., + ) if err != nil { return err } @@ -149,16 +184,16 @@ func handleAppE2eeVerificationAccept(ctx context.Context, cmd *cli.Command) erro ExplicitFormat: explicitFormat, Format: format, RawOutput: cmd.Root().Bool("raw-output"), - Title: "app:e2ee:verification accept", + Title: "bridges:login-sessions retrieve", Transform: transform, }) } -func handleAppE2eeVerificationCancel(ctx context.Context, cmd *cli.Command) error { +func handleBridgesLoginSessionsCancel(ctx context.Context, cmd *cli.Command) error { client := beeperdesktopapi.NewClient(getDefaultRequestOptions(cmd)...) unusedArgs := cmd.Args().Slice() - if !cmd.IsSet("verification-id") && len(unusedArgs) > 0 { - cmd.Set("verification-id", unusedArgs[0]) + if !cmd.IsSet("login-session-id") && len(unusedArgs) > 0 { + cmd.Set("login-session-id", unusedArgs[0]) unusedArgs = unusedArgs[1:] } if len(unusedArgs) > 0 { @@ -169,20 +204,22 @@ func handleAppE2eeVerificationCancel(ctx context.Context, cmd *cli.Command) erro cmd, apiquery.NestedQueryFormatBrackets, apiquery.ArrayQueryFormatRepeat, - ApplicationJSON, + EmptyBody, false, ) if err != nil { return err } - params := beeperdesktopapi.AppE2eeVerificationCancelParams{} + params := beeperdesktopapi.BridgeLoginSessionCancelParams{ + BridgeID: cmd.Value("bridge-id").(string), + } var res []byte options = append(options, option.WithResponseBodyInto(&res)) - _, err = client.App.E2ee.Verification.Cancel( + _, err = client.Bridges.LoginSessions.Cancel( ctx, - cmd.Value("verification-id").(string), + cmd.Value("login-session-id").(string), params, options..., ) @@ -198,7 +235,7 @@ func handleAppE2eeVerificationCancel(ctx context.Context, cmd *cli.Command) erro ExplicitFormat: explicitFormat, Format: format, RawOutput: cmd.Root().Bool("raw-output"), - Title: "app:e2ee:verification cancel", + Title: "bridges:login-sessions cancel", Transform: transform, }) } diff --git a/pkg/cmd/appe2eeverification_test.go b/pkg/cmd/bridgeloginsession_test.go similarity index 50% rename from pkg/cmd/appe2eeverification_test.go rename to pkg/cmd/bridgeloginsession_test.go index 57130ce2..eab860d9 100644 --- a/pkg/cmd/appe2eeverification_test.go +++ b/pkg/cmd/bridgeloginsession_test.go @@ -8,60 +8,54 @@ import ( "github.com/beeper/desktop-api-cli/internal/mocktest" ) -func TestAppE2eeVerificationCreate(t *testing.T) { +func TestBridgesLoginSessionsCreate(t *testing.T) { t.Run("regular flags", func(t *testing.T) { mocktest.TestRunMockTestWithFlags( t, "--access-token", "string", - "app:e2ee:verification", "create", - "--user-id", "userID", + "bridges:login-sessions", "create", + "--bridge-id", "local-whatsapp", + "--account-id", "x", + "--flow-id", "x", + "--login-id", "x", ) }) t.Run("piping data", func(t *testing.T) { // Test piping YAML data over stdin - pipeData := []byte("userID: userID") + pipeData := []byte("" + + "accountID: x\n" + + "flowID: x\n" + + "loginID: x\n") mocktest.TestRunMockTestWithPipeAndFlags( t, pipeData, "--access-token", "string", - "app:e2ee:verification", "create", + "bridges:login-sessions", "create", + "--bridge-id", "local-whatsapp", ) }) } -func TestAppE2eeVerificationAccept(t *testing.T) { +func TestBridgesLoginSessionsRetrieve(t *testing.T) { t.Run("regular flags", func(t *testing.T) { mocktest.TestRunMockTestWithFlags( t, "--access-token", "string", - "app:e2ee:verification", "accept", - "--verification-id", "x", + "bridges:login-sessions", "retrieve", + "--bridge-id", "local-whatsapp", + "--login-session-id", "123", ) }) } -func TestAppE2eeVerificationCancel(t *testing.T) { +func TestBridgesLoginSessionsCancel(t *testing.T) { t.Run("regular flags", func(t *testing.T) { mocktest.TestRunMockTestWithFlags( t, "--access-token", "string", - "app:e2ee:verification", "cancel", - "--verification-id", "x", - "--code", "code", - "--reason", "reason", - ) - }) - - t.Run("piping data", func(t *testing.T) { - // Test piping YAML data over stdin - pipeData := []byte("" + - "code: code\n" + - "reason: reason\n") - mocktest.TestRunMockTestWithPipeAndFlags( - t, pipeData, - "--access-token", "string", - "app:e2ee:verification", "cancel", - "--verification-id", "x", + "bridges:login-sessions", "cancel", + "--bridge-id", "local-whatsapp", + "--login-session-id", "123", ) }) } diff --git a/pkg/cmd/bridgeloginsessionstep.go b/pkg/cmd/bridgeloginsessionstep.go new file mode 100644 index 00000000..03f815d9 --- /dev/null +++ b/pkg/cmd/bridgeloginsessionstep.go @@ -0,0 +1,116 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "context" + "fmt" + + "github.com/beeper/desktop-api-cli/internal/apiquery" + "github.com/beeper/desktop-api-cli/internal/requestflag" + "github.com/beeper/desktop-api-go/v5" + "github.com/beeper/desktop-api-go/v5/option" + "github.com/tidwall/gjson" + "github.com/urfave/cli/v3" +) + +var bridgesLoginSessionsStepsSubmit = cli.Command{ + Name: "submit", + Usage: "Submit input for the current step of a bridge login session.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "bridge-id", + Usage: "Bridge ID.", + Required: true, + PathParam: "bridgeID", + }, + &requestflag.Flag[string]{ + Name: "login-session-id", + Usage: "Temporary bridge login session ID.", + Required: true, + PathParam: "loginSessionID", + }, + &requestflag.Flag[string]{ + Name: "step-id", + Usage: "Current bridge login session step ID.", + Required: true, + PathParam: "stepID", + }, + &requestflag.Flag[string]{ + Name: "type", + Usage: `Allowed values: "user_input", "cookies", "display_and_wait".`, + Required: true, + BodyPath: "type", + }, + &requestflag.Flag[map[string]any]{ + Name: "fields", + Usage: "Field values keyed by the field IDs from the current step.", + BodyPath: "fields", + }, + &requestflag.Flag[string]{ + Name: "last-url", + Usage: "Last browser URL reached during a cookies step, if available.", + BodyPath: "lastURL", + }, + &requestflag.Flag[string]{ + Name: "source", + Usage: "How the step was completed. Omit unless the client needs to distinguish an embedded webview or browser extension.", + BodyPath: "source", + }, + }, + Action: handleBridgesLoginSessionsStepsSubmit, + HideHelpCommand: true, +} + +func handleBridgesLoginSessionsStepsSubmit(ctx context.Context, cmd *cli.Command) error { + client := beeperdesktopapi.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("step-id") && len(unusedArgs) > 0 { + cmd.Set("step-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.ArrayQueryFormatRepeat, + ApplicationJSON, + false, + ) + if err != nil { + return err + } + + params := beeperdesktopapi.BridgeLoginSessionStepSubmitParams{ + BridgeID: cmd.Value("bridge-id").(string), + LoginSessionID: cmd.Value("login-session-id").(string), + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Bridges.LoginSessions.Steps.Submit( + ctx, + cmd.Value("step-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: "bridges:login-sessions:steps submit", + Transform: transform, + }) +} diff --git a/pkg/cmd/bridgeloginsessionstep_test.go b/pkg/cmd/bridgeloginsessionstep_test.go new file mode 100644 index 00000000..62805414 --- /dev/null +++ b/pkg/cmd/bridgeloginsessionstep_test.go @@ -0,0 +1,44 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "testing" + + "github.com/beeper/desktop-api-cli/internal/mocktest" +) + +func TestBridgesLoginSessionsStepsSubmit(t *testing.T) { + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--access-token", "string", + "bridges:login-sessions:steps", "submit", + "--bridge-id", "local-whatsapp", + "--login-session-id", "123", + "--step-id", "x", + "--type", "user_input", + "--fields", "{foo: string}", + "--last-url", "lastURL", + "--source", "api", + ) + }) + + t.Run("piping data", func(t *testing.T) { + // Test piping YAML data over stdin + pipeData := []byte("" + + "type: user_input\n" + + "fields:\n" + + " foo: string\n" + + "lastURL: lastURL\n" + + "source: api\n") + mocktest.TestRunMockTestWithPipeAndFlags( + t, pipeData, + "--access-token", "string", + "bridges:login-sessions:steps", "submit", + "--bridge-id", "local-whatsapp", + "--login-session-id", "123", + "--step-id", "x", + ) + }) +} diff --git a/pkg/cmd/chat.go b/pkg/cmd/chat.go index 4b22dc00..9c4c03ba 100644 --- a/pkg/cmd/chat.go +++ b/pkg/cmd/chat.go @@ -54,12 +54,12 @@ var chatsCreate = cli.Command{ var chatsRetrieve = cli.Command{ Name: "retrieve", - Usage: "Retrieve chat details including metadata, participants, and latest message", + Usage: "Retrieve chat details, including metadata, participants, and the latest message.", Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ Name: "chat-id", - Usage: "Chat ID. Input routes also accept the local chat ID from this Beeper Desktop installation when available.", + Usage: "Chat ID. Input routes also accept the local chat ID from this installation when available.", Required: true, PathParam: "chatID", }, @@ -76,12 +76,12 @@ var chatsRetrieve = cli.Command{ var chatsUpdate = requestflag.WithInnerFlags(cli.Command{ Name: "update", - Usage: "Update supported chat fields. Non-empty draft objects are accepted only when the\ncurrent draft is empty. Send draft=null to clear the draft before setting new\ndraft text or attachments.", + Usage: "Update supported chat fields. Non-empty drafts are accepted only when the\ncurrent draft is empty. Send draft=null to clear the draft before setting new\ndraft text or attachments.", Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ Name: "chat-id", - Usage: "Chat ID. Input routes also accept the local chat ID from this Beeper Desktop installation when available.", + Usage: "Chat ID. Input routes also accept the local chat ID from this installation when available.", Required: true, PathParam: "chatID", }, @@ -137,7 +137,7 @@ var chatsUpdate = requestflag.WithInnerFlags(cli.Command{ "draft": { &requestflag.InnerFlag[string]{ Name: "draft.text", - Usage: "Draft text. Plain text and Markdown are converted to Matrix HTML with the same rules used by send and edit.", + Usage: "Draft text. Plain text and Markdown are converted to Beeper rich text with the same rules used by send and edit.", InnerField: "text", }, &requestflag.InnerFlag[map[string]any]{ @@ -179,12 +179,12 @@ var chatsList = cli.Command{ var chatsArchive = cli.Command{ Name: "archive", - Usage: "Archive or unarchive a chat. Set archived=true to move to archive,\narchived=false to move back to inbox", + Usage: "Archive or unarchive a chat. Set archived=true to move it to Archive, or\narchived=false to move it back to the inbox.", Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ Name: "chat-id", - Usage: "Chat ID. Input routes also accept the local chat ID from this Beeper Desktop installation when available.", + Usage: "Chat ID. Input routes also accept the local chat ID from this installation when available.", Required: true, PathParam: "chatID", }, @@ -206,7 +206,7 @@ var chatsMarkRead = cli.Command{ Flags: []cli.Flag{ &requestflag.Flag[string]{ Name: "chat-id", - Usage: "Chat ID. Input routes also accept the local chat ID from this Beeper Desktop installation when available.", + Usage: "Chat ID. Input routes also accept the local chat ID from this installation when available.", Required: true, PathParam: "chatID", }, @@ -227,7 +227,7 @@ var chatsMarkUnread = cli.Command{ Flags: []cli.Flag{ &requestflag.Flag[string]{ Name: "chat-id", - Usage: "Chat ID. Input routes also accept the local chat ID from this Beeper Desktop installation when available.", + Usage: "Chat ID. Input routes also accept the local chat ID from this installation when available.", Required: true, PathParam: "chatID", }, @@ -243,12 +243,12 @@ var chatsMarkUnread = cli.Command{ var chatsNotifyAnyway = cli.Command{ Name: "notify-anyway", - Usage: "Force a delivery notification when supported by the underlying network.\nCurrently intended for iMessage on macOS; unsupported networks return an error.", + Usage: "Send a notification despite the recipient focus state when the network supports\nit. Currently intended for iMessage on macOS; unsupported networks return an\nerror.", Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ Name: "chat-id", - Usage: "Chat ID. Input routes also accept the local chat ID from this Beeper Desktop installation when available.", + Usage: "Chat ID. Input routes also accept the local chat ID from this installation when available.", Required: true, PathParam: "chatID", }, @@ -264,7 +264,7 @@ var chatsSearch = cli.Command{ Flags: []cli.Flag{ &requestflag.Flag[[]string]{ Name: "account-id", - Usage: "Provide an array of account IDs to filter chats from specific messaging accounts only", + Usage: "Limit results to specific chat accounts.", QueryPath: "accountIDs", }, &requestflag.Flag[string]{ @@ -290,12 +290,12 @@ var chatsSearch = cli.Command{ }, &requestflag.Flag[any]{ Name: "last-activity-after", - Usage: "Provide an ISO datetime string to only retrieve chats with last activity after this time", + Usage: "Only include chats with last activity after this ISO 8601 datetime.", QueryPath: "lastActivityAfter", }, &requestflag.Flag[any]{ Name: "last-activity-before", - Usage: "Provide an ISO datetime string to only retrieve chats with last activity before this time", + Usage: "Only include chats with last activity before this ISO 8601 datetime.", QueryPath: "lastActivityBefore", }, &requestflag.Flag[int64]{ @@ -306,7 +306,7 @@ var chatsSearch = cli.Command{ }, &requestflag.Flag[string]{ Name: "query", - Usage: `Literal token search (non-semantic). Use single words users type (e.g., "dinner"). When multiple words provided, ALL must match. Case-insensitive.`, + Usage: `Literal chat search. Use words the user typed, such as "dinner". When multiple words are provided, all must match. Case-insensitive.`, QueryPath: "query", }, &requestflag.Flag[string]{ @@ -337,7 +337,7 @@ var chatsSearch = cli.Command{ var chatsStart = requestflag.WithInnerFlags(cli.Command{ Name: "start", - Usage: "Resolve a user/contact and open a direct chat. Reuses and returns an existing\ndirect chat when one is found. Available in Beeper Desktop v4.2.808+.", + Usage: "Resolve a user/contact and open a direct chat. Reuses and returns an existing\ndirect chat when one is found. Available in Beeper v4.2.808+.", Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ @@ -348,7 +348,7 @@ var chatsStart = requestflag.WithInnerFlags(cli.Command{ }, &requestflag.Flag[map[string]any]{ Name: "user", - Usage: "Merged user-like contact payload used to resolve the best identifier.", + Usage: "Contact-like user payload used to resolve the best identifier.", Required: true, BodyPath: "user", }, @@ -474,11 +474,8 @@ func handleChatsRetrieve(ctx context.Context, cmd *cli.Command) error { } obj := gjson.ParseBytes(res) - format := "json" + format := cmd.Root().String("format") explicitFormat := cmd.Root().IsSet("format") - if explicitFormat { - format = cmd.Root().String("format") - } transform := cmd.Root().String("transform") return ShowJSON(obj, ShowJSONOpts{ ExplicitFormat: explicitFormat, @@ -559,11 +556,8 @@ func handleChatsList(ctx context.Context, cmd *cli.Command) error { params := beeperdesktopapi.ChatListParams{} - format := "json" + format := cmd.Root().String("format") explicitFormat := cmd.Root().IsSet("format") - if explicitFormat { - format = cmd.Root().String("format") - } transform := cmd.Root().String("transform") if format == "raw" { var res []byte @@ -796,11 +790,8 @@ func handleChatsSearch(ctx context.Context, cmd *cli.Command) error { params := beeperdesktopapi.ChatSearchParams{} - format := "json" + format := cmd.Root().String("format") explicitFormat := cmd.Root().IsSet("format") - if explicitFormat { - format = cmd.Root().String("format") - } transform := cmd.Root().String("transform") if format == "raw" { var res []byte diff --git a/pkg/cmd/chatmessagereaction.go b/pkg/cmd/chatmessagereaction.go index e1976b90..d161c474 100644 --- a/pkg/cmd/chatmessagereaction.go +++ b/pkg/cmd/chatmessagereaction.go @@ -21,7 +21,7 @@ var chatsMessagesReactionsDelete = cli.Command{ Flags: []cli.Flag{ &requestflag.Flag[string]{ Name: "chat-id", - Usage: "Chat ID. Input routes also accept the local chat ID from this Beeper Desktop installation when available.", + Usage: "Chat ID. Input routes also accept the local chat ID from this installation when available.", Required: true, PathParam: "chatID", }, @@ -49,7 +49,7 @@ var chatsMessagesReactionsAdd = cli.Command{ Flags: []cli.Flag{ &requestflag.Flag[string]{ Name: "chat-id", - Usage: "Chat ID. Input routes also accept the local chat ID from this Beeper Desktop installation when available.", + Usage: "Chat ID. Input routes also accept the local chat ID from this installation when available.", Required: true, PathParam: "chatID", }, diff --git a/pkg/cmd/chatreminder.go b/pkg/cmd/chatreminder.go index 535ee8aa..4901f09e 100644 --- a/pkg/cmd/chatreminder.go +++ b/pkg/cmd/chatreminder.go @@ -14,12 +14,12 @@ import ( var chatsRemindersCreate = requestflag.WithInnerFlags(cli.Command{ Name: "create", - Usage: "Set a reminder for a chat at a specific time", + Usage: "Set a reminder for a chat at a specific time.", Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ Name: "chat-id", - Usage: "Chat ID. Input routes also accept the local chat ID from this Beeper Desktop installation when available.", + Usage: "Chat ID. Input routes also accept the local chat ID from this installation when available.", Required: true, PathParam: "chatID", }, @@ -49,12 +49,12 @@ var chatsRemindersCreate = requestflag.WithInnerFlags(cli.Command{ var chatsRemindersDelete = cli.Command{ Name: "delete", - Usage: "Clear an existing reminder from a chat", + Usage: "Clear an existing reminder from a chat.", Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ Name: "chat-id", - Usage: "Chat ID. Input routes also accept the local chat ID from this Beeper Desktop installation when available.", + Usage: "Chat ID. Input routes also accept the local chat ID from this installation when available.", Required: true, PathParam: "chatID", }, diff --git a/pkg/cmd/cmd.go b/pkg/cmd/cmd.go index e45969cc..c693bc13 100644 --- a/pkg/cmd/cmd.go +++ b/pkg/cmd/cmd.go @@ -25,7 +25,7 @@ var ( func init() { Command = &cli.Command{ - Name: "beeper-desktop-cli", + Name: "beeper-desktop", Usage: "CLI for the beeperdesktop API", Suggest: true, Version: Version, @@ -46,7 +46,7 @@ func init() { &cli.StringFlag{ Name: "format", Usage: "The format for displaying response data (one of: " + strings.Join(OutputFormats, ", ") + ")", - Value: "json", + Value: "auto", Validator: func(format string) error { if !slices.Contains(OutputFormats, strings.ToLower(format)) { return fmt.Errorf("format must be one of: %s", strings.Join(OutputFormats, ", ")) @@ -57,7 +57,7 @@ func init() { &cli.StringFlag{ Name: "format-error", Usage: "The format for displaying error data (one of: " + strings.Join(OutputFormats, ", ") + ")", - Value: "json", + Value: "auto", Validator: func(format string) error { if !slices.Contains(OutputFormats, strings.ToLower(format)) { return fmt.Errorf("format must be one of: %s", strings.Join(OutputFormats, ", ")) @@ -87,76 +87,12 @@ func init() { Commands: []*cli.Command{ &focus, &search, - { - Name: "app", - Category: "API RESOURCE", - Suggest: true, - Commands: []*cli.Command{ - &appStatus, - }, - }, - { - Name: "app:login", - Category: "API RESOURCE", - Suggest: true, - Commands: []*cli.Command{ - &appLoginEmail, - &appLoginRegister, - &appLoginResponse, - &appLoginStart, - }, - }, - { - Name: "app:e2ee:recovery-code", - Category: "API RESOURCE", - Suggest: true, - Commands: []*cli.Command{ - &appE2eeRecoveryCodeMarkBackedUp, - &appE2eeRecoveryCodeVerify, - }, - }, - { - Name: "app:e2ee:recovery-code:reset", - Category: "API RESOURCE", - Suggest: true, - Commands: []*cli.Command{ - &appE2eeRecoveryCodeResetCreate, - &appE2eeRecoveryCodeResetConfirm, - }, - }, - { - Name: "app:e2ee:verification", - Category: "API RESOURCE", - Suggest: true, - Commands: []*cli.Command{ - &appE2eeVerificationCreate, - &appE2eeVerificationAccept, - &appE2eeVerificationCancel, - }, - }, - { - Name: "app:e2ee:verification:qr", - Category: "API RESOURCE", - Suggest: true, - Commands: []*cli.Command{ - &appE2eeVerificationQrConfirmScanned, - &appE2eeVerificationQrScan, - }, - }, - { - Name: "app:e2ee:verification:sas", - Category: "API RESOURCE", - Suggest: true, - Commands: []*cli.Command{ - &appE2eeVerificationSasConfirm, - &appE2eeVerificationSasStart, - }, - }, { Name: "accounts", Category: "API RESOURCE", Suggest: true, Commands: []*cli.Command{ + &accountsRetrieve, &accountsList, }, }, @@ -174,109 +110,35 @@ func init() { Category: "API RESOURCE", Suggest: true, Commands: []*cli.Command{ + &bridgesRetrieve, &bridgesList, + &bridgesRetrieveCapabilities, }, }, { - Name: "matrix:users", - Category: "API RESOURCE", - Suggest: true, - Commands: []*cli.Command{ - &matrixUsersRetrieveProfile, - }, - }, - { - Name: "matrix:users:account-data", - Category: "API RESOURCE", - Suggest: true, - Commands: []*cli.Command{ - &matrixUsersAccountDataRetrieve, - &matrixUsersAccountDataUpdate, - }, - }, - { - Name: "matrix:rooms", - Category: "API RESOURCE", - Suggest: true, - Commands: []*cli.Command{ - &matrixRoomsCreate, - &matrixRoomsJoin, - &matrixRoomsLeave, - }, - }, - { - Name: "matrix:rooms:account-data", - Category: "API RESOURCE", - Suggest: true, - Commands: []*cli.Command{ - &matrixRoomsAccountDataRetrieve, - &matrixRoomsAccountDataUpdate, - }, - }, - { - Name: "matrix:rooms:state", - Category: "API RESOURCE", - Suggest: true, - Commands: []*cli.Command{ - &matrixRoomsStateRetrieve, - &matrixRoomsStateList, - }, - }, - { - Name: "matrix:rooms:events", - Category: "API RESOURCE", - Suggest: true, - Commands: []*cli.Command{ - &matrixRoomsEventsRetrieve, - }, - }, - { - Name: "matrix:bridges:auth", - Category: "API RESOURCE", - Suggest: true, - Commands: []*cli.Command{ - &matrixBridgesAuthListFlows, - &matrixBridgesAuthListLogins, - &matrixBridgesAuthLogout, - &matrixBridgesAuthStartLogin, - &matrixBridgesAuthSubmitCookies, - &matrixBridgesAuthSubmitUserInput, - &matrixBridgesAuthWaitForStep, - &matrixBridgesAuthWhoami, - }, - }, - { - Name: "matrix:bridges:contacts", - Category: "API RESOURCE", - Suggest: true, - Commands: []*cli.Command{ - &matrixBridgesContactsList, - }, - }, - { - Name: "matrix:bridges:users", + Name: "bridges:login-flows", Category: "API RESOURCE", Suggest: true, Commands: []*cli.Command{ - &matrixBridgesUsersResolve, - &matrixBridgesUsersSearch, + &bridgesLoginFlowsList, }, }, { - Name: "matrix:bridges:rooms", + Name: "bridges:login-sessions", Category: "API RESOURCE", Suggest: true, Commands: []*cli.Command{ - &matrixBridgesRoomsCreateDm, - &matrixBridgesRoomsCreateGroup, + &bridgesLoginSessionsCreate, + &bridgesLoginSessionsRetrieve, + &bridgesLoginSessionsCancel, }, }, { - Name: "matrix:bridges:capabilities", + Name: "bridges:login-sessions:steps", Category: "API RESOURCE", Suggest: true, Commands: []*cli.Command{ - &matrixBridgesCapabilitiesRetrieve, + &bridgesLoginSessionsStepsSubmit, }, }, { @@ -349,7 +211,7 @@ func init() { { Name: "@manpages", Usage: "Generate documentation for 'man'", - UsageText: "beeper-desktop-cli @manpages [-o beeper-desktop-cli.1] [--gzip]", + UsageText: "beeper-desktop @manpages [-o beeper-desktop.1] [--gzip]", Hidden: true, Action: generateManpages, HideHelpCommand: true, @@ -402,7 +264,7 @@ func generateManpages(ctx context.Context, c *cli.Command) error { // handle error } if c.Bool("text") { - file, err := os.Create(filepath.Join(dir, "man1", "beeper-desktop-cli.1")) + file, err := os.Create(filepath.Join(dir, "man1", "beeper-desktop.1")) if err != nil { return err } @@ -412,7 +274,7 @@ func generateManpages(ctx context.Context, c *cli.Command) error { } } if c.Bool("gzip") { - file, err := os.Create(filepath.Join(dir, "man1", "beeper-desktop-cli.1.gz")) + file, err := os.Create(filepath.Join(dir, "man1", "beeper-desktop.1.gz")) if err != nil { return err } diff --git a/pkg/cmd/info.go b/pkg/cmd/info.go index fa4ea4de..aeec2887 100644 --- a/pkg/cmd/info.go +++ b/pkg/cmd/info.go @@ -15,7 +15,7 @@ import ( var infoRetrieve = cli.Command{ Name: "retrieve", - Usage: "Returns app, platform, server, endpoint discovery, OAuth, and WebSocket metadata\nfor this Beeper Desktop instance.", + Usage: "Returns app, platform, server, endpoint discovery, OAuth, and WebSocket metadata\nfor this Beeper Client API server.", Suggest: true, Flags: []cli.Flag{}, Action: handleInfoRetrieve, @@ -49,11 +49,8 @@ func handleInfoRetrieve(ctx context.Context, cmd *cli.Command) error { } obj := gjson.ParseBytes(res) - format := "json" + format := cmd.Root().String("format") explicitFormat := cmd.Root().IsSet("format") - if explicitFormat { - format = cmd.Root().String("format") - } transform := cmd.Root().String("transform") return ShowJSON(obj, ShowJSONOpts{ ExplicitFormat: explicitFormat, diff --git a/pkg/cmd/matrixbridgeauth.go b/pkg/cmd/matrixbridgeauth.go deleted file mode 100644 index ea8deb55..00000000 --- a/pkg/cmd/matrixbridgeauth.go +++ /dev/null @@ -1,584 +0,0 @@ -// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -package cmd - -import ( - "context" - "fmt" - - "github.com/beeper/desktop-api-cli/internal/apiquery" - "github.com/beeper/desktop-api-cli/internal/requestflag" - "github.com/beeper/desktop-api-go/v5" - "github.com/beeper/desktop-api-go/v5/option" - "github.com/tidwall/gjson" - "github.com/urfave/cli/v3" -) - -var matrixBridgesAuthListFlows = cli.Command{ - Name: "list-flows", - Usage: "Get the available login flows.", - Suggest: true, - Flags: []cli.Flag{ - &requestflag.Flag[string]{ - Name: "bridge-id", - Required: true, - PathParam: "bridgeID", - }, - }, - Action: handleMatrixBridgesAuthListFlows, - HideHelpCommand: true, -} - -var matrixBridgesAuthListLogins = cli.Command{ - Name: "list-logins", - Usage: "Get the login IDs of the current user.", - Suggest: true, - Flags: []cli.Flag{ - &requestflag.Flag[string]{ - Name: "bridge-id", - Required: true, - PathParam: "bridgeID", - }, - }, - Action: handleMatrixBridgesAuthListLogins, - HideHelpCommand: true, -} - -var matrixBridgesAuthLogout = cli.Command{ - Name: "logout", - Usage: "Log out of an existing login.", - Suggest: true, - Flags: []cli.Flag{ - &requestflag.Flag[string]{ - Name: "bridge-id", - Required: true, - PathParam: "bridgeID", - }, - &requestflag.Flag[string]{ - Name: "login-id", - Usage: "The unique ID of a login. Defined by the network connector.", - Required: true, - PathParam: "loginID", - }, - }, - Action: handleMatrixBridgesAuthLogout, - HideHelpCommand: true, -} - -var matrixBridgesAuthStartLogin = cli.Command{ - Name: "start-login", - Usage: "This endpoint starts a new login process, which is used to log into the bridge.", - Suggest: true, - Flags: []cli.Flag{ - &requestflag.Flag[string]{ - Name: "bridge-id", - Required: true, - PathParam: "bridgeID", - }, - &requestflag.Flag[string]{ - Name: "flow-id", - Required: true, - PathParam: "flowID", - }, - &requestflag.Flag[string]{ - Name: "login-id", - Usage: "An existing login ID to re-login as. If this is specified and the user logs into a different account, the provided ID will be logged out.", - QueryPath: "login_id", - }, - }, - Action: handleMatrixBridgesAuthStartLogin, - HideHelpCommand: true, -} - -var matrixBridgesAuthSubmitCookies = cli.Command{ - Name: "submit-cookies", - Usage: "Submit extracted cookies in a login process.", - Suggest: true, - Flags: []cli.Flag{ - &requestflag.Flag[string]{ - Name: "bridge-id", - Required: true, - PathParam: "bridgeID", - }, - &requestflag.Flag[string]{ - Name: "login-process-id", - Required: true, - PathParam: "loginProcessID", - }, - &requestflag.Flag[string]{ - Name: "step-id", - Required: true, - PathParam: "stepID", - }, - &requestflag.Flag[map[string]any]{ - Name: "body", - Required: true, - BodyRoot: true, - }, - }, - Action: handleMatrixBridgesAuthSubmitCookies, - HideHelpCommand: true, -} - -var matrixBridgesAuthSubmitUserInput = cli.Command{ - Name: "submit-user-input", - Usage: "Submit user input in a login process.", - Suggest: true, - Flags: []cli.Flag{ - &requestflag.Flag[string]{ - Name: "bridge-id", - Required: true, - PathParam: "bridgeID", - }, - &requestflag.Flag[string]{ - Name: "login-process-id", - Required: true, - PathParam: "loginProcessID", - }, - &requestflag.Flag[string]{ - Name: "step-id", - Required: true, - PathParam: "stepID", - }, - &requestflag.Flag[map[string]any]{ - Name: "body", - Required: true, - BodyRoot: true, - }, - }, - Action: handleMatrixBridgesAuthSubmitUserInput, - HideHelpCommand: true, -} - -var matrixBridgesAuthWaitForStep = cli.Command{ - Name: "wait-for-step", - Usage: "Wait for the next step after displaying data to the user.", - Suggest: true, - Flags: []cli.Flag{ - &requestflag.Flag[string]{ - Name: "bridge-id", - Required: true, - PathParam: "bridgeID", - }, - &requestflag.Flag[string]{ - Name: "login-process-id", - Required: true, - PathParam: "loginProcessID", - }, - &requestflag.Flag[string]{ - Name: "step-id", - Required: true, - PathParam: "stepID", - }, - }, - Action: handleMatrixBridgesAuthWaitForStep, - HideHelpCommand: true, -} - -var matrixBridgesAuthWhoami = cli.Command{ - Name: "whoami", - Usage: "Get all info that is useful for presenting this bridge in a manager interface.", - Suggest: true, - Flags: []cli.Flag{ - &requestflag.Flag[string]{ - Name: "bridge-id", - Required: true, - PathParam: "bridgeID", - }, - }, - Action: handleMatrixBridgesAuthWhoami, - HideHelpCommand: true, -} - -func handleMatrixBridgesAuthListFlows(ctx context.Context, cmd *cli.Command) error { - client := beeperdesktopapi.NewClient(getDefaultRequestOptions(cmd)...) - unusedArgs := cmd.Args().Slice() - if !cmd.IsSet("bridge-id") && len(unusedArgs) > 0 { - cmd.Set("bridge-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.ArrayQueryFormatRepeat, - EmptyBody, - false, - ) - if err != nil { - return err - } - - var res []byte - options = append(options, option.WithResponseBodyInto(&res)) - _, err = client.Matrix.Bridges.Auth.ListFlows(ctx, cmd.Value("bridge-id").(string), options...) - if err != nil { - return err - } - - obj := gjson.ParseBytes(res) - format := "json" - explicitFormat := cmd.Root().IsSet("format") - if explicitFormat { - format = cmd.Root().String("format") - } - transform := cmd.Root().String("transform") - return ShowJSON(obj, ShowJSONOpts{ - ExplicitFormat: explicitFormat, - Format: format, - RawOutput: cmd.Root().Bool("raw-output"), - Title: "matrix:bridges:auth list-flows", - Transform: transform, - }) -} - -func handleMatrixBridgesAuthListLogins(ctx context.Context, cmd *cli.Command) error { - client := beeperdesktopapi.NewClient(getDefaultRequestOptions(cmd)...) - unusedArgs := cmd.Args().Slice() - if !cmd.IsSet("bridge-id") && len(unusedArgs) > 0 { - cmd.Set("bridge-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.ArrayQueryFormatRepeat, - EmptyBody, - false, - ) - if err != nil { - return err - } - - var res []byte - options = append(options, option.WithResponseBodyInto(&res)) - _, err = client.Matrix.Bridges.Auth.ListLogins(ctx, cmd.Value("bridge-id").(string), options...) - if err != nil { - return err - } - - obj := gjson.ParseBytes(res) - format := "json" - explicitFormat := cmd.Root().IsSet("format") - if explicitFormat { - format = cmd.Root().String("format") - } - transform := cmd.Root().String("transform") - return ShowJSON(obj, ShowJSONOpts{ - ExplicitFormat: explicitFormat, - Format: format, - RawOutput: cmd.Root().Bool("raw-output"), - Title: "matrix:bridges:auth list-logins", - Transform: transform, - }) -} - -func handleMatrixBridgesAuthLogout(ctx context.Context, cmd *cli.Command) error { - client := beeperdesktopapi.NewClient(getDefaultRequestOptions(cmd)...) - unusedArgs := cmd.Args().Slice() - if !cmd.IsSet("login-id") && len(unusedArgs) > 0 { - cmd.Set("login-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.ArrayQueryFormatRepeat, - EmptyBody, - false, - ) - if err != nil { - return err - } - - params := beeperdesktopapi.MatrixBridgeAuthLogoutParams{ - BridgeID: cmd.Value("bridge-id").(string), - } - - var res []byte - options = append(options, option.WithResponseBodyInto(&res)) - _, err = client.Matrix.Bridges.Auth.Logout( - ctx, - cmd.Value("login-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: "matrix:bridges:auth logout", - Transform: transform, - }) -} - -func handleMatrixBridgesAuthStartLogin(ctx context.Context, cmd *cli.Command) error { - client := beeperdesktopapi.NewClient(getDefaultRequestOptions(cmd)...) - unusedArgs := cmd.Args().Slice() - if !cmd.IsSet("flow-id") && len(unusedArgs) > 0 { - cmd.Set("flow-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.ArrayQueryFormatRepeat, - EmptyBody, - false, - ) - if err != nil { - return err - } - - params := beeperdesktopapi.MatrixBridgeAuthStartLoginParams{ - BridgeID: cmd.Value("bridge-id").(string), - } - - var res []byte - options = append(options, option.WithResponseBodyInto(&res)) - _, err = client.Matrix.Bridges.Auth.StartLogin( - ctx, - cmd.Value("flow-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: "matrix:bridges:auth start-login", - Transform: transform, - }) -} - -func handleMatrixBridgesAuthSubmitCookies(ctx context.Context, cmd *cli.Command) error { - client := beeperdesktopapi.NewClient(getDefaultRequestOptions(cmd)...) - unusedArgs := cmd.Args().Slice() - if !cmd.IsSet("step-id") && len(unusedArgs) > 0 { - cmd.Set("step-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.ArrayQueryFormatRepeat, - ApplicationJSON, - false, - ) - if err != nil { - return err - } - - params := beeperdesktopapi.MatrixBridgeAuthSubmitCookiesParams{ - BridgeID: cmd.Value("bridge-id").(string), - LoginProcessID: cmd.Value("login-process-id").(string), - } - - var res []byte - options = append(options, option.WithResponseBodyInto(&res)) - _, err = client.Matrix.Bridges.Auth.SubmitCookies( - ctx, - cmd.Value("step-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: "matrix:bridges:auth submit-cookies", - Transform: transform, - }) -} - -func handleMatrixBridgesAuthSubmitUserInput(ctx context.Context, cmd *cli.Command) error { - client := beeperdesktopapi.NewClient(getDefaultRequestOptions(cmd)...) - unusedArgs := cmd.Args().Slice() - if !cmd.IsSet("step-id") && len(unusedArgs) > 0 { - cmd.Set("step-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.ArrayQueryFormatRepeat, - ApplicationJSON, - false, - ) - if err != nil { - return err - } - - params := beeperdesktopapi.MatrixBridgeAuthSubmitUserInputParams{ - BridgeID: cmd.Value("bridge-id").(string), - LoginProcessID: cmd.Value("login-process-id").(string), - } - - var res []byte - options = append(options, option.WithResponseBodyInto(&res)) - _, err = client.Matrix.Bridges.Auth.SubmitUserInput( - ctx, - cmd.Value("step-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: "matrix:bridges:auth submit-user-input", - Transform: transform, - }) -} - -func handleMatrixBridgesAuthWaitForStep(ctx context.Context, cmd *cli.Command) error { - client := beeperdesktopapi.NewClient(getDefaultRequestOptions(cmd)...) - unusedArgs := cmd.Args().Slice() - if !cmd.IsSet("step-id") && len(unusedArgs) > 0 { - cmd.Set("step-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.ArrayQueryFormatRepeat, - EmptyBody, - false, - ) - if err != nil { - return err - } - - params := beeperdesktopapi.MatrixBridgeAuthWaitForStepParams{ - BridgeID: cmd.Value("bridge-id").(string), - LoginProcessID: cmd.Value("login-process-id").(string), - } - - var res []byte - options = append(options, option.WithResponseBodyInto(&res)) - _, err = client.Matrix.Bridges.Auth.WaitForStep( - ctx, - cmd.Value("step-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: "matrix:bridges:auth wait-for-step", - Transform: transform, - }) -} - -func handleMatrixBridgesAuthWhoami(ctx context.Context, cmd *cli.Command) error { - client := beeperdesktopapi.NewClient(getDefaultRequestOptions(cmd)...) - unusedArgs := cmd.Args().Slice() - if !cmd.IsSet("bridge-id") && len(unusedArgs) > 0 { - cmd.Set("bridge-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.ArrayQueryFormatRepeat, - EmptyBody, - false, - ) - if err != nil { - return err - } - - var res []byte - options = append(options, option.WithResponseBodyInto(&res)) - _, err = client.Matrix.Bridges.Auth.Whoami(ctx, cmd.Value("bridge-id").(string), options...) - if err != nil { - return err - } - - obj := gjson.ParseBytes(res) - format := "json" - explicitFormat := cmd.Root().IsSet("format") - if explicitFormat { - format = cmd.Root().String("format") - } - transform := cmd.Root().String("transform") - return ShowJSON(obj, ShowJSONOpts{ - ExplicitFormat: explicitFormat, - Format: format, - RawOutput: cmd.Root().Bool("raw-output"), - Title: "matrix:bridges:auth whoami", - Transform: transform, - }) -} diff --git a/pkg/cmd/matrixbridgeauth_test.go b/pkg/cmd/matrixbridgeauth_test.go deleted file mode 100644 index 1776edae..00000000 --- a/pkg/cmd/matrixbridgeauth_test.go +++ /dev/null @@ -1,134 +0,0 @@ -// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -package cmd - -import ( - "testing" - - "github.com/beeper/desktop-api-cli/internal/mocktest" -) - -func TestMatrixBridgesAuthListFlows(t *testing.T) { - t.Run("regular flags", func(t *testing.T) { - mocktest.TestRunMockTestWithFlags( - t, - "--access-token", "string", - "matrix:bridges:auth", "list-flows", - "--bridge-id", "bridgeID", - ) - }) -} - -func TestMatrixBridgesAuthListLogins(t *testing.T) { - t.Run("regular flags", func(t *testing.T) { - mocktest.TestRunMockTestWithFlags( - t, - "--access-token", "string", - "matrix:bridges:auth", "list-logins", - "--bridge-id", "bridgeID", - ) - }) -} - -func TestMatrixBridgesAuthLogout(t *testing.T) { - t.Run("regular flags", func(t *testing.T) { - mocktest.TestRunMockTestWithFlags( - t, - "--access-token", "string", - "matrix:bridges:auth", "logout", - "--bridge-id", "bridgeID", - "--login-id", "bcc68892-b180-414f-9516-b4aadf7d0496", - ) - }) -} - -func TestMatrixBridgesAuthStartLogin(t *testing.T) { - t.Run("regular flags", func(t *testing.T) { - mocktest.TestRunMockTestWithFlags( - t, - "--access-token", "string", - "matrix:bridges:auth", "start-login", - "--bridge-id", "bridgeID", - "--flow-id", "qr", - "--login-id", "bcc68892-b180-414f-9516-b4aadf7d0496", - ) - }) -} - -func TestMatrixBridgesAuthSubmitCookies(t *testing.T) { - t.Run("regular flags", func(t *testing.T) { - mocktest.TestRunMockTestWithFlags( - t, - "--access-token", "string", - "matrix:bridges:auth", "submit-cookies", - "--bridge-id", "bridgeID", - "--login-process-id", "loginProcessID", - "--step-id", "stepID", - "--body", "{foo: string}", - ) - }) - - t.Run("piping data", func(t *testing.T) { - // Test piping YAML data over stdin - pipeData := []byte("foo: string") - mocktest.TestRunMockTestWithPipeAndFlags( - t, pipeData, - "--access-token", "string", - "matrix:bridges:auth", "submit-cookies", - "--bridge-id", "bridgeID", - "--login-process-id", "loginProcessID", - "--step-id", "stepID", - ) - }) -} - -func TestMatrixBridgesAuthSubmitUserInput(t *testing.T) { - t.Run("regular flags", func(t *testing.T) { - mocktest.TestRunMockTestWithFlags( - t, - "--access-token", "string", - "matrix:bridges:auth", "submit-user-input", - "--bridge-id", "bridgeID", - "--login-process-id", "loginProcessID", - "--step-id", "stepID", - "--body", "{foo: string}", - ) - }) - - t.Run("piping data", func(t *testing.T) { - // Test piping YAML data over stdin - pipeData := []byte("foo: string") - mocktest.TestRunMockTestWithPipeAndFlags( - t, pipeData, - "--access-token", "string", - "matrix:bridges:auth", "submit-user-input", - "--bridge-id", "bridgeID", - "--login-process-id", "loginProcessID", - "--step-id", "stepID", - ) - }) -} - -func TestMatrixBridgesAuthWaitForStep(t *testing.T) { - t.Run("regular flags", func(t *testing.T) { - mocktest.TestRunMockTestWithFlags( - t, - "--access-token", "string", - "matrix:bridges:auth", "wait-for-step", - "--bridge-id", "bridgeID", - "--login-process-id", "loginProcessID", - "--step-id", "stepID", - ) - }) -} - -func TestMatrixBridgesAuthWhoami(t *testing.T) { - t.Run("regular flags", func(t *testing.T) { - mocktest.TestRunMockTestWithFlags( - t, - "--access-token", "string", - "matrix:bridges:auth", "whoami", - "--bridge-id", "bridgeID", - ) - }) -} diff --git a/pkg/cmd/matrixbridgecapability_test.go b/pkg/cmd/matrixbridgecapability_test.go deleted file mode 100644 index d90cd364..00000000 --- a/pkg/cmd/matrixbridgecapability_test.go +++ /dev/null @@ -1,20 +0,0 @@ -// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -package cmd - -import ( - "testing" - - "github.com/beeper/desktop-api-cli/internal/mocktest" -) - -func TestMatrixBridgesCapabilitiesRetrieve(t *testing.T) { - t.Run("regular flags", func(t *testing.T) { - mocktest.TestRunMockTestWithFlags( - t, - "--access-token", "string", - "matrix:bridges:capabilities", "retrieve", - "--bridge-id", "bridgeID", - ) - }) -} diff --git a/pkg/cmd/matrixbridgecontact.go b/pkg/cmd/matrixbridgecontact.go deleted file mode 100644 index 55c3eb74..00000000 --- a/pkg/cmd/matrixbridgecontact.go +++ /dev/null @@ -1,87 +0,0 @@ -// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -package cmd - -import ( - "context" - "fmt" - - "github.com/beeper/desktop-api-cli/internal/apiquery" - "github.com/beeper/desktop-api-cli/internal/requestflag" - "github.com/beeper/desktop-api-go/v5" - "github.com/beeper/desktop-api-go/v5/option" - "github.com/tidwall/gjson" - "github.com/urfave/cli/v3" -) - -var matrixBridgesContactsList = cli.Command{ - Name: "list", - Usage: "Get a list of contacts.", - Suggest: true, - Flags: []cli.Flag{ - &requestflag.Flag[string]{ - Name: "bridge-id", - Required: true, - PathParam: "bridgeID", - }, - &requestflag.Flag[string]{ - Name: "login-id", - Usage: "An optional explicit login ID to do the action through.", - QueryPath: "login_id", - }, - }, - Action: handleMatrixBridgesContactsList, - HideHelpCommand: true, -} - -func handleMatrixBridgesContactsList(ctx context.Context, cmd *cli.Command) error { - client := beeperdesktopapi.NewClient(getDefaultRequestOptions(cmd)...) - unusedArgs := cmd.Args().Slice() - if !cmd.IsSet("bridge-id") && len(unusedArgs) > 0 { - cmd.Set("bridge-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.ArrayQueryFormatRepeat, - EmptyBody, - false, - ) - if err != nil { - return err - } - - params := beeperdesktopapi.MatrixBridgeContactListParams{} - - var res []byte - options = append(options, option.WithResponseBodyInto(&res)) - _, err = client.Matrix.Bridges.Contacts.List( - ctx, - cmd.Value("bridge-id").(string), - params, - options..., - ) - if err != nil { - return err - } - - obj := gjson.ParseBytes(res) - format := "json" - explicitFormat := cmd.Root().IsSet("format") - if explicitFormat { - format = cmd.Root().String("format") - } - transform := cmd.Root().String("transform") - return ShowJSON(obj, ShowJSONOpts{ - ExplicitFormat: explicitFormat, - Format: format, - RawOutput: cmd.Root().Bool("raw-output"), - Title: "matrix:bridges:contacts list", - Transform: transform, - }) -} diff --git a/pkg/cmd/matrixbridgecontact_test.go b/pkg/cmd/matrixbridgecontact_test.go deleted file mode 100644 index df14d107..00000000 --- a/pkg/cmd/matrixbridgecontact_test.go +++ /dev/null @@ -1,21 +0,0 @@ -// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -package cmd - -import ( - "testing" - - "github.com/beeper/desktop-api-cli/internal/mocktest" -) - -func TestMatrixBridgesContactsList(t *testing.T) { - t.Run("regular flags", func(t *testing.T) { - mocktest.TestRunMockTestWithFlags( - t, - "--access-token", "string", - "matrix:bridges:contacts", "list", - "--bridge-id", "bridgeID", - "--login-id", "bcc68892-b180-414f-9516-b4aadf7d0496", - ) - }) -} diff --git a/pkg/cmd/matrixbridgeroom.go b/pkg/cmd/matrixbridgeroom.go deleted file mode 100644 index 20d611df..00000000 --- a/pkg/cmd/matrixbridgeroom.go +++ /dev/null @@ -1,240 +0,0 @@ -// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -package cmd - -import ( - "context" - "fmt" - - "github.com/beeper/desktop-api-cli/internal/apiquery" - "github.com/beeper/desktop-api-cli/internal/requestflag" - "github.com/beeper/desktop-api-go/v5" - "github.com/beeper/desktop-api-go/v5/option" - "github.com/tidwall/gjson" - "github.com/urfave/cli/v3" -) - -var matrixBridgesRoomsCreateDm = cli.Command{ - Name: "create-dm", - Usage: "Create a direct chat with a user on the remote network.", - Suggest: true, - Flags: []cli.Flag{ - &requestflag.Flag[string]{ - Name: "bridge-id", - Required: true, - PathParam: "bridgeID", - }, - &requestflag.Flag[string]{ - Name: "identifier", - Required: true, - PathParam: "identifier", - }, - &requestflag.Flag[string]{ - Name: "login-id", - Usage: "An optional explicit login ID to do the action through.", - QueryPath: "login_id", - }, - }, - Action: handleMatrixBridgesRoomsCreateDm, - HideHelpCommand: true, -} - -var matrixBridgesRoomsCreateGroup = requestflag.WithInnerFlags(cli.Command{ - Name: "create-group", - Usage: "Create a group chat on the remote network.", - Suggest: true, - Flags: []cli.Flag{ - &requestflag.Flag[string]{ - Name: "bridge-id", - Required: true, - PathParam: "bridgeID", - }, - &requestflag.Flag[string]{ - Name: "group-type", - Required: true, - PathParam: "groupType", - }, - &requestflag.Flag[string]{ - Name: "login-id", - Usage: "An optional explicit login ID to do the action through.", - QueryPath: "login_id", - }, - &requestflag.Flag[map[string]any]{ - Name: "avatar", - Usage: "The `m.room.avatar` event content for the room.", - BodyPath: "avatar", - }, - &requestflag.Flag[map[string]any]{ - Name: "disappear", - Usage: "The `com.beeper.disappearing_timer` event content for the room.", - BodyPath: "disappear", - }, - &requestflag.Flag[map[string]any]{ - Name: "name", - Usage: "The `m.room.name` event content for the room.", - BodyPath: "name", - }, - &requestflag.Flag[any]{ - Name: "parent", - BodyPath: "parent", - }, - &requestflag.Flag[[]string]{ - Name: "participant", - Usage: "The users to add to the group initially.", - BodyPath: "participants", - }, - &requestflag.Flag[string]{ - Name: "room-id", - Usage: "An existing Matrix room ID to bridge to.\nThe other parameters must be already in sync with the room state when using this parameter.\n", - BodyPath: "room_id", - }, - &requestflag.Flag[map[string]any]{ - Name: "topic", - Usage: "The `m.room.topic` event content for the room.", - BodyPath: "topic", - }, - &requestflag.Flag[string]{ - Name: "type", - Usage: "The type of group to create.", - BodyPath: "type", - }, - &requestflag.Flag[string]{ - Name: "username", - Usage: "The public username for the created group.", - BodyPath: "username", - }, - }, - Action: handleMatrixBridgesRoomsCreateGroup, - HideHelpCommand: true, -}, map[string][]requestflag.HasOuterFlag{ - "avatar": { - &requestflag.InnerFlag[string]{ - Name: "avatar.url", - InnerField: "url", - }, - }, - "disappear": { - &requestflag.InnerFlag[float64]{ - Name: "disappear.timer", - InnerField: "timer", - }, - &requestflag.InnerFlag[string]{ - Name: "disappear.type", - InnerField: "type", - }, - }, - "name": { - &requestflag.InnerFlag[string]{ - Name: "name.name", - InnerField: "name", - }, - }, - "topic": { - &requestflag.InnerFlag[string]{ - Name: "topic.topic", - InnerField: "topic", - }, - }, -}) - -func handleMatrixBridgesRoomsCreateDm(ctx context.Context, cmd *cli.Command) error { - client := beeperdesktopapi.NewClient(getDefaultRequestOptions(cmd)...) - unusedArgs := cmd.Args().Slice() - if !cmd.IsSet("identifier") && len(unusedArgs) > 0 { - cmd.Set("identifier", unusedArgs[0]) - unusedArgs = unusedArgs[1:] - } - if len(unusedArgs) > 0 { - return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) - } - - options, err := flagOptions( - cmd, - apiquery.NestedQueryFormatBrackets, - apiquery.ArrayQueryFormatRepeat, - EmptyBody, - false, - ) - if err != nil { - return err - } - - params := beeperdesktopapi.MatrixBridgeRoomNewDmParams{ - BridgeID: cmd.Value("bridge-id").(string), - } - - var res []byte - options = append(options, option.WithResponseBodyInto(&res)) - _, err = client.Matrix.Bridges.Rooms.NewDm( - ctx, - cmd.Value("identifier").(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: "matrix:bridges:rooms create-dm", - Transform: transform, - }) -} - -func handleMatrixBridgesRoomsCreateGroup(ctx context.Context, cmd *cli.Command) error { - client := beeperdesktopapi.NewClient(getDefaultRequestOptions(cmd)...) - unusedArgs := cmd.Args().Slice() - if !cmd.IsSet("group-type") && len(unusedArgs) > 0 { - cmd.Set("group-type", unusedArgs[0]) - unusedArgs = unusedArgs[1:] - } - if len(unusedArgs) > 0 { - return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) - } - - options, err := flagOptions( - cmd, - apiquery.NestedQueryFormatBrackets, - apiquery.ArrayQueryFormatRepeat, - ApplicationJSON, - false, - ) - if err != nil { - return err - } - - params := beeperdesktopapi.MatrixBridgeRoomNewGroupParams{ - BridgeID: cmd.Value("bridge-id").(string), - } - - var res []byte - options = append(options, option.WithResponseBodyInto(&res)) - _, err = client.Matrix.Bridges.Rooms.NewGroup( - ctx, - cmd.Value("group-type").(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: "matrix:bridges:rooms create-group", - Transform: transform, - }) -} diff --git a/pkg/cmd/matrixbridgeroom_test.go b/pkg/cmd/matrixbridgeroom_test.go deleted file mode 100644 index 4e4c59e9..00000000 --- a/pkg/cmd/matrixbridgeroom_test.go +++ /dev/null @@ -1,98 +0,0 @@ -// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -package cmd - -import ( - "testing" - - "github.com/beeper/desktop-api-cli/internal/mocktest" - "github.com/beeper/desktop-api-cli/internal/requestflag" -) - -func TestMatrixBridgesRoomsCreateDm(t *testing.T) { - t.Run("regular flags", func(t *testing.T) { - mocktest.TestRunMockTestWithFlags( - t, - "--access-token", "string", - "matrix:bridges:rooms", "create-dm", - "--bridge-id", "bridgeID", - "--identifier", "identifier", - "--login-id", "bcc68892-b180-414f-9516-b4aadf7d0496", - ) - }) -} - -func TestMatrixBridgesRoomsCreateGroup(t *testing.T) { - t.Run("regular flags", func(t *testing.T) { - mocktest.TestRunMockTestWithFlags( - t, - "--access-token", "string", - "matrix:bridges:rooms", "create-group", - "--bridge-id", "bridgeID", - "--group-type", "groupType", - "--login-id", "bcc68892-b180-414f-9516-b4aadf7d0496", - "--avatar", "{url: url}", - "--disappear", "{timer: 0, type: type}", - "--name", "{name: name}", - "--parent", "{}", - "--participant", "string", - "--room-id", "room_id", - "--topic", "{topic: topic}", - "--type", "channel", - "--username", "username", - ) - }) - - t.Run("inner flags", func(t *testing.T) { - // Check that inner flags have been set up correctly - requestflag.CheckInnerFlags(matrixBridgesRoomsCreateGroup) - - // Alternative argument passing style using inner flags - mocktest.TestRunMockTestWithFlags( - t, - "--access-token", "string", - "matrix:bridges:rooms", "create-group", - "--bridge-id", "bridgeID", - "--group-type", "groupType", - "--login-id", "bcc68892-b180-414f-9516-b4aadf7d0496", - "--avatar.url", "url", - "--disappear.timer", "0", - "--disappear.type", "type", - "--name.name", "name", - "--parent", "{}", - "--participant", "string", - "--room-id", "room_id", - "--topic.topic", "topic", - "--type", "channel", - "--username", "username", - ) - }) - - t.Run("piping data", func(t *testing.T) { - // Test piping YAML data over stdin - pipeData := []byte("" + - "avatar:\n" + - " url: url\n" + - "disappear:\n" + - " timer: 0\n" + - " type: type\n" + - "name:\n" + - " name: name\n" + - "parent: {}\n" + - "participants:\n" + - " - string\n" + - "room_id: room_id\n" + - "topic:\n" + - " topic: topic\n" + - "type: channel\n" + - "username: username\n") - mocktest.TestRunMockTestWithPipeAndFlags( - t, pipeData, - "--access-token", "string", - "matrix:bridges:rooms", "create-group", - "--bridge-id", "bridgeID", - "--group-type", "groupType", - "--login-id", "bcc68892-b180-414f-9516-b4aadf7d0496", - ) - }) -} diff --git a/pkg/cmd/matrixbridgeuser.go b/pkg/cmd/matrixbridgeuser.go deleted file mode 100644 index 4596e5cd..00000000 --- a/pkg/cmd/matrixbridgeuser.go +++ /dev/null @@ -1,168 +0,0 @@ -// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -package cmd - -import ( - "context" - "fmt" - - "github.com/beeper/desktop-api-cli/internal/apiquery" - "github.com/beeper/desktop-api-cli/internal/requestflag" - "github.com/beeper/desktop-api-go/v5" - "github.com/beeper/desktop-api-go/v5/option" - "github.com/tidwall/gjson" - "github.com/urfave/cli/v3" -) - -var matrixBridgesUsersResolve = cli.Command{ - Name: "resolve", - Usage: "Resolve an identifier to a user on the remote network.", - Suggest: true, - Flags: []cli.Flag{ - &requestflag.Flag[string]{ - Name: "bridge-id", - Required: true, - PathParam: "bridgeID", - }, - &requestflag.Flag[string]{ - Name: "identifier", - Required: true, - PathParam: "identifier", - }, - &requestflag.Flag[string]{ - Name: "login-id", - Usage: "An optional explicit login ID to do the action through.", - QueryPath: "login_id", - }, - }, - Action: handleMatrixBridgesUsersResolve, - HideHelpCommand: true, -} - -var matrixBridgesUsersSearch = cli.Command{ - Name: "search", - Usage: "Search for users on the remote network", - Suggest: true, - Flags: []cli.Flag{ - &requestflag.Flag[string]{ - Name: "bridge-id", - Required: true, - PathParam: "bridgeID", - }, - &requestflag.Flag[string]{ - Name: "login-id", - Usage: "An optional explicit login ID to do the action through.", - QueryPath: "login_id", - }, - &requestflag.Flag[string]{ - Name: "query", - Usage: "The search query to send to the remote network", - BodyPath: "query", - }, - }, - Action: handleMatrixBridgesUsersSearch, - HideHelpCommand: true, -} - -func handleMatrixBridgesUsersResolve(ctx context.Context, cmd *cli.Command) error { - client := beeperdesktopapi.NewClient(getDefaultRequestOptions(cmd)...) - unusedArgs := cmd.Args().Slice() - if !cmd.IsSet("identifier") && len(unusedArgs) > 0 { - cmd.Set("identifier", unusedArgs[0]) - unusedArgs = unusedArgs[1:] - } - if len(unusedArgs) > 0 { - return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) - } - - options, err := flagOptions( - cmd, - apiquery.NestedQueryFormatBrackets, - apiquery.ArrayQueryFormatRepeat, - EmptyBody, - false, - ) - if err != nil { - return err - } - - params := beeperdesktopapi.MatrixBridgeUserResolveParams{ - BridgeID: cmd.Value("bridge-id").(string), - } - - var res []byte - options = append(options, option.WithResponseBodyInto(&res)) - _, err = client.Matrix.Bridges.Users.Resolve( - ctx, - cmd.Value("identifier").(string), - params, - options..., - ) - if err != nil { - return err - } - - obj := gjson.ParseBytes(res) - format := "json" - explicitFormat := cmd.Root().IsSet("format") - if explicitFormat { - format = cmd.Root().String("format") - } - transform := cmd.Root().String("transform") - return ShowJSON(obj, ShowJSONOpts{ - ExplicitFormat: explicitFormat, - Format: format, - RawOutput: cmd.Root().Bool("raw-output"), - Title: "matrix:bridges:users resolve", - Transform: transform, - }) -} - -func handleMatrixBridgesUsersSearch(ctx context.Context, cmd *cli.Command) error { - client := beeperdesktopapi.NewClient(getDefaultRequestOptions(cmd)...) - unusedArgs := cmd.Args().Slice() - if !cmd.IsSet("bridge-id") && len(unusedArgs) > 0 { - cmd.Set("bridge-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.ArrayQueryFormatRepeat, - ApplicationJSON, - false, - ) - if err != nil { - return err - } - - params := beeperdesktopapi.MatrixBridgeUserSearchParams{} - - var res []byte - options = append(options, option.WithResponseBodyInto(&res)) - _, err = client.Matrix.Bridges.Users.Search( - ctx, - cmd.Value("bridge-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: "matrix:bridges:users search", - Transform: transform, - }) -} diff --git a/pkg/cmd/matrixbridgeuser_test.go b/pkg/cmd/matrixbridgeuser_test.go deleted file mode 100644 index c3f11aa1..00000000 --- a/pkg/cmd/matrixbridgeuser_test.go +++ /dev/null @@ -1,47 +0,0 @@ -// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -package cmd - -import ( - "testing" - - "github.com/beeper/desktop-api-cli/internal/mocktest" -) - -func TestMatrixBridgesUsersResolve(t *testing.T) { - t.Run("regular flags", func(t *testing.T) { - mocktest.TestRunMockTestWithFlags( - t, - "--access-token", "string", - "matrix:bridges:users", "resolve", - "--bridge-id", "bridgeID", - "--identifier", "identifier", - "--login-id", "bcc68892-b180-414f-9516-b4aadf7d0496", - ) - }) -} - -func TestMatrixBridgesUsersSearch(t *testing.T) { - t.Run("regular flags", func(t *testing.T) { - mocktest.TestRunMockTestWithFlags( - t, - "--access-token", "string", - "matrix:bridges:users", "search", - "--bridge-id", "bridgeID", - "--login-id", "bcc68892-b180-414f-9516-b4aadf7d0496", - "--query", "query", - ) - }) - - t.Run("piping data", func(t *testing.T) { - // Test piping YAML data over stdin - pipeData := []byte("query: query") - mocktest.TestRunMockTestWithPipeAndFlags( - t, pipeData, - "--access-token", "string", - "matrix:bridges:users", "search", - "--bridge-id", "bridgeID", - "--login-id", "bcc68892-b180-414f-9516-b4aadf7d0496", - ) - }) -} diff --git a/pkg/cmd/matrixroom.go b/pkg/cmd/matrixroom.go deleted file mode 100644 index f870eaae..00000000 --- a/pkg/cmd/matrixroom.go +++ /dev/null @@ -1,337 +0,0 @@ -// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -package cmd - -import ( - "context" - "fmt" - - "github.com/beeper/desktop-api-cli/internal/apiquery" - "github.com/beeper/desktop-api-cli/internal/requestflag" - "github.com/beeper/desktop-api-go/v5" - "github.com/beeper/desktop-api-go/v5/option" - "github.com/tidwall/gjson" - "github.com/urfave/cli/v3" -) - -var matrixRoomsCreate = requestflag.WithInnerFlags(cli.Command{ - Name: "create", - Usage: "Create a new room with various configuration options.", - Suggest: true, - Flags: []cli.Flag{ - &requestflag.Flag[any]{ - Name: "creation-content", - Usage: "Extra keys, such as `m.federate`, to be added to the content\nof the [`m.room.create`](https://spec.matrix.org/v1.18/client-server-api/#mroomcreate) event.\n\nThe server will overwrite the following\nkeys: `creator`, `room_version`. Future versions of the specification\nmay allow the server to overwrite other keys.\n\nWhen using the `trusted_private_chat` preset, the server SHOULD combine\n`additional_creators` specified here and the `invite` array into the\neventual `m.room.create` event's `additional_creators`, deduplicating\nbetween the two parameters.", - BodyPath: "creation_content", - }, - &requestflag.Flag[[]map[string]any]{ - Name: "initial-state", - Usage: "A list of state events to set in the new room. This allows\nthe user to override the default state events set in the new\nroom. The expected format of the state events are an object\nwith type, state_key and content keys set.\n\nTakes precedence over events set by `preset`, but gets\noverridden by `name` and `topic` keys.", - BodyPath: "initial_state", - }, - &requestflag.Flag[[]string]{ - Name: "invite", - Usage: "A list of user IDs to invite to the room. This will tell the\nserver to invite everyone in the list to the newly created room.", - BodyPath: "invite", - }, - &requestflag.Flag[[]map[string]any]{ - Name: "invite-3pid", - Usage: "A list of objects representing third-party IDs to invite into\nthe room.", - BodyPath: "invite_3pid", - }, - &requestflag.Flag[bool]{ - Name: "is-direct", - Usage: "This flag makes the server set the `is_direct` flag on the\n`m.room.member` events sent to the users in `invite` and\n`invite_3pid`. See [Direct Messaging](https://spec.matrix.org/v1.18/client-server-api/#direct-messaging) for more information.", - BodyPath: "is_direct", - }, - &requestflag.Flag[string]{ - Name: "name", - Usage: "If this is included, an [`m.room.name`](https://spec.matrix.org/v1.18/client-server-api/#mroomname) event\nwill be sent into the room to indicate the name for the room.\nThis overwrites any [`m.room.name`](https://spec.matrix.org/v1.18/client-server-api/#mroomname)\nevent in `initial_state`.", - BodyPath: "name", - }, - &requestflag.Flag[any]{ - Name: "power-level-content-override", - Usage: "The power level content to override in the default power level\nevent. This object is applied on top of the generated\n[`m.room.power_levels`](https://spec.matrix.org/v1.18/client-server-api/#mroompower_levels)\nevent content prior to it being sent to the room. Defaults to\noverriding nothing.", - BodyPath: "power_level_content_override", - }, - &requestflag.Flag[string]{ - Name: "preset", - Usage: "Convenience parameter for setting various default state events\nbased on a preset.\n\nIf unspecified, the server should use the `visibility` to determine\nwhich preset to use. A visibility of `public` equates to a preset of\n`public_chat` and `private` visibility equates to a preset of\n`private_chat`.", - BodyPath: "preset", - }, - &requestflag.Flag[string]{ - Name: "room-alias-name", - Usage: "The desired room alias **local part**. If this is included, a\nroom alias will be created and mapped to the newly created\nroom. The alias will belong on the *same* homeserver which\ncreated the room. For example, if this was set to \"foo\" and\nsent to the homeserver \"example.com\" the complete room alias\nwould be `#foo:example.com`.\n\nThe complete room alias will become the canonical alias for\nthe room and an `m.room.canonical_alias` event will be sent\ninto the room.", - BodyPath: "room_alias_name", - }, - &requestflag.Flag[string]{ - Name: "room-version", - Usage: "The room version to set for the room. If not provided, the homeserver is\nto use its configured default. If provided, the homeserver will return a\n400 error with the errcode `M_UNSUPPORTED_ROOM_VERSION` if it does not\nsupport the room version.", - BodyPath: "room_version", - }, - &requestflag.Flag[string]{ - Name: "topic", - Usage: "If this is included, an [`m.room.topic`](https://spec.matrix.org/v1.18/client-server-api/#mroomtopic)\nevent with a `text/plain` mimetype will be sent into the room\nto indicate the topic for the room. This overwrites any\n[`m.room.topic`](https://spec.matrix.org/v1.18/client-server-api/#mroomtopic) event in `initial_state`.", - BodyPath: "topic", - }, - &requestflag.Flag[string]{ - Name: "visibility", - Usage: "The room's visibility in the server's\n[published room directory](https://spec.matrix.org/v1.18/client-server-api#published-room-directory).\nDefaults to `private`.", - BodyPath: "visibility", - }, - }, - Action: handleMatrixRoomsCreate, - HideHelpCommand: true, -}, map[string][]requestflag.HasOuterFlag{ - "initial-state": { - &requestflag.InnerFlag[any]{ - Name: "initial-state.content", - Usage: "The content of the event.", - InnerField: "content", - }, - &requestflag.InnerFlag[string]{ - Name: "initial-state.type", - Usage: "The type of event to send.", - InnerField: "type", - }, - &requestflag.InnerFlag[string]{ - Name: "initial-state.state-key", - Usage: "The state_key of the state event. Defaults to an empty string.", - InnerField: "state_key", - }, - }, - "invite-3pid": { - &requestflag.InnerFlag[string]{ - Name: "invite-3pid.address", - Usage: "The invitee's third-party identifier.", - InnerField: "address", - }, - &requestflag.InnerFlag[string]{ - Name: "invite-3pid.id-access-token", - Usage: "An access token previously registered with the identity server. Servers\ncan treat this as optional to distinguish between r0.5-compatible clients\nand this specification version.", - InnerField: "id_access_token", - }, - &requestflag.InnerFlag[string]{ - Name: "invite-3pid.id-server", - Usage: "The hostname+port of the identity server which should be used for third-party identifier lookups.", - InnerField: "id_server", - }, - &requestflag.InnerFlag[string]{ - Name: "invite-3pid.medium", - Usage: "The kind of address being passed in the address field, for example `email`\n(see [the list of recognised values](https://spec.matrix.org/v1.18/appendices/#3pid-types)).", - InnerField: "medium", - }, - }, -}) - -var matrixRoomsJoin = requestflag.WithInnerFlags(cli.Command{ - Name: "join", - Usage: "_Note that this API takes either a room ID or alias, unlike_\n`/rooms/{roomId}/join`.", - Suggest: true, - Flags: []cli.Flag{ - &requestflag.Flag[string]{ - Name: "room-id-or-alias", - Required: true, - PathParam: "roomIdOrAlias", - }, - &requestflag.Flag[[]string]{ - Name: "via", - Usage: "The servers to attempt to join the room through. One of the servers\nmust be participating in the room.", - QueryPath: "via", - }, - &requestflag.Flag[string]{ - Name: "reason", - Usage: "Optional reason to be included as the `reason` on the subsequent\nmembership event.", - BodyPath: "reason", - }, - &requestflag.Flag[map[string]any]{ - Name: "third-party-signed", - Usage: "A signature of an `m.third_party_invite` token to prove that this user\nowns a third-party identity which has been invited to the room.", - BodyPath: "third_party_signed", - }, - }, - Action: handleMatrixRoomsJoin, - HideHelpCommand: true, -}, map[string][]requestflag.HasOuterFlag{ - "third-party-signed": { - &requestflag.InnerFlag[string]{ - Name: "third-party-signed.token", - Usage: "The state key of the m.third_party_invite event.", - InnerField: "token", - }, - &requestflag.InnerFlag[string]{ - Name: "third-party-signed.mxid", - Usage: "The Matrix ID of the invitee.", - InnerField: "mxid", - }, - &requestflag.InnerFlag[string]{ - Name: "third-party-signed.sender", - Usage: "The Matrix ID of the user who issued the invite.", - InnerField: "sender", - }, - &requestflag.InnerFlag[map[string]any]{ - Name: "third-party-signed.signatures", - Usage: "A signatures object containing a signature of the entire signed object.", - InnerField: "signatures", - }, - }, -}) - -var matrixRoomsLeave = cli.Command{ - Name: "leave", - Usage: "This API stops a user participating in a particular room.", - Suggest: true, - Flags: []cli.Flag{ - &requestflag.Flag[string]{ - Name: "room-id", - Required: true, - PathParam: "roomId", - }, - &requestflag.Flag[string]{ - Name: "reason", - Usage: "Optional reason to be included as the `reason` on the subsequent\nmembership event.", - BodyPath: "reason", - }, - }, - Action: handleMatrixRoomsLeave, - HideHelpCommand: true, -} - -func handleMatrixRoomsCreate(ctx context.Context, cmd *cli.Command) error { - client := beeperdesktopapi.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.ArrayQueryFormatRepeat, - ApplicationJSON, - false, - ) - if err != nil { - return err - } - - params := beeperdesktopapi.MatrixRoomNewParams{} - - var res []byte - options = append(options, option.WithResponseBodyInto(&res)) - _, err = client.Matrix.Rooms.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: "matrix:rooms create", - Transform: transform, - }) -} - -func handleMatrixRoomsJoin(ctx context.Context, cmd *cli.Command) error { - client := beeperdesktopapi.NewClient(getDefaultRequestOptions(cmd)...) - unusedArgs := cmd.Args().Slice() - if !cmd.IsSet("room-id-or-alias") && len(unusedArgs) > 0 { - cmd.Set("room-id-or-alias", unusedArgs[0]) - unusedArgs = unusedArgs[1:] - } - if len(unusedArgs) > 0 { - return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) - } - - options, err := flagOptions( - cmd, - apiquery.NestedQueryFormatBrackets, - apiquery.ArrayQueryFormatRepeat, - ApplicationJSON, - false, - ) - if err != nil { - return err - } - - params := beeperdesktopapi.MatrixRoomJoinParams{} - - var res []byte - options = append(options, option.WithResponseBodyInto(&res)) - _, err = client.Matrix.Rooms.Join( - ctx, - cmd.Value("room-id-or-alias").(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: "matrix:rooms join", - Transform: transform, - }) -} - -func handleMatrixRoomsLeave(ctx context.Context, cmd *cli.Command) error { - client := beeperdesktopapi.NewClient(getDefaultRequestOptions(cmd)...) - unusedArgs := cmd.Args().Slice() - if !cmd.IsSet("room-id") && len(unusedArgs) > 0 { - cmd.Set("room-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.ArrayQueryFormatRepeat, - ApplicationJSON, - false, - ) - if err != nil { - return err - } - - params := beeperdesktopapi.MatrixRoomLeaveParams{} - - var res []byte - options = append(options, option.WithResponseBodyInto(&res)) - _, err = client.Matrix.Rooms.Leave( - ctx, - cmd.Value("room-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: "matrix:rooms leave", - Transform: transform, - }) -} diff --git a/pkg/cmd/matrixroom_test.go b/pkg/cmd/matrixroom_test.go deleted file mode 100644 index 241ec53a..00000000 --- a/pkg/cmd/matrixroom_test.go +++ /dev/null @@ -1,168 +0,0 @@ -// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -package cmd - -import ( - "testing" - - "github.com/beeper/desktop-api-cli/internal/mocktest" - "github.com/beeper/desktop-api-cli/internal/requestflag" -) - -func TestMatrixRoomsCreate(t *testing.T) { - t.Run("regular flags", func(t *testing.T) { - mocktest.TestRunMockTestWithFlags( - t, - "--access-token", "string", - "matrix:rooms", "create", - "--creation-content", "{m.federate: false}", - "--initial-state", "{content: {}, type: type, state_key: state_key}", - "--invite", "string", - "--invite-3pid", "{address: cheeky@monkey.com, id_access_token: abc123_OpaqueString, id_server: matrix.org, medium: email}", - "--is-direct=true", - "--name", "The Grand Duke Pub", - "--power-level-content-override", "{}", - "--preset", "public_chat", - "--room-alias-name", "thepub", - "--room-version", "1", - "--topic", "All about happy hour", - "--visibility", "public", - ) - }) - - t.Run("inner flags", func(t *testing.T) { - // Check that inner flags have been set up correctly - requestflag.CheckInnerFlags(matrixRoomsCreate) - - // Alternative argument passing style using inner flags - mocktest.TestRunMockTestWithFlags( - t, - "--access-token", "string", - "matrix:rooms", "create", - "--creation-content", "{m.federate: false}", - "--initial-state.content", "{}", - "--initial-state.type", "type", - "--initial-state.state-key", "state_key", - "--invite", "string", - "--invite-3pid.address", "cheeky@monkey.com", - "--invite-3pid.id-access-token", "abc123_OpaqueString", - "--invite-3pid.id-server", "matrix.org", - "--invite-3pid.medium", "email", - "--is-direct=true", - "--name", "The Grand Duke Pub", - "--power-level-content-override", "{}", - "--preset", "public_chat", - "--room-alias-name", "thepub", - "--room-version", "1", - "--topic", "All about happy hour", - "--visibility", "public", - ) - }) - - t.Run("piping data", func(t *testing.T) { - // Test piping YAML data over stdin - pipeData := []byte("" + - "creation_content:\n" + - " m.federate: false\n" + - "initial_state:\n" + - " - content: {}\n" + - " type: type\n" + - " state_key: state_key\n" + - "invite:\n" + - " - string\n" + - "invite_3pid:\n" + - " - address: cheeky@monkey.com\n" + - " id_access_token: abc123_OpaqueString\n" + - " id_server: matrix.org\n" + - " medium: email\n" + - "is_direct: true\n" + - "name: The Grand Duke Pub\n" + - "power_level_content_override: {}\n" + - "preset: public_chat\n" + - "room_alias_name: thepub\n" + - "room_version: '1'\n" + - "topic: All about happy hour\n" + - "visibility: public\n") - mocktest.TestRunMockTestWithPipeAndFlags( - t, pipeData, - "--access-token", "string", - "matrix:rooms", "create", - ) - }) -} - -func TestMatrixRoomsJoin(t *testing.T) { - t.Run("regular flags", func(t *testing.T) { - mocktest.TestRunMockTestWithFlags( - t, - "--access-token", "string", - "matrix:rooms", "join", - "--room-id-or-alias", "!monkeys:matrix.org", - "--via", "string", - "--reason", "Looking for support", - "--third-party-signed", "{token: random8nonce, mxid: bob, sender: alice, signatures: {example.org: {ed25519:0: some9signature}}}", - ) - }) - - t.Run("inner flags", func(t *testing.T) { - // Check that inner flags have been set up correctly - requestflag.CheckInnerFlags(matrixRoomsJoin) - - // Alternative argument passing style using inner flags - mocktest.TestRunMockTestWithFlags( - t, - "--access-token", "string", - "matrix:rooms", "join", - "--room-id-or-alias", "!monkeys:matrix.org", - "--via", "string", - "--reason", "Looking for support", - "--third-party-signed.token", "random8nonce", - "--third-party-signed.mxid", "bob", - "--third-party-signed.sender", "alice", - "--third-party-signed.signatures", "{example.org: {ed25519:0: some9signature}}", - ) - }) - - t.Run("piping data", func(t *testing.T) { - // Test piping YAML data over stdin - pipeData := []byte("" + - "reason: Looking for support\n" + - "third_party_signed:\n" + - " token: random8nonce\n" + - " mxid: bob\n" + - " sender: alice\n" + - " signatures:\n" + - " example.org:\n" + - " ed25519:0: some9signature\n") - mocktest.TestRunMockTestWithPipeAndFlags( - t, pipeData, - "--access-token", "string", - "matrix:rooms", "join", - "--room-id-or-alias", "!monkeys:matrix.org", - "--via", "string", - ) - }) -} - -func TestMatrixRoomsLeave(t *testing.T) { - t.Run("regular flags", func(t *testing.T) { - mocktest.TestRunMockTestWithFlags( - t, - "--access-token", "string", - "matrix:rooms", "leave", - "--room-id", "!nkl290a:matrix.org", - "--reason", "Saying farewell - thanks for the support!", - ) - }) - - t.Run("piping data", func(t *testing.T) { - // Test piping YAML data over stdin - pipeData := []byte("reason: Saying farewell - thanks for the support!") - mocktest.TestRunMockTestWithPipeAndFlags( - t, pipeData, - "--access-token", "string", - "matrix:rooms", "leave", - "--room-id", "!nkl290a:matrix.org", - ) - }) -} diff --git a/pkg/cmd/matrixroomaccountdata.go b/pkg/cmd/matrixroomaccountdata.go deleted file mode 100644 index 0d786806..00000000 --- a/pkg/cmd/matrixroomaccountdata.go +++ /dev/null @@ -1,177 +0,0 @@ -// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -package cmd - -import ( - "context" - "fmt" - - "github.com/beeper/desktop-api-cli/internal/apiquery" - "github.com/beeper/desktop-api-cli/internal/requestflag" - "github.com/beeper/desktop-api-go/v5" - "github.com/beeper/desktop-api-go/v5/option" - "github.com/tidwall/gjson" - "github.com/urfave/cli/v3" -) - -var matrixRoomsAccountDataRetrieve = cli.Command{ - Name: "retrieve", - Usage: "Get some account data for the client on a given room. This config is only\nvisible to the user that set the account data.", - Suggest: true, - Flags: []cli.Flag{ - &requestflag.Flag[string]{ - Name: "user-id", - Required: true, - PathParam: "userId", - }, - &requestflag.Flag[string]{ - Name: "room-id", - Required: true, - PathParam: "roomId", - }, - &requestflag.Flag[string]{ - Name: "type", - Required: true, - PathParam: "type", - }, - }, - Action: handleMatrixRoomsAccountDataRetrieve, - HideHelpCommand: true, -} - -var matrixRoomsAccountDataUpdate = cli.Command{ - Name: "update", - Usage: "Set some account data for the client on a given room. This config is only\nvisible to the user that set the account data. The config will be delivered to\nclients in the per-room entries via\n[/sync](https://spec.matrix.org/v1.18/client-server-api/#get_matrixclientv3sync).", - Suggest: true, - Flags: []cli.Flag{ - &requestflag.Flag[string]{ - Name: "user-id", - Required: true, - PathParam: "userId", - }, - &requestflag.Flag[string]{ - Name: "room-id", - Required: true, - PathParam: "roomId", - }, - &requestflag.Flag[string]{ - Name: "type", - Required: true, - PathParam: "type", - }, - &requestflag.Flag[any]{ - Name: "body", - Required: true, - BodyRoot: true, - }, - }, - Action: handleMatrixRoomsAccountDataUpdate, - HideHelpCommand: true, -} - -func handleMatrixRoomsAccountDataRetrieve(ctx context.Context, cmd *cli.Command) error { - client := beeperdesktopapi.NewClient(getDefaultRequestOptions(cmd)...) - unusedArgs := cmd.Args().Slice() - if !cmd.IsSet("type") && len(unusedArgs) > 0 { - cmd.Set("type", unusedArgs[0]) - unusedArgs = unusedArgs[1:] - } - if len(unusedArgs) > 0 { - return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) - } - - options, err := flagOptions( - cmd, - apiquery.NestedQueryFormatBrackets, - apiquery.ArrayQueryFormatRepeat, - EmptyBody, - false, - ) - if err != nil { - return err - } - - params := beeperdesktopapi.MatrixRoomAccountDataGetParams{ - UserID: cmd.Value("user-id").(string), - RoomID: cmd.Value("room-id").(string), - } - - var res []byte - options = append(options, option.WithResponseBodyInto(&res)) - _, err = client.Matrix.Rooms.AccountData.Get( - ctx, - cmd.Value("type").(string), - params, - options..., - ) - if err != nil { - return err - } - - obj := gjson.ParseBytes(res) - format := "json" - explicitFormat := cmd.Root().IsSet("format") - if explicitFormat { - format = cmd.Root().String("format") - } - transform := cmd.Root().String("transform") - return ShowJSON(obj, ShowJSONOpts{ - ExplicitFormat: explicitFormat, - Format: format, - RawOutput: cmd.Root().Bool("raw-output"), - Title: "matrix:rooms:account-data retrieve", - Transform: transform, - }) -} - -func handleMatrixRoomsAccountDataUpdate(ctx context.Context, cmd *cli.Command) error { - client := beeperdesktopapi.NewClient(getDefaultRequestOptions(cmd)...) - unusedArgs := cmd.Args().Slice() - if !cmd.IsSet("type") && len(unusedArgs) > 0 { - cmd.Set("type", unusedArgs[0]) - unusedArgs = unusedArgs[1:] - } - if len(unusedArgs) > 0 { - return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) - } - - options, err := flagOptions( - cmd, - apiquery.NestedQueryFormatBrackets, - apiquery.ArrayQueryFormatRepeat, - ApplicationJSON, - false, - ) - if err != nil { - return err - } - - params := beeperdesktopapi.MatrixRoomAccountDataUpdateParams{ - UserID: cmd.Value("user-id").(string), - RoomID: cmd.Value("room-id").(string), - } - - var res []byte - options = append(options, option.WithResponseBodyInto(&res)) - _, err = client.Matrix.Rooms.AccountData.Update( - ctx, - cmd.Value("type").(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: "matrix:rooms:account-data update", - Transform: transform, - }) -} diff --git a/pkg/cmd/matrixroomaccountdata_test.go b/pkg/cmd/matrixroomaccountdata_test.go deleted file mode 100644 index 08d8c650..00000000 --- a/pkg/cmd/matrixroomaccountdata_test.go +++ /dev/null @@ -1,49 +0,0 @@ -// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -package cmd - -import ( - "testing" - - "github.com/beeper/desktop-api-cli/internal/mocktest" -) - -func TestMatrixRoomsAccountDataRetrieve(t *testing.T) { - t.Run("regular flags", func(t *testing.T) { - mocktest.TestRunMockTestWithFlags( - t, - "--access-token", "string", - "matrix:rooms:account-data", "retrieve", - "--user-id", "@alice:example.com", - "--room-id", "!726s6s6q:example.com", - "--type", "org.example.custom.room.config", - ) - }) -} - -func TestMatrixRoomsAccountDataUpdate(t *testing.T) { - t.Run("regular flags", func(t *testing.T) { - mocktest.TestRunMockTestWithFlags( - t, - "--access-token", "string", - "matrix:rooms:account-data", "update", - "--user-id", "@alice:example.com", - "--room-id", "!726s6s6q:example.com", - "--type", "org.example.custom.room.config", - "--body", "{custom_account_data_key: custom_account_data_value}", - ) - }) - - t.Run("piping data", func(t *testing.T) { - // Test piping YAML data over stdin - pipeData := []byte("custom_account_data_key: custom_account_data_value") - mocktest.TestRunMockTestWithPipeAndFlags( - t, pipeData, - "--access-token", "string", - "matrix:rooms:account-data", "update", - "--user-id", "@alice:example.com", - "--room-id", "!726s6s6q:example.com", - "--type", "org.example.custom.room.config", - ) - }) -} diff --git a/pkg/cmd/matrixroomevent.go b/pkg/cmd/matrixroomevent.go deleted file mode 100644 index 072ae10a..00000000 --- a/pkg/cmd/matrixroomevent.go +++ /dev/null @@ -1,89 +0,0 @@ -// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -package cmd - -import ( - "context" - "fmt" - - "github.com/beeper/desktop-api-cli/internal/apiquery" - "github.com/beeper/desktop-api-cli/internal/requestflag" - "github.com/beeper/desktop-api-go/v5" - "github.com/beeper/desktop-api-go/v5/option" - "github.com/tidwall/gjson" - "github.com/urfave/cli/v3" -) - -var matrixRoomsEventsRetrieve = cli.Command{ - Name: "retrieve", - Usage: "Get a single event based on `roomId/eventId`. You must have permission to\nretrieve this event e.g. by being a member in the room for this event.", - Suggest: true, - Flags: []cli.Flag{ - &requestflag.Flag[string]{ - Name: "room-id", - Required: true, - PathParam: "roomId", - }, - &requestflag.Flag[string]{ - Name: "event-id", - Required: true, - PathParam: "eventId", - }, - }, - Action: handleMatrixRoomsEventsRetrieve, - HideHelpCommand: true, -} - -func handleMatrixRoomsEventsRetrieve(ctx context.Context, cmd *cli.Command) error { - client := beeperdesktopapi.NewClient(getDefaultRequestOptions(cmd)...) - unusedArgs := cmd.Args().Slice() - if !cmd.IsSet("event-id") && len(unusedArgs) > 0 { - cmd.Set("event-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.ArrayQueryFormatRepeat, - EmptyBody, - false, - ) - if err != nil { - return err - } - - params := beeperdesktopapi.MatrixRoomEventGetParams{ - RoomID: cmd.Value("room-id").(string), - } - - var res []byte - options = append(options, option.WithResponseBodyInto(&res)) - _, err = client.Matrix.Rooms.Events.Get( - ctx, - cmd.Value("event-id").(string), - params, - options..., - ) - if err != nil { - return err - } - - obj := gjson.ParseBytes(res) - format := "json" - explicitFormat := cmd.Root().IsSet("format") - if explicitFormat { - format = cmd.Root().String("format") - } - transform := cmd.Root().String("transform") - return ShowJSON(obj, ShowJSONOpts{ - ExplicitFormat: explicitFormat, - Format: format, - RawOutput: cmd.Root().Bool("raw-output"), - Title: "matrix:rooms:events retrieve", - Transform: transform, - }) -} diff --git a/pkg/cmd/matrixroomevent_test.go b/pkg/cmd/matrixroomevent_test.go deleted file mode 100644 index 31d44839..00000000 --- a/pkg/cmd/matrixroomevent_test.go +++ /dev/null @@ -1,21 +0,0 @@ -// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -package cmd - -import ( - "testing" - - "github.com/beeper/desktop-api-cli/internal/mocktest" -) - -func TestMatrixRoomsEventsRetrieve(t *testing.T) { - t.Run("regular flags", func(t *testing.T) { - mocktest.TestRunMockTestWithFlags( - t, - "--access-token", "string", - "matrix:rooms:events", "retrieve", - "--room-id", "!636q39766251:matrix.org", - "--event-id", "$asfDuShaf7Gafaw:matrix.org", - ) - }) -} diff --git a/pkg/cmd/matrixroomstate.go b/pkg/cmd/matrixroomstate.go deleted file mode 100644 index f09b1002..00000000 --- a/pkg/cmd/matrixroomstate.go +++ /dev/null @@ -1,160 +0,0 @@ -// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -package cmd - -import ( - "context" - "fmt" - - "github.com/beeper/desktop-api-cli/internal/apiquery" - "github.com/beeper/desktop-api-cli/internal/requestflag" - "github.com/beeper/desktop-api-go/v5" - "github.com/beeper/desktop-api-go/v5/option" - "github.com/tidwall/gjson" - "github.com/urfave/cli/v3" -) - -var matrixRoomsStateRetrieve = cli.Command{ - Name: "retrieve", - Usage: "Looks up the contents of a state event in a room. If the user is joined to the\nroom then the state is taken from the current state of the room. If the user has\nleft the room then the state is taken from the state of the room when they left.", - Suggest: true, - Flags: []cli.Flag{ - &requestflag.Flag[string]{ - Name: "room-id", - Required: true, - PathParam: "roomId", - }, - &requestflag.Flag[string]{ - Name: "event-type", - Required: true, - PathParam: "eventType", - }, - &requestflag.Flag[string]{ - Name: "state-key", - Required: true, - PathParam: "stateKey", - }, - &requestflag.Flag[string]{ - Name: "format", - Usage: "The format to use for the returned data. `content` (the default) will\nreturn only the content of the state event. `event` will return the entire\nevent in the usual format suitable for clients, including fields like event\nID, sender and timestamp.", - QueryPath: "format", - }, - }, - Action: handleMatrixRoomsStateRetrieve, - HideHelpCommand: true, -} - -var matrixRoomsStateList = cli.Command{ - Name: "list", - Usage: "Get the state events for the current state of a room.", - Suggest: true, - Flags: []cli.Flag{ - &requestflag.Flag[string]{ - Name: "room-id", - Required: true, - PathParam: "roomId", - }, - }, - Action: handleMatrixRoomsStateList, - HideHelpCommand: true, -} - -func handleMatrixRoomsStateRetrieve(ctx context.Context, cmd *cli.Command) error { - client := beeperdesktopapi.NewClient(getDefaultRequestOptions(cmd)...) - unusedArgs := cmd.Args().Slice() - if !cmd.IsSet("state-key") && len(unusedArgs) > 0 { - cmd.Set("state-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.ArrayQueryFormatRepeat, - EmptyBody, - false, - ) - if err != nil { - return err - } - - params := beeperdesktopapi.MatrixRoomStateGetParams{ - RoomID: cmd.Value("room-id").(string), - EventType: cmd.Value("event-type").(string), - } - - var res []byte - options = append(options, option.WithResponseBodyInto(&res)) - _, err = client.Matrix.Rooms.State.Get( - ctx, - cmd.Value("state-key").(string), - params, - options..., - ) - if err != nil { - return err - } - - obj := gjson.ParseBytes(res) - format := "json" - explicitFormat := cmd.Root().IsSet("format") - if explicitFormat { - format = cmd.Root().String("format") - } - transform := cmd.Root().String("transform") - return ShowJSON(obj, ShowJSONOpts{ - ExplicitFormat: explicitFormat, - Format: format, - RawOutput: cmd.Root().Bool("raw-output"), - Title: "matrix:rooms:state retrieve", - Transform: transform, - }) -} - -func handleMatrixRoomsStateList(ctx context.Context, cmd *cli.Command) error { - client := beeperdesktopapi.NewClient(getDefaultRequestOptions(cmd)...) - unusedArgs := cmd.Args().Slice() - if !cmd.IsSet("room-id") && len(unusedArgs) > 0 { - cmd.Set("room-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.ArrayQueryFormatRepeat, - EmptyBody, - false, - ) - if err != nil { - return err - } - - var res []byte - options = append(options, option.WithResponseBodyInto(&res)) - _, err = client.Matrix.Rooms.State.List(ctx, cmd.Value("room-id").(string), options...) - if err != nil { - return err - } - - obj := gjson.ParseBytes(res) - format := "json" - explicitFormat := cmd.Root().IsSet("format") - if explicitFormat { - format = cmd.Root().String("format") - } - transform := cmd.Root().String("transform") - return ShowJSON(obj, ShowJSONOpts{ - ExplicitFormat: explicitFormat, - Format: format, - RawOutput: cmd.Root().Bool("raw-output"), - Title: "matrix:rooms:state list", - Transform: transform, - }) -} diff --git a/pkg/cmd/matrixroomstate_test.go b/pkg/cmd/matrixroomstate_test.go deleted file mode 100644 index 8ff3c71e..00000000 --- a/pkg/cmd/matrixroomstate_test.go +++ /dev/null @@ -1,34 +0,0 @@ -// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -package cmd - -import ( - "testing" - - "github.com/beeper/desktop-api-cli/internal/mocktest" -) - -func TestMatrixRoomsStateRetrieve(t *testing.T) { - t.Run("regular flags", func(t *testing.T) { - mocktest.TestRunMockTestWithFlags( - t, - "--access-token", "string", - "matrix:rooms:state", "retrieve", - "--room-id", "!636q39766251:example.com", - "--event-type", "m.room.name", - "--state-key", "state_key", - "--format", "content", - ) - }) -} - -func TestMatrixRoomsStateList(t *testing.T) { - t.Run("regular flags", func(t *testing.T) { - mocktest.TestRunMockTestWithFlags( - t, - "--access-token", "string", - "matrix:rooms:state", "list", - "--room-id", "!636q39766251:example.com", - ) - }) -} diff --git a/pkg/cmd/matrixuser.go b/pkg/cmd/matrixuser.go deleted file mode 100644 index 50d1fb04..00000000 --- a/pkg/cmd/matrixuser.go +++ /dev/null @@ -1,75 +0,0 @@ -// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -package cmd - -import ( - "context" - "fmt" - - "github.com/beeper/desktop-api-cli/internal/apiquery" - "github.com/beeper/desktop-api-cli/internal/requestflag" - "github.com/beeper/desktop-api-go/v5" - "github.com/beeper/desktop-api-go/v5/option" - "github.com/tidwall/gjson" - "github.com/urfave/cli/v3" -) - -var matrixUsersRetrieveProfile = cli.Command{ - Name: "retrieve-profile", - Usage: "Get the complete profile for a user.", - Suggest: true, - Flags: []cli.Flag{ - &requestflag.Flag[string]{ - Name: "user-id", - Required: true, - PathParam: "userId", - }, - }, - Action: handleMatrixUsersRetrieveProfile, - HideHelpCommand: true, -} - -func handleMatrixUsersRetrieveProfile(ctx context.Context, cmd *cli.Command) error { - client := beeperdesktopapi.NewClient(getDefaultRequestOptions(cmd)...) - unusedArgs := cmd.Args().Slice() - if !cmd.IsSet("user-id") && len(unusedArgs) > 0 { - cmd.Set("user-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.ArrayQueryFormatRepeat, - EmptyBody, - false, - ) - if err != nil { - return err - } - - var res []byte - options = append(options, option.WithResponseBodyInto(&res)) - _, err = client.Matrix.Users.GetProfile(ctx, cmd.Value("user-id").(string), options...) - if err != nil { - return err - } - - obj := gjson.ParseBytes(res) - format := "json" - explicitFormat := cmd.Root().IsSet("format") - if explicitFormat { - format = cmd.Root().String("format") - } - transform := cmd.Root().String("transform") - return ShowJSON(obj, ShowJSONOpts{ - ExplicitFormat: explicitFormat, - Format: format, - RawOutput: cmd.Root().Bool("raw-output"), - Title: "matrix:users retrieve-profile", - Transform: transform, - }) -} diff --git a/pkg/cmd/matrixuser_test.go b/pkg/cmd/matrixuser_test.go deleted file mode 100644 index 6c48b75b..00000000 --- a/pkg/cmd/matrixuser_test.go +++ /dev/null @@ -1,20 +0,0 @@ -// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -package cmd - -import ( - "testing" - - "github.com/beeper/desktop-api-cli/internal/mocktest" -) - -func TestMatrixUsersRetrieveProfile(t *testing.T) { - t.Run("regular flags", func(t *testing.T) { - mocktest.TestRunMockTestWithFlags( - t, - "--access-token", "string", - "matrix:users", "retrieve-profile", - "--user-id", "@alice:example.com", - ) - }) -} diff --git a/pkg/cmd/matrixuseraccountdata.go b/pkg/cmd/matrixuseraccountdata.go deleted file mode 100644 index dd0347b9..00000000 --- a/pkg/cmd/matrixuseraccountdata.go +++ /dev/null @@ -1,165 +0,0 @@ -// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -package cmd - -import ( - "context" - "fmt" - - "github.com/beeper/desktop-api-cli/internal/apiquery" - "github.com/beeper/desktop-api-cli/internal/requestflag" - "github.com/beeper/desktop-api-go/v5" - "github.com/beeper/desktop-api-go/v5/option" - "github.com/tidwall/gjson" - "github.com/urfave/cli/v3" -) - -var matrixUsersAccountDataRetrieve = cli.Command{ - Name: "retrieve", - Usage: "Get some account data for the client. This config is only visible to the user\nthat set the account data.", - Suggest: true, - Flags: []cli.Flag{ - &requestflag.Flag[string]{ - Name: "user-id", - Required: true, - PathParam: "userId", - }, - &requestflag.Flag[string]{ - Name: "type", - Required: true, - PathParam: "type", - }, - }, - Action: handleMatrixUsersAccountDataRetrieve, - HideHelpCommand: true, -} - -var matrixUsersAccountDataUpdate = cli.Command{ - Name: "update", - Usage: "Set some account data for the client. This config is only visible to the user\nthat set the account data. The config will be available to clients through the\ntop-level `account_data` field in the homeserver response to\n[/sync](https://spec.matrix.org/v1.18/client-server-api/#get_matrixclientv3sync).", - Suggest: true, - Flags: []cli.Flag{ - &requestflag.Flag[string]{ - Name: "user-id", - Required: true, - PathParam: "userId", - }, - &requestflag.Flag[string]{ - Name: "type", - Required: true, - PathParam: "type", - }, - &requestflag.Flag[any]{ - Name: "body", - Required: true, - BodyRoot: true, - }, - }, - Action: handleMatrixUsersAccountDataUpdate, - HideHelpCommand: true, -} - -func handleMatrixUsersAccountDataRetrieve(ctx context.Context, cmd *cli.Command) error { - client := beeperdesktopapi.NewClient(getDefaultRequestOptions(cmd)...) - unusedArgs := cmd.Args().Slice() - if !cmd.IsSet("type") && len(unusedArgs) > 0 { - cmd.Set("type", unusedArgs[0]) - unusedArgs = unusedArgs[1:] - } - if len(unusedArgs) > 0 { - return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) - } - - options, err := flagOptions( - cmd, - apiquery.NestedQueryFormatBrackets, - apiquery.ArrayQueryFormatRepeat, - EmptyBody, - false, - ) - if err != nil { - return err - } - - params := beeperdesktopapi.MatrixUserAccountDataGetParams{ - UserID: cmd.Value("user-id").(string), - } - - var res []byte - options = append(options, option.WithResponseBodyInto(&res)) - _, err = client.Matrix.Users.AccountData.Get( - ctx, - cmd.Value("type").(string), - params, - options..., - ) - if err != nil { - return err - } - - obj := gjson.ParseBytes(res) - format := "json" - explicitFormat := cmd.Root().IsSet("format") - if explicitFormat { - format = cmd.Root().String("format") - } - transform := cmd.Root().String("transform") - return ShowJSON(obj, ShowJSONOpts{ - ExplicitFormat: explicitFormat, - Format: format, - RawOutput: cmd.Root().Bool("raw-output"), - Title: "matrix:users:account-data retrieve", - Transform: transform, - }) -} - -func handleMatrixUsersAccountDataUpdate(ctx context.Context, cmd *cli.Command) error { - client := beeperdesktopapi.NewClient(getDefaultRequestOptions(cmd)...) - unusedArgs := cmd.Args().Slice() - if !cmd.IsSet("type") && len(unusedArgs) > 0 { - cmd.Set("type", unusedArgs[0]) - unusedArgs = unusedArgs[1:] - } - if len(unusedArgs) > 0 { - return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) - } - - options, err := flagOptions( - cmd, - apiquery.NestedQueryFormatBrackets, - apiquery.ArrayQueryFormatRepeat, - ApplicationJSON, - false, - ) - if err != nil { - return err - } - - params := beeperdesktopapi.MatrixUserAccountDataUpdateParams{ - UserID: cmd.Value("user-id").(string), - } - - var res []byte - options = append(options, option.WithResponseBodyInto(&res)) - _, err = client.Matrix.Users.AccountData.Update( - ctx, - cmd.Value("type").(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: "matrix:users:account-data update", - Transform: transform, - }) -} diff --git a/pkg/cmd/matrixuseraccountdata_test.go b/pkg/cmd/matrixuseraccountdata_test.go deleted file mode 100644 index af509366..00000000 --- a/pkg/cmd/matrixuseraccountdata_test.go +++ /dev/null @@ -1,46 +0,0 @@ -// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -package cmd - -import ( - "testing" - - "github.com/beeper/desktop-api-cli/internal/mocktest" -) - -func TestMatrixUsersAccountDataRetrieve(t *testing.T) { - t.Run("regular flags", func(t *testing.T) { - mocktest.TestRunMockTestWithFlags( - t, - "--access-token", "string", - "matrix:users:account-data", "retrieve", - "--user-id", "@alice:example.com", - "--type", "org.example.custom.config", - ) - }) -} - -func TestMatrixUsersAccountDataUpdate(t *testing.T) { - t.Run("regular flags", func(t *testing.T) { - mocktest.TestRunMockTestWithFlags( - t, - "--access-token", "string", - "matrix:users:account-data", "update", - "--user-id", "@alice:example.com", - "--type", "org.example.custom.config", - "--body", "{custom_account_data_key: custom_config_value}", - ) - }) - - t.Run("piping data", func(t *testing.T) { - // Test piping YAML data over stdin - pipeData := []byte("custom_account_data_key: custom_config_value") - mocktest.TestRunMockTestWithPipeAndFlags( - t, pipeData, - "--access-token", "string", - "matrix:users:account-data", "update", - "--user-id", "@alice:example.com", - "--type", "org.example.custom.config", - ) - }) -} diff --git a/pkg/cmd/message.go b/pkg/cmd/message.go index 3b9b74e2..d885dbad 100644 --- a/pkg/cmd/message.go +++ b/pkg/cmd/message.go @@ -16,12 +16,12 @@ import ( var messagesRetrieve = cli.Command{ Name: "retrieve", - Usage: "Retrieve a message by final message ID, pendingMessageID, or Matrix event ID.\nChat ID may be a Beeper chat ID or local chat ID.", + Usage: "Retrieve a message by final message ID, pendingMessageID, or Matrix event ID.\nchatID may be a Beeper chat ID or a local chat ID.", Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ Name: "chat-id", - Usage: "Chat ID. Input routes also accept the local chat ID from this Beeper Desktop installation when available.", + Usage: "Chat ID. Input routes also accept the local chat ID from this installation when available.", Required: true, PathParam: "chatID", }, @@ -43,7 +43,7 @@ var messagesUpdate = cli.Command{ Flags: []cli.Flag{ &requestflag.Flag[string]{ Name: "chat-id", - Usage: "Chat ID. Input routes also accept the local chat ID from this Beeper Desktop installation when available.", + Usage: "Chat ID. Input routes also accept the local chat ID from this installation when available.", Required: true, PathParam: "chatID", }, @@ -71,7 +71,7 @@ var messagesList = cli.Command{ Flags: []cli.Flag{ &requestflag.Flag[string]{ Name: "chat-id", - Usage: "Chat ID. Input routes also accept the local chat ID from this Beeper Desktop installation when available.", + Usage: "Chat ID. Input routes also accept the local chat ID from this installation when available.", Required: true, PathParam: "chatID", }, @@ -101,7 +101,7 @@ var messagesDelete = cli.Command{ Flags: []cli.Flag{ &requestflag.Flag[string]{ Name: "chat-id", - Usage: "Chat ID. Input routes also accept the local chat ID from this Beeper Desktop installation when available.", + Usage: "Chat ID. Input routes also accept the local chat ID from this installation when available.", Required: true, PathParam: "chatID", }, @@ -187,7 +187,7 @@ var messagesSearch = cli.Command{ }, &requestflag.Flag[string]{ Name: "query", - Usage: `Literal word search (non-semantic). Finds messages containing these EXACT words in any order. Use single words users actually type, not concepts or phrases. Example: use "dinner" not "dinner plans", use "sick" not "health issues". If omitted, returns results filtered only by other parameters.`, + Usage: `Literal word search. Finds messages containing these words in any order. Use words the user actually typed, not inferred concepts. Example: use "dinner" rather than "dinner plans". If omitted, returns results filtered only by the other parameters.`, QueryPath: "query", }, &requestflag.Flag[string]{ @@ -211,7 +211,7 @@ var messagesSend = requestflag.WithInnerFlags(cli.Command{ Flags: []cli.Flag{ &requestflag.Flag[string]{ Name: "chat-id", - Usage: "Chat ID. Input routes also accept the local chat ID from this Beeper Desktop installation when available.", + Usage: "Chat ID. Input routes also accept the local chat ID from this installation when available.", Required: true, PathParam: "chatID", }, @@ -227,7 +227,7 @@ var messagesSend = requestflag.WithInnerFlags(cli.Command{ }, &requestflag.Flag[string]{ Name: "text", - Usage: "Draft text. Plain text and Markdown are converted to Matrix HTML with the same rules used by send and edit.", + Usage: "Draft text. Plain text and Markdown are converted to Beeper rich text with the same rules used by send and edit.", BodyPath: "text", }, }, @@ -307,11 +307,8 @@ func handleMessagesRetrieve(ctx context.Context, cmd *cli.Command) error { } obj := gjson.ParseBytes(res) - format := "json" + format := cmd.Root().String("format") explicitFormat := cmd.Root().IsSet("format") - if explicitFormat { - format = cmd.Root().String("format") - } transform := cmd.Root().String("transform") return ShowJSON(obj, ShowJSONOpts{ ExplicitFormat: explicitFormat, @@ -397,11 +394,8 @@ func handleMessagesList(ctx context.Context, cmd *cli.Command) error { params := beeperdesktopapi.MessageListParams{} - format := "json" + format := cmd.Root().String("format") explicitFormat := cmd.Root().IsSet("format") - if explicitFormat { - format = cmd.Root().String("format") - } transform := cmd.Root().String("transform") if format == "raw" { var res []byte @@ -499,11 +493,8 @@ func handleMessagesSearch(ctx context.Context, cmd *cli.Command) error { params := beeperdesktopapi.MessageSearchParams{} - format := "json" + format := cmd.Root().String("format") explicitFormat := cmd.Root().IsSet("format") - if explicitFormat { - format = cmd.Root().String("format") - } transform := cmd.Root().String("transform") if format == "raw" { var res []byte diff --git a/scripts/build b/scripts/build index d2675777..167f51a9 100755 --- a/scripts/build +++ b/scripts/build @@ -7,5 +7,5 @@ cd "$(dirname "$0")/.." # Mark the necessary Go modules as private to avoid Go's proxy export GOPRIVATE="${GOPRIVATE:+$GOPRIVATE,}github.com/beeper/desktop-api-go/v5,github.com/stainless-sdks/beeper-desktop-api-go/v5" -echo "==> Building beeper-desktop-cli" -go build ./cmd/beeper-desktop-cli +echo "==> Building beeper-desktop" +go build ./cmd/beeper-desktop diff --git a/scripts/run b/scripts/run index cf548690..05318050 100755 --- a/scripts/run +++ b/scripts/run @@ -7,4 +7,4 @@ cd "$(dirname "$0")/.." # Mark the necessary Go modules as private to avoid Go's proxy export GOPRIVATE="${GOPRIVATE:+$GOPRIVATE,}github.com/beeper/desktop-api-go/v5,github.com/stainless-sdks/beeper-desktop-api-go/v5" -go run ./cmd/beeper-desktop-cli "$@" +go run ./cmd/beeper-desktop "$@" diff --git a/scripts/utils/upload-artifact.sh b/scripts/utils/upload-artifact.sh index 98486966..029ca297 100755 --- a/scripts/utils/upload-artifact.sh +++ b/scripts/utils/upload-artifact.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash set -exuo pipefail -BINARY_NAME="beeper-desktop-cli" +BINARY_NAME="beeper-desktop" DIST_DIR="dist" FILENAME="dist.zip" From ec207b7100abb78add5ce28c554cece058a1dc2d Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sun, 17 May 2026 16:36:06 +0000 Subject: [PATCH 53/54] feat(api): add app session/login/verifications endpoints --- .stats.yml | 6 +- pkg/cmd/app.go | 62 ++++ pkg/cmd/app_test.go | 19 ++ pkg/cmd/applogin.go | 248 ++++++++++++++ pkg/cmd/applogin_test.go | 95 ++++++ pkg/cmd/apploginverificationrecoverykey.go | 72 ++++ .../apploginverificationrecoverykey_test.go | 30 ++ .../apploginverificationrecoverykeyreset.go | 128 +++++++ ...ploginverificationrecoverykeyreset_test.go | 51 +++ pkg/cmd/appverification.go | 316 ++++++++++++++++++ pkg/cmd/appverification_test.go | 91 +++++ pkg/cmd/appverificationqr.go | 130 +++++++ pkg/cmd/appverificationqr_test.go | 41 +++ pkg/cmd/appverificationsas.go | 131 ++++++++ pkg/cmd/appverificationsas_test.go | 31 ++ pkg/cmd/cmd.go | 66 ++++ 16 files changed, 1514 insertions(+), 3 deletions(-) create mode 100644 pkg/cmd/app.go create mode 100644 pkg/cmd/app_test.go create mode 100644 pkg/cmd/applogin.go create mode 100644 pkg/cmd/applogin_test.go create mode 100644 pkg/cmd/apploginverificationrecoverykey.go create mode 100644 pkg/cmd/apploginverificationrecoverykey_test.go create mode 100644 pkg/cmd/apploginverificationrecoverykeyreset.go create mode 100644 pkg/cmd/apploginverificationrecoverykeyreset_test.go create mode 100644 pkg/cmd/appverification.go create mode 100644 pkg/cmd/appverification_test.go create mode 100644 pkg/cmd/appverificationqr.go create mode 100644 pkg/cmd/appverificationqr_test.go create mode 100644 pkg/cmd/appverificationsas.go create mode 100644 pkg/cmd/appverificationsas_test.go diff --git a/.stats.yml b/.stats.yml index f8d62378..3e06ccc4 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 39 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper/beeper-desktop-api-87df69f641d994f09669f77093988df0b13da380d36076964d4a2563e9ce202e.yml +configured_endpoints: 56 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper/beeper-desktop-api-baac187842e51587134950c59c4d746bfcb59239f01919ed83b92c24c47d98f4.yml openapi_spec_hash: 9de80d05f7562b7ecd07c466f0fdf58b -config_hash: 2ebcc80e2cbd2342e132f4474ec24212 +config_hash: a8a4a8b869ccd5976fd4107e67d2ecae diff --git a/pkg/cmd/app.go b/pkg/cmd/app.go new file mode 100644 index 00000000..0adfa2d9 --- /dev/null +++ b/pkg/cmd/app.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/beeper/desktop-api-cli/internal/apiquery" + "github.com/beeper/desktop-api-go/v5" + "github.com/beeper/desktop-api-go/v5/option" + "github.com/tidwall/gjson" + "github.com/urfave/cli/v3" +) + +var appSession = cli.Command{ + Name: "session", + Usage: "Return the current Beeper Desktop or Beeper Server sign-in and encrypted\nmessaging setup state. This endpoint is public before sign-in so apps can\ndiscover that sign-in is needed; after sign-in, pass a read token.", + Suggest: true, + Flags: []cli.Flag{}, + Action: handleAppSession, + HideHelpCommand: true, +} + +func handleAppSession(ctx context.Context, cmd *cli.Command) error { + client := beeperdesktopapi.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.ArrayQueryFormatRepeat, + EmptyBody, + false, + ) + if err != nil { + return err + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.App.Session(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: "app session", + Transform: transform, + }) +} diff --git a/pkg/cmd/app_test.go b/pkg/cmd/app_test.go new file mode 100644 index 00000000..c74cc92f --- /dev/null +++ b/pkg/cmd/app_test.go @@ -0,0 +1,19 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "testing" + + "github.com/beeper/desktop-api-cli/internal/mocktest" +) + +func TestAppSession(t *testing.T) { + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--access-token", "string", + "app", "session", + ) + }) +} diff --git a/pkg/cmd/applogin.go b/pkg/cmd/applogin.go new file mode 100644 index 00000000..a1a0dd58 --- /dev/null +++ b/pkg/cmd/applogin.go @@ -0,0 +1,248 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "context" + "fmt" + + "github.com/beeper/desktop-api-cli/internal/apiquery" + "github.com/beeper/desktop-api-cli/internal/requestflag" + "github.com/beeper/desktop-api-go/v5" + "github.com/beeper/desktop-api-go/v5/option" + "github.com/tidwall/gjson" + "github.com/urfave/cli/v3" +) + +var appLoginEmail = cli.Command{ + Name: "email", + Usage: "Send a sign-in code to the user email address for app setup.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "email", + Usage: "Email address to send the sign-in code to.", + Required: true, + BodyPath: "email", + }, + &requestflag.Flag[string]{ + Name: "setup-request-id", + Usage: "Setup request ID returned by the start step.", + Required: true, + BodyPath: "setupRequestID", + }, + }, + Action: handleAppLoginEmail, + HideHelpCommand: true, +} + +var appLoginRegister = cli.Command{ + Name: "register", + Usage: "Create a Beeper account after the user chooses a username and accepts the Terms\nof Use.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[bool]{ + Name: "accept-terms", + Usage: "Confirms that the user agreed to our [terms of use](https://www.beeper.com/terms-onboarding) and has read our [privacy policy](https://www.beeper.com/privacy).", + Default: true, + Const: true, + BodyPath: "acceptTerms", + }, + &requestflag.Flag[string]{ + Name: "lead-token", + Usage: "Registration token returned by Beeper.", + Required: true, + BodyPath: "leadToken", + }, + &requestflag.Flag[string]{ + Name: "setup-request-id", + Usage: "Setup request ID returned by the start step.", + Required: true, + BodyPath: "setupRequestID", + }, + &requestflag.Flag[string]{ + Name: "username", + Usage: "Username selected by the user.", + Required: true, + BodyPath: "username", + }, + }, + Action: handleAppLoginRegister, + HideHelpCommand: true, +} + +var appLoginResponse = cli.Command{ + Name: "response", + Usage: "Finish setup sign-in with the code sent to the user email address. If the user\nneeds a new account, the response includes account creation copy and username\nsuggestions.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "response", + Usage: "Sign-in code from the user email.", + Required: true, + BodyPath: "response", + }, + &requestflag.Flag[string]{ + Name: "setup-request-id", + Usage: "Setup request ID returned by the start step.", + Required: true, + BodyPath: "setupRequestID", + }, + }, + Action: handleAppLoginResponse, + HideHelpCommand: true, +} + +var appLoginStart = cli.Command{ + Name: "start", + Usage: "Start setting up Beeper Desktop or Beeper Server. The flow supports existing\nBeeper accounts and new account creation.", + Suggest: true, + Flags: []cli.Flag{}, + Action: handleAppLoginStart, + HideHelpCommand: true, +} + +func handleAppLoginEmail(ctx context.Context, cmd *cli.Command) error { + client := beeperdesktopapi.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.ArrayQueryFormatRepeat, + ApplicationJSON, + false, + ) + if err != nil { + return err + } + + params := beeperdesktopapi.AppLoginEmailParams{} + + return client.App.Login.Email(ctx, params, options...) +} + +func handleAppLoginRegister(ctx context.Context, cmd *cli.Command) error { + client := beeperdesktopapi.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.ArrayQueryFormatRepeat, + ApplicationJSON, + false, + ) + if err != nil { + return err + } + + params := beeperdesktopapi.AppLoginRegisterParams{} + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.App.Login.Register(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: "app:login register", + Transform: transform, + }) +} + +func handleAppLoginResponse(ctx context.Context, cmd *cli.Command) error { + client := beeperdesktopapi.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.ArrayQueryFormatRepeat, + ApplicationJSON, + false, + ) + if err != nil { + return err + } + + params := beeperdesktopapi.AppLoginResponseParams{} + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.App.Login.Response(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: "app:login response", + Transform: transform, + }) +} + +func handleAppLoginStart(ctx context.Context, cmd *cli.Command) error { + client := beeperdesktopapi.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.ArrayQueryFormatRepeat, + EmptyBody, + false, + ) + if err != nil { + return err + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.App.Login.Start(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: "app:login start", + Transform: transform, + }) +} diff --git a/pkg/cmd/applogin_test.go b/pkg/cmd/applogin_test.go new file mode 100644 index 00000000..979cae5e --- /dev/null +++ b/pkg/cmd/applogin_test.go @@ -0,0 +1,95 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "testing" + + "github.com/beeper/desktop-api-cli/internal/mocktest" +) + +func TestAppLoginEmail(t *testing.T) { + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--access-token", "string", + "app:login", "email", + "--email", "dev@stainless.com", + "--setup-request-id", "setupRequestID", + ) + }) + + t.Run("piping data", func(t *testing.T) { + // Test piping YAML data over stdin + pipeData := []byte("" + + "email: dev@stainless.com\n" + + "setupRequestID: setupRequestID\n") + mocktest.TestRunMockTestWithPipeAndFlags( + t, pipeData, + "--access-token", "string", + "app:login", "email", + ) + }) +} + +func TestAppLoginRegister(t *testing.T) { + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--access-token", "string", + "app:login", "register", + "--accept-terms=true", + "--lead-token", "leadToken", + "--setup-request-id", "setupRequestID", + "--username", "x", + ) + }) + + t.Run("piping data", func(t *testing.T) { + // Test piping YAML data over stdin + pipeData := []byte("" + + "acceptTerms: true\n" + + "leadToken: leadToken\n" + + "setupRequestID: setupRequestID\n" + + "username: x\n") + mocktest.TestRunMockTestWithPipeAndFlags( + t, pipeData, + "--access-token", "string", + "app:login", "register", + ) + }) +} + +func TestAppLoginResponse(t *testing.T) { + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--access-token", "string", + "app:login", "response", + "--response", "response", + "--setup-request-id", "setupRequestID", + ) + }) + + t.Run("piping data", func(t *testing.T) { + // Test piping YAML data over stdin + pipeData := []byte("" + + "response: response\n" + + "setupRequestID: setupRequestID\n") + mocktest.TestRunMockTestWithPipeAndFlags( + t, pipeData, + "--access-token", "string", + "app:login", "response", + ) + }) +} + +func TestAppLoginStart(t *testing.T) { + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--access-token", "string", + "app:login", "start", + ) + }) +} diff --git a/pkg/cmd/apploginverificationrecoverykey.go b/pkg/cmd/apploginverificationrecoverykey.go new file mode 100644 index 00000000..4cc82a71 --- /dev/null +++ b/pkg/cmd/apploginverificationrecoverykey.go @@ -0,0 +1,72 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "context" + "fmt" + + "github.com/beeper/desktop-api-cli/internal/apiquery" + "github.com/beeper/desktop-api-cli/internal/requestflag" + "github.com/beeper/desktop-api-go/v5" + "github.com/beeper/desktop-api-go/v5/option" + "github.com/tidwall/gjson" + "github.com/urfave/cli/v3" +) + +var appLoginVerificationRecoveryKeyVerify = cli.Command{ + Name: "verify", + Usage: "Unlock encrypted messages with the user recovery key.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "recovery-key", + Usage: "Recovery key saved by the user.", + Required: true, + BodyPath: "recoveryKey", + }, + }, + Action: handleAppLoginVerificationRecoveryKeyVerify, + HideHelpCommand: true, +} + +func handleAppLoginVerificationRecoveryKeyVerify(ctx context.Context, cmd *cli.Command) error { + client := beeperdesktopapi.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.ArrayQueryFormatRepeat, + ApplicationJSON, + false, + ) + if err != nil { + return err + } + + params := beeperdesktopapi.AppLoginVerificationRecoveryKeyVerifyParams{} + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.App.Login.Verification.RecoveryKey.Verify(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: "app:login:verification:recovery-key verify", + Transform: transform, + }) +} diff --git a/pkg/cmd/apploginverificationrecoverykey_test.go b/pkg/cmd/apploginverificationrecoverykey_test.go new file mode 100644 index 00000000..aea56fe8 --- /dev/null +++ b/pkg/cmd/apploginverificationrecoverykey_test.go @@ -0,0 +1,30 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "testing" + + "github.com/beeper/desktop-api-cli/internal/mocktest" +) + +func TestAppLoginVerificationRecoveryKeyVerify(t *testing.T) { + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--access-token", "string", + "app:login:verification:recovery-key", "verify", + "--recovery-key", "x", + ) + }) + + t.Run("piping data", func(t *testing.T) { + // Test piping YAML data over stdin + pipeData := []byte("recoveryKey: x") + mocktest.TestRunMockTestWithPipeAndFlags( + t, pipeData, + "--access-token", "string", + "app:login:verification:recovery-key", "verify", + ) + }) +} diff --git a/pkg/cmd/apploginverificationrecoverykeyreset.go b/pkg/cmd/apploginverificationrecoverykeyreset.go new file mode 100644 index 00000000..a595d93c --- /dev/null +++ b/pkg/cmd/apploginverificationrecoverykeyreset.go @@ -0,0 +1,128 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "context" + "fmt" + + "github.com/beeper/desktop-api-cli/internal/apiquery" + "github.com/beeper/desktop-api-cli/internal/requestflag" + "github.com/beeper/desktop-api-go/v5" + "github.com/beeper/desktop-api-go/v5/option" + "github.com/tidwall/gjson" + "github.com/urfave/cli/v3" +) + +var appLoginVerificationRecoveryKeyResetCreate = cli.Command{ + Name: "create", + Usage: "Create a new recovery key when the user cannot use the existing one.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "existing-recovery-key", + Usage: "Existing recovery key, if the user has it.", + BodyPath: "existingRecoveryKey", + }, + }, + Action: handleAppLoginVerificationRecoveryKeyResetCreate, + HideHelpCommand: true, +} + +var appLoginVerificationRecoveryKeyResetConfirm = cli.Command{ + Name: "confirm", + Usage: "Confirm that the new recovery key should be used for this account.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "recovery-key", + Usage: "New recovery key returned by the reset step.", + Required: true, + BodyPath: "recoveryKey", + }, + }, + Action: handleAppLoginVerificationRecoveryKeyResetConfirm, + HideHelpCommand: true, +} + +func handleAppLoginVerificationRecoveryKeyResetCreate(ctx context.Context, cmd *cli.Command) error { + client := beeperdesktopapi.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.ArrayQueryFormatRepeat, + ApplicationJSON, + false, + ) + if err != nil { + return err + } + + params := beeperdesktopapi.AppLoginVerificationRecoveryKeyResetNewParams{} + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.App.Login.Verification.RecoveryKey.Reset.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: "app:login:verification:recovery-key:reset create", + Transform: transform, + }) +} + +func handleAppLoginVerificationRecoveryKeyResetConfirm(ctx context.Context, cmd *cli.Command) error { + client := beeperdesktopapi.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.ArrayQueryFormatRepeat, + ApplicationJSON, + false, + ) + if err != nil { + return err + } + + params := beeperdesktopapi.AppLoginVerificationRecoveryKeyResetConfirmParams{} + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.App.Login.Verification.RecoveryKey.Reset.Confirm(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: "app:login:verification:recovery-key:reset confirm", + Transform: transform, + }) +} diff --git a/pkg/cmd/apploginverificationrecoverykeyreset_test.go b/pkg/cmd/apploginverificationrecoverykeyreset_test.go new file mode 100644 index 00000000..4ff51d85 --- /dev/null +++ b/pkg/cmd/apploginverificationrecoverykeyreset_test.go @@ -0,0 +1,51 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "testing" + + "github.com/beeper/desktop-api-cli/internal/mocktest" +) + +func TestAppLoginVerificationRecoveryKeyResetCreate(t *testing.T) { + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--access-token", "string", + "app:login:verification:recovery-key:reset", "create", + "--existing-recovery-key", "existingRecoveryKey", + ) + }) + + t.Run("piping data", func(t *testing.T) { + // Test piping YAML data over stdin + pipeData := []byte("existingRecoveryKey: existingRecoveryKey") + mocktest.TestRunMockTestWithPipeAndFlags( + t, pipeData, + "--access-token", "string", + "app:login:verification:recovery-key:reset", "create", + ) + }) +} + +func TestAppLoginVerificationRecoveryKeyResetConfirm(t *testing.T) { + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--access-token", "string", + "app:login:verification:recovery-key:reset", "confirm", + "--recovery-key", "x", + ) + }) + + t.Run("piping data", func(t *testing.T) { + // Test piping YAML data over stdin + pipeData := []byte("recoveryKey: x") + mocktest.TestRunMockTestWithPipeAndFlags( + t, pipeData, + "--access-token", "string", + "app:login:verification:recovery-key:reset", "confirm", + ) + }) +} diff --git a/pkg/cmd/appverification.go b/pkg/cmd/appverification.go new file mode 100644 index 00000000..d50fc9b9 --- /dev/null +++ b/pkg/cmd/appverification.go @@ -0,0 +1,316 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "context" + "fmt" + + "github.com/beeper/desktop-api-cli/internal/apiquery" + "github.com/beeper/desktop-api-cli/internal/requestflag" + "github.com/beeper/desktop-api-go/v5" + "github.com/beeper/desktop-api-go/v5/option" + "github.com/tidwall/gjson" + "github.com/urfave/cli/v3" +) + +var appVerificationsCreate = cli.Command{ + Name: "create", + Usage: "Start verifying this device from another signed-in device.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "purpose", + Usage: "Why this verification is being started.", + Default: "device", + BodyPath: "purpose", + }, + &requestflag.Flag[string]{ + Name: "user-id", + Usage: "Beeper user ID to verify. Defaults to the signed-in user.", + BodyPath: "userID", + }, + }, + Action: handleAppVerificationsCreate, + HideHelpCommand: true, +} + +var appVerificationsRetrieve = cli.Command{ + Name: "retrieve", + Usage: "Get the current state of a device verification transaction.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "verification-id", + Usage: "Verification ID.", + Required: true, + PathParam: "verificationID", + }, + }, + Action: handleAppVerificationsRetrieve, + HideHelpCommand: true, +} + +var appVerificationsList = cli.Command{ + Name: "list", + Usage: "List pending and active device verifications. Use this to recover state without\na WebSocket connection.", + Suggest: true, + Flags: []cli.Flag{}, + Action: handleAppVerificationsList, + HideHelpCommand: true, +} + +var appVerificationsAccept = cli.Command{ + Name: "accept", + Usage: "Accept an incoming device verification request.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "verification-id", + Usage: "Verification ID.", + Required: true, + PathParam: "verificationID", + }, + }, + Action: handleAppVerificationsAccept, + HideHelpCommand: true, +} + +var appVerificationsCancel = cli.Command{ + Name: "cancel", + Usage: "Cancel an active device verification request.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "verification-id", + Usage: "Verification ID.", + Required: true, + PathParam: "verificationID", + }, + &requestflag.Flag[string]{ + Name: "code", + Usage: "Optional cancellation code.", + BodyPath: "code", + }, + &requestflag.Flag[string]{ + Name: "reason", + Usage: "Optional user-facing cancellation reason.", + BodyPath: "reason", + }, + }, + Action: handleAppVerificationsCancel, + HideHelpCommand: true, +} + +func handleAppVerificationsCreate(ctx context.Context, cmd *cli.Command) error { + client := beeperdesktopapi.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.ArrayQueryFormatRepeat, + ApplicationJSON, + false, + ) + if err != nil { + return err + } + + params := beeperdesktopapi.AppVerificationNewParams{} + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.App.Verifications.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: "app:verifications create", + Transform: transform, + }) +} + +func handleAppVerificationsRetrieve(ctx context.Context, cmd *cli.Command) error { + client := beeperdesktopapi.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("verification-id") && len(unusedArgs) > 0 { + cmd.Set("verification-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.ArrayQueryFormatRepeat, + EmptyBody, + false, + ) + if err != nil { + return err + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.App.Verifications.Get(ctx, cmd.Value("verification-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: "app:verifications retrieve", + Transform: transform, + }) +} + +func handleAppVerificationsList(ctx context.Context, cmd *cli.Command) error { + client := beeperdesktopapi.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.ArrayQueryFormatRepeat, + EmptyBody, + false, + ) + if err != nil { + return err + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.App.Verifications.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: "app:verifications list", + Transform: transform, + }) +} + +func handleAppVerificationsAccept(ctx context.Context, cmd *cli.Command) error { + client := beeperdesktopapi.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("verification-id") && len(unusedArgs) > 0 { + cmd.Set("verification-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.ArrayQueryFormatRepeat, + EmptyBody, + false, + ) + if err != nil { + return err + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.App.Verifications.Accept(ctx, cmd.Value("verification-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: "app:verifications accept", + Transform: transform, + }) +} + +func handleAppVerificationsCancel(ctx context.Context, cmd *cli.Command) error { + client := beeperdesktopapi.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("verification-id") && len(unusedArgs) > 0 { + cmd.Set("verification-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.ArrayQueryFormatRepeat, + ApplicationJSON, + false, + ) + if err != nil { + return err + } + + params := beeperdesktopapi.AppVerificationCancelParams{} + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.App.Verifications.Cancel( + ctx, + cmd.Value("verification-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: "app:verifications cancel", + Transform: transform, + }) +} diff --git a/pkg/cmd/appverification_test.go b/pkg/cmd/appverification_test.go new file mode 100644 index 00000000..fa2acd4d --- /dev/null +++ b/pkg/cmd/appverification_test.go @@ -0,0 +1,91 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "testing" + + "github.com/beeper/desktop-api-cli/internal/mocktest" +) + +func TestAppVerificationsCreate(t *testing.T) { + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--access-token", "string", + "app:verifications", "create", + "--purpose", "login", + "--user-id", "userID", + ) + }) + + t.Run("piping data", func(t *testing.T) { + // Test piping YAML data over stdin + pipeData := []byte("" + + "purpose: login\n" + + "userID: userID\n") + mocktest.TestRunMockTestWithPipeAndFlags( + t, pipeData, + "--access-token", "string", + "app:verifications", "create", + ) + }) +} + +func TestAppVerificationsRetrieve(t *testing.T) { + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--access-token", "string", + "app:verifications", "retrieve", + "--verification-id", "x", + ) + }) +} + +func TestAppVerificationsList(t *testing.T) { + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--access-token", "string", + "app:verifications", "list", + ) + }) +} + +func TestAppVerificationsAccept(t *testing.T) { + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--access-token", "string", + "app:verifications", "accept", + "--verification-id", "x", + ) + }) +} + +func TestAppVerificationsCancel(t *testing.T) { + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--access-token", "string", + "app:verifications", "cancel", + "--verification-id", "x", + "--code", "code", + "--reason", "reason", + ) + }) + + t.Run("piping data", func(t *testing.T) { + // Test piping YAML data over stdin + pipeData := []byte("" + + "code: code\n" + + "reason: reason\n") + mocktest.TestRunMockTestWithPipeAndFlags( + t, pipeData, + "--access-token", "string", + "app:verifications", "cancel", + "--verification-id", "x", + ) + }) +} diff --git a/pkg/cmd/appverificationqr.go b/pkg/cmd/appverificationqr.go new file mode 100644 index 00000000..d17e71f3 --- /dev/null +++ b/pkg/cmd/appverificationqr.go @@ -0,0 +1,130 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "context" + "fmt" + + "github.com/beeper/desktop-api-cli/internal/apiquery" + "github.com/beeper/desktop-api-cli/internal/requestflag" + "github.com/beeper/desktop-api-go/v5" + "github.com/beeper/desktop-api-go/v5/option" + "github.com/tidwall/gjson" + "github.com/urfave/cli/v3" +) + +var appVerificationsQrConfirmScanned = cli.Command{ + Name: "confirm-scanned", + Usage: "Confirm that another device scanned this device QR code.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "verification-id", + Usage: "Verification ID.", + Required: true, + PathParam: "verificationID", + }, + }, + Action: handleAppVerificationsQrConfirmScanned, + HideHelpCommand: true, +} + +var appVerificationsQrScan = cli.Command{ + Name: "scan", + Usage: "Submit the QR code scanned from another signed-in device.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "data", + Usage: "QR code payload scanned from the other device.", + Required: true, + BodyPath: "data", + }, + }, + Action: handleAppVerificationsQrScan, + HideHelpCommand: true, +} + +func handleAppVerificationsQrConfirmScanned(ctx context.Context, cmd *cli.Command) error { + client := beeperdesktopapi.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("verification-id") && len(unusedArgs) > 0 { + cmd.Set("verification-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.ArrayQueryFormatRepeat, + EmptyBody, + false, + ) + if err != nil { + return err + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.App.Verifications.Qr.ConfirmScanned(ctx, cmd.Value("verification-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: "app:verifications:qr confirm-scanned", + Transform: transform, + }) +} + +func handleAppVerificationsQrScan(ctx context.Context, cmd *cli.Command) error { + client := beeperdesktopapi.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.ArrayQueryFormatRepeat, + ApplicationJSON, + false, + ) + if err != nil { + return err + } + + params := beeperdesktopapi.AppVerificationQrScanParams{} + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.App.Verifications.Qr.Scan(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: "app:verifications:qr scan", + Transform: transform, + }) +} diff --git a/pkg/cmd/appverificationqr_test.go b/pkg/cmd/appverificationqr_test.go new file mode 100644 index 00000000..080d352e --- /dev/null +++ b/pkg/cmd/appverificationqr_test.go @@ -0,0 +1,41 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "testing" + + "github.com/beeper/desktop-api-cli/internal/mocktest" +) + +func TestAppVerificationsQrConfirmScanned(t *testing.T) { + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--access-token", "string", + "app:verifications:qr", "confirm-scanned", + "--verification-id", "x", + ) + }) +} + +func TestAppVerificationsQrScan(t *testing.T) { + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--access-token", "string", + "app:verifications:qr", "scan", + "--data", "x", + ) + }) + + t.Run("piping data", func(t *testing.T) { + // Test piping YAML data over stdin + pipeData := []byte("data: x") + mocktest.TestRunMockTestWithPipeAndFlags( + t, pipeData, + "--access-token", "string", + "app:verifications:qr", "scan", + ) + }) +} diff --git a/pkg/cmd/appverificationsas.go b/pkg/cmd/appverificationsas.go new file mode 100644 index 00000000..ec27a08e --- /dev/null +++ b/pkg/cmd/appverificationsas.go @@ -0,0 +1,131 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "context" + "fmt" + + "github.com/beeper/desktop-api-cli/internal/apiquery" + "github.com/beeper/desktop-api-cli/internal/requestflag" + "github.com/beeper/desktop-api-go/v5" + "github.com/beeper/desktop-api-go/v5/option" + "github.com/tidwall/gjson" + "github.com/urfave/cli/v3" +) + +var appVerificationsSASConfirm = cli.Command{ + Name: "confirm", + Usage: "Confirm that the emoji or number sequence matches on both devices.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "verification-id", + Usage: "Verification ID.", + Required: true, + PathParam: "verificationID", + }, + }, + Action: handleAppVerificationsSASConfirm, + HideHelpCommand: true, +} + +var appVerificationsSASStart = cli.Command{ + Name: "start", + Usage: "Start emoji comparison for device verification.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "verification-id", + Usage: "Verification ID.", + Required: true, + PathParam: "verificationID", + }, + }, + Action: handleAppVerificationsSASStart, + HideHelpCommand: true, +} + +func handleAppVerificationsSASConfirm(ctx context.Context, cmd *cli.Command) error { + client := beeperdesktopapi.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("verification-id") && len(unusedArgs) > 0 { + cmd.Set("verification-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.ArrayQueryFormatRepeat, + EmptyBody, + false, + ) + if err != nil { + return err + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.App.Verifications.SAS.Confirm(ctx, cmd.Value("verification-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: "app:verifications:sas confirm", + Transform: transform, + }) +} + +func handleAppVerificationsSASStart(ctx context.Context, cmd *cli.Command) error { + client := beeperdesktopapi.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("verification-id") && len(unusedArgs) > 0 { + cmd.Set("verification-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.ArrayQueryFormatRepeat, + EmptyBody, + false, + ) + if err != nil { + return err + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.App.Verifications.SAS.Start(ctx, cmd.Value("verification-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: "app:verifications:sas start", + Transform: transform, + }) +} diff --git a/pkg/cmd/appverificationsas_test.go b/pkg/cmd/appverificationsas_test.go new file mode 100644 index 00000000..526fada5 --- /dev/null +++ b/pkg/cmd/appverificationsas_test.go @@ -0,0 +1,31 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "testing" + + "github.com/beeper/desktop-api-cli/internal/mocktest" +) + +func TestAppVerificationsSASConfirm(t *testing.T) { + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--access-token", "string", + "app:verifications:sas", "confirm", + "--verification-id", "x", + ) + }) +} + +func TestAppVerificationsSASStart(t *testing.T) { + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--access-token", "string", + "app:verifications:sas", "start", + "--verification-id", "x", + ) + }) +} diff --git a/pkg/cmd/cmd.go b/pkg/cmd/cmd.go index c693bc13..fbad129d 100644 --- a/pkg/cmd/cmd.go +++ b/pkg/cmd/cmd.go @@ -208,6 +208,72 @@ func init() { &infoRetrieve, }, }, + { + Name: "app", + Category: "API RESOURCE", + Suggest: true, + Commands: []*cli.Command{ + &appSession, + }, + }, + { + Name: "app:login", + Category: "API RESOURCE", + Suggest: true, + Commands: []*cli.Command{ + &appLoginEmail, + &appLoginRegister, + &appLoginResponse, + &appLoginStart, + }, + }, + { + Name: "app:login:verification:recovery-key", + Category: "API RESOURCE", + Suggest: true, + Commands: []*cli.Command{ + &appLoginVerificationRecoveryKeyVerify, + }, + }, + { + Name: "app:login:verification:recovery-key:reset", + Category: "API RESOURCE", + Suggest: true, + Commands: []*cli.Command{ + &appLoginVerificationRecoveryKeyResetCreate, + &appLoginVerificationRecoveryKeyResetConfirm, + }, + }, + { + Name: "app:verifications", + Category: "API RESOURCE", + Suggest: true, + Commands: []*cli.Command{ + &appVerificationsCreate, + &appVerificationsRetrieve, + &appVerificationsList, + &appVerificationsAccept, + &appVerificationsCancel, + }, + }, + { + Name: "app:verifications:qr", + Category: "API RESOURCE", + Suggest: true, + Commands: []*cli.Command{ + &appVerificationsQrConfirmScanned, + &appVerificationsQrScan, + }, + }, + { + Name: "app:verifications:sas", + Category: "API RESOURCE", + Suggest: true, + Commands: []*cli.Command{ + &appVerificationsSASConfirm, + &appVerificationsSASStart, + }, + }, { Name: "@manpages", Usage: "Generate documentation for 'man'", From bab64e95d68ef211a00b4a4a505d0496e51abf8f Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sun, 17 May 2026 16:36:43 +0000 Subject: [PATCH 54/54] release: 5.0.0 --- .release-please-manifest.json | 2 +- CHANGELOG.md | 57 +++++++++++++++++++++++++++++++++++ pkg/cmd/version.go | 2 +- 3 files changed, 59 insertions(+), 2 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 6b7b74c5..8e76abb5 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.3.0" + ".": "5.0.0" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index a8fa2acd..e140e5d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,62 @@ # Changelog +## 5.0.0 (2026-05-17) + +Full Changelog: [v0.3.0...v5.0.0](https://github.com/beeper/desktop-api-cli/compare/v0.3.0...v5.0.0) + +### Features + +* allow `-` as value representing stdin to binary-only file parameters in CLIs ([eacfea0](https://github.com/beeper/desktop-api-cli/commit/eacfea0f881332d854999d73092a4786d0b3ae33)) +* **api:** add accounts/bridges endpoints, login-flows/sessions, remove app/matrix APIs ([dcd1348](https://github.com/beeper/desktop-api-cli/commit/dcd13484ebd39b36889b7ab284b78c2f4c619fd4)) +* **api:** add app session/login/verifications endpoints ([ec207b7](https://github.com/beeper/desktop-api-cli/commit/ec207b7100abb78add5ce28c554cece058a1dc2d)) +* **api:** add network, bridge fields to accounts ([ccd116d](https://github.com/beeper/desktop-api-cli/commit/ccd116df91e25275b2f63f53bedf9939aae8a64a)) +* **api:** api update ([3c79717](https://github.com/beeper/desktop-api-cli/commit/3c797176b7ced4499d6bf3ff71f7d3a94967e026)) +* **api:** api update ([3920764](https://github.com/beeper/desktop-api-cli/commit/3920764851961421776d64cf694dee1f999afa3e)) +* **api:** api update ([bf4b43c](https://github.com/beeper/desktop-api-cli/commit/bf4b43c832c3b56adda72018ffd91a7dc0660966)) +* better error message if scheme forgotten in CLI `*_BASE_URL`/`--base-url` ([8d976d4](https://github.com/beeper/desktop-api-cli/commit/8d976d441c86ddfaa67c8fad50a156f9374e0d16)) +* binary-only parameters become CLI flags that take filenames only ([ed64986](https://github.com/beeper/desktop-api-cli/commit/ed64986c234a83e6c875a1202a7c217df9efa206)) +* **cli:** add `--raw-output`/`-r` option to print raw (non-JSON) strings ([963cb22](https://github.com/beeper/desktop-api-cli/commit/963cb2275ff0b6900c09dbbd0cc45c37e745d561)) +* **cli:** alias parameters in data with `x-stainless-cli-data-alias` ([301f6f2](https://github.com/beeper/desktop-api-cli/commit/301f6f2894cae408e91bb5834d50d6c16b78c28f)) +* **cli:** send filename and content type when reading input from files ([b1ab49b](https://github.com/beeper/desktop-api-cli/commit/b1ab49bb30c6d4132fb9827cff19f73d83e7af5c)) +* set CLI flag constant values automatically where `x-stainless-const` is set ([c332ed8](https://github.com/beeper/desktop-api-cli/commit/c332ed822bc6e94cb521620032bdcf941d4b7593)) +* support passing path and query params over stdin ([5fe5a8d](https://github.com/beeper/desktop-api-cli/commit/5fe5a8d8aa02222e08a8407728d8063d08d9fdb9)) + + +### Bug Fixes + +* **cli:** correctly load zsh autocompletion ([2b1da99](https://github.com/beeper/desktop-api-cli/commit/2b1da99d8aa0d518a3a1e90126467b4eabff4a03)) +* fall back to main branch if linking fails in CI ([6960f67](https://github.com/beeper/desktop-api-cli/commit/6960f674e30ae4b66c8c727accf4b140df4aa257)) +* fix for failing to drop invalid module replace in link script ([5480c0e](https://github.com/beeper/desktop-api-cli/commit/5480c0e637e57e45ef1780847a2ec1638ef8550d)) +* fix for off-by-one error in pagination logic ([16566c5](https://github.com/beeper/desktop-api-cli/commit/16566c5d80a77de14f33e1895ec72cfa89da99c7)) +* fix quoting typo ([81a96d9](https://github.com/beeper/desktop-api-cli/commit/81a96d9880aee422f73677cdbe67134af37f6814)) +* flags for nullable body scalar fields are strictly typed ([865b851](https://github.com/beeper/desktop-api-cli/commit/865b85180334a84053ebbc19c718f17c29c9d9d7)) +* handle empty data set using `--format explore` ([6df0024](https://github.com/beeper/desktop-api-cli/commit/6df00248f562bbbd98e7cbccf8ddd82b35a40ae3)) +* use `RawJSON` when iterating items with `--format explore` in the CLI ([4af189d](https://github.com/beeper/desktop-api-cli/commit/4af189dc0e5add5c654204e0e90c91e199de479a)) + + +### Chores + +* add documentation for ./scripts/link ([b57a694](https://github.com/beeper/desktop-api-cli/commit/b57a69441818f20bf3d1bbf949a30b65c55aa728)) +* **ci:** skip lint on metadata-only changes ([d18d972](https://github.com/beeper/desktop-api-cli/commit/d18d972e88c1a03890fcbe3ba2f0dbffb06f5e48)) +* **ci:** support manually triggering release workflow ([5a018e8](https://github.com/beeper/desktop-api-cli/commit/5a018e83f5f9347b262b10176639fcc24ec5f370)) +* **cli:** additional test cases for `ShowJSONIterator` ([79f9f75](https://github.com/beeper/desktop-api-cli/commit/79f9f751f5fd415d9a34893e47b8409aa82dcb7c)) +* **cli:** fall back to JSON when using default "explore" with non-TTY ([de64b1c](https://github.com/beeper/desktop-api-cli/commit/de64b1c7b2620be74c563e4da9ae4ff6a486555e)) +* **cli:** let `--format raw` be used in conjunction with `--transform` ([c37cb3e](https://github.com/beeper/desktop-api-cli/commit/c37cb3e7e49185c7e7e398fb70a254ea8daf2456)) +* **cli:** switch long lists of positional args over to param structs ([d4e5630](https://github.com/beeper/desktop-api-cli/commit/d4e5630718dc3170dac5f9efe0acfd0be2f1a390)) +* **cli:** use `ShowJSONOpts` as argument to `formatJSON` instead of many positionals ([384a7f4](https://github.com/beeper/desktop-api-cli/commit/384a7f434bd6d40b626b6e635f005853f66ff917)) +* **internal:** codegen related update ([ec112ec](https://github.com/beeper/desktop-api-cli/commit/ec112ec91ee336d835177ab87406e72ad585966e)) +* **internal:** more robust bootstrap script ([888af53](https://github.com/beeper/desktop-api-cli/commit/888af53697b79c93159f9fce5e60cca4ea1e5a04)) +* **internal:** update multipart form array serialization ([180bf35](https://github.com/beeper/desktop-api-cli/commit/180bf353b36de3cfee9e9497dca4558622654e18)) +* mark all CLI-related tests in Go with `t.Parallel()` ([59678e1](https://github.com/beeper/desktop-api-cli/commit/59678e17b692460f3fdc03ca82ba6270d13ed460)) +* modify CLI tests to inject stdout so mutating `os.Stdout` isn't necessary ([9e6fe8f](https://github.com/beeper/desktop-api-cli/commit/9e6fe8f0d1cba9d5115fba0b82f24a5938b3e7bc)) +* omit full usage information when missing required CLI parameters ([8b749cf](https://github.com/beeper/desktop-api-cli/commit/8b749cf09b595e66687d4030cee0791aff7e14c4)) +* redact api-key headers in debug logs ([3b786e2](https://github.com/beeper/desktop-api-cli/commit/3b786e24f0f0b206fc0817fa1fc80fa3b084346a)) +* switch some CLI Go tests from `os.Chdir` to `t.Chdir` ([c0de07e](https://github.com/beeper/desktop-api-cli/commit/c0de07ef35b6d776781b0347a102c3855445f0cb)) +* **tests:** bump steady to v0.19.7 ([c7c519c](https://github.com/beeper/desktop-api-cli/commit/c7c519cf80adffc7cebbd3ec220227baf48672d7)) +* **tests:** bump steady to v0.20.1 ([533501d](https://github.com/beeper/desktop-api-cli/commit/533501d2960fc565d5891d5f610a7feef458d0b2)) +* **tests:** bump steady to v0.20.2 ([3a7b085](https://github.com/beeper/desktop-api-cli/commit/3a7b085b2130d21ee0a2b434dbaa3efada1e1bb3)) +* **tests:** bump steady to v0.22.1 ([2164645](https://github.com/beeper/desktop-api-cli/commit/216464542c88aca1c22ebf0e9af3ac0eb6854a38)) + ## 0.3.0 (2026-03-24) Full Changelog: [v0.2.0...v0.3.0](https://github.com/beeper/desktop-api-cli/compare/v0.2.0...v0.3.0) diff --git a/pkg/cmd/version.go b/pkg/cmd/version.go index 5266c84b..26ffb4fb 100644 --- a/pkg/cmd/version.go +++ b/pkg/cmd/version.go @@ -2,4 +2,4 @@ package cmd -const Version = "0.3.0" // x-release-please-version +const Version = "5.0.0" // x-release-please-version