Skip to content

Commit 71818f7

Browse files
committed
feat(cli): cache version checks and add TTY-aware table output
Version checking: - Results cached for 24h at ~/.config/raystack/<repo>/state.json - Eliminates HTTP call to GitHub on every command invocation - Cache respects XDG_CONFIG_HOME and APPDATA on Windows Table output: - TTY: tab-aligned columns with padding (human-friendly) - Non-TTY: raw tab-separated values (pipe to awk/cut/jq)
1 parent f560a14 commit 71818f7

2 files changed

Lines changed: 90 additions & 7 deletions

File tree

cli/printer/printer.go

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -112,13 +112,21 @@ func (o *Output) YAML(data interface{}) error {
112112
return nil
113113
}
114114

115-
// Table writes rows as a tab-aligned table.
115+
// Table writes rows as a tab-aligned table when the output is a TTY.
116+
// When piped (non-TTY), it writes tab-separated values for easy
117+
// processing with tools like awk, cut, or jq.
116118
func (o *Output) Table(rows [][]string) {
117-
tw := tabwriter.NewWriter(o.w, 0, 0, 2, ' ', 0)
118-
for _, row := range rows {
119-
fmt.Fprintln(tw, strings.Join(row, "\t"))
119+
if o.tty {
120+
tw := tabwriter.NewWriter(o.w, 0, 0, 2, ' ', 0)
121+
for _, row := range rows {
122+
fmt.Fprintln(tw, strings.Join(row, "\t"))
123+
}
124+
tw.Flush()
125+
} else {
126+
for _, row := range rows {
127+
fmt.Fprintln(o.w, strings.Join(row, "\t"))
128+
}
120129
}
121-
tw.Flush()
122130
}
123131

124132
// --- Markdown ---

cli/version/release.go

Lines changed: 77 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ import (
55
"fmt"
66
"io"
77
"net/http"
8+
"os"
9+
"path/filepath"
10+
"runtime"
811
"time"
912

1013
"github.com/hashicorp/go-version"
@@ -13,13 +16,19 @@ import (
1316
var (
1417
timeout = time.Second * 1
1518
apiFormat = "https://api.github.com/repos/%s/releases/latest"
19+
cacheTTL = 24 * time.Hour
1620
)
1721

1822
type releaseInfo struct {
1923
version string
2024
tarURL string
2125
}
2226

27+
type cacheEntry struct {
28+
CheckedAt time.Time `json:"checked_at"`
29+
LatestVersion string `json:"latest_version"`
30+
}
31+
2332
func fetchInfo(url string) (*releaseInfo, error) {
2433
httpClient := http.Client{Timeout: timeout}
2534
req, err := http.NewRequest(http.MethodGet, url, nil)
@@ -77,17 +86,83 @@ func compareVersions(current, latest string) (bool, error) {
7786
// CheckForUpdate checks GitHub for a newer release and returns an update
7887
// message if one is available. Returns an empty string if up-to-date or
7988
// if the check fails.
89+
//
90+
// Results are cached for 24 hours to avoid hitting GitHub on every invocation.
91+
// The cache is stored at ~/.config/raystack/<repo>/state.json.
8092
func CheckForUpdate(currentVersion, repo string) string {
93+
// Check cache first.
94+
if latest, ok := readCache(repo); ok {
95+
return buildMessage(currentVersion, latest)
96+
}
97+
98+
// Fetch from GitHub.
8199
releaseURL := fmt.Sprintf(apiFormat, repo)
82100
info, err := fetchInfo(releaseURL)
83101
if err != nil {
84102
return ""
85103
}
86104

87-
isLatest, err := compareVersions(currentVersion, info.version)
105+
// Cache the result.
106+
writeCache(repo, info.version)
107+
108+
return buildMessage(currentVersion, info.version)
109+
}
110+
111+
func buildMessage(current, latest string) string {
112+
isLatest, err := compareVersions(current, latest)
88113
if err != nil || isLatest {
89114
return ""
90115
}
116+
return fmt.Sprintf("A new release (%s) is available. consider updating to latest version.", latest)
117+
}
118+
119+
func cachePath(repo string) string {
120+
dir := configDir()
121+
return filepath.Join(dir, "raystack", repo, "state.json")
122+
}
123+
124+
func readCache(repo string) (string, bool) {
125+
data, err := os.ReadFile(cachePath(repo))
126+
if err != nil {
127+
return "", false
128+
}
129+
130+
var entry cacheEntry
131+
if err := json.Unmarshal(data, &entry); err != nil {
132+
return "", false
133+
}
134+
135+
if time.Since(entry.CheckedAt) > cacheTTL {
136+
return "", false
137+
}
138+
139+
return entry.LatestVersion, true
140+
}
141+
142+
func writeCache(repo, latestVersion string) {
143+
path := cachePath(repo)
144+
os.MkdirAll(filepath.Dir(path), 0755)
145+
146+
entry := cacheEntry{
147+
CheckedAt: time.Now(),
148+
LatestVersion: latestVersion,
149+
}
150+
data, err := json.Marshal(entry)
151+
if err != nil {
152+
return
153+
}
154+
os.WriteFile(path, data, 0644)
155+
}
91156

92-
return fmt.Sprintf("A new release (%s) is available. consider updating to latest version.", info.version)
157+
func configDir() string {
158+
if dir := os.Getenv("XDG_CONFIG_HOME"); dir != "" {
159+
return dir
160+
}
161+
if runtime.GOOS == "windows" {
162+
if dir := os.Getenv("APPDATA"); dir != "" {
163+
return dir
164+
}
165+
}
166+
home, _ := os.UserHomeDir()
167+
return filepath.Join(home, ".config")
93168
}

0 commit comments

Comments
 (0)