diff --git a/cmd/lk/app.go b/cmd/lk/app.go index 316ed23..f86e3b5 100644 --- a/cmd/lk/app.go +++ b/cmd/lk/app.go @@ -72,9 +72,10 @@ var ( Hidden: true, }, &cli.BoolFlag{ - Name: "install", - Usage: "Run installation tasks after creating the app", - Hidden: true, + Name: "install", + Aliases: []string{"i"}, + Usage: "Run installation tasks after creating the app", + Hidden: true, }, }, }, diff --git a/cmd/lk/cloud.go b/cmd/lk/cloud.go index 6950687..26c5639 100644 --- a/cmd/lk/cloud.go +++ b/cmd/lk/cloud.go @@ -72,20 +72,20 @@ var ( Action: handleAuth, Flags: []cli.Flag{ &cli.BoolFlag{ - Name: "R", - Aliases: []string{"revoke"}, + Name: "revoke", + Aliases: []string{"R"}, Destination: &revoke, }, &cli.IntFlag{ - Name: "t", - Aliases: []string{"timeout"}, + Name: "timeout", + Aliases: []string{"t"}, Usage: "Number of `SECONDS` to attempt authentication before giving up", Destination: &timeout, Value: 60 * 15, }, &cli.IntFlag{ - Name: "i", - Aliases: []string{"poll-interval"}, + Name: "poll-interval", + Aliases: []string{"i"}, Usage: "Number of `SECONDS` between poll requests to verify authentication", Destination: &interval, Value: 4, diff --git a/cmd/lk/egress.go b/cmd/lk/egress.go index f888c14..901e86b 100644 --- a/cmd/lk/egress.go +++ b/cmd/lk/egress.go @@ -103,9 +103,11 @@ var ( Usage: "Limits list to a certain room `NAME`", }, &cli.BoolFlag{ - Name: "active", - Usage: "Lists only active egresses", + Name: "active", + Aliases: []string{"a"}, + Usage: "Lists only active egresses", }, + jsonFlag, }, }, { @@ -595,48 +597,52 @@ func listEgress(ctx context.Context, cmd *cli.Command) error { items = res.Items } - table := CreateTable(). - Headers("EgressID", "Status", "Type", "Source", "Started At", "Error") - for _, item := range items { - var startedAt string - if item.StartedAt != 0 { - startedAt = fmt.Sprint(time.Unix(0, item.StartedAt)) - } - var egressType, egressSource string - switch req := item.Request.(type) { - case *livekit.EgressInfo_RoomComposite: - egressType = "room_composite" - egressSource = req.RoomComposite.RoomName - case *livekit.EgressInfo_Web: - egressType = "web" - egressSource = req.Web.Url - case *livekit.EgressInfo_Participant: - egressType = "participant" - egressSource = fmt.Sprintf("%s/%s", req.Participant.RoomName, req.Participant.Identity) - case *livekit.EgressInfo_TrackComposite: - egressType = "track_composite" - trackIDs := make([]string, 0) - if req.TrackComposite.VideoTrackId != "" { - trackIDs = append(trackIDs, req.TrackComposite.VideoTrackId) + if cmd.Bool("json") { + PrintJSON(items) + } else { + table := CreateTable(). + Headers("EgressID", "Status", "Type", "Source", "Started At", "Error") + for _, item := range items { + var startedAt string + if item.StartedAt != 0 { + startedAt = fmt.Sprint(time.Unix(0, item.StartedAt)) } - if req.TrackComposite.AudioTrackId != "" { - trackIDs = append(trackIDs, req.TrackComposite.AudioTrackId) + var egressType, egressSource string + switch req := item.Request.(type) { + case *livekit.EgressInfo_RoomComposite: + egressType = "room_composite" + egressSource = req.RoomComposite.RoomName + case *livekit.EgressInfo_Web: + egressType = "web" + egressSource = req.Web.Url + case *livekit.EgressInfo_Participant: + egressType = "participant" + egressSource = fmt.Sprintf("%s/%s", req.Participant.RoomName, req.Participant.Identity) + case *livekit.EgressInfo_TrackComposite: + egressType = "track_composite" + trackIDs := make([]string, 0) + if req.TrackComposite.VideoTrackId != "" { + trackIDs = append(trackIDs, req.TrackComposite.VideoTrackId) + } + if req.TrackComposite.AudioTrackId != "" { + trackIDs = append(trackIDs, req.TrackComposite.AudioTrackId) + } + egressSource = fmt.Sprintf("%s/%s", req.TrackComposite.RoomName, strings.Join(trackIDs, ",")) + case *livekit.EgressInfo_Track: + egressType = "track" + egressSource = fmt.Sprintf("%s/%s", req.Track.RoomName, req.Track.TrackId) } - egressSource = fmt.Sprintf("%s/%s", req.TrackComposite.RoomName, strings.Join(trackIDs, ",")) - case *livekit.EgressInfo_Track: - egressType = "track" - egressSource = fmt.Sprintf("%s/%s", req.Track.RoomName, req.Track.TrackId) + table.Row( + item.EgressId, + item.Status.String(), + egressType, + egressSource, + startedAt, + item.Error, + ) } - table.Row( - item.EgressId, - item.Status.String(), - egressType, - egressSource, - startedAt, - item.Error, - ) - } - fmt.Println(table) + fmt.Println(table) + } return nil } diff --git a/cmd/lk/ingress.go b/cmd/lk/ingress.go index e77a2e0..af15743 100644 --- a/cmd/lk/ingress.go +++ b/cmd/lk/ingress.go @@ -82,6 +82,7 @@ var ( Usage: "List a specific ingress `ID`", Required: false, }, + jsonFlag, }, }, { @@ -223,33 +224,36 @@ func listIngress(ctx context.Context, cmd *cli.Command) error { return err } - table := CreateTable(). - Headers("IngressID", "Name", "Room", "StreamKey", "URL", "Status", "Error") - for _, item := range res.Items { - if item == nil { - continue - } + // NOTE: previously, the `verbose` flag was used to output JSON in addition to the table. + // This is inconsistent with other commands in which verbose is used for debug info, but is + // kept for compatibility with the previous behavior. + if cmd.Bool("verbose") || cmd.Bool("json") { + PrintJSON(res) + } else { + table := CreateTable(). + Headers("IngressID", "Name", "Room", "StreamKey", "URL", "Status", "Error") + for _, item := range res.Items { + if item == nil { + continue + } - var status, errorStr string - if item.State != nil { - status = item.State.Status.String() - errorStr = item.State.Error - } + var status, errorStr string + if item.State != nil { + status = item.State.Status.String() + errorStr = item.State.Error + } - table.Row( - item.IngressId, - item.Name, - item.RoomName, - item.StreamKey, - item.Url, - status, - errorStr, - ) - } - fmt.Println(table) - - if cmd.Bool("verbose") { - PrintJSON(res) + table.Row( + item.IngressId, + item.Name, + item.RoomName, + item.StreamKey, + item.Url, + status, + errorStr, + ) + } + fmt.Println(table) } return nil diff --git a/cmd/lk/project.go b/cmd/lk/project.go index 2c2b415..16c27d3 100644 --- a/cmd/lk/project.go +++ b/cmd/lk/project.go @@ -67,6 +67,7 @@ var ( Usage: "List all configured projects", UsageText: "lk project list", Action: listProjects, + Flags: []cli.Flag{jsonFlag}, }, { Name: "remove", @@ -247,22 +248,26 @@ func listProjects(ctx context.Context, cmd *cli.Command) error { headerStyle := baseStyle.Bold(true) selectedStyle := theme.Focused.Title.Padding(0, 1) - table := CreateTable(). - StyleFunc(func(row, col int) lipgloss.Style { - switch { - case row == table.HeaderRow: - return headerStyle - case cliConfig.Projects[row].Name == cliConfig.DefaultProject: - return selectedStyle - default: - return baseStyle - } - }). - Headers("Name", "URL", "API Key", "Default") - for _, p := range cliConfig.Projects { - table.Row(p.Name, p.URL, p.APIKey, fmt.Sprint(p.Name == cliConfig.DefaultProject)) + if cmd.Bool("json") { + PrintJSON(cliConfig.Projects) + } else { + table := CreateTable(). + StyleFunc(func(row, col int) lipgloss.Style { + switch { + case row == table.HeaderRow: + return headerStyle + case cliConfig.Projects[row].Name == cliConfig.DefaultProject: + return selectedStyle + default: + return baseStyle + } + }). + Headers("Name", "URL", "API Key", "Default") + for _, p := range cliConfig.Projects { + table.Row(p.Name, p.URL, p.APIKey, fmt.Sprint(p.Name == cliConfig.DefaultProject)) + } + fmt.Println(table) } - fmt.Println(table) return nil } diff --git a/cmd/lk/proto.go b/cmd/lk/proto.go index 4623608..38cd087 100644 --- a/cmd/lk/proto.go +++ b/cmd/lk/proto.go @@ -191,22 +191,22 @@ func listAndPrint[ return err } - table := CreateTable(). - Headers(header...) - for _, item := range res.GetItems() { - if item == nil { - continue - } - row := tableRow(item) - if len(row) == 0 { - continue + if cmd.Bool("json") { + PrintJSON(res.GetItems()) + } else { + table := CreateTable(). + Headers(header...) + for _, item := range res.GetItems() { + if item == nil { + continue + } + row := tableRow(item) + if len(row) == 0 { + continue + } + table.Row(row...) } - table.Row(row...) - } - fmt.Println(table) - - if cmd.Bool("verbose") { - PrintJSON(res) + fmt.Println(table) } return nil diff --git a/cmd/lk/replay.go b/cmd/lk/replay.go index 6d49dbe..fd092f2 100644 --- a/cmd/lk/replay.go +++ b/cmd/lk/replay.go @@ -27,6 +27,7 @@ var ( Name: "list", Before: createReplayClient, Action: listReplays, + Flags: []cli.Flag{jsonFlag}, }, { Name: "load", @@ -114,7 +115,7 @@ func createReplayClient(ctx context.Context, cmd *cli.Command) error { return nil } -func listReplays(ctx context.Context, _ *cli.Command) error { +func listReplays(ctx context.Context, cmd *cli.Command) error { ctx, err := replayClient.withAuth(ctx) if err != nil { return err @@ -126,11 +127,15 @@ func listReplays(ctx context.Context, _ *cli.Command) error { return err } - table := CreateTable().Headers("ReplayID") - for _, info := range res.Replays { - table.Row(info.ReplayId) + if cmd.Bool("json") { + PrintJSON(res.Replays) + } else { + table := CreateTable().Headers("ReplayID") + for _, info := range res.Replays { + table.Row(info.ReplayId) + } + fmt.Println(table) } - fmt.Println(table) return nil } diff --git a/cmd/lk/room.go b/cmd/lk/room.go index 9d1e82c..90b0497 100644 --- a/cmd/lk/room.go +++ b/cmd/lk/room.go @@ -109,6 +109,7 @@ var ( Before: createRoomClient, Action: listRooms, ArgsUsage: "[ROOM_NAME ...]", + Flags: []cli.Flag{jsonFlag}, }, { Name: "update", @@ -641,6 +642,13 @@ func createRoom(ctx context.Context, cmd *cli.Command) error { func listRooms(ctx context.Context, cmd *cli.Command) error { names, _ := extractArgs(cmd) + if cmd.Bool("verbose") && len(names) > 0 { + fmt.Printf( + "Querying rooms matching %s", + strings.Join(mapStrings(names, wrapWith("\"")), ", "), + ) + } + req := livekit.ListRoomsRequest{} if len(names) > 0 { req.Names = names @@ -650,19 +658,22 @@ func listRooms(ctx context.Context, cmd *cli.Command) error { if err != nil { return err } - if len(res.Rooms) == 0 { - if len(names) > 0 { - fmt.Printf( - "there are no rooms matching %s", - strings.Join(mapStrings(names, wrapWith("\"")), ", "), + + if cmd.Bool("json") { + PrintJSON(res.Rooms) + } else { + table := CreateTable().Headers("RoomID", "Name", "Participants", "Publishers") + for _, rm := range res.Rooms { + table.Row( + rm.Sid, + rm.Name, + fmt.Sprintf("%d", rm.NumParticipants), + fmt.Sprintf("%d", rm.NumPublishers), ) - } else { - fmt.Println("there are no active rooms") } + fmt.Println(table) } - for _, rm := range res.Rooms { - fmt.Printf("%s\t%s\t%d participants\n", rm.Sid, rm.Name, rm.NumParticipants) - } + return nil } diff --git a/cmd/lk/sip.go b/cmd/lk/sip.go index 4220c00..4b5bf22 100644 --- a/cmd/lk/sip.go +++ b/cmd/lk/sip.go @@ -52,6 +52,7 @@ var ( Name: "list", Usage: "List all inbound SIP Trunks", Action: listSipInboundTrunk, + Flags: []cli.Flag{jsonFlag}, }, { Name: "create", @@ -77,6 +78,7 @@ var ( Name: "list", Usage: "List all outbound SIP Trunk", Action: listSipOutboundTrunk, + Flags: []cli.Flag{jsonFlag}, }, { Name: "create", @@ -102,6 +104,7 @@ var ( Name: "list", Usage: "List all SIP Dispatch Rule", Action: listSipDispatchRule, + Flags: []cli.Flag{jsonFlag}, }, { Name: "create", diff --git a/cmd/lk/utils.go b/cmd/lk/utils.go index cb79438..8127abf 100644 --- a/cmd/lk/utils.go +++ b/cmd/lk/utils.go @@ -48,6 +48,11 @@ var ( Usage: "`ID` of participant", Required: true, } + jsonFlag = &cli.BoolFlag{ + Name: "json", + Aliases: []string{"j"}, + Usage: "Output as JSON", + } printCurl bool persistentFlags = []cli.Flag{ &cli.StringFlag{ @@ -273,7 +278,7 @@ func loadProjectDetails(c *cli.Command, opts ...loadOption) (*config.ProjectConf if os.Getenv("LIVEKIT_API_SECRET") == pc.APISecret { envVars = append(envVars, "api-secret") } - if len(envVars) > 0 { + if c.Bool("verbose") && len(envVars) > 0 { fmt.Printf("Using %s from environment\n", strings.Join(envVars, ", ")) logDetails(c, pc) } @@ -283,8 +288,10 @@ func loadProjectDetails(c *cli.Command, opts ...loadOption) (*config.ProjectConf // load default project dp, err := config.LoadDefaultProject() if err == nil { - fmt.Println("Using default project [" + theme.Focused.Title.Render(dp.Name) + "]") - logDetails(c, dp) + if c.Bool("verbose") { + fmt.Println("Using default project [" + theme.Focused.Title.Render(dp.Name) + "]") + logDetails(c, dp) + } return dp, nil } diff --git a/cmd/lk/utils_test.go b/cmd/lk/utils_test.go new file mode 100644 index 0000000..6ded636 --- /dev/null +++ b/cmd/lk/utils_test.go @@ -0,0 +1,58 @@ +package main + +import ( + "slices" + "strings" + "testing" + + "github.com/urfave/cli/v3" +) + +func TestOptionalFlag(t *testing.T) { + requiredFlag := &cli.StringFlag{ + Name: "test", + Required: true, + } + optionalFlag := optional(requiredFlag) + + if requiredFlag == optionalFlag { + t.Error("optional should return a new flag") + } + if !requiredFlag.Required { + t.Error("optional should not mutate the original flag") + } + if optionalFlag.Required { + t.Error("optional should return a new flag with Required set to false") + } +} + +func TestHiddenFlag(t *testing.T) { + visibleFlag := &cli.StringFlag{ + Name: "test", + Hidden: false, + } + hiddenFlag := hidden(visibleFlag) + + if visibleFlag == hiddenFlag { + t.Error("hidden should return a new flag") + } + if visibleFlag.Hidden { + t.Error("hidden should not mutate the original flag") + } + if !hiddenFlag.Hidden { + t.Error("hidden should return a new flag with Hidden set to true") + } +} + +func TestMapStrings(t *testing.T) { + initial := []string{"a1", "b2", "c3"} + mapped := mapStrings(initial, func(s string) string { + return strings.ToUpper(s) + }) + if len(mapped) != len(initial) { + t.Error("mapStrings should return a slice of the same length") + } + if !slices.Equal([]string{"A1", "B2", "C3"}, mapped) { + t.Error("mapStrings should apply the function to all elements") + } +}