package main import ( "encoding/json" "flag" "fmt" "os" "os/exec" "path/filepath" "strings" "sync" "time" ) type Tool struct { Name string `json:"name"` Package string `json:"package"` Version string `json:"version"` Date string `json:"date"` } type ToolsFile struct { Tools []Tool `json:"tools"` } type ModuleInfo struct { Version string `json:"Version"` Time string `json:"Time"` } func main() { outputPath := flag.String("o", "tools.json", "output file path") parallel := flag.Int("j", 8, "number of parallel lookups") flag.Parse() // Read existing tools.json to get the package list data, err := os.ReadFile(*outputPath) if err != nil { fmt.Fprintf(os.Stderr, "error reading %s: %v\n", *outputPath, err) os.Exit(1) } var tf ToolsFile if err := json.Unmarshal(data, &tf); err != nil { fmt.Fprintf(os.Stderr, "error parsing %s: %v\n", *outputPath, err) os.Exit(1) } fmt.Printf("Updating %d tools...\n\n", len(tf.Tools)) var wg sync.WaitGroup sem := make(chan struct{}, *parallel) results := make(chan result, len(tf.Tools)) for i, t := range tf.Tools { wg.Add(1) go func(idx int, tool Tool) { defer wg.Done() sem <- struct{}{} defer func() { <-sem }() updated, err := fetchLatestVersion(tool) results <- result{idx: idx, tool: tool, updated: updated, err: err} }(i, t) } go func() { wg.Wait() close(results) }() updatedTools := make([]Tool, len(tf.Tools)) var succeeded, failed, changed int for r := range results { if r.err != nil { fmt.Printf("✗ %s: %v\n", r.tool.Name, r.err) updatedTools[r.idx] = r.tool // keep old version failed++ } else { updatedTools[r.idx] = r.updated if r.tool.Version != r.updated.Version { fmt.Printf("↑ %s: %s -> %s\n", r.tool.Name, r.tool.Version, r.updated.Version) changed++ } else { fmt.Printf("✓ %s: %s (unchanged)\n", r.tool.Name, r.tool.Version) } succeeded++ } } tf.Tools = updatedTools // Write updated tools.json output, err := json.MarshalIndent(tf, "", " ") if err != nil { fmt.Fprintf(os.Stderr, "error marshaling JSON: %v\n", err) os.Exit(1) } output = append(output, '\n') absPath, _ := filepath.Abs(*outputPath) if err := os.WriteFile(*outputPath, output, 0644); err != nil { fmt.Fprintf(os.Stderr, "error writing %s: %v\n", *outputPath, err) os.Exit(1) } fmt.Printf("\nDone: %d succeeded, %d failed, %d updated\n", succeeded, failed, changed) fmt.Printf("Written to %s\n", absPath) if failed > 0 { os.Exit(1) } } // getModulePath extracts the module path from a package path // e.g., "golang.org/x/tools/cmd/goimports" -> "golang.org/x/tools" func getModulePath(pkg string) string { // Handle special cases where cmd is in a subpath if strings.Contains(pkg, "/cmd/") { parts := strings.Split(pkg, "/cmd/") return parts[0] } // For packages like "github.com/cweill/gotests/gotests" // we need the parent module if strings.Contains(pkg, "/gojson") && strings.Contains(pkg, "ChimeraCoder") { return "github.com/ChimeraCoder/gojson" } if strings.HasSuffix(pkg, "/gotests") { return strings.TrimSuffix(pkg, "/gotests") } return pkg } func fetchLatestVersion(tool Tool) (Tool, error) { modPath := getModulePath(tool.Package) cmd := exec.Command("go", "list", "-m", "-json", modPath+"@latest") output, err := cmd.Output() if err != nil { return Tool{}, fmt.Errorf("go list failed: %w", err) } var info ModuleInfo if err := json.Unmarshal(output, &info); err != nil { return Tool{}, fmt.Errorf("parse error: %w", err) } // Parse date date := "" if info.Time != "" { if t, err := time.Parse(time.RFC3339, info.Time); err == nil { date = t.Format("2006-01-02") } } return Tool{ Name: tool.Name, Package: tool.Package, Version: info.Version, Date: date, }, nil } type result struct { idx int tool Tool updated Tool err error }