blob: 704583b4d61d12ce35c2c76f2e99689f1e6a5bf3 [file] [edit]
// 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
}