Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions SURFACE.txt
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ CMD fizzy help
CMD fizzy identity
CMD fizzy identity help
CMD fizzy identity show
CMD fizzy identity timezone-update
CMD fizzy identity view
CMD fizzy migrate
CMD fizzy migrate board
Expand Down Expand Up @@ -1979,6 +1980,21 @@ FLAG fizzy identity show --quiet type=bool
FLAG fizzy identity show --styled type=bool
FLAG fizzy identity show --token type=string
FLAG fizzy identity show --verbose type=bool
FLAG fizzy identity timezone-update --agent type=bool
FLAG fizzy identity timezone-update --api-url type=string
FLAG fizzy identity timezone-update --count type=bool
FLAG fizzy identity timezone-update --help type=bool
FLAG fizzy identity timezone-update --ids-only type=bool
FLAG fizzy identity timezone-update --jq type=string
FLAG fizzy identity timezone-update --json type=bool
FLAG fizzy identity timezone-update --limit type=int
FLAG fizzy identity timezone-update --markdown type=bool
FLAG fizzy identity timezone-update --profile type=string
FLAG fizzy identity timezone-update --quiet type=bool
FLAG fizzy identity timezone-update --styled type=bool
FLAG fizzy identity timezone-update --timezone type=string
FLAG fizzy identity timezone-update --token type=string
FLAG fizzy identity timezone-update --verbose type=bool
FLAG fizzy identity view --agent type=bool
FLAG fizzy identity view --api-url type=string
FLAG fizzy identity view --count type=bool
Expand Down Expand Up @@ -3435,6 +3451,7 @@ SUB fizzy help
SUB fizzy identity
SUB fizzy identity help
SUB fizzy identity show
SUB fizzy identity timezone-update
SUB fizzy identity view
SUB fizzy migrate
SUB fizzy migrate board
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ go 1.26

require (
github.com/basecamp/cli v0.2.1
github.com/basecamp/fizzy-sdk/go v0.2.1
github.com/basecamp/fizzy-sdk/go v0.2.2
github.com/charmbracelet/huh v1.0.0
github.com/charmbracelet/lipgloss v1.1.0
github.com/charmbracelet/x/term v0.2.2
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ github.com/aymanbagabas/go-udiff v0.3.1 h1:LV+qyBQ2pqe0u42ZsUEtPiCaUoqgA9gYRDs3v
github.com/aymanbagabas/go-udiff v0.3.1/go.mod h1:G0fsKmG+P6ylD0r6N/KgQD/nWzgfnl8ZBcNLgcbrw8E=
github.com/basecamp/cli v0.2.1 h1:8GyehPVtsTXla0oOPu4QgXRjwwzJ99prlByvyi+0HRQ=
github.com/basecamp/cli v0.2.1/go.mod h1:p8tt/DatJ2LAzWO6N6tNfV8x3gF5T3IxDTo+U8FfWPo=
github.com/basecamp/fizzy-sdk/go v0.2.1 h1:4xRRBQWP0V5rMkppAramn9bSDILl+zY0RD7ax8IDjKQ=
github.com/basecamp/fizzy-sdk/go v0.2.1/go.mod h1:XvOTc+2/6NaECvb2mVhIMq2pNsl9P2wNqwvybIUtQ2g=
github.com/basecamp/fizzy-sdk/go v0.2.2 h1:o2unUWdeJlUlm4L/0Y+/VhLiAjdR9tMNulSyTwFlg+c=
github.com/basecamp/fizzy-sdk/go v0.2.2/go.mod h1:XvOTc+2/6NaECvb2mVhIMq2pNsl9P2wNqwvybIUtQ2g=
github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY=
github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc=
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 h1:JFgG/xnwFfbezlUnFMJy0nusZvytYysV4SCS2cYbvws=
Expand Down
9 changes: 8 additions & 1 deletion internal/commands/board_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -385,7 +385,7 @@ func TestBoardUpdate(t *testing.T) {
},
}

