Skip to content

Commit b55d85b

Browse files
committed
Add board publication commands
1 parent 93a5a50 commit b55d85b

6 files changed

Lines changed: 294 additions & 2 deletions

File tree

SURFACE.txt

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ CMD fizzy board create
1111
CMD fizzy board delete
1212
CMD fizzy board help
1313
CMD fizzy board list
14+
CMD fizzy board publish
1415
CMD fizzy board show
16+
CMD fizzy board unpublish
1517
CMD fizzy board update
1618
CMD fizzy card
1719
CMD fizzy card assign
@@ -296,6 +298,19 @@ FLAG fizzy board list --quiet type=bool
296298
FLAG fizzy board list --styled type=bool
297299
FLAG fizzy board list --token type=string
298300
FLAG fizzy board list --verbose type=bool
301+
FLAG fizzy board publish --agent type=bool
302+
FLAG fizzy board publish --api-url type=string
303+
FLAG fizzy board publish --count type=bool
304+
FLAG fizzy board publish --help type=bool
305+
FLAG fizzy board publish --ids-only type=bool
306+
FLAG fizzy board publish --json type=bool
307+
FLAG fizzy board publish --limit type=int
308+
FLAG fizzy board publish --markdown type=bool
309+
FLAG fizzy board publish --profile type=string
310+
FLAG fizzy board publish --quiet type=bool
311+
FLAG fizzy board publish --styled type=bool
312+
FLAG fizzy board publish --token type=string
313+
FLAG fizzy board publish --verbose type=bool
299314
FLAG fizzy board show --agent type=bool
300315
FLAG fizzy board show --api-url type=string
301316
FLAG fizzy board show --count type=bool
@@ -309,6 +324,19 @@ FLAG fizzy board show --quiet type=bool
309324
FLAG fizzy board show --styled type=bool
310325
FLAG fizzy board show --token type=string
311326
FLAG fizzy board show --verbose type=bool
327+
FLAG fizzy board unpublish --agent type=bool
328+
FLAG fizzy board unpublish --api-url type=string
329+
FLAG fizzy board unpublish --count type=bool
330+
FLAG fizzy board unpublish --help type=bool
331+
FLAG fizzy board unpublish --ids-only type=bool
332+
FLAG fizzy board unpublish --json type=bool
333+
FLAG fizzy board unpublish --limit type=int
334+
FLAG fizzy board unpublish --markdown type=bool
335+
FLAG fizzy board unpublish --profile type=string
336+
FLAG fizzy board unpublish --quiet type=bool
337+
FLAG fizzy board unpublish --styled type=bool
338+
FLAG fizzy board unpublish --token type=string
339+
FLAG fizzy board unpublish --verbose type=bool
312340
FLAG fizzy board update --agent type=bool
313341
FLAG fizzy board update --all_access type=string
314342
FLAG fizzy board update --api-url type=string
@@ -1853,7 +1881,9 @@ SUB fizzy board create
18531881
SUB fizzy board delete
18541882
SUB fizzy board help
18551883
SUB fizzy board list
1884+
SUB fizzy board publish
18561885
SUB fizzy board show
1886+
SUB fizzy board unpublish
18571887
SUB fizzy board update
18581888
SUB fizzy card
18591889
SUB fizzy card assign

