fizzy-cli 0.2.1 → 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.
Files changed (42) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/IMPLEMENTATION_PLAN.md +338 -0
  3. package/bin/fizzy +0 -0
  4. package/cmd/board.go +1 -1
  5. package/cmd/card_assign.go +55 -0
  6. package/cmd/card_assign_test.go +130 -0
  7. package/cmd/card_golden.go +46 -0
  8. package/cmd/card_golden_test.go +92 -0
  9. package/cmd/card_not_now.go +46 -0
  10. package/cmd/card_not_now_test.go +92 -0
  11. package/cmd/card_tag.go +51 -0
  12. package/cmd/card_tag_test.go +112 -0
  13. package/cmd/card_triage.go +46 -0
  14. package/cmd/card_ungolden.go +46 -0
  15. package/cmd/card_ungolden_test.go +92 -0
  16. package/cmd/card_untriage.go +46 -0
  17. package/cmd/card_untriage_test.go +92 -0
  18. package/cmd/card_unwatch.go +46 -0
  19. package/cmd/card_unwatch_test.go +92 -0
  20. package/cmd/card_update.go +0 -1
  21. package/cmd/card_update_test.go +0 -2
  22. package/cmd/card_watch.go +46 -0
  23. package/cmd/card_watch_test.go +92 -0
  24. package/cmd/login.go +2 -1
  25. package/cmd/notification.go +14 -0
  26. package/cmd/notification_list.go +69 -0
  27. package/cmd/notification_list_test.go +288 -0
  28. package/cmd/notification_read.go +51 -0
  29. package/cmd/notification_read_all.go +38 -0
  30. package/cmd/notification_read_all_test.go +75 -0
  31. package/cmd/notification_read_test.go +138 -0
  32. package/cmd/notification_unread.go +44 -0
  33. package/cmd/notification_unread_test.go +99 -0
  34. package/cmd/tag.go +15 -0
  35. package/cmd/tag_list.go +47 -0
  36. package/cmd/tag_list_test.go +109 -0
  37. package/docs/API.md +180 -1
  38. package/go.mod +1 -1
  39. package/internal/api/client.go +275 -0
  40. package/internal/config/config.go +1 -0
  41. package/internal/ui/notification_list.go +27 -0
  42. package/package.json +1 -1
