| // 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 |
| } |