Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 0 additions & 32 deletions internal/brew/brew.go
Original file line number Diff line number Diff line change
Expand Up @@ -176,38 +176,6 @@ func UninstallCask(packages []string, dryRun bool) error {
return nil
}

// GetInstalledLeaves returns top-level formulae (not dependencies) as a set.
// This matches what `brew leaves` reports and is consistent with snapshot captures.
func GetInstalledLeaves() (map[string]bool, error) {
output, err := currentRunner().Output("leaves")
if err != nil {
return nil, fmt.Errorf("brew leaves: %w", err)
}

leaves := make(map[string]bool)
for _, name := range strings.Split(strings.TrimSpace(string(output)), "\n") {
if name != "" {
leaves[name] = true
}
}
return leaves, nil
}

func GetInstalledTaps() ([]string, error) {
output, err := currentRunner().Output("tap")
if err != nil {
return nil, fmt.Errorf("brew tap: %w", err)
}
var taps []string
for _, line := range strings.Split(strings.TrimSpace(string(output)), "\n") {
line = strings.TrimSpace(line)
if line != "" {
taps = append(taps, line)
}
}
return taps, nil
}

func Untap(taps []string, dryRun bool) error {
if len(taps) == 0 {
return nil
Expand Down
86 changes: 0 additions & 86 deletions internal/brew/brew_extra_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,92 +8,6 @@ import (
"github.com/stretchr/testify/require"
)

// ---------------------------------------------------------------------------
// GetInstalledLeaves
// ---------------------------------------------------------------------------

func TestGetInstalledLeaves_ParsesOutput(t *testing.T) {
withFakeBrew(t, func(args []string) ([]byte, error) {
if len(args) == 1 && args[0] == "leaves" {
return []byte("git\ncurl\nripgrep\n"), nil
}
return nil, nil
})

leaves, err := GetInstalledLeaves()
require.NoError(t, err)
assert.True(t, leaves["git"])
assert.True(t, leaves["curl"])
assert.True(t, leaves["ripgrep"])
assert.Len(t, leaves, 3)
}

func TestGetInstalledLeaves_EmptyOutput(t *testing.T) {
withFakeBrew(t, func(args []string) ([]byte, error) {
if len(args) == 1 && args[0] == "leaves" {
return []byte("\n"), nil
}
return nil, nil
})

leaves, err := GetInstalledLeaves()
require.NoError(t, err)
assert.Empty(t, leaves)
}

func TestGetInstalledLeaves_RunnerError(t *testing.T) {
withFakeBrew(t, func(args []string) ([]byte, error) {
return nil, errors.New("brew not found")
})

_, err := GetInstalledLeaves()
require.Error(t, err)
assert.Contains(t, err.Error(), "brew leaves")
}

// ---------------------------------------------------------------------------
// GetInstalledTaps
// ---------------------------------------------------------------------------

func TestGetInstalledTaps_ParsesOutput(t *testing.T) {
withFakeBrew(t, func(args []string) ([]byte, error) {
if len(args) == 1 && args[0] == "tap" {
return []byte("homebrew/cask\nhashicorp/tap\nopenbootdotdev/tap\n"), nil
}
return nil, nil
})

taps, err := GetInstalledTaps()
require.NoError(t, err)
require.Len(t, taps, 3)
assert.Equal(t, "homebrew/cask", taps[0])
assert.Equal(t, "hashicorp/tap", taps[1])
assert.Equal(t, "openbootdotdev/tap", taps[2])
}

func TestGetInstalledTaps_EmptyOutput(t *testing.T) {
withFakeBrew(t, func(args []string) ([]byte, error) {
if len(args) == 1 && args[0] == "tap" {
return []byte(""), nil
}
return nil, nil
})

taps, err := GetInstalledTaps()
require.NoError(t, err)
assert.Empty(t, taps)
}

func TestGetInstalledTaps_RunnerError(t *testing.T) {
withFakeBrew(t, func(args []string) ([]byte, error) {
return nil, errors.New("exit status 1")
})

_, err := GetInstalledTaps()
require.Error(t, err)
assert.Contains(t, err.Error(), "brew tap")
}

// ---------------------------------------------------------------------------
// Untap
// ---------------------------------------------------------------------------
Expand Down
4 changes: 2 additions & 2 deletions internal/npm/npm.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ var nodeVersionOutput = func() ([]byte, error) {
return exec.Command("node", "--version").Output()
}

func GetNodeVersion() (int, error) {
func getNodeVersion() (int, error) {
output, err := nodeVersionOutput()
if err != nil {
return 0, err
Expand Down Expand Up @@ -142,7 +142,7 @@ func Install(packages []string, dryRun bool) error {
// warnIfNodeVersionTooLow prints a warning when packages that require Node.js
// v22+ are requested but the installed version is older.
func warnIfNodeVersionTooLow(packages []string) {
nodeVersion, err := GetNodeVersion()
nodeVersion, err := getNodeVersion()
if err != nil || nodeVersion <= 0 {
return
}
Expand Down
2 changes: 1 addition & 1 deletion internal/npm/npm_command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ func TestGetNodeVersion_ParsesVersion(t *testing.T) {
t.Cleanup(func() { nodeVersionOutput = orig })
nodeVersionOutput = func() ([]byte, error) { return []byte("v22.1.0\n"), nil }

version, err := GetNodeVersion()
version, err := getNodeVersion()
require.NoError(t, err)
assert.Equal(t, 22, version)
}
Expand Down
12 changes: 6 additions & 6 deletions internal/npm/npm_extra_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (
)

// ---------------------------------------------------------------------------
// GetNodeVersion — error paths
// getNodeVersion — error paths
// ---------------------------------------------------------------------------

func TestGetNodeVersion_ExecError(t *testing.T) {
Expand All @@ -21,7 +21,7 @@ func TestGetNodeVersion_ExecError(t *testing.T) {
return nil, errors.New("node not found")
}

_, err := GetNodeVersion()
_, err := getNodeVersion()
require.Error(t, err)
}

Expand All @@ -32,7 +32,7 @@ func TestGetNodeVersion_InvalidFormat(t *testing.T) {
return []byte("not-a-version\n"), nil
}

_, err := GetNodeVersion()
_, err := getNodeVersion()
require.Error(t, err)
}

Expand All @@ -43,7 +43,7 @@ func TestGetNodeVersion_EmptyOutput(t *testing.T) {
return []byte(""), nil
}

_, err := GetNodeVersion()
_, err := getNodeVersion()
require.Error(t, err)
}

Expand Down Expand Up @@ -340,15 +340,15 @@ func TestGetInstalledPackages_EmptyOutput(t *testing.T) {
}

// ---------------------------------------------------------------------------
// GetNodeVersion — major-only version string edge case
// getNodeVersion — major-only version string edge case
// ---------------------------------------------------------------------------

func TestGetNodeVersion_MajorOnly(t *testing.T) {
orig := nodeVersionOutput
t.Cleanup(func() { nodeVersionOutput = orig })
nodeVersionOutput = func() ([]byte, error) { return []byte("v20\n"), nil }

ver, err := GetNodeVersion()
ver, err := getNodeVersion()
require.NoError(t, err)
assert.Equal(t, 20, ver)
}
Expand Down
2 changes: 1 addition & 1 deletion internal/npm/npm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ func TestGetNodeVersion(t *testing.T) {
if !IsAvailable() {
t.Skip("node not available")
}
ver, err := GetNodeVersion()
ver, err := getNodeVersion()
assert.NoError(t, err)
assert.Greater(t, ver, 0)
}
Expand Down
104 changes: 0 additions & 104 deletions internal/system/system.go
Original file line number Diff line number Diff line change
@@ -1,17 +1,10 @@
package system

import (
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"net/http"
"os"
"os/exec"
"runtime"
"strings"

"github.com/openbootdotdev/openboot/internal/httputil"
)

func HomeDir() (string, error) {
Expand All @@ -22,32 +15,6 @@ func HomeDir() (string, error) {
return home, nil
}

func Architecture() string {
return runtime.GOARCH
}

func HomebrewPrefix() string {
if Architecture() == "arm64" {
return "/opt/homebrew"
}
return "/usr/local"
}

func IsHomebrewInstalled() bool {
_, err := exec.LookPath("brew")
return err == nil
}

func IsXcodeCliInstalled() bool {
cmd := exec.Command("xcode-select", "-p")
return cmd.Run() == nil
}

func IsGumInstalled() bool {
_, err := exec.LookPath("gum")
return err == nil
}

func RunCommand(name string, args ...string) error {
cmd := exec.Command(name, args...) //nolint:gosec // intentional generic runner; callers are responsible for validating name and args
cmd.Stdout = os.Stdout
Expand All @@ -70,77 +37,6 @@ func RunCommandOutput(name string, args ...string) (string, error) {
return strings.TrimSpace(string(output)), err
}

// knownBrewInstallHash is the SHA256 of the Homebrew install script pinned on
// 2026-04-19 (Homebrew/install HEAD as of that date). Update this constant
// whenever the installer script changes upstream by running:
//
// curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh | sha256sum
const knownBrewInstallHash = "dfd5145fe2aa5956a600e35848765273f5798ce6def01bd08ecec088a1268d91"

const brewInstallURL = "https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh"

// brewHTTPClient is a var so tests can inject a mock transport.
var brewHTTPClient *http.Client = http.DefaultClient

func InstallHomebrew() error {
// Download the installer via httputil.Do so rate-limit handling is applied.
req, err := http.NewRequest(http.MethodGet, brewInstallURL, nil)
if err != nil {
return fmt.Errorf("create homebrew install request: %w", err)
}
resp, err := httputil.Do(brewHTTPClient, req)
if err != nil {
return fmt.Errorf("download homebrew install script: %w", err)
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
return fmt.Errorf("download homebrew install script: unexpected status %d", resp.StatusCode)
}

scriptBytes, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("read homebrew install script: %w", err)
}

// Verify SHA256 before executing anything.
sum := sha256.Sum256(scriptBytes)
got := hex.EncodeToString(sum[:])
if got != knownBrewInstallHash {
return fmt.Errorf("homebrew installer SHA256 mismatch: refusing to execute")
}

// Write verified script to a temp file, execute, then clean up.
tmpFile, err := os.CreateTemp("", "homebrew-install-*.sh")
if err != nil {
return fmt.Errorf("create temp file for homebrew install: %w", err)
}
defer os.Remove(tmpFile.Name())

if _, err := tmpFile.Write(scriptBytes); err != nil {
tmpFile.Close() //nolint:gosec,errcheck // error-path cleanup; original write error takes precedence
return fmt.Errorf("write homebrew install script: %w", err)
}
if err := tmpFile.Close(); err != nil {
return fmt.Errorf("close homebrew install script: %w", err)
}

if err := os.Chmod(tmpFile.Name(), 0700); err != nil { //nolint:gosec // install script must be executable
return fmt.Errorf("chmod homebrew install script: %w", err)
}

tty, opened := OpenTTY()
if opened {
defer tty.Close() //nolint:errcheck // best-effort TTY cleanup
}

cmd := exec.Command(tmpFile.Name()) //nolint:gosec // script content is SHA256-verified before execution
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Stdin = tty
return cmd.Run()
}

func GetGitConfig(key string) string {
// Try global first (most common)
output, err := RunCommandSilent("git", "config", "--global", key)
Expand Down
Loading
Loading