| 1 | #!/usr/bin/env node |
| 2 | // π€ Refresh the public contributors index (web/status/contributors.json) |
| 3 | // from GitHub + git history. |
| 4 | // |
| 5 | // Humans come from the GitHub contributors API (commit counts, avatars, |
| 6 | // profile links β needs `gh` authenticated for this private repo). AI |
| 7 | // contributors come from Co-Authored-By trailers with the anthropic email β |
| 8 | // this codebase is AI-written, so the models sit in the index alongside the |
| 9 | // humans, on the same metric. |
| 10 | // |
| 11 | // node tools/contributors.mjs # print the would-be JSON (dry run) |
| 12 | // node tools/contributors.mjs --write # rewrite web/status/contributors.json |
| 13 | // |
| 14 | // The JSON is COMMITTED (like carbon.json) and republished as-is by |
| 15 | // deploy/status-publish.sh. Stats fields (commits, url, avatar) regenerate on |
| 16 | // every run; the human-owned fields are PRESERVED: |
| 17 | // bio β seeded from the GitHub profile bio on first appearance, then |
| 18 | // yours: customise it by editing your entry in a PR |
| 19 | // name β display name, same rule |
| 20 | // hidden β set true to stay out of the published index entirely |
| 21 | // Avatars are embedded as data URIs (the status pages are self-contained β |
| 22 | // no external requests), fetched small (s=96). |
| 23 | import { readFileSync, writeFileSync, existsSync } from "node:fs"; |
| 24 | import { execSync } from "node:child_process"; |
| 25 | import { join } from "node:path"; |
| 26 | |
| 27 | const write = process.argv.includes("--write"); |
| 28 | const sh = (cmd) => execSync(cmd, { encoding: "utf8", maxBuffer: 64 * 1024 * 1024 }); |
| 29 | |
| 30 | const jsonPath = join(process.cwd(), "web/status/contributors.json"); |
| 31 | const prev = existsSync(jsonPath) ? JSON.parse(readFileSync(jsonPath, "utf8")) : { contributors: [] }; |
| 32 | const prevById = new Map(prev.contributors.map((c) => [c.id, c])); |
| 33 | |
| 34 | const REPO = "elevenmessenger/messenger"; |
| 35 | const contributors = []; |
| 36 | |
| 37 | // ---- humans: GitHub contributors API ----------------------------------------- |
| 38 | const ghContribs = JSON.parse(sh(`gh api repos/${REPO}/contributors`)); |
| 39 | for (const g of ghContribs) { |
| 40 | const old = prevById.get(g.login) || {}; |
| 41 | const profile = JSON.parse(sh(`gh api users/${g.login}`)); |
| 42 | // Avatar as a data URI so the page stays self-contained (no external hosts). |
| 43 | const png = sh(`curl -sL "${g.avatar_url}&s=96" | base64`).replace(/\n/g, ""); |
| 44 | contributors.push({ |
| 45 | id: g.login, |
| 46 | kind: "human", |
| 47 | name: old.name ?? profile.name ?? g.login, |
| 48 | bio: old.bio ?? profile.bio ?? "", |
| 49 | commits: g.contributions, |
| 50 | url: g.html_url, |
| 51 | avatar: "data:image/png;base64," + png, |
| 52 | ...(old.hidden ? { hidden: true } : {}), |
| 53 | }); |
| 54 | } |
| 55 | |
| 56 | // ---- AI: Co-Authored-By trailers (anthropic email only β human co-author |
| 57 | // trailers are already counted as commit authors above) ------------------------ |
| 58 | const trailerCounts = new Map(); |
| 59 | for (const line of sh(`git log --format='%(trailers:key=Co-Authored-By,valueonly)'`).split("\n")) { |
| 60 | const m = line.match(/^(.+?) <([^>]+)>\s*$/); |
| 61 | if (!m || !m[2].endsWith("@anthropic.com")) continue; |
| 62 | const name = m[1].replace(/\s*\(1M context\)\s*/, "").trim(); |
| 63 | trailerCounts.set(name, (trailerCounts.get(name) || 0) + 1); |
| 64 | } |
| 65 | for (const [name, commits] of trailerCounts) { |
| 66 | const id = name.toLowerCase().replace(/[^a-z0-9]+/g, "-"); |
| 67 | const old = prevById.get(id) || {}; |
| 68 | contributors.push({ |
| 69 | id, |
| 70 | kind: "ai", |
| 71 | name: old.name ?? name, |
| 72 | bio: old.bio ?? "", |
| 73 | commits, |
| 74 | ...(old.hidden ? { hidden: true } : {}), |
| 75 | }); |
| 76 | } |
| 77 | |
| 78 | contributors.sort((a, b) => b.commits - a.commits); |
| 79 | const out = { |
| 80 | updated_at: new Date().toISOString(), |
| 81 | repo: REPO, |
| 82 | metric: "commits to main β humans as commit authors (GitHub), AI models as Co-Authored-By trailers (git log)", |
| 83 | contributors, |
| 84 | }; |
| 85 | |
| 86 | if (write) { |
| 87 | writeFileSync(jsonPath, JSON.stringify(out, null, 2) + "\n"); |
| 88 | console.error("wrote " + jsonPath + " β " + contributors.length + " contributors (" + |
| 89 | contributors.filter((c) => c.kind === "human").length + " human, " + |
| 90 | contributors.filter((c) => c.kind === "ai").length + " ai)"); |
| 91 | } else { |
| 92 | // Dry run: avatars elided for readability. |
| 93 | console.log(JSON.stringify({ ...out, contributors: contributors.map((c) => ({ ...c, avatar: c.avatar ? "<data-uri>" : undefined })) }, null, 2)); |
| 94 | console.error("dry run β pass --write to rewrite " + jsonPath); |
| 95 | } |
| 96 | |