e2e/tests/board_test.go

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,76 @@ func TestBoardCRUD(t *testing.T) {
142142
if id != boardID {
143143
t.Errorf("expected id %q, got %q", boardID, id)
144144
}
145+
146+
if publicURL := result.GetDataString("public_url"); publicURL != "" {
147+
t.Errorf("expected unpublished board to omit public_url, got %q", publicURL)
148+
}
149+
})
150+
151+
t.Run("publish and unpublish board", func(t *testing.T) {
152+
if boardID == "" {
153+
t.Skip("no board ID from create test")
154+
}
155+
156+
publishResult := h.Run("board", "publish", boardID)
157+
158+
// Always unpublish on exit, even if assertions fail mid-test
159+
t.Cleanup(func() {
160+
h.Run("board", "unpublish", boardID)
161+
})
162+
163+
if publishResult.ExitCode != harness.ExitSuccess {
164+
t.Fatalf("expected exit code %d, got %d\nstderr: %s\nstdout: %s",
165+
harness.ExitSuccess, publishResult.ExitCode, publishResult.Stderr, publishResult.Stdout)
166+
}
167+
168+
if publishResult.Response == nil {
169+
t.Fatal("expected JSON response from publish")
170+
}
171+
172+
if !publishResult.Response.OK {
173+
t.Fatalf("expected ok=true, error: %+v", publishResult.Response.Error)
174+
}
175+
176+
publicURL := publishResult.GetDataString("public_url")
177+
if publicURL == "" {
178+
t.Fatal("expected public_url in publish response")
179+
}
180+
181+
showPublished := h.Run("board", "show", boardID)
182+
if showPublished.ExitCode != harness.ExitSuccess {
183+
t.Fatalf("failed to show published board: %s", showPublished.Stderr)
184+
}
185+
if got := showPublished.GetDataString("public_url"); got != publicURL {
186+
t.Errorf("expected public_url %q after publish, got %q", publicURL, got)
187+
}
188+
189+
// Unpublish is handled by t.Cleanup above; verify it works
190+
unpublishResult := h.Run("board", "unpublish", boardID)
191+
if unpublishResult.ExitCode != harness.ExitSuccess {
192+
t.Fatalf("expected exit code %d, got %d\nstderr: %s\nstdout: %s",
193+
harness.ExitSuccess, unpublishResult.ExitCode, unpublishResult.Stderr, unpublishResult.Stdout)
194+
}
195+
196+
if unpublishResult.Response == nil {
197+
t.Fatal("expected JSON response from unpublish")
198+
}
199+
200+
if !unpublishResult.Response.OK {
201+
t.Fatalf("expected ok=true, error: %+v", unpublishResult.Response.Error)
202+
}
203+
204+
if !unpublishResult.GetDataBool("unpublished") {
205+
t.Error("expected unpublished=true")
206+
}
207+
208+
showUnpublished := h.Run("board", "show", boardID)
209+
if showUnpublished.ExitCode != harness.ExitSuccess {
210+
t.Fatalf("failed to show unpublished board: %s", showUnpublished.Stderr)
211+
}
212+
if got := showUnpublished.GetDataString("public_url"); got != "" {
213+
t.Errorf("expected public_url to be removed after unpublish, got %q", got)
214+
}
145215
})
146216

