fizzy-cli 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/.env +1 -0
  2. package/.github/workflows/release.yml +29 -0
  3. package/.github/workflows/tests.yml +24 -0
  4. package/AGENTS.md +33 -0
  5. package/CHANGELOG.md +69 -0
  6. package/Makefile +20 -9
  7. package/README.md +88 -1
  8. package/bin/fizzy +0 -0
  9. package/cmd/account.go +14 -0
  10. package/cmd/account_list.go +44 -0
  11. package/cmd/account_list_test.go +118 -0
  12. package/cmd/board.go +38 -12
  13. package/cmd/board_create.go +60 -0
  14. package/cmd/board_create_test.go +158 -0
  15. package/cmd/board_list.go +18 -32
  16. package/cmd/board_list_test.go +115 -0
  17. package/cmd/board_test.go +92 -0
  18. package/cmd/card.go +24 -0
  19. package/cmd/card_close.go +46 -0
  20. package/cmd/card_close_test.go +92 -0
  21. package/cmd/card_create.go +73 -0
  22. package/cmd/card_create_test.go +206 -0
  23. package/cmd/card_delete.go +46 -0
  24. package/cmd/card_delete_test.go +92 -0
  25. package/cmd/card_list.go +53 -0
  26. package/cmd/card_list_test.go +148 -0
  27. package/cmd/card_reopen.go +46 -0
  28. package/cmd/card_reopen_test.go +92 -0
  29. package/cmd/card_show.go +46 -0
  30. package/cmd/card_show_test.go +92 -0
  31. package/cmd/card_update.go +74 -0
  32. package/cmd/card_update_test.go +147 -0
  33. package/cmd/column.go +14 -0
  34. package/cmd/column_create.go +80 -0
  35. package/cmd/column_create_test.go +196 -0
  36. package/cmd/column_list.go +44 -0
  37. package/cmd/column_list_test.go +138 -0
  38. package/cmd/login.go +61 -4
  39. package/cmd/login_test.go +98 -0
  40. package/cmd/root.go +15 -4
  41. package/cmd/use.go +85 -0
  42. package/cmd/use_test.go +186 -0
  43. package/docs/API.md +1168 -0
  44. package/go.mod +23 -2
  45. package/go.sum +43 -0
  46. package/internal/api/client.go +463 -0
  47. package/internal/app/app.go +49 -0
  48. package/internal/colors/colors.go +32 -0
  49. package/internal/config/config.go +69 -0
  50. package/internal/testutil/client.go +26 -0
  51. package/internal/ui/account_list.go +14 -0
  52. package/internal/ui/account_selector.go +63 -0
  53. package/internal/ui/board_list.go +14 -0
  54. package/internal/ui/card_list.go +14 -0
  55. package/internal/ui/card_show.go +23 -0
  56. package/internal/ui/column_list.go +28 -0
  57. package/internal/ui/format.go +14 -0
  58. package/main.go +1 -1
  59. package/package.json +1 -1
  60. package/scripts/postinstall.js +5 -1
package/go.mod CHANGED
@@ -1,10 +1,31 @@
1
- module github.com/rogeriopvl/fizzy-cli
1
+ module github.com/rogeriopvl/fizzy
2
2
 
3
3
  go 1.25.1
4
4
 
5
- require github.com/spf13/cobra v1.10.2
5
+ require (
6
+ github.com/charmbracelet/bubbletea v1.3.10
7
+ github.com/spf13/cobra v1.10.2
8
+ )
6
9
 
7
10
  require (
11
+ github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
12
+ github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
13
+ github.com/charmbracelet/lipgloss v1.1.0 // indirect
14
+ github.com/charmbracelet/x/ansi v0.10.1 // indirect
15
+ github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
16
+ github.com/charmbracelet/x/term v0.2.1 // indirect
17
+ github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
8
18
  github.com/inconshreveable/mousetrap v1.1.0 // indirect
19
+ github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
20
+ github.com/mattn/go-isatty v0.0.20 // indirect
21
+ github.com/mattn/go-localereader v0.0.1 // indirect
22
+ github.com/mattn/go-runewidth v0.0.16 // indirect
23
+ github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
24
+ github.com/muesli/cancelreader v0.2.2 // indirect
25
+ github.com/muesli/termenv v0.16.0 // indirect
26
+ github.com/rivo/uniseg v0.4.7 // indirect
9
27
  github.com/spf13/pflag v1.0.9 // indirect
28
+ github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
29
+ golang.org/x/sys v0.36.0 // indirect
30
+ golang.org/x/text v0.3.8 // indirect
10
31
  )
