diff --git a/pkg/cmd/plugin/install.go b/pkg/cmd/plugin/install.go index 93d73a8f..6247a569 100644 --- a/pkg/cmd/plugin/install.go +++ b/pkg/cmd/plugin/install.go @@ -9,6 +9,7 @@ import ( "strings" "syscall" + goversion "github.com/hashicorp/go-version" log "github.com/sirupsen/logrus" "github.com/spf13/afero" "github.com/spf13/cobra" @@ -65,55 +66,69 @@ func parseInstallArg(arg string) (string, string) { return plugin, version } -func (ic *InstallCmd) installPluginByName(cmd *cobra.Command, arg string) (version string, err error) { - pluginName, version := parseInstallArg(arg) +func (ic *InstallCmd) runInstallCmd(cmd *cobra.Command, args []string) error { + if err := stripe.ValidateAPIBaseURL(ic.apiBaseURL); err != nil { + return err + } - plugin, err := plugins.LookUpPlugin(cmd.Context(), ic.cfg, ic.fs, pluginName) + color := ansi.Color(os.Stdout) + // Refresh the plugin before proceeding + if err := plugins.RefreshPluginManifest(cmd.Context(), ic.cfg, ic.fs, ic.apiBaseURL); err != nil { + return err + } + + pluginName, version := parseInstallArg(args[0]) + + plugin, err := plugins.LookUpPlugin(cmd.Context(), ic.cfg, ic.fs, pluginName) if err != nil { - return version, err + return err } - if len(version) == 0 { + isLatest := len(version) == 0 + if isLatest { version = plugin.LookUpLatestVersion() } + if plugin.IsVersionInstalled(ic.cfg, ic.fs, version) { + if isLatest { + fmt.Println(color.Green(fmt.Sprintf("✔ v%s is already installed (latest).", version))) + } else { + fmt.Println(color.Green(fmt.Sprintf("✔ v%s is already installed.", version))) + } + return nil + } + + prevVersion := plugin.InstalledVersion(ic.cfg, ic.fs) + ctx := withSIGTERMCancel(cmd.Context(), func() { log.WithFields(log.Fields{ "prefix": "cmd.installCmd.runInstallCmd", }).Debug("Ctrl+C received, cleaning up...") }) - err = plugin.Install(ctx, ic.cfg, ic.fs, version, ic.apiBaseURL) - - return version, err -} - -func (ic *InstallCmd) runInstallCmd(cmd *cobra.Command, args []string) error { - if err := stripe.ValidateAPIBaseURL(ic.apiBaseURL); err != nil { + if err := plugin.Install(ctx, ic.cfg, ic.fs, version, ic.apiBaseURL); err != nil { return err } - var err error - var version string - color := ansi.Color(os.Stdout) - - // Refresh the plugin before proceeding - err = plugins.RefreshPluginManifest(cmd.Context(), ic.cfg, ic.fs, ic.apiBaseURL) - if err != nil { - return err + if prevVersion != "" { + fmt.Println(color.Green(fmt.Sprintf("✔ %s from v%s to v%s.", versionChangeVerb(prevVersion, version), prevVersion, version))) + } else { + fmt.Println(color.Green(fmt.Sprintf("✔ installation of v%s complete.", version))) } - version, err = ic.installPluginByName(cmd, args[0]) - if err != nil { - return err - } - - fmt.Println(color.Green(fmt.Sprintf("✔ installation of v%s complete.", version))) - return nil } +func versionChangeVerb(from, to string) string { + prev, prevErr := goversion.NewVersion(from) + next, nextErr := goversion.NewVersion(to) + if prevErr == nil && nextErr == nil && prev.GreaterThan(next) { + return "downgraded" + } + return "upgraded" +} + func withSIGTERMCancel(ctx context.Context, onCancel func()) context.Context { // Create a context that will be canceled when Ctrl+C is pressed ctx, cancel := context.WithCancel(ctx) diff --git a/pkg/cmd/plugin/upgrade.go b/pkg/cmd/plugin/upgrade.go index a63c7c44..5e126d22 100644 --- a/pkg/cmd/plugin/upgrade.go +++ b/pkg/cmd/plugin/upgrade.go @@ -70,13 +70,24 @@ func (uc *UpgradeCmd) runUpgradeCmd(cmd *cobra.Command, args []string) error { version := plugin.LookUpLatestVersion() - err = plugin.Install(ctx, uc.cfg, uc.fs, version, uc.apiBaseURL) + color := ansi.Color(os.Stdout) - if err == nil { - color := ansi.Color(os.Stdout) - successMsg := fmt.Sprintf("✔ upgrade to v%s complete.", version) - fmt.Println(color.Green(successMsg)) + if plugin.IsVersionInstalled(uc.cfg, uc.fs, version) { + fmt.Println(color.Green(fmt.Sprintf("✔ v%s is already installed (latest).", version))) + return nil } - return err + prevVersion := plugin.InstalledVersion(uc.cfg, uc.fs) + + if err := plugin.Install(ctx, uc.cfg, uc.fs, version, uc.apiBaseURL); err != nil { + return err + } + + if prevVersion != "" { + fmt.Println(color.Green(fmt.Sprintf("✔ %s from v%s to v%s.", versionChangeVerb(prevVersion, version), prevVersion, version))) + } else { + fmt.Println(color.Green(fmt.Sprintf("✔ upgrade to v%s complete.", version))) + } + + return nil } diff --git a/pkg/plugins/plugin.go b/pkg/plugins/plugin.go index 28ed8c96..35036538 100644 --- a/pkg/plugins/plugin.go +++ b/pkg/plugins/plugin.go @@ -215,6 +215,33 @@ func (p *Plugin) getReleaseForVersion(version string) *Release { return nil } +// IsVersionInstalled returns true if the given version of the plugin is already installed on disk. +func (p *Plugin) IsVersionInstalled(config config.IConfig, fs afero.Fs, version string) bool { + pluginDir := p.getPluginInstallPath(config, version) + pluginBinaryPath := filepath.Join(pluginDir, p.Binary) + GetBinaryExtension() + _, err := fs.Stat(pluginBinaryPath) + return err == nil +} + +// InstalledVersion returns the currently installed version of the plugin, or empty string if none. +func (p *Plugin) InstalledVersion(config config.IConfig, fs afero.Fs) string { + pluginsDir := getPluginsDir(config) + pluginDir := filepath.Join(pluginsDir, p.Shortname) + + entries, err := afero.ReadDir(fs, pluginDir) + if err != nil { + return "" + } + + for _, entry := range entries { + if entry.IsDir() { + return entry.Name() + } + } + + return "" +} + // Install installs the plugin of the given version func (p *Plugin) Install(ctx context.Context, cfg config.IConfig, fs afero.Fs, version string, baseURL string) error { spinner := ansi.StartNewSpinner(ansi.Faint(fmt.Sprintf("installing '%s' v%s...", p.Shortname, version)), os.Stdout)