Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 4 additions & 6 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,9 +94,7 @@ Each subcommand group lives in `internal/cmd/<group>/`:
All leaf commands are built using one of five generic base types. Always prefer these over raw `cobra.Command`.

#### `base.ListCmd[T]`
For listing resources. No args, no flags needed (extend via `BaseCobraCommand` if flags are required).

Prefer `OutputTable` over `PrintText` for table output. When `OutputTable` is set, the base automatically registers `--no-headers` and handles header suppression. `PrintText` is used as a fallback when `OutputTable` is not set.
For listing resources. `OutputTable` must be set. The base automatically registers `--no-headers` and handles header suppression. By default the command takes no positional args; set `Args` to accept them.

```go
base.ListCmd[*foov1.ListFoosResponse]{
Expand All @@ -105,11 +103,11 @@ base.ListCmd[*foov1.ListFoosResponse]{
Fetch: func(s *state.State, cmd *cobra.Command) (*foov1.ListFoosResponse, error) {
// call gRPC, return response
},
OutputTable: func(_ *cobra.Command, w io.Writer, resp *foov1.ListFoosResponse) output.Renderable {
OutputTable: func(_ *cobra.Command, w io.Writer, resp *foov1.ListFoosResponse) (output.TableRenderer, error) {
t := output.NewTable[*foov1.Foo](w)
t.AddField("ID", func(v *foov1.Foo) string { return v.GetId() })
t.SetItems(resp.Items)
return t
t.SetItems(resp.GetItems())
return t, nil
},
}.CobraCommand(s)
```
Expand Down
6 changes: 3 additions & 3 deletions internal/cmd/account/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ qcloud account list --json`,

return client.Account().ListAccounts(ctx, &accountv1.ListAccountsRequest{})
},
PrintText: func(_ *cobra.Command, w io.Writer, resp *accountv1.ListAccountsResponse) error {
OutputTable: func(_ *cobra.Command, w io.Writer, resp *accountv1.ListAccountsResponse) (output.TableRenderer, error) {
t := output.NewTable[*accountv1.Account](w)
t.AddField("ID", func(v *accountv1.Account) string { return v.GetId() })
t.AddField("NAME", func(v *accountv1.Account) string { return v.GetName() })
Expand All @@ -45,8 +45,8 @@ qcloud account list --json`,
}
return ""
})
t.Write(resp.GetItems())
return nil
t.SetItems(resp.GetItems())
return t, nil
},
}.CobraCommand(s)
}
16 changes: 16 additions & 0 deletions internal/cmd/account/list_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,19 @@ func TestAccountList_Empty(t *testing.T) {
assert.Contains(t, stdout, "ID")
assert.Contains(t, stdout, "NAME")
}

func TestAccountList_NoHeaders(t *testing.T) {
env := testutil.NewTestEnv(t)

env.AccountServer.ListAccountsCalls.Returns(&accountv1.ListAccountsResponse{
Items: []*accountv1.Account{
{Id: "acct-001", Name: "Production"},
},
}, nil)

stdout, _, err := testutil.Exec(t, env, "account", "list", "--no-headers")
require.NoError(t, err)
assert.NotContains(t, stdout, "ID")
assert.NotContains(t, stdout, "NAME")
assert.Contains(t, stdout, "acct-001")
}
6 changes: 3 additions & 3 deletions internal/cmd/account/member_list.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ qcloud account member list --json`,

return resp, nil
},
PrintText: func(_ *cobra.Command, w io.Writer, resp *accountv1.ListAccountMembersResponse) error {
OutputTable: func(_ *cobra.Command, w io.Writer, resp *accountv1.ListAccountMembersResponse) (output.TableRenderer, error) {
t := output.NewTable[*accountv1.AccountMember](w)
t.AddField("ID", func(v *accountv1.AccountMember) string {
return v.GetAccountMember().GetId()
Expand All @@ -65,8 +65,8 @@ qcloud account member list --json`,
}
return ""
})
t.Write(resp.GetItems())
return nil
t.SetItems(resp.GetItems())
return t, nil
},
}.CobraCommand(s)
}
16 changes: 16 additions & 0 deletions internal/cmd/account/member_list_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,3 +106,19 @@ func TestMemberList_Empty(t *testing.T) {
assert.Contains(t, stdout, "ID")
assert.Contains(t, stdout, "EMAIL")
}

