Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 38 additions & 23 deletions scripts/fetch-repo-websites.mjs
Original file line number Diff line number Diff line change
@@ -1,31 +1,34 @@
// Regenerates src/generated/repoWebsites.json: the homepage URL each
// registry repo declares in its GitHub metadata, keyed by fullName. Runs as
// the `prebuild` hook so every build ships whatever the projects currently
// point their GitHub "website" field at — no hand-edited list to go stale.
// Regenerates two snapshots of each registry repo's GitHub metadata, keyed
// by fullName, from a single GitHub fetch per repo:
// - src/generated/repoWebsites.json — the declared homepage URL
// - src/generated/repoDescriptions.json — the repo's one-line description
// Runs as the `prebuild` hook so every build ships whatever the projects
// currently declare — no hand-edited lists to go stale.
//
// Fail-safe by design: any fetch problem (registry down, GitHub rate limit)
// leaves the committed snapshot untouched and exits 0, so a build can never
// break — worst case the previews are as fresh as the last successful run.
// leaves the committed snapshots untouched and exits 0, so a build can never
// break — worst case the data is as fresh as the last successful run.
// Set GITHUB_TOKEN (GitHub Actions' built-in token works) to lift the
// unauthenticated 60 req/hr GitHub API limit shared by CI runner IPs.
import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';

