feat(cli): add upgrade command

This commit is contained in:
Jan De Dobbeleer 2024-05-31 16:02:34 +02:00 committed by Jan De Dobbeleer
parent bbe9f9bc02
commit a0d2289aec
10 changed files with 299 additions and 5 deletions

10
.vscode/launch.json vendored
View file

@ -145,6 +145,16 @@
"git"
]
},
{
"name": "Upgrade",
"type": "go",
"request": "launch",
"mode": "debug",
"program": "${workspaceRoot}/src",
"args": [
"upgrade"
]
},
{
"type": "node",
"request": "launch",

View file

@ -21,7 +21,7 @@ var noticeCmd = &cobra.Command{
env.Init()
defer env.Close()
if notice, hasNotice := upgrade.Notice(env); hasNotice {
if notice, hasNotice := upgrade.Notice(env, false); hasNotice {
fmt.Println(notice)
}
},

43
src/cli/upgrade.go Normal file
View file

@ -0,0 +1,43 @@
package cli
import (
"fmt"
"github.com/jandedobbeleer/oh-my-posh/src/platform"
"github.com/jandedobbeleer/oh-my-posh/src/upgrade"
"github.com/spf13/cobra"
)
var force bool
// noticeCmd represents the get command
var upgradeCmd = &cobra.Command{
Use: "upgrade",
Short: "Upgrade when a new version is available.",
Long: "Upgrade when a new version is available.",
Args: cobra.NoArgs,
Run: func(_ *cobra.Command, _ []string) {
if force {
upgrade.Run()
return
}
env := &platform.Shell{
CmdFlags: &platform.Flags{},
}
env.Init()
defer env.Close()
if _, hasNotice := upgrade.Notice(env, true); !hasNotice {
fmt.Print("\n ✅ no new version available\n\n")
return
}
upgrade.Run()
},
}
func init() {
upgradeCmd.Flags().BoolVarP(&force, "force", "f", false, "force the upgrade even if the version is up to date")
RootCmd.AddCommand(upgradeCmd)
}

View file

@ -160,6 +160,7 @@ func (e *Engine) ExtraPrompt(promptType ExtraPromptType) string {
Template: getTemplate(prompt.Template),
Env: e.Env,
}
promptText, err := tmpl.Render()
if err != nil {
promptText = err.Error()

View file

@ -268,7 +268,7 @@ func PrintInit(env platform.Environment) string {
// only run this for shells that support
// injecting the notice directly
if shell != PWSH && shell != PWSH5 {
notice, hasNotice = upgrade.Notice(env)
notice, hasNotice = upgrade.Notice(env, false)
}
return strings.NewReplacer(

133
src/upgrade/cli.go Normal file
View file

@ -0,0 +1,133 @@
package upgrade
import (
"context"
"fmt"
"io"
"net/http"
"os"
"github.com/charmbracelet/bubbles/spinner"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/jandedobbeleer/oh-my-posh/src/platform"
)
var (
program *tea.Program
textStyle = lipgloss.NewStyle().Margin(1, 0, 2, 4)
)
type resultMsg string
type state int
const (
validating state = iota
downloading
installing
)
type stateMsg state
type model struct {
spinner spinner.Model
message string
state state
}
func initialModel() *model {
s := spinner.New()
s.Spinner = spinner.Dot
s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("170"))
return &model{spinner: s}
}
func (m *model) Init() tea.Cmd {
defer func() {
go func() {
if err := install(); err != nil {
message := fmt.Sprintf("⚠️ %s", err)
program.Send(resultMsg(message))
return
}
program.Send(resultMsg(successMsg))
}()
}()
return m.spinner.Tick
}
func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "q", "esc", "ctrl+c":
return m, tea.Quit
default:
return m, nil
}
case resultMsg:
m.message = string(msg)
return m, tea.Quit
case stateMsg:
m.state = state(msg)
return m, nil
default:
var cmd tea.Cmd
m.spinner, cmd = m.spinner.Update(msg)
return m, cmd
}
}
func (m *model) View() string {
if len(m.message) > 0 {
return textStyle.Render(m.message)
}
var message string
m.spinner.Spinner = spinner.Dot
switch m.state {
case validating:
message = "Validating current installation"
case downloading:
m.spinner.Spinner = spinner.Globe
message = "Downloading latest version"
case installing:
message = "Installing"
}
return textStyle.Render(fmt.Sprintf("%s %s", m.spinner.View(), message))
}
func Run() {
program = tea.NewProgram(initialModel())
if _, err := program.Run(); err != nil {
fmt.Println(err)
os.Exit(1)
}
}
func downloadAsset(asset string) (io.ReadCloser, error) {
url := fmt.Sprintf("https://github.com/JanDeDobbeleer/oh-my-posh/releases/latest/download/%s", asset)
req, err := http.NewRequestWithContext(context.Background(), "GET", url, nil)
if err != nil {
return nil, err
}
resp, err := platform.Client.Do(req)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("Failed to download installer: %s", url)
}
return resp.Body, nil
}