SetTestModeWithSDK(mock)
result := SetTestModeWithSDK(mock)
SetTestConfig("token", "account", "https://api.example.com")
defer resetTest()

Expand All @@ -404,6 +404,13 @@ func TestBoardUpdate(t *testing.T) {
if mock.PatchCalls[0].Path != "/boards/123" {
t.Errorf("expected path '/boards/123', got '%s'", mock.PatchCalls[0].Path)
}
data := responseDataMap(t, result)
if got := data["name"]; got != "Updated Name" {
t.Errorf("expected update response body name, got %#v", got)
}
if got := data["id"]; got != "123" {
t.Errorf("expected update response body id, got %#v", got)
}
})

t.Run("handles API error", func(t *testing.T) {
Expand Down
5 changes: 4 additions & 1 deletion internal/commands/card_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1274,7 +1274,7 @@ func TestCardMove(t *testing.T) {
},
}

SetTestModeWithSDK(mock)
result := SetTestModeWithSDK(mock)
SetTestConfig("token", "account", "https://api.example.com")
defer resetTest()

Expand All @@ -1294,6 +1294,9 @@ func TestCardMove(t *testing.T) {
if body["board_id"] != "board-456" {
t.Errorf("expected board_id 'board-456', got '%v'", body["board_id"])
}
if got := responseDataMap(t, result)["title"]; got != "Test Card" {
t.Errorf("expected move response body title, got %#v", got)
}
})

t.Run("requires --to flag", func(t *testing.T) {
Expand Down
9 changes: 8 additions & 1 deletion internal/commands/column_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,7 @@ func TestColumnUpdate(t *testing.T) {
},
}

SetTestModeWithSDK(mock)
result := SetTestModeWithSDK(mock)
SetTestConfig("token", "account", "https://api.example.com")
defer resetTest()

Expand All @@ -282,6 +282,13 @@ func TestColumnUpdate(t *testing.T) {
if mock.PatchCalls[0].Path != "/boards/123/columns/col-1" {
t.Errorf("expected path '/boards/123/columns/col-1', got '%s'", mock.PatchCalls[0].Path)
}
data := responseDataMap(t, result)
if got := data["name"]; got != "Updated Column" {
t.Errorf("expected update response body name, got %#v", got)
}
if got := data["id"]; got != "col-1" {
t.Errorf("expected update response body id, got %#v", got)
}
})

t.Run("requires board flag", func(t *testing.T) {
Expand Down
9 changes: 8 additions & 1 deletion internal/commands/comment_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -323,7 +323,7 @@ func TestCommentUpdate(t *testing.T) {
},
}

SetTestModeWithSDK(mock)
result := SetTestModeWithSDK(mock)
SetTestConfig("token", "account", "https://api.example.com")
defer resetTest()

Expand All @@ -341,6 +341,13 @@ func TestCommentUpdate(t *testing.T) {
if mock.PatchCalls[0].Path != "/cards/42/comments/comment-1" {
t.Errorf("expected path '/cards/42/comments/comment-1', got '%s'", mock.PatchCalls[0].Path)
}
body, ok := responseDataMap(t, result)["body"].(map[string]any)
if !ok {
t.Fatalf("expected update response body map, got %#v", responseDataMap(t, result)["body"])
}
if got := body["plain_text"]; got != "Updated comment" {
t.Errorf("expected update response body plain_text, got %#v", got)
}
})

t.Run("uploads and appends inline attachments", func(t *testing.T) {
Expand Down
41 changes: 41 additions & 0 deletions internal/commands/identity.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package commands

import (
"github.com/basecamp/fizzy-sdk/go/pkg/generated"
"github.com/spf13/cobra"
)

Expand Down Expand Up @@ -37,7 +38,47 @@ var identityShowCmd = &cobra.Command{
},
}

var identityTimezoneUpdateTimezone string

var identityTimezoneUpdateCmd = &cobra.Command{
Use: "timezone-update",
Short: "Update your timezone",
Long: "Updates your timezone for the current account.",
RunE: func(cmd *cobra.Command, args []string) error {
if err := requireAuthAndAccount(); err != nil {
return err
}

if identityTimezoneUpdateTimezone == "" {
return newRequiredFlagError("timezone")
}

resp, err := getSDKClient().Identity().UpdateMyTimezone(cmd.Context(), cfg.Account, &generated.UpdateMyTimezoneRequest{
TimezoneName: identityTimezoneUpdateTimezone,
})
if err != nil {
return convertSDKError(err)
}

data := any(map[string]any{"timezone_name": identityTimezoneUpdateTimezone})
if resp != nil && len(resp.Data) > 0 {
if normalized := normalizeAny(resp.Data); normalized != nil {
data = normalized
}
}

breadcrumbs := []Breadcrumb{
breadcrumb("show", "fizzy identity show", "View identity"),
}

printMutation(data, "Timezone updated", breadcrumbs)
return nil
},
}

func init() {
rootCmd.AddCommand(identityCmd)
identityCmd.AddCommand(identityShowCmd)
identityTimezoneUpdateCmd.Flags().StringVar(&identityTimezoneUpdateTimezone, "timezone", "", "Timezone name, for example America/New_York (required)")
identityCmd.AddCommand(identityTimezoneUpdateCmd)
}
92 changes: 92 additions & 0 deletions internal/commands/identity_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,98 @@ import (
"github.com/basecamp/fizzy-cli/internal/errors"
)

func TestIdentityTimezoneUpdate(t *testing.T) {
t.Run("updates timezone", func(t *testing.T) {
mock := NewMockClient()
mock.PatchResponse = &client.APIResponse{
StatusCode: 200,
Data: map[string]any{
"timezone_name": "America/New_York",
"updated_at": "2026-06-03T21:15:00Z",
},
}

result := SetTestModeWithSDK(mock)
SetTestConfig("token", "account", "https://api.example.com")
identityTimezoneUpdateTimezone = "America/New_York"
defer func() {
identityTimezoneUpdateTimezone = ""
resetTest()
}()

err := identityTimezoneUpdateCmd.RunE(identityTimezoneUpdateCmd, []string{})
assertExitCode(t, err, 0)

if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(mock.PatchCalls) != 1 {
t.Fatalf("expected 1 patch call, got %d", len(mock.PatchCalls))
}
if mock.PatchCalls[0].Path != "/my/timezone.json" {
t.Errorf("expected path '/my/timezone.json', got %q", mock.PatchCalls[0].Path)
}
body, ok := mock.PatchCalls[0].Body.(map[string]any)
if !ok {
t.Fatalf("expected map body, got %#v", mock.PatchCalls[0].Body)
}
if body["timezone_name"] != "America/New_York" {
t.Errorf("expected timezone_name body, got %#v", body)
}
if result.Response.Summary != "Timezone updated" {
t.Errorf("expected timezone summary, got %q", result.Response.Summary)
}
if got := responseDataMap(t, result)["updated_at"]; got != "2026-06-03T21:15:00Z" {
t.Errorf("expected timezone response body updated_at, got %#v", got)
}
})

t.Run("falls back to requested timezone for empty response", func(t *testing.T) {
mock := NewMockClient()
mock.PatchResponse = &client.APIResponse{StatusCode: 204, Data: nil}

result := SetTestModeWithSDK(mock)
SetTestConfig("token", "account", "https://api.example.com")
identityTimezoneUpdateTimezone = "America/New_York"
defer func() {
identityTimezoneUpdateTimezone = ""
resetTest()
}()

err := identityTimezoneUpdateCmd.RunE(identityTimezoneUpdateCmd, []string{})
assertExitCode(t, err, 0)

if got := responseDataMap(t, result)["timezone_name"]; got != "America/New_York" {
t.Errorf("expected fallback timezone_name, got %#v", got)
}
})

t.Run("requires timezone", func(t *testing.T) {
mock := NewMockClient()
SetTestModeWithSDK(mock)
SetTestConfig("token", "account", "https://api.example.com")
identityTimezoneUpdateTimezone = ""
defer resetTest()

err := identityTimezoneUpdateCmd.RunE(identityTimezoneUpdateCmd, []string{})
assertExitCode(t, err, errors.ExitInvalidArgs)
})

t.Run("requires account", func(t *testing.T) {
mock := NewMockClient()
SetTestModeWithSDK(mock)
SetTestConfig("token", "", "https://api.example.com")
identityTimezoneUpdateTimezone = "America/New_York"
defer func() {
identityTimezoneUpdateTimezone = ""
resetTest()
}()

err := identityTimezoneUpdateCmd.RunE(identityTimezoneUpdateCmd, []string{})
assertExitCode(t, err, errors.ExitInvalidArgs)
})
}

func TestIdentityShow(t *testing.T) {
t.Run("shows identity", func(t *testing.T) {
mock := NewMockClient()
Expand Down
15 changes: 15 additions & 0 deletions internal/commands/response_assertions_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package commands

import "testing"

func responseDataMap(t *testing.T, result *CommandResult) map[string]any {
t.Helper()
if result == nil || result.Response == nil {
t.Fatal("expected command response")
}
data, ok := result.Response.Data.(map[string]any)
if !ok {
t.Fatalf("expected response data map, got %#v", result.Response.Data)
}
return data
}
16 changes: 13 additions & 3 deletions internal/commands/user_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,11 +80,14 @@ func TestUserUpdate(t *testing.T) {
t.Run("updates user name", func(t *testing.T) {
mock := NewMockClient()
mock.PatchResponse = &client.APIResponse{
StatusCode: 204,
Data: map[string]any{},
StatusCode: 200,
Data: map[string]any{
"id": "user-1",
"name": "New Name",
},
}

SetTestModeWithSDK(mock)
result := SetTestModeWithSDK(mock)
SetTestConfig("token", "account", "https://api.example.com")
defer resetTest()

Expand All @@ -104,6 +107,13 @@ func TestUserUpdate(t *testing.T) {
if body["name"] != "New Name" {
t.Errorf("expected name 'New Name', got '%v'", body["name"])
}
data := responseDataMap(t, result)
if got := data["name"]; got != "New Name" {
t.Errorf("expected update response body name, got %#v", got)
}
if got := data["id"]; got != "user-1" {
t.Errorf("expected update response body id, got %#v", got)
}
})

t.Run("updates user avatar via multipart", func(t *testing.T) {
Expand Down
2 changes: 2 additions & 0 deletions skills/fizzy/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ Want to change something?
| Resource | List | Show | Create | Update | Delete | Other |
|----------|------|------|--------|--------|--------|-------|
| account | - | `account show` | - | `account settings-update` | - | `account entropy`, `account export-create`, `account export-show EXPORT_ID`, `account join-code-show`, `account join-code-reset`, `account join-code-update` |
| identity | - | `identity show` | - | `identity timezone-update --timezone NAME` | - | - |
| board | `board list` | `board show ID` | `board create` | `board update ID` | `board delete ID` | `board accesses --board ID`, `board publish ID`, `board unpublish ID`, `board entropy ID`, `board closed`, `board postponed`, `board stream`, `board involvement ID`, `migrate board ID` |
| card | `card list` | `card show NUMBER` | `card create` | `card update NUMBER` | `card delete NUMBER` | `card move NUMBER`, `card publish NUMBER`, `card mark-read NUMBER`, `card mark-unread NUMBER` |
| search | `search QUERY` | - | - | - | - | - |
Expand Down Expand Up @@ -455,6 +456,7 @@ fizzy comment list --card 579 --jq '.data | length'

```bash
fizzy identity show # Show your identity and accessible accounts
fizzy identity timezone-update --timezone America/New_York # Update your timezone for the current account
```

### Account
Expand Down