Dax Raad пре 9 месеци
родитељ
комит
780419ecae
3 измењених фајлова са 260 додато и 1 уклоњено
  1. 30 0
      .github/workflows/stats.yml
  2. 225 0
      scripts/stats.ts
  3. 5 1
      tsconfig.json

+ 30 - 0
.github/workflows/stats.yml

@@ -0,0 +1,30 @@
+name: stats
+
+on:
+  schedule:
+    - cron: "0 12 * * *" # Run daily at 12:00 UTC
+  workflow_dispatch: # Allow manual trigger
+
+jobs:
+  stats:
+    runs-on: ubuntu-latest
+
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v4
+
+      - name: Setup Bun
+        uses: oven-sh/setup-bun@v2
+        with:
+          bun-version: latest
+
+      - name: Run stats script
+        run: bun scripts/stats.ts
+
+      - name: Commit stats
+        run: |
+          git config --local user.email "[email protected]"
+          git config --local user.name "GitHub Action"
+          git add STATS.md
+          git diff --staged --quiet || git commit -m "Update download stats $(date -I)"
+          git push

+ 225 - 0
scripts/stats.ts

@@ -0,0 +1,225 @@
+#!/usr/bin/env bun
+
+interface Asset {
+  name: string
+  download_count: number
+}
+
+interface Release {
+  tag_name: string
+  name: string
+  assets: Asset[]
+}
+
+interface NpmDownloadsRange {
+  start: string
+  end: string
+  package: string
+  downloads: Array<{
+    downloads: number
+    day: string
+  }>
+}
+
+async function fetchNpmDownloads(packageName: string): Promise<number> {
+  try {
+    // Use a range from 2020 to current year + 5 years to ensure it works forever
+    const currentYear = new Date().getFullYear()
+    const endYear = currentYear + 5
+    const response = await fetch(
+      `https://api.npmjs.org/downloads/range/2020-01-01:${endYear}-12-31/${packageName}`,
+    )
+    if (!response.ok) {
+      console.warn(
+        `Failed to fetch npm downloads for ${packageName}: ${response.status}`,
+      )
+      return 0
+    }
+    const data: NpmDownloadsRange = await response.json()
+    return data.downloads.reduce((total, day) => total + day.downloads, 0)
+  } catch (error) {
+    console.warn(`Error fetching npm downloads for ${packageName}:`, error)
+    return 0
+  }
+}
+
+async function fetchReleases(): Promise<Release[]> {
+  const releases: Release[] = []
+  let page = 1
+  const per = 100
+
+  while (true) {
+    const url = `https://api.github.com/repos/sst/opencode/releases?page=${page}&per_page=${per}`
+
+    const response = await fetch(url)
+    if (!response.ok) {
+      throw new Error(
+        `GitHub API error: ${response.status} ${response.statusText}`,
+      )
+    }
+
+    const batch: Release[] = await response.json()
+    if (batch.length === 0) break
+
+    releases.push(...batch)
+    console.log(`Fetched page ${page} with ${batch.length} releases`)
+
+    if (batch.length < per) break
+    page++
+  }
+
+  return releases
+}
+
+function calculate(releases: Release[]) {
+  let total = 0
+  const stats = []
+
+  for (const release of releases) {
+    let downloads = 0
+    const assets = []
+
+    for (const asset of release.assets) {
+      downloads += asset.download_count
+      assets.push({
+        name: asset.name,
+        downloads: asset.download_count,
+      })
+    }
+
+    total += downloads
+    stats.push({
+      tag: release.tag_name,
+      name: release.name,
+      downloads,
+      assets,
+    })
+  }
+
+  return { total, stats }
+}
+
+async function save(githubTotal: number, npmDownloads: number) {
+  const file = "STATS.md"
+  const date = new Date().toISOString().split("T")[0]
+  const total = githubTotal + npmDownloads
+
+  let previousGithub = 0
+  let previousNpm = 0
+  let previousTotal = 0
+  let content = ""
+
+  try {
+    content = await Bun.file(file).text()
+    const lines = content.trim().split("\n")
+
+    for (let i = lines.length - 1; i >= 0; i--) {
+      const line = lines[i].trim()
+      if (
+        line.startsWith("|") &&
+        !line.includes("Date") &&
+        !line.includes("---")
+      ) {
+        const match = line.match(
+          /\|\s*[\d-]+\s*\|\s*([\d,]+)\s*(?:\([^)]*\))?\s*\|\s*([\d,]+)\s*(?:\([^)]*\))?\s*\|\s*([\d,]+)\s*(?:\([^)]*\))?\s*\|/,
+        )
+        if (match) {
+          previousGithub = parseInt(match[1].replace(/,/g, ""))
+          previousNpm = parseInt(match[2].replace(/,/g, ""))
+          previousTotal = parseInt(match[3].replace(/,/g, ""))
+          break
+        }
+      }
+    }
+  } catch {
+    content =
+      "# Download Stats\n\n| Date | GitHub Downloads | npm Downloads | Total |\n|------|------------------|---------------|-------|\n"
+  }
+
+  const githubChange = githubTotal - previousGithub
+  const npmChange = npmDownloads - previousNpm
+  const totalChange = total - previousTotal
+
+  const githubChangeStr =
+    githubChange > 0
+      ? ` (+${githubChange.toLocaleString()})`
+      : githubChange < 0
+        ? ` (${githubChange.toLocaleString()})`
+        : " (+0)"
+  const npmChangeStr =
+    npmChange > 0
+      ? ` (+${npmChange.toLocaleString()})`
+      : npmChange < 0
+        ? ` (${npmChange.toLocaleString()})`
+        : " (+0)"
+  const totalChangeStr =
+    totalChange > 0
+      ? ` (+${totalChange.toLocaleString()})`
+      : totalChange < 0
+        ? ` (${totalChange.toLocaleString()})`
+        : " (+0)"
+  const line = `| ${date} | ${githubTotal.toLocaleString()}${githubChangeStr} | ${npmDownloads.toLocaleString()}${npmChangeStr} | ${total.toLocaleString()}${totalChangeStr} |\n`
+
+  if (!content.includes("# Download Stats")) {
+    content =
+      "# Download Stats\n\n| Date | GitHub Downloads | npm Downloads | Total |\n|------|------------------|---------------|-------|\n"
+  }
+
+  await Bun.write(file, content + line)
+  await Bun.spawn(["bunx", "prettier", "--write", file]).exited
+
+  console.log(
+    `\nAppended stats to ${file}: GitHub ${githubTotal.toLocaleString()}${githubChangeStr}, npm ${npmDownloads.toLocaleString()}${npmChangeStr}, Total ${total.toLocaleString()}${totalChangeStr}`,
+  )
+}
+
+console.log("Fetching GitHub releases for sst/opencode...\n")
+
+const releases = await fetchReleases()
+console.log(`\nFetched ${releases.length} releases total\n`)
+
+const { total: githubTotal, stats } = calculate(releases)
+
+console.log("Fetching npm all-time downloads for opencode-ai...\n")
+const npmDownloads = await fetchNpmDownloads("opencode-ai")
+console.log(
+  `Fetched npm all-time downloads: ${npmDownloads.toLocaleString()}\n`,
+)
+
+await save(githubTotal, npmDownloads)
+
+const totalDownloads = githubTotal + npmDownloads
+
+console.log("=".repeat(60))
+console.log(`TOTAL DOWNLOADS: ${totalDownloads.toLocaleString()}`)
+console.log(`  GitHub: ${githubTotal.toLocaleString()}`)
+console.log(`  npm: ${npmDownloads.toLocaleString()}`)
+console.log("=".repeat(60))
+
+console.log("\nDownloads by release:")
+console.log("-".repeat(60))
+
+stats
+  .sort((a, b) => b.downloads - a.downloads)
+  .forEach((release) => {
+    console.log(
+      `${release.tag.padEnd(15)} ${release.downloads.toLocaleString().padStart(10)} downloads`,
+    )
+
+    if (release.assets.length > 1) {
+      release.assets
+        .sort((a, b) => b.downloads - a.downloads)
+        .forEach((asset) => {
+          console.log(
+            `  └─ ${asset.name.padEnd(25)} ${asset.downloads.toLocaleString().padStart(8)}`,
+          )
+        })
+    }
+  })
+
+console.log("-".repeat(60))
+console.log(
+  `GitHub Total: ${githubTotal.toLocaleString()} downloads across ${releases.length} releases`,
+)
+console.log(`npm Total: ${npmDownloads.toLocaleString()} downloads`)
+console.log(`Combined Total: ${totalDownloads.toLocaleString()} downloads`)

+ 5 - 1
tsconfig.json

@@ -1 +1,5 @@
-{}
+{
+  "$schema": "https://json.schemastore.org/tsconfig",
+  "extends": "@tsconfig/bun/tsconfig.json",
+  "compilerOptions": {}
+}