stats.ts 6.2 KB

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