48
src/upgrade/cli_unix.go Normal file
View file

@ -0,0 +1,48 @@
//go:build !windows
package upgrade
import (
"errors"
"fmt"
"io"
"os"
"runtime"
)
var successMsg = "Upgrade successful, restart your shell to take full advantage of the new functionality."
func install() error {
program.Send(stateMsg(validating))
executable, err := os.Executable()
if err != nil {
return err
}
file, err := os.OpenFile(executable, os.O_WRONLY, 0755)
if err != nil {
return errors.New("we don't have permissions to upgrade oh-my-posh, please use elevated permissions to upgrade")
}
defer file.Close()
program.Send(stateMsg(downloading))
asset := fmt.Sprintf("posh-%s-%s", runtime.GOOS, runtime.GOARCH)
data, err := downloadAsset(asset)
if err != nil {
return err
}
defer data.Close()
program.Send(stateMsg(installing))
_, err = io.Copy(file, data)
if err != nil {
return err
}
return nil
}

View file

@ -0,0 +1,59 @@
package upgrade
import (
"errors"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"runtime"
)
var successMsg = "Oh My Posh is installing in the background.\nRestart your shell in a minute to take full advantage of the new functionality."
func install() error {
program.Send(stateMsg(downloading))
temp := os.Getenv("TEMP")
if len(temp) == 0 {
return errors.New("failed to get TEMP environment variable")
}
path := filepath.Join(temp, "install.exe")
if _, err := os.Stat(path); err == nil {
err := os.Remove(path)
if err != nil {
return errors.New("unable to remove existing installer")
}
}
file, err := os.Create(path)
if err != nil {
return err
}
asset := fmt.Sprintf("install-%s.exe", runtime.GOARCH)
data, err := downloadAsset(asset)
if err != nil {
return err
}
_, err = io.Copy(file, data)
if err != nil {
return err
}
data.Close()
file.Close()
// We need to run the installer in a separate process to avoid being blocked by the current process
go func() {
cmd := exec.Command(path, "/VERYSILENT", "/FORCECLOSEAPPLICATIONS")
_ = cmd.Run()
}()
return nil
}

View file

@ -58,9 +58,9 @@ func Latest(env platform.Environment) (string, error) {
// that should be displayed to the user.
//
// The upgrade check is only performed every other week.
func Notice(env platform.Environment) (string, bool) {
func Notice(env platform.Environment, force bool) (string, bool) {
// do not check when last validation was < 1 week ago
if _, OK := env.Cache().Get(CACHEKEY); OK {
if _, OK := env.Cache().Get(CACHEKEY); OK && !force {
return "", false
}

View file

@ -45,7 +45,7 @@ func TestCanUpgrade(t *testing.T) {
json := fmt.Sprintf(`{"tag_name":"%s"}`, tc.LatestVersion)
env.On("HTTPRequest", RELEASEURL).Return([]byte(json), tc.Error)
// ignore the notice
_, canUpgrade := Notice(env)
_, canUpgrade := Notice(env, false)
assert.Equal(t, tc.Expected, canUpgrade, tc.Case)
}
}