| 1 | #!/usr/bin/env node |
| 2 | import { readFile, writeFile } from 'node:fs/promises'; |
| 3 | |
| 4 | const repo = 'esengine/DeepSeek-Reasonix'; |
| 5 | const api = `https://api.github.com/repos/${repo}/contributors?per_page=20&anon=1`; |
| 6 | const startMarker = '<!-- reasonix-top-contributors:start -->'; |
| 7 | const endMarker = '<!-- reasonix-top-contributors:end -->'; |
| 8 | |
| 9 | const headers = { |
| 10 | Accept: 'application/vnd.github+json', |
| 11 | 'User-Agent': 'reasonix-acknowledgments-updater', |
| 12 | }; |
| 13 | if (process.env.GITHUB_TOKEN) { |
| 14 | headers.Authorization = `Bearer ${process.env.GITHUB_TOKEN}`; |
| 15 | } |
| 16 | |
| 17 | const res = await fetch(api, { headers }); |
| 18 | if (!res.ok) { |
| 19 | throw new Error(`GitHub contributors API failed: ${res.status} ${res.statusText}`); |
| 20 | } |
| 21 | |
| 22 | const contributors = await res.json(); |
| 23 | if (!Array.isArray(contributors) || contributors.length === 0) { |
| 24 | throw new Error('GitHub contributors API returned no contributors'); |
| 25 | } |
| 26 | |
| 27 | const top = contributors.slice(0, 20).map((c, index) => ({ |
| 28 | rank: index + 1, |
| 29 | login: typeof c.login === 'string' ? c.login : '', |
| 30 | name: typeof c.name === 'string' ? c.name : '', |
| 31 | url: typeof c.html_url === 'string' ? c.html_url : '', |
| 32 | type: typeof c.type === 'string' ? c.type : '', |
| 33 | commits: Number(c.contributions) || 0, |
| 34 | })); |
| 35 | |
| 36 | await updateReadme('README.md', renderTable(top, 'en')); |
| 37 | await updateReadme('README.zh-CN.md', renderTable(top, 'zh')); |
| 38 | |
| 39 | function renderTable(rows, locale) { |
| 40 | const header = '| Contributor | Contributor | Contributor | Contributor |\n| --- | --- | --- | --- |'; |
| 41 | const cells = rows.map((row) => renderContributor(row, locale)); |
| 42 | const tableRows = []; |
| 43 | for (let i = 0; i < cells.length; i += 4) { |
| 44 | tableRows.push(`| ${cells.slice(i, i + 4).join(' | ')} |`); |
| 45 | } |
| 46 | return [ |
| 47 | startMarker, |
| 48 | header, |
| 49 | ...tableRows, |
| 50 | endMarker, |
| 51 | ].join('\n'); |
| 52 | } |
| 53 | |
| 54 | function renderContributor(row, locale) { |
| 55 | const label = row.login || row.name || `anonymous-${row.rank}`; |
| 56 | const escaped = escapeMarkdown(label); |
| 57 | if (row.url) { |
| 58 | return `[**${escaped}**](${row.url})`; |
| 59 | } |
| 60 | const anonymous = locale === 'zh' ? '(anonymous)' : ' (anonymous)'; |
| 61 | return `**${escaped}**${anonymous}`; |
| 62 | } |
| 63 | |
| 64 | async function updateReadme(path, replacement) { |
| 65 | const original = await readFile(path, 'utf8'); |
| 66 | const start = original.indexOf(startMarker); |
| 67 | const end = original.indexOf(endMarker); |
| 68 | if (start === -1 || end === -1 || end < start) { |
| 69 | throw new Error(`${path} is missing ${startMarker}/${endMarker}`); |
| 70 | } |
| 71 | const next = original.slice(0, start) + replacement + original.slice(end + endMarker.length); |
| 72 | if (next !== original) { |
| 73 | await writeFile(path, next); |
| 74 | } |
| 75 | } |
| 76 | |
| 77 | function escapeMarkdown(value) { |
| 78 | return String(value).replace(/[\\|[\]]/g, '\\$&'); |
| 79 | } |
| 80 |