88
99const https = require ( "https" )
1010const fs = require ( "fs" )
11+ const { promisify } = require ( "util" )
1112const path = require ( "path" )
1213
14+ // Promisify filesystem operations
15+ const readFileAsync = promisify ( fs . readFile )
16+ const writeFileAsync = promisify ( fs . writeFile )
17+
1318// GitHub API URL for fetching contributors
1419const GITHUB_API_URL = "https://api.github.com/repos/RooVetGit/Roo-Code/contributors?per_page=100"
1520const README_PATH = path . join ( __dirname , ".." , "README.md" )
@@ -33,52 +38,144 @@ if (process.env.GITHUB_TOKEN) {
3338}
3439
3540/**
36- * Fetches contributors data from GitHub API
37- * @returns {Promise<Array> } Array of contributor objects
41+ * Parses the GitHub API Link header to extract pagination URLs
42+ * Based on RFC 5988 format for the Link header
43+ * @param {string } header The Link header from GitHub API response
44+ * @returns {Object } Object containing URLs for next, prev, first, last pages (if available)
3845 */
39- function fetchContributors ( ) {
46+ function parseLinkHeader ( header ) {
47+ // Return empty object if no header is provided
48+ if ( ! header || header . trim ( ) === "" ) return { }
49+
50+ // Initialize links object
51+ const links = { }
52+
53+ // Split the header into individual link entries
54+ // Example: <https://api.github.com/...?page=2>; rel="next", <https://api.github.com/...?page=5>; rel="last"
55+ const entries = header . split ( / , \s * / )
56+
57+ // Process each link entry
58+ for ( const entry of entries ) {
59+ // Extract the URL (between < and >) and the parameters (after >)
60+ const segments = entry . split ( ";" )
61+ if ( segments . length < 2 ) continue
62+
63+ // Extract URL from the first segment, removing < and >
64+ const urlMatch = segments [ 0 ] . match ( / < ( .+ ) > / )
65+ if ( ! urlMatch ) continue
66+ const url = urlMatch [ 1 ]
67+
68+ // Find the rel="value" parameter
69+ let rel = null
70+ for ( let i = 1 ; i < segments . length ; i ++ ) {
71+ const relMatch = segments [ i ] . match ( / \s * r e l \s * = \s * " ? ( [ ^ " ] + ) " ? / )
72+ if ( relMatch ) {
73+ rel = relMatch [ 1 ]
74+ break
75+ }
76+ }
77+
78+ // Only add to links if both URL and rel were found
79+ if ( rel ) {
80+ links [ rel ] = url
81+ }
82+ }
83+
84+ return links
85+ }
86+
87+ /**
88+ * Performs an HTTP GET request and returns the response
89+ * @param {string } url The URL to fetch
90+ * @param {Object } options Request options
91+ * @returns {Promise<Object> } Response object with status, headers and body
92+ */
93+ function httpGet ( url , options ) {
4094 return new Promise ( ( resolve , reject ) => {
4195 https
42- . get ( GITHUB_API_URL , options , ( res ) => {
43- if ( res . statusCode !== 200 ) {
44- reject ( new Error ( `GitHub API request failed with status code: ${ res . statusCode } ` ) )
45- return
46- }
47-
96+ . get ( url , options , ( res ) => {
4897 let data = ""
4998 res . on ( "data" , ( chunk ) => {
5099 data += chunk
51100 } )
52101
53102 res . on ( "end" , ( ) => {
54- try {
55- const contributors = JSON . parse ( data )
56- resolve ( contributors )
57- } catch ( error ) {
58- reject ( new Error ( `Failed to parse GitHub API response: ${ error . message } ` ) )
59- }
103+ resolve ( {
104+ statusCode : res . statusCode ,
105+ headers : res . headers ,
106+ body : data ,
107+ } )
60108 } )
61109 } )
62110 . on ( "error" , ( error ) => {
63- reject ( new Error ( `GitHub API request failed: ${ error . message } ` ) )
111+ reject ( error )
64112 } )
65113 } )
66114}
67115
116+ /**
117+ * Fetches a single page of contributors from GitHub API
118+ * @param {string } url The API URL to fetch
119+ * @returns {Promise<Object> } Object containing contributors and pagination links
120+ */
121+ async function fetchContributorsPage ( url ) {
122+ try {
123+ // Make the HTTP request
124+ const response = await httpGet ( url , options )
125+
126+ // Check for successful response
127+ if ( response . statusCode !== 200 ) {
128+ throw new Error ( `GitHub API request failed with status code: ${ response . statusCode } ` )
129+ }
130+
131+ // Parse the Link header for pagination
132+ const linkHeader = response . headers . link
133+ const links = parseLinkHeader ( linkHeader )
134+
135+ // Parse the JSON response
136+ const contributors = JSON . parse ( response . body )
137+
138+ return { contributors, links }
139+ } catch ( error ) {
140+ throw new Error ( `Failed to fetch contributors page: ${ error . message } ` )
141+ }
142+ }
143+
144+ /**
145+ * Fetches all contributors data from GitHub API (handling pagination)
146+ * @returns {Promise<Array> } Array of all contributor objects
147+ */
148+ async function fetchContributors ( ) {
149+ let allContributors = [ ]
150+ let currentUrl = GITHUB_API_URL
151+ let pageCount = 1
152+
153+ // Loop through all pages of contributors
154+ while ( currentUrl ) {
155+ console . log ( `Fetching contributors page ${ pageCount } ...` )
156+ const { contributors, links } = await fetchContributorsPage ( currentUrl )
157+
158+ allContributors = allContributors . concat ( contributors )
159+
160+ // Move to the next page if it exists
161+ currentUrl = links . next
162+ pageCount ++
163+ }
164+
165+ console . log ( `Fetched ${ allContributors . length } contributors from ${ pageCount - 1 } pages` )
166+ return allContributors
167+ }
168+
68169/**
69170 * Reads the README.md file
70171 * @returns {Promise<string> } README content
71172 */
72- function readReadme ( ) {
73- return new Promise ( ( resolve , reject ) => {
74- fs . readFile ( README_PATH , "utf8" , ( err , data ) => {
75- if ( err ) {
76- reject ( new Error ( `Failed to read README.md: ${ err . message } ` ) )
77- return
78- }
79- resolve ( data )
80- } )
81- } )
173+ async function readReadme ( ) {
174+ try {
175+ return await readFileAsync ( README_PATH , "utf8" )
176+ } catch ( err ) {
177+ throw new Error ( `Failed to read README.md: ${ err . message } ` )
178+ }
82179}
83180
84181/**
@@ -147,7 +244,7 @@ function formatContributorsSection(contributors) {
147244 * @param {string } contributorsSection HTML for contributors section
148245 * @returns {Promise<void> }
149246 */
150- function updateReadme ( readmeContent , contributorsSection ) {
247+ async function updateReadme ( readmeContent , contributorsSection ) {
151248 // Find existing contributors section markers
152249 const startPos = readmeContent . indexOf ( START_MARKER )
153250 const endPos = readmeContent . indexOf ( END_MARKER )
@@ -164,55 +261,49 @@ function updateReadme(readmeContent, contributorsSection) {
164261 // Ensure single newline separators between sections
165262 const updatedContent = beforeSection + "\n\n" + contributorsSection . trim ( ) + "\n\n" + afterSection
166263
167- return writeReadme ( updatedContent )
264+ await writeReadme ( updatedContent )
168265}
169266
170267/**
171268 * Writes updated content to README.md
172269 * @param {string } content Updated README content
173270 * @returns {Promise<void> }
174271 */
175- function writeReadme ( content ) {
176- return new Promise ( ( resolve , reject ) => {
177- fs . writeFile ( README_PATH , content , "utf8" , ( err ) => {
178- if ( err ) {
179- reject ( new Error ( `Failed to write updated README.md: ${ err . message } ` ) )
180- return
181- }
182- resolve ( )
183- } )
184- } )
272+ async function writeReadme ( content ) {
273+ try {
274+ await writeFileAsync ( README_PATH , content , "utf8" )
275+ } catch ( err ) {
276+ throw new Error ( `Failed to write updated README.md: ${ err . message } ` )
277+ }
185278}
186279/**
187280 * Finds all localized README files in the locales directory
188281 * @returns {Promise<string[]> } Array of README file paths
189282 */
190- function findLocalizedReadmes ( ) {
191- return new Promise ( ( resolve ) => {
192- const readmeFiles = [ ]
193-
194- // Check if locales directory exists
195- if ( ! fs . existsSync ( LOCALES_DIR ) ) {
196- // No localized READMEs found
197- return resolve ( readmeFiles )
198- }
283+ async function findLocalizedReadmes ( ) {
284+ const readmeFiles = [ ]
199285
200- // Get all language subdirectories
201- const languageDirs = fs
202- . readdirSync ( LOCALES_DIR , { withFileTypes : true } )
203- . filter ( ( dirent ) => dirent . isDirectory ( ) )
204- . map ( ( dirent ) => dirent . name )
205-
206- // Add all localized READMEs to the list
207- for ( const langDir of languageDirs ) {
208- const readmePath = path . join ( LOCALES_DIR , langDir , "README.md" )
209- if ( fs . existsSync ( readmePath ) ) {
210- readmeFiles . push ( readmePath )
211- }
286+ // Check if locales directory exists
287+ if ( ! fs . existsSync ( LOCALES_DIR ) ) {
288+ // No localized READMEs found
289+ return readmeFiles
290+ }
291+
292+ // Get all language subdirectories
293+ const languageDirs = fs
294+ . readdirSync ( LOCALES_DIR , { withFileTypes : true } )
295+ . filter ( ( dirent ) => dirent . isDirectory ( ) )
296+ . map ( ( dirent ) => dirent . name )
297+
298+ // Add all localized READMEs to the list
299+ for ( const langDir of languageDirs ) {
300+ const readmePath = path . join ( LOCALES_DIR , langDir , "README.md" )
301+ if ( fs . existsSync ( readmePath ) ) {
302+ readmeFiles . push ( readmePath )
212303 }
304+ }
213305
214- resolve ( readmeFiles )
215- } )
306+ return readmeFiles
216307}
217308
218309/**
@@ -221,50 +312,43 @@ function findLocalizedReadmes() {
221312 * @param {string } contributorsSection HTML for contributors section
222313 * @returns {Promise<void> }
223314 */
224- function updateLocalizedReadme ( filePath , contributorsSection ) {
225- return new Promise ( ( resolve , reject ) => {
226- fs . readFile ( filePath , "utf8" , ( err , readmeContent ) => {
227- if ( err ) {
228- console . warn ( `Warning: Could not read ${ filePath } : ${ err . message } ` )
229- return resolve ( )
230- }
315+ async function updateLocalizedReadme ( filePath , contributorsSection ) {
316+ try {
317+ // Read the file content
318+ const readmeContent = await readFileAsync ( filePath , "utf8" )
231319
232- // Find existing contributors section markers
233- const startPos = readmeContent . indexOf ( START_MARKER )
234- const endPos = readmeContent . indexOf ( END_MARKER )
320+ // Find existing contributors section markers
321+ const startPos = readmeContent . indexOf ( START_MARKER )
322+ const endPos = readmeContent . indexOf ( END_MARKER )
235323
236- if ( startPos === - 1 || endPos === - 1 ) {
237- console . warn ( `Warning: Could not find contributors section markers in ${ filePath } ` )
238- console . warn ( `Skipping update for ${ filePath } ` )
239- return resolve ( )
240- }
324+ if ( startPos === - 1 || endPos === - 1 ) {
325+ console . warn ( `Warning: Could not find contributors section markers in ${ filePath } ` )
326+ console . warn ( `Skipping update for ${ filePath } ` )
327+ return
328+ }
241329
242- // Replace existing section, trimming whitespace at section boundaries
243- const beforeSection = readmeContent . substring ( 0 , startPos ) . trimEnd ( )
244- const afterSection = readmeContent . substring ( endPos + END_MARKER . length ) . trimStart ( )
245- // Ensure single newline separators between sections
246- const updatedContent = beforeSection + "\n\n" + contributorsSection . trim ( ) + "\n\n" + afterSection
247-
248- fs . writeFile ( filePath , updatedContent , "utf8" , ( writeErr ) => {
249- if ( writeErr ) {
250- console . warn ( `Warning: Failed to update ${ filePath } : ${ writeErr . message } ` )
251- return resolve ( )
252- }
253- console . log ( `Updated ${ filePath } ` )
254- resolve ( )
255- } )
256- } )
257- } )
330+ // Replace existing section, trimming whitespace at section boundaries
331+ const beforeSection = readmeContent . substring ( 0 , startPos ) . trimEnd ( )
332+ const afterSection = readmeContent . substring ( endPos + END_MARKER . length ) . trimStart ( )
333+ // Ensure single newline separators between sections
334+ const updatedContent = beforeSection + "\n\n" + contributorsSection . trim ( ) + "\n\n" + afterSection
335+
336+ // Write the updated content
337+ await writeFileAsync ( filePath , updatedContent , "utf8" )
338+ console . log ( `Updated ${ filePath } ` )
339+ } catch ( err ) {
340+ console . warn ( `Warning: Could not update ${ filePath } : ${ err . message } ` )
341+ }
258342}
259343
260344/**
261345 * Main function that orchestrates the update process
262346 */
263347async function main ( ) {
264348 try {
265- // Fetch contributors from GitHub
349+ // Fetch contributors from GitHub (now handles pagination)
266350 const contributors = await fetchContributors ( )
267- console . log ( `Fetched ${ contributors . length } contributors from GitHub ` )
351+ console . log ( `Total contributors: ${ contributors . length } ` )
268352
269353 // Generate contributors section
270354 const contributorsSection = formatContributorsSection ( contributors )
0 commit comments