Skip to content

Commit 6c69a96

Browse files
committed
feat(cli): add subcommand for static output of a given package
$ vrs <packagename>
1 parent 4966f8a commit 6c69a96

2 files changed

Lines changed: 122 additions & 6 deletions

File tree

cli/README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ brew install grega/tap/vrs
1818

1919
## Usage
2020

21+
Run `vrs` with no arguments to launch the interactive TUI:
22+
2123
| Key | Action |
2224
|---|---|
2325
| Type | Fuzzy-search packages by name |
@@ -29,6 +31,18 @@ brew install grega/tap/vrs
2931
| `Esc` | Clear search / go back / quit |
3032
| `Ctrl+C` | Quit |
3133

34+
### Static mode
35+
36+
Pass a package name to print its release list directly to stdout - handy for quick lookups:
37+
38+
```sh
39+
vrs go
40+
vrs prettier
41+
vrs github cli # multi-word names work without quotes
42+
```
43+
44+
The match is case-insensitive. If the package isn't found, `vrs` exits with status 1 and suggests close matches.
45+
3246
## Caching
3347

3448
API responses are cached locally for 1 hour (the backend only refreshes every ~6 hours). The cache file lives at:

cli/main.go

Lines changed: 108 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -328,34 +328,47 @@ func (m model) Init() tea.Cmd {
328328

329329
// ── Commands ────────────────────────────────────────────────────────────────
330330

331-
func fetchPackages() tea.Msg {
331+
func loadPackages() ([]Package, bool, string, time.Time, error) {
332332
if path := cacheFilePath(); path != "" {
333333
if packages, fetchedAt := loadCache(path); packages != nil {
334-
return packagesFetchedMsg{packages: packages, fromCache: true, cachePath: path, cacheFetchedAt: fetchedAt}
334+
return packages, true, path, fetchedAt, nil
335335
}
336336
}
337337

338338
resp, err := http.Get(apiURL)
339339
if err != nil {
340-
return fetchErrMsg{err}
340+
return nil, false, "", time.Time{}, err
341341
}
342342
defer resp.Body.Close()
343343

344344
body, err := io.ReadAll(resp.Body)
345345
if err != nil {
346-
return fetchErrMsg{err}
346+
return nil, false, "", time.Time{}, err
347347
}
348348

349349
var packages []Package
350350
if err := json.Unmarshal(body, &packages); err != nil {
351-
return fetchErrMsg{err}
351+
return nil, false, "", time.Time{}, err
352352
}
353353

354354
if path := cacheFilePath(); path != "" {
355355
writeCache(path, packages)
356356
}
357357

358-
return packagesFetchedMsg{packages: packages}
358+
return packages, false, "", time.Time{}, nil
359+
}
360+
361+
func fetchPackages() tea.Msg {
362+
packages, fromCache, cachePath, fetchedAt, err := loadPackages()
363+
if err != nil {
364+
return fetchErrMsg{err}
365+
}
366+
return packagesFetchedMsg{
367+
packages: packages,
368+
fromCache: fromCache,
369+
cachePath: cachePath,
370+
cacheFetchedAt: fetchedAt,
371+
}
359372
}
360373

361374
// ── Filtering ───────────────────────────────────────────────────────────────
@@ -846,6 +859,91 @@ func renderName(name string, matchedIndexes []int, selected bool) string {
846859
return result.String()
847860
}
848861

862+
// ── Static (non-interactive) mode ───────────────────────────────────────────
863+
864+
func findPackage(packages []Package, query string) *Package {
865+
for i := range packages {
866+
if strings.EqualFold(packages[i].Name, query) {
867+
return &packages[i]
868+
}
869+
}
870+
return nil
871+
}
872+
873+
func renderStaticDetail(pkg *Package) string {
874+
var b strings.Builder
875+
876+
header := headerStyle.Render(pkg.Name)
877+
if len(pkg.Categories) > 0 {
878+
cats := categoryStyle.Render(" " + strings.Join(pkg.Categories, " · "))
879+
b.WriteString(lipgloss.JoinHorizontal(lipgloss.Center, header, cats) + "\n")
880+
} else {
881+
b.WriteString(header + "\n")
882+
}
883+
884+
relHeader := subtitleStyle.Bold(true).MarginTop(1).Render(" Releases")
885+
b.WriteString(relHeader + "\n")
886+
b.WriteString(" " + dividerStyle.Render(strings.Repeat("─", 52)) + "\n")
887+
888+
releases := make([]Release, 0, len(pkg.Releases)+1)
889+
releases = append(releases, pkg.LatestStable)
890+
for _, r := range pkg.Releases {
891+
if r.Version != pkg.LatestStable.Version {
892+
releases = append(releases, r)
893+
}
894+
}
895+
896+
for i, rel := range releases {
897+
isLatest := i == 0
898+
v := relVerDimStyle.Render(fmt.Sprintf("%-28s", rel.Version))
899+
d := dateStyle.Render(rel.Date)
900+
line := fmt.Sprintf(" %s %s", v, d)
901+
if rel.Prerelease {
902+
line += " " + prerelDimStyle.Render("pre")
903+
}
904+
if isLatest {
905+
line += " " + latestDimBadge.Render("latest")
906+
}
907+
b.WriteString(line + "\n")
908+
909+
if isLatest {
910+
b.WriteString(" " + dividerStyle.Render(strings.Repeat("─", 52)) + "\n")
911+
}
912+
}
913+
914+
return b.String()
915+
}
916+
917+
func runStatic(query string) int {
918+
packages, _, _, _, err := loadPackages()
919+
if err != nil {
920+
title := errorTitleStyle.Render("Failed to fetch package data")
921+
detail := errorDetailStyle.Render(err.Error())
922+
box := errorBoxStyle.Render(title + "\n" + detail)
923+
fmt.Fprintln(os.Stderr, "\n"+box+"\n")
924+
return 1
925+
}
926+
927+
pkg := findPackage(packages, query)
928+
if pkg == nil {
929+
fmt.Fprintf(os.Stderr, "Package %q not found.", query)
930+
matches := fuzzy.FindFrom(query, packageSource(packages))
931+
if len(matches) > 0 {
932+
limit := min(len(matches), 5)
933+
suggestions := make([]string, 0, limit)
934+
for _, m := range matches[:limit] {
935+
suggestions = append(suggestions, packages[m.Index].Name)
936+
}
937+
fmt.Fprintf(os.Stderr, " Did you mean: %s?", strings.Join(suggestions, ", "))
938+
}
939+
fmt.Fprintln(os.Stderr)
940+
return 1
941+
}
942+
943+
fmt.Println(renderStaticDetail(pkg))
944+
return 0
945+
}
946+
849947
// ── Main ────────────────────────────────────────────────────────────────────
850948

851949
func main() {
@@ -857,6 +955,10 @@ func main() {
857955
return
858956
}
859957

958+
if args := flag.Args(); len(args) > 0 {
959+
os.Exit(runStatic(strings.Join(args, " ")))
960+
}
961+
860962
p := tea.NewProgram(initialModel(), tea.WithAltScreen())
861963
finalModel, err := p.Run()
862964
if err != nil {

0 commit comments

Comments
 (0)