From c42cabba90e48fcfe0c63bc43d93ed52387b90c2 Mon Sep 17 00:00:00 2001 From: Nick Thompson Date: Mon, 9 Mar 2026 17:01:18 -0400 Subject: [PATCH 1/2] feat: add machine-friendly output modes to token create --- autocomplete/fish_autocomplete | 4 + cmd/lk/token.go | 108 ++++++++++++++++++++---- cmd/lk/token_test.go | 146 +++++++++++++++++++++++++++++++++ 3 files changed, 243 insertions(+), 15 deletions(-) create mode 100644 cmd/lk/token_test.go diff --git a/autocomplete/fish_autocomplete b/autocomplete/fish_autocomplete index 4fbef50e..61b7c0d8 100644 --- a/autocomplete/fish_autocomplete +++ b/autocomplete/fish_autocomplete @@ -295,6 +295,8 @@ complete -c lk -n '__fish_seen_subcommand_from create' -f -l allow-source -r -d complete -c lk -n '__fish_seen_subcommand_from create' -f -l identity -s i -r -d 'Unique `ID` of the participant, used with --join' complete -c lk -n '__fish_seen_subcommand_from create' -f -l name -s n -r -d '`NAME` of the participant, used with --join. defaults to identity' complete -c lk -n '__fish_seen_subcommand_from create' -f -l room -s r -r -d '`NAME` of the room to join' +complete -c lk -n '__fish_seen_subcommand_from create' -f -l json -s j -d 'Output as JSON' +complete -c lk -n '__fish_seen_subcommand_from create' -f -l token-only -d 'Output only the access token' complete -c lk -n '__fish_seen_subcommand_from create' -f -l metadata -r -d '`JSON` metadata to encode in the token, will be passed to participant' complete -c lk -n '__fish_seen_subcommand_from create' -f -l attribute -r -d 'set attributes in key=value format, can be used multiple times' complete -c lk -n '__fish_seen_subcommand_from create' -l attribute-file -r -d 'read attributes from a `JSON` file' @@ -315,6 +317,8 @@ complete -c lk -n '__fish_seen_subcommand_from create-token' -f -l allow-source complete -c lk -n '__fish_seen_subcommand_from create-token' -f -l identity -s i -r -d 'Unique `ID` of the participant, used with --join' complete -c lk -n '__fish_seen_subcommand_from create-token' -f -l name -s n -r -d '`NAME` of the participant, used with --join. defaults to identity' complete -c lk -n '__fish_seen_subcommand_from create-token' -f -l room -s r -r -d '`NAME` of the room to join' +complete -c lk -n '__fish_seen_subcommand_from create-token' -f -l json -s j -d 'Output as JSON' +complete -c lk -n '__fish_seen_subcommand_from create-token' -f -l token-only -d 'Output only the access token' complete -c lk -n '__fish_seen_subcommand_from create-token' -f -l room-configuration -r -d 'name of the room configuration to use when creating a room' complete -c lk -n '__fish_seen_subcommand_from create-token' -f -l metadata -r -d '`JSON` metadata to encode in the token, will be passed to participant' complete -c lk -n '__fish_seen_subcommand_from create-token' -f -l attribute -r -d 'set attributes in key=value format, can be used multiple times' diff --git a/cmd/lk/token.go b/cmd/lk/token.go index 999161db..829bbf2c 100644 --- a/cmd/lk/token.go +++ b/cmd/lk/token.go @@ -33,17 +33,22 @@ import ( ) const ( - usageCreate = "Ability to create or delete rooms" - usageList = "Ability to list rooms" - usageJoin = "Ability to join a room (requires --room and --identity)" - usageAdmin = "Ability to moderate a room (requires --room)" - usageEgress = "Ability to interact with Egress services" - usageIngress = "Ability to interact with Ingress services" + usageCreate = "Ability to create or delete rooms" + usageList = "Ability to list rooms" + usageJoin = "Ability to join a room (requires --room and --identity)" + usageAdmin = "Ability to moderate a room (requires --room)" + usageEgress = "Ability to interact with Egress services" + usageIngress = "Ability to interact with Ingress services" usageMetadata = "Ability to update their own name and metadata" usageInference = "Ability to perform inference (AI endpoints)" ) var ( + tokenOnlyFlag = &cli.BoolFlag{ + Name: "token-only", + Usage: "Output only the access token", + } + TokenCommands = []*cli.Command{ { Name: "token", @@ -58,6 +63,8 @@ var ( optional(roomFlag), optional(identityFlag), openFlag, + jsonFlag, + tokenOnlyFlag, &cli.BoolFlag{ Name: "create", @@ -143,6 +150,8 @@ var ( Action: createToken, Flags: []cli.Flag{ optional(roomFlag), + jsonFlag, + tokenOnlyFlag, &cli.BoolFlag{ Name: "create", @@ -222,6 +231,11 @@ var ( ) func createToken(ctx context.Context, c *cli.Command) error { + outputMode, err := resolveTokenCreateOutputMode(c) + if err != nil { + return err + } + name := c.String("name") metadata := c.String("metadata") validFor := c.String("valid-for") @@ -254,13 +268,17 @@ func createToken(ctx context.Context, c *cli.Command) error { participant := c.String("identity") if participant == "" { participant = util.ExpandTemplate("participant-%x") - fmt.Printf("Using generated participant identity [%s]\n", util.Accented(participant)) + if outputMode == tokenOutputModeHuman { + fmt.Printf("Using generated participant identity [%s]\n", util.Accented(participant)) + } } room := c.String("room") if room == "" { room = util.ExpandTemplate("room-%t") - fmt.Printf("Using generated room name [%s]\n", util.Accented(room)) + if outputMode == tokenOutputModeHuman { + fmt.Printf("Using generated room name [%s]\n", util.Accented(room)) + } } grant := &auth.VideoGrant{ @@ -419,7 +437,9 @@ func createToken(ctx context.Context, c *cli.Command) error { at.SetName(name) if validFor != "" { if dur, err := time.ParseDuration(validFor); err == nil { - fmt.Println("valid for (mins): ", int(dur/time.Minute)) + if outputMode == tokenOutputModeHuman { + fmt.Println("valid for (mins): ", int(dur/time.Minute)) + } at.SetValidFor(dur) } else { return err @@ -431,13 +451,16 @@ func createToken(ctx context.Context, c *cli.Command) error { return err } - fmt.Println("Token grants:") - util.PrintJSON(at.GetGrants()) - fmt.Println() - if project.URL != "" { - fmt.Println("Project URL:", project.URL) + if err = printTokenCreateOutput(outputMode, tokenCreateOutput{ + AccessToken: token, + ProjectURL: project.URL, + Identity: participant, + Name: name, + Room: room, + Grants: at.GetGrants(), + }); err != nil { + return err } - fmt.Println("Access token:", token) if c.IsSet("open") { switch c.String("open") { @@ -459,3 +482,58 @@ func accessToken(apiKey, apiSecret string, grant *auth.VideoGrant, identity stri SetIdentity(identity) return at } + +type tokenOutputMode string + +const ( + tokenOutputModeHuman tokenOutputMode = "human" + tokenOutputModeJSON tokenOutputMode = "json" + tokenOutputModeTokenOnly tokenOutputMode = "token-only" +) + +type tokenCreateOutput struct { + AccessToken string `json:"access_token"` + ProjectURL string `json:"project_url,omitempty"` + Identity string `json:"identity"` + Name string `json:"name"` + Room string `json:"room"` + Grants *auth.ClaimGrants `json:"grants"` +} + +func resolveTokenCreateOutputMode(c *cli.Command) (tokenOutputMode, error) { + jsonOutput := c.Bool("json") + tokenOnly := c.Bool("token-only") + + if jsonOutput && tokenOnly { + return "", errors.New("cannot combine --json and --token-only") + } + + if tokenOnly { + return tokenOutputModeTokenOnly, nil + } + if jsonOutput { + return tokenOutputModeJSON, nil + } + return tokenOutputModeHuman, nil +} + +func printTokenCreateOutput(mode tokenOutputMode, out tokenCreateOutput) error { + switch mode { + case tokenOutputModeTokenOnly: + fmt.Println(out.AccessToken) + case tokenOutputModeJSON: + util.PrintJSON(out) + case tokenOutputModeHuman: + fmt.Println("Token grants:") + util.PrintJSON(out.Grants) + fmt.Println() + if out.ProjectURL != "" { + fmt.Println("Project URL:", out.ProjectURL) + } + fmt.Println("Access token:", out.AccessToken) + default: + return fmt.Errorf("unknown token output mode: %s", mode) + } + + return nil +} diff --git a/cmd/lk/token_test.go b/cmd/lk/token_test.go new file mode 100644 index 00000000..af45c277 --- /dev/null +++ b/cmd/lk/token_test.go @@ -0,0 +1,146 @@ +package main + +import ( + "context" + "encoding/json" + "io" + "os" + "strings" + "testing" + + "github.com/livekit/protocol/auth" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/urfave/cli/v3" +) + +func TestTokenCommandTree(t *testing.T) { + tokenCmd := findCommandByName(TokenCommands, "token") + require.NotNil(t, tokenCmd, "top-level 'token' command must exist") + + createCmd := findCommandByName(tokenCmd.Commands, "create") + require.NotNil(t, createCmd, "'token create' command must exist") + require.NotNil(t, createCmd.Action, "'token create' must have an action") + assert.True(t, commandHasFlag(createCmd, "json"), "'token create' must have --json") + assert.True(t, commandHasFlag(createCmd, "token-only"), "'token create' must have --token-only") + + deprecatedCreateCmd := findCommandByName(TokenCommands, "create-token") + require.NotNil(t, deprecatedCreateCmd, "deprecated 'create-token' command must exist") + assert.True(t, commandHasFlag(deprecatedCreateCmd, "json"), "'create-token' must have --json") + assert.True(t, commandHasFlag(deprecatedCreateCmd, "token-only"), "'create-token' must have --token-only") +} + +func TestResolveTokenCreateOutputMode(t *testing.T) { + cmd := parseTokenOutputFlags(t) + mode, err := resolveTokenCreateOutputMode(cmd) + require.NoError(t, err) + assert.Equal(t, tokenOutputModeHuman, mode) + + cmd = parseTokenOutputFlags(t, "--json") + mode, err = resolveTokenCreateOutputMode(cmd) + require.NoError(t, err) + assert.Equal(t, tokenOutputModeJSON, mode) + + cmd = parseTokenOutputFlags(t, "--token-only") + mode, err = resolveTokenCreateOutputMode(cmd) + require.NoError(t, err) + assert.Equal(t, tokenOutputModeTokenOnly, mode) + + cmd = parseTokenOutputFlags(t, "--json", "--token-only") + _, err = resolveTokenCreateOutputMode(cmd) + require.Error(t, err) + assert.Contains(t, err.Error(), "cannot combine --json and --token-only") +} + +func TestPrintTokenCreateOutput(t *testing.T) { + out := tokenCreateOutput{ + AccessToken: "token-value", + ProjectURL: "https://example.livekit.cloud", + Identity: "test-id", + Name: "test-name", + Room: "test-room", + Grants: &auth.ClaimGrants{Identity: "test-id"}, + } + + stdout := captureStdout(t, func() { + err := printTokenCreateOutput(tokenOutputModeTokenOnly, out) + require.NoError(t, err) + }) + assert.Equal(t, "token-value\n", stdout) + + stdout = captureStdout(t, func() { + err := printTokenCreateOutput(tokenOutputModeJSON, out) + require.NoError(t, err) + }) + var decoded map[string]any + require.NoError(t, json.Unmarshal([]byte(stdout), &decoded)) + assert.Equal(t, "token-value", decoded["access_token"]) + assert.Equal(t, "https://example.livekit.cloud", decoded["project_url"]) + assert.Equal(t, "test-id", decoded["identity"]) + + stdout = captureStdout(t, func() { + err := printTokenCreateOutput(tokenOutputModeHuman, out) + require.NoError(t, err) + }) + assert.Contains(t, stdout, "Token grants:") + assert.Contains(t, stdout, "Project URL: https://example.livekit.cloud") + assert.Contains(t, stdout, "Access token: token-value") +} + +func commandHasFlag(cmd *cli.Command, flagName string) bool { + for _, flag := range cmd.Flags { + if slicesContains(flag.Names(), flagName) { + return true + } + } + return false +} + +func parseTokenOutputFlags(t *testing.T, args ...string) *cli.Command { + t.Helper() + + var parsedCmd *cli.Command + app := &cli.Command{ + Name: "lk", + Flags: []cli.Flag{jsonFlag, tokenOnlyFlag}, + Action: func(ctx context.Context, cmd *cli.Command) error { + parsedCmd = cmd + return nil + }, + } + + runArgs := append([]string{"lk"}, args...) + require.NoError(t, app.Run(context.Background(), runArgs)) + require.NotNil(t, parsedCmd) + return parsedCmd +} + +func captureStdout(t *testing.T, fn func()) string { + t.Helper() + + originalStdout := os.Stdout + r, w, err := os.Pipe() + require.NoError(t, err) + os.Stdout = w + + defer func() { + os.Stdout = originalStdout + }() + + fn() + + require.NoError(t, w.Close()) + out, err := io.ReadAll(r) + require.NoError(t, err) + require.NoError(t, r.Close()) + return string(out) +} + +func slicesContains(items []string, item string) bool { + for _, current := range items { + if strings.EqualFold(current, item) { + return true + } + } + return false +} From a196a11cd8ac6b0e13336cfd20880be9a1fa62a5 Mon Sep 17 00:00:00 2001 From: Nick Thompson Date: Wed, 11 Mar 2026 22:09:45 -0400 Subject: [PATCH 2/2] pr feedback --- cmd/lk/token.go | 90 ++++++++++++++--------------------- cmd/lk/token_test.go | 111 ++++++++++++++----------------------------- pkg/util/json.go | 17 +++++-- 3 files changed, 86 insertions(+), 132 deletions(-) diff --git a/cmd/lk/token.go b/cmd/lk/token.go index 829bbf2c..db123cb3 100644 --- a/cmd/lk/token.go +++ b/cmd/lk/token.go @@ -19,6 +19,7 @@ import ( "encoding/json" "errors" "fmt" + "io" "os" "slices" "time" @@ -49,6 +50,14 @@ var ( Usage: "Output only the access token", } + tokenOutputMutuallyExclusiveFlags = []cli.MutuallyExclusiveFlags{{ + Flags: [][]cli.Flag{{ + jsonFlag, + }, { + tokenOnlyFlag, + }}, + }} + TokenCommands = []*cli.Command{ { Name: "token", @@ -63,8 +72,6 @@ var ( optional(roomFlag), optional(identityFlag), openFlag, - jsonFlag, - tokenOnlyFlag, &cli.BoolFlag{ Name: "create", @@ -138,6 +145,7 @@ var ( Usage: "Metadata attached to job dispatched to the agent (ctx.job.metadata)", }, }, + MutuallyExclusiveFlags: tokenOutputMutuallyExclusiveFlags, }, }, }, @@ -150,8 +158,6 @@ var ( Action: createToken, Flags: []cli.Flag{ optional(roomFlag), - jsonFlag, - tokenOnlyFlag, &cli.BoolFlag{ Name: "create", @@ -226,15 +232,16 @@ var ( Usage: "Additional `VIDEO_GRANT` fields. It'll be merged with other arguments (JSON formatted)", }, }, + MutuallyExclusiveFlags: tokenOutputMutuallyExclusiveFlags, }, } ) func createToken(ctx context.Context, c *cli.Command) error { - outputMode, err := resolveTokenCreateOutputMode(c) - if err != nil { - return err - } + tokenOnly := c.Bool("token-only") + jsonOutput := c.Bool("json") + stdout := c.Root().Writer + stderr := c.Root().ErrWriter name := c.String("name") metadata := c.String("metadata") @@ -268,16 +275,16 @@ func createToken(ctx context.Context, c *cli.Command) error { participant := c.String("identity") if participant == "" { participant = util.ExpandTemplate("participant-%x") - if outputMode == tokenOutputModeHuman { - fmt.Printf("Using generated participant identity [%s]\n", util.Accented(participant)) + if !tokenOnly && !jsonOutput { + fmt.Fprintf(stderr, "Using generated participant identity [%s]\n", util.Accented(participant)) } } room := c.String("room") if room == "" { room = util.ExpandTemplate("room-%t") - if outputMode == tokenOutputModeHuman { - fmt.Printf("Using generated room name [%s]\n", util.Accented(room)) + if !tokenOnly && !jsonOutput { + fmt.Fprintf(stderr, "Using generated room name [%s]\n", util.Accented(room)) } } @@ -437,8 +444,8 @@ func createToken(ctx context.Context, c *cli.Command) error { at.SetName(name) if validFor != "" { if dur, err := time.ParseDuration(validFor); err == nil { - if outputMode == tokenOutputModeHuman { - fmt.Println("valid for (mins): ", int(dur/time.Minute)) + if !tokenOnly && !jsonOutput { + fmt.Fprintf(stderr, "valid for (mins): %d\n", int(dur/time.Minute)) } at.SetValidFor(dur) } else { @@ -451,7 +458,7 @@ func createToken(ctx context.Context, c *cli.Command) error { return err } - if err = printTokenCreateOutput(outputMode, tokenCreateOutput{ + if err = printTokenCreateOutput(stdout, tokenOnly, jsonOutput, tokenCreateOutput{ AccessToken: token, ProjectURL: project.URL, Identity: participant, @@ -483,14 +490,6 @@ func accessToken(apiKey, apiSecret string, grant *auth.VideoGrant, identity stri return at } -type tokenOutputMode string - -const ( - tokenOutputModeHuman tokenOutputMode = "human" - tokenOutputModeJSON tokenOutputMode = "json" - tokenOutputModeTokenOnly tokenOutputMode = "token-only" -) - type tokenCreateOutput struct { AccessToken string `json:"access_token"` ProjectURL string `json:"project_url,omitempty"` @@ -500,39 +499,22 @@ type tokenCreateOutput struct { Grants *auth.ClaimGrants `json:"grants"` } -func resolveTokenCreateOutputMode(c *cli.Command) (tokenOutputMode, error) { - jsonOutput := c.Bool("json") - tokenOnly := c.Bool("token-only") - - if jsonOutput && tokenOnly { - return "", errors.New("cannot combine --json and --token-only") - } - - if tokenOnly { - return tokenOutputModeTokenOnly, nil - } - if jsonOutput { - return tokenOutputModeJSON, nil - } - return tokenOutputModeHuman, nil -} - -func printTokenCreateOutput(mode tokenOutputMode, out tokenCreateOutput) error { - switch mode { - case tokenOutputModeTokenOnly: - fmt.Println(out.AccessToken) - case tokenOutputModeJSON: - util.PrintJSON(out) - case tokenOutputModeHuman: - fmt.Println("Token grants:") - util.PrintJSON(out.Grants) - fmt.Println() +func printTokenCreateOutput(w io.Writer, tokenOnly, jsonOutput bool, out tokenCreateOutput) error { + switch { + case tokenOnly: + _, _ = fmt.Fprintln(w, out.AccessToken) + case jsonOutput: + return util.PrintJSONTo(w, out) + default: + _, _ = fmt.Fprintln(w, "Token grants:") + if err := util.PrintJSONTo(w, out.Grants); err != nil { + return err + } + _, _ = fmt.Fprintln(w) if out.ProjectURL != "" { - fmt.Println("Project URL:", out.ProjectURL) + _, _ = fmt.Fprintln(w, "Project URL:", out.ProjectURL) } - fmt.Println("Access token:", out.AccessToken) - default: - return fmt.Errorf("unknown token output mode: %s", mode) + _, _ = fmt.Fprintln(w, "Access token:", out.AccessToken) } return nil diff --git a/cmd/lk/token_test.go b/cmd/lk/token_test.go index af45c277..8c39cd58 100644 --- a/cmd/lk/token_test.go +++ b/cmd/lk/token_test.go @@ -1,10 +1,9 @@ package main import ( + "bytes" "context" "encoding/json" - "io" - "os" "strings" "testing" @@ -30,26 +29,21 @@ func TestTokenCommandTree(t *testing.T) { assert.True(t, commandHasFlag(deprecatedCreateCmd, "token-only"), "'create-token' must have --token-only") } -func TestResolveTokenCreateOutputMode(t *testing.T) { - cmd := parseTokenOutputFlags(t) - mode, err := resolveTokenCreateOutputMode(cmd) - require.NoError(t, err) - assert.Equal(t, tokenOutputModeHuman, mode) - - cmd = parseTokenOutputFlags(t, "--json") - mode, err = resolveTokenCreateOutputMode(cmd) - require.NoError(t, err) - assert.Equal(t, tokenOutputModeJSON, mode) - - cmd = parseTokenOutputFlags(t, "--token-only") - mode, err = resolveTokenCreateOutputMode(cmd) - require.NoError(t, err) - assert.Equal(t, tokenOutputModeTokenOnly, mode) +func TestTokenOutputFlagsAreMutuallyExclusive(t *testing.T) { + var actionCalled bool + app := &cli.Command{ + Name: "lk", + MutuallyExclusiveFlags: tokenOutputMutuallyExclusiveFlags, + Action: func(ctx context.Context, cmd *cli.Command) error { + actionCalled = true + return nil + }, + } - cmd = parseTokenOutputFlags(t, "--json", "--token-only") - _, err = resolveTokenCreateOutputMode(cmd) + err := app.Run(context.Background(), []string{"lk", "--json", "--token-only"}) require.Error(t, err) - assert.Contains(t, err.Error(), "cannot combine --json and --token-only") + assert.False(t, actionCalled) + assert.Contains(t, err.Error(), "option json cannot be set along with option token-only") } func TestPrintTokenCreateOutput(t *testing.T) { @@ -62,33 +56,30 @@ func TestPrintTokenCreateOutput(t *testing.T) { Grants: &auth.ClaimGrants{Identity: "test-id"}, } - stdout := captureStdout(t, func() { - err := printTokenCreateOutput(tokenOutputModeTokenOnly, out) - require.NoError(t, err) - }) - assert.Equal(t, "token-value\n", stdout) + var stdout bytes.Buffer + err := printTokenCreateOutput(&stdout, true, false, out) + require.NoError(t, err) + assert.Equal(t, "token-value\n", stdout.String()) - stdout = captureStdout(t, func() { - err := printTokenCreateOutput(tokenOutputModeJSON, out) - require.NoError(t, err) - }) + stdout.Reset() + err = printTokenCreateOutput(&stdout, false, true, out) + require.NoError(t, err) var decoded map[string]any - require.NoError(t, json.Unmarshal([]byte(stdout), &decoded)) + require.NoError(t, json.Unmarshal(stdout.Bytes(), &decoded)) assert.Equal(t, "token-value", decoded["access_token"]) assert.Equal(t, "https://example.livekit.cloud", decoded["project_url"]) assert.Equal(t, "test-id", decoded["identity"]) - stdout = captureStdout(t, func() { - err := printTokenCreateOutput(tokenOutputModeHuman, out) - require.NoError(t, err) - }) - assert.Contains(t, stdout, "Token grants:") - assert.Contains(t, stdout, "Project URL: https://example.livekit.cloud") - assert.Contains(t, stdout, "Access token: token-value") + stdout.Reset() + err = printTokenCreateOutput(&stdout, false, false, out) + require.NoError(t, err) + assert.Contains(t, stdout.String(), "Token grants:") + assert.Contains(t, stdout.String(), "Project URL: https://example.livekit.cloud") + assert.Contains(t, stdout.String(), "Access token: token-value") } func commandHasFlag(cmd *cli.Command, flagName string) bool { - for _, flag := range cmd.Flags { + for _, flag := range commandFlags(cmd) { if slicesContains(flag.Names(), flagName) { return true } @@ -96,44 +87,14 @@ func commandHasFlag(cmd *cli.Command, flagName string) bool { return false } -func parseTokenOutputFlags(t *testing.T, args ...string) *cli.Command { - t.Helper() - - var parsedCmd *cli.Command - app := &cli.Command{ - Name: "lk", - Flags: []cli.Flag{jsonFlag, tokenOnlyFlag}, - Action: func(ctx context.Context, cmd *cli.Command) error { - parsedCmd = cmd - return nil - }, +func commandFlags(cmd *cli.Command) []cli.Flag { + flags := append([]cli.Flag{}, cmd.Flags...) + for _, group := range cmd.MutuallyExclusiveFlags { + for _, path := range group.Flags { + flags = append(flags, path...) + } } - - runArgs := append([]string{"lk"}, args...) - require.NoError(t, app.Run(context.Background(), runArgs)) - require.NotNil(t, parsedCmd) - return parsedCmd -} - -func captureStdout(t *testing.T, fn func()) string { - t.Helper() - - originalStdout := os.Stdout - r, w, err := os.Pipe() - require.NoError(t, err) - os.Stdout = w - - defer func() { - os.Stdout = originalStdout - }() - - fn() - - require.NoError(t, w.Close()) - out, err := io.ReadAll(r) - require.NoError(t, err) - require.NoError(t, r.Close()) - return string(out) + return flags } func slicesContains(items []string, item string) bool { diff --git a/pkg/util/json.go b/pkg/util/json.go index ec96359e..159babad 100644 --- a/pkg/util/json.go +++ b/pkg/util/json.go @@ -17,18 +17,29 @@ package util import ( "encoding/json" "fmt" + "io" + "os" "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/proto" ) func PrintJSON(obj any) { + _ = PrintJSONTo(os.Stdout, obj) +} + +func PrintJSONTo(w io.Writer, obj any) error { const indent = " " var txt []byte + var err error if m, ok := obj.(proto.Message); ok { - txt, _ = protojson.MarshalOptions{Indent: indent}.Marshal(m) + txt, err = protojson.MarshalOptions{Indent: indent}.Marshal(m) } else { - txt, _ = json.MarshalIndent(obj, "", indent) + txt, err = json.MarshalIndent(obj, "", indent) + } + if err != nil { + return err } - fmt.Println(string(txt)) + _, err = fmt.Fprintln(w, string(txt)) + return err }