#!/usr/bin/env node // 🤖 Refresh the public contributors index (web/status/contributors.json) // from GitHub + git history. // // Humans come from the GitHub contributors API (commit counts, avatars, // profile links — needs `gh` authenticated for this private repo). AI // contributors come from Co-Authored-By trailers with the anthropic email — // this codebase is AI-written, so the models sit in the index alongside the // humans, on the same metric. // // node tools/contributors.mjs # print the would-be JSON (dry run) // node tools/contributors.mjs --write # rewrite web/status/contributors.json // // The JSON is COMMITTED (like carbon.json) and republished as-is by // deploy/status-publish.sh. Stats fields (commits, url, avatar) regenerate on // every run; the human-owned fields are PRESERVED: // bio — seeded from the GitHub profile bio on first appearance, then // yours: customise it by editing your entry in a PR // name — display name, same rule // hidden — set true to stay out of the published index entirely // Avatars are embedded as data URIs (the status pages are self-contained — // no external requests), fetched small (s=96). import { readFileSync, writeFileSync, existsSync } from "node:fs"; import { execSync } from "node:child_process"; import { join } from "node:path"; const write = process.argv.includes("--write"); const sh = (cmd) => execSync(cmd, { encoding: "utf8", maxBuffer: 64 * 1024 * 1024 }); const jsonPath = join(process.cwd(), "web/status/contributors.json"); const prev = existsSync(jsonPath) ? JSON.parse(readFileSync(jsonPath, "utf8")) : { contributors: [] }; const prevById = new Map(prev.contributors.map((c) => [c.id, c])); const REPO = "elevenmessenger/messenger"; const contributors = []; // ---- humans: GitHub contributors API ----------------------------------------- const ghContribs = JSON.parse(sh(`gh api repos/${REPO}/contributors`)); for (const g of ghContribs) { const old = prevById.get(g.login) || {}; const profile = JSON.parse(sh(`gh api users/${g.login}`)); // Avatar as a data URI so the page stays self-contained (no external hosts). const png = sh(`curl -sL "${g.avatar_url}&s=96" | base64`).replace(/\n/g, ""); contributors.push({ id: g.login, kind: "human", name: old.name ?? profile.name ?? g.login, bio: old.bio ?? profile.bio ?? "", commits: g.contributions, url: g.html_url, avatar: "data:image/png;base64," + png, ...(old.hidden ? { hidden: true } : {}), }); } // ---- AI: Co-Authored-By trailers (anthropic email only — human co-author // trailers are already counted as commit authors above) ------------------------ const trailerCounts = new Map(); for (const line of sh(`git log --format='%(trailers:key=Co-Authored-By,valueonly)'`).split("\n")) { const m = line.match(/^(.+?) <([^>]+)>\s*$/); if (!m || !m[2].endsWith("@anthropic.com")) continue; const name = m[1].replace(/\s*\(1M context\)\s*/, "").trim(); trailerCounts.set(name, (trailerCounts.get(name) || 0) + 1); } for (const [name, commits] of trailerCounts) { const id = name.toLowerCase().replace(/[^a-z0-9]+/g, "-"); const old = prevById.get(id) || {}; contributors.push({ id, kind: "ai", name: old.name ?? name, bio: old.bio ?? "", commits, ...(old.hidden ? { hidden: true } : {}), }); } contributors.sort((a, b) => b.commits - a.commits); const out = { updated_at: new Date().toISOString(), repo: REPO, metric: "commits to main — humans as commit authors (GitHub), AI models as Co-Authored-By trailers (git log)", contributors, }; if (write) { writeFileSync(jsonPath, JSON.stringify(out, null, 2) + "\n"); console.error("wrote " + jsonPath + " — " + contributors.length + " contributors (" + contributors.filter((c) => c.kind === "human").length + " human, " + contributors.filter((c) => c.kind === "ai").length + " ai)"); } else { // Dry run: avatars elided for readability. console.log(JSON.stringify({ ...out, contributors: contributors.map((c) => ({ ...c, avatar: c.avatar ? "" : undefined })) }, null, 2)); console.error("dry run — pass --write to rewrite " + jsonPath); }