@@ -0,0 +1,99 @@
1
+ package cmd
2
+
3
+ import (
4
+ "context"
5
+ "net/http"
6
+ "net/http/httptest"
7
+ "testing"
8
+
9
+ "github.com/rogeriopvl/fizzy/internal/app"
10
+ "github.com/rogeriopvl/fizzy/internal/testutil"
11
+ )
12
+
13
+ func TestNotificationUnreadCommand(t *testing.T) {
14
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
15
+ if r.URL.Path != "/notifications/notif-123/reading" {
16
+ t.Errorf("expected /notifications/notif-123/reading, got %s", r.URL.Path)
17
+ }
18
+ if r.Method != http.MethodDelete {
19
+ t.Errorf("expected DELETE, got %s", r.Method)
20
+ }
21
+
22
+ auth := r.Header.Get("Authorization")
23
+ if auth != "Bearer test-token" {
24
+ t.Errorf("expected Bearer test-token, got %s", auth)
25
+ }
26
+
27
+ w.WriteHeader(http.StatusNoContent)
28
+ }))
29
+ defer server.Close()
30
+
31
+ client := testutil.NewTestClient(server.URL, "", "", "test-token")
32
+ testApp := &app.App{Client: client}
33
+
34
+ cmd := notificationUnreadCmd
35
+ cmd.SetContext(testApp.ToContext(context.Background()))
36
+
37
+ if err := handleUnreadNotification(cmd, "notif-123"); err != nil {
38
+ t.Fatalf("handleUnreadNotification failed: %v", err)
39
+ }
40
+ }
41
+
42
+ func TestNotificationUnreadCommandNotFound(t *testing.T) {
43
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
44
+ w.WriteHeader(http.StatusNotFound)
45
+ w.Write([]byte("Notification not found"))
46
+ }))
47
+ defer server.Close()
48
+
49
+ client := testutil.NewTestClient(server.URL, "", "", "test-token")
50
+ testApp := &app.App{Client: client}
51
+
52
+ cmd := notificationUnreadCmd
53
+ cmd.SetContext(testApp.ToContext(context.Background()))
54
+
55
+ err := handleUnreadNotification(cmd, "notif-invalid")
56
+ if err == nil {
57
+ t.Errorf("expected error for invalid notification")
58
+ }
59
+ if err.Error() != "notification not found" {
60
+ t.Errorf("expected 'notification not found' error, got %v", err)
61
+ }
62
+ }
63
+
64
+ func TestNotificationUnreadCommandAPIError(t *testing.T) {
65
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
66
+ w.WriteHeader(http.StatusInternalServerError)
67
+ w.Write([]byte("Internal Server Error"))
68
+ }))
69
+ defer server.Close()
70
+
71
+ client := testutil.NewTestClient(server.URL, "", "", "test-token")
72
+ testApp := &app.App{Client: client}
73
+
74
+ cmd := notificationUnreadCmd
75
+ cmd.SetContext(testApp.ToContext(context.Background()))
76
+
77
+ err := handleUnreadNotification(cmd, "notif-123")
78
+ if err == nil {
79
+ t.Errorf("expected error for API failure")
80
+ }
81
+ if err.Error() != "marking notification as unread: unexpected status code 500: Internal Server Error" {
82
+ t.Errorf("expected API error, got %v", err)
83
+ }
84
+ }
85
+
86
+ func TestNotificationUnreadCommandNoClient(t *testing.T) {
87
+ testApp := &app.App{}
88
+
89
+ cmd := notificationUnreadCmd
90
+ cmd.SetContext(testApp.ToContext(context.Background()))
91
+
92
+ err := handleUnreadNotification(cmd, "notif-123")
93
+ if err == nil {
94
+ t.Errorf("expected error when client not available")
95
+ }
96
+ if err.Error() != "API client not available" {
97
+ t.Errorf("expected 'client not available' error, got %v", err)
98
+ }
99
+ }
package/cmd/tag.go ADDED
@@ -0,0 +1,15 @@
1
+ package cmd
2
+
3
+ import (
4
+ "github.com/spf13/cobra"
5
+ )
6
+
7
+ var tagCmd = &cobra.Command{
8
+ Use: "tag",
9
+ Short: "Manage tags",
10
+ Long: `Manage tags in Fizzy`,
11
+ }
12
+
13
+ func init() {
14
+ rootCmd.AddCommand(tagCmd)
15
+ }
@@ -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
@@ -5,6 +5,13 @@ a bot to perform various actions for you.
5
5
 
6
6
  ## Authentication
7
7
 
8
+ There are two ways to authenticate with the Fizzy API:
9
+
10
+ 1. **Personal access tokens** - Long-lived tokens for scripts and integrations
11
+ 2. **Magic link authentication** - Session-based authentication for native apps
12
+
13
+ ### Personal Access Tokens
14
+
8
15
  To use the API you'll need an access token. To get one, go to your profile, then,
9
16
  in the API section, click on "Personal access tokens" and then click on
10
17
  "Generate new access token".
@@ -36,6 +43,81 @@ To authenticate a request using your access token, include it in the `Authorizat
36
43
  curl -H "Authorization: Bearer put-your-access-token-here" -H "Accept: application/json" https://app.fizzy.do/my/identity
37
44
  ```
38
45
 
