| // Copyright 2026 The Go Authors. All rights reserved. | |
| // Use of this source code is governed by a BSD-style | |
| // license that can be found in the LICENSE file. | |
| // Command pkgsite-cli queries the pkg.go.dev API for information about | |
| // Go packages and modules. | |
| // | |
| // For more information, see https://go.dev/blog/pkgsite-api. | |
| // | |
| // Usage: | |
| // | |
| // pkgsite-cli package <package>[@version] [flags] Show package details. | |
| // pkgsite-cli module <module>[@version] [flags] Show module details. | |
| // pkgsite-cli search <query> [flags] Search for packages. | |
| package main | |
| import ( | |
| "context" | |
| "encoding/json" | |
| "errors" | |
| "flag" | |
| "fmt" | |
| "io" | |
| "os" | |
| "path/filepath" | |
| "strings" | |
| "golang.org/x/pkgsite/cmd/internal/pkgsite-cli/client" | |
| "golang.org/x/telemetry" | |
| ) | |
| func main() { | |
| cfg := telemetry.Config{ | |
| ReportCrashes: true, | |
| } | |
| telemetry.Start(cfg) | |
| os.Exit(run(os.Args[1:], os.Stdout, os.Stderr)) | |
| } | |
| func run(args []string, stdout, stderr io.Writer) int { | |
| return dispatch(args, commands(), stdout, stderr) | |
| } | |
| func commands() []*command { | |
| var pf packageFlags | |
| pkgFS := flag.NewFlagSet(filepath.Base(os.Args[0]), flag.ContinueOnError) | |
| pf.register(pkgFS) | |
| var mf moduleFlags | |
| modFS := flag.NewFlagSet(filepath.Base(os.Args[0])+" module", flag.ContinueOnError) | |
| mf.register(modFS) | |
| var sf searchFlags | |
| searchFS := flag.NewFlagSet(filepath.Base(os.Args[0])+" search", flag.ContinueOnError) | |
| sf.register(searchFS) | |
| pkgRun := func(fs *flag.FlagSet, stdout, stderr io.Writer) int { return runPackage(fs, &pf, stdout, stderr) } | |
| const packageDoc = `Queries information about a specific Go package from pkg.go.dev. | |
| By default, this prints basic metadata. Use flags to request additional | |
| information such as exported symbols, reverse dependencies, or rendered documentation. | |
| When using -json, the output is a JSON object with the following structure: | |
| type packageResult struct { | |
| Package *Package // package information | |
| Symbols *PaginatedResponse[Symbol] // symbols (with -symbols) | |
| ImportedBy *PackageImportedBy // reverse dependencies (with -imported-by) | |
| } | |
| type Package struct { | |
| Path string | |
| Name string | |
| Synopsis string | |
| IsRedistributable bool | |
| ModulePath string | |
| Version string | |
| IsLatest bool | |
| IsStandardLibrary bool | |
| GOOS string | |
| GOARCH string | |
| Docs string // rendered documentation (with -doc) | |
| Imports []string // imports (with -imports) | |
| Licenses []License // licenses (with -licenses) | |
| } | |
| type License struct { | |
| Types []string | |
| FilePath string | |
| } | |
| ` | |
| const moduleDoc = `Queries information about a specific Go module from pkg.go.dev. | |
| By default, this prints basic metadata. Use flags to request additional | |
| information such as versions, vulnerabilities, or packages contained in the module. | |
| When using -json, the output is a JSON object with the following structure: | |
| type moduleResult struct { | |
| Module *Module // module information | |
| Versions *PaginatedResponse[VersionResponse] // versions (with -versions) | |
| Vulns *PaginatedResponse[Vulnerability] // vulnerabilities (with -vulns) | |
| Packages *PaginatedResponse[ModulePackageResponse] // packages (with -packages) | |
| } | |
| type Module struct { | |
| Path string | |
| Version string | |
| CommitTime time.Time | |
| IsLatest bool | |
| IsRedistributable bool | |
| IsStandardLibrary bool | |
| HasGoMod bool | |
| RepoURL string | |
| GoModContents string | |
| Readme *Readme | |
| Licenses []License | |
| } | |
| ` | |
| const searchDoc = `Searches for Go packages on pkg.go.dev matching the given query. | |
| By default, this prints a list of matching packages with their synopsis. | |
| When using -json, the output is a JSON object with the following structure: | |
| type PaginatedResponse[SearchResult] struct { | |
| Items []SearchResult | |
| Total int | |
| NextPageToken string | |
| } | |
| type SearchResult struct { | |
| PackagePath string | |
| ModulePath string | |
| Version string | |
| Synopsis string | |
| } | |
| ` | |
| var cmds []*command | |
| cmds = []*command{ | |
| { | |
| name: "package", | |
| args: "<package>[@version]", | |
| summary: "Show package details", | |
| description: strings.TrimSpace(packageDoc), | |
| flags: pkgFS, | |
| run: pkgRun, | |
| }, | |
| { | |
| name: "module", | |
| args: "<module>[@version]", | |
| summary: "Show module details", | |
| description: strings.TrimSpace(moduleDoc), | |
| flags: modFS, | |
| run: func(fs *flag.FlagSet, stdout, stderr io.Writer) int { return runModule(fs, &mf, stdout, stderr) }, | |
| }, | |
| { | |
| name: "search", | |
| args: "<query>", | |
| summary: "Search for packages", | |
| description: strings.TrimSpace(searchDoc), | |
| flags: searchFS, | |
| run: func(fs *flag.FlagSet, stdout, stderr io.Writer) int { return runSearch(fs, &sf, stdout, stderr) }, | |
| }, | |
| { | |
| name: "help", | |
| summary: "Show this help message", | |
| run: func(_ *flag.FlagSet, stdout, _ io.Writer) int { printUsage(stdout, cmds); return 0 }, | |
| }, | |
| { | |
| name: "version", | |
| summary: "Print version information", | |
| run: func(_ *flag.FlagSet, stdout, _ io.Writer) int { fmt.Fprintln(stdout, versionInfo()); return 0 }, | |
| }, | |
| } | |
| return cmds | |
| } | |
| // splitPathVersion splits "path@version" into its components. | |
| // If there is no @, version is empty. | |
| func splitPathVersion(s string) (path, version string) { | |
| path, version, _ = strings.Cut(s, "@") | |
| return path, version | |
| } | |
| // handleErr writes an error message. In JSON mode, the error is written | |
| // to stdout as a JSON object so callers can parse it. In text mode, it | |
| // goes to stderr. | |
| func handleErr(stdout, stderr io.Writer, err error, jsonMode bool) { | |
| if jsonMode { | |
| aerr, ok := err.(*client.Error) | |
| if !ok { | |
| aerr = &client.Error{Code: 1, Message: err.Error()} | |
| } | |
| if errors.Is(err, context.DeadlineExceeded) { | |
| aerr.Message = "request timed out" | |
| aerr.Fixes = []string{"use the -timeout flag to increase the timeout", "reduce size of request by limiting items returned"} | |
| } | |
| writeJSON(stdout, stderr, aerr) | |
| return | |
| } | |
| if errors.Is(err, context.DeadlineExceeded) { | |
| fmt.Fprintln(stderr, "Error: request timed out; use the -timeout flag to increase it") | |
| } else { | |
| fmt.Fprintln(stderr, err) | |
| } | |
| } | |
| func writeJSON(stdout, stderr io.Writer, v any) int { | |
| enc := json.NewEncoder(stdout) | |
| enc.SetIndent("", " ") | |
| if err := enc.Encode(v); err != nil { | |
| fmt.Fprintln(stderr, err) | |
| return 1 | |
| } | |
| return 0 | |
| } |