Skip to content

Commit e3e746e

Browse files
committed
feat: initial implementation
PLOS open-access CLI with search, recent, article, and journals commands. Solr REST API client with rate-pacing, retries, and httptest-based tests.
1 parent dc97bc0 commit e3e746e

12 files changed

Lines changed: 1253 additions & 76 deletions

File tree

cli/cmd_article.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package cli
2+
3+
import (
4+
"github.com/spf13/cobra"
5+
)
6+
7+
func (a *App) articleCmd() *cobra.Command {
8+
return &cobra.Command{
9+
Use: "article <doi>",
10+
Short: "Fetch a single PLOS article by DOI",
11+
Args: cobra.ExactArgs(1),
12+
RunE: func(cmd *cobra.Command, args []string) error {
13+
a.progressf("fetching article %s...", args[0])
14+
art, err := a.client.GetArticle(cmd.Context(), args[0])
15+
if err != nil {
16+
return mapFetchErr(err)
17+
}
18+
return a.render(art)
19+
},
20+
}
21+
}

cli/cmd_journals.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package cli
2+
3+
import (
4+
"github.com/spf13/cobra"
5+
)
6+
7+
func (a *App) journalsCmd() *cobra.Command {
8+
return &cobra.Command{
9+
Use: "journals",
10+
Short: "List available PLOS journals",
11+
RunE: func(cmd *cobra.Command, _ []string) error {
12+
journals := a.client.Journals()
13+
return a.renderOrEmpty(journals, len(journals))
14+
},
15+
}
16+
}

cli/cmd_search.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package cli
2+
3+
import (
4+
"github.com/spf13/cobra"
5+
)
6+
7+
func (a *App) searchCmd() *cobra.Command {
8+
return &cobra.Command{
9+
Use: "search <query>",
10+
Short: "Search PLOS articles",
11+
Args: cobra.ExactArgs(1),
12+
RunE: func(cmd *cobra.Command, args []string) error {
13+
n := a.effectiveLimit(20)
14+
a.progressf("searching for %q...", args[0])
15+
arts, err := a.client.Search(cmd.Context(), args[0], n)
16+
if err != nil {
17+
return mapFetchErr(err)
18+
}
19+
return a.renderOrEmpty(arts, len(arts))
20+
},
21+
}
22+
}
23+
24+
func (a *App) recentCmd() *cobra.Command {
25+
var journal string
26+
cmd := &cobra.Command{
27+
Use: "recent",
28+
Short: "Fetch recent PLOS articles",
29+
RunE: func(cmd *cobra.Command, _ []string) error {
30+
n := a.effectiveLimit(20)
31+
if journal != "" {
32+
a.progressf("fetching %d recent articles from %s...", n, journal)
33+
} else {
34+
a.progressf("fetching %d recent articles...", n)
35+
}
36+
arts, err := a.client.Recent(cmd.Context(), journal, n)
37+
if err != nil {
38+
return mapFetchErr(err)
39+
}
40+
return a.renderOrEmpty(arts, len(arts))
41+
},
42+
}
43+
cmd.Flags().StringVar(&journal, "journal", "", "filter by journal key (e.g. PLoSONE, PLoSBiology)")
44+
return cmd
45+
}

cli/errors.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package cli
2+
3+
import (
4+
"errors"
5+
6+
"github.com/tamnd/plosone-cli/plosone"
7+
)
8+
9+
func isNotFound(err error) bool {
10+
return errors.Is(err, plosone.ErrNotFound)
11+
}

cli/output.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package cli
2+
3+
import (
4+
"io"
5+
6+
"github.com/tamnd/plosone-cli/pkg/render"
7+
)
8+
9+
// Format aliases so command code reads cleanly.
10+
type Format = render.Format
11+
12+
const (
13+
FormatTable = render.FormatTable
14+
FormatJSON = render.FormatJSON
15+
FormatJSONL = render.FormatJSONL
16+
FormatCSV = render.FormatCSV
17+
FormatTSV = render.FormatTSV
18+
FormatURL = render.FormatURL
19+
FormatRaw = render.FormatRaw
20+
)
21+
22+
// NewRenderer builds a renderer writing to w.
23+
func NewRenderer(w io.Writer, format Format, fields []string, noHeader bool, tmpl string) *render.Renderer {
24+
return render.New(w, format, fields, noHeader, tmpl)
25+
}

cli/root.go

Lines changed: 125 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,12 @@
22
package cli
33