46
+ ### Magic Link Authentication
47
+
48
+ For native apps, you can authenticate users via magic links. This is a two-step process:
49
+
50
+ #### 1. Request a magic link
51
+
52
+ Send the user's email address to request a magic link be sent to them:
53
+
54
+ ```bash
55
+ curl -X POST \
56
+ -H "Content-Type: application/json" \
57
+ -H "Accept: application/json" \
58
+ -d '{"email_address": "user@example.com"}' \
59
+ https://app.fizzy.do/session
60
+ ```
61
+
62
+ __Response:__
63
+
64
+ ```
65
+ HTTP/1.1 201 Created
66
+ Set-Cookie: pending_authentication_token=...; HttpOnly; SameSite=Lax
67
+ ```
68
+
69
+ ```json
70
+ {
71
+ "pending_authentication_token": "eyJfcmFpbHMi..."
72
+ }
73
+ ```
74
+
75
+ The response includes a `pending_authentication_token` both in the JSON body and as a cookie.
76
+ Native apps should store this token and include it as a cookie when submitting the magic link code.
77
+
78
+ __Error responses:__
79
+
80
+ | Status Code | Description |
81
+ |--------|-------------|
82
+ | `422 Unprocessable entity` | Invalid email address, if sign ups are enabled and the value isn't a valid email address |
83
+ | `429 Too Many Requests` | Rate limit exceeded |
84
+
85
+ #### 2. Submit the magic link code
86
+
87
+ Once the user receives the magic link email, they'll have a 6-character code. Submit it to complete authentication:
88
+
89
+ ```bash
90
+ curl -X POST \
91
+ -H "Content-Type: application/json" \
92
+ -H "Accept: application/json" \
93
+ -H "Cookie: pending_authentication_token=eyJfcmFpbHMi..." \
94
+ -d '{"code": "ABC123"}' \
95
+ https://app.fizzy.do/session/magic_link
96
+ ```
97
+
98
+ __Response:__
99
+
100
+ ```json
101
+ {
102
+ "session_token": "eyJfcmFpbHMi..."
103
+ }
104
+ ```
105
+
106
+ The `session_token` can be used to authenticate subsequent requests by including it as a cookie:
107
+
108
+ ```bash
109
+ curl -H "Cookie: session_token=eyJfcmFpbHMi..." \
110
+ -H "Accept: application/json" \
111
+ https://app.fizzy.do/my/identity
112
+ ```
113
+
114
+ __Error responses:__
115
+
116
+ | Status Code | Description |
117
+ |--------|-------------|
118
+ | `401 Unauthorized` | Invalid `pending_authentication_token` or `code` |
119
+ | `429 Too Many Requests` | Rate limit exceeded |
120
+
39
121
  ## Caching
40
122
 
41
123
  Most endpoints return [ETag](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/ETag) and [Cache-Control](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cache-Control) headers. You can use these to avoid re-downloading unchanged data.
@@ -482,7 +564,72 @@ Returns a specific card by its number.
482
564
 
483
565
  __Response:__
484
566
 
485
- Same as the card object in the list response.
567
+ ```json
568
+ {
569
+ "id": "03f5vaeq985jlvwv3arl4srq2",
570
+ "number": 1,
571
+ "title": "First!",
572
+ "status": "published",
573
+ "description": "Hello, World!",
574
+ "description_html": "<div class=\"action-text-content\"><p>Hello, World!</p></div>",
575
+ "image_url": null,
576
+ "tags": ["programming"],
577
+ "closed": false,
578
+ "golden": false,
579
+ "last_active_at": "2025-12-05T19:38:48.553Z",
580
+ "created_at": "2025-12-05T19:38:48.540Z",
581
+ "url": "http://fizzy.localhost:3006/897362094/cards/4",
582
+ "board": {
583
+ "id": "03f5v9zkft4hj9qq0lsn9ohcm",
584
+ "name": "Fizzy",
585
+ "all_access": true,
586
+ "created_at": "2025-12-05T19:36:35.534Z",
587
+ "url": "http://fizzy.localhost:3006/897362094/boards/03f5v9zkft4hj9qq0lsn9ohcm",
588
+ "creator": {
589
+ "id": "03f5v9zjw7pz8717a4no1h8a7",
590
+ "name": "David Heinemeier Hansson",
591
+ "role": "owner",
592
+ "active": true,
593
+ "email_address": "david@example.com",
594
+ "created_at": "2025-12-05T19:36:35.401Z",
595
+ "url": "http://fizzy.localhost:3006/897362094/users/03f5v9zjw7pz8717a4no1h8a7"
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
+ },
607
+ "creator": {
608
+ "id": "03f5v9zjw7pz8717a4no1h8a7",
609
+ "name": "David Heinemeier Hansson",
610
+ "role": "owner",
611
+ "active": true,
612
+ "email_address": "david@example.com",
613
+ "created_at": "2025-12-05T19:36:35.401Z",
614
+ "url": "http://fizzy.localhost:3006/897362094/users/03f5v9zjw7pz8717a4no1h8a7"
615
+ },
616
+ "comments_url": "http://fizzy.localhost:3006/897362094/cards/4/comments",
617
+ "steps": [
618
+ {
619
+ "id": "03f8huu0sog76g3s975963b5e",
620
+ "content": "This is the first step",
621
+ "completed": false
622
+ },
623
+ {
624
+ "id": "03f8huu0sog76g3s975969734",
625
+ "content": "This is the second step",
626
+ "completed": false
627
+ }
628
+ ]
629
+ }
630
+ ```
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.
486
633
 
