Skip to content

Commit 07874d5

Browse files
authored
AGE-124: env output format for services (#49)
1 parent f573d1f commit 07874d5

File tree

17 files changed

+488
-340
lines changed

17 files changed

+488
-340
lines changed

CLAUDE.md

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -405,7 +405,6 @@ func buildRootCmd() *cobra.Command {
405405
// Declare ALL flag variables locally within this function
406406
var configDir string
407407
var debug bool
408-
var output string
409408
var projectID string
410409
var serviceID string
411410
var analytics bool
@@ -429,15 +428,13 @@ func buildRootCmd() *cobra.Command {
429428
cobra.OnInitialize(initConfigFunc)
430429
cmd.PersistentFlags().StringVar(&configDir, "config-dir", config.GetDefaultConfigDir(), "config directory")
431430
cmd.PersistentFlags().BoolVar(&debug, "debug", false, "enable debug logging")
432-
cmd.PersistentFlags().VarP((*outputFlag)(&output), "output", "o", "output format (json, yaml, table)")
433431
cmd.PersistentFlags().StringVar(&projectID, "project-id", "", "project ID")
434432
cmd.PersistentFlags().StringVar(&serviceID, "service-id", "", "service ID")
435433
cmd.PersistentFlags().BoolVar(&analytics, "analytics", true, "enable/disable usage analytics")
436434
cmd.PersistentFlags().StringVar(&passwordStorage, "password-storage", config.DefaultPasswordStorage, "password storage method (keyring, pgpass, none)")
437435

438436
// Bind flags to viper
439437
viper.BindPFlag("debug", cmd.PersistentFlags().Lookup("debug"))
440-
viper.BindPFlag("output", cmd.PersistentFlags().Lookup("output"))
441438
// ... bind remaining flags
442439

443440
// Add all subcommands (complete tree building)

README.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,6 @@ These flags are available on all commands and take precedence over both environm
238238
- `--config-dir <path>` - Path to configuration directory (default: `~/.config/tiger`)
239239
- `--project-id <id>` - Specify project ID
240240
- `--service-id <id>` - Specify service ID
241-
- `-o, --output <format>` - Output format: `json`, `yaml`, or `table`
242241
- `--analytics` - Enable/disable analytics
243242
- `--password-storage <method>` - Password storage method: `keyring`, `pgpass`, or `none`
244243
- `--debug` - Enable/disable debug logging

internal/tiger/cmd/config.go

Lines changed: 94 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ import (
44
"encoding/json"
55
"fmt"
66

7+
"github.com/olekukonko/tablewriter"
78
"github.com/spf13/cobra"
9+
"github.com/spf13/viper"
810
"go.uber.org/zap"
911
"gopkg.in/yaml.v3"
1012

@@ -13,7 +15,11 @@ import (
1315
)
1416

1517
func buildConfigShowCmd() *cobra.Command {
16-
return &cobra.Command{
18+
var output string
19+
var noDefaults bool
20+
var withEnv bool
21+
22+
cmd := &cobra.Command{
1723
Use: "show",
1824
Short: "Show current configuration",
1925
Long: `Display the current CLI configuration settings`,
@@ -26,16 +32,55 @@ func buildConfigShowCmd() *cobra.Command {
2632
return fmt.Errorf("failed to load config: %w", err)
2733
}
2834

29-
switch cfg.Output {
35+
// Use flag value if provided, otherwise use config value
36+
outputFormat := cfg.Output
37+
if cmd.Flags().Changed("output") {
38+
outputFormat = output
39+
}
40+
41+
configFile, err := cfg.EnsureConfigDir()
42+
if err != nil {
43+
return err
44+
}
45+
46+
// a new viper, free from env and cli flags
47+
v := viper.New()
48+
v.SetConfigFile(configFile)
49+
if withEnv {
50+
config.ApplyEnvOverrides(v)
51+
}
52+
if !noDefaults {
53+
config.ApplyDefaults(v)
54+
}
55+
if err := config.ReadInConfig(v); err != nil {
56+
return err
57+
}
58+
59+
cfgOut, err := config.ForOutputFromViper(v)
60+
if err != nil {
61+
return err
62+
}
63+
64+
if *cfgOut.ConfigDir == config.GetDefaultConfigDir() {
65+
cfgOut.ConfigDir = nil
66+
}
67+
68+
switch outputFormat {
3069
case "json":
31-
return outputJSON(cfg, cmd)
70+
return outputJSON(cfgOut, cmd)
3271
case "yaml":
33-
return outputYAML(cfg, cmd)
72+
return outputYAML(cfgOut, cmd)
3473
default:
35-
return outputTable(cfg, cmd)
74+
return outputTable(cfgOut, cmd)
3675
}
3776
},
3877
}
78+
79+
cmd.Flags().VarP((*outputFlag)(&output), "output", "o", "output format (json, yaml, table)")
80+
cmd.Flags().BoolVar(&noDefaults, "no-defaults", false, "do not show default values for unset fields")
81+
cmd.Flags().BoolVar(&withEnv, "with-env", false, "apply environment variable overrides")
82+
83+
return cmd
3984
}
4085

4186
func buildConfigSetCmd() *cobra.Command {
@@ -132,69 +177,56 @@ func buildConfigCmd() *cobra.Command {
132177
return cmd
133178
}
134179

135-
func outputTable(cfg *config.Config, cmd *cobra.Command) error {
136-
out := cmd.OutOrStdout()
137-
fmt.Fprintln(out, "Current Configuration:")
138-
fmt.Fprintf(out, " api_url: %s\n", cfg.APIURL)
139-
fmt.Fprintf(out, " console_url: %s\n", cfg.ConsoleURL)
140-
fmt.Fprintf(out, " gateway_url: %s\n", cfg.GatewayURL)
141-
fmt.Fprintf(out, " docs_mcp: %t\n", cfg.DocsMCP)
142-
fmt.Fprintf(out, " docs_mcp_url: %s\n", cfg.DocsMCPURL)
143-
fmt.Fprintf(out, " project_id: %s\n", valueOrEmpty(cfg.ProjectID))
144-
fmt.Fprintf(out, " service_id: %s\n", valueOrEmpty(cfg.ServiceID))
145-
fmt.Fprintf(out, " output: %s\n", cfg.Output)
146-
fmt.Fprintf(out, " analytics: %t\n", cfg.Analytics)
147-
fmt.Fprintf(out, " password_storage: %s\n", cfg.PasswordStorage)
148-
fmt.Fprintf(out, " debug: %t\n", cfg.Debug)
149-
fmt.Fprintf(out, " config_dir: %s\n", cfg.ConfigDir)
150-
return nil
151-
}
152-
153-
func outputJSON(cfg *config.Config, cmd *cobra.Command) error {
154-
data := map[string]interface{}{
155-
"api_url": cfg.APIURL,
156-
"console_url": cfg.ConsoleURL,
157-
"gateway_url": cfg.GatewayURL,
158-
"docs_mcp": cfg.DocsMCP,
159-
"docs_mcp_url": cfg.DocsMCPURL,
160-
"project_id": cfg.ProjectID,
161-
"service_id": cfg.ServiceID,
162-
"output": cfg.Output,
163-
"analytics": cfg.Analytics,
164-
"password_storage": cfg.PasswordStorage,
165-
"debug": cfg.Debug,
166-
"config_dir": cfg.ConfigDir,
180+
func outputTable(cfg *config.ConfigOutput, cmd *cobra.Command) error {
181+
table := tablewriter.NewWriter(cmd.OutOrStdout())
182+
table.Header("PROPERTY", "VALUE")
183+
if cfg.APIURL != nil {
184+
table.Append("api_url", *cfg.APIURL)
185+
}
186+
if cfg.ConsoleURL != nil {
187+
table.Append("console_url", *cfg.ConsoleURL)
188+
}
189+
if cfg.GatewayURL != nil {
190+
table.Append("gateway_url", *cfg.GatewayURL)
191+
}
192+
if cfg.DocsMCP != nil {
193+
table.Append("docs_mcp", fmt.Sprintf("%t", *cfg.DocsMCP))
194+
}
195+
if cfg.DocsMCPURL != nil {
196+
table.Append("docs_mcp_url", *cfg.DocsMCPURL)
197+
}
198+
if cfg.ProjectID != nil {
199+
table.Append("project_id", *cfg.ProjectID)
200+
}
201+
if cfg.ServiceID != nil {
202+
table.Append("service_id", *cfg.ServiceID)
203+
}
204+
if cfg.Output != nil {
205+
table.Append("output", *cfg.Output)
206+
}
207+
if cfg.Analytics != nil {
208+
table.Append("analytics", fmt.Sprintf("%t", *cfg.Analytics))
167209
}
210+
if cfg.PasswordStorage != nil {
211+
table.Append("password_storage", *cfg.PasswordStorage)
212+
}
213+
if cfg.Debug != nil {
214+
table.Append("debug", fmt.Sprintf("%t", *cfg.Debug))
215+
}
216+
if cfg.ConfigDir != nil {
217+
table.Append("config_dir", *cfg.ConfigDir)
218+
}
219+
return table.Render()
220+
}
168221

222+
func outputJSON(cfg *config.ConfigOutput, cmd *cobra.Command) error {
169223
encoder := json.NewEncoder(cmd.OutOrStdout())
170224
encoder.SetIndent("", " ")
171-
return encoder.Encode(data)
225+
return encoder.Encode(cfg)
172226
}
173227

174-
func outputYAML(cfg *config.Config, cmd *cobra.Command) error {
175-
data := map[string]interface{}{
176-
"api_url": cfg.APIURL,
177-
"console_url": cfg.ConsoleURL,
178-
"gateway_url": cfg.GatewayURL,
179-
"docs_mcp": cfg.DocsMCP,
180-
"docs_mcp_url": cfg.DocsMCPURL,
181-
"project_id": cfg.ProjectID,
182-
"service_id": cfg.ServiceID,
183-
"output": cfg.Output,
184-
"analytics": cfg.Analytics,
185-
"password_storage": cfg.PasswordStorage,
186-
"debug": cfg.Debug,
187-
"config_dir": cfg.ConfigDir,
188-
}
189-
228+
func outputYAML(cfg *config.ConfigOutput, cmd *cobra.Command) error {
190229
encoder := yaml.NewEncoder(cmd.OutOrStdout())
191230
defer encoder.Close()
192-
return encoder.Encode(data)
193-
}
194-
195-
func valueOrEmpty(s string) string {
196-
if s == "" {
197-
return "(not set)"
198-
}
199-
return s
231+
return encoder.Encode(cfg)
200232
}

internal/tiger/cmd/config_test.go

Lines changed: 71 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -79,24 +79,26 @@ password_storage: pgpass
7979
lines := strings.Split(output, "\n")
8080

8181
// Check table output contains all expected key:value lines
82-
expectedLines := []string{
83-
" api_url: https://test.api.com/v1",
84-
" console_url: https://console.cloud.timescale.com",
85-
" gateway_url: https://console.cloud.timescale.com/api",
86-
" docs_mcp: true",
87-
" docs_mcp_url: https://mcp.tigerdata.com/docs",
88-
" project_id: test-project",
89-
" service_id: test-service",
90-
" output: table",
91-
" analytics: false",
92-
" password_storage: pgpass",
93-
" debug: false",
94-
" config_dir: " + tmpDir,
95-
}
96-
97-
for _, expectedLine := range expectedLines {
98-
if !slices.Contains(lines, expectedLine) {
99-
t.Errorf("Output should contain line '%s', got: %s", expectedLine, output)
82+
expectedLines := map[string]string{
83+
"api_url": "https://test.api.com/v1",
84+
"console_url": "https://console.cloud.timescale.com",
85+
"gateway_url": "https://console.cloud.timescale.com/api",
86+
"docs_mcp": "true",
87+
"docs_mcp_url": "https://mcp.tigerdata.com/docs",
88+
"project_id": "test-project",
89+
"service_id": "test-service",
90+
"output": "table",
91+
"analytics": "false",
92+
"password_storage": "pgpass",
93+
"debug": "false",
94+
"config_dir": tmpDir,
95+
}
96+
97+
for key, expectedLine := range expectedLines {
98+
if !slices.ContainsFunc(lines, func(line string) bool {
99+
return strings.Contains(line, key) && strings.Contains(line, expectedLine)
100+
}) {
101+
t.Errorf("Output should contain line '%s':'%s', got: %s", key, expectedLine, output)
100102
}
101103
}
102104
}
@@ -209,26 +211,70 @@ password_storage: keyring
209211
}
210212
}
211213

212-
func TestConfigShow_EmptyValues(t *testing.T) {
214+
func TestConfigShow_OutputValueUnaffectedByCliArg(t *testing.T) {
213215
tmpDir, _ := setupConfigTest(t)
214216

215-
// Create minimal config (only defaults)
216-
configContent := `output: table
217+
// Create config file with table as default output
218+
configContent := `api_url: https://test.api.com/v1
219+
project_id: test-project
220+
output: table
217221
analytics: true
218222
`
219223
configFile := config.GetConfigFile(tmpDir)
220224
if err := os.WriteFile(configFile, []byte(configContent), 0644); err != nil {
221225
t.Fatalf("Failed to write config file: %v", err)
222226
}
223227

228+
// Test that -o json flag overrides config file setting for output format, but not the config value itself
229+
output, err := executeConfigCommand("config", "show", "-o", "json")
230+
if err != nil {
231+
t.Fatalf("Command failed: %v", err)
232+
}
233+
234+
// Should be valid JSON, not table format
235+
var result map[string]interface{}
236+
if err := json.Unmarshal([]byte(output), &result); err != nil {
237+
t.Fatalf("Expected JSON output but got: %v\nOutput was: %s", err, output)
238+
}
239+
240+
if result["output"] != "table" {
241+
t.Errorf("Expected output 'table' in JSON output, got %v", result["output"])
242+
}
243+
}
244+
245+
func TestConfigShow_OutputValueUnaffectedByEnvVar(t *testing.T) {
246+
tmpDir, _ := setupConfigTest(t)
247+
248+
// Create config file with table as default output
249+
configContent := `api_url: https://test.api.com/v1
250+
project_id: test-project
251+
output: table
252+
analytics: true
253+
`
254+
configFile := config.GetConfigFile(tmpDir)
255+
if err := os.WriteFile(configFile, []byte(configContent), 0644); err != nil {
256+
t.Fatalf("Failed to write config file: %v", err)
257+
}
258+
259+
// Test that env overrides config file setting for output format, but not the config value itself
260+
os.Setenv("TIGER_OUTPUT", "json")
261+
defer func() {
262+
os.Unsetenv("TIGER_OUTPUT")
263+
}()
264+
224265
output, err := executeConfigCommand("config", "show")
225266
if err != nil {
226267
t.Fatalf("Command failed: %v", err)
227268
}
228269

229-
// Check that empty values show "(not set)"
230-
if !strings.Contains(output, "(not set)") {
231-
t.Error("Output should contain '(not set)' for empty values")
270+
// Should be valid JSON, not table format
271+
var result map[string]interface{}
272+
if err := json.Unmarshal([]byte(output), &result); err != nil {
273+
t.Fatalf("Expected JSON output but got: %v\nOutput was: %s", err, output)
274+
}
275+
276+
if result["output"] != "table" {
277+
t.Errorf("Expected output 'table' in JSON output, got %v", result["output"])
232278
}
233279
}
234280

@@ -582,26 +628,6 @@ func TestConfigReset(t *testing.T) {
582628
}
583629
}
584630

585-
func TestValueOrEmpty(t *testing.T) {
586-
tests := []struct {
587-
input string
588-
expected string
589-
}{
590-
{"", "(not set)"},
591-
{"value", "value"},
592-
{"test-string", "test-string"},
593-
}
594-
595-
for _, tt := range tests {
596-
t.Run(tt.input, func(t *testing.T) {
597-
result := valueOrEmpty(tt.input)
598-
if result != tt.expected {
599-
t.Errorf("valueOrEmpty(%q) = %q, expected %q", tt.input, result, tt.expected)
600-
}
601-
})
602-
}
603-
}
604-
605631
func TestConfigCommands_Integration(t *testing.T) {
606632
_, _ = setupConfigTest(t)
607633

@@ -646,6 +672,7 @@ func TestConfigCommands_Integration(t *testing.T) {
646672
t.Fatalf("Failed to show config after unset: %v", err)
647673
}
648674

675+
result = make(map[string]any)
649676
json.Unmarshal([]byte(showOutput), &result)
650677
if result["project_id"] != "" {
651678
t.Errorf("Expected empty project_id after unset, got %v", result["project_id"])

0 commit comments

Comments
 (0)