Initial release v1.0.0
Open-source marketplace for AI Agent skills. Features: - Next.js 15 web app with i18n (en/fa) - CLI tool for skill installation (npx skillhub) - GitHub crawler/indexer with multi-strategy discovery - Security scanning for all indexed skills - Self-hostable with Docker Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
556
apps/web/app/[locale]/docs/api/page.tsx
Normal file
556
apps/web/app/[locale]/docs/api/page.tsx
Normal file
@@ -0,0 +1,556 @@
|
||||
import { getTranslations, setRequestLocale } from 'next-intl/server';
|
||||
import { Header } from '@/components/Header';
|
||||
import { Footer } from '@/components/Footer';
|
||||
import { ApiEndpointSection } from '@/components/ApiEndpointSection';
|
||||
import type { EndpointDef } from '@/components/ApiEndpointSection';
|
||||
import Link from 'next/link';
|
||||
import { ArrowLeft, ArrowRight, Search, FileCode, Users, Compass, Mail } from 'lucide-react';
|
||||
|
||||
export default async function ApiDocsPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ locale: string }>;
|
||||
}) {
|
||||
const { locale } = await params;
|
||||
setRequestLocale(locale);
|
||||
const t = await getTranslations('docs');
|
||||
const isRTL = locale === 'fa';
|
||||
const ArrowIcon = isRTL ? ArrowLeft : ArrowRight;
|
||||
const siteUrl = process.env.NEXT_PUBLIC_APP_URL || 'https://skills.palebluedot.live';
|
||||
|
||||
const labels = {
|
||||
parameters: t('api.labels.parameters'),
|
||||
requestBody: t('api.labels.requestBody'),
|
||||
required: t('api.labels.required'),
|
||||
optional: t('api.labels.optional'),
|
||||
default: t('api.labels.default'),
|
||||
responseExample: t('api.labels.responseExample'),
|
||||
rateLimit: t('api.labels.rateLimit'),
|
||||
cache: t('api.labels.cache'),
|
||||
authRequired: t('api.labels.authRequired'),
|
||||
notes: t('api.labels.notes'),
|
||||
};
|
||||
|
||||
// --- Section 1: Skills ---
|
||||
const skillsEndpoints: EndpointDef[] = [
|
||||
{
|
||||
method: 'GET',
|
||||
path: '/api/skills',
|
||||
description: t('api.endpoints.searchSkills'),
|
||||
auth: false,
|
||||
rateLimit: '60 req/min',
|
||||
cacheTTL: '5 min',
|
||||
params: [
|
||||
{ name: 'q', type: 'string', required: false, description: t('api.params.q') },
|
||||
{ name: 'category', type: 'string', required: false, description: t('api.params.category') },
|
||||
{ name: 'platform', type: 'string', required: false, description: t('api.params.platform') + ' (claude, codex, copilot, cursor, windsurf)' },
|
||||
{ name: 'format', type: 'string', required: false, description: t('api.params.format'), default: 'skill.md' },
|
||||
{ name: 'verified', type: 'boolean', required: false, description: t('api.params.verified') },
|
||||
{ name: 'minStars', type: 'number', required: false, description: t('api.params.minStars') },
|
||||
{ name: 'sort', type: 'string', required: false, description: t('api.params.sort') + ' (stars, downloads, rating, recent)', default: 'downloads' },
|
||||
{ name: 'page', type: 'number', required: false, description: t('api.params.page'), default: '1' },
|
||||
{ name: 'limit', type: 'number', required: false, description: t('api.params.limit'), default: '20' },
|
||||
],
|
||||
responseExample: `{
|
||||
"skills": [
|
||||
{
|
||||
"id": "anthropic/skills/code-review",
|
||||
"name": "code-review",
|
||||
"description": "AI-powered code review assistant",
|
||||
"githubOwner": "anthropic",
|
||||
"githubRepo": "skills",
|
||||
"githubStars": 1234,
|
||||
"downloadCount": 567,
|
||||
"securityStatus": "PASS",
|
||||
"rating": 4.5,
|
||||
"ratingCount": 23,
|
||||
"isVerified": true,
|
||||
"compatibility": { "platforms": ["claude", "cursor"] }
|
||||
}
|
||||
],
|
||||
"pagination": {
|
||||
"page": 1,
|
||||
"limit": 20,
|
||||
"total": 150,
|
||||
"totalPages": 8
|
||||
},
|
||||
"searchEngine": "meilisearch"
|
||||
}`,
|
||||
notes: t('api.notes.searchFallback'),
|
||||
},
|
||||
{
|
||||
method: 'GET',
|
||||
path: '/api/skills/:id',
|
||||
description: t('api.endpoints.getSkill'),
|
||||
auth: false,
|
||||
rateLimit: '120 req/min',
|
||||
responseExample: `{
|
||||
"id": "anthropic/skills/code-review",
|
||||
"name": "code-review",
|
||||
"description": "AI-powered code review assistant",
|
||||
"githubOwner": "anthropic",
|
||||
"githubRepo": "skills",
|
||||
"skillPath": "code-review",
|
||||
"branch": "main",
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"githubStars": 1234,
|
||||
"downloadCount": 567,
|
||||
"securityScore": 95,
|
||||
"securityStatus": "PASS",
|
||||
"isVerified": true,
|
||||
"compatibility": { "platforms": ["claude"] },
|
||||
"rawContent": "# Code Review\\n...",
|
||||
"sourceFormat": "skill.md"
|
||||
}`,
|
||||
notes: t('api.notes.viewCount'),
|
||||
},
|
||||
{
|
||||
method: 'GET',
|
||||
path: '/api/skills/featured',
|
||||
description: t('api.endpoints.featuredSkills'),
|
||||
auth: false,
|
||||
rateLimit: '120 req/min',
|
||||
cacheTTL: '2 hours',
|
||||
params: [
|
||||
{ name: 'limit', type: 'number', required: false, description: t('api.params.limitNum'), default: '6' },
|
||||
],
|
||||
},
|
||||
{
|
||||
method: 'GET',
|
||||
path: '/api/skills/recent',
|
||||
description: t('api.endpoints.recentSkills'),
|
||||
auth: false,
|
||||
rateLimit: '120 req/min',
|
||||
cacheTTL: '1 hour',
|
||||
params: [
|
||||
{ name: 'limit', type: 'number', required: false, description: t('api.params.limitNum'), default: '10' },
|
||||
],
|
||||
},
|
||||
{
|
||||
method: 'POST',
|
||||
path: '/api/skills/install',
|
||||
description: t('api.endpoints.trackInstall'),
|
||||
auth: false,
|
||||
bodyParams: [
|
||||
{ name: 'skillId', type: 'string', required: true, description: t('api.params.skillId') },
|
||||
{ name: 'platform', type: 'string', required: false, description: t('api.params.platform') },
|
||||
{ name: 'method', type: 'string', required: false, description: t('api.params.method') },
|
||||
],
|
||||
responseExample: `{ "success": true, "skillId": "owner/repo/skill", "platform": "claude", "method": "cli" }`,
|
||||
notes: t('api.notes.installDedup'),
|
||||
},
|
||||
{
|
||||
method: 'POST',
|
||||
path: '/api/skills/add-request',
|
||||
description: t('api.endpoints.addRequest'),
|
||||
auth: true,
|
||||
bodyParams: [
|
||||
{ name: 'repositoryUrl', type: 'string', required: true, description: t('api.params.repositoryUrl') },
|
||||
{ name: 'reason', type: 'string', required: false, description: t('api.params.reason') },
|
||||
],
|
||||
responseExample: `{ "success": true, "requestId": "uuid", "hasSkillMd": true, "skillCount": 3 }`,
|
||||
},
|
||||
{
|
||||
method: 'POST',
|
||||
path: '/api/skills/removal-request',
|
||||
description: t('api.endpoints.removalRequest'),
|
||||
auth: true,
|
||||
bodyParams: [
|
||||
{ name: 'skillId', type: 'string', required: true, description: t('api.params.skillId') },
|
||||
{ name: 'reason', type: 'string', required: false, description: t('api.params.reason') },
|
||||
],
|
||||
responseExample: `{ "success": true, "requestId": "uuid", "blocked": true }`,
|
||||
},
|
||||
];
|
||||
|
||||
// --- Section 2: Skill Files ---
|
||||
const skillFilesEndpoints: EndpointDef[] = [
|
||||
{
|
||||
method: 'GET',
|
||||
path: '/api/skill-files',
|
||||
description: t('api.endpoints.getSkillFiles'),
|
||||
auth: false,
|
||||
rateLimit: '60 req/min',
|
||||
params: [
|
||||
{ name: 'id', type: 'string', required: true, description: t('api.params.skillId') },
|
||||
],
|
||||
responseExample: `{
|
||||
"skillId": "owner/repo/skill",
|
||||
"githubOwner": "owner",
|
||||
"githubRepo": "repo",
|
||||
"skillPath": "skill",
|
||||
"branch": "main",
|
||||
"files": [
|
||||
{
|
||||
"name": "SKILL.md",
|
||||
"path": "SKILL.md",
|
||||
"type": "file",
|
||||
"size": 2048,
|
||||
"content": "# Skill Name\\n..."
|
||||
}
|
||||
],
|
||||
"fromCache": true,
|
||||
"cachedAt": "2026-01-15T10:30:00Z"
|
||||
}`,
|
||||
notes: t('api.notes.cacheInDB'),
|
||||
},
|
||||
{
|
||||
method: 'GET',
|
||||
path: '/api/skill-files/zip',
|
||||
description: t('api.endpoints.downloadZip'),
|
||||
auth: false,
|
||||
rateLimit: '60 req/min',
|
||||
params: [
|
||||
{ name: 'id', type: 'string', required: true, description: t('api.params.skillId') },
|
||||
{ name: 'platform', type: 'string', required: false, description: t('api.params.platformZip'), default: 'claude' },
|
||||
],
|
||||
notes: t('api.notes.zipTransform'),
|
||||
},
|
||||
];
|
||||
|
||||
// --- Section 3: User Actions ---
|
||||
const userActionsEndpoints: EndpointDef[] = [
|
||||
{
|
||||
method: 'GET',
|
||||
path: '/api/ratings',
|
||||
description: t('api.endpoints.getRatings'),
|
||||
auth: false,
|
||||
rateLimit: '120 req/min',
|
||||
params: [
|
||||
{ name: 'skillId', type: 'string', required: true, description: t('api.params.skillId') },
|
||||
{ name: 'limit', type: 'number', required: false, description: t('api.params.limitNum'), default: '10' },
|
||||
{ name: 'offset', type: 'number', required: false, description: t('api.params.offset'), default: '0' },
|
||||
],
|
||||
responseExample: `{
|
||||
"ratings": [
|
||||
{
|
||||
"id": "uuid",
|
||||
"rating": 5,
|
||||
"review": "Great skill!",
|
||||
"createdAt": "2026-01-10T00:00:00Z",
|
||||
"user": { "id": "uid", "username": "user1", "avatarUrl": "https://..." }
|
||||
}
|
||||
],
|
||||
"summary": { "average": 4.5, "count": 23 }
|
||||
}`,
|
||||
},
|
||||
{
|
||||
method: 'POST',
|
||||
path: '/api/ratings',
|
||||
description: t('api.endpoints.createRating'),
|
||||
auth: true,
|
||||
rateLimit: '600 req/min',
|
||||
bodyParams: [
|
||||
{ name: 'skillId', type: 'string', required: true, description: t('api.params.skillId') },
|
||||
{ name: 'rating', type: 'number', required: true, description: t('api.params.rating') },
|
||||
{ name: 'review', type: 'string', required: false, description: t('api.params.review') },
|
||||
],
|
||||
responseExample: `{ "rating": { "id": "uuid", "rating": 5, "review": "..." }, "summary": { "average": 4.5, "count": 24 } }`,
|
||||
},
|
||||
{
|
||||
method: 'GET',
|
||||
path: '/api/ratings/me',
|
||||
description: t('api.endpoints.getMyRating'),
|
||||
auth: true,
|
||||
rateLimit: '600 req/min',
|
||||
params: [
|
||||
{ name: 'skillId', type: 'string', required: true, description: t('api.params.skillId') },
|
||||
],
|
||||
responseExample: `{ "rating": { "id": "uuid", "rating": 5, "review": "Great!" } }`,
|
||||
},
|
||||
{
|
||||
method: 'GET',
|
||||
path: '/api/favorites',
|
||||
description: t('api.endpoints.getFavorites'),
|
||||
auth: true,
|
||||
rateLimit: '600 req/min',
|
||||
responseExample: `{
|
||||
"favorites": [
|
||||
{
|
||||
"id": "owner/repo/skill",
|
||||
"name": "skill-name",
|
||||
"description": "...",
|
||||
"githubStars": 100,
|
||||
"downloadCount": 50,
|
||||
"securityStatus": "PASS",
|
||||
"isVerified": true,
|
||||
"rating": 4.5,
|
||||
"ratingCount": 10
|
||||
}
|
||||
]
|
||||
}`,
|
||||
},
|
||||
{
|
||||
method: 'POST',
|
||||
path: '/api/favorites',
|
||||
description: t('api.endpoints.addFavorite'),
|
||||
auth: true,
|
||||
rateLimit: '600 req/min',
|
||||
bodyParams: [
|
||||
{ name: 'skillId', type: 'string', required: true, description: t('api.params.skillId') },
|
||||
],
|
||||
responseExample: `{ "success": true, "favorited": true }`,
|
||||
},
|
||||
{
|
||||
method: 'DELETE',
|
||||
path: '/api/favorites',
|
||||
description: t('api.endpoints.removeFavorite'),
|
||||
auth: true,
|
||||
rateLimit: '600 req/min',
|
||||
bodyParams: [
|
||||
{ name: 'skillId', type: 'string', required: true, description: t('api.params.skillId') },
|
||||
],
|
||||
responseExample: `{ "success": true, "favorited": false }`,
|
||||
},
|
||||
{
|
||||
method: 'POST',
|
||||
path: '/api/favorites/check',
|
||||
description: t('api.endpoints.checkFavorites'),
|
||||
auth: true,
|
||||
rateLimit: '600 req/min',
|
||||
bodyParams: [
|
||||
{ name: 'skillIds', type: 'string[]', required: true, description: t('api.params.skillIds') },
|
||||
],
|
||||
responseExample: `{ "favorited": { "owner/repo/skill-a": true, "owner/repo/skill-b": false } }`,
|
||||
},
|
||||
];
|
||||
|
||||
// --- Section 4: Discovery & Metadata ---
|
||||
const discoveryEndpoints: EndpointDef[] = [
|
||||
{
|
||||
method: 'GET',
|
||||
path: '/api/categories',
|
||||
description: t('api.endpoints.getCategories'),
|
||||
auth: false,
|
||||
rateLimit: '120 req/min',
|
||||
cacheTTL: '12 hours',
|
||||
responseExample: `{
|
||||
"categories": [
|
||||
{
|
||||
"id": "cat-id",
|
||||
"name": "Code Quality",
|
||||
"slug": "code-quality",
|
||||
"description": "...",
|
||||
"icon": "🔍",
|
||||
"skillCount": 15,
|
||||
"sortOrder": 1
|
||||
}
|
||||
]
|
||||
}`,
|
||||
},
|
||||
{
|
||||
method: 'GET',
|
||||
path: '/api/stats',
|
||||
description: t('api.endpoints.getStats'),
|
||||
auth: false,
|
||||
rateLimit: '120 req/min',
|
||||
cacheTTL: '1 hour',
|
||||
responseExample: `{
|
||||
"totalSkills": 850,
|
||||
"totalDownloads": 12500,
|
||||
"totalCategories": 23,
|
||||
"totalContributors": 320,
|
||||
"platforms": 5
|
||||
}`,
|
||||
},
|
||||
{
|
||||
method: 'GET',
|
||||
path: '/api/health',
|
||||
description: t('api.endpoints.healthCheck'),
|
||||
auth: false,
|
||||
responseExample: `{
|
||||
"status": "healthy",
|
||||
"timestamp": "2026-02-11T12:00:00Z",
|
||||
"version": "0.2.4",
|
||||
"isPrimary": true,
|
||||
"services": {
|
||||
"database": { "status": "healthy", "latency": 5 },
|
||||
"meilisearch": { "status": "healthy", "latency": 12 },
|
||||
"redis": { "status": "healthy", "latency": 3 }
|
||||
}
|
||||
}`,
|
||||
},
|
||||
{
|
||||
method: 'GET',
|
||||
path: '/api/attribution',
|
||||
description: t('api.endpoints.getAttribution'),
|
||||
auth: false,
|
||||
rateLimit: '120 req/min',
|
||||
cacheTTL: '1 hour',
|
||||
responseExample: `{
|
||||
"totalSkills": 850,
|
||||
"totalContributors": 320,
|
||||
"totalRepos": 150,
|
||||
"licenseDistribution": [
|
||||
{ "license": "MIT", "count": 50, "percentage": 50 }
|
||||
],
|
||||
"lastUpdated": "2026-02-11T00:00:00Z"
|
||||
}`,
|
||||
},
|
||||
];
|
||||
|
||||
// --- Section 5: Newsletter ---
|
||||
const newsletterEndpoints: EndpointDef[] = [
|
||||
{
|
||||
method: 'POST',
|
||||
path: '/api/newsletter/subscribe',
|
||||
description: t('api.endpoints.subscribe'),
|
||||
auth: false,
|
||||
rateLimit: '60 req/min',
|
||||
bodyParams: [
|
||||
{ name: 'email', type: 'string', required: true, description: t('api.params.email') },
|
||||
{ name: 'source', type: 'string', required: false, description: t('api.params.source') },
|
||||
{ name: 'locale', type: 'string', required: false, description: t('api.params.locale'), default: 'en' },
|
||||
],
|
||||
responseExample: `{ "success": true, "subscribed": true }`,
|
||||
},
|
||||
{
|
||||
method: 'POST',
|
||||
path: '/api/newsletter/unsubscribe',
|
||||
description: t('api.endpoints.unsubscribe'),
|
||||
auth: false,
|
||||
rateLimit: '60 req/min',
|
||||
bodyParams: [
|
||||
{ name: 'email', type: 'string', required: true, description: t('api.params.email') },
|
||||
],
|
||||
responseExample: `{ "success": true, "unsubscribed": true }`,
|
||||
notes: t('api.notes.alwaysSuccess'),
|
||||
},
|
||||
];
|
||||
|
||||
const sections = [
|
||||
{ title: t('api.sections.skills.title'), description: t('api.sections.skills.description'), icon: Search, endpoints: skillsEndpoints },
|
||||
{ title: t('api.sections.skillFiles.title'), description: t('api.sections.skillFiles.description'), icon: FileCode, endpoints: skillFilesEndpoints },
|
||||
{ title: t('api.sections.userActions.title'), description: t('api.sections.userActions.description'), icon: Users, endpoints: userActionsEndpoints },
|
||||
{ title: t('api.sections.discovery.title'), description: t('api.sections.discovery.description'), icon: Compass, endpoints: discoveryEndpoints },
|
||||
{ title: t('api.sections.newsletter.title'), description: t('api.sections.newsletter.description'), icon: Mail, endpoints: newsletterEndpoints },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col">
|
||||
<Header />
|
||||
<main className="flex-1">
|
||||
<section className="section bg-surface">
|
||||
<div className="container-main">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<Link
|
||||
href={`/${locale}/docs`}
|
||||
className="inline-flex items-center gap-2 text-primary-600 hover:text-primary-700 mb-8"
|
||||
>
|
||||
<ArrowIcon className="w-4 h-4 rotate-180" />
|
||||
{t('title')}
|
||||
</Link>
|
||||
|
||||
<h1 className="text-4xl font-bold mb-4">{t('api.title')}</h1>
|
||||
<p className="text-lg text-text-secondary mb-10">{t('api.description')}</p>
|
||||
|
||||
{/* Base URL */}
|
||||
<div className="mb-10">
|
||||
<h2 className="text-2xl font-bold mb-3">{t('api.baseUrl')}</h2>
|
||||
<div className="glass-card p-4" dir="ltr">
|
||||
<code className="text-sm font-mono text-left block">{siteUrl}</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Authentication */}
|
||||
<div className="mb-10">
|
||||
<h2 className="text-2xl font-bold mb-3">{t('api.authSection.title')}</h2>
|
||||
<p className="text-text-secondary mb-2">{t('api.authSection.description')}</p>
|
||||
<p className="text-text-secondary text-sm">{t('api.authSection.howTo')}</p>
|
||||
</div>
|
||||
|
||||
{/* Rate Limiting */}
|
||||
<div className="mb-10">
|
||||
<h2 className="text-2xl font-bold mb-3">{t('api.rateLimitSection.title')}</h2>
|
||||
<p className="text-text-secondary mb-4">{t('api.rateLimitSection.description')}</p>
|
||||
<div className="glass-card overflow-hidden mb-4" dir="ltr">
|
||||
<table className="w-full text-sm text-left">
|
||||
<thead>
|
||||
<tr className="border-b border-border/50 bg-surface-hover/50">
|
||||
<th className="py-2.5 px-4 font-medium">{t('api.rateLimitSection.tier')}</th>
|
||||
<th className="py-2.5 px-4 font-medium">{t('api.rateLimitSection.limit')}</th>
|
||||
<th className="py-2.5 px-4 font-medium">{t('api.rateLimitSection.use')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr className="border-b border-border/30">
|
||||
<td className="py-2 px-4 font-mono text-xs">{t('api.rateLimitSection.anonymous')}</td>
|
||||
<td className="py-2 px-4">120 req/min</td>
|
||||
<td className="py-2 px-4 text-text-secondary">{t('api.rateLimitSection.anonymousUse')}</td>
|
||||
</tr>
|
||||
<tr className="border-b border-border/30">
|
||||
<td className="py-2 px-4 font-mono text-xs">{t('api.rateLimitSection.search')}</td>
|
||||
<td className="py-2 px-4">60 req/min</td>
|
||||
<td className="py-2 px-4 text-text-secondary">{t('api.rateLimitSection.searchUse')}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="py-2 px-4 font-mono text-xs">{t('api.rateLimitSection.authenticated')}</td>
|
||||
<td className="py-2 px-4">600 req/min</td>
|
||||
<td className="py-2 px-4 text-text-secondary">{t('api.rateLimitSection.authenticatedUse')}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div dir="ltr">
|
||||
<h3 className="text-sm font-semibold mb-2 text-left">{t('api.rateLimitSection.headersTitle')}</h3>
|
||||
<div className="space-y-1 text-sm text-left">
|
||||
<p><code className="font-mono text-xs bg-surface-hover px-1.5 py-0.5 rounded">X-RateLimit-Limit</code> — {t('api.rateLimitSection.headerLimit')}</p>
|
||||
<p><code className="font-mono text-xs bg-surface-hover px-1.5 py-0.5 rounded">X-RateLimit-Remaining</code> — {t('api.rateLimitSection.headerRemaining')}</p>
|
||||
<p><code className="font-mono text-xs bg-surface-hover px-1.5 py-0.5 rounded">X-RateLimit-Reset</code> — {t('api.rateLimitSection.headerReset')}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Error Responses */}
|
||||
<div className="mb-10">
|
||||
<h2 className="text-2xl font-bold mb-3">{t('api.errorSection.title')}</h2>
|
||||
<p className="text-text-secondary mb-4">{t('api.errorSection.description')}</p>
|
||||
<div className="glass-card p-4 mb-4" dir="ltr">
|
||||
<pre className="text-sm font-mono overflow-x-auto text-left">{`{
|
||||
"error": "Too Many Requests",
|
||||
"message": "Rate limit exceeded",
|
||||
"retryAfter": 45,
|
||||
"limit": 60
|
||||
}`}</pre>
|
||||
</div>
|
||||
<div dir="ltr">
|
||||
<h3 className="text-sm font-semibold mb-2 text-left">{t('api.errorSection.commonCodes')}</h3>
|
||||
<div className="space-y-1 text-sm text-left">
|
||||
<p><code className="font-mono text-xs bg-surface-hover px-1.5 py-0.5 rounded">400</code> — {t('api.errorSection.code400')}</p>
|
||||
<p><code className="font-mono text-xs bg-surface-hover px-1.5 py-0.5 rounded">401</code> — {t('api.errorSection.code401')}</p>
|
||||
<p><code className="font-mono text-xs bg-surface-hover px-1.5 py-0.5 rounded">404</code> — {t('api.errorSection.code404')}</p>
|
||||
<p><code className="font-mono text-xs bg-surface-hover px-1.5 py-0.5 rounded">429</code> — {t('api.errorSection.code429')}</p>
|
||||
<p><code className="font-mono text-xs bg-surface-hover px-1.5 py-0.5 rounded">500</code> — {t('api.errorSection.code500')}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Quick example */}
|
||||
<div className="mb-10">
|
||||
<h2 className="text-2xl font-bold mb-3">Quick Example</h2>
|
||||
<div className="glass-card p-4" dir="ltr">
|
||||
<code className="text-sm font-mono whitespace-pre-wrap text-left block">{`curl "${siteUrl}/api/skills?q=code-review&limit=5"`}</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr className="border-border/50 mb-10" />
|
||||
|
||||
{/* Endpoint Sections */}
|
||||
{sections.map((section, index) => (
|
||||
<ApiEndpointSection
|
||||
key={index}
|
||||
title={section.title}
|
||||
description={section.description}
|
||||
endpoints={section.endpoints}
|
||||
labels={labels}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
152
apps/web/app/[locale]/docs/cli/page.tsx
Normal file
152
apps/web/app/[locale]/docs/cli/page.tsx
Normal file
@@ -0,0 +1,152 @@
|
||||
import { getTranslations, setRequestLocale } from 'next-intl/server';
|
||||
import { Header } from '@/components/Header';
|
||||
import { Footer } from '@/components/Footer';
|
||||
import Link from 'next/link';
|
||||
import { ArrowLeft, ArrowRight } from 'lucide-react';
|
||||
|
||||
export default async function CliDocsPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ locale: string }>;
|
||||
}) {
|
||||
const { locale } = await params;
|
||||
setRequestLocale(locale);
|
||||
const t = await getTranslations('docs');
|
||||
const tContent = await getTranslations('docs.content');
|
||||
const isRTL = locale === 'fa';
|
||||
const ArrowIcon = isRTL ? ArrowLeft : ArrowRight;
|
||||
|
||||
const commands = [
|
||||
{ name: 'install <skill-id>', desc: tContent('cmdInstallDesc') },
|
||||
{ name: 'search <query>', desc: tContent('cmdSearchDesc') },
|
||||
{ name: 'list', desc: tContent('cmdListDesc') },
|
||||
{ name: 'update [skill-name]', desc: tContent('cmdUpdateDesc') },
|
||||
{ name: 'uninstall <skill-name>', desc: tContent('cmdUninstallDesc') },
|
||||
{ name: 'config', desc: tContent('cmdConfigDesc') },
|
||||
];
|
||||
|
||||
const platforms = [
|
||||
{ name: 'Claude', path: tContent('platformClaudePath') },
|
||||
{ name: 'Codex', path: tContent('platformCodexPath') },
|
||||
{ name: 'Copilot', path: tContent('platformCopilotPath') },
|
||||
{ name: 'Cursor', path: tContent('platformCursorPath') },
|
||||
{ name: 'Windsurf', path: tContent('platformWindsurfPath') },
|
||||
];
|
||||
|
||||
const configKeys = [
|
||||
{ key: 'defaultPlatform', desc: tContent('configDefaultPlatform') },
|
||||
{ key: 'apiUrl', desc: tContent('configApiUrl') },
|
||||
{ key: 'githubToken', desc: tContent('configGithubToken') },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col">
|
||||
<Header />
|
||||
<main className="flex-1">
|
||||
<section className="section bg-surface">
|
||||
<div className="container-main">
|
||||
<div className="max-w-3xl mx-auto">
|
||||
<Link
|
||||
href={`/${locale}/docs`}
|
||||
className="inline-flex items-center gap-2 text-primary-600 hover:text-primary-700 mb-8"
|
||||
>
|
||||
<ArrowIcon className="w-4 h-4 rotate-180" />
|
||||
{t('title')}
|
||||
</Link>
|
||||
|
||||
<h1 className="text-4xl font-bold mb-6">{t('cli.title')}</h1>
|
||||
<p className="text-lg text-text-secondary mb-8">{t('cli.description')}</p>
|
||||
|
||||
<div className="prose prose-lg max-w-none">
|
||||
<h2>{tContent('installation')}</h2>
|
||||
<div className="glass-card p-4 my-4" dir="ltr">
|
||||
<code className="text-sm font-mono text-left block">npm install -g skillhub</code>
|
||||
</div>
|
||||
|
||||
<h2>{tContent('commands')}</h2>
|
||||
<div className="space-y-4 my-6" dir="ltr">
|
||||
{commands.map((cmd, index) => (
|
||||
<div key={index} className="glass-card p-4 text-left">
|
||||
<code className="text-sm font-mono text-primary-600">skillhub {cmd.name}</code>
|
||||
<p className="text-text-secondary mt-2">{cmd.desc}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<h2>{tContent('examples')}</h2>
|
||||
<div className="glass-card p-4 my-4 space-y-4" dir="ltr">
|
||||
{/* Install globally */}
|
||||
<div>
|
||||
<p className="text-sm text-text-muted mb-1">{tContent('exInstallGlobal')}</p>
|
||||
<code className="block text-sm font-mono">
|
||||
<span className="text-text-muted">$</span> npx skillhub install anthropics/skills/pdf
|
||||
</code>
|
||||
<code className="block text-sm font-mono text-success">
|
||||
✓ Skill installed to ~/.claude/skills/pdf/
|
||||
</code>
|
||||
</div>
|
||||
{/* Install in project */}
|
||||
<div>
|
||||
<p className="text-sm text-text-muted mb-1">{tContent('exInstallProject')}</p>
|
||||
<code className="block text-sm font-mono">
|
||||
<span className="text-text-muted">$</span> npx skillhub install anthropics/skills/pdf --project
|
||||
</code>
|
||||
<code className="block text-sm font-mono text-success">
|
||||
✓ Skill installed to ./.claude/skills/pdf/
|
||||
</code>
|
||||
</div>
|
||||
{/* Search */}
|
||||
<div>
|
||||
<p className="text-sm text-text-muted mb-1">{tContent('exSearch')}</p>
|
||||
<code className="block text-sm font-mono">
|
||||
<span className="text-text-muted">$</span> npx skillhub search pdf
|
||||
</code>
|
||||
</div>
|
||||
{/* Search with sort */}
|
||||
<div>
|
||||
<p className="text-sm text-text-muted mb-1">{tContent('exSearchSort')}</p>
|
||||
<code className="block text-sm font-mono">
|
||||
<span className="text-text-muted">$</span> npx skillhub search "code review" --sort stars --limit 5
|
||||
</code>
|
||||
</div>
|
||||
{/* Update all */}
|
||||
<div>
|
||||
<p className="text-sm text-text-muted mb-1">{tContent('exUpdateAll')}</p>
|
||||
<code className="block text-sm font-mono">
|
||||
<span className="text-text-muted">$</span> npx skillhub update --all
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Supported Platforms */}
|
||||
<h2>{tContent('platformsTitle')}</h2>
|
||||
<p>{tContent('platformsDesc')}</p>
|
||||
<div className="space-y-3 my-6" dir="ltr">
|
||||
{platforms.map((p) => (
|
||||
<div key={p.name} className="glass-card p-4 text-left flex items-center justify-between">
|
||||
<span className="text-sm font-semibold text-text-primary">{p.name}</span>
|
||||
<code className="text-sm font-mono text-text-secondary">{p.path}</code>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Configuration */}
|
||||
<h2>{tContent('configTitle')}</h2>
|
||||
<p>{tContent('configDesc')}</p>
|
||||
<div className="space-y-3 my-6" dir="ltr">
|
||||
{configKeys.map((c) => (
|
||||
<div key={c.key} className="glass-card p-4 text-left">
|
||||
<code className="text-sm font-mono text-primary-600">{c.key}</code>
|
||||
<p className="text-text-secondary mt-1 text-sm">{c.desc}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
206
apps/web/app/[locale]/docs/getting-started/page.tsx
Normal file
206
apps/web/app/[locale]/docs/getting-started/page.tsx
Normal file
@@ -0,0 +1,206 @@
|
||||
import { getTranslations, setRequestLocale } from 'next-intl/server';
|
||||
import { Header } from '@/components/Header';
|
||||
import { Footer } from '@/components/Footer';
|
||||
import { DiscoveryModes } from '@/components/DiscoveryModes';
|
||||
import Link from 'next/link';
|
||||
import { ArrowLeft, ArrowRight } from 'lucide-react';
|
||||
import { createDb, skills, sql } from '@skillhub/db';
|
||||
import { formatPromptSkillCount } from '@/lib/format-number';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
async function getSkillCount(): Promise<string> {
|
||||
try {
|
||||
const db = createDb();
|
||||
const result = await db
|
||||
.select({ count: sql<number>`count(*)::int` })
|
||||
.from(skills);
|
||||
return formatPromptSkillCount(result[0]?.count ?? 170000);
|
||||
} catch {
|
||||
return '170,000+';
|
||||
}
|
||||
}
|
||||
|
||||
function getDiscoveryPrompts(count: string) {
|
||||
const siteUrl = process.env.NEXT_PUBLIC_APP_URL || 'https://skills.palebluedot.live';
|
||||
return {
|
||||
standard: `## SkillHub - AI Agent Skills
|
||||
|
||||
${count} skills available at ${siteUrl}
|
||||
|
||||
Search when working with unfamiliar tech, complex workflows, or after a failed attempt:
|
||||
|
||||
\`\`\`bash
|
||||
npx skillhub search "<query>" --limit 5
|
||||
npx skillhub install <skill-id> --project
|
||||
npx skillhub uninstall <skill-name> # when done
|
||||
\`\`\`
|
||||
|
||||
Rules:
|
||||
- Only install skills with security status "Pass"
|
||||
- After installing, read the SKILL.md and follow its instructions
|
||||
- Search during planning phase for best results`,
|
||||
|
||||
compact: `## SkillHub (${count} skills) -- ${siteUrl}
|
||||
|
||||
\`\`\`bash
|
||||
npx skillhub search "<query>" --limit 5
|
||||
npx skillhub install <skill-id> --project
|
||||
npx skillhub uninstall <skill-name>
|
||||
\`\`\`
|
||||
|
||||
Search for unfamiliar tech or complex tasks. Only install "Pass" security status. Read SKILL.md after install.`,
|
||||
|
||||
oneshot: `You have access to SkillHub (${count} AI agent skills, ${siteUrl}). For unfamiliar or complex tasks: \`npx skillhub search "<query>" --limit 5\` then \`npx skillhub install <id> --project\`. Only "Pass" security. Read SKILL.md after install. Uninstall when done.`,
|
||||
};
|
||||
}
|
||||
|
||||
export default async function GettingStartedPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ locale: string }>;
|
||||
}) {
|
||||
const { locale } = await params;
|
||||
setRequestLocale(locale);
|
||||
const t = await getTranslations('docs');
|
||||
const tContent = await getTranslations('docs.content');
|
||||
const isRTL = locale === 'fa';
|
||||
const ArrowIcon = isRTL ? ArrowLeft : ArrowRight;
|
||||
|
||||
const skillCount = await getSkillCount();
|
||||
const prompts = getDiscoveryPrompts(skillCount);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col">
|
||||
<Header />
|
||||
<main className="flex-1">
|
||||
<section className="section bg-surface">
|
||||
<div className="container-main">
|
||||
<div className="max-w-3xl mx-auto">
|
||||
<Link
|
||||
href={`/${locale}/docs`}
|
||||
className="inline-flex items-center gap-2 text-primary-600 hover:text-primary-700 mb-8"
|
||||
>
|
||||
<ArrowIcon className="w-4 h-4 rotate-180" />
|
||||
{t('title')}
|
||||
</Link>
|
||||
|
||||
<h1 className="text-4xl font-bold mb-6">{t('gettingStarted.title')}</h1>
|
||||
<p className="text-lg text-text-secondary mb-8">{t('gettingStarted.description')}</p>
|
||||
|
||||
<div className="prose prose-lg max-w-none">
|
||||
<h2>{tContent('installation')}</h2>
|
||||
<p>{tContent('installCli')}</p>
|
||||
<div className="glass-card p-4 my-4" dir="ltr">
|
||||
<code className="text-sm font-mono text-left block">npm install -g skillhub</code>
|
||||
</div>
|
||||
|
||||
<h2>{tContent('usage')}</h2>
|
||||
|
||||
<h3>{tContent('searchSkills')}</h3>
|
||||
<div className="glass-card p-4 my-4" dir="ltr">
|
||||
<code className="text-sm font-mono text-left block">skillhub search code-review</code>
|
||||
</div>
|
||||
|
||||
<h3>{tContent('installSkill')}</h3>
|
||||
<div className="glass-card p-4 my-4" dir="ltr">
|
||||
<code className="text-sm font-mono text-left block">skillhub install anthropic/skills/code-review</code>
|
||||
</div>
|
||||
|
||||
<h3>{tContent('listInstalled')}</h3>
|
||||
<div className="glass-card p-4 my-4" dir="ltr">
|
||||
<code className="text-sm font-mono text-left block">skillhub list</code>
|
||||
</div>
|
||||
|
||||
{/* Web-based Installation */}
|
||||
<hr className="my-8 border-border" />
|
||||
<h2>{tContent('webInstallTitle')}</h2>
|
||||
<p>{tContent('webInstallDesc')}</p>
|
||||
<div className="not-prose my-4 space-y-3">
|
||||
<p className="text-text-primary">{tContent('webInstallStep1')}</p>
|
||||
<p className="text-text-primary">{tContent('webInstallStep2')}</p>
|
||||
<p className="text-text-primary">{tContent('webInstallStep3')}</p>
|
||||
<ul className="list-disc list-inside space-y-2 ms-4">
|
||||
<li className="text-text-secondary">{tContent('webInstallMethodFolder')}</li>
|
||||
<li className="text-text-secondary">{tContent('webInstallMethodZip')}</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Dynamic Skill Discovery */}
|
||||
<hr className="my-8 border-border" />
|
||||
<h2>{tContent('discoveryTitle')}</h2>
|
||||
<p>{tContent('discoveryIntro')}</p>
|
||||
|
||||
<h3>{tContent('discoveryModesTitle')}</h3>
|
||||
<p className="text-text-secondary">{tContent('selectMode')}</p>
|
||||
<DiscoveryModes
|
||||
modes={[
|
||||
{
|
||||
key: 'standard',
|
||||
letter: 'A',
|
||||
title: tContent('modeStandard'),
|
||||
description: tContent('modeStandardDesc'),
|
||||
latency: tContent('modeStandardLatency'),
|
||||
quality: tContent('modeStandardQuality'),
|
||||
bestFor: tContent('modeStandardBestFor'),
|
||||
recommended: true,
|
||||
prompt: prompts.standard,
|
||||
},
|
||||
{
|
||||
key: 'compact',
|
||||
letter: 'B',
|
||||
title: tContent('modeCompact'),
|
||||
description: tContent('modeCompactDesc'),
|
||||
latency: tContent('modeCompactLatency'),
|
||||
quality: tContent('modeCompactQuality'),
|
||||
bestFor: tContent('modeCompactBestFor'),
|
||||
prompt: prompts.compact,
|
||||
},
|
||||
{
|
||||
key: 'oneshot',
|
||||
letter: 'C',
|
||||
title: tContent('modeOneShot'),
|
||||
description: tContent('modeOneShotDesc'),
|
||||
latency: tContent('modeOneShotLatency'),
|
||||
quality: tContent('modeOneShotQuality'),
|
||||
bestFor: tContent('modeOneShotBestFor'),
|
||||
prompt: prompts.oneshot,
|
||||
},
|
||||
]}
|
||||
labels={{
|
||||
latency: tContent('latency'),
|
||||
quality: tContent('qualityBoost'),
|
||||
bestFor: tContent('bestFor'),
|
||||
recommended: tContent('recommended'),
|
||||
copyPrompt: tContent('copyPrompt'),
|
||||
copied: tContent('copied'),
|
||||
selectMode: tContent('selectMode'),
|
||||
addToFile: tContent('addToFile'),
|
||||
}}
|
||||
/>
|
||||
|
||||
<h3>{tContent('crossPlatformTitle')}</h3>
|
||||
<p>{tContent('crossPlatformDesc')}</p>
|
||||
<div className="not-prose my-4 space-y-2" dir="ltr">
|
||||
<div className="glass-card p-3 text-sm font-mono">{tContent('platformClaude')}</div>
|
||||
<div className="glass-card p-3 text-sm font-mono">{tContent('platformCodex')}</div>
|
||||
<div className="glass-card p-3 text-sm font-mono">{tContent('platformCopilot')}</div>
|
||||
<div className="glass-card p-3 text-sm font-mono">{tContent('platformCursor')}</div>
|
||||
<div className="glass-card p-3 text-sm font-mono">{tContent('platformWindsurf')}</div>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
{tContent('fullDocsLink')}{' '}
|
||||
<Link href={`/${locale}/docs/cli`} className="text-primary-600 hover:text-primary-700 no-underline">
|
||||
CLI Reference <ArrowIcon className="inline w-4 h-4" />
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
71
apps/web/app/[locale]/docs/page.tsx
Normal file
71
apps/web/app/[locale]/docs/page.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import { getTranslations, setRequestLocale } from 'next-intl/server';
|
||||
import { Header } from '@/components/Header';
|
||||
import { Footer } from '@/components/Footer';
|
||||
import { BookOpen, Terminal, Code } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
|
||||
export default async function DocsPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ locale: string }>;
|
||||
}) {
|
||||
const { locale } = await params;
|
||||
setRequestLocale(locale);
|
||||
const t = await getTranslations('docs');
|
||||
|
||||
const docs = [
|
||||
{
|
||||
icon: BookOpen,
|
||||
title: t('gettingStarted.title'),
|
||||
description: t('gettingStarted.description'),
|
||||
href: `/${locale}/docs/getting-started`,
|
||||
},
|
||||
{
|
||||
icon: Terminal,
|
||||
title: t('cli.title'),
|
||||
description: t('cli.description'),
|
||||
href: `/${locale}/docs/cli`,
|
||||
},
|
||||
{
|
||||
icon: Code,
|
||||
title: t('api.title'),
|
||||
description: t('api.description'),
|
||||
href: `/${locale}/docs/api`,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col">
|
||||
<Header />
|
||||
<main className="flex-1">
|
||||
<section className="section-header bg-gradient-subtle">
|
||||
<div className="container-main text-center">
|
||||
<h1 className="hero-title mb-4">{t('title')}</h1>
|
||||
<p className="hero-subtitle max-w-2xl mx-auto">{t('subtitle')}</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="section bg-surface">
|
||||
<div className="container-main">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 max-w-4xl mx-auto">
|
||||
{docs.map((doc, index) => (
|
||||
<Link
|
||||
key={index}
|
||||
href={doc.href}
|
||||
className="card p-6 text-center hover:border-primary-500 transition-colors"
|
||||
>
|
||||
<div className="inline-flex items-center justify-center w-14 h-14 rounded-xl bg-primary-50 text-primary-600 mb-4">
|
||||
<doc.icon className="w-7 h-7" />
|
||||
</div>
|
||||
<h3 className="text-xl font-semibold mb-2">{doc.title}</h3>
|
||||
<p className="text-text-secondary">{doc.description}</p>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user