487
634
  ### `POST /:account_slug/boards/:board_id/cards`
488
635
 
@@ -548,6 +695,14 @@ __Response:__
548
695
 
549
696
  Returns `204 No Content` on success.
550
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
+
551
706
  ### `POST /:account_slug/cards/:card_number/closure`
552
707
 
553
708
  Closes a card.
@@ -632,6 +787,22 @@ __Response:__
632
787
 
633
788
  Returns `204 No Content` on success.
634
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
+
635
806
  ## Comments
636
807
 
637
808
  Comments are attached to cards and support rich text.
@@ -661,6 +832,10 @@ __Response:__
661
832
  "created_at": "2025-12-05T19:36:35.401Z",
662
833
  "url": "http://fizzy.localhost:3006/897362094/users/03f5v9zjw7pz8717a4no1h8a7"
663
834
  },
835
+ "card": {
836
+ "id": "03f5v9zo9qlcwwpyc0ascnikz",
837
+ "url": "http://fizzy.localhost:3006/897362094/cards/03f5v9zo9qlcwwpyc0ascnikz"
838
+ },
664
839
  "reactions_url": "http://fizzy.localhost:3006/897362094/cards/3/comments/03f5v9zo9qlcwwpyc0ascnikz/reactions",
665
840
  "url": "http://fizzy.localhost:3006/897362094/cards/3/comments/03f5v9zo9qlcwwpyc0ascnikz"
666
841
  }
@@ -691,6 +866,10 @@ __Response:__
691
866
  "created_at": "2025-12-05T19:36:35.401Z",
692
867
  "url": "http://fizzy.localhost:3006/897362094/users/03f5v9zjw7pz8717a4no1h8a7"
693
868
  },
869
+ "card": {
870
+ "id": "03f5v9zo9qlcwwpyc0ascnikz",
871
+ "url": "http://fizzy.localhost:3006/897362094/cards/03f5v9zo9qlcwwpyc0ascnikz"
872
+ },
694
873
  "reactions_url": "http://fizzy.localhost:3006/897362094/cards/3/comments/03f5v9zo9qlcwwpyc0ascnikz/reactions",
695
874
  "url": "http://fizzy.localhost:3006/897362094/cards/3/comments/03f5v9zo9qlcwwpyc0ascnikz"
696
875
  }
package/go.mod CHANGED
@@ -4,13 +4,13 @@ go 1.25.1
4
4
 
5
5
  require (
6
6
  github.com/charmbracelet/bubbletea v1.3.10
7
+ github.com/charmbracelet/lipgloss v1.1.0
7
8
  github.com/spf13/cobra v1.10.2
8
9
  )
9
10
 
10
11
  require (
11
12
  github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
12
13
  github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
13
- github.com/charmbracelet/lipgloss v1.1.0 // indirect
14
14
  github.com/charmbracelet/x/ansi v0.10.1 // indirect
15
15
  github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
16
16
  github.com/charmbracelet/x/term v0.2.1 // indirect