feat: add GitHub stars syncing to metrics sync
- Add github.ts to npm-client with functions to fetch GitHub stars - Add parseGitHubUrl to handle various GitHub URL formats - Update metrics sync endpoint to fetch and store GitHub stars - GitHub stars now factor into tool quality score calculation - Optional GITHUB_TOKEN env var for higher API rate limits Closes #8 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
a4cfea5cc3
commit
724bf18822
4 changed files with 156 additions and 3 deletions
|
|
@ -1,5 +1,5 @@
|
|||
import { prisma } from '@tpmjs/db';
|
||||
import { fetchDownloadStats } from '@tpmjs/npm-client';
|
||||
import { fetchDownloadStats, fetchGitHubStarsFromRepository } from '@tpmjs/npm-client';
|
||||
import { type NextRequest, NextResponse } from 'next/server';
|
||||
import { env } from '~/env';
|
||||
|
||||
|
|
@ -44,12 +44,17 @@ export async function POST(request: NextRequest) {
|
|||
// Fetch download stats from NPM (package-level metric)
|
||||
const downloads = await fetchDownloadStats(pkg.npmPackageName);
|
||||
|
||||
// Fetch GitHub stars if repository is available
|
||||
const githubStars = await fetchGitHubStarsFromRepository(
|
||||
pkg.npmRepository as { type?: string; url?: string } | string | null
|
||||
);
|
||||
|
||||
// Update package metrics
|
||||
await prisma.package.update({
|
||||
where: { id: pkg.id },
|
||||
data: {
|
||||
npmDownloadsLastMonth: downloads,
|
||||
// githubStars would be updated here if we had GitHub API integration
|
||||
githubStars,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -58,7 +63,7 @@ export async function POST(request: NextRequest) {
|
|||
const qualityScore = calculateQualityScore({
|
||||
tier: pkg.tier, // Tier is at package level
|
||||
downloads, // Package downloads
|
||||
githubStars: pkg.githubStars || 0, // Package stars
|
||||
githubStars, // Use freshly fetched stars
|
||||
hasParameters: !!tool.parameters,
|
||||
hasReturns: !!tool.returns,
|
||||
hasAiAgent: !!tool.aiAgent,
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
"./search": "./src/search.ts",
|
||||
"./package": "./src/package.ts",
|
||||
"./stats": "./src/stats.ts",
|
||||
"./github": "./src/github.ts",
|
||||
"./rate-limiter": "./src/rate-limiter.ts"
|
||||
},
|
||||
"scripts": {
|
||||
|
|
|
|||
139
packages/npm-client/src/github.ts
Normal file
139
packages/npm-client/src/github.ts
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
/**
|
||||
* GitHub Repository Statistics Client
|
||||
* Fetches star counts from GitHub API
|
||||
*/
|
||||
|
||||
import { z } from 'zod';
|
||||
|
||||
const GITHUB_API_URL = 'https://api.github.com';
|
||||
|
||||
/**
|
||||
* Schema for GitHub repository response
|
||||
*/
|
||||
const GitHubRepoSchema = z.object({
|
||||
stargazers_count: z.number(),
|
||||
full_name: z.string(),
|
||||
});
|
||||
|
||||
export type GitHubRepoResponse = z.infer<typeof GitHubRepoSchema>;
|
||||
|
||||
/**
|
||||
* Parses a GitHub URL or repository string to extract owner and repo
|
||||
* Supports formats:
|
||||
* - https://github.com/owner/repo
|
||||
* - https://github.com/owner/repo.git
|
||||
* - git://github.com/owner/repo.git
|
||||
* - git+https://github.com/owner/repo.git
|
||||
* - github:owner/repo
|
||||
* - { url: "..." } object format
|
||||
*/
|
||||
export function parseGitHubUrl(
|
||||
repository: string | { type?: string; url?: string } | null | undefined
|
||||
): { owner: string; repo: string } | null {
|
||||
if (!repository) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let url: string;
|
||||
|
||||
if (typeof repository === 'object') {
|
||||
if (!repository.url) {
|
||||
return null;
|
||||
}
|
||||
url = repository.url;
|
||||
} else {
|
||||
url = repository;
|
||||
}
|
||||
|
||||
// Handle github: shorthand
|
||||
if (url.startsWith('github:')) {
|
||||
const parts = url.replace('github:', '').split('/');
|
||||
const owner = parts[0];
|
||||
const repo = parts[1];
|
||||
if (owner && repo) {
|
||||
return { owner, repo: repo.replace('.git', '') };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Handle various GitHub URL formats
|
||||
const githubRegex = /github\.com[/:]([\w.-]+)\/([\w.-]+?)(?:\.git)?(?:\/|$)/i;
|
||||
const match = url.match(githubRegex);
|
||||
|
||||
if (match?.[1] && match[2]) {
|
||||
return { owner: match[1], repo: match[2] };
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches star count for a GitHub repository
|
||||
* @param owner - Repository owner (user or organization)
|
||||
* @param repo - Repository name
|
||||
* @returns Star count, or 0 if not found or on error
|
||||
*/
|
||||
export async function fetchGitHubStars(owner: string, repo: string): Promise<number> {
|
||||
const url = `${GITHUB_API_URL}/repos/${owner}/${repo}`;
|
||||
|
||||
try {
|
||||
const headers: Record<string, string> = {
|
||||
Accept: 'application/vnd.github.v3+json',
|
||||
'User-Agent': 'tpmjs-registry',
|
||||
};
|
||||
|
||||
// Add GitHub token if available for higher rate limits
|
||||
const token = process.env.GITHUB_TOKEN;
|
||||
if (token) {
|
||||
headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
const response = await fetch(url, { headers });
|
||||
|
||||
if (response.status === 404) {
|
||||
// Repository not found
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (response.status === 403) {
|
||||
// Rate limited
|
||||
console.warn('GitHub API rate limited');
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
console.error(`GitHub API error: ${response.status} ${response.statusText}`);
|
||||
return 0;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const parsed = GitHubRepoSchema.safeParse(data);
|
||||
|
||||
if (!parsed.success) {
|
||||
console.error('Failed to parse GitHub response:', parsed.error);
|
||||
return 0;
|
||||
}
|
||||
|
||||
return parsed.data.stargazers_count;
|
||||
} catch (error) {
|
||||
console.error(`Failed to fetch GitHub stars for ${owner}/${repo}:`, error);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches star count from a repository URL or object
|
||||
* @param repository - Repository URL string or object with url property
|
||||
* @returns Star count, or 0 if not a GitHub repo or on error
|
||||
*/
|
||||
export async function fetchGitHubStarsFromRepository(
|
||||
repository: string | { type?: string; url?: string } | null | undefined
|
||||
): Promise<number> {
|
||||
const parsed = parseGitHubUrl(repository);
|
||||
|
||||
if (!parsed) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return fetchGitHubStars(parsed.owner, parsed.repo);
|
||||
}
|
||||
|
|
@ -36,5 +36,13 @@ export {
|
|||
// Download statistics
|
||||
export { fetchDownloadStats, fetchBulkDownloadStats, type DownloadsResponse } from './stats';
|
||||
|
||||
// GitHub statistics
|
||||
export {
|
||||
fetchGitHubStars,
|
||||
fetchGitHubStarsFromRepository,
|
||||
parseGitHubUrl,
|
||||
type GitHubRepoResponse,
|
||||
} from './github';
|
||||
|
||||
// Rate limiting
|
||||
export { delay, RateLimiter, npmRateLimiter, retryWithBackoff } from './rate-limiter';
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue