mirror of
https://github.com/gogrlx/snack.git
synced 2026-04-05 14:42:29 -07:00
Compare commits
4 Commits
v0.4.1
...
cd/brew-pa
| Author | SHA1 | Date | |
|---|---|---|---|
| 989206e001 | |||
| b86a793e1c | |||
|
|
adb8de7bee | ||
| 4ea7c3f93b |
@@ -7,9 +7,6 @@ before:
|
||||
hooks:
|
||||
- go mod tidy
|
||||
|
||||
snapshot:
|
||||
name_template: "{{ incpatch .Version }}-next"
|
||||
|
||||
builds:
|
||||
- main: ./cmd/snack/
|
||||
id: snack
|
||||
@@ -44,7 +41,7 @@ archives:
|
||||
nfpms:
|
||||
- id: snack
|
||||
package_name: snack
|
||||
builds: [snack]
|
||||
ids: [snack]
|
||||
formats: [apk, deb, rpm]
|
||||
bindir: /usr/bin
|
||||
description: "A unified CLI for system package managers"
|
||||
@@ -53,6 +50,19 @@ nfpms:
|
||||
homepage: https://github.com/gogrlx/snack
|
||||
vendor: Adatomic, Inc.
|
||||
|
||||
homebrew_casks:
|
||||
- ids: [snack, snack-universal]
|
||||
name: snack
|
||||
binaries:
|
||||
- snack
|
||||
repository:
|
||||
owner: gogrlx
|
||||
name: homebrew-tap
|
||||
directory: Casks
|
||||
homepage: https://github.com/gogrlx/snack
|
||||
description: "A unified CLI for system package managers"
|
||||
license: 0BSD
|
||||
|
||||
release:
|
||||
github:
|
||||
owner: gogrlx
|
||||
|
||||
@@ -206,6 +206,227 @@ func TestCapabilities(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseBrewInfoVersion(t *testing.T) {
|
||||
t.Run("formula", func(t *testing.T) {
|
||||
input := `{"formulae":[{"name":"git","full_name":"git","desc":"Distributed revision control system","versions":{"stable":"2.43.0"},"installed":[]}],"casks":[]}`
|
||||
ver := parseBrewInfoVersion(input)
|
||||
if ver != "2.43.0" {
|
||||
t.Errorf("expected '2.43.0', got %q", ver)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("cask", func(t *testing.T) {
|
||||
input := `{"formulae":[],"casks":[{"token":"visual-studio-code","name":["Visual Studio Code"],"desc":"Open-source code editor","version":"1.85.0"}]}`
|
||||
ver := parseBrewInfoVersion(input)
|
||||
if ver != "1.85.0" {
|
||||
t.Errorf("expected '1.85.0', got %q", ver)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("empty", func(t *testing.T) {
|
||||
ver := parseBrewInfoVersion("")
|
||||
if ver != "" {
|
||||
t.Errorf("expected empty, got %q", ver)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("invalid json", func(t *testing.T) {
|
||||
ver := parseBrewInfoVersion("not json")
|
||||
if ver != "" {
|
||||
t.Errorf("expected empty, got %q", ver)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("no formulae or casks", func(t *testing.T) {
|
||||
input := `{"formulae":[],"casks":[]}`
|
||||
ver := parseBrewInfoVersion(input)
|
||||
if ver != "" {
|
||||
t.Errorf("expected empty, got %q", ver)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestParseBrewOutdated(t *testing.T) {
|
||||
t.Run("formulae only", func(t *testing.T) {
|
||||
input := `{"formulae":[{"name":"git","installed_versions":["2.43.0"],"current_version":"2.44.0"},{"name":"go","installed_versions":["1.21.6"],"current_version":"1.22.0"}],"casks":[]}`
|
||||
pkgs := parseBrewOutdated(input)
|
||||
if len(pkgs) != 2 {
|
||||
t.Fatalf("expected 2 packages, got %d", len(pkgs))
|
||||
}
|
||||
if pkgs[0].Name != "git" || pkgs[0].Version != "2.44.0" {
|
||||
t.Errorf("unexpected first package: %+v", pkgs[0])
|
||||
}
|
||||
if !pkgs[0].Installed {
|
||||
t.Error("expected Installed=true")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("casks only", func(t *testing.T) {
|
||||
input := `{"formulae":[],"casks":[{"name":"firefox","installed_versions":"119.0","current_version":"120.0"}]}`
|
||||
pkgs := parseBrewOutdated(input)
|
||||
if len(pkgs) != 1 {
|
||||
t.Fatalf("expected 1 package, got %d", len(pkgs))
|
||||
}
|
||||
if pkgs[0].Name != "firefox" || pkgs[0].Version != "120.0" {
|
||||
t.Errorf("unexpected package: %+v", pkgs[0])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("mixed", func(t *testing.T) {
|
||||
input := `{"formulae":[{"name":"git","installed_versions":["2.43.0"],"current_version":"2.44.0"}],"casks":[{"name":"firefox","installed_versions":"119.0","current_version":"120.0"}]}`
|
||||
pkgs := parseBrewOutdated(input)
|
||||
if len(pkgs) != 2 {
|
||||
t.Fatalf("expected 2 packages, got %d", len(pkgs))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("empty", func(t *testing.T) {
|
||||
pkgs := parseBrewOutdated("")
|
||||
if len(pkgs) != 0 {
|
||||
t.Errorf("expected 0 packages, got %d", len(pkgs))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("no outdated", func(t *testing.T) {
|
||||
input := `{"formulae":[],"casks":[]}`
|
||||
pkgs := parseBrewOutdated(input)
|
||||
if len(pkgs) != 0 {
|
||||
t.Errorf("expected 0 packages, got %d", len(pkgs))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("invalid json", func(t *testing.T) {
|
||||
pkgs := parseBrewOutdated("not json")
|
||||
if len(pkgs) != 0 {
|
||||
t.Errorf("expected 0 packages, got %d", len(pkgs))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestSemverCmp(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
a, b string
|
||||
want int
|
||||
}{
|
||||
{"equal", "1.0.0", "1.0.0", 0},
|
||||
{"less major", "1.0.0", "2.0.0", -1},
|
||||
{"greater major", "2.0.0", "1.0.0", 1},
|
||||
{"less minor", "1.2.3", "1.3.0", -1},
|
||||
{"less patch", "1.2.3", "1.2.4", -1},
|
||||
{"multi-digit", "1.10.0", "1.9.0", 1},
|
||||
{"short vs long equal", "1.0", "1.0.0", 0},
|
||||
{"short vs long less", "1.0", "1.0.1", -1},
|
||||
{"short vs long greater", "1.1", "1.0.9", 1},
|
||||
{"single component", "5", "3", 1},
|
||||
{"single equal", "3", "3", 0},
|
||||
{"empty vs empty", "", "", 0},
|
||||
{"empty vs version", "", "1.0", -1},
|
||||
{"version vs empty", "1.0", "", 1},
|
||||
{"four components", "1.2.3.4", "1.2.3.5", -1},
|
||||
{"different lengths", "1.0.0.0", "1.0.0", 0},
|
||||
{"real brew versions", "2.43.0", "2.44.0", -1},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := semverCmp(tt.a, tt.b)
|
||||
if got != tt.want {
|
||||
t.Errorf("semverCmp(%q, %q) = %d, want %d", tt.a, tt.b, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseVersionSuffix(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
wantName string
|
||||
wantVersion string
|
||||
}{
|
||||
{"python@3.12", "python", "3.12"},
|
||||
{"node@18", "node", "18"},
|
||||
{"git", "git", ""},
|
||||
{"ruby@3.2", "ruby", "3.2"},
|
||||
{"", "", ""},
|
||||
{"@3.12", "@3.12", ""}, // @ at position 0, LastIndex returns 0 which is not > 0
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.input, func(t *testing.T) {
|
||||
gotName, gotVer := parseVersionSuffix(tt.input)
|
||||
if gotName != tt.wantName || gotVer != tt.wantVersion {
|
||||
t.Errorf("parseVersionSuffix(%q) = (%q, %q), want (%q, %q)",
|
||||
tt.input, gotName, gotVer, tt.wantName, tt.wantVersion)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseBrewInfo_Empty(t *testing.T) {
|
||||
pkg := parseBrewInfo("")
|
||||
if pkg != nil {
|
||||
t.Error("expected nil for empty input")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseBrewInfo_InvalidJSON(t *testing.T) {
|
||||
pkg := parseBrewInfo("not json")
|
||||
if pkg != nil {
|
||||
t.Error("expected nil for invalid JSON")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseBrewInfo_NoFormulaeOrCasks(t *testing.T) {
|
||||
input := `{"formulae":[],"casks":[]}`
|
||||
pkg := parseBrewInfo(input)
|
||||
if pkg != nil {
|
||||
t.Error("expected nil when no formulae or casks")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseBrewSearch_HeadersOnly(t *testing.T) {
|
||||
input := `==> Formulae
|
||||
|
||||
==> Casks
|
||||
`
|
||||
pkgs := parseBrewSearch(input)
|
||||
if len(pkgs) != 0 {
|
||||
t.Errorf("expected 0 packages, got %d", len(pkgs))
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseBrewSearch_MultiplePerLine(t *testing.T) {
|
||||
input := "git go vim curl\n"
|
||||
pkgs := parseBrewSearch(input)
|
||||
if len(pkgs) != 4 {
|
||||
t.Fatalf("expected 4 packages, got %d", len(pkgs))
|
||||
}
|
||||
names := []string{"git", "go", "vim", "curl"}
|
||||
for i, want := range names {
|
||||
if pkgs[i].Name != want {
|
||||
t.Errorf("pkg[%d].Name = %q, want %q", i, pkgs[i].Name, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseBrewList_NameOnly(t *testing.T) {
|
||||
input := "git\ncurl\n"
|
||||
pkgs := parseBrewList(input)
|
||||
if len(pkgs) != 2 {
|
||||
t.Fatalf("expected 2 packages, got %d", len(pkgs))
|
||||
}
|
||||
if pkgs[0].Version != "" {
|
||||
t.Errorf("expected empty version, got %q", pkgs[0].Version)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseBrewList_WhitespaceLines(t *testing.T) {
|
||||
input := " \n\n git 2.43.0\n \n"
|
||||
pkgs := parseBrewList(input)
|
||||
if len(pkgs) != 1 {
|
||||
t.Fatalf("expected 1 package, got %d", len(pkgs))
|
||||
}
|
||||
}
|
||||
|
||||
func TestInterfaceNonCompliance(t *testing.T) {
|
||||
var m snack.Manager = New()
|
||||
if _, ok := m.(snack.Holder); ok {
|
||||
|
||||
2
go.mod
2
go.mod
@@ -4,7 +4,7 @@ go 1.26.1
|
||||
|
||||
require (
|
||||
github.com/charmbracelet/fang v1.0.0
|
||||
github.com/go-git/go-git/v5 v5.17.0
|
||||
github.com/go-git/go-git/v5 v5.17.1
|
||||
github.com/spf13/cobra v1.10.2
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/testcontainers/testcontainers-go v0.40.0
|
||||
|
||||
4
go.sum
4
go.sum
@@ -87,8 +87,8 @@ github.com/go-git/go-billy/v5 v5.8.0 h1:I8hjc3LbBlXTtVuFNJuwYuMiHvQJDq1AT6u4DwDz
|
||||
github.com/go-git/go-billy/v5 v5.8.0/go.mod h1:RpvI/rw4Vr5QA+Z60c6d6LXH0rYJo0uD5SqfmrrheCY=
|
||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
|
||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
|
||||
github.com/go-git/go-git/v5 v5.17.0 h1:AbyI4xf+7DsjINHMu35quAh4wJygKBKBuXVjV/pxesM=
|
||||
github.com/go-git/go-git/v5 v5.17.0/go.mod h1:f82C4YiLx+Lhi8eHxltLeGC5uBTXSFa6PC5WW9o4SjI=
|
||||
github.com/go-git/go-git/v5 v5.17.1 h1:WnljyxIzSj9BRRUlnmAU35ohDsjRK0EKmL0evDqi5Jk=
|
||||
github.com/go-git/go-git/v5 v5.17.1/go.mod h1:pW/VmeqkanRFqR6AljLcs7EA7FbZaN5MQqO7oZADXpo=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
|
||||
53
install.sh
Executable file
53
install.sh
Executable file
@@ -0,0 +1,53 @@
|
||||
#!/bin/sh
|
||||
# Install snack - a unified CLI for system package managers
|
||||
# Usage: curl -sSfL https://raw.githubusercontent.com/gogrlx/snack/main/install.sh | sh
|
||||
set -e
|
||||
|
||||
REPO="gogrlx/snack"
|
||||
INSTALL_DIR="${INSTALL_DIR:-/usr/local/bin}"
|
||||
|
||||
# Detect OS and arch
|
||||
OS="$(uname -s | tr '[:upper:]' '[:lower:]')"
|
||||
ARCH="$(uname -m)"
|
||||
case "$ARCH" in
|
||||
x86_64) ARCH="amd64" ;;
|
||||
aarch64) ARCH="arm64" ;;
|
||||
armv*) ARCH="arm" ;;
|
||||
esac
|
||||
|
||||
# macOS universal binary
|
||||
if [ "$OS" = "darwin" ]; then
|
||||
ARCH="universal"
|
||||
fi
|
||||
|
||||
echo "Detected: ${OS}/${ARCH}"
|
||||
|
||||
# Get latest release tag
|
||||
TAG="$(curl -sSf "https://api.github.com/repos/${REPO}/releases/latest" | grep '"tag_name"' | sed -E 's/.*"([^"]+)".*/\1/')"
|
||||
VERSION="${TAG#v}"
|
||||
|
||||
if [ -z "$VERSION" ]; then
|
||||
echo "Error: could not determine latest version" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Installing snack ${VERSION}..."
|
||||
|
||||
TARBALL="snack-${VERSION}-${OS}-${ARCH}.tar.gz"
|
||||
URL="https://github.com/${REPO}/releases/download/${TAG}/${TARBALL}"
|
||||
|
||||
TMP="$(mktemp -d)"
|
||||
trap 'rm -rf "$TMP"' EXIT
|
||||
|
||||
curl -sSfL "$URL" -o "${TMP}/${TARBALL}"
|
||||
tar xzf "${TMP}/${TARBALL}" -C "$TMP"
|
||||
|
||||
if [ -w "$INSTALL_DIR" ]; then
|
||||
mv "${TMP}/snack" "${INSTALL_DIR}/snack"
|
||||
else
|
||||
echo "Installing to ${INSTALL_DIR} (requires sudo)..."
|
||||
sudo mv "${TMP}/snack" "${INSTALL_DIR}/snack"
|
||||
fi
|
||||
|
||||
chmod +x "${INSTALL_DIR}/snack"
|
||||
echo "snack ${VERSION} installed to ${INSTALL_DIR}/snack"
|
||||
Reference in New Issue
Block a user