#!/usr/bin/env bunasync function sendToPostHog(event: string, properties: Record<string, any>) {const key = process.env["POSTHOG_KEY"]if (!key) {console.warn("POSTHOG_API_KEY not set, skipping PostHog event")return}const response = await fetch("https://us.i.posthog.com/i/v0/e/", {method: "POST",headers: {"Content-Type": "application/json",},body: JSON.stringify({distinct_id: "download",api_key: key,event,properties: {...properties,},}),}).catch(() => null)if (response && !response.ok) {console.warn(`PostHog API error: ${response.status}`)}}interface Asset {name: stringdownload_count: number}interface Release {tag_name: stringname: stringassets: Asset[]}interface NpmDownloadsRange {start: stringend: stringpackage: stringdownloads: Array<{downloads: numberday: string}>}async function fetchNpmDownloads(packageName: string): Promise<number> {try {// Use a range from 2020 to current year + 5 years to ensure it works foreverconst currentYear = new Date().getFullYear()const endYear = currentYear + 5const 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 = 1const per = 100while (true) {const url = `https://api.github.com/repos/anomalyco/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) breakreleases.push(...batch)console.log(`Fetched page ${page} with ${batch.length} releases`)if (batch.length < per) breakpage++await new Promise((resolve) => setTimeout(resolve, 1000))}return releases}function calculate(releases: Release[]) {let total = 0const stats = []for (const release of releases) {let downloads = 0const assets = []for (const asset of release.assets) {downloads += asset.download_countassets.push({name: asset.name,downloads: asset.download_count,})}total += downloadsstats.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 + npmDownloadslet previousGithub = 0let previousNpm = 0let previousTotal = 0let 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 - previousGithubconst npmChange = npmDownloads - previousNpmconst totalChange = total - previousTotalconst 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]).exitedconsole.log(`\nAppended stats to ${file}: GitHub ${githubTotal.toLocaleString()}${githubChangeStr}, npm ${npmDownloads.toLocaleString()}${npmChangeStr}, Total ${total.toLocaleString()}${totalChangeStr}`,)}console.log("Fetching GitHub releases for anomalyco/opencode...\n")const releases = await fetchReleases()console.log(`\nFetched ${releases.length} releases total\n`)const { total: githubTotal } = 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)await sendToPostHog("download", {count: githubTotal,source: "github",})await sendToPostHog("download", {count: npmDownloads,source: "npm",})const totalDownloads = githubTotal + npmDownloadsconsole.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("-".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`)
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。