package/go.sum CHANGED
@@ -1,10 +1,53 @@
1
+ github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
2
+ github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
3
+ github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
4
+ github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
5
+ github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
6
+ github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
7
+ github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
8
+ github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
9
+ github.com/charmbracelet/x/ansi v0.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7D2jVDQ=
10
+ github.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
11
+ github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8=
12
+ github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
13
+ github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
14
+ github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
1
15
  github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
16
+ github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
17
+ github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
2
18
  github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
3
19
  github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
20
+ github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
21
+ github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
22
+ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
23
+ github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
24
+ github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
25
+ github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
26
+ github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
27
+ github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
28
+ github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
29
+ github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
30
+ github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
31
+ github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
32
+ github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
33
+ github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
34
+ github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
35
+ github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
36
+ github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
4
37
  github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
5
38
  github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
6
39
  github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
7
40
  github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
8
41
  github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
42
+ github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
43
+ github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
9
44
  go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
45
+ golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E=
46
+ golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
47
+ golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
48
+ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
49
+ golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
50
+ golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
51
+ golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY=
52
+ golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
10
53
  gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -0,0 +1,463 @@
1
+ // Package api
2
+ package api
3
+
4
+ import (
5
+ "bytes"
6
+ "context"
7
+ "encoding/json"
8
+ "fmt"
9
+ "io"
10
+ "net/http"
11
+ "os"
12
+ "time"
13
+
14
+ "github.com/rogeriopvl/fizzy/internal/colors"
15
+ )
16
+
17
+ type Client struct {
18
+ BaseURL string
19
+ AccountBaseURL string
20
+ BoardBaseURL string
21
+ AccessToken string
22
+ HTTPClient *http.Client
23
+ }
24
+
25
+ func NewClient(accountSlug string, boardID string) (*Client, error) {
26
+ baseURL := "https://app.fizzy.do"
27
+ accountBaseURL := baseURL + accountSlug
28
+
29
+ var boardBaseURL string
30
+ if boardID != "" {
31
+ boardBaseURL = accountBaseURL + "/boards" + "/" + boardID
32
+ }
33
+
34
+ token, isSet := os.LookupEnv("FIZZY_ACCESS_TOKEN")
35
+ if !isSet || token == "" {
36
+ return nil, fmt.Errorf("FIZZY_ACCESS_TOKEN environment variable is not set")
37
+ }
38
+
39
+ return &Client{
40
+ BaseURL: baseURL,
41
+ AccountBaseURL: accountBaseURL,
42
+ BoardBaseURL: boardBaseURL,
43
+ AccessToken: token,
44
+ HTTPClient: &http.Client{Timeout: 30 * time.Second},
45
+ }, nil
46
+ }
47
+
48
+ // newRequest makes an HTTP request with the required headers setup
49
+ func (c *Client) newRequest(ctx context.Context, method, url string, body any) (*http.Request, error) {
50
+ var bodyReader io.Reader
51
+ if body != nil {
52
+ data, err := json.Marshal(body)
53
+ if err != nil {
54
+ return nil, fmt.Errorf("failed to marshal request body: %w", err)
55
+ }
56
+ bodyReader = bytes.NewReader(data)
57
+ }
58
+
59
+ req, err := http.NewRequestWithContext(ctx, method, url, bodyReader)
60
+ if err != nil {
61
+ return nil, err
62
+ }
63
+
64
+ req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.AccessToken))
65
+ req.Header.Set("Accept", "application/json")
66
+ req.Header.Set("Content-Type", "application/json")
67
+
68
+ return req, nil
69
+ }
70
+
71
+ // decodeResponse executes a request and decodes the JSON response into v
72
+ // If expectedStatus is 0, it defaults to http.StatusOK
73
+ // If v is nil, the response body is not decoded
74
+ func (c *Client) decodeResponse(req *http.Request, v any, expectedStatus ...int) (int, error) {
75
+ expectedCode := http.StatusOK
76
+ if len(expectedStatus) > 0 {
77
+ expectedCode = expectedStatus[0]
78
+ }
79
+
80
+ res, err := c.HTTPClient.Do(req)
81
+ if err != nil {
82
+ return 0, fmt.Errorf("failed to make request: %w", err)
83
+ }
84
+ defer res.Body.Close()
85
+
86
+ if res.StatusCode != expectedCode {
87
+ body, _ := io.ReadAll(res.Body)
88
+ return 0, fmt.Errorf("unexpected status code %d: %s", res.StatusCode, string(body))
89
+ }
90
+
91
+ if v != nil {
92
+ if err := json.NewDecoder(res.Body).Decode(v); err != nil {
93
+ return 0, fmt.Errorf("failed to decode response: %w", err)
94
+ }
95
+ }
96
+
97
+ return res.StatusCode, nil
98
+ }
99
+
100
+ func (c *Client) GetBoards(ctx context.Context) ([]Board, error) {
101
+ endpointURL := c.AccountBaseURL + "/boards"
102
+
103
+ req, err := c.newRequest(ctx, http.MethodGet, endpointURL, nil)
104
+ if err != nil {
105
+ return nil, fmt.Errorf("failed to create request: %w", err)
106
+ }
107
+
108
+ var response []Board
109
+ _, err = c.decodeResponse(req, &response)
110
+ if err != nil {
111
+ return nil, err
112
+ }
113
+
114
+ return response, nil
115
+ }
116
+
117
+ func (c *Client) GetBoard(ctx context.Context, boardID string) (*Board, error) {
118
+ endpointURL := c.AccountBaseURL + "/boards/" + boardID
119
+
120
+ req, err := c.newRequest(ctx, http.MethodGet, endpointURL, nil)
121
+ if err != nil {
122
+ return nil, fmt.Errorf("failed to create request: %w", err)
123
+ }
124
+
125
+ var response Board
126
+ _, err = c.decodeResponse(req, &response)
127
+ if err != nil {
128
+ return nil, err
129
+ }
130
+
131
+ return &response, nil
132
+ }
133
+
134
+ func (c *Client) PostBoards(ctx context.Context, payload CreateBoardPayload) (bool, error) {
135
+ endpointURL := c.AccountBaseURL + "/boards"
136
+
137
+ body := map[string]CreateBoardPayload{"board": payload}
138
+
139
+ req, err := c.newRequest(ctx, http.MethodPost, endpointURL, body)
140
+ if err != nil {
141
+ return false, fmt.Errorf("failed to create board request: %w", err)
142
+ }
143
+
144
+ _, err = c.decodeResponse(req, nil, http.StatusCreated)
145
+ if err != nil {
146
+ return false, err
147
+ }
148
+
149
+ return true, nil
150
+ }
151
+
152
+ func (c *Client) GetColumns(ctx context.Context) ([]Column, error) {
153
+ if c.BoardBaseURL == "" {
154
+ return nil, fmt.Errorf("please select a board first with 'fizzy use --board <board_name>'")
155
+ }
156
+
157
+ endpointURL := c.BoardBaseURL + "/columns"
158
+
159
+ req, err := c.newRequest(ctx, http.MethodGet, endpointURL, nil)
160
+ if err != nil {
161
+ return nil, fmt.Errorf("failed to create get columns request: %w", err)
162
+ }
163
+
164
+ var response []Column
165
+ _, err = c.decodeResponse(req, &response)
166
+ if err != nil {
167
+ return nil, err
168
+ }
169
+
170
+ return response, nil
171
+ }
172
+
173
+ func (c *Client) PostColumns(ctx context.Context, payload CreateColumnPayload) (bool, error) {
174
+ if c.BoardBaseURL == "" {
175
+ return false, fmt.Errorf("please select a board first with 'fizzy use --board <board_name>'")
176
+ }
177
+
178
+ endpointURL := c.BoardBaseURL + "/columns"
179
+
180
+ body := map[string]CreateColumnPayload{"column": payload}
181
+
182
+ req, err := c.newRequest(ctx, http.MethodPost, endpointURL, body)
183
+ if err != nil {
184
+ return false, fmt.Errorf("failed to create column request: %w", err)
185
+ }
186
+
187
+ _, err = c.decodeResponse(req, nil, http.StatusCreated)
188
+ if err != nil {
189
+ return false, err
190
+ }
191
+
192
+ return true, nil
193
+ }
194
+
195
+ func (c *Client) GetCards(ctx context.Context, filters CardFilters) ([]Card, error) {
196
+ endpointURL := c.AccountBaseURL + "/cards"
197
+
198
+ req, err := c.newRequest(ctx, http.MethodGet, endpointURL, nil)
199
+ if err != nil {
200
+ return nil, fmt.Errorf("failed to create get cards request: %w", err)
201
+ }
202
+
203
+ if len(filters.BoardIDs) > 0 {
204
+ q := req.URL.Query()
205
+ for _, boardID := range filters.BoardIDs {
206
+ q.Add("board_ids[]", boardID)
207
+ }
208
+ req.URL.RawQuery = q.Encode()
209
+ }
210
+
211
+ var response []Card
212
+ _, err = c.decodeResponse(req, &response)
213
+ if err != nil {
214
+ return nil, err
215
+ }
216
+
217
+ return response, nil
218
+ }
219
+
220
+ func (c *Client) GetCard(ctx context.Context, cardNumber int) (*Card, error) {
221
+ endpointURL := fmt.Sprintf("%s/cards/%d", c.AccountBaseURL, cardNumber)
222
+
223
+ req, err := c.newRequest(ctx, http.MethodGet, endpointURL, nil)
224
+ if err != nil {
225
+ return nil, fmt.Errorf("failed to create get card by id request: %w", err)
226
+ }
227
+
228
+ var response Card
229
+ _, err = c.decodeResponse(req, &response)
230
+ if err != nil {
231
+ return nil, err
232
+ }
233
+
234
+ return &response, nil
235
+ }
236
+
237
+ func (c *Client) PostCards(ctx context.Context, payload CreateCardPayload) (bool, error) {
238
+ if c.BoardBaseURL == "" {
239
+ return false, fmt.Errorf("please select a board first with 'fizzy use --board <board_name>'")
240
+ }
241
+
242
+ endpointURL := c.BoardBaseURL + "/cards"
243
+
244
+ body := map[string]CreateCardPayload{"card": payload}
245
+
246
+ req, err := c.newRequest(ctx, http.MethodPost, endpointURL, body)
247
+ if err != nil {
248
+ return false, fmt.Errorf("failed to create card request: %w", err)
249
+ }
250
+
251
+ _, err = c.decodeResponse(req, nil, http.StatusCreated)
252
+ if err != nil {
253
+ return false, err
254
+ }
255
+
256
+ return true, nil
257
+ }
258
+
259
+ func (c *Client) PutCard(ctx context.Context, cardNumber int, payload UpdateCardPayload) (*Card, error) {
260
+ endpointURL := fmt.Sprintf("%s/cards/%d", c.AccountBaseURL, cardNumber)
261
+
262
+ body := map[string]UpdateCardPayload{"card": payload}
263
+
264
+ req, err := c.newRequest(ctx, http.MethodPut, endpointURL, body)
265
+ if err != nil {
266
+ return nil, fmt.Errorf("failed to create update card request: %w", err)
267
+ }
268
+
269
+ var response Card
270
+ _, err = c.decodeResponse(req, &response, http.StatusOK)
271
+ if err != nil {
272
+ return nil, err
273
+ }
274
+
275
+ return &response, nil
276
+ }
277
+
278
+ func (c *Client) DeleteCard(ctx context.Context, cardNumber int) (bool, error) {
279
+ endpointURL := fmt.Sprintf("%s/cards/%d", c.AccountBaseURL, cardNumber)
280
+
281
+ req, err := c.newRequest(ctx, http.MethodDelete, endpointURL, nil)
282
+ if err != nil {
283
+ return false, fmt.Errorf("failed to create delete card request: %w", err)
284
+ }
285
+
286
+ _, err = c.decodeResponse(req, nil, http.StatusNoContent)
287
+ if err != nil {
288
+ return false, err
289
+ }
290
+
291
+ return true, nil
292
+ }
293
+
294
+ func (c *Client) PostCardsClosure(ctx context.Context, cardNumber int) (bool, error) {
295
+ endpointURL := fmt.Sprintf("%s/cards/%d/closure", c.AccountBaseURL, cardNumber)
296
+
297
+ req, err := c.newRequest(ctx, http.MethodPost, endpointURL, nil)
298
+ if err != nil {
299
+ return false, fmt.Errorf("failed to create closure card request: %w", err)
300
+ }
301
+
302
+ _, err = c.decodeResponse(req, nil, http.StatusNoContent)
303
+ if err != nil {
304
+ return false, err
305
+ }
306
+
307
+ return true, nil
308
+ }
309
+
310
+ func (c *Client) DeleteCardsClosure(ctx context.Context, cardNumber int) (bool, error) {
311
+ endpointURL := fmt.Sprintf("%s/cards/%d/closure", c.AccountBaseURL, cardNumber)
312
+
313
+ req, err := c.newRequest(ctx, http.MethodDelete, endpointURL, nil)
314
+ if err != nil {
315
+ return false, fmt.Errorf("failed to create delete closure card request: %w", err)
316
+ }
317
+
318
+ _, err = c.decodeResponse(req, nil, http.StatusNoContent)
319
+ if err != nil {
320
+ return false, err
321
+ }
322
+
323
+ return true, nil
324
+ }
325
+
326
+ func (c *Client) GetMyIdentity(ctx context.Context) (*GetMyIdentityResponse, error) {
327
+ endpointURL := c.BaseURL + "/my/identity"
328
+
329
+ req, err := c.newRequest(ctx, http.MethodGet, endpointURL, nil)
330
+ if err != nil {
331
+ return nil, fmt.Errorf("failed to create request: %w", err)
332
+ }
333
+
334
+ var response GetMyIdentityResponse
335
+ _, err = c.decodeResponse(req, &response)
336
+ if err != nil {
337
+ return nil, err
338
+ }
339
+
340
+ return &response, nil
341
+ }
342
+
343
+ type Board struct {
344
+ ID string `json:"id"`
345
+ Name string `json:"name"`
346
+ AllAccess bool `json:"all_access"`
347
+ CreatedAt string `json:"created_at"`
348
+ URL string `json:"url"`
349
+ Creator User `json:"creator"`
350
+ }
351
+
352
+ type CreateBoardPayload struct {
353
+ Name string `json:"name"`
354
+ AllAccess bool `json:"all_access"`
355
+ AutoPostponePeriod int `json:"auto_postpone_period"`
356
+ PublicDescription string `json:"public_description"`
357
+ }
358
+
359
+ type Column struct {
360
+ ID string `json:"id"`
361
+ Name string `json:"name"`
362
+ Color ColorObject `json:"color"`
363
+ CreatedAt string `json:"created_at"`
364
+ }
365
+
366
+ type ColorObject struct {
367
+ Name string `json:"name"`
368
+ Value Color `json:"value"`
369
+ }
370
+
371
+ type CreateColumnPayload struct {
372
+ Name string `json:"name"`
373
+ Color *Color `json:"color,omitempty"`
374
+ }
375
+
376
+ type Card struct {
377
+ ID string `json:"id"`
378
+ Number int `json:"number"`
379
+ Title string `json:"title"`
380
+ Status string `json:"status"`
381
+ Description string `json:"description"`
382
+ DescriptionHTML string `json:"description_html"`
383
+ ImageURL string `json:"image_url"`
384
+ Tags []string `json:"tags"`
385
+ Golden bool `json:"golden"`
386
+ LastActiveAt string `json:"last_active_at"`
387
+ CreatedAt string `json:"created_at"`
388
+ URL string `json:"url"`
389
+ Board Board `json:"board"`
390
+ Creator User `json:"creator"`
391
+ CommentsURL string `json:"comments_url"`
392
+ }
393
+
394
+ type CardFilters struct {
395
+ BoardIDs []string
396
+ TagIDs []string
397
+ AssigneeIDs []string
398
+ CreatorIDs []string
399
+ CloserIDs []string
400
+ CardIDs []string
401
+ IndexedBy string
402
+ SortedBy string
403
+ AssignmentStatus string
404
+ CreationStatus string
405
+ ClosureStatus string
406
+ Terms []string
407
+ }
408
+
409
+ type CreateCardPayload struct {
410
+ Title string `json:"title"`
411
+ Description string `json:"description,omitempty"`
412
+ Status string `json:"status,omitempty"`
413
+ ImageURL string `json:"image_url,omitempty"`
414
+ TagIDS []string `json:"tag_ids,omitempty"`
415
+ CreatedAt string `json:"created_at,omitempty"`
416
+ LastActiveAt string `json:"last_active_at,omitempty"`
417
+ }
418
+
419
+ // UpdateCardPayload image not included because we don't support files yet
420
+ type UpdateCardPayload struct {
421
+ Title string `json:"title,omitempty"`
422
+ Description string `json:"description,omitempty"`
423
+ Status string `json:"status,omitempty"`
424
+ TagIDS []string `json:"tag_ids,omitempty"`
425
+ LastActiveAt string `json:"last_active_at,omitempty"`
426
+ }
427
+
428
+ type GetMyIdentityResponse struct {
429
+ Accounts []Account `json:"accounts"`
430
+ }
431
+
432
+ type Account struct {
433
+ ID string `json:"id"`
434
+ Name string `json:"name"`
435
+ User User `json:"user"`
436
+ Slug string `json:"slug"`
437
+ CreatedAt string `json:"created_at"`
438
+ }
439
+
440
+ type User struct {
441
+ ID string `json:"id"`
442
+ Email string `json:"email_address"`
443
+ Role string `json:"role"`
444
+ Active bool `json:"active"`
445
+ Name string `json:"name"`
446
+ CreatedAt string `json:"created_at"`
447
+ URL string `json:"url"`
448
+ }
449
+
450
+ type Color string
451
+
452
+ // Color constants using centralized definitions
453
+ var (
454
+ Blue Color = Color(colors.Blue.CSSValue)
455
+ Gray Color = Color(colors.Gray.CSSValue)
456
+ Tan Color = Color(colors.Tan.CSSValue)
457
+ Yellow Color = Color(colors.Yellow.CSSValue)
458
+ Lime Color = Color(colors.Lime.CSSValue)
459
+ Aqua Color = Color(colors.Aqua.CSSValue)
460
+ Violet Color = Color(colors.Violet.CSSValue)
461
+ Purple Color = Color(colors.Purple.CSSValue)
462
+ Pink Color = Color(colors.Pink.CSSValue)
463
+ )
@@ -0,0 +1,49 @@
1
+ // Package app
2
+ package app
3
+
4
+ import (
5
+ "context"
6
+ "fmt"
7
+ "os"
8
+
9
+ "github.com/rogeriopvl/fizzy/internal/api"
10
+ "github.com/rogeriopvl/fizzy/internal/config"
11
+ )
12
+
13
+ type App struct {
14
+ Client *api.Client
15
+ Config *config.Config
16
+ }
17
+
18
+ func New() (*App, error) {
19
+ cfg, err := config.Load()
20
+ if err != nil {
21
+ return nil, fmt.Errorf("loading config: %w", err)
22
+ }
23
+
24
+ token, isSet := os.LookupEnv("FIZZY_ACCESS_TOKEN")
25
+ if !isSet || token == "" {
26
+ return &App{Config: cfg}, nil // No token set, app will handle gracefully
27
+ }
28
+
29
+ client, err := api.NewClient(cfg.SelectedAccount, cfg.SelectedBoard)
30
+ if err != nil {
31
+ return nil, fmt.Errorf("creating API client: %w", err)
32
+ }
33
+
34
+ return &App{Client: client, Config: cfg}, nil
35
+ }
36
+
37
+ // contextKey is a type for context keys to avoid collisions.
38
+ type contextKey string
39
+
40
+ const appContextKey contextKey = "app"
41
+
42
+ func FromContext(ctx context.Context) *App {
43
+ app, _ := ctx.Value(appContextKey).(*App)
44
+ return app
45
+ }
46
+
47
+ func (a *App) ToContext(ctx context.Context) context.Context {
48
+ return context.WithValue(ctx, appContextKey, a)
49
+ }
@@ -0,0 +1,32 @@
1
+ package colors
2
+
3
+ import "github.com/charmbracelet/lipgloss"
4
+
5
+ type ColorDef struct {
6
+ Name string
7
+ CSSValue string
8
+ TermColor lipgloss.Color
9
+ }
10
+
11
+ var (
12
+ Blue = ColorDef{"Blue", "var(--color-card-default)", lipgloss.Color("12")}
13
+ Gray = ColorDef{"Gray", "var(--color-card-1)", lipgloss.Color("8")}
14
+ Tan = ColorDef{"Tan", "var(--color-card-2)", lipgloss.Color("180")}
15
+ Yellow = ColorDef{"Yellow", "var(--color-card-3)", lipgloss.Color("11")}
16
+ Lime = ColorDef{"Lime", "var(--color-card-4)", lipgloss.Color("10")}
17
+ Aqua = ColorDef{"Aqua", "var(--color-card-5)", lipgloss.Color("14")}
18
+ Violet = ColorDef{"Violet", "var(--color-card-6)", lipgloss.Color("177")}
19
+ Purple = ColorDef{"Purple", "var(--color-card-7)", lipgloss.Color("135")}
20
+ Pink = ColorDef{"Pink", "var(--color-card-8)", lipgloss.Color("205")}
21
+ )
22
+
23
+ var All = []ColorDef{Blue, Gray, Tan, Yellow, Lime, Aqua, Violet, Purple, Pink}
24
+
25
+ func ByName(name string) *ColorDef {
26
+ for _, c := range All {
27
+ if c.Name == name {
28
+ return &c
29
+ }
30
+ }
31
+ return nil
32
+ }
@@ -0,0 +1,69 @@
1
+ // Package config
2
+ package config
3
+
4
+ import (
5
+ "encoding/json"
6
+ "fmt"
7
+ "os"
8
+ "path/filepath"
9
+ )
10
+
11
+ const (
12
+ configDir = ".config/fizzy-cli"
13
+ configFile = "config.json"
14
+ )
15
+
16
+ type Config struct {
17
+ SelectedAccount string `json:"selected_account"`
18
+ SelectedBoard string `json:"selected_board"`
19
+ }
20
+
21
+ // Load reads the config file from $HOME/.config/fizzy-cli/config.json.
22
+ func Load() (*Config, error) {
23
+ homeDir, err := os.UserHomeDir()
24
+ if err != nil {
25
+ return nil, fmt.Errorf("getting home directory: %w", err)
26
+ }
27
+
28
+ configPath := filepath.Join(homeDir, configDir, configFile)
29
+
30
+ data, err := os.ReadFile(configPath)
31
+ if err != nil {
32
+ if os.IsNotExist(err) {
33
+ return &Config{}, nil
34
+ }
35
+ return nil, fmt.Errorf("reading config file: %w", err)
36
+ }
37
+
38
+ var config Config
39
+ if err := json.Unmarshal(data, &config); err != nil {
40
+ return nil, fmt.Errorf("parsing config file: %w", err)
41
+ }
42
+
43
+ return &config, nil
44
+ }
45
+
46
+ func (c *Config) Save() error {
47
+ homeDir, err := os.UserHomeDir()
48
+ if err != nil {
49
+ return fmt.Errorf("getting home directory: %w", err)
50
+ }
51
+
52
+ configDirPath := filepath.Join(homeDir, configDir)
53
+ if err := os.MkdirAll(configDirPath, 0o755); err != nil {
54
+ return fmt.Errorf("creating config directory: %w", err)
55
+ }
56
+
57
+ configPath := filepath.Join(configDirPath, configFile)
58
+
59
+ data, err := json.MarshalIndent(c, "", " ")
60
+ if err != nil {
61
+ return fmt.Errorf("marshaling config: %w", err)
62
+ }
63
+
64
+ if err := os.WriteFile(configPath, data, 0o644); err != nil {
65
+ return fmt.Errorf("writing config file: %w", err)
66
+ }
67
+
68
+ return nil
69
+ }