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 CHANGED
@@ -1,5 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.0 - 2026-01-19
4
+
5
+ ### Features
6
+
7
+ #### Card Management
8
+
9
+ - `fizzy card not-now <card_number>` - Move a card to "Not Now" status
10
+ - `fizzy card untriage <card_number>` - Send a card back to triage
11
+ - `fizzy card watch <card_number>` - Subscribe to card notifications
12
+ - `fizzy card unwatch <card_number>` - Unsubscribe from card notifications
13
+ - `fizzy card golden <card_number>` - Mark a card as golden
14
+ - `fizzy card ungolden <card_number>` - Remove golden status from a card
15
+
3
16
  ## 0.3.0 - 2026-01-11
4
17
 
5
18
  ### Features
@@ -0,0 +1,338 @@
1
+ # Fizzy CLI Implementation Analysis & Plan
2
+
3
+ ## Current Implementation Status
4
+
5
+ ### ✅ Implemented Commands
6
+
7
+ #### Authentication & Setup
8
+ - `fizzy login` - Authenticate with Fizzy API
9
+ - `fizzy use --account <slug>` - Select account
10
+ - `fizzy use --board <name>` - Select board
11
+
12
+ #### Accounts
13
+ - `fizzy account list` - List available accounts
14
+
15
+ #### Boards
16
+ - `fizzy board list` - GET `/boards`
17
+ - `fizzy board show <id>` - GET `/boards/:id`
18
+ - `fizzy board create <name>` - POST `/boards`
19
+
20
+ #### Cards
21
+ - `fizzy card list` - GET `/cards`
22
+ - `fizzy card show <number>` - GET `/cards/:number`
23
+ - `fizzy card create <title>` - POST `/boards/:id/cards`
24
+ - `fizzy card update <number> <title>` - PUT `/cards/:number`
25
+ - `fizzy card delete <number>` - DELETE `/cards/:number`
26
+ - `fizzy card close <number>` - POST `/cards/:number/closure`
27
+ - `fizzy card reopen <number>` - DELETE `/cards/:number/closure`
28
+ - `fizzy card tag <number> <tag>` - POST `/cards/:number/taggings`
29
+ - `fizzy card assign <number> <user_id>` - POST `/cards/:number/assignments`
30
+ - `fizzy card triage <number> <column_id>` - POST `/cards/:number/triage`
31
+
32
+ #### Columns
33
+ - `fizzy column list` - GET `/boards/:id/columns`
34
+ - `fizzy column create <name>` - POST `/boards/:id/columns`
35
+
36
+ #### Tags
37
+ - `fizzy tag list` - GET `/tags`
38
+
39
+ #### Notifications
40
+ - `fizzy notification list` - GET `/notifications`
41
+ - `fizzy notification read <id>` - POST `/notifications/:id/reading`
42
+ - `fizzy notification unread <id>` - DELETE `/notifications/:id/reading`
43
+ - `fizzy notification read-all` - POST `/notifications/bulk_reading`
44
+
45
+ ---
46
+
47
+ ## 📋 Missing API Endpoints (Not Yet Implemented)
48
+
49
+ ### High Priority (Core Functionality)
50
+
51
+ #### Cards - Additional Operations
52
+ - [x] `POST /cards/:number/not_now` - Move card to "Not Now" status
53
+ - [x] `DELETE /cards/:number/triage` - Send card back to triage
54
+ - [x] `POST /cards/:number/watch` - Subscribe to card notifications
55
+ - [x] `DELETE /cards/:number/watch` - Unsubscribe from card notifications
56
+ - [x] `POST /cards/:number/goldness` - Mark card as golden
57
+ - [x] `DELETE /cards/:number/goldness` - Remove golden status
58
+
59
+ #### Comments
60
+ - [ ] `GET /cards/:number/comments` - List card comments
61
+ - [ ] `GET /cards/:number/comments/:id` - Get specific comment
62
+ - [ ] `POST /cards/:number/comments` - Create comment
63
+ - [ ] `PUT /cards/:number/comments/:id` - Update comment
64
+ - [ ] `DELETE /cards/:number/comments/:id` - Delete comment
65
+
66
+ #### Reactions
67
+ - [ ] `GET /cards/:number/comments/:id/reactions` - List reactions on comment
68
+ - [ ] `POST /cards/:number/comments/:id/reactions` - Add reaction
69
+ - [ ] `DELETE /cards/:number/comments/:id/reactions/:id` - Remove reaction
70
+
71
+ #### Steps (To-do items)
72
+ - [ ] `GET /cards/:number/steps/:id` - Get step details
73
+ - [ ] `POST /cards/:number/steps` - Create step
74
+ - [ ] `PUT /cards/:number/steps/:id` - Update step
75
+ - [ ] `DELETE /cards/:number/steps/:id` - Delete step
76
+
77
+ #### Board Management
78
+ - [ ] `PUT /boards/:id` - Update board settings
79
+ - [ ] `DELETE /boards/:id` - Delete board
80
+ - [ ] Support for board parameters: `all_access`, `auto_postpone_period`, `public_description`, `user_ids`
81
+
82
+ #### Columns
83
+ - [ ] `GET /boards/:id/columns/:id` - Get specific column
84
+ - [ ] `PUT /boards/:id/columns/:id` - Update column
85
+ - [ ] `DELETE /boards/:id/columns/:id` - Delete column
86
+
87
+ #### Users
88
+ - [ ] `GET /users` - List account users
89
+ - [ ] `GET /users/:id` - Get user details
90
+ - [ ] `PUT /users/:id` - Update user
91
+ - [ ] `DELETE /users/:id` - Deactivate user
92
+
93
+ #### Identity
94
+ - [ ] `GET /my/identity` - Get current user's accounts and identity (may already be partially implemented)
95
+
96
+ ### Medium Priority (Enhancement Features)
97
+
98
+ #### Card Filtering & Search
99
+ - Implement all card filter parameters:
100
+ - `board_ids[]` (partial - implemented)
101
+ - `tag_ids[]`
102
+ - `assignee_ids[]`
103
+ - `creator_ids[]`
104
+ - `closer_ids[]`
105
+ - `card_ids[]`
106
+ - `indexed_by` (all, closed, not_now, stalled, postponing_soon, golden)
107
+ - `sorted_by` (latest, newest, oldest)
108
+ - `assignment_status` (unassigned)
109
+ - `creation` (date filters)
110
+ - `closure` (date filters)
111
+ - `terms[]` (search)
112
+
113
+ #### Advanced Features
114
+ - [ ] File uploads for card images
115
+ - [ ] Rich text field support with HTML validation
116
+ - [ ] ActionText direct upload for embedded files
117
+ - [ ] ETag/Cache-Control header support for bandwidth optimization
118
+ - [ ] Pagination support (Link header rel="next")
119
+
120
+ ---
121
+
122
+ ## 🎯 Implementation Priority Roadmap
123
+
124
+ ### Phase 1: Core Card Enhancements (Quick wins)
125
+ These are card-related operations with minimal API client work since the pattern is established.
126
+
127
+ 1. ✅ **card not-now** - Move card to "Not Now" status
128
+ 2. ✅ **card untriage** - Send card back to triage
129
+ 3. ✅ **card watch** / **card unwatch** - Notification subscriptions
130
+ 4. ✅ **card golden** / **card ungolden** - Mark as golden
131
+
132
+ ### Phase 2: Comments & Reactions (Foundational)
133
+ Users often need to collaborate through comments.
134
+
135
+ 1. **comment list** - `GET /cards/:number/comments`
136
+ 2. **comment show** - `GET /cards/:number/comments/:id`
137
+ 3. **comment add** - `POST /cards/:number/comments`
138
+ 4. **comment update** - `PUT /cards/:number/comments/:id`
139
+ 5. **comment delete** - `DELETE /cards/:number/comments/:id`
140
+ 6. **reaction add** - `POST /cards/:number/comments/:id/reactions`
141
+ 7. **reaction list** - `GET /cards/:number/comments/:id/reactions`
142
+ 8. **reaction remove** - `DELETE /cards/:number/comments/:id/reactions/:id`
143
+
144
+ ### Phase 3: Steps (To-do items)
145
+ Essential for breaking down work.
146
+
147
+ 1. **step add** - `POST /cards/:number/steps`
148
+ 2. **step list** - Via card show (embedded)
149
+ 3. **step update** - `PUT /cards/:number/steps/:id`
150
+ 4. **step remove** - `DELETE /cards/:number/steps/:id`
151
+
152
+ ### Phase 4: Board & Column Management
153
+ Administrative operations.
154
+
155
+ 1. **board update** - `PUT /boards/:id`
156
+ 2. **board delete** - `DELETE /boards/:id`
157
+ 3. **column show** - `GET /boards/:id/columns/:id`
158
+ 4. **column update** - `PUT /boards/:id/columns/:id`
159
+ 5. **column delete** - `DELETE /boards/:id/columns/:id`
160
+
161
+ ### Phase 5: Users & Advanced Features
162
+ Less common but important for team management.
163
+
164
+ 1. **user list** - `GET /users`
165
+ 2. **user show** - `GET /users/:id`
166
+ 3. **user update** - `PUT /users/:id`
167
+ 4. **user deactivate** - `DELETE /users/:id`
168
+ 5. Card filtering enhancements
169
+ 6. File uploads & rich text
170
+ 7. Pagination support
171
+ 8. ETag caching
172
+
173
+ ---
174
+
175
+ ## 📊 Command Structure Pattern
176
+
177
+ All commands follow this pattern:
178
+
179
+ ```go
180
+ // File: cmd/resource_action.go
181
+
182
+ var resourceActionCmd = &cobra.Command{
183
+ Use: "action <args>",
184
+ Short: "Brief description",
185
+ Long: `Longer description`,
186
+ Args: cobra.ExactArgs(N), // or other validators
187
+ Run: func(cmd *cobra.Command, args []string) {
188
+ if err := handleResourceAction(cmd, args...); err != nil {
189
+ fmt.Fprintf(cmd.OutOrStderr(), "Error: %v\n", err)
190
+ }
191
+ },
192
+ }
193
+
194
+ func handleResourceAction(cmd *cobra.Command, args ...string) error {
195
+ a := app.FromContext(cmd.Context())
196
+ if a == nil || a.Client == nil {
197
+ return fmt.Errorf("API client not available")
198
+ }
199
+
200
+ // API call
201
+ result, err := a.Client.MethodName(context.Background(), args...)
202
+ if err != nil {
203
+ return fmt.Errorf("operation failed: %w", err)
204
+ }
205
+
206
+ // Display result
207
+ return ui.DisplayResource(result)
208
+ }
209
+
210
+ func init() {
211
+ parentCmd.AddCommand(resourceActionCmd)
212
+ // Add flags if needed
213
+ }
214
+ ```
215
+
216
+ ---
217
+
218
+ ## 📝 API Client Work Required
219
+
220
+ ### New Methods Needed in `internal/api/client.go`
221
+
222
+ **Card Operations:**
223
+ ```go
224
+ func (c *Client) PostCardNotNow(ctx context.Context, cardNumber int) (bool, error)
225
+ func (c *Client) DeleteCardTriage(ctx context.Context, cardNumber int) (bool, error)
226
+ func (c *Client) PostCardWatch(ctx context.Context, cardNumber int) (bool, error)
227
+ func (c *Client) DeleteCardWatch(ctx context.Context, cardNumber int) (bool, error)
228
+ func (c *Client) PostCardGoldenness(ctx context.Context, cardNumber int) (bool, error)
229
+ func (c *Client) DeleteCardGoldenness(ctx context.Context, cardNumber int) (bool, error)
230
+ ```
231
+
232
+ **Comment Operations:**
233
+ ```go
234
+ func (c *Client) GetCardComments(ctx context.Context, cardNumber int) ([]Comment, error)
235
+ func (c *Client) GetCardComment(ctx context.Context, cardNumber int, commentID string) (*Comment, error)
236
+ func (c *Client) PostCardComment(ctx context.Context, cardNumber int, body string) (bool, error)
237
+ func (c *Client) PutCardComment(ctx context.Context, cardNumber int, commentID string, body string) (bool, error)
238
+ func (c *Client) DeleteCardComment(ctx context.Context, cardNumber int, commentID string) (bool, error)
239
+ ```
240
+
241
+ **Reaction Operations:**
242
+ ```go
243
+ func (c *Client) GetCommentReactions(ctx context.Context, cardNumber int, commentID string) ([]Reaction, error)
244
+ func (c *Client) PostCommentReaction(ctx context.Context, cardNumber int, commentID string, content string) (bool, error)
245
+ func (c *Client) DeleteCommentReaction(ctx context.Context, cardNumber int, commentID string, reactionID string) (bool, error)
246
+ ```
247
+
248
+ **Step Operations:**
249
+ ```go
250
+ func (c *Client) PostCardStep(ctx context.Context, cardNumber int, content string) (bool, error)
251
+ func (c *Client) GetCardStep(ctx context.Context, cardNumber int, stepID string) (*Step, error)
252
+ func (c *Client) PutCardStep(ctx context.Context, cardNumber int, stepID string, payload UpdateStepPayload) (bool, error)
253
+ func (c *Client) DeleteCardStep(ctx context.Context, cardNumber int, stepID string) (bool, error)
254
+ ```
255
+
256
+ **Board Operations:**
257
+ ```go
258
+ func (c *Client) PutBoard(ctx context.Context, boardID string, payload UpdateBoardPayload) (bool, error)
259
+ func (c *Client) DeleteBoard(ctx context.Context, boardID string) (bool, error)
260
+ ```
261
+
262
+ **Column Operations:**
263
+ ```go
264
+ func (c *Client) GetColumn(ctx context.Context, columnID string) (*Column, error)
265
+ func (c *Client) PutColumn(ctx context.Context, columnID string, payload UpdateColumnPayload) (bool, error)
266
+ func (c *Client) DeleteColumn(ctx context.Context, columnID string) (bool, error)
267
+ ```
268
+
269
+ **User Operations:**
270
+ ```go
271
+ func (c *Client) GetUsers(ctx context.Context) ([]User, error)
272
+ func (c *Client) GetUser(ctx context.Context, userID string) (*User, error)
273
+ func (c *Client) PutUser(ctx context.Context, userID string, payload UpdateUserPayload) (bool, error)
274
+ func (c *Client) DeleteUser(ctx context.Context, userID string) (bool, error)
275
+ ```
276
+
277
+ ### New Data Types Needed
278
+
279
+ ```go
280
+ type Comment struct {
281
+ ID string
282
+ CreatedAt string
283
+ UpdatedAt string
284
+ Body map[string]string // {plain_text, html}
285
+ Creator User
286
+ Card CardReference
287
+ ReactionsURL string
288
+ URL string
289
+ }
290
+
291
+ type Reaction struct {
292
+ ID string
293
+ Content string
294
+ Reacter User
295
+ URL string
296
+ }
297
+
298
+ type Step struct {
299
+ ID string
300
+ Content string
301
+ Completed bool
302
+ }
303
+
304
+ type UpdateStepPayload struct {
305
+ Content string `json:"content,omitempty"`
306
+ Completed bool `json:"completed,omitempty"`
307
+ }
308
+
309
+ type UpdateBoardPayload struct {
310
+ Name string `json:"name,omitempty"`
311
+ AllAccess bool `json:"all_access,omitempty"`
312
+ AutoPostponePeriod int `json:"auto_postpone_period,omitempty"`
313
+ PublicDescription string `json:"public_description,omitempty"`
314
+ UserIDs []string `json:"user_ids,omitempty"`
315
+ }
316
+
317
+ type UpdateColumnPayload struct {
318
+ Name string `json:"name,omitempty"`
319
+ Color *Color `json:"color,omitempty"`
320
+ }
321
+
322
+ type UpdateUserPayload struct {
323
+ Name string `json:"name,omitempty"`
324
+ Avatar string `json:"avatar,omitempty"` // file upload
325
+ }
326
+ ```
327
+
328
+ ---
329
+
330
+ ## 🔍 Next Steps Recommendation
331
+
332
+ **Start with Phase 1 (Card Enhancements)** as they:
333
+ - Have proven command patterns already in codebase
334
+ - Require minimal new API client code
335
+ - Deliver immediate user value
336
+ - Build confidence before tackling complex features like comments
337
+
338
+ **Suggested first command:** `card not-now` - single simple API call, no new data types needed.
@@ -0,0 +1,46 @@
1
+ package cmd
2
+
3
+ import (
4
+ "context"
5
+ "fmt"
6
+ "strconv"
7
+
8
+ "github.com/rogeriopvl/fizzy/internal/app"
9
+ "github.com/spf13/cobra"
10
+ )
11
+
12
+ var cardGoldenCmd = &cobra.Command{
13
+ Use: "golden <card_number>",
14
+ Short: "Mark a card as golden",
15
+ Long: `Mark an existing card as golden`,
16
+ Args: cobra.ExactArgs(1),
17
+ Run: func(cmd *cobra.Command, args []string) {
18
+ if err := handleGoldenCard(cmd, args[0]); err != nil {
19
+ fmt.Fprintf(cmd.OutOrStderr(), "Error: %v\n", err)
20
+ }
21
+ },
22
+ }
23
+
24
+ func handleGoldenCard(cmd *cobra.Command, cardNumber string) error {
25
+ cardNum, err := strconv.Atoi(cardNumber)
26
+ if err != nil {
27
+ return fmt.Errorf("invalid card number: %w", err)
28
+ }
29
+
30
+ a := app.FromContext(cmd.Context())
31
+ if a == nil || a.Client == nil {
32
+ return fmt.Errorf("API client not available")
33
+ }
34
+
35
+ _, err = a.Client.PostCardGoldenness(context.Background(), cardNum)
36
+ if err != nil {
37
+ return fmt.Errorf("marking card as golden: %w", err)
38
+ }
39
+
40
+ fmt.Printf("✓ Card #%d marked as golden\n", cardNum)
41
+ return nil
42
+ }
43
+
44
+ func init() {
45
+ cardCmd.AddCommand(cardGoldenCmd)
46
+ }
@@ -0,0 +1,92 @@
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 TestCardGoldenCommandSuccess(t *testing.T) {
14
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
15
+ if r.URL.Path != "/cards/123/goldness" {
16
+ t.Errorf("expected /cards/123/goldness, got %s", r.URL.Path)
17
+ }
18
+ if r.Method != http.MethodPost {
19
+ t.Errorf("expected POST, 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 := cardGoldenCmd
35
+ cmd.SetContext(testApp.ToContext(context.Background()))
36
+
37
+ if err := handleGoldenCard(cmd, "123"); err != nil {
38
+ t.Fatalf("handleGoldenCard failed: %v", err)
39
+ }
40
+ }
41
+
42
+ func TestCardGoldenCommandInvalidCardNumber(t *testing.T) {
43
+ testApp := &app.App{}
44
+
45
+ cmd := cardGoldenCmd
46
+ cmd.SetContext(testApp.ToContext(context.Background()))
47
+
48
+ err := handleGoldenCard(cmd, "not-a-number")
49
+ if err == nil {
50
+ t.Errorf("expected error for invalid card number")
51
+ }
52
+ if err.Error() != "invalid card number: strconv.Atoi: parsing \"not-a-number\": invalid syntax" {
53
+ t.Errorf("expected invalid card number error, got %v", err)
54
+ }
55
+ }
56
+
57
+ func TestCardGoldenCommandNoClient(t *testing.T) {
58
+ testApp := &app.App{}
59
+
60
+ cmd := cardGoldenCmd
61
+ cmd.SetContext(testApp.ToContext(context.Background()))
62
+
63
+ err := handleGoldenCard(cmd, "123")
64
+ if err == nil {
65
+ t.Errorf("expected error when client not available")
66
+ }
67
+ if err.Error() != "API client not available" {
68
+ t.Errorf("expected 'client not available' error, got %v", err)
69
+ }
70
+ }
71
+
72
+ func TestCardGoldenCommandAPIError(t *testing.T) {
73
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
74
+ w.WriteHeader(http.StatusInternalServerError)
75
+ w.Write([]byte("Internal Server Error"))
76
+ }))
77
+ defer server.Close()
78
+
79
+ client := testutil.NewTestClient(server.URL, "", "", "test-token")
80
+ testApp := &app.App{Client: client}
81
+
82
+ cmd := cardGoldenCmd
83
+ cmd.SetContext(testApp.ToContext(context.Background()))
84
+
85
+ err := handleGoldenCard(cmd, "123")
86
+ if err == nil {
87
+ t.Errorf("expected error for API failure")
88
+ }
89
+ if err.Error() != "marking card as golden: unexpected status code 500: Internal Server Error" {
90
+ t.Errorf("expected API error, got %v", err)
91
+ }
92
+ }
@@ -0,0 +1,46 @@
1
+ package cmd
2
+
3
+ import (
4
+ "context"
5
+ "fmt"
6
+ "strconv"
7
+
8
+ "github.com/rogeriopvl/fizzy/internal/app"
9
+ "github.com/spf13/cobra"
10
+ )
11
+
12
+ var cardNotNowCmd = &cobra.Command{
13
+ Use: "not-now <card_number>",
14
+ Short: "Move a card to Not Now status",
15
+ Long: `Move an existing card to the "Not Now" status`,
16
+ Args: cobra.ExactArgs(1),
17
+ Run: func(cmd *cobra.Command, args []string) {
18
+ if err := handleNotNowCard(cmd, args[0]); err != nil {
19
+ fmt.Fprintf(cmd.OutOrStderr(), "Error: %v\n", err)
20
+ }
21
+ },
22
+ }
23
+
24
+ func handleNotNowCard(cmd *cobra.Command, cardNumber string) error {
25
+ cardNum, err := strconv.Atoi(cardNumber)
26
+ if err != nil {
27
+ return fmt.Errorf("invalid card number: %w", err)
28
+ }
29
+
30
+ a := app.FromContext(cmd.Context())
31
+ if a == nil || a.Client == nil {
32
+ return fmt.Errorf("API client not available")
33
+ }
34
+
35
+ _, err = a.Client.PostCardNotNow(context.Background(), cardNum)
36
+ if err != nil {
37
+ return fmt.Errorf("moving card to not now: %w", err)
38
+ }
39
+
40
+ fmt.Printf("✓ Card #%d moved to Not Now successfully\n", cardNum)
41
+ return nil
42
+ }
43
+
44
+ func init() {
45
+ cardCmd.AddCommand(cardNotNowCmd)
46
+ }
@@ -0,0 +1,92 @@
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 TestCardNotNowCommandSuccess(t *testing.T) {
14
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
15
+ if r.URL.Path != "/cards/123/not_now" {
16
+ t.Errorf("expected /cards/123/not_now, got %s", r.URL.Path)
17
+ }
18
+ if r.Method != http.MethodPost {
19
+ t.Errorf("expected POST, 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 := cardNotNowCmd
35
+ cmd.SetContext(testApp.ToContext(context.Background()))
36
+
37
+ if err := handleNotNowCard(cmd, "123"); err != nil {
38
+ t.Fatalf("handleNotNowCard failed: %v", err)
39
+ }
40
+ }
41
+
42
+ func TestCardNotNowCommandInvalidCardNumber(t *testing.T) {
43
+ testApp := &app.App{}
44
+
45
+ cmd := cardNotNowCmd
46
+ cmd.SetContext(testApp.ToContext(context.Background()))
47
+
48
+ err := handleNotNowCard(cmd, "not-a-number")
49
+ if err == nil {
50
+ t.Errorf("expected error for invalid card number")
51
+ }
52
+ if err.Error() != "invalid card number: strconv.Atoi: parsing \"not-a-number\": invalid syntax" {
53
+ t.Errorf("expected invalid card number error, got %v", err)
54
+ }
55
+ }
56
+
57
+ func TestCardNotNowCommandNoClient(t *testing.T) {
58
+ testApp := &app.App{}
59
+
60
+ cmd := cardNotNowCmd
61
+ cmd.SetContext(testApp.ToContext(context.Background()))
62
+
63
+ err := handleNotNowCard(cmd, "123")
64
+ if err == nil {
65
+ t.Errorf("expected error when client not available")
66
+ }
67
+ if err.Error() != "API client not available" {
68
+ t.Errorf("expected 'client not available' error, got %v", err)
69
+ }
70
+ }
71
+
72
+ func TestCardNotNowCommandAPIError(t *testing.T) {
73
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
74
+ w.WriteHeader(http.StatusInternalServerError)
75
+ w.Write([]byte("Internal Server Error"))
76
+ }))
77
+ defer server.Close()
78
+
79
+ client := testutil.NewTestClient(server.URL, "", "", "test-token")
80
+ testApp := &app.App{Client: client}
81
+
82
+ cmd := cardNotNowCmd
83
+ cmd.SetContext(testApp.ToContext(context.Background()))
84
+
85
+ err := handleNotNowCard(cmd, "123")
86
+ if err == nil {
87
+ t.Errorf("expected error for API failure")
88
+ }
89
+ if err.Error() != "moving card to not now: unexpected status code 500: Internal Server Error" {
90
+ t.Errorf("expected API error, got %v", err)
91
+ }
92
+ }
@@ -0,0 +1,51 @@
1
+ package cmd
2
+
3
+ import (
4
+ "context"
5
+ "fmt"
6
+ "strconv"
7
+ "strings"
8
+
9
+ "github.com/rogeriopvl/fizzy/internal/app"
10
+ "github.com/spf13/cobra"
11
+ )
12
+
13
+ var cardTagCmd = &cobra.Command{
14
+ Use: "tag <card_number> <tag_title>",
15
+ Short: "Toggle a tag on or off for a card",
16
+ Long: `Toggle a tag on or off for a card. If the tag doesn't exist, it will be created.
17
+
18
+ The tag title can be specified with or without a leading # symbol.`,
19
+ Args: cobra.ExactArgs(2),
20
+ Run: func(cmd *cobra.Command, args []string) {
21
+ if err := handleTagCard(cmd, args[0], args[1]); err != nil {
22
+ fmt.Fprintf(cmd.OutOrStderr(), "Error: %v\n", err)
23
+ }
24
+ },
25
+ }
26
+
27
+ func handleTagCard(cmd *cobra.Command, cardNumber, tagTitle string) error {
28
+ cardNum, err := strconv.Atoi(cardNumber)
29
+ if err != nil {
30
+ return fmt.Errorf("invalid card number: %w", err)
31
+ }
32
+
33
+ a := app.FromContext(cmd.Context())
34
+ if a == nil || a.Client == nil {
35
+ return fmt.Errorf("API client not available")
36
+ }
37
+
38
+ tagTitle = strings.TrimPrefix(tagTitle, "#")
39
+
40
+ _, err = a.Client.PostCardTagging(context.Background(), cardNum, tagTitle)
41
+ if err != nil {
42
+ return fmt.Errorf("toggling tag on card: %w", err)
43
+ }
44
+
45
+ fmt.Printf("✓ Tag '%s' toggled on card #%d\n", tagTitle, cardNum)
46
+ return nil
47
+ }
48
+
49
+ func init() {
50
+ cardCmd.AddCommand(cardTagCmd)
51
+ }