func TestMemberList_NoHeaders(t *testing.T) {
env := testutil.NewTestEnv(t)

env.AccountServer.ListAccountMembersCalls.Returns(&accountv1.ListAccountMembersResponse{
Items: []*accountv1.AccountMember{
{AccountMember: &iamv1.User{Id: "user-001", Email: "owner@example.com"}},
},
}, nil)

stdout, _, err := testutil.Exec(t, env, "account", "member", "list", "--no-headers")
require.NoError(t, err)
assert.NotContains(t, stdout, "ID")
assert.NotContains(t, stdout, "EMAIL")
assert.Contains(t, stdout, "user-001")
}
6 changes: 3 additions & 3 deletions internal/cmd/backup/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ func newListCommand(s *state.State) *cobra.Command {
}
return resp, nil
},
PrintText: func(_ *cobra.Command, w io.Writer, resp *backupv1.ListBackupsResponse) error {
OutputTable: func(_ *cobra.Command, w io.Writer, resp *backupv1.ListBackupsResponse) (output.TableRenderer, error) {
t := output.NewTable[*backupv1.Backup](w)
t.AddField("ID", func(v *backupv1.Backup) string {
return v.GetId()
Expand All @@ -62,8 +62,8 @@ func newListCommand(s *state.State) *cobra.Command {
}
return ""
})
t.Write(resp.GetItems())
return nil
t.SetItems(resp.GetItems())
return t, nil
},
}.CobraCommand(s)

Expand Down
6 changes: 3 additions & 3 deletions internal/cmd/backup/restore_list.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ func newRestoreListCommand(s *state.State) *cobra.Command {
}
return resp, nil
},
PrintText: func(_ *cobra.Command, w io.Writer, resp *backupv1.ListBackupRestoresResponse) error {
OutputTable: func(_ *cobra.Command, w io.Writer, resp *backupv1.ListBackupRestoresResponse) (output.TableRenderer, error) {
t := output.NewTable[*backupv1.BackupRestore](w)
t.AddField("ID", func(v *backupv1.BackupRestore) string {
return v.GetId()
Expand All @@ -62,8 +62,8 @@ func newRestoreListCommand(s *state.State) *cobra.Command {
}
return ""
})
t.Write(resp.GetItems())
return nil
t.SetItems(resp.GetItems())
return t, nil
},
}.CobraCommand(s)

Expand Down
6 changes: 3 additions & 3 deletions internal/cmd/backup/schedule_list.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ func newScheduleListCommand(s *state.State) *cobra.Command {
}
return resp, nil
},
PrintText: func(_ *cobra.Command, w io.Writer, resp *backupv1.ListBackupSchedulesResponse) error {
OutputTable: func(_ *cobra.Command, w io.Writer, resp *backupv1.ListBackupSchedulesResponse) (output.TableRenderer, error) {
t := output.NewTable[*backupv1.BackupSchedule](w)
t.AddField("ID", func(v *backupv1.BackupSchedule) string {
return v.GetId()
Expand All @@ -68,8 +68,8 @@ func newScheduleListCommand(s *state.State) *cobra.Command {
}
return ""
})
t.Write(resp.GetItems())
return nil
t.SetItems(resp.GetItems())
return t, nil
},
}.CobraCommand(s)

Expand Down
36 changes: 17 additions & 19 deletions internal/cmd/base/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,28 +12,31 @@ import (
// ListCmd defines a command for fetching and displaying a list response.
// T is the full response proto message (e.g. *clusterv1.ListClustersResponse).
//
// For table output, prefer OutputTable over PrintText. When OutputTable is set,
// the base automatically registers --no-headers and handles header suppression.
// PrintText is used as a fallback when OutputTable is not set.
// OutputTable must be set. When set, --no-headers is automatically registered
// and handled.
type ListCmd[T any] struct {
Use string
Short string
Long string
Example string
Args cobra.PositionalArgs // optional; defaults to cobra.NoArgs
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cluster key list command required an argument, thus defaulting to NoArgs but allowing clients to set them if needed.

Fetch func(s *state.State, cmd *cobra.Command) (T, error)
OutputTable func(cmd *cobra.Command, out io.Writer, resp T) output.TableRenderer
PrintText func(cmd *cobra.Command, out io.Writer, resp T) error
OutputTable func(cmd *cobra.Command, out io.Writer, resp T) (output.TableRenderer, error)
ValidArgsFunction func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective)
}

