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}