diff --git a/internal/commands/upgrade.go b/internal/commands/upgrade.go index 10cb2b9..7c24d3d 100644 --- a/internal/commands/upgrade.go +++ b/internal/commands/upgrade.go @@ -29,6 +29,17 @@ type upgradeOptions struct { currentVersion string } +// IsUpgradeInvocation reports whether the given args resolve to the +// "upgrade" command. Used by main to suppress the update-available notice +// when the user is already upgrading. +func IsUpgradeInvocation(root *cobra.Command, args []string) bool { + cmd, _, err := root.Find(args) + if err != nil || cmd == nil { + return false + } + return cmd.Name() == "upgrade" +} + // AddUpgradeCommand registers the "upgrade" subcommand on the root command. // It requires the current build version to display during the upgrade flow. func AddUpgradeCommand(root *cobra.Command, currentVersion string) { diff --git a/internal/commands/upgrade_test.go b/internal/commands/upgrade_test.go index 631824f..727ee32 100644 --- a/internal/commands/upgrade_test.go +++ b/internal/commands/upgrade_test.go @@ -7,8 +7,38 @@ import ( "path/filepath" "runtime" "testing" + + "github.com/spf13/cobra" ) +func TestIsUpgradeInvocation(t *testing.T) { + root := &cobra.Command{Use: "wherobots"} + other := &cobra.Command{Use: "list", Run: func(*cobra.Command, []string) {}} + root.AddCommand(other) + AddUpgradeCommand(root, "1.0.0") + + tests := []struct { + name string + args []string + want bool + }{ + {"upgrade alone", []string{"upgrade"}, true}, + {"upgrade with flag", []string{"upgrade", "--tag", "v1.0.1"}, true}, + {"other command", []string{"list"}, false}, + {"no args", []string{}, false}, + {"unknown command", []string{"nope"}, false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := IsUpgradeInvocation(root, tt.args) + if got != tt.want { + t.Fatalf("IsUpgradeInvocation(%v) = %v, want %v", tt.args, got, tt.want) + } + }) + } +} + func TestDetectPlatform(t *testing.T) { osName, archName, err := detectPlatform() if err != nil { diff --git a/internal/version/check.go b/internal/version/check.go index 366f2d7..b825beb 100644 --- a/internal/version/check.go +++ b/internal/version/check.go @@ -78,7 +78,7 @@ func Collect(ch <-chan *Result) *Result { // FormatNotice returns the human-readable update message to display. func FormatNotice(r *Result) string { return fmt.Sprintf( - "A newer version of the Wherobots CLI is available: %s (current: %s).\nRun `wherobots upgrade` to update.", + "[!] A newer version of the Wherobots CLI is available: %s (current: %s).\n Run `wherobots upgrade` to update.", r.Latest, r.Current, ) } diff --git a/main.go b/main.go index 02749c7..35136c3 100644 --- a/main.go +++ b/main.go @@ -3,6 +3,7 @@ package main import ( "context" "fmt" + "io" "os" "wherobots/cli/internal/commands" @@ -50,16 +51,45 @@ func run(ctx context.Context) error { root.Version = versionString root.Short = fmt.Sprintf("Wherobots CLI %s", buildVersion) commands.AddUpgradeCommand(root, buildVersion) + + // Suppress the "update available" notice when the user is already running + // `upgrade` — the check races with the upgrade itself and would otherwise + // nag about the very version that just got installed. + suppressNotice := commands.IsUpgradeInvocation(root, os.Args[1:]) + execErr := root.ExecuteContext(ctx) - // After the command finishes, print an update notice if one is available. - if result := version.Collect(updateCh); result != nil { - fmt.Fprintln(os.Stderr, "") - fmt.Fprintln(os.Stderr, version.FormatNotice(result)) - if execErr != nil { - fmt.Fprintln(os.Stderr, "Note: your CLI is out of date. Run `wherobots upgrade` to update — it may resolve this issue.") + if !suppressNotice { + if result := version.Collect(updateCh); result != nil { + fmt.Fprintln(os.Stderr, "") + printUpdateNotice(os.Stderr, result) + if execErr != nil { + fmt.Fprintln(os.Stderr, "Note: your CLI is out of date. Run `wherobots upgrade` to update — it may resolve this issue.") + } } } return execErr } + +func printUpdateNotice(w io.Writer, r *version.Result) { + notice := version.FormatNotice(r) + if isTTY(w) { + notice = "\033[1;33m" + notice + "\033[0m" + } + fmt.Fprintln(w, notice) +} + +// isTTY reports whether w is an *os.File pointing at a terminal. Stdlib only; +// avoids pulling in a tty-detection dependency for a single notice. +func isTTY(w io.Writer) bool { + f, ok := w.(*os.File) + if !ok { + return false + } + fi, err := f.Stat() + if err != nil { + return false + } + return (fi.Mode() & os.ModeCharDevice) != 0 +}