const OUT_PATH = join(
const GENERATED_DIR = join(
dirname(fileURLToPath(import.meta.url)),
'..',
'src',
'generated',
'repoWebsites.json',
);
const WEBSITES_PATH = join(GENERATED_DIR, 'repoWebsites.json');
const DESCRIPTIONS_PATH = join(GENERATED_DIR, 'repoDescriptions.json');

const API_BASE =
process.env.VITE_REACT_APP_BASE_URL || 'https://api.gittensor.io';

const readSnapshot = () => {
const readSnapshot = (path) => {
try {
return JSON.parse(readFileSync(OUT_PATH, 'utf8'));
return JSON.parse(readFileSync(path, 'utf8'));
} catch {
return {};
}
Expand All @@ -41,7 +44,8 @@ const githubHeaders = process.env.GITHUB_TOKEN
? { Authorization: `Bearer ${process.env.GITHUB_TOKEN}` }
: {};

const snapshot = readSnapshot();
const websitesSnapshot = readSnapshot(WEBSITES_PATH);
const descriptionsSnapshot = readSnapshot(DESCRIPTIONS_PATH);

let registry;
try {
Expand All @@ -50,12 +54,13 @@ try {
if (!Array.isArray(registry)) throw new Error('unexpected registry shape');
} catch (error) {
console.warn(
`repo-websites: registry fetch failed, keeping snapshot (${error.message})`,
`repo-websites: registry fetch failed, keeping snapshots (${error.message})`,
);
process.exit(0);
}

const websites = {};
const descriptions = {};
let failures = 0;
for (const repo of registry) {
const fullName = repo.fullName;
Expand All @@ -67,24 +72,34 @@ for (const repo of registry) {
);
const homepage = (meta.homepage || '').trim();
if (/^https?:\/\//i.test(homepage)) websites[fullName] = homepage;
const description = (meta.description || '').trim();
if (description) descriptions[fullName] = description;
} catch (error) {
failures += 1;
console.warn(`repo-websites: ${fullName} fetch failed (${error.message})`);
if (snapshot[fullName]) websites[fullName] = snapshot[fullName];
if (websitesSnapshot[fullName])
websites[fullName] = websitesSnapshot[fullName];
if (descriptionsSnapshot[fullName])
descriptions[fullName] = descriptionsSnapshot[fullName];
}
}

// A blanket failure (e.g. rate limit) must not wipe the snapshot wholesale.
// A blanket failure (e.g. rate limit) must not wipe the snapshots wholesale.
if (failures === registry.length) {
console.warn('repo-websites: every GitHub fetch failed, keeping snapshot');
console.warn('repo-websites: every GitHub fetch failed, keeping snapshots');
process.exit(0);
}

const sorted = Object.fromEntries(
Object.entries(websites).sort(([a], [b]) => a.localeCompare(b)),
);
mkdirSync(dirname(OUT_PATH), { recursive: true });
writeFileSync(OUT_PATH, `${JSON.stringify(sorted, null, 2)}\n`);
console.log(
`repo-websites: wrote ${Object.keys(sorted).length} entries (${failures} fetch failures)`,
);
const writeSorted = (path, entries, label) => {
const sorted = Object.fromEntries(
Object.entries(entries).sort(([a], [b]) => a.localeCompare(b)),
);
writeFileSync(path, `${JSON.stringify(sorted, null, 2)}\n`);
console.log(
`repo-websites: wrote ${Object.keys(sorted).length} ${label} (${failures} fetch failures)`,
);
};

mkdirSync(GENERATED_DIR, { recursive: true });
writeSorted(WEBSITES_PATH, websites, 'websites');
writeSorted(DESCRIPTIONS_PATH, descriptions, 'descriptions');
16 changes: 16 additions & 0 deletions src/generated/repoDescriptions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"Autovara/kata": "Kata builds the best AI agent for a subnet through open competition — so anyone can mine that subnet with a proven, optimized agent.",
"entrius/das-github-mirror": "Mirror api for all tracked gittensor repositories",
"Geniepod/genie-claw": "🦞 Low-latency, limited-context AI harness for private on-device homes.",
"gittensor-ai-lab/sparkinfer": "Fastest MoE/LLM inference runtime for consumer and edge Blackwell GPUs. SN74 on Gittensor.",
"gittensor-model-hub/SparkDistill": "Trustless Triton-native AI distillation on SN74/Gittensor: verified datasets (SparkProof), training recipes, and eval harness for kernel-specialist LLMs on Blackwell.",
"gittensor-vanguard/vanguarstew": "An SN74 repo-maintainer agent + a GitHub-history benchmark that scores it — can an agent make the maintainer decisions a strong maintainer would?",
"JSONbored/awesome-claude": "HeyClaude is a curated registry and distribution surface for Claude and AI-workflow assets: agents, MCP servers, skills, commands, hooks, rules, guides, tools, jobs, Raycast feeds, static data exports, and an npm MCP package.",
"JSONbored/loopover": "Backend intelligence and MCP tooling for Gittensor contributors and maintainers.",
"JSONbored/metagraphed": "Operational metadata, health, schemas, and public interface discovery for Bittensor subnets.",
"mini-router/minirouter": "SN74 Gittensor miner workspace for MiniRouter: router training/eval code, benchmark configs, submission artifacts, and the web competition site for improving the tiny LLM router.",
"phase-rs/phase": "A rules engine and game client — Rust + WASM + React",
"vouchdev/vouch": "A git-native, review-gated knowledge base for AI agents: they propose writes, you approve them. Every claim cites a source, every change is a diff in your repo. MCP + CLI.",
"we-promise/sure": "The personal finance app for everyone (by everyone)",
"zeokin/Cuda-Compute-OSS": "CCO is an open-source system for managing, validating, and improving GPU."
}
6 changes: 3 additions & 3 deletions src/generated/repoWebsites.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
{
"Autovara/kata": "https://dashboardking.ngrok.app/",
"Geniepod/genie-claw": "https://genieclaw.org",
"gittensor-ai-lab/sparkinfer": "https://gittensor-ai-lab.github.io/sparkinfer/dashboard/",
"gittensor-agent-forge/gt-imagent": "https://tryimagent.com/",
"gittensor-ai-lab/sparkinfer": "https://sparkinfer.com/",
"gittensor-model-hub/SparkDistill": "https://gittensor-model-hub.github.io/SparkDistill/",
"gittensor-vanguard/vanguarstew": "https://gittensor-vanguard.github.io/vanguarstew/",
"imagent-ai/imagent": "https://tryimagent.com/",
"James-CUDA/Gittensor-TinyRouter": "https://james-cuda.github.io/Gittensor-TinyRouter/",
"JSONbored/awesome-claude": "https://heyclau.de",
"JSONbored/gittensory": "https://gittensory.aethereal.dev/",
"JSONbored/loopover": "https://loopover.ai",
"JSONbored/metagraphed": "https://metagraph.sh",
"mini-router/minirouter": "https://mini-router.github.io/minirouter/",
"phase-rs/phase": "http://preview.phase-rs.dev/",
Expand Down
Loading
Loading