44
import (
5+
"fmt"
6+
"os"
7+
8+
"github.com/mattn/go-isatty"
59
"github.com/spf13/cobra"
10+
"github.com/tamnd/plosone-cli/plosone"
611
)
712

813
// Build metadata, set via -ldflags at release time.
@@ -12,20 +17,135 @@ var (
1217
Date = "unknown"
1318
)
1419

20+
// exit codes.
21+
const (
22+
exitError = 1
23+
exitUsage = 2
24+
exitNoData = 3
25+
)
26+
27+
// ExitError carries a process exit code up to main.
28+
type ExitError struct {
29+
Code int
30+
Err error
31+
}
32+
33+
func (e *ExitError) Error() string {
34+
if e.Err != nil {
35+
return e.Err.Error()
36+
}
37+
return fmt.Sprintf("exit %d", e.Code)
38+
}
39+
40+
func (e *ExitError) Unwrap() error { return e.Err }
41+
42+
func codeError(code int, err error) error { return &ExitError{Code: code, Err: err} }
43+
44+
// App holds shared state threaded through every command.
45+
type App struct {
46+
client *plosone.Client
47+
cfg plosone.Config
48+
49+
output string
50+
fields []string
51+
noHeader bool
52+
template string
53+
limit int
54+
quiet bool
55+
}
56+
1557
// Root builds the root command and its subtree.
1658
func Root() *cobra.Command {
59+
app := &App{cfg: plosone.DefaultConfig()}
60+
1761
root := &cobra.Command{
1862
Use: "plos",
1963
Short: "Browse PLOS open-access scientific journals",
20-
Long: `Browse PLOS open-access scientific journals
64+
Long: `plos reads the PLOS (Public Library of Science) open-access journals
65+
through the public Solr REST API at api.plos.org. No API key, no account,
66+
no authentication. It returns records as table, JSON, JSONL, CSV, TSV, or URLs.
2167
22-
This is a fresh scaffold. Add your commands here on top of the plosone
23-
library package, then wire them into Root with root.AddCommand.`,
68+
plos is an independent tool and is not affiliated with PLOS.`,
2469
SilenceUsage: true,
2570
SilenceErrors: true,
71+
PersistentPreRunE: func(cmd *cobra.Command, _ []string) error {
72+
return app.setup()
73+
},
2674
}
2775

28-
root.AddCommand(newVersionCmd())
29-
// TODO: root.AddCommand(newGetCmd()), etc.
76+
pf := root.PersistentFlags()
77+
pf.StringVarP(&app.output, "output", "o", "auto", "output: table|json|jsonl|csv|tsv|url|raw (auto=table on TTY, jsonl piped)")
78+
pf.StringSliceVar(&app.fields, "fields", nil, "comma-separated columns to include")
79+
pf.BoolVar(&app.noHeader, "no-header", false, "omit the header row in table/csv/tsv")
80+
pf.StringVar(&app.template, "template", "", "Go text/template applied per record")
81+
pf.IntVarP(&app.limit, "limit", "n", 0, "limit number of records (0 = command default)")
82+
pf.BoolVarP(&app.quiet, "quiet", "q", false, "suppress progress on stderr")
83+
84+
pf.DurationVar(&app.cfg.Rate, "delay", app.cfg.Rate, "minimum spacing between requests")
85+
pf.DurationVar(&app.cfg.Timeout, "timeout", app.cfg.Timeout, "per-request timeout")
86+
pf.IntVar(&app.cfg.Retries, "retries", app.cfg.Retries, "retry attempts on 429/5xx")
87+
pf.StringVar(&app.cfg.UserAgent, "user-agent", app.cfg.UserAgent, "User-Agent sent with each request")
88+
89+
root.AddCommand(
90+
app.searchCmd(),
91+
app.recentCmd(),
92+
app.articleCmd(),
93+
app.journalsCmd(),
94+
newVersionCmd(),
95+
)
3096
return root
3197
}
98+
99+
func (a *App) setup() error {
100+
if a.output == "" || a.output == "auto" {
101+
if isatty.IsTerminal(os.Stdout.Fd()) {
102+
a.output = string(FormatTable)
103+
} else {
104+
a.output = string(FormatJSONL)
105+
}
106+
}
107+
if !Format(a.output).Valid() {
108+
return codeError(exitUsage, fmt.Errorf("unknown output format %q", a.output))
109+
}
110+
a.client = plosone.NewClient(a.cfg)
111+
return nil
112+
}
113+
114+
func (a *App) render(records any) error {
115+
r := NewRenderer(os.Stdout, Format(a.output), a.fields, a.noHeader, a.template)
116+
return r.Render(records)
117+
}
118+
119+
func (a *App) renderOrEmpty(records any, n int) error {
120+
if err := a.render(records); err != nil {
121+
return err
122+
}
123+
if n == 0 {
124+
return codeError(exitNoData, nil)
125+
}
126+
return nil
127+
}
128+
129+
func (a *App) progressf(format string, args ...any) {
130+
if a.quiet {
131+
return
132+
}
133+
_, _ = fmt.Fprintf(os.Stderr, format+"\n", args...)
134+
}
135+
136+
func mapFetchErr(err error) error {
137+
if err == nil {
138+
return nil
139+
}
140+
if isNotFound(err) {
141+
return codeError(exitNoData, err)
142+
}
143+
return codeError(exitError, err)
144+
}
145+
146+
func (a *App) effectiveLimit(def int) int {
147+
if a.limit > 0 {
148+
return a.limit
149+
}
150+
return def
151+
}

