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.
@@ -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.
@@ -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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fizzy-cli",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "CLI for https://fizzy.do",
5
5
  "main": "bin/fizzy",
6
6
  "type": "module",