@@ -10,30 +10,38 @@ import (
1010 "os"
1111 "path/filepath"
1212 "runtime"
13+ "strconv"
1314 "strings"
1415
16+ "github.com/fatih/color"
1517 "github.com/spf13/cobra"
1618)
1719
1820const (
1921 repoOwner = "karol-broda"
2022 repoName = "snitch"
2123 githubAPI = "https://api.github.com"
24+ firstUpgradeVersion = "0.1.8"
2225)
2326
24- var upgradeYes bool
27+ var (
28+ upgradeYes bool
29+ upgradeVersion string
30+ )
2531
2632var upgradeCmd = & cobra.Command {
2733 Use : "upgrade" ,
2834 Short : "Check for updates and optionally upgrade snitch" ,
2935 Long : `Check for available updates and show upgrade instructions.
3036
31- Use --yes to perform an in-place upgrade automatically.` ,
37+ Use --yes to perform an in-place upgrade automatically.
38+ Use --version to install a specific version.` ,
3239 RunE : runUpgrade ,
3340}
3441
3542func init () {
3643 upgradeCmd .Flags ().BoolVarP (& upgradeYes , "yes" , "y" , false , "Perform the upgrade automatically" )
44+ upgradeCmd .Flags ().StringVarP (& upgradeVersion , "version" , "v" , "" , "Install a specific version (e.g., v0.1.7)" )
3745 rootCmd .AddCommand (upgradeCmd )
3846}
3947
@@ -44,6 +52,11 @@ type githubRelease struct {
4452
4553func runUpgrade (cmd * cobra.Command , args []string ) error {
4654 current := Version
55+
56+ if upgradeVersion != "" {
57+ return handleSpecificVersion (current , upgradeVersion )
58+ }
59+
4760 latest , err := fetchLatestVersion ()
4861 if err != nil {
4962 return fmt .Errorf ("failed to check for updates: %w" , err )
@@ -52,35 +65,113 @@ func runUpgrade(cmd *cobra.Command, args []string) error {
5265 currentClean := strings .TrimPrefix (current , "v" )
5366 latestClean := strings .TrimPrefix (latest , "v" )
5467
55- fmt .Printf ("current: %s\n " , current )
56- fmt .Printf ("latest: %s\n " , latest )
57- fmt .Println ()
68+ printVersionComparison (current , latest )
5869
5970 if currentClean == latestClean {
60- fmt .Println ("you are running the latest version" )
71+ green := color .New (color .FgGreen )
72+ green .Println ("✓ you are running the latest version" )
6173 return nil
6274 }
6375
6476 if current == "dev" {
65- fmt .Println ("you are running a development build" )
77+ yellow := color .New (color .FgYellow )
78+ yellow .Println ("⚠ you are running a development build" )
79+ fmt .Println ()
6680 fmt .Println ("use one of the methods below to install a release version:" )
81+ fmt .Println ()
6782 printUpgradeInstructions ()
6883 return nil
6984 }
7085
71- fmt .Printf ("update available: %s -> %s\n " , current , latest )
86+ green := color .New (color .FgGreen , color .Bold )
87+ green .Printf ("✓ update available: %s → %s\n " , current , latest )
7288 fmt .Println ()
7389
7490 if ! upgradeYes {
7591 printUpgradeInstructions ()
7692 fmt .Println ()
77- fmt .Println ("or run 'snitch upgrade --yes' to upgrade in-place" )
93+ faint := color .New (color .Faint )
94+ cmdStyle := color .New (color .FgCyan )
95+ faint .Print (" in-place " )
96+ cmdStyle .Println ("snitch upgrade --yes" )
7897 return nil
7998 }
8099
81100 return performUpgrade (latest )
82101}
83102
103+ func handleSpecificVersion (current , target string ) error {
104+ if ! strings .HasPrefix (target , "v" ) {
105+ target = "v" + target
106+ }
107+ targetClean := strings .TrimPrefix (target , "v" )
108+
109+ printVersionComparisonTarget (current , target )
110+
111+ if isVersionLower (targetClean , firstUpgradeVersion ) {
112+ yellow := color .New (color .FgYellow )
113+ yellow .Printf ("⚠ warning: the upgrade command was introduced in v%s\n " , firstUpgradeVersion )
114+ faint := color .New (color .Faint )
115+ faint .Printf (" version %s does not include this command\n " , target )
116+ faint .Println (" you will need to use other methods to upgrade from that version" )
117+ fmt .Println ()
118+ }
119+
120+ currentClean := strings .TrimPrefix (current , "v" )
121+ if currentClean == targetClean {
122+ green := color .New (color .FgGreen )
123+ green .Println ("✓ you are already running this version" )
124+ return nil
125+ }
126+
127+ if ! upgradeYes {
128+ faint := color .New (color .Faint )
129+ cmdStyle := color .New (color .FgCyan )
130+ if isVersionLower (targetClean , currentClean ) {
131+ yellow := color .New (color .FgYellow )
132+ yellow .Printf ("↓ this will downgrade from %s to %s\n " , current , target )
133+ } else {
134+ green := color .New (color .FgGreen )
135+ green .Printf ("↑ this will upgrade from %s to %s\n " , current , target )
136+ }
137+ fmt .Println ()
138+ faint .Print ("run " )
139+ cmdStyle .Printf ("snitch upgrade --version %s --yes" , target )
140+ faint .Println (" to proceed" )
141+ return nil
142+ }
143+
144+ return performUpgrade (target )
145+ }
146+
147+ func isVersionLower (v1 , v2 string ) bool {
148+ parts1 := parseVersion (v1 )
149+ parts2 := parseVersion (v2 )
150+
151+ for i := 0 ; i < 3 ; i ++ {
152+ if parts1 [i ] < parts2 [i ] {
153+ return true
154+ }
155+ if parts1 [i ] > parts2 [i ] {
156+ return false
157+ }
158+ }
159+ return false
160+ }
161+
162+ func parseVersion (v string ) [3 ]int {
163+ var parts [3 ]int
164+ segments := strings .Split (v , "." )
165+
166+ for i := 0 ; i < len (segments ) && i < 3 ; i ++ {
167+ n , err := strconv .Atoi (segments [i ])
168+ if err == nil {
169+ parts [i ] = n
170+ }
171+ }
172+ return parts
173+ }
174+
84175func fetchLatestVersion () (string , error ) {
85176 url := fmt .Sprintf ("%s/repos/%s/%s/releases/latest" , githubAPI , repoOwner , repoName )
86177
@@ -106,20 +197,47 @@ func fetchLatestVersion() (string, error) {
106197 return release .TagName , nil
107198}
108199
109- func printUpgradeInstructions () {
110- fmt .Println ("upgrade options:" )
111- fmt .Println ()
112- fmt .Println (" go install:" )
113- fmt .Printf (" go install github.com/%s/%s@latest\n " , repoOwner , repoName )
200+ func printVersionComparison (current , latest string ) {
201+ faint := color .New (color .Faint )
202+ version := color .New (color .FgCyan )
203+
204+ faint .Print ("current " )
205+ version .Println (current )
206+ faint .Print ("latest " )
207+ version .Println (latest )
114208 fmt .Println ()
115- fmt .Println (" shell script:" )
116- fmt .Printf (" curl -sSL https://raw.githubusercontent.com/%s/%s/master/install.sh | sh\n " , repoOwner , repoName )
209+ }
210+
211+ func printVersionComparisonTarget (current , target string ) {
212+ faint := color .New (color .Faint )
213+ version := color .New (color .FgCyan )
214+
215+ faint .Print ("current " )
216+ version .Println (current )
217+ faint .Print ("target " )
218+ version .Println (target )
117219 fmt .Println ()
118- fmt .Println (" arch linux (aur):" )
119- fmt .Println (" yay -S snitch-bin" )
220+ }
221+
222+ func printUpgradeInstructions () {
223+ bold := color .New (color .Bold )
224+ faint := color .New (color .Faint )
225+ cmd := color .New (color .FgCyan )
226+
227+ bold .Println ("upgrade options:" )
120228 fmt .Println ()
121- fmt .Println (" nix:" )
122- fmt .Printf (" nix profile upgrade --inputs-from github:%s/%s\n " , repoOwner , repoName )
229+
230+ faint .Print (" go install " )
231+ cmd .Printf ("go install github.com/%s/%s@latest\n " , repoOwner , repoName )
232+
233+ faint .Print (" shell script " )
234+ cmd .Printf ("curl -sSL https://raw.githubusercontent.com/%s/%s/master/install.sh | sh\n " , repoOwner , repoName )
235+
236+ faint .Print (" arch (aur) " )
237+ cmd .Println ("yay -S snitch-bin" )
238+
239+ faint .Print (" nix " )
240+ cmd .Printf ("nix profile upgrade --inputs-from github:%s/%s\n " , repoOwner , repoName )
123241}
124242
125243func performUpgrade (version string ) error {
@@ -141,7 +259,11 @@ func performUpgrade(version string) error {
141259 downloadURL := fmt .Sprintf ("https://github.com/%s/%s/releases/download/%s/%s" ,
142260 repoOwner , repoName , version , archiveName )
143261
144- fmt .Printf ("downloading %s...\n " , archiveName )
262+ faint := color .New (color .Faint )
263+ cyan := color .New (color .FgCyan )
264+ faint .Print ("↓ downloading " )
265+ cyan .Printf ("%s" , archiveName )
266+ faint .Println ("..." )
145267
146268 resp , err := http .Get (downloadURL )
147269 if err != nil {
@@ -167,13 +289,17 @@ func performUpgrade(version string) error {
167289 // check if we can write to the target location
168290 targetDir := filepath .Dir (execPath )
169291 if ! isWritable (targetDir ) {
170- fmt .Printf ("elevated permissions required to install to %s\n " , targetDir )
292+ yellow := color .New (color .FgYellow )
293+ cmdStyle := color .New (color .FgCyan )
294+
295+ yellow .Printf ("⚠ elevated permissions required to install to %s\n " , targetDir )
171296 fmt .Println ()
172- fmt .Println ("run with sudo or install to a user-writable location:" )
173- fmt .Printf (" sudo snitch upgrade --yes\n " )
297+ faint .Println ("run with sudo or install to a user-writable location:" )
174298 fmt .Println ()
175- fmt .Println ("or use the install script with a custom directory:" )
176- fmt .Printf (" curl -sSL https://raw.githubusercontent.com/%s/%s/master/install.sh | INSTALL_DIR=~/.local/bin sh\n " ,
299+ faint .Print (" sudo " )
300+ cmdStyle .Println ("sudo snitch upgrade --yes" )
301+ faint .Print (" custom dir " )
302+ cmdStyle .Printf ("curl -sSL https://raw.githubusercontent.com/%s/%s/master/install.sh | INSTALL_DIR=~/.local/bin sh\n " ,
177303 repoOwner , repoName )
178304 return nil
179305 }
@@ -198,10 +324,12 @@ func performUpgrade(version string) error {
198324
199325 if err := os .Remove (backupPath ); err != nil {
200326 // non-fatal, just warn
201- fmt .Fprintf (os .Stderr , "warning: failed to remove backup file %s: %v\n " , backupPath , err )
327+ yellow := color .New (color .FgYellow )
328+ yellow .Fprintf (os .Stderr , "⚠ warning: failed to remove backup file %s: %v\n " , backupPath , err )
202329 }
203330
204- fmt .Printf ("successfully upgraded to %s\n " , version )
331+ green := color .New (color .FgGreen , color .Bold )
332+ green .Printf ("✓ successfully upgraded to %s\n " , version )
205333 return nil
206334}
207335
0 commit comments