// Command gendocs generates a markdown list of packages with pkg.go.dev links and synopses. package main import ( "flag" "fmt" "go/doc" "go/parser" "go/token" "os" "os/exec" "path/filepath" "slices" "strings" ) type pkg struct { Name string // package name (e.g. "throttle") ImportPath string // full import path (e.g. "atlas9.dev/c/core/throttle") Dir string // relative directory from repo root (e.g. "core/throttle") Synopsis string } func main() { format := flag.String("format", "markdown", "output format (markdown)") flag.Parse() root, err := repoRoot() if err != nil { fmt.Fprintf(os.Stderr, "error finding repo root: %v\n", err) os.Exit(1) } pkgs, err := discoverPackages(root) if err != nil { fmt.Fprintf(os.Stderr, "error discovering packages: %v\n", err) os.Exit(1) } switch *format { case "markdown": printMarkdown(pkgs) default: fmt.Fprintf(os.Stderr, "unknown format: %s\n", *format) os.Exit(1) } } // skipDirs are top-level directories to exclude from discovery. var skipDirs = []string{"apps", "cmd", "vendor", ".git"} func discoverPackages(root string) ([]pkg, error) { var pkgs []pkg err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error { if err != nil { return err } if !info.IsDir() { return nil } rel, _ := filepath.Rel(root, path) if rel == "." { return nil } // Skip excluded top-level directories. top := strings.SplitN(rel, string(filepath.Separator), 2)[0] if slices.Contains(skipDirs, top) { return filepath.SkipDir } // Check if this directory has .go files. entries, err := os.ReadDir(path) if err != nil { return err } hasGo := false for _, e := range entries { if !e.IsDir() && strings.HasSuffix(e.Name(), ".go") && !strings.HasSuffix(e.Name(), "_test.go") { hasGo = true break } } if !hasGo { return nil } importPath, err := resolveImportPath(root, rel) if err != nil { fmt.Fprintf(os.Stderr, "warning: %s: %v\n", rel, err) return nil } name, synopsis, err := parsePackage(path) if err != nil { fmt.Fprintf(os.Stderr, "warning: %s: %v\n", rel, err) return nil } pkgs = append(pkgs, pkg{ Name: name, ImportPath: importPath, Dir: rel, Synopsis: synopsis, }) return nil }) return pkgs, err } // resolveImportPath finds the nearest go.mod and computes the import path. func resolveImportPath(root, rel string) (string, error) { dir := filepath.Join(root, rel) for d := dir; ; d = filepath.Dir(d) { modFile := filepath.Join(d, "go.mod") if data, err := os.ReadFile(modFile); err == nil { modulePath := parseModulePath(string(data)) if modulePath == "" { return "", fmt.Errorf("no module directive in %s", modFile) } subdir, _ := filepath.Rel(d, dir) if subdir == "." { return modulePath, nil } return modulePath + "/" + filepath.ToSlash(subdir), nil } if d == root || d == filepath.Dir(d) { break } } return "", fmt.Errorf("no go.mod found for %s", rel) } // parseModulePath extracts the module path from go.mod content. func parseModulePath(content string) string { for _, line := range strings.Split(content, "\n") { line = strings.TrimSpace(line) if strings.HasPrefix(line, "module ") { return strings.TrimSpace(strings.TrimPrefix(line, "module ")) } } return "" } func parsePackage(dir string) (name, synopsis string, err error) { fset := token.NewFileSet() pkgs, err := parser.ParseDir(fset, dir, nil, parser.ParseComments) if err != nil { return "", "", err } // Pick the first non-test package. for pkgName, astPkg := range pkgs { if strings.HasSuffix(pkgName, "_test") { continue } d := doc.New(astPkg, "", doc.AllDecls) syn := doc.Synopsis(d.Doc) // Strip "Package X " prefix and any leading "- ". syn = strings.TrimPrefix(syn, "Package "+pkgName+" ") syn = strings.TrimPrefix(syn, "- ") return pkgName, syn, nil } return "", "", fmt.Errorf("no Go package found in %s", dir) } func printMarkdown(pkgs []pkg) { fmt.Println("") for _, p := range pkgs { fmt.Printf("\n", p.ImportPath, p.Dir, p.Synopsis) } fmt.Println("
%s%s
") } func repoRoot() (string, error) { out, err := exec.Command("git", "rev-parse", "--show-toplevel").Output() if err != nil { return "", err } return strings.TrimSpace(string(out)), nil }