Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,7 @@ Tiger CLI is a Go-based command-line interface for managing Tiger, the modern da
- `config.go` - Configuration management commands (show, set, unset, reset)
- `mcp.go` - MCP server commands (install, start, list, get)
- `version.go` - Version command
- `upgrade.go` - Self-update command (download latest release, verify checksum, replace running binary in place)
- **Configuration**: `internal/tiger/config/config.go` - Centralized config with Viper integration
- **Logging**: `internal/tiger/logging/logging.go` - Structured logging with zap
- **API Client**: `internal/tiger/api/` - Generated OpenAPI client with mocks
Expand Down Expand Up @@ -481,6 +482,7 @@ Tiger CLI uses a pure functional builder pattern with **zero global command stat
```
buildRootCmd() → Complete CLI with all commands and flags
├── buildVersionCmd()
├── buildUpgradeCmd()
├── buildConfigCmd()
│ ├── buildConfigShowCmd()
│ ├── buildConfigSetCmd()
Expand Down
15 changes: 13 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,16 @@ For manual repository installation instructions, see [here](https://packagecloud
go install github.com/timescale/tiger-cli/cmd/tiger@latest
```

## Updating

To upgrade an existing installation to the latest release:

```bash
tiger upgrade
```

This downloads the latest published binary, verifies its checksum, and replaces the currently running binary in place. If Tiger CLI was installed via a package manager (Homebrew, apt, yum/dnf), `tiger upgrade` will instead point you at the matching package-manager command.

## Quick Start

After installing Tiger CLI, authenticate with your Tiger Cloud account:
Expand Down Expand Up @@ -115,6 +125,7 @@ Tiger CLI provides the following commands:
- `list` - List available MCP tools, prompts, and resources
- `get` - Get detailed information about a specific MCP capability (aliases: `describe`, `show`)
- `tiger version` - Show version information
- `tiger upgrade` - Upgrade the Tiger CLI to the latest version (alias: `update`)

Use `tiger <command> --help` for detailed information about each command.

Expand Down Expand Up @@ -236,7 +247,7 @@ All configuration options can be set via `tiger config set <key> <value>`:
- `password_storage` - Password storage method: `keyring`, `pgpass`, or `none` (default: `keyring`)
- `read_only` - When `true`, mutating operations are refused: the `tiger service create`/`fork`/`start`/`stop`/`resize`/`update-password`/`delete` CLI commands and their MCP equivalents return an error, and `tiger db connect`, `tiger db connection-string`, and the `db_execute_query` MCP tool open the database session in Tiger Cloud's immutable read-only mode (writes and DDL are rejected by the server). Read commands/tools are unaffected. Default: `false`.
- `service_id` - Default service ID
- `version_check_interval` - How often the CLI will check for new versions, 0 to disable (default: `24h`)
- `version_check` - When `true`, the CLI checks for a newer version on each invocation (in an interactive terminal) and prints a notice if one is available. Set to `false` to disable. Default: `true`.

### Environment Variables

Expand All @@ -253,7 +264,7 @@ Environment variables override configuration file values. All variables use the
- `TIGER_PUBLIC_KEY` - Public key to use for authentication (takes priority over stored credentials)
- `TIGER_SECRET_KEY` - Secret key to use for authentication (takes priority over stored credentials)
- `TIGER_SERVICE_ID` - Default service ID
- `TIGER_VERSION_CHECK_INTERVAL` - How often the CLI will check for new versions, 0 to disable
- `TIGER_VERSION_CHECK` - When `true`, the CLI checks for a newer version on each invocation (in an interactive terminal) and prints a notice if one is available; `false` to disable

