22
33/**
44 * GitHub Stars Updater
5- * Updates github-stars.json with new manifest entries
5+ * Keeps repository keys in github-stars.json aligned with product manifests.
66 */
77
88import fs from 'node:fs'
@@ -11,122 +11,101 @@ import { fileURLToPath } from 'node:url'
1111
1212const __filename = fileURLToPath ( import . meta. url )
1313const __dirname = path . dirname ( __filename )
14+ const manifestDirectories = {
15+ cli : 'clis' ,
16+ desktop : 'desktops' ,
17+ extension : 'extensions' ,
18+ ide : 'ides' ,
19+ }
1420
15- /**
16- * Get project root directory
17- */
1821function getProjectRoot ( ) {
1922 return path . resolve ( __dirname , '../../../../..' )
2023}
2124
22- /**
23- * Get the path to github-stars.json
24- */
2525function getGithubStarsPath ( ) {
2626 return path . join ( getProjectRoot ( ) , 'data/github-stars.json' )
2727}
2828
29- /**
30- * Load github-stars.json
31- * @returns {Object } The current github-stars data
32- */
33- export function loadGithubStars ( ) {
34- const filePath = getGithubStarsPath ( )
29+ function getManifestPath ( type , id ) {
30+ const directory = manifestDirectories [ type ]
31+ return directory ? path . join ( getProjectRoot ( ) , 'manifests' , directory , `${ id } .json` ) : null
32+ }
3533
36- if ( ! fs . existsSync ( filePath ) ) {
37- throw new Error ( `github-stars.json not found at: ${ filePath } ` )
38- }
34+ function repositoryIdFromUrl ( url ) {
35+ const match = url
36+ ?. replace ( / \/ $ / , '' )
37+ . replace ( / \. g i t $ / , '' )
38+ . match ( / ^ h t t p s : \/ \/ g i t h u b \. c o m \/ ( .+ \/ .+ ) $ / )
39+ return match ?. [ 1 ] ?? null
40+ }
3941
40- const content = fs . readFileSync ( filePath , 'utf-8' )
41- return JSON . parse ( content )
42+ function loadManifestRepository ( type , id ) {
43+ const manifestPath = getManifestPath ( type , id )
44+ if ( ! manifestPath || ! fs . existsSync ( manifestPath ) ) return null
45+ const manifest = JSON . parse ( fs . readFileSync ( manifestPath , 'utf8' ) )
46+ return repositoryIdFromUrl ( manifest . githubUrl )
4247}
4348
44- /**
45- * Save github-stars.json
46- * @param {Object } data - The github-stars data to save
47- */
48- export function saveGithubStars ( data ) {
49- const filePath = getGithubStarsPath ( )
50- const content = `${ JSON . stringify ( data , null , 2 ) } \n`
51- fs . writeFileSync ( filePath , content , 'utf-8' )
49+ function countRepositoryAssociations ( repositoryId , excludedType , excludedId ) {
50+ let count = 0
51+ for ( const [ type , directory ] of Object . entries ( manifestDirectories ) ) {
52+ const directoryPath = path . join ( getProjectRoot ( ) , 'manifests' , directory )
53+ for ( const file of fs . readdirSync ( directoryPath ) . filter ( name => name . endsWith ( '.json' ) ) ) {
54+ const id = file . replace ( / \. j s o n $ / , '' )
55+ if ( type === excludedType && id === excludedId ) continue
56+ const manifest = JSON . parse ( fs . readFileSync ( path . join ( directoryPath , file ) , 'utf8' ) )
57+ if ( repositoryIdFromUrl ( manifest . githubUrl ) === repositoryId ) count += 1
58+ }
59+ }
60+ return count
5261}
5362
54- /**
55- * Get the tracked category name from manifest type.
56- * @param {string } type - Manifest type (cli, extension, ide, model)
57- * @returns {string } Category name for github-stars.json
58- */
59- function getCategoryName ( type ) {
60- const mapping = {
61- cli : 'clis' ,
62- extension : 'extensions' ,
63- ide : 'ides' ,
64- model : 'models' ,
63+ export function loadGithubStars ( ) {
64+ const filePath = getGithubStarsPath ( )
65+ if ( ! fs . existsSync ( filePath ) ) {
66+ throw new Error ( `github-stars.json not found at: ${ filePath } ` )
6567 }
68+ return JSON . parse ( fs . readFileSync ( filePath , 'utf8' ) )
69+ }
6670
67- return mapping [ type ] || null
71+ export function saveGithubStars ( data ) {
72+ const sortedRepositories = Object . fromEntries (
73+ Object . entries ( data . repositories ) . sort ( ( [ left ] , [ right ] ) => left . localeCompare ( right ) )
74+ )
75+ fs . writeFileSync (
76+ getGithubStarsPath ( ) ,
77+ `${ JSON . stringify ( { ...data , repositories : sortedRepositories } , null , 2 ) } \n` ,
78+ 'utf8'
79+ )
6880}
6981
70- /**
71- * Update github-stars.json with a new or updated manifest entry
72- * @param {string } type - Manifest type (cli, extension, ide, etc.)
73- * @param {string } id - Manifest id
74- * @param {Object } options - Options
75- * @param {boolean } options.isNew - Whether this is a new entry (true) or update (false)
76- * @returns {Object } Result with status and message
77- */
7882export function updateGithubStarsEntry ( type , id , options = { } ) {
7983 const { isNew = false } = options
8084
8185 try {
82- // Load current data
83- const githubStars = loadGithubStars ( )
84- const category = getCategoryName ( type )
85-
86- if ( ! category || ! Object . hasOwn ( githubStars , category ) ) {
86+ const repositoryId = loadManifestRepository ( type , id )
87+ if ( ! repositoryId ) {
8788 return {
8889 status : 'skipped' ,
89- message : `Manifest type "${ type } " is not tracked by data/github-stars.json ` ,
90+ message : `Manifest "${ type } : ${ id } " has no tracked GitHub repository ` ,
9091 }
9192 }
9293
93- // Check if entry already exists
94- const exists = id in githubStars [ category ]
95-
94+ const githubStars = loadGithubStars ( )
95+ const exists = Object . hasOwn ( githubStars . repositories , repositoryId )
9696 if ( isNew && exists ) {
9797 return {
9898 status : 'skipped' ,
99- message : `Entry "${ id } " already exists in github-stars.json under "${ category } "` ,
100- }
101- }
102-
103- if ( ! isNew && ! exists ) {
104- return {
105- status : 'skipped' ,
106- message : `Entry "${ id } " does not exist in github-stars.json under "${ category } "; no change made. Verify the official repository, then use the add command.` ,
99+ message : `Repository "${ repositoryId } " already exists in github-stars.json` ,
107100 }
108101 }
109102
110- // Add or update entry with null (stars will be fetched later)
111- githubStars [ category ] [ id ] = null
112-
113- // Sort entries alphabetically within category
114- const sortedCategory = Object . keys ( githubStars [ category ] )
115- . sort ( )
116- . reduce ( ( acc , key ) => {
117- acc [ key ] = githubStars [ category ] [ key ]
118- return acc
119- } , { } )
120-
121- githubStars [ category ] = sortedCategory
122-
123- // Save updated data
103+ githubStars . repositories [ repositoryId ] ??= null
124104 saveGithubStars ( githubStars )
125-
126105 return {
127106 status : 'success' ,
128- message : `Updated github-stars.json: ${ category } [ "${ id } "] = null ` ,
129- action : exists ? 'updated ' : 'added' ,
107+ message : `Tracked github-stars.json repository "${ repositoryId } " ` ,
108+ action : exists ? 'unchanged ' : 'added' ,
130109 }
131110 } catch ( error ) {
132111 return {
@@ -137,83 +116,59 @@ export function updateGithubStarsEntry(type, id, options = {}) {
137116 }
138117}
139118
140- /**
141- * Remove an entry from github-stars.json
142- * @param {string } type - Manifest type
143- * @param {string } id - Manifest id
144- * @returns {Object } Result with status and message
145- */
146119export function removeGithubStarsEntry ( type , id ) {
147120 try {
148- const githubStars = loadGithubStars ( )
149- const category = getCategoryName ( type )
121+ const repositoryId = loadManifestRepository ( type , id )
122+ if ( ! repositoryId ) {
123+ return {
124+ status : 'skipped' ,
125+ message : `Manifest "${ type } :${ id } " has no tracked GitHub repository` ,
126+ }
127+ }
150128
151- if ( ! category || ! Object . hasOwn ( githubStars , category ) ) {
129+ if ( countRepositoryAssociations ( repositoryId , type , id ) > 0 ) {
152130 return {
153131 status : 'skipped' ,
154- message : `Manifest type "${ type } " is not tracked by data/github-stars.json ` ,
132+ message : `Repository "${ repositoryId } " is still used by another product surface ` ,
155133 }
156134 }
157135
158- if ( ! githubStars [ category ] || ! ( id in githubStars [ category ] ) ) {
136+ const githubStars = loadGithubStars ( )
137+ if ( ! Object . hasOwn ( githubStars . repositories , repositoryId ) ) {
159138 return {
160139 status : 'skipped' ,
161- message : `Entry "${ id } " not found in github-stars.json under " ${ category } " ` ,
140+ message : `Repository "${ repositoryId } " is not tracked in github-stars.json` ,
162141 }
163142 }
164143
165- delete githubStars [ category ] [ id ]
144+ delete githubStars . repositories [ repositoryId ]
166145 saveGithubStars ( githubStars )
167-
168146 return {
169147 status : 'success' ,
170- message : `Removed "${ id } " from github-stars.json under " ${ category } " ` ,
148+ message : `Removed repository "${ repositoryId } " from github-stars.json` ,
171149 }
172150 } catch ( error ) {
173151 return {
174152 status : 'error' ,
175- message : `Failed to remove entry from github-stars.json: ${ error . message } ` ,
153+ message : `Failed to remove repository from github-stars.json: ${ error . message } ` ,
176154 error,
177155 }
178156 }
179157}
180158
181- /**
182- * CLI entry point for testing
183- */
184159if ( import . meta. url === `file://${ process . argv [ 1 ] } ` ) {
185160 const [ , , command , type , id ] = process . argv
186-
187- if ( ! command || ! [ 'add' , 'update' , 'remove' ] . includes ( command ) ) {
161+ if ( ! command || ! [ 'add' , 'update' , 'remove' ] . includes ( command ) || ! type || ! id ) {
188162 console . error ( 'Usage:' )
189- console . error ( ' node github-stars-updater.mjs add <type> <id>' )
190- console . error ( ' node github-stars-updater.mjs update <type> <id>' )
191- console . error ( ' node github-stars-updater.mjs remove <type> <id>' )
192- console . error ( '' )
193- console . error ( 'Examples:' )
194- console . error ( ' node github-stars-updater.mjs add cli cursor-cli' )
195- console . error ( ' node github-stars-updater.mjs update extension claude-code' )
196- console . error ( ' node github-stars-updater.mjs remove ide windsurf' )
197- process . exit ( 1 )
198- }
199-
200- if ( ! type || ! id ) {
201- console . error ( 'Error: type and id are required' )
163+ console . error ( ' node github-stars-updater.mjs <add|update|remove> <type> <id>' )
202164 process . exit ( 1 )
203165 }
204166
205- let result
206-
207- if ( command === 'add' || command === 'update' ) {
208- result = updateGithubStarsEntry ( type , id , { isNew : command === 'add' } )
209- } else {
210- result = removeGithubStarsEntry ( type , id )
211- }
212-
167+ const result =
168+ command === 'remove'
169+ ? removeGithubStarsEntry ( type , id )
170+ : updateGithubStarsEntry ( type , id , { isNew : command === 'add' } )
213171 console . log ( `Status: ${ result . status } ` )
214172 console . log ( `Message: ${ result . message } ` )
215-
216- if ( result . status === 'error' ) {
217- process . exit ( 1 )
218- }
173+ if ( result . status === 'error' ) process . exit ( 1 )
219174}
0 commit comments