go.mod

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,34 @@ go 1.26
44

55
require (
66
github.com/charmbracelet/fang v1.0.0
7+
github.com/mattn/go-isatty v0.0.22
78
github.com/spf13/cobra v1.10.2
89
)
10+
11+
require (
12+
charm.land/lipgloss/v2 v2.0.0-beta.3.0.20251106193318-19329a3e8410 // indirect
13+
github.com/charmbracelet/colorprofile v0.3.3 // indirect
14+
github.com/charmbracelet/ultraviolet v0.0.0-20251106190538-99ea45596692 // indirect
15+
github.com/charmbracelet/x/ansi v0.11.0 // indirect
16+
github.com/charmbracelet/x/exp/charmtone v0.0.0-20250603201427-c31516f43444 // indirect
17+
github.com/charmbracelet/x/term v0.2.2 // indirect
18+
github.com/charmbracelet/x/termios v0.1.1 // indirect
19+
github.com/charmbracelet/x/windows v0.2.2 // indirect
20+
github.com/clipperhouse/displaywidth v0.4.1 // indirect
21+
github.com/clipperhouse/stringish v0.1.1 // indirect
22+
github.com/clipperhouse/uax29/v2 v2.3.0 // indirect
23+
github.com/inconshreveable/mousetrap v1.1.0 // indirect
24+
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
25+
github.com/mattn/go-runewidth v0.0.19 // indirect
26+
github.com/muesli/cancelreader v0.2.2 // indirect
27+
github.com/muesli/mango v0.1.0 // indirect
28+
github.com/muesli/mango-cobra v1.2.0 // indirect
29+
github.com/muesli/mango-pflag v0.1.0 // indirect
30+
github.com/muesli/roff v0.1.0 // indirect
31+
github.com/rivo/uniseg v0.4.7 // indirect
32+
github.com/spf13/pflag v1.0.9 // indirect
33+
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
34+
golang.org/x/sync v0.17.0 // indirect
35+
golang.org/x/sys v0.37.0 // indirect
36+
golang.org/x/text v0.24.0 // indirect
37+
)