147217
t.Run("update board name", func(t *testing.T) {

internal/commands/board.go

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,13 @@ var boardShowCmd = &cobra.Command{
115115
breadcrumb("columns", fmt.Sprintf("fizzy column list --board %s", boardID), "List columns"),
116116
breadcrumb("create-card", fmt.Sprintf("fizzy card create --board %s --title \"title\"", boardID), "Create card"),
117117
}
118+
if board, ok := items.(map[string]any); ok {
119+
if publicURL, ok := board["public_url"].(string); ok && publicURL != "" {
120+
breadcrumbs = append(breadcrumbs, breadcrumb("unpublish", fmt.Sprintf("fizzy board unpublish %s", boardID), "Disable public board link"))
121+
} else {
122+
breadcrumbs = append(breadcrumbs, breadcrumb("publish", fmt.Sprintf("fizzy board publish %s", boardID), "Create public board link"))
123+
}
124+
}
118125

119126
printDetail(items, summary, breadcrumbs)
120127
return nil
@@ -272,6 +279,71 @@ var boardDeleteCmd = &cobra.Command{
272279
},
273280
}
274281

282+
var boardPublishCmd = &cobra.Command{
283+
Use: "publish BOARD_ID",
284+
Short: "Publish a board",
285+
Long: "Publishes a board and returns its public share URL.",
286+
Args: cobra.ExactArgs(1),
287+
RunE: func(cmd *cobra.Command, args []string) error {
288+
if err := requireAuthAndAccount(); err != nil {
289+
return err
290+
}
291+
292+
boardID := args[0]
293+
294+
client := getClient()
295+
resp, err := client.Post("/boards/"+boardID+"/publication.json", nil)
296+
if err != nil {
297+
return err
298+
}
299+
300+
breadcrumbs := []Breadcrumb{
301+
breadcrumb("show", fmt.Sprintf("fizzy board show %s", boardID), "View board"),
302+
breadcrumb("cards", fmt.Sprintf("fizzy card list --board %s", boardID), "List cards"),
303+
breadcrumb("unpublish", fmt.Sprintf("fizzy board unpublish %s", boardID), "Disable public board link"),
304+
}
305+
306+
data := resp.Data
307+
if data == nil {
308+
data = map[string]any{"published": true}
309+
}
310+
311+
printMutation(data, "", breadcrumbs)
312+
return nil
313+
},
314+
}
315+
316+
var boardUnpublishCmd = &cobra.Command{
317+
Use: "unpublish BOARD_ID",
318+
Short: "Unpublish a board",
319+
Long: "Removes a board's public share URL.",
320+
Args: cobra.ExactArgs(1),
321+
RunE: func(cmd *cobra.Command, args []string) error {
322+
if err := requireAuthAndAccount(); err != nil {
323+
return err
324+
}
325+
326+
boardID := args[0]
327+
328+
client := getClient()
329+
_, err := client.Delete("/boards/" + boardID + "/publication.json")
330+
if err != nil {
331+
return err
332+
}
333+
334+
breadcrumbs := []Breadcrumb{
335+
breadcrumb("show", fmt.Sprintf("fizzy board show %s", boardID), "View board"),
336+
breadcrumb("cards", fmt.Sprintf("fizzy card list --board %s", boardID), "List cards"),
337+
breadcrumb("publish", fmt.Sprintf("fizzy board publish %s", boardID), "Create public board link"),
338+
}
339+
340+
printMutation(map[string]any{
341+
"unpublished": true,
342+
}, "", breadcrumbs)
343+
return nil
344+
},
345+
}
346+
275347
func init() {
276348
rootCmd.AddCommand(boardCmd)
277349

@@ -297,4 +369,8 @@ func init() {
297369

298370
// Delete
299371
boardCmd.AddCommand(boardDeleteCmd)
372+
373+
// Publication
374+
boardCmd.AddCommand(boardPublishCmd)
375+
boardCmd.AddCommand(boardUnpublishCmd)
300376
}

internal/commands/board_test.go

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -389,3 +389,111 @@ func TestBoardDelete(t *testing.T) {
389389
assertExitCode(t, err, errors.ExitNotFound)
390390
})
391391
}
392+
393+
func TestBoardPublish(t *testing.T) {
394+
t.Run("publishes board", func(t *testing.T) {
395+
mock := NewMockClient()
396+
mock.PostResponse = &client.APIResponse{
397+
StatusCode: 201,
398+
Data: map[string]any{
399+
"id": "123",
400+
"name": "Published Board",
401+
"public_url": "https://app.fizzy.do/public/boards/test",
402+
},
403+
}
404+
405+
result := SetTestMode(mock)
406+
SetTestConfig("token", "account", "https://api.example.com")
407+
defer ResetTestMode()
408+
409+
err := boardPublishCmd.RunE(boardPublishCmd, []string{"123"})
410+
assertExitCode(t, err, 0)
411+
412+
if err != nil {
413+
t.Fatalf("unexpected error: %v", err)
414+
}
415+
if !result.Response.OK {
416+
t.Error("expected success response")
417+
}
418+
if len(mock.PostCalls) != 1 {
419+
t.Errorf("expected 1 Post call, got %d", len(mock.PostCalls))
420+
}
421+
if mock.PostCalls[0].Path != "/boards/123/publication.json" {
422+
t.Errorf("expected path '/boards/123/publication.json', got '%s'", mock.PostCalls[0].Path)
423+
}
424+
if result.Response == nil || result.Response.Data == nil {
425+
t.Fatal("expected response data")
426+
}
427+
data, ok := result.Response.Data.(map[string]any)
428+
if !ok {
429+
t.Fatal("expected response data map")
430+
}
431+
if data["public_url"] != "https://app.fizzy.do/public/boards/test" {
432+
t.Errorf("expected public_url in response, got %v", data["public_url"])
433+
}
434+
})
435+
436+
t.Run("handles API error", func(t *testing.T) {
437+
mock := NewMockClient()
438+
mock.PostError = errors.NewForbiddenError("Only admins can publish boards")
439+
440+
SetTestMode(mock)
441+
SetTestConfig("token", "account", "https://api.example.com")
442+
defer ResetTestMode()
443+
444+
err := boardPublishCmd.RunE(boardPublishCmd, []string{"123"})
445+
assertExitCode(t, err, errors.ExitForbidden)
446+
})
447+
}
448+
449+
func TestBoardUnpublish(t *testing.T) {
450+
t.Run("unpublishes board", func(t *testing.T) {
451+
mock := NewMockClient()
452+
mock.DeleteResponse = &client.APIResponse{
453+
StatusCode: 204,
454+
Data: map[string]any{},
455+
}
456+
457+
result := SetTestMode(mock)
458+
SetTestConfig("token", "account", "https://api.example.com")
459+
defer ResetTestMode()
460+
461+
err := boardUnpublishCmd.RunE(boardUnpublishCmd, []string{"123"})
462+
assertExitCode(t, err, 0)
463+
464+
if err != nil {
465+
t.Fatalf("unexpected error: %v", err)
466+
}
467+
if !result.Response.OK {
468+
t.Error("expected success response")
469+
}
470+
if len(mock.DeleteCalls) != 1 {
471+
t.Errorf("expected 1 Delete call, got %d", len(mock.DeleteCalls))
472+
}
473+
if mock.DeleteCalls[0].Path != "/boards/123/publication.json" {
474+
t.Errorf("expected path '/boards/123/publication.json', got '%s'", mock.DeleteCalls[0].Path)
475+
}
476+
if result.Response == nil || result.Response.Data == nil {
477+
t.Fatal("expected response data")
478+
}
479+
data, ok := result.Response.Data.(map[string]any)
480+
if !ok {
481+
t.Fatal("expected response data map")
482+
}
483+
if data["unpublished"] != true {
484+
t.Errorf("expected unpublished=true, got %v", data["unpublished"])
485+
}
486+
})
487+
488+
t.Run("handles not found", func(t *testing.T) {
489+
mock := NewMockClient()
490+
mock.DeleteError = errors.NewNotFoundError("Board not found")
491+
492+
SetTestMode(mock)
493+
SetTestConfig("token", "account", "https://api.example.com")
494+
defer ResetTestMode()
495+
496+
err := boardUnpublishCmd.RunE(boardUnpublishCmd, []string{"999"})
497+
assertExitCode(t, err, errors.ExitNotFound)
498+
})
499+
}

