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.
- package/CHANGELOG.md +29 -0
- package/IMPLEMENTATION_PLAN.md +338 -0
- package/bin/fizzy +0 -0
- package/cmd/board.go +1 -1
- package/cmd/card_assign.go +55 -0
- package/cmd/card_assign_test.go +130 -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_triage.go +46 -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_update.go +0 -1
- package/cmd/card_update_test.go +0 -2
- package/cmd/card_watch.go +46 -0
- package/cmd/card_watch_test.go +92 -0
- package/cmd/login.go +2 -1
- package/cmd/notification.go +14 -0
- package/cmd/notification_list.go +69 -0
- package/cmd/notification_list_test.go +288 -0
- package/cmd/notification_read.go +51 -0
- package/cmd/notification_read_all.go +38 -0
- package/cmd/notification_read_all_test.go +75 -0
- package/cmd/notification_read_test.go +138 -0
- package/cmd/notification_unread.go +44 -0
- package/cmd/notification_unread_test.go +99 -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 +180 -1
- package/go.mod +1 -1
- package/internal/api/client.go +275 -0
- package/internal/config/config.go +1 -0
- package/internal/ui/notification_list.go +27 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,34 @@
|
|
|
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
|
+
|
|
16
|
+
## 0.3.0 - 2026-01-11
|
|
17
|
+
|
|
18
|
+
### Features
|
|
19
|
+
|
|
20
|
+
#### Notification Management
|
|
21
|
+
|
|
22
|
+
- `fizzy notification list` - List all notifications with optional filtering
|
|
23
|
+
- `fizzy notification read <notification_id>` - Mark a notification as read and display it
|
|
24
|
+
- `fizzy notification unread <notification_id>` - Mark a notification as unread
|
|
25
|
+
- `fizzy notification read-all` - Mark all unread notifications as read
|
|
26
|
+
|
|
27
|
+
#### Card Management
|
|
28
|
+
|
|
29
|
+
- `fizzy card assign <card_number> <user_id>` - Assign or unassign a user to/from a card
|
|
30
|
+
- `fizzy card triage <card_number> <column_id>` - Move a card from triage into a column
|
|
31
|
+
|
|
3
32
|
## [0.2.1] - 2025-12-20
|
|
4
33
|
|
|
5
34
|
### Fixes
|
|
@@ -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.
|
package/bin/fizzy
CHANGED
|
Binary file
|
package/cmd/board.go
CHANGED
|
@@ -13,7 +13,7 @@ import (
|
|
|
13
13
|
var boardCmd = &cobra.Command{
|
|
14
14
|
Use: "board",
|
|
15
15
|
Short: "Show the currently selected board",
|
|
16
|
-
Long:
|
|
16
|
+
Long: `Display the name and ID of the currently selected board.
|
|
17
17
|
|
|
18
18
|
Use subcommands to list, create, or manage boards:
|
|
19
19
|
fizzy board list List all boards
|
|
@@ -0,0 +1,55 @@
|
|
|
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 cardAssignCmd = &cobra.Command{
|
|
13
|
+
Use: "assign <card_number> <user_id>",
|
|
14
|
+
Short: "Assign a user to a card",
|
|
15
|
+
Long: `Assign or unassign a user to/from a card.
|
|
16
|
+
|
|
17
|
+
Use "me" as the user_id to assign the card to yourself.`,
|
|
18
|
+
Args: cobra.ExactArgs(2),
|
|
19
|
+
Run: func(cmd *cobra.Command, args []string) {
|
|
20
|
+
if err := handleAssignCard(cmd, args[0], args[1]); err != nil {
|
|
21
|
+
fmt.Fprintf(cmd.OutOrStderr(), "Error: %v\n", err)
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
func handleAssignCard(cmd *cobra.Command, cardNumber, userID string) error {
|
|
27
|
+
cardNum, err := strconv.Atoi(cardNumber)
|
|
28
|
+
if err != nil {
|
|
29
|
+
return fmt.Errorf("invalid card number: %w", err)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
a := app.FromContext(cmd.Context())
|
|
33
|
+
if a == nil || a.Client == nil {
|
|
34
|
+
return fmt.Errorf("API client not available")
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if userID == "me" {
|
|
38
|
+
if a.Config.CurrentUserID == "" {
|
|
39
|
+
return fmt.Errorf("current user ID not available, please run 'fizzy login' first")
|
|
40
|
+
}
|
|
41
|
+
userID = a.Config.CurrentUserID
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
_, err = a.Client.PostCardAssignments(context.Background(), cardNum, userID)
|
|
45
|
+
if err != nil {
|
|
46
|
+
return fmt.Errorf("assigning card: %w", err)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
fmt.Printf("✓ Card #%d assignment toggled for user %s\n", cardNum, userID)
|
|
50
|
+
return nil
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
func init() {
|
|
54
|
+
cardCmd.AddCommand(cardAssignCmd)
|
|
55
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
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/config"
|
|
11
|
+
"github.com/rogeriopvl/fizzy/internal/testutil"
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
func TestCardAssignCommandSuccess(t *testing.T) {
|
|
15
|
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
16
|
+
if r.URL.Path != "/cards/123/assignments" {
|
|
17
|
+
t.Errorf("expected /cards/123/assignments, got %s", r.URL.Path)
|
|
18
|
+
}
|
|
19
|
+
if r.Method != http.MethodPost {
|
|
20
|
+
t.Errorf("expected POST, got %s", r.Method)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
auth := r.Header.Get("Authorization")
|
|
24
|
+
if auth != "Bearer test-token" {
|
|
25
|
+
t.Errorf("expected Bearer test-token, got %s", auth)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
w.WriteHeader(http.StatusNoContent)
|
|
29
|
+
}))
|
|
30
|
+
defer server.Close()
|
|
31
|
+
|
|
32
|
+
client := testutil.NewTestClient(server.URL, "", "", "test-token")
|
|
33
|
+
testApp := &app.App{Client: client}
|
|
34
|
+
|
|
35
|
+
cmd := cardAssignCmd
|
|
36
|
+
cmd.SetContext(testApp.ToContext(context.Background()))
|
|
37
|
+
|
|
38
|
+
if err := handleAssignCard(cmd, "123", "user-id-123"); err != nil {
|
|
39
|
+
t.Fatalf("handleAssignCard failed: %v", err)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
func TestCardAssignCommandWithMe(t *testing.T) {
|
|
44
|
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
45
|
+
if r.URL.Path != "/cards/123/assignments" {
|
|
46
|
+
t.Errorf("expected /cards/123/assignments, got %s", r.URL.Path)
|
|
47
|
+
}
|
|
48
|
+
w.WriteHeader(http.StatusNoContent)
|
|
49
|
+
}))
|
|
50
|
+
defer server.Close()
|
|
51
|
+
|
|
52
|
+
client := testutil.NewTestClient(server.URL, "", "", "test-token")
|
|
53
|
+
cfg := &config.Config{CurrentUserID: "my-user-id"}
|
|
54
|
+
testApp := &app.App{Client: client, Config: cfg}
|
|
55
|
+
|
|
56
|
+
cmd := cardAssignCmd
|
|
57
|
+
cmd.SetContext(testApp.ToContext(context.Background()))
|
|
58
|
+
|
|
59
|
+
if err := handleAssignCard(cmd, "123", "me"); err != nil {
|
|
60
|
+
t.Fatalf("handleAssignCard with 'me' failed: %v", err)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
func TestCardAssignCommandAPIError(t *testing.T) {
|
|
65
|
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
66
|
+
w.WriteHeader(http.StatusNotFound)
|
|
67
|
+
w.Write([]byte("Card not found"))
|
|
68
|
+
}))
|
|
69
|
+
defer server.Close()
|
|
70
|
+
|
|
71
|
+
client := testutil.NewTestClient(server.URL, "", "", "test-token")
|
|
72
|
+
testApp := &app.App{Client: client}
|
|
73
|
+
|
|
74
|
+
cmd := cardAssignCmd
|
|
75
|
+
cmd.SetContext(testApp.ToContext(context.Background()))
|
|
76
|
+
|
|
77
|
+
err := handleAssignCard(cmd, "999", "user-id-123")
|
|
78
|
+
if err == nil {
|
|
79
|
+
t.Errorf("expected error for API failure")
|
|
80
|
+
}
|
|
81
|
+
if err.Error() != "assigning card: unexpected status code 404: Card not found" {
|
|
82
|
+
t.Errorf("expected API error, got %v", err)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
func TestCardAssignCommandInvalidCardNumber(t *testing.T) {
|
|
87
|
+
testApp := &app.App{}
|
|
88
|
+
|
|
89
|
+
cmd := cardAssignCmd
|
|
90
|
+
cmd.SetContext(testApp.ToContext(context.Background()))
|
|
91
|
+
|
|
92
|
+
err := handleAssignCard(cmd, "not-a-number", "user-id-123")
|
|
93
|
+
if err == nil {
|
|
94
|
+
t.Errorf("expected error for invalid card number")
|
|
95
|
+
}
|
|
96
|
+
if err.Error() != "invalid card number: strconv.Atoi: parsing \"not-a-number\": invalid syntax" {
|
|
97
|
+
t.Errorf("expected invalid card number error, got %v", err)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
func TestCardAssignCommandNoClient(t *testing.T) {
|
|
102
|
+
testApp := &app.App{}
|
|
103
|
+
|
|
104
|
+
cmd := cardAssignCmd
|
|
105
|
+
cmd.SetContext(testApp.ToContext(context.Background()))
|
|
106
|
+
|
|
107
|
+
err := handleAssignCard(cmd, "123", "user-id-123")
|
|
108
|
+
if err == nil {
|
|
109
|
+
t.Errorf("expected error when client not available")
|
|
110
|
+
}
|
|
111
|
+
if err.Error() != "API client not available" {
|
|
112
|
+
t.Errorf("expected 'client not available' error, got %v", err)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
func TestCardAssignCommandMeWithoutUserID(t *testing.T) {
|
|
117
|
+
client := testutil.NewTestClient("http://localhost", "", "", "test-token")
|
|
118
|
+
testApp := &app.App{Client: client, Config: &config.Config{}}
|
|
119
|
+
|
|
120
|
+
cmd := cardAssignCmd
|
|
121
|
+
cmd.SetContext(testApp.ToContext(context.Background()))
|
|
122
|
+
|
|
123
|
+
err := handleAssignCard(cmd, "123", "me")
|
|
124
|
+
if err == nil {
|
|
125
|
+
t.Errorf("expected error when using 'me' without current user ID")
|
|
126
|
+
}
|
|
127
|
+
if err.Error() != "current user ID not available, please run 'fizzy login' first" {
|
|
128
|
+
t.Errorf("expected 'current user ID not available' error, got %v", err)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
@@ -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
|
+
}
|