Skip to content

Commit 22d7bf4

Browse files
authored
Merge pull request #3 from stacktodate/check-for-updates
Implement version update checking feature
2 parents 33cb366 + 1f3ad04 commit 22d7bf4

File tree

6 files changed

+831
-0
lines changed

6 files changed

+831
-0
lines changed

cmd/lib/installer/installer.go

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
package installer
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"os/exec"
7+
"strings"
8+
)
9+
10+
// InstallMethod represents how stacktodate was installed
11+
type InstallMethod int
12+
13+
const (
14+
Unknown InstallMethod = iota
15+
Homebrew
16+
Binary
17+
)
18+
19+
// String returns a string representation of the install method
20+
func (m InstallMethod) String() string {
21+
switch m {
22+
case Homebrew:
23+
return "homebrew"
24+
case Binary:
25+
return "binary"
26+
default:
27+
return "unknown"
28+
}
29+
}
30+
31+
// DetectInstallMethod attempts to determine how stacktodate was installed
32+
func DetectInstallMethod() InstallMethod {
33+
// Try to detect Homebrew installation first
34+
if IsHomebrew() {
35+
return Homebrew
36+
}
37+
38+
// Default to binary download
39+
return Binary
40+
}
41+
42+
// IsHomebrew checks if stacktodate was installed via Homebrew
43+
func IsHomebrew() bool {
44+
// Method 1: Check executable path for Homebrew-specific directories
45+
executable, err := os.Executable()
46+
if err == nil {
47+
if isHomebrewPath(executable) {
48+
return true
49+
}
50+
}
51+
52+
// Method 2: Verify with brew command (silent check)
53+
if isBrewInstalled() {
54+
return true
55+
}
56+
57+
return false
58+
}
59+
60+
// isHomebrewPath checks if the executable path looks like a Homebrew installation
61+
func isHomebrewPath(execPath string) bool {
62+
// Common Homebrew paths
63+
homebrewPatterns := []string{
64+
"/Cellar/stacktodate/", // Intel Macs, Linux
65+
"/opt/homebrew/Cellar/stacktodate", // Apple Silicon Macs
66+
"/opt/homebrew/bin/stacktodate",
67+
"/usr/local/bin/stacktodate",
68+
"/usr/local/Cellar/stacktodate/",
69+
}
70+
71+
for _, pattern := range homebrewPatterns {
72+
if strings.Contains(execPath, pattern) {
73+
return true
74+
}
75+
}
76+
77+
return false
78+
}
79+
80+
// isBrewInstalled checks if the brew command recognizes stacktodate
81+
func isBrewInstalled() bool {
82+
// Run: brew list stacktodate
83+
// This will succeed (exit code 0) if stacktodate is installed via Homebrew
84+
cmd := exec.Command("brew", "list", "stacktodate")
85+
86+
// Redirect output to /dev/null (we don't need the output)
87+
cmd.Stdout = nil
88+
cmd.Stderr = nil
89+
90+
// Silent execution - we only care about the exit code
91+
return cmd.Run() == nil
92+
}
93+
94+
// GetUpgradeInstructions returns the appropriate upgrade instructions based on install method
95+
func GetUpgradeInstructions(method InstallMethod, version string) string {
96+
switch method {
97+
case Homebrew:
98+
return "Upgrade: brew upgrade stacktodate"
99+
100+
case Binary:
101+
return fmt.Sprintf("Download: https://github.com/stacktodate/stacktodate-cli/releases/tag/%s", version)
102+
103+
default:
104+
return fmt.Sprintf("Visit: https://github.com/stacktodate/stacktodate-cli/releases/tag/%s", version)
105+
}
106+
}
107+
108+
// GetInstallerDownloadURL returns the download URL appropriate for the install method
109+
func GetInstallerDownloadURL(method InstallMethod) string {
110+
switch method {
111+
case Homebrew:
112+
return "https://github.com/stacktodate/homebrew-stacktodate"
113+
114+
default:
115+
return "https://github.com/stacktodate/stacktodate-cli/releases/latest"
116+
}
117+
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
package installer
2+
3+
import (
4+
"testing"
5+
)
6+
7+
func TestInstallMethodString(t *testing.T) {
8+
tests := []struct {
9+
method InstallMethod
10+
expected string
11+
}{
12+
{Homebrew, "homebrew"},
13+
{Binary, "binary"},
14+
{Unknown, "unknown"},
15+
}
16+
17+
for _, tt := range tests {
18+
t.Run(tt.expected, func(t *testing.T) {
19+
if got := tt.method.String(); got != tt.expected {
20+
t.Fatalf("expected %s, got %s", tt.expected, got)
21+
}
22+
})
23+
}
24+
}
25+
26+
func TestIsHomebrewPath(t *testing.T) {
27+
tests := []struct {
28+
name string
29+
path string
30+
expected bool
31+
}{
32+
{"Intel Mac Cellar", "/usr/local/Cellar/stacktodate/0.2.0/bin/stacktodate", true},
33+
{"Apple Silicon", "/opt/homebrew/Cellar/stacktodate/0.2.0/bin/stacktodate", true},
34+
{"Apple Silicon bin", "/opt/homebrew/bin/stacktodate", true},
35+
{"Standard usr local bin", "/usr/local/bin/stacktodate", true},
36+
{"Binary download", "/Users/username/Downloads/stacktodate", false},
37+
{"Build from source", "/Users/username/projects/stacktodate-cli/stacktodate", false},
38+
{"Go workspace", "/home/user/go/bin/stacktodate", false},
39+
}
40+
41+
for _, tt := range tests {
42+
t.Run(tt.name, func(t *testing.T) {
43+
if got := isHomebrewPath(tt.path); got != tt.expected {
44+
t.Fatalf("expected %v, got %v for path %s", tt.expected, got, tt.path)
45+
}
46+
})
47+
}
48+
}
49+
50+
func TestGetUpgradeInstructions(t *testing.T) {
51+
tests := []struct {
52+
name string
53+
method InstallMethod
54+
version string
55+
expected string
56+
}{
57+
{"Homebrew", Homebrew, "v0.3.0", "Upgrade: brew upgrade stacktodate"},
58+
{"Homebrew without v", Homebrew, "0.3.0", "Upgrade: brew upgrade stacktodate"},
59+
{"Binary with v", Binary, "v0.3.0", "Download: https://github.com/stacktodate/stacktodate-cli/releases/tag/v0.3.0"},
60+
{"Binary without v", Binary, "0.3.0", "Download: https://github.com/stacktodate/stacktodate-cli/releases/tag/0.3.0"},
61+
{"Unknown", Unknown, "v0.3.0", "Visit: https://github.com/stacktodate/stacktodate-cli/releases/tag/v0.3.0"},
62+
}
63+
64+
for _, tt := range tests {
65+
t.Run(tt.name, func(t *testing.T) {
66+
if got := GetUpgradeInstructions(tt.method, tt.version); got != tt.expected {
67+
t.Fatalf("expected %q, got %q", tt.expected, got)
68+
}
69+
})
70+
}
71+
}
72+
73+
func TestGetInstallerDownloadURL(t *testing.T) {
74+
tests := []struct {
75+
name string
76+
method InstallMethod
77+
expected string
78+
}{
79+
{"Homebrew", Homebrew, "https://github.com/stacktodate/homebrew-stacktodate"},
80+
{"Binary", Binary, "https://github.com/stacktodate/stacktodate-cli/releases/latest"},
81+
{"Unknown", Unknown, "https://github.com/stacktodate/stacktodate-cli/releases/latest"},
82+
}
83+
84+
for _, tt := range tests {
85+
t.Run(tt.name, func(t *testing.T) {
86+
if got := GetInstallerDownloadURL(tt.method); got != tt.expected {
87+
t.Fatalf("expected %q, got %q", tt.expected, got)
88+
}
89+
})
90+
}
91+
}
92+
93+
func TestDetectInstallMethod(t *testing.T) {
94+
// This test just verifies the function runs without panic
95+
// Actual detection result depends on environment
96+
method := DetectInstallMethod()
97+
98+
if method != Homebrew && method != Binary && method != Unknown {
99+
t.Fatalf("unexpected install method: %v", method)
100+
}
101+
}

0 commit comments

Comments
 (0)