combines reddit digest, github trending, new ai repos, and claude code release tracking into one CLI tool. usage: bun run feed <reddit|trending|new-repos|claude-releases|all>
71 lines
1.8 KiB
TypeScript
71 lines
1.8 KiB
TypeScript
export function daysAgo(n: number): string {
|
|
const d = new Date();
|
|
d.setDate(d.getDate() - n);
|
|
return d.toISOString().split('T')[0];
|
|
}
|
|
|
|
export function log(...args: unknown[]) {
|
|
console.error(...args);
|
|
}
|
|
|
|
export function sleep(ms: number): Promise<void> {
|
|
return new Promise((r) => setTimeout(r, ms));
|
|
}
|
|
|
|
export async function ghSearch(query: string): Promise<GHRepo[]> {
|
|
const url =
|
|
`https://api.github.com/search/repositories` +
|
|
`?q=${encodeURIComponent(query)}&sort=stars&order=desc&per_page=30`;
|
|
log(` fetching: ${query}`);
|
|
|
|
const res = await fetch(url, {
|
|
headers: { Accept: 'application/vnd.github.v3+json' },
|
|
});
|
|
|
|
if (!res.ok) {
|
|
log(` ⚠ GitHub API ${res.status}: ${await res.text()}`);
|
|
return [];
|
|
}
|
|
|
|
const data = (await res.json()) as { items: GHRepo[] };
|
|
return data.items ?? [];
|
|
}
|
|
|
|
export interface GHRepo {
|
|
full_name: string;
|
|
description: string | null;
|
|
stargazers_count: number;
|
|
language: string | null;
|
|
html_url: string;
|
|
created_at: string;
|
|
pushed_at: string;
|
|
topics?: string[];
|
|
}
|
|
|
|
export function formatRepos(repos: GHRepo[], title: string): string {
|
|
if (!repos.length) return `## ${title}\n\n_No results found._\n`;
|
|
|
|
const lines = [`## ${title}\n`];
|
|
for (const r of repos) {
|
|
const desc = r.description
|
|
? r.description.slice(0, 120)
|
|
: '_No description_';
|
|
const lang = r.language ?? '?';
|
|
lines.push(
|
|
`**[${r.full_name}](${r.html_url})** ` +
|
|
`⭐ ${r.stargazers_count.toLocaleString()} | \`${lang}\``
|
|
);
|
|
lines.push(`> ${desc}\n`);
|
|
}
|
|
return lines.join('\n');
|
|
}
|
|
|
|
export function dedupeRepos(repos: GHRepo[]): GHRepo[] {
|
|
const seen = new Set<string>();
|
|
return repos.filter((r) => {
|
|
if (seen.has(r.full_name)) return false;
|
|
seen.add(r.full_name);
|
|
return true;
|
|
});
|
|
}
|