feat: add landing page + builtins Worker + BETA_TEST guide + README

- landing/: Next.js 15 app for arcrun.dev (dashboard, integrations,
  API docs, login). Deploys via Cloudflare Pages — CI scan skips
  this via pages_build_output_dir marker.
- builtins/: minimal Hono Worker at arcrun-builtins (/init for
  one-shot component registry seeding). initComponents logic is
  flagged stale in src/index.ts for future rewrite.
- BETA_TEST.md: pre-launch validation playbook.
- README.md: updated to match current arcrun.dev / acr CLI flow.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-04-20 17:52:41 +08:00
parent 13b01328c1
commit 4516cdee4b
34 changed files with 5203 additions and 23 deletions
+175
View File
@@ -0,0 +1,175 @@
export const runtime = 'edge';
import Link from 'next/link';
type Recipe = {
id: string;
name: string;
primitive: 'static_key' | 'service_account';
category: string;
secrets: string[];
badge?: 'official';
};
const RECIPES: Recipe[] = [
// AI / LLM
{ id: 'openai', name: 'OpenAI', primitive: 'static_key', category: 'AI', secrets: ['OPENAI_API_KEY'], badge: 'official' },
{ id: 'anthropic', name: 'Anthropic', primitive: 'static_key', category: 'AI', secrets: ['ANTHROPIC_API_KEY'], badge: 'official' },
// Productivity
{ id: 'notion', name: 'Notion', primitive: 'static_key', category: 'Productivity', secrets: ['NOTION_TOKEN'], badge: 'official' },
{ id: 'airtable', name: 'Airtable', primitive: 'static_key', category: 'Productivity', secrets: ['AIRTABLE_TOKEN'], badge: 'official' },
{ id: 'typeform', name: 'Typeform', primitive: 'static_key', category: 'Productivity', secrets: ['TYPEFORM_TOKEN'], badge: 'official' },
{ id: 'jira', name: 'Jira', primitive: 'static_key', category: 'Productivity', secrets: ['JIRA_DOMAIN', 'JIRA_EMAIL', 'JIRA_API_TOKEN'], badge: 'official' },
// Communication
{ id: 'slack', name: 'Slack', primitive: 'static_key', category: 'Communication', secrets: ['SLACK_TOKEN'], badge: 'official' },
{ id: 'discord', name: 'Discord', primitive: 'static_key', category: 'Communication', secrets: ['DISCORD_BOT_TOKEN'], badge: 'official' },
{ id: 'twilio', name: 'Twilio', primitive: 'static_key', category: 'Communication', secrets: ['TWILIO_ACCOUNT_SID', 'TWILIO_AUTH_TOKEN'], badge: 'official' },
{ id: 'sendgrid', name: 'SendGrid', primitive: 'static_key', category: 'Communication', secrets: ['SENDGRID_API_KEY'], badge: 'official' },
{ id: 'resend', name: 'Resend', primitive: 'static_key', category: 'Communication', secrets: ['RESEND_API_KEY'], badge: 'official' },
// Dev / Code
{ id: 'github', name: 'GitHub', primitive: 'static_key', category: 'Dev', secrets: ['GITHUB_TOKEN'], badge: 'official' },
{ id: 'linear', name: 'Linear', primitive: 'static_key', category: 'Dev', secrets: ['LINEAR_API_KEY'], badge: 'official' },
{ id: 'supabase', name: 'Supabase', primitive: 'static_key', category: 'Dev', secrets: ['SUPABASE_URL', 'SUPABASE_SERVICE_ROLE_KEY'], badge: 'official' },
// Commerce
{ id: 'stripe', name: 'Stripe', primitive: 'static_key', category: 'Commerce', secrets: ['STRIPE_SECRET_KEY'], badge: 'official' },
{ id: 'shopify', name: 'Shopify', primitive: 'static_key', category: 'Commerce', secrets: ['SHOPIFY_STORE_DOMAIN', 'SHOPIFY_ACCESS_TOKEN'], badge: 'official' },
{ id: 'hubspot', name: 'HubSpot', primitive: 'static_key', category: 'Commerce', secrets: ['HUBSPOT_ACCESS_TOKEN'], badge: 'official' },
// Google Service Account
{ id: 'google_drive_sa', name: 'Google Drive', primitive: 'service_account', category: 'Google', secrets: ['GOOGLE_SERVICE_ACCOUNT_JSON'], badge: 'official' },
{ id: 'google_gmail_sa', name: 'Gmail', primitive: 'service_account', category: 'Google', secrets: ['GOOGLE_SERVICE_ACCOUNT_JSON'], badge: 'official' },
{ id: 'google_sheets_sa', name: 'Google Sheets', primitive: 'service_account', category: 'Google', secrets: ['GOOGLE_SERVICE_ACCOUNT_JSON'], badge: 'official' },
];
const CATEGORIES = ['All', 'AI', 'Productivity', 'Communication', 'Dev', 'Commerce', 'Google'];
export default function IntegrationsPage({
searchParams,
}: {
searchParams: Promise<{ cat?: string }>;
}) {
return <IntegrationsContent searchParamsPromise={searchParams} />;
}
async function IntegrationsContent({
searchParamsPromise,
}: {
searchParamsPromise: Promise<{ cat?: string }>;
}) {
const params = await searchParamsPromise;
const cat = params.cat ?? 'All';
const filtered = cat === 'All' ? RECIPES : RECIPES.filter(r => r.category === cat);
const staticCount = RECIPES.filter(r => r.primitive === 'static_key').length;
const saCount = RECIPES.filter(r => r.primitive === 'service_account').length;
return (
<div className="min-h-screen bg-[#0a0a0a] text-[#ededed]">
{/* Nav */}
<nav className="flex items-center justify-between px-6 py-4 border-b border-[#1a1a1a]">
<Link href="/" className="text-white font-bold text-lg tracking-tight hover:opacity-80 transition-opacity">
arcrun
</Link>
<div className="flex items-center gap-4 text-sm">
<Link href="/api-docs" className="text-[#666] hover:text-white transition-colors">API</Link>
<Link href="/login"
className="bg-indigo-600 hover:bg-indigo-500 text-white px-4 py-1.5 rounded-md text-sm font-medium transition-colors">
Get API Key
</Link>
</div>
</nav>
<div className="max-w-5xl mx-auto px-6 py-12">
{/* Header */}
<h1 className="text-3xl font-bold text-white mb-2">
{RECIPES.length}
</h1>
<p className="text-[#555] mb-2">
arcrun recipe
</p>
<div className="flex gap-4 text-sm text-[#444] mb-8">
<span>{staticCount} API Key </span>
<span>·</span>
<span>{saCount} Service Account </span>
</div>
{/* Category filter */}
<div className="flex gap-2 flex-wrap mb-8">
{CATEGORIES.map(c => (
<Link
key={c}
href={c === 'All' ? '/integrations' : `/integrations?cat=${c}`}
className={`px-3 py-1.5 rounded-full text-sm transition-colors ${
cat === c
? 'bg-indigo-600 text-white'
: 'bg-[#111] border border-[#222] text-[#666] hover:text-white hover:border-[#444]'
}`}
>
{c}
</Link>
))}
</div>
{/* Recipe grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mb-12">
{filtered.map(recipe => (
<RecipeCard key={recipe.id} recipe={recipe} />
))}
</div>
{/* Contribute CTA */}
<div className="bg-[#111] border border-[#222] rounded-2xl p-8 text-center">
<h2 className="text-white font-semibold text-xl mb-2"></h2>
<p className="text-[#555] text-sm mb-4 max-w-lg mx-auto">
API Key YAML
API AI PR
</p>
<div className="flex gap-3 justify-center flex-wrap">
<a href="https://github.com/richblack/arcrun" target="_blank" rel="noopener noreferrer"
className="bg-indigo-600 hover:bg-indigo-500 text-white px-5 py-2.5 rounded-lg text-sm font-medium transition-colors">
</a>
<a href="https://github.com/richblack/arcrun/blob/main/CONTRIBUTING.md" target="_blank" rel="noopener noreferrer"
className="border border-[#333] hover:border-[#555] text-[#aaa] hover:text-white px-5 py-2.5 rounded-lg text-sm font-medium transition-colors">
Recipe
</a>
</div>
</div>
</div>
</div>
);
}
function RecipeCard({ recipe }: { recipe: Recipe }) {
const primitiveLabel = recipe.primitive === 'static_key' ? 'API Key' : 'Service Account';
const primitiveColor = recipe.primitive === 'static_key' ? 'text-blue-400' : 'text-orange-400';
return (
<div className="bg-[#111] border border-[#1e1e1e] hover:border-[#333] rounded-xl p-5 transition-colors">
<div className="flex items-start justify-between mb-3">
<div>
<h3 className="text-white font-medium">{recipe.name}</h3>
<span className={`text-xs font-mono mt-0.5 ${primitiveColor}`}>{primitiveLabel}</span>
</div>
{recipe.badge === 'official' && (
<span className="text-xs bg-indigo-950/50 text-indigo-400 border border-indigo-900/30 px-2 py-0.5 rounded-full">
</span>
)}
</div>
<div className="text-xs text-[#444] space-y-1">
{recipe.secrets.map(s => (
<div key={s} className="font-mono flex items-center gap-1">
<span className="text-[#333]"></span> {s}
</div>
))}
</div>
<div className="mt-4 text-xs">
<code className="text-[#555] bg-[#0a0a0a] px-2 py-1 rounded font-mono">
acr auth-recipe scaffold {recipe.id}
</code>
</div>
</div>
);
}