github api

This commit is contained in:
Chris Wanstrath 2025-07-30 13:29:42 -07:00
parent eac2e4e009
commit c6977cd760
4 changed files with 208 additions and 11 deletions

View File

@ -0,0 +1 @@
GITHUB_TOKEN=your_github_token_here

111
packages/cubby/CLAUDE.md Normal file
View File

@ -0,0 +1,111 @@
---
description: Use Bun instead of Node.js, npm, pnpm, or vite.
globs: "*.ts, *.tsx, *.html, *.css, *.js, *.jsx, package.json"
alwaysApply: false
---
Default to using Bun instead of Node.js.
- Use `bun <file>` instead of `node <file>` or `ts-node <file>`
- Use `bun test` instead of `jest` or `vitest`
- Use `bun build <file.html|file.ts|file.css>` instead of `webpack` or `esbuild`
- Use `bun install` instead of `npm install` or `yarn install` or `pnpm install`
- Use `bun run <script>` instead of `npm run <script>` or `yarn run <script>` or `pnpm run <script>`
- Bun automatically loads .env, so don't use dotenv.
## APIs
- `Bun.serve()` supports WebSockets, HTTPS, and routes. Don't use `express`.
- `bun:sqlite` for SQLite. Don't use `better-sqlite3`.
- `Bun.redis` for Redis. Don't use `ioredis`.
- `Bun.sql` for Postgres. Don't use `pg` or `postgres.js`.
- `WebSocket` is built-in. Don't use `ws`.
- Prefer `Bun.file` over `node:fs`'s readFile/writeFile
- Bun.$`ls` instead of execa.
## Testing
Use `bun test` to run tests.
```ts#index.test.ts
import { test, expect } from "bun:test";
test("hello world", () => {
expect(1).toBe(1);
});
```
## Frontend
Use HTML imports with `Bun.serve()`. Don't use `vite`. HTML imports fully support React, CSS, Tailwind.
Server:
```ts#index.ts
import index from "./index.html"
Bun.serve({
routes: {
"/": index,
"/api/users/:id": {
GET: (req) => {
return new Response(JSON.stringify({ id: req.params.id }));
},
},
},
// optional websocket support
websocket: {
open: (ws) => {
ws.send("Hello, world!");
},
message: (ws, message) => {
ws.send(message);
},
close: (ws) => {
// handle close
}
},
development: {
hmr: true,
console: true,
}
})
```
HTML files can import .tsx, .jsx or .js files directly and Bun's bundler will transpile & bundle automatically. `<link>` tags can point to stylesheets and Bun's CSS bundler will bundle.
```html#index.html
<html>
<body>
<h1>Hello, world!</h1>
<script type="module" src="./frontend.tsx"></script>
</body>
</html>
```
With the following `frontend.tsx`:
```tsx#frontend.tsx
import React from "react";
// import .css files directly and it works
import './index.css';
import { createRoot } from "react-dom/client";
const root = createRoot(document.body);
export default function Frontend() {
return <h1>Hello, world!</h1>;
}
root.render(<Frontend />);
```
Then, run index.ts
```sh
bun --hot ./index.ts
```
For more information, read the Bun API docs in `node_modules/bun-types/docs/**.md`.

View File

View File

