stats.ts 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225
  1. #!/usr/bin/env bun
  2. interface Asset {
  3. name: string
  4. download_count: number
  5. }
  6. interface Release {
  7. tag_name: string
  8. name: string
  9. assets: Asset[]
  10. }
  11. interface NpmDownloadsRange {
  12. start: string
  13. end: string
  14. package: string
  15. downloads: Array<{
  16. downloads: number
  17. day: string
  18. }>
  19. }
  20. async function fetchNpmDownloads(packageName: string): Promise<number> {
  21. try {
  22. // Use a range from 2020 to current year + 5 years to ensure it works forever
  23. const currentYear = new Date().getFullYear()
  24. const endYear = currentYear + 5
  25. const response = await fetch(
  26. `https://api.npmjs.org/downloads/range/2020-01-01:${endYear}-12-31/${packageName}`,
  27. )
  28. if (!response.ok) {
  29. console.warn(
  30. `Failed to fetch npm downloads for ${packageName}: ${response.status}`,
  31. )
  32. return 0
  33. }
  34. const data: NpmDownloadsRange = await response.json()
  35. return data.downloads.reduce((total, day) => total + day.downloads, 0)
  36. } catch (error) {
  37. console.warn(`Error fetching npm downloads for ${packageName}:`, error)
  38. return 0
  39. }
  40. }
  41. async function fetchReleases(): Promise<Release[]> {
  42. const releases: Release[] = []
  43. let page = 1
  44. const per = 100
  45. while (true) {
  46. const url = `https://api.github.com/repos/sst/opencode/releases?page=${page}&per_page=${per}`
  47. const response = await fetch(url)
  48. if (!response.ok) {
  49. throw new Error(
  50. `GitHub API error: ${response.status} ${response.statusText}`,
  51. )
  52. }
  53. const batch: Release[] = await response.json()
  54. if (batch.length === 0) break
  55. releases.push(...batch)
  56. console.log(`Fetched page ${page} with ${batch.length} releases`)
  57. if (batch.length < per) break
  58. page++
  59. }
  60. return releases
  61. }
  62. function calculate(releases: Release[]) {
  63. let total = 0
  64. const stats = []
  65. for (const release of releases) {
  66. let downloads = 0
  67. const assets = []
  68. for (const asset of release.assets) {
  69. downloads += asset.download_count
  70. assets.push({
  71. name: asset.name,
  72. downloads: asset.download_count,
  73. })
  74. }
  75. total += downloads
  76. stats.push({
  77. tag: release.tag_name,
  78. name: release.name,
  79. downloads,
  80. assets,
  81. })
  82. }
  83. return { total, stats }
  84. }
  85. async function save(githubTotal: number, npmDownloads: number) {
  86. const file = "STATS.md"
  87. const date = new Date().toISOString().split("T")[0]
  88. const total = githubTotal + npmDownloads
  89. let previousGithub = 0
  90. let previousNpm = 0
  91. let previousTotal = 0
  92. let content = ""
  93. try {
  94. content = await Bun.file(file).text()
  95. const lines = content.trim().split("\n")
  96. for (let i = lines.length - 1; i >= 0; i--) {
  97. const line = lines[i].trim()
  98. if (
  99. line.startsWith("|") &&
  100. !line.includes("Date") &&
  101. !line.includes("---")
  102. ) {
  103. const match = line.match(
  104. /\|\s*[\d-]+\s*\|\s*([\d,]+)\s*(?:\([^)]*\))?\s*\|\s*([\d,]+)\s*(?:\([^)]*\))?\s*\|\s*([\d,]+)\s*(?:\([^)]*\))?\s*\|/,
  105. )
  106. if (match) {
  107. previousGithub = parseInt(match[1].replace(/,/g, ""))
  108. previousNpm = parseInt(match[2].replace(/,/g, ""))
  109. previousTotal = parseInt(match[3].replace(/,/g, ""))
  110. break
  111. }
  112. }
  113. }
  114. } catch {
  115. content =
  116. "# Download Stats\n\n| Date | GitHub Downloads | npm Downloads | Total |\n|------|------------------|---------------|-------|\n"
  117. }
  118. const githubChange = githubTotal - previousGithub
  119. const npmChange = npmDownloads - previousNpm
  120. const totalChange = total - previousTotal
  121. const githubChangeStr =
  122. githubChange > 0
  123. ? ` (+${githubChange.toLocaleString()})`
  124. : githubChange < 0
  125. ? ` (${githubChange.toLocaleString()})`
  126. : " (+0)"
  127. const npmChangeStr =
  128. npmChange > 0
  129. ? ` (+${npmChange.toLocaleString()})`
  130. : npmChange < 0
  131. ? ` (${npmChange.toLocaleString()})`
  132. : " (+0)"
  133. const totalChangeStr =
  134. totalChange > 0
  135. ? ` (+${totalChange.toLocaleString()})`
  136. : totalChange < 0
  137. ? ` (${totalChange.toLocaleString()})`
  138. : " (+0)"
  139. const line = `| ${date} | ${githubTotal.toLocaleString()}${githubChangeStr} | ${npmDownloads.toLocaleString()}${npmChangeStr} | ${total.toLocaleString()}${totalChangeStr} |\n`
  140. if (!content.includes("# Download Stats")) {
  141. content =
  142. "# Download Stats\n\n| Date | GitHub Downloads | npm Downloads | Total |\n|------|------------------|---------------|-------|\n"
  143. }
  144. await Bun.write(file, content + line)
  145. await Bun.spawn(["bunx", "prettier", "--write", file]).exited
  146. console.log(
  147. `\nAppended stats to ${file}: GitHub ${githubTotal.toLocaleString()}${githubChangeStr}, npm ${npmDownloads.toLocaleString()}${npmChangeStr}, Total ${total.toLocaleString()}${totalChangeStr}`,
  148. )
  149. }
  150. console.log("Fetching GitHub releases for sst/opencode...\n")
  151. const releases = await fetchReleases()
  152. console.log(`\nFetched ${releases.length} releases total\n`)
  153. const { total: githubTotal, stats } = calculate(releases)
  154. console.log("Fetching npm all-time downloads for opencode-ai...\n")
  155. const npmDownloads = await fetchNpmDownloads("opencode-ai")
  156. console.log(
  157. `Fetched npm all-time downloads: ${npmDownloads.toLocaleString()}\n`,
  158. )
  159. await save(githubTotal, npmDownloads)
  160. const totalDownloads = githubTotal + npmDownloads
  161. console.log("=".repeat(60))
  162. console.log(`TOTAL DOWNLOADS: ${totalDownloads.toLocaleString()}`)
  163. console.log(` GitHub: ${githubTotal.toLocaleString()}`)
  164. console.log(` npm: ${npmDownloads.toLocaleString()}`)
  165. console.log("=".repeat(60))
  166. console.log("\nDownloads by release:")
  167. console.log("-".repeat(60))
  168. stats
  169. .sort((a, b) => b.downloads - a.downloads)
  170. .forEach((release) => {
  171. console.log(
  172. `${release.tag.padEnd(15)} ${release.downloads.toLocaleString().padStart(10)} downloads`,
  173. )
  174. if (release.assets.length > 1) {
  175. release.assets
  176. .sort((a, b) => b.downloads - a.downloads)
  177. .forEach((asset) => {
  178. console.log(
  179. ` └─ ${asset.name.padEnd(25)} ${asset.downloads.toLocaleString().padStart(8)}`,
  180. )
  181. })
  182. }
  183. })
  184. console.log("-".repeat(60))
  185. console.log(
  186. `GitHub Total: ${githubTotal.toLocaleString()} downloads across ${releases.length} releases`,
  187. )
  188. console.log(`npm Total: ${npmDownloads.toLocaleString()} downloads`)
  189. console.log(`Combined Total: ${totalDownloads.toLocaleString()} downloads`)