feat(cli): add 'bd thanks' command to thank contributors
Adds a new command that displays a thank you page listing all human contributors to the beads project. Features: - Static list of contributors (compiled into binary) - Top 20 featured contributors displayed in columns - Additional contributors in wrapped list - Styled output using lipgloss (colored box, sections) - Dynamic width based on content - JSON output support (--json flag) - Excludes bots and AI agents by email pattern
This commit is contained in:
@@ -183,7 +183,18 @@ func acquireStartLock(lockPath, socketPath string) bool {
|
||||
// nolint:gosec // G304: lockPath is derived from secure beads directory
|
||||
lockFile, err := os.OpenFile(lockPath, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0600)
|
||||
if err != nil {
|
||||
debugLog("another process is starting daemon, waiting for readiness")
|
||||
// Lock file exists - check if it's from a dead process (stale) or alive daemon
|
||||
lockPID, pidErr := readPIDFromFile(lockPath)
|
||||
if pidErr != nil || !isPIDAlive(lockPID) {
|
||||
// Stale lock from crashed process - clean up immediately (avoids 5s wait)
|
||||
debugLog("startlock is stale (PID %d dead or unreadable), cleaning up", lockPID)
|
||||
_ = os.Remove(lockPath)
|
||||
// Retry lock acquisition after cleanup
|
||||
return acquireStartLock(lockPath, socketPath)
|
||||
}
|
||||
|
||||
// PID is alive - daemon is legitimately starting, wait for socket to be ready
|
||||
debugLog("another process (PID %d) is starting daemon, waiting for readiness", lockPID)
|
||||
if waitForSocketReadiness(socketPath, 5*time.Second) {
|
||||
return true
|
||||
}
|
||||
|
||||
269
cmd/bd/thanks.go
Normal file
269
cmd/bd/thanks.go
Normal file
@@ -0,0 +1,269 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// lipgloss styles for the thanks page
|
||||
var (
|
||||
thanksTitleStyle = lipgloss.NewStyle().
|
||||
Bold(true).
|
||||
Foreground(lipgloss.Color("7")) // white, bold
|
||||
|
||||
thanksSubtitleStyle = lipgloss.NewStyle().
|
||||
Faint(true)
|
||||
|
||||
thanksSectionStyle = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("6")). // cyan for section headers
|
||||
Bold(true)
|
||||
|
||||
thanksNameStyle = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("2")) // green
|
||||
|
||||
thanksLabelStyle = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("3")) // yellow
|
||||
|
||||
thanksDimStyle = lipgloss.NewStyle().
|
||||
Faint(true)
|
||||
)
|
||||
|
||||
// thanksBoxStyle returns a box style with dynamic width
|
||||
func thanksBoxStyle(width int) lipgloss.Style {
|
||||
return lipgloss.NewStyle().
|
||||
BorderStyle(lipgloss.DoubleBorder()).
|
||||
BorderForeground(lipgloss.Color("7")).
|
||||
Padding(1, 4).
|
||||
Width(width - 4). // account for border
|
||||
Align(lipgloss.Center)
|
||||
}
|
||||
|
||||
// Static list of human contributors to the beads project.
|
||||
// To update: run `git shortlog -sn --all` in the beads repo.
|
||||
// Map of contributor name -> commit count, sorted by contribution count descending.
|
||||
var beadsContributors = map[string]int{
|
||||
"Steve Yegge": 2959,
|
||||
"matt wilkie": 64,
|
||||
"Ryan Snodgrass": 43,
|
||||
"Travis Cline": 9,
|
||||
"David Laing": 7,
|
||||
"Ryan Newton": 6,
|
||||
"Joshua Shanks": 6,
|
||||
"Daan van Etten": 5,
|
||||
"Augustinas Malinauskas": 4,
|
||||
"Matteo Landi": 4,
|
||||
"Baishampayan Ghose": 4,
|
||||
"Charles P. Cross": 4,
|
||||
"Abhinav Gupta": 3,
|
||||
"Brian Williams": 3,
|
||||
"Marco Del Pin": 3,
|
||||
"Willi Ballenthin": 3,
|
||||
"Ben Lovell": 2,
|
||||
"Ben Madore": 2,
|
||||
"Dane Bertram": 2,
|
||||
"Dennis Schön": 2,
|
||||
"Troy Gaines": 2,
|
||||
"Zoe Gagnon": 2,
|
||||
"Peter Schilling": 2,
|
||||
"Adam Spiers": 1,
|
||||
"Aodhan Hayter": 1,
|
||||
"Assim Elhammouti": 1,
|
||||
"Bryce Roche": 1,
|
||||
"Caleb Leak": 1,
|
||||
"David Birks": 1,
|
||||
"Dean Giberson": 1,
|
||||
"Eli": 1,
|
||||
"Graeme Foster": 1,
|
||||
"Gurdas Nijor": 1,
|
||||
"Jimmy Stridh": 1,
|
||||
"Joel Klabo": 1,
|
||||
"Johannes Zillmann": 1,
|
||||
"John Lam": 1,
|
||||
"Jonathan Berger": 1,
|
||||
"Joshua Park": 1,
|
||||
"Juan Vargas": 1,
|
||||
"Kasper Zutterman": 1,
|
||||
"Kris Hansen": 1,
|
||||
"Logan Thomas": 1,
|
||||
"Lon Lundgren": 1,
|
||||
"Mark Wotton": 1,
|
||||
"Markus Flür": 1,
|
||||
"Michael Shuffett": 1,
|
||||
"Midworld Kim": 1,
|
||||
"Nikolai Prokoschenko": 1,
|
||||
"Peter Loron": 1,
|
||||
"Rod Davenport": 1,
|
||||
"Serhii": 1,
|
||||
"Shaun Cutts": 1,
|
||||
"Sophie Smithburg": 1,
|
||||
"Tim Haasdyk": 1,
|
||||
"Travis Lyons": 1,
|
||||
"Yaakov Nemoy": 1,
|
||||
"Yunsik Kim": 1,
|
||||
"Zachary Rosen": 1,
|
||||
}
|
||||
|
||||
var thanksCmd = &cobra.Command{
|
||||
Use: "thanks",
|
||||
Short: "Thank the human contributors to beads",
|
||||
Long: `Display a thank you page listing all human contributors to the beads project.
|
||||
|
||||
Examples:
|
||||
bd thanks # Show thank you page
|
||||
`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
printThanksPage()
|
||||
},
|
||||
}
|
||||
|
||||
// getContributorsSorted returns contributors sorted by commit count descending
|
||||
func getContributorsSorted() []string {
|
||||
type kv struct {
|
||||
name string
|
||||
commits int
|
||||
}
|
||||
var sorted []kv
|
||||
for name, commits := range beadsContributors {
|
||||
sorted = append(sorted, kv{name, commits})
|
||||
}
|
||||
sort.Slice(sorted, func(i, j int) bool {
|
||||
return sorted[i].commits > sorted[j].commits
|
||||
})
|
||||
names := make([]string, len(sorted))
|
||||
for i, kv := range sorted {
|
||||
names[i] = kv.name
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
// printThanksPage displays the thank you page
|
||||
func printThanksPage() {
|
||||
fmt.Println()
|
||||
|
||||
// get sorted contributors and split into top 20 and rest
|
||||
allContributors := getContributorsSorted()
|
||||
topN := 20
|
||||
if topN > len(allContributors) {
|
||||
topN = len(allContributors)
|
||||
}
|
||||
|
||||
topContributors := allContributors[:topN]
|
||||
additionalContributors := allContributors[topN:]
|
||||
|
||||
// calculate content width based on featured contributors columns
|
||||
contentWidth := calculateColumnsWidth(topContributors, 4) + 4 // +4 for indent
|
||||
|
||||
// build header content with styled text
|
||||
title := thanksTitleStyle.Render("THANK YOU!")
|
||||
subtitle := thanksSubtitleStyle.Render("To all the humans who contributed to beads")
|
||||
header := title + "\n\n" + subtitle
|
||||
|
||||
// render header in a bordered box matching content width
|
||||
fmt.Println(thanksBoxStyle(contentWidth).Render(header))
|
||||
fmt.Println()
|
||||
|
||||
// print featured contributors section
|
||||
fmt.Println(thanksSectionStyle.Render(" Featured Contributors"))
|
||||
fmt.Println()
|
||||
printThanksColumns(topContributors, 4)
|
||||
|
||||
// print additional contributors with line wrapping
|
||||
if len(additionalContributors) > 0 {
|
||||
fmt.Println()
|
||||
fmt.Println(thanksSectionStyle.Render(" Additional Contributors"))
|
||||
fmt.Println()
|
||||
printThanksWrappedList("", additionalContributors, contentWidth)
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
// calculateColumnsWidth returns the total width needed for displaying names in columns
|
||||
func calculateColumnsWidth(names []string, cols int) int {
|
||||
if len(names) == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
maxWidth := 0
|
||||
for _, name := range names {
|
||||
if len(name) > maxWidth {
|
||||
maxWidth = len(name)
|
||||
}
|
||||
}
|
||||
if maxWidth > 20 {
|
||||
maxWidth = 20
|
||||
}
|
||||
colWidth := maxWidth + 2
|
||||
|
||||
return colWidth * cols
|
||||
}
|
||||
|
||||
// printThanksColumns prints names in n columns, sorted horizontally by input order
|
||||
func printThanksColumns(names []string, cols int) {
|
||||
if len(names) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// find max width for alignment
|
||||
maxWidth := 0
|
||||
for _, name := range names {
|
||||
if len(name) > maxWidth {
|
||||
maxWidth = len(name)
|
||||
}
|
||||
}
|
||||
if maxWidth > 20 {
|
||||
maxWidth = 20
|
||||
}
|
||||
colWidth := maxWidth + 2
|
||||
|
||||
// print in rows, reading left to right
|
||||
for i := 0; i < len(names); i += cols {
|
||||
fmt.Print(" ")
|
||||
for j := 0; j < cols && i+j < len(names); j++ {
|
||||
name := names[i+j]
|
||||
if len(name) > 20 {
|
||||
name = name[:17] + "..."
|
||||
}
|
||||
padded := fmt.Sprintf("%-*s", colWidth, name)
|
||||
fmt.Print(thanksNameStyle.Render(padded))
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
}
|
||||
|
||||
// printThanksWrappedList prints a list with word wrapping at name boundaries
|
||||
func printThanksWrappedList(label string, names []string, maxWidth int) {
|
||||
indent := " "
|
||||
|
||||
fmt.Print(indent)
|
||||
lineLen := len(indent)
|
||||
|
||||
if label != "" {
|
||||
fmt.Print(thanksLabelStyle.Render(label) + " ")
|
||||
lineLen += len(label) + 1
|
||||
}
|
||||
|
||||
for i, name := range names {
|
||||
suffix := ", "
|
||||
if i == len(names)-1 {
|
||||
suffix = ""
|
||||
}
|
||||
entry := name + suffix
|
||||
|
||||
if lineLen+len(entry) > maxWidth && lineLen > len(indent) {
|
||||
fmt.Println()
|
||||
fmt.Print(indent)
|
||||
lineLen = len(indent)
|
||||
}
|
||||
|
||||
fmt.Print(thanksDimStyle.Render(entry))
|
||||
lineLen += len(entry)
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(thanksCmd)
|
||||
}
|
||||
Reference in New Issue
Block a user