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/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
|
+
}
|
package/cmd/card_tag.go
ADDED
|
@@ -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
|
+
}
|