diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 5547f83..10f3091 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.1.1" + ".": "0.2.0" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 5de4fdd..d678254 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,26 @@ # Changelog +## 0.2.0 (2026-03-03) + +Full Changelog: [v0.1.1...v0.2.0](https://github.com/beeper/desktop-api-cli/compare/v0.1.1...v0.2.0) + +### Features + +* add support for file downloads from binary response endpoints ([2e0b0a7](https://github.com/beeper/desktop-api-cli/commit/2e0b0a7080adef772b1c2b9f33d957f267d354ad)) +* improved documentation and flags for client options ([46e772c](https://github.com/beeper/desktop-api-cli/commit/46e772c72af1ea406d17ddaa7aeaf58e8b917a16)) + + +### Bug Fixes + +* avoid printing usage errors twice ([62fbdae](https://github.com/beeper/desktop-api-cli/commit/62fbdaedc3cf1724fc9102eae7dcb87e148aabb7)) +* more gracefully handle empty stdin input ([d758018](https://github.com/beeper/desktop-api-cli/commit/d75801861b11dfd1cb5568e2b5da3eb0176b7c21)) + + +### Chores + +* **internal:** codegen related update ([2fcd536](https://github.com/beeper/desktop-api-cli/commit/2fcd53660f6195309b786d5b50aaeb22595a5404)) +* zip READMEs as part of build artifact ([d1a1267](https://github.com/beeper/desktop-api-cli/commit/d1a12679c7c0366a8a50972010874bc9e9a6d643)) + ## 0.1.1 (2026-02-25) Full Changelog: [v0.1.0...v0.1.1](https://github.com/beeper/desktop-api-cli/compare/v0.1.0...v0.1.1) diff --git a/README.md b/README.md index 018d3f6..24b5143 100644 --- a/README.md +++ b/README.md @@ -53,25 +53,22 @@ beeper-desktop-cli [resource] [flags...] ```sh beeper-desktop-cli chats search \ - --account-id local-whatsapp_ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc \ - --account-id local-telegram_ba_QFrb5lrLPhO3OT5MFBeTWv0x4BI \ - --cursor '1725489123456|c29tZUltc2dQYWdl' \ - --direction before \ - --inbox primary \ --include-muted \ - --last-activity-after 2019-12-27T18:11:19.117Z \ - --last-activity-before 2019-12-27T18:11:19.117Z \ --limit 3 \ - --query x \ - --scope titles \ - --type single \ - --unread-only + --type single ``` For details about specific commands, use the `--help` flag. -### Global Flags +### 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 | + +### 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) - `--help` - Show command line usage - `--debug` - Enable debug logging (includes HTTP request/response details) - `--version`, `-v` - Show the CLI version diff --git a/cmd/beeper-desktop-cli/main.go b/cmd/beeper-desktop-cli/main.go index 1a40218..e27225a 100644 --- a/cmd/beeper-desktop-cli/main.go +++ b/cmd/beeper-desktop-cli/main.go @@ -42,7 +42,11 @@ func main() { fmt.Fprintf(os.Stderr, "%s\n", err.Error()) } } else { - fmt.Fprintf(os.Stderr, "%s\n", err.Error()) + if cmd.CommandErrorBuffer.Len() > 0 { + os.Stderr.Write(cmd.CommandErrorBuffer.Bytes()) + } else { + fmt.Fprintf(os.Stderr, "%s\n", err.Error()) + } } os.Exit(exitCode) } diff --git a/internal/requestflag/requestflag.go b/internal/requestflag/requestflag.go index 519fdce..cb11412 100644 --- a/internal/requestflag/requestflag.go +++ b/internal/requestflag/requestflag.go @@ -20,16 +20,17 @@ type Flag[ []float64 | []int64 | []bool | any | map[string]any | DateTimeValue | DateValue | TimeValue | string | float64 | int64 | bool, ] struct { - Name string // name of the flag - Category string // category of the flag, if any - DefaultText string // default text of the flag for usage purposes - HideDefault bool // whether to hide the default value in output - Usage string // usage string for help output - Required bool // whether the flag is required or not - Hidden bool // whether to hide the flag in help output - Default T // default value for this flag if not set by from any source - Aliases []string // aliases that are allowed for this flag - Validator func(T) error // custom function to validate this flag value + Name string // name of the flag + Category string // category of the flag, if any + DefaultText string // default text of the flag for usage purposes + HideDefault bool // whether to hide the default value in output + Usage string // usage string for help output + Sources cli.ValueSourceChain // sources to load flag value from + Required bool // whether the flag is required or not + Hidden bool // whether to hide the flag in help output + Default T // default value for this flag if not set by from any source + Aliases []string // aliases that are allowed for this flag + Validator func(T) error // custom function to validate this flag value QueryPath string // location in the request query string to put this flag's value HeaderPath string // location in the request header to put this flag's value @@ -127,6 +128,22 @@ func (f *Flag[T]) PreParse() error { } func (f *Flag[T]) PostParse() error { + if !f.hasBeenSet { + if val, source, found := f.Sources.LookupWithSource(); found { + if val != "" || reflect.TypeOf(f.value).Kind() == reflect.String { + if err := f.Set(f.Name, val); err != nil { + return fmt.Errorf( + "could not parse %[1]q as %[2]T value from %[3]s for flag %[4]s: %[5]s", + val, f.value, source, f.Name, err, + ) + } + } else if val == "" && reflect.TypeOf(f.value).Kind() == reflect.Bool { + _ = f.Set(f.Name, "false") + } + + f.hasBeenSet = true + } + } return nil } @@ -230,8 +247,9 @@ func (f *Flag[T]) GetDefaultText() string { return f.DefaultText } +// GetEnvVars returns the env vars for this flag func (f *Flag[T]) GetEnvVars() []string { - return nil + return f.Sources.EnvKeys() } func (f *Flag[T]) IsDefaultVisible() bool { diff --git a/pkg/cmd/account_test.go b/pkg/cmd/account_test.go index cd633e7..2586344 100644 --- a/pkg/cmd/account_test.go +++ b/pkg/cmd/account_test.go @@ -12,5 +12,6 @@ func TestAccountsList(t *testing.T) { mocktest.TestRunMockTestWithFlags( t, "accounts", "list", + "--access-token", "string", ) } diff --git a/pkg/cmd/accountcontact_test.go b/pkg/cmd/accountcontact_test.go index a9aca95..22d52c0 100644 --- a/pkg/cmd/accountcontact_test.go +++ b/pkg/cmd/accountcontact_test.go @@ -12,6 +12,7 @@ func TestAccountsContactsList(t *testing.T) { mocktest.TestRunMockTestWithFlags( t, "accounts:contacts", "list", + "--access-token", "string", "--account-id", "accountID", "--cursor", "1725489123456|c29tZUltc2dQYWdl", "--direction", "before", @@ -24,6 +25,7 @@ func TestAccountsContactsSearch(t *testing.T) { mocktest.TestRunMockTestWithFlags( t, "accounts:contacts", "search", + "--access-token", "string", "--account-id", "accountID", "--query", "x", ) diff --git a/pkg/cmd/asset_test.go b/pkg/cmd/asset_test.go index 036f0ec..af833ad 100644 --- a/pkg/cmd/asset_test.go +++ b/pkg/cmd/asset_test.go @@ -12,6 +12,7 @@ func TestAssetsDownload(t *testing.T) { mocktest.TestRunMockTestWithFlags( t, "assets", "download", + "--access-token", "string", "--url", "mxc://example.org/Q4x9CqGz1pB3Oa6XgJ", ) } @@ -20,6 +21,7 @@ func TestAssetsServe(t *testing.T) { mocktest.TestRunMockTestWithFlags( t, "assets", "serve", + "--access-token", "string", "--url", "x", ) } @@ -28,7 +30,8 @@ func TestAssetsUpload(t *testing.T) { mocktest.TestRunMockTestWithFlags( t, "assets", "upload", - "--file", "", + "--access-token", "string", + "--file", "...", "--file-name", "fileName", "--mime-type", "mimeType", ) @@ -38,6 +41,7 @@ func TestAssetsUploadBase64(t *testing.T) { mocktest.TestRunMockTestWithFlags( t, "assets", "upload-base64", + "--access-token", "string", "--content", "x", "--file-name", "fileName", "--mime-type", "mimeType", diff --git a/pkg/cmd/beeperdesktopapi_test.go b/pkg/cmd/beeperdesktopapi_test.go index 1cfef7d..251d144 100644 --- a/pkg/cmd/beeperdesktopapi_test.go +++ b/pkg/cmd/beeperdesktopapi_test.go @@ -12,6 +12,7 @@ func TestFocus(t *testing.T) { mocktest.TestRunMockTestWithFlags( t, "focus", + "--access-token", "string", "--chat-id", "!NCdzlIaMjZUmvmvyHU:beeper.com", "--draft-attachment-path", "draftAttachmentPath", "--draft-text", "draftText", @@ -23,6 +24,7 @@ func TestSearch(t *testing.T) { mocktest.TestRunMockTestWithFlags( t, "search", + "--access-token", "string", "--query", "x", ) } diff --git a/pkg/cmd/chat_test.go b/pkg/cmd/chat_test.go index 8739818..8e901e3 100644 --- a/pkg/cmd/chat_test.go +++ b/pkg/cmd/chat_test.go @@ -13,6 +13,7 @@ func TestChatsCreate(t *testing.T) { mocktest.TestRunMockTestWithFlags( t, "chats", "create", + "--access-token", "string", "--account-id", "accountID", "--allow-invite=true", "--message-text", "messageText", @@ -49,6 +50,7 @@ func TestChatsRetrieve(t *testing.T) { mocktest.TestRunMockTestWithFlags( t, "chats", "retrieve", + "--access-token", "string", "--chat-id", "!NCdzlIaMjZUmvmvyHU:beeper.com", "--max-participant-count", "50", ) @@ -58,6 +60,7 @@ func TestChatsList(t *testing.T) { mocktest.TestRunMockTestWithFlags( t, "chats", "list", + "--access-token", "string", "--account-id", "local-whatsapp_ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc", "--account-id", "local-instagram_ba_eRfQMmnSNy_p7Ih7HL7RduRpKFU", "--cursor", "1725489123456|c29tZUltc2dQYWdl", @@ -69,6 +72,7 @@ func TestChatsArchive(t *testing.T) { mocktest.TestRunMockTestWithFlags( t, "chats", "archive", + "--access-token", "string", "--chat-id", "!NCdzlIaMjZUmvmvyHU:beeper.com", "--archived=true", ) @@ -78,14 +82,15 @@ func TestChatsSearch(t *testing.T) { mocktest.TestRunMockTestWithFlags( t, "chats", "search", + "--access-token", "string", "--account-id", "local-whatsapp_ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc", "--account-id", "local-telegram_ba_QFrb5lrLPhO3OT5MFBeTWv0x4BI", "--cursor", "1725489123456|c29tZUltc2dQYWdl", "--direction", "before", "--inbox", "primary", "--include-muted=true", - "--last-activity-after", "2019-12-27T18:11:19.117Z", - "--last-activity-before", "2019-12-27T18:11:19.117Z", + "--last-activity-after", "'2019-12-27T18:11:19.117Z'", + "--last-activity-before", "'2019-12-27T18:11:19.117Z'", "--limit", "1", "--query", "x", "--scope", "titles", diff --git a/pkg/cmd/chatmessagereaction_test.go b/pkg/cmd/chatmessagereaction_test.go index 3a42a97..78a6135 100644 --- a/pkg/cmd/chatmessagereaction_test.go +++ b/pkg/cmd/chatmessagereaction_test.go @@ -12,6 +12,7 @@ func TestChatsMessagesReactionsDelete(t *testing.T) { mocktest.TestRunMockTestWithFlags( t, "chats:messages:reactions", "delete", + "--access-token", "string", "--chat-id", "!NCdzlIaMjZUmvmvyHU:beeper.com", "--message-id", "messageID", "--reaction-key", "x", @@ -22,6 +23,7 @@ func TestChatsMessagesReactionsAdd(t *testing.T) { mocktest.TestRunMockTestWithFlags( t, "chats:messages:reactions", "add", + "--access-token", "string", "--chat-id", "!NCdzlIaMjZUmvmvyHU:beeper.com", "--message-id", "messageID", "--reaction-key", "x", diff --git a/pkg/cmd/chatreminder_test.go b/pkg/cmd/chatreminder_test.go index 88b17c0..9cd0393 100644 --- a/pkg/cmd/chatreminder_test.go +++ b/pkg/cmd/chatreminder_test.go @@ -13,6 +13,7 @@ func TestChatsRemindersCreate(t *testing.T) { mocktest.TestRunMockTestWithFlags( t, "chats:reminders", "create", + "--access-token", "string", "--chat-id", "!NCdzlIaMjZUmvmvyHU:beeper.com", "--reminder", "{remindAtMs: 0, dismissOnIncomingMessage: true}", ) @@ -34,6 +35,7 @@ func TestChatsRemindersDelete(t *testing.T) { mocktest.TestRunMockTestWithFlags( t, "chats:reminders", "delete", + "--access-token", "string", "--chat-id", "!NCdzlIaMjZUmvmvyHU:beeper.com", ) } diff --git a/pkg/cmd/cmd.go b/pkg/cmd/cmd.go index d0c40cf..3351e7f 100644 --- a/pkg/cmd/cmd.go +++ b/pkg/cmd/cmd.go @@ -3,6 +3,7 @@ package cmd import ( + "bytes" "compress/gzip" "context" "fmt" @@ -12,20 +13,23 @@ import ( "strings" "github.com/beeper/desktop-api-cli/internal/autocomplete" + "github.com/beeper/desktop-api-cli/internal/requestflag" docs "github.com/urfave/cli-docs/v3" "github.com/urfave/cli/v3" ) var ( - Command *cli.Command + Command *cli.Command + CommandErrorBuffer bytes.Buffer ) func init() { Command = &cli.Command{ - Name: "beeper-desktop-cli", - Usage: "CLI for the beeperdesktop API", - Suggest: true, - Version: Version, + Name: "beeper-desktop-cli", + Usage: "CLI for the beeperdesktop API", + Suggest: true, + Version: Version, + ErrWriter: &CommandErrorBuffer, Flags: []cli.Flag{ &cli.BoolFlag{ Name: "debug", @@ -66,6 +70,11 @@ func init() { Name: "transform-error", Usage: "The GJSON transformation for errors.", }, + &requestflag.Flag[string]{ + Name: "access-token", + Usage: "Bearer access token obtained via OAuth2 PKCE flow or created in-app. Required for all API operations.", + Sources: cli.EnvVars("BEEPER_ACCESS_TOKEN"), + }, }, Commands: []*cli.Command{ &focus, diff --git a/pkg/cmd/cmdutil.go b/pkg/cmd/cmdutil.go index 86c83c6..f6ace38 100644 --- a/pkg/cmd/cmdutil.go +++ b/pkg/cmd/cmdutil.go @@ -6,11 +6,13 @@ import ( "fmt" "io" "log" + "mime" "net/http" "net/http/httputil" "os" "os/exec" "os/signal" + "path/filepath" "strings" "syscall" @@ -34,6 +36,7 @@ func getDefaultRequestOptions(cmd *cli.Command) []option.RequestOption { option.WithHeader("X-Stainless-Package-Version", Version), option.WithHeader("X-Stainless-Runtime", "cli"), option.WithHeader("X-Stainless-CLI-Command", cmd.FullName()), + option.WithAccessToken(cmd.String("access-token")), } // Override base URL if the --base-url flag is provided @@ -153,6 +156,108 @@ func streamToStdout(generateOutput func(w *os.File) error) error { return err } +func writeBinaryResponse(response *http.Response, outfile string) (string, error) { + defer response.Body.Close() + body, err := io.ReadAll(response.Body) + if err != nil { + return "", err + } + switch outfile { + case "-", "/dev/stdout": + _, err := os.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) + return "", err + } + + // If response has a suggested filename in the content-disposition + // header, then use that (with an optional suffix to ensure uniqueness): + file, err := createDownloadFile(response, body) + if err != nil { + return "", err + } + defer file.Close() + if _, err := file.Write(body); err != nil { + return "", err + } + return fmt.Sprintf("Wrote output to: %s", file.Name()), nil + default: + if err := os.WriteFile(outfile, body, 0644); err != nil { + return "", err + } + return fmt.Sprintf("Wrote output to: %s", outfile), nil + } +} + +// Return a writable file handle to a new file, which attempts to choose a good filename +// based on the Content-Disposition header or sniffing the MIME filetype of the response. +func createDownloadFile(response *http.Response, data []byte) (*os.File, error) { + filename := "file" + // If the header provided an output filename, use that + disp := response.Header.Get("Content-Disposition") + _, params, err := mime.ParseMediaType(disp) + if err == nil { + if dispFilename, ok := params["filename"]; ok { + // Only use the last path component to prevent directory traversal + filename = filepath.Base(dispFilename) + // Try to create the file with exclusive flag to avoid race conditions + file, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0644) + if err == nil { + return file, nil + } + } + } + + // If file already exists, create a unique filename using CreateTemp + ext := filepath.Ext(filename) + if ext == "" { + ext = guessExtension(data) + } + base := strings.TrimSuffix(filename, ext) + return os.CreateTemp(".", base+"-*"+ext) +} + +func guessExtension(data []byte) string { + ct := http.DetectContentType(data) + + // Prefer common extensions over obscure ones + switch ct { + case "application/gzip": + return ".gz" + case "application/pdf": + return ".pdf" + case "application/zip": + return ".zip" + case "audio/mpeg": + return ".mp3" + case "image/bmp": + return ".bmp" + case "image/gif": + return ".gif" + case "image/jpeg": + return ".jpg" + case "image/png": + return ".png" + case "image/webp": + return ".webp" + case "video/mp4": + return ".mp4" + } + + exts, err := mime.ExtensionsByType(ct) + if err == nil && len(exts) > 0 { + return exts[0] + } else if isUTF8TextFile(data) { + return ".txt" + } else { + return ".bin" + } +} + func shouldUseColors(w io.Writer) bool { force, ok := os.LookupEnv("FORCE_COLOR") if ok { diff --git a/pkg/cmd/cmdutil_test.go b/pkg/cmd/cmdutil_test.go index 027f3d4..0a46fd1 100644 --- a/pkg/cmd/cmdutil_test.go +++ b/pkg/cmd/cmdutil_test.go @@ -1,8 +1,15 @@ package cmd import ( + "bytes" + "io" + "net/http" "os" + "path/filepath" "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestStreamOutput(t *testing.T) { @@ -15,3 +22,106 @@ func TestStreamOutput(t *testing.T) { t.Errorf("streamOutput failed: %v", err) } } + +func TestWriteBinaryResponse(t *testing.T) { + t.Run("write to explicit file", func(t *testing.T) { + tmpDir := t.TempDir() + outfile := tmpDir + "/output.txt" + body := []byte("test content") + resp := &http.Response{ + Body: io.NopCloser(bytes.NewReader(body)), + } + + msg, err := writeBinaryResponse(resp, outfile) + + require.NoError(t, err) + assert.Contains(t, msg, outfile) + + content, err := os.ReadFile(outfile) + require.NoError(t, err) + assert.Equal(t, body, content) + }) + + t.Run("write to stdout", func(t *testing.T) { + oldStdout := os.Stdout + r, w, _ := os.Pipe() + os.Stdout = w + + body := []byte("stdout content") + resp := &http.Response{ + Body: io.NopCloser(bytes.NewReader(body)), + } + msg, err := writeBinaryResponse(resp, "-") + + w.Close() + os.Stdout = oldStdout + + require.NoError(t, err) + assert.Empty(t, msg) + + var buf bytes.Buffer + _, _ = buf.ReadFrom(r) + assert.Equal(t, body, buf.Bytes()) + }) +} + +func TestCreateDownloadFile(t *testing.T) { + t.Run("creates file with filename from header", func(t *testing.T) { + tmpDir := t.TempDir() + oldWd, _ := os.Getwd() + os.Chdir(tmpDir) + defer os.Chdir(oldWd) + + resp := &http.Response{ + Header: http.Header{ + "Content-Disposition": []string{`attachment; filename="test.txt"`}, + }, + } + file, err := createDownloadFile(resp, []byte("test content")) + require.NoError(t, err) + defer file.Close() + assert.Equal(t, "test.txt", filepath.Base(file.Name())) + + // Create a second file with the same name to ensure it doesn't clobber the first + resp2 := &http.Response{ + Header: http.Header{ + "Content-Disposition": []string{`attachment; filename="test.txt"`}, + }, + } + file2, err := createDownloadFile(resp2, []byte("second content")) + require.NoError(t, err) + defer file2.Close() + assert.NotEqual(t, file.Name(), file2.Name(), "second file should have a different name") + assert.Contains(t, filepath.Base(file2.Name()), "test") + }) + + 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) + + resp := &http.Response{Header: http.Header{}} + file, err := createDownloadFile(resp, []byte("test content")) + require.NoError(t, err) + defer file.Close() + assert.Contains(t, filepath.Base(file.Name()), "file-") + }) + + t.Run("prevents directory traversal", func(t *testing.T) { + tmpDir := t.TempDir() + oldWd, _ := os.Getwd() + os.Chdir(tmpDir) + defer os.Chdir(oldWd) + + resp := &http.Response{ + Header: http.Header{ + "Content-Disposition": []string{`attachment; filename="../../../etc/passwd"`}, + }, + } + file, err := createDownloadFile(resp, []byte("test content")) + require.NoError(t, err) + defer file.Close() + assert.Equal(t, "passwd", filepath.Base(file.Name())) + }) +} diff --git a/pkg/cmd/flagoptions.go b/pkg/cmd/flagoptions.go index b314e9a..250c513 100644 --- a/pkg/cmd/flagoptions.go +++ b/pkg/cmd/flagoptions.go @@ -217,13 +217,16 @@ func flagOptions( flagContents := requestflag.ExtractRequestContents(cmd) var bodyData any + var pipeData []byte if isInputPiped() && !stdinInUse { var err error - pipeData, err := io.ReadAll(os.Stdin) + pipeData, err = io.ReadAll(os.Stdin) if err != nil { return nil, err } + } + if len(pipeData) > 0 { if err := yaml.Unmarshal(pipeData, &bodyData); err == nil { if bodyMap, ok := bodyData.(map[string]any); ok { if flagMap, ok := flagContents.Body.(map[string]any); ok { diff --git a/pkg/cmd/info_test.go b/pkg/cmd/info_test.go index cc916cd..d45592f 100644 --- a/pkg/cmd/info_test.go +++ b/pkg/cmd/info_test.go @@ -12,5 +12,6 @@ func TestInfoRetrieve(t *testing.T) { mocktest.TestRunMockTestWithFlags( t, "info", "retrieve", + "--access-token", "string", ) } diff --git a/pkg/cmd/message_test.go b/pkg/cmd/message_test.go index 08bb115..76d3439 100644 --- a/pkg/cmd/message_test.go +++ b/pkg/cmd/message_test.go @@ -13,6 +13,7 @@ func TestMessagesUpdate(t *testing.T) { mocktest.TestRunMockTestWithFlags( t, "messages", "update", + "--access-token", "string", "--chat-id", "!NCdzlIaMjZUmvmvyHU:beeper.com", "--message-id", "messageID", "--text", "x", @@ -23,6 +24,7 @@ func TestMessagesList(t *testing.T) { mocktest.TestRunMockTestWithFlags( t, "messages", "list", + "--access-token", "string", "--chat-id", "!NCdzlIaMjZUmvmvyHU:beeper.com", "--cursor", "1725489123456|c29tZUltc2dQYWdl", "--direction", "before", @@ -33,14 +35,15 @@ func TestMessagesSearch(t *testing.T) { mocktest.TestRunMockTestWithFlags( t, "messages", "search", + "--access-token", "string", "--account-id", "local-whatsapp_ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc", "--account-id", "local-instagram_ba_eRfQMmnSNy_p7Ih7HL7RduRpKFU", "--chat-id", "!NCdzlIaMjZUmvmvyHU:beeper.com", "--chat-id", "1231073", "--chat-type", "group", "--cursor", "1725489123456|c29tZUltc2dQYWdl", - "--date-after", "2025-08-01T00:00:00Z", - "--date-before", "2025-08-31T23:59:59Z", + "--date-after", "'2025-08-01T00:00:00Z'", + "--date-before", "'2025-08-31T23:59:59Z'", "--direction", "before", "--exclude-low-priority=true", "--include-muted=true", @@ -55,6 +58,7 @@ func TestMessagesSend(t *testing.T) { mocktest.TestRunMockTestWithFlags( t, "messages", "send", + "--access-token", "string", "--chat-id", "!NCdzlIaMjZUmvmvyHU:beeper.com", "--attachment", "{uploadID: uploadID, duration: 0, fileName: fileName, mimeType: mimeType, size: {height: 0, width: 0}, type: gif}", "--reply-to-message-id", "replyToMessageID", diff --git a/pkg/cmd/version.go b/pkg/cmd/version.go index e6caf60..10d2893 100644 --- a/pkg/cmd/version.go +++ b/pkg/cmd/version.go @@ -2,4 +2,4 @@ package cmd -const Version = "0.1.1" // x-release-please-version +const Version = "0.2.0" // x-release-please-version diff --git a/scripts/utils/upload-artifact.sh b/scripts/utils/upload-artifact.sh index d453066..9848696 100755 --- a/scripts/utils/upload-artifact.sh +++ b/scripts/utils/upload-artifact.sh @@ -5,10 +5,15 @@ BINARY_NAME="beeper-desktop-cli" DIST_DIR="dist" FILENAME="dist.zip" -mapfile -d '' files < <( - find "$DIST_DIR" -regextype posix-extended -type f \ - -regex ".*/[^/]*(amd64|arm64)[^/]*/${BINARY_NAME}(\\.exe)?$" -print0 -) +files=() +while IFS= read -r -d '' file; do + files+=("$file") +done < <(find "$DIST_DIR" -type f \( \ + -path "*amd64*/$BINARY_NAME" -o \ + -path "*arm64*/$BINARY_NAME" -o \ + -path "*amd64*/${BINARY_NAME}.exe" -o \ + -path "*arm64*/${BINARY_NAME}.exe" \ + \) -print0) if [[ ${#files[@]} -eq 0 ]]; then echo -e "\033[31mNo binaries found for packaging.\033[0m" @@ -20,7 +25,8 @@ rm -f "${DIST_DIR}/${FILENAME}" while IFS= read -r -d '' dir; do printf "Remove the quarantine attribute before running the executable:\n\nxattr -d com.apple.quarantine %s\n" \ "$BINARY_NAME" >"${dir}/README.txt" -done < <(find "$DIST_DIR" -type d -name '*macos*' -print0) + files+=("${dir}/README.txt") +done < <(find "$DIST_DIR" -type d -path '*macos*' -print0) relative_files=() for file in "${files[@]}"; do @@ -46,7 +52,7 @@ UPLOAD_RESPONSE=$(curl -v -X PUT \ if echo "$UPLOAD_RESPONSE" | grep -q "HTTP/[0-9.]* 200"; then echo -e "\033[32mUploaded build to Stainless storage.\033[0m" - echo -e "\033[32mInstallation: Download and unzip: 'https://pkg.stainless.com/s/beeper-desktop-api-cli/$SHA/$FILENAME'. On macOS, run `xattr -d com.apple.quarantine {executable name}.`\033[0m" + echo -e "\033[32mInstallation: Download and unzip: 'https://pkg.stainless.com/s/beeper-desktop-api-cli/$SHA'. On macOS, run 'xattr -d com.apple.quarantine {executable name}'.\033[0m" else echo -e "\033[31mFailed to upload artifact.\033[0m" exit 1