@@ -109,6 +109,19 @@ const parseExcludeList = (value) => {
109109 . filter ( Boolean ) ;
110110} ;
111111
112+ /**
113+ * Parse a comma-separated include list of repository names ("owner/repo").
114+ * @param {string | undefined } value
115+ * @returns {string[] }
116+ */
117+ const parseIncludeList = ( value ) => {
118+ if ( ! value ) return [ ] ;
119+ return value
120+ . split ( "," )
121+ . map ( ( entry ) => entry . trim ( ) )
122+ . filter ( Boolean ) ;
123+ } ;
124+
112125/**
113126 * Check if a repository name should be excluded.
114127 * @param {string } repoName
@@ -149,6 +162,23 @@ const resolveOrgDisplayName = (ownerType, orgDisplayName, repoName) => {
149162// GitHub GraphQL fetcher
150163// ---------------------------------------------------------------------------
151164
165+ const REPO_INFO_QUERY = `
166+ query($owner: String!, $name: String!) {
167+ repository(owner: $owner, name: $name) {
168+ nameWithOwner
169+ isFork
170+ owner {
171+ __typename
172+ login
173+ avatarUrl
174+ ... on Organization { name }
175+ }
176+ stargazerCount
177+ primaryLanguage { name }
178+ }
179+ }
180+ ` ;
181+
152182const SEARCH_MERGED_PRS_QUERY = `
153183 query($searchQuery: String!, $after: String) {
154184 search(query: $searchQuery, type: ISSUE, first: 100, after: $after) {
@@ -188,9 +218,15 @@ const SEARCH_MERGED_PRS_QUERY = `
188218 * @param {string } username GitHub username.
189219 * @param {string } token GitHub PAT.
190220 * @param {string[] } [excludeList] List of repo name substrings to skip.
221+ * @param {string[] } [includeList] List of "owner/repo" names to always include.
191222 * @returns {Promise<UserPRsResult> } Aggregated PR data separated by external and own repos.
192223 */
193- const fetchUserPRs = async ( username , token , excludeList = [ ] ) => {
224+ const fetchUserPRs = async (
225+ username ,
226+ token ,
227+ excludeList = [ ] ,
228+ includeList = [ ] ,
229+ ) => {
194230 const headers = {
195231 Authorization : `bearer ${ token } ` ,
196232 "Content-Type" : "application/json" ,
@@ -333,6 +369,80 @@ const fetchUserPRs = async (username, token, excludeList = []) => {
333369 // Sort own repos descending by merged PRs to mirror external ordering.
334370 ownResult . sort ( ( a , b ) => b . mergedPRs - a . mergedPRs ) ;
335371
372+ // Process forced-include repos that may have no merged PRs or be forks.
373+ for ( const includeRepo of includeList ) {
374+ const slashIdx = includeRepo . indexOf ( "/" ) ;
375+ if ( slashIdx === - 1 ) continue ;
376+ const owner = includeRepo . slice ( 0 , slashIdx ) ;
377+ const name = includeRepo . slice ( slashIdx + 1 ) ;
378+ if ( ! owner || ! name ) continue ;
379+ const fullName = `${ owner } /${ name } ` ;
380+ const fullNameLower = fullName . toLowerCase ( ) ;
381+
382+ // Skip if already in results.
383+ const alreadyExternal = externalResult . some (
384+ ( e ) => e . repo . toLowerCase ( ) === fullNameLower ,
385+ ) ;
386+ const alreadyOwn = ownResult . some (
387+ ( e ) => e . repo . toLowerCase ( ) === fullNameLower ,
388+ ) ;
389+ if ( alreadyExternal || alreadyOwn ) continue ;
390+
391+ // Fetch repo info from GitHub.
392+ let repoNode ;
393+ try {
394+ const res = await fetch ( "https://api.github.com/graphql" , {
395+ method : "POST" ,
396+ headers,
397+ body : JSON . stringify ( {
398+ query : REPO_INFO_QUERY ,
399+ variables : { owner, name } ,
400+ } ) ,
401+ } ) ;
402+ if ( ! res . ok ) {
403+ console . warn (
404+ `Could not fetch included repository ${ fullName } : HTTP ${ res . status } ` ,
405+ ) ;
406+ continue ;
407+ }
408+ const json = await res . json ( ) ;
409+ repoNode = json . data ?. repository ;
410+ if ( ! repoNode ) {
411+ console . warn (
412+ `Included repository ${ fullName } was not found on GitHub.` ,
413+ ) ;
414+ }
415+ } catch ( err ) {
416+ console . warn ( `Failed to fetch included repository ${ fullName } : ${ err } ` ) ;
417+ continue ;
418+ }
419+ if ( ! repoNode ) continue ;
420+
421+ const ownerLogin = repoNode . owner . login ;
422+ const ownerType = repoNode . owner . __typename || "User" ;
423+ const displayName = resolveOrgDisplayName (
424+ ownerType ,
425+ repoNode . owner . name || ownerLogin ,
426+ repoNode . nameWithOwner ,
427+ ) ;
428+
429+ const entry = {
430+ org : ownerLogin ,
431+ orgDisplayName : displayName ,
432+ avatarUrl : repoNode . owner . avatarUrl ,
433+ repo : repoNode . nameWithOwner ,
434+ stars : repoNode . stargazerCount ,
435+ mergedPRs : 0 ,
436+ language : repoNode . primaryLanguage ?. name || "" ,
437+ } ;
438+
439+ if ( ownerLogin . toLowerCase ( ) === username . toLowerCase ( ) ) {
440+ ownResult . push ( entry ) ;
441+ } else {
442+ externalResult . push ( entry ) ;
443+ }
444+ }
445+
336446 return {
337447 external : externalResult ,
338448 own : ownResult ,
@@ -502,6 +612,14 @@ const renderOrgCard = async (
502612 return String ( n ) ;
503613 } ;
504614
615+ const mergedPRsSvg =
616+ data . mergedPRs > 0
617+ ? `<g transform="translate(80, 0)">
618+ ${ mergedIcon }
619+ <text x="20" y="13" class="stat">${ data . mergedPRs } merged</text>
620+ </g>`
621+ : "" ;
622+
505623 const svg = `<svg
506624 width="${ width } " height="${ height } "
507625 viewBox="0 0 ${ width } ${ height } "
@@ -541,10 +659,7 @@ const renderOrgCard = async (
541659 ${ starIcon }
542660 <text x="20" y="13" class="stat">${ formatCount ( data . stars ) } </text>
543661 </g>
544- <g transform="translate(80, 0)">
545- ${ mergedIcon }
546- <text x="20" y="13" class="stat">${ data . mergedPRs } merged</text>
547- </g>
662+ ${ mergedPRsSvg }
548663 </g>
549664</svg>` ;
550665
@@ -577,6 +692,7 @@ export {
577692 LANG_ICON_SLUGS ,
578693 parseCustomImages ,
579694 parseExcludeList ,
695+ parseIncludeList ,
580696 shouldExcludeRepo ,
581697 getRepoShortName ,
582698 resolveOrgDisplayName ,
0 commit comments