Skip to content

Commit 65d1bc3

Browse files
apucacaoclaude
andauthored
feat: add --json flag as shorthand for --output json (#656)
* feat: add --json flag as shorthand for --output json Adds a persistent --json boolean flag to all commands, matching the convention used by gh, kubectl, and other agent-friendly CLIs. Implements GetOutputKind(cmd) helper in cliflags that checks --json before falling back to --output, avoiding PersistentPreRun override issues with child commands. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: remove whoami command reference (lives on separate branch) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 24da87c commit 65d1bc3

File tree

12 files changed

+75
-22
lines changed

12 files changed

+75
-22
lines changed

cmd/cliflags/flags.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,18 @@
11
package cliflags
22

3+
import (
4+
"github.com/spf13/cobra"
5+
"github.com/spf13/viper"
6+
)
7+
8+
// GetOutputKind returns the effective output kind, giving precedence to --json over --output.
9+
func GetOutputKind(cmd *cobra.Command) string {
10+
if jsonFlag, err := cmd.Root().PersistentFlags().GetBool(JSONFlag); err == nil && jsonFlag {
11+
return "json"
12+
}
13+
return viper.GetString(OutputFlag)
14+
}
15+
316
const (
417
BaseURIDefault = "https://app.launchdarkly.com"
518
DevStreamURIDefault = "https://stream.launchdarkly.com"
@@ -15,6 +28,7 @@ const (
1528
EmailsFlag = "emails"
1629
EnvironmentFlag = "environment"
1730
FlagFlag = "flag"
31+
JSONFlag = "json"
1832
OutputFlag = "output"
1933
PortFlag = "port"
2034
ProjectFlag = "project"
@@ -29,6 +43,7 @@ const (
2943
DevStreamURIDescription = "Streaming service endpoint that the dev server uses to obtain authoritative flag data. This may be a LaunchDarkly or Relay Proxy endpoint"
3044
EnvironmentFlagDescription = "Default environment key"
3145
FlagFlagDescription = "Default feature flag key"
46+
JSONFlagDescription = "Output JSON format (shorthand for --output json)"
3247
OutputFlagDescription = "Command response output format in either JSON or plain text"
3348
PortFlagDescription = "Port for the dev server to run on"
3449
ProjectFlagDescription = "Default project key"

cmd/config/testdata/help.golden

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,5 @@ Global Flags:
2727
--access-token string LaunchDarkly access token with write-level access
2828
--analytics-opt-out Opt out of analytics tracking
2929
--base-uri string LaunchDarkly base URI (default "https://app.launchdarkly.com")
30+
--json Output JSON format (shorthand for --output json)
3031
-o, --output string Command response output format in either JSON or plain text (default "plaintext")

cmd/dev_server/overrides.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ func addOverride(client resources.Client) func(*cobra.Command, []string) error {
6464
jsonData,
6565
)
6666
if err != nil {
67-
return output.NewCmdOutputError(err, viper.GetString(cliflags.OutputFlag))
67+
return output.NewCmdOutputError(err, cliflags.GetOutputKind(cmd))
6868
}
6969

7070
fmt.Fprint(cmd.OutOrStdout(), string(res))
@@ -102,7 +102,7 @@ func deleteOverrides(client resources.Client) func(*cobra.Command, []string) err
102102
nil,
103103
)
104104
if err != nil {
105-
return output.NewCmdOutputError(err, viper.GetString(cliflags.OutputFlag))
105+
return output.NewCmdOutputError(err, cliflags.GetOutputKind(cmd))
106106
}
107107

108108
fmt.Fprint(cmd.OutOrStdout(), string(res))
@@ -145,7 +145,7 @@ func removeOverride(client resources.Client) func(*cobra.Command, []string) erro
145145
nil,
146146
)
147147
if err != nil {
148-
return output.NewCmdOutputError(err, viper.GetString(cliflags.OutputFlag))
148+
return output.NewCmdOutputError(err, cliflags.GetOutputKind(cmd))
149149
}
150150

151151
fmt.Fprint(cmd.OutOrStdout(), string(res))

cmd/dev_server/projects.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ func listProjects(client resources.Client) func(*cobra.Command, []string) error
3939
nil,
4040
)
4141
if err != nil {
42-
return output.NewCmdOutputError(err, viper.GetString(cliflags.OutputFlag))
42+
return output.NewCmdOutputError(err, cliflags.GetOutputKind(cmd))
4343
}
4444

4545
fmt.Fprint(cmd.OutOrStdout(), string(res))
@@ -107,7 +107,7 @@ func getProject(client resources.Client) func(*cobra.Command, []string) error {
107107
false, // not beta
108108
)
109109
if err != nil {
110-
return output.NewCmdOutputError(err, viper.GetString(cliflags.OutputFlag))
110+
return output.NewCmdOutputError(err, cliflags.GetOutputKind(cmd))
111111
}
112112
fmt.Fprint(cmd.OutOrStdout(), string(res))
113113

@@ -146,7 +146,7 @@ func syncProject(client resources.Client) func(*cobra.Command, []string) error {
146146
[]byte("{}"),
147147
)
148148
if err != nil {
149-
return output.NewCmdOutputError(err, viper.GetString(cliflags.OutputFlag))
149+
return output.NewCmdOutputError(err, cliflags.GetOutputKind(cmd))
150150
}
151151
_, err = fmt.Fprintf(cmd.OutOrStdout(), "'%s' project synced successfully\n", project)
152152
if err != nil {
@@ -187,7 +187,7 @@ func deleteProject(client resources.Client) func(*cobra.Command, []string) error
187187
nil,
188188
)
189189
if err != nil {
190-
return output.NewCmdOutputError(err, viper.GetString(cliflags.OutputFlag))
190+
return output.NewCmdOutputError(err, cliflags.GetOutputKind(cmd))
191191
}
192192

193193
fmt.Fprint(cmd.OutOrStdout(), string(res))
@@ -248,7 +248,7 @@ func addProject(client resources.Client) func(*cobra.Command, []string) error {
248248
jsonData,
249249
)
250250
if err != nil {
251-
return output.NewCmdOutputError(err, viper.GetString(cliflags.OutputFlag))
251+
return output.NewCmdOutputError(err, cliflags.GetOutputKind(cmd))
252252
}
253253

254254
fmt.Fprint(cmd.OutOrStdout(), string(res))
@@ -320,7 +320,7 @@ func updateProject(client resources.Client) func(*cobra.Command, []string) error
320320
jsonData,
321321
)
322322
if err != nil {
323-
return output.NewCmdOutputError(err, viper.GetString(cliflags.OutputFlag))
323+
return output.NewCmdOutputError(err, cliflags.GetOutputKind(cmd))
324324
}
325325