### Global Flags

Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ require (
github.com/charmbracelet/bubbletea v1.3.10
github.com/cli/safeexec v1.0.1
github.com/fatih/color v1.18.0
github.com/go-viper/mapstructure/v2 v2.4.0
github.com/google/jsonschema-go v0.4.2
github.com/jackc/pgx/v5 v5.8.0
github.com/modelcontextprotocol/go-sdk v1.2.0
Expand Down Expand Up @@ -91,6 +90,7 @@ require (
github.com/go-openapi/swag/stringutils v0.25.4 // indirect
github.com/go-openapi/swag/typeutils v0.25.4 // indirect
github.com/go-openapi/swag/yamlutils v0.25.4 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/gobwas/glob v0.2.3 // indirect
github.com/godbus/dbus/v5 v5.2.2 // indirect
github.com/gofrs/flock v0.13.0 // indirect
Expand Down
9 changes: 3 additions & 6 deletions internal/tiger/cmd/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package cmd
import (
"fmt"
"io"
"time"

"github.com/olekukonko/tablewriter"
"github.com/spf13/cobra"
Expand Down Expand Up @@ -52,6 +51,7 @@ func buildConfigShowCmd() *cobra.Command {
if err := config.ReadInConfig(v); err != nil {
return err
}
config.MigrateVersionCheck(v)

cfgOut, err := config.ForOutputFromViper(v)
if err != nil {
Expand Down Expand Up @@ -221,11 +221,8 @@ func outputTable(w io.Writer, cfg *config.ConfigOutput) error {
if cfg.ServiceID != nil {
table.Append("service_id", *cfg.ServiceID)
}
if cfg.VersionCheckInterval != nil {
table.Append("version_check_interval", cfg.VersionCheckInterval.String())
}
if cfg.VersionCheckLastTime != nil {
table.Append("version_check_last_time", cfg.VersionCheckLastTime.Format(time.RFC1123))
if cfg.VersionCheck != nil {
table.Append("version_check", fmt.Sprintf("%t", *cfg.VersionCheck))
}
return table.Render()
}
Expand Down
71 changes: 32 additions & 39 deletions internal/tiger/cmd/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import (
"slices"
"strings"
"testing"
"time"

"gopkg.in/yaml.v3"

Expand Down Expand Up @@ -116,14 +115,12 @@ password_storage: pgpass
func TestConfigShow_JSONOutput(t *testing.T) {
tmpDir, _ := setupConfigTest(t)

now := time.Now()

// Create config file with JSON output format
configContent := `api_url: https://json.api.com/v1
output: json
analytics: false
password_storage: keyring
version_check_last_time: ` + now.Format(time.RFC3339) + "\n"
`

configFile := config.GetConfigFile(tmpDir)
if err := os.WriteFile(configFile, []byte(configContent), 0644); err != nil {
Expand All @@ -143,22 +140,21 @@ version_check_last_time: ` + now.Format(time.RFC3339) + "\n"

// Verify ALL JSON keys and their expected values
expectedValues := map[string]interface{}{
"api_url": "https://json.api.com/v1",
"console_url": "https://console.cloud.tigerdata.com",
"gateway_url": "https://console.cloud.tigerdata.com/api",
"docs_mcp": true,
"docs_mcp_url": "https://mcp.tigerdata.com/docs",
"service_id": "",
"color": true,
"output": "json",
"analytics": false,
"password_storage": "keyring",
"read_only": false,
"debug": false,
"config_dir": tmpDir,
"releases_url": "https://cli.tigerdata.com",
"version_check_interval": "24h0m0s",
"version_check_last_time": now.Format(time.RFC3339),
"api_url": "https://json.api.com/v1",
"console_url": "https://console.cloud.tigerdata.com",
"gateway_url": "https://console.cloud.tigerdata.com/api",
"docs_mcp": true,
"docs_mcp_url": "https://mcp.tigerdata.com/docs",
"service_id": "",
"color": true,
"output": "json",
"analytics": false,
"password_storage": "keyring",
"read_only": false,
"debug": false,
"config_dir": tmpDir,
"releases_url": "https://cli.tigerdata.com",
"version_check": true,
}

for key, expectedValue := range expectedValues {
Expand All @@ -176,14 +172,12 @@ version_check_last_time: ` + now.Format(time.RFC3339) + "\n"
func TestConfigShow_YAMLOutput(t *testing.T) {
tmpDir, _ := setupConfigTest(t)

now := time.Now()

// Create config file with YAML output format
configContent := `api_url: https://yaml.api.com/v1
output: yaml
analytics: false
password_storage: keyring
version_check_last_time: ` + now.Format(time.RFC3339) + "\n"
`

configFile := config.GetConfigFile(tmpDir)
if err := os.WriteFile(configFile, []byte(configContent), 0644); err != nil {
Expand All @@ -203,22 +197,21 @@ version_check_last_time: ` + now.Format(time.RFC3339) + "\n"

// Verify ALL YAML keys and their expected values
expectedValues := map[string]any{
"api_url": "https://yaml.api.com/v1",
"console_url": "https://console.cloud.tigerdata.com",
"gateway_url": "https://console.cloud.tigerdata.com/api",
"docs_mcp": true,
"docs_mcp_url": "https://mcp.tigerdata.com/docs",
"service_id": "",
"color": true,
"output": "yaml",
"analytics": false,
"password_storage": "keyring",
"read_only": false,
"debug": false,
"config_dir": tmpDir,
"releases_url": "https://cli.tigerdata.com",
"version_check_interval": "24h0m0s",
"version_check_last_time": now.Format(time.RFC3339),
"api_url": "https://yaml.api.com/v1",
"console_url": "https://console.cloud.tigerdata.com",
"gateway_url": "https://console.cloud.tigerdata.com/api",
"docs_mcp": true,
"docs_mcp_url": "https://mcp.tigerdata.com/docs",
"service_id": "",
"color": true,
"output": "yaml",
"analytics": false,
"password_storage": "keyring",
"read_only": false,
"debug": false,
"config_dir": tmpDir,
"releases_url": "https://cli.tigerdata.com",
"version_check": true,
}

for key, expectedValue := range expectedValues {
Expand Down
41 changes: 34 additions & 7 deletions internal/tiger/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"github.com/timescale/tiger-cli/internal/tiger/common"
"github.com/timescale/tiger-cli/internal/tiger/config"
"github.com/timescale/tiger-cli/internal/tiger/logging"
"github.com/timescale/tiger-cli/internal/tiger/util"
"github.com/timescale/tiger-cli/internal/tiger/version"
)

Expand All @@ -28,6 +29,11 @@ func buildRootCmd(ctx context.Context) (*cobra.Command, error) {
var skipUpdateCheck bool
var colorFlag bool

// versionCheckCh receives the result of the background update check started
// in PersistentPreRunE and drained in PersistentPostRunE. nil when no check
// was launched (disabled, non-interactive, CI, or --skip-update-check).
var versionCheckCh chan *version.CheckResult

cmd := &cobra.Command{
Use: "tiger",
Short: "Tiger CLI - Tiger Cloud Platform command-line interface",
Expand Down Expand Up @@ -80,6 +86,27 @@ tiger auth login
color.NoColor = true
}

// Kick off a background check for a newer release so the network
// fetch overlaps with the command's actual work; the result is
// printed in PersistentPostRunE. Gated to interactive, non-CI
// terminals. `version --check` runs its own synchronous check.
isVersionCheckCmd := cmd.Name() == "version" && cmd.Flag("check") != nil && cmd.Flag("check").Changed
if cfg.VersionCheck && !skipUpdateCheck && !isVersionCheckCmd &&
!util.IsCI() && util.IsTerminal(cmd.ErrOrStderr()) {
versionCheckCh = make(chan *version.CheckResult, 1)
go func() {
result, err := version.CheckForUpdate(cfg)
if err != nil {
// A failed check (e.g. offline) shouldn't spam a warning
// on every command; surface it only in debug logs.
logging.Debug("background version check failed", zap.Error(err))
versionCheckCh <- nil
return
}
versionCheckCh <- result
}()
}

return nil
},
PersistentPostRunE: func(cmd *cobra.Command, args []string) error {
Expand All @@ -88,14 +115,13 @@ tiger auth login
return fmt.Errorf("failed to load config: %w", err)
}

// Skip update check if:
// 1. --skip-update-check flag was provided
// 2. Running "version --check" (version command handles its own check)
isVersionCheck := cmd.Name() == "version" && cmd.Flag("check").Changed
if !skipUpdateCheck && !isVersionCheck {
// Print the result of the background check started in
// PersistentPreRunE, if one was launched. Re-check cfg.VersionCheck
// in case the command itself toggled it off (e.g.
// `tiger config set version_check false`).
if versionCheckCh != nil && cfg.VersionCheck {
output := cmd.ErrOrStderr()
result := version.PerformCheck(cfg, &output, false)
version.PrintUpdateWarning(result, cfg, &output)
version.PrintUpdateWarning(<-versionCheckCh, cfg, &output)
}

logging.Sync()
Expand All @@ -114,6 +140,7 @@ tiger auth login

// Add all subcommands
cmd.AddCommand(buildVersionCmd())
cmd.AddCommand(buildUpgradeCmd())
cmd.AddCommand(buildConfigCmd())
cmd.AddCommand(buildAuthCmd())
cmd.AddCommand(buildServiceCmd())
Expand Down
6 changes: 3 additions & 3 deletions internal/tiger/cmd/service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1547,9 +1547,9 @@ func TestServiceList_OutputFlagAffectsCommandOnly(t *testing.T) {

// Set up config with output format explicitly set to "table"
cfg, err := config.UseTestConfig(tmpDir, map[string]any{
"api_url": "http://localhost:9999",
"output": "table",
"version_check_interval": 0,
"api_url": "http://localhost:9999",
"output": "table",
"version_check": false,
})
if err != nil {
t.Fatalf("Failed to setup test config: %v", err)
Expand Down
Loading
Loading