Files
tiennm99 237d7acd6a feat: initial scaffold of awesome-coding-agents tracker
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.
2026-05-13 14:43:35 +07:00

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
})
}