internal/skills/SKILL.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ Want to change something?
9797

9898
| Resource | List | Show | Create | Update | Delete | Other |
9999
|----------|------|------|--------|--------|--------|-------|
100-
| board | `board list` | `board show ID` | `board create` | `board update ID` | `board delete ID` | `migrate board ID` |
100+
| board | `board list` | `board show ID` | `board create` | `board update ID` | `board delete ID` | `board publish ID`, `board unpublish ID`, `migrate board ID` |
101101
| card | `card list` | `card show NUMBER` | `card create` | `card update NUMBER` | `card delete NUMBER` | `card move NUMBER` |
102102
| search | `search QUERY` | - | - | - | - | - |
103103
| column | `column list --board ID` | `column show ID --board ID` | `column create` | `column update ID` | `column delete ID` | - |
@@ -473,9 +473,13 @@ fizzy board list [--page N] [--all]
473473
fizzy board show BOARD_ID
474474
fizzy board create --name "Name" [--all_access true/false] [--auto_postpone_period N]
475475
fizzy board update BOARD_ID [--name "Name"] [--all_access true/false] [--auto_postpone_period N]
476+
fizzy board publish BOARD_ID
477+
fizzy board unpublish BOARD_ID
476478
fizzy board delete BOARD_ID
477479
```
478480

481+
`board show` includes `public_url` only when the board is published.
482+
479483
### Board Migration
480484

481485
Migrate boards between accounts (e.g., from personal to team account).

skills/fizzy/SKILL.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ Want to change something?
9797

9898
| Resource | List | Show | Create | Update | Delete | Other |
9999
|----------|------|------|--------|--------|--------|-------|
100-
| board | `board list` | `board show ID` | `board create` | `board update ID` | `board delete ID` | `migrate board ID` |
100+
| board | `board list` | `board show ID` | `board create` | `board update ID` | `board delete ID` | `board publish ID`, `board unpublish ID`, `migrate board ID` |
101101
| card | `card list` | `card show NUMBER` | `card create` | `card update NUMBER` | `card delete NUMBER` | `card move NUMBER` |
102102
| search | `search QUERY` | - | - | - | - | - |
103103
| column | `column list --board ID` | `column show ID --board ID` | `column create` | `column update ID` | `column delete ID` | - |
@@ -473,9 +473,13 @@ fizzy board list [--page N] [--all]
473473
fizzy board show BOARD_ID
474474
fizzy board create --name "Name" [--all_access true/false] [--auto_postpone_period N]
475475
fizzy board update BOARD_ID [--name "Name"] [--all_access true/false] [--auto_postpone_period N]
476+
fizzy board publish BOARD_ID
477+
fizzy board unpublish BOARD_ID
476478
fizzy board delete BOARD_ID
477479
```
478480

481+
`board show` includes `public_url` only when the board is published.
482+
479483
### Board Migration
480484

481485
Migrate boards between accounts (e.g., from personal to team account).

0 commit comments

Comments
 (0)