From b90c84dce7c4a56e4d603f920615914697ffc98a Mon Sep 17 00:00:00 2001 From: Jeremias Menichelli Date: Tue, 21 Jul 2026 12:23:25 +0200 Subject: [PATCH] feat: Add ai-skills federated content and data (#48045) --- .../ai-skills/AiSkills.utils.ts | 55 ++----------------- .../ai-skills/AiSkillsIndex.tsx | 54 +++++------------- .../getting-started/ai-skills/CopyButton.tsx | 29 ---------- .../internals/generate-guides-markdown.ts | 2 + .../markdown-schema/AiSkillsIndex.ts | 27 +++++++++ .../fetch-federated-content.ts | 55 ++++++++++++++++++- 6 files changed, 102 insertions(+), 120 deletions(-) delete mode 100644 apps/docs/app/guides/getting-started/ai-skills/CopyButton.tsx create mode 100644 apps/docs/internals/markdown-schema/AiSkillsIndex.ts diff --git a/apps/docs/app/guides/getting-started/ai-skills/AiSkills.utils.ts b/apps/docs/app/guides/getting-started/ai-skills/AiSkills.utils.ts index b6b68853082..b63abea02f3 100644 --- a/apps/docs/app/guides/getting-started/ai-skills/AiSkills.utils.ts +++ b/apps/docs/app/guides/getting-started/ai-skills/AiSkills.utils.ts @@ -1,21 +1,8 @@ -import matter from 'gray-matter' +import { readFile } from 'node:fs/promises' +import { join } from 'node:path' +import { GENERATED_DIRECTORY } from '~/lib/docs' import { cache } from 'react' -import { OCTOKIT_RETRY_OPTIONS, getGitHubFileContents, octokit } from '~/lib/octokit' - -const SKILLS_REPO = { - org: 'supabase', - repo: 'agent-skills', - branch: 'main', - path: 'skills', -} - -interface SkillMetadata { - name?: string - title?: string - description?: string -} - interface SkillSummary { name: string description: string @@ -23,40 +10,8 @@ interface SkillSummary { } async function getAiSkillsImpl(): Promise { - const { data: contents } = await octokit().request('GET /repos/{owner}/{repo}/contents/{path}', { - owner: SKILLS_REPO.org, - repo: SKILLS_REPO.repo, - path: SKILLS_REPO.path, - ref: SKILLS_REPO.branch, - request: OCTOKIT_RETRY_OPTIONS, - }) - - if (!Array.isArray(contents)) { - throw new Error('Expected directory listing from GitHub agent skills repo') - } - - const skillDirs = contents.filter((item) => item.type === 'dir') - - const skills = await Promise.all( - skillDirs.map(async (item) => { - const skillPath = `${SKILLS_REPO.path}/${item.name}/SKILL.md` - const rawContent = await getGitHubFileContents({ - org: SKILLS_REPO.org, - repo: SKILLS_REPO.repo, - branch: SKILLS_REPO.branch, - path: skillPath, - }) - const { data } = matter(rawContent) as { data: SkillMetadata } - - return { - name: item.name, - description: data.description || '', - installCommand: `npx skills add supabase/agent-skills --skill ${item.name}`, - } - }) - ) - - return skills.sort((a, b) => a.name.localeCompare(b.name)) + const raw = await readFile(join(GENERATED_DIRECTORY, 'ai-skills.json'), 'utf-8') + return JSON.parse(raw) } export const getAiSkills = cache(getAiSkillsImpl) diff --git a/apps/docs/app/guides/getting-started/ai-skills/AiSkillsIndex.tsx b/apps/docs/app/guides/getting-started/ai-skills/AiSkillsIndex.tsx index 1f5311a563c..0d04fd700cc 100644 --- a/apps/docs/app/guides/getting-started/ai-skills/AiSkillsIndex.tsx +++ b/apps/docs/app/guides/getting-started/ai-skills/AiSkillsIndex.tsx @@ -1,5 +1,7 @@ +import { CodeBlock } from 'ui-patterns/CodeBlock' +import Heading from 'ui/src/components/CustomHTMLElements/Heading' + import { getAiSkills } from './AiSkills.utils' -import { CopyButton } from './CopyButton' export async function AiSkillsIndex() { let skills: Awaited> = [] @@ -17,44 +19,18 @@ export async function AiSkillsIndex() { ) } + return ( -
- - - - - - - - - - {skills.map((skill) => ( - - - - - - ))} - -
SkillDescriptionInstall command
- - {skill.name} - - {skill.description} -
-
- - - {skill.installCommand} - -
-
-
-
+ <> + {skills.map((skill) => ( +
+ {skill.name} +

{skill.description}

+ + {skill.installCommand} + +
+ ))} + ) } diff --git a/apps/docs/app/guides/getting-started/ai-skills/CopyButton.tsx b/apps/docs/app/guides/getting-started/ai-skills/CopyButton.tsx deleted file mode 100644 index 1f974067655..00000000000 --- a/apps/docs/app/guides/getting-started/ai-skills/CopyButton.tsx +++ /dev/null @@ -1,29 +0,0 @@ -'use client' - -import { useState } from 'react' -import { Check, Copy } from 'lucide-react' -import { cn } from 'ui' - -export function CopyButton({ text }: { text: string }) { - const [copied, setCopied] = useState(false) - - const handleCopy = async () => { - await navigator.clipboard.writeText(text) - setCopied(true) - setTimeout(() => setCopied(false), 2000) - } - - return ( - - ) -} diff --git a/apps/docs/internals/generate-guides-markdown.ts b/apps/docs/internals/generate-guides-markdown.ts index 5e19d7fd2fb..e018300827d 100644 --- a/apps/docs/internals/generate-guides-markdown.ts +++ b/apps/docs/internals/generate-guides-markdown.ts @@ -15,6 +15,7 @@ import { mcpConfigPanelMarkdown as McpConfigPanel } from 'ui-patterns/McpUrlBuil import { addBaseUrlPrefix, getInternalLinkBaseUrl, withDocsBasePath } from './internal-links' import { AccordionItem } from './markdown-schema/Accordion' import { Admonition } from './markdown-schema/Admonition' +import { AiSkillsIndex } from './markdown-schema/AiSkillsIndex' import { AuthProviders } from './markdown-schema/AuthProviders' import { ComputeDiskLimitsTable } from './markdown-schema/ComputeDiskLimitsTable' import { ContentListings } from './markdown-schema/ContentListings' @@ -165,6 +166,7 @@ function applySchema(parent: Parent, schema: ComponentSchema): void { const SCHEMA: ComponentSchema = { AccordionItem, Admonition, + AiSkillsIndex, IconCheck, IconX, Image, diff --git a/apps/docs/internals/markdown-schema/AiSkillsIndex.ts b/apps/docs/internals/markdown-schema/AiSkillsIndex.ts new file mode 100644 index 00000000000..efa3e6174f4 --- /dev/null +++ b/apps/docs/internals/markdown-schema/AiSkillsIndex.ts @@ -0,0 +1,27 @@ +import { readFileSync } from 'node:fs' +import path from 'node:path' + +const SKILLS_PATH = path.join(process.cwd(), 'features/docs/generated/ai-skills.json') + +interface SkillSummary { + name: string + description: string + installCommand: string +} + +export const AiSkillsIndex = (): string => { + const skills: SkillSummary[] = JSON.parse(readFileSync(SKILLS_PATH, 'utf-8')) + + return skills + .map( + (skill) => + `### ${skill.name} + +${skill.description} + +\`\`\`sh +${skill.installCommand} +\`\`\`` + ) + .join('\n\n') +} diff --git a/apps/docs/scripts/federated-content/fetch-federated-content.ts b/apps/docs/scripts/federated-content/fetch-federated-content.ts index 23f19a912c7..b105012765a 100644 --- a/apps/docs/scripts/federated-content/fetch-federated-content.ts +++ b/apps/docs/scripts/federated-content/fetch-federated-content.ts @@ -8,7 +8,7 @@ import { GENERATED_DIRECTORY, GUIDES_DIRECTORY } from '~/lib/docs' import remarkMkDocsAdmonition from '~/lib/mdx/plugins/remarkAdmonition' import { removeTitle } from '~/lib/mdx/plugins/remarkRemoveTitle' import remarkPyMdownTabs from '~/lib/mdx/plugins/remarkTabs' -import { getGitHubFileContents } from '~/lib/octokit' +import { getGitHubFileContents, octokit, OCTOKIT_RETRY_OPTIONS } from '~/lib/octokit' import matter from 'gray-matter' import { fromMarkdown } from 'mdast-util-from-markdown' import { gfmFromMarkdown, gfmToMarkdown } from 'mdast-util-gfm' @@ -163,10 +163,61 @@ async function fetchSource(source: FederatedContentSource): Promise { ]) } +const AI_SKILLS_REPO = { + org: 'supabase', + repo: 'agent-skills', + branch: 'main', + path: 'skills', +} + +/** + * Lists the skill directories in the agent-skills repo, fetches each + * `SKILL.md`'s frontmatter, and writes the summary to + * `features/docs/generated/ai-skills.json` for `AiSkills.utils.ts` to read. + */ +async function fetchAiSkills(): Promise { + const { data: contents } = await octokit().request('GET /repos/{owner}/{repo}/contents/{path}', { + owner: AI_SKILLS_REPO.org, + repo: AI_SKILLS_REPO.repo, + path: AI_SKILLS_REPO.path, + ref: AI_SKILLS_REPO.branch, + request: OCTOKIT_RETRY_OPTIONS, + }) + + if (!Array.isArray(contents)) { + throw new Error('Expected directory listing from GitHub agent skills repo') + } + + const skillDirs = contents.filter((item) => item.type === 'dir') + + const skills = await Promise.all( + skillDirs.map(async (item) => { + const rawContent = await getGitHubFileContents({ + org: AI_SKILLS_REPO.org, + repo: AI_SKILLS_REPO.repo, + branch: AI_SKILLS_REPO.branch, + path: `${AI_SKILLS_REPO.path}/${item.name}/SKILL.md`, + }) + const { data } = matter(rawContent) as { data: { description?: string } } + + return { + name: item.name, + description: data.description || '', + installCommand: `npx skills add supabase/agent-skills --skill ${item.name}`, + } + }) + ) + + skills.sort((a, b) => a.name.localeCompare(b.name)) + + await mkdir(GENERATED_DIRECTORY, { recursive: true }) + await writeFile(join(GENERATED_DIRECTORY, 'ai-skills.json'), JSON.stringify(skills, null, 2)) +} + async function fetchFederatedContent() { const sources = await loadSources() - await Promise.all(sources.map(fetchSource)) + await Promise.all([...sources.map(fetchSource), fetchAiSkills()]) const pageCount = sources.reduce((sum, source) => sum + source.pageMap.length, 0) console.log(