mirror of
https://github.com/luxfi/cli.git
synced 2026-10-30 17:01:20 +00:00
When VERSION is set via ldflags from git describe --tags (which includes the 'v' prefix), the code was adding another 'v', resulting in 'vv1.23.0'. Now checks if version already starts with 'v' before prepending.
187 lines
5.4 KiB
Go
187 lines
5.4 KiB
Go
// Copyright (C) 2022-2025, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package updatecmd
|
|
|
|
import (
|
|
"errors"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/luxfi/cli/pkg/application"
|
|
"github.com/luxfi/cli/pkg/binutils"
|
|
"github.com/luxfi/cli/pkg/prompts"
|
|
"github.com/luxfi/cli/pkg/ux"
|
|
"github.com/luxfi/constants"
|
|
"github.com/spf13/cobra"
|
|
"golang.org/x/mod/semver"
|
|
)
|
|
|
|
var (
|
|
// ErrUserAbortedInstallation is returned when user cancels an installation.
|
|
ErrUserAbortedInstallation = errors.New("user canceled installation")
|
|
ErrNoVersion = errors.New("failed to find current version - did you install following official instructions?")
|
|
app *application.Lux
|
|
yes bool
|
|
)
|
|
|
|
// NewCmd creates the update command.
|
|
func NewCmd(injectedApp *application.Lux, version string) *cobra.Command {
|
|
app = injectedApp
|
|
cmd := &cobra.Command{
|
|
Use: "update",
|
|
Short: "Check for latest updates of Lux CLI",
|
|
Long: `Check if an update is available, and prompt the user to install it`,
|
|
RunE: runUpdate,
|
|
Args: cobra.ExactArgs(0),
|
|
SilenceUsage: true,
|
|
Version: version,
|
|
}
|
|
|
|
cmd.Flags().BoolVarP(&yes, "confirm", "c", false, "Assume yes for installation")
|
|
return cmd
|
|
}
|
|
|
|
func runUpdate(cmd *cobra.Command, _ []string) error {
|
|
isUserCalled := true
|
|
return Update(cmd, isUserCalled, "")
|
|
}
|
|
|
|
// Update checks for and installs the latest CLI version.
|
|
func Update(cmd *cobra.Command, isUserCalled bool, version string) error {
|
|
// first check if there is a new version exists
|
|
url := binutils.GetGithubLatestReleaseURL(constants.LuxOrg, constants.CliRepoName)
|
|
latest, err := app.Downloader.GetLatestReleaseVersion(url)
|
|
if err != nil {
|
|
app.Log.Warn("failed to get latest version for cli from repo", "error", err)
|
|
// If not user-called, don't block on network errors
|
|
if !isUserCalled {
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
|
|
this := cmd.Version
|
|
if this == "" {
|
|
if version != "" {
|
|
this = version
|
|
} else {
|
|
verFile := "VERSION"
|
|
bver, err := os.ReadFile(verFile)
|
|
if err != nil {
|
|
app.Log.Warn("failed to read version from file on disk", "error", err)
|
|
return ErrNoVersion
|
|
}
|
|
this = string(bver)
|
|
}
|
|
}
|
|
thisVFmt := this
|
|
if !strings.HasPrefix(thisVFmt, "v") {
|
|
thisVFmt = "v" + thisVFmt
|
|
}
|
|
|
|
// check this version needs update
|
|
// we skip if compare returns -1 (latest < this)
|
|
// or 0 (latest == this)
|
|
if semver.Compare(latest, thisVFmt) < 1 {
|
|
txt := "No new version found upstream; skipping update"
|
|
app.Log.Debug(txt)
|
|
if isUserCalled {
|
|
ux.Logger.PrintToUser("%s", txt)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// If not user-called (e.g., background check), just log and return - never prompt
|
|
if !isUserCalled {
|
|
app.Log.Debug("New version available but skipping prompt (not user-called)", "current", thisVFmt, "latest", latest)
|
|
return nil
|
|
}
|
|
|
|
// flag not provided - prompt user or fail in non-interactive mode
|
|
if !yes {
|
|
ux.Logger.PrintToUser("We found a new version of Lux CLI %s upstream. You are running %s", latest, thisVFmt)
|
|
if !prompts.IsInteractive() {
|
|
ux.Logger.PrintToUser("Use --confirm/-c to auto-confirm update in non-interactive mode")
|
|
return ErrUserAbortedInstallation
|
|
}
|
|
y, err := app.Prompt.CaptureYesNo("Do you want to update?")
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
if !y {
|
|
ux.Logger.PrintToUser("Aborted by user")
|
|
return ErrUserAbortedInstallation
|
|
}
|
|
}
|
|
|
|
// where is the tool running from?
|
|
ex, err := os.Executable()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
execPath := filepath.Dir(ex)
|
|
defaultDir := filepath.Join(os.ExpandEnv("$HOME"), "bin")
|
|
/* #nosec G204 */
|
|
downloadCmd := exec.Command("curl", "-sSfL", constants.CliInstallationURL)
|
|
|
|
// -s is for the sh command, -- separates the args for our install script,
|
|
// -n skips shell completion installation, which would result in an error,
|
|
// as it requires to launch the binary, but we are already executing it
|
|
installCmdArgs := []string{"-s", "--", "-n"}
|
|
// custom installation path
|
|
if execPath != defaultDir {
|
|
installCmdArgs = append(installCmdArgs, "-b", execPath)
|
|
}
|
|
|
|
app.Log.Debug("installing new version", "path", execPath)
|
|
|
|
installCmd := exec.Command("sh", installCmdArgs...)
|
|
|
|
// redirect the download command to the install
|
|
installCmd.Stdin, err = downloadCmd.StdoutPipe()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// we are going to collect the output from the command into a string
|
|
// instead of writing directly to the string
|
|
var outbuf, errbuf strings.Builder
|
|
installCmd.Stdout = &outbuf
|
|
installCmd.Stderr = &errbuf
|
|
|
|
ux.Logger.PrintToUser("Starting update...")
|
|
if err := installCmd.Start(); err != nil {
|
|
return err
|
|
}
|
|
ux.Logger.PrintToUser("Downloading new release...")
|
|
if err := downloadCmd.Run(); err != nil {
|
|
return err
|
|
}
|
|
ux.Logger.PrintToUser("Installing new release...")
|
|
if err := installCmd.Wait(); err != nil {
|
|
ux.Logger.PrintToUser("installation failed: %s", err.Error())
|
|
return err
|
|
}
|
|
|
|
// write to file when last updated
|
|
lastActs, err := app.ReadLastActionsFile()
|
|
if err != nil {
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
lastActs = &application.LastActions{}
|
|
}
|
|
}
|
|
|
|
lastActs.LastUpdated = time.Now()
|
|
app.WriteLastActionsFile(lastActs)
|
|
|
|
app.Log.Debug(outbuf.String())
|
|
app.Log.Debug(errbuf.String())
|
|
ux.Logger.PrintToUser("Installation successful. Please run the shell completion update manually after this process terminates.")
|
|
ux.Logger.PrintToUser("The new version will be used on next command execution")
|
|
return nil
|
|
}
|