blob: 4fca2f64386a10461308f9d70d0a44cb1e677ae7 [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.
package main
import (
"fmt"
"io"
"strings"
"golang.org/x/pkgsite/cmd/internal/pkgsite-cli/client"
)
// Result types combine base entity data with optional supplementary data.
// These are used for both text formatting and JSON output.
type packageResult struct {
Package *client.Package `json:"package"`
Symbols *client.PaginatedResponse[client.Symbol] `json:"symbols,omitempty"`
ImportedBy *client.PackageImportedBy `json:"importedBy,omitempty"`
}
type moduleResult struct {
Module *client.Module `json:"module"`
Versions *client.PaginatedResponse[client.VersionResponse] `json:"versions,omitempty"`
Vulns *client.PaginatedResponse[client.Vulnerability] `json:"vulns,omitempty"`
Packages *client.PaginatedResponse[client.ModulePackageResponse] `json:"packages,omitempty"`
}
func formatPackage(w io.Writer, r packageResult) {
p := r.Package
if p.IsStandardLibrary {
fmt.Fprintf(w, "%s (standard library)\n", p.Path)
} else {
fmt.Fprintf(w, "%s\n", p.Path)
}
fmt.Fprintf(w, " Name: %s\n", p.Name)
fmt.Fprintf(w, " Module: %s\n", p.ModulePath)
version := p.Version
if p.IsLatest {
version += " (latest)"
}
fmt.Fprintf(w, " Version: %s\n", version)
if p.Synopsis != "" {
fmt.Fprintf(w, " Synopsis: %s\n", p.Synopsis)
}
if p.GOOS != "" && p.GOARCH != "" {
fmt.Fprintf(w, " Context: %s/%s\n", p.GOOS, p.GOARCH)
}
if p.Docs != "" {
fmt.Fprintln(w)
fmt.Fprintln(w, p.Docs)
}
if len(p.Imports) > 0 {
fmt.Fprintln(w)
fmt.Fprintln(w, "Imports:")
for _, imp := range p.Imports {
fmt.Fprintf(w, " %s\n", imp)
}
}
if len(p.Licenses) > 0 {
fmt.Fprintln(w)
formatLicenses(w, p.Licenses)
}
if r.Symbols != nil && len(r.Symbols.Items) > 0 {
fmt.Fprintln(w)
fmt.Fprintln(w, "Symbols:")
for _, s := range r.Symbols.Items {
if s.Synopsis != "" {
fmt.Fprintf(w, " %s\n", s.Synopsis)
} else {
fmt.Fprintf(w, " %s %s\n", s.Kind, s.Name)
}
}
if t := r.Symbols.NextPageToken; t != "" {
fmt.Fprintf(w, " next page token: %s\n", t)
}
formatPaginationHint(w, len(r.Symbols.Items), r.Symbols.Total, "symbol-token")
}
if r.ImportedBy != nil {
ib := r.ImportedBy.ImportedBy
if len(ib.Items) > 0 {
fmt.Fprintln(w)
fmt.Fprintln(w, "Imported by:")
for _, pkg := range ib.Items {
fmt.Fprintf(w, " %s\n", pkg)
}
if t := ib.NextPageToken; t != "" {
fmt.Fprintf(w, " next page token: %s\n", t)
}
formatPaginationHint(w, len(ib.Items), ib.Total, "imported-by-token")
}
}
}
func formatModule(w io.Writer, r moduleResult) {
m := r.Module
fmt.Fprintf(w, "%s\n", m.Path)
version := m.Version
if m.IsLatest {
version += " (latest)"
}
fmt.Fprintf(w, " Version: %s\n", version)
if m.RepoURL != "" {
fmt.Fprintf(w, " Repository: %s\n", m.RepoURL)
}
fmt.Fprintf(w, " Has go.mod: %s\n", yesNo(m.HasGoMod))
fmt.Fprintf(w, " Redistributable: %s\n", yesNo(m.IsRedistributable))
if m.Readme != nil {
fmt.Fprintln(w)
fmt.Fprintf(w, "README (%s):\n", m.Readme.Filepath)
fmt.Fprintln(w, m.Readme.Contents)
}
if len(m.Licenses) > 0 {
fmt.Fprintln(w)
formatLicenses(w, m.Licenses)
}
if r.Versions != nil && len(r.Versions.Items) > 0 {
fmt.Fprintln(w)
fmt.Fprintln(w, "Versions:")
for _, v := range r.Versions.Items {
fmt.Fprintf(w, " %s\n", v.Version)
}
if t := r.Versions.NextPageToken; t != "" {
fmt.Fprintf(w, " next page token: %s\n", t)
}
formatPaginationHint(w, len(r.Versions.Items), r.Versions.Total, "versions-token")
}
if r.Vulns != nil && len(r.Vulns.Items) > 0 {
fmt.Fprintln(w)
fmt.Fprintln(w, "Vulnerabilities:")
for _, v := range r.Vulns.Items {
fmt.Fprintf(w, " %s\n", v.ID)
if v.Summary != "" {
fmt.Fprintf(w, " %s\n", v.Summary)
} else if v.Details != "" {
fmt.Fprintf(w, " %s\n", firstLine(v.Details))
}
if v.FixedVersion != "" {
fmt.Fprintf(w, " Fixed in: %s\n", v.FixedVersion)
}
}
if t := r.Vulns.NextPageToken; t != "" {
fmt.Fprintf(w, " next page token: %s\n", t)
}
formatPaginationHint(w, len(r.Vulns.Items), r.Vulns.Total, "vulns-token")
}
if r.Packages != nil && len(r.Packages.Items) > 0 {
fmt.Fprintln(w)
fmt.Fprintln(w, "Packages:")
for _, p := range r.Packages.Items {
if p.Synopsis != "" {
fmt.Fprintf(w, " %-40s %s\n", p.Path, p.Synopsis)
} else {
fmt.Fprintf(w, " %s\n", p.Path)
}
}
if t := r.Packages.NextPageToken; t != "" {
fmt.Fprintf(w, " next page token: %s\n", t)
}
formatPaginationHint(w, len(r.Packages.Items), r.Packages.Total, "packages-token")
}
}
func formatSearch(w io.Writer, r *client.PaginatedResponse[client.SearchResult]) {
if len(r.Items) == 0 {
fmt.Fprintln(w, "No results.")
return
}
for _, sr := range r.Items {
formatSearchResult(w, sr)
}
// Don't display the page token: search doesn't use one.
formatPaginationHint(w, len(r.Items), r.Total, "")
}
func formatSearchResult(w io.Writer, sr client.SearchResult) {
fmt.Fprintf(w, "%s\n", sr.PackagePath)
fmt.Fprintf(w, " Module: %s@%s\n", sr.ModulePath, sr.Version)
if sr.Synopsis != "" {
fmt.Fprintf(w, " Synopsis: %s\n", sr.Synopsis)
}
fmt.Fprintln(w)
}
func formatLicenses(w io.Writer, licenses []client.License) {
fmt.Fprintln(w, "Licenses:")
for _, l := range licenses {
fmt.Fprintf(w, " %s (%s)\n", strings.Join(l.Types, ", "), l.FilePath)
}
}
func formatPaginationHint(w io.Writer, shown, total int, flagName string) {
if total > shown {
fmt.Fprintf(w, " Showing %d of %d.", shown, total)
if flagName != "" {
fmt.Fprintf(w, " Use -%s NEXT_PAGE_TOKEN and/or -limit N to see more.", flagName)
}
fmt.Fprintln(w)
}
}
func yesNo(b bool) string {
if b {
return "yes"
}
return "no"
}
func firstLine(s string) string {
if before, _, ok := strings.Cut(s, "\n"); ok {
return before
}
return s
}