update.go

  1package cmd
  2
  3import (
  4	"context"
  5	"fmt"
  6	"os"
  7	"runtime"
  8	"time"
  9
 10	"charm.land/lipgloss/v2"
 11	"github.com/charmbracelet/crush/internal/format"
 12	"github.com/charmbracelet/crush/internal/tui/components/anim"
 13	"github.com/charmbracelet/crush/internal/tui/styles"
 14	"github.com/charmbracelet/crush/internal/update"
 15	"github.com/charmbracelet/crush/internal/version"
 16	"github.com/charmbracelet/x/term"
 17	"github.com/spf13/cobra"
 18)
 19
 20var updateCmd = &cobra.Command{
 21	Use:   "update",
 22	Short: "Check for and apply updates",
 23	Long: `Check if a new version of Crush is available.
 24Use 'update apply' to download and install the latest version.`,
 25	Example: `
 26# Check if an update is available
 27crush update
 28
 29# Apply the update if available
 30crush update apply
 31
 32# Force re-download even if already on latest version
 33crush update apply --force
 34  `,
 35	RunE: func(cmd *cobra.Command, args []string) error {
 36		ctx, cancel := context.WithTimeout(cmd.Context(), 30*time.Second)
 37		defer cancel()
 38
 39		spinner := newUpdateSpinner(ctx, cancel, "Checking for updates")
 40		spinner.Start()
 41
 42		info, err := update.Check(ctx, version.Version, update.Default)
 43		spinner.Stop()
 44
 45		if err != nil {
 46			return fmt.Errorf("failed to check for updates: %w", err)
 47		}
 48
 49		if info.IsDevelopment() {
 50			fmt.Fprint(os.Stderr, info.DevelopmentVersionMessage(false))
 51			return nil
 52		}
 53
 54		if !info.Available() {
 55			fmt.Fprintf(os.Stderr, "You are already running the latest version (v%s).\n", info.Current)
 56			return nil
 57		}
 58
 59		// Check install method and provide appropriate instructions.
 60		method := update.DetectInstallMethod()
 61		if !method.CanSelfUpdate() {
 62			fmt.Fprintf(os.Stderr, "Update available: v%s → v%s\n", info.Current, info.Latest)
 63			fmt.Fprintf(os.Stderr, "Crush was installed via %s. To update, run:\n", method)
 64			fmt.Fprintf(os.Stderr, "  %s\n", method.UpdateInstructions())
 65			return nil
 66		}
 67
 68		fmt.Fprintf(os.Stderr, "Update available: v%s → v%s\n", info.Current, info.Latest)
 69		fmt.Fprintf(os.Stderr, "Run 'crush update apply' to install the latest version.\n")
 70		fmt.Fprintf(os.Stderr, "Or visit %s to download manually.\n", info.URL)
 71
 72		return nil
 73	},
 74}
 75
 76var updateApplyCmd = &cobra.Command{
 77	Use:   "apply",
 78	Short: "Apply the latest update",
 79	Long:  `Download and install the latest version of Crush.`,
 80	Example: `
 81# Apply the latest update
 82crush update apply
 83
 84# Force re-download even if already on latest version
 85crush update apply --force
 86  `,
 87	RunE: func(cmd *cobra.Command, args []string) error {
 88		force, _ := cmd.Flags().GetBool("force")
 89
 90		ctx, cancel := context.WithTimeout(cmd.Context(), 5*time.Minute)
 91		defer cancel()
 92
 93		// Check install method first.
 94		method := update.DetectInstallMethod()
 95		if !method.CanSelfUpdate() {
 96			fmt.Fprintf(os.Stderr, "Crush was installed via %s.\n", method)
 97			fmt.Fprintf(os.Stderr, "Self-update is not supported for this installation method.\n")
 98			fmt.Fprintf(os.Stderr, "To update, run:\n")
 99			fmt.Fprintf(os.Stderr, "  %s\n", method.UpdateInstructions())
100			return nil
101		}
102
103		spinner := newUpdateSpinner(ctx, cancel, "Checking for updates")
104		spinner.Start()
105
106		info, err := update.Check(ctx, version.Version, update.Default)
107		if err != nil {
108			spinner.Stop()
109			return fmt.Errorf("failed to check for updates: %w", err)
110		}
111
112		if info.IsDevelopment() {
113			spinner.Stop()
114			fmt.Fprint(os.Stderr, info.DevelopmentVersionMessage(true))
115			return nil
116		}
117
118		if !info.Available() && !force {
119			spinner.Stop()
120			fmt.Fprintf(os.Stderr, "You are already running the latest version (v%s).\n", info.Current)
121			fmt.Fprintf(os.Stderr, "Use --force to re-download and reinstall.\n")
122			return nil
123		}
124
125		// Find the appropriate asset for this platform.
126		asset, err := update.FindAsset(info.Release.Assets)
127		if err != nil {
128			spinner.Stop()
129			return fmt.Errorf("failed to find update for your platform: %w", err)
130		}
131
132		spinner.Stop()
133		spinner = newUpdateSpinner(ctx, cancel, fmt.Sprintf("Downloading v%s", info.Latest))
134		spinner.Start()
135
136		// Download the asset.
137		binaryPath, err := update.Download(ctx, asset, info.Release)
138		if err != nil {
139			spinner.Stop()
140			return fmt.Errorf("failed to download update: %w", err)
141		}
142		defer os.Remove(binaryPath)
143
144		spinner.Stop()
145		spinner = newUpdateSpinner(ctx, cancel, "Installing")
146		spinner.Start()
147
148		// Apply the update.
149		if err := update.Apply(binaryPath); err != nil {
150			spinner.Stop()
151			return fmt.Errorf("failed to apply update: %w", err)
152		}
153
154		spinner.Stop()
155
156		if force && !info.Available() {
157			fmt.Fprintf(os.Stderr, "Successfully reinstalled v%s!\n", info.Latest)
158		} else {
159			fmt.Fprintf(os.Stderr, "Successfully updated to v%s!\n", info.Latest)
160		}
161		if runtime.GOOS == "windows" {
162			fmt.Fprintf(os.Stderr, "Restart Crush to use the new version.\n")
163		} else {
164			fmt.Fprintf(os.Stderr, "Run 'crush -v' to verify the new version.\n")
165		}
166
167		return nil
168	},
169}
170
171// newUpdateSpinner creates a spinner for update operations.
172func newUpdateSpinner(ctx context.Context, cancel context.CancelFunc, label string) *format.Spinner {
173	t := styles.CurrentTheme()
174
175	// Detect background color for appropriate text color.
176	hasDarkBG := true
177	if term.IsTerminal(os.Stderr.Fd()) {
178		hasDarkBG = lipgloss.HasDarkBackground(os.Stdin, os.Stderr)
179	}
180	defaultFG := lipgloss.LightDark(hasDarkBG)(lipgloss.Color("#fafafa"), t.FgBase)
181
182	return format.NewSpinner(ctx, cancel, anim.Settings{
183		Size:        10,
184		Label:       label,
185		LabelColor:  defaultFG,
186		GradColorA:  t.Primary,
187		GradColorB:  t.Secondary,
188		CycleColors: true,
189	})
190}
191
192func init() {
193	updateApplyCmd.Flags().BoolP("force", "f", false, "Force re-download even if already on latest version")
194	updateCmd.AddCommand(updateApplyCmd)
195}