mirror of
https://github.com/tiennm99/awesome-coding-agents.git
synced 2026-05-13 19:52:18 +00:00
237d7acd6a
Go updater that fetches AI agent coding tool repo stats via GitHub GraphQL (batched, one query), sorts by star count, appends a daily snapshot to data/history.jsonl, and regenerates README.md from templates/readme.tmpl. Daily workflow at .github/workflows/update.yml refreshes rankings and commits changes. Seed list in data/agents.yml covers 19 tracked repos.
132 lines
3.0 KiB
Go
132 lines
3.0 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type Stat struct {
|
|
Owner string
|
|
Repo string
|
|
Category string
|
|
Notes string
|
|
Description string
|
|
Stars int
|
|
Language string
|
|
PushedAt time.Time
|
|
URL string
|
|
NameWithOwner string
|
|
}
|
|
|
|
type repoNode struct {
|
|
StargazerCount int `json:"stargazerCount"`
|
|
Description string `json:"description"`
|
|
PrimaryLanguage *struct {
|
|
Name string `json:"name"`
|
|
} `json:"primaryLanguage"`
|
|
PushedAt time.Time `json:"pushedAt"`
|
|
URL string `json:"url"`
|
|
NameWithOwner string `json:"nameWithOwner"`
|
|
}
|
|
|
|
type graphQLResponse struct {
|
|
Data map[string]*repoNode `json:"data"`
|
|
Errors []struct {
|
|
Message string `json:"message"`
|
|
Path []any `json:"path"`
|
|
} `json:"errors"`
|
|
}
|
|
|
|
const repoFields = `
|
|
stargazerCount
|
|
description
|
|
primaryLanguage { name }
|
|
pushedAt
|
|
url
|
|
nameWithOwner
|
|
`
|
|
|
|
func fetchStats(token string, agents []Agent) ([]Stat, error) {
|
|
var b strings.Builder
|
|
b.WriteString("query {\n")
|
|
for i, a := range agents {
|
|
fmt.Fprintf(&b, " r%d: repository(owner: %q, name: %q) {%s}\n", i, a.Owner, a.Repo, repoFields)
|
|
}
|
|
b.WriteString("}\n")
|
|
|
|
body, err := json.Marshal(map[string]string{"query": b.String()})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
req, err := http.NewRequest("POST", "https://api.github.com/graphql", bytes.NewReader(body))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req.Header.Set("Authorization", "Bearer "+token)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("User-Agent", "awesome-coding-agents-updater")
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
raw, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("graphql HTTP %d: %s", resp.StatusCode, raw)
|
|
}
|
|
|
|
var out graphQLResponse
|
|
if err := json.Unmarshal(raw, &out); err != nil {
|
|
return nil, fmt.Errorf("decode response: %w (body=%s)", err, raw)
|
|
}
|
|
for _, e := range out.Errors {
|
|
// repos that 404 or are renamed land here; continue with partial data
|
|
fmt.Fprintf(os.Stderr, "graphql warn: %s (path=%v)\n", e.Message, e.Path)
|
|
}
|
|
|
|
stats := make([]Stat, 0, len(agents))
|
|
for i, a := range agents {
|
|
node := out.Data[fmt.Sprintf("r%d", i)]
|
|
if node == nil {
|
|
fmt.Fprintf(os.Stderr, "skip %s/%s — no data\n", a.Owner, a.Repo)
|
|
continue
|
|
}
|
|
lang := ""
|
|
if node.PrimaryLanguage != nil {
|
|
lang = node.PrimaryLanguage.Name
|
|
}
|
|
stats = append(stats, Stat{
|
|
Owner: a.Owner,
|
|
Repo: a.Repo,
|
|
Category: a.Category,
|
|
Notes: a.Notes,
|
|
Description: node.Description,
|
|
Stars: node.StargazerCount,
|
|
Language: lang,
|
|
PushedAt: node.PushedAt,
|
|
URL: node.URL,
|
|
NameWithOwner: node.NameWithOwner,
|
|
})
|
|
}
|
|
return stats, nil
|
|
}
|
|
|
|
func sortByStars(stats []Stat) {
|
|
sort.SliceStable(stats, func(i, j int) bool {
|
|
return stats[i].Stars > stats[j].Stars
|
|
})
|
|
}
|