Skip to content

Commit b18ab35

Browse files
authored
Merge pull request #117 from basecamp/ux/cli-help-output-polish
Improve CLI help and human output UX
2 parents b3a72ba + 799540b commit b18ab35

21 files changed

Lines changed: 1159 additions & 78 deletions

SURFACE.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,7 @@ FLAG fizzy --quiet type=bool
163163
FLAG fizzy --styled type=bool
164164
FLAG fizzy --token type=string
165165
FLAG fizzy --verbose type=bool
166+
FLAG fizzy --version type=bool
166167
FLAG fizzy account --agent type=bool
167168
FLAG fizzy account --api-url type=string
168169
FLAG fizzy account --count type=bool

internal/commands/auth.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,7 @@ var authListCmd = &cobra.Command{
237237
breadcrumb("login", "fizzy auth login <token>", "Log in"),
238238
breadcrumb("signup", "fizzy signup", "Sign up"),
239239
}
240-
printSuccessWithBreadcrumbs([]any{}, "No profiles configured", breadcrumbs)
240+
printList([]any{}, authProfileColumns, "No profiles configured", breadcrumbs)
241241
return nil
242242
}
243243

@@ -247,7 +247,7 @@ var authListCmd = &cobra.Command{
247247
breadcrumb("login", "fizzy auth login <token>", "Log in"),
248248
breadcrumb("signup", "fizzy signup", "Sign up"),
249249
}
250-
printSuccessWithBreadcrumbs([]any{}, "No profiles configured", breadcrumbs)
250+
printList([]any{}, authProfileColumns, "No profiles configured", breadcrumbs)
251251
return nil
252252
}
253253

@@ -282,7 +282,7 @@ var authListCmd = &cobra.Command{
282282
breadcrumb("switch", "fizzy auth switch <profile>", "Switch profile"),
283283
}
284284

285-
printSuccessWithBreadcrumbs(entries, fmt.Sprintf("%d profile(s)", len(entries)), breadcrumbs)
285+
printList(entries, authProfileColumns, fmt.Sprintf("%d profile(s)", len(entries)), breadcrumbs)
286286
return nil
287287
},
288288
}

internal/commands/auth_test.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@ import (
44
"encoding/json"
55
"os"
66
"path/filepath"
7+
"strings"
78
"testing"
89

910
"github.com/basecamp/cli/credstore"
11+
"github.com/basecamp/cli/output"
1012
"github.com/basecamp/cli/profile"
1113
"github.com/basecamp/fizzy-cli/internal/config"
1214
"gopkg.in/yaml.v3"
@@ -507,6 +509,41 @@ func TestAuthList(t *testing.T) {
507509
t.Errorf("expected 0 profiles, got %d", len(profiles))
508510
}
509511
})
512+
513+
t.Run("renders styled output with next steps", func(t *testing.T) {
514+
credDir := t.TempDir()
515+
profileDir := t.TempDir()
516+
517+
os.Setenv("FIZZY_LIST_STYLED_NO_KR", "1")
518+
defer os.Unsetenv("FIZZY_LIST_STYLED_NO_KR")
519+
store := credstore.NewStore(credstore.StoreOptions{
520+
ServiceName: "fizzy-list-styled-test",
521+
DisableEnvVar: "FIZZY_LIST_STYLED_NO_KR",
522+
FallbackDir: credDir,
523+
})
524+
profileStore := profile.NewStore(filepath.Join(profileDir, "config.json"))
525+
profileStore.Create(&profile.Profile{Name: "acme", BaseURL: "https://app.fizzy.do"})
526+
t1, _ := json.Marshal("token1")
527+
store.Save("profile:acme", t1)
528+
529+
mock := NewMockClient()
530+
SetTestModeWithSDK(mock)
531+
SetTestCreds(store)
532+
SetTestProfiles(profileStore)
533+
SetTestFormat(output.FormatStyled)
534+
defer resetTest()
535+
536+
err := authListCmd.RunE(authListCmd, []string{})
537+
assertExitCode(t, err, 0)
538+
539+
raw := TestOutput()
540+
if !strings.Contains(raw, "Profile") {
541+
t.Fatalf("expected styled table output, got:\n%s", raw)
542+
}
543+
if !strings.Contains(raw, "Next steps:") {
544+
t.Fatalf("expected next steps section, got:\n%s", raw)
545+
}
546+
})
510547
}
511548

