mirror of
https://github.com/block/goose.git
synced 2026-06-02 06:19:33 +02:00
[docs] Add OSS Skills Marketplace (#6752)
This commit is contained in:
@@ -0,0 +1,360 @@
|
||||
/**
|
||||
* 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();
|
||||
@@ -0,0 +1,130 @@
|
||||
/**
|
||||
* Generate ZIP files for skills from Agent-Skills repository
|
||||
*
|
||||
* This script creates ZIP files for each skill in the Agent-Skills repo
|
||||
* and outputs them to static/skills-data-zips/<skillId>.zip
|
||||
*
|
||||
* Note: This script should run AFTER generate-skills-manifest.js
|
||||
* because it relies on the cloned repo being present in .tmp/agent-skills
|
||||
*
|
||||
* Run this before building the documentation site.
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { execSync } = require('child_process');
|
||||
|
||||
// Configuration - must match generate-skills-manifest.js
|
||||
const AGENT_SKILLS_REPO = 'https://github.com/block/Agent-Skills.git';
|
||||
const TEMP_DIR = path.join(__dirname, '..', '.tmp');
|
||||
const CLONED_REPO_DIR = path.join(TEMP_DIR, 'agent-skills');
|
||||
const ZIPS_OUTPUT_DIR = path.join(__dirname, '..', 'static', 'skills-data-zips');
|
||||
|
||||
// Directories to skip when scanning for skills (not skill folders)
|
||||
const SKIP_DIRS = ['.github', 'node_modules', '.git'];
|
||||
|
||||
/**
|
||||
* Clone the Agent-Skills repository if not already present
|
||||
*/
|
||||
function ensureRepoCloned() {
|
||||
if (fs.existsSync(CLONED_REPO_DIR)) {
|
||||
console.log('[generate-skills-zips] Agent-Skills repo already cloned');
|
||||
return true;
|
||||
}
|
||||
|
||||
console.log('[generate-skills-zips] Cloning Agent-Skills repository...');
|
||||
|
||||
// Create temp directory if it doesn't exist
|
||||
if (!fs.existsSync(TEMP_DIR)) {
|
||||
fs.mkdirSync(TEMP_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
try {
|
||||
execSync(`git clone --depth 1 ${AGENT_SKILLS_REPO} ${CLONED_REPO_DIR}`, {
|
||||
stdio: 'pipe',
|
||||
timeout: 60000
|
||||
});
|
||||
console.log('[generate-skills-zips] Successfully cloned Agent-Skills repository');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('[generate-skills-zips] ERROR: Failed to clone Agent-Skills repository');
|
||||
console.error('[generate-skills-zips] Error:', error.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up temporary files
|
||||
*/
|
||||
function cleanup() {
|
||||
if (fs.existsSync(CLONED_REPO_DIR)) {
|
||||
console.log('[generate-skills-zips] Cleaning up temporary files...');
|
||||
fs.rmSync(CLONED_REPO_DIR, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
function generateSkillZips() {
|
||||
console.log('[generate-skills-zips] Starting...');
|
||||
|
||||
// Ensure repo is cloned
|
||||
if (!ensureRepoCloned()) {
|
||||
console.error('[generate-skills-zips] Cannot generate ZIPs without Agent-Skills repo');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Create output directory if it doesn't exist
|
||||
if (!fs.existsSync(ZIPS_OUTPUT_DIR)) {
|
||||
fs.mkdirSync(ZIPS_OUTPUT_DIR, { recursive: true });
|
||||
console.log(`[generate-skills-zips] Created output directory: ${ZIPS_OUTPUT_DIR}`);
|
||||
}
|
||||
|
||||
// Clean existing ZIPs
|
||||
const existingZips = fs.readdirSync(ZIPS_OUTPUT_DIR).filter(f => f.endsWith('.zip'));
|
||||
for (const zip of existingZips) {
|
||||
fs.unlinkSync(path.join(ZIPS_OUTPUT_DIR, zip));
|
||||
}
|
||||
console.log(`[generate-skills-zips] Cleaned ${existingZips.length} existing ZIP files`);
|
||||
|
||||
// Get all skill directories from the cloned repo
|
||||
const entries = fs.readdirSync(CLONED_REPO_DIR, { withFileTypes: true });
|
||||
const skillDirs = entries
|
||||
.filter(d => d.isDirectory() && !SKIP_DIRS.includes(d.name))
|
||||
.map(d => d.name)
|
||||
.filter(name => isSkillDirectory(path.join(CLONED_REPO_DIR, name)));
|
||||
|
||||
let generatedCount = 0;
|
||||
|
||||
for (const skillId of skillDirs) {
|
||||
const skillDir = path.join(CLONED_REPO_DIR, skillId);
|
||||
const zipPath = path.join(ZIPS_OUTPUT_DIR, `${skillId}.zip`);
|
||||
|
||||
try {
|
||||
// Use the system zip command to create the archive
|
||||
// cd into the cloned repo and zip the skill folder to preserve the folder name
|
||||
execSync(`cd "${CLONED_REPO_DIR}" && zip -r "${zipPath}" "${skillId}"`, {
|
||||
stdio: 'pipe'
|
||||
});
|
||||
|
||||
const stats = fs.statSync(zipPath);
|
||||
console.log(`[generate-skills-zips] Created: ${skillId}.zip (${(stats.size / 1024).toFixed(1)} KB)`);
|
||||
generatedCount++;
|
||||
} catch (error) {
|
||||
console.error(`[generate-skills-zips] Error creating ZIP for ${skillId}:`, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[generate-skills-zips] Generated ${generatedCount} ZIP files in ${ZIPS_OUTPUT_DIR}`);
|
||||
|
||||
// Clean up the cloned repo
|
||||
cleanup();
|
||||
}
|
||||
|
||||
generateSkillZips();
|
||||
Reference in New Issue
Block a user