go.sum

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
charm.land/lipgloss/v2 v2.0.0-beta.3.0.20251106193318-19329a3e8410 h1:D9PbaszZYpB4nj+d6HTWr1onlmlyuGVNfL9gAi8iB3k=
2+
charm.land/lipgloss/v2 v2.0.0-beta.3.0.20251106193318-19329a3e8410/go.mod h1:1qZyvvVCenJO2M1ac2mX0yyiIZJoZmDM4DG4s0udJkU=
3+
github.com/aymanbagabas/go-udiff v0.3.1 h1:LV+qyBQ2pqe0u42ZsUEtPiCaUoqgA9gYRDs3vj1nolY=
4+
github.com/aymanbagabas/go-udiff v0.3.1/go.mod h1:G0fsKmG+P6ylD0r6N/KgQD/nWzgfnl8ZBcNLgcbrw8E=
5+
github.com/charmbracelet/colorprofile v0.3.3 h1:DjJzJtLP6/NZ8p7Cgjno0CKGr7wwRJGxWUwh2IyhfAI=
6+
github.com/charmbracelet/colorprofile v0.3.3/go.mod h1:nB1FugsAbzq284eJcjfah2nhdSLppN2NqvfotkfRYP4=
7+
github.com/charmbracelet/fang v1.0.0 h1:jESBY40agJOlLYnnv9jE0mLqDGTxEk0hkOnx7YGyRlQ=
8+
github.com/charmbracelet/fang v1.0.0/go.mod h1:P5/DNb9DddQ0Z0dbc0P3ol4/ix5Po7Ofr2KMBfAqoCo=
9+
github.com/charmbracelet/ultraviolet v0.0.0-20251106190538-99ea45596692 h1:r/3jQZ1LjWW6ybp8HHfhrKrwHIWiJhUuY7wwYIWZulQ=
10+
github.com/charmbracelet/ultraviolet v0.0.0-20251106190538-99ea45596692/go.mod h1:Y8B4DzWeTb0ama8l3+KyopZtkE8fZjwRQ3aEAPEXHE0=
11+
github.com/charmbracelet/x/ansi v0.11.0 h1:uuIVK7GIplwX6UBIz8S2TF8nkr7xRlygSsBRjSJqIvA=
12+
github.com/charmbracelet/x/ansi v0.11.0/go.mod h1:uQt8bOrq/xgXjlGcFMc8U2WYbnxyjrKhnvTQluvfCaE=
13+
github.com/charmbracelet/x/exp/charmtone v0.0.0-20250603201427-c31516f43444 h1:IJDiTgVE56gkAGfq0lBEloWgkXMk4hl/bmuPoicI4R0=
14+
github.com/charmbracelet/x/exp/charmtone v0.0.0-20250603201427-c31516f43444/go.mod h1:T9jr8CzFpjhFVHjNjKwbAD7KwBNyFnj2pntAO7F2zw0=
15+
github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f h1:pk6gmGpCE7F3FcjaOEKYriCvpmIN4+6OS/RD0vm4uIA=
16+
github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f/go.mod h1:IfZAMTHB6XkZSeXUqriemErjAWCCzT0LwjKFYCZyw0I=
17+
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
18+
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
19+
github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY=
20+
github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo=
21+
github.com/charmbracelet/x/windows v0.2.2 h1:IofanmuvaxnKHuV04sC0eBy/smG6kIKrWG2/jYn2GuM=
22+
github.com/charmbracelet/x/windows v0.2.2/go.mod h1:/8XtdKZzedat74NQFn0NGlGL4soHB0YQZrETF96h75k=
23+
github.com/clipperhouse/displaywidth v0.4.1 h1:uVw9V8UDfnggg3K2U84VWY1YLQ/x2aKSCtkRyYozfoU=
24+
github.com/clipperhouse/displaywidth v0.4.1/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o=
25+
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
26+
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
27+
github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4=
28+
github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
29+
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
30+
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
31+
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
32+
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
33+
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
34+
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
35+
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
36+
github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
37+
github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
38+
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
39+
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
40+
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
41+
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
42+
github.com/muesli/mango v0.1.0 h1:DZQK45d2gGbql1arsYA4vfg4d7I9Hfx5rX/GCmzsAvI=
43+
github.com/muesli/mango v0.1.0/go.mod h1:5XFpbC8jY5UUv89YQciiXNlbi+iJgt29VDC5xbzrLL4=
44+
github.com/muesli/mango-cobra v1.2.0 h1:DQvjzAM0PMZr85Iv9LIMaYISpTOliMEg+uMFtNbYvWg=
45+
github.com/muesli/mango-cobra v1.2.0/go.mod h1:vMJL54QytZAJhCT13LPVDfkvCUJ5/4jNUKF/8NC2UjA=
46+
github.com/muesli/mango-pflag v0.1.0 h1:UADqbYgpUyRoBja3g6LUL+3LErjpsOwaC9ywvBWe7Sg=
47+
github.com/muesli/mango-pflag v0.1.0/go.mod h1:YEQomTxaCUp8PrbhFh10UfbhbQrM/xJ4i2PB8VTLLW0=
48+
github.com/muesli/roff v0.1.0 h1:YD0lalCotmYuF5HhZliKWlIx7IEhiXeSfq7hNjFqGF8=
49+
github.com/muesli/roff v0.1.0/go.mod h1:pjAHQM9hdUUwm/krAfrLGgJkXJ+YuhtsfZ42kieB2Ig=
50+
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
51+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
52+
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
53+
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
54+
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
55+
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
56+
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
57+
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
58+
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
59+
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
60+
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
61+
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
62+
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
63+
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
64+
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
65+
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
66+
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
67+
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
68+
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
69+
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
70+
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
71+
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
72+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
73+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
74+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

0 commit comments

Comments
 (0)