fizzy-cli 0.3.0 → 0.4.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/CHANGELOG.md +13 -0
- package/IMPLEMENTATION_PLAN.md +338 -0
- package/cmd/card_golden.go +46 -0
- package/cmd/card_golden_test.go +92 -0
- package/cmd/card_not_now.go +46 -0
- package/cmd/card_not_now_test.go +92 -0
- package/cmd/card_tag.go +51 -0
- package/cmd/card_tag_test.go +112 -0
- package/cmd/card_ungolden.go +46 -0
- package/cmd/card_ungolden_test.go +92 -0
- package/cmd/card_untriage.go +46 -0
- package/cmd/card_untriage_test.go +92 -0
- package/cmd/card_unwatch.go +46 -0
- package/cmd/card_unwatch_test.go +92 -0
- package/cmd/card_watch.go +46 -0
- package/cmd/card_watch_test.go +92 -0
- package/cmd/tag.go +15 -0
- package/cmd/tag_list.go +47 -0
- package/cmd/tag_list_test.go +109 -0
- package/docs/API.md +36 -0
- package/internal/api/client.go +138 -0
- package/package.json +1 -1
package/cmd/tag_list.go
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
package cmd
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"fmt"
|
|
6
|
+
|
|
7
|
+
"github.com/rogeriopvl/fizzy/internal/app"
|
|
8
|
+
"github.com/spf13/cobra"
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
var tagListCmd = &cobra.Command{
|
|
12
|
+
Use: "list",
|
|
13
|
+
Short: "List all tags",
|
|
14
|
+
Long: `Retrieve and display all tags in the account`,
|
|
15
|
+
Run: func(cmd *cobra.Command, args []string) {
|
|
16
|
+
if err := handleListTags(cmd); err != nil {
|
|
17
|
+
fmt.Fprintf(cmd.OutOrStderr(), "Error: %v\n", err)
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
func handleListTags(cmd *cobra.Command) error {
|
|
23
|
+
a := app.FromContext(cmd.Context())
|
|
24
|
+
if a == nil || a.Client == nil {
|
|
25
|
+
return fmt.Errorf("API client not available")
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
tags, err := a.Client.GetTags(context.Background())
|
|
29
|
+
if err != nil {
|
|
30
|
+
return fmt.Errorf("fetching tags: %w", err)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if len(tags) == 0 {
|
|
34
|
+
fmt.Println("No tags found")
|
|
35
|
+
return nil
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
for _, tag := range tags {
|
|
39
|
+
fmt.Printf("%s\n", tag.Title)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return nil
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
func init() {
|
|
46
|
+
tagCmd.AddCommand(tagListCmd)
|
|
47
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
package cmd
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"encoding/json"
|
|
6
|
+
"net/http"
|
|
7
|
+
"net/http/httptest"
|
|
8
|
+
"testing"
|
|
9
|
+
|
|
10
|
+
"github.com/rogeriopvl/fizzy/internal/api"
|
|
11
|
+
"github.com/rogeriopvl/fizzy/internal/app"
|
|
12
|
+
"github.com/rogeriopvl/fizzy/internal/testutil"
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
func TestTagListCommand(t *testing.T) {
|
|
16
|
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
17
|
+
if r.URL.Path != "/tags" {
|
|
18
|
+
t.Errorf("expected /tags, got %s", r.URL.Path)
|
|
19
|
+
}
|
|
20
|
+
if r.Method != http.MethodGet {
|
|
21
|
+
t.Errorf("expected GET, got %s", r.Method)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
auth := r.Header.Get("Authorization")
|
|
25
|
+
if auth != "Bearer test-token" {
|
|
26
|
+
t.Errorf("expected Bearer test-token, got %s", auth)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
w.Header().Set("Content-Type", "application/json")
|
|
30
|
+
response := []api.Tag{
|
|
31
|
+
{
|
|
32
|
+
ID: "tag-123",
|
|
33
|
+
Title: "bug",
|
|
34
|
+
CreatedAt: "2025-01-01T00:00:00Z",
|
|
35
|
+
URL: "http://fizzy.localhost:3006/897362094/cards?tag_ids[]=tag-123",
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
ID: "tag-456",
|
|
39
|
+
Title: "feature",
|
|
40
|
+
CreatedAt: "2025-01-02T00:00:00Z",
|
|
41
|
+
URL: "http://fizzy.localhost:3006/897362094/cards?tag_ids[]=tag-456",
|
|
42
|
+
},
|
|
43
|
+
}
|
|
44
|
+
json.NewEncoder(w).Encode(response)
|
|
45
|
+
}))
|
|
46
|
+
defer server.Close()
|
|
47
|
+
|
|
48
|
+
client := testutil.NewTestClient(server.URL, "", "", "test-token")
|
|
49
|
+
testApp := &app.App{Client: client}
|
|
50
|
+
|
|
51
|
+
cmd := tagListCmd
|
|
52
|
+
cmd.SetContext(testApp.ToContext(context.Background()))
|
|
53
|
+
|
|
54
|
+
if err := handleListTags(cmd); err != nil {
|
|
55
|
+
t.Fatalf("handleListTags failed: %v", err)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
func TestTagListCommandNoTags(t *testing.T) {
|
|
60
|
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
61
|
+
w.Header().Set("Content-Type", "application/json")
|
|
62
|
+
w.Write([]byte("[]"))
|
|
63
|
+
}))
|
|
64
|
+
defer server.Close()
|
|
65
|
+
|
|
66
|
+
client := testutil.NewTestClient(server.URL, "", "", "test-token")
|
|
67
|
+
testApp := &app.App{Client: client}
|
|
68
|
+
|
|
69
|
+
cmd := tagListCmd
|
|
70
|
+
cmd.SetContext(testApp.ToContext(context.Background()))
|
|
71
|
+
|
|
72
|
+
if err := handleListTags(cmd); err != nil {
|
|
73
|
+
t.Fatalf("handleListTags with no tags failed: %v", err)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
func TestTagListCommandAPIError(t *testing.T) {
|
|
78
|
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
79
|
+
w.WriteHeader(http.StatusInternalServerError)
|
|
80
|
+
w.Write([]byte("Internal Server Error"))
|
|
81
|
+
}))
|
|
82
|
+
defer server.Close()
|
|
83
|
+
|
|
84
|
+
client := testutil.NewTestClient(server.URL, "", "", "test-token")
|
|
85
|
+
testApp := &app.App{Client: client}
|
|
86
|
+
|
|
87
|
+
cmd := tagListCmd
|
|
88
|
+
cmd.SetContext(testApp.ToContext(context.Background()))
|
|
89
|
+
|
|
90
|
+
err := handleListTags(cmd)
|
|
91
|
+
if err == nil {
|
|
92
|
+
t.Errorf("expected error for API failure")
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
func TestTagListCommandNoClient(t *testing.T) {
|
|
97
|
+
testApp := &app.App{}
|
|
98
|
+
|
|
99
|
+
cmd := tagListCmd
|
|
100
|
+
cmd.SetContext(testApp.ToContext(context.Background()))
|
|
101
|
+
|
|
102
|
+
err := handleListTags(cmd)
|
|
103
|
+
if err == nil {
|
|
104
|
+
t.Errorf("expected error when client not available")
|
|
105
|
+
}
|
|
106
|
+
if err.Error() != "API client not available" {
|
|
107
|
+
t.Errorf("expected 'client not available' error, got %v", err)
|
|
108
|
+
}
|
|
109
|
+
}
|
package/docs/API.md
CHANGED
|
@@ -574,6 +574,7 @@ __Response:__
|
|
|
574
574
|
"description_html": "<div class=\"action-text-content\"><p>Hello, World!</p></div>",
|
|
575
575
|
"image_url": null,
|
|
576
576
|
"tags": ["programming"],
|
|
577
|
+
"closed": false,
|
|
577
578
|
"golden": false,
|
|
578
579
|
"last_active_at": "2025-12-05T19:38:48.553Z",
|
|
579
580
|
"created_at": "2025-12-05T19:38:48.540Z",
|
|
@@ -594,6 +595,15 @@ __Response:__
|
|
|
594
595
|
"url": "http://fizzy.localhost:3006/897362094/users/03f5v9zjw7pz8717a4no1h8a7"
|
|
595
596
|
}
|
|
596
597
|
},
|
|
598
|
+
"column": {
|
|
599
|
+
"id": "03f5v9zkft4hj9qq0lsn9ohcn",
|
|
600
|
+
"name": "In Progress",
|
|
601
|
+
"color": {
|
|
602
|
+
"name": "Lime",
|
|
603
|
+
"value": "var(--color-card-4)"
|
|
604
|
+
},
|
|
605
|
+
"created_at": "2025-12-05T19:36:35.534Z"
|
|
606
|
+
},
|
|
597
607
|
"creator": {
|
|
598
608
|
"id": "03f5v9zjw7pz8717a4no1h8a7",
|
|
599
609
|
"name": "David Heinemeier Hansson",
|
|
@@ -619,6 +629,8 @@ __Response:__
|
|
|
619
629
|
}
|
|
620
630
|
```
|
|
621
631
|
|
|
632
|
+
> **Note:** The `closed` field indicates whether the card is in the "Done" state. The `column` field is only present when the card has been triaged into a column; cards in "Maybe?", "Not Now" or "Done" will not have this field.
|
|
633
|
+
|
|
622
634
|
### `POST /:account_slug/boards/:board_id/cards`
|
|
623
635
|
|
|
624
636
|
Creates a new card in a board.
|
|
@@ -683,6 +695,14 @@ __Response:__
|
|
|
683
695
|
|
|
684
696
|
Returns `204 No Content` on success.
|
|
685
697
|
|
|
698
|
+
### `DELETE /:account_slug/cards/:card_number/image`
|
|
699
|
+
|
|
700
|
+
Removes the header image from a card.
|
|
701
|
+
|
|
702
|
+
__Response:__
|
|
703
|
+
|
|
704
|
+
Returns `204 No Content` on success.
|
|
705
|
+
|
|
686
706
|
### `POST /:account_slug/cards/:card_number/closure`
|
|
687
707
|
|
|
688
708
|
Closes a card.
|
|
@@ -767,6 +787,22 @@ __Response:__
|
|
|
767
787
|
|
|
768
788
|
Returns `204 No Content` on success.
|
|
769
789
|
|
|
790
|
+
### `POST /:account_slug/cards/:card_number/goldness`
|
|
791
|
+
|
|
792
|
+
Marks a card as golden.
|
|
793
|
+
|
|
794
|
+
__Response:__
|
|
795
|
+
|
|
796
|
+
Returns `204 No Content` on success.
|
|
797
|
+
|
|
798
|
+
### `DELETE /:account_slug/cards/:card_number/goldness`
|
|
799
|
+
|
|
800
|
+
Removes golden status from a card.
|
|
801
|
+
|
|
802
|
+
__Response:__
|
|
803
|
+
|
|
804
|
+
Returns `204 No Content` on success.
|
|
805
|
+
|
|
770
806
|
## Comments
|
|
771
807
|
|
|
772
808
|
Comments are attached to cards and support rich text.
|
package/internal/api/client.go
CHANGED
|
@@ -307,6 +307,22 @@ func (c *Client) PostCardsClosure(ctx context.Context, cardNumber int) (bool, er
|
|
|
307
307
|
return true, nil
|
|
308
308
|
}
|
|
309
309
|
|
|
310
|
+
func (c *Client) PostCardNotNow(ctx context.Context, cardNumber int) (bool, error) {
|
|
311
|
+
endpointURL := fmt.Sprintf("%s/cards/%d/not_now", c.AccountBaseURL, cardNumber)
|
|
312
|
+
|
|
313
|
+
req, err := c.newRequest(ctx, http.MethodPost, endpointURL, nil)
|
|
314
|
+
if err != nil {
|
|
315
|
+
return false, fmt.Errorf("failed to create post not now 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
|
+
|
|
310
326
|
func (c *Client) PostCardTriage(ctx context.Context, cardNumber int, columnID string) (bool, error) {
|
|
311
327
|
endpointURL := fmt.Sprintf("%s/cards/%d/triage", c.AccountBaseURL, cardNumber)
|
|
312
328
|
|
|
@@ -325,6 +341,86 @@ func (c *Client) PostCardTriage(ctx context.Context, cardNumber int, columnID st
|
|
|
325
341
|
return true, nil
|
|
326
342
|
}
|
|
327
343
|
|
|
344
|
+
func (c *Client) DeleteCardTriage(ctx context.Context, cardNumber int) (bool, error) {
|
|
345
|
+
endpointURL := fmt.Sprintf("%s/cards/%d/triage", c.AccountBaseURL, cardNumber)
|
|
346
|
+
|
|
347
|
+
req, err := c.newRequest(ctx, http.MethodDelete, endpointURL, nil)
|
|
348
|
+
if err != nil {
|
|
349
|
+
return false, fmt.Errorf("failed to create delete triage request: %w", err)
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
_, err = c.decodeResponse(req, nil, http.StatusNoContent)
|
|
353
|
+
if err != nil {
|
|
354
|
+
return false, err
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
return true, nil
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
func (c *Client) PostCardWatch(ctx context.Context, cardNumber int) (bool, error) {
|
|
361
|
+
endpointURL := fmt.Sprintf("%s/cards/%d/watch", c.AccountBaseURL, cardNumber)
|
|
362
|
+
|
|
363
|
+
req, err := c.newRequest(ctx, http.MethodPost, endpointURL, nil)
|
|
364
|
+
if err != nil {
|
|
365
|
+
return false, fmt.Errorf("failed to create post watch request: %w", err)
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
_, err = c.decodeResponse(req, nil, http.StatusNoContent)
|
|
369
|
+
if err != nil {
|
|
370
|
+
return false, err
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
return true, nil
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
func (c *Client) DeleteCardWatch(ctx context.Context, cardNumber int) (bool, error) {
|
|
377
|
+
endpointURL := fmt.Sprintf("%s/cards/%d/watch", c.AccountBaseURL, cardNumber)
|
|
378
|
+
|
|
379
|
+
req, err := c.newRequest(ctx, http.MethodDelete, endpointURL, nil)
|
|
380
|
+
if err != nil {
|
|
381
|
+
return false, fmt.Errorf("failed to create delete watch request: %w", err)
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
_, err = c.decodeResponse(req, nil, http.StatusNoContent)
|
|
385
|
+
if err != nil {
|
|
386
|
+
return false, err
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
return true, nil
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
func (c *Client) PostCardGoldenness(ctx context.Context, cardNumber int) (bool, error) {
|
|
393
|
+
endpointURL := fmt.Sprintf("%s/cards/%d/goldness", c.AccountBaseURL, cardNumber)
|
|
394
|
+
|
|
395
|
+
req, err := c.newRequest(ctx, http.MethodPost, endpointURL, nil)
|
|
396
|
+
if err != nil {
|
|
397
|
+
return false, fmt.Errorf("failed to create post goldness request: %w", err)
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
_, err = c.decodeResponse(req, nil, http.StatusNoContent)
|
|
401
|
+
if err != nil {
|
|
402
|
+
return false, err
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
return true, nil
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
func (c *Client) DeleteCardGoldenness(ctx context.Context, cardNumber int) (bool, error) {
|
|
409
|
+
endpointURL := fmt.Sprintf("%s/cards/%d/goldness", c.AccountBaseURL, cardNumber)
|
|
410
|
+
|
|
411
|
+
req, err := c.newRequest(ctx, http.MethodDelete, endpointURL, nil)
|
|
412
|
+
if err != nil {
|
|
413
|
+
return false, fmt.Errorf("failed to create delete goldness request: %w", err)
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
_, err = c.decodeResponse(req, nil, http.StatusNoContent)
|
|
417
|
+
if err != nil {
|
|
418
|
+
return false, err
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
return true, nil
|
|
422
|
+
}
|
|
423
|
+
|
|
328
424
|
func (c *Client) DeleteCardsClosure(ctx context.Context, cardNumber int) (bool, error) {
|
|
329
425
|
endpointURL := fmt.Sprintf("%s/cards/%d/closure", c.AccountBaseURL, cardNumber)
|
|
330
426
|
|
|
@@ -359,6 +455,24 @@ func (c *Client) PostCardAssignments(ctx context.Context, cardNumber int, userID
|
|
|
359
455
|
return true, nil
|
|
360
456
|
}
|
|
361
457
|
|
|
458
|
+
func (c *Client) PostCardTagging(ctx context.Context, cardNumber int, tagTitle string) (bool, error) {
|
|
459
|
+
endpointURL := fmt.Sprintf("%s/cards/%d/taggings", c.AccountBaseURL, cardNumber)
|
|
460
|
+
|
|
461
|
+
body := map[string]string{"tag_title": tagTitle}
|
|
462
|
+
|
|
463
|
+
req, err := c.newRequest(ctx, http.MethodPost, endpointURL, body)
|
|
464
|
+
if err != nil {
|
|
465
|
+
return false, fmt.Errorf("failed to create tagging request: %w", err)
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
_, err = c.decodeResponse(req, nil, http.StatusNoContent)
|
|
469
|
+
if err != nil {
|
|
470
|
+
return false, err
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
return true, nil
|
|
474
|
+
}
|
|
475
|
+
|
|
362
476
|
func (c *Client) GetMyIdentity(ctx context.Context) (*GetMyIdentityResponse, error) {
|
|
363
477
|
endpointURL := c.BaseURL + "/my/identity"
|
|
364
478
|
|
|
@@ -458,6 +572,23 @@ func (c *Client) PostBulkNotificationsReading(ctx context.Context) (bool, error)
|
|
|
458
572
|
return true, nil
|
|
459
573
|
}
|
|
460
574
|
|
|
575
|
+
func (c *Client) GetTags(ctx context.Context) ([]Tag, error) {
|
|
576
|
+
endpointURL := c.AccountBaseURL + "/tags"
|
|
577
|
+
|
|
578
|
+
req, err := c.newRequest(ctx, http.MethodGet, endpointURL, nil)
|
|
579
|
+
if err != nil {
|
|
580
|
+
return nil, fmt.Errorf("failed to create get tags request: %w", err)
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
var response []Tag
|
|
584
|
+
_, err = c.decodeResponse(req, &response)
|
|
585
|
+
if err != nil {
|
|
586
|
+
return nil, err
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
return response, nil
|
|
590
|
+
}
|
|
591
|
+
|
|
461
592
|
type Board struct {
|
|
462
593
|
ID string `json:"id"`
|
|
463
594
|
Name string `json:"name"`
|
|
@@ -584,6 +715,13 @@ type CardReference struct {
|
|
|
584
715
|
URL string `json:"url"`
|
|
585
716
|
}
|
|
586
717
|
|
|
718
|
+
type Tag struct {
|
|
719
|
+
ID string `json:"id"`
|
|
720
|
+
Title string `json:"title"`
|
|
721
|
+
CreatedAt string `json:"created_at"`
|
|
722
|
+
URL string `json:"url"`
|
|
723
|
+
}
|
|
724
|
+
|
|
587
725
|
type Color string
|
|
588
726
|
|
|
589
727
|
// Color constants using centralized definitions
|