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