mirror of
https://github.com/tiennm99/awesome-ai-dev-tools.git
synced 2026-10-11 03:13:05 +00:00
feat: let agents.yml override the GitHub repo description
This commit is contained in:
1 parent
14a013a151
commit
ebe7bce900
7 files changed
+88
-3
No files matched your search
@@ -13,6 +13,11 @@ type Agent struct {
|
||||
Tags []string `yaml:"tags"`
|
||||
Notes string `yaml:"notes,omitempty"`
|
||||
|
||||
// Description is a curated one-liner that replaces the repo's own GitHub
|
||||
// description in the README and dashboard. Many upstream descriptions are
|
||||
// empty, vague, or marketing copy that says nothing about what the tool is.
|
||||
Description string `yaml:"description,omitempty"`
|
||||
|
||||
// Category is the retired single-select field that tags replaced. It is
|
||||
// still parsed so a stale entry fails validation with a message naming
|
||||
// the replacement, rather than being silently ignored.
|
||||
|
||||
+12
-1
@@ -8,12 +8,13 @@ Edit [`data/agents.yml`](../data/agents.yml) and add an entry:
|
||||
agents:
|
||||
- owner: github-username-or-org
|
||||
repo: repository-name
|
||||
description: Terminal coding agent that edits files and runs commands in your repo
|
||||
tags: [terminal, byo-model, interactive, community]
|
||||
```
|
||||
|
||||
Required fields: `owner`, `repo`, `tags`
|
||||
|
||||
Optional fields: `notes` (for clarifications or caveats)
|
||||
Optional fields: `description` (curated one-liner), `notes` (for clarifications or caveats)
|
||||
|
||||
## Field Reference
|
||||
|
||||
@@ -22,8 +23,18 @@ Optional fields: `notes` (for clarifications or caveats)
|
||||
| `owner` | string | Yes | GitHub user or organization that owns the repo |
|
||||
| `repo` | string | Yes | Repository name on GitHub |
|
||||
| `tags` | list | Yes | Tags from the vocabulary below; at least one surface tag, at most one origin tag |
|
||||
| `description` | string | No | Curated one-line description shown instead of the repo's GitHub description; one line, no `\|`, at most 140 characters |
|
||||
| `notes` | string | No | Additional context or disclaimers |
|
||||
|
||||
### Writing a description
|
||||
|
||||
Say what the tool is (terminal agent, VS Code extension, editor, desktop app,
|
||||
self-hosted server) and the one trait that sets it apart, in one plain sentence
|
||||
with no trailing period. Base it on the repo's own README, like tags. Leave out
|
||||
marketing adjectives, emoji, star counts, and model version names, which go
|
||||
stale within months. Without a `description`, the README shows whatever the
|
||||
repo's GitHub description says, which is often empty or vague.
|
||||
|
||||
## Scope
|
||||
|
||||
This list ranks **developer tools built around AI** — things a developer uses to
|
||||
|
||||
@@ -140,13 +140,18 @@ func fetchStats(token string, agents []Agent) ([]Stat, error) {
|
||||
fmt.Printf("::warning::repo %s has no push in %d days — review against the maintenance criterion\n", canonicalKey, days)
|
||||
}
|
||||
|
||||
desc := node.Description
|
||||
if a.Description != "" {
|
||||
desc = a.Description
|
||||
}
|
||||
|
||||
stats = append(stats, Stat{
|
||||
CanonicalKey: canonicalKey,
|
||||
Owner: a.Owner,
|
||||
Repo: a.Repo,
|
||||
Tags: a.Tags,
|
||||
Notes: a.Notes,
|
||||
Description: node.Description,
|
||||
Description: desc,
|
||||
Stars: node.StargazerCount,
|
||||
Language: lang,
|
||||
PushedAt: node.PushedAt,
|
||||
|
||||
@@ -51,6 +51,8 @@ func TestFetchStats_ChunkingHappyPath(t *testing.T) {
|
||||
for i := range agents {
|
||||
agents[i] = Agent{Owner: "org", Repo: fmt.Sprintf("repo%02d", i), Tags: []string{"terminal", "community"}}
|
||||
}
|
||||
// A curated description in agents.yml replaces the GitHub one.
|
||||
agents[3].Description = "curated"
|
||||
|
||||
var requestCount int
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -114,6 +116,18 @@ func TestFetchStats_ChunkingHappyPath(t *testing.T) {
|
||||
if !found {
|
||||
t.Error("expected org/repo52 (second chunk) in results")
|
||||
}
|
||||
for _, s := range stats {
|
||||
switch s.NameWithOwner {
|
||||
case "org/repo03":
|
||||
if s.Description != "curated" {
|
||||
t.Errorf("org/repo03: expected curated description, got %q", s.Description)
|
||||
}
|
||||
case "org/repo04":
|
||||
if s.Description != "desc 4" {
|
||||
t.Errorf("org/repo04: expected GitHub description, got %q", s.Description)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFetchStats_MissingNodeNamesTheRepo(t *testing.T) {
|
||||
|
||||
@@ -45,6 +45,7 @@ Add an agent to [`data/agents.yml`](data/agents.yml):
|
||||
agents:
|
||||
- owner: github-username-or-org
|
||||
repo: repository-name
|
||||
description: Terminal coding agent that edits files and runs commands in your repo
|
||||
tags: [terminal, byo-model, interactive, community]
|
||||
```
|
||||
|
||||
@@ -62,7 +63,7 @@ Apply a tag only when the repo's own README, docs, or topics back it up. See [`d
|
||||
|
||||
Open a PR. The next daily run picks it up automatically.
|
||||
|
||||
PRs are validated automatically by CI (`go run . -check`): owner/repo must be non-empty and look like a real GitHub slug, every tag must come from the vocabulary above with at least one surface tag and at most one origin tag, and duplicates (case-insensitive) are rejected.
|
||||
PRs are validated automatically by CI (`go run . -check`): owner/repo must be non-empty and look like a real GitHub slug, every tag must come from the vocabulary above with at least one surface tag and at most one origin tag, an optional `description` must be one line of at most 140 characters without `|`, and duplicates (case-insensitive) are rejected.
|
||||
|
||||
## License
|
||||
|
||||
|
||||
+23
@@ -102,6 +102,7 @@ func validateAgents(agents []Agent) []string {
|
||||
violations = append(violations, fmt.Sprintf("%s: category %q is no longer a field — replace it with tags, e.g. tags: [terminal, byo-model, interactive, community]", ref, a.Category))
|
||||
}
|
||||
violations = append(violations, validateTags(ref, a.Tags)...)
|
||||
violations = append(violations, validateDescription(ref, a.Description)...)
|
||||
|
||||
key := strings.ToLower(a.Owner + "/" + a.Repo)
|
||||
if first, dup := seen[key]; dup {
|
||||
@@ -166,3 +167,25 @@ func validateTags(ref string, tags []string) []string {
|
||||
|
||||
return violations
|
||||
}
|
||||
|
||||
// maxDescriptionLen keeps a curated description to one readable table line.
|
||||
const maxDescriptionLen = 140
|
||||
|
||||
// validateDescription checks the optional curated description: one line,
|
||||
// bounded length, and no pipe, which would split the README table cell.
|
||||
func validateDescription(ref, desc string) []string {
|
||||
var violations []string
|
||||
if desc == "" {
|
||||
return nil
|
||||
}
|
||||
if strings.TrimSpace(desc) != desc {
|
||||
violations = append(violations, fmt.Sprintf("%s: description has leading or trailing whitespace", ref))
|
||||
}
|
||||
if strings.ContainsAny(desc, "|\n\r") {
|
||||
violations = append(violations, fmt.Sprintf("%s: description must be one line without '|'", ref))
|
||||
}
|
||||
if n := len([]rune(desc)); n > maxDescriptionLen {
|
||||
violations = append(violations, fmt.Sprintf("%s: description is %d characters, max %d", ref, n, maxDescriptionLen))
|
||||
}
|
||||
return violations
|
||||
}
|
||||
@@ -204,3 +204,29 @@ func anyContains(list []string, substr string) bool {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func TestValidateAgents_Description(t *testing.T) {
|
||||
tests := []struct {
|
||||
name, desc, want string
|
||||
}{
|
||||
{"valid", "Terminal coding agent with multi-provider support", ""},
|
||||
{"pipe", "CLI | agent", "without '|'"},
|
||||
{"newline", "line one\nline two", "without '|'"},
|
||||
{"padded", " padded", "leading or trailing whitespace"},
|
||||
{"too long", strings.Repeat("x", maxDescriptionLen+1), "max 140"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
violations := validateAgents([]Agent{{Owner: "o", Repo: "r", Tags: []string{"terminal"}, Description: tt.desc}})
|
||||
if tt.want == "" {
|
||||
if len(violations) != 0 {
|
||||
t.Errorf("expected no violations, got %v", violations)
|
||||
}
|
||||
return
|
||||
}
|
||||
if !anyContains(violations, tt.want) {
|
||||
t.Errorf("expected violation containing %q, got %v", tt.want, violations)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user