feat(brewfile): add remote Brewfile support via HTTPS URLs

Users can now load Brewfiles from remote URLs:
  bbrew -f https://example.com/Brewfile

Remote files are downloaded to a temp file and auto-cleaned on exit.
Only HTTPS URLs are supported for security.
This commit is contained in:
Vito Castellano 2025-12-29 16:52:26 +01:00
commit f4e9c32987
No known key found for this signature in database
GPG key ID: E13085DB38BC5819
2 changed files with 70 additions and 8 deletions

View file

@ -19,12 +19,13 @@ func main() {
fmt.Fprintf(os.Stderr, "Bold Brew - A TUI for Homebrew package management\n\n")
fmt.Fprintf(os.Stderr, "Usage: bbrew [options]\n\n")
fmt.Fprintf(os.Stderr, "Options:\n")
fmt.Fprintf(os.Stderr, " -f <path> Path to Brewfile (show only packages from this Brewfile)\n")
fmt.Fprintf(os.Stderr, " -f <path|url> Path or URL to Brewfile\n")
fmt.Fprintf(os.Stderr, " -v, --version Show version information\n")
fmt.Fprintf(os.Stderr, " -h, --help Show this help message\n")
fmt.Fprintf(os.Stderr, "\nExamples:\n")
fmt.Fprintf(os.Stderr, " bbrew Launch the TUI with all packages\n")
fmt.Fprintf(os.Stderr, " bbrew -f ~/Brewfile Launch with packages from Brewfile\n")
fmt.Fprintf(os.Stderr, " bbrew -f ~/Brewfile Launch with packages from local Brewfile\n")
fmt.Fprintf(os.Stderr, " bbrew -f https://... Launch with packages from remote Brewfile\n")
}
flag.Parse()
@ -35,15 +36,17 @@ func main() {
os.Exit(0)
}
// Validate Brewfile path if provided
// Resolve Brewfile path (handles both local and remote URLs)
var cleanup func()
if *brewfilePath != "" {
if _, err := os.Stat(*brewfilePath); os.IsNotExist(err) {
fmt.Fprintf(os.Stderr, "Error: Brewfile not found: %s\n", *brewfilePath)
os.Exit(1)
} else if err != nil {
fmt.Fprintf(os.Stderr, "Error: Cannot access Brewfile: %v\n", err)
localPath, cleanupFn, err := services.ResolveBrewfilePath(*brewfilePath)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
*brewfilePath = localPath
cleanup = cleanupFn
defer cleanup()
}
// Initialize app service

View file

@ -25,11 +25,70 @@ package services
import (
"bbrew/internal/models"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"sort"
"strings"
)
// ResolveBrewfilePath resolves a Brewfile path which can be local or a remote URL.
// Returns the local file path and a cleanup function to call when done.
// For local files, cleanup is a no-op. For remote files, cleanup removes the temp file.
func ResolveBrewfilePath(pathOrURL string) (localPath string, cleanup func(), err error) {
// Check if it's a remote URL (HTTPS only for security)
if strings.HasPrefix(pathOrURL, "https://") {
localPath, err = downloadBrewfile(pathOrURL)
if err != nil {
return "", nil, err
}
// Return cleanup function that removes the temp file
cleanup = func() { os.Remove(localPath) }
return localPath, cleanup, nil
}
// Local file - validate it exists
if _, err := os.Stat(pathOrURL); os.IsNotExist(err) {
return "", nil, fmt.Errorf("brewfile not found: %s", pathOrURL)
} else if err != nil {
return "", nil, fmt.Errorf("cannot access Brewfile: %w", err)
}
// No cleanup needed for local files
return pathOrURL, func() {}, nil
}
// downloadBrewfile downloads a remote Brewfile to a temporary file.
func downloadBrewfile(url string) (string, error) {
fmt.Fprintf(os.Stderr, "Downloading Brewfile from %s...\n", url)
resp, err := http.Get(url) // #nosec G107 - URL is user-provided, HTTPS enforced
if err != nil {
return "", fmt.Errorf("failed to fetch URL: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("HTTP %d: %s", resp.StatusCode, resp.Status)
}
// Create temp file
tempFile, err := os.CreateTemp(os.TempDir(), "bbrew-remote-*.brewfile")
if err != nil {
return "", fmt.Errorf("failed to create temp file: %w", err)
}
defer tempFile.Close()
// Copy content
if _, err = io.Copy(tempFile, resp.Body); err != nil {
os.Remove(tempFile.Name())
return "", fmt.Errorf("failed to save Brewfile: %w", err)
}
return filepath.Clean(tempFile.Name()), nil
}
// parseBrewfileWithTaps parses a Brewfile and returns taps and packages separately.
func parseBrewfileWithTaps(filepath string) (*models.BrewfileResult, error) {
// #nosec G304 -- filepath is user-provided via CLI flag