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
117 changes: 117 additions & 0 deletions cmd/lib/installer/installer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package installer

import (
"fmt"
"os"
"os/exec"
"strings"
)

// InstallMethod represents how stacktodate was installed
type InstallMethod int

const (
Unknown InstallMethod = iota
Homebrew
Binary
)

// String returns a string representation of the install method
func (m InstallMethod) String() string {
switch m {
case Homebrew:
return "homebrew"
case Binary:
return "binary"
default:
return "unknown"
}
}

// DetectInstallMethod attempts to determine how stacktodate was installed
func DetectInstallMethod() InstallMethod {
// Try to detect Homebrew installation first
if IsHomebrew() {
return Homebrew
}

// Default to binary download
return Binary
}

// IsHomebrew checks if stacktodate was installed via Homebrew
func IsHomebrew() bool {
// Method 1: Check executable path for Homebrew-specific directories
executable, err := os.Executable()
if err == nil {
if isHomebrewPath(executable) {
return true
}
}

// Method 2: Verify with brew command (silent check)
if isBrewInstalled() {
return true
}

return false
}

// isHomebrewPath checks if the executable path looks like a Homebrew installation
func isHomebrewPath(execPath string) bool {
// Common Homebrew paths
homebrewPatterns := []string{
"/Cellar/stacktodate/", // Intel Macs, Linux
"/opt/homebrew/Cellar/stacktodate", // Apple Silicon Macs
"/opt/homebrew/bin/stacktodate",
"/usr/local/bin/stacktodate",
"/usr/local/Cellar/stacktodate/",
}

for _, pattern := range homebrewPatterns {
if strings.Contains(execPath, pattern) {
return true
}
}

return false
}

// isBrewInstalled checks if the brew command recognizes stacktodate
func isBrewInstalled() bool {
// Run: brew list stacktodate
// This will succeed (exit code 0) if stacktodate is installed via Homebrew
cmd := exec.Command("brew", "list", "stacktodate")

// Redirect output to /dev/null (we don't need the output)
cmd.Stdout = nil
cmd.Stderr = nil

// Silent execution - we only care about the exit code
return cmd.Run() == nil
}

// GetUpgradeInstructions returns the appropriate upgrade instructions based on install method
func GetUpgradeInstructions(method InstallMethod, version string) string {
switch method {
case Homebrew:
return "Upgrade: brew upgrade stacktodate"

case Binary:
return fmt.Sprintf("Download: https://github.com/stacktodate/stacktodate-cli/releases/tag/%s", version)

default:
return fmt.Sprintf("Visit: https://github.com/stacktodate/stacktodate-cli/releases/tag/%s", version)
}
}

// GetInstallerDownloadURL returns the download URL appropriate for the install method
func GetInstallerDownloadURL(method InstallMethod) string {
switch method {
case Homebrew:
return "https://github.com/stacktodate/homebrew-stacktodate"

default:
return "https://github.com/stacktodate/stacktodate-cli/releases/latest"
}
}
101 changes: 101 additions & 0 deletions cmd/lib/installer/installer_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package installer

import (
"testing"
)

func TestInstallMethodString(t *testing.T) {
tests := []struct {
method InstallMethod
expected string
}{
{Homebrew, "homebrew"},
{Binary, "binary"},
{Unknown, "unknown"},
}

for _, tt := range tests {
t.Run(tt.expected, func(t *testing.T) {
if got := tt.method.String(); got != tt.expected {
t.Fatalf("expected %s, got %s", tt.expected, got)
}
})
}
}

func TestIsHomebrewPath(t *testing.T) {
tests := []struct {
name string
path string
expected bool
}{
{"Intel Mac Cellar", "/usr/local/Cellar/stacktodate/0.2.0/bin/stacktodate", true},
{"Apple Silicon", "/opt/homebrew/Cellar/stacktodate/0.2.0/bin/stacktodate", true},
{"Apple Silicon bin", "/opt/homebrew/bin/stacktodate", true},
{"Standard usr local bin", "/usr/local/bin/stacktodate", true},
{"Binary download", "/Users/username/Downloads/stacktodate", false},
{"Build from source", "/Users/username/projects/stacktodate-cli/stacktodate", false},
{"Go workspace", "/home/user/go/bin/stacktodate", false},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := isHomebrewPath(tt.path); got != tt.expected {
t.Fatalf("expected %v, got %v for path %s", tt.expected, got, tt.path)
}
})
}
}

func TestGetUpgradeInstructions(t *testing.T) {
tests := []struct {
name string
method InstallMethod
version string
expected string
}{
{"Homebrew", Homebrew, "v0.3.0", "Upgrade: brew upgrade stacktodate"},
{"Homebrew without v", Homebrew, "0.3.0", "Upgrade: brew upgrade stacktodate"},
{"Binary with v", Binary, "v0.3.0", "Download: https://github.com/stacktodate/stacktodate-cli/releases/tag/v0.3.0"},
{"Binary without v", Binary, "0.3.0", "Download: https://github.com/stacktodate/stacktodate-cli/releases/tag/0.3.0"},
{"Unknown", Unknown, "v0.3.0", "Visit: https://github.com/stacktodate/stacktodate-cli/releases/tag/v0.3.0"},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := GetUpgradeInstructions(tt.method, tt.version); got != tt.expected {
t.Fatalf("expected %q, got %q", tt.expected, got)
}
})
}
}

func TestGetInstallerDownloadURL(t *testing.T) {
tests := []struct {
name string
method InstallMethod
expected string
}{
{"Homebrew", Homebrew, "https://github.com/stacktodate/homebrew-stacktodate"},
{"Binary", Binary, "https://github.com/stacktodate/stacktodate-cli/releases/latest"},
{"Unknown", Unknown, "https://github.com/stacktodate/stacktodate-cli/releases/latest"},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := GetInstallerDownloadURL(tt.method); got != tt.expected {
t.Fatalf("expected %q, got %q", tt.expected, got)
}
})
}
}

func TestDetectInstallMethod(t *testing.T) {
// This test just verifies the function runs without panic
// Actual detection result depends on environment
method := DetectInstallMethod()

if method != Homebrew && method != Binary && method != Unknown {
t.Fatalf("unexpected install method: %v", method)
}
}
Loading
Loading