512549
func TestAuthSwitch(t *testing.T) {

internal/commands/column.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ var columnListCmd = &cobra.Command{
4141

4242
dataSlice := toSliceAny(items)
4343
if dataSlice == nil {
44-
printSuccess(items)
44+
printDetail(items, "", nil)
4545
return nil
4646
}
4747

internal/commands/columns.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,13 @@ var (
3939
{Header: "Name", Field: "name"},
4040
}
4141

42+
authProfileColumns = render.Columns{
43+
{Header: "Profile", Field: "profile"},
44+
{Header: "Active", Field: "active"},
45+
{Header: "Board", Field: "board"},
46+
{Header: "Base URL", Field: "base_url"},
47+
}
48+
4249
notificationColumns = render.Columns{
4350
{Header: "ID", Field: "id"},
4451
{Header: "Message", Field: "message"},

internal/commands/commands.go

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,11 @@ type flagInfo struct {
2424
Description string `json:"description"`
2525
}
2626

27-
// commandsCmd emits a flat catalog of all commands with their flags.
27+
// commandsCmd emits a catalog of all commands with their flags.
2828
var commandsCmd = &cobra.Command{
2929
Use: "commands",
3030
Short: "List all available commands",
31-
Long: "Lists all available commands with their flags in JSON format.",
31+
Long: "Lists all available commands. Use --json for a structured command catalog.",
3232
RunE: func(cmd *cobra.Command, args []string) error {
3333
catalog := walkCommands(rootCmd, "fizzy")
3434
printSuccess(catalog)
@@ -110,20 +110,3 @@ func agentHelp(cmd *cobra.Command, _ []string) {
110110
data, _ := json.MarshalIndent(info, "", " ")
111111
fmt.Fprintln(outWriter, string(data))
112112
}
113-
114-
// installAgentHelp sets the custom help function when --agent is active.
115-
func installAgentHelp() {
116-
rootCmd.SetHelpFunc(func(cmd *cobra.Command, args []string) {
117-
if cfgAgent {
118-
agentHelp(cmd, args)
119-
return
120-
}
121-
// Banner on root help only
122-
if cmd == rootCmd {
123-
printBanner()
124-
}
125-
// Fall back to Cobra's default help
126-
cmd.Root().SetHelpFunc(nil)
127-
_ = cmd.Help()
128-
})
129-
}

internal/commands/commands_test.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package commands
2+
3+
import (
4+
"strings"
5+
"testing"
6+
7+
"github.com/basecamp/cli/output"
8+
)
9+
10+
func TestCommandsStyledOutputRendersHumanCatalog(t *testing.T) {
11+
mock := NewMockClient()
12+
SetTestModeWithSDK(mock)
13+
SetTestFormat(output.FormatStyled)
14+
defer resetTest()
15+
16+
if err := commandsCmd.RunE(commandsCmd, []string{}); err != nil {
17+
t.Fatalf("unexpected error: %v", err)
18+
}
19+
20+
raw := TestOutput()
21+
if !strings.Contains(raw, "Name") {
22+
t.Fatalf("expected styled catalog header, got:\n%s", raw)
23+
}
24+
if !strings.Contains(raw, "fizzy auth") {
25+
t.Fatalf("expected styled catalog to include commands, got:\n%s", raw)
26+
}
27+
}
28+
29+
func TestCommandsJSONOutputReturnsStructuredCatalog(t *testing.T) {
30+
mock := NewMockClient()
31+
result := SetTestModeWithSDK(mock)
32+
defer resetTest()
33+
34+
if err := commandsCmd.RunE(commandsCmd, []string{}); err != nil {
35+
t.Fatalf("unexpected error: %v", err)
36+
}
37+
38+
if result.Response == nil || !result.Response.OK {
39+
t.Fatalf("expected OK JSON response, got %#v", result.Response)
40+
}
41+
42+
items, ok := result.Response.Data.([]any)
43+
if !ok {
44+
t.Fatalf("expected command catalog slice, got %#v", result.Response.Data)
45+
}
46+
if len(items) == 0 {
47+
t.Fatal("expected command catalog entries")
48+
}
49+
50+
found := false
51+
for _, item := range items {
52+
entry, ok := item.(map[string]any)
53+
if !ok {
54+
continue
55+
}
56+
if entry["name"] == "fizzy commands" {
57+
found = true
58+
break
59+
}
60+
}
61+
if !found {
62+
t.Fatalf("expected command catalog to include fizzy commands, got %#v", items)
63+
}
64+
}

0 commit comments

Comments
 (0)