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