fix: add more providers, set priority (#1495)

Drew Smirnoff created

## What?

Adds scoop and nix to matcha auto-updater (`matcha update`) and sets
per-OS priorities. Also, asks for `sudo`, when manual binary download
moving fails.

## Why?

Fixes #1249

---------

Signed-off-by: drew <me@andrinoff.com>

Change summary

main.go | 449 +++++++++++++++++++++++++++++++++++++++++-----------------
1 file changed, 316 insertions(+), 133 deletions(-)

Detailed changes

main.go šŸ”—

@@ -77,6 +77,8 @@ var (
 
 const (
 	goosDarwin        = "darwin"
+	goosLinux         = "linux"
+	goosWindows       = "windows"
 	folderInbox       = "INBOX"
 	actionKindDelete  = "delete"
 	actionKindArchive = "archive"
@@ -3574,9 +3576,9 @@ func downloadAttachmentCmd(account *config.Account, uid uint32, msg tui.Download
 			switch runtime.GOOS {
 			case goosDarwin:
 				cmd = exec.Command("open", p) //nolint:noctx
-			case "linux":
+			case goosLinux:
 				cmd = exec.Command("xdg-open", p) //nolint:noctx
-			case "windows":
+			case goosWindows:
 				// 'start' is a cmd builtin; provide an empty title argument to avoid interpreting the path as the title.
 				cmd = exec.Command("cmd", "/c", "start", "", p) //nolint:noctx
 			default:
@@ -3620,7 +3622,7 @@ func detectInstalledVersion() string {
 	}
 
 	// Try WinGet (Windows)
-	if runtime.GOOS == "windows" {
+	if runtime.GOOS == goosWindows {
 		if _, err := exec.LookPath("winget"); err == nil {
 			if out, err := exec.Command("winget", "list", "--id", "floatpane.matcha", "--disable-interactivity").Output(); err == nil { //nolint:noctx
 				lines := strings.Split(strings.TrimSpace(string(out)), "\n")
@@ -3639,7 +3641,7 @@ func detectInstalledVersion() string {
 	}
 
 	// Try snap (Linux)
-	if runtime.GOOS == "linux" {
+	if runtime.GOOS == goosLinux {
 		if _, err := exec.LookPath("snap"); err == nil {
 			if out, err := exec.Command("snap", "list", "matcha").Output(); err == nil { //nolint:noctx
 				lines := strings.Split(strings.TrimSpace(string(out)), "\n")
@@ -3923,7 +3925,7 @@ func isFlagSet(fs *flag.FlagSet, name string) bool {
 	return found
 }
 
-func runUpdateCLI() (err error) { //nolint:gocyclo
+func runUpdateCLI() (err error) {
 	const api = "https://api.github.com/repos/floatpane/matcha/releases/latest"
 	resp, err := httpClient.Get(api)
 	if err != nil {
@@ -3948,145 +3950,224 @@ func runUpdateCLI() (err error) { //nolint:gocyclo
 		return nil
 	}
 
-	// Detect Homebrew
-	if _, err := exec.LookPath("brew"); err == nil {
-		fmt.Println("Detected Homebrew — updating taps and attempting to upgrade via brew.")
+	// Determine OS and try package managers in priority order
+	osName := runtime.GOOS
 
-		updateCmd := exec.Command("brew", "update") //nolint:noctx
-		updateCmd.Stdout = os.Stdout
-		updateCmd.Stderr = os.Stderr
-		if err := updateCmd.Run(); err != nil {
-			fmt.Printf("Homebrew update failed: %v\n", err)
-			// continue to attempt upgrade even if update failed
+	switch osName {
+	case goosDarwin: // macOS
+		// Priority: Homebrew > Manual binary update
+		if tryHomebrewUpgrade() {
+			return nil
 		}
+		// Fall through to manual binary download
 
-		upgradeCmd := exec.Command("brew", "upgrade", "floatpane/matcha/matcha") //nolint:noctx
-		upgradeCmd.Stdout = os.Stdout
-		upgradeCmd.Stderr = os.Stderr
-		if err := upgradeCmd.Run(); err == nil {
-			fmt.Println("Successfully upgraded via Homebrew.")
+	case goosLinux: // Linux
+		// Priority: Snap > Flatpak > AUR (yay) > Nix > Manual binary update
+		if trySnapRefresh() {
 			return nil
 		}
-		fmt.Printf("Homebrew upgrade failed: %v\n", err)
-		// fallthrough to other methods
-	}
+		if tryFlatpakUpdate() {
+			return nil
+		}
+		if tryAURUpdate() {
+			return nil
+		}
+		if tryNixUpdate() {
+			return nil
+		}
+		// Fall through to manual binary download
 
-	// Detect snap
-	if _, err := exec.LookPath("snap"); err == nil {
-		// Check if matcha is installed as a snap
-		cmdCheck := exec.Command("snap", "list", "matcha") //nolint:noctx
-		if err := cmdCheck.Run(); err == nil {
-			fmt.Println("Detected Snap package — attempting to refresh.")
-			cmd := exec.Command("snap", "refresh", "matcha") //nolint:noctx
-			cmd.Stdout = os.Stdout
-			cmd.Stderr = os.Stderr
-			if err := cmd.Run(); err == nil {
-				fmt.Println("Successfully refreshed snap.")
-				return nil
-			}
-			fmt.Printf("Snap refresh failed: %v\n", err)
-			// fallthrough
+	case goosWindows: // Windows
+		// Priority: WinGet > Scoop > Manual binary update
+		if tryWinGetUpgrade() {
+			return nil
 		}
-	}
-	// Detect flatpak
-	if _, err := exec.LookPath("flatpak"); err == nil {
-		// Check if matcha is installed as a flatpak
-		cmdCheck := exec.Command("flatpak", "info", "com.floatpane.matcha") //nolint:noctx
-		if err := cmdCheck.Run(); err == nil {
-			fmt.Println("Detected Flatpak package — attempting to update.")
-			cmd := exec.Command("flatpak", "update", "-y", "com.floatpane.matcha") //nolint:noctx
-			cmd.Stdout = os.Stdout
-			cmd.Stderr = os.Stderr
-			if err := cmd.Run(); err == nil {
-				fmt.Println("Successfully updated flatpak.")
-				return nil
-			}
-			fmt.Printf("Flatpak update failed: %v\n", err)
-			// fallthrough
+		if tryScoopUpdate() {
+			return nil
 		}
+		// Fall through to manual binary download
 	}
 
-	// Detect WinGet
-	if _, err := exec.LookPath("winget"); err == nil {
-		cmdCheck := exec.Command("winget", "list", "--id", "floatpane.matcha", "--disable-interactivity") //nolint:noctx
-		if err := cmdCheck.Run(); err == nil {
-			fmt.Println("Detected WinGet package — attempting to upgrade.")
-			cmd := exec.Command("winget", "upgrade", "--id", "floatpane.matcha", "--disable-interactivity") //nolint:noctx
-			cmd.Stdout = os.Stdout
-			cmd.Stderr = os.Stderr
-			if err := cmd.Run(); err == nil {
-				fmt.Println("Successfully upgraded via WinGet.")
-				return nil
-			}
-			fmt.Printf("WinGet upgrade failed: %v\n", err)
-			// fallthrough
-		}
+	// If no package manager succeeded, fall back to manual binary download
+	return runUpdateCLIManual(latestTag, rel)
+}
+
+// tryHomebrewUpgrade attempts to upgrade via Homebrew
+func tryHomebrewUpgrade() bool {
+	if _, err := exec.LookPath("brew"); err != nil {
+		return false
 	}
 
-	// Otherwise attempt to download the proper release asset and replace the binary.
-	osName := runtime.GOOS
-	arch := runtime.GOARCH
+	fmt.Println("Detected Homebrew — updating taps and attempting to upgrade via brew.")
 
-	// Try to find a matching asset
-	var assetURL, assetName string
-	for _, a := range rel.Assets {
-		n := strings.ToLower(a.Name)
-		if strings.Contains(n, osName) && strings.Contains(n, arch) && (strings.HasSuffix(n, ".tar.gz") || strings.HasSuffix(n, ".tgz") || strings.HasSuffix(n, ".zip")) {
-			assetURL = a.BrowserDownloadURL
-			assetName = a.Name
-			break
-		}
+	updateCmd := exec.Command("brew", "update") //nolint:noctx
+	updateCmd.Stdout = os.Stdout
+	updateCmd.Stderr = os.Stderr
+	if err := updateCmd.Run(); err != nil {
+		fmt.Printf("Homebrew update failed: %v\n", err)
+		// continue to attempt upgrade even if update failed
 	}
-	if assetURL == "" {
-		// Try any asset that contains 'matcha' and os/arch as a fallback
-		for _, a := range rel.Assets {
-			n := strings.ToLower(a.Name)
-			if strings.Contains(n, "matcha") && (strings.Contains(n, osName) || strings.Contains(n, arch)) {
-				assetURL = a.BrowserDownloadURL
-				assetName = a.Name
-				break
-			}
-		}
+
+	upgradeCmd := exec.Command("brew", "upgrade", "floatpane/matcha/matcha") //nolint:noctx
+	upgradeCmd.Stdout = os.Stdout
+	upgradeCmd.Stderr = os.Stderr
+	if err := upgradeCmd.Run(); err == nil {
+		fmt.Println("Successfully upgraded via Homebrew.")
+		return true
 	}
+	fmt.Printf("Homebrew upgrade failed\n")
+	return false
+}
 
-	if assetURL == "" {
-		return fmt.Errorf("no suitable release artifact found for %s/%s", osName, arch)
+// trySnapRefresh attempts to refresh via Snap
+func trySnapRefresh() bool {
+	if _, err := exec.LookPath("snap"); err != nil {
+		return false
 	}
 
-	fmt.Printf("Found release asset: %s\n", assetName)
-	fmt.Println("Downloading...")
+	// Check if matcha is installed as a snap
+	cmdCheck := exec.Command("snap", "list", "matcha") //nolint:noctx
+	if err := cmdCheck.Run(); err != nil {
+		return false
+	}
 
-	// Download asset
-	respAsset, err := httpClient.Get(assetURL)
-	if err != nil {
-		return fmt.Errorf("download failed: %w", err)
+	fmt.Println("Detected Snap package — attempting to refresh.")
+	cmd := exec.Command("snap", "refresh", "matcha") //nolint:noctx
+	cmd.Stdout = os.Stdout
+	cmd.Stderr = os.Stderr
+	if err := cmd.Run(); err == nil {
+		fmt.Println("Successfully refreshed snap.")
+		return true
 	}
-	defer respAsset.Body.Close() //nolint:errcheck
+	fmt.Printf("Snap refresh failed\n")
+	return false
+}
 
-	// Create a temp file for the download
-	tmpDir, err := os.MkdirTemp("", "matcha-update-*")
-	if err != nil {
-		return fmt.Errorf("could not create temp dir: %w", err)
+// tryFlatpakUpdate attempts to update via Flatpak
+func tryFlatpakUpdate() bool {
+	if _, err := exec.LookPath("flatpak"); err != nil {
+		return false
 	}
-	defer os.RemoveAll(tmpDir) //nolint:errcheck
 
-	assetPath := filepath.Join(tmpDir, assetName)
-	outFile, err := os.Create(assetPath)
-	if err != nil {
-		return fmt.Errorf("could not create temp file: %w", err)
+	// Check if matcha is installed as a flatpak
+	cmdCheck := exec.Command("flatpak", "info", "com.floatpane.matcha") //nolint:noctx
+	if err := cmdCheck.Run(); err != nil {
+		return false
 	}
-	_, err = io.Copy(outFile, respAsset.Body)
-	if err != nil {
-		_ = outFile.Close()
-		return fmt.Errorf("could not write asset to disk: %w", err)
+
+	fmt.Println("Detected Flatpak package — attempting to update.")
+	cmd := exec.Command("flatpak", "update", "-y", "com.floatpane.matcha") //nolint:noctx
+	cmd.Stdout = os.Stdout
+	cmd.Stderr = os.Stderr
+	if err := cmd.Run(); err == nil {
+		fmt.Println("Successfully updated flatpak.")
+		return true
 	}
-	if err := outFile.Close(); err != nil {
-		return fmt.Errorf("could not finalize asset file: %w", err)
+	fmt.Printf("Flatpak update failed\n")
+	return false
+}
+
+// tryAURUpdate attempts to update via AUR (using yay)
+func tryAURUpdate() bool {
+	if _, err := exec.LookPath("yay"); err != nil {
+		return false
+	}
+
+	// Check if matcha-client-bin is installed
+	cmdCheck := exec.Command("yay", "-Q", "matcha-client-bin") //nolint:noctx
+	if err := cmdCheck.Run(); err != nil {
+		return false
+	}
+
+	fmt.Println("Detected AUR package (matcha-client-bin) — attempting to update via yay.")
+	cmd := exec.Command("yay", "-Syu", "--noconfirm", "matcha-client-bin") //nolint:noctx
+	cmd.Stdout = os.Stdout
+	cmd.Stderr = os.Stderr
+	if err := cmd.Run(); err == nil {
+		fmt.Println("Successfully updated via AUR.")
+		return true
+	}
+	fmt.Printf("AUR update failed\n")
+	return false
+}
+
+// tryNixUpdate attempts to update via Nix
+func tryNixUpdate() bool {
+	if _, err := exec.LookPath("nix"); err != nil {
+		return false
+	}
+
+	// Check if matcha is in the user's profile
+	cmdCheck := exec.Command("nix", "profile", "list") //nolint:noctx
+	output, err := cmdCheck.Output()
+	if err != nil || !strings.Contains(string(output), "matcha") {
+		return false
+	}
+
+	fmt.Println("Detected Nix package — attempting to update via nix profile upgrade.")
+	cmd := exec.Command("nix", "profile", "upgrade", "github:floatpane/matcha") //nolint:noctx
+	cmd.Stdout = os.Stdout
+	cmd.Stderr = os.Stderr
+	if err := cmd.Run(); err == nil {
+		fmt.Println("Successfully updated via Nix.")
+		return true
+	}
+	fmt.Printf("Nix update failed\n")
+	return false
+}
+
+// tryWinGetUpgrade attempts to upgrade via WinGet
+func tryWinGetUpgrade() bool {
+	if _, err := exec.LookPath("winget"); err != nil {
+		return false
 	}
 
+	cmdCheck := exec.Command("winget", "list", "--id", "floatpane.matcha", "--disable-interactivity") //nolint:noctx
+	if err := cmdCheck.Run(); err != nil {
+		return false
+	}
+
+	fmt.Println("Detected WinGet package — attempting to upgrade.")
+	cmd := exec.Command("winget", "upgrade", "--id", "floatpane.matcha", "--disable-interactivity") //nolint:noctx
+	cmd.Stdout = os.Stdout
+	cmd.Stderr = os.Stderr
+	if err := cmd.Run(); err == nil {
+		fmt.Println("Successfully upgraded via WinGet.")
+		return true
+	}
+	fmt.Printf("WinGet upgrade failed\n")
+	return false
+}
+
+// tryScoopUpdate attempts to update via Scoop
+func tryScoopUpdate() bool {
+	if _, err := exec.LookPath("scoop"); err != nil {
+		return false
+	}
+
+	// Check if matcha is installed via scoop
+	cmdCheck := exec.Command("scoop", "list", "matcha") //nolint:noctx
+	if err := cmdCheck.Run(); err != nil {
+		return false
+	}
+
+	fmt.Println("Detected Scoop package — attempting to update.")
+	cmd := exec.Command("scoop", "update", "matcha") //nolint:noctx
+	cmd.Stdout = os.Stdout
+	cmd.Stderr = os.Stderr
+	if err := cmd.Run(); err == nil {
+		fmt.Println("Successfully updated via Scoop.")
+		return true
+	}
+	fmt.Printf("Scoop update failed\n")
+	return false
+}
+
+// extractBinaryFromArchive extracts the matcha binary from a tar.gz, tgz, or zip archive
+func extractBinaryFromArchive(assetPath, assetName, tmpDir string) (string, error) {
 	// Determine the expected binary name based on the OS.
 	binaryName := "matcha"
-	if runtime.GOOS == "windows" {
+	if runtime.GOOS == goosWindows {
 		binaryName = "matcha.exe"
 	}
 
@@ -4095,12 +4176,12 @@ func runUpdateCLI() (err error) { //nolint:gocyclo
 	if strings.HasSuffix(assetName, ".tar.gz") || strings.HasSuffix(assetName, ".tgz") { //nolint:gocritic
 		f, err := os.Open(assetPath)
 		if err != nil {
-			return fmt.Errorf("could not open archive: %w", err)
+			return "", fmt.Errorf("could not open archive: %w", err)
 		}
 		defer f.Close() //nolint:errcheck
 		gzr, err := gzip.NewReader(f)
 		if err != nil {
-			return fmt.Errorf("could not create gzip reader: %w", err)
+			return "", fmt.Errorf("could not create gzip reader: %w", err)
 		}
 		tr := tar.NewReader(gzr)
 		for {
@@ -4109,24 +4190,24 @@ func runUpdateCLI() (err error) { //nolint:gocyclo
 				break
 			}
 			if err != nil {
-				return fmt.Errorf("error reading tar: %w", err)
+				return "", fmt.Errorf("error reading tar: %w", err)
 			}
 			name := filepath.Base(hdr.Name)
 			if name == binaryName || strings.Contains(strings.ToLower(name), "matcha") && (hdr.Typeflag == tar.TypeReg) {
 				binPath = filepath.Join(tmpDir, binaryName)
 				out, err := os.Create(binPath)
 				if err != nil {
-					return fmt.Errorf("could not create binary file: %w", err)
+					return "", fmt.Errorf("could not create binary file: %w", err)
 				}
 				if _, err := io.Copy(out, tr); err != nil { //nolint:gosec
 					_ = out.Close()
-					return fmt.Errorf("could not extract binary: %w", err)
+					return "", fmt.Errorf("could not extract binary: %w", err)
 				}
 				if err := out.Close(); err != nil {
-					return fmt.Errorf("could not finalize extracted binary: %w", err)
+					return "", fmt.Errorf("could not finalize extracted binary: %w", err)
 				}
 				if err := os.Chmod(binPath, 0755); err != nil { //nolint:gosec
-					return fmt.Errorf("could not make binary executable: %w", err)
+					return "", fmt.Errorf("could not make binary executable: %w", err)
 				}
 				break
 			}
@@ -4134,7 +4215,7 @@ func runUpdateCLI() (err error) { //nolint:gocyclo
 	} else if strings.HasSuffix(assetName, ".zip") {
 		zr, err := zip.OpenReader(assetPath)
 		if err != nil {
-			return fmt.Errorf("could not open zip archive: %w", err)
+			return "", fmt.Errorf("could not open zip archive: %w", err)
 		}
 		defer zr.Close() //nolint:errcheck
 		for _, zf := range zr.File {
@@ -4142,28 +4223,28 @@ func runUpdateCLI() (err error) { //nolint:gocyclo
 			if name == binaryName || strings.Contains(strings.ToLower(name), "matcha") && !zf.FileInfo().IsDir() {
 				rc, err := zf.Open()
 				if err != nil {
-					return fmt.Errorf("could not open file in zip: %w", err)
+					return "", fmt.Errorf("could not open file in zip: %w", err)
 				}
 				binPath = filepath.Join(tmpDir, binaryName)
 				out, err := os.Create(binPath)
 				if err != nil {
 					rc.Close() //nolint:errcheck,gosec
-					return fmt.Errorf("could not create binary file: %w", err)
+					return "", fmt.Errorf("could not create binary file: %w", err)
 				}
 				if _, err := io.Copy(out, rc); err != nil { //nolint:gosec
 					_ = out.Close()
 					_ = rc.Close()
-					return fmt.Errorf("could not extract binary: %w", err)
+					return "", fmt.Errorf("could not extract binary: %w", err)
 				}
 				if err := out.Close(); err != nil {
 					_ = rc.Close()
-					return fmt.Errorf("could not finalize extracted binary: %w", err)
+					return "", fmt.Errorf("could not finalize extracted binary: %w", err)
 				}
 				if err := rc.Close(); err != nil {
-					return fmt.Errorf("could not close zip entry: %w", err)
+					return "", fmt.Errorf("could not close zip entry: %w", err)
 				}
 				if err := os.Chmod(binPath, 0755); err != nil { //nolint:gosec
-					return fmt.Errorf("could not make binary executable: %w", err)
+					return "", fmt.Errorf("could not make binary executable: %w", err)
 				}
 				break
 			}
@@ -4178,17 +4259,20 @@ func runUpdateCLI() (err error) { //nolint:gocyclo
 	}
 
 	if binPath == "" {
-		return fmt.Errorf("could not locate matcha binary inside the release artifact")
+		return "", fmt.Errorf("could not locate matcha binary inside the release artifact")
 	}
 
-	// Replace the running executable with the new binary
+	return binPath, nil
+}
+
+// replaceExecutable atomically replaces the current executable with a new binary
+func replaceExecutable(binPath, execDir string) error {
 	execPath, err := os.Executable()
 	if err != nil {
 		return fmt.Errorf("could not determine executable path: %w", err)
 	}
 
 	// Write the new binary to a temp file in same dir, then rename for atomic replacement.
-	execDir := filepath.Dir(execPath)
 	tmpNew := filepath.Join(execDir, fmt.Sprintf("matcha.new.%d", time.Now().Unix()))
 	in, err := os.Open(binPath)
 	if err != nil {
@@ -4213,7 +4297,7 @@ func runUpdateCLI() (err error) { //nolint:gocyclo
 
 	// On Windows, a running executable cannot be overwritten directly.
 	// Move the old binary out of the way first, then rename the new one in.
-	if runtime.GOOS == "windows" {
+	if runtime.GOOS == goosWindows {
 		oldPath := execPath + ".old"
 		_ = os.Remove(oldPath) // clean up any previous leftover
 		if err := os.Rename(execPath, oldPath); err != nil {
@@ -4225,6 +4309,105 @@ func runUpdateCLI() (err error) { //nolint:gocyclo
 		return fmt.Errorf("could not replace executable: %w", err)
 	}
 
+	return nil
+}
+
+// runUpdateCLIManual handles manual binary download and replacement
+func runUpdateCLIManual(latestTag string, rel githubRelease) error {
+	// Otherwise attempt to download the proper release asset and replace the binary.
+	osName := runtime.GOOS
+	arch := runtime.GOARCH
+
+	// Check if we have write permissions to the executable directory
+	execPath, err := os.Executable()
+	if err != nil {
+		return fmt.Errorf("could not determine executable path: %w", err)
+	}
+	execDir := filepath.Dir(execPath)
+
+	// Test if we can write to the directory
+	testFile := filepath.Join(execDir, ".matcha_update_test")
+	if _, err := os.Create(testFile); err != nil {
+		// Cannot write - check if running with sudo or suggest it
+		if os.Geteuid() != 0 {
+			fmt.Println("\nāš ļø  Permission denied: Cannot write to installation directory.")
+			fmt.Println("   Try running with sudo: sudo matcha update")
+			fmt.Println("   Or reinstall using your package manager.")
+			return fmt.Errorf("permission denied: cannot write to %s", execDir)
+		}
+		// Running as root but still can't write - actual permission issue
+		return fmt.Errorf("cannot write to installation directory %s: %w", execDir, err)
+	}
+	_ = os.Remove(testFile) // Clean up test file
+
+	// Try to find a matching asset
+	var assetURL, assetName string
+	for _, a := range rel.Assets {
+		n := strings.ToLower(a.Name)
+		if strings.Contains(n, osName) && strings.Contains(n, arch) && (strings.HasSuffix(n, ".tar.gz") || strings.HasSuffix(n, ".tgz") || strings.HasSuffix(n, ".zip")) {
+			assetURL = a.BrowserDownloadURL
+			assetName = a.Name
+			break
+		}
+	}
+	if assetURL == "" {
+		// Try any asset that contains 'matcha' and os/arch as a fallback
+		for _, a := range rel.Assets {
+			n := strings.ToLower(a.Name)
+			if strings.Contains(n, "matcha") && (strings.Contains(n, osName) || strings.Contains(n, arch)) {
+				assetURL = a.BrowserDownloadURL
+				assetName = a.Name
+				break
+			}
+		}
+	}
+
+	if assetURL == "" {
+		return fmt.Errorf("no suitable release artifact found for %s/%s", osName, arch)
+	}
+
+	fmt.Printf("Found release asset: %s\n", assetName)
+	fmt.Println("Downloading...")
+
+	// Download asset
+	respAsset, err := httpClient.Get(assetURL)
+	if err != nil {
+		return fmt.Errorf("download failed: %w", err)
+	}
+	defer respAsset.Body.Close() //nolint:errcheck
+
+	// Create a temp file for the download
+	tmpDir, err := os.MkdirTemp("", "matcha-update-*")
+	if err != nil {
+		return fmt.Errorf("could not create temp dir: %w", err)
+	}
+	defer os.RemoveAll(tmpDir) //nolint:errcheck
+
+	assetPath := filepath.Join(tmpDir, assetName)
+	outFile, err := os.Create(assetPath)
+	if err != nil {
+		return fmt.Errorf("could not create temp file: %w", err)
+	}
+	_, err = io.Copy(outFile, respAsset.Body)
+	if err != nil {
+		_ = outFile.Close()
+		return fmt.Errorf("could not write asset to disk: %w", err)
+	}
+	if err := outFile.Close(); err != nil {
+		return fmt.Errorf("could not finalize asset file: %w", err)
+	}
+
+	// Extract binary from archive
+	binPath, err := extractBinaryFromArchive(assetPath, assetName, tmpDir)
+	if err != nil {
+		return err
+	}
+
+	// Replace the executable
+	if err := replaceExecutable(binPath, execDir); err != nil {
+		return err
+	}
+
 	fmt.Println("Successfully updated matcha to", latestTag)
 	return nil
 }