Skip to content

Commit 1221c03

Browse files
Merge branch 'main' into nathan/increase-polling-frequency
2 parents 462678c + fea6527 commit 1221c03

File tree

15 files changed

+1075
-162
lines changed

15 files changed

+1075
-162
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,8 @@ Environment variables override configuration file values. All variables use the
230230
- `TIGER_ANALYTICS` - Enable/disable analytics
231231
- `TIGER_PASSWORD_STORAGE` - Password storage method: `keyring`, `pgpass`, or `none`
232232
- `TIGER_DEBUG` - Enable/disable debug logging
233+
- `TIGER_VERSION_CHECK_URL` - URL to check for latest version
234+
- `TIGER_VERSION_CHECK_INTERVAL` - Seconds between version checks, 0 to disable
233235

234236
### Global Flags
235237

go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ require (
2828
al.essio.dev/pkg/shellescape v1.6.0 // indirect
2929
github.com/1password/onepassword-sdk-go v0.3.1 // indirect
3030
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
31+
github.com/Masterminds/semver/v3 v3.4.0 // indirect
3132
github.com/Microsoft/go-winio v0.6.2 // indirect
3233
github.com/adrg/xdg v0.5.3 // indirect
3334
github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect
@@ -38,6 +39,7 @@ require (
3839
github.com/charmbracelet/x/ansi v0.10.1 // indirect
3940
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
4041
github.com/charmbracelet/x/term v0.2.1 // indirect
42+
github.com/cli/safeexec v1.0.1 // indirect
4143
github.com/containerd/errdefs v1.0.0 // indirect
4244
github.com/containerd/errdefs/pkg v0.3.0 // indirect
4345
github.com/containerd/stargz-snapshotter/estargz v0.16.3 // indirect

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNE
3535
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
3636
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
3737
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
38+
github.com/cli/safeexec v1.0.1 h1:e/C79PbXF4yYTN/wauC4tviMxEV13BwljGj0N9j+N00=
39+
github.com/cli/safeexec v1.0.1/go.mod h1:Z/D4tTN8Vs5gXYHDCbaM1S/anmEDnJb1iW0+EJ5zx3Q=
3840
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
3941
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
4042
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=

internal/tiger/cmd/config.go

Lines changed: 32 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
11
package cmd
22

33
import (
4-
"encoding/json"
54
"fmt"
5+
"io"
6+
"time"
67

78
"github.com/olekukonko/tablewriter"
89
"github.com/spf13/cobra"
910
"github.com/spf13/viper"
1011
"go.uber.org/zap"
11-
"gopkg.in/yaml.v3"
1212

1313
"github.com/timescale/tiger-cli/internal/tiger/config"
1414
"github.com/timescale/tiger-cli/internal/tiger/logging"
15+
"github.com/timescale/tiger-cli/internal/tiger/util"
1516
)
1617

1718
func buildConfigShowCmd() *cobra.Command {
@@ -65,13 +66,14 @@ func buildConfigShowCmd() *cobra.Command {
6566
cfgOut.ConfigDir = nil
6667
}
6768

69+
output := cmd.OutOrStdout()
6870
switch outputFormat {
6971
case "json":
70-
return outputJSON(cfgOut, cmd)
72+
return util.SerializeToJSON(output, cfgOut)
7173
case "yaml":
72-
return outputYAML(cfgOut, cmd)
74+
return util.SerializeToYAML(output, cfgOut, false)
7375
default:
74-
return outputTable(cfgOut, cmd)
76+
return outputTable(output, cfgOut)
7577
}
7678
},
7779
}
@@ -177,56 +179,53 @@ func buildConfigCmd() *cobra.Command {
177179
return cmd
178180
}
179181

180-
func outputTable(cfg *config.ConfigOutput, cmd *cobra.Command) error {
181-
table := tablewriter.NewWriter(cmd.OutOrStdout())
182+
func outputTable(w io.Writer, cfg *config.ConfigOutput) error {
183+
table := tablewriter.NewWriter(w)
182184
table.Header("PROPERTY", "VALUE")
183185
if cfg.APIURL != nil {
184186
table.Append("api_url", *cfg.APIURL)
185187
}
188+
if cfg.Analytics != nil {
189+
table.Append("analytics", fmt.Sprintf("%t", *cfg.Analytics))
190+
}
191+
if cfg.ConfigDir != nil {
192+
table.Append("config_dir", *cfg.ConfigDir)
193+
}
186194
if cfg.ConsoleURL != nil {
187195
table.Append("console_url", *cfg.ConsoleURL)
188196
}
189-
if cfg.GatewayURL != nil {
190-
table.Append("gateway_url", *cfg.GatewayURL)
197+
if cfg.Debug != nil {
198+
table.Append("debug", fmt.Sprintf("%t", *cfg.Debug))
191199
}
192200
if cfg.DocsMCP != nil {
193201
table.Append("docs_mcp", fmt.Sprintf("%t", *cfg.DocsMCP))
194202
}
195203
if cfg.DocsMCPURL != nil {
196204
table.Append("docs_mcp_url", *cfg.DocsMCPURL)
197205
}
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)
206+
if cfg.GatewayURL != nil {
207+
table.Append("gateway_url", *cfg.GatewayURL)
203208
}
204209
if cfg.Output != nil {
205210
table.Append("output", *cfg.Output)
206211
}
207-
if cfg.Analytics != nil {
208-
table.Append("analytics", fmt.Sprintf("%t", *cfg.Analytics))
209-
}
210212
if cfg.PasswordStorage != nil {
211213
table.Append("password_storage", *cfg.PasswordStorage)
212214
}
213-
if cfg.Debug != nil {
214-
table.Append("debug", fmt.Sprintf("%t", *cfg.Debug))
215+
if cfg.ProjectID != nil {
216+
table.Append("project_id", *cfg.ProjectID)
215217
}
216-
if cfg.ConfigDir != nil {
217-
table.Append("config_dir", *cfg.ConfigDir)
218+
if cfg.ReleasesURL != nil {
219+
table.Append("releases_url", *cfg.ReleasesURL)
220+
}
221+
if cfg.ServiceID != nil {
222+
table.Append("service_id", *cfg.ServiceID)
223+
}
224+
if cfg.VersionCheckInterval != nil {
225+
table.Append("version_check_interval", cfg.VersionCheckInterval.String())
226+
}
227+
if cfg.VersionCheckLastTime != nil {
228+
table.Append("version_check_last_time", cfg.VersionCheckLastTime.Format(time.RFC1123))
218229
}
219230
return table.Render()
220231
}
221-
222-
func outputJSON(cfg *config.ConfigOutput, cmd *cobra.Command) error {
223-
encoder := json.NewEncoder(cmd.OutOrStdout())
224-
encoder.SetIndent("", " ")
225-
return encoder.Encode(cfg)
226-
}
227-
228-
func outputYAML(cfg *config.ConfigOutput, cmd *cobra.Command) error {
229-
encoder := yaml.NewEncoder(cmd.OutOrStdout())
230-
defer encoder.Close()
231-
return encoder.Encode(cfg)
232-
}

internal/tiger/cmd/config_test.go

Lines changed: 53 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"slices"
88
"strings"
99
"testing"
10+
"time"
1011

1112
"gopkg.in/yaml.v3"
1213

@@ -106,13 +107,17 @@ password_storage: pgpass
106107
func TestConfigShow_JSONOutput(t *testing.T) {
107108
tmpDir, _ := setupConfigTest(t)
108109

110+
now := time.Now()
111+
109112
// Create config file with JSON output format
110113
configContent := `api_url: https://json.api.com/v1
111114
project_id: json-project
112115
output: json
113116
analytics: true
114117
password_storage: none
115-
`
118+
version_check_interval: 1h
119+
version_check_last_time: ` + now.Format(time.RFC3339) + "\n"
120+
116121
configFile := config.GetConfigFile(tmpDir)
117122
if err := os.WriteFile(configFile, []byte(configContent), 0644); err != nil {
118123
t.Fatalf("Failed to write config file: %v", err)
@@ -131,18 +136,21 @@ password_storage: none
131136

132137
// Verify ALL JSON keys and their expected values
133138
expectedValues := map[string]interface{}{
134-
"api_url": "https://json.api.com/v1",
135-
"console_url": "https://console.cloud.timescale.com",
136-
"gateway_url": "https://console.cloud.timescale.com/api",
137-
"docs_mcp": true,
138-
"docs_mcp_url": "https://mcp.tigerdata.com/docs",
139-
"project_id": "json-project",
140-
"service_id": "",
141-
"output": "json",
142-
"analytics": true,
143-
"password_storage": "none",
144-
"debug": false,
145-
"config_dir": tmpDir,
139+
"api_url": "https://json.api.com/v1",
140+
"console_url": "https://console.cloud.timescale.com",
141+
"gateway_url": "https://console.cloud.timescale.com/api",
142+
"docs_mcp": true,
143+
"docs_mcp_url": "https://mcp.tigerdata.com/docs",
144+
"project_id": "json-project",
145+
"service_id": "",
146+
"output": "json",
147+
"analytics": true,
148+
"password_storage": "none",
149+
"debug": false,
150+
"config_dir": tmpDir,
151+
"releases_url": "https://cli.tigerdata.com",
152+
"version_check_interval": float64(3600000000000), // JSON unmarshals time.Duration as nanoseconds (1 hour = 3600000000000ns)
153+
"version_check_last_time": now.Format(time.RFC3339),
146154
}
147155

148156
for key, expectedValue := range expectedValues {
@@ -160,13 +168,16 @@ password_storage: none
160168
func TestConfigShow_YAMLOutput(t *testing.T) {
161169
tmpDir, _ := setupConfigTest(t)
162170

171+
now := time.Now()
172+
163173
// Create config file with YAML output format
164174
configContent := `api_url: https://yaml.api.com/v1
165175
project_id: yaml-project
166176
output: yaml
167177
analytics: false
168178
password_storage: keyring
169-
`
179+
version_check_last_time: ` + now.Format(time.RFC3339) + "\n"
180+
170181
configFile := config.GetConfigFile(tmpDir)
171182
if err := os.WriteFile(configFile, []byte(configContent), 0644); err != nil {
172183
t.Fatalf("Failed to write config file: %v", err)
@@ -178,30 +189,42 @@ password_storage: keyring
178189
}
179190

180191
// Parse YAML output
181-
var result map[string]interface{}
192+
var result map[string]any
182193
if err := yaml.Unmarshal([]byte(output), &result); err != nil {
183194
t.Fatalf("Failed to parse YAML output: %v", err)
184195
}
185196

186197
// Verify ALL YAML keys and their expected values
187-
expectedValues := map[string]interface{}{
188-
"api_url": "https://yaml.api.com/v1",
189-
"console_url": "https://console.cloud.timescale.com",
190-
"gateway_url": "https://console.cloud.timescale.com/api",
191-
"docs_mcp": true,
192-
"docs_mcp_url": "https://mcp.tigerdata.com/docs",
193-
"project_id": "yaml-project",
194-
"service_id": "",
195-
"output": "yaml",
196-
"analytics": false,
197-
"password_storage": "keyring",
198-
"debug": false,
199-
"config_dir": tmpDir,
198+
expectedValues := map[string]any{
199+
"api_url": "https://yaml.api.com/v1",
200+
"console_url": "https://console.cloud.timescale.com",
201+
"gateway_url": "https://console.cloud.timescale.com/api",
202+
"docs_mcp": true,
203+
"docs_mcp_url": "https://mcp.tigerdata.com/docs",
204+
"project_id": "yaml-project",
205+
"service_id": "",
206+
"output": "yaml",
207+
"analytics": false,
208+
"password_storage": "keyring",
209+
"debug": false,
210+
"config_dir": tmpDir,
211+
"releases_url": "https://cli.tigerdata.com",
212+
"version_check_interval": "24h0m0s", // YAML serializes time.Duration as string
213+
"version_check_last_time": now,
200214
}
201215

202216
for key, expectedValue := range expectedValues {
203-
if result[key] != expectedValue {
204-
t.Errorf("Expected %s '%v', got %v", key, expectedValue, result[key])
217+
switch expectedValue.(type) {
218+
case time.Time:
219+
// YAML unmarshals time.Time as time.Time type, so we need to compare differently
220+
if expectedValue.(time.Time).Format(time.RFC3339) != result[key].(time.Time).Format(time.RFC3339) {
221+
t.Errorf("foo Expected %s '%v', got %v", key, expectedValue, result[key])
222+
}
223+
default:
224+
// Other types can be compared directly
225+
if result[key] != expectedValue {
226+
t.Errorf("Expected %s '%v', got %v", key, expectedValue, result[key])
227+
}
205228
}
206229
}
207230

internal/tiger/cmd/root.go

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010

1111
"github.com/timescale/tiger-cli/internal/tiger/config"
1212
"github.com/timescale/tiger-cli/internal/tiger/logging"
13+
"github.com/timescale/tiger-cli/internal/tiger/version"
1314
)
1415

1516
func buildRootCmd() *cobra.Command {
@@ -19,6 +20,7 @@ func buildRootCmd() *cobra.Command {
1920
var serviceID string
2021
var analytics bool
2122
var passwordStorage string
23+
var skipUpdateCheck bool
2224

2325
cmd := &cobra.Command{
2426
Use: "tiger",
@@ -33,14 +35,13 @@ tiger auth login
3335
3436
`,
3537
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
36-
if err := logging.Init(debug); err != nil {
37-
return fmt.Errorf("failed to initialize logging: %w", err)
38-
}
39-
4038
cfg, err := config.Load()
4139
if err != nil {
42-
logging.Error("failed to load config", zap.Error(err))
43-
return err
40+
return fmt.Errorf("failed to load config: %w", err)
41+
}
42+
43+
if err := logging.Init(cfg.Debug); err != nil {
44+
return fmt.Errorf("failed to initialize logging: %w", err)
4445
}
4546

4647
logging.Debug("CLI initialized",
@@ -51,8 +52,24 @@ tiger auth login
5152

5253
return nil
5354
},
54-
PersistentPostRun: func(cmd *cobra.Command, args []string) {
55+
PersistentPostRunE: func(cmd *cobra.Command, args []string) error {
56+
cfg, err := config.Load()
57+
if err != nil {
58+
return fmt.Errorf("failed to load config: %w", err)
59+
}
60+
61+
// Skip update check if:
62+
// 1. --skip-update-check flag was provided
63+
// 2. Running "version --check" (version command handles its own check)
64+
isVersionCheck := cmd.Name() == "version" && cmd.Flags().Changed("check")
65+
if !skipUpdateCheck && !isVersionCheck {
66+
output := cmd.ErrOrStderr()
67+
result := version.PerformCheck(cfg, &output, false)
68+
version.PrintUpdateWarning(result, cfg, &output)
69+
}
70+
5571
logging.Sync()
72+
return nil
5673
},
5774
}
5875

@@ -74,6 +91,7 @@ tiger auth login
7491
cmd.PersistentFlags().StringVar(&serviceID, "service-id", "", "service ID")
7592
cmd.PersistentFlags().BoolVar(&analytics, "analytics", true, "enable/disable usage analytics")
7693
cmd.PersistentFlags().StringVar(&passwordStorage, "password-storage", config.DefaultPasswordStorage, "password storage method (keyring, pgpass, none)")
94+
cmd.PersistentFlags().BoolVar(&skipUpdateCheck, "skip-update-check", false, "skip checking for updates on startup")
7795

7896
// Bind flags to viper
7997
viper.BindPFlag("debug", cmd.PersistentFlags().Lookup("debug"))

0 commit comments

Comments
 (0)