From 1aa4e186120d73e5fc5a13eeb382d5ad92ffea62 Mon Sep 17 00:00:00 2001 From: Raphael Fakhri <153192858+RaphaelFakhri@users.noreply.github.com> Date: Tue, 29 Sep 2026 09:50:27 +0000 Subject: [PATCH] fix(cli): keep commas in --attribute values --- cmd/lk/perf.go | 7 ++--- cmd/lk/room.go | 11 ++++---- cmd/lk/token.go | 60 ++++++++++++++++++++++++++----------------- cmd/lk/token_test.go | 61 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 107 insertions(+), 32 deletions(-) diff --git a/cmd/lk/perf.go b/cmd/lk/perf.go index 219ab03b9..eab367987 100644 --- a/cmd/lk/perf.go +++ b/cmd/lk/perf.go @@ -88,9 +88,10 @@ var ( }, }, { - Name: "agent-load-test", - Usage: "Run load tests for a running agent", - Action: agentLoadTest, + Name: "agent-load-test", + Usage: "Run load tests for a running agent", + Action: agentLoadTest, + DisableSliceFlagSeparator: true, Flags: []cli.Flag{ &cli.IntFlag{ Name: "rooms", diff --git a/cmd/lk/room.go b/cmd/lk/room.go index 767bf0fd1..0a8db13d9 100644 --- a/cmd/lk/room.go +++ b/cmd/lk/room.go @@ -139,11 +139,12 @@ var ( ArgsUsage: "ROOM_NAME", }, { - Name: "join", - Usage: "Joins a room as a participant", - UsageText: "lk room join [OPTIONS] ROOM_NAME", - Action: joinRoom, - ArgsUsage: "ROOM_NAME", + Name: "join", + Usage: "Joins a room as a participant", + UsageText: "lk room join [OPTIONS] ROOM_NAME", + Action: joinRoom, + ArgsUsage: "ROOM_NAME", + DisableSliceFlagSeparator: true, Flags: []cli.Flag{ optional(identityFlag), optional(roomFlag), diff --git a/cmd/lk/token.go b/cmd/lk/token.go index 479c1a0c9..1edbb738f 100644 --- a/cmd/lk/token.go +++ b/cmd/lk/token.go @@ -23,6 +23,7 @@ import ( "maps" "os" "slices" + "strings" "time" "charm.land/huh/v2" @@ -66,9 +67,10 @@ var ( Before: loadProjectConfig, Commands: []*cli.Command{ { - Name: "create", - Usage: "Creates an access token", - Action: createToken, + Name: "create", + Usage: "Creates an access token", + Action: createToken, + DisableSliceFlagSeparator: true, Flags: []cli.Flag{ optional(roomFlag), optional(identityFlag), @@ -157,10 +159,11 @@ var ( // Deprecated commands kept for compatibility { - Hidden: true, // deprecated: use `token create` - Name: "create-token", - Usage: "Creates an access token", - Action: createToken, + Hidden: true, // deprecated: use `token create` + Name: "create-token", + Usage: "Creates an access token", + Action: createToken, + DisableSliceFlagSeparator: true, Flags: []cli.Flag{ optional(roomFlag), &cli.BoolFlag{ @@ -241,6 +244,29 @@ var ( } ) +// parseAllowSources converts --allow-source values to track sources. The token commands disable the +// slice flag separator so that --attribute values keep their commas, so split comma-separated sources here. +func parseAllowSources(values []string) ([]livekit.TrackSource, error) { + var sources []livekit.TrackSource + for _, value := range values { + for _, s := range strings.Split(value, ",") { + switch s { + case "camera": + sources = append(sources, livekit.TrackSource_CAMERA) + case "microphone": + sources = append(sources, livekit.TrackSource_MICROPHONE) + case "screen_share": + sources = append(sources, livekit.TrackSource_SCREEN_SHARE) + case "screen_share_audio": + sources = append(sources, livekit.TrackSource_SCREEN_SHARE_AUDIO) + default: + return nil, fmt.Errorf("invalid source: %s", s) + } + } + } + return sources, nil +} + func createToken(ctx context.Context, c *cli.Command) error { tokenOnly := c.Bool("token-only") jsonOutput := c.Bool("json") @@ -324,23 +350,9 @@ func createToken(ctx context.Context, c *cli.Command) error { hasPerms = true } if c.IsSet("allow-source") { - sourcesStr := c.StringSlice("allow-source") - sources := make([]livekit.TrackSource, 0, len(sourcesStr)) - for _, s := range sourcesStr { - var source livekit.TrackSource - switch s { - case "camera": - source = livekit.TrackSource_CAMERA - case "microphone": - source = livekit.TrackSource_MICROPHONE - case "screen_share": - source = livekit.TrackSource_SCREEN_SHARE - case "screen_share_audio": - source = livekit.TrackSource_SCREEN_SHARE_AUDIO - default: - return fmt.Errorf("invalid source: %s", s) - } - sources = append(sources, source) + sources, err := parseAllowSources(c.StringSlice("allow-source")) + if err != nil { + return err } grant.SetCanPublishSources(sources) } diff --git a/cmd/lk/token_test.go b/cmd/lk/token_test.go index 8c39cd582..599dc924e 100644 --- a/cmd/lk/token_test.go +++ b/cmd/lk/token_test.go @@ -8,6 +8,7 @@ import ( "testing" "github.com/livekit/protocol/auth" + "github.com/livekit/protocol/livekit" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/urfave/cli/v3" @@ -105,3 +106,63 @@ func slicesContains(items []string, item string) bool { } return false } + +// collectAttributeCommands returns every command in the tree that defines an "attribute" string slice flag. +func collectAttributeCommands(commands []*cli.Command) []*cli.Command { + var found []*cli.Command + for _, cmd := range commands { + if cmd == nil { + continue + } + for _, flag := range cmd.Flags { + if f, ok := flag.(*cli.StringSliceFlag); ok && f.Name == "attribute" { + found = append(found, cmd) + break + } + } + found = append(found, collectAttributeCommands(cmd.Commands)...) + } + return found +} + +func TestAttributeFlagKeepsCommasInValues(t *testing.T) { + var cmds []*cli.Command + cmds = append(cmds, collectAttributeCommands(TokenCommands)...) + cmds = append(cmds, collectAttributeCommands(RoomCommands)...) + cmds = append(cmds, collectAttributeCommands(PerfCommands)...) + require.NotEmpty(t, cmds) + + for _, c := range cmds { + t.Run(c.Name, func(t *testing.T) { + var got map[string]string + var parseErr error + // Run the real command's flags and slice separator setting with a stand-in action. + cmd := &cli.Command{ + Name: c.Name, + Flags: c.Flags, + DisableSliceFlagSeparator: c.DisableSliceFlagSeparator, + Action: func(_ context.Context, cmd *cli.Command) error { + got, parseErr = parseKeyValuePairs(cmd, "attribute") + return nil + }, + } + err := cmd.Run(context.Background(), []string{c.Name, "--attribute", "tags=a,b", "--attribute", "mode=x"}) + require.NoError(t, err) + require.NoError(t, parseErr) + assert.Equal(t, map[string]string{"tags": "a,b", "mode": "x"}, got) + }) + } +} + +func TestParseAllowSources(t *testing.T) { + sources, err := parseAllowSources([]string{"camera,microphone", "screen_share"}) + require.NoError(t, err) + assert.Equal(t, []livekit.TrackSource{ + livekit.TrackSource_CAMERA, + livekit.TrackSource_MICROPHONE, + livekit.TrackSource_SCREEN_SHARE, + }, sources) + + _, err = parseAllowSources([]string{"camera,video"}) + require.Error(t, err) +}