[docs] Add OSS Skills Marketplace (#6752)
This commit is contained in:
@@ -0,0 +1,316 @@
|
||||
import Layout from "@theme/Layout";
|
||||
import { ArrowLeft, Download, Copy, ExternalLink, FileText, Check } from "lucide-react";
|
||||
import { useLocation } from "@docusaurus/router";
|
||||
import { useEffect, useState } from "react";
|
||||
import Link from "@docusaurus/Link";
|
||||
import CodeBlock from "@theme/CodeBlock";
|
||||
import { Button } from "@site/src/components/ui/button";
|
||||
import { getSkillById } from "@site/src/utils/skills";
|
||||
import type { Skill } from "@site/src/pages/skills/types";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
|
||||
type PackageManager = 'npx' | 'pnpm' | 'bun';
|
||||
|
||||
const PACKAGE_MANAGERS: { id: PackageManager; label: string; prefix: string }[] = [
|
||||
{ id: 'npx', label: 'npx', prefix: 'npx' },
|
||||
{ id: 'pnpm', label: 'pnpm', prefix: 'pnpm dlx' },
|
||||
{ id: 'bun', label: 'bun', prefix: 'bunx' },
|
||||
];
|
||||
|
||||
function generateInstallCommand(repoUrl: string, skillId: string, packageManager: PackageManager): string {
|
||||
const prefix = PACKAGE_MANAGERS.find(pm => pm.id === packageManager)?.prefix || 'npx';
|
||||
return `${prefix} skills add ${repoUrl} --skill ${skillId}`;
|
||||
}
|
||||
|
||||
export default function SkillDetailPage(): JSX.Element {
|
||||
const location = useLocation();
|
||||
const [skill, setSkill] = useState<Skill | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [selectedPM, setSelectedPM] = useState<PackageManager>('npx');
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const loadSkill = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
const params = new URLSearchParams(location.search);
|
||||
const id = params.get("id");
|
||||
if (!id) {
|
||||
setError("No skill ID provided");
|
||||
return;
|
||||
}
|
||||
|
||||
const skillData = getSkillById(id);
|
||||
if (skillData) {
|
||||
setSkill(skillData);
|
||||
} else {
|
||||
setError("Skill not found");
|
||||
}
|
||||
} catch (err) {
|
||||
setError("Failed to load skill details");
|
||||
console.error(err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadSkill();
|
||||
}, [location]);
|
||||
|
||||
const handleCopyInstall = () => {
|
||||
if (skill) {
|
||||
const command = generateInstallCommand(skill.repoUrl, skill.id, selectedPM);
|
||||
navigator.clipboard.writeText(command);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDownload = () => {
|
||||
if (skill) {
|
||||
const zipUrl = `/goose/skills-data-zips/${skill.id}.zip`;
|
||||
const link = document.createElement('a');
|
||||
link.href = zipUrl;
|
||||
link.download = `${skill.id}.zip`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Layout>
|
||||
<div className="min-h-screen flex items-start justify-center py-16">
|
||||
<div className="container max-w-5xl mx-auto px-4 animate-pulse">
|
||||
<div className="h-12 w-48 bg-bgSubtle dark:bg-zinc-800 rounded-lg mb-4"></div>
|
||||
<div className="h-6 w-full bg-bgSubtle dark:bg-zinc-800 rounded-lg mb-2"></div>
|
||||
<div className="h-6 w-2/3 bg-bgSubtle dark:bg-zinc-800 rounded-lg"></div>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
if (error || !skill) {
|
||||
return (
|
||||
<Layout>
|
||||
<div className="min-h-screen flex items-start justify-center py-16">
|
||||
<div className="container max-w-5xl mx-auto px-4 text-red-500">
|
||||
{error || "Skill not found"}
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
const currentCommand = generateInstallCommand(skill.repoUrl, skill.id, selectedPM);
|
||||
|
||||
return (
|
||||
<Layout
|
||||
title={skill.name}
|
||||
description={skill.description}
|
||||
>
|
||||
<div className="min-h-screen py-12">
|
||||
<div className="max-w-4xl mx-auto px-4">
|
||||
{/* Header */}
|
||||
<div className="mb-8 flex justify-between items-start">
|
||||
<Link to="/skills">
|
||||
<Button className="flex items-center gap-2">
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
Back to Skills
|
||||
</Button>
|
||||
</Link>
|
||||
{skill.author && (
|
||||
<span className="text-sm text-textSubtle">
|
||||
by {skill.author}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="bg-white dark:bg-[#1A1A1A] border border-borderSubtle dark:border-zinc-700 rounded-xl p-8 shadow-md">
|
||||
{/* Title and badges */}
|
||||
<div className="flex items-start justify-between gap-4 mb-4">
|
||||
<h1 className="text-4xl font-semibold text-textProminent dark:text-white">
|
||||
{skill.name}
|
||||
</h1>
|
||||
<div className="flex gap-2 flex-shrink-0">
|
||||
{skill.isCommunity && (
|
||||
<span className="inline-flex items-center h-7 px-3 rounded-full bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200 text-sm font-medium border border-yellow-200 dark:border-yellow-800">
|
||||
Community
|
||||
</span>
|
||||
)}
|
||||
{skill.version && (
|
||||
<span className="inline-flex items-center h-7 px-3 rounded-full bg-zinc-100 text-zinc-600 dark:bg-zinc-800 dark:text-zinc-400 text-sm font-medium">
|
||||
v{skill.version}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<p className="text-textSubtle dark:text-zinc-400 text-lg mb-6">
|
||||
{skill.description}
|
||||
</p>
|
||||
|
||||
{/* Tags */}
|
||||
{skill.tags.length > 0 && (
|
||||
<div className="mb-6">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{skill.tags.map((tag, index) => (
|
||||
<Link
|
||||
key={index}
|
||||
to={`/skills?tag=${tag}`}
|
||||
className="inline-flex items-center h-7 px-3 rounded-full border border-zinc-300 bg-zinc-100 text-zinc-700 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-300 text-xs font-medium hover:bg-zinc-200 dark:hover:bg-zinc-800 transition-colors no-underline"
|
||||
>
|
||||
{tag}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Install section with tabs */}
|
||||
<div className="mb-6 p-4 bg-zinc-50 dark:bg-zinc-900 rounded-lg border border-zinc-200 dark:border-zinc-700">
|
||||
<h2 className="text-lg font-medium mb-3 text-textProminent dark:text-white flex items-center gap-2">
|
||||
<Download className="h-5 w-5" />
|
||||
Install
|
||||
</h2>
|
||||
|
||||
{/* Package manager tabs */}
|
||||
<div className="flex gap-1 mb-3 border-b border-zinc-200 dark:border-zinc-700">
|
||||
{PACKAGE_MANAGERS.map((pm) => (
|
||||
<button
|
||||
key={pm.id}
|
||||
onClick={() => setSelectedPM(pm.id)}
|
||||
className={`px-4 py-2 text-sm font-medium transition-colors relative ${
|
||||
selectedPM === pm.id
|
||||
? 'text-purple-600 dark:text-purple-400'
|
||||
: 'text-zinc-500 dark:text-zinc-400 hover:text-zinc-700 dark:hover:text-zinc-300'
|
||||
}`}
|
||||
>
|
||||
{pm.label}
|
||||
{selectedPM === pm.id && (
|
||||
<div className="absolute bottom-0 left-0 right-0 h-0.5 bg-purple-600 dark:bg-purple-400" />
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Install command */}
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<code className="flex-1 bg-zinc-200 dark:bg-zinc-800 px-3 py-2 rounded text-sm font-mono text-zinc-800 dark:text-zinc-200 overflow-x-auto">
|
||||
{currentCommand}
|
||||
</code>
|
||||
<Button
|
||||
onClick={handleCopyInstall}
|
||||
className={`flex items-center gap-2 flex-shrink-0 transition-colors ${
|
||||
copied
|
||||
? "bg-green-600 hover:bg-green-700 text-white"
|
||||
: "bg-purple-600 hover:bg-purple-700 text-white"
|
||||
}`}
|
||||
>
|
||||
{copied ? (
|
||||
<>
|
||||
<Check className="h-4 w-4" />
|
||||
Copied!
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Copy className="h-4 w-4" />
|
||||
Copy
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-zinc-500 dark:text-zinc-500">
|
||||
Requires <a href="/docs/guides/using-extensions#goose-skills" className="text-purple-600 hover:underline">Goose Skills extension</a> enabled
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* ZIP Download - secondary option */}
|
||||
<div className="mb-6 flex items-center gap-3 text-sm">
|
||||
<span className="text-zinc-500 dark:text-zinc-400">Prefer manual install?</span>
|
||||
<button
|
||||
onClick={handleDownload}
|
||||
className="inline-flex items-center gap-1.5 text-zinc-600 dark:text-zinc-400 hover:text-purple-600 dark:hover:text-purple-400 transition-colors"
|
||||
>
|
||||
<Download className="h-4 w-4" />
|
||||
Download ZIP
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* View Source - always show, links to Agent-Skills repo */}
|
||||
<div className="mb-6">
|
||||
<a
|
||||
href={skill.viewSourceUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-2 text-purple-600 hover:underline dark:text-purple-400"
|
||||
>
|
||||
<ExternalLink className="h-4 w-4" />
|
||||
View Source on GitHub
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{/* Supporting files */}
|
||||
{skill.hasSupporting && skill.supportingFiles.length > 0 && (
|
||||
<div className="mb-6 border-t border-borderSubtle dark:border-zinc-700 pt-6">
|
||||
<h2 className="text-xl font-medium mb-3 text-textProminent dark:text-white flex items-center gap-2">
|
||||
<FileText className="h-5 w-5" />
|
||||
Supporting Files
|
||||
</h2>
|
||||
<p className="text-sm text-zinc-600 dark:text-zinc-400 mb-3">
|
||||
This skill includes additional files that will be installed with it:
|
||||
</p>
|
||||
<ul className="list-disc list-inside text-sm text-zinc-600 dark:text-zinc-400 space-y-1">
|
||||
{skill.supportingFiles.map((file, index) => (
|
||||
<li key={index}>
|
||||
<code className="bg-zinc-100 dark:bg-zinc-800 px-1 rounded">{file}</code>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Skill content (markdown) */}
|
||||
<div className="border-t border-borderSubtle dark:border-zinc-700 pt-6">
|
||||
<h2 className="text-2xl font-medium mb-4 text-textProminent dark:text-white">
|
||||
Skill Instructions
|
||||
</h2>
|
||||
<div className="prose prose-zinc dark:prose-invert max-w-none">
|
||||
<ReactMarkdown
|
||||
components={{
|
||||
code({ node, inline, className, children, ...props }) {
|
||||
const match = /language-(\w+)/.exec(className || '');
|
||||
if (!inline && match) {
|
||||
return (
|
||||
<CodeBlock language={match[1]}>
|
||||
{String(children).replace(/\n$/, '')}
|
||||
</CodeBlock>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<code className={className} {...props}>
|
||||
{children}
|
||||
</code>
|
||||
);
|
||||
},
|
||||
h1({ children }) {
|
||||
return <h2 className="text-2xl font-semibold mt-6 mb-4">{children}</h2>;
|
||||
},
|
||||
}}
|
||||
>
|
||||
{skill.content}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
import { SkillCard } from "@site/src/components/skill-card";
|
||||
import { searchSkills, getAllTags } from "@site/src/utils/skills";
|
||||
import type { Skill } from "@site/src/pages/skills/types";
|
||||
import { useState, useEffect } from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import Layout from "@theme/Layout";
|
||||
import Admonition from "@theme/Admonition";
|
||||
import { Button } from "@site/src/components/ui/button";
|
||||
import { SidebarFilter, type SidebarFilterGroup } from "@site/src/components/ui/sidebar-filter";
|
||||
import { Menu, X } from "lucide-react";
|
||||
import Link from '@docusaurus/Link';
|
||||
|
||||
export default function SkillsPage() {
|
||||
const [skills, setSkills] = useState<Skill[]>([]);
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [selectedFilters, setSelectedFilters] = useState<Record<string, string[]>>({});
|
||||
const [isMobileFilterOpen, setIsMobileFilterOpen] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const skillsPerPage = 10;
|
||||
|
||||
// Build tag filter options from loaded skills
|
||||
const uniqueTags = Array.from(
|
||||
new Set(
|
||||
skills.flatMap((s) => s.tags || [])
|
||||
)
|
||||
).sort().map((tag) => ({
|
||||
label: tag.charAt(0).toUpperCase() + tag.slice(1),
|
||||
value: tag
|
||||
}));
|
||||
|
||||
// Build source filter options (Community only - official is the default)
|
||||
const sourceOptions = [
|
||||
{ label: "Community", value: "community" }
|
||||
];
|
||||
|
||||
const sidebarFilterGroups: SidebarFilterGroup[] = [
|
||||
{
|
||||
title: "Tags",
|
||||
options: uniqueTags
|
||||
},
|
||||
{
|
||||
title: "Source",
|
||||
options: sourceOptions
|
||||
}
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
const loadSkills = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
const results = await searchSkills(searchQuery);
|
||||
setSkills(results);
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : "Unknown error";
|
||||
setError(`Failed to load skills: ${errorMessage}`);
|
||||
console.error("Error loading skills:", err);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const timeoutId = setTimeout(loadSkills, 300);
|
||||
return () => clearTimeout(timeoutId);
|
||||
}, [searchQuery]);
|
||||
|
||||
// Apply filters
|
||||
let filteredSkills = skills;
|
||||
|
||||
Object.entries(selectedFilters).forEach(([group, values]) => {
|
||||
if (values.length > 0) {
|
||||
filteredSkills = filteredSkills.filter((skill) => {
|
||||
if (group === "Tags") {
|
||||
return skill.tags?.some((tag) => values.includes(tag)) ?? false;
|
||||
}
|
||||
if (group === "Source") {
|
||||
// Use isCommunity field from manifest (true if author is not "goose")
|
||||
const isCommunity = skill.isCommunity ?? false;
|
||||
if (values.includes("community")) return isCommunity;
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<Layout
|
||||
title="Skills Marketplace"
|
||||
description="Browse and install community-contributed skills for goose"
|
||||
>
|
||||
<div className="container mx-auto px-4 py-8 md:p-24">
|
||||
<div className="pb-8 md:pb-16">
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
<h1 className="text-4xl md:text-[64px] font-medium text-textProminent">
|
||||
Skills Marketplace
|
||||
</h1>
|
||||
<Button
|
||||
onClick={() => window.open('https://github.com/block/Agent-Skills?tab=readme-ov-file#contributing-a-skill', '_blank')}
|
||||
className="bg-purple-600 hover:bg-purple-700 text-white flex items-center gap-2 cursor-pointer"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M12 5v14M5 12h14"/>
|
||||
</svg>
|
||||
Submit Skill
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-textProminent">
|
||||
Browse community-contributed{" "}
|
||||
<Link to="/docs/guides/context-engineering/using-skills" className="text-purple-600 hover:underline">
|
||||
skills
|
||||
</Link>{" "}
|
||||
that teach goose how to perform specific tasks. Skills are reusable instruction sets with optional supporting files.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="search-container mb-6 md:mb-8">
|
||||
<input
|
||||
className="bg-bgApp font-light text-textProminent placeholder-textPlaceholder w-full px-3 py-2 md:py-3 text-2xl md:text-[40px] leading-tight md:leading-[52px] border-b border-borderSubtle focus:outline-none focus:ring-purple-500 focus:border-borderProminent caret-[#FF4F00] pl-0"
|
||||
placeholder="Search skills by name, description, or tag"
|
||||
value={searchQuery}
|
||||
onChange={(e) => {
|
||||
setSearchQuery(e.target.value);
|
||||
setCurrentPage(1);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="md:hidden mb-4">
|
||||
<Button onClick={() => setIsMobileFilterOpen(!isMobileFilterOpen)}>
|
||||
{isMobileFilterOpen ? <X size={20} /> : <Menu size={20} />}
|
||||
{isMobileFilterOpen ? "Close Filters" : "Show Filters"}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col md:flex-row gap-8">
|
||||
<div className={`${isMobileFilterOpen ? "block" : "hidden"} md:block md:w-64 mt-6`}>
|
||||
<SidebarFilter
|
||||
groups={sidebarFilterGroups}
|
||||
selectedValues={selectedFilters}
|
||||
onChange={(group, values) => {
|
||||
setSelectedFilters(prev => ({ ...prev, [group]: values }));
|
||||
setCurrentPage(1);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex-1">
|
||||
<div className={`${searchQuery ? "pb-2" : "pb-4 md:pb-8"}`}>
|
||||
<p className="text-gray-600">
|
||||
{searchQuery
|
||||
? `${filteredSkills.length} result${filteredSkills.length !== 1 ? "s" : ""} for "${searchQuery}"`
|
||||
: `${filteredSkills.length} skill${filteredSkills.length !== 1 ? "s" : ""} available`}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<Admonition type="danger" title="Error">
|
||||
<p>{error}</p>
|
||||
</Admonition>
|
||||
)}
|
||||
|
||||
{isLoading ? (
|
||||
<div className="py-8 text-xl text-gray-600">Loading skills...</div>
|
||||
) : filteredSkills.length === 0 ? (
|
||||
<Admonition type="info">
|
||||
<p>
|
||||
{searchQuery
|
||||
? "No skills found matching your search."
|
||||
: "No skills have been submitted yet."}
|
||||
</p>
|
||||
</Admonition>
|
||||
) : (
|
||||
<>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 md:gap-6">
|
||||
{filteredSkills
|
||||
.slice((currentPage - 1) * skillsPerPage, currentPage * skillsPerPage)
|
||||
.map((skill) => (
|
||||
<motion.div
|
||||
key={skill.id}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.6 }}
|
||||
>
|
||||
<SkillCard skill={skill} />
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{filteredSkills.length > skillsPerPage && (
|
||||
<div className="flex justify-center items-center gap-2 md:gap-4 mt-6 md:mt-8">
|
||||
<Button
|
||||
onClick={() => setCurrentPage(prev => Math.max(prev - 1, 1))}
|
||||
disabled={currentPage === 1}
|
||||
className="px-3 md:px-4 py-2 rounded-md border border-border bg-surfaceHighlight hover:bg-surface text-textProminent disabled:opacity-50 disabled:cursor-not-allowed transition-colors text-sm md:text-base"
|
||||
>
|
||||
Previous
|
||||
</Button>
|
||||
|
||||
<span className="text-textProminent text-sm md:text-base">
|
||||
Page {currentPage} of {Math.ceil(filteredSkills.length / skillsPerPage)}
|
||||
</span>
|
||||
|
||||
<Button
|
||||
onClick={() => setCurrentPage(prev => Math.min(Math.ceil(filteredSkills.length / skillsPerPage), prev + 1))}
|
||||
disabled={currentPage >= Math.ceil(filteredSkills.length / skillsPerPage)}
|
||||
className="px-3 md:px-4 py-2 rounded-md border border-border bg-surfaceHighlight hover:bg-surface text-textProminent disabled:opacity-50 disabled:cursor-not-allowed transition-colors text-sm md:text-base"
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
import React from 'react';
|
||||
import Layout from '@docusaurus/theme-classic/lib/theme/Layout';
|
||||
import CodeBlock from '@docusaurus/theme-classic/lib/theme/CodeBlock';
|
||||
|
||||
/**
|
||||
* Skill status indicator
|
||||
*/
|
||||
export type SkillStatus = 'experimental' | 'stable';
|
||||
|
||||
/**
|
||||
* Install method for a skill
|
||||
* - 'npx-single': npx skills add <owner>/<repo>
|
||||
* - 'npx-multi': npx skills add <url> --skill <name>
|
||||
* - 'download': No repo, show download button
|
||||
*/
|
||||
export type SkillInstallMethod = 'npx-single' | 'npx-multi' | 'download';
|
||||
|
||||
/**
|
||||
* Supporting files type - indicates what kind of extra files the skill includes
|
||||
* - 'scripts': Contains executable files (.sh, .py, .js, etc.)
|
||||
* - 'templates': Contains template files (.template., .example., etc.)
|
||||
* - 'multi-file': Contains other supporting files
|
||||
* - 'none': No supporting files
|
||||
*/
|
||||
export type SupportingFilesType = 'scripts' | 'templates' | 'multi-file' | 'none';
|
||||
|
||||
/**
|
||||
* Skill type definition
|
||||
*/
|
||||
export type Skill = {
|
||||
id: string; // Derived from directory name
|
||||
name: string; // From frontmatter (required)
|
||||
description: string; // From frontmatter (required)
|
||||
author?: string; // From frontmatter
|
||||
version?: string; // From frontmatter
|
||||
status: SkillStatus; // From frontmatter (default: 'stable')
|
||||
tags: string[]; // From frontmatter (default: [])
|
||||
sourceUrl?: string; // From frontmatter - optional external source URL
|
||||
content: string; // Markdown content after frontmatter
|
||||
hasSupporting: boolean; // Computed: has files beyond SKILL.md
|
||||
supportingFiles: string[]; // Computed: list of supporting file paths
|
||||
supportingFilesType: SupportingFilesType; // Computed: type of supporting files
|
||||
installMethod: SkillInstallMethod; // Computed based on source
|
||||
installCommand?: string; // Computed: npx command
|
||||
viewSourceUrl: string; // Computed: GitHub link to skill source
|
||||
repoUrl: string; // Repository URL (Agent-Skills for official, sourceUrl for external)
|
||||
isCommunity: boolean; // True if author is not "goose" (community-contributed)
|
||||
};
|
||||
|
||||
/**
|
||||
* Filter group for sidebar
|
||||
*/
|
||||
export type SkillFilterGroup = {
|
||||
title: string;
|
||||
options: { label: string; value: string; count?: number }[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Types documentation page
|
||||
*/
|
||||
const SkillTypes: React.FC = () => {
|
||||
return (
|
||||
<Layout title="Skill Types" description="Type definitions for the Skills Marketplace">
|
||||
<div className="container margin-vert--lg">
|
||||
<h1>Skill Type Definitions</h1>
|
||||
<p>This page contains the type definitions used in the Skills Marketplace.</p>
|
||||
|
||||
<h2>Skill Status</h2>
|
||||
<CodeBlock language="typescript">
|
||||
{`type SkillStatus = 'experimental' | 'stable';`}
|
||||
</CodeBlock>
|
||||
|
||||
<h2>Skill Install Method</h2>
|
||||
<CodeBlock language="typescript">
|
||||
{`// Install method for a skill
|
||||
// - 'npx-single': npx skills add <owner>/<repo>
|
||||
// - 'npx-multi': npx skills add <url> --skill <name>
|
||||
// - 'download': No repo, show download button
|
||||
type SkillInstallMethod = 'npx-single' | 'npx-multi' | 'download';`}
|
||||
</CodeBlock>
|
||||
|
||||
<h2>Skill</h2>
|
||||
<CodeBlock language="typescript">
|
||||
{`type Skill = {
|
||||
id: string; // Derived from directory name
|
||||
name: string; // From frontmatter (required)
|
||||
description: string; // From frontmatter (required)
|
||||
author?: string; // From frontmatter
|
||||
version?: string; // From frontmatter
|
||||
status: SkillStatus; // From frontmatter (default: 'stable')
|
||||
tags: string[]; // From frontmatter (default: [])
|
||||
sourceUrl?: string; // From frontmatter - optional external source URL
|
||||
content: string; // Markdown content after frontmatter
|
||||
hasSupporting: boolean; // Computed: has files beyond SKILL.md
|
||||
supportingFiles: string[]; // Computed: list of supporting file paths
|
||||
installMethod: SkillInstallMethod; // Computed based on source
|
||||
installCommand?: string; // Computed: npx command
|
||||
viewSourceUrl: string; // Computed: GitHub link to skill source
|
||||
repoUrl: string; // Repository URL (Agent-Skills for official, sourceUrl for external)
|
||||
isCommunity: boolean; // True if author is not "goose" (community-contributed)
|
||||
};`}
|
||||
</CodeBlock>
|
||||
|
||||
<h2>SKILL.md Frontmatter Schema</h2>
|
||||
<CodeBlock language="yaml">
|
||||
{`---
|
||||
# Required fields
|
||||
name: string # Skill identifier
|
||||
description: string # Brief description (1-2 sentences)
|
||||
|
||||
# Optional fields
|
||||
author: string # Author name or GitHub handle
|
||||
version: string # Semantic version (e.g., "1.0")
|
||||
status: experimental | stable # Development status (default: stable)
|
||||
tags: # Array of category tags
|
||||
- string
|
||||
source_url: string # GitHub repo URL for npx install
|
||||
---`}
|
||||
</CodeBlock>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default SkillTypes;
|
||||
Reference in New Issue
Block a user