326326
var response patchResponse

cmd/flags/archive.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,10 @@ func makeArchiveRequest(client resources.Client) func(*cobra.Command, []string)
4848
false,
4949
)
5050
if err != nil {
51-
return output.NewCmdOutputError(err, viper.GetString(cliflags.OutputFlag))
51+
return output.NewCmdOutputError(err, cliflags.GetOutputKind(cmd))
5252
}
5353

54-
output, err := output.CmdOutput("update", viper.GetString(cliflags.OutputFlag), res)
54+
output, err := output.CmdOutput("update", cliflags.GetOutputKind(cmd), res)
5555
if err != nil {
5656
return errors.NewError(err.Error())
5757
}

cmd/flags/toggle.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,10 +70,10 @@ func runE(client resources.Client) func(*cobra.Command, []string) error {
7070
false,
7171
)
7272
if err != nil {
73-
return output.NewCmdOutputError(err, viper.GetString(cliflags.OutputFlag))
73+
return output.NewCmdOutputError(err, cliflags.GetOutputKind(cmd))
7474
}
7575

76-
output, err := output.CmdOutput("update", viper.GetString(cliflags.OutputFlag), res)
76+
output, err := output.CmdOutput("update", cliflags.GetOutputKind(cmd), res)
7777
if err != nil {
7878
return errors.NewError(err.Error())
7979
}

