From 193c6bf6371f4bf91eba381e9a0bb7d8bf94dc14 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 15 Apr 2026 10:58:54 +0000 Subject: [PATCH] feat(scripts): add github-traffic.mjs for repo traffic insights Zero-dep Node script that fetches the four GitHub Traffic endpoints (views, clones, popular referrers, popular paths) plus core repo stats, and prints a formatted report or raw JSON. Supports --save to append JSONL snapshots so daily runs can build a >14-day history beyond the GitHub API's retention window. Wired as `npm run traffic`. Requires GITHUB_TOKEN with push access. --- package.json | 3 +- scripts/github-traffic.mjs | 185 +++++++++++++++++++++++++++++++++++++ 2 files changed, 187 insertions(+), 1 deletion(-) create mode 100644 scripts/github-traffic.mjs diff --git a/package.json b/package.json index e324dbd..29d51c5 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,8 @@ }, "scripts": { "start": "node server.mjs", - "setup": "node setup.mjs" + "setup": "node setup.mjs", + "traffic": "node scripts/github-traffic.mjs" }, "keywords": [ "openclaw", diff --git a/scripts/github-traffic.mjs b/scripts/github-traffic.mjs new file mode 100644 index 0000000..f91efcc --- /dev/null +++ b/scripts/github-traffic.mjs @@ -0,0 +1,185 @@ +#!/usr/bin/env node +// github-traffic.mjs — fetch GitHub Traffic Insights for this repo. +// +// Usage: +// GITHUB_TOKEN=ghp_xxx node scripts/github-traffic.mjs # pretty report +// GITHUB_TOKEN=ghp_xxx node scripts/github-traffic.mjs --json # raw JSON +// GITHUB_TOKEN=ghp_xxx node scripts/github-traffic.mjs --save # write snapshot file +// +// Options: +// --owner= Override repo owner (default: dtzp555-max) +// --repo= Override repo name (default: ocp) +// --json Print raw JSON instead of a formatted report +// --save[=path] Append snapshot as JSONL to path (default: ./traffic-history.jsonl) +// +// Requires a token with push access to the repository. Traffic endpoints +// return the last 14 days of data — run daily to build a longer history. + +const API = "https://api.github.com"; + +function parseArgs(argv) { + const args = { owner: "dtzp555-max", repo: "ocp", json: false, save: null }; + for (const a of argv.slice(2)) { + if (a === "--json") args.json = true; + else if (a === "--save") args.save = "traffic-history.jsonl"; + else if (a.startsWith("--save=")) args.save = a.slice(7); + else if (a.startsWith("--owner=")) args.owner = a.slice(8); + else if (a.startsWith("--repo=")) args.repo = a.slice(7); + else if (a === "-h" || a === "--help") { printHelp(); process.exit(0); } + else { console.error(`Unknown argument: ${a}`); process.exit(2); } + } + return args; +} + +function printHelp() { + console.log(`github-traffic.mjs — fetch GitHub Traffic Insights + +Usage: + GITHUB_TOKEN= node scripts/github-traffic.mjs [options] + +Options: + --owner= Repo owner (default: dtzp555-max) + --repo= Repo name (default: ocp) + --json Print raw JSON (for piping to jq) + --save[=path] Append snapshot as JSONL (default: ./traffic-history.jsonl) + -h, --help Show this help + +Requires GITHUB_TOKEN with push access to the repository.`); +} + +async function gh(path, token) { + const res = await fetch(API + path, { + headers: { + "Authorization": `Bearer ${token}`, + "Accept": "application/vnd.github+json", + "X-GitHub-Api-Version": "2022-11-28", + "User-Agent": "ocp-traffic-script", + }, + }); + if (!res.ok) { + const body = await res.text(); + throw new Error(`GET ${path} → ${res.status} ${res.statusText}: ${body.slice(0, 200)}`); + } + return res.json(); +} + +async function fetchAll(owner, repo, token) { + const base = `/repos/${owner}/${repo}`; + const [repoInfo, views, clones, referrers, paths] = await Promise.all([ + gh(base, token), + gh(`${base}/traffic/views`, token), + gh(`${base}/traffic/clones`, token), + gh(`${base}/traffic/popular/referrers`, token), + gh(`${base}/traffic/popular/paths`, token), + ]); + return { + fetched_at: new Date().toISOString(), + repo: { + full_name: repoInfo.full_name, + stars: repoInfo.stargazers_count, + watchers: repoInfo.subscribers_count, + forks: repoInfo.forks_count, + open_issues: repoInfo.open_issues_count, + pushed_at: repoInfo.pushed_at, + }, + views, clones, referrers, paths, + }; +} + +function bar(value, max, width = 20) { + if (!max) return ""; + const n = Math.round((value / max) * width); + return "█".repeat(n) + "░".repeat(width - n); +} + +function formatReport(data) { + const { repo, views, clones, referrers, paths } = data; + const lines = []; + lines.push(`\n📊 GitHub Traffic — ${repo.full_name}`); + lines.push(` ⭐ ${repo.stars} 👁 ${repo.watchers ?? "?"} 🍴 ${repo.forks} 🐛 ${repo.open_issues} open issues`); + lines.push(` Last push: ${repo.pushed_at}`); + lines.push(` Fetched: ${data.fetched_at}\n`); + + lines.push(`── Views (last 14 days) ────────────────────────────────────`); + lines.push(` Total: ${views.count} Unique: ${views.uniques}`); + const maxV = Math.max(1, ...views.views.map(v => v.count)); + for (const v of views.views) { + const day = v.timestamp.slice(0, 10); + lines.push(` ${day} ${String(v.count).padStart(4)} (${String(v.uniques).padStart(3)} uniq) ${bar(v.count, maxV)}`); + } + + lines.push(`\n── Clones (last 14 days) ───────────────────────────────────`); + lines.push(` Total: ${clones.count} Unique: ${clones.uniques}`); + if (clones.clones.length) { + const maxC = Math.max(1, ...clones.clones.map(c => c.count)); + for (const c of clones.clones) { + const day = c.timestamp.slice(0, 10); + lines.push(` ${day} ${String(c.count).padStart(4)} (${String(c.uniques).padStart(3)} uniq) ${bar(c.count, maxC)}`); + } + } else { + lines.push(` (no clones recorded)`); + } + + lines.push(`\n── Top Referrers ───────────────────────────────────────────`); + if (referrers.length) { + const maxR = Math.max(1, ...referrers.map(r => r.count)); + for (const r of referrers) { + lines.push(` ${r.referrer.padEnd(28)} ${String(r.count).padStart(5)} views (${r.uniques} uniq) ${bar(r.count, maxR, 15)}`); + } + } else { + lines.push(` (no referrer data)`); + } + + lines.push(`\n── Popular Content ─────────────────────────────────────────`); + if (paths.length) { + const maxP = Math.max(1, ...paths.map(p => p.count)); + for (const p of paths) { + lines.push(` ${String(p.count).padStart(5)} views ${String(p.uniques).padStart(4)} uniq ${p.path}`); + lines.push(` ${bar(p.count, maxP, 40)} ${p.title.slice(0, 60)}`); + } + } else { + lines.push(` (no popular content data)`); + } + lines.push(""); + return lines.join("\n"); +} + +async function main() { + const args = parseArgs(process.argv); + const token = process.env.GITHUB_TOKEN; + if (!token) { + console.error("Error: GITHUB_TOKEN environment variable is required."); + console.error("Create a token with 'repo' scope: https://github.com/settings/tokens"); + process.exit(1); + } + + let data; + try { + data = await fetchAll(args.owner, args.repo, token); + } catch (err) { + console.error(`Failed to fetch traffic: ${err.message}`); + if (err.message.includes("403")) { + console.error("Note: traffic endpoints require push access to the repository."); + } + process.exit(1); + } + + if (args.save) { + const { writeFileSync, appendFileSync, existsSync } = await import("node:fs"); + const line = JSON.stringify(data) + "\n"; + if (existsSync(args.save)) appendFileSync(args.save, line); + else writeFileSync(args.save, line); + console.error(`Snapshot appended to ${args.save}`); + } + + if (args.json) { + console.log(JSON.stringify(data, null, 2)); + } else { + console.log(formatReport(data)); + } +} + +main().catch(err => { + console.error(err); + process.exit(1); +});