// CobraCommand builds a cobra.Command from this ListCmd.
func (lc ListCmd[T]) CobraCommand(s *state.State) *cobra.Command {
posArgs := lc.Args
if posArgs == nil {
posArgs = cobra.NoArgs
}
cmd := &cobra.Command{
Use: lc.Use,
Short: lc.Short,
Long: lc.Long,
Example: lc.Example,
Args: cobra.NoArgs,
Args: posArgs,
RunE: func(cmd *cobra.Command, args []string) error {
resp, err := lc.Fetch(s, cmd)
if err != nil {
Expand All @@ -42,22 +45,17 @@ func (lc ListCmd[T]) CobraCommand(s *state.State) *cobra.Command {
if s.Config.JSONOutput() {
return output.PrintJSON(cmd.OutOrStdout(), resp)
}
if lc.OutputTable != nil {
r := lc.OutputTable(cmd, cmd.OutOrStdout(), resp)
noHeaders, _ := cmd.Flags().GetBool("no-headers")
r.SetNoHeaders(noHeaders)
r.Render()
return nil
}
if lc.PrintText == nil {
panic("ListCmd: either OutputTable or PrintText must be set")
r, err := lc.OutputTable(cmd, cmd.OutOrStdout(), resp)
if err != nil {
return err
}
return lc.PrintText(cmd, cmd.OutOrStdout(), resp)
noHeaders, _ := cmd.Flags().GetBool("no-headers")
r.SetNoHeaders(noHeaders)
r.Render()
return nil
},
}
if lc.OutputTable != nil {
cmd.Flags().Bool("no-headers", false, "Do not print column headers")
}
cmd.Flags().Bool("no-headers", false, "Do not print column headers")
if lc.ValidArgsFunction != nil {
cmd.ValidArgsFunction = lc.ValidArgsFunction
}
Expand Down
44 changes: 17 additions & 27 deletions internal/cmd/base/list_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package base_test

import (
"bytes"
"fmt"
"io"
"testing"

Expand Down Expand Up @@ -38,8 +37,8 @@ func TestListCmd_OutputTable(t *testing.T) {
lc := base.ListCmd[string]{
Use: "test",
Fetch: fetchHello,
OutputTable: func(_ *cobra.Command, out io.Writer, resp string) output.TableRenderer {
return stringTableRenderer(out, resp)
OutputTable: func(_ *cobra.Command, out io.Writer, resp string) (output.TableRenderer, error) {
return stringTableRenderer(out, resp), nil
},
}

Expand All @@ -49,43 +48,34 @@ func TestListCmd_OutputTable(t *testing.T) {
assert.Contains(t, stdout, "hello")
}

func TestListCmd_OutputTable_NoHeaders(t *testing.T) {
func TestListCmd_WithArgs(t *testing.T) {
lc := base.ListCmd[string]{
Use: "test",
Fetch: fetchHello,
OutputTable: func(_ *cobra.Command, out io.Writer, resp string) output.TableRenderer {
return stringTableRenderer(out, resp)
Use: "test <value>",
Args: cobra.ExactArgs(1),
Fetch: func(_ *state.State, cmd *cobra.Command) (string, error) {
return cmd.Flags().Arg(0), nil
},
OutputTable: func(_ *cobra.Command, out io.Writer, resp string) (output.TableRenderer, error) {
return stringTableRenderer(out, resp), nil
},
}

stdout, err := execListCmd(t, lc, "--no-headers")
stdout, err := execListCmd(t, lc, "world")
require.NoError(t, err)
assert.NotContains(t, stdout, "VALUE")
assert.Contains(t, stdout, "hello")
assert.Contains(t, stdout, "world")
}

func TestListCmd_PrintText(t *testing.T) {
func TestListCmd_OutputTable_NoHeaders(t *testing.T) {
lc := base.ListCmd[string]{
Use: "test",
Fetch: fetchHello,
PrintText: func(_ *cobra.Command, out io.Writer, resp string) error {
_, err := fmt.Fprintln(out, resp)
return err
OutputTable: func(_ *cobra.Command, out io.Writer, resp string) (output.TableRenderer, error) {
return stringTableRenderer(out, resp), nil
},
}

stdout, err := execListCmd(t, lc)
stdout, err := execListCmd(t, lc, "--no-headers")
require.NoError(t, err)
assert.NotContains(t, stdout, "VALUE")
assert.Contains(t, stdout, "hello")
}

func TestListCmd_NeitherOutputTableNorPrintText_Panics(t *testing.T) {
lc := base.ListCmd[string]{
Use: "test",
Fetch: fetchHello,
}

assert.Panics(t, func() {
_, _ = execListCmd(t, lc)
})
}
4 changes: 2 additions & 2 deletions internal/cmd/cloudprovider/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ func newListCommand(s *state.State) *cobra.Command {

return resp, nil
},
OutputTable: func(_ *cobra.Command, w io.Writer, resp *platformv1.ListCloudProvidersResponse) output.TableRenderer {
OutputTable: func(_ *cobra.Command, w io.Writer, resp *platformv1.ListCloudProvidersResponse) (output.TableRenderer, error) {
t := output.NewTable[*platformv1.CloudProvider](w)
t.AddField("ID", func(v *platformv1.CloudProvider) string {
return v.GetId()
Expand All @@ -51,7 +51,7 @@ func newListCommand(s *state.State) *cobra.Command {
return strconv.FormatBool(v.GetAvailable())
})
t.SetItems(resp.GetItems())
return t
return t, nil
},
}.CobraCommand(s)
}
6 changes: 3 additions & 3 deletions internal/cmd/cloudregion/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ func newListCommand(s *state.State) *cobra.Command {

return resp, nil
},
PrintText: func(_ *cobra.Command, w io.Writer, resp *platformv1.ListCloudProviderRegionsResponse) error {
OutputTable: func(_ *cobra.Command, w io.Writer, resp *platformv1.ListCloudProviderRegionsResponse) (output.TableRenderer, error) {
t := output.NewTable[*platformv1.CloudProviderRegion](w)
t.AddField("ID", func(v *platformv1.CloudProviderRegion) string {
return v.GetId()
Expand All @@ -57,8 +57,8 @@ func newListCommand(s *state.State) *cobra.Command {
t.AddField("AVAILABLE", func(v *platformv1.CloudProviderRegion) string {
return strconv.FormatBool(v.GetAvailable())
})
t.Write(resp.GetItems())
return nil
t.SetItems(resp.GetItems())
return t, nil
},
}.CobraCommand(s)

Expand Down
12 changes: 6 additions & 6 deletions internal/cmd/cluster/key_list.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,14 @@ import (
)

func newKeyListCommand(s *state.State) *cobra.Command {
return base.DescribeCmd[*clusterauthv2.ListDatabaseApiKeysResponse]{
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was a hack to get the argument, fixed.

return base.ListCmd[*clusterauthv2.ListDatabaseApiKeysResponse]{
Use: "list <cluster-id>",
Short: "List API keys for a cluster",
Example: `# List API keys for a cluster
qcloud cluster key list 7b2ea926-724b-4de2-b73a-8675c42a6ebe`,
Args: util.ExactArgs(1, "a cluster ID"),
Fetch: func(s *state.State, cmd *cobra.Command, args []string) (*clusterauthv2.ListDatabaseApiKeysResponse, error) {
clusterID := args[0]
Fetch: func(s *state.State, cmd *cobra.Command) (*clusterauthv2.ListDatabaseApiKeysResponse, error) {
clusterID := cmd.Flags().Arg(0)

ctx := cmd.Context()
client, err := s.Client(ctx)
Expand All @@ -46,7 +46,7 @@ qcloud cluster key list 7b2ea926-724b-4de2-b73a-8675c42a6ebe`,

return resp, nil
},
PrintText: func(_ *cobra.Command, w io.Writer, resp *clusterauthv2.ListDatabaseApiKeysResponse) error {
OutputTable: func(_ *cobra.Command, w io.Writer, resp *clusterauthv2.ListDatabaseApiKeysResponse) (output.TableRenderer, error) {
t := output.NewTable[*clusterauthv2.DatabaseApiKey](w)
t.AddField("ID", func(v *clusterauthv2.DatabaseApiKey) string {
return v.GetId()
Expand All @@ -69,8 +69,8 @@ qcloud cluster key list 7b2ea926-724b-4de2-b73a-8675c42a6ebe`,
}
return ""
})
t.Write(resp.GetItems())
return nil
t.SetItems(resp.GetItems())
return t, nil
},
ValidArgsFunction: completion.ClusterIDCompletion(s),
}.CobraCommand(s)
Expand Down
Loading
Loading