From 2d9ed06367516daa184ecf70bb0b2451c06da8d1 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Thu, 31 Jul 2025 01:25:24 -0400 Subject: ci: scripts --- .github/workflows/stats.yml | 2 +- package.json | 2 +- script/hooks | 15 ++++ script/hooks.bat | 16 ++++ script/publish.ts | 28 ++++++ script/release | 42 +++++++++ script/stats.ts | 204 ++++++++++++++++++++++++++++++++++++++++++++ scripts/hooks | 15 ---- scripts/hooks.bat | 16 ---- scripts/publish.ts | 28 ------ scripts/release | 42 --------- scripts/stats.ts | 204 -------------------------------------------- 12 files changed, 307 insertions(+), 307 deletions(-) create mode 100755 script/hooks create mode 100644 script/hooks.bat create mode 100755 script/publish.ts create mode 100755 script/release create mode 100755 script/stats.ts delete mode 100755 scripts/hooks delete mode 100644 scripts/hooks.bat delete mode 100755 scripts/publish.ts delete mode 100755 scripts/release delete mode 100755 scripts/stats.ts diff --git a/.github/workflows/stats.yml b/.github/workflows/stats.yml index 188996aa9..8ad02d251 100644 --- a/.github/workflows/stats.yml +++ b/.github/workflows/stats.yml @@ -21,7 +21,7 @@ jobs: bun-version: latest - name: Run stats script - run: bun scripts/stats.ts + run: bun script/stats.ts - name: Commit stats run: | diff --git a/package.json b/package.json index d1e51278d..3120b1f8f 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "dev": "bun run packages/opencode/src/index.ts", "typecheck": "bun run --filter='*' typecheck", "stainless": "./scripts/stainless", - "postinstall": "./scripts/hooks" + "postinstall": "./script/hooks" }, "workspaces": { "packages": [ diff --git a/script/hooks b/script/hooks new file mode 100755 index 000000000..4597c6f41 --- /dev/null +++ b/script/hooks @@ -0,0 +1,15 @@ +#!/bin/sh + +if [ ! -d ".git" ]; then + exit 0 +fi + +mkdir -p .git/hooks + +cat > .git/hooks/pre-push << 'EOF' +#!/bin/sh +bun run typecheck +EOF + +chmod +x .git/hooks/pre-push +echo "✅ Pre-push hook installed" diff --git a/script/hooks.bat b/script/hooks.bat new file mode 100644 index 000000000..52d690100 --- /dev/null +++ b/script/hooks.bat @@ -0,0 +1,16 @@ +@echo off + +if not exist ".git" ( + exit /b 0 +) + +if not exist ".git\hooks" ( + mkdir ".git\hooks" +) + +( + echo #!/bin/sh + echo bun run typecheck +) > ".git\hooks\pre-push" + +echo ✅ Pre-push hook installed diff --git a/script/publish.ts b/script/publish.ts new file mode 100755 index 000000000..13716b014 --- /dev/null +++ b/script/publish.ts @@ -0,0 +1,28 @@ +#!/usr/bin/env bun + +import { $ } from "bun" + +const snapshot = process.env["OPENCODE_SNAPSHOT"] === "true" +const version = snapshot + ? `0.0.0-${new Date().toISOString().slice(0, 16).replace(/[-:T]/g, "")}` + : process.env["OPENCODE_VERSION"] +if (!version) { + throw new Error("OPENCODE_VERSION is required") +} +process.env["OPENCODE_VERSION"] = version + +await import(`../packages/opencode/script/publish.ts`) +await import(`../packages/sdk/stainless/generate.ts`) +await import(`../packages/sdk/js/script/publish.ts`) + +if (!snapshot) { + await $`git commit -am "Release v${version}"` + await $`git tag v${version}` + await $`git push origin HEAD --tags` +} +if (snapshot) { + await $`git commit --allow-empty -m "Snapshot release v${version}"` + await $`git tag v${version}` + await $`git push origin v${version}` + await $`git reset --soft HEAD~1` +} diff --git a/script/release b/script/release new file mode 100755 index 000000000..631cd5a7b --- /dev/null +++ b/script/release @@ -0,0 +1,42 @@ +#!/usr/bin/env bash + +# Parse command line arguments +minor=false +while [ "$#" -gt 0 ]; do + case "$1" in + --minor) minor=true; shift 1;; + *) echo "Unknown parameter: $1"; exit 1;; + esac +done + +# Get the latest release from GitHub +latest_tag=$(gh release list --limit 1 --json tagName --jq '.[0].tagName') + +# If there is no tag, exit the script +if [ -z "$latest_tag" ]; then + echo "No tags found" + exit 1 +fi + +echo "Latest tag: $latest_tag" + +# Remove the 'v' prefix and split into major, minor, and patch numbers +version_without_v=${latest_tag#v} +IFS='.' read -ra VERSION <<< "$version_without_v" + +if [ "$minor" = true ]; then + # Increment the minor version and reset patch to 0 + minor_number=${VERSION[1]} + let "minor_number++" + new_version="${VERSION[0]}.$minor_number.0" +else + # Increment the patch version + patch_number=${VERSION[2]} + let "patch_number++" + new_version="${VERSION[0]}.${VERSION[1]}.$patch_number" +fi + +echo "New version: $new_version" + +gh workflow run publish.yml -f version="$new_version" + diff --git a/script/stats.ts b/script/stats.ts new file mode 100755 index 000000000..bce211855 --- /dev/null +++ b/script/stats.ts @@ -0,0 +1,204 @@ +#!/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 { + 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 { + 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++ + await new Promise((resolve) => setTimeout(resolve, 1000)) + } + + 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`) diff --git a/scripts/hooks b/scripts/hooks deleted file mode 100755 index 4597c6f41..000000000 --- a/scripts/hooks +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/sh - -if [ ! -d ".git" ]; then - exit 0 -fi - -mkdir -p .git/hooks - -cat > .git/hooks/pre-push << 'EOF' -#!/bin/sh -bun run typecheck -EOF - -chmod +x .git/hooks/pre-push -echo "✅ Pre-push hook installed" diff --git a/scripts/hooks.bat b/scripts/hooks.bat deleted file mode 100644 index 52d690100..000000000 --- a/scripts/hooks.bat +++ /dev/null @@ -1,16 +0,0 @@ -@echo off - -if not exist ".git" ( - exit /b 0 -) - -if not exist ".git\hooks" ( - mkdir ".git\hooks" -) - -( - echo #!/bin/sh - echo bun run typecheck -) > ".git\hooks\pre-push" - -echo ✅ Pre-push hook installed diff --git a/scripts/publish.ts b/scripts/publish.ts deleted file mode 100755 index 13716b014..000000000 --- a/scripts/publish.ts +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env bun - -import { $ } from "bun" - -const snapshot = process.env["OPENCODE_SNAPSHOT"] === "true" -const version = snapshot - ? `0.0.0-${new Date().toISOString().slice(0, 16).replace(/[-:T]/g, "")}` - : process.env["OPENCODE_VERSION"] -if (!version) { - throw new Error("OPENCODE_VERSION is required") -} -process.env["OPENCODE_VERSION"] = version - -await import(`../packages/opencode/script/publish.ts`) -await import(`../packages/sdk/stainless/generate.ts`) -await import(`../packages/sdk/js/script/publish.ts`) - -if (!snapshot) { - await $`git commit -am "Release v${version}"` - await $`git tag v${version}` - await $`git push origin HEAD --tags` -} -if (snapshot) { - await $`git commit --allow-empty -m "Snapshot release v${version}"` - await $`git tag v${version}` - await $`git push origin v${version}` - await $`git reset --soft HEAD~1` -} diff --git a/scripts/release b/scripts/release deleted file mode 100755 index 631cd5a7b..000000000 --- a/scripts/release +++ /dev/null @@ -1,42 +0,0 @@ -#!/usr/bin/env bash - -# Parse command line arguments -minor=false -while [ "$#" -gt 0 ]; do - case "$1" in - --minor) minor=true; shift 1;; - *) echo "Unknown parameter: $1"; exit 1;; - esac -done - -# Get the latest release from GitHub -latest_tag=$(gh release list --limit 1 --json tagName --jq '.[0].tagName') - -# If there is no tag, exit the script -if [ -z "$latest_tag" ]; then - echo "No tags found" - exit 1 -fi - -echo "Latest tag: $latest_tag" - -# Remove the 'v' prefix and split into major, minor, and patch numbers -version_without_v=${latest_tag#v} -IFS='.' read -ra VERSION <<< "$version_without_v" - -if [ "$minor" = true ]; then - # Increment the minor version and reset patch to 0 - minor_number=${VERSION[1]} - let "minor_number++" - new_version="${VERSION[0]}.$minor_number.0" -else - # Increment the patch version - patch_number=${VERSION[2]} - let "patch_number++" - new_version="${VERSION[0]}.${VERSION[1]}.$patch_number" -fi - -echo "New version: $new_version" - -gh workflow run publish.yml -f version="$new_version" - diff --git a/scripts/stats.ts b/scripts/stats.ts deleted file mode 100755 index bce211855..000000000 --- a/scripts/stats.ts +++ /dev/null @@ -1,204 +0,0 @@ -#!/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 { - 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 { - 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++ - await new Promise((resolve) => setTimeout(resolve, 1000)) - } - - 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`) -- cgit v1.2.3