fizzy-cli 0.1.0 → 0.2.0
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.
- package/.github/workflows/release.yml +29 -0
- package/.github/workflows/tests.yml +24 -0
- package/AGENTS.md +33 -0
- package/CHANGELOG.md +63 -0
- package/Makefile +20 -9
- package/README.md +88 -1
- package/bin/fizzy +0 -0
- package/bin/fizzy-darwin-amd64 +0 -0
- package/bin/fizzy-darwin-arm64 +0 -0
- package/bin/fizzy-linux-amd64 +0 -0
- package/bin/fizzy-linux-arm64 +0 -0
- package/bin/fizzy-windows-amd64.exe +0 -0
- package/cmd/account.go +14 -0
- package/cmd/account_list.go +44 -0
- package/cmd/account_list_test.go +118 -0
- package/cmd/board.go +38 -12
- package/cmd/board_create.go +60 -0
- package/cmd/board_create_test.go +158 -0
- package/cmd/board_list.go +18 -32
- package/cmd/board_list_test.go +115 -0
- package/cmd/board_test.go +92 -0
- package/cmd/card.go +24 -0
- package/cmd/card_close.go +46 -0
- package/cmd/card_close_test.go +92 -0
- package/cmd/card_create.go +73 -0
- package/cmd/card_create_test.go +206 -0
- package/cmd/card_delete.go +46 -0
- package/cmd/card_delete_test.go +92 -0
- package/cmd/card_list.go +53 -0
- package/cmd/card_list_test.go +148 -0
- package/cmd/card_reopen.go +46 -0
- package/cmd/card_reopen_test.go +92 -0
- package/cmd/card_show.go +46 -0
- package/cmd/card_show_test.go +92 -0
- package/cmd/card_update.go +74 -0
- package/cmd/card_update_test.go +147 -0
- package/cmd/column.go +14 -0
- package/cmd/column_create.go +80 -0
- package/cmd/column_create_test.go +196 -0
- package/cmd/column_list.go +44 -0
- package/cmd/column_list_test.go +138 -0
- package/cmd/login.go +61 -4
- package/cmd/login_test.go +98 -0
- package/cmd/root.go +15 -4
- package/cmd/use.go +85 -0
- package/cmd/use_test.go +186 -0
- package/docs/API.md +1168 -0
- package/go.mod +23 -2
- package/go.sum +43 -0
- package/internal/api/client.go +463 -0
- package/internal/app/app.go +49 -0
- package/internal/colors/colors.go +32 -0
- package/internal/config/config.go +69 -0
- package/internal/testutil/client.go +26 -0
- package/internal/ui/account_list.go +14 -0
- package/internal/ui/account_selector.go +63 -0
- package/internal/ui/board_list.go +14 -0
- package/internal/ui/card_list.go +14 -0
- package/internal/ui/card_show.go +23 -0
- package/internal/ui/column_list.go +28 -0
- package/internal/ui/format.go +14 -0
- package/main.go +1 -1
- package/package.json +1 -1
package/go.mod
CHANGED
|
@@ -1,10 +1,31 @@
|
|
|
1
|
-
module github.com/rogeriopvl/fizzy
|
|
1
|
+
module github.com/rogeriopvl/fizzy
|
|
2
2
|
|
|
3
3
|
go 1.25.1
|
|
4
4
|
|
|
5
|
-
require
|
|
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
|
+
}
|