update.go

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