update-contributors.js 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294
  1. #!/usr/bin/env node
  2. /**
  3. * This script fetches contributor data from GitHub and updates the README.md file
  4. * with a contributors section showing avatars and usernames.
  5. * It also updates all localized README files in the locales directory.
  6. */
  7. const https = require("https")
  8. const fs = require("fs")
  9. const path = require("path")
  10. // GitHub API URL for fetching contributors
  11. const GITHUB_API_URL = "https://api.github.com/repos/RooVetGit/Roo-Code/contributors?per_page=100"
  12. const README_PATH = path.join(__dirname, "..", "README.md")
  13. const LOCALES_DIR = path.join(__dirname, "..", "locales")
  14. // Sentinel markers for contributors section
  15. const START_MARKER = "<!-- START CONTRIBUTORS SECTION - AUTO-GENERATED, DO NOT EDIT MANUALLY -->"
  16. const END_MARKER = "<!-- END CONTRIBUTORS SECTION -->"
  17. // HTTP options for GitHub API request
  18. const options = {
  19. headers: {
  20. "User-Agent": "Roo-Code-Contributors-Script",
  21. },
  22. }
  23. // Add GitHub token for authentication if available
  24. if (process.env.GITHUB_TOKEN) {
  25. options.headers.Authorization = `token ${process.env.GITHUB_TOKEN}`
  26. console.log("Using GitHub token from environment variable")
  27. }
  28. /**
  29. * Fetches contributors data from GitHub API
  30. * @returns {Promise<Array>} Array of contributor objects
  31. */
  32. function fetchContributors() {
  33. return new Promise((resolve, reject) => {
  34. https
  35. .get(GITHUB_API_URL, options, (res) => {
  36. if (res.statusCode !== 200) {
  37. reject(new Error(`GitHub API request failed with status code: ${res.statusCode}`))
  38. return
  39. }
  40. let data = ""
  41. res.on("data", (chunk) => {
  42. data += chunk
  43. })
  44. res.on("end", () => {
  45. try {
  46. const contributors = JSON.parse(data)
  47. resolve(contributors)
  48. } catch (error) {
  49. reject(new Error(`Failed to parse GitHub API response: ${error.message}`))
  50. }
  51. })
  52. })
  53. .on("error", (error) => {
  54. reject(new Error(`GitHub API request failed: ${error.message}`))
  55. })
  56. })
  57. }
  58. /**
  59. * Reads the README.md file
  60. * @returns {Promise<string>} README content
  61. */
  62. function readReadme() {
  63. return new Promise((resolve, reject) => {
  64. fs.readFile(README_PATH, "utf8", (err, data) => {
  65. if (err) {
  66. reject(new Error(`Failed to read README.md: ${err.message}`))
  67. return
  68. }
  69. resolve(data)
  70. })
  71. })
  72. }
  73. /**
  74. * Creates HTML for the contributors section
  75. * @param {Array} contributors Array of contributor objects from GitHub API
  76. * @returns {string} HTML for contributors section
  77. */
  78. function formatContributorsSection(contributors) {
  79. // Filter out GitHub Actions bot
  80. const filteredContributors = contributors.filter((c) => !c.login.includes("[bot]") && !c.login.includes("R00-B0T"))
  81. // Start building with Markdown table format
  82. let markdown = `${START_MARKER}
  83. `
  84. // Number of columns in the table
  85. const COLUMNS = 6
  86. // Create contributor cell HTML
  87. const createCell = (contributor) => {
  88. return `<a href="${contributor.html_url}"><img src="${contributor.avatar_url}" width="100" height="100" alt="${contributor.login}"/><br /><sub><b>${contributor.login}</b></sub></a>`
  89. }
  90. if (filteredContributors.length > 0) {
  91. // Table header is the first row of contributors
  92. const headerCells = filteredContributors.slice(0, COLUMNS).map(createCell)
  93. // Fill any empty cells in header row
  94. while (headerCells.length < COLUMNS) {
  95. headerCells.push(" ")
  96. }
  97. // Add header row
  98. markdown += `|${headerCells.join("|")}|\n`
  99. // Add alignment row
  100. markdown += "|"
  101. for (let i = 0; i < COLUMNS; i++) {
  102. markdown += ":---:|"
  103. }
  104. markdown += "\n"
  105. // Add remaining contributor rows starting with the second batch
  106. for (let i = COLUMNS; i < filteredContributors.length; i += COLUMNS) {
  107. const rowContributors = filteredContributors.slice(i, i + COLUMNS)
  108. // Create cells for each contributor in this row
  109. const cells = rowContributors.map(createCell)
  110. // Fill any empty cells to maintain table structure
  111. while (cells.length < COLUMNS) {
  112. cells.push(" ")
  113. }
  114. // Add row to the table
  115. markdown += `|${cells.join("|")}|\n`
  116. }
  117. }
  118. markdown += `${END_MARKER}`
  119. return markdown
  120. }
  121. /**
  122. * Updates the README.md file with contributors section
  123. * @param {string} readmeContent Original README content
  124. * @param {string} contributorsSection HTML for contributors section
  125. * @returns {Promise<void>}
  126. */
  127. function updateReadme(readmeContent, contributorsSection) {
  128. // Find existing contributors section markers
  129. const startPos = readmeContent.indexOf(START_MARKER)
  130. const endPos = readmeContent.indexOf(END_MARKER)
  131. if (startPos === -1 || endPos === -1) {
  132. console.warn("Warning: Could not find contributors section markers in README.md")
  133. console.warn("Skipping update - please add markers to enable automatic updates.")
  134. return
  135. }
  136. // Replace existing section, trimming whitespace at section boundaries
  137. const beforeSection = readmeContent.substring(0, startPos).trimEnd()
  138. const afterSection = readmeContent.substring(endPos + END_MARKER.length).trimStart()
  139. // Ensure single newline separators between sections
  140. const updatedContent = beforeSection + "\n\n" + contributorsSection.trim() + "\n\n" + afterSection
  141. return writeReadme(updatedContent)
  142. }
  143. /**
  144. * Writes updated content to README.md
  145. * @param {string} content Updated README content
  146. * @returns {Promise<void>}
  147. */
  148. function writeReadme(content) {
  149. return new Promise((resolve, reject) => {
  150. fs.writeFile(README_PATH, content, "utf8", (err) => {
  151. if (err) {
  152. reject(new Error(`Failed to write updated README.md: ${err.message}`))
  153. return
  154. }
  155. resolve()
  156. })
  157. })
  158. }
  159. /**
  160. * Finds all localized README files in the locales directory
  161. * @returns {Promise<string[]>} Array of README file paths
  162. */
  163. function findLocalizedReadmes() {
  164. return new Promise((resolve) => {
  165. const readmeFiles = []
  166. // Check if locales directory exists
  167. if (!fs.existsSync(LOCALES_DIR)) {
  168. // No localized READMEs found
  169. return resolve(readmeFiles)
  170. }
  171. // Get all language subdirectories
  172. const languageDirs = fs
  173. .readdirSync(LOCALES_DIR, { withFileTypes: true })
  174. .filter((dirent) => dirent.isDirectory())
  175. .map((dirent) => dirent.name)
  176. // Add all localized READMEs to the list
  177. for (const langDir of languageDirs) {
  178. const readmePath = path.join(LOCALES_DIR, langDir, "README.md")
  179. if (fs.existsSync(readmePath)) {
  180. readmeFiles.push(readmePath)
  181. }
  182. }
  183. resolve(readmeFiles)
  184. })
  185. }
  186. /**
  187. * Updates a localized README file with contributors section
  188. * @param {string} filePath Path to the README file
  189. * @param {string} contributorsSection HTML for contributors section
  190. * @returns {Promise<void>}
  191. */
  192. function updateLocalizedReadme(filePath, contributorsSection) {
  193. return new Promise((resolve, reject) => {
  194. fs.readFile(filePath, "utf8", (err, readmeContent) => {
  195. if (err) {
  196. console.warn(`Warning: Could not read ${filePath}: ${err.message}`)
  197. return resolve()
  198. }
  199. // Find existing contributors section markers
  200. const startPos = readmeContent.indexOf(START_MARKER)
  201. const endPos = readmeContent.indexOf(END_MARKER)
  202. if (startPos === -1 || endPos === -1) {
  203. console.warn(`Warning: Could not find contributors section markers in ${filePath}`)
  204. console.warn(`Skipping update for ${filePath}`)
  205. return resolve()
  206. }
  207. // Replace existing section, trimming whitespace at section boundaries
  208. const beforeSection = readmeContent.substring(0, startPos).trimEnd()
  209. const afterSection = readmeContent.substring(endPos + END_MARKER.length).trimStart()
  210. // Ensure single newline separators between sections
  211. const updatedContent = beforeSection + "\n\n" + contributorsSection.trim() + "\n\n" + afterSection
  212. fs.writeFile(filePath, updatedContent, "utf8", (writeErr) => {
  213. if (writeErr) {
  214. console.warn(`Warning: Failed to update ${filePath}: ${writeErr.message}`)
  215. return resolve()
  216. }
  217. console.log(`Updated ${filePath}`)
  218. resolve()
  219. })
  220. })
  221. })
  222. }
  223. /**
  224. * Main function that orchestrates the update process
  225. */
  226. async function main() {
  227. try {
  228. // Fetch contributors from GitHub
  229. const contributors = await fetchContributors()
  230. console.log(`Fetched ${contributors.length} contributors from GitHub`)
  231. // Generate contributors section
  232. const contributorsSection = formatContributorsSection(contributors)
  233. // Update main README
  234. const readmeContent = await readReadme()
  235. await updateReadme(readmeContent, contributorsSection)
  236. console.log(`Updated ${README_PATH}`)
  237. // Find and update all localized README files
  238. const localizedReadmes = await findLocalizedReadmes()
  239. console.log(`Found ${localizedReadmes.length} localized README files`)
  240. // Update each localized README
  241. for (const readmePath of localizedReadmes) {
  242. await updateLocalizedReadme(readmePath, contributorsSection)
  243. }
  244. console.log("Contributors section update complete")
  245. } catch (error) {
  246. console.error(`Error: ${error.message}`)
  247. process.exit(1)
  248. }
  249. }
  250. // Run the script
  251. main()