cmd/members/invite.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,10 +60,10 @@ func runE(client resources.Client) func(*cobra.Command, []string) error {
6060
false,
6161
)
6262
if err != nil {
63-
return output.NewCmdOutputError(err, viper.GetString(cliflags.OutputFlag))
63+
return output.NewCmdOutputError(err, cliflags.GetOutputKind(cmd))
6464
}
6565

66-
output, err := output.CmdOutput("update", viper.GetString(cliflags.OutputFlag), res)
66+
output, err := output.CmdOutput("update", cliflags.GetOutputKind(cmd), res)
6767
if err != nil {
6868
return errors.NewError(err.Error())
6969
}

cmd/resources/resources.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -340,7 +340,7 @@ func (op *OperationCmd) makeRequest(cmd *cobra.Command, args []string) error {
340340
op.IsBeta,
341341
)
342342
if err != nil {
343-
return output.NewCmdOutputError(err, viper.GetString(cliflags.OutputFlag))
343+
return output.NewCmdOutputError(err, cliflags.GetOutputKind(cmd))
344344
}
345345

346346
if string(res) == "" {
@@ -349,7 +349,7 @@ func (op *OperationCmd) makeRequest(cmd *cobra.Command, args []string) error {
349349
res = []byte(fmt.Sprintf(`{"key": %q}`, urlParms[len(urlParms)-1]))
350350
}
351351

352-
output, err := output.CmdOutput(cmd.Use, viper.GetString(cliflags.OutputFlag), res)
352+
output, err := output.CmdOutput(cmd.Use, cliflags.GetOutputKind(cmd), res)
353353
if err != nil {
354354
return errors.NewError(err.Error())
355355
}

cmd/root.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ func NewRootCommand(
108108
cmd.DisableFlagParsing = true
109109
}
110110
}
111+
111112
},
112113
Annotations: make(map[string]string),
113114
// Handle errors differently based on type.
@@ -198,6 +199,12 @@ func NewRootCommand(
198199
return nil, err
199200
}
200201

202+
cmd.PersistentFlags().Bool(
203+
cliflags.JSONFlag,
204+
false,
205+
cliflags.JSONFlagDescription,
206+
)
207+
201208
configCmd := configcmd.NewConfigCmd(configService, analyticsTrackerFn)
202209
cmd.AddCommand(configCmd.Cmd())
203210
cmd.AddCommand(NewQuickStartCmd(analyticsTrackerFn, clients.EnvironmentsClient, clients.FlagsClient))

cmd/root_test.go

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
package cmd_test
22

33
import (
4-
"github.com/launchdarkly/ldcli/cmd"
5-
"github.com/launchdarkly/ldcli/internal/analytics"
64
"testing"
75

86
"github.com/stretchr/testify/assert"
97
"github.com/stretchr/testify/require"
8+
9+
"github.com/launchdarkly/ldcli/cmd"
10+
"github.com/launchdarkly/ldcli/internal/analytics"
11+
"github.com/launchdarkly/ldcli/internal/resources"
1012
)
1113

1214
func TestCreate(t *testing.T) {
@@ -26,3 +28,31 @@ func TestCreate(t *testing.T) {
2628
assert.Contains(t, string(output), `ldcli version test`)
2729
})
2830
}
31+
32+
func TestJSONFlag(t *testing.T) {
33+
mockClient := &resources.MockClient{
34+
Response: []byte(`{"key": "test-key", "name": "test-name"}`),
35+
}
36+
37+
t.Run("--json returns raw JSON output", func(t *testing.T) {
38+
args := []string{
39+
"flags", "toggle-on",
40+
"--access-token", "abcd1234",
41+
"--environment", "test-env",
42+
"--flag", "test-flag",
43+
"--project", "test-proj",
44+
"--json",
45+
}
46+
47+
output, err := cmd.CallCmd(
48+
t,
49+
cmd.APIClients{ResourcesClient: mockClient},
50+
analytics.NoopClientFn{}.Tracker(),
51+
args,
52+
)
53+
54+
require.NoError(t, err)
55+
assert.Contains(t, string(output), `"key": "test-key"`)
56+
assert.NotContains(t, string(output), "Successfully updated")
57+
})
58+
}

0 commit comments

Comments
 (0)