diff --git a/VERSION b/VERSION index dd1266b3..b74a3424 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.0.46 +2.0.51 diff --git a/cmd/deepsource/main.go b/cmd/deepsource/main.go index 00be64b5..4d535ab0 100644 --- a/cmd/deepsource/main.go +++ b/cmd/deepsource/main.go @@ -6,6 +6,7 @@ import ( "log" "net/http" "os" + "path/filepath" "strings" "time" @@ -16,6 +17,7 @@ import ( clierrors "github.com/deepsourcelabs/cli/internal/errors" "github.com/deepsourcelabs/cli/internal/update" "github.com/getsentry/sentry-go" + "github.com/pterm/pterm" ) var ( @@ -71,28 +73,20 @@ func mainRun() (exitCode int) { func run() int { v.SetBuildInfo(version, Date, buildMode) - // Two-phase auto-update: apply pending update or check for new one - if update.ShouldAutoUpdate() { + // Check for available updates and notify (skip when running "update" itself) + isUpdateCmd := len(os.Args) >= 2 && os.Args[1] == "update" + if !isUpdateCmd && update.ShouldCheckForUpdate() { + client := &http.Client{Timeout: 3 * time.Second} + if err := update.CheckForUpdate(client); err != nil { + debug.Log("update: %v", err) + } + state, err := update.ReadUpdateState() if err != nil { debug.Log("update: %v", err) } - if state != nil { - // Phase 2: a previous run found a newer version — apply it now - client := &http.Client{Timeout: 30 * time.Second} - newVer, err := update.ApplyUpdate(client) - if err != nil { - debug.Log("update: %v", err) - } else if newVer != "" { - fmt.Fprintf(os.Stderr, "%s\n", style.Yellow("Updated DeepSource CLI to v%s", newVer)) - } - } else { - // Phase 1: check manifest and write state file for next run - client := &http.Client{Timeout: 3 * time.Second} - if err := update.CheckForUpdate(client); err != nil { - debug.Log("update: %v", err) - } + fmt.Fprintln(os.Stderr, pterm.Yellow(fmt.Sprintf("Update available: v%s, run '%s update' to install.", state.Version, filepath.Base(os.Args[0])))) } } diff --git a/command/root.go b/command/root.go index ed15790a..f293b6df 100644 --- a/command/root.go +++ b/command/root.go @@ -8,6 +8,7 @@ import ( "github.com/deepsourcelabs/cli/buildinfo" "github.com/deepsourcelabs/cli/command/auth" completionCmd "github.com/deepsourcelabs/cli/command/completion" + updateCmd "github.com/deepsourcelabs/cli/command/update" "github.com/deepsourcelabs/cli/command/issues" "github.com/deepsourcelabs/cli/command/metrics" "github.com/deepsourcelabs/cli/command/report" @@ -89,7 +90,12 @@ func NewCmdRoot() *cobra.Command { completionC.GroupID = "setup" cmd.AddCommand(completionC) + updateC := updateCmd.NewCmdUpdate() + updateC.GroupID = "setup" + cmd.AddCommand(updateC) + cmd.PersistentFlags().Bool("skip-tls-verify", false, "Skip TLS certificate verification (for self-signed certs)") + cmd.Flags().BoolP("verbose", "v", false, "Show detailed output including examples") cmd.InitDefaultHelpFlag() cmd.InitDefaultVersionFlag() @@ -167,13 +173,19 @@ func rootHelpFunc(cmd *cobra.Command, _ []string) { } } - // Examples - examples := buildExampleText() - if examples != "" { - fmt.Fprintf(out, "%s\n", style.BoldCyan("Examples:")) - for _, line := range strings.Split(examples, "\n") { - fmt.Fprintf(out, " %s\n", line) + // Examples (shown only with --verbose / -v) + verbose, _ := cmd.Flags().GetBool("verbose") + if verbose { + examples := buildExampleText() + if examples != "" { + fmt.Fprintf(out, "%s\n", style.BoldCyan("Examples:")) + for _, line := range strings.Split(examples, "\n") { + fmt.Fprintf(out, " %s\n", line) + } + fmt.Fprintln(out) } + } else { + fmt.Fprintln(out, pterm.Gray("Use --help -v to see usage examples.")) fmt.Fprintln(out) } diff --git a/command/update/update.go b/command/update/update.go new file mode 100644 index 00000000..91ba0175 --- /dev/null +++ b/command/update/update.go @@ -0,0 +1,56 @@ +package update + +import ( + "fmt" + "net/http" + "time" + + "github.com/deepsourcelabs/cli/buildinfo" + "github.com/deepsourcelabs/cli/internal/update" + "github.com/spf13/cobra" +) + +func NewCmdUpdate() *cobra.Command { + return &cobra.Command{ + Use: "update", + Short: "Update DeepSource CLI to the latest version", + RunE: func(cmd *cobra.Command, _ []string) error { + return runUpdate(cmd) + }, + } +} + +func runUpdate(cmd *cobra.Command) error { + w := cmd.ErrOrStderr() + + // Check for the latest version + checkClient := &http.Client{Timeout: 10 * time.Second} + if err := update.CheckForUpdate(checkClient); err != nil { + return fmt.Errorf("checking for updates: %w", err) + } + + state, err := update.ReadUpdateState() + if err != nil { + return fmt.Errorf("reading update state: %w", err) + } + + if state == nil { + bi := buildinfo.GetBuildInfo() + fmt.Fprintf(w, "Already up to date (v%s)\n", bi.Version) + return nil + } + + fmt.Fprintf(w, "Updating to v%s...\n", state.Version) + + applyClient := &http.Client{Timeout: 30 * time.Second} + newVer, err := update.ApplyUpdate(applyClient) + if err != nil { + return fmt.Errorf("applying update: %w", err) + } + + if newVer != "" { + fmt.Fprintf(w, "Updated to v%s\n", newVer) + } + + return nil +} diff --git a/config/config.go b/config/config.go index f628b10c..2ee8a499 100644 --- a/config/config.go +++ b/config/config.go @@ -16,8 +16,7 @@ type CLIConfig struct { User string `toml:"user"` Token string `toml:"token"` TokenExpiresIn time.Time `toml:"token_expires_in,omitempty"` - AutoUpdate *bool `toml:"auto_update,omitempty"` - SkipTLSVerify bool `toml:"skip_tls_verify,omitempty"` +SkipTLSVerify bool `toml:"skip_tls_verify,omitempty"` TokenFromEnv bool `toml:"-"` } diff --git a/internal/update/updater.go b/internal/update/updater.go index 40208d0d..d5efc8ba 100644 --- a/internal/update/updater.go +++ b/internal/update/updater.go @@ -19,7 +19,6 @@ import ( "time" "github.com/deepsourcelabs/cli/buildinfo" - "github.com/deepsourcelabs/cli/config" "github.com/deepsourcelabs/cli/internal/debug" ) @@ -179,8 +178,8 @@ func ApplyUpdate(client *http.Client) (string, error) { return state.Version, nil } -// ShouldAutoUpdate reports whether the auto-updater should run. -func ShouldAutoUpdate() bool { +// ShouldCheckForUpdate reports whether the update check should run. +func ShouldCheckForUpdate() bool { bi := buildinfo.GetBuildInfo() if bi == nil { return false @@ -204,13 +203,6 @@ func ShouldAutoUpdate() bool { } } - // Check config - cfg, err := config.GetConfig() - if err == nil && cfg.AutoUpdate != nil && !*cfg.AutoUpdate { - debug.Log("update: skipping (disabled in config)") - return false - } - return true } diff --git a/internal/update/updater_test.go b/internal/update/updater_test.go index 6247d8b6..38832553 100644 --- a/internal/update/updater_test.go +++ b/internal/update/updater_test.go @@ -100,7 +100,7 @@ func TestReplaceBinary(t *testing.T) { } } -func TestShouldAutoUpdate_DevBuild(t *testing.T) { +func TestShouldCheckForUpdate_DevBuild(t *testing.T) { buildinfo.SetBuildInfo("2.0.3", "", "dev") // Clear CI vars so they don't interfere @@ -109,27 +109,27 @@ func TestShouldAutoUpdate_DevBuild(t *testing.T) { t.Setenv(v, "") } - if !ShouldAutoUpdate() { + if !ShouldCheckForUpdate() { t.Error("expected true for dev build with real version") } } -func TestShouldAutoUpdate_DevelopmentVersion(t *testing.T) { +func TestShouldCheckForUpdate_DevelopmentVersion(t *testing.T) { buildinfo.SetBuildInfo("development", "", "") - if ShouldAutoUpdate() { + if ShouldCheckForUpdate() { t.Error("expected false for development version") } } -func TestShouldAutoUpdate_CI(t *testing.T) { +func TestShouldCheckForUpdate_CI(t *testing.T) { buildinfo.SetBuildInfo("2.0.3", "", "prod") t.Setenv("CI", "true") - if ShouldAutoUpdate() { + if ShouldCheckForUpdate() { t.Error("expected false in CI") } } -func TestShouldAutoUpdate_Prod(t *testing.T) { +func TestShouldCheckForUpdate_Prod(t *testing.T) { buildinfo.SetBuildInfo("2.0.3", "", "prod") // Clear CI vars @@ -138,7 +138,7 @@ func TestShouldAutoUpdate_Prod(t *testing.T) { t.Setenv(v, "") } - if !ShouldAutoUpdate() { + if !ShouldCheckForUpdate() { t.Error("expected true for prod build outside CI") } }