package main import ( "flag" "fmt" "go/doc" "go/parser" "go/token" "os" "os/exec" "path/filepath" "slices" "strings" ) func cmdGendocs(args []string) { fs := flag.NewFlagSet("atlas9 gendocs", flag.ExitOnError) format := fs.String("format", "markdown", "output format (markdown)") fs.Parse(args) root, err := repoRoot() if err != nil { fmt.Fprintf(os.Stderr, "atlas9 gendocs: finding repo root: %v\n", err) os.Exit(1) } pkgs, err := discoverPackages(root) if err != nil { fmt.Fprintf(os.Stderr, "atlas9 gendocs: discovering packages: %v\n", err) os.Exit(1) } switch *format { case "markdown": printPackageMarkdown(pkgs) default: fmt.Fprintf(os.Stderr, "atlas9 gendocs: unknown format %q\n", *format) os.Exit(1) } } type docPkg struct { Name string ImportPath string Dir string Synopsis string } // skipDirs are top-level directories excluded from package discovery. var skipDirs = []string{"apps", "cmd", "vendor", ".git"} func discoverPackages(root string) ([]docPkg, error) { var pkgs []docPkg 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 } top := strings.SplitN(rel, string(filepath.Separator), 2)[0] if slices.Contains(skipDirs, top) { return filepath.SkipDir } 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 := parsePackageSynopsis(path) if err != nil { fmt.Fprintf(os.Stderr, "warning: %s: %v\n", rel, err) return nil } pkgs = append(pkgs, docPkg{ Name: name, ImportPath: importPath, Dir: rel, Synopsis: synopsis, }) return nil }) return pkgs, err } 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) } 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 parsePackageSynopsis(dir string) (name, synopsis string, err error) { fset := token.NewFileSet() pkgs, err := parser.ParseDir(fset, dir, nil, parser.ParseComments) if err != nil { return "", "", err } for pkgName, astPkg := range pkgs { if strings.HasSuffix(pkgName, "_test") { continue } d := doc.New(astPkg, "", doc.AllDecls) syn := doc.Synopsis(d.Doc) 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 printPackageMarkdown(pkgs []docPkg) { 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 }