feat: enhance install script and upgrade command

This commit is contained in:
Karol Broda
2025-12-21 21:19:07 +01:00
parent eadd1b3452
commit 0252087bd0
3 changed files with 175 additions and 32 deletions

View File

@@ -50,6 +50,8 @@ installs to `~/.local/bin` if available, otherwise `/usr/local/bin`. override wi
curl -sSL https://raw.githubusercontent.com/karol-broda/snitch/master/install.sh | INSTALL_DIR=~/bin sh curl -sSL https://raw.githubusercontent.com/karol-broda/snitch/master/install.sh | INSTALL_DIR=~/bin sh
``` ```
> **macos:** the install script automatically removes the quarantine attribute (`com.apple.quarantine`) from the binary to allow it to run without gatekeeper warnings. to disable this, set `KEEP_QUARANTINE=1`.
### binary ### binary
download from [releases](https://github.com/karol-broda/snitch/releases): download from [releases](https://github.com/karol-broda/snitch/releases):
@@ -144,6 +146,16 @@ snitch watch -i 1s | jq '.count'
snitch watch -l -i 500ms snitch watch -l -i 500ms
``` ```
### `snitch upgrade`
check for updates and upgrade in-place.
```bash
snitch upgrade # check for updates
snitch upgrade --yes # upgrade automatically
snitch upgrade -v 0.1.7 # install specific version
```
## filters ## filters
shortcut flags work on all commands: shortcut flags work on all commands:

View File

@@ -10,8 +10,10 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"runtime" "runtime"
"strconv"
"strings" "strings"
"github.com/fatih/color"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@@ -19,21 +21,27 @@ const (
repoOwner = "karol-broda" repoOwner = "karol-broda"
repoName = "snitch" repoName = "snitch"
githubAPI = "https://api.github.com" githubAPI = "https://api.github.com"
firstUpgradeVersion = "0.1.8"
) )
var upgradeYes bool var (
upgradeYes bool
upgradeVersion string
)
var upgradeCmd = &cobra.Command{ var upgradeCmd = &cobra.Command{
Use: "upgrade", Use: "upgrade",
Short: "Check for updates and optionally upgrade snitch", Short: "Check for updates and optionally upgrade snitch",
Long: `Check for available updates and show upgrade instructions. Long: `Check for available updates and show upgrade instructions.
Use --yes to perform an in-place upgrade automatically.`, Use --yes to perform an in-place upgrade automatically.
Use --version to install a specific version.`,
RunE: runUpgrade, RunE: runUpgrade,
} }
func init() { func init() {
upgradeCmd.Flags().BoolVarP(&upgradeYes, "yes", "y", false, "Perform the upgrade automatically") upgradeCmd.Flags().BoolVarP(&upgradeYes, "yes", "y", false, "Perform the upgrade automatically")
upgradeCmd.Flags().StringVarP(&upgradeVersion, "version", "v", "", "Install a specific version (e.g., v0.1.7)")
rootCmd.AddCommand(upgradeCmd) rootCmd.AddCommand(upgradeCmd)
} }
@@ -44,6 +52,11 @@ type githubRelease struct {
func runUpgrade(cmd *cobra.Command, args []string) error { func runUpgrade(cmd *cobra.Command, args []string) error {
current := Version current := Version
if upgradeVersion != "" {
return handleSpecificVersion(current, upgradeVersion)
}
latest, err := fetchLatestVersion() latest, err := fetchLatestVersion()
if err != nil { if err != nil {
return fmt.Errorf("failed to check for updates: %w", err) return fmt.Errorf("failed to check for updates: %w", err)
@@ -52,35 +65,113 @@ func runUpgrade(cmd *cobra.Command, args []string) error {
currentClean := strings.TrimPrefix(current, "v") currentClean := strings.TrimPrefix(current, "v")
latestClean := strings.TrimPrefix(latest, "v") latestClean := strings.TrimPrefix(latest, "v")
fmt.Printf("current: %s\n", current) printVersionComparison(current, latest)
fmt.Printf("latest: %s\n", latest)
fmt.Println()
if currentClean == latestClean { if currentClean == latestClean {
fmt.Println("you are running the latest version") green := color.New(color.FgGreen)
green.Println("✓ you are running the latest version")
return nil return nil
} }
if current == "dev" { if current == "dev" {
fmt.Println("you are running a development build") yellow := color.New(color.FgYellow)
yellow.Println("⚠ you are running a development build")
fmt.Println()
fmt.Println("use one of the methods below to install a release version:") fmt.Println("use one of the methods below to install a release version:")
fmt.Println()
printUpgradeInstructions() printUpgradeInstructions()
return nil return nil
} }
fmt.Printf("update available: %s -> %s\n", current, latest) green := color.New(color.FgGreen, color.Bold)
green.Printf("✓ update available: %s → %s\n", current, latest)
fmt.Println() fmt.Println()
if !upgradeYes { if !upgradeYes {
printUpgradeInstructions() printUpgradeInstructions()
fmt.Println() fmt.Println()
fmt.Println("or run 'snitch upgrade --yes' to upgrade in-place") faint := color.New(color.Faint)
cmdStyle := color.New(color.FgCyan)
faint.Print(" in-place ")
cmdStyle.Println("snitch upgrade --yes")
return nil return nil
} }
return performUpgrade(latest) return performUpgrade(latest)
} }
func handleSpecificVersion(current, target string) error {
if !strings.HasPrefix(target, "v") {
target = "v" + target
}
targetClean := strings.TrimPrefix(target, "v")
printVersionComparisonTarget(current, target)
if isVersionLower(targetClean, firstUpgradeVersion) {
yellow := color.New(color.FgYellow)
yellow.Printf("⚠ warning: the upgrade command was introduced in v%s\n", firstUpgradeVersion)
faint := color.New(color.Faint)
faint.Printf(" version %s does not include this command\n", target)
faint.Println(" you will need to use other methods to upgrade from that version")
fmt.Println()
}
currentClean := strings.TrimPrefix(current, "v")
if currentClean == targetClean {
green := color.New(color.FgGreen)
green.Println("✓ you are already running this version")
return nil
}
if !upgradeYes {
faint := color.New(color.Faint)
cmdStyle := color.New(color.FgCyan)
if isVersionLower(targetClean, currentClean) {
yellow := color.New(color.FgYellow)
yellow.Printf("↓ this will downgrade from %s to %s\n", current, target)
} else {
green := color.New(color.FgGreen)
green.Printf("↑ this will upgrade from %s to %s\n", current, target)
}
fmt.Println()
faint.Print("run ")
cmdStyle.Printf("snitch upgrade --version %s --yes", target)
faint.Println(" to proceed")
return nil
}
return performUpgrade(target)
}
func isVersionLower(v1, v2 string) bool {
parts1 := parseVersion(v1)
parts2 := parseVersion(v2)
for i := 0; i < 3; i++ {
if parts1[i] < parts2[i] {
return true
}
if parts1[i] > parts2[i] {
return false
}
}
return false
}
func parseVersion(v string) [3]int {
var parts [3]int
segments := strings.Split(v, ".")
for i := 0; i < len(segments) && i < 3; i++ {
n, err := strconv.Atoi(segments[i])
if err == nil {
parts[i] = n
}
}
return parts
}
func fetchLatestVersion() (string, error) { func fetchLatestVersion() (string, error) {
url := fmt.Sprintf("%s/repos/%s/%s/releases/latest", githubAPI, repoOwner, repoName) url := fmt.Sprintf("%s/repos/%s/%s/releases/latest", githubAPI, repoOwner, repoName)
@@ -106,20 +197,47 @@ func fetchLatestVersion() (string, error) {
return release.TagName, nil return release.TagName, nil
} }
func printVersionComparison(current, latest string) {
faint := color.New(color.Faint)
version := color.New(color.FgCyan)
faint.Print("current ")
version.Println(current)
faint.Print("latest ")
version.Println(latest)
fmt.Println()
}
func printVersionComparisonTarget(current, target string) {
faint := color.New(color.Faint)
version := color.New(color.FgCyan)
faint.Print("current ")
version.Println(current)
faint.Print("target ")
version.Println(target)
fmt.Println()
}
func printUpgradeInstructions() { func printUpgradeInstructions() {
fmt.Println("upgrade options:") bold := color.New(color.Bold)
faint := color.New(color.Faint)
cmd := color.New(color.FgCyan)
bold.Println("upgrade options:")
fmt.Println() fmt.Println()
fmt.Println(" go install:")
fmt.Printf(" go install github.com/%s/%s@latest\n", repoOwner, repoName) faint.Print(" go install ")
fmt.Println() cmd.Printf("go install github.com/%s/%s@latest\n", repoOwner, repoName)
fmt.Println(" shell script:")
fmt.Printf(" curl -sSL https://raw.githubusercontent.com/%s/%s/master/install.sh | sh\n", repoOwner, repoName) faint.Print(" shell script ")
fmt.Println() cmd.Printf("curl -sSL https://raw.githubusercontent.com/%s/%s/master/install.sh | sh\n", repoOwner, repoName)
fmt.Println(" arch linux (aur):")
fmt.Println(" yay -S snitch-bin") faint.Print(" arch (aur) ")
fmt.Println() cmd.Println("yay -S snitch-bin")
fmt.Println(" nix:")
fmt.Printf(" nix profile upgrade --inputs-from github:%s/%s\n", repoOwner, repoName) faint.Print(" nix ")
cmd.Printf("nix profile upgrade --inputs-from github:%s/%s\n", repoOwner, repoName)
} }
func performUpgrade(version string) error { func performUpgrade(version string) error {
@@ -141,7 +259,11 @@ func performUpgrade(version string) error {
downloadURL := fmt.Sprintf("https://github.com/%s/%s/releases/download/%s/%s", downloadURL := fmt.Sprintf("https://github.com/%s/%s/releases/download/%s/%s",
repoOwner, repoName, version, archiveName) repoOwner, repoName, version, archiveName)
fmt.Printf("downloading %s...\n", archiveName) faint := color.New(color.Faint)
cyan := color.New(color.FgCyan)
faint.Print("↓ downloading ")
cyan.Printf("%s", archiveName)
faint.Println("...")
resp, err := http.Get(downloadURL) resp, err := http.Get(downloadURL)
if err != nil { if err != nil {
@@ -167,13 +289,17 @@ func performUpgrade(version string) error {
// check if we can write to the target location // check if we can write to the target location
targetDir := filepath.Dir(execPath) targetDir := filepath.Dir(execPath)
if !isWritable(targetDir) { if !isWritable(targetDir) {
fmt.Printf("elevated permissions required to install to %s\n", targetDir) yellow := color.New(color.FgYellow)
cmdStyle := color.New(color.FgCyan)
yellow.Printf("⚠ elevated permissions required to install to %s\n", targetDir)
fmt.Println() fmt.Println()
fmt.Println("run with sudo or install to a user-writable location:") faint.Println("run with sudo or install to a user-writable location:")
fmt.Printf(" sudo snitch upgrade --yes\n")
fmt.Println() fmt.Println()
fmt.Println("or use the install script with a custom directory:") faint.Print(" sudo ")
fmt.Printf(" curl -sSL https://raw.githubusercontent.com/%s/%s/master/install.sh | INSTALL_DIR=~/.local/bin sh\n", cmdStyle.Println("sudo snitch upgrade --yes")
faint.Print(" custom dir ")
cmdStyle.Printf("curl -sSL https://raw.githubusercontent.com/%s/%s/master/install.sh | INSTALL_DIR=~/.local/bin sh\n",
repoOwner, repoName) repoOwner, repoName)
return nil return nil
} }
@@ -198,10 +324,12 @@ func performUpgrade(version string) error {
if err := os.Remove(backupPath); err != nil { if err := os.Remove(backupPath); err != nil {
// non-fatal, just warn // non-fatal, just warn
fmt.Fprintf(os.Stderr, "warning: failed to remove backup file %s: %v\n", backupPath, err) yellow := color.New(color.FgYellow)
yellow.Fprintf(os.Stderr, "⚠ warning: failed to remove backup file %s: %v\n", backupPath, err)
} }
fmt.Printf("successfully upgraded to %s\n", version) green := color.New(color.FgGreen, color.Bold)
green.Printf("✓ successfully upgraded to %s\n", version)
return nil return nil
} }

View File

@@ -6,6 +6,7 @@ BINARY_NAME="snitch"
# allow override via environment # allow override via environment
INSTALL_DIR="${INSTALL_DIR:-}" INSTALL_DIR="${INSTALL_DIR:-}"
KEEP_QUARANTINE="${KEEP_QUARANTINE:-}"
detect_install_dir() { detect_install_dir() {
if [ -n "$INSTALL_DIR" ]; then if [ -n "$INSTALL_DIR" ]; then
@@ -86,9 +87,11 @@ main() {
exit 1 exit 1
fi fi
# remove macos quarantine attribute # remove macos quarantine attribute unless disabled
if [ "$os" = "darwin" ]; then if [ "$os" = "darwin" ] && [ -z "$KEEP_QUARANTINE" ]; then
xattr -d com.apple.quarantine "${tmp_dir}/${BINARY_NAME}" 2>/dev/null || true if xattr -d com.apple.quarantine "${tmp_dir}/${BINARY_NAME}" 2>/dev/null; then
echo "warning: removed macOS quarantine attribute from binary"
fi
fi fi
# install binary # install binary