247 lines
6.6 KiB
TypeScript
247 lines
6.6 KiB
TypeScript
import type { Skill, SkillStatus, SkillInstallMethod, SupportingFilesType } from "@site/src/pages/skills/types";
|
|
import siteConfig from "@generated/docusaurus.config";
|
|
|
|
|
|
// Skills data is loaded from a generated JSON manifest at build time
|
|
// Generated at: documentation/static/skills-manifest.json
|
|
|
|
// Cache for loaded skills
|
|
let skillsCache: Skill[] | null = null;
|
|
let skillsPromise: Promise<Skill[]> | null = null;
|
|
|
|
/**
|
|
* Get a skill by its ID
|
|
*/
|
|
export function getSkillById(id: string): Skill | null {
|
|
const allSkills = loadAllSkillsSync();
|
|
return allSkills.find((skill) => skill.id === id) || null;
|
|
}
|
|
|
|
/**
|
|
* Search skills by query string
|
|
* Searches name, description, and tags
|
|
*/
|
|
export async function searchSkills(query: string): Promise<Skill[]> {
|
|
const allSkills = await loadAllSkills();
|
|
if (!query) return allSkills;
|
|
|
|
const lowerQuery = query.toLowerCase();
|
|
return allSkills.filter(
|
|
(skill) =>
|
|
skill.name?.toLowerCase().includes(lowerQuery) ||
|
|
skill.description?.toLowerCase().includes(lowerQuery) ||
|
|
skill.tags?.some((tag) => tag.toLowerCase().includes(lowerQuery))
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Load all skills - async version that fetches from manifest
|
|
*/
|
|
export async function loadAllSkills(): Promise<Skill[]> {
|
|
// Never fetch/cache during SSR (prevents "empty list" getting locked in on preview)
|
|
if (typeof window === "undefined") return [];
|
|
|
|
if (skillsCache) return skillsCache;
|
|
if (skillsPromise) return skillsPromise;
|
|
|
|
skillsPromise = fetchSkillsManifest();
|
|
|
|
const skills = await skillsPromise;
|
|
|
|
// Only cache if we actually got data (avoid caching [] due to a transient 404)
|
|
if (skills.length > 0) skillsCache = skills;
|
|
|
|
return skills;
|
|
}
|
|
|
|
/**
|
|
* Load all skills synchronously (uses cache, returns empty if not loaded)
|
|
*/
|
|
export function loadAllSkillsSync(): Skill[] {
|
|
if (skillsCache) return skillsCache;
|
|
|
|
// Trigger async load on client
|
|
if (typeof window !== "undefined") {
|
|
void loadAllSkills();
|
|
}
|
|
|
|
return [];
|
|
}
|
|
|
|
/**
|
|
* Fetch skills manifest from static files
|
|
*/
|
|
async function fetchSkillsManifest(): Promise<Skill[]> {
|
|
try {
|
|
// In Docusaurus, baseUrl changes automatically for PR previews.
|
|
// Example:
|
|
// prod: /goose/
|
|
// PR preview: /goose/pr-preview/pr-6752/
|
|
const baseUrl = siteConfig.baseUrl.endsWith("/")
|
|
? siteConfig.baseUrl
|
|
: `${siteConfig.baseUrl}/`;
|
|
|
|
const manifestUrl = `${baseUrl}skills-manifest.json`;
|
|
|
|
const response = await fetch(manifestUrl);
|
|
if (!response.ok) {
|
|
console.error("Failed to fetch skills manifest:", response.status, manifestUrl);
|
|
return [];
|
|
}
|
|
|
|
const manifest = await response.json();
|
|
return manifest.skills || [];
|
|
} catch (error) {
|
|
console.error("Error loading skills manifest:", error);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Normalize raw frontmatter-like data to Skill type
|
|
* (kept here in case you reuse it elsewhere)
|
|
*/
|
|
export function normalizeSkill(
|
|
parsed: { frontmatter: Record<string, any>; content: string },
|
|
id: string,
|
|
supportingFiles: string[]
|
|
): Skill {
|
|
const { frontmatter, content } = parsed;
|
|
|
|
const sourceUrl = frontmatter.source_url || frontmatter.sourceUrl;
|
|
const repoUrl = frontmatter.repo_url || frontmatter.repoUrl || sourceUrl;
|
|
const author = frontmatter.author;
|
|
const isCommunity = !!author && author.toLowerCase() !== "goose";
|
|
|
|
const installMethod = determineInstallMethod(sourceUrl, id);
|
|
const installCommand = generateInstallCommand(sourceUrl, id, installMethod);
|
|
const supportingFilesType = determineSupportingFilesType(supportingFiles);
|
|
|
|
return {
|
|
id,
|
|
name: frontmatter.name || id,
|
|
description: frontmatter.description || "No description provided.",
|
|
author,
|
|
version: frontmatter.version,
|
|
status: (frontmatter.status as SkillStatus) || "stable",
|
|
tags: Array.isArray(frontmatter.tags) ? frontmatter.tags : [],
|
|
sourceUrl,
|
|
repoUrl,
|
|
isCommunity,
|
|
content,
|
|
hasSupporting: supportingFiles.length > 0,
|
|
supportingFiles,
|
|
supportingFilesType,
|
|
installMethod,
|
|
installCommand,
|
|
viewSourceUrl: generateViewSourceUrl(id),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Determine the supporting files type based on file contents
|
|
*/
|
|
function determineSupportingFilesType(supportingFiles: string[]): SupportingFilesType {
|
|
if (supportingFiles.length === 0) {
|
|
return 'none';
|
|
}
|
|
|
|
// Executable file extensions
|
|
const executableExtensions = ['.sh', '.bash', '.zsh', '.ps1', '.bat', '.cmd', '.py', '.rb', '.js', '.mjs', '.ts'];
|
|
|
|
// Template-like patterns
|
|
const templatePatterns = [
|
|
/\.template\./i,
|
|
/\.tmpl\./i,
|
|
/\.tpl\./i,
|
|
/template\./i,
|
|
/\.example\./i,
|
|
/\.sample\./i,
|
|
/\.skeleton\./i,
|
|
/\.stub\./i,
|
|
/\.j2$/i,
|
|
/\.jinja2?$/i,
|
|
/\.mustache$/i,
|
|
/\.hbs$/i,
|
|
/\.handlebars$/i,
|
|
/\.ejs$/i,
|
|
/\.erb$/i,
|
|
];
|
|
|
|
const hasExecutable = supportingFiles.some(file => {
|
|
const ext = file.substring(file.lastIndexOf('.')).toLowerCase();
|
|
return executableExtensions.includes(ext);
|
|
});
|
|
|
|
if (hasExecutable) {
|
|
return 'scripts';
|
|
}
|
|
|
|
const hasTemplates = supportingFiles.some(file => {
|
|
return templatePatterns.some(pattern => pattern.test(file));
|
|
});
|
|
|
|
if (hasTemplates) {
|
|
return 'templates';
|
|
}
|
|
|
|
return 'multi-file';
|
|
}
|
|
|
|
/**
|
|
* Determine the install method based on source URL
|
|
*/
|
|
function determineInstallMethod(sourceUrl: string | undefined, skillId: string): SkillInstallMethod {
|
|
if (!sourceUrl) return "download";
|
|
if (sourceUrl.includes("block/goose")) return "npx-multi";
|
|
|
|
const simpleRepoPattern = /^https:\/\/github\.com\/[^\/]+\/[^\/]+\/?$/;
|
|
if (simpleRepoPattern.test(sourceUrl)) return "npx-single";
|
|
|
|
return "npx-multi";
|
|
}
|
|
|
|
/**
|
|
* Generate the install command based on method
|
|
*/
|
|
function generateInstallCommand(
|
|
sourceUrl: string | undefined,
|
|
skillId: string,
|
|
method: SkillInstallMethod
|
|
): string | undefined {
|
|
if (method === "download" || !sourceUrl) return undefined;
|
|
|
|
if (method === "npx-single") {
|
|
const match = sourceUrl.match(/github\.com\/([^\/]+\/[^\/]+)/);
|
|
if (match) return `npx skills add ${match[1]}`;
|
|
}
|
|
|
|
if (method === "npx-multi") {
|
|
return `npx skills add ${sourceUrl} --skill ${skillId}`;
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
/**
|
|
* Generate the view source URL for a skill in the Agent-Skills repo
|
|
*/
|
|
function generateViewSourceUrl(skillId: string): string {
|
|
return `https://github.com/block/Agent-Skills/tree/main/${skillId}`;
|
|
}
|
|
|
|
/**
|
|
* Get all unique tags from all skills (async)
|
|
*/
|
|
export async function getAllTags(): Promise<string[]> {
|
|
const allSkills = await loadAllSkills();
|
|
const tagSet = new Set<string>();
|
|
|
|
allSkills.forEach((skill) => {
|
|
skill.tags.forEach((tag) => tagSet.add(tag));
|
|
});
|
|
|
|
return Array.from(tagSet).sort();
|
|
}
|