@ -1,7 +1,6 @@
import { Hono } from "hono"
import { serveStatic } from "hono/bun"
import { render } from "preact-render-to-string"
import { $ } from "bun"
import { readdirSync } from "fs"
import { mkdir, readdir } from 'node:fs/promises'
import { basename, join } from 'path'
@ -141,7 +140,15 @@ app.delete('/p/:id/delete/:filename', async c => {
async function projects(): Promise<string[]> {
const subdirs = readdirSync(PROJECTS_DIR, { withFileTypes: true })
.filter((entry: any) => entry.isDirectory())
.map((entry: any) => entry.name).sort()
.map((entry: any) => entry.name)
.filter(name => {
try {
return Bun.file(`${PROJECTS_DIR}/${name}/package.json`).size > 0
} catch {
return false
}
})
.sort()
return subdirs
}
@ -162,24 +169,102 @@ async function isWorkshopApp(projectName: string): Promise<boolean> {
return json.scripts && json.scripts["subdomain:start"] !== undefined
}
async function projectsWithDates(): Promise<{ name: string, mtime: Date, status: "recent" | "active" | "inactive" }[]> {
type ProjectDate = {
name: string
mtime: Date
status: "recent" | "active" | "inactive"
}
async function projectsWithDates(): Promise<ProjectDate[]> {
const names = await projects()
const projectsWithDates = await Promise.all(names.map(async name => {
const lastModified = await mtime(name)
// Try to batch GitHub API calls for better performance
const dates = await batchedMtimes(names)
const projectsWithDates = names.map((name, index) => {
const lastModified = dates[index] || new Date(0)
const daysSince = (Date.now() - lastModified.getTime()) / (24 * 60 * 60 * 1000)
const status: "recent" | "active" | "inactive" = daysSince <= 14 ? "recent" : daysSince <= 30 ? "active" : "inactive"
const status: ProjectDate["status"] = daysSince <= 14 ? "recent" : daysSince <= 30 ? "active" : "inactive"
return { name, mtime: lastModified, status }
}))
}).filter((project): project is ProjectDate => project.mtime instanceof Date)
const sorted = projectsWithDates.sort((a, b) => b.mtime.getTime() - a.mtime.getTime())
return sorted
}
async function mtime(project: string): Promise<Date> {
const proc = await $`cd ${PROJECTS_DIR} && git log -1 --format=%ct -- ${project}`.quiet()
if (proc.exitCode !== 0) return new Date(0)
return new Date(parseInt(proc.stdout.toString().trim()) * 1000)
// Cache to avoid redundant API calls
const mtimeCache = new Map<string, { date: Date, timestamp: number }>()
const CACHE_TTL = 5 * 60 * 1000 // 5 minutes
async function batchedMtimes(projects: string[]): Promise<Date[]> {
const token = process.env.GITHUB_TOKEN
if (!token) {
console.warn('GITHUB_TOKEN not set, returning default dates')
return projects.map(() => new Date(0))
}
// Check cache for all projects first
const results: (Date | null)[] = new Array(projects.length).fill(null)
const uncachedProjects: { index: number, name: string }[] = []
projects.forEach((project, index) => {
const cached = mtimeCache.get(project)
if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
results[index] = cached.date
} else {
uncachedProjects.push({ index, name: project })
}
})
if (uncachedProjects.length === 0) {
return results as Date[]
}
try {
// Batch API calls with Promise.all for parallel execution
const apiResults = await Promise.all(uncachedProjects.map(async ({ index, name }) => {
try {
console.log(`Fetching GitHub API for ${name}`)
const response = await fetch(`https://api.github.com/repos/probablycorey/the-workshop/commits?path=packages/${encodeURIComponent(name)}&per_page=1`, {
headers: {
'Authorization': `Bearer ${token}`,
'Accept': 'application/vnd.github.v3+json',
'User-Agent': 'cubby-workshop-app'
}
})
if (!response.ok) {
console.warn(`GitHub API error for ${name}: ${response.status}`)
return { index, date: new Date(0) }
}
const commits = await response.json()
if (!commits || commits.length === 0) {
return { index, date: new Date(0) }
}
const date = new Date(commits[0].commit.committer.date)
mtimeCache.set(name, { date, timestamp: Date.now() })
return { index, date }
} catch (error) {
console.warn(`Error fetching GitHub data for ${name}:`, error)
return { index, date: new Date(0) }
}
}))
// Fill in the results array
apiResults.forEach(({ index, date }) => {
results[index] = date
})
return results as Date[]
} catch (error) {
console.warn('Batch GitHub API call failed, returning default dates:', error)
return projects.map(() => new Date(0))
}
}
function tsx(node: any) {
return "<!DOCTYPE html>" + render(<Layout>{node}</Layout>)
}