Files
goose/documentation/scripts/generate-skills-manifest.js
T
2026-01-28 22:46:18 -05:00

361 lines
11 KiB
JavaScript

/**
* Generate skills manifest from Agent-Skills repository
*
* This script clones the block/Agent-Skills repository and reads all SKILL.md files
* to generate a skills-manifest.json file that the frontend can fetch.
*
* It also supports external skills defined in a local external-skills.json file.
*
* Run this before building the documentation site.
*/
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
const matter = require('gray-matter');
// Configuration
const AGENT_SKILLS_REPO = 'https://github.com/block/Agent-Skills.git';
const AGENT_SKILLS_REPO_URL = 'https://github.com/block/Agent-Skills';
const TEMP_DIR = path.join(__dirname, '..', '.tmp');
const CLONED_REPO_DIR = path.join(TEMP_DIR, 'agent-skills');
const MANIFEST_OUTPUT = path.join(__dirname, '..', 'static', 'skills-manifest.json');
const EXTERNAL_SKILLS_FILE = path.join(__dirname, '..', 'static', 'external-skills.json');
// Directories to skip when scanning for skills (not skill folders)
const SKIP_DIRS = ['.github', 'node_modules', '.git'];
/**
* Clone or update the Agent-Skills repository
*/
function cloneAgentSkillsRepo() {
console.log('[generate-skills-manifest] Fetching Agent-Skills repository...');
// Create temp directory if it doesn't exist
if (!fs.existsSync(TEMP_DIR)) {
fs.mkdirSync(TEMP_DIR, { recursive: true });
}
// Remove existing clone if present
if (fs.existsSync(CLONED_REPO_DIR)) {
console.log('[generate-skills-manifest] Removing existing clone...');
fs.rmSync(CLONED_REPO_DIR, { recursive: true, force: true });
}
// Shallow clone the repository
try {
execSync(`git clone --depth 1 ${AGENT_SKILLS_REPO} ${CLONED_REPO_DIR}`, {
stdio: 'pipe',
timeout: 60000 // 60 second timeout
});
console.log('[generate-skills-manifest] Successfully cloned Agent-Skills repository');
} catch (error) {
console.error('[generate-skills-manifest] ERROR: Failed to clone Agent-Skills repository');
console.error('[generate-skills-manifest] Error:', error.message);
throw new Error('Failed to fetch Agent-Skills repository. Build cannot continue.');
}
}
/**
* Clean up temporary files
*/
function cleanup() {
if (fs.existsSync(CLONED_REPO_DIR)) {
console.log('[generate-skills-manifest] Cleaning up temporary files...');
fs.rmSync(CLONED_REPO_DIR, { recursive: true, force: true });
}
}
/**
* Determine install method based on source configuration
*/
function determineInstallMethod(isExternal, sourceUrl) {
if (isExternal && sourceUrl) {
// External skill with a source URL
const simpleRepoPattern = /^https:\/\/github\.com\/[^\/]+\/[^\/]+\/?$/;
if (simpleRepoPattern.test(sourceUrl)) {
return 'npx-single';
}
return 'npx-multi';
}
// Official skill from Agent-Skills repo
return 'npx-multi';
}
/**
* Generate install command based on method and source
*/
function generateInstallCommand(skillId, isExternal, sourceUrl) {
if (isExternal && sourceUrl) {
const simpleRepoPattern = /^https:\/\/github\.com\/[^\/]+\/[^\/]+\/?$/;
if (simpleRepoPattern.test(sourceUrl)) {
const match = sourceUrl.match(/github\.com\/([^\/]+\/[^\/]+)/);
if (match) {
return `npx skills add ${match[1]}`;
}
}
return `npx skills add ${sourceUrl} --skill ${skillId}`;
}
// Official skill from Agent-Skills repo
return `npx skills add ${AGENT_SKILLS_REPO_URL} --skill ${skillId}`;
}
/**
* Generate view source URL for a skill
*/
function generateViewSourceUrl(skillId, isExternal, sourceUrl) {
if (isExternal && sourceUrl) {
return sourceUrl;
}
// Official skill from Agent-Skills repo
return `${AGENT_SKILLS_REPO_URL}/tree/main/${skillId}`;
}
/**
* Get supporting files in a skill directory (excluding SKILL.md)
*/
function getSupportingFiles(skillDir) {
const files = [];
function walkDir(dir, prefix = '') {
const entries = fs.readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name;
if (entry.isDirectory()) {
walkDir(fullPath, relativePath);
} else if (entry.name !== 'SKILL.md') {
files.push(relativePath);
}
}
}
walkDir(skillDir);
return files;
}
/**
* Determine the supporting files type based on file contents
* Returns: 'scripts' | 'templates' | 'multi-file' | 'none'
*/
function determineSupportingFilesType(supportingFiles) {
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 (file names or extensions)
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 = path.extname(file).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';
}
/**
* Check if a directory contains a SKILL.md file (i.e., is a skill folder)
*/
function isSkillDirectory(dirPath) {
const skillMdPath = path.join(dirPath, 'SKILL.md');
return fs.existsSync(skillMdPath);
}
/**
* Process official skills from the cloned Agent-Skills repo
*/
function processOfficialSkills() {
const skills = [];
// Get all directories in the cloned repo
const entries = fs.readdirSync(CLONED_REPO_DIR, { withFileTypes: true });
for (const entry of entries) {
// Skip non-directories and special directories
if (!entry.isDirectory() || SKIP_DIRS.includes(entry.name)) {
continue;
}
const skillId = entry.name;
const skillDir = path.join(CLONED_REPO_DIR, skillId);
// Skip if not a skill directory (no SKILL.md)
if (!isSkillDirectory(skillDir)) {
continue;
}
const skillMdPath = path.join(skillDir, 'SKILL.md');
try {
const rawContent = fs.readFileSync(skillMdPath, 'utf8');
const parsed = matter(rawContent);
const frontmatter = parsed.data || {};
const content = parsed.content || '';
const supportingFiles = getSupportingFiles(skillDir);
const sourceUrl = frontmatter.source_url || frontmatter.sourceUrl;
const author = frontmatter.author;
const isCommunity = author && author.toLowerCase() !== 'goose';
const supportingFilesType = determineSupportingFilesType(supportingFiles);
const skill = {
id: skillId,
name: frontmatter.name || skillId,
description: frontmatter.description || 'No description provided.',
author,
version: frontmatter.version,
tags: Array.isArray(frontmatter.tags) ? frontmatter.tags : [],
sourceUrl, // Optional external source if skill references another repo
content,
hasSupporting: supportingFiles.length > 0,
supportingFiles,
supportingFilesType,
installMethod: determineInstallMethod(false, sourceUrl),
installCommand: generateInstallCommand(skillId, false, sourceUrl),
viewSourceUrl: generateViewSourceUrl(skillId, false, sourceUrl),
repoUrl: AGENT_SKILLS_REPO_URL,
isCommunity,
};
skills.push(skill);
console.log(`[generate-skills-manifest] Processed official skill: ${skillId}`);
} catch (error) {
console.error(`[generate-skills-manifest] Error processing ${skillId}:`, error.message);
}
}
return skills;
}
/**
* Process external skills from external-skills.json
*/
function processExternalSkills() {
const skills = [];
if (!fs.existsSync(EXTERNAL_SKILLS_FILE)) {
console.log('[generate-skills-manifest] No external-skills.json found, skipping external skills');
return skills;
}
try {
const externalData = JSON.parse(fs.readFileSync(EXTERNAL_SKILLS_FILE, 'utf8'));
const externalSkills = externalData.skills || [];
for (const extSkill of externalSkills) {
const skillId = extSkill.id;
const sourceUrl = extSkill.sourceUrl || extSkill.source_url;
const author = extSkill.author;
const isCommunity = author && author.toLowerCase() !== 'goose';
const skill = {
id: skillId,
name: extSkill.name || skillId,
description: extSkill.description || 'No description provided.',
author,
version: extSkill.version,
tags: Array.isArray(extSkill.tags) ? extSkill.tags : [],
sourceUrl,
content: extSkill.content || '', // External skills may not have content
hasSupporting: false,
supportingFiles: [],
supportingFilesType: 'none',
installMethod: determineInstallMethod(true, sourceUrl),
installCommand: generateInstallCommand(skillId, true, sourceUrl),
viewSourceUrl: generateViewSourceUrl(skillId, true, sourceUrl),
repoUrl: sourceUrl,
isCommunity,
};
skills.push(skill);
console.log(`[generate-skills-manifest] Processed external skill: ${skillId}`);
}
} catch (error) {
console.error('[generate-skills-manifest] Error processing external skills:', error.message);
}
return skills;
}
/**
* Main function to generate the manifest
*/
function generateManifest() {
console.log('[generate-skills-manifest] Starting...');
try {
// Clone the Agent-Skills repository
cloneAgentSkillsRepo();
// Process official skills from the cloned repo
const officialSkills = processOfficialSkills();
// Process external skills from local JSON file
const externalSkills = processExternalSkills();
// Combine all skills
const allSkills = [...officialSkills, ...externalSkills];
// Check if we have any skills
if (allSkills.length === 0) {
console.error('[generate-skills-manifest] ERROR: No skills found. Build cannot continue.');
throw new Error('No skills found in Agent-Skills repository.');
}
// Generate manifest
const manifest = {
skills: allSkills,
generatedAt: new Date().toISOString(),
count: allSkills.length,
officialCount: officialSkills.length,
externalCount: externalSkills.length,
sourceRepo: AGENT_SKILLS_REPO_URL,
};
// Write manifest
fs.writeFileSync(MANIFEST_OUTPUT, JSON.stringify(manifest, null, 2));
console.log(`[generate-skills-manifest] Generated manifest with ${allSkills.length} skills (${officialSkills.length} official, ${externalSkills.length} external): ${MANIFEST_OUTPUT}`);
} finally {
// Always clean up
cleanup();
}
}
// Run the script
generateManifest();