feat(web): import oh-my-openagent-web Next.js + Cloudflare Workers site

Imports the public marketing site previously living in
../oh-my-opencode-web. Independent of the npm plugin: own package.json,
bun.lock, tsconfig.json. Not included in the published package — root
files: array still only ships dist/, bin/, postinstall.mjs.

Stack:
- Next.js 15.5 App Router + RSC, deployed to Cloudflare Workers via
  @opennextjs/cloudflare (build target .open-next/worker.js).
- Tailwind v4 + shadcn/ui primitives.
- next-intl with 4 locales (en/ja/ko/zh) under app/[locale]/.
- Playwright e2e tests under web/e2e/.
- Custom domains ohmyopenagent.com (primary) and ohmyopencode.org
  (legacy alias) declared in web/wrangler.toml.

Source files were re-formatted via `bun run format` to bring them in
line with the existing .prettierrc (singleQuote: false). Functional code
unchanged.
This commit is contained in:
YeonGyu-Kim
2026-05-08 13:35:00 +09:00
parent 2b2f21e0a1
commit f94714bbdf
74 changed files with 8076 additions and 0 deletions
+39
View File
@@ -0,0 +1,39 @@
export const DOC_SECTION_IDS = [
"overview",
"quick-start",
"config-locations",
"agents",
"categories",
"skills",
"background-tasks",
"hooks",
"mcps",
"browser-automation",
"tmux",
"git-master",
"comment-checker",
"experimental",
"lsp",
"env-vars",
] as const
export type DocSectionId = (typeof DOC_SECTION_IDS)[number]
export const DOC_SECTION_TITLE_KEYS: Record<DocSectionId, string> = {
overview: "overview",
"quick-start": "quickStart",
"config-locations": "configLocations",
agents: "agents",
categories: "categories",
skills: "skills",
"background-tasks": "backgroundTasks",
hooks: "hooks",
mcps: "mcps",
"browser-automation": "browserAutomation",
tmux: "tmux",
"git-master": "gitMaster",
"comment-checker": "commentChecker",
experimental: "experimental",
lsp: "lsp",
"env-vars": "envVars",
}
+139
View File
@@ -0,0 +1,139 @@
const GITHUB_OWNER = "code-yeongyu"
const GITHUB_REPO = "oh-my-openagent"
const NPM_PACKAGES = ["oh-my-opencode", "oh-my-openagent"]
const NPM_FIRST_PUBLISH_YEAR = 2025
const CACHE_TTL_MS = 60 * 60 * 1000
interface StatsCache {
data: StatsData
timestamp: number
}
export interface StatsData {
stars: number
totalDownloads: number
monthlyDownloads: number
weeklyDownloads: number
}
let cache: StatsCache | null = null
function formatCount(num: number): string {
if (num >= 1_000_000) {
const formatted = (num / 1_000_000).toFixed(1)
return `${formatted.replace(/\.0$/, "")}M+`
}
if (num >= 1_000) {
const formatted = (num / 1_000).toFixed(1)
return `${formatted.replace(/\.0$/, "")}k`
}
return String(num)
}
async function fetchGitHubStars(): Promise<number> {
const headers: Record<string, string> = {
Accept: "application/vnd.github.v3+json",
"User-Agent": "oh-my-openagent-web",
}
const token = process.env.GITHUB_TOKEN
if (token) {
headers.Authorization = `Bearer ${token}`
}
const res = await fetch(`https://api.github.com/repos/${GITHUB_OWNER}/${GITHUB_REPO}`, {
headers,
next: { revalidate: 3600 },
})
if (!res.ok) {
throw new Error(`GitHub API error: ${res.status}`)
}
const data = await res.json()
return data.stargazers_count
}
async function fetchNpmDownloadsForPackage(period: string, pkg: string): Promise<number> {
try {
const res = await fetch(`https://api.npmjs.org/downloads/point/${period}/${pkg}`, {
next: { revalidate: 3600 },
})
if (!res.ok) return 0
const data = await res.json()
return data.downloads ?? 0
} catch {
return 0
}
}
async function fetchNpmDownloads(period: string): Promise<number> {
const results = await Promise.all(
NPM_PACKAGES.map((pkg) => fetchNpmDownloadsForPackage(period, pkg)),
)
return results.reduce((sum, n) => sum + n, 0)
}
async function fetchAllNpmDownloadsForPackage(pkg: string): Promise<number> {
const now = new Date()
let total = 0
let year = NPM_FIRST_PUBLISH_YEAR
while (year <= now.getFullYear()) {
const start = `${year}-01-01`
const endDate = new Date(year, 11, 31)
const end = endDate > now ? now.toISOString().split("T")[0]! : `${year}-12-31`
try {
const res = await fetch(`https://api.npmjs.org/downloads/point/${start}:${end}/${pkg}`, {
next: { revalidate: 3600 },
})
if (res.ok) {
const data = await res.json()
total += data.downloads ?? 0
}
} catch {
continue
}
year++
}
return total
}
async function fetchAllNpmDownloads(): Promise<number> {
const results = await Promise.all(NPM_PACKAGES.map((pkg) => fetchAllNpmDownloadsForPackage(pkg)))
return results.reduce((sum, n) => sum + n, 0)
}
export async function getStats(): Promise<StatsData> {
const now = Date.now()
if (cache && now - cache.timestamp < CACHE_TTL_MS) {
return cache.data
}
const [stars, monthlyDownloads, weeklyDownloads, totalDownloads] = await Promise.all([
fetchGitHubStars(),
fetchNpmDownloads("last-month"),
fetchNpmDownloads("last-week"),
fetchAllNpmDownloads(),
])
const data: StatsData = { stars, totalDownloads, monthlyDownloads, weeklyDownloads }
cache = { data, timestamp: now }
return data
}
export function formatStats(stats: StatsData) {
return {
stars: formatCount(stats.stars),
totalDownloads: formatCount(stats.totalDownloads),
monthlyDownloads: formatCount(stats.monthlyDownloads),
weeklyDownloads: formatCount(stats.weeklyDownloads),
}
}
+6
View File
@@ -0,0 +1,6 @@
import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}